diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 000000000..0a83160bd --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1 @@ +FROM ghcr.io/processone/devcontainer:latest diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..c216cd0c0 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,7 @@ +{ + "name": "ejabberd", + "build": {"dockerfile": "Dockerfile"}, + "extensions": ["erlang-ls.erlang-ls"], + "postCreateCommand": ".devcontainer/prepare-container.sh", + "remoteUser": "vscode" +} diff --git a/.devcontainer/prepare-container.sh b/.devcontainer/prepare-container.sh new file mode 100755 index 000000000..d57a472de --- /dev/null +++ b/.devcontainer/prepare-container.sh @@ -0,0 +1,3 @@ +echo "export PATH=/workspaces/ejabberd/_build/relive:$PATH" >>$HOME/.bashrc +echo "COOKIE" >$HOME/.erlang.cookie +chmod 400 $HOME/.erlang.cookie diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..6abcb6de0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,46 @@ +.git +.win32 +.examples +*.swp +*~ +\#*# +.#* +.edts +*.dump +/Makefile +/config.log +/config.status +/config/releases.exs +/configure +/aclocal.m4 +/*.cache +/deps/ +/.deps-update/ +/ebin/ +/ejabberd.init +/ejabberd.service +/ejabberdctl +/ejabberdctl.example +/rel/ejabberd/ +/rel/overlays/ +/src/eldap_filter_yecc.erl +/vars.config +/dialyzer/ +/test/*.beam +/test/*.ctc +/logs/ +/priv/bin/captcha*sh +/priv/sql +/rel/ejabberd +/_build +/database/ +/.rebar +/rebar.lock +/log/ +Mnesia.nonode@nohost/ +# Binaries created with tools/make-{binaries,installers,packages}: +/ejabberd_*.deb +/ejabberd-*.rpm +/ejabberd-*.run +/ejabberd-*.tar.gz +/.github/container/Dockerfile diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..d984ed09d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,31 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +assignees: '' + +--- + +Before creating a ticket, please consider if this should fit the [discussion forum](https://github.com/processone/ejabberd/discussions) better. + +## Environment + +- ejabberd version: 18.09 +- Erlang version: `erl +V` +- OS: Linux (Debian) +- Installed from: source | distro package | official deb/rpm | official binary installer | other + +## Configuration (only if needed): grep -Ev '^$|^\s*#' ejabberd.yml + +```yaml +loglevel: 4 +... +``` + +## Errors from error.log/crash.log + +No errors + +## Bug description + +Please, give us a precise description (what does not work, what is expected, etc.) diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..0ac588c37 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,26 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: Kind:Feature +assignees: '' + +--- + +Before creating a ticket, please consider if this should fit the [discussion forum](https://github.com/processone/ejabberd/discussions) better. + +**Is your feature request related to a problem? Please describe.** + +A clear and concise description of what the problem is. Ex. I'm always frustrated when... + +**Describe the solution you'd like** + +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** + +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** + +Add any other context or screenshots about the feature request here. diff --git a/.github/PULL_REQUEST_TEMPLATE b/.github/PULL_REQUEST_TEMPLATE new file mode 100644 index 000000000..643d025d0 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE @@ -0,0 +1,20 @@ +We are open to contributions for ejabberd, as GitHub pull requests (PR). +Here are a few points to consider before submitting your PR. (You can +remove the whole text after reading.) + +1. Does this PR address an issue? Please reference it in the PR + description. + +2. Have you properly described the proposed change? + +3. Please make sure the change is atomic and does only touch the needed + modules. If you have other changes/fixes to provide, please submit + them as separate PRs. + +4. If your change or new feature involves storage backends, did you make + sure your change works with all backends? + +5. Do you provide tests? How can we check the behavior of the code? + +6. Did you consider documentation changes in the + processone/docs.ejabberd.im repository? diff --git a/.github/container/Dockerfile b/.github/container/Dockerfile new file mode 100644 index 000000000..1d238339a --- /dev/null +++ b/.github/container/Dockerfile @@ -0,0 +1,206 @@ +#' Define default build variables +ARG OTP_VSN='27.3.4.3' +ARG ELIXIR_VSN='1.18.4' +ARG UID='9000' +ARG USER='ejabberd' +ARG HOME="opt/$USER" +ARG BUILD_DIR="/$USER" +ARG VERSION='master' + +################################################################################ +#' Compile ejabberdapi +FROM docker.io/golang:1.25-alpine AS api +RUN go install -v \ + github.com/processone/ejabberd-api/cmd/ejabberd@master \ + && mv bin/ejabberd bin/ejabberdapi + +################################################################################ +#' build and install ejabberd directly from source +FROM docker.io/erlang:${OTP_VSN}-alpine AS ejabberd + +RUN apk -U add --no-cache \ + autoconf \ + automake \ + bash \ + build-base \ + curl \ + expat-dev \ + file \ + gd-dev \ + git \ + jpeg-dev \ + libpng-dev \ + libwebp-dev \ + linux-pam-dev \ + openssl-dev \ + sqlite-dev \ + yaml-dev \ + zlib-dev + +ARG ELIXIR_VSN +RUN wget -O - https://github.com/elixir-lang/elixir/archive/v$ELIXIR_VSN.tar.gz \ + | tar -xzf - + +WORKDIR /elixir-$ELIXIR_VSN +ENV ERL_FLAGS="+JPperf true" +RUN make install clean + +RUN mix local.hex --force \ + && mix local.rebar --force + +ARG BUILD_DIR +COPY / $BUILD_DIR/ + +WORKDIR $BUILD_DIR +RUN mv .github/container/ejabberdctl.template . \ + && mv .github/container/ejabberd.yml.example . \ + && ./autogen.sh \ + && ./configure --with-rebar=mix --enable-all \ + && make deps \ + && make rel + +WORKDIR /rootfs +ARG VERSION +ARG HOME +RUN mkdir -p $HOME $HOME-$VERSION \ + && cp -r $BUILD_DIR/_build/prod/rel/ejabberd/* $HOME-$VERSION \ + && mv $HOME-$VERSION/conf $HOME/conf + +RUN cp -p $BUILD_DIR/tools/captcha*.sh $HOME-$VERSION/lib + +RUN find "$HOME-$VERSION/bin" -name 'ejabberd' -delete \ + && find "$HOME-$VERSION/releases" -name 'COOKIE' -delete + +RUN wget -O "$HOME/conf/cacert.pem" 'https://curl.se/ca/cacert.pem' + +#' Prepare ejabberd for runtime +RUN apk -U add --no-cache \ + git \ + libcap \ + openssl + +RUN mkdir -p usr/local/bin $HOME/conf $HOME/database $HOME/logs $HOME/upload + +COPY --from=api /go/bin/ejabberdapi usr/local/bin/ + +RUN if [ ! -d $HOME/.ejabberd-modules ]; \ + then \ + if [ -d $BUILD_DIR/.ejabberd-modules ]; \ + then cp -r $BUILD_DIR/.ejabberd-modules $HOME; \ + else git clone https://github.com/processone/ejabberd-contrib --depth 1 \ + $HOME/.ejabberd-modules/sources/ejabberd-contrib; \ + fi \ + fi + +RUN export PEM=$HOME/conf/server.pem \ + && openssl req -x509 \ + -batch \ + -nodes \ + -newkey rsa:4096 \ + -keyout $PEM \ + -out $PEM \ + -days 3650 \ + -subj "/CN=localhost" + +RUN sed -i 's|^#CTL_OVER_HTTP=|CTL_OVER_HTTP=../|' "$HOME/conf/ejabberdctl.cfg" + +RUN home_root_dir=$(echo $HOME | sed 's|\(.*\)/.*|\1 |') \ + && setcap 'cap_net_bind_service=+ep' $(find $home_root_dir -name beam.smp) \ + && echo -e \ + "#!/bin/sh \ + \n[ -z \$ERLANG_NODE_ARG ] && export ERLANG_NODE_ARG=ejabberd@localhost \ + \nexport EMA=\"\$EJABBERD_MACRO_ADMIN\" \ + \nexport HOST=\"\${EJABBERD_MACRO_HOST:-localhost}\" \ + \nif [ -n \"\$EMA\" ] \ + \nthen \ + \n if [ \"\$EMA\" != \"\${EMA%%@*}\" ] \ + \n then \ + \n export USERNAME=\"\${EMA%%@*}\" \ + \n export HOST=\"\${EMA##*@}\" \ + \n else \ + \n export USERNAME=\"\$EMA\" \ + \n export SHOW_WARNING=\"true\" \ + \n fi \ + \nelif [ -n \"\$REGISTER_ADMIN_PASSWORD\" ] \ + \nthen \ + \n export USERNAME=\"admin\" \ + \nelse \ + \n export USERNAME=\"\$(od -A n -N 8 -t x8 /dev/urandom)\" \ + \nfi \ + \nexport EJABBERD_MACRO_ADMIN=\"\$USERNAME@\$HOST\" \ + \n[ -n \"\$SHOW_WARNING\" ] && echo \"WARNING: The EJABBERD_MACRO_ADMIN environment variable was set to '\$EMA', but it should include the host... I'll overwrite it to become '\$EJABBERD_MACRO_ADMIN'.\" \ + \n[ -n \"\$CTL_ON_CREATE\" ] && export SEPARATOR=\";\" \ + \n[ -n \"\$REGISTER_ADMIN_PASSWORD\" ] && export CTL_ON_CREATE=\"register \${EJABBERD_MACRO_ADMIN%%@*} \${EJABBERD_MACRO_ADMIN##*@} \$REGISTER_ADMIN_PASSWORD \$SEPARATOR \$CTL_ON_CREATE\" \ + \nexport CONFIG_DIR=/$HOME/conf \ + \nexport LOGS_DIR=/$HOME/logs \ + \nexport SPOOL_DIR=/$HOME/database \ + \nexec /$(find $home_root_dir -name ejabberdctl) \"\$@\"" \ + > usr/local/bin/ejabberdctl \ + && chmod +x usr/local/bin/* \ + && scanelf --needed --nobanner --format '%n#p' --recursive $home_root_dir \ + | tr ',' '\n' \ + | sort -u \ + | awk 'system("[ -e $home_root_dir" $1 " ]") == 0 { next } { print "so:" $1 }' \ + | sed -e "s|so:libc.so|so:libc.musl-$(uname -m).so.1|" \ + > /tmp/runDeps + +ARG UID +RUN chown -R $UID:$UID $HOME + +RUN cp /rootfs/$HOME-$VERSION/lib/captcha*.sh usr/local/bin/ +RUN mkdir $HOME/sql \ + && find /rootfs/$HOME-$VERSION/lib/ -name *.sql -exec cp {} $HOME/sql \; -exec cp {} $HOME/database \; + +################################################################################ +#' Remove erlang/OTP & rebar3 +FROM docker.io/erlang:${OTP_VSN}-alpine AS runtime +RUN apk del .erlang-rundeps \ + && rm -f $(which rebar3) \ + && find /usr -type d -name 'erlang' -exec rm -rf {} + \ + && find /usr -type l -exec test ! -e {} \; -delete + +#' Update alpine, finalize runtime environment +COPY --from=ejabberd /tmp/runDeps /tmp/runDeps +RUN apk -U upgrade --available --no-cache \ + && apk add --no-cache \ + $(cat /tmp/runDeps) \ + so:libcap.so.2 \ + so:libtdsodbc.so.0 \ + curl \ + tini \ + && rm /tmp/runDeps \ + && ln -fs /usr/lib/libtdsodbc.so.0 /usr/lib/libtdsodbc.so + +ARG USER +ARG UID +ARG HOME +RUN addgroup $USER -g $UID \ + && adduser -s /sbin/nologin -D -u $UID -h /$HOME -G $USER $USER + +RUN ln -fs /usr/local/bin/ /opt/ejabberd/bin +RUN rm -rf /home \ + && ln -fs /opt /home + +################################################################################ +#' Build together production image +FROM scratch +ARG USER +ARG HOME + +COPY --from=runtime / / +COPY --from=ejabberd /rootfs / + +HEALTHCHECK \ + --interval=1m \ + --timeout=5s \ + --start-period=5s \ + --retries=10 \ + CMD ejabberdctl status + +WORKDIR /$HOME +USER $USER +VOLUME ["/$HOME"] +EXPOSE 1880 1883 4369-4399 5210 5222 5269 5280 5443 + +ENTRYPOINT ["/sbin/tini","--","ejabberdctl"] +CMD ["foreground"] diff --git a/.github/container/ejabberd-container-install.bat b/.github/container/ejabberd-container-install.bat new file mode 100755 index 000000000..704011a6d --- /dev/null +++ b/.github/container/ejabberd-container-install.bat @@ -0,0 +1,294 @@ +@echo off + +:: +:: ejabberd container installer for Windows +:: ------------------------------------- +:: v0.4 +:: +:: This batch script downloads an ejabberd container image +:: and setups a docker container to run ejabberd. + +:: +:: 1. Download and install Docker: +:: +:: If you use Windows 10, download Docker Desktop from: +:: https://www.docker.com/ +:: +:: If you use Windows 7 or 8, download Docker Toolbox from: +:: https://github.com/docker/toolbox/releases +:: After installation, run Docker Quickstart Installer +:: + +:: +:: 2. Edit those options: + +:: Directory where your ejabberd deployment files will be installed +:: (configuration, database, logs, ...) +:: +:: In Windows 10 you can configure the path: + +set INSTALL_DIR_WINDOWS10=C:\ejabberd + +:: In older Windows, not configurable, it will be installed in: +:: C:\Users\%USERNAME%\ejabberd + +:: Please enter the desired ejabberd domain name. +:: The domain is the visible attribute that is added to the username +:: to form the Jabber Identifier (for example: user@example.net). +:: This computer must be known on the network with this address name. +:: You can later add more in conf/ejabberd.yml + +set HOST=localhost + +:: Please enter the administrator username for the current +:: ejabberd installation. A Jabber account with this username +:: will be created and granted administrative privileges. +:: Don't use blankspaces in the username. + +set USER=admin + +:: Please provide a password for that new administrator account + +set PASSWORD= + +:: By default this downloads 'latest' ejabberd version, +:: but you can set a specific version, for example '22.05' +:: or the bleeding edge 'master'. See available tags in +:: https://github.com/processone/ejabberd/pkgs/container/ejabberd + +set VERSION=latest + +:: This tells docker what ports ejabberd will use. +:: You can later configure them in conf/ejabberd.yml + +set PORTS=5180 5222 5269 5443 + +:: +:: 3. Now save this script and run it. +:: + +:: +:: 4. When installation is completed: +:: +:: If using Windows 10, open Docker Desktop and you can: +:: +:: - (>) START the ejabberd container +:: - Enter WebAdmin: click the ([->]) OPEN IN BROWSER button +:: - To try ejabberdctl, click the (>_) CLI button, then: ejabberdctl +:: - ([]) STOP the ejabberd container +:: +:: If using an old Windows, open Kitematic and you can: +:: +:: - START the ejabberd container +:: - Open your configuration, logs, ... in Settings > Volumes +:: - Enter WebAdmin in Settings > Hostname/Ports > click on the 5180 port +:: - Try ejabberdctl in EXEC, then: ejabberdctl +:: - STOP the ejabberd container +:: +:: You can delete the container and create it again running this script, +:: the configuration and database are maintained. +:: + +::=============================================================== +:: Check Windows version +:: +::=============================================================== + +set INSTALL_DIR_DOCKER=c/Users/%USERNAME%/ejabberd + +for /f "tokens=4-5 delims=. " %%i in ('ver') do set WVERSION=%%i.%%j +if "%wversion%" == "10.0" ( + echo === Preparing paths to install in Windows 10... + set INSTALL_DIR=%INSTALL_DIR_WINDOWS10% + set VC=-v %INSTALL_DIR_WINDOWS10%\conf:/opt/ejabberd/conf + set VD=-v %INSTALL_DIR_WINDOWS10%\database:/opt/ejabberd/database + set VL=-v %INSTALL_DIR_WINDOWS10%\logs:/opt/ejabberd/logs + set VM=-v %INSTALL_DIR_WINDOWS10%\ejabberd-modules:/opt/ejabberd/.ejabberd-modules + set DOCKERDOWNLOAD="First download and install Docker Desktop from https://www.docker.com/" +) else ( + echo === Preparing paths to install in Windows older than 10... + set INSTALL_DIR=C:\Users\%USERNAME%\ejabberd + set VC=-v "/%INSTALL_DIR_DOCKER%/conf:/opt/ejabberd/conf" + set VD=-v "/%INSTALL_DIR_DOCKER%/database:/opt/ejabberd/database" + set VL=-v "/%INSTALL_DIR_DOCKER%/logs:/opt/ejabberd/logs" + set VM=-v "/%INSTALL_DIR_DOCKER%/ejabberd-modules:/opt/ejabberd/.ejabberd-modules" + set DOCKERDOWNLOAD="First download and install Docker Toolbox from https://github.com/docker/toolbox/releases" +) +set VOLUMES=%VC% %VD% %VL% %VM% + +::=============================================================== +:: Check docker is installed +:: +::=============================================================== + +docker version >NUL +if %ERRORLEVEL% NEQ 0 ( + echo. + echo === ERROR: It seems docker is not installed!!! + echo. + echo %DOCKERDOWNLOAD% + echo === Then try to run this script again. + echo. + pause + exit 1 +) + +::=============================================================== +:: Check install options are correctly set +:: +::=============================================================== + +if [%PASSWORD%]==[] ( + echo. + echo === ERROR: PASSWORD not set!!! + echo. + echo === Please edit this script and set the PASSWORD. + echo === Then try to run this script again. + echo. + pause + exit 1 +) + +::=============================================================== +:: Download Docker image +:: +::=============================================================== + +set IMAGE=ghcr.io/processone/ejabberd:%VERSION% + +echo. +echo === Checking if the '%IMAGE%' container image was already downloaded... +docker image history %IMAGE% >NUL +if %ERRORLEVEL% NEQ 0 ( + echo === The '%IMAGE%' container image was not downloaded yet. + echo. + echo === Downloading the '%IMAGE%' container image, please wait... + docker pull %IMAGE% +) else ( + echo === The '%IMAGE%' container image was already downloaded. +) + +::=============================================================== +:: Create preliminary container +:: +::=============================================================== + +echo. +echo === Checking if the 'ejabberd' container already exists... +docker container logs ejabberd +if %ERRORLEVEL% EQU 0 ( + echo. + echo === The 'ejabberd' container already exists. + echo === Nothing to do, so installation finishes now. + echo === You can go to Docker Desktop and start the 'ejabberd' container. + echo. + pause + exit 1 +) else ( + echo === The 'ejabberd' container doesn't yet exist, + echo === so let's continue the installation process. +) + +echo. +if exist %INSTALL_DIR% ( + echo === The INSTALL_DIR %INSTALL_DIR% already exists. + echo === No need to create the preliminary 'ejabberd-pre' image. +) else ( + echo === The INSTALL_DIR %INSTALL_DIR% doesn't exist. + echo === Let's create the preliminary 'ejabberd-pre' image. + CALL :create-ejabberd-pre +) + +::=============================================================== +:: Create final container +:: +::=============================================================== + +echo. +echo === Creating the final 'ejabberd' container using %IMAGE% image... + +setlocal EnableDelayedExpansion +set PS= +for %%a in (%PORTS%) do ( + set PS=!PS! -p %%a:%%a +) + +docker create --name ejabberd --hostname localhost %PS% %VOLUMES% %IMAGE% + +echo. +echo === Installation completed. +echo. +pause + +EXIT /B %ERRORLEVEL% + +::=============================================================== +:: Function to create preliminary container +:: +::=============================================================== + +:create-ejabberd-pre + +echo. +echo === Creating a preliminary 'ejabberd-pre' container using %IMAGE% image... +docker create --name ejabberd-pre --hostname localhost %IMAGE% + +echo. +echo === Now 'ejabberd-pre' will be started. +docker container start ejabberd-pre + +echo. +echo === Waiting ejabberd to be running... +set /A timeout = 10 +set status=4 +goto :while + +:statusstart +docker exec -it ejabberd-pre ejabberdctl status +goto :statusend + +:while +if %status% GTR 0 ( + echo. + timeout /t 1 /nobreak >NUL + set /A timeout = timeout - 1 + if %timeout% EQU 0 ( + set status=-1 + ) else ( + goto :statusstart + :statusend + set status=%ERRORLEVEL% + ) + goto :while +) + +echo. +echo === Setting a few options... +docker exec -it ejabberd-pre sed -i "s!- localhost!- %HOST%!g" conf/ejabberd.yml +docker exec -it ejabberd-pre sed -i "s!^acl:!acl:\n admin:\n user:\n - \"%USER%@%HOST%\"!g" conf/ejabberd.yml +docker exec -it ejabberd-pre sed -i "s!5280!5180!g" conf/ejabberd.yml +docker exec -it ejabberd-pre sed -i "s!/admin!/!g" conf/ejabberd.yml +docker exec -it ejabberd-pre ejabberdctl reload_config + +echo. +echo === Registering the administrator account... +docker exec -it ejabberd-pre ejabberdctl register %USER% %HOST% %PASSWORD% +docker exec -it ejabberd-pre ejabberdctl stop + +echo. +echo === Copying conf, database, logs... +mkdir %INSTALL_DIR% +mkdir %INSTALL_DIR%\conf +mkdir %INSTALL_DIR%\database +mkdir %INSTALL_DIR%\logs +mkdir %INSTALL_DIR%\ejabberd-modules +docker cp ejabberd-pre:/opt/ejabberd/conf/ %INSTALL_DIR% +docker cp ejabberd-pre:/opt/ejabberd/database/ %INSTALL_DIR% +docker cp ejabberd-pre:/opt/ejabberd/logs/ %INSTALL_DIR% + +echo. +echo === Deleting the preliminary 'ejabberd-pre' container... +docker stop ejabberd-pre +docker rm ejabberd-pre + +EXIT /B 0 diff --git a/.github/container/ejabberd.yml.example b/.github/container/ejabberd.yml.example new file mode 100644 index 000000000..2f63a2b64 --- /dev/null +++ b/.github/container/ejabberd.yml.example @@ -0,0 +1,278 @@ +### +### ejabberd configuration file +### +### The parameters used in this configuration file are explained at +### +### https://docs.ejabberd.im/admin/configuration +### +### The configuration file is written in YAML. +### ******************************************************* +### ******* !!! WARNING !!! ******* +### ******* YAML IS INDENTATION SENSITIVE ******* +### ******* MAKE SURE YOU INDENT SECTIONS CORRECTLY ******* +### ******************************************************* +### Refer to http://en.wikipedia.org/wiki/YAML for the brief description. +### + +define_macro: + HOST: localhost + ## ADMIN: ... # set by /usr/local/bin/ejabberdctl + PORT_C2S: 5222 + PORT_C2S_TLS: 5223 + PORT_S2S: 5269 + PORT_HTTP_TLS: 5443 + PORT_HTTP: 5280 + PORT_BROWSER: 1880 + PORT_STUN: 5478 + PORT_MQTT: 1883 + PORT_PROXY65: 7777 + +hosts: + - HOST + +loglevel: info + +## If you already have certificates, list them here +# certfiles: +# - /etc/letsencrypt/live/domain.tld/fullchain.pem +# - /etc/letsencrypt/live/domain.tld/privkey.pem + +ca_file: /opt/ejabberd/conf/cacert.pem +certfiles: + - /opt/ejabberd/conf/server.pem + +listen: + - + port: PORT_C2S + ip: "::" + module: ejabberd_c2s + max_stanza_size: 262144 + shaper: c2s_shaper + access: c2s + starttls_required: true + - + port: PORT_C2S_TLS + ip: "::" + module: ejabberd_c2s + max_stanza_size: 262144 + shaper: c2s_shaper + access: c2s + tls: true + - + port: PORT_S2S + ip: "::" + module: ejabberd_s2s_in + max_stanza_size: 524288 + shaper: s2s_shaper + - + port: PORT_HTTP_TLS + ip: "::" + module: ejabberd_http + tls: true + request_handlers: + /admin: ejabberd_web_admin + /api: mod_http_api + /bosh: mod_bosh + /captcha: ejabberd_captcha + /upload: mod_http_upload + /ws: ejabberd_http_ws + - + port: PORT_HTTP + ip: "::" + module: ejabberd_http + request_handlers: + /admin: ejabberd_web_admin + /.well-known/acme-challenge: ejabberd_acme + - + port: PORT_BROWSER + ip: "::" + module: ejabberd_http + request_handlers: + /: ejabberd_web_admin + - + port: "unix:../sockets/ctl_over_http.sock" + module: ejabberd_http + unix_socket: + mode: '0600' + request_handlers: + /ctl: ejabberd_ctl + - + port: PORT_STUN + ip: "::" + transport: udp + module: ejabberd_stun + use_turn: true + ## The server's public IPv4 address: + # turn_ipv4_address: "203.0.113.3" + ## The server's public IPv6 address: + # turn_ipv6_address: "2001:db8::3" + - + port: PORT_MQTT + ip: "::" + module: mod_mqtt + backlog: 1000 + +s2s_use_starttls: optional + +acl: + local: + user_regexp: "" + loopback: + ip: + - 127.0.0.0/8 + - ::1/128 + admin: + user: + - ADMIN + +access_rules: + local: + allow: local + c2s: + deny: blocked + allow: all + announce: + allow: admin + configure: + allow: admin + muc_create: + allow: local + pubsub_createnode: + allow: local + trusted_network: + allow: loopback + +api_permissions: + "console commands": + from: ejabberd_ctl + who: all + what: "*" + "webadmin commands": + from: ejabberd_web_admin + who: admin + what: "*" + "admin access": + who: + access: + allow: + - acl: loopback + - acl: admin + oauth: + scope: "ejabberd:admin" + access: + allow: + - acl: loopback + - acl: admin + what: + - "*" + - "!stop" + - "!start" + "public commands": + who: + ip: 127.0.0.1/8 + what: + - status + - connected_users_number + +shaper: + normal: + rate: 3000 + burst_size: 20000 + fast: 100000 + +shaper_rules: + max_user_sessions: 10 + max_user_offline_messages: + 5000: admin + 100: all + c2s_shaper: + none: admin + normal: all + s2s_shaper: fast + +modules: + mod_adhoc: {} + mod_admin_extra: {} + mod_announce: + access: announce + mod_avatar: {} + mod_blocking: {} + mod_bosh: {} + mod_caps: {} + mod_carboncopy: {} + mod_client_state: {} + mod_configure: {} + mod_disco: {} + mod_fail2ban: {} + mod_http_api: {} + mod_http_upload: + put_url: https://@HOST_URL_ENCODE@:5443/upload + custom_headers: + "Access-Control-Allow-Origin": "https://@HOST@" + "Access-Control-Allow-Methods": "GET,HEAD,PUT,OPTIONS" + "Access-Control-Allow-Headers": "Content-Type" + mod_last: {} + mod_mam: + ## Mnesia is limited to 2GB, better to use an SQL backend + ## For small servers SQLite is a good fit and is very easy + ## to configure. Uncomment this when you have SQL configured: + ## db_type: sql + assume_mam_usage: true + default: always + mod_mqtt: {} + mod_muc: + access: + - allow + access_admin: + - allow: admin + access_create: muc_create + access_persistent: muc_create + access_mam: + - allow + default_room_options: + mam: true + mod_muc_admin: {} + mod_offline: + access_max_user_messages: max_user_offline_messages + mod_ping: {} + mod_privacy: {} + mod_private: {} + mod_proxy65: + access: local + max_connections: 5 + port: PORT_PROXY65 + mod_pubsub: + access_createnode: pubsub_createnode + plugins: + - flat + - pep + force_node_config: + ## Avoid buggy clients to make their bookmarks public + storage:bookmarks: + access_model: whitelist + mod_push: {} + mod_push_keepalive: {} + mod_register: + ## Only accept registration requests from the "trusted" + ## network (see access_rules section above). + ## Think twice before enabling registration from any + ## address. See the Jabber SPAM Manifesto for details: + ## https://github.com/ge0rg/jabber-spam-fighting-manifesto + ip_access: trusted_network + mod_roster: + versioning: true + mod_s2s_bidi: {} + mod_s2s_dialback: {} + mod_shared_roster: {} + mod_stream_mgmt: + resend_on_timeout: if_offline + mod_stun_disco: {} + mod_vcard: {} + mod_vcard_xupdate: {} + mod_version: + show_os: false + +### Local Variables: +### mode: yaml +### End: +### vim: set filetype=yaml tabstop=8 diff --git a/.github/container/ejabberdctl.template b/.github/container/ejabberdctl.template new file mode 100755 index 000000000..b1f1d2179 --- /dev/null +++ b/.github/container/ejabberdctl.template @@ -0,0 +1,627 @@ +#!/bin/sh + +# define default configuration +POLL=true +ERL_MAX_PORTS=32000 +ERL_PROCESSES=250000 +ERL_MAX_ETS_TABLES=1400 +FIREWALL_WINDOW="" +INET_DIST_INTERFACE="" +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}}" +EPMD="{{epmd}}" +IEX="{{iexpath}}" +COOKIE_FILE="$HOME"/.erlang.cookie +[ -n "$ERLANG_COOKIE" ] && [ ! -f "$COOKIE_FILE" ] && echo "$ERLANG_COOKIE" > "$COOKIE_FILE" && chmod 400 "$COOKIE_FILE" + +# check the proper system user is used +case $(id -un) in + "$INSTALLUSER") + EXEC_CMD="as_current_user" + ;; + root) + if [ -n "$INSTALLUSER" ] ; then + EXEC_CMD="as_install_user" + else + EXEC_CMD="as_current_user" + echo "WARNING: It is not recommended to run ejabberd as root" >&2 + fi + ;; + *) + if [ -n "$INSTALLUSER" ] ; then + echo "ERROR: This command can only be run by root or the user $INSTALLUSER" >&2 + exit 7 + else + EXEC_CMD="as_current_user" + fi + ;; +esac + +# parse command line parameters +while [ $# -gt 0 ]; do + case $1 in + -n|--node) ERLANG_NODE_ARG=$2; shift 2;; + -s|--spool) SPOOL_DIR=$2; shift 2;; + -l|--logs) LOGS_DIR=$2; shift 2;; + -f|--config) EJABBERD_CONFIG_PATH=$2; shift 2;; + -c|--ctl-config) EJABBERDCTL_CONFIG_PATH=$2; shift 2;; + -d|--config-dir) CONFIG_DIR=$2; shift 2;; + -t|--no-timeout) NO_TIMEOUT="--no-timeout"; shift;; + *) break;; + esac +done + +# define ejabberd variables if not already defined from the command line +: "${CONFIG_DIR:="{{config_dir}}"}" +: "${LOGS_DIR:="{{logs_dir}}"}" +: "${EJABBERD_CONFIG_PATH:="$CONFIG_DIR/ejabberd.yml"}" +: "${EJABBERDCTL_CONFIG_PATH:="$CONFIG_DIR/ejabberdctl.cfg"}" +# Allows passing extra Erlang command-line arguments in vm.args file +: "${VMARGS:="$CONFIG_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" +: "${SPOOL_DIR:="{{spool_dir}}"}" +: "${EJABBERD_LOG_PATH:="$LOGS_DIR/ejabberd.log"}" + +# backward support for old mnesia spool dir path +: "${SPOOL_DIR_OLD:="$SPOOL_DIR/$ERLANG_NODE"}" +[ -r "$SPOOL_DIR_OLD/schema.DAT" ] && [ ! -r "$SPOOL_DIR/schema.DAT" ] && SPOOL_DIR="$SPOOL_DIR_OLD" + +# define erl parameters +ERLANG_OPTS="+K $POLL +P $ERL_PROCESSES $ERL_OPTIONS" +if [ -n "$FIREWALL_WINDOW" ] ; then + ERLANG_OPTS="$ERLANG_OPTS -kernel inet_dist_listen_min ${FIREWALL_WINDOW%-*} inet_dist_listen_max ${FIREWALL_WINDOW#*-}" +fi +if [ -n "$INET_DIST_INTERFACE" ] ; then + INET_DIST_INTERFACE2=$("$ERL" $ERLANG_OPTS -noshell -eval 'case inet:parse_address("'$INET_DIST_INTERFACE'") of {ok,IP} -> io:format("~p",[IP]); _ -> ok end.' -s erlang halt) + if [ -n "$INET_DIST_INTERFACE2" ] ; then + if [ "$(echo "$INET_DIST_INTERFACE2" | grep -o "," | wc -l)" -eq 7 ] ; then + INET_DIST_INTERFACE2="$INET_DIST_INTERFACE2 -proto_dist inet6_tcp" + fi + ERLANG_OPTS="$ERLANG_OPTS -kernel inet_dist_use_interface $INET_DIST_INTERFACE2" + fi +fi +[ -n "$ERL_DIST_PORT" ] && ERLANG_OPTS="$ERLANG_OPTS -erl_epmd_port $ERL_DIST_PORT -start_epmd false" +# 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_CRASH_DUMP="$LOGS_DIR"/erl_crash_$(date "+%Y%m%d-%H%M%S").dump +ERL_INETRC="$CONFIG_DIR"/inetrc + +# define ejabberd parameters +EJABBERD_OPTS="\ +$(sed '/^log_rotate_size/!d;s/:[ \t]*\([0-9]\{1,\}\).*/ \1/;s/:[ \t]*\(infinity\).*/ \1 /;s/^/ /' "$EJABBERD_CONFIG_PATH")\ +$(sed '/^log_rotate_count/!d;s/:[ \t]*\([0-9]*\).*/ \1 /;s/^/ /' "$EJABBERD_CONFIG_PATH")\ +$(sed '/^log_burst_limit_count/!d;s/:[ \t]*\([0-9]*\).*/ \1 /;s/^/ /' "$EJABBERD_CONFIG_PATH")\ +$(sed '/^log_burst_limit_window_time/!d;s/:[ \t]*\([0-9]*[a-z]*\).*/ \1 /;s/^/ /' "$EJABBERD_CONFIG_PATH")\ +$EJABBERD_OPTS" +[ -n "$EJABBERD_OPTS" ] && EJABBERD_OPTS="-ejabberd $EJABBERD_OPTS" +EJABBERD_OPTS="-mnesia dir \"$SPOOL_DIR\" $MNESIA_OPTIONS $EJABBERD_OPTS -s ejabberd" + +# export global variables +export EJABBERD_CONFIG_PATH +export EJABBERD_LOG_PATH +export EJABBERD_PID_PATH +export ERL_CRASH_DUMP +export ERL_EPMD_ADDRESS +export ERL_DIST_PORT +export ERL_INETRC +export ERL_MAX_PORTS +export ERL_MAX_ETS_TABLES +export CONTRIB_MODULES_PATH +export CONTRIB_MODULES_CONF_DIR +export ERL_LIBS +export SCRIPT_DIR + +set_dist_client() +{ + [ -n "$ERL_DIST_PORT" ] && ERLANG_OPTS="$ERLANG_OPTS -dist_listen false" +} + +# run command either directly or via su $INSTALLUSER +run_cmd() +{ + case $EXEC_CMD in + as_install_user) su -s /bin/sh -c '"$0" "$@"' "$INSTALLUSER" -- "$@" ;; + as_current_user) "$@" ;; + esac +} +exec_cmd() +{ + case $EXEC_CMD in + as_current_user) exec "$@" ;; + as_install_user) su -s /bin/sh -c 'exec "$0" "$@"' "$INSTALLUSER" -- "$@" ;; + esac +} +run_erl() +{ + NODE=$1; shift + run_cmd "$ERL" ${S:--}name "$NODE" $ERLANG_OPTS "$@" +} +exec_erl() +{ + NODE=$1; shift + exec_cmd "$ERL" ${S:--}name "$NODE" $ERLANG_OPTS "$@" +} +exec_iex() +{ + NODE=$1; shift + exec_cmd "$IEX" -${S:--}name "$NODE" --erl "$ERLANG_OPTS" "$@" +} + +# usage +debugwarning() +{ + if [ "$EJABBERD_BYPASS_WARNINGS" != "true" ] ; then + echo "--------------------------------------------------------------------" + echo "" + echo "IMPORTANT: we will attempt to attach an INTERACTIVE shell" + echo "to an already running ejabberd node." + echo "If an ERROR is printed, it means the connection was not successful." + echo "You can interact with the ejabberd node if you know how to use it." + echo "Please be extremely cautious with your actions," + echo "and exit immediately if you are not completely sure." + echo "" + echo "To exit and detach this shell from ejabberd, press:" + echo " control+g and then q" + echo "" + #vt100 echo "Please do NOT use control+c in this debug shell !" + #vt100 echo "" + echo "--------------------------------------------------------------------" + echo "To bypass permanently this warning, add to ejabberdctl.cfg the line:" + echo " EJABBERD_BYPASS_WARNINGS=true" + echo "Press return to continue" + read -r _ + echo "" + fi +} + +livewarning() +{ + if [ "$EJABBERD_BYPASS_WARNINGS" != "true" ] ; then + echo "--------------------------------------------------------------------" + echo "" + echo "IMPORTANT: ejabberd is going to start in LIVE (interactive) mode." + echo "All log messages will be shown in the command shell." + echo "You can interact with the ejabberd node if you know how to use it." + echo "Please be extremely cautious with your actions," + echo "and exit immediately if you are not completely sure." + echo "" + echo "To stop ejabberd gracefully:" + echo " ejabberd:stop()." + echo "To quit erlang immediately, press:" + echo " control+g and then q" + echo "" + echo "--------------------------------------------------------------------" + echo "To bypass permanently this warning, add to ejabberdctl.cfg the line:" + echo " EJABBERD_BYPASS_WARNINGS=true" + echo "Press return to continue" + read -r _ + echo "" + fi +} + +check_etop_result() +{ + result=$? + if [ $result -eq 1 ] ; then + echo "" + echo "It seems there was some problem running 'ejabberdctl etop'." + echo "Is the error message something like this?" + echo " Failed to load module 'etop' because it cannot be found..." + echo "Then probably ejabberd was compiled with development tools disabled." + echo "To use 'etop', recompile ejabberd with: ./configure --enable-tools" + echo "" + exit $result + fi +} + +check_iex_result() +{ + result=$? + if [ $result -eq 127 ] ; then + echo "" + echo "It seems there was some problem finding 'iex' binary from Elixir." + echo "Probably ejabberd was compiled with Rebar3 and Elixir disabled, like:" + echo " ./configure" + echo "which is equivalent to:" + echo " ./configure --with-rebar=rebar3 --disable-elixir" + echo "To use 'iex', recompile ejabberd enabling Elixir or using Mix:" + echo " ./configure --enable-elixir" + echo " ./configure --with-rebar=mix" + echo "" + exit $result + fi +} + +help() +{ + echo "" + echo "Commands to start an ejabberd node:" + echo " start Start in server mode" + echo " foreground Start in server mode (attached)" + echo " foreground-quiet Start in server mode (attached), show only critical messages" + echo " live Start in interactive mode, with Erlang shell" + echo " iexlive Start in interactive mode, with Elixir shell" + echo "" + echo "Commands to interact with a running ejabberd node:" + echo " debug Attach an interactive Erlang shell to a running node" + echo " iexdebug Attach an interactive Elixir shell to a running node" + echo " etop Attach to a running node and start Erlang Top" + echo " ping Send ping to the node, returns pong or pang" + echo " started|stopped Wait for the node to fully start|stop" + echo "" + echo "Optional parameters when starting an ejabberd node:" + echo " --config-dir dir Config ejabberd: $CONFIG_DIR" + echo " --config file Config ejabberd: $EJABBERD_CONFIG_PATH" + echo " --ctl-config file Config ejabberdctl: $EJABBERDCTL_CONFIG_PATH" + echo " --logs dir Directory for logs: $LOGS_DIR" + echo " --spool dir Database spool dir: $SPOOL_DIR" + echo " --node nodename ejabberd node name: $ERLANG_NODE" + echo "" +} + +# dynamic node name helper +uid() { + ERTSVERSION="$("$ERL" -version 2>&1 | sed 's|.* \([0-9]*[0-9]\).*|\1|g')" + if [ $ERTSVERSION -lt 11 ] ; then # otp 23.0 includes erts 11.0 + # Erlang/OTP lower than 23, which doesn's support dynamic node code + N=1 + PF=$(( $$ % 97 )) + while + case $# in + 0) NN="${PF}-${N}-${ERLANG_NODE}" + ;; + 1) NN="${PF}-${N}-${1}-${ERLANG_NODE}" + ;; + 2) NN="${PF}-${N}-${1}@${2}" + ;; + esac + N=$(( N + 1 + ( $$ % 5 ) )) + "$EPMD" -names 2>/dev/null | grep -q " ${NN%@*} " + do :; done + echo $NN + else + # Erlang/OTP 23 or higher: use native dynamic node code + # https://www.erlang.org/patches/otp-23.0#OTP-13812 + if [ "$ERLANG_NODE" != "${ERLANG_NODE%.*}" ]; then + echo "undefined@${ERLANG_NODE#*@}" + else + echo "undefined" + fi + fi +} + +# stop epmd if there is no other running node +stop_epmd() +{ + [ -n "$ERL_DIST_PORT" ] && return + "$EPMD" -names 2>/dev/null | grep -q name || "$EPMD" -kill >/dev/null +} + +# make sure node not already running and node name unregistered +# if all ok, ensure runtime directory exists and make it current directory +check_start() +{ + ECSIMAGE_DBPATH=$HOME/database/$ERLANG_NODE + [ ! -d "$ECSIMAGE_DBPATH" ] && ln -s $HOME/database $HOME/database/$ERLANG_NODE + [ -n "$ERL_DIST_PORT" ] && return + "$EPMD" -names 2>/dev/null | grep -q " ${ERLANG_NODE%@*} " && { + pgrep -f "$ERLANG_NODE" >/dev/null && { + echo "ERROR: The ejabberd node '$ERLANG_NODE' is already running." + exit 4 + } + pgrep beam >/dev/null && { + echo "ERROR: The ejabberd node '$ERLANG_NODE' is registered," + echo " but no related beam process has been found." + echo "Shutdown all other erlang nodes, and call 'epmd -kill'." + exit 5 + } + "$EPMD" -kill >/dev/null + } +} + +post_waiter_fork() +{ + (FIRST_RUN=$FIRST_RUN "$0" post_waiter)& +} + +post_waiter_waiting() +{ + $0 started + [ -n "$FIRST_RUN" ] && [ -n "$CTL_ON_CREATE" ] && (post_waiter_loop $CTL_ON_CREATE) + [ -n "$CTL_ON_START" ] && post_waiter_loop $CTL_ON_START +} + +post_waiter_loop() +{ + LIST=$@ + HEAD=${LIST%% ; *} + TAIL=${LIST#* ; } + HEAD2=${HEAD#\! *} + echo ":> ejabberdctl $HEAD2" + $0 $HEAD2 + ctlstatus=$? + if [ $ctlstatus -ne 0 ] ; then + if [ "$HEAD" != "$HEAD2" ] ; then + echo ":> FAILURE in command '$HEAD2' !!! Ignoring result" + else + echo ":> FAILURE in command '$HEAD' !!! Stopping ejabberd..." + $0 halt > /dev/null + exit $ctlstatus + fi + fi + [ "$HEAD" = "$TAIL" ] || post_waiter_loop $TAIL +} + +# allow sync calls +wait_status() +{ + wait_status_node "$ERLANG_NODE" $1 $2 $3 +} + +wait_status_node() +{ + CONNECT_NODE=$1 + shift + # args: status try delay + # return: 0 OK, 1 KO + timeout="$2" + status=4 + while [ "$status" -ne "$1" ] ; do + sleep "$3" + timeout=$((timeout - 1)) + if [ $timeout -eq 0 ] ; then + status="$1" + else + run_erl "$(uid ctl)" -hidden -noinput \ + -eval 'net_kernel:connect_node('"'$CONNECT_NODE'"')' \ + -s ejabberd_ctl \ + -extra "$CONNECT_NODE" $NO_TIMEOUT status > /dev/null + status="$?" + fi + done + [ $timeout -gt 0 ] +} + +exec_other_command() +{ + exec_other_command_node $ERLANG_NODE "$@" +} + +exec_other_command_node() +{ + CONNECT_NODE=$1 + shift + if [ -z "$CTL_OVER_HTTP" ] || [ ! -S "$CTL_OVER_HTTP" ] \ + || [ ! -x "$(command -v curl)" ] || [ -z "$1" ] || [ "$1" = "help" ] \ + || [ "$1" = "mnesia_info_ctl" ]|| [ "$1" = "print_sql_schema" ] ; then + run_erl "$(uid ctl)" -hidden -noinput \ + -eval 'net_kernel:connect_node('"'$CONNECT_NODE'"')' \ + -s ejabberd_ctl \ + -extra "$CONNECT_NODE" $NO_TIMEOUT "$@" + result=$? + case $result in + 3) help;; + *) :;; + esac + return $result + else + exec_ctl_over_http_socket "$@" + fi +} + +exec_ctl_over_http_socket() +{ + COMMAND=${1} + CARGS="" + while [ $# -gt 0 ]; do + [ -z "$CARGS" ] && CARGS="[" || CARGS="${CARGS}, " + CARGS="${CARGS}\"$1\"" + shift + done + CARGS="${CARGS}]" + TEMPHEADERS=temp-headers.log + curl \ + --unix-socket ${CTL_OVER_HTTP} \ + --header "Content-Type: application/json" \ + --header "Accept: application/json" \ + --data "${CARGS}" \ + --dump-header ${TEMPHEADERS} \ + --no-progress-meter \ + "http://localhost/ctl/${COMMAND}" + result=$(sed -n 's/.*status-code: \([0-9]*\).*/\1/p' < $TEMPHEADERS) + rm ${TEMPHEADERS} + case $result in + 2|3) exec_other_command help ${COMMAND};; + *) :;; + esac + exit $result +} + +# ensure we can change current directory to SPOOL_DIR +[ -f "$SPOOL_DIR/schema.DAT" ] || FIRST_RUN=true +[ -d "$SPOOL_DIR" ] || run_cmd mkdir -p "$SPOOL_DIR" +cd "$SPOOL_DIR" || { + echo "ERROR: can not access directory $SPOOL_DIR" + exit 6 +} + +printe() +{ + printf "\n" + printf "\e[1;40;32m==> %s\e[0m\n" "$1" +} + +## Function copied from tools/make-installers +user_agrees() +{ + question="$*" + + if [ -t 0 ] + then + printe "$question (y/n) [n]" + read -r response + case "$response" in + [Yy]|[Yy][Ee][Ss]) + return 0 + ;; + [Nn]|[Nn][Oo]|'') + return 1 + ;; + *) + echo 'Please respond with "yes" or "no".' + user_agrees "$question" + ;; + esac + else # Assume 'yes' if not running interactively. + return 0 + fi +} + +mnesia_change() +{ + ERLANG_NODE_OLD="$1" + [ "$ERLANG_NODE_OLD" = "" ] \ + && echo "Error: Please provide the old erlang node name, for example:" \ + && echo " ejabberdctl mnesia_change ejabberd@oldmachine" \ + && exit 1 + + SPOOL_DIR_BACKUP=$SPOOL_DIR/$ERLANG_NODE_OLD-backup/ + OLDFILE=$SPOOL_DIR_BACKUP/$ERLANG_NODE_OLD.backup + NEWFILE=$SPOOL_DIR_BACKUP/$ERLANG_NODE.backup + + printe "This changes your mnesia database from node name '$ERLANG_NODE_OLD' to '$ERLANG_NODE'" + + [ -d "$SPOOL_DIR_BACKUP" ] && printe "WARNING! A backup of old node already exists in $SPOOL_DIR_BACKUP" + + if ! user_agrees "Do you want to proceed?" + then + echo 'Operation aborted.' + exit 1 + fi + + printe "Starting ejabberd with old node name $ERLANG_NODE_OLD ..." + exec_erl "$ERLANG_NODE_OLD" $EJABBERD_OPTS -detached + wait_status_node $ERLANG_NODE_OLD 0 30 2 + result=$? + case $result in + 1) echo "There was a problem starting ejabberd with the old erlang node name. " \ + && echo "Check for log errors in $EJABBERD_LOG_PATH" \ + && exit $result;; + *) :;; + esac + exec_other_command_node $ERLANG_NODE_OLD "status" + + printe "Making backup of old database to file $OLDFILE ..." + mkdir $SPOOL_DIR_BACKUP + exec_other_command_node $ERLANG_NODE_OLD backup "$OLDFILE" + + printe "Changing node name in new backup file $NEWFILE ..." + exec_other_command_node $ERLANG_NODE_OLD mnesia_change_nodename "$ERLANG_NODE_OLD" "$ERLANG_NODE" "$OLDFILE" "$NEWFILE" + + printe "Stopping old ejabberd..." + exec_other_command_node $ERLANG_NODE_OLD "stop" + wait_status_node $ERLANG_NODE_OLD 3 30 2 && stop_epmd + + printe "Moving old mnesia spool files to backup subdirectory $SPOOL_DIR_BACKUP ..." + mv $SPOOL_DIR/*.DAT $SPOOL_DIR_BACKUP + mv $SPOOL_DIR/*.DCD $SPOOL_DIR_BACKUP + mv $SPOOL_DIR/*.LOG $SPOOL_DIR_BACKUP + + printe "Starting ejabberd with new node name $ERLANG_NODE ..." + exec_erl "$ERLANG_NODE" $EJABBERD_OPTS -detached + wait_status 0 30 2 + exec_other_command "status" + + printe "Installing fallback of new mnesia..." + exec_other_command install_fallback "$NEWFILE" + + printe "Stopping new ejabberd..." + exec_other_command "stop" + wait_status 3 30 2 && stop_epmd + + printe "Finished, now you can start ejabberd normally" +} + +# main +case $1 in + start) + check_start + exec_erl "$ERLANG_NODE" $EJABBERD_OPTS -detached + ;; + foreground) + check_start + post_waiter_fork + exec_erl "$ERLANG_NODE" $EJABBERD_OPTS -noinput + ;; + foreground-quiet) + check_start + exec_erl "$ERLANG_NODE" $EJABBERD_OPTS -noinput -ejabberd quiet true + ;; + live) + livewarning + check_start + exec_erl "$ERLANG_NODE" $EJABBERD_OPTS + ;; + debug) + debugwarning + set_dist_client + exec_erl "$(uid debug)" -hidden -remsh "$ERLANG_NODE" + ;; + etop) + set_dist_client + exec_erl "$(uid top)" -hidden -remsh "$ERLANG_NODE" \ + -eval 'net_kernel:connect_node('"'$ERLANG_NODE'"')' \ + -s etop \ + -output text + check_etop_result + ;; + iexdebug) + debugwarning + set_dist_client + exec_iex "$(uid debug)" --remsh "$ERLANG_NODE" + check_iex_result + ;; + iexlive) + livewarning + exec_iex "$ERLANG_NODE" --erl "$EJABBERD_OPTS" + check_iex_result + ;; + ping) + PEER=${2:-$ERLANG_NODE} + [ "$PEER" = "${PEER%.*}" ] && PS="-s" + set_dist_client + exec_cmd "$ERL" ${PS:--}name "$(uid ping "$(hostname $PS)")" $ERLANG_OPTS \ + -noinput -hidden \ + -eval 'net_kernel:connect_node('"'$PEER'"')' \ + -eval 'io:format("~p~n",[net_adm:ping('"'$PEER'"')])' \ + -s erlang halt -output text + ;; + started) + set_dist_client + wait_status 0 30 2 # wait 30x2s before timeout + ;; + stopped) + set_dist_client + wait_status 3 30 2 && stop_epmd # wait 30x2s before timeout + ;; + mnesia_change) + mnesia_change $2 + ;; + post_waiter) + post_waiter_waiting + ;; + *) + set_dist_client + exec_other_command "$@" + ;; +esac diff --git a/.github/lock.yml b/.github/lock.yml new file mode 100644 index 000000000..626f54530 --- /dev/null +++ b/.github/lock.yml @@ -0,0 +1,38 @@ +# Configuration for Lock Threads - https://github.com/dessant/lock-threads + +# Number of days of inactivity before a closed issue or pull request is locked +daysUntilLock: 365 + +# Skip issues and pull requests created before a given timestamp. Timestamp must +# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable +skipCreatedBefore: false + +# Issues and pull requests with these labels will be ignored. Set to `[]` to disable +exemptLabels: [] + +# Label to add before locking, such as `outdated`. Set to `false` to disable +lockLabel: false + +# Comment to post before locking. Set to `false` to disable +lockComment: > + This thread has been automatically locked since there has not been + any recent activity after it was closed. Please open a new issue for + related bugs. + +# Assign `resolved` as the reason for locking. Set to `false` to disable +setLockReason: true + +# Limit to only `issues` or `pulls` +# only: issues + +# Optionally, specify configuration settings just for `issues` or `pulls` +# issues: +# exemptLabels: +# - help-wanted +# lockLabel: outdated + +# pulls: +# daysUntilLock: 30 + +# Repository to extend settings from +# _extends: repo diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..ebf9da68c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,301 @@ +name: CI + +on: + push: + paths-ignore: + - '.devcontainer/**' + - 'examples/**' + - 'lib/**' + - 'man/**' + - 'priv/**' + - '**.md' + pull_request: + paths-ignore: + - '.devcontainer/**' + - 'examples/**' + - 'lib/**' + - 'man/**' + - 'priv/**' + - '**.md' + +jobs: + + tests: + name: Tests + strategy: + fail-fast: false + matrix: + otp: ['25', '26', '27', '28'] + runs-on: ubuntu-24.04 + services: + redis: + image: public.ecr.aws/docker/library/redis + ports: + - 6379:6379 + + steps: + + - uses: actions/checkout@v5 + + - name: Test shell scripts + if: matrix.otp == '27' + run: | + shellcheck test/ejabberd_SUITE_data/gencerts.sh + shellcheck tools/captcha.sh + shellcheck ejabberd.init.template + shellcheck -x ejabberdctl.template + + - name: Get specific Erlang/OTP + uses: erlef/setup-beam@v1 + with: + otp-version: ${{ matrix.otp }} + + - name: Install MS SQL Server + run: | + docker run -d -e "ACCEPT_EULA=Y" -e "SA_PASSWORD=ejabberd_Test1" \ + -v $(pwd)/test/docker/db/mssql/initdb/initdb_mssql.sql:/initdb_mssql.sql:ro \ + -v $(pwd)/sql/mssql.sql:/mssql.sql:ro \ + -v $(pwd)/sql/mssql.new.sql:/mssql.new.sql:ro \ + -p 1433:1433 --name ejabberd-mssql "mcr.microsoft.com/mssql/server:2019-latest" + sleep 10 + + - name: Prepare databases + run: | + docker exec ejabberd-mssql /opt/mssql-tools18/bin/sqlcmd -C -U SA -P ejabberd_Test1 -S localhost -i /initdb_mssql.sql + docker exec ejabberd-mssql /opt/mssql-tools18/bin/sqlcmd -C -U SA -P ejabberd_Test1 -S localhost -d ejabberd_test -i /mssql.sql + sudo systemctl start mysql.service + sudo systemctl start postgresql.service + mysql -u root -proot -e "CREATE DATABASE ejabberd_test;" + mysql -u root -proot -e "CREATE USER 'ejabberd_test'@'localhost' + IDENTIFIED BY 'ejabberd_test';" + mysql -u root -proot -e "GRANT ALL ON ejabberd_test.* + TO 'ejabberd_test'@'localhost';" + pg_isready + sudo -u postgres psql -c "CREATE DATABASE ejabberd_test;" + sudo -u postgres psql -c "CREATE USER ejabberd_test + WITH PASSWORD 'ejabberd_test';" + sudo -u postgres psql -c "GRANT ALL PRIVILEGES + ON DATABASE ejabberd_test TO ejabberd_test;" + sudo -u postgres psql -c "GRANT ALL ON SCHEMA public TO ejabberd_test;" + sudo -u postgres psql -c "ALTER DATABASE ejabberd_test OWNER 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;" + + - name: Prepare libraries + run: | + sudo apt-get -qq update + sudo apt-get -y purge libgd3 nginx + sudo apt-get -qq install libexpat1-dev libgd-dev libpam0g-dev \ + libsqlite3-dev libwebp-dev libyaml-dev + + - name: Remove syntax_tools from release + run: sed -i 's|, syntax_tools||g' src/ejabberd.app.src.script + + - name: Cache Hex.pm + uses: actions/cache@v4 + with: + path: | + ~/.cache/rebar3/ + key: ${{matrix.otp}}-${{hashFiles('rebar.config')}} + + - name: Compile + run: | + ./autogen.sh + ./configure --with-rebar=./rebar3 \ + --prefix=/tmp/ejabberd \ + --enable-all \ + --disable-elixir \ + --disable-mssql \ + --disable-odbc + make + + - run: make install -s + - run: make hooks + - run: make options + - run: make xref + - run: make dialyzer + - run: make test-eunit + - run: make elvis + if: matrix.otp >= '26' + + - name: Check Production Release + run: | + make rel + RE=_build/prod/rel/ejabberd + $RE/bin/ejabberdctl start + $RE/bin/ejabberdctl started + $RE/bin/ejabberdctl stop + $RE/bin/ejabberdctl stopped + cat $RE/logs/ejabberd.log + grep -q "is stopped in" $RE/logs/ejabberd.log + + - name: Start Development Release + run: | + make dev + RE=_build/dev/rel/ejabberd + sed -i 's/starttls_required: true/starttls_required: false/g' $RE/conf/ejabberd.yml + $RE/bin/ejabberdctl start + $RE/bin/ejabberdctl started + $RE/bin/ejabberdctl register admin localhost admin + grep -q "is started in" $RE/logs/ejabberd.log + + - name: Run XMPP Interoperability Tests against CI server. + if: matrix.otp == '27' + continue-on-error: true + uses: XMPP-Interop-Testing/xmpp-interop-tests-action@v1.6.1 + with: + domain: 'localhost' + adminAccountUsername: 'admin' + adminAccountPassword: 'admin' + disabledSpecifications: RFC6121,XEP-0030,XEP-0045,XEP-0054,XEP-0060,XEP-0080,XEP-0115,XEP-0118,XEP-0215,XEP-0347,XEP-0363,XEP-0384 + + - name: Stop Development Release + if: always() + run: | + RE=_build/dev/rel/ejabberd + $RE/bin/ejabberdctl stop + $RE/bin/ejabberdctl stopped + cat $RE/logs/ejabberd.log + grep -q "is stopped in" $RE/logs/ejabberd.log + + - name: Run tests + id: ct + run: | + (cd priv && ln -sf ../sql) + sed -i -e 's/ct:pal/ct:log/' test/suite.erl + 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() && (steps.ct.outcome != 'skipped') + id: ctresults + 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.ctresults.outcome == 'failure' + 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 == '27' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + DIAGNOSTIC=1 ./rebar3 as test coveralls send + curl -v -k https://coveralls.io/webhook \ + --header "Content-Type: application/json" \ + --data '{"repo_name":"$GITHUB_REPOSITORY", + "repo_token":"$GITHUB_TOKEN", + "payload":{"build_num":$GITHUB_RUN_ID, + "status":"done"}}' + + - name: Check for changes to trigger schema upgrade test + uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + sql: + - 'sql/**' + - 'src/mod_admin_update_sql.erl' + + - name: Prepare for schema upgrade test + id: prepupgradetest + if: ${{ steps.filter.outputs.sql == 'true' }} + run: | + [[ -d logs ]] && rm -rf logs + [[ -d _build/test/logs ]] && rm -rf _build/test/logs || true + sed -i 's|update_sql, false|update_sql, true|g' test/suite.erl + - name: Run DB tests on upgraded schema (mssql, mysql, pgsql) + run: CT_BACKENDS=mssql,mysql,pgsql make test + if: always() && steps.prepupgradetest.outcome != 'skipped' + id: ctupgradedschema + - name: Check results + if: always() && steps.ctupgradedschema.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.ctupgradedschema.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 '{}' ';' + + - name: Prepare new schema + run: | + [[ -d logs ]] && rm -rf logs + [[ -d _build/test/logs ]] && rm -rf _build/test/logs || true + docker exec ejabberd-mssql /opt/mssql-tools18/bin/sqlcmd -C -U SA -P ejabberd_Test1 -S localhost -Q "drop database [ejabberd_test];" + docker exec ejabberd-mssql /opt/mssql-tools18/bin/sqlcmd -C -U SA -P ejabberd_Test1 -S localhost -Q "drop login [ejabberd_test];" + mysql -u root -proot -e "DROP DATABASE ejabberd_test;" + sudo -u postgres psql -c "DROP DATABASE ejabberd_test;" + docker exec ejabberd-mssql /opt/mssql-tools18/bin/sqlcmd -C -U SA -P ejabberd_Test1 -S localhost -i /initdb_mssql.sql + docker exec ejabberd-mssql /opt/mssql-tools18/bin/sqlcmd -C -U SA -P ejabberd_Test1 -S localhost -d ejabberd_test -i /mssql.new.sql + mysql -u root -proot -e "CREATE DATABASE ejabberd_test;" + mysql -u root -proot -e "GRANT ALL ON ejabberd_test.* + TO 'ejabberd_test'@'localhost';" + sudo -u postgres psql -c "CREATE DATABASE ejabberd_test;" + sudo -u postgres psql -c "GRANT ALL PRIVILEGES + ON DATABASE ejabberd_test TO ejabberd_test;" + sudo -u postgres psql -c "GRANT ALL ON SCHEMA public TO ejabberd_test;" + sudo -u postgres psql -c "ALTER DATABASE ejabberd_test OWNER 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;" + sed -i 's|new_schema, false|new_schema, true|g' test/suite.erl + - name: Run DB tests on new schema (mssql, mysql, pgsql) + run: CT_BACKENDS=mssql,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 '{}' ';' + + - name: Upload CT logs + if: failure() + uses: actions/upload-artifact@v4 + with: + name: ejabberd-ct-logs-${{matrix.otp}} + # + # Appending the wildcard character ("*") is a trick to make + # "ejabberd-packages" the root directory of the uploaded ZIP file: + # + # https://github.com/actions/upload-artifact#upload-using-multiple-paths-and-exclusions + # + path: _build/test/logs + retention-days: 14 diff --git a/.github/workflows/container.yml b/.github/workflows/container.yml new file mode 100644 index 000000000..0bb169ccc --- /dev/null +++ b/.github/workflows/container.yml @@ -0,0 +1,74 @@ +name: Container + +on: + push: + paths-ignore: + - '.devcontainer/**' + - 'examples/**' + - 'lib/**' + - 'man/**' + - 'priv/**' + - '**.md' + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + container: + name: Container + runs-on: ubuntu-22.04 + permissions: + packages: write + steps: + - name: Check out repository code + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Checkout ejabberd-contrib + uses: actions/checkout@v5 + with: + repository: processone/ejabberd-contrib + path: .ejabberd-modules/sources/ejabberd-contrib + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Get git describe + id: gitdescribe + run: echo "ver=$(git describe --tags)" >> $GITHUB_OUTPUT + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + labels: | + org.opencontainers.image.revision=${{ steps.gitdescribe.outputs.ver }} + org.opencontainers.image.licenses=GPL-2.0 + org.opencontainers.image.vendor=ProcessOne + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and push Docker image + uses: docker/build-push-action@v6 + with: + build-args: | + VERSION=${{ steps.gitdescribe.outputs.ver }} + cache-from: type=gha + cache-to: type=gha,mode=max + context: . + file: .github/container/Dockerfile + labels: ${{ steps.meta.outputs.labels }} + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} diff --git a/.github/workflows/installers.yml b/.github/workflows/installers.yml new file mode 100644 index 000000000..37c8983b4 --- /dev/null +++ b/.github/workflows/installers.yml @@ -0,0 +1,84 @@ +name: Installers + +on: + push: + paths-ignore: + - '.devcontainer/**' + - 'examples/**' + - 'lib/**' + - 'man/**' + - 'priv/**' + - '**.md' + pull_request: + paths-ignore: + - '.devcontainer/**' + - 'examples/**' + - 'lib/**' + - 'man/**' + - 'priv/**' + - '**.md' + +jobs: + binaries: + name: Binaries + runs-on: ubuntu-22.04 + steps: + - name: Cache build directory + uses: actions/cache@v4 + with: + path: ~/build/ + key: ${{runner.os}}-ct-ng-1.27.0 + - name: Install prerequisites + run: | + sudo apt-get -qq update + sudo apt-get -qq install makeself + # https://github.com/crosstool-ng/crosstool-ng/blob/master/testing/docker/ubuntu21.10/Dockerfile + sudo apt-get -qq install build-essential autoconf bison flex gawk + sudo apt-get -qq install help2man libncurses5-dev libtool libtool-bin + sudo apt-get -qq install python3-dev texinfo unzip + - name: Install FPM + run: | + gem install --no-document --user-install fpm + echo $HOME/.local/share/gem/ruby/*/bin >> $GITHUB_PATH + - name: Check out repository code + uses: actions/checkout@v5 + with: + fetch-depth: 0 + - name: Build binary archives + run: CHECK_DEPS=false tools/make-binaries + - name: Build DEB and RPM packages + run: tools/make-packages + - name: Build installers + run: tools/make-installers + - name: Collect packages + run: | + mkdir ejabberd-packages + mv ejabberd_*.deb ejabberd-*.rpm ejabberd-*.run ejabberd-packages + - name: Upload packages + uses: actions/upload-artifact@v4 + with: + name: ejabberd-packages + # + # Appending the wildcard character ("*") is a trick to make + # "ejabberd-packages" the root directory of the uploaded ZIP file: + # + # https://github.com/actions/upload-artifact#upload-using-multiple-paths-and-exclusions + # + path: ejabberd-packages* + retention-days: 14 + + release: + name: Release + needs: [binaries] + runs-on: ubuntu-22.04 + if: github.ref_type == 'tag' + steps: + - name: Download packages + uses: actions/download-artifact@v5 + with: + name: ejabberd-packages + - name: Draft Release + uses: softprops/action-gh-release@v2 + with: + draft: true + files: ejabberd-packages/* diff --git a/.github/workflows/runtime.yml b/.github/workflows/runtime.yml new file mode 100644 index 000000000..cd599fe83 --- /dev/null +++ b/.github/workflows/runtime.yml @@ -0,0 +1,432 @@ +name: Runtime + +on: + push: + paths: + - '*' + - '!*.md' + - '.github/workflows/runtime.yml' + - 'checkouts/**' + - 'config/**' + - 'lib/**' + - 'm4/**' + - 'plugins/**' + - 'rel/**' + pull_request: + paths: + - '*' + - '!*.md' + - '.github/workflows/runtime.yml' + - 'checkouts/**' + - 'config/**' + - 'lib/**' + - 'm4/**' + - 'plugins/**' + - 'rel/**' + +jobs: + + rebars: + name: Rebars + strategy: + fail-fast: false + matrix: + otp: ['24', '25', '26', '27', '28'] + rebar: ['rebar', 'rebar3'] + exclude: + - otp: '24' + rebar: 'rebar' + - otp: '27' + rebar: 'rebar' + - otp: '28' + rebar: 'rebar' + runs-on: ubuntu-24.04 + container: + image: public.ecr.aws/docker/library/erlang:${{ matrix.otp }} + + steps: + + - uses: actions/checkout@v5 + + - name: Get old compatible Rebar binaries + if: matrix.otp < 24 + run: | + rm rebar + rm rebar3 + wget https://github.com/processone/ejabberd/raw/21.12/rebar + wget https://github.com/processone/ejabberd/raw/21.12/rebar3 + chmod +x rebar + chmod +x rebar3 + + - name: Get recent compatible Rebar binaries + if: matrix.otp > 23 && matrix.otp < 25 + run: | + rm rebar + rm rebar3 + wget https://github.com/processone/ejabberd/raw/24.12/rebar + wget https://github.com/processone/ejabberd/raw/24.12/rebar3 + chmod +x rebar + chmod +x rebar3 + + - name: Prepare libraries + run: | + apt-get -qq update + apt-get purge -y libgd3 nginx + apt-get -qq install libexpat1-dev libgd-dev libpam0g-dev \ + libsqlite3-dev libwebp-dev libyaml-dev + + - name: Cache Hex.pm + uses: actions/cache@v4 + with: + path: | + ~/.cache/rebar3/ + key: ${{matrix.otp}}-${{hashFiles('rebar.config')}} + + - name: Unlock eredis dependency + if: matrix.rebar == 'rebar3' && matrix.otp < 21 + run: rebar3 unlock eredis + + - name: Compile + run: | + ./autogen.sh + ./configure --with-rebar=./${{ matrix.rebar }} \ + --prefix=/tmp/ejabberd \ + --with-min-erlang=9.0.5 \ + --enable-all \ + --disable-elixir \ + --disable-tools \ + --disable-odbc + make + + - run: make xref + + - run: make dialyzer + + - name: Prepare rel (rebar2) + if: matrix.rebar == 'rebar' + run: | + mkdir -p _build/prod && ln -s `pwd`/rel/ _build/prod/rel + mkdir -p _build/dev && ln -s `pwd`/rel/ _build/dev/rel + + - name: Run rel + run: | + make rel + _build/prod/rel/ejabberd/bin/ejabberdctl start \ + && _build/prod/rel/ejabberd/bin/ejabberdctl started + _build/prod/rel/ejabberd/bin/ejabberdctl register user1 localhost s0mePass + _build/prod/rel/ejabberd/bin/ejabberdctl registered_users localhost > registered.log + _build/prod/rel/ejabberd/bin/ejabberdctl stop \ + && _build/prod/rel/ejabberd/bin/ejabberdctl stopped + + - name: Run dev + run: | + make dev + _build/dev/rel/ejabberd/bin/ejabberdctl start \ + && _build/dev/rel/ejabberd/bin/ejabberdctl started + _build/dev/rel/ejabberd/bin/ejabberdctl register user2 localhost s0mePass + _build/dev/rel/ejabberd/bin/ejabberdctl registered_users localhost >> registered.log + _build/dev/rel/ejabberd/bin/ejabberdctl stop \ + && _build/dev/rel/ejabberd/bin/ejabberdctl stopped + + - name: Run install + run: | + make install + /tmp/ejabberd/sbin/ejabberdctl start \ + && /tmp/ejabberd/sbin/ejabberdctl started + /tmp/ejabberd/sbin/ejabberdctl register user3 localhost s0mePass + /tmp/ejabberd/sbin/ejabberdctl registered_users localhost >> registered.log + /tmp/ejabberd/sbin/ejabberdctl stop \ + && /tmp/ejabberd/sbin/ejabberdctl stopped + + - name: View logs + run: | + echo "===> Registered:" + cat registered.log + echo "===> Prod:" + cat _build/prod/rel/ejabberd/logs/* + echo "===> Dev:" + cat _build/dev/rel/ejabberd/logs/* + echo "===> Install:" + cat /tmp/ejabberd/var/log/ejabberd/* + + - name: Check logs + run: | + grep -q '^user1$' registered.log + grep -q '^user2$' registered.log + grep -q '^user3$' registered.log + grep -q 'is started' _build/prod/rel/ejabberd/logs/ejabberd.log + grep -q 'is stopped' _build/prod/rel/ejabberd/logs/ejabberd.log + test $(find _build/prod/rel/ -empty -name error.log) + grep -q 'is started' _build/dev/rel/ejabberd/logs/ejabberd.log + grep -q 'is stopped' _build/dev/rel/ejabberd/logs/ejabberd.log + test $(find _build/dev/rel/ -empty -name error.log) + grep -q 'is started' /tmp/ejabberd/var/log/ejabberd/ejabberd.log + grep -q 'is stopped' /tmp/ejabberd/var/log/ejabberd/ejabberd.log + test $(find /tmp/ejabberd/var/log/ejabberd/ -empty -name error.log) + + - name: View logs failures + if: always() + run: | + cat _build/prod/rel/ejabberd/logs/ejabberd.log + cat _build/prod/rel/ejabberd/logs/error.log + cat _build/dev/rel/ejabberd/logs/ejabberd.log + cat _build/dev/rel/ejabberd/logs/error.log + cat /tmp/ejabberd/var/log/ejabberd/ejabberd.log + cat /tmp/ejabberd/var/log/ejabberd/error.log + + rebar3-elixir: + name: Rebar3+Elixir + strategy: + fail-fast: false + matrix: + elixir: ['1.14', '1.15', '1.16', '1.17', '1.18'] + runs-on: ubuntu-24.04 + container: + image: public.ecr.aws/docker/library/elixir:${{ matrix.elixir }} + + steps: + + - uses: actions/checkout@v5 + + - name: Prepare libraries + run: | + apt-get -qq update + apt-get -y purge libgd3 nginx + apt-get -qq install libexpat1-dev libgd-dev libpam0g-dev \ + libsqlite3-dev libwebp-dev libyaml-dev + + - name: Enable Module.Example and an Elixir dependency + run: | + sed -i "s|^modules:|modules:\n 'Ejabberd.Module.Example': {}|g" ejabberd.yml.example + cat ejabberd.yml.example + sed -i 's|^{deps, \[\(.*\)|{deps, [{decimal, ".*", {git, "https://github.com/ericmj/decimal", {branch, "main"}}},\n \1|g' rebar.config + cat rebar.config + + - name: Cache Hex.pm + uses: actions/cache@v4 + with: + path: | + ~/.cache/rebar3/ + key: ${{matrix.elixir}}-${{hashFiles('rebar.config')}} + + - name: Install Hex and Rebar3 manually on older Elixir + if: matrix.elixir <= '1.14' + run: | + mix local.hex --force + mix local.rebar --force + + - name: Compile + run: | + ./autogen.sh + ./configure --with-rebar=./rebar3 \ + --prefix=/tmp/ejabberd \ + --enable-all \ + --disable-odbc + make + + - run: make xref + + - name: Run rel + run: | + make rel + _build/prod/rel/ejabberd/bin/ejabberdctl start \ + && _build/prod/rel/ejabberd/bin/ejabberdctl started + _build/prod/rel/ejabberd/bin/ejabberdctl register user1 localhost s0mePass + _build/prod/rel/ejabberd/bin/ejabberdctl registered_users localhost > registered.log + _build/prod/rel/ejabberd/bin/ejabberdctl stop \ + && _build/prod/rel/ejabberd/bin/ejabberdctl stopped + + - name: Run dev + run: | + make dev + _build/dev/rel/ejabberd/bin/ejabberdctl start \ + && _build/dev/rel/ejabberd/bin/ejabberdctl started + _build/dev/rel/ejabberd/bin/ejabberdctl register user2 localhost s0mePass + _build/dev/rel/ejabberd/bin/ejabberdctl registered_users localhost >> registered.log + _build/dev/rel/ejabberd/bin/ejabberdctl stop \ + && _build/dev/rel/ejabberd/bin/ejabberdctl stopped + + - name: Run install + run: | + make install + /tmp/ejabberd/sbin/ejabberdctl start \ + && /tmp/ejabberd/sbin/ejabberdctl started + /tmp/ejabberd/sbin/ejabberdctl register user3 localhost s0mePass + /tmp/ejabberd/sbin/ejabberdctl registered_users localhost >> registered.log + /tmp/ejabberd/sbin/ejabberdctl stop \ + && /tmp/ejabberd/sbin/ejabberdctl stopped + + - name: View logs + if: always() + run: | + echo "===> Registered:" + cat registered.log + echo "===> Prod:" + cat _build/prod/rel/ejabberd/logs/* + echo "===> Dev:" + cat _build/dev/rel/ejabberd/logs/* + echo "===> Install:" + cat /tmp/ejabberd/var/log/ejabberd/* + + - name: Check logs + if: always() + run: | + grep -q '^user1$' registered.log + grep -q '^user2$' registered.log + grep -q '^user3$' registered.log + grep -q 'is started' _build/prod/rel/ejabberd/logs/ejabberd.log + grep -q 'is stopped' _build/prod/rel/ejabberd/logs/ejabberd.log + grep -q 'Stopping Ejabberd.Module.Example' _build/prod/rel/ejabberd/logs/ejabberd.log + test $(find _build/prod/ -empty -name error.log) + grep -q 'is started' _build/dev/rel/ejabberd/logs/ejabberd.log + grep -q 'is stopped' _build/dev/rel/ejabberd/logs/ejabberd.log + grep -q 'Stopping Ejabberd.Module.Example' _build/dev/rel/ejabberd/logs/ejabberd.log + test $(find _build/dev/ -empty -name error.log) + grep -q 'is started' /tmp/ejabberd/var/log/ejabberd/ejabberd.log + grep -q 'is stopped' /tmp/ejabberd/var/log/ejabberd/ejabberd.log + grep -q 'Stopping Ejabberd.Module.Example' /tmp/ejabberd/var/log/ejabberd/ejabberd.log + test $(find /tmp/ejabberd/var/log/ejabberd/ -empty -name error.log) + + - name: View logs failures + if: failure() + run: | + cat _build/prod/rel/ejabberd/logs/ejabberd.log + cat _build/prod/rel/ejabberd/logs/error.log + cat _build/dev/rel/ejabberd/logs/ejabberd.log + cat _build/dev/rel/ejabberd/logs/error.log + cat /tmp/ejabberd/var/log/ejabberd/ejabberd.log + cat /tmp/ejabberd/var/log/ejabberd/error.log + + mix: + name: Mix + strategy: + fail-fast: false + matrix: + elixir: ['1.14', '1.15', '1.16', '1.17', '1.18'] + runs-on: ubuntu-24.04 + container: + image: public.ecr.aws/docker/library/elixir:${{ matrix.elixir }} + + steps: + + - uses: actions/checkout@v5 + + - name: Prepare libraries + run: | + apt-get -qq update + apt-get -y purge libgd3 nginx + apt-get -qq install libexpat1-dev libgd-dev libpam0g-dev \ + libsqlite3-dev libwebp-dev libyaml-dev + + - name: Remove Elixir Matchers + run: | + echo "::remove-matcher owner=elixir-mixCompileWarning::" + echo "::remove-matcher owner=elixir-credoOutputDefault::" + echo "::remove-matcher owner=elixir-mixCompileError::" + echo "::remove-matcher owner=elixir-mixTestFailure::" + echo "::remove-matcher owner=elixir-dialyzerOutputDefault::" + + - name: Enable Module.Example and an Elixir dependency + run: | + sed -i "s|^modules:|modules:\n 'Ejabberd.Module.Example': {}|g" ejabberd.yml.example + cat ejabberd.yml.example + sed -i 's|^{deps, \(.*\)|{deps, \1\n {decimal, ".*", {git, "https://github.com/ericmj/decimal", {branch, "main"}}}, |g' rebar.config + cat rebar.config + + - name: Cache Hex.pm + uses: actions/cache@v4 + with: + path: | + ~/.hex/ + key: ${{matrix.elixir}}-${{hashFiles('mix.exs')}} + + - name: Install Hex and Rebar3 manually on older Elixir + if: matrix.elixir <= '1.14' + run: | + mix local.hex --force + mix local.rebar --force + + - name: Compile + run: | + ./autogen.sh + ./configure --with-rebar=mix \ + --prefix=/tmp/ejabberd \ + --enable-all + make + + - run: make xref + + - run: make dialyzer + + - run: make edoc + + - name: Run rel + run: | + make rel + _build/prod/rel/ejabberd/bin/ejabberdctl start \ + && _build/prod/rel/ejabberd/bin/ejabberdctl started + _build/prod/rel/ejabberd/bin/ejabberdctl register user1 localhost s0mePass + _build/prod/rel/ejabberd/bin/ejabberdctl registered_users localhost > registered.log + _build/prod/rel/ejabberd/bin/ejabberdctl stop \ + && _build/prod/rel/ejabberd/bin/ejabberdctl stopped + + - name: Run dev + run: | + make dev + _build/dev/rel/ejabberd/bin/ejabberdctl start \ + && _build/dev/rel/ejabberd/bin/ejabberdctl started + _build/dev/rel/ejabberd/bin/ejabberdctl register user2 localhost s0mePass + _build/dev/rel/ejabberd/bin/ejabberdctl registered_users localhost >> registered.log + _build/dev/rel/ejabberd/bin/ejabberdctl stop \ + && _build/dev/rel/ejabberd/bin/ejabberdctl stopped + + - name: Run install + run: | + make install + /tmp/ejabberd/sbin/ejabberdctl start \ + && /tmp/ejabberd/sbin/ejabberdctl started + /tmp/ejabberd/sbin/ejabberdctl register user3 localhost s0mePass + /tmp/ejabberd/sbin/ejabberdctl registered_users localhost >> registered.log + /tmp/ejabberd/sbin/ejabberdctl stop \ + && /tmp/ejabberd/sbin/ejabberdctl stopped + + - name: View logs + if: always() + run: | + echo "===> Registered:" + cat registered.log + echo "===> Prod:" + cat _build/prod/rel/ejabberd/logs/* + echo "===> Dev:" + cat _build/dev/rel/ejabberd/logs/* + echo "===> Install:" + cat /tmp/ejabberd/var/log/ejabberd/* + + - name: Check logs + if: always() + run: | + grep -q '^user1$' registered.log + grep -q '^user2$' registered.log + grep -q '^user3$' registered.log + grep -q 'is started' _build/prod/rel/ejabberd/logs/ejabberd.log + grep -q 'is stopped' _build/prod/rel/ejabberd/logs/ejabberd.log + grep -q 'Stopping Ejabberd.Module.Example' _build/prod/rel/ejabberd/logs/ejabberd.log + test $(find _build/prod/ -empty -name error.log) + grep -q 'is started' _build/dev/rel/ejabberd/logs/ejabberd.log + grep -q 'is stopped' _build/dev/rel/ejabberd/logs/ejabberd.log + grep -q 'Stopping Ejabberd.Module.Example' _build/dev/rel/ejabberd/logs/ejabberd.log + test $(find _build/dev/ -empty -name error.log) + grep -q 'is started' /tmp/ejabberd/var/log/ejabberd/ejabberd.log + grep -q 'is stopped' /tmp/ejabberd/var/log/ejabberd/ejabberd.log + grep -q 'Stopping Ejabberd.Module.Example' /tmp/ejabberd/var/log/ejabberd/ejabberd.log + test $(find /tmp/ejabberd/var/log/ejabberd/ -empty -name error.log) + + - name: View logs failures + if: failure() + run: | + cat _build/prod/rel/ejabberd/logs/ejabberd.log + cat _build/prod/rel/ejabberd/logs/error.log + cat _build/dev/rel/ejabberd/logs/ejabberd.log + cat _build/dev/rel/ejabberd/logs/error.log + cat /tmp/ejabberd/var/log/ejabberd/ejabberd.log + cat /tmp/ejabberd/var/log/ejabberd/error.log diff --git a/.gitignore b/.gitignore index c72fcdc36..0f69a0aa3 100644 --- a/.gitignore +++ b/.gitignore @@ -4,37 +4,46 @@ *~ \#*# .#* +.edts +.tool-versions +*.dump /Makefile +/doc /config.log /config.status +/config/releases.exs /configure /aclocal.m4 -/contrib/extract_translations/extract_translations.beam /*.cache /deps/ -/doc/*.aux -/doc/*.haux -/doc/*.html -/doc/*.htoc -/doc/*.idx -/doc/*.ilg -/doc/*.ind -/doc/*.log -/doc/*.out -/doc/*.pdf -/doc/*.toc -/doc/contributed_modules.tex -/doc/version.tex +/.deps-update/ +/.ejabberd-modules/ /ebin/ /ejabberd.init +/ejabberd.service +/ejabberdctl /ejabberdctl.example -/include/XmppAddr.hrl /rel/ejabberd/ -/src/XmppAddr.asn1db -/src/XmppAddr.erl -/src/ejabberd.app.src +/rel/overlays/ /src/eldap_filter_yecc.erl /vars.config /dialyzer/ /test/*.beam +/test/*.ctc /logs/ +/priv/bin/captcha*sh +/priv/sql +/rel/ejabberd +/recompile.log +/_build +/database/ +/.rebar +/log/ +Mnesia.nonode@nohost/ +/TAGS +/tags +# Binaries created with tools/make-{binaries,installers,packages}: +/ejabberd_*.deb +/ejabberd-*.rpm +/ejabberd-*.run +/ejabberd-*.tar.gz 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/.travis.yml b/.travis.yml deleted file mode 100644 index 1dd745a77..000000000 --- a/.travis.yml +++ /dev/null @@ -1,38 +0,0 @@ -language: erlang - -otp_release: - - R16B03 - - R15B01 - -services: - - riak - -before_install: - - sudo apt-get -qq update - -install: - - sudo apt-get -qq install libexpat1-dev libyaml-dev libpam0g-dev - -before_script: - - mysql -u root -e "CREATE USER 'ejabberd_test'@'localhost' IDENTIFIED BY 'ejabberd_test';" - - mysql -u root -e "CREATE DATABASE ejabberd_test;" - - mysql -u root -e "GRANT ALL ON ejabberd_test.* TO 'ejabberd_test'@'localhost';" - - psql -U postgres -c "CREATE USER ejabberd_test WITH PASSWORD 'ejabberd_test';" - - psql -U postgres -c "CREATE DATABASE ejabberd_test;" - - psql -U postgres -c "GRANT ALL PRIVILEGES ON DATABASE ejabberd_test TO ejabberd_test;" - -script: - - ./autogen.sh - - ./configure --enable-all --disable-odbc --disable-elixir - - make xref - - ERL_LIBS=$PWD make test - - grep -q 'TEST COMPLETE, \([[:digit:]]*\) ok, .* of \1 ' logs/raw.log - -after_script: - - find logs -name suite.log -exec cat '{}' ';' - -after_failure: - - find logs -name ejabberd.log -exec cat '{}' ';' - -notifications: - email: false diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000..6d51e0b07 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "erlang-ls.erlang-ls" + ] +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..78cdf45ae --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,66 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Relive (Vim)", + "type": "erlang", + "request": "launch", + "runinterminal": [ + "./rebar3", "shell", + "--apps", "ejabberd", + "--config", "rel/relive.config", + "--script", "rel/relive.escript", + "--name", "ejabberd@localhost", + "--setcookie", "COOKIE" + ], + "projectnode": "ejabberd@localhost", + "cookie": "COOKIE", + "timeout": 900, + "cwd": "." + }, + { + "name": "Relive (VSCode)", + "type": "erlang", + "request": "launch", + "runinterminal": [ + ".vscode/relive.sh" + ], + "projectnode": "ejabberd@localhost", + "cookie": "COOKIE", + "timeout": 300, + "cwd": "${workspaceRoot}" + }, + { + "name": "Relive (alternate)", + "type": "erlang", + "request": "launch", + "runinterminal": [ + "./rebar3", "shell", + "--apps", "ejabberd", + "--config", "rel/relive.config", + "--script", "rel/relive.escript", + "--name", "ejabberd@localhost", + "--setcookie", "COOKIE" + ], + "projectnode": "ejabberd@localhost", + "cookie": "COOKIE", + "timeout": 300, + "cwd": "${workspaceRoot}" + }, + { + "name": "Attach", + "type": "erlang", + "request": "attach", + "runinterminal": [ + "./rebar3", "shell", + "--sname", "clean@localhost", + "--setcookie", "COOKIE", + "--start-clean" + ], + "projectnode": "ejabberd@localhost", + "cookie": "COOKIE", + "timeout": 300, + "cwd": "${workspaceRoot}" + } + ] +} diff --git a/.vscode/relive.sh b/.vscode/relive.sh new file mode 100755 index 000000000..b10b83fb0 --- /dev/null +++ b/.vscode/relive.sh @@ -0,0 +1,6 @@ +[ ! -f Makefile ] \ + && ./autogen.sh \ + && ./configure --with-rebar=rebar3 \ + && make + +make relive diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..c8e01bd2a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,13 @@ +{ + "editor.tabSize": 8, + "remote.portsAttributes": { + "1883": {"label": "MQTT", "onAutoForward": "silent"}, + "4369": {"label": "EPMD", "onAutoForward": "silent"}, + "5222": {"label": "XMPP C2S", "onAutoForward": "silent"}, + "5223": {"label": "XMPP C2S (legacy)", "onAutoForward": "silent"}, + "5269": {"label": "XMPP S2S", "onAutoForward": "silent"}, + "5280": {"label": "HTTP", "onAutoForward": "silent"}, + "5443": {"label": "HTTPS", "onAutoForward": "silent"}, + "7777": {"label": "XMPP SOCKS5 (proxy65)", "onAutoForward": "silent"} + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..cadfc1c74 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,1673 @@ +## Version 25.08 + +#### API Commands + +- `ban_account`: Run `sm_kick_user` event when kicking account ([#4415](https://github.com/processone/ejabberd/issues/4415)) +- `ban_account`: No need to change password ([#4415](https://github.com/processone/ejabberd/issues/4415)) +- `mnesia_change`: New command in `ejabberdctl` script that helps changing the mnesia node name + +#### Configuration + +- Rename `auth_password_types_hidden_in_scram1` option to `auth_password_types_hidden_in_sasl1` +- `econf`: If a host in configuration is encoded IDNA, decode it ([#3519](https://github.com/processone/ejabberd/issues/3519)) +- `ejabberd_config`: New predefined keyword `HOST_URL_ENCODE` +- `ejabberd.yml.example`: Use `HOST_URL_ENCODE` to handle case when vhost is non-latin1 +- `mod_conversejs`: Add option `conversejs_plugins` ([#4413](https://github.com/processone/ejabberd/issues/4413)) +- `mod_matrix_gw`: Add `leave_timeout` option ([#4386](https://github.com/processone/ejabberd/issues/4386)) + +#### Documentation and Tests + +- `COMPILE.md`: Mention dependencies and add link to Docs ([#4431](https://github.com/processone/ejabberd/issues/4431)) +- `ejabberd_doc`: Document commands tags for modules +- CI: bump XMPP-Interop-Testing/xmpp-interop-tests-action ([#4425](https://github.com/processone/ejabberd/issues/4425)) +- Runtime: Raise the minimum Erlang tested to Erlang/OTP 24 + +#### Installers and Container + +- Bump Erlang/OTP version to 27.3.4.2 +- Bump OpenSSL version to 3.5.2 +- `make-binaries`: Disable Linux-PAM's `logind` support + +#### Core and Modules + +- Bump `p1_acme` to fix `'AttributePKCS-10'` and OTP 28 ([processone/p1_acme#4](https://github.com/processone/p1_acme/issues/4)) +- Prevent loops in `xml_compress:decode` with corrupted data +- `ejabberd_auth_mnesia`: Fix issue with filtering duplicates in `get_users()` +- `ejabberd_listener`: Add secret in temporary unix domain socket path ([#4422](https://github.com/processone/ejabberd/issues/4422)) +- `ejabberd_listener`: Log error when cannot set definitive unix socket ([#4422](https://github.com/processone/ejabberd/issues/4422)) +- `ejabberd_listener`: Try to create provisional socket in final directory ([#4422](https://github.com/processone/ejabberd/issues/4422)) +- `ejabberd_logger`: Print log lines colorized in console when using rebar3 +- `mod_conversejs`: Ensure assets_path ends in `/` as required by Converse ([#4414](https://github.com/processone/ejabberd/issues/4414)) +- `mod_conversejs`: Ensure plugins URL is separated with `/` ([#4413](https://github.com/processone/ejabberd/issues/4413)) +- `mod_http_upload`: Encode URLs into IDNA when showing to XMPP client ([#3519](https://github.com/processone/ejabberd/issues/3519)) +- `mod_matrix_gw`: Add support for null values in `is_canonical_json` ([#4421](https://github.com/processone/ejabberd/issues/4421)) +- `mod_matrix_gw`: Don't send empty direct Matrix messages ([#4420](https://github.com/processone/ejabberd/issues/4420)) +- `mod_matrix_gw`: Matrix gateway updates +- `mod_muc`: Report db failures when restoring rooms +- `mod_muc`: Unsubscribe users from members-only rooms when expelled ([#4412](https://github.com/processone/ejabberd/issues/4412)) +- `mod_providers`: New module to serve easily XMPP Providers files +- `mod_register`: Don't duplicate welcome subject and message +- `mod_scram_upgrade`: Fix format of passwords updates +- `mod_scram_upgrade`: Only offer upgrades to methods that aren't already stored + +## Version 25.07 + +#### Security fix + +- `ext_mod`: Add temporary workaround for zip including absolute path + +#### Compilation + +- Raise the minimum Elixir tested version to 1.14.0 ([#4281](https://github.com/processone/ejabberd/issues/4281)) +- Raise Erlang/OTP minimum requirement to 25.0 ([#4281](https://github.com/processone/ejabberd/issues/4281)) +- `configure.ac`: Allow to specify minimal erlang version using `--with-min-erlang` +- `Makefile.in`: Add target `test-` +- `rebar3-format.sh`: Replace csplit with perl +- Container: Bump Erlang/OTP 27.3.4.1, Elixir 1.18.4 +- Installers: Bump Erlang/OTP 27.3.4.1, Elixir 1.18.4, libexpat 2.7.1, OpenSSL 3.5.1 + +#### Configuration and Tests + +- Add `rest_proxy*` options to configure proxy used by rest module +- `ejabberd_c2s`: Add `auth_password_types_hidden_in_scram1` option +- `ejabberd_http`: Remove unused `default_host` option and state element +- `ejabberd_http`: New option `hosts_alias` and function `resolve_host_alias/1` ([#4400](https://github.com/processone/ejabberd/issues/4400)) +- New predefined keywords: `CONFIG_PATH` and `LOG_PATH` +- Fix macro used in string options when defined in env var +- Use auxiliary function to get `$HOME`, use Mnesia directory when not set ([#4402](https://github.com/processone/ejabberd/issues/4402)) +- `ejabberd_config`: Better `lists:uniq` substitute +- Tests: update readme and compose to work with current sw versions +- Update Elvis to 4.1.1, fix some warnings and enable their tests + +#### Erlang/OTP 28 support + +- Add workaround in `p1_acme` for Jose 1.11.10 not supporting OTP 28 `ecPrivkeyVer1` ([#4393](https://github.com/processone/ejabberd/issues/4393)) +- Bump `fast_xml` and `xmpp` for improved Erlang/OTP 28 support +- Bump `xmpp` and `p1_acme` patched with Erlang/OTP 28 support +- Fix `make options` in Erlang/OTP 28 ([#4352](https://github.com/processone/ejabberd/issues/4352)) +- Fix crash in `rebar3 cover` with Erlang/OTP 28 ([#4353](https://github.com/processone/ejabberd/issues/4353)) +- Rebar/Rebar3: Update binaries to work with Erlang/OTP 25-28 ([#4354](https://github.com/processone/ejabberd/issues/4354)) +- CI and Runtime: Add Erlang/OTP 28 to the versions matrix + +#### SQL + +- Fix mnesia to sql exporter after changes to auth tables +- Update code for switching to new schema type to users table changes +- Add mssql specific implementation of `delete_old_mam_messages` +- Make `delete_old_mam_messages_batch` work with sqlite +- `ejabberd_sm_sql`: Use misc:encode_pid/1 +- `mysql.sql`: Fix typo in commit 7862c6a when creating users table +- `pg.sql`: Fix missing comma in postgres schema ([#4409](https://github.com/processone/ejabberd/issues/4409)) + +#### Core and Modules + +- `ejabberd_s2s_in`: Allow S2S connections to accept client certificates that have only server purpose ([#4392](https://github.com/processone/ejabberd/issues/4392)) +- `ext_mod`: Recommend to write README.md instead txt (processone/ejabberd-contrib#363) +- `ext_mod`: Support library path installed from Debian (processone/ejabberd-contrib#363) +- `ext_mod`: When upgrading module, clean also the compiled directories +- `gen_mod`: Add support to prepare module stopping before actually stopping any module +- `mod_antispam`: Imported from ejabberd-contrib and improved ([#4373](https://github.com/processone/ejabberd/issues/4373)) +- `mod_auth_fast`: Clear tokens on kick, change pass and unregister ([#4397](https://github.com/processone/ejabberd/issues/4397))([#4398](https://github.com/processone/ejabberd/issues/4398))([#4399](https://github.com/processone/ejabberd/issues/4399)) +- `mod_conversejs`: Add link in WebAdmin to local Converse if configured +- `mod_mam`: Present mam full text search in xep-431 compatible way +- `mod_mam_mnesia`: Handle objects that don't need conversion in `transform/0` +- `mod_matrix_gw`: Don't send empty messages in Matrix rooms ([#4385](https://github.com/processone/ejabberd/issues/4385)) +- `mod_matrix_gw`: Support older Matrix rooms versions starting from version 4 +- `mod_matrix_gw`: When encoding JSON, handle term that is key-value list ([#4379](https://github.com/processone/ejabberd/issues/4379)) +- `mod_matrix_gw_s2s`: Fix key validation in `check_signature` +- `mod_mix` and `mod_muc_rtbl`: Support list of IDs in `pubsub-items-retract` (processone/xmpp#100) +- `mod_pubsub_serverinfo`: Imported module from ejabberd-contrib ([#4408](https://github.com/processone/ejabberd/issues/4408)) +- `mod_register`: Normalize username when determining if user want to change pass +- `mod_register`: Strip query data when returning errors +- WebAdmin: New hooks `webadmin_menu_system` to add items to system menu + +## Version 25.04 + +#### Security fixes +- Fixes issue with handling of user provided occupant-id in messages and presences sent to muc room. Server was replacing + just first instance of occupant-id with its own version, leaving other ones untouched. That would mean that depending + on order in which clients send occupant-id, they could see value provided by sender, and that could be used to spoof + as different sender. + +#### Commands API +- `kick_users`: New command to kick all logged users for a given host + +#### Bugfixes +- Fix issue with sql schema auto upgrade when using `sqlite` database +- Fix problem with container update, that could ignore previous data stored in `mnesia` database +- Revert limit of allowed characters in shared roster group names, that will again allow using symbols like `:` + +## Version 25.03 + +#### Commands API +- `ejabberdctl`: New option `CTL_OVER_HTTP` ([#4340](https://github.com/processone/ejabberd/issues/4340)) +- `ejabberd_web_admin`: Support commands with tuple arguments +- `mod_adhoc_api`: New module to execute API Commands using Ad-Hoc Commands ([#4357](https://github.com/processone/ejabberd/issues/4357)) +- `mod_http_api`: Sort list elements in a command result +- Show warning when registering command with an existing name +- Fix commands unregistration +- `change_room_option`: Add forgotten support to set `enable_hats` room option +- `change_room_option`: Verify room option value before setting it ([#4337](https://github.com/processone/ejabberd/issues/4337)) +- `create_room_with_opts`: Recommend using `;` and `=` separators +- `list_cluster_detailed`: Fix crash when a node is down +- `mnesia_list_tables`: Allow using this internal command +- `mnesia_table_change_storage`: Allow using this internal command +- `status`: Separate command result with newline +- `update_sql`: Fix updating tables created by ejabberd internally +- `update_sql`: Fix MySQL support + +#### Configuration +- `acl`: Fix bug matching the acl `shared_group: NAME` +- `define_keyword`: New option to define keywords ([#4350](https://github.com/processone/ejabberd/issues/4350)) +- `define_macro`: Add option to `globals()` because it's useless inside `host_config` +- `ejabberd.yml.example`: Enable `mod_muc_occupantid` by default +- Add support to use keywords in toplevel, listener and modules +- Show warning also when deprecated listener option is set as disabled ([#4345](https://github.com/processone/ejabberd/issues/4345)) + +#### Container +- Bump versions to Erlang/OTP 27.3 and Elixir 1.18.3 +- Add `ERL_FLAGS` to compile elixir on qemu cross-platform +- Copy files to stable path, add ecs backwards compatibility +- Fix warning about relative workdir +- Improve entrypoint script: register account, or set random +- Link path to Mnesia spool dir for backwards compatibility +- Place `sockets/` outside `database/` +- Use again direct METHOD, qemu got fixed ([#4280](https://github.com/processone/ejabberd/issues/4280)) +- `ejabberd.yml.example`: Copy main example configuration file +- `ejabberd.yml.example`: Define and use macros in the default configuration file +- `ejabberd.yml.example`: Enable `CTL_OVER_HTTP` by default +- `ejabberd.yml.example`: Listen for webadmin in a port number lower than any other +- `ejabberdapi`: Compile during build +- `CONTAINER.md`: Include documentation for ecs container image + +#### Core and Modules +- `ejabberd_auth`: Add support for `auth_stored_password_types` +- `ejabberd_router`: Don't rewrite "self-addressed" privileged IQs as results ([#4348](https://github.com/processone/ejabberd/issues/4348)) +- `misc`: Fix json version of `json_encode_with_kv_list` for nested kv lists ([#4338](https://github.com/processone/ejabberd/issues/4338)) +- OAuth: Fix crashes when oauth is feed with invalid jid ([#4355](https://github.com/processone/ejabberd/issues/4355)) +- PubSub: Bubble up db errors in `nodetree_tree_sql:set_node` +- `mod_configure`: Add option `access` to let configure the access name +- `mod_mix_pam`: Remove `Channels` roster group of mix channels ([#4297](https://github.com/processone/ejabberd/issues/4297)) +- `mod_muc`: Document MUC room option vcard_xupdate +- `mod_privilege`: Accept non-privileged IQs from privileged components ([#4341](https://github.com/processone/ejabberd/issues/4341)) +- `mod_private`: Improve exception handling +- `mod_private`: Don't warn on conversion errors +- `mod_private`: Handle invalid PEP-native bookmarks +- `mod_private`: Don't crash on invalid bookmarks +- `mod_s2s_bidi`: Stop processing other handlers in s2s_in_handle_info ([#4344](https://github.com/processone/ejabberd/issues/4344)) +- `mod_s2s_bidi`: Fix issue with wrong namespace + +#### Dependencies +- `ex_doc`: Bump to 0.37.2 +- `stringprep`: Bump to 1.0.31 +- `provider_asn1`: Bump to 0.4.1 +- `xmpp` Bump to bring fix for ssdp hash calculation +- `xmpp` Bump to get support for webchat_url ([#3041](https://github.com/processone/ejabberd/issues/3041)) +- `xmpp` Bump to get XEP-0317 Hats namespaces version 0.2.0 +- `xmpp` Bump to bring SSDP to XEP version 0.4 +- `yconf` Bump to support macro inside string + +#### Development and Testing +- `mix.exs`: Keep debug info when building `dev` release +- `mix.exs`: The `ex_doc` dependency is only relevant for the `edoc` Mix environment +- `ext_mod`: add `$libdir/include` to include path +- `ext_mod`: fix greedy include path ([#4359](https://github.com/processone/ejabberd/issues/4359)) +- `gen_mod`: Support registering commands and `hook_subscribe` in `start/2` result +- `c2s_handle_bind`: New event in `ejabberd_c2s` ([#4356](https://github.com/processone/ejabberd/issues/4356)) +- `muc_disco_info_extras`: New event `mod_muc_room` useful for `mod_muc_webchat_url` ([#3041](https://github.com/processone/ejabberd/issues/3041)) +- VSCode: Fix compiling support +- Add tests for config features `define_macro` and `define_keyword` +- Allow test to run using `ct_run` +- Fixes to handle re-running test after `update_sql` +- Uninstall `mod_example` when the tests has finished + +#### Documentation +- Add XEPs that are indirectly supported and required by XEP-0479 +- Document that XEP-0474 0.4.0 was recently upgraded +- Don't use backtick quotes for ejabberd name +- Fix values allowed in db_type of mod_auth_fast documentation +- Reword explanation about ACL names and definitions +- Update moved or broken URLs in documentation + +#### Installers +- Bump Erlang/OTP 27.3 and Elixir 1.18.3 +- Bump OpenSSL 3.4.1 +- Bump crosstool-NG 1.27.0 +- Fix building Termcap and Linux-PAM + +#### Matrix Gateway +- Preserve XMPP message IDs in Matrix rooms +- Better Matrix room topic and room roles to MUC conversion, support room aliases in invites +- Add `muc#user` element to presences and an initial empty subject +- Fix `gen_iq_handler:remove_iq_handler` call +- Properly handle IQ requests +- Support Matrix room aliases +- Fix handling of 3PI events + +#### Unix Domain Socket +- Add support for socket relative path +- Use `/tmp` for temporary socket, as path is restricted to 107 chars +- Handle unix socket when logging remote client +- When stopping listener, delete Unix Domain Socket file +- `get_auto_url` option: Don't build auto URL if port is unix domain socket ([#4345](https://github.com/processone/ejabberd/issues/4345)) + +## Version 24.12 + +#### Miscelanea + +- Elixir: support loading Elixir modules for auth ([#4315](https://github.com/processone/ejabberd/issues/4315)) +- Environment variables `EJABBERD_MACRO` to define macros +- Fix problem starting ejabberd when first host uses SQL, other one mnesia +- HTTP Websocket: Enable `allow_unencrypted_sasl2` on websockets ([#4323](https://github.com/processone/ejabberd/issues/4323)) +- Relax checks for channels bindings for connections using external encryption +- Redis: Add support for unix domain socket ([#4318](https://github.com/processone/ejabberd/issues/4318)) +- Redis: Use eredis 1.7.1 from Nordix when using mix/rebar3 and Erlang 21+ +- `mod_auth_fast`: New module with support XEP-0484: Fast Authentication Streamlining Tokens +- `mod_http_api`: Fix crash when module not enabled (for example, in CT tests) +- `mod_http_api`: New option `default_version` +- `mod_muc`: Make rsm handling in disco items, correctly count skipped rooms +- `mod_offline`: Only delete offline msgs when user has MAM enabled ([#4287](https://github.com/processone/ejabberd/issues/4287)) +- `mod_priviled`: Handle properly roster iq +- `mod_pubsub`: Send notifications on PEP item retract +- `mod_s2s_bidi`: Catch extra case in check for s2s bidi element +- `mod_scram_upgrade`: Don't abort the upgrade +- `mod_shared_roster`: The name of a new group is lowercased +- `mod_shared_roster`: Get back support for `groupid@vhost` in `displayed` +- `mod_stun_disco`: Fix syntax of credentials response + +#### Commands API + +- Change arguments and result to consistent names (API v3) +- `create_rooms_file`: Improve to support vhosts with different config +- `evacuate_kindly`: New command to kick users and prevent login ([#4309](https://github.com/processone/ejabberd/issues/4309)) +- `join_cluster`: Explain that this returns immediately (since 5a34020, 24.06) +- `mod_muc_admin`: Rename argument `name` to `room` for consistency + +#### Documentation + +- Fix some documentation syntax, add links to toplevel, modules and API +- `CONTAINER.md`: Add kubernetes yaml examples to use with podman +- `SECURITY.md`: Add security policy and reporting guidelines +- `ejabberd.service`: Disable the systemd watchdog by default +- `ejabberd.yml.example`: Use non-standard STUN port + +#### WebAdmin + +- Shared group names are case sensitive, use original case instead of lowercase +- Use lowercase username and server authentication credentials +- Fix calculation of node's uptime days +- Fix link to displayed group when it is from another vhost + +## Version 24.10 + +#### Miscelanea + +- `ejabberd_c2s`: Optionally allow unencrypted SASL2 +- `ejabberd_system_monitor`: Handle call by `gen_event:swap_handler` ([#4233](https://github.com/processone/ejabberd/issues/4233)) +- `ejabberd_http_ws`: Remove support for old websocket connection protocol +- `ejabberd_stun`: Omit `auth_realm` log message +- `ext_mod`: Handle `info` message when contrib module transfers table ownership +- `mod_block_strangers`: Add feature announcement to disco-info ([#4039](https://github.com/processone/ejabberd/issues/4039)) +- `mod_mam`: Advertise XEP-0424 feature in server disco-info ([#3340](https://github.com/processone/ejabberd/issues/3340)) +- `mod_muc_admin`: Better handling of malformed jids in `send_direct_invitation` command +- `mod_muc_rtbl`: Fix call to `gen_server:stop` ([#4260](https://github.com/processone/ejabberd/issues/4260)) +- `mod_privilege`: Support "IQ permission" from XEP-0356 0.4.1 ([#3889](https://github.com/processone/ejabberd/issues/3889)) +- `mod_pubsub`: Don't blindly echo PEP notification +- `mod_pubsub`: Skip non-delivery errors for local pubsub generated notifications +- `mod_pubsub`: Fall back to default plugin options +- `mod_pubsub`: Fix choice of node config defaults +- `mod_pubsub`: Fix merging of default node options +- `mod_pubsub`: Fix default node config parsing +- `mod_register`: Support to block IPs in a vhost using `append_host_config` ([#4038](https://github.com/processone/ejabberd/issues/4038)) +- `mod_s2s_bidi`: Add support for S2S Bidirectional +- `mod_scram_upgrade`: Add support for SCRAM upgrade tasks +- `mod_vcard`: Return error stanza when storage doesn't support vcard update ([#4266](https://github.com/processone/ejabberd/issues/4266)) +- `mod_vcard`: Return explicit error stanza when user attempts to modify other's vcard +- Minor improvements to support `mod_tombstones` (#2456) +- Update `fast_xml` to use `use_maps` and remove obsolete elixir files +- Update `fast_tls` and `xmpp` to improve s2s fallback for invalid direct tls connections +- `make-binaries`: Bump dependency versions: Elixir 1.17.2, OpenSSL 3.3.2, ... + +#### Administration + +- `ejabberdctl`: If `ERLANG_NODE` lacks host, add hostname ([#4288](https://github.com/processone/ejabberd/issues/4288)) +- `ejabberd_app`: At server start, log Erlang and Elixir versions +- MySQL: Fix column type in the schema update of `archive` table in schema update + +#### Commands API + +- `get_mam_count`: New command to get number of archived messages for an account +- `set_presence`: Return error when session not found +- `update`: Fix command output +- Add `mam` and `offline` tags to the related purge commands + +#### Code Quality + +- Fix warnings about unused macro definitions reported by Erlang LS +- Fix Elvis report: Fix dollar space syntax +- Fix Elvis report: Remove spaces in weird places +- Fix Elvis report: Don't use ignored variables +- Fix Elvis report: Remove trailing whitespace characters +- Define the types of options that `opt_type.sh` cannot derive automatically +- `ejabberd_http_ws`: Fix dialyzer warnings +- `mod_matrix_gw`: Remove useless option `persist` +- `mod_privilege`: Replace `try...catch` with a clean alternative + +#### Development Help + +- `elvis.config`: Fix file syntax, set vim mode, disable many tests +- `erlang_ls.config`: Let it find paths, update to Erlang 26, enable crossref +- `hooks_deps`: Hide false-positive warnings about `gen_mod` +- `Makefile`: Add support for `make elvis` when using rebar3 +- `.vscode/launch.json`: Experimental support for debugging with Neovim +- CI: Add Elvis tests +- CI: Add XMPP Interop tests +- Runtime: Cache hex.pm archive from rebar3 and mix + +#### Documentation + +- Add links in top-level options documentation to their Docs website sections +- Document which SQL servers can really use `update_sql_schema` +- Improve documentation of `ldap_servers` and `ldap_backups` options ([#3977](https://github.com/processone/ejabberd/issues/3977)) +- `mod_register`: Document behavior when `access` is set to `none` ([#4078](https://github.com/processone/ejabberd/issues/4078)) + +#### Elixir + +- Handle case when elixir support is enabled but not available +- Start ExSync manually to ensure it's started if (and only if) Relive +- `mix.exs`: Fix `mix release` error: `logger` being regular and included application ([#4265](https://github.com/processone/ejabberd/issues/4265)) +- `mix.exs`: Remove from `extra_applications` the apps already defined in `deps` ([#4265](https://github.com/processone/ejabberd/issues/4265)) + +#### WebAdmin + +- Add links in user page to offline and roster pages +- Add new "MAM Archive" page to webadmin +- Improve many pages to handle when modules are disabled +- `mod_admin_extra`: Move some webadmin pages to their modules + +## Version 24.07 + +#### Core + +- `ejabberd_options`: Add trailing `@` to `@VERSION@` parsing +- `mod_http_api`: Fix problem parsing tuples when using OTP 27 json library ([#4242](https://github.com/processone/ejabberd/issues/4242)) +- `mod_http_api`: Restore args conversion of `{"k":"v"}` to tuple lists +- `mod_matrix_gw`: Add misc:json_encode_With_kv_lists and use it in matrix sign function +- `mod_muc`: Output `muc#roominfo_avatarhash` in room disco info as per updated XEP-0486 ([#4234](https://github.com/processone/ejabberd/issues/4234)) +- `mod_muc`: Improve cross version handling of muc retractions +- `node_pep`: Add missing feature `item-ids` to node_pep +- `mod_register`: Send welcome message as `chat` too ([#4246](https://github.com/processone/ejabberd/issues/4246)) +- `ejabberd_hooks`: Support for ejabberd hook subscribers, useful for [mod_prometheus](https://github.com/processone/ejabberd-contrib/tree/master/mod_prometheus) +- `ejabberd.app`: Don't add `iex` to included_applications +- `make-installers`: Fix path in scripts in regular user install ([#4258](https://github.com/processone/ejabberd/issues/4258)) +- Test: New tests for API commands + +#### Documentation + +- `mod_matrix_gw`: Fix `matrix_id_as_jid` option documentation +- `mod_register`: Add example configuration of `welcome_message` option +- `mix.exs`: Add ejabberd example config files to the hex package +- Update `CODE_OF_CONDUCT.md` + +#### ext_mod + +- Fetch dependencies from hex.pm when mix is available +- files_to_path is deprecated, use compile_to_path +- Compile all Elixir files in a library with one function call +- Improve error result when problem compiling elixir file +- Handle case when contrib module has no `*.ex` and no `*.erl` +- `mix.exs`: Include Elixir's Logger in the OTP release, useful for [mod_libcluster](https://github.com/processone/ejabberd-contrib/tree/master/mod_libcluster) + +#### Logs + +- Print message when starting ejabberd application fails +- Use error_logger when printing startup failure message +- Use proper format depending on the formatter ([#4256](https://github.com/processone/ejabberd/issues/4256)) + +#### SQL + +- Add option `update_sql_schema_timeout` to allow schema update use longer timeouts +- Add ability to specify custom timeout for sql operations +- Allow to configure number of restart in `sql_transaction()` +- Make sql query in testsuite compatible with pg9.1 +- In `mysql.sql`, fix update instructions for the `archive` table, `origin_id` column ([#4259](https://github.com/processone/ejabberd/issues/4259)) + +#### WebAdmin + +- `ejabberd.yml.example`: Add `api_permissions` group for webadmin ([#4249](https://github.com/processone/ejabberd/issues/4249)) +- Don't use host from url in webadmin, prefer host used for authentication +- Fix number of accounts shown in the online-users page +- Fix crash when viewing old shared roster groups ([#4245](https://github.com/processone/ejabberd/issues/4245)) +- Support groupid with spaces when making shared roster result ([#4245](https://github.com/processone/ejabberd/issues/4245)) + +## Version 24.06 + +#### Core + +- `econf`: Add ability to use additional custom errors when parsing options +- `ejabberd_logger`: Reloading configuration will update logger settings +- `gen_mod`: Add support to specify a hook global, not vhost-specific +- `mod_configure`: Retract `Get User Password` command to update XEP-0133 1.3.0 +- `mod_conversejs`: Simplify support for `@HOST@` in `default_domain` option ([#4167](https://github.com/processone/ejabberd/issues/4167)) +- `mod_mam`: Document that XEP-0441 is implemented as well +- `mod_mam`: Update support for XEP-0425 version 0.3.0, keep supporting 0.2.1 ([#4193](https://github.com/processone/ejabberd/issues/4193)) +- `mod_matrix_gw`: Fix support for `@HOST@` in `matrix_domain` option ([#4167](https://github.com/processone/ejabberd/issues/4167)) +- `mod_muc_log`: Hide join/leave lines, add method to show them +- `mod_muc_log`: Support `allowpm` introduced in 2bd61ab +- `mod_muc_room`: Use ejabberd hooks instead of function calls to `mod_muc_log` ([#4191](https://github.com/processone/ejabberd/issues/4191)) +- `mod_private`): Cope with bookmark decoding errors +- `mod_vcard_xupdate`: Send hash after avatar get set for first time +- `prosody2ejabberd`: Handle the `approved` attribute. As feature isn't implemented, discard it ([#4188](https://github.com/processone/ejabberd/issues/4188)) + +#### SQL + +- `update_sql_schema`: Enable this option by default +- CI: Don't load database schema files for mysql and pgsql +- Support Unix Domain Socket with updated p1_pgsql and p1_mysql ([#3716](https://github.com/processone/ejabberd/issues/3716)) +- Fix handling of `mqtt_pub` table definition from `mysql.sql` and fix `should_update_schema/1` in `ejabberd_sql_schema.erl` +- Don't start sql connection pools for unknown hosts +- Add `update_primary_key` command to sql schema updater +- Fix crash running `export2sql` when MAM enabled but MUC disabled +- Improve detection of types in odbc + +#### Commands API + +- New ban commands use private storage to keep ban information ([#4201](https://github.com/processone/ejabberd/issues/4201)) +- `join_cluster_here`: New command to join a remote node into our local cluster +- Don't name integer and string results in API examples ([#4198](https://github.com/processone/ejabberd/issues/4198)) +- `get_user_subscriptions`: Fix validation of user field in that command +- `mod_admin_extra`: Handle case when `mod_private` is not enabled ([#4201](https://github.com/processone/ejabberd/issues/4201)) +- `mod_muc_admin`: Improve validation of arguments in several commands + +#### Compile + +- `ejabberdctl`: Comment ERTS_VSN variable when not used ([#4194](https://github.com/processone/ejabberd/issues/4194)) +- `ejabberdctl`: Fix iexlive after `make prod` when using Elixir +- `ejabberdctl`: If `INET_DIST_INTERFACE` is IPv6, set required option ([#4189](https://github.com/processone/ejabberd/issues/4189)) +- `ejabberdctl`: Make native dynamic node names work when using fully qualified domain names +- `rebar.config.script`: Support relaxed dependency version ([#4192](https://github.com/processone/ejabberd/issues/4192)) +- `rebar.config`: Update deps version to rebar3's relaxed versioning +- `rebar.lock`: Track file, now that rebar3 uses loose dependency versioning +- `configure.ac`: When using rebar3, unlock dependencies that are disabled ([#4212](https://github.com/processone/ejabberd/issues/4212)) +- `configure.ac`: When using rebar3 with old Erlang, unlock some dependencies ([#4213](https://github.com/processone/ejabberd/issues/4213)) +- `mix:exs`: Move `xmpp` from `included_applications` to `applications` + +#### Dependencies + +- Base64url: Use only when using rebar2 and Erlang lower than 24 +- Idna: Bump from 6.0.0 to 6.1.1 +- Jiffy: Use Json module when Erlang/OTP 27, jiffy with older ones +- Jose: Update to the new 1.11.10 for Erlang/OTP higher than 23 +- Luerl: Update to 1.2.0 when OTP same or higher than 20, simplifies commit a09f222 +- P1_acme: Update to support Jose 1.11.10 and Ipv6 support ([#4170](https://github.com/processone/ejabberd/issues/4170)) +- P1_acme: Update to use Erlang's json library instead of jiffy when OTP 27 +- Port_compiler: Update to 1.15.0 that supports Erlang/OTP 27.0 + +#### Development Help + +- `.gitignore`: Ignore ctags/etags files +- `make dialyzer`: Add support to run Dialyzer with Mix +- `make format|indent`: New targets to format and indent source code +- `make relive`: Add Sync tool with Rebar3, ExSync with Mix +- `hook_deps`: Use precise name: hooks are added and later deleted, not removed +- `hook_deps`: Fix to handle FileNo as tuple `{FileNumber, CharacterPosition}` +- Add support to test also EUnit suite +- Fix `code:lib_dir` call to work with Erlang/OTP 27.0-rc2 +- Set process flags when Erlang/OTP 27 to help debugging +- Test retractions in mam_tests + +#### Documentation + +- Add some XEPs support that was forgotten +- Fix documentation links to new URLs generated by MkDocs +- Remove `...` in example configuration: it is assumed and reduces verbosity +- Support for version note in modules too +- Mark toplevel options, commands and modules that changed in latest version +- Now modules themselves can have version annotations in `note` + +#### Installers and Container + +- make-binaries: Bump Erlang/OTP to 26.2.5 and Elixir 1.16.3 +- make-binaries: Bump OpenSSL to 3.3.1 +- make-binaries: Bump Linux-PAM to 1.6.1 +- make-binaries: Bump Expat to 2.6.2 +- make-binaries: Revert temporarily an OTP commit that breaks MSSQL ([#4178](https://github.com/processone/ejabberd/issues/4178)) +- CONTAINER.md: Invalid `CTL_ON_CREATE` usage in docker-compose example + +#### WebAdmin + +- ejabberd_ctl: Improve parsing of commas in arguments +- ejabberd_ctl: Fix output of UTF-8-encoded binaries +- WebAdmin: Remove webadmin_view for now, as commands allow more fine-grained permissions +- WebAdmin: Unauthorized response: include some text to direct to the logs +- WebAdmin: Improve home page +- WebAdmin: Sort alphabetically the menu items, except the most used ones +- WebAdmin: New login box in the left menu bar +- WebAdmin: Add make_command functions to produce HTML command element +- Document 'any' argument and result type, useful for internal commands +- Commands with 'internal' tag: don't list and block execution by frontends +- WebAdmin: Move content to commands; new pages; hook changes; new commands + +## Version 24.02 + +#### Core: + +- Added Matrix gateway in `mod_matrix_gw` +- Support SASL2 and Bind2 +- Support tls-server-end-point channel binding and sasl2 codec +- Support tls-exporter channel binding +- Support XEP-0474: SASL SCRAM Downgrade Protection +- Fix presenting features and returning results of inline bind2 elements +- `disable_sasl_scram_downgrade_protection`: New option to disable XEP-0474 +- `negotiation_timeout`: Increase default value from 30s to 2m +- mod_carboncopy: Teach how to interact with bind2 inline requests + +#### Other: + +- ejabberdctl: Fix startup problem when having set `EJABBERD_OPTS` and logger options +- ejabberdctl: Set EJABBERD_OPTS back to `""`, and use previous flags as example +- eldap: Change logic for `eldap tls_verify=soft` and `false` +- eldap: Don't set `fail_if_no_peer_cert` for eldap ssl client connections +- Ignore hints when checking for chat states +- mod_mam: Support XEP-0424 Message Retraction +- mod_mam: Fix XEP-0425: Message Moderation with SQL storage +- mod_ping: Support XEP-0198 pings when stream management is enabled +- mod_pubsub: Normalize pubsub `max_items` node options on read +- mod_pubsub: PEP nodetree: Fix reversed logic in node fixup function +- mod_pubsub: Only care about PEP bookmarks options when creating node from scratch + +#### SQL: + +- MySQL: Support `sha256_password` auth plugin +- ejabberd_sql_schema: Use the first unique index as a primary key +- Update SQL schema files for MAM's XEP-0424 +- New option [`sql_flags`](https://docs.ejabberd.im/admin/configuration/toplevel/#sql-flags): right now only useful to enable `mysql_alternative_upsert` + +#### Installers and Container: + +- Container: Add ability to ignore failures in execution of `CTL_ON_*` commands +- Container: Update to Erlang/OTP 26.2, Elixir 1.16.1 and Alpine 3.19 +- Container: Update this custom ejabberdctl to match the main one +- make-binaries: Bump OpenSSL 3.2.1, Erlang/OTP 26.2.2, Elixir 1.16.1 +- make-binaries: Bump many dependency versions + +#### Commands API: + +- `print_sql_schema`: New command available in ejabberdctl command-line script +- ejabberdctl: Rework temporary node name generation +- ejabberdctl: Print argument description, examples and note in help +- ejabberdctl: Document exclusive ejabberdctl commands like all the others +- Commands: Add a new `muc_sub` tag to all the relevant commands +- Commands: Improve syntax of many commands documentation +- Commands: Use list arguments in many commands that used separators +- Commands: `set_presence`: switch priority argument from string to integer +- ejabberd_commands: Add the command API version as [a tag `vX`](https://docs.ejabberd.im/developer/ejabberd-api/admin-tags/#v1) +- ejabberd_ctl: Add support for list and tuple arguments +- ejabberd_xmlrpc: Fix support for restuple error response +- mod_http_api: When no specific API version is requested, use the latest + +#### Compilation with Rebar3/Elixir/Mix: +- Fix compilation with Erlang/OTP 27: don't use the reserved word 'maybe' +- configure: Fix explanation of `--enable-group` option ([#4135](https://github.com/processone/ejabberd/issues/4135)) +- Add observer and runtime_tools in releases when `--enable-tools` +- Update "make translations" to reduce build requirements +- Use Luerl 1.0 for Erlang 20, 1.1.1 for 21-26, and temporary fork for 27 +- Makefile: Add `install-rel` and `uninstall-rel` +- Makefile: Rename `make rel` to `make prod` +- Makefile: Update `make edoc` to use ExDoc, requires mix +- Makefile: No need to use `escript` to run rebar|rebar3|mix +- configure: If `--with-rebar=rebar3` but rebar3 not system-installed, use local one +- configure: Use Mix or Rebar3 by default instead of Rebar2 to compile ejabberd +- ejabberdctl: Detect problem running iex or etop and show explanation +- Rebar3: Include Elixir files when making a release +- Rebar3: Workaround to fix protocol consolidation +- Rebar3: Add support to compile Elixir dependencies +- Rebar3: Compile explicitly our Elixir files when `--enable-elixir` +- Rebar3: Provide proper path to `iex` +- Rebar/Rebar3: Update binaries to work with Erlang/OTP 24-27 +- Rebar/Rebar3: Remove Elixir as a rebar dependency +- Rebar3/Mix: If `dev` profile/environment, enable tools automatically +- Elixir: Fix compiling ejabberd as a dependency ([#4128](https://github.com/processone/ejabberd/issues/4128)) +- Elixir: Fix ejabberdctl start/live when installed +- Elixir: Fix: `FORMATTER ERROR: bad return value` ([#4087](https://github.com/processone/ejabberd/issues/4087)) +- Elixir: Fix: Couldn't find file `Elixir Hex API` +- Mix: Enable stun by default when `vars.config` not found +- Mix: New option `vars_config_path` to set path to `vars.config` ([#4128](https://github.com/processone/ejabberd/issues/4128)) +- Mix: Fix ejabberdctl iexlive problem locating iex in an OTP release + +## Version 23.10 + +#### Compilation: + +- Erlang/OTP: Raise the requirement to Erlang/OTP 20.0 as a minimum +- CI: Update tests to Erlang/OTP 26 and recent Elixir +- Move Xref and Dialyzer options from workflows to `rebar.config` +- Add sections to `rebar.config` to organize its content +- Dialyzer dirty workarounds because `re:mp()` is not an exported type +- When installing module already configured, keep config as example +- Elixir 1.15 removed support for `--app` +- Elixir: Improve support to stop external modules written in Elixir +- Elixir: Update syntax of function calls as recommended by Elixir compiler +- Elixir: When building OTP release with mix, keep `ERLANG_NODE=ejabberd@localhost` +- `ejabberdctl`: Pass `ERLANG_OPTS` when calling `erl` to parse the `INET_DIST_INTERFACE` ([#4066](https://github.com/processone/ejabberd/issues/#4066) + +#### Commands: + +- `create_room_with_opts`: Fix typo and move examples to `args_example` ([#4080](https://github.com/processone/ejabberd/issues/#4080)) +- `etop`: Let `ejabberdctl etop` work in a release (if `observer` application is available) +- `get_roster`: Command now returns groups in a list instead of newlines ([#4088](https://github.com/processone/ejabberd/issues/#4088)) +- `halt`: New command to halt ejabberd abruptly with an error status code +- `ejabberdctl`: Fix calling ejabberdctl command with wrong number of arguments with Erlang 26 +- `ejabberdctl`: Improve printing lists in results +- `ejabberdctl`: Support `policy=user` in the help and return proper arguments +- `ejabberdctl`: Document how to stop a debug shell: control+g + +#### Container: + +- Dockerfile: Add missing dependency for mssql databases +- Dockerfile: Reorder stages and steps for consistency +- Dockerfile: Use Alpine as base for `METHOD=package` +- Dockerfile: Rename packages to improve compatibility +- Dockerfile: Provide specific OTP and elixir vsn for direct compilation +- Halt ejabberd if a command in `CTL_ON_` fails during ejabberd startup + +#### Core: + +- `auth_external_user_exists_check`: New option ([#3377](https://github.com/processone/ejabberd/issues/#3377)) +- `gen_mod`: Extend `gen_mod` API to simplify hooks and IQ handlers registration +- `gen_mod`: Add shorter forms for `gen_mod` hook/`iq_handler` API +- `gen_mod`: Update modules to the new `gen_mod` API +- `install_contrib_modules`: New option to define contrib modules to install automatically +- `unix_socket`: New listener option, useful when setting unix socket files ([#4059](https://github.com/processone/ejabberd/issues/#4059)) +- `ejabberd_systemd`: Add a few debug messages +- `ejabberd_systemd`: Avoid using `gen_server` timeout ([#4054](https://github.com/processone/ejabberd/issues/#4054))([#4058](https://github.com/processone/ejabberd/issues/#4058)) +- `ejabberd_listener`: Increase default listen queue backlog value to 128, which is the default value on both Linux and FreeBSD ([#4025](https://github.com/processone/ejabberd/issues/#4025)) +- OAuth: Handle `badpass` error message +- When sending message on behalf of user, trigger `user_send_packet` ([#3990](https://github.com/processone/ejabberd/issues/#3990)) +- Web Admin: In roster page move the `AddJID` textbox to top ([#4067](https://github.com/processone/ejabberd/issues/#4067)) +- Web Admin: Show a warning when visiting webadmin with non-privileged account ([#4089](https://github.com/processone/ejabberd/issues/#4089)) + +#### Docs: + +- Example configuration: clarify 5223 tls options; specify s2s shaper +- Make sure that `policy=user` commands have `host` instead of `server` arg in docs +- Improve syntax of many command descriptions for the Docs site +- Move example Perl extauth script from ejabberd git to Docs site +- Remove obsolete example files, and add link in Docs to the archived copies + +#### Installers (`make-binaries`): +- Bump Erlang/OTP version to 26.1.1, and other dependencies +- Remove outdated workaround +- Don't build Linux-PAM examples +- Fix check for current Expat version +- Apply minor simplifications +- Don't duplicate config entries +- Don't hard-code musl version +- Omit unnecessary glibc setting +- Set kernel version for all builds +- Let curl fail on HTTP errors + +#### Modules: + +- `mod_muc_log`: Add trailing backslash to URLs shown in disco info +- `mod_muc_occupantid`: New module with support for XEP-0421 Occupant Id ([#3397](https://github.com/processone/ejabberd/issues/#3397)) +- `mod_muc_rtbl`: Better error handling in ([#4050](https://github.com/processone/ejabberd/issues/#4050)) +- `mod_private`: Add support for XEP-0402 PEP Native Bookmarks +- `mod_privilege`: Don't fail to edit roster ([#3942](https://github.com/processone/ejabberd/issues/#3942)) +- `mod_pubsub`: Fix usage of `plugins` option, which produced `default_node_config` ignore ([#4070](https://github.com/processone/ejabberd/issues/#4070)) +- `mod_pubsub`: Add `pubsub_delete_item` hook +- `mod_pubsub`: Report support of `config-node-max` in pep +- `mod_pubsub`: Relay pubsub iq queries to muc members without using bare jid ([#4093](https://github.com/processone/ejabberd/issues/#4093)) +- `mod_pubsub`: Allow pubsub node owner to overwrite items published by other persons +- `mod_push_keepalive`: Delay `wake_on_start` +- `mod_push_keepalive`: Don't let hook crash +- `mod_push`: Add `notify_on` option +- `mod_push`: Set `last-message-sender` to bare JID +- `mod_register_web`: Make redirect to page that end with `/` ([#3177](https://github.com/processone/ejabberd/issues/#3177)) +- `mod_shared_roster_ldap`: Don't crash in `get_member_jid` on empty output ([#3614](https://github.com/processone/ejabberd/issues/#3614)) + +#### MUC: + +- Add support to register nick in a room ([#3455](https://github.com/processone/ejabberd/issues/#3455)) +- Convert `allow_private_message` MUC room option to `allowpm` ([#3736](https://github.com/processone/ejabberd/issues/#3736)) +- Update xmpp version to send `roomconfig_changesubject` in disco#info ([#4085](https://github.com/processone/ejabberd/issues/#4085)) +- Fix crash when loading room from DB older than ffa07c6, 23.04 +- Fix support to retract a MUC room message +- Don't always store messages passed through `muc_filter_message` ([#4083](https://github.com/processone/ejabberd/issues/#4083)) +- Pass also MUC room retract messages over the `muc_filter_message` ([#3397](https://github.com/processone/ejabberd/issues/#3397)) +- Pass MUC room private messages over the `muc_filter_message` too ([#3397](https://github.com/processone/ejabberd/issues/#3397)) +- Store the subject author JID, and run `muc_filter_message` when sending subject ([#3397](https://github.com/processone/ejabberd/issues/#3397)) +- Remove existing role information for users that are kicked from room ([#4035](https://github.com/processone/ejabberd/issues/#4035)) +- Expand rule "mucsub subscribers are members in members only rooms" to more places + +#### SQL: + +- Add ability to force alternative upsert implementation in mysql +- Properly parse mysql version even if it doesn't have type tag +- Use prepared statement with mysql +- Add alternate version of mysql upsert +- `ejabberd_auth_sql`: Reset scram fields when setting plain password +- `mod_privacy_sql`: Fix return values from `calculate_diff` +- `mod_privacy_sql`: Optimize `set_list` +- `mod_privacy_sql`: Use more efficient way to calculate changes in `set_privacy_list` + +## Version 23.04 + +#### General: + +- New `s2s_out_bounce_packet` hook +- Re-allow anonymous connection for connection without client certificates ([#3985](https://github.com/processone/ejabberd/issues/3985)) +- Stop `ejabberd_system_monitor` before stopping node +- `captcha_url` option now accepts `auto` value, and it's the default +- `mod_mam`: Add support for XEP-0425: Message Moderation +- `mod_mam_sql`: Fix problem with results of mam queries using rsm with max and before +- `mod_muc_rtbl`: New module for Real-Time Block List for MUC rooms ([#4017](https://github.com/processone/ejabberd/issues/4017)) +- `mod_roster`: Set roster name from XEP-0172, or the stored one ([#1611](https://github.com/processone/ejabberd/issues/1611)) +- `mod_roster`: Preliminary support to store extra elements in subscription request ([#840](https://github.com/processone/ejabberd/issues/840)) +- `mod_pubsub`: Pubsub xdata fields `max_item/item_expira/children_max` use `max` not `infinity` +- `mod_vcard_xupdate`: Invalidate `vcard_xupdate` cache on all nodes when vcard is updated + +#### Admin: + +- `ext_mod`: Improve support for loading `*.so` files from `ext_mod` dependencies +- Improve output in `gen_html_doc_for_commands` command +- Fix ejabberdctl output formatting ([#3979](https://github.com/processone/ejabberd/issues/3979)) +- Log HTTP handler exceptions + +#### MUC: + +- New command `get_room_history` +- Persist `none` role for outcasts +- Try to populate room history from mam when unhibernating +- Make `mod_muc_room:set_opts` process persistent flag first +- Allow passing affiliations and subscribers to `create_room_with_opts` command +- Store state in db in `mod_muc:create_room()` +- Make subscribers members by default + +#### SQL schemas: + +- Fix a long standing bug in new schema migration +- `update_sql` command: Many improvements in new schema migration +- `update_sql` command: Add support to migrate MySQL too +- Change PostgreSQL SERIAL to BIGSERIAL columns +- Fix minor SQL schema inconsistencies +- Remove unnecessary indexes +- New SQL schema migrate fix + +#### MS SQL: + +- MS SQL schema fixes +- Add `new` schema for MS SQL +- Add MS SQL support for new schema migration +- Minor MS SQL improvements +- Fix MS SQL error caused by `ORDER BY` in subquery + +#### SQL Tests: + +- Add support for running tests on MS SQL +- Add ability to run tests on upgraded DB +- Un-deprecate `ejabberd_config:set_option/2` +- Use python3 to run `extauth.py` for tests +- Correct README for creating test docker MS SQL DB +- Fix TSQLlint warnings in MSSQL test script + +#### Testing: + +- Fix Shellcheck warnings in shell scripts +- Fix Remark-lint warnings +- Fix Prospector and Pylint warnings in test `extauth.py` +- Stop testing ejabberd with Erlang/OTP 19.3, as Github Actions no longer supports ubuntu-18.04 +- Test only with oldest OTP supported (20.0), newest stable (25.3) and bleeding edge (26.0-rc2) +- Upload Common Test logs as artifact in case of failure + +#### `ecs` container image: +- Update Alpine to 3.17 to get Erlang/OTP 25 and Elixir 1.14 +- Add `tini` as runtime init +- Set `ERLANG_NODE` fixed to `ejabberd@localhost` +- Upload images as artifacts to Github Actions +- Publish tag images automatically to ghcr.io + +#### `ejabberd` container image: +- Update Alpine to 3.17 to get Erlang/OTP 25 and Elixir 1.14 +- Add `METHOD` to build container using packages ([#3983](https://github.com/processone/ejabberd/issues/3983)) +- Add `tini` as runtime init +- Detect runtime dependencies automatically +- Remove unused Mix stuff: ejabberd script and static COOKIE +- Copy captcha scripts to `/opt/ejabberd-*/lib` like the installers +- Expose only `HOME` volume, it contains all the required subdirs +- ejabberdctl: Don't use `.../releases/COOKIE`, it's no longer included + +#### Installers: + +- make-binaries: Bump versions, e.g. erlang/otp to 25.3 +- make-binaries: Fix building with erlang/otp v25.x +- make-packages: Fix for installers workflow, which didn't find lynx + +## Version 23.01 + +#### General: + +- Add `misc:uri_parse/2` to allow declaring default ports for protocols +- CAPTCHA: Add support to define module instead of path to script +- Clustering: Handle `mnesia_system_event mnesia_up` when other node joins this ([#3842](https://github.com/processone/ejabberd/issues/3842)) +- ConverseJS: Don't set i18n option because Converse enforces it instead of browser lang ([#3951](https://github.com/processone/ejabberd/issues/3951)) +- ConverseJS: Try to redirect access to files `mod_conversejs` to CDN when there is no local copies +- ext_mod: compile C files and install them in ejabberd's `priv` +- ext_mod: Support to get module status from Elixir modules +- make-binaries: reduce log output +- make-binaries: Bump zlib version to 1.2.13 +- MUC: Don't store mucsub presence events in offline storage +- MUC: `hibernation_time` is not an option worth storing in room state ([#3946](https://github.com/processone/ejabberd/issues/3946)) +- Multicast: Jid format when `multicastc` was cached ([#3950](https://github.com/processone/ejabberd/issues/3950)) +- mysql: Pass `ssl` options to mysql driver +- pgsql: Do not set `standard_conforming_strings` to `off` ([#3944](https://github.com/processone/ejabberd/issues/3944)) +- OAuth: Accept `jid` as a HTTP URL query argument +- OAuth: Handle when client is not identified +- PubSub: Expose the `pubsub#type` field in `disco#info` query to the node ([#3914](https://github.com/processone/ejabberd/issues/3914)) +- Translations: Update German translation + +#### Admin: + +- `api_permissions`: Fix option crash when doesn't have `who:` section +- `log_modules_fully`: New option to list modules that will log everything +- `outgoing_s2s_families`: Changed option's default to IPv6, and fall back to IPv4 +- Fix bash completion when using Relive or other install methods +- Fix portability issue with some shells ([#3970](https://github.com/processone/ejabberd/issues/3970)) +- Allow admin command to subscribe new users to `members_only` rooms +- Use alternative `split/2` function that works with Erlang/OTP as old as 19.3 +- Silent warning in OTP24 about not specified `cacerts` in SQL connections +- Fix compilation warnings with Elixir 1.14 + +#### DOAP: + +- Support extended `-protocol` erlang attribute +- Add extended RFCs and XEP details to some protocol attributes +- `tools/generate-doap.sh`: New script to generate DOAP file, add `make doap` ([#3915](https://github.com/processone/ejabberd/issues/3915)) +- `ejabberd.doap`: New DOAP file describing ejabberd supported protocols + +#### MQTT: + +- Add MQTT bridge module +- Add support for certificate authentication in MQTT bridge +- Implement reload in MQTT bridge +- Add support for websockets to MQTT bridge +- Recognize ws5/wss5 urls in MQTT bridge +- `mqtt_publish`: New hook for MQTT publish event +- `mqtt_(un)subscribe`: New hooks for MQTT subscribe & unsubscribe events + +#### VSCode: + +- Improve `.devcontainer` to use use devcontainer image and `.vscode` +- Add `.vscode` files to instruct VSCode how to run ejabberd +- Add Erlang LS default configuration +- Add Elvis default configuration + +## Version 22.10 + +#### Core: + +- Add `log_burst_limit_*` options ([#3865](https://github.com/processone/ejabberd/issues/3865)) +- Support `ERL_DIST_PORT` option to work without epmd +- Auth JWT: Catch all errors from `jose_jwt:verify` and log debugging details ([#3890](https://github.com/processone/ejabberd/issues/3890)) +- CAPTCHA: Support `@VERSION@` and `@SEMVER@` in `captcha_cmd` option ([#3835](https://github.com/processone/ejabberd/issues/3835)) +- HTTP: Fix unix socket support ([#3894](https://github.com/processone/ejabberd/issues/3894)) +- HTTP: Handle invalid values in `X-Forwarded-For` header more gracefuly +- Listeners: Let module take over socket +- Listeners: Don't register listeners that failed to start in config reload +- `mod_admin_extra`: Handle empty roster group names +- `mod_conversejs`: Fix crash when mod_register not enabled ([#3824](https://github.com/processone/ejabberd/issues/3824)) +- `mod_host_meta`: Complain at start if listener is not encrypted +- `mod_ping`: Fix regression on `stop_ping` in clustering context ([#3817](https://github.com/processone/ejabberd/issues/3817)) +- `mod_pubsub`: Don't crash on command failures +- `mod_shared_roster`: Fix cache invalidation +- `mod_shared_roster_ldap`: Update roster_get hook to use `#roster_item{}` +- `prosody2ejabberd`: Fix parsing of scram password from prosody + +#### MIX: + +- Fix MIX's filter_nodes +- Return user jid on join +- `mod_mix_pam`: Add new MIX namespaces to disco features +- `mod_mix_pam`: Add handling of IQs with newer MIX namespaces +- `mod_mix_pam`: Do roster pushes on join/leave +- `mod_mix_pam`: Parse sub elements of the mix join remote result +- `mod_mix_pam`: Provide MIX channels as roster entries via hook +- `mod_mix_pam`: Display joined channels on webadmin page +- `mod_mix_pam`: Adapt to renaming of `participant-id` from mix_roster_channel record +- `mod_roster`: Change hook type from `#roster{}` to `#roster_item{}` +- `mod_roster`: Respect MIX `` setting +- `mod_roster`: Adapt to change of mix_annotate type to boolean in roster_query +- `mod_shared_roster`: Fix wrong hook type `#roster{}` (now `#roster_item{}`) + +#### MUC: + +- Store role, and use it when joining a moderated room ([#3330](https://github.com/processone/ejabberd/issues/3330)) +- Don't persist `none` role ([#3330](https://github.com/processone/ejabberd/issues/3330)) +- Allow MUC service admins to bypass max_user_conferences limitation +- Show allow_query_users room option in disco info ([#3830](https://github.com/processone/ejabberd/issues/3830)) +- mod_muc_room: Don't set affiliation to `none` if it's already `none` in `process_item_change/3` +- Fix mucsub unsubscribe notification payload to have muc_unsubcribe in it +- Allow muc_{un}subscribe hooks to modify sent packets +- Pass room state to muc_{un}subscribed hook +- The archive_msg export fun requires MUC Service for room archives +- Export `mod_muc_admin:get_room_pid/2` +- Export function for getting room diagnostics + +#### SQL: + +- Handle errors reported from begin/commit inside transaction +- Make connection close errors bubble up from inside sql transaction +- Make first sql reconnect wait shorter time +- React to sql driver process exit earlier +- Skip connection exit message when we triggered reconnection +- Add syntax_tools to applications, required when using ejabberd_sql_pt ([#3869](https://github.com/processone/ejabberd/issues/3869)) +- Fix mam delete_old_messages_batch for sql backend +- Use `INSERT ... ON DUPLICATE KEY UPDATE` for upsert on mysql +- Update mysql library +- Catch mysql connection being close earlier + +#### Build: + +- `make all`: Generate start scripts here, not in `make install` ([#3821](https://github.com/processone/ejabberd/issues/3821)) +- `make clean`: Improve this and "distclean" +- `make deps`: Ensure deps configuration is ran when getting deps ([#3823](https://github.com/processone/ejabberd/issues/3823)) +- `make help`: Update with recent changes +- `make install`: Don't leak DESTDIR in files copied by 'make install' +- `make options`: Fix error reporting on OTP24+ +- `make update`: configure also in this case, similarly to `make deps` +- Add definition to detect OTP older than 25, used by ejabberd_auth_http +- Configure eimp with mix to detect image convert properly ([#3823](https://github.com/processone/ejabberd/issues/3823)) +- Remove unused macro definitions detected by rebar3_hank +- Remove unused header files which content is already in xmpp library + +#### Container: + +- Get ejabberd-contrib sources to include them +- Copy `.ejabberd-modules` directory if available +- Do not clone repo inside container build +- Use `make deps`, which performs additional steps ([#3823](https://github.com/processone/ejabberd/issues/3823)) +- Support `ERL_DIST_PORT` option to work without epmd +- Copy `ejabberd-docker-install.bat` from docker-ejabberd git and rename it +- Set a less frequent healthcheck to reduce CPU usage ([#3826](https://github.com/processone/ejabberd/issues/3826)) +- Fix build instructions, add more podman examples + +#### Installers: + +- make-binaries: Include CAPTCHA script with release +- make-binaries: Edit rebar.config more carefully +- make-binaries: Fix linking of EIMP dependencies +- make-binaries: Fix GitHub release version checks +- make-binaries: Adjust Mnesia spool directory path +- make-binaries: Bump Erlang/OTP version to 24.3.4.5 +- make-binaries: Bump Expat and libpng versions +- make-packages: Include systemd unit with RPM +- make-packages: Fix permissions on RPM systems +- make-installers: Support non-root installation +- make-installers: Override code on upgrade +- make-installers: Apply cosmetic changes + +#### External modules: + +- ext_mod: Support managing remote nodes in the cluster +- ext_mod: Handle correctly when COMMIT.json not found +- Don't bother with COMMIT.json user-friendly feature in automated user case +- Handle not found COMMIT.json, for example in GH Actions +- Add WebAdmin page for managing external modules + +#### Workflows Actions: + +- Update workflows to Erlang 25 +- Update workflows: Ubuntu 18 is deprecated and 22 is added +- CI: Remove syntax_tools from applications, as fast_xml fails Dialyzer +- Runtime: Add Xref options to be as strict as CI + +## Version 22.05 + +#### Core +- C2S: Don't expect that socket will be available in `c2s_terminated` hook +- Event handling process hook tracing +- Guard against `erlang:system_info(logical_processors)` not always returning a number +- `domain_balancing`: Allow for specifying `type` only, without specifying `component_number` + +#### MQTT +- Add TLS certificate authentication for MQTT connections +- Fix login when generating client id, keep connection record (#3593) +- Pass property name as expected in mqtt_codec (fixes login using MQTT 5) +- Support MQTT subscriptions spread over the cluster (#3750) + +#### MUC +- Attach meta field with real jid to mucsub subscription events +- Handle user removal +- Stop empty MUC rooms 30 seconds after creation +- `default_room_options`: Update options configurable +- `subscribe_room_many_max_users`: New option in `mod_muc_admin` + +#### mod_conversejs +- Improved options to support `@HOST@` and `auto` values +- Set `auth` and `register` options based on ejabberd configuration +- `conversejs_options`: New option +- `conversejs_resources`: New option + +#### PubSub +- `mod_pubsub`: Allow for limiting `item_expire` value +- `mod_pubsub`: Unsubscribe JID on whitelist removal +- `node_pep`: Add config-node and multi-items features (#3714) + +#### SQL +- Improve compatibility with various db engine versions +- Sync old-to-new schema script with reality (#3790) +- Slight improvement in MSSQL testing support, but not yet complete + +#### Other Modules +- `auth_jwt`: Checking if an user is active in SM for a JWT authenticated user (#3795) +- `mod_configure`: Implement Get List of Registered/Online Users from XEP-0133 +- `mod_host_meta`: New module to serve host-meta files, see XEP-0156 +- `mod_mam`: Store all mucsub notifications not only message notifications +- `mod_ping`: Delete ping timer if resource is gone after the ping has been sent +- `mod_ping`: Don't send ping if resource is gone +- `mod_push`: Fix notifications for pending sessions (XEP-0198) +- `mod_push`: Keep push session ID on session resume +- `mod_shared_roster`: Adjust special group cache size +- `mod_shared_roster`: Normalize JID on unset_presence (#3752) +- `mod_stun_disco`: Fix parsing of IPv6 listeners + +#### Dependencies +- autoconf: Supported from 2.59 to the new 2.71 +- fast_tls: Update to 1.1.14 to support OpenSSL 3 +- jiffy: Update to 1.1.1 to support Erlang/OTP 25.0-rc1 +- luerl: Update to 1.0.0, now available in hex.pm +- lager: This dependency is used only when Erlang is older than 22 +- rebar2: Updated binary to work from Erlang/OTP 22 to 25 +- rebar3: Updated binary to work from Erlang/OTP 22 to 25 +- `make update`: Fix when used with rebar 3.18 + +#### Compile +- `mix release`: Copy `include/` files for ejabberd, deps and otp, in `mix.exs` +- `rebar3 release`: Fix ERTS path in `ejabberdctl` +- `configure.ac`: Set default ejabberd version number when not using git +- `mix.exs`: Move some dependencies as optional +- `mix.exs`: No need to use Distillery, Elixir has built-in support for OTP releases (#3788) +- `tools/make-binaries`: New script for building Linux binaries +- `tools/make-installers`: New script for building command line installers + +#### Start +- New `make relive` similar to `ejabberdctl live` without installing +- `ejabberdctl`: Fix some warnings detected by ShellCheck +- `ejabberdctl`: Mention in the help: `etop`, `ping` and `started`/`stopped` +- `make rel`: Switch to paths: `conf/`, `database/`, `logs/` +- `mix.exs`: Add `-boot` and `-boot_var` in `ejabberdctl` instead of adding `vm.args` +- `tools/captcha.sh`: Fix some warnings detected by ShellCheck + +#### Commands +- Accept more types of ejabberdctl commands arguments as JSON-encoded +- `delete_old_mam_messages_batch`: New command with rate limit +- `delete_old_messages_batch`: New command with rate limit +- `get_room_occupants_number`: Don't request the whole MUC room state (#3684, #1964) +- `get_vcard`: Add support for MUC room vCard +- `oauth_revoke_token`: Add support to work with all backends +- `room_unused_*`: Optimize commands in SQL by reusing `created_at` +- `rooms_unused_...`: Let `get_all_rooms` handle `global` argument (#3726) +- `stop|restart`: Terminate ejabberd_sm before everything else to ensure sessions closing (#3641) +- `subscribe_room_many`: New command + +#### Translations +- Updated Catalan +- Updated French +- Updated German +- Updated Portuguese +- Updated Portuguese (Brazil) +- Updated Spanish + +#### Workflows +- CI: Publish CT logs and Cover on failure to an external GH Pages repo +- CI: Test shell scripts using ShellCheck (#3738) +- Container: New workflow to build and publish containers +- Installers: Add job to create draft release +- Installers: New workflow to build binary packages +- Runtime: New workflow to test compilation, rel, starting and ejabberdctl + +## 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 +- Add rebar3 3.15.2 binary +- Add support for mix to: `./configure --enable-rebar=mix` +- Improved `make rel` to work with rebar3 and mix +- Add `make dev` to build a development release with rebar3 or mix +- Hex: Add `sql/` and `vars.config` to Hex package files +- Hex: Update mix applications list to fix error `p1_utils is listed as both...` +- There are so many targets in Makefile... add `make help` +- Fix extauth.py failure in test suite with Python 3 +- Added experimental support for GitHub Codespaces +- Switch test service from TravisCI to GitHub Actions + +#### Commands: + +- Display extended error message in ejabberdctl +- Remove SMP option from ejabberdctl.cfg, `-smp` was removed in OTP 21 +- `create_room`: After creating room, store in DB if it's persistent +- `help`: Major changes in its usage and output +- `srg_create`: Update to use `label` parameter instead of `name` + +#### Modules: + +- ejabberd_listener: New `send_timeout` option +- mod_mix: Improvements to update to 0.14.1 +- mod_muc_room: Don't leak owner JIDs +- mod_multicast: Routing for more MUC packets +- mod_multicast: Correctly strip only other bcc addresses +- mod_mqtt: Allow shared roster group placeholder in mqtt topic +- mod_pubsub: Several fixes when using PubSub with RSM +- mod_push: Handle MUC/Sub events correctly +- mod_shared_roster: Delete cache after performing change to be sure that in cache will be up to date data +- mod_shared_roster: Improve database and caching +- mod_shared_roster: Reconfigure cache when options change +- mod_vcard: Fix invalid_encoding error when using extended plane characters in vcard +- mod_vcard: Update econf:vcard() to generate correct vcard_temp record +- WebAdmin: New simple pages to view mnesia tables information and content +- WebSocket: Fix typos + +#### SQL: + +- MySQL Backend Patch for scram-sha512 +- SQLite: When exporting for SQLite, use its specific escape options +- SQLite: Minor fixes for new_sql_schema support +- mod_privacy: Cast as boolean when exporting privacy_list_data to PostgreSQL +- mod_mqtt: Add mqtt_pub table definition for MSSQL +- mod_shared_roster: Add missing indexes to `sr_group` tables in all SQL databases + +## Version 21.04 + +#### API Commands: + +- `add_rosteritem/...`: Add argument guards to roster commands +- `get_user_subscriptions`: New command for MUC/Sub +- `remove_mam_for_user_with_peer`: Fix when removing room archive +- `send_message`: Fix bug introduced in ejabberd 21.01 +- `set_vcard`: Return modules errors + +#### Build and setup: + +- Allow ejabberd to be compatible as a dependency for an Erlang project using rebar3 +- CAPTCHA: New question/answer-based CAPTCHA script +- `--enable-lua`: new configure option for luerl instead of --enable-tools +- Remove support for HiPE, it was experimental and Erlang/OTP 24 removes it +- Update `sql_query` record to handle the Erlang/OTP 24 compiler reports +- Updated dependencies to fix Dialyzer warnings + +#### Miscellaneous: + +- CAPTCHA: Update `FORM_TYPE` from captcha to register +- LDAP: fix eldap certificate verification +- MySQL: Fix for "specified key was too long" +- Translations: updated the Esperanto, Greek, and Japanese translations +- Websocket: Fix PONG responses + +#### Modules: + +- `mod_block_strangers`: If stanza is type error, allow it passing +- `mod_caps`: Don't request roster when not needed +- `mod_caps`: Skip reading roster in one more case +- `mod_mam`: Remove `queryid` from MAM fin element +- `mod_mqtt`: When deregistering XMPP account, close its MQTT sessions +- `mod_muc`: Take in account subscriber's affiliation when checking access to moderated room +- `mod_muc`: Use monitors to track online and hard-killed rooms +- `mod_muc`: When occupant is banned, remove his subscriptions too +- `mod_privacy`: Make fetching roster lazy +- `mod_pubsub`: Don't fail on PEP unsubscribe +- `mod_pubsub`: Fix `gen_pubsub_node:get_state` return value +- `mod_vcard`: Obtain and provide photo type in vCard LDAP + +## Version 21.01 + +#### Miscellaneous changes: + +- `log_rotate_size` option: Fix handling of ‘infinity’ value +- `mod_time`: Fix invalid timezone +- Auth JWT: New `check_decoded_jwt` hook runs the default JWT verifier +- MUC: Allow non-occupant non-subscribed service admin send private MUC message +- MUC: New `max_password` and `max_captcha_whitelist` options +- OAuth: New `oauth_cache_rest_failure_life_time` option +- PEP: Skip reading pep nodes that we know won’t be requested due to caps +- SQL: Add sql script to migrate mysql from old schema to new +- SQL: Don’t use REPLACE for upsert when there are “-” fields. +- Shared Rosters LDAP: Add multi-domain support (and flexibility) +- Sqlite3: Fix dependency version +- Stun: Block loopback addresses by default +- Several documentation fixes and clarifications + +#### Commands: + +- `decide_room`: Use better fallback value for room activity time when skipping room +- `delete_old_message`: Fix when using sqlite spool table +- `module_install`: Make ext_mod compile module with debug_info flags +- `room_unused_*`: Don’t fetch subscribers list +- `send_message`: Don’t include empty in messages +- `set_room_affiliation`: Validate affiliations + +#### Running: + +- Docker: New `Dockerfile` and `devcontainer.json` +- New `ejabberdctl foreground-quiet` +- Systemd: Allow for listening on privileged ports +- Systemd: Integrate nicely with systemd + +#### Translations: + +- Moved gettext PO files to a new `ejabberd-po` repository +- Improved several translations: Catalan, Chinese, German, Greek, Indonesian, Norwegian, Portuguese (Brazil), Spanish. + +## Version 20.12 + +- Add support for `SCRAM-SHA-{256,512}-{PLUS}` authentication +- Don't use same value in cache for user don't exist and wrong password +- `outgoing_s2s_ipv*_address`: New options to set ipv4/ipv6 outbound s2s out interface +- s2s_send_packet: this hook now filters outgoing s2s stanzas +- start_room: new hook runs when a room process is started +- check_decoded_jwt: new hook to check decoded JWT after success authentication + +#### Admin +- Docker: Fix DB initialization +- New sql_odbc_driver option: choose the mssql ODBC driver +- Rebar3: Fully supported. Enable with `./configure --with-rebar=/path/to/rebar3` +- systemd: start ejabberd in foreground + +#### Modules: +- MAM: Make sure that jid used as base in mam xml_compress is bare +- MAM: Support for MAM Flipped Pages +- MUC: Always show MucSub subscribers nicks +- MUC: Don't forget not-persistent rooms in load_permanent_rooms +- MUC Admin: Better error reporting +- MUC Admin: Fix commands with hibernated rooms +- MUC Admin: Many improvements in rooms_unused_list/destroy +- MUC Admin: create_room_with_opts Store options only if room starts +- Pubsub: Remove 'dag' node plugin documentation +- Push: Fix API call return type on error +- Push: Support cache config changes on reload +- Register: Allow for account-removal-only setup again +- Roster: Make roster subscriptions work better with invalid roster state in db +- Vcard: Fix vCard search by User when using Mnesia +- WebAdmin: Allow vhost admins to view WebAdmin menus +- WebAdmin: Don't do double utf-8 conversion on translated strings +- WebAdmin: Mark dangerous buttons with CSS +- WebSocket: Make websocket send put back pressure on c2s process + +## Version 20.07 + +#### Changes in this version +- Add support for using unix sockets in listeners. +- Make this version compatible with erlang R23 +- Make room permissions checks more strict for subscribers +- Fix problem with muc rooms crashing when using muc logger + with some locales +- Limit stat calls that logger module issues +- Don't throw errors when using user_regexp acl rule and + having non-matching host +- Fix problem with leaving old data when updating shared rosters +- Fix edge case that caused failure of resuming old sessions with + stream management. +- Fix crash when room that was started with logging enabled was later + changed to logging disabled +- Increase default shaper limits (this should help with delays for + clients that are using jingle) +- Fix couple compatibility problems which prevented working on + erlang R19 +- Fix sending presence unavailable when session terminates for + clients that only send directed presences (helps with sometimes + not leaving muc rooms on disconnect). +- Prevent supervisor errors for sockets that were closed before + they were passed to handler modules +- Make stun module work better with ipv6 addresses + +## Version 20.03 + +#### Changes in this version +- Add support of ssl connection when connection to mysql + database (configured with `sql_ssl: true` option) +- Experimental support for cockroachdb when configured + with postgres connector +- Add cache and optimize queries issued by `mod_shared_roster`, + this should greatly improve performance of this module when + used with `sql` backend +- Fix problem with accessing webadmin +- Make webadmin work even when url is missing trailing slash +- When compiling external modules with ext_mod, use flags + that were detected during compilation of ejabberd +- Make config changed to ldap options be updated when issued + `reload_config` command +- Fix `room_empty_destory` command +- Fix reporting errors in `send_stanza` command when xml + passed to it couldn't be passed correctly + +## Version 20.02 + +#### Changes in this version +- Fix problems when trying to use string format with unicode + values directly in xmpp nodes +- Add missing oauth_client table declaration in lite.new.sql +- Improve compatibility with CocroachDB +- Fix importing of piefxis files that did use scram passwords +- Fix importing of piefxis files that had multiple includes + in them +- Update jiffy dependency +- Allow storage of emojis when using mssql database (Thanks + to Christoph Scholz) +- Make ejabberd_auth_http be able to use auth_opts +- Make custom_headers options in http modules correctly + override built-in values +- Fix return value of reload_config and dump_config commands + +## Version 20.01 + +#### New features +- Implement OAUTH authentication in mqtt +- Make logging infrastructure use new logger introduced + in Erlang (requires OTP22) +- New configuration parser/validator +- Initial work on being able to use CockroachDB as database backend +- Add gc command +- Add option to disable using prepared statements on Postgresql +- Implement routine for converting password to SCRAM format + for all backends not only SQL +- Add infrastructure for having module documentation directly + in individual module source code +- Generate man page automatically +- Implement copy feature in mod_carboncopy + +#### Fixes +- Make webadmin work with configurable paths +- Fix handling of result in xmlrpc module +- Make webadmin work even when accessed through not declared domain +- Better error reporting in xmlrpc +- Limit amount of results returned by disco queries to pubsub nodes +- Improve validation of configured JWT keys +- Fix race condition in Redis/SQL startup +- Fix loading order of third party modules +- Fix reloading of ACL rules +- Make account removal requests properly route response +- Improve handling of malformed inputs in send_message command +- Omit push notification if storing message in offline storage + failed +- Fix crash in stream management when timeout was not set + +## Version 19.09 + +#### Admin +- The minimum required Erlang/OTP version is now 19.3 +- Fix API call using OAuth (#2982) +- Rename MUC command arguments from Host to Service (#2976) + +#### Webadmin +- Don't treat 'Host' header as a virtual XMPP host (#2989) +- Fix some links to Guide in WebAdmin and add new ones (#3003) +- Use select fields to input host in WebAdmin Backup (#3000) +- Check account auth provided in WebAdmin is a local host (#3000) + +#### ACME +- Improve ACME implementation +- Fix IDA support in ACME requests +- Fix unicode formatting in ACME module +- Log an error message on IDNA failure +- Support IDN hostnames in ACME requests +- Don't attempt to create ACME directory on ejabberd startup +- Don't allow requesting certificates for localhost or IP-like domains +- Don't auto request certificate for localhost and IP-like domains +- Add listener for ACME challenge in example config + +#### Authentication +- JWT-only authentication for some users (#3012) + +#### MUC +- Apply default role after revoking admin affiliation (#3023) +- Custom exit message is not broadcast (#3004) +- Revert "Affiliations other than admin and owner cannot invite to members_only rooms" (#2987) +- When join new room with password, set pass and password_protected (#2668) +- Improve rooms_* commands to accept 'global' as MUC service argument (#2976) +- Rename MUC command arguments from Host to Service (#2976) + +#### SQL +- Fix transactions for Microsoft SQL Server (#2978) +- Spawn SQL connections on demand only + +#### Misc +- Add support for XEP-0328: JID Prep +- Added gsfonts for captcha +- Log Mnesia table type on creation +- Replicate Mnesia 'bosh' table when nodes are joined +- Fix certificate selection for s2s (#3015) +- Provide meaningful error when adding non-local users to shared roster (#3000) +- Websocket: don't treat 'Host' header as a virtual XMPP host (#2989) +- Fix sm ack related c2s error (#2984) +- Don't hide the reason why c2s connection has failed +- Unicode support +- Correctly handle unicode in log messages +- Fix unicode processing in ejabberd.yml + +## Version 19.08 + +#### Administration +- Improve ejabberd halting procedure +- Process unexpected erlang messages uniformly: logging a warning +- mod_configure: Remove modules management + +#### Configuration +- Use new configuration validator +- ejabberd_http: Use correct virtual host when consulting trusted_proxies +- Fix Elixir modules detection in the configuration file +- Make option 'validate_stream' global +- Allow multiple definitions of host_config and append_host_config +- Introduce option 'captcha_url' +- mod_stream_mgmt: Allow flexible timeout format +- mod_mqtt: Allow flexible timeout format in session_expiry option + +#### Misc +- Fix SQL connections leakage +- New authentication method using JWT tokens +- extauth: Add 'certauth' command +- Improve SQL pool logic +- Add and improve type specs +- Improve extraction of translated strings +- Improve error handling/reporting when loading language translations +- Improve hooks validator and fix bugs related to hooks registration +- Gracefully close inbound s2s connections +- mod_mqtt: Fix usage of TLS +- mod_offline: Make count_offline_messages cache work when using mam for storage +- mod_privacy: Don't attempt to query 'undefined' active list +- mod_privacy: Fix race condition + +#### MUC +- Add code for hibernating inactive muc_room processes +- Improve handling of unexpected iq in mod_muc_room +- Attach mod_muc_room processes to a supervisor +- Restore room when receiving message or generic iq for not started room +- Distribute routing of MUC messages across all CPU cores + +#### PubSub +- Fix pending nodes retrieval for SQL backend +- Check access_model when publishing PEP +- Remove deprecated pubsub plugins +- Expose access_model and publish_model in pubsub#metadata + +## Version 19.05 + +#### Admin +- The minimum required Erlang/OTP version is now 19.1 +- Provide a suggestion when unknown command, module, option or request handler is detected +- Deprecate some listening options: captcha, register, web_admin, http_bind and xmlrpc +- Add commands to get Mnesia info: mnesia_info and mnesia_table_info +- Fix Register command to respect mod_register's Access option +- Fixes in Prosody import: privacy and rooms +- Remove TLS options from the example config +- Improve request_handlers validator +- Fix syntax in example Elixir config file + +#### Auth +- Correctly support cache tags in ejabberd_auth +- Don't process failed EXTERNAL authentication by mod_fail2ban +- Don't call to mod_register when it's not loaded +- Make anonymous auth don't {de}register user when there are other resources + +#### Developer +- Rename listening callback from start/2 to start/3 +- New hook called when room gets destroyed: room_destroyed +- New hooks for tracking mucsub subscriptions changes: muc_subscribed, muc_unsubscribed +- Make static hooks analyzer working again + +#### MUC +- Service admins are allowed to recreate room even if archive is nonempty +- New option user_mucsub_from_muc_archive +- Avoid late arrival of get_disco_item response +- Handle get_subscribed_rooms call from mod_muc_room pid +- Fix room state cleanup from db on change of persistent option change +- Make get_subscribed_rooms work even for non-persistant rooms +- Allow non-moderator subscribers to get list of room subscribers + +#### Offline +- New option bounce_groupchat: make it not bounce mucsub/groupchat messages +- New option use_mam_for_storage: fetch data from mam instead of spool table +- When applying limit of max msgs in spool check only spool size +- Do not store mucsub wrapped messages with no-store hint in offline storage +- Always store ActivityMarker messages +- Don't issue count/message fetch queries for offline from mam when not needed +- Properly handle infinity as max number of message in mam offline storage +- Sort messages by stanza_id when using mam storage in mod_offline +- Return correct value from count_offline_messages with mam storage option +- Make mod_offline put msg ignored by mam in spool when mam storage is on + +#### SQL: +- Add SQL schemas for MQTT tables +- Report better errors on SQL terms decode failure +- Fix PostgreSQL compatibility in mod_offline_sql:remove_old_messages +- Fix handling of list arguments on pgsql +- Preliminary support for SQL in process_rosteritems command + +#### Tests +- Add tests for user mucsub mam from muc mam +- Add tests for offline with mam storage +- Add tests for offline use_mam_for_storage +- Initial Docker environment to run ejabberd test suite +- Test offline:use_mam_for_storage, mam:user_mucsub_from_muc_archive used together + +#### Websocket +- Add WebSockets support to mod_mqtt +- Return "Bad request" error when origin in websocket connection doesn't match +- Fix RFC6454 violation on websocket connection when validating Origin header +- Origin header validation on websocket connection + +#### Other modules +- mod_adhoc: Use xml:lang from stanza when it's missing in element +- mod_announce: Add 'sessionid' attribute when required +- mod_bosh: Don't put duplicate polling attribute in bosh payload +- mod_http_api: Improve argument error messages and log messages +- mod_http_upload: Feed whole image to eimp:identify/1 +- mod_http_upload: Log nicer warning on unknown host +- mod_http_upload: Case-insensitive host comparison +- mod_mqtt: Support other socket modules +- mod_push: Check for payload in encrypted messages + +## Version 19.02 + +#### Admin +- Fix in configure.ac the Erlang/OTP version: from 17.5 to 19.0 +- reload_config command: Fix crash when sql_pool_size option is used +- reload_config command: Fix crash when SQL is not configured +- rooms_empty_destroy command: Several fixes to behave more conservative +- Fix serverhost->host parameter name for muc_(un)register_nick API + +#### Configuration +- Allow specifying tag for listener for api_permission purposes +- Change default ciphers to intermediate +- Define default ciphers/protocol_option in example config +- Don't crash on malformed 'modules' section +- mod_mam: New option clear_archive_on_room_destroy to prevent archive removal on room destroy +- mod_mam: New option access_preferences to restrict who can modify the MAM preferences +- mod_muc: New option access_mam to restrict who can modify that room option +- mod_offline: New option store_groupchat to allow storing group chat messages + +#### Core +- Add MQTT protocol support +- Fix (un)setting of priority +- Use OTP application startup infrastructure for starting dependencies +- Improve starting order of several dependencies + +#### MAM +- mod_mam_mnesia/sql: Improve check for empty archive +- disallow room creation if archive not empty and clear_archive_on_room_destroy is false +- allow check if archive is empty for or user or room +- Additional checks for database failures + +#### MUC +- Make sure that room_destroyed is called even when some code throws in terminate +- Update muc room state after adding extra access field to it +- MUC/Sub: Send mucsub subscriber notification events with from set to room jid + +#### Shared Roster +- Don't perform roster push for non-local contacts +- Handle versioning result when shared roster group has remote account +- Fix SQL queries + +#### Miscelanea +- CAPTCHA: Add no-store hint to CAPTCHA challenge stanzas +- HTTP: Reject http_api request with malformed Authentication header +- mod_carboncopy: Don't lose carbons on presence change or session resumption +- mod_mix: Fix submission-id and channel resource +- mod_ping: Fix ping IQ reply/timeout processing (17.x regression) +- mod_private: Hardcode item ID for PEP bookmarks +- mod_push: Improve notification error handling +- PIEFXIS: Fix user export when password is scrammed +- Prosody: Improve import of roster items, rooms and attributes +- Translations: fixed "make translations" +- WebAdmin: Fix support to restart module with new options + +## Version 18.12 + +- MAM data store compression +- Proxy protocol support +- MUC Self-Ping optimization (XEP-0410) +- Bookmarks conversion (XEP-0411) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..e8855889e --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,61 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Guidelines for Respectful and Efficient Communication on Issues, Discussions, and PRs + +To ensure that our maintainers can efficiently manage issues and provide timely updates, we kindly ask that all comments on GitHub tickets remain relevant to the topic of the issue. Please avoid posting comments solely to ping maintainers or ask for updates. If you need information on the status of an issue, consider the following: + +- **Check the Issue Timeline:** Review the existing comments and updates on the issue before posting. +- **Use Reactions:** If you want to show that you are interested in an issue, use GitHub's reaction feature (e.g., thumbs up) instead of commenting. +- **Be Patient:** Understand that maintainers may be working on multiple tasks and will provide updates as soon as possible. + +Additionally, please be aware that: + +- **User Responses:** Users who report issues may no longer be using the software, may have switched to other projects, or may simply be busy. It is their right not to respond to follow-up questions or comments. +- **Maintainer Priorities:** Maintainers have the right to define their own priorities and schedule. They will address issues based on their availability and the project's needs. + +By following these guidelines, you help us maintain a productive and respectful environment for everyone involved. + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at the email address: conduct AT process-one.net. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [https://contributor-covenant.org/version/1/4][version] + +[homepage]: https://www.contributor-covenant.org/ +[version]: https://www.contributor-covenant.org/version/1/4/ diff --git a/COMPILE.md b/COMPILE.md new file mode 100644 index 000000000..2eb8a1739 --- /dev/null +++ b/COMPILE.md @@ -0,0 +1,126 @@ +Compile and Install ejabberd +============================ + +This document explains how to compile and install ejabberd +from source code. + +For a more detailed explanation, please check the +ejabberd Docs: [Source Code Installation][docs-source]. + +[docs-source]: https://docs.ejabberd.im/admin/install/source/ + + +Requirements +------------ + +To compile ejabberd you need: + +- GNU Make +- GCC +- Libexpat ≥ 1.95 +- Libyaml ≥ 0.1.4 +- Erlang/OTP ≥ 25.0 +- OpenSSL ≥ 1.0.0 + +Other optional libraries are: + +- Zlib ≥ 1.2.3, for Stream Compression support (XEP-0138) +- PAM library, for Pluggable Authentication Modules (PAM) +- ImageMagick's Convert program and Ghostscript fonts, for CAPTCHA + challenges +- Elixir ≥ 1.10.3, for Elixir support. It is recommended Elixir 1.14.0 or higher + +If your system splits packages in libraries and development headers, +install the development packages too. + + +Download Source Code +-------------------- + +There are several ways to obtain the ejabberd source code: + +- Source code archive from [ProcessOne Downloads][p1dl] +- Source code package from [ejabberd GitHub Releases][ghr] +- Latest development code from [ejabberd Git repository][gitrepo] + +[p1dl]: https://www.process-one.net/download/ejabberd/ +[ghr]: https://github.com/processone/ejabberd/releases +[gitrepo]: https://github.com/processone/ejabberd + + +Compile +------- + +The general instructions to compile ejabberd are: + + ./configure + make + +If the source code doesn't contain a `configure` script, +first of all install `autoconf` and run this to generate it: + + ./autogen.sh + +To configure the compilation, features, install paths... + + ./configure --help + +The build tool automatically downloads and compiles the +erlang libraries that [ejabberd depends on][docs-repo]. + +[docs-repo]: https://docs.ejabberd.im/developer/repositories/ + + +Install in the System +--------------------- + +To install ejabberd in the system, run this with system administrator rights (root user): + + sudo make install + +This will: + +- Install the configuration files in `/etc/ejabberd/` +- Install ejabberd binary, header and runtime files in `/lib/ejabberd/` +- Install the administration script: `/sbin/ejabberdctl` +- Install ejabberd documentation in `/share/doc/ejabberd/` +- Create a spool directory: `/var/lib/ejabberd/` +- Create a directory for log files: `/var/log/ejabberd/` + + +Build an OTP Release +-------------------- + +Instead of installing ejabberd in the system, you can build an OTP release +that includes all necessary to run ejabberd in a subdirectory: + + ./configure + make prod + +Check the full list of targets: + + make help + + +Start ejabberd +-------------- + +You can use the `ejabberdctl` command line administration script to +start and stop ejabberd. Some examples, depending on your installation method: + +- When installed in the system: + ``` + ejabberdctl start + /sbin/ejabberdctl start + ``` + +- When built an OTP production release: + ``` + _build/prod/rel/ejabberd/bin/ejabberdctl start + _build/prod/rel/ejabberd/bin/ejabberdctl live + ``` + +- Start interactively without installing or building OTP release: + ``` + make relive + ``` diff --git a/CONTAINER.md b/CONTAINER.md new file mode 100644 index 000000000..e96081112 --- /dev/null +++ b/CONTAINER.md @@ -0,0 +1,1095 @@ + +[![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/processone/ejabberd?sort=semver&logo=embarcadero&label=&color=49c0c4)](https://github.com/processone/ejabberd/tags) +[![ejabberd Container on GitHub](https://img.shields.io/github/v/tag/processone/ejabberd?label=ejabberd&sort=semver&logo=opencontainersinitiative&logoColor=2094f3)](https://github.com/processone/ejabberd/pkgs/container/ejabberd) +[![ecs Container on Docker](https://img.shields.io/docker/v/ejabberd/ecs?label=ecs&sort=semver&logo=docker)](https://hub.docker.com/r/ejabberd/ecs/) + +ejabberd Container Images +========================= + +[ejabberd][home] is an open-source, +robust, scalable and extensible realtime platform built using [Erlang/OTP][erlang], +that includes [XMPP][xmpp] Server, [MQTT][mqtt] Broker and [SIP][sip] Service. + +[home]: https://www.ejabberd.im/ +[erlang]: https://www.erlang.org/ +[xmpp]: https://xmpp.org/ +[mqtt]: https://mqtt.org/ +[sip]: https://en.wikipedia.org/wiki/Session_Initiation_Protocol + +This page documents those container images ([images comparison](#images-comparison)): + +- [![ejabberd Container](https://img.shields.io/badge/ejabberd-grey?logo=opencontainersinitiative&logoColor=2094f3)](https://github.com/processone/ejabberd/pkgs/container/ejabberd) + published in [ghcr.io/processone/ejabberd](https://github.com/processone/ejabberd/pkgs/container/ejabberd), + built using [ejabberd](https://github.com/processone/ejabberd/tree/master/.github/container) repository, + both for stable ejabberd releases and the `master` branch, in x64 and arm64 architectures. + +- [![ecs Container](https://img.shields.io/badge/ecs-grey?logo=docker&logoColor=2094f3)](https://hub.docker.com/r/ejabberd/ecs/) + published in [docker.io/ejabberd/ecs](https://hub.docker.com/r/ejabberd/ecs/), + built using [docker-ejabberd/ecs](https://github.com/processone/docker-ejabberd/tree/master/ecs) repository + for ejabberd stable releases in x64 architectures. + +For Microsoft Windows, see +[Docker Desktop for Windows 10](https://www.process-one.net/blog/install-ejabberd-on-windows-10-using-docker-desktop/), +and [Docker Toolbox for Windows 7](https://www.process-one.net/blog/install-ejabberd-on-windows-7-using-docker-toolbox/). + +For Kubernetes Helm, see [help-ejabberd](https://github.com/sando38/helm-ejabberd). + + +Start ejabberd +-------------- + +### daemon + +Start ejabberd in a new container: + +```bash +docker run --name ejabberd -d -p 5222:5222 ghcr.io/processone/ejabberd +``` + +That runs the container as a daemon, +using ejabberd default configuration file and XMPP domain `localhost`. + +Restart the stopped ejabberd container: + +```bash +docker restart ejabberd +``` + +Stop the running container: + +```bash +docker stop ejabberd +``` + +Remove the ejabberd container: + +```bash +docker rm ejabberd +``` + + +### with Erlang console + +Start ejabberd with an interactive Erlang console attached using the `live` command: + +```bash +docker run --name ejabberd -it -p 5222:5222 ghcr.io/processone/ejabberd live +``` + +That uses the default configuration file and XMPP domain `localhost`. + + +### with your data + +Pass a configuration file as a volume +and share the local directory to store database: + +```bash +mkdir conf && cp ejabberd.yml.example conf/ejabberd.yml + +mkdir database && chown ejabberd database + +docker run --name ejabberd -it \ + -v $(pwd)/conf/ejabberd.yml:/opt/ejabberd/conf/ejabberd.yml \ + -v $(pwd)/database:/opt/ejabberd/database \ + -p 5222:5222 ghcr.io/processone/ejabberd live +``` + +Notice that ejabberd runs in the container with an account named `ejabberd` +with UID 9000 and group `ejabberd` with GID 9000, +and the volumes you mount must grant proper rights to that account. + + +Next steps +---------- + +### Register admin account + +#### [![ejabberd Container](https://img.shields.io/badge/ejabberd-grey?logo=opencontainersinitiative&logoColor=2094f3)](https://github.com/processone/ejabberd/pkgs/container/ejabberd) [:orange_circle:](#images-comparison) + +If you set the `REGISTER_ADMIN_PASSWORD` environment variable, +an account is automatically registered with that password, +and admin privileges are granted to it. +The account created depends on what variables you have set: + +- `EJABBERD_MACRO_ADMIN=juliet@example.org` -> `juliet@example.org` +- `EJABBERD_MACRO_HOST=example.org` -> `admin@example.org` +- None of those variables are set -> `admin@localhost` + +The account registration is shown in the container log: + +``` +:> ejabberdctl register admin example.org somePassw0rd +User admin@example.org successfully registered +``` + +Alternatively, you can register the account manually yourself +and edit `conf/ejabberd.yml` and add the ACL as explained in +[ejabberd Docs: Administration Account](https://docs.ejabberd.im/admin/install/next-steps/#administration-account). + +--- + +#### [![ecs Container](https://img.shields.io/badge/ecs-grey?logo=docker&logoColor=2094f3)](https://hub.docker.com/r/ejabberd/ecs/) + +The default ejabberd configuration has already granted admin privilege +to an account that would be called `admin@localhost`, +so you just need to register it, for example: + +```bash +docker exec -it ejabberd ejabberdctl register admin localhost passw0rd +``` + +### Check ejabberd log + +Check the content of the log files inside the container, +even if you do not put it on a shared persistent drive: + +```bash +docker exec -it ejabberd tail -f logs/ejabberd.log +``` + + +### Inspect container files + +The container uses Alpine Linux. Start a shell inside the container: + +```bash +docker exec -it ejabberd sh +``` + + +### Open debug console + +Open an interactive debug Erlang console attached to a running ejabberd in a running container: + +```bash +docker exec -it ejabberd ejabberdctl debug +``` + + +### CAPTCHA + +ejabberd includes two example CAPTCHA scripts. +If you want to use any of them, first install some additional required libraries: + +```bash +docker exec --user root ejabberd apk add imagemagick ghostscript-fonts bash +``` + +Now update your ejabberd configuration file, for example: +```bash +docker exec -it ejabberd vi conf/ejabberd.yml +``` + +and add this option: +```yaml +captcha_cmd: "$HOME/bin/captcha.sh" +``` + +Finally, reload the configuration file or restart the container: +```bash +docker exec ejabberd ejabberdctl reload_config +``` + +If the CAPTCHA image is not visible, there may be a problem generating it +(the ejabberd log file may show some error message); +or the image URL may not be correctly detected by ejabberd, +in that case you can set the correct URL manually, for example: +```yaml +captcha_url: https://localhost:5443/captcha +``` + +For more details about CAPTCHA options, please check the +[CAPTCHA](https://docs.ejabberd.im/admin/configuration/basic/#captcha) +documentation section. + + +Advanced +-------- + +### Ports + +The container image exposes several ports +(check also [Docs: Firewall Settings](https://docs.ejabberd.im/admin/guide/security/#firewall-settings)): + +- `5222`: The default port for XMPP clients. +- `5269`: For XMPP federation. Only needed if you want to communicate with users on other servers. +- `5280`: For admin interface (URL is `admin/`). +- `1880`: For admin interface (URL is `/`, useful for [podman-desktop](https://podman-desktop.io/) and [docker-desktop](https://www.docker.com/products/docker-desktop/)) [:orange_circle:](#images-comparison) +- `5443`: With encryption, used for admin interface, API, CAPTCHA, OAuth, Websockets and XMPP BOSH. +- `1883`: Used for MQTT +- `4369-4399`: EPMD and Erlang connectivity, used for `ejabberdctl` and clustering +- `5210`: Erlang connectivity when `ERL_DIST_PORT` is set, alternative to EPMD [:orange_circle:](#images-comparison) + + +### Volumes + +ejabberd produces two types of data: log files and database spool files (Mnesia). +This is the kind of data you probably want to store on a persistent or local drive (at least the database). + +The volumes you may want to map: + +- `/opt/ejabberd/conf/`: Directory containing configuration and certificates +- `/opt/ejabberd/database/`: Directory containing Mnesia database. +You should back up or export the content of the directory to persistent storage +(host storage, local storage, any storage plugin) +- `/opt/ejabberd/logs/`: Directory containing log files +- `/opt/ejabberd/upload/`: Directory containing uploaded files. This should also be backed up. + +All these files are owned by an account named `ejabberd` with group `ejabberd` in the container. +Its corresponding `UID:GID` is `9000:9000`. +If you prefer bind mounts instead of volumes, then +you need to map this to valid `UID:GID` on your host to get read/write access on +mounted directories. + +If using Docker, try: +```bash +mkdir database +sudo chown 9000:9000 database +``` + +If using Podman, try: +```bash +mkdir database +podman unshare chown 9000:9000 database +``` + +It's possible to install additional ejabberd modules using volumes, check +[this Docs tutorial](https://docs.ejabberd.im/developer/extending-ejabberd/modules/#your-module-in-ejabberd-modules-with-ejabberd-container). + + +### Commands on start + +The ejabberdctl script reads the `CTL_ON_CREATE` environment variable +the first time the container is started, +and reads `CTL_ON_START` every time the container is started. +Those variables can contain one ejabberdctl command, +or several commands separated with the blankspace and `;` characters. + +If any of those commands returns a failure, the container starting gets aborted. +If there is a command with a result that can be ignored, +prefix that command with `!` + +This example, registers an `admin@localhost` account when the container is first created. +Everytime the container starts, it shows the list of registered accounts, +checks that the admin account exists and password is valid, +changes the password of an account if it exists (ignoring any failure), +and shows the ejabberd starts (check also the [full example](#customized-example)): +```yaml + environment: + - CTL_ON_CREATE=register admin localhost asd + - CTL_ON_START=stats registeredusers ; + check_password admin localhost asd ; + ! change_password bot123 localhost qqq ; + status +``` + + +### Macros in environment [:high_brightness:](#images-comparison) + +ejabberd reads `EJABBERD_MACRO_*` environment variables +and uses them to define the corresponding +[macros](https://docs.ejabberd.im/admin/configuration/file-format/#macros-in-configuration-file), +overwriting the corresponding macro definition if it was set in the configuration file. +This is supported since ejabberd 24.12. + +For example, if you configure this in `ejabberd.yml`: + +```yaml +acl: + admin: + user: ADMIN +``` + +now you can define the admin account JID using an environment variable: +```yaml + environment: + - EJABBERD_MACRO_ADMIN=admin@localhost +``` + +Check the [full example](#customized-example) for other example. + + +### ejabberd-contrib + +This section addresses those topics related to +[ejabberd-contrib](https://docs.ejabberd.im/admin/guide/modules/#ejabberd-contrib): + +- [Download source code](#download-source-code) +- [Install a module](#install-a-module) +- [Install git for dependencies](#install-git-for-dependencies) +- [Install your module](#install-your-module) + +--- + +#### Download source code + +The `ejabberd` container image includes the ejabberd-contrib git repository source code, +but `ecs` does not, so first download it: +```bash +$ docker exec ejabberd ejabberdctl modules_update_specs +``` + +#### Install a module + +Compile and install any of the contributed modules, for example: +```bash +docker exec ejabberd ejabberdctl module_install mod_statsdx + +Module mod_statsdx has been installed and started. +It's configured in the file: + /opt/ejabberd/.ejabberd-modules/mod_statsdx/conf/mod_statsdx.yml +Configure the module in that file, or remove it +and configure in your main ejabberd.yml +``` + +#### Install git for dependencies + +Some modules depend on erlang libraries, +but the container images do not include `git` or `mix` to download them. +Consequently, when you attempt to install such a module, +there will be error messages like: + +```bash +docker exec ejabberd ejabberdctl module_install ejabberd_observer_cli + +I'll download "recon" using git because I can't use Mix to fetch from hex.pm: + /bin/sh: mix: not found +Fetching dependency observer_cli: + /bin/sh: git: not found +... +``` + +the solution is to install `git` in the container image: + +```bash +docker exec --user root ejabberd apk add git + +fetch https://dl-cdn.alpinelinux.org/alpine/v3.21/main/x86_64/APKINDEX.tar.gz +fetch https://dl-cdn.alpinelinux.org/alpine/v3.21/community/x86_64/APKINDEX.tar.gz +(1/3) Installing pcre2 (10.43-r0) +(2/3) Installing git (2.47.2-r0) +(3/3) Installing git-init-template (2.47.2-r0) +Executing busybox-1.37.0-r12.trigger +OK: 27 MiB in 42 packages +``` + +and now you can upgrade the module: + +```bash +docker exec ejabberd ejabberdctl module_upgrade ejabberd_observer_cli + +I'll download "recon" using git because I can't use Mix to fetch from hex.pm: +/bin/sh: mix: not found +Fetching dependency observer_cli: Cloning into 'observer_cli'... +Fetching dependency os_stats: Cloning into 'os_stats'... +Fetching dependency recon: Cloning into 'recon'... +Inlining: inline_size=24 inline_effort=150 +Old inliner: threshold=0 functions=[{insert,2},{merge,2}] +Module ejabberd_observer_cli has been installed. +Now you can configure it in your ejabberd.yml +I'll download "recon" using git because I can't use Mix to fetch from hex.pm: +/bin/sh: mix: not found +``` + +#### Install your module + +If you [developed an ejabberd module](https://docs.ejabberd.im/developer/extending-ejabberd/modules/), +you can install it in your container image: + +1. Create a local directory for `ejabberd-modules`: + + ``` sh + mkdir docker-modules + ``` + +2. Then create the directory structure for your custom module: + + ``` sh + cd docker-modules + + mkdir -p sources/mod_hello_world/ + touch sources/mod_hello_world/mod_hello_world.spec + + mkdir sources/mod_hello_world/src/ + mv mod_hello_world.erl sources/mod_hello_world/src/ + + mkdir sources/mod_hello_world/conf/ + echo -e "modules:\n mod_hello_world: {}" > sources/mod_hello_world/conf/mod_hello_world.yml + + cd .. + ``` + +3. Grant ownership of that directory to the UID that ejabberd will use inside the Docker image: + + ``` sh + sudo chown 9000 -R docker-modules/ + ``` + +4. Start ejabberd in the container: + + ``` sh + sudo docker run \ + --name hellotest \ + -d \ + --volume "$(pwd)/docker-modules:/home/ejabberd/.ejabberd-modules/" \ + -p 5222:5222 \ + -p 5280:5280 \ + ejabberd/ecs + ``` + +5. Check the module is available for installing, and then install it: + + ``` sh + sudo docker exec -it hellotest ejabberdctl modules_available + mod_hello_world [] + + sudo docker exec -it hellotest ejabberdctl module_install mod_hello_world + ``` + +6. If the module works correctly, you will see `Hello` in the ejabberd logs when it starts: + + ``` sh + sudo docker exec -it hellotest grep Hello logs/ejabberd.log + 2020-10-06 13:40:13.154335+00:00 [info] + <0.492.0>@mod_hello_world:start/2:15 Hello, ejabberd world! + ``` + + +### ejabberdapi + +When the container is running (and thus ejabberd), you can exec commands inside the container +using `ejabberdctl` or any other of the available interfaces, see +[Understanding ejabberd "commands"](https://docs.ejabberd.im/developer/ejabberd-api/#understanding-ejabberd-commands) + +Additionally, the container image includes the `ejabberdapi` executable. +Please check the [ejabberd-api homepage](https://github.com/processone/ejabberd-api) +for configuration and usage details. + +For example, if you configure ejabberd like this: +```yaml +listen: + - + port: 5282 + module: ejabberd_http + request_handlers: + "/api": mod_http_api + +acl: + loopback: + ip: + - 127.0.0.0/8 + - ::1/128 + - ::FFFF:127.0.0.1/128 + +api_permissions: + "admin access": + who: + access: + allow: + acl: loopback + what: + - "register" +``` + +Then you could register new accounts with this query: + +```bash +docker exec -it ejabberd ejabberdapi register --endpoint=http://127.0.0.1:5282/ --jid=admin@localhost --password=passw0rd +``` + + +### Clustering + +When setting several containers to form a +[cluster of ejabberd nodes](https://docs.ejabberd.im/admin/guide/clustering/), +each one must have a different +[Erlang Node Name](https://docs.ejabberd.im/admin/guide/security/#erlang-node-name) +and the same +[Erlang Cookie](https://docs.ejabberd.im/admin/guide/security/#erlang-cookie). + +For this you can either: + +- edit `conf/ejabberdctl.cfg` and set variables `ERLANG_NODE` and `ERLANG_COOKIE` +- set the environment variables `ERLANG_NODE_ARG` and `ERLANG_COOKIE` + +--- + +Example to connect a local `ejabberdctl` to a containerized ejabberd: + +1. When creating the container, export port 5210, and set `ERLANG_COOKIE`: + ```sh + docker run --name ejabberd -it \ + -e ERLANG_COOKIE=`cat $HOME/.erlang.cookie` \ + -p 5210:5210 -p 5222:5222 \ + ghcr.io/processone/ejabberd + ``` +2. Set `ERL_DIST_PORT=5210` in `ejabberdctl.cfg` of container and local ejabberd +3. Restart the container +4. Now use `ejabberdctl` in your local ejabberd deployment + +To connect using a local `ejabberd` script: +```sh +ERL_DIST_PORT=5210 _build/dev/rel/ejabberd/bin/ejabberd ping +``` + +Example using environment variables (see full example [docker-compose.yml](https://github.com/processone/docker-ejabberd/issues/64#issuecomment-887741332)): +```yaml + environment: + - ERLANG_NODE_ARG=ejabberd@node7 + - ERLANG_COOKIE=dummycookie123 +``` + +--- + +Once you have the ejabberd nodes properly set and running, +you can tell the secondary nodes to join the master node using the +[`join_cluster`](https://docs.ejabberd.im/developer/ejabberd-api/admin-api/#join-cluster) +API call. + +Example using environment variables (see the full +[`docker-compose.yml` clustering example](#clustering-example)): +```yaml +environment: + - ERLANG_NODE_ARG=ejabberd@replica + - ERLANG_COOKIE=dummycookie123 + - CTL_ON_CREATE=join_cluster ejabberd@main +``` + +### Change Mnesia Node Name + +To use the same Mnesia database in a container with a different hostname, +it is necessary to change the old hostname stored in Mnesia. + +This section is equivalent to the ejabberd Documentation +[Change Computer Hostname](https://docs.ejabberd.im/admin/guide/managing/#change-computer-hostname), +but particularized to containers that use this +`ecs` container image from ejabberd 23.01 or older. + +#### Setup Old Container + +Let's assume a container running ejabberd 23.01 (or older) from +this `ecs` container image, with the database directory binded +and one registered account. +This can be produced with: +```bash +OLDCONTAINER=ejaold +NEWCONTAINER=ejanew + +mkdir database +sudo chown 9000:9000 database +docker run -d --name $OLDCONTAINER -p 5222:5222 \ + -v $(pwd)/database:/opt/ejabberd/database \ + ghcr.io/processone/ejabberd:23.01 +docker exec -it $OLDCONTAINER ejabberdctl started +docker exec -it $OLDCONTAINER ejabberdctl register user1 localhost somepass +docker exec -it $OLDCONTAINER ejabberdctl registered_users localhost +``` + +Methods to know the Erlang node name: +```bash +ls database/ | grep ejabberd@ +docker exec -it $OLDCONTAINER ejabberdctl status +docker exec -it $OLDCONTAINER grep "started in the node" logs/ejabberd.log +``` + +#### Change Mnesia Node + +First of all let's store the Erlang node names and paths in variables. +In this example they would be: +```bash +OLDCONTAINER=ejaold +NEWCONTAINER=ejanew +OLDNODE=ejabberd@95145ddee27c +NEWNODE=ejabberd@localhost +OLDFILE=/opt/ejabberd/database/old.backup +NEWFILE=/opt/ejabberd/database/new.backup +``` + +1. Start your old container that can still read the Mnesia database correctly. +If you have the Mnesia spool files, +but don't have access to the old container anymore, go to +[Create Temporary Container](#create-temporary-container) +and later come back here. + +2. Generate a backup file and check it was created: +```bash +docker exec -it $OLDCONTAINER ejabberdctl backup $OLDFILE +ls -l database/*.backup +``` + +3. Stop ejabberd: +```bash +docker stop $OLDCONTAINER +``` + +4. Create the new container. For example: +```bash +docker run \ + --name $NEWCONTAINER \ + -d \ + -p 5222:5222 \ + -v $(pwd)/database:/opt/ejabberd/database \ + ghcr.io/processone/ejabberd:latest +``` + +5. Convert the backup file to new node name: +```bash +docker exec -it $NEWCONTAINER ejabberdctl mnesia_change_nodename $OLDNODE $NEWNODE $OLDFILE $NEWFILE +``` + +6. Install the backup file as a fallback: +```bash +docker exec -it $NEWCONTAINER ejabberdctl install_fallback $NEWFILE +``` + +7. Restart the container: +```bash +docker restart $NEWCONTAINER +``` + +8. Check that the information of the old database is available. +In this example, it should show that the account `user1` is registered: +```bash +docker exec -it $NEWCONTAINER ejabberdctl registered_users localhost +``` + +9. When the new container is working perfectly with the converted Mnesia database, +you may want to remove the unneeded files: +the old container, the old Mnesia spool files, and the backup files. + +#### Create Temporary Container + +In case the old container that used the Mnesia database is not available anymore, +a temporary container can be created just to read the Mnesia database +and make a backup of it, as explained in the previous section. + +This method uses `--hostname` command line argument for docker, +and `ERLANG_NODE_ARG` environment variable for ejabberd. +Their values must be the hostname of your old container +and the Erlang node name of your old ejabberd node. +To know the Erlang node name please check +[Setup Old Container](#setup-old-container). + +Command line example: +```bash +OLDHOST=${OLDNODE#*@} +docker run \ + -d \ + --name $OLDCONTAINER \ + --hostname $OLDHOST \ + -p 5222:5222 \ + -v $(pwd)/database:/opt/ejabberd/database \ + -e ERLANG_NODE_ARG=$OLDNODE \ + ghcr.io/processone/ejabberd:latest +``` + +Check the old database content is available: +```bash +docker exec -it $OLDCONTAINER ejabberdctl registered_users localhost +``` + +Now that you have ejabberd running with access to the Mnesia database, +you can continue with step 2 of previous section +[Change Mnesia Node](#change-mnesia-node). + + +Build Container Image +---------------- + +The container image includes ejabberd as a standalone OTP release built using Elixir. + +### Build `ejabberd` [![ejabberd Container](https://img.shields.io/badge/ejabberd-grey?logo=opencontainersinitiative&logoColor=2094f3)](https://github.com/processone/ejabberd/pkgs/container/ejabberd) + +The ejabberd Erlang/OTP release is configured with: + +- `mix.exs`: Customize ejabberd release +- `vars.config`: ejabberd compilation configuration options +- `config/runtime.exs`: Customize ejabberd paths +- `ejabberd.yml.template`: ejabberd default config file + +#### Direct build + +Build ejabberd Community Server container image from ejabberd master git repository: + +```bash +docker buildx build \ + -t personal/ejabberd \ + -f .github/container/Dockerfile \ + . +``` + +#### Podman build + +To build the image using Podman, please notice: + +- `EXPOSE 4369-4399` port range is not supported, remove that in Dockerfile +- It mentions that `healthcheck` is not supported by the Open Container Initiative image format +- to start with command `live`, you may want to add environment variable `EJABBERD_BYPASS_WARNINGS=true` + +```bash +podman build \ + -t ejabberd \ + -f .github/container/Dockerfile \ + . + +podman run --name eja1 -d -p 5222:5222 localhost/ejabberd + +podman exec eja1 ejabberdctl status + +podman exec -it eja1 sh + +podman stop eja1 + +podman run --name eja1 -it -e EJABBERD_BYPASS_WARNINGS=true -p 5222:5222 localhost/ejabberd live +``` + +### Build `ecs` [![ecs Container](https://img.shields.io/badge/ecs-grey?logo=docker&logoColor=2094f3)](https://hub.docker.com/r/ejabberd/ecs/) + +The ejabberd Erlang/OTP release is configured with: + +- `rel/config.exs`: Customize ejabberd release +- `rel/dev.exs`: ejabberd environment configuration for development release +- `rel/prod.exs`: ejabberd environment configuration for production release +- `vars.config`: ejabberd compilation configuration options +- `conf/ejabberd.yml`: ejabberd default config file + +Build ejabberd Community Server base image from ejabberd master on Github: + +```bash +docker build -t personal/ejabberd . +``` + +Build ejabberd Community Server base image for a given ejabberd version: + +```bash +./build.sh 18.03 +``` + +Composer Examples +----------------- + +### Minimal Example + +This is the barely minimal file to get a usable ejabberd. + +If using Docker, write this `docker-compose.yml` file +and start it with `docker-compose up`: + +```yaml +services: + main: + image: ghcr.io/processone/ejabberd + container_name: ejabberd + ports: + - "5222:5222" + - "5269:5269" + - "5280:5280" + - "5443:5443" +``` + +If using Podman, write this `minimal.yml` file +and start it with `podman kube play minimal.yml`: + +```yaml +apiVersion: v1 + +kind: Pod + +metadata: + name: ejabberd + +spec: + containers: + + - name: ejabberd + image: ghcr.io/processone/ejabberd + ports: + - containerPort: 5222 + hostPort: 5222 + - containerPort: 5269 + hostPort: 5269 + - containerPort: 5280 + hostPort: 5280 + - containerPort: 5443 + hostPort: 5443 +``` + + +### Customized Example + +This example shows the usage of several customizations: +it uses a local configuration file, +defines a configuration macro using an environment variable, +stores the mnesia database in a local path, +registers an account when it's created, +and checks the number of registered accounts every time it's started. + +Prepare an ejabberd configuration file: +```bash +mkdir conf && cp ejabberd.yml.example conf/ejabberd.yml +``` + +Create the database directory and allow the container access to it: + +- Docker: + ```bash + mkdir database && sudo chown 9000:9000 database + ``` +- Podman: + ```bash + mkdir database && podman unshare chown 9000:9000 database + ``` + +If using Docker, write this `docker-compose.yml` file +and start it with `docker-compose up`: + +```yaml +version: '3.7' + +services: + + main: + image: ghcr.io/processone/ejabberd + container_name: ejabberd + environment: + - EJABBERD_MACRO_HOST=example.com + - EJABBERD_MACRO_ADMIN=admin@example.com + - REGISTER_ADMIN_PASSWORD=somePassw0rd + - CTL_ON_START=registered_users example.com ; + status + ports: + - "5222:5222" + - "5269:5269" + - "5280:5280" + - "5443:5443" + volumes: + - ./conf/ejabberd.yml:/opt/ejabberd/conf/ejabberd.yml:ro + - ./database:/opt/ejabberd/database +``` + +If using Podman, write this `custom.yml` file +and start it with `podman kube play custom.yml`: + +```yaml +apiVersion: v1 + +kind: Pod + +metadata: + name: ejabberd + +spec: + containers: + + - name: ejabberd + image: ghcr.io/processone/ejabberd + env: + - name: EJABBERD_MACRO_HOST + value: example.com + - name: EJABBERD_MACRO_ADMIN + value: admin@example.com + - name: REGISTER_ADMIN_PASSWORD + value: somePassw0rd + - name: CTL_ON_START + value: registered_users example.com ; + status + ports: + - containerPort: 5222 + hostPort: 5222 + - containerPort: 5269 + hostPort: 5269 + - containerPort: 5280 + hostPort: 5280 + - containerPort: 5443 + hostPort: 5443 + volumeMounts: + - mountPath: /opt/ejabberd/conf/ejabberd.yml + name: config + readOnly: true + - mountPath: /opt/ejabberd/database + name: db + + volumes: + - name: config + hostPath: + path: ./conf/ejabberd.yml + type: File + - name: db + hostPath: + path: ./database + type: DirectoryOrCreate +``` + + +### Clustering Example + +In this example, the main container is created first. +Once it is fully started and healthy, a second container is created, +and once ejabberd is started in it, it joins the first one. + +An account is registered in the first node when created (and +we ignore errors that can happen when doing that - for example +when account already exists), +and it should exist in the second node after join. + +Notice that in this example the main container does not have access +to the exterior; the replica exports the ports and can be accessed. + +If using Docker, write this `docker-compose.yml` file +and start it with `docker-compose up`: + +```yaml +version: '3.7' + +services: + + main: + image: ghcr.io/processone/ejabberd + container_name: main + environment: + - ERLANG_NODE_ARG=ejabberd@main + - ERLANG_COOKIE=dummycookie123 + - CTL_ON_CREATE=! register admin localhost asd + healthcheck: + test: netstat -nl | grep -q 5222 + start_period: 5s + interval: 5s + timeout: 5s + retries: 120 + + replica: + image: ghcr.io/processone/ejabberd + container_name: replica + depends_on: + main: + condition: service_healthy + environment: + - ERLANG_NODE_ARG=ejabberd@replica + - ERLANG_COOKIE=dummycookie123 + - CTL_ON_CREATE=join_cluster ejabberd@main + - CTL_ON_START=registered_users localhost ; + status + ports: + - "5222:5222" + - "5269:5269" + - "5280:5280" + - "5443:5443" +``` + +If using Podman, write this `cluster.yml` file +and start it with `podman kube play cluster.yml`: + +```yaml +apiVersion: v1 + +kind: Pod + +metadata: + name: cluster + +spec: + containers: + + - name: first + image: ghcr.io/processone/ejabberd + env: + - name: ERLANG_NODE_ARG + value: main@cluster + - name: ERLANG_COOKIE + value: dummycookie123 + - name: CTL_ON_CREATE + value: register admin localhost asd + - name: CTL_ON_START + value: stats registeredusers ; + status + - name: EJABBERD_MACRO_PORT_C2S + value: 6222 + - name: EJABBERD_MACRO_PORT_C2S_TLS + value: 6223 + - name: EJABBERD_MACRO_PORT_S2S + value: 6269 + - name: EJABBERD_MACRO_PORT_HTTP_TLS + value: 6443 + - name: EJABBERD_MACRO_PORT_HTTP + value: 6280 + - name: EJABBERD_MACRO_PORT_MQTT + value: 6883 + - name: EJABBERD_MACRO_PORT_PROXY65 + value: 6777 + volumeMounts: + - mountPath: /opt/ejabberd/conf/ejabberd.yml + name: config + readOnly: true + + - name: second + image: ghcr.io/processone/ejabberd + env: + - name: ERLANG_NODE_ARG + value: replica@cluster + - name: ERLANG_COOKIE + value: dummycookie123 + - name: CTL_ON_CREATE + value: join_cluster main@cluster ; + started ; + list_cluster + - name: CTL_ON_START + value: stats registeredusers ; + check_password admin localhost asd ; + status + ports: + - containerPort: 5222 + hostPort: 5222 + - containerPort: 5280 + hostPort: 5280 + volumeMounts: + - mountPath: /opt/ejabberd/conf/ejabberd.yml + name: config + readOnly: true + + volumes: + - name: config + hostPath: + path: ./conf/ejabberd.yml + type: File + +``` + + +Images Comparison +----------------- + +Let's summarize the differences between both container images. Legend: + +- :sparkle: is the recommended alternative +- :orange_circle: added in the latest release (ejabberd 25.03) +- :high_brightness: added in the previous release (ejabberd 24.12) +- :low_brightness: added in the pre-previous release (ejabberd 24.10) + +| | [![ejabberd Container](https://img.shields.io/badge/ejabberd-grey?logo=opencontainersinitiative&logoColor=2094f3)](https://github.com/processone/ejabberd/pkgs/container/ejabberd) | [![ecs Container](https://img.shields.io/badge/ecs-grey?logo=docker&logoColor=2094f3)](https://hub.docker.com/r/ejabberd/ecs/) | +|:----------------------|:------------------|:-----------------------| +| Source code | [ejabberd/.github/container](https://github.com/processone/ejabberd/tree/master/.github/container) | [docker-ejabberd/ecs](https://github.com/processone/docker-ejabberd/tree/master/ecs) | +| Generated by | [container.yml](https://github.com/processone/ejabberd/blob/master/.github/workflows/container.yml) | [tests.yml](https://github.com/processone/docker-ejabberd/blob/master/.github/workflows/tests.yml) | +| Built for | stable releases
`master` branch | stable releases
[`master` branch zip](https://github.com/processone/docker-ejabberd/actions/workflows/tests.yml) | +| Architectures | `linux/amd64`
`linux/arm64` | `linux/amd64` | +| Software | Erlang/OTP 27.3.4.3-alpine
Elixir 1.18.4 | Alpine 3.22
Erlang/OTP 26.2
Elixir 1.18.3 | +| Published in | [ghcr.io/processone/ejabberd](https://github.com/processone/ejabberd/pkgs/container/ejabberd) | [docker.io/ejabberd/ecs](https://hub.docker.com/r/ejabberd/ecs/)
[ghcr.io/processone/ecs](https://github.com/processone/docker-ejabberd/pkgs/container/ecs) | +| :black_square_button: **Additional content** | +| [ejabberd-contrib](#ejabberd-contrib) | included | not included | +| [ejabberdapi](#ejabberdapi) | included :orange_circle: | included | +| :black_square_button: **Ports** | +| [1880](#ports) for WebAdmin | yes :orange_circle: | yes :orange_circle: | +| [5210](#ports) for `ERL_DIST_PORT` | supported | supported :orange_circle: | +| :black_square_button: **Paths** | +| `$HOME` | `/opt/ejabberd/` | `/home/ejabberd/` | +| User data | `$HOME` :sparkle:
`/home/ejabberd/` :orange_circle: | `$HOME`
`/opt/ejabberd/` :sparkle: :low_brightness: | +| `ejabberdctl` | `ejabberdctl` :sparkle:
`bin/ejabberdctl` :orange_circle: | `bin/ejabberdctl`
`ejabberdctl` :sparkle: :low_brightness: | +| [`captcha.sh`](#captcha) | `$HOME/bin/captcha.sh` :orange_circle: | `$HOME/bin/captcha.sh` :orange_circle: | +| `*.sql` files | `$HOME/sql/*.sql` :sparkle: :orange_circle:
`$HOME/database/*.sql` :orange_circle: | `$HOME/database/*.sql`
`$HOME/sql/*.sql` :sparkle: :orange_circle: | +| Mnesia spool files | `$HOME/database/` :sparkle:
`$HOME/database/NODENAME/` :orange_circle: | `$HOME/database/NODENAME/`
`$HOME/database/` :sparkle: :orange_circle: | +| :black_square_button: **Variables** | +| [`EJABBERD_MACRO_*`](#macros-in-environment) | supported :high_brightness: | supported :high_brightness: | +| Macros used in `ejabberd.yml` | yes :orange_circle: | yes :orange_circle: | +| [`EJABBERD_MACRO_ADMIN`](#register-admin-account) | Grant admin rights :orange_circle:
(default `admin@localhost`)
| Hardcoded `admin@localhost` | +| [`REGISTER_ADMIN_PASSWORD`](#register-admin-account) | Register admin account :orange_circle: | unsupported | +| `CTL_OVER_HTTP` | enabled :orange_circle: | unsupported | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..819921ee5 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,149 @@ +# Contributing to ejabberd + +We'd love for you to contribute to our source code and to make ejabberd even better than it is +today! Here are the guidelines we'd like you to follow: + +* [Code of Conduct](#code-of-conduct) +* [Questions and Problems](#questions-bugs-features) +* [Issues and Bugs](#found-an-issue-or-bug) +* [Feature Requests](#missing-a-feature) +* [Issue Submission Guidelines](#issue-submission-guidelines) +* [Pull Request Submission Guidelines](#pull-request-submission-guidelines) +* [Signing the CLA](#signing-the-contributor-license-agreement-cla) + +## Code of Conduct + +Help us keep ejabberd community open-minded and inclusive. Please read and follow our [Code of Conduct][coc]. + +## Questions, Bugs, Features + +### Got a Question or Problem? + +Do not open issues for general support questions as we want to keep GitHub issues for bug reports +and feature requests. You've got much better chances of getting your question answered on dedicated +support platforms, the best being [Stack Overflow][stackoverflow]. + +Stack Overflow is a much better place to ask questions since: + +* there are thousands of people willing to help on Stack Overflow +* questions and answers stay available for public viewing so your question / answer might help + someone else +* Stack Overflow's voting system assures that the best answers are prominently visible. + +To save your and our time, we will systematically close all issues that are requests for general +support and redirect people to the section you are reading right now. + +Other channels for support are: + +* ejabberd XMPP room: [ejabberd@conference.process-one.net][muc] +* [ejabberd Mailing List][list] + +### Found an Issue or Bug? + +If you find a bug in the source code, you can help us by submitting an issue to our +[GitHub Repository][github]. Even better, you can submit a Pull Request with a fix. + +### Missing a Feature? + +You can request a new feature by submitting an issue to our [GitHub Repository][github-issues]. + +If you would like to implement a new feature then consider what kind of change it is: + +* **Major Changes** that you wish to contribute to the project should be discussed first in an + [GitHub issue][github-issues] that clearly outlines the changes and benefits of the feature. +* **Small Changes** can directly be crafted and submitted to the [GitHub Repository][github] + as a Pull Request. See the section about [Pull Request Submission Guidelines](#pull-request-submission-guidelines). + +## Issue Submission Guidelines + +Before you submit your issue search the archive, maybe your question was already answered. + +If your issue appears to be a bug, and hasn't been reported, open a new issue. Help us to maximize +the effort we can spend fixing issues and adding new features, by not reporting duplicate issues. + +The "[new issue][github-new-issue]" form contains a number of prompts that you should fill out to +make it easier to understand and categorize the issue. + +## Pull Request Submission Guidelines + +By submitting a pull request for a code or doc contribution, you need to have the right +to grant your contribution's copyright license to ProcessOne. Please check [ProcessOne CLA][cla] +for details. + +Before you submit your pull request consider the following guidelines: + +* Search [GitHub][github-pr] for an open or closed Pull Request + that relates to your submission. You don't want to duplicate effort. +* Create the [development environment][developer-setup] +* Make your changes in a new git branch: + + ```shell + git checkout -b my-fix-branch master + ``` + +* Test your changes and, if relevant, expand the automated test suite. +* Create your patch commit, including appropriate test cases. +* If the changes affect public APIs, change or add relevant [documentation][doc-repo]. +* Commit your changes using a descriptive commit message. + + ```shell + git commit -a + ``` + + Note: the optional commit `-a` command line option will automatically "add" and "rm" edited files. + +* Push your branch to GitHub: + + ```shell + git push origin my-fix-branch + ``` + +* In GitHub, send a pull request to `ejabberd:master`. This will trigger the automated testing. +We will also notify you if you have not yet signed the [contribution agreement][cla]. + +* If you find that the tests have failed, look into the logs to find out +if your changes caused test failures, the commit message was malformed etc. If you find that the +tests failed or times out for unrelated reasons, you can ping a team member so that the build can be +restarted. + +* If we suggest changes, then: + + * Make the required updates. + * Test your changes and test cases. + * Commit your changes to your branch (e.g. `my-fix-branch`). + * Push the changes to your GitHub repository (this will update your Pull Request). + + You can also amend the initial commits and force push them to the branch. + + ```shell + git rebase master -i + git push origin my-fix-branch -f + ``` + + This is generally easier to follow, but separate commits are useful if the Pull Request contains + iterations that might be interesting to see side-by-side. + +That's it! Thank you for your contribution! + +## Signing the Contributor License Agreement (CLA) + +Upon submitting a Pull Request, we will ask you to sign our CLA if you haven't done +so before. It's a quick process, we promise, and you will be able to do it all online + +Here's a link to the [ProcessOne Contribution License Agreement][cla]. + +This is part of the legal framework of the open-source ecosystem that adds some red tape, +but protects both the contributor and the company / foundation behind the project. It also +gives us the option to relicense the code with a more permissive license in the future. + +[coc]: https://github.com/processone/ejabberd/blob/master/CODE_OF_CONDUCT.md +[stackoverflow]: https://stackoverflow.com/questions/tagged/ejabberd?sort=newest +[list]: https://web.archive.org/web/20230319174915/http://lists.jabber.ru/mailman/listinfo/ejabberd +[muc]: xmpp:ejabberd@conference.process-one.net +[github]: https://github.com/processone/ejabberd +[github-issues]: https://github.com/processone/ejabberd/issues +[github-new-issue]: https://github.com/processone/ejabberd/issues/new +[github-pr]: https://github.com/processone/ejabberd/pulls +[doc-repo]: https://github.com/processone/docs.ejabberd.im +[developer-setup]: https://docs.ejabberd.im/developer/ +[cla]: https://cla.process-one.net/ diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md new file mode 100644 index 000000000..50c7d614a --- /dev/null +++ b/CONTRIBUTORS.md @@ -0,0 +1,37 @@ +# Contributors + +We would like to thanks official ejabberd source code contributors: + +- Sergey Abramyan +- Badlop +- Ludovic Bocquet +- Emilio Bustos +- Thiago Camargo +- Juan Pablo Carlino +- Paweł Chmielowski +- Gabriel Gatu +- Tsukasa Hamano +- Konstantinos Kallas +- Evgeny Khramtsov +- Ben Langfeld +- Peter Lemenkov +- Anna Mukharram +- Johan Oudinet +- Pablo Polvorin +- Mickaël Rémond +- Matthias Rieber +- Rafael Roemhild +- Christophe Romain +- Jérôme Sautret +- Sonny Scroggin +- Alexey Shchepin +- Shelley Shyan +- Radoslaw Szymczyszyn +- Stu Tomlinson +- Christian Ulrich +- Holger Weiß + +Please, if you think we are missing your contribution, do not hesitate to contact us at ProcessOne. +In case you do not want to appear in this list, please, let us know as well. + +Thanks ! diff --git a/Makefile.in b/Makefile.in index 27d55dad6..cf7480702 100644 --- a/Makefile.in +++ b/Makefile.in @@ -1,7 +1,23 @@ -REBAR = @ESCRIPT@ rebar +#. +#' definitions +# + +ESCRIPT = @ESCRIPT@ +REBAR = @rebar@ # rebar|rebar3|mix binary (or path to binary) +REBAR3 = @REBAR3@ # path to rebar3 binary +MIX = @rebar@ +AWK = @AWK@ INSTALL = @INSTALL@ +MKDIR_P = @MKDIR_P@ SED = @SED@ ERL = @ERL@ +EPMD = @EPMD@ +IEX = @IEX@ + +INSTALLUSER=@INSTALLUSER@ +INSTALLGROUP=@INSTALLGROUP@ + +REBAR_ENABLE_ELIXIR = @elixir@ prefix = @prefix@ exec_prefix = @exec_prefix@ @@ -9,21 +25,27 @@ exec_prefix = @exec_prefix@ DESTDIR = # /etc/ejabberd/ -ETCDIR = $(DESTDIR)@sysconfdir@/ejabberd +ETCDIR = @sysconfdir@/ejabberd # /bin/ -BINDIR = $(DESTDIR)@bindir@ +BINDIR = @bindir@ # /sbin/ -SBINDIR = $(DESTDIR)@sbindir@ +SBINDIR = @sbindir@ + +# /lib/ +LIBDIR = @libdir@ # /lib/ejabberd/ -EJABBERDDIR = $(DESTDIR)@libdir@/ejabberd +EJABBERDDIR = @libdir@/ejabberd # /share/doc/ejabberd PACKAGE_TARNAME = @PACKAGE_TARNAME@ datarootdir = @datarootdir@ -DOCDIR = $(DESTDIR)@docdir@ +DOCDIR = @docdir@ + +# /share/doc/man/man5 +MANDIR = @mandir@/man5 # /usr/lib/ejabberd/ebin/ BEAMDIR = $(EJABBERDDIR)/ebin @@ -43,19 +65,31 @@ SODIR = $(PRIVDIR)/lib # /usr/lib/ejabberd/priv/msgs MSGSDIR = $(PRIVDIR)/msgs +# /usr/lib/ejabberd/priv/css +CSSDIR = $(PRIVDIR)/css + +# /usr/lib/ejabberd/priv/img +IMGDIR = $(PRIVDIR)/img + +# /usr/lib/ejabberd/priv/js +JSDIR = $(PRIVDIR)/js + +# /usr/lib/ejabberd/priv/sql +SQLDIR = $(PRIVDIR)/sql + +# /usr/lib/ejabberd/priv/lua +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 +SPOOLDIR = @localstatedir@/lib/ejabberd # /var/log/ejabberd/ -LOGDIR = $(DESTDIR)@localstatedir@/log/ejabberd +LOGDIR = @localstatedir@/log/ejabberd + +#. +#' install user +# -INSTALLUSER=@INSTALLUSER@ # if no user was enabled, don't set privileges or ownership ifeq ($(INSTALLUSER),) O_USER= @@ -71,218 +105,521 @@ else INIT_USER=$(INSTALLUSER) endif -all: deps src +# if no group was enabled, don't set privileges or ownership +ifneq ($(INSTALLGROUP),) + G_USER=-g $(INSTALLGROUP) +endif -deps: deps/.got +#. +#' rebar / rebar3 / mix +# -deps/.got: - rm -rf deps/.got - rm -rf deps/.built - $(REBAR) get-deps && :> deps/.got +ifeq "$(notdir $(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 -deps/.built: deps/.got - $(REBAR) compile && :> deps/.built +ifeq "$(REBAR_VER)" "6" + REBAR=$(MIX) + SKIPDEPS= + LISTDEPS=deps.tree + UPDATEDEPS=deps.update + DEPSPATTERN="s/.*─ \([a-z0-9_]*\) .*/\1/p;" + DEPSBASE=_build + DEPSDIR=$(DEPSBASE)/dev/lib + GET_DEPS= deps.get + CONFIGURE_DEPS=(cd deps/eimp; ./configure) + EBINDIR=$(DEPSDIR)/ejabberd/ebin + XREFOPTIONS=graph + EDOCPRE=MIX_ENV=edoc + EDOCTASK=docs --proglang erlang + CLEANARG=--deps + ELIXIR_LIBDIR_RAW=$(shell elixir -e "IO.puts(:filename.dirname(:code.lib_dir(:elixir)))" -e ":erlang.halt") + ELIXIR_LIBDIR=":$(ELIXIR_LIBDIR_RAW)" + REBARREL=MIX_ENV=prod $(REBAR) release --overwrite + REBARDEV=MIX_ENV=dev $(REBAR) release --overwrite + RELIVECMD=$(ESCRIPT) rel/relive.escript && MIX_ENV=dev RELIVE=true $(IEX) --name ejabberd@localhost -S mix run + REL_LIB_DIR = _build/dev/rel/ejabberd/lib + COPY_REL_TARGET = dev + GET_DEPS_TRANSLATIONS=MIX_ENV=translations $(REBAR) $(GET_DEPS) + DEPSDIR_TRANSLATIONS=deps +else +ifeq ($(REBAR_ENABLE_ELIXIR),true) + ELIXIR_LIBDIR_RAW=$(shell elixir -e "IO.puts(:filename.dirname(:code.lib_dir(:elixir)))" -e ":erlang.halt") + ELIXIR_LIBDIR=":$(ELIXIR_LIBDIR_RAW)" + EXPLICIT_ELIXIR_COMPILE=MIX_ENV=default mix compile.elixir + EXPLICIT_ELIXIR_COMPILE_DEV=MIX_ENV=dev mix compile.elixir + PREPARE_ELIXIR_SCRIPTS=$(MKDIR_P) rel/overlays; cp $(ELIXIR_LIBDIR_RAW)/../bin/iex rel/overlays/; cp $(ELIXIR_LIBDIR_RAW)/../bin/elixir rel/overlays/; sed -i 's|ERTS_BIN=$$|ERTS_BIN=$$SCRIPT_PATH/../../erts-{{erts_vsn}}/bin/|' rel/overlays/elixir +endif +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 + GET_DEPS= get-deps + CONFIGURE_DEPS=$(REBAR) configure-deps + EBINDIR=$(DEPSDIR)/ejabberd/ebin + XREFOPTIONS= + CLEANARG=--all + REBARREL=$(REBAR) as prod tar + REBARDEV=$(REBAR) as dev release + RELIVECMD=$(REBAR) as dev relive + REL_LIB_DIR = _build/dev/rel/ejabberd/lib + COPY_REL_TARGET = dev + GET_DEPS_TRANSLATIONS=$(REBAR) as translations $(GET_DEPS) + DEPSDIR_TRANSLATIONS=_build/translations/lib +else + SKIPDEPS=skip_deps=true + LISTDEPS=-q list-deps + UPDATEDEPS=update-deps + DEPSPATTERN="/ TAG / s/ .*// p; / REV / s/ .*// p; / BRANCH / s/ .*// p;" + DEPSBASE=deps + DEPSDIR=$(DEPSBASE) + GET_DEPS= get-deps + CONFIGURE_DEPS=$(REBAR) configure-deps + EBINDIR=ebin + XREFOPTIONS= + CLEANARG= + REBARREL=$(REBAR) generate + REBARDEV= + RELIVECMD=@echo "Rebar2 detected... relive not supported.\ + \nTry: ./configure --with-rebar=rebar3 ; make relive" + REL_LIB_DIR = rel/ejabberd/lib + COPY_REL_TARGET = rel +endif +endif -src: deps/.built - $(REBAR) skip_deps=true compile +#. +#' main targets +# + +all: scripts deps src + +deps: $(DEPSDIR)/.got + +$(DEPSDIR)/.got: + rm -rf $(DEPSDIR)/.got + rm -rf $(DEPSDIR)/.built + $(MKDIR_P) $(DEPSDIR) + $(REBAR) $(GET_DEPS) && :> $(DEPSDIR)/.got + $(CONFIGURE_DEPS) + +$(DEPSDIR)/.built: $(DEPSDIR)/.got + $(REBAR) compile && :> $(DEPSDIR)/.built + +src: $(DEPSDIR)/.built + $(REBAR) $(SKIPDEPS) compile + $(EXPLICIT_ELIXIR_COMPILE) update: - rm -rf deps/.got - rm -rf deps/.built - $(REBAR) update-deps && :> deps/.got + rm -rf $(DEPSDIR)/.got + rm -rf $(DEPSDIR)/.built + $(REBAR) $(UPDATEDEPS) && :> $(DEPSDIR)/.got + $(CONFIGURE_DEPS) xref: all - $(REBAR) skip_deps=true xref + $(REBAR) $(SKIPDEPS) xref $(XREFOPTIONS) +hooks: all + tools/hook_deps.sh $(EBINDIR) + +options: all + tools/opt_types.sh ejabberd_option $(EBINDIR) translations: - contrib/extract_translations/prepare-translation.sh -updateall + $(GET_DEPS_TRANSLATIONS) + tools/prepare-tr.sh $(DEPSDIR_TRANSLATIONS) -doc: - echo making $$target in doc; \ - (cd doc && $(MAKE) $$target) || exit 1 +doap: + tools/generate-doap.sh -edoc: - $(ERL) -noinput +B -eval \ - 'case edoc:application(ejabberd, ".", []) of ok -> halt(0); error -> halt(1) end.' +#. +#' edoc +# -spec: - $(ERL) -noinput +B -pa ebin -pa deps/*/ebin -eval \ - 'case xml_gen:compile("tools/xmpp_codec.spec") of ok -> halt(0); _ -> halt(1) end.' +edoc: edoc_files edoc_compile + $(EDOCPRE) $(REBAR) $(EDOCTASK) -DLLs := $(wildcard deps/*/priv/*.so) $(wildcard deps/*/priv/lib/*.so) +edoc_compile: deps + $(EDOCPRE) $(REBAR) compile -install: all +edoc_files: _build/edoc/docs.md _build/edoc/logo.png + +_build/edoc/docs.md: edoc_compile + echo "For much more detailed and complete ejabberd documentation, " \ + "go to the [ejabberd Docs](https://docs.ejabberd.im/) site." \ + > _build/edoc/docs.md + +_build/edoc/logo.png: edoc_compile + wget https://docs.ejabberd.im/assets/img/footer_logo_e.png -O _build/edoc/logo.png + +#. +#' format / indent +# + +format: + tools/rebar3-format.sh $(REBAR3) + +indent: + tools/emacs-indent.sh + +#. +#' copy-files +# + +JOIN_PATHS=$(if $(wordlist 2,1000,$(1)),$(firstword $(1))/$(call JOIN_PATHS,$(wordlist 2,1000,$(1))),$(1)) + +VERSIONED_DEP=$(if $(DEP_$(1)_VERSION),$(DEP_$(1)_VERSION),$(1)) + +DEPIX:=$(words $(subst /, ,$(DEPSDIR))) +LIBIX:=$(shell expr "$(DEPIX)" + 2) + +ELIXIR_TO_DEST=$(LIBDIR) $(call VERSIONED_DEP,$(word 2,$(1))) $(wordlist 5,1000,$(1)) +DEPS_TO_DEST=$(LIBDIR) $(call VERSIONED_DEP,$(word 2,$(1))) $(wordlist 3,1000,$(1)) +MAIN_TO_DEST=$(LIBDIR) $(call VERSIONED_DEP,ejabberd) $(1) +TO_DEST_SINGLE=$(if $(subst X$(DEPSBASE)X,,X$(word 1,$(1))X),$(call MAIN_TO_DEST,$(1)),$(if $(subst XlibX,,X$(word $(LIBIX),$(1))X),$(call DEPS_TO_DEST,$(wordlist $(DEPIX),1000,$(1))),$(call ELIXIR_TO_DEST,$(wordlist $(DEPIX),1000,$(1))))) +TO_DEST=$(foreach path,$(1),$(call JOIN_PATHS,$(DESTDIR)$(call TO_DEST_SINGLE,$(subst /, ,$(path))))) + +FILTER_DIRS=$(foreach path,$(1),$(if $(wildcard $(path)/*),,$(path))) +FILES_WILDCARD=$(call FILTER_DIRS,$(foreach w,$(1),$(wildcard $(w)))) + +ifeq ($(MAKECMDGOALS),copy-files-sub) + +DEPS:=$(sort $(shell QUIET=1 $(REBAR) $(LISTDEPS) | $(SED) -ne $(DEPSPATTERN) )) + +DEPS_FILES=$(call FILES_WILDCARD,$(foreach DEP,$(DEPS),$(DEPSDIR)/$(DEP)/ebin/*.beam $(DEPSDIR)/$(DEP)/ebin/*.app $(DEPSDIR)/$(DEP)/priv/* $(DEPSDIR)/$(DEP)/priv/lib/* $(DEPSDIR)/$(DEP)/priv/bin/* $(DEPSDIR)/$(DEP)/include/*.hrl $(DEPSDIR)/$(DEP)/COPY* $(DEPSDIR)/$(DEP)/LICENSE* $(DEPSDIR)/$(DEP)/lib/*/ebin/*.beam $(DEPSDIR)/$(DEP)/lib/*/ebin/*.app)) + +BINARIES=$(DEPSDIR)/epam/priv/bin/epam $(DEPSDIR)/eimp/priv/bin/eimp $(DEPSDIR)/fs/priv/mac_listener + +DEPS_FILES_FILTERED=$(filter-out $(BINARIES) $(DEPSDIR)/elixir/ebin/elixir.app,$(DEPS_FILES)) +DEPS_DIRS=$(sort $(DEPSDIR)/ $(foreach DEP,$(DEPS),$(DEPSDIR)/$(DEP)/) $(dir $(DEPS_FILES))) + +MAIN_FILES=$(filter-out %/configure.beam,$(call FILES_WILDCARD,$(EBINDIR)/*.beam $(EBINDIR)/*.app priv/msgs/*.msg priv/css/*.css priv/img/*.png priv/js/*.js priv/lib/* include/*.hrl COPYING)) +MAIN_DIRS=$(sort $(dir $(MAIN_FILES)) priv/bin priv/sql priv/lua) + +define DEP_VERSION_template +DEP_$(1)_VERSION:=$(shell $(SED) -e '/vsn/!d;s/.*, *"/$(1)-/;s/".*//' $(2) 2>/dev/null) +endef + +DELETE_TARGET_SO=$(if $(subst X.soX,,X$(suffix $(1))X),,rm -f $(call TO_DEST,$(1));) + +$(foreach DEP,$(DEPS),$(eval $(call DEP_VERSION_template,$(DEP),$(DEPSDIR)/$(DEP)/ebin/$(DEP).app))) +$(eval $(call DEP_VERSION_template,ejabberd,$(EBINDIR)/ejabberd.app)) + +define COPY_template +$(call TO_DEST,$(1)): $(1) $(call TO_DEST,$(dir $(1))) ; $(call DELETE_TARGET_SO, $(1)) $$(INSTALL) -m 644 $(1) $(call TO_DEST,$(1)) +endef + +define COPY_BINARY_template +$(call TO_DEST,$(1)): $(1) $(call TO_DEST,$(dir $(1))) ; rm -f $(call TO_DEST,$(1)); $$(INSTALL) -m 755 $$(O_USER) $(1) $(call TO_DEST,$(1)) +endef + +$(foreach file,$(DEPS_FILES_FILTERED) $(MAIN_FILES),$(eval $(call COPY_template,$(file)))) + +$(foreach file,$(BINARIES),$(eval $(call COPY_BINARY_template,$(file)))) + +$(sort $(call TO_DEST,$(MAIN_DIRS) $(DEPS_DIRS))): + $(INSTALL) -d $@ + +$(call TO_DEST,priv/sql/lite.sql): sql/lite.sql $(call TO_DEST,priv/sql) + $(INSTALL) -m 644 $< $@ + +$(call TO_DEST,priv/sql/lite.new.sql): sql/lite.new.sql $(call TO_DEST,priv/sql) + $(INSTALL) -m 644 $< $@ + +$(call TO_DEST,priv/bin/captcha.sh): tools/captcha.sh $(call TO_DEST,priv/bin) + $(INSTALL) -m 755 $(O_USER) $< $@ + +$(call TO_DEST,priv/lua/redis_sm.lua): priv/lua/redis_sm.lua $(call TO_DEST,priv/lua) + $(INSTALL) -m 644 $< $@ + +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)) + +endif + +copy-files: + $(MAKE) copy-files-sub + +copy-files-sub: copy-files-sub2 + +#. +#' copy-files-rel +# + +copy-files-rel: $(COPY_REL_TARGET) # - # Configuration files - $(INSTALL) -d -m 750 $(G_USER) $(ETCDIR) - [ -f $(ETCDIR)/ejabberd.yml ] \ - && $(INSTALL) -b -m 640 $(G_USER) ejabberd.yml.example $(ETCDIR)/ejabberd.yml-new \ - || $(INSTALL) -b -m 640 $(G_USER) ejabberd.yml.example $(ETCDIR)/ejabberd.yml - $(SED) -e "s*{{rootdir}}*@prefix@*" \ - -e "s*{{installuser}}*@INSTALLUSER@*" \ - -e "s*{{bindir}}*@bindir@*" \ - -e "s*{{libdir}}*@libdir@*" \ - -e "s*{{sysconfdir}}*@sysconfdir@*" \ - -e "s*{{localstatedir}}*@localstatedir@*" \ - -e "s*{{docdir}}*@docdir@*" \ - -e "s*{{erl}}*@ERL@*" ejabberdctl.template \ - > ejabberdctl.example - [ -f $(ETCDIR)/ejabberdctl.cfg ] \ - && $(INSTALL) -b -m 640 $(G_USER) ejabberdctl.cfg.example $(ETCDIR)/ejabberdctl.cfg-new \ - || $(INSTALL) -b -m 640 $(G_USER) ejabberdctl.cfg.example $(ETCDIR)/ejabberdctl.cfg - $(INSTALL) -b -m 644 $(G_USER) inetrc $(ETCDIR)/inetrc + # Libraries + (cd $(REL_LIB_DIR) && find . -follow -type f ! -executable -exec $(INSTALL) -vDm 640 $(G_USER) {} $(DESTDIR)$(LIBDIR)/{} \;) # - # Administration script - [ -d $(SBINDIR) ] || $(INSTALL) -d -m 755 $(SBINDIR) - $(INSTALL) -m 550 $(G_USER) ejabberdctl.example $(SBINDIR)/ejabberdctl - # Elixir binaries - [ -d $(BINDIR) ] || $(INSTALL) -d -m 755 $(BINDIR) - -[ -f deps/elixir/bin/iex ] && $(INSTALL) -m 550 $(G_USER) deps/elixir/bin/iex $(BINDIR)/iex - -[ -f deps/elixir/bin/elixir ] && $(INSTALL) -m 550 $(G_USER) deps/elixir/bin/elixir $(BINDIR)/elixir - -[ -f deps/elixir/bin/mix ] && $(INSTALL) -m 550 $(G_USER) deps/elixir/bin/mix $(BINDIR)/mix + # *.so: + (cd $(REL_LIB_DIR) && find . -follow -type f -executable -name *.so -exec $(INSTALL) -vDm 640 $(G_USER) {} $(DESTDIR)$(LIBDIR)/{} \;) # - # Init script - $(SED) -e "s*@ctlscriptpath@*$(SBINDIR)*" \ - -e "s*@installuser@*$(INIT_USER)*" ejabberd.init.template \ + # Executable files + (cd $(REL_LIB_DIR) && find . -follow -type f -executable ! -name *.so -exec $(INSTALL) -vDm 550 $(G_USER) {} $(DESTDIR)$(LIBDIR)/{} \;) + +#. +#' uninstall-librel +# + +uninstall-librel: + (cd $(REL_LIB_DIR) && find . -follow -type f -exec rm -fv -v $(DESTDIR)$(LIBDIR)/{} \;) + (cd $(REL_LIB_DIR) && find . -follow -depth -type d -exec rm -dv -v $(DESTDIR)$(LIBDIR)/{} \;) + +#. +#' relive +# + +relive: + $(EXPLICIT_ELIXIR_COMPILE_DEV) + $(RELIVECMD) + +relivelibdir=$(shell pwd)/$(DEPSDIR) +relivedir=$(shell pwd)/_build/relive +CONFIG_DIR = ${relivedir}/conf +SPOOL_DIR = ${relivedir}/database +LOGS_DIR = ${relivedir}/logs + +#. +#' scripts +# + +ejabberdctl.relive: + $(SED) -e "s*{{installuser}}*${INSTALLUSER}*g" \ + -e "s*{{config_dir}}*${CONFIG_DIR}*g" \ + -e "s*{{logs_dir}}*${LOGS_DIR}*g" \ + -e "s*{{spool_dir}}*${SPOOL_DIR}*g" \ + -e "s*{{bindir}}*${BINDIR}*g" \ + -e "s*{{libdir}}*${relivelibdir}${ELIXIR_LIBDIR}*g" \ + -e "s*ERTS_VSN*# ERTS_VSN*g" \ + -e "s*{{iexpath}}*${IEX}*g" \ + -e "s*{{erl}}*${ERL}*g" \ + -e "s*{{epmd}}*${EPMD}*g" ejabberdctl.template \ + > ejabberdctl.relive + +ejabberd.init: + $(SED) -e "s*@ctlscriptpath@*$(SBINDIR)*g" \ + -e "s*@installuser@*$(INIT_USER)*g" ejabberd.init.template \ > ejabberd.init chmod 755 ejabberd.init + +ejabberd.service: + $(SED) -e "s*@ctlscriptpath@*$(SBINDIR)*g" \ + -e "s*@installuser@*$(INIT_USER)*g" ejabberd.service.template \ + > ejabberd.service + chmod 644 ejabberd.service + +ejabberdctl.example: vars.config + $(SED) -e "s*{{installuser}}*${INSTALLUSER}*g" \ + -e "s*{{config_dir}}*${ETCDIR}*g" \ + -e "s*{{logs_dir}}*${LOGDIR}*g" \ + -e "s*{{spool_dir}}*${SPOOLDIR}*g" \ + -e "s*{{bindir}}*${BINDIR}*g" \ + -e "s*{{libdir}}*${LIBDIR}${ELIXIR_LIBDIR}*g" \ + -e "s*ERTS_VSN*# ERTS_VSN*g" \ + -e "s*{{iexpath}}*${IEX}*g" \ + -e "s*{{erl}}*${ERL}*g" \ + -e "s*{{epmd}}*${EPMD}*g" ejabberdctl.template \ + > ejabberdctl.example + +scripts: ejabberd.init ejabberd.service ejabberdctl.example + +#. +#' install +# + +install: copy-files install-main + +install-rel: copy-files-rel install-main + +install-main: # - # Binary Erlang files - $(INSTALL) -d $(BEAMDIR) - $(INSTALL) -m 644 ebin/*.app $(BEAMDIR) - $(INSTALL) -m 644 ebin/*.beam $(BEAMDIR) - $(INSTALL) -m 644 deps/*/ebin/*.app $(BEAMDIR) - $(INSTALL) -m 644 deps/*/ebin/*.beam $(BEAMDIR) - # Install Elixir and Elixir dependancies - -$(INSTALL) -m 644 deps/*/lib/*/ebin/*.app $(BEAMDIR) - -$(INSTALL) -m 644 deps/*/lib/*/ebin/*.beam $(BEAMDIR) - rm -f $(BEAMDIR)/configure.beam + # Configuration files + $(INSTALL) -d -m 750 $(G_USER) $(DESTDIR)$(ETCDIR) + [ -f $(DESTDIR)$(ETCDIR)/ejabberd.yml ] \ + && $(INSTALL) -b -m 640 $(G_USER) ejabberd.yml.example $(DESTDIR)$(ETCDIR)/ejabberd.yml-new \ + || $(INSTALL) -b -m 640 $(G_USER) ejabberd.yml.example $(DESTDIR)$(ETCDIR)/ejabberd.yml + [ -f $(DESTDIR)$(ETCDIR)/ejabberdctl.cfg ] \ + && $(INSTALL) -b -m 640 $(G_USER) ejabberdctl.cfg.example $(DESTDIR)$(ETCDIR)/ejabberdctl.cfg-new \ + || $(INSTALL) -b -m 640 $(G_USER) ejabberdctl.cfg.example $(DESTDIR)$(ETCDIR)/ejabberdctl.cfg + $(INSTALL) -b -m 644 $(G_USER) inetrc $(DESTDIR)$(ETCDIR)/inetrc # - # ejabberd header files - $(INSTALL) -d $(INCLUDEDIR) - $(INSTALL) -m 644 include/*.hrl $(INCLUDEDIR) - $(INSTALL) -m 644 deps/*/include/*.hrl $(INCLUDEDIR) - # - # Binary C programs - $(INSTALL) -d $(PBINDIR) - $(INSTALL) -m 750 $(O_USER) tools/captcha.sh $(PBINDIR) - -[ -f deps/p1_pam/priv/bin/epam ] \ - && $(INSTALL) -m 750 $(O_USER) deps/p1_pam/priv/bin/epam $(PBINDIR) - # - # Binary system libraries - $(INSTALL) -d $(SODIR) - $(INSTALL) -m 644 $(DLLs) $(SODIR) - -[ -f $(SODIR)/jiffy.so ] && (cd $(PRIVDIR); ln -s lib/jiffy.so; true) - # - # Translated strings - $(INSTALL) -d $(MSGSDIR) - $(INSTALL) -m 644 priv/msgs/*.msg $(MSGSDIR) + # Administration script + [ -d $(DESTDIR)$(SBINDIR) ] || $(INSTALL) -d -m 755 $(DESTDIR)$(SBINDIR) + $(INSTALL) -m 550 $(G_USER) ejabberdctl.example $(DESTDIR)$(SBINDIR)/ejabberdctl + # Elixir binaries + [ -d $(DESTDIR)$(BINDIR) ] || $(INSTALL) -d -m 755 $(DESTDIR)$(BINDIR) + [ -f $(DEPSDIR)/elixir/bin/iex ] && $(INSTALL) -m 550 $(G_USER) $(DEPSDIR)/elixir/bin/iex $(DESTDIR)$(BINDIR)/iex || true + [ -f $(DEPSDIR)/elixir/bin/elixir ] && $(INSTALL) -m 550 $(G_USER) $(DEPSDIR)/elixir/bin/elixir $(DESTDIR)$(BINDIR)/elixir || true + [ -f $(DEPSDIR)/elixir/bin/mix ] && $(INSTALL) -m 550 $(G_USER) $(DEPSDIR)/elixir/bin/mix $(DESTDIR)$(BINDIR)/mix || true # # Spool directory - $(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) + $(INSTALL) -d -m 750 $(O_USER) $(DESTDIR)$(SPOOLDIR) + $(CHOWN_COMMAND) -R $(INSTALLUSER) $(DESTDIR)$(SPOOLDIR) >$(CHOWN_OUTPUT) + chmod -R 750 $(DESTDIR)$(SPOOLDIR) # # Log directory - $(INSTALL) -d -m 750 $(O_USER) $(LOGDIR) - $(CHOWN_COMMAND) -R @INSTALLUSER@ $(LOGDIR) >$(CHOWN_OUTPUT) - chmod -R 750 $(LOGDIR) + $(INSTALL) -d -m 750 $(O_USER) $(DESTDIR)$(LOGDIR) + $(CHOWN_COMMAND) -R $(INSTALLUSER) $(DESTDIR)$(LOGDIR) >$(CHOWN_OUTPUT) + chmod -R 750 $(DESTDIR)$(LOGDIR) # # Documentation - $(INSTALL) -d $(DOCDIR) - [ -f doc/dev.html ] \ - && $(INSTALL) -m 644 doc/dev.html $(DOCDIR) \ - || echo "No doc/dev.html was built" - [ -f doc/guide.html ] \ - && $(INSTALL) -m 644 doc/guide.html $(DOCDIR) \ - || echo "No doc/guide.html was built" - [ -f doc/guide.pdf ] \ - && $(INSTALL) -m 644 doc/guide.pdf $(DOCDIR) \ - || echo "No doc/guide.pdf was built" - $(INSTALL) -m 644 doc/*.png $(DOCDIR) - $(INSTALL) -m 644 COPYING $(DOCDIR) + $(INSTALL) -d $(DESTDIR)$(MANDIR) + $(INSTALL) -d $(DESTDIR)$(DOCDIR) + [ -f man/ejabberd.yml.5 ] \ + && $(INSTALL) -m 644 man/ejabberd.yml.5 $(DESTDIR)$(MANDIR) \ + || echo "Man page not included in sources" + $(INSTALL) -m 644 COPYING $(DESTDIR)$(DOCDIR) + +#. +#' uninstall +# uninstall: uninstall-binary +uninstall-rel: uninstall-binary uninstall-librel + uninstall-binary: - rm -f $(SBINDIR)/ejabberdctl - rm -f $(BINDIR)/iex - rm -f $(BINDIR)/elixir - rm -f $(BINDIR)/mix - rm -fr $(DOCDIR) - rm -f $(BEAMDIR)/*.beam - rm -f $(BEAMDIR)/*.app - rm -fr $(BEAMDIR) - rm -f $(INCLUDEDIR)/*.hrl - rm -fr $(INCLUDEDIR) - rm -fr $(PBINDIR) - rm -f $(SODIR)/*.so - rm -fr $(SODIR) - rm -f $(MSGSDIR)/*.msgs - rm -fr $(MSGSDIR) - rm -fr $(PRIVDIR) - rm -fr $(EJABBERDDIR) + rm -f $(DESTDIR)$(SBINDIR)/ejabberdctl + rm -f $(DESTDIR)$(BINDIR)/iex + rm -f $(DESTDIR)$(BINDIR)/elixir + rm -f $(DESTDIR)$(BINDIR)/mix + rm -fr $(DESTDIR)$(DOCDIR) + rm -f $(DESTDIR)$(BEAMDIR)/*.beam + rm -f $(DESTDIR)$(BEAMDIR)/*.app + rm -fr $(DESTDIR)$(BEAMDIR) + rm -f $(DESTDIR)$(INCLUDEDIR)/*.hrl + rm -fr $(DESTDIR)$(INCLUDEDIR) + rm -fr $(DESTDIR)$(PBINDIR) + rm -f $(DESTDIR)$(SODIR)/*.so + rm -fr $(DESTDIR)$(SODIR) + rm -f $(DESTDIR)$(MSGSDIR)/*.msg + rm -fr $(DESTDIR)$(MSGSDIR) + rm -f $(DESTDIR)$(CSSDIR)/*.css + rm -fr $(DESTDIR)$(CSSDIR) + rm -f $(DESTDIR)$(IMGDIR)/*.png + rm -fr $(DESTDIR)$(IMGDIR) + rm -f $(DESTDIR)$(JSDIR)/*.js + rm -fr $(DESTDIR)$(JSDIR) + rm -f $(DESTDIR)$(SQLDIR)/*.sql + rm -fr $(DESTDIR)$(SQLDIR) + rm -fr $(DESTDIR)$(LUADIR)/*.lua + rm -fr $(DESTDIR)$(LUADIR) + rm -fr $(DESTDIR)$(PRIVDIR) + rm -fr $(DESTDIR)$(EJABBERDDIR) + rm -f $(DESTDIR)$(MANDIR)/ejabberd.yml.5 uninstall-all: uninstall-binary - rm -rf $(ETCDIR) - rm -rf $(EJABBERDDIR) - rm -rf $(SPOOLDIR) - rm -rf $(CTLLOCKDIR) - rm -rf $(LOGDIR) + rm -rf $(DESTDIR)$(ETCDIR) + rm -rf $(DESTDIR)$(EJABBERDDIR) + rm -rf $(DESTDIR)$(SPOOLDIR) + rm -rf $(DESTDIR)$(LOGDIR) + +#. +#' clean +# clean: - rm -rf deps/.got - rm -rf deps/.built + rm -rf $(DEPSDIR)/.got + rm -rf $(DEPSDIR)/.built rm -rf test/*.beam - $(REBAR) clean + rm -f rebar.lock + rm -f ejabberdctl.example ejabberd.init ejabberd.service + $(REBAR) clean $(CLEANARG) clean-rel: rm -rf rel/ejabberd distclean: clean clean-rel + rm -f aclocal.m4 rm -f config.status rm -f config.log rm -rf autom4te.cache + rm -rf $(EBINDIR) + rm -rf $(DEPSBASE) rm -rf deps - rm -rf ebin rm -f Makefile rm -f vars.config - rm -f src/ejabberd.app.src - [ ! -f ../ChangeLog ] || rm -f ../ChangeLog -rel: all - $(REBAR) generate +#. +#' releases +# + +rel: prod + +prod: + $(PREPARE_ELIXIR_SCRIPTS) + $(REBARREL) + +DEV_CONFIG = _build/dev/rel/ejabberd/conf/ejabberd.yml + +dev $(DEV_CONFIG): + $(PREPARE_ELIXIR_SCRIPTS) + $(REBARDEV) + +#. +#' tags +# TAGS: - etags *.erl + etags src/*.erl + +#. +#' makefile +# Makefile: Makefile.in -deps := $(wildcard deps/*/ebin) +#. +#' dialyzer +# + +ifeq "$(REBAR_VER)" "6" # Mix +dialyzer: + MIX_ENV=test $(REBAR) dialyzer + +else +ifeq "$(REBAR_VER)" "3" # Rebar3 +dialyzer: + $(REBAR) dialyzer + +else # Rebar2 +deps := $(wildcard $(DEPSDIR)/*/ebin) dialyzer/erlang.plt: - @mkdir -p dialyzer + @$(MKDIR_P) dialyzer @dialyzer --build_plt --output_plt dialyzer/erlang.plt \ -o dialyzer/erlang.log --apps kernel stdlib sasl crypto \ - public_key ssl mnesia inets odbc tools compiler erts webtool \ - runtime_tools asn1 observer xmerl et gs wx syntax_tools; \ + public_key ssl mnesia inets odbc compiler erts \ + os_mon asn1 syntax_tools; \ status=$$? ; if [ $$status -ne 2 ]; then exit $$status; else exit 0; fi dialyzer/deps.plt: - @mkdir -p dialyzer + @$(MKDIR_P) dialyzer @dialyzer --build_plt --output_plt dialyzer/deps.plt \ -o dialyzer/deps.log $(deps); \ status=$$? ; if [ $$status -ne 2 ]; then exit $$status; else exit 0; fi dialyzer/ejabberd.plt: - @mkdir -p dialyzer + @$(MKDIR_P) dialyzer @dialyzer --build_plt --output_plt dialyzer/ejabberd.plt \ -o dialyzer/ejabberd.log ebin; \ status=$$? ; if [ $$status -ne 2 ]; then exit $$status; else exit 0; fi @@ -303,13 +640,91 @@ dialyzer: erlang_plt deps_plt ejabberd_plt @dialyzer --plts dialyzer/*.plt --no_check_plt \ --get_warnings -o dialyzer/error.log ebin; \ status=$$? ; if [ $$status -ne 2 ]; then exit $$status; else exit 0; fi +endif +endif + +#. +#' elvis +# + +elvis: + $(REBAR) lint + +#. +#' test +# test: @echo "************************** NOTICE ***************************************" @cat test/README @echo "*************************************************************************" - $(REBAR) skip_deps=true ct + @cd priv && ln -sf ../sql + $(REBAR) $(SKIPDEPS) ct -.PHONY: src doc edoc dialyzer Makefile TAGS clean clean-rel distclean rel \ - install uninstall uninstall-binary uninstall-all translations deps test spec \ - erlang_plt deps_plt ejabberd_plt +.PHONY: test-% +define test-group-target +test-$1: + $(REBAR) $(SKIPDEPS) ct --suite=test/ejabberd_SUITE --group=$1 +endef + +ifneq ($(filter test-%,$(MAKECMDGOALS)),) +group_to_test := $(patsubst test-%,%,$(filter test-%,$(MAKECMDGOALS))) +$(eval $(call test-group-target,$(group_to_test))) +endif + +test-eunit: + $(REBAR) $(SKIPDEPS) eunit --verbose + +#. +#' phony +# + +.PHONY: src edoc dialyzer Makefile TAGS clean clean-rel distclean prod rel \ + install uninstall uninstall-binary uninstall-all translations deps test test-eunit \ + all dev doap help install-rel relive scripts uninstall-rel update \ + erlang_plt deps_plt ejabberd_plt xref hooks options format indent + +#. +#' help +# + +help: + @echo "" + @echo " [all] " + @echo " scripts Prepare ejabberd start scripts" + @echo " deps Get and configure dependencies" + @echo " src Compile dependencies and ejabberd" + @echo " update Update dependencies source code" + @echo " clean Clean binary files" + @echo " distclean Clean completely the development files" + @echo "" + @echo " install Install ejabberd to /usr/local" + @echo " install-rel Install ejabberd to /usr/local (using release)" + @echo " uninstall Uninstall ejabberd (buggy)" + @echo " uninstall-rel Uninstall ejabberd (using release)" + @echo " uninstall-all Uninstall also configuration, logs, mnesia... (buggy)" + @echo "" + @echo " prod Build a production release" + @echo " dev Build a development release" + @echo " relive Start a live ejabberd in _build/relive/" + @echo "" + @echo " doap Generate DOAP file" + @echo " edoc Generate EDoc documentation [mix]" + @echo " options Generate ejabberd_option.erl" + @echo " translations Extract translation files" + @echo " TAGS Generate tags file for text editors" + @echo "" + @echo " format Format source code using rebar3_format" + @echo " indent Indent source code using erlang-mode [emacs]" + @echo "" + @echo " dialyzer Run Dialyzer static analyzer" + @echo " elvis Run Elvis source code style reviewer [rebar3]" + @echo " hooks Run hooks validator" + @echo " test Run Common Tests suite [rebar3]" + @echo " test-eunit Run EUnit suite [rebar3]" + @echo " test- Run Common Test suite for specific group only [rebar3]" + @echo " xref Run cross reference analysis [rebar3]" + +#. +#' +# vim: foldmarker=#',#. foldmethod=marker: diff --git a/Makefile.win32 b/Makefile.win32 deleted file mode 100644 index 8a1192fce..000000000 --- a/Makefile.win32 +++ /dev/null @@ -1,176 +0,0 @@ - -include Makefile.inc - -ALL : build - -REL=..\release -EREL=$(REL)\ejabberd-$(EJABBERD_VERSION) -EBIN_DIR=$(EREL)\ebin -SRC_DIR=$(EREL)\src -PRIV_DIR=$(EREL)\priv -SO_DIR=$(EREL) -MSGS_DIR=$(EREL)\msgs -WIN32_DIR=$(EREL)\win32 -DOC_DIR=$(EREL)\doc - -NSIS_SCRIPT=win32\ejabberd.nsi -NSIS_HEADER=win32\ejabberd.nsh - -installer : $(NSIS_SCRIPT) $(NSIS_HEADER) - makensis $(NSIS_SCRIPT) - -$(NSIS_HEADER) : Makefile.inc - echo !define OUTFILEDIR "..\$(REL)" >$(NSIS_HEADER) - echo !define TESTDIR "..\$(REL)\ejabberd-$(EJABBERD_VERSION)" >>$(NSIS_HEADER) - echo !define VERSION "$(EJABBERD_VERSION)" >>$(NSIS_HEADER) - -release_clean : - if exist $(REL) rd /s /q $(REL) - - -release : build release_clean - mkdir $(REL) - mkdir $(EREL) - mkdir $(EBIN_DIR) - copy *.beam $(EBIN_DIR) - @erase $(EBIN_DIR)\configure.beam - copy *.app $(EBIN_DIR) - copy *.dll $(SO_DIR) - mkdir $(MSGS_DIR) - copy msgs\*.msg $(MSGS_DIR) - mkdir $(WIN32_DIR) - copy win32\ejabberd.cfg $(EREL) - copy win32\inetrc $(EREL) - copy $(SYSTEMROOT)\system32\libeay32.dll $(EREL) - copy $(SYSTEMROOT)\system32\ssleay32.dll $(EREL) - copy win32\ejabberd.ico $(WIN32_DIR) - mkdir $(SRC_DIR) - copy *.app $(SRC_DIR) - copy *.erl $(SRC_DIR) - copy *.hrl $(SRC_DIR) - copy *.c $(SRC_DIR) - mkdir $(SRC_DIR)\eldap - copy eldap\eldap.* $(SRC_DIR)\eldap - copy eldap\ELDAPv3.asn $(SRC_DIR)\eldap - mkdir $(SRC_DIR)\mod_irc - copy mod_irc\*.erl $(SRC_DIR)\mod_irc - copy mod_irc\*.c $(SRC_DIR)\mod_irc - mkdir $(SRC_DIR)\mod_muc - copy mod_muc\*.erl $(SRC_DIR)\mod_muc - mkdir $(SRC_DIR)\mod_pubsub - copy mod_pubsub\*.erl $(SRC_DIR)\mod_pubsub - mkdir $(SRC_DIR)\mod_proxy65 - copy mod_proxy65\*.erl $(SRC_DIR)\mod_proxy65 - copy mod_proxy65\*.hrl $(SRC_DIR)\mod_proxy65 - mkdir $(SRC_DIR)\stringprep - copy stringprep\*.erl $(SRC_DIR)\stringprep - copy stringprep\*.c $(SRC_DIR)\stringprep - copy stringprep\*.tcl $(SRC_DIR)\stringprep - mkdir $(SRC_DIR)\stun - copy stun\*.erl $(SRC_DIR)\stun - copy stun\*.hrl $(SRC_DIR)\stun - mkdir $(SRC_DIR)\tls - copy tls\*.erl $(SRC_DIR)\tls - copy tls\*.c $(SRC_DIR)\tls - mkdir $(SRC_DIR)\ejabberd_zlib - copy ejabberd_zlib\*.erl $(SRC_DIR)\ejabberd_zlib - copy ejabberd_zlib\*.c $(SRC_DIR)\ejabberd_zlib - mkdir $(SRC_DIR)\web - copy web\*.erl $(SRC_DIR)\web - mkdir $(SRC_DIR)\odbc - copy odbc\*.erl $(SRC_DIR)\odbc - copy odbc\*.sql $(EREL) - mkdir $(DOC_DIR) - copy ..\doc\*.txt $(DOC_DIR) - copy ..\doc\*.html $(DOC_DIR) - copy ..\doc\*.png $(DOC_DIR) - -SOURCE = expat_erl.c -OBJECT = expat_erl.o -DLL = expat_erl.dll - -build : $(DLL) compile-beam all-recursive - -all-recursive : - cd eldap - nmake -nologo -f Makefile.win32 - cd ..\mod_irc - nmake -nologo -f Makefile.win32 - cd ..\mod_muc - nmake -nologo -f Makefile.win32 - cd ..\mod_pubsub - nmake -nologo -f Makefile.win32 - cd ..\mod_proxy65 - nmake -nologo -f Makefile.win32 - cd ..\stringprep - nmake -nologo -f Makefile.win32 - cd ..\stun - nmake -nologo -f Makefile.win32 - cd ..\tls - nmake -nologo -f Makefile.win32 - cd ..\ejabberd_zlib - nmake -nologo -f Makefile.win32 - cd ..\web - nmake -nologo -f Makefile.win32 - cd ..\odbc - nmake -nologo -f Makefile.win32 - cd .. - -compile-beam : XmppAddr.hrl - erl -s make all report -noinput -s erlang halt - -XmppAddr.hrl : XmppAddr.asn1 - erlc -bber_bin +der +compact_bit_string +optimize +noobj XmppAddr.asn1 - -CLEAN : clean-recursive clean-local - -clean-local : - -@erase $(OBJECT) - -@erase $(DLL) - -@erase expat_erl.exp - -@erase expat_erl.lib - -@erase *.beam - -@erase XmppAddr.asn1db - -@erase XmppAddr.erl - -@erase XmppAddr.hrl - -clean-recursive : - cd eldap - nmake -nologo -f Makefile.win32 clean - cd ..\mod_irc - nmake -nologo -f Makefile.win32 clean - cd ..\mod_muc - nmake -nologo -f Makefile.win32 clean - cd ..\mod_pubsub - nmake -nologo -f Makefile.win32 clean - cd ..\mod_proxy65 - nmake -nologo -f Makefile.win32 clean - cd ..\stringprep - nmake -nologo -f Makefile.win32 clean - cd ..\stun - nmake -nologo -f Makefile.win32 clean - cd ..\tls - nmake -nologo -f Makefile.win32 clean - cd ..\ejabberd_zlib - nmake -nologo -f Makefile.win32 clean - cd ..\web - nmake -nologo -f Makefile.win32 clean - cd ..\odbc - nmake -nologo -f Makefile.win32 clean - cd .. - -distclean : release_clean clean - -@erase $(NSIS_HEADER) - -@erase Makefile.inc - -CC=cl.exe -CC_FLAGS=-nologo -D__WIN32__ -DWIN32 -DWINDOWS -D_WIN32 -DNT $(EXPAT_FLAG) -MD -Ox -I"$(ERLANG_DIR)\usr\include" -I"$(EI_DIR)\include" -I"$(EXPAT_DIR)\source\lib" - -LD=link.exe -LD_FLAGS=-release -nologo -incremental:no -dll "$(EI_DIR)\lib\ei_md.lib" "$(EI_DIR)\lib\erl_interface_md.lib" "$(EXPAT_LIB)" MSVCRT.LIB kernel32.lib advapi32.lib gdi32.lib user32.lib comctl32.lib comdlg32.lib shell32.lib - -$(DLL) : $(OBJECT) - $(LD) $(LD_FLAGS) -out:$@ $< - -$(OBJECT) : $(SOURCE) - $(CC) $(CC_FLAGS) -c -Fo$@ $< diff --git a/README b/README deleted file mode 100644 index 35a6f4950..000000000 --- a/README +++ /dev/null @@ -1,161 +0,0 @@ -ejabberd Community Edition, by ProcessOne -========================================= - -ejabberd is a distributed, fault-tolerant technology that allows the creation -of large-scale instant messaging applications. The server can reliably support -thousands of simultaneous users on a single node and has been designed to -provide exceptional standards of fault tolerance. As an open source -technology, based on industry-standards, ejabberd can be used to build bespoke -solutions very cost effectively. - - -Key Features ------------- - -- **Cross-platform** - ejabberd runs under Microsoft Windows and Unix-derived systems such as - Linux, FreeBSD and NetBSD. - -- **Distributed** - You can run ejabberd on a cluster of machines and all of them will serve the - same XMPP domain(s). When you need more capacity you can simply add a new - cheap node to your cluster. Accordingly, you do not need to buy an expensive - high-end machine to support tens of thousands concurrent users. - -- **Fault-tolerant** - You can deploy an ejabberd cluster so that all the information required for - a properly working service will be replicated permanently on all nodes. This - means that if one of the nodes crashes, the others will continue working - without disruption. In addition, nodes also can be added or replaced ‘on - the fly’. - -- **Administrator-friendly** - ejabberd is built on top of the Open Source Erlang. As a result you do not - need to install an external database, an external web server, amongst others - because everything is already included, and ready to run out of the box. - Other administrator benefits include: - - Comprehensive documentation. - - Straightforward installers for Linux and Mac OS X. - - Web administration. - - Shared roster groups. - - Command line administration tool. - - Can integrate with existing authentication mechanisms. - - Capability to send announce messages. - -- **Internationalized** - ejabberd leads in internationalization. Hence it is very well suited in a - globalized world. Related features are: - - Translated to 25 languages. - - Support for IDNA. - -- **Open Standards** - ejabberd is the first Open Source Jabber server claiming to fully comply to - the XMPP standard. - - Fully XMPP-compliant. - - XML-based protocol. - - Many protocols supported. - - -Additional Features -------------------- - -Moreover, ejabberd comes with a wide range of other state-of-the-art features: - -- **Modularity** - - Load only the modules you want. - - Extend ejabberd with your own custom modules. - -- **Security** - - SASL and STARTTLS for c2s and s2s connections. - - STARTTLS and Dialback s2s connections. - - Web Admin accessible via HTTPS secure access. - -- **Databases** - - Internal database for fast deployment (Mnesia). - - Native MySQL support. - - Native PostgreSQL support. - - ODBC data storage support. - - Microsoft SQL Server support. - -- **Authentication** - - Internal authentication. - - PAM, LDAP and ODBC. - - External authentication script. - -- **Others** - - Support for virtual hosting. - - Compressing XML streams with Stream Compression (XEP-0138). - - Statistics via Statistics Gathering (XEP-0039). - - IPv6 support both for c2s and s2s connections. - - Multi-User Chat module with support for clustering and HTML logging. - - Users Directory based on users vCards. - - Publish-Subscribe component with support for Personal Eventing. - - Support for web clients: HTTP Polling and HTTP Binding (BOSH). - - IRC transport. - - Component support: interface with networks such as AIM, ICQ and MSN. - - -Quickstart guide ----------------- - -### 0. Requirements - -To compile ejabberd you need: - - - GNU Make. - - GCC. - - Libexpat 1.95 or higher. - - Libyaml 0.1.4 or higher. - - Erlang/OTP R15B or higher. - - OpenSSL 0.9.8 or higher, for STARTTLS, SASL and SSL encryption. - - Zlib 1.2.3 or higher, for Stream Compression support (XEP-0138). Optional. - - PAM library. Optional. For Pluggable Authentication Modules (PAM). - - GNU Iconv 1.8 or higher, for the IRC Transport (mod_irc). Optional. Not - needed on systems with GNU Libc. - - ImageMagick's Convert program. Optional. For CAPTCHA challenges. - - -### 1. Compile and install on *nix systems - -To compile ejabberd, execute the following commands. The first one is only -necessary if your source tree didn't come with a `configure` script. - - ./autogen.sh - ./configure - make - -To install ejabberd, run this command with system administrator rights (root -user): - - sudo make install - -These commands will: - -- Install the configuration files in `/etc/ejabberd/` -- Install ejabberd binary, header and runtime files in `/lib/ejabberd/` -- Install the administration script: `/sbin/ejabberdctl` -- Install ejabberd documentation in `/share/doc/ejabberd/` -- Create a spool directory: `/var/lib/ejabberd/` -- Create a directory for log files: `/var/log/ejabberd/` - - -### 2. Start ejabberd - -You can use the `ejabberdctl` command line administration script to -start and stop ejabberd. For example: - - ejabberdctl start - - -For detailed information please refer to the ejabberd Installation and -Operation Guide available online and in the `doc` directory of the source -tarball. - - -Links ------ - -- Guide: https://www.process-one.net/docs/ejabberd/guide_en.html -- Official site: https://www.process-one.net/en/ejabberd -- Community site: https://www.ejabberd.im -- Forum: https://www.process-one.net/en/forum diff --git a/README.md b/README.md deleted file mode 120000 index 100b93820..000000000 --- a/README.md +++ /dev/null @@ -1 +0,0 @@ -README \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 000000000..646cc4a17 --- /dev/null +++ b/README.md @@ -0,0 +1,136 @@ + +

+ +

+

+ + + + + + + + + + +
+ + + + + + + + +

+ +[ejabberd][im] is an open-source, +robust, scalable and extensible realtime platform built using [Erlang/OTP][erlang], +that includes [XMPP][xmpp] Server, [MQTT][mqtt] Broker and [SIP][sip] Service. + +Check the features in [ejabberd.im][im], [ejabberd Docs][features], +[ejabberd at ProcessOne][p1home], and the list of [supported protocols in ProcessOne][xeps] +and [XMPP.org][xmppej]. + +Installation +------------ + +There are several ways to install ejabberd: + +- Source code: compile yourself, see [COMPILE](COMPILE.md) +- Installers: + - [ProcessOne Download Page][p1download] or [GitHub Releases][releases] for releases. + - [GitHub Actions](https://github.com/processone/ejabberd/actions/workflows/installers.yml) for master branch (`run`/`deb`/`rpm` for `x64` and `arm64`) +- Docker Containers: + - `ecs` container image: [Docker Hub][hubecs] and [Github Packages][packagesecs], see [ecs README][docker-ecs-readme] (for `x64`) + - `ejabberd` container image: [Github Packages][packages] for releases and master branch, see [CONTAINER](CONTAINER.md) (for `x64` and `arm64`) +- Using your [Operating System package][osp] +- Using the [Homebrew][homebrew] package manager + +More info can be found in the `Installation` part of [ejabberd Docs](https://docs.ejabberd.im/admin/install/). + +Documentation +------------- + +Please check the [ejabberd Docs][docs] website. + +When compiling from source code, you can get some help with: + + ./configure --help + make help + +Once ejabberd is installed, try: + + ejabberdctl help + man ejabberd.yml + +Development +----------- + +Bug reports and features are tracked using [GitHub Issues][issues], +please check [CONTRIBUTING](CONTRIBUTING.md) for details. + +Translations can be improved online [using Weblate][weblate] +or in your local machine as explained in [Localization][localization]. + +Documentation for developers is available in [ejabberd docs: Developers][docs-dev]. + +There are nightly builds of ejabberd, both for `master` branch and for Pull Requests: + +- Installers: go to [GitHub Actions: Installers](https://github.com/processone/ejabberd/actions/workflows/installers.yml), open the most recent commit, on the bottom of that commit page, download the `ejabberd-packages.zip` artifact. +- `ejabberd` container image: go to [ejabberd Github Packages][packages] + +Security reports or concerns should preferably be reported privately, +please send an email to the address: contact at process-one dot net +or some other method from [ProcessOne Contact][p1contact]. + +For commercial offering and support, including [ejabberd Business Edition][p1home] +and [Fluux (ejabberd in the Cloud)][fluux], please check [ProcessOne ejabberd page][p1home]. + +Security +-------- + +For information on how to report security vulnerabilities, please refer to the [SECURITY.md](SECURITY.md) file. It contains guidelines on how to report vulnerabilities privately and securely, ensuring that any issues are addressed in a timely and confidential manner. + +Community +--------- + +There are several places to get in touch with other ejabberd developers and administrators: + +- ejabberd XMPP chatroom: [ejabberd@conference.process-one.net][muc] +- [GitHub Discussions][discussions] +- [Stack Overflow][stackoverflow] + +License +------- + +- ejabberd is released under the __GNU General Public License v2__ (see [COPYING](COPYING)) +- [ejabberd translations](https://github.com/processone/ejabberd-po/) under __MIT License__. + +[discussions]: https://github.com/processone/ejabberd/discussions +[docker-ecs-readme]: https://github.com/processone/docker-ejabberd/tree/master/ecs#readme +[docs-dev]: https://docs.ejabberd.im/developer/ +[docs]: https://docs.ejabberd.im +[erlang]: https://www.erlang.org/ +[features]: https://docs.ejabberd.im/admin/introduction/ +[fluux]: https://fluux.io/ +[homebrew]: https://docs.ejabberd.im/admin/install/homebrew/ +[hubecs]: https://hub.docker.com/r/ejabberd/ecs/ +[im]: https://www.ejabberd.im/ +[issues]: https://github.com/processone/ejabberd/issues +[localization]: https://docs.ejabberd.im/developer/extending-ejabberd/localization/ +[mqtt]: https://mqtt.org/ +[muc]: xmpp:ejabberd@conference.process-one.net +[osp]: https://docs.ejabberd.im/admin/install/os-package/ +[p1contact]: https://www.process-one.net/contact/ +[p1download]: https://www.process-one.net/download/ejabberd/ +[p1home]: https://www.process-one.net/ejabberd/ +[packages]: https://github.com/processone/ejabberd/pkgs/container/ejabberd +[packagesecs]: https://github.com/processone/docker-ejabberd/pkgs/container/ecs +[releases]: https://github.com/processone/ejabberd/releases +[sip]: https://en.wikipedia.org/wiki/Session_Initiation_Protocol +[stackoverflow]: https://stackoverflow.com/questions/tagged/ejabberd?sort=newest +[weblate]: https://hosted.weblate.org/projects/ejabberd/ejabberd-po/ +[xeps]: https://www.process-one.net/ejabberd-features/ +[xmpp]: https://xmpp.org/ +[xmppej]: https://xmpp.org/software/servers/ejabberd/ diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..bb2292826 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,45 @@ +# Security Policy + +## Supported Versions + +We recommend that all users always use the latest version of ejabberd. + +To ensure the best experience and security, upgrade to the latest version available on [this repo](https://github.com/processone/ejabberd). + +## Reporting a Vulnerability + +### Private Reporting + +**Preferred Method**: Use GitHub's private vulnerability reporting system by clicking the "Report a Vulnerability" button in the [Security tab of this repository](https://github.com/processone/ejabberd/security). This ensures your report is securely transmitted and tracked. + +**Alternative**: If you cannot use the GitHub system, send an email to **`contact@process-one.net`** with the following details: + +- A clear description of the vulnerability. +- Steps to reproduce the issue. +- Any potential impact or exploitation scenarios. + +### Response Time + +We aim to acknowledge receipt of your report within 72 hours. You can expect regular updates on the status of your report. + +### Resolution + +If the vulnerability is confirmed, we will work on a patch or mitigation strategy. +We will notify you once the issue is resolved and coordinate a public disclosure if needed. + +### Acknowledgements + +We value and appreciate the contributions of security researchers and community members. +If you wish, we are happy to acknowledge your efforts publicly by listing your name (or alias) below in this document. +Please let us know if you would like to be recognized when reporting the vulnerability. + +## Public Discussion + +For general inquiries or discussions about the project’s security, feel free to chat with us here: + +- XMPP room: `ejabberd@conference.process-one.net` +- [GitHub Discussions](https://github.com/processone/ejabberd/discussions) + +However, please note that if the issue is **critical** or potentially exploitable, **do not share it publicly**. Instead, we strongly recommend you contact the maintainers directly via the private reporting methods outlined above to ensure a secure and timely response. + +Thank you for helping us improve the security of ejabberd! diff --git a/_checkouts/configure_deps/rebar.config b/_checkouts/configure_deps/rebar.config new file mode 100644 index 000000000..f618f3e40 --- /dev/null +++ b/_checkouts/configure_deps/rebar.config @@ -0,0 +1,2 @@ +{erl_opts, [debug_info]}. +{deps, []}. \ No newline at end of file diff --git a/_checkouts/configure_deps/src/configure_deps.app.src b/_checkouts/configure_deps/src/configure_deps.app.src new file mode 100644 index 000000000..6ef9e0763 --- /dev/null +++ b/_checkouts/configure_deps/src/configure_deps.app.src @@ -0,0 +1,9 @@ +{application, configure_deps, + [{description, "A rebar3 plugin to explicitly run configure on dependencies"}, + {vsn, "0.0.1"}, + {registered, []}, + {applications, [kernel, stdlib]}, + {env,[]}, + {modules, []}, + {links, []} + ]}. diff --git a/_checkouts/configure_deps/src/configure_deps.erl b/_checkouts/configure_deps/src/configure_deps.erl new file mode 100644 index 000000000..5ec5beb45 --- /dev/null +++ b/_checkouts/configure_deps/src/configure_deps.erl @@ -0,0 +1,8 @@ +-module(configure_deps). + +-export([init/1]). + +-spec init(rebar_state:t()) -> {ok, rebar_state:t()}. +init(State) -> + {ok, State1} = configure_deps_prv:init(State), + {ok, State1}. diff --git a/_checkouts/configure_deps/src/configure_deps_prv.erl b/_checkouts/configure_deps/src/configure_deps_prv.erl new file mode 100644 index 000000000..91f2a3adc --- /dev/null +++ b/_checkouts/configure_deps/src/configure_deps_prv.erl @@ -0,0 +1,54 @@ +-module(configure_deps_prv). + +-export([init/1, do/1, format_error/1]). + +-define(PROVIDER, 'configure-deps'). +-define(DEPS, [install_deps]). + +%% =================================================================== +%% Public API +%% =================================================================== +-spec init(rebar_state:t()) -> {ok, rebar_state:t()}. +init(State) -> + Provider = providers:create([ + {namespace, default}, + {name, ?PROVIDER}, % The 'user friendly' name of the task + {module, ?MODULE}, % The module implementation of the task + {bare, true}, % The task can be run by the user, always true + {deps, ?DEPS}, % The list of dependencies + {example, "rebar3 configure-deps"}, % How to use the plugin + {opts, []}, % list of options understood by the plugin + {short_desc, "Explicitly run ./configure for dependencies"}, + {desc, "A rebar plugin to allow explicitly running ./configure on dependencies. Useful if dependencies might change prior to compilation when configure is run."} + ]), + {ok, rebar_state:add_provider(State, Provider)}. + + +-spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}. +do(State) -> + Apps = rebar_state:project_apps(State) ++ lists:usort(rebar_state:all_deps(State)), + lists:foreach(fun do_app/1, Apps), + {ok, State}. + +exec_configure({'configure-deps', Cmd}, Dir) -> + rebar_utils:sh(Cmd, [{cd, Dir}, {use_stdout, true}]); +exec_configure(_, Acc) -> Acc. + +parse_pre_hooks({pre_hooks, PreHooks}, Acc) -> + lists:foldl(fun exec_configure/2, Acc, PreHooks); +parse_pre_hooks(_, Acc) -> Acc. + +parse_additions({add, App, Additions}, {MyApp, Dir}) when App == MyApp -> + lists:foldl(fun parse_pre_hooks/2, Dir, Additions), + {MyApp, Dir}; +parse_additions(_, Acc) -> Acc. + +do_app(App) -> + Dir = rebar_app_info:dir(App), + Opts = rebar_app_info:opts(App), + Overrides = rebar_opts:get(Opts, overrides), + lists:foldl(fun parse_additions/2, {binary_to_atom(rebar_app_info:name(App), utf8), Dir}, Overrides). + +-spec format_error(any()) -> iolist(). +format_error(Reason) -> + io_lib:format("~p", [Reason]). diff --git a/asn1/ELDAPv3.asn1~ b/asn1/ELDAPv3.asn1~ deleted file mode 100644 index 1fec35cd8..000000000 --- a/asn1/ELDAPv3.asn1~ +++ /dev/null @@ -1,301 +0,0 @@ --- LDAPv3 ASN.1 specification, taken from RFC 2251 - --- Lightweight-Directory-Access-Protocol-V3 DEFINITIONS -ELDAPv3 DEFINITIONS -IMPLICIT TAGS ::= - -BEGIN - -LDAPMessage ::= SEQUENCE { - messageID MessageID, - protocolOp CHOICE { - bindRequest BindRequest, - bindResponse BindResponse, - unbindRequest UnbindRequest, - searchRequest SearchRequest, - searchResEntry SearchResultEntry, - searchResDone SearchResultDone, - searchResRef SearchResultReference, - modifyRequest ModifyRequest, - modifyResponse ModifyResponse, - addRequest AddRequest, - addResponse AddResponse, - delRequest DelRequest, - delResponse DelResponse, - modDNRequest ModifyDNRequest, - modDNResponse ModifyDNResponse, - compareRequest CompareRequest, - compareResponse CompareResponse, - abandonRequest AbandonRequest, - extendedReq ExtendedRequest, - extendedResp ExtendedResponse }, - controls [0] Controls OPTIONAL } - -MessageID ::= INTEGER (0 .. maxInt) - -maxInt INTEGER ::= 2147483647 -- (2^^31 - 1) -- - -LDAPString ::= OCTET STRING - -LDAPOID ::= OCTET STRING - -LDAPDN ::= LDAPString - -RelativeLDAPDN ::= LDAPString - -AttributeType ::= LDAPString - -AttributeDescription ::= LDAPString - - - - --- Wahl, et. al. Standards Track [Page 44] --- --- RFC 2251 LDAPv3 December 1997 - - -AttributeDescriptionList ::= SEQUENCE OF - AttributeDescription - -AttributeValue ::= OCTET STRING - -AttributeValueAssertion ::= SEQUENCE { - attributeDesc AttributeDescription, - assertionValue AssertionValue } - -AssertionValue ::= OCTET STRING - -Attribute ::= SEQUENCE { - type AttributeDescription, - vals SET OF AttributeValue } - -MatchingRuleId ::= LDAPString - -LDAPResult ::= SEQUENCE { - resultCode ENUMERATED { - success (0), - operationsError (1), - protocolError (2), - timeLimitExceeded (3), - sizeLimitExceeded (4), - compareFalse (5), - compareTrue (6), - authMethodNotSupported (7), - strongAuthRequired (8), - -- 9 reserved -- - referral (10), -- new - adminLimitExceeded (11), -- new - unavailableCriticalExtension (12), -- new - confidentialityRequired (13), -- new - saslBindInProgress (14), -- new - noSuchAttribute (16), - undefinedAttributeType (17), - inappropriateMatching (18), - constraintViolation (19), - attributeOrValueExists (20), - invalidAttributeSyntax (21), - -- 22-31 unused -- - noSuchObject (32), - aliasProblem (33), - invalidDNSyntax (34), - -- 35 reserved for undefined isLeaf -- - aliasDereferencingProblem (36), - -- 37-47 unused -- - inappropriateAuthentication (48), - --- Wahl, et. al. Standards Track [Page 45] --- --- RFC 2251 LDAPv3 December 1997 - - - invalidCredentials (49), - insufficientAccessRights (50), - busy (51), - unavailable (52), - unwillingToPerform (53), - loopDetect (54), - -- 55-63 unused -- - namingViolation (64), - objectClassViolation (65), - notAllowedOnNonLeaf (66), - notAllowedOnRDN (67), - entryAlreadyExists (68), - objectClassModsProhibited (69), - -- 70 reserved for CLDAP -- - affectsMultipleDSAs (71), -- new - -- 72-79 unused -- - other (80) }, - -- 81-90 reserved for APIs -- - matchedDN LDAPDN, - errorMessage LDAPString, - referral [3] Referral OPTIONAL } - -Referral ::= SEQUENCE OF LDAPURL - -LDAPURL ::= LDAPString -- limited to characters permitted in URLs - -Controls ::= SEQUENCE OF Control - -Control ::= SEQUENCE { - controlType LDAPOID, - criticality BOOLEAN DEFAULT FALSE, - controlValue OCTET STRING OPTIONAL } - -BindRequest ::= [APPLICATION 0] SEQUENCE { - version INTEGER (1 .. 127), - name LDAPDN, - authentication AuthenticationChoice } - -AuthenticationChoice ::= CHOICE { - simple [0] OCTET STRING, - -- 1 and 2 reserved - sasl [3] SaslCredentials } - -SaslCredentials ::= SEQUENCE { - mechanism LDAPString, - credentials OCTET STRING OPTIONAL } - -BindResponse ::= [APPLICATION 1] SEQUENCE { - --- Wahl, et. al. Standards Track [Page 46] --- --- RFC 2251 LDAPv3 December 1997 - - - COMPONENTS OF LDAPResult, - serverSaslCreds [7] OCTET STRING OPTIONAL } - -UnbindRequest ::= [APPLICATION 2] NULL - -SearchRequest ::= [APPLICATION 3] SEQUENCE { - baseObject LDAPDN, - scope ENUMERATED { - baseObject (0), - singleLevel (1), - wholeSubtree (2) }, - derefAliases ENUMERATED { - neverDerefAliases (0), - derefInSearching (1), - derefFindingBaseObj (2), - derefAlways (3) }, - sizeLimit INTEGER (0 .. maxInt), - timeLimit INTEGER (0 .. maxInt), - typesOnly BOOLEAN, - filter Filter, - attributes AttributeDescriptionList } - -Filter ::= CHOICE { - and [0] SET OF Filter, - or [1] SET OF Filter, - not [2] Filter, - equalityMatch [3] AttributeValueAssertion, - substrings [4] SubstringFilter, - greaterOrEqual [5] AttributeValueAssertion, - lessOrEqual [6] AttributeValueAssertion, - present [7] AttributeDescription, - approxMatch [8] AttributeValueAssertion, - extensibleMatch [9] MatchingRuleAssertion } - -SubstringFilter ::= SEQUENCE { - type AttributeDescription, - -- at least one must be present - substrings SEQUENCE OF CHOICE { - initial [0] LDAPString, - any [1] LDAPString, - final [2] LDAPString } } - -MatchingRuleAssertion ::= SEQUENCE { - matchingRule [1] MatchingRuleId OPTIONAL, - type [2] AttributeDescription OPTIONAL, - matchValue [3] AssertionValue, - dnAttributes [4] BOOLEAN DEFAULT FALSE } - --- Wahl, et. al. Standards Track [Page 47] --- --- RFC 2251 LDAPv3 December 1997 - -SearchResultEntry ::= [APPLICATION 4] SEQUENCE { - objectName LDAPDN, - attributes PartialAttributeList } - -PartialAttributeList ::= SEQUENCE OF SEQUENCE { - type AttributeDescription, - vals SET OF AttributeValue } - -SearchResultReference ::= [APPLICATION 19] SEQUENCE OF LDAPURL - -SearchResultDone ::= [APPLICATION 5] LDAPResult - -ModifyRequest ::= [APPLICATION 6] SEQUENCE { - object LDAPDN, - modification SEQUENCE OF SEQUENCE { - operation ENUMERATED { - add (0), - delete (1), - replace (2) }, - modification AttributeTypeAndValues } } - -AttributeTypeAndValues ::= SEQUENCE { - type AttributeDescription, - vals SET OF AttributeValue } - -ModifyResponse ::= [APPLICATION 7] LDAPResult - -AddRequest ::= [APPLICATION 8] SEQUENCE { - entry LDAPDN, - attributes AttributeList } - -AttributeList ::= SEQUENCE OF SEQUENCE { - type AttributeDescription, - vals SET OF AttributeValue } - -AddResponse ::= [APPLICATION 9] LDAPResult - -DelRequest ::= [APPLICATION 10] LDAPDN - -DelResponse ::= [APPLICATION 11] LDAPResult - -ModifyDNRequest ::= [APPLICATION 12] SEQUENCE { - entry LDAPDN, - newrdn RelativeLDAPDN, - deleteoldrdn BOOLEAN, - newSuperior [0] LDAPDN OPTIONAL } - -ModifyDNResponse ::= [APPLICATION 13] LDAPResult - --- Wahl, et. al. Standards Track [Page 48] --- --- RFC 2251 LDAPv3 December 1997 - - -CompareRequest ::= [APPLICATION 14] SEQUENCE { - entry LDAPDN, - ava AttributeValueAssertion } - -CompareResponse ::= [APPLICATION 15] LDAPResult - -AbandonRequest ::= [APPLICATION 16] MessageID - -ExtendedRequest ::= [APPLICATION 23] SEQUENCE { - requestName [0] LDAPOID, - requestValue [1] OCTET STRING OPTIONAL } - -ExtendedResponse ::= [APPLICATION 24] SEQUENCE { - COMPONENTS OF LDAPResult, - responseName [10] LDAPOID OPTIONAL, - response [11] OCTET STRING OPTIONAL } - -passwdModifyOID LDAPOID ::= "1.3.6.1.4.1.4203.1.11.1" - -PasswdModifyRequestValue ::= SEQUENCE { - userIdentity [0] OCTET STRING OPTIONAL, - oldPasswd [1] OCTET STRING OPTIONAL, - newPasswd [2] OCTET STRING OPTIONAL } - -PasswdModifyResponseValue ::= SEQUENCE { - genPasswd [0] OCTET STRING OPTIONAL } - -END - - diff --git a/asn1/XmppAddr.asn1 b/asn1/XmppAddr.asn1 deleted file mode 100644 index 14f350d3d..000000000 --- a/asn1/XmppAddr.asn1 +++ /dev/null @@ -1,14 +0,0 @@ -XmppAddr { iso(1) identified-organization(3) - dod(6) internet(1) security(5) mechanisms(5) pkix(7) - id-on(8) id-on-xmppAddr(5) } - -DEFINITIONS EXPLICIT TAGS ::= -BEGIN - -id-on-xmppAddr OBJECT IDENTIFIER ::= { iso(1) identified-organization(3) - dod(6) internet(1) security(5) mechanisms(5) pkix(7) - id-on(8) 5 } - -XmppAddr ::= UTF8String - -END diff --git a/config/ejabberd.exs b/config/ejabberd.exs new file mode 100644 index 000000000..296567f1c --- /dev/null +++ b/config/ejabberd.exs @@ -0,0 +1,164 @@ +defmodule Ejabberd.ConfigFile do + use Ejabberd.Config + + def start do + [loglevel: 4, + log_rotate_size: 10485760, + log_rotate_count: 1, + auth_method: :internal, + max_fsm_queue: 1000, + language: "en", + allow_contrib_modules: true, + hosts: ["localhost"], + shaper: shaper(), + acl: acl(), + access: access()] + end + + defp shaper do + [normal: 1000, + fast: 50000, + max_fsm_queue: 1000] + end + + defp acl do + [local: + [user_regexp: "", loopback: [ip: "127.0.0.0/8"]]] + end + + defp access do + [max_user_sessions: [all: 10], + max_user_offline_messages: [admin: 5000, all: 100], + local: [local: :allow], + c2s: [blocked: :deny, all: :allow], + c2s_shaper: [admin: :none, all: :normal], + s2s_shaper: [all: :fast], + announce: [admin: :allow], + configure: [admin: :allow], + muc_admin: [admin: :allow], + muc_create: [local: :allow], + muc: [all: :allow], + pubsub_createnode: [local: :allow], + register: [all: :allow], + trusted_network: [loopback: :allow]] + end + + listen :ejabberd_c2s do + @opts [ + port: 5222, + max_stanza_size: 65536, + shaper: :c2s_shaper, + access: :c2s] + end + + listen :ejabberd_s2s_in do + @opts [port: 5269] + end + + listen :ejabberd_http do + @opts [ + port: 5280, + web_admin: true, + http_bind: true, + captcha: true] + end + + module :mod_adhoc do + end + + module :mod_announce do + @opts [access: :announce] + end + + module :mod_blocking do + end + + module :mod_caps do + end + + module :mod_carboncopy do + end + + module :mod_client_state do + @opts [ + queue_chat_states: true, + queue_presence: false] + end + + module :mod_configure do + end + + module :mod_disco do + end + + module :mod_http_bind do + end + + module :mod_last do + end + + module :mod_muc do + @opts [ + access: :muc, + access_create: :muc_create, + access_persistent: :muc_create, + access_admin: :muc_admin] + end + + module :mod_offline do + @opts [access_max_user_messages: :max_user_offline_messages] + end + + module :mod_ping do + end + + module :mod_privacy do + end + + module :mod_private do + end + + module :mod_pubsub do + @opts [ + access_createnode: :pubsub_createnode, + ignore_pep_from_offline: true, + last_item_cache: true, + plugins: ["flat", "hometree", "pep"]] + end + + module :mod_register do + @opts [welcome_message: [ + subject: "Welcome!", + body: "Hi.\nWelcome to this XMPP server" + ], + ip_access: :trusted_network, + access: :register] + end + + module :mod_roster do + end + + module :mod_shared_roster do + end + + module :mod_stats do + end + + module :mod_time do + end + + module :mod_version do + end + + # Example of how to define a hook, called when the event + # specified is triggered. + # + # @event: Name of the event + # @opts: Params are optional. Available: :host and :priority. + # If missing, defaults are used. (host: :global | priority: 50) + # @callback Could be an anonymous function or a callback from a module, + # use the &ModuleName.function/arity format for that. + hook :register_user, [host: "localhost"], fn(user, server) -> + info("User registered: #{user} on #{server}") + end +end diff --git a/config/runtime.exs b/config/runtime.exs new file mode 100644 index 000000000..adfc18c06 --- /dev/null +++ b/config/runtime.exs @@ -0,0 +1,15 @@ +import Config + +rootdefault = case System.get_env("RELIVE", "false") do + "true" -> "_build/relive" + "false" -> "" +end + +rootpath = System.get_env("RELEASE_ROOT", rootdefault) +config :ejabberd, + file: Path.join(rootpath, "conf/ejabberd.yml"), + log_path: Path.join(rootpath, "logs/ejabberd.log") +config :mnesia, + dir: Path.join(rootpath, "database/") +config :exsync, + reload_callback: {:ejabberd_admin, :update, []} diff --git a/configure.ac b/configure.ac index 8715c86ba..b91595dc5 100644 --- a/configure.ac +++ b/configure.ac @@ -1,14 +1,27 @@ # -*- Autoconf -*- # Process this file with autoconf to produce a configure script. -AC_PREREQ(2.53) -AC_INIT(ejabberd, m4_esyscmd([echo `git describe --tags 2>/dev/null || echo 0.0` | sed 's/-g.*//;s/-/./' | tr -d '\012']), [ejabberd@process-one.net], [ejabberd]) -REQUIRE_ERLANG_MIN="5.9.1 (Erlang/OTP R15B01)" -REQUIRE_ERLANG_MAX="9.0.0 (No Max)" +AC_PREREQ(2.59) +AC_INIT(ejabberd, m4_esyscmd([echo `git describe --tags 2>/dev/null || echo 25.08` | sed 's/-g.*//;s/-/./' | tr -d '\012']), [ejabberd@process-one.net], [ejabberd]) + +AC_ARG_WITH(min-erlang, + AS_HELP_STRING([--with-min-erlang=version],[set minimal required erlang version, default to OTP25]), +[if test "X$withval" = "X"; then + REQUIRE_ERLANG_MIN="13.0 (Erlang/OTP 25.0)" +else + REQUIRE_ERLANG_MIN="$withval" +fi +], [REQUIRE_ERLANG_MIN="13.0 (Erlang/OTP 25.0)"]) + +REQUIRE_ERLANG_MAX="100.0.0 (No Max)" + +AC_CONFIG_MACRO_DIR([m4]) # Checks for programs. AC_PROG_MAKE_SET +AC_PROG_AWK AC_PROG_INSTALL +AC_PROG_MKDIR_P AC_PROG_SED if test "x$GCC" = "xyes"; then @@ -17,8 +30,7 @@ fi # Checks Erlang runtime and compiler AC_ARG_WITH(erlang, - AC_HELP_STRING([--with-erlang=dir], - [search for erlang in dir]), + AS_HELP_STRING([--with-erlang=dir],[search for erlang in dir]), [if test "$withval" = "yes" -o "$withval" = "no" -o "X$with_erlang" = "X"; then extra_erl_path="" else @@ -26,24 +38,46 @@ else fi ]) +AC_ARG_WITH(rebar, + AS_HELP_STRING([--with-rebar=bin],[use as build tool the rebar/rebar3/mix binary specified]), +[if test "$withval" = "yes" -o "$withval" = "no" -o "X$with_rebar" = "X"; then + rebar="rebar3" +else + rebar="$with_rebar" +fi +], [rebar="unconfigured"]) + AC_PATH_TOOL(ERL, erl, , [${extra_erl_path}$PATH]) AC_PATH_TOOL(ERLC, erlc, , [${extra_erl_path}$PATH]) +AC_PATH_TOOL(EPMD, epmd, , [${extra_erl_path}$PATH]) + +AC_PATH_TOOL(REBAR, rebar, , [${extra_erl_path}$PATH]) +AC_PATH_TOOL(REBAR3, rebar3, , [${extra_erl_path}$PATH]) +AC_PATH_TOOL(ELIXIR, elixir, , [${extra_erl_path}$PATH]) +AC_PATH_TOOL(IEX, iex, , [${extra_erl_path}$PATH]) +AC_PATH_TOOL(MIX, mix, , [${extra_erl_path}$PATH]) + +if test "$rebar" = unconfigured; then + if test "x$ELIXIR" = "x" -o "x$IEX" = "x" -o "x$MIX" = "x"; then + if test "x$REBAR3" = "x"; then + rebar="rebar3" + else + rebar=$REBAR3 + fi + else + rebar=$MIX + fi +fi +if test "x$rebar" = "xrebar" -a "x$REBAR" = "x" ; then + rebar="./rebar" +fi +if test "x$rebar" = "xrebar3" -a "x$REBAR3" = "x" ; then + rebar="./rebar3" +fi AC_ERLANG_NEED_ERL AC_ERLANG_NEED_ERLC -AC_ARG_ENABLE(erlang-version-check, -[AC_HELP_STRING([--enable-erlang-version-check], - [Check Erlang/OTP version @<:@default=yes@:>@])]) -case "$enable_erlang_version_check" in - yes|'') - ERLANG_VERSION_CHECK([$REQUIRE_ERLANG_MIN],[$REQUIRE_ERLANG_MAX]) - ;; - no) - ERLANG_VERSION_CHECK([$REQUIRE_ERLANG_MIN],[$REQUIRE_ERLANG_MAX],[warn]) - ;; -esac - # Checks and sets ERLANG_ROOT_DIR and ERLANG_LIB_DIR variable AC_ERLANG_SUBST_ROOT_DIR # AC_ERLANG_SUBST_LIB_DIR @@ -63,167 +97,191 @@ if test "x$MAKE" = "x"; then fi # Change default prefix -AC_PREFIX_DEFAULT(/) +AC_PREFIX_DEFAULT(/usr/local) -AC_ARG_ENABLE(hipe, -[AC_HELP_STRING([--enable-hipe], [compile natively with HiPE, not recommended (default: no)])], -[case "${enableval}" in - yes) hipe=true ;; - no) hipe=false ;; - *) AC_MSG_ERROR(bad value ${enableval} for --enable-hipe) ;; -esac],[hipe=false]) - -AC_ARG_ENABLE(roster_gateway_workaround, -[AC_HELP_STRING([--enable-roster-gateway-workaround], [turn on workaround for processing gateway subscriptions (default: no)])], -[case "${enableval}" in - yes) roster_gateway_workaround=true ;; - no) roster_gateway_workaround=false ;; - *) AC_MSG_ERROR(bad value ${enableval} for --enable-roster-gateway-workaround) ;; -esac],[roster_gateway_workaround=false]) - -AC_ARG_ENABLE(transient_supervisors, -[AC_HELP_STRING([--disable-transient-supervisors], [disable Erlang supervision for transient processes (default: no)])], -[case "${enableval}" in - yes) transient_supervisors=true ;; - no) transient_supervisors=false ;; - *) AC_MSG_ERROR(bad value ${enableval} for --enable-transient_supervisors) ;; -esac],[transient_supervisors=true]) - -AC_ARG_ENABLE(full_xml, -[AC_HELP_STRING([--enable-full-xml], [use XML features in XMPP stream (ex: CDATA) (default: no, requires XML compliant clients)])], -[case "${enableval}" in - yes) full_xml=true ;; - no) full_xml=false ;; - *) AC_MSG_ERROR(bad value ${enableval} for --enable-full-xml) ;; -esac],[full_xml=false]) - -AC_ARG_ENABLE(mssql, -[AC_HELP_STRING([--enable-mssql], [use Microsoft SQL Server database (default: no, requires --enable-odbc)])], -[case "${enableval}" in - yes) db_type=mssql ;; - no) db_type=generic ;; - *) AC_MSG_ERROR(bad value ${enableval} for --enable-mssql) ;; -esac],[db_type=generic]) +AC_CONFIG_FILES([Makefile + vars.config]) AC_ARG_ENABLE(all, -[AC_HELP_STRING([--enable-all], [same as --enable-nif --enable-odbc --enable-mysql --enable-pgsql --enable-pam --enable-zlib --enable-riak --enable-json --enable-elixir --enable-iconv --enable-debug --enable-lager --enable-tools (useful for Dialyzer checks, default: no)])], +[AS_HELP_STRING([--enable-all],[same as --enable-odbc --enable-mssql --enable-mysql --enable-pgsql --enable-sqlite --enable-pam --enable-zlib --enable-redis --enable-elixir --enable-stun --enable-sip --enable-debug --enable-lua --enable-tools (useful for Dialyzer checks, default: no)])], [case "${enableval}" in - yes) nif=true odbc=true mysql=true pgsql=true pam=true zlib=true riak=true json=true elixir=true iconv=true debug=true lager=true tools=true ;; - no) nif=false odbc=false mysql=false pgsql=false pam=false zlib=false riak=false json=false elixir=false iconv=false debug=false lager=false tools=false ;; + yes) odbc=true mssql=true mysql=true pgsql=true sqlite=true pam=true zlib=true redis=true elixir=true stun=true sip=true debug=true lua=true tools=true ;; + no) odbc=false mssql=false mysql=false pgsql=false sqlite=false pam=false zlib=false redis=false elixir=false stun=false sip=false debug=false lua=false tools=false ;; *) AC_MSG_ERROR(bad value ${enableval} for --enable-all) ;; esac],[]) -AC_ARG_ENABLE(tools, -[AC_HELP_STRING([--enable-tools], [build development tools (default: no)])], -[case "${enableval}" in - yes) tools=true ;; - no) tools=false ;; - *) AC_MSG_ERROR(bad value ${enableval} for --enable-tools) ;; -esac],[if test "x$tools" = "x"; then tools=false; fi]) - -AC_ARG_ENABLE(nif, -[AC_HELP_STRING([--enable-nif], [replace some functions with C equivalents. Requires Erlang R13B04 or higher (default: no)])], -[case "${enableval}" in - yes) nif=true ;; - no) nif=false ;; - *) AC_MSG_ERROR(bad value ${enableval} for --enable-nif) ;; -esac],[if test "x$nif" = "x"; then nif=false; fi]) - -AC_ARG_ENABLE(odbc, -[AC_HELP_STRING([--enable-odbc], [enable pure ODBC support (default: no)])], -[case "${enableval}" in - yes) odbc=true ;; - no) odbc=false ;; - *) AC_MSG_ERROR(bad value ${enableval} for --enable-odbc) ;; -esac],[if test "x$odbc" = "x"; then odbc=false; fi]) - -AC_ARG_ENABLE(mysql, -[AC_HELP_STRING([--enable-mysql], [enable MySQL support (default: no)])], -[case "${enableval}" in - yes) mysql=true ;; - no) mysql=false ;; - *) AC_MSG_ERROR(bad value ${enableval} for --enable-mysql) ;; -esac],[if test "x$mysql" = "x"; then mysql=false; fi]) - -AC_ARG_ENABLE(pgsql, -[AC_HELP_STRING([--enable-pgsql], [enable PostgreSQL support (default: no)])], -[case "${enableval}" in - yes) pgsql=true ;; - no) pgsql=false ;; - *) AC_MSG_ERROR(bad value ${enableval} for --enable-pgsql) ;; -esac],[if test "x$pgsql" = "x"; then pgsql=false; fi]) - -AC_ARG_ENABLE(pam, -[AC_HELP_STRING([--enable-pam], [enable PAM support (default: no)])], -[case "${enableval}" in - yes) pam=true ;; - no) pam=false ;; - *) AC_MSG_ERROR(bad value ${enableval} for --enable-pam) ;; -esac],[if test "x$pam" = "x"; then pam=false; fi]) - -AC_ARG_ENABLE(zlib, -[AC_HELP_STRING([--enable-zlib], [enable Stream Compression (XEP-0138) using zlib (default: yes)])], -[case "${enableval}" in - yes) zlib=true ;; - no) zlib=false ;; - *) AC_MSG_ERROR(bad value ${enableval} for --enable-zlib) ;; -esac],[if test "x$zlib" = "x"; then zlib=true; fi]) - -AC_ARG_ENABLE(riak, -[AC_HELP_STRING([--enable-riak], [enable Riak support (default: no)])], -[case "${enableval}" in - yes) riak=true ;; - no) riak=false ;; - *) AC_MSG_ERROR(bad value ${enableval} for --enable-riak) ;; -esac],[if test "x$riak" = "x"; then riak=false; fi]) - -AC_ARG_ENABLE(json, -[AC_HELP_STRING([--enable-json], [enable JSON support for mod_bosh (default: no)])], -[case "${enableval}" in - yes) json=true ;; - no) json=false ;; - *) AC_MSG_ERROR(bad value ${enableval} for --enable-json) ;; -esac],[if test "x$json" = "x"; then json=false; fi]) - -AC_ARG_ENABLE(elixir, -[AC_HELP_STRING([--enable-elixir], [enable Elixir support (default: no)])], -[case "${enableval}" in - yes) elixir=true ;; - no) elixir=false ;; - *) AC_MSG_ERROR(bad value ${enableval} for --enable-elixir) ;; -esac],[if test "x$elixir" = "x"; then elixir=false; fi]) - -AC_ARG_ENABLE(iconv, -[AC_HELP_STRING([--enable-iconv], [enable iconv support (default: yes)])], -[case "${enableval}" in - yes) iconv=true ;; - no) iconv=false ;; - *) AC_MSG_ERROR(bad value ${enableval} for --enable-iconv) ;; -esac],[if test "x$iconv" = "x"; then iconv=true; fi]) - AC_ARG_ENABLE(debug, -[AC_HELP_STRING([--enable-debug], [enable debug information (default: yes)])], +[AS_HELP_STRING([--enable-debug],[enable debug information (default: yes)])], [case "${enableval}" in yes) debug=true ;; no) debug=false ;; *) AC_MSG_ERROR(bad value ${enableval} for --enable-debug) ;; esac],[if test "x$debug" = "x"; then debug=true; fi]) -AC_ARG_ENABLE(lager, -[AC_HELP_STRING([--enable-lager], [enable lager support (default: yes)])], +AC_ARG_ENABLE(elixir, +[AS_HELP_STRING([--enable-elixir],[enable Elixir support in Rebar3 (default: no)])], [case "${enableval}" in - yes) lager=true ;; - no) lager=false ;; - *) AC_MSG_ERROR(bad value ${enableval} for --enable-lager) ;; -esac],[if test "x$lager" = "x"; then lager=true; fi]) + yes) elixir=true ;; + no) elixir=false ;; + *) AC_MSG_ERROR(bad value ${enableval} for --enable-elixir) ;; +esac],[if test "x$elixir" = "x"; then elixir=false; fi]) -AC_CONFIG_FILES([Makefile - vars.config - src/ejabberd.app.src]) +AC_ARG_ENABLE(erlang-version-check, +[AS_HELP_STRING([--enable-erlang-version-check],[Check Erlang/OTP version (default: yes)])]) +case "$enable_erlang_version_check" in + yes|'') + ERLANG_VERSION_CHECK([$REQUIRE_ERLANG_MIN],[$REQUIRE_ERLANG_MAX]) + ;; + no) + ERLANG_VERSION_CHECK([$REQUIRE_ERLANG_MIN],[$REQUIRE_ERLANG_MAX],[warn]) + ;; +esac + +AC_ARG_ENABLE(full_xml, +[AS_HELP_STRING([--enable-full-xml],[use XML features in XMPP stream (ex: CDATA) (default: no, requires XML compliant clients)])], +[case "${enableval}" in + yes) full_xml=true ;; + no) full_xml=false ;; + *) AC_MSG_ERROR(bad value ${enableval} for --enable-full-xml) ;; +esac],[full_xml=false]) + +ENABLEGROUP="" +AC_ARG_ENABLE(group, + [AS_HELP_STRING([--enable-group[[=GROUP]]], [specify the group of the account defined in --enable-user (default: no)])], + [case "${enableval}" in + yes) ENABLEGROUP=`groups |head -n 1` ;; + no) ENABLEGROUP="" ;; + *) ENABLEGROUP=$enableval + esac], + []) +if test "$ENABLEGROUP" != ""; then + echo "allow this system group to start ejabberd: $ENABLEGROUP" + AC_SUBST([INSTALLGROUP], [$ENABLEGROUP]) +fi + +AC_ARG_ENABLE(latest_deps, +[AS_HELP_STRING([--enable-latest-deps],[makes rebar use latest commits for dependencies instead of tagged versions (default: no)])], +[case "${enableval}" in + yes) latest_deps=true ;; + no) latest_deps=false ;; + *) AC_MSG_ERROR(bad value ${enableval} for --enable-latest-deps) ;; +esac],[if test "x$latest_deps" = "x"; then latest_deps=false; fi]) + +AC_ARG_ENABLE(lua, +[AS_HELP_STRING([--enable-lua],[enable Lua support, to import from Prosody (default: no)])], +[case "${enableval}" in + yes) lua=true ;; + no) lua=false ;; + *) AC_MSG_ERROR(bad value ${enableval} for --enable-lua) ;; +esac],[if test "x$lua" = "x"; then lua=false; fi]) + +AC_ARG_ENABLE(mssql, +[AS_HELP_STRING([--enable-mssql],[use Microsoft SQL Server database (default: no, requires --enable-odbc)])], +[case "${enableval}" in + yes) mssql=true ;; + no) mssql=false ;; + *) AC_MSG_ERROR(bad value ${enableval} for --enable-mssql) ;; +esac],[if test "x$mssql" = "x"; then mssql=false; fi]) + +AC_ARG_ENABLE(mysql, +[AS_HELP_STRING([--enable-mysql],[enable MySQL support (default: no)])], +[case "${enableval}" in + yes) mysql=true ;; + no) mysql=false ;; + *) AC_MSG_ERROR(bad value ${enableval} for --enable-mysql) ;; +esac],[if test "x$mysql" = "x"; then mysql=false; fi]) + +AC_ARG_ENABLE(new_sql_schema, +[AS_HELP_STRING([--enable-new-sql-schema],[use new SQL schema by default (default: no)])], +[case "${enableval}" in + yes) new_sql_schema=true ;; + no) new_sql_schema=false ;; + *) AC_MSG_ERROR(bad value ${enableval} for --enable-new-sql-schema) ;; +esac],[new_sql_schema=false]) + +AC_ARG_ENABLE(odbc, +[AS_HELP_STRING([--enable-odbc],[enable pure ODBC support (default: no)])], +[case "${enableval}" in + yes) odbc=true ;; + no) odbc=false ;; + *) AC_MSG_ERROR(bad value ${enableval} for --enable-odbc) ;; +esac],[if test "x$odbc" = "x"; then odbc=false; fi]) + +AC_ARG_ENABLE(pam, +[AS_HELP_STRING([--enable-pam],[enable PAM support (default: no)])], +[case "${enableval}" in + yes) pam=true ;; + no) pam=false ;; + *) AC_MSG_ERROR(bad value ${enableval} for --enable-pam) ;; +esac],[if test "x$pam" = "x"; then pam=false; fi]) + +AC_ARG_ENABLE(pgsql, +[AS_HELP_STRING([--enable-pgsql],[enable PostgreSQL support (default: no)])], +[case "${enableval}" in + yes) pgsql=true ;; + no) pgsql=false ;; + *) AC_MSG_ERROR(bad value ${enableval} for --enable-pgsql) ;; +esac],[if test "x$pgsql" = "x"; then pgsql=false; fi]) + +AC_ARG_ENABLE(redis, +[AS_HELP_STRING([--enable-redis],[enable Redis support (default: no)])], +[case "${enableval}" in + yes) redis=true ;; + no) redis=false ;; + *) AC_MSG_ERROR(bad value ${enableval} for --enable-redis) ;; +esac],[if test "x$redis" = "x"; then redis=false; fi]) + +AC_ARG_ENABLE(roster_gateway_workaround, +[AS_HELP_STRING([--enable-roster-gateway-workaround],[turn on workaround for processing gateway subscriptions (default: no)])], +[case "${enableval}" in + yes) roster_gateway_workaround=true ;; + no) roster_gateway_workaround=false ;; + *) AC_MSG_ERROR(bad value ${enableval} for --enable-roster-gateway-workaround) ;; +esac],[roster_gateway_workaround=false]) + +AC_ARG_ENABLE(sip, +[AS_HELP_STRING([--enable-sip],[enable SIP support (default: no)])], +[case "${enableval}" in + yes) sip=true ;; + no) sip=false ;; + *) AC_MSG_ERROR(bad value ${enableval} for --enable-sip) ;; +esac],[if test "x$sip" = "x"; then sip=false; fi]) + +AC_ARG_ENABLE(sqlite, +[AS_HELP_STRING([--enable-sqlite],[enable SQLite support (default: no)])], +[case "${enableval}" in + yes) sqlite=true ;; + no) sqlite=false ;; + *) AC_MSG_ERROR(bad value ${enableval} for --enable-sqlite) ;; +esac],[if test "x$sqlite" = "x"; then sqlite=false; fi]) + +AC_ARG_ENABLE(stun, +[AS_HELP_STRING([--enable-stun],[enable STUN/TURN support (default: yes)])], +[case "${enableval}" in + yes) stun=true ;; + no) stun=false ;; + *) AC_MSG_ERROR(bad value ${enableval} for --enable-stun) ;; +esac],[if test "x$stun" = "x"; then stun=true; fi]) + +AC_ARG_ENABLE(system_deps, +[AS_HELP_STRING([--enable-system-deps],[makes rebar use locally installed dependencies instead of downloading them (default: no)])], +[case "${enableval}" in + yes) system_deps=true ;; + no) system_deps=false ;; + *) AC_MSG_ERROR(bad value ${enableval} for --enable-system-deps) ;; +esac],[if test "x$system_deps" = "x"; then system_deps=false; fi]) + +AC_ARG_ENABLE(tools, +[AS_HELP_STRING([--enable-tools],[include debugging/development tools (default: no)])], +[case "${enableval}" in + yes) tools=true ;; + no) tools=false ;; + *) AC_MSG_ERROR(bad value ${enableval} for --enable-tools) ;; +esac],[if test "x$tools" = "x"; then tools=false; fi]) ENABLEUSER="" AC_ARG_ENABLE(user, - [AS_HELP_STRING([--enable-user[[[[=USER]]]]], [allow this system user to start ejabberd (default: no)])], + [AS_HELP_STRING([--enable-user[[=USER]]], [allow this system user to start ejabberd (default: no)])], [case "${enableval}" in yes) ENABLEUSER=`whoami` ;; no) ENABLEUSER="" ;; @@ -235,23 +293,77 @@ if test "$ENABLEUSER" != ""; then AC_SUBST([INSTALLUSER], [$ENABLEUSER]) fi -AC_SUBST(hipe) +AC_ARG_ENABLE(zlib, +[AS_HELP_STRING([--enable-zlib],[enable Stream Compression (XEP-0138) using zlib (default: yes)])], +[case "${enableval}" in + yes) zlib=true ;; + no) zlib=false ;; + *) AC_MSG_ERROR(bad value ${enableval} for --enable-zlib) ;; +esac],[if test "x$zlib" = "x"; then zlib=true; fi]) + +case "`uname`" in + "Darwin") + # Darwin (macos) erlang-sqlite is built using amalgamated lib, so no external dependency + ;; + *) + if test "$sqlite" = "true"; then + AX_LIB_SQLITE3([3.6.19]) + if test "x$SQLITE3_VERSION" = "x"; then + AC_MSG_ERROR(SQLite3 library >= 3.6.19 was not found) + fi + fi + ;; +esac + +AC_MSG_RESULT([build tool to use (change using --with-rebar): $rebar]) + AC_SUBST(roster_gateway_workaround) -AC_SUBST(transient_supervisors) +AC_SUBST(new_sql_schema) AC_SUBST(full_xml) -AC_SUBST(nif) -AC_SUBST(db_type) AC_SUBST(odbc) +AC_SUBST(mssql) AC_SUBST(mysql) AC_SUBST(pgsql) +AC_SUBST(sqlite) AC_SUBST(pam) AC_SUBST(zlib) -AC_SUBST(riak) -AC_SUBST(json) +AC_SUBST(rebar) +AC_SUBST(redis) AC_SUBST(elixir) -AC_SUBST(iconv) +AC_SUBST(stun) +AC_SUBST(sip) AC_SUBST(debug) -AC_SUBST(lager) +AC_SUBST(lua) AC_SUBST(tools) +AC_SUBST(latest_deps) +AC_SUBST(system_deps) +AC_SUBST(CFLAGS) +AC_SUBST(CPPFLAGS) +AC_SUBST(LDFLAGS) AC_OUTPUT + +AS_CASE([$rebar], + [*rebar3], [ + deps="" + AS_IF([test "x$stun" = "xfalse"], [deps="stun,$deps"]) + AS_IF([test "x$sqlite" = "xfalse"], [deps="sqlite3,$deps"]) + AS_IF([test "x$pgsql" = "xfalse"], [deps="p1_pgsql,$deps"]) + AS_IF([test "x$mysql" = "xfalse"], [deps="p1_mysql,$deps"]) + AS_IF([test "x$zlib" = "xfalse"], [deps="ezlib,$deps"]) + AS_IF([test "x$sip" = "xfalse"], [deps="esip,$deps"]) + AS_IF([test "x$redis" = "xfalse"], [deps="eredis,$deps"]) + AS_IF([test "x$pam" = "xfalse"], [deps="epam,$deps"]) + AS_IF([test "x$deps" = "x"], [], + [AC_MSG_NOTICE([unlocking disabled rebar3 dependencies: $deps]) + $rebar unlock "$deps"]) + deps="" + ERLANG_VERSION=m4_esyscmd([erl -noinput -noshell -eval 'erlang:display(list_to_integer(erlang:system_info(otp_release))), halt().']) + AS_IF([test "$ERLANG_VERSION" -lt "21"], [deps="luerl,$deps"]) + AS_IF([test "$ERLANG_VERSION" -lt "22"], [deps="lager,$deps"]) + AS_IF([test "$ERLANG_VERSION" -le "23"], [deps="jose,$deps"]) + AS_IF([test "$ERLANG_VERSION" -ge "27"], [deps="jiffy,$deps"]) + AS_IF([test "x$deps" = "x"], [], + [AC_MSG_NOTICE([unlocking rebar3 dependencies for old Erlang/OTP: $deps]) + $rebar unlock "$deps"]) + ]) diff --git a/contrib/extract_translations/README b/contrib/extract_translations/README deleted file mode 100644 index 9278dd106..000000000 --- a/contrib/extract_translations/README +++ /dev/null @@ -1,21 +0,0 @@ -extract_translations - auxiliary tool that extracts lines to be translated -from ejabberd source tree. - -Building: - erlc extract_translations.erl - -Invoking 1: - erl -noinput -s extract_translations -extra dirname message_file - - where dirname is the directory "src" in ejabberd's source tree root, - message_file is a file with translated messages (src/msgs/*.msg). - - Result is a list of messages from source files which aren't contained in - message file. - -Invoking 2: - erl -noinput -s extract_translations -extra -unused dirname message_file - - Result is a list of messages from message file which aren't in source - files anymore. - diff --git a/contrib/extract_translations/extract_translations.erl b/contrib/extract_translations/extract_translations.erl deleted file mode 100644 index 97bef684c..000000000 --- a/contrib/extract_translations/extract_translations.erl +++ /dev/null @@ -1,304 +0,0 @@ -%%%---------------------------------------------------------------------- -%%% File : extract_translations.erl -%%% Author : Sergei Golovan -%%% Purpose : Auxiliary tool for interface/messages translators -%%% Created : 23 Apr 2005 by Sergei Golovan -%%% Id : $Id$ -%%%---------------------------------------------------------------------- - --module(extract_translations). --author('sgolovan@nes.ru'). - --export([start/0]). - --define(STATUS_SUCCESS, 0). --define(STATUS_ERROR, 1). --define(STATUS_USAGE, 2). - --include_lib("kernel/include/file.hrl"). - - -start() -> - ets:new(translations, [named_table, public]), - ets:new(translations_obsolete, [named_table, public]), - ets:new(files, [named_table, public]), - ets:new(vars, [named_table, public]), - case init:get_plain_arguments() of - ["-srcmsg2po", Dir, File] -> - print_po_header(File), - Status = process(Dir, File, srcmsg2po), - halt(Status); - ["-unused", Dir, File] -> - Status = process(Dir, File, unused), - halt(Status); - [Dir, File] -> - Status = process(Dir, File, used), - halt(Status); - _ -> - print_usage(), - halt(?STATUS_USAGE) - end. - - -process(Dir, File, Used) -> - case load_file(File) of - {error, Reason} -> - io:format("~s: ~s~n", [File, file:format_error(Reason)]), - ?STATUS_ERROR; - _ -> - FileList = find_src_files(Dir), - lists:foreach( - fun(F) -> - parse_file(Dir, F, Used) - end, FileList), - case Used of - unused -> - ets:foldl(fun({Key, _}, _) -> - io:format("~p~n", [Key]) - end, ok, translations); - srcmsg2po -> - ets:foldl(fun({Key, Trans}, _) -> - print_translation_obsolete(Key, Trans) - end, ok, translations_obsolete); - _ -> - ok - end, - ?STATUS_SUCCESS - end. - -parse_file(Dir, File, Used) -> - ets:delete_all_objects(vars), - case epp:parse_file(File, [Dir, filename:dirname(File) | code:get_path()], []) of - {ok, Forms} -> - lists:foreach( - fun(F) -> - parse_form(Dir, File, F, Used) - end, Forms); - _ -> - ok - end. - -parse_form(Dir, File, Form, Used) -> - case Form of - %%{undefined, Something} -> - %% io:format("Undefined: ~p~n", [Something]); - {call, - _, - {remote, _, {atom, _, translate}, {atom, _, translate}}, - [_, {string, Line, Str}] - } -> - process_string(Dir, File, Line, Str, Used); - {call, - _, - {remote, _, {atom, _, translate}, {atom, _, translate}}, - [_, - {bin,_, - [{bin_element,_, - {string,Line,Str}, - default,default}]}] - } -> - process_string(Dir, File, Line, Str, Used); - {call, - _, - {remote, _, {atom, _, translate}, {atom, _, translate}}, - [_, {var, _, Name}] - } -> - case ets:lookup(vars, Name) of - [{_Name, Value, Line}] -> - process_string(Dir, File, Line, Value, Used); - _ -> - ok - end; - {match, - _, - {var, _, Name}, - {string, Line, Value} - } -> - ets:insert(vars, {Name, Value, Line}); - L when is_list(L) -> - lists:foreach( - fun(F) -> - parse_form(Dir, File, F, Used) - end, L); - T when is_tuple(T) -> - lists:foreach( - fun(F) -> - parse_form(Dir, File, F, Used) - end, tuple_to_list(T)); - _ -> - ok - end. - -process_string(_Dir, _File, _Line, "", _Used) -> - ok; - -process_string(_Dir, File, Line, Str, Used) -> - case {ets:lookup(translations, Str), Used} of - {[{_Key, _Trans}], unused} -> - ets:delete(translations, Str); - {[{_Key, _Trans}], used} -> - ok; - {[{_Key, Trans}], srcmsg2po} -> - ets:delete(translations_obsolete, Str), - print_translation(File, Line, Str, Trans); - {_, used} -> - case ets:lookup(files, File) of - [{_}] -> - ok; - _ -> - io:format("~n% ~s~n", [File]), - ets:insert(files, {File}) - end, - case Str of - [] -> ok; - _ -> io:format("{~p, \"\"}.~n", [Str]) - end, - ets:insert(translations, {Str, ""}); - {_, srcmsg2po} -> - case ets:lookup(files, File) of - [{_}] -> - ok; - _ -> - ets:insert(files, {File}) - end, - ets:insert(translations, {Str, ""}), - print_translation(File, Line, Str, ""); - _ -> - ok - end. - -load_file(File) -> - case file:consult(File) of - {ok, Terms} -> - lists:foreach( - fun({Orig, Trans}) -> - case Trans of - "" -> - ok; - _ -> - ets:insert(translations, {Orig, Trans}), - ets:insert(translations_obsolete, {Orig, Trans}) - end - end, Terms); - Err -> - Err - end. - -find_src_files(Dir) -> - case file:list_dir(Dir) of - {ok, FileList} -> - recurse_filelist( - lists:map( - fun(F) -> - filename:join(Dir, F) - end, FileList)); - _ -> - [] - end. - -recurse_filelist(FileList) -> - recurse_filelist(FileList, []). - -recurse_filelist([], Acc) -> - lists:reverse(Acc); - -recurse_filelist([H | T], Acc) -> - case file:read_file_info(H) of - {ok, #file_info{type = directory}} -> - recurse_filelist(T, lists:reverse(find_src_files(H)) ++ Acc); - {ok, #file_info{type = regular}} -> - case string:substr(H, string:len(H) - 3) of - ".erl" -> - recurse_filelist(T, [H | Acc]); - ".hrl" -> - recurse_filelist(T, [H | Acc]); - _ -> - recurse_filelist(T, Acc) - end; - _ -> - recurse_filelist(T, Acc) - end. - - -print_usage() -> - io:format( - "Usage: extract_translations [-unused] dir file~n" - "~n" - "Example:~n" - " extract_translations . ./msgs/ru.msg~n" - ). - - -%%% -%%% Gettext -%%% - -print_po_header(File) -> - MsgProps = get_msg_header_props(File), - {Language, [LastT | AddT]} = prepare_props(MsgProps), - application:load(ejabberd), - {ok, Version} = application:get_key(ejabberd, vsn), - print_po_header(Version, Language, LastT, AddT). - -get_msg_header_props(File) -> - {ok, F} = file:open(File, [read]), - Lines = get_msg_header_props(F, []), - file:close(F), - Lines. - -get_msg_header_props(F, Lines) -> - String = io:get_line(F, ""), - case io_lib:fread("% ", String) of - {ok, [], RemString} -> - case io_lib:fread("~s", RemString) of - {ok, [Key], Value} when Value /= "\n" -> - %% The first character in Value is a blankspace: - %% And the last characters are 'slash n' - ValueClean = string:substr(Value, 2, string:len(Value)-2), - get_msg_header_props(F, Lines ++ [{Key, ValueClean}]); - _ -> - get_msg_header_props(F, Lines) - end; - _ -> - Lines - end. - -prepare_props(MsgProps) -> - Language = proplists:get_value("Language:", MsgProps), - Authors = proplists:get_all_values("Author:", MsgProps), - {Language, Authors}. - -print_po_header(Version, Language, LastTranslator, AdditionalTranslatorsList) -> - AdditionalTranslatorsString = build_additional_translators(AdditionalTranslatorsList), - HeaderString = - "msgid \"\"\n" - "msgstr \"\"\n" - "\"Project-Id-Version: " ++ Version ++ "\\n\"\n" - ++ "\"X-Language: " ++ Language ++ "\\n\"\n" - "\"Last-Translator: " ++ LastTranslator ++ "\\n\"\n" - ++ AdditionalTranslatorsString ++ - "\"MIME-Version: 1.0\\n\"\n" - "\"Content-Type: text/plain; charset=UTF-8\\n\"\n" - "\"Content-Transfer-Encoding: 8bit\\n\"\n", - io:format("~s~n", [HeaderString]). - -build_additional_translators(List) -> - lists:foldl( - fun(T, Str) -> - Str ++ "\"X-Additional-Translator: " ++ T ++ "\\n\"\n" - end, - "", - List). - -print_translation(File, Line, Str, StrT) -> - StrQ = ejabberd_regexp:greplace(list_to_binary(Str), <<"\\\"">>, <<"\\\\\"">>), - StrTQ = ejabberd_regexp:greplace(list_to_binary(StrT), <<"\\\"">>, <<"\\\\\"">>), - io:format("#: ~s:~p~nmsgid \"~s\"~nmsgstr \"~s\"~n~n", [File, Line, StrQ, StrTQ]). - -print_translation_obsolete(Str, StrT) -> - File = "unknown.erl", - Line = 1, - StrQ = ejabberd_regexp:greplace(Str, "\\\"", "\\\\\""), - StrTQ = ejabberd_regexp:greplace(StrT, "\\\"", "\\\\\""), - io:format("#: ~s:~p~n#~~ msgid \"~s\"~n#~~ msgstr \"~s\"~n~n", [File, Line, StrQ, StrTQ]). - diff --git a/contrib/extract_translations/prepare-translation.sh b/contrib/extract_translations/prepare-translation.sh deleted file mode 100755 index 56f999247..000000000 --- a/contrib/extract_translations/prepare-translation.sh +++ /dev/null @@ -1,366 +0,0 @@ -#!/bin/bash - -# Frontend for ejabberd's extract_translations.erl -# by Badlop - -# How to create template files for a new language: -# NEWLANG=zh -# cp msgs/ejabberd.pot msgs/$NEWLANG.po -# echo \{\"\",\"\"\}. > msgs/$NEWLANG.msg -# ../../extract_translations/prepare-translation.sh -updateall - -prepare_dirs () -{ - # Where is Erlang binary - ERL=`which erl` - - EJA_SRC_DIR=$EJA_DIR/src/ - EJA_MSGS_DIR=$EJA_DIR/priv/msgs/ - EXTRACT_DIR=$EJA_DIR/contrib/extract_translations/ - EXTRACT_ERL=$EXTRACT_DIR/extract_translations.erl - EXTRACT_BEAM=$EXTRACT_DIR/extract_translations.beam - - SRC_DIR=$RUN_DIR/src - EBIN_DIR=$RUN_DIR/ebin - MSGS_DIR=$EJA_DIR/priv/msgs - - if !([[ -n $EJA_DIR ]]) - then - echo "ejabberd dir does not exist: $EJA_DIR" - fi - - if !([[ -x $EXTRACT_BEAM ]]) - then - sh -c "cd $EXTRACT_DIR; $ERL -compile $EXTRACT_ERL" - fi -} - -extract_lang () -{ - MSGS_FILE=$1 - MSGS_FILE2=$MSGS_FILE.translate - MSGS_PATH=$MSGS_DIR/$MSGS_FILE - MSGS_PATH2=$MSGS_DIR/$MSGS_FILE2 - - echo -n "Extracting language strings for '$MSGS_FILE':" - - echo -n " new..." - cd $SRC_DIR - $ERL -pa $EXTRACT_DIR -noinput -noshell -s extract_translations -s init stop -extra . $MSGS_PATH >$MSGS_PATH.new - sed -e 's/^% \.\//% /g;' $MSGS_PATH.new > $MSGS_PATH.new2 - mv $MSGS_PATH.new2 $MSGS_PATH.new - - echo -n " old..." - $ERL -pa $EXTRACT_DIR -noinput -noshell -s extract_translations -s init stop -extra -unused . $MSGS_PATH >$MSGS_PATH.unused - find_unused_full $MSGS_FILE $MSGS_FILE.unused - - echo "" >$MSGS_PATH2 - echo " ***** Translation file for ejabberd ***** " >>$MSGS_PATH2 - echo "" >>$MSGS_PATH2 - - echo "" >>$MSGS_PATH2 - echo " *** New strings: Can you please translate them? *** " >>$MSGS_PATH2 - cat $MSGS_PATH.new >>$MSGS_PATH2 - - echo "" >>$MSGS_PATH2 - echo "" >>$MSGS_PATH2 - echo " *** Unused strings: They will be removed automatically *** " >>$MSGS_PATH2 - cat $MSGS_PATH.unused.full >>$MSGS_PATH2 - - echo "" >>$MSGS_PATH2 - echo "" >>$MSGS_PATH2 - echo " *** Already translated strings: you can also modify any of them if you want *** " >>$MSGS_PATH2 - echo "" >>$MSGS_PATH2 - cat $MSGS_PATH.old_cleaned >>$MSGS_PATH2 - - echo " ok" - - rm $MSGS_PATH.new - rm $MSGS_PATH.old_cleaned - rm $MSGS_PATH.unused.full -} - -extract_lang_all () -{ - cd $MSGS_DIR - for i in $( ls *.msg ) ; do - extract_lang $i; - done - - echo -e "File\tMissing\tLanguage\t\tLast translator" - echo -e "----\t-------\t--------\t\t---------------" - cd $MSGS_DIR - for i in $( ls *.msg ) ; do - MISSING=`cat $i.translate | grep "\", \"\"}." | wc -l` - LANGUAGE=`grep "X-Language:" $i.translate | sed 's/% Language: //g'` - LASTAUTH=`grep "Author:" $i.translate | head -n 1 | sed 's/% Author: //g'` - echo -e "$i\t$MISSING\t$LANGUAGE\t$LASTAUTH" - done - - cd $MSGS_DIR - REVISION=`git describe --always` - zip $HOME/ejabberd-langs-$REVISION.zip *.translate; - - rm *.translate -} - -find_unused_full () -{ - DATFILE=$1 - DATFILEI=$1.old_cleaned - DELFILE=$2 - cd msgs - - DATFILE1=$DATFILE.t1 - DATFILE2=$DATFILE.t2 - - DELFILE1=$DELFILE.t1 - DELFILE2=$DELFILE.t2 - DELFILEF=$DATFILE.unused.full - echo "" >$DELFILEF - - grep -v "\\\\" $DELFILE >$DELFILE2 - echo ENDFILEMARK >>$DELFILE2 - cp $DATFILE $DATFILEI - cp $DATFILE $DATFILE2 - - cp $DELFILE2 $DELFILE1 - STRING=`head -1 $DELFILE1` - until [[ $STRING == ENDFILEMARK ]]; do - cp $DELFILE2 $DELFILE1 - cp $DATFILE2 $DATFILE1 - - STRING=`head -1 $DELFILE1` - - cat $DATFILE1 | grep "$STRING" >>$DELFILEF - cat $DATFILE1 | grep -v "$STRING" >$DATFILE2 - cat $DELFILE1 | grep -v "$STRING" >$DELFILE2 - done - - mv $DATFILE2 $DATFILEI - - rm -f $MSGS_PATH.t1 - rm $MSGS_PATH.unused - rm -f $MSGS_PATH.unused.t1 - rm $MSGS_PATH.unused.t2 - - cd .. -} - -extract_lang_srcmsg2po () -{ - LANG=$1 - LANG_CODE=$LANG.$PROJECT - MSGS_PATH=$MSGS_DIR/$LANG_CODE.msg - PO_PATH=$MSGS_DIR/$LANG_CODE.po - - echo $MSGS_PATH - - cd $SRC_DIR - $ERL -pa $EXTRACT_DIR -pa $EBIN_DIR -pa $EJA_SRC_DIR -pa /lib/ejabberd/include -noinput -noshell -s extract_translations -s init stop -extra -srcmsg2po . $MSGS_PATH >$PO_PATH.1 - sed -e 's/ \[\]$/ \"\"/g;' $PO_PATH.1 > $PO_PATH.2 - msguniq --sort-by-file $PO_PATH.2 --output-file=$PO_PATH - - rm $PO_PATH.* -} - -extract_lang_src2pot () -{ - LANG_CODE=$PROJECT - MSGS_PATH=$MSGS_DIR/$LANG_CODE.msg - POT_PATH=$MSGS_DIR/$LANG_CODE.pot - - echo -n "" >$MSGS_PATH - echo "% Language: Language Name" >>$MSGS_PATH - echo "% Author: Translator name and contact method" >>$MSGS_PATH - echo "" >>$MSGS_PATH - - cd $SRC_DIR - $ERL -pa $EXTRACT_DIR -pa $EBIN_DIR -pa $EJA_SRC_DIR -pa /lib/ejabberd/include -noinput -noshell -s extract_translations -s init stop -extra -srcmsg2po . $MSGS_PATH >$POT_PATH.1 - sed -e 's/ \[\]$/ \"\"/g;' $POT_PATH.1 > $POT_PATH.2 - - #msguniq --sort-by-file $POT_PATH.2 $EJA_MSGS_DIR --output-file=$POT_PATH - msguniq --sort-by-file $POT_PATH.2 --output-file=$POT_PATH - - rm $POT_PATH.* - rm $MSGS_PATH - - # If the project is a specific module, not the main ejabberd - if [[ $PROJECT != ejabberd ]] ; then - # Remove from project.pot the strings that are already present in the general ejabberd - EJABBERD_MSG_FILE=$EJA_MSGS_DIR/es.po # This is just some file with translated strings - POT_PATH_TEMP=$POT_PATH.temp - msgattrib --set-obsolete --only-file=$EJABBERD_MSG_FILE -o $POT_PATH_TEMP $POT_PATH - mv $POT_PATH_TEMP $POT_PATH - fi -} - -extract_lang_popot2po () -{ - LANG_CODE=$1 - PO_PATH=$MSGS_DIR/$LANG_CODE.po - POT_PATH=$MSGS_DIR/$PROJECT.pot - - msgmerge $PO_PATH $POT_PATH >$PO_PATH.translate 2>/dev/null - mv $PO_PATH.translate $PO_PATH -} - -extract_lang_po2msg () -{ - LANG_CODE=$1 - PO_PATH=$LANG_CODE.po - MS_PATH=$PO_PATH.ms - MSGID_PATH=$PO_PATH.msgid - MSGSTR_PATH=$PO_PATH.msgstr - MSGS_PATH=$LANG_CODE.msg - - cd $MSGS_DIR - - # Check PO has correct ~ - # Let's convert to C format so we can use msgfmt - PO_TEMP=$LANG_CODE.po.temp - cat $PO_PATH | sed 's/%/perc/g' | sed 's/~/%/g' | sed 's/#:.*/#, c-format/g' >$PO_TEMP - msgfmt $PO_TEMP --check-format - result=$? - rm $PO_TEMP - if [ $result -ne 0 ] ; then - exit 1 - fi - - msgattrib $PO_PATH --translated --no-fuzzy --no-obsolete --no-location --no-wrap | grep "^msg" | tail --lines=+3 >$MS_PATH - grep "^msgid" $PO_PATH.ms | sed 's/^msgid //g' >$MSGID_PATH - grep "^msgstr" $PO_PATH.ms | sed 's/^msgstr //g' >$MSGSTR_PATH - echo "%% -*- coding: latin-1 -*-" >$MSGS_PATH - paste $MSGID_PATH $MSGSTR_PATH --delimiter=, | awk '{print "{" $0 "}."}' | sort -g >>$MSGS_PATH - - rm $MS_PATH - rm $MSGID_PATH - rm $MSGSTR_PATH -} - -extract_lang_updateall () -{ - echo "Generating POT" - extract_lang_src2pot - - cd $MSGS_DIR - echo "" - echo -e "File Missing Language Last translator" - echo -e "---- ------- -------- ---------------" - for i in $( ls *.msg ) ; do - LANG_CODE=${i%.msg} - echo -n $LANG_CODE | awk '{printf "%-6s", $1 }' - - # Convert old MSG file to PO - PO=$LANG_CODE.po - [ -f $PO ] || extract_lang_srcmsg2po $LANG_CODE - - extract_lang_popot2po $LANG_CODE - extract_lang_po2msg $LANG_CODE - - MISSING=`msgfmt --statistics $PO 2>&1 | awk '{printf "%5s", $4 }'` - echo -n " $MISSING" - - LANGUAGE=`grep "X-Language:" $PO | sed 's/\"X-Language: //g' | sed 's/\\\\n\"//g' | awk '{printf "%-12s", $1}'` - echo -n " $LANGUAGE" - - LASTAUTH=`grep "Last-Translator" $PO | sed 's/\"Last-Translator: //g' | sed 's/\\\\n\"//g'` - echo " $LASTAUTH" - done - echo "" - rm messages.mo - - cd .. -} - -translation_instructions () -{ - echo "" - echo " A new file has been created for you, with the current, the new and the deprecated strings:" - echo " $MSGS_PATH2" - echo "" - echo " At the end of that file you will find the strings you must update:" - echo " - Untranslated strings are like this: {"March", ""}." - echo " To translate the string, add the text inside the commas. Example:" - echo " {"March", "Marzo"}." - echo " - Old strings that are not used: "Woowoa"" - echo " Search the entire file for those strings and remove them" - echo "" - echo " Once you have translated all the strings and removed all the old ones," - echo " rename the file to overwrite the previous one:" - echo " $MSGS_PATH" -} - -EJA_DIR=`pwd` -RUN_DIR=`pwd` -PROJECT=ejabberd - -while [ $# -ne 0 ] ; do - PARAM=$1 - shift - case $PARAM in - --) break ;; - -project) - PROJECT=$1 - shift - ;; - -ejadir) - EJA_DIR=$1 - shift - ;; - -rundir) - RUN_DIR=$1 - shift - ;; - -lang) - LANGU=$1 - prepare_dirs - extract_lang $LANGU - shift - ;; - -langall) - prepare_dirs - extract_lang_all - ;; - -srcmsg2po) - LANG_CODE=$1 - prepare_dirs - extract_lang_srcmsg2po $LANG_CODE - shift - ;; - -popot2po) - LANG_CODE=$1 - prepare_dirs - extract_lang_popot2po $LANG_CODE - shift - ;; - -src2pot) - prepare_dirs - extract_lang_src2pot - ;; - -po2msg) - LANG_CODE=$1 - prepare_dirs - extract_lang_po2msg $LANG_CODE - shift - ;; - -updateall) - prepare_dirs - extract_lang_updateall - ;; - *) - echo "Options:" - echo " -langall" - echo " -lang LANGUAGE_FILE" - echo " -srcmsg2po LANGUAGE Construct .msg file using source code to PO file" - echo " -src2pot Generate template POT file from source code" - echo " -popot2po LANGUAGE Update PO file with template POT file" - echo " -po2msg LANGUAGE Export PO file to MSG file" - echo " -updateall Generate POT and update all PO" - echo "" - echo "Example:" - echo " ./prepare-translation.sh -lang es.msg" - exit 0 - ;; - esac -done diff --git a/cover.spec b/cover.spec new file mode 100644 index 000000000..7d504d558 --- /dev/null +++ b/cover.spec @@ -0,0 +1,5 @@ +{level, details}. +{incl_dirs, ["src", "ebin"]}. +{excl_mods, [eldap, 'ELDAPv3']}. +{export, "logs/all.coverdata"}. + diff --git a/doc/Makefile b/doc/Makefile deleted file mode 100644 index 40c7a4652..000000000 --- a/doc/Makefile +++ /dev/null @@ -1,62 +0,0 @@ -# $Id$ - -SHELL = /bin/sh - -CONTRIBUTED_MODULES = "" -#ifeq ($(shell ls mod_http_bind.tex),mod_http_bind.tex) -# CONTRIBUTED_MODULES += "\\n\\setboolean{modhttpbind}{true}" -#endif - - -all: release pdf html - -release: - @printf '%s\n' "Notes for the releaser:" - @printf '%s\n' "* Do not forget to add a link to the release notes in guide.tex" - @printf '%s\n' "* Do not forget to update the version number in ebin/ejabberd.app!" - @printf '%s\n' "* Do not forget to update the features in introduction.tex (including \new{} and \improved{} tags)." - @printf '%s\n' "Press any key to continue" - ##@read foo - @printf '%s\n' "% ejabberd version (automatically generated)." > version.tex - @printf '%s\n' "\newcommand{\version}{"`sed '/vsn/!d;s/\(.*\)"\(.*\)"\(.*\)/\2/' ../ebin/ejabberd.app`"}" >> version.tex - @printf '%s' "% Contributed modules (automatically generated)." > contributed_modules.tex - @printf '%b\n' "$(CONTRIBUTED_MODULES)" >> contributed_modules.tex - -html: guide.html dev.html features.html - -pdf: guide.pdf features.pdf - -clean: - rm -f *.aux - rm -f *.haux - rm -f *.htoc - rm -f *.idx - rm -f *.ilg - rm -f *.ind - rm -f *.log - rm -f *.out - rm -f *.pdf - rm -f *.toc - [ ! -f contributed_modules.tex ] || rm contributed_modules.tex - -distclean: clean - rm -f *.html - -guide.html: guide.tex - hevea -fix -pedantic guide.tex - -dev.html: dev.tex - hevea -fix -pedantic dev.tex - -features.html: features.tex - hevea -fix -pedantic features.tex - -guide.pdf: guide.tex - pdflatex guide.tex - pdflatex guide.tex - pdflatex guide.tex - makeindex guide.idx - pdflatex guide.tex - -features.pdf: features.tex - pdflatex features.tex diff --git a/doc/api/Makefile b/doc/api/Makefile deleted file mode 100644 index de356ef20..000000000 --- a/doc/api/Makefile +++ /dev/null @@ -1,18 +0,0 @@ -APPNAME = ejabberd -VSN = $(shell sed '/vsn/!d;s/\(.*\)"\(.*\)"\(.*\)/\2/' ../../src/ejabberd.app) - -DOCDIR=. -SRCDIR=../../src - -.PHONY = all - -all: docs - -clean: - rm -f *.html - rm edoc-info - rm erlang.png - -docs: - erl -noshell -run edoc_run application \ - "'$(APPNAME)'" '"$(SRCDIR)"' '[{dir,"$(DOCDIR)"},{packages, false},{todo,true},{private,true},{def,{vsn,"$(VSN)"}},{stylesheet,"process-one.css"},{overview,"$(DOCDIR)/overview.edoc"}]' -s init stop diff --git a/doc/api/overview.edoc b/doc/api/overview.edoc deleted file mode 100644 index 61d620e87..000000000 --- a/doc/api/overview.edoc +++ /dev/null @@ -1,10 +0,0 @@ -@author Mickael Remond - [http://www.process-one.net/] -@copyright 2013 ProcessOne -@version {@vsn}, {@date} {@time} -@title ejabberd Development API Documentation - -@doc -== Introduction == - -TODO: Insert content from Jerome documentation. diff --git a/doc/api/process-one.css b/doc/api/process-one.css deleted file mode 100644 index 5cd371e10..000000000 --- a/doc/api/process-one.css +++ /dev/null @@ -1,92 +0,0 @@ -html, body { - font-family: Verdana, sans-serif; - color: #000; - background-color: #fff; -} - -h1 { - color: #4a5389; - border-bottom: solid 1px #000; -} - -h2 { - font-size: 24px; - text-align: right; - color: #4a5389; - border-bottom: 1px solid #000; -} - -h3 { - font-size: 18px; - color: #900; -} - -h4 { - font-size: 14px; - color: #000; -} - -a[href] { - color: #4a5389; -} - -a[href]:hover { - background-color: #ecefff; -} - -p, li, dd { - text-align: justify; -} - -li { - margin-top: 0.3em; -} - -li:first-child { - margin-top: 0px; -} - -blockquote { - color: #555; -} - -caption { - font-style: italic; - color: #009; - text-align: left; - margin-left: 20px; -} - -table[border="1"] { - border-collapse: collapse; - margin-bottom: 1em; -} - -table[border="1"] td { - border: 1px solid #ddd; -} - -pre, tt, code { - color: #461b7e; -} - -pre { - margin:1ex 2ex; - border:1px dashed lightgrey; - background-color:#f9f9f9; - padding:0.5ex; -} - -pre em { - font-style: normal; - font-weight: bold; -} - -dt { - margin:0ex 2ex; - font-weight:bold; -} - -dd { - margin:0ex 0ex 1ex 4ex; -} diff --git a/doc/dev.html b/doc/dev.html deleted file mode 100644 index 9c96d2716..000000000 --- a/doc/dev.html +++ /dev/null @@ -1,437 +0,0 @@ - - - - - - -Ejabberd community 14.05-120-gedfb5fc Developers Guide - - - - -

- -

-

Ejabberd community 14.05-120-gedfb5fc Developers Guide

Alexey Shchepin
- mailto:alexey@sevcom.net
- xmpp:aleksey@jabber.ru

- -logo.png - - -
I can thoroughly recommend ejabberd for ease of setup – -Kevin Smith, Current maintainer of the Psi project
-

Contents

Introduction -

ejabberd is a free and open source instant messaging server written in Erlang/OTP.

ejabberd is cross-platform, distributed, fault-tolerant, and based on open standards to achieve real-time communication.

ejabberd is designed to be a rock-solid and feature rich XMPP server.

ejabberd is suitable for small deployments, whether they need to be scalable or not, as well as extremely big deployments.

- -

1  Key Features

- -

ejabberd is: -

  • -Cross-platform: ejabberd runs under Microsoft Windows and Unix derived systems such as Linux, FreeBSD and NetBSD.
  • Distributed: You can run ejabberd on a cluster of machines and all of them will serve the same Jabber domain(s). When you need more capacity you can simply add a new cheap node to your cluster. Accordingly, you do not need to buy an expensive high-end machine to support tens of thousands concurrent users.
  • Fault-tolerant: You can deploy an ejabberd cluster so that all the information required for a properly working service will be replicated permanently on all nodes. This means that if one of the nodes crashes, the others will continue working without disruption. In addition, nodes also can be added or replaced ‘on the fly’.
  • Administrator Friendly: ejabberd is built on top of the Open Source Erlang. As a result you do not need to install an external database, an external web server, amongst others because everything is already included, and ready to run out of the box. Other administrator benefits include: -
    • -Comprehensive documentation. -
    • Straightforward installers for Linux, Mac OS X, and Windows.
    • Web Administration. -
    • Shared Roster Groups. -
    • Command line administration tool.
    • Can integrate with existing authentication mechanisms. -
    • Capability to send announce messages. -
  • Internationalized: ejabberd leads in internationalization. Hence it is very well suited in a globalized world. Related features are: -
    • -Translated to 25 languages.
    • Support for IDNA. -
  • Open Standards: ejabberd is the first Open Source Jabber server claiming to fully comply to the XMPP standard. -
- -

2  Additional Features

- -

Moreover, ejabberd comes with a wide range of other state-of-the-art features: -

  • -Modular -
    • -Load only the modules you want. -
    • Extend ejabberd with your own custom modules. -
    -
  • Security -
    • -SASL and STARTTLS for c2s and s2s connections. -
    • STARTTLS and Dialback s2s connections. -
    • Web Admin accessible via HTTPS secure access. -
    -
  • Databases -
    • -Internal database for fast deployment (Mnesia). -
    • Native MySQL support. -
    • Native PostgreSQL support. -
    • ODBC data storage support. -
    • Microsoft SQL Server support.
    • Riak NoSQL database support. -
    -
  • Authentication -
    • -Internal Authentication. -
    • PAM, LDAP, ODBC and Riak.
    • External Authentication script. -
    -
  • Others -
    • -Support for virtual hosting. -
    • Compressing XML streams with Stream Compression (XEP-0138). -
    • Statistics via Statistics Gathering (XEP-0039). -
    • IPv6 support both for c2s and s2s connections. -
    • Multi-User Chat module with support for clustering and HTML logging.
    • Users Directory based on users vCards. -
    • Publish-Subscribe component with support for Personal Eventing via Pubsub. -
    • Support for web clients: HTTP Polling and HTTP Binding (BOSH) services. -
    • IRC transport. -
    • SIP support. -
    • Component support: interface with networks such as AIM, ICQ and MSN installing special tranports. -
    -
- -

3  How it Works

-

A XMPP domain is served by one or more ejabberd nodes. These nodes can -be run on different machines that are connected via a network. They all must -have the ability to connect to port 4369 of all another nodes, and must have -the same magic cookie (see Erlang/OTP documentation, in other words the file -~ejabberd/.erlang.cookie must be the same on all nodes). This is -needed because all nodes exchange information about connected users, S2S -connections, registered services, etc…

Each ejabberd node have following modules: -

  • -router; -
  • local router. -
  • session manager; -
  • S2S manager; -
- -

3.1  Router

This module is the main router of XMPP packets on each node. It routes -them based on their destinations domains. It has two tables: local and global -routes. First, domain of packet destination searched in local table, and if it -found, then the packet is routed to appropriate process. If no, then it -searches in global table, and is routed to the appropriate ejabberd node or -process. If it does not exists in either tables, then it sent to the S2S -manager.

- -

3.2  Local Router

This module routes packets which have a destination domain equal to this server -name. If destination JID has a non-empty user part, then it routed to the -session manager, else it is processed depending on it’s content.

- -

3.3  Session Manager

This module routes packets to local users. It searches for what user resource -packet must be sent via presence table. If this resource is connected to -this node, it is routed to C2S process, if it connected via another node, then -the packet is sent to session manager on that node.

- -

3.4  S2S Manager

This module routes packets to other XMPP servers. First, it checks if an -open S2S connection from the domain of the packet source to the domain of -packet destination already exists. If it is open on another node, then it -routes the packet to S2S manager on that node, if it is open on this node, then -it is routed to the process that serves this connection, and if a connection -does not exist, then it is opened and registered.

- -

4  Authentication

- -

4.0.1  External

- -

The external authentication script follows -the erlang port driver API.

That script is supposed to do theses actions, in an infinite loop: -

  • -read from stdin: AABBBBBBBBB..... -
    • -A: 2 bytes of length data (a short in network byte order) -
    • B: a string of length found in A that contains operation in plain text -operation are as follows: -
      • -auth:User:Server:Password (check if a username/password pair is correct) -
      • isuser:User:Server (check if it’s a valid user) -
      • setpass:User:Server:Password (set user’s password) -
      • tryregister:User:Server:Password (try to register an account) -
      • removeuser:User:Server (remove this account) -
      • removeuser3:User:Server:Password (remove this account if the password is correct) -
      -
    -
  • write to stdout: AABB -
    • -A: the number 2 (coded as a short, which is bytes length of following result) -
    • B: the result code (coded as a short), should be 1 for success/valid, or 0 for failure/invalid -
    -

Example python script -

#!/usr/bin/python
-
-import sys
-from struct import *
-
-def from_ejabberd():
-    input_length = sys.stdin.read(2)
-    (size,) = unpack('>h', input_length)
-    return sys.stdin.read(size).split(':')
-
-def to_ejabberd(bool):
-    answer = 0
-    if bool:
-        answer = 1
-    token = pack('>hh', 2, answer)
-    sys.stdout.write(token)
-    sys.stdout.flush()
-
-def auth(username, server, password):
-    return True
-
-def isuser(username, server):
-    return True
-
-def setpass(username, server, password):
-    return True
-
-while True:
-    data = from_ejabberd()
-    success = False
-    if data[0] == "auth":
-        success = auth(data[1], data[2], data[3])
-    elif data[0] == "isuser":
-        success = isuser(data[1], data[2])
-    elif data[0] == "setpass":
-        success = setpass(data[1], data[2], data[3])
-    to_ejabberd(success)
-
- -

5  XML Representation

-

Each XML stanza is represented as the following tuple: -

XMLElement = {xmlelement, Name, Attrs, [ElementOrCDATA]}
-        Name = string()
-        Attrs = [Attr]
-        Attr = {Key, Val}
-        Key = string()
-        Val = string()
-        ElementOrCDATA = XMLElement | CDATA
-        CDATA = {xmlcdata, string()}
-

E. g. this stanza: -

<message to='test@conference.example.org' type='groupchat'>
-  <body>test</body>
-</message>
-

is represented as the following structure: -

{xmlelement, "message",
-    [{"to", "test@conference.example.org"},
-     {"type", "groupchat"}],
-    [{xmlelement, "body",
-         [],
-         [{xmlcdata, "test"}]}]}}
-
- -

6  Module xml

-

-
element_to_string(El) -> string() -
El = XMLElement
-
Returns string representation of XML stanza El.
crypt(S) -> string() -
S = string()
-
Returns string which correspond to S with encoded XML special -characters.
remove_cdata(ECList) -> EList -
ECList = [ElementOrCDATA]
-EList = [XMLElement]
-
EList is a list of all non-CDATA elements of ECList.
get_path_s(El, Path) -> Res -
El = XMLElement
-Path = [PathItem]
-PathItem = PathElem | PathAttr | PathCDATA
-PathElem = {elem, Name}
-PathAttr = {attr, Name}
-PathCDATA = cdata
-Name = string()
-Res = string() | XMLElement
-
If Path is empty, then returns El. Else sequentially -consider elements of Path. Each element is one of: -
-
{elem, Name} Name is name of subelement of -El, if such element exists, then this element considered in -following steps, else returns empty string. -
{attr, Name} If El have attribute Name, then -returns value of this attribute, else returns empty string. -
cdata Returns CDATA of El. -
TODO: -
         get_cdata/1, get_tag_cdata/1
-         get_attr/2, get_attr_s/2
-         get_tag_attr/2, get_tag_attr_s/2
-         get_subtag/2
-
- -

7  Module xml_stream

-

-
parse_element(Str) -> XMLElement | {error, Err} -
Str = string()
-Err = term()
-
Parses Str using XML parser, returns either parsed element or error -tuple. -
- -

8  Modules

-

- -

8.1  Module gen_iq_handler

-

The module gen_iq_handler allows to easily write handlers for IQ packets -of particular XML namespaces that addressed to server or to users bare JIDs.

In this module the following functions are defined: -

-
add_iq_handler(Component, Host, NS, Module, Function, Type) -
Component = Module = Function = atom()
-Host = NS = string()
-Type = no_queue | one_queue | parallel
-
Registers function Module:Function as handler for IQ packets on -virtual host Host that contain child of namespace NS in -Component. Queueing discipline is Type. There are at least -two components defined: -
-
ejabberd_local Handles packets that addressed to server JID; -
ejabberd_sm Handles packets that addressed to users bare JIDs. -
-
remove_iq_handler(Component, Host, NS) -
Component = atom()
-Host = NS = string()
-
Removes IQ handler on virtual host Host for namespace NS from -Component. -

Handler function must have the following type: -

-
Module:Function(From, To, IQ) -
From = To = jid()
-
-module(mod_cputime).
-
--behaviour(gen_mod).
-
--export([start/2,
-         stop/1,
-         process_local_iq/3]).
-
--include("ejabberd.hrl").
--include("jlib.hrl").
-
--define(NS_CPUTIME, "ejabberd:cputime").
-
-start(Host, Opts) ->
-    IQDisc = gen_mod:get_opt(iqdisc, Opts, one_queue),
-    gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_CPUTIME,
-                                  ?MODULE, process_local_iq, IQDisc).
-
-stop(Host) ->
-    gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_CPUTIME).
-
-process_local_iq(From, To, {iq, ID, Type, XMLNS, SubEl}) ->
-    case Type of
-        set ->
-            {iq, ID, error, XMLNS,
-             [SubEl, ?ERR_NOT_ALLOWED]};
-        get ->
-            CPUTime = element(1, erlang:statistics(runtime))/1000,
-            SCPUTime = lists:flatten(io_lib:format("~.3f", CPUTime)),
-            {iq, ID, result, XMLNS,
-             [{xmlelement, "query",
-               [{"xmlns", ?NS_CPUTIME}],
-               [{xmlelement, "cputime", [], [{xmlcdata, SCPUTime}]}]}]}
-    end.
-
- -

8.2  Services

-

-module(mod_echo).
-
--behaviour(gen_mod).
-
--export([start/2, init/1, stop/1]).
-
--include("ejabberd.hrl").
--include("jlib.hrl").
-
-start(Host, Opts) ->
-    MyHost = gen_mod:get_opt(host, Opts, "echo." ++ Host),
-    register(gen_mod:get_module_proc(Host, ?PROCNAME),
-             spawn(?MODULE, init, [MyHost])).
-
-init(Host) ->
-    ejabberd_router:register_local_route(Host),
-    loop(Host).
-
-loop(Host) ->
-    receive
-        {route, From, To, Packet} ->
-            ejabberd_router:route(To, From, Packet),
-            loop(Host);
-        stop ->
-            ejabberd_router:unregister_route(Host),
-            ok;
-        _ ->
-            loop(Host)
-    end.
-
-stop(Host) ->
-    Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
-    Proc ! stop,
-    {wait, Proc}.
-
- - - -
This document was translated from LATEX by -HEVEA.
- diff --git a/doc/dev.tex b/doc/dev.tex deleted file mode 100644 index 4e63b95ce..000000000 --- a/doc/dev.tex +++ /dev/null @@ -1,470 +0,0 @@ -\documentclass[a4paper,10pt]{article} - -%% Packages -\usepackage{graphics} -\usepackage{hevea} -\usepackage{makeidx} -\usepackage{verbatim} - -%% Index -\makeindex -% Remove the index anchors from the HTML version to save size and bandwith. -\newcommand{\ind}[1]{\begin{latexonly}\index{#1}\end{latexonly}} - -%% Images -\newcommand{\logoscale}{0.7} -\newcommand{\imgscale}{0.58} -\newcommand{\insimg}[1]{\insscaleimg{\imgscale}{#1}} -\newcommand{\insscaleimg}[2]{ - \imgsrc{#2}{} - \begin{latexonly} - \scalebox{#1}{\includegraphics{#2}} - \end{latexonly} -} - -%% Various -\newcommand{\ns}[1]{\texttt{#1}} -\newcommand{\ejabberd}{\texttt{ejabberd}} -\newcommand{\Jabber}{Jabber} -\newcommand{\XMPP}{XMPP} - -%% Modules -\newcommand{\module}[1]{\texttt{#1}} -\newcommand{\modadhoc}{\module{mod\_adhoc}} -\newcommand{\modannounce}{\module{mod\_announce}} -\newcommand{\modconfigure}{\module{mod\_configure}} -\newcommand{\moddisco}{\module{mod\_disco}} -\newcommand{\modecho}{\module{mod\_echo}} -\newcommand{\modirc}{\module{mod\_irc}} -\newcommand{\modlast}{\module{mod\_last}} -\newcommand{\modlastodbc}{\module{mod\_last\_odbc}} -\newcommand{\modmuc}{\module{mod\_muc}} -\newcommand{\modmuclog}{\module{mod\_muc\_log}} -\newcommand{\modoffline}{\module{mod\_offline}} -\newcommand{\modofflineodbc}{\module{mod\_offline\_odbc}} -\newcommand{\modprivacy}{\module{mod\_privacy}} -\newcommand{\modprivate}{\module{mod\_private}} -\newcommand{\modpubsub}{\module{mod\_pubsub}} -\newcommand{\modregister}{\module{mod\_register}} -\newcommand{\modroster}{\module{mod\_roster}} -\newcommand{\modrosterodbc}{\module{mod\_roster\_odbc}} -\newcommand{\modservicelog}{\module{mod\_service\_log}} -\newcommand{\modsharedroster}{\module{mod\_shared\_roster}} -\newcommand{\modstats}{\module{mod\_stats}} -\newcommand{\modtime}{\module{mod\_time}} -\newcommand{\modvcard}{\module{mod\_vcard}} -\newcommand{\modvcardldap}{\module{mod\_vcard\_ldap}} -\newcommand{\modvcardodbc}{\module{mod\_vcard\_odbc}} -\newcommand{\modversion}{\module{mod\_version}} - -%% Title page -\include{version} -\title{Ejabberd \version\ Developers Guide} -\author{Alexey Shchepin \\ - \ahrefurl{mailto:alexey@sevcom.net} \\ - \ahrefurl{xmpp:aleksey@jabber.ru}} - -%% Options -\newcommand{\marking}[1]{#1} % Marking disabled -\newcommand{\quoting}[2][yozhik]{} % Quotes disabled -\newcommand{\new}{\begin{latexonly}\marginpar{\textsc{new}}\end{latexonly}} % Highlight new features -\newcommand{\improved}{\begin{latexonly}\marginpar{\textsc{improved}}\end{latexonly}} % Highlight improved features -\newcommand{\moreinfo}[1]{} % Hide details - -%% Footnotes -\newcommand{\txepref}[2]{\footahref{http://www.xmpp.org/extensions/xep-#1.html}{#2}} -\newcommand{\xepref}[1]{\txepref{#1}{XEP-#1}} - -\begin{document} - -\label{titlepage} -\begin{titlepage} - \maketitle{} - - \begin{center} - {\insscaleimg{\logoscale}{logo.png} - \par - } - \end{center} - - \begin{quotation}\textit{I can thoroughly recommend ejabberd for ease of setup -- - Kevin Smith, Current maintainer of the Psi project}\end{quotation} - -\end{titlepage} - -\tableofcontents{} - -% Input introduction.tex -\input{introduction} - -\section{How it Works} -\label{howitworks} - - -A \XMPP{} domain is served by one or more \ejabberd{} nodes. These nodes can -be run on different machines that are connected via a network. They all must -have the ability to connect to port 4369 of all another nodes, and must have -the same magic cookie (see Erlang/OTP documentation, in other words the file -\texttt{\~{}ejabberd/.erlang.cookie} must be the same on all nodes). This is -needed because all nodes exchange information about connected users, S2S -connections, registered services, etc\ldots - - - -Each \ejabberd{} node have following modules: -\begin{itemize} -\item router; -\item local router. -\item session manager; -\item S2S manager; -\end{itemize} - - -\subsection{Router} - -This module is the main router of \XMPP{} packets on each node. It routes -them based on their destinations domains. It has two tables: local and global -routes. First, domain of packet destination searched in local table, and if it -found, then the packet is routed to appropriate process. If no, then it -searches in global table, and is routed to the appropriate \ejabberd{} node or -process. If it does not exists in either tables, then it sent to the S2S -manager. - - -\subsection{Local Router} - -This module routes packets which have a destination domain equal to this server -name. If destination JID has a non-empty user part, then it routed to the -session manager, else it is processed depending on it's content. - - -\subsection{Session Manager} - -This module routes packets to local users. It searches for what user resource -packet must be sent via presence table. If this resource is connected to -this node, it is routed to C2S process, if it connected via another node, then -the packet is sent to session manager on that node. - - -\subsection{S2S Manager} - -This module routes packets to other \XMPP{} servers. First, it checks if an -open S2S connection from the domain of the packet source to the domain of -packet destination already exists. If it is open on another node, then it -routes the packet to S2S manager on that node, if it is open on this node, then -it is routed to the process that serves this connection, and if a connection -does not exist, then it is opened and registered. - - -\section{Authentication} - -\subsubsection{External} -\label{externalauth} -\ind{external authentication} - -The external authentication script follows -\footahref{http://www.erlang.org/doc/tutorial/c_portdriver.html}{the erlang port driver API}. - -That script is supposed to do theses actions, in an infinite loop: -\begin{itemize} -\item read from stdin: AABBBBBBBBB..... - \begin{itemize} - \item A: 2 bytes of length data (a short in network byte order) - \item B: a string of length found in A that contains operation in plain text - operation are as follows: - \begin{itemize} - \item auth:User:Server:Password (check if a username/password pair is correct) - \item isuser:User:Server (check if it's a valid user) - \item setpass:User:Server:Password (set user's password) - \item tryregister:User:Server:Password (try to register an account) - \item removeuser:User:Server (remove this account) - \item removeuser3:User:Server:Password (remove this account if the password is correct) - \end{itemize} - \end{itemize} -\item write to stdout: AABB - \begin{itemize} - \item A: the number 2 (coded as a short, which is bytes length of following result) - \item B: the result code (coded as a short), should be 1 for success/valid, or 0 for failure/invalid - \end{itemize} -\end{itemize} - -Example python script -\begin{verbatim} -#!/usr/bin/python - -import sys -from struct import * - -def from_ejabberd(): - input_length = sys.stdin.read(2) - (size,) = unpack('>h', input_length) - return sys.stdin.read(size).split(':') - -def to_ejabberd(bool): - answer = 0 - if bool: - answer = 1 - token = pack('>hh', 2, answer) - sys.stdout.write(token) - sys.stdout.flush() - -def auth(username, server, password): - return True - -def isuser(username, server): - return True - -def setpass(username, server, password): - return True - -while True: - data = from_ejabberd() - success = False - if data[0] == "auth": - success = auth(data[1], data[2], data[3]) - elif data[0] == "isuser": - success = isuser(data[1], data[2]) - elif data[0] == "setpass": - success = setpass(data[1], data[2], data[3]) - to_ejabberd(success) -\end{verbatim} - -\section{XML Representation} -\label{xmlrepr} - -Each XML stanza is represented as the following tuple: -\begin{verbatim} -XMLElement = {xmlelement, Name, Attrs, [ElementOrCDATA]} - Name = string() - Attrs = [Attr] - Attr = {Key, Val} - Key = string() - Val = string() - ElementOrCDATA = XMLElement | CDATA - CDATA = {xmlcdata, string()} -\end{verbatim} -E.\,g. this stanza: -\begin{verbatim} - - test - -\end{verbatim} -is represented as the following structure: -\begin{verbatim} -{xmlelement, "message", - [{"to", "test@conference.example.org"}, - {"type", "groupchat"}], - [{xmlelement, "body", - [], - [{xmlcdata, "test"}]}]}} -\end{verbatim} - - - -\section{Module \texttt{xml}} -\label{xmlmod} - -\begin{description} -\item{\verb|element_to_string(El) -> string()|} -\begin{verbatim} -El = XMLElement -\end{verbatim} - Returns string representation of XML stanza \texttt{El}. - -\item{\verb|crypt(S) -> string()|} -\begin{verbatim} -S = string() -\end{verbatim} - Returns string which correspond to \texttt{S} with encoded XML special - characters. - -\item{\verb|remove_cdata(ECList) -> EList|} -\begin{verbatim} -ECList = [ElementOrCDATA] -EList = [XMLElement] -\end{verbatim} - \texttt{EList} is a list of all non-CDATA elements of ECList. - - - -\item{\verb|get_path_s(El, Path) -> Res|} -\begin{verbatim} -El = XMLElement -Path = [PathItem] -PathItem = PathElem | PathAttr | PathCDATA -PathElem = {elem, Name} -PathAttr = {attr, Name} -PathCDATA = cdata -Name = string() -Res = string() | XMLElement -\end{verbatim} - If \texttt{Path} is empty, then returns \texttt{El}. Else sequentially - consider elements of \texttt{Path}. Each element is one of: - \begin{description} - \item{\verb|{elem, Name}|} \texttt{Name} is name of subelement of - \texttt{El}, if such element exists, then this element considered in - following steps, else returns empty string. - \item{\verb|{attr, Name}|} If \texttt{El} have attribute \texttt{Name}, then - returns value of this attribute, else returns empty string. - \item{\verb|cdata|} Returns CDATA of \texttt{El}. - \end{description} - -\item{TODO:} -\begin{verbatim} - get_cdata/1, get_tag_cdata/1 - get_attr/2, get_attr_s/2 - get_tag_attr/2, get_tag_attr_s/2 - get_subtag/2 -\end{verbatim} -\end{description} - - -\section{Module \texttt{xml\_stream}} -\label{xmlstreammod} - -\begin{description} -\item{\verb!parse_element(Str) -> XMLElement | {error, Err}!} -\begin{verbatim} -Str = string() -Err = term() -\end{verbatim} - Parses \texttt{Str} using XML parser, returns either parsed element or error - tuple. -\end{description} - - -\section{Modules} -\label{emods} - - -%\subsection{gen\_mod behaviour} -%\label{genmod} - -%TBD - -\subsection{Module gen\_iq\_handler} -\label{geniqhandl} - -The module \verb|gen_iq_handler| allows to easily write handlers for IQ packets -of particular XML namespaces that addressed to server or to users bare JIDs. - -In this module the following functions are defined: -\begin{description} -\item{\verb|add_iq_handler(Component, Host, NS, Module, Function, Type)|} -\begin{verbatim} -Component = Module = Function = atom() -Host = NS = string() -Type = no_queue | one_queue | parallel -\end{verbatim} - Registers function \verb|Module:Function| as handler for IQ packets on - virtual host \verb|Host| that contain child of namespace \verb|NS| in - \verb|Component|. Queueing discipline is \verb|Type|. There are at least - two components defined: - \begin{description} - \item{\verb|ejabberd_local|} Handles packets that addressed to server JID; - \item{\verb|ejabberd_sm|} Handles packets that addressed to users bare JIDs. - \end{description} -\item{\verb|remove_iq_handler(Component, Host, NS)|} -\begin{verbatim} -Component = atom() -Host = NS = string() -\end{verbatim} - Removes IQ handler on virtual host \verb|Host| for namespace \verb|NS| from - \verb|Component|. -\end{description} - -Handler function must have the following type: -\begin{description} -\item{\verb|Module:Function(From, To, IQ)|} -\begin{verbatim} -From = To = jid() -\end{verbatim} -\end{description} - - - -\begin{verbatim} --module(mod_cputime). - --behaviour(gen_mod). - --export([start/2, - stop/1, - process_local_iq/3]). - --include("ejabberd.hrl"). --include("jlib.hrl"). - --define(NS_CPUTIME, "ejabberd:cputime"). - -start(Host, Opts) -> - IQDisc = gen_mod:get_opt(iqdisc, Opts, one_queue), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_CPUTIME, - ?MODULE, process_local_iq, IQDisc). - -stop(Host) -> - gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_CPUTIME). - -process_local_iq(From, To, {iq, ID, Type, XMLNS, SubEl}) -> - case Type of - set -> - {iq, ID, error, XMLNS, - [SubEl, ?ERR_NOT_ALLOWED]}; - get -> - CPUTime = element(1, erlang:statistics(runtime))/1000, - SCPUTime = lists:flatten(io_lib:format("~.3f", CPUTime)), - {iq, ID, result, XMLNS, - [{xmlelement, "query", - [{"xmlns", ?NS_CPUTIME}], - [{xmlelement, "cputime", [], [{xmlcdata, SCPUTime}]}]}]} - end. -\end{verbatim} - - -\subsection{Services} -\label{services} - -%TBD - - -%TODO: use \verb|proc_lib| -\begin{verbatim} --module(mod_echo). - --behaviour(gen_mod). - --export([start/2, init/1, stop/1]). - --include("ejabberd.hrl"). --include("jlib.hrl"). - -start(Host, Opts) -> - MyHost = gen_mod:get_opt(host, Opts, "echo." ++ Host), - register(gen_mod:get_module_proc(Host, ?PROCNAME), - spawn(?MODULE, init, [MyHost])). - -init(Host) -> - ejabberd_router:register_local_route(Host), - loop(Host). - -loop(Host) -> - receive - {route, From, To, Packet} -> - ejabberd_router:route(To, From, Packet), - loop(Host); - stop -> - ejabberd_router:unregister_route(Host), - ok; - _ -> - loop(Host) - end. - -stop(Host) -> - Proc = gen_mod:get_module_proc(Host, ?PROCNAME), - Proc ! stop, - {wait, Proc}. -\end{verbatim} - - - -\end{document} diff --git a/doc/discorus.png b/doc/discorus.png deleted file mode 100644 index 982f88182..000000000 Binary files a/doc/discorus.png and /dev/null differ diff --git a/doc/features.html b/doc/features.html deleted file mode 100644 index f922a1cca..000000000 --- a/doc/features.html +++ /dev/null @@ -1,143 +0,0 @@ - - - - - - -Ejabberd community 14.05-120-gedfb5fc Feature Sheet - - - - -

- -

-

Ejabberd community 14.05-120-gedfb5fc Feature Sheet

Sander Devrieze
- mailto:s.devrieze@pandora.be
- xmpp:sander@devrieze.dyndns.org

- -logo.png - - -
I can thoroughly recommend ejabberd for ease of setup – -Kevin Smith, Current maintainer of the Psi project

Introduction -

I just tried out ejabberd and was impressed both by ejabberd itself and the language it is written in, Erlang. — -Joeri

ejabberd is a free and open source instant messaging server written in Erlang/OTP.

ejabberd is cross-platform, distributed, fault-tolerant, and based on open standards to achieve real-time communication.

ejabberd is designed to be a rock-solid and feature rich XMPP server.

ejabberd is suitable for small deployments, whether they need to be scalable or not, as well as extremely big deployments.

- -

Key Features

- -

Erlang seems to be tailor-made for writing stable, robust servers. — -Peter Saint-André, Executive Director of the Jabber Software Foundation

ejabberd is: -

  • -Cross-platform: ejabberd runs under Microsoft Windows and Unix derived systems such as Linux, FreeBSD and NetBSD.
  • Distributed: You can run ejabberd on a cluster of machines and all of them will serve the same Jabber domain(s). When you need more capacity you can simply add a new cheap node to your cluster. Accordingly, you do not need to buy an expensive high-end machine to support tens of thousands concurrent users.
  • Fault-tolerant: You can deploy an ejabberd cluster so that all the information required for a properly working service will be replicated permanently on all nodes. This means that if one of the nodes crashes, the others will continue working without disruption. In addition, nodes also can be added or replaced ‘on the fly’.
  • Administrator Friendly: ejabberd is built on top of the Open Source Erlang. As a result you do not need to install an external database, an external web server, amongst others because everything is already included, and ready to run out of the box. Other administrator benefits include: -
    • -Comprehensive documentation. -
    • Straightforward installers for Linux, Mac OS X, and Windows.
    • Web Administration. -
    • Shared Roster Groups. -
    • Command line administration tool.
    • Can integrate with existing authentication mechanisms. -
    • Capability to send announce messages. -
  • Internationalized: ejabberd leads in internationalization. Hence it is very well suited in a globalized world. Related features are: -
    • -Translated to 25 languages.
    • Support for IDNA. -
  • Open Standards: ejabberd is the first Open Source Jabber server claiming to fully comply to the XMPP standard. -
- -

Additional Features

- -

ejabberd is making inroads to solving the "buggy incomplete server" problem — -Justin Karneges, Founder of the Psi and the Delta projects

Moreover, ejabberd comes with a wide range of other state-of-the-art features: -

  • -Modular -
    • -Load only the modules you want. -
    • Extend ejabberd with your own custom modules. -
    -
  • Security -
    • -SASL and STARTTLS for c2s and s2s connections. -
    • STARTTLS and Dialback s2s connections. -
    • Web Admin accessible via HTTPS secure access. -
    -
  • Databases -
    • -Internal database for fast deployment (Mnesia). -
    • Native MySQL support. -
    • Native PostgreSQL support. -
    • ODBC data storage support. -
    • Microsoft SQL Server support.
    • Riak NoSQL database support. -
    -
  • Authentication -
    • -Internal Authentication. -
    • PAM, LDAP, ODBC and Riak.
    • External Authentication script. -
    -
  • Others -
    • -Support for virtual hosting. -
    • Compressing XML streams with Stream Compression (XEP-0138). -
    • Statistics via Statistics Gathering (XEP-0039). -
    • IPv6 support both for c2s and s2s connections. -
    • Multi-User Chat module with support for clustering and HTML logging.
    • Users Directory based on users vCards. -
    • Publish-Subscribe component with support for Personal Eventing via Pubsub. -
    • Support for web clients: HTTP Polling and HTTP Binding (BOSH) services. -
    • IRC transport. -
    • SIP support. -
    • Component support: interface with networks such as AIM, ICQ and MSN installing special tranports. -
    -
- - - -
This document was translated from LATEX by -HEVEA.
- diff --git a/doc/features.tex b/doc/features.tex deleted file mode 100644 index 1a512571f..000000000 --- a/doc/features.tex +++ /dev/null @@ -1,136 +0,0 @@ -\documentclass[a4paper,10pt]{article} - -%% Packages -\usepackage{epsfig} -\usepackage{fancyhdr} -\usepackage{graphics} -\usepackage{hevea} -\usepackage[pdftex,colorlinks,unicode,urlcolor=blue,linkcolor=blue,pdftitle=Ejabberd\ - Feature\ Sheet,pdfauthor=Sander\ - Devrieze,pdfsubject=ejabberd,pdfkeywords=ejabberd]{hyperref} -\usepackage{verbatim} -\usepackage{color} - -%% Index -% Remove the index anchors from the HTML version to save size and bandwith. -\newcommand{\ind}[1]{\begin{latexonly}\index{#1}\end{latexonly}} - -%% Images -\newcommand{\logoscale}{0.7} -\newcommand{\imgscale}{0.58} -\newcommand{\insimg}[1]{\insscaleimg{\imgscale}{#1}} -\newcommand{\insscaleimg}[2]{ - \imgsrc{#2}{} - \begin{latexonly} - \scalebox{#1}{\includegraphics{#2}} - \end{latexonly} -} - -%% Various -\newcommand{\bracehack}{\def\{{\char"7B}\def\}{\char"7D}} -\newcommand{\titem}[1]{\item[\bracehack\texttt{#1}]} -\newcommand{\ns}[1]{\texttt{#1}} -\newcommand{\jid}[1]{\texttt{#1}} -\newcommand{\option}[1]{\texttt{#1}} -\newcommand{\poption}[1]{{\bracehack\texttt{#1}}} -\newcommand{\node}[1]{\texttt{#1}} -\newcommand{\term}[1]{\texttt{#1}} -\newcommand{\shell}[1]{\texttt{#1}} -\newcommand{\ejabberd}{\texttt{ejabberd}} -\newcommand{\Jabber}{Jabber} - -%% Title page -\include{version} -\title{Ejabberd \version\ Feature Sheet} -\author{Sander Devrieze \\ - \ahrefurl{mailto:s.devrieze@pandora.be} \\ - \ahrefurl{xmpp:sander@devrieze.dyndns.org}} - -% Options -\newcommand{\marking}[1]{\textbf{\begin{large}\textcolor{ejblue}{#1}\end{large}}} % Marking enabled -\newcommand{\quoting}[2][yozhik]{\begin{quotation}\textcolor{#1}{\textit{#2}}\end{quotation}} % Quotes enabled -\newcommand{\new}{\marginpar{\textcolor{red}{\textsc{new}}}} % Highlight new features -\newcommand{\improved}{\marginpar{\textcolor{orange}{\textsc{improved}}}} % Highlight improved features -\setcounter{secnumdepth}{-1} % Disable section numbering - -%% To by-pass errors in the HTML version. -\newstyle{SPAN}{width:20\%; float:right; text-align:left; margin-left:auto;} -\definecolor{orange} {cmyk}{0.000,0.333,1.000,0.000} - -%% Footnotes -\begin{latexonly} -\global\parskip=9pt plus 3pt minus 1pt -\global\parindent=0pt -\gdef\ahrefurl#1{\href{#1}{\texttt{#1}}} -\gdef\footahref#1#2{#2\footnote{\href{#1}{\texttt{#1}}}} -\end{latexonly} -\newcommand{\txepref}[2]{\footahref{http://www.xmpp.org/extensions/xep-#1.html}{#2}} -\newcommand{\xepref}[1]{\txepref{#1}{XEP-#1}} - -%% Fancy header -\fancyhf{} -\pagestyle{fancy} -\rhead{\textcolor{ejblue}{The Expandable Jabber/XMPP Daemon.}} -\renewcommand{\headrule}{{\color{ejblue}% -\hrule width\headwidth height\headrulewidth \vskip-\headrulewidth}} -\lhead{\setlength{\unitlength}{-6mm} - \begin{picture}(0,0) - \put(5.8,3.25){\includegraphics[width=1.3\textwidth]{yozhikheader.png}} - \end{picture}} - -% Official ejabberd colours -\definecolor{ejblue} {cmyk}{1.000,0.831,0.000,0.537} %RGB: 0,0,118 HTML: 000076 -\definecolor{ejgreenyellow}{cmyk}{0.079,0.000,0.275,0.102} %RGB: 209,229,159 HTML: d1e59f -\definecolor{ejgreendark} {cmyk}{0.131,0.000,0.146,0.220} %RGB: 166,199,162 HTML: a6c7a2 -\definecolor{ejgreen} {cmyk}{0.077,0.000,0.081,0.078} %RGB: 216,236,215 HTML: d8ecd7 -\definecolor{ejgreenwhite} {cmyk}{0.044,0.000,0.048,0.020} %RGB: 239,250,238 HTML: effaee -\definecolor{yozhik} {cmyk}{0.000,0.837,1.000,0.424} %RGB: 147,0,0 HTML: 930000 - -\begin{document} - -\label{titlepage} -\begin{titlepage} - \maketitle{} - - \thispagestyle{empty} - - \begin{center} - {\insscaleimg{\logoscale}{logo.png} - \par - } - \end{center} - \quoting{I can thoroughly recommend ejabberd for ease of setup -- - Kevin Smith, Current maintainer of the Psi project} - -\end{titlepage} - -\newpage -% Set the page counter to 2 so that the titlepage and the second page do not -% have the same page number. This fixes the PDFLaTeX warning "destination with -% the same identifier". -\begin{latexonly} -\setcounter{page}{2} -\pagecolor{ejgreenwhite} -\end{latexonly} - -% Input introduction.tex -\input{introduction} - -\end{document} - -%% TODO -% * illustrations (e.g. screenshot from web interface) -% * commented parts -% * slides, guide and html version -% * cleaning and improving LaTeX code -% * key features: something like this (shorter)? (more focussed on Erlang now): "To reach the goal of high -% availability, performance and clustering, ejabberd is written in Erlang, a programming language perfectly -% suited for this. Besides that, some parts are written in C to also incude the advantages of this language. In -% short, ejabberd is a perfect mix of mainly Erlang code, peppered with some C code to get the final touch!" -% -% * key features: saying that ejabberd the only XMPP server is that can do real clustering: -% http://www.jivesoftware.org/forums/thread.jspa?threadID=14602 -% "What I find interesting is that *no* XMPP servers truly provide clustering. This includes all the commercial -% servers. The one partial exception appears to be ejabberd, which can cluster certain data such as sessions, -% but not all services such as MUC." -% * try it today: links to migration tutorials diff --git a/doc/flow.dot b/doc/flow.dot deleted file mode 100644 index b1a8affb8..000000000 --- a/doc/flow.dot +++ /dev/null @@ -1,105 +0,0 @@ -digraph messages { - //concentrate=true; - subgraph clusterclients { - client1 [shape = box]; - client2 [shape = box]; - client3 [shape = box]; - - style = dashed; - label = "Clients"; - } - - subgraph clusternode1 { - subgraph clusterc2s1 { - c2s11; - c2s12; - style = invis; - } - subgraph clusterservices1 { - service11; - service12; - service13; - style = invis; - } - //subgraph clusters2s1 { - //s2s11; - //s2s12; - //style = invis; - //} - c2s11 -> auth1; - c2s12 -> auth1; - auth1 -> c2s11; - auth1 -> c2s12; - c2s11 -> sm1; - c2s11 -> router1; - c2s12 -> sm1; - c2s12 -> router1; - router1 -> local1; - router1 -> service11; - router1 -> service12; - router1 -> service13; - router1 -> s2s11; - router1 -> s2s12; - service11 -> router1; - service12 -> router1; - service13 -> router1; - s2s11 -> router1; - s2s12 -> router1; - local1 -> sm1; - sm1 -> c2s11; - sm1 -> c2s12; - - style = dashed; - label = "Node1"; - } - - subgraph clusternode2 { - c2s2 -> auth2; - auth2 -> c2s2; - c2s2 -> sm2; - c2s2 -> router2; - router2 -> local2; - router2 -> service21; - router2 -> s2s21; - service21 -> router2; - s2s21 -> router2; - local2 -> sm2; - sm2 -> c2s2; - - style = dashed; - label = "Node2"; - } - - - - subgraph clusterservers { - server1 [shape = box]; - server2 [shape = box]; - server3 [shape = box]; - - style = dashed; - label = "Servers"; - } - - - client1 -> c2s11; - client2 -> c2s12; - client3 -> c2s2; - c2s11 -> client1 [constraint=false]; - c2s12 -> client2 [constraint=false]; - c2s2 -> client3 [constraint=false]; - - s2s11 -> server1 [minlen = 2]; - s2s12 -> server2 [minlen = 2]; - s2s21 -> server3 [minlen = 2]; - server1 -> s2s11 [constraint=false]; - server2 -> s2s12 [constraint=false]; - server3 -> s2s21 [constraint=false]; - - router1 -> router2; - router2 -> router1; - sm1 -> sm2; - sm2 -> sm1; - - label = "Data Flows"; -} diff --git a/doc/guide.tex b/doc/guide.tex deleted file mode 100644 index 6237fe346..000000000 --- a/doc/guide.tex +++ /dev/null @@ -1,6506 +0,0 @@ -\documentclass[a4paper,10pt]{book} - -%% Packages -\usepackage{float} -\usepackage{graphics} -\usepackage{hevea} -\usepackage[pdftex,colorlinks,unicode,urlcolor=blue,linkcolor=blue, - pdftitle=Ejabberd\ Installation\ and\ Operation\ Guide,pdfauthor=ProcessOne,pdfsubject=ejabberd,pdfkeywords=ejabberd, - pdfpagelabels=false]{hyperref} -\usepackage{makeidx} -%\usepackage{showidx} % Only for verifying the index entries. -\usepackage{verbatim} -\usepackage{geometry} -\usepackage{fancyhdr} - -\pagestyle{fancy} %Forces the page to use the fancy template -\renewcommand{\chaptermark}[1]{\markboth{\textbf{\thechapter}.\ \emph{#1}}{}} -\renewcommand{\sectionmark}[1]{\markright{\thesection\ \boldmath\textbf{#1}\unboldmath}} -\fancyhf{} -\fancyhead[LE,RO]{\textbf{\thepage}} %Displays the page number in bold in the header, - % to the left on even pages and to the right on odd pages. -\fancyhead[RE]{\nouppercase{\leftmark}} %Displays the upper-level (chapter) information--- - % as determined above---in non-upper case in the header, to the right on even pages. -\fancyhead[LO]{\rightmark} %Displays the lower-level (section) information---as - % determined above---in the header, to the left on odd pages. -\renewcommand{\headrulewidth}{0.5pt} %Underlines the header. (Set to 0pt if not required). -\renewcommand{\footrulewidth}{0.5pt} %Underlines the footer. (Set to 0pt if not required). - -%% Index -\makeindex -% Remove the index anchors from the HTML version to save size and bandwith. -\newcommand{\ind}[1]{\begin{latexonly}\index{#1}\end{latexonly}} -\newcommand{\makechapter}[2]{ \aname{#1}{} \chapter{\ahrefloc{#1}{#2}} \label{#1} } -\newcommand{\makesection}[2]{ \aname{#1}{} \section{\ahrefloc{#1}{#2}} \label{#1} } -\newcommand{\makesubsection}[2]{ \aname{#1}{} \subsection{\ahrefloc{#1}{#2}} \label{#1} } -\newcommand{\makesubsubsection}[2]{ \aname{#1}{} \subsubsection{\ahrefloc{#1}{#2}} \label{#1} } -\newcommand{\makeparagraph}[2]{ \aname{#1}{} \paragraph{\ahrefloc{#1}{#2}} \label{#1} } - -%% Images -\newcommand{\logoscale}{0.7} -\newcommand{\imgscale}{0.58} -\newcommand{\insimg}[1]{\insscaleimg{\imgscale}{#1}} -\newcommand{\insscaleimg}[2]{ - \imgsrc{#2}{} - \begin{latexonly} - \scalebox{#1}{\includegraphics{#2}} - \end{latexonly} -} - -%% Various -\newcommand{\bracehack}{\def\{{\char"7B}\def\}{\char"7D}} -\newcommand{\titem}[1]{\item[\bracehack\texttt{#1}]} -\newcommand{\ns}[1]{\texttt{#1}} -\newcommand{\jid}[1]{\texttt{#1}} -\newcommand{\option}[1]{\texttt{#1}} -\newcommand{\poption}[1]{{\bracehack\texttt{#1}}} -\newcommand{\node}[1]{\texttt{#1}} -\newcommand{\term}[1]{\texttt{#1}} -\newcommand{\shell}[1]{\texttt{#1}} -\newcommand{\ejabberd}{\texttt{ejabberd}} -\newcommand{\Jabber}{Jabber} -\newcommand{\XMPP}{XMPP} -\newcommand{\esyntax}[1]{\begin{description}\titem{#1}\end{description}} - -%% Modules -\newcommand{\module}[1]{\texttt{#1}} -\newcommand{\modadhoc}{\module{mod\_adhoc}} -\newcommand{\modannounce}{\module{mod\_announce}} -\newcommand{\modclientstate}{\module{mod\_client\_state}} -\newcommand{\modblocking}{\module{mod\_blocking}} -\newcommand{\modcaps}{\module{mod\_caps}} -\newcommand{\modcarboncopy}{\module{mod\_carboncopy}} -\newcommand{\modconfigure}{\module{mod\_configure}} -\newcommand{\moddisco}{\module{mod\_disco}} -\newcommand{\modecho}{\module{mod\_echo}} -\newcommand{\modfailban}{\module{mod\_fail2ban}} -\newcommand{\modhttpbind}{\module{mod\_http\_bind}} -\newcommand{\modhttpfileserver}{\module{mod\_http\_fileserver}} -\newcommand{\modirc}{\module{mod\_irc}} -\newcommand{\modlast}{\module{mod\_last}} -\newcommand{\modmuc}{\module{mod\_muc}} -\newcommand{\modmuclog}{\module{mod\_muc\_log}} -\newcommand{\modoffline}{\module{mod\_offline}} -\newcommand{\modping}{\module{mod\_ping}} -\newcommand{\modprescounter}{\module{mod\_pres\_counter}} -\newcommand{\modprivacy}{\module{mod\_privacy}} -\newcommand{\modprivate}{\module{mod\_private}} -\newcommand{\modproxy}{\module{mod\_proxy65}} -\newcommand{\modpubsub}{\module{mod\_pubsub}} -\newcommand{\modpubsubodbc}{\module{mod\_pubsub\_odbc}} -\newcommand{\modregister}{\module{mod\_register}} -\newcommand{\modregisterweb}{\module{mod\_register\_web}} -\newcommand{\modroster}{\module{mod\_roster}} -\newcommand{\modservicelog}{\module{mod\_service\_log}} -\newcommand{\modsharedroster}{\module{mod\_shared\_roster}} -\newcommand{\modsharedrosterldap}{\module{mod\_shared\_roster\_ldap}} -\newcommand{\modsic}{\module{mod\_sic}} -\newcommand{\modsip}{\module{mod\_sip}} -\newcommand{\modstats}{\module{mod\_stats}} -\newcommand{\modtime}{\module{mod\_time}} -\newcommand{\modvcard}{\module{mod\_vcard}} -\newcommand{\modvcardldap}{\module{mod\_vcard\_ldap}} -\newcommand{\modvcardxupdate}{\module{mod\_vcard\_xupdate}} -\newcommand{\modversion}{\module{mod\_version}} - -%% Contributed modules -%\usepackage{ifthen} -%\newboolean{modhttpbind} -%\newcommand{\modhttpbind}{\module{mod\_http\_bind}} -%\include{contributed_modules} -% -% Then in the document you can input the partial tex file with: -%\ifthenelse{\boolean{modhttpbind}}{\input{mod_http_bind.tex}}{} - -%% Common options -\newcommand{\iqdiscitem}[1]{\titem{iqdisc: Discipline} \ind{options!iqdisc}This specifies -the processing discipline for #1 IQ queries (see section~\ref{modiqdiscoption}).} -\newcommand{\hostitem}[1]{ - \titem{host: HostName} \ind{options!host} This option defines the Jabber ID of the - service. If the \texttt{host} option is not specified, the Jabber ID will be the - hostname of the virtual host with the prefix `\jid{#1.}'. The keyword "@HOST@" - is replaced at start time with the real virtual host name. -} -\newcommand{\dbtype}{\titem{db\_type: internal|odbc} \ind{options!dbtype} - Define the type of storage where the module will create the tables and store user information. - The default is to store in the internal Mnesia database. - If \term{odbc} value is defined, make sure you have defined the database, see~\ref{database}. -} - -%% Title page -\include{version} -\newlength{\larg} -\setlength{\larg}{14.5cm} -\title{ -{\rule{\larg}{1mm}}\vspace{7mm} -\begin{tabular}{r} - {\huge {\bf ejabberd \version\ }} \\ - \\ - {\huge Installation and Operation Guide} -\end{tabular}\\ -\vspace{2mm} -{\rule{\larg}{1mm}} -\begin{latexonly} -\vspace{2mm} \\ -\vspace{5.5cm} -\end{latexonly} -} -\begin{latexonly} -\author{\begin{tabular}{p{13.7cm}} -ejabberd Development Team -\end{tabular}} -\date{} -\end{latexonly} - - -%% Options -\newcommand{\marking}[1]{#1} % Marking disabled -\newcommand{\quoting}[2][yozhik]{} % Quotes disabled -%\newcommand{\new}{\marginpar{\textsc{new}}} % Highlight new features -%\newcommand{\improved}{\marginpar{\textsc{improved}}} % Highlight improved features - -%% To by-pass errors in the HTML version: -\newstyle{.SPAN}{width:20\%; float:right; text-align:left; margin-left:auto;} -\newstyle{H1.titlemain HR}{display:none;} -\newstyle{TABLE.title}{border-top:1px solid grey;border-bottom:1px solid grey; background: \#efefef} -\newstyle{H1.chapter A, H2.section A, H3.subsection A, H4.subsubsection A, H5.paragraph A} - {color:\#000000; text-decoration:none;} -\newstyle{H1.chapter, H2.section, H3.subsection, H4.subsubsection, H5.paragraph} - {border-top: 1px solid grey; background: \#efefef; padding: 0.5ex} -\newstyle{pre.verbatim}{margin:1ex 2ex;border:1px dashed lightgrey;background-color:\#f9f9f9;padding:0.5ex;} -\newstyle{.dt-description}{margin:0ex 2ex;} -\newstyle{table[border="1"]}{border-collapse:collapse;margin-bottom:1em;} -\newstyle{table[border="1"] td}{border:1px solid \#aaa;padding:2px} -% Don't display
before and after tables or images: -\newstyle{BLOCKQUOTE.table DIV.center DIV.center HR}{display:none;} -\newstyle{BLOCKQUOTE.figure DIV.center DIV.center HR}{display:none;} - -%% Footnotes -\begin{latexonly} -\global\parskip=9pt plus 3pt minus 1pt -\global\parindent=0pt -\gdef\ahrefurl#1{\href{#1}{\texttt{#1}}} -\gdef\footahref#1#2{#2\footnote{\href{#1}{\texttt{#1}}}} -\end{latexonly} -\newcommand{\txepref}[2]{\footahref{http://xmpp.org/extensions/xep-#1.html}{#2}} -\newcommand{\xepref}[1]{\txepref{#1}{XEP-#1}} - -\begin{document} - -\label{titlepage} -\begin{titlepage} - \maketitle{} - -%% Commenting. Breaking clean layout for now: -%% \begin{center} -%% {\insscaleimg{\logoscale}{logo.png} -%% \par -%% } -%% \end{center} - -%% \begin{quotation}\textit{I can thoroughly recommend ejabberd for ease of setup --- -%% Kevin Smith, Current maintainer of the Psi project}\end{quotation} - -\end{titlepage} - -% Set the page counter to 2 so that the titlepage and the second page do not -% have the same page number. This fixes the PDFLaTeX warning "destination with -% the same identifier". -\begin{latexonly} -\setcounter{page}{2} -\end{latexonly} - -\label{toc} -\tableofcontents{} - -% Input introduction.tex -\input{introduction} - -\makechapter{installing}{Installing \ejabberd{}} - -\makesection{install.binary}{Installing \ejabberd{} with Binary Installer} - -Probably the easiest way to install an \ejabberd{} instant messaging server -is using the binary installer published by ProcessOne. -The binary installers of released \ejabberd{} versions -are available in the ProcessOne \ejabberd{} downloads page: -\ahrefurl{http://www.process-one.net/en/ejabberd/downloads} - -The installer will deploy and configure a full featured \ejabberd{} -server and does not require any extra dependencies. - -In *nix systems, remember to set executable the binary installer before starting it. For example: -\begin{verbatim} -chmod +x ejabberd-2.0.0_1-linux-x86-installer.bin -./ejabberd-2.0.0_1-linux-x86-installer.bin -\end{verbatim} - -\ejabberd{} can be started manually at any time, -or automatically by the operating system at system boot time. - -To start and stop \ejabberd{} manually, -use the desktop shortcuts created by the installer. -If the machine doesn't have a graphical system, use the scripts 'start' -and 'stop' in the 'bin' directory where \ejabberd{} is installed. - -The Windows installer also adds ejabberd as a system service, -and a shortcut to a debug console for experienced administrators. -If you want ejabberd to be started automatically at boot time, -go to the Windows service settings and set ejabberd to be automatically started. -Note that the Windows service is a feature still in development, -and for example it doesn't read the file ejabberdctl.cfg. - -On a *nix system, if you want ejabberd to be started as daemon at boot time, -copy \term{ejabberd.init} from the 'bin' directory to something like \term{/etc/init.d/ejabberd} -(depending on your distribution). -Create a system user called \term{ejabberd}, -give it write access to the directories \term{database/} and \term{logs/}, and set that as home; -the script will start the server with that user. -Then you can call \term{/etc/inid.d/ejabberd start} as root to start the server. - -When ejabberd is started, the processes that are started in the system -are \term{beam} or \term{beam.smp}, and also \term{epmd}. -In Microsoft Windows, the processes are \term{erl.exe} and \term{epmd.exe}. -For more information regarding \term{epmd} consult the section \ref{epmd}. - -If \term{ejabberd} doesn't start correctly in Windows, -try to start it using the shortcut in desktop or start menu. -If the window shows error 14001, the solution is to install: -"Microsoft Visual C++ 2005 SP1 Redistributable Package". -You can download it from -\footahref{http://www.microsoft.com/}{www.microsoft.com}. -Then uninstall \ejabberd{} and install it again. - -If \term{ejabberd} doesn't start correctly and a crash dump is generated, -there was a severe problem. -You can try starting \term{ejabberd} with -the script \term{bin/live.bat} in Windows, -or with the command \term{bin/ejabberdctl live} in other Operating Systems. -This way you see the error message provided by Erlang -and can identify what is exactly the problem. - -The \term{ejabberdctl} administration script is included in the \term{bin} directory. -Please refer to the section~\ref{ejabberdctl} for details about \term{ejabberdctl}, -and configurable options to fine tune the Erlang runtime system. - -\makesection{install.os}{Installing \ejabberd{} with Operating System Specific Packages} - -Some Operating Systems provide a specific \ejabberd{} package adapted to -the system architecture and libraries. -It usually also checks dependencies -and performs basic configuration tasks like creating the initial -administrator account. Some examples are Debian and Gentoo. Consult the -resources provided by your Operating System for more information. - -Usually those packages create a script like \term{/etc/init.d/ejabberd} -to start and stop \ejabberd{} as a service at boot time. - -\makesection{install.cean}{Installing \ejabberd{} with CEAN} - -\footahref{http://cean.process-one.net/}{CEAN} -(Comprehensive Erlang Archive Network) is a repository that hosts binary -packages from many Erlang programs, including \ejabberd{} and all its dependencies. -The binaries are available for many different system architectures, so this is an -alternative to the binary installer and Operating System's \ejabberd{} packages. - -You will have to create your own \ejabberd{} start -script depending of how you handle your CEAN installation. -The default \term{ejabberdctl} script is located -into \ejabberd{}'s priv directory and can be used as an example. - -\makesection{installation}{Installing \ejabberd{} from Source Code} -\ind{install} - -The canonical form for distribution of \ejabberd{} stable releases is the source code package. -Compiling \ejabberd{} from source code is quite easy in *nix systems, -as long as your system have all the dependencies. - -\makesubsection{installreq}{Requirements} -\ind{installation!requirements} - -To compile \ejabberd{} on a `Unix-like' operating system, you need: -\begin{itemize} -\item GNU Make -\item GCC -\item Libexpat 1.95 or higher -\item Erlang/OTP R15B or higher. -\item Libyaml 0.1.4 or higher -\item OpenSSL 0.9.8 or higher, for STARTTLS, SASL and SSL encryption. -\item Zlib 1.2.3 or higher, for Stream Compression support (\xepref{0138}). Optional. -\item PAM library. Optional. For Pluggable Authentication Modules (PAM). See section \ref{pam}. -\item GNU Iconv 1.8 or higher, for the IRC Transport (mod\_irc). Optional. Not needed on systems with GNU Libc. See section \ref{modirc}. -\item ImageMagick's Convert program. Optional. For CAPTCHA challenges. See section \ref{captcha}. -\end{itemize} - -\makesubsection{download}{Download Source Code} -\ind{install!download} - -Released versions of \ejabberd{} are available in the ProcessOne \ejabberd{} downloads page: -\ahrefurl{http://www.process-one.net/en/ejabberd/downloads} - -\ind{Git repository} -Alternatively, the latest development source code can be retrieved from the Git repository using the commands: -\begin{verbatim} -git clone git://github.com/processone/ejabberd.git ejabberd -cd ejabberd -./autogen.sh -\end{verbatim} - - -\makesubsection{compile}{Compile} -\ind{install!compile} - -To compile \ejabberd{} execute the commands: -\begin{verbatim} -./configure -make -\end{verbatim} - -The build configuration script allows several options. -To get the full list run the command: -\begin{verbatim} -./configure --help -\end{verbatim} - -Some options that you may be interested in modifying: -\begin{description} - \titem{--prefix=/} - Specify the path prefix where the files will be copied when running - the \term{make install} command. - - \titem{--enable-user[=USER]} - Allow this normal system user to execute the ejabberdctl script - (see section~\ref{ejabberdctl}), - read the configuration files, - read and write in the spool directory, - read and write in the log directory. - The account user and group must exist in the machine - before running \term{make install}. - This account doesn't need an explicit HOME directory, because - \term{/var/lib/ejabberd/} will be used by default. - - \titem{--enable-pam} - Enable the PAM authentication method (see section \ref{pam}). - - \titem{--enable-mssql} - Required if you want to use an external database. - See section~\ref{database} for more information. - - \titem{--enable-tools} - Enable the use of development tools. - - \titem{--enable-mysql} - Enable MySQL support (see section \ref{odbc}). - - \titem{--enable-pgsql} - Enable PostgreSQL support (see section \ref{odbc}). - - \titem{--enable-zlib} - Enable Stream Compression (XEP-0138) using zlib. - - \titem{--enable-iconv} - Enable iconv support. This is needed for \term{mod\_irc} (see seciont \ref{modirc}). - - \titem{--enable-debug} - Compile with \term{+debug\_info} enabled. - - \titem{--enable-full-xml} - Enable the use of XML based optimisations. - It will for example use CDATA to escape characters in the XMPP stream. - Use this option only if you are sure your XMPP clients include a fully compliant XML parser. - - \titem{--disable-transient-supervisors} - Disable the use of Erlang/OTP supervision for transient processes. - - \titem{--enable-nif} - Replaces some critical Erlang functions with equivalents written in C to improve performance. -\end{description} - -\makesubsection{install}{Install} -\ind{install!install} - -To install \ejabberd{} in the destination directories, run the command: -\begin{verbatim} -make install -\end{verbatim} -Note that you probably need administrative privileges in the system -to install \term{ejabberd}. - -The files and directories created are, by default: -\begin{description} - \titem{/etc/ejabberd/} Configuration directory: - \begin{description} - \titem{ejabberd.yml} ejabberd configuration file - \titem{ejabberdctl.cfg} Configuration file of the administration script - \titem{inetrc} Network DNS configuration file - \end{description} - \titem{/lib/ejabberd/} - \begin{description} - \titem{ebin/} Erlang binary files (*.beam) - \titem{include/} Erlang header files (*.hrl) - \titem{priv/} Additional files required at runtime - \begin{description} - \titem{bin/} Executable programs - \titem{lib/} Binary system libraries (*.so) - \titem{msgs/} Translation files (*.msgs) - \end{description} - \end{description} - \titem{/sbin/ejabberdctl} Administration script (see section~\ref{ejabberdctl}) - \titem{/share/doc/ejabberd/} Documentation of ejabberd - \titem{/var/lib/ejabberd/} Spool directory: - \begin{description} - \titem{.erlang.cookie} Erlang cookie file (see section \ref{cookie}) - \titem{acl.DCD, ...} Mnesia database spool files (*.DCD, *.DCL, *.DAT) - \end{description} - \titem{/var/log/ejabberd/} Log directory (see section~\ref{logfiles}): - \begin{description} - \titem{ejabberd.log} ejabberd service log - \titem{erlang.log} Erlang/OTP system log - \end{description} -\end{description} - - -\makesubsection{start}{Start} -\ind{install!start} - -You can use the \term{ejabberdctl} command line administration script to start and stop \ejabberd{}. -If you provided the configure option \term{--enable-user=USER} (see \ref{compile}), -you can execute \term{ejabberdctl} with either that system account or root. - -Usage example: -\begin{verbatim} -ejabberdctl start - -ejabberdctl status -The node ejabberd@localhost is started with status: started -ejabberd is running in that node - -ejabberdctl stop -\end{verbatim} - -If \term{ejabberd} doesn't start correctly and a crash dump is generated, -there was a severe problem. -You can try starting \term{ejabberd} with -the command \term{ejabberdctl live} -to see the error message provided by Erlang -and can identify what is exactly the problem. - -Please refer to the section~\ref{ejabberdctl} for details about \term{ejabberdctl}, -and configurable options to fine tune the Erlang runtime system. - -If you want ejabberd to be started as daemon at boot time, -copy \term{ejabberd.init} to something like \term{/etc/init.d/ejabberd} -(depending on your distribution). -Create a system user called \term{ejabberd}; -it will be used by the script to start the server. -Then you can call \term{/etc/inid.d/ejabberd start} as root to start the server. - -\makesubsection{bsd}{Specific Notes for BSD} -\ind{install!bsd} - -The command to compile \ejabberd{} in BSD systems is: -\begin{verbatim} -gmake -\end{verbatim} - - -\makesubsection{solaris}{Specific Notes for Sun Solaris} -\ind{install!solaris} - -You need to have \term{GNU install}, -but it isn't included in Solaris. -It can be easily installed if your Solaris system -is set up for \footahref{http://www.blastwave.org/}{blastwave.org} -package repository. -Make sure \term{/opt/csw/bin} is in your \term{PATH} and run: -\begin{verbatim} -pkg-get -i fileutils -\end{verbatim} - -If that program is called \term{ginstall}, -modify the \ejabberd{} \term{Makefile} script to suit your system, -for example: -\begin{verbatim} -cat Makefile | sed s/install/ginstall/ > Makefile.gi -\end{verbatim} -And finally install \ejabberd{} with: -\begin{verbatim} -gmake -f Makefile.gi ginstall -\end{verbatim} - - -\makesubsection{windows}{Specific Notes for Microsoft Windows} -\ind{install!windows} - -\makesubsubsection{windowsreq}{Requirements} - -To compile \ejabberd{} on a Microsoft Windows system, you need: -\begin{itemize} -\item MS Visual C++ 6.0 Compiler -\item \footahref{http://www.erlang.org/download.html}{Erlang/OTP R11B-5} -\item \footahref{http://sourceforge.net/project/showfiles.php?group\_id=10127\&package\_id=11277}{Expat 2.0.0 or higher} -\item -\footahref{http://www.gnu.org/software/libiconv/}{GNU Iconv 1.9.2} -(optional) -\item \footahref{http://www.slproweb.com/products/Win32OpenSSL.html}{Shining Light OpenSSL 0.9.8d or higher} -(to enable SSL connections) -\item \footahref{http://www.zlib.net/}{Zlib 1.2.3 or higher} -\end{itemize} - - -\makesubsubsection{windowscom}{Compilation} - -We assume that we will try to put as much library as possible into \verb|C:\sdk\| to make it easier to track what is install for \ejabberd{}. - -\begin{enumerate} -\item Install Erlang emulator (for example, into \verb|C:\sdk\erl5.5.5|). -\item Install Expat library into \verb|C:\sdk\Expat-2.0.0| - directory. - - Copy file \verb|C:\sdk\Expat-2.0.0\Libs\libexpat.dll| - to your Windows system directory (for example, \verb|C:\WINNT| or - \verb|C:\WINNT\System32|) -\item Build and install the Iconv library into the directory - \verb|C:\sdk\GnuWin32|. - - Copy file \verb|C:\sdk\GnuWin32\bin\lib*.dll| to your - Windows system directory (more installation instructions can be found in the - file README.woe32 in the iconv distribution). - - Note: instead of copying libexpat.dll and iconv.dll to the Windows - directory, you can add the directories - \verb|C:\sdk\Expat-2.0.0\Libs| and - \verb|C:\sdk\GnuWin32\bin| to the \verb|PATH| environment - variable. -\item Install OpenSSL in \verb|C:\sdk\OpenSSL| and add \verb|C:\sdk\OpenSSL\lib\VC| to your path or copy the binaries to your system directory. -\item Install ZLib in \verb|C:\sdk\gnuWin32|. Copy - \verb|C:\sdk\GnuWin32\bin\zlib1.dll| to your system directory. If you change your path it should already be set after libiconv install. -\item Make sure the you can access Erlang binaries from your path. For example: \verb|set PATH=%PATH%;"C:\sdk\erl5.6.5\bin"| -\item Depending on how you end up actually installing the library you might need to check and tweak the paths in the file configure.erl. -\item While in the directory \verb|ejabberd\src| run: -\begin{verbatim} -configure.bat -nmake -f Makefile.win32 -\end{verbatim} -\item Edit the file \verb|ejabberd\src\ejabberd.yml| and run -\begin{verbatim} -werl -s ejabberd -name ejabberd -\end{verbatim} -\end{enumerate} - -%TODO: how to compile database support on windows? - - -\makesection{initialadmin}{Create an XMPP Account for Administration} - -You need an XMPP account and grant him administrative privileges -to enter the \ejabberd{} Web Admin: -\begin{enumerate} -\item Register an XMPP account on your \ejabberd{} server, for example \term{admin1@example.org}. - There are two ways to register an XMPP account: - \begin{enumerate} - \item Using \term{ejabberdctl}\ind{ejabberdctl} (see section~\ref{ejabberdctl}): -\begin{verbatim} -ejabberdctl register admin1 example.org FgT5bk3 -\end{verbatim} - \item Using an XMPP client and In-Band Registration (see section~\ref{modregister}). - \end{enumerate} -\item Edit the \ejabberd{} configuration file to give administration rights to the XMPP account you created: -\begin{verbatim} -acl: - admin: - user: - - "admin1": "example.org" -access: - configure: - admin: allow -\end{verbatim} - You can grant administrative privileges to many XMPP accounts, - and also to accounts in other XMPP servers. -\item Restart \ejabberd{} to load the new configuration. -\item Open the Web Admin (\verb|http://server:port/admin/|) in your - favourite browser. Make sure to enter the \emph{full} JID as username (in this - example: \jid{admin1@example.org}. The reason that you also need to enter the - suffix, is because \ejabberd{}'s virtual hosting support. -\end{enumerate} - -\makesection{upgrade}{Upgrading \ejabberd{}} - -To upgrade an ejabberd installation to a new version, -simply uninstall the old version, and then install the new one. -Of course, it is important that the configuration file -and Mnesia database spool directory are not removed. - -\ejabberd{} automatically updates the Mnesia table definitions at startup when needed. -If you also use an external database for storage of some modules, -check if the release notes of the new ejabberd version -indicates you need to also update those tables. - - -\makechapter{configure}{Configuring \ejabberd{}} -\ind{configuration file} - -\makesection{basicconfig}{Basic Configuration} - -The configuration file will be loaded the first time you start \ejabberd{}. -The configuration file name MUST have ``.yml'' extension. This helps ejabberd -to differentiate between the new and legacy file formats (see section~\ref{oldconfig}). - -Note that \ejabberd{} never edits the configuration file. - -The configuration file is written in -\footahref{http://en.wikipedia.org/wiki/YAML}{YAML}. -However, different scalars are treated as different types: -\begin{itemize} -\item unquoted or single-quoted strings. The type is called \verb|atom()| - in this document. - Examples: \verb|dog|, \verb|'Jupiter'|, \verb|'3.14159'|, \verb|YELLOW|. -\item numeric literals. The type is called \verb|integer()|, \verb|float()| or, - if both are allowed, \verb|number()|. - Examples: \verb|3|, \verb|-45.0|, \verb|.0| -\item double-quoted or folded strings. The type is called \verb|string()|. - Examples of a double-quoted string: - \verb|"Lizzard"|, \verb|"orange"|, \verb|"3.14159"|. - Examples of a folded string: -\begin{verbatim} -> Art thou not Romeo, - and a Montague? -\end{verbatim} -\begin{verbatim} -| Neither, fair saint, - if either thee dislike. -\end{verbatim} -For associative arrays ("mappings") and lists you can use both outline -indentation and compact syntax (aka ``JSON style''). For example, the following is equivalent: -\begin{verbatim} -{param1: ["val1", "val2"], param2: ["val3", "val4"]} -\end{verbatim} -and -\begin{verbatim} -param1: - - "val1" - - "val2" -param2: - - "val3" - - "val4" -\end{verbatim} -Note that both styles are used in this document. -\end{itemize} - -\makesubsection{oldconfig}{Legacy Configuration File} -In previous \ejabberd{} version the configuration file should be written -in Erlang terms. The format is still supported, but it is highly recommended -to convert it to the new YAML format using \term{convert\_to\_yaml} command -from \term{ejabberdctl} (see~\ref{ejabberdctl} and \ref{list-eja-commands} for details). - -If you want to specify some options using the old Erlang format, -you can set them in an additional cfg file, and include it using -the \option{include\_config\_file} option, see \ref{includeconfigfile} -for the option description and a related example in \ref{accesscommands}. - -If you just want to provide an erlang term inside an option, -you can use the \term{"> erlangterm."} syntax for embedding erlang terms in a YAML file, for example: -\begin{verbatim} -modules: - mod_cron: - tasks: - - time: 10 - units: seconds - module: mnesia - function: info - arguments: "> []." - - time: 3 - units: seconds - module: ejabberd_auth - function: try_register - arguments: "> [\"user1\", \"localhost\", \"pass\"]." -\end{verbatim} - -\makesubsection{hostnames}{Host Names} -\ind{options!hosts}\ind{host names} - -The option \option{hosts} defines a list containing one or more domains that -\ejabberd{} will serve. - -The syntax is: -\esyntax{[HostName]} - -Examples: -\begin{itemize} -\item Serving one domain: -\begin{verbatim} -hosts: ["example.org"] -\end{verbatim} -\item Serving three domains: -\begin{verbatim} -hosts: - - "example.net" - - "example.com" - - "jabber.somesite.org" -\end{verbatim} -\end{itemize} - -\makesubsection{virtualhost}{Virtual Hosting} -\ind{virtual hosting}\ind{virtual hosts}\ind{virtual domains} - -Options can be defined separately for every virtual host using the -\term{host\_config} option. - -The syntax is: \ind{options!host\_config} -\esyntax{\{HostName: [Option, ...]\}} - -Examples: -\begin{itemize} -\item Domain \jid{example.net} is using the internal authentication method while - domain \jid{example.com} is using the \ind{LDAP}LDAP server running on the - domain \jid{localhost} to perform authentication: -\begin{verbatim} -host_config: - "example.net" - auth_method: internal - "example.com": - auth_method: ldap - ldap_servers: - - "localhost" - ldap_uids: - - "uid" - ldap_rootdn: "dc=localdomain" - ldap_rootdn: "dc=example,dc=com" - ldap_password: "" -\end{verbatim} -\item Domain \jid{example.net} is using \ind{odbc}ODBC to perform authentication - while domain \jid{example.com} is using the LDAP servers running on the domains - \jid{localhost} and \jid{otherhost}: -\begin{verbatim} -host_config: - "example.net": - auth_method: odbc - odbc_type: odbc - odbc_server: "DSN=ejabberd;UID=ejabberd;PWD=ejabberd" - "example.com": - auth_method: ldap - ldap_servers: - - "localhost" - - "otherhost" - ldap_uids: - - "uid" - ldap_rootdn: "dc=localdomain" - ldap_rootdn: "dc=example,dc=com" - ldap_password: "" -\end{verbatim} -\end{itemize} - -To define specific ejabberd modules in a virtual host, -you can define the global \term{modules} option with the common modules, -and later add specific modules to certain virtual hosts. -To accomplish that, instead of defining each option in \term{host\_config} -use \term{append\_host\_config} with the same syntax. - -In this example three virtual hosts have some similar modules, but there are also -other different modules for some specific virtual hosts: -\begin{verbatim} -## This ejabberd server has three vhosts: -hosts: - - "one.example.org" - - "two.example.org" - - "three.example.org" - -## Configuration of modules that are common to all vhosts -modules: - mod_roster: {} - mod_configure: {} - mod_disco: {} - mod_private: {} - mod_time: {} - mod_last: {} - mod_version: {} - -## Add some modules to vhost one: -append_host_config: - "one.example.org": - modules: - mod_echo: - host: "echo-service.one.example.org" - mod_http_bind: {} - mod_logxml: {} - -## Add a module just to vhost two: -append_host_config: - "two.example.org": - modules: - mod_echo: - host: "mirror.two.example.org" -\end{verbatim} - -\makesubsection{listened}{Listening Ports} -\ind{options!listen} - -The option \option{listen} defines for which ports, addresses and network protocols \ejabberd{} -will listen and what services will be run on them. Each element of the list is an -associative array with the following elements: -\begin{itemize} -\item Port number. Optionally also the IP address and/or a transport protocol. -\item Listening module that serves this port. -\item Options for the TCP socket and for the listening module. -\end{itemize} - -The option syntax is: -\esyntax{[Listener, ...]} -Example: -\begin{verbatim} -listen: - - - port: 5222 - module: ejabberd_c2s - starttls: true - certfile: "/path/to/certfile.pem" - - - port: 5269 - module: ejabberd_s2s_in - transport: tcp -\end{verbatim} - -\makesubsubsection{listened-port}{Port Number, IP Address and Transport Protocol} - -The port number defines which port to listen for incoming connections. -It can be a Jabber/XMPP standard port -(see section \ref{firewall}) or any other valid port number. - -The IP address can be represented as a string. -The socket will listen only in that network interface. -It is possible to specify a generic address, -so \ejabberd{} will listen in all addresses. -Depending in the type of the IP address, IPv4 or IPv6 will be used. -When not specified the IP address, it will listen on all IPv4 network addresses. - -Some example values for IP address: -\begin{itemize} -\item \verb|"0.0.0.0"| to listen in all IPv4 network interfaces. This is the default value when no IP is specified. -\item \verb|"::"| to listen in all IPv6 network interfaces -\item \verb|"10.11.12.13"| is the IPv4 address \verb|10.11.12.13| -\item \verb|"::FFFF:127.0.0.1"| is the IPv6 address \verb|::FFFF:127.0.0.1/128| -\end{itemize} - -The transport protocol can be \term{tcp} or \term{udp}. -Default is \term{tcp}. - - -\makesubsubsection{listened-module}{Listening Module} - -\ind{modules!ejabberd\_c2s}\ind{modules!ejabberd\_s2s\_in}\ind{modules!ejabberd\_service}\ind{modules!ejabberd\_http}\ind{protocols!XEP-0114: Jabber Component Protocol} -The available modules, their purpose and the options allowed by each one are: -\begin{description} - \titem{\texttt{ejabberd\_c2s}} - Handles c2s connections.\\ - Options: \texttt{access}, \texttt{certfile}, \texttt{ciphers}, \texttt{protocol\_options} - \texttt{max\_ack\_queue}, \texttt{max\_fsm\_queue}, - \texttt{max\_stanza\_size}, \texttt{resend\_on\_timeout}, - \texttt{resume\_timeout}, \texttt{shaper}, - \texttt{starttls}, \texttt{starttls\_required}, - \texttt{stream\_management}, \texttt{tls}, - \texttt{zlib}, \texttt{tls\_compression} - \titem{\texttt{ejabberd\_s2s\_in}} - Handles incoming s2s connections.\\ - Options: \texttt{max\_stanza\_size}, \texttt{shaper}, \texttt{tls\_compression} - \titem{\texttt{ejabberd\_service}} - Interacts with an \footahref{http://www.ejabberd.im/tutorials-transports}{external component} - (as defined in the Jabber Component Protocol (\xepref{0114}).\\ - Options: \texttt{access}, \texttt{hosts}, \texttt{max\_fsm\_queue}, - \texttt{service\_check\_from}, \texttt{shaper\_rule} - \titem{\texttt{ejabberd\_sip}} - Handles SIP requests as defined in - \footahref{http://tools.ietf.org/html/rfc3261}{RFC 3261}.\\ - Options: \texttt{certfile}, \texttt{tls} - \titem{\texttt{ejabberd\_stun}} - Handles STUN/TURN requests as defined in - \footahref{http://tools.ietf.org/html/rfc5389}{RFC 5389} and - \footahref{http://tools.ietf.org/html/rfc5766}{RFC 5766}.\\ - Options: \texttt{certfile}, \texttt{tls}, \texttt{use\_turn}, \texttt{turn\_ip}, - \texttt{turn\_port\_range}, \texttt{turn\_max\_allocations}, - \texttt{turn\_max\_permissions}, \texttt{shaper}, \texttt{server\_name}, - \texttt{auth\_realm}, \texttt{auth\_type} - \titem{\texttt{ejabberd\_http}} - Handles incoming HTTP connections.\\ - Options: \texttt{captcha}, \texttt{certfile}, \texttt{default\_host}, \texttt{http\_bind}, \texttt{http\_poll}, - \texttt{request\_handlers}, \texttt{tls}, \texttt{tls\_compression}, \texttt{trusted\_proxies}, \texttt{web\_admin}\\ - \titem{\texttt{ejabberd\_xmlrpc}} - Handles XML-RPC requests to execute ejabberd commands (\ref{eja-commands}).\\ - Options: \texttt{access\_commands}, \texttt{maxsessions}, \texttt{timeout}.\\ - You can find option explanations, example configuration in old and new format, - and example calls in several languages in the old - \footahref{http://www.ejabberd.im/ejabberd\_xmlrpc}{ejabberd\_xmlrpc documentation}. -\end{description} - - -\makesubsubsection{listened-options}{Options} - -This is a detailed description of each option allowed by the listening modules: -\begin{description} - \titem{access: AccessName} \ind{options!access}This option defines - access to the port. The default value is \term{all}. - \titem{backlog: Value} \ind{options!backlog}The backlog value - defines the maximum length that the queue of pending connections may - grow to. This should be increased if the server is going to handle - lots of new incoming connections as they may be dropped if there is - no space in the queue (and ejabberd was not able to accept them - immediately). Default value is 5. - \titem{captcha: true|false} \ind{options!http-captcha} - Simple web page that allows a user to fill a CAPTCHA challenge (see section \ref{captcha}). - \titem{certfile: Path} Full path to a file containing the default SSL certificate. - To define a certificate file specific for a given domain, use the global option \term{domain\_certfile}. - \titem{ciphers: Ciphers} OpenSSL ciphers list in the same format accepted by - `\verb|openssl ciphers|' command. - \titem{protocol\_options: ProtocolOpts} \ind{options!protocol\_options} - List of general options relating to SSL/TLS. These map to - \footahref{https://www.openssl.org/docs/ssl/SSL\_CTX\_set\_options.html}{OpenSSL's set\_options()}. - For a full list of options available in ejabberd, - \footahref{https://github.com/processone/tls/blob/master/c\_src/options.h}{see the source}. - The default entry is: \verb|"no_sslv2"| - \titem{default\_host: undefined|HostName\}} - If the HTTP request received by ejabberd contains the HTTP header \term{Host} - with an ambiguous virtual host that doesn't match any one defined in ejabberd (see \ref{hostnames}), - then this configured HostName is set as the request Host. - The default value of this option is: \term{undefined}. - \titem{hosts: \{Hostname: [HostOption, ...]\}}\ind{options!hosts} - The external Jabber component that connects to this \term{ejabberd\_service} - can serve one or more hostnames. - As \term{HostOption} you can define options for the component; - currently the only allowed option is the password required to the component - when attempt to connect to ejabberd: \poption{password: Secret}. - Note that you cannot define in a single \term{ejabberd\_service} components of - different services: add an \term{ejabberd\_service} for each service, - as seen in an example below. - \titem{http\_bind: true|false} \ind{options!http\_bind}\ind{protocols!XEP-0206: HTTP Binding}\ind{JWChat}\ind{web-based XMPP client} - This option enables HTTP Binding (\xepref{0124} and \xepref{0206}) support. HTTP Bind - enables access via HTTP requests to \ejabberd{} from behind firewalls which - do not allow outgoing sockets on port 5222. - - Remember that you must also install and enable the module mod\_http\_bind. - - If HTTP Bind is enabled, it will be available at - \verb|http://server:port/http-bind/|. Be aware that support for HTTP Bind - is also needed in the \XMPP{} client. Remark also that HTTP Bind can be - interesting to host a web-based \XMPP{} client such as - \footahref{http://jwchat.sourceforge.net/}{JWChat} - (check the tutorials to install JWChat with ejabberd and an - \footahref{http://www.ejabberd.im/jwchat-localserver}{embedded local web server} - or \footahref{http://www.ejabberd.im/jwchat-apache}{Apache}). - \titem{http\_poll: true|false} \ind{options!http\_poll}\ind{protocols!XEP-0025: HTTP Polling}\ind{JWChat}\ind{web-based XMPP client} - This option enables HTTP Polling (\xepref{0025}) support. HTTP Polling - enables access via HTTP requests to \ejabberd{} from behind firewalls which - do not allow outgoing sockets on port 5222. - - If HTTP Polling is enabled, it will be available at - \verb|http://server:port/http-poll/|. Be aware that support for HTTP Polling - is also needed in the \XMPP{} client. Remark also that HTTP Polling can be - interesting to host a web-based \XMPP{} client such as - \footahref{http://jwchat.sourceforge.net/}{JWChat}. - - The maximum period of time to keep a client session active without - an incoming POST request can be configured with the global option - \term{http\_poll\_timeout}. The default value is five minutes. - The option can be defined in \term{ejabberd.yml}, expressing the time - in seconds: \verb|{http_poll_timeout, 300}.| - \titem{max\_ack\_queue: Size} - This option specifies the maximum number of unacknowledged stanzas - queued for possible retransmission if \term{stream\_management} is - enabled. When the limit is exceeded, the client session is - terminated. This option can be specified for \term{ejabberd\_c2s} - listeners. The allowed values are positive integers and - \term{infinity}. Default value: \term{500}. - \titem{max\_fsm\_queue: Size} - This option specifies the maximum number of elements in the queue of the FSM - (Finite State Machine). - Roughly speaking, each message in such queues represents one XML - stanza queued to be sent into its relevant outgoing stream. If queue size - reaches the limit (because, for example, the receiver of stanzas is too slow), - the FSM and the corresponding connection (if any) will be terminated - and error message will be logged. - The reasonable value for this option depends on your hardware configuration. - However, there is no much sense to set the size above 1000 elements. - This option can be specified for \term{ejabberd\_service} and - \term{ejabberd\_c2s} listeners, - or also globally for \term{ejabberd\_s2s\_out}. - If the option is not specified for \term{ejabberd\_service} or - \term{ejabberd\_c2s} listeners, - the globally configured value is used. - The allowed values are integers and 'undefined'. - Default value: 'undefined'. - \titem{max\_stanza\_size: Size} - \ind{options!max\_stanza\_size}This option specifies an - approximate maximum size in bytes of XML stanzas. Approximate, - because it is calculated with the precision of one block of read - data. For example \verb|{max_stanza_size, 65536}|. The default - value is \term{infinity}. Recommended values are 65536 for c2s - connections and 131072 for s2s connections. s2s max stanza size - must always much higher than c2s limit. Change this value with - extreme care as it can cause unwanted disconnect if set too low. - \titem{request\_handlers: \{Path: Module\}} To define one or several handlers that will serve HTTP requests. - The Path is a string; so the URIs that start with that Path will be served by Module. - For example, if you want \term{mod\_foo} to serve the URIs that start with \term{/a/b/}, - and you also want \term{mod\_http\_bind} to serve the URIs \term{/http-bind/}, - use this option: -\begin{verbatim} -request_handlers: - /"a"/"b": mod_foo - /"http-bind": mod_http_bind -\end{verbatim} - \titem{resend\_on\_timeout: true|false|if\_offline} - If \term{stream\_management} is enabled and this option is set to - \term{true}, any stanzas that weren't acknowledged by the client - will be resent on session timeout. This behavior might often be - desired, but could have unexpected results under certain - circumstances. For example, a message that was sent to two resources - might get resent to one of them if the other one timed out. - Therefore, the default value for this option is \term{false}, which - tells ejabberd to generate an error message instead. As an - alternative, the option may be set to \term{if\_offline}. In this - case, unacknowledged stanzas are resent only if no other resource is - online when the session times out. Otherwise, error messages are - generated. The option can be specified for \term{ejabberd\_c2s} - listeners. - \titem{resume\_timeout: Seconds} - This option configures the number of seconds until a session times - out if the connection is lost. During this period of time, a client - may resume the session if \term{stream\_management} is enabled. This - option can be specified for \term{ejabberd\_c2s} listeners. Setting - it to \term{0} effectively disables session resumption. The default - value is \term{300}. - \titem{service\_check\_from: true|false} - \ind{options!service\_check\_from} - This option can be used with \term{ejabberd\_service} only. - \xepref{0114} requires that the domain must match the hostname of the component. - If this option is set to \term{false}, \ejabberd{} will allow the component - to send stanzas with any arbitrary domain in the 'from' attribute. - Only use this option if you are completely sure about it. - The default value is \term{true}, to be compliant with \xepref{0114}. - \titem{shaper: none|ShaperName} \ind{options!shaper}This option defines a - shaper for the port (see section~\ref{shapers}). The default value - is \term{none}. - \titem{shaper\_rule: none|ShaperRule} \ind{options!shaperrule}This option defines a - shaper rule for the \term{ejabberd\_service} (see section~\ref{shapers}). The recommended value - is \term{fast}. - \titem{starttls: true|false} \ind{options!starttls}\ind{STARTTLS}This option - specifies that STARTTLS encryption is available on connections to the port. - You should also set the \option{certfile} option. - You can define a certificate file for a specific domain using the global option \option{domain\_certfile}. - \titem{starttls\_required: true|false} \ind{options!starttls\_required}This option - specifies that STARTTLS encryption is required on connections to the port. - No unencrypted connections will be allowed. - You should also set the \option{certfile} option. - You can define a certificate file for a specific domain using the global option \option{domain\_certfile}. - \titem{stream\_management: true|false} - Setting this option to \term{false} disables ejabberd's support for - Stream Management (\xepref{0198}). It can be specified for - \term{ejabberd\_c2s} listeners. The default value is \term{true}. - \titem{timeout: Integer} \ind{options!timeout} - Timeout of the connections, expressed in milliseconds. - Default: 5000 - \titem{tls: true|false} \ind{options!tls}\ind{TLS}This option specifies that traffic on - the port will be encrypted using SSL immediately after connecting. - This was the traditional encryption method in the early Jabber software, - commonly on port 5223 for client-to-server communications. - But this method is nowadays deprecated and not recommended. - The preferable encryption method is STARTTLS on port 5222, as defined - \footahref{http://xmpp.org/rfcs/rfc3920.html\#tls}{RFC 3920: XMPP Core}, - which can be enabled in \ejabberd{} with the option \term{starttls}. - If this option is set, you should also set the \option{certfile} option. - The option \term{tls} can also be used in \term{ejabberd\_http} to support HTTPS. - \titem{tls\_compression: true|false} - Whether to enable or disable TLS compression. The default value is \term{true}. - \titem{trusted\_proxies: all | [IpString]} \ind{options!trusted\_proxies} - Specify what proxies are trusted when an HTTP request contains the header \term{X-Forwarded-For} - You can specify \term{all} to allow all proxies, or specify a list of IPs in string format. - The default value is: \term{["127.0.0.1"]} - \titem{web\_admin: true|false} \ind{options!web\_admin}\ind{web admin}This option - enables the Web Admin for \ejabberd{} administration which is available - at \verb|http://server:port/admin/|. Login and password are the username and - password of one of the registered users who are granted access by the - `configure' access rule. - \titem{zlib: true|false} \ind{options!zlib}\ind{protocols!XEP-0138: Stream Compression}\ind{Zlib}This - option specifies that Zlib stream compression (as defined in \xepref{0138}) - is available on connections to the port. -\end{description} - -There are some additional global options that can be specified in the ejabberd configuration file (outside \term{listen}): -\begin{description} - \titem{s2s\_use\_starttls: false|optional|required|required\_trusted} - \ind{options!s2s\_use\_starttls}\ind{STARTTLS}This option defines if - s2s connections don't use STARTTLS encryption; if STARTTLS can be used optionally; - if STARTTLS is required to establish the connection; - or if STARTTLS is required and the remote certificate must be valid and trusted. - The default value is to not use STARTTLS: \term{false}. - \titem{s2s\_certfile: Path} \ind{options!s2s\_certificate}Full path to a - file containing a SSL certificate. - \titem{domain\_certfile: Path} \ind{options!domain\_certfile} - Full path to the file containing the SSL certificate for a specific domain. - \titem{s2s\_ciphers: Ciphers} \ind{options!s2s\_ciphers} OpenSSL ciphers list - in the same format accepted by `\verb|openssl ciphers|' command. - \titem{s2s\_protocol\_options: ProtocolOpts} \ind{options!s2s\_protocol\_options} - List of general options relating to SSL/TLS. These map to - \footahref{https://www.openssl.org/docs/ssl/SSL\_CTX\_set\_options.html}{OpenSSL's set\_options()}. - For a full list of options available in ejabberd, - \footahref{https://github.com/processone/tls/blob/master/c\_src/options.h}{see the source}. - The default entry is: \verb|"no_sslv2"| - \titem{outgoing\_s2s\_families: [Family, ...]} \ind{options!outgoing\_s2s\_families} - Specify which address families to try, in what order. - By default it first tries connecting with IPv4, if that fails it tries using IPv6. - \titem{outgoing\_s2s\_timeout: Timeout} \ind{options!outgoing\_s2s\_timeout} - The timeout in milliseconds for outgoing S2S connection attempts. - \titem{s2s\_dns\_timeout: Timeout} \ind{options!s2s\_dns\_timeout} - The timeout in seconds for DNS resolving. The default value is \term{10}. - \titem{s2s\_dns\_retries: Number} \ind{options!s2s\_dns\_retries} - DNS resolving retries in seconds. The default value is \term{2}. - \titem{s2s\_policy: Access} \ind{options!s2s\_policy} - The policy for incoming and outgoing s2s connections to other XMPP servers. - The default value is \term{all}. - \titem{s2s\_max\_retry\_delay: Seconds} \ind{options!s2s\_max\_retry\_delay} - The maximum allowed delay for retry to connect after a failed connection attempt. - Specified in seconds. The default value is 300 seconds (5 minutes). - \titem{s2s\_tls\_compression: true|false} - Whether to enable or disable TLS compression for s2s connections. - The default value is \term{true}. - \titem{max\_fsm\_queue: Size} - This option specifies the maximum number of elements in the queue of the FSM - (Finite State Machine). - Roughly speaking, each message in such queues represents one XML - stanza queued to be sent into its relevant outgoing stream. If queue size - reaches the limit (because, for example, the receiver of stanzas is too slow), - the FSM and the corresponding connection (if any) will be terminated - and error message will be logged. - The reasonable value for this option depends on your hardware configuration. - However, there is no much sense to set the size above 1000 elements. - This option can be specified for \term{ejabberd\_service} and - \term{ejabberd\_c2s} listeners, - or also globally for \term{ejabberd\_s2s\_out}. - If the option is not specified for \term{ejabberd\_service} or - \term{ejabberd\_c2s} listeners, - the globally configured value is used. - The allowed values are integers and 'undefined'. - Default value: 'undefined'. - \titem{route\_subdomains: local|s2s} - Defines if ejabberd must route stanzas directed to subdomains locally (compliant with - \footahref{http://xmpp.org/rfcs/rfc3920.html\#rules.subdomain}{RFC 3920: XMPP Core}), - or to foreign server using S2S (compliant with - \footahref{http://tools.ietf.org/html/draft-saintandre-rfc3920bis-09\#section-11.3}{RFC 3920 bis}). -\end{description} - -\makesubsubsection{listened-examples}{Examples} - -For example, the following simple configuration defines: -\begin{itemize} -\item There are three domains. The default certificate file is \term{server.pem}. -However, the c2s and s2s connections to the domain \term{example.com} use the file \term{example\_com.pem}. -\item Port 5222 listens for c2s connections with STARTTLS, - and also allows plain connections for old clients. -\item Port 5223 listens for c2s connections with the old SSL. -\item Port 5269 listens for s2s connections with STARTTLS. The socket is set for IPv6 instead of IPv4. -\item Port 3478 listens for STUN requests over UDP. -\item Port 5280 listens for HTTP requests, and serves the HTTP Poll service. -\item Port 5281 listens for HTTP requests, using HTTPS to serve HTTP-Bind (BOSH) and the Web Admin as explained in - section~\ref{webadmin}. The socket only listens connections to the IP address 127.0.0.1. -\end{itemize} -\begin{verbatim} -hosts: - - "example.com" - - "example.org" - - "example.net" - -listen: - - - port: 5222 - module: ejabberd_c2s - access: c2s - shaper: c2s_shaper - starttls: true - certfile: "/etc/ejabberd/server.pem" - max_stanza_size: 65536 - - - port: 5223 - module: ejabberd_c2s - access: c2s - shaper: c2s_shaper - tls: true - certfile: "/etc/ejabberd/server.pem" - max_stanza_size: 65536 - - - port: 5269 - ip: "::" - module: ejabberd_s2s_in - shaper: s2s_shaper - max_stanza_size: 131072 - - - port: 3478 - transport: udp - module: ejabberd_stun - - - port: 5280 - module: ejabberd_http - http_poll: true - - - port: 5281 - ip: "127.0.0.1" - module: ejabberd_http - web_admin: true - http_bind: true - tls: true - certfile: "/etc/ejabberd/server.pem" - -s2s_use_starttls: optional -s2s_certfile: "/etc/ejabberd/server.pem" -host_config: - "example.com": - domain_certfile: "/etc/ejabberd/example_com.pem" -outgoing_s2s_families: - - ipv4 - - ipv6 -outgoing_s2s_timeout: 10000 -\end{verbatim} - -In this example, the following configuration defines that: -\begin{itemize} -\item c2s connections are listened for on port 5222 (all IPv4 addresses) and - on port 5223 (SSL, IP 192.168.0.1 and fdca:8ab6:a243:75ef::1) and denied - for the user called `\term{bad}'. -\item s2s connections are listened for on port 5269 (all IPv4 addresses) - with STARTTLS for secured traffic strictly required, and the certificates are verified. - Incoming and outgoing connections of remote XMPP servers are denied, - only two servers can connect: "jabber.example.org" and "example.com". -\item Port 5280 is serving the Web Admin and the HTTP Polling service - in all the IPv4 addresses. Note - that it is also possible to serve them on different ports. The second - example in section~\ref{webadmin} shows how exactly this can be done. -\item All users except for the administrators have a traffic of limit - 1,000\,Bytes/second -\item \ind{transports!AIM}The - \footahref{http://www.ejabberd.im/pyaimt}{AIM transport} - \jid{aim.example.org} is connected to port 5233 on localhost IP addresses - (127.0.0.1 and ::1) with password `\term{aimsecret}'. -\item \ind{transports!ICQ}The ICQ transport JIT (\jid{icq.example.org} and - \jid{sms.example.org}) is connected to port 5234 with password - `\term{jitsecret}'. -\item \ind{transports!MSN}The - \footahref{http://www.ejabberd.im/pymsnt}{MSN transport} - \jid{msn.example.org} is connected to port 5235 with password - `\term{msnsecret}'. -\item \ind{transports!Yahoo}The - \footahref{http://www.ejabberd.im/yahoo-transport-2}{Yahoo! transport} - \jid{yahoo.example.org} is connected to port 5236 with password - `\term{yahoosecret}'. -\item \ind{transports!Gadu-Gadu}The \footahref{http://www.ejabberd.im/jabber-gg-transport}{Gadu-Gadu transport} \jid{gg.example.org} is - connected to port 5237 with password `\term{ggsecret}'. -\item \ind{transports!email notifier}The - \footahref{http://www.ejabberd.im/jmc}{Jabber Mail Component} - \jid{jmc.example.org} is connected to port 5238 with password - `\term{jmcsecret}'. -\item The service custom has enabled the special option to avoiding checking the \term{from} attribute in the packets send by this component. The component can send packets in behalf of any users from the server, or even on behalf of any server. -\end{itemize} -\begin{verbatim} -acl: - blocked: - user: "bad" - trusted_servers: - server: - - "example.com" - - "jabber.example.org" - xmlrpc_bot: - user: - - "xmlrpc-robot": "example.org" -shaper: - normal: 1000 -access: - c2s: - blocked: deny - all: allow - c2s_shaper: - admin: none - all: normal - xmlrpc_access: - xmlrpc_bot: allow - s2s: - trusted_servers: allow - all: deny -s2s_certfile: "/path/to/ssl.pem" -s2s_access: s2s -s2s_use_starttls: required_trusted -listen: - - - port: 5222 - module: ejabberd_c2s - shaper: c2s_shaper - access: c2s - - - ip: "192.168.0.1" - port: 5223 - module: ejabberd_c2s - certfile: "/path/to/ssl.pem" - tls: true - access: c2s - - - ip: "FDCA:8AB6:A243:75EF::1" - port: 5223 - module: ejabberd_c2s - certfile: "/path/to/ssl.pem" - tls: true - access: c2s - - - port: 5269 - module: ejabberd_s2s_in - - - port: 5280 - module: ejabberd_http - web_admin: true - http_poll: true - - - port: 4560 - module: ejabberd_xmlrpc - - - ip: "127.0.0.1" - port: 5233 - module: ejabberd_service - hosts: - "aim.example.org": - password: "aimsecret" - - - ip: "::1" - port: 5233 - module: ejabberd_service - hosts: - "aim.example.org": - password: "aimsecret" - - - port: 5234 - module: ejabberd_service - hosts: - "icq.example.org": - password: "jitsecret" - "sms.example.org": - password: "jitsecret" - - - port: 5235 - module: ejabberd_service - hosts: - "msn.example.org": - password: "msnsecret" - - - port: 5236 - module: ejabberd_service - hosts: - "yahoo.example.org": - password: "yahoosecret" - - - port: 5237 - module: ejabberd_service - hosts: - "gg.example.org": - password: "ggsecret" - - - port: 5238 - module: ejabberd_service - hosts: - "jmc.example.org": - password: "jmcsecret" - - - port: 5239 - module: ejabberd_service - service_check_from: false - hosts: - "custom.example.org": - password: "customsecret" -\end{verbatim} -Note, that for services based in \ind{jabberd14}jabberd14 or \ind{WPJabber}WPJabber -you have to make the transports log and do \ind{XDB}XDB by themselves: -\begin{verbatim} - - - - - - %d: [%t] (%h): %s - /var/log/jabber/service.log - - - - - - - - - /usr/lib/jabber/xdb_file.so - - - /var/spool/jabber - - -\end{verbatim} - -\makesubsection{auth}{Authentication} -\ind{authentication}\ind{options!auth\_method} - -The option \option{auth\_method} defines the authentication methods that are used -for user authentication. The syntax is: -\esyntax{[Method, ...]} - -The following authentication methods are supported by \ejabberd{}: -\begin{itemize} -\item internal (default) --- See section~\ref{internalauth}. -\item external --- See section~\ref{extauth}. -\item ldap --- See section~\ref{ldap}. -\item odbc --- See section~\ref{odbc}. -\item anonymous --- See section~\ref{saslanonymous}. -\item pam --- See section~\ref{pam}. -\end{itemize} - -Account creation is only supported by internal, external and odbc methods. - -The option \option{resource\_conflict} defines the action when a client attempts to -login to an account with a resource that is already connected. -The option syntax is: -\esyntax{resource\_conflict: setresource|closenew|closeold} -The possible values match exactly the three possibilities described in -\footahref{http://tools.ietf.org/html/rfc6120\#section-7.7.2.2}{XMPP Core: section 7.7.2.2}. -The default value is \term{closeold}. -If the client uses old Jabber Non-SASL authentication (\xepref{0078}), -then this option is not respected, and the action performed is \term{closeold}. - -The option \option{fqdn} allows you to define the Fully Qualified Domain Name -of the machine, in case it isn't detected automatically. -The FQDN is used to authenticate some clients that use the DIGEST-MD5 SASL mechanism. -The option syntax is: -\esyntax{fqdn: undefined|FqdnString|[FqdnString]} - -The option \option{disable\_sasl\_mechanisms} specifies a list of SASL -mechanisms that should \emph{not} be offered to the client. The mechanisms can -be listed as lowercase or uppercase strings. The option syntax is: -\esyntax{disable\_sasl\_mechanisms: [Mechanism, ...]} - -\makesubsubsection{internalauth}{Internal} -\ind{internal authentication}\ind{Mnesia} - -\ejabberd{} uses its internal Mnesia database as the default authentication method. -The value \term{internal} will enable the internal authentication method. - -The option \term{auth\_password\_format: plain|scram} -defines in what format the users passwords are stored: -\begin{description} - \titem{plain} - 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 (\xepref{0078}), \term{SASL PLAIN}, - \term{SASL DIGEST-MD5}, and \term{SASL SCRAM-SHA-1}. - - \titem{scram} - 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 \term{plain} anymore. - This format allows clients to authenticate using: \term{SASL PLAIN} and \term{SASL SCRAM-SHA-1}. -\end{description} - -Examples: -\begin{itemize} -\item To use internal authentication on \jid{example.org} and LDAP - authentication on \jid{example.net}: -\begin{verbatim} -host_config: - "example.org": - auth_method: [internal] - "example.net": - auth_method: [ldap] -\end{verbatim} -\item To use internal authentication with hashed passwords on all virtual hosts: -\begin{verbatim} -auth_method: internal -auth_password_format: scram -\end{verbatim} -\end{itemize} - -\makesubsubsection{extauth}{External Script} -\ind{external authentication} - -In this authentication method, when \ejabberd{} starts, -it start a script, and calls it to perform authentication tasks. - -The server administrator can write the external authentication script -in any language. -The details on the interface between ejabberd and the script are described -in the \term{ejabberd Developers Guide}. -There are also \footahref{http://www.ejabberd.im/extauth}{several example authentication scripts}. - -These are the specific options: -\begin{description} - \titem{extauth\_program: PathToScript} - Indicate in this option the full path to the external authentication script. - The script must be executable by ejabberd. - - \titem{extauth\_instances: Integer} - Indicate how many instances of the script to run simultaneously to serve authentication in the virtual host. - The default value is the minimum number: 1. - - \titem{extauth\_cache: false|CacheTimeInteger} - The value \term{false} disables the caching feature, this is the default. - The integer \term{0} (zero) enables caching for statistics, but doesn't use that cached information to authenticate users. - If another integer value is set, caching is enabled both for statistics and for authentication: - the CacheTimeInteger indicates the number of seconds that ejabberd can reuse - the authentication information since the user last disconnected, - to verify again the user authentication without querying again the extauth script. - Note: caching should not be enabled in a host if internal auth is also enabled. - If caching is enabled, \term{mod\_last} must be enabled also in that vhost. -\end{description} - -This example sets external authentication, the extauth script, enables caching for 10 minutes, -and starts three instances of the script for each virtual host defined in ejabberd: -\begin{verbatim} -auth_method: [external] -extauth_program: "/etc/ejabberd/JabberAuth.class.php" -extauth_cache: 600 -extauth_instances: 3 -\end{verbatim} - - -\makesubsubsection{saslanonymous}{Anonymous Login and SASL Anonymous} -\ind{sasl anonymous}\ind{anonymous login} - -The \term{anonymous} authentication method enables two modes for anonymous authentication: -\begin{description} -\titem{Anonymous login:} This is a standard login, that use the - classical login and password mechanisms, but where password is - accepted or preconfigured for all anonymous users. This login is - compliant with SASL authentication, password and digest non-SASL - authentication, so this option will work with almost all XMPP - clients - -\titem{SASL Anonymous:} This is a special SASL authentication - mechanism that allows to login without providing username or - password (see \xepref{0175}). The main advantage of SASL Anonymous - is that the protocol was designed to give the user a login. This is - useful to avoid in some case, where the server has many users - already logged or registered and when it is hard to find a free - username. The main disavantage is that you need a client that - specifically supports the SASL Anonymous protocol. -\end{description} - -%TODO: introduction; tell what people can do with this -The anonymous authentication method can be configured with the following -options. Remember that you can use the \term{host\_config} option to set virtual -host specific options (see section~\ref{virtualhost}). - -\begin{description} -\titem{allow\_multiple\_connections: false|true} This option is only used - when the anonymous mode is - enabled. Setting it to \term{true} means that the same username can be taken - multiple times in anonymous login mode if different resource are used to - connect. This option is only useful in very special occasions. The default - value is \term{false}. -\titem{anonymous\_protocol: login\_anon | sasl\_anon | both} - \term{login\_anon} means that the anonymous login method will be used. - \term{sasl\_anon} means that the SASL Anonymous method will be used. - \term{both} means that SASL Anonymous and login anonymous are both enabled. -\end{description} - -Those options are defined for each virtual host with the \term{host\_config} -parameter (see section~\ref{virtualhost}). - -Examples: -\begin{itemize} -\item To enable anonymous login on all virtual hosts: -\begin{verbatim} -auth_method: [anonymous] -anonymous_protocol: login_anon -\end{verbatim} -\item Similar as previous example, but limited to \jid{public.example.org}: -\begin{verbatim} -host_config: - "public.example.org": - auth_method: [anonymous] - anonymous_protoco: login_anon -\end{verbatim} -\item To enable anonymous login and internal authentication on a virtual host: -\begin{verbatim} -host_config: - "public.example.org": - auth_method: - - internal - - anonymous - anonymous_protocol: login_anon -\end{verbatim} -\item To enable SASL Anonymous on a virtual host: -\begin{verbatim} -host_config: - "public.example.org": - auth_method: [anonymous] - anonymous_protocol: sasl_anon -\end{verbatim} -\item To enable SASL Anonymous and anonymous login on a virtual host: -\begin{verbatim} -host_config: - "public.example.org": - auth_method: [anonymous] - anonymous_protocol: both -\end{verbatim} -\item To enable SASL Anonymous, anonymous login, and internal authentication on -a virtual host: -\begin{verbatim} -host_config: - "public.example.org": - auth_method: - - internal - - anonymous - anonymous_protocol: both -\end{verbatim} -\end{itemize} - -There are more configuration examples and XMPP client example stanzas in -\footahref{http://www.ejabberd.im/Anonymous-users-support}{Anonymous users support}. - - -\makesubsubsection{pam}{PAM Authentication} -\ind{PAM authentication}\ind{Pluggable Authentication Modules} - -\ejabberd{} supports authentication via Pluggable Authentication Modules (PAM). -PAM is currently supported in AIX, FreeBSD, HP-UX, Linux, Mac OS X, NetBSD and Solaris. -PAM authentication is disabled by default, so you have to configure and compile -\ejabberd{} with PAM support enabled: -\begin{verbatim} -./configure --enable-pam && make install -\end{verbatim} - -Options: -\begin{description} -\titem{pam\_service: Name}\ind{options!pam\_service}This option defines the PAM service name. -Default is \term{"ejabberd"}. Refer to the PAM documentation of your operation system -for more information. -\titem{pam\_userinfotype: username|jid}\ind{options!pam\_userinfotype} -This option defines what type of information about the user ejabberd -provides to the PAM service: only the username, or the user JID. -Default is \term{username}. -\end{description} - -Example: -\begin{verbatim} -auth_method: [pam] -pam_service: "ejabberd" -\end{verbatim} - -Though it is quite easy to set up PAM support in \ejabberd{}, PAM itself introduces some -security issues: - -\begin{itemize} -\item To perform PAM authentication \ejabberd{} uses external C-program called -\term{epam}. By default, it is located in \verb|/var/lib/ejabberd/priv/bin/| -directory. You have to set it root on execution in the case when your PAM module -requires root privileges (\term{pam\_unix.so} for example). Also you have to grant access -for \ejabberd{} to this file and remove all other permissions from it. -Execute with root privileges: -\begin{verbatim} -chown root:ejabberd /var/lib/ejabberd/priv/bin/epam -chmod 4750 /var/lib/ejabberd/priv/bin/epam -\end{verbatim} -\item Make sure you have the latest version of PAM installed on your system. -Some old versions of PAM modules cause memory leaks. If you are not able to use the latest -version, you can \term{kill(1)} \term{epam} process periodically to reduce its memory -consumption: \ejabberd{} will restart this process immediately. -\item \term{epam} program tries to turn off delays on authentication failures. -However, some PAM modules ignore this behavior and rely on their own configuration options. -You can create a configuration file \term{ejabberd.pam}. -This example shows how to turn off delays in \term{pam\_unix.so} module: -\begin{verbatim} -#%PAM-1.0 -auth sufficient pam_unix.so likeauth nullok nodelay -account sufficient pam_unix.so -\end{verbatim} -That is not a ready to use configuration file: you must use it -as a hint when building your own PAM configuration instead. Note that if you want to disable -delays on authentication failures in the PAM configuration file, you have to restrict access -to this file, so a malicious user can't use your configuration to perform brute-force -attacks. -\item You may want to allow login access only for certain users. \term{pam\_listfile.so} -module provides such functionality. -\item If you use \term{pam\_winbind} to authorise against a Windows Active Directory, -then \term{/etc/nsswitch.conf} must be configured to use \term{winbind} as well. -\end{itemize} - -\makesubsection{accessrules}{Access Rules} -\ind{access rules}\ind{ACL}\ind{Access Control List} - -\makesubsubsection{ACLDefinition}{ACL Definition} -\ind{ACL}\ind{options!acl}\ind{ACL}\ind{Access Control List} - -Access control in \ejabberd{} is performed via Access Control Lists (ACLs). The -declarations of ACLs in the configuration file have the following syntax: -\esyntax{acl: \{ ACLName: \{ ACLType: ACLValue \} \}} - -\term{ACLType: ACLValue} can be one of the following: -\begin{description} -\titem{all} Matches all JIDs. Example: -\begin{verbatim} -acl: - world: all -\end{verbatim} -\titem{user: Username} Matches the user with the name - \term{Username} at the first virtual host. Example: -\begin{verbatim} -acl: - admin: - user: "yozhik" -\end{verbatim} -\titem{user: \{Username: Server\}} Matches the user with the JID - \term{Username@Server} and any resource. Example: -\begin{verbatim} -acl: - admin: - user: - "yozhik": "example.org" -\end{verbatim} -\titem{server: Server} Matches any JID from server - \term{Server}. Example: -\begin{verbatim} -acl: - exampleorg: - server: "example.org" -\end{verbatim} -\titem{resource: Resource} Matches any JID with a resource - \term{Resource}. Example: -\begin{verbatim} -acl: - mucklres: - resource: "muckl" -\end{verbatim} -\titem{shared\_group: Groupname} Matches any member of a Shared Roster Group with name \term{Groupname} in the virtual host. Example: -\begin{verbatim} -acl: - techgroupmembers: - shared_group: "techteam" -\end{verbatim} -\titem{shared\_group: \{Groupname: Server\}} Matches any member of a Shared Roster Group with name \term{Groupname} in the virtual host \term{Server}. Example: -\begin{verbatim} -acl: - techgroupmembers: - shared_group: - "techteam": "example.org" -\end{verbatim} -\titem{ip: Network} Matches any IP address from the \term{Network}. Example: -\begin{verbatim} -acl: - loopback: - ip: - - "127.0.0.0/8" - - "::" -\end{verbatim} -\titem{user\_regexp: Regexp} Matches any local user with a name that - matches \term{Regexp} on local virtual hosts. Example: -\begin{verbatim} -acl: - tests: - user_regexp: "^test[0-9]*$" -\end{verbatim} -%$ -\titem{user\_regexp: \{Regexp: Server\}} Matches any user with a name - that matches \term{Regexp} at server \term{Server}. Example: -\begin{verbatim} -acl: - tests: - user_regexp: - "^test": "example.org" -\end{verbatim} -\titem{server\_regexp: Regexp} Matches any JID from the server that - matches \term{Regexp}. Example: -\begin{verbatim} -acl: - icq: - server_regexp: "^icq\\." -\end{verbatim} -\titem{resource\_regexp: Regexp} Matches any JID with a resource that - matches \term{Regexp}. Example: -\begin{verbatim} -acl: - icq: - resource_regexp: "^laptop\\." -\end{verbatim} -\titem{node\_regexp: \{UserRegexp: ServerRegexp\}} Matches any user - with a name that matches \term{UserRegexp} at any server that matches - \term{ServerRegexp}. Example: -\begin{verbatim} -acl: - yozhik: - node_regexp: - "^yozhik$": "^example.(com|org)$" -\end{verbatim} -\titem{user\_glob: Glob\}} -\titem{user\_glob: \{Glob: Server\}} -\titem{server\_glob: Glob} -\titem{resource\_glob: Glob} -\titem{node\_glob: \{UserGlob: ServerGlob\}} This is the same as - above. However, it uses shell glob patterns instead of regexp. These patterns - can have the following special characters: - \begin{description} - \titem{*} matches any string including the null string. - \titem{?} matches any single character. - \titem{[...]} matches any of the enclosed characters. Character - ranges are specified by a pair of characters separated by a \term{`-'}. - If the first character after \term{`['} is a \term{`!'}, any - character not enclosed is matched. - \end{description} -\end{description} - -The following \term{ACLName} are pre-defined: -\begin{description} -\titem{all} Matches any JID. -\titem{none} Matches no JID. -\end{description} - -\makesubsubsection{AccessRights}{Access Rights} -\ind{access}\ind{ACL}\ind{options!acl}\ind{ACL}\ind{Access Control List} - -An entry allowing or denying access to different services. -The syntax is: -\esyntax{access: \{ AccessName: \{ ACLName: allow|deny \} \}} - -When a JID is checked to have access to \term{Accessname}, the server -sequentially checks if that JID matches any of the ACLs that are named in the -first elements of the tuples in the list. If it matches, the second element of -the first matched tuple is returned, otherwise the value `\term{deny}' is -returned. - -If you define specific Access rights in a virtual host, -remember that the globally defined Access rights have precedence over those. -This means that, in case of conflict, the Access granted or denied in the global server is used -and the Access of a virtual host doesn't have effect. - -Example: -\begin{verbatim} -access: - configure: - admin: allow - something - badmans: deny - all: allow -\end{verbatim} - -The following \term{AccessName} are pre-defined: -\begin{description} -\titem{all} Always returns the value `\term{allow}'. -\titem{none} Always returns the value `\term{deny}'. -\end{description} - -\makesubsubsection{configmaxsessions}{Limiting Opened Sessions with ACL} -\ind{options!max\_user\_sessions} - -The special access \term{max\_user\_sessions} specifies the maximum -number of sessions (authenticated connections) per user. If a user -tries to open more sessions by using different resources, the first -opened session will be disconnected. The error \term{session replaced} -will be sent to the disconnected session. The value for this option -can be either a number, or \term{infinity}. The default value is -\term{infinity}. - -The syntax is: -\esyntax{\{ max\_user\_sessions: \{ ACLName: MaxNumber \} \}} - -This example limits the number of sessions per user to 5 for all users, and to 10 for admins: -\begin{verbatim} -access: - max_user_sessions: - admin: 10 - all: 5 -\end{verbatim} - -\makesubsubsection{configmaxs2sconns}{Several connections to a remote XMPP server with ACL} -\ind{options!max\_s2s\_connections} - -The special access \term{max\_s2s\_connections} specifies how many -simultaneous S2S connections can be established to a specific remote XMPP server. -The default value is \term{1}. -There's also available the access \term{max\_s2s\_connections\_per\_node}. - -The syntax is: -\esyntax{\{ max\_s2s\_connections: \{ ACLName: MaxNumber \} \}} - -Examples: -\begin{itemize} -\item Allow up to 3 connections with each remote server: -\begin{verbatim} -access: - max_s2s_connections: - all: 3 -\end{verbatim} -\end{itemize} - -\makesubsection{shapers}{Shapers} -\ind{options!shaper}\ind{shapers}\ind{traffic speed} - -Shapers enable you to limit connection traffic. -The syntax is: -\esyntax{shaper: \{ ShaperName: Rate \}} -where \term{Rate} stands for the maximum allowed incoming rate in bytes per -second. -When a connection exceeds this limit, \ejabberd{} stops reading from the socket -until the average rate is again below the allowed maximum. - -Examples: -\begin{itemize} -\item To define a shaper named `\term{normal}' with traffic speed limited to -1,000\,bytes/second: -\begin{verbatim} -shaper: - normal: 1000 -\end{verbatim} -\item To define a shaper named `\term{fast}' with traffic speed limited to -50,000\,bytes/second: -\begin{verbatim} -shaper: - fast: 50000 -\end{verbatim} -\end{itemize} - -\makesubsection{language}{Default Language} -\ind{options!language}\ind{language} - -The option \option{language} defines the default language of server strings that -can be seen by \XMPP{} clients. If a \XMPP{} client does not support -\option{xml:lang}, the specified language is used. - -The option syntax is: -\esyntax{language: Language} - -The default value is \term{en}. -In order to take effect there must be a translation file -\term{Language.msg} in \ejabberd{}'s \term{msgs} directory. - -For example, to set Russian as default language: -\begin{verbatim} -language: "ru" -\end{verbatim} - -Appendix \ref{i18ni10n} provides more details about internationalization and localization. - - -\makesubsection{captcha}{CAPTCHA} -\ind{options!captcha}\ind{captcha} - -Some \ejabberd{} modules can be configured to require a CAPTCHA challenge on certain actions. -If the client does not support CAPTCHA Forms (\xepref{0158}), -a web link is provided so the user can fill the challenge in a web browser. - -An example script is provided that generates the image -using ImageMagick's Convert program. - -The configurable options are: -\begin{description} - \titem{captcha\_cmd: Path} - Full path to a script that generates the image. - The default value disables the feature: \term{undefined} - \titem{captcha\_host: ProtocolHostPort} - ProtocolHostPort is a string with the host, and optionally the Protocol and Port number. - It must identify where ejabberd listens for CAPTCHA requests. - The URL sent to the user is formed by: \term{Protocol://Host:Port/captcha/} - The default value is: protocol \term{http}, the first hostname configured, and port \term{80}. - If you specify a port number that does not match exactly an ejabberd listener - (because you are using a reverse proxy or other port-forwarding tool), - then you must specify the transfer protocol, as seen in the example below. -\end{description} - -Additionally, an \term{ejabberd\_http} listener must be enabled with the \term{captcha} option. -See section \ref{listened-module}. - -Example configuration: -\begin{verbatim} -hosts: ["example.org"] - -captcha_cmd: "/lib/ejabberd/priv/bin/captcha.sh" -captcha_host: "example.org:5280" -## captcha_host: "https://example.org:443" -## captcha_host: "http://example.com" - -listen: - ... - - - port: 5280 - module: ejabberd_http - captcha: true - ... -\end{verbatim} - -\makesubsection{stun}{STUN and TURN} -\ind{options!stun}\ind{stun} - -\ejabberd{} is able to act as a stand-alone STUN/TURN server -(\footahref{http://tools.ietf.org/html/rfc5389}{RFC 5389}/\footahref{http://tools.ietf.org/html/rfc5766}{RFC 5766}). In that role \ejabberd{} helps clients with ICE (\footahref{http://tools.ietf.org/html/rfc5245}{RFC 5245}) or Jingle ICE (\xepref{0176}) support to discover their external addresses and ports and to relay media traffic when it is impossible to establish direct -peer-to-peer connection. - -You should configure \term{ejabberd\_stun} listening module as described in \ref{listened} section. -The specific configurable options are: -\begin{description} - \titem{tls: true|false} - If enabled, \option{certfile} option must be set, otherwise \ejabberd{} - will not be able to accept TLS connections. Obviously, this option - makes sense for \term{tcp} transport only. The default is \term{false}. - \titem{certfile: Path} - Path to the certificate file. Only makes sense when \option{tls} is set. - \titem{use\_turn: true|false} - Enables/disables TURN (media relay) functionality. The default is \term{false}. - \titem{turn\_ip: String} - The IPv4 address advertised by your TURN server. The address should not be NAT'ed - or firewalled. There is not default, so you should set this option explicitly. - Implies \term{use\_turn}. - \titem{turn\_min\_port: Integer} - Together with \option{turn\_max\_port} forms port range to allocate from. - The default is 49152. Implies \term{use\_turn}. - \titem{turn\_max\_port: Integer} - Together with \option{turn\_min\_port} forms port range to allocate from. - The default is 65535. Implies \term{use\_turn}. - \titem{turn\_max\_allocations: Integer|infinity} - Maximum number of TURN allocations available from the particular IP address. - The default value is 10. Implies \term{use\_turn}. - \titem{turn\_max\_permissions: Integer|infinity} - Maximum number of TURN permissions available from the particular IP address. - The default value is 10. Implies \term{use\_turn}. - \titem{auth\_type: user|anonymous} - Which authentication type to use for TURN allocation requests. When type \term{user} - is set, ejabberd authentication backend is used. For \term{anonymous} type - no authentication is performed (not recommended for public services). - The default is \term{user}. Implies \term{use\_turn}. - \titem{auth\_realm: String} - When \option{auth\_type} is set to \term{user} and you have several virtual - hosts configured you should set this option explicitly to the virtual host - you want to serve on this particular listening port. Implies \term{use\_turn}. - \titem{shaper: Atom} - For \term{tcp} transports defines shaper to use. The default is \term{none}. - \titem{server\_name: String} - Defines software version to return with every response. The default is the - STUN library version. -\end{description} - -Example configuration with disabled TURN functionality (STUN only): -\begin{verbatim} -listen: - ... - - - port: 3478 - transport: udp - module: ejabberd_stun - - - port: 3478 - module: ejabberd_stun - - - port: 5349 - module: ejabberd_stun - certfile: "/etc/ejabberd/server.pem" - ... -\end{verbatim} - -Example configuration with TURN functionality. Note that STUN is always -enabled if TURN is enabled. Here, only UDP section is shown: -\begin{verbatim} -listen: - ... - - - port: 3478 - transport: udp - use_turn: true - turn_ip: "10.20.30.1" - module: ejabberd_stun - ... -\end{verbatim} - -You also need to configure DNS SRV records properly so clients can easily discover a -STUN/TURN server serving your XMPP domain. Refer to section -\footahref{http://tools.ietf.org/html/rfc5389\#section-9}{DNS Discovery of a Server} -of \footahref{http://tools.ietf.org/html/rfc5389}{RFC 5389} and section -\footahref{http://tools.ietf.org/html/rfc5766\#section-6}{Creating an Allocation} -of \footahref{http://tools.ietf.org/html/rfc5766}{RFC 5766} for details. - -Example DNS SRV configuration for STUN only: -\begin{verbatim} -_stun._udp IN SRV 0 0 3478 stun.example.com. -_stun._tcp IN SRV 0 0 3478 stun.example.com. -_stuns._tcp IN SRV 0 0 5349 stun.example.com. -\end{verbatim} - -And you should also add these in the case if TURN is enabled: -\begin{verbatim} -_turn._udp IN SRV 0 0 3478 turn.example.com. -_turn._tcp IN SRV 0 0 3478 turn.example.com. -_turns._tcp IN SRV 0 0 5349 turn.example.com. -\end{verbatim} - -\makesubsection{sip}{SIP} -\ind{options!sip}\ind{sip} - -\ejabberd{} has built-in SIP support. In order to activate it you need to add -listeners for it, configure DNS properly and enable \modsip{} for -the desired virtual host. - -To add a listener you should configure \term{ejabberd\_sip} listening module as -described in \ref{listened} section. If option \option{tls} is specified, option -\option{certfile} must be specified as well, otherwise incoming TLS connections would fail. - -Example configuration with standard ports -(as per \footahref{http://tools.ietf.org/html/rfc3261}{RFC 3261}): -\begin{verbatim} -listen: - ... - - - port: 5060 - transport: udp - module: ejabberd_sip - - - port: 5060 - module: ejabberd_sip - - - port: 5061 - module: ejabberd_sip - tls: true - certfile: "/etc/ejabberd/server.pem" - ... -\end{verbatim} - -Note that there is no StartTLS support in SIP and \footahref{http://en.wikipedia.org/wiki/Server\_Name\_Indication}{SNI} support is somewhat tricky, so for TLS you have to configure -different virtual hosts on different ports if you have different certificate files for them. - -Next you need to configure DNS SIP records for your virtual domains. -Refer to \footahref{http://tools.ietf.org/html/rfc3263}{RFC 3263} for the detailed explanation. -Simply put, you should add NAPTR and SRV records for your domains. -Skip NAPTR configuration if your DNS provider doesn't support this type of records. -It's not fatal, however, highly recommended. - -Example configuration of NAPTR records: -\begin{verbatim} -example.com IN NAPTR 10 0 "s" "SIPS+D2T" "" _sips._tcp.example.com. -example.com IN NAPTR 20 0 "s" "SIP+D2T" "" _sip._tcp.example.com. -example.com IN NAPTR 30 0 "s" "SIP+D2U" "" _sip._udp.example.com. -\end{verbatim} - -Example configuration of SRV records with standard ports -(as per \footahref{http://tools.ietf.org/html/rfc3261}{RFC 3261}): -\begin{verbatim} -_sip._udp IN SRV 0 0 5060 sip.example.com. -_sip._tcp IN SRV 0 0 5060 sip.example.com. -_sips._tcp IN SRV 0 0 5061 sip.example.com. -\end{verbatim} - -\makesubsection{includeconfigfile}{Include Additional Configuration Files} -\ind{options!includeconfigfile}\ind{includeconfigfile} - -The option \option{include\_config\_file} in a configuration file instructs \ejabberd{} to include other configuration files immediately. - -The basic syntax is: -\esyntax{include\_config\_file: [Filename]} -It is possible to specify suboptions using the full syntax: -\esyntax{include\_config\_file: \{ Filename: [Suboption, ...] \}} - -The filename can be indicated either as an absolute path, -or relative to the main \ejabberd{} configuration file. -It isn't possible to use wildcards. -The file must exist and be readable. - -The allowed suboptions are: -\begin{description} - \titem{disallow: [Optionname, ...]} Disallows the usage of those options in the included configuration file. - The options that match this criteria are not accepted. - The default value is an empty list: \term{[]} - \titem{allow\_only: [Optionname, ...]} Allows only the usage of those options in the included configuration file. - The options that do not match this criteria are not accepted. - The default value is: \term{all} -\end{description} - -This is a basic example: -\begin{verbatim} -include_config_file: "/etc/ejabberd/additional.yml" -\end{verbatim} - -In this example, the included file is not allowed to contain a \term{listen} option. -If such an option is present, the option will not be accepted. -The file is in a subdirectory from where the main configuration file is. -\begin{verbatim} -include_config_file: - "./example.org/additional_not_listen.yml": - disallow: [listen] -\end{verbatim} - -In this example, \term{ejabberd.yml} defines some ACL and Access rules, -and later includes another file with additional rules: -\begin{verbatim} -acl: - admin: - user: - - "admin": "localhost" -access: - announce: - admin: allow -include_config_file: - "/etc/ejabberd/acl_and_access.yml": - allow_only: - - acl - - access -\end{verbatim} -and content of the file \term{acl\_and\_access.yml} can be, for example: -\begin{verbatim} -acl: - admin: - user: - - "bob": "localhost" - - "jan": "localhost" -\end{verbatim} - - -\makesubsection{optionmacros}{Option Macros in Configuration File} -\ind{options!optionmacros}\ind{optionmacros} - -In the \ejabberd{} configuration file, -it is possible to define a macro for a value -and later use this macro when defining an option. - -A macro is defined with this syntax: -\esyntax{define\_macro: \{ 'MACRO': Value \}} -The \term{MACRO} must be surrounded by single quotation marks, -and all letters in uppercase; check the examples bellow. -The \term{value} can be any valid arbitrary Erlang term. - -The first definition of a macro is preserved, -and additional definitions of the same macro are forgotten. - -Macros are processed after -additional configuration files have been included, -so it is possible to use macros that -are defined in configuration files included before the usage. - -It isn't possible to use a macro in the definition -of another macro. - -This example shows the basic usage of a macro: -\begin{verbatim} -define_macro: - 'LOG_LEVEL_NUMBER': 5 -loglevel: 'LOG_LEVEL_NUMBER' -\end{verbatim} -The resulting option interpreted by \ejabberd{} is: \term{loglevel: 5}. - -This example shows that values can be any arbitrary Erlang term: -\begin{verbatim} -define_macro: - 'USERBOB': - user: - - "bob": "localhost" -acl: - admin: 'USERBOB' -\end{verbatim} -The resulting option interpreted by \ejabberd{} is: -\begin{verbatim} -acl: - admin: - user: - - "bob": "localhost" -\end{verbatim} -This complex example: -\begin{verbatim} -define_macro: - 'NUMBER_PORT_C2S': 5222 - 'NUMBER_PORT_HTTP': 5280 -listen: - - - port: 'NUMBER_PORT_C2S' - module: ejabberd_c2s - - - port: 'NUMBER_PORT_HTTP' - module: ejabberd_http -\end{verbatim} -produces this result after being interpreted: -\begin{verbatim} -listen: - - - port: 5222 - module: ejabberd_c2s - - - port: 5280 - module: ejabberd_http -\end{verbatim} -\makesection{database}{Database and LDAP Configuration} -\ind{database} -%TODO: this whole section is not yet 100% optimized - -\ejabberd{} uses its internal Mnesia database by default. However, it is -possible to use a relational database, key-value storage or an LDAP server to store persistent, -long-living data. \ejabberd{} is very flexible: you can configure different -authentication methods for different virtual hosts, you can configure different -authentication mechanisms for the same virtual host (fallback), you can set -different storage systems for modules, and so forth. - -The following databases are supported by \ejabberd{}: -\begin{itemize} -%%\item \footahref{http://www.microsoft.com/sql/}{Microsoft SQL Server} -\item \footahref{http://www.erlang.org/doc/apps/mnesia/index.html}{Mnesia} -\item \footahref{http://www.mysql.com/}{MySQL} -\item \footahref{http://en.wikipedia.org/wiki/Open\_Database\_Connectivity}{Any ODBC compatible database} -\item \footahref{http://www.postgresql.org/}{PostgreSQL} -\item \footahref{http://basho.com/riak/}{Riak} -\end{itemize} - -The following LDAP servers are tested with \ejabberd{}: -\begin{itemize} -\item \footahref{http://www.microsoft.com/activedirectory/}{Active Directory} - (see section~\ref{ad}) -\item \footahref{http://www.openldap.org/}{OpenLDAP} -\item \footahref{http://www.communigate.com/}{CommuniGate Pro} -\item Normally any LDAP compatible server should work; inform us about your - success with a not-listed server so that we can list it here. -\end{itemize} - -Important note about virtual hosting: -if you define several domains in ejabberd.yml (see section \ref{hostnames}), -you probably want that each virtual host uses a different configuration of database, authentication and storage, -so that usernames do not conflict and mix between different virtual hosts. -For that purpose, the options described in the next sections -must be set inside a \term{host\_config} for each vhost (see section \ref{virtualhost}). -For example: -\begin{verbatim} -host_config: - "public.example.org": - odbc_type: pgsql - odbc_server: "localhost" - odbc_database: "database-public-example-org" - odbc_username: "ejabberd" - odbc_password: "password" - auth_method: [odbc] -\end{verbatim} - - -\makesubsection{odbc}{ODBC}\ind{odbc} - -The actual database access is defined in the options with \term{odbc\_} prefix. The -values are used to define if we want to use ODBC, or one of the two native -interface available, PostgreSQL or MySQL. - -The following paramaters are available: -\begin{description} - \titem{odbc\_type: mysql | pgsql | odbc} The type of an ODBC connection. - The default is \term{odbc}. - \titem{odbc\_server: String} A hostname of the ODBC server. The default is - \term{``localhost''}. - \titem{odbc\_port: Port} The port where the ODBC server is accepting connections. - The option is only valid for \term{mysql} and \term{pgsql}. The default is - \term{3306} and \term{5432} respectively. - \titem{odbc\_database: String} The database name. The default is \term{``ejabberd''}. - The option is only valid for \term{mysql} and \term{pgsql}. - \titem{odbc\_username: String} The username. The default is \term{``ejabberd''}. - The option is only valid for \term{mysql} and \term{pgsql}. - \titem{odbc\_password: String} The password. The default is empty string. - The option is only valid for \term{mysql} and \term{pgsql}. - \titem{odbc\_pool\_size: N} By default \ejabberd{} opens 10 connections to - the database for each virtual host. You can change this number by using this option. - \titem{odbc\_keepalive\_interval: N} You can configure an interval to - make a dummy SQL request to keep alive the connections to the database. - The default value is 'undefined', so no keepalive requests are made. - Specify in seconds: for example 28800 means 8 hours. - \titem{odbc\_start\_interval: N} If the connection to the database fails, - \ejabberd{} waits 30 seconds before retrying. - You can modify this interval with this option. -\end{description} - -Example of plain ODBC connection: -\begin{verbatim} -odbc_server: "DSN=database;UID=ejabberd;PWD=password" -\end{verbatim} - -Example of MySQL connection: -\begin{verbatim} -odbc_type: mysql -odbc_server: "server.company.com" -odbc_port: 3306 # the default -odbc_database: "mydb" -odbc_username: "user1" -odbc_password: "**********" -odbc_pool_size: 5 -\end{verbatim} - -\makesubsubsection{odbcstorage}{Storage} -\ind{ODBC!storage} - -An ODBC compatible database also can be used to store information into from -several \ejabberd{} -modules. See section~\ref{modoverview} to see which modules can be used with -relational databases like MySQL. To enable storage to your database, just make -sure that your database is running well (see previous sections), and add the -module option \term{db\_type: odbc}. - -\makesubsection{ldap}{LDAP} -\ind{databases!LDAP} - -\ejabberd{} has built-in LDAP support. You can authenticate users against LDAP -server and use LDAP directory as vCard storage. - -Usually \ejabberd{} treats LDAP as a read-only storage: -it is possible to consult data, but not possible to -create accounts or edit vCard that is stored in LDAP. -However, it is possible to change passwords if \module{mod\_register} module is enabled -and LDAP server supports -\footahref{http://tools.ietf.org/html/rfc3062}{RFC 3062}. - - -\makesubsubsection{ldapconnection}{Connection} - -Two connections are established to the LDAP server per vhost, -one for authentication and other for regular calls. - -Parameters: -\begin{description} -\titem{ldap\_servers: [Servers, ...]} \ind{options!ldap\_server}List of IP addresses or DNS names of your -LDAP servers. This option is required. -\titem{ldap\_encrypt: none|tls} \ind{options!ldap\_encrypt}Type of connection encryption to the LDAP server. -Allowed values are: \term{none}, \term{tls}. -The value \term{tls} enables encryption by using LDAP over SSL. -Note that STARTTLS encryption is not supported. -The default value is: \term{none}. -\titem{ldap\_tls\_verify: false|soft|hard} \ind{options!ldap\_tls\_verify} -This option specifies whether to verify LDAP server certificate or not when TLS is enabled. -When \term{hard} is enabled \ejabberd{} doesn't proceed if a certificate is invalid. -When \term{soft} is enabled \ejabberd{} proceeds even if check fails. -The default is \term{false} which means no checks are performed. -\titem{ldap\_tls\_cacertfile: Path} \ind{options!ldap\_tls\_cacertfile} -Path to file containing PEM encoded CA certificates. This option is needed -(and required) when TLS verification is enabled. -\titem{ldap\_tls\_depth: Number} \ind{options!ldap\_tls\_depth} -Specifies the maximum verification depth when TLS verification is enabled, -i.e. how far in a chain of certificates the verification process can proceed -before the verification is considered to fail. -Peer certificate = 0, CA certificate = 1, higher level CA certificate = 2, etc. -The value 2 thus means that a chain can at most contain peer cert, -CA cert, next CA cert, and an additional CA cert. The default value is 1. -\titem{ldap\_port: Number} \ind{options!ldap\_port}Port to connect to your LDAP server. -The default port is~389 if encryption is disabled; and 636 if encryption is enabled. -If you configure a value, it is stored in \ejabberd{}'s database. -Then, if you remove that value from the configuration file, -the value previously stored in the database will be used instead of the default port. -\titem{ldap\_rootdn: RootDN} \ind{options!ldap\_rootdn}Bind DN. The default value - is~\term{""} which means `anonymous connection'. -\titem{ldap\_password: Password} \ind{options!ldap\_password}Bind password. The default - value is \term{""}. -\titem{ldap\_deref\_aliases: never|always|finding|searching} \ind{options!ldap\_deref\_aliases} Whether or not to dereference aliases. The default is \term{never}. -\end{description} - -Example: -\begin{verbatim} -auth_method: [ldap] -ldap_servers: - - "ldap1.example.org" -ldap_port: 389 -ldap_rootdn: "cn=Manager,dc=domain,dc=org" -ldap_password: "**********" -\end{verbatim} - -\makesubsubsection{ldapauth}{Authentication} - -You can authenticate users against an LDAP directory. -Note that current LDAP implementation does not support SASL authentication. - -Available options are: - -\begin{description} -\titem{ldap\_base: Base}\ind{options!ldap\_base}LDAP base directory which stores - users accounts. This option is required. - \titem{ldap\_uids: [ ldap\_uidattr | \{ldap\_uidattr: ldap\_uidattr\_format\} ]}\ind{options!ldap\_uids} - LDAP attribute which holds a list of attributes to use as alternatives for getting the JID. - The default attributes are \term{[\{"uid", "\%u"\}]}. - The attributes are of the form: - \term{[\{ldap\_uidattr\}]} or \term{[\{ldap\_uidattr, ldap\_uidattr\_format\}]}. - You can use as many comma separated attributes as needed. - The values for \term{ldap\_uidattr} and - \term{ldap\_uidattr\_format} are described as follow: - \begin{description} - \titem{ldap\_uidattr}\ind{options!ldap\_uidattr}LDAP attribute which holds - the user's part of a JID. The default value is \term{"uid"}. - \titem{ldap\_uidattr\_format}\ind{options!ldap\_uidattr\_format}Format of - the \term{ldap\_uidattr} variable. The format \emph{must} contain one and - only one pattern variable \term{"\%u"} which will be replaced by the - user's part of a JID. For example, \term{"\%u@example.org"}. The default - value is \term{"\%u"}. - \end{description} - \titem{ldap\_filter: Filter}\ind{options!ldap\_filter}\ind{protocols!RFC 4515: - LDAP String Representation of Search Filters} - \footahref{http://tools.ietf.org/html/rfc4515}{RFC 4515} LDAP filter. The - default Filter value is: \term{undefined}. Example: - \term{"(\&(objectClass=shadowAccount)(memberOf=Jabber Users))"}. Please, do - not forget to close brackets and do not use superfluous whitespaces. Also you - \emph{must not} use \option{ldap\_uidattr} attribute in filter because this - attribute will be substituted in LDAP filter automatically. - \titem{ldap\_dn\_filter: \{ Filter: FilterAttrs \}}\ind{options!ldap\_dn\_filter} - This filter is applied on the results returned by the main filter. This filter - performs additional LDAP lookup to make the complete result. This is useful - when you are unable to define all filter rules in \term{ldap\_filter}. You - can define \term{"\%u"}, \term{"\%d"}, \term{"\%s"} and \term{"\%D"} pattern - variables in Filter: \term{"\%u"} is replaced by a user's part of a JID, - \term{"\%d"} is replaced by the corresponding domain (virtual host), - all \term{"\%s"} variables are consecutively replaced by values of FilterAttrs - attributes and \term{"\%D"} is replaced by Distinguished Name. By default - \term{ldap\_dn\_filter} is undefined. - Example: -\begin{verbatim} -ldap_dn_filter: - "(&(name=%s)(owner=%D)(user=%u@%d))": ["sn"] -\end{verbatim} - Since this filter makes additional LDAP lookups, use it only in the - last resort: try to define all filter rules in \term{ldap\_filter} if possible. - \titem{\{ldap\_local\_filter, Filter\}}\ind{options!ldap\_local\_filter} - If you can't use \term{ldap\_filter} due to performance reasons - (the LDAP server has many users registered), - you can use this local filter. - The local filter checks an attribute in ejabberd, - not in LDAP, so this limits the load on the LDAP directory. - The default filter is: \term{undefined}. - Example values: -\begin{verbatim} -{ldap_local_filter, {notequal, {"accountStatus",["disabled"]}}}. -{ldap_local_filter, {equal, {"accountStatus",["enabled"]}}}. -{ldap_local_filter, undefined}. -\end{verbatim} - -\end{description} - -\makesubsubsection{ldapexamples}{Examples} - -\makeparagraph{ldapcommonexample}{Common example} - -Let's say \term{ldap.example.org} is the name of our LDAP server. We have -users with their passwords in \term{"ou=Users,dc=example,dc=org"} directory. -Also we have addressbook, which contains users emails and their additional -infos in \term{"ou=AddressBook,dc=example,dc=org"} directory. -The connection to the LDAP server is encrypted using TLS, -and using the custom port 6123. -Corresponding authentication section should looks like this: - -\begin{verbatim} -## Authentication method -auth_method: [ldap] -## DNS name of our LDAP server -ldap_servers: ["ldap.example.org"] -## Bind to LDAP server as "cn=Manager,dc=example,dc=org" with password "secret" -ldap_rootdn: "cn=Manager,dc=example,dc=org" -ldap_password: "secret" -ldap_encrypt: tls -ldap_port: 6123 -## Define the user's base -ldap_base: "ou=Users,dc=example,dc=org" -## We want to authorize users from 'shadowAccount' object class only -ldap_filter: "(objectClass=shadowAccount)" -\end{verbatim} - -Now we want to use users LDAP-info as their vCards. We have four attributes -defined in our LDAP schema: \term{"mail"} --- email address, \term{"givenName"} ---- first name, \term{"sn"} --- second name, \term{"birthDay"} --- birthday. -Also we want users to search each other. Let's see how we can set it up: - -\begin{verbatim} -modules: - ... - mod_vcard_ldap: - ## We use the same server and port, but want to bind anonymously because - ## our LDAP server accepts anonymous requests to - ## "ou=AddressBook,dc=example,dc=org" subtree. - ldap_rootdn: "" - ldap_password: "" - ## define the addressbook's base - ldap_base: "ou=AddressBook,dc=example,dc=org" - ## uidattr: user's part of JID is located in the "mail" attribute - ## uidattr_format: common format for our emails - ldap_uids: - "mail": "%u@mail.example.org" - ## We have to define empty filter here, because entries in addressbook does not - ## belong to shadowAccount object class - ldap_filter: "" - ## Now we want to define vCard pattern - ldap_vcard_map: - "NICKNAME": {"%u": []} # just use user's part of JID as his nickname - "GIVEN": {"%s": ["givenName"]} - "FAMILY": {"%s": ["sn"]} - "FN": {"%s, %s": ["sn", "givenName"]}, # example: "Smith, John" - "EMAIL": {"%s": ["mail"]} - "BDAY": {"%s": ["birthDay"]}]} - ## Search form - ldap_search_fields: - "User": "%u" - "Name": "givenName" - "Family Name": "sn" - "Email": "mail" - "Birthday": "birthDay" - ## vCard fields to be reported - ## Note that JID is always returned with search results - ldap_search_reported: - "Full Name": "FN" - "Nickname": "NICKNAME" - "Birthday": "BDAY" - ... -\end{verbatim} - -Note that \modvcardldap{} module checks for the existence of the user before -searching in his information in LDAP. - - -\makeparagraph{ad}{Active Directory} -\ind{databases!Active Directory} - -Active Directory is just an LDAP-server with predefined attributes. A sample -configuration is shown below: - -\begin{verbatim} -auth_method: [ldap] -ldap_servers: ["office.org"] # List of LDAP servers -ldap_base: "DC=office,DC=org" # Search base of LDAP directory -ldap_rootdn: "CN=Administrator,CN=Users,DC=office,DC=org" # LDAP manager -ldap_password: "*******" # Password to LDAP manager -ldap_uids: ["sAMAccountName"] -ldap_filter: "(memberOf=*)" - -modules: - ... - mod_vcard_ldap: - ldap_vcard_map: - "NICKNAME": {"%u", []} - "GIVEN": {"%s", ["givenName"]} - "MIDDLE": {"%s", ["initials"]} - "FAMILY": {"%s", ["sn"]} - "FN": {"%s", ["displayName"]} - "EMAIL": {"%s", ["mail"]} - "ORGNAME": {"%s", ["company"]} - "ORGUNIT": {"%s", ["department"]} - "CTRY": {"%s", ["c"]} - "LOCALITY": {"%s", ["l"]} - "STREET": {"%s", ["streetAddress"]} - "REGION": {"%s", ["st"]} - "PCODE": {"%s", ["postalCode"]} - "TITLE": {"%s", ["title"]} - "URL": {"%s", ["wWWHomePage"]} - "DESC": {"%s", ["description"]} - "TEL": {"%s", ["telephoneNumber"]}]} - ldap_search_fields: - "User": "%u" - "Name": "givenName" - "Family Name": "sn" - "Email": "mail" - "Company": "company" - "Department": "department" - "Role": "title" - "Description": "description" - "Phone": "telephoneNumber" - ldap_search_reported: - "Full Name": "FN" - "Nickname": "NICKNAME" - "Email": "EMAIL" - ... -\end{verbatim} - -\makesubsection{riak}{Riak} -\ind{databases!Riak} - -\footahref{http://basho.com/riak/}{Riak} is a distributed NoSQL key-value data store. -The actual database access is defined in the options with \term{riak\_} prefix. - -\makesubsubsection{riakconnection}{Connection} -\ind{riak!connection} - -The following paramaters are available: -\begin{description} - \titem{riak\_server: String} A hostname of the Riak server. The default is - \term{``localhost''}. - \titem{riak\_port: Port} The port where the Riak server is accepting connections. - The defalt is 8087. - \titem{riak\_pool\_size: N} By default \ejabberd{} opens 10 connections to - the Riak server. You can change this number by using this option. - \titem{riak\_start\_interval: N} If the connection to the Riak server fails, - \ejabberd{} waits 30 seconds before retrying. - You can modify this interval with this option. -\end{description} - -Example configuration: -\begin{verbatim} -riak_server: "riak.server.com" -riak_port: 9097 -\end{verbatim} - -\makesubsubsection{riakstorage}{Storage} -\ind{riak!storage} - -Several \ejabberd{} modules can be used to store information in Riak database. -Refer to the corresponding module documentation to see if it supports such -ability. To enable storage to Riak database, just make -sure that your database is running well (see the next section), and add the -module option \term{db\_type: riak}. - -\makesubsubsection{riakconfiguration}{Riak Configuration} -\ind{riak!configuration} - -First, you need to configure Riak to use -\footahref{http://en.wikipedia.org/wiki/LevelDB}{LevelDB} as a database backend. - -If you are using Riak 2.x and higher, configure \term{storage\_backend} option -of \term{/etc/riak/riak.conf} as follows: -\begin{verbatim} -... -storage_backend = leveldb -... -\end{verbatim} - -If you are using Riak 1.4.x and older, configure \term{storage\_backend} option -of \term{/etc/riak/app.config} in the section \term{riak\_kv} as follows: -\begin{verbatim} -... - {riak_kv, [ - ... - {storage_backend, riak_kv_eleveldb_backend}, -... -\end{verbatim} - -Second, Riak should be pointed to \ejabberd{} Erlang binary files (*.beam). -As described in \ref{install}, by default those are located -in \term{/lib/ejabberd/ebin} directory. So you -should add the following to \term{/etc/riak/vm.args}: -\begin{verbatim} -... -## Path to ejabberd beams in order to make map/reduce --pz /lib/ejabberd/ebin -... -\end{verbatim} -Important notice: make sure Riak has at least read access to that directory. -Otherwise its startup will likely fail. - -\makesection{modules}{Modules Configuration} -\ind{modules} - -The option \term{modules} defines the list of modules that will be loaded after -\ejabberd{}'s startup. Each entry in the list is a tuple in which the first -element is the name of a module and the second is a list of options for that -module. - -The syntax is: -\esyntax{modules: \{ ModuleName: ModuleOptions \}} - -Examples: -\begin{itemize} -\item In this example only the module \modecho{} is loaded and no module - options are specified between the square brackets: -\begin{verbatim} -modules: - mod_echo: {} -\end{verbatim} -\item In the second example the modules \modecho{}, \modtime{}, and - \modversion{} are loaded without options. -\begin{verbatim} -modules: - mod_echo: {} - mod_time: {} - mod_version: {} -\end{verbatim} -\end{itemize} - -\makesubsection{modoverview}{Modules Overview} -\ind{modules!overview}\ind{XMPP compliancy} - -The following table lists all modules included in \ejabberd{}. - -\begin{table}[H] - \centering - \begin{tabular}{|l|l|l|} - \hline {\bf Module} & {\bf Feature} & {\bf Dependencies} \\ - \hline - \hline \modadhoc{} & Ad-Hoc Commands (\xepref{0050}) & \\ - \hline \ahrefloc{modannounce}{\modannounce{}} & Manage announcements & recommends \modadhoc{} \\ - \hline \modblocking{} & Simple Communications Blocking (\xepref{0191}) & \modprivacy{} \\ - \hline \modcaps{} & Entity Capabilities (\xepref{0115}) & \\ - \hline \modcarboncopy{} & Message Carbons (\xepref{0280}) & \\ - \hline \ahrefloc{modclientstate}{\modclientstate{}} & Filter stanzas for inactive clients & \\ - \hline \modconfigure{} & Server configuration using Ad-Hoc & \modadhoc{} \\ - \hline \ahrefloc{moddisco}{\moddisco{}} & Service Discovery (\xepref{0030}) & \\ - \hline \ahrefloc{modecho}{\modecho{}} & Echoes XMPP stanzas & \\ - \hline \ahrefloc{modfail2ban}{\modfailban{}} & Bans IPs that show the malicious signs & \\ - \hline \ahrefloc{modhttpbind}{\modhttpbind{}} & XMPP over Bosh service (HTTP Binding) & \\ - \hline \ahrefloc{modhttpfileserver}{\modhttpfileserver{}} & Small HTTP file server & \\ - \hline \ahrefloc{modirc}{\modirc{}} & IRC transport & \\ - \hline \ahrefloc{modlast}{\modlast{}} & Last Activity (\xepref{0012}) & \\ - \hline \ahrefloc{modmuc}{\modmuc{}} & Multi-User Chat (\xepref{0045}) & \\ - \hline \ahrefloc{modmuclog}{\modmuclog{}} & Multi-User Chat room logging & \modmuc{} \\ - \hline \ahrefloc{modoffline}{\modoffline{}} & Offline message storage (\xepref{0160}) & \\ - \hline \ahrefloc{modping}{\modping{}} & XMPP Ping and periodic keepalives (\xepref{0199}) & \\ - \hline \ahrefloc{modprescounter}{\modprescounter{}} & Detect presence subscription flood & \\ - \hline \ahrefloc{modprivacy}{\modprivacy{}} & Blocking Communication (\xepref{0016}) & \\ - \hline \ahrefloc{modprivate}{\modprivate{}} & Private XML Storage (\xepref{0049}) & \\ - \hline \ahrefloc{modproxy}{\modproxy{}} & SOCKS5 Bytestreams (\xepref{0065}) & \\ - \hline \ahrefloc{modpubsub}{\modpubsub{}} & Pub-Sub (\xepref{0060}), PEP (\xepref{0163}) & \modcaps{} \\ - \hline \ahrefloc{modpubsub}{\modpubsubodbc{}} & Pub-Sub (\xepref{0060}), PEP (\xepref{0163}) & supported DB (*) and \modcaps{} \\ - \hline \ahrefloc{modregister}{\modregister{}} & In-Band Registration (\xepref{0077}) & \\ - \hline \ahrefloc{modregisterweb}{\modregisterweb{}} & Web for Account Registrations & \\ - \hline \ahrefloc{modroster}{\modroster{}} & Roster management (XMPP IM) & \\ - \hline \ahrefloc{modservicelog}{\modservicelog{}} & Copy user messages to logger service & \\ - \hline \ahrefloc{modsharedroster}{\modsharedroster{}} & Shared roster management & \modroster{} \\ - \hline \ahrefloc{modsharedrosterldap}{\modsharedrosterldap{}} & LDAP Shared roster management & \modroster{} \\ - \hline \ahrefloc{modsic}{\modsic{}} & Server IP Check (\xepref{0279}) & \\ - \hline \ahrefloc{modsip}{\modsip{}} & SIP Registrar/Proxy (\footahref{http://tools.ietf.org/html/rfc3261}{RFC 3261}) & \term{ejabberd\_sip} \\ - \hline \ahrefloc{modstats}{\modstats{}} & Statistics Gathering (\xepref{0039}) & \\ - \hline \ahrefloc{modtime}{\modtime{}} & Entity Time (\xepref{0202}) & \\ - \hline \ahrefloc{modvcard}{\modvcard{}} & vcard-temp (\xepref{0054}) & \\ - \hline \ahrefloc{modvcardldap}{\modvcardldap{}} & vcard-temp (\xepref{0054}) & LDAP server \\ - \hline \ahrefloc{modvcardxupdate}{\modvcardxupdate{}} & vCard-Based Avatars (\xepref{0153}) & \modvcard{} \\ - \hline \ahrefloc{modversion}{\modversion{}} & Software Version (\xepref{0092}) & \\ - \hline - \end{tabular} -\end{table} - -\begin{itemize} -\item (*) This module requires a supported database. For a list of supported databases, see section~\ref{database}. -\end{itemize} - -You can see which database backend each module needs by looking at the suffix: -\begin{itemize} -\item No suffix, this means that the module uses Erlang's built-in database - Mnesia as backend, Riak key-value store or ODBC database (see~\ref{database}). -\item `\_ldap', this means that the module needs an LDAP server as backend. -\end{itemize} - -You can find more -\footahref{http://www.ejabberd.im/contributions}{contributed modules} on the -\ejabberd{} website. Please remember that these contributions might not work or -that they can contain severe bugs and security leaks. Therefore, use them at -your own risk! - - -\makesubsection{modcommonoptions}{Common Options} - -The following options are used by many modules. Therefore, they are described in -this separate section. - -\makesubsubsection{modiqdiscoption}{\option{iqdisc}} -\ind{options!iqdisc} - -Many modules define handlers for processing IQ queries of different namespaces -to this server or to a user (e.\,g.\ to \jid{example.org} or to -\jid{user@example.org}). This option defines processing discipline for -these queries. - -The syntax is: -\esyntax{iqdisc: Value} - -Possible \term{Value} are: -\begin{description} -\titem{no\_queue} All queries of a namespace with this processing discipline are - processed directly. This means that the XMPP connection that sends this IQ query gets blocked: - no other packets can be processed - until this one has been completely processed. Hence this discipline is not - recommended if the processing of a query can take a relatively long time. -\titem{one\_queue} In this case a separate queue is created for the processing - of IQ queries of a namespace with this discipline. In addition, the processing - of this queue is done in parallel with that of other packets. This discipline - is most recommended. -\titem{N} N separate queues are created to process the - queries. The queries are thus processed in parallel, but in a - controlled way. -\titem{parallel} For every packet with this discipline a separate Erlang process - is spawned. Consequently, all these packets are processed in parallel. - Although spawning of Erlang process has a relatively low cost, this can break - the server's normal work, because the Erlang emulator has a limit on the - number of processes (32000 by default). -\end{description} - -Example: -\begin{verbatim} -modules: - ... - mod_time: - iqdisc: no_queue - ... -\end{verbatim} - -\makesubsubsection{modhostoption}{\option{host}} -\ind{options!host} - -This option defines the Jabber ID of a service provided by an \ejabberd{} module. - -The syntax is: -\esyntax{host: HostName} - -If you include the keyword "@HOST@" in the HostName, -it is replaced at start time with the real virtual host string. - -This example configures -the \ind{modules!\modecho{}}echo module to provide its echoing service -in the Jabber ID \jid{mirror.example.org}: -\begin{verbatim} -modules: - ... - mod_echo: - host: "mirror.example.org" - ... -\end{verbatim} - -However, if there are several virtual hosts and this module is enabled in all of them, -the "@HOST@" keyword must be used: -\begin{verbatim} -modules: - ... - mod_echo: - host: "mirror.@HOST@" - ... -\end{verbatim} - -\makesubsection{modannounce}{\modannounce{}} -\ind{modules!\modannounce{}}\ind{MOTD}\ind{message of the day}\ind{announcements} - -This module enables configured users to broadcast announcements and to set -the message of the day (MOTD). -Configured users can perform these actions with a -\XMPP{} client either using Ad-hoc commands -or sending messages to specific JIDs. - -The Ad-hoc commands are listed in the Server Discovery. -For this feature to work, \modadhoc{} must be enabled. - -The specific JIDs where messages can be sent are listed bellow. -The first JID in each entry will apply only to the specified virtual host -\jid{example.org}, while the JID between brackets will apply to all virtual -hosts in ejabberd. -\begin{description} -\titem{example.org/announce/all (example.org/announce/all-hosts/all)} The - message is sent to all registered users. If the user is online and connected - to several resources, only the resource with the highest priority will receive - the message. If the registered user is not connected, the message will be - stored offline in assumption that \ind{modules!\modoffline{}}offline storage - (see section~\ref{modoffline}) is enabled. -\titem{example.org/announce/online (example.org/announce/all-hosts/online)}The - message is sent to all connected users. If the user is online and connected - to several resources, all resources will receive the message. -\titem{example.org/announce/motd (example.org/announce/all-hosts/motd)}The - message is set as the message of the day (MOTD) and is sent to users when they - login. In addition the message is sent to all connected users (similar to - \term{announce/online}). -\titem{example.org/announce/motd/update (example.org/announce/all-hosts/motd/update)} - The message is set as message of the day (MOTD) and is sent to users when they - login. The message is \emph{not sent} to any currently connected user. -\titem{example.org/announce/motd/delete (example.org/announce/all-hosts/motd/delete)} - Any message sent to this JID removes the existing message of the day (MOTD). -\end{description} - -Options: -\begin{description} -\dbtype -\titem{access: AccessName} \ind{options!access}This option specifies who is allowed to - send announcements and to set the message of the day (by default, nobody is - able to send such messages). -\end{description} - -Examples: -\begin{itemize} -\item Only administrators can send announcements: -\begin{verbatim} -access: - announce: - admin: allow - -modules: - ... - mod_adhoc: {} - mod_announce: - access: announce - ... -\end{verbatim} -\item Administrators as well as the direction can send announcements: -\begin{verbatim} -acl: - direction: - user: - "big_boss": "example.org" - "assistant": "example.org" - admin: - user: - "admin": "example.org" -access: - announce: - admin: allow - direction: allow - -modules: - ... - mod_adhoc: {} - mod_announce: - access: announce - ... -\end{verbatim} -\end{itemize} - -Note that \modannounce{} can be resource intensive on large -deployments as it can broadcast lot of messages. This module should be -disabled for instances of \ejabberd{} with hundreds of thousands users. - -\makesubsection{modclientstate}{\modclientstate{}} -\ind{modules!\modclientstate{}}\ind{Client State Indication} -\ind{protocols!XEP-0352: Client State Indication} - -This module allows for queueing or dropping certain types of stanzas -when a client indicates that the user is not actively using the client -at the moment (see \xepref{0352}). This can save bandwidth and -resources. - -Options: -\begin{description} -\titem{drop\_chat\_states: true|false} \ind{options!drop\_chat\_states} - Drop most "standalone" Chat State Notifications (as defined in - \xepref{0085}) while a client indicates inactivity. The default value - is \term{false}. -\titem{queue\_presence: true|false} \ind{options!queue\_presence} - While a client is inactive, queue presence stanzas that indicate - (un)availability. The latest queued stanza of each contact is - delivered as soon as the client becomes active again. The default - value is \term{false}. -\end{description} - -Example: -\begin{verbatim} -modules: - ... - mod_client_state: - drop_chat_states: true - queue_presence: true - ... -\end{verbatim} - -\makesubsection{moddisco}{\moddisco{}} -\ind{modules!\moddisco{}} -\ind{protocols!XEP-0030: Service Discovery} -\ind{protocols!XEP-0011: Jabber Browsing} -\ind{protocols!XEP-0094: Agent Information} -\ind{protocols!XEP-0157: Contact Addresses for XMPP Services} - -This module adds support for Service Discovery (\xepref{0030}). With -this module enabled, services on your server can be discovered by -\XMPP{} clients. Note that \ejabberd{} has no modules with support -for the superseded Jabber Browsing (\xepref{0011}) and Agent Information -(\xepref{0094}). Accordingly, \XMPP{} clients need to have support for -the newer Service Discovery protocol if you want them be able to discover -the services you offer. - -Options: -\begin{description} -\iqdiscitem{Service Discovery (\ns{http://jabber.org/protocol/disco\#items} and - \ns{http://jabber.org/protocol/disco\#info})} -\titem{extra\_domains: [Domain, ...]} \ind{options!extra\_domains}With this option, - you can specify a list of extra domains that are added to the Service Discovery item list. -\titem{server\_info: [ \{ modules: Modules, name: Name, urls: [URL, ...] \} ]} \ind{options!server\_info} - Specify additional information about the server, - as described in Contact Addresses for XMPP Services (\xepref{0157}). - \term{Modules} can be the keyword `all', - in which case the information is reported in all the services; - or a list of \ejabberd{} modules, - in which case the information is only specified for the services provided by those modules. - Any arbitrary \term{Name} and \term{URL} can be specified, not only contact addresses. -\end{description} - -Examples: -\begin{itemize} -\item To serve a link to the Jabber User Directory on \jid{jabber.org}: -\begin{verbatim} -modules: - ... - mod_disco: - extra_domains: ["users.jabber.org"] - ... -\end{verbatim} -\item To serve a link to the transports on another server: -\begin{verbatim} -modules: - ... - mod_disco: - extra_domains: - - "icq.example.com" - - "msn.example.com" - ... -\end{verbatim} -\item To serve a link to a few friendly servers: -\begin{verbatim} -modules: - ... - mod_disco: - extra_domains: - - "example.org" - - "example.com" - ... -\end{verbatim} -\item With this configuration, all services show abuse addresses, -feedback address on the main server, -and admin addresses for both the main server and the vJUD service: -\begin{verbatim} -modules: - ... - mod_disco: - server_info: - - - modules: all - name: "abuse-addresses" - urls: ["mailto:abuse@shakespeare.lit"] - - - modules: [mod_muc] - name: "Web chatroom logs" - urls: ["http://www.example.org/muc-logs"] - - - modules: [mod_disco] - name: "feedback-addresses" - urls: - - "http://shakespeare.lit/feedback.php" - - "mailto:feedback@shakespeare.lit" - - "xmpp:feedback@shakespeare.lit" - - - modules: - - mod_disco - - mod_vcard - name: "admin-addresses" - urls: - - "mailto:xmpp@shakespeare.lit" - - "xmpp:admins@shakespeare.lit" - ... -\end{verbatim} -\end{itemize} - -\makesubsection{modecho}{\modecho{}} -\ind{modules!\modecho{}}\ind{debugging} - -This module simply echoes any \XMPP{} -packet back to the sender. This mirror can be of interest for -\ejabberd{} and \XMPP{} client debugging. - -Options: -\begin{description} -\hostitem{echo} -\end{description} - -Example: Mirror, mirror, on the wall, who is the most beautiful - of them all? -\begin{verbatim} -modules: - ... - mod_echo: - host: "mirror.example.org" - ... -\end{verbatim} - -\makesubsection{modfail2ban}{\modfailban{}} -\ind{modules!\modfailban{}}\ind{modfail2ban} - -The module bans IPs that show the malicious signs. Currently only C2S authentication -failures are detected. - -Available options: -\begin{description} - \titem{c2s\_auth\_ban\_lifetime: Seconds} The lifetime of the IP ban caused by too - many C2S authentication failures. The default is 3600, i.e. one hour. - \titem{c2s\_max\_auth\_failures: Integer} The number of C2S authentication failures to - trigger the IP ban. The default is 20. -\end{description} - -Example: -\begin{verbatim} -modules: - ... - mod_fail2ban: - c2s_auth_block_lifetime: 7200 - c2s_max_auth_failures: 50 - ... -\end{verbatim} - -\makesubsection{modhttpbind}{\modhttpbind{}} -\ind{modules!\modhttpbind{}}\ind{modhttpbind} - -This module implements XMPP over Bosh (formerly known as HTTP Binding) -as defined in \xepref{0124} and \xepref{0206}. -It extends ejabberd's built in HTTP service with a configurable -resource at which this service will be hosted. - -To use HTTP-Binding, enable the module: -\begin{verbatim} -modules: - ... - mod_http_bind: {} - ... -\end{verbatim} -and add \verb|http_bind| in the HTTP service. For example: -\begin{verbatim} -listen: - ... - - - port: 5280 - module: ejabberd_http - http_bind: true - http_poll: true - web_admin: true - ... -\end{verbatim} -With this configuration, the module will serve the requests sent to -\verb|http://example.org:5280/http-bind/| -Remember that this page is not designed to be used by web browsers, -it is used by XMPP clients that support XMPP over Bosh. - -If you want to set the service in a different URI path or use a different module, -you can configure it manually using the option \verb|request_handlers|. -For example: -\begin{verbatim} -listen: - ... - - - port: 5280 - module: ejabberd_http - request_handlers: - "/http-bind": mod_http_bind - http_poll: true - web_admin: true - ... -\end{verbatim} - -Options: -\begin{description} - \titem{\{max\_inactivity, Seconds\}} \ind{options!max\_inactivity} - Define the maximum inactivity period in seconds. - Default value is 30 seconds. - For example, to set 50 seconds: -\begin{verbatim} -modules: - ... - mod_http_bind: - max_inactivity: 50 - ... -\end{verbatim} -\end{description} - - -\makesubsection{modhttpfileserver}{\modhttpfileserver{}} -\ind{modules!\modhttpfileserver{}}\ind{modhttpfileserver} - -This simple module serves files from the local disk over HTTP. - -Options: -\begin{description} - \titem{docroot: Path} \ind{options!docroot} - Directory to serve the files. - \titem{accesslog: Path} \ind{options!accesslog} - File to log accesses using an Apache-like format. - No log will be recorded if this option is not specified. - \titem{directory\_indices: [Index, ...]} \ind{options!directoryindices} - Indicate one or more directory index files, similarly to Apache's - DirectoryIndex variable. When a web request hits a directory - instead of a regular file, those directory indices are looked in - order, and the first one found is returned. - \titem{custom\_headers: \{Name: Value\}} \ind{options!customheaders} - Indicate custom HTTP headers to be included in all responses. - Default value is: \term{[]} - \titem{content\_types: \{Name: Type\}} \ind{options!contenttypes} - Specify mappings of extension to content type. - There are several content types already defined, - with this option you can add new definitions, modify or delete existing ones. - To delete an existing definition, simply define it with a value: `undefined'. - \titem{default\_content\_type: Type} \ind{options!defaultcontenttype} - Specify the content type to use for unknown extensions. - Default value is `application/octet-stream'. -\end{description} - -This example configuration will serve the files from -the local directory \verb|/var/www| -in the address \verb|http://example.org:5280/pub/archive/|. -In this example a new content type \term{ogg} is defined, -\term{png} is redefined, and \term{jpg} definition is deleted. -To use this module you must enable it: -\begin{verbatim} -modules: - ... - mod_http_fileserver: - docroot: "/var/www" - accesslog: "/var/log/ejabberd/access.log" - directory_indices: - - "index.html" - - "main.htm" - custom_headers: - "X-Powered-By": "Erlang/OTP" - "X-Fry": "It's a widely-believed fact!" - content_types: - ".ogg": "audio/ogg" - ".png": "image/png" - ".jpg": undefined - default_content_type: "text/html" - ... -\end{verbatim} -And define it as a handler in the HTTP service: -\begin{verbatim} -listen: - ... - - - port: 5280 - module: ejabberd_http - request_handlers: - ... - "/pub/archive": mod_http_fileserver - ... - ... -\end{verbatim} - -\makesubsection{modirc}{\modirc{}} -\ind{modules!\modirc{}}\ind{IRC} - -This module is an IRC transport that can be used to join channels on IRC -servers. - -End user information: -\ind{protocols!groupchat 1.0}\ind{protocols!XEP-0045: Multi-User Chat} -\begin{itemize} -\item A \XMPP{} client with `groupchat 1.0' support or Multi-User - Chat support (\xepref{0045}) is necessary to join IRC channels. -\item An IRC channel can be joined in nearly the same way as joining a - \XMPP{} Multi-User Chat room. The difference is that the room name will - be `channel\%\jid{irc.example.org}' in case \jid{irc.example.org} is - the IRC server hosting `channel'. And of course the host should point - to the IRC transport instead of the Multi-User Chat service. -\item You can register your nickame by sending `IDENTIFY password' to \\ - \jid{nickserver!irc.example.org@irc.jabberserver.org}. -\item Entering your password is possible by sending `LOGIN nick password' \\ - to \jid{nickserver!irc.example.org@irc.jabberserver.org}. -\item The IRC transport provides Ad-Hoc Commands (\xepref{0050}) - to join a channel, and to set custom IRC username and encoding. -\item When using a popular \XMPP{} server, it can occur that no - connection can be achieved with some IRC servers because they limit the - number of connections from one IP. -\end{itemize} - -Options: -\begin{description} -\hostitem{irc} -\dbtype -\titem{access: AccessName} \ind{options!access}This option can be used to specify who - may use the IRC transport (default value: \term{all}). -\titem{default\_encoding: Encoding} \ind{options!defaultencoding}Set the default IRC encoding. - Default value: \term{"iso8859-1"} -\end{description} - -Examples: -\begin{itemize} -\item In the first example, the IRC transport is available on (all) your - virtual host(s) with the prefix `\jid{irc.}'. Furthermore, anyone is - able to use the transport. The default encoding is set to "iso8859-15". -\begin{verbatim} -modules: - ... - mod_irc: - access: all - default_encoding: "iso8859-15" - ... -\end{verbatim} -\item In next example the IRC transport is available with JIDs with prefix \jid{irc-t.net}. - Moreover, the transport is only accessible to two users - of \term{example.org}, and any user of \term{example.com}: -\begin{verbatim} -acl: - paying_customers: - user: - - "customer1": "example.org" - - "customer2": "example.org" - server: "example.com" - -access: - irc_users: - paying_customers: allow - all: deny - -modules: - ... - mod_irc: - access: irc_users - host: "irc.example.net" - ... -\end{verbatim} -\end{itemize} - -\makesubsection{modlast}{\modlast{}} -\ind{modules!\modlast{}}\ind{protocols!XEP-0012: Last Activity} - -This module adds support for Last Activity (\xepref{0012}). It can be used to -discover when a disconnected user last accessed the server, to know when a -connected user was last active on the server, or to query the uptime of the -\ejabberd{} server. - -Options: -\begin{description} -\iqdiscitem{Last activity (\ns{jabber:iq:last})} -\dbtype -\end{description} - -\makesubsection{modmuc}{\modmuc{}} -\ind{modules!\modmuc{}}\ind{protocols!XEP-0045: Multi-User Chat}\ind{conferencing} - -This module provides a Multi-User Chat (\xepref{0045}) service. -Users can discover existing rooms, join or create them. -Occupants of a room can chat in public or have private chats. - -Some of the features of Multi-User Chat: -\begin{itemize} -\item Sending public and private messages to room occupants. -\item Inviting other users to a room. -\item Setting a room subject. -\item Creating password protected rooms. -\item Kicking and banning occupants. -\end{itemize} - -The MUC service allows any Jabber ID to register a nickname, -so nobody else can use that nickname in any room in the MUC service. -To register a nickname, open the Service Discovery in your -XMPP client and register in the MUC service. - -This module supports clustering and load -balancing. One module can be started per cluster node. Rooms are -distributed at creation time on all available MUC module -instances. The multi-user chat module is clustered but the rooms -themselves are not clustered nor fault-tolerant: if the node managing a -set of rooms goes down, the rooms disappear and they will be recreated -on an available node on first connection attempt. - -Module options: -\begin{description} -\hostitem{conference} -\dbtype -\titem{access: AccessName} \ind{options!access}You can specify who is allowed to use - the Multi-User Chat service. By default everyone is allowed to use it. -\titem{access\_create: AccessName} \ind{options!access\_create}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. -\titem{access\_persistent: AccessName} \ind{options!access\_persistent}To configure who is - allowed to modify the 'persistent' room option. - By default any account in the local ejabberd server is allowed to modify that option. -\titem{access\_admin: AccessName} \ind{options!access\_admin}This option specifies - who is allowed to administrate the Multi-User Chat service. The default - value is \term{none}, which means that only the room creator can - administer his room. - The administrators can send a normal message to the service JID, - and it will be shown in all active rooms as a service message. - The administrators can send a groupchat message to the JID of an active room, - and the message will be shown in the room as a service message. -\titem{history\_size: Size} \ind{options!history\_size}A small history of - the current discussion is sent to users when they enter the - room. With this option you can define the number of history messages - to keep and send to users joining the room. The value is an - integer. Setting the value to \term{0} disables the history feature - and, as a result, nothing is kept in memory. The default value is - \term{20}. This value is global and thus affects all rooms on the - service. -\titem{max\_users: Number} \ind{options!max\_users} This option defines at - the service level, the maximum number of users allowed per - room. It can be lowered in each room configuration but cannot be - increased in individual room configuration. The default value is - 200. -\titem{max\_users\_admin\_threshold: Number} - \ind{options!max\_users\_admin\_threshold} This option defines the - number of service admins or room owners allowed to enter the room when - the maximum number of allowed occupants was reached. The default limit - is 5. -\titem{max\_user\_conferences: Number} - \ind{options!max\_user\_conferences} This option defines the maximum - number of rooms that any given user can join. The default value - is 10. This option is used to prevent possible abuses. Note that - this is a soft limit: some users can sometimes join more conferences - in cluster configurations. -\titem{max\_room\_id: Number} \ind{options!max\_room\_id} - This option defines the maximum number of characters that Room ID - can have when creating a new room. - The default value is to not limit: \term{infinity}. -\titem{max\_room\_name: Number} \ind{options!max\_room\_name} - This option defines the maximum number of characters that Room Name - can have when configuring the room. - The default value is to not limit: \term{infinity}. -\titem{max\_room\_desc: Number} \ind{options!max\_room\_desc} - This option defines the maximum number of characters that Room Description - can have when configuring the room. - The default value is to not limit: \term{infinity}. -\titem{min\_message\_interval: Number} \ind{options!min\_message\_interval} - This option defines the minimum interval between two messages send - by an occupant in seconds. This option is global and valid for all - rooms. A decimal value can be used. When this option is not defined, - message rate is not limited. This feature can be used to protect a - MUC service from occupant abuses and limit number of messages that will - be broadcasted by the service. A good value for this minimum message - interval is 0.4 second. If an occupant tries to send messages faster, an - error is send back explaining that the message has been discarded - and describing the reason why the message is not acceptable. -\titem{min\_presence\_interval: Number} - \ind{options!min\_presence\_interval} This option defines the - minimum of time between presence changes coming from a given occupant in - seconds. This option is global and valid for all rooms. A - decimal value can be used. When this option is not defined, no - restriction is applied. This option can be used to protect a MUC - service for occupants abuses. If an occupant tries - to change its presence more often than the specified interval, the - presence is cached by \ejabberd{} and only the last presence is - broadcasted to all occupants in the room after expiration of the - interval delay. Intermediate presence packets are silently - discarded. A good value for this option is 4 seconds. -\titem{default\_room\_options: \{OptionName: OptionValue\}} \ind{options!default\_room\_options} - This module option allows to define the desired default room options. - Note that the creator of a room can modify the options of his room - at any time using an XMPP client with MUC capability. - The available room options and the default values are: - \begin{description} - \titem{allow\_change\_subj: true|false} Allow occupants to change the subject. - \titem{allow\_private\_messages: true|false} Occupants can send private messages to other occupants. - \titem{allow\_private\_messages\_from\_visitors: anyone|moderators|nobody} Visitors can send private messages to other occupants. - \titem{allow\_query\_users: true|false} Occupants can send IQ queries to other occupants. - \titem{allow\_user\_invites: false|true} Allow occupants to send invitations. - \titem{allow\_visitor\_nickchange: true|false} Allow visitors to - change nickname. - \titem{allow\_visitor\_status: true|false} Allow visitors to send - status text in presence updates. If disallowed, the \term{status} - text is stripped before broadcasting the presence update to all - the room occupants. - \titem{anonymous: true|false} The room is anonymous: - occupants don't see the real JIDs of other occupants. - Note that the room moderators can always see the real JIDs of the occupants. - \titem{captcha\_protected: false} - When a user tries to join a room where he has no affiliation (not owner, admin or member), - the room requires him to fill a CAPTCHA challenge (see section \ref{captcha}) - in order to accept her join in the room. - \titem{logging: false|true} The public messages are logged using \term{mod\_muc\_log}. - \titem{max\_users: 200} Maximum number of occupants in the room. - \titem{members\_by\_default: true|false} The occupants that enter the room are participants by default, so they have 'voice'. - \titem{members\_only: false|true} Only members of the room can enter. - \titem{moderated: true|false} Only occupants with 'voice' can send public messages. - \titem{password: "roompass123"} Password of the room. You may want to enable the next option too. - \titem{password\_protected: false|true} The password is required to enter the room. - \titem{persistent: false|true} The room persists even if the last participant leaves. - \titem{public: true|false} The room is public in the list of the MUC service, so it can be discovered. - \titem{public\_list: true|false} The list of participants is public, without requiring to enter the room. - \titem{title: "Room Title"} A human-readable title of the room. - \end{description} - All of those room options can be set to \term{true} or \term{false}, - except \term{password} and \term{title} which are strings, - and \term{max\_users} that is integer. -\end{description} - -Examples: -\begin{itemize} -\item In the first example everyone is allowed to use the Multi-User Chat - service. Everyone will also be able to create new rooms but only the user - \jid{admin@example.org} is allowed to administrate any room. In this - example he is also a global administrator. When \jid{admin@example.org} - sends a message such as `Tomorrow, the \XMPP{} server will be moved - to new hardware. This will involve service breakdowns around 23:00 UMT. - We apologise for this inconvenience.' to \jid{conference.example.org}, - it will be displayed in all active rooms. In this example the history - feature is disabled. -\begin{verbatim} -acl: - admin: - user: - - "admin": "example.org" - -access: - muc_admin: - admin: allow - -modules: - ... - mod_muc: - access: all - access_create: all - access_admin: muc_admin - history_size: 0 - ... -\end{verbatim} -\item In the second example the Multi-User Chat service is only accessible by - paying customers registered on our domains and on other servers. Of course - the administrator is also allowed to access rooms. In addition, he is the - only authority able to create and administer rooms. When - \jid{admin@example.org} sends a message such as `Tomorrow, the \Jabber{} - server will be moved to new hardware. This will involve service breakdowns - around 23:00 UMT. We apologise for this inconvenience.' to - \jid{conference.example.org}, it will be displayed in all active rooms. No - \term{history\_size} option is used, this means that the feature is enabled - and the default value of 20 history messages will be send to the users. -\begin{verbatim} -acl: - paying_customers: - user: - - "customer1": "example.net" - - "customer2": "example.com" - - "customer3": "example.org" - admin: - user: - - "admin": "example.org" - -access: - muc_admin - admin: allow - all: deny - muc_access: - paying_customers: allow - admin: allow - all: deny - -modules: - ... - mod_muc: - access: muc_access - access_create: muc_admin - access_admin: muc_admin - ... -\end{verbatim} - -\item In the following example, MUC anti abuse options are used. An -occupant cannot send more than one message every 0.4 seconds and cannot -change its presence more than once every 4 seconds. -The length of Room IDs and Room Names are limited to 20 characters, -and Room Description to 300 characters. No ACLs are -defined, but some user restriction could be added as well: - -\begin{verbatim} -modules: - ... - mod_muc: - min_message_interval: 0.4 - min_presence_interval: 4 - max_room_id: 20 - max_room_name: 20 - max_room_desc: 300 - ... -\end{verbatim} - -\item This example shows how to use \option{default\_room\_options} to make sure - the newly created rooms have by default those options. -\begin{verbatim} -modules: - ... - mod_muc: - access: muc_access - access_create: muc_admin - default_room_options: - allow_change_subj: false - allow_query_users: true - allow_private_messages: true - members_by_default: false - title: "New chatroom" - anonymous: false - access_admin: muc_admin - ... -\end{verbatim} -\end{itemize} - -\makesubsection{modmuclog}{\modmuclog{}} -\ind{modules!\modmuclog{}} - -This module enables optional logging of Multi-User Chat (MUC) public conversations to -HTML. Once you enable this module, users can join a room using a MUC capable -XMPP client, and if they have enough privileges, they can request the -configuration form in which they can set the option to enable room logging. - -Features: -\begin{itemize} -\item Room details are added on top of each page: room title, JID, - author, subject and configuration. -\item \ind{protocols!RFC 5122: Internationalized Resource Identifiers (IRIs) and Uniform Resource Identifiers (URIs) for the Extensible Messaging and Presence Protocol (XMPP)} - The room JID in the generated HTML is a link to join the room (using - \footahref{http://xmpp.org/rfcs/rfc5122.html}{XMPP URI}). -\item Subject and room configuration changes are tracked and displayed. -\item Joins, leaves, nick changes, kicks, bans and `/me' are tracked and - displayed, including the reason if available. -\item Generated HTML files are XHTML 1.0 Transitional and CSS compliant. -\item Timestamps are self-referencing links. -\item Links on top for quicker navigation: Previous day, Next day, Up. -\item CSS is used for style definition, and a custom CSS file can be used. -\item URLs on messages and subjects are converted to hyperlinks. -\item Timezone used on timestamps is shown on the log files. -\item A custom link can be added on top of each page. -\end{itemize} - -Options: -\begin{description} -\titem{access\_log: AccessName}\ind{options!access\_log} - This option restricts which occupants are allowed to enable or disable room - logging. The default value is \term{muc\_admin}. Note for this default setting - you need to have an access rule for \term{muc\_admin} in order to take effect. -\titem{cssfile: false|URL}\ind{options!cssfile} - With this option you can set whether the HTML files should have a custom CSS - file or if they need to use the embedded CSS file. Allowed values are - \term{false} and an URL to a CSS file. With the first value, HTML files will - include the embedded CSS code. With the latter, you can specify the URL of the - custom CSS file (for example: \term{"http://example.com/my.css"}). The default value - is \term{false}. -\titem{dirname: room\_jid|room\_name}\ind{options!dirname} - Allows to configure the name of the room directory. - Allowed values are \term{room\_jid} and \term{room\_name}. - With the first value, the room directory name will be the full room JID. - With the latter, the room directory name will be only the room name, - not including the MUC service name. - The default value is \term{room\_jid}. -\titem{dirtype: subdirs|plain}\ind{options!dirtype} - The type of the created directories can be specified with this option. Allowed - values are \term{subdirs} and \term{plain}. With the first value, - subdirectories are created for each year and month. With the latter, the - names of the log files contain the full date, and there are no subdirectories. - The default value is \term{subdirs}. -\titem{file\_format: html|plaintext}\ind{options!file\_format} - Define the format of the log files: - \term{html} stores in HTML format, - \term{plaintext} stores in plain text. - The default value is \term{html}. -\titem{file\_permissions: \{mode: Mode, group: Group\}}\ind{options!file\_permissions} - Define the permissions that must be used when creating the log files: - the number of the mode, and the numeric id of the group that will own the files. - The default value is \term{\{644, 33\}}. -\titem{outdir: Path}\ind{options!outdir} - This option sets the full path to the directory in which the HTML files should - be stored. Make sure the \ejabberd{} daemon user has write access on that - directory. The default value is \term{"www/muc"}. -\titem{spam\_prevention: true|false}\ind{options!spam\_prevention} - To prevent spam, the \term{spam\_prevention} option adds a special attribute - to links that prevent their indexation by search engines. The default value - is \term{true}, which mean that nofollow attributes will be added to user - submitted links. -\titem{timezone: local|universal}\ind{options!timezone} - The time zone for the logs is configurable with this option. Allowed values - are \term{local} and \term{universal}. With the first value, the local time, - as reported to Erlang by the operating system, will be used. With the latter, - GMT/UTC time will be used. The default value is \term{local}. -\titem{top\_link: \{URL: Text\}}\ind{options!top\_link} - With this option you can customize the link on the top right corner of each - log file. The default value is \term{\{"/", "Home"\}}. -\end{description} - -Examples: -\begin{itemize} -\item In the first example any room owner can enable logging, and a - custom CSS file will be used (http://example.com/my.css). The names - of the log files will contain the full date, and there will be no - subdirectories. The log files will be stored in /var/www/muclogs, and the - time zone will be GMT/UTC. Finally, the top link will be - \verb|Jabber.ru|. -\begin{verbatim} -access: - muc: - all: allow - -modules: - ... - mod_muc_log: - access_log: muc - cssfile: "http://example.com/my.css" - dirtype: plain - dirname: room_jid - outdir: "/var/www/muclogs" - timezone: universal - spam_prevention: true - top_link: - "http://www.jabber.ru/": "Jabber.ru" - ... -\end{verbatim} - \item In the second example only \jid{admin1@example.org} and - \jid{admin2@example.net} can enable logging, and the embedded CSS file will be - used. The names of the log files will only contain the day (number), - and there will be subdirectories for each year and month. The log files will - be stored in /var/www/muclogs, and the local time will be used. Finally, the - top link will be the default \verb|Home|. -\begin{verbatim} -acl: - admin: - user: - - "admin1": "example.org" - - "admin2": "example.net" -access: - muc_log: - admin: allow - all: deny - -modules: - ... - mod_muc_log: - access_log: muc_log - cssfile: false - dirtype: subdirs - file_permissions: - mode: 644 - group: 33 - outdir: "/var/www/muclogs" - timezone: local - ... -\end{verbatim} -\end{itemize} - -\makesubsection{modoffline}{\modoffline{}} -\ind{modules!\modoffline{}} - -This module implements offline message storage (\xepref{0160}). -This means that all messages -sent to an offline user will be stored on the server until that user comes -online again. Thus it is very similar to how email works. Note that -\term{ejabberdctl}\ind{ejabberdctl} has a command to delete expired messages -(see section~\ref{ejabberdctl}). - -\begin{description} - \dbtype - \titem{access\_max\_user\_messages: AccessName}\ind{options!access\_max\_user\_messages} - This option defines which access rule will be enforced to limit - the maximum number of offline messages that a user can have (quota). - When a user has too many offline messages, any new messages that he receive are discarded, - and a resource-constraint error is returned to the sender. - The default value is \term{max\_user\_offline\_messages}. - Then you can define an access rule with a syntax similar to - \term{max\_user\_sessions} (see \ref{configmaxsessions}). - \titem{store\_empty\_body: true|false}\ind{options!store\_empty\_body} Whether or not - to store messages with empty \term{} element. The default value is \term{true}. -\end{description} - -This example allows power users to have as much as 5000 offline messages, -administrators up to 2000, -and all the other users up to 100. -\begin{verbatim} -acl: - admin: - user: - - "admin1": "localhost" - - "admin2": "example.org" - poweruser: - user: - - "bob": "example.org" - - "jane": "example.org" - -access: - max_user_offline_messages: - poweruser: 5000 - admin: 2000 - all: 100 - -modules: - ... - mod_offline: - access_max_user_messages: max_user_offline_messages - ... -\end{verbatim} - -\makesubsection{modping}{\modping{}} -\ind{modules!\modping{}} - -This module implements support for XMPP Ping (\xepref{0199}) and periodic keepalives. -When this module is enabled ejabberd responds correctly to -ping requests, as defined in the protocol. - -Configuration options: -\begin{description} - \titem{send\_pings: true|false}\ind{options!send\_pings} - If this option is set to \term{true}, the server sends pings to connected clients - that are not active in a given interval \term{ping\_interval}. - This is useful to keep client connections alive or checking availability. - By default this option is disabled. - % because it is mostly not needed and consumes resources. - \titem{ping\_interval: Seconds}\ind{options!ping\_interval} - How often to send pings to connected clients, if the previous option is enabled. - If a client connection does not send or receive any stanza in this interval, - a ping request is sent to the client. - The default value is 60 seconds. - \titem{timeout\_action: none|kill}\ind{options!timeout\_action} - What to do when a client does not answer to a server ping request in less than 32 seconds. - % Those 32 seconds are defined in ejabberd_local.erl: -define(IQ_TIMEOUT, 32000). - The default is to do nothing. -\end{description} - -This example enables Ping responses, configures the module to send pings -to client connections that are inactive for 4 minutes, -and if a client does not answer to the ping in less than 32 seconds, its connection is closed: -\begin{verbatim} -modules: - ... - mod_ping: - send_pings: true - ping_interval: 240 - timeout_action: kill - ... -\end{verbatim} - -\makesubsection{modprescounter}{\modprescounter{}} -\ind{modules!\modprescounter{}} - -This module detects flood/spam in presence subscription stanza traffic. -If a user sends or receives more of those stanzas in a time interval, -the exceeding stanzas are silently dropped, and warning is logged. - -Configuration options: -\begin{description} - \titem{count: StanzaNumber}\ind{options!count} - The number of subscription presence stanzas - (subscribe, unsubscribe, subscribed, unsubscribed) - allowed for any direction (input or output) - per time interval. - Please note that two users subscribing to each other usually generate - 4 stanzas, so the recommended value is 4 or more. - The default value is: 5. - \titem{interval: Seconds}\ind{options!interval} - The time interval defined in seconds. - The default value is 60. -\end{description} - -This example enables the module, and allows up to 5 presence subscription stanzas -to be sent or received by the users in 60 seconds: -\begin{verbatim} -modules: - ... - mod_pres_counter: - count: 5 - interval: 60 - ... -\end{verbatim} - -\makesubsection{modprivacy}{\modprivacy{}} -\ind{modules!\modprivacy{}}\ind{Blocking Communication}\ind{Privacy Rules}\ind{protocols!RFC 3921: XMPP IM} - -This module implements \footahref{http://xmpp.org/rfcs/rfc3921.html\#privacy}{Blocking Communication} -(also known as Privacy Rules). -If end users have support for it in their \XMPP{} client, they will be able to: -\begin{quote} -\begin{itemize} -\item Retrieving one's privacy lists. -\item Adding, removing, and editing one's privacy lists. -\item Setting, changing, or declining active lists. -\item Setting, changing, or declining the default list (i.e., the list that - is active by default). -\item Allowing or blocking messages based on JID, group, or subscription type - (or globally). -\item Allowing or blocking inbound presence notifications based on JID, group, - or subscription type (or globally). -\item Allowing or blocking outbound presence notifications based on JID, group, - or subscription type (or globally). -\item Allowing or blocking IQ stanzas based on JID, group, or subscription type - (or globally). -\item Allowing or blocking all communications based on JID, group, or - subscription type (or globally). -\end{itemize} -(from \ahrefurl{http://xmpp.org/rfcs/rfc3921.html\#privacy}) -\end{quote} - -Options: -\begin{description} -\iqdiscitem{Blocking Communication (\ns{jabber:iq:privacy})} -\dbtype -\end{description} - -\makesubsection{modprivate}{\modprivate{}} -\ind{modules!\modprivate{}}\ind{protocols!XEP-0049: Private XML Storage}\ind{protocols!XEP-0048: Bookmark Storage} - -This module adds support for Private XML Storage (\xepref{0049}): -\begin{quote} -Using this method, XMPP entities can store private data on the server and -retrieve it whenever necessary. The data stored might be anything, as long as -it is valid XML. One typical usage for this namespace is the server-side storage -of client-specific preferences; another is Bookmark Storage (\xepref{0048}). -\end{quote} - -Options: -\begin{description} -\iqdiscitem{Private XML Storage (\ns{jabber:iq:private})} -\dbtype -\end{description} - -\makesubsection{modproxy}{\modproxy{}} -\ind{modules!\modversion{}}\ind{protocols!XEP-0065: SOCKS5 Bytestreams} - -This module implements SOCKS5 Bytestreams (\xepref{0065}). -It allows \ejabberd{} to act as a file transfer proxy between two -XMPP clients. - -Options: -\begin{description} -\hostitem{proxy} -\titem{name: Text}\ind{options!name}Defines Service Discovery name of the service. -Default is \term{"SOCKS5 Bytestreams"}. -\titem{ip: IP}\ind{options!ip}This option specifies which network interface -to listen for. Default is an IP address of the service's DNS name, or, -if fails, \verb|"127.0.0.1"|. -\titem{port: Number}\ind{options!port}This option defines port to listen for -incoming connections. Default is~7777. -\titem{hostname: HostName}\ind{options!hostname}Defines a hostname advertised -by the service when establishing a session with clients. This is useful when -you run the service behind a NAT. The default is the value of \term{ip} option. -Examples: \term{"proxy.mydomain.org"}, \term{"200.150.100.50"}. Note that -not all clients understand domain names in stream negotiation, -so you should think twice before setting domain name in this option. -\titem{auth\_type: anonymous|plain}\ind{options!auth\_type}SOCKS5 authentication type. -Possible values are \term{anonymous} and \term{plain}. Default is -\term{anonymous}. -\titem{access: AccessName}\ind{options!access}Defines ACL for file transfer initiators. -Default is \term{all}. -\titem{max\_connections: Number}\ind{options!max\_connections}Maximum number of -active connections per file transfer initiator. No limit by default. -\titem{shaper: none|ShaperName}\ind{options!shaper}This option defines shaper for -the file transfer peers. Shaper with the maximum bandwidth will be selected. -Default is \term{none}. -\end{description} - -Examples: -\begin{itemize} -\item The simpliest configuration of the module: -\begin{verbatim} -modules: - ... - mod_proxy65: {} - ... -\end{verbatim} -\item More complicated configuration. -\begin{verbatim} -acl: - admin: - user: - - "admin": "example.org" - proxy_users: - server: - - "example.org" - -access: - proxy65_access: - proxy_users: allow - all: deny - proxy65_shaper: - admin: none - proxy_users: proxyrate - -shaper: - proxyrate: 10240 - -modules: - ... - mod_proxy65: - host: "proxy1.example.org" - name: "File Transfer Proxy" - ip: "200.150.100.1" - port: 7778 - max_connections: 5 - access: proxy65_access - shaper: proxy65_shaper - ... -\end{verbatim} -\end{itemize} - -\makesubsection{modpubsub}{\modpubsub{}} -\ind{modules!\modpubsub{}}\ind{protocols!XEP-0060: Publish-Subscribe} - -This module offers a Publish-Subscribe Service (\xepref{0060}). -The functionality in \modpubsub{} can be extended using plugins. -The plugin that implements PEP (Personal Eventing via Pubsub) (\xepref{0163}) -is enabled in the default ejabberd configuration file, -and it requires \modcaps{}. - -Options: -\begin{description} -\hostitem{pubsub} - If you use \modpubsubodbc, please ensure the prefix contains only one dot, - for example `\jid{pubsub.}', or `\jid{publish.}',. -\titem{access\_createnode: AccessName} \ind{options!access\_createnode} - This option restricts which users are allowed to create pubsub nodes using - ACL and ACCESS. - By default any account in the local ejabberd server is allowed to create pubsub nodes. -\titem{max\_items\_node: MaxItems} \ind{options!max\_items\_node} - Define the maximum number of items that can be stored in a node. - Default value is 10. -\titem{plugins: [ Plugin, ...]} \ind{options!plugins} - To specify which pubsub node plugins to use. - The first one in the list is used by default. - If this option is not defined, the default plugins list is: \term{["flat"]}. - PubSub clients can define which plugin to use when creating a node: - add \term{type='plugin-name'} attribute to the \term{create} stanza element. -\titem{nodetree: Nodetree} \ind{options!nodetree} - To specify which nodetree to use. - If not defined, the default pubsub nodetree is used: "tree". - Only one nodetree can be used per host, and is shared by all node plugins. - - The "virtual" nodetree does not store nodes on database. - This saves resources on systems with tons of nodes. - If using the "virtual" nodetree, - you can only enable those node plugins: - ["flat","pep"] or ["flat"]; - any other plugins configuration will not work. - Also, all nodes will have the defaut configuration, - and this can not be changed. - Using "virtual" nodetree requires to start from a clean database, - it will not work if you used the default "tree" nodetree before. - - The "dag" nodetree provides experimental support for PubSub Collection Nodes (\xepref{0248}). - In that case you should also add "dag" node plugin as default, for example: - \term{plugins: ["dag","flat","hometree","pep"]} -\titem{ignore\_pep\_from\_offline: false|true} \ind{options!ignore\_pep\_from\_offline} - To specify whether or not we should get last published PEP items - from users in our roster which are offline when we connect. Value is true or false. - If not defined, pubsub assumes true so we only get last items of online contacts. -\titem{last\_item\_cache: false|true} \ind{options!last\_item\_cache} - To specify whether or not pubsub should cache last items. Value is true - or false. If not defined, pubsub do 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. -\titem{pep\_mapping: \{Key, Value\}} \ind{pep\_mapping} - This allow to define a Key-Value list to choose defined node plugins on given PEP namespace. - The following example will use node\_tune instead of node\_pep for every PEP node with tune namespace: -\begin{verbatim} -modules: - ... - mod_pubsub: - pep_mapping: - "http://jabber.org/protocol/tune": "tune" - ... -\end{verbatim} -%\titem{served\_hosts} \ind{options!served\_hosts} -% This option allows to create additional pubsub virtual hosts in a single module instance. -\end{description} - -Example of configuration that uses flat nodes as default, and allows use of flat, nodetree and pep nodes: -\begin{verbatim} -modules: - ... - mod_pubsub: - access_createnode: pubsub_createnode - plugins: - - "flat" - - "hometree" - - "pep" - ... -\end{verbatim} - -Using ODBC database requires using mod\_pubsub\_odbc without option changes. Only flat, hometree and pep plugins supports ODBC. -The following example shows previous configuration with ODBC usage: -\begin{verbatim} -modules: - ... - mod_pubsub_odbc: - access_createnode: pubsub_createnode - plugins: - - "flat" - - "hometree" - - "pep" - ... -\end{verbatim} - -\makesubsection{modregister}{\modregister{}} -\ind{modules!\modregister{}}\ind{protocols!XEP-0077: In-Band Registration}\ind{public registration} - -This module adds support for In-Band Registration (\xepref{0077}). This protocol -enables end users to use a \XMPP{} client to: -\begin{itemize} -\item Register a new account on the server. -\item Change the password from an existing account on the server. -\item Delete an existing account on the server. -\end{itemize} - - -Options: -\begin{description} -\titem{access: AccessName} \ind{options!access} - Specify rules to restrict what usernames can be registered and unregistered. - If a rule returns `deny' on the requested username, - registration and unregistration of that user name is denied. - There are no restrictions by default. -\titem{access\_from: AccessName} \ind{options!access\_from}By default, \ejabberd{} -doesn't allow to register new accounts from s2s or existing c2s sessions. You can -change it by defining access rule in this option. Use with care: allowing registration -from s2s leads to uncontrolled massive accounts creation by rogue users. -\titem{captcha\_protected: false|true} \ind{options!captcha\_protected} -Protect registrations with CAPTCHA (see section \ref{captcha}). The default is \term{false}. -\titem{ip\_access: AccessName} \ind{options!ip\_access} - Define rules to allow or deny account registration depending - on the IP address of the XMPP client. The \term{AccessName} should be - of type \term{ip}. The default value is \term{all}. -\titem{password\_strength: Entropy} \ind{options!password\_strength} -This option sets the minimum informational entropy for passwords. The value \term{Entropy} -is a number of bits of entropy. The recommended minimum is 32 bits. -The default is 0, i.e. no checks are performed. -\titem{welcome\_message: \{subject: Subject, body: Body\}} - \ind{options!welcomem} Set a welcome message that - is sent to each newly registered account. The first string is the subject, and - the second string is the message body. -\titem{registration\_watchers: [ JID, ...]} \ind{options!rwatchers}This option defines a - list of JIDs which will be notified each time a new account is registered. -\iqdiscitem{In-Band Registration (\ns{jabber:iq:register})} -\end{description} - -This module reads also another option defined globally for the server: -\term{registration\_timeout: Timeout}. \ind{options!registratimeout} -This option limits the frequency of registration from a given IP or username. -So, a user that tries to register a new account from the same IP address or JID during -this number of seconds after his previous registration -will receive an error \term{resource-constraint} with the explanation: -``Users are not allowed to register accounts so quickly''. -The timeout is expressed in seconds, and it must be an integer. -To disable this limitation, -instead of an integer put a word like: \term{infinity}. -Default value: 600 seconds. - -Examples: -\begin{itemize} -\item Next example prohibits the registration of too short account names, -and allows to create accounts only to clients of the local network: -\begin{verbatim} -acl: - loopback: - ip: - - "127.0.0.0/8" - - "::" - shortname: - user_glob: - - "?" - - "??" - ## The same using regexp: - ##user_regexp: "^..?$" - -access: - mynetworks: - loopback: allow - all: deny - register: - shortname: deny - all: allow - -modules: - mod_register: - ip_access: mynetworks - access: register -\end{verbatim} -\item This configuration prohibits usage of In-Band Registration - to create or delete accounts, - but allows existing accounts to change the password: -\begin{verbatim} -access: - register: - all: deny - -modules: - ... - mod_register: - access: register - ... -\end{verbatim} -\item - This configuration disables all In-Band Registration - functionality: create, delete accounts and change password: -\begin{verbatim} -modules: - ... - ## mod_register: - ## access: register - ... -\end{verbatim} -\item Define the welcome message and two registration watchers. -Also define a registration timeout of one hour: -\begin{verbatim} -registration_timeout: 3600 -modules: - ... - mod_register: - welcome_message: - subject: "Welcome!" - body: |- - Hi. - Welcome to this Jabber server. - Check http://www.jabber.org - - Bye - registration_watchers: - - "admin1@example.org" - - "boss@example.net" - ... -\end{verbatim} -\end{itemize} - -\makesubsection{modregisterweb}{\modregisterweb{}} -\ind{modules!\modregisterweb{}} - -This module provides a web page where people can: -\begin{itemize} -\item Register a new account on the server. -\item Change the password from an existing account on the server. -\item Delete an existing account on the server. -\end{itemize} - -This module supports CAPTCHA image to register a new account. -To enable this feature, configure the options captcha\_cmd and captcha\_host. - -Options: -\begin{description} -\titem{registration\_watchers: [ JID, ...]} \ind{options!rwatchers}This option defines a - list of JIDs which will be notified each time a new account is registered. -\end{description} - -This example configuration shows how to enable the module and the web handler: -\begin{verbatim} -hosts: - - "localhost" - - "example.org" - - "example.com" -listen: - ... - - - port: 5281 - module: ejabberd_http - register: true - certfile: "/etc/ejabberd/certificate.pem" - tls: true - ... - -modules: - ... - mod_register_web: {} - ... -\end{verbatim} - -For example, the users of the host \term{example.org} can visit the page: -\ns{https://example.org:5281/register/} -It is important to include the last / character in the URL, -otherwise the subpages URL will be incorrect. - -\makesubsection{modroster}{\modroster{}} -\ind{modules!\modroster{}}\ind{roster management}\ind{protocols!RFC 6121: XMPP IM} - -This module implements roster management as defined in -\footahref{http://tools.ietf.org/html/rfc6121\#section-2}{RFC 6121: XMPP IM}. -It also supports Roster Versioning (\xepref{0237}). - -Options: -\begin{description} -\iqdiscitem{Roster Management (\ns{jabber:iq:roster})} -\dbtype - \titem{versioning: false|true} \ind{options!versioning}Enables - Roster Versioning. - This option is disabled by default. - \titem{store\_current\_id: false|true} \ind{options!storecurrentid} - If this option is enabled, the current version number is stored on the database. - If disabled, the version number is calculated on the fly each time. - Enabling this option reduces the load for both ejabberd and the database. - This option does not affect the client in any way. - This option is only useful if Roster Versioning is enabled. - This option is disabled by default. - Important: if you use \modsharedroster{} or \modsharedrosterldap{}, - you must disable this option. - \titem{access} \ind{options!access} - This option can be configured to specify rules to restrict roster management. - If a rule returns `deny' on the requested user name, - that user cannot modify his personal roster: - not add/remove/modify contacts, - or subscribe/unsubscribe presence. - By default there aren't restrictions. - \titem{managers} \ind{options!managers} - List of remote entities that can manage users rosters using Remote Roster Management - (\xepref{0321}). - The protocol sections implemented are: - \term{4.2. The remote entity requests current user's roster}. - \term{4.3. The user updates roster}. - \term{4.4. The remote entity updates the user's roster}. - A remote entity cab only get or modify roster items that have the same domain as the entity. - Default value is: \term{[]}. -\end{description} - -This example configuration enables Roster Versioning with storage of current id. -The ICQ and MSN transports can get ICQ and MSN contacts, add them, or remove them for any local account: -\begin{verbatim} -modules: - ... - mod_roster: - versioning: true - store_current_id: true - managers: - - "icq.example.org" - - "msn.example.org" - ... -\end{verbatim} - -With this example configuration, only admins can manage their rosters; -everybody else cannot modify the roster: -\begin{verbatim} -acl: - admin: - user: - - "sarah": "example.org" -access: - roster: - admin: allow - -modules: - ... - mod_roster: - access: roster - ... -\end{verbatim} - -\makesubsection{modservicelog}{\modservicelog{}} -\ind{modules!\modservicelog{}}\ind{message auditing}\ind{Bandersnatch} - -This module adds support for logging end user packets via a \XMPP{} message -auditing service such as -\footahref{http://www.funkypenguin.info/project/bandersnatch/}{Bandersnatch}. All user -packets are encapsulated in a \verb|| element and sent to the specified -service(s). - -Options: -\begin{description} -\titem{loggers: [Names, ...]} \ind{options!loggers}With this option a (list of) service(s) - that will receive the packets can be specified. -\end{description} - -Examples: -\begin{itemize} -\item To log all end user packets to the Bandersnatch service running on - \jid{bandersnatch.example.com}: -\begin{verbatim} -modules: - ... - mod_service_log: - loggers: ["bandersnatch.example.com"] - ... -\end{verbatim} -\item To log all end user packets to the Bandersnatch service running on - \jid{bandersnatch.example.com} and the backup service on - \jid{bandersnatch.example.org}: -\begin{verbatim} -modules: - ... - mod_service_log: - loggers: - - "bandersnatch.example.com" - - "bandersnatch.example.org" - ... -\end{verbatim} -\end{itemize} - -\makesubsection{modsharedroster}{\modsharedroster{}} -\ind{modules!\modsharedroster{}}\ind{shared roster groups} - -This module enables you to create shared roster groups. This means that you can -create groups of people that can see members from (other) groups in their -rosters. The big advantages of this feature are that end users do not need to -manually add all users to their rosters, and that they cannot permanently delete -users from the shared roster groups. -A shared roster group can have members from any XMPP server, -but the presence will only be available from and to members -of the same virtual host where the group is created. - -Options: -\begin{description} -\dbtype -\end{description} - -Shared roster groups can be edited \emph{only} via the Web Admin. Each group -has a unique identification and the following parameters: -\begin{description} -\item[Name] The name of the group, which will be displayed in the roster. -\item[Description] The description of the group. This parameter does not affect - anything. -\item[Members] A list of JIDs of group members, entered one per line in - the Web Admin. - The special member directive \term{@all@} - represents all the registered users in the virtual host; - which is only recommended for a small server with just a few hundred users. - The special member directive \term{@online@} - represents the online users in the virtual host. -\item[Displayed groups] - A list of groups that will be in the rosters of this group's members. - A group of other vhost can be identified with \term{groupid@vhost} -\end{description} - -Examples: -\begin{itemize} -\item Take the case of a computer club that wants all its members seeing each - other in their rosters. To achieve this, they need to create a shared roster - group similar to next table: -\begin{table}[H] - \centering - \begin{tabular}{|l|l|} - \hline Identification& Group `\texttt{club\_members}'\\ - \hline Name& Club Members\\ - \hline Description& Members from the computer club\\ - \hline Members& - {\begin{tabular}{l} - \jid{member1@example.org}\\ - \jid{member2@example.org}\\ - \jid{member3@example.org} - \end{tabular} - }\\ - \hline Displayed groups& \texttt{club\_members}\\ - \hline - \end{tabular} -\end{table} -\item In another case we have a company which has three divisions: Management, - Marketing and Sales. All group members should see all other members in their - rosters. Additionally, all managers should have all marketing and sales people - in their roster. Simultaneously, all marketeers and the whole sales team - should see all managers. This scenario can be achieved by creating shared - roster groups as shown in the following table: -\begin{table}[H] - \centering - \begin{tabular}{|l|l|l|l|} - \hline Identification& - Group `\texttt{management}'& - Group `\texttt{marketing}'& - Group `\texttt{sales}'\\ - \hline Name& Management& Marketing& Sales\\ - \hline Description& \\ - Members& - {\begin{tabular}{l} - \jid{manager1@example.org}\\ - \jid{manager2@example.org}\\ - \jid{manager3@example.org}\\ - \jid{manager4@example.org} - \end{tabular} - }& - {\begin{tabular}{l} - \jid{marketeer1@example.org}\\ - \jid{marketeer2@example.org}\\ - \jid{marketeer3@example.org}\\ - \jid{marketeer4@example.org} - \end{tabular} - }& - {\begin{tabular}{l} - \jid{saleswoman1@example.org}\\ - \jid{salesman1@example.org}\\ - \jid{saleswoman2@example.org}\\ - \jid{salesman2@example.org} - \end{tabular} - }\\ - \hline Displayed groups& - {\begin{tabular}{l} - \texttt{management}\\ - \texttt{marketing}\\ - \texttt{sales} - \end{tabular} - }& - {\begin{tabular}{l} - \texttt{management}\\ - \texttt{marketing} - \end{tabular} - }& - {\begin{tabular}{l} - \texttt{management}\\ - \texttt{sales} - \end{tabular} - }\\ - \hline - \end{tabular} -\end{table} -\end{itemize} - -\makesubsection{modsharedrosterldap}{\modsharedrosterldap{}} -\ind{modules!\modsharedrosterldap{}}\ind{shared roster groups ldap} - -This module lets the server administrator -automatically populate users' rosters (contact lists) with entries based on -users and groups defined in an LDAP-based directory. - -\makesubsubsection{msrlconfigparams}{Configuration parameters} - -The module accepts the following configuration parameters. Some of them, if -unspecified, default to the values specified for the top level of -configuration. This lets you avoid specifying, for example, the bind password, -in multiple places. - -\makeparagraph{msrlfilters}{Filters} - -These parameters specify LDAP filters used to query for shared roster information. -All of them are run against the \verb|ldap_base|. - -\begin{description} - - \titem{{\tt ldap\_rfilter}} - So called ``Roster Filter''. Used to find names of all ``shared roster'' groups. - See also the \verb|ldap_groupattr| parameter. - If unspecified, defaults to the top-level parameter of the same name. - You {\em must} specify it in some place in the configuration, there is no default. - - \titem{{\tt ldap\_ufilter}} - ``User Filter'' -- used for retrieving the human-readable name of roster - entries (usually full names of people in the roster). - See also the parameters \verb|ldap_userdesc| and \verb|ldap_useruid|. - If unspecified, defaults to the top-level parameter of the same name. - If that one also is unspecified, then the filter is assembled from values of - other parameters as follows (\verb|[ldap_SOMETHING]| is used to mean ``the - value of the configuration parameter {\tt ldap\_SOMETHING}''): - -\begin{verbatim} -(&(&([ldap_memberattr]=[ldap_memberattr_format])([ldap_groupattr]=%g))[ldap_filter]) -\end{verbatim} - - Subsequently {\tt \%u} and {\tt \%g} are replaced with a {\tt *}. This means - that given the defaults, the filter sent to the LDAP server is would be - \verb|(&(memberUid=*)(cn=*))|. If however the {\tt ldap\_memberattr\_format} - is something like \verb|uid=%u,ou=People,o=org|, then the filter will be - \verb|(&(memberUid=uid=*,ou=People,o=org)(cn=*))|. - - \titem{{\tt ldap\_gfilter}} - ``Group Filter'' -- used when retrieving human-readable name (a.k.a. - ``Display Name'') and the members of a group. - See also the parameters \verb|ldap_groupattr|, \verb|ldap_groupdesc| and \verb|ldap_memberattr|. - If unspecified, defaults to the top-level parameter of the same name. - If that one also is unspecified, then the filter is constructed exactly in the - same way as {\tt User Filter}. - - \titem{{\tt ldap\_filter}} - Additional filter which is AND-ed together with {\tt User Filter} and {\tt - Group Filter}. - If unspecified, defaults to the top-level parameter of the same name. If that - one is also unspecified, then no additional filter is merged with the other - filters. -\end{description} - -Note that you will probably need to manually define the {\tt User} and {\tt -Group Filter}s (since the auto-assembled ones will not work) if: -\begin{itemize} -\item your {\tt ldap\_memberattr\_format} is anything other than a simple {\tt \%u}, -\item {\bf and} the attribute specified with {\tt ldap\_memberattr} does not support substring matches. -\end{itemize} -An example where it is the case is OpenLDAP and {\tt (unique)MemberName} attribute from the {\tt groupOf(Unique)Names} objectClass. -A symptom of this problem is that you will see messages such as the following in your {\tt slapd.log}: -\begin{verbatim} -get_filter: unknown filter type=130 -filter="(&(?=undefined)(?=undefined)(something=else))" -\end{verbatim} - -\makesubsubsection{msrlattrs}{Attributes} - -These parameters specify the names of the attributes which hold interesting data -in the entries returned by running filters specified in -section~\ref{msrlfilters}. - -\begin{description} - \titem{{\tt ldap\_groupattr}} - The name of the attribute that holds the group name, and that is used to differentiate between them. - Retrieved from results of the ``Roster Filter'' and ``Group Filter''. - Defaults to {\tt cn}. - - \titem{{\tt ldap\_groupdesc}} - The name of the attribute which holds the human-readable group name in the - objects you use to represent groups. - Retrieved from results of the ``Group Filter''. - Defaults to whatever {\tt ldap\_groupattr} is set. - - \titem{{\tt ldap\_memberattr}} - The name of the attribute which holds the IDs of the members of a group. - Retrieved from results of the ``Group Filter''. - Defaults to {\tt memberUid}. - - The name of the attribute differs depending on the {\tt objectClass} you use - for your group objects, for example: - \begin{description} - \item{{\tt posixGroup}} $\rightarrow{}$ {\tt memberUid} - \item{{\tt groupOfNames}} $\rightarrow{}$ {\tt member} - \item{{\tt groupOfUniqueNames}} $\rightarrow{}$ {\tt uniqueMember} - \end{description} - - \titem{{\tt ldap\_userdesc}} - The name of the attribute which holds the human-readable user name. - Retrieved from results of the ``User Filter''. - Defaults to {\tt cn}. - - \titem{{\tt ldap\_useruid}} - The name of the attribute which holds the ID of a roster item. Value of this - attribute in the roster item objects needs to match the ID retrieved from the - {\tt ldap\_memberattr} attribute of a group object. - Retrieved from results of the ``User Filter''. - Defaults to {\tt cn}. -\end{description} - -\makesubsubsection{msrlcontrolparams}{Control parameters} - -These paramters control the behaviour of the module. - -\begin{description} - - \titem{{\tt ldap\_memberattr\_format}} - A globbing format for extracting user ID from the value of the attribute named by - \verb|ldap_memberattr|. - Defaults to {\tt \%u}, which means that the whole value is the member ID. If - you change it to something different, you may also need to specify the User - and Group Filters manually --- see section~\ref{msrlfilters}. - - \titem{{\tt ldap\_memberattr\_format\_re}} - A regex for extracting user ID from the value of the attribute named by - \verb|ldap_memberattr|. - - An example value {\tt "CN=($\backslash{}\backslash{}$w*),(OU=.*,)*DC=company,DC=com"} works for user IDs such as the following: - \begin{itemize} - \item \texttt{CN=Romeo,OU=Montague,DC=company,DC=com} - \item \texttt{CN=Abram,OU=Servants,OU=Montague,DC=company,DC=com} - \item \texttt{CN=Juliet,OU=Capulet,DC=company,DC=com} - \item \texttt{CN=Peter,OU=Servants,OU=Capulet,DC=company,DC=com} - \end{itemize} - - In case: - \begin{itemize} - \item the option is unset, - \item or the {\tt re} module in unavailable in the current Erlang environment, - \item or the regular expression does not compile, - \end{itemize} - then instead of a regular expression, a simple format specified by {\tt - ldap\_memberattr\_format} is used. Also, in the last two cases an error - message is logged during the module initialization. - - Also, note that in all cases {\tt ldap\_memberattr\_format} (and {\em not} the - regex version) is used for constructing the default ``User/Group Filter'' --- - see section~\ref{msrlfilters}. - - \titem{{\tt ldap\_auth\_check}} - Whether the module should check (via the ejabberd authentication subsystem) - for existence of each user in the shared LDAP roster. See - section~\ref{msrlconfigroster} form more information. Set to {\tt off} if you - want to disable the check. - Defaults to {\tt on}. - - \titem{{\tt ldap\_user\_cache\_validity}} - Number of seconds for which the cache for roster item full names is considered - fresh after retrieval. 300 by default. See section~\ref{msrlconfigroster} on - how it is used during roster retrieval. - - \titem{{\tt ldap\_group\_cache\_validity}} - Number of seconds for which the cache for group membership is considered - fresh after retrieval. 300 by default. See section~\ref{msrlconfigroster} on - how it is used during roster retrieval. -\end{description} - -\makesubsubsection{msrlconnparams}{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~\ref{ldapconnection} -for more information about them. - -\makesubsubsection{msrlconfigroster}{Retrieving the roster} - -When the module is called to retrieve the shared roster for a user, the -following algorithm is used: - -\begin{enumerate} -\item \label{step:rfilter} A list of names of groups to display is created: the {\tt Roster Filter} -is run against the base DN, retrieving the values of the attribute named by -{\tt ldap\_groupattr}. - -\item Unless the group cache is fresh (see the {\tt -ldap\_group\_cache\_validity} option), it is refreshed: - - \begin{enumerate} - \item Information for all groups is retrieved using a single query: the {\tt - Group Filter} is run against the Base DN, retrieving the values of attributes - named by {\tt ldap\_groupattr} (group ID), {\tt ldap\_groupdesc} (group - ``Display Name'') and {\tt ldap\_memberattr} (IDs of group members). - - \item group ``Display Name'', read from the attribute named by {\tt - ldap\_groupdesc}, is stored in the cache for the given group - - \item the following processing takes place for each retrieved value of - attribute named by {\tt ldap\_memberattr}: - \begin{enumerate} - \item the user ID part of it is extracted using {\tt - ldap\_memberattr\_format(\_re)}, - - \item then (unless {\tt ldap\_auth\_check} is set to {\tt off}) for each - found user ID, the module checks (using the \ejabberd{} authentication - subsystem) whether such user exists in the given virtual host. It is - skipped if the check is enabled and fails. - - This step is here for historical reasons. If you have a tidy DIT and - properly defined ``Roster Filter'' and ``Group Filter'', it is safe to - disable it by setting {\tt ldap\_auth\_check} to {\tt off} --- it will - speed up the roster retrieval. - - \item the user ID is stored in the list of members in the cache for the - given group - \end{enumerate} - \end{enumerate} - -\item For each item (group name) in the list of groups retrieved in step~\ref{step:rfilter}: - - \begin{enumerate} - \item the display name of a shared roster group is retrieved from the group - cache - - \item for each IDs of users which belong to the group, retrieved from the - group cache: - - \begin{enumerate} - \item the ID is skipped if it's the same as the one for which we are - retrieving the roster. This is so that the user does not have himself in - the roster. - - \item the display name of a shared roster user is retrieved: - \begin{enumerate} - \item first, unless the user name cache is fresh (see the {\tt - ldap\_user\_cache\_validity} option), it is refreshed by running the - {\tt User Filter}, against the Base DN, retrieving the values of - attributes named by {\tt ldap\_useruid} and {\tt ldap\_userdesc}. - \item then, the display name for the given user ID is retrieved from the - user name cache. - \end{enumerate} - \end{enumerate} - - \end{enumerate} - -\end{enumerate} - -\makesubsubsection{msrlconfigexample}{Configuration examples} - -Since there are many possible -\footahref{http://en.wikipedia.org/wiki/Directory\_Information\_Tree}{DIT} -layouts, it will probably be easiest to understand how to configure the module -by looking at an example for a given DIT (or one resembling it). - -\makeparagraph{msrlconfigexampleflat}{Flat DIT} - -This seems to be the kind of DIT for which this module was initially designed. -Basically there are just user objects, and group membership is stored in an -attribute individually for each user. For example in a layout shown in -figure~\ref{fig:msrl-dit-flat}, the group of each user is stored in its {\tt -ou} attribute. - -\begin{figure}[htbp] - \centering - \insscaleimg{0.4}{msrl-dit-flat.png} - \caption{Flat DIT graph} - \label{fig:msrl-dit-flat} -\end{figure} - -Such layout has a few downsides, including: -\begin{itemize} -\item information duplication -- the group name is repeated in every member object -\item difficult group management -- information about group members is not - centralized, but distributed between member objects -\item inefficiency -- the list of unique group names has to be computed by iterating over all users -\end{itemize} - -This however seems to be a common DIT layout, so the module keeps supporting it. -You can use the following configuration\ldots -\begin{verbatim} -modules: - ... - mod_shared_roster_ldap: - ldap_base: "ou=flat,dc=nodomain" - ldap_rfilter: "(objectClass=inetOrgPerson)" - ldap_groupattr: "ou" - ldap_memberattr: "cn" - ldap_filter: "(objectClass=inetOrgPerson)" - ldap_userdesc: "displayName" - ... -\end{verbatim} - -\ldots to be provided with a roster as shown in figure~\ref{fig:msrl-roster-flat} upon connecting as user {\tt czesio}. - -\begin{figure}[htbp] - \centering - \insscaleimg{1}{msrl-roster-flat.png} - \caption{Roster from flat DIT} - \label{fig:msrl-roster-flat} -\end{figure} - -\makeparagraph{msrlconfigexampledeep}{Deep DIT} - -This type of DIT contains distinctly typed objects for users and groups -- see figure~\ref{fig:msrl-dit-deep}. -They are shown separated into different subtrees, but it's not a requirement. - -\begin{figure}[htbp] - \centering - \insscaleimg{0.35}{msrl-dit-deep.png} - \caption{Example ``deep'' DIT graph} - \label{fig:msrl-dit-deep} -\end{figure} - -If you use the following example module configuration with it: -\begin{verbatim} -modules: - ... - mod_shared_roster_ldap: - ldap_base: "ou=deep,dc=nodomain" - ldap_rfilter: "(objectClass=groupOfUniqueNames)" - ldap_filter: "" - ldap_gfilter: "(&(objectClass=groupOfUniqueNames)(cn=%g))" - ldap_groupdesc: "description" - ldap_memberattr: "uniqueMember" - ldap_memberattr_format: "cn=%u,ou=people,ou=deep,dc=nodomain" - ldap_ufilter: "(&(objectClass=inetOrgPerson)(cn=%u))" - ldap_userdesc: "displayName" - ... -\end{verbatim} - -\ldots and connect as user {\tt czesio}, then \ejabberd{} will provide you with -the roster shown in figure~\ref{fig:msrl-roster-deep}. - -\begin{figure}[htbp] - \centering - \insscaleimg{1}{msrl-roster-deep.png} - \caption{Example roster from ``deep'' DIT} - \label{fig:msrl-roster-deep} -\end{figure} - -\makesubsection{modsic}{\modsic{}} -\ind{modules!\modstats{}}\ind{protocols!XEP-0279: Server IP Check} - -This module adds support for Server IP Check (\xepref{0279}). This protocol -enables a client to discover its external IP address. - -Options: -\begin{description} -\iqdiscitem{\ns{urn:xmpp:sic:0}} -\end{description} - -\makesubsection{modsip}{\modsip{}} -\ind{modules!\modsip{}} -This module adds SIP proxy/registrar support for the corresponding virtual host. -Note that it is not enough to just load this module only. You should also configure -listeners and DNS records properly. See section \ref{sip} for the full explanation. - -Example configuration: -\begin{verbatim} -modules: - ... - mod_sip: {} - ... -\end{verbatim} - -Options: -\begin{description} -\titem{record\_route: SIP\_URI}\ind{options!record\_route}When the option -\term{always\_record\_route} is set or when SIP outbound -is utilized \footahref{http://tools.ietf.org/html/rfc5626}{RFC 5626}, -\ejabberd{} inserts \term{Record-Route} header field with this \term{SIP\_URI} -into a SIP message. The default is SIP URI constructed from the virtual host. -\titem{always\_record\_route: true|false}\ind{options!always\_record\_route} -Always insert \term{Record-Route} header into SIP messages. This approach allows -to bypass NATs/firewalls a bit more easily. The default is \term{true}. -\titem{routes: [SIP\_URI]}\ind{options!routes}You can set a list of SIP URIs of routes -pointing to this proxy server. The default is a list of a SIP URI constructed -from the virtual host. -\titem{flow\_timeout\_udp: Seconds}For SIP outbound UDP connections set a keep-alive -timer to \term{Seconds}. The default is 29. -\titem{flow\_timeout\_tcp: Seconds}For SIP outbound TCP connections set a keep-alive -timer to \term{Seconds}. The default is 120. -\titem{via: [\{type: Type, host: Host, port: Port\}]}\ind{options!via}With -this option for every \term{Type} you can specify \term{Host} and \term{Port} -to set in \term{Via} header of outgoing SIP messages, where \term{Type} can be -\term{udp}, \term{tcp} or \term{tls}. \term{Host} is a string and \term{Port} is -a non negative integer. This is useful if you're running your server in a non-standard -network topology. -\end{description} -Example complex configuration: -\begin{verbatim} -modules: - ... - mod_sip: - always_record_route: false - record_route: sip:example.com;lr - routes: - - sip:example.com;lr - - sip:sip.example.com;lr - flow_timeout_udp: 30 - flow_timeout_tcp: 130 - via: - - - type: tls - host: "sip-tls.example.com" - port: 5061 - - - type: tcp - host: "sip-tcp.example.com" - port: 5060 - - - type: udp - host: "sip-udp.example.com" - port: 5060 - ... -\end{verbatim} - -\makesubsection{modstats}{\modstats{}} -\ind{modules!\modstats{}}\ind{protocols!XEP-0039: Statistics Gathering}\ind{statistics} - -This module adds support for Statistics Gathering (\xepref{0039}). This protocol -allows you to retrieve next statistics from your \ejabberd{} deployment: -\begin{itemize} -\item Total number of registered users on the current virtual host (users/total). -\item Total number of registered users on all virtual hosts (users/all-hosts/total). -\item Total number of online users on the current virtual host (users/online). -\item Total number of online users on all virtual hosts (users/all-hosts/online). -\end{itemize} - -Options: -\begin{description} -\iqdiscitem{Statistics Gathering (\ns{http://jabber.org/protocol/stats})} -\end{description} - -As there are only a small amount of clients (for \ind{Tkabber}example -\footahref{http://tkabber.jabber.ru/}{Tkabber}) and software libraries with -support for this XEP, a few examples are given of the XML you need to send -in order to get the statistics. Here they are: -\begin{itemize} -\item You can request the number of online users on the current virtual host - (\jid{example.org}) by sending: -\begin{verbatim} - - - - - -\end{verbatim} -\item You can request the total number of registered users on all virtual hosts - by sending: -\begin{verbatim} - - - - - -\end{verbatim} -\end{itemize} - -\makesubsection{modtime}{\modtime{}} -\ind{modules!\modtime{}}\ind{protocols!XEP-0202: Entity Time} - -This module features support for Entity Time (\xepref{0202}). By using this XEP, -you are able to discover the time at another entity's location. - -Options: -\begin{description} -\iqdiscitem{Entity Time (\ns{jabber:iq:time})} -\end{description} - -\makesubsection{modvcard}{\modvcard{}} -\ind{modules!\modvcard{}}\ind{JUD}\ind{Jabber User Directory}\ind{vCard}\ind{protocols!XEP-0054: vcard-temp} - -This module allows end users to store and retrieve their vCard, and to retrieve -other users vCards, as defined in vcard-temp (\xepref{0054}). The module also -implements an uncomplicated \Jabber{} User Directory based on the vCards of -these users. Moreover, it enables the server to send its vCard when queried. - -Options: -\begin{description} -\hostitem{vjud} -\iqdiscitem{\ns{vcard-temp}} -\dbtype -\titem{search: true|false}\ind{options!search}This option specifies whether the search - functionality is enabled or not - If disabled, the option \term{host} will be ignored and the - \Jabber{} User Directory service will not appear in the Service Discovery item - list. The default value is \term{true}. -\titem{matches: infinity|Number}\ind{options!matches}With this option, the number of reported - search results can be limited. If the option's value is set to \term{infinity}, - all search results are reported. The default value is \term{30}. -\titem{allow\_return\_all: false|true}\ind{options!allow\_return\_all}This option enables - you to specify if search operations with empty input fields should return all - users who added some information to their vCard. The default value is - \term{false}. -\titem{search\_all\_hosts, true|false}\ind{options!search\_all\_hosts}If this option is set - to \term{true}, search operations will apply to all virtual hosts. Otherwise - only the current host will be searched. The default value is \term{true}. - This option is available in \modvcard when using Mnesia, but not when using ODBC storage. -\end{description} - -Examples: -\begin{itemize} -\item In this first situation, search results are limited to twenty items, - every user who added information to their vCard will be listed when people - do an empty search, and only users from the current host will be returned: -\begin{verbatim} -modules: - ... - mod_vcard: - search: true - matches: 20 - allow_return_all: true - search_all_hosts: false - ... -\end{verbatim} -\item The second situation differs in a way that search results are not limited, - and that all virtual hosts will be searched instead of only the current one: -\begin{verbatim} -modules: - ... - mod_vcard: - search: true - matches: infinity - allow_return_all: true - ... -\end{verbatim} -\end{itemize} - -\makesubsection{modvcardldap}{\modvcardldap{}} -\ind{modules!\modvcardldap{}}\ind{JUD}\ind{Jabber User Directory}\ind{vCard}\ind{protocols!XEP-0054: vcard-temp} - -%TODO: verify if the referrers to the LDAP section are still correct - -\ejabberd{} can map LDAP attributes to vCard fields. This behaviour is -implemented in the \modvcardldap{} module. This module does not depend on the -authentication method (see~\ref{ldapauth}). - -Usually \ejabberd{} treats LDAP as a read-only storage: -it is possible to consult data, but not possible to -create accounts or edit vCard that is stored in LDAP. -However, it is possible to change passwords if \module{mod\_register} module is enabled -and LDAP server supports -\footahref{http://tools.ietf.org/html/rfc3062}{RFC 3062}. - -The \modvcardldap{} module has -its own optional parameters. The first group of parameters has the same -meaning as the top-level LDAP parameters to set the authentication method: -\option{ldap\_servers}, \option{ldap\_port}, \option{ldap\_rootdn}, -\option{ldap\_password}, \option{ldap\_base}, \option{ldap\_uids}, -\option{ldap\_deref\_aliases} and \option{ldap\_filter}. -See section~\ref{ldapauth} for detailed information -about these options. If one of these options is not set, \ejabberd{} will look -for the top-level option with the same name. - -The second group of parameters -consists of the following \modvcardldap{}-specific options: - -\begin{description} -\hostitem{vjud} -\iqdiscitem{\ns{vcard-temp}} -\titem{\{search, true|false\}}\ind{options!search}This option specifies whether the search - functionality is enabled (value: \term{true}) or disabled (value: - \term{false}). If disabled, the option \term{host} will be ignored and the - \Jabber{} User Directory service will not appear in the Service Discovery item - list. The default value is \term{true}. -\titem{\{matches, infinity|Number\}}\ind{options!matches}With this option, the number of reported - search results can be limited. If the option's value is set to \term{infinity}, - all search results are reported. The default value is \term{30}. -\titem{\{ldap\_vcard\_map, [ \{Name, Pattern, LDAPattributes\}, ...]\}} \ind{options!ldap\_vcard\_map} - With this option you can set the table that maps LDAP attributes to vCard fields. - \ind{protocols!RFC 2426: vCard MIME Directory Profile} - \term{Name} is the type name of the vCard as defined in - \footahref{http://tools.ietf.org/html/rfc2426}{RFC 2426}. - \term{Pattern} is a string which contains pattern variables - \term{"\%u"}, \term{"\%d"} or \term{"\%s"}. - \term{LDAPattributes} is the list containing LDAP attributes. - The pattern variables - \term{"\%s"} will be sequentially replaced - with the values of LDAP attributes from \term{List\_of\_LDAP\_attributes}, - \term{"\%u"} will be replaced with the user part of a JID, - and \term{"\%d"} will be replaced with the domain part of a JID. - The default is: -\begin{verbatim} -[{"NICKNAME", "%u", []}, - {"FN", "%s", ["displayName"]}, - {"LAST", "%s", ["sn"]}, - {"FIRST", "%s", ["givenName"]}, - {"MIDDLE", "%s", ["initials"]}, - {"ORGNAME", "%s", ["o"]}, - {"ORGUNIT", "%s", ["ou"]}, - {"CTRY", "%s", ["c"]}, - {"LOCALITY", "%s", ["l"]}, - {"STREET", "%s", ["street"]}, - {"REGION", "%s", ["st"]}, - {"PCODE", "%s", ["postalCode"]}, - {"TITLE", "%s", ["title"]}, - {"URL", "%s", ["labeleduri"]}, - {"DESC", "%s", ["description"]}, - {"TEL", "%s", ["telephoneNumber"]}, - {"EMAIL", "%s", ["mail"]}, - {"BDAY", "%s", ["birthDay"]}, - {"ROLE", "%s", ["employeeType"]}, - {"PHOTO", "%s", ["jpegPhoto"]}] -\end{verbatim} -\titem{\{ldap\_search\_fields, [ \{Name, Attribute\}, ...]\}}\ind{options!ldap\_search\_fields}This option - defines the search form and the LDAP attributes to search within. - \term{Name} is the name of a search form - field which will be automatically translated by using the translation - files (see \term{msgs/*.msg} for available words). \term{Attribute} is the - LDAP attribute or the pattern \term{"\%u"}. The default is: -\begin{verbatim} -[{"User", "%u"}, - {"Full Name", "displayName"}, - {"Given Name", "givenName"}, - {"Middle Name", "initials"}, - {"Family Name", "sn"}, - {"Nickname", "%u"}, - {"Birthday", "birthDay"}, - {"Country", "c"}, - {"City", "l"}, - {"Email", "mail"}, - {"Organization Name", "o"}, - {"Organization Unit", "ou"}] -\end{verbatim} -\titem{\{ldap\_search\_reported, [ \{SearchField, VcardField\}, ...]\}}\ind{options!ldap\_search\_reported}This option - defines which search fields should be reported. - \term{SearchField} is the name of a search form - field which will be automatically translated by using the translation - files (see \term{msgs/*.msg} for available words). \term{VcardField} is the - vCard field name defined in the \option{ldap\_vcard\_map} option. The default - is: -\begin{verbatim} -[{"Full Name", "FN"}, - {"Given Name", "FIRST"}, - {"Middle Name", "MIDDLE"}, - {"Family Name", "LAST"}, - {"Nickname", "NICKNAME"}, - {"Birthday", "BDAY"}, - {"Country", "CTRY"}, - {"City", "LOCALITY"}, - {"Email", "EMAIL"}, - {"Organization Name", "ORGNAME"}, - {"Organization Unit", "ORGUNIT"}] -\end{verbatim} -\end{description} - -%TODO: this examples still should be organised better -Examples: -\begin{itemize} -\item - -Let's say \term{ldap.example.org} is the name of our LDAP server. We have -users with their passwords in \term{"ou=Users,dc=example,dc=org"} directory. -Also we have addressbook, which contains users emails and their additional -infos in \term{"ou=AddressBook,dc=example,dc=org"} directory. Corresponding -authentication section should looks like this: - -\begin{verbatim} -%% authentication method -{auth_method, ldap}. -%% DNS name of our LDAP server -{ldap_servers, ["ldap.example.org"]}. -%% We want to authorize users from 'shadowAccount' object class only -{ldap_filter, "(objectClass=shadowAccount)"}. -\end{verbatim} - -Now we want to use users LDAP-info as their vCards. We have four attributes -defined in our LDAP schema: \term{"mail"} --- email address, \term{"givenName"} ---- first name, \term{"sn"} --- second name, \term{"birthDay"} --- birthday. -Also we want users to search each other. Let's see how we can set it up: - -\begin{verbatim} -{modules, - ... - {mod_vcard_ldap, - [ - %% We use the same server and port, but want to bind anonymously because - %% our LDAP server accepts anonymous requests to - %% "ou=AddressBook,dc=example,dc=org" subtree. - {ldap_rootdn, ""}, - {ldap_password, ""}, - %% define the addressbook's base - {ldap_base, "ou=AddressBook,dc=example,dc=org"}, - %% uidattr: user's part of JID is located in the "mail" attribute - %% uidattr_format: common format for our emails - {ldap_uids, [{"mail","%u@mail.example.org"}]}, - %% We have to define empty filter here, because entries in addressbook does not - %% belong to shadowAccount object class - {ldap_filter, ""}, - %% Now we want to define vCard pattern - {ldap_vcard_map, - [{"NICKNAME", "%u", []}, % just use user's part of JID as his nickname - {"FIRST", "%s", ["givenName"]}, - {"LAST", "%s", ["sn"]}, - {"FN", "%s, %s", ["sn", "givenName"]}, % example: "Smith, John" - {"EMAIL", "%s", ["mail"]}, - {"BDAY", "%s", ["birthDay"]}]}, - %% Search form - {ldap_search_fields, - [{"User", "%u"}, - {"Name", "givenName"}, - {"Family Name", "sn"}, - {"Email", "mail"}, - {"Birthday", "birthDay"}]}, - %% vCard fields to be reported - %% Note that JID is always returned with search results - {ldap_search_reported, - [{"Full Name", "FN"}, - {"Nickname", "NICKNAME"}, - {"Birthday", "BDAY"}]} - ]} - ... -}. -\end{verbatim} - -Note that \modvcardldap{} module checks an existence of the user before -searching his info in LDAP. - -\item \term{ldap\_vcard\_map} example: -\begin{verbatim} -{ldap_vcard_map, - [{"NICKNAME", "%u", []}, - {"FN", "%s", ["displayName"]}, - {"CTRY", "Russia", []}, - {"EMAIL", "%u@%d", []}, - {"DESC", "%s\n%s", ["title", "description"]} - ]}, -\end{verbatim} -\item \term{ldap\_search\_fields} example: -\begin{verbatim} -{ldap_search_fields, - [{"User", "uid"}, - {"Full Name", "displayName"}, - {"Email", "mail"} - ]}, -\end{verbatim} -\item \term{ldap\_search\_reported} example: -\begin{verbatim} -{ldap_search_reported, - [{"Full Name", "FN"}, - {"Email", "EMAIL"}, - {"Birthday", "BDAY"}, - {"Nickname", "NICKNAME"} - ]}, -\end{verbatim} -\end{itemize} - -\makesubsection{modvcardxupdate}{\modvcardxupdate{}} -\ind{modules!\modvcardxupdate{}}\ind{protocols!XEP-0153: vCard-Based Avatars} - -The user's client can store an avatar in the user vCard. -The vCard-Based Avatars protocol (\xepref{0153}) -provides a method for clients to inform the contacts what is the avatar hash value. -However, simple or small clients may not implement that protocol. - -If this module is enabled, all the outgoing client presence stanzas get automatically -the avatar hash on behalf of the client. -So, the contacts receive the presence stanzas with the Update Data described -in \xepref{0153} as if the client would had inserted it itself. -If the client had already included such element in the presence stanza, -it is replaced with the element generated by ejabberd. - -By enabling this module, each vCard modification produces a hash recalculation, -and each presence sent by a client produces hash retrieval and a -presence stanza rewrite. -For this reason, enabling this module will introduce a computational overhead -in servers with clients that change frequently their presence. - -Options: -\begin{description} -\dbtype -\end{description} - -\makesubsection{modversion}{\modversion{}} -\ind{modules!\modversion{}}\ind{protocols!XEP-0092: Software Version} - -This module implements Software Version (\xepref{0092}). Consequently, it -answers \ejabberd{}'s version when queried. - -Options: -\begin{description} -\titem{show\_os: true|false}\ind{options!showos}Should the operating system be revealed or not. - The default value is \term{true}. -\iqdiscitem{Software Version (\ns{jabber:iq:version})} -\end{description} - -\makechapter{manage}{Managing an \ejabberd{} Server} - - -\makesection{ejabberdctl}{\term{ejabberdctl}} - -With the \term{ejabberdctl} command line administration script -you can execute \term{ejabberdctl commands} (described in the next section, \ref{ectl-commands}) -and also many general \term{ejabberd commands} (described in section \ref{eja-commands}). -This means you can start, stop and perform many other administrative tasks -in a local or remote \ejabberd{} server (by providing the argument \term{--node NODENAME}). - -The \term{ejabberdctl} script can be configured in the file \term{ejabberdctl.cfg}. -This file includes detailed information about each configurable option. See section \ref{erlangconfiguration}. - -The \term{ejabberdctl} script returns a numerical status code. -Success is represented by \term{0}, -error is represented by \term{1}, -and other codes may be used for specific results. -This can be used by other scripts to determine automatically -if a command succeeded or failed, -for example using: \term{echo \$?} - -If you use Bash, you can get Bash completion by copying the file \term{tools/ejabberdctl.bc} -to the directory \term{/etc/bash\_completion.d/} (in Debian, Ubuntu, Fedora and maybe others). - -\makesubsection{ectl-commands}{ejabberdctl Commands} - -When \term{ejabberdctl} is executed without any parameter, -it displays the available options. If there isn't an \ejabberd{} server running, -the available parameters are: -\begin{description} -\titem{start} Start \ejabberd{} in background mode. This is the default method. -\titem{debug} Attach an Erlang shell to an already existing \ejabberd{} server. This allows to execute commands interactively in the \ejabberd{} server. -\titem{live} Start \ejabberd{} in live mode: the shell keeps attached to the started server, showing log messages and allowing to execute interactive commands. -\end{description} - -If there is an \ejabberd{} server running in the system, -\term{ejabberdctl} shows the \term{ejabberdctl commands} described bellow -and all the \term{ejabberd commands} available in that server (see \ref{list-eja-commands}). - -The \term{ejabberdctl commands} are: -\begin{description} -\titem{help} Get help about ejabberdctl or any available command. Try \term{ejabberdctl help help}. -\titem{status} Check the status of the \ejabberd{} server. -\titem{stop} Stop the \ejabberd{} server. -\titem{restart} Restart the \ejabberd{} server. -\titem{mnesia} Get information about the Mnesia database. -\end{description} - -The \term{ejabberdctl} script can be restricted to require authentication -and execute some \term{ejabberd commands}; see \ref{accesscommands}. - -If account \term{robot1@example.org} is registered in \ejabberd{} with password \term{abcdef} -(which MD5 is E8B501798950FC58AAD83C8C14978E), -and your old-format configuration file contains this setting: -\begin{verbatim} -{hosts, ["example.org"]}. -{acl, bots, {user, "robot1", "example.org"}}. -{access, ctlaccess, [{allow, bots}]}. -{ejabberdctl_access_commands, [ {ctlaccess, [registered_users, register], []} ]}. -\end{verbatim} -then you can do this in the shell: -\begin{verbatim} -$ ejabberdctl registered_users example.org -Error: no_auth_provided -$ ejabberdctl --auth robot1 example.org abcdef registered_users example.org -robot1 -testuser1 -testuser2 -\end{verbatim} - - -\makesubsection{erlangconfiguration}{Erlang Runtime System} - -\ejabberd{} is an Erlang/OTP application that runs inside an Erlang runtime system. -This system is configured using environment variables and command line parameters. -The \term{ejabberdctl} administration script uses many of those possibilities. -You can configure some of them with the file \term{ejabberdctl.cfg}, -which includes detailed description about them. -This section describes for reference purposes -all the environment variables and command line parameters. - -The environment variables: -\begin{description} - \titem{EJABBERD\_CONFIG\_PATH} - Path to the ejabberd configuration file. - \titem{EJABBERD\_MSGS\_PATH} - Path to the directory with translated strings. - \titem{EJABBERD\_LOG\_PATH} - Path to the ejabberd service log file. - \titem{EJABBERD\_SO\_PATH} - Path to the directory with binary system libraries. - \titem{EJABBERD\_DOC\_PATH} - Path to the directory with ejabberd documentation. - \titem{EJABBERD\_PID\_PATH} - Path to the PID file that ejabberd can create when started. - \titem{HOME} - Path to the directory that is considered \ejabberd{}'s home. - This path is used to read the file \term{.erlang.cookie}. - \titem{ERL\_CRASH\_DUMP} - Path to the file where crash reports will be dumped. - \titem{ERL\_EPMD\_ADDRESS} - IP address where epmd listens for connections (see section \ref{epmd}). - \titem{ERL\_INETRC} - Indicates which IP name resolution to use. - If using \term{-sname}, specify either this option or \term{-kernel inetrc filepath}. - \titem{ERL\_MAX\_PORTS} - Maximum number of simultaneously open Erlang ports. - \titem{ERL\_MAX\_ETS\_TABLES} - Maximum number of ETS and Mnesia tables. -\end{description} - -The command line parameters: -\begin{description} - \titem{-sname ejabberd} - The Erlang node will be identified using only the first part - of the host name, i.\,e. other Erlang nodes outside this domain cannot contact - this node. This is the preferable option in most cases. - \titem{-name ejabberd} - The Erlang node will be fully identified. - This is only useful if you plan to setup an \ejabberd{} cluster with nodes in different networks. - \titem{-kernel inetrc '"/etc/ejabberd/inetrc"'} - Indicates which IP name resolution to use. - If using \term{-sname}, specify either this option or \term{ERL\_INETRC}. - \titem{-kernel inet\_dist\_listen\_min 4200 inet\_dist\_listen\_min 4210} - Define the first and last ports that \term{epmd} (section \ref{epmd}) can listen to. - \titem{-kernel inet\_dist\_use\_interface "\{ 127,0,0,1 \}"} - Define the IP address where this Erlang node listens for other nodes - connections (see section \ref{epmd}). - \titem{-detached} - Starts the Erlang system detached from the system console. - Useful for running daemons and background processes. - \titem{-noinput} - Ensures that the Erlang system never tries to read any input. - Useful for running daemons and background processes. - \titem{-pa /var/lib/ejabberd/ebin} - Specify the directory where Erlang binary files (*.beam) are located. - \titem{-s ejabberd} - Tell Erlang runtime system to start the \ejabberd{} application. - \titem{-mnesia dir '"/var/lib/ejabberd/"'} - Specify the Mnesia database directory. - \titem{-sasl sasl\_error\_logger \{file, "/var/log/ejabberd/erlang.log"\}} - Path to the Erlang/OTP system log file. - SASL here means ``System Architecture Support Libraries'' - not ``Simple Authentication and Security Layer''. - \titem{+K [true|false]} - Kernel polling. - \titem{-smp [auto|enable|disable]} - SMP support. - \titem{+P 250000} - Maximum number of Erlang processes. - \titem{-remsh ejabberd@localhost} - Open an Erlang shell in a remote Erlang node. - \titem{-hidden} - The connections to other nodes are hidden (not published). - The result is that this node is not considered part of the cluster. - This is important when starting a temporary \term{ctl} or \term{debug} node. -\end{description} -Note that some characters need to be escaped when used in shell scripts, for instance \verb|"| and \verb|{}|. -You can find other options in the Erlang manual page (\shell{erl -man erl}). - -\makesection{eja-commands}{\ejabberd{} Commands} - -An \term{ejabberd command} is an abstract function identified by a name, -with a defined number and type of calling arguments and type of result -that is registered in the \term{ejabberd\_commands} service. -Those commands can be defined in any Erlang module and executed using any valid frontend. - -\ejabberd{} includes two frontends to execute \term{ejabberd commands}: the script \term{ejabberdctl} (\ref{ejabberdctl}) -and the \term{ejabberd\_xmlrpc} listener (\ref{listened-module}). -Other known frontends that can be installed to execute ejabberd commands in different ways are: -\term{mod\_rest} (HTTP POST service), -\term{mod\_shcommands} (ejabberd WebAdmin page). - -\makesubsection{list-eja-commands}{List of ejabberd Commands} - -\ejabberd{} includes a few ejabberd Commands by default as listed below. -When more modules are installed, new commands may be available in the frontends. - -The easiest way to get a list of the available commands, and get help for them is to use -the ejabberdctl script: -\begin{verbatim} -$ ejabberdctl help -Usage: ejabberdctl [--node nodename] [--auth user host password] command [options] - -Available commands in this ejabberd node: - backup file Store the database to backup file - connected_users List all established sessions - connected_users_number Get the number of established sessions - ... -\end{verbatim} - -The commands included in ejabberd by default are: -\begin{description} -\titem{stop\_kindly delay announcement} Inform users and rooms, wait, and stop the server. - Provide the delay in seconds, and the announcement quoted. -\titem{registered\_vhosts} List all registered vhosts in SERVER -\titem{reopen\_log} Reopen the log files after they were renamed. - If the old files were not renamed before calling this command, - they are automatically renamed to \term{"*-old.log"}. See section \ref{logfiles}. -\titem {convert\_to\_yaml /etc/ejabberd/ejabberd.cfg /etc/ejabberd/ejabberd-converted.yml} - Convert an old ejabberd.cfg file to the YAML syntax in a new file. -\titem {backup ejabberd.backup} - Store internal Mnesia database to a binary backup file. -\titem {restore ejabberd.backup} - Restore immediately from a binary backup file the internal Mnesia database. - This will consume a lot of memory if you have a large database, - so better use \term{install\_fallback}. -\titem {install\_fallback ejabberd.backup} - 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 \term{restore}. -\titem {dump ejabberd.dump} - Dump internal Mnesia database to a text file dump. -\titem {load ejabberd.dump} - Restore immediately from a text file dump. - This is not recommended for big databases, as it will consume much time, - memory and processor. In that case it's preferable to use \term{backup} and \term{install\_fallback}. -\titem{import\_piefxis, export\_piefxis, export\_piefxis\_host} \ind{migrate between servers} - These options can be used to migrate accounts - using \xepref{0227} formatted XML files - from/to other Jabber/XMPP servers - or move users of a vhost to another ejabberd installation. - See also \footahref{https://support.process-one.net/doc/display/MESSENGER/ejabberd+migration+kit}{ejabberd migration kit}. -\titem{import\_file, import\_dir} \ind{migration from other software} - These options can be used to migrate accounts - using jabberd1.4 formatted XML files. - from other Jabber/XMPP servers - There exist tutorials to - \footahref{http://www.ejabberd.im/migrate-to-ejabberd}{migrate from other software to ejabberd}. -\titem{set\_master nodename} - Set master node of the clustered Mnesia tables. - If you provide as nodename "self", this node will be set as its own master. -\titem{mnesia\_change\_nodename oldnodename newnodename oldbackup newbackup} - Change the erlang node name in a backup file -\titem{export2odbc virtualhost directory} \ind{export mnesia data to SQL files} - Export virtual host information from Mnesia tables to SQL files. -\titem{update\_list} List modified modules that can be updated -\titem{update module} Update the given module, or use the keyword: all -\titem{reload\_config} Reload ejabberd configuration file into memory -\titem{delete\_expired\_messages} This option can be used to delete old messages - in offline storage. This might be useful when the number of offline messages - is very high. -\titem{delete\_old\_messages days} Delete offline messages older than the given days. -\titem{incoming\_s2s\_number} Number of incoming s2s connections on the node -\titem{outgoing\_s2s\_number} Number of outgoing s2s connections on the node -\titem{register user host password} Register an account in that domain with the given password. -\titem{unregister user host} Unregister the given account. -\titem{registered\_users host} List all registered users in HOST -\titem{connected\_users} List all established sessions -\titem{connected\_users\_number} Get the number of established sessions -\titem{user\_resources user host} List user's connected resources -\titem{kick\_user user host} Disconnect user's active sessions - -\end{description} - -\makesubsection{accesscommands}{Restrict Execution with AccessCommands} - -The frontends can be configured to restrict access to certain commands -using the \term{AccessCommands}. -In that case, authentication information must be provided. - -This option allows quite complex settings, so it does not use the YAML format, -instead it uses the Erlang format. -If you want to set that option, -then you must move the frontend definition to another config file -and include it using the \term{include\_config\_file} option -(see section~\ref{includeconfigfile} and the example below). - -In each frontend the \term{AccessCommands} option is defined -in a different place. But in all cases the option syntax is the same: -\begin{verbatim} -AccessCommands = [ {Access, CommandNames, Arguments}, ...] -Access = atom() -CommandNames = all | [CommandName] -CommandName = atom() -Arguments = [ {ArgumentName, ArgumentValue}, ...] -ArgumentName = atom() -ArgumentValue = any() -\end{verbatim} - -The default value is to not define any restriction: \term{[]}. -The authentication information is provided when executing a command, -and is Username, Hostname and Password of a local XMPP account -that has permission to execute the corresponding command. -This means that the account must be registered in the local ejabberd, -because the information will be verified. - -When one or several access restrictions are defined and the -authentication information is provided, -each restriction is verified until one matches completely: -the account matches the Access rule, -the command name is listed in CommandNames, -and the provided arguments do not contradict Arguments. - -As an example to understand the syntax, let's suppose those options: -\begin{verbatim} -{hosts, ["example.org"]}. -{acl, bots, {user, "robot1", "example.org"}}. -{access, commaccess, [{allow, bots}]}. -\end{verbatim} - -This list of access restrictions allows only \term{robot1@example.org} to execute all commands: -\begin{verbatim} -[{commaccess, all, []}] -\end{verbatim} - -See another list of restrictions (the corresponding ACL and ACCESS are not shown): -\begin{verbatim} -[ - %% This bot can execute all commands: - {bot, all, []}, - %% This bot can only execute the command 'dump'. No argument restriction: - {bot_backups, [dump], []} - %% This bot can execute all commands, - %% but if a 'host' argument is provided, it must be "example.org": - {bot_all_example, all, [{host, "example.org"}]}, - %% This bot can only execute the command 'register', - %% and if argument 'host' is provided, it must be "example.org": - {bot_reg_example, [register], [{host, "example.org"}]}, - %% This bot can execute the commands 'register' and 'unregister', - %% if argument host is provided, it must be "test.org": - {_bot_reg_test, [register, unregister], [{host, "test.org"}]} -] -\end{verbatim} - -In summary, you put the frontends configurations in a CFG file using Erlang format, for example a file called \term{additional.cfg}: -\begin{verbatim} -{ejabberdctl_access_commands, [ {ctlaccess, [registered_users, register], []} ]}. - -{listen, [ - {4560, ejabberd_xmlrpc, [{maxsessions, 10}, {timeout, 5000}, - {access_commands, [ - {ctlaccess, [registered_users], [{host, "localhost"}]} - ]} - ]} - ]}. - -{modules, [ - {mod_rest, [ - {allowed_ips, [ {127,0,0,1}, {192,168,1,12} ]}, - {allowed_destinations, [ "nolan@localhost", "admin@example.com" ]}, - {allowed_stanza_types, [ "message", "presence", "iq" ]}, - {access_commands, [ - {ctlaccess, [registered_users], [{host, "localhost"}]} - ]} - ]} - ]}. -\end{verbatim} -and then add this line at the end of your main ejabberd configuration file, usually called \term{ejabberd.yml}: -\begin{verbatim} -include_config_file: "/etc/ejabberd/additional.cfg" -\end{verbatim} - -\makesection{webadmin}{Web Admin} -\ind{web admin} - -The \ejabberd{} Web Admin allows to administer most of \ejabberd{} using a web browser. - -This feature is enabled by default: -a \term{ejabberd\_http} listener with the option \term{web\_admin} (see -section~\ref{listened}) is included in the listening ports. Then you can open -\verb|http://server:port/admin/| in your favourite web browser. You -will be asked to enter the username (the \emph{full} \Jabber{} ID) and password -of an \ejabberd{} user with administrator rights. After authentication -you will see a page similar to figure~\ref{fig:webadmmain}. - -\begin{figure}[htbp] - \centering - \insimg{webadmmain.png} - \caption{Top page from the Web Admin} - \label{fig:webadmmain} -\end{figure} -Here you can edit access restrictions, manage users, create backups, -manage the database, enable/disable ports listened for, view server -statistics,\ldots - -The access rule \term{configure} determines what accounts can access the Web Admin and modify it. -The access rule \term{webadmin\_view} is to grant only view access: those accounts can browse the Web Admin with read-only access. - -Example configurations: -\begin{itemize} -\item You can serve the Web Admin on the same port as the - \ind{protocols!XEP-0025: HTTP Polling}HTTP Polling interface. In this example - you should point your web browser to \verb|http://example.org:5280/admin/| to - administer all virtual hosts or to - \verb|http://example.org:5280/admin/server/example.com/| to administer only - the virtual host \jid{example.com}. Before you get access to the Web Admin - you need to enter as username, the JID and password from a registered user - that is allowed to configure \ejabberd{}. In this example you can enter as - username `\jid{admin@example.net}' to administer all virtual hosts (first - URL). If you log in with `\jid{admin@example.com}' on \\ - \verb|http://example.org:5280/admin/server/example.com/| you can only - administer the virtual host \jid{example.com}. - The account `\jid{reviewer@example.com}' can browse that vhost in read-only mode. -\begin{verbatim} -acl: - admin: - user: - - "admin": "example.net" - -host_config: - "example.com": - acl: - admin: - user: - - "admin": "example.com" - viewers: - user: - - "reviewer": "example.com" - -access: - configure: - admin: allow - webadmin_view: - viewers: allow - -hosts: - - "example.org" - -listen: - ... - - - port: 5280 - module: ejabberd_http - web_admin: true - http_poll: true - ... -\end{verbatim} -\item For security reasons, you can serve the Web Admin on a secured - connection, on a port differing from the HTTP Polling interface, and bind it - to the internal LAN IP. The Web Admin will be accessible by pointing your - web browser to \verb|https://192.168.1.1:5282/admin/|: -\begin{verbatim} -hosts: - - "example.org" -listen: - ... - - - port: 5280 - module: ejabberd_http - http_poll: true - - - ip: "192.168.1.1" - port: 5282 - module: ejabberd_http - certfile: "/usr/local/etc/server.pem" - tls: true - web_admin: true - ... -\end{verbatim} -\end{itemize} - -Certain pages in the ejabberd Web Admin contain a link to a related -section in the ejabberd Installation and Operation Guide. -In order to view such links, a copy in HTML format of the Guide must -be installed in the system. -The file is searched by default in -\term{"/share/doc/ejabberd/guide.html"}. -The directory of the documentation can be specified in -the environment variable \term{EJABBERD\_DOC\_PATH}. -See section \ref{erlangconfiguration}. - - -\makesection{adhoccommands}{Ad-hoc Commands} - -If you enable \modconfigure\ and \modadhoc, -you can perform several administrative tasks in \ejabberd{} -with an XMPP client. -The client must support Ad-Hoc Commands (\xepref{0050}), -and you must login in the XMPP server with -an account with proper privileges. - - -\makesection{changeerlangnodename}{Change Computer Hostname} - -\ejabberd{} uses the distributed Mnesia database. -Being distributed, Mnesia enforces consistency of its file, -so it stores the name of the Erlang node in it (see section \ref{nodename}). -The name of an Erlang node includes the hostname of the computer. -So, the name of the Erlang node changes -if you change the name of the machine in which \ejabberd{} runs, -or when you move \ejabberd{} to a different machine. - -You have two ways to use the old Mnesia database in an ejabberd with new node name: -put the old node name in \term{ejabberdctl.cfg}, -or convert the database to the new node name. - -Those example steps will backup, convert and load the Mnesia database. -You need to have either the old Mnesia spool dir or a backup of Mnesia. -If you already have a backup file of the old database, you can go directly to step 5. -You also need to know the old node name and the new node name. -If you don't know them, look for them by executing \term{ejabberdctl} -or in the ejabberd log files. - -Before starting, setup some variables: -\begin{verbatim} -OLDNODE=ejabberd@oldmachine -NEWNODE=ejabberd@newmachine -OLDFILE=/tmp/old.backup -NEWFILE=/tmp/new.backup -\end{verbatim} - -\begin{enumerate} -\item Start ejabberd enforcing the old node name: -\begin{verbatim} -ejabberdctl --node $OLDNODE start -\end{verbatim} - -\item Generate a backup file: -\begin{verbatim} -ejabberdctl --node $OLDNODE backup $OLDFILE -\end{verbatim} - -\item Stop the old node: -\begin{verbatim} -ejabberdctl --node $OLDNODE stop -\end{verbatim} - -\item Make sure there aren't files in the Mnesia spool dir. For example: -\begin{verbatim} -mkdir /var/lib/ejabberd/oldfiles -mv /var/lib/ejabberd/*.* /var/lib/ejabberd/oldfiles/ -\end{verbatim} - -\item Start ejabberd. There isn't any need to specify the node name anymore: -\begin{verbatim} -ejabberdctl start -\end{verbatim} - -\item Convert the backup to new node name: -\begin{verbatim} -ejabberdctl mnesia_change_nodename $OLDNODE $NEWNODE $OLDFILE $NEWFILE -\end{verbatim} - -\item Install the backup file as a fallback: -\begin{verbatim} -ejabberdctl install_fallback $NEWFILE -\end{verbatim} - -\item Stop ejabberd: -\begin{verbatim} -ejabberdctl stop -\end{verbatim} -You may see an error message in the log files, it's normal, so don't worry: -\begin{verbatim} -Mnesia(ejabberd@newmachine): -** ERROR ** (ignoring core) -** FATAL ** A fallback is installed and Mnesia must be restarted. - Forcing shutdown after mnesia_down from ejabberd@newmachine... -\end{verbatim} - -\item Now you can finally start ejabberd: -\begin{verbatim} -ejabberdctl start -\end{verbatim} - -\item Check that the information of the old database is available: accounts, rosters... -After you finish, remember to delete the temporary backup files from public directories. -\end{enumerate} - - -\makechapter{secure}{Securing \ejabberd{}} - -\makesection{firewall}{Firewall Settings} -\ind{firewall}\ind{ports}\ind{SASL}\ind{TLS}\ind{clustering!ports} - -You need to take the following TCP ports in mind when configuring your firewall: -\begin{table}[H] - \centering - \begin{tabular}{|l|l|} - \hline {\bf Port} & {\bf Description} \\ - \hline \hline 5222& Standard port for Jabber/XMPP client connections, plain or STARTTLS.\\ - \hline 5223& Standard port for Jabber client connections using the old SSL method.\\ - \hline 5269& Standard port for Jabber/XMPP server connections.\\ - \hline 4369& EPMD (section \ref{epmd}) listens for Erlang node name requests.\\ - \hline port range& Used for connections between Erlang nodes. This range is configurable (see section \ref{epmd}).\\ - \hline - \end{tabular} -\end{table} - -\makesection{epmd}{epmd} - -\footahref{http://www.erlang.org/doc/man/epmd.html}{epmd (Erlang Port Mapper Daemon)} -is a small name server included in Erlang/OTP -and used by Erlang programs when establishing distributed Erlang communications. -\ejabberd{} needs \term{epmd} to use \term{ejabberdctl} and also when clustering \ejabberd{} nodes. -This small program is automatically started by Erlang, and is never stopped. -If \ejabberd{} is stopped, and there aren't any other Erlang programs -running in the system, you can safely stop \term{epmd} if you want. - -\ejabberd{} runs inside an Erlang node. -To communicate with \ejabberd{}, the script \term{ejabberdctl} starts a new Erlang node -and connects to the Erlang node that holds \ejabberd{}. -In order for this communication to work, -\term{epmd} must be running and listening for name requests in the port 4369. -You should block the port 4369 in the firewall in such a way that -only the programs in your machine can access it. -or configure the option \term{ERL\_EPMD\_ADDRESS} in the file \term{ejabberdctl.cfg}. - -If you build a cluster of several \ejabberd{} instances, -each \ejabberd{} instance is called an \ejabberd{} node. -Those \ejabberd{} nodes use a special Erlang communication method to -build the cluster, and EPMD is again needed listening in the port 4369. -So, if you plan to build a cluster of \ejabberd{} nodes -you must open the port 4369 for the machines involved in the cluster. -Remember to block the port so Internet doesn't have access to it. - -Once an Erlang node solved the node name of another Erlang node using EPMD and port 4369, -the nodes communicate directly. -The ports used in this case by default are random, -but can be configured in the file \term{ejabberdctl.cfg}. -The Erlang command-line parameter used internally is, for example: -\begin{verbatim} -erl ... -kernel inet_dist_listen_min 4370 inet_dist_listen_max 4375 -\end{verbatim} -It is also possible to configure in \term{ejabberdctl.cfg} -the network interface where the Erlang node will listen and accept connections. -The Erlang command-line parameter used internally is, for example: -\begin{verbatim} -erl ... -kernel inet_dist_use_interface "{127,0,0,1}" -\end{verbatim} - - -\makesection{cookie}{Erlang Cookie} - -The Erlang cookie is a string with numbers and letters. -An Erlang node reads the cookie at startup from the command-line parameter \term{-setcookie}. -If not indicated, the cookie is read from the cookie file \term{\$HOME/.erlang.cookie}. -If this file does not exist, it is created immediately with a random cookie. -Two Erlang nodes communicate only if they have the same cookie. -Setting a cookie on the Erlang node allows you to structure your Erlang network -and define which nodes are allowed to connect to which. - -Thanks to Erlang cookies, you can prevent access to the Erlang node by mistake, -for example when there are several Erlang nodes running different programs in the same machine. - -Setting a secret cookie is a simple method -to difficult unauthorized access to your Erlang node. -However, the cookie system is not ultimately effective -to prevent unauthorized access or intrusion to an Erlang node. -The communication between Erlang nodes are not encrypted, -so the cookie could be read sniffing the traffic on the network. -The recommended way to secure the Erlang node is to block the port 4369. - - -\makesection{nodename}{Erlang Node Name} - -An Erlang node may have a node name. -The name can be short (if indicated with the command-line parameter \term{-sname}) -or long (if indicated with the parameter \term{-name}). -Starting an Erlang node with -sname limits the communication between Erlang nodes to the LAN. - -Using the option \term{-sname} instead of \term{-name} is a simple method -to difficult unauthorized access to your Erlang node. -However, it is not ultimately effective to prevent access to the Erlang node, -because it may be possible to fake the fact that you are on another network -using a modified version of Erlang \term{epmd}. -The recommended way to secure the Erlang node is to block the port 4369. - - -\makesection{secure-files}{Securing Sensitive Files} - -\ejabberd{} stores sensitive data in the file system either in plain text or binary files. -The file system permissions should be set to only allow the proper user to read, -write and execute those files and directories. - -\begin{description} - \titem{ejabberd configuration file: /etc/ejabberd/ejabberd.yml} - Contains the JID of administrators - and passwords of external components. - The backup files probably contain also this information, - so it is preferable to secure the whole \term{/etc/ejabberd/} directory. - \titem{ejabberd service log: /var/log/ejabberd/ejabberd.log} - Contains IP addresses of clients. - If the loglevel is set to 5, it contains whole conversations and passwords. - If a logrotate system is used, there may be several log files with similar information, - so it is preferable to secure the whole \term{/var/log/ejabberd/} directory. - \titem{Mnesia database spool files in /var/lib/ejabberd/} - The files store binary data, but some parts are still readable. - The files are generated by Mnesia and their permissions cannot be set directly, - so it is preferable to secure the whole \term{/var/lib/ejabberd/} directory. - \titem{Erlang cookie file: /var/lib/ejabberd/.erlang.cookie} - See section \ref{cookie}. -\end{description} - - -\makechapter{clustering}{Clustering} -\ind{clustering} - -\makesection{howitworks}{How it Works} -\ind{clustering!how it works} - -A \XMPP{} domain is served by one or more \ejabberd{} nodes. These nodes can -be run on different machines that are connected via a network. They all -must have the ability to connect to port 4369 of all another nodes, and must -have the same magic cookie (see Erlang/OTP documentation, in other words the -file \term{\~{}ejabberd/.erlang.cookie} must be the same on all nodes). This is -needed because all nodes exchange information about connected users, s2s -connections, registered services, etc\ldots - -Each \ejabberd{} node has the following modules: -\begin{itemize} -\item router, -\item local router, -\item session manager, -\item s2s manager. -\end{itemize} - -\makesubsection{router}{Router} -\ind{clustering!router} - -This module is the main router of \XMPP{} packets on each node. It -routes them based on their destination's domains. It uses a global -routing table. The domain of the packet's destination is searched in the -routing table, and if it is found, the packet is routed to the -appropriate process. If not, it is sent to the s2s manager. - -\makesubsection{localrouter}{Local Router} -\ind{clustering!local router} - -This module routes packets which have a destination domain equal to -one of this server's host names. If the destination JID has a non-empty user -part, it is routed to the session manager, otherwise it is processed depending -on its content. - -\makesubsection{sessionmanager}{Session Manager} -\ind{clustering!session manager} - -This module routes packets to local users. It looks up to which user -resource a packet must be sent via a presence table. Then the packet is -either routed to the appropriate c2s process, or stored in offline -storage, or bounced back. - -\makesubsection{s2smanager}{s2s Manager} -\ind{clustering!s2s manager} - -This module routes packets to other \XMPP{} servers. First, it -checks if an opened s2s connection from the domain of the packet's -source to the domain of the packet's destination exists. If that is the case, -the s2s manager routes the packet to the process -serving this connection, otherwise a new connection is opened. - -\makesection{cluster}{Clustering Setup} -\ind{clustering!setup} - -Suppose you already configured \ejabberd{} on one machine named (\term{first}), -and you need to setup another one to make an \ejabberd{} cluster. Then do -following steps: - -\begin{enumerate} -\item Copy \verb|~ejabberd/.erlang.cookie| file from \term{first} to - \term{second}. - - (alt) You can also add `\verb|-setcookie content_of_.erlang.cookie|' - option to all `\shell{erl}' commands below. - -\item On \term{second} run the following command as the \ejabberd{} daemon user, - in the working directory of \ejabberd{}: - -\begin{verbatim} -erl -sname ejabberd \ - -mnesia dir '"/var/lib/ejabberd/"' \ - -mnesia extra_db_nodes "['ejabberd@first']" \ - -s mnesia -\end{verbatim} - - This will start Mnesia serving the same database as \node{ejabberd@first}. - You can check this by running the command `\verb|mnesia:info().|'. You - should see a lot of remote tables and a line like the following: - - Note: the Mnesia directory may be different in your system. - To know where does ejabberd expect Mnesia to be installed by default, - call \ref{ejabberdctl} without options and it will show some help, - including the Mnesia database spool dir. - -\begin{verbatim} -running db nodes = [ejabberd@first, ejabberd@second] -\end{verbatim} - - -\item Now run the following in the same `\shell{erl}' session: - -\begin{verbatim} -mnesia:change_table_copy_type(schema, node(), disc_copies). -\end{verbatim} - - This will create local disc storage for the database. - - (alt) Change storage type of the \term{scheme} table to `RAM and disc - copy' on the second node via the Web Admin. - - -\item Now you can add replicas of various tables to this node with - `\verb|mnesia:add_table_copy|' or - `\verb|mnesia:change_table_copy_type|' as above (just replace - `\verb|schema|' with another table name and `\verb|disc_copies|' - can be replaced with `\verb|ram_copies|' or - `\verb|disc_only_copies|'). - - Which tables to replicate is very dependant on your needs, you can get - some hints from the command `\verb|mnesia:info().|', by looking at the - size of tables and the default storage type for each table on 'first'. - - Replicating a table makes lookups in this table faster on this node. - Writing, on the other hand, will be slower. And of course if machine with one - of the replicas is down, other replicas will be used. - - Also \footahref{http://www.erlang.org/doc/apps/mnesia/Mnesia\_chap5.html\#5.3} - {section 5.3 (Table Fragmentation) of Mnesia User's Guide} can be helpful. - % The above URL needs update every Erlang release! - - (alt) Same as in previous item, but for other tables. - - -\item Run `\verb|init:stop().|' or just `\verb|q().|' to exit from - the Erlang shell. This probably can take some time if Mnesia has not yet - transfered and processed all data it needed from \term{first}. - - -\item Now run \ejabberd{} on \term{second} with a configuration similar as - on \term{first}: you probably do not need to duplicate `\verb|acl|' - and `\verb|access|' options because they will be taken from - \term{first}; and \verb|mod_irc| should be - enabled only on one machine in the cluster. -\end{enumerate} - -You can repeat these steps for other machines supposed to serve this -domain. - -\makesection{servicelb}{Service Load-Balancing} -\ind{component load-balancing} - -% This section never had content, should it? -% \makesubsection{componentlb}{Components Load-Balancing} - -\makesubsection{domainlb}{Domain Load-Balancing Algorithm} -\ind{options!domain\_balancing} - -\ejabberd{} includes an algorithm to load balance the components that are plugged on an \ejabberd{} cluster. It means that you can plug one or several instances of the same component on each \ejabberd{} cluster and that the traffic will be automatically distributed. - -The default distribution algorithm try to deliver to a local instance of a component. If several local instances are available, one instance is chosen randomly. If no instance is available locally, one instance is chosen randomly among the remote component instances. - -If you need a different behaviour, you can change the load balancing behaviour with the option \option{domain\_balancing}. The syntax of the option is the following: -\esyntax{domain\_balancing: BalancingCriteria} - -Several balancing criteria are available: -\begin{itemize} -\item \term{destination}: the full JID of the packet \term{to} attribute is used. -\item \term{source}: the full JID of the packet \term{from} attribute is used. -\item \term{bare\_destination}: the bare JID (without resource) of the packet \term{to} attribute is used. -\item \term{bare\_source}: the bare JID (without resource) of the packet \term{from} attribute is used. -\end{itemize} - -If the value corresponding to the criteria is the same, the same component instance in the cluster will be used. - -\makesubsection{lbbuckets}{Load-Balancing Buckets} -\ind{options!domain\_balancing\_component\_number} - -When there is a risk of failure for a given component, domain balancing can cause service trouble. If one component is failing the service will not work correctly unless the sessions are rebalanced. - -In this case, it is best to limit the problem to the sessions handled by the failing component. This is what the \term{domain\_balancing\_component\_number} option does, making the load balancing algorithm not dynamic, but sticky on a fix number of component instances. - -The syntax is: -\esyntax{domain\_balancing\_component\_number: Number} - - - -% TODO -% See also the section about ejabberdctl!!!! -%\section{Backup and Restore} -%\label{backup} -%\ind{backup} - -\makechapter{debugging}{Debugging} -\ind{debugging} - -\makesection{logfiles}{Log Files} - -An \ejabberd{} node writes three log files: -\begin{description} - \titem{ejabberd.log} is the ejabberd service log, with the messages reported by \ejabberd{} code - \titem{error.log} is the file accumulating error messages from \term{ejabberd.log} - \titem{crash.log} is the Erlang/OTP log, with the crash messages reported by Erlang/OTP using SASL (System Architecture Support Libraries) -\end{description} - -The option \term{loglevel} modifies the verbosity of the file ejabberd.log. The syntax: -\begin{description} - \titem{loglevel: Level} The standard form to set a global log level. -\end{description} - -The possible \term{Level} are: -\begin{description} - \titem{0} No ejabberd log at all (not recommended) - \titem{1} Critical - \titem{2} Error - \titem{3} Warning - \titem{4} Info - \titem{5} Debug -\end{description} -For example, the default configuration is: -\begin{verbatim} -loglevel: 4 -\end{verbatim} - -Option \term{log\_rate\_limit} is useful if you want to protect the logging -mechanism from being overloaded by excessive amount of log messages. -The syntax is: -\begin{description} - \titem{log\_rate\_limit: N} Where N is a maximum number of log messages per second. - The default value is 100. -\end{description} -When the limit is reached the similar warning message is logged: -\begin{verbatim} -lager_error_logger_h dropped 800 messages in the last second that exceeded the limit of 100 messages/sec -\end{verbatim} - -By default \ejabberd{} rotates the log files when they get grown above a certain size. -The exact value is controlled by \term{log\_rotate\_size} option. -The syntax is: -\begin{description} - \titem{log\_rotate\_size: N} Where N is the maximum size of a log file in bytes. - The default value is 10485760 (10Mb). -\end{description} - -\ejabberd{} can also rotates the log files at given date interval. -The exact value is controlled by \term{log\_rotate\_date} option. -The syntax is: -\begin{description} - \titem{log\_rotate\_date: D} Where D is a string with syntax is taken from the syntax newsyslog uses in newsyslog.conf. - The default value is \term{""} (no rotation triggered by date). -\end{description} - -However, you can rotate the log files manually. -For doing this, set \term{log\_rotate\_size} option to 0 and \term{log\_rotate\_date} -to empty list, then, when you need to rotate the files, rename and then reopen them. -The ejabberdctl command \term{reopen-log} -(please refer to section \ref{ectl-commands}) -reopens the log files, -and also renames the old ones if you didn't rename them. - -The option \term{log\_rotate\_count} defines the number of rotated files to keep -by \term{reopen-log} command. -Every such file has a numeric suffix. The exact format is: -\begin{description} - \titem{log\_rotate\_count: N} The default value is 1, - which means only \term{ejabberd.log.0}, \term{error.log.0} - and \term{crash.log.0} will be kept. -\end{description} - -\makesection{debugconsole}{Debug Console} - -The Debug Console is an Erlang shell attached to an already running \ejabberd{} server. -With this Erlang shell, an experienced administrator can perform complex tasks. - -This shell gives complete control over the \ejabberd{} server, -so it is important to use it with extremely care. -There are some simple and safe examples in the article -\footahref{http://www.ejabberd.im/interconnect-erl-nodes}{Interconnecting Erlang Nodes} - -To exit the shell, close the window or press the keys: control+c control+c. - - -\makesection{watchdog}{Watchdog Alerts} -\ind{debugging!watchdog} - -\ejabberd{} includes a watchdog mechanism that may be useful to developers -when troubleshooting a problem related to memory usage. -If a process in the \ejabberd{} server consumes more memory than the configured threshold, -a message is sent to the XMPP accounts defined with the option -\term{watchdog\_admins} -\ind{options!watchdog\_admins} in the \ejabberd{} configuration file. - -The syntax is: -\esyntax{watchdog\_admins: [JID, ...]} - -The memory consumed is measured in \term{words}: -a word on 32-bit architecture is 4 bytes, -and a word on 64-bit architecture is 8 bytes. -The threshold by default is 1000000 words. -This value can be configured with the option \term{watchdog\_large\_heap}, -or in a conversation with the watchdog alert bot. - -The syntax is: -\esyntax{watchdog\_large\_heap: Number} - -Example configuration: -\begin{verbatim} -watchdog_admins: - - "admin2@localhost" - - "admin2@example.org" -watchdog_large_heap: 30000000 -\end{verbatim} - -To remove watchdog admins, remove them in the option. -To remove all watchdog admins, set the option with an empty list: -\begin{verbatim} -watchdog_admins: [] -\end{verbatim} - -\appendix{} - -\makechapter{i18ni10n}{Internationalization and Localization} -\ind{xml:lang}\ind{internationalization}\ind{localization}\ind{i18n}\ind{l10n} - -The source code of \ejabberd{} supports localization. -The translators can edit the -\footahref{http://www.gnu.org/software/gettext/}{gettext} .po files -using any capable program (KBabel, Lokalize, Poedit...) or a simple text editor. - -Then gettext -is used to extract, update and export those .po files to the .msg format read by \ejabberd{}. -To perform those management tasks, in the \term{src/} directory execute \term{make translations}. -The translatable strings are extracted from source code to generate the file \term{ejabberd.pot}. -This file is merged with each .po file to produce updated .po files. -Finally those .po files are exported to .msg files, that have a format easily readable by \ejabberd{}. - -All built-in modules support the \texttt{xml:lang} attribute inside IQ queries. -Figure~\ref{fig:discorus}, for example, shows the reply to the following query: -\begin{verbatim} - - - -\end{verbatim} - -\begin{figure}[htbp] - \centering - \insimg{discorus.png} - \caption{Service Discovery when \texttt{xml:lang='ru'}} - \label{fig:discorus} -\end{figure} - -The Web Admin also supports the \verb|Accept-Language| HTTP header. - -\begin{figure}[htbp] - \centering - \insimg{webadmmainru.png} - \caption{Web Admin showing a virtual host when the web browser provides the - HTTP header `Accept-Language: ru'} - \label{fig:webadmmainru} -\end{figure} - - -\makechapter{releasenotes}{Release Notes} -\ind{release notes} - -Release notes are available from \footahref{http://www.process-one.net/en/ejabberd/release\_notes/}{ejabberd Home Page} - -\makechapter{acknowledgements}{Acknowledgements} - -Thanks to all people who contributed to this guide: -\begin{itemize} -\item Alexey Shchepin (\ahrefurl{xmpp:aleksey@jabber.ru}) -\item Badlop (\ahrefurl{xmpp:badlop@jabberes.org}) -\item Evgeniy Khramtsov (\ahrefurl{xmpp:xram@jabber.ru}) -\item Florian Zumbiehl (\ahrefurl{xmpp:florz@florz.de}) -\item Ludovic Bocquet (\ahrefurl{xmpp:lbocquet@jabber.org}) -\item Marcin Owsiany (\ahrefurl{xmpp:marcin.owsiany@gmail.com}) -\item Michael Grigutsch (\ahrefurl{xmpp:migri@jabber.i-pobox.net}) -\item Mickael Remond (\ahrefurl{xmpp:mremond@process-one.net}) -\item Sander Devrieze (\ahrefurl{xmpp:s.devrieze@gmail.com}) -\item Sergei Golovan (\ahrefurl{xmpp:sgolovan@nes.ru}) -\item Vsevolod Pelipas (\ahrefurl{xmpp:vsevoload@jabber.ru}) -\end{itemize} - - -\makechapter{copyright}{Copyright Information} - -Ejabberd Installation and Operation Guide.\\ -Copyright \copyright{} 2003 --- 2015 ProcessOne - -This document 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 document 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 document; if not, write to the Free Software Foundation, Inc., 51 Franklin -Street, Fifth Floor, Boston, MA 02110-1301, USA. - -%TODO: a glossary describing common terms -%\makesection{glossary}{Glossary} -%\ind{glossary} - -%\begin{description} -%\titem{c2s} -%\titem{s2s} -%\titem{STARTTLS} -%\titem{XEP} (\XMPP{} Extension Protocol) -%\titem{Resource} -%\titem{Roster} -%\titem{Transport} -%\titem{JID} (\Jabber{} ID) -%\titem{JUD} (\Jabber{} User Directory) -%\titem{vCard} -%\titem{Publish-Subscribe} -%\titem{Namespace} -%\titem{Erlang} -%\titem{Fault-tolerant} -%\titem{Distributed} -%\titem{Node} -%\titem{Tuple} -%\titem{Regular Expression} -%\titem{ACL} (Access Control List) -%\titem{IPv6} -%\titem{XMPP} -%\titem{LDAP} (Lightweight Directory Access Protocol) -%\titem{ODBC} (Open Database Connectivity) -%\titem{Virtual Hosting} - -%\end{description} - - - -% Remove the index from the HTML version to save size and bandwith. -\begin{latexonly} -\printindex -\end{latexonly} - -\end{document} diff --git a/doc/introduction.tex b/doc/introduction.tex deleted file mode 100644 index b6f4b6c93..000000000 --- a/doc/introduction.tex +++ /dev/null @@ -1,135 +0,0 @@ -\chapter{Introduction} -\label{intro} - -%% TODO: improve the feature sheet with a nice table to highlight new features. - -\quoting{I just tried out ejabberd and was impressed both by ejabberd itself and the language it is written in, Erlang. --- -Joeri} - -%ejabberd is a free and open source instant messaging server written in Erlang. ejabberd is cross-platform, distributed, fault-tolerant, and based on open standards to achieve real-time communication (Jabber/XMPP). - -\ejabberd{} is a \marking{free and open source} instant messaging server written in \footahref{http://www.erlang.org/}{Erlang/OTP}. - -\ejabberd{} is \marking{cross-platform}, distributed, fault-tolerant, and based on open standards to achieve real-time communication. - -\ejabberd{} is designed to be a \marking{rock-solid and feature rich} XMPP server. - -\ejabberd{} is suitable for small deployments, whether they need to be \marking{scalable} or not, as well as extremely big deployments. - -%\subsection{Layout with example deployment (title needs a better name)} -%\label{layout} - -%In this section there will be a graphical overview like these:\\ -%\verb|http://www.tipic.com/var/timp/timp_dep.gif| \\ -%\verb|http://www.jabber.com/images/jabber_Com_Platform.jpg| \\ -%\verb|http://www.antepo.com/files/OPN45systemdatasheet.pdf| \\ - -%A page full with names of Jabber client that are known to work with ejabberd. \begin{tiny}tiny font\end{tiny} - -%\subsection{Try It Today} -%\label{trytoday} - -%(Not sure if I will include/finish this section for the next version.) - -%\begin{itemize} -%\item Erlang REPOS -%\item Packages in distributions -%\item Windows binary -%\item source tar.gz -%\item Migration from Jabberd14 (and so also Jabberd2 because you can migrate from version 2 back to 14) and Jabber Inc. XCP possible. -%\end{itemize} - -\newpage -\section{Key Features} -\label{keyfeatures} -\ind{features!key features} - -\quoting{Erlang seems to be tailor-made for writing stable, robust servers. --- -Peter Saint-Andr\'e, Executive Director of the Jabber Software Foundation} - -\ejabberd{} is: -\begin{itemize} -\item \marking{Cross-platform:} \ejabberd{} runs under Microsoft Windows and Unix derived systems such as Linux, FreeBSD and NetBSD. - -\item \marking{Distributed:} You can run \ejabberd{} on a cluster of machines and all of them will serve the same \Jabber{} domain(s). When you need more capacity you can simply add a new cheap node to your cluster. Accordingly, you do not need to buy an expensive high-end machine to support tens of thousands concurrent users. - -\item \marking{Fault-tolerant:} You can deploy an \ejabberd{} cluster so that all the information required for a properly working service will be replicated permanently on all nodes. This means that if one of the nodes crashes, the others will continue working without disruption. In addition, nodes also can be added or replaced `on the fly'. - -\item \marking{Administrator Friendly:} \ejabberd{} is built on top of the Open Source Erlang. As a result you do not need to install an external database, an external web server, amongst others because everything is already included, and ready to run out of the box. Other administrator benefits include: -\begin{itemize} -\item Comprehensive documentation. -\item Straightforward installers for Linux, Mac OS X, and Windows. %%\improved{} -\item Web Administration. -\item Shared Roster Groups. -\item Command line administration tool. %%\improved{} -\item Can integrate with existing authentication mechanisms. -\item Capability to send announce messages. -\end{itemize} - -\item \marking{Internationalized:} \ejabberd{} leads in internationalization. Hence it is very well suited in a globalized world. Related features are: -\begin{itemize} -\item Translated to 25 languages. %%\improved{} -\item Support for \footahref{http://tools.ietf.org/html/rfc3490}{IDNA}. -\end{itemize} - -\item \marking{Open Standards:} \ejabberd{} is the first Open Source Jabber server claiming to fully comply to the XMPP standard. -\begin{itemize} -\item Fully XMPP compliant. -\item XML-based protocol. -\item \footahref{http://www.ejabberd.im/protocols}{Many protocols supported}. -\end{itemize} - -\end{itemize} - -\newpage - -\section{Additional Features} -\label{addfeatures} -\ind{features!additional features} - -\quoting{ejabberd is making inroads to solving the "buggy incomplete server" problem --- -Justin Karneges, Founder of the Psi and the Delta projects} - -Moreover, \ejabberd{} comes with a wide range of other state-of-the-art features: -\begin{itemize} -\item Modular -\begin{itemize} -\item Load only the modules you want. -\item Extend \ejabberd{} with your own custom modules. -\end{itemize} -\item Security -\begin{itemize} -\item SASL and STARTTLS for c2s and s2s connections. -\item STARTTLS and Dialback s2s connections. -\item Web Admin accessible via HTTPS secure access. -\end{itemize} -\item Databases -\begin{itemize} -\item Internal database for fast deployment (Mnesia). -\item Native MySQL support. -\item Native PostgreSQL support. -\item ODBC data storage support. -\item Microsoft SQL Server support. %%\new{} -\item Riak NoSQL database support. -\end{itemize} -\item Authentication -\begin{itemize} -\item Internal Authentication. -\item PAM, LDAP, ODBC and Riak. %%\improved{} -\item External Authentication script. -\end{itemize} -\item Others -\begin{itemize} -\item Support for virtual hosting. -\item Compressing XML streams with Stream Compression (\xepref{0138}). -\item Statistics via Statistics Gathering (\xepref{0039}). -\item IPv6 support both for c2s and s2s connections. -\item \txepref{0045}{Multi-User Chat} module with support for clustering and HTML logging. %%\improved{} -\item Users Directory based on users vCards. -\item \txepref{0060}{Publish-Subscribe} component with support for \txepref{0163}{Personal Eventing via Pubsub}. -\item Support for web clients: \txepref{0025}{HTTP Polling} and \txepref{0206}{HTTP Binding (BOSH)} services. -\item IRC transport. -\item SIP support. -\item Component support: interface with networks such as AIM, ICQ and MSN installing special tranports. -\end{itemize} -\end{itemize} diff --git a/doc/logo.png b/doc/logo.png deleted file mode 100644 index b8d17ebfd..000000000 Binary files a/doc/logo.png and /dev/null differ diff --git a/doc/msrl-dit-deep.png b/doc/msrl-dit-deep.png deleted file mode 100644 index 25afcf4fb..000000000 Binary files a/doc/msrl-dit-deep.png and /dev/null differ diff --git a/doc/msrl-dit-flat.png b/doc/msrl-dit-flat.png deleted file mode 100644 index 82d76036d..000000000 Binary files a/doc/msrl-dit-flat.png and /dev/null differ diff --git a/doc/msrl-roster-deep.png b/doc/msrl-roster-deep.png deleted file mode 100644 index aa1017ecf..000000000 Binary files a/doc/msrl-roster-deep.png and /dev/null differ diff --git a/doc/msrl-roster-flat.png b/doc/msrl-roster-flat.png deleted file mode 100644 index b998d9225..000000000 Binary files a/doc/msrl-roster-flat.png and /dev/null differ diff --git a/doc/release_notes_0.9.1.txt b/doc/release_notes_0.9.1.txt deleted file mode 100644 index 39637e4fb..000000000 --- a/doc/release_notes_0.9.1.txt +++ /dev/null @@ -1,62 +0,0 @@ - Release notes - ejabberd 0.9.1 - - This document describes the main changes from [25]ejabberd 0.9. - - The code can be downloaded from the [26]download page. - - For more detailed information, please refer to ejabberd [27]User Guide. - - -Groupchat (Multi-user chat and IRC) improvements - - The multi-user chat code has been improved to comply with the latest version - of Jabber Enhancement Proposal 0045. - - The IRC (Internet Relay Chat) features now support WHOIS and USERINFO - requests. - - -Web interface - - ejabberd modules management features have been added to the web interface. - They now allow to start or stop extension module without restarting the - ejabberd server. - - -Publish and subscribe - - It is now possible to a subscribe node with a JabberID that includes a - resource. - - -Translations - - A new script has been included to help translate ejabberd into new languages - and maintain existing translations. - - As a result, ejabberd is now translating into 10 languages: - * Dutch - * English - * French - * German - * Polish - * Portuguese - * Russian - * Spanish - * Swedish - * Ukrainian - - -Migration - - No changes have been made to the database. No particular conversion steps - are needed. However, you should backup your database before upgrading to a - new ejabberd version. - - -Bugfixes - - This release contains several bugfixes and architectural changes. Please - refer to the Changelog file supplied with this release for details of all - improvements in the ejabberd code. diff --git a/doc/release_notes_0.9.8.txt b/doc/release_notes_0.9.8.txt deleted file mode 100644 index b9b65b63b..000000000 --- a/doc/release_notes_0.9.8.txt +++ /dev/null @@ -1,99 +0,0 @@ - Release notes - ejabberd 0.9.8 - 2005-08-01 - - This document describes the main changes in ejabberd 0.9.8. This - version prepares the way for the release of ejabberd 1.0, which - is due later this year. - - The code can be downloaded from the Process-one website: - http://www.process-one.net/en/projects/ejabberd/ - - For more detailed information, please refer to ejabberd User Guide - on the Process-one website: - http://www.process-one.net/en/projects/ejabberd/docs.html - - - Recent changes include.... - - -Enhanced virtual hosting - - Virtual hosting applies to many more setting options and - features and is transparent. Virtual hosting accepts different - parameters for different virtual hosts regarding the following - features: authentication method, access control lists and access - rules, users management, statistics, and shared roster. The web - interface gives access to each virtual host's parameters. - - -Enhanced Publish-Subscribe module - - ejabberd's Publish-Subscribe module integrates enhancements - coming from J-EAI, an XMPP-based integration server built on - ejabberd. ejabberd thus supports Publish-Subscribe node - configuration. It is possible to define nodes that should be - persistent, and the number of items to persist. Besides that, it - is also possible to define various notification parameters, such - as the delivery of the payload with the notifications, and the - notification of subscribers when some changes occur on items. - Other examples are: the maximum size of the items payload, the - subscription approvers, the limitation of the notification to - online users only, etc. - - -Code reorganisation and update - - - The mod_register module has been cleaned up. - - ODBC support has been updated and several bugs have been fixed. - - -Development API - - To ease the work of Jabber/XMPP developers, a filter_packet hook - has been added. As a result it is possible to develop plugins to - filter or modify packets flowing through ejabberd. - - -Translations - - - Translations have been updated to support the new Publish-Subscribe features. - - A new Brazilian Portuguese translation has been contributed. - - -Web interface - - - The CSS stylesheet from the web interface is W3C compliant. - - -Installers - - Installers are provided for Microsoft Windows and Linux/x86. The - Linux installer includes Erlang ASN.1 modules for LDAP - authentication support. - - -Bugfixes - - - This release contains several bugfixes and architectural - changes. Among other bugfixes include improvements in LDAP - authentication. Please refer to the ChangeLog file supplied - with this release regarding all improvements in ejabberd. - - -References - - The ejabberd feature sheet helps comparing with other Jabber/XMPP - servers: - http://www.process-one.net/en/projects/ejabberd/docs/features.pdf - - Contributed tutorials of interest are: - - Migration from Jabberd1.4 to ejabberd: - http://ejabberd.jabber.ru/jabberd1-to-ejabberd - - Migration from Jabberd2 to ejabberd: - http://ejabberd.jabber.ru/jabberd2-to-ejabberd - - Transport configuration for connecting to other networks: - http://ejabberd.jabber.ru/tutorials-transports - -END - diff --git a/doc/release_notes_0.9.txt b/doc/release_notes_0.9.txt deleted file mode 100644 index 6bdce830f..000000000 --- a/doc/release_notes_0.9.txt +++ /dev/null @@ -1,88 +0,0 @@ - Release notes - ejabberd 0.9 - - This document describes the major new features of and changes to - ejabberd 0.9, compared to latest public release ejabber 0.7.5. - - For more detailed information, please refer to ejabberd User - Guide. - - -Virtual Hosting - - ejabberd now can host several domain on the same instance. - This option is enabled by using: - - {hosts, ["erlang-projects.org", "erlang-fr.org"]}. - - instead of the previous host directive. - - Note that you are now using a list of hosts. The main one should - be the first listed. See migration section further in this release - note for details. - - -Shared Roster - - Shared roster is a new feature that allow the ejabberd - administrator to add jabber user that will be present in the - roster of every users on the server. - Shared roster are enabled by adding: - - {mod_shared_roster, []} - - at the end of your module list in your ejabberd.cfg file. - - -PostgreSQL (ODBC) support - - This feature is experimental and not yet properly documented. This - feature is released for testing purpose. - - You need to have Erlang/OTP R10 to compile with ODBC on various - flavour of *nix. You should use Erlang/OTP R10B-4, as this task - has became easier with this release. It comes already build in - Erlang/OTP Microsoft Windows binary. - - PostgreSQL support is enabled by using the following module in - ejabberd.cfg instead of their standard counterpart: - - mod_last_odbc.erl - mod_offline_odbc.erl - mod_roster_odbc.erl - - The database schema is located in the src/odbc/pq.sql file. - - Look at the src/ejabberd.cfg.example file for more information on - how to configure ejabberd with odbc support. You can get support - on how to configure ejabberd with a relational database. - - -Migration from ejabberd 0.7.5 - - Migration is pretty straightforward as Mnesia database schema - conversions is handled automatically. Remember however that you - must backup your ejabberd database before migration. - - Here are the following steps to proceed: - - 1. Stop your instance of ejabberd. - - 2. In ejabberd.cfg, define the host lists. Change the host - directive to the hosts one: - Before: - {host, "erlang-projects.org"}. - After: - {hosts, ["erlang-projects.org", "erlang-fr.org"]}. - Note that when you restart the server the existing users will be - affected to the first virtual host, so the order is important. You - should keep the previous hostname as the first virtual host. - - 3. Restart ejabberd. - - -Bugfixes - - This release contains several bugfixes and architectural changes. - Please refer to the Changelog file supplied with this release for - details of all improvements in the ejabberd code. diff --git a/doc/release_notes_1.0.0.txt b/doc/release_notes_1.0.0.txt deleted file mode 100644 index 426ba63fa..000000000 --- a/doc/release_notes_1.0.0.txt +++ /dev/null @@ -1,120 +0,0 @@ - Release Notes - ejabberd 1.0.0 - 14 December 2005 - - This document describes the main changes in ejabberd 1.0.0. Unique in this - version is the compliancy with the XMPP (eXtensible Messaging and Presence - Protocol) standard. ejabberd is the first Open Source Jabber server claiming - to fully comply to the XMPP standard. - - ejabberd can be downloaded from the Process-one website: - http://www.process-one.net/en/projects/ejabberd/ - - Detailed information can be found in the ejabberd Feature Sheet and User - Guide which are available on the Process-one website: - http://www.process-one.net/en/projects/ejabberd/docs.html - - - Recent changes include: - - -Server-to-server Encryption for Enhanced Security - - - Support for STARTTLS and SASL EXTERNAL to secure server-to-server traffic - has been added. - - Also, STARTTLS and Dialback has been implemented for server-to-server (s2s) - connections. Detailed information about these new features can be found on - http://ejabberd.jabber.ru/s2s-encryption - - commonName and dNSName fields matching were introduced to ease the process - of retrieving certificates. - - Different certificates can be defined for each virtual host. - -ODBC Support - - - ODBC support has been improved to allow production use of ejabberd with - relational databases. - - Support for vCard storage in ODBC has been added. - - ejd2odbc.erl is a tool to convert an installation from Erlang's database - Mnesia to an ODBC compatible relational database. - -Native PostgreSQL Support - - - Native PostgreSQL support gives you a better performance when you use - PostgreSQL. - -Shared Roster groups - - - Shared Roster groups support has been enhanced. New is the ability to add - all registered users to everyone's roster. Detailed information about this - new feature can be found on http://ejabberd.jabber.ru/shared-roster-all - -Web Interface - - - The web interface internal code has been modified for better integration - and compliancy with J-EAI, an ejabberd-based Enterprise Application - Integration platform. - - More XHTML 1.0 Transitional compliancy work was done. - -Transports - - - A transport workaround can be enabled during compilation. To do this, you - can pass the "--enable-roster-gateway-workaround" option to the configure - script. (./configure --enable-roster-gateway-workaround) - This option allows transports to add items with subscription "to" in the - roster by sending stanza to user. This option - is only needed for JIT ICQ transport. - Warning: by enabling this option, ejabberd will not be fully XMPP compliant - anymore. - -Documentation and Internationalization - - - Documentation has been extended to cover more topics. - - Translations have been updated. - -Bugfixes - - - This release contains several bugfixes. - - Among other bugfixes include improvements to the client-to-server (c2s) - connection management module. - - Please refer to the ChangeLog file supplied - with this release regarding all improvements in ejabberd. - - - Installation Notes - - -Supported Erlang Version - - - You need at least Erlang/OTP R9C to run ejabberd 1.0.0. - -Installation - - Installers are provided for Microsoft Windows and Linux/x86. - Installers can be retrieved from: - http://www.process-one.net/en/projects/ejabberd/download.html - -Migration Notes - - - Before any migration, ejabberd system and database must be properly - backed up. - - When upgrading an ODBC-based installation, you will need to change the - relational database schema. The following SQL commands must be run on the - database: - CREATE SEQUENCE spool_seq_seq; - ALTER TABLE spool ADD COLUMN seq integer; - ALTER TABLE spool ALTER COLUMN seq SET DEFAULT nextval('spool_seq_seq'); - UPDATE spool SET seq = DEFAULT; - ALTER TABLE spool ALTER COLUMN seq SET NOT NULL; - -References - - Contributed tutorials of interest are: - - Migration from Jabberd1.4 to ejabberd: - http://ejabberd.jabber.ru/jabberd1-to-ejabberd - - Migration from Jabberd2 to ejabberd: - http://ejabberd.jabber.ru/jabberd2-to-ejabberd - - Transport configuration for connecting to other networks: - http://ejabberd.jabber.ru/tutorials-transports - -END - diff --git a/doc/release_notes_1.1.0.txt b/doc/release_notes_1.1.0.txt deleted file mode 100644 index fbef6a934..000000000 --- a/doc/release_notes_1.1.0.txt +++ /dev/null @@ -1,115 +0,0 @@ - Release Notes - ejabberd 1.1.0 - 24 April 2006 - - This document describes the main changes in ejabberd 1.1.0. This version - introduce new features including support for new Jabber Enhancement - Proposals and several performance improvements enabling deployments on an - even larger scale than already possible. - - ejabberd can be downloaded from the Process-one website: - http://www.process-one.net/en/projects/ejabberd/ - - Detailed information can be found in the ejabberd Feature Sheet and User - Guide which are available on the Process-one website: - http://www.process-one.net/en/projects/ejabberd/docs.html - - A complete list of changes is available from: - http://support.process-one.net/secure/ReleaseNote.jspa?projectId=10011&styleName=Html&version=10025 - - - Recent changes include: - - -New Jabber Enhancement Proposal support: - - - JEP-0050: Ad-Hoc Commands. - - JEP-0138: Stream Compression. - - JEP-0175: SASL anonymous. - -Anonymous login - - - SASL anonymous. - - Anonymous login for clients that do not yet support SASL Anonymous. - -Relational database Support - - - MySQL is now fully supported through ODBC and in native mode. - - Various improvements to the native database interfaces. - - The migration tool can use relational databases. - -Multi-User Chat improvements - - - Logging of room discussion to text file is now supported. - - Better reconfiguration support. - - Security oriented fixes. - - Several improvements and updates to latest JEP-0045. - -Performance scalability improvements for large clusters - - - Improved session synchronisation management between cluster nodes. - - Internal architecture has been reworked to use generalize Erlang/OTP - framework usage. - - Speed improvement on logger. - - TCP/IP packet reception change for better network throttling and - regulation. - As a result, these improvements will reduce load on large scale deployments. - -XMPP Protocol related improvements - - - XML stanza size can be limited. - - Messages are send to all resources with the same highest priority. - -Documentation and Internationalization - - - Documentation has been extended to cover more topics. - - Translations have been updated. - -Web interface - - - XHTML 1.0 compliance. - -Bugfixes - - - This release contains many bugfixes on various areas such as Publish-Subscribe, build - chain, installers, IRC gateway, ejabberdctl, amongst others. - - Please refer to the ChangeLog file supplied with this release regarding - all improvements in ejabberd. - - - - Installation Notes - -Supported Erlang Version - - - You need at least Erlang/OTP R9C-2 to run ejabberd 1.1.0. - -Installation - - Installers are provided for Microsoft Windows, Linux/x86 and MacOSX/PPC. - Installers can be retrieved from: - http://www.process-one.net/en/projects/ejabberd/download.html - -Migration Notes - - - Before any migration, ejabberd system and database must be properly - backed up. - - The database schema has not been changed comparing to version 1.0.0 and - consequently it does not require any migration. - - -References - - Contributed tutorials and documents of interest are: - - Migration from Jabberd1.4, Jabberd2 and WPJabber to ejabberd: - http://ejabberd.jabber.ru/migrate-to-ejabberd - - Transport configuration for connecting to other networks: - http://ejabberd.jabber.ru/tutorials-transports - - Using ejabberd with MySQL native driver: - http://support.process-one.net/doc/display/MESSENGER/Using+ejabberd+with+MySQL+native+driver - - Anonymous User Support: - http://support.process-one.net/doc/display/MESSENGER/Anonymous+users+support - - Frequently Asked Questions: - http://ejabberd.jabber.ru/faq - -END diff --git a/doc/release_notes_1.1.1.txt b/doc/release_notes_1.1.1.txt deleted file mode 100644 index e2f4273da..000000000 --- a/doc/release_notes_1.1.1.txt +++ /dev/null @@ -1,119 +0,0 @@ - Release Notes - ejabberd 1.1.1 - 28 April 2006 - - This document describes the main changes in ejabberd 1.1.x. This version - introduce new features including support for new Jabber Enhancement - Proposals and several performance improvements enabling deployments on an - even larger scale than already possible. - - This release fix a security issue introduced in ejabberd 1.1.0. In SASL - mode, anonymous login was enabled as a default. Upgrading ejabberd 1.1.0 to - ejabberd 1.1.1 is highly recommended. - - ejabberd can be downloaded from the Process-one website: - http://www.process-one.net/en/projects/ejabberd/ - - Detailed information can be found in the ejabberd Feature Sheet and User - Guide which are available on the Process-one website: - http://www.process-one.net/en/projects/ejabberd/docs.html - - A complete list of changes is available from: - http://support.process-one.net/secure/ReleaseNote.jspa?projectId=10011&styleName=Html&version=10025 - - - Recent changes include: - - -New Jabber Enhancement Proposal support: - - - JEP-0050: Ad-Hoc Commands. - - JEP-0138: Stream Compression. - - JEP-0175: SASL anonymous. - -Anonymous login - - - SASL anonymous. - - Anonymous login for clients that do not yet support SASL Anonymous. - -Relational database Support - - - MySQL is now fully supported through ODBC and in native mode. - - Various improvements to the native database interfaces. - - The migration tool can use relational databases. - -Multi-User Chat improvements - - - Logging of room discussion to text file is now supported. - - Better reconfiguration support. - - Security oriented fixes. - - Several improvements and updates to latest JEP-0045. - -Performance scalability improvements for large clusters - - - Improved session synchronisation management between cluster nodes. - - Internal architecture has been reworked to use generalize Erlang/OTP - framework usage. - - Speed improvement on logger. - - TCP/IP packet reception change for better network throttling and - regulation. - As a result, these improvements will reduce load on large scale deployments. - -XMPP Protocol related improvements - - - XML stanza size can be limited. - - Messages are send to all resources with the same highest priority. - -Documentation and Internationalization - - - Documentation has been extended to cover more topics. - - Translations have been updated. - -Web interface - - - XHTML 1.0 compliance. - -Bugfixes - - - This release contains many bugfixes on various areas such as Publish-Subscribe, build - chain, installers, IRC gateway, ejabberdctl, amongst others. - - Please refer to the ChangeLog file supplied with this release regarding - all improvements in ejabberd. - - - - Installation Notes - -Supported Erlang Version - - - You need at least Erlang/OTP R9C-2 to run ejabberd 1.1.0. - -Installation - - Installers are provided for Microsoft Windows, Linux/x86 and MacOSX/PPC. - Installers can be retrieved from: - http://www.process-one.net/en/projects/ejabberd/download.html - -Migration Notes - - - Before any migration, ejabberd system and database must be properly - backed up. - - The database schema has not been changed comparing to version 1.0.0 and - consequently it does not require any migration. - - -References - - Contributed tutorials and documents of interest are: - - Migration from Jabberd1.4, Jabberd2 and WPJabber to ejabberd: - http://ejabberd.jabber.ru/migrate-to-ejabberd - - Transport configuration for connecting to other networks: - http://ejabberd.jabber.ru/tutorials-transports - - Using ejabberd with MySQL native driver: - http://support.process-one.net/doc/display/MESSENGER/Using+ejabberd+with+MySQL+native+driver - - Anonymous User Support: - http://support.process-one.net/doc/display/MESSENGER/Anonymous+users+support - - Frequently Asked Questions: - http://ejabberd.jabber.ru/faq - -END diff --git a/doc/release_notes_1.1.2.txt b/doc/release_notes_1.1.2.txt deleted file mode 100644 index e7c8f3551..000000000 --- a/doc/release_notes_1.1.2.txt +++ /dev/null @@ -1,119 +0,0 @@ - Release Notes - ejabberd 1.1.2 - 27 September 2006 - - This document describes the main changes in ejabberd 1.1.2. - - This version is a major improvement over ejabberd 1.1.1, improving the - overall behaviour of the server in many areas. Users of ejabberd 1.1.1 - should upgrade to this new release for improved robustness and compliance. - - ejabberd can be downloaded from the Process-one website: - http://www.process-one.net/en/projects/ejabberd/ - - Detailed information can be found in the Feature Sheet and in the - Installation and Operation Guide which are both available on the - Process-one website: - http://www.process-one.net/en/projects/ejabberd/docs.html - - ejabberd includes 44 improvements. A complete list of changes can be - retrieved from: - http://redir.process-one.net/ejabberd-1.1.2 - - - Recent changes include: - -LDAP Improvements - - - Major improvements have been made on the LDAP module. It is now more - flexible and more robust. - -HTTP Polling Fixes - - - The HTTP polling modules have been fixed and improved: the connections are - closed properly and polled messages cannot be lost anymore. - -Roster Management Improvement - - - Roster management improvements increase reliability, especially in cases - where users are on different servers. - - Shared rosters are more reliable. - -Improved Robustness - - - It is now possible to limit the number of opened connections for a single - user. - -Relational databases - - - Database support: Microsoft SQL Server is now officially supported in ODBC - mode. - -Publish-Subscribe Improvement - - - Restricting node creation with a dedicated ACL rule is now possible. - -Localization - - - A Czech translation has been added. - - Translations have been updated. - -Binary Installer - - - New binary installer for Windows including all requirements. - - Improved installers for Linux and MacOSX (PowerPC) - -XMPP Compliancy - - - Some protocol compliance fix have been added, after the Portland XMPP - Interop Meeting in July. - -Miscelanous - - - MUC have been improved (logging rendering). - - The command line tool ejabberdctl has been improved. - - The build chain has been improved, including MacOSX support. - - The documentation has been improved and updated to describe the new - features. - -Bugfixes - - - Anonymous login bugfixes. - - Please refer to the ChangeLog file supplied with this release regarding - all improvements in ejabberd. - - - Installation Notes - -Supported Erlang Version - - - You need at least Erlang/OTP R9C-2 to run ejabberd 1.1.2. - - The recommanded version is Erlang/OTP R10B-10. - - Erlang/OTP R11B has not yet been fully certified for ejabberd. - -Installation - - Installers are provided for Microsoft Windows, Linux/x86 and MacOSX/PPC. - They can be retrieved from: - http://www.process-one.net/en/projects/ejabberd/download.html - -Migration Notes - - - Before any migration, ejabberd system and database must be properly - backed up. - - The relational database schema has changed between version 1.1.1 and - 1.1.2. An "askmessage" column needs to be added in the "rosterusers" table - to perform the migration. - - -References - - Contributed tutorials and documents of interest are: - - Migration from other XMPP servers to ejabberd: - http://ejabberd.jabber.ru/migrate-to-ejabberd - - Transport configuration for connecting to other networks: - http://ejabberd.jabber.ru/tutorials-transports - - Frequently Asked Questions: - http://ejabberd.jabber.ru/faq - -END diff --git a/doc/release_notes_1.1.3.txt b/doc/release_notes_1.1.3.txt deleted file mode 100644 index f298f2bab..000000000 --- a/doc/release_notes_1.1.3.txt +++ /dev/null @@ -1,14 +0,0 @@ - Release Notes - ejabberd 1.1.3 - 2 February 2007 - - ejabberd 1.1.3 is a security fix release for ejabberd roster ODBC - module. - - The upgrade is only necessary if you are using ejabberd with the - mod_roster_odbc. - - The new code can be downloaded from ejabberd download page: - http://www.process-one.net/en/projects/ejabberd/ - -END diff --git a/doc/release_notes_1.1.4.txt b/doc/release_notes_1.1.4.txt deleted file mode 100644 index 76c108904..000000000 --- a/doc/release_notes_1.1.4.txt +++ /dev/null @@ -1,31 +0,0 @@ - Release Notes - ejabberd 1.1.4 - 3 september 2007 - - ejabberd 1.1.4 is a bugfix release for ejabberd 1.1.x branch. - - ejabberd 1.1.4 includes 10 improvements. A complete list of changes - can be retrieved from: - http://redir.process-one.net/ejabberd-1.1.4 - - The new code can be downloaded from ejabberd download page: - http://www.process-one.net/en/projects/ejabberd/ - - - Recent changes include: - -- Better LDAP support. -- Support for UTF-8 with MySQL 5. -- Roster migration script bugfixes. -- Performance improvements on user removal. -- Traffic shapers bugfix. -- Configuration: host value is now case insensitive. -- Build: ejabberd.cfg file is not overwritten with 'make install' command. - - -Bugs report - - You can officially report bugs on Process-one support site: - http://support.process-one.net/ - -END diff --git a/doc/release_notes_2.0.0.txt b/doc/release_notes_2.0.0.txt deleted file mode 100644 index 963a65246..000000000 --- a/doc/release_notes_2.0.0.txt +++ /dev/null @@ -1,208 +0,0 @@ - - Release Notes - ejabberd 2.0.0 - 21 February 2008 - - ejabberd 2.0.0 is a major new version for ejabberd adding plenty of - new features, performance and scalability improvements and - architectural changes. - - ejabberd 2.0.0 includes more than 200 improvements over ejabberd - 1.1.x. A complete list of changes can be retrieved from: - http://redir.process-one.net/ejabberd-2.0.0 - - The new code can be downloaded from ejabberd downloads page: - http://www.process-one.net/en/ejabberd/ - - - Recent changes include: - - -* Clustering and Architecture - -- New front-end and back-end cluster architecture for better - scalability and robustness. Back-end nodes are able to run a fully - fault-tolerant XMPP router and services, but you can now deploy - many front-end nodes to share the load without needing to synchronize - any state with the back-ends. -- All components now run in cluster mode (For example, Multi-User chat - service and file transfer proxy). -- New load balancing algorithm to support Multi-User chat and gateways - clustering. More generally it supports any external component load - balancing. -- ejabberd watchdog to receive warning on suspicious resources consumption. -- Traffic shapers are now supported on components. This protect - ejabberd from components and gateways abuses. - - -* Publish and Subscribe - -- Complete rewrite of the PubSub module. The new PubSub module is - plugin-based, allowing developers to create new nodes type. Any - application can be plugged to ejabberd and can provide rich presence - as a pubsub plugin. -- Personal Eventing via Pubsub support (XEP-0163). This module is - implemented as a PubSub service. It supports user mood (XEP-107), - User Tune (XEP-118), user location (XEP-0080) or user avatar - (XEP-0084) for example. - - -* Server to Server (s2s) - -- More robust code with connection timeout implementation. -- Support for multiple s2s connections per domain. -- s2s whitelist and blacklist support. -- s2s retrial interval. - - -* LDAP - -- Many enterprise-class enhancements such as better behaviour under - heavy load. -- Support for LDAP servers pool. -- Simplified use of virtual hosting with LDAP with domain substitution - in config. -- Ability to match on several userid attributes. - - -* Multi-User Chat - -- Clustering and load balancing support. -- Ability to define default room configuration in ejabberd config file. -- Many anti abuse features have been added: - . New ACL to limit the creation of persistent room to authorized users. - . Ability to define the maximum number of users per room. - . Limitation of the rate of message and presence packets. - . Limitation of the maximum number of room a user can join at the same time. - - -* File Transfer - -- XEP-0065 - Proxy65 file transfer proxy. The proxy can run in - cluster mode. - - -* Authentication - -- PAM (Pluggable Authentication Modules) support on *nix systems. -- External Authentication protocol is now fully documented. - - -* Web Client Support - -- XEP-0124 - BOSH support: BOSH (Bidirectional-streams Over - Synchronous HTTP) was formerly known as "HTTP binding". It provides - an efficient alternative to HTTP polling for scalable Web based chat - solutions. -- HTTP module can now serve static documents (with - mod_http_fileserver). It is needed for high-performance Web 2.0 chat - / IM application. System administrators can now avoid using a proxy - (like Apache) that handles much less simultaneous than ejabberd HTTP - module. -- Added limitations enforcement on HTTP poll and HTTP bind modules - (bandwidth, packet size). - - -* System Administration - -- XEP-0133 - Service administration support. System administrators can - now perform lot of ejabberd related admin tasks from their XMPP - client, through adhoc commands. -- Dynamic log levels: Improved logging with more log levels. You can - now change the loglevel at run time. No performance penalty is - involved when less verbose levels are used. -- The ejabberdctl command-line administration script now can start - and stop ejabberd. It also includes other useful options. - - -* Localization - -- ejabberd is now translated to 24 languages: Catalan, Chinese, Czech, - Dutch, English, Esperanto, French, Galician, German, Italian, Japanese, - Norwegian, Polish, Portuguese, Portuguese (Brazil), Russian, Slovak, - Spanish, Swedish, Thai, Turkish, Ukrainian, Vietnamese, Walloon. - - -* Build and Installer - -- Many launch script improvements. -- New translations. The binary installer is now available in Chinese, - Dutch, English, French, German, Spanish, Russian. -- Makefile now implements uninstall command. -- Full MacOSX compliance in Makefile. -- Configure script is clever at finding libraries in unusual places. - - -* Development API - -- Several hooks have been added for module developers (most notably - presence related hooks). -- HTTP request handler to write HTTP based plugins. -- Manage connections IP address. - - -* Bugfixes - -- ejabberd 2.0.0 also fixes numerous small bugs :) Read the full - changelog for details. - - - - Important Note: - -- Since this release, ejabberd requires Erlang R10B-5 or higher. - R11B-5 is the recommended version. R12 is not yet officially - supported, and is not recommended for production servers. - - - - Upgrading From ejabberd 1.x: - -- If you upgrade from a version older than 1.1.4, please check the - Release Notes of the intermediate versions for additional - information about database or configuration changes. - -- The database schemas didn't change since ejabberd 1.1.4. Of course, - you are encouraged to make a database backup of your SQL database, - or your Mnesia spool directory before upgrading ejabberd. - -- The ejabberdctl command line administration script is improved in - ejabberd 2.0.0, and now it can start and stop ejabberd. If you - already wrote your own start script for ejabberd 1.x, you can - continue using it, or try ejabberdctl. For your convenience, the - ejabberd Guide describes all the ejabberd and Erlang options used by - ejabberdctl. - -- The example ejabberd.cfg file has been reorganized, but its format - and syntax rules are the same. So, you can continue using your - ejabberd.cfg file from 1.x if you want. The most important changes - are described now. - -- The 'ssl' option is no longer available in the listening ports. For - legacy SSL encryption use the option 'tls'. For STARTTLS encryption - as defined in RFC 3920 XMPP-CORE use the option 'starttls'. Check - the ejabberd Guide for more information about configuring listening - ports. - -- The options 'welcome_message' and 'registration_watchers' are now - options of the module mod_register. Check in the ejabberd Guide how - to configure that module. - -- To enable PEP support in mod_pubsub, you need to enable it in the - mod_pubsub configuration, and also enable the new module - mod_caps. Check the section about mod_pubsub in the ejabberd Guide. - -- Other new features and improvements also require changes in the - ejabberd.cfg, like mod_http_bind, mod_http_fileserver, mod_proxy65, - loglevel, pam_service, and watchdog_admins. Search for those words - in the ejabberd Guide and the example ejabberd.cfg. - - - - Bug Reports - - You can officially report bugs on Process-one support site: - https://support.process-one.net/ - - -END \ No newline at end of file diff --git a/doc/release_notes_2.0.1.txt b/doc/release_notes_2.0.1.txt deleted file mode 100644 index b9b075576..000000000 --- a/doc/release_notes_2.0.1.txt +++ /dev/null @@ -1,30 +0,0 @@ - - Release Notes - ejabberd 2.0.1 - 20 May 2008 - - ejabberd 2.0.1 is a bugfix release for ejabberd 2.0.x branch. - - ejabberd 2.0.1 includes 10 improvements and 32 bugfixes. - A complete list of changes can be retrieved from: - http://redir.process-one.net/ejabberd-2.0.1 - - The new code can be downloaded from ejabberd download page: - http://www.process-one.net/en/projects/ejabberd/ - - - Recent changes include: - -- Erlang R12 support. -- Better LDAP handling. -- PubSub bugfixes. -- Documentation improvements. -- inband registration limitation per IP. -- s2s improvements. - -Bugs report - - You can officially report bugs on Process-one support site: - http://support.process-one.net/ - -END diff --git a/doc/release_notes_2.0.2.txt b/doc/release_notes_2.0.2.txt deleted file mode 100644 index 767ae1367..000000000 --- a/doc/release_notes_2.0.2.txt +++ /dev/null @@ -1,34 +0,0 @@ - - Release Notes - ejabberd 2.0.2 - 28 August 2008 - - ejabberd 2.0.2 is the second bug fix release for ejabberd 2.0.x branch. - - ejabberd 2.0.2 includes many bugfixes and a few improvements. - A complete list of changes can be retrieved from: - http://redir.process-one.net/ejabberd-2.0.2 - - The new code can be downloaded from ejabberd download page: - http://www.process-one.net/en/ejabberd/ - - - Recent changes include: - -- Anti-abuse feature: client blacklist support by IP. -- Guide: new section Securing ejabberd; improved usability. -- LDAP filter optimisation: ability to filter user in ejabberd and not LDAP. -- MUC improvements: room options to restrict visitors; broadcast reasons. -- Privacy rules: fix MySQL storage. -- Pub/Sub and PEP: many improvements in implementation and protocol compliance. -- Proxy65: send valid SOCKS5 reply (removed support for Psi < 0.10). -- Web server embedded: better support for HTTPS. -- Binary installers: SMP on Windows; don't remove config when uninstalling. - - - Bug reports - - You can officially report bugs on ProcessOne support site: - http://support.process-one.net/ - -END diff --git a/doc/release_notes_2.0.3.txt b/doc/release_notes_2.0.3.txt deleted file mode 100644 index 13fe9a174..000000000 --- a/doc/release_notes_2.0.3.txt +++ /dev/null @@ -1,35 +0,0 @@ - - Release Notes - ejabberd 2.0.3 - 14 January 2009 - - ejabberd 2.0.3 is the third bugfix release for ejabberd 2.0.x branch. - - ejabberd 2.0.3 includes several bugfixes and a few improvements. - A complete list of changes can be retrieved from: - http://redir.process-one.net/ejabberd-2.0.3 - - The new code can be downloaded from ejabberd download page: - http://www.process-one.net/en/ejabberd/ - - - Recent changes include: - -- Do not ask certificate for client (c2s) -- Check digest-uri in SASL digest authentication -- Use send timeout to avoid locking on gen_tcp:send -- Fix ejabberd reconnection to database -- HTTP-Bind: handle wrong order of packets -- MUC: Improve traffic regulation management -- PubSub: Several bugfixes and improvements for best coverage of XEP-0060 v1.12 -- Shared Roster Groups: push immediately membership changes -- Rotate also sasl.log on "reopen-log" command -- Binary Windows installer: better detect "Error running Post Install Script" - - - Bug reports - - You can officially report bugs on ProcessOne support site: - http://support.process-one.net/ - -END diff --git a/doc/release_notes_2.0.4.txt b/doc/release_notes_2.0.4.txt deleted file mode 100644 index 24f943779..000000000 --- a/doc/release_notes_2.0.4.txt +++ /dev/null @@ -1,44 +0,0 @@ - - Release Notes - ejabberd 2.0.4 - - ejabberd 2.0.4 is the fourth bugfix release for ejabberd 2.0.x branch. - - ejabberd 2.0.4 includes several bugfixes. - A detailed list of changes can be retrieved from: - http://redir.process-one.net/ejabberd-2.0.4 - - The new code can be downloaded from ejabberd download page: - http://www.process-one.net/en/ejabberd/ - - - The changes are: - -- Ensure ID attribute in roster push is unique -- Authentication: Fix Anonymous auth when enabled with broken ODBC -- Authentication: Unquote correctly backslash in DIGEST-MD5 SASL responses -- Authentication: Cancel presence subscriptions on account deletion -- LDAP: Close a connection on tcp_error -- LDAP: Implemented queue for pending queries -- LDAP: On failure of LDAP connection, waiting is done on pending queue -- MUC: Owner of a password protected room must also provide the password -- MUC: Prevent XSS in MUC logs by linkifying only a few known protocols -- Privacy rules: Items are now processed in the specified order -- Privacy rules: Fix to correctly block subscription requests -- Proxy65: If ip option is not defined, take an IP address of a local hostname -- PubSub: Add roster subscription handling; send PEP events to all resources -- PubSub: Allow node creation without configure item -- PubSub: Requesting items on a node which exists, but empty returns an error -- PEP: Fix sending notifications to other domains and s2s -- S2S: Fix problem with encrypted connection to Gtalk and recent Openfire -- S2S: Workaround to get DNS SRV lookup to work on Windows machine -- Shared Roster Groups: Fix to not resend authorization request -- WebAdmin: Fix encryption problem for ejabberd_http after timeout - - - Bug reports - - You can officially report bugs on ProcessOne support site: - http://support.process-one.net/ - -END diff --git a/doc/release_notes_2.0.5.txt b/doc/release_notes_2.0.5.txt deleted file mode 100644 index cf652ccfe..000000000 --- a/doc/release_notes_2.0.5.txt +++ /dev/null @@ -1,33 +0,0 @@ - - Release Notes - ejabberd 2.0.5 - - ejabberd 2.0.5 is the fifth bugfix release in ejabberd 2.0.x branch. - - ejabberd 2.0.5 includes three bugfixes. - More details of those fixes can be retrieved from: - http://redir.process-one.net/ejabberd-2.0.5 - - The new code can be downloaded from ejabberd download page: - http://www.process-one.net/en/ejabberd/ - - - The changes are: - -- Fix two problems introduced in ejabberd 2.0.4: subscription request - produced many authorization requests with some clients and - transports; and subscription requests were not stored for later - delivery when receiver was offline. - -- Fix warning in expat_erl.c about implicit declaration of x_fix_buff - -- HTTP-Bind (BOSH): Fix a missing stream:error in the returned - remote-stream-error stanza - - - Bug reports - - You can officially report bugs on ProcessOne support site: - http://support.process-one.net/ - -END diff --git a/doc/release_notes_2.1.0.txt b/doc/release_notes_2.1.0.txt deleted file mode 100644 index 85f2f48ff..000000000 --- a/doc/release_notes_2.1.0.txt +++ /dev/null @@ -1,281 +0,0 @@ - - Release Notes - ejabberd 2.1.0 - - ejabberd 2.1.0 is a major new version for ejabberd adding many - new features, performance and scalability improvements. - - ejabberd 2.1.0 includes many new features, improvements and bug fixes. - A complete list of changes can be retrieved from: - http://redir.process-one.net/ejabberd-2.1.0 - - The new code can be downloaded from ejabberd download page: - http://www.process-one.net/en/ejabberd/ - - - New features and improvements: - -* Anti-abuse -- Captcha support (XEP-0158). The example script uses ImageMagick. -- New option: registration_timeout to limit registrations by time -- Use send timeout to avoid locking on gen_tcp:send -- mod_ip_blacklist: client blacklist support by IP - -* API -- ejabberd_http provides Host, Port, Headers and Protocol in HTTP requests -- Export function to create MUC room -- New events: s2s_send_packet and s2s_receive_packet -- New event: webadmin_user_parse_query when POST in web admin user page -- Support distributed hooks over the cluster - -* Authentification -- Extauth responses: log strange responses and add timeout - -* Binary Installer -- Includes exmpp library to support import/export XML files - -* Caps -- Remove useless caps tables entries -- mod_caps must handle correctly external contacts with several resources -- Complain if mod_caps disabled and mod_pubsub has PEP plugin enabled - -* Clustering and Architecture - -* Configuration -- Added option access_max_user_messages for mod_offline -- Added option backlog for ejabberd_listener to increase TCP backlog -- Added option define_macro and use_macro -- Added option include_config_file to include additional configuration files -- Added option max_fsm_queue -- Added option outgoing_s2s_options to define IP address families and timeout -- Added option registration_timeout to ejabberd.cfg.example -- Added option s2s_dns_options to define DNS timeout and retries -- Added option ERL_OPTIONS to ejabberdctl.cfg -- Added option FIREWALL_WINDOW to ejabberdctl.cfg -- Added option EJABBERD_PID_PATH to ejabberdctl.cfg -- Deleted option user_max_messages of mod_offline -- Check certfiles are readable on server start and listener start -- Config file management mix file reading and sanity check -- Include example PAM configuration file: ejabberd.pam -- New ejabberd listener: ejabberd_stun -- Support to bind the same port to multiple interfaces -- New syntax to specify the IP address and IPv6 in listeners - configuration. The old options {ip,{1,2,3,4}} and inet6 are - supported even if they aren't documented. -- New syntax to specify the network protocol: tcp or udp -- Report error at startup if a listener module isn't available -- Only listen in a port when actually ready to serve requests -- In default config, only local accounts can create rooms and PubSub nodes - -* Core architecture -- More verbose error reporting for xml:element_to_string -- Deliver messages when first presence is Invisible -- Better log message when config file is not found -- Include original timestamp on delayed presences - -* Crypto -- Do not ask certificate for client (c2s) -- SSL code remove from ejabberd in favor of TLS -- Support Zlib compression after STARTTLS encryption -- tls v1 client hello - -* Documentation -- Document possible default MUC room options -- Document service_check_from in the Guide -- Document s2s_default_policy and s2s_host in the Guide -- new command and guide instructions to change node name in a Mnesia database - -* ejabberd commands -- ejabberd commands: separate command definition and calling interface -- access_commands restricts who can execute what commands and arguments -- ejabberdctl script now displays help and categorization of commands - -* HTTP Binding and HTTP Polling -- HTTP-Bind: module optimization and clean-up -- HTTP-Bind: allow configuration of max_inactivity timeout -- HTTP-Poll: turn session timeout into a config file parameter - -* Jingle -- STUN server that facilitates the client-to-client negotiation process - -* LDAP -- Faster reconnection to LDAP servers -- LDAP filter optimisation: Add ability to filter user in ejabberd and not LDAP -- LDAP differentiates failed auth and unavailable auth service -- Improve LDAP logging -- LDAPS support using TLS. - -* Localization -- Use Gettext PO for translators, export to ejabberd MSG -- Support translation files for additional projects -- Most translations are updated to latest code -- New translation to Greek language - -* Multi-User Chat (MUC) -- Allow admins to send messages to rooms -- Allow to store room description -- Captcha support in MUC: the admin of a room can configure it to - require participants to fill a captcha to join the room. -- Limit number of characters in Room ID, Name and Description -- Prevent unvoiced occupants from changing nick -- Support Result Set Management (XEP-0059) for listing rooms -- Support for decline of invitation to MUC room -- mod_muc_log options: plaintext format; filename with only room name - -* Performance -- Run roster_get_jid_info only if privacy list has subscription or group item -- Significant PubSub performance improvements - -* Publish-Subscribe -- Add nodetree filtering/authorization -- Add subscription option support for collection nodes -- Allow Multiple Subscriptions -- Check option of the nodetree instead of checking configuration -- Implement whitelist authorize and roster access model -- Implicit item deletion is not notified when deleting node -- Make PubSub x-data configuration form handles list value -- Make default node name convention XEP-compatible, document usage of hierarchy -- Node names are used verbatim, without separating by slash, unless a - node plugin uses its own separator -- Send authorization update event (XEP-0060, 8.6) -- Support of collection node subscription options -- Support ODBC storage. Experimental, needs more testing. - -* Relational databases: -- Added MSSQL 2000 and 2005 -- Privacy rules storage in MySQL -- Implement reliable ODBC transaction nesting - -* Source Package -- Default installation directories changed. Please see the upgrade notes below. -- Allow more environment variable overrides in ejabberdctl -- ChangeLog is not edited manually anymore; it's generated automatically. -- Install the ejabberd Guide -- Install the ejabberd include files -- New option for the 'configure' script: --enable-user which installs - ejabberd granting permission to manage it to a regular system user; - no need to use root account to. -- Only try to install epam if pam was enabled in configure script -- Spool, config and log dirs: owner writes, group reads, others do nothing. -- Provides an example ejabberd.init file - -* S2S -- Option to define s2s outgoing behaviour: IPv4, IPv6 and timeout -- DNS timeout and retries, configurable with s2s_dns_options. - -* Shared rosters -- When a member is added/removed to group, send roster upgrade to group members - -* Users management -- When account is deleted, cancel presence subscription for all roster items - -* XEP Support -- Added XEP-0059 Result Set Management (for listing rooms) -- Added XEP-0082 Date Time -- Added XEP-0085 Chat State Notifications -- Added XEP-0157 Contact Addresses for XMPP Services -- Added XEP-0158 CAPTCHA Forms (in MUC rooms) -- Added STUN server, for XEP-0176: Jingle ICE-UDP Transport Method -- Added XEP-0199 XMPP Ping -- Added XEP-0202 Entity Time -- Added XEP-0203 Delayed Delivery -- Added XEP-0227 Portable Import/Export Format for XMPP-IM Servers -- Added XEP-0237 Roster Versioning - -* Web Admin -- Display the connection method of user sessions -- Cross link of ejabberd users in the list of users and rosters -- Improved the browsing menu: don't disappear when browsing a host or node -- Include Last-Modified HTTP header in responses to allow caching -- Make some Input areas multiline: options of listening ports and modules -- Support PUT and DELETE methods in ejabberd_http -- WebAdmin serves Guide and links to related sections - -* Web plugins -- mod_http_fileserver: new option directory_indices, and improve logging - - - Important Notes: - -- ejabberd 2.1.0 requires Erlang R10B-9 or higher. - R12B-5 is the recommended version. Support for R13B is experimental. - - - Upgrading From ejabberd 1.x.x: - -- Check the Release Notes of the intermediate versions for additional - information about database or configuration changes. - - - Upgrading From ejabberd 2.0.x: - -- The database schemas have three changes since ejabberd 2.0.x. - Check the database creation SQL files and update your database. - 1) New table roster_version to support roster versioning. - 2) Six new tables for optional pubsub ODBC storage. - 3) Some tables in the MySQL database have a new created_at column. - -- As usual, it is recommended to backup the Mnesia spool directory and - your SQL database (if used) before upgrading ejabberd. - -- Between ejabberd 2.0.0 and 2.0.5, mod_pubsub used "default" as the - default node plugin. But in 2.1.0 this is renamed to "hometree". - You have to edit your ejabberd config file and replace those names. - If you used ejabberd 2.0.5 or older, the database will be updated - automatically. But if you were using ejabberd from SVN, you must - manually run ejabberdctl with the command: rename_default_nodeplugin. - Running this command on already updated database does nothing. - -- The listener options 'ip' and 'inet6' are not documented anymore - but they are supported and you can continue using them. - There is a new syntax to define IP address and IP version. - As usual, check the ejabberd Guide for more information. - -- The log file sasl.log is now called erlang.log - -- ejabberdctl commands now have _ characters instead of -. - For backwards compatibility, it is still supported -. - -- mod_offline has a new option: access_max_user_messages. - The old option user_max_messages is no longer supported. - -- If you upgrade from ejabberd trunk SVN, you must execute this: - $ ejabberdctl rename_default_nodeplugin - -- Default installation directories changed a bit: - * The Mnesia spool files that were previously stored in - /var/lib/ejabberd/db/NODENAME/* - are now stored in - /var/lib/ejabberd/* - * The directories - /var/lib/ejabberd/ebin - /var/lib/ejabberd/priv - and their content is now installed as - /lib/ejabberd/ebin - /lib/ejabberd/priv - * There is a new directory with Erlang header files: - /lib/ejabberd/include - * There is a new directory for ejabberd documentation, - which includes the Admin Guide and the release notes:: - /share/doc/ejabberd - -- How to upgrade from previous version to ejabberd 2.1.0: - 1. Stop the old instance of ejabberd. - 2. Run 'make install' of new ejabberd 2.1.0 to create the new directories. - 3. Copy the content of your old directory: - /var/lib/ejabberd/db/NODENAME/ - to the new location: - /var/lib/ejabberd/ - so you will have the files like this: - /var/lib/ejabberd/acl.DCD ... - 4. You can backup the content of those directories and delete them: - /var/lib/ejabberd/ebin - /var/lib/ejabberd/priv - /var/lib/ejabberd/db - 5. Now try to start your new ejabberd 2.1.0. - - - Bug reports - - You can officially report bugs on ProcessOne support site: - http://support.process-one.net/ diff --git a/doc/release_notes_2.1.1.txt b/doc/release_notes_2.1.1.txt deleted file mode 100644 index b97462c9f..000000000 --- a/doc/release_notes_2.1.1.txt +++ /dev/null @@ -1,47 +0,0 @@ - - Release Notes - ejabberd 2.1.1 - - ejabberd 2.1.1 is the first bugfix release in ejabberd 2.1.x branch. - - ejabberd 2.1.1 includes several important bugfixes. - More details of those fixes can be retrieved from: - http://redir.process-one.net/ejabberd-2.1.1 - - The new code can be downloaded from ejabberd download page: - http://www.process-one.net/en/ejabberd/ - - - The changes are: - -* Core -- Call ejabberd_router:route/3 instead of sending a message -- Can't connect if starttls_required and zlib are set -- Routes vCard request to the occupant full JID, but should to bare JID -- S2S: fix allow_host/2 on subdomains. added hook s2s_allow_host - -* MUC -- Support converting one-to-one chat to MUC -- Add support for serving a Unique Room Name - -* Publish Subscribe -- Receive same last published PEP items at reconnect if several resources online -- Typo in mod_pubsub_odbc breaks Service Discovery and more - -* Web -- Fix memory and port leak when TLS is enabled in HTTP -- WebAdmin doesn't report correct last activity with postgresql backend -- Option to define custom HTTP headers in mod_http_fileserver -- Show informative webpage when browsing the HTTP-Poll page - -* Other -- Change captcha.sh to not depend on bash -- Generate main XML file also when exporting only a vhost -- Fix last newline in ejabberdctl result -- Guide: fix -setcookie, mod_pubsub_odbc host, content_types - - - Bug reports - - You can officially report bugs on ProcessOne support site: - http://support.process-one.net/ diff --git a/doc/release_notes_2.1.10.txt b/doc/release_notes_2.1.10.txt deleted file mode 100644 index bf118594e..000000000 --- a/doc/release_notes_2.1.10.txt +++ /dev/null @@ -1,39 +0,0 @@ - - Release Notes - ejabberd 2.1.10 - - ejabberd 2.1.10 includes a few bugfixes and improvements. - - Read more details about the changes in: - http://redir.process-one.net/ejabberd-2.1.10 - - Download the source code and installers from: - http://www.process-one.net/en/ejabberd/ - - - The major changes are: - -* Erlang/OTP compatibility -- Support Erlang/OTP R15B regexp and drivers (EJAB-1521) -- Fix modules update in R14B04 and higher -- Fix modules update of stripped beams (EJAB-1520) - -* XMPP Core -- Fix presence problem in C2S after first unavailable (EJAB-1466) -- Fix bug on S2S shaper when TLS is used -- Prevent overload of incoming S2S connections - -* XEPs -- BOSH: Get rid of useless mnesia transaction (EJAB-1502) -- MUC: Don't reveal invitee resource when room informs invitor -- Privacy: Activate "Blocked Contacts" to current c2s connection (EJAB-1519) -- Privacy: Always allow packets from user's server and bare jid (EJAB-1441) -- Pubsub: Add hooks for node creation/deletion (EJAB-1470) -- Shared Rosters: support groupname@vhost in Displayed Groups (EJAB-506) -- Vcard: Fix error when lowercasing some search results (EJAB-1490) - - - Bug reports - - You can officially report bugs on ProcessOne support site: - http://support.process-one.net/ diff --git a/doc/release_notes_2.1.11.txt b/doc/release_notes_2.1.11.txt deleted file mode 100644 index 44d15f0b2..000000000 --- a/doc/release_notes_2.1.11.txt +++ /dev/null @@ -1,58 +0,0 @@ - - Release Notes - ejabberd 2.1.11 - - ejabberd 2.1.11 includes a few bugfixes and improvements. - - Read more details about the changes in: - http://redir.process-one.net/ejabberd-2.1.11 - - Download the source code and installers from: - http://www.process-one.net/en/ejabberd/ - - - The major changes are: - -* HTTP service -- Fix ejabberd_http:get_line -- Don't use binary:match to extract lines from binaries -- Parse and encode https header names like native http parser does -- Parse correctly https request split into multiple packets -- Properly handle HEAD request in mod_http_bind (EJAB-1538) -- New option default_host for handling requests with ambiguous Host (EJAB-1261) - -* ODBC -- New ODBC support for mod_announce -- New ODBC support for mod_blocking -- New ODBC support for mod_irc -- New ODBC support for mod_muc -- New ODBC support for mod_shared_roster -- New ODBC support for mod_vcard_xupdate -- Add ODBC exporting function for privacy table -- Work also with some unicode strings in PgSQL (EJAB-1490) -- Replace a single quote with double quotes in an ODBC escape - -* SSL -- Make sure that res is initialized in all cases -- Parse correctly https request split into multiple packets (EJAB-1537) -- Added missed tls:recv_data/2 -- Don't ignore Length parameter in tls:recv -- Avoid quadratic behavior in reading SSL data -- Dix http_bind webserver TLS fail on Chrome (EJAB-1530) - -* Miscelanea -- Assume we have only one CPU when an auto-detection fails (EJAB-1516) -- Auth: Relax digest-uri handling (EJAB-1529) -- Caps: Cache caps timestamp before the IQ-request is done -- IRC: Use of MUC password -- Private: misc errors cases fixes -- Pubsub: return user affiliation for a specified node (EJAB-1294) -- Shared Roster: Foreign items were not pushed (EJAB-1509) -- Shared Roster LDAP: user substitution in ldap_rfilter (EJAB-1555) -- Windows: Fix makefile rules for building DLLs - - - Bug reports - - You can officially report bugs on ProcessOne support site: - http://support.process-one.net/ diff --git a/doc/release_notes_2.1.12.txt b/doc/release_notes_2.1.12.txt deleted file mode 100644 index f8fd7382c..000000000 --- a/doc/release_notes_2.1.12.txt +++ /dev/null @@ -1,67 +0,0 @@ - - Release Notes - ejabberd 2.1.12 - - ejabberd 2.1.12 includes a many bugfixes and a few improvements. - - Read more details about the changes in: - http://redir.process-one.net/ejabberd-2.1.12 - - Download the source code and installers from: - http://www.process-one.net/en/ejabberd/ - - - The changes are: - -* Core ejabberd -- Make terms serialization faster -- Reduce size of XML stream state - -* Administration -- Add SCRAM and remove MD5 support to ejabberd commands auth verification -- Added command to list all the vhosts registered in an ejabberd node -- Added export2odbc command, copied from mod_admin_extra.erl -- Fix ejabberdctl number of arguments error report with R15 -- Check node name is available before starting ejabberd (EJAB-1572) -- Fix ejabberd_xmlrpc commands authentication with SCRAM -- Fix mod_offline:store_offline_msg argument (EJAB-1581) -- Log IP address when auth attempt fails -- Make sure update_info returns atoms only (EJAB-1595) -- On shutdown, first stop listeners, then modules - -* Encryption -- Detect OpenSSL version at runtime, not at compile time -- Fixed signedness issue in tls_drv GET_DESCRYPTED_INPUT (EJAB-1591) -- Enable DHE key exchange in TLS driver -- Enable ECDHE key exchange in TSL driver -- Disable old and unsecure ciphers in TLS driver -- Disable SSL 2.0 in TLS driver - -* HTTP-Bind -- Do not trigger item-not-found errors in mod_http_bind -- Repeated http-bind request should abort only requests with same rid -- Receiving missing request shouldn't close waiting out-ouf-order request - -* XMPP -- Allow multiple fqdn values in configuration (EJAB-1578) -- Fix get_subscription_lists/4 -- Fix account registration -- Send announce Message stanzas as Headline type instead of Normal - -* Other -- Guide: Fix file name of Name Service Switch -- Guide: Document the db_type modules option (EJAB-1560) -- LDAP: Fix broken JPEG photo (EJAB-1526) -- LDAP: Fix compatibility with Erlang R16A (EJAB-1612) -- MUC: Fix angle brackets handle in plaintext log (EJAB-1610) -- MUC: Fix MUC start when Mnesia tables don't exist yet -- MUC: New mod_muc_log option file_permissions (EJAB-1588) -- ODBC: Merge SQL and Mnesia code into one module (EJAB-1560) -- Translation: New Hebrew -- Translation: Update Slovak - - - Bug reports - - You can officially report bugs on ProcessOne support site: - http://support.process-one.net/ diff --git a/doc/release_notes_2.1.2.txt b/doc/release_notes_2.1.2.txt deleted file mode 100644 index 56f506935..000000000 --- a/doc/release_notes_2.1.2.txt +++ /dev/null @@ -1,51 +0,0 @@ - - Release Notes - ejabberd 2.1.2 - - ejabberd 2.1.2 is the second bugfix release in ejabberd 2.1.x branch. - - ejabberd 2.1.2 includes several bugfixes. - More details of those fixes can be retrieved from: - http://redir.process-one.net/ejabberd-2.1.2 - - The new code can be downloaded from ejabberd download page: - http://www.process-one.net/en/ejabberd/ - - - The major changes are: - -* Core -- Close sessions that were half connected -- Fix SASL PLAIN authentication message for RFC4616 compliance -- Fix support for old Erlang/OTP R10 and R11 -- Return proper error (not 'conflict') when register is forbidden by ACL -- When ejabberd stops, send stream close to clients - -* ejabberdctl -- Check for EGID in ejabberdctl command -- Command to stop ejabberd informing users, with grace period -- If there's a problem in config file, display config lines and stop node - -* MUC -- Kick occupants with reason when room is stopped due to MUC shutdown -- Write in room log when a room is created, destroyed, started, stopped - -* PubSub and PEP -- Don't call gen_server on internal event (improves performance and scalability) -- Fix duplicate SHIM header in Pubsub message -- Notification messages of Pubsub node config change contained a SHIM header -- SubID SHIM header missing in Pubsub message with multiple - subscriptions on the same node -- PEP: last published item not sent from unavailable users when the - subscription is implicit (XEP-0115) -- pep_mapping not working due to Node type mismatch - -* WebAdmin -- If big offline message queue, show only subset on WebAdmin -- Support in user list page of WebAdmin when mod_offline is disabled - - - Bug reports - - You can officially report bugs on ProcessOne support site: - http://support.process-one.net/ diff --git a/doc/release_notes_2.1.3.txt b/doc/release_notes_2.1.3.txt deleted file mode 100644 index ac2c26e6f..000000000 --- a/doc/release_notes_2.1.3.txt +++ /dev/null @@ -1,91 +0,0 @@ - - Release Notes - ejabberd 2.1.3 - - ejabberd 2.1.3 is the third release in ejabberd 2.1.x branch. - - ejabberd 2.1.3 includes many bugfixes, and some improvements. - More details of those fixes can be retrieved from: - http://redir.process-one.net/ejabberd-2.1.3 - - The new code can be downloaded from ejabberd download page: - http://www.process-one.net/en/ejabberd/ - - - This is the full list of changes: - -* Client connections -- Avoid 'invalid' value in iq record -- Avoid resending stream:error stanzas on terminate (EJAB-1180) -- Close also legacy sessions that were half connected (EJAB-1165) -- iq_query_info/1 now returns 'invalid' if XMLNS is invalid -- New ejabberd_c2s option support: max_fsm_queue -- Rewrite mnesia counter functions to use dirty_update_counter (EJAB-1177) -- Run user_receive_packet also when sending offline messages (EJAB-1193) -- Use p1_fsm behaviour in c2s FSM (EJAB-1173) - -* Clustering -- Fix cluster race condition in route read -- New command to set master Mnesia node -- Use mnesia:async_dirty when cleaning table from failed node - -* Documentation -- Add quotes in documentation of some erl arguments (EJAB-1191) -- Add option access_from (EJAB-1187) -- Add option max_fsm_queue (EJAB-1185) -- Fix documentation installation, no need for executable permission (EJAB-1170) -- Fix typo in EJABBERD_BIN_PATH (EJAB-891) -- Fix typos in example config comments (EJAB-1192) - -* ejabberdctl -- Support concurrent connections with bound connection names -- Add support for Jot in ctl and TTY in debug -- Support help command names with old - characters -- Fix to really use the variable ERL_PROCESSES - -* Erlang compatibility -- Don't call queue:filter/2 to keep compatibility with older Erlang versions -- Use alternative of file:read_line/1 to not require R13B02 - -* HTTP -- Add new debugging hook to the http receiving process -- Allow a request_handler to serve a file in root of HTTP - -* HTTP-Bind (BOSH) -- Cross-domain HTTP-Bind support (EJAB-1168) -- Hibernate http-bind process after handling a request -- Reduce verbosity of HTTP Binding log messages - -* LDAP -- Document ldap_dn_filter, fetch only needed attributes in search (EJAB-1204) -- Use "%u" pattern as default for ldap_uids (EJAB-1203) - -* Localization -- Fix German translation (EJAB-1195) -- Fix Russian translation - -* ODBC -- Fix MSSQL support, which was broken (EJAB-1201) -- Improved SQL reconnect behaviour - -* Pubsub, PEP and Caps -- Add extended stanza addressing 'replyto' on PEP (EJAB-1198) -- Add pubsub#purge_offline (EJAB-1186) -- Fix pubsub#title option (EJAB-1190) -- Fix remove_user for node subscriptions (EJAB-1172) -- Optimizations in mod_caps - -* Other -- mod_register: Add new acl access_from, default is to deny -- mod_sic: new module for the experimental XEP-0279 Server IP Check (EJAB-1205) -- PIEFXIS: Catch errors when exporting to PIEFXIS file (EJAB-1178) -- Proxy65: new option "hostname" (EJAB-838) -- Roster: Fix resending authorization problem -- Shared Roster Groups: get contacts nickname from vcard (EJAB-114) -- S2S: Improved s2s connections clean up (EJAB-1202) - - - Bug reports - - You can officially report bugs on ProcessOne support site: - http://support.process-one.net/ diff --git a/doc/release_notes_2.1.4.txt b/doc/release_notes_2.1.4.txt deleted file mode 100644 index c4fe4bf8c..000000000 --- a/doc/release_notes_2.1.4.txt +++ /dev/null @@ -1,80 +0,0 @@ - - Release Notes - ejabberd 2.1.4 - - ejabberd 2.1.4 is the fourth release in ejabberd 2.1.x branch, - and includes many small bugfixes and improvements. - - Read more details about the changes in: - http://redir.process-one.net/ejabberd-2.1.4 - - Download the source code and installers from: - http://www.process-one.net/en/ejabberd/ - - - This is the full list of changes: - -* Authentication -- Extauth: Optionally cache extauth users in mnesia (EJAB-641) -- LDAP: Allow inband password change (EJAB-199) -- LDAP: Extensible match support (EJAB-722) -- LDAP: New option ldap_tls_verify is added (EJAB-1229) -- PAM: New option pam_userinfotype to provide username or JID (EJAB-652) - -* HTTP -- Add xml default content type -- Don't show HTTP request in logs, because reveals password (EJAB-1231) -- Move HTTP session timeout log from warning level to info -- New Access rule webadmin_view for read-only - -* HTTP-Bind (BOSH) -- Change max inactivity from 30 to 120 seconds -- Export functions to facilitate prebinding methods -- Use dirty_delete when removing the session -- Remove an unneeded delay of 100 milliseconds - -* Pubsub, PEP and Caps -- Enforce pubsub#presence_based_delivery (EJAB-1221) -- Enforce pubsub#show_values subscription option (EJAB-1096) -- Fix error code when unsubscribing from a non-existent node -- Fix to send node notifications (EJAB-1225) -- Full support for XEP-0115 v1.5 (EJAB-1223)(EJAB-1189) -- Make last_item_cache feature to be cluster aware -- Prevent orphaned pubsub node (EJAB-1233) -- Send created node notifications - -* Other -- Bounce messages when closing c2s session -- Bugfixes when handling Service Discovery to contacts (EJAB-1207) -- Compilation of ejabberd_debug.erl is now optional -- Don't send error stanza as reply to error stanza (EJAB-930) -- Don't store blocked messages in offline queue -- Reduce verbosity of log when captcha_cmd is checked but not configured -- Use a standard method to get a random seed (EJAB-1229) -- Commands: new update_list and update to update modified modules (EJAB-1237) -- Localization: Updated most translations -- MUC: Refactor code to reduce calls to get_affiliation and get_role -- ODBC: Add created_at column also to PostgreSQL schema -- Vcard: Automatic vcard avatar addition in presence - - - Upgrading From previous ejabberd releases: - -- If you use PostgreSQL, maybe you want to add the column created_at - to several tables. This is only a suggestion; ejabberd doesn't use - that column. Add it to your existing database executing those SQL - statements: - -ALTER TABLE users ADD COLUMN created_at TIMESTAMP NOT NULL DEFAULT now(); -ALTER TABLE rosterusers ADD COLUMN created_at TIMESTAMP NOT NULL DEFAULT now(); -ALTER TABLE spool ADD COLUMN created_at TIMESTAMP NOT NULL DEFAULT now(); -ALTER TABLE vcard ADD COLUMN created_at TIMESTAMP NOT NULL DEFAULT now(); -ALTER TABLE privacy_list ADD COLUMN created_at TIMESTAMP NOT NULL DEFAULT now(); -ALTER TABLE privacy_storage ADD COLUMN created_at TIMESTAMP NOT NULL DEFAULT now(); - - - Bug reports - - You can officially report bugs on ProcessOne support site: - http://support.process-one.net/ - diff --git a/doc/release_notes_2.1.5.txt b/doc/release_notes_2.1.5.txt deleted file mode 100644 index a13b452d6..000000000 --- a/doc/release_notes_2.1.5.txt +++ /dev/null @@ -1,70 +0,0 @@ - - Release Notes - ejabberd 2.1.5 - - ejabberd 2.1.5 is the fifth release in ejabberd 2.1.x branch, - and includes several minor bugfixes and a few improvements. - - Read more details about the changes in: - http://redir.process-one.net/ejabberd-2.1.4 - - Download the source code and installers from: - http://www.process-one.net/en/ejabberd/ - - - This is the full list of changes: - -* Authentication -- Extauth: Support parallel script running (EJAB-1280) -- mod_register: Return Registered element when account exists - -* ejabberdctl -- Fix print of command result that contains ~ -- Fix problem when FIREWALL_WINDOW options for erl kernel were used -- Fix typo in update_list command (EJAB-1237) -- Some systems delete the lock dir; in such case don't use flock at all -- The command Update now returns meaningful message and exit-status (EJAB-1237) - -* HTTP-Bind (BOSH) -- Don't say v1.2 in the Bind HTTP page -- New optional BOSH connection attribute process-delay (EJAB-1257) - -* MUC -- Document the mod_muc option captcha_protected -- Now admins are able to see private rooms in disco (EJAB-1269) -- Show some more room options in the log file - -* ODBC -- Correct handling of SQL boolean types (EJAB-1275) -- Discard too old queued requests (the caller has already got a timeout) -- Fixes wrong SQL escaping when --enable-full-xml is set -- Use ets insead of asking supervisor in ejabberd_odbc_sup:get_pids/1 - -* Pubsub, PEP and Caps -- Enforce disco features results (EJAB-1033, EJAB-1228, EJAB-1238) -- Support all the hash functions required by Caps XEP-0115 - -* Requirements -- Fixed support for Erlang R12; which doesn't support: true andalso ok -- Support OTP R14A by using public_key library instead of old ssl (EJAB-953) -- Requirement of OpenSSL increased from 0.9.6 to 0.9.8 -- OpenSSL is now required, not optional - -* Other -- Don't ask for client certificate when using tls (EJAB-1267) -- Fix typo in --enable-transient_supervisors -- Fix privacy check when serving local Last (EJAB-1271) -- Inform client that SSL session caching is disabled -- New configure option: --enable-nif -- Use driver allocator in C files for reflecting memory in erlang:memory(system) -- Debug: New p1_prof compiled with: make debugtools=true -- Debug: Added functions to collect stats about queues, memory, reductions etc -- HTTP: Log error if request has ambiguous Host header (EJAB-1261) -- Logs: When logging s2s out connection attempt or success, log if TLS is used -- Shared Rosters: When account is deleted, delete also member of stored rosters - - - Bug reports - - You can officially report bugs on ProcessOne support site: - http://support.process-one.net/ diff --git a/doc/release_notes_2.1.6.txt b/doc/release_notes_2.1.6.txt deleted file mode 100644 index 64f02a3bc..000000000 --- a/doc/release_notes_2.1.6.txt +++ /dev/null @@ -1,67 +0,0 @@ - - Release Notes - ejabberd 2.1.6 - - ejabberd 2.1.6 is the sixth release in ejabberd 2.1.x branch, - and includes a lot of bugfixes and improvements. - - Read more details about the changes in: - http://redir.process-one.net/ejabberd-2.1.6 - - Download the source code and installers from: - http://www.process-one.net/en/ejabberd/ - - - Some of the changes are: - -* Account register -- mod_register: New ip_access option restricts which IPs can register (EJAB-915) -- mod_register: Default configuration allows registrations only from localhost -- mod_register: New password_strength for entropy check (EJAB-1326) -- mod_register: New captcha_protected option to require CAPTCHA (EJAB-1262) -- mod_register_web: New module, with CAPTCHA support (EJAB-471) - -* BOSH -- Don't loop when there is nothing after a stream start (EJAB-1358) -- Fix http-bind supervisor to support multiple vhosts (EJAB-1321) -- Support to restart http-bind (EJAB-1318) -- Support for X-Forwarded-For HTTP header (EJAB-1356) - -* Erlang/OTP compatibility -- R11: Fix detection of Erlang R11 and older (EJAB-1287) -- R12B5: Fix compatibility in ejabberd_http_bind.erl (EJAB-1343) -- R14A: Make xml.c correctly compile (EJAB-1288) -- R14A, R14B: Disapprove the use of R14A and R14B due to the rwlock bug -- R14B: Use pg2 from this version in systems with older ones (EJAB-1349) - -* Listeners -- Bind listener ports early and start accepting connections later -- Fix a leak of ejabberd_receiver processes -- Speed up ejabberd_s2s:is_service/2, allow_host/2 (EJAB-1319) -- S2S: New option to require encryption (EJAB-495) -- S2S: New option to reject connection if untrusted certificate (EJAB-464) -- S2S: Include From attribute in the stream header of outgoing S2S connections -- S2S: Fix domain_certfile tlsopts modifications for S2S connections (EJAB-1086) - -* Pubsub/PEP/Caps -- Fix pubsub cross domain eventing (EJAB-1340) -- Use one_queue IQ discipline by default -- Implement lifetime for broken hashes -- New CAPS processing - -* ODBC -- Increase maximum restart strategy to handle some SQL timeouts -- Support PostgreSQL 9.0 (EJAB-1359) -- Use MEDIUMTEXT type for vcard avatars in MySQL schema (EJAB-1252) - -* Miscellanea: -- mod_shared_roster_ldap: New Shared Roster Groups using LDAP information -- mod_privacy: Fix to allow block by group and subscription again -- Support timezone West of UTC (EJAB-1301) -- Support to change loglevel per module at runtime (EJAB-225) - - - Bug reports - - You can officially report bugs on ProcessOne support site: - http://support.process-one.net/ diff --git a/doc/release_notes_2.1.7.txt b/doc/release_notes_2.1.7.txt deleted file mode 100644 index 3f4c224bf..000000000 --- a/doc/release_notes_2.1.7.txt +++ /dev/null @@ -1,97 +0,0 @@ - - Release Notes - ejabberd 2.1.7 - - ejabberd 2.1.7 is the eighth release in ejabberd 2.1.x branch, - and includes a lot of bugfixes and improvements. - - Read more details about the changes in: - http://redir.process-one.net/ejabberd-2.1.7 - - Download the source code and installers from: - http://www.process-one.net/en/ejabberd/ - - - The changes are: - -* BOSH and Web -- Clarify error message when BOSH query is sent to non-running module -- Keep the order of stanzas when BOSH sends several (EJAB-1374) -- Show configuration for HTTPS http_bind -- Support as read-only HTTP method not only GET, also HEAD -- The responses to HEAD must have empty Body - -* CAPTCHA -- If the port number isn't listener, then specify the protocol (EJAB-1418) -- New CAPTCHA limit -- New CAPTCHA whitelist support -- Only check system at startup if option is enabled -- Provide HTTPS URL in CAPTCHA form when listener has 'tls' option (EJAB-1406) -- Show captcha_limit option in the example config -- Support more captcha_host value formats (EJAB-1418) -- Throw error when captcha fails at server start, not later at runtime -- captcha_host must have the port number to get protocol (EJAB-1418) - -* Core ejabberd -- Disable all entity expansions (EJAB-1451) -- Do not accept XML with undefined prefixes (EJAB-680) -- Make jlib:ip_to_list safe to use -- Make sure 'closed' event is correctly processed on every state -- New route_iq/5 accepting Timeout (EJAB-1398) -- Take into consideration internal queue length when sorting processes queues -- Use route instead of send_element to go through standard workflow - -* Erlang/OTP compatibility -- Remove Type and Spec, backport list comprehensions, so R12B-5 can compile -- Tweak pg2_backport.erl to work with Erlang older than R13A (EJAB-1349) - -* ODBC -- Don't let presence-in privacy rule block a presence subscription (EJAB-255) -- Escape user input in mod_privacy_odbc (EJAB-1442) -- Try to improve support for roster_version in MSSQL (EJAB-1437) - -* Pubsub/PEP/Caps -- Apply filtered notification to PEP last items (EJAB-1456) -- Fix empty pubsub payload check -- Owner can delete any items from its own node (EJAB-1445) -- Pubsub node maxitem forced to 0 if non persistent node (EJAB-1434) -- Reorganize the push_item function, and handle version not_found (EJAB-1420) - -* Scripts -- ejabberd.init: Several fixes and improvements -- ejabberdctl: Escape output from ctlexec() to erl script (EJAB-1399) -- ejabberdctl: Fix bashism and mimic master branch (EJAB-1404) -- ejabberdctl: Fix space between INET_DIST_INTERFACE (EJAB-1416) -- ejabberdctl: New DIST_USE_INTERFACE restricts IP of erlang listen (EJAB-1404) -- ejabberdctl: New ERL_EPMD_ADDRESS that works since Erlang/OTP R14B03 -- extauth: Fix delayed response of timeout was reused for next login (EJAB-1385) -- extauth: Forward old messages to newly spawned extauth process (EJAB-1385) -- extauth: If script crashes, ejabberd should restart it (EJAB-1428) - -* XEP support -- mod_blocking: New XEP-0191 Simple Communications Blocking (EJAB-695) -- No need to inform that XEP-0237 is optional; clarified in XEP version 1.2 - -* Miscellanea: -- If a module start fails during server start, stop erlang (EJAB-1446) -- New Indonesian translation (EJAB-1407) -- LDAP: Note that ejabberd works with CGP LDAP server -- S2S: Handle Tigase's unexpected version=1.0 (EJAB-1379) -- mod_irc: Send presence unavailable to the departing occupant (EJAB-1417) -- mod_last: Allow user to query his own Last activity -- mod_muc: Do not decrease MUC admin's role/affiliation -- mod_muc: Send jid attribute when occupant is banned (EJAB-1432) -- mod_offline: Change c2s state before offline messages resending -- mod_ping: Use iqdisc no_queue by default (EJAB-1435) -- mod_pres_counter: Prevent subscription flood (EJAB-1388) -- mod_register Access now also controls account unregistrations -- mod_register: Clarify more the expected content of welcome_message option -- mod_shared_roster: Fix support for anonymous accounts in @all@ (EJAB-1264) -- mod_shared_roster: New @online@ directive (EJAB-1391) - - - Bug reports - - You can officially report bugs on ProcessOne support site: - http://support.process-one.net/ - diff --git a/doc/release_notes_2.1.8.txt b/doc/release_notes_2.1.8.txt deleted file mode 100644 index 3e109da44..000000000 --- a/doc/release_notes_2.1.8.txt +++ /dev/null @@ -1,21 +0,0 @@ - - Release Notes - ejabberd 2.1.8 - - ejabberd 2.1.8 is the ninth release in ejabberd 2.1.x branch, - and includes a PubSub regression bugfix. - - Download the source code and installers from: - http://www.process-one.net/en/ejabberd/ - - - The change is: - -- Fix issue on PubSub preventing publication of items (EJAB-1457) - - - - Bug reports - - You can officially report bugs on ProcessOne support site: - http://support.process-one.net/ diff --git a/doc/release_notes_2.1.9.txt b/doc/release_notes_2.1.9.txt deleted file mode 100644 index 77cc9ac1d..000000000 --- a/doc/release_notes_2.1.9.txt +++ /dev/null @@ -1,56 +0,0 @@ - - Release Notes - ejabberd 2.1.9 - - ejabberd 2.1.9 is the eighth release in ejabberd 2.1.x branch, - and includes a lot of bugfixes and improvements. - - Read more details about the changes in: - http://redir.process-one.net/ejabberd-2.1.9 - - Download the source code and installers from: - http://www.process-one.net/en/ejabberd/ - - - The changes are: - -* Core ejabberd -- Decrease CPU usage caused by tls:send with large data -- Escape iolist correctly when NIFs are disabled (EJAB-1462) -- Fix code to satisfy Dialyzer warnings -- Fix compilation in Windows -- Replace calls of OTP's Binary, since they would require R14 - -* LDAP -- Document ldap_tls_cacertfile and ldap_tls_depth options (EJAB-1299) -- Log an error when an LDAP filter is incorrect (EJAB-1395) -- New options: ldap_tls_cacertfile and ldap_tls_depth (EJAB-1299) -- New option: ldap_deref_aliases (EJAB-639) -- Match ldap_uidattr_format case-insensitively (EJAB-1449) - -* MUC -- Support for multiple entry with same nick to MUC rooms (EJAB-305) -- Support voice request and approvement -- New room option: allow_private_messages_from_visitors -- New room options: allow_voice_requests and voice_request_min_interval -- Include status 110 in presence to new occupant (EJAB-740) -- Fix mod_muc_log crash when first log entry is room destroy (EJAB-1499) -- Many fixes and improvements in mod_muc - -* Pubsub -- Enable pubsub#deliver_notification checking (EJAB-1453) -- Fix Denial of Service when user sends malformed publish stanza (EJAB-1498) - -* ODBC -- Fix ODBC account counting (EJAB-1491) -- Optimized mod_roster_odbc:get_roster - -* Miscellanea: -- New SASL SCRAM-SHA-1 authentication mechanism (EJAB-1196) -- New option: resource_conflict (EJAB-650) - - - Bug reports - - You can officially report bugs on ProcessOne support site: - http://support.process-one.net/ diff --git a/doc/webadmmain.png b/doc/webadmmain.png deleted file mode 100644 index 2228dcd68..000000000 Binary files a/doc/webadmmain.png and /dev/null differ diff --git a/doc/webadmmainru.png b/doc/webadmmainru.png deleted file mode 100644 index 3fd34ae8c..000000000 Binary files a/doc/webadmmainru.png and /dev/null differ diff --git a/doc/yozhikheader.png b/doc/yozhikheader.png deleted file mode 100644 index f2a4e4f00..000000000 Binary files a/doc/yozhikheader.png and /dev/null differ diff --git a/ejabberd.doap b/ejabberd.doap new file mode 100644 index 000000000..bfd3f5636 --- /dev/null +++ b/ejabberd.doap @@ -0,0 +1,849 @@ + + + + ejabberd + XMPP Server with MQTT Broker and SIP Service + Robust, Ubiquitous and Massively Scalable Messaging Platform (XMPP Server, MQTT Broker, SIP Service) + 2002-11-16 + BSD + Linux + macOS + Windows + Erlang + C + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2.9 + 0.5.0 + complete + + + + + + + 2.0 + 0.5.0 + complete + mod_last + + + + + + 1.2 + 16.02 + complete + mod_offline + + + + + + 1.6 + 0.5.0 + complete + mod_privacy + + + + + + 1.4 + 0.1.0 + complete + mod_offline + + + + + + 1.3 + 0.7.5 + complete + mod_offline + + + + + + 2.4 + 0.1.0 + complete + mod_disco + + + + + + 1.1 + 15.04 + complete + mod_multicast + + + + + + 0.6.0 + 0.1.0 + complete + mod_stats + + + + + + 1.25 + 0.5.0 + complete + mod_muc + + + + + + 1.2 + 0.5.0 + complete + mod_pubsub + + + + + + 1.2 + 0.1.0 + complete + mod_private + + + + + + 1.2 + 1.1.0 + complete + mod_adhoc + + + + + + 1.2 + 0.1.0 + complete + mod_vcard + + + + + + 1.3 + 0.1.0 + complete + mod_vcard + + + + + + 1.0 + 2.1.0 + complete + + + + + + + 1.14 + 0.5.0 + partial + mod_pubsub + + + + + + 1.8 + 2.0.0 + complete + mod_proxy65 + + + + + + 2.4 + 0.1.0 + complete + mod_register + + + + + + 2.5 + 17.03 + complete + mod_legacy_auth + + + + + + 1.1.1 + 2.1.0 + complete + + + + + + + 2.1 + 2.1.0 + complete + mod_client_state + + + + + + 1.0 + 0.5.0 + complete + + + + + + + 1.1 + 0.1.0 + complete + mod_version + + + + + + 1.1 + 0.5.0 + complete + + + + + + + 1.6 + 0.1.0 + complete + ejabberd_service + + + + + + 1.5 + 2.1.4 + complete + mod_caps + + + + + + 1.11 + 16.12 + complete + ejabberd_bosh + + + + + + 1.3.0 + 13.10 + partial + mod_configure + + + + + + 2.1 + 1.1.0 + complete + ejabberd_c2s + + + + + + 1.1 + 17.09 + complete + mod_vcard + + + + + + 1.4.0 + 22.05 + complete + mod_host_meta + + + + + + 1.0 + 2.1.0 + complete + mod_disco + + + + + + 1.0 + 2.1.0 + complete + ejabberd_captcha + + + + + + 1.0 + 16.01 + complete + mod_offline + + + + + + 1.2 + 2.0.0 + complete + mod_pubsub + + + + + + 1.0 + 17.12 + complete + + + + + + + 1.2 + 1.1.0 + complete + ejabberd_auth_anonymous + + + + + + 1.1 + 17.03 + complete + + + + + + + 1.0 + 17.03 + complete + mod_s2s_dialback + + + + + + 1.2 + 2.1.7 + complete + mod_blocking + + + + + + 1.5.2 + 14.05 + complete + mod_stream_mgmt + + + + + + 2.0 + 2.1.0 + complete + mod_ping + + + + + + 2.0 + 2.1.0 + complete + mod_time + + + + + + 2.0 + 2.1.0 + complete + mod_offline + + + + + + 1.0 + 1.1.2 + complete + + + + + + + 1.4 + 16.12 + complete + ejabberd_bosh + + + + + + 0.7 + 20.04 + complete + mod_stun_disco + + + + + + 1.1.1 + 17.03 + complete + mod_s2s_dialback + + + + + + 1.1.1 + 2.0.0 + complete + mod_pubsub + + + + + + 1.1 + 2.1.0 + partial + ejabberd_piefxis + + + + + + 1.0 + 2.1.0 + complete + ejabberd_captcha + + + + + + 1.3 + 2.1.0 + complete + mod_roster + + + + + + 0.2 + 2.1.0 + complete + mod_pubsub + + + + + + 1.2 + 0.5.0 + complete + mod_muc + + + + + + 0.2 + 2.1.3 + complete + mod_sic + + + + + + 1.0.1 + 13.06 + complete + mod_carboncopy + + + + + + 1.0.1 + 24.10 + complete + mod_s2s_bidi + + + + + + 0.6.1 + 15.06 + complete + mod_mam + + + + + + 0.3.1 + 21.12 + complete + mod_muc_room, 0.3.1 since 25.xx + + + + + + 0.1 + 19.09 + complete + mod_jidprep + + + + + + 0.2 + 16.01 + complete + mod_mam, mod_muc_log, mod_offline + + + + + + 0.1 + 14.12 + complete + mod_client_state + + + + + + 0.4.1 + 16.09 + complete + mod_delegation + + + + + + 0.4.1 + 24.10 + complete + mod_privilege + + + + + + 0.2 + 17.08 + complete + mod_push + + + + + + 0.5.0 + 15.09 + complete + mod_mam + + + + + + 0.3.0 + 15.10 + complete + mod_http_upload + + + + + + 1.1.0 + 17.09 + complete + + + + + + + 0.14.1 + 16.03 + complete + mod_mix + + + + + + 0.8.3 + 21.12 + complete + node_pep + + + + + + 0.3.0 + 24.02 + complete + + + + + + + 0.4.0 + 24.02 + complete + + + + + + + 0.2.0 + 18.03 + complete + mod_avatar + + + + + + 1.1.3 + 23.10 + complete + mod_private + + + + + + 0.3.0 + 19.02 + complete + mod_mix_pam + + + + + + 1.1.0 + 18.12 + complete + mod_muc_room + + + + + + 0.2.0 + 18.12 + complete + mod_private + + + + + + 0.1.0 + 23.10 + complete + mod_muc_occupantid + + + + + + 0.4.2 + 24.02 + partial + mod_mam, Tombstones not implemented + + + + + + 0.3.0 + 24.06 + complete + mod_mam + + + + + + 0.2.0 + 24.12 + complete + mod_mam + + + + + + 0.4.0 + 24.02 + complete + + + + + + + 0.2.0 + 15.06 + complete + mod_mam + + + + + + 0.4.0 + 24.02 + complete + , 0.4.0 since 25.03 + + + + + + 0.2.0 + 24.10 + complete + mod_scram_upgrade + + + + + + 0.2.0 + 24.12 + complete + mod_auth_fast + + + + + + 0.1.1 + 25.07 + complete + mod_pubsub_serverinfo + + + + + + 0.1.0 + 24.07 + complete + mod_muc + + + + diff --git a/ejabberd.init.template b/ejabberd.init.template index 90d75404f..060d64317 100644 --- a/ejabberd.init.template +++ b/ejabberd.init.template @@ -6,8 +6,8 @@ # Required-Stop: $remote_fs $network $named $time # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 -# Short-Description: Starts ejabberd jabber server -# Description: Starts ejabberd jabber server, an XMPP +# Short-Description: Starts ejabberd XMPP server +# Description: Starts ejabberd XMPP server, an XMPP # compliant server written in Erlang. ### END INIT INFO diff --git a/ejabberd.service.template b/ejabberd.service.template new file mode 100644 index 000000000..902a81cb2 --- /dev/null +++ b/ejabberd.service.template @@ -0,0 +1,21 @@ +[Unit] +Description=XMPP Server +After=network.target + +[Service] +Type=notify +User=@installuser@ +Group=@installuser@ +LimitNOFILE=65536 +Restart=on-failure +RestartSec=5 +ExecStart=@ctlscriptpath@/ejabberdctl foreground +ExecStop=/bin/sh -c '@ctlscriptpath@/ejabberdctl stop && @ctlscriptpath@/ejabberdctl stopped' +ExecReload=@ctlscriptpath@/ejabberdctl reload_config +NotifyAccess=all +PrivateDevices=true +AmbientCapabilities=CAP_NET_BIND_SERVICE +TimeoutSec=300 + +[Install] +WantedBy=multi-user.target diff --git a/ejabberd.yml.example b/ejabberd.yml.example index 1a6d11b89..b9df7799b 100644 --- a/ejabberd.yml.example +++ b/ejabberd.yml.example @@ -1,656 +1,249 @@ ### -### ejabberd configuration file +### ejabberd configuration file ### +### The parameters used in this configuration file are explained at +### +### https://docs.ejabberd.im/admin/configuration ### - -### The parameters used in this configuration file are explained in more detail -### in the ejabberd Installation and Operation Guide. -### Please consult the Guide in case of doubts, it is included with -### your copy of ejabberd, and is also available online at -### http://www.process-one.net/en/ejabberd/docs/ - ### The configuration file is written in YAML. +### ******************************************************* +### ******* !!! WARNING !!! ******* +### ******* YAML IS INDENTATION SENSITIVE ******* +### ******* MAKE SURE YOU INDENT SECTIONS CORRECTLY ******* +### ******************************************************* ### Refer to http://en.wikipedia.org/wiki/YAML for the brief description. -### However, ejabberd treats different literals as different types: ### -### - unquoted or single-quoted strings. They are called "atoms". -### Example: dog, 'Jupiter', '3.14159', YELLOW -### -### - numeric literals. Example: 3, -45.0, .0 -### -### - quoted or folded strings. -### Examples of quoted string: "Lizzard", "orange". -### Example of folded string: -### > Art thou not Romeo, -### and a Montague? -### ======= -### LOGGING - -## -## loglevel: Verbosity of log files generated by ejabberd. -## 0: No ejabberd log at all (not recommended) -## 1: Critical -## 2: Error -## 3: Warning -## 4: Info -## 5: Debug -## -loglevel: 4 - -## -## rotation: Describe how to rotate logs. Either size and/or date can trigger -## log rotation. Setting count to N keeps N rotated logs. Setting count to 0 -## does not disable rotation, it instead rotates the file and keeps no previous -## versions around. Setting size to X rotate log when it reaches X bytes. -## To disable rotation set the size to 0 and the date to "" -## Date syntax is taken from the syntax newsyslog uses in newsyslog.conf. -## Some examples: -## $D0 rotate every night at midnight -## $D23 rotate every day at 23:00 hr -## $W0D23 rotate every week on Sunday at 23:00 hr -## $W5D16 rotate every week on Friday at 16:00 hr -## $M1D0 rotate on the first day of every month at midnight -## $M5D6 rotate on every 5th day of the month at 6:00 hr -## -log_rotate_size: 10485760 -log_rotate_date: "" -log_rotate_count: 1 - -## -## overload protection: If you want to limit the number of messages per second -## allowed from error_logger, which is a good idea if you want to avoid a flood -## of messages when system is overloaded, you can set a limit. -## 100 is ejabberd's default. -log_rate_limit: 100 - -## -## watchdog_admins: Only useful for developers: if an ejabberd process -## consumes a lot of memory, send live notifications to these XMPP -## accounts. -## -## watchdog_admins: -## - "bob@example.com" - - -### ================ -### SERVED HOSTNAMES - -## -## hosts: Domains served by ejabberd. -## You can define one or several, for example: -## hosts: -## - "example.net" -## - "example.com" -## - "example.org" -## hosts: - - "localhost" + - localhost -## -## route_subdomains: Delegate subdomains to other XMPP servers. -## For example, if this ejabberd serves example.org and you want -## to allow communication with an XMPP server called im.example.org. -## -## route_subdomains: s2s +loglevel: info -### =============== -### LISTENING PORTS +## If you already have certificates, list them here +# certfiles: +# - /etc/letsencrypt/live/domain.tld/fullchain.pem +# - /etc/letsencrypt/live/domain.tld/privkey.pem -## -## listen: The ports ejabberd will listen on, which service each is handled -## by and what options to start it with. -## -listen: - - +listen: + - port: 5222 + ip: "::" module: ejabberd_c2s - ## - ## If TLS is compiled in and you installed a SSL - ## certificate, specify the full path to the - ## file and uncomment these lines: - ## - ## certfile: "/path/to/ssl.pem" - ## starttls: true - ## - ## To enforce TLS encryption for client connections, - ## use this instead of the "starttls" option: - ## - ## starttls_required: true - ## - ## Custom OpenSSL options - ## - ## protocol_options: - ## - "no_sslv3" - ## - "no_tlsv1" - max_stanza_size: 65536 + max_stanza_size: 262144 shaper: c2s_shaper access: c2s - - + starttls_required: true + - + port: 5223 + ip: "::" + module: ejabberd_c2s + max_stanza_size: 262144 + shaper: c2s_shaper + access: c2s + tls: true + - port: 5269 + ip: "::" module: ejabberd_s2s_in - ## - ## ejabberd_service: Interact with external components (transports, ...) - ## - ## - - ## port: 8888 - ## module: ejabberd_service - ## access: all - ## shaper_rule: fast - ## ip: "127.0.0.1" - ## hosts: - ## "icq.example.org": - ## password: "secret" - ## "sms.example.org": - ## password: "secret" - - ## - ## ejabberd_stun: Handles STUN Binding requests - ## - ## - - ## port: 3478 - ## transport: udp - ## module: ejabberd_stun - - ## - ## To handle XML-RPC requests that provide admin credentials: - ## - ## - - ## port: 4560 - ## module: ejabberd_xmlrpc - - - port: 5280 + max_stanza_size: 524288 + shaper: s2s_shaper + - + port: 5443 + ip: "::" module: ejabberd_http - ## request_handlers: - ## "/pub/archive": mod_http_fileserver - web_admin: true - http_poll: true - http_bind: true - ## register: true - captcha: true + tls: true + request_handlers: + /admin: ejabberd_web_admin + /api: mod_http_api + /bosh: mod_bosh + /captcha: ejabberd_captcha + /upload: mod_http_upload + /ws: ejabberd_http_ws + - + port: 5280 + ip: "::" + module: ejabberd_http + request_handlers: + /admin: ejabberd_web_admin + /.well-known/acme-challenge: ejabberd_acme + - + port: 5478 + ip: "::" + transport: udp + module: ejabberd_stun + use_turn: true + ## The server's public IPv4 address: + # turn_ipv4_address: "203.0.113.3" + ## The server's public IPv6 address: + # turn_ipv6_address: "2001:db8::3" + - + port: 1883 + ip: "::" + module: mod_mqtt + backlog: 1000 -## -## s2s_use_starttls: Enable STARTTLS + Dialback for S2S connections. -## Allowed values are: false optional required required_trusted -## You must specify a certificate file. -## -## s2s_use_starttls: optional +s2s_use_starttls: optional -## -## s2s_certfile: Specify a certificate file. -## -## s2s_certfile: "/path/to/ssl.pem" - -## Custom OpenSSL options -## -## s2s_protocol_options: -## - "no_sslv3" -## - "no_tlsv1" - -## -## domain_certfile: Specify a different certificate for each served hostname. -## -## host_config: -## "example.org": -## domain_certfile: "/path/to/example_org.pem" -## "example.com": -## domain_certfile: "/path/to/example_com.pem" - -## -## S2S whitelist or blacklist -## -## Default s2s policy for undefined hosts. -## -## s2s_access: s2s - -## -## Outgoing S2S options -## -## Preferred address families (which to try first) and connect timeout -## in milliseconds. -## -## outgoing_s2s_families: -## - ipv4 -## - ipv6 -## outgoing_s2s_timeout: 10000 - -### ============== -### AUTHENTICATION - -## -## auth_method: Method used to authenticate the users. -## The default method is the internal. -## If you want to use a different method, -## comment this line and enable the correct ones. -## -auth_method: internal - -## -## Store the plain passwords or hashed for SCRAM: -## auth_password_format: plain -## auth_password_format: scram -## -## Define the FQDN if ejabberd doesn't detect it: -## fqdn: "server3.example.com" - -## -## Authentication using external script -## Make sure the script is executable by ejabberd. -## -## auth_method: external -## extauth_program: "/path/to/authentication/script" - -## -## Authentication using ODBC -## Remember to setup a database in the next section. -## -## auth_method: odbc - -## -## Authentication using PAM -## -## auth_method: pam -## pam_service: "pamservicename" - -## -## Authentication using LDAP -## -## auth_method: ldap -## -## List of LDAP servers: -## ldap_servers: -## - "localhost" -## -## Encryption of connection to LDAP servers: -## ldap_encrypt: none -## ldap_encrypt: tls -## -## Port to connect to on LDAP servers: -## ldap_port: 389 -## ldap_port: 636 -## -## LDAP manager: -## ldap_rootdn: "dc=example,dc=com" -## -## Password of LDAP manager: -## ldap_password: "******" -## -## Search base of LDAP directory: -## ldap_base: "dc=example,dc=com" -## -## LDAP attribute that holds user ID: -## ldap_uids: -## - "mail": "%u@mail.example.org" -## -## LDAP filter: -## ldap_filter: "(objectClass=shadowAccount)" - -## -## Anonymous login support: -## auth_method: anonymous -## anonymous_protocol: sasl_anon | login_anon | both -## allow_multiple_connections: true | false -## -## host_config: -## "public.example.org": -## auth_method: anonymous -## allow_multiple_connections: false -## anonymous_protocol: sasl_anon -## -## To use both anonymous and internal authentication: -## -## host_config: -## "public.example.org": -## auth_method: -## - internal -## - anonymous - -### ============== -### DATABASE SETUP - -## ejabberd by default uses the internal Mnesia database, -## so you do not necessarily need this section. -## This section provides configuration examples in case -## you want to use other database backends. -## Please consult the ejabberd Guide for details on database creation. - -## -## MySQL server: -## -## odbc_type: mysql -## odbc_server: "server" -## odbc_database: "database" -## odbc_username: "username" -## odbc_password: "password" -## -## If you want to specify the port: -## odbc_port: 1234 - -## -## PostgreSQL server: -## -## odbc_type: pgsql -## odbc_server: "server" -## odbc_database: "database" -## odbc_username: "username" -## odbc_password: "password" -## -## If you want to specify the port: -## odbc_port: 1234 -## -## If you use PostgreSQL, have a large database, and need a -## faster but inexact replacement for "select count(*) from users" -## -## pgsql_users_number_estimate: true - -## -## ODBC compatible or MSSQL server: -## -## odbc_type: odbc -## odbc_server: "DSN=ejabberd;UID=ejabberd;PWD=ejabberd" - -## -## Number of connections to open to the database for each virtual host -## -## odbc_pool_size: 10 - -## -## Interval to make a dummy SQL request to keep the connections to the -## database alive. Specify in seconds: for example 28800 means 8 hours -## -## odbc_keepalive_interval: undefined - -### =============== -### TRAFFIC SHAPERS - -shaper: - ## - ## The "normal" shaper limits traffic speed to 1000 B/s - ## - normal: 1000 - - ## - ## The "fast" shaper limits traffic speed to 50000 B/s - ## - fast: 50000 - -## -## This option specifies the maximum number of elements in the queue -## of the FSM. Refer to the documentation for details. -## -max_fsm_queue: 1000 - -###. ==================== -###' ACCESS CONTROL LISTS acl: - ## - ## The 'admin' ACL grants administrative privileges to XMPP accounts. - ## You can put here as many accounts as you want. - ## - ## admin: - ## user: - ## - "aleksey": "localhost" - ## - "ermine": "example.org" - ## - ## Blocked users - ## - ## blocked: - ## user: - ## - "baduser": "example.org" - ## - "test" - - ## Local users: don't modify this. - ## - local: + local: user_regexp: "" - - ## - ## More examples of ACLs - ## - ## jabberorg: - ## server: - ## - "jabber.org" - ## aleksey: - ## user: - ## - "aleksey": "jabber.ru" - ## test: - ## user_regexp: "^test" - ## user_glob: "test*" - - ## - ## Loopback network - ## loopback: ip: - - "127.0.0.0/8" + - 127.0.0.0/8 + - ::1/128 - ## - ## Bad XMPP servers - ## - ## bad_servers: - ## server: - ## - "xmpp.zombie.org" - ## - "xmpp.spam.com" +access_rules: + local: + allow: local + c2s: + deny: blocked + allow: all + announce: + allow: admin + configure: + allow: admin + muc_create: + allow: local + pubsub_createnode: + allow: local + trusted_network: + allow: loopback -## -## Define specific ACLs in a virtual host. -## -## host_config: -## "localhost": -## acl: -## admin: -## user: -## - "bob-local": "localhost" +api_permissions: + "console commands": + from: ejabberd_ctl + who: all + what: "*" + "webadmin commands": + from: ejabberd_web_admin + who: admin + what: "*" + "adhoc commands": + from: mod_adhoc_api + who: admin + what: "*" + "http access": + from: mod_http_api + who: + access: + allow: + - acl: loopback + - acl: admin + oauth: + scope: "ejabberd:admin" + access: + allow: + - acl: loopback + - acl: admin + what: + - "*" + - "!stop" + - "!start" + "public commands": + who: + ip: 127.0.0.1/8 + what: + - status + - connected_users_number -### ============ -### ACCESS RULES -access: - ## Maximum number of simultaneous sessions allowed for a single user: - max_user_sessions: - all: 10 - ## Maximum number of offline messages that users can have: - max_user_offline_messages: - admin: 5000 - all: 100 - ## This rule allows access only for local users: - local: - local: allow - ## Only non-blocked users can use c2s connections: - c2s: - blocked: deny - all: allow - ## For C2S connections, all users except admins use the "normal" shaper - c2s_shaper: - admin: none - all: normal - ## All S2S connections use the "fast" shaper - s2s_shaper: - all: fast - ## Only admins can send announcement messages: - announce: - admin: allow - ## Only admins can use the configuration interface: - configure: - admin: allow - ## Admins of this server are also admins of the MUC service: - muc_admin: - admin: allow - ## Only accounts of the local ejabberd server can create rooms: - muc_create: - local: allow - ## All users are allowed to use the MUC service: - muc: - all: allow - ## Only accounts on the local ejabberd server can create Pubsub nodes: - pubsub_createnode: - local: allow - ## In-band registration allows registration of any possible username. - ## To disable in-band registration, replace 'allow' with 'deny'. - register: - all: allow - ## Only allow to register from localhost - trusted_network: - loopback: allow - ## Do not establish S2S connections with bad servers - ## s2s: - ## bad_servers: deny - ## all: allow +shaper: + normal: + rate: 3000 + burst_size: 20000 + fast: 100000 -## By default the frequency of account registrations from the same IP -## is limited to 1 account every 10 minutes. To disable, specify: infinity -## registration_timeout: 600 - -## -## Define specific Access Rules in a virtual host. -## -## host_config: -## "localhost": -## access: -## c2s: -## admin: allow -## all: deny -## register: -## all: deny +shaper_rules: + max_user_sessions: 10 + max_user_offline_messages: + 5000: admin + 100: all + c2s_shaper: + none: admin + normal: all + s2s_shaper: fast -### ================ -### DEFAULT LANGUAGE - -## -## language: Default language used for server messages. -## -language: "en" - -## -## Set a different default language in a virtual host. -## -## host_config: -## "localhost": -## language: "ru" - -### ======= -### CAPTCHA - -## -## Full path to a script that generates the image. -## -## captcha_cmd: "/lib/ejabberd/priv/bin/captcha.sh" - -## -## Host for the URL and port where ejabberd listens for CAPTCHA requests. -## -## captcha_host: "example.org:5280" - -## -## Limit CAPTCHA calls per minute for JID/IP to avoid DoS. -## -## captcha_limit: 5 - -### ======= -### MODULES - -## -## Modules enabled in all ejabberd virtual hosts. -## -modules: +modules: mod_adhoc: {} - mod_announce: # recommends mod_adhoc + mod_adhoc_api: {} + mod_admin_extra: {} + mod_announce: access: announce - mod_blocking: {} # requires mod_privacy + mod_avatar: {} + mod_blocking: {} + mod_bosh: {} mod_caps: {} mod_carboncopy: {} - mod_client_state: - drop_chat_states: true - queue_presence: false - mod_configure: {} # requires mod_adhoc + mod_client_state: {} + mod_configure: {} mod_disco: {} - ## mod_echo: {} - mod_irc: {} - mod_http_bind: {} - ## mod_http_fileserver: - ## docroot: "/var/www" - ## accesslog: "/var/log/ejabberd/access.log" + mod_fail2ban: {} + mod_http_api: {} + mod_http_upload: + put_url: https://@HOST_URL_ENCODE@:5443/upload + custom_headers: + "Access-Control-Allow-Origin": "https://@HOST@" + "Access-Control-Allow-Methods": "GET,HEAD,PUT,OPTIONS" + "Access-Control-Allow-Headers": "Content-Type" mod_last: {} - mod_muc: - ## host: "conference.@HOST@" - access: muc + mod_mam: + ## Mnesia is limited to 2GB, better to use an SQL backend + ## For small servers SQLite is a good fit and is very easy + ## to configure. Uncomment this when you have SQL configured: + ## db_type: sql + assume_mam_usage: true + default: always + mod_mqtt: {} + mod_muc: + access: + - allow + access_admin: + - allow: admin access_create: muc_create access_persistent: muc_create - access_admin: muc_admin - ## mod_muc_log: {} - mod_offline: + access_mam: + - allow + default_room_options: + mam: true + mod_muc_admin: {} + mod_muc_occupantid: {} + mod_offline: access_max_user_messages: max_user_offline_messages mod_ping: {} - ## mod_pres_counter: - ## count: 5 - ## interval: 60 mod_privacy: {} mod_private: {} - ## mod_proxy65: {} - mod_pubsub: + mod_proxy65: + access: local + max_connections: 5 + mod_pubsub: access_createnode: pubsub_createnode - ## reduces resource comsumption, but XEP incompliant - ignore_pep_from_offline: true - ## XEP compliant, but increases resource comsumption - ## ignore_pep_from_offline: false - last_item_cache: false - plugins: - - "flat" - - "hometree" - - "pep" # pep requires mod_caps - mod_register: - ## - ## Protect In-Band account registrations with CAPTCHA. - ## - ## captcha_protected: true - - ## - ## Set the minimum informational entropy for passwords. - ## - ## password_strength: 32 - - ## - ## After successful registration, the user receives - ## a message with this subject and body. - ## - welcome_message: - subject: "Welcome!" - body: |- - Hi. - Welcome to this XMPP server. - - ## - ## When a user registers, send a notification to - ## these XMPP accounts. - ## - ## registration_watchers: - ## - "admin1@example.org" - - ## - ## Only clients in the server machine can register accounts - ## + plugins: + - flat + - pep + force_node_config: + ## Avoid buggy clients to make their bookmarks public + storage:bookmarks: + access_model: whitelist + mod_push: {} + mod_push_keepalive: {} + mod_register: + ## Only accept registration requests from the "trusted" + ## network (see access_rules section above). + ## Think twice before enabling registration from any + ## address. See the Jabber SPAM Manifesto for details: + ## https://github.com/ge0rg/jabber-spam-fighting-manifesto ip_access: trusted_network - - ## - ## Local c2s or remote s2s users cannot register accounts - ## - ## access_from: deny - - access: register - mod_roster: {} + mod_roster: + versioning: true + mod_s2s_bidi: {} + mod_s2s_dialback: {} mod_shared_roster: {} - mod_stats: {} - mod_time: {} + mod_stream_mgmt: + resend_on_timeout: if_offline + mod_stun_disco: {} mod_vcard: {} - mod_version: {} - -## -## Enable modules with custom options in a specific virtual host -## -## host_config: -## "localhost": -## modules: -## mod_echo: -## host: "mirror.localhost" + mod_vcard_xupdate: {} + mod_version: + show_os: false ### Local Variables: ### mode: yaml diff --git a/ejabberdctl.cfg.example b/ejabberdctl.cfg.example index b582527eb..88f99cd78 100644 --- a/ejabberdctl.cfg.example +++ b/ejabberdctl.cfg.example @@ -12,36 +12,22 @@ # #POLL=true -#. -#' SMP: SMP support ([enable|auto|disable]) -# -# Explanation in Erlang/OTP documentation: -# enable: starts the Erlang runtime system with SMP support enabled. -# This may fail if no runtime system with SMP support is available. -# auto: starts the Erlang runtime system with SMP support enabled if it -# is available and more than one logical processor are detected. -# disable: starts a runtime system without SMP support. -# -# Default: auto -# -#SMP=auto - #. #' ERL_MAX_PORTS: Maximum number of simultaneously open Erlang ports # # ejabberd consumes two or three ports for every connection, either -# from a client or from another Jabber server. So take this into +# from a client or from another XMPP server. So take this into # account when setting this limit. # -# Default: 32000 +# Default: 65536 (or 8196 on Windows) # Maximum: 268435456 # -#ERL_MAX_PORTS=32000 +#ERL_MAX_PORTS=65536 #. #' FIREWALL_WINDOW: Range of allowed ports to pass through a firewall # -# If Ejabberd is configured to run in cluster, and a firewall is blocking ports, +# If ejabberd is configured to run in cluster, and a firewall is blocking ports, # it's possible to make Erlang use a defined range of port (instead of dynamic # ports) for node communication. # @@ -61,12 +47,28 @@ #INET_DIST_INTERFACE=127.0.0.1 #. -#' ERL_EPMD_ADDRESS: IP addresses where epmd listens for connections +#' ERL_DIST_PORT: Port number for Erlang distribution # -# IMPORTANT: This option works only in Erlang/OTP R14B03 and newer. +# For Erlang distribution, clustering and ejabberdctl usage, the +# Erlang VM listens in a random TCP port number, and the Erlang Port +# Mapper Daemon (EPMD) is spawned and used to determine this port +# number. +# +# ERL_DIST_PORT can define this port number. In that case, EPMD is +# not spawned during ejabberd startup, and ERL_EPMD_ADDRESS is +# ignored. ERL_DIST_PORT must be set to the same port number during +# ejabberd startup and when calling ejabberdctl. This feature +# requires at least Erlang/OTP 23.1. +# +# Default: not defined +# +#ERL_DIST_PORT=5210 + +#. +#' ERL_EPMD_ADDRESS: IP addresses where EPMD listens for connections # # This environment variable may be set to a comma-separated -# list of IP addresses, in which case the epmd daemon +# list of IP addresses, in which case the EPMD daemon # will listen only on the specified address(es) and on the # loopback address (which is implicitly added to the list if it # has not been specified). The default behaviour is to listen on @@ -85,10 +87,10 @@ # Erlang, and therefore not related to the operating system processes, you do # not have to worry about allowing a huge number of them. # -# Default: 250000 +# Default: 262144 # Maximum: 268435456 # -#ERL_PROCESSES=250000 +#ERL_PROCESSES=262144 #. #' ERL_MAX_ETS_TABLES: Maximum number of ETS and Mnesia tables @@ -99,17 +101,18 @@ # You can safely increase this limit when starting ejabberd. It impacts memory # consumption but the difference will be quite small. # -# Default: 1400 +# Default: 2053 # -#ERL_MAX_ETS_TABLES=1400 +#ERL_MAX_ETS_TABLES=2053 #. #' ERL_OPTIONS: Additional Erlang options # -# The next variable allows to specify additional options passed to erlang while -# starting ejabberd. Some useful options are -noshell, -detached, -heart. When -# ejabberd is started from an init.d script options -noshell and -detached are -# added implicitly. See erl(1) for more info. +# The next variable allows to specify additional options passed to +# all commands using erlang interpreter. This applies to starting +# ejabberd server itself but also auxiliary commands like for example +# starting debug shell. See erl(1) for list of commands that can be +# used here. # # It might be useful to add "-pa /usr/local/lib/ejabberd/ebin" if you # want to add local modules in this path. @@ -118,6 +121,20 @@ # #ERL_OPTIONS="" +#. +#' EJABBERD_OPTS: Additional Erlang options to start ejabberd +# +# The next variable allows to specify additional options passed to erlang while +# starting ejabberd. Some useful options are -noshell, -detached, -heart. When +# ejabberd is started from an init.d script options -noshell and -detached are +# added implicitly. See erl(1) for more info. +# +# For example you can use value "-heart -env HEART_BEAT_TIMEOUT 120 -env ERL_CRASH_DUMP_SECONDS 60" +# +# Default: "" +# +#EJABBERD_OPTS="" + #. #' ERLANG_NODE: Erlang node name # @@ -152,13 +169,46 @@ #' EJABBERD_CONFIG_PATH: ejabberd configuration file # # Specify the full path to the ejabberd configuration file. If the file name has -# a ".yml" extension, it is parsed as a YAML file; otherwise, Erlang syntax is +# yml or yaml extension, it is parsed as a YAML file; otherwise, Erlang syntax is # expected. # # Default: $ETC_DIR/ejabberd.yml # #EJABBERD_CONFIG_PATH=/etc/ejabberd/ejabberd.yml +#. +#' CONTRIB_MODULES_PATH: contributed ejabberd modules path +# +# Specify the full path to the contributed ejabberd modules. If the path is not +# defined, ejabberd will use ~/.ejabberd-modules in home of user running ejabberd. +# +# Default: $HOME/.ejabberd-modules +# +#CONTRIB_MODULES_PATH=/opt/ejabberd-modules + +#. +#' CONTRIB_MODULES_CONF_DIR: configuration directory for contributed modules +# +# Specify the full path to the configuration directory for contributed ejabberd +# modules. In order to configure a module named mod_foo, a mod_foo.yml file can +# be created in this directory. This file will then be used instead of the +# default configuration file provided with the module. +# +# Default: $CONTRIB_MODULES_PATH/conf +# +#CONTRIB_MODULES_CONF_DIR=/etc/ejabberd/modules + +#. +#' CTL_OVER_HTTP: Path to ejabberdctl HTTP listener socket +# +# To speedup ejabberdctl execution time for ejabberd commands, +# you can setup an ejabberd_http listener with ejabberd_ctl handling requests, +# listening in a unix domain socket. +# +# Default: disabled +# +#CTL_OVER_HTTP=sockets/ctl_over_http.sock + #. #' # vim: foldmarker=#',#. foldmethod=marker: diff --git a/ejabberdctl.template b/ejabberdctl.template index 2025fd22d..0d124bead 100755 --- a/ejabberdctl.template +++ b/ejabberdctl.template @@ -2,248 +2,150 @@ # define default configuration POLL=true -SMP=auto ERL_MAX_PORTS=32000 ERL_PROCESSES=250000 ERL_MAX_ETS_TABLES=1400 FIREWALL_WINDOW="" +INET_DIST_INTERFACE="" ERLANG_NODE=ejabberd@localhost # define default environment variables -SCRIPT_DIR=`cd ${0%/*} && pwd` -ERL={{erl}} -IEX={{bindir}}/iex -INSTALLUSER={{installuser}} +[ -z "$SCRIPT" ] && SCRIPT=$0 +SCRIPT_DIR="$(cd "$(dirname "$SCRIPT")" && pwd -P)" +# shellcheck disable=SC2034 +ERTS_VSN="{{erts_vsn}}" +ERL="{{erl}}" +EPMD="{{epmd}}" +IEX="{{iexpath}}" +INSTALLUSER="{{installuser}}" -# Compatibility in ZSH -#setopt shwordsplit 2>/dev/null - -# check the proper system user is used if defined -if [ "$INSTALLUSER" != "" ] ; then - EXEC_CMD="false" - for GID in `id -G`; do - if [ $GID -eq 0 ] ; then - INSTALLUSER_HOME=$(getent passwd "$INSTALLUSER" | cut -d: -f6) - if [ -n "$INSTALLUSER_HOME" ] && [ ! -d "$INSTALLUSER_HOME" ] ; then - mkdir -p "$INSTALLUSER_HOME" - chown "$INSTALLUSER" "$INSTALLUSER_HOME" - fi - EXEC_CMD="su $INSTALLUSER -c" +# check the proper system user is used +case $(id -un) in + "$INSTALLUSER") + EXEC_CMD="as_current_user" + ;; + root) + if [ -n "$INSTALLUSER" ] ; then + EXEC_CMD="as_install_user" + else + EXEC_CMD="as_current_user" + echo "WARNING: It is not recommended to run ejabberd as root" >&2 fi - done - if [ `id -g` -eq `id -g $INSTALLUSER` ] ; then - EXEC_CMD="sh -c" - fi - if [ "$EXEC_CMD" = "false" ] ; then - echo "This command can only be run by root or the user $INSTALLUSER" >&2 - exit 4 - fi -else - EXEC_CMD="sh -c" -fi + ;; + *) + if [ -n "$INSTALLUSER" ] ; then + echo "ERROR: This command can only be run by root or the user $INSTALLUSER" >&2 + exit 7 + else + EXEC_CMD="as_current_user" + fi + ;; +esac # parse command line parameters -ARGS="" -while [ $# -ne 0 ] ; do - PARAM=$1 - shift - case $PARAM in - --) break ;; - --node) ERLANG_NODE_ARG=$1 ; shift ;; - --config-dir) ETC_DIR="$1" ; shift ;; - --config) EJABBERD_CONFIG_PATH="$1" ; shift ;; - --ctl-config) EJABBERDCTL_CONFIG_PATH="$1" ; shift ;; - --logs) LOGS_DIR="$1" ; shift ;; - --spool) SPOOL_DIR="$1" ; shift ;; - *) ARGS="$ARGS $PARAM" ;; +while [ $# -gt 0 ]; do + case $1 in + -n|--node) ERLANG_NODE_ARG=$2; shift 2;; + -s|--spool) SPOOL_DIR=$2; shift 2;; + -l|--logs) LOGS_DIR=$2; shift 2;; + -f|--config) EJABBERD_CONFIG_PATH=$2; shift 2;; + -c|--ctl-config) EJABBERDCTL_CONFIG_PATH=$2; shift 2;; + -d|--config-dir) CONFIG_DIR=$2; shift 2;; + -t|--no-timeout) NO_TIMEOUT="--no-timeout"; shift;; + *) break;; esac done -# Define ejabberd variable if they have not been defined from the command line -if [ "$ETC_DIR" = "" ] ; then - ETC_DIR={{sysconfdir}}/ejabberd -fi -if [ "$EJABBERDCTL_CONFIG_PATH" = "" ] ; then - EJABBERDCTL_CONFIG_PATH=$ETC_DIR/ejabberdctl.cfg -fi -if [ -f "$EJABBERDCTL_CONFIG_PATH" ] ; then - . "$EJABBERDCTL_CONFIG_PATH" -fi -if [ "$EJABBERD_CONFIG_PATH" = "" ] ; then - EJABBERD_CONFIG_PATH=$ETC_DIR/ejabberd.yml -fi -if [ "$LOGS_DIR" = "" ] ; then - LOGS_DIR={{localstatedir}}/log/ejabberd -fi -if [ "$SPOOL_DIR" = "" ] ; then - SPOOL_DIR={{localstatedir}}/lib/ejabberd -fi -if [ "$EJABBERD_DOC_PATH" = "" ] ; then - EJABBERD_DOC_PATH={{docdir}} -fi -if [ "$ERLANG_NODE_ARG" != "" ] ; then - ERLANG_NODE=$ERLANG_NODE_ARG - NODE=${ERLANG_NODE%@*} -fi -if [ "{{release}}" != "true" ] ; then - if [ "$EJABBERDDIR" = "" ] ; then - EJABBERDDIR={{libdir}}/ejabberd - fi - if [ "$EJABBERD_EBIN_PATH" = "" ] ; then - EJABBERD_EBIN_PATH=$EJABBERDDIR/ebin - fi - if [ "$EJABBERD_PRIV_PATH" = "" ] ; then - EJABBERD_PRIV_PATH=$EJABBERDDIR/priv - fi - if [ "$EJABBERD_BIN_PATH" = "" ] ; then - EJABBERD_BIN_PATH=$EJABBERD_PRIV_PATH/bin - fi - if [ "$EJABBERD_SO_PATH" = "" ] ; then - EJABBERD_SO_PATH=$EJABBERD_PRIV_PATH/lib - fi - if [ "$EJABBERD_MSGS_PATH" = "" ] ; then - EJABBERD_MSGS_PATH=$EJABBERD_PRIV_PATH/msgs - fi -fi -EJABBERD_LOG_PATH=$LOGS_DIR/ejabberd.log -SASL_LOG_PATH=$LOGS_DIR/erlang.log -DATETIME=`date "+%Y%m%d-%H%M%S"` -ERL_CRASH_DUMP=$LOGS_DIR/erl_crash_$DATETIME.dump -ERL_INETRC=$ETC_DIR/inetrc +# define ejabberd variables if not already defined from the command line +: "${CONFIG_DIR:="{{config_dir}}"}" +: "${LOGS_DIR:="{{logs_dir}}"}" +: "${EJABBERD_CONFIG_PATH:="$CONFIG_DIR/ejabberd.yml"}" +: "${EJABBERDCTL_CONFIG_PATH:="$CONFIG_DIR/ejabberdctl.cfg"}" +# Allows passing extra Erlang command-line arguments in vm.args file +: "${VMARGS:="$CONFIG_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%@*}" ] && ERLANG_NODE="$ERLANG_NODE@$(hostname -s)" +[ "$ERLANG_NODE" = "${ERLANG_NODE%.*}" ] && S="-s" +: "${SPOOL_DIR:="{{spool_dir}}"}" +: "${EJABBERD_LOG_PATH:="$LOGS_DIR/ejabberd.log"}" + +# backward support for old mnesia spool dir path +: "${SPOOL_DIR_OLD:="$SPOOL_DIR/$ERLANG_NODE"}" +[ -r "$SPOOL_DIR_OLD/schema.DAT" ] && [ ! -r "$SPOOL_DIR/schema.DAT" ] && SPOOL_DIR="$SPOOL_DIR_OLD" # define erl parameters -ERLANG_OPTS="+K $POLL -smp $SMP +P $ERL_PROCESSES $ERL_OPTIONS" -KERNEL_OPTS="" -if [ "$FIREWALL_WINDOW" != "" ] ; then - KERNEL_OPTS="${KERNEL_OPTS} -kernel inet_dist_listen_min ${FIREWALL_WINDOW%-*} inet_dist_listen_max ${FIREWALL_WINDOW#*-}" +ERLANG_OPTS="+K $POLL +P $ERL_PROCESSES $ERL_OPTIONS" +if [ -n "$FIREWALL_WINDOW" ] ; then + ERLANG_OPTS="$ERLANG_OPTS -kernel inet_dist_listen_min ${FIREWALL_WINDOW%-*} inet_dist_listen_max ${FIREWALL_WINDOW#*-}" fi -if [ "$INET_DIST_INTERFACE" != "" ] ; then - INET_DIST_INTERFACE2="$(echo $INET_DIST_INTERFACE | sed 's/\./,/g')" - if [ "$INET_DIST_INTERFACE" != "$INET_DIST_INTERFACE2" ] ; then - INET_DIST_INTERFACE2="{$INET_DIST_INTERFACE2}" +if [ -n "$INET_DIST_INTERFACE" ] ; then + INET_DIST_INTERFACE2=$("$ERL" $ERLANG_OPTS -noshell -eval 'case inet:parse_address("'$INET_DIST_INTERFACE'") of {ok,IP} -> io:format("~p",[IP]); _ -> ok end.' -s erlang halt) + if [ -n "$INET_DIST_INTERFACE2" ] ; then + if [ "$(echo "$INET_DIST_INTERFACE2" | grep -o "," | wc -l)" -eq 7 ] ; then + INET_DIST_INTERFACE2="$INET_DIST_INTERFACE2 -proto_dist inet6_tcp" + fi + ERLANG_OPTS="$ERLANG_OPTS -kernel inet_dist_use_interface $INET_DIST_INTERFACE2" fi - KERNEL_OPTS="${KERNEL_OPTS} -kernel inet_dist_use_interface \"${INET_DIST_INTERFACE2}\"" fi -if [ "$ERLANG_NODE" = "${ERLANG_NODE%.*}" ] ; then - NAME="-sname" -else - NAME="-name" -fi -IEXNAME="-$NAME" +[ -n "$ERL_DIST_PORT" ] && ERLANG_OPTS="$ERLANG_OPTS -erl_epmd_port $ERL_DIST_PORT -start_epmd false" +# 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_CRASH_DUMP="$LOGS_DIR"/erl_crash_$(date "+%Y%m%d-%H%M%S").dump +ERL_INETRC="$CONFIG_DIR"/inetrc -# define ejabberd environment parameters -if [ "$EJABBERD_CONFIG_PATH" != "${EJABBERD_CONFIG_PATH%.yml}" ] ; then - rate=$(sed '/^[ ]*log_rate_limit/!d;s/.*://;s/ *//' $EJABBERD_CONFIG_PATH) - rotate=$(sed '/^[ ]*log_rotate_size/!d;s/.*://;s/ *//' $EJABBERD_CONFIG_PATH) - count=$(sed '/^[ ]*log_rotate_count/!d;s/.*://;s/ *//' $EJABBERD_CONFIG_PATH) - date=$(sed '/^[ ]*log_rotate_date/!d;s/.*://;s/ *//' $EJABBERD_CONFIG_PATH) -else - rate=$(sed '/^[ ]*log_rate_limit/!d;s/.*,//;s/ *//;s/}\.//' $EJABBERD_CONFIG_PATH) - rotate=$(sed '/^[ ]*log_rotate_size/!d;s/.*,//;s/ *//;s/}\.//' $EJABBERD_CONFIG_PATH) - count=$(sed '/^[ ]*log_rotate_count/!d;s/.*,//;s/ *//;s/}\.//' $EJABBERD_CONFIG_PATH) - date=$(sed '/^[ ]*log_rotate_date/!d;s/.*,//;s/ *//;s/}\.//' $EJABBERD_CONFIG_PATH) -fi -[ -z "$rate" ] || EJABBERD_OPTS="log_rate_limit $rate" -[ -z "$rotate" ] || EJABBERD_OPTS="${EJABBERD_OPTS} log_rotate_size $rotate" -[ -z "$count" ] || EJABBERD_OPTS="${EJABBERD_OPTS} log_rotate_count $count" -[ -z "$date" ] || EJABBERD_OPTS="${EJABBERD_OPTS} log_rotate_date '$date'" -[ -z "$EJABBERD_OPTS" ] || EJABBERD_OPTS="-ejabberd ${EJABBERD_OPTS}" - -[ -d $SPOOL_DIR ] || $EXEC_CMD "mkdir -p $SPOOL_DIR" -cd $SPOOL_DIR +# define ejabberd parameters +EJABBERD_OPTS="\ +$(sed '/^log_rotate_size/!d;s/:[ \t]*\([0-9]\{1,\}\).*/ \1/;s/:[ \t]*\(infinity\).*/ \1 /;s/^/ /' "$EJABBERD_CONFIG_PATH")\ +$(sed '/^log_rotate_count/!d;s/:[ \t]*\([0-9]*\).*/ \1 /;s/^/ /' "$EJABBERD_CONFIG_PATH")\ +$(sed '/^log_burst_limit_count/!d;s/:[ \t]*\([0-9]*\).*/ \1 /;s/^/ /' "$EJABBERD_CONFIG_PATH")\ +$(sed '/^log_burst_limit_window_time/!d;s/:[ \t]*\([0-9]*[a-z]*\).*/ \1 /;s/^/ /' "$EJABBERD_CONFIG_PATH")\ +$EJABBERD_OPTS" +[ -n "$EJABBERD_OPTS" ] && EJABBERD_OPTS="-ejabberd $EJABBERD_OPTS" +EJABBERD_OPTS="-mnesia dir \"$SPOOL_DIR\" $MNESIA_OPTIONS $EJABBERD_OPTS -s ejabberd" # export global variables export EJABBERD_CONFIG_PATH -export EJABBERD_MSGS_PATH export EJABBERD_LOG_PATH -export EJABBERD_SO_PATH -export EJABBERD_BIN_PATH -export EJABBERD_DOC_PATH export EJABBERD_PID_PATH export ERL_CRASH_DUMP export ERL_EPMD_ADDRESS +export ERL_DIST_PORT export ERL_INETRC export ERL_MAX_PORTS export ERL_MAX_ETS_TABLES +export CONTRIB_MODULES_PATH +export CONTRIB_MODULES_CONF_DIR +export ERL_LIBS +export SCRIPT_DIR -# start server -start() +set_dist_client() { - check_start - $EXEC_CMD "$ERL \ - $NAME $ERLANG_NODE \ - -noinput -detached \ - -pa $EJABBERD_EBIN_PATH \ - -mnesia dir \"\\\"$SPOOL_DIR\\\"\" \ - $KERNEL_OPTS \ - $EJABBERD_OPTS \ - -s ejabberd \ - -sasl sasl_error_logger \\{file,\\\"$SASL_LOG_PATH\\\"\\} \ - $ERLANG_OPTS $ARGS \"$@\"" + [ -n "$ERL_DIST_PORT" ] && ERLANG_OPTS="$ERLANG_OPTS -dist_listen false" } -# attach to server -debug() +# run command either directly or via su $INSTALLUSER +exec_cmd() { - debugwarning - TTY=`tty | sed -e 's/.*\///g'` - $EXEC_CMD "$ERL \ - $NAME debug-${TTY}-${ERLANG_NODE} \ - -remsh $ERLANG_NODE \ - -hidden \ - $KERNEL_OPTS \ - $ERLANG_OPTS $ARGS \"$@\"" + case $EXEC_CMD in + as_install_user) su -s /bin/sh -c 'exec "$0" "$@"' "$INSTALLUSER" -- "$@" ;; + as_current_user) "$@" ;; + esac +} +exec_erl() +{ + NODE=$1; shift + exec_cmd "$ERL" ${S:--}name "$NODE" $ERLANG_OPTS "$@" +} +exec_iex() +{ + NODE=$1; shift + exec_cmd "$IEX" -${S:--}name "$NODE" --erl "$ERLANG_OPTS" "$@" } -# attach to server using Elixir -iexdebug() -{ - debugwarning - TTY=`tty | sed -e 's/.*\///g'` - # Elixir shell is hidden as default - $EXEC_CMD "$IEX \ - $IEXNAME debug-${TTY}-${ERLANG_NODE} \ - --remsh $ERLANG_NODE \ - --erl \"$KERNEL_OPTS\" \ - --erl \"$ERLANG_OPTS\" --erl \"$ARGS\" --erl \"$@\"" -} - -# start interactive server -live() -{ - livewarning - $EXEC_CMD "$ERL \ - $NAME $ERLANG_NODE \ - -pa $EJABBERD_EBIN_PATH \ - -mnesia dir \"\\\"$SPOOL_DIR\\\"\" \ - $KERNEL_OPTS \ - $EJABBERD_OPTS \ - -s ejabberd \ - $ERLANG_OPTS $ARGS \"$@\"" -} - -# start interactive server with Elixir -iexlive() -{ - livewarning - $EXEC_CMD "$IEX \ - $IEXNAME $ERLANG_NODE \ - -pa $EJABBERD_EBIN_PATH \ - --erl \"-mnesia dir \\\"$SPOOL_DIR\\\"\" \ - --erl \"$KERNEL_OPTS\" \ - --erl \"$EJABBERD_OPTS\" \ - --app ejabberd \ - --erl \"$ERLANG_OPTS\" --erl $ARGS --erl \"$@\"" -} - -etop() -{ - $EXEC_CMD "$ERL \ - $NAME debug-${TTY}-${ERLANG_NODE} \ - -hidden -s etop -s erlang halt -output text -node $ERLANG_NODE" -} - -# TODO: refactor debug warning and livewarning +# usage debugwarning() { if [ "$EJABBERD_BYPASS_WARNINGS" != "true" ] ; then @@ -256,21 +158,22 @@ debugwarning() echo "Please be extremely cautious with your actions," echo "and exit immediately if you are not completely sure." echo "" - echo "To detach this shell from ejabberd, press:" - echo " control+c, control+c" + echo "To exit and detach this shell from ejabberd, press:" + echo " control+g and then q" echo "" + #vt100 echo "Please do NOT use control+c in this debug shell !" + #vt100 echo "" echo "--------------------------------------------------------------------" echo "To bypass permanently this warning, add to ejabberdctl.cfg the line:" echo " EJABBERD_BYPASS_WARNINGS=true" echo "Press return to continue" - read foo + read -r _ echo "" - fi + fi } livewarning() { - check_start if [ "$EJABBERD_BYPASS_WARNINGS" != "true" ] ; then echo "--------------------------------------------------------------------" echo "" @@ -280,31 +183,72 @@ livewarning() echo "Please be extremely cautious with your actions," echo "and exit immediately if you are not completely sure." echo "" - echo "To exit this LIVE mode and stop ejabberd, press:" - echo " q(). and press the Enter key" + echo "To stop ejabberd gracefully:" + echo " ejabberd:stop()." + echo "To quit erlang immediately, press:" + echo " control+g and then q" echo "" echo "--------------------------------------------------------------------" echo "To bypass permanently this warning, add to ejabberdctl.cfg the line:" echo " EJABBERD_BYPASS_WARNINGS=true" echo "Press return to continue" - read foo + read -r _ echo "" fi } -# TODO: Make iex command display only if ejabberd Elixir support has been enabled +check_etop_result() +{ + result=$? + if [ $result -eq 1 ] ; then + echo "" + echo "It seems there was some problem running 'ejabberdctl etop'." + echo "Is the error message something like this?" + echo " Failed to load module 'etop' because it cannot be found..." + echo "Then probably ejabberd was compiled with development tools disabled." + echo "To use 'etop', recompile ejabberd with: ./configure --enable-tools" + echo "" + exit $result + fi +} + +check_iex_result() +{ + result=$? + if [ $result -eq 127 ] ; then + echo "" + echo "It seems there was some problem finding 'iex' binary from Elixir." + echo "Probably ejabberd was compiled with Rebar3 and Elixir disabled, like:" + echo " ./configure" + echo "which is equivalent to:" + echo " ./configure --with-rebar=rebar3 --disable-elixir" + echo "To use 'iex', recompile ejabberd enabling Elixir or using Mix:" + echo " ./configure --enable-elixir" + echo " ./configure --with-rebar=mix" + echo "" + exit $result + fi +} + help() { echo "" echo "Commands to start an ejabberd node:" - echo " start Start an ejabberd node in server mode" - echo " debug Attach an interactive Erlang shell to a running ejabberd node" - echo " iexdebug Attach an interactive Elixir shell to a running ejabberd node" - echo " live Start an ejabberd node in live (interactive) mode" - echo " iexlive Start an ejabberd node in live (interactive) mode, within an Elixir shell" + echo " start Start in server mode" + echo " foreground Start in server mode (attached)" + echo " foreground-quiet Start in server mode (attached), show only critical messages" + echo " live Start in interactive mode, with Erlang shell" + echo " iexlive Start in interactive mode, with Elixir shell" + echo "" + echo "Commands to interact with a running ejabberd node:" + echo " debug Attach an interactive Erlang shell to a running node" + echo " iexdebug Attach an interactive Elixir shell to a running node" + echo " etop Attach to a running node and start Erlang Top" + echo " ping Send ping to the node, returns pong or pang" + echo " started|stopped Wait for the node to fully start|stop" echo "" echo "Optional parameters when starting an ejabberd node:" - echo " --config-dir dir Config ejabberd: $ETC_DIR" + echo " --config-dir dir Config ejabberd: $CONFIG_DIR" echo " --config file Config ejabberd: $EJABBERD_CONFIG_PATH" echo " --ctl-config file Config ejabberdctl: $EJABBERDCTL_CONFIG_PATH" echo " --logs dir Directory for logs: $LOGS_DIR" @@ -313,150 +257,318 @@ help() echo "" } -# common control function -ctl() -{ - COMMAND=$@ - - # Control number of connections identifiers - # using flock if available. Expects a linux-style - # flock that can lock a file descriptor. - MAXCONNID=100 - CONNLOCKDIR={{localstatedir}}/lock/ejabberdctl - FLOCK='/usr/bin/flock' - if [ ! -x "$FLOCK" ] || [ ! -d "$CONNLOCKDIR" ] ; then - JOT='/usr/bin/jot' - if [ ! -x "$JOT" ] ; then - # no flock or jot, simply invoke ctlexec() - CTL_CONN="ctl-${ERLANG_NODE}" - ctlexec $CTL_CONN $COMMAND - result=$? - else - # no flock, but at least there is jot - RAND=`jot -r 1 0 $MAXCONNID` - CTL_CONN="ctl-${RAND}-${ERLANG_NODE}" - ctlexec $CTL_CONN $COMMAND - result=$? - fi +# dynamic node name helper +uid() { + ERTSVERSION="$("$ERL" -version 2>&1 | sed 's|.* \([0-9]*[0-9]\).*|\1|g')" + if [ $ERTSVERSION -lt 11 ] ; then # otp 23.0 includes erts 11.0 + # Erlang/OTP lower than 23, which doesn's support dynamic node code + N=1 + PF=$(( $$ % 97 )) + while + case $# in + 0) NN="${PF}-${N}-${ERLANG_NODE}" + ;; + 1) NN="${PF}-${N}-${1}-${ERLANG_NODE}" + ;; + 2) NN="${PF}-${N}-${1}@${2}" + ;; + esac + N=$(( N + 1 + ( $$ % 5 ) )) + "$EPMD" -names 2>/dev/null | grep -q " ${NN%@*} " + do :; done + echo $NN + else + # Erlang/OTP 23 or higher: use native dynamic node code + # https://www.erlang.org/patches/otp-23.0#OTP-13812 + if [ "$ERLANG_NODE" != "${ERLANG_NODE%.*}" ]; then + echo "undefined@${ERLANG_NODE#*@}" else - # we have flock so we get a lock - # on one of a limited number of - # conn names -- this allows - # concurrent invocations using a bound - # number of atoms - for N in `seq 1 $MAXCONNID`; do - CTL_CONN="ejabberdctl-$N" - CTL_LOCKFILE="$CONNLOCKDIR/$CTL_CONN" - ( - exec 8>"$CTL_LOCKFILE" - if flock --nb 8; then - ctlexec $CTL_CONN $COMMAND - ssresult=$? - # segregate from possible flock exit(1) - ssresult=`expr $ssresult \* 10` - exit $ssresult - else - exit 1 - fi - ) - result=$? - if [ $result -eq 1 ] ; then - # means we errored out in flock - # rather than in the exec - stay in the loop - # trying other conn names... - badlock=1 - else - badlock="" - break; - fi - done - result=`expr $result / 10` + echo "undefined" fi - - if [ "$badlock" ] ;then - echo "Ran out of connections to try. Your ejabberd processes" >&2 - echo "may be stuck or this is a very busy server. For very" >&2 - echo "busy servers, consider raising MAXCONNID in ejabberdctl">&2 - exit 1; - fi - - case $result in - 0) :;; - 1) :;; - 2) help;; - 3) help;; - esac - return $result -} - -ctlexec() -{ - CONN_NAME=$1; shift - COMMAND=$@ - $EXEC_CMD "$ERL \ - $NAME ${CONN_NAME} \ - -noinput \ - -hidden \ - -pa $EJABBERD_EBIN_PATH \ - $KERNEL_OPTS \ - -s ejabberd_ctl -extra $ERLANG_NODE $COMMAND" + fi } # stop epmd if there is no other running node stop_epmd() { - epmd -names 2>/dev/null | grep -q name || epmd -kill >/dev/null + [ -n "$ERL_DIST_PORT" ] && return + "$EPMD" -names 2>/dev/null | grep -q name || "$EPMD" -kill >/dev/null } # make sure node not already running and node name unregistered +# if all ok, ensure runtime directory exists and make it current directory check_start() { - epmd -names 2>/dev/null | grep -q " ${ERLANG_NODE%@*} " && { - ps ux | grep -v grep | grep -q " $ERLANG_NODE " && { + [ -n "$ERL_DIST_PORT" ] && return + "$EPMD" -names 2>/dev/null | grep -q " ${ERLANG_NODE%@*} " && { + pgrep -f "$ERLANG_NODE" >/dev/null && { echo "ERROR: The ejabberd node '$ERLANG_NODE' is already running." exit 4 - } || { - ps ux | grep -v grep | grep -q beam && { - echo "ERROR: The ejabberd node '$ERLANG_NODE' is registered," - echo " but no related beam process has been found." - echo "Shutdown all other erlang nodes, and call 'epmd -kill'." - exit 5 - } || { - epmd -kill >/dev/null - } } + pgrep beam >/dev/null && { + echo "ERROR: The ejabberd node '$ERLANG_NODE' is registered," + echo " but no related beam process has been found." + echo "Shutdown all other erlang nodes, and call 'epmd -kill'." + exit 5 + } + "$EPMD" -kill >/dev/null } } # allow sync calls -wait_for_status() +wait_status() { - # args: status try delay - # return: 0 OK, 1 KO - timeout=$2 - status=4 - while [ $status -ne $1 ] ; do - sleep $3 - timeout=`expr $timeout - 1` - [ $timeout -eq 0 ] && { - status=$1 - } || { - ctl status > /dev/null - status=$? - } - done - [ $timeout -eq 0 ] && return 1 || return 0 + wait_status_node "$ERLANG_NODE" $1 $2 $3 } -# main handler -case $ARGS in - ' start') start;; - ' debug') debug;; - ' iexdebug') iexdebug;; - ' live') live;; - ' iexlive') iexlive;; - ' etop') etop;; - ' started') wait_for_status 0 30 2;; # wait 30x2s before timeout - ' stopped') wait_for_status 3 15 2 && stop_epmd;; # wait 15x2s before timeout - *) ctl $ARGS;; +wait_status_node() +{ + CONNECT_NODE=$1 + shift + # args: status try delay + # return: 0 OK, 1 KO + timeout="$2" + status=4 + while [ "$status" -ne "$1" ] ; do + sleep "$3" + timeout=$((timeout - 1)) + if [ $timeout -eq 0 ] ; then + status="$1" + else + exec_erl "$(uid ctl)" -hidden -noinput \ + -eval 'net_kernel:connect_node('"'$CONNECT_NODE'"')' \ + -s ejabberd_ctl \ + -extra "$CONNECT_NODE" $NO_TIMEOUT status > /dev/null + status="$?" + fi + done + [ $timeout -gt 0 ] +} + +exec_other_command() +{ + exec_other_command_node $ERLANG_NODE "$@" +} + +exec_other_command_node() +{ + CONNECT_NODE=$1 + shift + if [ -z "$CTL_OVER_HTTP" ] || [ ! -S "$CTL_OVER_HTTP" ] \ + || [ ! -x "$(command -v curl)" ] || [ -z "$1" ] || [ "$1" = "help" ] \ + || [ "$1" = "mnesia_info_ctl" ]|| [ "$1" = "print_sql_schema" ] ; then + exec_erl "$(uid ctl)" -hidden -noinput \ + -eval 'net_kernel:connect_node('"'$CONNECT_NODE'"')' \ + -s ejabberd_ctl \ + -extra "$CONNECT_NODE" $NO_TIMEOUT "$@" + result=$? + case $result in + 3) help;; + *) :;; + esac + return $result + else + exec_ctl_over_http_socket "$@" + fi +} + +exec_ctl_over_http_socket() +{ + COMMAND=${1} + CARGS="" + while [ $# -gt 0 ]; do + [ -z "$CARGS" ] && CARGS="[" || CARGS="${CARGS}, " + CARGS="${CARGS}\"$1\"" + shift + done + CARGS="${CARGS}]" + TEMPHEADERS=temp-headers.log + curl \ + --unix-socket ${CTL_OVER_HTTP} \ + --header "Content-Type: application/json" \ + --header "Accept: application/json" \ + --data "${CARGS}" \ + --dump-header ${TEMPHEADERS} \ + --no-progress-meter \ + "http://localhost/ctl/${COMMAND}" + result=$(sed -n 's/.*status-code: \([0-9]*\).*/\1/p' < $TEMPHEADERS) + rm ${TEMPHEADERS} + case $result in + 2|3) exec_other_command help ${COMMAND};; + *) :;; + esac + exit $result +} + +# ensure we can change current directory to SPOOL_DIR +[ -d "$SPOOL_DIR" ] || exec_cmd mkdir -p "$SPOOL_DIR" +cd "$SPOOL_DIR" || { + echo "ERROR: can not access directory $SPOOL_DIR" + exit 6 +} + +printe() +{ + printf "\n" + printf "\e[1;40;32m==> %s\e[0m\n" "$1" +} + +## Function copied from tools/make-installers +user_agrees() +{ + question="$*" + + if [ -t 0 ] + then + printe "$question (y/n) [n]" + read -r response + case "$response" in + [Yy]|[Yy][Ee][Ss]) + return 0 + ;; + [Nn]|[Nn][Oo]|'') + return 1 + ;; + *) + echo 'Please respond with "yes" or "no".' + user_agrees "$question" + ;; + esac + else # Assume 'yes' if not running interactively. + return 0 + fi +} + +mnesia_change() +{ + ERLANG_NODE_OLD="$1" + [ "$ERLANG_NODE_OLD" = "" ] \ + && echo "Error: Please provide the old erlang node name, for example:" \ + && echo " ejabberdctl mnesia_change ejabberd@oldmachine" \ + && exit 1 + + SPOOL_DIR_BACKUP=$SPOOL_DIR/$ERLANG_NODE_OLD-backup/ + OLDFILE=$SPOOL_DIR_BACKUP/$ERLANG_NODE_OLD.backup + NEWFILE=$SPOOL_DIR_BACKUP/$ERLANG_NODE.backup + + printe "This changes your mnesia database from node name '$ERLANG_NODE_OLD' to '$ERLANG_NODE'" + + [ -d "$SPOOL_DIR_BACKUP" ] && printe "WARNING! A backup of old node already exists in $SPOOL_DIR_BACKUP" + + if ! user_agrees "Do you want to proceed?" + then + echo 'Operation aborted.' + exit 1 + fi + + printe "Starting ejabberd with old node name $ERLANG_NODE_OLD ..." + exec_erl "$ERLANG_NODE_OLD" $EJABBERD_OPTS -detached + wait_status_node $ERLANG_NODE_OLD 0 30 2 + result=$? + case $result in + 1) echo "There was a problem starting ejabberd with the old erlang node name. " \ + && echo "Check for log errors in $EJABBERD_LOG_PATH" \ + && exit $result;; + *) :;; + esac + exec_other_command_node $ERLANG_NODE_OLD "status" + + printe "Making backup of old database to file $OLDFILE ..." + mkdir $SPOOL_DIR_BACKUP + exec_other_command_node $ERLANG_NODE_OLD backup "$OLDFILE" + + printe "Changing node name in new backup file $NEWFILE ..." + exec_other_command_node $ERLANG_NODE_OLD mnesia_change_nodename "$ERLANG_NODE_OLD" "$ERLANG_NODE" "$OLDFILE" "$NEWFILE" + + printe "Stopping old ejabberd..." + exec_other_command_node $ERLANG_NODE_OLD "stop" + wait_status_node $ERLANG_NODE_OLD 3 30 2 && stop_epmd + + printe "Moving old mnesia spool files to backup subdirectory $SPOOL_DIR_BACKUP ..." + mv $SPOOL_DIR/*.DAT $SPOOL_DIR_BACKUP + mv $SPOOL_DIR/*.DCD $SPOOL_DIR_BACKUP + mv $SPOOL_DIR/*.LOG $SPOOL_DIR_BACKUP + + printe "Starting ejabberd with new node name $ERLANG_NODE ..." + exec_erl "$ERLANG_NODE" $EJABBERD_OPTS -detached + wait_status 0 30 2 + exec_other_command "status" + + printe "Installing fallback of new mnesia..." + exec_other_command install_fallback "$NEWFILE" + + printe "Stopping new ejabberd..." + exec_other_command "stop" + wait_status 3 30 2 && stop_epmd + + printe "Finished, now you can start ejabberd normally" +} + +# main +case $1 in + start) + check_start + exec_erl "$ERLANG_NODE" $EJABBERD_OPTS -detached + ;; + foreground) + check_start + exec_erl "$ERLANG_NODE" $EJABBERD_OPTS -noinput + ;; + foreground-quiet) + check_start + exec_erl "$ERLANG_NODE" $EJABBERD_OPTS -noinput -ejabberd quiet true + ;; + live) + livewarning + check_start + exec_erl "$ERLANG_NODE" $EJABBERD_OPTS + ;; + debug) + debugwarning + set_dist_client + exec_erl "$(uid debug)" -hidden -remsh "$ERLANG_NODE" + ;; + etop) + set_dist_client + exec_erl "$(uid top)" -hidden -remsh "$ERLANG_NODE" -s etop \ + -output text + check_etop_result + ;; + iexdebug) + debugwarning + set_dist_client + exec_iex "$(uid debug)" --remsh "$ERLANG_NODE" + check_iex_result + ;; + iexlive) + livewarning + exec_iex "$ERLANG_NODE" --erl "$EJABBERD_OPTS" + check_iex_result + ;; + ping) + PEER=${2:-$ERLANG_NODE} + [ "$PEER" = "${PEER%.*}" ] && PS="-s" + set_dist_client + exec_cmd "$ERL" ${PS:--}name "$(uid ping "$(hostname $PS)")" $ERLANG_OPTS \ + -noinput -hidden \ + -eval 'net_kernel:connect_node('"'$PEER'"')' \ + -eval 'io:format("~p~n",[net_adm:ping('"'$PEER'"')])' \ + -eval 'halt(case net_adm:ping('"'$PEER'"') of pong -> 0; pang -> 1 end).' \ + -output text + ;; + started) + set_dist_client + wait_status 0 30 2 # wait 30x2s before timeout + ;; + stopped) + set_dist_client + wait_status 3 30 2 && stop_epmd # wait 30x2s before timeout + ;; + mnesia_change) + mnesia_change $2 + ;; + *) + set_dist_client + exec_other_command "$@" + ;; esac diff --git a/elvis.config b/elvis.config new file mode 100644 index 000000000..4ac4df9e1 --- /dev/null +++ b/elvis.config @@ -0,0 +1,59 @@ +[ + { + elvis, + [ + {config, + [#{dirs => ["src"], + filter => "*.erl", + ignore => ['ELDAPv3', eldap_filter_yecc], + ruleset => erl_files, + rules => [{elvis_text_style, line_length, #{limit => 1000, skip_comments => false}}, + {elvis_text_style, no_tabs, disable}, + {elvis_style, atom_naming_convention, disable}, + {elvis_style, consistent_variable_casing, disable}, + {elvis_style, dont_repeat_yourself, #{min_complexity => 70}}, + {elvis_style, export_used_types, disable}, + {elvis_style, function_naming_convention, disable}, + {elvis_style, god_modules, #{limit => 300}}, + {elvis_style, invalid_dynamic_call, disable}, + {elvis_style, macro_names, disable}, + {elvis_style, max_function_arity, disable}, % #{max_arity => 15}}, + {elvis_style, nesting_level, disable}, + {elvis_style, no_author, disable}, + {elvis_style, no_boolean_in_comparison, disable}, + {elvis_style, no_catch_expressions, disable}, + {elvis_style, no_debug_call, disable}, + {elvis_style, no_if_expression, disable}, + {elvis_style, no_import, disable}, + {elvis_style, no_nested_try_catch, disable}, + {elvis_style, no_operation_on_same_value, disable}, + {elvis_style, no_receive_without_timeout, disable}, + {elvis_style, no_single_clause_case, disable}, + {elvis_style, no_spec_with_records, disable}, + {elvis_style, no_throw, disable}, + {elvis_style, operator_spaces, disable}, + {elvis_style, param_pattern_matching, disable}, + {elvis_style, private_data_types, disable}, + {elvis_style, variable_naming_convention, disable} + ] + }, + + %#{dirs => ["include"], + % filter => "*.hrl", + % ruleset => hrl_files}, + + #{dirs => ["."], + filter => "Makefile.in", + ruleset => makefiles, + rules => [{elvis_text_style, line_length, #{limit => 400, + skip_comments => false}}, + {elvis_style, dont_repeat_yourself, #{min_complexity => 20}} + ] + } + ] + } + ] + } +]. + +%% vim: set filetype=erlang tabstop=8: diff --git a/erlang_ls.config b/erlang_ls.config new file mode 100644 index 000000000..83e3e52a5 --- /dev/null +++ b/erlang_ls.config @@ -0,0 +1,32 @@ +#otp_path: "/usr/lib/erlang" +#plt_path: "_build/default/rebar3_24.3.3_plt" +#code_reload: +# node: ejabberd@localhost +apps_dirs: + - "_build/default/lib/*" +deps_dirs: + - "_build/default/lib/*" +include_dirs: + - "_build/default/lib" + - "_build/default/lib/*/include" + - "include" +macros: + - name: DEPRECATED_GET_STACKTRACE + - name: HAVE_ERL_ERROR + - name: HAVE_URI_STRING + - name: OTP_BELOW_27 + - name: SIP + - name: STUN +diagnostics: + enabled: + - crossref + disabled: +# - dialyzer + - unused_includes # Otherwise it complains about unused logger.hrl +lenses: + disabled: + - ct-run-test + - function-references + - server-info + - show-behaviour-usages + - suggest-spec diff --git a/examples/extauth/check_pass_null.pl b/examples/extauth/check_pass_null.pl deleted file mode 100755 index cbf179202..000000000 --- a/examples/extauth/check_pass_null.pl +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/perl - -use Unix::Syslog qw(:macros :subs); - -my $domain = $ARGV[0] || "example.com"; - -while(1) - { - # my $rin = '',$rout; - # vec($rin,fileno(STDIN),1) = 1; - # $ein = $rin; - # my $nfound = select($rout=$rin,undef,undef,undef); - - my $buf = ""; - syslog LOG_INFO,"waiting for packet"; - my $nread = sysread STDIN,$buf,2; - do { syslog LOG_INFO,"port closed"; exit; } unless $nread == 2; - my $len = unpack "n",$buf; - my $nread = sysread STDIN,$buf,$len; - - my ($op,$user,$host,$password) = split /:/,$buf; - #$user =~ s/\./\//og; - my $jid = "$user\@$domain"; - my $result; - - syslog(LOG_INFO,"request (%s)", $op); - - SWITCH: - { - $op eq 'auth' and do - { - $result = 1; - },last SWITCH; - - $op eq 'setpass' and do - { - $result = 1; - },last SWITCH; - - $op eq 'isuser' and do - { - # password is null. Return 1 if the user $user\@$domain exitst. - $result = 1; - },last SWITCH; - - $op eq 'tryregister' and do - { - $result = 1; - },last SWITCH; - - $op eq 'removeuser' and do - { - # password is null. Return 1 if the user $user\@$domain exitst. - $result = 1; - },last SWITCH; - - $op eq 'removeuser3' and do - { - $result = 1; - },last SWITCH; - }; - my $out = pack "nn",2,$result ? 1 : 0; - syswrite STDOUT,$out; - } - -closelog; diff --git a/examples/mtr/ejabberd b/examples/mtr/ejabberd deleted file mode 100644 index 4328b0697..000000000 --- a/examples/mtr/ejabberd +++ /dev/null @@ -1,75 +0,0 @@ -#!/bin/sh -# -# PROVIDE: ejabberd -# REQUIRE: DAEMON -# KEYWORD: shutdown -# - -HOME=/usr/pkg/jabber D=/usr/pkg/jabber/ejabberd export HOME - -name="ejabberd" -rcvar=$name - -if [ -r /etc/rc.conf ] -then - . /etc/rc.conf -else - eval ${rcvar}=YES -fi - -# $flags from environment overrides ${rcvar}_flags -if [ -n "${flags}" ] -then - eval ${rcvar}_flags="${flags}" -fi - -checkyesno() -{ - eval _value=\$${1} - case $_value in - [Yy][Ee][Ss]|[Tt][Rr][Uu][Ee]|[Oo][Nn]|1) return 0 ;; - [Nn][Oo]|[Ff][Aa][Ll][Ss][Ee]|[Oo][Ff][Ff]|0) return 1 ;; - *) - echo "\$${1} is not set properly." - return 1 - ;; - esac -} - -cmd=${1:-start} -case ${cmd} in -force*) - cmd=${cmd#force} - eval ${rcvar}=YES - ;; -esac - -if checkyesno ${rcvar} -then -else - exit 0 -fi - -case ${cmd} in -start) - if [ -x $D/src ]; then - echo "Starting ${name}." - cd $D/src - ERL_MAX_PORTS=32000 export ERL_MAX_PORTS - ulimit -n $ERL_MAX_PORTS - su jabber -c "/usr/pkg/bin/erl -sname ejabberd -s ejabberd -heart -detached -sasl sasl_error_logger '{file, \"ejabberd-sasl.log\"}' &" \ - 1>/dev/null 2>&1 - fi - ;; -stop) - echo "rpc:call('ejabberd@`hostname -s`', init, stop, [])." | \ - su jabber -c "/usr/pkg/bin/erl -sname ejabberdstop" - ;; -restart) - echo "rpc:call('ejabberd@`hostname -s`', init, restart, [])." | \ - su jabber -c "/usr/pkg/bin/erl -sname ejabberdrestart" - ;; -*) - echo "Usage: $0 {start|stop|restart}" - exit 1 -esac diff --git a/examples/mtr/ejabberd-netbsd.sh b/examples/mtr/ejabberd-netbsd.sh deleted file mode 100644 index 9896c9bc4..000000000 --- a/examples/mtr/ejabberd-netbsd.sh +++ /dev/null @@ -1,81 +0,0 @@ -#!/bin/sh - -echo '1. fetch, compile, and install erlang' - -if [ ! pkg_info erlang 1>/dev/null 2>&1 ]; then - cd /usr/pkgsrc/lang/erlang - make fetch-list|sh - make - make install -fi -if pkg_info erlang | grep -q erlang-9.1nb1; then -else - echo "erlang-9.1nb1 not installed" 1>&2 - exit 1 -fi - - -echo '2. install crypt_drv.so' - -if [ ! -d /usr/pkg/lib/erlang/lib/crypto-1.1.2.1/priv/lib ] ; then - mkdir -p /usr/pkg/lib/erlang/lib/crypto-1.1.2.1/priv/lib -fi -if [ ! -f /usr/pkg/lib/erlang/lib/crypto-1.1.2.1/priv/lib/crypto_drv.so ]; then - cp work/otp*/lib/crypto/priv/*/*/crypto_drv.so \ - /usr/pkg/lib/erlang/lib/crypto-1.1.2.1/priv/lib -fi - - -echo '3. compile and install elibcrypto.so' - -if [ ! -f /usr/pkg/lib/erlang/lib/crypto-1.1.2.1/priv/lib/elibcrypto.so ]; then -cd /usr/pkgsrc/lang/erlang/work/otp_src_R9B-1/lib/crypto/c_src -ld -r -u CRYPTO_set_mem_functions -u MD5 -u MD5_Init -u MD5_Update \ - -u MD5_Final -u SHA1 -u SHA1_Init -u SHA1_Update -u SHA1_Final \ - -u des_set_key -u des_ncbc_encrypt -u des_ede3_cbc_encrypt \ - -L/usr/lib -lcrypto -o ../priv/obj/i386--netbsdelf/elibcrypto.o -cc -shared \ - -L/usr/pkgsrc/lang/erlang/work/otp_src_R9B-1/lib/erl_interface/obj/i386--netbsdelf \ - -o ../priv/obj/i386--netbsdelf/elibcrypto.so \ - ../priv/obj/i386--netbsdelf/elibcrypto.o -L/usr/lib -lcrypto -cp ../priv/obj/i386--netbsdelf/elibcrypto.so \ - /usr/pkg/lib/erlang/lib/crypto-1.1.2.1/priv/lib -fi - - -echo '4. compile and install ssl_esock' - -if [ ! -f /usr/pkg/lib/erlang/lib/ssl-2.3.5/priv/bin/ssl_esock ]; then - cd /usr/pkg/lib/erlang/lib/ssl-2.3.5/priv/obj/ - make -fi - - -echo '5. initial ejabberd configuration' - -cd /usr/pkg/jabber/ejabberd/src -./configure - - -echo '6. edit ejabberd Makefiles' - -for M in Makefile mod_*/Makefile; do - if [ ! -f $M.orig ]; then - mv $M $M.orig - sed -e s%/usr/local%/usr/pkg%g < $M.orig > $M - fi -done - - -echo '7. compile ejabberd' - -gmake -for A in mod_irc mod_muc mod_pubsub; do - (cd $A; gmake) -done - - -echo '' -echo 'now edit ejabberd.cfg' -echo '' -echo 'to start ejabberd: erl -sname ejabberd -s ejabberd' diff --git a/examples/mtr/ejabberd.cfg b/examples/mtr/ejabberd.cfg deleted file mode 100644 index 6f4f4b284..000000000 --- a/examples/mtr/ejabberd.cfg +++ /dev/null @@ -1,66 +0,0 @@ -% jabber.dbc.mtview.ca.us - -override_acls. - -{acl, admin, {user, "mrose", "jabber.dbc.mtview.ca.us"}}. - - -{access, announce, [{allow, admin}, - {deny, all}]}. -{access, c2s, [{deny, blocked}, - {allow, all}]}. -{access, c2s_shaper, [{none, admin}, - {normal, all}]}. -{access, configure, [{allow, admin}, - {deny, all}]}. -{access, disco_admin, [{allow, admin}, - {deny, all}]}. -{access, muc_admin, [{allow, admin}, - {deny, all}]}. -{access, register, [{deny, all}]}. -{access, s2s_shaper, [{fast, all}]}. - - -{auth_method, internal}. -{host, "jabber.dbc.mtview.ca.us"}. -{outgoing_s2s_port, 5269}. -{shaper, normal, {maxrate, 1000}}. -{shaper, fast, {maxrate, 50000}}. -{welcome_message, none}. - - -{listen, [{5222, ejabberd_c2s, - [{access, c2s}, - {shaper, c2s_shaper}]}, - {5223, ejabberd_c2s, - [{access, c2s}, - {shaper, c2s_shaper}, - {ssl, [{certfile, "/etc/openssl/certs/ejabberd.pem"}]}]}, - {5269, ejabberd_s2s_in, - [{shaper, s2s_shaper}]}]}. - - -{modules, [ - {mod_register, []}, - {mod_roster, []}, - {mod_privacy, []}, - {mod_configure, []}, - {mod_disco, []}, - {mod_stats, []}, - {mod_vcard, []}, - {mod_offline, []}, - {mod_echo, [{host, "echo.jabber.dbc.mtview.ca.us"}]}, - {mod_private, []}, -% {mod_irc, []}, - {mod_muc, []}, - {mod_pubsub, []}, - {mod_time, []}, - {mod_last, []}, - {mod_version, []} - ]}. - - - -% Local Variables: -% mode: erlang -% End: diff --git a/examples/transport-configs/configs/aim-transport.xml b/examples/transport-configs/configs/aim-transport.xml deleted file mode 100644 index 41804c69d..000000000 --- a/examples/transport-configs/configs/aim-transport.xml +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - - - - - %d: [%t] (%h): %s - /var/log/jabber/aim-transport-error.log - - - - - record - %d %h %s - /var/log/jabber/aim-transport-record.log - - - - - - - - /usr/local/lib/jabber/libjabberdxdbfile.so - - - /var/spool/jabber - - - - - - - - - - AIM/ICQ Transport - This is the AIM/ICQ Transport. - EMAIL@ADDRESS.COM - http://aim-transport.jabberstudio.org/ - - cp1252 - - - - /usr/local/lib/jabber/aim-transport.so - - - - - - - - - 127.0.0.1 - 5233 - SECRET - - - - /var/run/jabber/aim-transport.pid - - diff --git a/examples/transport-configs/configs/ile.xml b/examples/transport-configs/configs/ile.xml deleted file mode 100644 index 5999f0fbd..000000000 --- a/examples/transport-configs/configs/ile.xml +++ /dev/null @@ -1,136 +0,0 @@ - - - - - 127.0.0.1 - 5238 - SECRET - ile.SERVER.COM - 7 - en - - I Love Email - With this service you can receive email notifications. - -Security warning: Be careful when using this. Your password will travel in clear from your client to your jabber server if you don't use SSL and it will probably travel in clear from the jabber server to your email server. Use with care. This shouldn't be an issue in your Intranet, but it is if you use an ILE installed in a foreign jabber server. - EMAIL@ADDRESS.COM - http://ile.jabberstudio.org/ - - - - - /var/log/jabber/ile.log - 1 - - - - 10 - 20 - - - - /var/spool/jabber/ile.SERVER.COM/users.db - /var/spool/jabber/ile.SERVER.COM/passwords.db - /var/spool/jabber/ile.SERVER.COM/hosts.db - /var/spool/jabber/ile.SERVER.COM/types.db - /var/spool/jabber/ile.SERVER.COM/notifyxa.db - /var/spool/jabber/ile.SERVER.COM/notifydnd.db - /var/spool/jabber/ile.SERVER.COM/urls.db - - -
- - Please fill in the fields,according to your email account settings and notification preferences - ILE: Email notification service - Email account settings - Username - Password - Hostname - Type - You have received NUM email messages since last time I checked, which was CHECKINTERVAL minutes ago. - There was an error while trying to check mail for ACCOUNT. - Notification Options - Notify even when Xtended Away (XA) - Notify even when Do Not Disturb (DND) - Webmail URL - Login to ACCOUNT - ILE: an email notifier component: http://ile.jabberstudio.org - - - - Por favor, rellene los campos del formulario. - ILE: Servicio de notificación de correo - Configuración de la cuenta de correo - Usuario - Clave - Host - Tipo - Ha recibido NUM email(s) desde la última comprobación que fue hace CHECKINTERVAL minutos - Ha habido un error en la comprobación del correo para la cuenta ACCOUNT. - Opciones de notificación - Notificar incluso si muy ausente (XA) - Notificar incluso si no molestar (DND) - Webmail URL - Leer correo de ACCOUNT - ILE: un notificador de nuevo email - http://ile.jabberstudio.org - - - - Ompli els camps del formulari. - ILE: Servei de notificació de nou email - Dades del compte de mail - Usuari - Clau - Host - Tipus - Ha rebut NUM email(s) des de la última comprobació que va ser fa CHECKINTERVAL minuts. - S'ha produit un error en la comprobació del correu per al compte ACCOUNT. - Opcions de notificació - Notificar si molt absent (XA) - Notificar si no molestar (DND) - Webmail URL - Llegir correu de ACCOUNT - ILE: un notificador de nou email - http://ile.jabberstudio.org - - - - - Va rog completati urmatoarele campuri - I Love Email: new email notification service - Email account settings - Nume utilizator - Parola - Nume gazda - Tip - Ati primit NUM mesaj(e) de la ultima verificare, care a fost acum CHECKINTERVAL minute. - A fost eroare in timp ce incercam sa verific posta pentru ACCOUNT. - Notification Options - Notify even when Xtended Away (XA) - Notify even when Do Not Disturb (DND) - Webmail URL - Login to ACCOUNT - ILE: an email notifier component: http://ile.jabberstudio.org - - - - - Vul volgende velden in. - ILE: Dienst voor e-mailnotificaties - Instellingen van e-mailaccount - Gebruikersnaam - Wachtwoord - Inkomende mailserver - Type verbinding - U hebt NUM berichten ontvangen sinds CHECKINTERVAL minuten geleden. - Fout tijdens controle op nieuwe e-mails bij ACCOUNT. ILE zal deze account niet meer opnieuw controleren tot u uw registratiegegevens wijzigt of opnieuw aanmeldt. - Notificatie-instellingen - Notificeer ook in de status Niet Beschikbaar (XA) - Notificeer ook in de status Niet Storen (DND) - URL van webmail - Aanmelden op ACCOUNT - ILE: een dienst om e-mailnotificaties te ontvangen: http://ile.jabberstudio.org - - -
- -
\ No newline at end of file diff --git a/examples/transport-configs/configs/jabber-gg-transport.xml b/examples/transport-configs/configs/jabber-gg-transport.xml deleted file mode 100644 index 39d8c0b64..000000000 --- a/examples/transport-configs/configs/jabber-gg-transport.xml +++ /dev/null @@ -1,149 +0,0 @@ - - - - - - - 127.0.0.1 - 5237 - SECRET - - - - - - Fill in your GG number (after "username") - and password to register on the transport. -

To change your information in the GaduGadu directory you need to fill in the other fields. -

To remove registration you need to leave the form blank. - - - - - - - To search people:
- First fill in surname or family name, nickname, city, birthyear or range of birthyears (eg. 1950-1960) - and gender (you may fill in more fields at once).
- or
- Fill in phone number
- or
- Fill in the GG number of the person you are searching. -
-
- - - - Please fill in the GaduGadu number of the person you want to add. - - GG Nummer - - - - Gadu-Gadu Transport - This is the Gadu-Gadu Transport. - EMAIL@ADDRESS.COM - http://www.jabberstudio.org/projects/jabber-gg-transport/ - - - - - - - - /var/log/jabber/jabber-gg-transport.log - - - - - - 60 - 10 - - - 315360000 - - - 300 - - - 60 - - - 5 - - - - - - - /var/spool/jabber/gg.SERVER.COM/ - - - /var/run/jabber/jabber-gg-transport.pid - - - GG_TRANSPORT_ADMIN@SERVER.COM - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/examples/transport-configs/configs/jit.xml b/examples/transport-configs/configs/jit.xml deleted file mode 100644 index c0d78c40a..000000000 --- a/examples/transport-configs/configs/jit.xml +++ /dev/null @@ -1,128 +0,0 @@ - - - - - - - - - - /var/log/jabber/jit-error - - - - - record - /var/log/jabber/jit-record - - - - - - - - /usr/local/lib/jabber/xdb_file.so - - - /var/spool/jabber - - - - - - - - sms.icq.SERVER.COM - - - - sms.icq.SERVER.COM - - away - - - Fill in your UIN and password. - Search ICQ users. - - ICQ Transport (JIT) - This is the Jabber ICQ Transport. - EMAIL@ADDRESS.COM - http://jit.jabberstudio.org/ - - - 3907 - - - - - - - - - - - - /var/spool/jabber/jit-count - - 5 - - 5 - - 18000 - windows-1252 - - login.icq.com - - - - - /usr/local/lib/jabber/jit.so - - - - - - - SERVER.COM - - - 127.0.0.1 - 5234 - SECRET - - - - /var/run/jabber/jit.pid - - diff --git a/examples/transport-configs/configs/msn-transport.xml b/examples/transport-configs/configs/msn-transport.xml deleted file mode 100644 index a6f1391db..000000000 --- a/examples/transport-configs/configs/msn-transport.xml +++ /dev/null @@ -1,118 +0,0 @@ - - - - - - - - - - %d: [%t] (%h): %s - /var/log/jabber/msn-transport-error.log - - - - - record - %d %h %s - /var/log/jabber/msn-transport-record.log - - - - - - - - /usr/local/lib/jabber/libjabberdxdbfile.so - - - /var/spool/jabber - - - - - - - - - Fill in your MSN account and password (eg: user1@hotmail.com). A nickname is optional. - - MSN Transport - This is the MSN Transport. - EMAIL@ADDRESS.COM - http://msn-transport.jabberstudio.org/ - - - - - More than one user entered this chat session. Enter this room to switch to groupchat modus. - - is available - has leaved the room - - - - - - - - - - /usr/local/lib/jabber/msn-transport.so - - - - - - - - - 127.0.0.1 - 5235 - SECRET - - - - /var/run/jabber/msn-transport.pid - - diff --git a/examples/transport-configs/configs/yahoo-transport-2.xml b/examples/transport-configs/configs/yahoo-transport-2.xml deleted file mode 100644 index 2d077aa7a..000000000 --- a/examples/transport-configs/configs/yahoo-transport-2.xml +++ /dev/null @@ -1,86 +0,0 @@ - - - - - - - - - - %d: [%t] (%h): %s - /var/log/jabber/yahoo-transport-2-error.log - - - - - - - - - /usr/local/lib/jabber/libjabberdxdbfile.so - - - /var/spool/jabber - - - - - - - - - - Yahoo! Transport - vCard not implemented in current version - This is the Yahoo! transport. - EMAIL@ADDRESS.COM - http://yahoo-transport-2.jabberstudio.org/ - - Fill in your YAHOO! Messenger username and password to register on this transport. - scs.msg.yahoo.com - 5050 - - CP1252 - - - - - - /usr/local/lib/jabber/yahoo-transport-2.so - - - - - - - - - 127.0.0.1 - 5236 - SECRET - - - - /var/run/jabber/yahoo-transport-2.pid - - diff --git a/examples/transport-configs/init-scripts/aim-transport b/examples/transport-configs/init-scripts/aim-transport deleted file mode 100755 index e13d6572f..000000000 --- a/examples/transport-configs/init-scripts/aim-transport +++ /dev/null @@ -1,45 +0,0 @@ -#!/bin/sh -######################################################### -# -# aim-transport -- script to start aim-transport. -# -######################################################### - -DAEMON=/usr/local/sbin/jabberd-aim-transport -CONF=/etc/jabber/aim-transport.xml -NAME=jabberd-aim-transport -HOME=/etc/jabber/ -USER=ejabberd - -######################################################### - -if [ "`/usr/bin/whoami`" != "$USER" ]; then - - echo "You need to be" $USER "user to run this script." - exit 1 -fi - -case "$1" in - debug) - test -f $DAEMON -a -f $CONF || exit 0 - echo "Starting $NAME in debugging mode." - $DAEMON -D -H $HOME -c $CONF & - ;; - start) - test -f $DAEMON -a -f $CONF || exit 0 - echo "Starting $NAME." - $DAEMON -H $HOME -c $CONF & - ;; - stop) - echo "Stopping $NAME." - killall $NAME & - ;; - restart|reload) - $0 stop - sleep 3 - $0 start - ;; - *) - echo "Usage: $0 {debug|start|stop|restart}" - exit 1 -esac diff --git a/examples/transport-configs/init-scripts/ile b/examples/transport-configs/init-scripts/ile deleted file mode 100755 index a1e072f2c..000000000 --- a/examples/transport-configs/init-scripts/ile +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/sh -######################################################### -# -# ile -- script to start ILE. -# -######################################################### - -DAEMON=/usr/local/sbin/ile.pl -NAME=ile.pl -CONF=/etc/jabber/ile.xml -USER=ejabberd - -######################################################### - -if [ "`/usr/bin/whoami`" != "$USER" ]; then - - echo "You need to be" $USER "user to run this script." - exit 1 -fi - -case "$1" in - debug) - echo "Not implemented yet. Starting in normal mode" - $0 start - ;; - start) - test -f $DAEMON || exit 0 - echo "Starting $NAME." - $DAEMON $CONF & - ;; - stop) - echo "Stopping $NAME." - killall $NAME & - ;; - restart|reload) - $0 stop - sleep 3 - $0 start - ;; - *) - echo "Usage: $0 {debug|start|stop|status|restart}" - exit 1 -esac diff --git a/examples/transport-configs/init-scripts/jabber-gg-transport b/examples/transport-configs/init-scripts/jabber-gg-transport deleted file mode 100755 index 269685d0d..000000000 --- a/examples/transport-configs/init-scripts/jabber-gg-transport +++ /dev/null @@ -1,47 +0,0 @@ -#!/bin/sh -######################################################### -# -# jabber-gg-transport -- script to start jabber-gg-transport. -# -######################################################### - -DAEMON=/usr/local/sbin/jggtrans -CONF=/etc/jabber/jabber-gg-transport.xml -NAME=jggtrans -HOME=/etc/jabber/ -USER=ejabberd - -######################################################### - -if [ "`/usr/bin/whoami`" != "$USER" ]; then - - echo "You need to be" $USER "user to run this script." - exit 1 -fi - -case "$1" in - debug) - test -f $DAEMON -a -f $CONF || exit 0 - echo "Starting $NAME in debugging mode." - $DAEMON -D -H $HOME -c $CONF & - ;; - start) - test -f $DAEMON -a -f $CONF || exit 0 - echo "Starting $NAME." - $DAEMON $CONF & - ;; - stop) - echo "Stopping $NAME." - killall $NAME & - rm /var/run/jabber/jabber-gg-transport.pid - ;; - - restart|reload) - $0 stop - sleep 3 - $0 start - ;; - *) - echo "Usage: $0 {debug|start|stop|restart}" - exit 1 -esac diff --git a/examples/transport-configs/init-scripts/jit b/examples/transport-configs/init-scripts/jit deleted file mode 100755 index 55e000ee8..000000000 --- a/examples/transport-configs/init-scripts/jit +++ /dev/null @@ -1,45 +0,0 @@ -#!/bin/sh -######################################################### -# -# jit -- script to start JIT. -# -######################################################### - -DAEMON=/usr/local/sbin/wpjabber-jit -CONF=/etc/jabber/jit.xml -NAME=wpjabber-jit -HOME=/etc/jabber/ -USER=ejabberd - -######################################################### - -if [ "`/usr/bin/whoami`" != "$USER" ]; then - - echo "You need to be" $USER "user to run this script." - exit 1 -fi - -case "$1" in - debug) - test -f $DAEMON -a -f $CONF || exit 0 - echo "Starting $NAME in debugging mode." - $DAEMON -D -H $HOME -c $CONF & - ;; - start) - test -f $DAEMON -a -f $CONF || exit 0 - echo "Starting $NAME." - $DAEMON -H $HOME -c $CONF & - ;; - stop) - echo "Stopping $NAME." - killall $NAME & - ;; - restart|reload) - $0 stop - sleep 3 - $0 start - ;; - *) - echo "Usage: $0 {debug|start|stop|restart}" - exit 1 -esac diff --git a/examples/transport-configs/init-scripts/msn-transport b/examples/transport-configs/init-scripts/msn-transport deleted file mode 100755 index 555ba2b0f..000000000 --- a/examples/transport-configs/init-scripts/msn-transport +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/sh -######################################################### -# -# msn-transport -- script to start MSN Transport. -# -######################################################### - -DAEMON=/usr/local/sbin/jabberd-msn-transport -CONF=/etc/jabber/msn-transport.xml -NAME=jabberd-msn-transport -HOME=/etc/jabber/ -USER=ejabberd - -######################################################### - -if [ "`/usr/bin/whoami`" != "$USER" ]; then - - echo "You need to be" $USER "user to run this script." - exit 1 -fi - -case "$1" in - strace) - test -f $DAEMON -a -f $CONF || exit 0 - echo "Starting $NAME in strace mode." - strace -o /opt/ejabberd/var/log/jabber/strace.log $DAEMON -H $HOME -c $CONF & - ;; - debug) - test -f $DAEMON -a -f $CONF || exit 0 - echo "Starting $NAME in debugging mode." - $DAEMON -D -H $HOME -c $CONF & - ;; - start) - test -f $DAEMON -a -f $CONF || exit 0 - echo "Starting $NAME." - $DAEMON -H $HOME -c $CONF & - ;; - stop) - echo "Stopping $NAME." - killall $NAME & - ;; - restart|reload) - $0 stop - sleep 3 - $0 start - ;; - *) - echo "Usage: $0 {debug|start|stop|restart}" - exit 1 -esac diff --git a/examples/transport-configs/init-scripts/yahoo-transport-2 b/examples/transport-configs/init-scripts/yahoo-transport-2 deleted file mode 100755 index fde78a913..000000000 --- a/examples/transport-configs/init-scripts/yahoo-transport-2 +++ /dev/null @@ -1,45 +0,0 @@ -#!/bin/sh -############################################################## -# -# yahoo-transport-2 -- script to start Yahoo-transport-2. -# -############################################################# - -DAEMON=/usr/local/sbin/jabberd-yahoo-transport-2 -CONF=/etc/jabber/yahoo-transport-2.xml -NAME=jabberd-yahoo-transport-2 -HOME=/etc/jabber/ -USER=ejabberd - -############################################################# - -if [ "`/usr/bin/whoami`" != "$USER" ]; then - - echo "You need to be" $USER "user to run this script." - exit 1 -fi - -case "$1" in - debug) - test -f $DAEMON -a -f $CONF || exit 0 - echo "Starting $NAME in debugging mode." - $DAEMON -D -H $HOME -c $CONF & - ;; - start) - test -f $DAEMON -a -f $CONF || exit 0 - echo "Starting $NAME." - $DAEMON -H $HOME -c $CONF & - ;; - stop) - echo "Stopping $NAME." - killall $NAME & - ;; - restart|reload) - $0 stop - sleep 3 - $0 start - ;; - *) - echo "Usage: $0 {debug|start|stop|restart}" - exit 1 -esac diff --git a/include/ejabberd.hrl b/include/bosh.hrl similarity index 50% rename from include/ejabberd.hrl rename to include/bosh.hrl index 6e21836ae..dd9f1b6a1 100644 --- a/include/ejabberd.hrl +++ b/include/bosh.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -18,34 +18,34 @@ %%% %%%---------------------------------------------------------------------- -%% This macro returns a string of the ejabberd version running, e.g. "2.3.4" -%% If the ejabberd application description isn't loaded, returns atom: undefined --define(VERSION, ejabberd_config:get_version()). +-define(CT_XML, + {<<"Content-Type">>, <<"text/xml; charset=utf-8">>}). --define(MYHOSTS, ejabberd_config:get_myhosts()). +-define(CT_PLAIN, + {<<"Content-Type">>, <<"text/plain">>}). --define(MYNAME, hd(ejabberd_config:get_myhosts())). +-define(CT_JSON, + {<<"Content-Type">>, <<"application/json">>}). --define(MYLANG, ejabberd_config:get_mylang()). +-define(AC_ALLOW_ORIGIN, + {<<"Access-Control-Allow-Origin">>, <<"*">>}). --define(MSGS_DIR, filename:join(["priv", "msgs"])). +-define(AC_ALLOW_METHODS, + {<<"Access-Control-Allow-Methods">>, + <<"GET, POST, OPTIONS">>}). --define(CONFIG_PATH, <<"ejabberd.cfg">>). +-define(AC_ALLOW_HEADERS, + {<<"Access-Control-Allow-Headers">>, + <<"Content-Type">>}). --define(LOG_PATH, <<"ejabberd.log">>). +-define(AC_MAX_AGE, + {<<"Access-Control-Max-Age">>, <<"86400">>}). --define(EJABBERD_URI, <<"http://www.process-one.net/en/ejabberd/">>). +-define(OPTIONS_HEADER, + [?CT_PLAIN, ?AC_ALLOW_ORIGIN, ?AC_ALLOW_METHODS, + ?AC_ALLOW_HEADERS, ?AC_MAX_AGE]). --define(S2STIMEOUT, 600000). +-define(HEADER(CType), + [CType, ?AC_ALLOW_ORIGIN, ?AC_ALLOW_HEADERS]). -%%-define(DBGFSM, true). - --record(scram, - {storedkey = <<"">>, - serverkey = <<"">>, - salt = <<"">>, - iterationcount = 0 :: integer()}). - --type scram() :: #scram{}. - --define(SCRAM_DEFAULT_ITERATION_COUNT, 4096). +-define(BOSH_CACHE, bosh_cache). diff --git a/include/ejabberd_auth.hrl b/include/ejabberd_auth.hrl new file mode 100644 index 000000000..bf7660d3f --- /dev/null +++ b/include/ejabberd_auth.hrl @@ -0,0 +1,22 @@ +%%%---------------------------------------------------------------------- +%%% +%%% ejabberd, Copyright (C) 2002-2025 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. +%%% +%%%---------------------------------------------------------------------- + +-record(passwd, {us = {<<"">>, <<"">>} :: {binary(), binary()} | {binary(), binary(), atom()} | '$1', + password = <<"">> :: binary() | scram() | '_'}). diff --git a/include/ejabberd_commands.hrl b/include/ejabberd_commands.hrl index 3ab15ca31..14d19d2e1 100644 --- a/include/ejabberd_commands.hrl +++ b/include/ejabberd_commands.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -19,56 +19,89 @@ %%%---------------------------------------------------------------------- -type aterm() :: {atom(), atype()}. --type atype() :: integer | string | binary | +-type atype() :: integer | string | binary | any | atom | {tuple, [aterm()]} | {list, aterm()}. -type rterm() :: {atom(), rtype()}. --type rtype() :: integer | string | atom | +-type rtype() :: integer | string | atom | any | {tuple, [rterm()]} | {list, rterm()} | rescode | restuple. +%% The 'any' and 'atom' argument types and 'any' result type +%% should only be used %% by commands with tag 'internal', +%% which are meant to be used only internally in ejabberd, +%% and not called using external frontends. + +%% The purpose of a command can either be: +%% - informative: its purpose is to obtain information +%% - modifier: its purpose is to produce some change in the server +%% +%% A modifier command should be designed just to produce its desired side-effect, +%% and its result term should just be success or failure: rescode or restuple. +%% +%% ejabberd_web_admin:make_command/2 considers that commands +%% with result type different than rescode or restuple +%% are commands that can be safely executed automatically +%% to get information and build the web page. + +-type oauth_scope() :: atom(). + +%% ejabberd_commands OAuth ReST ACL definition: +%% Two fields exist that are used to control access on a command from ReST API: +%% 1. Policy +%% If policy is: +%% - restricted: command is not exposed as OAuth Rest API. +%% - admin: Command is allowed for user that have Admin Rest command enabled by access rule: commands_admin_access +%% - user: Command might be called by any server user. +%% - open: Command can be called by anyone. +%% +%% Policy is just used to control who can call the command. A specific additional access rules can be performed, as +%% defined by access option. +%% Access option can be a list of: +%% - {Module, accessName, DefaultValue}: Reference and existing module access to limit who can use the command. +%% - AccessRule name: direct name of the access rule to check in config file. +%% TODO: Access option could be atom command (not a list). In the case, User performing the command, will be added as first parameter +%% to command, so that the command can perform additional check. + -record(ejabberd_commands, - {name :: atom(), + {name :: atom(), tags = [] :: [atom()] | '_' | '$2', desc = "" :: string() | '_' | '$3', longdesc = "" :: string() | '_', - module :: atom(), - function :: atom(), + version = 0 :: integer(), + note = "" :: string(), + weight = 1 :: integer(), + module :: atom() | '_', + function :: atom() | '_', args = [] :: [aterm()] | '_' | '$1' | '$2', - result = {res, rescode} :: rterm() | '_' | '$2'}). + policy = restricted :: open | restricted | admin | user, + %% access is: [accessRuleName] or [{Module, AccessOption, DefaultAccessRuleName}] + access = [] :: [{atom(),atom(),atom()}|atom()], + definer = unknown :: atom(), + result = {res, rescode} :: rterm() | '_' | '$2', + args_rename = [] :: [{atom(),atom()}], + args_desc = none :: none | [string()] | '_', + result_desc = none :: none | string() | '_', + args_example = none :: none | [any()] | '_', + result_example = none :: any()}). --type ejabberd_commands() :: #ejabberd_commands{name :: atom(), - tags :: [atom()], - desc :: string(), - longdesc :: string(), - module :: atom(), - function :: atom(), - args :: [aterm()], - result :: rterm()}. - -%% @type ejabberd_commands() = #ejabberd_commands{ -%% name = atom(), -%% tags = [atom()], -%% desc = string(), -%% longdesc = string(), -%% module = atom(), -%% function = atom(), -%% args = [aterm()], -%% result = rterm() -%% }. -%% desc: Description of the command -%% args: Describe the accepted arguments. -%% This way the function that calls the command can format the -%% arguments before calling. - -%% @type atype() = integer | string | {tuple, [aterm()]} | {list, aterm()}. -%% Allowed types for arguments are integer, string, tuple and list. - -%% @type rtype() = integer | string | atom | {tuple, [rterm()]} | {list, rterm()} | rescode | restuple. -%% A rtype is either an atom or a tuple with two elements. - -%% @type aterm() = {Name::atom(), Type::atype()}. -%% An argument term is a tuple with the term name and the term type. - -%% @type rterm() = {Name::atom(), Type::rtype()}. -%% A result term is a tuple with the term name and the term type. +-type ejabberd_commands() :: #ejabberd_commands{name :: atom(), + tags :: [atom()], + desc :: string(), + longdesc :: string(), + version :: integer(), + note :: string(), + weight :: integer(), + module :: atom(), + function :: atom(), + args :: [aterm()], + policy :: open | restricted | admin | user, + access :: [{atom(),atom(),atom()}|atom()], + definer :: atom(), + result :: rterm(), + args_rename :: [{atom(),atom()}], + args_desc :: none | [string()] | '_', + result_desc :: none | string() | '_', + args_example :: none | [any()] | '_', + result_example :: any() + }. diff --git a/include/ejabberd_ctl.hrl b/include/ejabberd_ctl.hrl index 8b56ad261..cad82da89 100644 --- a/include/ejabberd_ctl.hrl +++ b/include/ejabberd_ctl.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/include/ejabberd_http.hrl b/include/ejabberd_http.hrl index 931706342..9e1373ce6 100644 --- a/include/ejabberd_http.hrl +++ b/include/ejabberd_http.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -19,18 +19,37 @@ %%%---------------------------------------------------------------------- -record(request, - {method, % :: method(), + {method :: method(), path = [] :: [binary()], + raw_path :: binary(), q = [] :: [{binary() | nokey, binary()}], us = {<<>>, <<>>} :: {binary(), binary()}, - auth :: {binary(), binary()} | - {auth_jid, {binary(), binary()}, jlib:jid()}, + auth :: {binary(), binary()} | {oauth, binary(), []} | undefined | invalid, lang = <<"">> :: binary(), data = <<"">> :: binary(), ip :: {inet:ip_address(), inet:port_number()}, host = <<"">> :: binary(), port = 5280 :: inet:port_number(), - tp = http, % :: protocol(), opts = [] :: list(), - headers = [] :: [{atom() | binary(), binary()}]}). + tp = http :: protocol(), + headers = [] :: [{atom() | binary(), binary()}], + length = 0 :: non_neg_integer(), + sockmod :: gen_tcp | fast_tls, + socket :: inet:socket() | fast_tls:tls_socket()}). +-record(ws, + {socket :: inet:socket() | fast_tls:tls_socket(), + sockmod = gen_tcp :: gen_tcp | fast_tls, + ip :: {inet:ip_address(), inet:port_number()}, + host = <<"">> :: binary(), + port = 5280 :: inet:port_number(), + path = [] :: [binary()], + headers = [] :: [{atom() | binary(), binary()}], + local_path = [] :: [binary()], + q = [] :: [{binary() | nokey, binary()}], + buf :: binary(), + http_opts = [] :: list()}). + +-type method() :: 'GET' | 'HEAD' | 'DELETE' | 'OPTIONS' | 'PUT' | 'POST' | 'TRACE' | 'PATCH'. +-type protocol() :: http | https. +-type http_request() :: #request{}. diff --git a/include/adhoc.hrl b/include/ejabberd_oauth.hrl similarity index 54% rename from include/adhoc.hrl rename to include/ejabberd_oauth.hrl index f16aedd3b..4798d9070 100644 --- a/include/adhoc.hrl +++ b/include/ejabberd_oauth.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -18,27 +18,16 @@ %%% %%%---------------------------------------------------------------------- --record(adhoc_request, -{ - lang = <<"">> :: binary(), - node = <<"">> :: binary(), - sessionid = <<"">> :: binary(), - action = <<"">> :: binary(), - xdata = false :: false | xmlel(), - others = [] :: [xmlel()] -}). +-record(oauth_token, { + token = <<"">> :: binary() | '_', + us = {<<"">>, <<"">>} :: {binary(), binary()} | '_', + scope = [] :: [binary()] | '_', + expire :: integer() | '$1' | '_' + }). --record(adhoc_response, -{ - lang = <<"">> :: binary(), - node = <<"">> :: binary(), - sessionid = <<"">> :: binary(), - status :: atom(), - defaultaction = <<"">> :: binary(), - actions = [] :: [binary()], - notes = [] :: [{binary(), binary()}], - elements = [] :: [xmlel()] -}). - --type adhoc_request() :: #adhoc_request{}. --type adhoc_response() :: #adhoc_response{}. +-record(oauth_client, { + client_id = <<"">> :: binary() | '_', + client_name = <<"">> :: binary() | '_', + grant_type :: password | implicit | '_', + options :: [any()] | '_' + }). diff --git a/include/ejabberd_router.hrl b/include/ejabberd_router.hrl new file mode 100644 index 000000000..060ab79a1 --- /dev/null +++ b/include/ejabberd_router.hrl @@ -0,0 +1,8 @@ +-define(ROUTES_CACHE, routes_cache). + +-type local_hint() :: integer() | {apply, atom(), atom()}. + +-record(route, {domain :: binary(), + server_host :: binary(), + pid :: undefined | pid(), + local_hint :: local_hint() | undefined}). diff --git a/include/ejabberd_sm.hrl b/include/ejabberd_sm.hrl new file mode 100644 index 000000000..54a828e1a --- /dev/null +++ b/include/ejabberd_sm.hrl @@ -0,0 +1,36 @@ +%%%---------------------------------------------------------------------- +%%% +%%% ejabberd, Copyright (C) 2002-2025 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. +%%% +%%%---------------------------------------------------------------------- + +-ifndef(EJABBERD_SM_HRL). +-define(EJABBERD_SM_HRL, true). + +-define(SM_CACHE, sm_cache). + +-record(session, {sid, usr, us, priority, info = []}). +-record(session_counter, {vhost, count}). +-type sid() :: {erlang:timestamp(), pid()}. +-type ip() :: {inet:ip_address(), inet:port_number()} | undefined. +-type info() :: [{conn, atom()} | {ip, ip()} | {node, atom()} + | {oor, boolean()} | {auth_module, atom()} + | {num_stanzas_in, non_neg_integer()} + | {atom(), term()}]. +-type prio() :: undefined | integer(). + +-endif. diff --git a/include/ejabberd_sql.hrl b/include/ejabberd_sql.hrl new file mode 100644 index 000000000..d0ab55cba --- /dev/null +++ b/include/ejabberd_sql.hrl @@ -0,0 +1,75 @@ +%%%---------------------------------------------------------------------- +%%% +%%% ejabberd, Copyright (C) 2002-2025 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. +%%% +%%%---------------------------------------------------------------------- +-define(SQL_MARK, sql__mark_). +-define(SQL(SQL), ?SQL_MARK(SQL)). + +-define(SQL_UPSERT_MARK, sql_upsert__mark_). +-define(SQL_UPSERT(Host, Table, Fields), + ejabberd_sql:sql_query(Host, ?SQL_UPSERT_MARK(Table, Fields))). +-define(SQL_UPSERT_T(Table, Fields), + ejabberd_sql:sql_query_t(?SQL_UPSERT_MARK(Table, Fields))). + +-define(SQL_INSERT_MARK, sql_insert__mark_). +-define(SQL_INSERT(Table, Fields), ?SQL_INSERT_MARK(Table, Fields)). + +-ifdef(COMPILER_REPORTS_ONLY_LINES). +-record(sql_query, {hash :: binary(), + format_query :: fun(), + format_res :: fun(), + args :: fun(), + flags :: non_neg_integer(), + loc :: {module(), pos_integer()}}). +-else. +-record(sql_query, {hash :: binary(), + format_query :: fun(), + format_res :: fun(), + args :: fun(), + flags :: non_neg_integer(), + loc :: {module(), {pos_integer(), pos_integer()}}}). +-endif. + +-record(sql_escape, {string :: fun((binary()) -> binary()), + integer :: fun((integer()) -> binary()), + boolean :: fun((boolean()) -> binary()), + in_array_string :: fun((binary()) -> binary()), + like_escape :: fun(() -> binary())}). + + +-record(sql_index, {columns, + unique = false :: boolean(), + meta = #{}}). +-record(sql_column, {name :: binary(), + type, + default = false, + opts = []}). +-record(sql_table, {name :: binary(), + columns :: [#sql_column{}], + indices = [] :: [#sql_index{}], + post_create}). +-record(sql_schema, {version :: integer(), + tables :: [#sql_table{}], + update = []}). +-record(sql_references, {table :: binary(), + column :: binary()}). + +-record(sql_schema_info, + {db_type :: pgsql | mysql | sqlite, + db_version :: any(), + new_schema = true :: boolean()}). diff --git a/include/ejabberd_sql_pt.hrl b/include/ejabberd_sql_pt.hrl new file mode 100644 index 000000000..f89f5c969 --- /dev/null +++ b/include/ejabberd_sql_pt.hrl @@ -0,0 +1,21 @@ +%%%---------------------------------------------------------------------- +%%% +%%% ejabberd, Copyright (C) 2002-2025 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. +%%% +%%%---------------------------------------------------------------------- +-compile([{parse_transform, ejabberd_sql_pt}]). +-include("ejabberd_sql.hrl"). diff --git a/include/ejabberd_web_admin.hrl b/include/ejabberd_web_admin.hrl index 852e537b6..45e4beada 100644 --- a/include/ejabberd_web_admin.hrl +++ b/include/ejabberd_web_admin.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -37,14 +37,12 @@ -define(XAC(Name, Attrs, Text), ?XAE(Name, Attrs, [?C(Text)])). --define(T(Text), translate:translate(Lang, Text)). +-define(CT(Text), ?C((translate:translate(Lang, Text)))). --define(CT(Text), ?C((?T(Text)))). - --define(XCT(Name, Text), ?XC(Name, (?T(Text)))). +-define(XCT(Name, Text), ?XC(Name, (translate:translate(Lang, Text)))). -define(XACT(Name, Attrs, Text), - ?XAC(Name, Attrs, (?T(Text)))). + ?XAC(Name, Attrs, (translate:translate(Lang, Text)))). -define(LI(Els), ?XE(<<"li">>, Els)). @@ -53,7 +51,7 @@ -define(AC(URL, Text), ?A(URL, [?C(Text)])). --define(ACT(URL, Text), ?AC(URL, (?T(Text)))). +-define(ACT(URL, Text), ?AC(URL, (translate:translate(Lang, Text)))). -define(P, ?X(<<"p">>)). @@ -64,8 +62,21 @@ [{<<"type">>, Type}, {<<"name">>, Name}, {<<"value">>, Value}])). +-define(INPUTPH(Type, Name, Value, PlaceHolder), + ?XA(<<"input">>, + [{<<"type">>, Type}, {<<"name">>, Name}, + {<<"value">>, Value}, {<<"placeholder">>, PlaceHolder}])). + -define(INPUTT(Type, Name, Value), - ?INPUT(Type, Name, (?T(Value)))). + ?INPUT(Type, Name, (translate:translate(Lang, Value)))). + +-define(INPUTD(Type, Name, Value), + ?XA(<<"input">>, + [{<<"type">>, Type}, {<<"name">>, Name}, + {<<"class">>, <<"btn-danger">>}, {<<"value">>, Value}])). + +-define(INPUTTD(Type, Name, Value), + ?INPUTD(Type, Name, (translate:translate(Lang, Value)))). -define(INPUTS(Type, Name, Value, Size), ?XA(<<"input">>, @@ -73,7 +84,7 @@ {<<"value">>, Value}, {<<"size">>, Size}])). -define(INPUTST(Type, Name, Value, Size), - ?INPUT(Type, Name, (?T(Value)), Size)). + ?INPUT(Type, Name, (translate:translate(Lang, Value)), Size)). -define(ACLINPUT(Text), ?XE(<<"td">>, @@ -89,16 +100,27 @@ -define(XRES(Text), ?XAC(<<"p">>, [{<<"class">>, <<"result">>}], Text)). +-define(DIVRES(Elements), + ?XAE(<<"div">>, [{<<"class">>, <<"result">>}], Elements)). + %% Guide Link --define(XREST(Text), ?XRES((?T(Text)))). +-define(XREST(Text), ?XRES((translate:translate(Lang, Text)))). -define(GL(Ref, Title), ?XAE(<<"div">>, [{<<"class">>, <<"guidelink">>}], [?XAE(<<"a">>, - [{<<"href">>, <<"/admin/doc/guide.html#", Ref/binary>>}, + [{<<"href">>, <<"https://docs.ejabberd.im/", Ref/binary>>}, {<<"target">>, <<"_blank">>}], - [?C(<<"[Guide: ", Title/binary, "]">>)])])). + [?C(<<"docs: ", Title/binary>>)])])). %% h1 with a Guide Link --define(H1GL(Name, Ref, Title), - [?XC(<<"h1">>, Name), ?GL(Ref, Title)]). +-define(H1GLraw(Name, Ref, Title), + [?XC(<<"h1">>, Name), ?GL(Ref, Title), ?BR, ?BR]). +-define(H1GL(Name, RefConf, Title), + ?H1GLraw(Name, <<"admin/configuration/", RefConf/binary>>, Title)). + +-define(ANCHORL(Ref), + ?XAE(<<"div">>, [{<<"class">>, <<"anchorlink">>}], + [?XAE(<<"a">>, + [{<<"href">>, <<"#", Ref/binary>>}], + [?C(unicode:characters_to_binary("¶"))])])). diff --git a/include/eldap.hrl b/include/eldap.hrl index 6c30f5456..0b6dc97e5 100644 --- a/include/eldap.hrl +++ b/include/eldap.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -27,7 +27,7 @@ -record(eldap_search, {scope = wholeSubtree :: scope(), base = <<"">> :: binary(), - filter :: eldap:filter(), + filter :: eldap:filter() | undefined, limit = 0 :: non_neg_integer(), attributes = [] :: [binary()], types_only = false :: boolean(), @@ -44,6 +44,7 @@ attributes = [] :: [{binary(), [binary()]}]}). -type tlsopts() :: [{encrypt, tls | starttls | none} | + {tls_certfile, binary() | undefined} | {tls_cacertfile, binary() | undefined} | {tls_depth, non_neg_integer() | undefined} | {tls_verify, hard | soft | false}]. @@ -61,3 +62,18 @@ -type eldap_config() :: #eldap_config{}. -type eldap_search() :: #eldap_search{}. -type eldap_entry() :: #eldap_entry{}. + +-define(eldap_config(M, H), + #eldap_config{ + servers = M:ldap_servers(H), + backups = M:ldap_backups(H), + tls_options = [{encrypt, M:ldap_encrypt(H)}, + {tls_verify, M:ldap_tls_verify(H)}, + {tls_certfile, M:ldap_tls_certfile(H)}, + {tls_cacertfile, M:ldap_tls_cacertfile(H)}, + {tls_depth, M:ldap_tls_depth(H)}], + port = M:ldap_port(H), + dn = M:ldap_rootdn(H), + password = M:ldap_password(H), + base = M:ldap_base(H), + deref_aliases = M:ldap_deref_aliases(H)}). diff --git a/include/http_bind.hrl b/include/http_bind.hrl index 446e47105..ab1294e7d 100644 --- a/include/http_bind.hrl +++ b/include/http_bind.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -38,9 +38,12 @@ -define(AC_MAX_AGE, {<<"Access-Control-Max-Age">>, <<"86400">>}). +-define(NO_CACHE, + {<<"Cache-Control">>, <<"max-age=0, no-cache, no-store">>}). + -define(OPTIONS_HEADER, [?CT_PLAIN, ?AC_ALLOW_ORIGIN, ?AC_ALLOW_METHODS, ?AC_ALLOW_HEADERS, ?AC_MAX_AGE]). -define(HEADER, - [?CT_XML, ?AC_ALLOW_ORIGIN, ?AC_ALLOW_HEADERS]). + [?CT_XML, ?AC_ALLOW_ORIGIN, ?AC_ALLOW_HEADERS, ?NO_CACHE]). diff --git a/include/jlib.hrl b/include/jlib.hrl deleted file mode 100644 index e4c7ca641..000000000 --- a/include/jlib.hrl +++ /dev/null @@ -1,501 +0,0 @@ -%%%---------------------------------------------------------------------- -%%% -%%% ejabberd, Copyright (C) 2002-2015 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. -%%% -%%%---------------------------------------------------------------------- - --include("ns.hrl"). --include_lib("p1_xml/include/xml.hrl"). - --define(STANZA_ERROR(Code, Type, Condition), - #xmlel{name = <<"error">>, - attrs = [{<<"code">>, Code}, {<<"type">>, Type}], - children = - [#xmlel{name = Condition, - attrs = [{<<"xmlns">>, ?NS_STANZAS}], - children = []}]}). - --define(ERR_BAD_FORMAT, - ?STANZA_ERROR(<<"406">>, <<"modify">>, - <<"bad-format">>)). - --define(ERR_BAD_REQUEST, - ?STANZA_ERROR(<<"400">>, <<"modify">>, - <<"bad-request">>)). - --define(ERR_CONFLICT, - ?STANZA_ERROR(<<"409">>, <<"cancel">>, <<"conflict">>)). - --define(ERR_FEATURE_NOT_IMPLEMENTED, - ?STANZA_ERROR(<<"501">>, <<"cancel">>, - <<"feature-not-implemented">>)). - --define(ERR_FORBIDDEN, - ?STANZA_ERROR(<<"403">>, <<"auth">>, <<"forbidden">>)). - --define(ERR_GONE, - ?STANZA_ERROR(<<"302">>, <<"modify">>, <<"gone">>)). - --define(ERR_INTERNAL_SERVER_ERROR, - ?STANZA_ERROR(<<"500">>, <<"wait">>, - <<"internal-server-error">>)). - --define(ERR_ITEM_NOT_FOUND, - ?STANZA_ERROR(<<"404">>, <<"cancel">>, - <<"item-not-found">>)). - --define(ERR_JID_MALFORMED, - ?STANZA_ERROR(<<"400">>, <<"modify">>, - <<"jid-malformed">>)). - --define(ERR_NOT_ACCEPTABLE, - ?STANZA_ERROR(<<"406">>, <<"modify">>, - <<"not-acceptable">>)). - --define(ERR_NOT_ALLOWED, - ?STANZA_ERROR(<<"405">>, <<"cancel">>, - <<"not-allowed">>)). - --define(ERR_NOT_AUTHORIZED, - ?STANZA_ERROR(<<"401">>, <<"auth">>, - <<"not-authorized">>)). - --define(ERR_PAYMENT_REQUIRED, - ?STANZA_ERROR(<<"402">>, <<"auth">>, - <<"payment-required">>)). - --define(ERR_RECIPIENT_UNAVAILABLE, - ?STANZA_ERROR(<<"404">>, <<"wait">>, - <<"recipient-unavailable">>)). - --define(ERR_REDIRECT, - ?STANZA_ERROR(<<"302">>, <<"modify">>, <<"redirect">>)). - --define(ERR_REGISTRATION_REQUIRED, - ?STANZA_ERROR(<<"407">>, <<"auth">>, - <<"registration-required">>)). - --define(ERR_REMOTE_SERVER_NOT_FOUND, - ?STANZA_ERROR(<<"404">>, <<"cancel">>, - <<"remote-server-not-found">>)). - --define(ERR_REMOTE_SERVER_TIMEOUT, - ?STANZA_ERROR(<<"504">>, <<"wait">>, - <<"remote-server-timeout">>)). - --define(ERR_RESOURCE_CONSTRAINT, - ?STANZA_ERROR(<<"500">>, <<"wait">>, - <<"resource-constraint">>)). - --define(ERR_SERVICE_UNAVAILABLE, - ?STANZA_ERROR(<<"503">>, <<"cancel">>, - <<"service-unavailable">>)). - --define(ERR_SUBSCRIPTION_REQUIRED, - ?STANZA_ERROR(<<"407">>, <<"auth">>, - <<"subscription-required">>)). - --define(ERR_UNEXPECTED_REQUEST, - ?STANZA_ERROR(<<"400">>, <<"wait">>, - <<"unexpected-request">>)). - --define(ERR_UNEXPECTED_REQUEST_CANCEL, - ?STANZA_ERROR(<<"401">>, <<"cancel">>, - <<"unexpected-request">>)). - -%-define(ERR_, -% ?STANZA_ERROR("", "", "")). - --define(STANZA_ERRORT(Code, Type, Condition, Lang, - Text), - #xmlel{name = <<"error">>, - attrs = [{<<"code">>, Code}, {<<"type">>, Type}], - children = - [#xmlel{name = Condition, - attrs = [{<<"xmlns">>, ?NS_STANZAS}], children = []}, - #xmlel{name = <<"text">>, - attrs = [{<<"xmlns">>, ?NS_STANZAS}], - children = - [{xmlcdata, - translate:translate(Lang, Text)}]}]}). - --define(ERRT_BAD_FORMAT(Lang, Text), - ?STANZA_ERRORT(<<"406">>, <<"modify">>, - <<"bad-format">>, Lang, Text)). - --define(ERRT_BAD_REQUEST(Lang, Text), - ?STANZA_ERRORT(<<"400">>, <<"modify">>, - <<"bad-request">>, Lang, Text)). - --define(ERRT_CONFLICT(Lang, Text), - ?STANZA_ERRORT(<<"409">>, <<"cancel">>, <<"conflict">>, - Lang, Text)). - --define(ERRT_FEATURE_NOT_IMPLEMENTED(Lang, Text), - ?STANZA_ERRORT(<<"501">>, <<"cancel">>, - <<"feature-not-implemented">>, Lang, Text)). - --define(ERRT_FORBIDDEN(Lang, Text), - ?STANZA_ERRORT(<<"403">>, <<"auth">>, <<"forbidden">>, - Lang, Text)). - --define(ERRT_GONE(Lang, Text), - ?STANZA_ERRORT(<<"302">>, <<"modify">>, <<"gone">>, - Lang, Text)). - --define(ERRT_INTERNAL_SERVER_ERROR(Lang, Text), - ?STANZA_ERRORT(<<"500">>, <<"wait">>, - <<"internal-server-error">>, Lang, Text)). - --define(ERRT_ITEM_NOT_FOUND(Lang, Text), - ?STANZA_ERRORT(<<"404">>, <<"cancel">>, - <<"item-not-found">>, Lang, Text)). - --define(ERRT_JID_MALFORMED(Lang, Text), - ?STANZA_ERRORT(<<"400">>, <<"modify">>, - <<"jid-malformed">>, Lang, Text)). - --define(ERRT_NOT_ACCEPTABLE(Lang, Text), - ?STANZA_ERRORT(<<"406">>, <<"modify">>, - <<"not-acceptable">>, Lang, Text)). - --define(ERRT_NOT_ALLOWED(Lang, Text), - ?STANZA_ERRORT(<<"405">>, <<"cancel">>, - <<"not-allowed">>, Lang, Text)). - --define(ERRT_NOT_AUTHORIZED(Lang, Text), - ?STANZA_ERRORT(<<"401">>, <<"auth">>, - <<"not-authorized">>, Lang, Text)). - --define(ERRT_PAYMENT_REQUIRED(Lang, Text), - ?STANZA_ERRORT(<<"402">>, <<"auth">>, - <<"payment-required">>, Lang, Text)). - --define(ERRT_RECIPIENT_UNAVAILABLE(Lang, Text), - ?STANZA_ERRORT(<<"404">>, <<"wait">>, - <<"recipient-unavailable">>, Lang, Text)). - --define(ERRT_REDIRECT(Lang, Text), - ?STANZA_ERRORT(<<"302">>, <<"modify">>, <<"redirect">>, - Lang, Text)). - --define(ERRT_REGISTRATION_REQUIRED(Lang, Text), - ?STANZA_ERRORT(<<"407">>, <<"auth">>, - <<"registration-required">>, Lang, Text)). - --define(ERRT_REMOTE_SERVER_NOT_FOUND(Lang, Text), - ?STANZA_ERRORT(<<"404">>, <<"cancel">>, - <<"remote-server-not-found">>, Lang, Text)). - --define(ERRT_REMOTE_SERVER_TIMEOUT(Lang, Text), - ?STANZA_ERRORT(<<"504">>, <<"wait">>, - <<"remote-server-timeout">>, Lang, Text)). - --define(ERRT_RESOURCE_CONSTRAINT(Lang, Text), - ?STANZA_ERRORT(<<"500">>, <<"wait">>, - <<"resource-constraint">>, Lang, Text)). - --define(ERRT_SERVICE_UNAVAILABLE(Lang, Text), - ?STANZA_ERRORT(<<"503">>, <<"cancel">>, - <<"service-unavailable">>, Lang, Text)). - --define(ERRT_SUBSCRIPTION_REQUIRED(Lang, Text), - ?STANZA_ERRORT(<<"407">>, <<"auth">>, - <<"subscription-required">>, Lang, Text)). - --define(ERRT_UNEXPECTED_REQUEST(Lang, Text), - ?STANZA_ERRORT(<<"400">>, <<"wait">>, - <<"unexpected-request">>, Lang, Text)). - --define(ERR_AUTH_NO_RESOURCE_PROVIDED(Lang), - ?ERRT_NOT_ACCEPTABLE(Lang, <<"No resource provided">>)). - --define(ERR_AUTH_BAD_RESOURCE_FORMAT(Lang), - ?ERRT_NOT_ACCEPTABLE(Lang, - <<"Illegal resource format">>)). - --define(ERR_AUTH_RESOURCE_CONFLICT(Lang), - ?ERRT_CONFLICT(Lang, <<"Resource conflict">>)). - --define(STREAM_ERROR(Condition, Cdata), - #xmlel{name = <<"stream:error">>, attrs = [], - children = - [#xmlel{name = Condition, - attrs = [{<<"xmlns">>, ?NS_STREAMS}], - children = [{xmlcdata, Cdata}]}]}). - --define(SERR_BAD_FORMAT, - ?STREAM_ERROR(<<"bad-format">>, <<"">>)). - --define(SERR_BAD_NAMESPACE_PREFIX, - ?STREAM_ERROR(<<"bad-namespace-prefix">>, <<"">>)). - --define(SERR_CONFLICT, - ?STREAM_ERROR(<<"conflict">>, <<"">>)). - --define(SERR_CONNECTION_TIMEOUT, - ?STREAM_ERROR(<<"connection-timeout">>, <<"">>)). - --define(SERR_HOST_GONE, - ?STREAM_ERROR(<<"host-gone">>, <<"">>)). - --define(SERR_HOST_UNKNOWN, - ?STREAM_ERROR(<<"host-unknown">>, <<"">>)). - --define(SERR_IMPROPER_ADDRESSING, - ?STREAM_ERROR(<<"improper-addressing">>, <<"">>)). - --define(SERR_INTERNAL_SERVER_ERROR, - ?STREAM_ERROR(<<"internal-server-error">>, <<"">>)). - --define(SERR_INVALID_FROM, - ?STREAM_ERROR(<<"invalid-from">>, <<"">>)). - --define(SERR_INVALID_ID, - ?STREAM_ERROR(<<"invalid-id">>, <<"">>)). - --define(SERR_INVALID_NAMESPACE, - ?STREAM_ERROR(<<"invalid-namespace">>, <<"">>)). - --define(SERR_INVALID_XML, - ?STREAM_ERROR(<<"invalid-xml">>, <<"">>)). - --define(SERR_NOT_AUTHORIZED, - ?STREAM_ERROR(<<"not-authorized">>, <<"">>)). - --define(SERR_POLICY_VIOLATION, - ?STREAM_ERROR(<<"policy-violation">>, <<"">>)). - --define(SERR_REMOTE_CONNECTION_FAILED, - ?STREAM_ERROR(<<"remote-connection-failed">>, <<"">>)). - --define(SERR_RESOURSE_CONSTRAINT, - ?STREAM_ERROR(<<"resource-constraint">>, <<"">>)). - --define(SERR_RESTRICTED_XML, - ?STREAM_ERROR(<<"restricted-xml">>, <<"">>)). - --define(SERR_SEE_OTHER_HOST(Host), - ?STREAM_ERROR(<<"see-other-host">>, Host)). - --define(SERR_SYSTEM_SHUTDOWN, - ?STREAM_ERROR(<<"system-shutdown">>, <<"">>)). - --define(SERR_UNSUPPORTED_ENCODING, - ?STREAM_ERROR(<<"unsupported-encoding">>, <<"">>)). - --define(SERR_UNSUPPORTED_STANZA_TYPE, - ?STREAM_ERROR(<<"unsupported-stanza-type">>, <<"">>)). - --define(SERR_UNSUPPORTED_VERSION, - ?STREAM_ERROR(<<"unsupported-version">>, <<"">>)). - --define(SERR_XML_NOT_WELL_FORMED, - ?STREAM_ERROR(<<"xml-not-well-formed">>, <<"">>)). - -%-define(SERR_, -% ?STREAM_ERROR("", "")). - --define(STREAM_ERRORT(Condition, Cdata, Lang, Text), - #xmlel{name = <<"stream:error">>, attrs = [], - children = - [#xmlel{name = Condition, - attrs = [{<<"xmlns">>, ?NS_STREAMS}], - children = [{xmlcdata, Cdata}]}, - #xmlel{name = <<"text">>, - attrs = - [{<<"xml:lang">>, Lang}, - {<<"xmlns">>, ?NS_STREAMS}], - children = - [{xmlcdata, - translate:translate(Lang, Text)}]}]}). - --define(SERRT_BAD_FORMAT(Lang, Text), - ?STREAM_ERRORT(<<"bad-format">>, <<"">>, Lang, Text)). - --define(SERRT_BAD_NAMESPACE_PREFIX(Lang, Text), - ?STREAM_ERRORT(<<"bad-namespace-prefix">>, <<"">>, Lang, - Text)). - --define(SERRT_CONFLICT(Lang, Text), - ?STREAM_ERRORT(<<"conflict">>, <<"">>, Lang, Text)). - --define(SERRT_CONNECTION_TIMEOUT(Lang, Text), - ?STREAM_ERRORT(<<"connection-timeout">>, <<"">>, Lang, - Text)). - --define(SERRT_HOST_GONE(Lang, Text), - ?STREAM_ERRORT(<<"host-gone">>, <<"">>, Lang, Text)). - --define(SERRT_HOST_UNKNOWN(Lang, Text), - ?STREAM_ERRORT(<<"host-unknown">>, <<"">>, Lang, Text)). - --define(SERRT_IMPROPER_ADDRESSING(Lang, Text), - ?STREAM_ERRORT(<<"improper-addressing">>, <<"">>, Lang, - Text)). - --define(SERRT_INTERNAL_SERVER_ERROR(Lang, Text), - ?STREAM_ERRORT(<<"internal-server-error">>, <<"">>, - Lang, Text)). - --define(SERRT_INVALID_FROM(Lang, Text), - ?STREAM_ERRORT(<<"invalid-from">>, <<"">>, Lang, Text)). - --define(SERRT_INVALID_ID(Lang, Text), - ?STREAM_ERRORT(<<"invalid-id">>, <<"">>, Lang, Text)). - --define(SERRT_INVALID_NAMESPACE(Lang, Text), - ?STREAM_ERRORT(<<"invalid-namespace">>, <<"">>, Lang, - Text)). - --define(SERRT_INVALID_XML(Lang, Text), - ?STREAM_ERRORT(<<"invalid-xml">>, <<"">>, Lang, Text)). - --define(SERRT_NOT_AUTHORIZED(Lang, Text), - ?STREAM_ERRORT(<<"not-authorized">>, <<"">>, Lang, - Text)). - --define(SERRT_POLICY_VIOLATION(Lang, Text), - ?STREAM_ERRORT(<<"policy-violation">>, <<"">>, Lang, - Text)). - --define(SERRT_REMOTE_CONNECTION_FAILED(Lang, Text), - ?STREAM_ERRORT(<<"remote-connection-failed">>, <<"">>, - Lang, Text)). - --define(SERRT_RESOURSE_CONSTRAINT(Lang, Text), - ?STREAM_ERRORT(<<"resource-constraint">>, <<"">>, Lang, - Text)). - --define(SERRT_RESTRICTED_XML(Lang, Text), - ?STREAM_ERRORT(<<"restricted-xml">>, <<"">>, Lang, - Text)). - --define(SERRT_SEE_OTHER_HOST(Host, Lang, Text), - ?STREAM_ERRORT(<<"see-other-host">>, Host, Lang, Text)). - --define(SERRT_SYSTEM_SHUTDOWN(Lang, Text), - ?STREAM_ERRORT(<<"system-shutdown">>, <<"">>, Lang, - Text)). - --define(SERRT_UNSUPPORTED_ENCODING(Lang, Text), - ?STREAM_ERRORT(<<"unsupported-encoding">>, <<"">>, Lang, - Text)). - --define(SERRT_UNSUPPORTED_STANZA_TYPE(Lang, Text), - ?STREAM_ERRORT(<<"unsupported-stanza-type">>, <<"">>, - Lang, Text)). - --define(SERRT_UNSUPPORTED_VERSION(Lang, Text), - ?STREAM_ERRORT(<<"unsupported-version">>, <<"">>, Lang, - Text)). - --define(SERRT_XML_NOT_WELL_FORMED(Lang, Text), - ?STREAM_ERRORT(<<"xml-not-well-formed">>, <<"">>, Lang, - Text)). - --record(jid, {user = <<"">> :: binary(), - server = <<"">> :: binary(), - resource = <<"">> :: binary(), - luser = <<"">> :: binary(), - lserver = <<"">> :: binary(), - lresource = <<"">> :: binary()}). - --type(jid() :: #jid{}). - --type(ljid() :: {binary(), binary(), binary()}). - --record(iq, {id = <<"">> :: binary(), - type = get :: get | set | result | error, - xmlns = <<"">> :: binary(), - lang = <<"">> :: binary(), - sub_el = #xmlel{} :: xmlel() | [xmlel()]}). - --type(iq_get() - :: #iq{ - id :: binary(), - type :: get, - xmlns :: binary(), - lang :: binary(), - sub_el :: xmlel() - } -). - --type(iq_set() - :: #iq{ - id :: binary(), - type :: set, - xmlns :: binary(), - lang :: binary(), - sub_el :: xmlel() - } -). - --type iq_request() :: iq_get() | iq_set(). - --type(iq_result() - :: #iq{ - id :: binary(), - type :: result, - xmlns :: binary(), - lang :: binary(), - sub_el :: [xmlel()] - } -). - --type(iq_error() - :: #iq{ - id :: binary(), - type :: error, - xmlns :: binary(), - lang :: binary(), - sub_el :: [xmlel()] - } -). - --type iq_reply() :: iq_result() | iq_error() . - --type(iq() :: iq_request() | iq_reply()). - --record(rsm_in, {max :: integer(), - direction :: before | aft, - id :: binary(), - index :: integer()}). - --record(rsm_out, {count :: integer(), - index :: integer(), - first :: binary(), - last :: binary()}). - --type(rsm_in() :: #rsm_in{}). - --type(rsm_out() :: #rsm_out{}). - --type broadcast() :: {broadcast, broadcast_data()}. - --type broadcast_data() :: - {rebind, pid(), binary()} | %% ejabberd_c2s - {item, ljid(), mod_roster:subscription()} | %% mod_roster/mod_shared_roster - {exit, binary()} | %% mod_roster/mod_shared_roster - {privacy_list, mod_privacy:userlist(), binary()} | %% mod_privacy - {blocking, unblock_all | {block | unblock, [ljid()]}}. %% mod_blocking - --record(xmlelement, {name = "" :: string(), - attrs = [] :: [{string(), string()}], - children = [] :: [{xmlcdata, iodata()} | xmlelement()]}). - --type xmlelement() :: #xmlelement{}. diff --git a/include/logger.hrl b/include/logger.hrl index b8fdc31f9..e41ab73dd 100644 --- a/include/logger.hrl +++ b/include/logger.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -23,34 +23,70 @@ -compile([{parse_transform, lager_transform}]). -define(DEBUG(Format, Args), - lager:debug(Format, Args)). + begin lager:debug(Format, Args), ok end). -define(INFO_MSG(Format, Args), - lager:info(Format, Args)). + begin lager:info(Format, Args), ok end). -define(WARNING_MSG(Format, Args), - lager:warning(Format, Args)). + begin lager:warning(Format, Args), ok end). -define(ERROR_MSG(Format, Args), - lager:error(Format, Args)). + begin lager:error(Format, Args), ok end). -define(CRITICAL_MSG(Format, Args), - lager:critical(Format, Args)). - + begin lager:critical(Format, Args), ok end). -else. +-include_lib("kernel/include/logger.hrl"). + +-define(CLEAD, "\e[1"). % bold +-define(CMID, "\e[0"). % normal +-define(CCLEAN, "\e[0m"). % clean + +-define(CDEFAULT, ";49;95m"). % light magenta +-define(CDEBUG, ";49;90m"). % dark gray +-define(CINFO, ";49;92m"). % green +-define(CWARNING, ";49;93m"). % light yellow +-define(CERROR, ";49;91m"). % light magenta +-define(CCRITICAL,";49;31m"). % light red -define(DEBUG(Format, Args), - p1_logger:debug_msg(?MODULE, ?LINE, Format, Args)). + begin ?LOG_DEBUG(Format, Args, + #{clevel => ?CLEAD ++ ?CDEBUG, + ctext => ?CMID ++ ?CDEBUG}), + ok end). -define(INFO_MSG(Format, Args), - p1_logger:info_msg(?MODULE, ?LINE, Format, Args)). + begin ?LOG_INFO(Format, Args, + #{clevel => ?CLEAD ++ ?CINFO, + ctext => ?CCLEAN}), + ok end). -define(WARNING_MSG(Format, Args), - p1_logger:warning_msg(?MODULE, ?LINE, Format, Args)). + begin ?LOG_WARNING(Format, Args, + #{clevel => ?CLEAD ++ ?CWARNING, + ctext => ?CMID ++ ?CWARNING}), + ok end). -define(ERROR_MSG(Format, Args), - p1_logger:error_msg(?MODULE, ?LINE, Format, Args)). + begin ?LOG_ERROR(Format, Args, + #{clevel => ?CLEAD ++ ?CERROR, + ctext => ?CMID ++ ?CERROR}), + ok end). -define(CRITICAL_MSG(Format, Args), - p1_logger:critical_msg(?MODULE, ?LINE, Format, Args)). + begin ?LOG_CRITICAL(Format, Args, + #{clevel => ?CLEAD++ ?CCRITICAL, + ctext => ?CMID ++ ?CCRITICAL}), + ok end). -endif. + +%% Use only when trying to troubleshoot test problem with ExUnit +-define(EXUNIT_LOG(Format, Args), + case lists:keyfind(logger, 1, application:loaded_applications()) of + false -> ok; + _ -> 'Elixir.Logger':bare_log(error, io_lib:format(Format, Args), [?MODULE]) + end). + +%% Uncomment if you want to debug p1_fsm/gen_fsm +%%-define(DBGFSM, true). diff --git a/include/mod_announce.hrl b/include/mod_announce.hrl new file mode 100644 index 000000000..77badf90e --- /dev/null +++ b/include/mod_announce.hrl @@ -0,0 +1,25 @@ +%%%---------------------------------------------------------------------- +%%% +%%% ejabberd, Copyright (C) 2002-2025 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. +%%% +%%%---------------------------------------------------------------------- + +-record(motd, {server = <<"">> :: binary(), + packet = #xmlel{} :: xmlel()}). + +-record(motd_users, {us = {<<"">>, <<"">>} :: {binary(), binary()} | '$1', + dummy = [] :: [] | '_'}). diff --git a/src/randoms.erl b/include/mod_antispam.hrl similarity index 53% rename from src/randoms.erl rename to include/mod_antispam.hrl index 950f29fc3..c30f24620 100644 --- a/src/randoms.erl +++ b/include/mod_antispam.hrl @@ -1,11 +1,6 @@ %%%---------------------------------------------------------------------- -%%% File : randoms.erl -%%% Author : Alexey Shchepin -%%% Purpose : Random generation number wrapper -%%% Created : 13 Dec 2002 by Alexey Shchepin %%% -%%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -23,29 +18,19 @@ %%% %%%---------------------------------------------------------------------- --module(randoms). +-define(MODULE_ANTISPAM, mod_antispam). --author('alexey@process-one.net'). +-type url() :: binary(). +-type filename() :: binary() | none | false. +-type jid_set() :: sets:set(ljid()). +-type url_set() :: sets:set(url()). --export([get_string/0]). +-define(DEFAULT_RTBL_DOMAINS_NODE, <<"spam_source_domains">>). --export([start/0, init/0]). +-record(rtbl_service, + {host = none :: binary() | none, + node = ?DEFAULT_RTBL_DOMAINS_NODE :: binary(), + subscribed = false :: boolean(), + retry_timer = undefined :: reference() | undefined}). -start() -> - register(random_generator, spawn(randoms, init, [])). - -init() -> - {A1, A2, A3} = now(), random:seed(A1, A2, A3), loop(). - -loop() -> - receive - {From, get_random, N} -> - From ! {random, random:uniform(N)}, loop(); - _ -> loop() - end. - -get_string() -> - random_generator ! {self(), get_random, 65536 * 65536}, - receive - {random, R} -> jlib:integer_to_binary(R) - end. +-type rtbl_service() :: #rtbl_service{}. diff --git a/include/mod_caps.hrl b/include/mod_caps.hrl new file mode 100644 index 000000000..ee1bbe44e --- /dev/null +++ b/include/mod_caps.hrl @@ -0,0 +1,24 @@ +%%%---------------------------------------------------------------------- +%%% +%%% ejabberd, Copyright (C) 2002-2025 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. +%%% +%%%---------------------------------------------------------------------- + +-record(caps_features, + {node_pair = {<<"">>, <<"">>} :: {binary(), binary()}, + features = [] :: [binary()] | pos_integer() + }). diff --git a/include/mod_last.hrl b/include/mod_last.hrl new file mode 100644 index 000000000..b1c13621a --- /dev/null +++ b/include/mod_last.hrl @@ -0,0 +1,23 @@ +%%%---------------------------------------------------------------------- +%%% +%%% ejabberd, Copyright (C) 2002-2025 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. +%%% +%%%---------------------------------------------------------------------- + +-record(last_activity, {us = {<<"">>, <<"">>} :: {binary(), binary()}, + timestamp = 0 :: non_neg_integer(), + status = <<"">> :: binary()}). diff --git a/include/mod_mam.hrl b/include/mod_mam.hrl new file mode 100644 index 000000000..77ea54a5e --- /dev/null +++ b/include/mod_mam.hrl @@ -0,0 +1,36 @@ +%%%---------------------------------------------------------------------- +%%% +%%% ejabberd, Copyright (C) 2002-2025 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. +%%% +%%%---------------------------------------------------------------------- + +-record(archive_msg, + {us = {<<"">>, <<"">>} :: {binary(), binary()}, + id = <<>> :: binary(), + timestamp = erlang:timestamp() :: erlang:timestamp(), + peer = {<<"">>, <<"">>, <<"">>} :: ljid() | undefined, + bare_peer = {<<"">>, <<"">>, <<"">>} :: ljid(), + packet = #xmlel{} :: xmlel() | message(), + nick = <<"">> :: binary(), + type = chat :: chat | groupchat, + origin_id = <<"">> :: binary()}). + +-record(archive_prefs, + {us = {<<"">>, <<"">>} :: {binary(), binary()}, + default = never :: never | always | roster, + always = [] :: [ljid()], + never = [] :: [ljid()]}). diff --git a/include/mod_matrix_gw.hrl b/include/mod_matrix_gw.hrl new file mode 100644 index 000000000..cdb272e8e --- /dev/null +++ b/include/mod_matrix_gw.hrl @@ -0,0 +1,36 @@ +%%%---------------------------------------------------------------------- +%%% +%%% ejabberd, Copyright (C) 2002-2025 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. +%%% +%%%---------------------------------------------------------------------- + +-record(room_version, + {id :: binary(), + %% use the same field names as in Synapse + enforce_key_validity :: boolean(), + special_case_aliases_auth :: boolean(), + strict_canonicaljson :: boolean(), + limit_notifications_power_levels :: boolean(), + knock_join_rule :: boolean(), + restricted_join_rule :: boolean(), + restricted_join_rule_fix :: boolean(), + knock_restricted_join_rule :: boolean(), + enforce_int_power_levels :: boolean(), + implicit_room_creator :: boolean(), + updated_redaction_rules :: boolean(), + hydra :: boolean() + }). diff --git a/include/mod_muc.hrl b/include/mod_muc.hrl new file mode 100644 index 000000000..f801b29e1 --- /dev/null +++ b/include/mod_muc.hrl @@ -0,0 +1,36 @@ +%%%---------------------------------------------------------------------- +%%% +%%% ejabberd, Copyright (C) 2002-2025 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. +%%% +%%%---------------------------------------------------------------------- + +-record(muc_room, {name_host = {<<"">>, <<"">>} :: {binary(), binary()} | + {'_', binary()}, + opts = [] :: list() | '_'}). + +-record(muc_registered, + {us_host = {{<<"">>, <<"">>}, <<"">>} :: {{binary(), binary()}, binary()} | '$1', + nick = <<"">> :: binary()}). + +-record(muc_online_room, + {name_host :: {binary(), binary()} | '$1' | {'_', binary()} | '_', + pid :: pid() | '$2' | '_' | '$1'}). + +-record(muc_online_users, {us :: {binary(), binary()}, + resource :: binary() | '_', + room :: binary() | '_' | '$1', + host :: binary() | '_' | '$2'}). diff --git a/include/mod_muc_room.hrl b/include/mod_muc_room.hrl index 9220da41a..5f81fe026 100644 --- a/include/mod_muc_room.hrl +++ b/include/mod_muc_room.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -22,16 +22,15 @@ -define(SETS, gb_sets). --define(DICT, dict). - -record(lqueue, { - queue :: queue(), - len :: integer(), - max :: integer() + queue = p1_queue:new() :: p1_queue:queue(lqueue_elem()), + max = 0 :: integer() }). -type lqueue() :: #lqueue{}. +-type lqueue_elem() :: {binary(), message(), boolean(), + erlang:timestamp(), non_neg_integer()}. -record(config, { @@ -39,49 +38,74 @@ description = <<"">> :: binary(), allow_change_subj = true :: boolean(), allow_query_users = true :: boolean(), - allow_private_messages = true :: boolean(), + allowpm = anyone :: anyone | participants | moderators | none, allow_private_messages_from_visitors = anyone :: anyone | moderators | nobody , allow_visitor_status = true :: boolean(), allow_visitor_nickchange = true :: boolean(), public = true :: boolean(), public_list = true :: boolean(), - persistent = false :: boolean(), + persistent = false :: boolean() | {destroying, boolean()}, moderated = true :: boolean(), captcha_protected = false :: boolean(), members_by_default = true :: boolean(), members_only = false :: boolean(), allow_user_invites = false :: boolean(), + allow_subscription = false :: boolean(), password_protected = false :: boolean(), password = <<"">> :: binary(), anonymous = true :: boolean(), + presence_broadcast = [moderator, participant, visitor] :: + [moderator | participant | visitor], allow_voice_requests = true :: boolean(), voice_request_min_interval = 1800 :: non_neg_integer(), max_users = ?MAX_USERS_DEFAULT :: non_neg_integer() | none, logging = false :: boolean(), - vcard = <<"">> :: boolean(), - captcha_whitelist = (?SETS):empty() :: gb_set() + vcard = <<"">> :: binary(), + vcard_xupdate = undefined :: undefined | external | binary(), + captcha_whitelist = (?SETS):empty() :: gb_sets:set(), + mam = false :: boolean(), + pubsub = <<"">> :: binary(), + enable_hats = false :: boolean(), + lang = ejabberd_option:language() :: binary() }). -type config() :: #config{}. -type role() :: moderator | participant | visitor | none. +-type affiliation() :: admin | member | outcast | owner | none. -record(user, { jid :: jid(), nick :: binary(), role :: role(), - last_presence :: xmlel() + %%is_subscriber = false :: boolean(), + %%subscriptions = [] :: [binary()], + last_presence :: presence() | undefined }). +-record(subscriber, {jid :: jid(), + nick = <<>> :: binary(), + nodes = [] :: [binary()]}). + +-record(muc_subscribers, + {subscribers = #{} :: subscribers(), + subscriber_nicks = #{} :: subscriber_nicks(), + subscriber_nodes = #{} :: subscriber_nodes() + }). + +-type subscribers() :: #{ljid() => #subscriber{}}. +-type subscriber_nicks() :: #{binary() => [ljid()]}. +-type subscriber_nodes() :: #{binary() => subscribers()}. + -record(activity, { message_time = 0 :: integer(), presence_time = 0 :: integer(), - message_shaper :: shaper:shaper(), - presence_shaper :: shaper:shaper(), - message :: xmlel(), - presence :: {binary(), xmlel()} + message_shaper = none :: ejabberd_shaper:shaper(), + presence_shaper = none :: ejabberd_shaper:shaper(), + message :: message() | undefined, + presence :: {binary(), presence()} | undefined }). -record(state, @@ -89,28 +113,30 @@ room = <<"">> :: binary(), host = <<"">> :: binary(), server_host = <<"">> :: binary(), - access = {none,none,none,none} :: {atom(), atom(), atom(), atom()}, + access = {none,none,none,none,none} :: {atom(), atom(), atom(), atom(), atom()}, jid = #jid{} :: jid(), config = #config{} :: config(), - users = (?DICT):new() :: dict(), + users = #{} :: users(), + muc_subscribers = #muc_subscribers{} :: #muc_subscribers{}, last_voice_request_time = treap:empty() :: treap:treap(), - robots = (?DICT):new() :: dict(), - nicks = (?DICT):new() :: dict(), - affiliations = (?DICT):new() :: dict(), - history :: lqueue(), - subject = <<"">> :: binary(), - subject_author = <<"">> :: binary(), - just_created = false :: boolean(), + robots = #{} :: robots(), + nicks = #{} :: nicks(), + affiliations = #{} :: affiliations(), + roles = #{} :: roles(), + history = #lqueue{} :: lqueue(), + subject = [] :: [text()], + subject_author = {<<"">>, #jid{}} :: {binary(), jid()}, + hats_defs = #{} :: #{binary() => {binary(), binary()}}, + hats_users = #{} :: #{ljid() => [binary()]}, + just_created = erlang:system_time(microsecond) :: true | integer(), activity = treap:empty() :: treap:treap(), - room_shaper = none :: shaper:shaper(), - room_queue = queue:new() :: queue() + room_shaper = none :: ejabberd_shaper:shaper(), + room_queue :: p1_queue:queue({message | presence, jid()}) | undefined, + hibernate_timer = none :: reference() | none | hibernating }). --record(muc_online_users, {us = {<<>>, <<>>} :: {binary(), binary()}, - resource = <<>> :: binary() | '_', - room = <<>> :: binary() | '_', - host = <<>> :: binary() | '_'}). - --type muc_online_users() :: #muc_online_users{}. - --type muc_room_state() :: #state{}. +-type users() :: #{ljid() => #user{}}. +-type robots() :: #{jid() => {binary(), stanza()}}. +-type nicks() :: #{binary() => [ljid()]}. +-type affiliations() :: #{ljid() => affiliation() | {affiliation(), binary()}}. +-type roles() :: #{ljid() => role() | {role(), binary()}}. diff --git a/include/mod_offline.hrl b/include/mod_offline.hrl new file mode 100644 index 000000000..e1bb236f6 --- /dev/null +++ b/include/mod_offline.hrl @@ -0,0 +1,31 @@ +%%%---------------------------------------------------------------------- +%%% +%%% ejabberd, Copyright (C) 2002-2025 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. +%%% +%%%---------------------------------------------------------------------- + +-record(offline_msg, + {us = {<<"">>, <<"">>} :: {binary(), binary()}, + timestamp :: erlang:timestamp() | '_' | undefined, + expire :: erlang:timestamp() | never | undefined | '_', + from = #jid{} :: jid() | '_', + to = #jid{} :: jid() | '_', + packet = #xmlel{} :: xmlel() | message() | '_'}). + +-record(state, + {host = <<"">> :: binary(), + access_max_offline_messages}). diff --git a/include/mod_privacy.hrl b/include/mod_privacy.hrl index b3dfd4e7c..8118a6de6 100644 --- a/include/mod_privacy.hrl +++ b/include/mod_privacy.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -22,9 +22,11 @@ default = none :: none | binary(), lists = [] :: [{binary(), [listitem()]}]}). --record(listitem, {type = none :: none | jid | group | subscription, - value = none :: none | both | from | to | ljid() | binary(), - action = allow :: allow | deny, +-type privacy() :: #privacy{}. + +-record(listitem, {type = none :: listitem_type(), + value = none :: listitem_value(), + action = allow :: listitem_action(), order = 0 :: integer(), match_all = false :: boolean(), match_iq = false :: boolean(), @@ -33,11 +35,6 @@ match_presence_out = false :: boolean()}). -type listitem() :: #listitem{}. - --record(userlist, {name = none :: none | binary(), - list = [] :: [listitem()], - needdb = false :: boolean()}). - --type userlist() :: #userlist{}. - --export_type([userlist/0]). +-type listitem_type() :: none | jid | group | subscription. +-type listitem_value() :: none | both | from | to | jid:ljid() | binary(). +-type listitem_action() :: allow | deny. diff --git a/include/mod_private.hrl b/include/mod_private.hrl new file mode 100644 index 000000000..05adc7d8b --- /dev/null +++ b/include/mod_private.hrl @@ -0,0 +1,24 @@ +%%%---------------------------------------------------------------------- +%%% +%%% ejabberd, Copyright (C) 2002-2025 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. +%%% +%%%---------------------------------------------------------------------- + +-record(private_storage, + {usns = {<<"">>, <<"">>, <<"">>} :: {binary(), binary(), binary() | + '$1' | '_'}, + xml = #xmlel{} :: xmlel() | '_' | '$1'}). diff --git a/include/mod_proxy65.hrl b/include/mod_proxy65.hrl index 70181bf82..4f017124a 100644 --- a/include/mod_proxy65.hrl +++ b/include/mod_proxy65.hrl @@ -2,7 +2,7 @@ %%% RFC 1928 constants. %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/include/mod_push.hrl b/include/mod_push.hrl new file mode 100644 index 000000000..8a9de102b --- /dev/null +++ b/include/mod_push.hrl @@ -0,0 +1,24 @@ +%%%---------------------------------------------------------------------- +%%% ejabberd, Copyright (C) 2017-2025 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. +%%% +%%%---------------------------------------------------------------------- +-record(push_session, + {us = {<<"">>, <<"">>} :: {binary(), binary()}, + timestamp = erlang:timestamp() :: erlang:timestamp(), + service = {<<"">>, <<"">>, <<"">>} :: ljid(), + node = <<"">> :: binary(), + xml :: undefined | xmlel()}). diff --git a/include/mod_roster.hrl b/include/mod_roster.hrl index ea060a5cb..a056dd22c 100644 --- a/include/mod_roster.hrl +++ b/include/mod_roster.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -20,15 +20,15 @@ -record(roster, { - usj = {<<>>, <<>>, {<<>>, <<>>, <<>>}} :: {binary(), binary(), ljid()} | '_', + usj = {<<>>, <<>>, {<<>>, <<>>, <<>>}} :: {binary(), binary(), jid:ljid()} | '_', us = {<<>>, <<>>} :: {binary(), binary()} | '_', - jid = {<<>>, <<>>, <<>>} :: ljid(), + jid = {<<>>, <<>>, <<>>} :: jid:ljid(), name = <<>> :: binary() | '_', subscription = none :: subscription() | '_', ask = none :: ask() | '_', groups = [] :: [binary()] | '_', askmessage = <<"">> :: binary() | '_', - xs = [] :: [xmlel()] | '_' + xs = [] :: [fxml:xmlel()] | '_' }). -record(roster_version, diff --git a/include/ejabberd_config.hrl b/include/mod_shared_roster.hrl similarity index 68% rename from include/ejabberd_config.hrl rename to include/mod_shared_roster.hrl index eb4de2609..4c35878e8 100644 --- a/include/ejabberd_config.hrl +++ b/include/mod_shared_roster.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -18,13 +18,8 @@ %%% %%%---------------------------------------------------------------------- --record(local_config, {key :: any(), value :: any()}). +-record(sr_group, {group_host = {<<"">>, <<"">>} :: {'$1' | binary(), '$2' | binary()}, + opts = [] :: list() | '_' | '$2'}). --type local_config() :: #local_config{}. - --record(state, - {opts = [] :: [acl:acl() | local_config()], - hosts = [] :: [binary()], - override_local = false :: boolean(), - override_global = false :: boolean(), - override_acls = false :: boolean()}). +-record(sr_user, {us = {<<"">>, <<"">>} :: {binary(), binary()}, + group_host = {<<"">>, <<"">>} :: {binary(), binary()}}). diff --git a/include/mod_vcard.hrl b/include/mod_vcard.hrl new file mode 100644 index 000000000..d97e5c900 --- /dev/null +++ b/include/mod_vcard.hrl @@ -0,0 +1,28 @@ +%%%---------------------------------------------------------------------- +%%% +%%% ejabberd, Copyright (C) 2002-2025 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. +%%% +%%%---------------------------------------------------------------------- + +-record(vcard_search, + {us, user, luser, fn, lfn, family, lfamily, given, + lgiven, middle, lmiddle, nickname, lnickname, bday, + lbday, ctry, lctry, locality, llocality, email, lemail, + orgname, lorgname, orgunit, lorgunit}). + +-record(vcard, {us = {<<"">>, <<"">>} :: {binary(), binary()} | binary(), + vcard = #xmlel{} :: xmlel()}). diff --git a/include/mqtt.hrl b/include/mqtt.hrl new file mode 100644 index 000000000..bf910368f --- /dev/null +++ b/include/mqtt.hrl @@ -0,0 +1,209 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov +%%% @copyright (C) 2002-2025 ProcessOne, SARL. All Rights Reserved. +%%% +%%% Licensed under the Apache License, Version 2.0 (the "License"); +%%% you may not use this file except in compliance with the License. +%%% You may obtain a copy of the License at +%%% +%%% http://www.apache.org/licenses/LICENSE-2.0 +%%% +%%% Unless required by applicable law or agreed to in writing, software +%%% distributed under the License is distributed on an "AS IS" BASIS, +%%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%%% See the License for the specific language governing permissions and +%%% limitations under the License. +%%% +%%%------------------------------------------------------------------- +-define(MQTT_VERSION_4, 4). +-define(MQTT_VERSION_5, 5). + +-record(connect, {proto_level = 4 :: non_neg_integer(), + will :: undefined | publish(), + clean_start = true :: boolean(), + keep_alive = 0 :: non_neg_integer(), + client_id = <<>> :: binary(), + username = <<>> :: binary(), + password = <<>> :: binary(), + will_properties = #{} :: properties(), + properties = #{} :: properties()}). +-record(connack, {session_present = false :: boolean(), + code = success :: reason_code(), + properties = #{} :: properties()}). + +-record(publish, {id :: undefined | non_neg_integer(), + dup = false :: boolean(), + qos = 0 :: qos(), + retain = false :: boolean(), + topic :: binary(), + payload :: binary(), + properties = #{} :: properties(), + meta = #{} :: map()}). +-record(puback, {id :: non_neg_integer(), + code = success :: reason_code(), + properties = #{} :: properties()}). +-record(pubrec, {id :: non_neg_integer(), + code = success :: reason_code(), + properties = #{} :: properties()}). +-record(pubrel, {id :: non_neg_integer(), + code = success :: reason_code(), + properties = #{} :: properties(), + meta = #{} :: map()}). +-record(pubcomp, {id :: non_neg_integer(), + code = success :: reason_code(), + properties = #{} :: properties()}). + +-record(subscribe, {id :: non_neg_integer(), + filters :: [{binary(), sub_opts()}], + properties = #{} :: properties(), + meta = #{} :: map()}). +-record(suback, {id :: non_neg_integer(), + codes = [] :: [char() | reason_code()], + properties = #{} :: properties()}). + +-record(unsubscribe, {id :: non_neg_integer(), + filters :: [binary()], + properties = #{} :: properties(), + meta = #{} :: map()}). +-record(unsuback, {id :: non_neg_integer(), + codes = [] :: [reason_code()], + properties = #{} :: properties()}). + +-record(pingreq, {meta = #{} :: map()}). +-record(pingresp, {}). + +-record(disconnect, {code = 'normal-disconnection' :: reason_code(), + properties = #{} :: properties()}). + +-record(auth, {code = success :: reason_code(), + properties = #{} :: properties()}). + +-record(sub_opts, {qos = 0 :: qos(), + no_local = false :: boolean(), + retain_as_published = false :: boolean(), + retain_handling = 0 :: 0..2}). + +-type qos() :: 0|1|2. +-type sub_opts() :: #sub_opts{}. +-type utf8_pair() :: {binary(), binary()}. +-type properties() :: #{assigned_client_identifier => binary(), + authentication_data => binary(), + authentication_method => binary(), + content_type => binary(), + correlation_data => binary(), + maximum_packet_size => pos_integer(), + maximum_qos => 0|1, + message_expiry_interval => non_neg_integer(), + payload_format_indicator => binary | utf8, + reason_string => binary(), + receive_maximum => pos_integer(), + request_problem_information => boolean(), + request_response_information => boolean(), + response_information => binary(), + response_topic => binary(), + retain_available => boolean(), + server_keep_alive => non_neg_integer(), + server_reference => binary(), + session_expiry_interval => non_neg_integer(), + shared_subscription_available => boolean(), + subscription_identifier => [non_neg_integer()] | non_neg_integer(), + subscription_identifiers_available => boolean(), + topic_alias => pos_integer(), + topic_alias_maximum => non_neg_integer(), + user_property => [utf8_pair()], + wildcard_subscription_available => boolean(), + will_delay_interval => non_neg_integer()}. +-type property() :: assigned_client_identifier | + authentication_data | + authentication_method | + content_type | + correlation_data | + maximum_packet_size | + maximum_qos | + message_expiry_interval | + payload_format_indicator | + reason_string | + receive_maximum | + request_problem_information | + request_response_information | + response_information | + response_topic | + retain_available | + server_keep_alive | + server_reference | + session_expiry_interval | + shared_subscription_available | + subscription_identifier | + subscription_identifiers_available | + topic_alias | + topic_alias_maximum | + user_property | + wildcard_subscription_available | + will_delay_interval. +-type reason_code() :: 'success' | + 'normal-disconnection' | + 'granted-qos-0' | + 'granted-qos-1' | + 'granted-qos-2' | + 'disconnect-with-will-message' | + 'no-matching-subscribers' | + 'no-subscription-existed' | + 'continue-authentication' | + 're-authenticate' | + 'unspecified-error' | + 'malformed-packet' | + 'protocol-error' | + 'implementation-specific-error' | + 'unsupported-protocol-version' | + 'client-identifier-not-valid' | + 'bad-user-name-or-password' | + 'not-authorized' | + 'server-unavailable' | + 'server-busy' | + 'banned' | + 'server-shutting-down' | + 'bad-authentication-method' | + 'keep-alive-timeout' | + 'session-taken-over' | + 'topic-filter-invalid' | + 'topic-name-invalid' | + 'packet-identifier-in-use' | + 'packet-identifier-not-found' | + 'receive-maximum-exceeded' | + 'topic-alias-invalid' | + 'packet-too-large' | + 'message-rate-too-high' | + 'quota-exceeded' | + 'administrative-action' | + 'payload-format-invalid' | + 'retain-not-supported' | + 'qos-not-supported' | + 'use-another-server' | + 'server-moved' | + 'shared-subscriptions-not-supported' | + 'connection-rate-exceeded' | + 'maximum-connect-time' | + 'subscription-identifiers-not-supported' | + 'wildcard-subscriptions-not-supported'. + +-type connect() :: #connect{}. +-type connack() :: #connack{}. +-type publish() :: #publish{}. +-type puback() :: #puback{}. +-type pubrel() :: #pubrel{}. +-type pubrec() :: #pubrec{}. +-type pubcomp() :: #pubcomp{}. +-type subscribe() :: #subscribe{}. +-type suback() :: #suback{}. +-type unsubscribe() :: #unsubscribe{}. +-type unsuback() :: #unsuback{}. +-type pingreq() :: #pingreq{}. +-type pingresp() :: #pingresp{}. +-type disconnect() :: #disconnect{}. +-type auth() :: #auth{}. + +-type mqtt_packet() :: connect() | connack() | publish() | puback() | + pubrel() | pubrec() | pubcomp() | subscribe() | + suback() | unsubscribe() | unsuback() | pingreq() | + pingresp() | disconnect() | auth(). +-type mqtt_version() :: ?MQTT_VERSION_4 | ?MQTT_VERSION_5. diff --git a/include/ns.hrl b/include/ns.hrl deleted file mode 100644 index a96edc7ab..000000000 --- a/include/ns.hrl +++ /dev/null @@ -1,152 +0,0 @@ -%%%---------------------------------------------------------------------- -%%% -%%% ejabberd, Copyright (C) 2002-2015 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. -%%% -%%%---------------------------------------------------------------------- - --define(NS_DISCO_ITEMS, - <<"http://jabber.org/protocol/disco#items">>). --define(NS_DISCO_INFO, - <<"http://jabber.org/protocol/disco#info">>). --define(NS_VCARD, <<"vcard-temp">>). --define(NS_VCARD_UPDATE, <<"vcard-temp:x:update">>). --define(NS_AUTH, <<"jabber:iq:auth">>). --define(NS_AUTH_ERROR, <<"jabber:iq:auth:error">>). --define(NS_REGISTER, <<"jabber:iq:register">>). --define(NS_SEARCH, <<"jabber:iq:search">>). --define(NS_ROSTER, <<"jabber:iq:roster">>). --define(NS_ROSTER_VER, - <<"urn:xmpp:features:rosterver">>). --define(NS_PRIVACY, <<"jabber:iq:privacy">>). --define(NS_BLOCKING, <<"urn:xmpp:blocking">>). --define(NS_PRIVATE, <<"jabber:iq:private">>). --define(NS_VERSION, <<"jabber:iq:version">>). --define(NS_TIME90, <<"jabber:iq:time">>). --define(NS_TIME, <<"urn:xmpp:time">>). --define(NS_LAST, <<"jabber:iq:last">>). --define(NS_XDATA, <<"jabber:x:data">>). --define(NS_IQDATA, <<"jabber:iq:data">>). --define(NS_DELAY91, <<"jabber:x:delay">>). --define(NS_DELAY, <<"urn:xmpp:delay">>). --define(NS_HINTS, <<"urn:xmpp:hints">>). --define(NS_EXPIRE, <<"jabber:x:expire">>). --define(NS_EVENT, <<"jabber:x:event">>). --define(NS_CHATSTATES, - <<"http://jabber.org/protocol/chatstates">>). --define(NS_XCONFERENCE, <<"jabber:x:conference">>). --define(NS_STATS, - <<"http://jabber.org/protocol/stats">>). --define(NS_MUC, <<"http://jabber.org/protocol/muc">>). --define(NS_MUC_USER, - <<"http://jabber.org/protocol/muc#user">>). --define(NS_MUC_ADMIN, - <<"http://jabber.org/protocol/muc#admin">>). --define(NS_MUC_OWNER, - <<"http://jabber.org/protocol/muc#owner">>). --define(NS_MUC_UNIQUE, - <<"http://jabber.org/protocol/muc#unique">>). --define(NS_PUBSUB, - <<"http://jabber.org/protocol/pubsub">>). --define(NS_PUBSUB_EVENT, - <<"http://jabber.org/protocol/pubsub#event">>). --define(NS_PUBSUB_META_DATA, - <<"http://jabber.org/protocol/pubsub#meta-data">>). --define(NS_PUBSUB_OWNER, - <<"http://jabber.org/protocol/pubsub#owner">>). --define(NS_PUBSUB_NMI, - <<"http://jabber.org/protocol/pubsub#node-meta-info">>). --define(NS_PUBSUB_ERRORS, - <<"http://jabber.org/protocol/pubsub#errors">>). --define(NS_PUBSUB_NODE_CONFIG, - <<"http://jabber.org/protocol/pubsub#node_config">>). --define(NS_PUBSUB_SUB_OPTIONS, - <<"http://jabber.org/protocol/pubsub#subscribe_options">>). --define(NS_PUBSUB_SUBSCRIBE_OPTIONS, - <<"http://jabber.org/protocol/pubsub#subscribe_options">>). --define(NS_PUBSUB_PUBLISH_OPTIONS, - <<"http://jabber.org/protocol/pubsub#publish_options">>). --define(NS_PUBSUB_SUB_AUTH, - <<"http://jabber.org/protocol/pubsub#subscribe_authorization">>). --define(NS_PUBSUB_GET_PENDING, - <<"http://jabber.org/protocol/pubsub#get-pending">>). --define(NS_COMMANDS, - <<"http://jabber.org/protocol/commands">>). --define(NS_BYTESTREAMS, - <<"http://jabber.org/protocol/bytestreams">>). --define(NS_ADMIN, - <<"http://jabber.org/protocol/admin">>). --define(NS_ADMIN_ANNOUNCE, - <<"http://jabber.org/protocol/admin#announce">>). --define(NS_ADMIN_ANNOUNCE_ALL, - <<"http://jabber.org/protocol/admin#announce-all">>). --define(NS_ADMIN_SET_MOTD, - <<"http://jabber.org/protocol/admin#set-motd">>). --define(NS_ADMIN_EDIT_MOTD, - <<"http://jabber.org/protocol/admin#edit-motd">>). --define(NS_ADMIN_DELETE_MOTD, - <<"http://jabber.org/protocol/admin#delete-motd">>). --define(NS_ADMIN_ANNOUNCE_ALLHOSTS, - <<"http://jabber.org/protocol/admin#announce-allhosts">>). --define(NS_ADMIN_ANNOUNCE_ALL_ALLHOSTS, - <<"http://jabber.org/protocol/admin#announce-all-allhosts">>). --define(NS_ADMIN_SET_MOTD_ALLHOSTS, - <<"http://jabber.org/protocol/admin#set-motd-allhosts">>). --define(NS_ADMIN_EDIT_MOTD_ALLHOSTS, - <<"http://jabber.org/protocol/admin#edit-motd-allhosts">>). --define(NS_ADMIN_DELETE_MOTD_ALLHOSTS, - <<"http://jabber.org/protocol/admin#delete-motd-allhosts">>). --define(NS_SERVERINFO, - <<"http://jabber.org/network/serverinfo">>). --define(NS_RSM, <<"http://jabber.org/protocol/rsm">>). --define(NS_EJABBERD_CONFIG, <<"ejabberd:config">>). --define(NS_STREAM, - <<"http://etherx.jabber.org/streams">>). --define(NS_STANZAS, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>). --define(NS_STREAMS, - <<"urn:ietf:params:xml:ns:xmpp-streams">>). --define(NS_TLS, <<"urn:ietf:params:xml:ns:xmpp-tls">>). --define(NS_SASL, - <<"urn:ietf:params:xml:ns:xmpp-sasl">>). --define(NS_SESSION, - <<"urn:ietf:params:xml:ns:xmpp-session">>). --define(NS_BIND, - <<"urn:ietf:params:xml:ns:xmpp-bind">>). --define(NS_FEATURE_IQAUTH, - <<"http://jabber.org/features/iq-auth">>). --define(NS_FEATURE_IQREGISTER, - <<"http://jabber.org/features/iq-register">>). --define(NS_FEATURE_COMPRESS, - <<"http://jabber.org/features/compress">>). --define(NS_FEATURE_MSGOFFLINE, <<"msgoffline">>). --define(NS_COMPRESS, - <<"http://jabber.org/protocol/compress">>). --define(NS_CAPS, <<"http://jabber.org/protocol/caps">>). --define(NS_SHIM, <<"http://jabber.org/protocol/shim">>). --define(NS_ADDRESS, - <<"http://jabber.org/protocol/address">>). --define(NS_OOB, <<"jabber:x:oob">>). --define(NS_CAPTCHA, <<"urn:xmpp:captcha">>). --define(NS_MEDIA, <<"urn:xmpp:media-element">>). --define(NS_BOB, <<"urn:xmpp:bob">>). --define(NS_PING, <<"urn:xmpp:ping">>). --define(NS_CARBONS_2, <<"urn:xmpp:carbons:2">>). --define(NS_CARBONS_1, <<"urn:xmpp:carbons:1">>). --define(NS_FORWARD, <<"urn:xmpp:forward:0">>). --define(NS_CLIENT_STATE, <<"urn:xmpp:csi:0">>). --define(NS_STREAM_MGMT_2, <<"urn:xmpp:sm:2">>). --define(NS_STREAM_MGMT_3, <<"urn:xmpp:sm:3">>). diff --git a/include/pubsub.hrl b/include/pubsub.hrl index bfbba7c55..316be342a 100644 --- a/include/pubsub.hrl +++ b/include/pubsub.hrl @@ -1,44 +1,37 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. -%%% +%%%---------------------------------------------------------------------- %%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. -%%% +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% -%%% The Initial Developer of the Original Code is ProcessOne. -%%% Portions created by ProcessOne are Copyright 2006-2015, ProcessOne -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2015, 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. %%% -%%% copyright 2006-2015 ProcessOne +%%% 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. %%% -%%% This file contains pubsub types definition. -%%% ==================================================================== +%%%---------------------------------------------------------------------- %% ------------------------------- %% Pubsub constants --define(ERR_EXTENDED(E, C), - mod_pubsub:extended_error(E, C)). +-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. --define(MAX_PAYLOAD_SIZE, 60000). +%% Would be nice to have it configurable. +-define(MAX_PAYLOAD_SIZE, 250000). %% ------------------------------- %% Pubsub types -%% @type hostPubsub() = string(). -type(hostPubsub() :: binary()). %%

hostPubsub is the name of the PubSub service. For example, it can be %% "pubsub.localhost".

@@ -57,12 +50,15 @@ -type(nodeId() :: binary()). %% @type nodeId() = binary(). %%

A node is defined by a list of its ancestors. The last element is the name -%% of the current node. For example: %% of the current node. For example: %% ```<<"/home/localhost/user">>'''

--type(nodeIdx() :: pos_integer()). -%% @type nodeIdx() = integer(). +-type(nodeIdx() :: pos_integer() | binary()). +%% @type nodeIdx() = integer() | binary(). +%% note: pos_integer() should always be used, but we allow anything else coded +%% as binary, so one can have a custom implementation of nodetree with custom +%% indexing (see nodetree_virtual). this also allows to use any kind of key for +%% indexing nodes, as this can be useful with external backends such as sql. -type(itemId() :: binary()). %% @type itemId() = string(). @@ -70,28 +66,12 @@ -type(subId() :: binary()). %% @type subId() = string(). - -%% @type payload() = [#xmlelement{} | #xmlcdata{}]. - -%% @type stanzaError() = #xmlelement{}. -%% Example: -%% Example: -%% ```{xmlelement, "error", -%% [{"code", Code}, {"type", Type}], -%% [{xmlelement, Condition, [{"xmlns", ?NS_STANZAS}], []}]}''' -%% @type pubsubIQResponse() = #xmlelement{}. -%% Example: -%% ```{xmlelement, "pubsub", -%% [{"xmlns", ?NS_PUBSUB_EVENT}], -%% [{xmlelement, "affiliations", [], -%% []}]}''' - -type(nodeOption() :: {Option::atom(), - Value::binary() | [binary()] | boolean() | non_neg_integer() + Value::atom() | [binary()] | boolean() | non_neg_integer() }). --type(nodeOptions() :: [NodeOption::mod_pubsub:nodeOption(),...]). +-type(nodeOptions() :: [mod_pubsub:nodeOption(),...]). %% @type nodeOption() = {Option, Value} %% Option = atom() @@ -104,43 +84,24 @@ Value::binary() | [binary()] | boolean() }). --type(subOptions() :: [SubOption::mod_pubsub:subOption(),...]). +-type(subOptions() :: [mod_pubsub:subOption()]). -%% @type nodeType() = string(). -%%

The nodeType is a string containing the name of the PubSub -%% plugin to use to manage a given node. For example, it can be -%% "flat", "hometree" or "blog".

+-type(pubOption() :: + {Option::binary(), + Values::[binary()] +}). -%% @type jid() = {jid, User, Server, Resource, LUser, LServer, LResource} -%% User = string() -%% Server = string() -%% Resource = string() -%% LUser = string() -%% LServer = string() -%% LResource = string(). - -%-type(ljid() :: {binary(), binary(), binary()}). -%% @type ljid() = {User, Server, Resource} -%% User = string() -%% Server = string() -%% Resource = string(). +-type(pubOptions() :: [mod_pubsub:pubOption()]). -type(affiliation() :: 'none' | 'owner' | 'publisher' - %| 'publish-only' + | 'publish_only' | 'member' | 'outcast' ). %% @type affiliation() = 'none' | 'owner' | 'publisher' | 'publish-only' | 'member' | 'outcast'. --type(subscription() :: 'none' - | 'pending' - | 'unconfigured' - | 'subscribed' -). -%% @type subscription() = 'none' | 'pending' | 'unconfigured' | 'subscribed'. - -type(accessModel() :: 'open' | 'presence' | 'roster' @@ -149,16 +110,11 @@ ). %% @type accessModel() = 'open' | 'presence' | 'roster' | 'authorize' | 'whitelist'. -%% @type pubsubIndex() = {pubsub_index, Index, Last, Free} -%% Index = atom() -%% Last = integer() -%% Free = [integer()]. -%% internal pubsub index table -type(publishModel() :: 'publishers' | 'subscribers' | 'open' ). - +%% @type publishModel() = 'publishers' | 'subscribers' | 'open' -record(pubsub_index, { @@ -167,91 +123,50 @@ free :: [mod_pubsub:nodeIdx()] }). -%% @type pubsubNode() = {pubsub_node, NodeId, Id, Parents, Type, Owners, Options} -%% NodeId = {host() | ljid(), nodeId()} -%% Id = nodeIdx() -%% Parents = [nodeId()] -%% Type = nodeType() -%% Owners = [ljid()] -%% Options = [nodeOption()]. -%%

This is the format of the nodes table. The type of the table -%% is: set,ram/disc.

-%%

The Parents and type fields are indexed.

-%% id can be anything you want. -record(pubsub_node, { - nodeid ,%:: {Host::mod_pubsub:host(), NodeId::mod_pubsub:nodeId()}, - id ,%:: mod_pubsub:nodeIdx(), - parents = [] ,%:: [Parent_NodeId::mod_pubsub:nodeId()], - type = <<"flat">> ,%:: binary(), - owners = [] ,%:: [Owner::ljid(),...], - options = [] %:: mod_pubsub:nodeOptions() + nodeid ,% :: {mod_pubsub:host(), mod_pubsub:nodeId()}, + id ,% :: mod_pubsub:nodeIdx(), + parents = [] ,% :: [mod_pubsub:nodeId(),...], + type = <<"flat">>,% :: binary(), + owners = [] ,% :: [jid:ljid(),...], + options = [] % :: mod_pubsub:nodeOptions() }). -%% @type pubsubState() = {pubsub_state, StateId, Items, Affiliation, Subscriptions} -%% StateId = {ljid(), nodeIdx()} -%% Items = [itemId()] -%% Affiliation = affiliation() -%% Subscriptions = [{subscription(), subId()}]. -%%

This is the format of the affiliations table. The type of the -%% table is: set,ram/disc.

- -%-record(pubsub_state, -% {stateid, items = [], affiliation = none, -% subscriptions = []}). -record(pubsub_state, { - stateid ,%:: {Entity::ljid(), NodeIdx::mod_pubsub:nodeIdx()}, - items = [] ,%:: [ItemId::mod_pubsub:itemId()], - affiliation = 'none' ,%:: mod_pubsub:affiliation(), - subscriptions = [] %:: [{mod_pubsub:subscription(), mod_pubsub:subId()}] + stateid ,% :: {jid:ljid(), mod_pubsub:nodeIdx()}, + nodeidx ,% :: mod_pubsub:nodeIdx(), + items = [] ,% :: [mod_pubsub:itemId(),...], + affiliation = 'none',% :: mod_pubsub:affiliation(), + subscriptions = [] % :: [{mod_pubsub:subscription(), mod_pubsub:subId()}] }). -%% @type pubsubItem() = {pubsub_item, ItemId, Creation, Modification, Payload} -%% ItemId = {itemId(), nodeIdx()} -%% Creation = {now(), ljid()} -%% Modification = {now(), ljid()} -%% Payload = payload(). -%%

This is the format of the published items table. The type of the -%% table is: set,disc,fragmented.

-%-record(pubsub_item, -% {itemid, creation = {unknown, unknown}, -% modification = {unknown, unknown}, payload = []}). - -record(pubsub_item, { - itemid ,%:: {mod_pubsub:itemId(), mod_pubsub:nodeIdx()}, - creation = {unknown, unknown} ,%:: {erlang:timestamp(), ljid()}, - modification = {unknown, unknown} ,%:: {erlang:timestamp(), ljid()}, - payload = [] %:: mod_pubsub:payload() + itemid ,% :: {mod_pubsub:itemId(), mod_pubsub:nodeIdx()}, + nodeidx ,% :: mod_pubsub:nodeIdx(), + creation = {unknown, unknown},% :: {erlang:timestamp(), jid:ljid()}, + modification = {unknown, unknown},% :: {erlang:timestamp(), jid:ljid()}, + payload = [] % :: mod_pubsub:payload() }). -%% @type pubsubSubscription() = {pubsub_subscription, SubId, Options} -%% SubId = subId() -%% Options = [nodeOption()]. -%%

This is the format of the subscriptions table. The type of the -%% table is: set,ram/disc.

-%-record(pubsub_subscription, {subid, options}). -record(pubsub_subscription, { - subid ,%:: mod_pubsub:subId(), - options %:: [] | mod_pubsub:subOptions() + subid ,% :: mod_pubsub:subId(), + options = [] % :: mod_pubsub:subOptions() }). -%% @type pubsubLastItem() = {pubsub_last_item, NodeId, ItemId, Creation, Payload} -%% NodeId = nodeIdx() -%% ItemId = itemId() -%% Creation = {now(),ljid()} -%% Payload = payload(). -%%

This is the format of the last items table. it stores last item payload -%% for every node

-%-record(pubsub_last_item, -% {nodeid, itemid, creation, payload}). - -record(pubsub_last_item, { - nodeid ,%:: mod_pubsub:nodeIdx(), - itemid ,%:: mod_pubsub:itemId(), - creation ,%:: {erlang:timestamp(), ljid()}, - payload %:: mod_pubsub:payload() + nodeid ,% :: {binary(), mod_pubsub:nodeIdx()}, + itemid ,% :: mod_pubsub:itemId(), + creation ,% :: {erlang:timestamp(), jid:ljid()}, + payload % :: mod_pubsub:payload() +}). + +-record(pubsub_orphan, +{ + nodeid ,% :: mod_pubsub:nodeIdx(), + items = [] % :: list() }). diff --git a/include/translate.hrl b/include/translate.hrl new file mode 100644 index 000000000..b0e50e7d6 --- /dev/null +++ b/include/translate.hrl @@ -0,0 +1 @@ +-define(T(S), <>). diff --git a/install-sh b/install-sh old mode 100644 new mode 100755 diff --git a/lib/Ejabberd/logger.ex b/lib/Ejabberd/logger.ex deleted file mode 100644 index bef1cb3aa..000000000 --- a/lib/Ejabberd/logger.ex +++ /dev/null @@ -1,9 +0,0 @@ -defmodule Ejabberd.Logger do - - def critical(message, args \\ []), do: :lager.log(:critical, [], message, args) - def error(message, args \\ []), do: :lager.log(:error, [], message, args) - def warning(message, args \\ []), do: :lager.log(:warning, [], message, args) - def info(message, args \\ []), do: :lager.log(:info, [], message, args) - def debug(message, args \\ []), do: :lager.log(:debug, [], message, args) - -end diff --git a/lib/ejabberd.ex b/lib/ejabberd.ex deleted file mode 100644 index a843abc97..000000000 --- a/lib/ejabberd.ex +++ /dev/null @@ -1,2 +0,0 @@ -defmodule Ejabberd do -end diff --git a/lib/ejabberd/config/attr.ex b/lib/ejabberd/config/attr.ex new file mode 100644 index 000000000..85d19191b --- /dev/null +++ b/lib/ejabberd/config/attr.ex @@ -0,0 +1,119 @@ +defmodule Ejabberd.Config.Attr do + @moduledoc """ + Module used to work with the attributes parsed from + an elixir block (do...end). + + Contains functions for extracting attrs from a block + and validation. + """ + + @type attr :: {atom(), any()} + + @attr_supported [ + active: + [type: :boolean, default: true], + git: + [type: :string, default: ""], + name: + [type: :string, default: ""], + opts: + [type: :list, default: []], + dependency: + [type: :list, default: []] + ] + + @doc """ + Takes a block with annotations and extracts the list + of attributes. + """ + @spec extract_attrs_from_block_with_defaults(any()) :: [attr] + def extract_attrs_from_block_with_defaults(block) do + block + |> extract_attrs_from_block + |> put_into_list_if_not_already + |> insert_default_attrs_if_missing + end + + @doc """ + Takes an attribute or a list of attrs and validate them. + + Returns a {:ok, attr} or {:error, attr, cause} for each of the attributes. + """ + @spec validate([attr]) :: [{:ok, attr}] | [{:error, attr, atom()}] + def validate(attrs) when is_list(attrs), do: Enum.map(attrs, &valid_attr?/1) + def validate(attr), do: validate([attr]) + + @doc """ + Returns the type of an attribute, given its name. + """ + @spec get_type_for_attr(atom()) :: atom() + def get_type_for_attr(attr_name) do + @attr_supported + |> Keyword.get(attr_name) + |> Keyword.get(:type) + end + + @doc """ + Returns the default value for an attribute, given its name. + """ + @spec get_default_for_attr(atom()) :: any() + def get_default_for_attr(attr_name) do + @attr_supported + |> Keyword.get(attr_name) + |> Keyword.get(:default) + end + + # Private API + + # Given an elixir block (do...end) returns a list with the annotations + # or a single annotation. + @spec extract_attrs_from_block(any()) :: [attr] | attr + defp extract_attrs_from_block({:__block__, [], attrs}), do: Enum.map(attrs, &extract_attrs_from_block/1) + defp extract_attrs_from_block({:@, _, [attrs]}), do: extract_attrs_from_block(attrs) + defp extract_attrs_from_block({attr_name, _, [value]}), do: {attr_name, value} + defp extract_attrs_from_block(nil), do: [] + + # In case extract_attrs_from_block returns a single attribute, + # then put it into a list. (Ensures attrs are always into a list). + @spec put_into_list_if_not_already([attr] | attr) :: [attr] + defp put_into_list_if_not_already(attrs) when is_list(attrs), do: attrs + defp put_into_list_if_not_already(attr), do: [attr] + + # Given a list of attributes, it inserts the missing attribute with their + # default value. + @spec insert_default_attrs_if_missing([attr]) :: [attr] + defp insert_default_attrs_if_missing(attrs) do + Enum.reduce @attr_supported, attrs, fn({attr_name, _}, acc) -> + case Keyword.has_key?(acc, attr_name) do + true -> acc + false -> Keyword.put(acc, attr_name, get_default_for_attr(attr_name)) + end + end + end + + # Given an attribute, validates it and return a tuple with + # {:ok, attr} or {:error, attr, cause} + @spec valid_attr?(attr) :: {:ok, attr} | {:error, attr, atom()} + defp valid_attr?({attr_name, param} = attr) do + case Keyword.get(@attr_supported, attr_name) do + nil -> {:error, attr, :attr_not_supported} + [{:type, param_type} | _] -> case is_of_type?(param, param_type) do + true -> {:ok, attr} + false -> {:error, attr, :type_not_supported} + end + end + end + + # Given an attribute value and a type, it returns a true + # if the value its of the type specified, false otherwise. + + # Usefoul for checking if an attr value respects the type + # specified for the annotation. + @spec is_of_type?(any(), atom()) :: boolean() + defp is_of_type?(param, type) when type == :boolean and is_boolean(param), do: true + defp is_of_type?(param, type) when type == :string and is_bitstring(param), do: true + defp is_of_type?(param, type) when type == :list and is_list(param), do: true + defp is_of_type?(param, type) when type == :atom and is_atom(param), do: true + defp is_of_type?(_param, type) when type == :any, do: true + defp is_of_type?(_, _), do: false +end diff --git a/lib/ejabberd/config/config.ex b/lib/ejabberd/config/config.ex new file mode 100644 index 000000000..a8805e612 --- /dev/null +++ b/lib/ejabberd/config/config.ex @@ -0,0 +1,142 @@ +defmodule Ejabberd.Config do + @moduledoc """ + Base module for configuration file. + + Imports macros for the config DSL and contains functions + for working/starting the configuration parsed. + """ + + alias Ejabberd.Config.EjabberdModule + alias Ejabberd.Config.Attr + alias Ejabberd.Config.EjabberdLogger + + defmacro __using__(_opts) do + quote do + import Ejabberd.Config, only: :macros + import Ejabberd.Logger + + @before_compile Ejabberd.Config + end + end + + # Validate the modules parsed and log validation errors at compile time. + # Could be also possible to interrupt the compilation&execution by throwing + # an exception if necessary. + def __before_compile__(_env) do + get_modules_parsed_in_order() + |> EjabberdModule.validate + |> EjabberdLogger.log_errors + end + + @doc """ + Given the path of the config file, it evaluates it. + """ + def init(file_path, force \\ false) do + init_already_executed = Ejabberd.Config.Store.get(:module_name) != [] + + case force do + true -> + Ejabberd.Config.Store.stop() + Ejabberd.Config.Store.start_link() + do_init(file_path) + false -> + if not init_already_executed, do: do_init(file_path) + end + end + + @doc """ + Returns a list with all the opts, formatted for ejabberd. + """ + def get_ejabberd_opts do + get_general_opts() + |> Map.put(:modules, get_modules_parsed_in_order()) + |> Map.put(:listeners, get_listeners_parsed_in_order()) + |> Ejabberd.Config.OptsFormatter.format_opts_for_ejabberd + end + + @doc """ + Register the hooks defined inside the elixir config file. + """ + def start_hooks do + get_hooks_parsed_in_order() + |> Enum.each(&Ejabberd.Config.EjabberdHook.start/1) + end + + ### + ### MACROS + ### + + defmacro listen(module, do: block) do + attrs = Attr.extract_attrs_from_block_with_defaults(block) + + quote do + Ejabberd.Config.Store.put(:listeners, %EjabberdModule{ + module: unquote(module), + attrs: unquote(attrs) + }) + end + end + + defmacro module(module, do: block) do + attrs = Attr.extract_attrs_from_block_with_defaults(block) + + quote do + Ejabberd.Config.Store.put(:modules, %EjabberdModule{ + module: unquote(module), + attrs: unquote(attrs) + }) + end + end + + defmacro hook(hook_name, opts, fun) do + quote do + Ejabberd.Config.Store.put(:hooks, %Ejabberd.Config.EjabberdHook{ + hook: unquote(hook_name), + opts: unquote(opts), + fun: unquote(fun) + }) + end + end + + # Private API + + defp do_init(file_path) do + # File evaluation + Code.eval_file(file_path) |> extract_and_store_module_name() + + # Getting start/0 config + [module] = Ejabberd.Config.Store.get(:module_name) + call_start_func_and_store_data(module) + + # Fetching git modules and install them + get_modules_parsed_in_order() + |> EjabberdModule.fetch_git_repos + end + + # Returns the modules from the store + defp get_modules_parsed_in_order, + do: Ejabberd.Config.Store.get(:modules) |> Enum.reverse + + # Returns the listeners from the store + defp get_listeners_parsed_in_order, + do: Ejabberd.Config.Store.get(:listeners) |> Enum.reverse + + defp get_hooks_parsed_in_order, + do: Ejabberd.Config.Store.get(:hooks) |> Enum.reverse + + # Returns the general config options + defp get_general_opts, + do: Ejabberd.Config.Store.get(:general) |> List.first + + # Gets the general ejabberd options calling + # the start/0 function and stores them. + defp call_start_func_and_store_data(module) do + opts = apply(module, :start, []) + Ejabberd.Config.Store.put(:general, opts) + end + + # Stores the configuration module name + defp extract_and_store_module_name({{:module, mod, _bytes, _}, _}) do + Ejabberd.Config.Store.put(:module_name, mod) + end +end diff --git a/lib/ejabberd/config/ejabberd_hook.ex b/lib/ejabberd/config/ejabberd_hook.ex new file mode 100644 index 000000000..5f9de4aa0 --- /dev/null +++ b/lib/ejabberd/config/ejabberd_hook.ex @@ -0,0 +1,22 @@ +defmodule Ejabberd.Config.EjabberdHook do + @moduledoc """ + Module containing functions for manipulating + ejabberd hooks. + """ + + defstruct hook: nil, opts: [], fun: nil + + alias Ejabberd.Config.EjabberdHook + + @type t :: %EjabberdHook{} + + @doc """ + Register a hook to ejabberd. + """ + def start(%EjabberdHook{hook: hook, opts: opts, fun: fun}) do + host = Keyword.get(opts, :host, :global) + priority = Keyword.get(opts, :priority, 50) + + :ejabberd_hooks.add(hook, host, fun, priority) + end +end diff --git a/lib/ejabberd/config/ejabberd_module.ex b/lib/ejabberd/config/ejabberd_module.ex new file mode 100644 index 000000000..6d5b1e467 --- /dev/null +++ b/lib/ejabberd/config/ejabberd_module.ex @@ -0,0 +1,69 @@ +defmodule Ejabberd.Config.EjabberdModule do + @moduledoc """ + Module representing a module block in the configuration file. + It offers functions for validation and for starting the modules. + + Warning: The name is EjabberdModule to not collide with + the already existing Elixir.Module. + """ + + alias Ejabberd.Config.EjabberdModule + alias Ejabberd.Config.Validation + alias Ejabberd.Config.Attr + + @type t :: %{module: atom, attrs: [Attr.attr]} + + defstruct [:module, :attrs] + + @doc """ + Given a list of modules / single module + it runs different validators on them. + + For each module, returns a {:ok, mod} or {:error, mod, errors} + """ + def validate(modules) do + Validation.validate(modules) + end + + @doc """ + Given a list of modules, it takes only the ones with + a git attribute and tries to fetch the repo, + then, it install them through :ext_mod.install/1 + """ + def fetch_git_repos(modules) do + modules + |> Enum.filter(&is_git_module?/1) + |> Enum.each(&fetch_and_install_git_module/1) + end + + # Private API + + defp is_git_module?(%EjabberdModule{attrs: attrs}) do + case Keyword.get(attrs, :git) do + "" -> false + repo -> String.match?(repo, ~r/((git|ssh|http(s)?)|(git@[\w\.]+))(:(\/\/)?)([\w\.@\:\/\-~]+)(\.git)(\/)?/) + end + end + + defp fetch_and_install_git_module(%EjabberdModule{attrs: attrs}) do + repo = Keyword.get(attrs, :git) + mod_name = case Keyword.get(attrs, :name) do + "" -> infer_mod_name_from_git_url(repo) + name -> name + end + + path = "#{:ext_mod.modules_dir()}/sources/ejabberd-contrib\/#{mod_name}" + fetch_and_store_repo_source_if_not_exists(path, repo) + :ext_mod.install(mod_name) # Have to check if overwrites an already present mod + end + + defp fetch_and_store_repo_source_if_not_exists(path, repo) do + unless File.exists?(path) do + IO.puts "[info] Fetching: #{repo}" + :os.cmd(~c"git clone #{repo} #{path}") + end + end + + defp infer_mod_name_from_git_url(repo), + do: String.split(repo, "/") |> List.last |> String.replace(".git", "") +end diff --git a/lib/ejabberd/config/logger/ejabberd_logger.ex b/lib/ejabberd/config/logger/ejabberd_logger.ex new file mode 100644 index 000000000..822571916 --- /dev/null +++ b/lib/ejabberd/config/logger/ejabberd_logger.ex @@ -0,0 +1,32 @@ +defmodule Ejabberd.Config.EjabberdLogger do + @moduledoc """ + Module used to log validation errors given validated modules + given validated modules. + """ + + alias Ejabberd.Config.EjabberdModule + + @doc """ + Given a list of modules validated, in the form of {:ok, mod} or + {:error, mod, errors}, it logs to the user the errors found. + """ + @spec log_errors([EjabberdModule.t]) :: [EjabberdModule.t] + def log_errors(modules_validated) when is_list(modules_validated) do + Enum.each modules_validated, &do_log_errors/1 + modules_validated + end + + defp do_log_errors({:ok, _mod}), do: nil + defp do_log_errors({:error, _mod, errors}), do: (Enum.each errors, &do_log_errors/1) + defp do_log_errors({:attribute, errors}), do: (Enum.each errors, &log_attribute_error/1) + defp do_log_errors({:dependency, errors}), do: (Enum.each errors, &log_dependency_error/1) + + defp log_attribute_error({{attr_name, _val}, :attr_not_supported}), do: + IO.puts "[ WARN ] Annotation @#{attr_name} is not supported." + + defp log_attribute_error({{attr_name, val}, :type_not_supported}), do: + IO.puts "[ WARN ] Annotation @#{attr_name} with value #{inspect val} is not supported (type mismatch)." + + defp log_dependency_error({module, :not_found}), do: + IO.puts "[ WARN ] Module #{inspect module} was not found, but is required as a dependency." +end diff --git a/lib/ejabberd/config/opts_formatter.ex b/lib/ejabberd/config/opts_formatter.ex new file mode 100644 index 000000000..3d3db926f --- /dev/null +++ b/lib/ejabberd/config/opts_formatter.ex @@ -0,0 +1,46 @@ +defmodule Ejabberd.Config.OptsFormatter do + @moduledoc """ + Module for formatting options parsed into the format + ejabberd uses. + """ + + alias Ejabberd.Config.EjabberdModule + + @doc """ + Takes a keyword list with keys corresponding to + the keys requested by the ejabberd config (ex: modules: mods) + and formats them to be correctly evaluated by ejabberd. + + Look at how Config.get_ejabberd_opts/0 is constructed for + more informations. + """ + @spec format_opts_for_ejabberd(map) :: list() + def format_opts_for_ejabberd(opts) do + opts + |> format_attrs_for_ejabberd + end + + defp format_attrs_for_ejabberd({:listeners, mods}), + do: {:listen, format_listeners_for_ejabberd(mods)} + + defp format_attrs_for_ejabberd({:modules, mods}), + do: {:modules, format_mods_for_ejabberd(mods)} + + defp format_attrs_for_ejabberd({key, opts}) when is_atom(key), + do: {key, opts} + + defp format_attrs_for_ejabberd(opts), + do: (Enum.map opts, &format_attrs_for_ejabberd/1) + + defp format_mods_for_ejabberd(mods) do + Enum.map mods, fn %EjabberdModule{module: mod, attrs: attrs} -> + {mod, attrs[:opts]} + end + end + + defp format_listeners_for_ejabberd(mods) do + Enum.map mods, fn %EjabberdModule{module: mod, attrs: attrs} -> + Keyword.put(attrs[:opts], :module, mod) + end + end +end diff --git a/lib/ejabberd/config/store.ex b/lib/ejabberd/config/store.ex new file mode 100644 index 000000000..72beea64c --- /dev/null +++ b/lib/ejabberd/config/store.ex @@ -0,0 +1,55 @@ +defmodule Ejabberd.Config.Store do + @moduledoc """ + Module used for storing the modules parsed from + the configuration file. + + Example: + - Store.put(:modules, mod1) + - Store.put(:modules, mod2) + + - Store.get(:modules) :: [mod1, mod2] + + Be carefoul: when retrieving data you get them + in the order inserted into the store, which normally + is the reversed order of how the modules are specified + inside the configuration file. To resolve this just use + a Enum.reverse/1. + """ + + @name __MODULE__ + + def start_link do + Agent.start_link(fn -> %{} end, name: @name) + end + + @doc """ + Stores a value based on the key. If the key already exists, + then it inserts the new element, maintaining all the others. + It uses a list for this. + """ + @spec put(atom, any) :: :ok + def put(key, val) do + Agent.update @name, &Map.update(&1, key, [val], fn coll -> + [val | coll] + end) + end + + @doc """ + Gets a value based on the key passed. + Returns always a list. + """ + @spec get(atom) :: [any] + def get(key) do + Agent.get @name, &Map.get(&1, key, []) + end + + @doc """ + Stops the store. + It uses Agent.stop underneath, so be aware that exit + could be called. + """ + @spec stop() :: :ok + def stop do + Agent.stop @name + end +end diff --git a/lib/ejabberd/config/validator/validation.ex b/lib/ejabberd/config/validator/validation.ex new file mode 100644 index 000000000..227a3545f --- /dev/null +++ b/lib/ejabberd/config/validator/validation.ex @@ -0,0 +1,38 @@ +defmodule Ejabberd.Config.Validation do + @moduledoc """ + Module used to validate a list of modules. + """ + + alias Ejabberd.Config.EjabberdModule + alias Ejabberd.Config.Validator + + @type mod_validation :: {[EjabberdModule.t], EjabberdModule.t, map} + @type mod_validation_result :: {:ok, EjabberdModule.t} | {:error, EjabberdModule.t, map} + + @doc """ + Given a module or a list of modules it runs validators on them + and returns {:ok, mod} or {:error, mod, errors}, for each + of them. + """ + @spec validate([EjabberdModule.t] | EjabberdModule.t) :: [mod_validation_result] + def validate(modules) when is_list(modules), do: Enum.map(modules, &do_validate(modules, &1)) + def validate(module), do: validate([module]) + + # Private API + + @spec do_validate([EjabberdModule.t], EjabberdModule.t) :: mod_validation_result + defp do_validate(modules, mod) do + {modules, mod, %{}} + |> Validator.Attrs.validate + |> Validator.Dependencies.validate + |> resolve_validation_result + end + + @spec resolve_validation_result(mod_validation) :: mod_validation_result + defp resolve_validation_result({_modules, mod, errors}) do + case errors do + err when err == %{} -> {:ok, mod} + err -> {:error, mod, err} + end + end +end diff --git a/lib/ejabberd/config/validator/validator_attrs.ex b/lib/ejabberd/config/validator/validator_attrs.ex new file mode 100644 index 000000000..e0e133b61 --- /dev/null +++ b/lib/ejabberd/config/validator/validator_attrs.ex @@ -0,0 +1,29 @@ +defmodule Ejabberd.Config.Validator.Attrs do + @moduledoc """ + Validator module used to validate attributes. + """ + + import Ejabberd.Config.ValidatorUtility + alias Ejabberd.Config.Attr + alias Ejabberd.Config.EjabberdModule + + # TODO: Duplicated from validator.ex !!! + @type mod_validation :: {[EjabberdModule.t], EjabberdModule.t, map} + + @doc """ + Given a module (with the form used for validation) + it runs Attr.validate/1 on each attribute and + returns the validation tuple with the errors updated, if found. + """ + @spec validate(mod_validation) :: mod_validation + def validate({modules, mod, errors}) do + errors = Enum.reduce mod.attrs, errors, fn(attr, err) -> + case Attr.validate([attr]) do + [{:ok, _attr}] -> err + [{:error, attr, cause}] -> put_error(err, :attribute, {attr, cause}) + end + end + + {modules, mod, errors} + end +end diff --git a/lib/ejabberd/config/validator/validator_dependencies.ex b/lib/ejabberd/config/validator/validator_dependencies.ex new file mode 100644 index 000000000..4eb466663 --- /dev/null +++ b/lib/ejabberd/config/validator/validator_dependencies.ex @@ -0,0 +1,32 @@ +defmodule Ejabberd.Config.Validator.Dependencies do + @moduledoc """ + Validator module used to validate dependencies specified + with the @dependency annotation. + """ + + alias Ejabberd.Config.EjabberdModule + + # TODO: Duplicated from validator.ex !!! + @type mod_validation :: {[EjabberdModule.t], EjabberdModule.t, map} + import Ejabberd.Config.ValidatorUtility + + @doc """ + Given a module (with the form used for validation) + it checks if the @dependency annotation is respected and + returns the validation tuple with the errors updated, if found. + """ + @spec validate(mod_validation) :: mod_validation + def validate({modules, mod, errors}) do + module_names = extract_module_names(modules) + dependencies = mod.attrs[:dependency] + + errors = Enum.reduce dependencies, errors, fn(req_module, err) -> + case req_module in module_names do + true -> err + false -> put_error(err, :dependency, {req_module, :not_found}) + end + end + + {modules, mod, errors} + end +end diff --git a/lib/ejabberd/config/validator/validator_utility.ex b/lib/ejabberd/config/validator/validator_utility.ex new file mode 100644 index 000000000..6047618b6 --- /dev/null +++ b/lib/ejabberd/config/validator/validator_utility.ex @@ -0,0 +1,27 @@ +defmodule Ejabberd.Config.ValidatorUtility do + @moduledoc """ + Module used as a base validator for validation modules. + Imports utility functions for working with validation structures. + """ + + @doc """ + Inserts an error inside the errors collection, for the given key. + If the key doesn't exists then it creates an empty collection + and inserts the value passed. + """ + @spec put_error(map, atom, any) :: map + def put_error(errors, key, val) do + Map.update errors, key, [val], fn coll -> + [val | coll] + end + end + + @doc """ + Given a list of modules it extracts and returns a list + of the module names (which are Elixir.Module). + """ + def extract_module_names(modules) when is_list(modules) do + modules + |> Enum.map(&Map.get(&1, :module)) + end +end diff --git a/lib/ejabberd/config_util.ex b/lib/ejabberd/config_util.ex new file mode 100644 index 000000000..71d854f15 --- /dev/null +++ b/lib/ejabberd/config_util.ex @@ -0,0 +1,18 @@ +defmodule Ejabberd.ConfigUtil do + @moduledoc """ + Module containing utility functions for + the config file. + """ + + @doc """ + Returns true when the config file is based on elixir. + """ + @spec is_elixir_config(binary) :: boolean + def is_elixir_config(filename) when is_list(filename) do + is_elixir_config(to_string(filename)) + end + + def is_elixir_config(filename) do + String.ends_with?(filename, "exs") + end +end diff --git a/lib/Ejabberd/hooks.ex b/lib/ejabberd/hooks.ex similarity index 100% rename from lib/Ejabberd/hooks.ex rename to lib/ejabberd/hooks.ex diff --git a/lib/ejabberd/logger.ex b/lib/ejabberd/logger.ex new file mode 100644 index 000000000..3f97b4ccc --- /dev/null +++ b/lib/ejabberd/logger.ex @@ -0,0 +1,9 @@ +defmodule Ejabberd.Logger do + + def critical(message, args \\ []), do: :logger.critical(message, args) + def error(message, args \\ []), do: :logger.error(message, args) + def warning(message, args \\ []), do: :logger.warning(message, args) + def info(message, args \\ []), do: :logger.info(message, args) + def debug(message, args \\ []), do: :logger.debug( message, args) + +end diff --git a/lib/ejabberd_auth_example.ex b/lib/ejabberd_auth_example.ex new file mode 100644 index 000000000..5bc37e093 --- /dev/null +++ b/lib/ejabberd_auth_example.ex @@ -0,0 +1,44 @@ +defmodule Ejabberd.Auth.Example do + + @moduledoc """ + Example ejabberd auth method written in Elixir. + + This is an example to demonstrate the usage of Elixir to + create ejabberd auth methods. + + Example configuration: + auth_method: 'Ejabberd.Auth.Example' + """ + + @behaviour :ejabberd_auth + import Ejabberd.Logger + + @impl true + def start(host) do + info("Starting Ejabberd.Auth.Example to authenticate '#{host}' users") + nil + end + + @impl true + def stop(host) do + info("Stopping Ejabberd.Auth.Example to authenticate '#{host}' users") + nil + end + + @impl true + def check_password("alice", _authz_id, _host, "secret"), do: {:nocache, true} + def check_password(_username, _authz_id, _host, _secret), do: {:nocache, false} + + @impl true + def user_exists("alice", _host), do: {:nocache, true} + def user_exists(_username, _host), do: {:nocache, false} + + @impl true + def plain_password_required(_binary), do: true + + @impl true + def store_type(_host), do: :external + + @impl true + def use_cache(_host), do: false +end diff --git a/lib/mix/tasks/deps.tree.ex b/lib/mix/tasks/deps.tree.ex new file mode 100644 index 000000000..e93b4aa48 --- /dev/null +++ b/lib/mix/tasks/deps.tree.ex @@ -0,0 +1,94 @@ +defmodule Mix.Tasks.Ejabberd.Deps.Tree do + use Mix.Task + + alias Ejabberd.Config.EjabberdModule + + @shortdoc "Lists all ejabberd modules and their dependencies" + + @moduledoc """ + Lists all ejabberd modules and their dependencies. + + The project must have ejabberd as a dependency. + """ + + def run(_argv) do + # First we need to start manually the store to be available + # during the compilation of the config file. + Ejabberd.Config.Store.start_link() + Ejabberd.Config.init(:ejabberd_config.path()) + + Mix.shell().info "ejabberd modules" + + Ejabberd.Config.Store.get(:modules) + |> Enum.reverse # Because of how mods are stored inside the store + |> format_mods + |> Mix.shell().info + end + + defp format_mods(mods) when is_list(mods) do + deps_tree = build_dependency_tree(mods) + mods_used_as_dependency = get_mods_used_as_dependency(deps_tree) + + keep_only_mods_not_used_as_dep(deps_tree, mods_used_as_dependency) + |> format_mods_into_string + end + + defp build_dependency_tree(mods) do + Enum.map mods, fn %EjabberdModule{module: mod, attrs: attrs} -> + deps = attrs[:dependency] + build_dependency_tree(mods, mod, deps) + end + end + + defp build_dependency_tree(_mods, mod, []), do: %{module: mod, dependency: []} + defp build_dependency_tree(mods, mod, deps) when is_list(deps) do + dependencies = Enum.map deps, fn dep -> + dep_deps = get_dependencies_of_mod(mods, dep) + build_dependency_tree(mods, dep, dep_deps) + end + + %{module: mod, dependency: dependencies} + end + + defp get_mods_used_as_dependency(mods) when is_list(mods) do + Enum.reduce mods, [], fn(mod, acc) -> + case mod do + %{dependency: []} -> acc + %{dependency: deps} -> get_mod_names(deps) ++ acc + end + end + end + + defp get_mod_names([]), do: [] + defp get_mod_names(mods) when is_list(mods), do: Enum.map(mods, &get_mod_names/1) |> List.flatten + defp get_mod_names(%{module: mod, dependency: deps}), do: [mod | get_mod_names(deps)] + + defp keep_only_mods_not_used_as_dep(mods, mods_used_as_dep) do + Enum.filter mods, fn %{module: mod} -> + not (mod in mods_used_as_dep) + end + end + + defp get_dependencies_of_mod(deps, mod_name) do + Enum.find(deps, &(Map.get(&1, :module) == mod_name)) + |> Map.get(:attrs) + |> Keyword.get(:dependency) + end + + defp format_mods_into_string(mods), do: format_mods_into_string(mods, 0) + defp format_mods_into_string([], _indentation), do: "" + defp format_mods_into_string(mods, indentation) when is_list(mods) do + Enum.reduce mods, "", fn(mod, acc) -> + acc <> format_mods_into_string(mod, indentation) + end + end + + defp format_mods_into_string(%{module: mod, dependency: deps}, 0) do + "\n├── #{mod}" <> format_mods_into_string(deps, 2) + end + + defp format_mods_into_string(%{module: mod, dependency: deps}, indentation) do + spaces = Enum.reduce 0..indentation, "", fn(_, acc) -> " " <> acc end + "\n│#{spaces}└── #{mod}" <> format_mods_into_string(deps, indentation + 4) + end +end diff --git a/lib/mod_example.ex b/lib/mod_example.ex new file mode 100644 index 000000000..12166810a --- /dev/null +++ b/lib/mod_example.ex @@ -0,0 +1,46 @@ +defmodule Ejabberd.Module.Example do + + @moduledoc """ + Example ejabberd module written in Elixir. + + This is an example to demonstrate the usage of Elixir to + create ejabberd modules. + + Example configuration: + modules: + 'Ejabberd.Module.Example': {} + """ + + @behaviour :gen_mod + import Ejabberd.Logger + + def start(host, _opts) do + info("Starting Ejabberd.Module.Example for host '#{host}'") + Ejabberd.Hooks.add(:set_presence_hook, host, __MODULE__, :on_presence, 50) + :ok + end + + def stop(host) do + info("Stopping Ejabberd.Module.Example for host '#{host}'") + Ejabberd.Hooks.delete(:set_presence_hook, host, __MODULE__, :on_presence, 50) + :ok + end + + def on_presence(user, _server, _resource, _packet) do + info("Receive presence for #{user}") + :none + end + + def depends(_host, _opts) do + [] + end + + def mod_options(_host) do + [] + end + + def mod_doc() do + %{:desc => "This is just a demonstration."} + end + +end diff --git a/lib/mod_presence_demo.ex b/lib/mod_presence_demo.ex deleted file mode 100644 index 89fc60d87..000000000 --- a/lib/mod_presence_demo.ex +++ /dev/null @@ -1,21 +0,0 @@ -defmodule ModPresenceDemo do - import Ejabberd.Logger # this allow using info, error, etc for logging - @behaviour :gen_mod - - def start(host, _opts) do - info('Starting ejabberd module Presence Demo') - Ejabberd.Hooks.add(:set_presence_hook, host, __ENV__.module, :on_presence, 50) - :ok - end - - def stop(host) do - info('Stopping ejabberd module Presence Demo') - Ejabberd.Hooks.delete(:set_presence_hook, host, __ENV__.module, :on_presence, 50) - :ok - end - - def on_presence(user, _server, _resource, _packet) do - info('Receive presence for #{user}') - :none - end -end diff --git a/m4/ax_lib_sqlite3.m4 b/m4/ax_lib_sqlite3.m4 new file mode 100644 index 000000000..77717167f --- /dev/null +++ b/m4/ax_lib_sqlite3.m4 @@ -0,0 +1,156 @@ +# =========================================================================== +# http://www.gnu.org/software/autoconf-archive/ax_lib_sqlite3.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_LIB_SQLITE3([MINIMUM-VERSION]) +# +# DESCRIPTION +# +# Test for the SQLite 3 library of a particular version (or newer) +# +# This macro takes only one optional argument, required version of SQLite +# 3 library. If required version is not passed, 3.0.0 is used in the test +# of existance of SQLite 3. +# +# If no intallation prefix to the installed SQLite library is given the +# macro searches under /usr, /usr/local, and /opt. +# +# This macro calls: +# +# AC_SUBST(SQLITE3_CFLAGS) +# AC_SUBST(SQLITE3_LDFLAGS) +# AC_SUBST(SQLITE3_VERSION) +# +# And sets: +# +# HAVE_SQLITE3 +# +# LICENSE +# +# Copyright (c) 2008 Mateusz Loskot +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice +# and this notice are preserved. This file is offered as-is, without any +# warranty. + +#serial 14 + +AC_DEFUN([AX_LIB_SQLITE3], +[ + AC_ARG_WITH([sqlite3], + AS_HELP_STRING( + [--with-sqlite3=@<:@ARG@:>@], + [use SQLite 3 library @<:@default=yes@:>@, optionally specify the prefix for sqlite3 library] + ), + [ + if test "$withval" = "no"; then + WANT_SQLITE3="no" + elif test "$withval" = "yes"; then + WANT_SQLITE3="yes" + ac_sqlite3_path="" + else + WANT_SQLITE3="yes" + ac_sqlite3_path="$withval" + fi + ], + [WANT_SQLITE3="yes"] + ) + + SQLITE3_CFLAGS="" + SQLITE3_LDFLAGS="" + SQLITE3_VERSION="" + + if test "x$WANT_SQLITE3" = "xyes"; then + + ac_sqlite3_header="sqlite3.h" + + sqlite3_version_req=ifelse([$1], [], [3.0.0], [$1]) + sqlite3_version_req_shorten=`expr $sqlite3_version_req : '\([[0-9]]*\.[[0-9]]*\)'` + sqlite3_version_req_major=`expr $sqlite3_version_req : '\([[0-9]]*\)'` + sqlite3_version_req_minor=`expr $sqlite3_version_req : '[[0-9]]*\.\([[0-9]]*\)'` + sqlite3_version_req_micro=`expr $sqlite3_version_req : '[[0-9]]*\.[[0-9]]*\.\([[0-9]]*\)'` + if test "x$sqlite3_version_req_micro" = "x" ; then + sqlite3_version_req_micro="0" + fi + + sqlite3_version_req_number=`expr $sqlite3_version_req_major \* 1000000 \ + \+ $sqlite3_version_req_minor \* 1000 \ + \+ $sqlite3_version_req_micro` + + AC_MSG_CHECKING([for SQLite3 library >= $sqlite3_version_req]) + + if test "$ac_sqlite3_path" != ""; then + ac_sqlite3_ldflags="-L$ac_sqlite3_path/lib" + ac_sqlite3_cppflags="-I$ac_sqlite3_path/include" + else + for ac_sqlite3_path_tmp in /usr /usr/local /opt ; do + if test -f "$ac_sqlite3_path_tmp/include/$ac_sqlite3_header" \ + && test -r "$ac_sqlite3_path_tmp/include/$ac_sqlite3_header"; then + ac_sqlite3_path=$ac_sqlite3_path_tmp + ac_sqlite3_cppflags="-I$ac_sqlite3_path_tmp/include" + ac_sqlite3_ldflags="-L$ac_sqlite3_path_tmp/lib" + break; + fi + done + fi + + ac_sqlite3_ldflags="$ac_sqlite3_ldflags -lsqlite3" + + saved_CPPFLAGS="$CPPFLAGS" + CPPFLAGS="$CPPFLAGS $ac_sqlite3_cppflags" + + AC_LANG_PUSH(C) + AC_COMPILE_IFELSE( + [ + AC_LANG_PROGRAM([[@%:@include ]], + [[ +#if (SQLITE_VERSION_NUMBER >= $sqlite3_version_req_number) +/* Everything is okay */ +#else +# error SQLite version is too old +#endif + ]] + ) + ], + [ + AC_MSG_RESULT([yes]) + success="yes" + ], + [ + AC_MSG_RESULT([not found]) + success="no" + ] + ) + AC_LANG_POP(C) + + CPPFLAGS="$saved_CPPFLAGS" + + if test "$success" = "yes"; then + + SQLITE3_CFLAGS="$ac_sqlite3_cppflags" + SQLITE3_LDFLAGS="$ac_sqlite3_ldflags" + + ac_sqlite3_header_path="$ac_sqlite3_path/include/$ac_sqlite3_header" + + dnl Retrieve SQLite release version + if test "x$ac_sqlite3_header_path" != "x"; then + ac_sqlite3_version=`cat $ac_sqlite3_header_path \ + | grep '#define.*SQLITE_VERSION.*\"' | sed -e 's/.* "//' \ + | sed -e 's/"//'` + if test $ac_sqlite3_version != ""; then + SQLITE3_VERSION=$ac_sqlite3_version + else + AC_MSG_WARN([Cannot find SQLITE_VERSION macro in sqlite3.h header to retrieve SQLite version!]) + fi + fi + + AC_SUBST(SQLITE3_CFLAGS) + AC_SUBST(SQLITE3_LDFLAGS) + AC_SUBST(SQLITE3_VERSION) + AC_DEFINE([HAVE_SQLITE3], [], [Have the SQLITE3 library]) + fi + fi +]) diff --git a/m4/erlang-extra.m4 b/m4/erlang-extra.m4 index 4a7311bad..95c4b138d 100644 --- a/m4/erlang-extra.m4 +++ b/m4/erlang-extra.m4 @@ -75,7 +75,7 @@ EOF if test "x`cat conftest.out`" != "xok"; then AC_MSG_RESULT([failed]) X="`cat conftest.out`" - if test "[$3]" == "warn"; then + if test "[$3]" = "warn"; then AC_MSG_WARN([$X]) else AC_MSG_FAILURE([$X]) diff --git a/man/ejabberd.yml.5 b/man/ejabberd.yml.5 new file mode 100644 index 000000000..aa42e20b2 --- /dev/null +++ b/man/ejabberd.yml.5 @@ -0,0 +1,8890 @@ +'\" t +.\" Title: ejabberd.yml +.\" Author: [see the "AUTHOR" section] +.\" Generator: DocBook XSL Stylesheets vsnapshot +.\" Date: 08/22/2025 +.\" Manual: \ \& +.\" Source: \ \& +.\" Language: English +.\" +.TH "EJABBERD\&.YML" "5" "08/22/2025" "\ \&" "\ \&" +.\" ----------------------------------------------------------------- +.\" * Define some portability stuff +.\" ----------------------------------------------------------------- +.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.\" http://bugs.debian.org/507673 +.\" http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html +.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.\" ----------------------------------------------------------------- +.\" * set default formatting +.\" ----------------------------------------------------------------- +.\" disable hyphenation +.nh +.\" disable justification (adjust text to left margin only) +.ad l +.\" ----------------------------------------------------------------- +.\" * MAIN CONTENT STARTS HERE * +.\" ----------------------------------------------------------------- +.SH "NAME" +ejabberd.yml \- main configuration file for ejabberd\&. +.SH "SYNOPSIS" +.sp +ejabberd\&.yml +.SH "DESCRIPTION" +.sp +The configuration file is written in YAML language\&. +.if n \{\ +.sp +.\} +.RS 4 +.it 1 an-trap +.nr an-no-space-flag 1 +.nr an-break-flag 1 +.br +.ps +1 +\fBWarning\fR +.ps -1 +.br +.sp +YAML is indentation sensitive, so make sure you respect indentation, or otherwise you will get pretty cryptic configuration errors\&. +.sp .5v +.RE +.sp +Logically, configuration options are split into 3 main categories: \fIModules\fR, \fIListeners\fR and everything else called \fITop Level\fR options\&. Thus this document is split into 3 main chapters describing each category separately\&. So, the contents of ejabberd\&.yml will typically look like this: +.sp +.if n \{\ +.RS 4 +.\} +.nf +hosts: + \- example\&.com + \- domain\&.tld +loglevel: info +\&.\&.\&. +listen: + \- + port: 5222 + module: ejabberd_c2s + \&.\&.\&. +modules: + mod_roster: {} + \&.\&.\&. +.fi +.if n \{\ +.RE +.\} +.sp +Any configuration error (such as syntax error, unknown option or invalid option value) is fatal in the sense that ejabberd will refuse to load the whole configuration file and will not start or will abort configuration reload\&. +.sp +All options can be changed in runtime by running \fIejabberdctl reload\-config\fR command\&. Configuration reload is atomic: either all options are accepted and applied simultaneously or the new configuration is refused without any impact on currently running configuration\&. +.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/25\&.08/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" +.sp +This section describes top level options of ejabberd 25\&.08\&. The options that changed in this version are marked with 🟤\&. +.PP +\fBaccess_rules\fR: \fI{AccessName: {allow|deny: ACLName|ACLDefinition}}\fR +.RS 4 +This option defines +\fIbasic\&.md#access\-rules|Access Rules\fR\&. Each access rule is assigned a name that can be referenced from other parts of the configuration file (mostly from +\fIaccess\fR +options of ejabberd modules)\&. Each rule definition may contain arbitrary number of +\fIallow\fR +or +\fIdeny\fR +sections, and each section may contain any number of ACL rules (see +\fIacl\fR +option)\&. There are no access rules defined by default\&. +.sp +\fBExample\fR: +.sp +.if n \{\ +.RS 4 +.\} +.nf +access_rules: + configure: + allow: admin + something: + deny: someone + allow: all + s2s_banned: + deny: problematic_hosts + deny: banned_forever + deny: + ip: 222\&.111\&.222\&.111/32 + deny: + ip: 111\&.222\&.111\&.222/32 + allow: all + xmlrpc_access: + allow: + user: peter@example\&.com + allow: + user: ivone@example\&.com + allow: + user: bot@example\&.com + ip: 10\&.0\&.0\&.0/24 +.fi +.if n \{\ +.RE +.\} +.RE +.PP +\fBacl\fR: \fI{ACLName: {ACLType: ACLValue}}\fR +.RS 4 +This option defines +\fI\&.\&./configuration/basic\&.md#acl|access control lists\fR: named sets of rules which are used to match against different targets (such as a JID or an IP address)\&. Every set of rules has name +\fIACLName\fR: it can be any string except +\fIall\fR +or +\fInone\fR +(those are predefined names for the rules that match all or nothing respectively)\&. The name +\fIACLName\fR +can be referenced from other parts of the configuration file, for example in +\fIaccess_rules\fR +option\&. The rules of +\fIACLName\fR +are represented by mapping +\fI{ACLType: ACLValue}\fR\&. These can be one of the following: +.PP +\fBip\fR: \fINetwork\fR +.RS 4 +The rule matches any IP address from the +\fINetwork\fR\&. +.RE +.PP +\fBnode_glob\fR: \fIPattern\fR +.RS 4 +Same as +\fInode_regexp\fR, but matching is performed on a specified +\fIPattern\fR +according to the rules used by the Unix shell\&. +.RE +.PP +\fBnode_regexp\fR: \fIuser_regexp@server_regexp\fR +.RS 4 +The rule matches any JID with node part matching regular expression +\fIuser_regexp\fR +and server part matching regular expression +\fIserver_regexp\fR\&. +.RE +.PP +\fBresource\fR: \fIResource\fR +.RS 4 +The rule matches any JID with a resource +\fIResource\fR\&. +.RE +.PP +\fBresource_glob\fR: \fIPattern\fR +.RS 4 +Same as +\fIresource_regexp\fR, but matching is performed on a specified +\fIPattern\fR +according to the rules used by the Unix shell\&. +.RE +.PP +\fBresource_regexp\fR: \fIRegexp\fR +.RS 4 +The rule matches any JID with a resource that matches regular expression +\fIRegexp\fR\&. +.RE +.PP +\fBserver\fR: \fIServer\fR +.RS 4 +The rule matches any JID from server +\fIServer\fR\&. The value of +\fIServer\fR +must be a valid hostname or an IP address\&. +.RE +.PP +\fBserver_glob\fR: \fIPattern\fR +.RS 4 +Same as +\fIserver_regexp\fR, but matching is performed on a specified +\fIPattern\fR +according to the rules used by the Unix shell\&. +.RE +.PP +\fBserver_regexp\fR: \fIRegexp\fR +.RS 4 +The rule matches any JID from the server that matches regular expression +\fIRegexp\fR\&. +.RE +.PP +\fBuser\fR: \fIUsername\fR +.RS 4 +If +\fIUsername\fR +is in the form of "user@server", the rule matches a JID against this value\&. Otherwise, if +\fIUsername\fR +is in the form of "user", the rule matches any JID that has +\fIUsername\fR +in the node part as long as the server part of this JID is any virtual host served by ejabberd\&. +.RE +.PP +\fBuser_glob\fR: \fIPattern\fR +.RS 4 +Same as +\fIuser_regexp\fR, but matching is performed on a specified +\fIPattern\fR +according to the rules used by the Unix shell\&. +.RE +.PP +\fBuser_regexp\fR: \fIRegexp\fR +.RS 4 +If +\fIRegexp\fR +is in the form of "regexp@server", the rule matches any JID with node part matching regular expression "regexp" as long as the server part of this JID is equal to "server"\&. If +\fIRegexp\fR +is in the form of "regexp", the rule matches any JID with node part matching regular expression "regexp" as long as the server part of this JID is any virtual host served by ejabberd\&. +.RE +.RE +.PP +\fBacme\fR: \fIOptions\fR +.RS 4 +\fIbasic\&.md#acme|ACME\fR +configuration, to automatically obtain SSL certificates for the domains served by ejabberd, which means that certificate requests and renewals are performed to some CA server (aka "ACME server") in a fully automated mode\&. The +\fIOptions\fR +are: +.PP +\fBauto\fR: \fItrue | false\fR +.RS 4 +Whether to automatically request certificates for all configured domains (that yet have no a certificate) on server start or configuration reload\&. The default is +\fItrue\fR\&. +.RE +.PP +\fBca_url\fR: \fIURL\fR +.RS 4 +The ACME directory URL used as an entry point for the ACME server\&. The default value is +https://acme\-v02\&.api\&.letsencrypt\&.org/directory +\- the directory URL of Let\(cqs Encrypt authority\&. +.RE +.PP +\fBcert_type\fR: \fIrsa | ec\fR +.RS 4 +A type of a certificate key\&. Available values are +\fIec\fR +and +\fIrsa\fR +for EC and RSA certificates respectively\&. It\(cqs better to have RSA certificates for the purpose of backward compatibility with legacy clients and servers, thus the default is +\fIrsa\fR\&. +.RE +.PP +\fBcontact\fR: \fI[Contact, \&.\&.\&.]\fR +.RS 4 +A list of contact addresses (typically emails) where an ACME server will send notifications when problems occur\&. The value of +\fIContact\fR +must be in the form of "scheme:address" (e\&.g\&. "mailto:user@domain\&.tld")\&. The default is an empty list which means an ACME server will send no notices\&. +.RE +.sp +\fBExample\fR: +.sp +.if n \{\ +.RS 4 +.\} +.nf +acme: + ca_url: https://acme\-v02\&.api\&.letsencrypt\&.org/directory + contact: + \- mailto:admin@domain\&.tld + \- mailto:bot@domain\&.tld + auto: true + cert_type: rsa +.fi +.if n \{\ +.RE +.\} +.RE +.PP +\fBallow_contrib_modules\fR: \fItrue | false\fR +.RS 4 +Whether to allow installation of third\-party modules or not\&. See +\fI\&.\&./\&.\&./admin/guide/modules\&.md#ejabberd\-contrib|ejabberd\-contrib\fR +documentation section\&. The default value is +\fItrue\fR\&. +.RE +.PP +\fBallow_multiple_connections\fR: \fItrue | false\fR +.RS 4 +This option is only used when the anonymous mode is enabled\&. Setting it to +\fItrue\fR +means that the same username can be taken multiple times in anonymous login mode if different resource are used to connect\&. This option is only useful in very special occasions\&. The default value is +\fIfalse\fR\&. +.RE +.PP +\fBanonymous_protocol\fR: \fIlogin_anon | sasl_anon | both\fR +.RS 4 +Define what +\fIauthentication\&.md#anonymous\-login\-and\-sasl\-anonymous|anonymous\fR +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\&. +.RE +.RE +.sp +The default value is \fIsasl_anon\fR\&. +.PP +\fBapi_permissions\fR: \fI[Permission, \&.\&.\&.]\fR +.RS 4 +Define the permissions for API access\&. Please consult the ejabberd Docs web → For Developers → ejabberd ReST API → +\fI\&.\&./\&.\&./developer/ejabberd\-api/permissions\&.md|API Permissions\fR\&. +.RE +.PP +\fBappend_host_config\fR: \fI{Host: Options}\fR +.RS 4 +Add a few specific options to a certain +\fI\&.\&./configuration/basic\&.md#virtual\-hosting|virtual host\fR\&. +.RE +.PP +\fBauth_cache_life_time\fR: \fItimeout()\fR +.RS 4 +Same as +\fIcache_life_time\fR, but applied to authentication cache only\&. If not set, the value from +\fIcache_life_time\fR +will be used\&. +.RE +.PP +\fBauth_cache_missed\fR: \fItrue | false\fR +.RS 4 +Same as +\fIcache_missed\fR, but applied to authentication cache only\&. If not set, the value from +\fIcache_missed\fR +will be used\&. +.RE +.PP +\fBauth_cache_size\fR: \fIpos_integer() | infinity\fR +.RS 4 +Same as +\fIcache_size\fR, but applied to authentication cache only\&. If not set, the value from +\fIcache_size\fR +will be used\&. +.RE +.PP +\fBauth_external_user_exists_check\fR: \fItrue | false\fR +.RS 4 +\fINote\fR +about this option: added in 23\&.10\&. Supplement check for user existence based on +\fImod_last\fR +data, for authentication methods that don\(cqt have a way to reliably tell if a user exists (like is the case for +\fIjwt\fR +and certificate based authentication)\&. This helps with processing offline message for those users\&. The default value is +\fItrue\fR\&. +.RE +.PP +\fBauth_method\fR: \fI[mnesia | sql | anonymous | external | jwt | ldap | pam, \&.\&.\&.]\fR +.RS 4 +A list of +\fIauthentication\&.md|authentication\fR +methods to use\&. If several methods are defined, authentication is considered successful as long as authentication of at least one of the methods succeeds\&. The default value is +\fI[mnesia]\fR\&. +.RE +.PP +\fBauth_opts\fR: \fI[Option, \&.\&.\&.]\fR +.RS 4 +This is used by the contributed module +\fIejabberd_auth_http\fR +that can be installed from the +\fI\&.\&./\&.\&./admin/guide/modules\&.md#ejabberd\-contrib|ejabberd\-contrib\fR +Git repository\&. Please refer to that module\(cqs README file for details\&. +.RE +.PP +\fBauth_password_format\fR: \fIplain | scram\fR +.RS 4 +\fINote\fR +about this option: improved in 20\&.01\&. The option defines in what format the users passwords are stored, plain text or in +\fIauthentication\&.md#scram|SCRAM\fR +format: +.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/256/512(\-PLUS)\&. +.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 required 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/256/512(\-PLUS)\&. The SCRAM variant depends on the +\fIauth_scram_hash\fR +option\&. +.RE +.RE +.sp +The default value is \fIplain\fR\&. +.PP +\fBauth_password_types_hidden_in_sasl1\fR: \fI[plain | scram_sha1 | scram_sha256 | scram_sha512]\fR +.RS 4 +\fINote\fR +about this option: added in 25\&.07\&. List of password types that should not be offered in SASL1 authenticatication\&. Because SASL1, unlike SASL2, can\(cqt have list of available mechanisms tailored to individual user, it\(cqs possible that offered mechanisms will not be compatible with stored password, especially if new password type was added recently\&. This option allows disabling offering some mechanisms in SASL1, to a time until new password type will be available for all users\&. +.RE +.PP +\fBauth_scram_hash\fR: \fIsha | sha256 | sha512\fR +.RS 4 +Hash algorithm that should be used to store password in +\fIauthentication\&.md#scram|SCRAM\fR +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_stored_password_types\fR: \fI[plain | scram_sha1 | scram_sha256 | scram_sha512]\fR +.RS 4 +\fINote\fR +about this option: added in 25\&.03\&. List of password types that should be stored simultaneously for each user in database\&. When the user sets the account password, database will be updated to store the password in formats compatible with each type listed here\&. This can be used to migrate user passwords to a more secure format\&. If this option if set, it will override values set in +\fIauth_scram_hash\fR +and +\fIauth_password_format\fR +options\&. The default value is +[]\&. +.RE +.PP +\fBauth_use_cache\fR: \fItrue | false\fR +.RS 4 +Same as +\fIuse_cache\fR, but applied to authentication cache only\&. If not set, the value from +\fIuse_cache\fR +will be used\&. +.RE +.PP +\fBc2s_cafile\fR: \fIPath\fR +.RS 4 +Full path to a file containing one or more CA certificates in PEM format\&. All client certificates should be signed by one of these root CA certificates and should contain the corresponding JID(s) in +\fIsubjectAltName\fR +field\&. There is no default value\&. +.RE +.sp +You can use \fIhost_config\fR to specify this option per\-vhost\&. +.sp +To set a specific file per listener, use the listener\(cqs \fIlisten\-options\&.md#cafile|cafile\fR option\&. Please notice that \fIc2s_cafile\fR overrides the listener\(cqs \fIcafile\fR option\&. +.PP +\fBc2s_ciphers\fR: \fI[Cipher, \&.\&.\&.]\fR +.RS 4 +A list of OpenSSL ciphers to use for c2s connections\&. The default value is shown in the example below: +.sp +\fBExample\fR: +.sp +.if n \{\ +.RS 4 +.\} +.nf +c2s_ciphers: + \- HIGH + \- "!aNULL" + \- "!eNULL" + \- "!3DES" + \- "@STRENGTH" +.fi +.if n \{\ +.RE +.\} +.RE +.PP +\fBc2s_dhfile\fR: \fIPath\fR +.RS 4 +Full path to a file containing custom DH parameters to use for c2s connections\&. Such a file could be created with the command +\fI"openssl dhparam \-out dh\&.pem 2048"\fR\&. If this option is not specified, 2048\-bit MODP Group with 256\-bit Prime Order Subgroup will be used as defined in RFC5114 Section 2\&.3\&. +.RE +.PP +\fBc2s_protocol_options\fR: \fI[Option, \&.\&.\&.]\fR +.RS 4 +List of general SSL options to use for c2s connections\&. These map to OpenSSL\(cqs +\fIset_options()\fR\&. The default value is shown in the example below: +.sp +\fBExample\fR: +.sp +.if n \{\ +.RS 4 +.\} +.nf +c2s_protocol_options: + \- no_sslv3 + \- cipher_server_preference + \- no_compression +.fi +.if n \{\ +.RE +.\} +.RE +.PP +\fBc2s_tls_compression\fR: \fItrue | false\fR +.RS 4 +Whether to enable or disable TLS compression for c2s connections\&. The default value is +\fIfalse\fR\&. +.RE +.PP +\fBca_file\fR: \fIPath\fR +.RS 4 +Path to a file of CA root certificates\&. The default is to use system defined file if possible\&. +.RE +.sp +For server connections, this \fIca_file\fR option is overridden by the \fIs2s_cafile\fR option\&. +.PP +\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 +\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 +\fIsm_cache_life_time\fR\&. +.RE +.PP +\fBcache_missed\fR: \fItrue | false\fR +.RS 4 +Whether or not to cache missed lookups\&. When there is an attempt to lookup for a value in a database and this value is not found and the option is set to +\fItrue\fR, this attempt will be cached and no attempts will be performed until the cache expires (see +\fIcache_life_time\fR)\&. Usually you don\(cqt want to change it\&. Default is +\fItrue\fR\&. Several modules have a similar option; and some core ejabberd parts support similar options too, see +\fIauth_cache_missed\fR, +\fIoauth_cache_missed\fR, +\fIrouter_cache_missed\fR, and +\fIsm_cache_missed\fR\&. +.RE +.PP +\fBcache_size\fR: \fIpos_integer() | infinity\fR +.RS 4 +A maximum number of items (not memory!) in cache\&. The rule of thumb, for all tables except rosters, you should set it to the number of maximum online users you expect\&. For roster multiply this number by 20 or so\&. If the cache size reaches this threshold, it\(cqs fully cleared, i\&.e\&. all items are deleted, and the corresponding warning is logged\&. You should avoid frequent cache clearance, because this degrades performance\&. The default value is +\fI1000\fR\&. Several modules have a similar option; and some core ejabberd parts support similar options too, see +\fIauth_cache_size\fR, +\fIoauth_cache_size\fR, +\fIrouter_cache_size\fR, and +\fIsm_cache_size\fR\&. +.RE +.PP +\fBcaptcha_cmd\fR: \fIPath | ModuleName\fR +.RS 4 +\fINote\fR +about this option: improved in 23\&.01\&. Full path to a script that generates +\fIbasic\&.md#captcha|CAPTCHA\fR +images\&. The keyword +\fI@VERSION@\fR +is replaced with ejabberd version number in +\fIXX\&.YY\fR +format\&. The keyword +\fI@SEMVER@\fR +is replaced with ejabberd version number in semver format when compiled with Elixir\(cqs mix, or XX\&.YY format otherwise\&. Alternatively, it can be the name of a module that implements ejabberd CAPTCHA support\&. There is no default value: when this option is not set, CAPTCHA functionality is completely disabled\&. +.sp +\fBExamples\fR: +.sp +When using the ejabberd installers or container image, the example captcha scripts can be used like this: +.sp +.if n \{\ +.RS 4 +.\} +.nf +captcha_cmd: /opt/ejabberd\-@VERSION@/lib/ejabberd\-@SEMVER@/priv/bin/captcha\&.sh +.fi +.if n \{\ +.RE +.\} +.RE +.PP +\fBcaptcha_host\fR: \fIString\fR +.RS 4 +Deprecated\&. Use +\fIcaptcha_url\fR +instead\&. +.RE +.PP +\fBcaptcha_limit\fR: \fIpos_integer() | infinity\fR +.RS 4 +Maximum number of +\fIbasic\&.md#captcha|CAPTCHA\fR +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 | auto | undefined\fR +.RS 4 +\fINote\fR +about this option: improved in 23\&.04\&. An URL where +\fIbasic\&.md#captcha|CAPTCHA\fR +requests should be sent\&. NOTE: you need to configure +\fIrequest_handlers\fR +for +\fIejabberd_http\fR +listener as well\&. If set to +\fIauto\fR, it builds the URL using a +\fIrequest_handler\fR +already enabled, with encryption if available\&. If set to +\fIundefined\fR, it builds the URL using the deprecated +\fIcaptcha_host\fR +\fI+ /captcha\fR\&. The default value is +\fIauto\fR\&. +.RE +.PP +\fBcertfiles\fR: \fI[Path, \&.\&.\&.]\fR +.RS 4 +The option accepts a list of file paths (optionally with wildcards) containing either PEM certificates or PEM private keys\&. At startup or configuration reload, ejabberd reads all certificates from these files, sorts them, removes duplicates, finds matching private keys and then rebuilds full certificate chains for the use in TLS connections\&. Use this option when TLS is enabled in either of ejabberd listeners: +\fIejabberd_c2s\fR, +\fIejabberd_http\fR +and so on\&. NOTE: if you modify the certificate files or change the value of the option, run +\fIejabberdctl reload\-config\fR +in order to rebuild and reload the certificate chains\&. +.sp +\fBExamples\fR: +.sp +If you use +Let\(cqs Encrypt +certificates for your domain "domain\&.tld", the configuration will look like this: +.sp +.if n \{\ +.RS 4 +.\} +.nf +certfiles: + \- /etc/letsencrypt/live/domain\&.tld/fullchain\&.pem + \- /etc/letsencrypt/live/domain\&.tld/privkey\&.pem +.fi +.if n \{\ +.RE +.\} +.RE +.PP +\fBcluster_backend\fR: \fIBackend\fR +.RS 4 +A database backend to use for storing information about cluster\&. The only available value so far is +\fImnesia\fR\&. +.RE +.PP +\fBcluster_nodes\fR: \fI[Node, \&.\&.\&.]\fR +.RS 4 +A list of Erlang nodes to connect on ejabberd startup\&. This option is mostly intended for ejabberd customization and sophisticated setups\&. The default value is an empty list\&. +.RE +.PP +\fBdefault_db\fR: \fImnesia | sql\fR +.RS 4 +\fIdatabase\&.md#default\-database|Default database\fR +to store persistent data in ejabberd\&. Some components can be configured with specific toplevel options like +\fIoauth_db_type\fR\&. Many modules can be configured with specific module options, usually named +db_type\&. The default value is +\fImnesia\fR\&. +.RE +.PP +\fBdefault_ram_db\fR: \fImnesia | redis | sql\fR +.RS 4 +Default volatile (in\-memory) storage for ejabberd\&. Some components can be configured with specific toplevel options like +\fIrouter_db_type\fR +and +\fIsm_db_type\fR\&. Some modules can be configured with specific module options, usually named +ram_db_type\&. The default value is +\fImnesia\fR\&. +.RE +.PP +\fBdefine_keyword\fR: \fI{NAME: Value}\fR +.RS 4 +\fINote\fR +about this option: added in 25\&.03\&. Allows to define configuration +\fI\&.\&./configuration/file\-format\&.md#macros\-and\-keywords|keywords\fR\&. +.sp +\fBExample\fR: +.sp +.if n \{\ +.RS 4 +.\} +.nf +define_keyword: + SQL_USERNAME: "eja\&.global" + +host_config: + localhost: + define_keyword: + SQL_USERNAME: "eja\&.localhost" + +sql_username: "prefix\&.@SQL_USERNAME@" +.fi +.if n \{\ +.RE +.\} +.RE +.PP +\fBdefine_macro\fR: \fI{NAME: Value}\fR +.RS 4 +\fINote\fR +about this option: improved in 25\&.03\&. Allows to define configuration +\fI\&.\&./configuration/file\-format\&.md#macros\-and\-keywords|macros\fR\&. +.sp +\fBExample\fR: +.sp +.if n \{\ +.RS 4 +.\} +.nf +define_macro: + DEBUG: debug + LOG_LEVEL: DEBUG + USERBOB: + user: bob@localhost + +loglevel: LOG_LEVEL + +acl: + admin: USERBOB +.fi +.if n \{\ +.RE +.\} +.RE +.PP +\fBdisable_sasl_mechanisms\fR: \fI[Mechanism, \&.\&.\&.]\fR +.RS 4 +Specify a list of SASL mechanisms (such as +\fIDIGEST\-MD5\fR +or +\fISCRAM\-SHA1\fR) that should not be offered to the client\&. For convenience, the value of +\fIMechanism\fR +is case\-insensitive\&. The default value is an empty list, i\&.e\&. no mechanisms are disabled by default\&. +.RE +.PP +\fBdisable_sasl_scram_downgrade_protection\fR: \fItrue | false\fR +.RS 4 +Allows to disable sending data required by +\fIXEP\-0474: SASL SCRAM Downgrade Protection\fR\&. There are known buggy clients (like those that use strophejs 1\&.6\&.2) which will not be able to authenticatate when servers sends data from that specification\&. This options allows server to disable it to allow even buggy clients connects, but in exchange decrease MITM protection\&. The default value of this option is +\fIfalse\fR +which enables this extension\&. +.RE +.PP +\fBdomain_balancing\fR: \fI{Domain: Options}\fR +.RS 4 +An algorithm to +\fI\&.\&./guide/clustering\&.md#service\-load\-balancing|load\-balance\fR +the components that are plugged on an ejabberd cluster\&. It means that you can plug one or several instances of the same component on each ejabberd node and that the traffic will be automatically distributed\&. The algorithm to deliver messages to the component(s) can be specified by this option\&. For any component connected as +\fIDomain\fR, available +\fIOptions\fR +are: +.PP +\fBcomponent_number\fR: \fI2\&.\&.1000\fR +.RS 4 +The number of components to balance\&. +.RE +.PP +\fBtype\fR: \fIValue\fR +.RS 4 +How to deliver stanzas to connected components\&. The default value is +\fIrandom\fR\&. Possible values: +.RE +.PP +\fB\- bare_destination\fR +.RS 4 +by the bare JID (without resource) of the packet\(cqs +\fIto\fR +attribute +.RE +.PP +\fB\- bare_source\fR +.RS 4 +by the bare JID (without resource) of the packet\(cqs +\fIfrom\fR +attribute is used +.RE +.PP +\fB\- destination\fR +.RS 4 +an instance is chosen by the full JID of the packet\(cqs +\fIto\fR +attribute +.RE +.PP +\fB\- random\fR +.RS 4 +an instance is chosen at random +.RE +.PP +\fB\- source\fR +.RS 4 +by the full JID of the packet\(cqs +\fIfrom\fR +attribute +.RE +.sp +\fBExample\fR: +.sp +.if n \{\ +.RS 4 +.\} +.nf +domain_balancing: + component\&.domain\&.tld: + type: destination + component_number: 5 + transport\&.example\&.org: + type: bare_source +.fi +.if n \{\ +.RE +.\} +.RE +.PP +\fBext_api_headers\fR: \fIHeaders\fR +.RS 4 +String of headers (separated with commas +\fI,\fR) that will be provided by ejabberd when sending ReST requests\&. The default value is an empty string of headers: +\fI""\fR\&. +.RE +.PP +\fBext_api_http_pool_size\fR: \fIpos_integer()\fR +.RS 4 +Define the size of the HTTP pool, that is, the maximum number of sessions that the ejabberd ReST service will handle simultaneously\&. The default value is: +\fI100\fR\&. +.RE +.PP +\fBext_api_path_oauth\fR: \fIPath\fR +.RS 4 +Define the base URI path when performing OAUTH ReST requests\&. The default value is: +\fI"/oauth"\fR\&. +.RE +.PP +\fBext_api_url\fR: \fIURL\fR +.RS 4 +Define the base URI when performing ReST requests\&. The default value is: +\fI"http://localhost/api"\fR\&. +.RE +.PP +\fBextauth_pool_name\fR: \fIName\fR +.RS 4 +Define the pool name appendix in +\fIauthentication\&.md#external\-script|external auth\fR, so the full pool name will be +\fIextauth_pool_Name\fR\&. The default value is the hostname\&. +.RE +.PP +\fBextauth_pool_size\fR: \fISize\fR +.RS 4 +The option defines the number of instances of the same +\fIauthentication\&.md#external\-script|external auth\fR +program to start for better load balancing\&. The default is the number of available CPU cores\&. +.RE +.PP +\fBextauth_program\fR: \fIPath\fR +.RS 4 +Indicate in this option the full path to the +\fIauthentication\&.md#external\-script|external authentication script\fR\&. The script must be executable by ejabberd\&. +.RE +.PP +\fBfqdn\fR: \fIDomain\fR +.RS 4 +A fully qualified domain name that will be used in SASL DIGEST\-MD5 authentication\&. The default is detected automatically\&. +.RE +.PP +\fBhide_sensitive_log_data\fR: \fItrue | false\fR +.RS 4 +A privacy option to not log sensitive data (mostly IP addresses)\&. The default value is +\fIfalse\fR +for backward compatibility\&. +.RE +.PP +\fBhost_config\fR: \fI{Host: Options}\fR +.RS 4 +The option is used to redefine +\fIOptions\fR +for +\fI\&.\&./configuration/basic\&.md#virtual\-hosting|virtual host\fR +\fIHost\fR\&. In the example below LDAP authentication method will be used on virtual host +\fIdomain\&.tld\fR +and SQL method will be used on virtual host +\fIexample\&.org\fR\&. +.sp +\fBExample\fR: +.sp +.if n \{\ +.RS 4 +.\} +.nf +hosts: + \- domain\&.tld + \- example\&.org + +auth_method: + \- sql + +host_config: + domain\&.tld: + auth_method: + \- ldap +.fi +.if n \{\ +.RE +.\} +.RE +.PP +\fBhosts\fR: \fI[Domain1, Domain2, \&.\&.\&.]\fR +.RS 4 +List of one or more +\fI\&.\&./configuration/basic\&.md#host\-names|host names\fR +(or domains) that ejabberd will serve\&. This is a +\fBmandatory\fR +option\&. +.RE +.PP +\fBhosts_alias\fR: \fI{Alias: Host}\fR +.RS 4 +\fINote\fR +about this option: added in 25\&.07\&. Define aliases for existing vhosts managed by ejabberd\&. An alias may be a regexp expression\&. This option is only consulted by the +\fIejabberd_http\fR +listener\&. +.sp +\fBExample\fR: +.sp +.if n \{\ +.RS 4 +.\} +.nf +hosts: + \- domain\&.tld + \- example\&.org + +hosts_alias: + xmpp\&.domain\&.tld: domain\&.tld + jabber\&.domain\&.tld: domain\&.tld + mytest\&.net: example\&.org + "exa*": example\&.org +.fi +.if n \{\ +.RE +.\} +.RE +.PP +\fBinclude_config_file\fR: \fI[Filename, \&.\&.\&.] | {Filename: Options}\fR +.RS 4 +Read and +\fI\&.\&./configuration/file\-format\&.md#include\-additional\-files|include additional file\fR +from +\fIFilename\fR\&. If the value is provided in +\fI{Filename: Options}\fR +format, the +\fIOptions\fR +must be one of the following: +.PP +\fBallow_only\fR: \fI[OptionName, \&.\&.\&.]\fR +.RS 4 +Allows only the usage of those options in the included file +\fIFilename\fR\&. The options that do not match this criteria are not accepted\&. The default value is to include all options\&. +.RE +.PP +\fBdisallow\fR: \fI[OptionName, \&.\&.\&.]\fR +.RS 4 +Disallows the usage of those options in the included file +\fIFilename\fR\&. The options that match this criteria are not accepted\&. The default value is an empty list\&. +.RE +.RE +.PP +\fBinstall_contrib_modules\fR: \fI[Module, \&.\&.\&.]\fR +.RS 4 +\fINote\fR +about this option: added in 23\&.10\&. Modules to install from +\fI\&.\&./\&.\&./admin/guide/modules\&.md#ejabberd\-contrib|ejabberd\-contrib\fR +at start time\&. The default value is an empty list of modules: +\fI[]\fR\&. +.RE +.PP +\fBjwt_auth_only_rule\fR: \fIAccessName\fR +.RS 4 +This ACL rule defines accounts that can use only the +\fIauthentication\&.md#jwt\-authentication|JWT\fR +auth method, even if others are also defined in the ejabberd configuration file\&. In other words: if there are several auth methods enabled for this host (JWT, SQL, \&...), users that match this rule can only use JWT\&. The default value is +\fInone\fR\&. +.RE +.PP +\fBjwt_jid_field\fR: \fIFieldName\fR +.RS 4 +By default, the JID is defined in the +\fI"jid"\fR +JWT field\&. In this option you can specify other +\fIauthentication\&.md#jwt\-authentication|JWT\fR +field name where the JID is defined\&. +.RE +.PP +\fBjwt_key\fR: \fIFilePath\fR +.RS 4 +Path to the file that contains the +\fIauthentication\&.md#jwt\-authentication|JWT\fR +key\&. The default value is +\fIundefined\fR\&. +.RE +.PP +\fBlanguage\fR: \fILanguage\fR +.RS 4 +Define the +\fI\&.\&./configuration/basic\&.md#default\-language|default language\fR +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\&. The default value is +\fI"en"\fR\&. +.RE +.PP +\fBldap_backups\fR: \fI[Host, \&.\&.\&.]\fR +.RS 4 +A list of IP addresses or DNS names of LDAP backup servers (see +\fI\&.\&./configuration/ldap\&.md#ldap\-connection|LDAP connection\fR)\&. When no servers listed in +\fIldap_servers\fR +option are reachable, ejabberd connects to these backup servers\&. The default is an empty list, i\&.e\&. no backup servers specified\&. Please notice that ejabberd only connects to the next server when the existing connection is lost; it doesn\(cqt detect when a previously\-attempted server becomes available again\&. +.RE +.PP +\fBldap_base\fR: \fIBase\fR +.RS 4 +LDAP base directory which stores users accounts\&. There is no default value: you must set the option in order for LDAP connections to work properly\&. +.RE +.PP +\fBldap_deref_aliases\fR: \fInever | always | finding | searching\fR +.RS 4 +Whether to dereference aliases or not\&. The default value is +\fInever\fR\&. +.RE +.PP +\fBldap_dn_filter\fR: \fI{Filter: FilterAttrs}\fR +.RS 4 +This filter is applied on the results returned by the main filter\&. The filter performs an additional LDAP lookup to make the complete result\&. This is useful when you are unable to define all filter rules in +\fIldap_filter\fR\&. You can define +\fI"%u"\fR, +\fI"%d"\fR, +\fI"%s"\fR +and +\fI"%D"\fR +pattern variables in +\fIFilter: "%u"\fR +is replaced by a user\(cqs part of the JID, +\fI"%d"\fR +is replaced by the corresponding domain (virtual host), all +\fI"%s"\fR +variables are consecutively replaced by values from the attributes in +\fIFilterAttrs\fR +and +\fI"%D"\fR +is replaced by Distinguished Name from the result set\&. There is no default value, which means the result is not filtered\&. WARNING: Since this filter makes additional LDAP lookups, use it only as the last resort: try to define all filter rules in +\fIldap_filter\fR +option if possible\&. +.sp +\fBExample\fR: +.sp +.if n \{\ +.RS 4 +.\} +.nf +ldap_dn_filter: + "(&(name=%s)(owner=%D)(user=%u@%d))": [sn] +.fi +.if n \{\ +.RE +.\} +.RE +.PP +\fBldap_encrypt\fR: \fItls | none\fR +.RS 4 +Whether to encrypt LDAP connection using TLS or not\&. The default value is +\fInone\fR\&. NOTE: STARTTLS encryption is not supported\&. +.RE +.PP +\fBldap_filter\fR: \fIFilter\fR +.RS 4 +An LDAP filter as defined in +RFC4515\&. There is no default value\&. Example: +\fI"(&(objectClass=shadowAccount)(memberOf=XMPP Users))"\fR\&. NOTE: don\(cqt forget to close brackets and don\(cqt use superfluous whitespaces\&. Also you must not use +\fI"uid"\fR +attribute in the filter because this attribute will be appended to the filter automatically\&. +.RE +.PP +\fBldap_password\fR: \fIPassword\fR +.RS 4 +Bind password\&. The default value is an empty string\&. +.RE +.PP +\fBldap_port\fR: \fI1\&.\&.65535\fR +.RS 4 +Port to connect to your LDAP server\&. The default port is +\fI389\fR +if encryption is disabled and +\fI636\fR +if encryption is enabled\&. +.RE +.PP +\fBldap_rootdn\fR: \fIRootDN\fR +.RS 4 +Bind Distinguished Name\&. The default value is an empty string, which means "anonymous connection"\&. +.RE +.PP +\fBldap_servers\fR: \fI[Host, \&.\&.\&.]\fR +.RS 4 +A list of IP addresses or DNS names of your LDAP servers (see +\fI\&.\&./configuration/ldap\&.md#ldap\-connection|LDAP connection\fR)\&. ejabberd connects immediately to all of them, and reconnects infinitely if connection is lost\&. The default value is +\fI[localhost]\fR\&. +.RE +.PP +\fBldap_tls_cacertfile\fR: \fIPath\fR +.RS 4 +A path to a file containing PEM encoded CA certificates\&. This option is required when TLS verification is enabled\&. +.RE +.PP +\fBldap_tls_certfile\fR: \fIPath\fR +.RS 4 +A path to a file containing PEM encoded certificate along with PEM encoded private key\&. This certificate will be provided by ejabberd when TLS enabled for LDAP connections\&. There is no default value, which means no client certificate will be sent\&. +.RE +.PP +\fBldap_tls_depth\fR: \fINumber\fR +.RS 4 +Specifies the maximum verification depth when TLS verification is enabled, i\&.e\&. how far in a chain of certificates the verification process can proceed before the verification is considered to be failed\&. Peer certificate = 0, CA certificate = 1, higher level CA certificate = 2, etc\&. The value +\fI2\fR +thus means that a chain can at most contain peer cert, CA cert, next CA cert, and an additional CA cert\&. The default value is +\fI1\fR\&. +.RE +.PP +\fBldap_tls_verify\fR: \fIfalse | soft | hard\fR +.RS 4 +This option specifies whether to verify LDAP server certificate or not when TLS is enabled\&. When +\fIhard\fR +is set, ejabberd doesn\(cqt proceed if the certificate is invalid\&. When +\fIsoft\fR +is set, ejabberd proceeds even if the check has failed\&. The default is +\fIfalse\fR, which means no checks are performed\&. +.RE +.PP +\fBldap_uids\fR: \fI[Attr] | {Attr: AttrFormat}\fR +.RS 4 +LDAP attributes which hold a list of attributes to use as alternatives for getting the JID, where +\fIAttr\fR +is an LDAP attribute which holds the user\(cqs part of the JID and +\fIAttrFormat\fR +must contain one and only one pattern variable +\fI"%u"\fR +which will be replaced by the user\(cqs part of the JID\&. For example, +\fI"%\fR\fIu@example\fR\fI\&.org"\fR\&. If the value is in the form of +\fI[Attr]\fR +then +\fIAttrFormat\fR +is assumed to be +\fI"%u"\fR\&. +.RE +.PP +\fBlisten\fR: \fI[Options, \&.\&.\&.]\fR +.RS 4 +The option for listeners configuration\&. See the +\fIlisten\&.md|Listen Modules\fR +section for details\&. +.RE +.PP +\fBlog_burst_limit_count\fR: \fINumber\fR +.RS 4 +\fINote\fR +about this option: added in 22\&.10\&. The number of messages to accept in +log_burst_limit_window_time +period before starting to drop them\&. Default +500 +.RE +.PP +\fBlog_burst_limit_window_time\fR: \fINumber\fR +.RS 4 +\fINote\fR +about this option: added in 22\&.10\&. The time period to rate\-limit log messages by\&. Defaults to +1 +second\&. +.RE +.PP +\fBlog_modules_fully\fR: \fI[Module, \&.\&.\&.]\fR +.RS 4 +\fINote\fR +about this option: added in 23\&.01\&. List of modules that will log everything independently from the general loglevel option\&. +.RE +.PP +\fBlog_rotate_count\fR: \fINumber\fR +.RS 4 +The number of rotated log files to keep\&. The default value is +\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 +.RS 4 +The size (in bytes) of a log file to trigger rotation\&. If set to +\fIinfinity\fR, log rotation is disabled\&. The default value is 10 Mb expressed in bytes: +\fI10485760\fR\&. +.RE +.PP +\fBloglevel\fR: \fInone | emergency | alert | critical | error | warning | notice | info | debug\fR +.RS 4 +Verbosity of ejabberd +\fI\&.\&./configuration/basic\&.md#logging|logging\fR\&. The default value is +\fIinfo\fR\&. NOTE: previous versions of ejabberd had log levels defined in numeric format (\fI0\&.\&.5\fR)\&. The numeric values are still accepted for backward compatibility, but are not recommended\&. +.RE +.PP +\fBmax_fsm_queue\fR: \fISize\fR +.RS 4 +This option specifies the maximum number of elements in the queue of the FSM (Finite State Machine)\&. Roughly speaking, each message in such queues represents one XML stanza queued to be sent into its relevant outgoing stream\&. If queue size reaches the limit (because, for example, the receiver of stanzas is too slow), the FSM and the corresponding connection (if any) will be terminated and error message will be logged\&. The reasonable value for this option depends on your hardware configuration\&. The allowed values are positive integers\&. The default value is +\fI10000\fR\&. +.RE +.PP +\fBmodules\fR: \fI{Module: Options}\fR +.RS 4 +Set all the +\fImodules\&.md|modules\fR +configuration options\&. +.RE +.PP +\fBnegotiation_timeout\fR: \fItimeout()\fR +.RS 4 +Time to wait for an XMPP stream negotiation to complete\&. When timeout occurs, the corresponding XMPP stream is closed\&. The default value is +\fI120\fR +seconds\&. +.RE +.PP +\fBnet_ticktime\fR: \fItimeout()\fR +.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 minute\fR\&. +.RE +.PP +\fBnew_sql_schema\fR: \fItrue | false\fR +.RS 4 +Whether to use the +\fIdatabase\&.md#default\-and\-new\-schemas|new SQL schema\fR\&. All schemas are located at +https://github\&.com/processone/ejabberd/tree/25\&.08/sql\&. There are two schemas available\&. The default legacy schema stores one XMPP domain into one ejabberd database\&. The +\fInew\fR +schema can handle several XMPP domains in a single ejabberd database\&. Using this +\fInew\fR +schema is best when serving several XMPP domains and/or changing domains from time to time\&. This avoid need to manage several databases and handle complex configuration changes\&. The default depends on configuration flag +\fI\-\-enable\-new\-sql\-schema\fR +which is set at compile time\&. +.RE +.PP +\fBoauth_access\fR: \fIAccessName\fR +.RS 4 +By default creating OAuth tokens is not allowed\&. To define which users can create OAuth tokens, you can refer to an ejabberd access rule in the +\fIoauth_access\fR +option\&. Use +\fIall\fR +to allow everyone to create tokens\&. +.RE +.PP +\fBoauth_cache_life_time\fR: \fItimeout()\fR +.RS 4 +Same as +\fIcache_life_time\fR, but applied to OAuth cache only\&. If not set, the value from +\fIcache_life_time\fR +will be used\&. +.RE +.PP +\fBoauth_cache_missed\fR: \fItrue | false\fR +.RS 4 +Same as +\fIcache_missed\fR, but applied to OAuth cache only\&. If not set, the value from +\fIcache_missed\fR +will be used\&. +.RE +.PP +\fBoauth_cache_rest_failure_life_time\fR: \fItimeout()\fR +.RS 4 +\fINote\fR +about this option: added in 21\&.01\&. The time that a failure in OAuth ReST is cached\&. The default value is +\fIinfinity\fR\&. +.RE +.PP +\fBoauth_cache_size\fR: \fIpos_integer() | infinity\fR +.RS 4 +Same as +\fIcache_size\fR, but applied to OAuth cache only\&. If not set, the value from +\fIcache_size\fR +will be used\&. +.RE +.PP +\fBoauth_client_id_check\fR: \fIallow | db | deny\fR +.RS 4 +Define whether the client authentication is always allowed, denied, or it will depend if the client ID is present in the database\&. The default value is +\fIallow\fR\&. +.RE +.PP +\fBoauth_db_type\fR: \fImnesia | sql\fR +.RS 4 +Database backend to use for OAuth authentication\&. The default value is picked from +\fIdefault_db\fR +option, or if it\(cqs not set, +\fImnesia\fR +will be used\&. +.RE +.PP +\fBoauth_expire\fR: \fItimeout()\fR +.RS 4 +Time during which the OAuth token is valid, in seconds\&. After that amount of time, the token expires and the delegated credential cannot be used and is removed from the database\&. The default is +\fI4294967\fR +seconds\&. +.RE +.PP +\fBoauth_use_cache\fR: \fItrue | false\fR +.RS 4 +Same as +\fIuse_cache\fR, but applied to OAuth cache only\&. If not set, the value from +\fIuse_cache\fR +will be used\&. +.RE +.PP +\fBoom_killer\fR: \fItrue | false\fR +.RS 4 +Enable or disable OOM (out\-of\-memory) killer\&. When system memory raises above the limit defined in +\fIoom_watermark\fR +option, ejabberd triggers OOM killer to terminate most memory consuming Erlang processes\&. Note that in order to maintain functionality, ejabberd only attempts to kill transient processes, such as those managing client sessions, s2s or database connections\&. The default value is +\fItrue\fR\&. +.RE +.PP +\fBoom_queue\fR: \fISize\fR +.RS 4 +Trigger OOM killer when some of the running Erlang processes have messages queue above this +\fISize\fR\&. Note that such processes won\(cqt be killed if +\fIoom_killer\fR +option is set to +\fIfalse\fR +or if +\fIoom_watermark\fR +is not reached yet\&. +.RE +.PP +\fBoom_watermark\fR: \fIPercent\fR +.RS 4 +A percent of total system memory consumed at which OOM killer should be activated with some of the processes possibly be killed (see +\fIoom_killer\fR +option)\&. Later, when memory drops below this +\fIPercent\fR, OOM killer is deactivated\&. The default value is +\fI80\fR +percents\&. +.RE +.PP +\fBoutgoing_s2s_families\fR: \fI[ipv6 | ipv4, \&.\&.\&.]\fR +.RS 4 +\fINote\fR +about this option: changed in 23\&.01\&. Specify which address families to try, in what order\&. The default is +\fI[ipv6, ipv4]\fR +which means it first tries connecting with IPv6, if that fails it tries using IPv4\&. This option is obsolete and irrelevant when using ejabberd 23\&.01 and Erlang/OTP 22, or newer versions of them\&. +.RE +.PP +\fBoutgoing_s2s_ipv4_address\fR: \fIAddress\fR +.RS 4 +\fINote\fR +about this option: added in 20\&.12\&. Specify the IPv4 address that will be used when establishing an outgoing S2S IPv4 connection, for example +\fI"127\&.0\&.0\&.1"\fR\&. The default value is +\fIundefined\fR\&. +.RE +.PP +\fBoutgoing_s2s_ipv6_address\fR: \fIAddress\fR +.RS 4 +\fINote\fR +about this option: added in 20\&.12\&. Specify the IPv6 address that will be used when establishing an outgoing S2S IPv6 connection, for example +\fI"::FFFF:127\&.0\&.0\&.1"\fR\&. The default value is +\fIundefined\fR\&. +.RE +.PP +\fBoutgoing_s2s_port\fR: \fI1\&.\&.65535\fR +.RS 4 +A port number to use for outgoing s2s connections when the target server doesn\(cqt have an SRV record\&. The default value is +\fI5269\fR\&. +.RE +.PP +\fBoutgoing_s2s_timeout\fR: \fItimeout()\fR +.RS 4 +The timeout in seconds for outgoing S2S connection attempts\&. The default value is +\fI10\fR +seconds\&. +.RE +.PP +\fBpam_service\fR: \fIName\fR +.RS 4 +This option defines the +\fIauthentication\&.md#pam\-authentication|PAM\fR +service name\&. Refer to the PAM documentation of your operation system for more information\&. The default value is +\fIejabberd\fR\&. +.RE +.PP +\fBpam_userinfotype\fR: \fIusername | jid\fR +.RS 4 +This option defines what type of information about the user ejabberd provides to the +\fIauthentication\&.md#pam\-authentication|PAM\fR +service: only the username, or the user\(cqs JID\&. Default is +\fIusername\fR\&. +.RE +.PP +\fBpgsql_users_number_estimate\fR: \fItrue | false\fR +.RS 4 +Whether to use PostgreSQL estimation when counting registered users\&. The default value is +\fIfalse\fR\&. +.RE +.PP +\fBqueue_dir\fR: \fIDirectory\fR +.RS 4 +If +\fIqueue_type\fR +option is set to +\fIfile\fR, use this +\fIDirectory\fR +to store file queues\&. The default is to keep queues inside Mnesia directory\&. +.RE +.PP +\fBqueue_type\fR: \fIram | file\fR +.RS 4 +Default type of queues in ejabberd\&. Modules may have its own value of the option\&. The value of +\fIram\fR +means that queues will be kept in memory\&. If value +\fIfile\fR +is set, you may also specify directory in +\fIqueue_dir\fR +option where file queues will be placed\&. The default value is +\fIram\fR\&. +.RE +.PP +\fBredis_connect_timeout\fR: \fItimeout()\fR +.RS 4 +A timeout to wait for the connection to be re\-established to the +\fIdatabase\&.md#redis|Redis\fR +server\&. The default is +\fI1 second\fR\&. +.RE +.PP +\fBredis_db\fR: \fINumber\fR +.RS 4 +\fIdatabase\&.md#redis|Redis\fR +database number\&. The default is +\fI0\fR\&. +.RE +.PP +\fBredis_password\fR: \fIPassword\fR +.RS 4 +The password to the +\fIdatabase\&.md#redis|Redis\fR +server\&. The default is an empty string, i\&.e\&. no password\&. +.RE +.PP +\fBredis_pool_size\fR: \fINumber\fR +.RS 4 +The number of simultaneous connections to the +\fIdatabase\&.md#redis|Redis\fR +server\&. The default value is +\fI10\fR\&. +.RE +.PP +\fBredis_port\fR: \fI1\&.\&.65535\fR +.RS 4 +The port where the +\fIdatabase\&.md#redis|Redis\fR +server is accepting connections\&. The default is +\fI6379\fR\&. +.RE +.PP +\fBredis_queue_type\fR: \fIram | file\fR +.RS 4 +The type of request queue for the +\fIdatabase\&.md#redis|Redis\fR +server\&. See description of +\fIqueue_type\fR +option for the explanation\&. The default value is the value defined in +\fIqueue_type\fR +or +\fIram\fR +if the latter is not set\&. +.RE +.PP +\fBredis_server\fR: \fIHost | IP Address | Unix Socket Path\fR +.RS 4 +\fINote\fR +about this option: improved in 24\&.12\&. A hostname, IP address or unix domain socket file of the +\fIdatabase\&.md#redis|Redis\fR +server\&. Setup the path to unix domain socket like: +\fI"unix:/path/to/socket"\fR\&. The default value is +\fIlocalhost\fR\&. +.RE +.PP +\fBregistration_timeout\fR: \fItimeout()\fR +.RS 4 +This is a global option for module +\fImod_register\fR\&. It limits the frequency of registrations from a given IP or username\&. So, a user that tries to register a new account from the same IP address or JID during this time after their previous registration will receive an error with the corresponding explanation\&. To disable this limitation, set the value to +\fIinfinity\fR\&. The default value is +\fI600 seconds\fR\&. +.RE +.PP +\fBresource_conflict\fR: \fIsetresource | closeold | closenew\fR +.RS 4 +NOTE: this option is deprecated and may be removed anytime in the future versions\&. The possible values match exactly the three possibilities described in +XMPP Core: section 7\&.7\&.2\&.2\&. The default value is +\fIcloseold\fR\&. If the client uses old Jabber Non\-SASL authentication (XEP\-0078), then this option is not respected, and the action performed is +\fIcloseold\fR\&. +.RE +.PP +\fBrest_proxy\fR: \fIHost\fR +.RS 4 +\fINote\fR +about this option: added in 25\&.07\&. Address of a HTTP Connect proxy used by modules issuing rest calls (like ejabberd_oauth_rest) +.RE +.PP +\fBrest_proxy_password\fR: \fIstring()\fR +.RS 4 +\fINote\fR +about this option: added in 25\&.07\&. Password used to authenticate to HTTP Connect proxy used by modules issuing rest calls (like ejabberd_oauth_rest) +.RE +.PP +\fBrest_proxy_port\fR: \fI1\&.\&.65535\fR +.RS 4 +\fINote\fR +about this option: added in 25\&.07\&. Port of a HTTP Connect proxy used by modules issuing rest calls (like ejabberd_oauth_rest) +.RE +.PP +\fBrest_proxy_username\fR: \fIstring()\fR +.RS 4 +\fINote\fR +about this option: added in 25\&.07\&. Username used to authenticate to HTTP Connect proxy used by modules issuing rest calls (like ejabberd_oauth_rest) +.RE +.PP +\fBrouter_cache_life_time\fR: \fItimeout()\fR +.RS 4 +Same as +\fIcache_life_time\fR, but applied to routing table cache only\&. If not set, the value from +\fIcache_life_time\fR +will be used\&. +.RE +.PP +\fBrouter_cache_missed\fR: \fItrue | false\fR +.RS 4 +Same as +\fIcache_missed\fR, but applied to routing table cache only\&. If not set, the value from +\fIcache_missed\fR +will be used\&. +.RE +.PP +\fBrouter_cache_size\fR: \fIpos_integer() | infinity\fR +.RS 4 +Same as +\fIcache_size\fR, but applied to routing table cache only\&. If not set, the value from +\fIcache_size\fR +will be used\&. +.RE +.PP +\fBrouter_db_type\fR: \fImnesia | redis | sql\fR +.RS 4 +Database backend to use for routing information\&. The default value is picked from +\fIdefault_ram_db\fR +option, or if it\(cqs not set, +\fImnesia\fR +will be used\&. +.RE +.PP +\fBrouter_use_cache\fR: \fItrue | false\fR +.RS 4 +Same as +\fIuse_cache\fR, but applied to routing table cache only\&. If not set, the value from +\fIuse_cache\fR +will be used\&. +.RE +.PP +\fBrpc_timeout\fR: \fItimeout()\fR +.RS 4 +A timeout for remote function calls between nodes in an ejabberd cluster\&. You should probably never change this value since those calls are used for internal needs only\&. The default value is +\fI5\fR +seconds\&. +.RE +.PP +\fBs2s_access\fR: \fIAccess\fR +.RS 4 +This +\fIbasic\&.md#access\-rules|Access Rule\fR +defines to what remote servers can s2s connections be established\&. The default value is +\fIall\fR; no restrictions are applied, it is allowed to connect s2s to/from all remote servers\&. +.RE +.PP +\fBs2s_cafile\fR: \fIPath\fR +.RS 4 +A path to a file with CA root certificates that will be used to authenticate s2s connections\&. If not set, the value of +\fIca_file\fR +will be used\&. +.RE +.sp +You can use \fIhost_config\fR to specify this option per\-vhost\&. +.PP +\fBs2s_ciphers\fR: \fI[Cipher, \&.\&.\&.]\fR +.RS 4 +A list of OpenSSL ciphers to use for s2s connections\&. The default value is shown in the example below: +.sp +\fBExample\fR: +.sp +.if n \{\ +.RS 4 +.\} +.nf +s2s_ciphers: + \- HIGH + \- "!aNULL" + \- "!eNULL" + \- "!3DES" + \- "@STRENGTH" +.fi +.if n \{\ +.RE +.\} +.RE +.PP +\fBs2s_dhfile\fR: \fIPath\fR +.RS 4 +Full path to a file containing custom DH parameters to use for s2s connections\&. Such a file could be created with the command +\fI"openssl dhparam \-out dh\&.pem 2048"\fR\&. If this option is not specified, 2048\-bit MODP Group with 256\-bit Prime Order Subgroup will be used as defined in RFC5114 Section 2\&.3\&. +.RE +.PP +\fBs2s_dns_retries\fR: \fINumber\fR +.RS 4 +DNS resolving retries\&. The default value is +\fI2\fR\&. +.RE +.PP +\fBs2s_dns_timeout\fR: \fItimeout()\fR +.RS 4 +The timeout for DNS resolving\&. The default value is +\fI10\fR +seconds\&. +.RE +.PP +\fBs2s_max_retry_delay\fR: \fItimeout()\fR +.RS 4 +The maximum allowed delay for s2s connection retry to connect after a failed connection attempt\&. The default value is +\fI300\fR +seconds (5 minutes)\&. +.RE +.PP +\fBs2s_protocol_options\fR: \fI[Option, \&.\&.\&.]\fR +.RS 4 +List of general SSL options to use for s2s connections\&. These map to OpenSSL\(cqs +\fIset_options()\fR\&. The default value is shown in the example below: +.sp +\fBExample\fR: +.sp +.if n \{\ +.RS 4 +.\} +.nf +s2s_protocol_options: + \- no_sslv3 + \- cipher_server_preference + \- no_compression +.fi +.if n \{\ +.RE +.\} +.RE +.PP +\fBs2s_queue_type\fR: \fIram | file\fR +.RS 4 +The type of a queue for s2s packets\&. See description of +\fIqueue_type\fR +option for the explanation\&. The default value is the value defined in +\fIqueue_type\fR +or +\fIram\fR +if the latter is not set\&. +.RE +.PP +\fBs2s_timeout\fR: \fItimeout()\fR +.RS 4 +A time to wait before closing an idle s2s connection\&. The default value is +\fI1\fR +hour\&. +.RE +.PP +\fBs2s_tls_compression\fR: \fItrue | false\fR +.RS 4 +Whether to enable or disable TLS compression for s2s connections\&. The default value is +\fIfalse\fR\&. +.RE +.PP +\fBs2s_use_starttls\fR: \fItrue | false | optional | required\fR +.RS 4 +Whether to use STARTTLS for s2s connections\&. The value of +\fIfalse\fR +means STARTTLS is prohibited\&. The value of +\fItrue\fR +or +\fIoptional\fR +means STARTTLS is enabled but plain connections are still allowed\&. And the value of +\fIrequired\fR +means that only STARTTLS connections are allowed\&. The default value is +\fIfalse\fR +(for historical reasons)\&. +.RE +.PP +\fBs2s_zlib\fR: \fItrue | false\fR +.RS 4 +Whether to use +\fIzlib\fR +compression (as defined in +XEP\-0138) or not\&. The default value is +\fIfalse\fR\&. WARNING: this type of compression is nowadays considered insecure\&. +.RE +.PP +\fBshaper\fR: \fI{ShaperName: Rate}\fR +.RS 4 +The option defines a set of +\fI\&.\&./configuration/basic\&.md#shapers|shapers\fR\&. Every shaper is assigned a name +\fIShaperName\fR +that can be used in other parts of the configuration file, such as +\fIshaper_rules\fR +option\&. The shaper itself is defined by its +\fIRate\fR, where +\fIRate\fR +stands for the maximum allowed incoming rate in +\fBbytes\fR +per second\&. When a connection exceeds this limit, ejabberd stops reading from the socket until the average rate is again below the allowed maximum\&. In the example below shaper +\fInormal\fR +limits the traffic speed to 1,000 bytes/sec and shaper +\fIfast\fR +limits the traffic speed to 50,000 bytes/sec: +.sp +\fBExample\fR: +.sp +.if n \{\ +.RS 4 +.\} +.nf +shaper: + normal: 1000 + fast: 50000 +.fi +.if n \{\ +.RE +.\} +.RE +.PP +\fBshaper_rules\fR: \fI{ShaperRuleName: {Number|ShaperName: ACLName|ACLDefinition}}\fR +.RS 4 +This option defines +\fI\&.\&./configuration/basic\&.md#shaper\-rules|shaper rules\fR +to use for matching user/hosts\&. Semantics is similar to +\fIaccess_rules\fR +option, the only difference is that instead using +\fIallow\fR +or +\fIdeny\fR, a name of a shaper (defined in +\fIshaper\fR +option) or a positive number should be used\&. +.sp +\fBExample\fR: +.sp +.if n \{\ +.RS 4 +.\} +.nf +shaper_rules: + connections_limit: + 10: + user: peter@example\&.com + 100: admin + 5: all + download_speed: + fast: admin + slow: anonymous_users + normal: all + log_days: 30 +.fi +.if n \{\ +.RE +.\} +.RE +.PP +\fBsm_cache_life_time\fR: \fItimeout()\fR +.RS 4 +Same as +\fIcache_life_time\fR, but applied to client sessions table cache only\&. If not set, the value from +\fIcache_life_time\fR +will be used\&. +.RE +.PP +\fBsm_cache_missed\fR: \fItrue | false\fR +.RS 4 +Same as +\fIcache_missed\fR, but applied to client sessions table cache only\&. If not set, the value from +\fIcache_missed\fR +will be used\&. +.RE +.PP +\fBsm_cache_size\fR: \fIpos_integer() | infinity\fR +.RS 4 +Same as +\fIcache_size\fR, but applied to client sessions table cache only\&. If not set, the value from +\fIcache_size\fR +will be used\&. +.RE +.PP +\fBsm_db_type\fR: \fImnesia | redis | sql\fR +.RS 4 +Database backend to use for client sessions information\&. The default value is picked from +\fIdefault_ram_db\fR +option, or if it\(cqs not set, +\fImnesia\fR +will be used\&. +.RE +.PP +\fBsm_use_cache\fR: \fItrue | false\fR +.RS 4 +Same as +\fIuse_cache\fR, but applied to client sessions table cache only\&. If not set, the value from +\fIuse_cache\fR +will be used\&. +.RE +.PP +\fBsql_connect_timeout\fR: \fItimeout()\fR +.RS 4 +A time to wait for connection to an SQL server to be established\&. The default value is +\fI5\fR +seconds\&. +.RE +.PP +\fBsql_database\fR: \fIDatabase\fR +.RS 4 +An SQL database name\&. For SQLite this must be a full path to a database file\&. The default value is +\fIejabberd\fR\&. +.RE +.PP +\fBsql_flags\fR: \fI[mysql_alternative_upsert]\fR +.RS 4 +\fINote\fR +about this option: added in 24\&.02\&. This option accepts a list of SQL flags, and is empty by default\&. +\fImysql_alternative_upsert\fR +forces the alternative upsert implementation in MySQL\&. +.RE +.PP +\fBsql_keepalive_interval\fR: \fItimeout()\fR +.RS 4 +An interval to make a dummy SQL request to keep alive the connections to the database\&. There is no default value, so no keepalive requests are made\&. +.RE +.PP +\fBsql_odbc_driver\fR: \fIPath\fR +.RS 4 +\fINote\fR +about this option: added in 20\&.12\&. Path to the ODBC driver to use to connect to a Microsoft SQL Server database\&. This option only applies if the +\fIsql_type\fR +option is set to +\fImssql\fR +and +\fIsql_server\fR +is not an ODBC connection string\&. The default value is: +\fIlibtdsodbc\&.so\fR +.RE +.PP +\fBsql_password\fR: \fIPassword\fR +.RS 4 +The password for SQL authentication\&. The default is empty string\&. +.RE +.PP +\fBsql_pool_size\fR: \fISize\fR +.RS 4 +Number of connections to the SQL server that ejabberd will open for each virtual host\&. The default value is +\fI10\fR\&. WARNING: for SQLite this value is +\fI1\fR +by default and it\(cqs not recommended to change it due to potential race conditions\&. +.RE +.PP +\fBsql_port\fR: \fI1\&.\&.65535\fR +.RS 4 +The port where the SQL server is accepting connections\&. The default is +\fI3306\fR +for MySQL, +\fI5432\fR +for PostgreSQL and +\fI1433\fR +for MS SQL\&. The option has no effect for SQLite\&. +.RE +.PP +\fBsql_prepared_statements\fR: \fItrue | false\fR +.RS 4 +\fINote\fR +about this option: added in 20\&.01\&. This option is +\fItrue\fR +by default, and is useful to disable prepared statements\&. The option is valid for PostgreSQL and MySQL\&. +.RE +.PP +\fBsql_query_timeout\fR: \fItimeout()\fR +.RS 4 +A time to wait for an SQL query response\&. The default value is +\fI60\fR +seconds\&. +.RE +.PP +\fBsql_queue_type\fR: \fIram | file\fR +.RS 4 +The type of a request queue for the SQL server\&. See description of +\fIqueue_type\fR +option for the explanation\&. The default value is the value defined in +\fIqueue_type\fR +or +\fIram\fR +if the latter is not set\&. +.RE +.PP +\fBsql_server\fR: \fIHost | IP Address | ODBC Connection String | Unix Socket Path\fR +.RS 4 +\fINote\fR +about this option: improved in 24\&.06\&. The hostname or IP address of the SQL server\&. For +\fIsql_type\fR +\fImssql\fR +or +\fIodbc\fR +this can also be an ODBC connection string\&. When +\fIsql_type\fR +is +\fImysql\fR +or +\fIpgsql\fR, this can be the path to a unix domain socket expressed like: +\fI"unix:/path/to/socket"\fR\&.The default value is +\fIlocalhost\fR\&. +.RE +.PP +\fBsql_ssl\fR: \fItrue | false\fR +.RS 4 +\fINote\fR +about this option: improved in 20\&.03\&. Whether to use SSL encrypted connections to the SQL server\&. The option is only available for MySQL, MS SQL and PostgreSQL\&. The default value is +\fIfalse\fR\&. +.RE +.PP +\fBsql_ssl_cafile\fR: \fIPath\fR +.RS 4 +A path to a file with CA root certificates that will be used to verify SQL connections\&. Implies +\fIsql_ssl\fR +and +\fIsql_ssl_verify\fR +options are set to +\fItrue\fR\&. There is no default which means certificate verification is disabled\&. This option has no effect for MS SQL\&. +.RE +.PP +\fBsql_ssl_certfile\fR: \fIPath\fR +.RS 4 +A path to a certificate file that will be used for SSL connections to the SQL server\&. Implies +\fIsql_ssl\fR +option is set to +\fItrue\fR\&. There is no default which means ejabberd won\(cqt provide a client certificate to the SQL server\&. This option has no effect for MS SQL\&. +.RE +.PP +\fBsql_ssl_verify\fR: \fItrue | false\fR +.RS 4 +Whether to verify SSL connection to the SQL server against CA root certificates defined in +\fIsql_ssl_cafile\fR +option\&. Implies +\fIsql_ssl\fR +option is set to +\fItrue\fR\&. This option has no effect for MS SQL\&. The default value is +\fIfalse\fR\&. +.RE +.PP +\fBsql_start_interval\fR: \fItimeout()\fR +.RS 4 +A time to wait before retrying to restore failed SQL connection\&. The default value is +\fI30\fR +seconds\&. +.RE +.PP +\fBsql_type\fR: \fImssql | mysql | odbc | pgsql | sqlite\fR +.RS 4 +The type of an SQL connection\&. The default is +\fIodbc\fR\&. +.RE +.PP +\fBsql_username\fR: \fIUsername\fR +.RS 4 +A user name for SQL authentication\&. The default value is +\fIejabberd\fR\&. +.RE +.PP +\fBtrusted_proxies\fR: \fIall | [Network1, Network2, \&.\&.\&.]\fR +.RS 4 +Specify what proxies are trusted when an HTTP request contains the header +\fIX\-Forwarded\-For\fR\&. You can specify +\fIall\fR +to allow all proxies, or specify a list of IPs, possibly with masks\&. The default value is an empty list\&. Using this option you can know the real IP of the request, for admin purpose, or security configuration (for example using +\fImod_fail2ban\fR)\&. IMPORTANT: The proxy MUST be configured to set the +\fIX\-Forwarded\-For\fR +header if you enable this option as, otherwise, the client can set it itself and as a result the IP value cannot be trusted for security rules in ejabberd\&. +.RE +.PP +\fBupdate_sql_schema\fR: \fItrue | false\fR +.RS 4 +\fINote\fR +about this option: updated in 24\&.06\&. Allow ejabberd to update SQL schema in MySQL, PostgreSQL and SQLite databases\&. This option was added in ejabberd 23\&.10, and enabled by default since 24\&.06\&. The default value is +\fItrue\fR\&. +.RE +.PP +\fBupdate_sql_schema_timeout\fR: \fItimeout()\fR +.RS 4 +\fINote\fR +about this option: added in 24\&.07\&. Time allocated to SQL schema update queries\&. The default value is set to 5 minutes\&. +.RE +.PP +\fBuse_cache\fR: \fItrue | false\fR +.RS 4 +Enable or disable cache\&. The default is +\fItrue\fR\&. Several modules have a similar option; and some core ejabberd parts support similar options too, see +\fIauth_use_cache\fR, +\fIoauth_use_cache\fR, +\fIrouter_use_cache\fR, and +\fIsm_use_cache\fR\&. +.RE +.PP +\fBvalidate_stream\fR: \fItrue | false\fR +.RS 4 +Whether to validate any incoming XML packet according to the schemas of +supported XMPP extensions\&. WARNING: the validation is only intended for the use by client developers \- don\(cqt enable it in production environment\&. The default value is +\fIfalse\fR\&. +.RE +.PP +\fBversion\fR: \fIstring()\fR +.RS 4 +The option can be used to set custom ejabberd version, that will be used by different parts of ejabberd, for example by +\fImod_version\fR +module\&. The default value is obtained at compile time from the underlying version control system\&. +.RE +.PP +\fBwebsocket_origin\fR: \fIignore | URL\fR +.RS 4 +This option enables validation for +\fIOrigin\fR +header to protect against connections from other domains than given in the configuration file\&. In this way, the lower layer load balancer can be chosen for a specific ejabberd implementation while still providing a secure WebSocket connection\&. The default value is +\fIignore\fR\&. An example value of the +\fIURL\fR +is +\fI"https://test\&.example\&.org:8081"\fR\&. +.RE +.PP +\fBwebsocket_ping_interval\fR: \fItimeout()\fR +.RS 4 +Defines time between pings sent by the server to a client (WebSocket level protocol pings are used for this) to keep a connection active\&. If the client doesn\(cqt respond to two consecutive pings, the connection will be assumed as closed\&. The value of +\fI0\fR +can be used to disable the feature\&. This option makes the server sending pings only for connections using the RFC compliant protocol\&. For older style connections the server expects that whitespace pings would be used for this purpose\&. The default value is +\fI60\fR +seconds\&. +.RE +.PP +\fBwebsocket_timeout\fR: \fItimeout()\fR +.RS 4 +Amount of time without any communication after which the connection would be closed\&. The default value is +\fI300\fR +seconds\&. +.RE +.SH "MODULES" +.sp +This section describes modules options of ejabberd 25\&.08\&. The modules that changed in this version are marked with 🟤\&. +.SS "mod_adhoc" +.sp +def:ad\-hoc command +.sp +: Command that can be executed by an XMPP client using XEP\-0050\&. +.sp +This module implements XEP\-0050: Ad\-Hoc Commands\&. It\(cqs an auxiliary module and is only needed by some of the other modules\&. +.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 +\fBreport_commands_node\fR: \fItrue | false\fR +.RS 4 +Provide the Commands item in the Service Discovery\&. Default value: +\fIfalse\fR\&. +.RE +.RE +.SS "mod_adhoc_api" +.sp +\fINote\fR about this option: added in 25\&.03\&. +.sp +Execute (def:API commands) in a XMPP client using XEP\-0050: Ad\-Hoc Commands\&. This module requires \fImod_adhoc\fR (to execute the commands), and recommends \fImod_disco\fR (to discover the commands)\&. +.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 +\fBdefault_version\fR: \fIinteger() | string()\fR +.RS 4 +What API version to use\&. If setting an ejabberd version, it will use the latest API version that was available in that (def:c2s) ejabberd version\&. For example, setting +\fI"24\&.06"\fR +in this option implies +\fI2\fR\&. The default value is the latest version\&. +.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 +acl: + admin: + user: jan@localhost + +api_permissions: + "adhoc commands": + from: mod_adhoc_api + who: admin + what: + \- "[tag:roster]" + \- "[tag:session]" + \- stats + \- status + +modules: + mod_adhoc_api: + default_version: 2 +.fi +.if n \{\ +.RE +.\} +.RE +.SS "mod_admin_extra" +.sp +This module provides additional administrative commands\&. +.sp +Details for some commands: +.sp +\fIban_account\fR API: This command kicks all the connected sessions of the account from the server\&. It also changes their password to a randomly generated one, so they can\(cqt login anymore unless a server administrator changes their password again\&. It is possible to define the reason of the ban\&. The new password also includes the reason and the date and time of the ban\&. See an example below\&. +.sp +\fIpush_roster\fR API (and \fIpush_roster_all\fR API): The roster file must be placed, if using Windows, on the directory where you installed ejabberd: C:/Program Files/ejabberd or similar\&. If you use other Operating System, place the file on the same directory where the \&.beam files are installed\&. See below an example roster file\&. +.sp +\fIsrg_create\fR API: If you want to put a group Name with blank spaces, use the characters \fI"\fR\*(Aq and \fI\*(Aq"\fR to define when the Name starts and ends\&. See an example below\&. +.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 +\fBExamples:\fR +.RS 4 +.sp +With this configuration, vCards can only be modified with mod_admin_extra commands: +.sp +.if n \{\ +.RS 4 +.\} +.nf +acl: + adminextraresource: + \- resource: "modadminextraf8x,31ad" +access_rules: + vcard_set: + \- allow: adminextraresource +modules: + mod_admin_extra: {} + mod_vcard: + access_set: vcard_set +.fi +.if n \{\ +.RE +.\} +.sp +Content of roster file for \fIpush_roster\fR API: +.sp +.if n \{\ +.RS 4 +.\} +.nf +[{<<"bob">>, <<"example\&.org">>, <<"workers">>, <<"Bob">>}, +{<<"mart">>, <<"example\&.org">>, <<"workers">>, <<"Mart">>}, +{<<"Rich">>, <<"example\&.org">>, <<"bosses">>, <<"Rich">>}]\&. +.fi +.if n \{\ +.RE +.\} +.sp +With this call, the sessions of the local account which JID is \fIboby@example\&.org\fR will be kicked, and its password will be set to something like \fIBANNED_ACCOUNT\(em20080425T21:45:07\(em2176635\(emSpammed_rooms\fR +.sp +.if n \{\ +.RS 4 +.\} +.nf +ejabberdctl vhost example\&.org ban_account boby "Spammed rooms" +.fi +.if n \{\ +.RE +.\} +.sp +Call to \fIsrg_create\fR API using double\-quotes and single\-quotes: +.sp +.if n \{\ +.RS 4 +.\} +.nf +ejabberdctl srg_create g1 example\&.org "\*(AqGroup number 1\*(Aq" this_is_g1 g1 +.fi +.if n \{\ +.RE +.\} +.sp +\fBAPI Tags:\fR \fI\&.\&./\&.\&./developer/ejabberd\-api/admin\-tags\&.md#accounts|accounts\fR, \fI\&.\&./\&.\&./developer/ejabberd\-api/admin\-tags\&.md#erlang|erlang\fR, \fI\&.\&./\&.\&./developer/ejabberd\-api/admin\-tags\&.md#last|last\fR, \fI\&.\&./\&.\&./developer/ejabberd\-api/admin\-tags\&.md#private|private\fR, \fI\&.\&./\&.\&./developer/ejabberd\-api/admin\-tags\&.md#purge|purge\fR, \fI\&.\&./\&.\&./developer/ejabberd\-api/admin\-tags\&.md#roster|roster\fR, \fI\&.\&./\&.\&./developer/ejabberd\-api/admin\-tags\&.md#session|session\fR, \fI\&.\&./\&.\&./developer/ejabberd\-api/admin\-tags\&.md#shared_roster_group|shared_roster_group\fR, \fI\&.\&./\&.\&./developer/ejabberd\-api/admin\-tags\&.md#stanza|stanza\fR, \fI\&.\&./\&.\&./developer/ejabberd\-api/admin\-tags\&.md#statistics|statistics\fR, \fI\&.\&./\&.\&./developer/ejabberd\-api/admin\-tags\&.md#vcard|vcard\fR +.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 \fIdatabase\&.md#default\-and\-new\-schemas|Default and New Schemas\fR for details\&. Please note that only MS SQL, MySQL, and PostgreSQL are supported\&. When the module is loaded use \fIupdate_sql\fR API\&. +.sp +The module has no options\&. +.SS "mod_announce" +.sp +This module enables configured users to broadcast announcements and to set the message of the day (MOTD)\&. Configured users can perform these actions with an XMPP client either using Ad\-Hoc Commands or sending messages to specific JIDs\&. +.if n \{\ +.sp +.\} +.RS 4 +.it 1 an-trap +.nr an-no-space-flag 1 +.nr an-break-flag 1 +.br +.ps +1 +\fBNote\fR +.ps -1 +.br +.sp +This module can be resource intensive on large deployments as it may broadcast a lot of messages\&. This module should be disabled for instances of ejabberd with hundreds of thousands users\&. +.sp .5v +.RE +.sp +To send announcements using XEP\-0050: Ad\-Hoc Commands, this module requires \fImod_adhoc\fR (to execute the commands), and recommends \fImod_disco\fR (to discover the commands)\&. +.sp +To send announcements by sending messages to specific JIDs, these are the destination JIDs: +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +\fIexample\&.org/announce/all\fR: Send the message to all registered users in that vhost\&. If the user is online and connected to several resources, only the resource with the highest priority will receive the message\&. If the registered user is not connected, the message is stored offline in assumption that offline storage (see +\fImod_offline\fR) is enabled\&. +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +\fIexample\&.org/announce/online\fR: Send the message to all connected users\&. If the user is online and connected to several resources, all resources will receive the message\&. +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +\fIexample\&.org/announce/motd\fR: Set the message of the day (MOTD) that is sent to users when they login\&. Also sends the message to all connected users (similar to +\fIannounce/online\fR)\&. +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +\fIexample\&.org/announce/motd/update\fR: Set the message of the day (MOTD) that is sent to users when they login\&. This does not send the message to any currently connected user\&. +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +\fIexample\&.org/announce/motd/delete\fR: Remove the existing message of the day (MOTD) by sending a message to this JID\&. +.RE +.sp +There are similar destination JIDs to apply to all virtual hosts in ejabberd: +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +\fIexample\&.org/announce/all\-hosts/all\fR: send to all registered accounts +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +\fIexample\&.org/announce/all\-hosts/online\fR: send to online sessions +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +\fIexample\&.org/announce/all\-hosts/motd\fR: set MOTD and send to online +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +\fIexample\&.org/announce/all\-hosts/motd/update\fR: update MOTD +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +\fIexample\&.org/announce/all\-hosts/motd/delete\fR: delete MOTD +.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 +\fBaccess\fR: \fIAccessName\fR +.RS 4 +This option specifies who is allowed to send announcements and to set the message of the day\&. The default value is +\fInone\fR +(i\&.e\&. nobody is able to send such messages)\&. +.RE +.PP +\fBcache_life_time\fR: \fItimeout()\fR +.RS 4 +Same as top\-level +\fIcache_life_time\fR +option, but applied to this module only\&. +.RE +.PP +\fBcache_missed\fR: \fItrue | false\fR +.RS 4 +Same as top\-level +\fIcache_missed\fR +option, but applied to this module only\&. +.RE +.PP +\fBcache_size\fR: \fIpos_integer() | infinity\fR +.RS 4 +Same as top\-level +\fIcache_size\fR +option, but applied to this module only\&. +.RE +.PP +\fBdb_type\fR: \fImnesia | sql\fR +.RS 4 +Same as top\-level +\fIdefault_db\fR +option, but applied to this module only\&. +.RE +.PP +\fBuse_cache\fR: \fItrue | false\fR +.RS 4 +Same as top\-level +\fIuse_cache\fR +option, but applied to this module only\&. +.RE +.RE +.SS "mod_antispam" +.sp +\fINote\fR about this option: added in 25\&.07\&. +.sp +Filter spam messages and subscription requests received from remote servers based on Real\-Time Block Lists (RTBL), lists of known spammer JIDs and/or URLs mentioned in spam messages\&. Traffic classified as spam is rejected with an error (and an \fI[info]\fR message is logged) unless the sender is subscribed to the recipient\(cqs presence\&. +.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 +\fBaccess_spam\fR: \fIAccess\fR +.RS 4 +Access rule that controls what accounts may receive spam messages\&. If the rule returns +\fIallow\fR +for a given recipient, spam messages aren\(cqt rejected for that recipient\&. The default value is +\fInone\fR, which means that all recipients are subject to spam filtering verification\&. +.RE +.PP +\fBcache_size\fR: \fIpos_integer()\fR +.RS 4 +Maximum number of JIDs that will be cached due to sending spam URLs\&. If that limit is exceeded, the least recently used entries are removed from the cache\&. Setting this option to +\fI0\fR +disables the caching feature\&. Note that separate caches are used for each virtual host, and that the caches aren\(cqt distributed across cluster nodes\&. The default value is +\fI10000\fR\&. +.RE +.PP +\fBrtbl_services\fR: \fI[Service]\fR +.RS 4 +Query a RTBL service to get domains to block, as provided by +xmppbl\&.org\&. Please note right now this option only supports one service in that list\&. For blocking spam and abuse on MUC channels, please use +\fImod_muc_rtbl\fR +for now\&. If only the host is provided, the default node names will be assumed\&. If the node name is different than +\fIspam_source_domains\fR, you can setup the custom node name with the option +\fIspam_source_domains_node\fR\&. The default value is an empty list of services\&. +.sp +\fBExample\fR: +.sp +.if n \{\ +.RS 4 +.\} +.nf +rtbl_services: + \- pubsub\&.server1\&.localhost: + spam_source_domains_node: actual_custom_pubsub_node +.fi +.if n \{\ +.RE +.\} +.RE +.PP +\fBspam_domains_file\fR: \fInone | Path\fR +.RS 4 +Path to a plain text file containing a list of known spam domains, one domain per line\&. Messages and subscription requests sent from one of the listed domains are classified as spam if sender is not in recipient\(cqs roster\&. This list of domains gets merged with the one retrieved by an RTBL host if any given\&. Use an absolute path, or the +\fI@CONFIG_PATH@\fR +predefined keyword +if the file is available in the configuration directory\&. The default value is +\fInone\fR\&. +.RE +.PP +\fBspam_dump_file\fR: \fIfalse | true | Path\fR +.RS 4 +Path to the file to store blocked messages\&. Use an absolute path, or the +\fI@LOG_PATH@\fR +predefined keyword +to store logs in the same place that the other ejabberd log files\&. If set to +\fIfalse\fR, it doesn\(cqt dump stanzas, which is the default\&. If set to +\fItrue\fR, it stores in +\fI"@LOG_PATH@/spam_dump_@HOST@\&.log"\fR\&. +.RE +.PP +\fBspam_jids_file\fR: \fInone | Path\fR +.RS 4 +Path to a plain text file containing a list of known spammer JIDs, one JID per line\&. Messages and subscription requests sent from one of the listed JIDs are classified as spam\&. Messages containing at least one of the listed JIDsare classified as spam as well\&. Furthermore, the sender\(cqs JID will be cached, so that future traffic originating from that JID will also be classified as spam\&. Use an absolute path, or the +\fI@CONFIG_PATH@\fR +predefined keyword +if the file is available in the configuration directory\&. The default value is +\fInone\fR\&. +.RE +.PP +\fBspam_urls_file\fR: \fInone | Path\fR +.RS 4 +Path to a plain text file containing a list of URLs known to be mentioned in spam message bodies\&. Messages containing at least one of the listed URLs are classified as spam\&. Furthermore, the sender\(cqs JID will be cached, so that future traffic originating from that JID will be classified as spam as well\&. Use an absolute path, or the +\fI@CONFIG_PATH@\fR +predefined keyword +if the file is available in the configuration directory\&. The default value is +\fInone\fR\&. +.RE +.PP +\fBwhitelist_domains_file\fR: \fInone | Path\fR +.RS 4 +Path to a file containing a list of domains to whitelist from being blocked, one per line\&. If either it is in +\fIspam_domains_file\fR +or more realistically in a domain sent by a RTBL host (see option +\fIrtbl_services\fR) then this domain will be ignored and stanzas from there won\(cqt be blocked\&. Use an absolute path, or the +\fI@CONFIG_PATH@\fR +predefined keyword +if the file is available in the configuration directory\&. The default value is +\fInone\fR\&. +.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 +modules: + mod_antispam: + rtbl_services: + \- xmppbl\&.org + spam_jids_file: "@CONFIG_PATH@/spam_jids\&.txt" + spam_dump_file: "@LOG_PATH@/spam/host\-@HOST@\&.log" +.fi +.if n \{\ +.RE +.\} +.RE +.SS "mod_auth_fast" +.sp +\fINote\fR about this option: added in 24\&.12\&. +.sp +The module adds support for XEP\-0484: Fast Authentication Streamlining Tokens that allows users to authenticate using self\-managed tokens\&. +.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 +\fBdb_type\fR: \fImnesia\fR +.RS 4 +Same as top\-level +\fIdefault_db\fR +option, but applied to this module only\&. +.RE +.PP +\fBtoken_lifetime\fR: \fItimeout()\fR +.RS 4 +Time that tokens will be kept, measured from it\(cqs creation time\&. Default value set to 30 days +.RE +.PP +\fBtoken_refresh_age\fR: \fItimeout()\fR +.RS 4 +This time determines age of token, that qualifies for automatic refresh\&. Default value set to 1 day +.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 +modules: + mod_auth_fast: + token_lifetime: 14days +.fi +.if n \{\ +.RE +.\} +.RE +.SS "mod_avatar" +.sp +The purpose of the module is to cope with legacy and modern XMPP clients posting avatars\&. The process is described in XEP\-0398: User Avatar to vCard\-Based Avatars Conversion\&. +.sp +Also, the module supports conversion between avatar image formats on the fly\&. +.sp +The module depends on \fImod_vcard\fR, \fImod_vcard_xupdate\fR and \fImod_pubsub\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 +\fBconvert\fR: \fI{From: To}\fR +.RS 4 +Defines image conversion rules: the format in +\fIFrom\fR +will be converted to format in +\fITo\fR\&. The value of +\fIFrom\fR +can also be +\fIdefault\fR, which is match\-all rule\&. NOTE: the list of supported formats is detected at compile time depending on the image libraries installed in the system\&. +.sp +\fBExample\fR: +.sp +.if n \{\ +.RS 4 +.\} +.nf +convert: + webp: jpg + default: png +.fi +.if n \{\ +.RE +.\} +.RE +.PP +\fBrate_limit\fR: \fINumber\fR +.RS 4 +Limit any given JID by the number of avatars it is able to convert per minute\&. This is to protect the server from image conversion DoS\&. The default value is +\fI10\fR\&. +.RE +.RE +.SS "mod_block_strangers" +.sp +This module blocks and logs any messages coming from an unknown entity\&. If a writing entity is not in your roster, you can let this module drop and/or log the message\&. By default you\(cqll just not receive message from that entity\&. Enable this module if you want to drop SPAM messages\&. +.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 +\fBaccess\fR: \fIAccessName\fR +.RS 4 +The option is supposed to be used when +\fIallow_local_users\fR +and +\fIallow_transports\fR +are not enough\&. It\(cqs an ACL where +\fIdeny\fR +means the message will be rejected (or a CAPTCHA would be generated for a presence, if configured), and +\fIallow\fR +means the sender is whitelisted and the stanza will pass through\&. The default value is +\fInone\fR, which means nothing is whitelisted\&. +.RE +.PP +\fBallow_local_users\fR: \fItrue | false\fR +.RS 4 +This option specifies if strangers from the same local host should be accepted or not\&. The default value is +\fItrue\fR\&. +.RE +.PP +\fBallow_transports\fR: \fItrue | false\fR +.RS 4 +If set to +\fItrue\fR +and some server\(cqs JID is in user\(cqs roster, then messages from any user of this server are accepted even if no subscription present\&. The default value is +\fItrue\fR\&. +.RE +.PP +\fBcaptcha\fR: \fItrue | false\fR +.RS 4 +Whether to generate CAPTCHA or not in response to messages from strangers\&. See also section +\fIbasic\&.md#captcha|CAPTCHA\fR +of the Configuration Guide\&. The default value is +\fIfalse\fR\&. +.RE +.PP +\fBdrop\fR: \fItrue | false\fR +.RS 4 +This option specifies if strangers messages should be dropped or not\&. The default value is +\fItrue\fR\&. +.RE +.PP +\fBlog\fR: \fItrue | false\fR +.RS 4 +This option specifies if strangers\*(Aq messages should be logged (as info message) in ejabberd\&.log\&. The default value is +\fIfalse\fR\&. +.RE +.RE +.SS "mod_blocking" +.sp +The module implements XEP\-0191: Blocking Command\&. +.sp +This module depends on \fImod_privacy\fR where all the configuration is performed\&. +.sp +The module has no options\&. +.SS "mod_bosh" +.sp +This module implements XMPP over BOSH as defined in XEP\-0124 and XEP\-0206\&. BOSH stands for Bidirectional\-streams Over Synchronous HTTP\&. It makes it possible to simulate long lived connections required by XMPP over the HTTP protocol\&. In practice, this module makes it possible to use XMPP in a browser without WebSocket support and more generally to have a way to use XMPP while having to get through an HTTP proxy\&. +.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 +\fBcache_life_time\fR: \fItimeout()\fR +.RS 4 +Same as top\-level +\fIcache_life_time\fR +option, but applied to this module only\&. +.RE +.PP +\fBcache_missed\fR: \fItrue | false\fR +.RS 4 +Same as top\-level +\fIcache_missed\fR +option, but applied to this module only\&. +.RE +.PP +\fBcache_size\fR: \fIpos_integer() | infinity\fR +.RS 4 +Same as top\-level +\fIcache_size\fR +option, but applied to this module only\&. +.RE +.PP +\fBjson\fR: \fItrue | false\fR +.RS 4 +This option has no effect\&. +.RE +.PP +\fBmax_concat\fR: \fIpos_integer() | infinity\fR +.RS 4 +This option limits the number of stanzas that the server will send in a single bosh request\&. The default value is +\fIunlimited\fR\&. +.RE +.PP +\fBmax_inactivity\fR: \fItimeout()\fR +.RS 4 +The option defines the maximum inactivity period\&. The default value is +\fI30\fR +seconds\&. +.RE +.PP +\fBmax_pause\fR: \fIpos_integer()\fR +.RS 4 +Indicate the maximum length of a temporary session pause (in seconds) that a client can request\&. The default value is +\fI120\fR\&. +.RE +.PP +\fBprebind\fR: \fItrue | false\fR +.RS 4 +If enabled, the client can create the session without going through authentication\&. Basically, it creates a new session with anonymous authentication\&. The default value is +\fIfalse\fR\&. +.RE +.PP +\fBqueue_type\fR: \fIram | file\fR +.RS 4 +Same as top\-level +\fIqueue_type\fR +option, but applied to this module only\&. +.RE +.PP +\fBram_db_type\fR: \fImnesia | sql | redis\fR +.RS 4 +Same as top\-level +\fIdefault_ram_db\fR +option, but applied to this module only\&. +.RE +.PP +\fBuse_cache\fR: \fItrue | false\fR +.RS 4 +Same as top\-level +\fIuse_cache\fR +option, but applied to this module only\&. +.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: 5222 + module: ejabberd_c2s + \- + port: 5443 + module: ejabberd_http + request_handlers: + /bosh: mod_bosh + +modules: + mod_bosh: {} +.fi +.if n \{\ +.RE +.\} +.RE +.SS "mod_caps" +.sp +This module implements XEP\-0115: Entity Capabilities\&. The main purpose of the module is to provide PEP functionality (see \fImod_pubsub\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 +\fBcache_life_time\fR: \fItimeout()\fR +.RS 4 +Same as top\-level +\fIcache_life_time\fR +option, but applied to this module only\&. +.RE +.PP +\fBcache_missed\fR: \fItrue | false\fR +.RS 4 +Same as top\-level +\fIcache_missed\fR +option, but applied to this module only\&. +.RE +.PP +\fBcache_size\fR: \fIpos_integer() | infinity\fR +.RS 4 +Same as top\-level +\fIcache_size\fR +option, but applied to this module only\&. +.RE +.PP +\fBdb_type\fR: \fImnesia | sql\fR +.RS 4 +Same as top\-level +\fIdefault_db\fR +option, but applied to this module only\&. +.RE +.PP +\fBuse_cache\fR: \fItrue | false\fR +.RS 4 +Same as top\-level +\fIuse_cache\fR +option, but applied to this module only\&. +.RE +.RE +.SS "mod_carboncopy" +.sp +The module implements XEP\-0280: Message Carbons\&. The module broadcasts messages on all connected user resources (devices)\&. +.sp +The module has no options\&. +.SS "mod_client_state" +.sp +This module allows for queueing certain types of stanzas when a client indicates that the user is not actively using the client right now (see XEP\-0352: Client State Indication)\&. This can save bandwidth and resources\&. +.sp +A stanza is dropped from the queue if it\(cqs effectively obsoleted by a new one (e\&.g\&., a new presence stanza would replace an old one from the same client)\&. The queue is flushed if a stanza arrives that won\(cqt be queued, or if the queue size reaches a certain limit (currently 100 stanzas), or if the client becomes active again\&. +.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 +\fBqueue_chat_states\fR: \fItrue | false\fR +.RS 4 +Queue "standalone" chat state notifications (as defined in +XEP\-0085: Chat State Notifications) while a client indicates inactivity\&. The default value is +\fItrue\fR\&. +.RE +.PP +\fBqueue_pep\fR: \fItrue | false\fR +.RS 4 +Queue PEP notifications while a client is inactive\&. When the queue is flushed, only the most recent notification of a given PEP node is delivered\&. The default value is +\fItrue\fR\&. +.RE +.PP +\fBqueue_presence\fR: \fItrue | false\fR +.RS 4 +While a client is inactive, queue presence stanzas that indicate (un)availability\&. The default value is +\fItrue\fR\&. +.RE +.RE +.SS "mod_configure" +.sp +The module provides server configuration functionalities using XEP\-0030: Service Discovery and XEP\-0050: Ad\-Hoc Commands: +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +List and discover outgoing s2s, online client sessions and all registered accounts +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +Most of the ad\-hoc commands defined in +XEP\-0133: Service Administration +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +Additional custom ad\-hoc commands specific to ejabberd +.RE +.sp +This module requires \fImod_adhoc\fR (to execute the commands), and recommends \fImod_disco\fR (to discover the commands)\&. +.sp +Please notice that all the ad\-hoc commands implemented by this module have an equivalent API Command that you can execute using \fImod_adhoc_api\fR or any other API frontend\&. +.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 +\fBaccess\fR: \fIAccessName\fR +.RS 4 +\fINote\fR +about this option: added in 25\&.03\&. This option defines which access rule will be used to control who is allowed to access the features provided by this module\&. The default value is +\fIconfigure\fR\&. +.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 +acl: + admin: + user: sun@localhost + +access_rules: + configure: + allow: admin + +modules: + mod_configure: + access: configure +.fi +.if n \{\ +.RE +.\} +.RE +.SS "mod_conversejs" +.sp +\fINote\fR about this option: improved in 25\&.07\&. +.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 → \fIlisten\-options\&.md#request_handlers|request_handlers\fR\&. +.sp +Make sure either \fImod_bosh\fR or \fIlisten\&.md#ejabberd_http_ws|ejabberd_http_ws\fR are enabled in at least one \fIrequest_handlers\fR\&. +.sp +When \fIconversejs_css\fR and \fIconversejs_script\fR are \fIauto\fR, by default they point to the public Converse client\&. +.sp +This module is available since ejabberd 21\&.12\&. +.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: \fIauto | BoshURL\fR +.RS 4 +BOSH service URL to which Converse can connect to\&. The keyword +\fI@HOST@\fR +is replaced with the real virtual host name\&. If set to +\fIauto\fR, it will build the URL of the first configured BOSH request handler\&. The default value is +\fIauto\fR\&. +.RE +.PP +\fBconversejs_css\fR: \fIauto | URL\fR +.RS 4 +Converse CSS URL\&. The keyword +\fI@HOST@\fR +is replaced with the hostname\&. The default value is +\fIauto\fR\&. +.RE +.PP +\fBconversejs_options\fR: \fI{Name: Value}\fR +.RS 4 +\fINote\fR +about this option: added in 22\&.05\&. Specify additional options to be passed to Converse\&. See +Converse configuration\&. Only boolean, integer and string values are supported; lists are not supported\&. +.RE +.PP +\fBconversejs_plugins\fR: \fI[Filename]\fR +.RS 4 +List of additional local files to include as scripts in the homepage\&. Please make sure those files are available in the path specified in +\fIconversejs_resources\fR +option, in subdirectory +\fIplugins/\fR\&. If using the public Converse client, then +\fI"libsignal"\fR +gets replaced with the URL of the public library\&. The default value is +\fI[]\fR\&. +.RE +.PP +\fBconversejs_resources\fR: \fIPath\fR +.RS 4 +\fINote\fR +about this option: added in 22\&.05\&. Local path to the Converse files\&. If not set, the public Converse client will be used instead\&. +.RE +.PP +\fBconversejs_script\fR: \fIauto | URL\fR +.RS 4 +Converse main script URL\&. The keyword +\fI@HOST@\fR +is replaced with the hostname\&. The default value is +\fIauto\fR\&. +.RE +.PP +\fBdefault_domain\fR: \fIDomain\fR +.RS 4 +Specify a domain to act as the default for user JIDs\&. The keyword +\fI@HOST@\fR +is replaced with the hostname\&. The default value is +\fI@HOST@\fR\&. +.RE +.PP +\fBwebsocket_url\fR: \fIauto | WebSocketURL\fR +.RS 4 +A WebSocket URL to which Converse can connect to\&. The +\fI@HOST@\fR +keyword is replaced with the real virtual host name\&. If set to +\fIauto\fR, it will build the URL of the first configured WebSocket request handler\&. The default value is +\fIauto\fR\&. +.RE +.RE +.sp +.it 1 an-trap +.nr an-no-space-flag 1 +.nr an-break-flag 1 +.br +.ps +1 +\fBExamples:\fR +.RS 4 +.sp +Manually setup WebSocket url, and use the public Converse client: +.sp +.if n \{\ +.RS 4 +.\} +.nf +listen: + \- + port: 5280 + module: ejabberd_http + request_handlers: + /bosh: mod_bosh + /websocket: ejabberd_http_ws + /conversejs: mod_conversejs + +modules: + mod_bosh: {} + mod_conversejs: + conversejs_plugins: ["libsignal"] + websocket_url: "ws://@HOST@:5280/websocket" +.fi +.if n \{\ +.RE +.\} +.sp +Host Converse locally and let auto detection of WebSocket and Converse URLs: +.sp +.if n \{\ +.RS 4 +.\} +.nf +listen: + \- + port: 443 + module: ejabberd_http + tls: true + request_handlers: + /websocket: ejabberd_http_ws + /conversejs: mod_conversejs + +modules: + mod_conversejs: + conversejs_resources: "/home/ejabberd/conversejs\-x\&.y\&.z/package/dist" + conversejs_plugins: ["libsignal\-protocol\&.min\&.js"] + # File path is: /home/ejabberd/conversejs\-x\&.y\&.z/package/dist/plugins/libsignal\-protocol\&.min\&.js +.fi +.if n \{\ +.RE +.\} +.sp +Configure some additional options for Converse +.sp +.if n \{\ +.RS 4 +.\} +.nf +modules: + mod_conversejs: + websocket_url: auto + conversejs_options: + auto_away: 30 + clear_cache_on_logout: true + i18n: "pt" + locked_domain: "@HOST@" + message_archiving: always + theme: dracula +.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\&. +.if n \{\ +.sp +.\} +.RS 4 +.it 1 an-trap +.nr an-no-space-flag 1 +.nr an-break-flag 1 +.br +.ps +1 +\fBWarning\fR +.ps -1 +.br +.sp +Security issue: Namespace delegation gives components access to sensitive data, so permission should be granted carefully, only if you trust the component\&. +.sp .5v +.RE +.if n \{\ +.sp +.\} +.RS 4 +.it 1 an-trap +.nr an-no-space-flag 1 +.nr an-break-flag 1 +.br +.ps +1 +\fBNote\fR +.ps -1 +.br +.sp +This module is complementary to \fImod_privilege\fR but can also be used separately\&. +.sp .5v +.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 +\fBnamespaces\fR: \fI{Namespace: Options}\fR +.RS 4 +If you want to delegate namespaces to a component, specify them in this option, and associate them to an access rule\&. The +\fIOptions\fR +are: +.PP +\fBaccess\fR: \fIAccessName\fR +.RS 4 +The option defines which components are allowed for namespace delegation\&. The default value is +\fInone\fR\&. +.RE +.PP +\fBfiltering\fR: \fIAttributes\fR +.RS 4 +The list of attributes\&. Currently not used\&. +.RE +.RE +.RE +.sp +.it 1 an-trap +.nr an-no-space-flag 1 +.nr an-break-flag 1 +.br +.ps +1 +\fBExamples:\fR +.RS 4 +.sp +Make sure you do not delegate the same namespace to several services at the same time\&. As in the example provided later, to have the \fIsat\-pubsub\&.example\&.org\fR component perform correctly disable the \fImod_pubsub\fR module\&. +.sp +.if n \{\ +.RS 4 +.\} +.nf +access_rules: + external_pubsub: + allow: external_component + external_mam: + allow: external_component + +acl: + external_component: + server: sat\-pubsub\&.example\&.org + +modules: + mod_delegation: + namespaces: + urn:xmpp:mam:1: + access: external_mam + http://jabber\&.org/protocol/pubsub: + access: external_pubsub +.fi +.if n \{\ +.RE +.\} +.RE +.SS "mod_disco" +.sp +This module adds support for XEP\-0030: Service Discovery\&. With this module enabled, services on your server can be discovered by XMPP clients\&. +.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 +\fBextra_domains\fR: \fI[Domain, \&.\&.\&.]\fR +.RS 4 +With this option, you can specify a list of extra domains that are added to the Service Discovery item list\&. The default value is an empty list\&. +.RE +.PP +\fBname\fR: \fIName\fR +.RS 4 +A name of the server in the Service Discovery\&. This will only be displayed by special XMPP clients\&. The default value is +\fIejabberd\fR\&. +.RE +.PP +\fBserver_info\fR: \fI[Info, \&.\&.\&.]\fR +.RS 4 +Specify additional information about the server, as described in +XEP\-0157: Contact Addresses for XMPP Services\&. Every +\fIInfo\fR +element in the list is constructed from the following options: +.PP +\fBmodules\fR: \fIall | [Module, \&.\&.\&.]\fR +.RS 4 +The value can be the keyword +\fIall\fR, in which case the information is reported in all the services, or a list of ejabberd modules, in which case the information is only specified for the services provided by those modules\&. +.RE +.PP +\fBname\fR: \fIName\fR +.RS 4 +The field +\fIvar\fR +name that will be defined\&. See XEP\-0157 for some standardized names\&. +.RE +.PP +\fBurls\fR: \fI[URI, \&.\&.\&.]\fR +.RS 4 +A list of contact URIs, such as HTTP URLs, XMPP URIs and so on\&. +.RE +.sp +\fBExample\fR: +.sp +.if n \{\ +.RS 4 +.\} +.nf +server_info: + \- + modules: all + name: abuse\-addresses + urls: ["mailto:abuse@shakespeare\&.lit"] + \- + modules: [mod_muc] + name: "Web chatroom logs" + urls: ["http://www\&.example\&.org/muc\-logs"] + \- + modules: [mod_disco] + name: feedback\-addresses + urls: + \- http://shakespeare\&.lit/feedback\&.php + \- mailto:feedback@shakespeare\&.lit + \- xmpp:feedback@shakespeare\&.lit + \- + modules: + \- mod_disco + \- mod_vcard + name: admin\-addresses + urls: + \- mailto:xmpp@shakespeare\&.lit + \- xmpp:admins@shakespeare\&.lit +.fi +.if n \{\ +.RE +.\} +.RE +.RE +.SS "mod_fail2ban" +.sp +The module bans IPs that show the malicious signs\&. Currently only C2S authentication failures are detected\&. +.sp +Unlike the standalone program, \fImod_fail2ban\fR clears the record of authentication failures after some time since the first failure or on a successful authentication\&. It also does not simply block network traffic, but provides the client with a descriptive error message\&. +.if n \{\ +.sp +.\} +.RS 4 +.it 1 an-trap +.nr an-no-space-flag 1 +.nr an-break-flag 1 +.br +.ps +1 +\fBWarning\fR +.ps -1 +.br +.sp +You should not use this module behind a proxy or load balancer\&. ejabberd will see the failures as coming from the load balancer and, when the threshold of auth failures is reached, will reject all connections coming from the load balancer\&. You can lock all your user base out of ejabberd when using this module behind a proxy\&. +.sp .5v +.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 +\fBaccess\fR: \fIAccessName\fR +.RS 4 +Specify an access rule for whitelisting IP addresses or networks\&. If the rule returns +\fIallow\fR +for a given IP address, that address will never be banned\&. The +\fIAccessName\fR +should be of type +\fIip\fR\&. The default value is +\fInone\fR\&. +.RE +.PP +\fBc2s_auth_ban_lifetime\fR: \fItimeout()\fR +.RS 4 +The lifetime of the IP ban caused by too many C2S authentication failures\&. The default value is +\fI1\fR +hour\&. +.RE +.PP +\fBc2s_max_auth_failures\fR: \fINumber\fR +.RS 4 +The number of C2S authentication failures to trigger the IP ban\&. The default value is +\fI20\fR\&. +.RE +.sp +\fBAPI Tags:\fR \fI\&.\&./\&.\&./developer/ejabberd\-api/admin\-tags\&.md#accounts|accounts\fR +.RE +.SS "mod_host_meta" +.sp +\fINote\fR about this option: added in 22\&.05\&. +.sp +This module serves small \fIhost\-meta\fR files as described in XEP\-0156: Discovering Alternative XMPP Connection Methods\&. +.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 → \fIlisten\-options\&.md#request_handlers|request_handlers\fR\&. +.sp +Notice it only works if \fIlisten\&.md#ejabberd_http|ejabberd_http\fR has \fIlisten\-options\&.md#tls|tls\fR enabled\&. +.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: \fIundefined | auto | BoshURL\fR +.RS 4 +BOSH service URL to announce\&. The keyword +\fI@HOST@\fR +is replaced with the real virtual host name\&. If set to +\fIauto\fR, it will build the URL of the first configured BOSH request handler\&. The default value is +\fIauto\fR\&. +.RE +.PP +\fBwebsocket_url\fR: \fIundefined | auto | WebSocketURL\fR +.RS 4 +WebSocket URL to announce\&. The keyword +\fI@HOST@\fR +is replaced with the real virtual host name\&. If set to +\fIauto\fR, it will build the URL of the first configured WebSocket request handler\&. The default value is +\fIauto\fR\&. +.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: 443 + module: ejabberd_http + tls: true + request_handlers: + /bosh: mod_bosh + /ws: ejabberd_http_ws + /\&.well\-known/host\-meta: mod_host_meta + /\&.well\-known/host\-meta\&.json: mod_host_meta + +modules: + mod_bosh: {} + mod_host_meta: + bosh_service_url: "https://@HOST@:5443/bosh" + websocket_url: "wss://@HOST@:5443/ws" +.fi +.if n \{\ +.RE +.\} +.RE +.SS "mod_http_api" +.sp +This module provides a ReST interface to call \fI\&.\&./\&.\&./developer/ejabberd\-api/index\&.md|ejabberd API\fR commands using JSON data\&. +.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 → \fIlisten\-options\&.md#request_handlers|request_handlers\fR\&. +.sp +To use a specific API version N, when defining the URL path in the request_handlers, add a vN\&. 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 +.it 1 an-trap +.nr an-no-space-flag 1 +.nr an-break-flag 1 +.br +.ps +1 +\fBAvailable options:\fR +.RS 4 +.PP +\fBdefault_version\fR: \fIinteger() | string()\fR +.RS 4 +\fINote\fR +about this option: added in 24\&.12\&. What API version to use when none is specified in the URL path\&. If setting an ejabberd version, it will use the latest API version that was available in that ejabberd version\&. For example, setting +\fI"24\&.06"\fR +in this option implies +\fI2\fR\&. The default value is the latest version\&. +.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: + /api: mod_http_api + +modules: + mod_http_api: + default_version: 2 +.fi +.if n \{\ +.RE +.\} +.RE +.SS "mod_http_fileserver" +.sp +This simple module serves files from the local disk over HTTP\&. +.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 +\fBaccesslog\fR: \fIPath\fR +.RS 4 +File to log accesses using an Apache\-like format\&. No log will be recorded if this option is not specified\&. +.RE +.PP +\fBcontent_types\fR: \fI{Extension: Type}\fR +.RS 4 +Specify mappings of extension to content type\&. There are several content types already defined\&. With this option you can add new definitions or modify existing ones\&. The default values are: +.sp +\fBExample\fR: +.sp +.if n \{\ +.RS 4 +.\} +.nf +content_types: + \&.css: text/css + \&.gif: image/gif + \&.html: text/html + \&.jar: application/java\-archive + \&.jpeg: image/jpeg + \&.jpg: image/jpeg + \&.js: text/javascript + \&.png: image/png + \&.svg: image/svg+xml + \&.txt: text/plain + \&.xml: application/xml + \&.xpi: application/x\-xpinstall + \&.xul: application/vnd\&.mozilla\&.xul+xml +.fi +.if n \{\ +.RE +.\} +.RE +.PP +\fBcustom_headers\fR: \fI{Name: Value}\fR +.RS 4 +Indicate custom HTTP headers to be included in all responses\&. There are no custom headers by default\&. +.RE +.PP +\fBdefault_content_type\fR: \fIType\fR +.RS 4 +Specify the content type to use for unknown extensions\&. The default value is +\fIapplication/octet\-stream\fR\&. +.RE +.PP +\fBdirectory_indices\fR: \fI[Index, \&.\&.\&.]\fR +.RS 4 +Indicate one or more directory index files, similarly to Apache\(cqs +\fIDirectoryIndex\fR +variable\&. When an HTTP request hits a directory instead of a regular file, those directory indices are looked in order, and the first one found is returned\&. The default value is an empty list\&. +.RE +.PP +\fBdocroot\fR: \fIPath\fR +.RS 4 +Directory to serve the files from\&. This is a mandatory option\&. +.RE +.PP +\fBmust_authenticate_with\fR: \fI[{Username, Hostname}, \&.\&.\&.]\fR +.RS 4 +List of accounts that are allowed to use this service\&. Default value: +\fI[]\fR\&. +.RE +.RE +.sp +.it 1 an-trap +.nr an-no-space-flag 1 +.nr an-break-flag 1 +.br +.ps +1 +\fBExamples:\fR +.RS 4 +.sp +This example configuration will serve the files from the local directory \fI/var/www\fR in the address \fIhttp://example\&.org:5280/pub/content/\fR\&. In this example a new content type \fIogg\fR is defined, \fIpng\fR is redefined, and \fIjpg\fR definition is deleted: +.sp +.if n \{\ +.RS 4 +.\} +.nf +listen: + \- + port: 5280 + module: ejabberd_http + request_handlers: + /pub/content: mod_http_fileserver + +modules: + mod_http_fileserver: + docroot: /var/www + accesslog: /var/log/ejabberd/access\&.log + directory_indices: + \- index\&.html + \- main\&.htm + custom_headers: + X\-Powered\-By: Erlang/OTP + X\-Fry: "It\*(Aqs a widely\-believed fact!" + content_types: + \&.ogg: audio/ogg + \&.png: image/png + default_content_type: text/html +.fi +.if n \{\ +.RE +.\} +.RE +.SS "mod_http_upload" +.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 enabled in \fIlisten\fR → \fIejabberd_http\fR → \fIlisten\-options\&.md#request_handlers|request_handlers\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 +\fBaccess\fR: \fIAccessName\fR +.RS 4 +This option defines the access rule to limit who is permitted to use the HTTP upload service\&. The default value is +\fIlocal\fR\&. If no access rule of that name exists, no user will be allowed to use the service\&. +.RE +.PP +\fBcustom_headers\fR: \fI{Name: Value}\fR +.RS 4 +This option specifies additional header fields to be included in all HTTP responses\&. By default no custom headers are included\&. +.RE +.PP +\fBdir_mode\fR: \fIPermission\fR +.RS 4 +This option defines the permission bits of the +\fIdocroot\fR +directory and any directories created during file uploads\&. The bits are specified as an octal number (see the +\fIchmod(1)\fR +manual page) within double quotes\&. For example: +\fI"0755"\fR\&. The default is undefined, which means no explicit permissions will be set\&. +.RE +.PP +\fBdocroot\fR: \fIPath\fR +.RS 4 +Uploaded files are stored below the directory specified (as an absolute path) with this option\&. The keyword +\fI@HOME@\fR +is replaced with the home directory of the user running ejabberd, and the keyword +\fI@HOST@\fR +with the virtual host name\&. The default value is +\fI"@HOME@/upload"\fR\&. +.RE +.PP +\fBexternal_secret\fR: \fIText\fR +.RS 4 +This option makes it possible to offload all HTTP Upload processing to a separate HTTP server\&. Both ejabberd and the HTTP server should share this secret and behave exactly as described at +Prosody\(cqs mod_http_upload_external: Implementation\&. There is no default value\&. +.RE +.PP +\fBfile_mode\fR: \fIPermission\fR +.RS 4 +This option defines the permission bits of uploaded files\&. The bits are specified as an octal number (see the +\fIchmod(1)\fR +manual page) within double quotes\&. For example: +\fI"0644"\fR\&. The default is undefined, which means no explicit permissions will be set\&. +.RE +.PP +\fBget_url\fR: \fIURL\fR +.RS 4 +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 +\fI@HOST@\fR +is replaced with the virtual host name\&. NOTE: if GET requests are handled by this module, the +\fIget_url\fR +must match the +\fIput_url\fR\&. Setting it to a different value only makes sense if an external web server or +\fImod_http_fileserver\fR +is used to serve the uploaded files\&. +.RE +.PP +\fBhost\fR +.RS 4 +Deprecated\&. Use +\fIhosts\fR +instead\&. +.RE +.PP +\fBhosts\fR: \fI[Host, \&.\&.\&.]\fR +.RS 4 +This option defines the Jabber IDs of the service\&. If the +\fIhosts\fR +option is not specified, the only Jabber ID will be the hostname of the virtual host with the prefix +\fI"upload\&."\fR\&. The keyword +\fI@HOST@\fR +is replaced with the real virtual host name\&. +.RE +.PP +\fBjid_in_url\fR: \fInode | sha1\fR +.RS 4 +When this option is set to +\fInode\fR, the node identifier of the user\(cqs JID (i\&.e\&., the user name) is included in the GET and PUT URLs generated by +\fImod_http_upload\fR\&. Otherwise, a SHA\-1 hash of the user\(cqs bare JID is included instead\&. The default value is +\fIsha1\fR\&. +.RE +.PP +\fBmax_size\fR: \fISize\fR +.RS 4 +This option limits the acceptable file size\&. Either a number of bytes (larger than zero) or +\fIinfinity\fR +must be specified\&. The default value is +\fI104857600\fR\&. +.RE +.PP +\fBname\fR: \fIName\fR +.RS 4 +A name of the service in the Service Discovery\&. The default value is +\fI"HTTP File Upload"\fR\&. Please note this will only be displayed by some XMPP clients\&. +.RE +.PP +\fBput_url\fR: \fIURL\fR +.RS 4 +This option specifies the initial part of the PUT URLs used for file uploads\&. The keyword +\fI@HOST@\fR +is replaced with the virtual host name\&. And +\fI@HOST_URL_ENCODE@\fR +is replaced with the host name encoded for URL, useful when your virtual hosts contain non\-latin characters\&. NOTE: different virtual hosts cannot use the same PUT URL\&. The default value is +\fI"https://@HOST@:5443/upload"\fR\&. +.RE +.PP +\fBrm_on_unregister\fR: \fItrue | false\fR +.RS 4 +This option specifies whether files uploaded by a user should be removed when that user is unregistered\&. The default value is +\fItrue\fR\&. +.RE +.PP +\fBsecret_length\fR: \fILength\fR +.RS 4 +This option defines the length of the random string included in the GET and PUT URLs generated by +\fImod_http_upload\fR\&. The minimum length is +\fI8\fR +characters, but it is recommended to choose a larger value\&. The default value is +\fI40\fR\&. +.RE +.PP +\fBservice_url\fR +.RS 4 +Deprecated\&. +.RE +.PP +\fBthumbnail\fR: \fItrue | false\fR +.RS 4 +This option specifies whether ejabberd should create thumbnails of uploaded images\&. If a thumbnail is created, a element that contains the download and some metadata is returned with the PUT response\&. The default value is +\fIfalse\fR\&. +.RE +.PP +\fBvcard\fR: \fIvCard\fR +.RS 4 +A custom vCard of the service that will be displayed by some XMPP clients in Service Discovery\&. The value of +\fIvCard\fR +is a YAML map constructed from an XML representation of vCard\&. Since the representation has no attributes, the mapping is straightforward\&. +.sp +\fBExample\fR: +.sp +.if n \{\ +.RS 4 +.\} +.nf +# This XML representation of vCard: +# +# Conferences +# +# +# Elm Street +# +# +# +# is translated to: +vcard: + fn: Conferences + adr: + \- + work: true + street: Elm Street +.fi +.if n \{\ +.RE +.\} +.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: 5443 + module: ejabberd_http + tls: true + request_handlers: + /upload: mod_http_upload + +modules: + mod_http_upload: + docroot: /ejabberd/upload + put_url: "https://@HOST@:5443/upload" +.fi +.if n \{\ +.RE +.\} +.RE +.SS "mod_http_upload_quota" +.sp +This module adds quota support for mod_http_upload\&. +.sp +This module depends on \fImod_http_upload\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 +\fBaccess_hard_quota\fR: \fIAccessName\fR +.RS 4 +This option defines which access rule is used to specify the "hard quota" for the matching JIDs\&. That rule must yield a positive number for any JID that is supposed to have a quota limit\&. This is the number of megabytes a corresponding user may upload\&. When this threshold is exceeded, ejabberd deletes the oldest files uploaded by that user until their disk usage equals or falls below the specified soft quota (see also option +\fIaccess_soft_quota\fR)\&. The default value is +\fIhard_upload_quota\fR\&. +.RE +.PP +\fBaccess_soft_quota\fR: \fIAccessName\fR +.RS 4 +This option defines which access rule is used to specify the "soft quota" for the matching JIDs\&. That rule must yield a positive number of megabytes for any JID that is supposed to have a quota limit\&. See the description of the +\fIaccess_hard_quota\fR +option for details\&. The default value is +\fIsoft_upload_quota\fR\&. +.RE +.PP +\fBmax_days\fR: \fIDays\fR +.RS 4 +If a number larger than zero is specified, any files (and directories) older than this number of days are removed from the subdirectories of the +\fIdocroot\fR +directory, once per day\&. The default value is +\fIinfinity\fR\&. +.RE +.RE +.sp +.it 1 an-trap +.nr an-no-space-flag 1 +.nr an-break-flag 1 +.br +.ps +1 +\fBExamples:\fR +.RS 4 +.sp +Notice it\(cqs not necessary to specify the \fIaccess_hard_quota\fR and \fIaccess_soft_quota\fR options in order to use the quota feature\&. You can stick to the default names and just specify access rules such as those in this example: +.sp +.if n \{\ +.RS 4 +.\} +.nf +shaper_rules: + soft_upload_quota: + 1000: all # MiB + hard_upload_quota: + 1100: all # MiB + +modules: + mod_http_upload: {} + mod_http_upload_quota: + max_days: 100 +.fi +.if n \{\ +.RE +.\} +.RE +.SS "mod_jidprep" +.sp +This module allows XMPP clients to ask the server to normalize a JID as per the rules specified in RFC 6122: XMPP Address Format\&. This might be useful for clients in certain constrained environments, or for testing purposes\&. +.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 +\fBaccess\fR: \fIAccessName\fR +.RS 4 +This option defines which access rule will be used to control who is allowed to use this service\&. The default value is +\fIlocal\fR\&. +.RE +.RE +.SS "mod_last" +.sp +This module adds support for XEP\-0012: Last Activity\&. It can be used to discover when a disconnected user last accessed the server, to know when a connected user was last active on the server, or to query the uptime of the ejabberd server\&. +.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 +\fBcache_life_time\fR: \fItimeout()\fR +.RS 4 +Same as top\-level +\fIcache_life_time\fR +option, but applied to this module only\&. +.RE +.PP +\fBcache_missed\fR: \fItrue | false\fR +.RS 4 +Same as top\-level +\fIcache_missed\fR +option, but applied to this module only\&. +.RE +.PP +\fBcache_size\fR: \fIpos_integer() | infinity\fR +.RS 4 +Same as top\-level +\fIcache_size\fR +option, but applied to this module only\&. +.RE +.PP +\fBdb_type\fR: \fImnesia | sql\fR +.RS 4 +Same as top\-level +\fIdefault_db\fR +option, but applied to this module only\&. +.RE +.PP +\fBuse_cache\fR: \fItrue | false\fR +.RS 4 +Same as top\-level +\fIuse_cache\fR +option, but applied to this module only\&. +.RE +.RE +.SS "mod_legacy_auth" +.sp +The module implements XEP\-0078: Non\-SASL Authentication\&. +.if n \{\ +.sp +.\} +.RS 4 +.it 1 an-trap +.nr an-no-space-flag 1 +.nr an-break-flag 1 +.br +.ps +1 +\fBNote\fR +.ps -1 +.br +.sp +This type of authentication was obsoleted in 2008 and you unlikely need this module unless you have something like outdated Jabber bots\&. +.sp .5v +.RE +.sp +The module has no options\&. +.SS "mod_mam" +.sp +This module implements XEP\-0313: Message Archive Management and XEP\-0441: Message Archive Management Preferences\&. Compatible XMPP clients can use it to store their chat history on the server\&. +.if n \{\ +.sp +.\} +.RS 4 +.it 1 an-trap +.nr an-no-space-flag 1 +.nr an-break-flag 1 +.br +.ps +1 +\fBNote\fR +.ps -1 +.br +.sp +Mnesia backend for mod_mam is not recommended: it\(cqs limited to 2GB and often gets corrupted when reaching this limit\&. SQL backend is recommended\&. Namely, for small servers SQLite is a preferred choice because it\(cqs very easy to configure\&. +.sp .5v +.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 +\fBaccess_preferences\fR: \fIAccessName\fR +.RS 4 +This access rule defines who is allowed to modify the MAM preferences\&. The default value is +\fIall\fR\&. +.RE +.PP +\fBassume_mam_usage\fR: \fItrue | false\fR +.RS 4 +This option determines how ejabberd\(cqs stream management code (see +\fImod_stream_mgmt\fR) handles unacknowledged messages when the connection is lost\&. Usually, such messages are either bounced or resent\&. However, neither is done for messages that were stored in the user\(cqs MAM archive if this option is set to +\fItrue\fR\&. In this case, ejabberd assumes those messages will be retrieved from the archive\&. The default value is +\fIfalse\fR\&. +.RE +.PP +\fBcache_life_time\fR: \fItimeout()\fR +.RS 4 +Same as top\-level +\fIcache_life_time\fR +option, but applied to this module only\&. +.RE +.PP +\fBcache_missed\fR: \fItrue | false\fR +.RS 4 +Same as top\-level +\fIcache_missed\fR +option, but applied to this module only\&. +.RE +.PP +\fBcache_size\fR: \fIpos_integer() | infinity\fR +.RS 4 +Same as top\-level +\fIcache_size\fR +option, but applied to this module only\&. +.RE +.PP +\fBclear_archive_on_room_destroy\fR: \fItrue | false\fR +.RS 4 +Whether to destroy message archive of a room (see +\fImod_muc\fR) when it gets destroyed\&. The default value is +\fItrue\fR\&. +.RE +.PP +\fBcompress_xml\fR: \fItrue | false\fR +.RS 4 +When enabled, new messages added to archives are compressed using a custom compression algorithm\&. This feature works only with SQL backends\&. The default value is +\fIfalse\fR\&. +.RE +.PP +\fBdb_type\fR: \fImnesia | sql\fR +.RS 4 +Same as top\-level +\fIdefault_db\fR +option, but applied to this module only\&. +.RE +.PP +\fBdefault\fR: \fIalways | never | roster\fR +.RS 4 +The option defines default policy for chat history\&. When +\fIalways\fR +is set every chat message is stored\&. With +\fIroster\fR +only chat history with contacts from user\(cqs roster is stored\&. And +\fInever\fR +fully disables chat history\&. Note that a client can change its policy via protocol commands\&. The default value is +\fInever\fR\&. +.RE +.PP +\fBrequest_activates_archiving\fR: \fItrue | false\fR +.RS 4 +If the value is +\fItrue\fR, no messages are stored for a user until their client issue a MAM request, regardless of the value of the +\fIdefault\fR +option\&. Once the server received a request, that user\(cqs messages are archived as usual\&. The default value is +\fIfalse\fR\&. +.RE +.PP +\fBuse_cache\fR: \fItrue | false\fR +.RS 4 +Same as top\-level +\fIuse_cache\fR +option, but applied to this module only\&. +.RE +.PP +\fBuser_mucsub_from_muc_archive\fR: \fItrue | false\fR +.RS 4 +When this option is disabled, for each individual subscriber a separate mucsub message is stored\&. With this option enabled, when a user fetches archive virtual mucsub, messages are generated from muc archives\&. The default value is +\fIfalse\fR\&. +.RE +.sp +\fBAPI Tags:\fR \fI\&.\&./\&.\&./developer/ejabberd\-api/admin\-tags\&.md#mam|mam\fR, \fI\&.\&./\&.\&./developer/ejabberd\-api/admin\-tags\&.md#purge|purge\fR +.RE +.SS "mod_matrix_gw 🟤" +.sp +\fINote\fR about this option: improved in 25\&.08\&. +.sp +Matrix gateway\&. Supports room versions 9, 10 and 11 since ejabberd 25\&.03; room versions 4 and higher since ejabberd 25\&.07; room version 12 (hydra rooms) since ejabberd 25\&.08\&. Erlang/OTP 25 or higher is required to use this module\&. This module is available since ejabberd 24\&.02\&. +.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 +\fBhost\fR: \fIHost\fR +.RS 4 +This option defines the Jabber IDs of the service\&. If the +\fIhost\fR +option is not specified, the Jabber ID will be the hostname of the virtual host with the prefix +\fI"matrix\&."\fR\&. The keyword +\fI@HOST@\fR +is replaced with the real virtual host name\&. +.RE +.PP +\fBkey\fR: \fIstring()\fR +.RS 4 +Value of the matrix signing key, in base64\&. +.RE +.PP +\fBkey_name\fR: \fIstring()\fR +.RS 4 +Name of the matrix signing key\&. +.RE +.PP +\fBleave_timeout\fR: \fIinteger()\fR +.RS 4 +Delay in seconds between a user leaving a MUC room and sending +\fIleave\fR +Matrix event\&. +.RE +.PP +\fBmatrix_domain\fR: \fIDomain\fR +.RS 4 +Specify a domain in the Matrix federation\&. The keyword +\fI@HOST@\fR +is replaced with the hostname\&. The default value is +\fI@HOST@\fR\&. +.RE +.PP +\fBmatrix_id_as_jid\fR: \fItrue | false\fR +.RS 4 +If set to +\fItrue\fR, all packets failing to be delivered via an XMPP server\-to\-server connection will then be routed to the Matrix gateway by translating a Jabber ID +\fIuser@matrixdomain\&.tld\fR +to a Matrix user identifier +\fI@user:matrixdomain\&.tld\fR\&. When set to +\fIfalse\fR, messages must be explicitly sent to the matrix gateway service Jabber ID to be routed to a remote Matrix server\&. In this case, to send a message to Matrix user +\fI@user:matrixdomain\&.tld\fR, the client must send a message to the JID +\fIuser%\fR\fImatrixdomain\&.tld@matrix\&.myxmppdomain\fR\fI\&.tld\fR, where +\fImatrix\&.myxmppdomain\&.tld\fR +is the JID of the gateway service as set by the +\fIhost\fR +option\&. The default is +\fIfalse\fR\&. +.RE +.PP +\fBnotary_servers\fR: \fI[Server, \&.\&.\&.]\fR +.RS 4 +A list of notary servers\&. +.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: 8448 + module: ejabberd_http + tls: true + request_handlers: + "/_matrix": mod_matrix_gw + +modules: + mod_matrix_gw: + key_name: "key1" + key: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + matrix_id_as_jid: true +.fi +.if n \{\ +.RE +.\} +.RE +.SS "mod_metrics" +.sp +This module sends events to external backend (by now only grapherl is supported)\&. Supported events are: +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +sm_register_connection +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +sm_remove_connection +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +user_send_packet +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +user_receive_packet +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +s2s_send_packet +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +s2s_receive_packet +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +register_user +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +remove_user +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +offline_message +.RE +.sp +When enabled, every call to these hooks triggers a counter event to be sent to the external backend\&. +.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 +\fBip\fR: \fIIPv4Address\fR +.RS 4 +IPv4 address where the backend is located\&. The default value is +\fI127\&.0\&.0\&.1\fR\&. +.RE +.PP +\fBport\fR: \fIPort\fR +.RS 4 +An internet port number at which the backend is listening for incoming connections/packets\&. The default value is +\fI11111\fR\&. +.RE +.RE +.SS "mod_mix" +.sp +\fINote\fR about this option: added in 16\&.03 and improved in 19\&.02\&. +.sp +This module is an experimental implementation of XEP\-0369: Mediated Information eXchange (MIX)\&. It\(cqs asserted that the MIX protocol is going to replace the MUC protocol in the future (see \fImod_muc\fR)\&. +.sp +To learn more about how to use that feature, you can refer to our tutorial: \fI\&.\&./\&.\&./tutorials/mix\-010\&.md|Getting started with MIX\fR +.sp +The module depends on \fImod_mam\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 +\fBaccess_create\fR: \fIAccessName\fR +.RS 4 +An access rule to control MIX channels creations\&. The default value is +\fIall\fR\&. +.RE +.PP +\fBdb_type\fR: \fImnesia | sql\fR +.RS 4 +Same as top\-level +\fIdefault_db\fR +option, but applied to this module only\&. +.RE +.PP +\fBhost\fR +.RS 4 +Deprecated\&. Use +\fIhosts\fR +instead\&. +.RE +.PP +\fBhosts\fR: \fI[Host, \&.\&.\&.]\fR +.RS 4 +This option defines the Jabber IDs of the service\&. If the +\fIhosts\fR +option is not specified, the only Jabber ID will be the hostname of the virtual host with the prefix +\fI"mix\&."\fR\&. The keyword +\fI@HOST@\fR +is replaced with the real virtual host name\&. +.RE +.PP +\fBname\fR: \fIName\fR +.RS 4 +A name of the service in the Service Discovery\&. This will only be displayed by special XMPP clients\&. The default value is +\fIChannels\fR\&. +.RE +.RE +.SS "mod_mix_pam" +.sp +This module implements XEP\-0405: Mediated Information eXchange (MIX): Participant Server Requirements\&. The module is needed if MIX compatible clients on your server are going to join MIX channels (either on your server or on any remote servers)\&. +.if n \{\ +.sp +.\} +.RS 4 +.it 1 an-trap +.nr an-no-space-flag 1 +.nr an-break-flag 1 +.br +.ps +1 +\fBNote\fR +.ps -1 +.br +.sp +\fImod_mix\fR is not required for this module to work, however, without \fImod_mix_pam\fR the MIX functionality of your local XMPP clients will be impaired\&. +.sp .5v +.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 +\fBcache_life_time\fR: \fItimeout()\fR +.RS 4 +Same as top\-level +\fIcache_life_time\fR +option, but applied to this module only\&. +.RE +.PP +\fBcache_missed\fR: \fItrue | false\fR +.RS 4 +Same as top\-level +\fIcache_missed\fR +option, but applied to this module only\&. +.RE +.PP +\fBcache_size\fR: \fIpos_integer() | infinity\fR +.RS 4 +Same as top\-level +\fIcache_size\fR +option, but applied to this module only\&. +.RE +.PP +\fBdb_type\fR: \fImnesia | sql\fR +.RS 4 +Same as top\-level +\fIdefault_db\fR +option, but applied to this module only\&. +.RE +.PP +\fBuse_cache\fR: \fItrue | false\fR +.RS 4 +Same as top\-level +\fIuse_cache\fR +option, but applied to this module only\&. +.RE +.RE +.SS "mod_mqtt" +.sp +This module adds \fI\&.\&./guide/mqtt/index\&.md|support for the MQTT\fR protocol version \fI3\&.1\&.1\fR and \fI5\&.0\fR\&. Remember to configure \fImod_mqtt\fR in \fImodules\fR and \fIlisten\fR sections\&. +.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 +\fBaccess_publish\fR: \fI{TopicFilter: AccessName}\fR +.RS 4 +Access rules to restrict access to topics for publishers\&. By default there are no restrictions\&. +.RE +.PP +\fBaccess_subscribe\fR: \fI{TopicFilter: AccessName}\fR +.RS 4 +Access rules to restrict access to topics for subscribers\&. By default there are no restrictions\&. +.RE +.PP +\fBcache_life_time\fR: \fItimeout()\fR +.RS 4 +Same as top\-level +\fIcache_life_time\fR +option, but applied to this module only\&. +.RE +.PP +\fBcache_missed\fR: \fItrue | false\fR +.RS 4 +Same as top\-level +\fIcache_missed\fR +option, but applied to this module only\&. +.RE +.PP +\fBcache_size\fR: \fIpos_integer() | infinity\fR +.RS 4 +Same as top\-level +\fIcache_size\fR +option, but applied to this module only\&. +.RE +.PP +\fBdb_type\fR: \fImnesia | sql\fR +.RS 4 +Same as top\-level +\fIdefault_db\fR +option, but applied to this module only\&. +.RE +.PP +\fBmatch_retained_limit\fR: \fIpos_integer() | infinity\fR +.RS 4 +The option limits the number of retained messages returned to a client when it subscribes to some topic filter\&. The default value is +\fI1000\fR\&. +.RE +.PP +\fBmax_queue\fR: \fISize\fR +.RS 4 +Maximum queue size for outgoing packets\&. The default value is +\fI5000\fR\&. +.RE +.PP +\fBmax_topic_aliases\fR: \fI0\&.\&.65535\fR +.RS 4 +The maximum number of aliases a client is able to associate with the topics\&. The default value is +\fI100\fR\&. +.RE +.PP +\fBmax_topic_depth\fR: \fIDepth\fR +.RS 4 +The maximum topic depth, i\&.e\&. the number of slashes (\fI/\fR) in the topic\&. The default value is +\fI8\fR\&. +.RE +.PP +\fBqueue_type\fR: \fIram | file\fR +.RS 4 +Same as top\-level +\fIqueue_type\fR +option, but applied to this module only\&. +.RE +.PP +\fBram_db_type\fR: \fImnesia\fR +.RS 4 +Same as top\-level +\fIdefault_ram_db\fR +option, but applied to this module only\&. +.RE +.PP +\fBsession_expiry\fR: \fItimeout()\fR +.RS 4 +The option specifies how long to wait for an MQTT session resumption\&. When +\fI0\fR +is set, the session gets destroyed when the underlying client connection is closed\&. The default value is +\fI5\fR +minutes\&. +.RE +.PP +\fBuse_cache\fR: \fItrue | false\fR +.RS 4 +Same as top\-level +\fIuse_cache\fR +option, but applied to this module only\&. +.RE +.RE +.SS "mod_mqtt_bridge" +.sp +This module adds ability to synchronize local MQTT topics with data on remote servers It can update topics on remote servers when local user updates local topic, or can subscribe for changes on remote server, and update local copy when remote data is updated\&. It is available since ejabberd 23\&.01\&. +.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 +\fBreplication_user\fR: \fIJID\fR +.RS 4 +Identifier of a user that will be assigned as owner of local changes\&. +.RE +.PP +\fBservers\fR: \fI{ServerUrl: {Key: Value}}\fR +.RS 4 +Declaration of data to share for each ServerUrl\&. Server URLs can use schemas: +\fImqtt\fR, +\fImqtts\fR +(mqtt with tls), +\fImqtt5\fR, +\fImqtt5s\fR +(both to trigger v5 protocol), +\fIws\fR, +\fIwss\fR, +\fIws5\fR, +\fIwss5\fR\&. Keys must be: +.PP +\fBauthentication\fR: \fI{AuthKey: AuthValue}\fR +.RS 4 +List of authentication information, where AuthKey can be: +\fIusername\fR +and +\fIpassword\fR +fields, or +\fIcertfile\fR +pointing to client certificate\&. Certificate authentication can be used only with mqtts, mqtt5s, wss, wss5\&. +.RE +.PP +\fBpublish\fR: \fI{LocalTopic: RemoteTopic}\fR +.RS 4 +Either publish or subscribe must be set, or both\&. +.RE +.PP +\fBsubscribe\fR: \fI{RemoteTopic: LocalTopic}\fR +.RS 4 +Either publish or subscribe must be set, or both\&. +.RE +.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 +modules: + mod_mqtt_bridge: + replication_user: "mqtt@xmpp\&.server\&.com" + servers: + "mqtt://server\&.com": + authentication: + certfile: "/etc/ejabberd/mqtt_server\&.pem" + publish: + "localA": "remoteA" # local changes to \*(AqlocalA\*(Aq will be replicated on remote server as \*(AqremoteA\*(Aq + "topicB": "topicB" + subscribe: + "remoteB": "localB" # changes to \*(AqremoteB\*(Aq on remote server will be stored as \*(AqlocalB\*(Aq on local server +.fi +.if n \{\ +.RE +.\} +.RE +.SS "mod_muc" +.sp +This module provides support for XEP\-0045: Multi\-User Chat\&. Users can discover existing rooms, join or create them\&. Occupants of a room can chat in public or have private chats\&. +.sp +The MUC service allows any Jabber ID to register a nickname, so nobody else can use that nickname in any room in the MUC service\&. To register a nickname, open the Service Discovery in your XMPP client and register in the MUC service\&. +.sp +It is also possible to register a nickname in a room, so nobody else can use that nickname in that room\&. If a nick is registered in the MUC service, that nick cannot be registered in any room, and vice versa: a nick that is registered in a room cannot be registered at the MUC service\&. +.sp +This module supports clustering and load balancing\&. One module can be started per cluster node\&. Rooms are distributed at creation time on all available MUC module instances\&. The multi\-user chat module is clustered but the rooms themselves are not clustered nor fault\-tolerant: if the node managing a set of rooms goes down, the rooms disappear and they will be recreated on an available node on first connection attempt\&. +.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 +\fBaccess\fR: \fIAccessName\fR +.RS 4 +You can specify who is allowed to use the Multi\-User Chat service\&. By default everyone is allowed to use it\&. +.RE +.PP +\fBaccess_admin\fR: \fIAccessName\fR +.RS 4 +This option specifies who is allowed to administrate the Multi\-User Chat service\&. The default value is +\fInone\fR, which means that only the room creator can administer their room\&. The administrators can send a normal message to the service JID, and it will be shown in all active rooms as a service message\&. The administrators can send a groupchat message to the JID of an active room, and the message will be shown in the room as a service message\&. +.RE +.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\&. 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\&. 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\&. The default value is +\fIall\fR, which means everyone is allowed to modify that option\&. +.RE +.PP +\fBaccess_register\fR: \fIAccessName\fR +.RS 4 +\fINote\fR +about this option: improved in 23\&.10\&. This option specifies who is allowed to register nickname within the Multi\-User Chat service and rooms\&. The default is +\fIall\fR +for backward compatibility, which means that any user is allowed to register any free nick in the MUC service and in the rooms\&. +.RE +.PP +\fBcleanup_affiliations_on_start\fR: \fItrue | false\fR +.RS 4 +\fINote\fR +about this option: added in 22\&.05\&. Remove affiliations for non\-existing local users on startup\&. The default value is +\fIfalse\fR\&. +.RE +.PP +\fBdb_type\fR: \fImnesia | sql\fR +.RS 4 +Same as top\-level +\fIdefault_db\fR +option, but applied to this module only\&. +.RE +.PP +\fBdefault_room_options\fR: \fIOptions\fR +.RS 4 +Define the default room options\&. Note that the creator of a room can modify the options of his room at any time using an XMPP client with MUC capability\&. The +\fIOptions\fR +are: +.PP +\fBallow_change_subj\fR: \fItrue | false\fR +.RS 4 +Allow occupants to change the subject\&. The default value is +\fItrue\fR\&. +.RE +.PP +\fBallow_private_messages_from_visitors\fR: \fIanyone | moderators | nobody\fR +.RS 4 +Visitors can send private messages to other occupants\&. The default value is +\fIanyone\fR +which means visitors can send private messages to any occupant\&. +.RE +.PP +\fBallow_query_users\fR: \fItrue | false\fR +.RS 4 +Occupants can send IQ queries to other occupants\&. The default value is +\fItrue\fR\&. +.RE +.PP +\fBallow_subscription\fR: \fItrue | false\fR +.RS 4 +Allow users to subscribe to room events as described in +\fI\&.\&./\&.\&./developer/xmpp\-clients\-bots/extensions/muc\-sub\&.md|Multi\-User Chat Subscriptions\fR\&. The default value is +\fIfalse\fR\&. +.RE +.PP +\fBallow_user_invites\fR: \fItrue | false\fR +.RS 4 +Allow occupants to send invitations\&. The default value is +\fIfalse\fR\&. +.RE +.PP +\fBallow_visitor_nickchange\fR: \fItrue | false\fR +.RS 4 +Allow visitors to change nickname\&. The default value is +\fItrue\fR\&. +.RE +.PP +\fBallow_visitor_status\fR: \fItrue | false\fR +.RS 4 +Allow visitors to send status text in presence updates\&. If disallowed, the status text is stripped before broadcasting the presence update to all the room occupants\&. The default value is +\fItrue\fR\&. +.RE +.PP +\fBallow_voice_requests\fR: \fItrue | false\fR +.RS 4 +Allow visitors in a moderated room to request voice\&. The default value is +\fItrue\fR\&. +.RE +.PP +\fBallowpm\fR: \fIanyone | participants | moderators | none\fR +.RS 4 +Who can send private messages\&. The default value is +\fIanyone\fR\&. +.RE +.PP +\fBanonymous\fR: \fItrue | false\fR +.RS 4 +The room is anonymous: occupants don\(cqt see the real JIDs of other occupants\&. Note that the room moderators can always see the real JIDs of the occupants\&. The default value is +\fItrue\fR\&. +.RE +.PP +\fBcaptcha_protected\fR: \fItrue | false\fR +.RS 4 +When a user tries to join a room where they have no affiliation (not owner, admin or member), the room requires them to fill a CAPTCHA challenge (see section +\fIbasic\&.md#captcha|CAPTCHA\fR +in order to accept their join in the room\&. The default value is +\fIfalse\fR\&. +.RE +.PP +\fBdescription\fR: \fIRoom Description\fR +.RS 4 +Short description of the room\&. The default value is an empty string\&. +.RE +.PP +\fBenable_hats\fR: \fItrue | false\fR +.RS 4 +\fINote\fR +about this option: improved in 25\&.03\&. Allow extended roles as defined in XEP\-0317 Hats\&. Check the +\fI\&.\&./\&.\&./tutorials/muc\-hats\&.md|MUC Hats\fR +tutorial\&. The default value is +\fIfalse\fR\&. +.RE +.PP +\fBlang\fR: \fILanguage\fR +.RS 4 +Preferred language for the discussions in the room\&. The language format should conform to RFC 5646\&. There is no value by default\&. +.RE +.PP +\fBlogging\fR: \fItrue | false\fR +.RS 4 +The public messages are logged using +\fImod_muc_log\fR\&. The default value is +\fIfalse\fR\&. +.RE +.PP +\fBmam\fR: \fItrue | false\fR +.RS 4 +Enable message archiving\&. Implies mod_mam is enabled\&. The default value is +\fIfalse\fR\&. +.RE +.PP +\fBmax_users\fR: \fINumber\fR +.RS 4 +Maximum number of occupants in the room\&. The default value is +\fI200\fR\&. +.RE +.PP +\fBmembers_by_default\fR: \fItrue | false\fR +.RS 4 +The occupants that enter the room are participants by default, so they have "voice"\&. The default value is +\fItrue\fR\&. +.RE +.PP +\fBmembers_only\fR: \fItrue | false\fR +.RS 4 +Only members of the room can enter\&. The default value is +\fIfalse\fR\&. +.RE +.PP +\fBmoderated\fR: \fItrue | false\fR +.RS 4 +Only occupants with "voice" can send public messages\&. The default value is +\fItrue\fR\&. +.RE +.PP +\fBpassword\fR: \fIPassword\fR +.RS 4 +Password of the room\&. Implies option +\fIpassword_protected\fR +set to +\fItrue\fR\&. There is no default value\&. +.RE +.PP +\fBpassword_protected\fR: \fItrue | false\fR +.RS 4 +The password is required to enter the room\&. The default value is +\fIfalse\fR\&. +.RE +.PP +\fBpersistent\fR: \fItrue | false\fR +.RS 4 +The room persists even if the last participant leaves\&. The default value is +\fIfalse\fR\&. +.RE +.PP +\fBpresence_broadcast\fR: \fI[Role]\fR +.RS 4 +List of roles for which presence is broadcasted\&. The list can contain one or several of: +\fImoderator\fR, +\fIparticipant\fR, +\fIvisitor\fR\&. The default value is shown in the example below: +.sp +\fBExample\fR: +.sp +.if n \{\ +.RS 4 +.\} +.nf +presence_broadcast: + \- moderator + \- participant + \- visitor +.fi +.if n \{\ +.RE +.\} +.RE +.PP +\fBpublic\fR: \fItrue | false\fR +.RS 4 +The room is public in the list of the MUC service, so it can be discovered\&. MUC admins and room participants will see private rooms in Service Discovery if their XMPP client supports this feature\&. The default value is +\fItrue\fR\&. +.RE +.PP +\fBpublic_list\fR: \fItrue | false\fR +.RS 4 +The list of participants is public, without requiring to enter the room\&. The default value is +\fItrue\fR\&. +.RE +.PP +\fBpubsub\fR: \fIPubSub Node\fR +.RS 4 +XMPP URI of associated Publish/Subscribe node\&. The default value is an empty string\&. +.RE +.PP +\fBtitle\fR: \fIRoom Title\fR +.RS 4 +A human\-readable title of the room\&. There is no default value +.RE +.PP +\fBvcard\fR: \fIvCard\fR +.RS 4 +A custom vCard for the room\&. See the equivalent mod_muc option\&.The default value is an empty string\&. +.RE +.PP +\fBvcard_xupdate\fR: \fIundefined | external | AvatarHash\fR +.RS 4 +Set the hash of the avatar image\&. The default value is +\fIundefined\fR\&. +.RE +.PP +\fBvoice_request_min_interval\fR: \fINumber\fR +.RS 4 +Minimum interval between voice requests, in seconds\&. The default value is +\fI1800\fR\&. +.RE +.RE +.PP +\fBhibernation_timeout\fR: \fIinfinity | Seconds\fR +.RS 4 +Timeout before hibernating the room process, expressed in seconds\&. The default value is +\fIinfinity\fR\&. +.RE +.PP +\fBhistory_size\fR: \fISize\fR +.RS 4 +A small history of the current discussion is sent to users when they enter the room\&. With this option you can define the number of history messages to keep and send to users joining the room\&. The value is a non\-negative integer\&. Setting the value to +\fI0\fR +disables the history feature and, as a result, nothing is kept in memory\&. The default value is +\fI20\fR\&. This value affects all rooms on the service\&. NOTE: modern XMPP clients rely on Message Archives (XEP\-0313), so feel free to disable the history feature if you\(cqre only using modern clients and have +\fImod_mam\fR +module loaded\&. +.RE +.PP +\fBhost\fR +.RS 4 +Deprecated\&. Use +\fIhosts\fR +instead\&. +.RE +.PP +\fBhosts\fR: \fI[Host, \&.\&.\&.]\fR +.RS 4 +This option defines the Jabber IDs of the service\&. If the +\fIhosts\fR +option is not specified, the only Jabber ID will be the hostname of the virtual host with the prefix "conference\&."\&. The keyword +\fI@HOST@\fR +is replaced with the real virtual host name\&. +.RE +.PP +\fBmax_captcha_whitelist\fR: \fINumber\fR +.RS 4 +\fINote\fR +about this option: added in 21\&.01\&. This option defines the maximum number of characters that Captcha Whitelist can have when configuring the room\&. The default value is +\fIinfinity\fR\&. +.RE +.PP +\fBmax_password\fR: \fINumber\fR +.RS 4 +\fINote\fR +about this option: added in 21\&.01\&. This option defines the maximum number of characters that Password can have when configuring the room\&. The default value is +\fIinfinity\fR\&. +.RE +.PP +\fBmax_room_desc\fR: \fINumber\fR +.RS 4 +This option defines the maximum number of characters that Room Description can have when configuring the room\&. The default value is +\fIinfinity\fR\&. +.RE +.PP +\fBmax_room_id\fR: \fINumber\fR +.RS 4 +This option defines the maximum number of characters that Room ID can have when creating a new room\&. The default value is +\fIinfinity\fR\&. +.RE +.PP +\fBmax_room_name\fR: \fINumber\fR +.RS 4 +This option defines the maximum number of characters that Room Name can have when configuring the room\&. The default value is +\fIinfinity\fR\&. +.RE +.PP +\fBmax_rooms_discoitems\fR: \fINumber\fR +.RS 4 +When there are more rooms than this +\fINumber\fR, only the non\-empty ones are returned in a Service Discovery query\&. The default value is +\fI100\fR\&. +.RE +.PP +\fBmax_user_conferences\fR: \fINumber\fR +.RS 4 +This option defines the maximum number of rooms that any given user can join\&. The default value is +\fI100\fR\&. This option is used to prevent possible abuses\&. Note that this is a soft limit: some users can sometimes join more conferences in cluster configurations\&. +.RE +.PP +\fBmax_users\fR: \fINumber\fR +.RS 4 +This option defines at the service level, the maximum number of users allowed per room\&. It can be lowered in each room configuration but cannot be increased in individual room configuration\&. The default value is +\fI200\fR\&. +.RE +.PP +\fBmax_users_admin_threshold\fR: \fINumber\fR +.RS 4 +This option defines the number of service admins or room owners allowed to enter the room when the maximum number of allowed occupants was reached\&. The default limit is +\fI5\fR\&. +.RE +.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 overcrowded, 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 +.RS 4 +This option defines the minimum interval between two messages send by an occupant in seconds\&. This option is global and valid for all rooms\&. A decimal value can be used\&. When this option is not defined, message rate is not limited\&. This feature can be used to protect a MUC service from occupant abuses and limit number of messages that will be broadcasted by the service\&. A good value for this minimum message interval is +\fI0\&.4\fR +second\&. If an occupant tries to send messages faster, an error is send back explaining that the message has been discarded and describing the reason why the message is not acceptable\&. +.RE +.PP +\fBmin_presence_interval\fR: \fINumber\fR +.RS 4 +This option defines the minimum of time between presence changes coming from a given occupant in seconds\&. This option is global and valid for all rooms\&. A decimal value can be used\&. When this option is not defined, no restriction is applied\&. This option can be used to protect a MUC service for occupants abuses\&. If an occupant tries to change its presence more often than the specified interval, the presence is cached by ejabberd and only the last presence is broadcasted to all occupants in the room after expiration of the interval delay\&. Intermediate presence packets are silently discarded\&. A good value for this option is +\fI4\fR +seconds\&. +.RE +.PP +\fBname\fR: \fIstring()\fR +.RS 4 +The value of the service name\&. This name is only visible in some clients that support +XEP\-0030: Service Discovery\&. The default is +\fIChatrooms\fR\&. +.RE +.PP +\fBpreload_rooms\fR: \fItrue | false\fR +.RS 4 +Whether to load all persistent rooms in memory on startup\&. If disabled, the room is only loaded on first participant join\&. The default is +\fItrue\fR\&. It makes sense to disable room preloading when the number of rooms is high: this will improve server startup time and memory consumption\&. +.RE +.PP +\fBqueue_type\fR: \fIram | file\fR +.RS 4 +Same as top\-level +\fIqueue_type\fR +option, but applied to this module only\&. +.RE +.PP +\fBram_db_type\fR: \fImnesia | sql\fR +.RS 4 +Same as top\-level +\fIdefault_ram_db\fR +option, but applied to this module only\&. +.RE +.PP +\fBregexp_room_id\fR: \fIstring()\fR +.RS 4 +This option defines the regular expression that a Room ID must satisfy to allow the room creation\&. The default value is the empty string\&. +.RE +.PP +\fBroom_shaper\fR: \fInone | ShaperName\fR +.RS 4 +This option defines shaper for the MUC rooms\&. The default value is +\fInone\fR\&. +.RE +.PP +\fBuser_message_shaper\fR: \fInone | ShaperName\fR +.RS 4 +This option defines shaper for the users messages\&. The default value is +\fInone\fR\&. +.RE +.PP +\fBuser_presence_shaper\fR: \fInone | ShaperName\fR +.RS 4 +This option defines shaper for the users presences\&. The default value is +\fInone\fR\&. +.RE +.PP +\fBvcard\fR: \fIvCard\fR +.RS 4 +A custom vCard of the service that will be displayed by some XMPP clients in Service Discovery\&. The value of +\fIvCard\fR +is a YAML map constructed from an XML representation of vCard\&. Since the representation has no attributes, the mapping is straightforward\&. +.sp +\fBExample\fR: +.sp +.if n \{\ +.RS 4 +.\} +.nf +# This XML representation of vCard: +# +# Conferences +# +# +# Elm Street +# +# +# +# is translated to: +vcard: + fn: Conferences + adr: + \- + work: true + street: Elm Street +.fi +.if n \{\ +.RE +.\} +.RE +.RE +.SS "mod_muc_admin" +.sp +This module provides commands to administer local MUC services and their MUC rooms\&. It also provides simple WebAdmin pages to view the existing rooms\&. +.sp +This module depends on \fImod_muc\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 +\fBsubscribe_room_many_max_users\fR: \fINumber\fR +.RS 4 +\fINote\fR +about this option: added in 22\&.05\&. How many users can be subscribed to a room at once using the +\fIsubscribe_room_many\fR +API\&. The default value is +\fI50\fR\&. +.RE +.sp +\fBAPI Tags:\fR \fI\&.\&./\&.\&./developer/ejabberd\-api/admin\-tags\&.md#muc|muc\fR, \fI\&.\&./\&.\&./developer/ejabberd\-api/admin\-tags\&.md#muc_room|muc_room\fR, \fI\&.\&./\&.\&./developer/ejabberd\-api/admin\-tags\&.md#muc_sub|muc_sub\fR +.RE +.SS "mod_muc_log" +.sp +This module enables optional logging of Multi\-User Chat (MUC) public conversations to HTML\&. Once you enable this module, users can join a room using a MUC capable XMPP client, and if they have enough privileges, they can request the configuration form in which they can set the option to enable room logging\&. +.sp +Features: +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +Room details are added on top of each page: room title, JID, author, subject and configuration\&. +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +The room JID in the generated HTML is a link to join the room (using XMPP URI)\&. +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +Subject and room configuration changes are tracked and displayed\&. +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +Joins, leaves, nick changes, kicks, bans and +\fI/me\fR +are tracked and displayed, including the reason if available\&. +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +Generated HTML files are XHTML 1\&.0 Transitional and CSS compliant\&. +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +Timestamps are self\-referencing links\&. +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +Links on top for quicker navigation: Previous day, Next day, Up\&. +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +CSS is used for style definition, and a custom CSS file can be used\&. +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +URLs on messages and subjects are converted to hyperlinks\&. +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +Timezone used on timestamps is shown on the log files\&. +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +A custom link can be added on top of each page\&. +.RE +.sp +The module depends on \fImod_muc\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 +\fBaccess_log\fR: \fIAccessName\fR +.RS 4 +This option restricts which occupants are allowed to enable or disable room logging\&. The default value is +\fImuc_admin\fR\&. NOTE: for this default setting you need to have an access rule for +\fImuc_admin\fR +in order to take effect\&. +.RE +.PP +\fBcssfile\fR: \fIPath | URL\fR +.RS 4 +With this option you can set whether the HTML files should have a custom CSS file or if they need to use the embedded CSS\&. Allowed values are either +\fIPath\fR +to local file or an +\fIURL\fR +to a remote file\&. By default a predefined CSS will be embedded into the HTML page\&. +.RE +.PP +\fBdirname\fR: \fIroom_jid | room_name\fR +.RS 4 +Configure the name of the room directory\&. If set to +\fIroom_jid\fR, the room directory name will be the full room JID\&. Otherwise, the room directory name will be only the room name, not including the MUC service name\&. The default value is +\fIroom_jid\fR\&. +.RE +.PP +\fBdirtype\fR: \fIsubdirs | plain\fR +.RS 4 +The type of the created directories can be specified with this option\&. If set to +\fIsubdirs\fR, subdirectories are created for each year and month\&. Otherwise, the names of the log files contain the full date, and there are no subdirectories\&. The default value is +\fIsubdirs\fR\&. +.RE +.PP +\fBfile_format\fR: \fIhtml | plaintext\fR +.RS 4 +Define the format of the log files: +\fIhtml\fR +stores in HTML format, +\fIplaintext\fR +stores in plain text\&. The default value is +\fIhtml\fR\&. +.RE +.PP +\fBfile_permissions\fR: \fI{mode: Mode, group: Group}\fR +.RS 4 +Define the permissions that must be used when creating the log files: the number of the mode, and the numeric id of the group that will own the files\&. The default value is shown in the example below: +.sp +\fBExample\fR: +.sp +.if n \{\ +.RS 4 +.\} +.nf +file_permissions: + mode: 644 + group: 33 +.fi +.if n \{\ +.RE +.\} +.RE +.PP +\fBoutdir\fR: \fIPath\fR +.RS 4 +This option sets the full path to the directory in which the HTML files should be stored\&. Make sure the ejabberd daemon user has write access on that directory\&. The default value is +\fIwww/muc\fR\&. +.RE +.PP +\fBspam_prevention\fR: \fItrue | false\fR +.RS 4 +If set to +\fItrue\fR, a special attribute is added to links that prevent their indexation by search engines\&. The default value is +\fItrue\fR, which mean that +\fInofollow\fR +attributes will be added to user submitted links\&. +.RE +.PP +\fBtimezone\fR: \fIlocal | universal\fR +.RS 4 +The time zone for the logs is configurable with this option\&. If set to +\fIlocal\fR, the local time, as reported to Erlang emulator by the operating system, will be used\&. Otherwise, UTC time will be used\&. The default value is +\fIlocal\fR\&. +.RE +.PP +\fBtop_link\fR: \fI{URL: Text}\fR +.RS 4 +With this option you can customize the link on the top right corner of each log file\&. The default value is shown in the example below: +.sp +\fBExample\fR: +.sp +.if n \{\ +.RS 4 +.\} +.nf +top_link: + /: Home +.fi +.if n \{\ +.RE +.\} +.RE +.PP +\fBurl\fR: \fIURL\fR +.RS 4 +A top level +\fIURL\fR +where a client can access logs of a particular conference\&. The conference name is appended to the URL if +\fIdirname\fR +option is set to +\fIroom_name\fR +or a conference JID is appended to the +\fIURL\fR +otherwise\&. There is no default value\&. +.RE +.RE +.SS "mod_muc_occupantid" +.sp +\fINote\fR about this option: added in 23\&.10\&. +.sp +This module implements XEP\-0421: Anonymous unique occupant identifiers for MUCs\&. +.sp +When the module is enabled, the feature is enabled in all semi\-anonymous rooms\&. +.sp +The module has no options\&. +.SS "mod_muc_rtbl" +.sp +\fINote\fR about this option: added in 23\&.04\&. +.sp +This module implement Real\-time blocklists for MUC rooms\&. +.sp +It works by observing remote pubsub node conforming with specification described in https://xmppbl\&.org/\&. +.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 +\fBrtbl_node\fR: \fIPubsubNodeName\fR +.RS 4 +Name of pubsub node that should be used to track blocked users\&. The default value is +\fImuc_bans_sha256\fR\&. +.RE +.PP +\fBrtbl_server\fR: \fIDomain\fR +.RS 4 +Domain of xmpp server that serves block list\&. The default value is +\fIxmppbl\&.org\fR +.RE +.RE +.SS "mod_multicast" +.sp +This module implements a service for XEP\-0033: Extended Stanza Addressing\&. +.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 +\fBaccess\fR: \fIAccess\fR +.RS 4 +The access rule to restrict who can send packets to the multicast service\&. Default value: +\fIall\fR\&. +.RE +.PP +\fBhost\fR +.RS 4 +Deprecated\&. Use +\fIhosts\fR +instead\&. +.RE +.PP +\fBhosts\fR: \fI[Host, \&.\&.\&.]\fR +.RS 4 +This option defines the Jabber IDs of the service\&. If the +\fIhosts\fR +option is not specified, the only Jabber ID will be the hostname of the virtual host with the prefix "multicast\&."\&. The keyword +\fI@HOST@\fR +is replaced with the real virtual host name\&. The default value is +\fImulticast\&.@HOST@\fR\&. +.RE +.PP +\fBlimits\fR: \fISender: Stanza: Number\fR +.RS 4 +Specify a list of custom limits which override the default ones defined in XEP\-0033\&. Limits are defined per sender type and stanza type, where: +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +\fIsender\fR +can be: +\fIlocal\fR +or +\fIremote\fR\&. +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +\fIstanza\fR +can be: +\fImessage\fR +or +\fIpresence\fR\&. +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +\fInumber\fR +can be a positive integer or +\fIinfinite\fR\&. +.sp +\fBExample\fR: +.sp +.if n \{\ +.RS 4 +.\} +.nf +# Default values: +local: + message: 100 + presence: 100 +remote: + message: 20 + presence: 20 +.fi +.if n \{\ +.RE +.\} +.RE +.RE +.PP +\fBname\fR +.RS 4 +Service name to provide in the Info query to the Service Discovery\&. Default is +\fI"Multicast"\fR\&. +.RE +.PP +\fBvcard\fR +.RS 4 +vCard element to return when queried\&. Default value is +\fIundefined\fR\&. +.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 +# Only admins can send packets to multicast service +access_rules: + multicast: + \- allow: admin + +# If you want to allow all your users: +access_rules: + multicast: + \- allow + +# This allows both admins and remote users to send packets, +# but does not allow local users +acl: + allservers: + server_glob: "*" +access_rules: + multicast: + \- allow: admin + \- deny: local + \- allow: allservers + +modules: + mod_multicast: + host: multicast\&.example\&.org + access: multicast + limits: + local: + message: 40 + presence: infinite + remote: + message: 150 +.fi +.if n \{\ +.RE +.\} +.RE +.SS "mod_offline" +.sp +This module implements XEP\-0160: Best Practices for Handling Offline Messages and XEP\-0013: Flexible Offline Message Retrieval\&. This means that all messages sent to an offline user will be stored on the server until that user comes online again\&. Thus it is very similar to how email works\&. A user is considered offline if no session presence priority > 0 are currently open\&. +.sp +The \fIdelete_expired_messages\fR API allows to delete expired messages, and \fIdelete_old_messages\fR API deletes older ones\&. +.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 +\fBaccess_max_user_messages\fR: \fIAccessName\fR +.RS 4 +This option defines which access rule will be enforced to limit the maximum number of offline messages that a user can have (quota)\&. When a user has too many offline messages, any new messages that they receive are discarded, and a +\fI\fR +error is returned to the sender\&. The default value is +\fImax_user_offline_messages\fR\&. +.RE +.PP +\fBbounce_groupchat\fR: \fItrue | false\fR +.RS 4 +This option is use the disable an optimization that avoids bouncing error messages when groupchat messages could not be stored as offline\&. It will reduce chat room load, without any drawback in standard use cases\&. You may change default value only if you have a custom module which uses offline hook after +\fImod_offline\fR\&. This option can be useful for both standard MUC and MucSub, but the bounce is much more likely to happen in the context of MucSub, so it is even more important to have it on large MucSub services\&. The default value is +\fIfalse\fR, meaning the optimization is enabled\&. +.RE +.PP +\fBcache_life_time\fR: \fItimeout()\fR +.RS 4 +Same as top\-level +\fIcache_life_time\fR +option, but applied to this module only\&. +.RE +.PP +\fBcache_size\fR: \fIpos_integer() | infinity\fR +.RS 4 +Same as top\-level +\fIcache_size\fR +option, but applied to this module only\&. +.RE +.PP +\fBdb_type\fR: \fImnesia | sql\fR +.RS 4 +Same as top\-level +\fIdefault_db\fR +option, but applied to this module only\&. +.RE +.PP +\fBstore_empty_body\fR: \fItrue | false | unless_chat_state\fR +.RS 4 +Whether or not to store messages that lack a +\fI\fR +element\&. The default value is +\fIunless_chat_state\fR, which tells ejabberd to store messages even if they lack the +\fI\fR +element, unless they only contain a chat state notification (as defined in +XEP\-0085: Chat State Notifications\&. +.RE +.PP +\fBstore_groupchat\fR: \fItrue | false\fR +.RS 4 +Whether or not to store groupchat messages\&. The default value is +\fIfalse\fR\&. +.RE +.PP +\fBuse_cache\fR: \fItrue | false\fR +.RS 4 +Same as top\-level +\fIuse_cache\fR +option, but applied to this module only\&. +.RE +.PP +\fBuse_mam_for_storage\fR: \fItrue | false\fR +.RS 4 +This is an experimental option\&. By enabling the option, this module uses the +\fIarchive\fR +table from +\fImod_mam\fR +instead of its own spool table to retrieve the messages received when the user was offline\&. This allows client developers to slowly drop XEP\-0160 and rely on XEP\-0313 instead\&. It also further reduces the storage required when you enable MucSub\&. Enabling this option has a known drawback for the moment: most of flexible message retrieval queries don\(cqt work (those that allow retrieval/deletion of messages by id), but this specification is not widely used\&. The default value is +\fIfalse\fR +to keep former behaviour as default\&. +.RE +.RE +.sp +.it 1 an-trap +.nr an-no-space-flag 1 +.nr an-break-flag 1 +.br +.ps +1 +\fBExamples:\fR +.RS 4 +.sp +This example allows power users to have as much as 5000 offline messages, administrators up to 2000, and all the other users up to 100: +.sp +.if n \{\ +.RS 4 +.\} +.nf +acl: + admin: + user: + \- admin1@localhost + \- admin2@example\&.org + poweruser: + user: + \- bob@example\&.org + \- jane@example\&.org + +shaper_rules: + max_user_offline_messages: + \- 5000: poweruser + \- 2000: admin + \- 100 + +modules: + \&.\&.\&. + mod_offline: + access_max_user_messages: max_user_offline_messages + \&.\&.\&. +.fi +.if n \{\ +.RE +.\} +.sp +\fBAPI Tags:\fR \fI\&.\&./\&.\&./developer/ejabberd\-api/admin\-tags\&.md#offline|offline\fR +.RE +.SS "mod_ping" +.sp +This module implements support for XEP\-0199: XMPP Ping and periodic keepalives\&. When this module is enabled ejabberd responds correctly to ping requests, as defined by the protocol\&. +.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 +\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\&. NOTE: when +\fImod_stream_mgmt\fR +is loaded and stream management is enabled by a client, this value is ignored, and the +ack_timeout +applies instead\&. The default value is +\fIundefined\fR\&. +.RE +.PP +\fBping_interval\fR: \fItimeout()\fR +.RS 4 +How often to send pings to connected clients, if option +\fIsend_pings\fR +is set to +\fItrue\fR\&. If a client connection does not send or receive any stanza within this interval, a ping request is sent to the client\&. The default value is +\fI1\fR +minute\&. +.RE +.PP +\fBsend_pings\fR: \fItrue | false\fR +.RS 4 +If this option is set to +\fItrue\fR, the server sends pings to connected clients that are not active in a given interval defined in +\fIping_interval\fR +option\&. This is useful to keep client connections alive or checking availability\&. The default value is +\fIfalse\fR\&. +.RE +.PP +\fBtimeout_action\fR: \fInone | kill\fR +.RS 4 +What to do when a client does not answer to a server ping request in less than period defined in +\fIping_ack_timeout\fR +option: +\fIkill\fR +means destroying the underlying connection, +\fInone\fR +means to do nothing\&. NOTE: when +\fImod_stream_mgmt\fR +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 +.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 +modules: + mod_ping: + send_pings: true + ping_interval: 4 min + timeout_action: kill +.fi +.if n \{\ +.RE +.\} +.RE +.SS "mod_pres_counter" +.sp +This module detects flood/spam in presence subscriptions traffic\&. If a user sends or receives more of those stanzas in a given time interval, the exceeding stanzas are silently dropped, and a warning is logged\&. +.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 +\fBcount\fR: \fINumber\fR +.RS 4 +The number of subscription presence stanzas (subscribe, unsubscribe, subscribed, unsubscribed) allowed for any direction (input or output) per time defined in +\fIinterval\fR +option\&. Please note that two users subscribing to each other usually generate 4 stanzas, so the recommended value is +\fI4\fR +or more\&. The default value is +\fI5\fR\&. +.RE +.PP +\fBinterval\fR: \fItimeout()\fR +.RS 4 +The time interval\&. The default value is +\fI1\fR +minute\&. +.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 +modules: + mod_pres_counter: + count: 5 + interval: 30 secs +.fi +.if n \{\ +.RE +.\} +.RE +.SS "mod_privacy" +.sp +This module implements XEP\-0016: Privacy Lists\&. +.if n \{\ +.sp +.\} +.RS 4 +.it 1 an-trap +.nr an-no-space-flag 1 +.nr an-break-flag 1 +.br +.ps +1 +\fBNote\fR +.ps -1 +.br +.sp +Nowadays modern XMPP clients rely on XEP\-0191: Blocking Command which is implemented by \fImod_blocking\fR\&. However, you still need \fImod_privacy\fR loaded in order for \fImod_blocking\fR to work\&. +.sp .5v +.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 +\fBcache_life_time\fR: \fItimeout()\fR +.RS 4 +Same as top\-level +\fIcache_life_time\fR +option, but applied to this module only\&. +.RE +.PP +\fBcache_missed\fR: \fItrue | false\fR +.RS 4 +Same as top\-level +\fIcache_missed\fR +option, but applied to this module only\&. +.RE +.PP +\fBcache_size\fR: \fIpos_integer() | infinity\fR +.RS 4 +Same as top\-level +\fIcache_size\fR +option, but applied to this module only\&. +.RE +.PP +\fBdb_type\fR: \fImnesia | sql\fR +.RS 4 +Same as top\-level +\fIdefault_db\fR +option, but applied to this module only\&. +.RE +.PP +\fBuse_cache\fR: \fItrue | false\fR +.RS 4 +Same as top\-level +\fIuse_cache\fR +option, but applied to this module only\&. +.RE +.RE +.SS "mod_private" +.sp +This module adds support for XEP\-0049: Private XML Storage\&. +.sp +Using this method, XMPP entities can store private data on the server, retrieve it whenever necessary and share it between multiple connected clients of the same user\&. The data stored might be anything, as long as it is a valid XML\&. One typical usage is storing a bookmark of all user\(cqs conferences (XEP\-0048: Bookmarks)\&. +.sp +It also implements the bookmark conversion described in XEP\-0402: PEP Native Bookmarks, see \fIbookmarks_to_pep\fR API\&. +.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 +\fBcache_life_time\fR: \fItimeout()\fR +.RS 4 +Same as top\-level +\fIcache_life_time\fR +option, but applied to this module only\&. +.RE +.PP +\fBcache_missed\fR: \fItrue | false\fR +.RS 4 +Same as top\-level +\fIcache_missed\fR +option, but applied to this module only\&. +.RE +.PP +\fBcache_size\fR: \fIpos_integer() | infinity\fR +.RS 4 +Same as top\-level +\fIcache_size\fR +option, but applied to this module only\&. +.RE +.PP +\fBdb_type\fR: \fImnesia | sql\fR +.RS 4 +Same as top\-level +\fIdefault_db\fR +option, but applied to this module only\&. +.RE +.PP +\fBuse_cache\fR: \fItrue | false\fR +.RS 4 +Same as top\-level +\fIuse_cache\fR +option, but applied to this module only\&. +.RE +.sp +\fBAPI Tags:\fR \fI\&.\&./\&.\&./developer/ejabberd\-api/admin\-tags\&.md#private|private\fR +.RE +.SS "mod_privilege" +.sp +\fINote\fR about this option: improved in 24\&.10\&. +.sp +This module is an implementation of XEP\-0356: Privileged Entity\&. This extension allows components to have privileged access to other entity data (send messages on behalf of the server or on behalf of a user, get/set user roster, access presence information, etc\&.)\&. This may be used to write powerful external components, for example implementing an external PEP or MAM service\&. +.sp +By default a component does not have any privileged access\&. It is worth noting that the permissions grant access to the component to a specific data type for all users of the virtual host on which \fImod_privilege\fR is loaded\&. +.sp +Make sure you have a listener configured to connect your component\&. Check the section about listening ports for more information\&. +.if n \{\ +.sp +.\} +.RS 4 +.it 1 an-trap +.nr an-no-space-flag 1 +.nr an-break-flag 1 +.br +.ps +1 +\fBWarning\fR +.ps -1 +.br +.sp +Security issue: Privileged access gives components access to sensitive data, so permission should be granted carefully, only if you trust a component\&. +.sp .5v +.RE +.if n \{\ +.sp +.\} +.RS 4 +.it 1 an-trap +.nr an-no-space-flag 1 +.nr an-break-flag 1 +.br +.ps +1 +\fBNote\fR +.ps -1 +.br +.sp +This module is complementary to \fImod_delegation\fR, but can also be used separately\&. +.sp .5v +.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 +\fBiq\fR: \fI{Namespace: Options}\fR +.RS 4 +This option defines namespaces and their IQ permissions\&. By default no permissions are given\&. The +\fIOptions\fR +are: +.PP +\fBboth\fR: \fIAccessName\fR +.RS 4 +Allows sending IQ stanzas of type +\fIget\fR +and +\fIset\fR\&. The default value is +\fInone\fR\&. +.RE +.PP +\fBget\fR: \fIAccessName\fR +.RS 4 +Allows sending IQ stanzas of type +\fIget\fR\&. The default value is +\fInone\fR\&. +.RE +.PP +\fBset\fR: \fIAccessName\fR +.RS 4 +Allows sending IQ stanzas of type +\fIset\fR\&. The default value is +\fInone\fR\&. +.RE +.RE +.PP +\fBmessage\fR: \fIOptions\fR +.RS 4 +This option defines permissions for messages\&. By default no permissions are given\&. The +\fIOptions\fR +are: +.PP +\fBoutgoing\fR: \fIAccessName\fR +.RS 4 +The option defines an access rule for sending outgoing messages by the component\&. The default value is +\fInone\fR\&. +.RE +.RE +.PP +\fBpresence\fR: \fIOptions\fR +.RS 4 +This option defines permissions for presences\&. By default no permissions are given\&. The +\fIOptions\fR +are: +.PP +\fBmanaged_entity\fR: \fIAccessName\fR +.RS 4 +An access rule that gives permissions to the component to receive server presences\&. The default value is +\fInone\fR\&. +.RE +.PP +\fBroster\fR: \fIAccessName\fR +.RS 4 +An access rule that gives permissions to the component to receive the presence of both the users and the contacts in their roster\&. The default value is +\fInone\fR\&. +.RE +.RE +.PP +\fBroster\fR: \fIOptions\fR +.RS 4 +This option defines roster permissions\&. By default no permissions are given\&. The +\fIOptions\fR +are: +.PP +\fBboth\fR: \fIAccessName\fR +.RS 4 +Sets read/write access to a user\(cqs roster\&. The default value is +\fInone\fR\&. +.RE +.PP +\fBget\fR: \fIAccessName\fR +.RS 4 +Sets read access to a user\(cqs roster\&. The default value is +\fInone\fR\&. +.RE +.PP +\fBset\fR: \fIAccessName\fR +.RS 4 +Sets write access to a user\(cqs roster\&. The default value is +\fInone\fR\&. +.RE +.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 +modules: + mod_privilege: + iq: + http://jabber\&.org/protocol/pubsub: + get: all + roster: + get: all + presence: + managed_entity: all + message: + outgoing: all +.fi +.if n \{\ +.RE +.\} +.RE +.SS "mod_providers 🟤" +.sp +\fINote\fR about this option: added in 25\&.08\&. +.sp +This module serves JSON provider files API v2 as described by XMPP Providers\&. +.sp +It attempts to fill some properties gathering values automatically from your existing ejabberd configuration\&. Try enabling the module, check what values are displayed, and then customize using the options\&. +.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 → \fIlisten\-options\&.md#request_handlers|request_handlers\fR\&. Notice you should set in \fIlisten\&.md#ejabberd_http|ejabberd_http\fR the option \fIlisten\-options\&.md#tls|tls\fR enabled\&. +.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 +\fBalternativeJids\fR: \fI[string()]\fR +.RS 4 +List of JIDs (XMPP server domains) a provider offers for registration other than its main JID\&. The default value is +\fI[]\fR\&. +.RE +.PP +\fBbusFactor\fR: \fIinteger()\fR +.RS 4 +Bus factor of the XMPP service (i\&.e\&., the minimum number of team members that the service could not survive losing) or +\fI\-1\fR +for n/a\&. The default value is +\fI\-1\fR\&. +.RE +.PP +\fBfreeOfCharge\fR: \fItrue | false\fR +.RS 4 +Whether the XMPP service can be used for free\&. The default value is +\fIfalse\fR\&. +.RE +.PP +\fBlanguages\fR: \fI[string()]\fR +.RS 4 +List of language codes that your pages are available\&. Some options define URL where the keyword +\fI@LANGUAGE_URL@\fR +will be replaced with each of those language codes\&. The default value is a list with the language set in the option +\fIlanguage\fR, for example: +\fI[en]\fR\&. +.RE +.PP +\fBlegalNotice\fR: \fIstring()\fR +.RS 4 +Legal notice web page (per language)\&. The keyword +\fI@LANGUAGE_URL@\fR +is replaced with each language\&. The default value is +\fI""\fR\&. +.RE +.PP +\fBmaximumHttpFileUploadStorageTime\fR: \fIinteger()\fR +.RS 4 +Maximum storage duration of each shared file (number in days, +\fI0\fR +for no limit or +\fI\-1\fR +for less than 1 day)\&. The default value is the same as option +\fImax_days\fR +from module +\fImod_http_upload_quota\fR, or +\fI0\fR +otherwise\&. +.RE +.PP +\fBmaximumHttpFileUploadTotalSize\fR: \fIinteger()\fR +.RS 4 +Maximum size of all shared files in total per user (number in megabytes (MB), +\fI0\fR +for no limit or +\fI\-1\fR +for less than 1 MB)\&. Attention: MB is used instead of MiB (e\&.g\&., 104,857,600 bytes = 100 MiB H 104 MB)\&. This property is not about the maximum size of each shared file, which is already retrieved via XMPP\&. The default value is the value of the shaper value of option +\fIaccess_hard_quota\fR +from module +\fImod_http_upload_quota\fR, or +\fI0\fR +otherwise\&. +.RE +.PP +\fBmaximumMessageArchiveManagementStorageTime\fR: \fIinteger()\fR +.RS 4 +Maximum storage duration of each exchanged message (number in days, +\fI0\fR +for no limit or +\fI\-1\fR +for less than 1 day)\&. The default value is +\fI0\fR\&. +.RE +.PP +\fBorganization\fR: \fIstring()\fR +.RS 4 +Type of organization providing the XMPP service\&. Allowed values are: +\fIcompany\fR, +\fI"commercial person"\fR, +\fI"private person"\fR, +\fIgovernmental\fR, +\fI"non\-governmental"\fR +or +\fI""\fR\&. The default value is +\fI""\fR\&. +.RE +.PP +\fBpasswordReset\fR: \fIstring()\fR +.RS 4 +Password reset web page (per language) used for an automatic password reset (e\&.g\&., via email) or describing how to manually reset a password (e\&.g\&., by contacting the provider)\&. The keyword +\fI@LANGUAGE_URL@\fR +is replaced with each language\&. The default value is an URL built automatically if +\fImod_register_web\fR +is configured as a +\fIrequest_handler\fR, or +\fI""\fR +otherwise\&. +.RE +.PP +\fBprofessionalHosting\fR: \fItrue | false\fR +.RS 4 +Whether the XMPP server is hosted with good internet connection speed, uninterruptible power supply, access protection and regular backups\&. The default value is +\fIfalse\fR\&. +.RE +.PP +\fBserverLocations\fR: \fI[string()]\fR +.RS 4 +List of language codes of Server/Backup locations\&. The default value is an empty list: +\fI[]\fR\&. +.RE +.PP +\fBserverTesting\fR: \fItrue | false\fR +.RS 4 +Whether tests against the provider\(cqs server are allowed (e\&.g\&., certificate checks and uptime monitoring)\&. The default value is +\fIfalse\fR\&. +.RE +.PP +\fBsince\fR: \fIstring()\fR +.RS 4 +Date since the XMPP service is available\&. The default value is an empty string: +\fI""\fR\&. +.RE +.PP +\fBwebsite\fR: \fIstring()\fR +.RS 4 +Provider website\&. The keyword +\fI@LANGUAGE_URL@\fR +is replaced with each language\&. The default value is +\fI""\fR\&. +.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: 443 + module: ejabberd_http + tls: true + request_handlers: + /\&.well\-known/xmpp\-provider\-v2\&.json: mod_providers + +modules: + mod_providers: + alternativeJids: ["example1\&.com", "example2\&.com"] + busFactor: 1 + freeOfCharge: true + languages: [ag, ao, bg, en] + legalNotice: "http://@HOST@/legal/@LANGUAGE_URL@/" + maximumHttpFileUploadStorageTime: 0 + maximumHttpFileUploadTotalSize: 0 + maximumMessageArchiveManagementStorageTime: 0 + organization: "non\-governmental" + passwordReset: "http://@HOST@/reset/@LANGUAGE_URL@/" + professionalHosting: true + serverLocations: [ao, bg] + serverTesting: true + since: "2025\-12\-31" + website: "http://@HOST@/website/@LANGUAGE_URL@/" +.fi +.if n \{\ +.RE +.\} +.RE +.SS "mod_proxy65" +.sp +This module implements XEP\-0065: SOCKS5 Bytestreams\&. It allows ejabberd to act as a file transfer proxy between two XMPP clients\&. +.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 +\fBaccess\fR: \fIAccessName\fR +.RS 4 +Defines an access rule for file transfer initiators\&. The default value is +\fIall\fR\&. You may want to restrict access to the users of your server only, in order to avoid abusing your proxy by the users of remote servers\&. +.RE +.PP +\fBauth_type\fR: \fIanonymous | plain\fR +.RS 4 +SOCKS5 authentication type\&. The default value is +\fIanonymous\fR\&. If set to +\fIplain\fR, ejabberd will use authentication backend as it would for SASL PLAIN\&. +.RE +.PP +\fBhost\fR +.RS 4 +Deprecated\&. Use +\fIhosts\fR +instead\&. +.RE +.PP +\fBhostname\fR: \fIHost\fR +.RS 4 +Defines a hostname offered by the proxy when establishing a session with clients\&. This is useful when you run the proxy behind a NAT\&. The keyword +\fI@HOST@\fR +is replaced with the virtual host name\&. The default is to use the value of +\fIip\fR +option\&. Examples: +\fIproxy\&.mydomain\&.org\fR, +\fI200\&.150\&.100\&.50\fR\&. +.RE +.PP +\fBhosts\fR: \fI[Host, \&.\&.\&.]\fR +.RS 4 +This option defines the Jabber IDs of the service\&. If the +\fIhosts\fR +option is not specified, the only Jabber ID will be the hostname of the virtual host with the prefix "proxy\&."\&. The keyword +\fI@HOST@\fR +is replaced with the real virtual host name\&. +.RE +.PP +\fBip\fR: \fIIPAddress\fR +.RS 4 +This option specifies which network interface to listen for\&. The default value is an IP address of the service\(cqs DNS name, or, if fails, +\fI127\&.0\&.0\&.1\fR\&. +.RE +.PP +\fBmax_connections\fR: \fIpos_integer() | infinity\fR +.RS 4 +Maximum number of active connections per file transfer initiator\&. The default value is +\fIinfinity\fR\&. +.RE +.PP +\fBname\fR: \fIName\fR +.RS 4 +The value of the service name\&. This name is only visible in some clients that support +XEP\-0030: Service Discovery\&. The default is "SOCKS5 Bytestreams"\&. +.RE +.PP +\fBport\fR: \fI1\&.\&.65535\fR +.RS 4 +A port number to listen for incoming connections\&. The default value is +\fI7777\fR\&. +.RE +.PP +\fBram_db_type\fR: \fImnesia | redis | sql\fR +.RS 4 +Same as top\-level +\fIdefault_ram_db\fR +option, but applied to this module only\&. +.RE +.PP +\fBrecbuf\fR: \fISize\fR +.RS 4 +A size of the buffer for incoming packets\&. If you define a shaper, set the value of this option to the size of the shaper in order to avoid traffic spikes in file transfers\&. The default value is +\fI65536\fR +bytes\&. +.RE +.PP +\fBshaper\fR: \fIShaper\fR +.RS 4 +This option defines a shaper for the file transfer peers\&. A shaper with the maximum bandwidth will be selected\&. The default is +\fInone\fR, i\&.e\&. no shaper\&. +.RE +.PP +\fBsndbuf\fR: \fISize\fR +.RS 4 +A size of the buffer for outgoing packets\&. If you define a shaper, set the value of this option to the size of the shaper in order to avoid traffic spikes in file transfers\&. The default value is +\fI65536\fR +bytes\&. +.RE +.PP +\fBvcard\fR: \fIvCard\fR +.RS 4 +A custom vCard of the service that will be displayed by some XMPP clients in Service Discovery\&. The value of +\fIvCard\fR +is a YAML map constructed from an XML representation of vCard\&. Since the representation has no attributes, the mapping is straightforward\&. +.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 +acl: + admin: + user: admin@example\&.org + proxy_users: + server: example\&.org + +access_rules: + proxy65_access: + allow: proxy_users + +shaper_rules: + proxy65_shaper: + none: admin + proxyrate: proxy_users + +shaper: + proxyrate: 10240 + +modules: + mod_proxy65: + host: proxy1\&.example\&.org + name: "File Transfer Proxy" + ip: 200\&.150\&.100\&.1 + port: 7778 + max_connections: 5 + access: proxy65_access + shaper: proxy65_shaper + recbuf: 10240 + sndbuf: 10240 +.fi +.if n \{\ +.RE +.\} +.RE +.SS "mod_pubsub" +.sp +This module offers a service for XEP\-0060: Publish\-Subscribe\&. The functionality in \fImod_pubsub\fR can be extended using plugins\&. The plugin that implements PEP (XEP\-0163: Personal Eventing via Pubsub) is enabled in the default ejabberd configuration file, and it requires \fImod_caps\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 +\fBaccess_createnode\fR: \fIAccessName\fR +.RS 4 +This option restricts which users are allowed to create pubsub nodes using +\fIacl\fR +and +\fIaccess\fR\&. By default any account in the local ejabberd server is allowed to create pubsub nodes\&. The default value is: +\fIall\fR\&. +.RE +.PP +\fBdb_type\fR: \fImnesia | sql\fR +.RS 4 +Same as top\-level +\fIdefault_db\fR +option, but applied to this module only\&. +.RE +.PP +\fBdefault_node_config\fR: \fIList of Key:Value\fR +.RS 4 +To override default node configuration, regardless of node plugin\&. Value is a list of key\-value definition\&. Node configuration still uses default configuration defined by node plugin, and overrides any items by value defined in this configurable list\&. +.RE +.PP +\fBforce_node_config\fR: \fIList of Node and the list of its Key:Value\fR +.RS 4 +Define the configuration for given nodes\&. The default value is: +\fI[]\fR\&. +.sp +\fBExample\fR: +.sp +.if n \{\ +.RS 4 +.\} +.nf +force_node_config: + ## Avoid buggy clients to make their bookmarks public + storage:bookmarks: + access_model: whitelist +.fi +.if n \{\ +.RE +.\} +.RE +.PP +\fBhost\fR +.RS 4 +Deprecated\&. Use +\fIhosts\fR +instead\&. +.RE +.PP +\fBhosts\fR: \fI[Host, \&.\&.\&.]\fR +.RS 4 +This option defines the Jabber IDs of the service\&. If the +\fIhosts\fR +option is not specified, the only Jabber ID will be the hostname of the virtual host with the prefix "pubsub\&."\&. The keyword +\fI@HOST@\fR +is replaced with the real virtual host name\&. +.RE +.PP +\fBignore_pep_from_offline\fR: \fIfalse | true\fR +.RS 4 +To specify whether or not we should get last published PEP items from users in our roster which are offline when we connect\&. Value is +\fItrue\fR +or +\fIfalse\fR\&. If not defined, pubsub assumes true so we only get last items of online contacts\&. +.RE +.PP +\fBlast_item_cache\fR: \fIfalse | true\fR +.RS 4 +To specify whether or not pubsub should cache last items\&. Value is +\fItrue\fR +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 you to raise the user connection rate\&. The cost is memory usage, as every item is stored in memory\&. +.RE +.PP +\fBmax_item_expire_node\fR: \fItimeout() | infinity\fR +.RS 4 +\fINote\fR +about this option: added in 21\&.12\&. 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: +\fI1000\fR\&. +.RE +.PP +\fBmax_nodes_discoitems\fR: \fIpos_integer() | infinity\fR +.RS 4 +The maximum number of nodes to return in a discoitem response\&. The default value is: +\fI100\fR\&. +.RE +.PP +\fBmax_subscriptions_node\fR: \fIMaxSubs\fR +.RS 4 +Define the maximum number of subscriptions managed by a node\&. Default value is no limitation: +\fIundefined\fR\&. +.RE +.PP +\fBname\fR: \fIName\fR +.RS 4 +The value of the service name\&. This name is only visible in some clients that support +XEP\-0030: Service Discovery\&. The default is +\fIvCard User Search\fR\&. +.RE +.PP +\fBnodetree\fR: \fINodetree\fR +.RS 4 +To specify which nodetree to use\&. If not defined, the default pubsub nodetree is used: +\fItree\fR\&. Only one nodetree can be used per host, and is shared by all node plugins\&. +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +\fItree\fR +nodetree store node configuration and relations on the database\&. +\fIflat\fR +nodes are stored without any relationship, and +\fIhometree\fR +nodes can have child nodes\&. +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +\fIvirtual\fR +nodetree does not store nodes on database\&. This saves resources on systems with tons of nodes\&. If using the +\fIvirtual\fR +nodetree, you can only enable those node plugins: +\fI[flat, pep]\fR +or +\fI[flat]\fR; any other plugins configuration will not work\&. Also, all nodes will have the default configuration, and this can not be changed\&. Using +\fIvirtual\fR +nodetree requires to start from a clean database, it will not work if you used the default +\fItree\fR +nodetree before\&. +.RE +.RE +.PP +\fBpep_mapping\fR: \fIList of Key:Value\fR +.RS 4 +In this option you can provide a list of key\-value to choose defined node plugins on given PEP namespace\&. The following example will use +\fInode_tune\fR +instead of +\fInode_pep\fR +for every PEP node with the tune namespace: +.sp +\fBExample\fR: +.sp +.if n \{\ +.RS 4 +.\} +.nf +modules: + \&.\&.\&. + mod_pubsub: + pep_mapping: + http://jabber\&.org/protocol/tune: tune + \&.\&.\&. +.fi +.if n \{\ +.RE +.\} +.RE +.PP +\fBplugins\fR: \fI[Plugin, \&.\&.\&.]\fR +.RS 4 +To specify which pubsub node plugins to use\&. The first one in the list is used by default\&. If this option is not defined, the default plugins list is: +\fI[flat]\fR\&. PubSub clients can define which plugin to use when creating a node: add +\fItype=\*(Aqplugin\-name\fR\*(Aq attribute to the +\fIcreate\fR +stanza element\&. +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +\fIflat\fR +plugin handles the default behaviour and follows standard XEP\-0060 implementation\&. +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +\fIpep\fR +plugin adds extension to handle Personal Eventing Protocol (XEP\-0163) to the PubSub engine\&. When enabled, PEP is handled automatically\&. +.RE +.RE +.PP +\fBvcard\fR: \fIvCard\fR +.RS 4 +A custom vCard of the server that will be displayed by some XMPP clients in Service Discovery\&. The value of +\fIvCard\fR +is a YAML map constructed from an XML representation of vCard\&. Since the representation has no attributes, the mapping is straightforward\&. +.sp +\fBExample\fR: +.sp +.if n \{\ +.RS 4 +.\} +.nf +# This XML representation of vCard: +# +# Conferences +# +# +# Elm Street +# +# +# +# is translated to: +vcard: + fn: Conferences + adr: + \- + work: true + street: Elm Street +.fi +.if n \{\ +.RE +.\} +.RE +.RE +.sp +.it 1 an-trap +.nr an-no-space-flag 1 +.nr an-break-flag 1 +.br +.ps +1 +\fBExamples:\fR +.RS 4 +.sp +Example of configuration that uses flat nodes as default, and allows use of flat, hometree and pep nodes: +.sp +.if n \{\ +.RS 4 +.\} +.nf +modules: + mod_pubsub: + access_createnode: pubsub_createnode + max_subscriptions_node: 100 + default_node_config: + notification_type: normal + notify_retract: false + max_items: 4 + plugins: + \- flat + \- pep +.fi +.if n \{\ +.RE +.\} +.sp +Using relational database requires using mod_pubsub with db_type \fIsql\fR\&. Only flat, hometree and pep plugins supports SQL\&. The following example shows previous configuration with SQL usage: +.sp +.if n \{\ +.RS 4 +.\} +.nf +modules: + mod_pubsub: + db_type: sql + access_createnode: pubsub_createnode + ignore_pep_from_offline: true + last_item_cache: false + plugins: + \- flat + \- pep +.fi +.if n \{\ +.RE +.\} +.sp +\fBAPI Tags:\fR \fI\&.\&./\&.\&./developer/ejabberd\-api/admin\-tags\&.md#purge|purge\fR +.RE +.SS "mod_pubsub_serverinfo" +.sp +\fINote\fR about this option: added in 25\&.07\&. +.sp +This module adds support for XEP\-0485: PubSub Server Information to expose S2S information over the Pub/Sub service\&. +.sp +Active S2S connections are published to a local PubSub node\&. Currently the node name is hardcoded as \fI"serverinfo"\fR\&. +.sp +Connections that support this feature are exposed with their domain names, otherwise they are shown as anonymous nodes\&. At startup a list of well known public servers is fetched\&. Those are not shown as anonymous even if they don\(cqt support this feature\&. +.sp +Please note that the module only shows S2S connections established while the module is running\&. If you install the module at runtime, run \fIstop_s2s_connections\fR API or restart ejabberd to force S2S reconnections that the module will detect and publish\&. +.sp +This module depends on \fImod_pubsub\fR and \fImod_disco\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 +\fBpubsub_host\fR: \fIundefined | string()\fR +.RS 4 +Use this local PubSub host to advertise S2S connections\&. This must be a host local to this service handled by +\fImod_pubsub\fR\&. This option is only needed if your configuration has more than one host in mod_pubsub\(cqs +\fIhosts\fR +option\&. The default value is the first host defined in mod_pubsub +\fIhosts\fR +option\&. +.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 +modules: + mod_pubsub_serverinfo: + pubsub_host: custom\&.pubsub\&.domain\&.local +.fi +.if n \{\ +.RE +.\} +.RE +.SS "mod_push" +.sp +This module implements the XMPP server\(cqs part of the push notification solution specified in XEP\-0357: Push Notifications\&. It does not generate, for example, APNS or FCM notifications directly\&. Instead, it\(cqs designed to work with so\-called "app servers" operated by third\-party vendors of mobile apps\&. Those app servers will usually trigger notification delivery to the user\(cqs mobile device using platform\-dependent backend services such as FCM or APNS\&. +.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 +\fBcache_life_time\fR: \fItimeout()\fR +.RS 4 +Same as top\-level +\fIcache_life_time\fR +option, but applied to this module only\&. +.RE +.PP +\fBcache_missed\fR: \fItrue | false\fR +.RS 4 +Same as top\-level +\fIcache_missed\fR +option, but applied to this module only\&. +.RE +.PP +\fBcache_size\fR: \fIpos_integer() | infinity\fR +.RS 4 +Same as top\-level +\fIcache_size\fR +option, but applied to this module only\&. +.RE +.PP +\fBdb_type\fR: \fImnesia | sql\fR +.RS 4 +Same as top\-level +\fIdefault_db\fR +option, but applied to this module only\&. +.RE +.PP +\fBinclude_body\fR: \fItrue | false | Text\fR +.RS 4 +If this option is set to +\fItrue\fR, the message text is included with push notifications generated for incoming messages with a body\&. The option can instead be set to a static +\fIText\fR, in which case the specified text will be included in place of the actual message body\&. This can be useful to signal the app server whether the notification was triggered by a message with body (as opposed to other types of traffic) without leaking actual message contents\&. The default value is "New message"\&. +.RE +.PP +\fBinclude_sender\fR: \fItrue | false\fR +.RS 4 +If this option is set to +\fItrue\fR, the sender\(cqs JID is included with push notifications generated for incoming messages with a body\&. The default value is +\fIfalse\fR\&. +.RE +.PP +\fBnotify_on\fR: \fImessages | all\fR +.RS 4 +\fINote\fR +about this option: added in 23\&.10\&. If this option is set to +\fImessages\fR, notifications are generated only for actual chat messages with a body text (or some encrypted payload)\&. If it\(cqs set to +\fIall\fR, any kind of XMPP stanza will trigger a notification\&. If unsure, it\(cqs strongly recommended to stick to +\fIall\fR, which is the default value\&. +.RE +.PP +\fBuse_cache\fR: \fItrue | false\fR +.RS 4 +Same as top\-level +\fIuse_cache\fR +option, but applied to this module only\&. +.RE +.sp +\fBAPI Tags:\fR \fI\&.\&./\&.\&./developer/ejabberd\-api/admin\-tags\&.md#purge|purge\fR +.RE +.SS "mod_push_keepalive" +.sp +This module tries to keep the stream management session (see \fImod_stream_mgmt\fR) of a disconnected mobile client alive if the client enabled push notifications for that session\&. However, the normal session resumption timeout is restored once a push notification is issued, so the session will be closed if the client doesn\(cqt respond to push notifications\&. +.sp +The module depends on \fImod_push\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 +\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 +\fImod_stream_mgmt\fR +is restored\&. The default value is +\fI72\fR +hours\&. +.RE +.PP +\fBwake_on_start\fR: \fItrue | false\fR +.RS 4 +If this option is set to +\fItrue\fR, notifications are generated for +\fBall\fR +registered push clients during server startup\&. This option should not be enabled on servers with many push clients as it can generate significant load on the involved push services and the server itself\&. The default value is +\fIfalse\fR\&. +.RE +.PP +\fBwake_on_timeout\fR: \fItrue | false\fR +.RS 4 +If this option is set to +\fItrue\fR, a notification is generated shortly before the session would time out as per the +\fIresume_timeout\fR +option\&. The default value is +\fItrue\fR\&. +.RE +.RE +.SS "mod_register" +.sp +This module adds support for XEP\-0077: In\-Band Registration\&. This protocol enables end users to use an XMPP client to: +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +Register a new account on the server\&. +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +Change the password from an existing account on the server\&. +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +Delete an existing account on the server\&. +.RE +.sp +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 +.nr an-break-flag 1 +.br +.ps +1 +\fBAvailable options:\fR +.RS 4 +.PP +\fBaccess\fR: \fIAccessName\fR +.RS 4 +Specify rules to restrict what usernames can be registered\&. If a rule returns +\fIdeny\fR +on the requested username, registration of that user name is denied\&. There are no restrictions by default\&. If +\fIAccessName\fR +is +\fInone\fR, then registering new accounts using In\-Band Registration is disabled and the corresponding stream feature is not announced to clients\&. +.RE +.PP +\fBaccess_from\fR: \fIAccessName\fR +.RS 4 +By default, ejabberd doesn\(cqt allow the client to register new accounts from s2s or existing c2s sessions\&. You can change it by defining access rule in this option\&. Use with care: allowing registration from s2s leads to uncontrolled massive accounts creation by rogue users\&. +.RE +.PP +\fBaccess_remove\fR: \fIAccessName\fR +.RS 4 +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 +\fINote\fR +about this option: added in 21\&.12\&. 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 +\fIbasic\&.md#captcha|CAPTCHA\fR\&. The default is +\fIfalse\fR\&. +.RE +.PP +\fBip_access\fR: \fIAccessName\fR +.RS 4 +Define rules to allow or deny account registration depending on the IP address of the XMPP client\&. The +\fIAccessName\fR +should be of type +\fIip\fR\&. The default value is +\fIall\fR\&. +.RE +.PP +\fBpassword_strength\fR: \fIEntropy\fR +.RS 4 +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 +\fI0\fR, i\&.e\&. no checks are performed\&. +.RE +.PP +\fBredirect_url\fR: \fIURL\fR +.RS 4 +This option enables registration redirection as described in +XEP\-0077: In\-Band Registration: Redirection\&. +.RE +.PP +\fBregistration_watchers\fR: \fI[JID, \&.\&.\&.]\fR +.RS 4 +This option defines a list of JIDs which will be notified each time a new account is registered\&. +.RE +.PP +\fBwelcome_message\fR: \fI{subject: Subject, body: Body}\fR +.RS 4 +Set a welcome message that is sent to each newly registered account\&. The message will have subject +\fISubject\fR +and text +\fIBody\fR\&. +.sp +\fBExample\fR: +.sp +.if n \{\ +.RS 4 +.\} +.nf +modules: + mod_register: + welcome_message: + subject: "Welcome!" + body: |\- + Hi! + Welcome to this XMPP server +.fi +.if n \{\ +.RE +.\} +.RE +.RE +.SS "mod_register_web" +.sp +This module provides a web page where users can: +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +Register a new account on the server\&. +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +Change the password from an existing account on the server\&. +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +Unregister an existing account on the server\&. +.RE +.sp +This module supports \fIbasic\&.md#captcha|CAPTCHA\fR 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 \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 +This module is enabled in \fIlisten\fR → \fIejabberd_http\fR → \fIlisten\-options\&.md#request_handlers|request_handlers\fR, 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\&. +.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 +\fBaccess\fR: \fIAccessName\fR +.RS 4 +This option can be configured to specify rules to restrict roster management\&. If the rule returns +\fIdeny\fR +on the requested user name, that user cannot modify their personal roster, i\&.e\&. they cannot add/remove/modify contacts or send presence subscriptions\&. The default value is +\fIall\fR, i\&.e\&. no restrictions\&. +.RE +.PP +\fBcache_life_time\fR: \fItimeout()\fR +.RS 4 +Same as top\-level +\fIcache_life_time\fR +option, but applied to this module only\&. +.RE +.PP +\fBcache_missed\fR: \fItrue | false\fR +.RS 4 +Same as top\-level +\fIcache_missed\fR +option, but applied to this module only\&. +.RE +.PP +\fBcache_size\fR: \fIpos_integer() | infinity\fR +.RS 4 +Same as top\-level +\fIcache_size\fR +option, but applied to this module only\&. +.RE +.PP +\fBdb_type\fR: \fImnesia | sql\fR +.RS 4 +Same as top\-level +\fIdefault_db\fR +option, but applied to this module only\&. +.RE +.PP +\fBstore_current_id\fR: \fItrue | false\fR +.RS 4 +If this option is set to +\fItrue\fR, the current roster version number is stored on the database\&. If set to +\fIfalse\fR, the roster version number is calculated on the fly each time\&. Enabling this option reduces the load for both ejabberd and the database\&. This option does not affect the client in any way\&. This option is only useful if option +\fIversioning\fR +is set to +\fItrue\fR\&. The default value is +\fIfalse\fR\&. IMPORTANT: if you use +\fImod_shared_roster\fR +or +\fImod_shared_roster_ldap\fR, you must set the value of the option to +\fIfalse\fR\&. +.RE +.PP +\fBuse_cache\fR: \fItrue | false\fR +.RS 4 +Same as top\-level +\fIuse_cache\fR +option, but applied to this module only\&. +.RE +.PP +\fBversioning\fR: \fItrue | false\fR +.RS 4 +Enables/disables Roster Versioning\&. The default value is +\fIfalse\fR\&. +.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 +modules: + mod_roster: + versioning: true + store_current_id: false +.fi +.if n \{\ +.RE +.\} +.sp +\fBAPI Tags:\fR \fI\&.\&./\&.\&./developer/ejabberd\-api/admin\-tags\&.md#roster|roster\fR +.RE +.SS "mod_s2s_bidi" +.sp +\fINote\fR about this option: added in 24\&.10\&. +.sp +The module adds support for XEP\-0288: Bidirectional Server\-to\-Server Connections that allows using single s2s connection to communicate in both directions\&. +.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 +modules: + mod_s2s_bidi: {} +.fi +.if n \{\ +.RE +.\} +.RE +.SS "mod_s2s_dialback" +.sp +The module adds support for XEP\-0220: Server Dialback to provide server identity verification based on DNS\&. +.if n \{\ +.sp +.\} +.RS 4 +.it 1 an-trap +.nr an-no-space-flag 1 +.nr an-break-flag 1 +.br +.ps +1 +\fBWarning\fR +.ps -1 +.br +.sp +DNS\-based verification is vulnerable to DNS cache poisoning, so modern servers rely on verification based on PKIX certificates\&. Thus this module is only recommended for backward compatibility with servers running outdated software or non\-TLS servers, or those with invalid certificates (as long as you accept the risks, e\&.g\&. you assume that the remote server has an invalid certificate due to poor administration and not because it\(cqs compromised)\&. +.sp .5v +.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 +\fBaccess\fR: \fIAccessName\fR +.RS 4 +An access rule that can be used to restrict dialback for some servers\&. The default value is +\fIall\fR\&. +.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 +modules: + mod_s2s_dialback: + access: + allow: + server: legacy\&.domain\&.tld + server: invalid\-cert\&.example\&.org + deny: all +.fi +.if n \{\ +.RE +.\} +.RE +.SS "mod_scram_upgrade" +.sp +\fINote\fR about this option: added in 24\&.10\&. +.sp +The module adds support for XEP\-0480: SASL Upgrade Tasks that allows users to upgrade passwords to more secure representation\&. +.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 +\fBoffered_upgrades\fR: \fIlist(sha256, sha512)\fR +.RS 4 +List with upgrade types that should be offered +.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 +modules: + mod_scram_upgrade: + offered_upgrades: + \- sha256 + \- sha512 +.fi +.if n \{\ +.RE +.\} +.RE +.SS "mod_service_log" +.sp +This module forwards copies of all stanzas to remote XMPP servers or components\&. Every stanza is encapsulated into element as described in XEP\-0297: Stanza Forwarding\&. +.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 +\fBloggers\fR: \fI[Domain, \&.\&.\&.]\fR +.RS 4 +A list of servers or connected components to which stanzas will be forwarded\&. +.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 +modules: + mod_service_log: + loggers: + \- xmpp\-server\&.tld + \- component\&.domain\&.tld +.fi +.if n \{\ +.RE +.\} +.RE +.SS "mod_shared_roster" +.sp +This module enables you to create shared roster groups: groups of accounts that can see members from (other) groups in their rosters\&. +.sp +The big advantages of this feature are that end users do not need to manually add all users to their rosters, and that they cannot permanently delete users from the shared roster groups\&. A shared roster group can have members from any XMPP server, but the presence will only be available from and to members of the same virtual host where the group is created\&. It still allows the users to have / add their own contacts, as it does not replace the standard roster\&. Instead, the shared roster contacts are merged to the relevant users at retrieval time\&. The standard user rosters thus stay unmodified\&. +.sp +Shared roster groups can be edited via the Web Admin, and some API commands called \fIsrg_\fR, for example \fIsrg_add\fR API\&. Each group has a unique name and those parameters: +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +Label: Used in the rosters where this group is displayed\&. +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +Description: of the group, which has no effect\&. +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +Members: A list of JIDs of group members, entered one per line in the Web Admin\&. The special member directive +\fI@all@\fR +represents all the registered users in the virtual host; which is only recommended for a small server with just a few hundred users\&. The special member directive +\fI@online@\fR +represents the online users in the virtual host\&. With those two directives, the actual list of members in those shared rosters is generated dynamically at retrieval time\&. +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +Displayed: A list of groups that will be in the rosters of this group\(cqs members\&. A group of other vhost can be identified with +\fIgroupid@vhost\fR\&. +.RE +.sp +This module depends on \fImod_roster\fR\&. If not enabled, roster queries will return 503 errors\&. +.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 +\fBcache_life_time\fR: \fItimeout()\fR +.RS 4 +Same as top\-level +\fIcache_life_time\fR +option, but applied to this module only\&. +.RE +.PP +\fBcache_missed\fR: \fItrue | false\fR +.RS 4 +Same as top\-level +\fIcache_missed\fR +option, but applied to this module only\&. +.RE +.PP +\fBcache_size\fR: \fIpos_integer() | infinity\fR +.RS 4 +Same as top\-level +\fIcache_size\fR +option, but applied to this module only\&. +.RE +.PP +\fBdb_type\fR: \fImnesia | sql\fR +.RS 4 +Same as top\-level +\fIdefault_db\fR +option, but applied to this module only\&. +.RE +.PP +\fBuse_cache\fR: \fItrue | false\fR +.RS 4 +Same as top\-level +\fIuse_cache\fR +option, but applied to this module only\&. +.RE +.RE +.sp +.it 1 an-trap +.nr an-no-space-flag 1 +.nr an-break-flag 1 +.br +.ps +1 +\fBExamples:\fR +.RS 4 +.sp +Take the case of a computer club that wants all its members seeing each other in their rosters\&. To achieve this, they need to create a shared roster group similar to this one: +.sp +.if n \{\ +.RS 4 +.\} +.nf +Name: club_members +Label: Club Members +Description: Members from the computer club +Members: member1@example\&.org, member2@example\&.org, member3@example\&.org +Displayed Groups: club_members +.fi +.if n \{\ +.RE +.\} +.sp +In another case we have a company which has three divisions: Management, Marketing and Sales\&. All group members should see all other members in their rosters\&. Additionally, all managers should have all marketing and sales people in their roster\&. Simultaneously, all marketeers and the whole sales team should see all managers\&. This scenario can be achieved by creating shared roster groups as shown in the following lists: +.sp +.if n \{\ +.RS 4 +.\} +.nf +First list: +Name: management +Label: Management +Description: Management +Members: manager1@example\&.org, manager2@example\&.org +Displayed: management, marketing, sales + +Second list: +Name: marketing +Label: Marketing +Description: Marketing +Members: marketeer1@example\&.org, marketeer2@example\&.org, marketeer3@example\&.org +Displayed: management, marketing + +Third list: +Name: sales +Label: Sales +Description: Sales +Members: salesman1@example\&.org, salesman2@example\&.org, salesman3@example\&.org +Displayed: management, sales +.fi +.if n \{\ +.RE +.\} +.RE +.SS "mod_shared_roster_ldap" +.sp +This module lets the server administrator automatically populate users\*(Aq rosters (contact lists) with entries based on users and groups defined in an LDAP\-based directory\&. +.if n \{\ +.sp +.\} +.RS 4 +.it 1 an-trap +.nr an-no-space-flag 1 +.nr an-break-flag 1 +.br +.ps +1 +\fBNote\fR +.ps -1 +.br +.sp +\fImod_shared_roster_ldap\fR depends on \fImod_roster\fR being enabled\&. Roster queries will return \fI503\fR errors if \fImod_roster\fR is not enabled\&. +.sp .5v +.RE +.sp +The module accepts many configuration options\&. Some of them, if unspecified, default to the values specified for the top level of configuration\&. This lets you avoid specifying, for example, the bind password in multiple places\&. +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +Filters: +\fIldap_rfilter\fR, +\fIldap_ufilter\fR, +\fIldap_gfilter\fR, +\fIldap_filter\fR\&. These options specify LDAP filters used to query for shared roster information\&. All of them are run against the ldap_base\&. +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +Attributes: +\fIldap_groupattr\fR, +\fIldap_groupdesc\fR, +\fIldap_memberattr\fR, +\fIldap_userdesc\fR, +\fIldap_useruid\fR\&. These options specify the names of the attributes which hold interesting data in the entries returned by running filters specified with the filter options\&. +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +Control parameters: +\fIldap_auth_check\fR, +\fIldap_group_cache_validity\fR, +\fIldap_memberattr_format\fR, +\fIldap_memberattr_format_re\fR, +\fIldap_user_cache_validity\fR\&. These parameters control the behaviour of the module\&. +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +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 +\fIldap\&.md#ldap\-connection|LDAP Connection\fR +section for more information about them\&. +.RE +.sp +Check also the \fIldap\&.md#ldap\-examples|Configuration examples\fR section to get details about retrieving the roster, and configuration examples including Flat DIT and Deep DIT\&. +.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 +\fBcache_life_time\fR +.RS 4 +Same as top\-level +\fIcache_life_time\fR +option, but applied to this module only\&. +.RE +.PP +\fBcache_missed\fR +.RS 4 +Same as top\-level +\fIcache_missed\fR +option, but applied to this module only\&. +.RE +.PP +\fBcache_size\fR +.RS 4 +Same as top\-level +\fIcache_size\fR +option, but applied to this module only\&. +.RE +.PP +\fBldap_auth_check\fR: \fItrue | false\fR +.RS 4 +Whether the module should check (via the ejabberd authentication subsystem) for existence of each user in the shared LDAP roster\&. Set to +\fIfalse\fR +if you want to disable the check\&. Default value is +\fItrue\fR\&. +.RE +.PP +\fBldap_backups\fR +.RS 4 +Same as top\-level +\fIldap_backups\fR +option, but applied to this module only\&. +.RE +.PP +\fBldap_base\fR +.RS 4 +Same as top\-level +\fIldap_base\fR +option, but applied to this module only\&. +.RE +.PP +\fBldap_deref_aliases\fR +.RS 4 +Same as top\-level +\fIldap_deref_aliases\fR +option, but applied to this module only\&. +.RE +.PP +\fBldap_encrypt\fR +.RS 4 +Same as top\-level +\fIldap_encrypt\fR +option, but applied to this module only\&. +.RE +.PP +\fBldap_filter\fR +.RS 4 +Additional filter which is AND\-ed together with "User Filter" and "Group Filter"\&. For more information check the LDAP +\fIldap\&.md#filters|Filters\fR +section\&. +.RE +.PP +\fBldap_gfilter\fR +.RS 4 +"Group Filter", used when retrieving human\-readable name (a\&.k\&.a\&. "Display Name") and the members of a group\&. See also the parameters +\fIldap_groupattr\fR, +\fIldap_groupdesc\fR +and +\fIldap_memberattr\fR\&. If unspecified, defaults to the top\-level parameter of the same name\&. If that one also is unspecified, then the filter is constructed exactly like "User Filter"\&. +.RE +.PP +\fBldap_groupattr\fR +.RS 4 +The name of the attribute that holds the group name, and that is used to differentiate between them\&. Retrieved from results of the "Roster Filter" and "Group Filter"\&. Defaults to +\fIcn\fR\&. +.RE +.PP +\fBldap_groupdesc\fR +.RS 4 +The name of the attribute which holds the human\-readable group name in the objects you use to represent groups\&. Retrieved from results of the "Group Filter"\&. Defaults to whatever +\fIldap_groupattr\fR +is set\&. +.RE +.PP +\fBldap_memberattr\fR +.RS 4 +The name of the attribute which holds the IDs of the members of a group\&. Retrieved from results of the "Group Filter"\&. Defaults to +\fImemberUid\fR\&. The name of the attribute differs depending on the objectClass you use for your group objects, for example: +\fIposixGroup\fR +→ +\fImemberUid\fR; +\fIgroupOfNames\fR +→ +\fImember\fR; +\fIgroupOfUniqueNames\fR +→ +\fIuniqueMember\fR\&. +.RE +.PP +\fBldap_memberattr_format\fR +.RS 4 +A globbing format for extracting user ID from the value of the attribute named by +\fIldap_memberattr\fR\&. Defaults to +\fI%u\fR, which means that the whole value is the member ID\&. If you change it to something different, you may also need to specify the User and Group Filters manually; see section Filters\&. +.RE +.PP +\fBldap_memberattr_format_re\fR +.RS 4 +A regex for extracting user ID from the value of the attribute named by +\fIldap_memberattr\fR\&. Check the LDAP +\fIldap\&.md#control\-parameters|Control Parameters\fR +section\&. +.RE +.PP +\fBldap_password\fR +.RS 4 +Same as top\-level +\fIldap_password\fR +option, but applied to this module only\&. +.RE +.PP +\fBldap_port\fR +.RS 4 +Same as top\-level +\fIldap_port\fR +option, but applied to this module only\&. +.RE +.PP +\fBldap_rfilter\fR +.RS 4 +So called "Roster Filter"\&. Used to find names of all "shared roster" groups\&. See also the +\fIldap_groupattr\fR +parameter\&. If unspecified, defaults to the top\-level parameter of the same name\&. You must specify it in some place in the configuration, there is no default\&. +.RE +.PP +\fBldap_rootdn\fR +.RS 4 +Same as top\-level +\fIldap_rootdn\fR +option, but applied to this module only\&. +.RE +.PP +\fBldap_servers\fR +.RS 4 +Same as top\-level +\fIldap_servers\fR +option, but applied to this module only\&. +.RE +.PP +\fBldap_tls_cacertfile\fR +.RS 4 +Same as top\-level +\fIldap_tls_cacertfile\fR +option, but applied to this module only\&. +.RE +.PP +\fBldap_tls_certfile\fR +.RS 4 +Same as top\-level +\fIldap_tls_certfile\fR +option, but applied to this module only\&. +.RE +.PP +\fBldap_tls_depth\fR +.RS 4 +Same as top\-level +\fIldap_tls_depth\fR +option, but applied to this module only\&. +.RE +.PP +\fBldap_tls_verify\fR +.RS 4 +Same as top\-level +\fIldap_tls_verify\fR +option, but applied to this module only\&. +.RE +.PP +\fBldap_ufilter\fR +.RS 4 +"User Filter", used for retrieving the human\-readable name of roster entries (usually full names of people in the roster)\&. See also the parameters +\fIldap_userdesc\fR +and +\fIldap_useruid\fR\&. For more information check the LDAP +\fIldap\&.md#filters|Filters\fR +section\&. +.RE +.PP +\fBldap_uids\fR +.RS 4 +Same as top\-level +\fIldap_uids\fR +option, but applied to this module only\&. +.RE +.PP +\fBldap_userdesc\fR +.RS 4 +The name of the attribute which holds the human\-readable user name\&. Retrieved from results of the "User Filter"\&. Defaults to +\fIcn\fR\&. +.RE +.PP +\fBldap_userjidattr\fR +.RS 4 +The name of the attribute which is used to map user id to XMPP jid\&. If not specified (and that is default value of this option), user jid will be created from user id and this module host\&. +.RE +.PP +\fBldap_useruid\fR +.RS 4 +The name of the attribute which holds the ID of a roster item\&. Value of this attribute in the roster item objects needs to match the ID retrieved from the +\fIldap_memberattr\fR +attribute of a group object\&. Retrieved from results of the "User Filter"\&. Defaults to +\fIcn\fR\&. +.RE +.PP +\fBuse_cache\fR +.RS 4 +Same as top\-level +\fIuse_cache\fR +option, but applied to this module only\&. +.RE +.RE +.SS "mod_sic" +.sp +This module adds support for XEP\-0279: Server IP Check\&. This protocol enables a client to discover its external IP address\&. +.if n \{\ +.sp +.\} +.RS 4 +.it 1 an-trap +.nr an-no-space-flag 1 +.nr an-break-flag 1 +.br +.ps +1 +\fBWarning\fR +.ps -1 +.br +.sp +The protocol extension is deferred and seems like there are no clients supporting it, so using this module is not recommended and, furthermore, the module might be removed in the future\&. +.sp .5v +.RE +.sp +The module has no options\&. +.SS "mod_sip" +.sp +This module adds SIP proxy/registrar support for the corresponding virtual host\&. +.if n \{\ +.sp +.\} +.RS 4 +.it 1 an-trap +.nr an-no-space-flag 1 +.nr an-break-flag 1 +.br +.ps +1 +\fBNote\fR +.ps -1 +.br +.sp +It is not enough to just load this module\&. You should also configure listeners and DNS records properly\&. For details see the section about the \fIlisten\&.md#ejabberd_sip|ejabberd_sip\fR listen module in the ejabberd Documentation\&. +.sp .5v +.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 +\fBalways_record_route\fR: \fItrue | false\fR +.RS 4 +Always insert "Record\-Route" header into SIP messages\&. With this approach it is possible to bypass NATs/firewalls a bit more easily\&. The default value is +\fItrue\fR\&. +.RE +.PP +\fBflow_timeout_tcp\fR: \fItimeout()\fR +.RS 4 +The option sets a keep\-alive timer for +SIP outbound +TCP connections\&. The default value is +\fI2\fR +minutes\&. +.RE +.PP +\fBflow_timeout_udp\fR: \fItimeout()\fR +.RS 4 +The options sets a keep\-alive timer for +SIP outbound +UDP connections\&. The default value is +\fI29\fR +seconds\&. +.RE +.PP +\fBrecord_route\fR: \fIURI\fR +.RS 4 +When the option +\fIalways_record_route\fR +is set to +\fItrue\fR +or when +SIP outbound +is utilized, ejabberd inserts "Record\-Route" header field with this +\fIURI\fR +into a SIP message\&. The default is a SIP URI constructed from the virtual host on which the module is loaded\&. +.RE +.PP +\fBroutes\fR: \fI[URI, \&.\&.\&.]\fR +.RS 4 +You can set a list of SIP URIs of routes pointing to this SIP proxy server\&. The default is a list containing a single SIP URI constructed from the virtual host on which the module is loaded\&. +.RE +.PP +\fBvia\fR: \fI[URI, \&.\&.\&.]\fR +.RS 4 +A list to construct "Via" headers for inserting them into outgoing SIP messages\&. This is useful if you\(cqre running your SIP proxy in a non\-standard network topology\&. Every +\fIURI\fR +element in the list must be in the form of "scheme://host:port", where "transport" must be +\fItls\fR, +\fItcp\fR, or +\fIudp\fR, "host" must be a domain name or an IP address and "port" must be an internet port number\&. Note that all parts of the +\fIURI\fR +are mandatory (e\&.g\&. you cannot omit "port" or "scheme")\&. +.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 +modules: + mod_sip: + always_record_route: false + record_route: "sip:example\&.com;lr" + routes: + \- "sip:example\&.com;lr" + \- "sip:sip\&.example\&.com;lr" + flow_timeout_udp: 30 sec + flow_timeout_tcp: 1 min + via: + \- tls://sip\-tls\&.example\&.com:5061 + \- tcp://sip\-tcp\&.example\&.com:5060 + \- udp://sip\-udp\&.example\&.com:5060 +.fi +.if n \{\ +.RE +.\} +.RE +.SS "mod_stats" +.sp +This module adds support for XEP\-0039: Statistics Gathering\&. This protocol allows you to retrieve the following statistics from your ejabberd server: +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +Total number of registered users on the current virtual host (users/total)\&. +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +Total number of registered users on all virtual hosts (users/all\-hosts/total)\&. +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +Total number of online users on the current virtual host (users/online)\&. +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +Total number of online users on all virtual hosts (users/all\-hosts/online)\&. +.RE +.if n \{\ +.sp +.\} +.RS 4 +.it 1 an-trap +.nr an-no-space-flag 1 +.nr an-break-flag 1 +.br +.ps +1 +\fBNote\fR +.ps -1 +.br +.sp +The protocol extension is deferred and seems like even a few clients that were supporting it are now abandoned\&. So using this module makes very little sense\&. +.sp .5v +.RE +.sp +The module has no options\&. +.SS "mod_stream_mgmt" +.sp +This module adds support for XEP\-0198: Stream Management\&. This protocol allows active management of an XML stream between two XMPP entities, including features for stanza acknowledgments and stream resumption\&. +.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 +\fBack_timeout\fR: \fItimeout()\fR +.RS 4 +A time to wait for stanza acknowledgments\&. Setting it to +\fIinfinity\fR +effectively disables the timeout\&. The default value is +\fI1\fR +minute\&. +.RE +.PP +\fBcache_life_time\fR: \fItimeout()\fR +.RS 4 +Same as top\-level +\fIcache_life_time\fR +option, but applied to this module only\&. The default value is +\fI48 hours\fR\&. +.RE +.PP +\fBcache_size\fR: \fIpos_integer() | infinity\fR +.RS 4 +Same as top\-level +\fIcache_size\fR +option, but applied to this module only\&. +.RE +.PP +\fBmax_ack_queue\fR: \fISize\fR +.RS 4 +This option specifies the maximum number of unacknowledged stanzas queued for possible retransmission\&. When the limit is exceeded, the client session is terminated\&. The allowed values are positive integers and +\fIinfinity\fR\&. You should be careful when setting this value as it should not be set too low, otherwise, you could kill sessions in a loop, before they get the chance to finish proper session initiation\&. It should definitely be set higher that the size of the offline queue (for example at least 3 times the value of the max offline queue and never lower than +\fI1000\fR)\&. The default value is +\fI5000\fR\&. +.RE +.PP +\fBmax_resume_timeout\fR: \fItimeout()\fR +.RS 4 +A client may specify the period of time until a session times out if the connection is lost\&. During this period of time, the client may resume its session\&. This option limits the period of time a client is permitted to request\&. It must be set to a timeout equal to or larger than the default +\fIresume_timeout\fR\&. By default, it is set to the same value as the +\fIresume_timeout\fR +option\&. +.RE +.PP +\fBqueue_type\fR: \fIram | file\fR +.RS 4 +Same as top\-level +\fIqueue_type\fR +option, but applied to this module only\&. +.RE +.PP +\fBresend_on_timeout\fR: \fItrue | false | if_offline\fR +.RS 4 +If this option is set to +\fItrue\fR, any message stanzas that weren\(cqt acknowledged by the client will be resent on session timeout\&. This behavior might often be desired, but could have unexpected results under certain circumstances\&. For example, a message that was sent to two resources might get resent to one of them if the other one timed out\&. Therefore, the default value for this option is +\fIfalse\fR, which tells ejabberd to generate an error message instead\&. As an alternative, the option may be set to +\fIif_offline\fR\&. In this case, unacknowledged messages are resent only if no other resource is online when the session times out\&. Otherwise, error messages are generated\&. +.RE +.PP +\fBresume_timeout\fR: \fItimeout()\fR +.RS 4 +This option configures the (default) period of time until a session times out if the connection is lost\&. During this period of time, a client may resume its session\&. Note that the client may request a different timeout value, see the +\fImax_resume_timeout\fR +option\&. Setting it to +\fI0\fR +effectively disables session resumption\&. The default value is +\fI5\fR +minutes\&. +.RE +.RE +.SS "mod_stun_disco" +.sp +\fINote\fR about this option: added in 20\&.04\&. +.sp +This module allows XMPP clients to discover STUN/TURN services and to obtain temporary credentials for using them as per XEP\-0215: External Service Discovery\&. +.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 +\fBaccess\fR: \fIAccessName\fR +.RS 4 +This option defines which access rule will be used to control who is allowed to discover STUN/TURN services and to request temporary credentials\&. The default value is +\fIlocal\fR\&. +.RE +.PP +\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 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 +.PP +\fBoffer_local_services\fR: \fItrue | false\fR +.RS 4 +This option specifies whether local STUN/TURN services configured as ejabberd listeners should be announced automatically\&. Note that this will not include TLS\-enabled services, which must be configured manually using the +\fIservices\fR +option (see below)\&. For non\-anonymous TURN services, temporary credentials will be offered to the client\&. The default value is +\fItrue\fR\&. +.RE +.PP +\fBsecret\fR: \fIText\fR +.RS 4 +The secret used for generating temporary credentials\&. If this option isn\(cqt specified, a secret will be auto\-generated\&. However, a secret must be specified explicitly if non\-anonymous TURN services running on other ejabberd nodes and/or external TURN +\fIservices\fR +are configured\&. Also note that auto\-generated secrets are lost when the node is restarted, which invalidates any credentials offered before the restart\&. Therefore, it\(cqs recommended to explicitly specify a secret if clients cache retrieved credentials (for later use) across service restarts\&. +.RE +.PP +\fBservices\fR: \fI[Service, \&.\&.\&.]\fR +.RS 4 +The list of services offered to clients\&. This list can include STUN/TURN services running on any ejabberd node and/or external services\&. However, if any listed TURN service not running on the local ejabberd node requires authentication, a +\fIsecret\fR +must be specified explicitly, and must be shared with that service\&. This will only work with ejabberd\(cqs built\-in STUN/TURN server and with external servers that support the same +REST API For Access To TURN Services\&. Unless the +\fIoffer_local_services\fR +is set to +\fIfalse\fR, the explicitly listed services will be offered in addition to those announced automatically\&. +.PP +\fBhost\fR: \fIHost\fR +.RS 4 +The hostname or IP address the STUN/TURN service is listening on\&. For non\-TLS services, it\(cqs recommended to specify an IP address (to avoid additional DNS lookup latency on the client side)\&. For TLS services, the hostname (or IP address) should match the certificate\&. Specifying the +\fIhost\fR +option is mandatory\&. +.RE +.PP +\fBport\fR: \fI1\&.\&.65535\fR +.RS 4 +The port number the STUN/TURN service is listening on\&. The default port number is 3478 for non\-TLS services and 5349 for TLS services\&. +.RE +.PP +\fBrestricted\fR: \fItrue | false\fR +.RS 4 +This option determines whether temporary credentials for accessing the service are offered\&. The default is +\fIfalse\fR +for STUN/STUNS services and +\fItrue\fR +for TURN/TURNS services\&. +.RE +.PP +\fBtransport\fR: \fItcp | udp\fR +.RS 4 +The transport protocol supported by the service\&. The default is +\fIudp\fR +for non\-TLS services and +\fItcp\fR +for TLS services\&. +.RE +.PP +\fBtype\fR: \fIstun | turn | stuns | turns\fR +.RS 4 +The type of service\&. Must be +\fIstun\fR +or +\fIturn\fR +for non\-TLS services, +\fIstuns\fR +or +\fIturns\fR +for TLS services\&. The default type is +\fIstun\fR\&. +.RE +.sp +\fBExample\fR: +.sp +.if n \{\ +.RS 4 +.\} +.nf +services: + \- + host: 203\&.0\&.113\&.3 + port: 3478 + type: stun + transport: udp + restricted: false + \- + host: 203\&.0\&.113\&.3 + port: 3478 + type: turn + transport: udp + restricted: true + \- + host: 2001:db8::3 + port: 3478 + type: stun + transport: udp + restricted: false + \- + host: 2001:db8::3 + port: 3478 + type: turn + transport: udp + restricted: true + \- + host: server\&.example\&.com + port: 5349 + type: turns + transport: tcp + restricted: true +.fi +.if n \{\ +.RE +.\} +.RE +.RE +.SS "mod_time" +.sp +This module adds support for XEP\-0202: Entity Time\&. In other words, the module reports server\(cqs system time\&. +.sp +The module has no options\&. +.SS "mod_vcard" +.sp +This module allows end users to store and retrieve their vCard, and to retrieve other users vCards, as defined in XEP\-0054: vcard\-temp\&. The module also implements an uncomplicated Jabber User Directory based on the vCards of these users\&. Moreover, it enables the server to send its vCard when queried\&. +.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 +\fBallow_return_all\fR: \fItrue | false\fR +.RS 4 +This option enables you to specify if search operations with empty input fields should return all users who added some information to their vCard\&. The default value is +\fIfalse\fR\&. +.RE +.PP +\fBcache_life_time\fR: \fItimeout()\fR +.RS 4 +Same as top\-level +\fIcache_life_time\fR +option, but applied to this module only\&. +.RE +.PP +\fBcache_missed\fR: \fItrue | false\fR +.RS 4 +Same as top\-level +\fIcache_missed\fR +option, but applied to this module only\&. +.RE +.PP +\fBcache_size\fR: \fIpos_integer() | infinity\fR +.RS 4 +Same as top\-level +\fIcache_size\fR +option, but applied to this module only\&. +.RE +.PP +\fBdb_type\fR: \fImnesia | sql | ldap\fR +.RS 4 +Same as top\-level +\fIdefault_db\fR +option, but applied to this module only\&. +.RE +.PP +\fBhost\fR +.RS 4 +Deprecated\&. Use +\fIhosts\fR +instead\&. +.RE +.PP +\fBhosts\fR: \fI[Host, \&.\&.\&.]\fR +.RS 4 +This option defines the Jabber IDs of the service\&. If the +\fIhosts\fR +option is not specified, the only Jabber ID will be the hostname of the virtual host with the prefix "vjud\&."\&. The keyword +\fI@HOST@\fR +is replaced with the real virtual host name\&. +.RE +.PP +\fBmatches\fR: \fIpos_integer() | infinity\fR +.RS 4 +With this option, the number of reported search results can be limited\&. If the option\(cqs value is set to +\fIinfinity\fR, all search results are reported\&. The default value is +\fI30\fR\&. +.RE +.PP +\fBname\fR: \fIName\fR +.RS 4 +The value of the service name\&. This name is only visible in some clients that support +XEP\-0030: Service Discovery\&. The default is +\fIvCard User Search\fR\&. +.RE +.PP +\fBsearch\fR: \fItrue | false\fR +.RS 4 +This option specifies whether the search functionality is enabled or not\&. If disabled, the options +\fIhosts\fR, +\fIname\fR +and +\fIvcard\fR +will be ignored and the Jabber User Directory service will not appear in the Service Discovery item list\&. The default value is +\fIfalse\fR\&. +.RE +.PP +\fBuse_cache\fR: \fItrue | false\fR +.RS 4 +Same as top\-level +\fIuse_cache\fR +option, but applied to this module only\&. +.RE +.PP +\fBvcard\fR: \fIvCard\fR +.RS 4 +A custom vCard of the server that will be displayed by some XMPP clients in Service Discovery\&. The value of +\fIvCard\fR +is a YAML map constructed from an XML representation of vCard\&. Since the representation has no attributes, the mapping is straightforward\&. +.sp +\fBExample\fR: +.sp +.if n \{\ +.RS 4 +.\} +.nf +# This XML representation of vCard: +# +# +# Conferences +# +# +# Elm Street +# +# +# +# is translated to: +# +vcard: + fn: Conferences + adr: + \- + work: true + street: Elm Street +.fi +.if n \{\ +.RE +.\} +.RE +.RE +.sp +.it 1 an-trap +.nr an-no-space-flag 1 +.nr an-break-flag 1 +.br +.ps +1 +\fBAvailable options for ldap backend:\fR +.RS 4 +.PP +\fBldap_backups\fR +.RS 4 +Same as top\-level +\fIldap_backups\fR +option, but applied to this module only\&. +.RE +.PP +\fBldap_base\fR +.RS 4 +Same as top\-level +\fIldap_base\fR +option, but applied to this module only\&. +.RE +.PP +\fBldap_deref_aliases\fR +.RS 4 +Same as top\-level +\fIldap_deref_aliases\fR +option, but applied to this module only\&. +.RE +.PP +\fBldap_encrypt\fR +.RS 4 +Same as top\-level +\fIldap_encrypt\fR +option, but applied to this module only\&. +.RE +.PP +\fBldap_filter\fR +.RS 4 +Same as top\-level +\fIldap_filter\fR +option, but applied to this module only\&. +.RE +.PP +\fBldap_password\fR +.RS 4 +Same as top\-level +\fIldap_password\fR +option, but applied to this module only\&. +.RE +.PP +\fBldap_port\fR +.RS 4 +Same as top\-level +\fIldap_port\fR +option, but applied to this module only\&. +.RE +.PP +\fBldap_rootdn\fR +.RS 4 +Same as top\-level +\fIldap_rootdn\fR +option, but applied to this module only\&. +.RE +.PP +\fBldap_search_fields\fR: \fI{Name: Attribute, \&.\&.\&.}\fR +.RS 4 +This option defines the search form and the LDAP attributes to search within\&. +\fIName\fR +is the name of a search form field which will be automatically translated by using the translation files (see +\fImsgs/*\&.msg\fR +for available words)\&. +\fIAttribute\fR +is the LDAP attribute or the pattern +\fI%u\fR\&. +.sp +\fBExamples\fR: +.sp +The default is: +.sp +.if n \{\ +.RS 4 +.\} +.nf +User: "%u" +"Full Name": displayName +"Given Name": givenName +"Middle Name": initials +"Family Name": sn +Nickname: "%u" +Birthday: birthDay +Country: c +City: l +Email: mail +"Organization Name": o +"Organization Unit": ou +.fi +.if n \{\ +.RE +.\} +.RE +.PP +\fBldap_search_reported\fR: \fI{SearchField: VcardField}, \&.\&.\&.}\fR +.RS 4 +This option defines which search fields should be reported\&. +\fISearchField\fR +is the name of a search form field which will be automatically translated by using the translation files (see +\fImsgs/*\&.msg\fR +for available words)\&. +\fIVcardField\fR +is the vCard field name defined in the +\fIldap_vcard_map\fR +option\&. +.sp +\fBExamples\fR: +.sp +The default is: +.sp +.if n \{\ +.RS 4 +.\} +.nf +"Full Name": FN +"Given Name": FIRST +"Middle Name": MIDDLE +"Family Name": LAST +"Nickname": NICKNAME +"Birthday": BDAY +"Country": CTRY +"City": LOCALITY +"Email": EMAIL +"Organization Name": ORGNAME +"Organization Unit": ORGUNIT +.fi +.if n \{\ +.RE +.\} +.RE +.PP +\fBldap_servers\fR +.RS 4 +Same as top\-level +\fIldap_servers\fR +option, but applied to this module only\&. +.RE +.PP +\fBldap_tls_cacertfile\fR +.RS 4 +Same as top\-level +\fIldap_tls_cacertfile\fR +option, but applied to this module only\&. +.RE +.PP +\fBldap_tls_certfile\fR +.RS 4 +Same as top\-level +\fIldap_tls_certfile\fR +option, but applied to this module only\&. +.RE +.PP +\fBldap_tls_depth\fR +.RS 4 +Same as top\-level +\fIldap_tls_depth\fR +option, but applied to this module only\&. +.RE +.PP +\fBldap_tls_verify\fR +.RS 4 +Same as top\-level +\fIldap_tls_verify\fR +option, but applied to this module only\&. +.RE +.PP +\fBldap_uids\fR +.RS 4 +Same as top\-level +\fIldap_uids\fR +option, but applied to this module only\&. +.RE +.PP +\fBldap_vcard_map\fR: \fI{Name: {Pattern, LDAPattributes}, \&.\&.\&.}\fR +.RS 4 +With this option you can set the table that maps LDAP attributes to vCard fields\&. +\fIName\fR +is the type name of the vCard as defined in +RFC 2426\&. +\fIPattern\fR +is a string which contains pattern variables +\fI%u\fR, +\fI%d\fR +or +\fI%s\fR\&. +\fILDAPattributes\fR +is the list containing LDAP attributes\&. The pattern variables +\fI%s\fR +will be sequentially replaced with the values of LDAP attributes from +\fIList_of_LDAP_attributes\fR, +\fI%u\fR +will be replaced with the user part of a JID, and +\fI%d\fR +will be replaced with the domain part of a JID\&. +.sp +\fBExamples\fR: +.sp +The default is: +.sp +.if n \{\ +.RS 4 +.\} +.nf +NICKNAME: {"%u": []} +FN: {"%s": [displayName]} +LAST: {"%s": [sn]} +FIRST: {"%s": [givenName]} +MIDDLE: {"%s": [initials]} +ORGNAME: {"%s": [o]} +ORGUNIT: {"%s": [ou]} +CTRY: {"%s": [c]} +LOCALITY: {"%s": [l]} +STREET: {"%s": [street]} +REGION: {"%s": [st]} +PCODE: {"%s": [postalCode]} +TITLE: {"%s": [title]} +URL: {"%s": [labeleduri]} +DESC: {"%s": [description]} +TEL: {"%s": [telephoneNumber]} +EMAIL: {"%s": [mail]} +BDAY: {"%s": [birthDay]} +ROLE: {"%s": [employeeType]} +PHOTO: {"%s": [jpegPhoto]} +.fi +.if n \{\ +.RE +.\} +.RE +.RE +.sp +.it 1 an-trap +.nr an-no-space-flag 1 +.nr an-break-flag 1 +.br +.ps +1 +\fBAvailable options for mnesia backend:\fR +.RS 4 +.PP +\fBsearch_all_hosts\fR: \fItrue | false\fR +.RS 4 +Whether to perform search on all virtual hosts or not\&. The default value is +\fItrue\fR\&. +.RE +.RE +.SS "mod_vcard_xupdate" +.sp +The user\(cqs client can store an avatar in the user vCard\&. The vCard\-Based Avatars protocol (XEP\-0153) provides a method for clients to inform the contacts what is the avatar hash value\&. However, simple or small clients may not implement that protocol\&. +.sp +If this module is enabled, all the outgoing client presence stanzas get automatically the avatar hash on behalf of the client\&. So, the contacts receive the presence stanzas with the \fIUpdate Data\fR described in XEP\-0153 as if the client would had inserted it itself\&. If the client had already included such element in the presence stanza, it is replaced with the element generated by ejabberd\&. +.sp +By enabling this module, each vCard modification produces a hash recalculation, and each presence sent by a client produces hash retrieval and a presence stanza rewrite\&. For this reason, enabling this module will introduce a computational overhead in servers with clients that change frequently their presence\&. However, the overhead is significantly reduced by the use of caching, so you probably don\(cqt want to set \fIuse_cache\fR to \fIfalse\fR\&. +.sp +The module depends on \fImod_vcard\fR\&. +.if n \{\ +.sp +.\} +.RS 4 +.it 1 an-trap +.nr an-no-space-flag 1 +.nr an-break-flag 1 +.br +.ps +1 +\fBNote\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 for providing backward compatibility\&. +.sp .5v +.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 +\fBcache_life_time\fR: \fItimeout()\fR +.RS 4 +Same as top\-level +\fIcache_life_time\fR +option, but applied to this module only\&. +.RE +.PP +\fBcache_missed\fR: \fItrue | false\fR +.RS 4 +Same as top\-level +\fIcache_missed\fR +option, but applied to this module only\&. +.RE +.PP +\fBcache_size\fR: \fIpos_integer() | infinity\fR +.RS 4 +Same as top\-level +\fIcache_size\fR +option, but applied to this module only\&. +.RE +.PP +\fBuse_cache\fR: \fItrue | false\fR +.RS 4 +Same as top\-level +\fIuse_cache\fR +option, but applied to this module only\&. +.RE +.RE +.SS "mod_version" +.sp +This module implements XEP\-0092: Software Version\&. Consequently, it answers ejabberd\(cqs version when queried\&. +.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 +\fBshow_os\fR: \fItrue | false\fR +.RS 4 +Should the operating system be revealed or not\&. The default value is +\fItrue\fR\&. +.RE +.RE +.SH "LISTENERS" +.sp +This section describes listeners options of ejabberd 25\&.08\&. +.sp +TODO +.SH "AUTHOR" +.sp +ProcessOne\&. +.SH "VERSION" +.sp +This document describes the configuration file of ejabberd 25\&.08\&. 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/25\&.08/ejabberd\&.yml\&.example +.sp +Main site: https://ejabberd\&.im +.sp +Documentation: https://docs\&.ejabberd\&.im +.sp +Configuration Guide: https://docs\&.ejabberd\&.im/admin/configuration +.sp +Source code: https://github\&.com/processone/ejabberd +.SH "COPYING" +.sp +Copyright (c) 2002\-2025 ProcessOne\&. diff --git a/mix.exs b/mix.exs new file mode 100644 index 000000000..fcb3ac39e --- /dev/null +++ b/mix.exs @@ -0,0 +1,437 @@ +defmodule Ejabberd.MixProject do + use Mix.Project + + def project do + [app: :ejabberd, + source_url: "https://github.com/processone/ejabberd", + version: version(), + description: description(), + elixir: elixir_required_version(), + elixirc_paths: ["lib"], + compile_path: ".", + compilers: [:asn1, :yecc] ++ Mix.compilers(), + erlc_options: erlc_options(), + erlc_paths: ["asn1", "src"], + # Elixir tests are starting the part of ejabberd they need + aliases: [test: "test --no-start"], + start_permanent: Mix.env() == :prod, + language: :erlang, + dialyzer: dialyzer(), + releases: releases(), + package: package(), + docs: docs(), + deps: deps()] + end + + def version do + case config(:vsn) do + :false -> "0.0.0" # ./configure wasn't run: vars.config not created + ~c"0.0" -> "0.0.0" # the full git repository wasn't downloaded + ~c"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}.0" + vsn -> String.replace(:erlang.list_to_binary(vsn), ~r/\.0+([0-9])/, ".\\1") + end + end + + def description do + """ + Robust, Ubiquitous and Massively Scalable Messaging Platform (XMPP, MQTT, SIP Server) + """ + end + + def application do + [mod: {:ejabberd_app, []}, + extra_applications: [:inets, :kernel, :sasl, :ssl, :stdlib, :syntax_tools, + :logger, :mix] + ++ cond_apps(), + included_applications: [:mnesia, :os_mon, + :cache_tab, :eimp, :mqtree, :p1_acme, + :p1_oauth2, :pkix] + ++ cond_included_apps()] + end + + defp dialyzer do + [ + plt_add_apps: [ + :mnesia, :odbc, :os_mon, :stdlib, + :eredis, :luerl, + :cache_tab, :eimp, :epam, :esip, :ezlib, :mqtree, + :p1_acme, :p1_mysql, :p1_oauth2, :p1_pgsql, :pkix, + :sqlite3, :stun, :xmpp], + ] + end + + defp if_version_above(ver, okResult) do + if :erlang.system_info(:otp_release) > ver do + okResult + else + [] + end + end + + defp if_version_below(ver, okResult) do + if :erlang.system_info(:otp_release) < ver do + okResult + else + [] + end + end + + defp erlc_options do + # Use our own includes + includes from all dependencies + includes = ["include", deps_include()] + result = [{:d, :ELIXIR_ENABLED}] ++ + cond_options() ++ + Enum.map(includes, fn (path) -> {:i, path} end) ++ + if_version_above(~c"20", [{:d, :HAVE_URI_STRING}]) ++ + if_version_above(~c"20", [{:d, :HAVE_ERL_ERROR}]) ++ + if_version_below(~c"21", [{:d, :USE_OLD_HTTP_URI}]) ++ + if_version_below(~c"22", [{:d, :LAGER}]) ++ + if_version_below(~c"21", [{:d, :NO_CUSTOMIZE_HOSTNAME_CHECK}]) ++ + if_version_below(~c"23", [{:d, :USE_OLD_CRYPTO_HMAC}]) ++ + if_version_below(~c"23", [{:d, :USE_OLD_PG2}]) ++ + if_version_below(~c"24", [{:d, :COMPILER_REPORTS_ONLY_LINES}]) ++ + if_version_below(~c"24", [{:d, :SYSTOOLS_APP_DEF_WITHOUT_OPTIONAL}]) ++ + if_version_below(~c"24", [{:d, :OTP_BELOW_24}]) ++ + if_version_below(~c"25", [{:d, :OTP_BELOW_25}]) ++ + if_version_below(~c"26", [{:d, :OTP_BELOW_26}]) ++ + if_version_below(~c"27", [{:d, :OTP_BELOW_27}]) ++ + if_version_below(~c"28", [{:d, :OTP_BELOW_28}]) + defines = for {:d, value} <- result, do: {:d, value} + result ++ [{:d, :ALL_DEFS, defines}] + end + + defp cond_options do + for {:true, option} <- [{config(:sip), {:d, :SIP}}, + {config(:stun), {:d, :STUN}}, + {config(:debug), :debug_info}, + {not config(:debug), {:debug_info, false}}, + {config(:roster_gateway_workaround), {:d, :ROSTER_GATEWAY_WORKAROUND}}, + {config(:new_sql_schema), {:d, :NEW_SQL_SCHEMA}} + ], do: + option + end + + defp deps do + [{:cache_tab, "~> 1.0"}, + {:dialyxir, "~> 1.2", only: [:test], runtime: false}, + {:eimp, "~> 1.0"}, + {:ex_doc, "~> 0.31", only: [:edoc], runtime: false}, + {:fast_tls, "~> 1.1.24"}, + {:fast_xml, "~> 1.1.56"}, + {:fast_yaml, "~> 1.0"}, + {:idna, "~> 6.0"}, + {:mqtree, "~> 1.0"}, + {:p1_acme, ">= 1.0.28"}, + {:p1_oauth2, "~> 0.6"}, + {:p1_utils, "~> 1.0"}, + {:pkix, "~> 1.0"}, + {:stringprep, ">= 1.0.26"}, + {:xmpp, git: "https://github.com/processone/xmpp", ref: "e9d901ea84fd3910ad32b715853397eb1155b41c", override: true}, + {:yconf, git: "https://github.com/processone/yconf", ref: "95692795a8a8d950ba560e5b07e6b80660557259", override: true}] + ++ cond_deps() + end + + defp deps_include() do + if Mix.Project.umbrella?() do + "../../deps" + else + case Mix.Project.deps_paths()[:ejabberd] do + nil -> "deps" + _ -> ".." + end + end + end + + defp cond_deps do + for {:true, dep} <- [{config(:pam), {:epam, "~> 1.0"}}, + {Mix.env() == :translations, + {:ejabberd_po, git: "https://github.com/processone/ejabberd-po.git"}}, + {Mix.env() == :dev, + {:exsync, "~> 0.2", optional: true, runtime: false}}, + {config(:redis), {:eredis, "~> 1.7.1"}}, + {config(:sip), {:esip, "~> 1.0"}}, + {config(:zlib), {:ezlib, "~> 1.0"}}, + {if_version_above(~c"23", true), {:jose, "~> 1.11.10"}}, + {if_version_below(~c"24", true), {:jose, "1.11.1", override: true}}, + {if_version_below(~c"27", true), {:jiffy, "~> 1.1.1"}}, + {if_version_below(~c"22", true), {:lager, "~> 3.9.1"}}, + {config(:lua), {:luerl, "~> 1.2.0"}}, + {config(:mysql), {:p1_mysql, ">= 1.0.24"}}, + {config(:pgsql), {:p1_pgsql, ">= 1.1.32"}}, + {config(:sqlite), {:sqlite3, "~> 1.1"}}, + {config(:stun), {:stun, "~> 1.0"}}], do: + dep + end + + defp cond_apps do + for {:true, app} <- [{config(:stun), :stun}, + {if_version_below(~c"27", true), :jiffy}, + {config(:tools), :debugger}, + {config(:tools), :observer}, + {config(:tools), :wx}], do: + app + end + + defp cond_included_apps do + for {:true, app} <- [{config(:pam), :epam}, + {config(:lua), :luerl}, + {config(:redis), :eredis}, + {Mix.env() == :edoc, :ex_doc}, + {Mix.env() == :test, :dialyxir}, + {if_version_below(~c"22", true), :lager}, + {config(:mysql), :p1_mysql}, + {config(:sip), :esip}, + {config(:odbc), :odbc}, + {config(:pgsql), :p1_pgsql}, + {config(:sqlite), :sqlite3}], do: + app + end + + defp package do + [# These are the default files included in the package + files: ["include", "lib", "priv", "sql", "src", + "COPYING", "README.md", + "ejabberd.yml.example", "config/runtime.exs", + "mix.exs", "rebar.config", "rebar.config.script", "vars.config"], + maintainers: ["ProcessOne"], + licenses: ["GPL-2.0-or-later"], + links: %{"ejabberd.im" => "https://www.ejabberd.im", + "ejabberd Docs" => "https://docs.ejabberd.im", + "GitHub" => "https://github.com/processone/ejabberd", + "ProcessOne" => "https://www.process-one.net/"}] + end + + defp vars do + filepath = case Application.fetch_env(:ejabberd, :vars_config_path) do + :error -> + "vars.config" + {:ok, path} -> + path + end + config2 = case :file.consult(filepath) do + {:ok,config} -> config + _ -> [stun: true, zlib: true] + end + case Mix.env() do + :dev -> List.keystore(config2, :tools, 0, {:tools, true}) + _ -> config2 + end + end + + defp config(key) do + case vars()[key] do + nil -> false + value -> value + end + end + + defp elixir_required_version do + case {Map.get(System.get_env(), "RELIVE", "false"), + MapSet.member?(MapSet.new(System.argv()), "release")} + do + {"true", _} -> + case Version.match?(System.version(), "~> 1.11") do + false -> + IO.puts("ERROR: To use 'make relive', Elixir 1.11.0 or higher is required.") + _ -> :ok + end + "~> 1.11" + {_, true} -> + case Version.match?(System.version(), "~> 1.10") do + false -> + IO.puts("ERROR: To build releases, Elixir 1.10.0 or higher is required.") + _ -> :ok + end + case Version.match?(System.version(), "< 1.11.4") + and :erlang.system_info(:otp_release) > ~c"23" do + true -> + IO.puts("ERROR: To build releases with Elixir lower than 1.11.4, Erlang/OTP lower than 24 is required.") + _ -> :ok + end + "~> 1.10" + _ -> + "~> 1.4" + end + end + + defp releases do + maybe_tar = case Mix.env() do + :prod -> [:tar] + _ -> [] + end + [ + ejabberd: [ + include_executables_for: [:unix], + # applications: [runtime_tools: :permanent] + strip_beams: Mix.env() != :dev, + steps: [©_extra_files/1, :assemble | maybe_tar] + ] + ] + end + + defp copy_extra_files(release) do + assigns = [ + version: version(), + rootdir: config(:rootdir), + installuser: config(:installuser), + libdir: config(:libdir), + sysconfdir: config(:sysconfdir), + localstatedir: config(:localstatedir), + config_dir: config(:config_dir), + logs_dir: config(:logs_dir), + spool_dir: config(:spool_dir), + vsn: version(), + iexpath: config(:iexpath), + erl: config(:erl), + 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" + File.rm_rf(ro) + + # Elixir lower than 1.12.0 don't have System.shell + execute = fn(command) -> + case function_exported?(System, :shell, 1) do + true -> + System.shell(command, into: IO.stream()) + false -> + :os.cmd(to_charlist(command)) + end + end + + # Mix/Elixir lower than 1.11.0 use config/releases.exs instead of runtime.exs + case Version.match?(System.version(), "~> 1.11") do + true -> + :ok + false -> + execute.("cp config/runtime.exs config/releases.exs") + end + + 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.example2a") + Mix.Generator.copy_template("ejabberdctl.example2a", "ejabberdctl.example2b", assigns) + execute.("sed -e 's|{{\\(\[_a-z\]*\\)}}|<%= @\\1 %>|g' ejabberdctl.example2b > ejabberdctl.example4") + execute.("sed -e 's|^ERLANG_OPTS=\"|ERLANG_OPTS=\"-boot ../releases/#{release.version}/start_clean -boot_var RELEASE_LIB ../lib |' ejabberdctl.example4 > ejabberdctl.example5") + execute.("sed -e 's|^INSTALLUSER=|ERL_OPTIONS=\"-setcookie \\$\\(cat \"\\${SCRIPT_DIR%/*}/releases/COOKIE\")\"\\nINSTALLUSER=|g' ejabberdctl.example5 > ejabberdctl.example6") + Mix.Generator.copy_template("ejabberdctl.example6", "#{ro}/bin/ejabberdctl", assigns) + File.chmod("#{ro}/bin/ejabberdctl", 0o755) + + 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") + File.rm("ejabberdctl.example6") + + suffix = case Mix.env() do + :dev -> + Mix.Generator.copy_file("test/ejabberd_SUITE_data/ca.pem", "#{ro}/conf/ca.pem") + Mix.Generator.copy_file("test/ejabberd_SUITE_data/cert.pem", "#{ro}/conf/cert.pem") + ".example" + _ -> "" + end + + Mix.Generator.copy_file("ejabberd.yml.example", "#{ro}/conf/ejabberd.yml#{suffix}") + Mix.Generator.copy_file("ejabberdctl.cfg.example", "#{ro}/conf/ejabberdctl.cfg#{suffix}") + Mix.Generator.copy_file("inetrc", "#{ro}/conf/inetrc") + + Enum.each(File.ls!("sql"), + fn x -> + Mix.Generator.copy_file("sql/#{x}", "#{ro}/lib/ejabberd-#{release.version}/priv/sql/#{x}") + end) + + File.cp_r!("include", "#{ro}/lib/ejabberd-#{release.version}/include") + for {name, details} <- Map.to_list(release.applications) do + {_, is_otp_app} = List.keyfind(details, :otp_app?, 0) + {_, vsn} = List.keyfind(details, :vsn, 0) + {_, path} = List.keyfind(details, :path, 0) + source_dir = case is_otp_app do + :true -> "#{path}/include" + :false -> "deps/#{name}/include" + end + target_dir = "#{ro}/lib/#{name}-#{vsn}/include" + File.exists?(source_dir) + && File.mkdir_p(target_dir) + && File.cp_r!(source_dir, target_dir) + end + + case Mix.env() do + :dev -> execute.("REL_DIR_TEMP=$PWD/rel/overlays/ rel/setup-dev.sh mix") + _ -> :ok + end + + release + end + + defp docs do + [ + main: "readme", + logo: "_build/edoc/logo.png", + source_ref: "master", + extra_section: "", # No need for Pages section name, it's the only one + api_reference: false, # API section has just Elixir, hide it + filter_modules: "aaaaa", # Module section has just Elixir modules, hide them + extras: [ + "README.md": [title: "Readme"], + "COMPILE.md": [title: "Compile and Install"], + "CONTAINER.md": [title: "Container Image"], + "CONTRIBUTING.md": [title: "Contributing"], + "CONTRIBUTORS.md": [title: "Contributors"], + "CODE_OF_CONDUCT.md": [title: "Code of Conduct"], + "CHANGELOG.md": [title: "ChangeLog"], + "COPYING": [title: "Copying License"], + "_build/edoc/docs.md": [title: "⟹ ejabberd Docs"] + ], + groups_for_extras: [ + "": Path.wildcard("*.md") ++ ["COPYING"], + "For more documentation": "_build/edoc/docs.md" + ] + ] + end +end + +defmodule Mix.Tasks.Compile.Asn1 do + use Mix.Task + alias Mix.Compilers.Erlang + + @recursive true + @manifest ".compile.asn1" + + def run(args) do + {opts, _, _} = OptionParser.parse(args, switches: [force: :boolean]) + + project = Mix.Project.config() + source_paths = project[:asn1_paths] || ["asn1"] + dest_paths = project[:asn1_target] || ["src"] + mappings = Enum.zip(source_paths, dest_paths) + options = project[:asn1_options] || [] + + force = case opts[:force] do + :true -> [force: true] + _ -> [force: false] + end + + Erlang.compile(manifest(), mappings, :asn1, :erl, force, fn + input, output -> + options = options ++ [:noobj, outdir: Erlang.to_erl_file(Path.dirname(output))] + case :asn1ct.compile(Erlang.to_erl_file(input), options) do + :ok -> {:ok, :done} + error -> error + end + end) + end + + def manifests, do: [manifest()] + defp manifest, do: Path.join(Mix.Project.manifest_path(), @manifest) + + def clean, do: Erlang.clean(manifest()) +end diff --git a/mix.lock b/mix.lock new file mode 100644 index 000000000..3eb53bce9 --- /dev/null +++ b/mix.lock @@ -0,0 +1,39 @@ +%{ + "base64url": {:hex, :base64url, "1.0.1", "f8c7f2da04ca9a5d0f5f50258f055e1d699f0e8bf4cfdb30b750865368403cf6", [:rebar3], [], "hexpm", "f9b3add4731a02a9b0410398b475b33e7566a695365237a6bdee1bb447719f5c"}, + "cache_tab": {:hex, :cache_tab, "1.0.33", "e2542afb34f17ee3ca19d2b0f546a074922c2b99fb6b2acfb38160d7d0336ec3", [:rebar3], [{:p1_utils, "1.0.28", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "4258009eb050b22aabe0c848e230bba58401a6895c58c2ff74dfb635e3c35900"}, + "dialyxir": {:hex, :dialyxir, "1.4.6", "7cca478334bf8307e968664343cbdb432ee95b4b68a9cba95bdabb0ad5bdfd9a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "8cf5615c5cd4c2da6c501faae642839c8405b49f8aa057ad4ae401cb808ef64d"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, + "eimp": {:hex, :eimp, "1.0.26", "c0b05f32e35629c4d9bcfb832ff879a92b0f92b19844bc7835e0a45635f2899a", [:rebar3], [{:p1_utils, "~> 1.0.25", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "d96d4e8572b9dfc40f271e47f0cb1d8849373bc98a21223268781765ed52044c"}, + "epam": {:hex, :epam, "1.0.14", "aa0b85d27f4ef3a756ae995179df952a0721237e83c6b79d644347b75016681a", [:rebar3], [], "hexpm", "2f3449e72885a72a6c2a843f561add0fc2f70d7a21f61456930a547473d4d989"}, + "eredis": {:hex, :eredis, "1.7.1", "39e31aa02adcd651c657f39aafd4d31a9b2f63c6c700dc9cece98d4bc3c897ab", [:mix, :rebar3], [], "hexpm", "7c2b54c566fed55feef3341ca79b0100a6348fd3f162184b7ed5118d258c3cc1"}, + "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, + "esip": {:hex, :esip, "1.0.59", "eb202f8c62928193588091dfedbc545fe3274c34ecd209961f86dcb6c9ebce88", [:rebar3], [{:fast_tls, "1.1.25", [hex: :fast_tls, repo: "hexpm", optional: false]}, {:p1_utils, "1.0.28", [hex: :p1_utils, repo: "hexpm", optional: false]}, {:stun, "1.2.21", [hex: :stun, repo: "hexpm", optional: false]}], "hexpm", "0bdf2e3c349dc0b144f173150329e675c6a51ac473d7a0b2e362245faad3fbe6"}, + "ex_doc": {:hex, :ex_doc, "0.38.3", "ddafe36b8e9fe101c093620879f6604f6254861a95133022101c08e75e6c759a", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "ecaa785456a67f63b4e7d7f200e8832fa108279e7eb73fd9928e7e66215a01f9"}, + "exsync": {:hex, :exsync, "0.4.1", "0a14fe4bfcb80a509d8a0856be3dd070fffe619b9ba90fec13c58b316c176594", [:mix], [{:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "cefb22aa805ec97ffc5b75a4e1dc54bcaf781e8b32564bf74abbe5803d1b5178"}, + "ezlib": {:hex, :ezlib, "1.0.15", "d74f5df191784744726a5b1ae9062522c606334f11086363385eb3b772d91357", [:rebar3], [{:p1_utils, "1.0.28", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "dd14ba6c12521af5cfe6923e73e3d545f4a0897dc66bfab5287fbb7ae3962eab"}, + "fast_tls": {:hex, :fast_tls, "1.1.25", "da8ed6f05a2452121b087158b17234749f36704c1f2b74dc51db99a1e27ed5e8", [:rebar3], [{:p1_utils, "~> 1.0.26", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "59e183b5740e670e02b8aa6be673b5e7779e5fe5bfcc679fe2d4993d1949a821"}, + "fast_xml": {:hex, :fast_xml, "1.1.57", "31efc0f9bceda92069704f7a25830407da5dc3dad1272b810d6f2e13e73cc11a", [:rebar3], [{:p1_utils, "1.0.28", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "eec34e90adacafe467d5ddab635a014ded73b98b4061554b2d1972173d929c39"}, + "fast_yaml": {:hex, :fast_yaml, "1.0.39", "2e71168091949bab0e5f583b340a99072b4d22d93eb86624e7850a12b1517be4", [:rebar3], [{:p1_utils, "1.0.28", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "24c7b9ab9e2b9269d64e45f4a2a1280966adb17d31e63365cfd3ee277fb0a78d"}, + "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, + "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, + "jiffy": {:hex, :jiffy, "1.1.2", "a9b6c9a7ec268e7cf493d028f0a4c9144f59ccb878b1afe42841597800840a1b", [:rebar3], [], "hexpm", "bb61bc42a720bbd33cb09a410e48bb79a61012c74cb8b3e75f26d988485cf381"}, + "jose": {:hex, :jose, "1.11.10", "a903f5227417bd2a08c8a00a0cbcc458118be84480955e8d251297a425723f83", [:mix, :rebar3], [], "hexpm", "0d6cd36ff8ba174db29148fc112b5842186b68a90ce9fc2b3ec3afe76593e614"}, + "luerl": {:hex, :luerl, "1.2.3", "df25f41944e57a7c4d9ef09d238bc3e850276c46039cfc12b8bb42eccf36fcb1", [:rebar3], [], "hexpm", "1b4b9d0ca5d7d280d1d2787a6a5ee9f5a212641b62bff91556baa53805df3aed"}, + "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, + "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, + "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, + "mqtree": {:hex, :mqtree, "1.0.19", "d769c25f898810725fc7db0dbffe5f72098647048b1be2e6d772f1c2f31d8476", [:rebar3], [{:p1_utils, "1.0.28", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "c81065715c49a1882812f80a5ae2d842e80dd3f2d130530df35990248bf8ce3c"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, + "p1_acme": {:hex, :p1_acme, "1.0.28", "64d9c17f5412aa92d75b29206b2b984d734a4fe1b7eacb66c3d7a7c697ac612c", [:rebar3], [{:base64url, "~> 1.0", [hex: :base64url, repo: "hexpm", optional: false]}, {:idna, "~> 6.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jiffy, "~> 1.1.1", [hex: :jiffy, repo: "hexpm", optional: false]}, {:jose, "~> 1.11.10", [hex: :jose, repo: "hexpm", optional: false]}, {:yconf, "~> 1.0.17", [hex: :yconf, repo: "hexpm", optional: false]}], "hexpm", "ce686986de3f9d5fd285afe87523cb45329a349c6c6be7acc1ed916725d46423"}, + "p1_mysql": {:hex, :p1_mysql, "1.0.26", "574d07c9936c53b1ec3556db3cf064cc14a6c39039835b3d940471bfa5ac8e2b", [:rebar3], [], "hexpm", "ea138083f2c54719b9cf549dbf5802a288b0019ea3e5449b354c74cc03fafdec"}, + "p1_oauth2": {:hex, :p1_oauth2, "0.6.14", "1c5f82535574de87e2059695ac4b91f8f9aebacbc1c80287dae6f02552d47aea", [:rebar3], [], "hexpm", "1fd3ac474e43722d9d5a87c6df8d36f698ed87af7bb81cbbb66361451d99ae8f"}, + "p1_pgsql": {:hex, :p1_pgsql, "1.1.35", "e13d89f14d717553e85c88a152ce77461916b013d88fcb851e354a0b332d4218", [:rebar3], [{:xmpp, "~> 1.11.0", [hex: :xmpp, repo: "hexpm", optional: false]}], "hexpm", "e99594446c411c660696795b062336f5c4bd800451d8f620bb4d4ce304e255c2"}, + "p1_utils": {:hex, :p1_utils, "1.0.28", "9a7088a98d788b4c4880fd3c82d0c135650db13f2e4ef7e10db179791bc94d59", [:rebar3], [], "hexpm", "c49bd44bc4a40ad996691af826dd7e0aa56d4d0cd730817190a1f84d1a7f0033"}, + "pkix": {:hex, :pkix, "1.0.10", "d3bfadf7b7cfe2a3636f1b256c9cce5f646a07ce31e57ee527668502850765a0", [:rebar3], [], "hexpm", "e02164f83094cb124c41b1ab28988a615d54b9adc38575f00f19a597a3ac5d0e"}, + "sqlite3": {:hex, :sqlite3, "1.1.15", "e819defd280145c328457d7af897d2e45e8e5270e18812ee30b607c99cdd21af", [:rebar3], [], "hexpm", "3c0ba4e13322c2ad49de4e2ddd28311366adde54beae8dba9d9e3888f69d2857"}, + "stringprep": {:hex, :stringprep, "1.0.33", "22f42866b4f6f3c238ea2b9cb6241791184ddedbab55e94a025511f46325f3ca", [:rebar3], [{:p1_utils, "1.0.28", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "96f8b30bc50887f605b33b46bca1d248c19a879319b8c482790e3b4da5da98c0"}, + "stun": {:hex, :stun, "1.2.21", "735855314ad22cb7816b88597d2f5ca22e24aa5e4d6010a0ef3affb33ceed6a5", [:rebar3], [{:fast_tls, "1.1.25", [hex: :fast_tls, repo: "hexpm", optional: false]}, {:p1_utils, "1.0.28", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "3d7fe8efb9d05b240a6aa9a6bf8b8b7bff2d802895d170443c588987dc1e12d9"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"}, + "xmpp": {:git, "https://github.com/processone/xmpp", "e9d901ea84fd3910ad32b715853397eb1155b41c", [ref: "e9d901ea84fd3910ad32b715853397eb1155b41c"]}, + "yconf": {:git, "https://github.com/processone/yconf", "95692795a8a8d950ba560e5b07e6b80660557259", [ref: "95692795a8a8d950ba560e5b07e6b80660557259"]}, +} diff --git a/plugins/configure_deps.erl b/plugins/configure_deps.erl new file mode 100644 index 000000000..181da0b02 --- /dev/null +++ b/plugins/configure_deps.erl @@ -0,0 +1,5 @@ +-module(configure_deps). +-export(['configure-deps'/2]). + +'configure-deps'(Config, Vals) -> + {ok, Config}. diff --git a/plugins/deps_erl_opts.erl b/plugins/deps_erl_opts.erl new file mode 100644 index 000000000..725802664 --- /dev/null +++ b/plugins/deps_erl_opts.erl @@ -0,0 +1,12 @@ +-module(deps_erl_opts). +-export([preprocess/2]). + +preprocess(Config, Dirs) -> + ExtraOpts = rebar_config:get(Config, deps_erl_opts, []), + Opts = rebar_config:get(Config, erl_opts, []), + NewOpts = lists:foldl(fun(Opt, Acc) when is_tuple(Opt) -> + lists:keystore(element(1, Opt), 1, Acc, Opt); + (Opt, Acc) -> + [Opt | lists:delete(Opt, Acc)] + end, Opts, ExtraOpts), + {ok, rebar_config:set(Config, erl_opts, NewOpts), []}. diff --git a/plugins/override_deps_versions2.erl b/plugins/override_deps_versions2.erl new file mode 100644 index 000000000..de08b5482 --- /dev/null +++ b/plugins/override_deps_versions2.erl @@ -0,0 +1,126 @@ +-module(override_deps_versions2). +-export([preprocess/2, 'pre_update-deps'/2, new_replace/1, new_replace/0]). + +preprocess(Config, _Dirs) -> + update_deps(Config). + +update_deps(Config) -> + LocalDeps = rebar_config:get_local(Config, deps, []), + TopDeps = case rebar_config:get_xconf(Config, top_deps, []) of + [] -> LocalDeps; + Val -> Val + end, + Config2 = rebar_config:set_xconf(Config, top_deps, TopDeps), + NewDeps = lists:map(fun({Name, _, _} = Dep) -> + case lists:keyfind(Name, 1, TopDeps) of + false -> Dep; + TopDep -> TopDep + end + end, LocalDeps), + %io:format("LD ~p~n", [LocalDeps]), + %io:format("TD ~p~n", [TopDeps]), + + Config3 = rebar_config:set(Config2, deps, NewDeps), + {ok, Config3, []}. + + +'pre_update-deps'(Config, _Dirs) -> + {ok, Config2, _} = update_deps(Config), + + case code:is_loaded(old_rebar_config) of + false -> + {_, Beam, _} = code:get_object_code(rebar_config), + NBeam = rename(Beam, old_rebar_config), + code:load_binary(old_rebar_config, "blank", NBeam), + replace_mod(Beam); + _ -> + ok + end, + {ok, Config2}. + +new_replace() -> + old_rebar_config:new(). +new_replace(Config) -> + NC = old_rebar_config:new(Config), + {ok, Conf, _} = update_deps(NC), + Conf. + +replace_mod(Beam) -> + {ok, {_, [{exports, Exports}]}} = beam_lib:chunks(Beam, [exports]), + Funcs = lists:filtermap( + fun({module_info, _}) -> + false; + ({Name, Arity}) -> + Args = args(Arity), + Call = case Name of + new -> + [erl_syntax:application( + erl_syntax:abstract(override_deps_versions2), + erl_syntax:abstract(new_replace), + Args)]; + _ -> + [erl_syntax:application( + erl_syntax:abstract(old_rebar_config), + erl_syntax:abstract(Name), + Args)] + end, + {true, erl_syntax:function(erl_syntax:abstract(Name), + [erl_syntax:clause(Args, none, + Call)])} + end, Exports), + Forms0 = ([erl_syntax:attribute(erl_syntax:abstract(module), + [erl_syntax:abstract(rebar_config)])] + ++ Funcs), + Forms = [erl_syntax:revert(Form) || Form <- Forms0], + %io:format("--------------------------------------------------~n" + % "~s~n", + % [[erl_pp:form(Form) || Form <- Forms]]), + {ok, Mod, Bin} = compile:forms(Forms, [report, export_all]), + code:purge(rebar_config), + {module, Mod} = code:load_binary(rebar_config, "mock", Bin). + + +args(0) -> + []; +args(N) -> + [arg(N) | args(N-1)]. + +arg(N) -> + erl_syntax:variable(list_to_atom("A"++integer_to_list(N))). + +rename(BeamBin0, Name) -> + BeamBin = replace_in_atab(BeamBin0, Name), + update_form_size(BeamBin). + +%% Replace the first atom of the atom table with the new name +replace_in_atab(<<"Atom", CnkSz0:32, Cnk:CnkSz0/binary, Rest/binary>>, Name) -> + replace_first_atom(<<"Atom">>, Cnk, CnkSz0, Rest, latin1, Name); +replace_in_atab(<<"AtU8", CnkSz0:32, Cnk:CnkSz0/binary, Rest/binary>>, Name) -> + replace_first_atom(<<"AtU8">>, Cnk, CnkSz0, Rest, unicode, Name); +replace_in_atab(<>, Name) -> + <>. + +replace_first_atom(CnkName, Cnk, CnkSz0, Rest, Encoding, Name) -> + <> = Cnk, + NumPad0 = num_pad_bytes(CnkSz0), + <<_:NumPad0/unit:8, NextCnks/binary>> = Rest, + NameBin = atom_to_binary(Name, Encoding), + NameSz = byte_size(NameBin), + CnkSz = CnkSz0 + NameSz - NameSz0, + NumPad = num_pad_bytes(CnkSz), + <>. + + +%% Calculate the number of padding bytes that have to be added for the +%% BinSize to be an even multiple of ?beam_num_bytes_alignment. +num_pad_bytes(BinSize) -> + case 4 - (BinSize rem 4) of + 4 -> 0; + N -> N + end. + +%% Update the size within the top-level form +update_form_size(<<"FOR1", _OldSz:32, Rest/binary>> = Bin) -> + Sz = size(Bin) - 8, +<<"FOR1", Sz:32, Rest/binary>>. diff --git a/plugins/override_opts.erl b/plugins/override_opts.erl new file mode 100644 index 000000000..818f53e87 --- /dev/null +++ b/plugins/override_opts.erl @@ -0,0 +1,43 @@ +-module(override_opts). +-export([preprocess/2]). + +override_opts(override, Config, Opts) -> + lists:foldl(fun({Opt, Value}, Conf) -> + rebar_config:set(Conf, Opt, Value) + end, Config, Opts); +override_opts(add, Config, Opts) -> + lists:foldl(fun({Opt, Value}, Conf) -> + V = rebar_config:get_local(Conf, Opt, []), + rebar_config:set(Conf, Opt, V ++ Value) + end, Config, Opts); +override_opts(del, Config, Opts) -> + lists:foldl(fun({Opt, Value}, Conf) -> + V = rebar_config:get_local(Conf, Opt, []), + rebar_config:set(Conf, Opt, V -- Value) + end, Config, Opts). + +preprocess(Config, _Dirs) -> + Overrides = rebar_config:get_local(Config, overrides, []), + TopOverrides = case rebar_config:get_xconf(Config, top_overrides, []) of + [] -> Overrides; + Val -> Val + end, + Config2 = rebar_config:set_xconf(Config, top_overrides, TopOverrides), + try + Config3 = case rebar_app_utils:load_app_file(Config2, _Dirs) of + {ok, C, AppName, _AppData} -> + lists:foldl(fun({Type, AppName2, Opts}, Conf1) when + AppName2 == AppName -> + override_opts(Type, Conf1, Opts); + ({Type, Opts}, Conf1a) -> + override_opts(Type, Conf1a, Opts); + (_, Conf2) -> + Conf2 + end, C, TopOverrides); + _ -> + Config2 + end, + {ok, Config3, []} + catch + error:badarg -> {ok, Config2, []} + end. diff --git a/priv/css/admin.css b/priv/css/admin.css new file mode 100644 index 000000000..12fa97e22 --- /dev/null +++ b/priv/css/admin.css @@ -0,0 +1,322 @@ +html,body { + margin: 0; + padding: 0; + height: 100%; + background: #f9f9f9; + font-family: sans-serif; +} +body { + min-width: 990px; +} +a { + text-decoration: none; + color: #3eaffa; +} +a:hover, +a:active { + text-decoration: underline; +} +#container { + position: relative; + padding: 0; + margin: 0 auto; + max-width: 1280px; + min-height: 100%; + height: 100%; + margin-bottom: -30px; + z-index: 1; +} +html>body #container { + height: auto; +} +#header h1 { + width: 100%; + height: 50px; + padding: 0; + margin: 0; + background-color: #49cbc1; +} +#header h1 a { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 50px; + padding: 0; + margin: 0; + background: url('@BASE@logo.png') 10px center no-repeat transparent; + background-size: auto 25px; + display: block; + text-indent: -9999px; +} +#clearcopyright { + display: block; + width: 100%; + height: 30px; +} +#copyrightouter { + position: relative; + display: table; + width: 100%; + height: 30px; + z-index: 2; +} +#copyright { + display: table-cell; + vertical-align: bottom; + width: 100%; + height: 30px; +} +#copyright a { + font-weight: bold; + color: #fff; +} +#copyright p { + margin-left: 0; + margin-right: 0; + margin-top: 5px; + margin-bottom: 0; + padding-left: 0; + padding-right: 0; + padding-top: 5px; + padding-bottom: 5px; + width: 100%; + color: #fff; + background-color: #30353E; + font-size: 0.75em; + text-align: center; +} +#navigation { + display: inline-block; + vertical-align: top; + width: 30%; +} +#navigation ul { + padding: 0; + margin: 0; + width: 90%; + background: #fff; +} +#navigation ul li { + list-style: none; + margin: 0; + + border-bottom: 1px solid #f9f9f9; + text-align: left; +} +#navigation ul li a { + margin: 0; + display: inline-block; + padding: 10px; + color: #333; +} +ul li #navhead a, ul li #navheadsub a, ul li #navheadsubsub a { + font-size: 1.5em; + color: inherit; +} +#navitemsub { + border-left: 0.5em solid #424a55; +} +#navitemsubsub { + border-left: 2em solid #424a55; +} +#navheadsub, +#navheadsubsub { + padding-left: 0.5em; +} +#navhead, +#navheadsub, +#navheadsubsub { + border-top: 3px solid #49cbc1; + background: #424a55; + color: #fff; +} + +#navitemlogin-start { + border-top: 0.2em solid #cae7e4; +} +#navitemlogin { + padding: 0.5em; + border-bottom: 0.2em solid #cae7e4; + padding-left: 0.5em; +} +#welcome { + padding: 2em; + border-top: 0.2em solid #cae7e4; + border-bottom: 0.2em solid #cae7e4; + background-color: #f4f9f9; +} +#lastactivity li { + padding: 2px; + margin-bottom: -1px; +} +thead tr td { + background: #cae7e4; +} +td.copy { + text-align: center; +} +tr.head { + color: #fff; + background-color: #3b547a; + text-align: center; +} +tr.oddraw { + color: #412c75; + background-color: #ccd4df; + text-align: center; +} +tr.evenraw { + color: #412c75; + background-color: #dbe0e8; + text-align: center; +} +td.leftheader { + color: #412c75; + background-color: #ccccc1; + padding-left: 5px; + padding-top: 2px; + padding-bottom: 2px; + margin-top: 0px; + margin-bottom: 0px; +} +td.leftcontent { + color: #000044; + background-color: #e6e6df; + padding-left: 5px; + padding-right: 5px; + padding-top: 2px; + padding-bottom: 2px; + margin-top: 0px; + margin-bottom: 0px; +} +td.rightcontent { + color: #000044; + text-align: justify; + padding-left: 10px; + padding-right: 10px; + padding-bottom: 5px; +} + +h1 { + color: #000044; + padding-top: 2px; + padding-bottom: 2px; + margin-top: 0px; + margin-bottom: 0px; +} +h2 { + color: #000044; + text-align: center; + padding-top: 2px; + padding-bottom: 2px; + margin-top: 0px; + margin-bottom: 0px; +} +h3 { + color: #000044; + text-align: left; + padding-top: 20px; + padding-bottom: 2px; + margin-top: 0px; + margin-bottom: 0px; +} +#content ul { + padding-left: 1.1em; + margin-top: 1em; +} +#content ul li { + list-style-type: disc; + padding: 5px; +} +#content ul.nolistyle>li { + list-style-type: none; +} +#content { + display: inline-block; + vertical-align: top; + padding-top: 25px; + width: 70%; +} +div.anchorlink { + display: inline-block; + float: right; + margin-top: 1em; + margin-right: 1em; +} +div.anchorlink a { + padding: 3px; + background: #cae7e4; + font-size: 0.75em; + color: black; +} +div.guidelink, +p[dir=ltr] { + display: inline-block; + float: right; + margin-top: 1em; + margin-right: 1em; +} +div.guidelink a, +p[dir=ltr] a { + padding: 3px; + background: #3eaffa; + font-size: 0.75em; + color: #fff; + border-radius: 2px; +} +table { + margin-top: 1em; +} +table tr td { + padding: 0.5em; +} +table tr:nth-child(odd) { + background: #fff; +} +table.withtextareas>tbody>tr>td { + vertical-align: top; +} +textarea { + margin-bottom: 1em; +} +input, +select { + font-size: 1em; +} +.result { + border: 1px; + border-style: dashed; + border-color: #FE8A02; + padding: 1em; + margin-right: 1em; + background: #FFE3C9; +} +*.alignright { + text-align: right; +} +.btn-danger:hover { + color: #fff; + background-color: #cb2431; +} +.btn-danger { + color: #cb2431; + transition: none; +} +h3.api { + border-bottom: 1px solid #b6b6b6; +} +details > summary { + background-color: #dbeceb; + border: none; + cursor: pointer; + list-style: none; + padding: 8px; + border-radius: 4px; +} +details > pre, details > p { + background-color: #f2f8f7; + border-bottom: 0.2em solid #dbeceb; + margin: 0; + padding: 10px; +} diff --git a/priv/css/bosh.css b/priv/css/bosh.css new file mode 100644 index 000000000..efa6b68b5 --- /dev/null +++ b/priv/css/bosh.css @@ -0,0 +1,51 @@ +body { + margin: 0; + padding: 0; + font-family: sans-serif; + color: #fff; +} +h1 { + font-size: 3em; + color: #444; +} +p { + line-height: 1.5em; + color: #888; +} +a { + color: #fff; +} +a:hover, +a:active { + text-decoration: underline; +} +.container { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: #424A55; + background-image: -webkit-linear-gradient(270deg, rgba(48,52,62,0) 24%, #30353e 100%); + background-image: linear-gradient(-180deg, rgba(48,52,62,0) 24%, #30353e 100%); +} +.section { + padding: 3em; +} +.white.section { + background: #fff; + border-bottom: 4px solid #41AFCA; +} +.white.section a { + text-decoration: none; + color: #41AFCA; +} +.white.section a:hover, +.white.section a:active { + text-decoration: underline; +} +.block { + margin: 0 auto; + max-width: 900px; + width: 100%; +} diff --git a/priv/css/muc.css b/priv/css/muc.css new file mode 100644 index 000000000..b81ad5b52 --- /dev/null +++ b/priv/css/muc.css @@ -0,0 +1,29 @@ +.ts {color: #AAAAAA; text-decoration: none;} +.mrcm {color: #009900; font-style: italic; font-weight: bold;} +.msc {color: #009900; font-style: italic; font-weight: bold;} +.msm {color: #000099; font-style: italic; font-weight: bold;} +.mj {color: #009900; font-style: italic;} +.ml {color: #009900; font-style: italic;} +.mk {color: #009900; font-style: italic;} +.mb {color: #009900; font-style: italic;} +.mnc {color: #009900; font-style: italic;} +.mn {color: #0000AA;} +.mne {color: #AA0099;} +a.nav {color: #AAAAAA; font-family: monospace; letter-spacing: 3px; text-decoration: none;} +div.roomtitle {border-bottom: #224466 solid 3pt; margin-left: 20pt;} +div.roomtitle {color: #336699; font-size: 24px; font-weight: bold; font-family: sans-serif; letter-spacing: 3px; text-decoration: none;} +a.roomjid {color: #336699; font-size: 24px; font-weight: bold; font-family: sans-serif; letter-spacing: 3px; margin-left: 20pt; text-decoration: none;} +div.logdate {color: #663399; font-size: 20px; font-weight: bold; font-family: sans-serif; letter-spacing: 2px; border-bottom: #224466 solid 1pt; margin-left:80pt; margin-top:20px;} +div.roomsubject {color: #336699; font-size: 18px; font-family: sans-serif; margin-left: 80pt; margin-bottom: 10px;} +div.rc {color: #336699; font-size: 12px; font-family: sans-serif; margin-left: 50%; text-align: right; background: #f3f6f9; border-bottom: 1px solid #336699; border-right: 4px solid #336699;} +div.rct {font-weight: bold; background: #e3e6e9; padding-right: 10px; cursor: pointer;} +div.rct:hover {text-decoration: underline;} +div.rcos {padding-right: 10px;} +div.rcoe {color: green;} +div.rcod {color: red;} +div.rcoe:after {content: ": v";} +div.rcod:after {content: ": x";} +div.rcot:after {} +div.jl {display: none;} +.legend {width: 100%; margin-top: 30px; border-top: #224466 solid 1pt; padding: 10px 0px 10px 0px; text-align: left; font-family: monospace; letter-spacing: 2px;} +.w3c {position: absolute; right: 10px; width: 60%; text-align: right; font-family: monospace; letter-spacing: 1px;} diff --git a/priv/css/oauth.css b/priv/css/oauth.css new file mode 100644 index 000000000..41112f39c --- /dev/null +++ b/priv/css/oauth.css @@ -0,0 +1,103 @@ +body { + margin: 0; + padding: 0; + + font-family: sans-serif; + color: #fff; +} + +h1 { + font-size: 3em; + color: #444; +} + +p { + line-height: 1.5em; + color: #888; +} + +a { + color: #fff; +} +a:hover, +a:active { + text-decoration: underline; +} + +em { + display: inline-block; + padding: 0 5px; + background: #f4f4f4; + border-radius: 5px; + font-style: normal; + font-weight: bold; + color: #444; +} + +form { + color: #444; +} +label { + display: block; + font-weight: bold; +} + +input[type=text], +input[type=password] { + margin-bottom: 1em; + padding: 0.4em; + max-width: 330px; + width: 100%; + border: 1px solid #c4c4c4; + border-radius: 5px; + outline: 0; + font-size: 1.2em; +} +input[type=text]:focus, +input[type=password]:focus, +input[type=text]:active, +input[type=password]:active { + border-color: #41AFCA; +} + +input[type=submit] { + font-size: 1em; +} + +.container { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: #424A55; + background-image: -webkit-linear-gradient(270deg, rgba(48,52,62,0) 24%, #30353e 100%); + background-image: linear-gradient(-180deg, rgba(48,52,62,0) 24%, #30353e 100%); +} + +.section { + padding: 3em; +} +.white.section { + background: #fff; + border-bottom: 4px solid #41AFCA; +} + +.white.section a { + text-decoration: none; + color: #41AFCA; +} +.white.section a:hover, +.white.section a:active { + text-decoration: underline; +} + +.container > .section { + background: #424A55; +} + +.block { + margin: 0 auto; + max-width: 900px; + width: 100%; +} diff --git a/priv/css/register.css b/priv/css/register.css new file mode 100644 index 000000000..f38461eb1 --- /dev/null +++ b/priv/css/register.css @@ -0,0 +1,65 @@ +@viewport { + width: device-width; + zoom: 1.0; +} + +html,body { + font-family: sans-serif; + background: white; + + padding: 0.5em; + margin: auto; + max-width: 800px; + height: 100%; +} + +form { + padding: 0.5em 0; +} + +ul { + list-style: none; +} + ul > li { + margin-bottom: 2em; + } + +ol { + list-style: none; + padding: 0; +} + ol > li { + margin-bottom: 2em; + font-weight: bold; + font-size: 0.75em; + } + ol > li > ul { + list-style: decimal; + font-weight: normal; + font-style: italic; + } + ol > li > ul > li { + margin-bottom: auto; + } + +input { + display: block; + padding: 0.25em; + font-size: 1.5em; + border: 1px solid #ccc; + border-radius: 0; + + -webkit-appearance: none; + -moz-appearance: none; +} + input:focus { + border-color: #428bca; + } + input[type=submit] { + padding: 0.33em 1em; + background-color: #428bca; + border-radius: 2px; + cursor: pointer; + border: none; + color: #fff; + } diff --git a/priv/css/sortable.min.css b/priv/css/sortable.min.css new file mode 100644 index 000000000..5296c0f9f --- /dev/null +++ b/priv/css/sortable.min.css @@ -0,0 +1 @@ +.sortable thead th:not(.no-sort){cursor:pointer}.sortable thead th:not(.no-sort)::after,.sortable thead th:not(.no-sort)::before{transition:color .1s ease-in-out;font-size:1.2em;color:rgba(0,0,0,0)}.sortable thead th:not(.no-sort)::after{margin-left:3px;content:"▸"}.sortable thead th:not(.no-sort):hover::after{color:inherit}.sortable thead th:not(.no-sort)[aria-sort=descending]::after{color:inherit;content:"▾"}.sortable thead th:not(.no-sort)[aria-sort=ascending]::after{color:inherit;content:"▴"}.sortable thead th:not(.no-sort).indicator-left::after{content:""}.sortable thead th:not(.no-sort).indicator-left::before{margin-right:3px;content:"▸"}.sortable thead th:not(.no-sort).indicator-left:hover::before{color:inherit}.sortable thead th:not(.no-sort).indicator-left[aria-sort=descending]::before{color:inherit;content:"▾"}.sortable thead th:not(.no-sort).indicator-left[aria-sort=ascending]::before{color:inherit;content:"▴"}/*# sourceMappingURL=sortable-base.min.css.map */ diff --git a/priv/img/admin-logo.png b/priv/img/admin-logo.png new file mode 100644 index 000000000..041b37c69 Binary files /dev/null and b/priv/img/admin-logo.png differ diff --git a/priv/img/bosh-logo.png b/priv/img/bosh-logo.png new file mode 100644 index 000000000..25a46f9f9 Binary files /dev/null and b/priv/img/bosh-logo.png differ diff --git a/priv/img/favicon.png b/priv/img/favicon.png new file mode 100644 index 000000000..56c8b09c9 Binary files /dev/null and b/priv/img/favicon.png differ diff --git a/priv/img/oauth-logo.png b/priv/img/oauth-logo.png new file mode 100644 index 000000000..25a46f9f9 Binary files /dev/null and b/priv/img/oauth-logo.png differ diff --git a/priv/img/powered-by-ejabberd.png b/priv/img/powered-by-ejabberd.png new file mode 100644 index 000000000..2667f57f7 Binary files /dev/null and b/priv/img/powered-by-ejabberd.png differ diff --git a/priv/img/powered-by-erlang.png b/priv/img/powered-by-erlang.png new file mode 100644 index 000000000..4dfdb06a8 Binary files /dev/null and b/priv/img/powered-by-erlang.png differ diff --git a/priv/img/valid-xhtml10.png b/priv/img/valid-xhtml10.png new file mode 100644 index 000000000..2275ee6ea Binary files /dev/null and b/priv/img/valid-xhtml10.png differ diff --git a/priv/img/vcss.png b/priv/img/vcss.png new file mode 100644 index 000000000..9b2f596e0 Binary files /dev/null and b/priv/img/vcss.png differ diff --git a/priv/js/admin.js b/priv/js/admin.js new file mode 100644 index 000000000..d726e42c9 --- /dev/null +++ b/priv/js/admin.js @@ -0,0 +1,15 @@ + +function selectAll() { + for(i=0;i elements are not allowed by RFC6121","Повече от един елемента не се разрешават от RFC6121"}. +{"Multi-User Chat","Групов чат (MUC)"}. +{"Name","Име"}. +{"Natural Language for Room Discussions","Език за дискусии в стаята"}. +{"Natural-Language Room Name","Име на стаята на предпочитания език"}. +{"Neither 'jid' nor 'nick' attribute found","Атрибутите 'jid' и 'nick' не са намерени"}. +{"Neither 'role' nor 'affiliation' attribute found","Атрибути 'role' или 'affiliation' не са намерени"}. +{"Never","Никога"}. +{"New Password:","Нова парола:"}. +{"Nickname can't be empty","Псевдонимът не може да бъде празен"}. +{"Nickname Registration at ","Регистрация на псевдоним в "}. +{"Nickname ~s does not exist in the room","Псевдонимът ~s не присъства в стаята"}. +{"Nickname","Псевдоним"}. +{"No address elements found","Не е намерен адресен елемент"}. +{"No addresses element found","Не са намерени адресни елементи"}. +{"No 'affiliation' attribute found","Атрибут 'affiliation' не е намерен"}. +{"No available resource found","Не е намерен наличен ресурс"}. +{"No body provided for announce message","Не е предоставен текст за съобщение тип обява"}. +{"No child elements found","Не са открити подчинени елементи"}. +{"No data form found","Не е намерена форма за данни"}. +{"No Data","Няма данни"}. +{"No features available","Няма налични функции"}. +{"No element found","Елементът не е намерен"}. +{"No hook has processed this command","Никоя кука не е обработила тази команда"}. +{"No info about last activity found","Няма информация за последна активновт"}. +{"No 'item' element found","Елементът 'item' не е намерен"}. +{"No items found in this query","Няма намерени елементи в тази заявка"}. +{"No limit","Няма ограничение"}. +{"No module is handling this query","Нито един модул не обработва тази заявка"}. +{"No node specified","Не е посочен нод"}. +{"No 'password' found in data form","Не е намерен 'password' във формата за данни"}. +{"No 'password' found in this query","В заявката не е намерен 'password'"}. +{"No 'path' found in data form","Не е намерен 'path' във формата за данни"}. +{"No pending subscriptions found","Не са намерени чакащи абонаменти"}. +{"No privacy list with this name found","Не е намерен списък за поверителност с това име"}. +{"No private data found in this query","Няма открити лични данни в тази заявка"}. +{"No running node found","Не е намерен работещ нод"}. +{"No services available","Няма налични услуги"}. +{"No statistics found for this item","Не е налична статистика за този елемент"}. +{"No 'to' attribute found in the invitation","Атрибутът 'to' не е намерен в поканата"}. +{"Nobody","Никой"}. +{"Node already exists","Нодът вече съществува"}. +{"Node ID","ID на нода"}. +{"Node index not found","Индексът на нода не е намерен"}. +{"Node not found","Нодът не е намерен"}. +{"Node ~p","Нод ~p"}. +{"Nodeprep has failed","Nodeprep е неуспешен"}. +{"Nodes","Нодове"}. +{"Node","Нод"}. +{"None","Нито един"}. +{"Not allowed","Не е разрешено"}. +{"Not Found","Не е намерен"}. +{"Not subscribed","Няма абонамент"}. +{"Notify subscribers when items are removed from the node","Уведоми абонатите, когато елементите бъдат премахнати от нода"}. +{"Notify subscribers when the node configuration changes","Уведоми абонатите, когато конфигурацията на нода се промени"}. +{"Notify subscribers when the node is deleted","Уведоми абонатите, когато нодът бъде изтрит"}. +{"November","Ноември"}. +{"Number of answers required","Брой на необходимите отговори"}. +{"Number of occupants","Брой участници"}. +{"Number of Offline Messages","Брой офлайн съобщения"}. +{"Number of online users","Брой онлайн потребители"}. +{"Number of registered users","Брой регистрирани потребители"}. +{"Number of seconds after which to automatically purge items, or `max` for no specific limit other than a server imposed maximum","Брой секунди, след които автоматично да се изчистят елементите, или `max` за липса на конкретно ограничение, различно от наложения от сървъра максимум"}. +{"Occupants are allowed to invite others","На участниците е позволено да канят други"}. +{"Occupants are allowed to query others","Участниците могат да отправят заявки към други лица"}. +{"Occupants May Change the Subject","Участниците могат да променят темата"}. +{"October","Октомври"}. +{"OK","ДОБРЕ"}. +{"Old Password:","Стара парола:"}. +{"Online Users","Онлайн потребители"}. +{"Online","Онлайн"}. +{"Only collection node owners may associate leaf nodes with the collection","Само собственици на колекционни нодове имат право да свързват листови нодове към колекцията"}. +{"Only deliver notifications to available users","Доставяне на известия само до наличните потребители"}. +{"Only or tags are allowed","Само тагове и са разрешени"}. +{"Only element is allowed in this query","Само елементът е разрешен за тази заявка"}. +{"Only members may query archives of this room","Само членовете могат да търсят архиви на тази стая"}. +{"Only moderators and participants are allowed to change the subject in this room","Само модератори и участници имат право да променят темата в тази стая"}. +{"Only moderators are allowed to change the subject in this room","Само модераторите имат право да сменят темата в тази стая"}. +{"Only moderators are allowed to retract messages","Само модераторите имат право да оттеглят съобщения"}. +{"Only moderators can approve voice requests","Само модераторите могат да одобряват гласови заявки"}. +{"Only occupants are allowed to send messages to the conference","Само участници имат право да изпращат съобщения до конференцията"}. +{"Only occupants are allowed to send queries to the conference","Само участници имат право да изпращат запитвания до конференцията"}. +{"Only publishers may publish","Само издателите могат да публикуват"}. +{"Only service administrators are allowed to send service messages","Само администраторите на услуги имат право да изпращат системни съобщения"}. +{"Only those on a whitelist may associate leaf nodes with the collection","Само тези от списъка с позволени могат да свързват листови нодове с колекцията"}. +{"Only those on a whitelist may subscribe and retrieve items","Само тези от списъка с позволени могат да се абонират и да извличат елементи"}. +{"Organization Name","Име на организацията"}. +{"Organization Unit","Отдел"}. +{"Other Modules Available:","Други налични модули:"}. +{"Outgoing s2s Connections","Изходящи s2s връзки"}. +{"Owner privileges required","Изискват се привилегии на собственик"}. +{"Packet relay is denied by service policy","Предаването на пакети е отказано от политиката на услугата"}. +{"Participant ID","ID на участник"}. +{"Participant","Участник"}. +{"Password Verification","Проверка на паролата"}. +{"Password Verification:","Проверка на паролата:"}. +{"Password","Парола"}. +{"Password:","Парола:"}. +{"Path to Dir","Път към директория"}. +{"Path to File","Път до файл"}. +{"Payload semantic type information","Информация за семантичен тип полезен товар"}. +{"Period: ","Период: "}. +{"Persist items to storage","Запазване на елементите в хранилището"}. +{"Persistent","Постоянен"}. +{"Ping query is incorrect","Заявката за пинг е неправилна"}. +{"Ping","Пинг"}. +{"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Обърнете внимание, че тези опции ще направят резервно копие само на вградената (Mnesia) база данни. Ако използвате модула ODBC, трябва да направите резервно копие на SQL базата данни отделно."}. +{"Please, wait for a while before sending new voice request","Моля, изчакайте известно време, преди да изпратите нова заявка за гласова връзка"}. +{"Pong","Понг"}. +{"Possessing 'ask' attribute is not allowed by RFC6121","Притежаването на атрибут 'ask' не е разрешено от RFC6121"}. +{"Present real Jabber IDs to","Покажи истински Jabber ID-та на"}. +{"Previous session not found","Предишната сесия не е намерена"}. +{"Previous session PID has been killed","PID от предишната сесия е унищожен"}. +{"Previous session PID has exited","Предишният PID на сесията е излязъл"}. +{"Previous session PID is dead","PID от предишната сесия не съществува"}. +{"Previous session timed out","Времето на предишната сесия изтече"}. +{"private, ","частна, "}. +{"Public","Публичен"}. +{"Publish model","Модел за публикуване"}. +{"Publish-Subscribe","Публикуване-Абониране"}. +{"PubSub subscriber request","Заявка от абонат за PubSub"}. +{"Purge all items when the relevant publisher goes offline","Изчисти всички елементи, когато съответният публикуващ премине в режим офлайн"}. +{"Push record not found","Push записът не е намерен"}. +{"Queries to the conference members are not allowed in this room","В тази стая не се допускат запитвания към членовете на конференцията"}. +{"Query to another users is forbidden","Заявка към други потребители е забранена"}. +{"RAM and disc copy","Копие в RAM и на диск"}. +{"RAM copy","Копие в RAM"}. +{"Really delete message of the day?","Наистина ли желаете да изтриете съобщението на деня?"}. +{"Receive notification from all descendent nodes","Получаване на известие от всички низходящи нодове"}. +{"Receive notification from direct child nodes only","Получаване на известия само от директни подчинени нодове"}. +{"Receive notification of new items only","Получаване на известия само за нови елементи"}. +{"Receive notification of new nodes only","Получаване на известия само за нови нодове"}. +{"Recipient is not in the conference room","Получателят не е в конферентната стая"}. +{"Register an XMPP account","Регистрирай XMPP акаунт"}. +{"Register","Регистрирай"}. +{"Remote copy","Отдалечено копие"}. +{"Remove a hat from a user","Премахни шапка от потребител"}. +{"Remove User","Премахни потребител"}. +{"Replaced by new connection","Заменен от нова връзка"}. +{"Request has timed out","Времето за заявка изтече"}. +{"Request is ignored","Заявката е игнорирано"}. +{"Requested role","Заявена роля"}. +{"Resources","Ресурси"}. +{"Restart Service","Рестартирай услугата"}. +{"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:","Възстановете незабавно копие от обикновен текст:"}. +{"Restore","Възстанови"}. +{"Roles and Affiliations that May Retrieve Member List","Роли и принадлежности, които могат да извличат списък с членове"}. +{"Roles for which Presence is Broadcasted","Роли, за които се излъчва присъствие"}. +{"Roles that May Send Private Messages","Роли, които могат да изпращат лични съобщения"}. +{"Room Configuration","Конфигурация на стаята"}. +{"Room creation is denied by service policy","Създаването на стая е отказано поради политика на услугата"}. +{"Room description","Описание на стаята"}. +{"Room Occupants","Участници в стаята"}. +{"Room terminates","Стаята се прекратява"}. +{"Room title","Заглавие на стаята"}. +{"Roster groups allowed to subscribe","Групи от списъци с контакти, на които е разрешено да се абонират"}. +{"Roster size","Размер на списъка с контакти"}. +{"Running Nodes","Работещи нодове"}. +{"~s invites you to the room ~s","~s ви кани в стая ~s"}. +{"Saturday","Събота"}. +{"Search from the date","Търси от дата"}. +{"Search Results for ","Резултати от търсенето за "}. +{"Search the text","Търси текста"}. +{"Search until the date","Търси до дата"}. +{"Search users in ","Търси потребители в "}. +{"Send announcement to all online users on all hosts","Изпрати съобщение до всички онлайн потребители на всички хостове"}. +{"Send announcement to all online users","Изпрати съобщение до всички онлайн потребители"}. +{"Send announcement to all users on all hosts","Изпрати съобщение до всички потребители на всички хостове"}. +{"Send announcement to all users","Изпрати съобщение до всички потребители"}. +{"September","Септември"}. +{"Server:","Сървър:"}. +{"Service list retrieval timed out","Времето за изчакване на извличането на списъка с услуги изтече"}. +{"Session state copying timed out","Времето за изчакване на копирането на състоянието на сесията изтече"}. +{"Set message of the day and send to online users","Задай съобщение на деня и го изпрати на онлайн потребителите"}. +{"Set message of the day on all hosts and send to online users","Задавай съобщение на деня на всички хостове и изпрати на онлайн потребителите"}. +{"Shared Roster Groups","Споделени групи от списъци с контакти"}. +{"Show Integral Table","Покажи интегрална таблица"}. +{"Show Occupants Join/Leave","Покажи участници Влязъл/Напускнал"}. +{"Show Ordinary Table","Покажи обикновена таблица"}. +{"Shut Down Service","Изключи услугата"}. +{"SOCKS5 Bytestreams","SOCKS5 байтови потоци"}. +{"Some XMPP clients can store your password in the computer, but you should do this only in your personal computer for safety reasons.","Някои XMPP клиенти могат да съхраняват паролата Ви в компютъра, но от съображения за сигурност трябва да го правите само на личния си компютър."}. +{"Sources Specs:","Спецификации на източниците:"}. +{"Specify the access model","Задай модела за достъп"}. +{"Specify the event message type","Задай типа на съобщението за събитие"}. +{"Specify the publisher model","Задайте модела на публикуващия"}. +{"Stanza id is not valid","Невалидно ID на строфата"}. +{"Stanza ID","ID на строфа"}. +{"Statically specify a replyto of the node owner(s)","Статично задаване на replyto на собственика(ците) на нода"}. +{"Stopped Nodes","Спрени нодове"}. +{"Store binary backup:","Запази бинарен архив:"}. +{"Store plain text backup:","Запази архив като обикновен текст:"}. +{"Stream management is already enabled","Управлението на потока вече е активирано"}. +{"Stream management is not enabled","Управлението на потока не е активирано"}. +{"Subject","Тема"}. +{"Submitted","Изпратено"}. +{"Subscriber Address","Адрес на абоната"}. +{"Subscribers may publish","Абонатите могат да публикуват"}. +{"Subscription requests must be approved and only subscribers may retrieve items","Заявките за абонамент трябва да бъдат одобрени и само абонатите могат да извличат елементи"}. +{"Subscriptions are not allowed","Абонаментите не са разрешени"}. +{"Sunday","Неделя"}. +{"Text associated with a picture","Текст, свързан със снимка"}. +{"Text associated with a sound","Текст, свързан със звук"}. +{"Text associated with a video","Текст, свързан с видео"}. +{"Text associated with speech","Текст, свързан с реч"}. +{"That nickname is already in use by another occupant","Този псевдоним вече се използва от друг участник"}. +{"That nickname is registered by another person","Този псевдоним е регистриран от друго лице"}. +{"The account already exists","Профилът вече съществува"}. +{"The account was not unregistered","Профилът не е дерегистриран"}. +{"The body text of the last received message","Текстът на последното получено съобщение"}. +{"The CAPTCHA is valid.","CAPTCHA предизвикателството е валидно."}. +{"The CAPTCHA verification has failed","Проверката на CAPTCHA предизвикателството е неуспешна"}. +{"The captcha you entered is wrong","Въведеният captcha код е грешен"}. +{"The child nodes (leaf or collection) associated with a collection","Дъщерните нодове (листови или колекция), свързани с колекция"}. +{"The collections with which a node is affiliated","Колекциите, с които даден нод е свързан"}. +{"The DateTime at which a leased subscription will end or has ended","Датата и часът, на който абонамент ще приключи или е приключил"}. +{"The datetime when the node was created","Датата, когато нодът е бил създаден"}. +{"The default language of the node","Езикът по подразбиране на нода"}. +{"The feature requested is not supported by the conference","Исканата функция не се поддържа от конференцията"}. +{"The JID of the node creator","JID на създателя на нода"}. +{"The JIDs of those to contact with questions","JID на лицата, с които да се свържете при въпроси"}. +{"The JIDs of those with an affiliation of owner","JID на лицата с принадлежност на собственик"}. +{"The JIDs of those with an affiliation of publisher","JID-та на лица с принадлежност към публикуващи"}. +{"The list of all online users","Списък на всички онлайн потребители"}. +{"The list of all users","Списък на всички потребители"}. +{"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, or `max` for no specific limit other than a server imposed maximum","Максимален брой подчинени нодове, които могат да бъдат свързани с колекция, или `max` за липса на конкретен лимит, различен от наложения от сървъра максимум"}. +{"The minimum number of milliseconds between sending any two notification digests","Минималният брой милисекунди между изпращането на две извадки на известия"}. +{"The name of the node","Името на нода"}. +{"The node is a collection node","Нодът е от тип колекция"}. +{"The node is a leaf node (default)","Нодът е листов (по подразбиране)"}. +{"The NodeID of the relevant node","NodeID на съответния нод"}. +{"The number of pending incoming presence subscription requests","Броят на чакащите входящи заявки за абонамент за присъствие"}. +{"The number of subscribers to the node","Бротят абонати на нода"}. +{"The number of unread or undelivered messages","Броят непрочетени или недоставени съобщения"}. +{"The password contains unacceptable characters","Паролата съдържа недопустими символи"}. +{"The password is too weak","Паролата е твърде слаба"}. +{"the password is","паролата е"}. +{"The password of your XMPP account was successfully changed.","Паролата на вашия XMPP профил беше успешно променена."}. +{"The password was not changed","Паролата не е променена"}. +{"The passwords are different","Паролите са различни"}. +{"The presence states for which an entity wants to receive notifications","Състояния на присъствие, за които даден субект желае да получава известия"}. +{"The query is only allowed from local users","Заявката е разрешена само за локални потребители"}. +{"The query must not contain elements","Заявката не може да съдържа елементи "}. +{"The room subject can be modified by participants","Темата на стаята може да бъде променяна от участниците"}. +{"The semantic type information of data in the node, usually specified by the namespace of the payload (if any)","Информацията за семантичния тип данни на нода, обикновено зададена от пространството на имената на прикачените данни (ако има такива)"}. +{"The sender of the last received message","Подателят на последното получено съобщение"}. +{"The stanza MUST contain only one element, one element, or one element","Строфата ТРЯБВА да съдържа само един елемент, един елемент или един елемент"}. +{"The subscription identifier associated with the subscription request","Идентификаторът на абонамента, свързан със заявката за абонамент"}. +{"The URL of an XSL transformation which can be applied to payloads in order to generate an appropriate message body element.","URL адрес на XSL трансформацията, която може да се приложи към прикачените данни, за да се генерира подходящ елемент от тялото на съобщението."}. +{"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","URL адрес на XSL трансформацията, която може да се приложи към формата на прикачените данни, за да се генерира валиден резултат от Data Forms, който клиентът може да покаже с помощта на общ механизъм за визуализация на Data Forms"}. +{"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) ще бъде във формата: username@server. Моля, прочетете внимателно инструкциите, за да попълните правилно полетата."}. +{"This page allows to unregister an XMPP account in this XMPP server.","Тази страница позволява премахване на XMPP профил от този сървър."}. +{"This room is not anonymous","Тази стая не е анонимна"}. +{"This service can not process the address: ~s","Тази услуга не може да обработи адреса: ~s"}. +{"Thursday","Четвъртък"}. +{"Time delay","Закъснение"}. +{"Timed out waiting for stream resumption","Времето за изчакване за възобновяване на потока изтече"}. +{"To register, visit ~s","За да се регистрирате, посетете ~s"}. +{"To ~ts","До ~ts"}. +{"Token TTL","Токен TTL"}. +{"Too many active bytestreams","Твърде много активни \"bytestreams\" потоци"}. +{"Too many CAPTCHA requests","Твърде много CAPTCHA заявки"}. +{"Too many child elements","Твърде много дъщерни елементи"}. +{"Too many elements","Твърде много елементи"}. +{"Too many elements","Твърде много елементи"}. +{"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","Твърде много (~p) неуспешни опити за удостоверявания от този IP адрес (~s). Адресът ще бъде деблокиран в ~s UTC"}. +{"Too many receiver fields were specified","Посочени са твърде много полета за получател"}. +{"Too many unacked stanzas","Твърде много непотвърдени строфи"}. +{"Too many users in this conference","Твърде много потребители в тази конференция"}. +{"Traffic rate limit is exceeded","Лимитът за трафик е надвишен"}. +{"~ts's MAM Archive","~ts's MAM архив"}. +{"~ts's Offline Messages Queue","Офлайн съобщения на ~ts"}. +{"Tuesday","Вторник"}. +{"Unable to generate a CAPTCHA","Не може да се генерира CAPTCHA"}. +{"Unable to register route on existing local domain","Не може да се регистрира маршрут в съществуващ локален домейн"}. +{"Unauthorized","Неоторизиран"}. +{"Unexpected action","Неочаквано действие"}. +{"Unexpected error condition: ~p","Неочаквано състояние на грешка: ~p"}. +{"Uninstall","Деинсталирай"}. +{"Unregister an XMPP account","Дерегистрирай XMPP профил"}. +{"Unregister","Дерегистрирай"}. +{"Unsupported element","Неподдържан елемент "}. +{"Unsupported version","Неподдържана версия"}. +{"Update message of the day (don't send)","Актуализирай съобщението на деня (не изпращай)"}. +{"Update message of the day on all hosts (don't send)","Актуализирай съобщението на деня на всички хостове (не изпращай)"}. +{"Update specs to get modules source, then install desired ones.","Актуализирайте спецификациите, за да получите източник на модули, след което инсталирайте желаните."}. +{"Update Specs","Актуализирай спецификациите"}. +{"Updating the vCard is not supported by the vCard storage backend","Актуализирането на vCard не се поддържа от настройката за съхранение на vCard"}. +{"Upgrade","Обнови"}. +{"URL for Archived Discussion Logs","URL адрес за дневници на архивирани дискусии"}. +{"User already exists","Потребителят вече съществува"}. +{"User (jid)","Потребител (jid)"}. +{"User JID","Потребител JID"}. +{"User Management","Управление на потребители"}. +{"User not allowed to perform an IQ set on another user's vCard.","Потребителят не може да извършва IQ настрийка на vCard на друг потребител."}. +{"User removed","Потребителят е премахнат"}. +{"User session not found","Потребителската сесия не е намерена"}. +{"User session terminated","Потребителската сесия е прекратена"}. +{"User ~ts","Потребител ~ts"}. +{"Username:","Потребителско име:"}. +{"Users are not allowed to register accounts so quickly","Не е разрешено потребителите да регистрират профили толкова бързо"}. +{"Users Last Activity","Последна активност на потребителите"}. +{"Users","Потребители"}. +{"User","Потребител"}. +{"Value 'get' of 'type' attribute is not allowed","Стойността 'get' на атрибут 'type' не е разрешена"}. +{"Value of '~s' should be boolean","Стойността на '~s' трябва да е булева"}. +{"Value of '~s' should be datetime string","Стойността на '~s' трябва да бъде низ за дата и час"}. +{"Value of '~s' should be integer","Стойността на '~s' трябва да бъде цяло число"}. +{"Value 'set' of 'type' attribute is not allowed","Стойността 'set' на атрибут 'type' не е разрешена"}. +{"vCard User Search","vCard търсене на потребител"}. +{"View joined MIX channels","Вижте присъединените MIX канали"}. +{"Virtual Hosts","Виртуални хостове"}. +{"Visitors are not allowed to change their nicknames in this room","Посетителите нямат право да променят псевдонимите си в тази стая"}. +{"Visitors are not allowed to send messages to all occupants","На посетителите не е разрешено да изпращат съобщения до всички участници"}. +{"Visitor","Посетител"}. +{"Voice requests are disabled in this conference","Гласовите обаждания са деактивирани в тази конференция"}. +{"Voice request","Заявка за гласово обаждане"}. +{"Wednesday","Сряда"}. +{"When a new subscription is processed and whenever a subscriber comes online","Когато се обработва нов абонамент и всеки път, когато абонат се появи онлайн"}. +{"When a new subscription is processed","Когато се обработва нов абонамент"}. +{"When to send the last published item","Кога да изпратите последния публикуван елемент"}. +{"Whether an entity wants to receive an XMPP message body in addition to the payload format","Дали даден обект иска да получи тяло на XMPP съобщение в допълнение към формата на полезен товар"}. +{"Whether an entity wants to receive digests (aggregations) of notifications or all notifications individually","Дали даден обект желае да получава обобщения за известия или всички известия поотделно"}. +{"Whether an entity wants to receive or disable notifications","Дали даден обект желае да получава или деактивира известия"}. +{"Whether owners or publisher should receive replies to items","Дали собствениците или публикуващите трябва да получават отговори на елементи"}. +{"Whether the node is a leaf (default) or a collection","Дали нодът е листов (по подразбиране) или колекция"}. +{"Whether to allow subscriptions","Дали да се разрешат абонаменти"}. +{"Whether to make all subscriptions temporary, based on subscriber presence","Дали всички абонаменти да бъдат временни въз основа на присъствието на абонат"}. +{"Whether to notify owners about new subscribers and unsubscribes","Дали да се уведомяват собствениците за нови абонати и откази от абонамент"}. +{"Who can send private messages","Кой може да изпраща лични съобщения"}. +{"Who may associate leaf nodes with a collection","Кой може да асоциира листови нодове с колекция"}. +{"Wrong parameters in the web formulary","Грешни параметри в уеб формуляра"}. +{"Wrong xmlns","Грешен xmlns"}. +{"XMPP Account Registration","Регистриране на XMPP профил"}. +{"XMPP Domains","XMPP домейни"}. +{"XMPP Show Value of Away","XMPP покажи стойност на Отсъства"}. +{"XMPP Show Value of Chat","XMPP покажи стойност на Чат"}. +{"XMPP Show Value of DND (Do Not Disturb)","XMPP покажи стойност на DND (Не ме безпокой)"}. +{"XMPP Show Value of XA (Extended Away)","XMPP покажи стойност на Продължително отсъствие"}. +{"XMPP URI of Associated Publish-Subscribe Node","XMPP URI на асоцииран Publish-Subscribe нод"}. +{"You are being removed from the room because of a system shutdown","Премахнати сте от стаята поради изключване на системата"}. +{"You are not allowed to send private messages","Нямате право да изпращате лични съобщения"}. +{"You are not joined to the channel","Не сте присъединени към канала"}. +{"You can later change your password using an XMPP client.","По-късно можете да промените паролата си с помощта на XMPP клиент."}. +{"You have been banned from this room","Достъпът ви до тази стая е забранен"}. +{"You have joined too many conferences","Присъединили сте се към твърде много конференции"}. +{"You must fill in field \"Nickname\" in the form","Трябва да попълните полето \"Псевдоним\" във формата"}. +{"You need a client that supports x:data and CAPTCHA to register","За да се регистрирате Ви е нужен клиент, който поддържа x:data и CAPTCHA"}. +{"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 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 акаунт, беше успешно дерегистриран."}. +{"You're not allowed to create nodes","Нямате право да създавате нодове"}. diff --git a/priv/msgs/ca.msg b/priv/msgs/ca.msg index 5df6fcf2a..951430a9b 100644 --- a/priv/msgs/ca.msg +++ b/priv/msgs/ca.msg @@ -1,43 +1,81 @@ -{"Access Configuration","Configuració d'accesos"}. -{"Access Control List Configuration","Configuració de la Llista de Control d'Accés"}. -{"Access Control Lists","Llista de Control d'Accés"}. -{"Access control lists","Llistes de Control de Accés"}. -{"Access denied by service policy","Accés denegat per la política del servei"}. -{"Access rules","Regles d'accés"}. -{"Access Rules","Regles d'Accés"}. -{"Action on user","Acció en l'usuari"}. -{"Add Jabber ID","Afegir Jabber ID"}. -{"Add New","Afegir nou"}. -{"Add User","Afegir usuari"}. -{"Administration","Administració"}. -{"Administration of ","Administració de "}. -{"Administrator privileges required","Es necessita tenir privilegis d'administrador"}. +%% Generated automatically +%% DO NOT EDIT: run `make translations` instead +%% To improve translations please read: +%% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ + +{" (Add * to the end of field to match substring)"," (Afegix * al final d'un camp per a buscar subcadenes)"}. +{" has set the subject to: "," ha posat el tema: "}. +{"# participants","# participants"}. +{"A description of the node","Una descripció del node"}. {"A friendly name for the node","Un nom per al node"}. +{"A password is required to enter this room","Es necessita contrasenya per a entrar en aquesta sala"}. +{"A Web Page","Una Pàgina Web"}. +{"Accept","Acceptar"}. +{"Access denied by service policy","Accés denegat per la política del servei"}. +{"Access model","Model d'Accés"}. +{"Account doesn't exist","El compte no existeix"}. +{"Action on user","Acció en l'usuari"}. +{"Add a hat to a user","Afegir un barret a un usuari"}. +{"Add User","Afegir usuari"}. +{"Administration of ","Administració de "}. +{"Administration","Administració"}. +{"Administrator privileges required","Es necessita tenir privilegis d'administrador"}. {"All activity","Tota l'activitat"}. -{"Allow this Jabber ID to subscribe to this pubsub node?","Permetre que aquesta Jabber ID es puga subscriure a aquest node pubsub"}. -{"Allow users to change the subject","Permetre que els usuaris canviin el tema"}. +{"All Users","Tots els usuaris"}. +{"Allow subscription","Permetre subscripció"}. +{"Allow this Jabber ID to subscribe to this pubsub node?","Permetre que aquesta Jabber ID es puga subscriure a aquest node pubsub?"}. +{"Allow this person to register with the room?","Permetre a esta persona registrar-se a la sala?"}. +{"Allow users to change the subject","Permetre que els usuaris canviïn el tema"}. {"Allow users to query other users","Permetre que els usuaris fagen peticions a altres usuaris"}. {"Allow users to send invites","Permetre que els usuaris envien invitacions"}. {"Allow users to send private messages","Permetre que els usuaris envien missatges privats"}. -{"Allow visitors to change nickname","Permetre als visitants canviar el Nickname"}. +{"Allow visitors to change nickname","Permetre als visitants canviar el sobrenom"}. {"Allow visitors to send private messages to","Permetre als visitants enviar missatges privats a"}. {"Allow visitors to send status text in presence updates","Permetre als visitants enviar text d'estat en les actualitzacions de presència"}. {"Allow visitors to send voice requests","Permetre als visitants enviar peticions de veu"}. -{"All Users","Tots els usuaris"}. +{"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 grup LDAP associat que defineix membresía a la sala; esto deuria ser un Nombre Distinguible de LDAP, d'acord amb una definició de grup específica d'implementació o de instal·lació."}. {"Announcements","Anuncis"}. -{"anyone","qualsevol"}. -{"A password is required to enter this room","Es necessita contrasenya per a entrar en aquesta sala"}. +{"Answer associated with a picture","Resposta associada amb una imatge"}. +{"Answer associated with a video","Resposta associada amb un vídeo"}. +{"Answer associated with speech","Resposta associada amb un parlament"}. +{"Answer to a question","Resposta a una pregunta"}. +{"Anyone in the specified roster group(s) may subscribe and retrieve items","Qualsevol en el grup de contactes especificat pot subscriure's i recuperar elements"}. +{"Anyone may associate leaf nodes with the collection","Qualsevol pot associar nodes fulla amb la col·lecció"}. +{"Anyone may publish","Qualsevol pot publicar"}. +{"Anyone may subscribe and retrieve items","Qualsevol pot publicar i recuperar elements"}. +{"Anyone with a presence subscription of both or from may subscribe and retrieve items","Qualsevol amb una subscripció de presencia de 'both' o 'from' pot subscriure's i publicar elements"}. +{"Anyone with Voice","Qualsevol amb Veu"}. +{"Anyone","Qualsevol"}. +{"API Commands","Comandaments API"}. {"April","Abril"}. +{"Arguments","Arguments"}. +{"Attribute 'channel' is required for this request","L'atribut 'channel' és necessari per a aquesta petició"}. +{"Attribute 'id' is mandatory for MIX messages","L'atribut 'id' es necessari per a missatges MIX"}. +{"Attribute 'jid' is not allowed here","L'atribut 'jid' no està permès ací"}. +{"Attribute 'node' is not allowed here","L'atribut 'node' no està permès ací"}. +{"Attribute 'to' of stanza that triggered challenge","L'atribut 'to' del paquet que va disparar la comprovació"}. {"August","Agost"}. -{"Backup","Guardar còpia de seguretat"}. +{"Automatic node creation is not enabled","La creació automàtica de nodes no està activada"}. {"Backup Management","Gestió de còpia de seguretat"}. -{"Backup of ","Còpia de seguretat de "}. +{"Backup of ~p","Còpia de seguretat de ~p"}. {"Backup to File at ","Desar còpia de seguretat a fitxer en "}. +{"Backup","Guardar còpia de seguretat"}. {"Bad format","Format erroni"}. {"Birthday","Aniversari"}. +{"Both the username and the resource are required","Es requereixen tant el nom d'usuari com el recurs"}. +{"Bytestream already activated","El Bytestream ja està activat"}. +{"Cannot remove active list","No es pot eliminar la llista activa"}. +{"Cannot remove default list","No es pot eliminar la llista per defecte"}. {"CAPTCHA web page","Pàgina web del CAPTCHA"}. +{"Challenge ID","ID de la comprovació"}. {"Change Password","Canviar Contrasenya"}. {"Change User Password","Canviar Contrasenya d'Usuari"}. +{"Changing password is not allowed","No està permès canviar la contrasenya"}. +{"Changing role/affiliation is not allowed","No està permès canviar el rol/afiliació"}. +{"Channel already exists","El canal ja existeix"}. +{"Channel does not exist","El canal no existeix"}. +{"Channel JID","JID del Canal"}. +{"Channels","Canals"}. {"Characters not allowed:","Caràcters no permesos:"}. {"Chatroom configuration modified","Configuració de la sala de xat modificada"}. {"Chatroom is created","La sala s'ha creat"}. @@ -46,376 +84,547 @@ {"Chatroom is stopped","La sala s'ha aturat"}. {"Chatrooms","Sales de xat"}. {"Choose a username and password to register with this server","Tria nom d'usuari i contrasenya per a registrar-te en aquest servidor"}. -{"Choose modules to stop","Selecciona mòduls a detindre"}. {"Choose storage type of tables","Selecciona el tipus d'almacenament de les taules"}. -{"Choose whether to approve this entity's subscription.","Tria si aprova aquesta entitat de subscripció"}. +{"Choose whether to approve this entity's subscription.","Tria si aproves aquesta entitat de subscripció."}. {"City","Ciutat"}. +{"Client acknowledged more stanzas than sent by server","El client ha reconegut més paquets dels que ha enviat el servidor"}. +{"Clustering","Clustering"}. {"Commands","Comandaments"}. {"Conference room does not exist","La sala de conferències no existeix"}. -{"Configuration","Configuració"}. {"Configuration of room ~s","Configuració de la sala ~s"}. -{"Connected Resources:","Recursos connectats:"}. -{"Connections parameters","Paràmetres de connexió"}. +{"Configuration","Configuració"}. +{"Contact Addresses (normally, room owner or owners)","Adreces de contacte (normalment, propietaris de la sala)"}. {"Country","Pais"}. -{"CPU Time:","Temps de CPU"}. -{"Database","Base de dades"}. -{"Database Tables at ","Taules de la base de dades en "}. +{"Current Discussion Topic","Assumpte de discussió actual"}. +{"Database failure","Error a la base de dades"}. {"Database Tables Configuration at ","Configuració de la base de dades en "}. +{"Database","Base de dades"}. {"December","Decembre"}. {"Default users as participants","Els usuaris són participants per defecte"}. -{"Delete","Eliminar"}. -{"Delete message of the day","Eliminar el missatge del dia"}. {"Delete message of the day on all hosts","Elimina el missatge del dis de tots els hosts"}. -{"Delete Selected","Eliminar els seleccionats"}. +{"Delete message of the day","Eliminar el missatge del dia"}. {"Delete User","Eliminar Usuari"}. {"Deliver event notifications","Entrega de notificacions d'events"}. {"Deliver payloads with event notifications","Enviar payloads junt a les notificacions d'events"}. -{"Description:","Descripció:"}. {"Disc only copy","Còpia sols en disc"}. -{"Displayed Groups:","Mostrar grups:"}. -{"Don't tell your password to anybody, not even the administrators of the Jabber server.","No li donis la teva contrasenya a ningú, ni tan sols als administradors del servidor Jabber."}. +{"Don't tell your password to anybody, not even the administrators of the XMPP server.","No li donis la teva contrasenya a ningú, ni tan sols als administradors del servidor XMPP."}. {"Dump Backup to Text File at ","Exporta còpia de seguretat a fitxer de text en "}. {"Dump to Text File","Exportar a fitxer de text"}. +{"Duplicated groups are not allowed by RFC6121","No estan permesos els grups duplicats al RFC6121"}. +{"Dynamically specify a replyto of the item publisher","Especifica dinàmicament l'adreça on respondre al publicador del element"}. {"Edit Properties","Editar propietats"}. -{"Either approve or decline the voice request.","Aprova o denega la petició de veu"}. -{"ejabberd IRC module","mòdul ejabberd IRC"}. +{"Either approve or decline the voice request.","Aprova o denega la petició de veu."}. +{"ejabberd HTTP Upload service","ejabberd - servei de HTTP Upload"}. {"ejabberd MUC module","mòdul ejabberd MUC"}. -{"ejabberd Publish-Subscribe module","Mòdul ejabberd Publicar-Subscriure"}. +{"ejabberd Multicast service","ejabberd - servei de Multicast"}. +{"ejabberd Publish-Subscribe module","ejabberd - Mòdul Publicar-Subscriure"}. {"ejabberd SOCKS5 Bytestreams module","mòdul ejabberd SOCKS5 Bytestreams"}. -{"ejabberd vCard module","Mòdul ejabberd vCard"}. -{"ejabberd Web Admin","Web d'administració del ejabberd"}. -{"Elements","Elements"}. -{"Email","Email"}. +{"ejabberd vCard module","ejabberd mòdul vCard"}. +{"ejabberd Web Admin","ejabberd Web d'administració"}. +{"ejabberd","ejabberd"}. +{"Email Address","Adreça de correu"}. +{"Email","Correu"}. +{"Enable hats","Activar barrets"}. {"Enable logging","Habilitar el registre de la conversa"}. -{"Encoding for server ~b","Codificació pel servidor ~b"}. +{"Enable message archiving","Activar l'emmagatzematge de missatges"}. +{"Enabling push without 'node' attribute is not supported","No està suportat activar Push sense l'atribut 'node'"}. {"End User Session","Finalitzar Sesió d'Usuari"}. -{"Enter list of {Module, [Options]}","Introdueix llista de {mòdul, [opcions]}"}. -{"Enter nickname you want to register","Introdueix el nickname que vols registrar"}. +{"Enter nickname you want to register","Introdueix el sobrenom que vols registrar"}. {"Enter path to backup file","Introdueix ruta al fitxer de còpia de seguretat"}. {"Enter path to jabberd14 spool dir","Introdueix la ruta al directori de jabberd14 spools"}. {"Enter path to jabberd14 spool file","Introdueix ruta al fitxer jabberd14 spool"}. {"Enter path to text file","Introdueix ruta al fitxer de text"}. {"Enter the text you see","Introdueix el text que veus"}. -{"Enter username and encodings you wish to use for connecting to IRC servers. Press 'Next' to get more fields to fill in. Press 'Complete' to save settings.","Introdueix el nom d'usuari i les codificacions de caràcters per a utilitzar als servidors de IRC. Apreta \"Seguent\" per veure més caps per omplir. Apreta \"Completar\" per guardar la configuració. "}. -{"Enter username, encodings, ports and passwords you wish to use for connecting to IRC servers","Introdueix el nom d'usuari, les codificacions de caràcters, els ports i contrasenyes per a utilitzar al connectar als servidors de IRC"}. -{"Erlang Jabber Server","Servidor Erlang Jabber"}. -{"Error","Error"}. -{"Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef.net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}].","Exemple: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef.net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]."}. +{"Erlang XMPP Server","Servidor Erlang XMPP"}. {"Exclude Jabber IDs from CAPTCHA challenge","Excloure Jabber IDs de la comprovació CAPTCHA"}. +{"Export all tables as SQL queries to a file:","Exporta totes les taules a un fitxer SQL:"}. {"Export data of all users in the server to PIEFXIS files (XEP-0227):","Exportar dades de tots els usuaris del servidor a arxius PIEFXIS (XEP-0227):"}. {"Export data of users in a host to PIEFXIS files (XEP-0227):","Exportar dades d'usuaris d'un host a arxius PIEFXIS (XEP-0227):"}. +{"External component failure","Error al component extern"}. +{"External component timeout","Temps esgotat al component extern"}. +{"Failed to activate bytestream","Errada al activar bytestream"}. {"Failed to extract JID from your voice request approval","No s'ha pogut extraure el JID de la teva aprovació de petició de veu"}. +{"Failed to map delegated namespace to external component","Ha fallat mapejar la delegació de l'espai de noms al component extern"}. +{"Failed to parse HTTP response","Ha fallat el processat de la resposta HTTP"}. +{"Failed to process option '~s'","Ha fallat el processat de la opció '~s'"}. {"Family Name","Cognom"}. +{"FAQ Entry","Entrada a la FAQ"}. {"February","Febrer"}. -{"Fill in fields to search for any matching Jabber User","Emplena camps per a buscar usuaris Jabber que concorden"}. -{"Fill in the form to search for any matching Jabber User (Add * to the end of field to match substring)","Emplena el formulari per a buscar usuaris Jabber. Afegix * al final d'un camp per a buscar subcadenes."}. +{"File larger than ~w bytes","El fitxer es més gran que ~w bytes"}. +{"Fill in the form to search for any matching XMPP User","Emplena camps per a buscar usuaris XMPP que concorden"}. {"Friday","Divendres"}. -{"From","De"}. -{"From ~s","De ~s"}. +{"From ~ts","De ~ts"}. +{"Full List of Room Admins","Llista completa de administradors de la sala"}. +{"Full List of Room Owners","Llista completa de propietaris de la sala"}. {"Full Name","Nom complet"}. +{"Get List of Online Users","Obté la llista d'usuaris en línia"}. +{"Get List of Registered Users","Obté la llista d'usuaris registrats"}. {"Get Number of Online Users","Obtenir Número d'Usuaris Connectats"}. {"Get Number of Registered Users","Obtenir Número d'Usuaris Registrats"}. +{"Get Pending","Obtenir Pendents"}. {"Get User Last Login Time","Obtenir la última connexió d'Usuari"}. -{"Get User Password","Obtenir Contrasenya d'usuari"}. {"Get User Statistics","Obtenir Estadístiques d'Usuari"}. +{"Given Name","Nom propi"}. {"Grant voice to this person?","Concedir veu a aquesta persona?"}. -{"Group ","Grup "}. -{"Groups","Grups"}. -{"has been banned","Has sigut banejat"}. -{"has been kicked because of an affiliation change","Has sigut expulsat a causa d'un canvi d'afiliació"}. -{"has been kicked because of a system shutdown","Has sigut expulsat perquè el sistema s'ha apagat"}. -{"has been kicked because the room has been changed to members-only","Has sigut expulsat perquè la sala ha canviat a sols membres"}. -{"has been kicked","Has sigut expulsat"}. -{" has set the subject to: "," ha posat l'assumpte: "}. -{"Host","Host"}. +{"has been banned","ha sigut bloquejat"}. +{"has been kicked because of a system shutdown","ha sigut expulsat perquè el sistema va a apagar-se"}. +{"has been kicked because of an affiliation change","ha sigut expulsat a causa d'un canvi d'afiliació"}. +{"has been kicked because the room has been changed to members-only","ha sigut expulsat perquè la sala ara és només per a membres"}. +{"has been kicked","ha sigut expulsat"}. +{"Hash of the vCard-temp avatar of this room","Hash del avatar a vCard-temp d'esta sala"}. +{"Hat title","Títol del barret"}. +{"Hat URI","URI del barret"}. +{"Hats limit exceeded","El límit de tràfic ha sigut sobrepassat"}. +{"Host unknown","Host desconegut"}. +{"HTTP File Upload","HTTP File Upload"}. +{"Idle connection","Connexió sense us"}. {"If you don't see the CAPTCHA image here, visit the web page.","Si no veus la imatge CAPTCHA açí, visita la pàgina web."}. -{"If you want to specify different ports, passwords, encodings for IRC servers, fill this list with values in format '{\"irc server\", \"encoding\", port, \"password\"}'. By default this service use \"~s\" encoding, port ~p, empty password.","Si vols especificar codificacions de caràcters diferents per a cada servidor IRC emplena aquesta llista amb els valors amb el format '{\"servidor irc\", \"codificació\", port, \"contrasenya\"}'. Aquest servei utilitza per defecte la codificació \"~s\", port ~p, no contrasenya."}. {"Import Directory","Importar directori"}. {"Import File","Importar fitxer"}. -{"Import user data from jabberd14 spool file:","Importar dades d'usuaris de l'arxiu de spool de jabberd14"}. +{"Import user data from jabberd14 spool file:","Importar dades d'usuaris de l'arxiu de spool de jabberd14:"}. {"Import User from File at ","Importa usuari des de fitxer en "}. {"Import users data from a PIEFXIS file (XEP-0227):","Importar dades d'usuaris des d'un arxiu PIEFXIS (XEP-0227):"}. {"Import users data from jabberd14 spool directory:","Importar dades d'usuaris del directori de spool de jabberd14:"}. {"Import Users from Dir at ","Importar usuaris des del directori en "}. {"Import Users From jabberd14 Spool Files","Importar usuaris de jabberd14"}. +{"Improper domain part of 'from' attribute","La part de domini de l'atribut 'from' es impròpia"}. {"Improper message type","Tipus de missatge incorrecte"}. +{"Incorrect CAPTCHA submit","El CAPTCHA proporcionat és incorrecte"}. +{"Incorrect data form","El formulari de dades és incorrecte"}. {"Incorrect password","Contrasenya incorrecta"}. -{"Invalid affiliation: ~s","Afiliació invàlida: ~s"}. -{"Invalid role: ~s","Rol invàlid: ~s"}. +{"Incorrect value of 'action' attribute","Valor incorrecte del atribut 'action'"}. +{"Incorrect value of 'action' in data form","Valor incorrecte de 'action' al formulari de dades"}. +{"Incorrect value of 'path' in data form","Valor incorrecte de 'path' al formulari de dades"}. +{"Installed Modules:","Mòduls instal·lats:"}. +{"Install","Instal·lar"}. +{"Insufficient privilege","Privilegi insuficient"}. +{"Internal server error","Error intern del servidor"}. +{"Invalid 'from' attribute in forwarded message","Atribut 'from' invàlid al missatge reenviat"}. +{"Invalid node name","Nom de node no vàlid"}. +{"Invalid 'previd' value","Valor no vàlid de 'previd'"}. +{"Invitations are not allowed in this conference","Les invitacions no estan permeses en aquesta sala de conferència"}. {"IP addresses","Adreça IP"}. -{"IP","IP"}. -{"IRC channel (don't put the first #)","Canal d'IRC (no posis la primera #)"}. -{"IRC server","Servidor d'IRC"}. -{"IRC settings","Configuració d'IRC."}. -{"IRC Transport","Transport a IRC"}. -{"IRC username","Nom d'usuari al IRC"}. -{"IRC Username","Nom d'usuari al IRC"}. {"is now known as","ara es conegut com"}. -{"It is not allowed to send private messages","No està permés enviar missatges privats"}. +{"It is not allowed to send error messages to the room. The participant (~s) has sent an error message (~s) and got kicked from the room","No està permés enviar missatges d'error a la sala. El participant (~s) ha enviat un missatge d'error (~s) i ha sigut expulsat de la sala"}. {"It is not allowed to send private messages of type \"groupchat\"","No està permés enviar missatges del tipus \"groupchat\""}. {"It is not allowed to send private messages to the conference","No està permès l'enviament de missatges privats a la sala"}. -{"Jabber Account Registration","Registre de compte Jabber"}. {"Jabber ID","ID Jabber"}. -{"Jabber ID ~s is invalid","El Jabber ID ~s no és vàlid"}. {"January","Gener"}. -{"Join IRC channel","Entra a canal d'IRC"}. -{"joins the room","Entrar a la sala"}. -{"Join the IRC channel here.","Entra al canal d'IRC aquí."}. -{"Join the IRC channel in this Jabber ID: ~s","Entra al canal d'IRC en aquesta Jabber ID: ~s"}. +{"JID normalization denied by service policy","S'ha denegat la normalització del JID per política del servei"}. +{"JID normalization failed","Ha fallat la normalització del JID"}. +{"Joined MIX channels of ~ts","Canals MIX units de ~ts"}. +{"Joined MIX channels:","Canals MIX units:"}. +{"joins the room","entra a la sala"}. {"July","Juliol"}. {"June","Juny"}. +{"Just created","Creació recent"}. {"Last Activity","Última activitat"}. {"Last login","Últim login"}. +{"Last message","Últim missatge"}. {"Last month","Últim mes"}. {"Last year","Últim any"}. -{"leaves the room","Deixar la sala"}. -{"Listened Ports at ","Ports a la escolta en "}. -{"Listened Ports","Ports a l'escolta"}. -{"List of modules to start","Llista de mòduls a iniciar"}. -{"Low level update script","Script d'actualització de baix nivell"}. +{"Least significant bits of SHA-256 hash of text should equal hexadecimal label","Els bits menys significants del hash SHA-256 del text deurien ser iguals a l'etiqueta hexadecimal"}. +{"leaves the room","surt de la sala"}. +{"List of users with hats","Llista d'usuaris amb barrets"}. +{"List users with hats","Llista d'usuaris amb barrets"}. +{"Logged Out","Desconectat"}. +{"Logging","Registre"}. {"Make participants list public","Crear una llista de participants pública"}. {"Make room CAPTCHA protected","Crear una sala protegida per CAPTCHA"}. -{"Make room members-only","Crear una sala de \"només membres\""}. +{"Make room members-only","Crear una sala només per a membres"}. {"Make room moderated","Crear una sala moderada"}. {"Make room password protected","Crear una sala amb contrasenya"}. {"Make room persistent","Crear una sala persistent"}. {"Make room public searchable","Crear una sala pública"}. +{"Malformed username","Nom d'usuari mal format"}. +{"MAM preference modification denied by service policy","Se t'ha denegat la modificació de la preferència de MAM per política del servei"}. {"March","Març"}. -{"Maximum Number of Occupants","Número màxim d'ocupants"}. -{"Max # of items to persist","Màxim # d'elements que persistixen"}. +{"Max # of items to persist, or `max` for no specific limit other than a server imposed maximum","Màxim # d'elements a persistir, o `max` per a no tindre altre límit més que el màxim imposat pel servidor"}. {"Max payload size in bytes","Màxim tamany del payload en bytes"}. +{"Maximum file size","Mida màxima de fitxer"}. +{"Maximum Number of History Messages Returned by Room","Numero màxim de missatges de l'historia que retorna la sala"}. +{"Maximum number of items to persist","Número màxim d'elements que persistixen"}. +{"Maximum Number of Occupants","Número màxim d'ocupants"}. {"May","Maig"}. {"Membership is required to enter this room","Necessites ser membre d'aquesta sala per a poder entrar"}. -{"Members:","Membre:"}. -{"Memorize your password, or write it in a paper placed in a safe place. In Jabber there isn't an automated way to recover your password if you forget it.","Memoritza la teva contrasenya, o escriu-la en un paper guardat a un lloc segur.A Jabber no hi ha una forma automatitzada de recuperar la teva contrasenya si la oblides."}. -{"Memory","Memòria"}. +{"Memorize your password, or write it in a paper placed in a safe place. In XMPP there isn't an automated way to recover your password if you forget it.","Memoritza la teva contrasenya, o escriu-la en un paper guardat a un lloc segur. A XMPP no hi ha una forma automatitzada de recuperar la teva contrasenya si la oblides."}. +{"Mere Availability in XMPP (No Show Value)","Simplement disponibilitat a XMPP (sense valor de 'show')"}. {"Message body","Missatge"}. +{"Message not found in forwarded payload","Missatge no trobat al contingut reenviat"}. +{"Messages from strangers are rejected","Els missatges de desconeguts son rebutjats"}. +{"Messages of type headline","Missatges de tipus titular"}. +{"Messages of type normal","Missatges de tipus normal"}. {"Middle Name","Segon nom"}. {"Minimum interval between voice requests (in seconds)","Interval mínim entre peticions de veu (en segons)"}. {"Moderator privileges required","Es necessita tenir privilegis de moderador"}. -{"moderators only","només moderadors"}. -{"Modified modules","Mòduls modificats"}. -{"Module","Mòdul"}. -{"Modules at ","Mòduls en "}. -{"Modules","Mòduls"}. +{"Moderator","Moderador"}. +{"Moderators Only","Només moderadors"}. +{"Module failed to handle the query","El modul ha fallat al gestionar la petició"}. {"Monday","Dilluns"}. -{"Name:","Nom:"}. +{"Multicast","Multicast"}. +{"Multiple elements are not allowed by RFC6121","No estan permesos múltiples elements per RFC6121"}. +{"Multi-User Chat","Multi-Usuari Converses"}. {"Name","Nom"}. +{"Natural Language for Room Discussions","Llengua natural per a les discussions a les sales"}. +{"Natural-Language Room Name","Nom de la sala a la seua llengua natural"}. +{"Neither 'jid' nor 'nick' attribute found","No s'han trobat els atributs 'jid' ni 'nick'"}. +{"Neither 'role' nor 'affiliation' attribute found","No s'han trobat els atributs 'role' ni 'affiliation'"}. {"Never","Mai"}. {"New Password:","Nova Contrasenya:"}. -{"Nickname","Nickname"}. -{"Nickname Registration at ","Registre del Nickname en "}. -{"Nickname ~s does not exist in the room","El Nickname ~s no existeix a la sala"}. -{"nobody","ningú"}. +{"Nickname can't be empty","El sobrenom no pot estar buit"}. +{"Nickname Registration at ","Registre del sobrenom en "}. +{"Nickname ~s does not exist in the room","El sobrenom ~s no existeix a la sala"}. +{"Nickname","Sobrenom"}. +{"No address elements found","No s'han trobat elements d'adreces ('address')"}. +{"No addresses element found","No s'ha trobat l'element d'adreces ('addresses')"}. +{"No 'affiliation' attribute found","No s'ha trobat l'atribut 'affiliation'"}. +{"No available resource found","No s'ha trobat un recurs disponible"}. {"No body provided for announce message","No hi ha proveedor per al missatge anunci"}. +{"No child elements found","No s'han trobat subelements"}. +{"No data form found","No s'ha trobat el formulari de dades"}. {"No Data","No hi ha dades"}. -{"Node ID","ID del Node"}. -{"Node ","Node "}. -{"Node not found","Node no trobat"}. -{"Nodes","Nodes"}. +{"No features available","No n'hi ha característiques disponibles"}. +{"No element found","No s'ha trobat cap element "}. +{"No hook has processed this command","Cap event ha processat este comandament"}. +{"No info about last activity found","No s'ha trobat informació de l'ultima activitat"}. +{"No 'item' element found","No s'ha trobat cap element 'item'"}. +{"No items found in this query","En aquesta petició no s'ha trobat cap element"}. {"No limit","Sense Llímit"}. +{"No module is handling this query","Cap element està manegant esta petició"}. +{"No node specified","No s'ha especificat node"}. +{"No 'password' found in data form","No s'ha trobat 'password' al formulari de dades"}. +{"No 'password' found in this query","No s'ha trobat 'password' en esta petició"}. +{"No 'path' found in data form","No s'ha trobat 'path' en el formulari de dades"}. +{"No pending subscriptions found","No s'han trobat subscripcions pendents"}. +{"No privacy list with this name found","No s'ha trobat cap llista de privacitat amb aquest nom"}. +{"No private data found in this query","No s'ha trobat dades privades en esta petició"}. +{"No running node found","No s'ha trobat node en marxa"}. +{"No services available","No n'hi ha serveis disponibles"}. +{"No statistics found for this item","No n'hi ha estadístiques disponibles per a aquest element"}. +{"No 'to' attribute found in the invitation","No s'ha trobat l'atribut 'to' en la invitació"}. +{"Nobody","Ningú"}. +{"Node already exists","El node ja existeix"}. +{"Node ID","ID del Node"}. +{"Node index not found","Index de node no trobat"}. +{"Node not found","Node no trobat"}. +{"Node ~p","Node ~p"}. +{"Node","Node"}. +{"Nodeprep has failed","Ha fallat Nodeprep"}. +{"Nodes","Nodes"}. {"None","Cap"}. -{"No resource provided","Recurs no disponible"}. +{"Not allowed","No permès"}. {"Not Found","No Trobat"}. +{"Not subscribed","No subscrit"}. {"Notify subscribers when items are removed from the node","Notificar subscriptors quan els elements són eliminats del node"}. {"Notify subscribers when the node configuration changes","Notificar subscriptors quan canvia la configuració del node"}. {"Notify subscribers when the node is deleted","Notificar subscriptors quan el node és eliminat"}. {"November","Novembre"}. +{"Number of answers required","Número de respostes requerides"}. {"Number of occupants","Número d'ocupants"}. +{"Number of Offline Messages","Número de missatges offline"}. {"Number of online users","Número d'usuaris connectats"}. {"Number of registered users","Número d'Usuaris Registrats"}. +{"Number of seconds after which to automatically purge items, or `max` for no specific limit other than a server imposed maximum","Número de segons després dels quals es purgaran automàticament elements, o `max` per a no tindre altre límit més que el màxim imposat pel servidor"}. +{"Occupants are allowed to invite others","Els ocupants poden invitar a altres"}. +{"Occupants are allowed to query others","Els ocupants poden enviar peticions a altres"}. +{"Occupants May Change the Subject","Els ocupants poden canviar el Tema"}. {"October","Octubre"}. -{"Offline Messages:","Missatges fora de línia:"}. -{"Offline Messages","Missatges offline"}. {"OK","Acceptar"}. {"Old Password:","Antiga contrasenya:"}. -{"Online","Connectat"}. {"Online Users","Usuaris conectats"}. -{"Online Users:","Usuaris en línia:"}. +{"Online","Connectat"}. +{"Only collection node owners may associate leaf nodes with the collection","Només els propietaris de la col·lecció de nodes poden associar nodes fulla amb la col·lecció"}. {"Only deliver notifications to available users","Sols enviar notificacions als usuaris disponibles"}. -{"Only moderators and participants are allowed to change the subject in this room","Només els moderadors i participants poden canviar l'assumpte d'aquesta sala"}. -{"Only moderators are allowed to change the subject in this room","Només els moderadors poden canviar l'assumpte d'aquesta sala"}. +{"Only or tags are allowed","Només es permeten etiquetes o "}. +{"Only element is allowed in this query","En esta petició només es permet l'element "}. +{"Only members may query archives of this room","Només membres poden consultar l'arxiu de missatges d'aquesta sala"}. +{"Only moderators and participants are allowed to change the subject in this room","Només els moderadors i participants poden canviar el tema d'aquesta sala"}. +{"Only moderators are allowed to change the subject in this room","Només els moderadors poden canviar el tema d'aquesta sala"}. +{"Only moderators are allowed to retract messages","Només els moderadors tenen permís per a retractar missatges"}. {"Only moderators can approve voice requests","Només els moderadors poden aprovar les peticions de veu"}. {"Only occupants are allowed to send messages to the conference","Sols els ocupants poden enviar missatges a la sala"}. {"Only occupants are allowed to send queries to the conference","Sols els ocupants poden enviar sol·licituds a la sala"}. +{"Only publishers may publish","Només els publicadors poden publicar"}. {"Only service administrators are allowed to send service messages","Sols els administradors del servei tenen permís per a enviar missatges de servei"}. -{"Options","Opcions"}. +{"Only those on a whitelist may associate leaf nodes with the collection","Només qui estiga a una llista blanca pot associar nodes fulla amb la col·lecció"}. +{"Only those on a whitelist may subscribe and retrieve items","Només qui estiga a una llista blanca pot subscriure's i recuperar elements"}. {"Organization Name","Nom de la organizació"}. {"Organization Unit","Unitat de la organizació"}. -{"Outgoing s2s Connections:","Connexions d'eixida s2s"}. +{"Other Modules Available:","Altres mòduls disponibles:"}. {"Outgoing s2s Connections","Connexions s2s d'eixida"}. -{"Outgoing s2s Servers:","Servidors d'eixida de s2s"}. {"Owner privileges required","Es requerixen privilegis de propietari de la sala"}. -{"Packet","Paquet"}. -{"Password ~b","Contrasenya ~b"}. -{"Password:","Contrasenya:"}. -{"Password","Contrasenya"}. -{"Password Verification:","Verificació de la Contrasenya:"}. +{"Packet relay is denied by service policy","S'ha denegat el reenviament del paquet per política del servei"}. +{"Participant ID","ID del Participant"}. +{"Participant","Participant"}. {"Password Verification","Verificació de la Contrasenya"}. +{"Password Verification:","Verificació de la Contrasenya:"}. +{"Password","Contrasenya"}. +{"Password:","Contrasenya:"}. {"Path to Dir","Ruta al directori"}. {"Path to File","Ruta al fitxer"}. -{"Pending","Pendent"}. +{"Payload semantic type information","Informació sobre el tipus semàntic de la carrega útil"}. {"Period: ","Període: "}. {"Persist items to storage","Persistir elements al guardar"}. +{"Persistent","Persistent"}. +{"Ping query is incorrect","La petició de Ping es incorrecta"}. {"Ping","Ping"}. {"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Recorda que aquestes opcions només fan còpia de seguretat de la base de dades Mnesia. Si estàs utilitzant el mòdul d'ODBC també deus de fer una còpia de seguretat de la base de dades de SQL a part."}. {"Please, wait for a while before sending new voice request","Si us plau, espera una mica abans d'enviar una nova petició de veu"}. {"Pong","Pong"}. -{"Port ~b","Port ~b"}. -{"Port","Port"}. +{"Possessing 'ask' attribute is not allowed by RFC6121","Posseir l'atribut 'ask' no està permès per RFC6121"}. {"Present real Jabber IDs to","Presentar Jabber ID's reals a"}. -{"private, ","privat"}. -{"Protocol","Protocol"}. +{"Previous session not found","No s'ha trobat la sessió prèvia"}. +{"Previous session PID has been killed","El procés de la sessió prèvia ha sigut matat"}. +{"Previous session PID has exited","El procés de la sessió prèvia ha sortit"}. +{"Previous session PID is dead","El procés de la sessió prèvia està mort"}. +{"Previous session timed out","La sessió prèvia ha caducat"}. +{"private, ","privat, "}. +{"Public","Public"}. +{"Publish model","Model de publicació"}. {"Publish-Subscribe","Publicar-subscriure't"}. {"PubSub subscriber request","Petició de subscriptor PubSub"}. {"Purge all items when the relevant publisher goes offline","Eliminar tots els elements quan el publicant relevant es desconnecti"}. -{"Queries to the conference members are not allowed in this room"," En aquesta sala no es permeten sol·licituds als membres de la conferència"}. +{"Push record not found","No s'ha trobat l'element Push"}. +{"Queries to the conference members are not allowed in this room","En aquesta sala no es permeten sol·licituds als membres"}. +{"Query to another users is forbidden","Enviar peticions a altres usuaris no està permès"}. {"RAM and disc copy","Còpia en RAM i disc"}. {"RAM copy","Còpia en RAM"}. -{"Raw","en format text"}. {"Really delete message of the day?","Segur que vols eliminar el missatge del dia?"}. +{"Receive notification from all descendent nodes","Rebre notificació de tots els nodes descendents"}. +{"Receive notification from direct child nodes only","Rebre notificació només de nodes fills directes"}. +{"Receive notification of new items only","Rebre notificació només de nous elements"}. +{"Receive notification of new nodes only","Rebre notificació només de nous nodes"}. {"Recipient is not in the conference room","El receptor no està en la sala de conferència"}. -{"Register a Jabber account","Registrar un compte Jabber"}. -{"Registered Users:","Usuaris registrats:"}. -{"Registered Users","Usuaris registrats"}. +{"Register an XMPP account","Registrar un compte XMPP"}. {"Register","Registrar"}. -{"Registration in mod_irc for ","Registre en mod_irc per a"}. {"Remote copy","Còpia remota"}. -{"Remove All Offline Messages","Eliminar tots els missatges offline"}. -{"Remove","Borrar"}. +{"Remove a hat from a user","Eliminar un barret d'un usuari"}. {"Remove User","Eliminar usuari"}. {"Replaced by new connection","Reemplaçat per una nova connexió"}. +{"Request has timed out","La petició ha caducat"}. +{"Request is ignored","La petició ha sigut ignorada"}. +{"Requested role","Rol sol·licitat"}. {"Resources","Recursos"}. -{"Restart","Reiniciar"}. {"Restart Service","Reiniciar el Servei"}. {"Restore Backup from File at ","Restaura còpia de seguretat des del fitxer en "}. {"Restore binary backup after next ejabberd restart (requires less memory):","Restaurar una còpia de seguretat binària després de reiniciar el ejabberd (requereix menys memòria:"}. -{"Restore binary backup immediately:","Restaurar una còpia de seguretat binària ara mateix."}. +{"Restore binary backup immediately:","Restaurar una còpia de seguretat binària ara mateix:"}. {"Restore plain text backup immediately:","Restaurar una còpia de seguretat en format de text pla ara mateix:"}. {"Restore","Restaurar"}. +{"Result","Resultat"}. +{"Roles and Affiliations that May Retrieve Member List","Rols i Afiliacions que poden recuperar la llista de membres"}. +{"Roles for which Presence is Broadcasted","Rols per als que sí se difon la seua presencia"}. +{"Roles that May Send Private Messages","Rols que poden enviar missatges privats"}. {"Room Configuration","Configuració de la sala"}. {"Room creation is denied by service policy","Se t'ha denegat el crear la sala per política del servei"}. -{"Room description","Descripció de la sala:"}. -{"Room Occupants","Nombre d'ocupants"}. +{"Room description","Descripció de la sala"}. +{"Room Occupants","Ocupants de la sala"}. +{"Room terminates","La sala està terminant"}. {"Room title","Títol de la sala"}. {"Roster groups allowed to subscribe","Llista de grups que tenen permés subscriures"}. -{"Roster","Llista de contactes"}. -{"Roster of ","Llista de contactes de "}. -{"Roster size","Tamany de la llista"}. -{"RPC Call Error","Error de cridada RPC"}. +{"Roster size","Mida de la llista"}. {"Running Nodes","Nodes funcionant"}. -{"~s access rule configuration","Configuració de les Regles d'Accés ~s"}. +{"~s invites you to the room ~s","~s et convida a la sala ~s"}. {"Saturday","Dissabte"}. -{"Script check","Comprovar script"}. -{"Search Results for ","Resultat de la búsqueda"}. +{"Search from the date","Buscar des de la data"}. +{"Search Results for ","Resultats de la búsqueda "}. +{"Search the text","Buscar el text"}. +{"Search until the date","Buscar fins la data"}. {"Search users in ","Cerca usuaris en "}. -{"Send announcement to all online users","Enviar anunci a tots els usuaris connectats"}. {"Send announcement to all online users on all hosts","Enviar anunci a tots els usuaris connectats a tots els hosts"}. -{"Send announcement to all users","Enviar anunci a tots els usuaris"}. +{"Send announcement to all online users","Enviar anunci a tots els usuaris connectats"}. {"Send announcement to all users on all hosts","Enviar anunci a tots els usuaris de tots els hosts"}. +{"Send announcement to all users","Enviar anunci a tots els usuaris"}. {"September","Setembre"}. -{"Server ~b","Servidor ~b"}. {"Server:","Servidor:"}. +{"Service list retrieval timed out","L'intent de recuperar la llista de serveis ha caducat"}. +{"Session state copying timed out","La copia del estat de la sessió ha caducat"}. {"Set message of the day and send to online users","Configurar el missatge del dia i enviar a tots els usuaris"}. {"Set message of the day on all hosts and send to online users","Escriure missatge del dia en tots els hosts i enviar-ho als usuaris connectats"}. {"Shared Roster Groups","Grups de contactes compartits"}. {"Show Integral Table","Mostrar Taula Integral"}. +{"Show Occupants Join/Leave","Mostrar Entrades/Eixides dels Ocupants"}. {"Show Ordinary Table","Mostrar Taula Ordinaria"}. {"Shut Down Service","Apager el Servei"}. -{"~s invites you to the room ~s","~s et convida a la sala ~s"}. -{"Some Jabber clients can store your password in your computer. Use that feature only if you trust your computer is safe.","Alguns clients Jabber poden emmagatzemar la teva contrasenya al teu ordinador. Fes servir aquesta característica només si saps que el teu ordinador és segur."}. +{"SOCKS5 Bytestreams","SOCKS5 Bytestreams"}. +{"Some XMPP clients can store your password in the computer, but you should do this only in your personal computer for safety reasons.","Alguns clients XMPP poden emmagatzemar la teva contrasenya al ordinador, però només hauries de fer això al teu ordinador personal, per raons de seguretat."}. +{"Sources Specs:","Especificacions de Codi Font:"}. {"Specify the access model","Especificar el model d'accés"}. {"Specify the event message type","Especifica el tipus de missatge d'event"}. {"Specify the publisher model","Especificar el model del publicant"}. -{"~s's Offline Messages Queue","~s's cua de missatges offline"}. -{"Start","Iniciar"}. -{"Start Modules at ","Iniciar mòduls en "}. -{"Start Modules","Iniciar mòduls"}. -{"Statistics","Estadístiques"}. -{"Statistics of ~p","Estadístiques de ~p"}. -{"Stop","Detindre"}. -{"Stop Modules at ","Detindre mòduls en "}. -{"Stop Modules","Parar mòduls"}. +{"Stanza id is not valid","L'identificador del paquet no es vàlid"}. +{"Stanza ID","ID del paquet"}. +{"Statically specify a replyto of the node owner(s)","Especifica estaticament una adreça on respondre al propietari del node"}. {"Stopped Nodes","Nodes parats"}. -{"Storage Type","Tipus d'emmagatzematge"}. {"Store binary backup:","Guardar una còpia de seguretat binària:"}. {"Store plain text backup:","Guardar una còpia de seguretat en format de text pla:"}. -{"Subject","Assumpte"}. -{"Submit","Enviar"}. +{"Stream management is already enabled","L'administració de la connexió (stream management) ja està activada"}. +{"Stream management is not enabled","L'administració de la conexió (stream management) no està activada"}. +{"Subject","Tema"}. {"Submitted","Enviat"}. {"Subscriber Address","Adreça del Subscriptor"}. -{"Subscription","Subscripció"}. +{"Subscribers may publish","Els subscriptors poden publicar"}. +{"Subscription requests must be approved and only subscribers may retrieve items","Les peticiones de subscripció han de ser aprovades i només els subscriptors poden recuperar elements"}. +{"Subscriptions are not allowed","Les subscripcions no estan permeses"}. {"Sunday","Diumenge"}. -{"That nickname is already in use by another occupant","El Nickname està siguent utilitzat per una altra persona"}. -{"That nickname is registered by another person","El nickname ja està registrat per una altra persona"}. +{"Text associated with a picture","Text associat amb una imatge"}. +{"Text associated with a sound","Text associat amb un so"}. +{"Text associated with a video","Text associat amb un vídeo"}. +{"Text associated with speech","Text associat amb una veu"}. +{"That nickname is already in use by another occupant","El sobrenom ja l'està utilitzant una altra persona"}. +{"That nickname is registered by another person","El sobrenom ja està registrat per una altra persona"}. +{"The account already exists","El compte ha existeix"}. +{"The account was not unregistered","El compte no ha sigut esborrat"}. +{"The body text of the last received message","El contingut del text de l'ultim missatge rebut"}. {"The CAPTCHA is valid.","El CAPTCHA es vàlid."}. {"The CAPTCHA verification has failed","La verificació CAPTCHA ha fallat"}. +{"The captcha you entered is wrong","El CAPTCHA que has proporcionat és incorrecte"}. +{"The child nodes (leaf or collection) associated with a collection","El nodes fills (fulla o col·leccions) associats amb una col·lecció"}. {"The collections with which a node is affiliated","Les col.leccions amb les que un node està afiliat"}. -{"the password is","la contrasenya és"}. +{"The DateTime at which a leased subscription will end or has ended","La Data i Hora a la que una subscripció prestada terminarà o ha terminat"}. +{"The datetime when the node was created","La data i hora a la que un node va ser creat"}. +{"The default language of the node","El llenguatge per defecte d'un node"}. +{"The feature requested is not supported by the conference","La característica sol·licitada no està suportada per la sala de conferència"}. +{"The JID of the node creator","El JID del creador del node"}. +{"The JIDs of those to contact with questions","Els JIDs a qui contactar amb preguntes"}. +{"The JIDs of those with an affiliation of owner","Els JIDs de qui tenen una afiliació de propietaris"}. +{"The JIDs of those with an affiliation of publisher","Els JIDs de qui tenen una afiliació de publicadors"}. +{"The list of all online users","La llista de tots els usuaris en línia"}. +{"The list of all users","La llista de tots els usuaris"}. +{"The list of JIDs that may associate leaf nodes with a collection","La llista de JIDs que poden associar nodes fulla amb una col·lecció"}. +{"The maximum number of child nodes that can be associated with a collection, or `max` for no specific limit other than a server imposed maximum","El màxim número de nodes fills que poden associar-se amb una col·lecció, o `max` per a no tindre altre límit més que el màxim imposat pel servidor"}. +{"The minimum number of milliseconds between sending any two notification digests","El número mínim de mil·lisegons entre l'enviament de dos resums de notificacions"}. +{"The name of the node","El nom del node"}. +{"The node is a collection node","El node es una col·lecció"}. +{"The node is a leaf node (default)","El node es un node fulla (per defecte)"}. +{"The NodeID of the relevant node","El NodeID del node rellevant"}. +{"The number of pending incoming presence subscription requests","El número de peticions rebudes de subscripció de presencia pendents"}. +{"The number of subscribers to the node","El número de subscriptors al node"}. +{"The number of unread or undelivered messages","El número de missatges no llegits o no enviats"}. +{"The password contains unacceptable characters","La contrasenya conté caràcters inacceptables"}. {"The password is too weak","La contrasenya és massa simple"}. -{"The password of your Jabber account was successfully changed.","La contrasenya del teu compte Jabber s'ha canviat correctament."}. +{"the password is","la contrasenya és"}. +{"The password of your XMPP account was successfully changed.","La contrasenya del teu compte XMPP s'ha canviat correctament."}. +{"The password was not changed","La contrasenya no ha sigut canviada"}. +{"The passwords are different","Les contrasenyes son diferents"}. +{"The presence states for which an entity wants to receive notifications","El estats de presencia per als quals una entitat vol rebre notificacions"}. +{"The query is only allowed from local users","La petició està permesa només d'usuaris locals"}. +{"The query must not contain elements","La petició no pot contenir elements "}. +{"The room subject can be modified by participants","El tema de la sala pot modificar-lo els participants"}. +{"The semantic type information of data in the node, usually specified by the namespace of the payload (if any)","La informació semàntica de les dades al node, usualment especificat pel espai de noms de la càrrega util (si n'hi ha)"}. +{"The sender of the last received message","Qui ha enviat l'ultim missatge rebut"}. +{"The stanza MUST contain only one element, one element, or one element","El paquet DEU contindre només un element , un element , o un element "}. +{"The subscription identifier associated with the subscription request","L'identificador de subscripció associat amb la petició de subscripció"}. +{"The URL of an XSL transformation which can be applied to payloads in order to generate an appropriate message body element.","La URL de uns transformació XSL que pot ser aplicada als payloads per a generar un element apropiat de contingut de missatge."}. +{"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","La URL de una transformació XSL que pot ser aplicada al format de payload per a generar un resultat valid de Data Forms, que el client puga mostrar usant un métode generic de Data Forms"}. {"There was an error changing the password: ","Hi ha hagut un error canviant la contrasenya: "}. {"There was an error creating the account: ","Hi ha hagut un error creant el compte: "}. {"There was an error deleting the account: ","Hi ha hagut un error esborrant el compte: "}. {"This is case insensitive: macbeth is the same that MacBeth and Macbeth.","Això no distingeix majúscules de minúscules: macbeth es el mateix que MacBeth i Macbeth."}. -{"This page allows to create a Jabber account in this Jabber server. Your JID (Jabber IDentifier) will be of the form: username@server. Please read carefully the instructions to fill correctly the fields.","Aquesta pàgina permet crear un compte Jabber en aquest servidor Jabber. El teu JID (Jabber IDentifier; Identificador Jabber) tindrà aquesta forma: usuari@servidor. Si us plau, llegeix amb cura les instruccions per emplenar correctament els camps."}. -{"This page allows to unregister a Jabber account in this Jabber server.","Aquesta pàgina permet anul·lar el registre d'un compte Jabber en aquest servidor Jabber."}. -{"This participant is kicked from the room because he sent an error message","Aquest participant ha sigut expulsat de la sala perque ha enviat un missatge d'error"}. -{"This participant is kicked from the room because he sent an error message to another participant","Aquest participant ha sigut expulsat de la sala perque ha enviat un missatge erroni a un altre participant"}. -{"This participant is kicked from the room because he sent an error presence","Aquest participant ha sigut expulsat de la sala perque ha enviat un error de presencia"}. +{"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.","Aquesta pàgina permet crear un compte XMPP en aquest servidor XMPP. El teu JID (Jabber ID; Identificador Jabber) tindrà aquesta forma: usuari@servidor. Si us plau, llegeix amb cura les instruccions per emplenar correctament els camps."}. +{"This page allows to unregister an XMPP account in this XMPP server.","Aquesta pàgina permet esborrar un compte XMPP en aquest servidor XMPP."}. {"This room is not anonymous","Aquesta sala no és anònima"}. +{"This service can not process the address: ~s","Este servei no pot processar la direcció: ~s"}. {"Thursday","Dijous"}. -{"Time","Data"}. {"Time delay","Temps de retard"}. +{"Timed out waiting for stream resumption","Massa temps esperant que es resumisca la connexió"}. +{"To register, visit ~s","Per a registrar-te, visita ~s"}. +{"To ~ts","A ~ts"}. +{"Token TTL","Token TTL"}. +{"Too many active bytestreams","N'hi ha massa Bytestreams actius"}. {"Too many CAPTCHA requests","Massa peticions de CAPTCHA"}. -{"To","Per a"}. -{"To ~s","A ~s"}. -{"Traffic rate limit is exceeded","El llímit de tràfic ha sigut sobrepassat"}. -{"Transactions Aborted:","Transaccions Avortades"}. -{"Transactions Committed:","Transaccions Realitzades:"}. -{"Transactions Logged:","Transaccions registrades"}. -{"Transactions Restarted:","Transaccions reiniciades"}. +{"Too many child elements","N'hi ha massa subelements"}. +{"Too many elements","N'hi ha massa elements "}. +{"Too many elements","N'hi ha massa elements "}. +{"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","Massa autenticacions (~p) han fallat des d'aquesta adreça IP (~s). L'adreça serà desbloquejada en ~s UTC"}. +{"Too many receiver fields were specified","S'han especificat massa camps de receptors"}. +{"Too many unacked stanzas","Massa missatges sense haver reconegut la seva recepció"}. +{"Too many users in this conference","N'hi ha massa usuaris en esta sala de conferència"}. +{"Traffic rate limit is exceeded","El límit de tràfic ha sigut sobrepassat"}. +{"~ts's MAM Archive","Arxiu MAM de ~ts"}. +{"~ts's Offline Messages Queue","~ts's cua de missatges offline"}. {"Tuesday","Dimarts"}. {"Unable to generate a CAPTCHA","No s'ha pogut generar un CAPTCHA"}. +{"Unable to register route on existing local domain","No s'ha pogut registrar la ruta al domini local existent"}. {"Unauthorized","No autoritzat"}. -{"Unregister a Jabber account","Anul·lar el registre d'un compte Jabber"}. +{"Unexpected action","Acció inesperada"}. +{"Unexpected error condition: ~p","Condició d'error inesperada: ~p"}. +{"Uninstall","Desinstal·lar"}. +{"Unregister an XMPP account","Anul·lar el registre d'un compte XMPP"}. {"Unregister","Anul·lar el registre"}. -{"Update ","Actualitzar"}. -{"Update","Actualitzar"}. +{"Unsupported element","Element no soportat"}. +{"Unsupported version","Versió no suportada"}. {"Update message of the day (don't send)","Actualitzar el missatge del dia (no enviar)"}. {"Update message of the day on all hosts (don't send)","Actualitza el missatge del dia en tots els hosts (no enviar)"}. -{"Update plan","Pla d'actualització"}. -{"Update script","Script d'actualització"}. -{"Uptime:","Temps en marxa"}. -{"Use of STARTTLS required","És obligatori utilitzar STARTTLS"}. -{"User JID","JID del usuari "}. +{"Update specs to get modules source, then install desired ones.","Actualitza les especificacions per obtindre el codi font dels mòduls, després instal·la els que vulgues."}. +{"Update Specs","Actualitzar Especificacions"}. +{"Updating the vCard is not supported by the vCard storage backend","El sistema d'almacenament de vCard no te capacitat per a actualitzar la vCard"}. +{"Upgrade","Actualitza"}. +{"URL for Archived Discussion Logs","URL dels Arxius de Discussions"}. +{"User already exists","El usuari ja existeix"}. +{"User JID","JID del usuari"}. +{"User (jid)","Usuari (jid)"}. {"User Management","Gestió d'Usuaris"}. +{"User not allowed to perform an IQ set on another user's vCard.","L'usuari no te permis per a modificar la vCard d'altre usuari."}. +{"User removed","Usuari borrat"}. +{"User session not found","Sessió d'usuari no trobada"}. +{"User session terminated","Sessió d'usuari terminada"}. +{"User ~ts","Usuari ~ts"}. {"Username:","Nom d'usuari:"}. {"Users are not allowed to register accounts so quickly","Els usuaris no tenen permís per a crear comptes tan depresa"}. {"Users Last Activity","Última activitat d'usuari"}. {"Users","Usuaris"}. -{"User ","Usuari "}. {"User","Usuari"}. -{"Validate","Validar"}. -{"vCard User Search","Recerca de vCard d'usuari"}. +{"Value 'get' of 'type' attribute is not allowed","El valor 'get' a l'atribut 'type' no és permès"}. +{"Value of '~s' should be boolean","El valor de '~s' deuria ser booleà"}. +{"Value of '~s' should be datetime string","El valor de '~s' deuria ser una data"}. +{"Value of '~s' should be integer","El valor de '~s' deuria ser un numero enter"}. +{"Value 'set' of 'type' attribute is not allowed","El valor 'set' a l'atribut 'type' no és permès"}. +{"vCard User Search","vCard recerca d'usuari"}. +{"View joined MIX channels","Vore els canals MIX units"}. {"Virtual Hosts","Hosts virtuals"}. {"Visitors are not allowed to change their nicknames in this room","Els visitants no tenen permés canviar el seus Nicknames en esta sala"}. {"Visitors are not allowed to send messages to all occupants","Els visitants no poden enviar missatges a tots els ocupants"}. +{"Visitor","Visitant"}. {"Voice request","Petició de veu"}. {"Voice requests are disabled in this conference","Les peticions de veu es troben desactivades en aquesta conferència"}. +{"Web client which allows to join the room anonymously","Client web que permet entrar a la sala anonimament"}. {"Wednesday","Dimecres"}. +{"When a new subscription is processed and whenever a subscriber comes online","Quan es processa una nova subscripció i un subscriptor es connecta"}. +{"When a new subscription is processed","Quan es processa una nova subscripció"}. {"When to send the last published item","Quan s'ha enviat l'última publicació"}. -{"Whether to allow subscriptions","Permetre subscripcions"}. -{"You can later change your password using a Jabber client.","Podràs canviar la teva contrasenya més endavant utilitzant un client Jabber."}. +{"Whether an entity wants to receive an XMPP message body in addition to the payload format","Si una entitat vol rebre un missatge XMPP amb el format payload"}. +{"Whether an entity wants to receive digests (aggregations) of notifications or all notifications individually","Si una entitat vol rebre resums (agregacions) de notificacions o totes les notificacions individualment"}. +{"Whether an entity wants to receive or disable notifications","Si una entitat vol rebre notificacions o no"}. +{"Whether owners or publisher should receive replies to items","Si el propietaris o publicadors deurien de rebre respostes als elements"}. +{"Whether the node is a leaf (default) or a collection","Si el node es fulla (per defecte) o es una col·lecció"}. +{"Whether to allow subscriptions","Si s'hauria de permetre subscripcions"}. +{"Whether to make all subscriptions temporary, based on subscriber presence","Si fer totes les subscripcions temporals, basat en la presencia del subscriptor"}. +{"Whether to notify owners about new subscribers and unsubscribes","Si notificar als propietaris sobre noves subscripcions i desubscripcions"}. +{"Who can send private messages","Qui pot enviar missatges privats"}. +{"Who may associate leaf nodes with a collection","Qui pot associar nodes fulla amb una col·lecció"}. +{"Wrong parameters in the web formulary","Paràmetres incorrectes en el formulari web"}. +{"Wrong xmlns","El xmlns ès incorrecte"}. +{"XMPP Account Registration","Registre de compte XMPP"}. +{"XMPP Domains","Dominis XMPP"}. +{"XMPP Show Value of Away","Valor 'show' de XMPP: Ausent"}. +{"XMPP Show Value of Chat","Valor 'show' de XMPP: Disposat per a xarrar"}. +{"XMPP Show Value of DND (Do Not Disturb)","Valor 'show' de XMPP: DND (No Molestar)"}. +{"XMPP Show Value of XA (Extended Away)","Valor 'show' de XMPP: XA (Molt Ausent)"}. +{"XMPP URI of Associated Publish-Subscribe Node","URI XMPP del Node Associat Publish-Subscribe"}. +{"You are being removed from the room because of a system shutdown","Has sigut expulsat de la sala perquè el sistema va a apagar-se"}. +{"You are not allowed to send private messages","No tens permés enviar missatges privats"}. +{"You are not joined to the channel","No t'has unit al canal"}. +{"You can later change your password using an XMPP client.","Podràs canviar la teva contrasenya més endavant utilitzant un client XMPP."}. {"You have been banned from this room","Has sigut bloquejat en aquesta sala"}. +{"You have joined too many conferences","Has entrat en massa sales de conferència"}. {"You must fill in field \"Nickname\" in the form","Deus d'omplir el camp \"Nickname\" al formulari"}. {"You need a client that supports x:data and CAPTCHA to register","Necessites un client amb suport x:data i de CAPTCHA para poder registrar-te"}. -{"You need a client that supports x:data to register the nickname","Necessites un client amb suport x:data per a poder registrar el Nickname"}. -{"You need an x:data capable client to configure mod_irc settings","Necessites un client amb suport x:data per a configurar les opcions de mod_irc"}. -{"You need an x:data capable client to configure room","Necessites un client amb suport x:data per a configurar la sala"}. +{"You need a client that supports x:data to register the nickname","Necessites un client amb suport x:data per a poder registrar el sobrenom"}. {"You need an x:data capable client to search","Necessites un client amb suport x:data per a poder buscar"}. {"Your active privacy list has denied the routing of this stanza.","La teva llista de privacitat activa ha denegat l'encaminament d'aquesta stanza."}. -{"Your contact offline message queue is full. The message has been discarded.","La cua de missatges offline és plena. El missatge ha sigut descartat"}. -{"Your Jabber account was successfully created.","El teu compte Jabber ha sigut creat correctament."}. -{"Your Jabber account was successfully deleted.","El teu compte Jabber ha sigut esborrat correctament."}. -{"Your messages to ~s are being blocked. To unblock them, visit ~s","Els teus missatges per ~s s'estan bloquejant. Per desbloquejar-los, visita ~s"}. +{"Your contact offline message queue is full. The message has been discarded.","La teua cua de missatges offline és plena. El missatge ha sigut descartat."}. +{"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","La teua petició de subscripció i/o missatges a ~s han sigut bloquejats. Per a desbloquejar-los, visita ~s"}. +{"Your XMPP account was successfully registered.","El teu compte XMPP ha sigut creat correctament."}. +{"Your XMPP account was successfully unregistered.","El teu compte XMPP ha sigut esborrat correctament."}. +{"You're not allowed to create nodes","No tens permís per a crear nodes"}. diff --git a/priv/msgs/ca.po b/priv/msgs/ca.po deleted file mode 100644 index 412962bba..000000000 --- a/priv/msgs/ca.po +++ /dev/null @@ -1,1872 +0,0 @@ -# Jan, 2012. -msgid "" -msgstr "" -"Project-Id-Version: 2.1.0-alpha\n" -"POT-Creation-Date: \n" -"PO-Revision-Date: 2012-04-22 00:19+0200\n" -"Last-Translator: JanKusanagi\n" -"Language-Team: American English \n" -"Language: en_US\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Language: Catalan (català)\n" -"X-Additional-Translator: Vicent Alberola Canet\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" -"X-Generator: Lokalize 1.4\n" - -#: ejabberd_c2s.erl:424 ejabberd_c2s.erl:727 -msgid "Use of STARTTLS required" -msgstr "És obligatori utilitzar STARTTLS" - -#: ejabberd_c2s.erl:503 -msgid "No resource provided" -msgstr "Recurs no disponible" - -#: ejabberd_c2s.erl:1197 -msgid "Replaced by new connection" -msgstr "Reemplaçat per una nova connexió" - -#: ejabberd_c2s.erl:1885 -msgid "Your active privacy list has denied the routing of this stanza." -msgstr "" -"La teva llista de privacitat activa ha denegat l'encaminament d'aquesta " -"stanza." - -#: ejabberd_captcha.erl:96 ejabberd_captcha.erl:152 ejabberd_captcha.erl:178 -msgid "Enter the text you see" -msgstr "Introdueix el text que veus" - -#: ejabberd_captcha.erl:101 -msgid "Your messages to ~s are being blocked. To unblock them, visit ~s" -msgstr "" -"Els teus missatges per ~s s'estan bloquejant. Per desbloquejar-los, visita ~s" - -#: ejabberd_captcha.erl:134 -msgid "If you don't see the CAPTCHA image here, visit the web page." -msgstr "Si no veus la imatge CAPTCHA açí, visita la pàgina web." - -#: ejabberd_captcha.erl:146 -msgid "CAPTCHA web page" -msgstr "Pàgina web del CAPTCHA" - -#: ejabberd_captcha.erl:307 -msgid "The CAPTCHA is valid." -msgstr "El CAPTCHA es vàlid." - -#: mod_adhoc.erl:95 mod_adhoc.erl:125 mod_adhoc.erl:143 mod_adhoc.erl:161 -msgid "Commands" -msgstr "Comandaments" - -#: mod_adhoc.erl:149 mod_adhoc.erl:243 -msgid "Ping" -msgstr "Ping" - -#: mod_adhoc.erl:260 -msgid "Pong" -msgstr "Pong" - -#: mod_announce.erl:507 mod_announce_odbc.erl:499 -msgid "Really delete message of the day?" -msgstr "Segur que vols eliminar el missatge del dia?" - -#: mod_announce.erl:515 mod_announce_odbc.erl:507 mod_configure.erl:1083 -#: mod_configure.erl:1128 -msgid "Subject" -msgstr "Assumpte" - -#: mod_announce.erl:520 mod_announce_odbc.erl:512 mod_configure.erl:1088 -#: mod_configure.erl:1133 -msgid "Message body" -msgstr "Missatge" - -#: mod_announce.erl:600 mod_announce_odbc.erl:592 -msgid "No body provided for announce message" -msgstr "No hi ha proveedor per al missatge anunci" - -#: mod_announce.erl:635 mod_announce_odbc.erl:627 -msgid "Announcements" -msgstr "Anuncis" - -#: mod_announce.erl:637 mod_announce_odbc.erl:629 -msgid "Send announcement to all users" -msgstr "Enviar anunci a tots els usuaris" - -#: mod_announce.erl:639 mod_announce_odbc.erl:631 -msgid "Send announcement to all users on all hosts" -msgstr "Enviar anunci a tots els usuaris de tots els hosts" - -#: mod_announce.erl:641 mod_announce_odbc.erl:633 -msgid "Send announcement to all online users" -msgstr "Enviar anunci a tots els usuaris connectats" - -#: mod_announce.erl:643 mod_announce_odbc.erl:635 mod_configure.erl:1078 -#: mod_configure.erl:1123 -msgid "Send announcement to all online users on all hosts" -msgstr "Enviar anunci a tots els usuaris connectats a tots els hosts" - -#: mod_announce.erl:645 mod_announce_odbc.erl:637 -msgid "Set message of the day and send to online users" -msgstr "Configurar el missatge del dia i enviar a tots els usuaris" - -#: mod_announce.erl:647 mod_announce_odbc.erl:639 -msgid "Set message of the day on all hosts and send to online users" -msgstr "" -"Escriure missatge del dia en tots els hosts i enviar-ho als usuaris " -"connectats" - -#: mod_announce.erl:649 mod_announce_odbc.erl:641 -msgid "Update message of the day (don't send)" -msgstr "Actualitzar el missatge del dia (no enviar)" - -#: mod_announce.erl:651 mod_announce_odbc.erl:643 -msgid "Update message of the day on all hosts (don't send)" -msgstr "Actualitza el missatge del dia en tots els hosts (no enviar)" - -#: mod_announce.erl:653 mod_announce_odbc.erl:645 -msgid "Delete message of the day" -msgstr "Eliminar el missatge del dia" - -#: mod_announce.erl:655 mod_announce_odbc.erl:647 -msgid "Delete message of the day on all hosts" -msgstr "Elimina el missatge del dis de tots els hosts" - -#: mod_configure.erl:114 mod_configure.erl:274 mod_configure.erl:296 -#: mod_configure.erl:498 -msgid "Configuration" -msgstr "Configuració" - -#: mod_configure.erl:125 mod_configure.erl:576 web/ejabberd_web_admin.erl:1936 -msgid "Database" -msgstr "Base de dades" - -#: mod_configure.erl:127 mod_configure.erl:595 -msgid "Start Modules" -msgstr "Iniciar mòduls" - -#: mod_configure.erl:129 mod_configure.erl:596 -msgid "Stop Modules" -msgstr "Parar mòduls" - -#: mod_configure.erl:131 mod_configure.erl:604 web/ejabberd_web_admin.erl:1937 -msgid "Backup" -msgstr "Guardar còpia de seguretat" - -#: mod_configure.erl:133 mod_configure.erl:605 -msgid "Restore" -msgstr "Restaurar" - -#: mod_configure.erl:135 mod_configure.erl:606 -msgid "Dump to Text File" -msgstr "Exportar a fitxer de text" - -#: mod_configure.erl:137 mod_configure.erl:615 -msgid "Import File" -msgstr "Importar fitxer" - -#: mod_configure.erl:139 mod_configure.erl:616 -msgid "Import Directory" -msgstr "Importar directori" - -#: mod_configure.erl:141 mod_configure.erl:581 mod_configure.erl:1057 -msgid "Restart Service" -msgstr "Reiniciar el Servei" - -#: mod_configure.erl:143 mod_configure.erl:582 mod_configure.erl:1102 -msgid "Shut Down Service" -msgstr "Apager el Servei" - -#: mod_configure.erl:145 mod_configure.erl:518 mod_configure.erl:1197 -#: web/ejabberd_web_admin.erl:1527 -msgid "Add User" -msgstr "Afegir usuari" - -#: mod_configure.erl:147 mod_configure.erl:519 mod_configure.erl:1219 -msgid "Delete User" -msgstr "Eliminar Usuari" - -#: mod_configure.erl:149 mod_configure.erl:520 mod_configure.erl:1231 -msgid "End User Session" -msgstr "Finalitzar Sesió d'Usuari" - -#: mod_configure.erl:151 mod_configure.erl:521 mod_configure.erl:1243 -#: mod_configure.erl:1255 -msgid "Get User Password" -msgstr "Obtenir Contrasenya d'usuari" - -#: mod_configure.erl:153 mod_configure.erl:522 -msgid "Change User Password" -msgstr "Canviar Contrasenya d'Usuari" - -#: mod_configure.erl:155 mod_configure.erl:523 mod_configure.erl:1272 -msgid "Get User Last Login Time" -msgstr "Obtenir la última connexió d'Usuari" - -#: mod_configure.erl:157 mod_configure.erl:524 mod_configure.erl:1284 -msgid "Get User Statistics" -msgstr "Obtenir Estadístiques d'Usuari" - -#: mod_configure.erl:159 mod_configure.erl:525 -msgid "Get Number of Registered Users" -msgstr "Obtenir Número d'Usuaris Registrats" - -#: mod_configure.erl:161 mod_configure.erl:526 -msgid "Get Number of Online Users" -msgstr "Obtenir Número d'Usuaris Connectats" - -#: mod_configure.erl:163 mod_configure.erl:509 web/ejabberd_web_admin.erl:831 -#: web/ejabberd_web_admin.erl:872 -msgid "Access Control Lists" -msgstr "Llista de Control d'Accés" - -#: mod_configure.erl:165 mod_configure.erl:510 web/ejabberd_web_admin.erl:940 -#: web/ejabberd_web_admin.erl:976 -msgid "Access Rules" -msgstr "Regles d'Accés" - -#: mod_configure.erl:297 mod_configure.erl:499 -msgid "User Management" -msgstr "Gestió d'Usuaris" - -#: mod_configure.erl:500 web/ejabberd_web_admin.erl:1058 -#: web/ejabberd_web_admin.erl:1462 -msgid "Online Users" -msgstr "Usuaris conectats" - -#: mod_configure.erl:501 -msgid "All Users" -msgstr "Tots els usuaris" - -#: mod_configure.erl:502 -msgid "Outgoing s2s Connections" -msgstr "Connexions s2s d'eixida" - -#: mod_configure.erl:503 web/ejabberd_web_admin.erl:1907 -msgid "Running Nodes" -msgstr "Nodes funcionant" - -#: mod_configure.erl:504 web/ejabberd_web_admin.erl:1909 -msgid "Stopped Nodes" -msgstr "Nodes parats" - -#: mod_configure.erl:577 mod_configure.erl:587 web/ejabberd_web_admin.erl:1953 -msgid "Modules" -msgstr "Mòduls" - -#: mod_configure.erl:578 -msgid "Backup Management" -msgstr "Gestió de còpia de seguretat" - -#: mod_configure.erl:579 -msgid "Import Users From jabberd14 Spool Files" -msgstr "Importar usuaris de jabberd14" - -#: mod_configure.erl:699 -msgid "To ~s" -msgstr "A ~s" - -#: mod_configure.erl:717 -msgid "From ~s" -msgstr "De ~s" - -#: mod_configure.erl:913 -msgid "Database Tables Configuration at " -msgstr "Configuració de la base de dades en " - -#: mod_configure.erl:918 -msgid "Choose storage type of tables" -msgstr "Selecciona el tipus d'almacenament de les taules" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Disc only copy" -msgstr "Còpia sols en disc" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM and disc copy" -msgstr "Còpia en RAM i disc" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM copy" -msgstr "Còpia en RAM" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Remote copy" -msgstr "Còpia remota" - -#: mod_configure.erl:950 -msgid "Stop Modules at " -msgstr "Detindre mòduls en " - -#: mod_configure.erl:954 -msgid "Choose modules to stop" -msgstr "Selecciona mòduls a detindre" - -#: mod_configure.erl:969 -msgid "Start Modules at " -msgstr "Iniciar mòduls en " - -#: mod_configure.erl:973 -msgid "Enter list of {Module, [Options]}" -msgstr "Introdueix llista de {mòdul, [opcions]}" - -#: mod_configure.erl:974 -msgid "List of modules to start" -msgstr "Llista de mòduls a iniciar" - -#: mod_configure.erl:983 -msgid "Backup to File at " -msgstr "Desar còpia de seguretat a fitxer en " - -#: mod_configure.erl:987 mod_configure.erl:1001 -msgid "Enter path to backup file" -msgstr "Introdueix ruta al fitxer de còpia de seguretat" - -#: mod_configure.erl:988 mod_configure.erl:1002 mod_configure.erl:1016 -#: mod_configure.erl:1030 -msgid "Path to File" -msgstr "Ruta al fitxer" - -#: mod_configure.erl:997 -msgid "Restore Backup from File at " -msgstr "Restaura còpia de seguretat des del fitxer en " - -#: mod_configure.erl:1011 -msgid "Dump Backup to Text File at " -msgstr "Exporta còpia de seguretat a fitxer de text en " - -#: mod_configure.erl:1015 -msgid "Enter path to text file" -msgstr "Introdueix ruta al fitxer de text" - -#: mod_configure.erl:1025 -msgid "Import User from File at " -msgstr "Importa usuari des de fitxer en " - -#: mod_configure.erl:1029 -msgid "Enter path to jabberd14 spool file" -msgstr "Introdueix ruta al fitxer jabberd14 spool" - -#: mod_configure.erl:1039 -msgid "Import Users from Dir at " -msgstr "Importar usuaris des del directori en " - -#: mod_configure.erl:1043 -msgid "Enter path to jabberd14 spool dir" -msgstr "Introdueix la ruta al directori de jabberd14 spools" - -#: mod_configure.erl:1044 -msgid "Path to Dir" -msgstr "Ruta al directori" - -#: mod_configure.erl:1060 mod_configure.erl:1105 -msgid "Time delay" -msgstr "Temps de retard" - -#: mod_configure.erl:1143 -msgid "Access Control List Configuration" -msgstr "Configuració de la Llista de Control d'Accés" - -#: mod_configure.erl:1147 -msgid "Access control lists" -msgstr "Llistes de Control de Accés" - -#: mod_configure.erl:1171 -msgid "Access Configuration" -msgstr "Configuració d'accesos" - -#: mod_configure.erl:1175 -msgid "Access rules" -msgstr "Regles d'accés" - -#: mod_configure.erl:1200 mod_configure.erl:1222 mod_configure.erl:1234 -#: mod_configure.erl:1246 mod_configure.erl:1258 mod_configure.erl:1275 -#: mod_configure.erl:1287 mod_configure.erl:1650 mod_configure.erl:1700 -#: mod_configure.erl:1721 mod_roster.erl:943 mod_roster_odbc.erl:1060 -#: mod_vcard.erl:472 mod_vcard_ldap.erl:554 mod_vcard_odbc.erl:448 -msgid "Jabber ID" -msgstr "ID Jabber" - -#: mod_configure.erl:1205 mod_configure.erl:1263 mod_configure.erl:1651 -#: mod_configure.erl:1863 mod_muc/mod_muc_room.erl:3224 mod_register.erl:235 -#: web/ejabberd_web_admin.erl:1520 -msgid "Password" -msgstr "Contrasenya" - -#: mod_configure.erl:1210 -msgid "Password Verification" -msgstr "Verificació de la Contrasenya" - -#: mod_configure.erl:1301 -msgid "Number of registered users" -msgstr "Número d'Usuaris Registrats" - -#: mod_configure.erl:1315 -msgid "Number of online users" -msgstr "Número d'usuaris connectats" - -#: mod_configure.erl:1682 web/ejabberd_web_admin.erl:1588 -#: web/ejabberd_web_admin.erl:1743 -msgid "Never" -msgstr "Mai" - -#: mod_configure.erl:1696 web/ejabberd_web_admin.erl:1601 -#: web/ejabberd_web_admin.erl:1756 -msgid "Online" -msgstr "Connectat" - -#: mod_configure.erl:1701 -msgid "Last login" -msgstr "Últim login" - -#: mod_configure.erl:1722 -msgid "Roster size" -msgstr "Tamany de la llista" - -#: mod_configure.erl:1723 -msgid "IP addresses" -msgstr "Adreça IP" - -#: mod_configure.erl:1724 -msgid "Resources" -msgstr "Recursos" - -#: mod_configure.erl:1850 -msgid "Administration of " -msgstr "Administració de " - -#: mod_configure.erl:1853 -msgid "Action on user" -msgstr "Acció en l'usuari" - -#: mod_configure.erl:1857 -msgid "Edit Properties" -msgstr "Editar propietats" - -#: mod_configure.erl:1860 web/ejabberd_web_admin.erl:1769 -msgid "Remove User" -msgstr "Eliminar usuari" - -#: mod_irc/mod_irc.erl:201 mod_irc/mod_irc_odbc.erl:196 -#: mod_muc/mod_muc.erl:336 mod_muc/mod_muc_odbc.erl:342 -msgid "Access denied by service policy" -msgstr "Accés denegat per la política del servei" - -#: mod_irc/mod_irc.erl:401 mod_irc/mod_irc_odbc.erl:398 -msgid "IRC Transport" -msgstr "Transport a IRC" - -#: mod_irc/mod_irc.erl:428 mod_irc/mod_irc_odbc.erl:425 -msgid "ejabberd IRC module" -msgstr "mòdul ejabberd IRC" - -#: mod_irc/mod_irc.erl:559 mod_irc/mod_irc_odbc.erl:568 -msgid "You need an x:data capable client to configure mod_irc settings" -msgstr "" -"Necessites un client amb suport x:data per a configurar les opcions de " -"mod_irc" - -#: mod_irc/mod_irc.erl:566 mod_irc/mod_irc_odbc.erl:575 -msgid "Registration in mod_irc for " -msgstr "Registre en mod_irc per a" - -#: mod_irc/mod_irc.erl:571 mod_irc/mod_irc_odbc.erl:580 -msgid "" -"Enter username, encodings, ports and passwords you wish to use for " -"connecting to IRC servers" -msgstr "" -"Introdueix el nom d'usuari, les codificacions de caràcters, els ports i " -"contrasenyes per a utilitzar al connectar als servidors de IRC" - -#: mod_irc/mod_irc.erl:576 mod_irc/mod_irc_odbc.erl:585 -msgid "IRC Username" -msgstr "Nom d'usuari al IRC" - -#: mod_irc/mod_irc.erl:586 mod_irc/mod_irc_odbc.erl:595 -msgid "" -"If you want to specify different ports, passwords, encodings for IRC " -"servers, fill this list with values in format '{\"irc server\", \"encoding" -"\", port, \"password\"}'. By default this service use \"~s\" encoding, port " -"~p, empty password." -msgstr "" -"Si vols especificar codificacions de caràcters diferents per a cada servidor " -"IRC emplena aquesta llista amb els valors amb el format '{\"servidor irc\", " -"\"codificació\", port, \"contrasenya\"}'. Aquest servei utilitza per " -"defecte la codificació \"~s\", port ~p, no contrasenya." - -#: mod_irc/mod_irc.erl:598 mod_irc/mod_irc_odbc.erl:607 -msgid "" -"Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef." -"net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]." -msgstr "" -"Exemple: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef." -"net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]." - -#: mod_irc/mod_irc.erl:603 mod_irc/mod_irc_odbc.erl:612 -msgid "Connections parameters" -msgstr "Paràmetres de connexió" - -#: mod_irc/mod_irc.erl:728 mod_irc/mod_irc_odbc.erl:757 -msgid "Join IRC channel" -msgstr "Entra a canal d'IRC" - -#: mod_irc/mod_irc.erl:732 mod_irc/mod_irc_odbc.erl:761 -msgid "IRC channel (don't put the first #)" -msgstr "Canal d'IRC (no posis la primera #)" - -#: mod_irc/mod_irc.erl:737 mod_irc/mod_irc_odbc.erl:766 -msgid "IRC server" -msgstr "Servidor d'IRC" - -#: mod_irc/mod_irc.erl:770 mod_irc/mod_irc.erl:774 -#: mod_irc/mod_irc_odbc.erl:799 mod_irc/mod_irc_odbc.erl:803 -msgid "Join the IRC channel here." -msgstr "Entra al canal d'IRC aquí." - -#: mod_irc/mod_irc.erl:778 mod_irc/mod_irc_odbc.erl:807 -msgid "Join the IRC channel in this Jabber ID: ~s" -msgstr "Entra al canal d'IRC en aquesta Jabber ID: ~s" - -#: mod_irc/mod_irc.erl:863 mod_irc/mod_irc_odbc.erl:904 -msgid "IRC settings" -msgstr "Configuració d'IRC." - -#: mod_irc/mod_irc.erl:868 mod_irc/mod_irc_odbc.erl:909 -msgid "" -"Enter username and encodings you wish to use for connecting to IRC servers. " -"Press 'Next' to get more fields to fill in. Press 'Complete' to save " -"settings." -msgstr "" -"Introdueix el nom d'usuari i les codificacions de caràcters per a utilitzar " -"als servidors de IRC. Apreta \"Seguent\" per veure més caps per omplir. " -"Apreta \"Completar\" per guardar la configuració. " - -#: mod_irc/mod_irc.erl:874 mod_irc/mod_irc_odbc.erl:915 -msgid "IRC username" -msgstr "Nom d'usuari al IRC" - -#: mod_irc/mod_irc.erl:923 mod_irc/mod_irc_odbc.erl:964 -msgid "Password ~b" -msgstr "Contrasenya ~b" - -#: mod_irc/mod_irc.erl:928 mod_irc/mod_irc_odbc.erl:969 -msgid "Port ~b" -msgstr "Port ~b" - -#: mod_irc/mod_irc.erl:933 mod_irc/mod_irc_odbc.erl:974 -msgid "Encoding for server ~b" -msgstr "Codificació pel servidor ~b" - -#: mod_irc/mod_irc.erl:942 mod_irc/mod_irc_odbc.erl:983 -msgid "Server ~b" -msgstr "Servidor ~b" - -#: mod_muc/mod_muc.erl:449 mod_muc/mod_muc_odbc.erl:456 -msgid "Only service administrators are allowed to send service messages" -msgstr "" -"Sols els administradors del servei tenen permís per a enviar missatges de " -"servei" - -#: mod_muc/mod_muc.erl:493 mod_muc/mod_muc_odbc.erl:500 -msgid "Room creation is denied by service policy" -msgstr "Se t'ha denegat el crear la sala per política del servei" - -#: mod_muc/mod_muc.erl:500 mod_muc/mod_muc_odbc.erl:507 -msgid "Conference room does not exist" -msgstr "La sala de conferències no existeix" - -#: mod_muc/mod_muc.erl:582 mod_muc/mod_muc_odbc.erl:595 -msgid "Chatrooms" -msgstr "Sales de xat" - -#: mod_muc/mod_muc.erl:712 mod_muc/mod_muc_odbc.erl:729 -msgid "You need a client that supports x:data to register the nickname" -msgstr "" -"Necessites un client amb suport x:data per a poder registrar el Nickname" - -#: mod_muc/mod_muc.erl:718 mod_muc/mod_muc_odbc.erl:735 -msgid "Nickname Registration at " -msgstr "Registre del Nickname en " - -#: mod_muc/mod_muc.erl:722 mod_muc/mod_muc_odbc.erl:739 -msgid "Enter nickname you want to register" -msgstr "Introdueix el nickname que vols registrar" - -#: mod_muc/mod_muc.erl:723 mod_muc/mod_muc_odbc.erl:740 -#: mod_muc/mod_muc_room.erl:3810 mod_roster.erl:944 mod_roster_odbc.erl:1061 -#: mod_vcard.erl:364 mod_vcard.erl:477 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:453 -msgid "Nickname" -msgstr "Nickname" - -#: mod_muc/mod_muc.erl:762 mod_muc/mod_muc_odbc.erl:784 -#: mod_muc/mod_muc_room.erl:1064 mod_muc/mod_muc_room.erl:1787 -msgid "That nickname is registered by another person" -msgstr "El nickname ja està registrat per una altra persona" - -#: mod_muc/mod_muc.erl:788 mod_muc/mod_muc_odbc.erl:811 -msgid "You must fill in field \"Nickname\" in the form" -msgstr "Deus d'omplir el camp \"Nickname\" al formulari" - -#: mod_muc/mod_muc.erl:808 mod_muc/mod_muc_odbc.erl:831 -msgid "ejabberd MUC module" -msgstr "mòdul ejabberd MUC" - -#: mod_muc/mod_muc_log.erl:374 mod_muc/mod_muc_log.erl:381 -msgid "Chatroom configuration modified" -msgstr "Configuració de la sala de xat modificada" - -#: mod_muc/mod_muc_log.erl:384 -msgid "joins the room" -msgstr "Entrar a la sala" - -#: mod_muc/mod_muc_log.erl:387 mod_muc/mod_muc_log.erl:390 -msgid "leaves the room" -msgstr "Deixar la sala" - -#: mod_muc/mod_muc_log.erl:393 mod_muc/mod_muc_log.erl:396 -msgid "has been banned" -msgstr "Has sigut banejat" - -#: mod_muc/mod_muc_log.erl:399 mod_muc/mod_muc_log.erl:402 -msgid "has been kicked" -msgstr "Has sigut expulsat" - -#: mod_muc/mod_muc_log.erl:405 -msgid "has been kicked because of an affiliation change" -msgstr "Has sigut expulsat a causa d'un canvi d'afiliació" - -#: mod_muc/mod_muc_log.erl:408 -msgid "has been kicked because the room has been changed to members-only" -msgstr "Has sigut expulsat perquè la sala ha canviat a sols membres" - -#: mod_muc/mod_muc_log.erl:411 -msgid "has been kicked because of a system shutdown" -msgstr "Has sigut expulsat perquè el sistema s'ha apagat" - -#: mod_muc/mod_muc_log.erl:414 -msgid "is now known as" -msgstr "ara es conegut com" - -#: mod_muc/mod_muc_log.erl:417 mod_muc/mod_muc_log.erl:688 -#: mod_muc/mod_muc_room.erl:2393 -msgid " has set the subject to: " -msgstr " ha posat l'assumpte: " - -#: mod_muc/mod_muc_log.erl:452 -msgid "Chatroom is created" -msgstr "La sala s'ha creat" - -#: mod_muc/mod_muc_log.erl:453 -msgid "Chatroom is destroyed" -msgstr "La sala s'ha destruït" - -#: mod_muc/mod_muc_log.erl:454 -msgid "Chatroom is started" -msgstr "La sala s'ha iniciat" - -#: mod_muc/mod_muc_log.erl:455 -msgid "Chatroom is stopped" -msgstr "La sala s'ha aturat" - -#: mod_muc/mod_muc_log.erl:459 -msgid "Monday" -msgstr "Dilluns" - -#: mod_muc/mod_muc_log.erl:460 -msgid "Tuesday" -msgstr "Dimarts" - -#: mod_muc/mod_muc_log.erl:461 -msgid "Wednesday" -msgstr "Dimecres" - -#: mod_muc/mod_muc_log.erl:462 -msgid "Thursday" -msgstr "Dijous" - -#: mod_muc/mod_muc_log.erl:463 -msgid "Friday" -msgstr "Divendres" - -#: mod_muc/mod_muc_log.erl:464 -msgid "Saturday" -msgstr "Dissabte" - -#: mod_muc/mod_muc_log.erl:465 -msgid "Sunday" -msgstr "Diumenge" - -#: mod_muc/mod_muc_log.erl:469 -msgid "January" -msgstr "Gener" - -#: mod_muc/mod_muc_log.erl:470 -msgid "February" -msgstr "Febrer" - -#: mod_muc/mod_muc_log.erl:471 -msgid "March" -msgstr "Març" - -#: mod_muc/mod_muc_log.erl:472 -msgid "April" -msgstr "Abril" - -#: mod_muc/mod_muc_log.erl:473 -msgid "May" -msgstr "Maig" - -#: mod_muc/mod_muc_log.erl:474 -msgid "June" -msgstr "Juny" - -#: mod_muc/mod_muc_log.erl:475 -msgid "July" -msgstr "Juliol" - -#: mod_muc/mod_muc_log.erl:476 -msgid "August" -msgstr "Agost" - -#: mod_muc/mod_muc_log.erl:477 -msgid "September" -msgstr "Setembre" - -#: mod_muc/mod_muc_log.erl:478 -msgid "October" -msgstr "Octubre" - -#: mod_muc/mod_muc_log.erl:479 -msgid "November" -msgstr "Novembre" - -#: mod_muc/mod_muc_log.erl:480 -msgid "December" -msgstr "Decembre" - -#: mod_muc/mod_muc_log.erl:750 -msgid "Room Configuration" -msgstr "Configuració de la sala" - -#: mod_muc/mod_muc_log.erl:759 -msgid "Room Occupants" -msgstr "Nombre d'ocupants" - -#: mod_muc/mod_muc_room.erl:174 -msgid "Traffic rate limit is exceeded" -msgstr "El llímit de tràfic ha sigut sobrepassat" - -#: mod_muc/mod_muc_room.erl:246 -msgid "" -"This participant is kicked from the room because he sent an error message" -msgstr "" -"Aquest participant ha sigut expulsat de la sala perque ha enviat un missatge " -"d'error" - -#: mod_muc/mod_muc_room.erl:255 -msgid "It is not allowed to send private messages to the conference" -msgstr "No està permès l'enviament de missatges privats a la sala" - -#: mod_muc/mod_muc_room.erl:332 -msgid "Please, wait for a while before sending new voice request" -msgstr "Si us plau, espera una mica abans d'enviar una nova petició de veu" - -#: mod_muc/mod_muc_room.erl:347 -msgid "Voice requests are disabled in this conference" -msgstr "Les peticions de veu es troben desactivades en aquesta conferència" - -#: mod_muc/mod_muc_room.erl:364 -msgid "Failed to extract JID from your voice request approval" -msgstr "No s'ha pogut extraure el JID de la teva aprovació de petició de veu" - -#: mod_muc/mod_muc_room.erl:393 -msgid "Only moderators can approve voice requests" -msgstr "Només els moderadors poden aprovar les peticions de veu" - -#: mod_muc/mod_muc_room.erl:408 -msgid "Improper message type" -msgstr "Tipus de missatge incorrecte" - -#: mod_muc/mod_muc_room.erl:518 -msgid "" -"This participant is kicked from the room because he sent an error message to " -"another participant" -msgstr "" -"Aquest participant ha sigut expulsat de la sala perque ha enviat un missatge " -"erroni a un altre participant" - -#: mod_muc/mod_muc_room.erl:531 -msgid "It is not allowed to send private messages of type \"groupchat\"" -msgstr "No està permés enviar missatges del tipus \"groupchat\"" - -#: mod_muc/mod_muc_room.erl:543 mod_muc/mod_muc_room.erl:611 -msgid "Recipient is not in the conference room" -msgstr "El receptor no està en la sala de conferència" - -#: mod_muc/mod_muc_room.erl:564 mod_muc/mod_muc_room.erl:585 -msgid "It is not allowed to send private messages" -msgstr "No està permés enviar missatges privats" - -#: mod_muc/mod_muc_room.erl:576 mod_muc/mod_muc_room.erl:956 -#: mod_muc/mod_muc_room.erl:4040 -msgid "Only occupants are allowed to send messages to the conference" -msgstr "Sols els ocupants poden enviar missatges a la sala" - -#: mod_muc/mod_muc_room.erl:634 -msgid "Only occupants are allowed to send queries to the conference" -msgstr "Sols els ocupants poden enviar sol·licituds a la sala" - -#: mod_muc/mod_muc_room.erl:646 -msgid "Queries to the conference members are not allowed in this room" -msgstr "" -" En aquesta sala no es permeten sol·licituds als membres de la conferència" - -#: mod_muc/mod_muc_room.erl:932 -msgid "" -"Only moderators and participants are allowed to change the subject in this " -"room" -msgstr "" -"Només els moderadors i participants poden canviar l'assumpte d'aquesta sala" - -#: mod_muc/mod_muc_room.erl:937 -msgid "Only moderators are allowed to change the subject in this room" -msgstr "Només els moderadors poden canviar l'assumpte d'aquesta sala" - -#: mod_muc/mod_muc_room.erl:947 -msgid "Visitors are not allowed to send messages to all occupants" -msgstr "Els visitants no poden enviar missatges a tots els ocupants" - -#: mod_muc/mod_muc_room.erl:1021 -msgid "" -"This participant is kicked from the room because he sent an error presence" -msgstr "" -"Aquest participant ha sigut expulsat de la sala perque ha enviat un error de " -"presencia" - -#: mod_muc/mod_muc_room.erl:1040 -msgid "Visitors are not allowed to change their nicknames in this room" -msgstr "Els visitants no tenen permés canviar el seus Nicknames en esta sala" - -#: mod_muc/mod_muc_room.erl:1053 mod_muc/mod_muc_room.erl:1779 -msgid "That nickname is already in use by another occupant" -msgstr "El Nickname està siguent utilitzat per una altra persona" - -#: mod_muc/mod_muc_room.erl:1768 -msgid "You have been banned from this room" -msgstr "Has sigut bloquejat en aquesta sala" - -#: mod_muc/mod_muc_room.erl:1771 -msgid "Membership is required to enter this room" -msgstr "Necessites ser membre d'aquesta sala per a poder entrar" - -#: mod_muc/mod_muc_room.erl:1807 -msgid "This room is not anonymous" -msgstr "Aquesta sala no és anònima" - -#: mod_muc/mod_muc_room.erl:1833 -msgid "A password is required to enter this room" -msgstr "Es necessita contrasenya per a entrar en aquesta sala" - -#: mod_muc/mod_muc_room.erl:1855 mod_register.erl:246 -msgid "Too many CAPTCHA requests" -msgstr "Massa peticions de CAPTCHA" - -#: mod_muc/mod_muc_room.erl:1864 mod_register.erl:251 -msgid "Unable to generate a CAPTCHA" -msgstr "No s'ha pogut generar un CAPTCHA" - -#: mod_muc/mod_muc_room.erl:1874 -msgid "Incorrect password" -msgstr "Contrasenya incorrecta" - -#: mod_muc/mod_muc_room.erl:2448 -msgid "Administrator privileges required" -msgstr "Es necessita tenir privilegis d'administrador" - -#: mod_muc/mod_muc_room.erl:2463 -msgid "Moderator privileges required" -msgstr "Es necessita tenir privilegis de moderador" - -#: mod_muc/mod_muc_room.erl:2619 -msgid "Jabber ID ~s is invalid" -msgstr "El Jabber ID ~s no és vàlid" - -#: mod_muc/mod_muc_room.erl:2633 -msgid "Nickname ~s does not exist in the room" -msgstr "El Nickname ~s no existeix a la sala" - -#: mod_muc/mod_muc_room.erl:2659 mod_muc/mod_muc_room.erl:3049 -msgid "Invalid affiliation: ~s" -msgstr "Afiliació invàlida: ~s" - -#: mod_muc/mod_muc_room.erl:2713 -msgid "Invalid role: ~s" -msgstr "Rol invàlid: ~s" - -#: mod_muc/mod_muc_room.erl:3026 mod_muc/mod_muc_room.erl:3062 -msgid "Owner privileges required" -msgstr "Es requerixen privilegis de propietari de la sala" - -#: mod_muc/mod_muc_room.erl:3195 -msgid "Configuration of room ~s" -msgstr "Configuració de la sala ~s" - -#: mod_muc/mod_muc_room.erl:3200 -msgid "Room title" -msgstr "Títol de la sala" - -#: mod_muc/mod_muc_room.erl:3203 mod_muc/mod_muc_room.erl:3692 -msgid "Room description" -msgstr "Descripció de la sala:" - -#: mod_muc/mod_muc_room.erl:3210 -msgid "Make room persistent" -msgstr "Crear una sala persistent" - -#: mod_muc/mod_muc_room.erl:3215 -msgid "Make room public searchable" -msgstr "Crear una sala pública" - -#: mod_muc/mod_muc_room.erl:3218 -msgid "Make participants list public" -msgstr "Crear una llista de participants pública" - -#: mod_muc/mod_muc_room.erl:3221 -msgid "Make room password protected" -msgstr "Crear una sala amb contrasenya" - -#: mod_muc/mod_muc_room.erl:3232 -msgid "Maximum Number of Occupants" -msgstr "Número màxim d'ocupants" - -#: mod_muc/mod_muc_room.erl:3239 -msgid "No limit" -msgstr "Sense Llímit" - -#: mod_muc/mod_muc_room.erl:3250 -msgid "Present real Jabber IDs to" -msgstr "Presentar Jabber ID's reals a" - -#: mod_muc/mod_muc_room.erl:3258 mod_muc/mod_muc_room.erl:3292 -msgid "moderators only" -msgstr "només moderadors" - -#: mod_muc/mod_muc_room.erl:3260 mod_muc/mod_muc_room.erl:3294 -msgid "anyone" -msgstr "qualsevol" - -#: mod_muc/mod_muc_room.erl:3262 -msgid "Make room members-only" -msgstr "Crear una sala de \"només membres\"" - -#: mod_muc/mod_muc_room.erl:3265 -msgid "Make room moderated" -msgstr "Crear una sala moderada" - -#: mod_muc/mod_muc_room.erl:3268 -msgid "Default users as participants" -msgstr "Els usuaris són participants per defecte" - -#: mod_muc/mod_muc_room.erl:3271 -msgid "Allow users to change the subject" -msgstr "Permetre que els usuaris canviin el tema" - -#: mod_muc/mod_muc_room.erl:3274 -msgid "Allow users to send private messages" -msgstr "Permetre que els usuaris envien missatges privats" - -#: mod_muc/mod_muc_room.erl:3279 -msgid "Allow visitors to send private messages to" -msgstr "Permetre als visitants enviar missatges privats a" - -#: mod_muc/mod_muc_room.erl:3290 -msgid "nobody" -msgstr "ningú" - -#: mod_muc/mod_muc_room.erl:3296 -msgid "Allow users to query other users" -msgstr "Permetre que els usuaris fagen peticions a altres usuaris" - -#: mod_muc/mod_muc_room.erl:3299 -msgid "Allow users to send invites" -msgstr "Permetre que els usuaris envien invitacions" - -#: mod_muc/mod_muc_room.erl:3302 -msgid "Allow visitors to send status text in presence updates" -msgstr "" -"Permetre als visitants enviar text d'estat en les actualitzacions de " -"presència" - -#: mod_muc/mod_muc_room.erl:3305 -msgid "Allow visitors to change nickname" -msgstr "Permetre als visitants canviar el Nickname" - -#: mod_muc/mod_muc_room.erl:3308 -msgid "Allow visitors to send voice requests" -msgstr "Permetre als visitants enviar peticions de veu" - -#: mod_muc/mod_muc_room.erl:3311 -msgid "Minimum interval between voice requests (in seconds)" -msgstr "Interval mínim entre peticions de veu (en segons)" - -#: mod_muc/mod_muc_room.erl:3317 -msgid "Make room CAPTCHA protected" -msgstr "Crear una sala protegida per CAPTCHA" - -#: mod_muc/mod_muc_room.erl:3322 -msgid "Exclude Jabber IDs from CAPTCHA challenge" -msgstr "Excloure Jabber IDs de la comprovació CAPTCHA" - -#: mod_muc/mod_muc_room.erl:3329 -msgid "Enable logging" -msgstr "Habilitar el registre de la conversa" - -#: mod_muc/mod_muc_room.erl:3337 -msgid "You need an x:data capable client to configure room" -msgstr "Necessites un client amb suport x:data per a configurar la sala" - -#: mod_muc/mod_muc_room.erl:3694 -msgid "Number of occupants" -msgstr "Número d'ocupants" - -#: mod_muc/mod_muc_room.erl:3750 -msgid "private, " -msgstr "privat" - -#: mod_muc/mod_muc_room.erl:3799 -msgid "Voice request" -msgstr "Petició de veu" - -#: mod_muc/mod_muc_room.erl:3803 -msgid "Either approve or decline the voice request." -msgstr "Aprova o denega la petició de veu" - -#: mod_muc/mod_muc_room.erl:3809 -msgid "User JID" -msgstr "JID del usuari " - -#: mod_muc/mod_muc_room.erl:3811 -msgid "Grant voice to this person?" -msgstr "Concedir veu a aquesta persona?" - -#: mod_muc/mod_muc_room.erl:3960 -msgid "~s invites you to the room ~s" -msgstr "~s et convida a la sala ~s" - -#: mod_muc/mod_muc_room.erl:3969 -msgid "the password is" -msgstr "la contrasenya és" - -#: mod_offline.erl:510 mod_offline_odbc.erl:352 -msgid "" -"Your contact offline message queue is full. The message has been discarded." -msgstr "La cua de missatges offline és plena. El missatge ha sigut descartat" - -#: mod_offline.erl:560 mod_offline_odbc.erl:408 -msgid "~s's Offline Messages Queue" -msgstr "~s's cua de missatges offline" - -#: mod_offline.erl:563 mod_offline_odbc.erl:411 mod_roster.erl:987 -#: mod_roster_odbc.erl:1104 mod_shared_roster.erl:901 -#: mod_shared_roster.erl:1010 mod_shared_roster_odbc.erl:930 -#: mod_shared_roster_odbc.erl:1039 web/ejabberd_web_admin.erl:833 -#: web/ejabberd_web_admin.erl:874 web/ejabberd_web_admin.erl:942 -#: web/ejabberd_web_admin.erl:978 web/ejabberd_web_admin.erl:1019 -#: web/ejabberd_web_admin.erl:1508 web/ejabberd_web_admin.erl:1760 -#: web/ejabberd_web_admin.erl:1931 web/ejabberd_web_admin.erl:1963 -#: web/ejabberd_web_admin.erl:2031 web/ejabberd_web_admin.erl:2135 -#: web/ejabberd_web_admin.erl:2160 web/ejabberd_web_admin.erl:2248 -msgid "Submitted" -msgstr "Enviat" - -#: mod_offline.erl:571 -msgid "Time" -msgstr "Data" - -#: mod_offline.erl:572 -msgid "From" -msgstr "De" - -#: mod_offline.erl:573 -msgid "To" -msgstr "Per a" - -#: mod_offline.erl:574 mod_offline_odbc.erl:419 -msgid "Packet" -msgstr "Paquet" - -#: mod_offline.erl:587 mod_offline_odbc.erl:432 mod_shared_roster.erl:908 -#: mod_shared_roster_odbc.erl:937 web/ejabberd_web_admin.erl:882 -#: web/ejabberd_web_admin.erl:986 -msgid "Delete Selected" -msgstr "Eliminar els seleccionats" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -msgid "Offline Messages:" -msgstr "Missatges fora de línia:" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -msgid "Remove All Offline Messages" -msgstr "Eliminar tots els missatges offline" - -#: mod_proxy65/mod_proxy65_service.erl:213 -msgid "ejabberd SOCKS5 Bytestreams module" -msgstr "mòdul ejabberd SOCKS5 Bytestreams" - -#: mod_pubsub/mod_pubsub.erl:1120 mod_pubsub/mod_pubsub_odbc.erl:929 -msgid "Publish-Subscribe" -msgstr "Publicar-subscriure't" - -#: mod_pubsub/mod_pubsub.erl:1214 mod_pubsub/mod_pubsub_odbc.erl:1025 -msgid "ejabberd Publish-Subscribe module" -msgstr "Mòdul ejabberd Publicar-Subscriure" - -#: mod_pubsub/mod_pubsub.erl:1497 mod_pubsub/mod_pubsub_odbc.erl:1312 -msgid "PubSub subscriber request" -msgstr "Petició de subscriptor PubSub" - -#: mod_pubsub/mod_pubsub.erl:1499 mod_pubsub/mod_pubsub_odbc.erl:1314 -msgid "Choose whether to approve this entity's subscription." -msgstr "Tria si aprova aquesta entitat de subscripció" - -#: mod_pubsub/mod_pubsub.erl:1505 mod_pubsub/mod_pubsub_odbc.erl:1320 -msgid "Node ID" -msgstr "ID del Node" - -#: mod_pubsub/mod_pubsub.erl:1510 mod_pubsub/mod_pubsub_odbc.erl:1325 -msgid "Subscriber Address" -msgstr "Adreça del Subscriptor" - -#: mod_pubsub/mod_pubsub.erl:1516 mod_pubsub/mod_pubsub_odbc.erl:1331 -msgid "Allow this Jabber ID to subscribe to this pubsub node?" -msgstr "Permetre que aquesta Jabber ID es puga subscriure a aquest node pubsub" - -#: mod_pubsub/mod_pubsub.erl:3474 mod_pubsub/mod_pubsub_odbc.erl:3287 -msgid "Deliver payloads with event notifications" -msgstr "Enviar payloads junt a les notificacions d'events" - -#: mod_pubsub/mod_pubsub.erl:3475 mod_pubsub/mod_pubsub_odbc.erl:3288 -msgid "Deliver event notifications" -msgstr "Entrega de notificacions d'events" - -#: mod_pubsub/mod_pubsub.erl:3476 mod_pubsub/mod_pubsub_odbc.erl:3289 -msgid "Notify subscribers when the node configuration changes" -msgstr "Notificar subscriptors quan canvia la configuració del node" - -#: mod_pubsub/mod_pubsub.erl:3477 mod_pubsub/mod_pubsub_odbc.erl:3290 -msgid "Notify subscribers when the node is deleted" -msgstr "Notificar subscriptors quan el node és eliminat" - -#: mod_pubsub/mod_pubsub.erl:3478 mod_pubsub/mod_pubsub_odbc.erl:3291 -msgid "Notify subscribers when items are removed from the node" -msgstr "Notificar subscriptors quan els elements són eliminats del node" - -#: mod_pubsub/mod_pubsub.erl:3479 mod_pubsub/mod_pubsub_odbc.erl:3292 -msgid "Persist items to storage" -msgstr "Persistir elements al guardar" - -#: mod_pubsub/mod_pubsub.erl:3480 mod_pubsub/mod_pubsub_odbc.erl:3293 -msgid "A friendly name for the node" -msgstr "Un nom per al node" - -#: mod_pubsub/mod_pubsub.erl:3481 mod_pubsub/mod_pubsub_odbc.erl:3294 -msgid "Max # of items to persist" -msgstr "Màxim # d'elements que persistixen" - -#: mod_pubsub/mod_pubsub.erl:3482 mod_pubsub/mod_pubsub_odbc.erl:3295 -msgid "Whether to allow subscriptions" -msgstr "Permetre subscripcions" - -#: mod_pubsub/mod_pubsub.erl:3483 mod_pubsub/mod_pubsub_odbc.erl:3296 -msgid "Specify the access model" -msgstr "Especificar el model d'accés" - -#: mod_pubsub/mod_pubsub.erl:3486 mod_pubsub/mod_pubsub_odbc.erl:3299 -msgid "Roster groups allowed to subscribe" -msgstr "Llista de grups que tenen permés subscriures" - -#: mod_pubsub/mod_pubsub.erl:3487 mod_pubsub/mod_pubsub_odbc.erl:3300 -msgid "Specify the publisher model" -msgstr "Especificar el model del publicant" - -#: mod_pubsub/mod_pubsub.erl:3489 mod_pubsub/mod_pubsub_odbc.erl:3302 -msgid "Purge all items when the relevant publisher goes offline" -msgstr "Eliminar tots els elements quan el publicant relevant es desconnecti" - -#: mod_pubsub/mod_pubsub.erl:3490 mod_pubsub/mod_pubsub_odbc.erl:3303 -msgid "Specify the event message type" -msgstr "Especifica el tipus de missatge d'event" - -#: mod_pubsub/mod_pubsub.erl:3492 mod_pubsub/mod_pubsub_odbc.erl:3305 -msgid "Max payload size in bytes" -msgstr "Màxim tamany del payload en bytes" - -#: mod_pubsub/mod_pubsub.erl:3493 mod_pubsub/mod_pubsub_odbc.erl:3306 -msgid "When to send the last published item" -msgstr "Quan s'ha enviat l'última publicació" - -#: mod_pubsub/mod_pubsub.erl:3495 mod_pubsub/mod_pubsub_odbc.erl:3308 -msgid "Only deliver notifications to available users" -msgstr "Sols enviar notificacions als usuaris disponibles" - -#: mod_pubsub/mod_pubsub.erl:3496 mod_pubsub/mod_pubsub_odbc.erl:3309 -msgid "The collections with which a node is affiliated" -msgstr "Les col.leccions amb les que un node està afiliat" - -#: mod_register.erl:193 -msgid "The CAPTCHA verification has failed" -msgstr "La verificació CAPTCHA ha fallat" - -#: mod_register.erl:220 -msgid "You need a client that supports x:data and CAPTCHA to register" -msgstr "" -"Necessites un client amb suport x:data i de CAPTCHA para poder registrar-te" - -#: mod_register.erl:226 mod_register.erl:265 -msgid "Choose a username and password to register with this server" -msgstr "Tria nom d'usuari i contrasenya per a registrar-te en aquest servidor" - -#: mod_register.erl:230 mod_vcard.erl:364 mod_vcard_odbc.erl:342 -#: web/ejabberd_web_admin.erl:1515 web/ejabberd_web_admin.erl:1572 -msgid "User" -msgstr "Usuari" - -#: mod_register.erl:316 mod_register.erl:361 -msgid "The password is too weak" -msgstr "La contrasenya és massa simple" - -#: mod_register.erl:365 -msgid "Users are not allowed to register accounts so quickly" -msgstr "Els usuaris no tenen permís per a crear comptes tan depresa" - -#: mod_roster.erl:938 mod_roster_odbc.erl:1055 web/ejabberd_web_admin.erl:1701 -#: web/ejabberd_web_admin.erl:1886 web/ejabberd_web_admin.erl:1897 -#: web/ejabberd_web_admin.erl:2219 -msgid "None" -msgstr "Cap" - -#: mod_roster.erl:945 mod_roster_odbc.erl:1062 -msgid "Subscription" -msgstr "Subscripció" - -#: mod_roster.erl:946 mod_roster_odbc.erl:1063 -msgid "Pending" -msgstr "Pendent" - -#: mod_roster.erl:947 mod_roster_odbc.erl:1064 -msgid "Groups" -msgstr "Grups" - -#: mod_roster.erl:974 mod_roster_odbc.erl:1091 -msgid "Validate" -msgstr "Validar" - -#: mod_roster.erl:982 mod_roster_odbc.erl:1099 -msgid "Remove" -msgstr "Borrar" - -#: mod_roster.erl:985 mod_roster_odbc.erl:1102 -msgid "Roster of " -msgstr "Llista de contactes de " - -#: mod_roster.erl:988 mod_roster_odbc.erl:1105 mod_shared_roster.erl:902 -#: mod_shared_roster.erl:1011 mod_shared_roster_odbc.erl:931 -#: mod_shared_roster_odbc.erl:1040 web/ejabberd_web_admin.erl:834 -#: web/ejabberd_web_admin.erl:875 web/ejabberd_web_admin.erl:943 -#: web/ejabberd_web_admin.erl:979 web/ejabberd_web_admin.erl:1020 -#: web/ejabberd_web_admin.erl:1509 web/ejabberd_web_admin.erl:1761 -#: web/ejabberd_web_admin.erl:1932 web/ejabberd_web_admin.erl:2136 -#: web/ejabberd_web_admin.erl:2161 -msgid "Bad format" -msgstr "Format erroni" - -#: mod_roster.erl:995 mod_roster_odbc.erl:1112 -msgid "Add Jabber ID" -msgstr "Afegir Jabber ID" - -#: mod_roster.erl:1094 mod_roster_odbc.erl:1211 -msgid "Roster" -msgstr "Llista de contactes" - -#: mod_shared_roster.erl:857 mod_shared_roster.erl:899 -#: mod_shared_roster.erl:1007 mod_shared_roster_odbc.erl:886 -#: mod_shared_roster_odbc.erl:928 mod_shared_roster_odbc.erl:1036 -msgid "Shared Roster Groups" -msgstr "Grups de contactes compartits" - -#: mod_shared_roster.erl:895 mod_shared_roster_odbc.erl:924 -#: web/ejabberd_web_admin.erl:1365 web/ejabberd_web_admin.erl:2461 -msgid "Add New" -msgstr "Afegir nou" - -#: mod_shared_roster.erl:978 mod_shared_roster_odbc.erl:1007 -msgid "Name:" -msgstr "Nom:" - -#: mod_shared_roster.erl:983 mod_shared_roster_odbc.erl:1012 -msgid "Description:" -msgstr "Descripció:" - -#: mod_shared_roster.erl:991 mod_shared_roster_odbc.erl:1020 -msgid "Members:" -msgstr "Membre:" - -#: mod_shared_roster.erl:999 mod_shared_roster_odbc.erl:1028 -msgid "Displayed Groups:" -msgstr "Mostrar grups:" - -#: mod_shared_roster.erl:1008 mod_shared_roster_odbc.erl:1037 -msgid "Group " -msgstr "Grup " - -#: mod_shared_roster.erl:1017 mod_shared_roster_odbc.erl:1046 -#: web/ejabberd_web_admin.erl:840 web/ejabberd_web_admin.erl:884 -#: web/ejabberd_web_admin.erl:949 web/ejabberd_web_admin.erl:1026 -#: web/ejabberd_web_admin.erl:2017 -msgid "Submit" -msgstr "Enviar" - -#: mod_vcard.erl:165 mod_vcard_ldap.erl:238 mod_vcard_odbc.erl:129 -msgid "Erlang Jabber Server" -msgstr "Servidor Erlang Jabber" - -#: mod_vcard.erl:364 mod_vcard.erl:478 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:454 -msgid "Birthday" -msgstr "Aniversari" - -#: mod_vcard.erl:364 mod_vcard.erl:480 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:456 -msgid "City" -msgstr "Ciutat" - -#: mod_vcard.erl:364 mod_vcard.erl:479 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:455 -msgid "Country" -msgstr "Pais" - -#: mod_vcard.erl:364 mod_vcard.erl:481 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:457 -msgid "Email" -msgstr "Email" - -#: mod_vcard.erl:364 mod_vcard.erl:476 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:452 -msgid "Family Name" -msgstr "Cognom" - -#: mod_vcard.erl:364 mod_vcard_odbc.erl:342 -msgid "" -"Fill in the form to search for any matching Jabber User (Add * to the end of " -"field to match substring)" -msgstr "" -"Emplena el formulari per a buscar usuaris Jabber. Afegix * al final d'un " -"camp per a buscar subcadenes." - -#: mod_vcard.erl:364 mod_vcard.erl:473 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:449 -msgid "Full Name" -msgstr "Nom complet" - -#: mod_vcard.erl:364 mod_vcard.erl:475 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:451 -msgid "Middle Name" -msgstr "Segon nom" - -#: mod_vcard.erl:364 mod_vcard.erl:474 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:450 web/ejabberd_web_admin.erl:2006 -msgid "Name" -msgstr "Nom" - -#: mod_vcard.erl:364 mod_vcard.erl:482 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:458 -msgid "Organization Name" -msgstr "Nom de la organizació" - -#: mod_vcard.erl:364 mod_vcard.erl:483 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:459 -msgid "Organization Unit" -msgstr "Unitat de la organizació" - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "Search users in " -msgstr "Cerca usuaris en " - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "You need an x:data capable client to search" -msgstr "Necessites un client amb suport x:data per a poder buscar" - -#: mod_vcard.erl:389 mod_vcard_ldap.erl:487 mod_vcard_odbc.erl:367 -msgid "vCard User Search" -msgstr "Recerca de vCard d'usuari" - -#: mod_vcard.erl:445 mod_vcard_ldap.erl:541 mod_vcard_odbc.erl:421 -msgid "ejabberd vCard module" -msgstr "Mòdul ejabberd vCard" - -#: mod_vcard.erl:469 mod_vcard_ldap.erl:551 mod_vcard_odbc.erl:445 -msgid "Search Results for " -msgstr "Resultat de la búsqueda" - -#: mod_vcard_ldap.erl:462 -msgid "Fill in fields to search for any matching Jabber User" -msgstr "Emplena camps per a buscar usuaris Jabber que concorden" - -#: web/ejabberd_web_admin.erl:193 web/ejabberd_web_admin.erl:203 -#: web/ejabberd_web_admin.erl:219 web/ejabberd_web_admin.erl:229 -msgid "Unauthorized" -msgstr "No autoritzat" - -#: web/ejabberd_web_admin.erl:286 web/ejabberd_web_admin.erl:303 -msgid "ejabberd Web Admin" -msgstr "Web d'administració del ejabberd" - -#: web/ejabberd_web_admin.erl:769 web/ejabberd_web_admin.erl:780 -msgid "Administration" -msgstr "Administració" - -#: web/ejabberd_web_admin.erl:878 web/ejabberd_web_admin.erl:982 -msgid "Raw" -msgstr "en format text" - -#: web/ejabberd_web_admin.erl:1017 -msgid "~s access rule configuration" -msgstr "Configuració de les Regles d'Accés ~s" - -#: web/ejabberd_web_admin.erl:1035 -msgid "Virtual Hosts" -msgstr "Hosts virtuals" - -#: web/ejabberd_web_admin.erl:1043 web/ejabberd_web_admin.erl:1050 -msgid "Users" -msgstr "Usuaris" - -#: web/ejabberd_web_admin.erl:1078 -msgid "Users Last Activity" -msgstr "Última activitat d'usuari" - -#: web/ejabberd_web_admin.erl:1080 -msgid "Period: " -msgstr "Període: " - -#: web/ejabberd_web_admin.erl:1090 -msgid "Last month" -msgstr "Últim mes" - -#: web/ejabberd_web_admin.erl:1091 -msgid "Last year" -msgstr "Últim any" - -#: web/ejabberd_web_admin.erl:1092 -msgid "All activity" -msgstr "Tota l'activitat" - -#: web/ejabberd_web_admin.erl:1094 -msgid "Show Ordinary Table" -msgstr "Mostrar Taula Ordinaria" - -#: web/ejabberd_web_admin.erl:1096 -msgid "Show Integral Table" -msgstr "Mostrar Taula Integral" - -#: web/ejabberd_web_admin.erl:1105 web/ejabberd_web_admin.erl:1939 -msgid "Statistics" -msgstr "Estadístiques" - -#: web/ejabberd_web_admin.erl:1117 -msgid "Not Found" -msgstr "No Trobat" - -#: web/ejabberd_web_admin.erl:1134 -msgid "Node not found" -msgstr "Node no trobat" - -#: web/ejabberd_web_admin.erl:1460 -msgid "Host" -msgstr "Host" - -#: web/ejabberd_web_admin.erl:1461 -msgid "Registered Users" -msgstr "Usuaris registrats" - -#: web/ejabberd_web_admin.erl:1573 -msgid "Offline Messages" -msgstr "Missatges offline" - -#: web/ejabberd_web_admin.erl:1574 web/ejabberd_web_admin.erl:1767 -msgid "Last Activity" -msgstr "Última activitat" - -#: web/ejabberd_web_admin.erl:1659 web/ejabberd_web_admin.erl:1675 -msgid "Registered Users:" -msgstr "Usuaris registrats:" - -#: web/ejabberd_web_admin.erl:1661 web/ejabberd_web_admin.erl:1677 -#: web/ejabberd_web_admin.erl:2192 -msgid "Online Users:" -msgstr "Usuaris en línia:" - -#: web/ejabberd_web_admin.erl:1663 -msgid "Outgoing s2s Connections:" -msgstr "Connexions d'eixida s2s" - -#: web/ejabberd_web_admin.erl:1665 -msgid "Outgoing s2s Servers:" -msgstr "Servidors d'eixida de s2s" - -#: web/ejabberd_web_admin.erl:1734 web/mod_register_web.erl:175 -#: web/mod_register_web.erl:372 web/mod_register_web.erl:381 -#: web/mod_register_web.erl:411 -msgid "Change Password" -msgstr "Canviar Contrasenya" - -#: web/ejabberd_web_admin.erl:1758 -msgid "User " -msgstr "Usuari " - -#: web/ejabberd_web_admin.erl:1765 -msgid "Connected Resources:" -msgstr "Recursos connectats:" - -#: web/ejabberd_web_admin.erl:1766 web/mod_register_web.erl:227 -#: web/mod_register_web.erl:519 -msgid "Password:" -msgstr "Contrasenya:" - -#: web/ejabberd_web_admin.erl:1828 -msgid "No Data" -msgstr "No hi ha dades" - -#: web/ejabberd_web_admin.erl:1906 -msgid "Nodes" -msgstr "Nodes" - -#: web/ejabberd_web_admin.erl:1929 web/ejabberd_web_admin.erl:1951 -msgid "Node " -msgstr "Node " - -#: web/ejabberd_web_admin.erl:1938 -msgid "Listened Ports" -msgstr "Ports a l'escolta" - -#: web/ejabberd_web_admin.erl:1940 web/ejabberd_web_admin.erl:2260 -#: web/ejabberd_web_admin.erl:2448 -msgid "Update" -msgstr "Actualitzar" - -#: web/ejabberd_web_admin.erl:1943 web/ejabberd_web_admin.erl:2569 -msgid "Restart" -msgstr "Reiniciar" - -#: web/ejabberd_web_admin.erl:1945 web/ejabberd_web_admin.erl:2571 -msgid "Stop" -msgstr "Detindre" - -#: web/ejabberd_web_admin.erl:1959 -msgid "RPC Call Error" -msgstr "Error de cridada RPC" - -#: web/ejabberd_web_admin.erl:2000 -msgid "Database Tables at " -msgstr "Taules de la base de dades en " - -#: web/ejabberd_web_admin.erl:2007 -msgid "Storage Type" -msgstr "Tipus d'emmagatzematge" - -#: web/ejabberd_web_admin.erl:2008 -msgid "Elements" -msgstr "Elements" - -#: web/ejabberd_web_admin.erl:2009 -msgid "Memory" -msgstr "Memòria" - -#: web/ejabberd_web_admin.erl:2032 web/ejabberd_web_admin.erl:2137 -msgid "Error" -msgstr "Error" - -#: web/ejabberd_web_admin.erl:2034 -msgid "Backup of " -msgstr "Còpia de seguretat de " - -#: web/ejabberd_web_admin.erl:2036 -msgid "" -"Please note that these options will only backup the builtin Mnesia database. " -"If you are using the ODBC module, you also need to backup your SQL database " -"separately." -msgstr "" -"Recorda que aquestes opcions només fan còpia de seguretat de la base de " -"dades Mnesia. Si estàs utilitzant el mòdul d'ODBC també deus de fer una " -"còpia de seguretat de la base de dades de SQL a part." - -#: web/ejabberd_web_admin.erl:2041 -msgid "Store binary backup:" -msgstr "Guardar una còpia de seguretat binària:" - -#: web/ejabberd_web_admin.erl:2045 web/ejabberd_web_admin.erl:2052 -#: web/ejabberd_web_admin.erl:2060 web/ejabberd_web_admin.erl:2067 -#: web/ejabberd_web_admin.erl:2074 web/ejabberd_web_admin.erl:2081 -#: web/ejabberd_web_admin.erl:2088 web/ejabberd_web_admin.erl:2096 -#: web/ejabberd_web_admin.erl:2103 web/ejabberd_web_admin.erl:2110 -msgid "OK" -msgstr "Acceptar" - -#: web/ejabberd_web_admin.erl:2048 -msgid "Restore binary backup immediately:" -msgstr "Restaurar una còpia de seguretat binària ara mateix." - -#: web/ejabberd_web_admin.erl:2056 -msgid "" -"Restore binary backup after next ejabberd restart (requires less memory):" -msgstr "" -"Restaurar una còpia de seguretat binària després de reiniciar el ejabberd " -"(requereix menys memòria:" - -#: web/ejabberd_web_admin.erl:2063 -msgid "Store plain text backup:" -msgstr "Guardar una còpia de seguretat en format de text pla:" - -#: web/ejabberd_web_admin.erl:2070 -msgid "Restore plain text backup immediately:" -msgstr "Restaurar una còpia de seguretat en format de text pla ara mateix:" - -#: web/ejabberd_web_admin.erl:2077 -msgid "Import users data from a PIEFXIS file (XEP-0227):" -msgstr "Importar dades d'usuaris des d'un arxiu PIEFXIS (XEP-0227):" - -#: web/ejabberd_web_admin.erl:2084 -msgid "Export data of all users in the server to PIEFXIS files (XEP-0227):" -msgstr "" -"Exportar dades de tots els usuaris del servidor a arxius PIEFXIS (XEP-0227):" - -#: web/ejabberd_web_admin.erl:2091 -msgid "Export data of users in a host to PIEFXIS files (XEP-0227):" -msgstr "Exportar dades d'usuaris d'un host a arxius PIEFXIS (XEP-0227):" - -#: web/ejabberd_web_admin.erl:2099 -msgid "Import user data from jabberd14 spool file:" -msgstr "Importar dades d'usuaris de l'arxiu de spool de jabberd14" - -#: web/ejabberd_web_admin.erl:2106 -msgid "Import users data from jabberd14 spool directory:" -msgstr "Importar dades d'usuaris del directori de spool de jabberd14:" - -#: web/ejabberd_web_admin.erl:2132 -msgid "Listened Ports at " -msgstr "Ports a la escolta en " - -#: web/ejabberd_web_admin.erl:2157 -msgid "Modules at " -msgstr "Mòduls en " - -#: web/ejabberd_web_admin.erl:2183 -msgid "Statistics of ~p" -msgstr "Estadístiques de ~p" - -#: web/ejabberd_web_admin.erl:2186 -msgid "Uptime:" -msgstr "Temps en marxa" - -#: web/ejabberd_web_admin.erl:2189 -msgid "CPU Time:" -msgstr "Temps de CPU" - -#: web/ejabberd_web_admin.erl:2195 -msgid "Transactions Committed:" -msgstr "Transaccions Realitzades:" - -#: web/ejabberd_web_admin.erl:2198 -msgid "Transactions Aborted:" -msgstr "Transaccions Avortades" - -#: web/ejabberd_web_admin.erl:2201 -msgid "Transactions Restarted:" -msgstr "Transaccions reiniciades" - -#: web/ejabberd_web_admin.erl:2204 -msgid "Transactions Logged:" -msgstr "Transaccions registrades" - -#: web/ejabberd_web_admin.erl:2246 -msgid "Update " -msgstr "Actualitzar" - -#: web/ejabberd_web_admin.erl:2254 -msgid "Update plan" -msgstr "Pla d'actualització" - -#: web/ejabberd_web_admin.erl:2255 -msgid "Modified modules" -msgstr "Mòduls modificats" - -#: web/ejabberd_web_admin.erl:2256 -msgid "Update script" -msgstr "Script d'actualització" - -#: web/ejabberd_web_admin.erl:2257 -msgid "Low level update script" -msgstr "Script d'actualització de baix nivell" - -#: web/ejabberd_web_admin.erl:2258 -msgid "Script check" -msgstr "Comprovar script" - -#: web/ejabberd_web_admin.erl:2426 -msgid "Port" -msgstr "Port" - -#: web/ejabberd_web_admin.erl:2427 -msgid "IP" -msgstr "IP" - -#: web/ejabberd_web_admin.erl:2428 -msgid "Protocol" -msgstr "Protocol" - -#: web/ejabberd_web_admin.erl:2429 web/ejabberd_web_admin.erl:2556 -msgid "Module" -msgstr "Mòdul" - -#: web/ejabberd_web_admin.erl:2430 web/ejabberd_web_admin.erl:2557 -msgid "Options" -msgstr "Opcions" - -#: web/ejabberd_web_admin.erl:2450 -msgid "Delete" -msgstr "Eliminar" - -#: web/ejabberd_web_admin.erl:2579 -msgid "Start" -msgstr "Iniciar" - -#: web/mod_register_web.erl:104 -msgid "Your Jabber account was successfully created." -msgstr "El teu compte Jabber ha sigut creat correctament." - -#: web/mod_register_web.erl:107 -msgid "There was an error creating the account: " -msgstr "Hi ha hagut un error creant el compte: " - -#: web/mod_register_web.erl:115 -msgid "Your Jabber account was successfully deleted." -msgstr "El teu compte Jabber ha sigut esborrat correctament." - -#: web/mod_register_web.erl:118 -msgid "There was an error deleting the account: " -msgstr "Hi ha hagut un error esborrant el compte: " - -#: web/mod_register_web.erl:128 -msgid "The password of your Jabber account was successfully changed." -msgstr "La contrasenya del teu compte Jabber s'ha canviat correctament." - -#: web/mod_register_web.erl:131 -msgid "There was an error changing the password: " -msgstr "Hi ha hagut un error canviant la contrasenya: " - -#: web/mod_register_web.erl:163 web/mod_register_web.erl:172 -msgid "Jabber Account Registration" -msgstr "Registre de compte Jabber" - -#: web/mod_register_web.erl:174 web/mod_register_web.erl:192 -#: web/mod_register_web.erl:201 -msgid "Register a Jabber account" -msgstr "Registrar un compte Jabber" - -#: web/mod_register_web.erl:176 web/mod_register_web.erl:493 -#: web/mod_register_web.erl:502 -msgid "Unregister a Jabber account" -msgstr "Anul·lar el registre d'un compte Jabber" - -#: web/mod_register_web.erl:203 -msgid "" -"This page allows to create a Jabber account in this Jabber server. Your JID " -"(Jabber IDentifier) will be of the form: username@server. Please read " -"carefully the instructions to fill correctly the fields." -msgstr "" -"Aquesta pàgina permet crear un compte Jabber en aquest servidor Jabber. El " -"teu JID (Jabber IDentifier; Identificador Jabber) tindrà aquesta forma: " -"usuari@servidor. Si us plau, llegeix amb cura les instruccions per emplenar " -"correctament els camps." - -#: web/mod_register_web.erl:212 web/mod_register_web.erl:386 -#: web/mod_register_web.erl:509 -msgid "Username:" -msgstr "Nom d'usuari:" - -#: web/mod_register_web.erl:217 -msgid "This is case insensitive: macbeth is the same that MacBeth and Macbeth." -msgstr "" -"Això no distingeix majúscules de minúscules: macbeth es el mateix que " -"MacBeth i Macbeth." - -#: web/mod_register_web.erl:218 -msgid "Characters not allowed:" -msgstr "Caràcters no permesos:" - -#: web/mod_register_web.erl:222 web/mod_register_web.erl:391 -#: web/mod_register_web.erl:514 -msgid "Server:" -msgstr "Servidor:" - -#: web/mod_register_web.erl:232 -msgid "" -"Don't tell your password to anybody, not even the administrators of the " -"Jabber server." -msgstr "" -"No li donis la teva contrasenya a ningú, ni tan sols als administradors del " -"servidor Jabber." - -#: web/mod_register_web.erl:234 -msgid "You can later change your password using a Jabber client." -msgstr "" -"Podràs canviar la teva contrasenya més endavant utilitzant un client Jabber." - -#: web/mod_register_web.erl:235 -msgid "" -"Some Jabber clients can store your password in your computer. Use that " -"feature only if you trust your computer is safe." -msgstr "" -"Alguns clients Jabber poden emmagatzemar la teva contrasenya al teu " -"ordinador. Fes servir aquesta característica només si saps que el teu " -"ordinador és segur." - -#: web/mod_register_web.erl:237 -msgid "" -"Memorize your password, or write it in a paper placed in a safe place. In " -"Jabber there isn't an automated way to recover your password if you forget " -"it." -msgstr "" -"Memoritza la teva contrasenya, o escriu-la en un paper guardat a un lloc " -"segur.A Jabber no hi ha una forma automatitzada de recuperar la teva " -"contrasenya si la oblides." - -#: web/mod_register_web.erl:242 web/mod_register_web.erl:406 -msgid "Password Verification:" -msgstr "Verificació de la Contrasenya:" - -#: web/mod_register_web.erl:250 -msgid "Register" -msgstr "Registrar" - -#: web/mod_register_web.erl:396 -msgid "Old Password:" -msgstr "Antiga contrasenya:" - -#: web/mod_register_web.erl:401 -msgid "New Password:" -msgstr "Nova Contrasenya:" - -#: web/mod_register_web.erl:504 -msgid "This page allows to unregister a Jabber account in this Jabber server." -msgstr "" -"Aquesta pàgina permet anul·lar el registre d'un compte Jabber en aquest " -"servidor Jabber." - -#: web/mod_register_web.erl:524 -msgid "Unregister" -msgstr "Anul·lar el registre" - -#, fuzzy -#~ msgid "Captcha test failed" -#~ msgstr "El CAPTCHA es vàlid." diff --git a/priv/msgs/cs.msg b/priv/msgs/cs.msg index 279ce36f0..cd57ebefb 100644 --- a/priv/msgs/cs.msg +++ b/priv/msgs/cs.msg @@ -1,19 +1,20 @@ -{"Access Configuration","Konfigurace přístupů"}. -{"Access Control List Configuration","Konfigurace seznamu přístupových práv (ACL)"}. -{"Access control lists","Seznamy přístupových práv (ACL)"}. -{"Access Control Lists","Seznamy přístupových práv (ACL)"}. -{"Access denied by service policy","Přístup byl zamítnut nastavením služby"}. -{"Access rules","Pravidla přístupů"}. -{"Access Rules","Pravidla přístupů"}. -{"Action on user","Akce aplikovaná na uživatele"}. -{"Add Jabber ID","Přidat Jabber ID"}. -{"Add New","Přidat nový"}. -{"Add User","Přidat uživatele"}. -{"Administration","Administrace"}. -{"Administration of ","Administrace "}. -{"Administrator privileges required","Potřebujete práva administrátora"}. +%% Generated automatically +%% DO NOT EDIT: run `make translations` instead +%% To improve translations please read: +%% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ + +{" has set the subject to: "," změnil(a) téma na: "}. {"A friendly name for the node","Přívětivé jméno pro uzel"}. +{"A password is required to enter this room","Pro vstup do místnosti musíte zadat heslo"}. +{"Accept","Přijmout"}. +{"Access denied by service policy","Přístup byl zamítnut nastavením služby"}. +{"Action on user","Akce aplikovaná na uživatele"}. +{"Add User","Přidat uživatele"}. +{"Administration of ","Administrace "}. +{"Administration","Administrace"}. +{"Administrator privileges required","Potřebujete práva administrátora"}. {"All activity","Všechny aktivity"}. +{"All Users","Všichni uživatelé"}. {"Allow this Jabber ID to subscribe to this pubsub node?","Povolit tomuto Jabber ID odebírat tento pubsub uzel?"}. {"Allow users to change the subject","Povolit uživatelům měnit téma místnosti"}. {"Allow users to query other users","Povolit uživatelům odesílat požadavky (query) ostatním uživatelům"}. @@ -23,113 +24,103 @@ {"Allow visitors to send private messages to","Povolit návštěvníkům odesílat soukromé zprávy"}. {"Allow visitors to send status text in presence updates","Povolit návštěvníkům posílat stavové zprávy ve statusu"}. {"Allow visitors to send voice requests","Povolit uživatelům posílat žádosti o voice práva"}. -{"All Users","Všichni uživatelé"}. {"Announcements","Oznámení"}. -{"anyone","každému"}. -{"A password is required to enter this room","Pro vstup do místnosti musíte zadat heslo"}. {"April",". dubna"}. {"August",". srpna"}. +{"Automatic node creation is not enabled","Automatické vytváření uzlů není povoleno"}. {"Backup Management","Správa zálohování"}. -{"Backup of ","Záloha na "}. +{"Backup of ~p","Záloha ~p"}. {"Backup to File at ","Záloha do souboru na "}. {"Backup","Zálohovat"}. {"Bad format","Nesprávný formát"}. {"Birthday","Datum narození"}. +{"Both the username and the resource are required","Uživatelské jméno i zdroj jsou požadované položky"}. +{"Bytestream already activated","Bytestream již byl aktivován"}. +{"Cannot remove active list","Aktivní seznam nelze odebrat"}. +{"Cannot remove default list","Výchozí seznam nelze odebrat"}. {"CAPTCHA web page","Webová stránka CAPTCHA"}. {"Change Password","Změnit heslo"}. {"Change User Password","Změnit heslo uživatele"}. +{"Changing password is not allowed","Změna hesla není povolena"}. +{"Changing role/affiliation is not allowed","Změna role/příslušnosti není povolena"}. {"Characters not allowed:","Nepřípustné znaky:"}. {"Chatroom configuration modified","Nastavení diskuzní místnosti bylo změněno"}. -{"Chatroom is created","Konference vytvořena"}. -{"Chatroom is destroyed","Konference zrušena"}. -{"Chatroom is started","Konference spuštěna"}. -{"Chatroom is stopped","Konference zastavena"}. -{"Chatrooms","Konference"}. +{"Chatroom is created","Místnost vytvořena"}. +{"Chatroom is destroyed","Místnost zrušena"}. +{"Chatroom is started","Místnost spuštěna"}. +{"Chatroom is stopped","Místnost zastavena"}. +{"Chatrooms","Místnosti"}. {"Choose a username and password to register with this server","Zadejte jméno uživatele a heslo pro registraci na tomto serveru"}. -{"Choose modules to stop","Vyberte moduly, které mají být zastaveny"}. {"Choose storage type of tables","Vyberte typ úložiště pro tabulky"}. -{"Choose whether to approve this entity's subscription.","Zvolte, zda chcete schválit odebírání touto entitou"}. +{"Choose whether to approve this entity's subscription.","Zvolte, zda chcete schválit odebírání touto entitou."}. {"City","Město"}. {"Commands","Příkazy"}. -{"Conference room does not exist","Konferenční místnost neexistuje"}. -{"Configuration","Konfigurace"}. +{"Conference room does not exist","Místnost neexistuje"}. {"Configuration of room ~s","Konfigurace místnosti ~s"}. -{"Connected Resources:","Připojené zdroje:"}. -{"Connections parameters","Parametry spojení"}. +{"Configuration","Konfigurace"}. {"Country","Země"}. -{"CPU Time:","Čas procesoru"}. -{"Database","Databáze"}. -{"Database Tables at ","Databázové tabulky na "}. +{"Database failure","Chyba databáze"}. {"Database Tables Configuration at ","Konfigurace databázových tabulek "}. +{"Database","Databáze"}. {"December",". prosince"}. {"Default users as participants","Uživatelé jsou implicitně členy"}. {"Delete message of the day on all hosts","Smazat zprávu dne na všech hostitelích"}. {"Delete message of the day","Smazat zprávu dne"}. -{"Delete Selected","Smazat vybrané"}. -{"Delete","Smazat"}. {"Delete User","Smazat uživatele"}. {"Deliver event notifications","Doručovat upozornění na události"}. {"Deliver payloads with event notifications","Doručovat náklad s upozorněním na událost"}. -{"Description:","Popis:"}. {"Disc only copy","Jen kopie disku"}. -{"Displayed Groups:","Zobrazené skupiny:"}. -{"Don't tell your password to anybody, not even the administrators of the Jabber server.","Nikdy nikomu nesdělujte své heslo, ani administrátorovi serveru Jabberu."}. {"Dump Backup to Text File at ","Uložit zálohu do textového souboru na "}. {"Dump to Text File","Uložit do textového souboru"}. {"Edit Properties","Upravit vlastnosti"}. {"Either approve or decline the voice request.","Povolit nebo odmítnout voice žádost."}. -{"ejabberd IRC module","ejabberd IRC modul"}. {"ejabberd MUC module","ejabberd MUC modul"}. +{"ejabberd Multicast service","Služba ejabberd Multicast"}. {"ejabberd Publish-Subscribe module","ejabberd Publish-Subscribe modul"}. {"ejabberd SOCKS5 Bytestreams module","ejabberd SOCKS5 Bytestreams modul"}. {"ejabberd vCard module","ejabberd vCard modul"}. {"ejabberd Web Admin","Webová administrace ejabberd"}. -{"Elements","Položek"}. {"Email","E-mail"}. {"Enable logging","Zaznamenávat konverzace"}. -{"Encoding for server ~b","Kódování pro server ~b"}. +{"Enable message archiving","Povolit ukládání historie zpráv"}. +{"Enabling push without 'node' attribute is not supported","Aktivováno push bez atributu 'node' není podporováno"}. {"End User Session","Ukončit sezení uživatele"}. -{"Enter list of {Module, [Options]}","Vložte seznam modulů {Modul, [Parametry]}"}. {"Enter nickname you want to register","Zadejte přezdívku, kterou chcete zaregistrovat"}. {"Enter path to backup file","Zadajte cestu k souboru se zálohou"}. {"Enter path to jabberd14 spool dir","Zadejte cestu k jabberd14 spool adresáři"}. {"Enter path to jabberd14 spool file","Zadejte cestu k spool souboru jabberd14"}. {"Enter path to text file","Zadajte cestu k textovému souboru"}. {"Enter the text you see","Zadejte text, který vidíte"}. -{"Enter username and encodings you wish to use for connecting to IRC servers. Press 'Next' to get more fields to fill in. Press 'Complete' to save settings.","Zadejte přezdívku a kódování, které chcete používat pro připojení k serverům IRC. Stiskněte 'Další' pro více políček k vyplnění. Stiskněte 'Dokončit' pro uložení nastavení."}. -{"Enter username, encodings, ports and passwords you wish to use for connecting to IRC servers","Zadejte přezdívku, kódování, porty a hesla, které chcete používat pro připojení k serverům IRC"}. -{"Erlang Jabber Server","Erlang Jabber Server"}. -{"Error","Chyba"}. -{"Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef.net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}].","Příklad: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef.net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}].2\"}]."}. {"Exclude Jabber IDs from CAPTCHA challenge","Vyloučit Jabber ID z procesu CAPTCHA ověřování"}. +{"Export all tables as SQL queries to a file:","Zálohovat všechny tabulky jako SQL dotazy do souboru:"}. {"Export data of all users in the server to PIEFXIS files (XEP-0227):","Exportovat všechny uživatele do souboru ve formátu PIEFXIS (XEP-0227):"}. {"Export data of users in a host to PIEFXIS files (XEP-0227):","Exportovat uživatele na hostiteli do souboru ve formátu PIEFXIS (XEP-0227):"}. +{"External component failure","Chyba externí komponenty"}. +{"External component timeout","Timeout externí komponenty"}. +{"Failed to activate bytestream","Chyba při aktivaci bytestreamu"}. {"Failed to extract JID from your voice request approval","Došlo k chybě při získávání Jabber ID z vaší žádosti o voice práva"}. +{"Failed to map delegated namespace to external component","Chyba při mapování namespace pro externí komponentu"}. +{"Failed to parse HTTP response","Chyba parsování HTTP odpovědi"}. +{"Failed to process option '~s'","Chyba při zpracování možnosti '~s'"}. {"Family Name","Příjmení"}. {"February",". února"}. -{"Fill in fields to search for any matching Jabber User","Vyplňte políčka pro vyhledání uživatele Jabberu"}. -{"Fill in the form to search for any matching Jabber User (Add * to the end of field to match substring)","Pro vyhledání uživatele Jabberu vyplňte formulář (na konec přidejte znak * pro vyhledání podřetězce)"}. +{"File larger than ~w bytes","Soubor větší než ~w bytů"}. {"Friday","Pátek"}. -{"From","Od"}. -{"From ~s","Od ~s"}. +{"From ~ts","Od ~ts"}. {"Full Name","Celé jméno"}. {"Get Number of Online Users","Získat počet online uživatelů"}. {"Get Number of Registered Users","Získat počet registrovaných uživatelů"}. {"Get User Last Login Time","Získat čas podleního přihlášení uživatele"}. -{"Get User Password","Získat heslo uživatele"}. {"Get User Statistics","Získat statistiky uživatele"}. +{"Given Name","Křestní jméno"}. {"Grant voice to this person?","Udělit voice práva této osobě?"}. -{"Group ","Skupina "}. -{"Groups","Skupiny"}. {"has been banned","byl(a) zablokován(a)"}. -{"has been kicked because of an affiliation change","byl(a) vyhozen(a) kvůli změně přiřazení"}. {"has been kicked because of a system shutdown","byl(a) vyhozen(a), protože dojde k vypnutí systému"}. +{"has been kicked because of an affiliation change","byl(a) vyhozen(a) kvůli změně přiřazení"}. {"has been kicked because the room has been changed to members-only","byl(a) vyhozen(a), protože mísnost je nyní pouze pro členy"}. {"has been kicked","byl(a) vyhozen(a) z místnosti"}. -{" has set the subject to: "," změnil(a) téma na: "}. -{"Host","Hostitel"}. +{"Host unknown","Neznámý hostitel"}. {"If you don't see the CAPTCHA image here, visit the web page.","Pokud zde nevidíte obrázek CAPTCHA, přejděte na webovou stránku."}. -{"If you want to specify different ports, passwords, encodings for IRC servers, fill this list with values in format '{\"irc server\", \"encoding\", port, \"password\"}'. By default this service use \"~s\" encoding, port ~p, empty password.","Pokud chcete zadat jiné kódování pro IRC servery, vyplňte seznam s hodnotami ve formátu '{\"irc server\",\"encoding\", port, \"password\"}'. Výchozí kódování pro tuto službu je \"~s\", port ~p, empty password."}. {"Import Directory","Import adresáře"}. {"Import File","Import souboru"}. {"Import user data from jabberd14 spool file:","Importovat uživatele z jabberd14 spool souborů:"}. @@ -138,30 +129,27 @@ {"Import users data from jabberd14 spool directory:","Importovat uživatele z jabberd14 spool souborů:"}. {"Import Users from Dir at ","Importovat uživatele z adresáře na "}. {"Import Users From jabberd14 Spool Files","Importovat uživatele z jabberd14 spool souborů"}. +{"Improper domain part of 'from' attribute","Nesprávná část s doménou atributu 'from'"}. {"Improper message type","Nesprávný typ zprávy"}. +{"Incorrect CAPTCHA submit","Nesprávné odeslání CAPTCHA"}. +{"Incorrect data form","Nesprávný datový formulář"}. {"Incorrect password","Nesprávné heslo"}. -{"Invalid affiliation: ~s","Neplatné přiřazení: ~s"}. -{"Invalid role: ~s","Neplatná role: ~s"}. +{"Incorrect value of 'action' attribute","Nesprávná hodnota atributu 'action'"}. +{"Incorrect value of 'action' in data form","Nesprávná hodnota atributu 'action' v datovém formuláři"}. +{"Incorrect value of 'path' in data form","Nesprávná hodnota atributu 'path' v datovém formuláři"}. +{"Installed Modules:","Instalované moduly:"}. +{"Insufficient privilege","Nedostatečné oprávnění"}. +{"Invalid 'from' attribute in forwarded message","Nesprávný atribut 'from' v přeposlané zprávě"}. +{"Invitations are not allowed in this conference","Pozvánky nejsou povoleny v této místnosti"}. {"IP addresses","IP adresy"}. -{"IP","IP"}. -{"IRC channel (don't put the first #)","IRC kanál (bez počátečního #)"}. -{"IRC server","IRC přezdívka"}. -{"IRC settings","Nastavení IRC"}. -{"IRC Transport","IRC transport"}. -{"IRC username","IRC přezdívka"}. -{"IRC Username","IRC přezdívka"}. {"is now known as","se přejmenoval(a) na"}. -{"It is not allowed to send private messages","Je zakázáno posílat soukromé zprávy"}. -{"It is not allowed to send private messages of type \"groupchat\"","Není dovoleno odeslání soukromé zprávy typu \"skupinová zpráva\" "}. -{"It is not allowed to send private messages to the conference","Není povoleno odesílat soukromé zprávy do konference"}. -{"Jabber Account Registration","Registrace účtu Jabberu"}. +{"It is not allowed to send error messages to the room. The participant (~s) has sent an error message (~s) and got kicked from the room","Není povoleno posílat chybové zprávy do místnosti. Účastník (~s) odeslal chybovou zprávu (~s) a byl vyhozen z místnosti"}. +{"It is not allowed to send private messages of type \"groupchat\"","Není dovoleno odeslání soukromých zpráv typu \"skupinová zpráva\""}. +{"It is not allowed to send private messages to the conference","Není povoleno odesílat soukromé zprávy v této místnosti"}. {"Jabber ID","Jabber ID"}. -{"Jabber ID ~s is invalid","Jabber ID ~s je neplatné"}. {"January",". ledna"}. -{"Join IRC channel","Vstoupit do IRC kanálu"}. +{"Joined MIX channels:","Připojené MIX kanály:"}. {"joins the room","vstoupil(a) do místnosti"}. -{"Join the IRC channel here.","Vstoupit do tohoto IRC kanálu."}. -{"Join the IRC channel in this Jabber ID: ~s","Vstupte do IRC kanálu s tímto Jabber ID: ~s"}. {"July",". července"}. {"June",". června"}. {"Last Activity","Poslední aktivita"}. @@ -169,10 +157,6 @@ {"Last month","Poslední měsíc"}. {"Last year","Poslední rok"}. {"leaves the room","opustil(a) místnost"}. -{"Listened Ports at ","Otevřené porty na "}. -{"Listened Ports","Otevřené porty"}. -{"List of modules to start","Seznam modulů, které mají být spuštěné"}. -{"Low level update script","Nízkoúrovňový aktualizační skript"}. {"Make participants list public","Nastavit seznam účastníků jako veřejný"}. {"Make room CAPTCHA protected","Chránit místnost pomocí CAPTCHA"}. {"Make room members-only","Zpřístupnit místnost jen členům"}. @@ -180,43 +164,63 @@ {"Make room password protected","Chránit místnost heslem"}. {"Make room persistent","Nastavit místnost jako stálou"}. {"Make room public searchable","Nastavit místnost jako veřejnou"}. +{"Malformed username","Chybně formátováné jméno uživatele"}. {"March",". března"}. -{"Maximum Number of Occupants","Počet účastníků"}. -{"Max # of items to persist","Maximální počet položek, které je možné natrvalo uložit"}. {"Max payload size in bytes","Maximální náklad v bajtech"}. +{"Maximum Number of Occupants","Maximální počet účastníků"}. {"May",". května"}. -{"Members:","Členové:"}. {"Membership is required to enter this room","Pro vstup do místnosti musíte být členem"}. -{"Memorize your password, or write it in a paper placed in a safe place. In Jabber there isn't an automated way to recover your password if you forget it.","Svoje heslo si zapamatujte, nebo si jej poznamenejte na papírek a ten uschovejte v bezpečí. Jabber nemá žádný automatizovaný způsob obnovy hesla."}. -{"Memory","Paměť"}. {"Message body","Tělo zprávy"}. +{"Message not found in forwarded payload","Zpráva nebyla nalezena v přeposlaném obsahu"}. {"Middle Name","Druhé jméno"}. {"Minimum interval between voice requests (in seconds)","Minimální interval mezi žádostmi o voice práva (v sekundách)"}. {"Moderator privileges required","Potřebujete práva moderátora"}. -{"moderators only","moderátorům"}. -{"Modified modules","Aktualizované moduly"}. -{"Module","Modul"}. -{"Modules at ","Moduly na "}. -{"Modules","Moduly"}. +{"Moderator","Moderátor"}. +{"Module failed to handle the query","Modul chyboval při zpracování dotazu"}. {"Monday","Pondělí"}. -{"Name:","Jméno:"}. +{"Multicast","Multicast"}. +{"Multi-User Chat","Víceuživatelský chat"}. {"Name","Jméno"}. +{"Neither 'jid' nor 'nick' attribute found","Nebyl nalezen atribut 'jid' ani 'nick'"}. +{"Neither 'role' nor 'affiliation' attribute found","Nebyl nalezen atribut 'role' ani 'affiliation'"}. {"Never","Nikdy"}. {"New Password:","Nové heslo:"}. -{"Nickname","Přezdívka"}. {"Nickname Registration at ","Registrace přezdívky na "}. {"Nickname ~s does not exist in the room","Přezdívka ~s v místnosti neexistuje"}. -{"nobody","nikdo"}. +{"Nickname","Přezdívka"}. +{"No 'affiliation' attribute found","Chybějící atribut 'affiliation'"}. +{"No available resource found","Nebyl nalezen žádný dostupný zdroj"}. {"No body provided for announce message","Zpráva neobsahuje text"}. +{"No data form found","Nebyl nalezen datový formulář"}. {"No Data","Žádná data"}. -{"Node ID","ID uzlu"}. -{"Node not found","Uzel nenalezen"}. -{"Nodes","Uzly"}. -{"Node ","Uzel "}. +{"No features available","Žádné funce nejsou dostupné"}. +{"No hook has processed this command","Žádný hook nebyl zpracován tímto příkazem"}. +{"No info about last activity found","Nebyla žádná informace o poslední aktivitě"}. +{"No 'item' element found","Element 'item' nebyl nalezen"}. +{"No items found in this query","Žádné položky nebyly nalezeny v tomto dotazu"}. {"No limit","Bez limitu"}. +{"No module is handling this query","Žádný modul neobsluhuje tento dotaz"}. +{"No node specified","Žádný uzel nebyl specifikován"}. +{"No 'password' found in data form","Chybějící atribut 'password' v datovém formuláři"}. +{"No 'password' found in this query","Chybějící atribut 'password' v tomto dotazu"}. +{"No 'path' found in data form","Chybějící atribut 'path' v datovém formuláři"}. +{"No pending subscriptions found","Žádné čekající předplatné nebylo nalezeno"}. +{"No privacy list with this name found","Žádný privacy list s tímto jménem nebyl nalezen"}. +{"No private data found in this query","Žádná soukromá data nebyla nalezena tímto dotazem"}. +{"No running node found","Nebyl nalezen žádný běžící uzel"}. +{"No services available","Žádné služby nejsou dostupné"}. +{"No statistics found for this item","Nebyly nalezeny statistiky pro uvedenou položku"}. +{"No 'to' attribute found in the invitation","Chybějící atribut 'to' v pozvánce"}. +{"Node already exists","Uzel již existuje"}. +{"Node ID","ID uzlu"}. +{"Node index not found","Index uzlu nebyl nalezen"}. +{"Node not found","Uzel nenalezen"}. +{"Node ~p","Uzel ~p"}. +{"Nodeprep has failed","Nodeprep chyboval"}. +{"Nodes","Uzly"}. {"None","Nic"}. -{"No resource provided","Nebyl poskytnut žádný zdroj"}. {"Not Found","Nenalezeno"}. +{"Not subscribed","Není odebíráno"}. {"Notify subscribers when items are removed from the node","Upozornit odběratele na odstranění položek z uzlu"}. {"Notify subscribers when the node configuration changes","Upozornit odběratele na změnu nastavení uzlu"}. {"Notify subscribers when the node is deleted","Upozornit odběratele na smazání uzlu"}. @@ -225,197 +229,168 @@ {"Number of online users","Počet online uživatelů"}. {"Number of registered users","Počet registrovaných uživatelů"}. {"October",". října"}. -{"Offline Messages:","Offline zprávy:"}. -{"Offline Messages","Offline zprávy"}. {"OK","OK"}. {"Old Password:","Současné heslo:"}. +{"Online Users","Připojení uživatelé"}. {"Online","Online"}. -{"Online Users:","Online uživatelé:"}. -{"Online Users","Online uživatelé"}. {"Only deliver notifications to available users","Doručovat upozornění jen právě přihlášeným uživatelům"}. +{"Only or tags are allowed","Pouze značky nebo jsou povoleny"}. +{"Only element is allowed in this query","Pouze element je povolen v tomto dotazu"}. +{"Only members may query archives of this room","Pouze moderátoři mají povoleno měnit téma místnosti"}. {"Only moderators and participants are allowed to change the subject in this room","Jen moderátoři a účastníci mají povoleno měnit téma této místnosti"}. {"Only moderators are allowed to change the subject in this room","Jen moderátoři mají povoleno měnit téma místnosti"}. {"Only moderators can approve voice requests","Pouze moderátoři mohou schválit žádosti o voice práva"}. -{"Only occupants are allowed to send messages to the conference","Jen členové mají povolené zasílat zprávy do konference"}. -{"Only occupants are allowed to send queries to the conference","Jen členové mohou odesílat požadavky (query) do konference"}. +{"Only occupants are allowed to send messages to the conference","Jen členové mají povolené zasílat zprávy do místnosti"}. +{"Only occupants are allowed to send queries to the conference","Jen členové mohou odesílat požadavky (query) do místnosti"}. {"Only service administrators are allowed to send service messages","Pouze správci služby smí odesílat servisní zprávy"}. -{"Options","Nastavení"}. {"Organization Name","Název firmy"}. {"Organization Unit","Oddělení"}. -{"Outgoing s2s Connections:","Odchozí s2s spojení:"}. +{"Other Modules Available:","Ostatní dostupné moduly:"}. {"Outgoing s2s Connections","Odchozí s2s spojení"}. -{"Outgoing s2s Servers:","Odchozí s2s servery:"}. {"Owner privileges required","Jsou vyžadována práva vlastníka"}. -{"Packet","Paket"}. -{"Password ~b","Heslo ~b"}. -{"Password:","Heslo:"}. -{"Password","Heslo"}. -{"Password Verification:","Ověření hesla:"}. +{"Participant","Účastník"}. {"Password Verification","Ověření hesla"}. +{"Password Verification:","Ověření hesla:"}. +{"Password","Heslo"}. +{"Password:","Heslo:"}. {"Path to Dir","Cesta k adresáři"}. {"Path to File","Cesta k souboru"}. -{"Pending","Čekající"}. {"Period: ","Čas: "}. {"Persist items to storage","Uložit položky natrvalo do úložiště"}. +{"Ping query is incorrect","Ping dotaz je nesprávný"}. {"Ping","Ping"}. {"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Podotýkáme, že tato nastavení budou zálohována do zabudované databáze Mnesia. Pokud používáte ODBC modul, musíte zálohovat svoji SQL databázi samostatně."}. {"Please, wait for a while before sending new voice request","Prosím, počkejte chvíli před posláním nové žádosti o voice práva"}. {"Pong","Pong"}. -{"Port ~b","Port ~b"}. -{"Port","Port"}. {"Present real Jabber IDs to","Odhalovat skutečná Jabber ID"}. {"private, ","soukromá, "}. -{"Protocol","Protokol"}. {"Publish-Subscribe","Publish-Subscribe"}. {"PubSub subscriber request","Žádost odběratele PubSub"}. {"Purge all items when the relevant publisher goes offline","Smazat všechny položky, pokud se příslušný poskytovatel odpojí"}. -{"Queries to the conference members are not allowed in this room","Požadavky (queries) na členy konference nejsou v této místnosti povolené"}. +{"Queries to the conference members are not allowed in this room","Požadavky (queries) na členy místnosti nejsou v této místnosti povolené"}. +{"Query to another users is forbidden","Dotaz na jiné uživatele je zakázán"}. {"RAM and disc copy","Kopie RAM a disku"}. {"RAM copy","Kopie RAM"}. -{"Raw","Zdroj"}. {"Really delete message of the day?","Skutečně smazat zprávu dne?"}. -{"Recipient is not in the conference room","Příjemce se nenachází v konferenční místnosti"}. -{"Register a Jabber account","Zaregistrujte si účet Jabberu"}. -{"Registered Users","Registrovaní uživatelé"}. -{"Registered Users:","Registrovaní živatelé:"}. +{"Recipient is not in the conference room","Příjemce se nenachází v místnosti"}. {"Register","Zaregistrovat se"}. -{"Registration in mod_irc for ","Registrace do mod_irc na "}. {"Remote copy","Vzdálená kopie"}. -{"Remove All Offline Messages","Odstranit všechny offline zprávy"}. -{"Remove","Odstranit"}. {"Remove User","Odstranit uživatele"}. {"Replaced by new connection","Nahrazeno novým spojením"}. {"Resources","Zdroje"}. -{"Restart","Restart"}. {"Restart Service","Restartovat službu"}. {"Restore Backup from File at ","Obnovit zálohu ze souboru na "}. -{"Restore binary backup after next ejabberd restart (requires less memory):","Obnovit binární zálohu při následujícím restartu ejabberd (vyžaduje méně paměti)"}. +{"Restore binary backup after next ejabberd restart (requires less memory):","Obnovit binární zálohu při následujícím restartu ejabberd (vyžaduje méně paměti):"}. {"Restore binary backup immediately:","Okamžitě obnovit binární zálohu:"}. -{"Restore","Obnovit"}. {"Restore plain text backup immediately:","Okamžitě obnovit zálohu z textového souboru:"}. +{"Restore","Obnovit"}. +{"Roles for which Presence is Broadcasted","Role, pro které je zpráva o stavu šířena"}. {"Room Configuration","Nastavení místnosti"}. {"Room creation is denied by service policy","Pravidla služby nepovolují vytvořit místnost"}. {"Room description","Popis místnosti"}. {"Room Occupants","Počet účastníků"}. {"Room title","Název místnosti"}. {"Roster groups allowed to subscribe","Skupiny kontaktů, které mohou odebírat"}. -{"Roster of ","Seznam kontaktů "}. -{"Roster","Seznam kontaktů"}. {"Roster size","Velikost seznamu kontaktů"}. -{"RPC Call Error","Chyba RPC volání"}. {"Running Nodes","Běžící uzly"}. -{"~s access rule configuration","~s konfigurace pravidla přístupu"}. {"Saturday","Sobota"}. -{"Script check","Kontrola skriptu"}. {"Search Results for ","Výsledky hledání pro "}. {"Search users in ","Hledat uživatele v "}. -{"Send announcement to all online users","Odeslat oznámení všem online uživatelům"}. {"Send announcement to all online users on all hosts","Odeslat oznámení všem online uživatelům na všech hostitelích"}. -{"Send announcement to all users","Odeslat oznámení všem uživatelům"}. +{"Send announcement to all online users","Odeslat oznámení všem online uživatelům"}. {"Send announcement to all users on all hosts","Odeslat oznámení všem uživatelům na všech hostitelích"}. +{"Send announcement to all users","Odeslat oznámení všem uživatelům"}. {"September",". září"}. -{"Server ~b","Server ~b"}. {"Server:","Server:"}. {"Set message of the day and send to online users","Nastavit zprávu dne a odeslat ji online uživatelům"}. -{"Set message of the day on all hosts and send to online users","Nastavit zprávu dne a odeslat ji online uživatelům"}. +{"Set message of the day on all hosts and send to online users","Nastavit zprávu dne na všech hostitelích a odeslat ji online uživatelům"}. {"Shared Roster Groups","Skupiny pro sdílený seznam kontaktů"}. {"Show Integral Table","Zobrazit kompletní tabulku"}. {"Show Ordinary Table","Zobrazit běžnou tabulku"}. {"Shut Down Service","Vypnout službu"}. -{"~s invites you to the room ~s","~s vás zve do místnosti ~s"}. -{"Some Jabber clients can store your password in your computer. Use that feature only if you trust your computer is safe.","Někteří klienti umí uložit vaše heslo na disk počítače. Tuto funkci používejte, pouze pokud věříte zabezpečení svého počítače."}. {"Specify the access model","Uveďte přístupový model"}. {"Specify the event message type","Zvolte typ zpráv pro události"}. {"Specify the publisher model","Specifikovat model pro publikování"}. -{"~s's Offline Messages Queue","Fronta offline zpráv uživatele ~s"}. -{"Start Modules at ","Spustit moduly na "}. -{"Start Modules","Spustit moduly"}. -{"Start","Start"}. -{"Statistics of ~p","Statistiky ~p"}. -{"Statistics","Statistiky"}. -{"Stop Modules at ","Zastavit moduly na "}. -{"Stop Modules","Zastavit moduly"}. {"Stopped Nodes","Zastavené uzly"}. -{"Stop","Stop"}. -{"Storage Type","Typ úložiště"}. {"Store binary backup:","Uložit binární zálohu:"}. {"Store plain text backup:","Uložit zálohu do textového souboru:"}. {"Subject","Předmět"}. -{"Submit","Odeslat"}. {"Submitted","Odeslané"}. {"Subscriber Address","Adresa odběratele"}. -{"Subscription","Přihlášení"}. +{"Subscriptions are not allowed","Předplatné není povoleno"}. {"Sunday","Neděle"}. {"That nickname is already in use by another occupant","Přezdívka je již používána jiným členem"}. {"That nickname is registered by another person","Přezdívka je zaregistrována jinou osobou"}. +{"The account was not unregistered","Účet nebyl smazán"}. {"The CAPTCHA is valid.","CAPTCHA souhlasí."}. {"The CAPTCHA verification has failed","Ověření CAPTCHA se nezdařilo"}. {"The collections with which a node is affiliated","Kolekce, se kterými je uzel spřízněn"}. -{"the password is","heslo je"}. +{"The feature requested is not supported by the conference","Požadovaná vlastnost není podporována touto místností"}. +{"The number of subscribers to the node","Počet odběratelů uzlu"}. +{"The password contains unacceptable characters","Heslo obsahuje nepovolené znaky"}. {"The password is too weak","Heslo je příliš slabé"}. -{"The password of your Jabber account was successfully changed.","Heslo vašeho účtu Jabberu bylo úspěšně změněno."}. +{"the password is","heslo je"}. +{"The query is only allowed from local users","Dotaz je povolen pouze pro místní uživatele"}. +{"The query must not contain elements","Dotaz nesmí obsahovat elementy "}. +{"The stanza MUST contain only one element, one element, or one element","Stanza MUSÍ obsahovat pouze jeden element , jeden element nebo jeden element "}. {"There was an error changing the password: ","Při změně hesla došlo k chybě: "}. -{"There was an error creating the account: ","Při vytváření účtu došlo k chybě."}. +{"There was an error creating the account: ","Při vytváření účtu došlo k chybě: "}. {"There was an error deleting the account: ","Při mazání účtu došlo k chybě: "}. -{"This is case insensitive: macbeth is the same that MacBeth and Macbeth.","Zde nezáleží na velikosti písmen: macbeth je stejný jako MacBeth a Macbeth."}. -{"This page allows to create a Jabber account in this Jabber server. Your JID (Jabber IDentifier) will be of the form: username@server. Please read carefully the instructions to fill correctly the fields.","Na této stránce si můžete vytvořit účet na tomto serveru Jabberu. Vaše JID (Jabber IDentifikátor) bude mít tvar: uživatelskéjméno@server. Přečtěte si prosím pozorně instrukce pro vyplnění údajů."}. -{"This page allows to unregister a Jabber account in this Jabber server.","Zde můžete zrušit registraci účtu na tomto serveru Jabberu."}. -{"This participant is kicked from the room because he sent an error message","Tento účastník byl vyhozen, protože odeslal chybovou zprávu"}. -{"This participant is kicked from the room because he sent an error message to another participant","Tento účastník byl vyhozen, protože odeslal chybovou zprávu jinému účastníkovi"}. -{"This participant is kicked from the room because he sent an error presence","Tento účastník byl vyhozen, protože odeslal chybový status"}. {"This room is not anonymous","Tato místnost není anonymní"}. {"Thursday","Čtvrtek"}. -{"Time","Čas"}. {"Time delay","Časový posun"}. +{"To register, visit ~s","Pokud se chcete zaregistrovat, navštivte ~s"}. +{"Token TTL","Token TTL"}. +{"Too many active bytestreams","Příliš mnoho aktivních bytestreamů"}. {"Too many CAPTCHA requests","Přiliš mnoho CAPTCHA žádostí"}. -{"To","Pro"}. -{"To ~s","Pro ~s"}. +{"Too many elements","Příliš mnoho elementů "}. +{"Too many elements","Přilíš mnoho elementů "}. +{"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","Příliš mnoho (~p) chybných pokusů o přihlášení z této IP adresy (~s). Adresa bude zablokována do ~s UTC"}. +{"Too many unacked stanzas","Příliš mnoho nepotvrzených stanz"}. +{"Too many users in this conference","Přiliš mnoho uživatelů v této místnosti"}. {"Traffic rate limit is exceeded","Byl překročen limit"}. -{"Transactions Aborted:","Transakce zrušena"}. -{"Transactions Committed:","Transakce potvrzena"}. -{"Transactions Logged:","Transakce zaznamenána"}. -{"Transactions Restarted:","Transakce restartována"}. {"Tuesday","Úterý"}. {"Unable to generate a CAPTCHA","Nebylo možné vygenerovat CAPTCHA"}. +{"Unable to register route on existing local domain","Není možné zaregistrovat routu na existující místní doménu"}. {"Unauthorized","Nemáte oprávnění"}. -{"Unregister a Jabber account","Zrušte registraci účtu Jabberu"}. +{"Unexpected action","Neočekávaná akce"}. {"Unregister","Zrušit registraci"}. -{"Update ","Aktualizovat "}. -{"Update","Aktualizovat"}. +{"Unsupported element","Nepodporovaný element"}. {"Update message of the day (don't send)","Aktualizovat zprávu dne (neodesílat)"}. {"Update message of the day on all hosts (don't send)","Aktualizovat zprávu dne pro všechny hostitele (neodesílat)"}. -{"Update plan","Aktualizovat plán"}. -{"Update script","Aktualizované skripty"}. -{"Uptime:","Čas běhu:"}. -{"Use of STARTTLS required","Je vyžadováno STARTTLS"}. +{"User already exists","Uživatel již existuje"}. {"User JID","Jabber ID uživatele"}. +{"User (jid)","Uživatel (JID)"}. {"User Management","Správa uživatelů"}. +{"User session not found","Sezení uživatele nebylo nalezeno"}. +{"User session terminated","Sezení uživatele bylo ukončeno"}. {"Username:","Uživatelské jméno:"}. {"Users are not allowed to register accounts so quickly","Je zakázáno registrovat účty v tak rychlém sledu"}. {"Users Last Activity","Poslední aktivita uživatele"}. {"Users","Uživatelé"}. -{"User ","Uživatel "}. {"User","Uživatel"}. -{"Validate","Ověřit"}. -{"vCard User Search","Hledání uživatelů podle vizitek"}. +{"Value 'get' of 'type' attribute is not allowed","Hodnota 'get' atrubutu 'type' není povolena"}. +{"Value of '~s' should be boolean","Hodnota '~s' by měla být boolean"}. +{"Value of '~s' should be datetime string","Hodnota '~s' by měla být datetime řetězec"}. +{"Value of '~s' should be integer","Hodnota '~s' by měla být celé číslo"}. +{"Value 'set' of 'type' attribute is not allowed","Hodnota 'set' atrubutu 'type' není povolena"}. +{"vCard User Search","Hledání uživatelů ve vizitkách"}. {"Virtual Hosts","Virtuální hostitelé"}. +{"Visitor","Návštěvník"}. {"Visitors are not allowed to change their nicknames in this room","Návštěvníkům této místnosti je zakázáno měnit přezdívku"}. -{"Visitors are not allowed to send messages to all occupants","Návštevníci nemají povoleno zasílat zprávy všem účastníkům konference"}. -{"Voice requests are disabled in this conference","Voice žádosti jsou v této konferenci zakázány"}. +{"Visitors are not allowed to send messages to all occupants","Návštevníci nemají povoleno zasílat zprávy všem účastníkům v této místnosti"}. +{"Voice requests are disabled in this conference","Voice žádosti jsou v této místnosti zakázány"}. {"Voice request","Žádost o voice práva"}. {"Wednesday","Středa"}. {"When to send the last published item","Kdy odeslat poslední publikovanou položku"}. {"Whether to allow subscriptions","Povolit odebírání"}. -{"You can later change your password using a Jabber client.","Později můžete své heslo změnit pomocí klienta Jabberu."}. {"You have been banned from this room","Byl jste vyloučen z této místnosti"}. +{"You have joined too many conferences","Vstoupil jste do příliš velkého množství místností"}. {"You must fill in field \"Nickname\" in the form","Musíte vyplnit políčko \"Přezdívka\" ve formuláři"}. {"You need a client that supports x:data and CAPTCHA to register","Pro registraci potřebujete klienta s podporou x:data a CAPTCHA"}. {"You need a client that supports x:data to register the nickname","Pro registraci přezdívky potřebujete klienta s podporou x:data"}. -{"You need an x:data capable client to configure mod_irc settings","Pro konfiguraci mod_irc potřebujete klienta s podporou x:data"}. -{"You need an x:data capable client to configure room","Ke konfiguraci místnosti potřebujete klienta podporujícího x:data"}. {"You need an x:data capable client to search","K vyhledávání potřebujete klienta podporujícího x:data"}. {"Your active privacy list has denied the routing of this stanza.","Vaše nastavení soukromí znemožnilo směrování této stance."}. {"Your contact offline message queue is full. The message has been discarded.","Fronta offline zpráv pro váš kontakt je plná. Zpráva byla zahozena."}. -{"Your Jabber account was successfully created.","Váš účet Jabberu byl úspěšně vytvořen."}. -{"Your Jabber account was successfully deleted.","Váš účet Jabberu byl úspěšně smazán."}. -{"Your messages to ~s are being blocked. To unblock them, visit ~s","Nesmíte posílat zprávy na ~s. Pro povolení navštivte ~s"}. +{"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","Nesmíte posílat zprávy na ~s. Pro povolení navštivte ~s"}. +{"You're not allowed to create nodes","Nemáte povoleno vytvářet uzly"}. diff --git a/priv/msgs/cs.po b/priv/msgs/cs.po deleted file mode 100644 index 789da39b8..000000000 --- a/priv/msgs/cs.po +++ /dev/null @@ -1,1838 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: 2.1.0-alpha\n" -"Last-Translator: Lukáš Polívka [spike411] xmpp:spike411@jabber.cz\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Language: Czech (čeština)\n" -"X-Additional-Translator: Milos Svasek [DuxforD] from openheads.net\n" - -#: ejabberd_c2s.erl:424 ejabberd_c2s.erl:727 -msgid "Use of STARTTLS required" -msgstr "Je vyžadováno STARTTLS" - -#: ejabberd_c2s.erl:503 -msgid "No resource provided" -msgstr "Nebyl poskytnut žádný zdroj" - -#: ejabberd_c2s.erl:1197 -msgid "Replaced by new connection" -msgstr "Nahrazeno novým spojením" - -#: ejabberd_c2s.erl:1885 -msgid "Your active privacy list has denied the routing of this stanza." -msgstr "Vaše nastavení soukromí znemožnilo směrování této stance." - -#: ejabberd_captcha.erl:96 ejabberd_captcha.erl:152 ejabberd_captcha.erl:178 -msgid "Enter the text you see" -msgstr "Zadejte text, který vidíte" - -#: ejabberd_captcha.erl:101 -msgid "Your messages to ~s are being blocked. To unblock them, visit ~s" -msgstr "Nesmíte posílat zprávy na ~s. Pro povolení navštivte ~s" - -#: ejabberd_captcha.erl:134 -msgid "If you don't see the CAPTCHA image here, visit the web page." -msgstr "Pokud zde nevidíte obrázek CAPTCHA, přejděte na webovou stránku." - -#: ejabberd_captcha.erl:146 -msgid "CAPTCHA web page" -msgstr "Webová stránka CAPTCHA" - -#: ejabberd_captcha.erl:307 -msgid "The CAPTCHA is valid." -msgstr "CAPTCHA souhlasí." - -#: mod_adhoc.erl:95 mod_adhoc.erl:125 mod_adhoc.erl:143 mod_adhoc.erl:161 -msgid "Commands" -msgstr "Příkazy" - -#: mod_adhoc.erl:149 mod_adhoc.erl:243 -msgid "Ping" -msgstr "Ping" - -#: mod_adhoc.erl:260 -msgid "Pong" -msgstr "Pong" - -#: mod_announce.erl:507 mod_announce_odbc.erl:499 -msgid "Really delete message of the day?" -msgstr "Skutečně smazat zprávu dne?" - -#: mod_announce.erl:515 mod_announce_odbc.erl:507 mod_configure.erl:1083 -#: mod_configure.erl:1128 -msgid "Subject" -msgstr "Předmět" - -#: mod_announce.erl:520 mod_announce_odbc.erl:512 mod_configure.erl:1088 -#: mod_configure.erl:1133 -msgid "Message body" -msgstr "Tělo zprávy" - -#: mod_announce.erl:600 mod_announce_odbc.erl:592 -msgid "No body provided for announce message" -msgstr "Zpráva neobsahuje text" - -#: mod_announce.erl:635 mod_announce_odbc.erl:627 -msgid "Announcements" -msgstr "Oznámení" - -#: mod_announce.erl:637 mod_announce_odbc.erl:629 -msgid "Send announcement to all users" -msgstr "Odeslat oznámení všem uživatelům" - -#: mod_announce.erl:639 mod_announce_odbc.erl:631 -msgid "Send announcement to all users on all hosts" -msgstr "Odeslat oznámení všem uživatelům na všech hostitelích" - -#: mod_announce.erl:641 mod_announce_odbc.erl:633 -msgid "Send announcement to all online users" -msgstr "Odeslat oznámení všem online uživatelům" - -#: mod_announce.erl:643 mod_announce_odbc.erl:635 mod_configure.erl:1078 -#: mod_configure.erl:1123 -msgid "Send announcement to all online users on all hosts" -msgstr "Odeslat oznámení všem online uživatelům na všech hostitelích" - -#: mod_announce.erl:645 mod_announce_odbc.erl:637 -msgid "Set message of the day and send to online users" -msgstr "Nastavit zprávu dne a odeslat ji online uživatelům" - -#: mod_announce.erl:647 mod_announce_odbc.erl:639 -msgid "Set message of the day on all hosts and send to online users" -msgstr "Nastavit zprávu dne a odeslat ji online uživatelům" - -#: mod_announce.erl:649 mod_announce_odbc.erl:641 -msgid "Update message of the day (don't send)" -msgstr "Aktualizovat zprávu dne (neodesílat)" - -#: mod_announce.erl:651 mod_announce_odbc.erl:643 -msgid "Update message of the day on all hosts (don't send)" -msgstr "Aktualizovat zprávu dne pro všechny hostitele (neodesílat)" - -#: mod_announce.erl:653 mod_announce_odbc.erl:645 -msgid "Delete message of the day" -msgstr "Smazat zprávu dne" - -#: mod_announce.erl:655 mod_announce_odbc.erl:647 -msgid "Delete message of the day on all hosts" -msgstr "Smazat zprávu dne na všech hostitelích" - -#: mod_configure.erl:114 mod_configure.erl:274 mod_configure.erl:296 -#: mod_configure.erl:498 -msgid "Configuration" -msgstr "Konfigurace" - -#: mod_configure.erl:125 mod_configure.erl:576 web/ejabberd_web_admin.erl:1936 -msgid "Database" -msgstr "Databáze" - -#: mod_configure.erl:127 mod_configure.erl:595 -msgid "Start Modules" -msgstr "Spustit moduly" - -#: mod_configure.erl:129 mod_configure.erl:596 -msgid "Stop Modules" -msgstr "Zastavit moduly" - -#: mod_configure.erl:131 mod_configure.erl:604 web/ejabberd_web_admin.erl:1937 -msgid "Backup" -msgstr "Zálohovat" - -#: mod_configure.erl:133 mod_configure.erl:605 -msgid "Restore" -msgstr "Obnovit" - -#: mod_configure.erl:135 mod_configure.erl:606 -msgid "Dump to Text File" -msgstr "Uložit do textového souboru" - -#: mod_configure.erl:137 mod_configure.erl:615 -msgid "Import File" -msgstr "Import souboru" - -#: mod_configure.erl:139 mod_configure.erl:616 -msgid "Import Directory" -msgstr "Import adresáře" - -#: mod_configure.erl:141 mod_configure.erl:581 mod_configure.erl:1057 -msgid "Restart Service" -msgstr "Restartovat službu" - -#: mod_configure.erl:143 mod_configure.erl:582 mod_configure.erl:1102 -msgid "Shut Down Service" -msgstr "Vypnout službu" - -#: mod_configure.erl:145 mod_configure.erl:518 mod_configure.erl:1197 -#: web/ejabberd_web_admin.erl:1527 -msgid "Add User" -msgstr "Přidat uživatele" - -#: mod_configure.erl:147 mod_configure.erl:519 mod_configure.erl:1219 -msgid "Delete User" -msgstr "Smazat uživatele" - -#: mod_configure.erl:149 mod_configure.erl:520 mod_configure.erl:1231 -msgid "End User Session" -msgstr "Ukončit sezení uživatele" - -#: mod_configure.erl:151 mod_configure.erl:521 mod_configure.erl:1243 -#: mod_configure.erl:1255 -msgid "Get User Password" -msgstr "Získat heslo uživatele" - -#: mod_configure.erl:153 mod_configure.erl:522 -msgid "Change User Password" -msgstr "Změnit heslo uživatele" - -#: mod_configure.erl:155 mod_configure.erl:523 mod_configure.erl:1272 -msgid "Get User Last Login Time" -msgstr "Získat čas podleního přihlášení uživatele" - -#: mod_configure.erl:157 mod_configure.erl:524 mod_configure.erl:1284 -msgid "Get User Statistics" -msgstr "Získat statistiky uživatele" - -#: mod_configure.erl:159 mod_configure.erl:525 -msgid "Get Number of Registered Users" -msgstr "Získat počet registrovaných uživatelů" - -#: mod_configure.erl:161 mod_configure.erl:526 -msgid "Get Number of Online Users" -msgstr "Získat počet online uživatelů" - -#: mod_configure.erl:163 mod_configure.erl:509 web/ejabberd_web_admin.erl:831 -#: web/ejabberd_web_admin.erl:872 -msgid "Access Control Lists" -msgstr "Seznamy přístupových práv (ACL)" - -#: mod_configure.erl:165 mod_configure.erl:510 web/ejabberd_web_admin.erl:940 -#: web/ejabberd_web_admin.erl:976 -msgid "Access Rules" -msgstr "Pravidla přístupů" - -#: mod_configure.erl:297 mod_configure.erl:499 -msgid "User Management" -msgstr "Správa uživatelů" - -#: mod_configure.erl:500 web/ejabberd_web_admin.erl:1058 -#: web/ejabberd_web_admin.erl:1462 -msgid "Online Users" -msgstr "Online uživatelé" - -#: mod_configure.erl:501 -msgid "All Users" -msgstr "Všichni uživatelé" - -#: mod_configure.erl:502 -msgid "Outgoing s2s Connections" -msgstr "Odchozí s2s spojení" - -#: mod_configure.erl:503 web/ejabberd_web_admin.erl:1907 -msgid "Running Nodes" -msgstr "Běžící uzly" - -#: mod_configure.erl:504 web/ejabberd_web_admin.erl:1909 -msgid "Stopped Nodes" -msgstr "Zastavené uzly" - -#: mod_configure.erl:577 mod_configure.erl:587 web/ejabberd_web_admin.erl:1953 -msgid "Modules" -msgstr "Moduly" - -#: mod_configure.erl:578 -msgid "Backup Management" -msgstr "Správa zálohování" - -#: mod_configure.erl:579 -msgid "Import Users From jabberd14 Spool Files" -msgstr "Importovat uživatele z jabberd14 spool souborů" - -#: mod_configure.erl:699 -msgid "To ~s" -msgstr "Pro ~s" - -#: mod_configure.erl:717 -msgid "From ~s" -msgstr "Od ~s" - -#: mod_configure.erl:913 -msgid "Database Tables Configuration at " -msgstr "Konfigurace databázových tabulek " - -#: mod_configure.erl:918 -msgid "Choose storage type of tables" -msgstr "Vyberte typ úložiště pro tabulky" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Disc only copy" -msgstr "Jen kopie disku" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM and disc copy" -msgstr "Kopie RAM a disku" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM copy" -msgstr "Kopie RAM" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Remote copy" -msgstr "Vzdálená kopie" - -#: mod_configure.erl:950 -msgid "Stop Modules at " -msgstr "Zastavit moduly na " - -#: mod_configure.erl:954 -msgid "Choose modules to stop" -msgstr "Vyberte moduly, které mají být zastaveny" - -#: mod_configure.erl:969 -msgid "Start Modules at " -msgstr "Spustit moduly na " - -#: mod_configure.erl:973 -msgid "Enter list of {Module, [Options]}" -msgstr "Vložte seznam modulů {Modul, [Parametry]}" - -#: mod_configure.erl:974 -msgid "List of modules to start" -msgstr "Seznam modulů, které mají být spuštěné" - -#: mod_configure.erl:983 -msgid "Backup to File at " -msgstr "Záloha do souboru na " - -#: mod_configure.erl:987 mod_configure.erl:1001 -msgid "Enter path to backup file" -msgstr "Zadajte cestu k souboru se zálohou" - -#: mod_configure.erl:988 mod_configure.erl:1002 mod_configure.erl:1016 -#: mod_configure.erl:1030 -msgid "Path to File" -msgstr "Cesta k souboru" - -#: mod_configure.erl:997 -msgid "Restore Backup from File at " -msgstr "Obnovit zálohu ze souboru na " - -#: mod_configure.erl:1011 -msgid "Dump Backup to Text File at " -msgstr "Uložit zálohu do textového souboru na " - -#: mod_configure.erl:1015 -msgid "Enter path to text file" -msgstr "Zadajte cestu k textovému souboru" - -#: mod_configure.erl:1025 -msgid "Import User from File at " -msgstr "Importovat uživatele ze souboru na " - -#: mod_configure.erl:1029 -msgid "Enter path to jabberd14 spool file" -msgstr "Zadejte cestu k spool souboru jabberd14" - -#: mod_configure.erl:1039 -msgid "Import Users from Dir at " -msgstr "Importovat uživatele z adresáře na " - -#: mod_configure.erl:1043 -msgid "Enter path to jabberd14 spool dir" -msgstr "Zadejte cestu k jabberd14 spool adresáři" - -#: mod_configure.erl:1044 -msgid "Path to Dir" -msgstr "Cesta k adresáři" - -#: mod_configure.erl:1060 mod_configure.erl:1105 -msgid "Time delay" -msgstr "Časový posun" - -#: mod_configure.erl:1143 -msgid "Access Control List Configuration" -msgstr "Konfigurace seznamu přístupových práv (ACL)" - -#: mod_configure.erl:1147 -msgid "Access control lists" -msgstr "Seznamy přístupových práv (ACL)" - -#: mod_configure.erl:1171 -msgid "Access Configuration" -msgstr "Konfigurace přístupů" - -#: mod_configure.erl:1175 -msgid "Access rules" -msgstr "Pravidla přístupů" - -#: mod_configure.erl:1200 mod_configure.erl:1222 mod_configure.erl:1234 -#: mod_configure.erl:1246 mod_configure.erl:1258 mod_configure.erl:1275 -#: mod_configure.erl:1287 mod_configure.erl:1650 mod_configure.erl:1700 -#: mod_configure.erl:1721 mod_roster.erl:943 mod_roster_odbc.erl:1060 -#: mod_vcard.erl:472 mod_vcard_ldap.erl:554 mod_vcard_odbc.erl:448 -msgid "Jabber ID" -msgstr "Jabber ID" - -#: mod_configure.erl:1205 mod_configure.erl:1263 mod_configure.erl:1651 -#: mod_configure.erl:1863 mod_muc/mod_muc_room.erl:3224 mod_register.erl:235 -#: web/ejabberd_web_admin.erl:1520 -msgid "Password" -msgstr "Heslo" - -#: mod_configure.erl:1210 -msgid "Password Verification" -msgstr "Ověření hesla" - -#: mod_configure.erl:1301 -msgid "Number of registered users" -msgstr "Počet registrovaných uživatelů" - -#: mod_configure.erl:1315 -msgid "Number of online users" -msgstr "Počet online uživatelů" - -#: mod_configure.erl:1682 web/ejabberd_web_admin.erl:1588 -#: web/ejabberd_web_admin.erl:1743 -msgid "Never" -msgstr "Nikdy" - -#: mod_configure.erl:1696 web/ejabberd_web_admin.erl:1601 -#: web/ejabberd_web_admin.erl:1756 -msgid "Online" -msgstr "Online" - -#: mod_configure.erl:1701 -msgid "Last login" -msgstr "Poslední přihlášení" - -#: mod_configure.erl:1722 -msgid "Roster size" -msgstr "Velikost seznamu kontaktů" - -#: mod_configure.erl:1723 -msgid "IP addresses" -msgstr "IP adresy" - -#: mod_configure.erl:1724 -msgid "Resources" -msgstr "Zdroje" - -#: mod_configure.erl:1850 -msgid "Administration of " -msgstr "Administrace " - -#: mod_configure.erl:1853 -msgid "Action on user" -msgstr "Akce aplikovaná na uživatele" - -#: mod_configure.erl:1857 -msgid "Edit Properties" -msgstr "Upravit vlastnosti" - -#: mod_configure.erl:1860 web/ejabberd_web_admin.erl:1769 -msgid "Remove User" -msgstr "Odstranit uživatele" - -#: mod_irc/mod_irc.erl:201 mod_irc/mod_irc_odbc.erl:196 -#: mod_muc/mod_muc.erl:336 mod_muc/mod_muc_odbc.erl:342 -msgid "Access denied by service policy" -msgstr "Přístup byl zamítnut nastavením služby" - -#: mod_irc/mod_irc.erl:401 mod_irc/mod_irc_odbc.erl:398 -msgid "IRC Transport" -msgstr "IRC transport" - -#: mod_irc/mod_irc.erl:428 mod_irc/mod_irc_odbc.erl:425 -msgid "ejabberd IRC module" -msgstr "ejabberd IRC modul" - -#: mod_irc/mod_irc.erl:559 mod_irc/mod_irc_odbc.erl:568 -msgid "You need an x:data capable client to configure mod_irc settings" -msgstr "Pro konfiguraci mod_irc potřebujete klienta s podporou x:data" - -#: mod_irc/mod_irc.erl:566 mod_irc/mod_irc_odbc.erl:575 -msgid "Registration in mod_irc for " -msgstr "Registrace do mod_irc na " - -#: mod_irc/mod_irc.erl:571 mod_irc/mod_irc_odbc.erl:580 -msgid "" -"Enter username, encodings, ports and passwords you wish to use for " -"connecting to IRC servers" -msgstr "" -"Zadejte přezdívku, kódování, porty a hesla, které chcete používat pro " -"připojení k serverům IRC" - -#: mod_irc/mod_irc.erl:576 mod_irc/mod_irc_odbc.erl:585 -msgid "IRC Username" -msgstr "IRC přezdívka" - -#: mod_irc/mod_irc.erl:586 mod_irc/mod_irc_odbc.erl:595 -msgid "" -"If you want to specify different ports, passwords, encodings for IRC " -"servers, fill this list with values in format '{\"irc server\", \"encoding" -"\", port, \"password\"}'. By default this service use \"~s\" encoding, port " -"~p, empty password." -msgstr "" -"Pokud chcete zadat jiné kódování pro IRC servery, vyplňte seznam s hodnotami " -"ve formátu '{\"irc server\",\"encoding\", port, \"password\"}'. Výchozí " -"kódování pro tuto službu je \"~s\", port ~p, empty password." - -#: mod_irc/mod_irc.erl:598 mod_irc/mod_irc_odbc.erl:607 -msgid "" -"Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef." -"net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]." -msgstr "" -"Příklad: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef." -"net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}].2\"}]." - -#: mod_irc/mod_irc.erl:603 mod_irc/mod_irc_odbc.erl:612 -msgid "Connections parameters" -msgstr "Parametry spojení" - -#: mod_irc/mod_irc.erl:728 mod_irc/mod_irc_odbc.erl:757 -msgid "Join IRC channel" -msgstr "Vstoupit do IRC kanálu" - -#: mod_irc/mod_irc.erl:732 mod_irc/mod_irc_odbc.erl:761 -msgid "IRC channel (don't put the first #)" -msgstr "IRC kanál (bez počátečního #)" - -#: mod_irc/mod_irc.erl:737 mod_irc/mod_irc_odbc.erl:766 -msgid "IRC server" -msgstr "IRC přezdívka" - -#: mod_irc/mod_irc.erl:770 mod_irc/mod_irc.erl:774 -#: mod_irc/mod_irc_odbc.erl:799 mod_irc/mod_irc_odbc.erl:803 -msgid "Join the IRC channel here." -msgstr "Vstoupit do tohoto IRC kanálu." - -#: mod_irc/mod_irc.erl:778 mod_irc/mod_irc_odbc.erl:807 -msgid "Join the IRC channel in this Jabber ID: ~s" -msgstr "Vstupte do IRC kanálu s tímto Jabber ID: ~s" - -#: mod_irc/mod_irc.erl:863 mod_irc/mod_irc_odbc.erl:904 -msgid "IRC settings" -msgstr "Nastavení IRC" - -#: mod_irc/mod_irc.erl:868 mod_irc/mod_irc_odbc.erl:909 -msgid "" -"Enter username and encodings you wish to use for connecting to IRC servers. " -"Press 'Next' to get more fields to fill in. Press 'Complete' to save " -"settings." -msgstr "" -"Zadejte přezdívku a kódování, které chcete používat pro připojení k serverům " -"IRC. Stiskněte 'Další' pro více políček k vyplnění. Stiskněte 'Dokončit' pro " -"uložení nastavení." - -#: mod_irc/mod_irc.erl:874 mod_irc/mod_irc_odbc.erl:915 -msgid "IRC username" -msgstr "IRC přezdívka" - -#: mod_irc/mod_irc.erl:923 mod_irc/mod_irc_odbc.erl:964 -msgid "Password ~b" -msgstr "Heslo ~b" - -#: mod_irc/mod_irc.erl:928 mod_irc/mod_irc_odbc.erl:969 -msgid "Port ~b" -msgstr "Port ~b" - -#: mod_irc/mod_irc.erl:933 mod_irc/mod_irc_odbc.erl:974 -msgid "Encoding for server ~b" -msgstr "Kódování pro server ~b" - -#: mod_irc/mod_irc.erl:942 mod_irc/mod_irc_odbc.erl:983 -msgid "Server ~b" -msgstr "Server ~b" - -#: mod_muc/mod_muc.erl:449 mod_muc/mod_muc_odbc.erl:456 -msgid "Only service administrators are allowed to send service messages" -msgstr "Pouze správci služby smí odesílat servisní zprávy" - -#: mod_muc/mod_muc.erl:493 mod_muc/mod_muc_odbc.erl:500 -msgid "Room creation is denied by service policy" -msgstr "Pravidla služby nepovolují vytvořit místnost" - -#: mod_muc/mod_muc.erl:500 mod_muc/mod_muc_odbc.erl:507 -msgid "Conference room does not exist" -msgstr "Konferenční místnost neexistuje" - -#: mod_muc/mod_muc.erl:582 mod_muc/mod_muc_odbc.erl:595 -msgid "Chatrooms" -msgstr "Konference" - -#: mod_muc/mod_muc.erl:712 mod_muc/mod_muc_odbc.erl:729 -msgid "You need a client that supports x:data to register the nickname" -msgstr "Pro registraci přezdívky potřebujete klienta s podporou x:data" - -#: mod_muc/mod_muc.erl:718 mod_muc/mod_muc_odbc.erl:735 -msgid "Nickname Registration at " -msgstr "Registrace přezdívky na " - -#: mod_muc/mod_muc.erl:722 mod_muc/mod_muc_odbc.erl:739 -msgid "Enter nickname you want to register" -msgstr "Zadejte přezdívku, kterou chcete zaregistrovat" - -#: mod_muc/mod_muc.erl:723 mod_muc/mod_muc_odbc.erl:740 -#: mod_muc/mod_muc_room.erl:3810 mod_roster.erl:944 mod_roster_odbc.erl:1061 -#: mod_vcard.erl:364 mod_vcard.erl:477 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:453 -msgid "Nickname" -msgstr "Přezdívka" - -#: mod_muc/mod_muc.erl:762 mod_muc/mod_muc_odbc.erl:784 -#: mod_muc/mod_muc_room.erl:1064 mod_muc/mod_muc_room.erl:1787 -msgid "That nickname is registered by another person" -msgstr "Přezdívka je zaregistrována jinou osobou" - -#: mod_muc/mod_muc.erl:788 mod_muc/mod_muc_odbc.erl:811 -msgid "You must fill in field \"Nickname\" in the form" -msgstr "Musíte vyplnit políčko \"Přezdívka\" ve formuláři" - -#: mod_muc/mod_muc.erl:808 mod_muc/mod_muc_odbc.erl:831 -msgid "ejabberd MUC module" -msgstr "ejabberd MUC modul" - -#: mod_muc/mod_muc_log.erl:374 mod_muc/mod_muc_log.erl:381 -msgid "Chatroom configuration modified" -msgstr "Nastavení diskuzní místnosti bylo změněno" - -#: mod_muc/mod_muc_log.erl:384 -msgid "joins the room" -msgstr "vstoupil(a) do místnosti" - -#: mod_muc/mod_muc_log.erl:387 mod_muc/mod_muc_log.erl:390 -msgid "leaves the room" -msgstr "opustil(a) místnost" - -#: mod_muc/mod_muc_log.erl:393 mod_muc/mod_muc_log.erl:396 -msgid "has been banned" -msgstr "byl(a) zablokován(a)" - -#: mod_muc/mod_muc_log.erl:399 mod_muc/mod_muc_log.erl:402 -msgid "has been kicked" -msgstr "byl(a) vyhozen(a) z místnosti" - -#: mod_muc/mod_muc_log.erl:405 -msgid "has been kicked because of an affiliation change" -msgstr "byl(a) vyhozen(a) kvůli změně přiřazení" - -#: mod_muc/mod_muc_log.erl:408 -msgid "has been kicked because the room has been changed to members-only" -msgstr "byl(a) vyhozen(a), protože mísnost je nyní pouze pro členy" - -#: mod_muc/mod_muc_log.erl:411 -msgid "has been kicked because of a system shutdown" -msgstr "byl(a) vyhozen(a), protože dojde k vypnutí systému" - -#: mod_muc/mod_muc_log.erl:414 -msgid "is now known as" -msgstr "se přejmenoval(a) na" - -#: mod_muc/mod_muc_log.erl:417 mod_muc/mod_muc_log.erl:688 -#: mod_muc/mod_muc_room.erl:2393 -msgid " has set the subject to: " -msgstr " změnil(a) téma na: " - -#: mod_muc/mod_muc_log.erl:452 -msgid "Chatroom is created" -msgstr "Konference vytvořena" - -#: mod_muc/mod_muc_log.erl:453 -msgid "Chatroom is destroyed" -msgstr "Konference zrušena" - -#: mod_muc/mod_muc_log.erl:454 -msgid "Chatroom is started" -msgstr "Konference spuštěna" - -#: mod_muc/mod_muc_log.erl:455 -msgid "Chatroom is stopped" -msgstr "Konference zastavena" - -#: mod_muc/mod_muc_log.erl:459 -msgid "Monday" -msgstr "Pondělí" - -#: mod_muc/mod_muc_log.erl:460 -msgid "Tuesday" -msgstr "Úterý" - -#: mod_muc/mod_muc_log.erl:461 -msgid "Wednesday" -msgstr "Středa" - -#: mod_muc/mod_muc_log.erl:462 -msgid "Thursday" -msgstr "Čtvrtek" - -#: mod_muc/mod_muc_log.erl:463 -msgid "Friday" -msgstr "Pátek" - -#: mod_muc/mod_muc_log.erl:464 -msgid "Saturday" -msgstr "Sobota" - -#: mod_muc/mod_muc_log.erl:465 -msgid "Sunday" -msgstr "Neděle" - -#: mod_muc/mod_muc_log.erl:469 -msgid "January" -msgstr ". ledna" - -#: mod_muc/mod_muc_log.erl:470 -msgid "February" -msgstr ". února" - -#: mod_muc/mod_muc_log.erl:471 -msgid "March" -msgstr ". března" - -#: mod_muc/mod_muc_log.erl:472 -msgid "April" -msgstr ". dubna" - -#: mod_muc/mod_muc_log.erl:473 -msgid "May" -msgstr ". května" - -#: mod_muc/mod_muc_log.erl:474 -msgid "June" -msgstr ". června" - -#: mod_muc/mod_muc_log.erl:475 -msgid "July" -msgstr ". července" - -#: mod_muc/mod_muc_log.erl:476 -msgid "August" -msgstr ". srpna" - -#: mod_muc/mod_muc_log.erl:477 -msgid "September" -msgstr ". září" - -#: mod_muc/mod_muc_log.erl:478 -msgid "October" -msgstr ". října" - -#: mod_muc/mod_muc_log.erl:479 -msgid "November" -msgstr ". listopadu" - -#: mod_muc/mod_muc_log.erl:480 -msgid "December" -msgstr ". prosince" - -#: mod_muc/mod_muc_log.erl:750 -msgid "Room Configuration" -msgstr "Nastavení místnosti" - -#: mod_muc/mod_muc_log.erl:759 -msgid "Room Occupants" -msgstr "Počet účastníků" - -#: mod_muc/mod_muc_room.erl:174 -msgid "Traffic rate limit is exceeded" -msgstr "Byl překročen limit" - -#: mod_muc/mod_muc_room.erl:246 -msgid "" -"This participant is kicked from the room because he sent an error message" -msgstr "Tento účastník byl vyhozen, protože odeslal chybovou zprávu" - -#: mod_muc/mod_muc_room.erl:255 -msgid "It is not allowed to send private messages to the conference" -msgstr "Není povoleno odesílat soukromé zprávy do konference" - -#: mod_muc/mod_muc_room.erl:332 -msgid "Please, wait for a while before sending new voice request" -msgstr "Prosím, počkejte chvíli před posláním nové žádosti o voice práva" - -#: mod_muc/mod_muc_room.erl:347 -msgid "Voice requests are disabled in this conference" -msgstr "Voice žádosti jsou v této konferenci zakázány" - -#: mod_muc/mod_muc_room.erl:364 -msgid "Failed to extract JID from your voice request approval" -msgstr "Došlo k chybě při získávání Jabber ID z vaší žádosti o voice práva" - -#: mod_muc/mod_muc_room.erl:393 -msgid "Only moderators can approve voice requests" -msgstr "Pouze moderátoři mohou schválit žádosti o voice práva" - -#: mod_muc/mod_muc_room.erl:408 -msgid "Improper message type" -msgstr "Nesprávný typ zprávy" - -#: mod_muc/mod_muc_room.erl:518 -msgid "" -"This participant is kicked from the room because he sent an error message to " -"another participant" -msgstr "" -"Tento účastník byl vyhozen, protože odeslal chybovou zprávu jinému " -"účastníkovi" - -#: mod_muc/mod_muc_room.erl:531 -msgid "It is not allowed to send private messages of type \"groupchat\"" -msgstr "Není dovoleno odeslání soukromé zprávy typu \"skupinová zpráva\" " - -#: mod_muc/mod_muc_room.erl:543 mod_muc/mod_muc_room.erl:611 -msgid "Recipient is not in the conference room" -msgstr "Příjemce se nenachází v konferenční místnosti" - -#: mod_muc/mod_muc_room.erl:564 mod_muc/mod_muc_room.erl:585 -msgid "It is not allowed to send private messages" -msgstr "Je zakázáno posílat soukromé zprávy" - -#: mod_muc/mod_muc_room.erl:576 mod_muc/mod_muc_room.erl:956 -#: mod_muc/mod_muc_room.erl:4040 -msgid "Only occupants are allowed to send messages to the conference" -msgstr "Jen členové mají povolené zasílat zprávy do konference" - -#: mod_muc/mod_muc_room.erl:634 -msgid "Only occupants are allowed to send queries to the conference" -msgstr "Jen členové mohou odesílat požadavky (query) do konference" - -#: mod_muc/mod_muc_room.erl:646 -msgid "Queries to the conference members are not allowed in this room" -msgstr "" -"Požadavky (queries) na členy konference nejsou v této místnosti povolené" - -#: mod_muc/mod_muc_room.erl:932 -msgid "" -"Only moderators and participants are allowed to change the subject in this " -"room" -msgstr "Jen moderátoři a účastníci mají povoleno měnit téma této místnosti" - -#: mod_muc/mod_muc_room.erl:937 -msgid "Only moderators are allowed to change the subject in this room" -msgstr "Jen moderátoři mají povoleno měnit téma místnosti" - -#: mod_muc/mod_muc_room.erl:947 -msgid "Visitors are not allowed to send messages to all occupants" -msgstr "Návštevníci nemají povoleno zasílat zprávy všem účastníkům konference" - -#: mod_muc/mod_muc_room.erl:1021 -msgid "" -"This participant is kicked from the room because he sent an error presence" -msgstr "Tento účastník byl vyhozen, protože odeslal chybový status" - -#: mod_muc/mod_muc_room.erl:1040 -msgid "Visitors are not allowed to change their nicknames in this room" -msgstr "Návštěvníkům této místnosti je zakázáno měnit přezdívku" - -#: mod_muc/mod_muc_room.erl:1053 mod_muc/mod_muc_room.erl:1779 -msgid "That nickname is already in use by another occupant" -msgstr "Přezdívka je již používána jiným členem" - -#: mod_muc/mod_muc_room.erl:1768 -msgid "You have been banned from this room" -msgstr "Byl jste vyloučen z této místnosti" - -#: mod_muc/mod_muc_room.erl:1771 -msgid "Membership is required to enter this room" -msgstr "Pro vstup do místnosti musíte být členem" - -#: mod_muc/mod_muc_room.erl:1807 -msgid "This room is not anonymous" -msgstr "Tato místnost není anonymní" - -#: mod_muc/mod_muc_room.erl:1833 -msgid "A password is required to enter this room" -msgstr "Pro vstup do místnosti musíte zadat heslo" - -#: mod_muc/mod_muc_room.erl:1855 mod_register.erl:246 -msgid "Too many CAPTCHA requests" -msgstr "Přiliš mnoho CAPTCHA žádostí" - -#: mod_muc/mod_muc_room.erl:1864 mod_register.erl:251 -msgid "Unable to generate a CAPTCHA" -msgstr "Nebylo možné vygenerovat CAPTCHA" - -#: mod_muc/mod_muc_room.erl:1874 -msgid "Incorrect password" -msgstr "Nesprávné heslo" - -#: mod_muc/mod_muc_room.erl:2448 -msgid "Administrator privileges required" -msgstr "Potřebujete práva administrátora" - -#: mod_muc/mod_muc_room.erl:2463 -msgid "Moderator privileges required" -msgstr "Potřebujete práva moderátora" - -#: mod_muc/mod_muc_room.erl:2619 -msgid "Jabber ID ~s is invalid" -msgstr "Jabber ID ~s je neplatné" - -#: mod_muc/mod_muc_room.erl:2633 -msgid "Nickname ~s does not exist in the room" -msgstr "Přezdívka ~s v místnosti neexistuje" - -#: mod_muc/mod_muc_room.erl:2659 mod_muc/mod_muc_room.erl:3049 -msgid "Invalid affiliation: ~s" -msgstr "Neplatné přiřazení: ~s" - -#: mod_muc/mod_muc_room.erl:2713 -msgid "Invalid role: ~s" -msgstr "Neplatná role: ~s" - -#: mod_muc/mod_muc_room.erl:3026 mod_muc/mod_muc_room.erl:3062 -msgid "Owner privileges required" -msgstr "Jsou vyžadována práva vlastníka" - -#: mod_muc/mod_muc_room.erl:3195 -msgid "Configuration of room ~s" -msgstr "Konfigurace místnosti ~s" - -#: mod_muc/mod_muc_room.erl:3200 -msgid "Room title" -msgstr "Název místnosti" - -#: mod_muc/mod_muc_room.erl:3203 mod_muc/mod_muc_room.erl:3692 -msgid "Room description" -msgstr "Popis místnosti" - -#: mod_muc/mod_muc_room.erl:3210 -msgid "Make room persistent" -msgstr "Nastavit místnost jako stálou" - -#: mod_muc/mod_muc_room.erl:3215 -msgid "Make room public searchable" -msgstr "Nastavit místnost jako veřejnou" - -#: mod_muc/mod_muc_room.erl:3218 -msgid "Make participants list public" -msgstr "Nastavit seznam účastníků jako veřejný" - -#: mod_muc/mod_muc_room.erl:3221 -msgid "Make room password protected" -msgstr "Chránit místnost heslem" - -#: mod_muc/mod_muc_room.erl:3232 -msgid "Maximum Number of Occupants" -msgstr "Počet účastníků" - -#: mod_muc/mod_muc_room.erl:3239 -msgid "No limit" -msgstr "Bez limitu" - -#: mod_muc/mod_muc_room.erl:3250 -msgid "Present real Jabber IDs to" -msgstr "Odhalovat skutečná Jabber ID" - -#: mod_muc/mod_muc_room.erl:3258 mod_muc/mod_muc_room.erl:3292 -msgid "moderators only" -msgstr "moderátorům" - -#: mod_muc/mod_muc_room.erl:3260 mod_muc/mod_muc_room.erl:3294 -msgid "anyone" -msgstr "každému" - -#: mod_muc/mod_muc_room.erl:3262 -msgid "Make room members-only" -msgstr "Zpřístupnit místnost jen členům" - -#: mod_muc/mod_muc_room.erl:3265 -msgid "Make room moderated" -msgstr "Nastavit místnost jako moderovanou" - -#: mod_muc/mod_muc_room.erl:3268 -msgid "Default users as participants" -msgstr "Uživatelé jsou implicitně členy" - -#: mod_muc/mod_muc_room.erl:3271 -msgid "Allow users to change the subject" -msgstr "Povolit uživatelům měnit téma místnosti" - -#: mod_muc/mod_muc_room.erl:3274 -msgid "Allow users to send private messages" -msgstr "Povolit uživatelům odesílat soukromé zprávy" - -#: mod_muc/mod_muc_room.erl:3279 -msgid "Allow visitors to send private messages to" -msgstr "Povolit návštěvníkům odesílat soukromé zprávy" - -#: mod_muc/mod_muc_room.erl:3290 -msgid "nobody" -msgstr "nikdo" - -#: mod_muc/mod_muc_room.erl:3296 -msgid "Allow users to query other users" -msgstr "Povolit uživatelům odesílat požadavky (query) ostatním uživatelům" - -#: mod_muc/mod_muc_room.erl:3299 -msgid "Allow users to send invites" -msgstr "Povolit uživatelům posílání pozvánek" - -#: mod_muc/mod_muc_room.erl:3302 -msgid "Allow visitors to send status text in presence updates" -msgstr "Povolit návštěvníkům posílat stavové zprávy ve statusu" - -#: mod_muc/mod_muc_room.erl:3305 -msgid "Allow visitors to change nickname" -msgstr "Povolit návštěvníkům měnit přezdívku" - -#: mod_muc/mod_muc_room.erl:3308 -msgid "Allow visitors to send voice requests" -msgstr "Povolit uživatelům posílat žádosti o voice práva" - -#: mod_muc/mod_muc_room.erl:3311 -msgid "Minimum interval between voice requests (in seconds)" -msgstr "Minimální interval mezi žádostmi o voice práva (v sekundách)" - -#: mod_muc/mod_muc_room.erl:3317 -msgid "Make room CAPTCHA protected" -msgstr "Chránit místnost pomocí CAPTCHA" - -#: mod_muc/mod_muc_room.erl:3322 -msgid "Exclude Jabber IDs from CAPTCHA challenge" -msgstr "Vyloučit Jabber ID z procesu CAPTCHA ověřování" - -#: mod_muc/mod_muc_room.erl:3329 -msgid "Enable logging" -msgstr "Zaznamenávat konverzace" - -#: mod_muc/mod_muc_room.erl:3337 -msgid "You need an x:data capable client to configure room" -msgstr "Ke konfiguraci místnosti potřebujete klienta podporujícího x:data" - -#: mod_muc/mod_muc_room.erl:3694 -msgid "Number of occupants" -msgstr "Počet účastníků" - -#: mod_muc/mod_muc_room.erl:3750 -msgid "private, " -msgstr "soukromá, " - -#: mod_muc/mod_muc_room.erl:3799 -msgid "Voice request" -msgstr "Žádost o voice práva" - -#: mod_muc/mod_muc_room.erl:3803 -msgid "Either approve or decline the voice request." -msgstr "Povolit nebo odmítnout voice žádost." - -#: mod_muc/mod_muc_room.erl:3809 -msgid "User JID" -msgstr "Jabber ID uživatele" - -#: mod_muc/mod_muc_room.erl:3811 -msgid "Grant voice to this person?" -msgstr "Udělit voice práva této osobě?" - -#: mod_muc/mod_muc_room.erl:3960 -msgid "~s invites you to the room ~s" -msgstr "~s vás zve do místnosti ~s" - -#: mod_muc/mod_muc_room.erl:3969 -msgid "the password is" -msgstr "heslo je" - -#: mod_offline.erl:510 mod_offline_odbc.erl:352 -msgid "" -"Your contact offline message queue is full. The message has been discarded." -msgstr "Fronta offline zpráv pro váš kontakt je plná. Zpráva byla zahozena." - -#: mod_offline.erl:560 mod_offline_odbc.erl:408 -msgid "~s's Offline Messages Queue" -msgstr "Fronta offline zpráv uživatele ~s" - -#: mod_offline.erl:563 mod_offline_odbc.erl:411 mod_roster.erl:987 -#: mod_roster_odbc.erl:1104 mod_shared_roster.erl:901 -#: mod_shared_roster.erl:1010 mod_shared_roster_odbc.erl:930 -#: mod_shared_roster_odbc.erl:1039 web/ejabberd_web_admin.erl:833 -#: web/ejabberd_web_admin.erl:874 web/ejabberd_web_admin.erl:942 -#: web/ejabberd_web_admin.erl:978 web/ejabberd_web_admin.erl:1019 -#: web/ejabberd_web_admin.erl:1508 web/ejabberd_web_admin.erl:1760 -#: web/ejabberd_web_admin.erl:1931 web/ejabberd_web_admin.erl:1963 -#: web/ejabberd_web_admin.erl:2031 web/ejabberd_web_admin.erl:2135 -#: web/ejabberd_web_admin.erl:2160 web/ejabberd_web_admin.erl:2248 -msgid "Submitted" -msgstr "Odeslané" - -#: mod_offline.erl:571 -msgid "Time" -msgstr "Čas" - -#: mod_offline.erl:572 -msgid "From" -msgstr "Od" - -#: mod_offline.erl:573 -msgid "To" -msgstr "Pro" - -#: mod_offline.erl:574 mod_offline_odbc.erl:419 -msgid "Packet" -msgstr "Paket" - -#: mod_offline.erl:587 mod_offline_odbc.erl:432 mod_shared_roster.erl:908 -#: mod_shared_roster_odbc.erl:937 web/ejabberd_web_admin.erl:882 -#: web/ejabberd_web_admin.erl:986 -msgid "Delete Selected" -msgstr "Smazat vybrané" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -msgid "Offline Messages:" -msgstr "Offline zprávy:" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -msgid "Remove All Offline Messages" -msgstr "Odstranit všechny offline zprávy" - -#: mod_proxy65/mod_proxy65_service.erl:213 -msgid "ejabberd SOCKS5 Bytestreams module" -msgstr "ejabberd SOCKS5 Bytestreams modul" - -#: mod_pubsub/mod_pubsub.erl:1120 mod_pubsub/mod_pubsub_odbc.erl:929 -msgid "Publish-Subscribe" -msgstr "Publish-Subscribe" - -#: mod_pubsub/mod_pubsub.erl:1214 mod_pubsub/mod_pubsub_odbc.erl:1025 -msgid "ejabberd Publish-Subscribe module" -msgstr "ejabberd Publish-Subscribe modul" - -#: mod_pubsub/mod_pubsub.erl:1497 mod_pubsub/mod_pubsub_odbc.erl:1312 -msgid "PubSub subscriber request" -msgstr "Žádost odběratele PubSub" - -#: mod_pubsub/mod_pubsub.erl:1499 mod_pubsub/mod_pubsub_odbc.erl:1314 -msgid "Choose whether to approve this entity's subscription." -msgstr "Zvolte, zda chcete schválit odebírání touto entitou" - -#: mod_pubsub/mod_pubsub.erl:1505 mod_pubsub/mod_pubsub_odbc.erl:1320 -msgid "Node ID" -msgstr "ID uzlu" - -#: mod_pubsub/mod_pubsub.erl:1510 mod_pubsub/mod_pubsub_odbc.erl:1325 -msgid "Subscriber Address" -msgstr "Adresa odběratele" - -#: mod_pubsub/mod_pubsub.erl:1516 mod_pubsub/mod_pubsub_odbc.erl:1331 -msgid "Allow this Jabber ID to subscribe to this pubsub node?" -msgstr "Povolit tomuto Jabber ID odebírat tento pubsub uzel?" - -#: mod_pubsub/mod_pubsub.erl:3474 mod_pubsub/mod_pubsub_odbc.erl:3287 -msgid "Deliver payloads with event notifications" -msgstr "Doručovat náklad s upozorněním na událost" - -#: mod_pubsub/mod_pubsub.erl:3475 mod_pubsub/mod_pubsub_odbc.erl:3288 -msgid "Deliver event notifications" -msgstr "Doručovat upozornění na události" - -#: mod_pubsub/mod_pubsub.erl:3476 mod_pubsub/mod_pubsub_odbc.erl:3289 -msgid "Notify subscribers when the node configuration changes" -msgstr "Upozornit odběratele na změnu nastavení uzlu" - -#: mod_pubsub/mod_pubsub.erl:3477 mod_pubsub/mod_pubsub_odbc.erl:3290 -msgid "Notify subscribers when the node is deleted" -msgstr "Upozornit odběratele na smazání uzlu" - -#: mod_pubsub/mod_pubsub.erl:3478 mod_pubsub/mod_pubsub_odbc.erl:3291 -msgid "Notify subscribers when items are removed from the node" -msgstr "Upozornit odběratele na odstranění položek z uzlu" - -#: mod_pubsub/mod_pubsub.erl:3479 mod_pubsub/mod_pubsub_odbc.erl:3292 -msgid "Persist items to storage" -msgstr "Uložit položky natrvalo do úložiště" - -#: mod_pubsub/mod_pubsub.erl:3480 mod_pubsub/mod_pubsub_odbc.erl:3293 -msgid "A friendly name for the node" -msgstr "Přívětivé jméno pro uzel" - -#: mod_pubsub/mod_pubsub.erl:3481 mod_pubsub/mod_pubsub_odbc.erl:3294 -msgid "Max # of items to persist" -msgstr "Maximální počet položek, které je možné natrvalo uložit" - -#: mod_pubsub/mod_pubsub.erl:3482 mod_pubsub/mod_pubsub_odbc.erl:3295 -msgid "Whether to allow subscriptions" -msgstr "Povolit odebírání" - -#: mod_pubsub/mod_pubsub.erl:3483 mod_pubsub/mod_pubsub_odbc.erl:3296 -msgid "Specify the access model" -msgstr "Uveďte přístupový model" - -#: mod_pubsub/mod_pubsub.erl:3486 mod_pubsub/mod_pubsub_odbc.erl:3299 -msgid "Roster groups allowed to subscribe" -msgstr "Skupiny kontaktů, které mohou odebírat" - -#: mod_pubsub/mod_pubsub.erl:3487 mod_pubsub/mod_pubsub_odbc.erl:3300 -msgid "Specify the publisher model" -msgstr "Specifikovat model pro publikování" - -#: mod_pubsub/mod_pubsub.erl:3489 mod_pubsub/mod_pubsub_odbc.erl:3302 -msgid "Purge all items when the relevant publisher goes offline" -msgstr "Smazat všechny položky, pokud se příslušný poskytovatel odpojí" - -#: mod_pubsub/mod_pubsub.erl:3490 mod_pubsub/mod_pubsub_odbc.erl:3303 -msgid "Specify the event message type" -msgstr "Zvolte typ zpráv pro události" - -#: mod_pubsub/mod_pubsub.erl:3492 mod_pubsub/mod_pubsub_odbc.erl:3305 -msgid "Max payload size in bytes" -msgstr "Maximální náklad v bajtech" - -#: mod_pubsub/mod_pubsub.erl:3493 mod_pubsub/mod_pubsub_odbc.erl:3306 -msgid "When to send the last published item" -msgstr "Kdy odeslat poslední publikovanou položku" - -#: mod_pubsub/mod_pubsub.erl:3495 mod_pubsub/mod_pubsub_odbc.erl:3308 -msgid "Only deliver notifications to available users" -msgstr "Doručovat upozornění jen právě přihlášeným uživatelům" - -#: mod_pubsub/mod_pubsub.erl:3496 mod_pubsub/mod_pubsub_odbc.erl:3309 -msgid "The collections with which a node is affiliated" -msgstr "Kolekce, se kterými je uzel spřízněn" - -#: mod_register.erl:193 -msgid "The CAPTCHA verification has failed" -msgstr "Ověření CAPTCHA se nezdařilo" - -#: mod_register.erl:220 -msgid "You need a client that supports x:data and CAPTCHA to register" -msgstr "Pro registraci potřebujete klienta s podporou x:data a CAPTCHA" - -#: mod_register.erl:226 mod_register.erl:265 -msgid "Choose a username and password to register with this server" -msgstr "Zadejte jméno uživatele a heslo pro registraci na tomto serveru" - -#: mod_register.erl:230 mod_vcard.erl:364 mod_vcard_odbc.erl:342 -#: web/ejabberd_web_admin.erl:1515 web/ejabberd_web_admin.erl:1572 -msgid "User" -msgstr "Uživatel" - -#: mod_register.erl:316 mod_register.erl:361 -msgid "The password is too weak" -msgstr "Heslo je příliš slabé" - -#: mod_register.erl:365 -msgid "Users are not allowed to register accounts so quickly" -msgstr "Je zakázáno registrovat účty v tak rychlém sledu" - -#: mod_roster.erl:938 mod_roster_odbc.erl:1055 web/ejabberd_web_admin.erl:1701 -#: web/ejabberd_web_admin.erl:1886 web/ejabberd_web_admin.erl:1897 -#: web/ejabberd_web_admin.erl:2219 -msgid "None" -msgstr "Nic" - -#: mod_roster.erl:945 mod_roster_odbc.erl:1062 -msgid "Subscription" -msgstr "Přihlášení" - -#: mod_roster.erl:946 mod_roster_odbc.erl:1063 -msgid "Pending" -msgstr "Čekající" - -#: mod_roster.erl:947 mod_roster_odbc.erl:1064 -msgid "Groups" -msgstr "Skupiny" - -#: mod_roster.erl:974 mod_roster_odbc.erl:1091 -msgid "Validate" -msgstr "Ověřit" - -#: mod_roster.erl:982 mod_roster_odbc.erl:1099 -msgid "Remove" -msgstr "Odstranit" - -#: mod_roster.erl:985 mod_roster_odbc.erl:1102 -msgid "Roster of " -msgstr "Seznam kontaktů " - -#: mod_roster.erl:988 mod_roster_odbc.erl:1105 mod_shared_roster.erl:902 -#: mod_shared_roster.erl:1011 mod_shared_roster_odbc.erl:931 -#: mod_shared_roster_odbc.erl:1040 web/ejabberd_web_admin.erl:834 -#: web/ejabberd_web_admin.erl:875 web/ejabberd_web_admin.erl:943 -#: web/ejabberd_web_admin.erl:979 web/ejabberd_web_admin.erl:1020 -#: web/ejabberd_web_admin.erl:1509 web/ejabberd_web_admin.erl:1761 -#: web/ejabberd_web_admin.erl:1932 web/ejabberd_web_admin.erl:2136 -#: web/ejabberd_web_admin.erl:2161 -msgid "Bad format" -msgstr "Nesprávný formát" - -#: mod_roster.erl:995 mod_roster_odbc.erl:1112 -msgid "Add Jabber ID" -msgstr "Přidat Jabber ID" - -#: mod_roster.erl:1094 mod_roster_odbc.erl:1211 -msgid "Roster" -msgstr "Seznam kontaktů" - -#: mod_shared_roster.erl:857 mod_shared_roster.erl:899 -#: mod_shared_roster.erl:1007 mod_shared_roster_odbc.erl:886 -#: mod_shared_roster_odbc.erl:928 mod_shared_roster_odbc.erl:1036 -msgid "Shared Roster Groups" -msgstr "Skupiny pro sdílený seznam kontaktů" - -#: mod_shared_roster.erl:895 mod_shared_roster_odbc.erl:924 -#: web/ejabberd_web_admin.erl:1365 web/ejabberd_web_admin.erl:2461 -msgid "Add New" -msgstr "Přidat nový" - -#: mod_shared_roster.erl:978 mod_shared_roster_odbc.erl:1007 -msgid "Name:" -msgstr "Jméno:" - -#: mod_shared_roster.erl:983 mod_shared_roster_odbc.erl:1012 -msgid "Description:" -msgstr "Popis:" - -#: mod_shared_roster.erl:991 mod_shared_roster_odbc.erl:1020 -msgid "Members:" -msgstr "Členové:" - -#: mod_shared_roster.erl:999 mod_shared_roster_odbc.erl:1028 -msgid "Displayed Groups:" -msgstr "Zobrazené skupiny:" - -#: mod_shared_roster.erl:1008 mod_shared_roster_odbc.erl:1037 -msgid "Group " -msgstr "Skupina " - -#: mod_shared_roster.erl:1017 mod_shared_roster_odbc.erl:1046 -#: web/ejabberd_web_admin.erl:840 web/ejabberd_web_admin.erl:884 -#: web/ejabberd_web_admin.erl:949 web/ejabberd_web_admin.erl:1026 -#: web/ejabberd_web_admin.erl:2017 -msgid "Submit" -msgstr "Odeslat" - -#: mod_vcard.erl:165 mod_vcard_ldap.erl:238 mod_vcard_odbc.erl:129 -msgid "Erlang Jabber Server" -msgstr "Erlang Jabber Server" - -#: mod_vcard.erl:364 mod_vcard.erl:478 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:454 -msgid "Birthday" -msgstr "Datum narození" - -#: mod_vcard.erl:364 mod_vcard.erl:480 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:456 -msgid "City" -msgstr "Město" - -#: mod_vcard.erl:364 mod_vcard.erl:479 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:455 -msgid "Country" -msgstr "Země" - -#: mod_vcard.erl:364 mod_vcard.erl:481 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:457 -msgid "Email" -msgstr "E-mail" - -#: mod_vcard.erl:364 mod_vcard.erl:476 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:452 -msgid "Family Name" -msgstr "Příjmení" - -#: mod_vcard.erl:364 mod_vcard_odbc.erl:342 -msgid "" -"Fill in the form to search for any matching Jabber User (Add * to the end of " -"field to match substring)" -msgstr "" -"Pro vyhledání uživatele Jabberu vyplňte formulář (na konec přidejte znak * " -"pro vyhledání podřetězce)" - -#: mod_vcard.erl:364 mod_vcard.erl:473 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:449 -msgid "Full Name" -msgstr "Celé jméno" - -#: mod_vcard.erl:364 mod_vcard.erl:475 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:451 -msgid "Middle Name" -msgstr "Druhé jméno" - -#: mod_vcard.erl:364 mod_vcard.erl:474 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:450 web/ejabberd_web_admin.erl:2006 -msgid "Name" -msgstr "Jméno" - -#: mod_vcard.erl:364 mod_vcard.erl:482 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:458 -msgid "Organization Name" -msgstr "Název firmy" - -#: mod_vcard.erl:364 mod_vcard.erl:483 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:459 -msgid "Organization Unit" -msgstr "Oddělení" - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "Search users in " -msgstr "Hledat uživatele v " - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "You need an x:data capable client to search" -msgstr "K vyhledávání potřebujete klienta podporujícího x:data" - -#: mod_vcard.erl:389 mod_vcard_ldap.erl:487 mod_vcard_odbc.erl:367 -msgid "vCard User Search" -msgstr "Hledání uživatelů podle vizitek" - -#: mod_vcard.erl:445 mod_vcard_ldap.erl:541 mod_vcard_odbc.erl:421 -msgid "ejabberd vCard module" -msgstr "ejabberd vCard modul" - -#: mod_vcard.erl:469 mod_vcard_ldap.erl:551 mod_vcard_odbc.erl:445 -msgid "Search Results for " -msgstr "Výsledky hledání pro " - -#: mod_vcard_ldap.erl:462 -msgid "Fill in fields to search for any matching Jabber User" -msgstr "Vyplňte políčka pro vyhledání uživatele Jabberu" - -#: web/ejabberd_web_admin.erl:193 web/ejabberd_web_admin.erl:203 -#: web/ejabberd_web_admin.erl:219 web/ejabberd_web_admin.erl:229 -msgid "Unauthorized" -msgstr "Nemáte oprávnění" - -#: web/ejabberd_web_admin.erl:286 web/ejabberd_web_admin.erl:303 -msgid "ejabberd Web Admin" -msgstr "Webová administrace ejabberd" - -#: web/ejabberd_web_admin.erl:769 web/ejabberd_web_admin.erl:780 -msgid "Administration" -msgstr "Administrace" - -#: web/ejabberd_web_admin.erl:878 web/ejabberd_web_admin.erl:982 -msgid "Raw" -msgstr "Zdroj" - -#: web/ejabberd_web_admin.erl:1017 -msgid "~s access rule configuration" -msgstr "~s konfigurace pravidla přístupu" - -#: web/ejabberd_web_admin.erl:1035 -msgid "Virtual Hosts" -msgstr "Virtuální hostitelé" - -#: web/ejabberd_web_admin.erl:1043 web/ejabberd_web_admin.erl:1050 -msgid "Users" -msgstr "Uživatelé" - -#: web/ejabberd_web_admin.erl:1078 -msgid "Users Last Activity" -msgstr "Poslední aktivita uživatele" - -#: web/ejabberd_web_admin.erl:1080 -msgid "Period: " -msgstr "Čas: " - -#: web/ejabberd_web_admin.erl:1090 -msgid "Last month" -msgstr "Poslední měsíc" - -#: web/ejabberd_web_admin.erl:1091 -msgid "Last year" -msgstr "Poslední rok" - -#: web/ejabberd_web_admin.erl:1092 -msgid "All activity" -msgstr "Všechny aktivity" - -#: web/ejabberd_web_admin.erl:1094 -msgid "Show Ordinary Table" -msgstr "Zobrazit běžnou tabulku" - -#: web/ejabberd_web_admin.erl:1096 -msgid "Show Integral Table" -msgstr "Zobrazit kompletní tabulku" - -#: web/ejabberd_web_admin.erl:1105 web/ejabberd_web_admin.erl:1939 -msgid "Statistics" -msgstr "Statistiky" - -#: web/ejabberd_web_admin.erl:1117 -msgid "Not Found" -msgstr "Nenalezeno" - -#: web/ejabberd_web_admin.erl:1134 -msgid "Node not found" -msgstr "Uzel nenalezen" - -#: web/ejabberd_web_admin.erl:1460 -msgid "Host" -msgstr "Hostitel" - -#: web/ejabberd_web_admin.erl:1461 -msgid "Registered Users" -msgstr "Registrovaní uživatelé" - -#: web/ejabberd_web_admin.erl:1573 -msgid "Offline Messages" -msgstr "Offline zprávy" - -#: web/ejabberd_web_admin.erl:1574 web/ejabberd_web_admin.erl:1767 -msgid "Last Activity" -msgstr "Poslední aktivita" - -#: web/ejabberd_web_admin.erl:1659 web/ejabberd_web_admin.erl:1675 -msgid "Registered Users:" -msgstr "Registrovaní živatelé:" - -#: web/ejabberd_web_admin.erl:1661 web/ejabberd_web_admin.erl:1677 -#: web/ejabberd_web_admin.erl:2192 -msgid "Online Users:" -msgstr "Online uživatelé:" - -#: web/ejabberd_web_admin.erl:1663 -msgid "Outgoing s2s Connections:" -msgstr "Odchozí s2s spojení:" - -#: web/ejabberd_web_admin.erl:1665 -msgid "Outgoing s2s Servers:" -msgstr "Odchozí s2s servery:" - -#: web/ejabberd_web_admin.erl:1734 web/mod_register_web.erl:175 -#: web/mod_register_web.erl:372 web/mod_register_web.erl:381 -#: web/mod_register_web.erl:411 -msgid "Change Password" -msgstr "Změnit heslo" - -#: web/ejabberd_web_admin.erl:1758 -msgid "User " -msgstr "Uživatel " - -#: web/ejabberd_web_admin.erl:1765 -msgid "Connected Resources:" -msgstr "Připojené zdroje:" - -#: web/ejabberd_web_admin.erl:1766 web/mod_register_web.erl:227 -#: web/mod_register_web.erl:519 -msgid "Password:" -msgstr "Heslo:" - -#: web/ejabberd_web_admin.erl:1828 -msgid "No Data" -msgstr "Žádná data" - -#: web/ejabberd_web_admin.erl:1906 -msgid "Nodes" -msgstr "Uzly" - -#: web/ejabberd_web_admin.erl:1929 web/ejabberd_web_admin.erl:1951 -msgid "Node " -msgstr "Uzel " - -#: web/ejabberd_web_admin.erl:1938 -msgid "Listened Ports" -msgstr "Otevřené porty" - -#: web/ejabberd_web_admin.erl:1940 web/ejabberd_web_admin.erl:2260 -#: web/ejabberd_web_admin.erl:2448 -msgid "Update" -msgstr "Aktualizovat" - -#: web/ejabberd_web_admin.erl:1943 web/ejabberd_web_admin.erl:2569 -msgid "Restart" -msgstr "Restart" - -#: web/ejabberd_web_admin.erl:1945 web/ejabberd_web_admin.erl:2571 -msgid "Stop" -msgstr "Stop" - -#: web/ejabberd_web_admin.erl:1959 -msgid "RPC Call Error" -msgstr "Chyba RPC volání" - -#: web/ejabberd_web_admin.erl:2000 -msgid "Database Tables at " -msgstr "Databázové tabulky na " - -#: web/ejabberd_web_admin.erl:2007 -msgid "Storage Type" -msgstr "Typ úložiště" - -#: web/ejabberd_web_admin.erl:2008 -msgid "Elements" -msgstr "Položek" - -#: web/ejabberd_web_admin.erl:2009 -msgid "Memory" -msgstr "Paměť" - -#: web/ejabberd_web_admin.erl:2032 web/ejabberd_web_admin.erl:2137 -msgid "Error" -msgstr "Chyba" - -#: web/ejabberd_web_admin.erl:2034 -msgid "Backup of " -msgstr "Záloha na " - -#: web/ejabberd_web_admin.erl:2036 -msgid "" -"Please note that these options will only backup the builtin Mnesia database. " -"If you are using the ODBC module, you also need to backup your SQL database " -"separately." -msgstr "" -"Podotýkáme, že tato nastavení budou zálohována do zabudované databáze " -"Mnesia. Pokud používáte ODBC modul, musíte zálohovat svoji SQL databázi " -"samostatně." - -#: web/ejabberd_web_admin.erl:2041 -msgid "Store binary backup:" -msgstr "Uložit binární zálohu:" - -#: web/ejabberd_web_admin.erl:2045 web/ejabberd_web_admin.erl:2052 -#: web/ejabberd_web_admin.erl:2060 web/ejabberd_web_admin.erl:2067 -#: web/ejabberd_web_admin.erl:2074 web/ejabberd_web_admin.erl:2081 -#: web/ejabberd_web_admin.erl:2088 web/ejabberd_web_admin.erl:2096 -#: web/ejabberd_web_admin.erl:2103 web/ejabberd_web_admin.erl:2110 -msgid "OK" -msgstr "OK" - -#: web/ejabberd_web_admin.erl:2048 -msgid "Restore binary backup immediately:" -msgstr "Okamžitě obnovit binární zálohu:" - -#: web/ejabberd_web_admin.erl:2056 -msgid "" -"Restore binary backup after next ejabberd restart (requires less memory):" -msgstr "" -"Obnovit binární zálohu při následujícím restartu ejabberd (vyžaduje méně " -"paměti)" - -#: web/ejabberd_web_admin.erl:2063 -msgid "Store plain text backup:" -msgstr "Uložit zálohu do textového souboru:" - -#: web/ejabberd_web_admin.erl:2070 -msgid "Restore plain text backup immediately:" -msgstr "Okamžitě obnovit zálohu z textového souboru:" - -#: web/ejabberd_web_admin.erl:2077 -msgid "Import users data from a PIEFXIS file (XEP-0227):" -msgstr "Importovat uživatele ze souboru ve formátu PIEFXIS (XEP-0227):" - -#: web/ejabberd_web_admin.erl:2084 -msgid "Export data of all users in the server to PIEFXIS files (XEP-0227):" -msgstr "Exportovat všechny uživatele do souboru ve formátu PIEFXIS (XEP-0227):" - -#: web/ejabberd_web_admin.erl:2091 -msgid "Export data of users in a host to PIEFXIS files (XEP-0227):" -msgstr "" -"Exportovat uživatele na hostiteli do souboru ve formátu PIEFXIS (XEP-0227):" - -#: web/ejabberd_web_admin.erl:2099 -msgid "Import user data from jabberd14 spool file:" -msgstr "Importovat uživatele z jabberd14 spool souborů:" - -#: web/ejabberd_web_admin.erl:2106 -msgid "Import users data from jabberd14 spool directory:" -msgstr "Importovat uživatele z jabberd14 spool souborů:" - -#: web/ejabberd_web_admin.erl:2132 -msgid "Listened Ports at " -msgstr "Otevřené porty na " - -#: web/ejabberd_web_admin.erl:2157 -msgid "Modules at " -msgstr "Moduly na " - -#: web/ejabberd_web_admin.erl:2183 -msgid "Statistics of ~p" -msgstr "Statistiky ~p" - -#: web/ejabberd_web_admin.erl:2186 -msgid "Uptime:" -msgstr "Čas běhu:" - -#: web/ejabberd_web_admin.erl:2189 -msgid "CPU Time:" -msgstr "Čas procesoru" - -#: web/ejabberd_web_admin.erl:2195 -msgid "Transactions Committed:" -msgstr "Transakce potvrzena" - -#: web/ejabberd_web_admin.erl:2198 -msgid "Transactions Aborted:" -msgstr "Transakce zrušena" - -#: web/ejabberd_web_admin.erl:2201 -msgid "Transactions Restarted:" -msgstr "Transakce restartována" - -#: web/ejabberd_web_admin.erl:2204 -msgid "Transactions Logged:" -msgstr "Transakce zaznamenána" - -#: web/ejabberd_web_admin.erl:2246 -msgid "Update " -msgstr "Aktualizovat " - -#: web/ejabberd_web_admin.erl:2254 -msgid "Update plan" -msgstr "Aktualizovat plán" - -#: web/ejabberd_web_admin.erl:2255 -msgid "Modified modules" -msgstr "Aktualizované moduly" - -#: web/ejabberd_web_admin.erl:2256 -msgid "Update script" -msgstr "Aktualizované skripty" - -#: web/ejabberd_web_admin.erl:2257 -msgid "Low level update script" -msgstr "Nízkoúrovňový aktualizační skript" - -#: web/ejabberd_web_admin.erl:2258 -msgid "Script check" -msgstr "Kontrola skriptu" - -#: web/ejabberd_web_admin.erl:2426 -msgid "Port" -msgstr "Port" - -#: web/ejabberd_web_admin.erl:2427 -msgid "IP" -msgstr "IP" - -#: web/ejabberd_web_admin.erl:2428 -msgid "Protocol" -msgstr "Protokol" - -#: web/ejabberd_web_admin.erl:2429 web/ejabberd_web_admin.erl:2556 -msgid "Module" -msgstr "Modul" - -#: web/ejabberd_web_admin.erl:2430 web/ejabberd_web_admin.erl:2557 -msgid "Options" -msgstr "Nastavení" - -#: web/ejabberd_web_admin.erl:2450 -msgid "Delete" -msgstr "Smazat" - -#: web/ejabberd_web_admin.erl:2579 -msgid "Start" -msgstr "Start" - -#: web/mod_register_web.erl:104 -msgid "Your Jabber account was successfully created." -msgstr "Váš účet Jabberu byl úspěšně vytvořen." - -#: web/mod_register_web.erl:107 -msgid "There was an error creating the account: " -msgstr "Při vytváření účtu došlo k chybě." - -#: web/mod_register_web.erl:115 -msgid "Your Jabber account was successfully deleted." -msgstr "Váš účet Jabberu byl úspěšně smazán." - -#: web/mod_register_web.erl:118 -msgid "There was an error deleting the account: " -msgstr "Při mazání účtu došlo k chybě: " - -#: web/mod_register_web.erl:128 -msgid "The password of your Jabber account was successfully changed." -msgstr "Heslo vašeho účtu Jabberu bylo úspěšně změněno." - -#: web/mod_register_web.erl:131 -msgid "There was an error changing the password: " -msgstr "Při změně hesla došlo k chybě: " - -#: web/mod_register_web.erl:163 web/mod_register_web.erl:172 -msgid "Jabber Account Registration" -msgstr "Registrace účtu Jabberu" - -#: web/mod_register_web.erl:174 web/mod_register_web.erl:192 -#: web/mod_register_web.erl:201 -msgid "Register a Jabber account" -msgstr "Zaregistrujte si účet Jabberu" - -#: web/mod_register_web.erl:176 web/mod_register_web.erl:493 -#: web/mod_register_web.erl:502 -msgid "Unregister a Jabber account" -msgstr "Zrušte registraci účtu Jabberu" - -#: web/mod_register_web.erl:203 -msgid "" -"This page allows to create a Jabber account in this Jabber server. Your JID " -"(Jabber IDentifier) will be of the form: username@server. Please read " -"carefully the instructions to fill correctly the fields." -msgstr "" -"Na této stránce si můžete vytvořit účet na tomto serveru Jabberu. Vaše JID " -"(Jabber IDentifikátor) bude mít tvar: uživatelskéjméno@server. Přečtěte si " -"prosím pozorně instrukce pro vyplnění údajů." - -#: web/mod_register_web.erl:212 web/mod_register_web.erl:386 -#: web/mod_register_web.erl:509 -msgid "Username:" -msgstr "Uživatelské jméno:" - -#: web/mod_register_web.erl:217 -msgid "This is case insensitive: macbeth is the same that MacBeth and Macbeth." -msgstr "" -"Zde nezáleží na velikosti písmen: macbeth je stejný jako MacBeth a Macbeth." - -#: web/mod_register_web.erl:218 -msgid "Characters not allowed:" -msgstr "Nepřípustné znaky:" - -#: web/mod_register_web.erl:222 web/mod_register_web.erl:391 -#: web/mod_register_web.erl:514 -msgid "Server:" -msgstr "Server:" - -#: web/mod_register_web.erl:232 -msgid "" -"Don't tell your password to anybody, not even the administrators of the " -"Jabber server." -msgstr "" -"Nikdy nikomu nesdělujte své heslo, ani administrátorovi serveru Jabberu." - -#: web/mod_register_web.erl:234 -msgid "You can later change your password using a Jabber client." -msgstr "Později můžete své heslo změnit pomocí klienta Jabberu." - -#: web/mod_register_web.erl:235 -msgid "" -"Some Jabber clients can store your password in your computer. Use that " -"feature only if you trust your computer is safe." -msgstr "" -"Někteří klienti umí uložit vaše heslo na disk počítače. Tuto funkci " -"používejte, pouze pokud věříte zabezpečení svého počítače." - -#: web/mod_register_web.erl:237 -msgid "" -"Memorize your password, or write it in a paper placed in a safe place. In " -"Jabber there isn't an automated way to recover your password if you forget " -"it." -msgstr "" -"Svoje heslo si zapamatujte, nebo si jej poznamenejte na papírek a ten " -"uschovejte v bezpečí. Jabber nemá žádný automatizovaný způsob obnovy hesla." - -#: web/mod_register_web.erl:242 web/mod_register_web.erl:406 -msgid "Password Verification:" -msgstr "Ověření hesla:" - -#: web/mod_register_web.erl:250 -msgid "Register" -msgstr "Zaregistrovat se" - -#: web/mod_register_web.erl:396 -msgid "Old Password:" -msgstr "Současné heslo:" - -#: web/mod_register_web.erl:401 -msgid "New Password:" -msgstr "Nové heslo:" - -#: web/mod_register_web.erl:504 -msgid "This page allows to unregister a Jabber account in this Jabber server." -msgstr "Zde můžete zrušit registraci účtu na tomto serveru Jabberu." - -#: web/mod_register_web.erl:524 -msgid "Unregister" -msgstr "Zrušit registraci" - -#, fuzzy -#~ msgid "Captcha test failed" -#~ msgstr "Zkouška CAPTCHA neprošla." diff --git a/priv/msgs/de.msg b/priv/msgs/de.msg index 883fdf884..7247d5f55 100644 --- a/priv/msgs/de.msg +++ b/priv/msgs/de.msg @@ -1,421 +1,625 @@ -{"Access Configuration","Zugangskonfiguration"}. -{"Access Control List Configuration","Konfiguration der Zugangskontrolllisten"}. -{"Access control lists","Zugangskontroll-Listen (ACL)"}. -{"Access Control Lists","Zugangskontroll-Listen (ACL)"}. -{"Access denied by service policy","Zugang aufgrund der Dienstrichtlinien verweigert"}. -{"Access rules","Zugangsregeln"}. -{"Access Rules","Zugangsregeln"}. +%% Generated automatically +%% DO NOT EDIT: run `make translations` instead +%% To improve translations please read: +%% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ + +{" (Add * to the end of field to match substring)"," (Fügen Sie * am Ende des Feldes hinzu um nach Teilzeichenketten zu suchen)"}. +{" has set the subject to: "," hat das Thema geändert auf: "}. +{"# participants","# Teilnehmer"}. +{"A description of the node","Eine Beschreibung des Knotens"}. +{"A friendly name for the node","Ein benutzerfreundlicher Name für den Knoten"}. +{"A password is required to enter this room","Ein Passwort ist erforderlich um diesen Raum zu betreten"}. +{"A Web Page","Eine Webseite"}. +{"Accept","Akzeptieren"}. +{"Access denied by service policy","Zugriff aufgrund der Dienstrichtlinien verweigert"}. +{"Access model","Zugriffsmodell"}. +{"Account doesn't exist","Konto existiert nicht"}. {"Action on user","Aktion auf Benutzer"}. -{"Add Jabber ID","Jabber-ID hinzufügen"}. -{"Add New","Neue hinzufügen"}. +{"Add a hat to a user","Funktion zu einem Benutzer hinzufügen"}. {"Add User","Benutzer hinzufügen"}. {"Administration of ","Administration von "}. {"Administration","Verwaltung"}. -{"Administrator privileges required","Administratorenrechte benötigt"}. -{"A friendly name for the node","Ein merkbarer Name für den Knoten"}. +{"Administrator privileges required","Administratorrechte erforderlich"}. {"All activity","Alle Aktivitäten"}. +{"All Users","Alle Benutzer"}. +{"Allow subscription","Abonnement erlauben"}. {"Allow this Jabber ID to subscribe to this pubsub node?","Dieser Jabber-ID das Abonnement dieses pubsub-Knotens erlauben?"}. +{"Allow this person to register with the room?","Dieser Person erlauben, sich beim Raum anzumelden?"}. {"Allow users to change the subject","Erlaube Benutzern das Thema zu ändern"}. {"Allow users to query other users","Erlaube Benutzern Informationen über andere Benutzer abzufragen"}. {"Allow users to send invites","Erlaube Benutzern Einladungen zu senden"}. {"Allow users to send private messages","Erlaube Benutzern private Nachrichten zu senden"}. -{"Allow visitors to change nickname","Erlaube Besuchern ihren Spitznamen zu ändern"}. +{"Allow visitors to change nickname","Erlaube Besuchern ihren Benutzernamen zu ändern"}. {"Allow visitors to send private messages to","Erlaube Besuchern das Senden von privaten Nachrichten an"}. -{"Allow visitors to send status text in presence updates","Erlaube Besuchern einen Text bei Statusänderung zu senden"}. -{"Allow visitors to send voice requests","Anfragen von Sprachrechten für Benutzer erlauben"}. -{"All Users","Alle Benutzer"}. +{"Allow visitors to send status text in presence updates","Erlaube Besuchern einen Statustext bei Präsenzupdates zu senden"}. +{"Allow visitors to send voice requests","Erlaube Besuchern Sprachrecht-Anforderungen zu senden"}. +{"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.","Eine zugehörige LDAP-Gruppe die Raummitgliedschaft definiert; dies sollte ein 'LDAP Distinguished Name' gemäß einer implementierungs- oder bereitstellungsspezifischen Definition einer Gruppe sein."}. {"Announcements","Ankündigungen"}. -{"anyone","jeden"}. -{"A password is required to enter this room","Sie brauchen ein Passwort um diesen Raum zu betreten"}. +{"Answer associated with a picture","Antwort verbunden mit einem Bild"}. +{"Answer associated with a video","Antwort verbunden mit einem Video"}. +{"Answer associated with speech","Antwort verbunden mit Sprache"}. +{"Answer to a question","Antwort auf eine Frage"}. +{"Anyone in the specified roster group(s) may subscribe and retrieve items","Jeder in der/den angeführten Kontaktlistengruppe(n) darf Items abonnieren und abrufen"}. +{"Anyone may associate leaf nodes with the collection","Jeder darf Blattknoten mit der Sammlung verknüpfen"}. +{"Anyone may publish","Jeder darf veröffentlichen"}. +{"Anyone may subscribe and retrieve items","Jeder darf Items abonnieren und abrufen"}. +{"Anyone with a presence subscription of both or from may subscribe and retrieve items","Jeder mit einem Präsenzabonnement von beiden oder davon darf Items abonnieren oder abrufen"}. +{"Anyone with Voice","Jeder mit Stimme"}. +{"Anyone","Jeder"}. +{"API Commands","API Befehle"}. {"April","April"}. +{"Arguments","Argumente"}. +{"Attribute 'channel' is required for this request","Attribut 'channel' ist für diese Anforderung erforderlich"}. +{"Attribute 'id' is mandatory for MIX messages","Attribut 'id' ist verpflichtend für MIX-Nachrichten"}. +{"Attribute 'jid' is not allowed here","Attribut 'jid' ist hier nicht erlaubt"}. +{"Attribute 'node' is not allowed here","Attribut 'node' ist hier nicht erlaubt"}. +{"Attribute 'to' of stanza that triggered challenge","Attribut 'to' des Stanza das die Herausforderung ausgelöst hat"}. {"August","August"}. -{"Backup","Datensicherung"}. -{"Backup Management","Datensicherungsverwaltung"}. -{"Backup of ","Sicherung von "}. -{"Backup to File at ","Datensicherung in die Datei "}. +{"Automatic node creation is not enabled","Automatische Knotenerstellung ist nicht aktiviert"}. +{"Backup Management","Backupverwaltung"}. +{"Backup of ~p","Backup von ~p"}. +{"Backup to File at ","Backup in Datei bei "}. +{"Backup","Backup"}. {"Bad format","Ungültiges Format"}. {"Birthday","Geburtsdatum"}. -{"CAPTCHA web page","CAPTCHA Webseite"}. +{"Both the username and the resource are required","Sowohl der Benutzername als auch die Ressource sind erforderlich"}. +{"Bytestream already activated","Bytestream bereits aktiviert"}. +{"Cannot remove active list","Kann aktive Liste nicht entfernen"}. +{"Cannot remove default list","Kann Standardliste nicht entfernen"}. +{"CAPTCHA web page","CAPTCHA -Webseite"}. +{"Challenge ID","Herausforderungs-ID"}. {"Change Password","Passwort ändern"}. -{"Change User Password","Benutzer-Passwort ändern"}. +{"Change User Password","Benutzerpasswort ändern"}. +{"Changing password is not allowed","Ändern des Passwortes ist nicht erlaubt"}. +{"Changing role/affiliation is not allowed","Ändern der Rolle/Zugehörigkeit ist nicht erlaubt"}. +{"Channel already exists","Kanal existiert bereits"}. +{"Channel does not exist","Kanal existiert nicht"}. +{"Channel JID","Kanal-JID"}. +{"Channels","Kanäle"}. {"Characters not allowed:","Nicht erlaubte Zeichen:"}. {"Chatroom configuration modified","Chatraum-Konfiguration geändert"}. -{"Chatroom is created","Chatraum wurde erstellt"}. -{"Chatroom is destroyed","Chatraum wurde entfernt"}. -{"Chatroom is started","Chatraum wurde gestartet"}. -{"Chatroom is stopped","Chatraum wurde beendet"}. +{"Chatroom is created","Chatraum ist erstellt"}. +{"Chatroom is destroyed","Chatraum ist entfernt"}. +{"Chatroom is started","Chatraum ist gestartet"}. +{"Chatroom is stopped","Chatraum ist beendet"}. {"Chatrooms","Chaträume"}. -{"Choose a username and password to register with this server","Wählen sie zum Registrieren einen Benutzernamen und ein Passwort"}. -{"Choose modules to stop","Wähle zu stoppende Module"}. +{"Choose a username and password to register with this server","Wählen Sie zum Registrieren auf diesem Server einen Benutzernamen und ein Passwort"}. {"Choose storage type of tables","Wähle Speichertyp der Tabellen"}. -{"Choose whether to approve this entity's subscription.","Wähle Sie, ob dieses Abonnement akzeptiert werden soll."}. +{"Choose whether to approve this entity's subscription.","Wählen Sie, ob das Abonnement dieser Entität genehmigt werden soll."}. {"City","Stadt"}. +{"Client acknowledged more stanzas than sent by server","Client bestätigte mehr Stanzas als vom Server gesendet"}. {"Commands","Befehle"}. {"Conference room does not exist","Konferenzraum existiert nicht"}. +{"Configuration of room ~s","Konfiguration des Raumes ~s"}. {"Configuration","Konfiguration"}. -{"Configuration of room ~s","Konfiguration für Raum ~s"}. -{"Connected Resources:","Verbundene Ressourcen:"}. -{"Connections parameters","Verbindungsparameter"}. +{"Contact Addresses (normally, room owner or owners)","Kontaktadresse (normalerweise Raumbesitzer)"}. {"Country","Land"}. -{"CPU Time:","CPU-Zeit:"}. +{"Current Discussion Topic","Aktuelles Diskussionsthema"}. +{"Database failure","Datenbankfehler"}. +{"Database Tables Configuration at ","Datenbanktabellen-Konfiguration bei "}. {"Database","Datenbank"}. -{"Database Tables at ","Datenbanktabellen auf "}. -{"Database Tables Configuration at ","Datenbanktabellen-Konfiguration auf "}. {"December","Dezember"}. -{"Default users as participants","Standardbenutzer als Teilnehmer"}. -{"Delete","Löschen"}. -{"Delete message of the day","Lösche Nachricht des Tages"}. +{"Default users as participants","Benutzer werden standardmäßig Teilnehmer"}. {"Delete message of the day on all hosts","Lösche Nachricht des Tages auf allen Hosts"}. -{"Delete Selected","Markierte löschen"}. +{"Delete message of the day","Lösche Nachricht des Tages"}. {"Delete User","Benutzer löschen"}. -{"Deliver event notifications","Ereignisbenachrichtigung zustellen"}. -{"Deliver payloads with event notifications","Nachrichten mit Ereignis-Benachrichtigungen zustellen"}. -{"Description:","Beschreibung:"}. +{"Deliver event notifications","Ereignisbenachrichtigungen zustellen"}. +{"Deliver payloads with event notifications","Nutzdaten mit Ereignisbenachrichtigungen zustellen"}. {"Disc only copy","Nur auf Festplatte"}. -{"Displayed Groups:","Angezeigte Gruppen:"}. -{"Don't tell your password to anybody, not even the administrators of the Jabber server.","Geben sie niemandem ihr Passwort, auch nicht den Administratoren des Jabber Servers."}. -{"Dump Backup to Text File at ","Ausgabe der Sicherung in diese Textdatei "}. +{"Don't tell your password to anybody, not even the administrators of the XMPP server.","Geben Sie niemandem Ihr Passwort, auch nicht den Administratoren des XMPP-Servers."}. +{"Dump Backup to Text File at ","Gib Backup in Textdatei aus bei "}. {"Dump to Text File","Ausgabe in Textdatei"}. -{"Edit Properties","Einstellungen ändern"}. -{"Either approve or decline the voice request.","Diese Anfrage für Sprachrechte bestätigen oder ablehnen."}. -{"ejabberd IRC module","ejabberd IRC-Modul"}. +{"Duplicated groups are not allowed by RFC6121","Doppelte Gruppen sind laut RFC6121 nicht erlaubt"}. +{"Dynamically specify a replyto of the item publisher","Dynamisch ein 'replyto' des Item-Veröffentlichers angeben"}. +{"Edit Properties","Eigenschaften ändern"}. +{"Either approve or decline the voice request.","Sprachrecht-Anforderung entweder genehmigen oder ablehnen."}. +{"ejabberd HTTP Upload service","ejabberd HTTP Upload-Dienst"}. {"ejabberd MUC module","ejabberd MUC-Modul"}. +{"ejabberd Multicast service","ejabberd Multicast-Dienst"}. {"ejabberd Publish-Subscribe module","ejabberd Publish-Subscribe-Modul"}. {"ejabberd SOCKS5 Bytestreams module","ejabberd SOCKS5-Bytestreams-Modul"}. {"ejabberd vCard module","ejabberd vCard-Modul"}. {"ejabberd Web Admin","ejabberd Web-Admin"}. -{"Elements","Elemente"}. +{"ejabberd","ejabberd"}. +{"Email Address","E-Mail-Adresse"}. {"Email","E-Mail"}. +{"Enable hats","Funktion einschalten"}. {"Enable logging","Protokollierung aktivieren"}. -{"Encoding for server ~b","Kodierung für Server ~b"}. -{"End User Session","Benutzer-Sitzung beenden"}. -{"Enter list of {Module, [Options]}","Geben sie eine Liste bestehend aus {Modul, [Optionen]} ein"}. -{"Enter nickname you want to register","Geben sie den zu registrierenden Benutzernamen ein"}. -{"Enter path to backup file","Geben sie den Pfad zur Datensicherung ein"}. -{"Enter path to jabberd14 spool dir","Geben Sie den Pfad zum jabberd14-Spool-Verzeichnis ein"}. -{"Enter path to jabberd14 spool file","Geben Sie den Pfad zur jabberd14-Spool-Datei ein"}. -{"Enter path to text file","Geben sie den Pfad zur Textdatei ein"}. -{"Enter the text you see","Geben sie den Text den sie sehen ein"}. -{"Enter username and encodings you wish to use for connecting to IRC servers. Press 'Next' to get more fields to fill in. Press 'Complete' to save settings.","Geben sie Benutzernamen und Kodierung für Verbindungen zu IRC Servern an. Drücken sie 'Mehr' um leere Felder hinzuzufügen. Drücken sie 'Beenden' um die Einstellungen zu speichern."}. -{"Enter username, encodings, ports and passwords you wish to use for connecting to IRC servers","Geben Sie Benutzernamen und Zeichenkodierung für die Verbindung zum IRC-Server an"}. -{"Erlang Jabber Server","Erlang Jabber Server"}. -{"Error","Fehler"}. -{"Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef.net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}].","Beispiel: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef.net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]."}. -{"Exclude Jabber IDs from CAPTCHA challenge","Von CAPTCHA Überprüfung ausgeschlossene Jabber IDs"}. -{"Export data of all users in the server to PIEFXIS files (XEP-0227):","Alle Benutzerdaten des Servers in PIEFXIS Dateien (XEP-0227) exportieren:"}. -{"Export data of users in a host to PIEFXIS files (XEP-0227):","Alle Benutzerdaten des Hosts in PIEFXIS Dateien (XEP-0227) exportieren:"}. -{"Failed to extract JID from your voice request approval","Fehler beim Auslesen der JID aus der Anfragenbestätigung für Sprachrechte"}. +{"Enable message archiving","Nachrichtenarchivierung aktivieren"}. +{"Enabling push without 'node' attribute is not supported","push ohne 'node'-Attribut zu aktivieren wird nicht unterstützt"}. +{"End User Session","Benutzersitzung beenden"}. +{"Enter nickname you want to register","Geben Sie den Spitznamen ein den Sie registrieren wollen"}. +{"Enter path to backup file","Geben Sie den Pfad zur Backupdatei ein"}. +{"Enter path to jabberd14 spool dir","Geben Sie den Pfad zum jabberd14-Spoolverzeichnis ein"}. +{"Enter path to jabberd14 spool file","Geben Sie den Pfad zur jabberd14-Spooldatei ein"}. +{"Enter path to text file","Geben Sie den Pfad zur Textdatei ein"}. +{"Enter the text you see","Geben Sie den Text ein den Sie sehen"}. +{"Erlang XMPP Server","Erlang XMPP-Server"}. +{"Exclude Jabber IDs from CAPTCHA challenge","Jabber-IDs von CAPTCHA-Herausforderung ausschließen"}. +{"Export all tables as SQL queries to a file:","Alle Tabellen als SQL-Abfragen in eine Datei exportieren:"}. +{"Export data of all users in the server to PIEFXIS files (XEP-0227):","Alle Benutzerdaten des Servers in PIEFXIS-Dateien (XEP-0227) exportieren:"}. +{"Export data of users in a host to PIEFXIS files (XEP-0227):","Alle Benutzerdaten des Hosts in PIEFXIS-Dateien (XEP-0227) exportieren:"}. +{"External component failure","Fehler externer Komponente"}. +{"External component timeout","Zeitüberschreitung externer Komponente"}. +{"Failed to activate bytestream","Konnte Bytestream nicht aktivieren"}. +{"Failed to extract JID from your voice request approval","Konnte JID nicht aus Ihrer Genehmigung der Sprachrecht-Anforderung extrahieren"}. +{"Failed to map delegated namespace to external component","Konnte delegierten Namensraum nicht externer Komponente zuordnen"}. +{"Failed to parse HTTP response","Konnte HTTP-Antwort nicht parsen"}. +{"Failed to process option '~s'","Konnte Option '~s' nicht verarbeiten"}. {"Family Name","Nachname"}. +{"FAQ Entry","FAQ-Eintrag"}. {"February","Februar"}. -{"Fill in fields to search for any matching Jabber User","Füllen Sie die Felder aus, um nach passenden Jabber-Benutzern zu suchen"}. -{"Fill in the form to search for any matching Jabber User (Add * to the end of field to match substring)","Füllen Sie die Felder aus, um nach passenden Jabber-Benutzern zu suchen (beenden Sie ein Feld mit *, um auch nach Teilzeichenketten zu suchen)"}. +{"File larger than ~w bytes","Datei größer als ~w Bytes"}. +{"Fill in the form to search for any matching XMPP User","Füllen Sie das Formular aus, um nach jeglichen passenden XMPP-Benutzern zu suchen"}. {"Friday","Freitag"}. -{"From ~s","Von ~s"}. -{"From","Von"}. +{"From ~ts","Von ~ts"}. +{"Full List of Room Admins","Vollständige Liste der Raumadmins"}. +{"Full List of Room Owners","Vollständige Liste der Raumbesitzer"}. {"Full Name","Vollständiger Name"}. +{"Get List of Online Users","Liste der angemeldeten Benutzer abrufen"}. +{"Get List of Registered Users","Liste der registrierten Benutzer abrufen"}. {"Get Number of Online Users","Anzahl der angemeldeten Benutzer abrufen"}. {"Get Number of Registered Users","Anzahl der registrierten Benutzer abrufen"}. -{"Get User Last Login Time","letzte Anmeldezeit abrufen"}. -{"Get User Password","Benutzer-Passwort abrufen"}. -{"Get User Statistics","Benutzer-Statistiken abrufen"}. -{"Grant voice to this person?","Sprachrechte dieser Person erteilen?"}. -{"Group ","Gruppe "}. -{"Groups","Gruppen"}. +{"Get Pending","Ausstehende abrufen"}. +{"Get User Last Login Time","letzte Anmeldezeit des Benutzers abrufen"}. +{"Get User Statistics","Benutzerstatistiken abrufen"}. +{"Given Name","Vorname"}. +{"Grant voice to this person?","Dieser Person Sprachrechte erteilen?"}. {"has been banned","wurde gebannt"}. -{"has been kicked because of an affiliation change","wurde wegen Änderung des Mitgliederstatus gekickt"}. -{"has been kicked because of a system shutdown","wurde wegen Systemabschaltung gekickt"}. -{"has been kicked because the room has been changed to members-only","wurde gekickt weil der Raum auf Nur-Mitglieder umgestellt wurde"}. -{"has been kicked","wurde gekickt"}. -{" has set the subject to: "," hat das Thema geändert auf: "}. -{"Host","Host"}. -{"If you don't see the CAPTCHA image here, visit the web page.","Wenn sie das CAPTCHA Bild nicht sehen, besuchen sie bitte die Webseite."}. -{"If you want to specify different ports, passwords, encodings for IRC servers, fill this list with values in format '{\"irc server\", \"encoding\", port, \"password\"}'. By default this service use \"~s\" encoding, port ~p, empty password.","Wenn sie verschiedene Ports, Passwörter und Kodierungen für IRC Server angeben wollen, erstellen sie die Liste mit folgendem Format '{\"IRC Server\", \"Kodierung\", Port, \"Passwort\"}'. Standardmäßig benutzt dieser Dienst die \"~s\" Kodierung, den Port ~p und kein Passwort."}. +{"has been kicked because of a system shutdown","wurde wegen einer Systemabschaltung hinausgeworfen"}. +{"has been kicked because of an affiliation change","wurde wegen einer Änderung der Zugehörigkeit hinausgeworfen"}. +{"has been kicked because the room has been changed to members-only","wurde hinausgeworfen weil der Raum zu Nur-Mitglieder geändert wurde"}. +{"has been kicked","wurde hinausgeworfen"}. +{"Hash of the vCard-temp avatar of this room","Hash des vCard-temp Avatars dieses Raums"}. +{"Hat title","Funktionstitel"}. +{"Hat URI","Funktions-URI"}. +{"Hats limit exceeded","Funktionslimit wurde überschritten"}. +{"Host unknown","Host unbekannt"}. +{"HTTP File Upload","HTTP-Dateiupload"}. +{"Idle connection","Inaktive Verbindung"}. +{"If you don't see the CAPTCHA image here, visit the web page.","Wenn Sie das CAPTCHA-Bild nicht sehen, besuchen Sie die Webseite."}. {"Import Directory","Verzeichnis importieren"}. {"Import File","Datei importieren"}. -{"Import user data from jabberd14 spool file:","Importiere Benutzer von jabberd14 Spool Datei:"}. -{"Import User from File at ","Benutzer aus dieser Datei importieren "}. -{"Import users data from a PIEFXIS file (XEP-0227):","Benutzerdaten von einer PIEFXIS Datei (XEP-0227) importieren:"}. -{"Import users data from jabberd14 spool directory:","Importiere Benutzer von jabberd14 Spool Verzeichnis:"}. -{"Import Users from Dir at ","Benutzer importieren aus dem Verzeichnis "}. -{"Import Users From jabberd14 Spool Files","Importiere Benutzer aus jabberd14-Spool-Dateien"}. +{"Import user data from jabberd14 spool file:","Importiere Benutzer von jabberd14-Spooldatei:"}. +{"Import User from File at ","Benutzer importieren aus Datei bei "}. +{"Import users data from a PIEFXIS file (XEP-0227):","Benutzerdaten von einer PIEFXIS-Datei (XEP-0227) importieren:"}. +{"Import users data from jabberd14 spool directory:","Importiere Benutzer von jabberd14-Spoolverzeichnis:"}. +{"Import Users from Dir at ","Benutzer importieren aus Verzeichnis bei "}. +{"Import Users From jabberd14 Spool Files","Importiere Benutzer aus jabberd14-Spooldateien"}. +{"Improper domain part of 'from' attribute","Falscher Domänenteil des 'from'-Attributs"}. {"Improper message type","Unzulässiger Nachrichtentyp"}. +{"Incorrect CAPTCHA submit","Falsche CAPTCHA-Eingabe"}. +{"Incorrect data form","Falsches Datenformular"}. {"Incorrect password","Falsches Passwort"}. -{"Invalid affiliation: ~s","Ungültige Mitgliedschaft: ~s"}. -{"Invalid role: ~s","Ungültige Rolle: ~s"}. -{"IP addresses","IP Adressen"}. -{"IP","IP"}. -{"IRC channel (don't put the first #)","IRC Channel (ohne dem ersten #)"}. -{"IRC server","IRC Server"}. -{"IRC settings","IRC Einstellungen"}. -{"IRC Transport","IRC Transport"}. -{"IRC username","IRC Benutzername"}. -{"IRC Username","IRC-Benutzername"}. +{"Incorrect value of 'action' attribute","Falscher Wert des 'action'-Attributs"}. +{"Incorrect value of 'action' in data form","Falscher Wert von 'action' in Datenformular"}. +{"Incorrect value of 'path' in data form","Falscher Wert von 'path' in Datenformular"}. +{"Installed Modules:","Installierte Module:"}. +{"Install","Installieren"}. +{"Insufficient privilege","Unzureichende Privilegien"}. +{"Internal server error","Interner Serverfehler"}. +{"Invalid 'from' attribute in forwarded message","Ungültiges 'from'-Attribut in weitergeleiteter Nachricht"}. +{"Invalid node name","Ungültiger Knotenname"}. +{"Invalid 'previd' value","Ungültiger 'previd'-Wert"}. +{"Invitations are not allowed in this conference","Einladungen sind in dieser Konferenz nicht erlaubt"}. +{"IP addresses","IP-Adressen"}. {"is now known as","ist nun bekannt als"}. -{"It is not allowed to send private messages","Es ist nicht erlaubt private Nachrichten zu senden"}. -{"It is not allowed to send private messages of type \"groupchat\"","Es ist nicht erlaubt private Nachrichten des Typs \"Gruppenchat\" zu senden"}. -{"It is not allowed to send private messages to the conference","Es ist nicht erlaubt private Nachrichten an den Raum zu schicken"}. -{"Jabber Account Registration","Jabber Konto Anmeldung"}. -{"Jabber ID","Jabber ID"}. -{"Jabber ID ~s is invalid","Die Jabber-ID ~s ist ungültig"}. +{"It is not allowed to send error messages to the room. The participant (~s) has sent an error message (~s) and got kicked from the room","Es ist nicht erlaubt Fehlermeldungen an den Raum zu senden. Der Teilnehmer (~s) hat eine Fehlermeldung (~s) gesendet und wurde aus dem Raum geworfen"}. +{"It is not allowed to send private messages of type \"groupchat\"","Es ist nicht erlaubt private Nachrichten des Typs \"groupchat\" zu senden"}. +{"It is not allowed to send private messages to the conference","Es ist nicht erlaubt private Nachrichten an die Konferenz zu senden"}. +{"Jabber ID","Jabber-ID"}. {"January","Januar"}. -{"Join IRC channel","IRC Channel beitreten"}. -{"joins the room","betretet den Raum"}. -{"Join the IRC channel here.","Hier den IRC Channel beitreten."}. -{"Join the IRC channel in this Jabber ID: ~s","Den IRC Channel mit dieser Jabber ID beitreten: ~s"}. +{"JID normalization denied by service policy","JID-Normalisierung aufgrund der Dienstrichtlinien verweigert"}. +{"JID normalization failed","JID-Normalisierung fehlgeschlagen"}. +{"Joined MIX channels of ~ts","Beigetretene MIX-Channels von ~ts"}. +{"Joined MIX channels:","Beigetretene MIX-Channels:"}. +{"joins the room","betritt den Raum"}. {"July","Juli"}. {"June","Juni"}. +{"Just created","Gerade erstellt"}. {"Last Activity","Letzte Aktivität"}. {"Last login","Letzte Anmeldung"}. +{"Last message","Letzte Nachricht"}. {"Last month","Letzter Monat"}. {"Last year","Letztes Jahr"}. +{"Least significant bits of SHA-256 hash of text should equal hexadecimal label","Niederwertigstes Bit des SHA-256-Hashes des Textes sollte hexadezimalem Label gleichen"}. {"leaves the room","verlässt den Raum"}. -{"Listened Ports","Aktive Ports"}. -{"Listened Ports at ","Aktive Ports bei"}. -{"List of modules to start","Liste der zu startenden Module"}. -{"Low level update script","Low level Aktualisierungsscript"}. +{"List of users with hats","Liste der Benutzer mit Funktionen"}. +{"List users with hats","Benutzer mit Funktionen auflisten"}. +{"Logged Out","Abgemeldet"}. +{"Logging","Protokollierung"}. {"Make participants list public","Teilnehmerliste öffentlich machen"}. -{"Make room CAPTCHA protected","Raum mit Verifizierung (Captcha) versehen"}. +{"Make room CAPTCHA protected","Raum mittels CAPTCHA schützen"}. {"Make room members-only","Raum nur für Mitglieder zugänglich machen"}. {"Make room moderated","Raum moderiert machen"}. {"Make room password protected","Raum mit Passwort schützen"}. {"Make room persistent","Raum persistent machen"}. {"Make room public searchable","Raum öffentlich suchbar machen"}. +{"Malformed username","Ungültiger Benutzername"}. +{"MAM preference modification denied by service policy","Modifikation der MAM-Präferenzen aufgrund der Dienstrichtlinien verweigert"}. {"March","März"}. -{"Maximum Number of Occupants","Maximale Anzahl von Teilnehmern"}. -{"Max # of items to persist","Maximale Anzahl dauerhaft zu speichernder Einträge"}. -{"Max payload size in bytes","Maximale Nutzlastgrösse in Bytes"}. +{"Max # of items to persist, or `max` for no specific limit other than a server imposed maximum","Maximale Anzahl der aufzubewahrenden Elemente oder `max`, wenn es keine spezifische Begrenzung gibt, außer einer vom Server festgelegten Höchstzahl"}. +{"Max payload size in bytes","Maximale Nutzdatengröße in Bytes"}. +{"Maximum file size","Maximale Dateigröße"}. +{"Maximum Number of History Messages Returned by Room","Maximale Anzahl der vom Raum zurückgegebenen History-Nachrichten"}. +{"Maximum number of items to persist","Maximale Anzahl persistenter Items"}. +{"Maximum Number of Occupants","Maximale Anzahl der Teilnehmer"}. {"May","Mai"}. -{"Membership is required to enter this room","Um diesen Raum zu betreten müssen sie Mitglied sein"}. -{"Members:","Mitglieder:"}. -{"Memorize your password, or write it in a paper placed in a safe place. In Jabber there isn't an automated way to recover your password if you forget it.","Merken sie sich ihr Passwort, oder schreiben sie es auf einen Zettel den sie sicher verwahren. Bei Jabber gibt es keine automatische Möglichkeit, das Passwort wiederherzustellen."}. -{"Memory","Speicher"}. +{"Membership is required to enter this room","Mitgliedschaft ist erforderlich um diesen Raum zu betreten"}. +{"Memorize your password, or write it in a paper placed in a safe place. In XMPP there isn't an automated way to recover your password if you forget it.","Merken Sie sich Ihr Passwort, oder schreiben Sie es auf einen Zettel den Sie sicher verwahren. Bei XMPP gibt es keine automatische Möglichkeit, das Passwort wiederherzustellen falls Sie es vergessen."}. +{"Mere Availability in XMPP (No Show Value)","Bloße Verfügbarkeit in XMPP (kein Anzeigewert)"}. {"Message body","Nachrichtentext"}. +{"Message not found in forwarded payload","Nachricht nicht in weitergeleiteten Nutzdaten gefunden"}. +{"Messages from strangers are rejected","Nachrichten von Fremden werden zurückgewiesen"}. +{"Messages of type headline","Nachrichten vom Typ 'headline'"}. +{"Messages of type normal","Nachrichten vom Typ 'normal'"}. {"Middle Name","Zweiter Vorname"}. -{"Minimum interval between voice requests (in seconds)","Mindestdauer zwischen Anfragen für Sprachrechte (in Sekunden)"}. -{"Moderator privileges required","Moderatorrechte benötigt"}. -{"moderators only","ausschliesslich Moderatoren"}. -{"Modified modules","Geänderte Module"}. -{"Module","Modul"}. -{"Modules at ","Module bei "}. -{"Modules","Module"}. +{"Minimum interval between voice requests (in seconds)","Mindestdauer zwischen Sprachrecht-Anforderung (in Sekunden)"}. +{"Moderator privileges required","Moderatorrechte erforderlich"}. +{"Moderator","Moderator"}. +{"Moderators Only","nur Moderatoren"}. +{"Module failed to handle the query","Modul konnte die Anfrage nicht verarbeiten"}. {"Monday","Montag"}. -{"Name:","Name:"}. +{"Multicast","Multicast"}. +{"Multiple elements are not allowed by RFC6121","Mehrere -Elemente sind laut RFC6121 nicht erlaubt"}. +{"Multi-User Chat","Mehrbenutzer-Chat (MUC)"}. {"Name","Vorname"}. +{"Natural Language for Room Discussions","Natürliche Sprache für Raumdiskussionen"}. +{"Natural-Language Room Name","Raumname in natürlicher Sprache"}. +{"Neither 'jid' nor 'nick' attribute found","Weder 'jid'- noch 'nick'-Attribut gefunden"}. +{"Neither 'role' nor 'affiliation' attribute found","Weder 'role'- noch 'affiliation'-Attribut gefunden"}. {"Never","Nie"}. {"New Password:","Neues Passwort:"}. -{"Nickname","Benutzername"}. -{"Nickname Registration at ","Registrieren des Benutzernames auf"}. -{"Nickname ~s does not exist in the room","Der Benutzername ~s existiert im Raum nicht"}. -{"nobody","niemanden"}. -{"No body provided for announce message","Kein Text für die Ankündigung angegeben"}. +{"Nickname can't be empty","Spitzname darf nicht leer sein"}. +{"Nickname Registration at ","Registrieren des Spitznamens auf "}. +{"Nickname ~s does not exist in the room","Der Spitzname ~s existiert nicht im Raum"}. +{"Nickname","Spitzname"}. +{"No address elements found","Keine 'address'-Elemente gefunden"}. +{"No addresses element found","Kein 'addresses'-Element gefunden"}. +{"No 'affiliation' attribute found","Kein 'affiliation'-Attribut gefunden"}. +{"No available resource found","Keine verfügbare Ressource gefunden"}. +{"No body provided for announce message","Kein Text für die Ankündigungsnachricht angegeben"}. +{"No child elements found","Keine 'child'-Elemente gefunden"}. +{"No data form found","Kein Datenformular gefunden"}. {"No Data","Keine Daten"}. -{"Node ID","Knoten-ID"}. -{"Node ","Knoten "}. -{"Node not found","Knoten nicht gefunden"}. -{"Nodes","Knoten"}. +{"No features available","Keine Eigenschaften verfügbar"}. +{"No element found","Kein -Element gefunden"}. +{"No hook has processed this command","Kein Hook hat diesen Befehl verarbeitet"}. +{"No info about last activity found","Keine Informationen über letzte Aktivität gefunden"}. +{"No 'item' element found","Kein 'item'-Element gefunden"}. +{"No items found in this query","Keine Items in dieser Anfrage gefunden"}. {"No limit","Keine Begrenzung"}. +{"No module is handling this query","Kein Modul verarbeitet diese Anfrage"}. +{"No node specified","Kein Knoten angegeben"}. +{"No 'password' found in data form","Kein 'password' im Datenformular gefunden"}. +{"No 'password' found in this query","Kein 'password' in dieser Anfrage gefunden"}. +{"No 'path' found in data form","Kein 'path' im Datenformular gefunden"}. +{"No pending subscriptions found","Keine ausstehenden Abonnements gefunden"}. +{"No privacy list with this name found","Keine Privacy-Liste mit diesem Namen gefunden"}. +{"No private data found in this query","Keine privaten Daten in dieser Anfrage gefunden"}. +{"No running node found","Kein laufender Knoten gefunden"}. +{"No services available","Keine Dienste verfügbar"}. +{"No statistics found for this item","Keine Statistiken für dieses Item gefunden"}. +{"No 'to' attribute found in the invitation","Kein 'to'-Attribut in der Einladung gefunden"}. +{"Nobody","Niemand"}. +{"Node already exists","Knoten existiert bereits"}. +{"Node ID","Knoten-ID"}. +{"Node index not found","Knotenindex nicht gefunden"}. +{"Node not found","Knoten nicht gefunden"}. +{"Node ~p","Knoten ~p"}. +{"Node","Knoten"}. +{"Nodeprep has failed","Nodeprep fehlgeschlagen"}. +{"Nodes","Knoten"}. {"None","Keine"}. -{"No resource provided","Keine Ressource angegeben"}. +{"Not allowed","Nicht erlaubt"}. {"Not Found","Nicht gefunden"}. -{"Notify subscribers when items are removed from the node","Abonnenten benachrichtigen, wenn Einträge vom Knoten entfernt werden"}. +{"Not subscribed","Nicht abonniert"}. +{"Notify subscribers when items are removed from the node","Abonnenten benachrichtigen, wenn Items vom Knoten entfernt werden"}. {"Notify subscribers when the node configuration changes","Abonnenten benachrichtigen, wenn sich die Knotenkonfiguration ändert"}. {"Notify subscribers when the node is deleted","Abonnenten benachrichtigen, wenn der Knoten gelöscht wird"}. {"November","November"}. +{"Number of answers required","Anzahl der erforderlichen Antworten"}. {"Number of occupants","Anzahl der Teilnehmer"}. +{"Number of Offline Messages","Anzahl der Offline-Nachrichten"}. {"Number of online users","Anzahl der angemeldeten Benutzer"}. {"Number of registered users","Anzahl der registrierten Benutzer"}. +{"Number of seconds after which to automatically purge items, or `max` for no specific limit other than a server imposed maximum","Anzahl der Sekunden, nach denen Elemente automatisch gelöscht werden sollen, oder `max`, wenn es keine spezifische Grenze gibt, außer einer vom Server festgelegten Höchstgrenze"}. +{"Occupants are allowed to invite others","Teilnehmer dürfen andere einladen"}. +{"Occupants are allowed to query others","Teilnehmer dürfen andere abfragen"}. +{"Occupants May Change the Subject","Teilnehmer dürfen das Thema ändern"}. {"October","Oktober"}. -{"Offline Messages:","Offline-Nachrichten:"}. -{"Offline Messages","Offline-Nachrichten"}. {"OK","OK"}. -{"Old Password:","Aktuelles Passwort:"}. -{"Online","Angemeldet"}. -{"Online Users:","Angemeldete Benutzer:"}. +{"Old Password:","Altes Passwort:"}. {"Online Users","Angemeldete Benutzer"}. +{"Online","Angemeldet"}. +{"Only collection node owners may associate leaf nodes with the collection","Nur Sammlungsknoten-Besitzer dürfen Blattknoten mit der Sammlung verknüpfen"}. {"Only deliver notifications to available users","Benachrichtigungen nur an verfügbare Benutzer schicken"}. -{"Only moderators and participants are allowed to change the subject in this room","Nur Moderatoren und Mitglieder dürfen das Thema in diesem Raum ändern"}. +{"Only or tags are allowed","Nur - oder -Tags sind erlaubt"}. +{"Only element is allowed in this query","Nur -Elemente sind in dieser Anfrage erlaubt"}. +{"Only members may query archives of this room","Nur Mitglieder dürfen den Verlauf dieses Raumes abrufen"}. +{"Only moderators and participants are allowed to change the subject in this room","Nur Moderatoren und Teilnehmer dürfen das Thema in diesem Raum ändern"}. {"Only moderators are allowed to change the subject in this room","Nur Moderatoren dürfen das Thema in diesem Raum ändern"}. -{"Only moderators can approve voice requests","Nur Moderatoren können Anfragen für Sprachrechte bestätigen"}. -{"Only occupants are allowed to send messages to the conference","Nur Teilnehmer dürfen Nachrichten an den Raum schicken"}. -{"Only occupants are allowed to send queries to the conference","Nur Teilnehmer sind berechtigt Anfragen an die Konferenz zu senden"}. -{"Only service administrators are allowed to send service messages","Nur Service-Administratoren sind berechtigt, Servicenachrichten zu versenden"}. -{"Options","Optionen"}. -{"Organization Name","Organisation"}. +{"Only moderators are allowed to retract messages","Nur Moderatoren dürfen Nachrichten zurückziehen"}. +{"Only moderators can approve voice requests","Nur Moderatoren können Sprachrecht-Anforderungen genehmigen"}. +{"Only occupants are allowed to send messages to the conference","Nur Teilnehmer dürfen Nachrichten an die Konferenz senden"}. +{"Only occupants are allowed to send queries to the conference","Nur Teilnehmer dürfen Anfragen an die Konferenz senden"}. +{"Only publishers may publish","Nur Veröffentlicher dürfen veröffentlichen"}. +{"Only service administrators are allowed to send service messages","Nur Service-Administratoren dürfen Servicenachrichten senden"}. +{"Only those on a whitelist may associate leaf nodes with the collection","Nur jemand auf einer Whitelist darf Blattknoten mit der Sammlung verknüpfen"}. +{"Only those on a whitelist may subscribe and retrieve items","Nur jemand auf einer Whitelist darf Items abonnieren und abrufen"}. +{"Organization Name","Name der Organisation"}. {"Organization Unit","Abteilung"}. -{"Outgoing s2s Connections:","Ausgehende s2s-Verbindungen:"}. +{"Other Modules Available:","Andere Module verfügbar:"}. {"Outgoing s2s Connections","Ausgehende s2s-Verbindungen"}. -{"Outgoing s2s Servers:","Ausgehende s2s-Server:"}. -{"Owner privileges required","Besitzerrechte benötigt"}. -{"Packet","Paket"}. -{"Password ~b","Passwort ~b"}. -{"Password:","Passwort:"}. -{"Password","Passwort"}. -{"Password Verification:","Passwort bestätigen:"}. +{"Owner privileges required","Besitzerrechte erforderlich"}. +{"Packet relay is denied by service policy","Paket-Relay aufgrund der Dienstrichtlinien verweigert"}. +{"Participant ID","Teilnehmer-ID"}. +{"Participant","Teilnehmer"}. {"Password Verification","Passwort bestätigen"}. +{"Password Verification:","Passwort bestätigen:"}. +{"Password","Passwort"}. +{"Password:","Passwort:"}. {"Path to Dir","Pfad zum Verzeichnis"}. {"Path to File","Pfad zur Datei"}. -{"Pending","anhängig"}. {"Period: ","Zeitraum: "}. -{"Persist items to storage","Einträge dauerhaft speichern"}. +{"Persist items to storage","Items dauerhaft speichern"}. +{"Persistent","Persistent"}. +{"Ping query is incorrect","Ping-Anfrage ist falsch"}. {"Ping","Ping"}. -{"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Beachten sie, das diese Optionen nur die eingebaute Mnesia-Datenbank sichern. Wenn sie das ODBC-Modul verwenden, müssen sie die SQL-Datenbank manuell sichern."}. -{"Please, wait for a while before sending new voice request","Bitte warten sie ein wenig, bevor sie eine weitere Anfrage für Sprachrechte senden"}. +{"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Beachten Sie, dass diese Optionen nur die eingebaute Mnesia-Datenbank sichern. Wenn Sie das ODBC-Modul verwenden, müssen Sie auch Ihre SQL-Datenbank separat sichern."}. +{"Please, wait for a while before sending new voice request","Bitte warten Sie ein wenig, bevor Sie eine weitere Sprachrecht-Anforderung senden"}. {"Pong","Pong"}. -{"Port ~b","Port ~b"}. -{"Port","Port"}. +{"Possessing 'ask' attribute is not allowed by RFC6121","Ein 'ask'-Attribut zu besitzen ist laut RFC6121 nicht erlaubt"}. {"Present real Jabber IDs to","Echte Jabber-IDs anzeigen für"}. +{"Previous session not found","Vorherige Sitzung nicht gefunden"}. +{"Previous session PID has been killed","Vorherige Sitzungs-PID wurde getötet"}. +{"Previous session PID has exited","Vorherige Sitzungs-PID wurde beendet"}. +{"Previous session PID is dead","Vorherige Sitzungs-PID ist tot"}. +{"Previous session timed out","Zeitüberschreitung bei vorheriger Sitzung"}. {"private, ","privat, "}. -{"Protocol","Protokoll"}. +{"Public","Öffentlich"}. +{"Publish model","Veröffentlichungsmodell"}. {"Publish-Subscribe","Publish-Subscribe"}. -{"PubSub subscriber request","PubSub-Abonnenten-Anfrage"}. -{"Purge all items when the relevant publisher goes offline","Alle Einträge entfernen, wenn der relevante Veröffentlicher offline geht"}. -{"Queries to the conference members are not allowed in this room","Anfragen an die Teilnehmer sind in diesem Raum nicht erlaubt"}. +{"PubSub subscriber request","PubSub-Abonnenten-Anforderung"}. +{"Purge all items when the relevant publisher goes offline","Alle Items löschen, wenn der relevante Veröffentlicher offline geht"}. +{"Push record not found","Push-Eintrag nicht gefunden"}. +{"Queries to the conference members are not allowed in this room","Anfragen an die Konferenzteilnehmer sind in diesem Raum nicht erlaubt"}. +{"Query to another users is forbidden","Anfrage an andere Benutzer ist verboten"}. {"RAM and disc copy","RAM und Festplatte"}. {"RAM copy","Nur RAM"}. -{"Raw","Unformatiert"}. -{"Really delete message of the day?","Die Nachricht des Tages wirklich löschen?"}. -{"Recipient is not in the conference room","Der Empfänger ist nicht im Raum"}. -{"Register a Jabber account","Jabber Konto registrieren"}. +{"Really delete message of the day?","Nachricht des Tages wirklich löschen?"}. +{"Receive notification from all descendent nodes","Benachrichtigung von allen abstammenden Nodes erhalten"}. +{"Receive notification from direct child nodes only","Benachrichtigung nur von direkten Kindknoten erhalten"}. +{"Receive notification of new items only","Benachrichtigung nur von neuen Items erhalten"}. +{"Receive notification of new nodes only","Benachrichtigung nur von neuen Knoten erhalten"}. +{"Recipient is not in the conference room","Empfänger ist nicht im Konferenzraum"}. +{"Register an XMPP account","Ein XMPP-Konto registrieren"}. {"Register","Anmelden"}. -{"Registered Users:","Registrierte Benutzer:"}. -{"Registered Users","Registrierte Benutzer"}. -{"Registration in mod_irc for ","Registrierung in mod_irc für "}. {"Remote copy","Fernkopie"}. -{"Remove All Offline Messages","Alle Offline Nachrichten löschen"}. -{"Remove","Entfernen"}. +{"Remove a hat from a user","Eine Funktion bei einem Benutzer entfernen"}. {"Remove User","Benutzer löschen"}. {"Replaced by new connection","Durch neue Verbindung ersetzt"}. +{"Request has timed out","Zeitüberschreitung bei Anforderung"}. +{"Request is ignored","Anforderung wird ignoriert"}. +{"Requested role","Angeforderte Rolle"}. {"Resources","Ressourcen"}. -{"Restart","Neustart"}. {"Restart Service","Dienst neustarten"}. -{"Restore Backup from File at ","Datenwiederherstellung aus der Datei "}. -{"Restore binary backup after next ejabberd restart (requires less memory):","Stelle binäre Sicherung beim nächsten ejabberd-Neustart wieder her (benötigt weniger Speicher):"}. -{"Restore binary backup immediately:","Stelle binäre Sicherung sofort wieder her:"}. -{"Restore plain text backup immediately:","Stelle Klartext-Sicherung sofort wieder her:"}. +{"Restore Backup from File at ","Backup wiederherstellen aus Datei bei "}. +{"Restore binary backup after next ejabberd restart (requires less memory):","Stelle binäres Backup beim nächsten ejabberd-Neustart wieder her (benötigt weniger Speicher):"}. +{"Restore binary backup immediately:","Stelle binäres Backup sofort wieder her:"}. +{"Restore plain text backup immediately:","Stelle Klartext-Backup sofort wieder her:"}. {"Restore","Wiederherstellung"}. -{"Room Configuration","Raum-Konfiguration"}. +{"Result","Ergebnis"}. +{"Roles and Affiliations that May Retrieve Member List","Rollen und Zugehörigkeiten die Mitgliederliste abrufen dürfen"}. +{"Roles for which Presence is Broadcasted","Rollen für welche die Präsenz übertragen wird"}. +{"Roles that May Send Private Messages","Rollen die Privatnachrichten senden dürfen"}. +{"Room Configuration","Raumkonfiguration"}. {"Room creation is denied by service policy","Anlegen des Raumes aufgrund der Dienstrichtlinien verweigert"}. -{"Room description","Raum Beschreibung"}. -{"Room Occupants","Teilnehmer in diesem Raum"}. +{"Room description","Raumbeschreibung"}. +{"Room Occupants","Raumteilnehmer"}. +{"Room terminates","Raum wird beendet"}. {"Room title","Raumname"}. -{"Roster groups allowed to subscribe","Kontaktlisten-Gruppen die abonnieren dürfen"}. -{"Roster","Kontaktliste"}. -{"Roster of ","Kontaktliste von "}. +{"Roster groups allowed to subscribe","Kontaktlistengruppen die abonnieren dürfen"}. {"Roster size","Kontaktlistengröße"}. -{"RPC Call Error","Fehler bei RPC-Aufruf"}. -{"Running Nodes","Aktive Knoten"}. -{"~s access rule configuration","~s Zugangsregel-Konfiguration"}. -{"Saturday","Samstag"}. -{"Script check","Script-Überprüfung"}. -{"Search Results for ","Suchergebnisse für "}. -{"Search users in ","Benutzer suchen in "}. -{"Send announcement to all online users on all hosts","Sende Ankündigung an alle angemeldeten Benutzer auf allen Hosts"}. -{"Send announcement to all online users","Sende Ankündigung an alle angemeldeten Benutzer"}. -{"Send announcement to all users on all hosts","Sende Ankündigung an alle Benutzer auf allen Hosts"}. -{"Send announcement to all users","Sende Ankündigung an alle Benutzer"}. -{"September","September"}. -{"Server ~b","Server ~b"}. -{"Server:","Server:"}. -{"Set message of the day and send to online users","Setze Nachricht des Tages und sende sie an alle angemeldeten Benutzer"}. -{"Set message of the day on all hosts and send to online users","Setze Nachricht des Tages auf allen Hosts und sende sie an alle angemeldeten Benutzer"}. -{"Shared Roster Groups","Gruppen der gemeinsamen Kontaktliste"}. -{"Show Integral Table","Vollständige Tabelle anzeigen"}. -{"Show Ordinary Table","Normale Tabelle anzeigen"}. -{"Shut Down Service","Dienst herunterfahren"}. +{"Running Nodes","Laufende Knoten"}. {"~s invites you to the room ~s","~s lädt Sie in den Raum ~s ein"}. -{"Some Jabber clients can store your password in your computer. Use that feature only if you trust your computer is safe.","Einige Jabber Client Programme speichern ihr Passwort auf ihrem Computer. Verwenden sie diese Möglichkeit nur auf Computern, die sie als sicher einstufen."}. -{"Specify the access model","Geben sie das Zugangsmodell an"}. -{"Specify the event message type","Geben sie den Ereignis-Nachrichtentyp an"}. -{"Specify the publisher model","Geben sie das Publikationsmodell an"}. -{"~s's Offline Messages Queue","~s's Offline-Nachrichten-Warteschlange"}. -{"Start Modules at ","Starte Module auf "}. -{"Start Modules","Module starten"}. -{"Start","Starten"}. -{"Statistics of ~p","Statistiken von ~p"}. -{"Statistics","Statistik"}. -{"Stop Modules at ","Stoppe Module auf "}. -{"Stop Modules","Module stoppen"}. -{"Stopped Nodes","Inaktive Knoten"}. -{"Stop","Stoppen"}. -{"Storage Type","Speichertyp"}. -{"Store binary backup:","Speichere binäre Sicherung:"}. -{"Store plain text backup:","Speichere Klartext-Sicherung:"}. +{"Saturday","Samstag"}. +{"Search from the date","Suche ab Datum"}. +{"Search Results for ","Suchergebnisse für "}. +{"Search the text","Text durchsuchen"}. +{"Search until the date","Suche bis Datum"}. +{"Search users in ","Suche Benutzer in "}. +{"Send announcement to all online users on all hosts","Ankündigung an alle angemeldeten Benutzer auf allen Hosts senden"}. +{"Send announcement to all online users","Ankündigung an alle angemeldeten Benutzer senden"}. +{"Send announcement to all users on all hosts","Ankündigung an alle Benutzer auf allen Hosts senden"}. +{"Send announcement to all users","Ankündigung an alle Benutzer senden"}. +{"September","September"}. +{"Server:","Server:"}. +{"Service list retrieval timed out","Zeitüberschreitung bei Abfrage der Serviceliste"}. +{"Session state copying timed out","Zeitüberschreitung beim Kopieren des Sitzungszustandes"}. +{"Set message of the day and send to online users","Nachricht des Tages setzen und an alle angemeldeten Benutzer senden"}. +{"Set message of the day on all hosts and send to online users","Nachricht des Tages auf allen Hosts setzen und an alle angemeldeten Benutzer senden"}. +{"Shared Roster Groups","Gruppen der gemeinsamen Kontaktliste"}. +{"Show Integral Table","Integral-Tabelle anzeigen"}. +{"Show Ordinary Table","Gewöhnliche Tabelle anzeigen"}. +{"Shut Down Service","Dienst herunterfahren"}. +{"SOCKS5 Bytestreams","SOCKS5-Bytestreams"}. +{"Some XMPP clients can store your password in the computer, but you should do this only in your personal computer for safety reasons.","Einige XMPP-Clients speichern Ihr Passwort auf dem Computer. Aus Sicherheitsgründen sollten Sie das nur auf Ihrem persönlichen Computer tun."}. +{"Sources Specs:","Quellenspezifikationen:"}. +{"Specify the access model","Geben Sie das Zugangsmodell an"}. +{"Specify the event message type","Geben Sie den Ereignisnachrichtentyp an"}. +{"Specify the publisher model","Geben Sie das Veröffentlichermodell an"}. +{"Stanza id is not valid","Stanza-ID ist ungültig"}. +{"Stanza ID","Stanza-ID"}. +{"Statically specify a replyto of the node owner(s)","Ein 'replyto' des/der Nodebesitzer(s) statisch angeben"}. +{"Stopped Nodes","Angehaltene Knoten"}. +{"Store binary backup:","Speichere binäres Backup:"}. +{"Store plain text backup:","Speichere Klartext-Backup:"}. +{"Stream management is already enabled","Stream-Verwaltung ist bereits aktiviert"}. +{"Stream management is not enabled","Stream-Verwaltung ist nicht aktiviert"}. {"Subject","Betreff"}. -{"Submit","Senden"}. {"Submitted","Gesendet"}. {"Subscriber Address","Abonnenten-Adresse"}. -{"Subscription","Abonnement"}. +{"Subscribers may publish","Abonnenten dürfen veröffentlichen"}. +{"Subscription requests must be approved and only subscribers may retrieve items","Abonnement-Anforderungen müssen genehmigt werden und nur Abonnenten dürfen Items abrufen"}. +{"Subscriptions are not allowed","Abonnements sind nicht erlaubt"}. {"Sunday","Sonntag"}. -{"That nickname is already in use by another occupant","Dieser Benutzername wird bereits von einem Teilnehmer genutzt"}. -{"That nickname is registered by another person","Dieser Benutzername wurde bereits von jemand anderem registriert"}. -{"The CAPTCHA is valid.","Die Verifizierung ist gültig."}. -{"The CAPTCHA verification has failed","Die CAPTCHA Verifizierung schlug fehl"}. -{"The collections with which a node is affiliated","Sammlungen, mit denen ein Knoten verknüpft ist"}. +{"Text associated with a picture","Text verbunden mit einem Bild"}. +{"Text associated with a sound","Text verbunden mit einem Klang"}. +{"Text associated with a video","Text verbunden mit einem Video"}. +{"Text associated with speech","Text verbunden mit Sprache"}. +{"That nickname is already in use by another occupant","Dieser Spitzname wird bereits von einem anderen Teilnehmer verwendet"}. +{"That nickname is registered by another person","Dieser Spitzname wurde von jemand anderem registriert"}. +{"The account already exists","Das Konto existiert bereits"}. +{"The account was not unregistered","Das Konto wurde nicht entfernt"}. +{"The body text of the last received message","Der Nachrichtenkörper der letzten erhaltenen Nachricht"}. +{"The CAPTCHA is valid.","Das CAPTCHA ist gültig."}. +{"The CAPTCHA verification has failed","Die CAPTCHA-Verifizierung ist fehlgeschlagen"}. +{"The captcha you entered is wrong","Das CAPTCHA das Sie eingegeben haben ist falsch"}. +{"The child nodes (leaf or collection) associated with a collection","Die mit einer Sammlung verknüpften Kindknoten (Blatt oder Sammlung)"}. +{"The collections with which a node is affiliated","Sammlungen, mit welchen ein Knoten in Verbindung steht"}. +{"The DateTime at which a leased subscription will end or has ended","Das DateTime an welchem ein geleastes Abonnement enden wird oder geendet hat"}. +{"The datetime when the node was created","Das DateTime an welchem der Knoten erstellt wurde"}. +{"The default language of the node","Die voreingestellte Sprache des Knotens"}. +{"The feature requested is not supported by the conference","Die angeforderte Eigenschaft wird von der Konferenz nicht unterstützt"}. +{"The JID of the node creator","Die JID des Nodeerstellers"}. +{"The JIDs of those to contact with questions","Die JIDs jener, die bei Fragen zu kontaktieren sind"}. +{"The JIDs of those with an affiliation of owner","Die JIDs jener mit einer Zugehörigkeit von Besitzer"}. +{"The JIDs of those with an affiliation of publisher","Die JIDs jener mit einer Zugehörigkeit von Veröffentlicher"}. +{"The list of all online users","Die Liste aller angemeldeter Benutzer"}. +{"The list of all users","Die Liste aller Benutzer"}. +{"The list of JIDs that may associate leaf nodes with a collection","Die Liste der JIDs die Blattknoten mit einer Sammlung verknüpfen dürfen"}. +{"The maximum number of child nodes that can be associated with a collection, or `max` for no specific limit other than a server imposed maximum","Die Höchstzahl der untergeordneten Knoten, die einer Sammlung zugeordnet werden können, oder `max`, wenn es keine spezifische Begrenzung gibt, sondern nur eine vom Server festgelegte Höchstzahl"}. +{"The minimum number of milliseconds between sending any two notification digests","Die minimale Anzahl an Millisekunden zwischen dem Senden von zwei Benachrichtigungs-Übersichten"}. +{"The name of the node","Der Name des Knotens"}. +{"The node is a collection node","Der Knoten ist ein Sammlungsknoten"}. +{"The node is a leaf node (default)","Der Knoten ist ein Blattknoten (Voreinstellung)"}. +{"The NodeID of the relevant node","Die NodeID des relevanten Knotens"}. +{"The number of pending incoming presence subscription requests","Die Anzahl der ausstehenden eintreffenden Präsenzabonnement-Anforderungen"}. +{"The number of subscribers to the node","Die Anzahl der Abonnenten des Knotens"}. +{"The number of unread or undelivered messages","Die Anzahl der ungelesenen oder nicht zugestellten Nachrichten"}. +{"The password contains unacceptable characters","Das Passwort enthält ungültige Zeichen"}. +{"The password is too weak","Das Passwort ist zu schwach"}. {"the password is","das Passwort lautet"}. -{"The password is too weak","Das Passwort ist zu einfach"}. -{"The password of your Jabber account was successfully changed.","Das Passwort von ihrem Jabber Konto wurde geändert."}. -{"There was an error changing the password: ","Es trat ein Fehler beim Ändern des Passworts auf:"}. -{"There was an error creating the account: ","Es trat ein Fehler beim erstellen des Kontos auf:"}. -{"There was an error deleting the account: ","Es trat ein Fehler beim Löschen des Kontos auf:"}. -{"This is case insensitive: macbeth is the same that MacBeth and Macbeth.","Groß/Klein-Schreibung spielt hierbei keine Rolle: macbeth ist gleich MacBeth und Macbeth."}. -{"This page allows to create a Jabber account in this Jabber server. Your JID (Jabber IDentifier) will be of the form: username@server. Please read carefully the instructions to fill correctly the fields.","Diese Seite erlaubt das anlegen eines Jabber Kontos auf diesem Jabber Server. Ihre JID (Jabber IDentifier) setzt sich folgend zusammen: benutzername@server. Bitte lesen sie die Hinweise genau durch, um die Felder korrekt auszufüllen."}. -{"This page allows to unregister a Jabber account in this Jabber server.","Diese Seite erlaubt es, ein Jabber Konto von diesem Server zu entfernen."}. -{"This participant is kicked from the room because he sent an error message","Dieser Teilnehmer wurde aus dem Raum geworfen, da er eine fehlerhafte Nachricht gesendet hat"}. -{"This participant is kicked from the room because he sent an error message to another participant","Dieser Teilnehmer wurde aus dem Raum geworfen, da er eine fehlerhafte Nachricht an einen anderen Teilnehmer gesendet hat"}. -{"This participant is kicked from the room because he sent an error presence","Dieser Teilnehmer wurde aus dem Raum gekickt, da er einen fehlerhaften Status gesendet hat"}. +{"The password of your XMPP account was successfully changed.","Das Passwort Ihres XMPP-Kontos wurde erfolgreich geändert."}. +{"The password was not changed","Das Passwort wurde nicht geändert"}. +{"The passwords are different","Die Passwörter sind unterschiedlich"}. +{"The presence states for which an entity wants to receive notifications","Die Präsenzzustände für welche eine Entität Benachrichtigungen erhalten will"}. +{"The query is only allowed from local users","Die Anfrage ist nur von lokalen Benutzern erlaubt"}. +{"The query must not contain elements","Die Anfrage darf keine -Elemente enthalten"}. +{"The room subject can be modified by participants","Das Raum-Thema kann von Teilnehmern geändert werden"}. +{"The sender of the last received message","Der Absender der letzten erhaltenen Nachricht"}. +{"The stanza MUST contain only one element, one element, or one element","Das Stanza darf nur ein -Element, ein -Element oder ein -Element enthalten"}. +{"The subscription identifier associated with the subscription request","Die mit der Abonnement-Anforderung verknüpfte Abonnement-Bezeichnung"}. +{"The URL of an XSL transformation which can be applied to payloads in order to generate an appropriate message body element.","Die URL einer XSL-Transformation welche auf Nutzdaten angewendet werden kann, um ein geeignetes Nachrichtenkörper-Element zu generieren."}. +{"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","Die URL einer XSL-Transformation welche auf das Nutzdaten-Format angewendet werden kann, um ein gültiges Data Forms-Ergebnis zu generieren das der Client mit Hilfe einer generischen Data Forms-Rendering-Engine anzeigen könnte"}. +{"There was an error changing the password: ","Es trat ein Fehler beim Ändern des Passwortes auf: "}. +{"There was an error creating the account: ","Es trat ein Fehler beim Erstellen des Kontos auf: "}. +{"There was an error deleting the account: ","Es trat ein Fehler beim Löschen des Kontos auf: "}. +{"This is case insensitive: macbeth is the same that MacBeth and Macbeth.","Dies ist schreibungsunabhängig: macbeth ist gleich MacBeth und 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.","Diese Seite erlaubt das Anlegen eines XMPP-Kontos auf diesem XMPP-Server. Ihre JID (Jabber-ID) wird diese Form aufweisen: benutzername@server. Bitte lesen Sie die Anweisungen genau durch, um die Felder korrekt auszufüllen."}. +{"This page allows to unregister an XMPP account in this XMPP server.","Diese Seite erlaubt es, ein XMPP-Konto von diesem XMPP-Server zu entfernen."}. {"This room is not anonymous","Dieser Raum ist nicht anonym"}. +{"This service can not process the address: ~s","Dieser Dienst kann die Adresse nicht verarbeiten: ~s"}. {"Thursday","Donnerstag"}. {"Time delay","Zeitverzögerung"}. -{"Time","Zeit"}. -{"To","An"}. -{"Too many CAPTCHA requests","Zu viele CAPTCHA Anfragen"}. -{"To ~s","An ~s"}. +{"Timed out waiting for stream resumption","Zeitüberschreitung beim Warten auf Streamfortsetzung"}. +{"To register, visit ~s","Um sich zu registrieren, besuchen Sie ~s"}. +{"To ~ts","An ~ts"}. +{"Token TTL","Token-TTL"}. +{"Too many active bytestreams","Zu viele aktive Bytestreams"}. +{"Too many CAPTCHA requests","Zu viele CAPTCHA-Anforderungen"}. +{"Too many child elements","Zu viele 'child'-Elemente"}. +{"Too many elements","Zu viele -Elemente"}. +{"Too many elements","Zu viele -Elemente"}. +{"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","Zu viele (~p) fehlgeschlagene Authentifizierungen von dieser IP-Adresse (~s). Die Adresse wird an ~s UTC entsperrt"}. +{"Too many receiver fields were specified","Zu viele Empfängerfelder wurden angegeben"}. +{"Too many unacked stanzas","Zu viele unbestätigte Stanzas"}. +{"Too many users in this conference","Zu viele Benutzer in dieser Konferenz"}. {"Traffic rate limit is exceeded","Datenratenlimit wurde überschritten"}. -{"Transactions Aborted:","Abgebrochene Transaktionen:"}. -{"Transactions Committed:","Durchgeführte Transaktionen:"}. -{"Transactions Logged:","Protokollierte Transaktionen:"}. -{"Transactions Restarted:","Neu gestartete Transaktionen:"}. +{"~ts's MAM Archive","~ts's MAM Archiv"}. +{"~ts's Offline Messages Queue","Offline-Nachrichten-Warteschlange von ~ts"}. {"Tuesday","Dienstag"}. -{"Unable to generate a CAPTCHA","Konnte CAPTCHA nicht erstellen"}. -{"Unauthorized","Nicht berechtigt"}. -{"Unregister","Abmelden"}. -{"Unregister a Jabber account","Jabber Konto entfernen"}. -{"Update","Aktualisieren"}. -{"Update ","Aktualisierung "}. +{"Unable to generate a CAPTCHA","Konnte kein CAPTCHA erstellen"}. +{"Unable to register route on existing local domain","Konnte Route auf existierender lokaler Domäne nicht registrieren"}. +{"Unauthorized","Nicht autorisiert"}. +{"Unexpected action","Unerwartete Aktion"}. +{"Unexpected error condition: ~p","Unerwarteter Fehlerzustand: ~p"}. +{"Uninstall","Deinstallieren"}. +{"Unregister an XMPP account","Ein XMPP-Konto entfernen"}. +{"Unregister","Deregistrieren"}. +{"Unsupported element","Nicht unterstütztes -Element"}. +{"Unsupported version","Nicht unterstützte Version"}. {"Update message of the day (don't send)","Aktualisiere Nachricht des Tages (nicht senden)"}. {"Update message of the day on all hosts (don't send)","Aktualisiere Nachricht des Tages auf allen Hosts (nicht senden)"}. -{"Update plan","Aktualisierungsplan"}. -{"Update script","Aktualisierungsscript"}. -{"Uptime:","Betriebszeit:"}. -{"Use of STARTTLS required","Verwendung von STARTTLS erforderlich"}. -{"User ","Benutzer "}. -{"User","Benutzer"}. -{"User JID","Benutzer JID"}. +{"Update specs to get modules source, then install desired ones.","Aktualisieren Sie die Spezifikationen, um den Quellcode der Module zu erhalten und installieren Sie dann die gewünschten Module."}. +{"Update Specs","Spezifikationen aktualisieren"}. +{"Updating the vCard is not supported by the vCard storage backend","Aktualisierung der vCard wird vom vCard-Speicher-Backend nicht unterstützt"}. +{"Upgrade","Upgrade"}. +{"URL for Archived Discussion Logs","URL für archivierte Diskussionsprotokolle"}. +{"User already exists","Benutzer existiert bereits"}. +{"User (jid)","Benutzer (JID)"}. +{"User JID","Benutzer-JID"}. {"User Management","Benutzerverwaltung"}. +{"User removed","Benutzer entfernt"}. +{"User session not found","Benutzersitzung nicht gefunden"}. +{"User session terminated","Benutzersitzung beendet"}. +{"User ~ts","Benutzer ~ts"}. +{"User","Benutzer"}. {"Username:","Benutzername:"}. {"Users are not allowed to register accounts so quickly","Benutzer dürfen Konten nicht so schnell registrieren"}. -{"Users","Benutzer"}. {"Users Last Activity","Letzte Benutzeraktivität"}. -{"Validate","Validieren"}. +{"Users","Benutzer"}. +{"Value 'get' of 'type' attribute is not allowed","Wert 'get' des 'type'-Attributs ist nicht erlaubt"}. +{"Value of '~s' should be boolean","Wert von '~s' sollte boolesch sein"}. +{"Value of '~s' should be datetime string","Wert von '~s' sollte DateTime-Zeichenkette sein"}. +{"Value of '~s' should be integer","Wert von '~s' sollte eine Ganzzahl sein"}. +{"Value 'set' of 'type' attribute is not allowed","Wert 'set' des 'type'-Attributs ist nicht erlaubt"}. {"vCard User Search","vCard-Benutzer-Suche"}. +{"View joined MIX channels","Beitretene MIX-Channel ansehen"}. {"Virtual Hosts","Virtuelle Hosts"}. -{"Visitors are not allowed to change their nicknames in this room","Besucher dürfen in diesem Raum ihren Benutzernamen nicht ändern"}. -{"Visitors are not allowed to send messages to all occupants","Besucher dürfen nicht an alle Teilnehmer Nachrichten verschicken"}. -{"Voice request","Anfrage für Sprachrechte"}. -{"Voice requests are disabled in this conference","Anfragen für Sprachrechte sind in diesem Raum deaktiviert"}. +{"Visitor","Besucher"}. +{"Visitors are not allowed to change their nicknames in this room","Besucher dürfen in diesem Raum ihren Spitznamen nicht ändern"}. +{"Visitors are not allowed to send messages to all occupants","Besucher dürfen nicht an alle Teilnehmer Nachrichten versenden"}. +{"Voice requests are disabled in this conference","Sprachrecht-Anforderungen sind in diesem Raum deaktiviert"}. +{"Voice request","Sprachrecht-Anforderung"}. +{"Web client which allows to join the room anonymously","Web-Client, der es ermöglicht, dem Raum anonym beizutreten"}. {"Wednesday","Mittwoch"}. -{"When to send the last published item","Wann soll das letzte veröffentlichte Objekt gesendet werden"}. +{"When a new subscription is processed and whenever a subscriber comes online","Sobald ein neues Abonnement verarbeitet wird und wann immer ein Abonnent sich anmeldet"}. +{"When a new subscription is processed","Sobald ein neues Abonnement verarbeitet wird"}. +{"When to send the last published item","Wann das letzte veröffentlichte Item gesendet werden soll"}. +{"Whether an entity wants to receive an XMPP message body in addition to the payload format","Ob eine Entität zusätzlich zum Nutzdatenformat einen XMPP-Nachrichtenkörper erhalten will"}. +{"Whether an entity wants to receive digests (aggregations) of notifications or all notifications individually","Ob eine Entität Übersichten (Gruppierungen) von Benachrichtigungen oder alle Benachrichtigungen separat erhalten will"}. +{"Whether an entity wants to receive or disable notifications","Ob eine Entität Benachrichtigungen erhalten oder deaktivieren will"}. +{"Whether owners or publisher should receive replies to items","Ob Besitzer oder Veröffentlicher Antworten auf Items erhalten sollen"}. +{"Whether the node is a leaf (default) or a collection","Ob der Knoten ein Blatt (Voreinstellung) oder eine Sammlung ist"}. {"Whether to allow subscriptions","Ob Abonnements erlaubt sind"}. -{"You can later change your password using a Jabber client.","Sie können das Passwort später mit einem Jabber Client Programm ändern."}. +{"Whether to make all subscriptions temporary, based on subscriber presence","Ob alle Abonnements temporär gemacht werden sollen, basierend auf der Abonnentenpräsenz"}. +{"Whether to notify owners about new subscribers and unsubscribes","Ob Besitzer über neue Abonnenten und Abbestellungen benachrichtigt werden sollen"}. +{"Who can send private messages","Wer kann private Nachrichten senden"}. +{"Who may associate leaf nodes with a collection","Wer Blattknoten mit einer Sammlung verknüpfen darf"}. +{"Wrong parameters in the web formulary","Falsche Parameter im Webformular"}. +{"Wrong xmlns","Falscher xmlns"}. +{"XMPP Account Registration","XMPP-Konto-Registrierung"}. +{"XMPP Domains","XMPP-Domänen"}. +{"XMPP Show Value of Away","XMPP-Anzeigewert von Abwesend"}. +{"XMPP Show Value of Chat","XMPP-Anzeigewert von Chat"}. +{"XMPP Show Value of DND (Do Not Disturb)","XMPP-Anzeigewert von DND (Do Not Disturb/Bitte nicht stören)"}. +{"XMPP Show Value of XA (Extended Away)","XMPP-Anzeigewert von XA (Extended Away/für längere Zeit abwesend)"}. +{"XMPP URI of Associated Publish-Subscribe Node","XMPP-URI des verknüpften Publish-Subscribe-Knotens"}. +{"You are being removed from the room because of a system shutdown","Sie werden wegen einer Systemabschaltung aus dem Raum entfernt"}. +{"You are not allowed to send private messages","Sie dürfen keine privaten Nachrichten senden"}. +{"You are not joined to the channel","Sie sind dem Raum nicht beigetreten"}. +{"You can later change your password using an XMPP client.","Sie können Ihr Passwort später mit einem XMPP-Client ändern."}. {"You have been banned from this room","Sie wurden aus diesem Raum verbannt"}. -{"You must fill in field \"Nickname\" in the form","Sie müssen das Feld \"Benutzername\" ausfüllen"}. -{"You need a client that supports x:data and CAPTCHA to register","Sie benötigen einen Client, der x:data und CAPTCHA unterstützt, um Ihren Benutzernamen zu registrieren"}. -{"You need a client that supports x:data to register the nickname","Sie benötigen einen Client, der x:data unterstützt, um Ihren Benutzernamen zu registrieren"}. -{"You need an x:data capable client to configure mod_irc settings","Sie benötigen einen Client, der x:data unterstützt, um die mod_irc-Einstellungen zu konfigurieren"}. -{"You need an x:data capable client to configure room","Sie benötigen einen Client, der x:data unterstützt, um den Raum zu konfigurieren"}. -{"You need an x:data capable client to search","Sie benötigen einen Client, der x:data unterstützt, um die Suche verwenden zu können"}. -{"Your active privacy list has denied the routing of this stanza.","Ihre aktive Privacy Liste hat die Weiterleitung des Stanzas unterbunden."}. -{"Your contact offline message queue is full. The message has been discarded.","Ihre Offline-Nachrichten-Warteschlange ist voll. Die Nachricht wurde verworfen."}. -{"Your Jabber account was successfully created.","Ihr Jabber Konto wurde erfolgreich erstellt."}. -{"Your Jabber account was successfully deleted.","Ihr Jabber Konto wurde erfolgreich gelöscht."}. -{"Your messages to ~s are being blocked. To unblock them, visit ~s","Ihre Nachrichten an ~s werden blockiert. Um dies zu ändern, besuchen sie ~s"}. +{"You have joined too many conferences","Sie sind zu vielen Konferenzen beigetreten"}. +{"You must fill in field \"Nickname\" in the form","Sie müssen das Feld \"Spitzname\" im Formular ausfüllen"}. +{"You need a client that supports x:data and CAPTCHA to register","Sie benötigen einen Client der x:data und CAPTCHA unterstützt, um sich zu registrieren"}. +{"You need a client that supports x:data to register the nickname","Sie benötigen einen Client der x:data unterstützt, um Ihren Spitznamen zu registrieren"}. +{"You need an x:data capable client to search","Sie benötigen einen Client der x:data unterstützt, um zu suchen"}. +{"Your active privacy list has denied the routing of this stanza.","Ihre aktive Privacy-Liste hat das Routing dieses Stanzas verweigert."}. +{"Your contact offline message queue is full. The message has been discarded.","Die Offline-Nachrichten-Warteschlange Ihres Kontaktes ist voll. Die Nachricht wurde verworfen."}. +{"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","Ihre Abonnement-Anforderung und/oder Nachrichten an ~s wurden blockiert. Um Ihre Abonnement-Anforderungen freizugeben, besuchen Sie ~s"}. +{"Your XMPP account was successfully registered.","Ihr XMPP-Konto wurde erfolgreich registriert."}. +{"Your XMPP account was successfully unregistered.","Ihr XMPP-Konto wurde erfolgreich entfernt."}. +{"You're not allowed to create nodes","Sie dürfen keine Knoten erstellen"}. diff --git a/priv/msgs/de.po b/priv/msgs/de.po deleted file mode 100644 index 7eb0b3b5f..000000000 --- a/priv/msgs/de.po +++ /dev/null @@ -1,1883 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: 2.1.0-alpha\n" -"POT-Creation-Date: \n" -"PO-Revision-Date: \n" -"Last-Translator: Nikolaus Polak \n" -"Language-Team: \n" -"Language: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Language: German (deutsch)\n" -"X-Additional-Translator: Florian Zumbiehl\n" -"X-Additional-Translator: Cord Beermann\n" -"X-Additional-Translator: Marvin Preuss\n" -"X-Additional-Translator: Patrick Dreker\n" -"X-Additional-Translator: Torsten Werner\n" -"X-Additional-Translator: Marina Hahn\n" - -#: ejabberd_c2s.erl:424 ejabberd_c2s.erl:727 -msgid "Use of STARTTLS required" -msgstr "Verwendung von STARTTLS erforderlich" - -#: ejabberd_c2s.erl:503 -msgid "No resource provided" -msgstr "Keine Ressource angegeben" - -#: ejabberd_c2s.erl:1197 -msgid "Replaced by new connection" -msgstr "Durch neue Verbindung ersetzt" - -#: ejabberd_c2s.erl:1885 -msgid "Your active privacy list has denied the routing of this stanza." -msgstr "" -"Ihre aktive Privacy Liste hat die Weiterleitung des Stanzas unterbunden." - -#: ejabberd_captcha.erl:96 ejabberd_captcha.erl:152 ejabberd_captcha.erl:178 -msgid "Enter the text you see" -msgstr "Geben sie den Text den sie sehen ein" - -#: ejabberd_captcha.erl:101 -msgid "Your messages to ~s are being blocked. To unblock them, visit ~s" -msgstr "" -"Ihre Nachrichten an ~s werden blockiert. Um dies zu ändern, besuchen sie ~s" - -#: ejabberd_captcha.erl:134 -msgid "If you don't see the CAPTCHA image here, visit the web page." -msgstr "" -"Wenn sie das CAPTCHA Bild nicht sehen, besuchen sie bitte die Webseite." - -#: ejabberd_captcha.erl:146 -msgid "CAPTCHA web page" -msgstr "CAPTCHA Webseite" - -#: ejabberd_captcha.erl:307 -msgid "The CAPTCHA is valid." -msgstr "Die Verifizierung ist gültig." - -#: mod_adhoc.erl:95 mod_adhoc.erl:125 mod_adhoc.erl:143 mod_adhoc.erl:161 -msgid "Commands" -msgstr "Befehle" - -#: mod_adhoc.erl:149 mod_adhoc.erl:243 -msgid "Ping" -msgstr "Ping" - -#: mod_adhoc.erl:260 -msgid "Pong" -msgstr "Pong" - -#: mod_announce.erl:507 mod_announce_odbc.erl:499 -msgid "Really delete message of the day?" -msgstr "Die Nachricht des Tages wirklich löschen?" - -#: mod_announce.erl:515 mod_announce_odbc.erl:507 mod_configure.erl:1083 -#: mod_configure.erl:1128 -msgid "Subject" -msgstr "Betreff" - -#: mod_announce.erl:520 mod_announce_odbc.erl:512 mod_configure.erl:1088 -#: mod_configure.erl:1133 -msgid "Message body" -msgstr "Nachrichtentext" - -#: mod_announce.erl:600 mod_announce_odbc.erl:592 -msgid "No body provided for announce message" -msgstr "Kein Text für die Ankündigung angegeben" - -#: mod_announce.erl:635 mod_announce_odbc.erl:627 -msgid "Announcements" -msgstr "Ankündigungen" - -#: mod_announce.erl:637 mod_announce_odbc.erl:629 -msgid "Send announcement to all users" -msgstr "Sende Ankündigung an alle Benutzer" - -#: mod_announce.erl:639 mod_announce_odbc.erl:631 -msgid "Send announcement to all users on all hosts" -msgstr "Sende Ankündigung an alle Benutzer auf allen Hosts" - -#: mod_announce.erl:641 mod_announce_odbc.erl:633 -msgid "Send announcement to all online users" -msgstr "Sende Ankündigung an alle angemeldeten Benutzer" - -#: mod_announce.erl:643 mod_announce_odbc.erl:635 mod_configure.erl:1078 -#: mod_configure.erl:1123 -msgid "Send announcement to all online users on all hosts" -msgstr "Sende Ankündigung an alle angemeldeten Benutzer auf allen Hosts" - -#: mod_announce.erl:645 mod_announce_odbc.erl:637 -msgid "Set message of the day and send to online users" -msgstr "Setze Nachricht des Tages und sende sie an alle angemeldeten Benutzer" - -#: mod_announce.erl:647 mod_announce_odbc.erl:639 -msgid "Set message of the day on all hosts and send to online users" -msgstr "" -"Setze Nachricht des Tages auf allen Hosts und sende sie an alle angemeldeten " -"Benutzer" - -#: mod_announce.erl:649 mod_announce_odbc.erl:641 -msgid "Update message of the day (don't send)" -msgstr "Aktualisiere Nachricht des Tages (nicht senden)" - -#: mod_announce.erl:651 mod_announce_odbc.erl:643 -msgid "Update message of the day on all hosts (don't send)" -msgstr "Aktualisiere Nachricht des Tages auf allen Hosts (nicht senden)" - -#: mod_announce.erl:653 mod_announce_odbc.erl:645 -msgid "Delete message of the day" -msgstr "Lösche Nachricht des Tages" - -#: mod_announce.erl:655 mod_announce_odbc.erl:647 -msgid "Delete message of the day on all hosts" -msgstr "Lösche Nachricht des Tages auf allen Hosts" - -#: mod_configure.erl:114 mod_configure.erl:274 mod_configure.erl:296 -#: mod_configure.erl:498 -msgid "Configuration" -msgstr "Konfiguration" - -#: mod_configure.erl:125 mod_configure.erl:576 web/ejabberd_web_admin.erl:1936 -msgid "Database" -msgstr "Datenbank" - -#: mod_configure.erl:127 mod_configure.erl:595 -msgid "Start Modules" -msgstr "Module starten" - -#: mod_configure.erl:129 mod_configure.erl:596 -msgid "Stop Modules" -msgstr "Module stoppen" - -#: mod_configure.erl:131 mod_configure.erl:604 web/ejabberd_web_admin.erl:1937 -msgid "Backup" -msgstr "Datensicherung" - -#: mod_configure.erl:133 mod_configure.erl:605 -msgid "Restore" -msgstr "Wiederherstellung" - -#: mod_configure.erl:135 mod_configure.erl:606 -msgid "Dump to Text File" -msgstr "Ausgabe in Textdatei" - -#: mod_configure.erl:137 mod_configure.erl:615 -msgid "Import File" -msgstr "Datei importieren" - -#: mod_configure.erl:139 mod_configure.erl:616 -msgid "Import Directory" -msgstr "Verzeichnis importieren" - -#: mod_configure.erl:141 mod_configure.erl:581 mod_configure.erl:1057 -msgid "Restart Service" -msgstr "Dienst neustarten" - -#: mod_configure.erl:143 mod_configure.erl:582 mod_configure.erl:1102 -msgid "Shut Down Service" -msgstr "Dienst herunterfahren" - -#: mod_configure.erl:145 mod_configure.erl:518 mod_configure.erl:1197 -#: web/ejabberd_web_admin.erl:1527 -msgid "Add User" -msgstr "Benutzer hinzufügen" - -#: mod_configure.erl:147 mod_configure.erl:519 mod_configure.erl:1219 -msgid "Delete User" -msgstr "Benutzer löschen" - -#: mod_configure.erl:149 mod_configure.erl:520 mod_configure.erl:1231 -msgid "End User Session" -msgstr "Benutzer-Sitzung beenden" - -#: mod_configure.erl:151 mod_configure.erl:521 mod_configure.erl:1243 -#: mod_configure.erl:1255 -msgid "Get User Password" -msgstr "Benutzer-Passwort abrufen" - -#: mod_configure.erl:153 mod_configure.erl:522 -msgid "Change User Password" -msgstr "Benutzer-Passwort ändern" - -#: mod_configure.erl:155 mod_configure.erl:523 mod_configure.erl:1272 -msgid "Get User Last Login Time" -msgstr "letzte Anmeldezeit abrufen" - -#: mod_configure.erl:157 mod_configure.erl:524 mod_configure.erl:1284 -msgid "Get User Statistics" -msgstr "Benutzer-Statistiken abrufen" - -#: mod_configure.erl:159 mod_configure.erl:525 -msgid "Get Number of Registered Users" -msgstr "Anzahl der registrierten Benutzer abrufen" - -#: mod_configure.erl:161 mod_configure.erl:526 -msgid "Get Number of Online Users" -msgstr "Anzahl der angemeldeten Benutzer abrufen" - -#: mod_configure.erl:163 mod_configure.erl:509 web/ejabberd_web_admin.erl:831 -#: web/ejabberd_web_admin.erl:872 -msgid "Access Control Lists" -msgstr "Zugangskontroll-Listen (ACL)" - -#: mod_configure.erl:165 mod_configure.erl:510 web/ejabberd_web_admin.erl:940 -#: web/ejabberd_web_admin.erl:976 -msgid "Access Rules" -msgstr "Zugangsregeln" - -#: mod_configure.erl:297 mod_configure.erl:499 -msgid "User Management" -msgstr "Benutzerverwaltung" - -#: mod_configure.erl:500 web/ejabberd_web_admin.erl:1058 -#: web/ejabberd_web_admin.erl:1462 -msgid "Online Users" -msgstr "Angemeldete Benutzer" - -#: mod_configure.erl:501 -msgid "All Users" -msgstr "Alle Benutzer" - -#: mod_configure.erl:502 -msgid "Outgoing s2s Connections" -msgstr "Ausgehende s2s-Verbindungen" - -#: mod_configure.erl:503 web/ejabberd_web_admin.erl:1907 -msgid "Running Nodes" -msgstr "Aktive Knoten" - -#: mod_configure.erl:504 web/ejabberd_web_admin.erl:1909 -msgid "Stopped Nodes" -msgstr "Inaktive Knoten" - -#: mod_configure.erl:577 mod_configure.erl:587 web/ejabberd_web_admin.erl:1953 -msgid "Modules" -msgstr "Module" - -#: mod_configure.erl:578 -msgid "Backup Management" -msgstr "Datensicherungsverwaltung" - -#: mod_configure.erl:579 -msgid "Import Users From jabberd14 Spool Files" -msgstr "Importiere Benutzer aus jabberd14-Spool-Dateien" - -#: mod_configure.erl:699 -msgid "To ~s" -msgstr "An ~s" - -#: mod_configure.erl:717 -msgid "From ~s" -msgstr "Von ~s" - -#: mod_configure.erl:913 -msgid "Database Tables Configuration at " -msgstr "Datenbanktabellen-Konfiguration auf " - -#: mod_configure.erl:918 -msgid "Choose storage type of tables" -msgstr "Wähle Speichertyp der Tabellen" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Disc only copy" -msgstr "Nur auf Festplatte" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM and disc copy" -msgstr "RAM und Festplatte" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM copy" -msgstr "Nur RAM" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Remote copy" -msgstr "Fernkopie" - -#: mod_configure.erl:950 -msgid "Stop Modules at " -msgstr "Stoppe Module auf " - -#: mod_configure.erl:954 -msgid "Choose modules to stop" -msgstr "Wähle zu stoppende Module" - -#: mod_configure.erl:969 -msgid "Start Modules at " -msgstr "Starte Module auf " - -#: mod_configure.erl:973 -msgid "Enter list of {Module, [Options]}" -msgstr "Geben sie eine Liste bestehend aus {Modul, [Optionen]} ein" - -#: mod_configure.erl:974 -msgid "List of modules to start" -msgstr "Liste der zu startenden Module" - -#: mod_configure.erl:983 -msgid "Backup to File at " -msgstr "Datensicherung in die Datei " - -#: mod_configure.erl:987 mod_configure.erl:1001 -msgid "Enter path to backup file" -msgstr "Geben sie den Pfad zur Datensicherung ein" - -#: mod_configure.erl:988 mod_configure.erl:1002 mod_configure.erl:1016 -#: mod_configure.erl:1030 -msgid "Path to File" -msgstr "Pfad zur Datei" - -#: mod_configure.erl:997 -msgid "Restore Backup from File at " -msgstr "Datenwiederherstellung aus der Datei " - -#: mod_configure.erl:1011 -msgid "Dump Backup to Text File at " -msgstr "Ausgabe der Sicherung in diese Textdatei " - -#: mod_configure.erl:1015 -msgid "Enter path to text file" -msgstr "Geben sie den Pfad zur Textdatei ein" - -#: mod_configure.erl:1025 -msgid "Import User from File at " -msgstr "Benutzer aus dieser Datei importieren " - -#: mod_configure.erl:1029 -msgid "Enter path to jabberd14 spool file" -msgstr "Geben Sie den Pfad zur jabberd14-Spool-Datei ein" - -#: mod_configure.erl:1039 -msgid "Import Users from Dir at " -msgstr "Benutzer importieren aus dem Verzeichnis " - -#: mod_configure.erl:1043 -msgid "Enter path to jabberd14 spool dir" -msgstr "Geben Sie den Pfad zum jabberd14-Spool-Verzeichnis ein" - -#: mod_configure.erl:1044 -msgid "Path to Dir" -msgstr "Pfad zum Verzeichnis" - -#: mod_configure.erl:1060 mod_configure.erl:1105 -msgid "Time delay" -msgstr "Zeitverzögerung" - -#: mod_configure.erl:1143 -msgid "Access Control List Configuration" -msgstr "Konfiguration der Zugangskontrolllisten" - -#: mod_configure.erl:1147 -msgid "Access control lists" -msgstr "Zugangskontroll-Listen (ACL)" - -#: mod_configure.erl:1171 -msgid "Access Configuration" -msgstr "Zugangskonfiguration" - -#: mod_configure.erl:1175 -msgid "Access rules" -msgstr "Zugangsregeln" - -#: mod_configure.erl:1200 mod_configure.erl:1222 mod_configure.erl:1234 -#: mod_configure.erl:1246 mod_configure.erl:1258 mod_configure.erl:1275 -#: mod_configure.erl:1287 mod_configure.erl:1650 mod_configure.erl:1700 -#: mod_configure.erl:1721 mod_roster.erl:943 mod_roster_odbc.erl:1060 -#: mod_vcard.erl:472 mod_vcard_ldap.erl:554 mod_vcard_odbc.erl:448 -msgid "Jabber ID" -msgstr "Jabber ID" - -#: mod_configure.erl:1205 mod_configure.erl:1263 mod_configure.erl:1651 -#: mod_configure.erl:1863 mod_muc/mod_muc_room.erl:3224 mod_register.erl:235 -#: web/ejabberd_web_admin.erl:1520 -msgid "Password" -msgstr "Passwort" - -#: mod_configure.erl:1210 -msgid "Password Verification" -msgstr "Passwort bestätigen" - -#: mod_configure.erl:1301 -msgid "Number of registered users" -msgstr "Anzahl der registrierten Benutzer" - -#: mod_configure.erl:1315 -msgid "Number of online users" -msgstr "Anzahl der angemeldeten Benutzer" - -#: mod_configure.erl:1682 web/ejabberd_web_admin.erl:1588 -#: web/ejabberd_web_admin.erl:1743 -msgid "Never" -msgstr "Nie" - -#: mod_configure.erl:1696 web/ejabberd_web_admin.erl:1601 -#: web/ejabberd_web_admin.erl:1756 -msgid "Online" -msgstr "Angemeldet" - -#: mod_configure.erl:1701 -msgid "Last login" -msgstr "Letzte Anmeldung" - -#: mod_configure.erl:1722 -msgid "Roster size" -msgstr "Kontaktlistengröße" - -#: mod_configure.erl:1723 -msgid "IP addresses" -msgstr "IP Adressen" - -#: mod_configure.erl:1724 -msgid "Resources" -msgstr "Ressourcen" - -#: mod_configure.erl:1850 -msgid "Administration of " -msgstr "Administration von " - -#: mod_configure.erl:1853 -msgid "Action on user" -msgstr "Aktion auf Benutzer" - -#: mod_configure.erl:1857 -msgid "Edit Properties" -msgstr "Einstellungen ändern" - -#: mod_configure.erl:1860 web/ejabberd_web_admin.erl:1769 -msgid "Remove User" -msgstr "Benutzer löschen" - -#: mod_irc/mod_irc.erl:201 mod_irc/mod_irc_odbc.erl:196 -#: mod_muc/mod_muc.erl:336 mod_muc/mod_muc_odbc.erl:342 -msgid "Access denied by service policy" -msgstr "Zugang aufgrund der Dienstrichtlinien verweigert" - -#: mod_irc/mod_irc.erl:401 mod_irc/mod_irc_odbc.erl:398 -msgid "IRC Transport" -msgstr "IRC Transport" - -#: mod_irc/mod_irc.erl:428 mod_irc/mod_irc_odbc.erl:425 -msgid "ejabberd IRC module" -msgstr "ejabberd IRC-Modul" - -#: mod_irc/mod_irc.erl:559 mod_irc/mod_irc_odbc.erl:568 -msgid "You need an x:data capable client to configure mod_irc settings" -msgstr "" -"Sie benötigen einen Client, der x:data unterstützt, um die mod_irc-" -"Einstellungen zu konfigurieren" - -#: mod_irc/mod_irc.erl:566 mod_irc/mod_irc_odbc.erl:575 -msgid "Registration in mod_irc for " -msgstr "Registrierung in mod_irc für " - -#: mod_irc/mod_irc.erl:571 mod_irc/mod_irc_odbc.erl:580 -msgid "" -"Enter username, encodings, ports and passwords you wish to use for " -"connecting to IRC servers" -msgstr "" -"Geben Sie Benutzernamen und Zeichenkodierung für die Verbindung zum IRC-" -"Server an" - -#: mod_irc/mod_irc.erl:576 mod_irc/mod_irc_odbc.erl:585 -msgid "IRC Username" -msgstr "IRC-Benutzername" - -#: mod_irc/mod_irc.erl:586 mod_irc/mod_irc_odbc.erl:595 -msgid "" -"If you want to specify different ports, passwords, encodings for IRC " -"servers, fill this list with values in format '{\"irc server\", \"encoding" -"\", port, \"password\"}'. By default this service use \"~s\" encoding, port " -"~p, empty password." -msgstr "" -"Wenn sie verschiedene Ports, Passwörter und Kodierungen für IRC Server " -"angeben wollen, erstellen sie die Liste mit folgendem Format '{\"IRC Server" -"\", \"Kodierung\", Port, \"Passwort\"}'. Standardmäßig benutzt dieser " -"Dienst die \"~s\" Kodierung, den Port ~p und kein Passwort." - -#: mod_irc/mod_irc.erl:598 mod_irc/mod_irc_odbc.erl:607 -msgid "" -"Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef." -"net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]." -msgstr "" -"Beispiel: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta." -"fef.net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]." - -#: mod_irc/mod_irc.erl:603 mod_irc/mod_irc_odbc.erl:612 -msgid "Connections parameters" -msgstr "Verbindungsparameter" - -#: mod_irc/mod_irc.erl:728 mod_irc/mod_irc_odbc.erl:757 -msgid "Join IRC channel" -msgstr "IRC Channel beitreten" - -#: mod_irc/mod_irc.erl:732 mod_irc/mod_irc_odbc.erl:761 -msgid "IRC channel (don't put the first #)" -msgstr "IRC Channel (ohne dem ersten #)" - -#: mod_irc/mod_irc.erl:737 mod_irc/mod_irc_odbc.erl:766 -msgid "IRC server" -msgstr "IRC Server" - -#: mod_irc/mod_irc.erl:770 mod_irc/mod_irc.erl:774 -#: mod_irc/mod_irc_odbc.erl:799 mod_irc/mod_irc_odbc.erl:803 -msgid "Join the IRC channel here." -msgstr "Hier den IRC Channel beitreten." - -#: mod_irc/mod_irc.erl:778 mod_irc/mod_irc_odbc.erl:807 -msgid "Join the IRC channel in this Jabber ID: ~s" -msgstr "Den IRC Channel mit dieser Jabber ID beitreten: ~s" - -#: mod_irc/mod_irc.erl:863 mod_irc/mod_irc_odbc.erl:904 -msgid "IRC settings" -msgstr "IRC Einstellungen" - -#: mod_irc/mod_irc.erl:868 mod_irc/mod_irc_odbc.erl:909 -msgid "" -"Enter username and encodings you wish to use for connecting to IRC servers. " -"Press 'Next' to get more fields to fill in. Press 'Complete' to save " -"settings." -msgstr "" -"Geben sie Benutzernamen und Kodierung für Verbindungen zu IRC Servern an. " -"Drücken sie 'Mehr' um leere Felder hinzuzufügen. Drücken sie 'Beenden' um " -"die Einstellungen zu speichern." - -#: mod_irc/mod_irc.erl:874 mod_irc/mod_irc_odbc.erl:915 -msgid "IRC username" -msgstr "IRC Benutzername" - -#: mod_irc/mod_irc.erl:923 mod_irc/mod_irc_odbc.erl:964 -msgid "Password ~b" -msgstr "Passwort ~b" - -#: mod_irc/mod_irc.erl:928 mod_irc/mod_irc_odbc.erl:969 -msgid "Port ~b" -msgstr "Port ~b" - -#: mod_irc/mod_irc.erl:933 mod_irc/mod_irc_odbc.erl:974 -msgid "Encoding for server ~b" -msgstr "Kodierung für Server ~b" - -#: mod_irc/mod_irc.erl:942 mod_irc/mod_irc_odbc.erl:983 -msgid "Server ~b" -msgstr "Server ~b" - -#: mod_muc/mod_muc.erl:449 mod_muc/mod_muc_odbc.erl:456 -msgid "Only service administrators are allowed to send service messages" -msgstr "" -"Nur Service-Administratoren sind berechtigt, Servicenachrichten zu versenden" - -#: mod_muc/mod_muc.erl:493 mod_muc/mod_muc_odbc.erl:500 -msgid "Room creation is denied by service policy" -msgstr "Anlegen des Raumes aufgrund der Dienstrichtlinien verweigert" - -#: mod_muc/mod_muc.erl:500 mod_muc/mod_muc_odbc.erl:507 -msgid "Conference room does not exist" -msgstr "Konferenzraum existiert nicht" - -#: mod_muc/mod_muc.erl:582 mod_muc/mod_muc_odbc.erl:595 -msgid "Chatrooms" -msgstr "Chaträume" - -#: mod_muc/mod_muc.erl:712 mod_muc/mod_muc_odbc.erl:729 -msgid "You need a client that supports x:data to register the nickname" -msgstr "" -"Sie benötigen einen Client, der x:data unterstützt, um Ihren Benutzernamen " -"zu registrieren" - -#: mod_muc/mod_muc.erl:718 mod_muc/mod_muc_odbc.erl:735 -msgid "Nickname Registration at " -msgstr "Registrieren des Benutzernames auf" - -#: mod_muc/mod_muc.erl:722 mod_muc/mod_muc_odbc.erl:739 -msgid "Enter nickname you want to register" -msgstr "Geben sie den zu registrierenden Benutzernamen ein" - -#: mod_muc/mod_muc.erl:723 mod_muc/mod_muc_odbc.erl:740 -#: mod_muc/mod_muc_room.erl:3810 mod_roster.erl:944 mod_roster_odbc.erl:1061 -#: mod_vcard.erl:364 mod_vcard.erl:477 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:453 -msgid "Nickname" -msgstr "Benutzername" - -#: mod_muc/mod_muc.erl:762 mod_muc/mod_muc_odbc.erl:784 -#: mod_muc/mod_muc_room.erl:1064 mod_muc/mod_muc_room.erl:1787 -msgid "That nickname is registered by another person" -msgstr "Dieser Benutzername wurde bereits von jemand anderem registriert" - -#: mod_muc/mod_muc.erl:788 mod_muc/mod_muc_odbc.erl:811 -msgid "You must fill in field \"Nickname\" in the form" -msgstr "Sie müssen das Feld \"Benutzername\" ausfüllen" - -#: mod_muc/mod_muc.erl:808 mod_muc/mod_muc_odbc.erl:831 -msgid "ejabberd MUC module" -msgstr "ejabberd MUC-Modul" - -#: mod_muc/mod_muc_log.erl:374 mod_muc/mod_muc_log.erl:381 -msgid "Chatroom configuration modified" -msgstr "Chatraum-Konfiguration geändert" - -#: mod_muc/mod_muc_log.erl:384 -msgid "joins the room" -msgstr "betretet den Raum" - -#: mod_muc/mod_muc_log.erl:387 mod_muc/mod_muc_log.erl:390 -msgid "leaves the room" -msgstr "verlässt den Raum" - -#: mod_muc/mod_muc_log.erl:393 mod_muc/mod_muc_log.erl:396 -msgid "has been banned" -msgstr "wurde gebannt" - -#: mod_muc/mod_muc_log.erl:399 mod_muc/mod_muc_log.erl:402 -msgid "has been kicked" -msgstr "wurde gekickt" - -#: mod_muc/mod_muc_log.erl:405 -msgid "has been kicked because of an affiliation change" -msgstr "wurde wegen Änderung des Mitgliederstatus gekickt" - -#: mod_muc/mod_muc_log.erl:408 -msgid "has been kicked because the room has been changed to members-only" -msgstr "wurde gekickt weil der Raum auf Nur-Mitglieder umgestellt wurde" - -#: mod_muc/mod_muc_log.erl:411 -msgid "has been kicked because of a system shutdown" -msgstr "wurde wegen Systemabschaltung gekickt" - -#: mod_muc/mod_muc_log.erl:414 -msgid "is now known as" -msgstr "ist nun bekannt als" - -#: mod_muc/mod_muc_log.erl:417 mod_muc/mod_muc_log.erl:688 -#: mod_muc/mod_muc_room.erl:2393 -msgid " has set the subject to: " -msgstr " hat das Thema geändert auf: " - -#: mod_muc/mod_muc_log.erl:452 -msgid "Chatroom is created" -msgstr "Chatraum wurde erstellt" - -#: mod_muc/mod_muc_log.erl:453 -msgid "Chatroom is destroyed" -msgstr "Chatraum wurde entfernt" - -#: mod_muc/mod_muc_log.erl:454 -msgid "Chatroom is started" -msgstr "Chatraum wurde gestartet" - -#: mod_muc/mod_muc_log.erl:455 -msgid "Chatroom is stopped" -msgstr "Chatraum wurde beendet" - -#: mod_muc/mod_muc_log.erl:459 -msgid "Monday" -msgstr "Montag" - -#: mod_muc/mod_muc_log.erl:460 -msgid "Tuesday" -msgstr "Dienstag" - -#: mod_muc/mod_muc_log.erl:461 -msgid "Wednesday" -msgstr "Mittwoch" - -#: mod_muc/mod_muc_log.erl:462 -msgid "Thursday" -msgstr "Donnerstag" - -#: mod_muc/mod_muc_log.erl:463 -msgid "Friday" -msgstr "Freitag" - -#: mod_muc/mod_muc_log.erl:464 -msgid "Saturday" -msgstr "Samstag" - -#: mod_muc/mod_muc_log.erl:465 -msgid "Sunday" -msgstr "Sonntag" - -#: mod_muc/mod_muc_log.erl:469 -msgid "January" -msgstr "Januar" - -#: mod_muc/mod_muc_log.erl:470 -msgid "February" -msgstr "Februar" - -#: mod_muc/mod_muc_log.erl:471 -msgid "March" -msgstr "März" - -#: mod_muc/mod_muc_log.erl:472 -msgid "April" -msgstr "April" - -#: mod_muc/mod_muc_log.erl:473 -msgid "May" -msgstr "Mai" - -#: mod_muc/mod_muc_log.erl:474 -msgid "June" -msgstr "Juni" - -#: mod_muc/mod_muc_log.erl:475 -msgid "July" -msgstr "Juli" - -#: mod_muc/mod_muc_log.erl:476 -msgid "August" -msgstr "August" - -#: mod_muc/mod_muc_log.erl:477 -msgid "September" -msgstr "September" - -#: mod_muc/mod_muc_log.erl:478 -msgid "October" -msgstr "Oktober" - -#: mod_muc/mod_muc_log.erl:479 -msgid "November" -msgstr "November" - -#: mod_muc/mod_muc_log.erl:480 -msgid "December" -msgstr "Dezember" - -#: mod_muc/mod_muc_log.erl:750 -msgid "Room Configuration" -msgstr "Raum-Konfiguration" - -#: mod_muc/mod_muc_log.erl:759 -msgid "Room Occupants" -msgstr "Teilnehmer in diesem Raum" - -#: mod_muc/mod_muc_room.erl:174 -msgid "Traffic rate limit is exceeded" -msgstr "Datenratenlimit wurde überschritten" - -#: mod_muc/mod_muc_room.erl:246 -msgid "" -"This participant is kicked from the room because he sent an error message" -msgstr "" -"Dieser Teilnehmer wurde aus dem Raum geworfen, da er eine fehlerhafte " -"Nachricht gesendet hat" - -#: mod_muc/mod_muc_room.erl:255 -msgid "It is not allowed to send private messages to the conference" -msgstr "Es ist nicht erlaubt private Nachrichten an den Raum zu schicken" - -#: mod_muc/mod_muc_room.erl:332 -msgid "Please, wait for a while before sending new voice request" -msgstr "" -"Bitte warten sie ein wenig, bevor sie eine weitere Anfrage für Sprachrechte " -"senden" - -#: mod_muc/mod_muc_room.erl:347 -msgid "Voice requests are disabled in this conference" -msgstr "Anfragen für Sprachrechte sind in diesem Raum deaktiviert" - -#: mod_muc/mod_muc_room.erl:364 -msgid "Failed to extract JID from your voice request approval" -msgstr "" -"Fehler beim Auslesen der JID aus der Anfragenbestätigung für Sprachrechte" - -#: mod_muc/mod_muc_room.erl:393 -msgid "Only moderators can approve voice requests" -msgstr "Nur Moderatoren können Anfragen für Sprachrechte bestätigen" - -#: mod_muc/mod_muc_room.erl:408 -msgid "Improper message type" -msgstr "Unzulässiger Nachrichtentyp" - -#: mod_muc/mod_muc_room.erl:518 -msgid "" -"This participant is kicked from the room because he sent an error message to " -"another participant" -msgstr "" -"Dieser Teilnehmer wurde aus dem Raum geworfen, da er eine fehlerhafte " -"Nachricht an einen anderen Teilnehmer gesendet hat" - -#: mod_muc/mod_muc_room.erl:531 -msgid "It is not allowed to send private messages of type \"groupchat\"" -msgstr "" -"Es ist nicht erlaubt private Nachrichten des Typs \"Gruppenchat\" zu senden" - -#: mod_muc/mod_muc_room.erl:543 mod_muc/mod_muc_room.erl:611 -msgid "Recipient is not in the conference room" -msgstr "Der Empfänger ist nicht im Raum" - -#: mod_muc/mod_muc_room.erl:564 mod_muc/mod_muc_room.erl:585 -msgid "It is not allowed to send private messages" -msgstr "Es ist nicht erlaubt private Nachrichten zu senden" - -#: mod_muc/mod_muc_room.erl:576 mod_muc/mod_muc_room.erl:956 -#: mod_muc/mod_muc_room.erl:4040 -msgid "Only occupants are allowed to send messages to the conference" -msgstr "Nur Teilnehmer dürfen Nachrichten an den Raum schicken" - -#: mod_muc/mod_muc_room.erl:634 -msgid "Only occupants are allowed to send queries to the conference" -msgstr "Nur Teilnehmer sind berechtigt Anfragen an die Konferenz zu senden" - -#: mod_muc/mod_muc_room.erl:646 -msgid "Queries to the conference members are not allowed in this room" -msgstr "Anfragen an die Teilnehmer sind in diesem Raum nicht erlaubt" - -#: mod_muc/mod_muc_room.erl:932 -msgid "" -"Only moderators and participants are allowed to change the subject in this " -"room" -msgstr "Nur Moderatoren und Mitglieder dürfen das Thema in diesem Raum ändern" - -#: mod_muc/mod_muc_room.erl:937 -msgid "Only moderators are allowed to change the subject in this room" -msgstr "Nur Moderatoren dürfen das Thema in diesem Raum ändern" - -#: mod_muc/mod_muc_room.erl:947 -msgid "Visitors are not allowed to send messages to all occupants" -msgstr "Besucher dürfen nicht an alle Teilnehmer Nachrichten verschicken" - -#: mod_muc/mod_muc_room.erl:1021 -msgid "" -"This participant is kicked from the room because he sent an error presence" -msgstr "" -"Dieser Teilnehmer wurde aus dem Raum gekickt, da er einen fehlerhaften " -"Status gesendet hat" - -#: mod_muc/mod_muc_room.erl:1040 -msgid "Visitors are not allowed to change their nicknames in this room" -msgstr "Besucher dürfen in diesem Raum ihren Benutzernamen nicht ändern" - -#: mod_muc/mod_muc_room.erl:1053 mod_muc/mod_muc_room.erl:1779 -msgid "That nickname is already in use by another occupant" -msgstr "Dieser Benutzername wird bereits von einem Teilnehmer genutzt" - -#: mod_muc/mod_muc_room.erl:1768 -msgid "You have been banned from this room" -msgstr "Sie wurden aus diesem Raum verbannt" - -#: mod_muc/mod_muc_room.erl:1771 -msgid "Membership is required to enter this room" -msgstr "Um diesen Raum zu betreten müssen sie Mitglied sein" - -#: mod_muc/mod_muc_room.erl:1807 -msgid "This room is not anonymous" -msgstr "Dieser Raum ist nicht anonym" - -#: mod_muc/mod_muc_room.erl:1833 -msgid "A password is required to enter this room" -msgstr "Sie brauchen ein Passwort um diesen Raum zu betreten" - -#: mod_muc/mod_muc_room.erl:1855 mod_register.erl:246 -msgid "Too many CAPTCHA requests" -msgstr "Zu viele CAPTCHA Anfragen" - -#: mod_muc/mod_muc_room.erl:1864 mod_register.erl:251 -msgid "Unable to generate a CAPTCHA" -msgstr "Konnte CAPTCHA nicht erstellen" - -#: mod_muc/mod_muc_room.erl:1874 -msgid "Incorrect password" -msgstr "Falsches Passwort" - -#: mod_muc/mod_muc_room.erl:2448 -msgid "Administrator privileges required" -msgstr "Administratorenrechte benötigt" - -#: mod_muc/mod_muc_room.erl:2463 -msgid "Moderator privileges required" -msgstr "Moderatorrechte benötigt" - -#: mod_muc/mod_muc_room.erl:2619 -msgid "Jabber ID ~s is invalid" -msgstr "Die Jabber-ID ~s ist ungültig" - -#: mod_muc/mod_muc_room.erl:2633 -msgid "Nickname ~s does not exist in the room" -msgstr "Der Benutzername ~s existiert im Raum nicht" - -#: mod_muc/mod_muc_room.erl:2659 mod_muc/mod_muc_room.erl:3049 -msgid "Invalid affiliation: ~s" -msgstr "Ungültige Mitgliedschaft: ~s" - -#: mod_muc/mod_muc_room.erl:2713 -msgid "Invalid role: ~s" -msgstr "Ungültige Rolle: ~s" - -#: mod_muc/mod_muc_room.erl:3026 mod_muc/mod_muc_room.erl:3062 -msgid "Owner privileges required" -msgstr "Besitzerrechte benötigt" - -#: mod_muc/mod_muc_room.erl:3195 -msgid "Configuration of room ~s" -msgstr "Konfiguration für Raum ~s" - -#: mod_muc/mod_muc_room.erl:3200 -msgid "Room title" -msgstr "Raumname" - -#: mod_muc/mod_muc_room.erl:3203 mod_muc/mod_muc_room.erl:3692 -msgid "Room description" -msgstr "Raum Beschreibung" - -#: mod_muc/mod_muc_room.erl:3210 -msgid "Make room persistent" -msgstr "Raum persistent machen" - -#: mod_muc/mod_muc_room.erl:3215 -msgid "Make room public searchable" -msgstr "Raum öffentlich suchbar machen" - -#: mod_muc/mod_muc_room.erl:3218 -msgid "Make participants list public" -msgstr "Teilnehmerliste öffentlich machen" - -#: mod_muc/mod_muc_room.erl:3221 -msgid "Make room password protected" -msgstr "Raum mit Passwort schützen" - -#: mod_muc/mod_muc_room.erl:3232 -msgid "Maximum Number of Occupants" -msgstr "Maximale Anzahl von Teilnehmern" - -#: mod_muc/mod_muc_room.erl:3239 -msgid "No limit" -msgstr "Keine Begrenzung" - -#: mod_muc/mod_muc_room.erl:3250 -msgid "Present real Jabber IDs to" -msgstr "Echte Jabber-IDs anzeigen für" - -#: mod_muc/mod_muc_room.erl:3258 mod_muc/mod_muc_room.erl:3292 -msgid "moderators only" -msgstr "ausschliesslich Moderatoren" - -#: mod_muc/mod_muc_room.erl:3260 mod_muc/mod_muc_room.erl:3294 -msgid "anyone" -msgstr "jeden" - -#: mod_muc/mod_muc_room.erl:3262 -msgid "Make room members-only" -msgstr "Raum nur für Mitglieder zugänglich machen" - -#: mod_muc/mod_muc_room.erl:3265 -msgid "Make room moderated" -msgstr "Raum moderiert machen" - -#: mod_muc/mod_muc_room.erl:3268 -msgid "Default users as participants" -msgstr "Standardbenutzer als Teilnehmer" - -#: mod_muc/mod_muc_room.erl:3271 -msgid "Allow users to change the subject" -msgstr "Erlaube Benutzern das Thema zu ändern" - -#: mod_muc/mod_muc_room.erl:3274 -msgid "Allow users to send private messages" -msgstr "Erlaube Benutzern private Nachrichten zu senden" - -#: mod_muc/mod_muc_room.erl:3279 -msgid "Allow visitors to send private messages to" -msgstr "Erlaube Besuchern das Senden von privaten Nachrichten an" - -#: mod_muc/mod_muc_room.erl:3290 -msgid "nobody" -msgstr "niemanden" - -#: mod_muc/mod_muc_room.erl:3296 -msgid "Allow users to query other users" -msgstr "Erlaube Benutzern Informationen über andere Benutzer abzufragen" - -#: mod_muc/mod_muc_room.erl:3299 -msgid "Allow users to send invites" -msgstr "Erlaube Benutzern Einladungen zu senden" - -#: mod_muc/mod_muc_room.erl:3302 -msgid "Allow visitors to send status text in presence updates" -msgstr "Erlaube Besuchern einen Text bei Statusänderung zu senden" - -#: mod_muc/mod_muc_room.erl:3305 -msgid "Allow visitors to change nickname" -msgstr "Erlaube Besuchern ihren Spitznamen zu ändern" - -#: mod_muc/mod_muc_room.erl:3308 -msgid "Allow visitors to send voice requests" -msgstr "Anfragen von Sprachrechten für Benutzer erlauben" - -#: mod_muc/mod_muc_room.erl:3311 -msgid "Minimum interval between voice requests (in seconds)" -msgstr "Mindestdauer zwischen Anfragen für Sprachrechte (in Sekunden)" - -#: mod_muc/mod_muc_room.erl:3317 -msgid "Make room CAPTCHA protected" -msgstr "Raum mit Verifizierung (Captcha) versehen" - -#: mod_muc/mod_muc_room.erl:3322 -msgid "Exclude Jabber IDs from CAPTCHA challenge" -msgstr "Von CAPTCHA Überprüfung ausgeschlossene Jabber IDs" - -#: mod_muc/mod_muc_room.erl:3329 -msgid "Enable logging" -msgstr "Protokollierung aktivieren" - -#: mod_muc/mod_muc_room.erl:3337 -msgid "You need an x:data capable client to configure room" -msgstr "" -"Sie benötigen einen Client, der x:data unterstützt, um den Raum zu " -"konfigurieren" - -#: mod_muc/mod_muc_room.erl:3694 -msgid "Number of occupants" -msgstr "Anzahl der Teilnehmer" - -#: mod_muc/mod_muc_room.erl:3750 -msgid "private, " -msgstr "privat, " - -#: mod_muc/mod_muc_room.erl:3799 -msgid "Voice request" -msgstr "Anfrage für Sprachrechte" - -#: mod_muc/mod_muc_room.erl:3803 -msgid "Either approve or decline the voice request." -msgstr "Diese Anfrage für Sprachrechte bestätigen oder ablehnen." - -#: mod_muc/mod_muc_room.erl:3809 -msgid "User JID" -msgstr "Benutzer JID" - -#: mod_muc/mod_muc_room.erl:3811 -msgid "Grant voice to this person?" -msgstr "Sprachrechte dieser Person erteilen?" - -#: mod_muc/mod_muc_room.erl:3960 -msgid "~s invites you to the room ~s" -msgstr "~s lädt Sie in den Raum ~s ein" - -#: mod_muc/mod_muc_room.erl:3969 -msgid "the password is" -msgstr "das Passwort lautet" - -#: mod_offline.erl:510 mod_offline_odbc.erl:352 -msgid "" -"Your contact offline message queue is full. The message has been discarded." -msgstr "" -"Ihre Offline-Nachrichten-Warteschlange ist voll. Die Nachricht wurde " -"verworfen." - -#: mod_offline.erl:560 mod_offline_odbc.erl:408 -msgid "~s's Offline Messages Queue" -msgstr "~s's Offline-Nachrichten-Warteschlange" - -#: mod_offline.erl:563 mod_offline_odbc.erl:411 mod_roster.erl:987 -#: mod_roster_odbc.erl:1104 mod_shared_roster.erl:901 -#: mod_shared_roster.erl:1010 mod_shared_roster_odbc.erl:930 -#: mod_shared_roster_odbc.erl:1039 web/ejabberd_web_admin.erl:833 -#: web/ejabberd_web_admin.erl:874 web/ejabberd_web_admin.erl:942 -#: web/ejabberd_web_admin.erl:978 web/ejabberd_web_admin.erl:1019 -#: web/ejabberd_web_admin.erl:1508 web/ejabberd_web_admin.erl:1760 -#: web/ejabberd_web_admin.erl:1931 web/ejabberd_web_admin.erl:1963 -#: web/ejabberd_web_admin.erl:2031 web/ejabberd_web_admin.erl:2135 -#: web/ejabberd_web_admin.erl:2160 web/ejabberd_web_admin.erl:2248 -msgid "Submitted" -msgstr "Gesendet" - -#: mod_offline.erl:571 -msgid "Time" -msgstr "Zeit" - -#: mod_offline.erl:572 -msgid "From" -msgstr "Von" - -#: mod_offline.erl:573 -msgid "To" -msgstr "An" - -#: mod_offline.erl:574 mod_offline_odbc.erl:419 -msgid "Packet" -msgstr "Paket" - -#: mod_offline.erl:587 mod_offline_odbc.erl:432 mod_shared_roster.erl:908 -#: mod_shared_roster_odbc.erl:937 web/ejabberd_web_admin.erl:882 -#: web/ejabberd_web_admin.erl:986 -msgid "Delete Selected" -msgstr "Markierte löschen" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -msgid "Offline Messages:" -msgstr "Offline-Nachrichten:" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -msgid "Remove All Offline Messages" -msgstr "Alle Offline Nachrichten löschen" - -#: mod_proxy65/mod_proxy65_service.erl:213 -msgid "ejabberd SOCKS5 Bytestreams module" -msgstr "ejabberd SOCKS5-Bytestreams-Modul" - -#: mod_pubsub/mod_pubsub.erl:1120 mod_pubsub/mod_pubsub_odbc.erl:929 -msgid "Publish-Subscribe" -msgstr "Publish-Subscribe" - -#: mod_pubsub/mod_pubsub.erl:1214 mod_pubsub/mod_pubsub_odbc.erl:1025 -msgid "ejabberd Publish-Subscribe module" -msgstr "ejabberd Publish-Subscribe-Modul" - -#: mod_pubsub/mod_pubsub.erl:1497 mod_pubsub/mod_pubsub_odbc.erl:1312 -msgid "PubSub subscriber request" -msgstr "PubSub-Abonnenten-Anfrage" - -#: mod_pubsub/mod_pubsub.erl:1499 mod_pubsub/mod_pubsub_odbc.erl:1314 -msgid "Choose whether to approve this entity's subscription." -msgstr "Wähle Sie, ob dieses Abonnement akzeptiert werden soll." - -#: mod_pubsub/mod_pubsub.erl:1505 mod_pubsub/mod_pubsub_odbc.erl:1320 -msgid "Node ID" -msgstr "Knoten-ID" - -#: mod_pubsub/mod_pubsub.erl:1510 mod_pubsub/mod_pubsub_odbc.erl:1325 -msgid "Subscriber Address" -msgstr "Abonnenten-Adresse" - -#: mod_pubsub/mod_pubsub.erl:1516 mod_pubsub/mod_pubsub_odbc.erl:1331 -msgid "Allow this Jabber ID to subscribe to this pubsub node?" -msgstr "Dieser Jabber-ID das Abonnement dieses pubsub-Knotens erlauben?" - -#: mod_pubsub/mod_pubsub.erl:3474 mod_pubsub/mod_pubsub_odbc.erl:3287 -msgid "Deliver payloads with event notifications" -msgstr "Nachrichten mit Ereignis-Benachrichtigungen zustellen" - -#: mod_pubsub/mod_pubsub.erl:3475 mod_pubsub/mod_pubsub_odbc.erl:3288 -msgid "Deliver event notifications" -msgstr "Ereignisbenachrichtigung zustellen" - -#: mod_pubsub/mod_pubsub.erl:3476 mod_pubsub/mod_pubsub_odbc.erl:3289 -msgid "Notify subscribers when the node configuration changes" -msgstr "Abonnenten benachrichtigen, wenn sich die Knotenkonfiguration ändert" - -#: mod_pubsub/mod_pubsub.erl:3477 mod_pubsub/mod_pubsub_odbc.erl:3290 -msgid "Notify subscribers when the node is deleted" -msgstr "Abonnenten benachrichtigen, wenn der Knoten gelöscht wird" - -#: mod_pubsub/mod_pubsub.erl:3478 mod_pubsub/mod_pubsub_odbc.erl:3291 -msgid "Notify subscribers when items are removed from the node" -msgstr "Abonnenten benachrichtigen, wenn Einträge vom Knoten entfernt werden" - -#: mod_pubsub/mod_pubsub.erl:3479 mod_pubsub/mod_pubsub_odbc.erl:3292 -msgid "Persist items to storage" -msgstr "Einträge dauerhaft speichern" - -#: mod_pubsub/mod_pubsub.erl:3480 mod_pubsub/mod_pubsub_odbc.erl:3293 -msgid "A friendly name for the node" -msgstr "Ein merkbarer Name für den Knoten" - -#: mod_pubsub/mod_pubsub.erl:3481 mod_pubsub/mod_pubsub_odbc.erl:3294 -msgid "Max # of items to persist" -msgstr "Maximale Anzahl dauerhaft zu speichernder Einträge" - -#: mod_pubsub/mod_pubsub.erl:3482 mod_pubsub/mod_pubsub_odbc.erl:3295 -msgid "Whether to allow subscriptions" -msgstr "Ob Abonnements erlaubt sind" - -#: mod_pubsub/mod_pubsub.erl:3483 mod_pubsub/mod_pubsub_odbc.erl:3296 -msgid "Specify the access model" -msgstr "Geben sie das Zugangsmodell an" - -#: mod_pubsub/mod_pubsub.erl:3486 mod_pubsub/mod_pubsub_odbc.erl:3299 -msgid "Roster groups allowed to subscribe" -msgstr "Kontaktlisten-Gruppen die abonnieren dürfen" - -#: mod_pubsub/mod_pubsub.erl:3487 mod_pubsub/mod_pubsub_odbc.erl:3300 -msgid "Specify the publisher model" -msgstr "Geben sie das Publikationsmodell an" - -#: mod_pubsub/mod_pubsub.erl:3489 mod_pubsub/mod_pubsub_odbc.erl:3302 -msgid "Purge all items when the relevant publisher goes offline" -msgstr "" -"Alle Einträge entfernen, wenn der relevante Veröffentlicher offline geht" - -#: mod_pubsub/mod_pubsub.erl:3490 mod_pubsub/mod_pubsub_odbc.erl:3303 -msgid "Specify the event message type" -msgstr "Geben sie den Ereignis-Nachrichtentyp an" - -#: mod_pubsub/mod_pubsub.erl:3492 mod_pubsub/mod_pubsub_odbc.erl:3305 -msgid "Max payload size in bytes" -msgstr "Maximale Nutzlastgrösse in Bytes" - -#: mod_pubsub/mod_pubsub.erl:3493 mod_pubsub/mod_pubsub_odbc.erl:3306 -msgid "When to send the last published item" -msgstr "Wann soll das letzte veröffentlichte Objekt gesendet werden" - -#: mod_pubsub/mod_pubsub.erl:3495 mod_pubsub/mod_pubsub_odbc.erl:3308 -msgid "Only deliver notifications to available users" -msgstr "Benachrichtigungen nur an verfügbare Benutzer schicken" - -#: mod_pubsub/mod_pubsub.erl:3496 mod_pubsub/mod_pubsub_odbc.erl:3309 -msgid "The collections with which a node is affiliated" -msgstr "Sammlungen, mit denen ein Knoten verknüpft ist" - -#: mod_register.erl:193 -msgid "The CAPTCHA verification has failed" -msgstr "Die CAPTCHA Verifizierung schlug fehl" - -#: mod_register.erl:220 -msgid "You need a client that supports x:data and CAPTCHA to register" -msgstr "" -"Sie benötigen einen Client, der x:data und CAPTCHA unterstützt, um Ihren " -"Benutzernamen zu registrieren" - -#: mod_register.erl:226 mod_register.erl:265 -msgid "Choose a username and password to register with this server" -msgstr "Wählen sie zum Registrieren einen Benutzernamen und ein Passwort" - -#: mod_register.erl:230 mod_vcard.erl:364 mod_vcard_odbc.erl:342 -#: web/ejabberd_web_admin.erl:1515 web/ejabberd_web_admin.erl:1572 -msgid "User" -msgstr "Benutzer" - -#: mod_register.erl:316 mod_register.erl:361 -msgid "The password is too weak" -msgstr "Das Passwort ist zu einfach" - -#: mod_register.erl:365 -msgid "Users are not allowed to register accounts so quickly" -msgstr "Benutzer dürfen Konten nicht so schnell registrieren" - -#: mod_roster.erl:938 mod_roster_odbc.erl:1055 web/ejabberd_web_admin.erl:1701 -#: web/ejabberd_web_admin.erl:1886 web/ejabberd_web_admin.erl:1897 -#: web/ejabberd_web_admin.erl:2219 -msgid "None" -msgstr "Keine" - -#: mod_roster.erl:945 mod_roster_odbc.erl:1062 -msgid "Subscription" -msgstr "Abonnement" - -#: mod_roster.erl:946 mod_roster_odbc.erl:1063 -msgid "Pending" -msgstr "anhängig" - -#: mod_roster.erl:947 mod_roster_odbc.erl:1064 -msgid "Groups" -msgstr "Gruppen" - -#: mod_roster.erl:974 mod_roster_odbc.erl:1091 -msgid "Validate" -msgstr "Validieren" - -#: mod_roster.erl:982 mod_roster_odbc.erl:1099 -msgid "Remove" -msgstr "Entfernen" - -#: mod_roster.erl:985 mod_roster_odbc.erl:1102 -msgid "Roster of " -msgstr "Kontaktliste von " - -#: mod_roster.erl:988 mod_roster_odbc.erl:1105 mod_shared_roster.erl:902 -#: mod_shared_roster.erl:1011 mod_shared_roster_odbc.erl:931 -#: mod_shared_roster_odbc.erl:1040 web/ejabberd_web_admin.erl:834 -#: web/ejabberd_web_admin.erl:875 web/ejabberd_web_admin.erl:943 -#: web/ejabberd_web_admin.erl:979 web/ejabberd_web_admin.erl:1020 -#: web/ejabberd_web_admin.erl:1509 web/ejabberd_web_admin.erl:1761 -#: web/ejabberd_web_admin.erl:1932 web/ejabberd_web_admin.erl:2136 -#: web/ejabberd_web_admin.erl:2161 -msgid "Bad format" -msgstr "Ungültiges Format" - -#: mod_roster.erl:995 mod_roster_odbc.erl:1112 -msgid "Add Jabber ID" -msgstr "Jabber-ID hinzufügen" - -#: mod_roster.erl:1094 mod_roster_odbc.erl:1211 -msgid "Roster" -msgstr "Kontaktliste" - -#: mod_shared_roster.erl:857 mod_shared_roster.erl:899 -#: mod_shared_roster.erl:1007 mod_shared_roster_odbc.erl:886 -#: mod_shared_roster_odbc.erl:928 mod_shared_roster_odbc.erl:1036 -msgid "Shared Roster Groups" -msgstr "Gruppen der gemeinsamen Kontaktliste" - -#: mod_shared_roster.erl:895 mod_shared_roster_odbc.erl:924 -#: web/ejabberd_web_admin.erl:1365 web/ejabberd_web_admin.erl:2461 -msgid "Add New" -msgstr "Neue hinzufügen" - -#: mod_shared_roster.erl:978 mod_shared_roster_odbc.erl:1007 -msgid "Name:" -msgstr "Name:" - -#: mod_shared_roster.erl:983 mod_shared_roster_odbc.erl:1012 -msgid "Description:" -msgstr "Beschreibung:" - -#: mod_shared_roster.erl:991 mod_shared_roster_odbc.erl:1020 -msgid "Members:" -msgstr "Mitglieder:" - -#: mod_shared_roster.erl:999 mod_shared_roster_odbc.erl:1028 -msgid "Displayed Groups:" -msgstr "Angezeigte Gruppen:" - -#: mod_shared_roster.erl:1008 mod_shared_roster_odbc.erl:1037 -msgid "Group " -msgstr "Gruppe " - -#: mod_shared_roster.erl:1017 mod_shared_roster_odbc.erl:1046 -#: web/ejabberd_web_admin.erl:840 web/ejabberd_web_admin.erl:884 -#: web/ejabberd_web_admin.erl:949 web/ejabberd_web_admin.erl:1026 -#: web/ejabberd_web_admin.erl:2017 -msgid "Submit" -msgstr "Senden" - -#: mod_vcard.erl:165 mod_vcard_ldap.erl:238 mod_vcard_odbc.erl:129 -msgid "Erlang Jabber Server" -msgstr "Erlang Jabber Server" - -#: mod_vcard.erl:364 mod_vcard.erl:478 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:454 -msgid "Birthday" -msgstr "Geburtsdatum" - -#: mod_vcard.erl:364 mod_vcard.erl:480 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:456 -msgid "City" -msgstr "Stadt" - -#: mod_vcard.erl:364 mod_vcard.erl:479 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:455 -msgid "Country" -msgstr "Land" - -#: mod_vcard.erl:364 mod_vcard.erl:481 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:457 -msgid "Email" -msgstr "E-Mail" - -#: mod_vcard.erl:364 mod_vcard.erl:476 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:452 -msgid "Family Name" -msgstr "Nachname" - -#: mod_vcard.erl:364 mod_vcard_odbc.erl:342 -msgid "" -"Fill in the form to search for any matching Jabber User (Add * to the end of " -"field to match substring)" -msgstr "" -"Füllen Sie die Felder aus, um nach passenden Jabber-Benutzern zu suchen " -"(beenden Sie ein Feld mit *, um auch nach Teilzeichenketten zu suchen)" - -#: mod_vcard.erl:364 mod_vcard.erl:473 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:449 -msgid "Full Name" -msgstr "Vollständiger Name" - -#: mod_vcard.erl:364 mod_vcard.erl:475 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:451 -msgid "Middle Name" -msgstr "Zweiter Vorname" - -#: mod_vcard.erl:364 mod_vcard.erl:474 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:450 web/ejabberd_web_admin.erl:2006 -msgid "Name" -msgstr "Vorname" - -#: mod_vcard.erl:364 mod_vcard.erl:482 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:458 -msgid "Organization Name" -msgstr "Organisation" - -#: mod_vcard.erl:364 mod_vcard.erl:483 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:459 -msgid "Organization Unit" -msgstr "Abteilung" - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "Search users in " -msgstr "Benutzer suchen in " - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "You need an x:data capable client to search" -msgstr "" -"Sie benötigen einen Client, der x:data unterstützt, um die Suche verwenden " -"zu können" - -#: mod_vcard.erl:389 mod_vcard_ldap.erl:487 mod_vcard_odbc.erl:367 -msgid "vCard User Search" -msgstr "vCard-Benutzer-Suche" - -#: mod_vcard.erl:445 mod_vcard_ldap.erl:541 mod_vcard_odbc.erl:421 -msgid "ejabberd vCard module" -msgstr "ejabberd vCard-Modul" - -#: mod_vcard.erl:469 mod_vcard_ldap.erl:551 mod_vcard_odbc.erl:445 -msgid "Search Results for " -msgstr "Suchergebnisse für " - -#: mod_vcard_ldap.erl:462 -msgid "Fill in fields to search for any matching Jabber User" -msgstr "" -"Füllen Sie die Felder aus, um nach passenden Jabber-Benutzern zu suchen" - -#: web/ejabberd_web_admin.erl:193 web/ejabberd_web_admin.erl:203 -#: web/ejabberd_web_admin.erl:219 web/ejabberd_web_admin.erl:229 -msgid "Unauthorized" -msgstr "Nicht berechtigt" - -#: web/ejabberd_web_admin.erl:286 web/ejabberd_web_admin.erl:303 -msgid "ejabberd Web Admin" -msgstr "ejabberd Web-Admin" - -#: web/ejabberd_web_admin.erl:769 web/ejabberd_web_admin.erl:780 -msgid "Administration" -msgstr "Verwaltung" - -#: web/ejabberd_web_admin.erl:878 web/ejabberd_web_admin.erl:982 -msgid "Raw" -msgstr "Unformatiert" - -#: web/ejabberd_web_admin.erl:1017 -msgid "~s access rule configuration" -msgstr "~s Zugangsregel-Konfiguration" - -#: web/ejabberd_web_admin.erl:1035 -msgid "Virtual Hosts" -msgstr "Virtuelle Hosts" - -#: web/ejabberd_web_admin.erl:1043 web/ejabberd_web_admin.erl:1050 -msgid "Users" -msgstr "Benutzer" - -#: web/ejabberd_web_admin.erl:1078 -msgid "Users Last Activity" -msgstr "Letzte Benutzeraktivität" - -#: web/ejabberd_web_admin.erl:1080 -msgid "Period: " -msgstr "Zeitraum: " - -#: web/ejabberd_web_admin.erl:1090 -msgid "Last month" -msgstr "Letzter Monat" - -#: web/ejabberd_web_admin.erl:1091 -msgid "Last year" -msgstr "Letztes Jahr" - -#: web/ejabberd_web_admin.erl:1092 -msgid "All activity" -msgstr "Alle Aktivitäten" - -#: web/ejabberd_web_admin.erl:1094 -msgid "Show Ordinary Table" -msgstr "Normale Tabelle anzeigen" - -#: web/ejabberd_web_admin.erl:1096 -msgid "Show Integral Table" -msgstr "Vollständige Tabelle anzeigen" - -#: web/ejabberd_web_admin.erl:1105 web/ejabberd_web_admin.erl:1939 -msgid "Statistics" -msgstr "Statistik" - -#: web/ejabberd_web_admin.erl:1117 -msgid "Not Found" -msgstr "Nicht gefunden" - -#: web/ejabberd_web_admin.erl:1134 -msgid "Node not found" -msgstr "Knoten nicht gefunden" - -#: web/ejabberd_web_admin.erl:1460 -msgid "Host" -msgstr "Host" - -#: web/ejabberd_web_admin.erl:1461 -msgid "Registered Users" -msgstr "Registrierte Benutzer" - -#: web/ejabberd_web_admin.erl:1573 -msgid "Offline Messages" -msgstr "Offline-Nachrichten" - -#: web/ejabberd_web_admin.erl:1574 web/ejabberd_web_admin.erl:1767 -msgid "Last Activity" -msgstr "Letzte Aktivität" - -#: web/ejabberd_web_admin.erl:1659 web/ejabberd_web_admin.erl:1675 -msgid "Registered Users:" -msgstr "Registrierte Benutzer:" - -#: web/ejabberd_web_admin.erl:1661 web/ejabberd_web_admin.erl:1677 -#: web/ejabberd_web_admin.erl:2192 -msgid "Online Users:" -msgstr "Angemeldete Benutzer:" - -#: web/ejabberd_web_admin.erl:1663 -msgid "Outgoing s2s Connections:" -msgstr "Ausgehende s2s-Verbindungen:" - -#: web/ejabberd_web_admin.erl:1665 -msgid "Outgoing s2s Servers:" -msgstr "Ausgehende s2s-Server:" - -#: web/ejabberd_web_admin.erl:1734 web/mod_register_web.erl:175 -#: web/mod_register_web.erl:372 web/mod_register_web.erl:381 -#: web/mod_register_web.erl:411 -msgid "Change Password" -msgstr "Passwort ändern" - -#: web/ejabberd_web_admin.erl:1758 -msgid "User " -msgstr "Benutzer " - -#: web/ejabberd_web_admin.erl:1765 -msgid "Connected Resources:" -msgstr "Verbundene Ressourcen:" - -#: web/ejabberd_web_admin.erl:1766 web/mod_register_web.erl:227 -#: web/mod_register_web.erl:519 -msgid "Password:" -msgstr "Passwort:" - -#: web/ejabberd_web_admin.erl:1828 -msgid "No Data" -msgstr "Keine Daten" - -#: web/ejabberd_web_admin.erl:1906 -msgid "Nodes" -msgstr "Knoten" - -#: web/ejabberd_web_admin.erl:1929 web/ejabberd_web_admin.erl:1951 -msgid "Node " -msgstr "Knoten " - -#: web/ejabberd_web_admin.erl:1938 -msgid "Listened Ports" -msgstr "Aktive Ports" - -#: web/ejabberd_web_admin.erl:1940 web/ejabberd_web_admin.erl:2260 -#: web/ejabberd_web_admin.erl:2448 -msgid "Update" -msgstr "Aktualisieren" - -#: web/ejabberd_web_admin.erl:1943 web/ejabberd_web_admin.erl:2569 -msgid "Restart" -msgstr "Neustart" - -#: web/ejabberd_web_admin.erl:1945 web/ejabberd_web_admin.erl:2571 -msgid "Stop" -msgstr "Stoppen" - -#: web/ejabberd_web_admin.erl:1959 -msgid "RPC Call Error" -msgstr "Fehler bei RPC-Aufruf" - -#: web/ejabberd_web_admin.erl:2000 -msgid "Database Tables at " -msgstr "Datenbanktabellen auf " - -#: web/ejabberd_web_admin.erl:2007 -msgid "Storage Type" -msgstr "Speichertyp" - -#: web/ejabberd_web_admin.erl:2008 -msgid "Elements" -msgstr "Elemente" - -#: web/ejabberd_web_admin.erl:2009 -msgid "Memory" -msgstr "Speicher" - -#: web/ejabberd_web_admin.erl:2032 web/ejabberd_web_admin.erl:2137 -msgid "Error" -msgstr "Fehler" - -#: web/ejabberd_web_admin.erl:2034 -msgid "Backup of " -msgstr "Sicherung von " - -#: web/ejabberd_web_admin.erl:2036 -msgid "" -"Please note that these options will only backup the builtin Mnesia database. " -"If you are using the ODBC module, you also need to backup your SQL database " -"separately." -msgstr "" -"Beachten sie, das diese Optionen nur die eingebaute Mnesia-Datenbank " -"sichern. Wenn sie das ODBC-Modul verwenden, müssen sie die SQL-Datenbank " -"manuell sichern." - -#: web/ejabberd_web_admin.erl:2041 -msgid "Store binary backup:" -msgstr "Speichere binäre Sicherung:" - -#: web/ejabberd_web_admin.erl:2045 web/ejabberd_web_admin.erl:2052 -#: web/ejabberd_web_admin.erl:2060 web/ejabberd_web_admin.erl:2067 -#: web/ejabberd_web_admin.erl:2074 web/ejabberd_web_admin.erl:2081 -#: web/ejabberd_web_admin.erl:2088 web/ejabberd_web_admin.erl:2096 -#: web/ejabberd_web_admin.erl:2103 web/ejabberd_web_admin.erl:2110 -msgid "OK" -msgstr "OK" - -#: web/ejabberd_web_admin.erl:2048 -msgid "Restore binary backup immediately:" -msgstr "Stelle binäre Sicherung sofort wieder her:" - -#: web/ejabberd_web_admin.erl:2056 -msgid "" -"Restore binary backup after next ejabberd restart (requires less memory):" -msgstr "" -"Stelle binäre Sicherung beim nächsten ejabberd-Neustart wieder her (benötigt " -"weniger Speicher):" - -#: web/ejabberd_web_admin.erl:2063 -msgid "Store plain text backup:" -msgstr "Speichere Klartext-Sicherung:" - -#: web/ejabberd_web_admin.erl:2070 -msgid "Restore plain text backup immediately:" -msgstr "Stelle Klartext-Sicherung sofort wieder her:" - -#: web/ejabberd_web_admin.erl:2077 -msgid "Import users data from a PIEFXIS file (XEP-0227):" -msgstr "Benutzerdaten von einer PIEFXIS Datei (XEP-0227) importieren:" - -#: web/ejabberd_web_admin.erl:2084 -msgid "Export data of all users in the server to PIEFXIS files (XEP-0227):" -msgstr "" -"Alle Benutzerdaten des Servers in PIEFXIS Dateien (XEP-0227) exportieren:" - -#: web/ejabberd_web_admin.erl:2091 -msgid "Export data of users in a host to PIEFXIS files (XEP-0227):" -msgstr "" -"Alle Benutzerdaten des Hosts in PIEFXIS Dateien (XEP-0227) exportieren:" - -#: web/ejabberd_web_admin.erl:2099 -msgid "Import user data from jabberd14 spool file:" -msgstr "Importiere Benutzer von jabberd14 Spool Datei:" - -#: web/ejabberd_web_admin.erl:2106 -msgid "Import users data from jabberd14 spool directory:" -msgstr "Importiere Benutzer von jabberd14 Spool Verzeichnis:" - -#: web/ejabberd_web_admin.erl:2132 -msgid "Listened Ports at " -msgstr "Aktive Ports bei" - -#: web/ejabberd_web_admin.erl:2157 -msgid "Modules at " -msgstr "Module bei " - -#: web/ejabberd_web_admin.erl:2183 -msgid "Statistics of ~p" -msgstr "Statistiken von ~p" - -#: web/ejabberd_web_admin.erl:2186 -msgid "Uptime:" -msgstr "Betriebszeit:" - -#: web/ejabberd_web_admin.erl:2189 -msgid "CPU Time:" -msgstr "CPU-Zeit:" - -#: web/ejabberd_web_admin.erl:2195 -msgid "Transactions Committed:" -msgstr "Durchgeführte Transaktionen:" - -#: web/ejabberd_web_admin.erl:2198 -msgid "Transactions Aborted:" -msgstr "Abgebrochene Transaktionen:" - -#: web/ejabberd_web_admin.erl:2201 -msgid "Transactions Restarted:" -msgstr "Neu gestartete Transaktionen:" - -#: web/ejabberd_web_admin.erl:2204 -msgid "Transactions Logged:" -msgstr "Protokollierte Transaktionen:" - -#: web/ejabberd_web_admin.erl:2246 -msgid "Update " -msgstr "Aktualisierung " - -#: web/ejabberd_web_admin.erl:2254 -msgid "Update plan" -msgstr "Aktualisierungsplan" - -#: web/ejabberd_web_admin.erl:2255 -msgid "Modified modules" -msgstr "Geänderte Module" - -#: web/ejabberd_web_admin.erl:2256 -msgid "Update script" -msgstr "Aktualisierungsscript" - -#: web/ejabberd_web_admin.erl:2257 -msgid "Low level update script" -msgstr "Low level Aktualisierungsscript" - -#: web/ejabberd_web_admin.erl:2258 -msgid "Script check" -msgstr "Script-Überprüfung" - -#: web/ejabberd_web_admin.erl:2426 -msgid "Port" -msgstr "Port" - -#: web/ejabberd_web_admin.erl:2427 -msgid "IP" -msgstr "IP" - -#: web/ejabberd_web_admin.erl:2428 -msgid "Protocol" -msgstr "Protokoll" - -#: web/ejabberd_web_admin.erl:2429 web/ejabberd_web_admin.erl:2556 -msgid "Module" -msgstr "Modul" - -#: web/ejabberd_web_admin.erl:2430 web/ejabberd_web_admin.erl:2557 -msgid "Options" -msgstr "Optionen" - -#: web/ejabberd_web_admin.erl:2450 -msgid "Delete" -msgstr "Löschen" - -#: web/ejabberd_web_admin.erl:2579 -msgid "Start" -msgstr "Starten" - -#: web/mod_register_web.erl:104 -msgid "Your Jabber account was successfully created." -msgstr "Ihr Jabber Konto wurde erfolgreich erstellt." - -#: web/mod_register_web.erl:107 -msgid "There was an error creating the account: " -msgstr "Es trat ein Fehler beim erstellen des Kontos auf:" - -#: web/mod_register_web.erl:115 -msgid "Your Jabber account was successfully deleted." -msgstr "Ihr Jabber Konto wurde erfolgreich gelöscht." - -#: web/mod_register_web.erl:118 -msgid "There was an error deleting the account: " -msgstr "Es trat ein Fehler beim Löschen des Kontos auf:" - -#: web/mod_register_web.erl:128 -msgid "The password of your Jabber account was successfully changed." -msgstr "Das Passwort von ihrem Jabber Konto wurde geändert." - -#: web/mod_register_web.erl:131 -msgid "There was an error changing the password: " -msgstr "Es trat ein Fehler beim Ändern des Passworts auf:" - -#: web/mod_register_web.erl:163 web/mod_register_web.erl:172 -msgid "Jabber Account Registration" -msgstr "Jabber Konto Anmeldung" - -#: web/mod_register_web.erl:174 web/mod_register_web.erl:192 -#: web/mod_register_web.erl:201 -msgid "Register a Jabber account" -msgstr "Jabber Konto registrieren" - -#: web/mod_register_web.erl:176 web/mod_register_web.erl:493 -#: web/mod_register_web.erl:502 -msgid "Unregister a Jabber account" -msgstr "Jabber Konto entfernen" - -#: web/mod_register_web.erl:203 -msgid "" -"This page allows to create a Jabber account in this Jabber server. Your JID " -"(Jabber IDentifier) will be of the form: username@server. Please read " -"carefully the instructions to fill correctly the fields." -msgstr "" -"Diese Seite erlaubt das anlegen eines Jabber Kontos auf diesem Jabber " -"Server. Ihre JID (Jabber IDentifier) setzt sich folgend zusammen: " -"benutzername@server. Bitte lesen sie die Hinweise genau durch, um die Felder " -"korrekt auszufüllen." - -#: web/mod_register_web.erl:212 web/mod_register_web.erl:386 -#: web/mod_register_web.erl:509 -msgid "Username:" -msgstr "Benutzername:" - -#: web/mod_register_web.erl:217 -msgid "This is case insensitive: macbeth is the same that MacBeth and Macbeth." -msgstr "" -"Groß/Klein-Schreibung spielt hierbei keine Rolle: macbeth ist gleich MacBeth " -"und Macbeth." - -#: web/mod_register_web.erl:218 -msgid "Characters not allowed:" -msgstr "Nicht erlaubte Zeichen:" - -#: web/mod_register_web.erl:222 web/mod_register_web.erl:391 -#: web/mod_register_web.erl:514 -msgid "Server:" -msgstr "Server:" - -#: web/mod_register_web.erl:232 -msgid "" -"Don't tell your password to anybody, not even the administrators of the " -"Jabber server." -msgstr "" -"Geben sie niemandem ihr Passwort, auch nicht den Administratoren des Jabber " -"Servers." - -#: web/mod_register_web.erl:234 -msgid "You can later change your password using a Jabber client." -msgstr "" -"Sie können das Passwort später mit einem Jabber Client Programm ändern." - -#: web/mod_register_web.erl:235 -msgid "" -"Some Jabber clients can store your password in your computer. Use that " -"feature only if you trust your computer is safe." -msgstr "" -"Einige Jabber Client Programme speichern ihr Passwort auf ihrem Computer. " -"Verwenden sie diese Möglichkeit nur auf Computern, die sie als sicher " -"einstufen." - -#: web/mod_register_web.erl:237 -msgid "" -"Memorize your password, or write it in a paper placed in a safe place. In " -"Jabber there isn't an automated way to recover your password if you forget " -"it." -msgstr "" -"Merken sie sich ihr Passwort, oder schreiben sie es auf einen Zettel den sie " -"sicher verwahren. Bei Jabber gibt es keine automatische Möglichkeit, das " -"Passwort wiederherzustellen." - -#: web/mod_register_web.erl:242 web/mod_register_web.erl:406 -msgid "Password Verification:" -msgstr "Passwort bestätigen:" - -#: web/mod_register_web.erl:250 -msgid "Register" -msgstr "Anmelden" - -#: web/mod_register_web.erl:396 -msgid "Old Password:" -msgstr "Aktuelles Passwort:" - -#: web/mod_register_web.erl:401 -msgid "New Password:" -msgstr "Neues Passwort:" - -#: web/mod_register_web.erl:504 -msgid "This page allows to unregister a Jabber account in this Jabber server." -msgstr "" -"Diese Seite erlaubt es, ein Jabber Konto von diesem Server zu entfernen." - -#: web/mod_register_web.erl:524 -msgid "Unregister" -msgstr "Abmelden" - -#, fuzzy -#~ msgid "Captcha test failed" -#~ msgstr "Die Verifizierung ist gültig." diff --git a/priv/msgs/ejabberd.pot b/priv/msgs/ejabberd.pot deleted file mode 100644 index b62c81032..000000000 --- a/priv/msgs/ejabberd.pot +++ /dev/null @@ -1,1803 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: 2.1.x\n" -"X-Language: Language Name\n" -"Last-Translator: Translator name and contact method\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" - -#: ejabberd_c2s.erl:424 ejabberd_c2s.erl:727 -msgid "Use of STARTTLS required" -msgstr "" - -#: ejabberd_c2s.erl:503 -msgid "No resource provided" -msgstr "" - -#: ejabberd_c2s.erl:1197 -msgid "Replaced by new connection" -msgstr "" - -#: ejabberd_c2s.erl:1885 -msgid "Your active privacy list has denied the routing of this stanza." -msgstr "" - -#: ejabberd_captcha.erl:96 ejabberd_captcha.erl:152 ejabberd_captcha.erl:178 -msgid "Enter the text you see" -msgstr "" - -#: ejabberd_captcha.erl:101 -msgid "Your messages to ~s are being blocked. To unblock them, visit ~s" -msgstr "" - -#: ejabberd_captcha.erl:134 -msgid "If you don't see the CAPTCHA image here, visit the web page." -msgstr "" - -#: ejabberd_captcha.erl:146 -msgid "CAPTCHA web page" -msgstr "" - -#: ejabberd_captcha.erl:307 -msgid "The CAPTCHA is valid." -msgstr "" - -#: mod_adhoc.erl:95 mod_adhoc.erl:125 mod_adhoc.erl:143 mod_adhoc.erl:161 -msgid "Commands" -msgstr "" - -#: mod_adhoc.erl:149 mod_adhoc.erl:243 -msgid "Ping" -msgstr "" - -#: mod_adhoc.erl:260 -msgid "Pong" -msgstr "" - -#: mod_announce.erl:507 mod_announce_odbc.erl:499 -msgid "Really delete message of the day?" -msgstr "" - -#: mod_announce.erl:515 mod_announce_odbc.erl:507 mod_configure.erl:1083 -#: mod_configure.erl:1128 -msgid "Subject" -msgstr "" - -#: mod_announce.erl:520 mod_announce_odbc.erl:512 mod_configure.erl:1088 -#: mod_configure.erl:1133 -msgid "Message body" -msgstr "" - -#: mod_announce.erl:600 mod_announce_odbc.erl:592 -msgid "No body provided for announce message" -msgstr "" - -#: mod_announce.erl:635 mod_announce_odbc.erl:627 -msgid "Announcements" -msgstr "" - -#: mod_announce.erl:637 mod_announce_odbc.erl:629 -msgid "Send announcement to all users" -msgstr "" - -#: mod_announce.erl:639 mod_announce_odbc.erl:631 -msgid "Send announcement to all users on all hosts" -msgstr "" - -#: mod_announce.erl:641 mod_announce_odbc.erl:633 -msgid "Send announcement to all online users" -msgstr "" - -#: mod_announce.erl:643 mod_announce_odbc.erl:635 mod_configure.erl:1078 -#: mod_configure.erl:1123 -msgid "Send announcement to all online users on all hosts" -msgstr "" - -#: mod_announce.erl:645 mod_announce_odbc.erl:637 -msgid "Set message of the day and send to online users" -msgstr "" - -#: mod_announce.erl:647 mod_announce_odbc.erl:639 -msgid "Set message of the day on all hosts and send to online users" -msgstr "" - -#: mod_announce.erl:649 mod_announce_odbc.erl:641 -msgid "Update message of the day (don't send)" -msgstr "" - -#: mod_announce.erl:651 mod_announce_odbc.erl:643 -msgid "Update message of the day on all hosts (don't send)" -msgstr "" - -#: mod_announce.erl:653 mod_announce_odbc.erl:645 -msgid "Delete message of the day" -msgstr "" - -#: mod_announce.erl:655 mod_announce_odbc.erl:647 -msgid "Delete message of the day on all hosts" -msgstr "" - -#: mod_configure.erl:114 mod_configure.erl:274 mod_configure.erl:296 -#: mod_configure.erl:498 -msgid "Configuration" -msgstr "" - -#: mod_configure.erl:125 mod_configure.erl:576 web/ejabberd_web_admin.erl:1936 -msgid "Database" -msgstr "" - -#: mod_configure.erl:127 mod_configure.erl:595 -msgid "Start Modules" -msgstr "" - -#: mod_configure.erl:129 mod_configure.erl:596 -msgid "Stop Modules" -msgstr "" - -#: mod_configure.erl:131 mod_configure.erl:604 web/ejabberd_web_admin.erl:1937 -msgid "Backup" -msgstr "" - -#: mod_configure.erl:133 mod_configure.erl:605 -msgid "Restore" -msgstr "" - -#: mod_configure.erl:135 mod_configure.erl:606 -msgid "Dump to Text File" -msgstr "" - -#: mod_configure.erl:137 mod_configure.erl:615 -msgid "Import File" -msgstr "" - -#: mod_configure.erl:139 mod_configure.erl:616 -msgid "Import Directory" -msgstr "" - -#: mod_configure.erl:141 mod_configure.erl:581 mod_configure.erl:1057 -msgid "Restart Service" -msgstr "" - -#: mod_configure.erl:143 mod_configure.erl:582 mod_configure.erl:1102 -msgid "Shut Down Service" -msgstr "" - -#: mod_configure.erl:145 mod_configure.erl:518 mod_configure.erl:1197 -#: web/ejabberd_web_admin.erl:1527 -msgid "Add User" -msgstr "" - -#: mod_configure.erl:147 mod_configure.erl:519 mod_configure.erl:1219 -msgid "Delete User" -msgstr "" - -#: mod_configure.erl:149 mod_configure.erl:520 mod_configure.erl:1231 -msgid "End User Session" -msgstr "" - -#: mod_configure.erl:151 mod_configure.erl:521 mod_configure.erl:1243 -#: mod_configure.erl:1255 -msgid "Get User Password" -msgstr "" - -#: mod_configure.erl:153 mod_configure.erl:522 -msgid "Change User Password" -msgstr "" - -#: mod_configure.erl:155 mod_configure.erl:523 mod_configure.erl:1272 -msgid "Get User Last Login Time" -msgstr "" - -#: mod_configure.erl:157 mod_configure.erl:524 mod_configure.erl:1284 -msgid "Get User Statistics" -msgstr "" - -#: mod_configure.erl:159 mod_configure.erl:525 -msgid "Get Number of Registered Users" -msgstr "" - -#: mod_configure.erl:161 mod_configure.erl:526 -msgid "Get Number of Online Users" -msgstr "" - -#: mod_configure.erl:163 mod_configure.erl:509 web/ejabberd_web_admin.erl:831 -#: web/ejabberd_web_admin.erl:872 -msgid "Access Control Lists" -msgstr "" - -#: mod_configure.erl:165 mod_configure.erl:510 web/ejabberd_web_admin.erl:940 -#: web/ejabberd_web_admin.erl:976 -msgid "Access Rules" -msgstr "" - -#: mod_configure.erl:297 mod_configure.erl:499 -msgid "User Management" -msgstr "" - -#: mod_configure.erl:500 web/ejabberd_web_admin.erl:1058 -#: web/ejabberd_web_admin.erl:1462 -msgid "Online Users" -msgstr "" - -#: mod_configure.erl:501 -msgid "All Users" -msgstr "" - -#: mod_configure.erl:502 -msgid "Outgoing s2s Connections" -msgstr "" - -#: mod_configure.erl:503 web/ejabberd_web_admin.erl:1907 -msgid "Running Nodes" -msgstr "" - -#: mod_configure.erl:504 web/ejabberd_web_admin.erl:1909 -msgid "Stopped Nodes" -msgstr "" - -#: mod_configure.erl:577 mod_configure.erl:587 web/ejabberd_web_admin.erl:1953 -msgid "Modules" -msgstr "" - -#: mod_configure.erl:578 -msgid "Backup Management" -msgstr "" - -#: mod_configure.erl:579 -msgid "Import Users From jabberd14 Spool Files" -msgstr "" - -#: mod_configure.erl:699 -msgid "To ~s" -msgstr "" - -#: mod_configure.erl:717 -msgid "From ~s" -msgstr "" - -#: mod_configure.erl:913 -msgid "Database Tables Configuration at " -msgstr "" - -#: mod_configure.erl:918 -msgid "Choose storage type of tables" -msgstr "" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Disc only copy" -msgstr "" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM and disc copy" -msgstr "" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM copy" -msgstr "" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Remote copy" -msgstr "" - -#: mod_configure.erl:950 -msgid "Stop Modules at " -msgstr "" - -#: mod_configure.erl:954 -msgid "Choose modules to stop" -msgstr "" - -#: mod_configure.erl:969 -msgid "Start Modules at " -msgstr "" - -#: mod_configure.erl:973 -msgid "Enter list of {Module, [Options]}" -msgstr "" - -#: mod_configure.erl:974 -msgid "List of modules to start" -msgstr "" - -#: mod_configure.erl:983 -msgid "Backup to File at " -msgstr "" - -#: mod_configure.erl:987 mod_configure.erl:1001 -msgid "Enter path to backup file" -msgstr "" - -#: mod_configure.erl:988 mod_configure.erl:1002 mod_configure.erl:1016 -#: mod_configure.erl:1030 -msgid "Path to File" -msgstr "" - -#: mod_configure.erl:997 -msgid "Restore Backup from File at " -msgstr "" - -#: mod_configure.erl:1011 -msgid "Dump Backup to Text File at " -msgstr "" - -#: mod_configure.erl:1015 -msgid "Enter path to text file" -msgstr "" - -#: mod_configure.erl:1025 -msgid "Import User from File at " -msgstr "" - -#: mod_configure.erl:1029 -msgid "Enter path to jabberd14 spool file" -msgstr "" - -#: mod_configure.erl:1039 -msgid "Import Users from Dir at " -msgstr "" - -#: mod_configure.erl:1043 -msgid "Enter path to jabberd14 spool dir" -msgstr "" - -#: mod_configure.erl:1044 -msgid "Path to Dir" -msgstr "" - -#: mod_configure.erl:1060 mod_configure.erl:1105 -msgid "Time delay" -msgstr "" - -#: mod_configure.erl:1143 -msgid "Access Control List Configuration" -msgstr "" - -#: mod_configure.erl:1147 -msgid "Access control lists" -msgstr "" - -#: mod_configure.erl:1171 -msgid "Access Configuration" -msgstr "" - -#: mod_configure.erl:1175 -msgid "Access rules" -msgstr "" - -#: mod_configure.erl:1200 mod_configure.erl:1222 mod_configure.erl:1234 -#: mod_configure.erl:1246 mod_configure.erl:1258 mod_configure.erl:1275 -#: mod_configure.erl:1287 mod_configure.erl:1650 mod_configure.erl:1700 -#: mod_configure.erl:1721 mod_roster.erl:943 mod_roster_odbc.erl:1060 -#: mod_vcard.erl:472 mod_vcard_ldap.erl:554 mod_vcard_odbc.erl:448 -msgid "Jabber ID" -msgstr "" - -#: mod_configure.erl:1205 mod_configure.erl:1263 mod_configure.erl:1651 -#: mod_configure.erl:1863 mod_muc/mod_muc_room.erl:3224 mod_register.erl:235 -#: web/ejabberd_web_admin.erl:1520 -msgid "Password" -msgstr "" - -#: mod_configure.erl:1210 -msgid "Password Verification" -msgstr "" - -#: mod_configure.erl:1301 -msgid "Number of registered users" -msgstr "" - -#: mod_configure.erl:1315 -msgid "Number of online users" -msgstr "" - -#: mod_configure.erl:1682 web/ejabberd_web_admin.erl:1588 -#: web/ejabberd_web_admin.erl:1743 -msgid "Never" -msgstr "" - -#: mod_configure.erl:1696 web/ejabberd_web_admin.erl:1601 -#: web/ejabberd_web_admin.erl:1756 -msgid "Online" -msgstr "" - -#: mod_configure.erl:1701 -msgid "Last login" -msgstr "" - -#: mod_configure.erl:1722 -msgid "Roster size" -msgstr "" - -#: mod_configure.erl:1723 -msgid "IP addresses" -msgstr "" - -#: mod_configure.erl:1724 -msgid "Resources" -msgstr "" - -#: mod_configure.erl:1850 -msgid "Administration of " -msgstr "" - -#: mod_configure.erl:1853 -msgid "Action on user" -msgstr "" - -#: mod_configure.erl:1857 -msgid "Edit Properties" -msgstr "" - -#: mod_configure.erl:1860 web/ejabberd_web_admin.erl:1769 -msgid "Remove User" -msgstr "" - -#: mod_irc/mod_irc.erl:201 mod_irc/mod_irc_odbc.erl:196 -#: mod_muc/mod_muc.erl:336 mod_muc/mod_muc_odbc.erl:342 -msgid "Access denied by service policy" -msgstr "" - -#: mod_irc/mod_irc.erl:401 mod_irc/mod_irc_odbc.erl:398 -msgid "IRC Transport" -msgstr "" - -#: mod_irc/mod_irc.erl:428 mod_irc/mod_irc_odbc.erl:425 -msgid "ejabberd IRC module" -msgstr "" - -#: mod_irc/mod_irc.erl:559 mod_irc/mod_irc_odbc.erl:568 -msgid "You need an x:data capable client to configure mod_irc settings" -msgstr "" - -#: mod_irc/mod_irc.erl:566 mod_irc/mod_irc_odbc.erl:575 -msgid "Registration in mod_irc for " -msgstr "" - -#: mod_irc/mod_irc.erl:571 mod_irc/mod_irc_odbc.erl:580 -msgid "" -"Enter username, encodings, ports and passwords you wish to use for " -"connecting to IRC servers" -msgstr "" - -#: mod_irc/mod_irc.erl:576 mod_irc/mod_irc_odbc.erl:585 -msgid "IRC Username" -msgstr "" - -#: mod_irc/mod_irc.erl:586 mod_irc/mod_irc_odbc.erl:595 -msgid "" -"If you want to specify different ports, passwords, encodings for IRC " -"servers, fill this list with values in format '{\"irc server\", \"encoding" -"\", port, \"password\"}'. By default this service use \"~s\" encoding, port " -"~p, empty password." -msgstr "" - -#: mod_irc/mod_irc.erl:598 mod_irc/mod_irc_odbc.erl:607 -msgid "" -"Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef." -"net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]." -msgstr "" - -#: mod_irc/mod_irc.erl:603 mod_irc/mod_irc_odbc.erl:612 -msgid "Connections parameters" -msgstr "" - -#: mod_irc/mod_irc.erl:728 mod_irc/mod_irc_odbc.erl:757 -msgid "Join IRC channel" -msgstr "" - -#: mod_irc/mod_irc.erl:732 mod_irc/mod_irc_odbc.erl:761 -msgid "IRC channel (don't put the first #)" -msgstr "" - -#: mod_irc/mod_irc.erl:737 mod_irc/mod_irc_odbc.erl:766 -msgid "IRC server" -msgstr "" - -#: mod_irc/mod_irc.erl:770 mod_irc/mod_irc.erl:774 -#: mod_irc/mod_irc_odbc.erl:799 mod_irc/mod_irc_odbc.erl:803 -msgid "Join the IRC channel here." -msgstr "" - -#: mod_irc/mod_irc.erl:778 mod_irc/mod_irc_odbc.erl:807 -msgid "Join the IRC channel in this Jabber ID: ~s" -msgstr "" - -#: mod_irc/mod_irc.erl:863 mod_irc/mod_irc_odbc.erl:904 -msgid "IRC settings" -msgstr "" - -#: mod_irc/mod_irc.erl:868 mod_irc/mod_irc_odbc.erl:909 -msgid "" -"Enter username and encodings you wish to use for connecting to IRC servers. " -"Press 'Next' to get more fields to fill in. Press 'Complete' to save " -"settings." -msgstr "" - -#: mod_irc/mod_irc.erl:874 mod_irc/mod_irc_odbc.erl:915 -msgid "IRC username" -msgstr "" - -#: mod_irc/mod_irc.erl:923 mod_irc/mod_irc_odbc.erl:964 -msgid "Password ~b" -msgstr "" - -#: mod_irc/mod_irc.erl:928 mod_irc/mod_irc_odbc.erl:969 -msgid "Port ~b" -msgstr "" - -#: mod_irc/mod_irc.erl:933 mod_irc/mod_irc_odbc.erl:974 -msgid "Encoding for server ~b" -msgstr "" - -#: mod_irc/mod_irc.erl:942 mod_irc/mod_irc_odbc.erl:983 -msgid "Server ~b" -msgstr "" - -#: mod_muc/mod_muc.erl:449 mod_muc/mod_muc_odbc.erl:456 -msgid "Only service administrators are allowed to send service messages" -msgstr "" - -#: mod_muc/mod_muc.erl:493 mod_muc/mod_muc_odbc.erl:500 -msgid "Room creation is denied by service policy" -msgstr "" - -#: mod_muc/mod_muc.erl:500 mod_muc/mod_muc_odbc.erl:507 -msgid "Conference room does not exist" -msgstr "" - -#: mod_muc/mod_muc.erl:582 mod_muc/mod_muc_odbc.erl:595 -msgid "Chatrooms" -msgstr "" - -#: mod_muc/mod_muc.erl:712 mod_muc/mod_muc_odbc.erl:729 -msgid "You need a client that supports x:data to register the nickname" -msgstr "" - -#: mod_muc/mod_muc.erl:718 mod_muc/mod_muc_odbc.erl:735 -msgid "Nickname Registration at " -msgstr "" - -#: mod_muc/mod_muc.erl:722 mod_muc/mod_muc_odbc.erl:739 -msgid "Enter nickname you want to register" -msgstr "" - -#: mod_muc/mod_muc.erl:723 mod_muc/mod_muc_odbc.erl:740 -#: mod_muc/mod_muc_room.erl:3810 mod_roster.erl:944 mod_roster_odbc.erl:1061 -#: mod_vcard.erl:364 mod_vcard.erl:477 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:453 -msgid "Nickname" -msgstr "" - -#: mod_muc/mod_muc.erl:762 mod_muc/mod_muc_odbc.erl:784 -#: mod_muc/mod_muc_room.erl:1064 mod_muc/mod_muc_room.erl:1787 -msgid "That nickname is registered by another person" -msgstr "" - -#: mod_muc/mod_muc.erl:788 mod_muc/mod_muc_odbc.erl:811 -msgid "You must fill in field \"Nickname\" in the form" -msgstr "" - -#: mod_muc/mod_muc.erl:808 mod_muc/mod_muc_odbc.erl:831 -msgid "ejabberd MUC module" -msgstr "" - -#: mod_muc/mod_muc_log.erl:374 mod_muc/mod_muc_log.erl:381 -msgid "Chatroom configuration modified" -msgstr "" - -#: mod_muc/mod_muc_log.erl:384 -msgid "joins the room" -msgstr "" - -#: mod_muc/mod_muc_log.erl:387 mod_muc/mod_muc_log.erl:390 -msgid "leaves the room" -msgstr "" - -#: mod_muc/mod_muc_log.erl:393 mod_muc/mod_muc_log.erl:396 -msgid "has been banned" -msgstr "" - -#: mod_muc/mod_muc_log.erl:399 mod_muc/mod_muc_log.erl:402 -msgid "has been kicked" -msgstr "" - -#: mod_muc/mod_muc_log.erl:405 -msgid "has been kicked because of an affiliation change" -msgstr "" - -#: mod_muc/mod_muc_log.erl:408 -msgid "has been kicked because the room has been changed to members-only" -msgstr "" - -#: mod_muc/mod_muc_log.erl:411 -msgid "has been kicked because of a system shutdown" -msgstr "" - -#: mod_muc/mod_muc_log.erl:414 -msgid "is now known as" -msgstr "" - -#: mod_muc/mod_muc_log.erl:417 mod_muc/mod_muc_log.erl:688 -#: mod_muc/mod_muc_room.erl:2393 -msgid " has set the subject to: " -msgstr "" - -#: mod_muc/mod_muc_log.erl:452 -msgid "Chatroom is created" -msgstr "" - -#: mod_muc/mod_muc_log.erl:453 -msgid "Chatroom is destroyed" -msgstr "" - -#: mod_muc/mod_muc_log.erl:454 -msgid "Chatroom is started" -msgstr "" - -#: mod_muc/mod_muc_log.erl:455 -msgid "Chatroom is stopped" -msgstr "" - -#: mod_muc/mod_muc_log.erl:459 -msgid "Monday" -msgstr "" - -#: mod_muc/mod_muc_log.erl:460 -msgid "Tuesday" -msgstr "" - -#: mod_muc/mod_muc_log.erl:461 -msgid "Wednesday" -msgstr "" - -#: mod_muc/mod_muc_log.erl:462 -msgid "Thursday" -msgstr "" - -#: mod_muc/mod_muc_log.erl:463 -msgid "Friday" -msgstr "" - -#: mod_muc/mod_muc_log.erl:464 -msgid "Saturday" -msgstr "" - -#: mod_muc/mod_muc_log.erl:465 -msgid "Sunday" -msgstr "" - -#: mod_muc/mod_muc_log.erl:469 -msgid "January" -msgstr "" - -#: mod_muc/mod_muc_log.erl:470 -msgid "February" -msgstr "" - -#: mod_muc/mod_muc_log.erl:471 -msgid "March" -msgstr "" - -#: mod_muc/mod_muc_log.erl:472 -msgid "April" -msgstr "" - -#: mod_muc/mod_muc_log.erl:473 -msgid "May" -msgstr "" - -#: mod_muc/mod_muc_log.erl:474 -msgid "June" -msgstr "" - -#: mod_muc/mod_muc_log.erl:475 -msgid "July" -msgstr "" - -#: mod_muc/mod_muc_log.erl:476 -msgid "August" -msgstr "" - -#: mod_muc/mod_muc_log.erl:477 -msgid "September" -msgstr "" - -#: mod_muc/mod_muc_log.erl:478 -msgid "October" -msgstr "" - -#: mod_muc/mod_muc_log.erl:479 -msgid "November" -msgstr "" - -#: mod_muc/mod_muc_log.erl:480 -msgid "December" -msgstr "" - -#: mod_muc/mod_muc_log.erl:750 -msgid "Room Configuration" -msgstr "" - -#: mod_muc/mod_muc_log.erl:759 -msgid "Room Occupants" -msgstr "" - -#: mod_muc/mod_muc_room.erl:174 -msgid "Traffic rate limit is exceeded" -msgstr "" - -#: mod_muc/mod_muc_room.erl:246 -msgid "" -"This participant is kicked from the room because he sent an error message" -msgstr "" - -#: mod_muc/mod_muc_room.erl:255 -msgid "It is not allowed to send private messages to the conference" -msgstr "" - -#: mod_muc/mod_muc_room.erl:332 -msgid "Please, wait for a while before sending new voice request" -msgstr "" - -#: mod_muc/mod_muc_room.erl:347 -msgid "Voice requests are disabled in this conference" -msgstr "" - -#: mod_muc/mod_muc_room.erl:364 -msgid "Failed to extract JID from your voice request approval" -msgstr "" - -#: mod_muc/mod_muc_room.erl:393 -msgid "Only moderators can approve voice requests" -msgstr "" - -#: mod_muc/mod_muc_room.erl:408 -msgid "Improper message type" -msgstr "" - -#: mod_muc/mod_muc_room.erl:518 -msgid "" -"This participant is kicked from the room because he sent an error message to " -"another participant" -msgstr "" - -#: mod_muc/mod_muc_room.erl:531 -msgid "It is not allowed to send private messages of type \"groupchat\"" -msgstr "" - -#: mod_muc/mod_muc_room.erl:543 mod_muc/mod_muc_room.erl:611 -msgid "Recipient is not in the conference room" -msgstr "" - -#: mod_muc/mod_muc_room.erl:564 mod_muc/mod_muc_room.erl:585 -msgid "It is not allowed to send private messages" -msgstr "" - -#: mod_muc/mod_muc_room.erl:576 mod_muc/mod_muc_room.erl:956 -#: mod_muc/mod_muc_room.erl:4040 -msgid "Only occupants are allowed to send messages to the conference" -msgstr "" - -#: mod_muc/mod_muc_room.erl:634 -msgid "Only occupants are allowed to send queries to the conference" -msgstr "" - -#: mod_muc/mod_muc_room.erl:646 -msgid "Queries to the conference members are not allowed in this room" -msgstr "" - -#: mod_muc/mod_muc_room.erl:932 -msgid "" -"Only moderators and participants are allowed to change the subject in this " -"room" -msgstr "" - -#: mod_muc/mod_muc_room.erl:937 -msgid "Only moderators are allowed to change the subject in this room" -msgstr "" - -#: mod_muc/mod_muc_room.erl:947 -msgid "Visitors are not allowed to send messages to all occupants" -msgstr "" - -#: mod_muc/mod_muc_room.erl:1021 -msgid "" -"This participant is kicked from the room because he sent an error presence" -msgstr "" - -#: mod_muc/mod_muc_room.erl:1040 -msgid "Visitors are not allowed to change their nicknames in this room" -msgstr "" - -#: mod_muc/mod_muc_room.erl:1053 mod_muc/mod_muc_room.erl:1779 -msgid "That nickname is already in use by another occupant" -msgstr "" - -#: mod_muc/mod_muc_room.erl:1768 -msgid "You have been banned from this room" -msgstr "" - -#: mod_muc/mod_muc_room.erl:1771 -msgid "Membership is required to enter this room" -msgstr "" - -#: mod_muc/mod_muc_room.erl:1807 -msgid "This room is not anonymous" -msgstr "" - -#: mod_muc/mod_muc_room.erl:1833 -msgid "A password is required to enter this room" -msgstr "" - -#: mod_muc/mod_muc_room.erl:1855 mod_register.erl:246 -msgid "Too many CAPTCHA requests" -msgstr "" - -#: mod_muc/mod_muc_room.erl:1864 mod_register.erl:251 -msgid "Unable to generate a CAPTCHA" -msgstr "" - -#: mod_muc/mod_muc_room.erl:1874 -msgid "Incorrect password" -msgstr "" - -#: mod_muc/mod_muc_room.erl:2448 -msgid "Administrator privileges required" -msgstr "" - -#: mod_muc/mod_muc_room.erl:2463 -msgid "Moderator privileges required" -msgstr "" - -#: mod_muc/mod_muc_room.erl:2619 -msgid "Jabber ID ~s is invalid" -msgstr "" - -#: mod_muc/mod_muc_room.erl:2633 -msgid "Nickname ~s does not exist in the room" -msgstr "" - -#: mod_muc/mod_muc_room.erl:2659 mod_muc/mod_muc_room.erl:3049 -msgid "Invalid affiliation: ~s" -msgstr "" - -#: mod_muc/mod_muc_room.erl:2713 -msgid "Invalid role: ~s" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3026 mod_muc/mod_muc_room.erl:3062 -msgid "Owner privileges required" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3195 -msgid "Configuration of room ~s" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3200 -msgid "Room title" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3203 mod_muc/mod_muc_room.erl:3692 -msgid "Room description" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3210 -msgid "Make room persistent" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3215 -msgid "Make room public searchable" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3218 -msgid "Make participants list public" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3221 -msgid "Make room password protected" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3232 -msgid "Maximum Number of Occupants" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3239 -msgid "No limit" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3250 -msgid "Present real Jabber IDs to" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3258 mod_muc/mod_muc_room.erl:3292 -msgid "moderators only" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3260 mod_muc/mod_muc_room.erl:3294 -msgid "anyone" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3262 -msgid "Make room members-only" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3265 -msgid "Make room moderated" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3268 -msgid "Default users as participants" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3271 -msgid "Allow users to change the subject" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3274 -msgid "Allow users to send private messages" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3279 -msgid "Allow visitors to send private messages to" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3290 -msgid "nobody" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3296 -msgid "Allow users to query other users" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3299 -msgid "Allow users to send invites" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3302 -msgid "Allow visitors to send status text in presence updates" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3305 -msgid "Allow visitors to change nickname" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3308 -msgid "Allow visitors to send voice requests" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3311 -msgid "Minimum interval between voice requests (in seconds)" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3317 -msgid "Make room CAPTCHA protected" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3322 -msgid "Exclude Jabber IDs from CAPTCHA challenge" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3329 -msgid "Enable logging" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3337 -msgid "You need an x:data capable client to configure room" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3694 -msgid "Number of occupants" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3750 -msgid "private, " -msgstr "" - -#: mod_muc/mod_muc_room.erl:3799 -msgid "Voice request" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3803 -msgid "Either approve or decline the voice request." -msgstr "" - -#: mod_muc/mod_muc_room.erl:3809 -msgid "User JID" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3811 -msgid "Grant voice to this person?" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3960 -msgid "~s invites you to the room ~s" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3969 -msgid "the password is" -msgstr "" - -#: mod_offline.erl:510 mod_offline_odbc.erl:352 -msgid "" -"Your contact offline message queue is full. The message has been discarded." -msgstr "" - -#: mod_offline.erl:560 mod_offline_odbc.erl:408 -msgid "~s's Offline Messages Queue" -msgstr "" - -#: mod_offline.erl:563 mod_offline_odbc.erl:411 mod_roster.erl:987 -#: mod_roster_odbc.erl:1104 mod_shared_roster.erl:901 -#: mod_shared_roster.erl:1010 mod_shared_roster_odbc.erl:930 -#: mod_shared_roster_odbc.erl:1039 web/ejabberd_web_admin.erl:833 -#: web/ejabberd_web_admin.erl:874 web/ejabberd_web_admin.erl:942 -#: web/ejabberd_web_admin.erl:978 web/ejabberd_web_admin.erl:1019 -#: web/ejabberd_web_admin.erl:1508 web/ejabberd_web_admin.erl:1760 -#: web/ejabberd_web_admin.erl:1931 web/ejabberd_web_admin.erl:1963 -#: web/ejabberd_web_admin.erl:2031 web/ejabberd_web_admin.erl:2135 -#: web/ejabberd_web_admin.erl:2160 web/ejabberd_web_admin.erl:2248 -msgid "Submitted" -msgstr "" - -#: mod_offline.erl:571 -msgid "Time" -msgstr "" - -#: mod_offline.erl:572 -msgid "From" -msgstr "" - -#: mod_offline.erl:573 -msgid "To" -msgstr "" - -#: mod_offline.erl:574 mod_offline_odbc.erl:419 -msgid "Packet" -msgstr "" - -#: mod_offline.erl:587 mod_offline_odbc.erl:432 mod_shared_roster.erl:908 -#: mod_shared_roster_odbc.erl:937 web/ejabberd_web_admin.erl:882 -#: web/ejabberd_web_admin.erl:986 -msgid "Delete Selected" -msgstr "" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -msgid "Offline Messages:" -msgstr "" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -msgid "Remove All Offline Messages" -msgstr "" - -#: mod_proxy65/mod_proxy65_service.erl:213 -msgid "ejabberd SOCKS5 Bytestreams module" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:1120 mod_pubsub/mod_pubsub_odbc.erl:929 -msgid "Publish-Subscribe" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:1214 mod_pubsub/mod_pubsub_odbc.erl:1025 -msgid "ejabberd Publish-Subscribe module" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:1497 mod_pubsub/mod_pubsub_odbc.erl:1312 -msgid "PubSub subscriber request" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:1499 mod_pubsub/mod_pubsub_odbc.erl:1314 -msgid "Choose whether to approve this entity's subscription." -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:1505 mod_pubsub/mod_pubsub_odbc.erl:1320 -msgid "Node ID" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:1510 mod_pubsub/mod_pubsub_odbc.erl:1325 -msgid "Subscriber Address" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:1516 mod_pubsub/mod_pubsub_odbc.erl:1331 -msgid "Allow this Jabber ID to subscribe to this pubsub node?" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:3474 mod_pubsub/mod_pubsub_odbc.erl:3287 -msgid "Deliver payloads with event notifications" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:3475 mod_pubsub/mod_pubsub_odbc.erl:3288 -msgid "Deliver event notifications" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:3476 mod_pubsub/mod_pubsub_odbc.erl:3289 -msgid "Notify subscribers when the node configuration changes" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:3477 mod_pubsub/mod_pubsub_odbc.erl:3290 -msgid "Notify subscribers when the node is deleted" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:3478 mod_pubsub/mod_pubsub_odbc.erl:3291 -msgid "Notify subscribers when items are removed from the node" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:3479 mod_pubsub/mod_pubsub_odbc.erl:3292 -msgid "Persist items to storage" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:3480 mod_pubsub/mod_pubsub_odbc.erl:3293 -msgid "A friendly name for the node" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:3481 mod_pubsub/mod_pubsub_odbc.erl:3294 -msgid "Max # of items to persist" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:3482 mod_pubsub/mod_pubsub_odbc.erl:3295 -msgid "Whether to allow subscriptions" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:3483 mod_pubsub/mod_pubsub_odbc.erl:3296 -msgid "Specify the access model" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:3486 mod_pubsub/mod_pubsub_odbc.erl:3299 -msgid "Roster groups allowed to subscribe" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:3487 mod_pubsub/mod_pubsub_odbc.erl:3300 -msgid "Specify the publisher model" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:3489 mod_pubsub/mod_pubsub_odbc.erl:3302 -msgid "Purge all items when the relevant publisher goes offline" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:3490 mod_pubsub/mod_pubsub_odbc.erl:3303 -msgid "Specify the event message type" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:3492 mod_pubsub/mod_pubsub_odbc.erl:3305 -msgid "Max payload size in bytes" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:3493 mod_pubsub/mod_pubsub_odbc.erl:3306 -msgid "When to send the last published item" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:3495 mod_pubsub/mod_pubsub_odbc.erl:3308 -msgid "Only deliver notifications to available users" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:3496 mod_pubsub/mod_pubsub_odbc.erl:3309 -msgid "The collections with which a node is affiliated" -msgstr "" - -#: mod_register.erl:193 -msgid "The CAPTCHA verification has failed" -msgstr "" - -#: mod_register.erl:220 -msgid "You need a client that supports x:data and CAPTCHA to register" -msgstr "" - -#: mod_register.erl:226 mod_register.erl:265 -msgid "Choose a username and password to register with this server" -msgstr "" - -#: mod_register.erl:230 mod_vcard.erl:364 mod_vcard_odbc.erl:342 -#: web/ejabberd_web_admin.erl:1515 web/ejabberd_web_admin.erl:1572 -msgid "User" -msgstr "" - -#: mod_register.erl:316 mod_register.erl:361 -msgid "The password is too weak" -msgstr "" - -#: mod_register.erl:365 -msgid "Users are not allowed to register accounts so quickly" -msgstr "" - -#: mod_roster.erl:938 mod_roster_odbc.erl:1055 web/ejabberd_web_admin.erl:1701 -#: web/ejabberd_web_admin.erl:1886 web/ejabberd_web_admin.erl:1897 -#: web/ejabberd_web_admin.erl:2219 -msgid "None" -msgstr "" - -#: mod_roster.erl:945 mod_roster_odbc.erl:1062 -msgid "Subscription" -msgstr "" - -#: mod_roster.erl:946 mod_roster_odbc.erl:1063 -msgid "Pending" -msgstr "" - -#: mod_roster.erl:947 mod_roster_odbc.erl:1064 -msgid "Groups" -msgstr "" - -#: mod_roster.erl:974 mod_roster_odbc.erl:1091 -msgid "Validate" -msgstr "" - -#: mod_roster.erl:982 mod_roster_odbc.erl:1099 -msgid "Remove" -msgstr "" - -#: mod_roster.erl:985 mod_roster_odbc.erl:1102 -msgid "Roster of " -msgstr "" - -#: mod_roster.erl:988 mod_roster_odbc.erl:1105 mod_shared_roster.erl:902 -#: mod_shared_roster.erl:1011 mod_shared_roster_odbc.erl:931 -#: mod_shared_roster_odbc.erl:1040 web/ejabberd_web_admin.erl:834 -#: web/ejabberd_web_admin.erl:875 web/ejabberd_web_admin.erl:943 -#: web/ejabberd_web_admin.erl:979 web/ejabberd_web_admin.erl:1020 -#: web/ejabberd_web_admin.erl:1509 web/ejabberd_web_admin.erl:1761 -#: web/ejabberd_web_admin.erl:1932 web/ejabberd_web_admin.erl:2136 -#: web/ejabberd_web_admin.erl:2161 -msgid "Bad format" -msgstr "" - -#: mod_roster.erl:995 mod_roster_odbc.erl:1112 -msgid "Add Jabber ID" -msgstr "" - -#: mod_roster.erl:1094 mod_roster_odbc.erl:1211 -msgid "Roster" -msgstr "" - -#: mod_shared_roster.erl:857 mod_shared_roster.erl:899 -#: mod_shared_roster.erl:1007 mod_shared_roster_odbc.erl:886 -#: mod_shared_roster_odbc.erl:928 mod_shared_roster_odbc.erl:1036 -msgid "Shared Roster Groups" -msgstr "" - -#: mod_shared_roster.erl:895 mod_shared_roster_odbc.erl:924 -#: web/ejabberd_web_admin.erl:1365 web/ejabberd_web_admin.erl:2461 -msgid "Add New" -msgstr "" - -#: mod_shared_roster.erl:978 mod_shared_roster_odbc.erl:1007 -msgid "Name:" -msgstr "" - -#: mod_shared_roster.erl:983 mod_shared_roster_odbc.erl:1012 -msgid "Description:" -msgstr "" - -#: mod_shared_roster.erl:991 mod_shared_roster_odbc.erl:1020 -msgid "Members:" -msgstr "" - -#: mod_shared_roster.erl:999 mod_shared_roster_odbc.erl:1028 -msgid "Displayed Groups:" -msgstr "" - -#: mod_shared_roster.erl:1008 mod_shared_roster_odbc.erl:1037 -msgid "Group " -msgstr "" - -#: mod_shared_roster.erl:1017 mod_shared_roster_odbc.erl:1046 -#: web/ejabberd_web_admin.erl:840 web/ejabberd_web_admin.erl:884 -#: web/ejabberd_web_admin.erl:949 web/ejabberd_web_admin.erl:1026 -#: web/ejabberd_web_admin.erl:2017 -msgid "Submit" -msgstr "" - -#: mod_vcard.erl:165 mod_vcard_ldap.erl:238 mod_vcard_odbc.erl:129 -msgid "Erlang Jabber Server" -msgstr "" - -#: mod_vcard.erl:364 mod_vcard.erl:478 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:454 -msgid "Birthday" -msgstr "" - -#: mod_vcard.erl:364 mod_vcard.erl:480 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:456 -msgid "City" -msgstr "" - -#: mod_vcard.erl:364 mod_vcard.erl:479 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:455 -msgid "Country" -msgstr "" - -#: mod_vcard.erl:364 mod_vcard.erl:481 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:457 -msgid "Email" -msgstr "" - -#: mod_vcard.erl:364 mod_vcard.erl:476 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:452 -msgid "Family Name" -msgstr "" - -#: mod_vcard.erl:364 mod_vcard_odbc.erl:342 -msgid "" -"Fill in the form to search for any matching Jabber User (Add * to the end of " -"field to match substring)" -msgstr "" - -#: mod_vcard.erl:364 mod_vcard.erl:473 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:449 -msgid "Full Name" -msgstr "" - -#: mod_vcard.erl:364 mod_vcard.erl:475 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:451 -msgid "Middle Name" -msgstr "" - -#: mod_vcard.erl:364 mod_vcard.erl:474 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:450 web/ejabberd_web_admin.erl:2006 -msgid "Name" -msgstr "" - -#: mod_vcard.erl:364 mod_vcard.erl:482 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:458 -msgid "Organization Name" -msgstr "" - -#: mod_vcard.erl:364 mod_vcard.erl:483 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:459 -msgid "Organization Unit" -msgstr "" - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "Search users in " -msgstr "" - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "You need an x:data capable client to search" -msgstr "" - -#: mod_vcard.erl:389 mod_vcard_ldap.erl:487 mod_vcard_odbc.erl:367 -msgid "vCard User Search" -msgstr "" - -#: mod_vcard.erl:445 mod_vcard_ldap.erl:541 mod_vcard_odbc.erl:421 -msgid "ejabberd vCard module" -msgstr "" - -#: mod_vcard.erl:469 mod_vcard_ldap.erl:551 mod_vcard_odbc.erl:445 -msgid "Search Results for " -msgstr "" - -#: mod_vcard_ldap.erl:462 -msgid "Fill in fields to search for any matching Jabber User" -msgstr "" - -#: web/ejabberd_web_admin.erl:193 web/ejabberd_web_admin.erl:203 -#: web/ejabberd_web_admin.erl:219 web/ejabberd_web_admin.erl:229 -msgid "Unauthorized" -msgstr "" - -#: web/ejabberd_web_admin.erl:286 web/ejabberd_web_admin.erl:303 -msgid "ejabberd Web Admin" -msgstr "" - -#: web/ejabberd_web_admin.erl:769 web/ejabberd_web_admin.erl:780 -msgid "Administration" -msgstr "" - -#: web/ejabberd_web_admin.erl:878 web/ejabberd_web_admin.erl:982 -msgid "Raw" -msgstr "" - -#: web/ejabberd_web_admin.erl:1017 -msgid "~s access rule configuration" -msgstr "" - -#: web/ejabberd_web_admin.erl:1035 -msgid "Virtual Hosts" -msgstr "" - -#: web/ejabberd_web_admin.erl:1043 web/ejabberd_web_admin.erl:1050 -msgid "Users" -msgstr "" - -#: web/ejabberd_web_admin.erl:1078 -msgid "Users Last Activity" -msgstr "" - -#: web/ejabberd_web_admin.erl:1080 -msgid "Period: " -msgstr "" - -#: web/ejabberd_web_admin.erl:1090 -msgid "Last month" -msgstr "" - -#: web/ejabberd_web_admin.erl:1091 -msgid "Last year" -msgstr "" - -#: web/ejabberd_web_admin.erl:1092 -msgid "All activity" -msgstr "" - -#: web/ejabberd_web_admin.erl:1094 -msgid "Show Ordinary Table" -msgstr "" - -#: web/ejabberd_web_admin.erl:1096 -msgid "Show Integral Table" -msgstr "" - -#: web/ejabberd_web_admin.erl:1105 web/ejabberd_web_admin.erl:1939 -msgid "Statistics" -msgstr "" - -#: web/ejabberd_web_admin.erl:1117 -msgid "Not Found" -msgstr "" - -#: web/ejabberd_web_admin.erl:1134 -msgid "Node not found" -msgstr "" - -#: web/ejabberd_web_admin.erl:1460 -msgid "Host" -msgstr "" - -#: web/ejabberd_web_admin.erl:1461 -msgid "Registered Users" -msgstr "" - -#: web/ejabberd_web_admin.erl:1573 -msgid "Offline Messages" -msgstr "" - -#: web/ejabberd_web_admin.erl:1574 web/ejabberd_web_admin.erl:1767 -msgid "Last Activity" -msgstr "" - -#: web/ejabberd_web_admin.erl:1659 web/ejabberd_web_admin.erl:1675 -msgid "Registered Users:" -msgstr "" - -#: web/ejabberd_web_admin.erl:1661 web/ejabberd_web_admin.erl:1677 -#: web/ejabberd_web_admin.erl:2192 -msgid "Online Users:" -msgstr "" - -#: web/ejabberd_web_admin.erl:1663 -msgid "Outgoing s2s Connections:" -msgstr "" - -#: web/ejabberd_web_admin.erl:1665 -msgid "Outgoing s2s Servers:" -msgstr "" - -#: web/ejabberd_web_admin.erl:1734 web/mod_register_web.erl:175 -#: web/mod_register_web.erl:372 web/mod_register_web.erl:381 -#: web/mod_register_web.erl:411 -msgid "Change Password" -msgstr "" - -#: web/ejabberd_web_admin.erl:1758 -msgid "User " -msgstr "" - -#: web/ejabberd_web_admin.erl:1765 -msgid "Connected Resources:" -msgstr "" - -#: web/ejabberd_web_admin.erl:1766 web/mod_register_web.erl:227 -#: web/mod_register_web.erl:519 -msgid "Password:" -msgstr "" - -#: web/ejabberd_web_admin.erl:1828 -msgid "No Data" -msgstr "" - -#: web/ejabberd_web_admin.erl:1906 -msgid "Nodes" -msgstr "" - -#: web/ejabberd_web_admin.erl:1929 web/ejabberd_web_admin.erl:1951 -msgid "Node " -msgstr "" - -#: web/ejabberd_web_admin.erl:1938 -msgid "Listened Ports" -msgstr "" - -#: web/ejabberd_web_admin.erl:1940 web/ejabberd_web_admin.erl:2260 -#: web/ejabberd_web_admin.erl:2448 -msgid "Update" -msgstr "" - -#: web/ejabberd_web_admin.erl:1943 web/ejabberd_web_admin.erl:2569 -msgid "Restart" -msgstr "" - -#: web/ejabberd_web_admin.erl:1945 web/ejabberd_web_admin.erl:2571 -msgid "Stop" -msgstr "" - -#: web/ejabberd_web_admin.erl:1959 -msgid "RPC Call Error" -msgstr "" - -#: web/ejabberd_web_admin.erl:2000 -msgid "Database Tables at " -msgstr "" - -#: web/ejabberd_web_admin.erl:2007 -msgid "Storage Type" -msgstr "" - -#: web/ejabberd_web_admin.erl:2008 -msgid "Elements" -msgstr "" - -#: web/ejabberd_web_admin.erl:2009 -msgid "Memory" -msgstr "" - -#: web/ejabberd_web_admin.erl:2032 web/ejabberd_web_admin.erl:2137 -msgid "Error" -msgstr "" - -#: web/ejabberd_web_admin.erl:2034 -msgid "Backup of " -msgstr "" - -#: web/ejabberd_web_admin.erl:2036 -msgid "" -"Please note that these options will only backup the builtin Mnesia database. " -"If you are using the ODBC module, you also need to backup your SQL database " -"separately." -msgstr "" - -#: web/ejabberd_web_admin.erl:2041 -msgid "Store binary backup:" -msgstr "" - -#: web/ejabberd_web_admin.erl:2045 web/ejabberd_web_admin.erl:2052 -#: web/ejabberd_web_admin.erl:2060 web/ejabberd_web_admin.erl:2067 -#: web/ejabberd_web_admin.erl:2074 web/ejabberd_web_admin.erl:2081 -#: web/ejabberd_web_admin.erl:2088 web/ejabberd_web_admin.erl:2096 -#: web/ejabberd_web_admin.erl:2103 web/ejabberd_web_admin.erl:2110 -msgid "OK" -msgstr "" - -#: web/ejabberd_web_admin.erl:2048 -msgid "Restore binary backup immediately:" -msgstr "" - -#: web/ejabberd_web_admin.erl:2056 -msgid "" -"Restore binary backup after next ejabberd restart (requires less memory):" -msgstr "" - -#: web/ejabberd_web_admin.erl:2063 -msgid "Store plain text backup:" -msgstr "" - -#: web/ejabberd_web_admin.erl:2070 -msgid "Restore plain text backup immediately:" -msgstr "" - -#: web/ejabberd_web_admin.erl:2077 -msgid "Import users data from a PIEFXIS file (XEP-0227):" -msgstr "" - -#: web/ejabberd_web_admin.erl:2084 -msgid "Export data of all users in the server to PIEFXIS files (XEP-0227):" -msgstr "" - -#: web/ejabberd_web_admin.erl:2091 -msgid "Export data of users in a host to PIEFXIS files (XEP-0227):" -msgstr "" - -#: web/ejabberd_web_admin.erl:2099 -msgid "Import user data from jabberd14 spool file:" -msgstr "" - -#: web/ejabberd_web_admin.erl:2106 -msgid "Import users data from jabberd14 spool directory:" -msgstr "" - -#: web/ejabberd_web_admin.erl:2132 -msgid "Listened Ports at " -msgstr "" - -#: web/ejabberd_web_admin.erl:2157 -msgid "Modules at " -msgstr "" - -#: web/ejabberd_web_admin.erl:2183 -msgid "Statistics of ~p" -msgstr "" - -#: web/ejabberd_web_admin.erl:2186 -msgid "Uptime:" -msgstr "" - -#: web/ejabberd_web_admin.erl:2189 -msgid "CPU Time:" -msgstr "" - -#: web/ejabberd_web_admin.erl:2195 -msgid "Transactions Committed:" -msgstr "" - -#: web/ejabberd_web_admin.erl:2198 -msgid "Transactions Aborted:" -msgstr "" - -#: web/ejabberd_web_admin.erl:2201 -msgid "Transactions Restarted:" -msgstr "" - -#: web/ejabberd_web_admin.erl:2204 -msgid "Transactions Logged:" -msgstr "" - -#: web/ejabberd_web_admin.erl:2246 -msgid "Update " -msgstr "" - -#: web/ejabberd_web_admin.erl:2254 -msgid "Update plan" -msgstr "" - -#: web/ejabberd_web_admin.erl:2255 -msgid "Modified modules" -msgstr "" - -#: web/ejabberd_web_admin.erl:2256 -msgid "Update script" -msgstr "" - -#: web/ejabberd_web_admin.erl:2257 -msgid "Low level update script" -msgstr "" - -#: web/ejabberd_web_admin.erl:2258 -msgid "Script check" -msgstr "" - -#: web/ejabberd_web_admin.erl:2426 -msgid "Port" -msgstr "" - -#: web/ejabberd_web_admin.erl:2427 -msgid "IP" -msgstr "" - -#: web/ejabberd_web_admin.erl:2428 -msgid "Protocol" -msgstr "" - -#: web/ejabberd_web_admin.erl:2429 web/ejabberd_web_admin.erl:2556 -msgid "Module" -msgstr "" - -#: web/ejabberd_web_admin.erl:2430 web/ejabberd_web_admin.erl:2557 -msgid "Options" -msgstr "" - -#: web/ejabberd_web_admin.erl:2450 -msgid "Delete" -msgstr "" - -#: web/ejabberd_web_admin.erl:2579 -msgid "Start" -msgstr "" - -#: web/mod_register_web.erl:104 -msgid "Your Jabber account was successfully created." -msgstr "" - -#: web/mod_register_web.erl:107 -msgid "There was an error creating the account: " -msgstr "" - -#: web/mod_register_web.erl:115 -msgid "Your Jabber account was successfully deleted." -msgstr "" - -#: web/mod_register_web.erl:118 -msgid "There was an error deleting the account: " -msgstr "" - -#: web/mod_register_web.erl:128 -msgid "The password of your Jabber account was successfully changed." -msgstr "" - -#: web/mod_register_web.erl:131 -msgid "There was an error changing the password: " -msgstr "" - -#: web/mod_register_web.erl:163 web/mod_register_web.erl:172 -msgid "Jabber Account Registration" -msgstr "" - -#: web/mod_register_web.erl:174 web/mod_register_web.erl:192 -#: web/mod_register_web.erl:201 -msgid "Register a Jabber account" -msgstr "" - -#: web/mod_register_web.erl:176 web/mod_register_web.erl:493 -#: web/mod_register_web.erl:502 -msgid "Unregister a Jabber account" -msgstr "" - -#: web/mod_register_web.erl:203 -msgid "" -"This page allows to create a Jabber account in this Jabber server. Your JID " -"(Jabber IDentifier) will be of the form: username@server. Please read " -"carefully the instructions to fill correctly the fields." -msgstr "" - -#: web/mod_register_web.erl:212 web/mod_register_web.erl:386 -#: web/mod_register_web.erl:509 -msgid "Username:" -msgstr "" - -#: web/mod_register_web.erl:217 -msgid "This is case insensitive: macbeth is the same that MacBeth and Macbeth." -msgstr "" - -#: web/mod_register_web.erl:218 -msgid "Characters not allowed:" -msgstr "" - -#: web/mod_register_web.erl:222 web/mod_register_web.erl:391 -#: web/mod_register_web.erl:514 -msgid "Server:" -msgstr "" - -#: web/mod_register_web.erl:232 -msgid "" -"Don't tell your password to anybody, not even the administrators of the " -"Jabber server." -msgstr "" - -#: web/mod_register_web.erl:234 -msgid "You can later change your password using a Jabber client." -msgstr "" - -#: web/mod_register_web.erl:235 -msgid "" -"Some Jabber clients can store your password in your computer. Use that " -"feature only if you trust your computer is safe." -msgstr "" - -#: web/mod_register_web.erl:237 -msgid "" -"Memorize your password, or write it in a paper placed in a safe place. In " -"Jabber there isn't an automated way to recover your password if you forget " -"it." -msgstr "" - -#: web/mod_register_web.erl:242 web/mod_register_web.erl:406 -msgid "Password Verification:" -msgstr "" - -#: web/mod_register_web.erl:250 -msgid "Register" -msgstr "" - -#: web/mod_register_web.erl:396 -msgid "Old Password:" -msgstr "" - -#: web/mod_register_web.erl:401 -msgid "New Password:" -msgstr "" - -#: web/mod_register_web.erl:504 -msgid "This page allows to unregister a Jabber account in this Jabber server." -msgstr "" - -#: web/mod_register_web.erl:524 -msgid "Unregister" -msgstr "" diff --git a/priv/msgs/el.msg b/priv/msgs/el.msg index 70fe3ac0e..0d18b30c4 100644 --- a/priv/msgs/el.msg +++ b/priv/msgs/el.msg @@ -1,135 +1,184 @@ -{"Access Configuration","Διαμόρφωση Πρόσβασης"}. -{"Access Control List Configuration","Διαχείριση στις Λίστες Ελέγχου Πρόσβασης"}. -{"Access control lists","Λίστες Ελέγχου Πρόσβασης"}. -{"Access Control Lists","Λίστες Ελέγχου Πρόσβασης"}. +%% Generated automatically +%% DO NOT EDIT: run `make translations` instead +%% To improve translations please read: +%% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ + +{" (Add * to the end of field to match substring)"," (Προσθέστε * στο τέλος του πεδίου για ταίριασμα με το substring)"}. +{" has set the subject to: "," έχει θέσει το θέμα σε: "}. +{"# participants","# συμμετέχοντες"}. +{"A description of the node","Μία περιγραφή του κόμβου"}. +{"A friendly name for the node","Ένα φιλικό όνομα για τον κόμβο"}. +{"A password is required to enter this room","Απαιτείται κωδικός πρόσβασης για είσοδο σε αυτή την αίθουσα"}. +{"A Web Page","Μία ιστοσελίδα"}. +{"Accept","Αποδοχή"}. {"Access denied by service policy","Άρνηση πρόσβασης, λόγω τακτικής παροχής υπηρεσιών"}. -{"Access rules","Κανόνες Πρόσβασης"}. -{"Access Rules","Κανόνες Πρόσβασης"}. +{"Access model","Μοντέλο πρόσβασης"}. +{"Account doesn't exist","Ο λογαριασμός δεν υπάρχει"}. {"Action on user","Eνέργεια για το χρήστη"}. -{"Add Jabber ID","Προσθήκη Jabber Ταυτότητας"}. -{"Add New","Προσθήκη νέου"}. +{"Add a hat to a user","Προσθέστε ένα καπέλο σε έναν χρήστη"}. {"Add User","Προσθήκη Χρήστη"}. -{"Administration of ","Διαχείριση του"}. +{"Administration of ","Διαχείριση του "}. {"Administration","Διαχείριση"}. {"Administrator privileges required","Aπαιτούνται προνόμια διαχειριστή"}. -{"A friendly name for the node","Ένα φιλικό όνομα για τον κόμβο"}. {"All activity","Όλες οι δραστηριότητες"}. +{"All Users","Όλοι οι χρήστες"}. +{"Allow subscription","Επιτρέψτε την Συνδρομή"}. {"Allow this Jabber ID to subscribe to this pubsub node?","Επιτρέπετε σε αυτή την Jabber Ταυτότητα να εγγραφεί σε αυτό τον κόμβο Δημοσίευσης-Εγγραφής;"}. -{"Allow users to change the subject","Επιτρέψετε στους χρήστες να αλλάζουν το θέμα"}. -{"Allow users to query other users","Επιτρέπστε στους χρήστες να ερωτούν άλλους χρήστες"}. -{"Allow users to send invites","Επιτρέψετε στους χρήστες να αποστέλλουν προσκλήσεις"}. -{"Allow users to send private messages","Επιτρέψετε στους χρήστες να αποστέλλουν ιδιωτικά μηνύματα"}. -{"Allow visitors to change nickname","Επιτρέψετε στους επισκέπτες να αλλάζου ψευδώνυμο"}. -{"Allow visitors to send private messages to","Επιτρέψετε στους χρήστες να αποστέλλουν ιδιωτικά μηνύματα σε"}. +{"Allow this person to register with the room?","Επιτρέπτε στο άτομο να καταχωρηθεί στην αίθουσα;"}. +{"Allow users to change the subject","Επιτρέψτε στους χρήστες να αλλάζουν το θέμα"}. +{"Allow users to query other users","Επιτρέψτε στους χρήστες να ερωτούν άλλους χρήστες"}. +{"Allow users to send invites","Επιτρέψτε στους χρήστες να αποστέλλουν προσκλήσεις"}. +{"Allow users to send private messages","Επιτρέψτε στους χρήστες να αποστέλλουν ιδιωτικά μηνύματα"}. +{"Allow visitors to change nickname","Επιτρέψτε στους επισκέπτες να αλλάζου ψευδώνυμο"}. +{"Allow visitors to send private messages to","Επιτρέψτε στους χρήστες να αποστέλλουν ιδιωτικά μηνύματα σε"}. {"Allow visitors to send status text in presence updates","Επιτρέψτε στους επισκέπτες να αποστέλλουν κατάσταση στις ενημερώσεις παρουσίας"}. {"Allow visitors to send voice requests","Επιτρέψτε στους επισκέπτες να στέλνουν αιτήματα φωνής"}. -{"All Users","Όλοι οι χρήστες"}. +{"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.","Μία σχετική LDAP ομάδα που καθορίζει τις συνδρομές στην αίθουσα: αυτό θα πρέπει να είναι ένα Διακεκριμένο Όνομα LDAP, σύμφωνα με τον προσδιορισμό κατεύθυνσης υλοποίησης ή ανάπτυξης της ομάδας."}. {"Announcements","Ανακοινώσεις"}. -{"anyone","οποιοσδήποτε"}. -{"A password is required to enter this room","Απαιτείται κωδικός πρόσβασης για είσοδο σε αυτή την αίθουσα"}. +{"Answer associated with a picture","Η απάντηση σχετίστηκε με μια εικόνα"}. +{"Answer associated with a video","Η απάντηση σχετίστηκε με ένα βίντεο"}. +{"Answer associated with speech","Η απάντηση σχετίστηκε με ομιλία"}. +{"Answer to a question","Απάντηση σε ερώτηση"}. +{"Anyone in the specified roster group(s) may subscribe and retrieve items","Οποιοσδήποτε στις ορισθείσες Λίστες Ομάδων δύναται να εγγραφεί και να παραλάβει αντικείμενα"}. +{"Anyone may associate leaf nodes with the collection","Οποιοσδήποτε μπορεί να συσχετίσει κόμβους φύλλων με τη συλλογή"}. +{"Anyone may publish","Οποιοσδήποτε δύναται να δημοσιέυει"}. +{"Anyone may subscribe and retrieve items","Οποιοσδήποτε δύναται να εγγραφεί και να παραλάβει αντικείμενα"}. +{"Anyone with a presence subscription of both or from may subscribe and retrieve items","Όποιος έχει συνδρομή παρουσίας και των δύο ή από μπορεί να εγγραφεί και να ανακτήσει στοιχεία"}. +{"Anyone with Voice","Οποιοσδήποτε με Φωνή"}. +{"Anyone","Οποιοσδήποτε"}. +{"API Commands","Εντολές του API"}. {"April","Απρίλιος"}. +{"Arguments","Επιχειρήματα"}. +{"Attribute 'channel' is required for this request","Το δηλωτικό 'channel' απαιτείται για αυτό το Ερώτημα"}. +{"Attribute 'id' is mandatory for MIX messages","Το δηλωτικό 'id' επιτακτικό για μηνύματα MIX"}. +{"Attribute 'jid' is not allowed here","Το δηλωτικό 'jid' δεν επιτρέπεται εδώ"}. +{"Attribute 'node' is not allowed here","Το δηλωτικό 'node' δεν επιτρέπεται εδώ"}. +{"Attribute 'to' of stanza that triggered challenge","Το δηλωτικό 'to' του Δωματίου που ξεκίνησε την πρόκληση"}. {"August","Αύγουστος"}. +{"Automatic node creation is not enabled","Η αυτόματη δημιουργία κόμβων δεν είναι ενεργοποιημένη"}. {"Backup Management","Διαχείριση Αντιγράφου Ασφαλείας"}. -{"Backup of ","Αντιγράφο Ασφαλείας του "}. +{"Backup of ~p","Αντιγράφο Ασφαλείας του ~p"}. {"Backup to File at ","Αποθήκευση Αντιγράφου Ασφαλείας σε Αρχείο στο "}. {"Backup","Αποθήκευση Αντιγράφου Ασφαλείας"}. {"Bad format","Ακατάλληλη μορφή"}. {"Birthday","Γενέθλια"}. -{"CAPTCHA web page","Ιστοσελίδα CAPTCHA "}. -{"Change Password","Αλλαγή κωδικού"}. -{"Change User Password","Αλλαγή Κωδικού Πρόσβασης Χρήστη"}. -{"Characters not allowed:","Χαρακτήρες δεν επιτρέπονται:"}. -{"Chatroom configuration modified","Διαμόρφωση Αίθουσaς σύνεδριασης τροποποιηθηκε"}. -{"Chatroom is created","Η αίθουσα σύνεδριασης δημιουργήθηκε"}. +{"Both the username and the resource are required","Τόσο το όνομα χρήστη όσο και ο πόρος είναι απαραίτητα"}. +{"Bytestream already activated","Το Bytestream έχει ήδη ενεργοποιηθεί"}. +{"Cannot remove active list","Δεν είναι δυνατή η κατάργηση της ενεργής λίστας"}. +{"Cannot remove default list","Δεν μπορείτε να καταργήσετε την προεπιλεγμένη λίστα"}. +{"CAPTCHA web page","Ιστοσελίδα CAPTCHA"}. +{"Challenge ID","Ταυτότητα Πρόκλησης"}. +{"Change Password","Αλλαγή κωδικού πρόσβασης"}. +{"Change User Password","Αλλαγή κωδικού πρόσβασης χρήστη"}. +{"Changing password is not allowed","Η αλλαγή του κωδικού πρόσβασης δεν επιτρέπεται"}. +{"Changing role/affiliation is not allowed","Η αλλαγή ρόλου/ομάδας δεν επιτρέπεται"}. +{"Channel already exists","Το κανάλι υπάρχει ήδη"}. +{"Channel does not exist","Το κανάλι δεν υπάρχει"}. +{"Channel JID","JID καναλιού"}. +{"Channels","Κανάλια"}. +{"Characters not allowed:","Χαρακτήρες που δεν επιτρέπονται:"}. +{"Chatroom configuration modified","Η ρύθμιση παραμέτρων της αίθουσας σύνεδριασης τροποποιηθηκε"}. +{"Chatroom is created","Η αίθουσα συνομιλίας έχει δημιουργηθεί"}. {"Chatroom is destroyed","Η αίθουσα σύνεδριασης διαγράφηκε"}. {"Chatroom is started","Η αίθουσα σύνεδριασης έχει ξεκινήσει"}. {"Chatroom is stopped","Η αίθουσα σύνεδριασης έχει σταματήσει"}. {"Chatrooms","Αίθουσες σύνεδριασης"}. {"Choose a username and password to register with this server","Επιλέξτε ένα όνομα χρήστη και κωδικό πρόσβασης για να εγγραφείτε σε αυτό τον διακομιστή"}. -{"Choose modules to stop","Επιλέξτε modules για να σταματήσουν"}. {"Choose storage type of tables","Επιλέξτε τύπο αποθήκευσης των πινάκων"}. {"Choose whether to approve this entity's subscription.","Επιλέξτε αν θα εγκρίθεί η εγγραφή αυτής της οντότητας."}. {"City","Πόλη"}. +{"Client acknowledged more stanzas than sent by server","Ο πελάτης γνωρίζει περισσότερα δωμάτια από αυτά που στάλθηκαν από τον εξυπηρετητή"}. +{"Clustering","Συσταδοποίηση"}. {"Commands","Εντολές"}. -{"Conference room does not exist","Αίθουσα σύνεδριασης δεν υπάρχει"}. -{"Configuration of room ~s","Διαμόρφωση Αίθουσας σύνεδριασης ~s"}. -{"Configuration","Διαμόρφωση"}. -{"Connected Resources:","Συνδεδεμένοι Πόροι:"}. -{"Connections parameters","Παράμετροι Συνδέσης"}. +{"Conference room does not exist","Η αίθουσα σύνεδριασης δεν υπάρχει"}. +{"Configuration of room ~s","Διαμόρφωση δωματίου ~ s"}. +{"Configuration","Ρύθμιση παραμέτρων"}. +{"Contact Addresses (normally, room owner or owners)","Διευθύνσεις της Επαφής (κανονικά, ιδιοκτήτης (-ες) αίθουσας)"}. {"Country","Χώρα"}. -{"CPU Time:","Ώρα CPU:"}. -{"Database Tables at ","Πίνακες βάσης δεδομένων στο "}. +{"Current Discussion Topic","Τρέχων θέμα συζήτησης"}. +{"Database failure","Αποτυχία βάσης δεδομένων"}. {"Database Tables Configuration at ","Διαμόρφωση Πίνακων βάσης δεδομένων στο "}. {"Database","Βάση δεδομένων"}. {"December","Δεκέμβριος"}. -{"Default users as participants","Προεπιλογη χρήστων ως συμμετέχοντες"}. +{"Default users as participants","Προρυθμισμένοι χρήστες ως συμμετέχοντες"}. {"Delete message of the day on all hosts","Διαγράψτε το μήνυμα της ημέρας σε όλους τους κεντρικούς υπολογιστές"}. {"Delete message of the day","Διαγράψτε το μήνυμα της ημέρας"}. -{"Delete Selected","Διαγραφή επιλεγμένων"}. {"Delete User","Διαγραφή Χρήστη"}. -{"Delete","Διαγραφή"}. -{"Deliver event notifications","Κοινοποιήσεις παράδοσης"}. -{"Deliver payloads with event notifications","Κοινοποιήσεις με την παράδοση φορτίων"}. -{"Description:","Περιγραφή:"}. +{"Deliver event notifications","Παράδοση ειδοποιήσεων συμβάντων"}. +{"Deliver payloads with event notifications","Κοινοποίηση φόρτου εργασιών με τις ειδοποιήσεις συμβάντων"}. {"Disc only copy","Αντίγραφο μόνο σε δίσκο"}. -{"Displayed Groups:","Εμφανίσμενες Ομάδες:"}. -{"Don't tell your password to anybody, not even the administrators of the Jabber server.","Μην πείτε τον κωδικό πρόσβασής σας σε κανέναν, ακόμη και στους διαχειριστές του διακομιστή Jabber."}. +{"Don't tell your password to anybody, not even the administrators of the XMPP server.","Μην πείτε τον κωδικό πρόσβασής σας σε κανέναν, ούτε στους διαχειριστές του διακομιστή XMPP."}. {"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","Δυναμικά προσδιορίστε το Απάντηση σε του εκδότη του αντικειμένου"}. {"Edit Properties","Επεξεργασία ιδιοτήτων"}. {"Either approve or decline the voice request.","Είτε εγκρίνετε ή απορρίψτε το αίτημα φωνής."}. -{"ejabberd IRC module","ejabberd IRC module"}. -{"ejabberd MUC module","ejabberd MUC module"}. +{"ejabberd HTTP Upload service","Υπηρεσία ανεβάσματος αρχείων του ejabberd"}. +{"ejabberd MUC module","ενότητα ejabberd MUC"}. +{"ejabberd Multicast service","υπηρεσία ejabberd Multicast"}. {"ejabberd Publish-Subscribe module","ejabberd module Δημοσίευσης-Εγγραφής"}. -{"ejabberd SOCKS5 Bytestreams module","ejabberd SOCKS5 Bytestreams module"}. +{"ejabberd SOCKS5 Bytestreams module","ενότητα ejabberd SOCKS5 Bytestreams"}. {"ejabberd vCard module","ejabberd vCard module"}. {"ejabberd Web Admin","ejabberd Web Admin"}. -{"Elements","Στοιχεία"}. -{"Email","Email"}. +{"ejabberd","ejabberd"}. +{"Email Address","Ηλεκτρονική Διεύθυνση"}. +{"Email","Ηλεκτρονικό ταχυδρομείο"}. +{"Enable hats","Ενεργοποίηση καπέλων"}. {"Enable logging","Ενεργοποίηση καταγραφής"}. -{"Encoding for server ~b","Κωδικοποίηση για διακομιστή ~b"}. +{"Enable message archiving","Ενεργοποιήστε την αρχειοθέτηση μηνυμάτων"}. +{"Enabling push without 'node' attribute is not supported","Η ενεργοποίηση της ώθησης χωρίς το χαρακτηριστικό 'κόμβος' δεν υποστηρίζεται"}. {"End User Session","Τερματισμός Συνεδρίας Χρήστη"}. -{"Enter list of {Module, [Options]}","Εισάγετε κατάλογο των (Module, [Επιλογές])"}. -{"Enter nickname you want to register","Πληκτρολογήστε το ψευδώνυμο που θέλετε να εγγραφείτε"}. +{"Enter nickname you want to register","Πληκτρολογήστε το ψευδώνυμο που θέλετε να καταχωρήσετε"}. {"Enter path to backup file","Εισάγετε τοποθεσία αρχείου αντιγράφου ασφαλείας"}. {"Enter path to jabberd14 spool dir","Εισάγετε κατάλογο αρχείων σειράς jabberd14"}. {"Enter path to jabberd14 spool file","Εισάγετε τοποθεσία αρχείου σειράς jabberd14"}. {"Enter path to text file","Εισάγετε Τοποθεσία Αρχείου Κειμένου"}. {"Enter the text you see","Πληκτρολογήστε το κείμενο που βλέπετε"}. -{"Enter username and encodings you wish to use for connecting to IRC servers. Press 'Next' to get more fields to fill in. Press 'Complete' to save settings.","Πληκτρολογήστε το όνομα χρήστη και κωδικοποιήσεις που θέλετε να χρησιμοποιήσετε για τη σύνδεση με διακομιστές IRC. Πατήστε 'Next' για να πάρετε περισσότερα πεδία να συμπληρώσετε. Πατήστε 'Complete' για να αποθηκεύσετε ρυθμίσεις."}. -{"Enter username, encodings, ports and passwords you wish to use for connecting to IRC servers","Εισάγετε το όνομα χρήστη, κωδικοποιήσεις, τις θύρες και τους κωδικούς πρόσβασης που θέλετε να χρησιμοποιήσετε για σύνδεση με IRC διακομιστή"}. -{"Erlang Jabber Server","Erlang Jabber Διακομιστής"}. -{"Error","Σφάλμα"}. -{"Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef.net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}].","Παράδειγμα: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef.net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]."}. -{"Exclude Jabber IDs from CAPTCHA challenge","Εξαίρεση από τις ταυτότητες Jabber, ή CAPTCHA πρόκληση"}. +{"Erlang XMPP Server","Διακομιστής Erlang XMPP"}. +{"Exclude Jabber IDs from CAPTCHA challenge","Εξαίρεσε αυτές τις ταυτότητες Jabber από την CAPTCHA πρόκληση"}. +{"Export all tables as SQL queries to a file:","Εξαγωγή όλων των πινάκων ως ερωτημάτων SQL σε ένα αρχείο:"}. {"Export data of all users in the server to PIEFXIS files (XEP-0227):","Εξαγωγή δεδομένων όλων των χρηστών του διακομιστή σε PIEFXIS αρχεία (XEP-0227):"}. {"Export data of users in a host to PIEFXIS files (XEP-0227):","Εξαγωγή δεδομένων των χρηστών κεντρικού υπολογιστή σε PIEFXIS αρχεία (XEP-0227):"}. +{"External component failure","Βλάβη εξωτερικού στοιχείου"}. +{"External component timeout","Τέλος χρονικού όριου εξωτερικού στοιχείου"}. +{"Failed to activate bytestream","Απέτυχε η ενεργοποίηση του bytestream"}. {"Failed to extract JID from your voice request approval","Απέτυχε η εξαγωγή JID από την έγκριση του αιτήματος φωνής σας"}. +{"Failed to map delegated namespace to external component","Αποτυχία ταξιθέτησης μεταγεγραμμένου χώρου ονομάτων σε εξωτερικό στοιχείο"}. +{"Failed to parse HTTP response","Αποτυχία ανάλυσης της απόκρισης HTTP"}. +{"Failed to process option '~s'","Αποτυχία επεξεργασίας της επιλογής '~s'"}. {"Family Name","Επώνυμο"}. +{"FAQ Entry","Καταχώριση συχνών ερωτήσεων"}. {"February","Φεβρουάριος"}. -{"Fill in fields to search for any matching Jabber User","Συμπληρώστε τα πεδία για να αναζητήσετε οποιαδήποτε ταιριάζοντα Jabber χρήστη"}. -{"Fill in the form to search for any matching Jabber User (Add * to the end of field to match substring)","Συμπληρώστε τη φόρμα για να αναζητήσετε οποιαδήποτε Jabber χρήστη που ταιριάζει (Προσθέστε * στο τέλος τού πεδίου για να ταιριάξει σε μεγαλύτερες γραμματοσηρές)"}. +{"File larger than ~w bytes","Αρχείο μεγαλύτερο από ~w bytes"}. +{"Fill in the form to search for any matching XMPP User","Συμπληρώστε την φόρμα για αναζήτηση χρηστών XMPP"}. {"Friday","Παρασκευή"}. -{"From ~s","Από ~s"}. -{"From","Από"}. +{"From ~ts","Από ~ts"}. +{"Full List of Room Admins","Πλήρης Κατάλογος Διαχειριστών αιθουσών"}. +{"Full List of Room Owners","Πλήρης Κατάλογος Ιδιοκτητών αιθουσών"}. {"Full Name","Ονοματεπώνυμο"}. +{"Get List of Online Users","Λίστα online χρηστών"}. +{"Get List of Registered Users","Λίστα εγγεγραμμένων χρηστών"}. {"Get Number of Online Users","Έκθεση αριθμού συνδεδεμένων χρηστών"}. {"Get Number of Registered Users","Έκθεση αριθμού εγγεγραμμένων χρηστών"}. +{"Get Pending","Λήψη των εκκρεμοτήτων"}. {"Get User Last Login Time","Έκθεση Τελευταίας Ώρας Σύνδεσης Χρήστη"}. -{"Get User Password","Έκθεση Κωδικού Πρόσβασης Χρήστη"}. {"Get User Statistics","Έκθεση Στατιστικών Χρήστη"}. +{"Given Name","Όνομα"}. {"Grant voice to this person?","Παραχώρηση φωνής σε αυτό το άτομο;"}. -{"Groups","Ομάδες"}. -{"Group ","Ομάδα"}. -{"has been banned","έχει απαγορευθεί"}. -{"has been kicked because of an affiliation change","Έχει αποβληθεί λόγω αλλαγής υπαγωγής"}. +{"has been banned","έχει αποβληθεί διαπαντώς"}. {"has been kicked because of a system shutdown","αποβλήθηκε λόγω τερματισμού συστήματος"}. +{"has been kicked because of an affiliation change","έχει αποβληθεί λόγω αλλαγής υπαγωγής"}. {"has been kicked because the room has been changed to members-only","αποβλήθηκε επειδή η αίθουσα αλλάξε γιά μέλη μόνο"}. -{"has been kicked","αποβλήθηκε "}. -{" has set the subject to: "," έχει θέσει το θέμα σε: "}. -{"Host","Κεντρικός Υπολογιστής"}. +{"has been kicked","αποβλήθηκε"}. +{"Hash of the vCard-temp avatar of this room","Hash του vCard-temp avatar αυτού του δωματίου"}. +{"Hat title","Τίτλος καπέλου"}. +{"Hat URI","Καπέλο URI"}. +{"Hats limit exceeded","Υπέρβαση του ορίου καπέλων"}. +{"Host unknown","Άγνωστος εξυπηρετητής"}. +{"HTTP File Upload","Ανέβασμα αρχείου"}. +{"Idle connection","Αδρανής σύνδεση"}. {"If you don't see the CAPTCHA image here, visit the web page.","Εάν δεν βλέπετε την εικόνα CAPTCHA εδώ, επισκεφθείτε την ιστοσελίδα."}. -{"If you want to specify different ports, passwords, encodings for IRC servers, fill this list with values in format '{\"irc server\", \"encoding\", port, \"password\"}'. By default this service use \"~s\" encoding, port ~p, empty password.","Εάν θέλετε να καθορίσετε διαφορετικές θύρες, κωδικούς πρόσβασης, κωδικοποιήσεις για IRC διακομιστές, εισάγετε πληροφορίες στη μορφή '{\"irc διακομιστής\", \"κωδικοποιήσεις\", θύρα, \"κωδικός πρόσβασης\"}'. Προεπιλεγμενα αυτή η υπηρεσία χρησιμοποιεί \"~s\" κωδικοποιήση, θύρα ~p, κενό κωδικό πρόσβασης."}. {"Import Directory","Εισαγωγή κατάλογου αρχείων"}. {"Import File","Εισαγωγή αρχείων"}. {"Import user data from jabberd14 spool file:","Εισαγωγή δεδομένων χρήστη από το αρχείο σειράς jabberd14:"}. @@ -138,284 +187,444 @@ {"Import users data from jabberd14 spool directory:","Εισαγωγή δεδομένων χρηστών από κατάλογο αρχείων σειράς jabberd14:"}. {"Import Users from Dir at ","Εισαγωγή χρηστών από κατάλογο αρχείων στο "}. {"Import Users From jabberd14 Spool Files","Εισαγωγή Χρηστών από αρχεία σειράς jabberd14"}. +{"Improper domain part of 'from' attribute","Ανάρμοστο τμήμα τομέα του χαρακτηριστικού 'from'"}. {"Improper message type","Ακατάλληλο είδος μηνύματος"}. +{"Incorrect CAPTCHA submit","Λάθος υποβολή CAPTCHA"}. +{"Incorrect data form","Εσφαλμένη φόρμα δεδομένων"}. {"Incorrect password","Εσφαλμένος κωδικός πρόσβασης"}. -{"Invalid affiliation: ~s","Άκυρη υπαγωγή: ~s"}. -{"Invalid role: ~s","Άκυρος ρόλο: ~s"}. +{"Incorrect value of 'action' attribute","Λανθασμένη τιμή του χαρακτηριστικού 'action'"}. +{"Incorrect value of 'action' in data form","Λανθασμένη τιμή 'action' στη φόρμα δεδομένων"}. +{"Incorrect value of 'path' in data form","Λανθασμένη τιμή 'path' στη φόρμα δεδομένων"}. +{"Installed Modules:","Εγκατεστημένες ενότητες:"}. +{"Install","Εγκατάσταση"}. +{"Insufficient privilege","Ανεπαρκή προνόμια"}. +{"Internal server error","Εσωτερικό σφάλμα"}. +{"Invalid 'from' attribute in forwarded message","Μη έγκυρο χαρακτηριστικό 'από' στο προωθούμενο μήνυμα"}. +{"Invalid node name","Μη έγκυρο όνομα κόμβου"}. +{"Invalid 'previd' value","Μη έγκυρη τιμή 'previd'"}. +{"Invitations are not allowed in this conference","Οι προσκλήσεις δεν επιτρέπονται σε αυτή τη διάσκεψη"}. {"IP addresses","Διευθύνσεις IP"}. -{"IP","IP"}. -{"IRC channel (don't put the first #)","IRC κανάλι (μην τεθεί το πρώτο #)"}. -{"IRC server","Διακομιστής IRC"}. -{"IRC settings","IRC Ρυθμίσεις"}. -{"IRC Transport","IRC Διαβιβάσεις"}. -{"IRC username","IRC όνομα χρήστη"}. -{"IRC Username","IRC Όνομα χρήστη"}. {"is now known as","είναι τώρα γνωστή ως"}. -{"It is not allowed to send private messages of type \"groupchat\"","Δεν επιτρέπεται να στείλει προσωπικά μηνύματα του τύπου \"groupchat\""}. +{"It is not allowed to send error messages to the room. The participant (~s) has sent an error message (~s) and got kicked from the room","Δεν επιτρέπεται η αποστολή μηνυμάτων σφάλματος στο δωμάτιο. Ο συμμετέχων (~s) έχει στείλει ένα μήνυμα σφάλματος (~s) και έχει πεταχτεί έξω από την αίθουσα"}. +{"It is not allowed to send private messages of type \"groupchat\"","Δεν επιτρέπεται η αποστολή προσωπικών μηνυμάτων του τύπου \"groupchat\""}. {"It is not allowed to send private messages to the conference","Δεν επιτρέπεται να στείλει προσωπικά μηνύματα για τη διάσκεψη"}. -{"It is not allowed to send private messages","Δεν επιτρέπεται η αποστολή προσωπικών μηνυμάτων"}. -{"Jabber Account Registration","Εγγραφή λογαριασμού Jabber"}. -{"Jabber ID ~s is invalid","Η Jabber Ταυτότητα ~s είναι άκυρη"}. {"Jabber ID","Ταυτότητα Jabber"}. {"January","Ιανουάριος"}. -{"Join IRC channel","Είσοδος στο IRC κανάλι"}. -{"joins the room","συνδέετε στην αίθουσα"}. -{"Join the IRC channel here.","Είσοδος στο κανάλι IRC εδώ."}. -{"Join the IRC channel in this Jabber ID: ~s","Είσοδος στο κανάλι IRC αυτής της Jabber Ταυτότητας: ~s"}. +{"JID normalization denied by service policy","Απετράπη η κανονικοποίηση του JID, λόγω της τακτικής Παροχής Υπηρεσιών"}. +{"JID normalization failed","Απετράπη η κανονικοποίηση του JID"}. +{"Joined MIX channels of ~ts","Ενσωματωμένα κανάλια MIX του ~ts"}. +{"Joined MIX channels:","Ενσωματωμένα κανάλια MIX:"}. +{"joins the room","συνδέεται στην αίθουσα"}. {"July","Ιούλιος"}. {"June","Ιούνιος"}. +{"Just created","Μόλις δημιουργήθηκε"}. {"Last Activity","Τελευταία Δραστηριότητα"}. {"Last login","Τελευταία σύνδεση"}. +{"Last message","Τελευταίο μήνυμα"}. {"Last month","Περασμένο μήνα"}. {"Last year","Πέρυσι"}. +{"Least significant bits of SHA-256 hash of text should equal hexadecimal label","Τα ψηφία μικρότερης αξίας του αθροίσματος SHA-256 του κειμένου θα έπρεπε να ισούνται με την δεκαεξαδική ετικέτα"}. {"leaves the room","εγκαταλείπει την αίθουσα"}. -{"Listened Ports at ","Παρακολουθούμενες Θύρες στο "}. -{"Listened Ports","Παρακολουθούμενες Θύρες"}. -{"List of modules to start","Λίστα των Module για Εκκίνηση"}. -{"Low level update script","Προγράμα ενημέρωσης χαμηλού επίπεδου "}. -{"Make participants list public","Κάντε κοινό τον κατάλογο συμμετεχόντων"}. -{"Make room CAPTCHA protected","Κάντε την αίθουσα CAPTCHA προστατεύονομενη"}. +{"List of users with hats","Λίστα των χρηστών με καπέλα"}. +{"List users with hats","Λίστα χρηστών με καπέλα"}. +{"Logged Out","Αποσυνδεδεμένος"}. +{"Logging","Καταγραφή"}. +{"Make participants list public","Κάντε δημόσιο τον κατάλογο συμμετεχόντων"}. +{"Make room CAPTCHA protected","Κάντε την αίθουσα προστατεύομενη με CAPTCHA"}. {"Make room members-only","Κάντε την αίθουσα μόνο για μέλη"}. -{"Make room moderated","Κάντε την αίθουσα εποπτεύονομενη"}. +{"Make room moderated","Κάντε την αίθουσα εποπτεύομενη"}. {"Make room password protected","Κάντε την αίθουσα προστατεύομενη με κωδικό πρόσβασης"}. -{"Make room persistent","Κάντε αίθουσα μόνιμη"}. +{"Make room persistent","Κάντε την αίθουσα μόνιμη"}. {"Make room public searchable","Κάντε την δημόσια αναζήτηση δυνατή για αυτή την αίθουσα"}. +{"Malformed username","Λανθασμένη μορφή ονόματος χρήστη"}. +{"MAM preference modification denied by service policy","Άρνηση αλλαγής προτιμήσεων MAM, λόγω της τακτικής Παροχής Υπηρεσιών"}. {"March","Μάρτιος"}. -{"Maximum Number of Occupants","Μέγιστος αριθμός συμετεχόντων"}. -{"Max # of items to persist","Μέγιστος αριθμός μόνιμων στοιχείων"}. +{"Max # of items to persist, or `max` for no specific limit other than a server imposed maximum","Μέγιστος αριθμός στοιχείων που πρέπει να παραμείνουν, ή « Μέγιστο » για κανένα συγκεκριμένο όριο εκτός από το μέγιστο που επιβάλλει ο διακομιστής"}. {"Max payload size in bytes","Μέγιστο μέγεθος φορτίου σε bytes"}. +{"Maximum file size","Μέγιστο μέγεθος αρχείου"}. +{"Maximum Number of History Messages Returned by Room","Μέγιστος αριθμός μηνυμάτων Ιστορικού που επιστρέφονται από την Αίθουσα"}. +{"Maximum number of items to persist","Μέγιστος αριθμός μόνιμων στοιχείων"}. +{"Maximum Number of Occupants","Μέγιστος αριθμός συμμετεχόντων"}. {"May","Μάιος"}. {"Membership is required to enter this room","Απαιτείται αίτηση συμετοχής για είσοδο σε αυτή την αίθουσα"}. -{"Members:","Μέλη:"}. -{"Memorize your password, or write it in a paper placed in a safe place. In Jabber there isn't an automated way to recover your password if you forget it.","Απομνημονεύστε τον κωδικό πρόσβασής σας, ή γράψετε τον σε ένα χαρτί που είχε τοποθετηθεί σε ασφαλές μέρος. Στο Jabber δεν υπάρχει αυτοματοποιημένος τρόπος για να ανακτήσετε τον κωδικό σας αν τον ξεχάσετε."}. -{"Memory","Μνήμη"}. -{"Message body","Περιεχόμενο μηνυμάτως"}. +{"Memorize your password, or write it in a paper placed in a safe place. In XMPP there isn't an automated way to recover your password if you forget it.","Απομνημονεύστε τον κωδικό πρόσβασής σας ή γράψτε τον σε χαρτί που βρίσκεται σε ασφαλές μέρος. Στο XMPP δεν υπάρχει αυτοματοποιημένος τρόπος ανάκτησης του κωδικού πρόσβασής σας εάν τον ξεχάσετε."}. +{"Mere Availability in XMPP (No Show Value)","Διαθεσιμότητα στο XMPP (Χωρίς ένδειξη)"}. +{"Message body","Περιεχόμενο μηνύματος"}. +{"Message not found in forwarded payload","Δεν βρέθηκε μήνυμα στον προωθημένο φόρτο εργασίας"}. +{"Messages from strangers are rejected","Τα μηνύματα από αγνώστους απορρίπτονται"}. +{"Messages of type headline","Μηνύματα του τύπου headline"}. +{"Messages of type normal","Μηνύματα του τύπου normal"}. {"Middle Name","Πατρώνυμο"}. {"Minimum interval between voice requests (in seconds)","Ελάχιστο χρονικό διάστημα μεταξύ αιτημάτων φωνής (σε δευτερόλεπτα)"}. -{"Moderator privileges required","Aπαιτούνται προνόμια συντονιστή"}. -{"moderators only","συντονιστές μόνο"}. -{"Modified modules","Τροποποιημένα modules"}. -{"Module","Module"}. -{"Modules at ","Modules στο "}. -{"Modules","Modules"}. +{"Moderator privileges required","Aπαιτούνται προνόμια επόπτου"}. +{"Moderators Only","Επόπτες μόμον"}. +{"Moderator","Επόπτης"}. +{"Module failed to handle the query","Το module απέτυχε να χειριστεί το ερώτημα"}. {"Monday","Δευτέρα"}. -{"Name:","Όνομα:"}. +{"Multicast","Πολλαπλή διανομή (Multicast)"}. +{"Multiple elements are not allowed by RFC6121","Πολλαπλά στοιχεία δεν επιτρέπονται από το RFC6121"}. +{"Multi-User Chat","Συνομιλία με πολλούς χρήστες"}. {"Name","Όνομα"}. +{"Natural Language for Room Discussions","Μητρική Γλώσσα για τις Συζητήσεις Αιθουσών"}. +{"Natural-Language Room Name","Αίθουσα Μητρικής Γλώσσας"}. +{"Neither 'jid' nor 'nick' attribute found","Δεν βρέθηκε κανένα χαρακτηριστικό 'jid' ούτε 'nick'"}. +{"Neither 'role' nor 'affiliation' attribute found","Δεν βρέθηκε ούτε χαρακτηριστικό 'role' ούτε 'affiliation'"}. {"Never","Ποτέ"}. {"New Password:","Νέος κωδικός πρόσβασης:"}. +{"Nickname can't be empty","Το Ψευδώνυμο δεν μπορεί να είναι άδειο"}. {"Nickname Registration at ","Εγγραφή με Ψευδώνυμο στο "}. {"Nickname ~s does not exist in the room","Ψευδώνυμο ~s δεν υπάρχει σε αυτή την αίθουσα"}. {"Nickname","Ψευδώνυμο"}. -{"No body provided for announce message","Δεν προμηθεύτικε περιεχόμενο ανακοινώσης"}. -{"nobody","κανείς"}. +{"No address elements found","Δεν βρέθηκαν στοιχεία διεύθυνσης"}. +{"No addresses element found","Δεν βρέθηκε στοιχείο διεύθυνσης"}. +{"No 'affiliation' attribute found","Δεν βρέθηκε το χαρακτηριστικό 'affiliation'"}. +{"No available resource found","Δεν βρέθηκε διαθέσιμος πόρος"}. +{"No body provided for announce message","Δεν προμηθεύτηκε περιεχόμενο ανακοινώσης"}. +{"No child elements found","Δεν βρέθηκαν θυγατρικά στοιχεία"}. +{"No data form found","Δεν βρέθηκε φόρμα δεδομένων"}. {"No Data","Κανένα στοιχείο"}. -{"Node ID","Ταυτότητα Κόμβου"}. -{"Node not found","Κόμβος δεν βρέθηκε"}. -{"Nodes","Κόμβοι"}. -{"Node ","Κόμβος"}. +{"No features available","Δεν υπάρχουν διαθέσιμες λειτουργίες"}. +{"No element found","Δεν βρέθηκε στοιχείο "}. +{"No hook has processed this command","Κανένα άγκιστρο δεν έχει επεξεργαστεί αυτήν την εντολή"}. +{"No info about last activity found","Δεν βρέθηκαν πληροφορίες για την τελευταία δραστηριότητα"}. +{"No 'item' element found","Δεν βρέθηκε το στοιχείο 'item'"}. +{"No items found in this query","Δεν βρέθηκαν στοιχεία σε αυτό το ερώτημα"}. {"No limit","Χωρίς όριο"}. +{"No module is handling this query","Κανένα module δεν χειρίζεται αυτό το ερώτημα"}. +{"No node specified","Δεν καθορίστηκε κόμβος"}. +{"No 'password' found in data form","Δεν υπάρχει 'password' στη φόρμα δεδομένων"}. +{"No 'password' found in this query","Δεν βρέθηκε 'password' σε αυτό το ερώτημα"}. +{"No 'path' found in data form","Δεν υπάρχει 'path' στη φόρμα δεδομένων"}. +{"No pending subscriptions found","Δεν βρέθηκαν εκκρεμείς συνδρομές"}. +{"No privacy list with this name found","Δεν βρέθηκε κατάλογος απορρήτου με αυτό το όνομα"}. +{"No private data found in this query","Δεν βρέθηκαν ιδιωτικά δεδομένα σε αυτό το ερώτημα"}. +{"No running node found","Δεν βρέθηκε ενεργός κόμβος"}. +{"No services available","Δεν υπάρχουν διαθέσιμες υπηρεσίες"}. +{"No statistics found for this item","Δεν βρέθηκαν στατιστικά στοιχεία για αυτό το στοιχείο"}. +{"No 'to' attribute found in the invitation","Δε βρέθηκε το χαρακτηριστικό 'to' στην πρόσκληση"}. +{"Nobody","Κανείς"}. +{"Node already exists","Ο κόμβος υπάρχει ήδη"}. +{"Node ID","Ταυτότητα Κόμβου"}. +{"Node index not found","Ο δείκτης κόμβου δεν βρέθηκε"}. +{"Node not found","Κόμβος δεν βρέθηκε"}. +{"Node ~p","Κόμβος ~p"}. +{"Nodeprep has failed","Το Nodeprep απέτυχε"}. +{"Nodes","Κόμβοι"}. +{"Node","Κόμβος"}. {"None","Κανένα"}. -{"No resource provided","Δεν προμηθεύτικε πόρος"}. -{"Not Found","Δεν Βρέθηκε"}. -{"Notify subscribers when items are removed from the node","Ειδοποιηση στους συνδρομητές όταν αφαίρούντε στοιχεία από τον κόμβο"}. -{"Notify subscribers when the node configuration changes","Ειδοποιηση στους συνδρομητές όταν αλλάζει η διαμόρφωση κόμβου"}. -{"Notify subscribers when the node is deleted","Ειδοποιηση στους συνδρομητές όταν ο κόμβος διαγράφεται"}. +{"Not allowed","Δεν επιτρέπεται"}. +{"Not Found","Δε βρέθηκε"}. +{"Not subscribed","Δεν έχετε εγγραφεί"}. +{"Notify subscribers when items are removed from the node","Ειδοποιήστε τους συνδρομητές όταν αφαιρούνται στοιχεία από τον κόμβο"}. +{"Notify subscribers when the node configuration changes","Ειδοποίηση στους συνδρομητές όταν αλλάζει η διαμόρφωση κόμβου"}. +{"Notify subscribers when the node is deleted","Ειδοποίηση στους συνδρομητές όταν ο κόμβος διαγράφεται"}. {"November","Νοέμβριος"}. +{"Number of answers required","Πλήθος αναζητημένων ερωτημάτων"}. {"Number of occupants","Αριθμός συμετεχόντων"}. +{"Number of Offline Messages","Πλήθος μηνυμάτων Χωρίς Σύνδεση"}. {"Number of online users","Αριθμός συνδεδεμένων χρηστών"}. {"Number of registered users","Αριθμός εγγεγραμμένων χρηστών"}. +{"Number of seconds after which to automatically purge items, or `max` for no specific limit other than a server imposed maximum","Αριθμός δευτερολέπτων μετά από τα οποία θα καθαρίζονται αυτόματα τα στοιχεία, ή `max` για κανένα συγκεκριμένο όριο εκτός από το μέγιστο που επιβάλλει ο διακομιστής"}. +{"Occupants are allowed to invite others","Οι συμμετέχοντες μπορούν να προσκαλέσουν και άλλους"}. +{"Occupants are allowed to query others","Οι κάτοικοι επιτρέπεται να ρωτούν άλλους"}. +{"Occupants May Change the Subject","Επιτρέψτε στους χρήστες να αλλάζουν το Θέμα"}. {"October","Οκτώβριος"}. -{"Offline Messages:","Χωρίς Σύνδεση Μηνύματα:"}. -{"Offline Messages","Χωρίς Σύνδεση Μηνύματα"}. -{"OK","Όλλα Καλά"}. +{"OK","Εντάξει"}. {"Old Password:","Παλαιός κωδικός πρόσβασης:"}. -{"Online Users:","Online Χρήστες:"}. {"Online Users","Συνδεμένοι χρήστες"}. {"Online","Συνδεδεμένο"}. -{"Only deliver notifications to available users","Παράδωση κοινοποιήσεων μόνο σε διαθέσιμους χρήστες"}. +{"Only collection node owners may associate leaf nodes with the collection","Μόνον οι ιδιοκτήτες των κόμβων μπορούν να συσχετίσουν leaf nodes με την Συλλογή"}. +{"Only deliver notifications to available users","Παράδοση ειδοποιήσεων μόνο σε διαθέσιμους χρήστες"}. +{"Only or tags are allowed","Επιτρέπονται μόνο tags ή "}. +{"Only element is allowed in this query","Στο ερώτημα αυτό επιτρέπεται μόνο το στοιχείο "}. +{"Only members may query archives of this room","Μόνο μέλη μπορούν να δούνε τα αρχεία αυτής της αίθουσας"}. {"Only moderators and participants are allowed to change the subject in this room","Μόνο οι συντονιστές και οι συμμετέχοντες μπορούν να αλλάξουν το θέμα αυτής της αίθουσας"}. {"Only moderators are allowed to change the subject in this room","Μόνο οι συντονιστές μπορούν να αλλάξουν το θέμα αυτής της αίθουσας"}. +{"Only moderators are allowed to retract messages","Μόνο οι συντονιστές επιτρέπεται να αποσύρουν μηνύματα"}. {"Only moderators can approve voice requests","Μόνο οι συντονιστές μπορούν να εγκρίνουν τις αιτήσεις φωνής"}. -{"Only occupants are allowed to send messages to the conference","Μόνο οι συμμετέχωντες μπορούν να στέλνουν μηνύματα στο συνέδριο"}. -{"Only occupants are allowed to send queries to the conference","Μόνο οι συμετεχόντες μπορούν να στείλουν ερωτήματα στη διάσκεψη"}. +{"Only occupants are allowed to send messages to the conference","Μόνο οι συμμετέχοντες επιτρέπεται να στέλνουν μηνύματα στο συνέδριο"}. +{"Only occupants are allowed to send queries to the conference","Μόνο οι συμμετέχοντες επιτρέπεται να στείλουν ερωτήματα στη διάσκεψη"}. +{"Only publishers may publish","Μόνον εκδότες μπορούν να δημοσιεύσουν"}. {"Only service administrators are allowed to send service messages","Μόνο οι διαχειριστές των υπηρεσιών επιτρέπεται να στείλουν υπηρεσιακά μηνύματα"}. -{"Options","Επιλογές"}. +{"Only those on a whitelist may associate leaf nodes with the collection","Μόνον οι εξαιρεθέντες μπορούν να συσχετίσουν leaf nodes με τη συλλογή"}. +{"Only those on a whitelist may subscribe and retrieve items","Μόνο όσοι βρίσκονται στη λίστα επιτρεπόμενων μπορούν να εγγραφούν και να ανακτήσουν αντικείμενα"}. {"Organization Name","Όνομα Οργανισμού"}. {"Organization Unit","Μονάδα Οργανισμού"}. -{"Outgoing s2s Connections:","Εξερχόμενες S2S Συνδέσεις:"}. +{"Other Modules Available:","Διαθέσιμες άλλες ενότητες:"}. {"Outgoing s2s Connections","Εξερχόμενες S2S Συνδέσεις"}. -{"Outgoing s2s Servers:","Εξερχόμενοι S2S διακομιστές:"}. {"Owner privileges required","Aπαιτούνται προνόμια ιδιοκτήτη"}. -{"Packet","Πακέτο"}. -{"Password ~b","Κωδικός πρόσβασης ~b"}. -{"Password Verification:","Επαλήθευση κωδικού πρόσβασης:"}. +{"Packet relay is denied by service policy","Απαγορεύεται η αναμετάδοση πακέτων, λόγω της τακτικής Παροχής Υπηρεσιών"}. +{"Participant ID","ID συμμετέχοντος"}. +{"Participant","Συμμετέχων"}. {"Password Verification","Επαλήθευση κωδικού πρόσβασης"}. -{"Password","Κωδικός Πρόσβασης"}. +{"Password Verification:","Επαλήθευση κωδικού πρόσβασης:"}. +{"Password","Κωδικός πρόσβασης"}. {"Password:","Κωδικός πρόσβασης:"}. {"Path to Dir","Τοποθεσία κατάλογου αρχείων"}. {"Path to File","Τοποθεσία Αρχείου"}. -{"Pending","Εκκρεμεί"}. +{"Payload semantic type information","Πληροφορίες σημασιολογικού τύπου ωφέλιμου φορτίου"}. {"Period: ","Περίοδος: "}. -{"Persist items to storage","Μονιμη αποθήκευση στοιχείων"}. -{"Ping","Πινγκ"}. -{"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Παρακαλώ σημειώστε ότι οι επιλογές αυτές θα αποθήκευσουν Αντιγράφο Ασφαλείας μόνο της ενσωματωμένης βάσης δεδομένων Mnesia. Εάν χρησιμοποιείτε το module ODBC, θα πρέπει επίσης να κάνετε χωριστά Αντιγράφο Ασφαλείας της SQL βάση δεδομένων σας ."}. +{"Persist items to storage","Μόνιμη αποθήκευση στοιχείων"}. +{"Persistent","Μόνιμη"}. +{"Ping query is incorrect","Το Ping είναι λανθασμένο"}. +{"Ping","Ping"}. +{"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Παρακαλώ σημειώστε ότι οι επιλογές αυτές θα αποθήκευσουν Αντιγράφο Ασφαλείας μόνο της ενσωματωμένης βάσης δεδομένων Mnesia. Εάν χρησιμοποιείτε το module ODBC, θα πρέπει επίσης να κάνετε χωριστά Αντιγράφο Ασφαλείας της SQL βάσης δεδομένων σας."}. {"Please, wait for a while before sending new voice request","Παρακαλώ, περιμένετε για λίγο πριν την αποστολή νέου αιτήματος φωνής"}. {"Pong","Πονγκ"}. -{"Port ~b","Θύρα ~b"}. -{"Port","Θύρα"}. +{"Possessing 'ask' attribute is not allowed by RFC6121","Η κατοχή χαρακτηριστικού \"ask\" δεν επιτρέπεται από το RFC6121"}. {"Present real Jabber IDs to","Παρούσιαση πραγματικών ταυτοτήτων Jabber σε"}. -{"private, ","ιδιωτικό,"}. -{"Protocol","Πρωτόκολλο"}. +{"Previous session not found","Η προηγούμενη περίοδος σύνδεσης χρήστη δεν βρέθηκε"}. +{"Previous session PID has been killed","Το προηγούμενο αναγνωριστικό περιόδου σύνδεσης PID αφαιρέθηκε"}. +{"Previous session PID has exited","Το προηγούμενο αναγνωριστικό περιόδου σύνδεσης PID τερμάτισε"}. +{"Previous session PID is dead","Το προηγούμενο αναγνωριστικό περιόδου σύνδεσης PID είναι νεκρό"}. +{"Previous session timed out","Η προηγούμενη περίοδος σύνδεσης χρήστη έληξε"}. +{"private, ","ιδιωτικό, "}. +{"Public","Δημόσιο"}. +{"Publish model","Δημοσιεύστε μοντέλο"}. {"Publish-Subscribe","Δημοσίευση-Εγγραφή"}. {"PubSub subscriber request","Αίτηση συνδρομητή Δημοσίευσης-Εγγραφής"}. {"Purge all items when the relevant publisher goes offline","Διαγραφή όλων των στοιχείων όταν ο σχετικός εκδότης αποσυνδέεται"}. -{"Queries to the conference members are not allowed in this room","Ερωτήματα πρώς τα μέλη της διασκέψεως δεν επιτρέπονται σε αυτήν την αίθουσα"}. +{"Push record not found","Το αρχείο Ώσης δεν βρέθηκε"}. +{"Queries to the conference members are not allowed in this room","Ερωτήματα προς τα μέλη της διασκέψεως δεν επιτρέπονται σε αυτήν την αίθουσα"}. +{"Query to another users is forbidden","Το ερώτημα σε άλλους χρήστες είναι απαγορευμένο"}. {"RAM and disc copy","Αντίγραφο μόνο σε RAM καί δίσκο"}. {"RAM copy","Αντίγραφο σε RAM"}. -{"Raw","Ακατέργαστο"}. -{"Really delete message of the day?","Πραγματικά να διαγράψετε το μήνυμα της ημέρας;"}. -{"Recipient is not in the conference room","Παραλήπτης δεν είναι στην αίθουσα συνεδριάσεων"}. -{"Register a Jabber account","Καταχωρήστε έναν λογαριασμό Jabber"}. -{"Registered Users:","Εγγεγραμμένοι Χρήστες:"}. -{"Registered Users","Εγγεγραμμένοι Χρήστες"}. +{"Really delete message of the day?","Πραγματικά να διαγραφεί το μήνυμα της ημέρας;"}. +{"Receive notification from all descendent nodes","Λάβετε ειδοποίηση από όλους τους υπό-κόμβους"}. +{"Receive notification from direct child nodes only","Λάβετε ειδοποίηση μόνο από direct child κόμβους"}. +{"Receive notification of new items only","Λάβετε ειδοποίηση μόνο από νέα αντικείμενα"}. +{"Receive notification of new nodes only","Λάβετε ειδοποίηση μόνο από νέους κόμβους"}. +{"Recipient is not in the conference room","Ο παραλήπτης δεν είναι στην αίθουσα συνεδριάσεων"}. +{"Register an XMPP account","Καταχωρείστε έναν XMPP λογαριασμό χρήστη"}. {"Register","Καταχωρήστε"}. -{"Registration in mod_irc for ","Εγγραφή στο mod_irc για "}. -{"Remote copy","Απομεμακρυσμένο αντίγραφο"}. -{"Remove All Offline Messages","Αφαίρεση Όλων των Χωρίς Σύνδεση Μηνύματων"}. +{"Remote copy","Εξ αποστάσεως αντίγραφο"}. +{"Remove a hat from a user","Αφαίρεση ενός καπέλου από έναν χρήστη"}. {"Remove User","Αφαίρεση χρήστη"}. -{"Remove","Αφαίρεστε"}. -{"Replaced by new connection","Αντικαταστάθικε από νέα σύνδεση"}. +{"Replaced by new connection","Αντικαταστάθηκε από μια νέα σύνδεση"}. +{"Request has timed out","Το αίτημα έληξε"}. +{"Request is ignored","Το αίτημα θα αγνοηθεί"}. +{"Requested role","Αιτούμενος ρόλος"}. {"Resources","Πόροι"}. {"Restart Service","Επανεκκίνηση Υπηρεσίας"}. -{"Restart","Επανεκκίνηση"}. {"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:","Επαναφορά αντιγράφου ασφαλείας από αρχείο κειμένου αμέσως:"}. {"Restore","Επαναφορά Αντιγράφου Ασφαλείας"}. +{"Result","Αποτέλεσμα"}. +{"Roles and Affiliations that May Retrieve Member List","Ρόλοι και δεσμοί που μπορούν να λάβουν την λίστα μελών"}. +{"Roles for which Presence is Broadcasted","Ρόλοι των οποίων η παρουσία δηλώνεται δημόσια"}. +{"Roles that May Send Private Messages","Ρόλοι που επιτρέπεται να αποστέλλουν ιδιωτικά μηνύματα"}. {"Room Configuration","Διαμόρφωση Αίθουσας σύνεδριασης"}. -{"Room creation is denied by service policy","Άρνηση δημιουργίας αίθουσας , λόγω τακτικής παροχής υπηρεσιών"}. -{"Room description","Περιγραφή Αίθουσας"}. -{"Room Occupants","Συμετεχόντες Αίθουσας σύνεδριασης"}. -{"Room title","Τίτλος Αίθουσας "}. +{"Room creation is denied by service policy","Άρνηση δημιουργίας αίθουσας, λόγω της τακτικής Παροχής Υπηρεσιών"}. +{"Room description","Περιγραφή αίθουσας"}. +{"Room Occupants","Συμμετεχόντες Αίθουσας σύνεδριασης"}. +{"Room terminates","Τερματισμός Αίθουσας"}. +{"Room title","Τίτλος Αίθουσας"}. {"Roster groups allowed to subscribe","Ομάδες Καταλόγου Επαφών μπορούν να εγγραφούν"}. -{"Roster of ","Καταλόγος Επαφών τού"}. {"Roster size","Μέγεθος Καταλόγου Επαφών"}. -{"Roster","Καταλόγος Επαφών"}. -{"RPC Call Error","Σφάλμα RPC Κλήσης"}. {"Running Nodes","Ενεργοί Κόμβοι"}. -{"~s access rule configuration","~s διαμόρφωση κανόνα πρόσβασης"}. +{"~s invites you to the room ~s","~s Σας καλεί στο δωμάτιο ~s"}. {"Saturday","Σάββατο"}. -{"Script check","Script ελέγχου"}. +{"Search from the date","Αναζήτηση από της"}. {"Search Results for ","Αποτελέσματα αναζήτησης για "}. -{"Search users in ","Αναζήτηση χρηστών στο"}. +{"Search the text","Αναζήτηση του κειμένου"}. +{"Search until the date","Αναζήτηση μέχρι της"}. +{"Search users in ","Αναζήτηση χρηστών στο "}. {"Send announcement to all online users on all hosts","Αποστολή ανακοίνωσης σε όλους τους συνδεδεμένους χρήστες σε όλους τους κεντρικούς υπολογιστές"}. {"Send announcement to all online users","Αποστολή ανακοίνωσης σε όλους τους συνδεδεμένους χρήστες"}. {"Send announcement to all users on all hosts","Αποστολή ανακοίνωσης σε όλους τους χρήστες σε όλους τους κεντρικούς υπολογιστές"}. {"Send announcement to all users","Αποστολή ανακοίνωσης σε όλους τους χρήστες"}. {"September","Σεπτέμβριος"}. -{"Server ~b","Διακομιστής ~b"}. {"Server:","Διακομιστής:"}. +{"Service list retrieval timed out","Η λήψη της λίστας Υπηρεσιών έληξε"}. +{"Session state copying timed out","Η αντιγραφή της καταστασης περιόδου σύνδεσης έληξε"}. {"Set message of the day and send to online users","Ορίστε μήνυμα ημέρας και αποστολή στους συνδεδεμένους χρήστες"}. {"Set message of the day on all hosts and send to online users","Ορίστε μήνυμα ημέρας και άμεση αποστολή στους συνδεδεμένους χρήστες σε όλους τους κεντρικούς υπολογιστές"}. {"Shared Roster Groups","Κοινές Ομάδες Καταλόγων Επαφών"}. {"Show Integral Table","Δείτε Ολοκληρωτικό Πίνακα"}. +{"Show Occupants Join/Leave","Εμφάνιση ενοίκων Join/Leave"}. {"Show Ordinary Table","Δείτε Κοινό Πίνακα"}. -{"Shut Down Service","Κλείσιμο Υπηρεσίας"}. -{"~s invites you to the room ~s","~s σας προσκαλεί στην αίθουσα ~s"}. -{"Some Jabber clients can store your password in your computer. Use that feature only if you trust your computer is safe.","Μερικοί πελάτες Jabber μπορεί να αποθηκεύσουν τον κωδικό πρόσβασής σας στον υπολογιστή σας. Χρησιμοποιήστε αυτό το χαρακτηριστικό μόνο εάν εμπιστεύεστε την ασφάλεια του υπολογιστή σας."}. +{"Shut Down Service","Τερματισμός Υπηρεσίας"}. +{"SOCKS5 Bytestreams","Bytestreams του SOCKS5"}. +{"Some XMPP clients can store your password in the computer, but you should do this only in your personal computer for safety reasons.","Ορισμένοι πελάτες XMPP μπορούν να αποθηκεύσουν τον κωδικό πρόσβασής σας στον υπολογιστή, αλλά θα πρέπει να το κάνετε μόνο στον προσωπικό σας υπολογιστή για λόγους ασφαλείας."}. +{"Sources Specs:","Πηγές Προδιαγραφές:"}. {"Specify the access model","Καθορίστε το μοντέλο πρόσβασης"}. {"Specify the event message type","Καθορίστε τον τύπο μηνύματος συμβάντος"}. {"Specify the publisher model","Καθορίστε το μοντέλο εκδότη"}. -{"~s's Offline Messages Queue","Η Σειρά Χωρίς Σύνδεση Μηνύματων τού ~s"}. -{"Start Modules at ","Εκκίνηση Modules στο "}. -{"Start Modules","Εκκίνηση Modules"}. -{"Start","Εκκίνηση"}. -{"Statistics of ~p","Στατιστικές του ~p"}. -{"Statistics","Στατιστικές"}. -{"Stop Modules at ","Παύση Modules στο "}. -{"Stop Modules","ΠαύσηModules"}. +{"Stanza id is not valid","Το Stanza id δεν είναι έγκυρο"}. +{"Stanza ID","Ταυτότητα Δωματίου"}. +{"Statically specify a replyto of the node owner(s)","Προσδιορίστε (στατικά) το Απάντηση Προς του ιδιοκτήτη-ων του κόμβου"}. {"Stopped Nodes","Σταματημένοι Κόμβοι"}. -{"Stop","Σταμάτημα"}. -{"Storage Type","Τύπος Αποθήκευσης"}. {"Store binary backup:","Αποθηκεύση δυαδικού αντιγράφου ασφαλείας:"}. {"Store plain text backup:","Αποθηκεύση αντιγράφου ασφαλείας σε αρχείο κειμένου:"}. +{"Stream management is already enabled","Η διαχείριση Ροών επιτρέπεται ηδη"}. +{"Stream management is not enabled","Η διαχείριση Ροών δεν είναι ενεργοποιημένη"}. {"Subject","Θέμα"}. {"Submitted","Υποβλήθηκε"}. -{"Submit","Υποβοβολή"}. {"Subscriber Address","Διεύθυνση Συνδρομητή"}. -{"Subscription","Συνδρομή"}. +{"Subscribers may publish","Οι συνδρομητές μπορούν να δημοσιεύσουν"}. +{"Subscription requests must be approved and only subscribers may retrieve items","Τα αιτήματα για συνδρομή πρέπει να εγκριθούν και μόνο οι συνδρομητές μπορούν να λάβουν αντικείμενα"}. +{"Subscriptions are not allowed","Οι συνδρομές δεν επιτρέπονται"}. {"Sunday","Κυριακή"}. -{"That nickname is already in use by another occupant","Αυτό το ψευδώνυμο είναι ήδη σε χρήση από άλλον συμμετέχων"}. +{"Text associated with a picture","Το κείμενο σχετίστηκε με μία εικόνα"}. +{"Text associated with a sound","Το κείμενο σχετίστηκε με έναν ήχο"}. +{"Text associated with a video","Το κείμενο σχετίστηκε με ένα βίντεο"}. +{"Text associated with speech","Το κείμενο σχετίστηκε με ομιλία"}. +{"That nickname is already in use by another occupant","Αυτό το ψευδώνυμο είναι ήδη σε χρήση από άλλον συμμετέχοντα"}. {"That nickname is registered by another person","Αυτό το ψευδώνυμο είναι καταχωρημένο από άλλο πρόσωπο"}. +{"The account already exists","Ο λογαριασμός υπάρχει ήδη"}. +{"The account was not unregistered","Ο λογαριασμός δεν καταχωρήθηκε"}. +{"The body text of the last received message","Ο κορμός του κειμένου του τελευταίου μηνύματος"}. {"The CAPTCHA is valid.","Το CAPTCHA είναι έγκυρο."}. {"The CAPTCHA verification has failed","Η επαλήθευση της εικόνας CAPTCHA απέτυχε"}. -{"The collections with which a node is affiliated","Οι συλλογές με την οποία είναι ένας κόμβος συνδέεται"}. -{"The password is too weak","Ο κωδικός πρόσβασης είναι πολύ ασθενές"}. +{"The captcha you entered is wrong","Το captcha που εισαγάγατε είναι λάθος"}. +{"The child nodes (leaf or collection) associated with a collection","Οι θυγατρικοί κόμβοι (leaf ή collection) που σχετίζονται με μια συλλογή"}. +{"The collections with which a node is affiliated","Οι συλλογές με τις οποίες ένας κόμβος σχετίζεται"}. +{"The DateTime at which a leased subscription will end or has ended","Ο Χρόνος στον οποίο μια μισθωμένη συνδρομή θα Εκπνεύσει ή Τελειώσει"}. +{"The datetime when the node was created","Η χρονοσφραγίδα δημιουργίας του κόμβου"}. +{"The default language of the node","Η προρυθμισμένη γλώσσα του κόμβου"}. +{"The feature requested is not supported by the conference","Η λειτουργία που ζητήθηκε δεν υποστηρίζεται από τη διάσκεψη"}. +{"The JID of the node creator","Το JID του δημιουγού του κόμβου"}. +{"The JIDs of those to contact with questions","Το JID αυτών με τους οποίους θα επικοινωνήσετε με ερωτήσεις"}. +{"The JIDs of those with an affiliation of owner","Το JID αυτών που σχετίζονται με τον ιδιοκτήτη"}. +{"The JIDs of those with an affiliation of publisher","Το JID αυτών που σχετίζονται με τον εκδότη"}. +{"The list of all online users","Ο κατάλογος όλων των online χρηστών"}. +{"The list of all users","Ο κατάλογος όλων των χρηστών"}. +{"The list of JIDs that may associate leaf nodes with a collection","Λίστα των JIDs που μπορούν να σχετίζουν leaf κόμβους με μια Συλλογή"}. +{"The maximum number of child nodes that can be associated with a collection, or `max` for no specific limit other than a server imposed maximum","Ο μέγιστος αριθμός των παιδικών κόμβων που μπορούν να συσχετιστούν με μια συλλογή, ή `max` για κανένα συγκεκριμένο όριο εκτός από το μέγιστο που επιβάλλει ο διακομιστής"}. +{"The minimum number of milliseconds between sending any two notification digests","Το ελάχιστο πλήθος χιλιοστών του δευτερολέπτου μεταξύ της αποστολής δύο συγχωνεύσεων ειδοποιήσεων"}. +{"The name of the node","Το όνομα του κόμβου"}. +{"The node is a collection node","Ο κόμβος είναι κόμβος Συλλογής"}. +{"The node is a leaf node (default)","Ο κόμβος είναι leaf κόμβος (προεπιλογή)"}. +{"The NodeID of the relevant node","Το NodeID του σχετικού κόμβου"}. +{"The number of pending incoming presence subscription requests","Το πλήθος των αιτημάτων εισερχομένων συνδρομών παρουσίας σε αναμονή"}. +{"The number of subscribers to the node","Το πλήθος των συνδρομητών στον κόμβο"}. +{"The number of unread or undelivered messages","Το πλήθος των μη αναγνωσμένων ή μη παραδοτέων μηνυμάτων"}. +{"The password contains unacceptable characters","Ο κωδικός πρόσβασης περιέχει μη αποδεκτούς χαρακτήρες"}. +{"The password is too weak","Ο κωδικός πρόσβασης είναι πολύ αδύναμος"}. {"the password is","ο κωδικός πρόσβασης είναι"}. -{"The password of your Jabber account was successfully changed.","Ο κωδικός πρόσβασης του Jabber λογαριασμού σας έχει αλλάξει επιτυχώς."}. -{"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.","Ανεξαρτήτως με πεζά ή κεφαλαία: 'μιαλεξη' είναι το ίδιο με 'ΜιαΛέξη' και 'Μιαλέξη'."}. -{"This page allows to create a Jabber account in this Jabber server. Your JID (Jabber IDentifier) will be of the form: username@server. Please read carefully the instructions to fill correctly the fields.","Αυτή η σελίδα σας επιτρέπει να δημιουργήσετε ένα λογαριασμό Jabber σε αυτόν το διακομιστή Jabber. JID σας (Jabber Identifier) θα είναι της μορφής: όνομα_χρήστη@διακομιστής_Jabber. Παρακαλώ διαβάστε προσεκτικά τις οδηγίες για να συμπληρώσετε σωστά τα πεδία."}. -{"This page allows to unregister a Jabber account in this Jabber server.","Η σελίδα αυτή δίνει τη δυνατότητα να καταργήσετε την καταχώρηση ενός λογαριασμό Jabber σε αυτόν το διακομιστή Jabber."}. -{"This participant is kicked from the room because he sent an error message to another participant","Αυτός ο συμμετέχων αποβλήθηκε από την αίθουσα, επειδή έστειλε ένα μήνυμα σφάλματος σε άλλον συμμετέχων"}. -{"This participant is kicked from the room because he sent an error message","Αυτός ο συμμετέχων αποβλήθηκε από την αίθουσα, επειδή έστειλε ένα μήνυμα σφάλματος"}. -{"This participant is kicked from the room because he sent an error presence","Αυτός ο συμμετέχων αποβλήθηκε από την αίθουσα, επειδή έστειλε σφάλμα παρουσίας "}. +{"The password of your XMPP account was successfully changed.","Ο κωδικός πρόσβασης του XMPP λογαριασμού σας έχει αλλάξει επιτυχώς."}. +{"The password was not changed","Ο κωδικός πρόσβασης δεν άλλαξε"}. +{"The passwords are different","Οι κωδικοί πρόσβασης δεν ταιριάζουν"}. +{"The presence states for which an entity wants to receive notifications","Η παρουσία δηλώνει για ποιους θέλει κάποιος να λαμβάνει ειδοποιήσεις"}. +{"The query is only allowed from local users","Το ερώτημα επιτρέπεται μόνο από τοπικούς χρήστες"}. +{"The query must not contain elements","Το ερώτημα δεν πρέπει να περιέχει στοιχείο "}. +{"The room subject can be modified by participants","Το θέμα μπορεί να τροποποιηθεί από τους συμμετέχοντες"}. +{"The semantic type information of data in the node, usually specified by the namespace of the payload (if any)","Οι πληροφορίες σημασιολογικού τύπου των δεδομένων στον κόμβο, που συνήθως καθορίζονται από το χώρο ονομάτων του ωφέλιμου φορτίου (εάν υπάρχει)"}. +{"The sender of the last received message","Ο αποστολέας του τελευταίου εισερχομένου μηνύματος"}. +{"The stanza MUST contain only one element, one element, or one element","Η stanza ΠΡΕΠΕΙ να περιέχει μόνο ένα στοιχείο , ένα στοιχείο ή ένα στοιχείο "}. +{"The subscription identifier associated with the subscription request","Το αναγνωριστικό συνδρομής συσχετίστηκε με το αίτημα συνδρομής"}. +{"The URL of an XSL transformation which can be applied to payloads in order to generate an appropriate message body element.","Το URL ενός μετασχηματισμού XSL το οποίο μπορεί να εφαρμοστεί σε φόρτους εργασίας για να παραχθεί το κατάλληλο στοιχείο του σώματος του μηνύματος."}. +{"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","Το URL ενός μετασχηματισμού XSL, το οποίο μπορεί να εφαρμοστεί στους τύπους φόρτου εργασίας για να παραχθεί έγκυρο αποτέλεσμα Data Forms, τέτοιο που ο πελάτης μπορεί να εμφανίσει, χρησιμοποιώντας μια ευρείας χρήσης μηχανή επεξεργασίας Data Forms"}. +{"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.","Αυτό σημαίνει ότι δεν έχει σημασία αν είναι κεφαλαία ή πεζά γράμματα: \"κατσαντώνης\" είναι το ίδιο με \"ΚατσΑντώνης\" , όπως και \"Κατσαντώνης\"."}. +{"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 σε αυτόν τον διακομιστή XMPP. Το JID (Jabber ID) θα έχει τη μορφή: όνομαχρήστη@διακομιστής. Διαβάστε προσεκτικά τις οδηγίες για να συμπληρώσετε σωστά τα πεδία."}. +{"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","Πέμπτη"}. {"Time delay","Χρόνος καθυστέρησης"}. -{"Time","Χρόνος"}. +{"Timed out waiting for stream resumption","Υπερέβην το όριο αναμονής για επανασύνδεση της Ροής"}. +{"To register, visit ~s","Για να εγγραφείτε, επισκεφθείτε το ~s"}. +{"To ~ts","Προς ~ts"}. +{"Token TTL","Διακριτικό TTL"}. +{"Too many active bytestreams","Πάρα πολλά ενεργά bytestreams"}. {"Too many CAPTCHA requests","Πάρα πολλά αιτήματα CAPTCHA"}. -{"To ~s","Πρώς ~s"}. -{"To","Πρώς"}. +{"Too many child elements","Πάρα πολλά θυγατρικά στοιχεία"}. +{"Too many elements","Πάρα πολλά στοιχεία "}. +{"Too many elements","Πάρα πολλά στοιχεία "}. +{"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","Πάρα πολλές (~p) αποτυχημένες προσπάθειες σύνδεσης από την IP σας (~s). Άρση φραγής στις ~s UTC"}. +{"Too many receiver fields were specified","Πάρα πολλά πεδία δεκτών προσδιορίστηκαν"}. +{"Too many unacked stanzas","Πάρα πολλές μη αναγνωρισμένες stanzas"}. +{"Too many users in this conference","Πάρα πολλοί χρήστες σε αυτή τη διάσκεψη"}. {"Traffic rate limit is exceeded","Υπέρφορτωση"}. -{"Transactions Aborted:","Αποτυχημένες συναλλαγές:"}. -{"Transactions Committed:","Παραδοθείς συναλλαγές:"}. -{"Transactions Logged:","Καταγραμμένες συναλλαγές:"}. -{"Transactions Restarted:","Επανειλημμένες συναλλαγές:"}. +{"~ts's MAM Archive","Αρχείο MAM του ~ts"}. +{"~ts's Offline Messages Queue","~ts's Χωρίς Σύνδεση Μηνύματα"}. {"Tuesday","Τρίτη"}. -{"Unable to generate a CAPTCHA","Αδήνατο να δημιουργηθεί CAPTCHA"}. -{"Unauthorized","Χορίς Εξουσιοδότηση"}. -{"Unregister a Jabber account","Καταργήστε την εγγραφή ενός λογαριασμού Jabber"}. +{"Unable to generate a CAPTCHA","Αδύνατη η δημιουργία CAPTCHA"}. +{"Unable to register route on existing local domain","Δεν είναι δυνατή η καταχώρηση της διαδρομής σε υπάρχοντα τοπικό τομέα"}. +{"Unauthorized","Χωρίς Εξουσιοδότηση"}. +{"Unexpected action","Απροσδόκητη ενέργεια"}. +{"Unexpected error condition: ~p","Απροσδόκητες συνθήκες σφάλματος: ~p"}. +{"Uninstall","Απεγκατάσταση"}. +{"Unregister an XMPP account","Καταργήση λογαριασμού XMPP"}. {"Unregister","Καταργήση εγγραφής"}. -{"Update message of the day (don't send)","Ενημέρωση μηνύματως ημέρας (χωρίς άμεση αποστολή)"}. -{"Update message of the day on all hosts (don't send)","Ενημέρωση μηνύματως ημέρας σε όλους τους κεντρικούς υπολογιστές (χωρίς άμεση αποστολή)"}. -{"Update plan","Σχέδιο ενημέρωσης"}. -{"Update script","Προγράμα ενημέρωσης"}. -{"Update ","Ενημέρωση"}. -{"Update","Ενημέρωση"}. -{"Uptime:","Uptime:"}. -{"Use of STARTTLS required","Απαιτείται χρήση STARTTLS "}. +{"Unsupported element","Μη υποστηριζόμενο στοιχείο "}. +{"Unsupported version","Μη υποστηριζόμενη έκδοση"}. +{"Update message of the day (don't send)","Ενημέρωση μηνύματος ημέρας (χωρίς άμεση αποστολή)"}. +{"Update message of the day on all hosts (don't send)","Ενημέρωση μηνύματος ημέρας σε όλους τους κεντρικούς υπολογιστές (χωρίς άμεση αποστολή)"}. +{"Update specs to get modules source, then install desired ones.","Ενημερώστε τις προδιαγραφές για να λάβετε την πηγή των ενοτήτων και, στη συνέχεια, εγκαταστήστε τις επιθυμητές."}. +{"Update Specs","Προδιαγραφές ενημέρωσης"}. +{"Updating the vCard is not supported by the vCard storage backend","Η ενημέρωση της vCard δεν υποστηρίζεται από το backend αποθήκευσης vCard"}. +{"Upgrade","Αναβάθμιση"}. +{"URL for Archived Discussion Logs","URL αρχειοθετημένων καταγραφών συζητήσεων"}. +{"User already exists","Ο χρήστης υπάρχει ήδη"}. {"User JID","JID Χρήστη"}. +{"User (jid)","Χρήστης (jid)"}. {"User Management","Διαχείριση χρηστών"}. -{"Username:","Όνομα χρήστη"}. -{"Users are not allowed to register accounts so quickly","Οι χρήστες δεν επιτρέπεται να εγγραφούν λογαριασμούς τόσο γρήγορα"}. +{"User not allowed to perform an IQ set on another user's vCard.","Ο χρήστης δεν επιτρέπεται να εκτελέσει ένα σετ IQ στην vCard ενός άλλου χρήστη."}. +{"User removed","Ο Χρήστης αφαιρέθηκε"}. +{"User session not found","Η περίοδος σύνδεσης χρήστη δεν βρέθηκε"}. +{"User session terminated","Η περίοδος σύνδεσης χρήστη τερματίστηκε"}. +{"User ~ts","Χρήστης ~ts"}. +{"Username:","Όνομα χρήστη:"}. +{"Users are not allowed to register accounts so quickly","Οι χρήστες δεν επιτρέπεται να δημιουργούν λογαριασμούς τόσο γρήγορα"}. {"Users Last Activity","Τελευταία Δραστηριότητα Χρήστη"}. {"Users","Χρήστες"}. -{"User ","Χρήστης"}. {"User","Χρήστης"}. -{"Validate","Επαληθεύστε"}. +{"Value 'get' of 'type' attribute is not allowed","Η τιμή 'get' του 'type' δεν επιτρέπεται"}. +{"Value of '~s' should be boolean","Η τιμή του '~s' πρέπει να είναι boolean"}. +{"Value of '~s' should be datetime string","Η τιμή του '~s' θα πρέπει να είναι χρονοσειρά"}. +{"Value of '~s' should be integer","Η τιμή του '~s' θα πρέπει να είναι ακέραιος"}. +{"Value 'set' of 'type' attribute is not allowed","Δεν επιτρέπεται η παράμετρος 'set' του 'type'"}. {"vCard User Search","vCard Αναζήτηση χρηστών"}. -{"Virtual Hosts","εικονικοί κεντρικοί υπολογιστές"}. +{"View joined MIX channels","Προβολή ενταγμένων καναλιών MIX"}. +{"Virtual Hosts","Eικονικοί κεντρικοί υπολογιστές"}. {"Visitors are not allowed to change their nicknames in this room","Οι επισκέπτες δεν επιτρέπεται να αλλάξουν τα ψευδώνυμα τους σε αυτή την αίθουσα"}. -{"Visitors are not allowed to send messages to all occupants","Οι επισκέπτες δεν επιτρέπεται να στείλουν μηνύματα σε όλους τους συμμετέχωντες"}. +{"Visitors are not allowed to send messages to all occupants","Οι επισκέπτες δεν επιτρέπεται να στείλουν μηνύματα σε όλους τους συμμετέχοντες"}. +{"Visitor","Επισκέπτης"}. {"Voice requests are disabled in this conference","Τα αιτήματα φωνής είναι απενεργοποιημένα, σε αυτό το συνέδριο"}. {"Voice request","Αίτημα φωνής"}. +{"Web client which allows to join the room anonymously","Web client που επιτρέπει την ανώνυμη είσοδο στην αίθουσα"}. {"Wednesday","Τετάρτη"}. -{"When to send the last published item","Πότε να αποσταλθεί το τελευταίο στοιχείο που δημοσιεύθηκε"}. -{"Whether to allow subscriptions","Εάν επιτρέποντε συνδρομές"}. -{"You can later change your password using a Jabber client.","Μπορείτε αργότερα να αλλάξετε τον κωδικό πρόσβασής σας χρησιμοποιώντας έναν πελάτη Jabber."}. +{"When a new subscription is processed and whenever a subscriber comes online","Όταν μία νέα συνδρομή βρίσκεται εν επεξεργασία και όποτε ένας συνδρομητής συνδεθεί"}. +{"When a new subscription is processed","Όταν μία νέα συνδρομή βρίσκεται εν επεξεργασία"}. +{"When to send the last published item","Πότε να αποσταλεί το τελευταίο στοιχείο που δημοσιεύθηκε"}. +{"Whether an entity wants to receive an XMPP message body in addition to the payload format","Εάν κάποιος θέλει να λάβει το κυρίως XMPP μήνυμα, επιπροσθέτως του τύπου φόρτου εργασιών"}. +{"Whether an entity wants to receive digests (aggregations) of notifications or all notifications individually","Εάν μία οντότητα επιθυμεί να λαμβάνει αθροιστικές συνόψεις των ειδοποιήσεων ή όλες τις ειδοποιήσεις ξεχωριστά"}. +{"Whether an entity wants to receive or disable notifications","Εάν μία οντότητα επιθυμεί να λαμβάνει ή όχι ειδοποιήσεις"}. +{"Whether owners or publisher should receive replies to items","Εάν οι ιδιοκτήτες επιθυμούν να λαμβάνουν απαντήσεις στα αντικείμενα"}. +{"Whether the node is a leaf (default) or a collection","Εάν ο κόμβος είναι leaf (προεπιλογή) ή συλλογή"}. +{"Whether to allow subscriptions","Εάν επιτρέπονται συνδρομές"}. +{"Whether to make all subscriptions temporary, based on subscriber presence","Αν επιτρέπεται να γίνουν όλες οι συνδρομές προσωρινές, βασιζόμενοι στην παρουσία του συνδρομητή"}. +{"Whether to notify owners about new subscribers and unsubscribes","Αν πρέπει να ειδοποιούνται οι ιδιοκτήτες για νέους συνδρομητές και αποχωρήσεις"}. +{"Who can send private messages","Ποιος μπορεί να στείλει ιδιωτικά μηνύματα"}. +{"Who may associate leaf nodes with a collection","Ποιός μπορεί να συσχετίζει leaf nodes με μία συλλογή"}. +{"Wrong parameters in the web formulary","Εσφαλμένες παράμετροι στην διαμόρφωση τυπικότητας του δυκτίου"}. +{"Wrong xmlns","Εσφαλμένο xmlns"}. +{"XMPP Account Registration","Εγγραφή λογαριασμού XMPP"}. +{"XMPP Domains","Ονόματα χώρου XMPP"}. +{"XMPP Show Value of Away","Δείξε τιμή XMPP Απεμακρύνθην"}. +{"XMPP Show Value of Chat","Δείξε τιμή XMPP Αξία Συνομιλίας"}. +{"XMPP Show Value of DND (Do Not Disturb)","Δείξε τιμή XMPP Αξία του Μην Ενοχλείτε"}. +{"XMPP Show Value of XA (Extended Away)","Δείξε τιμή XMPP Αξία του Λίαν Απομακρυσμένος"}. +{"XMPP URI of Associated Publish-Subscribe Node","XMPP URI του συσχετισμένου κόμβου Δημοσίευσης-Εγγραφής"}. +{"You are being removed from the room because of a system shutdown","Απαιτείται η απομάκρυνσή σας από την αίθουσα, λόγω τερματισμού συστήματος"}. +{"You are not allowed to send private messages","Δεν επιτρέπεται η αποστολή ιδιωτικών μηνυμάτων"}. +{"You are not joined to the channel","Δεν λαμβάνετε μέρος στο κανάλι"}. +{"You can later change your password using an XMPP client.","Μπορείτε αργότερα να αλλάξετε τον κωδικό πρόσβασής σας χρησιμοποιώντας ένα πρόγραμμα-πελάτη XMPP."}. {"You have been banned from this room","Σας έχει απαγορευθεί η είσοδος σε αυτή την αίθουσα"}. -{"You must fill in field \"Nickname\" in the form","Θα πρέπει να συμπληρώσετε το πεδίο \"Ψευδώνυμο\" στη φόρμα"}. -{"You need a client that supports x:data and CAPTCHA to register","Χρειάζεστε ένα x:data και CAPTCHA ικανό πελάτη για εγγραφή"}. -{"You need a client that supports x:data to register the nickname","Χρειάζεστε ένα x:data ικανό πελάτη για εγγραφή με ψευδώνυμο"}. -{"You need an x:data capable client to configure mod_irc settings","Χρειάζεστε ένα x:data ικανό πελάτη για να ρυθμίσετε το mod_irc"}. -{"You need an x:data capable client to configure room","Χρειάζεστε ένα x:data ικανό πελάτη για να ρυθμίσετε την αίθουσα "}. -{"You need an x:data capable client to search","Χρειάζεστε ένα x:data ικανό πελάτη για αναζήτηση"}. +{"You have joined too many conferences","Είσθε σε πάρα πολλά συνέδρια"}. +{"You must fill in field \"Nickname\" in the form","Απαιτείται να συμπληρώσετε το πεδίο \"Ψευδώνυμο\" στη φόρμα"}. +{"You need a client that supports x:data and CAPTCHA to register","Χρειάζεστε έναν πελάτη που να υποστηρίζει x:data και CAPTCHA"}. +{"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.","Ο ενεργός κατάλογος απορρήτου, έχει αρνηθεί τη δρομολόγηση αυτής της στροφής (stanza)."}. -{"Your contact offline message queue is full. The message has been discarded.","Η μνήμη χωρίς σύνδεση μήνυματών είναι πλήρης. Το μήνυμα έχει απορριφθεί."}. -{"Your Jabber account was successfully created.","Ο Jabber λογαριασμός σας δημιουργήθηκε με επιτυχία."}. -{"Your Jabber account was successfully deleted.","Ο Jabber λογαριασμός σας διαγράφηκε με επιτυχία."}. -{"Your messages to ~s are being blocked. To unblock them, visit ~s","Τα μηνύματά σας πρως ~s είναι αποκλεισμένα. Για αποδεσμεύση, επισκεφθείτε ~s"}. +{"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 λογαριασμός σας διαγράφηκε με επιτυχία."}. +{"You're not allowed to create nodes","Δεν σας επιτρέπεται η δημιουργία κόμβων"}. diff --git a/priv/msgs/el.po b/priv/msgs/el.po deleted file mode 100644 index 15a709b68..000000000 --- a/priv/msgs/el.po +++ /dev/null @@ -1,1889 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: el\n" -"POT-Creation-Date: \n" -"PO-Revision-Date: 2012-04-18 12:50-0000\n" -"Last-Translator: James Iakovos Mandelis \n" -"Language-Team: \n" -"Language: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Language: Greek (ελληνικά)\n" - -#: ejabberd_c2s.erl:424 ejabberd_c2s.erl:727 -msgid "Use of STARTTLS required" -msgstr "Απαιτείται χρήση STARTTLS " - -#: ejabberd_c2s.erl:503 -msgid "No resource provided" -msgstr "Δεν προμηθεύτικε πόρος" - -#: ejabberd_c2s.erl:1197 -msgid "Replaced by new connection" -msgstr "Αντικαταστάθικε από νέα σύνδεση" - -#: ejabberd_c2s.erl:1885 -msgid "Your active privacy list has denied the routing of this stanza." -msgstr "" -"Ο ενεργός κατάλογος απορρήτου, έχει αρνηθεί τη δρομολόγηση αυτής της στροφής " -"(stanza)." - -#: ejabberd_captcha.erl:96 ejabberd_captcha.erl:152 ejabberd_captcha.erl:178 -msgid "Enter the text you see" -msgstr "Πληκτρολογήστε το κείμενο που βλέπετε" - -#: ejabberd_captcha.erl:101 -msgid "Your messages to ~s are being blocked. To unblock them, visit ~s" -msgstr "" -"Τα μηνύματά σας πρως ~s είναι αποκλεισμένα. Για αποδεσμεύση, επισκεφθείτε ~s" - -#: ejabberd_captcha.erl:134 -msgid "If you don't see the CAPTCHA image here, visit the web page." -msgstr "Εάν δεν βλέπετε την εικόνα CAPTCHA εδώ, επισκεφθείτε την ιστοσελίδα." - -#: ejabberd_captcha.erl:146 -msgid "CAPTCHA web page" -msgstr "Ιστοσελίδα CAPTCHA " - -#: ejabberd_captcha.erl:307 -msgid "The CAPTCHA is valid." -msgstr "Το CAPTCHA είναι έγκυρο." - -#: mod_adhoc.erl:95 mod_adhoc.erl:125 mod_adhoc.erl:143 mod_adhoc.erl:161 -msgid "Commands" -msgstr "Εντολές" - -#: mod_adhoc.erl:149 mod_adhoc.erl:243 -msgid "Ping" -msgstr "Πινγκ" - -#: mod_adhoc.erl:260 -msgid "Pong" -msgstr "Πονγκ" - -#: mod_announce.erl:507 mod_announce_odbc.erl:499 -msgid "Really delete message of the day?" -msgstr "Πραγματικά να διαγράψετε το μήνυμα της ημέρας;" - -#: mod_announce.erl:515 mod_announce_odbc.erl:507 mod_configure.erl:1083 -#: mod_configure.erl:1128 -msgid "Subject" -msgstr "Θέμα" - -#: mod_announce.erl:520 mod_announce_odbc.erl:512 mod_configure.erl:1088 -#: mod_configure.erl:1133 -msgid "Message body" -msgstr "Περιεχόμενο μηνυμάτως" - -#: mod_announce.erl:600 mod_announce_odbc.erl:592 -msgid "No body provided for announce message" -msgstr "Δεν προμηθεύτικε περιεχόμενο ανακοινώσης" - -#: mod_announce.erl:635 mod_announce_odbc.erl:627 -msgid "Announcements" -msgstr "Ανακοινώσεις" - -#: mod_announce.erl:637 mod_announce_odbc.erl:629 -msgid "Send announcement to all users" -msgstr "Αποστολή ανακοίνωσης σε όλους τους χρήστες" - -#: mod_announce.erl:639 mod_announce_odbc.erl:631 -msgid "Send announcement to all users on all hosts" -msgstr "" -"Αποστολή ανακοίνωσης σε όλους τους χρήστες σε όλους τους κεντρικούς " -"υπολογιστές" - -#: mod_announce.erl:641 mod_announce_odbc.erl:633 -msgid "Send announcement to all online users" -msgstr "Αποστολή ανακοίνωσης σε όλους τους συνδεδεμένους χρήστες" - -#: mod_announce.erl:643 mod_announce_odbc.erl:635 mod_configure.erl:1078 -#: mod_configure.erl:1123 -msgid "Send announcement to all online users on all hosts" -msgstr "" -"Αποστολή ανακοίνωσης σε όλους τους συνδεδεμένους χρήστες σε όλους τους " -"κεντρικούς υπολογιστές" - -#: mod_announce.erl:645 mod_announce_odbc.erl:637 -msgid "Set message of the day and send to online users" -msgstr "Ορίστε μήνυμα ημέρας και αποστολή στους συνδεδεμένους χρήστες" - -#: mod_announce.erl:647 mod_announce_odbc.erl:639 -msgid "Set message of the day on all hosts and send to online users" -msgstr "" -"Ορίστε μήνυμα ημέρας και άμεση αποστολή στους συνδεδεμένους χρήστες σε όλους " -"τους κεντρικούς υπολογιστές" - -#: mod_announce.erl:649 mod_announce_odbc.erl:641 -msgid "Update message of the day (don't send)" -msgstr "Ενημέρωση μηνύματως ημέρας (χωρίς άμεση αποστολή)" - -#: mod_announce.erl:651 mod_announce_odbc.erl:643 -msgid "Update message of the day on all hosts (don't send)" -msgstr "" -"Ενημέρωση μηνύματως ημέρας σε όλους τους κεντρικούς υπολογιστές (χωρίς άμεση " -"αποστολή)" - -#: mod_announce.erl:653 mod_announce_odbc.erl:645 -msgid "Delete message of the day" -msgstr "Διαγράψτε το μήνυμα της ημέρας" - -#: mod_announce.erl:655 mod_announce_odbc.erl:647 -msgid "Delete message of the day on all hosts" -msgstr "Διαγράψτε το μήνυμα της ημέρας σε όλους τους κεντρικούς υπολογιστές" - -#: mod_configure.erl:114 mod_configure.erl:274 mod_configure.erl:296 -#: mod_configure.erl:498 -msgid "Configuration" -msgstr "Διαμόρφωση" - -#: mod_configure.erl:125 mod_configure.erl:576 web/ejabberd_web_admin.erl:1936 -msgid "Database" -msgstr "Βάση δεδομένων" - -#: mod_configure.erl:127 mod_configure.erl:595 -msgid "Start Modules" -msgstr "Εκκίνηση Modules" - -#: mod_configure.erl:129 mod_configure.erl:596 -msgid "Stop Modules" -msgstr "ΠαύσηModules" - -#: mod_configure.erl:131 mod_configure.erl:604 web/ejabberd_web_admin.erl:1937 -msgid "Backup" -msgstr "Αποθήκευση Αντιγράφου Ασφαλείας" - -#: mod_configure.erl:133 mod_configure.erl:605 -msgid "Restore" -msgstr "Επαναφορά Αντιγράφου Ασφαλείας" - -#: mod_configure.erl:135 mod_configure.erl:606 -msgid "Dump to Text File" -msgstr "Αποθήκευση σε αρχείο κειμένου" - -#: mod_configure.erl:137 mod_configure.erl:615 -msgid "Import File" -msgstr "Εισαγωγή αρχείων" - -#: mod_configure.erl:139 mod_configure.erl:616 -msgid "Import Directory" -msgstr "Εισαγωγή κατάλογου αρχείων" - -#: mod_configure.erl:141 mod_configure.erl:581 mod_configure.erl:1057 -msgid "Restart Service" -msgstr "Επανεκκίνηση Υπηρεσίας" - -#: mod_configure.erl:143 mod_configure.erl:582 mod_configure.erl:1102 -msgid "Shut Down Service" -msgstr "Κλείσιμο Υπηρεσίας" - -#: mod_configure.erl:145 mod_configure.erl:518 mod_configure.erl:1197 -#: web/ejabberd_web_admin.erl:1527 -msgid "Add User" -msgstr "Προσθήκη Χρήστη" - -#: mod_configure.erl:147 mod_configure.erl:519 mod_configure.erl:1219 -msgid "Delete User" -msgstr "Διαγραφή Χρήστη" - -#: mod_configure.erl:149 mod_configure.erl:520 mod_configure.erl:1231 -msgid "End User Session" -msgstr "Τερματισμός Συνεδρίας Χρήστη" - -#: mod_configure.erl:151 mod_configure.erl:521 mod_configure.erl:1243 -#: mod_configure.erl:1255 -msgid "Get User Password" -msgstr "Έκθεση Κωδικού Πρόσβασης Χρήστη" - -#: mod_configure.erl:153 mod_configure.erl:522 -msgid "Change User Password" -msgstr "Αλλαγή Κωδικού Πρόσβασης Χρήστη" - -#: mod_configure.erl:155 mod_configure.erl:523 mod_configure.erl:1272 -msgid "Get User Last Login Time" -msgstr "Έκθεση Τελευταίας Ώρας Σύνδεσης Χρήστη" - -#: mod_configure.erl:157 mod_configure.erl:524 mod_configure.erl:1284 -msgid "Get User Statistics" -msgstr "Έκθεση Στατιστικών Χρήστη" - -#: mod_configure.erl:159 mod_configure.erl:525 -msgid "Get Number of Registered Users" -msgstr "Έκθεση αριθμού εγγεγραμμένων χρηστών" - -#: mod_configure.erl:161 mod_configure.erl:526 -msgid "Get Number of Online Users" -msgstr "Έκθεση αριθμού συνδεδεμένων χρηστών" - -#: mod_configure.erl:163 mod_configure.erl:509 web/ejabberd_web_admin.erl:831 -#: web/ejabberd_web_admin.erl:872 -msgid "Access Control Lists" -msgstr "Λίστες Ελέγχου Πρόσβασης" - -#: mod_configure.erl:165 mod_configure.erl:510 web/ejabberd_web_admin.erl:940 -#: web/ejabberd_web_admin.erl:976 -msgid "Access Rules" -msgstr "Κανόνες Πρόσβασης" - -#: mod_configure.erl:297 mod_configure.erl:499 -msgid "User Management" -msgstr "Διαχείριση χρηστών" - -#: mod_configure.erl:500 web/ejabberd_web_admin.erl:1058 -#: web/ejabberd_web_admin.erl:1462 -msgid "Online Users" -msgstr "Συνδεμένοι χρήστες" - -#: mod_configure.erl:501 -msgid "All Users" -msgstr "Όλοι οι χρήστες" - -#: mod_configure.erl:502 -msgid "Outgoing s2s Connections" -msgstr "Εξερχόμενες S2S Συνδέσεις" - -#: mod_configure.erl:503 web/ejabberd_web_admin.erl:1907 -msgid "Running Nodes" -msgstr "Ενεργοί Κόμβοι" - -#: mod_configure.erl:504 web/ejabberd_web_admin.erl:1909 -msgid "Stopped Nodes" -msgstr "Σταματημένοι Κόμβοι" - -#: mod_configure.erl:577 mod_configure.erl:587 web/ejabberd_web_admin.erl:1953 -msgid "Modules" -msgstr "Modules" - -#: mod_configure.erl:578 -msgid "Backup Management" -msgstr "Διαχείριση Αντιγράφου Ασφαλείας" - -#: mod_configure.erl:579 -msgid "Import Users From jabberd14 Spool Files" -msgstr "Εισαγωγή Χρηστών από αρχεία σειράς jabberd14" - -#: mod_configure.erl:699 -msgid "To ~s" -msgstr "Πρώς ~s" - -#: mod_configure.erl:717 -msgid "From ~s" -msgstr "Από ~s" - -#: mod_configure.erl:913 -msgid "Database Tables Configuration at " -msgstr "Διαμόρφωση Πίνακων βάσης δεδομένων στο " - -#: mod_configure.erl:918 -msgid "Choose storage type of tables" -msgstr "Επιλέξτε τύπο αποθήκευσης των πινάκων" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Disc only copy" -msgstr "Αντίγραφο μόνο σε δίσκο" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM and disc copy" -msgstr "Αντίγραφο μόνο σε RAM καί δίσκο" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM copy" -msgstr "Αντίγραφο σε RAM" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Remote copy" -msgstr "Απομεμακρυσμένο αντίγραφο" - -#: mod_configure.erl:950 -msgid "Stop Modules at " -msgstr "Παύση Modules στο " - -#: mod_configure.erl:954 -msgid "Choose modules to stop" -msgstr "Επιλέξτε modules για να σταματήσουν" - -#: mod_configure.erl:969 -msgid "Start Modules at " -msgstr "Εκκίνηση Modules στο " - -#: mod_configure.erl:973 -msgid "Enter list of {Module, [Options]}" -msgstr "Εισάγετε κατάλογο των (Module, [Επιλογές])" - -#: mod_configure.erl:974 -msgid "List of modules to start" -msgstr "Λίστα των Module για Εκκίνηση" - -#: mod_configure.erl:983 -msgid "Backup to File at " -msgstr "Αποθήκευση Αντιγράφου Ασφαλείας σε Αρχείο στο " - -#: mod_configure.erl:987 mod_configure.erl:1001 -msgid "Enter path to backup file" -msgstr "Εισάγετε τοποθεσία αρχείου αντιγράφου ασφαλείας" - -#: mod_configure.erl:988 mod_configure.erl:1002 mod_configure.erl:1016 -#: mod_configure.erl:1030 -msgid "Path to File" -msgstr "Τοποθεσία Αρχείου" - -#: mod_configure.erl:997 -msgid "Restore Backup from File at " -msgstr "Επαναφορά Αντιγράφου Ασφαλείας από αρχείο στο " - -#: mod_configure.erl:1011 -msgid "Dump Backup to Text File at " -msgstr "Αποθήκευση Αντιγράφου Ασφαλείας σε αρχείο κειμένου στο " - -#: mod_configure.erl:1015 -msgid "Enter path to text file" -msgstr "Εισάγετε Τοποθεσία Αρχείου Κειμένου" - -#: mod_configure.erl:1025 -msgid "Import User from File at " -msgstr "Εισαγωγή χρηστών από αρχείο στο " - -#: mod_configure.erl:1029 -msgid "Enter path to jabberd14 spool file" -msgstr "Εισάγετε τοποθεσία αρχείου σειράς jabberd14" - -#: mod_configure.erl:1039 -msgid "Import Users from Dir at " -msgstr "Εισαγωγή χρηστών από κατάλογο αρχείων στο " - -#: mod_configure.erl:1043 -msgid "Enter path to jabberd14 spool dir" -msgstr "Εισάγετε κατάλογο αρχείων σειράς jabberd14" - -#: mod_configure.erl:1044 -msgid "Path to Dir" -msgstr "Τοποθεσία κατάλογου αρχείων" - -#: mod_configure.erl:1060 mod_configure.erl:1105 -msgid "Time delay" -msgstr "Χρόνος καθυστέρησης" - -#: mod_configure.erl:1143 -msgid "Access Control List Configuration" -msgstr "Διαχείριση στις Λίστες Ελέγχου Πρόσβασης" - -#: mod_configure.erl:1147 -msgid "Access control lists" -msgstr "Λίστες Ελέγχου Πρόσβασης" - -#: mod_configure.erl:1171 -msgid "Access Configuration" -msgstr "Διαμόρφωση Πρόσβασης" - -#: mod_configure.erl:1175 -msgid "Access rules" -msgstr "Κανόνες Πρόσβασης" - -#: mod_configure.erl:1200 mod_configure.erl:1222 mod_configure.erl:1234 -#: mod_configure.erl:1246 mod_configure.erl:1258 mod_configure.erl:1275 -#: mod_configure.erl:1287 mod_configure.erl:1650 mod_configure.erl:1700 -#: mod_configure.erl:1721 mod_roster.erl:943 mod_roster_odbc.erl:1060 -#: mod_vcard.erl:472 mod_vcard_ldap.erl:554 mod_vcard_odbc.erl:448 -msgid "Jabber ID" -msgstr "Ταυτότητα Jabber" - -#: mod_configure.erl:1205 mod_configure.erl:1263 mod_configure.erl:1651 -#: mod_configure.erl:1863 mod_muc/mod_muc_room.erl:3224 mod_register.erl:235 -#: web/ejabberd_web_admin.erl:1520 -msgid "Password" -msgstr "Κωδικός Πρόσβασης" - -#: mod_configure.erl:1210 -msgid "Password Verification" -msgstr "Επαλήθευση κωδικού πρόσβασης" - -#: mod_configure.erl:1301 -msgid "Number of registered users" -msgstr "Αριθμός εγγεγραμμένων χρηστών" - -#: mod_configure.erl:1315 -msgid "Number of online users" -msgstr "Αριθμός συνδεδεμένων χρηστών" - -#: mod_configure.erl:1682 web/ejabberd_web_admin.erl:1588 -#: web/ejabberd_web_admin.erl:1743 -msgid "Never" -msgstr "Ποτέ" - -#: mod_configure.erl:1696 web/ejabberd_web_admin.erl:1601 -#: web/ejabberd_web_admin.erl:1756 -msgid "Online" -msgstr "Συνδεδεμένο" - -#: mod_configure.erl:1701 -msgid "Last login" -msgstr "Τελευταία σύνδεση" - -#: mod_configure.erl:1722 -msgid "Roster size" -msgstr "Μέγεθος Καταλόγου Επαφών" - -#: mod_configure.erl:1723 -msgid "IP addresses" -msgstr "Διευθύνσεις IP" - -#: mod_configure.erl:1724 -msgid "Resources" -msgstr "Πόροι" - -#: mod_configure.erl:1850 -msgid "Administration of " -msgstr "Διαχείριση του" - -#: mod_configure.erl:1853 -msgid "Action on user" -msgstr "Eνέργεια για το χρήστη" - -#: mod_configure.erl:1857 -msgid "Edit Properties" -msgstr "Επεξεργασία ιδιοτήτων" - -#: mod_configure.erl:1860 web/ejabberd_web_admin.erl:1769 -msgid "Remove User" -msgstr "Αφαίρεση χρήστη" - -#: mod_irc/mod_irc.erl:201 mod_irc/mod_irc_odbc.erl:196 -#: mod_muc/mod_muc.erl:336 mod_muc/mod_muc_odbc.erl:342 -msgid "Access denied by service policy" -msgstr "Άρνηση πρόσβασης, λόγω τακτικής παροχής υπηρεσιών" - -#: mod_irc/mod_irc.erl:401 mod_irc/mod_irc_odbc.erl:398 -msgid "IRC Transport" -msgstr "IRC Διαβιβάσεις" - -#: mod_irc/mod_irc.erl:428 mod_irc/mod_irc_odbc.erl:425 -msgid "ejabberd IRC module" -msgstr "ejabberd IRC module" - -#: mod_irc/mod_irc.erl:559 mod_irc/mod_irc_odbc.erl:568 -msgid "You need an x:data capable client to configure mod_irc settings" -msgstr "Χρειάζεστε ένα x:data ικανό πελάτη για να ρυθμίσετε το mod_irc" - -#: mod_irc/mod_irc.erl:566 mod_irc/mod_irc_odbc.erl:575 -msgid "Registration in mod_irc for " -msgstr "Εγγραφή στο mod_irc για " - -#: mod_irc/mod_irc.erl:571 mod_irc/mod_irc_odbc.erl:580 -msgid "" -"Enter username, encodings, ports and passwords you wish to use for " -"connecting to IRC servers" -msgstr "" -"Εισάγετε το όνομα χρήστη, κωδικοποιήσεις, τις θύρες και τους κωδικούς " -"πρόσβασης που θέλετε να χρησιμοποιήσετε για σύνδεση με IRC διακομιστή" - -#: mod_irc/mod_irc.erl:576 mod_irc/mod_irc_odbc.erl:585 -msgid "IRC Username" -msgstr "IRC Όνομα χρήστη" - -#: mod_irc/mod_irc.erl:586 mod_irc/mod_irc_odbc.erl:595 -msgid "" -"If you want to specify different ports, passwords, encodings for IRC " -"servers, fill this list with values in format '{\"irc server\", \"encoding" -"\", port, \"password\"}'. By default this service use \"~s\" encoding, port " -"~p, empty password." -msgstr "" -"Εάν θέλετε να καθορίσετε διαφορετικές θύρες, κωδικούς πρόσβασης, " -"κωδικοποιήσεις για IRC διακομιστές, εισάγετε πληροφορίες στη μορφή '{\"irc " -"διακομιστής\", \"κωδικοποιήσεις\", θύρα, \"κωδικός πρόσβασης\"}'. " -"Προεπιλεγμενα αυτή η υπηρεσία χρησιμοποιεί \"~s\" κωδικοποιήση, θύρα ~p, " -"κενό κωδικό πρόσβασης." - -#: mod_irc/mod_irc.erl:598 mod_irc/mod_irc_odbc.erl:607 -msgid "" -"Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef." -"net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]." -msgstr "" -"Παράδειγμα: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta." -"fef.net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]." - -#: mod_irc/mod_irc.erl:603 mod_irc/mod_irc_odbc.erl:612 -msgid "Connections parameters" -msgstr "Παράμετροι Συνδέσης" - -#: mod_irc/mod_irc.erl:728 mod_irc/mod_irc_odbc.erl:757 -msgid "Join IRC channel" -msgstr "Είσοδος στο IRC κανάλι" - -#: mod_irc/mod_irc.erl:732 mod_irc/mod_irc_odbc.erl:761 -msgid "IRC channel (don't put the first #)" -msgstr "IRC κανάλι (μην τεθεί το πρώτο #)" - -#: mod_irc/mod_irc.erl:737 mod_irc/mod_irc_odbc.erl:766 -msgid "IRC server" -msgstr "Διακομιστής IRC" - -#: mod_irc/mod_irc.erl:770 mod_irc/mod_irc.erl:774 -#: mod_irc/mod_irc_odbc.erl:799 mod_irc/mod_irc_odbc.erl:803 -msgid "Join the IRC channel here." -msgstr "Είσοδος στο κανάλι IRC εδώ." - -#: mod_irc/mod_irc.erl:778 mod_irc/mod_irc_odbc.erl:807 -msgid "Join the IRC channel in this Jabber ID: ~s" -msgstr "Είσοδος στο κανάλι IRC αυτής της Jabber Ταυτότητας: ~s" - -#: mod_irc/mod_irc.erl:863 mod_irc/mod_irc_odbc.erl:904 -msgid "IRC settings" -msgstr "IRC Ρυθμίσεις" - -#: mod_irc/mod_irc.erl:868 mod_irc/mod_irc_odbc.erl:909 -msgid "" -"Enter username and encodings you wish to use for connecting to IRC servers. " -"Press 'Next' to get more fields to fill in. Press 'Complete' to save " -"settings." -msgstr "" -"Πληκτρολογήστε το όνομα χρήστη και κωδικοποιήσεις που θέλετε να " -"χρησιμοποιήσετε για τη σύνδεση με διακομιστές IRC. Πατήστε 'Next' για να " -"πάρετε περισσότερα πεδία να συμπληρώσετε. Πατήστε 'Complete' για να " -"αποθηκεύσετε ρυθμίσεις." - -#: mod_irc/mod_irc.erl:874 mod_irc/mod_irc_odbc.erl:915 -msgid "IRC username" -msgstr "IRC όνομα χρήστη" - -#: mod_irc/mod_irc.erl:923 mod_irc/mod_irc_odbc.erl:964 -msgid "Password ~b" -msgstr "Κωδικός πρόσβασης ~b" - -#: mod_irc/mod_irc.erl:928 mod_irc/mod_irc_odbc.erl:969 -msgid "Port ~b" -msgstr "Θύρα ~b" - -#: mod_irc/mod_irc.erl:933 mod_irc/mod_irc_odbc.erl:974 -msgid "Encoding for server ~b" -msgstr "Κωδικοποίηση για διακομιστή ~b" - -#: mod_irc/mod_irc.erl:942 mod_irc/mod_irc_odbc.erl:983 -msgid "Server ~b" -msgstr "Διακομιστής ~b" - -#: mod_muc/mod_muc.erl:449 mod_muc/mod_muc_odbc.erl:456 -msgid "Only service administrators are allowed to send service messages" -msgstr "" -"Μόνο οι διαχειριστές των υπηρεσιών επιτρέπεται να στείλουν υπηρεσιακά " -"μηνύματα" - -#: mod_muc/mod_muc.erl:493 mod_muc/mod_muc_odbc.erl:500 -msgid "Room creation is denied by service policy" -msgstr "Άρνηση δημιουργίας αίθουσας , λόγω τακτικής παροχής υπηρεσιών" - -#: mod_muc/mod_muc.erl:500 mod_muc/mod_muc_odbc.erl:507 -msgid "Conference room does not exist" -msgstr "Αίθουσα σύνεδριασης δεν υπάρχει" - -#: mod_muc/mod_muc.erl:582 mod_muc/mod_muc_odbc.erl:595 -msgid "Chatrooms" -msgstr "Αίθουσες σύνεδριασης" - -#: mod_muc/mod_muc.erl:712 mod_muc/mod_muc_odbc.erl:729 -msgid "You need a client that supports x:data to register the nickname" -msgstr "Χρειάζεστε ένα x:data ικανό πελάτη για εγγραφή με ψευδώνυμο" - -#: mod_muc/mod_muc.erl:718 mod_muc/mod_muc_odbc.erl:735 -msgid "Nickname Registration at " -msgstr "Εγγραφή με Ψευδώνυμο στο " - -#: mod_muc/mod_muc.erl:722 mod_muc/mod_muc_odbc.erl:739 -msgid "Enter nickname you want to register" -msgstr "Πληκτρολογήστε το ψευδώνυμο που θέλετε να εγγραφείτε" - -#: mod_muc/mod_muc.erl:723 mod_muc/mod_muc_odbc.erl:740 -#: mod_muc/mod_muc_room.erl:3810 mod_roster.erl:944 mod_roster_odbc.erl:1061 -#: mod_vcard.erl:364 mod_vcard.erl:477 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:453 -msgid "Nickname" -msgstr "Ψευδώνυμο" - -#: mod_muc/mod_muc.erl:762 mod_muc/mod_muc_odbc.erl:784 -#: mod_muc/mod_muc_room.erl:1064 mod_muc/mod_muc_room.erl:1787 -msgid "That nickname is registered by another person" -msgstr "Αυτό το ψευδώνυμο είναι καταχωρημένο από άλλο πρόσωπο" - -#: mod_muc/mod_muc.erl:788 mod_muc/mod_muc_odbc.erl:811 -msgid "You must fill in field \"Nickname\" in the form" -msgstr "Θα πρέπει να συμπληρώσετε το πεδίο \"Ψευδώνυμο\" στη φόρμα" - -#: mod_muc/mod_muc.erl:808 mod_muc/mod_muc_odbc.erl:831 -msgid "ejabberd MUC module" -msgstr "ejabberd MUC module" - -#: mod_muc/mod_muc_log.erl:374 mod_muc/mod_muc_log.erl:381 -msgid "Chatroom configuration modified" -msgstr "Διαμόρφωση Αίθουσaς σύνεδριασης τροποποιηθηκε" - -#: mod_muc/mod_muc_log.erl:384 -msgid "joins the room" -msgstr "συνδέετε στην αίθουσα" - -#: mod_muc/mod_muc_log.erl:387 mod_muc/mod_muc_log.erl:390 -msgid "leaves the room" -msgstr "εγκαταλείπει την αίθουσα" - -#: mod_muc/mod_muc_log.erl:393 mod_muc/mod_muc_log.erl:396 -msgid "has been banned" -msgstr "έχει απαγορευθεί" - -#: mod_muc/mod_muc_log.erl:399 mod_muc/mod_muc_log.erl:402 -msgid "has been kicked" -msgstr "αποβλήθηκε " - -#: mod_muc/mod_muc_log.erl:405 -msgid "has been kicked because of an affiliation change" -msgstr "Έχει αποβληθεί λόγω αλλαγής υπαγωγής" - -#: mod_muc/mod_muc_log.erl:408 -msgid "has been kicked because the room has been changed to members-only" -msgstr "αποβλήθηκε επειδή η αίθουσα αλλάξε γιά μέλη μόνο" - -#: mod_muc/mod_muc_log.erl:411 -msgid "has been kicked because of a system shutdown" -msgstr "αποβλήθηκε λόγω τερματισμού συστήματος" - -#: mod_muc/mod_muc_log.erl:414 -msgid "is now known as" -msgstr "είναι τώρα γνωστή ως" - -#: mod_muc/mod_muc_log.erl:417 mod_muc/mod_muc_log.erl:688 -#: mod_muc/mod_muc_room.erl:2393 -msgid " has set the subject to: " -msgstr " έχει θέσει το θέμα σε: " - -#: mod_muc/mod_muc_log.erl:452 -msgid "Chatroom is created" -msgstr "Η αίθουσα σύνεδριασης δημιουργήθηκε" - -#: mod_muc/mod_muc_log.erl:453 -msgid "Chatroom is destroyed" -msgstr "Η αίθουσα σύνεδριασης διαγράφηκε" - -#: mod_muc/mod_muc_log.erl:454 -msgid "Chatroom is started" -msgstr "Η αίθουσα σύνεδριασης έχει ξεκινήσει" - -#: mod_muc/mod_muc_log.erl:455 -msgid "Chatroom is stopped" -msgstr "Η αίθουσα σύνεδριασης έχει σταματήσει" - -#: mod_muc/mod_muc_log.erl:459 -msgid "Monday" -msgstr "Δευτέρα" - -#: mod_muc/mod_muc_log.erl:460 -msgid "Tuesday" -msgstr "Τρίτη" - -#: mod_muc/mod_muc_log.erl:461 -msgid "Wednesday" -msgstr "Τετάρτη" - -#: mod_muc/mod_muc_log.erl:462 -msgid "Thursday" -msgstr "Πέμπτη" - -#: mod_muc/mod_muc_log.erl:463 -msgid "Friday" -msgstr "Παρασκευή" - -#: mod_muc/mod_muc_log.erl:464 -msgid "Saturday" -msgstr "Σάββατο" - -#: mod_muc/mod_muc_log.erl:465 -msgid "Sunday" -msgstr "Κυριακή" - -#: mod_muc/mod_muc_log.erl:469 -msgid "January" -msgstr "Ιανουάριος" - -#: mod_muc/mod_muc_log.erl:470 -msgid "February" -msgstr "Φεβρουάριος" - -#: mod_muc/mod_muc_log.erl:471 -msgid "March" -msgstr "Μάρτιος" - -#: mod_muc/mod_muc_log.erl:472 -msgid "April" -msgstr "Απρίλιος" - -#: mod_muc/mod_muc_log.erl:473 -msgid "May" -msgstr "Μάιος" - -#: mod_muc/mod_muc_log.erl:474 -msgid "June" -msgstr "Ιούνιος" - -#: mod_muc/mod_muc_log.erl:475 -msgid "July" -msgstr "Ιούλιος" - -#: mod_muc/mod_muc_log.erl:476 -msgid "August" -msgstr "Αύγουστος" - -#: mod_muc/mod_muc_log.erl:477 -msgid "September" -msgstr "Σεπτέμβριος" - -#: mod_muc/mod_muc_log.erl:478 -msgid "October" -msgstr "Οκτώβριος" - -#: mod_muc/mod_muc_log.erl:479 -msgid "November" -msgstr "Νοέμβριος" - -#: mod_muc/mod_muc_log.erl:480 -msgid "December" -msgstr "Δεκέμβριος" - -#: mod_muc/mod_muc_log.erl:750 -msgid "Room Configuration" -msgstr "Διαμόρφωση Αίθουσας σύνεδριασης" - -#: mod_muc/mod_muc_log.erl:759 -msgid "Room Occupants" -msgstr "Συμετεχόντες Αίθουσας σύνεδριασης" - -#: mod_muc/mod_muc_room.erl:174 -msgid "Traffic rate limit is exceeded" -msgstr "Υπέρφορτωση" - -#: mod_muc/mod_muc_room.erl:246 -msgid "" -"This participant is kicked from the room because he sent an error message" -msgstr "" -"Αυτός ο συμμετέχων αποβλήθηκε από την αίθουσα, επειδή έστειλε ένα μήνυμα " -"σφάλματος" - -#: mod_muc/mod_muc_room.erl:255 -msgid "It is not allowed to send private messages to the conference" -msgstr "Δεν επιτρέπεται να στείλει προσωπικά μηνύματα για τη διάσκεψη" - -#: mod_muc/mod_muc_room.erl:332 -msgid "Please, wait for a while before sending new voice request" -msgstr "Παρακαλώ, περιμένετε για λίγο πριν την αποστολή νέου αιτήματος φωνής" - -#: mod_muc/mod_muc_room.erl:347 -msgid "Voice requests are disabled in this conference" -msgstr "Τα αιτήματα φωνής είναι απενεργοποιημένα, σε αυτό το συνέδριο" - -#: mod_muc/mod_muc_room.erl:364 -msgid "Failed to extract JID from your voice request approval" -msgstr "Απέτυχε η εξαγωγή JID από την έγκριση του αιτήματος φωνής σας" - -#: mod_muc/mod_muc_room.erl:393 -msgid "Only moderators can approve voice requests" -msgstr "Μόνο οι συντονιστές μπορούν να εγκρίνουν τις αιτήσεις φωνής" - -#: mod_muc/mod_muc_room.erl:408 -msgid "Improper message type" -msgstr "Ακατάλληλο είδος μηνύματος" - -#: mod_muc/mod_muc_room.erl:518 -msgid "" -"This participant is kicked from the room because he sent an error message to " -"another participant" -msgstr "" -"Αυτός ο συμμετέχων αποβλήθηκε από την αίθουσα, επειδή έστειλε ένα μήνυμα " -"σφάλματος σε άλλον συμμετέχων" - -#: mod_muc/mod_muc_room.erl:531 -msgid "It is not allowed to send private messages of type \"groupchat\"" -msgstr "Δεν επιτρέπεται να στείλει προσωπικά μηνύματα του τύπου \"groupchat\"" - -#: mod_muc/mod_muc_room.erl:543 mod_muc/mod_muc_room.erl:611 -msgid "Recipient is not in the conference room" -msgstr "Παραλήπτης δεν είναι στην αίθουσα συνεδριάσεων" - -#: mod_muc/mod_muc_room.erl:564 mod_muc/mod_muc_room.erl:585 -msgid "It is not allowed to send private messages" -msgstr "Δεν επιτρέπεται η αποστολή προσωπικών μηνυμάτων" - -#: mod_muc/mod_muc_room.erl:576 mod_muc/mod_muc_room.erl:956 -#: mod_muc/mod_muc_room.erl:4040 -msgid "Only occupants are allowed to send messages to the conference" -msgstr "Μόνο οι συμμετέχωντες μπορούν να στέλνουν μηνύματα στο συνέδριο" - -#: mod_muc/mod_muc_room.erl:634 -msgid "Only occupants are allowed to send queries to the conference" -msgstr "Μόνο οι συμετεχόντες μπορούν να στείλουν ερωτήματα στη διάσκεψη" - -#: mod_muc/mod_muc_room.erl:646 -msgid "Queries to the conference members are not allowed in this room" -msgstr "" -"Ερωτήματα πρώς τα μέλη της διασκέψεως δεν επιτρέπονται σε αυτήν την αίθουσα" - -#: mod_muc/mod_muc_room.erl:932 -msgid "" -"Only moderators and participants are allowed to change the subject in this " -"room" -msgstr "" -"Μόνο οι συντονιστές και οι συμμετέχοντες μπορούν να αλλάξουν το θέμα αυτής " -"της αίθουσας" - -#: mod_muc/mod_muc_room.erl:937 -msgid "Only moderators are allowed to change the subject in this room" -msgstr "Μόνο οι συντονιστές μπορούν να αλλάξουν το θέμα αυτής της αίθουσας" - -#: mod_muc/mod_muc_room.erl:947 -msgid "Visitors are not allowed to send messages to all occupants" -msgstr "" -"Οι επισκέπτες δεν επιτρέπεται να στείλουν μηνύματα σε όλους τους " -"συμμετέχωντες" - -#: mod_muc/mod_muc_room.erl:1021 -msgid "" -"This participant is kicked from the room because he sent an error presence" -msgstr "" -"Αυτός ο συμμετέχων αποβλήθηκε από την αίθουσα, επειδή έστειλε σφάλμα " -"παρουσίας " - -#: mod_muc/mod_muc_room.erl:1040 -msgid "Visitors are not allowed to change their nicknames in this room" -msgstr "" -"Οι επισκέπτες δεν επιτρέπεται να αλλάξουν τα ψευδώνυμα τους σε αυτή την " -"αίθουσα" - -#: mod_muc/mod_muc_room.erl:1053 mod_muc/mod_muc_room.erl:1779 -msgid "That nickname is already in use by another occupant" -msgstr "Αυτό το ψευδώνυμο είναι ήδη σε χρήση από άλλον συμμετέχων" - -#: mod_muc/mod_muc_room.erl:1768 -msgid "You have been banned from this room" -msgstr "Σας έχει απαγορευθεί η είσοδος σε αυτή την αίθουσα" - -#: mod_muc/mod_muc_room.erl:1771 -msgid "Membership is required to enter this room" -msgstr "Απαιτείται αίτηση συμετοχής για είσοδο σε αυτή την αίθουσα" - -#: mod_muc/mod_muc_room.erl:1807 -msgid "This room is not anonymous" -msgstr "Η αίθουσα αυτή δεν είναι ανώνυμη" - -#: mod_muc/mod_muc_room.erl:1833 -msgid "A password is required to enter this room" -msgstr "Απαιτείται κωδικός πρόσβασης για είσοδο σε αυτή την αίθουσα" - -#: mod_muc/mod_muc_room.erl:1855 mod_register.erl:246 -msgid "Too many CAPTCHA requests" -msgstr "Πάρα πολλά αιτήματα CAPTCHA" - -#: mod_muc/mod_muc_room.erl:1864 mod_register.erl:251 -msgid "Unable to generate a CAPTCHA" -msgstr "Αδήνατο να δημιουργηθεί CAPTCHA" - -#: mod_muc/mod_muc_room.erl:1874 -msgid "Incorrect password" -msgstr "Εσφαλμένος κωδικός πρόσβασης" - -#: mod_muc/mod_muc_room.erl:2448 -msgid "Administrator privileges required" -msgstr "Aπαιτούνται προνόμια διαχειριστή" - -#: mod_muc/mod_muc_room.erl:2463 -msgid "Moderator privileges required" -msgstr "Aπαιτούνται προνόμια συντονιστή" - -#: mod_muc/mod_muc_room.erl:2619 -msgid "Jabber ID ~s is invalid" -msgstr "Η Jabber Ταυτότητα ~s είναι άκυρη" - -#: mod_muc/mod_muc_room.erl:2633 -msgid "Nickname ~s does not exist in the room" -msgstr "Ψευδώνυμο ~s δεν υπάρχει σε αυτή την αίθουσα" - -#: mod_muc/mod_muc_room.erl:2659 mod_muc/mod_muc_room.erl:3049 -msgid "Invalid affiliation: ~s" -msgstr "Άκυρη υπαγωγή: ~s" - -#: mod_muc/mod_muc_room.erl:2713 -msgid "Invalid role: ~s" -msgstr "Άκυρος ρόλο: ~s" - -#: mod_muc/mod_muc_room.erl:3026 mod_muc/mod_muc_room.erl:3062 -msgid "Owner privileges required" -msgstr "Aπαιτούνται προνόμια ιδιοκτήτη" - -#: mod_muc/mod_muc_room.erl:3195 -msgid "Configuration of room ~s" -msgstr "Διαμόρφωση Αίθουσας σύνεδριασης ~s" - -#: mod_muc/mod_muc_room.erl:3200 -msgid "Room title" -msgstr "Τίτλος Αίθουσας " - -#: mod_muc/mod_muc_room.erl:3203 mod_muc/mod_muc_room.erl:3692 -msgid "Room description" -msgstr "Περιγραφή Αίθουσας" - -#: mod_muc/mod_muc_room.erl:3210 -msgid "Make room persistent" -msgstr "Κάντε αίθουσα μόνιμη" - -#: mod_muc/mod_muc_room.erl:3215 -msgid "Make room public searchable" -msgstr "Κάντε την δημόσια αναζήτηση δυνατή για αυτή την αίθουσα" - -#: mod_muc/mod_muc_room.erl:3218 -msgid "Make participants list public" -msgstr "Κάντε κοινό τον κατάλογο συμμετεχόντων" - -#: mod_muc/mod_muc_room.erl:3221 -msgid "Make room password protected" -msgstr "Κάντε την αίθουσα προστατεύομενη με κωδικό πρόσβασης" - -#: mod_muc/mod_muc_room.erl:3232 -msgid "Maximum Number of Occupants" -msgstr "Μέγιστος αριθμός συμετεχόντων" - -#: mod_muc/mod_muc_room.erl:3239 -msgid "No limit" -msgstr "Χωρίς όριο" - -#: mod_muc/mod_muc_room.erl:3250 -msgid "Present real Jabber IDs to" -msgstr "Παρούσιαση πραγματικών ταυτοτήτων Jabber σε" - -#: mod_muc/mod_muc_room.erl:3258 mod_muc/mod_muc_room.erl:3292 -msgid "moderators only" -msgstr "συντονιστές μόνο" - -#: mod_muc/mod_muc_room.erl:3260 mod_muc/mod_muc_room.erl:3294 -msgid "anyone" -msgstr "οποιοσδήποτε" - -#: mod_muc/mod_muc_room.erl:3262 -msgid "Make room members-only" -msgstr "Κάντε την αίθουσα μόνο για μέλη" - -#: mod_muc/mod_muc_room.erl:3265 -msgid "Make room moderated" -msgstr "Κάντε την αίθουσα εποπτεύονομενη" - -#: mod_muc/mod_muc_room.erl:3268 -msgid "Default users as participants" -msgstr "Προεπιλογη χρήστων ως συμμετέχοντες" - -#: mod_muc/mod_muc_room.erl:3271 -msgid "Allow users to change the subject" -msgstr "Επιτρέψετε στους χρήστες να αλλάζουν το θέμα" - -#: mod_muc/mod_muc_room.erl:3274 -msgid "Allow users to send private messages" -msgstr "Επιτρέψετε στους χρήστες να αποστέλλουν ιδιωτικά μηνύματα" - -#: mod_muc/mod_muc_room.erl:3279 -msgid "Allow visitors to send private messages to" -msgstr "Επιτρέψετε στους χρήστες να αποστέλλουν ιδιωτικά μηνύματα σε" - -#: mod_muc/mod_muc_room.erl:3290 -msgid "nobody" -msgstr "κανείς" - -#: mod_muc/mod_muc_room.erl:3296 -msgid "Allow users to query other users" -msgstr "Επιτρέπστε στους χρήστες να ερωτούν άλλους χρήστες" - -#: mod_muc/mod_muc_room.erl:3299 -msgid "Allow users to send invites" -msgstr "Επιτρέψετε στους χρήστες να αποστέλλουν προσκλήσεις" - -#: mod_muc/mod_muc_room.erl:3302 -msgid "Allow visitors to send status text in presence updates" -msgstr "" -"Επιτρέψτε στους επισκέπτες να αποστέλλουν κατάσταση στις ενημερώσεις " -"παρουσίας" - -#: mod_muc/mod_muc_room.erl:3305 -msgid "Allow visitors to change nickname" -msgstr "Επιτρέψετε στους επισκέπτες να αλλάζου ψευδώνυμο" - -#: mod_muc/mod_muc_room.erl:3308 -msgid "Allow visitors to send voice requests" -msgstr "Επιτρέψτε στους επισκέπτες να στέλνουν αιτήματα φωνής" - -#: mod_muc/mod_muc_room.erl:3311 -msgid "Minimum interval between voice requests (in seconds)" -msgstr "Ελάχιστο χρονικό διάστημα μεταξύ αιτημάτων φωνής (σε δευτερόλεπτα)" - -#: mod_muc/mod_muc_room.erl:3317 -msgid "Make room CAPTCHA protected" -msgstr "Κάντε την αίθουσα CAPTCHA προστατεύονομενη" - -#: mod_muc/mod_muc_room.erl:3322 -msgid "Exclude Jabber IDs from CAPTCHA challenge" -msgstr "Εξαίρεση από τις ταυτότητες Jabber, ή CAPTCHA πρόκληση" - -#: mod_muc/mod_muc_room.erl:3329 -msgid "Enable logging" -msgstr "Ενεργοποίηση καταγραφής" - -#: mod_muc/mod_muc_room.erl:3337 -msgid "You need an x:data capable client to configure room" -msgstr "Χρειάζεστε ένα x:data ικανό πελάτη για να ρυθμίσετε την αίθουσα " - -#: mod_muc/mod_muc_room.erl:3694 -msgid "Number of occupants" -msgstr "Αριθμός συμετεχόντων" - -#: mod_muc/mod_muc_room.erl:3750 -msgid "private, " -msgstr "ιδιωτικό," - -#: mod_muc/mod_muc_room.erl:3799 -msgid "Voice request" -msgstr "Αίτημα φωνής" - -#: mod_muc/mod_muc_room.erl:3803 -msgid "Either approve or decline the voice request." -msgstr "Είτε εγκρίνετε ή απορρίψτε το αίτημα φωνής." - -#: mod_muc/mod_muc_room.erl:3809 -msgid "User JID" -msgstr "JID Χρήστη" - -#: mod_muc/mod_muc_room.erl:3811 -msgid "Grant voice to this person?" -msgstr "Παραχώρηση φωνής σε αυτό το άτομο;" - -#: mod_muc/mod_muc_room.erl:3960 -msgid "~s invites you to the room ~s" -msgstr "~s σας προσκαλεί στην αίθουσα ~s" - -#: mod_muc/mod_muc_room.erl:3969 -msgid "the password is" -msgstr "ο κωδικός πρόσβασης είναι" - -#: mod_offline.erl:510 mod_offline_odbc.erl:352 -msgid "" -"Your contact offline message queue is full. The message has been discarded." -msgstr "" -"Η μνήμη χωρίς σύνδεση μήνυματών είναι πλήρης. Το μήνυμα έχει απορριφθεί." - -#: mod_offline.erl:560 mod_offline_odbc.erl:408 -msgid "~s's Offline Messages Queue" -msgstr "Η Σειρά Χωρίς Σύνδεση Μηνύματων τού ~s" - -#: mod_offline.erl:563 mod_offline_odbc.erl:411 mod_roster.erl:987 -#: mod_roster_odbc.erl:1104 mod_shared_roster.erl:901 -#: mod_shared_roster.erl:1010 mod_shared_roster_odbc.erl:930 -#: mod_shared_roster_odbc.erl:1039 web/ejabberd_web_admin.erl:833 -#: web/ejabberd_web_admin.erl:874 web/ejabberd_web_admin.erl:942 -#: web/ejabberd_web_admin.erl:978 web/ejabberd_web_admin.erl:1019 -#: web/ejabberd_web_admin.erl:1508 web/ejabberd_web_admin.erl:1760 -#: web/ejabberd_web_admin.erl:1931 web/ejabberd_web_admin.erl:1963 -#: web/ejabberd_web_admin.erl:2031 web/ejabberd_web_admin.erl:2135 -#: web/ejabberd_web_admin.erl:2160 web/ejabberd_web_admin.erl:2248 -msgid "Submitted" -msgstr "Υποβλήθηκε" - -#: mod_offline.erl:571 -msgid "Time" -msgstr "Χρόνος" - -#: mod_offline.erl:572 -msgid "From" -msgstr "Από" - -#: mod_offline.erl:573 -msgid "To" -msgstr "Πρώς" - -#: mod_offline.erl:574 mod_offline_odbc.erl:419 -msgid "Packet" -msgstr "Πακέτο" - -#: mod_offline.erl:587 mod_offline_odbc.erl:432 mod_shared_roster.erl:908 -#: mod_shared_roster_odbc.erl:937 web/ejabberd_web_admin.erl:882 -#: web/ejabberd_web_admin.erl:986 -msgid "Delete Selected" -msgstr "Διαγραφή επιλεγμένων" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -msgid "Offline Messages:" -msgstr "Χωρίς Σύνδεση Μηνύματα:" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -msgid "Remove All Offline Messages" -msgstr "Αφαίρεση Όλων των Χωρίς Σύνδεση Μηνύματων" - -#: mod_proxy65/mod_proxy65_service.erl:213 -msgid "ejabberd SOCKS5 Bytestreams module" -msgstr "ejabberd SOCKS5 Bytestreams module" - -#: mod_pubsub/mod_pubsub.erl:1120 mod_pubsub/mod_pubsub_odbc.erl:929 -msgid "Publish-Subscribe" -msgstr "Δημοσίευση-Εγγραφή" - -#: mod_pubsub/mod_pubsub.erl:1214 mod_pubsub/mod_pubsub_odbc.erl:1025 -msgid "ejabberd Publish-Subscribe module" -msgstr "ejabberd module Δημοσίευσης-Εγγραφής" - -#: mod_pubsub/mod_pubsub.erl:1497 mod_pubsub/mod_pubsub_odbc.erl:1312 -msgid "PubSub subscriber request" -msgstr "Αίτηση συνδρομητή Δημοσίευσης-Εγγραφής" - -#: mod_pubsub/mod_pubsub.erl:1499 mod_pubsub/mod_pubsub_odbc.erl:1314 -msgid "Choose whether to approve this entity's subscription." -msgstr "Επιλέξτε αν θα εγκρίθεί η εγγραφή αυτής της οντότητας." - -#: mod_pubsub/mod_pubsub.erl:1505 mod_pubsub/mod_pubsub_odbc.erl:1320 -msgid "Node ID" -msgstr "Ταυτότητα Κόμβου" - -#: mod_pubsub/mod_pubsub.erl:1510 mod_pubsub/mod_pubsub_odbc.erl:1325 -msgid "Subscriber Address" -msgstr "Διεύθυνση Συνδρομητή" - -#: mod_pubsub/mod_pubsub.erl:1516 mod_pubsub/mod_pubsub_odbc.erl:1331 -msgid "Allow this Jabber ID to subscribe to this pubsub node?" -msgstr "" -"Επιτρέπετε σε αυτή την Jabber Ταυτότητα να εγγραφεί σε αυτό τον κόμβο " -"Δημοσίευσης-Εγγραφής;" - -#: mod_pubsub/mod_pubsub.erl:3474 mod_pubsub/mod_pubsub_odbc.erl:3287 -msgid "Deliver payloads with event notifications" -msgstr "Κοινοποιήσεις με την παράδοση φορτίων" - -#: mod_pubsub/mod_pubsub.erl:3475 mod_pubsub/mod_pubsub_odbc.erl:3288 -msgid "Deliver event notifications" -msgstr "Κοινοποιήσεις παράδοσης" - -#: mod_pubsub/mod_pubsub.erl:3476 mod_pubsub/mod_pubsub_odbc.erl:3289 -msgid "Notify subscribers when the node configuration changes" -msgstr "Ειδοποιηση στους συνδρομητές όταν αλλάζει η διαμόρφωση κόμβου" - -#: mod_pubsub/mod_pubsub.erl:3477 mod_pubsub/mod_pubsub_odbc.erl:3290 -msgid "Notify subscribers when the node is deleted" -msgstr "Ειδοποιηση στους συνδρομητές όταν ο κόμβος διαγράφεται" - -#: mod_pubsub/mod_pubsub.erl:3478 mod_pubsub/mod_pubsub_odbc.erl:3291 -msgid "Notify subscribers when items are removed from the node" -msgstr "Ειδοποιηση στους συνδρομητές όταν αφαίρούντε στοιχεία από τον κόμβο" - -#: mod_pubsub/mod_pubsub.erl:3479 mod_pubsub/mod_pubsub_odbc.erl:3292 -msgid "Persist items to storage" -msgstr "Μονιμη αποθήκευση στοιχείων" - -#: mod_pubsub/mod_pubsub.erl:3480 mod_pubsub/mod_pubsub_odbc.erl:3293 -msgid "A friendly name for the node" -msgstr "Ένα φιλικό όνομα για τον κόμβο" - -#: mod_pubsub/mod_pubsub.erl:3481 mod_pubsub/mod_pubsub_odbc.erl:3294 -msgid "Max # of items to persist" -msgstr "Μέγιστος αριθμός μόνιμων στοιχείων" - -#: mod_pubsub/mod_pubsub.erl:3482 mod_pubsub/mod_pubsub_odbc.erl:3295 -msgid "Whether to allow subscriptions" -msgstr "Εάν επιτρέποντε συνδρομές" - -#: mod_pubsub/mod_pubsub.erl:3483 mod_pubsub/mod_pubsub_odbc.erl:3296 -msgid "Specify the access model" -msgstr "Καθορίστε το μοντέλο πρόσβασης" - -#: mod_pubsub/mod_pubsub.erl:3486 mod_pubsub/mod_pubsub_odbc.erl:3299 -msgid "Roster groups allowed to subscribe" -msgstr "Ομάδες Καταλόγου Επαφών μπορούν να εγγραφούν" - -#: mod_pubsub/mod_pubsub.erl:3487 mod_pubsub/mod_pubsub_odbc.erl:3300 -msgid "Specify the publisher model" -msgstr "Καθορίστε το μοντέλο εκδότη" - -#: mod_pubsub/mod_pubsub.erl:3489 mod_pubsub/mod_pubsub_odbc.erl:3302 -msgid "Purge all items when the relevant publisher goes offline" -msgstr "Διαγραφή όλων των στοιχείων όταν ο σχετικός εκδότης αποσυνδέεται" - -#: mod_pubsub/mod_pubsub.erl:3490 mod_pubsub/mod_pubsub_odbc.erl:3303 -msgid "Specify the event message type" -msgstr "Καθορίστε τον τύπο μηνύματος συμβάντος" - -#: mod_pubsub/mod_pubsub.erl:3492 mod_pubsub/mod_pubsub_odbc.erl:3305 -msgid "Max payload size in bytes" -msgstr "Μέγιστο μέγεθος φορτίου σε bytes" - -#: mod_pubsub/mod_pubsub.erl:3493 mod_pubsub/mod_pubsub_odbc.erl:3306 -msgid "When to send the last published item" -msgstr "Πότε να αποσταλθεί το τελευταίο στοιχείο που δημοσιεύθηκε" - -#: mod_pubsub/mod_pubsub.erl:3495 mod_pubsub/mod_pubsub_odbc.erl:3308 -msgid "Only deliver notifications to available users" -msgstr "Παράδωση κοινοποιήσεων μόνο σε διαθέσιμους χρήστες" - -#: mod_pubsub/mod_pubsub.erl:3496 mod_pubsub/mod_pubsub_odbc.erl:3309 -msgid "The collections with which a node is affiliated" -msgstr "Οι συλλογές με την οποία είναι ένας κόμβος συνδέεται" - -#: mod_register.erl:193 -msgid "The CAPTCHA verification has failed" -msgstr "Η επαλήθευση της εικόνας CAPTCHA απέτυχε" - -#: mod_register.erl:220 -msgid "You need a client that supports x:data and CAPTCHA to register" -msgstr "Χρειάζεστε ένα x:data και CAPTCHA ικανό πελάτη για εγγραφή" - -#: mod_register.erl:226 mod_register.erl:265 -msgid "Choose a username and password to register with this server" -msgstr "" -"Επιλέξτε ένα όνομα χρήστη και κωδικό πρόσβασης για να εγγραφείτε σε αυτό τον " -"διακομιστή" - -#: mod_register.erl:230 mod_vcard.erl:364 mod_vcard_odbc.erl:342 -#: web/ejabberd_web_admin.erl:1515 web/ejabberd_web_admin.erl:1572 -msgid "User" -msgstr "Χρήστης" - -#: mod_register.erl:316 mod_register.erl:361 -msgid "The password is too weak" -msgstr "Ο κωδικός πρόσβασης είναι πολύ ασθενές" - -#: mod_register.erl:365 -msgid "Users are not allowed to register accounts so quickly" -msgstr "Οι χρήστες δεν επιτρέπεται να εγγραφούν λογαριασμούς τόσο γρήγορα" - -#: mod_roster.erl:938 mod_roster_odbc.erl:1055 web/ejabberd_web_admin.erl:1701 -#: web/ejabberd_web_admin.erl:1886 web/ejabberd_web_admin.erl:1897 -#: web/ejabberd_web_admin.erl:2219 -msgid "None" -msgstr "Κανένα" - -#: mod_roster.erl:945 mod_roster_odbc.erl:1062 -msgid "Subscription" -msgstr "Συνδρομή" - -#: mod_roster.erl:946 mod_roster_odbc.erl:1063 -msgid "Pending" -msgstr "Εκκρεμεί" - -#: mod_roster.erl:947 mod_roster_odbc.erl:1064 -msgid "Groups" -msgstr "Ομάδες" - -#: mod_roster.erl:974 mod_roster_odbc.erl:1091 -msgid "Validate" -msgstr "Επαληθεύστε" - -#: mod_roster.erl:982 mod_roster_odbc.erl:1099 -msgid "Remove" -msgstr "Αφαίρεστε" - -#: mod_roster.erl:985 mod_roster_odbc.erl:1102 -msgid "Roster of " -msgstr "Καταλόγος Επαφών τού" - -#: mod_roster.erl:988 mod_roster_odbc.erl:1105 mod_shared_roster.erl:902 -#: mod_shared_roster.erl:1011 mod_shared_roster_odbc.erl:931 -#: mod_shared_roster_odbc.erl:1040 web/ejabberd_web_admin.erl:834 -#: web/ejabberd_web_admin.erl:875 web/ejabberd_web_admin.erl:943 -#: web/ejabberd_web_admin.erl:979 web/ejabberd_web_admin.erl:1020 -#: web/ejabberd_web_admin.erl:1509 web/ejabberd_web_admin.erl:1761 -#: web/ejabberd_web_admin.erl:1932 web/ejabberd_web_admin.erl:2136 -#: web/ejabberd_web_admin.erl:2161 -msgid "Bad format" -msgstr "Ακατάλληλη μορφή" - -#: mod_roster.erl:995 mod_roster_odbc.erl:1112 -msgid "Add Jabber ID" -msgstr "Προσθήκη Jabber Ταυτότητας" - -#: mod_roster.erl:1094 mod_roster_odbc.erl:1211 -msgid "Roster" -msgstr "Καταλόγος Επαφών" - -#: mod_shared_roster.erl:857 mod_shared_roster.erl:899 -#: mod_shared_roster.erl:1007 mod_shared_roster_odbc.erl:886 -#: mod_shared_roster_odbc.erl:928 mod_shared_roster_odbc.erl:1036 -msgid "Shared Roster Groups" -msgstr "Κοινές Ομάδες Καταλόγων Επαφών" - -#: mod_shared_roster.erl:895 mod_shared_roster_odbc.erl:924 -#: web/ejabberd_web_admin.erl:1365 web/ejabberd_web_admin.erl:2461 -msgid "Add New" -msgstr "Προσθήκη νέου" - -#: mod_shared_roster.erl:978 mod_shared_roster_odbc.erl:1007 -msgid "Name:" -msgstr "Όνομα:" - -#: mod_shared_roster.erl:983 mod_shared_roster_odbc.erl:1012 -msgid "Description:" -msgstr "Περιγραφή:" - -#: mod_shared_roster.erl:991 mod_shared_roster_odbc.erl:1020 -msgid "Members:" -msgstr "Μέλη:" - -#: mod_shared_roster.erl:999 mod_shared_roster_odbc.erl:1028 -msgid "Displayed Groups:" -msgstr "Εμφανίσμενες Ομάδες:" - -#: mod_shared_roster.erl:1008 mod_shared_roster_odbc.erl:1037 -msgid "Group " -msgstr "Ομάδα" - -#: mod_shared_roster.erl:1017 mod_shared_roster_odbc.erl:1046 -#: web/ejabberd_web_admin.erl:840 web/ejabberd_web_admin.erl:884 -#: web/ejabberd_web_admin.erl:949 web/ejabberd_web_admin.erl:1026 -#: web/ejabberd_web_admin.erl:2017 -msgid "Submit" -msgstr "Υποβοβολή" - -#: mod_vcard.erl:165 mod_vcard_ldap.erl:238 mod_vcard_odbc.erl:129 -msgid "Erlang Jabber Server" -msgstr "Erlang Jabber Διακομιστής" - -#: mod_vcard.erl:364 mod_vcard.erl:478 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:454 -msgid "Birthday" -msgstr "Γενέθλια" - -#: mod_vcard.erl:364 mod_vcard.erl:480 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:456 -msgid "City" -msgstr "Πόλη" - -#: mod_vcard.erl:364 mod_vcard.erl:479 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:455 -msgid "Country" -msgstr "Χώρα" - -#: mod_vcard.erl:364 mod_vcard.erl:481 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:457 -msgid "Email" -msgstr "Email" - -#: mod_vcard.erl:364 mod_vcard.erl:476 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:452 -msgid "Family Name" -msgstr "Επώνυμο" - -#: mod_vcard.erl:364 mod_vcard_odbc.erl:342 -msgid "" -"Fill in the form to search for any matching Jabber User (Add * to the end of " -"field to match substring)" -msgstr "" -"Συμπληρώστε τη φόρμα για να αναζητήσετε οποιαδήποτε Jabber χρήστη που " -"ταιριάζει (Προσθέστε * στο τέλος τού πεδίου για να ταιριάξει σε μεγαλύτερες " -"γραμματοσηρές)" - -#: mod_vcard.erl:364 mod_vcard.erl:473 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:449 -msgid "Full Name" -msgstr "Ονοματεπώνυμο" - -#: mod_vcard.erl:364 mod_vcard.erl:475 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:451 -msgid "Middle Name" -msgstr "Πατρώνυμο" - -#: mod_vcard.erl:364 mod_vcard.erl:474 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:450 web/ejabberd_web_admin.erl:2006 -msgid "Name" -msgstr "Όνομα" - -#: mod_vcard.erl:364 mod_vcard.erl:482 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:458 -msgid "Organization Name" -msgstr "Όνομα Οργανισμού" - -#: mod_vcard.erl:364 mod_vcard.erl:483 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:459 -msgid "Organization Unit" -msgstr "Μονάδα Οργανισμού" - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "Search users in " -msgstr "Αναζήτηση χρηστών στο" - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "You need an x:data capable client to search" -msgstr "Χρειάζεστε ένα x:data ικανό πελάτη για αναζήτηση" - -#: mod_vcard.erl:389 mod_vcard_ldap.erl:487 mod_vcard_odbc.erl:367 -msgid "vCard User Search" -msgstr "vCard Αναζήτηση χρηστών" - -#: mod_vcard.erl:445 mod_vcard_ldap.erl:541 mod_vcard_odbc.erl:421 -msgid "ejabberd vCard module" -msgstr "ejabberd vCard module" - -#: mod_vcard.erl:469 mod_vcard_ldap.erl:551 mod_vcard_odbc.erl:445 -msgid "Search Results for " -msgstr "Αποτελέσματα αναζήτησης για " - -#: mod_vcard_ldap.erl:462 -msgid "Fill in fields to search for any matching Jabber User" -msgstr "" -"Συμπληρώστε τα πεδία για να αναζητήσετε οποιαδήποτε ταιριάζοντα Jabber χρήστη" - -#: web/ejabberd_web_admin.erl:193 web/ejabberd_web_admin.erl:203 -#: web/ejabberd_web_admin.erl:219 web/ejabberd_web_admin.erl:229 -msgid "Unauthorized" -msgstr "Χορίς Εξουσιοδότηση" - -#: web/ejabberd_web_admin.erl:286 web/ejabberd_web_admin.erl:303 -msgid "ejabberd Web Admin" -msgstr "ejabberd Web Admin" - -#: web/ejabberd_web_admin.erl:769 web/ejabberd_web_admin.erl:780 -msgid "Administration" -msgstr "Διαχείριση" - -#: web/ejabberd_web_admin.erl:878 web/ejabberd_web_admin.erl:982 -msgid "Raw" -msgstr "Ακατέργαστο" - -#: web/ejabberd_web_admin.erl:1017 -msgid "~s access rule configuration" -msgstr "~s διαμόρφωση κανόνα πρόσβασης" - -#: web/ejabberd_web_admin.erl:1035 -msgid "Virtual Hosts" -msgstr "εικονικοί κεντρικοί υπολογιστές" - -#: web/ejabberd_web_admin.erl:1043 web/ejabberd_web_admin.erl:1050 -msgid "Users" -msgstr "Χρήστες" - -#: web/ejabberd_web_admin.erl:1078 -msgid "Users Last Activity" -msgstr "Τελευταία Δραστηριότητα Χρήστη" - -#: web/ejabberd_web_admin.erl:1080 -msgid "Period: " -msgstr "Περίοδος: " - -#: web/ejabberd_web_admin.erl:1090 -msgid "Last month" -msgstr "Περασμένο μήνα" - -#: web/ejabberd_web_admin.erl:1091 -msgid "Last year" -msgstr "Πέρυσι" - -#: web/ejabberd_web_admin.erl:1092 -msgid "All activity" -msgstr "Όλες οι δραστηριότητες" - -#: web/ejabberd_web_admin.erl:1094 -msgid "Show Ordinary Table" -msgstr "Δείτε Κοινό Πίνακα" - -#: web/ejabberd_web_admin.erl:1096 -msgid "Show Integral Table" -msgstr "Δείτε Ολοκληρωτικό Πίνακα" - -#: web/ejabberd_web_admin.erl:1105 web/ejabberd_web_admin.erl:1939 -msgid "Statistics" -msgstr "Στατιστικές" - -#: web/ejabberd_web_admin.erl:1117 -msgid "Not Found" -msgstr "Δεν Βρέθηκε" - -#: web/ejabberd_web_admin.erl:1134 -msgid "Node not found" -msgstr "Κόμβος δεν βρέθηκε" - -#: web/ejabberd_web_admin.erl:1460 -msgid "Host" -msgstr "Κεντρικός Υπολογιστής" - -#: web/ejabberd_web_admin.erl:1461 -msgid "Registered Users" -msgstr "Εγγεγραμμένοι Χρήστες" - -#: web/ejabberd_web_admin.erl:1573 -msgid "Offline Messages" -msgstr "Χωρίς Σύνδεση Μηνύματα" - -#: web/ejabberd_web_admin.erl:1574 web/ejabberd_web_admin.erl:1767 -msgid "Last Activity" -msgstr "Τελευταία Δραστηριότητα" - -#: web/ejabberd_web_admin.erl:1659 web/ejabberd_web_admin.erl:1675 -msgid "Registered Users:" -msgstr "Εγγεγραμμένοι Χρήστες:" - -#: web/ejabberd_web_admin.erl:1661 web/ejabberd_web_admin.erl:1677 -#: web/ejabberd_web_admin.erl:2192 -msgid "Online Users:" -msgstr "Online Χρήστες:" - -#: web/ejabberd_web_admin.erl:1663 -msgid "Outgoing s2s Connections:" -msgstr "Εξερχόμενες S2S Συνδέσεις:" - -#: web/ejabberd_web_admin.erl:1665 -msgid "Outgoing s2s Servers:" -msgstr "Εξερχόμενοι S2S διακομιστές:" - -#: web/ejabberd_web_admin.erl:1734 web/mod_register_web.erl:175 -#: web/mod_register_web.erl:372 web/mod_register_web.erl:381 -#: web/mod_register_web.erl:411 -msgid "Change Password" -msgstr "Αλλαγή κωδικού" - -#: web/ejabberd_web_admin.erl:1758 -msgid "User " -msgstr "Χρήστης" - -#: web/ejabberd_web_admin.erl:1765 -msgid "Connected Resources:" -msgstr "Συνδεδεμένοι Πόροι:" - -#: web/ejabberd_web_admin.erl:1766 web/mod_register_web.erl:227 -#: web/mod_register_web.erl:519 -msgid "Password:" -msgstr "Κωδικός πρόσβασης:" - -#: web/ejabberd_web_admin.erl:1828 -msgid "No Data" -msgstr "Κανένα στοιχείο" - -#: web/ejabberd_web_admin.erl:1906 -msgid "Nodes" -msgstr "Κόμβοι" - -#: web/ejabberd_web_admin.erl:1929 web/ejabberd_web_admin.erl:1951 -msgid "Node " -msgstr "Κόμβος" - -#: web/ejabberd_web_admin.erl:1938 -msgid "Listened Ports" -msgstr "Παρακολουθούμενες Θύρες" - -#: web/ejabberd_web_admin.erl:1940 web/ejabberd_web_admin.erl:2260 -#: web/ejabberd_web_admin.erl:2448 -msgid "Update" -msgstr "Ενημέρωση" - -#: web/ejabberd_web_admin.erl:1943 web/ejabberd_web_admin.erl:2569 -msgid "Restart" -msgstr "Επανεκκίνηση" - -#: web/ejabberd_web_admin.erl:1945 web/ejabberd_web_admin.erl:2571 -msgid "Stop" -msgstr "Σταμάτημα" - -#: web/ejabberd_web_admin.erl:1959 -msgid "RPC Call Error" -msgstr "Σφάλμα RPC Κλήσης" - -#: web/ejabberd_web_admin.erl:2000 -msgid "Database Tables at " -msgstr "Πίνακες βάσης δεδομένων στο " - -#: web/ejabberd_web_admin.erl:2007 -msgid "Storage Type" -msgstr "Τύπος Αποθήκευσης" - -#: web/ejabberd_web_admin.erl:2008 -msgid "Elements" -msgstr "Στοιχεία" - -#: web/ejabberd_web_admin.erl:2009 -msgid "Memory" -msgstr "Μνήμη" - -#: web/ejabberd_web_admin.erl:2032 web/ejabberd_web_admin.erl:2137 -msgid "Error" -msgstr "Σφάλμα" - -#: web/ejabberd_web_admin.erl:2034 -msgid "Backup of " -msgstr "Αντιγράφο Ασφαλείας του " - -#: web/ejabberd_web_admin.erl:2036 -msgid "" -"Please note that these options will only backup the builtin Mnesia database. " -"If you are using the ODBC module, you also need to backup your SQL database " -"separately." -msgstr "" -"Παρακαλώ σημειώστε ότι οι επιλογές αυτές θα αποθήκευσουν Αντιγράφο Ασφαλείας " -"μόνο της ενσωματωμένης βάσης δεδομένων Mnesia. Εάν χρησιμοποιείτε το module " -"ODBC, θα πρέπει επίσης να κάνετε χωριστά Αντιγράφο Ασφαλείας της SQL βάση " -"δεδομένων σας ." - -#: web/ejabberd_web_admin.erl:2041 -msgid "Store binary backup:" -msgstr "Αποθηκεύση δυαδικού αντιγράφου ασφαλείας:" - -#: web/ejabberd_web_admin.erl:2045 web/ejabberd_web_admin.erl:2052 -#: web/ejabberd_web_admin.erl:2060 web/ejabberd_web_admin.erl:2067 -#: web/ejabberd_web_admin.erl:2074 web/ejabberd_web_admin.erl:2081 -#: web/ejabberd_web_admin.erl:2088 web/ejabberd_web_admin.erl:2096 -#: web/ejabberd_web_admin.erl:2103 web/ejabberd_web_admin.erl:2110 -msgid "OK" -msgstr "Όλλα Καλά" - -#: web/ejabberd_web_admin.erl:2048 -msgid "Restore binary backup immediately:" -msgstr "Επαναφορά δυαδικού αντιγράφου ασφαλείας αμέσως:" - -#: web/ejabberd_web_admin.erl:2056 -msgid "" -"Restore binary backup after next ejabberd restart (requires less memory):" -msgstr "" -"Επαναφορά δυαδικού αντιγράφου ασφαλείας μετά την επόμενη επανεκκίνηση του " -"ejabberd (απαιτεί λιγότερη μνήμη):" - -#: web/ejabberd_web_admin.erl:2063 -msgid "Store plain text backup:" -msgstr "Αποθηκεύση αντιγράφου ασφαλείας σε αρχείο κειμένου:" - -#: web/ejabberd_web_admin.erl:2070 -msgid "Restore plain text backup immediately:" -msgstr "Επαναφορά αντιγράφου ασφαλείας από αρχείο κειμένου αμέσως:" - -#: web/ejabberd_web_admin.erl:2077 -msgid "Import users data from a PIEFXIS file (XEP-0227):" -msgstr "Εισαγωγή δεδομένων χρηστών από ένα αρχείο PIEFXIS (XEP-0227):" - -#: web/ejabberd_web_admin.erl:2084 -msgid "Export data of all users in the server to PIEFXIS files (XEP-0227):" -msgstr "" -"Εξαγωγή δεδομένων όλων των χρηστών του διακομιστή σε PIEFXIS αρχεία " -"(XEP-0227):" - -#: web/ejabberd_web_admin.erl:2091 -msgid "Export data of users in a host to PIEFXIS files (XEP-0227):" -msgstr "" -"Εξαγωγή δεδομένων των χρηστών κεντρικού υπολογιστή σε PIEFXIS αρχεία " -"(XEP-0227):" - -#: web/ejabberd_web_admin.erl:2099 -msgid "Import user data from jabberd14 spool file:" -msgstr "Εισαγωγή δεδομένων χρήστη από το αρχείο σειράς jabberd14:" - -#: web/ejabberd_web_admin.erl:2106 -msgid "Import users data from jabberd14 spool directory:" -msgstr "Εισαγωγή δεδομένων χρηστών από κατάλογο αρχείων σειράς jabberd14:" - -#: web/ejabberd_web_admin.erl:2132 -msgid "Listened Ports at " -msgstr "Παρακολουθούμενες Θύρες στο " - -#: web/ejabberd_web_admin.erl:2157 -msgid "Modules at " -msgstr "Modules στο " - -#: web/ejabberd_web_admin.erl:2183 -msgid "Statistics of ~p" -msgstr "Στατιστικές του ~p" - -#: web/ejabberd_web_admin.erl:2186 -msgid "Uptime:" -msgstr "Uptime:" - -#: web/ejabberd_web_admin.erl:2189 -msgid "CPU Time:" -msgstr "Ώρα CPU:" - -#: web/ejabberd_web_admin.erl:2195 -msgid "Transactions Committed:" -msgstr "Παραδοθείς συναλλαγές:" - -#: web/ejabberd_web_admin.erl:2198 -msgid "Transactions Aborted:" -msgstr "Αποτυχημένες συναλλαγές:" - -#: web/ejabberd_web_admin.erl:2201 -msgid "Transactions Restarted:" -msgstr "Επανειλημμένες συναλλαγές:" - -#: web/ejabberd_web_admin.erl:2204 -msgid "Transactions Logged:" -msgstr "Καταγραμμένες συναλλαγές:" - -#: web/ejabberd_web_admin.erl:2246 -msgid "Update " -msgstr "Ενημέρωση" - -#: web/ejabberd_web_admin.erl:2254 -msgid "Update plan" -msgstr "Σχέδιο ενημέρωσης" - -#: web/ejabberd_web_admin.erl:2255 -msgid "Modified modules" -msgstr "Τροποποιημένα modules" - -#: web/ejabberd_web_admin.erl:2256 -msgid "Update script" -msgstr "Προγράμα ενημέρωσης" - -#: web/ejabberd_web_admin.erl:2257 -msgid "Low level update script" -msgstr "Προγράμα ενημέρωσης χαμηλού επίπεδου " - -#: web/ejabberd_web_admin.erl:2258 -msgid "Script check" -msgstr "Script ελέγχου" - -#: web/ejabberd_web_admin.erl:2426 -msgid "Port" -msgstr "Θύρα" - -#: web/ejabberd_web_admin.erl:2427 -msgid "IP" -msgstr "IP" - -#: web/ejabberd_web_admin.erl:2428 -msgid "Protocol" -msgstr "Πρωτόκολλο" - -#: web/ejabberd_web_admin.erl:2429 web/ejabberd_web_admin.erl:2556 -msgid "Module" -msgstr "Module" - -#: web/ejabberd_web_admin.erl:2430 web/ejabberd_web_admin.erl:2557 -msgid "Options" -msgstr "Επιλογές" - -#: web/ejabberd_web_admin.erl:2450 -msgid "Delete" -msgstr "Διαγραφή" - -#: web/ejabberd_web_admin.erl:2579 -msgid "Start" -msgstr "Εκκίνηση" - -#: web/mod_register_web.erl:104 -msgid "Your Jabber account was successfully created." -msgstr "Ο Jabber λογαριασμός σας δημιουργήθηκε με επιτυχία." - -#: web/mod_register_web.erl:107 -msgid "There was an error creating the account: " -msgstr "Υπήρξε ένα σφάλμα κατά τη δημιουργία του λογαριασμού:" - -#: web/mod_register_web.erl:115 -msgid "Your Jabber account was successfully deleted." -msgstr "Ο Jabber λογαριασμός σας διαγράφηκε με επιτυχία." - -#: web/mod_register_web.erl:118 -msgid "There was an error deleting the account: " -msgstr "Υπήρξε ένα σφάλμα κατά τη διαγραφή του λογαριασμού:" - -#: web/mod_register_web.erl:128 -msgid "The password of your Jabber account was successfully changed." -msgstr "Ο κωδικός πρόσβασης του Jabber λογαριασμού σας έχει αλλάξει επιτυχώς." - -#: web/mod_register_web.erl:131 -msgid "There was an error changing the password: " -msgstr "Υπήρξε ένα σφάλμα κατά την αλλαγή του κωδικού πρόσβασης:" - -#: web/mod_register_web.erl:163 web/mod_register_web.erl:172 -msgid "Jabber Account Registration" -msgstr "Εγγραφή λογαριασμού Jabber" - -#: web/mod_register_web.erl:174 web/mod_register_web.erl:192 -#: web/mod_register_web.erl:201 -msgid "Register a Jabber account" -msgstr "Καταχωρήστε έναν λογαριασμό Jabber" - -#: web/mod_register_web.erl:176 web/mod_register_web.erl:493 -#: web/mod_register_web.erl:502 -msgid "Unregister a Jabber account" -msgstr "Καταργήστε την εγγραφή ενός λογαριασμού Jabber" - -#: web/mod_register_web.erl:203 -msgid "" -"This page allows to create a Jabber account in this Jabber server. Your JID " -"(Jabber IDentifier) will be of the form: username@server. Please read " -"carefully the instructions to fill correctly the fields." -msgstr "" -"Αυτή η σελίδα σας επιτρέπει να δημιουργήσετε ένα λογαριασμό Jabber σε αυτόν " -"το διακομιστή Jabber. JID σας (Jabber Identifier) θα είναι της μορφής: " -"όνομα_χρήστη@διακομιστής_Jabber. Παρακαλώ διαβάστε προσεκτικά τις οδηγίες " -"για να συμπληρώσετε σωστά τα πεδία." - -#: web/mod_register_web.erl:212 web/mod_register_web.erl:386 -#: web/mod_register_web.erl:509 -msgid "Username:" -msgstr "Όνομα χρήστη" - -#: web/mod_register_web.erl:217 -msgid "This is case insensitive: macbeth is the same that MacBeth and Macbeth." -msgstr "" -"Ανεξαρτήτως με πεζά ή κεφαλαία: 'μιαλεξη' είναι το ίδιο με 'ΜιαΛέξη' και " -"'Μιαλέξη'." - -#: web/mod_register_web.erl:218 -msgid "Characters not allowed:" -msgstr "Χαρακτήρες δεν επιτρέπονται:" - -#: web/mod_register_web.erl:222 web/mod_register_web.erl:391 -#: web/mod_register_web.erl:514 -msgid "Server:" -msgstr "Διακομιστής:" - -#: web/mod_register_web.erl:232 -msgid "" -"Don't tell your password to anybody, not even the administrators of the " -"Jabber server." -msgstr "" -"Μην πείτε τον κωδικό πρόσβασής σας σε κανέναν, ακόμη και στους διαχειριστές " -"του διακομιστή Jabber." - -#: web/mod_register_web.erl:234 -msgid "You can later change your password using a Jabber client." -msgstr "" -"Μπορείτε αργότερα να αλλάξετε τον κωδικό πρόσβασής σας χρησιμοποιώντας έναν " -"πελάτη Jabber." - -#: web/mod_register_web.erl:235 -msgid "" -"Some Jabber clients can store your password in your computer. Use that " -"feature only if you trust your computer is safe." -msgstr "" -"Μερικοί πελάτες Jabber μπορεί να αποθηκεύσουν τον κωδικό πρόσβασής σας στον " -"υπολογιστή σας. Χρησιμοποιήστε αυτό το χαρακτηριστικό μόνο εάν εμπιστεύεστε " -"την ασφάλεια του υπολογιστή σας." - -#: web/mod_register_web.erl:237 -msgid "" -"Memorize your password, or write it in a paper placed in a safe place. In " -"Jabber there isn't an automated way to recover your password if you forget " -"it." -msgstr "" -"Απομνημονεύστε τον κωδικό πρόσβασής σας, ή γράψετε τον σε ένα χαρτί που είχε " -"τοποθετηθεί σε ασφαλές μέρος. Στο Jabber δεν υπάρχει αυτοματοποιημένος " -"τρόπος για να ανακτήσετε τον κωδικό σας αν τον ξεχάσετε." - -#: web/mod_register_web.erl:242 web/mod_register_web.erl:406 -msgid "Password Verification:" -msgstr "Επαλήθευση κωδικού πρόσβασης:" - -#: web/mod_register_web.erl:250 -msgid "Register" -msgstr "Καταχωρήστε" - -#: web/mod_register_web.erl:396 -msgid "Old Password:" -msgstr "Παλαιός κωδικός πρόσβασης:" - -#: web/mod_register_web.erl:401 -msgid "New Password:" -msgstr "Νέος κωδικός πρόσβασης:" - -#: web/mod_register_web.erl:504 -msgid "This page allows to unregister a Jabber account in this Jabber server." -msgstr "" -"Η σελίδα αυτή δίνει τη δυνατότητα να καταργήσετε την καταχώρηση ενός " -"λογαριασμό Jabber σε αυτόν το διακομιστή Jabber." - -#: web/mod_register_web.erl:524 -msgid "Unregister" -msgstr "Καταργήση εγγραφής" - -#, fuzzy -#~ msgid "Captcha test failed" -#~ msgstr "Το CAPTCHA είναι έγκυρο." diff --git a/priv/msgs/eo.msg b/priv/msgs/eo.msg index 2b336f07a..553980422 100644 --- a/priv/msgs/eo.msg +++ b/priv/msgs/eo.msg @@ -1,20 +1,28 @@ -{"Access Configuration","Agordo de atingo"}. -{"Access Control List Configuration","Agordo de atingokontrolo"}. -{"Access control lists","Atingokontrol-listoj"}. -{"Access Control Lists","Atingokontrol-listoj"}. +%% Generated automatically +%% DO NOT EDIT: run `make translations` instead +%% To improve translations please read: +%% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ + +{" (Add * to the end of field to match substring)"," (Aldonu * al la fino de la kampo por kongruigi subĉenon)"}. +{" has set the subject to: "," ŝanĝis la temon al: "}. +{"# participants","Nombro de partoprenantoj"}. +{"A description of the node","Priskribo de la nodo"}. +{"A friendly name for the node","Kromnomo de la nodo"}. +{"A password is required to enter this room","Pasvorto estas bezonata por eniri ĉi tiun babilejon"}. +{"A Web Page","Retpaĝo"}. +{"Accept","Akcepti"}. {"Access denied by service policy","Atingo rifuzita de serv-politiko"}. -{"Access rules","Atingo-reguloj"}. -{"Access Rules","Atingo-reguloj"}. +{"Access model","Atingomodelo"}. +{"Account doesn't exist","Konto ne ekzistas"}. {"Action on user","Ago je uzanto"}. -{"Add Jabber ID","Aldonu Jabber ID"}. -{"Add New","Aldonu novan"}. {"Add User","Aldonu Uzanton"}. -{"Administration","Administro"}. {"Administration of ","Mastrumado de "}. +{"Administration","Administro"}. {"Administrator privileges required","Administrantaj rajtoj bezonata"}. -{"A friendly name for the node","Kromnomo por ĉi tiu nodo"}. {"All activity","Ĉiu aktiveco"}. -{"Allow this Jabber ID to subscribe to this pubsub node?","Ĉu permesi ĉi tiun Jabber ID aboni al la jena PubAbo-nodo"}. +{"All Users","Ĉiuj Uzantoj"}. +{"Allow subscription","Permesi abonon"}. +{"Allow this Jabber ID to subscribe to this pubsub node?","Ĉu permesi ĉi tiun Jabber ID aboni al la jena PubAbo-nodo?"}. {"Allow users to change the subject","Permesu uzantojn ŝanĝi la temon"}. {"Allow users to query other users","Permesu uzantojn informpeti aliajn uzantojn"}. {"Allow users to send invites","Permesu uzantojn sendi invitojn"}. @@ -23,21 +31,34 @@ {"Allow visitors to send private messages to","Permesu uzantojn sendi privatajn mesaĝojn al"}. {"Allow visitors to send status text in presence updates","Permesu al vizitantoj sendi statmesaĝon en ĉeest-sciigoj"}. {"Allow visitors to send voice requests","Permesu uzantojn sendi voĉ-petojn"}. -{"All Users","Ĉiuj Uzantoj"}. {"Announcements","Anoncoj"}. -{"anyone","iu ajn"}. -{"A password is required to enter this room","Pasvorto estas bezonata por eniri ĉi tiun babilejon"}. +{"Answer associated with a picture","Respondo asociita kun bildo"}. +{"Answer associated with a video","Respondo asociita kun filmeto"}. +{"Answer associated with speech","Respondo asociita kun parolo"}. +{"Answer to a question","Respondo al demando"}. +{"Anyone may publish","Ĉiu rajtas publici"}. +{"Anyone with Voice","Iu ajn kun Voĉo"}. +{"Anyone","Iu ajn"}. {"April","Aprilo"}. +{"Attribute 'channel' is required for this request","Atributo 'channel' necesas por ĉi tiu peto"}. +{"Attribute 'id' is mandatory for MIX messages","Atributo 'id' estas deviga en MIX-mesaĝo"}. +{"Attribute 'jid' is not allowed here","Atributo 'jid' ne estas permesita ĉi tie"}. +{"Attribute 'node' is not allowed here","Atributo 'node' ne estas permesita ĉi tie"}. {"August","Aŭgusto"}. -{"Backup","Faru Sekurkopion"}. {"Backup Management","Mastrumado de sekurkopioj"}. -{"Backup of ","Sekurkopio de "}. +{"Backup of ~p","Sekurkopio de ~p"}. {"Backup to File at ","Faru sekurkopion je "}. +{"Backup","Faru Sekurkopion"}. {"Bad format","Malĝusta formo"}. {"Birthday","Naskiĝtago"}. +{"Cannot remove active list","Ne povas forigi aktivan liston"}. {"CAPTCHA web page","CAPTCHA teksaĵ-paĝo"}. +{"Challenge ID","Identigilo de Defio"}. {"Change Password","Ŝanĝu pasvorton"}. {"Change User Password","Ŝanĝu pasvorton de uzanto"}. +{"Channel already exists","Kanalo jam ekzistas"}. +{"Channel does not exist","Kanalo ne ekzistas"}. +{"Channels","Kanaloj"}. {"Characters not allowed:","Karaktroj ne permesata:"}. {"Chatroom configuration modified","Agordo de babilejo ŝanĝita"}. {"Chatroom is created","Babilejo kreita"}. @@ -46,89 +67,70 @@ {"Chatroom is stopped","Babilejo haltita"}. {"Chatrooms","Babilejoj"}. {"Choose a username and password to register with this server","Elektu uzantnomon kaj pasvorton por registri je ĉi tiu servilo"}. -{"Choose modules to stop","Elektu modulojn por fini"}. {"Choose storage type of tables","Elektu konserv-tipon de tabeloj"}. -{"Choose whether to approve this entity's subscription.","Elektu ĉu permesi la abonon de ĉi tiu ento"}. +{"Choose whether to approve this entity's subscription.","Elektu ĉu permesi la abonon de ĉi tiu ento."}. {"City","Urbo"}. {"Commands","Ordonoj"}. {"Conference room does not exist","Babilejo ne ekzistas"}. -{"Configuration","Agordo"}. {"Configuration of room ~s","Agordo de babilejo ~s"}. -{"Connected Resources:","Konektataj risurcoj:"}. -{"Connections parameters","Konekto-parametroj"}. +{"Configuration","Agordo"}. {"Country","Lando"}. -{"CPU Time:","CPU-tempo"}. -{"Database","Datumbazo"}. -{"Database Tables at ","Datumbaz-tabeloj je "}. +{"Current Discussion Topic","Aktuala Diskuta Temo"}. {"Database Tables Configuration at ","Agordo de datumbaz-tabeloj je "}. +{"Database","Datumbazo"}. {"December","Decembro"}. {"Default users as participants","Kutime farigu uzantojn kiel partpoprenantoj"}. -{"Delete","Forigu"}. -{"Delete message of the day","Forigu mesaĝo de la tago"}. {"Delete message of the day on all hosts","Forigu mesaĝo de la tago je ĉiu gastigo"}. -{"Delete Selected","Forigu elektata(j)n"}. +{"Delete message of the day","Forigu mesaĝo de la tago"}. {"Delete User","Forigu Uzanton"}. {"Deliver event notifications","Liveru event-sciigojn"}. {"Deliver payloads with event notifications","Liveru aĵojn de event-sciigoj"}. -{"Description:","Priskribo:"}. {"Disc only copy","Nur disk-kopio"}. -{"Displayed Groups:","Montrataj grupoj:"}. -{"Don't tell your password to anybody, not even the administrators of the Jabber server.","Ne donu vian pasvorton al iun ajn, eĉ ne al la administrantoj de la Ĵabber-servilo."}. {"Dump Backup to Text File at ","Skribu sekurkopion en plata teksto al "}. {"Dump to Text File","Skribu en plata tekst-dosiero"}. +{"Duplicated groups are not allowed by RFC6121","RFC 6121 ne permesas duplikatajn grupojn"}. {"Edit Properties","Redaktu atributojn"}. {"Either approve or decline the voice request.","Ĉu aprobu, aŭ malaprobu la voĉ-peton."}. -{"ejabberd IRC module","ejabberd IRC-modulo"}. {"ejabberd MUC module","ejabberd MUC-modulo"}. +{"ejabberd Multicast service","ejabberd Multicast-servo"}. {"ejabberd Publish-Subscribe module","ejabberd Public-Abonada modulo"}. {"ejabberd SOCKS5 Bytestreams module","ejabberd SOCKS5 Bajtfluo modulo"}. {"ejabberd vCard module","ejabberd vCard-modulo"}. {"ejabberd Web Admin","ejabberd Teksaĵa Administro"}. -{"Elements","Eroj"}. +{"ejabberd","ejabberd"}. +{"Email Address","Retpoŝta Adreso"}. {"Email","Retpoŝto"}. {"Enable logging","Ŝaltu protokoladon"}. -{"Encoding for server ~b","Enkodigo por servilo ~b"}. +{"Enable message archiving","Ŝaltu mesaĝo-arkivo"}. {"End User Session","Haltigu Uzant-seancon"}. -{"Enter list of {Module, [Options]}","Enmetu liston de {Modulo, [Elektebloj]}"}. {"Enter nickname you want to register","Enmetu kaŝnomon kiun vi volas registri"}. {"Enter path to backup file","Enmetu vojon por sekurkopio"}. {"Enter path to jabberd14 spool dir","Enmetu vojon al jabberd14-uzantdosierujo"}. {"Enter path to jabberd14 spool file","Enmetu vojon al jabberd14-uzantdosiero"}. {"Enter path to text file","Enmetu vojon al plata teksto"}. {"Enter the text you see","Enmetu montrita teksto"}. -{"Enter username, encodings, ports and passwords you wish to use for connecting to IRC servers","Enmetu uzantnomon,j enkodigojn, pordojn kaj pasvortojn kiujn vi volas uzi por konektoj al IRC-serviloj"}. -{"Erlang Jabber Server","Erlang-a Jabber-Servilo"}. -{"Error","Eraro"}. -{"Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef.net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}].","Ekzemplo: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"sekreto\"}, {\"vendetta.fef.net\", \"iso8859-1\", 7000}, {\"irc.iutestservilo.net\", \"utf-8\"}]."}. {"Exclude Jabber IDs from CAPTCHA challenge","Esceptu Ĵabber-identigilojn je CAPTCHA-defio"}. +{"Export all tables as SQL queries to a file:","Eksportu ĉiuj tabeloj kiel SQL-informmendo al dosierujo:"}. {"Export data of all users in the server to PIEFXIS files (XEP-0227):","Eksportu datumojn de ĉiuj uzantoj en servilo al PIEFXIS dosieroj (XEP-0227):"}. {"Export data of users in a host to PIEFXIS files (XEP-0227):","Eksportu datumoj de uzantoj en gastigo al PIEFXIS dosieroj (XEP-0227):"}. {"Failed to extract JID from your voice request approval","Malsukcesis ekstrakti JID-on de via voĉ-pet-aprobo"}. {"Family Name","Lasta Nomo"}. {"February","Februaro"}. -{"Fill in fields to search for any matching Jabber User","Kompletigu la formon por serĉi rekonata Jabber-uzanto"}. -{"Fill in the form to search for any matching Jabber User (Add * to the end of field to match substring)","Kompletigu la formon por serĉi rekonata Jabber-uzanto (Aldonu * je la fino de la kampo por rekoni subĉenon"}. +{"File larger than ~w bytes","Dosiero pli granda ol ~w bajtoj"}. {"Friday","Vendredo"}. -{"From","De"}. -{"From ~s","De ~s"}. {"Full Name","Plena Nomo"}. {"Get Number of Online Users","Montru nombron de konektataj uzantoj"}. {"Get Number of Registered Users","Montru nombron de registritaj uzantoj"}. {"Get User Last Login Time","Montru tempon de lasta ensaluto"}. -{"Get User Password","Montru pasvorton de uzanto"}. {"Get User Statistics","Montru statistikojn de uzanto"}. +{"Given Name","Persona Nomo"}. {"Grant voice to this person?","Koncedu voĉon al ĉi-persono?"}. -{"Group ","Grupo "}. -{"Groups","Grupoj"}. {"has been banned","estas forbarita"}. -{"has been kicked because of an affiliation change","estas forpelita pro aparteneca ŝanĝo"}. {"has been kicked because of a system shutdown","estas forpelita pro sistem-haltigo"}. +{"has been kicked because of an affiliation change","estas forpelita pro aparteneca ŝanĝo"}. {"has been kicked because the room has been changed to members-only","estas forpelita ĉar la babilejo fariĝis sole por membroj"}. {"has been kicked","estas forpelita"}. -{" has set the subject to: "," ŝanĝis la temon al: "}. -{"Host","Gastigo"}. {"If you don't see the CAPTCHA image here, visit the web page.","Se vi ne vidas la CAPTCHA-imagon jene, vizitu la teksaĵ-paĝon."}. -{"If you want to specify different ports, passwords, encodings for IRC servers, fill this list with values in format '{\"irc server\", \"encoding\", port, \"password\"}'. By default this service use \"~s\" encoding, port ~p, empty password.","Se vi volas specifi diversajn pordojn, pasvortojn, enkodigojn por IRC-serviloj, kompletigu la jenan liston kun la formo '{\"irc-servilo\", \"enkodigo\", porto, \"pasvorto\"}'. Se ne specifita, ĉi tiu servilo uzas la enkodigo \"~s\", porto ~p, malplena pasvorto."}. {"Import Directory","Importu dosierujo"}. {"Import File","Importu dosieron"}. {"Import user data from jabberd14 spool file:","Importu uzantojn de jabberd14-uzantdosieroj"}. @@ -138,83 +140,68 @@ {"Import Users from Dir at ","Importu uzantojn de dosierujo ĉe "}. {"Import Users From jabberd14 Spool Files","Importu uzantojn de jabberd14-uzantdosieroj"}. {"Improper message type","Malĝusta mesaĝo-tipo"}. +{"Incorrect CAPTCHA submit","Neĝusta CAPTCHA-submeto"}. {"Incorrect password","Nekorekta pasvorto"}. -{"Invalid affiliation: ~s","Nevalida aparteneco: ~s"}. -{"Invalid role: ~s","Nevalida rolo: ~s"}. {"IP addresses","IP-adresoj"}. -{"IP","IP"}. -{"IRC channel (don't put the first #)","IRC-babilejo (ne aldonu #-prefikson)"}. -{"IRC server","IRC-servilo"}. -{"IRC settings","IRC agordoj"}. -{"IRC Transport","IRC-transportilo"}. -{"IRC Username","IRC-kaŝnomo"}. -{"IRC username","IRC-uzantnomo"}. {"is now known as","nun nomiĝas"}. -{"It is not allowed to send private messages","Ne estas permesata sendi privatajn mesaĝojn"}. {"It is not allowed to send private messages of type \"groupchat\"","Malpermesas sendi mesaĝojn de tipo \"groupchat\""}. {"It is not allowed to send private messages to the conference","Nur partoprenantoj rajtas sendi privatajn mesaĝojn al la babilejo"}. -{"Jabber Account Registration","Ĵabber-konto registrado"}. {"Jabber ID","Jabber ID"}. -{"Jabber ID ~s is invalid","Jabber ID ~s estas nevalida"}. {"January","Januaro"}. -{"Join IRC channel","Eniras IRC-babilejon"}. {"joins the room","eniras la babilejo"}. -{"Join the IRC channel here.","Eniru IRC-babilejon jen"}. -{"Join the IRC channel in this Jabber ID: ~s","Eniru IRC-babilejon en ĉi Jabber-ID: ~s"}. {"July","Julio"}. {"June","Junio"}. +{"Just created","Ĵus kreita"}. {"Last Activity","Lasta aktiveco"}. {"Last login","Lasta ensaluto"}. {"Last month","Lasta monato"}. {"Last year","Lasta jaro"}. {"leaves the room","eliras la babilejo"}. -{"Listened Ports at ","Atentataj pordoj je "}. -{"Listened Ports","Atentataj pordoj"}. -{"List of modules to start","Listo de moduloj por starti"}. -{"Low level update script","Bazanivela ĝisdatigo-skripto"}. {"Make participants list public","Farigu partoprento-liston publika"}. -{"Make room CAPTCHA protected","Farigu babilejon protektata per CAPTCHA"}. +{"Make room CAPTCHA protected","Protektu babilejon per CAPTCHA"}. {"Make room members-only","Farigu babilejon sole por membroj"}. {"Make room moderated","Farigu babilejon moderigata"}. {"Make room password protected","Farigu babilejon protektata per pasvorto"}. {"Make room persistent","Farigu babilejon daŭra"}. {"Make room public searchable","Farigu babilejon publike trovebla"}. {"March","Marĉo"}. -{"Maximum Number of Occupants","Limigo de nombro de partoprenantoj"}. -{"Max # of items to persist","Maksimuma kiomo de eroj en konservado"}. {"Max payload size in bytes","Maksimuma aĵo-grando je bajtoj"}. +{"Maximum file size","Maksimuma grando de dosiero"}. +{"Maximum Number of Occupants","Limigo de nombro de partoprenantoj"}. {"May","Majo"}. {"Membership is required to enter this room","Membreco estas bezonata por eniri ĉi tiun babilejon"}. -{"Members:","Membroj:"}. -{"Memorize your password, or write it in a paper placed in a safe place. In Jabber there isn't an automated way to recover your password if you forget it.","Memoru vian pasvorton, aŭ skribu ĝin sur papero formetata je sekura loko. Je Ĵabber ne ekzistas aŭtomata metodo por reakiri vian pasvorton se vi forgesas ĝin."}. -{"Memory","Memoro"}. {"Message body","Teksto de mesaĝo"}. {"Middle Name","Meza Nomo"}. {"Minimum interval between voice requests (in seconds)","Minimuma intervalo inter voĉ-petoj (je sekundoj)"}. {"Moderator privileges required","Moderantaj rajtoj bezonata"}. -{"moderators only","moderantoj sole"}. -{"Modified modules","Ĝisdatigitaj moduloj"}. -{"Module","Modulo"}. -{"Modules at ","Moduloj je "}. -{"Modules","Moduloj"}. +{"Module failed to handle the query","Modulo malsukcesis trakti la informpeton"}. {"Monday","Lundo"}. -{"Name:","Nomo:"}. +{"Multicast","Multicast"}. +{"Multiple elements are not allowed by RFC6121","RFC 6121 ne permesas plurajn -elementojn"}. +{"Multi-User Chat","Grupbabilado"}. {"Name","Nomo"}. +{"Natural Language for Room Discussions","Homa Lingvo por Diskutoj en Babilejo"}. {"Never","Neniam"}. {"New Password:","Nova Pasvorto:"}. -{"Nickname","Kaŝnomo"}. {"Nickname Registration at ","Kaŝnomo-registrado je "}. {"Nickname ~s does not exist in the room","Kaŝnomo ~s ne ekzistas en la babilejo"}. -{"nobody","neniu"}. +{"Nickname","Kaŝnomo"}. +{"No address elements found","Adresa elemento ne trovita"}. +{"No addresses element found","Adresa elemento ne trovita"}. {"No body provided for announce message","Neniu teksto donita por anonc-mesaĝo"}. +{"No child elements found","Ida elemento ne trovita"}. {"No Data","Neniu datumo"}. -{"Node ID","Nodo ID"}. -{"Node ","Nodo "}. -{"Node not found","Nodo ne trovita"}. -{"Nodes","Nodoj"}. +{"No element found","Elemento ne trovita"}. +{"No items found in this query","Neniu elemento trovita en ĉi tiu informpeto"}. {"No limit","Neniu limigo"}. +{"No module is handling this query","Neniu modulo traktas ĉi tiun informpeton"}. +{"No 'password' found in this query","Neniu pasvorto trovita en ĉi tiu informpeto"}. +{"No private data found in this query","Neniu privata dateno trovita en ĉi tiu informpeto"}. +{"Node ID","Nodo ID"}. +{"Node not found","Nodo ne trovita"}. +{"Node ~p","Nodo ~p"}. +{"Nodes","Nodoj"}. {"None","Nenio"}. -{"No resource provided","Neniu risurco donita"}. {"Not Found","Ne trovita"}. {"Notify subscribers when items are removed from the node","Sciigu abonantoj kiam eroj estas forigita de la nodo"}. {"Notify subscribers when the node configuration changes","Sciigu abonantoj kiam la agordo de la nodo ŝanĝas"}. @@ -224,68 +211,54 @@ {"Number of online users","Nombro de konektataj uzantoj"}. {"Number of registered users","Nombro de registritaj uzantoj"}. {"October","Oktobro"}. -{"Offline Messages:","Liverontaj mesaĝoj"}. -{"Offline Messages","Liverontaj mesaĝoj"}. {"OK","Bone"}. {"Old Password:","Malnova Pasvorto:"}. -{"Online","Konektata"}. -{"Online Users:","Konektataj uzantoj:"}. {"Online Users","Konektataj Uzantoj"}. +{"Online","Konektata"}. {"Only deliver notifications to available users","Nur liveru sciigojn al konektataj uzantoj"}. +{"Only element is allowed in this query","Nur la elemento estas permesita en ĉi tiu informpeto"}. {"Only moderators and participants are allowed to change the subject in this room","Nur moderigantoj kaj partoprenantoj rajtas ŝanĝi la temon en ĉi tiu babilejo"}. {"Only moderators are allowed to change the subject in this room","Nur moderigantoj rajtas ŝanĝi la temon en ĉi tiu babilejo"}. {"Only moderators can approve voice requests","Nur moderigantoj povas aprobi voĉ-petojn"}. {"Only occupants are allowed to send messages to the conference","Nur partoprenantoj rajtas sendi mesaĝojn al la babilejo"}. {"Only occupants are allowed to send queries to the conference","Nur partoprenantoj rajtas sendi informmendojn al la babilejoj"}. +{"Only publishers may publish","Nur publicantoj rajtas publici"}. {"Only service administrators are allowed to send service messages","Nur servo-administrantoj rajtas sendi serv-mesaĝojn"}. -{"Options","Elektebloj"}. +{"Only those on a whitelist may associate leaf nodes with the collection","Nur tiuj en permesolisto rajtas asocii foliajn nodojn kun la kolekto"}. +{"Only those on a whitelist may subscribe and retrieve items","Nur tiuj en permesolisto rajtas aboni kaj preni erojn"}. {"Organization Name","Organiz-nomo"}. {"Organization Unit","Organiz-parto"}. -{"Outgoing s2s Connections:","Elirantaj s-al-s-konektoj:"}. {"Outgoing s2s Connections","Elirantaj s-al-s-konektoj"}. -{"Outgoing s2s Servers:","Elirantaj s-al-s-serviloj"}. {"Owner privileges required","Mastraj rajtoj bezonata"}. -{"Packet","Pakaĵo"}. -{"Password ~b","Pasvorto ~b"}. -{"Password:","Pasvorto:"}. -{"Password","Pasvorto"}. -{"Password Verification:","Pasvortkontrolo:"}. {"Password Verification","Pasvortkontrolo"}. +{"Password Verification:","Pasvortkontrolo:"}. +{"Password","Pasvorto"}. +{"Password:","Pasvorto:"}. {"Path to Dir","Vojo al dosierujo"}. {"Path to File","Voje de dosiero"}. -{"Pending","Atendanta"}. {"Period: ","Periodo: "}. {"Persist items to storage","Savu erojn en konservado"}. {"Ping","Sondaĵo"}. {"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Rimarku ke ĉi tiuj elektebloj nur sekurkopias la propran Mnesia-datumbazon. Se vi uzas la ODBC-modulon, vi ankaŭ devas sekurkopii tiujn SQL-datumbazoj aparte."}. {"Please, wait for a while before sending new voice request","Bonvolu atendi iomete antaŭ ol sendi plian voĉ-peton"}. {"Pong","Resondaĵo"}. -{"Port ~b","Pordo ~b"}. -{"Port","Pordo"}. {"Present real Jabber IDs to","Montru verajn Jabber ID-ojn al"}. {"private, ","privata, "}. -{"Protocol","Protokolo"}. -{"Publish-Subscribe","Public-Abonado"}. +{"Publish model","Publici modelon"}. +{"Publish-Subscribe","Publici-Aboni"}. {"PubSub subscriber request","PubAbo abonpeto"}. -{"Purge all items when the relevant publisher goes offline","Forigu ĉiujn erojn kiam la rilata publikanto malkonektiĝas"}. +{"Purge all items when the relevant publisher goes offline","Forigu ĉiujn erojn kiam la rilata publicanto malkonektiĝas"}. {"Queries to the conference members are not allowed in this room","Malpermesas informmendoj al partoprenantoj en ĉi tiu babilejo"}. +{"Query to another users is forbidden","Informpeto al aliaj uzantoj estas malpermesita"}. {"RAM and disc copy","RAM- kaj disk-kopio"}. {"RAM copy","RAM-kopio"}. -{"Raw","Kruda"}. {"Really delete message of the day?","Ĉu vere forigi mesaĝon de la tago?"}. -{"Recipient is not in the conference room","Ricevanto ne ĉeestas en la babilejo "}. -{"Register a Jabber account","Registru Ĵabber-konton"}. -{"Registered Users:","Registritaj uzantoj:"}. -{"Registered Users","Registritaj uzantoj"}. +{"Recipient is not in the conference room","Ricevanto ne ĉeestas en la babilejo"}. {"Register","Registru"}. -{"Registration in mod_irc for ","Registraĵo en mod_irc de "}. {"Remote copy","Fora kopio"}. -{"Remove All Offline Messages","Forigu ĉiujn liverontajn mesaĝojn"}. -{"Remove","Forigu"}. {"Remove User","Forigu uzanton"}. {"Replaced by new connection","Anstataŭigita je nova konekto"}. {"Resources","Risurcoj"}. -{"Restart","Restartu"}. {"Restart Service","Restartu Servon"}. {"Restore Backup from File at ","Restaŭrigu de dosiero el "}. {"Restore binary backup after next ejabberd restart (requires less memory):","Restaŭrigu duuman sekurkopion post sekvonta ejabberd-restarto"}. @@ -298,14 +271,9 @@ {"Room Occupants","Nombro de ĉeestantoj"}. {"Room title","Babilejo-nomo"}. {"Roster groups allowed to subscribe","Kontaktlist-grupoj kiuj rajtas aboni"}. -{"Roster","Kontaktlisto"}. -{"Roster of ","Kontaktlisto de "}. {"Roster size","Kontaktlist-grando"}. -{"RPC Call Error","Eraro de RPC-alvoko"}. {"Running Nodes","Funkciantaj Nodoj"}. -{"~s access rule configuration","Agordo de atingo-reguloj de ~s"}. {"Saturday","Sabato"}. -{"Script check","Skript-kontrolo"}. {"Search Results for ","Serĉ-rezultoj de "}. {"Search users in ","Serĉu uzantojn en "}. {"Send announcement to all online users on all hosts","Sendu anoncon al ĉiu konektata uzanto de ĉiu gastigo"}. @@ -313,7 +281,6 @@ {"Send announcement to all users on all hosts","Sendu anoncon al ĉiu uzanto de ĉiu gastigo"}. {"Send announcement to all users","Sendu anoncon al ĉiu uzanto"}. {"September","Septembro"}. -{"Server ~b","Servilo ~b"}. {"Server:","Servilo:"}. {"Set message of the day and send to online users","Enmetu mesaĝon de la tago kaj sendu al konektataj uzantoj"}. {"Set message of the day on all hosts and send to online users","Enmetu mesaĝon de la tago je ĉiu gastigo kaj sendu al konektataj uzantoj"}. @@ -321,81 +288,50 @@ {"Show Integral Table","Montru integran tabelon"}. {"Show Ordinary Table","Montru ordinaran tabelon"}. {"Shut Down Service","Haltigu Servon"}. -{"~s invites you to the room ~s","~s invitas vin al la babilejo ~s"}. -{"Some Jabber clients can store your password in your computer. Use that feature only if you trust your computer is safe.","Kelkaj Ĵabber-klientoj povas memori vian pasvorton je via komputilo. Nur uzu tiun eblon se vi fidas ke via komputilo estas sekura."}. {"Specify the access model","Specifu atingo-modelon"}. {"Specify the event message type","Specifu tipo de event-mesaĝo"}. {"Specify the publisher model","Enmetu publikadan modelon"}. -{"~s's Offline Messages Queue","Mesaĝo-atendovico de ~s"}. -{"Start Modules at ","Startu modulojn je "}. -{"Start Modules","Startu Modulojn"}. -{"Start","Startu"}. -{"Statistics of ~p","Statistikoj de ~p"}. -{"Statistics","Statistikoj"}. -{"Stop","Haltigu"}. -{"Stop Modules at ","Haltigu modulojn je "}. -{"Stop Modules","Haltigu Modulojn"}. {"Stopped Nodes","Neaktivaj Nodoj"}. -{"Storage Type","Konserv-tipo"}. {"Store binary backup:","Konservu duuman sekurkopion:"}. {"Store plain text backup:","Skribu sekurkopion en plata tekstdosiero"}. {"Subject","Temo"}. -{"Submit","Sendu"}. {"Submitted","Sendita"}. {"Subscriber Address","Abonanta adreso"}. -{"Subscription","Abono"}. +{"Subscribers may publish","Abonantoj rajtas publici"}. {"Sunday","Dimanĉo"}. {"That nickname is already in use by another occupant","Tiu kaŝnomo jam estas uzata de alia partoprenanto"}. {"That nickname is registered by another person","Kaŝnomo estas registrita de alia persono"}. -{"The CAPTCHA is valid.","La CAPTCHA ĝustas"}. +{"The CAPTCHA is valid.","La CAPTCHA ĝustas."}. {"The CAPTCHA verification has failed","La CAPTCHA-kontrolado malsukcesis"}. +{"The captcha you entered is wrong","La CAPTCHA enigita de vi malĝustas"}. {"The collections with which a node is affiliated","Aro kun kiu nodo estas filigita"}. -{"the password is","la pasvorto estas"}. {"The password is too weak","La pasvorto estas ne sufiĉe forta"}. -{"The password of your Jabber account was successfully changed.","La pasvorto de via Ĵabber-konto estas sukcese ŝanĝata."}. -{"There was an error changing the password: ","Estis eraro dum ŝanĝi de la pasvortro:"}. +{"the password is","la pasvorto estas"}. +{"The query is only allowed from local users","La informpeto estas permesita nur de lokaj uzantoj"}. +{"The query must not contain elements","La informpeto devas ne enhavi elementojn "}. {"There was an error creating the account: ","Estis eraro dum kreado de la konto:"}. {"There was an error deleting the account: ","Estis eraro dum forigado de la konto:"}. -{"This is case insensitive: macbeth is the same that MacBeth and Macbeth.","Uskleco ne signifas: macbeth estas la sama ol MacBeth kaj Macbeth."}. -{"This page allows to create a Jabber account in this Jabber server. Your JID (Jabber IDentifier) will be of the form: username@server. Please read carefully the instructions to fill correctly the fields.","Jena paĝo ebligas kreadon de Ĵabber-konto je ĉi-Ĵabber-servilo. Via JID (Ĵabber-IDentigilo) estos ĉi-tiel: uzantnomo@servilo. Bonvolu legu bone la instrukciojn por korekta enmetigo de la kampoj. "}. -{"This page allows to unregister a Jabber account in this Jabber server.","Jena pagxo ebligas malregistri Jxabber-konton je ĉi-servilo."}. -{"This participant is kicked from the room because he sent an error message","Ĉi tiu partoprenanta estas forpelata de la babilejo pro sendado de erar-mesaĝo"}. -{"This participant is kicked from the room because he sent an error message to another participant","Ĉi tiu partoprenanto estas forpelata de la babilejo pro sendo de erar-mesaĝo al alia partoprenanto"}. -{"This participant is kicked from the room because he sent an error presence","Ĉi tiu partoprenanto estas forpelata de la babilejo pro sendo de erar-ĉeesto"}. {"This room is not anonymous","Ĉi tiu babilejo ne estas anonima"}. {"Thursday","Ĵaŭdo"}. {"Time delay","Prokrasto"}. -{"Time","Tempo"}. -{"To","Ĝis"}. {"Too many CAPTCHA requests","Tro multaj CAPTCHA-petoj"}. -{"To ~s","Al ~s"}. +{"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","Tro da malsukcesaj aŭtentprovoj (~p) de ĉi tiu IP-adreso (~s). La adreso estos malbarata je ~s UTC."}. +{"Too many unacked stanzas","Tro da neagnoskitaj stancoj"}. {"Traffic rate limit is exceeded","Trafikrapida limigo superita"}. -{"Transactions Aborted:","Transakcioj nuligitaj"}. -{"Transactions Committed:","Transakcioj enmetitaj"}. -{"Transactions Logged:","Transakcioj protokolitaj"}. -{"Transactions Restarted:","Transakcioj restartitaj"}. {"Tuesday","Mardo"}. {"Unable to generate a CAPTCHA","Ne eblis krei CAPTCHA"}. {"Unauthorized","Nepermesita"}. -{"Unregister a Jabber account","Malregistru Ĵabber-konton"}. {"Unregister","Malregistru"}. -{"Update ","Ĝisdatigu "}. -{"Update","Ĝisdatigu"}. {"Update message of the day (don't send)","Ŝanĝu mesaĝon de la tago (ne sendu)"}. {"Update message of the day on all hosts (don't send)","Ŝanĝu mesaĝon de la tago je ĉiu gastigo (ne sendu)"}. -{"Update plan","Ĝisdatigo-plano"}. -{"Update script","Ĝisdatigo-skripto"}. -{"Uptime:","Daŭro de funkciado"}. -{"Use of STARTTLS required","Uzo de STARTTLS bezonata"}. +{"URL for Archived Discussion Logs","Retpaĝa adreso de Enarkivigitaj Diskutprotokoloj"}. {"User JID","Uzant-JID"}. {"User Management","Uzanto-administrado"}. {"Username:","Uzantnomo"}. {"Users are not allowed to register accounts so quickly","Ne estas permesata al uzantoj registri tiel rapide"}. {"Users Last Activity","Lasta aktiveco de uzanto"}. {"Users","Uzantoj"}. -{"User ","Uzanto "}. {"User","Uzanto"}. -{"Validate","Validigu"}. {"vCard User Search","Serĉado de vizitkartoj"}. {"Virtual Hosts","Virtual-gastigoj"}. {"Visitors are not allowed to change their nicknames in this room","Ne estas permesata al vizitantoj ŝanĝi siajn kaŝnomojn en ĉi tiu ĉambro"}. @@ -405,16 +341,13 @@ {"Wednesday","Merkredo"}. {"When to send the last published item","Kiam sendi la laste publicitan eron"}. {"Whether to allow subscriptions","Ĉu permesi aboni"}. -{"You can later change your password using a Jabber client.","Poste vi povas ŝanĝi vian pasvorton per Ĵabber-kliento."}. +{"Wrong xmlns","Malĝusta XML-nomspaco (xmlns)"}. +{"XMPP Account Registration","Registrado de XMPP-Konto"}. {"You have been banned from this room","Vi estas malpermesata en ĉi tiu babilejo"}. {"You must fill in field \"Nickname\" in the form","Vi devas kompletigi la \"Kaŝnomo\" kampon"}. {"You need a client that supports x:data and CAPTCHA to register","Vi bezonas klienton subtenante x:data-funkcio kaj CAPTCHA por registri kaŝnomon"}. {"You need a client that supports x:data to register the nickname","Vi bezonas klienton subtenante x:data-funkcio por registri kaŝnomon"}. -{"You need an x:data capable client to configure mod_irc settings","Vi bezonas klienton kun x:data-funkcio por agordi mod_irc"}. -{"You need an x:data capable client to configure room","Vi bezonas klienton kun x:data-funkcio por agordi la babilejon"}. {"You need an x:data capable client to search","Vi bezonas klienton kun x:data-funkcio por serĉado"}. {"Your active privacy list has denied the routing of this stanza.","Via aktiva privatec-listo malpermesas enkursigi ĉi-tiun pakaĵon"}. {"Your contact offline message queue is full. The message has been discarded.","Mesaĝo-atendovico de la senkonekta kontakto estas plena. La mesaĝo estas forĵetita"}. -{"Your Jabber account was successfully created.","Via Ĵabber-konto estis sukcese kreata."}. -{"Your Jabber account was successfully deleted.","Via Ĵabber-konto estas sukcese forigita."}. -{"Your messages to ~s are being blocked. To unblock them, visit ~s","Viaj mesaĝoj al ~s estas blokata. Por malbloki ilin, iru al ~s"}. +{"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","Viaj mesaĝoj al ~s estas blokata. Por malbloki ilin, iru al ~s"}. diff --git a/priv/msgs/eo.po b/priv/msgs/eo.po deleted file mode 100644 index ed53b2a56..000000000 --- a/priv/msgs/eo.po +++ /dev/null @@ -1,1866 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: 2.1.0-alpha\n" -"POT-Creation-Date: \n" -"PO-Revision-Date: \n" -"Last-Translator: Andreas van Cranenburgh \n" -"Language-Team: \n" -"Language: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Language: Esperanto\n" - -#: ejabberd_c2s.erl:424 ejabberd_c2s.erl:727 -msgid "Use of STARTTLS required" -msgstr "Uzo de STARTTLS bezonata" - -#: ejabberd_c2s.erl:503 -msgid "No resource provided" -msgstr "Neniu risurco donita" - -#: ejabberd_c2s.erl:1197 -msgid "Replaced by new connection" -msgstr "Anstataŭigita je nova konekto" - -#: ejabberd_c2s.erl:1885 -msgid "Your active privacy list has denied the routing of this stanza." -msgstr "Via aktiva privatec-listo malpermesas enkursigi ĉi-tiun pakaĵon" - -#: ejabberd_captcha.erl:96 ejabberd_captcha.erl:152 ejabberd_captcha.erl:178 -msgid "Enter the text you see" -msgstr "Enmetu montrita teksto" - -#: ejabberd_captcha.erl:101 -msgid "Your messages to ~s are being blocked. To unblock them, visit ~s" -msgstr "Viaj mesaĝoj al ~s estas blokata. Por malbloki ilin, iru al ~s" - -#: ejabberd_captcha.erl:134 -msgid "If you don't see the CAPTCHA image here, visit the web page." -msgstr "Se vi ne vidas la CAPTCHA-imagon jene, vizitu la teksaĵ-paĝon." - -#: ejabberd_captcha.erl:146 -msgid "CAPTCHA web page" -msgstr "CAPTCHA teksaĵ-paĝo" - -#: ejabberd_captcha.erl:307 -msgid "The CAPTCHA is valid." -msgstr "La CAPTCHA ĝustas" - -#: mod_adhoc.erl:95 mod_adhoc.erl:125 mod_adhoc.erl:143 mod_adhoc.erl:161 -msgid "Commands" -msgstr "Ordonoj" - -#: mod_adhoc.erl:149 mod_adhoc.erl:243 -msgid "Ping" -msgstr "Sondaĵo" - -#: mod_adhoc.erl:260 -msgid "Pong" -msgstr "Resondaĵo" - -#: mod_announce.erl:507 mod_announce_odbc.erl:499 -msgid "Really delete message of the day?" -msgstr "Ĉu vere forigi mesaĝon de la tago?" - -#: mod_announce.erl:515 mod_announce_odbc.erl:507 mod_configure.erl:1083 -#: mod_configure.erl:1128 -msgid "Subject" -msgstr "Temo" - -#: mod_announce.erl:520 mod_announce_odbc.erl:512 mod_configure.erl:1088 -#: mod_configure.erl:1133 -msgid "Message body" -msgstr "Teksto de mesaĝo" - -#: mod_announce.erl:600 mod_announce_odbc.erl:592 -msgid "No body provided for announce message" -msgstr "Neniu teksto donita por anonc-mesaĝo" - -#: mod_announce.erl:635 mod_announce_odbc.erl:627 -msgid "Announcements" -msgstr "Anoncoj" - -#: mod_announce.erl:637 mod_announce_odbc.erl:629 -msgid "Send announcement to all users" -msgstr "Sendu anoncon al ĉiu uzanto" - -#: mod_announce.erl:639 mod_announce_odbc.erl:631 -msgid "Send announcement to all users on all hosts" -msgstr "Sendu anoncon al ĉiu uzanto de ĉiu gastigo" - -#: mod_announce.erl:641 mod_announce_odbc.erl:633 -msgid "Send announcement to all online users" -msgstr "Sendu anoncon al ĉiu konektata uzanto" - -#: mod_announce.erl:643 mod_announce_odbc.erl:635 mod_configure.erl:1078 -#: mod_configure.erl:1123 -msgid "Send announcement to all online users on all hosts" -msgstr "Sendu anoncon al ĉiu konektata uzanto de ĉiu gastigo" - -#: mod_announce.erl:645 mod_announce_odbc.erl:637 -msgid "Set message of the day and send to online users" -msgstr "Enmetu mesaĝon de la tago kaj sendu al konektataj uzantoj" - -#: mod_announce.erl:647 mod_announce_odbc.erl:639 -msgid "Set message of the day on all hosts and send to online users" -msgstr "" -"Enmetu mesaĝon de la tago je ĉiu gastigo kaj sendu al konektataj uzantoj" - -#: mod_announce.erl:649 mod_announce_odbc.erl:641 -msgid "Update message of the day (don't send)" -msgstr "Ŝanĝu mesaĝon de la tago (ne sendu)" - -#: mod_announce.erl:651 mod_announce_odbc.erl:643 -msgid "Update message of the day on all hosts (don't send)" -msgstr "Ŝanĝu mesaĝon de la tago je ĉiu gastigo (ne sendu)" - -#: mod_announce.erl:653 mod_announce_odbc.erl:645 -msgid "Delete message of the day" -msgstr "Forigu mesaĝo de la tago" - -#: mod_announce.erl:655 mod_announce_odbc.erl:647 -msgid "Delete message of the day on all hosts" -msgstr "Forigu mesaĝo de la tago je ĉiu gastigo" - -#: mod_configure.erl:114 mod_configure.erl:274 mod_configure.erl:296 -#: mod_configure.erl:498 -msgid "Configuration" -msgstr "Agordo" - -#: mod_configure.erl:125 mod_configure.erl:576 web/ejabberd_web_admin.erl:1936 -msgid "Database" -msgstr "Datumbazo" - -#: mod_configure.erl:127 mod_configure.erl:595 -msgid "Start Modules" -msgstr "Startu Modulojn" - -#: mod_configure.erl:129 mod_configure.erl:596 -msgid "Stop Modules" -msgstr "Haltigu Modulojn" - -#: mod_configure.erl:131 mod_configure.erl:604 web/ejabberd_web_admin.erl:1937 -msgid "Backup" -msgstr "Faru Sekurkopion" - -#: mod_configure.erl:133 mod_configure.erl:605 -msgid "Restore" -msgstr "Restaŭru" - -#: mod_configure.erl:135 mod_configure.erl:606 -msgid "Dump to Text File" -msgstr "Skribu en plata tekst-dosiero" - -#: mod_configure.erl:137 mod_configure.erl:615 -msgid "Import File" -msgstr "Importu dosieron" - -#: mod_configure.erl:139 mod_configure.erl:616 -msgid "Import Directory" -msgstr "Importu dosierujo" - -#: mod_configure.erl:141 mod_configure.erl:581 mod_configure.erl:1057 -msgid "Restart Service" -msgstr "Restartu Servon" - -#: mod_configure.erl:143 mod_configure.erl:582 mod_configure.erl:1102 -msgid "Shut Down Service" -msgstr "Haltigu Servon" - -#: mod_configure.erl:145 mod_configure.erl:518 mod_configure.erl:1197 -#: web/ejabberd_web_admin.erl:1527 -msgid "Add User" -msgstr "Aldonu Uzanton" - -#: mod_configure.erl:147 mod_configure.erl:519 mod_configure.erl:1219 -msgid "Delete User" -msgstr "Forigu Uzanton" - -#: mod_configure.erl:149 mod_configure.erl:520 mod_configure.erl:1231 -msgid "End User Session" -msgstr "Haltigu Uzant-seancon" - -#: mod_configure.erl:151 mod_configure.erl:521 mod_configure.erl:1243 -#: mod_configure.erl:1255 -msgid "Get User Password" -msgstr "Montru pasvorton de uzanto" - -#: mod_configure.erl:153 mod_configure.erl:522 -msgid "Change User Password" -msgstr "Ŝanĝu pasvorton de uzanto" - -#: mod_configure.erl:155 mod_configure.erl:523 mod_configure.erl:1272 -msgid "Get User Last Login Time" -msgstr "Montru tempon de lasta ensaluto" - -#: mod_configure.erl:157 mod_configure.erl:524 mod_configure.erl:1284 -msgid "Get User Statistics" -msgstr "Montru statistikojn de uzanto" - -#: mod_configure.erl:159 mod_configure.erl:525 -msgid "Get Number of Registered Users" -msgstr "Montru nombron de registritaj uzantoj" - -#: mod_configure.erl:161 mod_configure.erl:526 -msgid "Get Number of Online Users" -msgstr "Montru nombron de konektataj uzantoj" - -#: mod_configure.erl:163 mod_configure.erl:509 web/ejabberd_web_admin.erl:831 -#: web/ejabberd_web_admin.erl:872 -msgid "Access Control Lists" -msgstr "Atingokontrol-listoj" - -#: mod_configure.erl:165 mod_configure.erl:510 web/ejabberd_web_admin.erl:940 -#: web/ejabberd_web_admin.erl:976 -msgid "Access Rules" -msgstr "Atingo-reguloj" - -#: mod_configure.erl:297 mod_configure.erl:499 -msgid "User Management" -msgstr "Uzanto-administrado" - -#: mod_configure.erl:500 web/ejabberd_web_admin.erl:1058 -#: web/ejabberd_web_admin.erl:1462 -msgid "Online Users" -msgstr "Konektataj Uzantoj" - -#: mod_configure.erl:501 -msgid "All Users" -msgstr "Ĉiuj Uzantoj" - -#: mod_configure.erl:502 -msgid "Outgoing s2s Connections" -msgstr "Elirantaj s-al-s-konektoj" - -#: mod_configure.erl:503 web/ejabberd_web_admin.erl:1907 -msgid "Running Nodes" -msgstr "Funkciantaj Nodoj" - -#: mod_configure.erl:504 web/ejabberd_web_admin.erl:1909 -msgid "Stopped Nodes" -msgstr "Neaktivaj Nodoj" - -#: mod_configure.erl:577 mod_configure.erl:587 web/ejabberd_web_admin.erl:1953 -msgid "Modules" -msgstr "Moduloj" - -#: mod_configure.erl:578 -msgid "Backup Management" -msgstr "Mastrumado de sekurkopioj" - -#: mod_configure.erl:579 -msgid "Import Users From jabberd14 Spool Files" -msgstr "Importu uzantojn de jabberd14-uzantdosieroj" - -#: mod_configure.erl:699 -msgid "To ~s" -msgstr "Al ~s" - -#: mod_configure.erl:717 -msgid "From ~s" -msgstr "De ~s" - -#: mod_configure.erl:913 -msgid "Database Tables Configuration at " -msgstr "Agordo de datumbaz-tabeloj je " - -#: mod_configure.erl:918 -msgid "Choose storage type of tables" -msgstr "Elektu konserv-tipon de tabeloj" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Disc only copy" -msgstr "Nur disk-kopio" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM and disc copy" -msgstr "RAM- kaj disk-kopio" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM copy" -msgstr "RAM-kopio" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Remote copy" -msgstr "Fora kopio" - -#: mod_configure.erl:950 -msgid "Stop Modules at " -msgstr "Haltigu modulojn je " - -#: mod_configure.erl:954 -msgid "Choose modules to stop" -msgstr "Elektu modulojn por fini" - -#: mod_configure.erl:969 -msgid "Start Modules at " -msgstr "Startu modulojn je " - -#: mod_configure.erl:973 -msgid "Enter list of {Module, [Options]}" -msgstr "Enmetu liston de {Modulo, [Elektebloj]}" - -#: mod_configure.erl:974 -msgid "List of modules to start" -msgstr "Listo de moduloj por starti" - -#: mod_configure.erl:983 -msgid "Backup to File at " -msgstr "Faru sekurkopion je " - -#: mod_configure.erl:987 mod_configure.erl:1001 -msgid "Enter path to backup file" -msgstr "Enmetu vojon por sekurkopio" - -#: mod_configure.erl:988 mod_configure.erl:1002 mod_configure.erl:1016 -#: mod_configure.erl:1030 -msgid "Path to File" -msgstr "Voje de dosiero" - -#: mod_configure.erl:997 -msgid "Restore Backup from File at " -msgstr "Restaŭrigu de dosiero el " - -#: mod_configure.erl:1011 -msgid "Dump Backup to Text File at " -msgstr "Skribu sekurkopion en plata teksto al " - -#: mod_configure.erl:1015 -msgid "Enter path to text file" -msgstr "Enmetu vojon al plata teksto" - -#: mod_configure.erl:1025 -msgid "Import User from File at " -msgstr "Importu uzanton de dosiero el " - -#: mod_configure.erl:1029 -msgid "Enter path to jabberd14 spool file" -msgstr "Enmetu vojon al jabberd14-uzantdosiero" - -#: mod_configure.erl:1039 -msgid "Import Users from Dir at " -msgstr "Importu uzantojn de dosierujo ĉe " - -#: mod_configure.erl:1043 -msgid "Enter path to jabberd14 spool dir" -msgstr "Enmetu vojon al jabberd14-uzantdosierujo" - -#: mod_configure.erl:1044 -msgid "Path to Dir" -msgstr "Vojo al dosierujo" - -#: mod_configure.erl:1060 mod_configure.erl:1105 -msgid "Time delay" -msgstr "Prokrasto" - -#: mod_configure.erl:1143 -msgid "Access Control List Configuration" -msgstr "Agordo de atingokontrolo" - -#: mod_configure.erl:1147 -msgid "Access control lists" -msgstr "Atingokontrol-listoj" - -#: mod_configure.erl:1171 -msgid "Access Configuration" -msgstr "Agordo de atingo" - -#: mod_configure.erl:1175 -msgid "Access rules" -msgstr "Atingo-reguloj" - -#: mod_configure.erl:1200 mod_configure.erl:1222 mod_configure.erl:1234 -#: mod_configure.erl:1246 mod_configure.erl:1258 mod_configure.erl:1275 -#: mod_configure.erl:1287 mod_configure.erl:1650 mod_configure.erl:1700 -#: mod_configure.erl:1721 mod_roster.erl:943 mod_roster_odbc.erl:1060 -#: mod_vcard.erl:472 mod_vcard_ldap.erl:554 mod_vcard_odbc.erl:448 -msgid "Jabber ID" -msgstr "Jabber ID" - -#: mod_configure.erl:1205 mod_configure.erl:1263 mod_configure.erl:1651 -#: mod_configure.erl:1863 mod_muc/mod_muc_room.erl:3224 mod_register.erl:235 -#: web/ejabberd_web_admin.erl:1520 -msgid "Password" -msgstr "Pasvorto" - -#: mod_configure.erl:1210 -msgid "Password Verification" -msgstr "Pasvortkontrolo" - -#: mod_configure.erl:1301 -msgid "Number of registered users" -msgstr "Nombro de registritaj uzantoj" - -#: mod_configure.erl:1315 -msgid "Number of online users" -msgstr "Nombro de konektataj uzantoj" - -#: mod_configure.erl:1682 web/ejabberd_web_admin.erl:1588 -#: web/ejabberd_web_admin.erl:1743 -msgid "Never" -msgstr "Neniam" - -#: mod_configure.erl:1696 web/ejabberd_web_admin.erl:1601 -#: web/ejabberd_web_admin.erl:1756 -msgid "Online" -msgstr "Konektata" - -#: mod_configure.erl:1701 -msgid "Last login" -msgstr "Lasta ensaluto" - -#: mod_configure.erl:1722 -msgid "Roster size" -msgstr "Kontaktlist-grando" - -#: mod_configure.erl:1723 -msgid "IP addresses" -msgstr "IP-adresoj" - -#: mod_configure.erl:1724 -msgid "Resources" -msgstr "Risurcoj" - -#: mod_configure.erl:1850 -msgid "Administration of " -msgstr "Mastrumado de " - -#: mod_configure.erl:1853 -msgid "Action on user" -msgstr "Ago je uzanto" - -#: mod_configure.erl:1857 -msgid "Edit Properties" -msgstr "Redaktu atributojn" - -#: mod_configure.erl:1860 web/ejabberd_web_admin.erl:1769 -msgid "Remove User" -msgstr "Forigu uzanton" - -#: mod_irc/mod_irc.erl:201 mod_irc/mod_irc_odbc.erl:196 -#: mod_muc/mod_muc.erl:336 mod_muc/mod_muc_odbc.erl:342 -msgid "Access denied by service policy" -msgstr "Atingo rifuzita de serv-politiko" - -#: mod_irc/mod_irc.erl:401 mod_irc/mod_irc_odbc.erl:398 -msgid "IRC Transport" -msgstr "IRC-transportilo" - -#: mod_irc/mod_irc.erl:428 mod_irc/mod_irc_odbc.erl:425 -msgid "ejabberd IRC module" -msgstr "ejabberd IRC-modulo" - -#: mod_irc/mod_irc.erl:559 mod_irc/mod_irc_odbc.erl:568 -msgid "You need an x:data capable client to configure mod_irc settings" -msgstr "Vi bezonas klienton kun x:data-funkcio por agordi mod_irc" - -#: mod_irc/mod_irc.erl:566 mod_irc/mod_irc_odbc.erl:575 -msgid "Registration in mod_irc for " -msgstr "Registraĵo en mod_irc de " - -#: mod_irc/mod_irc.erl:571 mod_irc/mod_irc_odbc.erl:580 -msgid "" -"Enter username, encodings, ports and passwords you wish to use for " -"connecting to IRC servers" -msgstr "" -"Enmetu uzantnomon,j enkodigojn, pordojn kaj pasvortojn kiujn vi volas uzi " -"por konektoj al IRC-serviloj" - -#: mod_irc/mod_irc.erl:576 mod_irc/mod_irc_odbc.erl:585 -msgid "IRC Username" -msgstr "IRC-kaŝnomo" - -#: mod_irc/mod_irc.erl:586 mod_irc/mod_irc_odbc.erl:595 -msgid "" -"If you want to specify different ports, passwords, encodings for IRC " -"servers, fill this list with values in format '{\"irc server\", \"encoding" -"\", port, \"password\"}'. By default this service use \"~s\" encoding, port " -"~p, empty password." -msgstr "" -"Se vi volas specifi diversajn pordojn, pasvortojn, enkodigojn por IRC-" -"serviloj, kompletigu la jenan liston kun la formo '{\"irc-servilo\", " -"\"enkodigo\", porto, \"pasvorto\"}'. Se ne specifita, ĉi tiu servilo uzas la " -"enkodigo \"~s\", porto ~p, malplena pasvorto." - -#: mod_irc/mod_irc.erl:598 mod_irc/mod_irc_odbc.erl:607 -msgid "" -"Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef." -"net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]." -msgstr "" -"Ekzemplo: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"sekreto\"}, {\"vendetta." -"fef.net\", \"iso8859-1\", 7000}, {\"irc.iutestservilo.net\", \"utf-8\"}]." - -#: mod_irc/mod_irc.erl:603 mod_irc/mod_irc_odbc.erl:612 -msgid "Connections parameters" -msgstr "Konekto-parametroj" - -#: mod_irc/mod_irc.erl:728 mod_irc/mod_irc_odbc.erl:757 -msgid "Join IRC channel" -msgstr "Eniras IRC-babilejon" - -#: mod_irc/mod_irc.erl:732 mod_irc/mod_irc_odbc.erl:761 -msgid "IRC channel (don't put the first #)" -msgstr "IRC-babilejo (ne aldonu #-prefikson)" - -#: mod_irc/mod_irc.erl:737 mod_irc/mod_irc_odbc.erl:766 -msgid "IRC server" -msgstr "IRC-servilo" - -#: mod_irc/mod_irc.erl:770 mod_irc/mod_irc.erl:774 -#: mod_irc/mod_irc_odbc.erl:799 mod_irc/mod_irc_odbc.erl:803 -msgid "Join the IRC channel here." -msgstr "Eniru IRC-babilejon jen" - -#: mod_irc/mod_irc.erl:778 mod_irc/mod_irc_odbc.erl:807 -msgid "Join the IRC channel in this Jabber ID: ~s" -msgstr "Eniru IRC-babilejon en ĉi Jabber-ID: ~s" - -#: mod_irc/mod_irc.erl:863 mod_irc/mod_irc_odbc.erl:904 -msgid "IRC settings" -msgstr "IRC agordoj" - -#: mod_irc/mod_irc.erl:868 mod_irc/mod_irc_odbc.erl:909 -#, fuzzy -msgid "" -"Enter username and encodings you wish to use for connecting to IRC servers. " -"Press 'Next' to get more fields to fill in. Press 'Complete' to save " -"settings." -msgstr "" -"Enmetu uzantnomon kaj enkodigoj kiujn vi volas uzi por konektoj al IRC-" -"serviloj. Elektu \"Sekvonto\" por ekhavi pliajn kampojn. Elektu \"Kompletigu" -"\" por savi agordojn." - -#: mod_irc/mod_irc.erl:874 mod_irc/mod_irc_odbc.erl:915 -msgid "IRC username" -msgstr "IRC-uzantnomo" - -#: mod_irc/mod_irc.erl:923 mod_irc/mod_irc_odbc.erl:964 -msgid "Password ~b" -msgstr "Pasvorto ~b" - -#: mod_irc/mod_irc.erl:928 mod_irc/mod_irc_odbc.erl:969 -msgid "Port ~b" -msgstr "Pordo ~b" - -#: mod_irc/mod_irc.erl:933 mod_irc/mod_irc_odbc.erl:974 -msgid "Encoding for server ~b" -msgstr "Enkodigo por servilo ~b" - -#: mod_irc/mod_irc.erl:942 mod_irc/mod_irc_odbc.erl:983 -msgid "Server ~b" -msgstr "Servilo ~b" - -#: mod_muc/mod_muc.erl:449 mod_muc/mod_muc_odbc.erl:456 -msgid "Only service administrators are allowed to send service messages" -msgstr "Nur servo-administrantoj rajtas sendi serv-mesaĝojn" - -#: mod_muc/mod_muc.erl:493 mod_muc/mod_muc_odbc.erl:500 -msgid "Room creation is denied by service policy" -msgstr "Ĉi tiu serv-politiko ne permesas babilejo-kreadon" - -#: mod_muc/mod_muc.erl:500 mod_muc/mod_muc_odbc.erl:507 -msgid "Conference room does not exist" -msgstr "Babilejo ne ekzistas" - -#: mod_muc/mod_muc.erl:582 mod_muc/mod_muc_odbc.erl:595 -msgid "Chatrooms" -msgstr "Babilejoj" - -#: mod_muc/mod_muc.erl:712 mod_muc/mod_muc_odbc.erl:729 -msgid "You need a client that supports x:data to register the nickname" -msgstr "Vi bezonas klienton subtenante x:data-funkcio por registri kaŝnomon" - -#: mod_muc/mod_muc.erl:718 mod_muc/mod_muc_odbc.erl:735 -msgid "Nickname Registration at " -msgstr "Kaŝnomo-registrado je " - -#: mod_muc/mod_muc.erl:722 mod_muc/mod_muc_odbc.erl:739 -msgid "Enter nickname you want to register" -msgstr "Enmetu kaŝnomon kiun vi volas registri" - -#: mod_muc/mod_muc.erl:723 mod_muc/mod_muc_odbc.erl:740 -#: mod_muc/mod_muc_room.erl:3810 mod_roster.erl:944 mod_roster_odbc.erl:1061 -#: mod_vcard.erl:364 mod_vcard.erl:477 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:453 -msgid "Nickname" -msgstr "Kaŝnomo" - -#: mod_muc/mod_muc.erl:762 mod_muc/mod_muc_odbc.erl:784 -#: mod_muc/mod_muc_room.erl:1064 mod_muc/mod_muc_room.erl:1787 -msgid "That nickname is registered by another person" -msgstr "Kaŝnomo estas registrita de alia persono" - -#: mod_muc/mod_muc.erl:788 mod_muc/mod_muc_odbc.erl:811 -msgid "You must fill in field \"Nickname\" in the form" -msgstr "Vi devas kompletigi la \"Kaŝnomo\" kampon" - -#: mod_muc/mod_muc.erl:808 mod_muc/mod_muc_odbc.erl:831 -msgid "ejabberd MUC module" -msgstr "ejabberd MUC-modulo" - -#: mod_muc/mod_muc_log.erl:374 mod_muc/mod_muc_log.erl:381 -msgid "Chatroom configuration modified" -msgstr "Agordo de babilejo ŝanĝita" - -#: mod_muc/mod_muc_log.erl:384 -msgid "joins the room" -msgstr "eniras la babilejo" - -#: mod_muc/mod_muc_log.erl:387 mod_muc/mod_muc_log.erl:390 -msgid "leaves the room" -msgstr "eliras la babilejo" - -#: mod_muc/mod_muc_log.erl:393 mod_muc/mod_muc_log.erl:396 -msgid "has been banned" -msgstr "estas forbarita" - -#: mod_muc/mod_muc_log.erl:399 mod_muc/mod_muc_log.erl:402 -msgid "has been kicked" -msgstr "estas forpelita" - -#: mod_muc/mod_muc_log.erl:405 -msgid "has been kicked because of an affiliation change" -msgstr "estas forpelita pro aparteneca ŝanĝo" - -#: mod_muc/mod_muc_log.erl:408 -msgid "has been kicked because the room has been changed to members-only" -msgstr "estas forpelita ĉar la babilejo fariĝis sole por membroj" - -#: mod_muc/mod_muc_log.erl:411 -msgid "has been kicked because of a system shutdown" -msgstr "estas forpelita pro sistem-haltigo" - -#: mod_muc/mod_muc_log.erl:414 -msgid "is now known as" -msgstr "nun nomiĝas" - -#: mod_muc/mod_muc_log.erl:417 mod_muc/mod_muc_log.erl:688 -#: mod_muc/mod_muc_room.erl:2393 -msgid " has set the subject to: " -msgstr " ŝanĝis la temon al: " - -#: mod_muc/mod_muc_log.erl:452 -msgid "Chatroom is created" -msgstr "Babilejo kreita" - -#: mod_muc/mod_muc_log.erl:453 -msgid "Chatroom is destroyed" -msgstr "Babilejo neniigita" - -#: mod_muc/mod_muc_log.erl:454 -msgid "Chatroom is started" -msgstr "Babilejo lanĉita" - -#: mod_muc/mod_muc_log.erl:455 -msgid "Chatroom is stopped" -msgstr "Babilejo haltita" - -#: mod_muc/mod_muc_log.erl:459 -msgid "Monday" -msgstr "Lundo" - -#: mod_muc/mod_muc_log.erl:460 -msgid "Tuesday" -msgstr "Mardo" - -#: mod_muc/mod_muc_log.erl:461 -msgid "Wednesday" -msgstr "Merkredo" - -#: mod_muc/mod_muc_log.erl:462 -msgid "Thursday" -msgstr "Ĵaŭdo" - -#: mod_muc/mod_muc_log.erl:463 -msgid "Friday" -msgstr "Vendredo" - -#: mod_muc/mod_muc_log.erl:464 -msgid "Saturday" -msgstr "Sabato" - -#: mod_muc/mod_muc_log.erl:465 -msgid "Sunday" -msgstr "Dimanĉo" - -#: mod_muc/mod_muc_log.erl:469 -msgid "January" -msgstr "Januaro" - -#: mod_muc/mod_muc_log.erl:470 -msgid "February" -msgstr "Februaro" - -#: mod_muc/mod_muc_log.erl:471 -msgid "March" -msgstr "Marĉo" - -#: mod_muc/mod_muc_log.erl:472 -msgid "April" -msgstr "Aprilo" - -#: mod_muc/mod_muc_log.erl:473 -msgid "May" -msgstr "Majo" - -#: mod_muc/mod_muc_log.erl:474 -msgid "June" -msgstr "Junio" - -#: mod_muc/mod_muc_log.erl:475 -msgid "July" -msgstr "Julio" - -#: mod_muc/mod_muc_log.erl:476 -msgid "August" -msgstr "Aŭgusto" - -#: mod_muc/mod_muc_log.erl:477 -msgid "September" -msgstr "Septembro" - -#: mod_muc/mod_muc_log.erl:478 -msgid "October" -msgstr "Oktobro" - -#: mod_muc/mod_muc_log.erl:479 -msgid "November" -msgstr "Novembro" - -#: mod_muc/mod_muc_log.erl:480 -msgid "December" -msgstr "Decembro" - -#: mod_muc/mod_muc_log.erl:750 -msgid "Room Configuration" -msgstr "Babilejo-agordo" - -#: mod_muc/mod_muc_log.erl:759 -msgid "Room Occupants" -msgstr "Nombro de ĉeestantoj" - -#: mod_muc/mod_muc_room.erl:174 -msgid "Traffic rate limit is exceeded" -msgstr "Trafikrapida limigo superita" - -#: mod_muc/mod_muc_room.erl:246 -msgid "" -"This participant is kicked from the room because he sent an error message" -msgstr "" -"Ĉi tiu partoprenanta estas forpelata de la babilejo pro sendado de erar-" -"mesaĝo" - -#: mod_muc/mod_muc_room.erl:255 -msgid "It is not allowed to send private messages to the conference" -msgstr "Nur partoprenantoj rajtas sendi privatajn mesaĝojn al la babilejo" - -#: mod_muc/mod_muc_room.erl:332 -msgid "Please, wait for a while before sending new voice request" -msgstr "Bonvolu atendi iomete antaŭ ol sendi plian voĉ-peton" - -#: mod_muc/mod_muc_room.erl:347 -msgid "Voice requests are disabled in this conference" -msgstr "Voĉ-petoj estas malebligita en jena babilejo" - -#: mod_muc/mod_muc_room.erl:364 -msgid "Failed to extract JID from your voice request approval" -msgstr "Malsukcesis ekstrakti JID-on de via voĉ-pet-aprobo" - -#: mod_muc/mod_muc_room.erl:393 -msgid "Only moderators can approve voice requests" -msgstr "Nur moderigantoj povas aprobi voĉ-petojn" - -#: mod_muc/mod_muc_room.erl:408 -msgid "Improper message type" -msgstr "Malĝusta mesaĝo-tipo" - -#: mod_muc/mod_muc_room.erl:518 -msgid "" -"This participant is kicked from the room because he sent an error message to " -"another participant" -msgstr "" -"Ĉi tiu partoprenanto estas forpelata de la babilejo pro sendo de erar-mesaĝo " -"al alia partoprenanto" - -#: mod_muc/mod_muc_room.erl:531 -msgid "It is not allowed to send private messages of type \"groupchat\"" -msgstr "Malpermesas sendi mesaĝojn de tipo \"groupchat\"" - -#: mod_muc/mod_muc_room.erl:543 mod_muc/mod_muc_room.erl:611 -msgid "Recipient is not in the conference room" -msgstr "Ricevanto ne ĉeestas en la babilejo " - -#: mod_muc/mod_muc_room.erl:564 mod_muc/mod_muc_room.erl:585 -msgid "It is not allowed to send private messages" -msgstr "Ne estas permesata sendi privatajn mesaĝojn" - -#: mod_muc/mod_muc_room.erl:576 mod_muc/mod_muc_room.erl:956 -#: mod_muc/mod_muc_room.erl:4040 -msgid "Only occupants are allowed to send messages to the conference" -msgstr "Nur partoprenantoj rajtas sendi mesaĝojn al la babilejo" - -#: mod_muc/mod_muc_room.erl:634 -msgid "Only occupants are allowed to send queries to the conference" -msgstr "Nur partoprenantoj rajtas sendi informmendojn al la babilejoj" - -#: mod_muc/mod_muc_room.erl:646 -msgid "Queries to the conference members are not allowed in this room" -msgstr "Malpermesas informmendoj al partoprenantoj en ĉi tiu babilejo" - -#: mod_muc/mod_muc_room.erl:932 -msgid "" -"Only moderators and participants are allowed to change the subject in this " -"room" -msgstr "" -"Nur moderigantoj kaj partoprenantoj rajtas ŝanĝi la temon en ĉi tiu babilejo" - -#: mod_muc/mod_muc_room.erl:937 -msgid "Only moderators are allowed to change the subject in this room" -msgstr "Nur moderigantoj rajtas ŝanĝi la temon en ĉi tiu babilejo" - -#: mod_muc/mod_muc_room.erl:947 -msgid "Visitors are not allowed to send messages to all occupants" -msgstr "Vizitantoj ne rajtas sendi mesaĝojn al ĉiuj partoprenantoj" - -#: mod_muc/mod_muc_room.erl:1021 -msgid "" -"This participant is kicked from the room because he sent an error presence" -msgstr "" -"Ĉi tiu partoprenanto estas forpelata de la babilejo pro sendo de erar-ĉeesto" - -#: mod_muc/mod_muc_room.erl:1040 -msgid "Visitors are not allowed to change their nicknames in this room" -msgstr "" -"Ne estas permesata al vizitantoj ŝanĝi siajn kaŝnomojn en ĉi tiu ĉambro" - -#: mod_muc/mod_muc_room.erl:1053 mod_muc/mod_muc_room.erl:1779 -msgid "That nickname is already in use by another occupant" -msgstr "Tiu kaŝnomo jam estas uzata de alia partoprenanto" - -#: mod_muc/mod_muc_room.erl:1768 -msgid "You have been banned from this room" -msgstr "Vi estas malpermesata en ĉi tiu babilejo" - -#: mod_muc/mod_muc_room.erl:1771 -msgid "Membership is required to enter this room" -msgstr "Membreco estas bezonata por eniri ĉi tiun babilejon" - -#: mod_muc/mod_muc_room.erl:1807 -msgid "This room is not anonymous" -msgstr "Ĉi tiu babilejo ne estas anonima" - -#: mod_muc/mod_muc_room.erl:1833 -msgid "A password is required to enter this room" -msgstr "Pasvorto estas bezonata por eniri ĉi tiun babilejon" - -#: mod_muc/mod_muc_room.erl:1855 mod_register.erl:246 -msgid "Too many CAPTCHA requests" -msgstr "Tro multaj CAPTCHA-petoj" - -#: mod_muc/mod_muc_room.erl:1864 mod_register.erl:251 -msgid "Unable to generate a CAPTCHA" -msgstr "Ne eblis krei CAPTCHA" - -#: mod_muc/mod_muc_room.erl:1874 -msgid "Incorrect password" -msgstr "Nekorekta pasvorto" - -#: mod_muc/mod_muc_room.erl:2448 -msgid "Administrator privileges required" -msgstr "Administrantaj rajtoj bezonata" - -#: mod_muc/mod_muc_room.erl:2463 -msgid "Moderator privileges required" -msgstr "Moderantaj rajtoj bezonata" - -#: mod_muc/mod_muc_room.erl:2619 -msgid "Jabber ID ~s is invalid" -msgstr "Jabber ID ~s estas nevalida" - -#: mod_muc/mod_muc_room.erl:2633 -msgid "Nickname ~s does not exist in the room" -msgstr "Kaŝnomo ~s ne ekzistas en la babilejo" - -#: mod_muc/mod_muc_room.erl:2659 mod_muc/mod_muc_room.erl:3049 -msgid "Invalid affiliation: ~s" -msgstr "Nevalida aparteneco: ~s" - -#: mod_muc/mod_muc_room.erl:2713 -msgid "Invalid role: ~s" -msgstr "Nevalida rolo: ~s" - -#: mod_muc/mod_muc_room.erl:3026 mod_muc/mod_muc_room.erl:3062 -msgid "Owner privileges required" -msgstr "Mastraj rajtoj bezonata" - -#: mod_muc/mod_muc_room.erl:3195 -msgid "Configuration of room ~s" -msgstr "Agordo de babilejo ~s" - -#: mod_muc/mod_muc_room.erl:3200 -msgid "Room title" -msgstr "Babilejo-nomo" - -#: mod_muc/mod_muc_room.erl:3203 mod_muc/mod_muc_room.erl:3692 -msgid "Room description" -msgstr "Babilejo-priskribo" - -#: mod_muc/mod_muc_room.erl:3210 -msgid "Make room persistent" -msgstr "Farigu babilejon daŭra" - -#: mod_muc/mod_muc_room.erl:3215 -msgid "Make room public searchable" -msgstr "Farigu babilejon publike trovebla" - -#: mod_muc/mod_muc_room.erl:3218 -msgid "Make participants list public" -msgstr "Farigu partoprento-liston publika" - -#: mod_muc/mod_muc_room.erl:3221 -msgid "Make room password protected" -msgstr "Farigu babilejon protektata per pasvorto" - -#: mod_muc/mod_muc_room.erl:3232 -msgid "Maximum Number of Occupants" -msgstr "Limigo de nombro de partoprenantoj" - -#: mod_muc/mod_muc_room.erl:3239 -msgid "No limit" -msgstr "Neniu limigo" - -#: mod_muc/mod_muc_room.erl:3250 -msgid "Present real Jabber IDs to" -msgstr "Montru verajn Jabber ID-ojn al" - -#: mod_muc/mod_muc_room.erl:3258 mod_muc/mod_muc_room.erl:3292 -msgid "moderators only" -msgstr "moderantoj sole" - -#: mod_muc/mod_muc_room.erl:3260 mod_muc/mod_muc_room.erl:3294 -msgid "anyone" -msgstr "iu ajn" - -#: mod_muc/mod_muc_room.erl:3262 -msgid "Make room members-only" -msgstr "Farigu babilejon sole por membroj" - -#: mod_muc/mod_muc_room.erl:3265 -msgid "Make room moderated" -msgstr "Farigu babilejon moderigata" - -#: mod_muc/mod_muc_room.erl:3268 -msgid "Default users as participants" -msgstr "Kutime farigu uzantojn kiel partpoprenantoj" - -#: mod_muc/mod_muc_room.erl:3271 -msgid "Allow users to change the subject" -msgstr "Permesu uzantojn ŝanĝi la temon" - -#: mod_muc/mod_muc_room.erl:3274 -msgid "Allow users to send private messages" -msgstr "Permesu uzantojn sendi privatajn mesaĝojn" - -#: mod_muc/mod_muc_room.erl:3279 -msgid "Allow visitors to send private messages to" -msgstr "Permesu uzantojn sendi privatajn mesaĝojn al" - -#: mod_muc/mod_muc_room.erl:3290 -msgid "nobody" -msgstr "neniu" - -#: mod_muc/mod_muc_room.erl:3296 -msgid "Allow users to query other users" -msgstr "Permesu uzantojn informpeti aliajn uzantojn" - -#: mod_muc/mod_muc_room.erl:3299 -msgid "Allow users to send invites" -msgstr "Permesu uzantojn sendi invitojn" - -#: mod_muc/mod_muc_room.erl:3302 -msgid "Allow visitors to send status text in presence updates" -msgstr "Permesu al vizitantoj sendi statmesaĝon en ĉeest-sciigoj" - -#: mod_muc/mod_muc_room.erl:3305 -msgid "Allow visitors to change nickname" -msgstr "Permesu al vizitantoj ŝanĝi siajn kaŝnomojn" - -#: mod_muc/mod_muc_room.erl:3308 -msgid "Allow visitors to send voice requests" -msgstr "Permesu uzantojn sendi voĉ-petojn" - -#: mod_muc/mod_muc_room.erl:3311 -msgid "Minimum interval between voice requests (in seconds)" -msgstr "Minimuma intervalo inter voĉ-petoj (je sekundoj)" - -#: mod_muc/mod_muc_room.erl:3317 -msgid "Make room CAPTCHA protected" -msgstr "Farigu babilejon protektata per CAPTCHA" - -#: mod_muc/mod_muc_room.erl:3322 -msgid "Exclude Jabber IDs from CAPTCHA challenge" -msgstr "Esceptu Ĵabber-identigilojn je CAPTCHA-defio" - -#: mod_muc/mod_muc_room.erl:3329 -msgid "Enable logging" -msgstr "Ŝaltu protokoladon" - -#: mod_muc/mod_muc_room.erl:3337 -msgid "You need an x:data capable client to configure room" -msgstr "Vi bezonas klienton kun x:data-funkcio por agordi la babilejon" - -#: mod_muc/mod_muc_room.erl:3694 -msgid "Number of occupants" -msgstr "Nombro de ĉeestantoj" - -#: mod_muc/mod_muc_room.erl:3750 -msgid "private, " -msgstr "privata, " - -#: mod_muc/mod_muc_room.erl:3799 -msgid "Voice request" -msgstr "Voĉ-peto" - -#: mod_muc/mod_muc_room.erl:3803 -msgid "Either approve or decline the voice request." -msgstr "Ĉu aprobu, aŭ malaprobu la voĉ-peton." - -#: mod_muc/mod_muc_room.erl:3809 -msgid "User JID" -msgstr "Uzant-JID" - -#: mod_muc/mod_muc_room.erl:3811 -msgid "Grant voice to this person?" -msgstr "Koncedu voĉon al ĉi-persono?" - -#: mod_muc/mod_muc_room.erl:3960 -msgid "~s invites you to the room ~s" -msgstr "~s invitas vin al la babilejo ~s" - -#: mod_muc/mod_muc_room.erl:3969 -msgid "the password is" -msgstr "la pasvorto estas" - -#: mod_offline.erl:510 mod_offline_odbc.erl:352 -msgid "" -"Your contact offline message queue is full. The message has been discarded." -msgstr "" -"Mesaĝo-atendovico de la senkonekta kontakto estas plena. La mesaĝo estas " -"forĵetita" - -#: mod_offline.erl:560 mod_offline_odbc.erl:408 -msgid "~s's Offline Messages Queue" -msgstr "Mesaĝo-atendovico de ~s" - -#: mod_offline.erl:563 mod_offline_odbc.erl:411 mod_roster.erl:987 -#: mod_roster_odbc.erl:1104 mod_shared_roster.erl:901 -#: mod_shared_roster.erl:1010 mod_shared_roster_odbc.erl:930 -#: mod_shared_roster_odbc.erl:1039 web/ejabberd_web_admin.erl:833 -#: web/ejabberd_web_admin.erl:874 web/ejabberd_web_admin.erl:942 -#: web/ejabberd_web_admin.erl:978 web/ejabberd_web_admin.erl:1019 -#: web/ejabberd_web_admin.erl:1508 web/ejabberd_web_admin.erl:1760 -#: web/ejabberd_web_admin.erl:1931 web/ejabberd_web_admin.erl:1963 -#: web/ejabberd_web_admin.erl:2031 web/ejabberd_web_admin.erl:2135 -#: web/ejabberd_web_admin.erl:2160 web/ejabberd_web_admin.erl:2248 -msgid "Submitted" -msgstr "Sendita" - -#: mod_offline.erl:571 -msgid "Time" -msgstr "Tempo" - -#: mod_offline.erl:572 -msgid "From" -msgstr "De" - -#: mod_offline.erl:573 -msgid "To" -msgstr "Ĝis" - -#: mod_offline.erl:574 mod_offline_odbc.erl:419 -msgid "Packet" -msgstr "Pakaĵo" - -#: mod_offline.erl:587 mod_offline_odbc.erl:432 mod_shared_roster.erl:908 -#: mod_shared_roster_odbc.erl:937 web/ejabberd_web_admin.erl:882 -#: web/ejabberd_web_admin.erl:986 -msgid "Delete Selected" -msgstr "Forigu elektata(j)n" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -msgid "Offline Messages:" -msgstr "Liverontaj mesaĝoj" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -msgid "Remove All Offline Messages" -msgstr "Forigu ĉiujn liverontajn mesaĝojn" - -#: mod_proxy65/mod_proxy65_service.erl:213 -msgid "ejabberd SOCKS5 Bytestreams module" -msgstr "ejabberd SOCKS5 Bajtfluo modulo" - -#: mod_pubsub/mod_pubsub.erl:1120 mod_pubsub/mod_pubsub_odbc.erl:929 -msgid "Publish-Subscribe" -msgstr "Public-Abonado" - -#: mod_pubsub/mod_pubsub.erl:1214 mod_pubsub/mod_pubsub_odbc.erl:1025 -msgid "ejabberd Publish-Subscribe module" -msgstr "ejabberd Public-Abonada modulo" - -#: mod_pubsub/mod_pubsub.erl:1497 mod_pubsub/mod_pubsub_odbc.erl:1312 -msgid "PubSub subscriber request" -msgstr "PubAbo abonpeto" - -#: mod_pubsub/mod_pubsub.erl:1499 mod_pubsub/mod_pubsub_odbc.erl:1314 -msgid "Choose whether to approve this entity's subscription." -msgstr "Elektu ĉu permesi la abonon de ĉi tiu ento" - -#: mod_pubsub/mod_pubsub.erl:1505 mod_pubsub/mod_pubsub_odbc.erl:1320 -msgid "Node ID" -msgstr "Nodo ID" - -#: mod_pubsub/mod_pubsub.erl:1510 mod_pubsub/mod_pubsub_odbc.erl:1325 -msgid "Subscriber Address" -msgstr "Abonanta adreso" - -#: mod_pubsub/mod_pubsub.erl:1516 mod_pubsub/mod_pubsub_odbc.erl:1331 -msgid "Allow this Jabber ID to subscribe to this pubsub node?" -msgstr "Ĉu permesi ĉi tiun Jabber ID aboni al la jena PubAbo-nodo" - -#: mod_pubsub/mod_pubsub.erl:3474 mod_pubsub/mod_pubsub_odbc.erl:3287 -msgid "Deliver payloads with event notifications" -msgstr "Liveru aĵojn de event-sciigoj" - -#: mod_pubsub/mod_pubsub.erl:3475 mod_pubsub/mod_pubsub_odbc.erl:3288 -msgid "Deliver event notifications" -msgstr "Liveru event-sciigojn" - -#: mod_pubsub/mod_pubsub.erl:3476 mod_pubsub/mod_pubsub_odbc.erl:3289 -msgid "Notify subscribers when the node configuration changes" -msgstr "Sciigu abonantoj kiam la agordo de la nodo ŝanĝas" - -#: mod_pubsub/mod_pubsub.erl:3477 mod_pubsub/mod_pubsub_odbc.erl:3290 -msgid "Notify subscribers when the node is deleted" -msgstr "Sciigu abonantoj kiam la nodo estas forigita" - -#: mod_pubsub/mod_pubsub.erl:3478 mod_pubsub/mod_pubsub_odbc.erl:3291 -msgid "Notify subscribers when items are removed from the node" -msgstr "Sciigu abonantoj kiam eroj estas forigita de la nodo" - -#: mod_pubsub/mod_pubsub.erl:3479 mod_pubsub/mod_pubsub_odbc.erl:3292 -msgid "Persist items to storage" -msgstr "Savu erojn en konservado" - -#: mod_pubsub/mod_pubsub.erl:3480 mod_pubsub/mod_pubsub_odbc.erl:3293 -msgid "A friendly name for the node" -msgstr "Kromnomo por ĉi tiu nodo" - -#: mod_pubsub/mod_pubsub.erl:3481 mod_pubsub/mod_pubsub_odbc.erl:3294 -msgid "Max # of items to persist" -msgstr "Maksimuma kiomo de eroj en konservado" - -#: mod_pubsub/mod_pubsub.erl:3482 mod_pubsub/mod_pubsub_odbc.erl:3295 -msgid "Whether to allow subscriptions" -msgstr "Ĉu permesi aboni" - -#: mod_pubsub/mod_pubsub.erl:3483 mod_pubsub/mod_pubsub_odbc.erl:3296 -msgid "Specify the access model" -msgstr "Specifu atingo-modelon" - -#: mod_pubsub/mod_pubsub.erl:3486 mod_pubsub/mod_pubsub_odbc.erl:3299 -msgid "Roster groups allowed to subscribe" -msgstr "Kontaktlist-grupoj kiuj rajtas aboni" - -#: mod_pubsub/mod_pubsub.erl:3487 mod_pubsub/mod_pubsub_odbc.erl:3300 -msgid "Specify the publisher model" -msgstr "Enmetu publikadan modelon" - -#: mod_pubsub/mod_pubsub.erl:3489 mod_pubsub/mod_pubsub_odbc.erl:3302 -msgid "Purge all items when the relevant publisher goes offline" -msgstr "Forigu ĉiujn erojn kiam la rilata publikanto malkonektiĝas" - -#: mod_pubsub/mod_pubsub.erl:3490 mod_pubsub/mod_pubsub_odbc.erl:3303 -msgid "Specify the event message type" -msgstr "Specifu tipo de event-mesaĝo" - -#: mod_pubsub/mod_pubsub.erl:3492 mod_pubsub/mod_pubsub_odbc.erl:3305 -msgid "Max payload size in bytes" -msgstr "Maksimuma aĵo-grando je bajtoj" - -#: mod_pubsub/mod_pubsub.erl:3493 mod_pubsub/mod_pubsub_odbc.erl:3306 -msgid "When to send the last published item" -msgstr "Kiam sendi la laste publicitan eron" - -#: mod_pubsub/mod_pubsub.erl:3495 mod_pubsub/mod_pubsub_odbc.erl:3308 -msgid "Only deliver notifications to available users" -msgstr "Nur liveru sciigojn al konektataj uzantoj" - -#: mod_pubsub/mod_pubsub.erl:3496 mod_pubsub/mod_pubsub_odbc.erl:3309 -msgid "The collections with which a node is affiliated" -msgstr "Aro kun kiu nodo estas filigita" - -#: mod_register.erl:193 -msgid "The CAPTCHA verification has failed" -msgstr "La CAPTCHA-kontrolado malsukcesis" - -#: mod_register.erl:220 -msgid "You need a client that supports x:data and CAPTCHA to register" -msgstr "" -"Vi bezonas klienton subtenante x:data-funkcio kaj CAPTCHA por registri " -"kaŝnomon" - -#: mod_register.erl:226 mod_register.erl:265 -msgid "Choose a username and password to register with this server" -msgstr "Elektu uzantnomon kaj pasvorton por registri je ĉi tiu servilo" - -#: mod_register.erl:230 mod_vcard.erl:364 mod_vcard_odbc.erl:342 -#: web/ejabberd_web_admin.erl:1515 web/ejabberd_web_admin.erl:1572 -msgid "User" -msgstr "Uzanto" - -#: mod_register.erl:316 mod_register.erl:361 -msgid "The password is too weak" -msgstr "La pasvorto estas ne sufiĉe forta" - -#: mod_register.erl:365 -msgid "Users are not allowed to register accounts so quickly" -msgstr "Ne estas permesata al uzantoj registri tiel rapide" - -#: mod_roster.erl:938 mod_roster_odbc.erl:1055 web/ejabberd_web_admin.erl:1701 -#: web/ejabberd_web_admin.erl:1886 web/ejabberd_web_admin.erl:1897 -#: web/ejabberd_web_admin.erl:2219 -msgid "None" -msgstr "Nenio" - -#: mod_roster.erl:945 mod_roster_odbc.erl:1062 -msgid "Subscription" -msgstr "Abono" - -#: mod_roster.erl:946 mod_roster_odbc.erl:1063 -msgid "Pending" -msgstr "Atendanta" - -#: mod_roster.erl:947 mod_roster_odbc.erl:1064 -msgid "Groups" -msgstr "Grupoj" - -#: mod_roster.erl:974 mod_roster_odbc.erl:1091 -msgid "Validate" -msgstr "Validigu" - -#: mod_roster.erl:982 mod_roster_odbc.erl:1099 -msgid "Remove" -msgstr "Forigu" - -#: mod_roster.erl:985 mod_roster_odbc.erl:1102 -msgid "Roster of " -msgstr "Kontaktlisto de " - -#: mod_roster.erl:988 mod_roster_odbc.erl:1105 mod_shared_roster.erl:902 -#: mod_shared_roster.erl:1011 mod_shared_roster_odbc.erl:931 -#: mod_shared_roster_odbc.erl:1040 web/ejabberd_web_admin.erl:834 -#: web/ejabberd_web_admin.erl:875 web/ejabberd_web_admin.erl:943 -#: web/ejabberd_web_admin.erl:979 web/ejabberd_web_admin.erl:1020 -#: web/ejabberd_web_admin.erl:1509 web/ejabberd_web_admin.erl:1761 -#: web/ejabberd_web_admin.erl:1932 web/ejabberd_web_admin.erl:2136 -#: web/ejabberd_web_admin.erl:2161 -msgid "Bad format" -msgstr "Malĝusta formo" - -#: mod_roster.erl:995 mod_roster_odbc.erl:1112 -msgid "Add Jabber ID" -msgstr "Aldonu Jabber ID" - -#: mod_roster.erl:1094 mod_roster_odbc.erl:1211 -msgid "Roster" -msgstr "Kontaktlisto" - -#: mod_shared_roster.erl:857 mod_shared_roster.erl:899 -#: mod_shared_roster.erl:1007 mod_shared_roster_odbc.erl:886 -#: mod_shared_roster_odbc.erl:928 mod_shared_roster_odbc.erl:1036 -msgid "Shared Roster Groups" -msgstr "Komuna Kontaktlist-grupo" - -#: mod_shared_roster.erl:895 mod_shared_roster_odbc.erl:924 -#: web/ejabberd_web_admin.erl:1365 web/ejabberd_web_admin.erl:2461 -msgid "Add New" -msgstr "Aldonu novan" - -#: mod_shared_roster.erl:978 mod_shared_roster_odbc.erl:1007 -msgid "Name:" -msgstr "Nomo:" - -#: mod_shared_roster.erl:983 mod_shared_roster_odbc.erl:1012 -msgid "Description:" -msgstr "Priskribo:" - -#: mod_shared_roster.erl:991 mod_shared_roster_odbc.erl:1020 -msgid "Members:" -msgstr "Membroj:" - -#: mod_shared_roster.erl:999 mod_shared_roster_odbc.erl:1028 -msgid "Displayed Groups:" -msgstr "Montrataj grupoj:" - -#: mod_shared_roster.erl:1008 mod_shared_roster_odbc.erl:1037 -msgid "Group " -msgstr "Grupo " - -#: mod_shared_roster.erl:1017 mod_shared_roster_odbc.erl:1046 -#: web/ejabberd_web_admin.erl:840 web/ejabberd_web_admin.erl:884 -#: web/ejabberd_web_admin.erl:949 web/ejabberd_web_admin.erl:1026 -#: web/ejabberd_web_admin.erl:2017 -msgid "Submit" -msgstr "Sendu" - -#: mod_vcard.erl:165 mod_vcard_ldap.erl:238 mod_vcard_odbc.erl:129 -msgid "Erlang Jabber Server" -msgstr "Erlang-a Jabber-Servilo" - -#: mod_vcard.erl:364 mod_vcard.erl:478 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:454 -msgid "Birthday" -msgstr "Naskiĝtago" - -#: mod_vcard.erl:364 mod_vcard.erl:480 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:456 -msgid "City" -msgstr "Urbo" - -#: mod_vcard.erl:364 mod_vcard.erl:479 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:455 -msgid "Country" -msgstr "Lando" - -#: mod_vcard.erl:364 mod_vcard.erl:481 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:457 -msgid "Email" -msgstr "Retpoŝto" - -#: mod_vcard.erl:364 mod_vcard.erl:476 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:452 -msgid "Family Name" -msgstr "Lasta Nomo" - -#: mod_vcard.erl:364 mod_vcard_odbc.erl:342 -msgid "" -"Fill in the form to search for any matching Jabber User (Add * to the end of " -"field to match substring)" -msgstr "" -"Kompletigu la formon por serĉi rekonata Jabber-uzanto (Aldonu * je la fino " -"de la kampo por rekoni subĉenon" - -#: mod_vcard.erl:364 mod_vcard.erl:473 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:449 -msgid "Full Name" -msgstr "Plena Nomo" - -#: mod_vcard.erl:364 mod_vcard.erl:475 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:451 -msgid "Middle Name" -msgstr "Meza Nomo" - -#: mod_vcard.erl:364 mod_vcard.erl:474 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:450 web/ejabberd_web_admin.erl:2006 -msgid "Name" -msgstr "Nomo" - -#: mod_vcard.erl:364 mod_vcard.erl:482 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:458 -msgid "Organization Name" -msgstr "Organiz-nomo" - -#: mod_vcard.erl:364 mod_vcard.erl:483 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:459 -msgid "Organization Unit" -msgstr "Organiz-parto" - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "Search users in " -msgstr "Serĉu uzantojn en " - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "You need an x:data capable client to search" -msgstr "Vi bezonas klienton kun x:data-funkcio por serĉado" - -#: mod_vcard.erl:389 mod_vcard_ldap.erl:487 mod_vcard_odbc.erl:367 -msgid "vCard User Search" -msgstr "Serĉado de vizitkartoj" - -#: mod_vcard.erl:445 mod_vcard_ldap.erl:541 mod_vcard_odbc.erl:421 -msgid "ejabberd vCard module" -msgstr "ejabberd vCard-modulo" - -#: mod_vcard.erl:469 mod_vcard_ldap.erl:551 mod_vcard_odbc.erl:445 -msgid "Search Results for " -msgstr "Serĉ-rezultoj de " - -#: mod_vcard_ldap.erl:462 -msgid "Fill in fields to search for any matching Jabber User" -msgstr "Kompletigu la formon por serĉi rekonata Jabber-uzanto" - -#: web/ejabberd_web_admin.erl:193 web/ejabberd_web_admin.erl:203 -#: web/ejabberd_web_admin.erl:219 web/ejabberd_web_admin.erl:229 -msgid "Unauthorized" -msgstr "Nepermesita" - -#: web/ejabberd_web_admin.erl:286 web/ejabberd_web_admin.erl:303 -msgid "ejabberd Web Admin" -msgstr "ejabberd Teksaĵa Administro" - -#: web/ejabberd_web_admin.erl:769 web/ejabberd_web_admin.erl:780 -msgid "Administration" -msgstr "Administro" - -#: web/ejabberd_web_admin.erl:878 web/ejabberd_web_admin.erl:982 -msgid "Raw" -msgstr "Kruda" - -#: web/ejabberd_web_admin.erl:1017 -msgid "~s access rule configuration" -msgstr "Agordo de atingo-reguloj de ~s" - -#: web/ejabberd_web_admin.erl:1035 -msgid "Virtual Hosts" -msgstr "Virtual-gastigoj" - -#: web/ejabberd_web_admin.erl:1043 web/ejabberd_web_admin.erl:1050 -msgid "Users" -msgstr "Uzantoj" - -#: web/ejabberd_web_admin.erl:1078 -msgid "Users Last Activity" -msgstr "Lasta aktiveco de uzanto" - -#: web/ejabberd_web_admin.erl:1080 -msgid "Period: " -msgstr "Periodo: " - -#: web/ejabberd_web_admin.erl:1090 -msgid "Last month" -msgstr "Lasta monato" - -#: web/ejabberd_web_admin.erl:1091 -msgid "Last year" -msgstr "Lasta jaro" - -#: web/ejabberd_web_admin.erl:1092 -msgid "All activity" -msgstr "Ĉiu aktiveco" - -#: web/ejabberd_web_admin.erl:1094 -msgid "Show Ordinary Table" -msgstr "Montru ordinaran tabelon" - -#: web/ejabberd_web_admin.erl:1096 -msgid "Show Integral Table" -msgstr "Montru integran tabelon" - -#: web/ejabberd_web_admin.erl:1105 web/ejabberd_web_admin.erl:1939 -msgid "Statistics" -msgstr "Statistikoj" - -#: web/ejabberd_web_admin.erl:1117 -msgid "Not Found" -msgstr "Ne trovita" - -#: web/ejabberd_web_admin.erl:1134 -msgid "Node not found" -msgstr "Nodo ne trovita" - -#: web/ejabberd_web_admin.erl:1460 -msgid "Host" -msgstr "Gastigo" - -#: web/ejabberd_web_admin.erl:1461 -msgid "Registered Users" -msgstr "Registritaj uzantoj" - -#: web/ejabberd_web_admin.erl:1573 -msgid "Offline Messages" -msgstr "Liverontaj mesaĝoj" - -#: web/ejabberd_web_admin.erl:1574 web/ejabberd_web_admin.erl:1767 -msgid "Last Activity" -msgstr "Lasta aktiveco" - -#: web/ejabberd_web_admin.erl:1659 web/ejabberd_web_admin.erl:1675 -msgid "Registered Users:" -msgstr "Registritaj uzantoj:" - -#: web/ejabberd_web_admin.erl:1661 web/ejabberd_web_admin.erl:1677 -#: web/ejabberd_web_admin.erl:2192 -msgid "Online Users:" -msgstr "Konektataj uzantoj:" - -#: web/ejabberd_web_admin.erl:1663 -msgid "Outgoing s2s Connections:" -msgstr "Elirantaj s-al-s-konektoj:" - -#: web/ejabberd_web_admin.erl:1665 -msgid "Outgoing s2s Servers:" -msgstr "Elirantaj s-al-s-serviloj" - -#: web/ejabberd_web_admin.erl:1734 web/mod_register_web.erl:175 -#: web/mod_register_web.erl:372 web/mod_register_web.erl:381 -#: web/mod_register_web.erl:411 -msgid "Change Password" -msgstr "Ŝanĝu pasvorton" - -#: web/ejabberd_web_admin.erl:1758 -msgid "User " -msgstr "Uzanto " - -#: web/ejabberd_web_admin.erl:1765 -msgid "Connected Resources:" -msgstr "Konektataj risurcoj:" - -#: web/ejabberd_web_admin.erl:1766 web/mod_register_web.erl:227 -#: web/mod_register_web.erl:519 -msgid "Password:" -msgstr "Pasvorto:" - -#: web/ejabberd_web_admin.erl:1828 -msgid "No Data" -msgstr "Neniu datumo" - -#: web/ejabberd_web_admin.erl:1906 -msgid "Nodes" -msgstr "Nodoj" - -#: web/ejabberd_web_admin.erl:1929 web/ejabberd_web_admin.erl:1951 -msgid "Node " -msgstr "Nodo " - -#: web/ejabberd_web_admin.erl:1938 -msgid "Listened Ports" -msgstr "Atentataj pordoj" - -#: web/ejabberd_web_admin.erl:1940 web/ejabberd_web_admin.erl:2260 -#: web/ejabberd_web_admin.erl:2448 -msgid "Update" -msgstr "Ĝisdatigu" - -#: web/ejabberd_web_admin.erl:1943 web/ejabberd_web_admin.erl:2569 -msgid "Restart" -msgstr "Restartu" - -#: web/ejabberd_web_admin.erl:1945 web/ejabberd_web_admin.erl:2571 -msgid "Stop" -msgstr "Haltigu" - -#: web/ejabberd_web_admin.erl:1959 -msgid "RPC Call Error" -msgstr "Eraro de RPC-alvoko" - -#: web/ejabberd_web_admin.erl:2000 -msgid "Database Tables at " -msgstr "Datumbaz-tabeloj je " - -#: web/ejabberd_web_admin.erl:2007 -msgid "Storage Type" -msgstr "Konserv-tipo" - -#: web/ejabberd_web_admin.erl:2008 -msgid "Elements" -msgstr "Eroj" - -#: web/ejabberd_web_admin.erl:2009 -msgid "Memory" -msgstr "Memoro" - -#: web/ejabberd_web_admin.erl:2032 web/ejabberd_web_admin.erl:2137 -msgid "Error" -msgstr "Eraro" - -#: web/ejabberd_web_admin.erl:2034 -msgid "Backup of " -msgstr "Sekurkopio de " - -#: web/ejabberd_web_admin.erl:2036 -msgid "" -"Please note that these options will only backup the builtin Mnesia database. " -"If you are using the ODBC module, you also need to backup your SQL database " -"separately." -msgstr "" -"Rimarku ke ĉi tiuj elektebloj nur sekurkopias la propran Mnesia-datumbazon. " -"Se vi uzas la ODBC-modulon, vi ankaŭ devas sekurkopii tiujn SQL-datumbazoj " -"aparte." - -#: web/ejabberd_web_admin.erl:2041 -msgid "Store binary backup:" -msgstr "Konservu duuman sekurkopion:" - -#: web/ejabberd_web_admin.erl:2045 web/ejabberd_web_admin.erl:2052 -#: web/ejabberd_web_admin.erl:2060 web/ejabberd_web_admin.erl:2067 -#: web/ejabberd_web_admin.erl:2074 web/ejabberd_web_admin.erl:2081 -#: web/ejabberd_web_admin.erl:2088 web/ejabberd_web_admin.erl:2096 -#: web/ejabberd_web_admin.erl:2103 web/ejabberd_web_admin.erl:2110 -msgid "OK" -msgstr "Bone" - -#: web/ejabberd_web_admin.erl:2048 -msgid "Restore binary backup immediately:" -msgstr "Restaŭrigu duuman sekurkopion tuj:" - -#: web/ejabberd_web_admin.erl:2056 -msgid "" -"Restore binary backup after next ejabberd restart (requires less memory):" -msgstr "Restaŭrigu duuman sekurkopion post sekvonta ejabberd-restarto" - -#: web/ejabberd_web_admin.erl:2063 -msgid "Store plain text backup:" -msgstr "Skribu sekurkopion en plata tekstdosiero" - -#: web/ejabberd_web_admin.erl:2070 -msgid "Restore plain text backup immediately:" -msgstr "Restaŭrigu sekurkopion el plata tekstdosiero tuj" - -#: web/ejabberd_web_admin.erl:2077 -msgid "Import users data from a PIEFXIS file (XEP-0227):" -msgstr "Importu uzanto-datumojn de PIEFXIS dosiero (XEP-0227):" - -#: web/ejabberd_web_admin.erl:2084 -msgid "Export data of all users in the server to PIEFXIS files (XEP-0227):" -msgstr "" -"Eksportu datumojn de ĉiuj uzantoj en servilo al PIEFXIS dosieroj (XEP-0227):" - -#: web/ejabberd_web_admin.erl:2091 -msgid "Export data of users in a host to PIEFXIS files (XEP-0227):" -msgstr "Eksportu datumoj de uzantoj en gastigo al PIEFXIS dosieroj (XEP-0227):" - -#: web/ejabberd_web_admin.erl:2099 -msgid "Import user data from jabberd14 spool file:" -msgstr "Importu uzantojn de jabberd14-uzantdosieroj" - -#: web/ejabberd_web_admin.erl:2106 -msgid "Import users data from jabberd14 spool directory:" -msgstr "Importu uzantojn de jabberd14-uzantdosieroj" - -#: web/ejabberd_web_admin.erl:2132 -msgid "Listened Ports at " -msgstr "Atentataj pordoj je " - -#: web/ejabberd_web_admin.erl:2157 -msgid "Modules at " -msgstr "Moduloj je " - -#: web/ejabberd_web_admin.erl:2183 -msgid "Statistics of ~p" -msgstr "Statistikoj de ~p" - -#: web/ejabberd_web_admin.erl:2186 -msgid "Uptime:" -msgstr "Daŭro de funkciado" - -#: web/ejabberd_web_admin.erl:2189 -msgid "CPU Time:" -msgstr "CPU-tempo" - -#: web/ejabberd_web_admin.erl:2195 -msgid "Transactions Committed:" -msgstr "Transakcioj enmetitaj" - -#: web/ejabberd_web_admin.erl:2198 -msgid "Transactions Aborted:" -msgstr "Transakcioj nuligitaj" - -#: web/ejabberd_web_admin.erl:2201 -msgid "Transactions Restarted:" -msgstr "Transakcioj restartitaj" - -#: web/ejabberd_web_admin.erl:2204 -msgid "Transactions Logged:" -msgstr "Transakcioj protokolitaj" - -#: web/ejabberd_web_admin.erl:2246 -msgid "Update " -msgstr "Ĝisdatigu " - -#: web/ejabberd_web_admin.erl:2254 -msgid "Update plan" -msgstr "Ĝisdatigo-plano" - -#: web/ejabberd_web_admin.erl:2255 -msgid "Modified modules" -msgstr "Ĝisdatigitaj moduloj" - -#: web/ejabberd_web_admin.erl:2256 -msgid "Update script" -msgstr "Ĝisdatigo-skripto" - -#: web/ejabberd_web_admin.erl:2257 -msgid "Low level update script" -msgstr "Bazanivela ĝisdatigo-skripto" - -#: web/ejabberd_web_admin.erl:2258 -msgid "Script check" -msgstr "Skript-kontrolo" - -#: web/ejabberd_web_admin.erl:2426 -msgid "Port" -msgstr "Pordo" - -#: web/ejabberd_web_admin.erl:2427 -msgid "IP" -msgstr "IP" - -#: web/ejabberd_web_admin.erl:2428 -msgid "Protocol" -msgstr "Protokolo" - -#: web/ejabberd_web_admin.erl:2429 web/ejabberd_web_admin.erl:2556 -msgid "Module" -msgstr "Modulo" - -#: web/ejabberd_web_admin.erl:2430 web/ejabberd_web_admin.erl:2557 -msgid "Options" -msgstr "Elektebloj" - -#: web/ejabberd_web_admin.erl:2450 -msgid "Delete" -msgstr "Forigu" - -#: web/ejabberd_web_admin.erl:2579 -msgid "Start" -msgstr "Startu" - -#: web/mod_register_web.erl:104 -msgid "Your Jabber account was successfully created." -msgstr "Via Ĵabber-konto estis sukcese kreata." - -#: web/mod_register_web.erl:107 -msgid "There was an error creating the account: " -msgstr "Estis eraro dum kreado de la konto:" - -#: web/mod_register_web.erl:115 -msgid "Your Jabber account was successfully deleted." -msgstr "Via Ĵabber-konto estas sukcese forigita." - -#: web/mod_register_web.erl:118 -msgid "There was an error deleting the account: " -msgstr "Estis eraro dum forigado de la konto:" - -#: web/mod_register_web.erl:128 -msgid "The password of your Jabber account was successfully changed." -msgstr "La pasvorto de via Ĵabber-konto estas sukcese ŝanĝata." - -#: web/mod_register_web.erl:131 -msgid "There was an error changing the password: " -msgstr "Estis eraro dum ŝanĝi de la pasvortro:" - -#: web/mod_register_web.erl:163 web/mod_register_web.erl:172 -msgid "Jabber Account Registration" -msgstr "Ĵabber-konto registrado" - -#: web/mod_register_web.erl:174 web/mod_register_web.erl:192 -#: web/mod_register_web.erl:201 -msgid "Register a Jabber account" -msgstr "Registru Ĵabber-konton" - -#: web/mod_register_web.erl:176 web/mod_register_web.erl:493 -#: web/mod_register_web.erl:502 -msgid "Unregister a Jabber account" -msgstr "Malregistru Ĵabber-konton" - -#: web/mod_register_web.erl:203 -msgid "" -"This page allows to create a Jabber account in this Jabber server. Your JID " -"(Jabber IDentifier) will be of the form: username@server. Please read " -"carefully the instructions to fill correctly the fields." -msgstr "" -"Jena paĝo ebligas kreadon de Ĵabber-konto je ĉi-Ĵabber-servilo. Via JID " -"(Ĵabber-IDentigilo) estos ĉi-tiel: uzantnomo@servilo. Bonvolu legu bone la " -"instrukciojn por korekta enmetigo de la kampoj. " - -#: web/mod_register_web.erl:212 web/mod_register_web.erl:386 -#: web/mod_register_web.erl:509 -msgid "Username:" -msgstr "Uzantnomo" - -#: web/mod_register_web.erl:217 -msgid "This is case insensitive: macbeth is the same that MacBeth and Macbeth." -msgstr "Uskleco ne signifas: macbeth estas la sama ol MacBeth kaj Macbeth." - -#: web/mod_register_web.erl:218 -msgid "Characters not allowed:" -msgstr "Karaktroj ne permesata:" - -#: web/mod_register_web.erl:222 web/mod_register_web.erl:391 -#: web/mod_register_web.erl:514 -msgid "Server:" -msgstr "Servilo:" - -#: web/mod_register_web.erl:232 -msgid "" -"Don't tell your password to anybody, not even the administrators of the " -"Jabber server." -msgstr "" -"Ne donu vian pasvorton al iun ajn, eĉ ne al la administrantoj de la Ĵabber-" -"servilo." - -#: web/mod_register_web.erl:234 -msgid "You can later change your password using a Jabber client." -msgstr "Poste vi povas ŝanĝi vian pasvorton per Ĵabber-kliento." - -#: web/mod_register_web.erl:235 -msgid "" -"Some Jabber clients can store your password in your computer. Use that " -"feature only if you trust your computer is safe." -msgstr "" -"Kelkaj Ĵabber-klientoj povas memori vian pasvorton je via komputilo. Nur uzu " -"tiun eblon se vi fidas ke via komputilo estas sekura." - -#: web/mod_register_web.erl:237 -msgid "" -"Memorize your password, or write it in a paper placed in a safe place. In " -"Jabber there isn't an automated way to recover your password if you forget " -"it." -msgstr "" -"Memoru vian pasvorton, aŭ skribu ĝin sur papero formetata je sekura loko. Je " -"Ĵabber ne ekzistas aŭtomata metodo por reakiri vian pasvorton se vi forgesas " -"ĝin." - -#: web/mod_register_web.erl:242 web/mod_register_web.erl:406 -msgid "Password Verification:" -msgstr "Pasvortkontrolo:" - -#: web/mod_register_web.erl:250 -msgid "Register" -msgstr "Registru" - -#: web/mod_register_web.erl:396 -msgid "Old Password:" -msgstr "Malnova Pasvorto:" - -#: web/mod_register_web.erl:401 -msgid "New Password:" -msgstr "Nova Pasvorto:" - -#: web/mod_register_web.erl:504 -msgid "This page allows to unregister a Jabber account in this Jabber server." -msgstr "Jena pagxo ebligas malregistri Jxabber-konton je ĉi-servilo." - -#: web/mod_register_web.erl:524 -msgid "Unregister" -msgstr "Malregistru" - -#, fuzzy -#~ msgid "Captcha test failed" -#~ msgstr "La aŭtomata Turingtesto estas ĝusta" - -#~ msgid "Encodings" -#~ msgstr "Enkodigoj" - -#~ msgid "(Raw)" -#~ msgstr "(Kruda)" - -#~ msgid "Specified nickname is already registered" -#~ msgstr "Donita kaŝnomo jam estas registrita" - -#~ msgid "Size" -#~ msgstr "Grando" - -#~ msgid "You must fill in field \"nick\" in the form" -#~ msgstr "Vi devas enmeti kampon \"kaŝnomo\"" diff --git a/priv/msgs/es.msg b/priv/msgs/es.msg index 904a0de8a..a914a43a1 100644 --- a/priv/msgs/es.msg +++ b/priv/msgs/es.msg @@ -1,20 +1,30 @@ -{"Access Configuration","Configuración de accesos"}. -{"Access Control List Configuration","Configuración de la Lista de Control de Acceso"}. -{"Access control lists","Listas de Control de Acceso"}. -{"Access Control Lists","Listas de Control de Acceso"}. -{"Access denied by service policy","Acceso denegado por la política del servicio"}. -{"Access rules","Reglas de acceso"}. -{"Access Rules","Reglas de Acceso"}. -{"Action on user","Acción en el usuario"}. -{"Add Jabber ID","Añadir Jabber ID"}. -{"Add New","Añadir nuevo"}. -{"Add User","Añadir usuario"}. -{"Administration","Administración"}. -{"Administration of ","Administración de "}. -{"Administrator privileges required","Se necesita privilegios de administrador"}. +%% Generated automatically +%% DO NOT EDIT: run `make translations` instead +%% To improve translations please read: +%% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ + +{" (Add * to the end of field to match substring)"," (Añade * al final del campo para buscar subcadenas)"}. +{" has set the subject to: "," ha puesto el asunto: "}. +{"# participants","# participantes"}. +{"A description of the node","Una descripción del nodo"}. {"A friendly name for the node","Un nombre sencillo para el nodo"}. +{"A password is required to enter this room","(Añade * al final del campo para buscar subcadenas)"}. +{"A Web Page","Una página web"}. +{"Accept","Aceptar"}. +{"Access denied by service policy","Acceso denegado por la política del servicio"}. +{"Access model","Modelo de Acceso"}. +{"Account doesn't exist","La cuenta no existe"}. +{"Action on user","Acción en el usuario"}. +{"Add a hat to a user","Añade un sombrero a un usuario"}. +{"Add User","Añadir usuario"}. +{"Administration of ","Administración de "}. +{"Administration","Administración"}. +{"Administrator privileges required","Se necesita privilegios de administrador"}. {"All activity","Toda la actividad"}. +{"All Users","Todos los usuarios"}. +{"Allow subscription","Permitir la subscripción"}. {"Allow this Jabber ID to subscribe to this pubsub node?","¿Deseas permitir a este Jabber ID que se subscriba a este nodo PubSub?"}. +{"Allow this person to register with the room?","¿Permitir a esta persona que se registre en la sala?"}. {"Allow users to change the subject","Permitir a los usuarios cambiar el asunto"}. {"Allow users to query other users","Permitir a los usuarios consultar a otros usuarios"}. {"Allow users to send invites","Permitir a los usuarios enviar invitaciones"}. @@ -23,21 +33,49 @@ {"Allow visitors to send private messages to","Permitir a los visitantes enviar mensajes privados a"}. {"Allow visitors to send status text in presence updates","Permitir a los visitantes enviar texto de estado en las actualizaciones de presencia"}. {"Allow visitors to send voice requests","Permitir a los visitantes enviar peticiones de voz"}. -{"All Users","Todos los usuarios"}. +{"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 grupo LDAP asociado que define la membresía a la sala; este debería ser un Nombre Distinguido de LDAP, de acuerdo con una definición de grupo específica de la implementación o de esta instalación."}. {"Announcements","Anuncios"}. -{"anyone","cualquiera"}. -{"A password is required to enter this room","Se necesita contraseña para entrar en esta sala"}. -{"April","abril"}. -{"August","agosto"}. -{"Backup","Guardar copia de seguridad"}. +{"Answer associated with a picture","Respuesta asociada con una imagen"}. +{"Answer associated with a video","Respuesta asociada con un video"}. +{"Answer associated with speech","Respuesta asociada con un audio"}. +{"Answer to a question","Responde a una pregunta"}. +{"Anyone in the specified roster group(s) may subscribe and retrieve items","Cualquiera que esté en el grupo(s) de contactos especificado puede suscribirse y recibir elementos"}. +{"Anyone may associate leaf nodes with the collection","Cualquiera puede asociar nodos hoja con la colección"}. +{"Anyone may publish","Cualquiera puede publicar"}. +{"Anyone may subscribe and retrieve items","Cualquiera puede suscribirse y recibir elementos"}. +{"Anyone with a presence subscription of both or from may subscribe and retrieve items","Cualquiera con una suscripción a la presencia de 'ambos' o 'de' puede suscribirse y recibir elementos"}. +{"Anyone with Voice","Cualquiera con Voz"}. +{"Anyone","Cualquiera"}. +{"API Commands","Comandos API"}. +{"April","Abril"}. +{"Arguments","Argumentos"}. +{"Attribute 'channel' is required for this request","El atributo 'channel' es necesario para esta petición"}. +{"Attribute 'id' is mandatory for MIX messages","El atributo 'id' es necesario para mensajes MIX"}. +{"Attribute 'jid' is not allowed here","El atributo 'jid' no está permitido aqui"}. +{"Attribute 'node' is not allowed here","El atributo 'node' no está permitido aqui"}. +{"Attribute 'to' of stanza that triggered challenge","Atributo 'to' del paquete que disparó la comprobación"}. +{"August","Agosto"}. +{"Automatic node creation is not enabled","La creación automática de nodo no está activada"}. {"Backup Management","Gestión de copia de seguridad"}. -{"Backup of ","Copia de seguridad de "}. +{"Backup of ~p","Copia de seguridad de ~p"}. {"Backup to File at ","Guardar copia de seguridad en fichero en "}. +{"Backup","Guardar copia de seguridad"}. {"Bad format","Mal formato"}. {"Birthday","Cumpleaños"}. +{"Both the username and the resource are required","Se requiere tanto el nombre de usuario como el recurso"}. +{"Bytestream already activated","Bytestream ya está activado"}. +{"Cannot remove active list","No se puede borrar la lista activa"}. +{"Cannot remove default list","No se puede borrar la lista por defecto"}. {"CAPTCHA web page","Página web de CAPTCHA"}. +{"Challenge ID","ID de la comprobación"}. {"Change Password","Cambiar contraseña"}. {"Change User Password","Cambiar contraseña de usuario"}. +{"Changing password is not allowed","No está permitido cambiar la contraseña"}. +{"Changing role/affiliation is not allowed","No está permitido cambiar el rol/afiliación"}. +{"Channel already exists","El canal ya existe"}. +{"Channel does not exist","El canal no existe"}. +{"Channel JID","JID del Canal"}. +{"Channels","Canales"}. {"Characters not allowed:","Caracteres no permitidos:"}. {"Chatroom configuration modified","Configuración de la sala modificada"}. {"Chatroom is created","Se ha creado la sala"}. @@ -46,90 +84,101 @@ {"Chatroom is stopped","Se ha detenido la sala"}. {"Chatrooms","Salas de charla"}. {"Choose a username and password to register with this server","Escoge un nombre de usuario y contraseña para registrarte en este servidor"}. -{"Choose modules to stop","Selecciona módulos a detener"}. {"Choose storage type of tables","Selecciona tipo de almacenamiento de las tablas"}. {"Choose whether to approve this entity's subscription.","Decidir si aprobar la subscripción de esta entidad."}. {"City","Ciudad"}. +{"Client acknowledged more stanzas than sent by server","El cliente ha reconocido más paquetes de los que el servidor ha enviado"}. +{"Clustering","Clustering"}. {"Commands","Comandos"}. {"Conference room does not exist","La sala de conferencias no existe"}. -{"Configuration","Configuración"}. {"Configuration of room ~s","Configuración para la sala ~s"}. -{"Connected Resources:","Recursos conectados:"}. -{"Connections parameters","Parámetros de conexiones"}. +{"Configuration","Configuración"}. +{"Contact Addresses (normally, room owner or owners)","Direcciones de contacto (normalmente la del dueño o dueños de la sala)"}. {"Country","País"}. -{"CPU Time:","Tiempo consumido de CPU:"}. -{"Database","Base de datos"}. -{"Database Tables at ","Tablas de la base de datos en "}. +{"Current Discussion Topic","Tema de discusión actual"}. +{"Database failure","Error en la base de datos"}. {"Database Tables Configuration at ","Configuración de tablas de la base de datos en "}. -{"December","diciembre"}. +{"Database","Base de datos"}. +{"December","Diciembre"}. {"Default users as participants","Los usuarios son participantes por defecto"}. -{"Delete","Eliminar"}. -{"Delete message of the day","Borrar mensaje del dia"}. {"Delete message of the day on all hosts","Borrar el mensaje del día en todos los dominios"}. -{"Delete Selected","Eliminar los seleccionados"}. +{"Delete message of the day","Borrar mensaje del dia"}. {"Delete User","Borrar usuario"}. {"Deliver event notifications","Entregar notificaciones de eventos"}. {"Deliver payloads with event notifications","Enviar contenidos junto con las notificaciones de eventos"}. -{"Description:","Descripción:"}. {"Disc only copy","Copia en disco solamente"}. -{"Displayed Groups:","Mostrar grupos:"}. -{"Don't tell your password to anybody, not even the administrators of the Jabber server.","No le digas tu contraseña a nadie, ni siquiera a los administradores del servidor Jabber."}. +{"Don't tell your password to anybody, not even the administrators of the XMPP server.","No le digas tu contraseña a nadie, ni siquiera a los administradores del servidor XMPP."}. {"Dump Backup to Text File at ","Exporta copia de seguridad a fichero de texto en "}. {"Dump to Text File","Exportar a fichero de texto"}. +{"Duplicated groups are not allowed by RFC6121","Los grupos duplicados no están permitidos por RFC6121"}. +{"Dynamically specify a replyto of the item publisher","Especificar dinámicamente como dirección de respuesta al publicador del elemento"}. {"Edit Properties","Editar propiedades"}. {"Either approve or decline the voice request.","Aprueba o rechaza la petición de voz."}. -{"ejabberd IRC module","Módulo de IRC para ejabberd"}. +{"ejabberd HTTP Upload service","Servicio HTTP Upload de ejabberd"}. {"ejabberd MUC module","Módulo de MUC para ejabberd"}. +{"ejabberd Multicast service","Servicio Multicast de ejabberd"}. {"ejabberd Publish-Subscribe module","Módulo de Publicar-Subscribir de ejabberd"}. {"ejabberd SOCKS5 Bytestreams module","Módulo SOCKS5 Bytestreams para ejabberd"}. {"ejabberd vCard module","Módulo vCard para ejabberd"}. {"ejabberd Web Admin","ejabberd Web Admin"}. -{"Elements","Elementos"}. -{"Email","correo"}. +{"ejabberd","ejabberd"}. +{"Email Address","Dirección de correo electrónico"}. +{"Email","Correo electrónico"}. +{"Enable hats","Activar sombreros"}. {"Enable logging","Guardar históricos"}. -{"Encoding for server ~b","Codificación del servidor ~b"}. +{"Enable message archiving","Activar el almacenamiento de mensajes"}. +{"Enabling push without 'node' attribute is not supported","No está soportado activar Push sin el atributo 'node'"}. {"End User Session","Cerrar sesión de usuario"}. -{"Enter list of {Module, [Options]}","Introduce lista de {módulo, [opciones]}"}. {"Enter nickname you want to register","Introduce el apodo que quieras registrar"}. {"Enter path to backup file","Introduce ruta al fichero de copia de seguridad"}. {"Enter path to jabberd14 spool dir","Introduce la ruta al directorio de jabberd14 spools"}. {"Enter path to jabberd14 spool file","Introduce ruta al fichero jabberd14 spool"}. {"Enter path to text file","Introduce ruta al fichero de texto"}. {"Enter the text you see","Teclea el texto que ves"}. -{"Enter username and encodings you wish to use for connecting to IRC servers. Press 'Next' to get more fields to fill in. Press 'Complete' to save settings.","Introduce el nombre de usuario y codificaciones de carácteres que quieras usar al conectar en los servidores de IRC. Pulsa Siguiente para conseguir más campos en el formulario. Pulsa Completar para guardar las opciones."}. -{"Enter username, encodings, ports and passwords you wish to use for connecting to IRC servers","Introduce el nombre de usuario, codificaciones de carácteres, puertos y contraseñas que quieras usar al conectar en los servidores de IRC"}. -{"Erlang Jabber Server","Servidor Jabber en Erlang"}. -{"Error","Error"}. -{"Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef.net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}].","Ejemplo: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef.net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]."}. +{"Erlang XMPP Server","Servidor XMPP en Erlang"}. {"Exclude Jabber IDs from CAPTCHA challenge","Excluir Jabber IDs de las pruebas de CAPTCHA"}. +{"Export all tables as SQL queries to a file:","Exportar todas las tablas a un fichero SQL:"}. {"Export data of all users in the server to PIEFXIS files (XEP-0227):","Exportar datos de todos los usuarios del servidor a ficheros PIEFXIS (XEP-0227):"}. {"Export data of users in a host to PIEFXIS files (XEP-0227):","Exportar datos de los usuarios de un dominio a ficheros PIEFXIS (XEP-0227):"}. +{"External component failure","Fallo en el componente externo"}. +{"External component timeout","Demasiado retraso (timeout) en el componente externo"}. +{"Failed to activate bytestream","Falló la activación de bytestream"}. {"Failed to extract JID from your voice request approval","Fallo al extraer el Jabber ID de tu aprobación de petición de voz"}. +{"Failed to map delegated namespace to external component","Falló el mapeo de espacio de nombres delegado al componente externo"}. +{"Failed to parse HTTP response","Falló la comprensión de la respuesta HTTP"}. +{"Failed to process option '~s'","Falló el procesado de la opción '~s'"}. {"Family Name","Apellido"}. -{"February","febrero"}. -{"Fill in fields to search for any matching Jabber User","Rellena campos para buscar usuarios Jabber que concuerden"}. -{"Fill in the form to search for any matching Jabber User (Add * to the end of field to match substring)","Rellena el formulario para buscar usuarios Jabber. Añade * al final de un campo para buscar subcadenas."}. -{"Friday","viernes"}. -{"From","De"}. -{"From ~s","De ~s"}. +{"FAQ Entry","Apunte en la FAQ"}. +{"February","Febrero"}. +{"File larger than ~w bytes","El fichero es más grande que ~w bytes"}. +{"Fill in the form to search for any matching XMPP User","Rellena campos para buscar usuarios XMPP que concuerden"}. +{"Friday","Viernes"}. +{"From ~ts","De ~ts"}. +{"Full List of Room Admins","Lista completa de administradores de la sala"}. +{"Full List of Room Owners","Lista completa de dueños de la sala"}. {"Full Name","Nombre completo"}. +{"Get List of Online Users","Ver lista de usuarios conectados"}. +{"Get List of Registered Users","Ver lista de usuarios registrados"}. {"Get Number of Online Users","Ver número de usuarios conectados"}. {"Get Number of Registered Users","Ver número de usuarios registrados"}. +{"Get Pending","Obtener pendientes"}. {"Get User Last Login Time","Ver fecha de la última conexión de usuario"}. -{"Get User Password","Ver contraseña de usuario"}. {"Get User Statistics","Ver estadísticas de usuario"}. +{"Given Name","Nombre de pila"}. {"Grant voice to this person?","¿Conceder voz a esta persona?"}. -{"Group ","Grupo "}. -{"Groups","Grupos"}. {"has been banned","ha sido bloqueado"}. -{"has been kicked because of an affiliation change","ha sido expulsado por un cambio de su afiliación"}. {"has been kicked because of a system shutdown","ha sido expulsado porque el sistema se va a detener"}. +{"has been kicked because of an affiliation change","ha sido expulsado por un cambio de su afiliación"}. {"has been kicked because the room has been changed to members-only","ha sido expulsado porque la sala es ahora solo para miembros"}. {"has been kicked","ha sido expulsado"}. -{" has set the subject to: "," ha puesto el asunto: "}. -{"Host","Dominio"}. +{"Hash of the vCard-temp avatar of this room","Hash del avatar vCard-temp de esta sala"}. +{"Hat title","Título del sombrero"}. +{"Hat URI","Dirección del sombrero"}. +{"Hats limit exceeded","Se ha excedido el límite de sombreros"}. +{"Host unknown","Dominio desconocido"}. +{"HTTP File Upload","Subir fichero por HTTP"}. +{"Idle connection","Conexión sin uso"}. {"If you don't see the CAPTCHA image here, visit the web page.","Si no ves la imagen CAPTCHA aquí, visita la página web."}. -{"If you want to specify different ports, passwords, encodings for IRC servers, fill this list with values in format '{\"irc server\", \"encoding\", port, \"password\"}'. By default this service use \"~s\" encoding, port ~p, empty password.","Si quieres especificar distintos codificaciones de carácteres, contraseñas o puertos para cada servidor IRC rellena esta lista con valores en el formato '{\"servidor irc\", \"codificación\", \"puerto\", \"contrasela\"}'. Este servicio usa por defecto la codificación \"~s\", puerto ~p, sin contraseña."}. {"Import Directory","Importar directorio"}. {"Import File","Importar fichero"}. {"Import user data from jabberd14 spool file:","Importar usuario de fichero spool de jabberd14:"}. @@ -138,41 +187,48 @@ {"Import users data from jabberd14 spool directory:","Importar usuarios del directorio spool de jabberd14:"}. {"Import Users from Dir at ","Importar usuarios desde el directorio en "}. {"Import Users From jabberd14 Spool Files","Importar usuarios de ficheros spool de jabberd-1.4"}. +{"Improper domain part of 'from' attribute","Parte de dominio impropia en el atributo 'from'"}. {"Improper message type","Tipo de mensaje incorrecto"}. +{"Incorrect CAPTCHA submit","El CAPTCHA proporcionado es incorrecto"}. +{"Incorrect data form","Formulario de datos incorrecto"}. {"Incorrect password","Contraseña incorrecta"}. -{"Invalid affiliation: ~s","Afiliación no válida: ~s"}. -{"Invalid role: ~s","Rol no válido: ~s"}. +{"Incorrect value of 'action' attribute","Valor incorrecto del atributo 'action'"}. +{"Incorrect value of 'action' in data form","Valor incorrecto de 'action' en el formulario de datos"}. +{"Incorrect value of 'path' in data form","Valor incorrecto de 'path' en el formulario de datos"}. +{"Installed Modules:","Módulos Instalados:"}. +{"Install","Instalar"}. +{"Insufficient privilege","Privilegio insuficiente"}. +{"Internal server error","Error interno en el servidor"}. +{"Invalid 'from' attribute in forwarded message","Atributo 'from' no válido en el mensaje reenviado"}. +{"Invalid node name","Nombre de nodo no válido"}. +{"Invalid 'previd' value","Valor de 'previd' no válido"}. +{"Invitations are not allowed in this conference","Las invitaciones no están permitidas en esta sala"}. {"IP addresses","Direcciones IP"}. -{"IP","IP"}. -{"IRC channel (don't put the first #)","Canal IRC (no pongas el # del principio)"}. -{"IRC server","Servidor IRC"}. -{"IRC settings","Opciones de IRC"}. -{"IRC Transport","Transporte de IRC"}. -{"IRC username","Nombre de usuario en IRC"}. -{"IRC Username","Nombre de usuario en IRC"}. {"is now known as","se cambia el nombre a"}. -{"It is not allowed to send private messages","No está permitido enviar mensajes privados"}. +{"It is not allowed to send error messages to the room. The participant (~s) has sent an error message (~s) and got kicked from the room","No está permitido enviar mensajes de error a la sala. Este participante (~s) ha enviado un mensaje de error (~s) y fue expulsado de la sala"}. {"It is not allowed to send private messages of type \"groupchat\"","No está permitido enviar mensajes privados del tipo \"groupchat\""}. {"It is not allowed to send private messages to the conference","Impedir el envio de mensajes privados a la sala"}. -{"Jabber Account Registration","Registro de Cuenta Jabber"}. {"Jabber ID","Jabber ID"}. -{"Jabber ID ~s is invalid","El Jabber ID ~s no es válido"}. -{"January","enero"}. -{"Join IRC channel","Entrar en canal IRC"}. +{"January","Enero"}. +{"JID normalization denied by service policy","Se ha denegado la normalización del JID por política del servicio"}. +{"JID normalization failed","Ha fallado la normalización del JID"}. +{"Joined MIX channels of ~ts","Canales MIX unidos de ~ts"}. +{"Joined MIX channels:","Canales MIX unidos:"}. {"joins the room","entra en la sala"}. -{"Join the IRC channel here.","Entrar en el canal de IRC aquí"}. -{"Join the IRC channel in this Jabber ID: ~s","Entra en el canal de IRC en esta dirección Jabber: ~s"}. -{"July","julio"}. -{"June","junio"}. +{"July","Julio"}. +{"June","Junio"}. +{"Just created","Recién creada"}. {"Last Activity","Última actividad"}. {"Last login","Última conexión"}. +{"Last message","Último mensaje"}. {"Last month","Último mes"}. {"Last year","Último año"}. +{"Least significant bits of SHA-256 hash of text should equal hexadecimal label","Los bits menos significativos del hash SHA-256 del texto deberían ser iguales a la etiqueta hexadecimal"}. {"leaves the room","sale de la sala"}. -{"Listened Ports at ","Puertos de escucha en "}. -{"Listened Ports","Puertos de escucha"}. -{"List of modules to start","Lista de módulos a iniciar"}. -{"Low level update script","Script de actualización a bajo nivel"}. +{"List of users with hats","Lista de usuarios con sombreros"}. +{"List users with hats","Listar usuarios con sombreros"}. +{"Logged Out","Desconectad@"}. +{"Logging","Histórico de mensajes"}. {"Make participants list public","La lista de participantes es pública"}. {"Make room CAPTCHA protected","Proteger la sala con CAPTCHA"}. {"Make room members-only","Sala sólo para miembros"}. @@ -180,242 +236,395 @@ {"Make room password protected","Proteger la sala con contraseña"}. {"Make room persistent","Sala permanente"}. {"Make room public searchable","Sala públicamente visible"}. -{"March","marzo"}. -{"Maximum Number of Occupants","Número máximo de ocupantes"}. -{"Max # of items to persist","Máximo # de elementos que persisten"}. +{"Malformed username","Nombre de usuario mal formado"}. +{"MAM preference modification denied by service policy","Se ha denegado modificar la preferencia MAM por política del servicio"}. +{"March","Marzo"}. +{"Max # of items to persist, or `max` for no specific limit other than a server imposed maximum","Máximo # de elementos a persistir, o `max` para no especificar un límite, más que el máximo impuesto por el servidor"}. {"Max payload size in bytes","Máximo tamaño del contenido en bytes"}. -{"May","mayo"}. +{"Maximum file size","Tamaño máximo de fichero"}. +{"Maximum Number of History Messages Returned by Room","Máximo número de mensajes del historial devueltos por la sala"}. +{"Maximum number of items to persist","Máximo número de elementos que persisten"}. +{"Maximum Number of Occupants","Número máximo de ocupantes"}. +{"May","Mayo"}. {"Membership is required to enter this room","Necesitas ser miembro de esta sala para poder entrar"}. -{"Members:","Miembros:"}. -{"Memorize your password, or write it in a paper placed in a safe place. In Jabber there isn't an automated way to recover your password if you forget it.","Memoriza tu contraseña, o apúntala en un papel en un lugar seguro. En Jabber no hay un método automatizado para recuperar la contraseña si la olvidas."}. -{"Memory","Memoria"}. +{"Memorize your password, or write it in a paper placed in a safe place. In XMPP there isn't an automated way to recover your password if you forget it.","Memoriza tu contraseña, o apúntala en un papel en un lugar seguro. En XMPP no hay un método automatizado para recuperar la contraseña si la olvidas."}. +{"Mere Availability in XMPP (No Show Value)","Disponible en XMPP (sin valor de Mostrado)"}. {"Message body","Cuerpo del mensaje"}. +{"Message not found in forwarded payload","Mensaje no encontrado en el contenido reenviado"}. +{"Messages from strangers are rejected","Los mensajes de extraños son rechazados"}. +{"Messages of type headline","Mensajes de tipo titular"}. +{"Messages of type normal","Mensajes de tipo normal"}. {"Middle Name","Segundo nombre"}. {"Minimum interval between voice requests (in seconds)","Intervalo mínimo entre peticiones de voz (en segundos)"}. {"Moderator privileges required","Se necesita privilegios de moderador"}. -{"moderators only","solo moderadores"}. -{"Modified modules","Módulos modificados"}. -{"Module","Módulo"}. -{"Modules at ","Módulos en "}. -{"Modules","Módulos"}. -{"Monday","lunes"}. -{"Name:","Nombre:"}. +{"Moderator","Moderador"}. +{"Moderators Only","Solo moderadores"}. +{"Module failed to handle the query","El módulo falló al gestionar la petición"}. +{"Monday","Lunes"}. +{"Multicast","Multidifusión"}. +{"Multiple elements are not allowed by RFC6121","No se permiten múltiples elementos en RFC6121"}. +{"Multi-User Chat","Salas de Charla"}. {"Name","Nombre"}. +{"Natural Language for Room Discussions","Idioma natural en las charlas de la sala"}. +{"Natural-Language Room Name","Nombre de la sala en el idioma natural de la sala"}. +{"Neither 'jid' nor 'nick' attribute found","No se encontraron los atributos 'jid' ni 'nick'"}. +{"Neither 'role' nor 'affiliation' attribute found","No se encontraron los atributos 'role' ni 'affiliation'"}. {"Never","Nunca"}. {"New Password:","Nueva contraseña:"}. -{"Nickname","Apodo"}. +{"Nickname can't be empty","El apodo no puede estar vacío"}. {"Nickname Registration at ","Registro del apodo en "}. {"Nickname ~s does not exist in the room","El apodo ~s no existe en la sala"}. -{"nobody","nadie"}. +{"Nickname","Apodo"}. +{"No address elements found","No se encontraron elementos de dirección ('address')"}. +{"No addresses element found","No se encontró elemento de direcciones ('addresses')"}. +{"No 'affiliation' attribute found","No se encontró el atributo 'affiliation'"}. +{"No available resource found","No se encontró un recurso conectado"}. {"No body provided for announce message","No se ha proporcionado cuerpo de mensaje para el anuncio"}. +{"No child elements found","No se encontraron subelementos"}. +{"No data form found","No se encontró formulario de datos"}. {"No Data","Sin datos"}. -{"Node ID","Nodo ID"}. -{"Node ","Nodo "}. -{"Node not found","Nodo no encontrado"}. -{"Nodes","Nodos"}. +{"No features available","No hay características disponibles"}. +{"No element found","No se ha encontrado elemento "}. +{"No hook has processed this command","Ningún evento ha procesado este comando"}. +{"No info about last activity found","No hay información respeto a la última actividad"}. +{"No 'item' element found","No se encontró el elemento 'item'"}. +{"No items found in this query","No se han encontrado elementos en esta petición"}. {"No limit","Sin límite"}. +{"No module is handling this query","Ningún modulo está gestionando esta petición"}. +{"No node specified","No se ha especificado ningún nodo"}. +{"No 'password' found in data form","No se encontró 'password' en el formulario de datos"}. +{"No 'password' found in this query","No se encontró 'password' en esta petición"}. +{"No 'path' found in data form","No se encontró 'path' en este formulario de datos"}. +{"No pending subscriptions found","No se han encontrado suscripciones pendientes"}. +{"No privacy list with this name found","No se ha encontrado una lista de privacidad con este nombre"}. +{"No private data found in this query","No se ha encontrado ningún elemento de dato privado en esta petición"}. +{"No running node found","No se ha encontrado ningún nodo activo"}. +{"No services available","No hay servicios disponibles"}. +{"No statistics found for this item","No se han encontrado estadísticas para este elemento"}. +{"No 'to' attribute found in the invitation","No se encontró el atributo 'to' en la invitación"}. +{"Nobody","Nadie"}. +{"Node already exists","El nodo ya existe"}. +{"Node ID","Nodo ID"}. +{"Node index not found","No se ha encontrado índice de nodo"}. +{"Node not found","Nodo no encontrado"}. +{"Node ~p","Nodo ~p"}. +{"Node","Nodo"}. +{"Nodeprep has failed","Ha fallado el procesado del nombre de nodo (nodeprep)"}. +{"Nodes","Nodos"}. {"None","Ninguno"}. -{"No resource provided","No se ha proporcionado recurso"}. +{"Not allowed","No permitido"}. {"Not Found","No encontrado"}. +{"Not subscribed","No suscrito"}. {"Notify subscribers when items are removed from the node","Notificar subscriptores cuando los elementos se borran del nodo"}. {"Notify subscribers when the node configuration changes","Notificar subscriptores cuando cambia la configuración del nodo"}. {"Notify subscribers when the node is deleted","Notificar subscriptores cuando el nodo se borra"}. -{"November","noviembre"}. +{"November","Noviembre"}. +{"Number of answers required","Número de respuestas necesarias"}. {"Number of occupants","Número de ocupantes"}. +{"Number of Offline Messages","Número de mensajes diferidos"}. {"Number of online users","Número de usuarios conectados"}. {"Number of registered users","Número de usuarios registrados"}. -{"October","octubre"}. -{"Offline Messages:","Mensajes diferidos:"}. -{"Offline Messages","Mensajes diferidos"}. +{"Number of seconds after which to automatically purge items, or `max` for no specific limit other than a server imposed maximum","Número de segundos después de los cuales se purgarán elementos automáticamente, o `max` para no especificar un límite, más que el máximo impuesto por el servidor"}. +{"Occupants are allowed to invite others","Los ocupantes pueden invitar a otras personas"}. +{"Occupants are allowed to query others","Los ocupantes pueden enviar peticiones a otros"}. +{"Occupants May Change the Subject","Los ocupantes pueden cambiar el Asunto"}. +{"October","Octubre"}. {"OK","Aceptar"}. {"Old Password:","Contraseña antigua:"}. -{"Online","Conectado"}. -{"Online Users:","Usuarios conectados:"}. {"Online Users","Usuarios conectados"}. +{"Online","Conectado"}. +{"Only collection node owners may associate leaf nodes with the collection","Solo los dueños e la colección de nodos pueden asociar nodos hoja a la colección"}. {"Only deliver notifications to available users","Solo enviar notificaciones a los usuarios disponibles"}. +{"Only or tags are allowed","Solo se permiten las etiquetas o "}. +{"Only element is allowed in this query","Solo se permite el elemento en esta petición"}. +{"Only members may query archives of this room","Solo miembros pueden consultar el archivo de mensajes de la sala"}. {"Only moderators and participants are allowed to change the subject in this room","Solo los moderadores y participantes pueden cambiar el asunto de esta sala"}. {"Only moderators are allowed to change the subject in this room","Solo los moderadores pueden cambiar el asunto de esta sala"}. +{"Only moderators are allowed to retract messages","Solo los moderadores pueden retractarse de los mensajes"}. {"Only moderators can approve voice requests","Solo los moderadores pueden aprobar peticiones de voz"}. {"Only occupants are allowed to send messages to the conference","Solo los ocupantes pueden enviar mensajes a la sala"}. {"Only occupants are allowed to send queries to the conference","Solo los ocupantes pueden enviar solicitudes a la sala"}. +{"Only publishers may publish","Solo los publicadores pueden publicar"}. {"Only service administrators are allowed to send service messages","Solo los administradores del servicio tienen permiso para enviar mensajes de servicio"}. -{"Options","Opciones"}. +{"Only those on a whitelist may associate leaf nodes with the collection","Solo quienes están en una lista blanca pueden asociar nodos hoja a la colección"}. +{"Only those on a whitelist may subscribe and retrieve items","Solo quienes están en una lista blanca pueden suscribirse y recibir elementos"}. {"Organization Name","Nombre de la organización"}. {"Organization Unit","Unidad de la organización"}. -{"Outgoing s2s Connections:","Conexiones S2S salientes:"}. +{"Other Modules Available:","Otros módulos disponibles:"}. {"Outgoing s2s Connections","Conexiones S2S salientes"}. -{"Outgoing s2s Servers:","Servidores S2S salientes:"}. {"Owner privileges required","Se requieren privilegios de propietario de la sala"}. -{"Packet","Paquete"}. -{"Password ~b","Contraseña ~b"}. -{"Password:","Contraseña:"}. -{"Password","Contraseña"}. -{"Password Verification:","Verificación de la contraseña:"}. +{"Packet relay is denied by service policy","Se ha denegado el reenvío del paquete por política del servicio"}. +{"Participant ID","ID del Participante"}. +{"Participant","Participante"}. {"Password Verification","Verificación de la contraseña"}. +{"Password Verification:","Verificación de la contraseña:"}. +{"Password","Contraseña"}. +{"Password:","Contraseña:"}. {"Path to Dir","Ruta al directorio"}. {"Path to File","Ruta al fichero"}. -{"Pending","Pendiente"}. +{"Payload semantic type information","Información sobre el tipo semántico de la carga útil"}. {"Period: ","Periodo: "}. {"Persist items to storage","Persistir elementos al almacenar"}. +{"Persistent","Permanente"}. +{"Ping query is incorrect","La petición de Ping es incorrecta"}. {"Ping","Ping"}. {"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Ten en cuenta que estas opciones solo harán copia de seguridad de la base de datos Mnesia embebida. Si estás usando ODBC tendrás que hacer también copia de seguridad de tu base de datos SQL."}. {"Please, wait for a while before sending new voice request","Por favor, espera un poco antes de enviar otra petición de voz"}. {"Pong","Pong"}. -{"Port ~b","Puerto ~b"}. -{"Port","Puerto"}. +{"Possessing 'ask' attribute is not allowed by RFC6121","Poseer el atributo 'ask' no está permitido por RFC6121"}. {"Present real Jabber IDs to","Los Jabber ID reales pueden verlos"}. -{"private, ","privado"}. -{"Protocol","Protocolo"}. +{"Previous session not found","La sesión previa no ha sido encontrada"}. +{"Previous session PID has been killed","El proceso de la sesión previa ha sido cerrado"}. +{"Previous session PID has exited","El proceso de la sesión previa ha terminado"}. +{"Previous session PID is dead","El proceso de la sesión previa está muerto"}. +{"Previous session timed out","La sesión previa ha caducado"}. +{"private, ","privado, "}. +{"Public","Público"}. +{"Publish model","Modelo de publicación"}. {"Publish-Subscribe","Servicio de Publicar-Subscribir"}. {"PubSub subscriber request","Petición de subscriptor de PubSub"}. {"Purge all items when the relevant publisher goes offline","Borra todos los elementos cuando el publicador relevante se desconecta"}. +{"Push record not found","No se encontró registro Push"}. {"Queries to the conference members are not allowed in this room","En esta sala no se permiten solicitudes a los miembros de la sala"}. +{"Query to another users is forbidden","Enviar solicitudes a otros usuarios está prohibido"}. {"RAM and disc copy","Copia en RAM y disco"}. {"RAM copy","Copia en RAM"}. -{"Raw","Crudo"}. {"Really delete message of the day?","¿Está seguro de quere borrar el mensaje del dia?"}. +{"Receive notification from all descendent nodes","Recibir notificaciones de todos los nodos descendientes"}. +{"Receive notification from direct child nodes only","Recibir notificaciones solo de los nodos que son hijos directos"}. +{"Receive notification of new items only","Recibir notificaciones solo de nuevos elementos"}. +{"Receive notification of new nodes only","Recibir notificaciones solo de nuevos nodos"}. {"Recipient is not in the conference room","El receptor no está en la sala de conferencia"}. -{"Register a Jabber account","Registrar una cuenta Jabber"}. -{"Registered Users:","Usuarios registrados:"}. -{"Registered Users","Usuarios registrados"}. +{"Register an XMPP account","Registrar una cuenta XMPP"}. {"Register","Registrar"}. -{"Registration in mod_irc for ","Registro en mod_irc para"}. {"Remote copy","Copia remota"}. -{"Remove All Offline Messages","Borrar todos los mensajes diferidos"}. -{"Remove","Borrar"}. +{"Remove a hat from a user","Quitarle un sombrero a un usuario"}. {"Remove User","Eliminar usuario"}. {"Replaced by new connection","Reemplazado por una nueva conexión"}. +{"Request has timed out","La petición ha caducado"}. +{"Request is ignored","La petición ha sido ignorada"}. +{"Requested role","Rol solicitado"}. {"Resources","Recursos"}. -{"Restart","Reiniciar"}. {"Restart Service","Reiniciar el servicio"}. {"Restore Backup from File at ","Restaura copia de seguridad desde el fichero en "}. {"Restore binary backup after next ejabberd restart (requires less memory):","Restaurar copia de seguridad binaria en el siguiente reinicio de ejabberd (requiere menos memoria que si instantánea):"}. {"Restore binary backup immediately:","Restaurar inmediatamente copia de seguridad binaria:"}. {"Restore plain text backup immediately:","Restaurar copias de seguridad de texto plano inmediatamente:"}. {"Restore","Restaurar"}. +{"Result","Resultado"}. +{"Roles and Affiliations that May Retrieve Member List","Roles y Afiliaciones que pueden obtener la lista de miembros"}. +{"Roles for which Presence is Broadcasted","Roles para los que sí se difunde su Presencia"}. +{"Roles that May Send Private Messages","Roles que pueden enviar mensajes privados"}. {"Room Configuration","Configuración de la sala"}. {"Room creation is denied by service policy","Se te ha denegado crear la sala por política del servicio"}. {"Room description","Descripción de la sala"}. {"Room Occupants","Ocupantes de la sala"}. +{"Room terminates","Cerrando la sala"}. {"Room title","Título de la sala"}. {"Roster groups allowed to subscribe","Grupos de contactos que pueden suscribirse"}. -{"Roster","Lista de contactos"}. -{"Roster of ","Lista de contactos de "}. {"Roster size","Tamaño de la lista de contactos"}. -{"RPC Call Error","Error en la llamada RPC"}. {"Running Nodes","Nodos funcionando"}. -{"~s access rule configuration","Configuración de las Regla de Acceso ~s"}. -{"Saturday","sábado"}. -{"Script check","Comprobación de script"}. +{"~s invites you to the room ~s","~s te invita a la sala ~s"}. +{"Saturday","Sábado"}. +{"Search from the date","Buscar desde la fecha"}. {"Search Results for ","Buscar resultados por "}. +{"Search the text","Buscar el texto"}. +{"Search until the date","Buscar hasta la fecha"}. {"Search users in ","Buscar usuarios en "}. -{"Send announcement to all online users","Enviar anuncio a todos los usuarios conectados"}. {"Send announcement to all online users on all hosts","Enviar anuncio a todos los usuarios conectados en todos los dominios"}. -{"Send announcement to all users","Enviar anuncio a todos los usuarios"}. +{"Send announcement to all online users","Enviar anuncio a todos los usuarios conectados"}. {"Send announcement to all users on all hosts","Enviar anuncio a todos los usuarios en todos los dominios"}. -{"September","septiembre"}. -{"Server ~b","Servidor ~b"}. +{"Send announcement to all users","Enviar anuncio a todos los usuarios"}. +{"September","Septiembre"}. {"Server:","Servidor:"}. +{"Service list retrieval timed out","Ha caducado la obtención de la lista de servicio"}. +{"Session state copying timed out","El copiado del estado de la sesión ha caducado"}. {"Set message of the day and send to online users","Poner mensaje del dia y enviar a todos los usuarios conectados"}. {"Set message of the day on all hosts and send to online users","Poner mensaje del día en todos los dominios y enviar a los usuarios conectados"}. {"Shared Roster Groups","Grupos Compartidos"}. {"Show Integral Table","Mostrar Tabla Integral"}. +{"Show Occupants Join/Leave","Mostrar personas activas Entrar/Salir"}. {"Show Ordinary Table","Mostrar Tabla Ordinaria"}. {"Shut Down Service","Detener el servicio"}. -{"~s invites you to the room ~s","~s te invita a la sala ~s"}. -{"Some Jabber clients can store your password in your computer. Use that feature only if you trust your computer is safe.","Algunos clientes Jabber pueden recordar tu contraseña en la máquina. Usa esa opción solo si confías en que la máquina que usas es segura."}. +{"SOCKS5 Bytestreams","SOCKS5 Bytestreams"}. +{"Some XMPP clients can store your password in the computer, but you should do this only in your personal computer for safety reasons.","Algunos clientes XMPP pueden guardar tu contraseña en la máquina, pero solo deberías hacer esto en tu propia máquina personal, por razones de seguridad."}. +{"Sources Specs:","Especificaciones de Códigos Fuente:"}. {"Specify the access model","Especifica el modelo de acceso"}. {"Specify the event message type","Especifica el tipo del mensaje de evento"}. {"Specify the publisher model","Especificar el modelo del publicante"}. -{"~s's Offline Messages Queue","Cola de mensajes diferidos de ~s"}. -{"Start","Iniciar"}. -{"Start Modules at ","Iniciar módulos en "}. -{"Start Modules","Iniciar módulos"}. -{"Statistics","Estadísticas"}. -{"Statistics of ~p","Estadísticas de ~p"}. -{"Stop","Detener"}. -{"Stop Modules at ","Detener módulos en "}. -{"Stop Modules","Detener módulos"}. +{"Stanza id is not valid","El identificador de la estrofa no es válido"}. +{"Stanza ID","ID del paquete"}. +{"Statically specify a replyto of the node owner(s)","Especificar de forma estática un 'replyto' de dueño(s) del nodo"}. {"Stopped Nodes","Nodos detenidos"}. -{"Storage Type","Tipo de almacenamiento"}. {"Store binary backup:","Guardar copia de seguridad binaria:"}. {"Store plain text backup:","Guardar copia de seguridad en texto plano:"}. +{"Stream management is already enabled","Ya está activada la administración de la conexión"}. +{"Stream management is not enabled","No está activada la administración de la conexión"}. {"Subject","Asunto"}. -{"Submit","Enviar"}. {"Submitted","Enviado"}. {"Subscriber Address","Dirección del subscriptor"}. -{"Subscription","Subscripción"}. -{"Sunday","domingo"}. +{"Subscribers may publish","Los suscriptores pueden publicar"}. +{"Subscription requests must be approved and only subscribers may retrieve items","Las peticiones de suscripción deben ser aprobadas y solo los suscriptores pueden obtener elementos"}. +{"Subscriptions are not allowed","Las subscripciones no están permitidas"}. +{"Sunday","Domingo"}. +{"Text associated with a picture","Texto asociado con una imagen"}. +{"Text associated with a sound","Texto asociado con un sonido"}. +{"Text associated with a video","Texto asociado con un vídeo"}. +{"Text associated with speech","Texto asociado con una charla"}. {"That nickname is already in use by another occupant","Ese apodo ya está siendo usado por otro ocupante"}. {"That nickname is registered by another person","El apodo ya está registrado por otra persona"}. +{"The account already exists","La cuenta ya existe"}. +{"The account was not unregistered","La cuenta no fue eliminada"}. +{"The body text of the last received message","El contenido de texto del último mensaje recibido"}. {"The CAPTCHA is valid.","El CAPTCHA es válido."}. {"The CAPTCHA verification has failed","La verificación de CAPTCHA ha fallado"}. +{"The captcha you entered is wrong","El CAPTCHA que has introducido es erróneo"}. +{"The child nodes (leaf or collection) associated with a collection","Los nodos hijos (ya sean hojas o colecciones) asociados con una colección"}. {"The collections with which a node is affiliated","Las colecciones a las que un nodo está afiliado"}. -{"the password is","la contraseña es"}. +{"The DateTime at which a leased subscription will end or has ended","La FechayHora en la que una suscripción prestada acabará o ha terminado"}. +{"The datetime when the node was created","La fechayhora cuando el nodo fue creado"}. +{"The default language of the node","El nombre por defecto del nodo"}. +{"The feature requested is not supported by the conference","La característica solicitada no está soportada por la sala de conferencia"}. +{"The JID of the node creator","El JID del creador del nodo"}. +{"The JIDs of those to contact with questions","Los JIDs a quienes contactar con preguntas"}. +{"The JIDs of those with an affiliation of owner","Los JIDs de quienes tienen una afiliación de dueños"}. +{"The JIDs of those with an affiliation of publisher","Los JIDs de quienes tienen una afiliación de publicadores"}. +{"The list of all online users","La lista de todos los usuarios conectados"}. +{"The list of all users","La lista de todos los usuarios"}. +{"The list of JIDs that may associate leaf nodes with a collection","La lista de JIDs que pueden asociar nodos hijo con una colección"}. +{"The maximum number of child nodes that can be associated with a collection, or `max` for no specific limit other than a server imposed maximum","El número máximo de nodos hijo que pueden asociarse a una colección, o `max` para no especificar un límite, más que el máximo impuesto por el servidor"}. +{"The minimum number of milliseconds between sending any two notification digests","El número mínimo de milisegundos entre dos envíos de resumen de notificaciones"}. +{"The name of the node","El nombre del nodo"}. +{"The node is a collection node","El nodo es una colección"}. +{"The node is a leaf node (default)","El nodo es un nodo hoja (por defecto)"}. +{"The NodeID of the relevant node","El NodoID del nodo relevante"}. +{"The number of pending incoming presence subscription requests","El número de peticiones de suscripción a presencia que están pendientes de llegar"}. +{"The number of subscribers to the node","El número de suscriptores al nodo"}. +{"The number of unread or undelivered messages","El número de mensajes sin leer o sin entregar"}. +{"The password contains unacceptable characters","La contraseña contiene caracteres inaceptables"}. {"The password is too weak","La contraseña es demasiado débil"}. -{"The password of your Jabber account was successfully changed.","La contraseña de tu cuenta Jabber se ha cambiado correctamente."}. -{"There was an error changing the password: ","Hubo un error cambiando la contraseña."}. -{"There was an error creating the account: ","Hubo uno error al crear la cuenta:"}. -{"There was an error deleting the account: ","Hubo un error borrando la cuenta."}. +{"the password is","la contraseña es"}. +{"The password of your XMPP account was successfully changed.","La contraseña de tu cuenta XMPP se ha cambiado correctamente."}. +{"The password was not changed","La contraseña no fue cambiada"}. +{"The passwords are different","Las contraseñas son diferentes"}. +{"The presence states for which an entity wants to receive notifications","Los estados de presencia para los cuales una entidad quiere recibir notificaciones"}. +{"The query is only allowed from local users","La solicitud está permitida solo para usuarios locales"}. +{"The query must not contain elements","La solicitud no debe contener elementos "}. +{"The room subject can be modified by participants","El asunto de la sala puede ser modificado por los participantes"}. +{"The semantic type information of data in the node, usually specified by the namespace of the payload (if any)","La información semántica de los datos del nodo, normalmente es especificada por el espacio de los nombres de la carga útil (si existe)"}. +{"The sender of the last received message","El emisor del último mensaje recibido"}. +{"The stanza MUST contain only one element, one element, or one element","El paquete DEBE contener solo un elemento , un elemento , o un elemento "}. +{"The subscription identifier associated with the subscription request","El identificador de suscripción asociado con la petición de suscripción"}. +{"The URL of an XSL transformation which can be applied to payloads in order to generate an appropriate message body element.","La URL de una transformación XSL que puede aplicarse a payloads para generar un elemento de contenido del mensaje apropiado."}. +{"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","La URL de una transformación XSL que puede aplicarse al formato de payload para generar un resultado de Formulario de Datos válido, que el cliente pueda mostrar usando un mecanismo de dibujado genérico de Formulario de Datos"}. +{"There was an error changing the password: ","Hubo uno error al cambiar la contaseña: "}. +{"There was an error creating the account: ","Hubo uno error al crear la cuenta: "}. +{"There was an error deleting the account: ","Hubo un error borrando la cuenta: "}. {"This is case insensitive: macbeth is the same that MacBeth and Macbeth.","No importa si usas mayúsculas: macbeth es lo mismo que MacBeth y Macbeth."}. -{"This page allows to create a Jabber account in this Jabber server. Your JID (Jabber IDentifier) will be of the form: username@server. Please read carefully the instructions to fill correctly the fields.","Esta página te permite crear una cuenta Jabber este servidor Jabber. Tu JID (Jabber IDentificador) será de la forma: nombredeusuario@servidor. Por favor lee detenidamente las instrucciones para rellenar correctamente los campos."}. -{"This page allows to unregister a Jabber account in this Jabber server.","Esta página te permite borrar tu cuenta Jabber en este servidor Jabber."}. -{"This participant is kicked from the room because he sent an error message","Este participante ha sido expulsado de la sala porque envió un mensaje de error"}. -{"This participant is kicked from the room because he sent an error message to another participant","Este participante ha sido expulsado de la sala porque envió un mensaje de error a otro participante"}. -{"This participant is kicked from the room because he sent an error presence","Este participante ha sido expulsado de la sala porque envió una presencia de error"}. +{"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.","Esta página te permite crear una cuenta XMPP este servidor XMPP. Tu JID (Jabber ID) será de la forma: nombredeusuario@servidor. Por favor lee detenidamente las instrucciones para rellenar correctamente los campos."}. +{"This page allows to unregister an XMPP account in this XMPP server.","Esta página te permite borrar tu cuenta XMPP en este servidor XMPP."}. {"This room is not anonymous","Sala no anónima"}. -{"Thursday","jueves"}. +{"This service can not process the address: ~s","Este servicio no puede procesar la dirección: ~s"}. +{"Thursday","Jueves"}. {"Time delay","Retraso temporal"}. -{"Time","Fecha"}. +{"Timed out waiting for stream resumption","Ha pasado demasiado tiempo esperando que la conexión se restablezca"}. +{"To register, visit ~s","Para registrarte, visita ~s"}. +{"To ~ts","A ~ts"}. +{"Token TTL","Token TTL"}. +{"Too many active bytestreams","Demasiados bytestreams activos"}. {"Too many CAPTCHA requests","Demasiadas peticiones de CAPTCHA"}. -{"To","Para"}. -{"To ~s","A ~s"}. -{"Traffic rate limit is exceeded","Se ha exedido el límite de tráfico"}. -{"Transactions Aborted:","Transacciones abortadas:"}. -{"Transactions Committed:","Transacciones finalizadas:"}. -{"Transactions Logged:","Transacciones registradas:"}. -{"Transactions Restarted:","Transacciones reiniciadas:"}. -{"Tuesday","martes"}. +{"Too many child elements","Demasiados subelementos"}. +{"Too many elements","Demasiados elementos "}. +{"Too many elements","Demasiados elementos "}. +{"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","Demasiadas (~p) autenticaciones fallidas de esta dirección IP (~s). La dirección será desbloqueada en ~s UTC"}. +{"Too many receiver fields were specified","Se han especificado demasiados campos de destinatario"}. +{"Too many unacked stanzas","Demasiados mensajes sin haber reconocido recibirlos"}. +{"Too many users in this conference","Demasiados usuarios en esta sala"}. +{"Traffic rate limit is exceeded","Se ha excedido el límite de tráfico"}. +{"~ts's MAM Archive","Archivo MAM de ~ts"}. +{"~ts's Offline Messages Queue","Cola de mensajes diferidos de ~ts"}. +{"Tuesday","Martes"}. {"Unable to generate a CAPTCHA","No se pudo generar un CAPTCHA"}. +{"Unable to register route on existing local domain","No se ha podido registrar la ruta en este dominio local existente"}. {"Unauthorized","No autorizado"}. -{"Unregister a Jabber account","Borrar una cuenta Jabber"}. +{"Unexpected action","Acción inesperada"}. +{"Unexpected error condition: ~p","Condición de error inesperada: ~p"}. +{"Uninstall","Desinstalar"}. +{"Unregister an XMPP account","Borrar una cuenta XMPP"}. {"Unregister","Borrar"}. -{"Update ","Actualizar "}. -{"Update","Actualizar"}. +{"Unsupported element","Elemento no soportado"}. +{"Unsupported version","Versión no soportada"}. {"Update message of the day (don't send)","Actualizar mensaje del dia, pero no enviarlo"}. {"Update message of the day on all hosts (don't send)","Actualizar el mensaje del día en todos los dominos (pero no enviarlo)"}. -{"Update plan","Plan de actualización"}. -{"Update script","Script de actualización"}. -{"Uptime:","Tiempo desde el inicio:"}. -{"Use of STARTTLS required","Es obligatorio usar STARTTLS"}. +{"Update specs to get modules source, then install desired ones.","Actualizar Especificaciones para conseguir el código fuente de los módulos, luego instala los que quieras."}. +{"Update Specs","Actualizar Especificaciones"}. +{"Updating the vCard is not supported by the vCard storage backend","La actualización de la vCard no es compatible con el vCard almacenamiento backend"}. +{"Upgrade","Actualizar"}. +{"URL for Archived Discussion Logs","URL del registro de discusiones archivadas"}. +{"User already exists","El usuario ya existe"}. {"User JID","Jabber ID del usuario"}. +{"User (jid)","Usuario (jid)"}. {"User Management","Administración de usuarios"}. +{"User not allowed to perform an IQ set on another user's vCard.","No se permite al usuario realizar un IQ establecido en la vCard de otro usuario."}. +{"User removed","Usuario eliminado"}. +{"User session not found","Sesión de usuario no encontrada"}. +{"User session terminated","Sesión de usuario terminada"}. +{"User ~ts","Usuario ~ts"}. {"Username:","Nombre de usuario:"}. {"Users are not allowed to register accounts so quickly","Los usuarios no tienen permitido crear cuentas con tanta rapidez"}. {"Users Last Activity","Última actividad de los usuarios"}. {"Users","Usuarios"}. -{"User ","Usuario "}. {"User","Usuario"}. -{"Validate","Validar"}. -{"vCard User Search","Buscar vCard de usuario"}. +{"Value 'get' of 'type' attribute is not allowed","El valor 'get' del atributo 'type' no está permitido"}. +{"Value of '~s' should be boolean","El valor de '~s' debería ser booleano"}. +{"Value of '~s' should be datetime string","El valor de '~s' debería ser una fecha"}. +{"Value of '~s' should be integer","El valor de '~s' debería ser un entero"}. +{"Value 'set' of 'type' attribute is not allowed","El valor 'set' del atributo 'type' no está permitido"}. +{"vCard User Search","Búsqueda de vCard de usuarios"}. +{"View joined MIX channels","Ver los canales MIX unidos"}. {"Virtual Hosts","Dominios Virtuales"}. {"Visitors are not allowed to change their nicknames in this room","Los visitantes no tienen permitido cambiar sus apodos en esta sala"}. {"Visitors are not allowed to send messages to all occupants","Los visitantes no pueden enviar mensajes a todos los ocupantes"}. +{"Visitor","Visitante"}. {"Voice request","Petición de voz"}. {"Voice requests are disabled in this conference","Las peticiones de voz están desactivadas en esta sala"}. -{"Wednesday","miércoles"}. +{"Web client which allows to join the room anonymously","Cliente web que permite entrar en la sala anonimamente"}. +{"Wednesday","Miércoles"}. +{"When a new subscription is processed and whenever a subscriber comes online","Cuando se procesa una nueva suscripción y cuando un suscriptor se conecta"}. +{"When a new subscription is processed","Cuando se procesa una nueva suscripción"}. {"When to send the last published item","Cuando enviar el último elemento publicado"}. +{"Whether an entity wants to receive an XMPP message body in addition to the payload format","Si una entidad quiere recibir un cuerpo de mensaje XMPP adicionalmente al formato de payload"}. +{"Whether an entity wants to receive digests (aggregations) of notifications or all notifications individually","Si una entidad quiere recibir resúmenes (agregados) de notificaciones o todas las notificaciones individualmente"}. +{"Whether an entity wants to receive or disable notifications","Si una entidad quiere recibir o desactivar las notificaciones"}. +{"Whether owners or publisher should receive replies to items","Si dueños y publicadores deberían recibir respuestas de los elementos"}. +{"Whether the node is a leaf (default) or a collection","Si el nodo es una hoja (por defecto) o una colección"}. {"Whether to allow subscriptions","Permitir subscripciones"}. -{"You can later change your password using a Jabber client.","Puedes cambiar tu contraseña después, usando un cliente Jabber."}. +{"Whether to make all subscriptions temporary, based on subscriber presence","Si hacer que todas las suscripciones sean temporales, basado en la presencia del suscriptor"}. +{"Whether to notify owners about new subscribers and unsubscribes","Si notificar a los dueños sobre nuevas suscripciones y desuscripciones"}. +{"Who can send private messages","Quién puede enviar mensajes privados"}. +{"Who may associate leaf nodes with a collection","Quien puede asociar nodos hoja con una colección"}. +{"Wrong parameters in the web formulary","Parámetros incorrectos en el formulario web"}. +{"Wrong xmlns","XMLNS incorrecto"}. +{"XMPP Account Registration","Registro de Cuenta XMPP"}. +{"XMPP Domains","Dominios XMPP"}. +{"XMPP Show Value of Away","Valor 'Show' de XMPP: Ausente"}. +{"XMPP Show Value of Chat","Valor 'Show' de XMPP: Charlador"}. +{"XMPP Show Value of DND (Do Not Disturb)","Valor 'Show' de XMPP: DND (No Molestar)"}. +{"XMPP Show Value of XA (Extended Away)","Valor 'Show' de XMPP: XA (Ausente Extendido)"}. +{"XMPP URI of Associated Publish-Subscribe Node","URI XMPP del Nodo Asociado de Publicar-Subscribir"}. +{"You are being removed from the room because of a system shutdown","Estás siendo expulsado de la sala porque el sistema se va a detener"}. +{"You are not allowed to send private messages","No tienes permitido enviar mensajes privados"}. +{"You are not joined to the channel","No has entrado en el canal"}. +{"You can later change your password using an XMPP client.","Puedes cambiar tu contraseña después, usando un cliente XMPP."}. {"You have been banned from this room","Has sido bloqueado en esta sala"}. +{"You have joined too many conferences","Has entrado en demasiadas salas de conferencia"}. {"You must fill in field \"Nickname\" in the form","Debes rellenar el campo \"Apodo\" en el formulario"}. {"You need a client that supports x:data and CAPTCHA to register","Necesitas un cliente con soporte de x:data y CAPTCHA para registrarte"}. {"You need a client that supports x:data to register the nickname","Necesitas un cliente con soporte de x:data para poder registrar el apodo"}. -{"You need an x:data capable client to configure mod_irc settings","Necesitas un cliente con soporte de x:data para configurar las opciones de mod_irc"}. -{"You need an x:data capable client to configure room","Necesitas un cliente con soporte de x:data para configurar la sala"}. {"You need an x:data capable client to search","Necesitas un cliente con soporte de x:data para poder buscar"}. -{"Your active privacy list has denied the routing of this stanza.","Tu lista de privacidad activa ha denegado el encío de este paquete."}. +{"Your active privacy list has denied the routing of this stanza.","Tu lista de privacidad activa ha denegado el envío de este paquete."}. {"Your contact offline message queue is full. The message has been discarded.","Tu cola de mensajes diferidos de contactos está llena. El mensaje se ha descartado."}. -{"Your Jabber account was successfully created.","Tu cuenta Jabber se ha creado correctamente."}. -{"Your Jabber account was successfully deleted.","Tu cuenta Jabber se ha borrado correctamente."}. -{"Your messages to ~s are being blocked. To unblock them, visit ~s","Tus mensajes a ~s están siendo bloqueados. Para desbloquearlos, visita ~s"}. +{"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","Tu petición de suscripción y/o mensajes a ~s ha sido bloqueado. Para desbloquear tu petición de suscripción visita ~s"}. +{"Your XMPP account was successfully registered.","Tu cuenta XMPP se ha registrado correctamente."}. +{"Your XMPP account was successfully unregistered.","Tu cuenta XMPP se ha borrado correctamente."}. +{"You're not allowed to create nodes","No tienes permitido crear nodos"}. diff --git a/priv/msgs/es.po b/priv/msgs/es.po deleted file mode 100644 index b5929edd3..000000000 --- a/priv/msgs/es.po +++ /dev/null @@ -1,1862 +0,0 @@ -# translation of es.po to -# Badlop , 2009. -msgid "" -msgstr "" -"Project-Id-Version: es\n" -"POT-Creation-Date: \n" -"PO-Revision-Date: 2012-04-15 00:10+0100\n" -"Last-Translator: Badlop \n" -"Language-Team: \n" -"Language: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Language: Spanish (castellano)\n" -"X-Generator: KBabel 1.11.4\n" - -#: ejabberd_c2s.erl:424 ejabberd_c2s.erl:727 -msgid "Use of STARTTLS required" -msgstr "Es obligatorio usar STARTTLS" - -#: ejabberd_c2s.erl:503 -msgid "No resource provided" -msgstr "No se ha proporcionado recurso" - -#: ejabberd_c2s.erl:1197 -msgid "Replaced by new connection" -msgstr "Reemplazado por una nueva conexión" - -#: ejabberd_c2s.erl:1885 -msgid "Your active privacy list has denied the routing of this stanza." -msgstr "Tu lista de privacidad activa ha denegado el encío de este paquete." - -#: ejabberd_captcha.erl:96 ejabberd_captcha.erl:152 ejabberd_captcha.erl:178 -msgid "Enter the text you see" -msgstr "Teclea el texto que ves" - -#: ejabberd_captcha.erl:101 -msgid "Your messages to ~s are being blocked. To unblock them, visit ~s" -msgstr "" -"Tus mensajes a ~s están siendo bloqueados. Para desbloquearlos, visita ~s" - -#: ejabberd_captcha.erl:134 -msgid "If you don't see the CAPTCHA image here, visit the web page." -msgstr "Si no ves la imagen CAPTCHA aquí, visita la página web." - -#: ejabberd_captcha.erl:146 -msgid "CAPTCHA web page" -msgstr "Página web de CAPTCHA" - -#: ejabberd_captcha.erl:307 -msgid "The CAPTCHA is valid." -msgstr "El CAPTCHA es válido." - -#: mod_adhoc.erl:95 mod_adhoc.erl:125 mod_adhoc.erl:143 mod_adhoc.erl:161 -msgid "Commands" -msgstr "Comandos" - -#: mod_adhoc.erl:149 mod_adhoc.erl:243 -msgid "Ping" -msgstr "Ping" - -#: mod_adhoc.erl:260 -msgid "Pong" -msgstr "Pong" - -#: mod_announce.erl:507 mod_announce_odbc.erl:499 -msgid "Really delete message of the day?" -msgstr "¿Está seguro de quere borrar el mensaje del dia?" - -#: mod_announce.erl:515 mod_announce_odbc.erl:507 mod_configure.erl:1083 -#: mod_configure.erl:1128 -msgid "Subject" -msgstr "Asunto" - -#: mod_announce.erl:520 mod_announce_odbc.erl:512 mod_configure.erl:1088 -#: mod_configure.erl:1133 -msgid "Message body" -msgstr "Cuerpo del mensaje" - -#: mod_announce.erl:600 mod_announce_odbc.erl:592 -msgid "No body provided for announce message" -msgstr "No se ha proporcionado cuerpo de mensaje para el anuncio" - -#: mod_announce.erl:635 mod_announce_odbc.erl:627 -msgid "Announcements" -msgstr "Anuncios" - -#: mod_announce.erl:637 mod_announce_odbc.erl:629 -msgid "Send announcement to all users" -msgstr "Enviar anuncio a todos los usuarios" - -#: mod_announce.erl:639 mod_announce_odbc.erl:631 -msgid "Send announcement to all users on all hosts" -msgstr "Enviar anuncio a todos los usuarios en todos los dominios" - -#: mod_announce.erl:641 mod_announce_odbc.erl:633 -msgid "Send announcement to all online users" -msgstr "Enviar anuncio a todos los usuarios conectados" - -#: mod_announce.erl:643 mod_announce_odbc.erl:635 mod_configure.erl:1078 -#: mod_configure.erl:1123 -msgid "Send announcement to all online users on all hosts" -msgstr "Enviar anuncio a todos los usuarios conectados en todos los dominios" - -#: mod_announce.erl:645 mod_announce_odbc.erl:637 -msgid "Set message of the day and send to online users" -msgstr "Poner mensaje del dia y enviar a todos los usuarios conectados" - -#: mod_announce.erl:647 mod_announce_odbc.erl:639 -msgid "Set message of the day on all hosts and send to online users" -msgstr "" -"Poner mensaje del día en todos los dominios y enviar a los usuarios " -"conectados" - -#: mod_announce.erl:649 mod_announce_odbc.erl:641 -msgid "Update message of the day (don't send)" -msgstr "Actualizar mensaje del dia, pero no enviarlo" - -#: mod_announce.erl:651 mod_announce_odbc.erl:643 -msgid "Update message of the day on all hosts (don't send)" -msgstr "Actualizar el mensaje del día en todos los dominos (pero no enviarlo)" - -#: mod_announce.erl:653 mod_announce_odbc.erl:645 -msgid "Delete message of the day" -msgstr "Borrar mensaje del dia" - -#: mod_announce.erl:655 mod_announce_odbc.erl:647 -msgid "Delete message of the day on all hosts" -msgstr "Borrar el mensaje del día en todos los dominios" - -#: mod_configure.erl:114 mod_configure.erl:274 mod_configure.erl:296 -#: mod_configure.erl:498 -msgid "Configuration" -msgstr "Configuración" - -#: mod_configure.erl:125 mod_configure.erl:576 web/ejabberd_web_admin.erl:1936 -msgid "Database" -msgstr "Base de datos" - -#: mod_configure.erl:127 mod_configure.erl:595 -msgid "Start Modules" -msgstr "Iniciar módulos" - -#: mod_configure.erl:129 mod_configure.erl:596 -msgid "Stop Modules" -msgstr "Detener módulos" - -#: mod_configure.erl:131 mod_configure.erl:604 web/ejabberd_web_admin.erl:1937 -msgid "Backup" -msgstr "Guardar copia de seguridad" - -#: mod_configure.erl:133 mod_configure.erl:605 -msgid "Restore" -msgstr "Restaurar" - -#: mod_configure.erl:135 mod_configure.erl:606 -msgid "Dump to Text File" -msgstr "Exportar a fichero de texto" - -#: mod_configure.erl:137 mod_configure.erl:615 -msgid "Import File" -msgstr "Importar fichero" - -#: mod_configure.erl:139 mod_configure.erl:616 -msgid "Import Directory" -msgstr "Importar directorio" - -#: mod_configure.erl:141 mod_configure.erl:581 mod_configure.erl:1057 -msgid "Restart Service" -msgstr "Reiniciar el servicio" - -#: mod_configure.erl:143 mod_configure.erl:582 mod_configure.erl:1102 -msgid "Shut Down Service" -msgstr "Detener el servicio" - -#: mod_configure.erl:145 mod_configure.erl:518 mod_configure.erl:1197 -#: web/ejabberd_web_admin.erl:1527 -msgid "Add User" -msgstr "Añadir usuario" - -#: mod_configure.erl:147 mod_configure.erl:519 mod_configure.erl:1219 -msgid "Delete User" -msgstr "Borrar usuario" - -#: mod_configure.erl:149 mod_configure.erl:520 mod_configure.erl:1231 -msgid "End User Session" -msgstr "Cerrar sesión de usuario" - -#: mod_configure.erl:151 mod_configure.erl:521 mod_configure.erl:1243 -#: mod_configure.erl:1255 -msgid "Get User Password" -msgstr "Ver contraseña de usuario" - -#: mod_configure.erl:153 mod_configure.erl:522 -msgid "Change User Password" -msgstr "Cambiar contraseña de usuario" - -#: mod_configure.erl:155 mod_configure.erl:523 mod_configure.erl:1272 -msgid "Get User Last Login Time" -msgstr "Ver fecha de la última conexión de usuario" - -#: mod_configure.erl:157 mod_configure.erl:524 mod_configure.erl:1284 -msgid "Get User Statistics" -msgstr "Ver estadísticas de usuario" - -#: mod_configure.erl:159 mod_configure.erl:525 -msgid "Get Number of Registered Users" -msgstr "Ver número de usuarios registrados" - -#: mod_configure.erl:161 mod_configure.erl:526 -msgid "Get Number of Online Users" -msgstr "Ver número de usuarios conectados" - -#: mod_configure.erl:163 mod_configure.erl:509 web/ejabberd_web_admin.erl:831 -#: web/ejabberd_web_admin.erl:872 -msgid "Access Control Lists" -msgstr "Listas de Control de Acceso" - -#: mod_configure.erl:165 mod_configure.erl:510 web/ejabberd_web_admin.erl:940 -#: web/ejabberd_web_admin.erl:976 -msgid "Access Rules" -msgstr "Reglas de Acceso" - -#: mod_configure.erl:297 mod_configure.erl:499 -msgid "User Management" -msgstr "Administración de usuarios" - -#: mod_configure.erl:500 web/ejabberd_web_admin.erl:1058 -#: web/ejabberd_web_admin.erl:1462 -msgid "Online Users" -msgstr "Usuarios conectados" - -#: mod_configure.erl:501 -msgid "All Users" -msgstr "Todos los usuarios" - -#: mod_configure.erl:502 -msgid "Outgoing s2s Connections" -msgstr "Conexiones S2S salientes" - -#: mod_configure.erl:503 web/ejabberd_web_admin.erl:1907 -msgid "Running Nodes" -msgstr "Nodos funcionando" - -#: mod_configure.erl:504 web/ejabberd_web_admin.erl:1909 -msgid "Stopped Nodes" -msgstr "Nodos detenidos" - -#: mod_configure.erl:577 mod_configure.erl:587 web/ejabberd_web_admin.erl:1953 -msgid "Modules" -msgstr "Módulos" - -#: mod_configure.erl:578 -msgid "Backup Management" -msgstr "Gestión de copia de seguridad" - -#: mod_configure.erl:579 -msgid "Import Users From jabberd14 Spool Files" -msgstr "Importar usuarios de ficheros spool de jabberd-1.4" - -#: mod_configure.erl:699 -msgid "To ~s" -msgstr "A ~s" - -#: mod_configure.erl:717 -msgid "From ~s" -msgstr "De ~s" - -#: mod_configure.erl:913 -msgid "Database Tables Configuration at " -msgstr "Configuración de tablas de la base de datos en " - -#: mod_configure.erl:918 -msgid "Choose storage type of tables" -msgstr "Selecciona tipo de almacenamiento de las tablas" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Disc only copy" -msgstr "Copia en disco solamente" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM and disc copy" -msgstr "Copia en RAM y disco" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM copy" -msgstr "Copia en RAM" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Remote copy" -msgstr "Copia remota" - -#: mod_configure.erl:950 -msgid "Stop Modules at " -msgstr "Detener módulos en " - -#: mod_configure.erl:954 -msgid "Choose modules to stop" -msgstr "Selecciona módulos a detener" - -#: mod_configure.erl:969 -msgid "Start Modules at " -msgstr "Iniciar módulos en " - -#: mod_configure.erl:973 -msgid "Enter list of {Module, [Options]}" -msgstr "Introduce lista de {módulo, [opciones]}" - -#: mod_configure.erl:974 -msgid "List of modules to start" -msgstr "Lista de módulos a iniciar" - -#: mod_configure.erl:983 -msgid "Backup to File at " -msgstr "Guardar copia de seguridad en fichero en " - -#: mod_configure.erl:987 mod_configure.erl:1001 -msgid "Enter path to backup file" -msgstr "Introduce ruta al fichero de copia de seguridad" - -#: mod_configure.erl:988 mod_configure.erl:1002 mod_configure.erl:1016 -#: mod_configure.erl:1030 -msgid "Path to File" -msgstr "Ruta al fichero" - -#: mod_configure.erl:997 -msgid "Restore Backup from File at " -msgstr "Restaura copia de seguridad desde el fichero en " - -#: mod_configure.erl:1011 -msgid "Dump Backup to Text File at " -msgstr "Exporta copia de seguridad a fichero de texto en " - -#: mod_configure.erl:1015 -msgid "Enter path to text file" -msgstr "Introduce ruta al fichero de texto" - -#: mod_configure.erl:1025 -msgid "Import User from File at " -msgstr "Importa usuario desde fichero en " - -#: mod_configure.erl:1029 -msgid "Enter path to jabberd14 spool file" -msgstr "Introduce ruta al fichero jabberd14 spool" - -#: mod_configure.erl:1039 -msgid "Import Users from Dir at " -msgstr "Importar usuarios desde el directorio en " - -#: mod_configure.erl:1043 -msgid "Enter path to jabberd14 spool dir" -msgstr "Introduce la ruta al directorio de jabberd14 spools" - -#: mod_configure.erl:1044 -msgid "Path to Dir" -msgstr "Ruta al directorio" - -#: mod_configure.erl:1060 mod_configure.erl:1105 -msgid "Time delay" -msgstr "Retraso temporal" - -#: mod_configure.erl:1143 -msgid "Access Control List Configuration" -msgstr "Configuración de la Lista de Control de Acceso" - -#: mod_configure.erl:1147 -msgid "Access control lists" -msgstr "Listas de Control de Acceso" - -#: mod_configure.erl:1171 -msgid "Access Configuration" -msgstr "Configuración de accesos" - -#: mod_configure.erl:1175 -msgid "Access rules" -msgstr "Reglas de acceso" - -#: mod_configure.erl:1200 mod_configure.erl:1222 mod_configure.erl:1234 -#: mod_configure.erl:1246 mod_configure.erl:1258 mod_configure.erl:1275 -#: mod_configure.erl:1287 mod_configure.erl:1650 mod_configure.erl:1700 -#: mod_configure.erl:1721 mod_roster.erl:943 mod_roster_odbc.erl:1060 -#: mod_vcard.erl:472 mod_vcard_ldap.erl:554 mod_vcard_odbc.erl:448 -msgid "Jabber ID" -msgstr "Jabber ID" - -#: mod_configure.erl:1205 mod_configure.erl:1263 mod_configure.erl:1651 -#: mod_configure.erl:1863 mod_muc/mod_muc_room.erl:3224 mod_register.erl:235 -#: web/ejabberd_web_admin.erl:1520 -msgid "Password" -msgstr "Contraseña" - -#: mod_configure.erl:1210 -msgid "Password Verification" -msgstr "Verificación de la contraseña" - -#: mod_configure.erl:1301 -msgid "Number of registered users" -msgstr "Número de usuarios registrados" - -#: mod_configure.erl:1315 -msgid "Number of online users" -msgstr "Número de usuarios conectados" - -#: mod_configure.erl:1682 web/ejabberd_web_admin.erl:1588 -#: web/ejabberd_web_admin.erl:1743 -msgid "Never" -msgstr "Nunca" - -#: mod_configure.erl:1696 web/ejabberd_web_admin.erl:1601 -#: web/ejabberd_web_admin.erl:1756 -msgid "Online" -msgstr "Conectado" - -#: mod_configure.erl:1701 -msgid "Last login" -msgstr "Última conexión" - -#: mod_configure.erl:1722 -msgid "Roster size" -msgstr "Tamaño de la lista de contactos" - -#: mod_configure.erl:1723 -msgid "IP addresses" -msgstr "Direcciones IP" - -#: mod_configure.erl:1724 -msgid "Resources" -msgstr "Recursos" - -#: mod_configure.erl:1850 -msgid "Administration of " -msgstr "Administración de " - -#: mod_configure.erl:1853 -msgid "Action on user" -msgstr "Acción en el usuario" - -#: mod_configure.erl:1857 -msgid "Edit Properties" -msgstr "Editar propiedades" - -#: mod_configure.erl:1860 web/ejabberd_web_admin.erl:1769 -msgid "Remove User" -msgstr "Eliminar usuario" - -#: mod_irc/mod_irc.erl:201 mod_irc/mod_irc_odbc.erl:196 -#: mod_muc/mod_muc.erl:336 mod_muc/mod_muc_odbc.erl:342 -msgid "Access denied by service policy" -msgstr "Acceso denegado por la política del servicio" - -#: mod_irc/mod_irc.erl:401 mod_irc/mod_irc_odbc.erl:398 -msgid "IRC Transport" -msgstr "Transporte de IRC" - -#: mod_irc/mod_irc.erl:428 mod_irc/mod_irc_odbc.erl:425 -msgid "ejabberd IRC module" -msgstr "Módulo de IRC para ejabberd" - -#: mod_irc/mod_irc.erl:559 mod_irc/mod_irc_odbc.erl:568 -msgid "You need an x:data capable client to configure mod_irc settings" -msgstr "" -"Necesitas un cliente con soporte de x:data para configurar las opciones de " -"mod_irc" - -#: mod_irc/mod_irc.erl:566 mod_irc/mod_irc_odbc.erl:575 -msgid "Registration in mod_irc for " -msgstr "Registro en mod_irc para" - -#: mod_irc/mod_irc.erl:571 mod_irc/mod_irc_odbc.erl:580 -msgid "" -"Enter username, encodings, ports and passwords you wish to use for " -"connecting to IRC servers" -msgstr "" -"Introduce el nombre de usuario, codificaciones de carácteres, puertos y " -"contraseñas que quieras usar al conectar en los servidores de IRC" - -#: mod_irc/mod_irc.erl:576 mod_irc/mod_irc_odbc.erl:585 -msgid "IRC Username" -msgstr "Nombre de usuario en IRC" - -#: mod_irc/mod_irc.erl:586 mod_irc/mod_irc_odbc.erl:595 -msgid "" -"If you want to specify different ports, passwords, encodings for IRC " -"servers, fill this list with values in format '{\"irc server\", \"encoding" -"\", port, \"password\"}'. By default this service use \"~s\" encoding, port " -"~p, empty password." -msgstr "" -"Si quieres especificar distintos codificaciones de carácteres, contraseñas o " -"puertos para cada servidor IRC rellena esta lista con valores en el formato " -"'{\"servidor irc\", \"codificación\", \"puerto\", \"contrasela\"}'. Este " -"servicio usa por defecto la codificación \"~s\", puerto ~p, sin contraseña." - -#: mod_irc/mod_irc.erl:598 mod_irc/mod_irc_odbc.erl:607 -msgid "" -"Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef." -"net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]." -msgstr "" -"Ejemplo: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef." -"net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]." - -#: mod_irc/mod_irc.erl:603 mod_irc/mod_irc_odbc.erl:612 -msgid "Connections parameters" -msgstr "Parámetros de conexiones" - -#: mod_irc/mod_irc.erl:728 mod_irc/mod_irc_odbc.erl:757 -msgid "Join IRC channel" -msgstr "Entrar en canal IRC" - -#: mod_irc/mod_irc.erl:732 mod_irc/mod_irc_odbc.erl:761 -msgid "IRC channel (don't put the first #)" -msgstr "Canal IRC (no pongas el # del principio)" - -#: mod_irc/mod_irc.erl:737 mod_irc/mod_irc_odbc.erl:766 -msgid "IRC server" -msgstr "Servidor IRC" - -#: mod_irc/mod_irc.erl:770 mod_irc/mod_irc.erl:774 -#: mod_irc/mod_irc_odbc.erl:799 mod_irc/mod_irc_odbc.erl:803 -msgid "Join the IRC channel here." -msgstr "Entrar en el canal de IRC aquí" - -#: mod_irc/mod_irc.erl:778 mod_irc/mod_irc_odbc.erl:807 -msgid "Join the IRC channel in this Jabber ID: ~s" -msgstr "Entra en el canal de IRC en esta dirección Jabber: ~s" - -#: mod_irc/mod_irc.erl:863 mod_irc/mod_irc_odbc.erl:904 -msgid "IRC settings" -msgstr "Opciones de IRC" - -#: mod_irc/mod_irc.erl:868 mod_irc/mod_irc_odbc.erl:909 -msgid "" -"Enter username and encodings you wish to use for connecting to IRC servers. " -"Press 'Next' to get more fields to fill in. Press 'Complete' to save " -"settings." -msgstr "" -"Introduce el nombre de usuario y codificaciones de carácteres que quieras " -"usar al conectar en los servidores de IRC. Pulsa Siguiente para conseguir " -"más campos en el formulario. Pulsa Completar para guardar las opciones." - -#: mod_irc/mod_irc.erl:874 mod_irc/mod_irc_odbc.erl:915 -msgid "IRC username" -msgstr "Nombre de usuario en IRC" - -#: mod_irc/mod_irc.erl:923 mod_irc/mod_irc_odbc.erl:964 -msgid "Password ~b" -msgstr "Contraseña ~b" - -#: mod_irc/mod_irc.erl:928 mod_irc/mod_irc_odbc.erl:969 -msgid "Port ~b" -msgstr "Puerto ~b" - -#: mod_irc/mod_irc.erl:933 mod_irc/mod_irc_odbc.erl:974 -msgid "Encoding for server ~b" -msgstr "Codificación del servidor ~b" - -#: mod_irc/mod_irc.erl:942 mod_irc/mod_irc_odbc.erl:983 -msgid "Server ~b" -msgstr "Servidor ~b" - -#: mod_muc/mod_muc.erl:449 mod_muc/mod_muc_odbc.erl:456 -msgid "Only service administrators are allowed to send service messages" -msgstr "" -"Solo los administradores del servicio tienen permiso para enviar mensajes de " -"servicio" - -#: mod_muc/mod_muc.erl:493 mod_muc/mod_muc_odbc.erl:500 -msgid "Room creation is denied by service policy" -msgstr "Se te ha denegado crear la sala por política del servicio" - -#: mod_muc/mod_muc.erl:500 mod_muc/mod_muc_odbc.erl:507 -msgid "Conference room does not exist" -msgstr "La sala de conferencias no existe" - -#: mod_muc/mod_muc.erl:582 mod_muc/mod_muc_odbc.erl:595 -msgid "Chatrooms" -msgstr "Salas de charla" - -#: mod_muc/mod_muc.erl:712 mod_muc/mod_muc_odbc.erl:729 -msgid "You need a client that supports x:data to register the nickname" -msgstr "" -"Necesitas un cliente con soporte de x:data para poder registrar el apodo" - -#: mod_muc/mod_muc.erl:718 mod_muc/mod_muc_odbc.erl:735 -msgid "Nickname Registration at " -msgstr "Registro del apodo en " - -#: mod_muc/mod_muc.erl:722 mod_muc/mod_muc_odbc.erl:739 -msgid "Enter nickname you want to register" -msgstr "Introduce el apodo que quieras registrar" - -#: mod_muc/mod_muc.erl:723 mod_muc/mod_muc_odbc.erl:740 -#: mod_muc/mod_muc_room.erl:3810 mod_roster.erl:944 mod_roster_odbc.erl:1061 -#: mod_vcard.erl:364 mod_vcard.erl:477 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:453 -msgid "Nickname" -msgstr "Apodo" - -#: mod_muc/mod_muc.erl:762 mod_muc/mod_muc_odbc.erl:784 -#: mod_muc/mod_muc_room.erl:1064 mod_muc/mod_muc_room.erl:1787 -msgid "That nickname is registered by another person" -msgstr "El apodo ya está registrado por otra persona" - -#: mod_muc/mod_muc.erl:788 mod_muc/mod_muc_odbc.erl:811 -msgid "You must fill in field \"Nickname\" in the form" -msgstr "Debes rellenar el campo \"Apodo\" en el formulario" - -#: mod_muc/mod_muc.erl:808 mod_muc/mod_muc_odbc.erl:831 -msgid "ejabberd MUC module" -msgstr "Módulo de MUC para ejabberd" - -#: mod_muc/mod_muc_log.erl:374 mod_muc/mod_muc_log.erl:381 -msgid "Chatroom configuration modified" -msgstr "Configuración de la sala modificada" - -#: mod_muc/mod_muc_log.erl:384 -msgid "joins the room" -msgstr "entra en la sala" - -#: mod_muc/mod_muc_log.erl:387 mod_muc/mod_muc_log.erl:390 -msgid "leaves the room" -msgstr "sale de la sala" - -#: mod_muc/mod_muc_log.erl:393 mod_muc/mod_muc_log.erl:396 -msgid "has been banned" -msgstr "ha sido bloqueado" - -#: mod_muc/mod_muc_log.erl:399 mod_muc/mod_muc_log.erl:402 -msgid "has been kicked" -msgstr "ha sido expulsado" - -#: mod_muc/mod_muc_log.erl:405 -msgid "has been kicked because of an affiliation change" -msgstr "ha sido expulsado por un cambio de su afiliación" - -#: mod_muc/mod_muc_log.erl:408 -msgid "has been kicked because the room has been changed to members-only" -msgstr "ha sido expulsado porque la sala es ahora solo para miembros" - -#: mod_muc/mod_muc_log.erl:411 -msgid "has been kicked because of a system shutdown" -msgstr "ha sido expulsado porque el sistema se va a detener" - -#: mod_muc/mod_muc_log.erl:414 -msgid "is now known as" -msgstr "se cambia el nombre a" - -#: mod_muc/mod_muc_log.erl:417 mod_muc/mod_muc_log.erl:688 -#: mod_muc/mod_muc_room.erl:2393 -msgid " has set the subject to: " -msgstr " ha puesto el asunto: " - -#: mod_muc/mod_muc_log.erl:452 -msgid "Chatroom is created" -msgstr "Se ha creado la sala" - -#: mod_muc/mod_muc_log.erl:453 -msgid "Chatroom is destroyed" -msgstr "Se ha destruido la sala" - -#: mod_muc/mod_muc_log.erl:454 -msgid "Chatroom is started" -msgstr "Se ha iniciado la sala" - -#: mod_muc/mod_muc_log.erl:455 -msgid "Chatroom is stopped" -msgstr "Se ha detenido la sala" - -#: mod_muc/mod_muc_log.erl:459 -msgid "Monday" -msgstr "lunes" - -#: mod_muc/mod_muc_log.erl:460 -msgid "Tuesday" -msgstr "martes" - -#: mod_muc/mod_muc_log.erl:461 -msgid "Wednesday" -msgstr "miércoles" - -#: mod_muc/mod_muc_log.erl:462 -msgid "Thursday" -msgstr "jueves" - -#: mod_muc/mod_muc_log.erl:463 -msgid "Friday" -msgstr "viernes" - -#: mod_muc/mod_muc_log.erl:464 -msgid "Saturday" -msgstr "sábado" - -#: mod_muc/mod_muc_log.erl:465 -msgid "Sunday" -msgstr "domingo" - -#: mod_muc/mod_muc_log.erl:469 -msgid "January" -msgstr "enero" - -#: mod_muc/mod_muc_log.erl:470 -msgid "February" -msgstr "febrero" - -#: mod_muc/mod_muc_log.erl:471 -msgid "March" -msgstr "marzo" - -#: mod_muc/mod_muc_log.erl:472 -msgid "April" -msgstr "abril" - -#: mod_muc/mod_muc_log.erl:473 -msgid "May" -msgstr "mayo" - -#: mod_muc/mod_muc_log.erl:474 -msgid "June" -msgstr "junio" - -#: mod_muc/mod_muc_log.erl:475 -msgid "July" -msgstr "julio" - -#: mod_muc/mod_muc_log.erl:476 -msgid "August" -msgstr "agosto" - -#: mod_muc/mod_muc_log.erl:477 -msgid "September" -msgstr "septiembre" - -#: mod_muc/mod_muc_log.erl:478 -msgid "October" -msgstr "octubre" - -#: mod_muc/mod_muc_log.erl:479 -msgid "November" -msgstr "noviembre" - -#: mod_muc/mod_muc_log.erl:480 -msgid "December" -msgstr "diciembre" - -#: mod_muc/mod_muc_log.erl:750 -msgid "Room Configuration" -msgstr "Configuración de la sala" - -#: mod_muc/mod_muc_log.erl:759 -msgid "Room Occupants" -msgstr "Ocupantes de la sala" - -#: mod_muc/mod_muc_room.erl:174 -msgid "Traffic rate limit is exceeded" -msgstr "Se ha exedido el límite de tráfico" - -#: mod_muc/mod_muc_room.erl:246 -msgid "" -"This participant is kicked from the room because he sent an error message" -msgstr "" -"Este participante ha sido expulsado de la sala porque envió un mensaje de " -"error" - -#: mod_muc/mod_muc_room.erl:255 -msgid "It is not allowed to send private messages to the conference" -msgstr "Impedir el envio de mensajes privados a la sala" - -#: mod_muc/mod_muc_room.erl:332 -msgid "Please, wait for a while before sending new voice request" -msgstr "Por favor, espera un poco antes de enviar otra petición de voz" - -#: mod_muc/mod_muc_room.erl:347 -msgid "Voice requests are disabled in this conference" -msgstr "Las peticiones de voz están desactivadas en esta sala" - -#: mod_muc/mod_muc_room.erl:364 -msgid "Failed to extract JID from your voice request approval" -msgstr "Fallo al extraer el Jabber ID de tu aprobación de petición de voz" - -#: mod_muc/mod_muc_room.erl:393 -msgid "Only moderators can approve voice requests" -msgstr "Solo los moderadores pueden aprobar peticiones de voz" - -#: mod_muc/mod_muc_room.erl:408 -msgid "Improper message type" -msgstr "Tipo de mensaje incorrecto" - -#: mod_muc/mod_muc_room.erl:518 -msgid "" -"This participant is kicked from the room because he sent an error message to " -"another participant" -msgstr "" -"Este participante ha sido expulsado de la sala porque envió un mensaje de " -"error a otro participante" - -#: mod_muc/mod_muc_room.erl:531 -msgid "It is not allowed to send private messages of type \"groupchat\"" -msgstr "No está permitido enviar mensajes privados del tipo \"groupchat\"" - -#: mod_muc/mod_muc_room.erl:543 mod_muc/mod_muc_room.erl:611 -msgid "Recipient is not in the conference room" -msgstr "El receptor no está en la sala de conferencia" - -#: mod_muc/mod_muc_room.erl:564 mod_muc/mod_muc_room.erl:585 -msgid "It is not allowed to send private messages" -msgstr "No está permitido enviar mensajes privados" - -#: mod_muc/mod_muc_room.erl:576 mod_muc/mod_muc_room.erl:956 -#: mod_muc/mod_muc_room.erl:4040 -msgid "Only occupants are allowed to send messages to the conference" -msgstr "Solo los ocupantes pueden enviar mensajes a la sala" - -#: mod_muc/mod_muc_room.erl:634 -msgid "Only occupants are allowed to send queries to the conference" -msgstr "Solo los ocupantes pueden enviar solicitudes a la sala" - -#: mod_muc/mod_muc_room.erl:646 -msgid "Queries to the conference members are not allowed in this room" -msgstr "En esta sala no se permiten solicitudes a los miembros de la sala" - -#: mod_muc/mod_muc_room.erl:932 -msgid "" -"Only moderators and participants are allowed to change the subject in this " -"room" -msgstr "" -"Solo los moderadores y participantes pueden cambiar el asunto de esta sala" - -#: mod_muc/mod_muc_room.erl:937 -msgid "Only moderators are allowed to change the subject in this room" -msgstr "Solo los moderadores pueden cambiar el asunto de esta sala" - -#: mod_muc/mod_muc_room.erl:947 -msgid "Visitors are not allowed to send messages to all occupants" -msgstr "Los visitantes no pueden enviar mensajes a todos los ocupantes" - -#: mod_muc/mod_muc_room.erl:1021 -msgid "" -"This participant is kicked from the room because he sent an error presence" -msgstr "" -"Este participante ha sido expulsado de la sala porque envió una presencia de " -"error" - -#: mod_muc/mod_muc_room.erl:1040 -msgid "Visitors are not allowed to change their nicknames in this room" -msgstr "Los visitantes no tienen permitido cambiar sus apodos en esta sala" - -#: mod_muc/mod_muc_room.erl:1053 mod_muc/mod_muc_room.erl:1779 -msgid "That nickname is already in use by another occupant" -msgstr "Ese apodo ya está siendo usado por otro ocupante" - -#: mod_muc/mod_muc_room.erl:1768 -msgid "You have been banned from this room" -msgstr "Has sido bloqueado en esta sala" - -#: mod_muc/mod_muc_room.erl:1771 -msgid "Membership is required to enter this room" -msgstr "Necesitas ser miembro de esta sala para poder entrar" - -#: mod_muc/mod_muc_room.erl:1807 -msgid "This room is not anonymous" -msgstr "Sala no anónima" - -#: mod_muc/mod_muc_room.erl:1833 -msgid "A password is required to enter this room" -msgstr "Se necesita contraseña para entrar en esta sala" - -#: mod_muc/mod_muc_room.erl:1855 mod_register.erl:246 -msgid "Too many CAPTCHA requests" -msgstr "Demasiadas peticiones de CAPTCHA" - -#: mod_muc/mod_muc_room.erl:1864 mod_register.erl:251 -msgid "Unable to generate a CAPTCHA" -msgstr "No se pudo generar un CAPTCHA" - -#: mod_muc/mod_muc_room.erl:1874 -msgid "Incorrect password" -msgstr "Contraseña incorrecta" - -#: mod_muc/mod_muc_room.erl:2448 -msgid "Administrator privileges required" -msgstr "Se necesita privilegios de administrador" - -#: mod_muc/mod_muc_room.erl:2463 -msgid "Moderator privileges required" -msgstr "Se necesita privilegios de moderador" - -#: mod_muc/mod_muc_room.erl:2619 -msgid "Jabber ID ~s is invalid" -msgstr "El Jabber ID ~s no es válido" - -#: mod_muc/mod_muc_room.erl:2633 -msgid "Nickname ~s does not exist in the room" -msgstr "El apodo ~s no existe en la sala" - -#: mod_muc/mod_muc_room.erl:2659 mod_muc/mod_muc_room.erl:3049 -msgid "Invalid affiliation: ~s" -msgstr "Afiliación no válida: ~s" - -#: mod_muc/mod_muc_room.erl:2713 -msgid "Invalid role: ~s" -msgstr "Rol no válido: ~s" - -#: mod_muc/mod_muc_room.erl:3026 mod_muc/mod_muc_room.erl:3062 -msgid "Owner privileges required" -msgstr "Se requieren privilegios de propietario de la sala" - -#: mod_muc/mod_muc_room.erl:3195 -msgid "Configuration of room ~s" -msgstr "Configuración para la sala ~s" - -#: mod_muc/mod_muc_room.erl:3200 -msgid "Room title" -msgstr "Título de la sala" - -#: mod_muc/mod_muc_room.erl:3203 mod_muc/mod_muc_room.erl:3692 -msgid "Room description" -msgstr "Descripción de la sala" - -#: mod_muc/mod_muc_room.erl:3210 -msgid "Make room persistent" -msgstr "Sala permanente" - -#: mod_muc/mod_muc_room.erl:3215 -msgid "Make room public searchable" -msgstr "Sala públicamente visible" - -#: mod_muc/mod_muc_room.erl:3218 -msgid "Make participants list public" -msgstr "La lista de participantes es pública" - -#: mod_muc/mod_muc_room.erl:3221 -msgid "Make room password protected" -msgstr "Proteger la sala con contraseña" - -#: mod_muc/mod_muc_room.erl:3232 -msgid "Maximum Number of Occupants" -msgstr "Número máximo de ocupantes" - -#: mod_muc/mod_muc_room.erl:3239 -msgid "No limit" -msgstr "Sin límite" - -#: mod_muc/mod_muc_room.erl:3250 -msgid "Present real Jabber IDs to" -msgstr "Los Jabber ID reales pueden verlos" - -#: mod_muc/mod_muc_room.erl:3258 mod_muc/mod_muc_room.erl:3292 -msgid "moderators only" -msgstr "solo moderadores" - -#: mod_muc/mod_muc_room.erl:3260 mod_muc/mod_muc_room.erl:3294 -msgid "anyone" -msgstr "cualquiera" - -#: mod_muc/mod_muc_room.erl:3262 -msgid "Make room members-only" -msgstr "Sala sólo para miembros" - -#: mod_muc/mod_muc_room.erl:3265 -msgid "Make room moderated" -msgstr "Sala moderada" - -#: mod_muc/mod_muc_room.erl:3268 -msgid "Default users as participants" -msgstr "Los usuarios son participantes por defecto" - -#: mod_muc/mod_muc_room.erl:3271 -msgid "Allow users to change the subject" -msgstr "Permitir a los usuarios cambiar el asunto" - -#: mod_muc/mod_muc_room.erl:3274 -msgid "Allow users to send private messages" -msgstr "Permitir a los usuarios enviar mensajes privados" - -#: mod_muc/mod_muc_room.erl:3279 -msgid "Allow visitors to send private messages to" -msgstr "Permitir a los visitantes enviar mensajes privados a" - -#: mod_muc/mod_muc_room.erl:3290 -msgid "nobody" -msgstr "nadie" - -#: mod_muc/mod_muc_room.erl:3296 -msgid "Allow users to query other users" -msgstr "Permitir a los usuarios consultar a otros usuarios" - -#: mod_muc/mod_muc_room.erl:3299 -msgid "Allow users to send invites" -msgstr "Permitir a los usuarios enviar invitaciones" - -#: mod_muc/mod_muc_room.erl:3302 -msgid "Allow visitors to send status text in presence updates" -msgstr "" -"Permitir a los visitantes enviar texto de estado en las actualizaciones de " -"presencia" - -#: mod_muc/mod_muc_room.erl:3305 -msgid "Allow visitors to change nickname" -msgstr "Permitir a los visitantes cambiarse el apodo" - -#: mod_muc/mod_muc_room.erl:3308 -msgid "Allow visitors to send voice requests" -msgstr "Permitir a los visitantes enviar peticiones de voz" - -#: mod_muc/mod_muc_room.erl:3311 -msgid "Minimum interval between voice requests (in seconds)" -msgstr "Intervalo mínimo entre peticiones de voz (en segundos)" - -#: mod_muc/mod_muc_room.erl:3317 -msgid "Make room CAPTCHA protected" -msgstr "Proteger la sala con CAPTCHA" - -#: mod_muc/mod_muc_room.erl:3322 -msgid "Exclude Jabber IDs from CAPTCHA challenge" -msgstr "Excluir Jabber IDs de las pruebas de CAPTCHA" - -#: mod_muc/mod_muc_room.erl:3329 -msgid "Enable logging" -msgstr "Guardar históricos" - -#: mod_muc/mod_muc_room.erl:3337 -msgid "You need an x:data capable client to configure room" -msgstr "Necesitas un cliente con soporte de x:data para configurar la sala" - -#: mod_muc/mod_muc_room.erl:3694 -msgid "Number of occupants" -msgstr "Número de ocupantes" - -#: mod_muc/mod_muc_room.erl:3750 -msgid "private, " -msgstr "privado" - -#: mod_muc/mod_muc_room.erl:3799 -msgid "Voice request" -msgstr "Petición de voz" - -#: mod_muc/mod_muc_room.erl:3803 -msgid "Either approve or decline the voice request." -msgstr "Aprueba o rechaza la petición de voz." - -#: mod_muc/mod_muc_room.erl:3809 -msgid "User JID" -msgstr "Jabber ID del usuario" - -#: mod_muc/mod_muc_room.erl:3811 -msgid "Grant voice to this person?" -msgstr "¿Conceder voz a esta persona?" - -#: mod_muc/mod_muc_room.erl:3960 -msgid "~s invites you to the room ~s" -msgstr "~s te invita a la sala ~s" - -#: mod_muc/mod_muc_room.erl:3969 -msgid "the password is" -msgstr "la contraseña es" - -#: mod_offline.erl:510 mod_offline_odbc.erl:352 -msgid "" -"Your contact offline message queue is full. The message has been discarded." -msgstr "" -"Tu cola de mensajes diferidos de contactos está llena. El mensaje se ha " -"descartado." - -#: mod_offline.erl:560 mod_offline_odbc.erl:408 -msgid "~s's Offline Messages Queue" -msgstr "Cola de mensajes diferidos de ~s" - -#: mod_offline.erl:563 mod_offline_odbc.erl:411 mod_roster.erl:987 -#: mod_roster_odbc.erl:1104 mod_shared_roster.erl:901 -#: mod_shared_roster.erl:1010 mod_shared_roster_odbc.erl:930 -#: mod_shared_roster_odbc.erl:1039 web/ejabberd_web_admin.erl:833 -#: web/ejabberd_web_admin.erl:874 web/ejabberd_web_admin.erl:942 -#: web/ejabberd_web_admin.erl:978 web/ejabberd_web_admin.erl:1019 -#: web/ejabberd_web_admin.erl:1508 web/ejabberd_web_admin.erl:1760 -#: web/ejabberd_web_admin.erl:1931 web/ejabberd_web_admin.erl:1963 -#: web/ejabberd_web_admin.erl:2031 web/ejabberd_web_admin.erl:2135 -#: web/ejabberd_web_admin.erl:2160 web/ejabberd_web_admin.erl:2248 -msgid "Submitted" -msgstr "Enviado" - -#: mod_offline.erl:571 -msgid "Time" -msgstr "Fecha" - -#: mod_offline.erl:572 -msgid "From" -msgstr "De" - -#: mod_offline.erl:573 -msgid "To" -msgstr "Para" - -#: mod_offline.erl:574 mod_offline_odbc.erl:419 -msgid "Packet" -msgstr "Paquete" - -#: mod_offline.erl:587 mod_offline_odbc.erl:432 mod_shared_roster.erl:908 -#: mod_shared_roster_odbc.erl:937 web/ejabberd_web_admin.erl:882 -#: web/ejabberd_web_admin.erl:986 -msgid "Delete Selected" -msgstr "Eliminar los seleccionados" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -msgid "Offline Messages:" -msgstr "Mensajes diferidos:" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -msgid "Remove All Offline Messages" -msgstr "Borrar todos los mensajes diferidos" - -#: mod_proxy65/mod_proxy65_service.erl:213 -msgid "ejabberd SOCKS5 Bytestreams module" -msgstr "Módulo SOCKS5 Bytestreams para ejabberd" - -#: mod_pubsub/mod_pubsub.erl:1120 mod_pubsub/mod_pubsub_odbc.erl:929 -msgid "Publish-Subscribe" -msgstr "Servicio de Publicar-Subscribir" - -#: mod_pubsub/mod_pubsub.erl:1214 mod_pubsub/mod_pubsub_odbc.erl:1025 -msgid "ejabberd Publish-Subscribe module" -msgstr "Módulo de Publicar-Subscribir de ejabberd" - -#: mod_pubsub/mod_pubsub.erl:1497 mod_pubsub/mod_pubsub_odbc.erl:1312 -msgid "PubSub subscriber request" -msgstr "Petición de subscriptor de PubSub" - -#: mod_pubsub/mod_pubsub.erl:1499 mod_pubsub/mod_pubsub_odbc.erl:1314 -msgid "Choose whether to approve this entity's subscription." -msgstr "Decidir si aprobar la subscripción de esta entidad." - -#: mod_pubsub/mod_pubsub.erl:1505 mod_pubsub/mod_pubsub_odbc.erl:1320 -msgid "Node ID" -msgstr "Nodo ID" - -#: mod_pubsub/mod_pubsub.erl:1510 mod_pubsub/mod_pubsub_odbc.erl:1325 -msgid "Subscriber Address" -msgstr "Dirección del subscriptor" - -#: mod_pubsub/mod_pubsub.erl:1516 mod_pubsub/mod_pubsub_odbc.erl:1331 -msgid "Allow this Jabber ID to subscribe to this pubsub node?" -msgstr "¿Deseas permitir a este Jabber ID que se subscriba a este nodo PubSub?" - -#: mod_pubsub/mod_pubsub.erl:3474 mod_pubsub/mod_pubsub_odbc.erl:3287 -msgid "Deliver payloads with event notifications" -msgstr "Enviar contenidos junto con las notificaciones de eventos" - -#: mod_pubsub/mod_pubsub.erl:3475 mod_pubsub/mod_pubsub_odbc.erl:3288 -msgid "Deliver event notifications" -msgstr "Entregar notificaciones de eventos" - -#: mod_pubsub/mod_pubsub.erl:3476 mod_pubsub/mod_pubsub_odbc.erl:3289 -msgid "Notify subscribers when the node configuration changes" -msgstr "Notificar subscriptores cuando cambia la configuración del nodo" - -#: mod_pubsub/mod_pubsub.erl:3477 mod_pubsub/mod_pubsub_odbc.erl:3290 -msgid "Notify subscribers when the node is deleted" -msgstr "Notificar subscriptores cuando el nodo se borra" - -#: mod_pubsub/mod_pubsub.erl:3478 mod_pubsub/mod_pubsub_odbc.erl:3291 -msgid "Notify subscribers when items are removed from the node" -msgstr "Notificar subscriptores cuando los elementos se borran del nodo" - -#: mod_pubsub/mod_pubsub.erl:3479 mod_pubsub/mod_pubsub_odbc.erl:3292 -msgid "Persist items to storage" -msgstr "Persistir elementos al almacenar" - -#: mod_pubsub/mod_pubsub.erl:3480 mod_pubsub/mod_pubsub_odbc.erl:3293 -msgid "A friendly name for the node" -msgstr "Un nombre sencillo para el nodo" - -#: mod_pubsub/mod_pubsub.erl:3481 mod_pubsub/mod_pubsub_odbc.erl:3294 -msgid "Max # of items to persist" -msgstr "Máximo # de elementos que persisten" - -#: mod_pubsub/mod_pubsub.erl:3482 mod_pubsub/mod_pubsub_odbc.erl:3295 -msgid "Whether to allow subscriptions" -msgstr "Permitir subscripciones" - -#: mod_pubsub/mod_pubsub.erl:3483 mod_pubsub/mod_pubsub_odbc.erl:3296 -msgid "Specify the access model" -msgstr "Especifica el modelo de acceso" - -#: mod_pubsub/mod_pubsub.erl:3486 mod_pubsub/mod_pubsub_odbc.erl:3299 -msgid "Roster groups allowed to subscribe" -msgstr "Grupos de contactos que pueden suscribirse" - -#: mod_pubsub/mod_pubsub.erl:3487 mod_pubsub/mod_pubsub_odbc.erl:3300 -msgid "Specify the publisher model" -msgstr "Especificar el modelo del publicante" - -#: mod_pubsub/mod_pubsub.erl:3489 mod_pubsub/mod_pubsub_odbc.erl:3302 -msgid "Purge all items when the relevant publisher goes offline" -msgstr "Borra todos los elementos cuando el publicador relevante se desconecta" - -#: mod_pubsub/mod_pubsub.erl:3490 mod_pubsub/mod_pubsub_odbc.erl:3303 -msgid "Specify the event message type" -msgstr "Especifica el tipo del mensaje de evento" - -#: mod_pubsub/mod_pubsub.erl:3492 mod_pubsub/mod_pubsub_odbc.erl:3305 -msgid "Max payload size in bytes" -msgstr "Máximo tamaño del contenido en bytes" - -#: mod_pubsub/mod_pubsub.erl:3493 mod_pubsub/mod_pubsub_odbc.erl:3306 -msgid "When to send the last published item" -msgstr "Cuando enviar el último elemento publicado" - -#: mod_pubsub/mod_pubsub.erl:3495 mod_pubsub/mod_pubsub_odbc.erl:3308 -msgid "Only deliver notifications to available users" -msgstr "Solo enviar notificaciones a los usuarios disponibles" - -#: mod_pubsub/mod_pubsub.erl:3496 mod_pubsub/mod_pubsub_odbc.erl:3309 -msgid "The collections with which a node is affiliated" -msgstr "Las colecciones a las que un nodo está afiliado" - -#: mod_register.erl:193 -msgid "The CAPTCHA verification has failed" -msgstr "La verificación de CAPTCHA ha fallado" - -#: mod_register.erl:220 -msgid "You need a client that supports x:data and CAPTCHA to register" -msgstr "Necesitas un cliente con soporte de x:data y CAPTCHA para registrarte" - -#: mod_register.erl:226 mod_register.erl:265 -msgid "Choose a username and password to register with this server" -msgstr "" -"Escoge un nombre de usuario y contraseña para registrarte en este servidor" - -#: mod_register.erl:230 mod_vcard.erl:364 mod_vcard_odbc.erl:342 -#: web/ejabberd_web_admin.erl:1515 web/ejabberd_web_admin.erl:1572 -msgid "User" -msgstr "Usuario" - -#: mod_register.erl:316 mod_register.erl:361 -msgid "The password is too weak" -msgstr "La contraseña es demasiado débil" - -#: mod_register.erl:365 -msgid "Users are not allowed to register accounts so quickly" -msgstr "Los usuarios no tienen permitido crear cuentas con tanta rapidez" - -#: mod_roster.erl:938 mod_roster_odbc.erl:1055 web/ejabberd_web_admin.erl:1701 -#: web/ejabberd_web_admin.erl:1886 web/ejabberd_web_admin.erl:1897 -#: web/ejabberd_web_admin.erl:2219 -msgid "None" -msgstr "Ninguno" - -#: mod_roster.erl:945 mod_roster_odbc.erl:1062 -msgid "Subscription" -msgstr "Subscripción" - -#: mod_roster.erl:946 mod_roster_odbc.erl:1063 -msgid "Pending" -msgstr "Pendiente" - -#: mod_roster.erl:947 mod_roster_odbc.erl:1064 -msgid "Groups" -msgstr "Grupos" - -#: mod_roster.erl:974 mod_roster_odbc.erl:1091 -msgid "Validate" -msgstr "Validar" - -#: mod_roster.erl:982 mod_roster_odbc.erl:1099 -msgid "Remove" -msgstr "Borrar" - -#: mod_roster.erl:985 mod_roster_odbc.erl:1102 -msgid "Roster of " -msgstr "Lista de contactos de " - -#: mod_roster.erl:988 mod_roster_odbc.erl:1105 mod_shared_roster.erl:902 -#: mod_shared_roster.erl:1011 mod_shared_roster_odbc.erl:931 -#: mod_shared_roster_odbc.erl:1040 web/ejabberd_web_admin.erl:834 -#: web/ejabberd_web_admin.erl:875 web/ejabberd_web_admin.erl:943 -#: web/ejabberd_web_admin.erl:979 web/ejabberd_web_admin.erl:1020 -#: web/ejabberd_web_admin.erl:1509 web/ejabberd_web_admin.erl:1761 -#: web/ejabberd_web_admin.erl:1932 web/ejabberd_web_admin.erl:2136 -#: web/ejabberd_web_admin.erl:2161 -msgid "Bad format" -msgstr "Mal formato" - -#: mod_roster.erl:995 mod_roster_odbc.erl:1112 -msgid "Add Jabber ID" -msgstr "Añadir Jabber ID" - -#: mod_roster.erl:1094 mod_roster_odbc.erl:1211 -msgid "Roster" -msgstr "Lista de contactos" - -#: mod_shared_roster.erl:857 mod_shared_roster.erl:899 -#: mod_shared_roster.erl:1007 mod_shared_roster_odbc.erl:886 -#: mod_shared_roster_odbc.erl:928 mod_shared_roster_odbc.erl:1036 -msgid "Shared Roster Groups" -msgstr "Grupos Compartidos" - -#: mod_shared_roster.erl:895 mod_shared_roster_odbc.erl:924 -#: web/ejabberd_web_admin.erl:1365 web/ejabberd_web_admin.erl:2461 -msgid "Add New" -msgstr "Añadir nuevo" - -#: mod_shared_roster.erl:978 mod_shared_roster_odbc.erl:1007 -msgid "Name:" -msgstr "Nombre:" - -#: mod_shared_roster.erl:983 mod_shared_roster_odbc.erl:1012 -msgid "Description:" -msgstr "Descripción:" - -#: mod_shared_roster.erl:991 mod_shared_roster_odbc.erl:1020 -msgid "Members:" -msgstr "Miembros:" - -#: mod_shared_roster.erl:999 mod_shared_roster_odbc.erl:1028 -msgid "Displayed Groups:" -msgstr "Mostrar grupos:" - -#: mod_shared_roster.erl:1008 mod_shared_roster_odbc.erl:1037 -msgid "Group " -msgstr "Grupo " - -#: mod_shared_roster.erl:1017 mod_shared_roster_odbc.erl:1046 -#: web/ejabberd_web_admin.erl:840 web/ejabberd_web_admin.erl:884 -#: web/ejabberd_web_admin.erl:949 web/ejabberd_web_admin.erl:1026 -#: web/ejabberd_web_admin.erl:2017 -msgid "Submit" -msgstr "Enviar" - -#: mod_vcard.erl:165 mod_vcard_ldap.erl:238 mod_vcard_odbc.erl:129 -msgid "Erlang Jabber Server" -msgstr "Servidor Jabber en Erlang" - -#: mod_vcard.erl:364 mod_vcard.erl:478 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:454 -msgid "Birthday" -msgstr "Cumpleaños" - -#: mod_vcard.erl:364 mod_vcard.erl:480 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:456 -msgid "City" -msgstr "Ciudad" - -#: mod_vcard.erl:364 mod_vcard.erl:479 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:455 -msgid "Country" -msgstr "País" - -#: mod_vcard.erl:364 mod_vcard.erl:481 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:457 -msgid "Email" -msgstr "correo" - -#: mod_vcard.erl:364 mod_vcard.erl:476 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:452 -msgid "Family Name" -msgstr "Apellido" - -#: mod_vcard.erl:364 mod_vcard_odbc.erl:342 -msgid "" -"Fill in the form to search for any matching Jabber User (Add * to the end of " -"field to match substring)" -msgstr "" -"Rellena el formulario para buscar usuarios Jabber. Añade * al final de un " -"campo para buscar subcadenas." - -#: mod_vcard.erl:364 mod_vcard.erl:473 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:449 -msgid "Full Name" -msgstr "Nombre completo" - -#: mod_vcard.erl:364 mod_vcard.erl:475 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:451 -msgid "Middle Name" -msgstr "Segundo nombre" - -#: mod_vcard.erl:364 mod_vcard.erl:474 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:450 web/ejabberd_web_admin.erl:2006 -msgid "Name" -msgstr "Nombre" - -#: mod_vcard.erl:364 mod_vcard.erl:482 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:458 -msgid "Organization Name" -msgstr "Nombre de la organización" - -#: mod_vcard.erl:364 mod_vcard.erl:483 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:459 -msgid "Organization Unit" -msgstr "Unidad de la organización" - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "Search users in " -msgstr "Buscar usuarios en " - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "You need an x:data capable client to search" -msgstr "Necesitas un cliente con soporte de x:data para poder buscar" - -#: mod_vcard.erl:389 mod_vcard_ldap.erl:487 mod_vcard_odbc.erl:367 -msgid "vCard User Search" -msgstr "Buscar vCard de usuario" - -#: mod_vcard.erl:445 mod_vcard_ldap.erl:541 mod_vcard_odbc.erl:421 -msgid "ejabberd vCard module" -msgstr "Módulo vCard para ejabberd" - -#: mod_vcard.erl:469 mod_vcard_ldap.erl:551 mod_vcard_odbc.erl:445 -msgid "Search Results for " -msgstr "Buscar resultados por " - -#: mod_vcard_ldap.erl:462 -msgid "Fill in fields to search for any matching Jabber User" -msgstr "Rellena campos para buscar usuarios Jabber que concuerden" - -#: web/ejabberd_web_admin.erl:193 web/ejabberd_web_admin.erl:203 -#: web/ejabberd_web_admin.erl:219 web/ejabberd_web_admin.erl:229 -msgid "Unauthorized" -msgstr "No autorizado" - -#: web/ejabberd_web_admin.erl:286 web/ejabberd_web_admin.erl:303 -msgid "ejabberd Web Admin" -msgstr "ejabberd Web Admin" - -#: web/ejabberd_web_admin.erl:769 web/ejabberd_web_admin.erl:780 -msgid "Administration" -msgstr "Administración" - -#: web/ejabberd_web_admin.erl:878 web/ejabberd_web_admin.erl:982 -msgid "Raw" -msgstr "Crudo" - -#: web/ejabberd_web_admin.erl:1017 -msgid "~s access rule configuration" -msgstr "Configuración de las Regla de Acceso ~s" - -#: web/ejabberd_web_admin.erl:1035 -msgid "Virtual Hosts" -msgstr "Dominios Virtuales" - -#: web/ejabberd_web_admin.erl:1043 web/ejabberd_web_admin.erl:1050 -msgid "Users" -msgstr "Usuarios" - -#: web/ejabberd_web_admin.erl:1078 -msgid "Users Last Activity" -msgstr "Última actividad de los usuarios" - -#: web/ejabberd_web_admin.erl:1080 -msgid "Period: " -msgstr "Periodo: " - -#: web/ejabberd_web_admin.erl:1090 -msgid "Last month" -msgstr "Último mes" - -#: web/ejabberd_web_admin.erl:1091 -msgid "Last year" -msgstr "Último año" - -#: web/ejabberd_web_admin.erl:1092 -msgid "All activity" -msgstr "Toda la actividad" - -#: web/ejabberd_web_admin.erl:1094 -msgid "Show Ordinary Table" -msgstr "Mostrar Tabla Ordinaria" - -#: web/ejabberd_web_admin.erl:1096 -msgid "Show Integral Table" -msgstr "Mostrar Tabla Integral" - -#: web/ejabberd_web_admin.erl:1105 web/ejabberd_web_admin.erl:1939 -msgid "Statistics" -msgstr "Estadísticas" - -#: web/ejabberd_web_admin.erl:1117 -msgid "Not Found" -msgstr "No encontrado" - -#: web/ejabberd_web_admin.erl:1134 -msgid "Node not found" -msgstr "Nodo no encontrado" - -#: web/ejabberd_web_admin.erl:1460 -msgid "Host" -msgstr "Dominio" - -#: web/ejabberd_web_admin.erl:1461 -msgid "Registered Users" -msgstr "Usuarios registrados" - -#: web/ejabberd_web_admin.erl:1573 -msgid "Offline Messages" -msgstr "Mensajes diferidos" - -#: web/ejabberd_web_admin.erl:1574 web/ejabberd_web_admin.erl:1767 -msgid "Last Activity" -msgstr "Última actividad" - -#: web/ejabberd_web_admin.erl:1659 web/ejabberd_web_admin.erl:1675 -msgid "Registered Users:" -msgstr "Usuarios registrados:" - -#: web/ejabberd_web_admin.erl:1661 web/ejabberd_web_admin.erl:1677 -#: web/ejabberd_web_admin.erl:2192 -msgid "Online Users:" -msgstr "Usuarios conectados:" - -#: web/ejabberd_web_admin.erl:1663 -msgid "Outgoing s2s Connections:" -msgstr "Conexiones S2S salientes:" - -#: web/ejabberd_web_admin.erl:1665 -msgid "Outgoing s2s Servers:" -msgstr "Servidores S2S salientes:" - -#: web/ejabberd_web_admin.erl:1734 web/mod_register_web.erl:175 -#: web/mod_register_web.erl:372 web/mod_register_web.erl:381 -#: web/mod_register_web.erl:411 -msgid "Change Password" -msgstr "Cambiar contraseña" - -#: web/ejabberd_web_admin.erl:1758 -msgid "User " -msgstr "Usuario " - -#: web/ejabberd_web_admin.erl:1765 -msgid "Connected Resources:" -msgstr "Recursos conectados:" - -#: web/ejabberd_web_admin.erl:1766 web/mod_register_web.erl:227 -#: web/mod_register_web.erl:519 -msgid "Password:" -msgstr "Contraseña:" - -#: web/ejabberd_web_admin.erl:1828 -msgid "No Data" -msgstr "Sin datos" - -#: web/ejabberd_web_admin.erl:1906 -msgid "Nodes" -msgstr "Nodos" - -#: web/ejabberd_web_admin.erl:1929 web/ejabberd_web_admin.erl:1951 -msgid "Node " -msgstr "Nodo " - -#: web/ejabberd_web_admin.erl:1938 -msgid "Listened Ports" -msgstr "Puertos de escucha" - -#: web/ejabberd_web_admin.erl:1940 web/ejabberd_web_admin.erl:2260 -#: web/ejabberd_web_admin.erl:2448 -msgid "Update" -msgstr "Actualizar" - -#: web/ejabberd_web_admin.erl:1943 web/ejabberd_web_admin.erl:2569 -msgid "Restart" -msgstr "Reiniciar" - -#: web/ejabberd_web_admin.erl:1945 web/ejabberd_web_admin.erl:2571 -msgid "Stop" -msgstr "Detener" - -#: web/ejabberd_web_admin.erl:1959 -msgid "RPC Call Error" -msgstr "Error en la llamada RPC" - -#: web/ejabberd_web_admin.erl:2000 -msgid "Database Tables at " -msgstr "Tablas de la base de datos en " - -#: web/ejabberd_web_admin.erl:2007 -msgid "Storage Type" -msgstr "Tipo de almacenamiento" - -#: web/ejabberd_web_admin.erl:2008 -msgid "Elements" -msgstr "Elementos" - -#: web/ejabberd_web_admin.erl:2009 -msgid "Memory" -msgstr "Memoria" - -#: web/ejabberd_web_admin.erl:2032 web/ejabberd_web_admin.erl:2137 -msgid "Error" -msgstr "Error" - -#: web/ejabberd_web_admin.erl:2034 -msgid "Backup of " -msgstr "Copia de seguridad de " - -#: web/ejabberd_web_admin.erl:2036 -msgid "" -"Please note that these options will only backup the builtin Mnesia database. " -"If you are using the ODBC module, you also need to backup your SQL database " -"separately." -msgstr "" -"Ten en cuenta que estas opciones solo harán copia de seguridad de la base de " -"datos Mnesia embebida. Si estás usando ODBC tendrás que hacer también copia " -"de seguridad de tu base de datos SQL." - -#: web/ejabberd_web_admin.erl:2041 -msgid "Store binary backup:" -msgstr "Guardar copia de seguridad binaria:" - -#: web/ejabberd_web_admin.erl:2045 web/ejabberd_web_admin.erl:2052 -#: web/ejabberd_web_admin.erl:2060 web/ejabberd_web_admin.erl:2067 -#: web/ejabberd_web_admin.erl:2074 web/ejabberd_web_admin.erl:2081 -#: web/ejabberd_web_admin.erl:2088 web/ejabberd_web_admin.erl:2096 -#: web/ejabberd_web_admin.erl:2103 web/ejabberd_web_admin.erl:2110 -msgid "OK" -msgstr "Aceptar" - -#: web/ejabberd_web_admin.erl:2048 -msgid "Restore binary backup immediately:" -msgstr "Restaurar inmediatamente copia de seguridad binaria:" - -#: web/ejabberd_web_admin.erl:2056 -msgid "" -"Restore binary backup after next ejabberd restart (requires less memory):" -msgstr "" -"Restaurar copia de seguridad binaria en el siguiente reinicio de ejabberd " -"(requiere menos memoria que si instantánea):" - -#: web/ejabberd_web_admin.erl:2063 -msgid "Store plain text backup:" -msgstr "Guardar copia de seguridad en texto plano:" - -#: web/ejabberd_web_admin.erl:2070 -msgid "Restore plain text backup immediately:" -msgstr "Restaurar copias de seguridad de texto plano inmediatamente:" - -#: web/ejabberd_web_admin.erl:2077 -msgid "Import users data from a PIEFXIS file (XEP-0227):" -msgstr "Importar usuarios desde un fichero PIEFXIS (XEP-0227):" - -#: web/ejabberd_web_admin.erl:2084 -msgid "Export data of all users in the server to PIEFXIS files (XEP-0227):" -msgstr "" -"Exportar datos de todos los usuarios del servidor a ficheros PIEFXIS " -"(XEP-0227):" - -#: web/ejabberd_web_admin.erl:2091 -msgid "Export data of users in a host to PIEFXIS files (XEP-0227):" -msgstr "" -"Exportar datos de los usuarios de un dominio a ficheros PIEFXIS (XEP-0227):" - -#: web/ejabberd_web_admin.erl:2099 -msgid "Import user data from jabberd14 spool file:" -msgstr "Importar usuario de fichero spool de jabberd14:" - -#: web/ejabberd_web_admin.erl:2106 -msgid "Import users data from jabberd14 spool directory:" -msgstr "Importar usuarios del directorio spool de jabberd14:" - -#: web/ejabberd_web_admin.erl:2132 -msgid "Listened Ports at " -msgstr "Puertos de escucha en " - -#: web/ejabberd_web_admin.erl:2157 -msgid "Modules at " -msgstr "Módulos en " - -#: web/ejabberd_web_admin.erl:2183 -msgid "Statistics of ~p" -msgstr "Estadísticas de ~p" - -#: web/ejabberd_web_admin.erl:2186 -msgid "Uptime:" -msgstr "Tiempo desde el inicio:" - -#: web/ejabberd_web_admin.erl:2189 -msgid "CPU Time:" -msgstr "Tiempo consumido de CPU:" - -#: web/ejabberd_web_admin.erl:2195 -msgid "Transactions Committed:" -msgstr "Transacciones finalizadas:" - -#: web/ejabberd_web_admin.erl:2198 -msgid "Transactions Aborted:" -msgstr "Transacciones abortadas:" - -#: web/ejabberd_web_admin.erl:2201 -msgid "Transactions Restarted:" -msgstr "Transacciones reiniciadas:" - -#: web/ejabberd_web_admin.erl:2204 -msgid "Transactions Logged:" -msgstr "Transacciones registradas:" - -#: web/ejabberd_web_admin.erl:2246 -msgid "Update " -msgstr "Actualizar " - -#: web/ejabberd_web_admin.erl:2254 -msgid "Update plan" -msgstr "Plan de actualización" - -#: web/ejabberd_web_admin.erl:2255 -msgid "Modified modules" -msgstr "Módulos modificados" - -#: web/ejabberd_web_admin.erl:2256 -msgid "Update script" -msgstr "Script de actualización" - -#: web/ejabberd_web_admin.erl:2257 -msgid "Low level update script" -msgstr "Script de actualización a bajo nivel" - -#: web/ejabberd_web_admin.erl:2258 -msgid "Script check" -msgstr "Comprobación de script" - -#: web/ejabberd_web_admin.erl:2426 -msgid "Port" -msgstr "Puerto" - -#: web/ejabberd_web_admin.erl:2427 -msgid "IP" -msgstr "IP" - -#: web/ejabberd_web_admin.erl:2428 -msgid "Protocol" -msgstr "Protocolo" - -#: web/ejabberd_web_admin.erl:2429 web/ejabberd_web_admin.erl:2556 -msgid "Module" -msgstr "Módulo" - -#: web/ejabberd_web_admin.erl:2430 web/ejabberd_web_admin.erl:2557 -msgid "Options" -msgstr "Opciones" - -#: web/ejabberd_web_admin.erl:2450 -msgid "Delete" -msgstr "Eliminar" - -#: web/ejabberd_web_admin.erl:2579 -msgid "Start" -msgstr "Iniciar" - -#: web/mod_register_web.erl:104 -msgid "Your Jabber account was successfully created." -msgstr "Tu cuenta Jabber se ha creado correctamente." - -#: web/mod_register_web.erl:107 -msgid "There was an error creating the account: " -msgstr "Hubo uno error al crear la cuenta:" - -#: web/mod_register_web.erl:115 -msgid "Your Jabber account was successfully deleted." -msgstr "Tu cuenta Jabber se ha borrado correctamente." - -#: web/mod_register_web.erl:118 -msgid "There was an error deleting the account: " -msgstr "Hubo un error borrando la cuenta." - -#: web/mod_register_web.erl:128 -msgid "The password of your Jabber account was successfully changed." -msgstr "La contraseña de tu cuenta Jabber se ha cambiado correctamente." - -#: web/mod_register_web.erl:131 -msgid "There was an error changing the password: " -msgstr "Hubo un error cambiando la contraseña." - -#: web/mod_register_web.erl:163 web/mod_register_web.erl:172 -msgid "Jabber Account Registration" -msgstr "Registro de Cuenta Jabber" - -#: web/mod_register_web.erl:174 web/mod_register_web.erl:192 -#: web/mod_register_web.erl:201 -msgid "Register a Jabber account" -msgstr "Registrar una cuenta Jabber" - -#: web/mod_register_web.erl:176 web/mod_register_web.erl:493 -#: web/mod_register_web.erl:502 -msgid "Unregister a Jabber account" -msgstr "Borrar una cuenta Jabber" - -#: web/mod_register_web.erl:203 -msgid "" -"This page allows to create a Jabber account in this Jabber server. Your JID " -"(Jabber IDentifier) will be of the form: username@server. Please read " -"carefully the instructions to fill correctly the fields." -msgstr "" -"Esta página te permite crear una cuenta Jabber este servidor Jabber. Tu JID " -"(Jabber IDentificador) será de la forma: nombredeusuario@servidor. Por favor " -"lee detenidamente las instrucciones para rellenar correctamente los campos." - -#: web/mod_register_web.erl:212 web/mod_register_web.erl:386 -#: web/mod_register_web.erl:509 -msgid "Username:" -msgstr "Nombre de usuario:" - -#: web/mod_register_web.erl:217 -msgid "This is case insensitive: macbeth is the same that MacBeth and Macbeth." -msgstr "" -"No importa si usas mayúsculas: macbeth es lo mismo que MacBeth y Macbeth." - -#: web/mod_register_web.erl:218 -msgid "Characters not allowed:" -msgstr "Caracteres no permitidos:" - -#: web/mod_register_web.erl:222 web/mod_register_web.erl:391 -#: web/mod_register_web.erl:514 -msgid "Server:" -msgstr "Servidor:" - -#: web/mod_register_web.erl:232 -msgid "" -"Don't tell your password to anybody, not even the administrators of the " -"Jabber server." -msgstr "" -"No le digas tu contraseña a nadie, ni siquiera a los administradores del " -"servidor Jabber." - -#: web/mod_register_web.erl:234 -msgid "You can later change your password using a Jabber client." -msgstr "Puedes cambiar tu contraseña después, usando un cliente Jabber." - -#: web/mod_register_web.erl:235 -msgid "" -"Some Jabber clients can store your password in your computer. Use that " -"feature only if you trust your computer is safe." -msgstr "" -"Algunos clientes Jabber pueden recordar tu contraseña en la máquina. Usa esa " -"opción solo si confías en que la máquina que usas es segura." - -#: web/mod_register_web.erl:237 -msgid "" -"Memorize your password, or write it in a paper placed in a safe place. In " -"Jabber there isn't an automated way to recover your password if you forget " -"it." -msgstr "" -"Memoriza tu contraseña, o apúntala en un papel en un lugar seguro. En Jabber " -"no hay un método automatizado para recuperar la contraseña si la olvidas." - -#: web/mod_register_web.erl:242 web/mod_register_web.erl:406 -msgid "Password Verification:" -msgstr "Verificación de la contraseña:" - -#: web/mod_register_web.erl:250 -msgid "Register" -msgstr "Registrar" - -#: web/mod_register_web.erl:396 -msgid "Old Password:" -msgstr "Contraseña antigua:" - -#: web/mod_register_web.erl:401 -msgid "New Password:" -msgstr "Nueva contraseña:" - -#: web/mod_register_web.erl:504 -msgid "This page allows to unregister a Jabber account in this Jabber server." -msgstr "" -"Esta página te permite borrar tu cuenta Jabber en este servidor Jabber." - -#: web/mod_register_web.erl:524 -msgid "Unregister" -msgstr "Borrar" diff --git a/priv/msgs/fr.msg b/priv/msgs/fr.msg index 3d508ec20..0f8e99eeb 100644 --- a/priv/msgs/fr.msg +++ b/priv/msgs/fr.msg @@ -1,172 +1,213 @@ -{"Access Configuration","Configuration d'accès"}. -{"Access Control List Configuration","Configuration des droits (ACL)"}. -{"Access control lists","Droits (ACL)"}. -{"Access Control Lists","Droits (ACL)"}. +%% Generated automatically +%% DO NOT EDIT: run `make translations` instead +%% To improve translations please read: +%% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ + +{" (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 rules","Règles d'accès"}. -{"Access Rules","Règles d'accès"}. +{"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"}. {"Add User","Ajouter un utilisateur"}. -{"Administration","Administration"}. {"Administration of ","Administration de "}. +{"Administration","Administration"}. {"Administrator privileges required","Les droits d'administrateur sont nécessaires"}. -{"A friendly name for the node","Un nom convivial pour le noeud"}. {"All activity","Toute activité"}. -{"Allow this Jabber ID to subscribe to this pubsub node?","Autoriser ce Jabber ID à s'abonner à ce nœud PubSub"}. +{"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","Permettre aux utilisateurs d'envoyer des requêtes aux autres utilisateurs"}. -{"Allow users to send invites","Permettre aux utilisateurs d'envoyer des invitations"}. +{"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"}. {"Allow users to send private messages","Autoriser les utilisateurs à envoyer des messages privés"}. {"Allow visitors to change nickname","Autoriser les visiteurs à changer de pseudo"}. +{"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"}. -{"All Users","Tous les utilisateurs"}. +{"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"}. -{"anyone","tout le monde"}. -{"A password is required to enter this room","Un mot de passe est nécessaire pour accèder à ce salon"}. +{"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"}. -{"Backup of ","Sauvegarde de "}. +{"Backup of ~p","Sauvegarde de ~p"}. +{"Backup to File at ","Sauvegarde fichier sur "}. {"Backup","Sauvegarde"}. -{"Backup to File at ","Sauvegarde sur fichier sur "}. {"Bad format","Mauvais format"}. {"Birthday","Date d'anniversaire"}. +{"Both the username and the resource are required","Le nom d'utilisateur et sa ressource sont nécessaires"}. +{"Bytestream already activated","Le flux SOCKS5 est déjà activé"}. +{"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"}. -{"Characters not allowed:","Caractères non-autorisés :"}. +{"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éé"}. {"Chatroom is destroyed","Le salon de discussion est détruit"}. {"Chatroom is started","Le salon de discussion a démarré"}. {"Chatroom is stopped","Le salon de discussion est stoppé"}. {"Chatrooms","Salons de discussion"}. -{"Choose a username and password to register with this server","Choisissez un nom d'utilisateur et un mot de passe pour s'enregistrer sur ce serveur"}. -{"Choose modules to stop","Sélectionnez les modules à arrêter"}. +{"Choose a username and password to register with this server","Choisissez un nom d'utilisateur et un mot de passe pour ce serveur"}. {"Choose storage type of tables","Choisissez un type de stockage pour les tables"}. -{"Choose whether to approve this entity's subscription.","Accepter cet abonnement ?"}. +{"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","La salle de conférence n'existe pas"}. -{"Configuration","Configuration"}. +{"Conference room does not exist","Le salon de discussion n'existe pas"}. {"Configuration of room ~s","Configuration pour le salon ~s"}. -{"Connected Resources:","Ressources connectées:"}. -{"Connections parameters","Paramètres de connexion"}. +{"Configuration","Configuration"}. +{"Contact Addresses (normally, room owner or owners)","Adresses de contact (normalement les administrateurs du salon)"}. {"Country","Pays"}. -{"CPU Time:","Temps CPU :"}. -{"Database","Base de données"}. -{"Database Tables at ","Tables de base de données sur "}. +{"Current Discussion Topic","Sujet de discussion courant"}. +{"Database failure","Échec sur la base de données"}. {"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 par défaut participant"}. +{"Default users as participants","Les utilisateurs sont participant par défaut"}. {"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","Supprimer"}. {"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:","Groupes affichés :"}. -{"Don't tell your password to anybody, not even the administrators of the Jabber server.","Ne révélez votre mot de passe à personne, pas même l'administrateur de ce serveur."}. +{"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"}. -{"ejabberd IRC module","Module IRC ejabberd"}. +{"Either approve or decline the voice request.","Accepter ou refuser la demande de voix."}. {"ejabberd MUC module","Module MUC ejabberd"}. +{"ejabberd Multicast service","Service de Multidiffusion d'ejabberd"}. {"ejabberd Publish-Subscribe module","Module Publish-Subscribe d'ejabberd"}. -{"ejabberd SOCKS5 Bytestreams module","ejabberd SOCKS5 Bytestreams module"}. +{"ejabberd SOCKS5 Bytestreams module","Module SOCKS5 Bytestreams per ejabberd"}. {"ejabberd vCard module","Module vCard ejabberd"}. {"ejabberd Web Admin","Console Web d'administration de ejabberd"}. -{"Elements","Éléments"}. -{"Email","Email"}. +{"ejabberd","ejabberd"}. +{"Email Address","Adresse courriel"}. +{"Email","Courriel"}. {"Enable logging","Activer l'archivage"}. -{"Encoding for server ~b","Codage pour le serveur ~b"}. +{"Enable message archiving","Activer l'archivage de messages"}. +{"Enabling push without 'node' attribute is not supported","L'activation push ne peut se faire sans l'attribut 'node'"}. {"End User Session","Terminer la session de l'utilisateur"}. -{"Enter list of {Module, [Options]}","Entrez une liste de {Module, [Options]}"}. {"Enter nickname you want to register","Entrez le pseudo que vous souhaitez enregistrer"}. {"Enter path to backup file","Entrez le chemin vers le fichier de sauvegarde"}. -{"Enter path to jabberd14 spool dir","Entrez le chemin vers le répertoire de spool jabberd14"}. -{"Enter path to jabberd14 spool file","Entrez le chemin vers le fichier spool jabberd14"}. +{"Enter path to jabberd14 spool dir","Entrez le chemin vers le répertoire spool de Jabberd 1.4"}. +{"Enter path to jabberd14 spool file","Entrez le chemin vers le fichier spool de Jabberd 1.4"}. {"Enter path to text file","Entrez le chemin vers le fichier texte"}. {"Enter the text you see","Tapez le texte que vous voyez"}. -{"Enter username and encodings you wish to use for connecting to IRC servers. Press 'Next' to get more fields to fill in. Press 'Complete' to save settings.","Entrez le nom d'utilisateur et les encodages que vous souhaitez utiliser pour vous connecter aux serveurs IRC. Appuyez sur 'Suivant' pour pour avoir d'autres champs à remplir. Appuyez sur 'Terminer' pour sauver les paramètres."}. -{"Enter username, encodings, ports and passwords you wish to use for connecting to IRC servers","Entrez le nom d'utilisateur, les encodages, les ports et mots de passe que vous souhaitez utiliser pour vous connecter aux serveurs IRC"}. -{"Erlang Jabber Server","Serveur Jabber Erlang"}. -{"Error","Erreur"}. -{"Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef.net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}].","Exemple: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef.net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]."}. -{"Export data of all users in the server to PIEFXIS files (XEP-0227):","Exporter les données de tous les utilisateurs du serveur vers un fichier PIEFXIS (XEP-0227):"}. -{"Export data of users in a host to PIEFXIS files (XEP-0227):","Exporter les données utilisateurs d'un hôte vers un fichier PIEFXIS (XEP-0227):"}. +{"Erlang XMPP Server","Serveur XMPP Erlang"}. +{"Exclude Jabber IDs from CAPTCHA challenge","Exempter des Jabberd IDs du test CAPTCHA"}. +{"Export all tables as SQL queries to a file:","Exporter toutes les tables vers un fichier SQL :"}. +{"Export data of all users in the server to PIEFXIS files (XEP-0227):","Exporter les données de tous les utilisateurs du serveur vers un fichier PIEFXIS (XEP-0227) :"}. +{"Export data of users in a host to PIEFXIS files (XEP-0227):","Exporter les données utilisateurs d'un hôte vers un fichier PIEFXIS (XEP-0227) :"}. +{"External component failure","Erreur de composant externe"}. +{"External component timeout","Dépassement de delai du composant externe"}. +{"Failed to activate bytestream","Échec d'activation de bytestream"}. +{"Failed to extract JID from your voice request approval","Échec d'extraction du JID dans la requête de voix"}. +{"Failed to map delegated namespace to external component","Échec d'association d'espace de nom vers un composant externe"}. +{"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"}. -{"Fill in fields to search for any matching Jabber User","Remplissez les champs pour rechercher un utilisateur Jabber"}. -{"Fill in the form to search for any matching Jabber User (Add * to the end of field to match substring)","Remplissez le formulaire pour recherche un utilisateur Jabber (Ajouter * à la fin du champ pour chercher n'importe quelle fin de chaîne"}. +{"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"}. {"Friday","Vendredi"}. -{"From","De"}. -{"From ~s","De ~s"}. +{"From ~ts","De ~ts"}. +{"Full List of Room Admins","Liste complète des administrateurs des salons"}. +{"Full List of Room Owners","Liste complète des propriétaires des salons"}. {"Full Name","Nom complet"}. +{"Get List of Online Users","Récupérer les utilisateurs en ligne"}. +{"Get List of Registered Users","Récupérer les utilisateurs enregistrés"}. {"Get Number of Online Users","Récupérer le nombre d'utilisateurs en ligne"}. {"Get Number of Registered Users","Récupérer le nombre d'utilisateurs enregistrés"}. {"Get User Last Login Time","Récupérer la dernière date de connexion de l'utilisateur"}. -{"Get User Password","Récupérer le mot de passe de l'utilisateur"}. {"Get User Statistics","Récupérer les statistiques de l'utilisateur"}. -{"Group ","Groupe "}. -{"Groups","Groupes"}. +{"Given Name","Nom"}. +{"Grant voice to this person?","Accorder le droit de parole à cet utilisateur ?"}. {"has been banned","a été banni"}. -{"has been kicked","a été expulsé"}. -{"has been kicked because of an affiliation change","a été éjecté à cause d'un changement d'autorisation"}. {"has been kicked because of a system shutdown","a été éjecté en raison de l'arrêt du système"}. +{"has been kicked because of an affiliation change","a été éjecté à cause d'un changement d'autorisation"}. {"has been kicked because the room has been changed to members-only","a été éjecté car la salle est désormais réservée aux membres"}. -{" has set the subject to: "," a changé le sujet pour: "}. -{"Host","Serveur"}. +{"has been kicked","a été expulsé"}. +{"Hats limit exceeded","La limite a été dépassée"}. +{"Host unknown","Serveur inconnu"}. +{"HTTP File Upload","Téléversement de fichier HTTP"}. +{"Idle connection","Connexion inactive"}. {"If you don't see the CAPTCHA image here, visit the web page.","SI vous ne voyez pas l'image CAPTCHA ici, visitez la page web."}. -{"If you want to specify different ports, passwords, encodings for IRC servers, fill this list with values in format '{\"irc server\", \"encoding\", port, \"password\"}'. By default this service use \"~s\" encoding, port ~p, empty password.","Si vous voulez préciser différents ports, mots de passe, et encodages pour les serveurs IRC, remplissez cette liste avec des valeurs dans le format '{\"serveur irc\", \"encodage\", port, \"mot de passe\"}'. Par défaut ce service utilise l'encodage \"~s\", port ~p, mot de passe vide."}. -{"Import Directory","Importer une répertoire"}. +{"Import Directory","Importer un répertoire"}. {"Import File","Importer un fichier"}. -{"Import user data from jabberd14 spool file:","Importer des utilisateurs depuis un fichier spool Jabberd 1.4:"}. +{"Import user data from jabberd14 spool file:","Importer des utilisateurs depuis un fichier spool Jabberd 1.4 :"}. {"Import User from File at ","Importer un utilisateur depuis le fichier sur "}. -{"Import users data from a PIEFXIS file (XEP-0227):","Importer les données utilisateurs à partir d'un fichier PIEFXIS (XEP-0227):"}. -{"Import users data from jabberd14 spool directory:","Importer des utilisateurs depuis un fichier spool Jabberd 1.4:"}. +{"Import users data from a PIEFXIS file (XEP-0227):","Importer les données utilisateurs à partir d'un fichier PIEFXIS (XEP-0227) :"}. +{"Import users data from jabberd14 spool directory:","Importer des utilisateurs depuis un fichier spool Jabberd 1.4 :"}. {"Import Users from Dir at ","Importer des utilisateurs depuis le répertoire sur "}. {"Import Users From jabberd14 Spool Files","Importer des utilisateurs depuis un fichier spool Jabberd 1.4"}. +{"Improper domain part of 'from' attribute","Le domaine de l'attribut 'from' est incorrect"}. {"Improper message type","Mauvais type de message"}. +{"Incorrect CAPTCHA submit","Entrée CAPTCHA incorrecte"}. +{"Incorrect data form","Formulaire incorrect"}. {"Incorrect password","Mot de passe incorrect"}. -{"Invalid affiliation: ~s","Affiliation invalide : ~s"}. -{"Invalid role: ~s","Role invalide : ~s"}. +{"Incorrect value of 'action' attribute","Valeur de l'attribut 'action' incorrecte"}. +{"Incorrect value of 'action' in data form","Valeur de l'attribut 'action' incorrecte dans le formulaire"}. +{"Incorrect value of 'path' in data form","Valeur de l'attribut 'path' incorrecte dans le formulaire"}. +{"Install","Installer"}. +{"Insufficient privilege","Droits insuffisants"}. +{"Internal server error","Erreur interne du serveur"}. +{"Invalid 'from' attribute in forwarded message","L'attribut 'from' du message transféré est incorrect"}. +{"Invalid node name","Nom de nœud invalide"}. +{"Invalid 'previd' value","Valeur 'previd' invalide"}. +{"Invitations are not allowed in this conference","Les invitations ne sont pas autorisées dans ce salon"}. {"IP addresses","Adresses IP"}. -{"IP","IP"}. -{"IRC channel (don't put the first #)","Canal IRC (ne pas insérer le premier caractère #)"}. -{"IRC server","Serveur IRC"}. -{"IRC settings","Configuration IRC"}. -{"IRC Transport","Passerelle IRC"}. -{"IRC username","Nom d'utilisateur IRC"}. -{"IRC Username","Nom d'utilisateur IRC"}. {"is now known as","est maintenant connu comme"}. -{"It is not allowed to send private messages","L'envoi de messages privés n'est pas autorisé"}. +{"It is not allowed to send error messages to the room. The participant (~s) has sent an error message (~s) and got kicked from the room","L'envoyer de messages d'erreur au salon n'est pas autorisé. Le participant (~s) à envoyé un message d'erreur (~s) et à été expulsé du salon"}. {"It is not allowed to send private messages of type \"groupchat\"","Il n'est pas permis d'envoyer des messages privés de type \"groupchat\""}. -{"It is not allowed to send private messages to the conference","Il n'est pas permis d'envoyer des messages \"normaux\" à la conférence"}. -{"Jabber Account Registration","Enregistrement du Compte Jabber"}. +{"It is not allowed to send private messages to the conference","Il n'est pas permis d'envoyer des messages privés à la conférence"}. {"Jabber ID","Jabber ID"}. -{"Jabber ID ~s is invalid","Le Jabber ID ~s n'est pas valide"}. {"January","Janvier"}. -{"Join IRC channel","Rejoindre un canal IRC"}. {"joins the room","rejoint le salon"}. -{"Join the IRC channel here.","Rejoindre un canal IRC ici"}. -{"Join the IRC channel in this Jabber ID: ~s","Rejoindre un canal IRC avec ce Jabber ID: ~s"}. {"July","Juillet"}. {"June","Juin"}. -{"Last Activity","Dernière Activité"}. +{"Just created","Vient d'être créé"}. +{"Last Activity","Dernière activité"}. {"Last login","Dernière connexion"}. +{"Last message","Dernier message"}. {"Last month","Dernier mois"}. {"Last year","Dernière année"}. {"leaves the room","quitte le salon"}. -{"Listened Ports at ","Ports ouverts sur "}. -{"Listened Ports","Ports ouverts"}. -{"List of modules to start","Liste des modules à démarrer"}. -{"Low level update script","Script de mise à jour de bas-niveau"}. {"Make participants list public","Rendre la liste des participants publique"}. {"Make room CAPTCHA protected","Protéger le salon par un CAPTCHA"}. {"Make room members-only","Réserver le salon aux membres uniquement"}. @@ -174,234 +215,329 @@ {"Make room password protected","Protéger le salon par mot de passe"}. {"Make room persistent","Rendre le salon persistant"}. {"Make room public searchable","Rendre le salon public"}. +{"Malformed username","Nom d'utilisateur invalide"}. {"March","Mars"}. -{"Maximum Number of Occupants","Nombre maximum d'occupants"}. -{"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"}. +{"Maximum number of items to persist","Nombre maximal d'éléments à conserver"}. +{"Maximum Number of Occupants","Nombre maximal d'occupants"}. {"May","Mai"}. {"Membership is required to enter this room","Vous devez être membre pour accèder à ce salon"}. -{"Members:","Membres :"}. -{"Memorize your password, or write it in a paper placed in a safe place. In Jabber there isn't an automated way to recover your password if you forget it.","Mémorisez votre mot de passe, ou écrivez-le sur un papier conservé dans un endroit secret. Dans Jabber il n'y a pas de mécanisme pour retrouver votre mot de passe si vous l'avez oublié."}. -{"Memory","Mémoire"}. +{"Memorize your password, or write it in a paper placed in a safe place. In XMPP there isn't an automated way to recover your password if you forget it.","Mémorisez votre mot de passe, ou écrivez-le sur un papier conservé dans un endroit secret. Dans XMPP il n'y a pas de mécanisme pour retrouver votre mot de passe si vous l'avez oublié."}. {"Message body","Corps du message"}. +{"Message not found in forwarded payload","Message non trouvé dans l'enveloppe transférée"}. +{"Messages from strangers are rejected","Les messages d'étrangers sont rejetés"}. +{"Messages of type headline","Messages de type titre"}. +{"Messages of type normal","Messages de type normal"}. {"Middle Name","Autre nom"}. +{"Minimum interval between voice requests (in seconds)","Intervalle minimum entre les demandes de 'voice' (en secondes)"}. {"Moderator privileges required","Les droits de modérateur sont nécessaires"}. -{"moderators only","modérateurs seulement"}. -{"Modified modules","Modules mis à jour"}. -{"Module","Module"}. -{"Modules at ","Modules sur "}. -{"Modules","Modules"}. +{"Moderator","Modérateur"}. +{"Moderators Only","Modérateurs uniquement"}. +{"Module failed to handle the query","Échec de traitement de la demande"}. {"Monday","Lundi"}. -{"Name:","Nom :"}. +{"Multicast","Multidiffusion"}. +{"Multiple elements are not allowed by RFC6121","Les multiples éléments ne sont pas autorisés avec RFC6121"}. +{"Multi-User Chat","Discussion de groupe"}. {"Name","Nom"}. +{"Natural Language for Room Discussions","Langue naturelle pour les discussions en salle"}. +{"Natural-Language Room Name","Nom de la salle en langue naturelle"}. +{"Neither 'jid' nor 'nick' attribute found","Attribut 'jid' ou 'nick' absent"}. +{"Neither 'role' nor 'affiliation' attribute found","Attribut 'role' ou 'affiliation' absent"}. {"Never","Jamais"}. -{"New Password:","Nouveau mot de passe:"}. -{"Nickname","Pseudo"}. +{"New Password:","Nouveau mot de passe :"}. +{"Nickname can't be empty","Le pseudonyme ne peut être laissé vide"}. {"Nickname Registration at ","Enregistrement d'un pseudo sur "}. {"Nickname ~s does not exist in the room","Le pseudo ~s n'existe pas dans ce salon"}. +{"Nickname","Pseudo"}. +{"No address elements found","Aucun élément d'adresse trouvé"}. +{"No addresses element found","Aucun élément d'adresses trouvé"}. +{"No 'affiliation' attribute found","Attribut 'affiliation' absent"}. +{"No available resource found","Aucune ressource disponible"}. {"No body provided for announce message","Pas de corps de message pour l'annonce"}. +{"No child elements found","Aucun élément enfant trouvé"}. +{"No data form found","Formulaire non trouvé"}. {"No Data","Aucune information disponible"}. -{"Node ID","Identifiant du nœud"}. -{"Node ","Noeud "}. -{"Node not found","Noeud non trouvé"}. -{"Nodes","Noeuds"}. +{"No features available","Aucune fonctionalité disponible"}. +{"No element found","Aucun élément trouvé"}. +{"No hook has processed this command","Aucun gestionnaire n'a pris en charge cette commande"}. +{"No info about last activity found","Aucune activité précédente trouvée"}. +{"No 'item' element found","Aucun élément 'item' trouvé"}. +{"No items found in this query","Aucun item trouvé dans cette requête"}. {"No limit","Pas de limite"}. +{"No module is handling this query","Aucun module ne supporte cette requête"}. +{"No node specified","Nœud non spécifié"}. +{"No 'password' found in data form","Entrée 'password' absente du formulaire"}. +{"No 'password' found in this query","L'élément 'password' est absent de la requête"}. +{"No 'path' found in data form","Entrée 'path' absente du formulaire"}. +{"No pending subscriptions found","Aucune demande d'abonnement trouvée"}. +{"No privacy list with this name found","Liste non trouvée"}. +{"No private data found in this query","Aucune donnée privée trouvée dans cette requête"}. +{"No running node found","Nœud non trouvé"}. +{"No services available","Aucun service disponible"}. +{"No statistics found for this item","Pas de statistiques"}. +{"No 'to' attribute found in the invitation","L'élément 'to' est absent de l'invitation"}. +{"Nobody","Personne"}. +{"Node already exists","Ce nœud existe déjà"}. +{"Node ID","Identifiant du nœud"}. +{"Node index not found","Index de nœud non trouvé"}. +{"Node not found","Nœud non trouvé"}. +{"Node ~p","Nœud ~p"}. +{"Node","Nœud"}. +{"Nodeprep has failed","Échec de formattage"}. +{"Nodes","Nœuds"}. {"None","Aucun"}. -{"No resource provided","Aucune ressource fournie"}. +{"Not allowed","Non autorisé"}. {"Not Found","Nœud non trouvé"}. +{"Not subscribed","Pas abonné"}. {"Notify subscribers when items are removed from the node","Avertir les abonnés lorsque des éléments sont supprimés sur le nœud"}. {"Notify subscribers when the node configuration changes","Avertir les abonnés lorsque la configuration du nœud change"}. {"Notify subscribers when the node is deleted","Avertir les abonnés lorsque le nœud est supprimé"}. {"November","Novembre"}. +{"Number of answers required","Nombre de réponses requises"}. {"Number of occupants","Nombre d'occupants"}. +{"Number of Offline Messages","Nombre de messages hors ligne"}. {"Number of online users","Nombre d'utilisateurs en ligne"}. {"Number of registered users","Nombre d'utilisateurs enregistrés"}. +{"Occupants are allowed to invite others","Les occupants sont autorisés à inviter d’autres personnes"}. +{"Occupants May Change the Subject","Les occupants peuvent changer le sujet"}. {"October","Octobre"}. -{"Offline Messages:","Messages en attente :"}. -{"Offline Messages","Messages en attente"}. {"OK","OK"}. -{"Old Password:","Ancien mot de passe:"}. -{"Online","En ligne"}. -{"Online Users:","Utilisateurs connectés:"}. +{"Old Password:","Ancien mot de passe :"}. {"Online Users","Utilisateurs en ligne"}. +{"Online","En ligne"}. {"Only deliver notifications to available users","Envoyer les notifications uniquement aux utilisateurs disponibles"}. +{"Only or tags are allowed","Seul le tag ou est autorisé"}. +{"Only element is allowed in this query","Seul l'élément est autorisé dans cette requête"}. +{"Only members may query archives of this room","Seuls les membres peuvent accéder aux archives de ce salon"}. {"Only moderators and participants are allowed to change the subject in this room","Seuls les modérateurs et les participants peuvent changer le sujet dans ce salon"}. {"Only moderators are allowed to change the subject in this room","Seuls les modérateurs peuvent changer le sujet dans ce salon"}. +{"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"}. -{"Options","Options"}. {"Organization Name","Nom de l'organisation"}. {"Organization Unit","Unité de l'organisation"}. -{"Outgoing s2s Connections:","Connexions s2s sortantes:"}. {"Outgoing s2s Connections","Connexions s2s sortantes"}. -{"Outgoing s2s Servers:","Serveurs s2s sortants"}. {"Owner privileges required","Les droits de propriétaire sont nécessaires"}. -{"Packet","Paquet"}. -{"Password ~b","Mot de passe ~b"}. -{"Password:","Mot de passe:"}. -{"Password","Mot de passe"}. -{"Password Verification:","Vérification du mot de passe :"}. +{"Participant","Participant"}. {"Password Verification","Vérification du mot de passe"}. +{"Password Verification:","Vérification du mot de passe :"}. +{"Password","Mot de passe"}. +{"Password:","Mot de passe :"}. {"Path to Dir","Chemin vers le répertoire"}. {"Path to File","Chemin vers le fichier"}. -{"Pending","En suspens"}. -{"Period: ","Période :"}. +{"Period: ","Période : "}. {"Persist items to storage","Stockage persistant des éléments"}. +{"Persistent","Persistant"}. +{"Ping query is incorrect","Requête ping incorrecte"}. {"Ping","Ping"}. {"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Ces options sauvegardent uniquement la base de données interne Mnesia. Si vous utilisez le module ODBC vous devez sauvegarde votre base SQL séparément."}. +{"Please, wait for a while before sending new voice request","Attendez un moment avant de re-lancer une requête de voix"}. {"Pong","Pong"}. -{"Port ~b","Port ~b"}. -{"Port","Port"}. +{"Possessing 'ask' attribute is not allowed by RFC6121","L'appartenance de l'attribut 'ask' n'est pas autorisé avec RFC6121"}. {"Present real Jabber IDs to","Rendre le Jabber ID réel visible pour"}. -{"private, ","privé"}. -{"Protocol","Protocole"}. +{"Previous session not found","Session précédente introuvable"}. +{"Previous session PID has been killed","Le précédent PID de session a été tuée"}. +{"Previous session PID has exited","Le précédent PID de session a quitté"}. +{"Previous session PID is dead","Le précédent PID de session est mort"}. +{"Previous session timed out","La session précédente a expiré"}. +{"private, ","privé, "}. +{"Public","Public"}. {"Publish-Subscribe","Publication-Abonnement"}. {"PubSub subscriber request","Demande d'abonnement PubSub"}. {"Purge all items when the relevant publisher goes offline","Purger tous les items lorsque publieur est hors-ligne"}. {"Queries to the conference members are not allowed in this room","Les requêtes sur les membres de la conférence ne sont pas autorisé dans ce salon"}. +{"Query to another users is forbidden","Requête vers un autre utilisateur interdite"}. {"RAM and disc copy","Copie en mémoire vive (RAM) et sur disque"}. {"RAM copy","Copie en mémoire vive (RAM)"}. -{"Raw","Brut"}. {"Really delete message of the day?","Confirmer la suppression du message du jour ?"}. +{"Receive notification from all descendent nodes","Recevoir les notifications de tous les nœuds descendants"}. +{"Receive notification from direct child nodes only","Recevoir les notifications des nœuds enfants seulement"}. +{"Receive notification of new items only","Recevoir les notifications des nouveaux éléments uniquement"}. +{"Receive notification of new nodes only","Recevoir les notifications de tous les nouveaux nœuds descendants"}. {"Recipient is not in the conference room","Le destinataire n'est pas dans la conférence"}. -{"Register a Jabber account","Enregistrer un compte Jabber"}. -{"Registered Users:","Utilisateurs enregistrés:"}. -{"Registered Users","Utilisateurs enregistrés"}. +{"Register an XMPP account","Inscrire un compte XMPP"}. {"Register","Enregistrer"}. -{"Registration in mod_irc for ","Enregistrement du mod_irc pour "}. {"Remote copy","Copie distante"}. -{"Remove All Offline Messages","Effacer tous les messages hors ligne"}. -{"Remove","Enlever"}. {"Remove User","Supprimer l'utilisateur"}. {"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","Redémarrer"}. {"Restart Service","Redémarrer le service"}. {"Restore Backup from File at ","Restaurer la sauvegarde depuis le fichier sur "}. -{"Restore binary backup after next ejabberd restart (requires less memory):","Restauration de la sauvegarde binaire après redémarrage (nécessite moins de mémoire):"}. -{"Restore binary backup immediately:","Restauration immédiate d'une sauvegarde binaire:"}. -{"Restore plain text backup immediately:","Restauration immédiate d'une sauvegarde texte:"}. +{"Restore binary backup after next ejabberd restart (requires less memory):","Restauration de la sauvegarde binaire après redémarrage (nécessite moins de mémoire) :"}. +{"Restore binary backup immediately:","Restauration immédiate d'une sauvegarde binaire :"}. +{"Restore plain text backup immediately:","Restauration immédiate d'une sauvegarde texte :"}. {"Restore","Restauration"}. {"Room Configuration","Configuration du salon"}. {"Room creation is denied by service policy","La création de salons est interdite par le service"}. -{"Room description","Description :"}. +{"Room description","Description du salon"}. {"Room Occupants","Occupants du salon"}. {"Room title","Titre du salon"}. {"Roster groups allowed to subscribe","Groupes de liste de contact autorisés à s'abonner"}. -{"Roster","Liste de contacts"}. -{"Roster of ","Liste de contact de "}. {"Roster size","Taille de la liste de contacts"}. -{"RPC Call Error","Erreur d'appel RPC"}. -{"Running Nodes","Noeuds actifs"}. -{"~s access rule configuration","Configuration des règles d'accès ~s"}. +{"Running Nodes","Nœuds actifs"}. +{"~s invites you to the room ~s","~s vous invite dans la salle de discussion ~s"}. {"Saturday","Samedi"}. -{"Script check","Validation du script"}. {"Search Results for ","Résultats de recherche pour "}. +{"Search the text","Recherche le texte"}. +{"Search until the date","Rechercher jusqu’à la date"}. {"Search users in ","Rechercher des utilisateurs "}. -{"Send announcement to all online users","Envoyer l'annonce à tous les utilisateurs en ligne"}. {"Send announcement to all online users on all hosts","Envoyer l'annonce à tous les utilisateurs en ligne sur tous les serveurs"}. -{"Send announcement to all users","Envoyer l'annonce à tous les utilisateurs"}. +{"Send announcement to all online users","Envoyer l'annonce à tous les utilisateurs en ligne"}. {"Send announcement to all users on all hosts","Envoyer une annonce à tous les utilisateurs de tous les domaines"}. +{"Send announcement to all users","Envoyer l'annonce à tous les utilisateurs"}. {"September","Septembre"}. -{"Server ~b","Serveur ~b"}. -{"Server:","Serveur :"}. +{"Server:","Serveur :"}. +{"Service list retrieval timed out","La récupération de la liste des services a expiré"}. {"Set message of the day and send to online users","Définir le message du jour et l'envoyer aux utilisateurs en ligne"}. {"Set message of the day on all hosts and send to online users","Définir le message du jour pour tous domaines et l'envoyer aux utilisateurs en ligne"}. {"Shared Roster Groups","Groupes de liste de contacts partagée"}. {"Show Integral Table","Montrer la table intégralement"}. {"Show Ordinary Table","Montrer la table ordinaire"}. {"Shut Down Service","Arrêter le service"}. -{"~s invites you to the room ~s","~s vous a invité dans la salle de discussion ~s"}. -{"Some Jabber clients can store your password in your computer. Use that feature only if you trust your computer is safe.","Certains clients Jabber peuvent stocker votre mot de passe sur votre ordinateur. N'utilisez cette fonctionnalité que si vous avez confiance en la sécurité de votre ordinateur."}. +{"SOCKS5 Bytestreams","SOCKS5 Bytestreams"}. +{"Some XMPP clients can store your password in the computer, but you should do this only in your personal computer for safety reasons.","Certains clients XMPP peuvent stocker votre mot de passe sur votre ordinateur. N'utilisez cette fonctionnalité que si vous avez confiance en la sécurité de votre ordinateur."}. {"Specify the access model","Définir le modèle d'accès"}. {"Specify the event message type","Définir le type de message d'événement"}. {"Specify the publisher model","Définir le modèle de publication"}. -{"~s's Offline Messages Queue","~s messages en file d'attente"}. -{"Start","Démarrer"}. -{"Start Modules at ","Démarrer les modules sur "}. -{"Start Modules","Modules de démarrage"}. -{"Statistics of ~p","Statistiques de ~p"}. -{"Statistics","Statistiques"}. -{"Stop","Arrêter"}. -{"Stop Modules at ","Arrêter les modules sur "}. -{"Stop Modules","Modules d'arrêt"}. -{"Stopped Nodes","Noeuds arrêtés"}. -{"Storage Type","Type de stockage"}. -{"Store binary backup:","Sauvegarde binaire:"}. -{"Store plain text backup:","Sauvegarde texte:"}. +{"Stanza ID","Identifiant Stanza"}. +{"Stopped Nodes","Nœuds arrêtés"}. +{"Store binary backup:","Sauvegarde binaire :"}. +{"Store plain text backup:","Sauvegarde texte :"}. +{"Stream management is already enabled","La gestion des flux est déjà activée"}. {"Subject","Sujet"}. -{"Submit","Soumettre"}. {"Submitted","Soumis"}. {"Subscriber Address","Adresse de l'abonné"}. -{"Subscription","Abonnement"}. +{"Subscribers may publish","Les souscripteurs peuvent publier"}. +{"Subscriptions are not allowed","Les abonnement ne sont pas autorisés"}. {"Sunday","Dimanche"}. +{"Text associated with a picture","Texte associé à une image"}. +{"Text associated with a sound","Texte associé à un son"}. +{"Text associated with a video","Texte associé à une vidéo"}. +{"Text associated with speech","Texte associé au discours"}. {"That nickname is already in use by another occupant","Le pseudo est déjà utilisé par un autre occupant"}. {"That nickname is registered by another person","Le pseudo est enregistré par une autre personne"}. -{"The CAPTCHA is valid.","Le CAPTCHA est valide"}. +{"The account already exists","Le compte existe déjà"}. +{"The account was not unregistered","Le compte n’a pas été désinscrit"}. +{"The body text of the last received message","Le corps du texte du dernier message reçu"}. +{"The CAPTCHA is valid.","Le CAPTCHA est valide."}. {"The CAPTCHA verification has failed","La vérification du CAPTCHA a échoué"}. +{"The captcha you entered is wrong","Le captcha que vous avez saisi est erroné"}. {"The collections with which a node is affiliated","Les collections avec lesquelle un nœud est affilié"}. -{"the password is","le mot de passe est"}. +{"The datetime when the node was created","La date à laquelle le nœud a été créé"}. +{"The default language of the node","La langue par défaut du nœud"}. +{"The feature requested is not supported by the conference","La demande de fonctionalité n'est pas supportée par la conférence"}. +{"The JID of the node creator","Le JID du créateur du nœud"}. +{"The list of all online users","Les utilisateurs en ligne"}. +{"The list of all users","La liste de tous les utilisateurs"}. +{"The name of the node","Le nom du nœud"}. +{"The node is a collection node","Le nœud est un nœud de collecte"}. +{"The node is a leaf node (default)","Ce nœud est un nœud feuille (défaut)"}. +{"The number of subscribers to the node","Le nombre d’enregistrés au nœud"}. +{"The number of unread or undelivered messages","Le nombre de messages non lus ou non remis"}. +{"The password contains unacceptable characters","Le mot de passe contient des caractères non-acceptables"}. {"The password is too weak","Le mot de passe est trop faible"}. -{"The password of your Jabber account was successfully changed.","Le mot de passe de votre compte Jabber a été changé avec succès."}. -{"There was an error changing the password: ","Il y a eu une erreur en changeant le mot de passe :"}. -{"There was an error creating the account: ","Il y a eu une erreur en créant le compte :"}. -{"There was an error deleting the account: ","Il y a eu une erreur en effaçant le compte :"}. +{"the password is","le mot de passe est"}. +{"The password of your XMPP account was successfully changed.","Le mot de passe de votre compte XMPP a été modifié avec succès."}. +{"The password was not changed","Le mot de passe n’a pas été modifié"}. +{"The passwords are different","Les mots de passe sont différents"}. +{"The query is only allowed from local users","La requête n'est autorisé qu'aux utilisateurs locaux"}. +{"The query must not contain elements","La requête ne doit pas contenir d'élément "}. +{"The room subject can be modified by participants","Le sujet du salon peut être modifié par les participants"}. +{"The sender of the last received message","L’expéditeur du dernier message reçu"}. +{"The subscription identifier associated with the subscription request","L’identificateur d’abonnement associé à la demande d’abonnement"}. +{"There was an error changing the password: ","Une erreur s’est produite lors de la modification du mot de passe : "}. +{"There was an error creating the account: ","Il y a eu une erreur en créant le compte : "}. +{"There was an error deleting the account: ","Il y a eu une erreur en effaçant le compte : "}. {"This is case insensitive: macbeth is the same that MacBeth and Macbeth.","C'est insensible à la casse : macbeth est identique à MacBeth et Macbeth."}. -{"This page allows to create a Jabber account in this Jabber server. Your JID (Jabber IDentifier) will be of the form: username@server. Please read carefully the instructions to fill correctly the fields.","Cette page permet de créer un compte Jabber sur ce serveur Jabber. Votre JID (Jabber IDentifier, identifiant Jabber) sera de la forme : nom@serveur. Prière de lire avec attention les instructions pour remplir correctement ces champs."}. -{"This page allows to unregister a Jabber account in this Jabber server.","Cette page permet d'effacer un compte Jabber sur ce serveur Jabber."}. -{"This participant is kicked from the room because he sent an error message","Ce participant est expulsé du salon pour avoir envoyé un message erronée"}. -{"This participant is kicked from the room because he sent an error message to another participant","Ce participant est expulsé du salon pour avoir envoyé un message erronée à un autre participant"}. -{"This participant is kicked from the room because he sent an error presence","Ce participant est expulsé du salon pour avoir envoyé une présence erronée"}. +{"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.","Cette page permet de créer un compte XMPP sur ce serveur XMPP. Votre JID (Jabber IDentifier, identifiant Jabber) sera de la forme : nom@serveur. Prière de lire avec attention les instructions pour remplir correctement ces champs."}. +{"This page allows to unregister an XMPP account in this XMPP server.","Cette page permet de désenregistrer un compte XMPP sur ce serveur XMPP."}. {"This room is not anonymous","Ce salon n'est pas anonyme"}. +{"This service can not process the address: ~s","Ce service ne peut pas traiter l’adresse : ~s"}. {"Thursday","Jeudi"}. {"Time delay","Délais"}. -{"Time","Heure"}. -{"To","A"}. -{"To ~s","A ~s"}. +{"Timed out waiting for stream resumption","Expiration du délai d’attente pour la reprise du flux"}. +{"To register, visit ~s","Pour vous enregistrer, visitez ~s"}. +{"To ~ts","À ~ts"}. +{"Token TTL","Jeton TTL"}. +{"Too many active bytestreams","Trop de flux SOCKS5 actifs"}. +{"Too many CAPTCHA requests","Trop de requêtes CAPTCHA"}. +{"Too many elements","Trop d'éléments "}. +{"Too many elements","Trop d'éléments "}. +{"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","Trop (~p) d'authentification ont échoué pour cette adresse IP (~s). L'adresse sera débloquée à ~s UTC"}. +{"Too many receiver fields were specified","Trop de champs de récepteurs ont été spécifiés"}. +{"Too many unacked stanzas","Trop de stanzas sans accusé de réception (ack)"}. +{"Too many users in this conference","Trop d'utilisateurs dans cette conférence"}. {"Traffic rate limit is exceeded","La limite de trafic a été dépassée"}. -{"Transactions Aborted:","Transactions annulées :"}. -{"Transactions Committed:","Transactions commitées :"}. -{"Transactions Logged:","Transactions journalisées :"}. -{"Transactions Restarted:","Transactions redémarrées :"}. {"Tuesday","Mardi"}. {"Unable to generate a CAPTCHA","Impossible de générer le CAPTCHA"}. +{"Unable to register route on existing local domain","Impossible d'enregistrer la route sur un domaine locale existant"}. {"Unauthorized","Non autorisé"}. -{"Unregister a Jabber account","Effacer un compte Jabber"}. -{"Unregister","Effacer"}. +{"Unexpected action","Action inattendu"}. +{"Unexpected error condition: ~p","Condition d’erreur inattendue : ~p"}. +{"Uninstall","Désinstaller"}. +{"Unregister an XMPP account","Annuler l’enregistrement d’un compte XMPP"}. +{"Unregister","Désinscrire"}. +{"Unsupported element","Elément non supporté"}. +{"Unsupported version","Version non prise en charge"}. {"Update message of the day (don't send)","Mise à jour du message du jour (pas d'envoi)"}. {"Update message of the day on all hosts (don't send)","Mettre à jour le message du jour sur tous les domaines (ne pas envoyer)"}. -{"Update","Mettre à jour"}. -{"Update ","Mise à jour "}. -{"Update plan","Plan de mise à jour"}. -{"Update script","Script de mise à jour"}. -{"Uptime:","Temps depuis le démarrage :"}. -{"Use of STARTTLS required","L'utilisation de STARTTLS est impérative"}. +{"Upgrade","Mise à niveau"}. +{"URL for Archived Discussion Logs","URL des journaux de discussion archivés"}. +{"User already exists","L'utilisateur existe déjà"}. +{"User JID","JID de l'utilisateur"}. +{"User (jid)","Utilisateur (jid)"}. {"User Management","Gestion des utilisateurs"}. -{"Username:","Nom d'utilisateur :"}. +{"User removed","Utilisateur supprimé"}. +{"User session not found","Session utilisateur non trouvée"}. +{"User session terminated","Session utilisateur terminée"}. +{"User ~ts","Utilisateur ~ts"}. +{"Username:","Nom d'utilisateur :"}. {"Users are not allowed to register accounts so quickly","Les utilisateurs ne sont pas autorisés à enregistrer des comptes si rapidement"}. {"Users Last Activity","Dernière activité des utilisateurs"}. {"Users","Utilisateurs"}. -{"User ","Utilisateur "}. {"User","Utilisateur"}. -{"Validate","Valider"}. +{"Value 'get' of 'type' attribute is not allowed","La valeur de l'attribut 'type' ne peut être 'get'"}. +{"Value of '~s' should be boolean","La valeur de '~s' ne peut être booléen"}. +{"Value of '~s' should be datetime string","La valeur de '~s' doit être une chaine datetime"}. +{"Value of '~s' should be integer","La valeur de '~s' doit être un entier"}. +{"Value 'set' of 'type' attribute is not allowed","La valeur de l'attribut 'type' ne peut être 'set'"}. {"vCard User Search","Recherche dans l'annnuaire"}. {"Virtual Hosts","Serveurs virtuels"}. {"Visitors are not allowed to change their nicknames in this room","Les visiteurs ne sont pas autorisés à changer de pseudo dans ce salon"}. {"Visitors are not allowed to send messages to all occupants","Les visiteurs ne sont pas autorisés à envoyer des messages à tout les occupants"}. +{"Visitor","Visiteur"}. +{"Voice request","Demande de voix"}. +{"Voice requests are disabled in this conference","Les demandes de voix sont désactivées dans cette conférence"}. {"Wednesday","Mercredi"}. +{"When a new subscription is processed","Quand un nouvel abonnement est traité"}. {"When to send the last published item","A quel moment envoyer le dernier élément publié"}. -{"Whether to allow subscriptions","Autoriser l'abonnement ?"}. -{"You can later change your password using a Jabber client.","Vous pouvez changer votre mot de passe plus tard en utilisant un client Jabber."}. +{"Whether an entity wants to receive or disable notifications","Quand une entité veut recevoir ou désactiver les notifications"}. +{"Whether owners or publisher should receive replies to items","Quand les propriétaires ou annonceurs doivent revoir des réponses à leurs éléments"}. +{"Whether to allow subscriptions","Autoriser ou non les abonnements"}. +{"Whether to notify owners about new subscribers and unsubscribes","Quand notifier le propriétaire à propos des nouvelles souscriptions et désinscriptions"}. +{"Wrong parameters in the web formulary","Paramètres erronés dans le formulaire Web"}. +{"Wrong xmlns","Xmlns incorrect"}. +{"XMPP Account Registration","Enregistrement de compte XMPP"}. +{"XMPP Domains","Domaines XMPP"}. +{"You are being removed from the room because of a system shutdown","Vous avez été éjecté du salon de discussion en raison de l'arrêt du système"}. +{"You are not joined to the channel","Vous n'avez pas rejoint ce canal"}. +{"You can later change your password using an XMPP client.","Vous pouvez modifier ultérieurement votre mot de passe à l’aide d’un client XMPP."}. {"You have been banned from this room","Vous avez été exclus de ce salon"}. +{"You have joined too many conferences","Vous avec rejoint trop de conférences"}. {"You must fill in field \"Nickname\" in the form","Vous devez préciser le champ \"pseudo\" dans le formulaire"}. {"You need a client that supports x:data and CAPTCHA to register","Vous avez besoin d'un client prenant en charge x:data et CAPTCHA pour enregistrer un pseudo"}. {"You need a client that supports x:data to register the nickname","Vous avez besoin d'un client prenant en charge x:data pour enregistrer un pseudo"}. -{"You need an x:data capable client to configure mod_irc settings","Vous avez besoin d'un client supportant x:data pour configurer le module IRC"}. -{"You need an x:data capable client to configure room","Vous avez besoin d'un client supportant x:data pour configurer le salon"}. {"You need an x:data capable client to search","Vous avez besoin d'un client supportant x:data pour faire une recherche"}. {"Your active privacy list has denied the routing of this stanza.","Votre règle de flitrage active a empêché le routage de ce stanza."}. {"Your contact offline message queue is full. The message has been discarded.","La file d'attente de message de votre contact est pleine. Votre message a été détruit."}. -{"Your Jabber account was successfully created.","Votre compte Jabber a été créé avec succès."}. -{"Your Jabber account was successfully deleted.","Votre compte Jabber a été effacé avec succès."}. -{"Your messages to ~s are being blocked. To unblock them, visit ~s","Vos messages pour ~s sont bloqués. Pour les débloquer, veuillez visiter ~s"}. +{"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","Vos messages pour ~s sont bloqués. Pour les débloquer, veuillez visiter ~s"}. +{"Your XMPP account was successfully registered.","Votre compte XMPP a été enregistré avec succès."}. +{"Your XMPP account was successfully unregistered.","Votre compte XMPP a été désinscrit avec succès."}. +{"You're not allowed to create nodes","Vous n'êtes pas autorisé à créer des nœuds"}. diff --git a/priv/msgs/fr.po b/priv/msgs/fr.po deleted file mode 100644 index 6eb1a8cff..000000000 --- a/priv/msgs/fr.po +++ /dev/null @@ -1,1893 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: 2.1.0-alpha\n" -"POT-Creation-Date: \n" -"PO-Revision-Date: \n" -"Last-Translator: Nicolas Vérité \n" -"Language-Team: \n" -"Language: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Language: French (française)\n" -"X-Additional-Translator: Christophe Romain\n" -"X-Additional-Translator: Mickaël Rémond\n" -"X-Additional-Translator: Vincent Ricard\n" - -#: ejabberd_c2s.erl:424 ejabberd_c2s.erl:727 -msgid "Use of STARTTLS required" -msgstr "L'utilisation de STARTTLS est impérative" - -#: ejabberd_c2s.erl:503 -msgid "No resource provided" -msgstr "Aucune ressource fournie" - -#: ejabberd_c2s.erl:1197 -msgid "Replaced by new connection" -msgstr "Remplacé par une nouvelle connexion" - -#: ejabberd_c2s.erl:1885 -msgid "Your active privacy list has denied the routing of this stanza." -msgstr "Votre règle de flitrage active a empêché le routage de ce stanza." - -#: ejabberd_captcha.erl:96 ejabberd_captcha.erl:152 ejabberd_captcha.erl:178 -msgid "Enter the text you see" -msgstr "Tapez le texte que vous voyez" - -#: ejabberd_captcha.erl:101 -msgid "Your messages to ~s are being blocked. To unblock them, visit ~s" -msgstr "" -"Vos messages pour ~s sont bloqués. Pour les débloquer, veuillez visiter ~s" - -#: ejabberd_captcha.erl:134 -msgid "If you don't see the CAPTCHA image here, visit the web page." -msgstr "SI vous ne voyez pas l'image CAPTCHA ici, visitez la page web." - -#: ejabberd_captcha.erl:146 -msgid "CAPTCHA web page" -msgstr "Page web de CAPTCHA" - -#: ejabberd_captcha.erl:307 -msgid "The CAPTCHA is valid." -msgstr "Le CAPTCHA est valide" - -#: mod_adhoc.erl:95 mod_adhoc.erl:125 mod_adhoc.erl:143 mod_adhoc.erl:161 -msgid "Commands" -msgstr "Commandes" - -#: mod_adhoc.erl:149 mod_adhoc.erl:243 -msgid "Ping" -msgstr "Ping" - -#: mod_adhoc.erl:260 -msgid "Pong" -msgstr "Pong" - -#: mod_announce.erl:507 mod_announce_odbc.erl:499 -msgid "Really delete message of the day?" -msgstr "Confirmer la suppression du message du jour ?" - -#: mod_announce.erl:515 mod_announce_odbc.erl:507 mod_configure.erl:1083 -#: mod_configure.erl:1128 -msgid "Subject" -msgstr "Sujet" - -#: mod_announce.erl:520 mod_announce_odbc.erl:512 mod_configure.erl:1088 -#: mod_configure.erl:1133 -msgid "Message body" -msgstr "Corps du message" - -#: mod_announce.erl:600 mod_announce_odbc.erl:592 -msgid "No body provided for announce message" -msgstr "Pas de corps de message pour l'annonce" - -#: mod_announce.erl:635 mod_announce_odbc.erl:627 -msgid "Announcements" -msgstr "Annonces" - -#: mod_announce.erl:637 mod_announce_odbc.erl:629 -msgid "Send announcement to all users" -msgstr "Envoyer l'annonce à tous les utilisateurs" - -#: mod_announce.erl:639 mod_announce_odbc.erl:631 -msgid "Send announcement to all users on all hosts" -msgstr "Envoyer une annonce à tous les utilisateurs de tous les domaines" - -#: mod_announce.erl:641 mod_announce_odbc.erl:633 -msgid "Send announcement to all online users" -msgstr "Envoyer l'annonce à tous les utilisateurs en ligne" - -#: mod_announce.erl:643 mod_announce_odbc.erl:635 mod_configure.erl:1078 -#: mod_configure.erl:1123 -msgid "Send announcement to all online users on all hosts" -msgstr "" -"Envoyer l'annonce à tous les utilisateurs en ligne sur tous les serveurs" - -#: mod_announce.erl:645 mod_announce_odbc.erl:637 -msgid "Set message of the day and send to online users" -msgstr "Définir le message du jour et l'envoyer aux utilisateurs en ligne" - -#: mod_announce.erl:647 mod_announce_odbc.erl:639 -msgid "Set message of the day on all hosts and send to online users" -msgstr "" -"Définir le message du jour pour tous domaines et l'envoyer aux utilisateurs " -"en ligne" - -#: mod_announce.erl:649 mod_announce_odbc.erl:641 -msgid "Update message of the day (don't send)" -msgstr "Mise à jour du message du jour (pas d'envoi)" - -#: mod_announce.erl:651 mod_announce_odbc.erl:643 -msgid "Update message of the day on all hosts (don't send)" -msgstr "" -"Mettre à jour le message du jour sur tous les domaines (ne pas envoyer)" - -#: mod_announce.erl:653 mod_announce_odbc.erl:645 -msgid "Delete message of the day" -msgstr "Supprimer le message du jour" - -#: mod_announce.erl:655 mod_announce_odbc.erl:647 -msgid "Delete message of the day on all hosts" -msgstr "Supprimer le message du jour sur tous les domaines" - -#: mod_configure.erl:114 mod_configure.erl:274 mod_configure.erl:296 -#: mod_configure.erl:498 -msgid "Configuration" -msgstr "Configuration" - -#: mod_configure.erl:125 mod_configure.erl:576 web/ejabberd_web_admin.erl:1936 -msgid "Database" -msgstr "Base de données" - -#: mod_configure.erl:127 mod_configure.erl:595 -msgid "Start Modules" -msgstr "Modules de démarrage" - -#: mod_configure.erl:129 mod_configure.erl:596 -msgid "Stop Modules" -msgstr "Modules d'arrêt" - -#: mod_configure.erl:131 mod_configure.erl:604 web/ejabberd_web_admin.erl:1937 -msgid "Backup" -msgstr "Sauvegarde" - -#: mod_configure.erl:133 mod_configure.erl:605 -msgid "Restore" -msgstr "Restauration" - -#: mod_configure.erl:135 mod_configure.erl:606 -msgid "Dump to Text File" -msgstr "Sauvegarder dans un fichier texte" - -#: mod_configure.erl:137 mod_configure.erl:615 -msgid "Import File" -msgstr "Importer un fichier" - -#: mod_configure.erl:139 mod_configure.erl:616 -msgid "Import Directory" -msgstr "Importer une répertoire" - -#: mod_configure.erl:141 mod_configure.erl:581 mod_configure.erl:1057 -msgid "Restart Service" -msgstr "Redémarrer le service" - -#: mod_configure.erl:143 mod_configure.erl:582 mod_configure.erl:1102 -msgid "Shut Down Service" -msgstr "Arrêter le service" - -#: mod_configure.erl:145 mod_configure.erl:518 mod_configure.erl:1197 -#: web/ejabberd_web_admin.erl:1527 -msgid "Add User" -msgstr "Ajouter un utilisateur" - -#: mod_configure.erl:147 mod_configure.erl:519 mod_configure.erl:1219 -msgid "Delete User" -msgstr "Supprimer l'utilisateur" - -#: mod_configure.erl:149 mod_configure.erl:520 mod_configure.erl:1231 -msgid "End User Session" -msgstr "Terminer la session de l'utilisateur" - -#: mod_configure.erl:151 mod_configure.erl:521 mod_configure.erl:1243 -#: mod_configure.erl:1255 -msgid "Get User Password" -msgstr "Récupérer le mot de passe de l'utilisateur" - -#: mod_configure.erl:153 mod_configure.erl:522 -msgid "Change User Password" -msgstr "Changer le mot de passe de l'utilisateur" - -#: mod_configure.erl:155 mod_configure.erl:523 mod_configure.erl:1272 -msgid "Get User Last Login Time" -msgstr "Récupérer la dernière date de connexion de l'utilisateur" - -#: mod_configure.erl:157 mod_configure.erl:524 mod_configure.erl:1284 -msgid "Get User Statistics" -msgstr "Récupérer les statistiques de l'utilisateur" - -#: mod_configure.erl:159 mod_configure.erl:525 -msgid "Get Number of Registered Users" -msgstr "Récupérer le nombre d'utilisateurs enregistrés" - -#: mod_configure.erl:161 mod_configure.erl:526 -msgid "Get Number of Online Users" -msgstr "Récupérer le nombre d'utilisateurs en ligne" - -#: mod_configure.erl:163 mod_configure.erl:509 web/ejabberd_web_admin.erl:831 -#: web/ejabberd_web_admin.erl:872 -msgid "Access Control Lists" -msgstr "Droits (ACL)" - -#: mod_configure.erl:165 mod_configure.erl:510 web/ejabberd_web_admin.erl:940 -#: web/ejabberd_web_admin.erl:976 -msgid "Access Rules" -msgstr "Règles d'accès" - -#: mod_configure.erl:297 mod_configure.erl:499 -msgid "User Management" -msgstr "Gestion des utilisateurs" - -#: mod_configure.erl:500 web/ejabberd_web_admin.erl:1058 -#: web/ejabberd_web_admin.erl:1462 -msgid "Online Users" -msgstr "Utilisateurs en ligne" - -#: mod_configure.erl:501 -msgid "All Users" -msgstr "Tous les utilisateurs" - -#: mod_configure.erl:502 -msgid "Outgoing s2s Connections" -msgstr "Connexions s2s sortantes" - -#: mod_configure.erl:503 web/ejabberd_web_admin.erl:1907 -msgid "Running Nodes" -msgstr "Noeuds actifs" - -#: mod_configure.erl:504 web/ejabberd_web_admin.erl:1909 -msgid "Stopped Nodes" -msgstr "Noeuds arrêtés" - -#: mod_configure.erl:577 mod_configure.erl:587 web/ejabberd_web_admin.erl:1953 -msgid "Modules" -msgstr "Modules" - -#: mod_configure.erl:578 -msgid "Backup Management" -msgstr "Gestion des sauvegardes" - -#: mod_configure.erl:579 -msgid "Import Users From jabberd14 Spool Files" -msgstr "Importer des utilisateurs depuis un fichier spool Jabberd 1.4" - -#: mod_configure.erl:699 -msgid "To ~s" -msgstr "A ~s" - -#: mod_configure.erl:717 -msgid "From ~s" -msgstr "De ~s" - -#: mod_configure.erl:913 -msgid "Database Tables Configuration at " -msgstr "Configuration des tables de base de données sur " - -#: mod_configure.erl:918 -msgid "Choose storage type of tables" -msgstr "Choisissez un type de stockage pour les tables" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Disc only copy" -msgstr "Copie sur disque uniquement" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM and disc copy" -msgstr "Copie en mémoire vive (RAM) et sur disque" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM copy" -msgstr "Copie en mémoire vive (RAM)" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Remote copy" -msgstr "Copie distante" - -#: mod_configure.erl:950 -msgid "Stop Modules at " -msgstr "Arrêter les modules sur " - -#: mod_configure.erl:954 -msgid "Choose modules to stop" -msgstr "Sélectionnez les modules à arrêter" - -#: mod_configure.erl:969 -msgid "Start Modules at " -msgstr "Démarrer les modules sur " - -#: mod_configure.erl:973 -msgid "Enter list of {Module, [Options]}" -msgstr "Entrez une liste de {Module, [Options]}" - -#: mod_configure.erl:974 -msgid "List of modules to start" -msgstr "Liste des modules à démarrer" - -#: mod_configure.erl:983 -msgid "Backup to File at " -msgstr "Sauvegarde sur fichier sur " - -#: mod_configure.erl:987 mod_configure.erl:1001 -msgid "Enter path to backup file" -msgstr "Entrez le chemin vers le fichier de sauvegarde" - -#: mod_configure.erl:988 mod_configure.erl:1002 mod_configure.erl:1016 -#: mod_configure.erl:1030 -msgid "Path to File" -msgstr "Chemin vers le fichier" - -#: mod_configure.erl:997 -msgid "Restore Backup from File at " -msgstr "Restaurer la sauvegarde depuis le fichier sur " - -#: mod_configure.erl:1011 -msgid "Dump Backup to Text File at " -msgstr "Enregistrer la sauvegarde dans un fichier texte sur " - -#: mod_configure.erl:1015 -msgid "Enter path to text file" -msgstr "Entrez le chemin vers le fichier texte" - -#: mod_configure.erl:1025 -msgid "Import User from File at " -msgstr "Importer un utilisateur depuis le fichier sur " - -#: mod_configure.erl:1029 -msgid "Enter path to jabberd14 spool file" -msgstr "Entrez le chemin vers le fichier spool jabberd14" - -#: mod_configure.erl:1039 -msgid "Import Users from Dir at " -msgstr "Importer des utilisateurs depuis le répertoire sur " - -#: mod_configure.erl:1043 -msgid "Enter path to jabberd14 spool dir" -msgstr "Entrez le chemin vers le répertoire de spool jabberd14" - -#: mod_configure.erl:1044 -msgid "Path to Dir" -msgstr "Chemin vers le répertoire" - -#: mod_configure.erl:1060 mod_configure.erl:1105 -msgid "Time delay" -msgstr "Délais" - -#: mod_configure.erl:1143 -msgid "Access Control List Configuration" -msgstr "Configuration des droits (ACL)" - -#: mod_configure.erl:1147 -msgid "Access control lists" -msgstr "Droits (ACL)" - -#: mod_configure.erl:1171 -msgid "Access Configuration" -msgstr "Configuration d'accès" - -#: mod_configure.erl:1175 -msgid "Access rules" -msgstr "Règles d'accès" - -#: mod_configure.erl:1200 mod_configure.erl:1222 mod_configure.erl:1234 -#: mod_configure.erl:1246 mod_configure.erl:1258 mod_configure.erl:1275 -#: mod_configure.erl:1287 mod_configure.erl:1650 mod_configure.erl:1700 -#: mod_configure.erl:1721 mod_roster.erl:943 mod_roster_odbc.erl:1060 -#: mod_vcard.erl:472 mod_vcard_ldap.erl:554 mod_vcard_odbc.erl:448 -msgid "Jabber ID" -msgstr "Jabber ID" - -#: mod_configure.erl:1205 mod_configure.erl:1263 mod_configure.erl:1651 -#: mod_configure.erl:1863 mod_muc/mod_muc_room.erl:3224 mod_register.erl:235 -#: web/ejabberd_web_admin.erl:1520 -msgid "Password" -msgstr "Mot de passe" - -#: mod_configure.erl:1210 -msgid "Password Verification" -msgstr "Vérification du mot de passe" - -#: mod_configure.erl:1301 -msgid "Number of registered users" -msgstr "Nombre d'utilisateurs enregistrés" - -#: mod_configure.erl:1315 -msgid "Number of online users" -msgstr "Nombre d'utilisateurs en ligne" - -#: mod_configure.erl:1682 web/ejabberd_web_admin.erl:1588 -#: web/ejabberd_web_admin.erl:1743 -msgid "Never" -msgstr "Jamais" - -#: mod_configure.erl:1696 web/ejabberd_web_admin.erl:1601 -#: web/ejabberd_web_admin.erl:1756 -msgid "Online" -msgstr "En ligne" - -#: mod_configure.erl:1701 -msgid "Last login" -msgstr "Dernière connexion" - -#: mod_configure.erl:1722 -msgid "Roster size" -msgstr "Taille de la liste de contacts" - -#: mod_configure.erl:1723 -msgid "IP addresses" -msgstr "Adresses IP" - -#: mod_configure.erl:1724 -msgid "Resources" -msgstr "Ressources" - -#: mod_configure.erl:1850 -msgid "Administration of " -msgstr "Administration de " - -#: mod_configure.erl:1853 -msgid "Action on user" -msgstr "Action sur l'utilisateur" - -#: mod_configure.erl:1857 -msgid "Edit Properties" -msgstr "Modifier les propriétés" - -#: mod_configure.erl:1860 web/ejabberd_web_admin.erl:1769 -msgid "Remove User" -msgstr "Supprimer l'utilisateur" - -#: mod_irc/mod_irc.erl:201 mod_irc/mod_irc_odbc.erl:196 -#: mod_muc/mod_muc.erl:336 mod_muc/mod_muc_odbc.erl:342 -msgid "Access denied by service policy" -msgstr "L'accès au service est refusé" - -#: mod_irc/mod_irc.erl:401 mod_irc/mod_irc_odbc.erl:398 -msgid "IRC Transport" -msgstr "Passerelle IRC" - -#: mod_irc/mod_irc.erl:428 mod_irc/mod_irc_odbc.erl:425 -msgid "ejabberd IRC module" -msgstr "Module IRC ejabberd" - -#: mod_irc/mod_irc.erl:559 mod_irc/mod_irc_odbc.erl:568 -msgid "You need an x:data capable client to configure mod_irc settings" -msgstr "" -"Vous avez besoin d'un client supportant x:data pour configurer le module IRC" - -#: mod_irc/mod_irc.erl:566 mod_irc/mod_irc_odbc.erl:575 -msgid "Registration in mod_irc for " -msgstr "Enregistrement du mod_irc pour " - -#: mod_irc/mod_irc.erl:571 mod_irc/mod_irc_odbc.erl:580 -msgid "" -"Enter username, encodings, ports and passwords you wish to use for " -"connecting to IRC servers" -msgstr "" -"Entrez le nom d'utilisateur, les encodages, les ports et mots de passe que " -"vous souhaitez utiliser pour vous connecter aux serveurs IRC" - -#: mod_irc/mod_irc.erl:576 mod_irc/mod_irc_odbc.erl:585 -msgid "IRC Username" -msgstr "Nom d'utilisateur IRC" - -#: mod_irc/mod_irc.erl:586 mod_irc/mod_irc_odbc.erl:595 -msgid "" -"If you want to specify different ports, passwords, encodings for IRC " -"servers, fill this list with values in format '{\"irc server\", \"encoding" -"\", port, \"password\"}'. By default this service use \"~s\" encoding, port " -"~p, empty password." -msgstr "" -"Si vous voulez préciser différents ports, mots de passe, et encodages pour " -"les serveurs IRC, remplissez cette liste avec des valeurs dans le format " -"'{\"serveur irc\", \"encodage\", port, \"mot de passe\"}'. Par défaut ce " -"service utilise l'encodage \"~s\", port ~p, mot de passe vide." - -#: mod_irc/mod_irc.erl:598 mod_irc/mod_irc_odbc.erl:607 -msgid "" -"Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef." -"net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]." -msgstr "" -"Exemple: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef." -"net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]." - -#: mod_irc/mod_irc.erl:603 mod_irc/mod_irc_odbc.erl:612 -msgid "Connections parameters" -msgstr "Paramètres de connexion" - -#: mod_irc/mod_irc.erl:728 mod_irc/mod_irc_odbc.erl:757 -msgid "Join IRC channel" -msgstr "Rejoindre un canal IRC" - -#: mod_irc/mod_irc.erl:732 mod_irc/mod_irc_odbc.erl:761 -msgid "IRC channel (don't put the first #)" -msgstr "Canal IRC (ne pas insérer le premier caractère #)" - -#: mod_irc/mod_irc.erl:737 mod_irc/mod_irc_odbc.erl:766 -msgid "IRC server" -msgstr "Serveur IRC" - -#: mod_irc/mod_irc.erl:770 mod_irc/mod_irc.erl:774 -#: mod_irc/mod_irc_odbc.erl:799 mod_irc/mod_irc_odbc.erl:803 -msgid "Join the IRC channel here." -msgstr "Rejoindre un canal IRC ici" - -#: mod_irc/mod_irc.erl:778 mod_irc/mod_irc_odbc.erl:807 -msgid "Join the IRC channel in this Jabber ID: ~s" -msgstr "Rejoindre un canal IRC avec ce Jabber ID: ~s" - -#: mod_irc/mod_irc.erl:863 mod_irc/mod_irc_odbc.erl:904 -msgid "IRC settings" -msgstr "Configuration IRC" - -#: mod_irc/mod_irc.erl:868 mod_irc/mod_irc_odbc.erl:909 -msgid "" -"Enter username and encodings you wish to use for connecting to IRC servers. " -"Press 'Next' to get more fields to fill in. Press 'Complete' to save " -"settings." -msgstr "" -"Entrez le nom d'utilisateur et les encodages que vous souhaitez utiliser " -"pour vous connecter aux serveurs IRC. Appuyez sur 'Suivant' pour pour avoir " -"d'autres champs à remplir. Appuyez sur 'Terminer' pour sauver les paramètres." - -#: mod_irc/mod_irc.erl:874 mod_irc/mod_irc_odbc.erl:915 -msgid "IRC username" -msgstr "Nom d'utilisateur IRC" - -#: mod_irc/mod_irc.erl:923 mod_irc/mod_irc_odbc.erl:964 -msgid "Password ~b" -msgstr "Mot de passe ~b" - -#: mod_irc/mod_irc.erl:928 mod_irc/mod_irc_odbc.erl:969 -msgid "Port ~b" -msgstr "Port ~b" - -#: mod_irc/mod_irc.erl:933 mod_irc/mod_irc_odbc.erl:974 -msgid "Encoding for server ~b" -msgstr "Codage pour le serveur ~b" - -#: mod_irc/mod_irc.erl:942 mod_irc/mod_irc_odbc.erl:983 -msgid "Server ~b" -msgstr "Serveur ~b" - -#: mod_muc/mod_muc.erl:449 mod_muc/mod_muc_odbc.erl:456 -msgid "Only service administrators are allowed to send service messages" -msgstr "" -"Seuls les administrateurs du service sont autoriser à envoyer des messages " -"de service" - -#: mod_muc/mod_muc.erl:493 mod_muc/mod_muc_odbc.erl:500 -msgid "Room creation is denied by service policy" -msgstr "La création de salons est interdite par le service" - -#: mod_muc/mod_muc.erl:500 mod_muc/mod_muc_odbc.erl:507 -msgid "Conference room does not exist" -msgstr "La salle de conférence n'existe pas" - -#: mod_muc/mod_muc.erl:582 mod_muc/mod_muc_odbc.erl:595 -msgid "Chatrooms" -msgstr "Salons de discussion" - -#: mod_muc/mod_muc.erl:712 mod_muc/mod_muc_odbc.erl:729 -msgid "You need a client that supports x:data to register the nickname" -msgstr "" -"Vous avez besoin d'un client prenant en charge x:data pour enregistrer un " -"pseudo" - -#: mod_muc/mod_muc.erl:718 mod_muc/mod_muc_odbc.erl:735 -msgid "Nickname Registration at " -msgstr "Enregistrement d'un pseudo sur " - -#: mod_muc/mod_muc.erl:722 mod_muc/mod_muc_odbc.erl:739 -msgid "Enter nickname you want to register" -msgstr "Entrez le pseudo que vous souhaitez enregistrer" - -#: mod_muc/mod_muc.erl:723 mod_muc/mod_muc_odbc.erl:740 -#: mod_muc/mod_muc_room.erl:3810 mod_roster.erl:944 mod_roster_odbc.erl:1061 -#: mod_vcard.erl:364 mod_vcard.erl:477 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:453 -msgid "Nickname" -msgstr "Pseudo" - -#: mod_muc/mod_muc.erl:762 mod_muc/mod_muc_odbc.erl:784 -#: mod_muc/mod_muc_room.erl:1064 mod_muc/mod_muc_room.erl:1787 -msgid "That nickname is registered by another person" -msgstr "Le pseudo est enregistré par une autre personne" - -#: mod_muc/mod_muc.erl:788 mod_muc/mod_muc_odbc.erl:811 -msgid "You must fill in field \"Nickname\" in the form" -msgstr "Vous devez préciser le champ \"pseudo\" dans le formulaire" - -#: mod_muc/mod_muc.erl:808 mod_muc/mod_muc_odbc.erl:831 -msgid "ejabberd MUC module" -msgstr "Module MUC ejabberd" - -#: mod_muc/mod_muc_log.erl:374 mod_muc/mod_muc_log.erl:381 -msgid "Chatroom configuration modified" -msgstr "Configuration du salon modifiée" - -#: mod_muc/mod_muc_log.erl:384 -msgid "joins the room" -msgstr "rejoint le salon" - -#: mod_muc/mod_muc_log.erl:387 mod_muc/mod_muc_log.erl:390 -msgid "leaves the room" -msgstr "quitte le salon" - -#: mod_muc/mod_muc_log.erl:393 mod_muc/mod_muc_log.erl:396 -msgid "has been banned" -msgstr "a été banni" - -#: mod_muc/mod_muc_log.erl:399 mod_muc/mod_muc_log.erl:402 -msgid "has been kicked" -msgstr "a été expulsé" - -#: mod_muc/mod_muc_log.erl:405 -msgid "has been kicked because of an affiliation change" -msgstr "a été éjecté à cause d'un changement d'autorisation" - -#: mod_muc/mod_muc_log.erl:408 -msgid "has been kicked because the room has been changed to members-only" -msgstr "a été éjecté car la salle est désormais réservée aux membres" - -#: mod_muc/mod_muc_log.erl:411 -msgid "has been kicked because of a system shutdown" -msgstr "a été éjecté en raison de l'arrêt du système" - -#: mod_muc/mod_muc_log.erl:414 -msgid "is now known as" -msgstr "est maintenant connu comme" - -#: mod_muc/mod_muc_log.erl:417 mod_muc/mod_muc_log.erl:688 -#: mod_muc/mod_muc_room.erl:2393 -msgid " has set the subject to: " -msgstr " a changé le sujet pour: " - -#: mod_muc/mod_muc_log.erl:452 -msgid "Chatroom is created" -msgstr "Le salon de discussion est créé" - -#: mod_muc/mod_muc_log.erl:453 -msgid "Chatroom is destroyed" -msgstr "Le salon de discussion est détruit" - -#: mod_muc/mod_muc_log.erl:454 -msgid "Chatroom is started" -msgstr "Le salon de discussion a démarré" - -#: mod_muc/mod_muc_log.erl:455 -msgid "Chatroom is stopped" -msgstr "Le salon de discussion est stoppé" - -#: mod_muc/mod_muc_log.erl:459 -msgid "Monday" -msgstr "Lundi" - -#: mod_muc/mod_muc_log.erl:460 -msgid "Tuesday" -msgstr "Mardi" - -#: mod_muc/mod_muc_log.erl:461 -msgid "Wednesday" -msgstr "Mercredi" - -#: mod_muc/mod_muc_log.erl:462 -msgid "Thursday" -msgstr "Jeudi" - -#: mod_muc/mod_muc_log.erl:463 -msgid "Friday" -msgstr "Vendredi" - -#: mod_muc/mod_muc_log.erl:464 -msgid "Saturday" -msgstr "Samedi" - -#: mod_muc/mod_muc_log.erl:465 -msgid "Sunday" -msgstr "Dimanche" - -#: mod_muc/mod_muc_log.erl:469 -msgid "January" -msgstr "Janvier" - -#: mod_muc/mod_muc_log.erl:470 -msgid "February" -msgstr "Février" - -#: mod_muc/mod_muc_log.erl:471 -msgid "March" -msgstr "Mars" - -#: mod_muc/mod_muc_log.erl:472 -msgid "April" -msgstr "Avril" - -#: mod_muc/mod_muc_log.erl:473 -msgid "May" -msgstr "Mai" - -#: mod_muc/mod_muc_log.erl:474 -msgid "June" -msgstr "Juin" - -#: mod_muc/mod_muc_log.erl:475 -msgid "July" -msgstr "Juillet" - -#: mod_muc/mod_muc_log.erl:476 -msgid "August" -msgstr "Août" - -#: mod_muc/mod_muc_log.erl:477 -msgid "September" -msgstr "Septembre" - -#: mod_muc/mod_muc_log.erl:478 -msgid "October" -msgstr "Octobre" - -#: mod_muc/mod_muc_log.erl:479 -msgid "November" -msgstr "Novembre" - -#: mod_muc/mod_muc_log.erl:480 -msgid "December" -msgstr "Décembre" - -#: mod_muc/mod_muc_log.erl:750 -msgid "Room Configuration" -msgstr "Configuration du salon" - -#: mod_muc/mod_muc_log.erl:759 -msgid "Room Occupants" -msgstr "Occupants du salon" - -#: mod_muc/mod_muc_room.erl:174 -msgid "Traffic rate limit is exceeded" -msgstr "La limite de trafic a été dépassée" - -#: mod_muc/mod_muc_room.erl:246 -msgid "" -"This participant is kicked from the room because he sent an error message" -msgstr "" -"Ce participant est expulsé du salon pour avoir envoyé un message erronée" - -#: mod_muc/mod_muc_room.erl:255 -msgid "It is not allowed to send private messages to the conference" -msgstr "Il n'est pas permis d'envoyer des messages \"normaux\" à la conférence" - -#: mod_muc/mod_muc_room.erl:332 -msgid "Please, wait for a while before sending new voice request" -msgstr "" - -#: mod_muc/mod_muc_room.erl:347 -msgid "Voice requests are disabled in this conference" -msgstr "" - -#: mod_muc/mod_muc_room.erl:364 -msgid "Failed to extract JID from your voice request approval" -msgstr "" - -#: mod_muc/mod_muc_room.erl:393 -msgid "Only moderators can approve voice requests" -msgstr "" - -#: mod_muc/mod_muc_room.erl:408 -msgid "Improper message type" -msgstr "Mauvais type de message" - -#: mod_muc/mod_muc_room.erl:518 -msgid "" -"This participant is kicked from the room because he sent an error message to " -"another participant" -msgstr "" -"Ce participant est expulsé du salon pour avoir envoyé un message erronée à " -"un autre participant" - -#: mod_muc/mod_muc_room.erl:531 -msgid "It is not allowed to send private messages of type \"groupchat\"" -msgstr "" -"Il n'est pas permis d'envoyer des messages privés de type \"groupchat\"" - -#: mod_muc/mod_muc_room.erl:543 mod_muc/mod_muc_room.erl:611 -msgid "Recipient is not in the conference room" -msgstr "Le destinataire n'est pas dans la conférence" - -#: mod_muc/mod_muc_room.erl:564 mod_muc/mod_muc_room.erl:585 -msgid "It is not allowed to send private messages" -msgstr "L'envoi de messages privés n'est pas autorisé" - -#: mod_muc/mod_muc_room.erl:576 mod_muc/mod_muc_room.erl:956 -#: mod_muc/mod_muc_room.erl:4040 -msgid "Only occupants are allowed to send messages to the conference" -msgstr "Seuls les occupants peuvent envoyer des messages à la conférence" - -#: mod_muc/mod_muc_room.erl:634 -msgid "Only occupants are allowed to send queries to the conference" -msgstr "" -"Seuls les occupants sont autorisés à envoyer des requêtes à la conférence" - -#: mod_muc/mod_muc_room.erl:646 -msgid "Queries to the conference members are not allowed in this room" -msgstr "" -"Les requêtes sur les membres de la conférence ne sont pas autorisé dans ce " -"salon" - -#: mod_muc/mod_muc_room.erl:932 -msgid "" -"Only moderators and participants are allowed to change the subject in this " -"room" -msgstr "" -"Seuls les modérateurs et les participants peuvent changer le sujet dans ce " -"salon" - -#: mod_muc/mod_muc_room.erl:937 -msgid "Only moderators are allowed to change the subject in this room" -msgstr "Seuls les modérateurs peuvent changer le sujet dans ce salon" - -#: mod_muc/mod_muc_room.erl:947 -msgid "Visitors are not allowed to send messages to all occupants" -msgstr "" -"Les visiteurs ne sont pas autorisés à envoyer des messages à tout les " -"occupants" - -#: mod_muc/mod_muc_room.erl:1021 -msgid "" -"This participant is kicked from the room because he sent an error presence" -msgstr "" -"Ce participant est expulsé du salon pour avoir envoyé une présence erronée" - -#: mod_muc/mod_muc_room.erl:1040 -msgid "Visitors are not allowed to change their nicknames in this room" -msgstr "Les visiteurs ne sont pas autorisés à changer de pseudo dans ce salon" - -#: mod_muc/mod_muc_room.erl:1053 mod_muc/mod_muc_room.erl:1779 -msgid "That nickname is already in use by another occupant" -msgstr "Le pseudo est déjà utilisé par un autre occupant" - -#: mod_muc/mod_muc_room.erl:1768 -msgid "You have been banned from this room" -msgstr "Vous avez été exclus de ce salon" - -#: mod_muc/mod_muc_room.erl:1771 -msgid "Membership is required to enter this room" -msgstr "Vous devez être membre pour accèder à ce salon" - -#: mod_muc/mod_muc_room.erl:1807 -msgid "This room is not anonymous" -msgstr "Ce salon n'est pas anonyme" - -#: mod_muc/mod_muc_room.erl:1833 -msgid "A password is required to enter this room" -msgstr "Un mot de passe est nécessaire pour accèder à ce salon" - -#: mod_muc/mod_muc_room.erl:1855 mod_register.erl:246 -msgid "Too many CAPTCHA requests" -msgstr "" - -#: mod_muc/mod_muc_room.erl:1864 mod_register.erl:251 -msgid "Unable to generate a CAPTCHA" -msgstr "Impossible de générer le CAPTCHA" - -#: mod_muc/mod_muc_room.erl:1874 -msgid "Incorrect password" -msgstr "Mot de passe incorrect" - -#: mod_muc/mod_muc_room.erl:2448 -msgid "Administrator privileges required" -msgstr "Les droits d'administrateur sont nécessaires" - -#: mod_muc/mod_muc_room.erl:2463 -msgid "Moderator privileges required" -msgstr "Les droits de modérateur sont nécessaires" - -#: mod_muc/mod_muc_room.erl:2619 -msgid "Jabber ID ~s is invalid" -msgstr "Le Jabber ID ~s n'est pas valide" - -#: mod_muc/mod_muc_room.erl:2633 -msgid "Nickname ~s does not exist in the room" -msgstr "Le pseudo ~s n'existe pas dans ce salon" - -#: mod_muc/mod_muc_room.erl:2659 mod_muc/mod_muc_room.erl:3049 -msgid "Invalid affiliation: ~s" -msgstr "Affiliation invalide : ~s" - -#: mod_muc/mod_muc_room.erl:2713 -msgid "Invalid role: ~s" -msgstr "Role invalide : ~s" - -#: mod_muc/mod_muc_room.erl:3026 mod_muc/mod_muc_room.erl:3062 -msgid "Owner privileges required" -msgstr "Les droits de propriétaire sont nécessaires" - -#: mod_muc/mod_muc_room.erl:3195 -msgid "Configuration of room ~s" -msgstr "Configuration pour le salon ~s" - -#: mod_muc/mod_muc_room.erl:3200 -msgid "Room title" -msgstr "Titre du salon" - -#: mod_muc/mod_muc_room.erl:3203 mod_muc/mod_muc_room.erl:3692 -msgid "Room description" -msgstr "Description :" - -#: mod_muc/mod_muc_room.erl:3210 -msgid "Make room persistent" -msgstr "Rendre le salon persistant" - -#: mod_muc/mod_muc_room.erl:3215 -msgid "Make room public searchable" -msgstr "Rendre le salon public" - -#: mod_muc/mod_muc_room.erl:3218 -msgid "Make participants list public" -msgstr "Rendre la liste des participants publique" - -#: mod_muc/mod_muc_room.erl:3221 -msgid "Make room password protected" -msgstr "Protéger le salon par mot de passe" - -#: mod_muc/mod_muc_room.erl:3232 -msgid "Maximum Number of Occupants" -msgstr "Nombre maximum d'occupants" - -#: mod_muc/mod_muc_room.erl:3239 -msgid "No limit" -msgstr "Pas de limite" - -#: mod_muc/mod_muc_room.erl:3250 -msgid "Present real Jabber IDs to" -msgstr "Rendre le Jabber ID réel visible pour" - -#: mod_muc/mod_muc_room.erl:3258 mod_muc/mod_muc_room.erl:3292 -msgid "moderators only" -msgstr "modérateurs seulement" - -#: mod_muc/mod_muc_room.erl:3260 mod_muc/mod_muc_room.erl:3294 -msgid "anyone" -msgstr "tout le monde" - -#: mod_muc/mod_muc_room.erl:3262 -msgid "Make room members-only" -msgstr "Réserver le salon aux membres uniquement" - -#: mod_muc/mod_muc_room.erl:3265 -msgid "Make room moderated" -msgstr "Rendre le salon modéré" - -#: mod_muc/mod_muc_room.erl:3268 -msgid "Default users as participants" -msgstr "Les utilisateurs sont par défaut participant" - -#: mod_muc/mod_muc_room.erl:3271 -msgid "Allow users to change the subject" -msgstr "Autoriser les utilisateurs à changer le sujet" - -#: mod_muc/mod_muc_room.erl:3274 -msgid "Allow users to send private messages" -msgstr "Autoriser les utilisateurs à envoyer des messages privés" - -#: mod_muc/mod_muc_room.erl:3279 -#, fuzzy -msgid "Allow visitors to send private messages to" -msgstr "Autoriser les utilisateurs à envoyer des messages privés" - -#: mod_muc/mod_muc_room.erl:3290 -msgid "nobody" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3296 -msgid "Allow users to query other users" -msgstr "" -"Permettre aux utilisateurs d'envoyer des requêtes aux autres utilisateurs" - -#: mod_muc/mod_muc_room.erl:3299 -msgid "Allow users to send invites" -msgstr "Permettre aux utilisateurs d'envoyer des invitations" - -#: mod_muc/mod_muc_room.erl:3302 -msgid "Allow visitors to send status text in presence updates" -msgstr "Autoriser les visiteurs à envoyer un message d'état avec leur présence" - -#: mod_muc/mod_muc_room.erl:3305 -msgid "Allow visitors to change nickname" -msgstr "Autoriser les visiteurs à changer de pseudo" - -#: mod_muc/mod_muc_room.erl:3308 -#, fuzzy -msgid "Allow visitors to send voice requests" -msgstr "Permettre aux utilisateurs d'envoyer des invitations" - -#: mod_muc/mod_muc_room.erl:3311 -msgid "Minimum interval between voice requests (in seconds)" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3317 -msgid "Make room CAPTCHA protected" -msgstr "Protéger le salon par un CAPTCHA" - -#: mod_muc/mod_muc_room.erl:3322 -msgid "Exclude Jabber IDs from CAPTCHA challenge" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3329 -msgid "Enable logging" -msgstr "Activer l'archivage" - -#: mod_muc/mod_muc_room.erl:3337 -msgid "You need an x:data capable client to configure room" -msgstr "" -"Vous avez besoin d'un client supportant x:data pour configurer le salon" - -#: mod_muc/mod_muc_room.erl:3694 -msgid "Number of occupants" -msgstr "Nombre d'occupants" - -#: mod_muc/mod_muc_room.erl:3750 -msgid "private, " -msgstr "privé" - -#: mod_muc/mod_muc_room.erl:3799 -msgid "Voice request" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3803 -msgid "Either approve or decline the voice request." -msgstr "" - -#: mod_muc/mod_muc_room.erl:3809 -#, fuzzy -msgid "User JID" -msgstr "Utilisateur " - -#: mod_muc/mod_muc_room.erl:3811 -msgid "Grant voice to this person?" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3960 -msgid "~s invites you to the room ~s" -msgstr "~s vous a invité dans la salle de discussion ~s" - -#: mod_muc/mod_muc_room.erl:3969 -msgid "the password is" -msgstr "le mot de passe est" - -#: mod_offline.erl:510 mod_offline_odbc.erl:352 -msgid "" -"Your contact offline message queue is full. The message has been discarded." -msgstr "" -"La file d'attente de message de votre contact est pleine. Votre message a " -"été détruit." - -#: mod_offline.erl:560 mod_offline_odbc.erl:408 -msgid "~s's Offline Messages Queue" -msgstr "~s messages en file d'attente" - -#: mod_offline.erl:563 mod_offline_odbc.erl:411 mod_roster.erl:987 -#: mod_roster_odbc.erl:1104 mod_shared_roster.erl:901 -#: mod_shared_roster.erl:1010 mod_shared_roster_odbc.erl:930 -#: mod_shared_roster_odbc.erl:1039 web/ejabberd_web_admin.erl:833 -#: web/ejabberd_web_admin.erl:874 web/ejabberd_web_admin.erl:942 -#: web/ejabberd_web_admin.erl:978 web/ejabberd_web_admin.erl:1019 -#: web/ejabberd_web_admin.erl:1508 web/ejabberd_web_admin.erl:1760 -#: web/ejabberd_web_admin.erl:1931 web/ejabberd_web_admin.erl:1963 -#: web/ejabberd_web_admin.erl:2031 web/ejabberd_web_admin.erl:2135 -#: web/ejabberd_web_admin.erl:2160 web/ejabberd_web_admin.erl:2248 -msgid "Submitted" -msgstr "Soumis" - -#: mod_offline.erl:571 -msgid "Time" -msgstr "Heure" - -#: mod_offline.erl:572 -msgid "From" -msgstr "De" - -#: mod_offline.erl:573 -msgid "To" -msgstr "A" - -#: mod_offline.erl:574 mod_offline_odbc.erl:419 -msgid "Packet" -msgstr "Paquet" - -#: mod_offline.erl:587 mod_offline_odbc.erl:432 mod_shared_roster.erl:908 -#: mod_shared_roster_odbc.erl:937 web/ejabberd_web_admin.erl:882 -#: web/ejabberd_web_admin.erl:986 -msgid "Delete Selected" -msgstr "Suppression des éléments sélectionnés" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -msgid "Offline Messages:" -msgstr "Messages en attente :" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -msgid "Remove All Offline Messages" -msgstr "Effacer tous les messages hors ligne" - -#: mod_proxy65/mod_proxy65_service.erl:213 -msgid "ejabberd SOCKS5 Bytestreams module" -msgstr "ejabberd SOCKS5 Bytestreams module" - -#: mod_pubsub/mod_pubsub.erl:1120 mod_pubsub/mod_pubsub_odbc.erl:929 -msgid "Publish-Subscribe" -msgstr "Publication-Abonnement" - -#: mod_pubsub/mod_pubsub.erl:1214 mod_pubsub/mod_pubsub_odbc.erl:1025 -msgid "ejabberd Publish-Subscribe module" -msgstr "Module Publish-Subscribe d'ejabberd" - -#: mod_pubsub/mod_pubsub.erl:1497 mod_pubsub/mod_pubsub_odbc.erl:1312 -msgid "PubSub subscriber request" -msgstr "Demande d'abonnement PubSub" - -#: mod_pubsub/mod_pubsub.erl:1499 mod_pubsub/mod_pubsub_odbc.erl:1314 -msgid "Choose whether to approve this entity's subscription." -msgstr "Accepter cet abonnement ?" - -#: mod_pubsub/mod_pubsub.erl:1505 mod_pubsub/mod_pubsub_odbc.erl:1320 -msgid "Node ID" -msgstr "Identifiant du nœud" - -#: mod_pubsub/mod_pubsub.erl:1510 mod_pubsub/mod_pubsub_odbc.erl:1325 -msgid "Subscriber Address" -msgstr "Adresse de l'abonné" - -#: mod_pubsub/mod_pubsub.erl:1516 mod_pubsub/mod_pubsub_odbc.erl:1331 -msgid "Allow this Jabber ID to subscribe to this pubsub node?" -msgstr "Autoriser ce Jabber ID à s'abonner à ce nœud PubSub" - -#: mod_pubsub/mod_pubsub.erl:3474 mod_pubsub/mod_pubsub_odbc.erl:3287 -msgid "Deliver payloads with event notifications" -msgstr "Inclure le contenu du message avec la notification" - -#: mod_pubsub/mod_pubsub.erl:3475 mod_pubsub/mod_pubsub_odbc.erl:3288 -msgid "Deliver event notifications" -msgstr "Envoyer les notifications d'événement" - -#: mod_pubsub/mod_pubsub.erl:3476 mod_pubsub/mod_pubsub_odbc.erl:3289 -msgid "Notify subscribers when the node configuration changes" -msgstr "Avertir les abonnés lorsque la configuration du nœud change" - -#: mod_pubsub/mod_pubsub.erl:3477 mod_pubsub/mod_pubsub_odbc.erl:3290 -msgid "Notify subscribers when the node is deleted" -msgstr "Avertir les abonnés lorsque le nœud est supprimé" - -#: mod_pubsub/mod_pubsub.erl:3478 mod_pubsub/mod_pubsub_odbc.erl:3291 -msgid "Notify subscribers when items are removed from the node" -msgstr "Avertir les abonnés lorsque des éléments sont supprimés sur le nœud" - -#: mod_pubsub/mod_pubsub.erl:3479 mod_pubsub/mod_pubsub_odbc.erl:3292 -msgid "Persist items to storage" -msgstr "Stockage persistant des éléments" - -#: mod_pubsub/mod_pubsub.erl:3480 mod_pubsub/mod_pubsub_odbc.erl:3293 -msgid "A friendly name for the node" -msgstr "Un nom convivial pour le noeud" - -#: mod_pubsub/mod_pubsub.erl:3481 mod_pubsub/mod_pubsub_odbc.erl:3294 -msgid "Max # of items to persist" -msgstr "Nombre maximum d'éléments à stocker" - -#: mod_pubsub/mod_pubsub.erl:3482 mod_pubsub/mod_pubsub_odbc.erl:3295 -msgid "Whether to allow subscriptions" -msgstr "Autoriser l'abonnement ?" - -#: mod_pubsub/mod_pubsub.erl:3483 mod_pubsub/mod_pubsub_odbc.erl:3296 -msgid "Specify the access model" -msgstr "Définir le modèle d'accès" - -#: mod_pubsub/mod_pubsub.erl:3486 mod_pubsub/mod_pubsub_odbc.erl:3299 -msgid "Roster groups allowed to subscribe" -msgstr "Groupes de liste de contact autorisés à s'abonner" - -#: mod_pubsub/mod_pubsub.erl:3487 mod_pubsub/mod_pubsub_odbc.erl:3300 -msgid "Specify the publisher model" -msgstr "Définir le modèle de publication" - -#: mod_pubsub/mod_pubsub.erl:3489 mod_pubsub/mod_pubsub_odbc.erl:3302 -msgid "Purge all items when the relevant publisher goes offline" -msgstr "Purger tous les items lorsque publieur est hors-ligne" - -#: mod_pubsub/mod_pubsub.erl:3490 mod_pubsub/mod_pubsub_odbc.erl:3303 -msgid "Specify the event message type" -msgstr "Définir le type de message d'événement" - -#: mod_pubsub/mod_pubsub.erl:3492 mod_pubsub/mod_pubsub_odbc.erl:3305 -msgid "Max payload size in bytes" -msgstr "Taille maximum pour le contenu du message en octet" - -#: mod_pubsub/mod_pubsub.erl:3493 mod_pubsub/mod_pubsub_odbc.erl:3306 -msgid "When to send the last published item" -msgstr "A quel moment envoyer le dernier élément publié" - -#: mod_pubsub/mod_pubsub.erl:3495 mod_pubsub/mod_pubsub_odbc.erl:3308 -msgid "Only deliver notifications to available users" -msgstr "Envoyer les notifications uniquement aux utilisateurs disponibles" - -#: mod_pubsub/mod_pubsub.erl:3496 mod_pubsub/mod_pubsub_odbc.erl:3309 -msgid "The collections with which a node is affiliated" -msgstr "Les collections avec lesquelle un nœud est affilié" - -#: mod_register.erl:193 -msgid "The CAPTCHA verification has failed" -msgstr "La vérification du CAPTCHA a échoué" - -#: mod_register.erl:220 -msgid "You need a client that supports x:data and CAPTCHA to register" -msgstr "" -"Vous avez besoin d'un client prenant en charge x:data et CAPTCHA pour " -"enregistrer un pseudo" - -#: mod_register.erl:226 mod_register.erl:265 -msgid "Choose a username and password to register with this server" -msgstr "" -"Choisissez un nom d'utilisateur et un mot de passe pour s'enregistrer sur ce " -"serveur" - -#: mod_register.erl:230 mod_vcard.erl:364 mod_vcard_odbc.erl:342 -#: web/ejabberd_web_admin.erl:1515 web/ejabberd_web_admin.erl:1572 -msgid "User" -msgstr "Utilisateur" - -#: mod_register.erl:316 mod_register.erl:361 -msgid "The password is too weak" -msgstr "Le mot de passe est trop faible" - -#: mod_register.erl:365 -msgid "Users are not allowed to register accounts so quickly" -msgstr "" -"Les utilisateurs ne sont pas autorisés à enregistrer des comptes si " -"rapidement" - -#: mod_roster.erl:938 mod_roster_odbc.erl:1055 web/ejabberd_web_admin.erl:1701 -#: web/ejabberd_web_admin.erl:1886 web/ejabberd_web_admin.erl:1897 -#: web/ejabberd_web_admin.erl:2219 -msgid "None" -msgstr "Aucun" - -#: mod_roster.erl:945 mod_roster_odbc.erl:1062 -msgid "Subscription" -msgstr "Abonnement" - -#: mod_roster.erl:946 mod_roster_odbc.erl:1063 -msgid "Pending" -msgstr "En suspens" - -#: mod_roster.erl:947 mod_roster_odbc.erl:1064 -msgid "Groups" -msgstr "Groupes" - -#: mod_roster.erl:974 mod_roster_odbc.erl:1091 -msgid "Validate" -msgstr "Valider" - -#: mod_roster.erl:982 mod_roster_odbc.erl:1099 -msgid "Remove" -msgstr "Enlever" - -#: mod_roster.erl:985 mod_roster_odbc.erl:1102 -msgid "Roster of " -msgstr "Liste de contact de " - -#: mod_roster.erl:988 mod_roster_odbc.erl:1105 mod_shared_roster.erl:902 -#: mod_shared_roster.erl:1011 mod_shared_roster_odbc.erl:931 -#: mod_shared_roster_odbc.erl:1040 web/ejabberd_web_admin.erl:834 -#: web/ejabberd_web_admin.erl:875 web/ejabberd_web_admin.erl:943 -#: web/ejabberd_web_admin.erl:979 web/ejabberd_web_admin.erl:1020 -#: web/ejabberd_web_admin.erl:1509 web/ejabberd_web_admin.erl:1761 -#: web/ejabberd_web_admin.erl:1932 web/ejabberd_web_admin.erl:2136 -#: web/ejabberd_web_admin.erl:2161 -msgid "Bad format" -msgstr "Mauvais format" - -#: mod_roster.erl:995 mod_roster_odbc.erl:1112 -msgid "Add Jabber ID" -msgstr "Ajouter un Jabber ID" - -#: mod_roster.erl:1094 mod_roster_odbc.erl:1211 -msgid "Roster" -msgstr "Liste de contacts" - -#: mod_shared_roster.erl:857 mod_shared_roster.erl:899 -#: mod_shared_roster.erl:1007 mod_shared_roster_odbc.erl:886 -#: mod_shared_roster_odbc.erl:928 mod_shared_roster_odbc.erl:1036 -msgid "Shared Roster Groups" -msgstr "Groupes de liste de contacts partagée" - -#: mod_shared_roster.erl:895 mod_shared_roster_odbc.erl:924 -#: web/ejabberd_web_admin.erl:1365 web/ejabberd_web_admin.erl:2461 -msgid "Add New" -msgstr "Ajouter" - -#: mod_shared_roster.erl:978 mod_shared_roster_odbc.erl:1007 -msgid "Name:" -msgstr "Nom :" - -#: mod_shared_roster.erl:983 mod_shared_roster_odbc.erl:1012 -msgid "Description:" -msgstr "Description :" - -#: mod_shared_roster.erl:991 mod_shared_roster_odbc.erl:1020 -msgid "Members:" -msgstr "Membres :" - -#: mod_shared_roster.erl:999 mod_shared_roster_odbc.erl:1028 -msgid "Displayed Groups:" -msgstr "Groupes affichés :" - -#: mod_shared_roster.erl:1008 mod_shared_roster_odbc.erl:1037 -msgid "Group " -msgstr "Groupe " - -#: mod_shared_roster.erl:1017 mod_shared_roster_odbc.erl:1046 -#: web/ejabberd_web_admin.erl:840 web/ejabberd_web_admin.erl:884 -#: web/ejabberd_web_admin.erl:949 web/ejabberd_web_admin.erl:1026 -#: web/ejabberd_web_admin.erl:2017 -msgid "Submit" -msgstr "Soumettre" - -#: mod_vcard.erl:165 mod_vcard_ldap.erl:238 mod_vcard_odbc.erl:129 -msgid "Erlang Jabber Server" -msgstr "Serveur Jabber Erlang" - -#: mod_vcard.erl:364 mod_vcard.erl:478 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:454 -msgid "Birthday" -msgstr "Date d'anniversaire" - -#: mod_vcard.erl:364 mod_vcard.erl:480 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:456 -msgid "City" -msgstr "Ville" - -#: mod_vcard.erl:364 mod_vcard.erl:479 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:455 -msgid "Country" -msgstr "Pays" - -#: mod_vcard.erl:364 mod_vcard.erl:481 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:457 -msgid "Email" -msgstr "Email" - -#: mod_vcard.erl:364 mod_vcard.erl:476 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:452 -msgid "Family Name" -msgstr "Nom de famille" - -#: mod_vcard.erl:364 mod_vcard_odbc.erl:342 -msgid "" -"Fill in the form to search for any matching Jabber User (Add * to the end of " -"field to match substring)" -msgstr "" -"Remplissez le formulaire pour recherche un utilisateur Jabber (Ajouter * à " -"la fin du champ pour chercher n'importe quelle fin de chaîne" - -#: mod_vcard.erl:364 mod_vcard.erl:473 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:449 -msgid "Full Name" -msgstr "Nom complet" - -#: mod_vcard.erl:364 mod_vcard.erl:475 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:451 -msgid "Middle Name" -msgstr "Autre nom" - -#: mod_vcard.erl:364 mod_vcard.erl:474 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:450 web/ejabberd_web_admin.erl:2006 -msgid "Name" -msgstr "Nom" - -#: mod_vcard.erl:364 mod_vcard.erl:482 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:458 -msgid "Organization Name" -msgstr "Nom de l'organisation" - -#: mod_vcard.erl:364 mod_vcard.erl:483 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:459 -msgid "Organization Unit" -msgstr "Unité de l'organisation" - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "Search users in " -msgstr "Rechercher des utilisateurs " - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "You need an x:data capable client to search" -msgstr "" -"Vous avez besoin d'un client supportant x:data pour faire une recherche" - -#: mod_vcard.erl:389 mod_vcard_ldap.erl:487 mod_vcard_odbc.erl:367 -msgid "vCard User Search" -msgstr "Recherche dans l'annnuaire" - -#: mod_vcard.erl:445 mod_vcard_ldap.erl:541 mod_vcard_odbc.erl:421 -msgid "ejabberd vCard module" -msgstr "Module vCard ejabberd" - -#: mod_vcard.erl:469 mod_vcard_ldap.erl:551 mod_vcard_odbc.erl:445 -msgid "Search Results for " -msgstr "Résultats de recherche pour " - -#: mod_vcard_ldap.erl:462 -msgid "Fill in fields to search for any matching Jabber User" -msgstr "Remplissez les champs pour rechercher un utilisateur Jabber" - -#: web/ejabberd_web_admin.erl:193 web/ejabberd_web_admin.erl:203 -#: web/ejabberd_web_admin.erl:219 web/ejabberd_web_admin.erl:229 -msgid "Unauthorized" -msgstr "Non autorisé" - -#: web/ejabberd_web_admin.erl:286 web/ejabberd_web_admin.erl:303 -msgid "ejabberd Web Admin" -msgstr "Console Web d'administration de ejabberd" - -#: web/ejabberd_web_admin.erl:769 web/ejabberd_web_admin.erl:780 -msgid "Administration" -msgstr "Administration" - -#: web/ejabberd_web_admin.erl:878 web/ejabberd_web_admin.erl:982 -msgid "Raw" -msgstr "Brut" - -#: web/ejabberd_web_admin.erl:1017 -msgid "~s access rule configuration" -msgstr "Configuration des règles d'accès ~s" - -#: web/ejabberd_web_admin.erl:1035 -msgid "Virtual Hosts" -msgstr "Serveurs virtuels" - -#: web/ejabberd_web_admin.erl:1043 web/ejabberd_web_admin.erl:1050 -msgid "Users" -msgstr "Utilisateurs" - -#: web/ejabberd_web_admin.erl:1078 -msgid "Users Last Activity" -msgstr "Dernière activité des utilisateurs" - -#: web/ejabberd_web_admin.erl:1080 -msgid "Period: " -msgstr "Période :" - -#: web/ejabberd_web_admin.erl:1090 -msgid "Last month" -msgstr "Dernier mois" - -#: web/ejabberd_web_admin.erl:1091 -msgid "Last year" -msgstr "Dernière année" - -#: web/ejabberd_web_admin.erl:1092 -msgid "All activity" -msgstr "Toute activité" - -#: web/ejabberd_web_admin.erl:1094 -msgid "Show Ordinary Table" -msgstr "Montrer la table ordinaire" - -#: web/ejabberd_web_admin.erl:1096 -msgid "Show Integral Table" -msgstr "Montrer la table intégralement" - -#: web/ejabberd_web_admin.erl:1105 web/ejabberd_web_admin.erl:1939 -msgid "Statistics" -msgstr "Statistiques" - -#: web/ejabberd_web_admin.erl:1117 -msgid "Not Found" -msgstr "Nœud non trouvé" - -#: web/ejabberd_web_admin.erl:1134 -msgid "Node not found" -msgstr "Noeud non trouvé" - -#: web/ejabberd_web_admin.erl:1460 -msgid "Host" -msgstr "Serveur" - -#: web/ejabberd_web_admin.erl:1461 -msgid "Registered Users" -msgstr "Utilisateurs enregistrés" - -#: web/ejabberd_web_admin.erl:1573 -msgid "Offline Messages" -msgstr "Messages en attente" - -#: web/ejabberd_web_admin.erl:1574 web/ejabberd_web_admin.erl:1767 -msgid "Last Activity" -msgstr "Dernière Activité" - -#: web/ejabberd_web_admin.erl:1659 web/ejabberd_web_admin.erl:1675 -msgid "Registered Users:" -msgstr "Utilisateurs enregistrés:" - -#: web/ejabberd_web_admin.erl:1661 web/ejabberd_web_admin.erl:1677 -#: web/ejabberd_web_admin.erl:2192 -msgid "Online Users:" -msgstr "Utilisateurs connectés:" - -#: web/ejabberd_web_admin.erl:1663 -msgid "Outgoing s2s Connections:" -msgstr "Connexions s2s sortantes:" - -#: web/ejabberd_web_admin.erl:1665 -msgid "Outgoing s2s Servers:" -msgstr "Serveurs s2s sortants" - -#: web/ejabberd_web_admin.erl:1734 web/mod_register_web.erl:175 -#: web/mod_register_web.erl:372 web/mod_register_web.erl:381 -#: web/mod_register_web.erl:411 -msgid "Change Password" -msgstr "Modifier le mot de passe" - -#: web/ejabberd_web_admin.erl:1758 -msgid "User " -msgstr "Utilisateur " - -#: web/ejabberd_web_admin.erl:1765 -msgid "Connected Resources:" -msgstr "Ressources connectées:" - -#: web/ejabberd_web_admin.erl:1766 web/mod_register_web.erl:227 -#: web/mod_register_web.erl:519 -msgid "Password:" -msgstr "Mot de passe:" - -#: web/ejabberd_web_admin.erl:1828 -msgid "No Data" -msgstr "Aucune information disponible" - -#: web/ejabberd_web_admin.erl:1906 -msgid "Nodes" -msgstr "Noeuds" - -#: web/ejabberd_web_admin.erl:1929 web/ejabberd_web_admin.erl:1951 -msgid "Node " -msgstr "Noeud " - -#: web/ejabberd_web_admin.erl:1938 -msgid "Listened Ports" -msgstr "Ports ouverts" - -#: web/ejabberd_web_admin.erl:1940 web/ejabberd_web_admin.erl:2260 -#: web/ejabberd_web_admin.erl:2448 -msgid "Update" -msgstr "Mettre à jour" - -#: web/ejabberd_web_admin.erl:1943 web/ejabberd_web_admin.erl:2569 -msgid "Restart" -msgstr "Redémarrer" - -#: web/ejabberd_web_admin.erl:1945 web/ejabberd_web_admin.erl:2571 -msgid "Stop" -msgstr "Arrêter" - -#: web/ejabberd_web_admin.erl:1959 -msgid "RPC Call Error" -msgstr "Erreur d'appel RPC" - -#: web/ejabberd_web_admin.erl:2000 -msgid "Database Tables at " -msgstr "Tables de base de données sur " - -#: web/ejabberd_web_admin.erl:2007 -msgid "Storage Type" -msgstr "Type de stockage" - -#: web/ejabberd_web_admin.erl:2008 -msgid "Elements" -msgstr "Éléments" - -#: web/ejabberd_web_admin.erl:2009 -msgid "Memory" -msgstr "Mémoire" - -#: web/ejabberd_web_admin.erl:2032 web/ejabberd_web_admin.erl:2137 -msgid "Error" -msgstr "Erreur" - -#: web/ejabberd_web_admin.erl:2034 -msgid "Backup of " -msgstr "Sauvegarde de " - -#: web/ejabberd_web_admin.erl:2036 -msgid "" -"Please note that these options will only backup the builtin Mnesia database. " -"If you are using the ODBC module, you also need to backup your SQL database " -"separately." -msgstr "" -"Ces options sauvegardent uniquement la base de données interne Mnesia. Si " -"vous utilisez le module ODBC vous devez sauvegarde votre base SQL séparément." - -#: web/ejabberd_web_admin.erl:2041 -msgid "Store binary backup:" -msgstr "Sauvegarde binaire:" - -#: web/ejabberd_web_admin.erl:2045 web/ejabberd_web_admin.erl:2052 -#: web/ejabberd_web_admin.erl:2060 web/ejabberd_web_admin.erl:2067 -#: web/ejabberd_web_admin.erl:2074 web/ejabberd_web_admin.erl:2081 -#: web/ejabberd_web_admin.erl:2088 web/ejabberd_web_admin.erl:2096 -#: web/ejabberd_web_admin.erl:2103 web/ejabberd_web_admin.erl:2110 -msgid "OK" -msgstr "OK" - -#: web/ejabberd_web_admin.erl:2048 -msgid "Restore binary backup immediately:" -msgstr "Restauration immédiate d'une sauvegarde binaire:" - -#: web/ejabberd_web_admin.erl:2056 -msgid "" -"Restore binary backup after next ejabberd restart (requires less memory):" -msgstr "" -"Restauration de la sauvegarde binaire après redémarrage (nécessite moins de " -"mémoire):" - -#: web/ejabberd_web_admin.erl:2063 -msgid "Store plain text backup:" -msgstr "Sauvegarde texte:" - -#: web/ejabberd_web_admin.erl:2070 -msgid "Restore plain text backup immediately:" -msgstr "Restauration immédiate d'une sauvegarde texte:" - -#: web/ejabberd_web_admin.erl:2077 -msgid "Import users data from a PIEFXIS file (XEP-0227):" -msgstr "" -"Importer les données utilisateurs à partir d'un fichier PIEFXIS (XEP-0227):" - -#: web/ejabberd_web_admin.erl:2084 -msgid "Export data of all users in the server to PIEFXIS files (XEP-0227):" -msgstr "" -"Exporter les données de tous les utilisateurs du serveur vers un fichier " -"PIEFXIS (XEP-0227):" - -#: web/ejabberd_web_admin.erl:2091 -msgid "Export data of users in a host to PIEFXIS files (XEP-0227):" -msgstr "" -"Exporter les données utilisateurs d'un hôte vers un fichier PIEFXIS " -"(XEP-0227):" - -#: web/ejabberd_web_admin.erl:2099 -msgid "Import user data from jabberd14 spool file:" -msgstr "Importer des utilisateurs depuis un fichier spool Jabberd 1.4:" - -#: web/ejabberd_web_admin.erl:2106 -msgid "Import users data from jabberd14 spool directory:" -msgstr "Importer des utilisateurs depuis un fichier spool Jabberd 1.4:" - -#: web/ejabberd_web_admin.erl:2132 -msgid "Listened Ports at " -msgstr "Ports ouverts sur " - -#: web/ejabberd_web_admin.erl:2157 -msgid "Modules at " -msgstr "Modules sur " - -#: web/ejabberd_web_admin.erl:2183 -msgid "Statistics of ~p" -msgstr "Statistiques de ~p" - -#: web/ejabberd_web_admin.erl:2186 -msgid "Uptime:" -msgstr "Temps depuis le démarrage :" - -#: web/ejabberd_web_admin.erl:2189 -msgid "CPU Time:" -msgstr "Temps CPU :" - -#: web/ejabberd_web_admin.erl:2195 -msgid "Transactions Committed:" -msgstr "Transactions commitées :" - -#: web/ejabberd_web_admin.erl:2198 -msgid "Transactions Aborted:" -msgstr "Transactions annulées :" - -#: web/ejabberd_web_admin.erl:2201 -msgid "Transactions Restarted:" -msgstr "Transactions redémarrées :" - -#: web/ejabberd_web_admin.erl:2204 -msgid "Transactions Logged:" -msgstr "Transactions journalisées :" - -#: web/ejabberd_web_admin.erl:2246 -msgid "Update " -msgstr "Mise à jour " - -#: web/ejabberd_web_admin.erl:2254 -msgid "Update plan" -msgstr "Plan de mise à jour" - -#: web/ejabberd_web_admin.erl:2255 -msgid "Modified modules" -msgstr "Modules mis à jour" - -#: web/ejabberd_web_admin.erl:2256 -msgid "Update script" -msgstr "Script de mise à jour" - -#: web/ejabberd_web_admin.erl:2257 -msgid "Low level update script" -msgstr "Script de mise à jour de bas-niveau" - -#: web/ejabberd_web_admin.erl:2258 -msgid "Script check" -msgstr "Validation du script" - -#: web/ejabberd_web_admin.erl:2426 -msgid "Port" -msgstr "Port" - -#: web/ejabberd_web_admin.erl:2427 -msgid "IP" -msgstr "IP" - -#: web/ejabberd_web_admin.erl:2428 -msgid "Protocol" -msgstr "Protocole" - -#: web/ejabberd_web_admin.erl:2429 web/ejabberd_web_admin.erl:2556 -msgid "Module" -msgstr "Module" - -#: web/ejabberd_web_admin.erl:2430 web/ejabberd_web_admin.erl:2557 -msgid "Options" -msgstr "Options" - -#: web/ejabberd_web_admin.erl:2450 -msgid "Delete" -msgstr "Supprimer" - -#: web/ejabberd_web_admin.erl:2579 -msgid "Start" -msgstr "Démarrer" - -#: web/mod_register_web.erl:104 -msgid "Your Jabber account was successfully created." -msgstr "Votre compte Jabber a été créé avec succès." - -#: web/mod_register_web.erl:107 -msgid "There was an error creating the account: " -msgstr "Il y a eu une erreur en créant le compte :" - -#: web/mod_register_web.erl:115 -msgid "Your Jabber account was successfully deleted." -msgstr "Votre compte Jabber a été effacé avec succès." - -#: web/mod_register_web.erl:118 -msgid "There was an error deleting the account: " -msgstr "Il y a eu une erreur en effaçant le compte :" - -#: web/mod_register_web.erl:128 -msgid "The password of your Jabber account was successfully changed." -msgstr "Le mot de passe de votre compte Jabber a été changé avec succès." - -#: web/mod_register_web.erl:131 -msgid "There was an error changing the password: " -msgstr "Il y a eu une erreur en changeant le mot de passe :" - -#: web/mod_register_web.erl:163 web/mod_register_web.erl:172 -msgid "Jabber Account Registration" -msgstr "Enregistrement du Compte Jabber" - -#: web/mod_register_web.erl:174 web/mod_register_web.erl:192 -#: web/mod_register_web.erl:201 -msgid "Register a Jabber account" -msgstr "Enregistrer un compte Jabber" - -#: web/mod_register_web.erl:176 web/mod_register_web.erl:493 -#: web/mod_register_web.erl:502 -msgid "Unregister a Jabber account" -msgstr "Effacer un compte Jabber" - -#: web/mod_register_web.erl:203 -msgid "" -"This page allows to create a Jabber account in this Jabber server. Your JID " -"(Jabber IDentifier) will be of the form: username@server. Please read " -"carefully the instructions to fill correctly the fields." -msgstr "" -"Cette page permet de créer un compte Jabber sur ce serveur Jabber. Votre JID " -"(Jabber IDentifier, identifiant Jabber) sera de la forme : nom@serveur. " -"Prière de lire avec attention les instructions pour remplir correctement ces " -"champs." - -#: web/mod_register_web.erl:212 web/mod_register_web.erl:386 -#: web/mod_register_web.erl:509 -msgid "Username:" -msgstr "Nom d'utilisateur :" - -#: web/mod_register_web.erl:217 -msgid "This is case insensitive: macbeth is the same that MacBeth and Macbeth." -msgstr "" -"C'est insensible à la casse : macbeth est identique à MacBeth et Macbeth." - -#: web/mod_register_web.erl:218 -msgid "Characters not allowed:" -msgstr "Caractères non-autorisés :" - -#: web/mod_register_web.erl:222 web/mod_register_web.erl:391 -#: web/mod_register_web.erl:514 -msgid "Server:" -msgstr "Serveur :" - -#: web/mod_register_web.erl:232 -msgid "" -"Don't tell your password to anybody, not even the administrators of the " -"Jabber server." -msgstr "" -"Ne révélez votre mot de passe à personne, pas même l'administrateur de ce " -"serveur." - -#: web/mod_register_web.erl:234 -msgid "You can later change your password using a Jabber client." -msgstr "" -"Vous pouvez changer votre mot de passe plus tard en utilisant un client " -"Jabber." - -#: web/mod_register_web.erl:235 -msgid "" -"Some Jabber clients can store your password in your computer. Use that " -"feature only if you trust your computer is safe." -msgstr "" -"Certains clients Jabber peuvent stocker votre mot de passe sur votre " -"ordinateur. N'utilisez cette fonctionnalité que si vous avez confiance en la " -"sécurité de votre ordinateur." - -#: web/mod_register_web.erl:237 -msgid "" -"Memorize your password, or write it in a paper placed in a safe place. In " -"Jabber there isn't an automated way to recover your password if you forget " -"it." -msgstr "" -"Mémorisez votre mot de passe, ou écrivez-le sur un papier conservé dans un " -"endroit secret. Dans Jabber il n'y a pas de mécanisme pour retrouver votre " -"mot de passe si vous l'avez oublié." - -#: web/mod_register_web.erl:242 web/mod_register_web.erl:406 -msgid "Password Verification:" -msgstr "Vérification du mot de passe :" - -#: web/mod_register_web.erl:250 -msgid "Register" -msgstr "Enregistrer" - -#: web/mod_register_web.erl:396 -msgid "Old Password:" -msgstr "Ancien mot de passe:" - -#: web/mod_register_web.erl:401 -msgid "New Password:" -msgstr "Nouveau mot de passe:" - -#: web/mod_register_web.erl:504 -msgid "This page allows to unregister a Jabber account in this Jabber server." -msgstr "Cette page permet d'effacer un compte Jabber sur ce serveur Jabber." - -#: web/mod_register_web.erl:524 -msgid "Unregister" -msgstr "Effacer" - -#, fuzzy -#~ msgid "Captcha test failed" -#~ msgstr "Le CAPTCHA est valide" - -#~ msgid "Encodings" -#~ msgstr "Encodages" - -#~ msgid "(Raw)" -#~ msgstr "(Brut)" diff --git a/priv/msgs/gl.msg b/priv/msgs/gl.msg index bbcf11359..a72e07360 100644 --- a/priv/msgs/gl.msg +++ b/priv/msgs/gl.msg @@ -1,152 +1,152 @@ -{"Access Configuration","Configuración de accesos"}. -{"Access Control List Configuration","Configuración da Lista de Control de Acceso"}. -{"Access control lists","Listas de Control de Acceso"}. -{"Access Control Lists","Listas de Control de Acceso"}. +%% Generated automatically +%% DO NOT EDIT: run `make translations` instead +%% To improve translations please read: +%% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ + +{" has set the subject to: "," puxo o asunto: "}. +{"A friendly name for the node","Un nome sinxelo para o nodo"}. +{"A password is required to enter this room","Necesítase contrasinal para entrar nesta sala"}. +{"Accept","Aceptar"}. {"Access denied by service policy","Acceso denegado pola política do servizo"}. -{"Access rules","Regras de acceso"}. -{"Access Rules","Regras de Acceso"}. {"Action on user","Acción no usuario"}. -{"Add Jabber ID","Engadir ID Jabber"}. -{"Add New","Engadir novo"}. {"Add User","Engadir usuario"}. -{"Administration","Administración"}. {"Administration of ","Administración de "}. +{"Administration","Administración"}. {"Administrator privileges required","Necesítase privilexios de administrador"}. -{"A friendly name for the node","Un nome para o nodo"}. {"All activity","Toda a actividade"}. +{"All Users","Todos os usuarios"}. {"Allow this Jabber ID to subscribe to this pubsub node?","Desexas permitir a este JabberID que se subscriba a este nodo PubSub?"}. {"Allow users to change the subject","Permitir aos usuarios cambiar o asunto"}. {"Allow users to query other users","Permitir aos usuarios consultar a outros usuarios"}. {"Allow users to send invites","Permitir aos usuarios enviar invitacións"}. {"Allow users to send private messages","Permitir aos usuarios enviar mensaxes privadas"}. {"Allow visitors to change nickname","Permitir aos visitantes cambiarse o alcume"}. +{"Allow visitors to send private messages to","Permitir aos visitantes enviar mensaxes privadas a"}. {"Allow visitors to send status text in presence updates","Permitir aos visitantes enviar texto de estado nas actualizacións depresenza"}. -{"All Users","Todos os usuarios"}. +{"Allow visitors to send voice requests","Permitir aos visitantes enviar peticións de voz"}. {"Announcements","Anuncios"}. -{"anyone","calquera"}. -{"A password is required to enter this room","Necesítase contrasinal para entrar nesta sala"}. {"April","Abril"}. {"August","Agosto"}. -{"Backup","Gardar copia de seguridade"}. +{"Automatic node creation is not enabled","A creación automática de nodos non está habilitada"}. {"Backup Management","Xestión de copia de seguridade"}. -{"Backup of ","Copia de seguridade de "}. +{"Backup of ~p","Copia de seguridade de ~p"}. {"Backup to File at ","Copia de seguridade de arquivos en "}. +{"Backup","Copia de seguridade"}. {"Bad format","Mal formato"}. {"Birthday","Aniversario"}. +{"Both the username and the resource are required","Tanto o nome de usuario como o recurso son necesarios"}. +{"Bytestream already activated","Bytestream xa está activado"}. +{"Cannot remove active list","Non se pode eliminar a lista activa"}. +{"Cannot remove default list","Non se pode eliminar a lista predeterminada"}. +{"CAPTCHA web page","CAPTCHA páxina Web"}. {"Change Password","Cambiar contrasinal"}. {"Change User Password","Cambiar contrasinal de usuario"}. -{"Chatroom configuration modified","Configuración de la sala modificada"}. +{"Changing password is not allowed","Non se permite cambiar o contrasinal"}. +{"Changing role/affiliation is not allowed","O cambio de rol/afiliación non está permitido"}. +{"Characters not allowed:","Caracteres non permitidos:"}. +{"Chatroom configuration modified","Configuración da sala modificada"}. +{"Chatroom is created","Creouse a sala"}. +{"Chatroom is destroyed","Destruíuse a sala"}. +{"Chatroom is started","Iniciouse a sala"}. +{"Chatroom is stopped","Detívose a sala"}. {"Chatrooms","Salas de charla"}. {"Choose a username and password to register with this server","Escolle un nome de usuario e contrasinal para rexistrarche neste servidor"}. -{"Choose modules to stop","Selecciona módulos a deter"}. {"Choose storage type of tables","Selecciona tipo de almacenamento das táboas"}. {"Choose whether to approve this entity's subscription.","Decidir se aprobar a subscripción desta entidade."}. {"City","Cidade"}. {"Commands","Comandos"}. {"Conference room does not exist","A sala de conferencias non existe"}. -{"Configuration","Configuración"}. {"Configuration of room ~s","Configuración para a sala ~s"}. -{"Connected Resources:","Recursos conectados:"}. -{"Connections parameters","Parámetros de conexiones"}. +{"Configuration","Configuración"}. {"Country","País"}. -{"CPU Time:","Tempo consumido de CPU:"}. -{"Database","Base de datos"}. -{"Database Tables at ","Táboas da base de datos en "}. +{"Database failure","Erro na base de datos"}. {"Database Tables Configuration at ","Configuración de táboas da base de datos en "}. +{"Database","Base de datos"}. {"December","Decembro"}. {"Default users as participants","Os usuarios son participantes por defecto"}. -{"Delete","Eliminar"}. -{"Delete message of the day","Borrar mensaxe do dia"}. {"Delete message of the day on all hosts","Borrar a mensaxe do día en todos os dominios"}. -{"Delete Selected","Eliminar os seleccionados"}. +{"Delete message of the day","Borrar mensaxe do dia"}. {"Delete User","Borrar usuario"}. {"Deliver event notifications","Entregar notificacións de eventos"}. {"Deliver payloads with event notifications","Enviar payloads xunto coas notificacións de eventos"}. -{"Description:","Descrición:"}. {"Disc only copy","Copia en disco soamente"}. -{"Displayed Groups:","Mostrar grupos:"}. {"Dump Backup to Text File at ","Exporta copia de seguridade a ficheiro de texto en "}. {"Dump to Text File","Exportar a ficheiro de texto"}. -{"Edit Properties","Editar propiedades"}. -{"ejabberd IRC module","Módulo de IRC para ejabberd"}. +{"Edit Properties","Editar Propiedades"}. +{"Either approve or decline the voice request.","Aproba ou rexeita a petición de voz."}. {"ejabberd MUC module","Módulo de MUC para ejabberd"}. +{"ejabberd Multicast service","Servizo Multicast de ejabberd"}. {"ejabberd Publish-Subscribe module","Módulo de Publicar-Subscribir de ejabberd"}. -{"ejabberd SOCKS5 Bytestreams module","ejabberd SOCKS5 Bytestreams module"}. +{"ejabberd SOCKS5 Bytestreams module","Módulo SOCKS5 Bytestreams para ejabberd"}. {"ejabberd vCard module","Módulo vCard para ejabberd"}. -{"ejabberd Web Admin","Ejabberd Administrador Web"}. -{"Elements","Elementos"}. +{"ejabberd Web Admin","ejabberd Administrador Web"}. {"Email","Email"}. {"Enable logging","Gardar históricos"}. -{"Encoding for server ~b","Codificación de servidor ~b"}. +{"Enable message archiving","Activar o almacenamento de mensaxes"}. +{"Enabling push without 'node' attribute is not supported","Non se admite a activación do empuxe sen o atributo 'nodo'"}. {"End User Session","Pechar sesión de usuario"}. -{"Enter list of {Module, [Options]}","Introduce lista de {Módulo, [Opcións]}"}. {"Enter nickname you want to register","Introduce o alcume que queiras rexistrar"}. {"Enter path to backup file","Introduce ruta ao ficheiro de copia de seguridade"}. {"Enter path to jabberd14 spool dir","Introduce a ruta ao directorio de jabberd14 spools"}. {"Enter path to jabberd14 spool file","Introduce ruta ao ficheiro jabberd14 spool"}. {"Enter path to text file","Introduce ruta ao ficheiro de texto"}. {"Enter the text you see","Introduza o texto que ves"}. -{"Enter username and encodings you wish to use for connecting to IRC servers. Press 'Next' to get more fields to fill in. Press 'Complete' to save settings.","Introduce o nome de usuario e codificaciones de carácteres que queiras usar ao conectar nos servidores de IRC. Presione 'Siguiente' para obtener más campos para rellenar Presione 'completo' para guardar axustes."}. -{"Enter username, encodings, ports and passwords you wish to use for connecting to IRC servers","Introduza o nome de usuario, codificaciones de carácter, portos e contrasinal que pretende utilizar a conectar a servidores de IRC"}. -{"Erlang Jabber Server","Servidor Jabber en Erlang"}. -{"Error","Erro"}. -{"Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef.net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}].","Exemplo: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef.net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]."}. +{"Exclude Jabber IDs from CAPTCHA challenge","Excluír Jabber IDs das probas de CAPTCHA"}. +{"Export all tables as SQL queries to a file:","Exportar todas as táboas a un ficheiro SQL:"}. {"Export data of all users in the server to PIEFXIS files (XEP-0227):","Exportar datos de todos os usuarios do servidor a ficheros PIEFXIS (XEP-0227):"}. -{"Export data of users in a host to PIEFXIS files (XEP-0227):","Exportar datos de todos os usuarios do servidor a ficheros PIEFXIS (XEP-0227):"}. +{"Export data of users in a host to PIEFXIS files (XEP-0227):","Exportar datos dos usuarios dun dominio a ficheiros PIEFXIS (XEP-0227):"}. +{"External component failure","Fallo de compoñente externo"}. +{"External component timeout","Paso o tempo de espera do compoñente externo"}. +{"Failed to activate bytestream","Fallo ao activar bytestream"}. +{"Failed to extract JID from your voice request approval","Fallo ao extraer o Jabber ID da túa aprobación de petición de voz"}. +{"Failed to map delegated namespace to external component","O mapeo de espazo de nomes delegado fallou ao compoñente externo"}. +{"Failed to parse HTTP response","Non se puido analizar a resposta HTTP"}. +{"Failed to process option '~s'","Fallo ao procesar a opción '~s'"}. {"Family Name","Apelido"}. {"February","Febreiro"}. -{"Fill in fields to search for any matching Jabber User","Rechea campos para buscar usuarios Jabber que concuerden"}. -{"Fill in the form to search for any matching Jabber User (Add * to the end of field to match substring)","Enche o formulario para buscar usuarios Jabber. Engade * ao final dun campo para buscar subcadenas."}. +{"File larger than ~w bytes","O ficheiro é maior que ~w bytes"}. {"Friday","Venres"}. -{"From","De"}. -{"From ~s","De ~s"}. {"Full Name","Nome completo"}. {"Get Number of Online Users","Ver número de usuarios conectados"}. {"Get Number of Registered Users","Ver número de usuarios rexistrados"}. {"Get User Last Login Time","Ver data da última conexión de usuario"}. -{"Get User Password","Ver contrasinal de usuario"}. {"Get User Statistics","Ver estatísticas de usuario"}. -{"Group ","Grupo "}. -{"Groups","Grupos"}. +{"Given Name","Nome"}. +{"Grant voice to this person?","¿Conceder voz a esta persoa?"}. {"has been banned","foi bloqueado"}. +{"has been kicked because of a system shutdown","foi expulsado porque o sistema vaise a deter"}. {"has been kicked because of an affiliation change","foi expulsado debido a un cambio de afiliación"}. -{"has been kicked because of a system shutdown","foi expulsado por mor dun sistema de peche"}. {"has been kicked because the room has been changed to members-only","foi expulsado, porque a sala cambiouse a só-membros"}. {"has been kicked","foi expulsado"}. -{" has set the subject to: "," puxo o asunto: "}. -{"Host","Host"}. -{"If you want to specify different ports, passwords, encodings for IRC servers, fill this list with values in format '{\"irc server\", \"encoding\", port, \"password\"}'. By default this service use \"~s\" encoding, port ~p, empty password.","Se quere especificar codificaciones de caracteres diferentes, contrasinal ou servidor IRC rechea esta lista con valores no formato '{\"servidor irc\", \"codificación\", \"porto\", \"contrasinal\"}'. Este servizo utiliza por defecto a codificación \"~s\", porto ~p, sen contrasinal."}. +{"Host unknown","Dominio descoñecido"}. +{"If you don't see the CAPTCHA image here, visit the web page.","Si non ves a imaxe CAPTCHA aquí, visita a páxina web."}. {"Import Directory","Importar directorio"}. {"Import File","Importar ficheiro"}. -{"Import user data from jabberd14 spool file:","Importar usuario de fichero spool de jabberd14:"}. +{"Import user data from jabberd14 spool file:","Importar usuario de ficheiro spool de jabberd14:"}. {"Import User from File at ","Importa usuario desde ficheiro en "}. -{"Import users data from a PIEFXIS file (XEP-0227):","Importar usuarios desde un fichero PIEFXIS"}. +{"Import users data from a PIEFXIS file (XEP-0227):","Importar usuarios en un fichero PIEFXIS (XEP-0227):"}. {"Import users data from jabberd14 spool directory:","Importar usuarios do directorio spool de jabberd14:"}. {"Import Users from Dir at ","Importar usuarios desde o directorio en "}. {"Import Users From jabberd14 Spool Files","Importar usuarios de ficheiros spool de jabberd-1.4"}. +{"Improper domain part of 'from' attribute","Parte de dominio impropio no atributo 'from'"}. {"Improper message type","Tipo de mensaxe incorrecta"}. +{"Incorrect CAPTCHA submit","O CAPTCHA proporcionado é incorrecto"}. +{"Incorrect data form","Formulario de datos incorrecto"}. {"Incorrect password","Contrasinal incorrecta"}. -{"Invalid affiliation: ~s","Afiliación non válida: ~s"}. -{"Invalid role: ~s","Rol non válido: ~s"}. +{"Incorrect value of 'action' attribute","Valor incorrecto do atributo 'action'"}. +{"Incorrect value of 'action' in data form","Valor incorrecto de 'action' no formulario de datos"}. +{"Incorrect value of 'path' in data form","Valor incorrecto de 'path' no formulario de datos"}. +{"Insufficient privilege","Privilexio insuficiente"}. +{"Invalid 'from' attribute in forwarded message","Atributo 'from'' non é válido na mensaxe reenviada"}. +{"Invitations are not allowed in this conference","As invitacións non están permitidas nesta sala"}. {"IP addresses","Direccións IP"}. -{"IP","IP"}. -{"IRC channel (don't put the first #)","Canle de IRC (non poñer o primeiro #)"}. -{"IRC server","Servidor IRC"}. -{"IRC settings","IRC axustes"}. -{"IRC Transport","Transporte IRC"}. -{"IRC username","Nome de usuario en IRC"}. -{"IRC Username","Nome de usuario en IRC"}. -{"is now known as","cámbiase o nome a"}. -{"It is not allowed to send private messages","Non está permitido enviar mensaxes privadas"}. +{"is now known as","agora coñécese como"}. +{"It is not allowed to send error messages to the room. The participant (~s) has sent an error message (~s) and got kicked from the room","Non está permitido enviar mensaxes de erro á sala. Este participante (~s) enviou unha mensaxe de erro (~s) e foi expulsado da sala"}. {"It is not allowed to send private messages of type \"groupchat\"","Non está permitido enviar mensaxes privadas do tipo \"groupchat\""}. {"It is not allowed to send private messages to the conference","Impedir o envio de mensaxes privadas á sala"}. {"Jabber ID","Jabber ID"}. -{"Jabber ID ~s is invalid","O Jabber ID ~s non é válido"}. {"January","Xaneiro"}. -{"Join IRC channel","Entrar en canle IRC"}. -{"joins the room","entra en la sala"}. -{"Join the IRC channel here.","Únete á canle de IRC aquí."}. -{"Join the IRC channel in this Jabber ID: ~s","Únete á canle de IRC con este IDE de Jabber: ~s"}. +{"joins the room","entra na sala"}. {"July","Xullo"}. {"June","Xuño"}. {"Last Activity","Última actividade"}. @@ -154,10 +154,6 @@ {"Last month","Último mes"}. {"Last year","Último ano"}. {"leaves the room","sae da sala"}. -{"Listened Ports at ","Portos de escoita en "}. -{"Listened Ports","Portos de escoita"}. -{"List of modules to start","Lista de módulos a iniciar"}. -{"Low level update script","Script de actualización a baixo nivel"}. {"Make participants list public","A lista de participantes é pública"}. {"Make room CAPTCHA protected","Protexer a sala con CAPTCHA"}. {"Make room members-only","Sala só para membros"}. @@ -165,39 +161,63 @@ {"Make room password protected","Protexer a sala con contrasinal"}. {"Make room persistent","Sala permanente"}. {"Make room public searchable","Sala publicamente visible"}. +{"Malformed username","Nome de usuario mal formado"}. {"March","Marzo"}. -{"Maximum Number of Occupants","Número máximo de ocupantes"}. -{"Max # of items to persist","Máximo # de elementos que persisten"}. {"Max payload size in bytes","Máximo tamaño do payload en bytes"}. +{"Maximum Number of Occupants","Número máximo de ocupantes"}. {"May","Maio"}. {"Membership is required to enter this room","Necesitas ser membro desta sala para poder entrar"}. -{"Members:","Membros:"}. -{"Memory","Memoria"}. {"Message body","Corpo da mensaxe"}. +{"Message not found in forwarded payload","Mensaxe non atopada no contido reenviado"}. {"Middle Name","Segundo nome"}. +{"Minimum interval between voice requests (in seconds)","Intervalo mínimo entre peticións de voz (en segundos)"}. {"Moderator privileges required","Necesítase privilexios de moderador"}. -{"moderators only","só moderadores"}. -{"Modified modules","Módulos Modificados"}. -{"Module","Módulo"}. -{"Modules at ","Módulos en "}. -{"Modules","Módulos"}. +{"Moderator","Moderator"}. +{"Module failed to handle the query","O módulo non puido xestionar a consulta"}. {"Monday","Luns"}. -{"Name:","Nome:"}. +{"Multicast","Multicast"}. +{"Multi-User Chat","Salas de Charla"}. {"Name","Nome"}. +{"Neither 'jid' nor 'nick' attribute found","Non se atopou o atributo 'jid' nin 'nick'"}. +{"Neither 'role' nor 'affiliation' attribute found","Non se atopou o atributo 'role' nin 'affiliation'"}. {"Never","Nunca"}. -{"Nickname","Alcume"}. +{"New Password:","Novo contrasinal:"}. {"Nickname Registration at ","Rexistro do alcume en "}. {"Nickname ~s does not exist in the room","O alcume ~s non existe na sala"}. +{"Nickname","Alcume"}. +{"No 'affiliation' attribute found","Non se atopou o atributo de 'affiliation'"}. +{"No available resource found","Non se atopou ningún recurso"}. {"No body provided for announce message","Non se proporcionou corpo de mensaxe para o anuncio"}. +{"No data form found","Non se atopou formulario de datos"}. {"No Data","Sen datos"}. -{"Node ID","Nodo IDE"}. -{"Node ","Nodo "}. -{"Node not found","Nodo non atopado"}. -{"Nodes","Nodos"}. +{"No features available","Non hai características dispoñibles"}. +{"No hook has processed this command","Ningún evento procesou este comando"}. +{"No info about last activity found","Non se atopou información sobre a última actividade"}. +{"No 'item' element found","Non se atopou o elemento 'item'"}. +{"No items found in this query","Non se atoparon elementos nesta consulta"}. {"No limit","Sen límite"}. +{"No module is handling this query","Ningún módulo manexa esta consulta"}. +{"No node specified","Non se especificou nodo"}. +{"No 'password' found in data form","Non se atopou 'password' no formulario de datos"}. +{"No 'password' found in this query","Non se atopou 'password' nesta solicitude"}. +{"No 'path' found in data form","Non se atopou 'path' neste formulario de datos"}. +{"No pending subscriptions found","Non se atoparon subscricións pendentes"}. +{"No privacy list with this name found","Non se atopou ningunha lista de privacidade con este nome"}. +{"No private data found in this query","Non se atopou ningún elemento de datos privado nesta solicitude"}. +{"No running node found","Non se atoparon nodos activos"}. +{"No services available","Non hai servizos dispoñibles"}. +{"No statistics found for this item","Non se atopou ningunha estatística para este elemento"}. +{"No 'to' attribute found in the invitation","O atributo 'to' non se atopou na invitación"}. +{"Node already exists","O nodo xa existe"}. +{"Node ID","Nodo ID"}. +{"Node index not found","Non se atopou índice de nodo"}. +{"Node not found","Nodo non atopado"}. +{"Node ~p","Nodo ~p"}. +{"Nodeprep has failed","Nodeprep fallou"}. +{"Nodes","Nodos"}. {"None","Ningún"}. -{"No resource provided","Non se proporcionou recurso"}. {"Not Found","Non atopado"}. +{"Not subscribed","Non subscrito"}. {"Notify subscribers when items are removed from the node","Notificar subscriptores cando os elementos bórranse do nodo"}. {"Notify subscribers when the node configuration changes","Notificar subscriptores cando cambia a configuración do nodo"}. {"Notify subscribers when the node is deleted","Notificar subscriptores cando o nodo bórrase"}. @@ -206,164 +226,164 @@ {"Number of online users","Número de usuarios conectados"}. {"Number of registered users","Número de usuarios rexistrados"}. {"October","Outubro"}. -{"Offline Messages","Mensaxes diferidas"}. -{"Offline Messages:","Mensaxes sen conexión:"}. {"OK","Aceptar"}. -{"Online","Conectado"}. -{"Online Users:","Usuarios conectados:"}. +{"Old Password:","Contrasinal anterior:"}. {"Online Users","Usuarios conectados"}. +{"Online","Conectado"}. {"Only deliver notifications to available users","Só enviar notificacións aos usuarios dispoñibles"}. +{"Only or tags are allowed","Só se permiten etiquetas ou "}. +{"Only element is allowed in this query","Só se admite o elemento nesta consulta"}. +{"Only members may query archives of this room","Só membros poden consultar o arquivo de mensaxes da sala"}. {"Only moderators and participants are allowed to change the subject in this room","Só os moderadores e os participantes se lles permite cambiar o tema nesta sala"}. {"Only moderators are allowed to change the subject in this room","Só os moderadores están autorizados a cambiar o tema nesta sala"}. +{"Only moderators can approve voice requests","Só os moderadores poden aprobar peticións de voz"}. {"Only occupants are allowed to send messages to the conference","Só os ocupantes poden enviar mensaxes á sala"}. {"Only occupants are allowed to send queries to the conference","Só os ocupantes poden enviar solicitudes á sala"}. {"Only service administrators are allowed to send service messages","Só os administradores do servizo teñen permiso para enviar mensaxes de servizo"}. -{"Options","Opcións"}. {"Organization Name","Nome da organización"}. {"Organization Unit","Unidade da organización"}. -{"Outgoing s2s Connections:","Conexións S2S saíntes:"}. {"Outgoing s2s Connections","Conexións S2S saíntes"}. -{"Outgoing s2s Servers:","Servidores S2S saíntes:"}. {"Owner privileges required","Requírense privilexios de propietario da sala"}. -{"Packet","Paquete"}. -{"Password ~b","Contrasinal ~b"}. -{"Password:","Contrasinal:"}. -{"Password","Contrasinal"}. +{"Participant","Participante"}. {"Password Verification","Verificación da contrasinal"}. +{"Password Verification:","Verificación da Contrasinal:"}. +{"Password","Contrasinal"}. +{"Password:","Contrasinal:"}. {"Path to Dir","Ruta ao directorio"}. {"Path to File","Ruta ao ficheiro"}. -{"Pending","Pendente"}. {"Period: ","Periodo: "}. {"Persist items to storage","Persistir elementos ao almacenar"}. +{"Ping query is incorrect","A solicitude de Ping é incorrecta"}. {"Ping","Ping"}. {"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Ten en conta que estas opcións só farán copia de seguridade da base de datos Mnesia. Se está a utilizar o módulo de ODBC, tamén necesita unha copia de seguridade da súa base de datos SQL por separado."}. +{"Please, wait for a while before sending new voice request","Por favor, espera un pouco antes de enviar outra petición de voz"}. {"Pong","Pong"}. -{"Port ~b","Porto ~b"}. -{"Port","Porto"}. {"Present real Jabber IDs to","Os Jabber ID reais poden velos"}. -{"private, ","privado"}. -{"Protocol","Protocolo"}. +{"private, ","privado, "}. {"Publish-Subscribe","Publicar-Subscribir"}. {"PubSub subscriber request","Petición de subscriptor de PubSub"}. +{"Purge all items when the relevant publisher goes offline","Purgar todos os elementos cando o editor correspondente desconéctase"}. {"Queries to the conference members are not allowed in this room","Nesta sala non se permiten solicitudes aos membros da sala"}. +{"Query to another users is forbidden","É prohibido enviar solicitudes a outros usuarios"}. {"RAM and disc copy","Copia en RAM e disco"}. {"RAM copy","Copia en RAM"}. -{"Raw","Cru"}. -{"Really delete message of the day?","Está seguro de quere borrar a mensaxe do dia?"}. +{"Really delete message of the day?","¿Está seguro que quere borrar a mensaxe do dia?"}. {"Recipient is not in the conference room","O receptor non está na sala de conferencia"}. -{"Registered Users:","Usuarios rexistrados:"}. -{"Registered Users","Usuarios rexistrados"}. -{"Registration in mod_irc for ","Rexistro en mod_irc para"}. +{"Register","Rexistrar"}. {"Remote copy","Copia remota"}. -{"Remove All Offline Messages","Borrar Todas as Mensaxes Sen conexión"}. -{"Remove","Borrar"}. {"Remove User","Eliminar usuario"}. {"Replaced by new connection","Substituído por unha nova conexión"}. {"Resources","Recursos"}. -{"Restart","Reiniciar"}. {"Restart Service","Reiniciar o servizo"}. {"Restore Backup from File at ","Restaura copia de seguridade desde o ficheiro en "}. -{"Restore binary backup after next ejabberd restart (requires less memory):","Restaurar copia de seguridade binaria no seguinte reinicio de ejabberd (require menos memoria que se instantánea):"}. +{"Restore binary backup after next ejabberd restart (requires less memory):","Restaurar copia de seguridade binaria no seguinte reinicio de ejabberd (require menos memoria):"}. {"Restore binary backup immediately:","Restaurar inmediatamente copia de seguridade binaria:"}. {"Restore plain text backup immediately:","Restaurar copias de seguridade de texto plano inmediatamente:"}. {"Restore","Restaurar"}. +{"Roles for which Presence is Broadcasted","Roles para os que si se difunde a súa Presenza"}. {"Room Configuration","Configuración da Sala"}. {"Room creation is denied by service policy","Denegar crear a sala por política do servizo"}. {"Room description","Descrición da sala"}. {"Room Occupants","Ocupantes da sala"}. {"Room title","Título da sala"}. {"Roster groups allowed to subscribe","Lista de grupos autorizados a subscribir"}. -{"Roster","Lista de contactos"}. -{"Roster of ","Lista de contactos de "}. {"Roster size","Tamaño da lista de contactos"}. -{"RPC Call Error","Erro na chamada RPC"}. {"Running Nodes","Nodos funcionando"}. -{"~s access rule configuration","Configuración das Regra de Acceso ~s"}. {"Saturday","Sábado"}. -{"Script check","Comprobación de script"}. {"Search Results for ","Buscar resultados por "}. {"Search users in ","Buscar usuarios en "}. -{"Send announcement to all online users","Enviar anuncio a todos los usuarios conectados"}. {"Send announcement to all online users on all hosts","Enviar anuncio a todos os usuarios conectados en todos os dominios"}. -{"Send announcement to all users","Enviar anuncio a todos os usuarios"}. +{"Send announcement to all online users","Enviar anuncio a todos os usuarios conectados"}. {"Send announcement to all users on all hosts","Enviar anuncio a todos os usuarios en todos os dominios"}. +{"Send announcement to all users","Enviar anuncio a todos os usuarios"}. {"September","Setembro"}. -{"Server ~b","Servidor ~b"}. +{"Server:","Servidor:"}. {"Set message of the day and send to online users","Pór mensaxe do dia e enviar a todos os usuarios conectados"}. {"Set message of the day on all hosts and send to online users","Pór mensaxe do día en todos os dominios e enviar aos usuarios conectados"}. {"Shared Roster Groups","Grupos Compartidos"}. {"Show Integral Table","Mostrar Táboa Integral"}. {"Show Ordinary Table","Mostrar Táboa Ordinaria"}. {"Shut Down Service","Deter o servizo"}. -{"~s invites you to the room ~s","~s invítache á sala ~s"}. {"Specify the access model","Especifica o modelo de acceso"}. +{"Specify the event message type","Especifica o tipo da mensaxe de evento"}. {"Specify the publisher model","Especificar o modelo do publicante"}. -{"~s's Offline Messages Queue","Cola de mensaxes diferidas de ~s"}. -{"Start","Iniciar"}. -{"Start Modules at ","Iniciar módulos en "}. -{"Start Modules","Iniciar módulos"}. -{"Statistics","Estatísticas"}. -{"Statistics of ~p","Estatísticas de ~p"}. -{"Stop","Deter"}. -{"Stop Modules at ","Deter módulos en "}. -{"Stop Modules","Detener módulos"}. {"Stopped Nodes","Nodos detidos"}. -{"Storage Type","Tipo de almacenamiento"}. {"Store binary backup:","Gardar copia de seguridade binaria:"}. {"Store plain text backup:","Gardar copia de seguridade en texto plano:"}. {"Subject","Asunto"}. -{"Submit","Enviar"}. {"Submitted","Enviado"}. {"Subscriber Address","Dirección do subscriptor"}. -{"Subscription","Subscripción"}. +{"Subscriptions are not allowed","Non se permiten subscricións"}. {"Sunday","Domingo"}. -{"That nickname is already in use by another occupant","Ese alcume que xa está en uso por outro ocupante"}. +{"That nickname is already in use by another occupant","Ese alcume xa está a ser usado por outro ocupante"}. {"That nickname is registered by another person","O alcume xa está rexistrado por outra persoa"}. {"The CAPTCHA is valid.","O CAPTCHA é válido."}. +{"The CAPTCHA verification has failed","A verificación de CAPTCHA fallou"}. {"The collections with which a node is affiliated","As coleccións coas que un nodo está afiliado"}. +{"The feature requested is not supported by the conference","A sala de conferencias non admite a función solicitada"}. +{"The password contains unacceptable characters","O contrasinal contén caracteres inaceptables"}. +{"The password is too weak","O contrasinal é demasiado débil"}. {"the password is","a contrasinal é"}. -{"This participant is kicked from the room because he sent an error message","Este participante é expulsado da sala, xa que enviou unha mensaxe de erro"}. -{"This participant is kicked from the room because he sent an error message to another participant","Este participante é expulsado da sala, porque el enviou unha mensaxe de erro a outro participante"}. -{"This participant is kicked from the room because he sent an error presence","Este participante é expulsado da sala, porque el enviou un erro de presenza"}. +{"The query is only allowed from local users","A solicitude só se permite para usuarios locais"}. +{"The query must not contain elements","A solicitude non debe conter elementos "}. +{"The stanza MUST contain only one element, one element, or one element","A estroa DEBEN conter un elemento , un elemento ou un elemento "}. +{"There was an error creating the account: ","Produciuse un erro ao crear a conta: "}. +{"There was an error deleting the account: ","Produciuse un erro ao eliminar a conta: "}. {"This room is not anonymous","Sala non anónima"}. {"Thursday","Xoves"}. -{"Time","Data"}. {"Time delay","Atraso temporal"}. -{"To","Para"}. -{"To ~s","A ~s"}. +{"To register, visit ~s","Para rexistrarse, visita ~s"}. +{"Token TTL","Token TTL"}. +{"Too many active bytestreams","Demasiados bytestreams activos"}. +{"Too many CAPTCHA requests","Demasiadas solicitudes CAPTCHA"}. +{"Too many elements","Demasiados elementos "}. +{"Too many elements","Demasiados elementos "}. +{"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","Demasiados (~p) fallou autenticaciones desde esta dirección IP (~s). A dirección será desbloqueada as ~s UTC"}. +{"Too many unacked stanzas","Demasiadas mensaxes sen recoñecer recibilos"}. +{"Too many users in this conference","Demasiados usuarios nesta sala"}. {"Traffic rate limit is exceeded","Hase exedido o límite de tráfico"}. -{"Transactions Aborted:","Transaccións abortadas:"}. -{"Transactions Committed:","Transaccións finalizadas:"}. -{"Transactions Logged:","Transaccións rexistradas:"}. -{"Transactions Restarted:","Transaccións reiniciadas:"}. {"Tuesday","Martes"}. +{"Unable to generate a CAPTCHA","No se pudo generar un CAPTCHA"}. +{"Unable to register route on existing local domain","Non se pode rexistrar a ruta no dominio local existente"}. {"Unauthorized","Non autorizado"}. -{"Update ","Actualizar"}. -{"Update","Actualizar"}. +{"Unexpected action","Acción inesperada"}. +{"Unregister","Eliminar rexistro"}. +{"Unsupported element","Elemento non soportado"}. {"Update message of the day (don't send)","Actualizar mensaxe do dia, pero non envialo"}. {"Update message of the day on all hosts (don't send)","Actualizar a mensaxe do día en todos os dominos (pero non envialo)"}. -{"Update plan","Plan de actualización"}. -{"Update script","Script de actualización"}. -{"Uptime:","Tempo desde o inicio:"}. -{"Use of STARTTLS required","É obrigatorio usar STARTTLS"}. +{"User already exists","O usuario xa existe"}. +{"User JID","Jabber ID do usuario"}. +{"User (jid)","Usuario (jid)"}. {"User Management","Administración de usuarios"}. +{"User session not found","Sesión de usuario non atopada"}. +{"User session terminated","Sesión de usuario completada"}. +{"Username:","Nome de usuario:"}. {"Users are not allowed to register accounts so quickly","Os usuarios non están autorizados a rexistrar contas con tanta rapidez"}. {"Users Last Activity","Última actividade dos usuarios"}. {"Users","Usuarios"}. -{"User ","Usuario "}. {"User","Usuario"}. -{"Validate","Validar"}. -{"vCard User Search","Procura de usuario en vCard"}. +{"Value 'get' of 'type' attribute is not allowed","O valor \"get\" do atributo 'type' non está permitido"}. +{"Value of '~s' should be boolean","O valor de '~s' debería ser booleano"}. +{"Value of '~s' should be datetime string","O valor de '~s' debería ser unha data"}. +{"Value of '~s' should be integer","O valor de '~s' debería ser un enteiro"}. +{"Value 'set' of 'type' attribute is not allowed","O valor \"set\" do atributo 'type' non está permitido"}. +{"vCard User Search","vCard busqueda de usuario"}. {"Virtual Hosts","Hosts Virtuais"}. -{"Visitors are not allowed to change their nicknames in this room","Os visitantes non están autorizados a cambiar os seus That alcumes nesta sala"}. +{"Visitors are not allowed to change their nicknames in this room","Os visitantes non teñen permitido cambiar os seus alcumes nesta sala"}. {"Visitors are not allowed to send messages to all occupants","Os visitantes non poden enviar mensaxes a todos os ocupantes"}. +{"Visitor","Visitante"}. +{"Voice request","Petición de voz"}. +{"Voice requests are disabled in this conference","As peticións de voz están desactivadas nesta sala"}. {"Wednesday","Mércores"}. {"When to send the last published item","Cando enviar o último elemento publicado"}. {"Whether to allow subscriptions","Permitir subscripciones"}. -{"You have been banned from this room","fuches bloqueado nesta sala"}. +{"You have been banned from this room","Fuches bloqueado nesta sala"}. +{"You have joined too many conferences","Entrou en demasiadas salas de conferencia"}. {"You must fill in field \"Nickname\" in the form","Debes encher o campo \"Alcumo\" no formulario"}. -{"You need an x:data capable client to configure mod_irc settings","Necesitas un cliente con soporte de x:data para configurar as opcións de mod_irc"}. -{"You need an x:data capable client to configure room","Necesitas un cliente con soporte de x:data para configurar a sala"}. +{"You need a client that supports x:data and CAPTCHA to register","Necesitas un cliente con soporte de x:data e CAPTCHA para rexistrarche"}. +{"You need a client that supports x:data to register the nickname","Necesitas un cliente con soporte de x:data para poder rexistrar o alcume"}. {"You need an x:data capable client to search","Necesitas un cliente con soporte de x:data para poder buscar"}. +{"Your active privacy list has denied the routing of this stanza.","A súa lista de privacidade activa negou o encaminamiento desta estrofa."}. {"Your contact offline message queue is full. The message has been discarded.","A túa cola de mensaxes diferidas de contactos está chea. A mensaxe descartouse."}. -{"Your messages to ~s are being blocked. To unblock them, visit ~s","As súas mensaxes a ~s encóntranse bloqueadas. Para desbloquear, visite ~s"}. +{"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","As súas mensaxes a ~s encóntranse bloqueadas. Para desbloquear, visite ~s"}. +{"You're not allowed to create nodes","Non tes permiso para crear nodos"}. diff --git a/priv/msgs/gl.po b/priv/msgs/gl.po deleted file mode 100644 index d33f4ebd1..000000000 --- a/priv/msgs/gl.po +++ /dev/null @@ -1,1878 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: 2.1.0-alpha\n" -"Last-Translator: Carlos E. Lopez - suso AT jabber-hispano.org\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Language: Galician (galego)\n" - -#: ejabberd_c2s.erl:424 ejabberd_c2s.erl:727 -msgid "Use of STARTTLS required" -msgstr "É obrigatorio usar STARTTLS" - -#: ejabberd_c2s.erl:503 -msgid "No resource provided" -msgstr "Non se proporcionou recurso" - -#: ejabberd_c2s.erl:1197 -msgid "Replaced by new connection" -msgstr "Substituído por unha nova conexión" - -#: ejabberd_c2s.erl:1885 -msgid "Your active privacy list has denied the routing of this stanza." -msgstr "" - -#: ejabberd_captcha.erl:96 ejabberd_captcha.erl:152 ejabberd_captcha.erl:178 -msgid "Enter the text you see" -msgstr "Introduza o texto que ves" - -#: ejabberd_captcha.erl:101 -msgid "Your messages to ~s are being blocked. To unblock them, visit ~s" -msgstr "" -"As súas mensaxes a ~s encóntranse bloqueadas. Para desbloquear, visite ~s" - -#: ejabberd_captcha.erl:134 -msgid "If you don't see the CAPTCHA image here, visit the web page." -msgstr "" - -#: ejabberd_captcha.erl:146 -msgid "CAPTCHA web page" -msgstr "" - -#: ejabberd_captcha.erl:307 -msgid "The CAPTCHA is valid." -msgstr "O CAPTCHA é válido." - -#: mod_adhoc.erl:95 mod_adhoc.erl:125 mod_adhoc.erl:143 mod_adhoc.erl:161 -msgid "Commands" -msgstr "Comandos" - -#: mod_adhoc.erl:149 mod_adhoc.erl:243 -msgid "Ping" -msgstr "Ping" - -#: mod_adhoc.erl:260 -msgid "Pong" -msgstr "Pong" - -#: mod_announce.erl:507 mod_announce_odbc.erl:499 -msgid "Really delete message of the day?" -msgstr "Está seguro de quere borrar a mensaxe do dia?" - -#: mod_announce.erl:515 mod_announce_odbc.erl:507 mod_configure.erl:1083 -#: mod_configure.erl:1128 -msgid "Subject" -msgstr "Asunto" - -#: mod_announce.erl:520 mod_announce_odbc.erl:512 mod_configure.erl:1088 -#: mod_configure.erl:1133 -msgid "Message body" -msgstr "Corpo da mensaxe" - -#: mod_announce.erl:600 mod_announce_odbc.erl:592 -msgid "No body provided for announce message" -msgstr "Non se proporcionou corpo de mensaxe para o anuncio" - -#: mod_announce.erl:635 mod_announce_odbc.erl:627 -msgid "Announcements" -msgstr "Anuncios" - -#: mod_announce.erl:637 mod_announce_odbc.erl:629 -msgid "Send announcement to all users" -msgstr "Enviar anuncio a todos os usuarios" - -#: mod_announce.erl:639 mod_announce_odbc.erl:631 -msgid "Send announcement to all users on all hosts" -msgstr "Enviar anuncio a todos os usuarios en todos os dominios" - -#: mod_announce.erl:641 mod_announce_odbc.erl:633 -msgid "Send announcement to all online users" -msgstr "Enviar anuncio a todos los usuarios conectados" - -#: mod_announce.erl:643 mod_announce_odbc.erl:635 mod_configure.erl:1078 -#: mod_configure.erl:1123 -msgid "Send announcement to all online users on all hosts" -msgstr "Enviar anuncio a todos os usuarios conectados en todos os dominios" - -#: mod_announce.erl:645 mod_announce_odbc.erl:637 -msgid "Set message of the day and send to online users" -msgstr "Pór mensaxe do dia e enviar a todos os usuarios conectados" - -#: mod_announce.erl:647 mod_announce_odbc.erl:639 -msgid "Set message of the day on all hosts and send to online users" -msgstr "" -"Pór mensaxe do día en todos os dominios e enviar aos usuarios conectados" - -#: mod_announce.erl:649 mod_announce_odbc.erl:641 -msgid "Update message of the day (don't send)" -msgstr "Actualizar mensaxe do dia, pero non envialo" - -#: mod_announce.erl:651 mod_announce_odbc.erl:643 -msgid "Update message of the day on all hosts (don't send)" -msgstr "Actualizar a mensaxe do día en todos os dominos (pero non envialo)" - -#: mod_announce.erl:653 mod_announce_odbc.erl:645 -msgid "Delete message of the day" -msgstr "Borrar mensaxe do dia" - -#: mod_announce.erl:655 mod_announce_odbc.erl:647 -msgid "Delete message of the day on all hosts" -msgstr "Borrar a mensaxe do día en todos os dominios" - -#: mod_configure.erl:114 mod_configure.erl:274 mod_configure.erl:296 -#: mod_configure.erl:498 -msgid "Configuration" -msgstr "Configuración" - -#: mod_configure.erl:125 mod_configure.erl:576 web/ejabberd_web_admin.erl:1936 -msgid "Database" -msgstr "Base de datos" - -#: mod_configure.erl:127 mod_configure.erl:595 -msgid "Start Modules" -msgstr "Iniciar módulos" - -#: mod_configure.erl:129 mod_configure.erl:596 -msgid "Stop Modules" -msgstr "Detener módulos" - -#: mod_configure.erl:131 mod_configure.erl:604 web/ejabberd_web_admin.erl:1937 -msgid "Backup" -msgstr "Gardar copia de seguridade" - -#: mod_configure.erl:133 mod_configure.erl:605 -msgid "Restore" -msgstr "Restaurar" - -#: mod_configure.erl:135 mod_configure.erl:606 -msgid "Dump to Text File" -msgstr "Exportar a ficheiro de texto" - -#: mod_configure.erl:137 mod_configure.erl:615 -msgid "Import File" -msgstr "Importar ficheiro" - -#: mod_configure.erl:139 mod_configure.erl:616 -msgid "Import Directory" -msgstr "Importar directorio" - -#: mod_configure.erl:141 mod_configure.erl:581 mod_configure.erl:1057 -msgid "Restart Service" -msgstr "Reiniciar o servizo" - -#: mod_configure.erl:143 mod_configure.erl:582 mod_configure.erl:1102 -msgid "Shut Down Service" -msgstr "Deter o servizo" - -#: mod_configure.erl:145 mod_configure.erl:518 mod_configure.erl:1197 -#: web/ejabberd_web_admin.erl:1527 -msgid "Add User" -msgstr "Engadir usuario" - -#: mod_configure.erl:147 mod_configure.erl:519 mod_configure.erl:1219 -msgid "Delete User" -msgstr "Borrar usuario" - -#: mod_configure.erl:149 mod_configure.erl:520 mod_configure.erl:1231 -msgid "End User Session" -msgstr "Pechar sesión de usuario" - -#: mod_configure.erl:151 mod_configure.erl:521 mod_configure.erl:1243 -#: mod_configure.erl:1255 -msgid "Get User Password" -msgstr "Ver contrasinal de usuario" - -#: mod_configure.erl:153 mod_configure.erl:522 -msgid "Change User Password" -msgstr "Cambiar contrasinal de usuario" - -#: mod_configure.erl:155 mod_configure.erl:523 mod_configure.erl:1272 -msgid "Get User Last Login Time" -msgstr "Ver data da última conexión de usuario" - -#: mod_configure.erl:157 mod_configure.erl:524 mod_configure.erl:1284 -msgid "Get User Statistics" -msgstr "Ver estatísticas de usuario" - -#: mod_configure.erl:159 mod_configure.erl:525 -msgid "Get Number of Registered Users" -msgstr "Ver número de usuarios rexistrados" - -#: mod_configure.erl:161 mod_configure.erl:526 -msgid "Get Number of Online Users" -msgstr "Ver número de usuarios conectados" - -#: mod_configure.erl:163 mod_configure.erl:509 web/ejabberd_web_admin.erl:831 -#: web/ejabberd_web_admin.erl:872 -msgid "Access Control Lists" -msgstr "Listas de Control de Acceso" - -#: mod_configure.erl:165 mod_configure.erl:510 web/ejabberd_web_admin.erl:940 -#: web/ejabberd_web_admin.erl:976 -msgid "Access Rules" -msgstr "Regras de Acceso" - -#: mod_configure.erl:297 mod_configure.erl:499 -msgid "User Management" -msgstr "Administración de usuarios" - -#: mod_configure.erl:500 web/ejabberd_web_admin.erl:1058 -#: web/ejabberd_web_admin.erl:1462 -msgid "Online Users" -msgstr "Usuarios conectados" - -#: mod_configure.erl:501 -msgid "All Users" -msgstr "Todos os usuarios" - -#: mod_configure.erl:502 -msgid "Outgoing s2s Connections" -msgstr "Conexións S2S saíntes" - -#: mod_configure.erl:503 web/ejabberd_web_admin.erl:1907 -msgid "Running Nodes" -msgstr "Nodos funcionando" - -#: mod_configure.erl:504 web/ejabberd_web_admin.erl:1909 -msgid "Stopped Nodes" -msgstr "Nodos detidos" - -#: mod_configure.erl:577 mod_configure.erl:587 web/ejabberd_web_admin.erl:1953 -msgid "Modules" -msgstr "Módulos" - -#: mod_configure.erl:578 -msgid "Backup Management" -msgstr "Xestión de copia de seguridade" - -#: mod_configure.erl:579 -msgid "Import Users From jabberd14 Spool Files" -msgstr "Importar usuarios de ficheiros spool de jabberd-1.4" - -#: mod_configure.erl:699 -msgid "To ~s" -msgstr "A ~s" - -#: mod_configure.erl:717 -msgid "From ~s" -msgstr "De ~s" - -#: mod_configure.erl:913 -msgid "Database Tables Configuration at " -msgstr "Configuración de táboas da base de datos en " - -#: mod_configure.erl:918 -msgid "Choose storage type of tables" -msgstr "Selecciona tipo de almacenamento das táboas" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Disc only copy" -msgstr "Copia en disco soamente" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM and disc copy" -msgstr "Copia en RAM e disco" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM copy" -msgstr "Copia en RAM" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Remote copy" -msgstr "Copia remota" - -#: mod_configure.erl:950 -msgid "Stop Modules at " -msgstr "Deter módulos en " - -#: mod_configure.erl:954 -msgid "Choose modules to stop" -msgstr "Selecciona módulos a deter" - -#: mod_configure.erl:969 -msgid "Start Modules at " -msgstr "Iniciar módulos en " - -#: mod_configure.erl:973 -msgid "Enter list of {Module, [Options]}" -msgstr "Introduce lista de {Módulo, [Opcións]}" - -#: mod_configure.erl:974 -msgid "List of modules to start" -msgstr "Lista de módulos a iniciar" - -#: mod_configure.erl:983 -msgid "Backup to File at " -msgstr "Copia de seguridade de arquivos en " - -#: mod_configure.erl:987 mod_configure.erl:1001 -msgid "Enter path to backup file" -msgstr "Introduce ruta ao ficheiro de copia de seguridade" - -#: mod_configure.erl:988 mod_configure.erl:1002 mod_configure.erl:1016 -#: mod_configure.erl:1030 -msgid "Path to File" -msgstr "Ruta ao ficheiro" - -#: mod_configure.erl:997 -msgid "Restore Backup from File at " -msgstr "Restaura copia de seguridade desde o ficheiro en " - -#: mod_configure.erl:1011 -msgid "Dump Backup to Text File at " -msgstr "Exporta copia de seguridade a ficheiro de texto en " - -#: mod_configure.erl:1015 -msgid "Enter path to text file" -msgstr "Introduce ruta ao ficheiro de texto" - -#: mod_configure.erl:1025 -msgid "Import User from File at " -msgstr "Importa usuario desde ficheiro en " - -#: mod_configure.erl:1029 -msgid "Enter path to jabberd14 spool file" -msgstr "Introduce ruta ao ficheiro jabberd14 spool" - -#: mod_configure.erl:1039 -msgid "Import Users from Dir at " -msgstr "Importar usuarios desde o directorio en " - -#: mod_configure.erl:1043 -msgid "Enter path to jabberd14 spool dir" -msgstr "Introduce a ruta ao directorio de jabberd14 spools" - -#: mod_configure.erl:1044 -msgid "Path to Dir" -msgstr "Ruta ao directorio" - -#: mod_configure.erl:1060 mod_configure.erl:1105 -msgid "Time delay" -msgstr "Atraso temporal" - -#: mod_configure.erl:1143 -msgid "Access Control List Configuration" -msgstr "Configuración da Lista de Control de Acceso" - -#: mod_configure.erl:1147 -msgid "Access control lists" -msgstr "Listas de Control de Acceso" - -#: mod_configure.erl:1171 -msgid "Access Configuration" -msgstr "Configuración de accesos" - -#: mod_configure.erl:1175 -msgid "Access rules" -msgstr "Regras de acceso" - -#: mod_configure.erl:1200 mod_configure.erl:1222 mod_configure.erl:1234 -#: mod_configure.erl:1246 mod_configure.erl:1258 mod_configure.erl:1275 -#: mod_configure.erl:1287 mod_configure.erl:1650 mod_configure.erl:1700 -#: mod_configure.erl:1721 mod_roster.erl:943 mod_roster_odbc.erl:1060 -#: mod_vcard.erl:472 mod_vcard_ldap.erl:554 mod_vcard_odbc.erl:448 -msgid "Jabber ID" -msgstr "Jabber ID" - -#: mod_configure.erl:1205 mod_configure.erl:1263 mod_configure.erl:1651 -#: mod_configure.erl:1863 mod_muc/mod_muc_room.erl:3224 mod_register.erl:235 -#: web/ejabberd_web_admin.erl:1520 -msgid "Password" -msgstr "Contrasinal" - -#: mod_configure.erl:1210 -msgid "Password Verification" -msgstr "Verificación da contrasinal" - -#: mod_configure.erl:1301 -msgid "Number of registered users" -msgstr "Número de usuarios rexistrados" - -#: mod_configure.erl:1315 -msgid "Number of online users" -msgstr "Número de usuarios conectados" - -#: mod_configure.erl:1682 web/ejabberd_web_admin.erl:1588 -#: web/ejabberd_web_admin.erl:1743 -msgid "Never" -msgstr "Nunca" - -#: mod_configure.erl:1696 web/ejabberd_web_admin.erl:1601 -#: web/ejabberd_web_admin.erl:1756 -msgid "Online" -msgstr "Conectado" - -#: mod_configure.erl:1701 -msgid "Last login" -msgstr "Última conexión" - -#: mod_configure.erl:1722 -msgid "Roster size" -msgstr "Tamaño da lista de contactos" - -#: mod_configure.erl:1723 -msgid "IP addresses" -msgstr "Direccións IP" - -#: mod_configure.erl:1724 -msgid "Resources" -msgstr "Recursos" - -#: mod_configure.erl:1850 -msgid "Administration of " -msgstr "Administración de " - -#: mod_configure.erl:1853 -msgid "Action on user" -msgstr "Acción no usuario" - -#: mod_configure.erl:1857 -msgid "Edit Properties" -msgstr "Editar propiedades" - -#: mod_configure.erl:1860 web/ejabberd_web_admin.erl:1769 -msgid "Remove User" -msgstr "Eliminar usuario" - -#: mod_irc/mod_irc.erl:201 mod_irc/mod_irc_odbc.erl:196 -#: mod_muc/mod_muc.erl:336 mod_muc/mod_muc_odbc.erl:342 -msgid "Access denied by service policy" -msgstr "Acceso denegado pola política do servizo" - -#: mod_irc/mod_irc.erl:401 mod_irc/mod_irc_odbc.erl:398 -msgid "IRC Transport" -msgstr "Transporte IRC" - -#: mod_irc/mod_irc.erl:428 mod_irc/mod_irc_odbc.erl:425 -msgid "ejabberd IRC module" -msgstr "Módulo de IRC para ejabberd" - -#: mod_irc/mod_irc.erl:559 mod_irc/mod_irc_odbc.erl:568 -msgid "You need an x:data capable client to configure mod_irc settings" -msgstr "" -"Necesitas un cliente con soporte de x:data para configurar as opcións de " -"mod_irc" - -#: mod_irc/mod_irc.erl:566 mod_irc/mod_irc_odbc.erl:575 -msgid "Registration in mod_irc for " -msgstr "Rexistro en mod_irc para" - -#: mod_irc/mod_irc.erl:571 mod_irc/mod_irc_odbc.erl:580 -msgid "" -"Enter username, encodings, ports and passwords you wish to use for " -"connecting to IRC servers" -msgstr "" -"Introduza o nome de usuario, codificaciones de carácter, portos e " -"contrasinal que pretende utilizar a conectar a servidores de IRC" - -#: mod_irc/mod_irc.erl:576 mod_irc/mod_irc_odbc.erl:585 -msgid "IRC Username" -msgstr "Nome de usuario en IRC" - -#: mod_irc/mod_irc.erl:586 mod_irc/mod_irc_odbc.erl:595 -msgid "" -"If you want to specify different ports, passwords, encodings for IRC " -"servers, fill this list with values in format '{\"irc server\", \"encoding" -"\", port, \"password\"}'. By default this service use \"~s\" encoding, port " -"~p, empty password." -msgstr "" -"Se quere especificar codificaciones de caracteres diferentes, contrasinal ou " -"servidor IRC rechea esta lista con valores no formato '{\"servidor irc\", " -"\"codificación\", \"porto\", \"contrasinal\"}'. Este servizo utiliza por " -"defecto a codificación \"~s\", porto ~p, sen contrasinal." - -#: mod_irc/mod_irc.erl:598 mod_irc/mod_irc_odbc.erl:607 -msgid "" -"Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef." -"net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]." -msgstr "" -"Exemplo: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef." -"net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]." - -#: mod_irc/mod_irc.erl:603 mod_irc/mod_irc_odbc.erl:612 -msgid "Connections parameters" -msgstr "Parámetros de conexiones" - -#: mod_irc/mod_irc.erl:728 mod_irc/mod_irc_odbc.erl:757 -msgid "Join IRC channel" -msgstr "Entrar en canle IRC" - -#: mod_irc/mod_irc.erl:732 mod_irc/mod_irc_odbc.erl:761 -msgid "IRC channel (don't put the first #)" -msgstr "Canle de IRC (non poñer o primeiro #)" - -#: mod_irc/mod_irc.erl:737 mod_irc/mod_irc_odbc.erl:766 -msgid "IRC server" -msgstr "Servidor IRC" - -#: mod_irc/mod_irc.erl:770 mod_irc/mod_irc.erl:774 -#: mod_irc/mod_irc_odbc.erl:799 mod_irc/mod_irc_odbc.erl:803 -msgid "Join the IRC channel here." -msgstr "Únete á canle de IRC aquí." - -#: mod_irc/mod_irc.erl:778 mod_irc/mod_irc_odbc.erl:807 -msgid "Join the IRC channel in this Jabber ID: ~s" -msgstr "Únete á canle de IRC con este IDE de Jabber: ~s" - -#: mod_irc/mod_irc.erl:863 mod_irc/mod_irc_odbc.erl:904 -msgid "IRC settings" -msgstr "IRC axustes" - -#: mod_irc/mod_irc.erl:868 mod_irc/mod_irc_odbc.erl:909 -msgid "" -"Enter username and encodings you wish to use for connecting to IRC servers. " -"Press 'Next' to get more fields to fill in. Press 'Complete' to save " -"settings." -msgstr "" -"Introduce o nome de usuario e codificaciones de carácteres que queiras usar " -"ao conectar nos servidores de IRC. Presione 'Siguiente' para obtener más " -"campos para rellenar Presione 'completo' para guardar axustes." - -#: mod_irc/mod_irc.erl:874 mod_irc/mod_irc_odbc.erl:915 -msgid "IRC username" -msgstr "Nome de usuario en IRC" - -#: mod_irc/mod_irc.erl:923 mod_irc/mod_irc_odbc.erl:964 -msgid "Password ~b" -msgstr "Contrasinal ~b" - -#: mod_irc/mod_irc.erl:928 mod_irc/mod_irc_odbc.erl:969 -msgid "Port ~b" -msgstr "Porto ~b" - -#: mod_irc/mod_irc.erl:933 mod_irc/mod_irc_odbc.erl:974 -msgid "Encoding for server ~b" -msgstr "Codificación de servidor ~b" - -#: mod_irc/mod_irc.erl:942 mod_irc/mod_irc_odbc.erl:983 -msgid "Server ~b" -msgstr "Servidor ~b" - -#: mod_muc/mod_muc.erl:449 mod_muc/mod_muc_odbc.erl:456 -msgid "Only service administrators are allowed to send service messages" -msgstr "" -"Só os administradores do servizo teñen permiso para enviar mensaxes de " -"servizo" - -#: mod_muc/mod_muc.erl:493 mod_muc/mod_muc_odbc.erl:500 -msgid "Room creation is denied by service policy" -msgstr "Denegar crear a sala por política do servizo" - -#: mod_muc/mod_muc.erl:500 mod_muc/mod_muc_odbc.erl:507 -msgid "Conference room does not exist" -msgstr "A sala de conferencias non existe" - -#: mod_muc/mod_muc.erl:582 mod_muc/mod_muc_odbc.erl:595 -msgid "Chatrooms" -msgstr "Salas de charla" - -#: mod_muc/mod_muc.erl:712 mod_muc/mod_muc_odbc.erl:729 -#, fuzzy -msgid "You need a client that supports x:data to register the nickname" -msgstr "" -"Necesitas un cliente con soporte de x:data para poder rexistrar o alcume" - -#: mod_muc/mod_muc.erl:718 mod_muc/mod_muc_odbc.erl:735 -msgid "Nickname Registration at " -msgstr "Rexistro do alcume en " - -#: mod_muc/mod_muc.erl:722 mod_muc/mod_muc_odbc.erl:739 -msgid "Enter nickname you want to register" -msgstr "Introduce o alcume que queiras rexistrar" - -#: mod_muc/mod_muc.erl:723 mod_muc/mod_muc_odbc.erl:740 -#: mod_muc/mod_muc_room.erl:3810 mod_roster.erl:944 mod_roster_odbc.erl:1061 -#: mod_vcard.erl:364 mod_vcard.erl:477 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:453 -msgid "Nickname" -msgstr "Alcume" - -#: mod_muc/mod_muc.erl:762 mod_muc/mod_muc_odbc.erl:784 -#: mod_muc/mod_muc_room.erl:1064 mod_muc/mod_muc_room.erl:1787 -msgid "That nickname is registered by another person" -msgstr "O alcume xa está rexistrado por outra persoa" - -#: mod_muc/mod_muc.erl:788 mod_muc/mod_muc_odbc.erl:811 -msgid "You must fill in field \"Nickname\" in the form" -msgstr "Debes encher o campo \"Alcumo\" no formulario" - -#: mod_muc/mod_muc.erl:808 mod_muc/mod_muc_odbc.erl:831 -msgid "ejabberd MUC module" -msgstr "Módulo de MUC para ejabberd" - -#: mod_muc/mod_muc_log.erl:374 mod_muc/mod_muc_log.erl:381 -msgid "Chatroom configuration modified" -msgstr "Configuración de la sala modificada" - -#: mod_muc/mod_muc_log.erl:384 -msgid "joins the room" -msgstr "entra en la sala" - -#: mod_muc/mod_muc_log.erl:387 mod_muc/mod_muc_log.erl:390 -msgid "leaves the room" -msgstr "sae da sala" - -#: mod_muc/mod_muc_log.erl:393 mod_muc/mod_muc_log.erl:396 -msgid "has been banned" -msgstr "foi bloqueado" - -#: mod_muc/mod_muc_log.erl:399 mod_muc/mod_muc_log.erl:402 -msgid "has been kicked" -msgstr "foi expulsado" - -#: mod_muc/mod_muc_log.erl:405 -msgid "has been kicked because of an affiliation change" -msgstr "foi expulsado debido a un cambio de afiliación" - -#: mod_muc/mod_muc_log.erl:408 -msgid "has been kicked because the room has been changed to members-only" -msgstr "foi expulsado, porque a sala cambiouse a só-membros" - -#: mod_muc/mod_muc_log.erl:411 -msgid "has been kicked because of a system shutdown" -msgstr "foi expulsado por mor dun sistema de peche" - -#: mod_muc/mod_muc_log.erl:414 -msgid "is now known as" -msgstr "cámbiase o nome a" - -#: mod_muc/mod_muc_log.erl:417 mod_muc/mod_muc_log.erl:688 -#: mod_muc/mod_muc_room.erl:2393 -msgid " has set the subject to: " -msgstr " puxo o asunto: " - -#: mod_muc/mod_muc_log.erl:452 -#, fuzzy -msgid "Chatroom is created" -msgstr "Salas de charla" - -#: mod_muc/mod_muc_log.erl:453 -#, fuzzy -msgid "Chatroom is destroyed" -msgstr "Salas de charla" - -#: mod_muc/mod_muc_log.erl:454 -#, fuzzy -msgid "Chatroom is started" -msgstr "Salas de charla" - -#: mod_muc/mod_muc_log.erl:455 -#, fuzzy -msgid "Chatroom is stopped" -msgstr "Salas de charla" - -#: mod_muc/mod_muc_log.erl:459 -msgid "Monday" -msgstr "Luns" - -#: mod_muc/mod_muc_log.erl:460 -msgid "Tuesday" -msgstr "Martes" - -#: mod_muc/mod_muc_log.erl:461 -msgid "Wednesday" -msgstr "Mércores" - -#: mod_muc/mod_muc_log.erl:462 -msgid "Thursday" -msgstr "Xoves" - -#: mod_muc/mod_muc_log.erl:463 -msgid "Friday" -msgstr "Venres" - -#: mod_muc/mod_muc_log.erl:464 -msgid "Saturday" -msgstr "Sábado" - -#: mod_muc/mod_muc_log.erl:465 -msgid "Sunday" -msgstr "Domingo" - -#: mod_muc/mod_muc_log.erl:469 -msgid "January" -msgstr "Xaneiro" - -#: mod_muc/mod_muc_log.erl:470 -msgid "February" -msgstr "Febreiro" - -#: mod_muc/mod_muc_log.erl:471 -msgid "March" -msgstr "Marzo" - -#: mod_muc/mod_muc_log.erl:472 -msgid "April" -msgstr "Abril" - -#: mod_muc/mod_muc_log.erl:473 -msgid "May" -msgstr "Maio" - -#: mod_muc/mod_muc_log.erl:474 -msgid "June" -msgstr "Xuño" - -#: mod_muc/mod_muc_log.erl:475 -msgid "July" -msgstr "Xullo" - -#: mod_muc/mod_muc_log.erl:476 -msgid "August" -msgstr "Agosto" - -#: mod_muc/mod_muc_log.erl:477 -msgid "September" -msgstr "Setembro" - -#: mod_muc/mod_muc_log.erl:478 -msgid "October" -msgstr "Outubro" - -#: mod_muc/mod_muc_log.erl:479 -msgid "November" -msgstr "Novembro" - -#: mod_muc/mod_muc_log.erl:480 -msgid "December" -msgstr "Decembro" - -#: mod_muc/mod_muc_log.erl:750 -msgid "Room Configuration" -msgstr "Configuración da Sala" - -#: mod_muc/mod_muc_log.erl:759 -msgid "Room Occupants" -msgstr "Ocupantes da sala" - -#: mod_muc/mod_muc_room.erl:174 -msgid "Traffic rate limit is exceeded" -msgstr "Hase exedido o límite de tráfico" - -#: mod_muc/mod_muc_room.erl:246 -msgid "" -"This participant is kicked from the room because he sent an error message" -msgstr "" -"Este participante é expulsado da sala, xa que enviou unha mensaxe de erro" - -#: mod_muc/mod_muc_room.erl:255 -msgid "It is not allowed to send private messages to the conference" -msgstr "Impedir o envio de mensaxes privadas á sala" - -#: mod_muc/mod_muc_room.erl:332 -msgid "Please, wait for a while before sending new voice request" -msgstr "" - -#: mod_muc/mod_muc_room.erl:347 -msgid "Voice requests are disabled in this conference" -msgstr "" - -#: mod_muc/mod_muc_room.erl:364 -msgid "Failed to extract JID from your voice request approval" -msgstr "" - -#: mod_muc/mod_muc_room.erl:393 -msgid "Only moderators can approve voice requests" -msgstr "" - -#: mod_muc/mod_muc_room.erl:408 -msgid "Improper message type" -msgstr "Tipo de mensaxe incorrecta" - -#: mod_muc/mod_muc_room.erl:518 -msgid "" -"This participant is kicked from the room because he sent an error message to " -"another participant" -msgstr "" -"Este participante é expulsado da sala, porque el enviou unha mensaxe de erro " -"a outro participante" - -#: mod_muc/mod_muc_room.erl:531 -msgid "It is not allowed to send private messages of type \"groupchat\"" -msgstr "Non está permitido enviar mensaxes privadas do tipo \"groupchat\"" - -#: mod_muc/mod_muc_room.erl:543 mod_muc/mod_muc_room.erl:611 -msgid "Recipient is not in the conference room" -msgstr "O receptor non está na sala de conferencia" - -#: mod_muc/mod_muc_room.erl:564 mod_muc/mod_muc_room.erl:585 -msgid "It is not allowed to send private messages" -msgstr "Non está permitido enviar mensaxes privadas" - -#: mod_muc/mod_muc_room.erl:576 mod_muc/mod_muc_room.erl:956 -#: mod_muc/mod_muc_room.erl:4040 -msgid "Only occupants are allowed to send messages to the conference" -msgstr "Só os ocupantes poden enviar mensaxes á sala" - -#: mod_muc/mod_muc_room.erl:634 -msgid "Only occupants are allowed to send queries to the conference" -msgstr "Só os ocupantes poden enviar solicitudes á sala" - -#: mod_muc/mod_muc_room.erl:646 -msgid "Queries to the conference members are not allowed in this room" -msgstr "Nesta sala non se permiten solicitudes aos membros da sala" - -#: mod_muc/mod_muc_room.erl:932 -msgid "" -"Only moderators and participants are allowed to change the subject in this " -"room" -msgstr "" -"Só os moderadores e os participantes se lles permite cambiar o tema nesta " -"sala" - -#: mod_muc/mod_muc_room.erl:937 -msgid "Only moderators are allowed to change the subject in this room" -msgstr "Só os moderadores están autorizados a cambiar o tema nesta sala" - -#: mod_muc/mod_muc_room.erl:947 -msgid "Visitors are not allowed to send messages to all occupants" -msgstr "Os visitantes non poden enviar mensaxes a todos os ocupantes" - -#: mod_muc/mod_muc_room.erl:1021 -msgid "" -"This participant is kicked from the room because he sent an error presence" -msgstr "" -"Este participante é expulsado da sala, porque el enviou un erro de presenza" - -#: mod_muc/mod_muc_room.erl:1040 -msgid "Visitors are not allowed to change their nicknames in this room" -msgstr "" -"Os visitantes non están autorizados a cambiar os seus That alcumes nesta sala" - -#: mod_muc/mod_muc_room.erl:1053 mod_muc/mod_muc_room.erl:1779 -msgid "That nickname is already in use by another occupant" -msgstr "Ese alcume que xa está en uso por outro ocupante" - -#: mod_muc/mod_muc_room.erl:1768 -msgid "You have been banned from this room" -msgstr "fuches bloqueado nesta sala" - -#: mod_muc/mod_muc_room.erl:1771 -msgid "Membership is required to enter this room" -msgstr "Necesitas ser membro desta sala para poder entrar" - -#: mod_muc/mod_muc_room.erl:1807 -msgid "This room is not anonymous" -msgstr "Sala non anónima" - -#: mod_muc/mod_muc_room.erl:1833 -msgid "A password is required to enter this room" -msgstr "Necesítase contrasinal para entrar nesta sala" - -#: mod_muc/mod_muc_room.erl:1855 mod_register.erl:246 -msgid "Too many CAPTCHA requests" -msgstr "" - -#: mod_muc/mod_muc_room.erl:1864 mod_register.erl:251 -#, fuzzy -msgid "Unable to generate a CAPTCHA" -msgstr "Non se pode xerar un CAPTCHA" - -#: mod_muc/mod_muc_room.erl:1874 -msgid "Incorrect password" -msgstr "Contrasinal incorrecta" - -#: mod_muc/mod_muc_room.erl:2448 -msgid "Administrator privileges required" -msgstr "Necesítase privilexios de administrador" - -#: mod_muc/mod_muc_room.erl:2463 -msgid "Moderator privileges required" -msgstr "Necesítase privilexios de moderador" - -#: mod_muc/mod_muc_room.erl:2619 -msgid "Jabber ID ~s is invalid" -msgstr "O Jabber ID ~s non é válido" - -#: mod_muc/mod_muc_room.erl:2633 -msgid "Nickname ~s does not exist in the room" -msgstr "O alcume ~s non existe na sala" - -#: mod_muc/mod_muc_room.erl:2659 mod_muc/mod_muc_room.erl:3049 -msgid "Invalid affiliation: ~s" -msgstr "Afiliación non válida: ~s" - -#: mod_muc/mod_muc_room.erl:2713 -msgid "Invalid role: ~s" -msgstr "Rol non válido: ~s" - -#: mod_muc/mod_muc_room.erl:3026 mod_muc/mod_muc_room.erl:3062 -msgid "Owner privileges required" -msgstr "Requírense privilexios de propietario da sala" - -#: mod_muc/mod_muc_room.erl:3195 -msgid "Configuration of room ~s" -msgstr "Configuración para a sala ~s" - -#: mod_muc/mod_muc_room.erl:3200 -msgid "Room title" -msgstr "Título da sala" - -#: mod_muc/mod_muc_room.erl:3203 mod_muc/mod_muc_room.erl:3692 -msgid "Room description" -msgstr "Descrición da sala" - -#: mod_muc/mod_muc_room.erl:3210 -msgid "Make room persistent" -msgstr "Sala permanente" - -#: mod_muc/mod_muc_room.erl:3215 -msgid "Make room public searchable" -msgstr "Sala publicamente visible" - -#: mod_muc/mod_muc_room.erl:3218 -msgid "Make participants list public" -msgstr "A lista de participantes é pública" - -#: mod_muc/mod_muc_room.erl:3221 -msgid "Make room password protected" -msgstr "Protexer a sala con contrasinal" - -#: mod_muc/mod_muc_room.erl:3232 -msgid "Maximum Number of Occupants" -msgstr "Número máximo de ocupantes" - -#: mod_muc/mod_muc_room.erl:3239 -msgid "No limit" -msgstr "Sen límite" - -#: mod_muc/mod_muc_room.erl:3250 -msgid "Present real Jabber IDs to" -msgstr "Os Jabber ID reais poden velos" - -#: mod_muc/mod_muc_room.erl:3258 mod_muc/mod_muc_room.erl:3292 -msgid "moderators only" -msgstr "só moderadores" - -#: mod_muc/mod_muc_room.erl:3260 mod_muc/mod_muc_room.erl:3294 -msgid "anyone" -msgstr "calquera" - -#: mod_muc/mod_muc_room.erl:3262 -msgid "Make room members-only" -msgstr "Sala só para membros" - -#: mod_muc/mod_muc_room.erl:3265 -msgid "Make room moderated" -msgstr "Facer sala moderada" - -#: mod_muc/mod_muc_room.erl:3268 -msgid "Default users as participants" -msgstr "Os usuarios son participantes por defecto" - -#: mod_muc/mod_muc_room.erl:3271 -msgid "Allow users to change the subject" -msgstr "Permitir aos usuarios cambiar o asunto" - -#: mod_muc/mod_muc_room.erl:3274 -msgid "Allow users to send private messages" -msgstr "Permitir aos usuarios enviar mensaxes privadas" - -#: mod_muc/mod_muc_room.erl:3279 -#, fuzzy -msgid "Allow visitors to send private messages to" -msgstr "Permitir aos usuarios enviar mensaxes privadas" - -#: mod_muc/mod_muc_room.erl:3290 -msgid "nobody" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3296 -msgid "Allow users to query other users" -msgstr "Permitir aos usuarios consultar a outros usuarios" - -#: mod_muc/mod_muc_room.erl:3299 -msgid "Allow users to send invites" -msgstr "Permitir aos usuarios enviar invitacións" - -#: mod_muc/mod_muc_room.erl:3302 -msgid "Allow visitors to send status text in presence updates" -msgstr "" -"Permitir aos visitantes enviar texto de estado nas actualizacións depresenza" - -#: mod_muc/mod_muc_room.erl:3305 -msgid "Allow visitors to change nickname" -msgstr "Permitir aos visitantes cambiarse o alcume" - -#: mod_muc/mod_muc_room.erl:3308 -#, fuzzy -msgid "Allow visitors to send voice requests" -msgstr "Permitir aos usuarios enviar invitacións" - -#: mod_muc/mod_muc_room.erl:3311 -msgid "Minimum interval between voice requests (in seconds)" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3317 -msgid "Make room CAPTCHA protected" -msgstr "Protexer a sala con CAPTCHA" - -#: mod_muc/mod_muc_room.erl:3322 -msgid "Exclude Jabber IDs from CAPTCHA challenge" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3329 -msgid "Enable logging" -msgstr "Gardar históricos" - -#: mod_muc/mod_muc_room.erl:3337 -msgid "You need an x:data capable client to configure room" -msgstr "Necesitas un cliente con soporte de x:data para configurar a sala" - -#: mod_muc/mod_muc_room.erl:3694 -msgid "Number of occupants" -msgstr "Número de ocupantes" - -#: mod_muc/mod_muc_room.erl:3750 -msgid "private, " -msgstr "privado" - -#: mod_muc/mod_muc_room.erl:3799 -msgid "Voice request" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3803 -msgid "Either approve or decline the voice request." -msgstr "" - -#: mod_muc/mod_muc_room.erl:3809 -#, fuzzy -msgid "User JID" -msgstr "Usuario " - -#: mod_muc/mod_muc_room.erl:3811 -msgid "Grant voice to this person?" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3960 -msgid "~s invites you to the room ~s" -msgstr "~s invítache á sala ~s" - -#: mod_muc/mod_muc_room.erl:3969 -msgid "the password is" -msgstr "a contrasinal é" - -#: mod_offline.erl:510 mod_offline_odbc.erl:352 -msgid "" -"Your contact offline message queue is full. The message has been discarded." -msgstr "" -"A túa cola de mensaxes diferidas de contactos está chea. A mensaxe " -"descartouse." - -#: mod_offline.erl:560 mod_offline_odbc.erl:408 -msgid "~s's Offline Messages Queue" -msgstr "Cola de mensaxes diferidas de ~s" - -#: mod_offline.erl:563 mod_offline_odbc.erl:411 mod_roster.erl:987 -#: mod_roster_odbc.erl:1104 mod_shared_roster.erl:901 -#: mod_shared_roster.erl:1010 mod_shared_roster_odbc.erl:930 -#: mod_shared_roster_odbc.erl:1039 web/ejabberd_web_admin.erl:833 -#: web/ejabberd_web_admin.erl:874 web/ejabberd_web_admin.erl:942 -#: web/ejabberd_web_admin.erl:978 web/ejabberd_web_admin.erl:1019 -#: web/ejabberd_web_admin.erl:1508 web/ejabberd_web_admin.erl:1760 -#: web/ejabberd_web_admin.erl:1931 web/ejabberd_web_admin.erl:1963 -#: web/ejabberd_web_admin.erl:2031 web/ejabberd_web_admin.erl:2135 -#: web/ejabberd_web_admin.erl:2160 web/ejabberd_web_admin.erl:2248 -msgid "Submitted" -msgstr "Enviado" - -#: mod_offline.erl:571 -msgid "Time" -msgstr "Data" - -#: mod_offline.erl:572 -msgid "From" -msgstr "De" - -#: mod_offline.erl:573 -msgid "To" -msgstr "Para" - -#: mod_offline.erl:574 mod_offline_odbc.erl:419 -msgid "Packet" -msgstr "Paquete" - -#: mod_offline.erl:587 mod_offline_odbc.erl:432 mod_shared_roster.erl:908 -#: mod_shared_roster_odbc.erl:937 web/ejabberd_web_admin.erl:882 -#: web/ejabberd_web_admin.erl:986 -msgid "Delete Selected" -msgstr "Eliminar os seleccionados" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -msgid "Offline Messages:" -msgstr "Mensaxes sen conexión:" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -msgid "Remove All Offline Messages" -msgstr "Borrar Todas as Mensaxes Sen conexión" - -#: mod_proxy65/mod_proxy65_service.erl:213 -msgid "ejabberd SOCKS5 Bytestreams module" -msgstr "ejabberd SOCKS5 Bytestreams module" - -#: mod_pubsub/mod_pubsub.erl:1120 mod_pubsub/mod_pubsub_odbc.erl:929 -msgid "Publish-Subscribe" -msgstr "Publicar-Subscribir" - -#: mod_pubsub/mod_pubsub.erl:1214 mod_pubsub/mod_pubsub_odbc.erl:1025 -msgid "ejabberd Publish-Subscribe module" -msgstr "Módulo de Publicar-Subscribir de ejabberd" - -#: mod_pubsub/mod_pubsub.erl:1497 mod_pubsub/mod_pubsub_odbc.erl:1312 -msgid "PubSub subscriber request" -msgstr "Petición de subscriptor de PubSub" - -#: mod_pubsub/mod_pubsub.erl:1499 mod_pubsub/mod_pubsub_odbc.erl:1314 -msgid "Choose whether to approve this entity's subscription." -msgstr "Decidir se aprobar a subscripción desta entidade." - -#: mod_pubsub/mod_pubsub.erl:1505 mod_pubsub/mod_pubsub_odbc.erl:1320 -msgid "Node ID" -msgstr "Nodo IDE" - -#: mod_pubsub/mod_pubsub.erl:1510 mod_pubsub/mod_pubsub_odbc.erl:1325 -msgid "Subscriber Address" -msgstr "Dirección do subscriptor" - -#: mod_pubsub/mod_pubsub.erl:1516 mod_pubsub/mod_pubsub_odbc.erl:1331 -msgid "Allow this Jabber ID to subscribe to this pubsub node?" -msgstr "Desexas permitir a este JabberID que se subscriba a este nodo PubSub?" - -#: mod_pubsub/mod_pubsub.erl:3474 mod_pubsub/mod_pubsub_odbc.erl:3287 -msgid "Deliver payloads with event notifications" -msgstr "Enviar payloads xunto coas notificacións de eventos" - -#: mod_pubsub/mod_pubsub.erl:3475 mod_pubsub/mod_pubsub_odbc.erl:3288 -msgid "Deliver event notifications" -msgstr "Entregar notificacións de eventos" - -#: mod_pubsub/mod_pubsub.erl:3476 mod_pubsub/mod_pubsub_odbc.erl:3289 -msgid "Notify subscribers when the node configuration changes" -msgstr "Notificar subscriptores cando cambia a configuración do nodo" - -#: mod_pubsub/mod_pubsub.erl:3477 mod_pubsub/mod_pubsub_odbc.erl:3290 -msgid "Notify subscribers when the node is deleted" -msgstr "Notificar subscriptores cando o nodo bórrase" - -#: mod_pubsub/mod_pubsub.erl:3478 mod_pubsub/mod_pubsub_odbc.erl:3291 -msgid "Notify subscribers when items are removed from the node" -msgstr "Notificar subscriptores cando os elementos bórranse do nodo" - -#: mod_pubsub/mod_pubsub.erl:3479 mod_pubsub/mod_pubsub_odbc.erl:3292 -msgid "Persist items to storage" -msgstr "Persistir elementos ao almacenar" - -#: mod_pubsub/mod_pubsub.erl:3480 mod_pubsub/mod_pubsub_odbc.erl:3293 -msgid "A friendly name for the node" -msgstr "Un nome para o nodo" - -#: mod_pubsub/mod_pubsub.erl:3481 mod_pubsub/mod_pubsub_odbc.erl:3294 -msgid "Max # of items to persist" -msgstr "Máximo # de elementos que persisten" - -#: mod_pubsub/mod_pubsub.erl:3482 mod_pubsub/mod_pubsub_odbc.erl:3295 -msgid "Whether to allow subscriptions" -msgstr "Permitir subscripciones" - -#: mod_pubsub/mod_pubsub.erl:3483 mod_pubsub/mod_pubsub_odbc.erl:3296 -msgid "Specify the access model" -msgstr "Especifica o modelo de acceso" - -#: mod_pubsub/mod_pubsub.erl:3486 mod_pubsub/mod_pubsub_odbc.erl:3299 -msgid "Roster groups allowed to subscribe" -msgstr "Lista de grupos autorizados a subscribir" - -#: mod_pubsub/mod_pubsub.erl:3487 mod_pubsub/mod_pubsub_odbc.erl:3300 -msgid "Specify the publisher model" -msgstr "Especificar o modelo do publicante" - -#: mod_pubsub/mod_pubsub.erl:3489 mod_pubsub/mod_pubsub_odbc.erl:3302 -msgid "Purge all items when the relevant publisher goes offline" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:3490 mod_pubsub/mod_pubsub_odbc.erl:3303 -#, fuzzy -msgid "Specify the event message type" -msgstr "Especifica o modelo de acceso" - -#: mod_pubsub/mod_pubsub.erl:3492 mod_pubsub/mod_pubsub_odbc.erl:3305 -msgid "Max payload size in bytes" -msgstr "Máximo tamaño do payload en bytes" - -#: mod_pubsub/mod_pubsub.erl:3493 mod_pubsub/mod_pubsub_odbc.erl:3306 -msgid "When to send the last published item" -msgstr "Cando enviar o último elemento publicado" - -#: mod_pubsub/mod_pubsub.erl:3495 mod_pubsub/mod_pubsub_odbc.erl:3308 -msgid "Only deliver notifications to available users" -msgstr "Só enviar notificacións aos usuarios dispoñibles" - -#: mod_pubsub/mod_pubsub.erl:3496 mod_pubsub/mod_pubsub_odbc.erl:3309 -msgid "The collections with which a node is affiliated" -msgstr "As coleccións coas que un nodo está afiliado" - -#: mod_register.erl:193 -msgid "The CAPTCHA verification has failed" -msgstr "" - -#: mod_register.erl:220 -#, fuzzy -msgid "You need a client that supports x:data and CAPTCHA to register" -msgstr "" -"Necesitas un cliente con soporte de x:data para poder rexistrar o alcume" - -#: mod_register.erl:226 mod_register.erl:265 -msgid "Choose a username and password to register with this server" -msgstr "" -"Escolle un nome de usuario e contrasinal para rexistrarche neste servidor" - -#: mod_register.erl:230 mod_vcard.erl:364 mod_vcard_odbc.erl:342 -#: web/ejabberd_web_admin.erl:1515 web/ejabberd_web_admin.erl:1572 -msgid "User" -msgstr "Usuario" - -#: mod_register.erl:316 mod_register.erl:361 -#, fuzzy -msgid "The password is too weak" -msgstr "a contrasinal é" - -#: mod_register.erl:365 -msgid "Users are not allowed to register accounts so quickly" -msgstr "Os usuarios non están autorizados a rexistrar contas con tanta rapidez" - -#: mod_roster.erl:938 mod_roster_odbc.erl:1055 web/ejabberd_web_admin.erl:1701 -#: web/ejabberd_web_admin.erl:1886 web/ejabberd_web_admin.erl:1897 -#: web/ejabberd_web_admin.erl:2219 -msgid "None" -msgstr "Ningún" - -#: mod_roster.erl:945 mod_roster_odbc.erl:1062 -msgid "Subscription" -msgstr "Subscripción" - -#: mod_roster.erl:946 mod_roster_odbc.erl:1063 -msgid "Pending" -msgstr "Pendente" - -#: mod_roster.erl:947 mod_roster_odbc.erl:1064 -msgid "Groups" -msgstr "Grupos" - -#: mod_roster.erl:974 mod_roster_odbc.erl:1091 -msgid "Validate" -msgstr "Validar" - -#: mod_roster.erl:982 mod_roster_odbc.erl:1099 -msgid "Remove" -msgstr "Borrar" - -#: mod_roster.erl:985 mod_roster_odbc.erl:1102 -msgid "Roster of " -msgstr "Lista de contactos de " - -#: mod_roster.erl:988 mod_roster_odbc.erl:1105 mod_shared_roster.erl:902 -#: mod_shared_roster.erl:1011 mod_shared_roster_odbc.erl:931 -#: mod_shared_roster_odbc.erl:1040 web/ejabberd_web_admin.erl:834 -#: web/ejabberd_web_admin.erl:875 web/ejabberd_web_admin.erl:943 -#: web/ejabberd_web_admin.erl:979 web/ejabberd_web_admin.erl:1020 -#: web/ejabberd_web_admin.erl:1509 web/ejabberd_web_admin.erl:1761 -#: web/ejabberd_web_admin.erl:1932 web/ejabberd_web_admin.erl:2136 -#: web/ejabberd_web_admin.erl:2161 -msgid "Bad format" -msgstr "Mal formato" - -#: mod_roster.erl:995 mod_roster_odbc.erl:1112 -msgid "Add Jabber ID" -msgstr "Engadir ID Jabber" - -#: mod_roster.erl:1094 mod_roster_odbc.erl:1211 -msgid "Roster" -msgstr "Lista de contactos" - -#: mod_shared_roster.erl:857 mod_shared_roster.erl:899 -#: mod_shared_roster.erl:1007 mod_shared_roster_odbc.erl:886 -#: mod_shared_roster_odbc.erl:928 mod_shared_roster_odbc.erl:1036 -msgid "Shared Roster Groups" -msgstr "Grupos Compartidos" - -#: mod_shared_roster.erl:895 mod_shared_roster_odbc.erl:924 -#: web/ejabberd_web_admin.erl:1365 web/ejabberd_web_admin.erl:2461 -msgid "Add New" -msgstr "Engadir novo" - -#: mod_shared_roster.erl:978 mod_shared_roster_odbc.erl:1007 -msgid "Name:" -msgstr "Nome:" - -#: mod_shared_roster.erl:983 mod_shared_roster_odbc.erl:1012 -msgid "Description:" -msgstr "Descrición:" - -#: mod_shared_roster.erl:991 mod_shared_roster_odbc.erl:1020 -msgid "Members:" -msgstr "Membros:" - -#: mod_shared_roster.erl:999 mod_shared_roster_odbc.erl:1028 -msgid "Displayed Groups:" -msgstr "Mostrar grupos:" - -#: mod_shared_roster.erl:1008 mod_shared_roster_odbc.erl:1037 -msgid "Group " -msgstr "Grupo " - -#: mod_shared_roster.erl:1017 mod_shared_roster_odbc.erl:1046 -#: web/ejabberd_web_admin.erl:840 web/ejabberd_web_admin.erl:884 -#: web/ejabberd_web_admin.erl:949 web/ejabberd_web_admin.erl:1026 -#: web/ejabberd_web_admin.erl:2017 -msgid "Submit" -msgstr "Enviar" - -#: mod_vcard.erl:165 mod_vcard_ldap.erl:238 mod_vcard_odbc.erl:129 -msgid "Erlang Jabber Server" -msgstr "Servidor Jabber en Erlang" - -#: mod_vcard.erl:364 mod_vcard.erl:478 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:454 -msgid "Birthday" -msgstr "Aniversario" - -#: mod_vcard.erl:364 mod_vcard.erl:480 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:456 -msgid "City" -msgstr "Cidade" - -#: mod_vcard.erl:364 mod_vcard.erl:479 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:455 -msgid "Country" -msgstr "País" - -#: mod_vcard.erl:364 mod_vcard.erl:481 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:457 -msgid "Email" -msgstr "Email" - -#: mod_vcard.erl:364 mod_vcard.erl:476 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:452 -msgid "Family Name" -msgstr "Apelido" - -#: mod_vcard.erl:364 mod_vcard_odbc.erl:342 -msgid "" -"Fill in the form to search for any matching Jabber User (Add * to the end of " -"field to match substring)" -msgstr "" -"Enche o formulario para buscar usuarios Jabber. Engade * ao final dun campo " -"para buscar subcadenas." - -#: mod_vcard.erl:364 mod_vcard.erl:473 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:449 -msgid "Full Name" -msgstr "Nome completo" - -#: mod_vcard.erl:364 mod_vcard.erl:475 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:451 -msgid "Middle Name" -msgstr "Segundo nome" - -#: mod_vcard.erl:364 mod_vcard.erl:474 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:450 web/ejabberd_web_admin.erl:2006 -msgid "Name" -msgstr "Nome" - -#: mod_vcard.erl:364 mod_vcard.erl:482 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:458 -msgid "Organization Name" -msgstr "Nome da organización" - -#: mod_vcard.erl:364 mod_vcard.erl:483 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:459 -msgid "Organization Unit" -msgstr "Unidade da organización" - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "Search users in " -msgstr "Buscar usuarios en " - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "You need an x:data capable client to search" -msgstr "Necesitas un cliente con soporte de x:data para poder buscar" - -#: mod_vcard.erl:389 mod_vcard_ldap.erl:487 mod_vcard_odbc.erl:367 -msgid "vCard User Search" -msgstr "Procura de usuario en vCard" - -#: mod_vcard.erl:445 mod_vcard_ldap.erl:541 mod_vcard_odbc.erl:421 -msgid "ejabberd vCard module" -msgstr "Módulo vCard para ejabberd" - -#: mod_vcard.erl:469 mod_vcard_ldap.erl:551 mod_vcard_odbc.erl:445 -msgid "Search Results for " -msgstr "Buscar resultados por " - -#: mod_vcard_ldap.erl:462 -msgid "Fill in fields to search for any matching Jabber User" -msgstr "Rechea campos para buscar usuarios Jabber que concuerden" - -#: web/ejabberd_web_admin.erl:193 web/ejabberd_web_admin.erl:203 -#: web/ejabberd_web_admin.erl:219 web/ejabberd_web_admin.erl:229 -msgid "Unauthorized" -msgstr "Non autorizado" - -#: web/ejabberd_web_admin.erl:286 web/ejabberd_web_admin.erl:303 -msgid "ejabberd Web Admin" -msgstr "Ejabberd Administrador Web" - -#: web/ejabberd_web_admin.erl:769 web/ejabberd_web_admin.erl:780 -msgid "Administration" -msgstr "Administración" - -#: web/ejabberd_web_admin.erl:878 web/ejabberd_web_admin.erl:982 -msgid "Raw" -msgstr "Cru" - -#: web/ejabberd_web_admin.erl:1017 -msgid "~s access rule configuration" -msgstr "Configuración das Regra de Acceso ~s" - -#: web/ejabberd_web_admin.erl:1035 -msgid "Virtual Hosts" -msgstr "Hosts Virtuais" - -#: web/ejabberd_web_admin.erl:1043 web/ejabberd_web_admin.erl:1050 -msgid "Users" -msgstr "Usuarios" - -#: web/ejabberd_web_admin.erl:1078 -msgid "Users Last Activity" -msgstr "Última actividade dos usuarios" - -#: web/ejabberd_web_admin.erl:1080 -msgid "Period: " -msgstr "Periodo: " - -#: web/ejabberd_web_admin.erl:1090 -msgid "Last month" -msgstr "Último mes" - -#: web/ejabberd_web_admin.erl:1091 -msgid "Last year" -msgstr "Último ano" - -#: web/ejabberd_web_admin.erl:1092 -msgid "All activity" -msgstr "Toda a actividade" - -#: web/ejabberd_web_admin.erl:1094 -msgid "Show Ordinary Table" -msgstr "Mostrar Táboa Ordinaria" - -#: web/ejabberd_web_admin.erl:1096 -msgid "Show Integral Table" -msgstr "Mostrar Táboa Integral" - -#: web/ejabberd_web_admin.erl:1105 web/ejabberd_web_admin.erl:1939 -msgid "Statistics" -msgstr "Estatísticas" - -#: web/ejabberd_web_admin.erl:1117 -msgid "Not Found" -msgstr "Non atopado" - -#: web/ejabberd_web_admin.erl:1134 -msgid "Node not found" -msgstr "Nodo non atopado" - -#: web/ejabberd_web_admin.erl:1460 -msgid "Host" -msgstr "Host" - -#: web/ejabberd_web_admin.erl:1461 -msgid "Registered Users" -msgstr "Usuarios rexistrados" - -#: web/ejabberd_web_admin.erl:1573 -msgid "Offline Messages" -msgstr "Mensaxes diferidas" - -#: web/ejabberd_web_admin.erl:1574 web/ejabberd_web_admin.erl:1767 -msgid "Last Activity" -msgstr "Última actividade" - -#: web/ejabberd_web_admin.erl:1659 web/ejabberd_web_admin.erl:1675 -msgid "Registered Users:" -msgstr "Usuarios rexistrados:" - -#: web/ejabberd_web_admin.erl:1661 web/ejabberd_web_admin.erl:1677 -#: web/ejabberd_web_admin.erl:2192 -msgid "Online Users:" -msgstr "Usuarios conectados:" - -#: web/ejabberd_web_admin.erl:1663 -msgid "Outgoing s2s Connections:" -msgstr "Conexións S2S saíntes:" - -#: web/ejabberd_web_admin.erl:1665 -msgid "Outgoing s2s Servers:" -msgstr "Servidores S2S saíntes:" - -#: web/ejabberd_web_admin.erl:1734 web/mod_register_web.erl:175 -#: web/mod_register_web.erl:372 web/mod_register_web.erl:381 -#: web/mod_register_web.erl:411 -msgid "Change Password" -msgstr "Cambiar contrasinal" - -#: web/ejabberd_web_admin.erl:1758 -msgid "User " -msgstr "Usuario " - -#: web/ejabberd_web_admin.erl:1765 -msgid "Connected Resources:" -msgstr "Recursos conectados:" - -#: web/ejabberd_web_admin.erl:1766 web/mod_register_web.erl:227 -#: web/mod_register_web.erl:519 -msgid "Password:" -msgstr "Contrasinal:" - -#: web/ejabberd_web_admin.erl:1828 -msgid "No Data" -msgstr "Sen datos" - -#: web/ejabberd_web_admin.erl:1906 -msgid "Nodes" -msgstr "Nodos" - -#: web/ejabberd_web_admin.erl:1929 web/ejabberd_web_admin.erl:1951 -msgid "Node " -msgstr "Nodo " - -#: web/ejabberd_web_admin.erl:1938 -msgid "Listened Ports" -msgstr "Portos de escoita" - -#: web/ejabberd_web_admin.erl:1940 web/ejabberd_web_admin.erl:2260 -#: web/ejabberd_web_admin.erl:2448 -msgid "Update" -msgstr "Actualizar" - -#: web/ejabberd_web_admin.erl:1943 web/ejabberd_web_admin.erl:2569 -msgid "Restart" -msgstr "Reiniciar" - -#: web/ejabberd_web_admin.erl:1945 web/ejabberd_web_admin.erl:2571 -msgid "Stop" -msgstr "Deter" - -#: web/ejabberd_web_admin.erl:1959 -msgid "RPC Call Error" -msgstr "Erro na chamada RPC" - -#: web/ejabberd_web_admin.erl:2000 -msgid "Database Tables at " -msgstr "Táboas da base de datos en " - -#: web/ejabberd_web_admin.erl:2007 -msgid "Storage Type" -msgstr "Tipo de almacenamiento" - -#: web/ejabberd_web_admin.erl:2008 -msgid "Elements" -msgstr "Elementos" - -#: web/ejabberd_web_admin.erl:2009 -msgid "Memory" -msgstr "Memoria" - -#: web/ejabberd_web_admin.erl:2032 web/ejabberd_web_admin.erl:2137 -msgid "Error" -msgstr "Erro" - -#: web/ejabberd_web_admin.erl:2034 -msgid "Backup of " -msgstr "Copia de seguridade de " - -#: web/ejabberd_web_admin.erl:2036 -msgid "" -"Please note that these options will only backup the builtin Mnesia database. " -"If you are using the ODBC module, you also need to backup your SQL database " -"separately." -msgstr "" -"Ten en conta que estas opcións só farán copia de seguridade da base de datos " -"Mnesia. Se está a utilizar o módulo de ODBC, tamén necesita unha copia de " -"seguridade da súa base de datos SQL por separado." - -#: web/ejabberd_web_admin.erl:2041 -msgid "Store binary backup:" -msgstr "Gardar copia de seguridade binaria:" - -#: web/ejabberd_web_admin.erl:2045 web/ejabberd_web_admin.erl:2052 -#: web/ejabberd_web_admin.erl:2060 web/ejabberd_web_admin.erl:2067 -#: web/ejabberd_web_admin.erl:2074 web/ejabberd_web_admin.erl:2081 -#: web/ejabberd_web_admin.erl:2088 web/ejabberd_web_admin.erl:2096 -#: web/ejabberd_web_admin.erl:2103 web/ejabberd_web_admin.erl:2110 -msgid "OK" -msgstr "Aceptar" - -#: web/ejabberd_web_admin.erl:2048 -msgid "Restore binary backup immediately:" -msgstr "Restaurar inmediatamente copia de seguridade binaria:" - -#: web/ejabberd_web_admin.erl:2056 -msgid "" -"Restore binary backup after next ejabberd restart (requires less memory):" -msgstr "" -"Restaurar copia de seguridade binaria no seguinte reinicio de ejabberd " -"(require menos memoria que se instantánea):" - -#: web/ejabberd_web_admin.erl:2063 -msgid "Store plain text backup:" -msgstr "Gardar copia de seguridade en texto plano:" - -#: web/ejabberd_web_admin.erl:2070 -msgid "Restore plain text backup immediately:" -msgstr "Restaurar copias de seguridade de texto plano inmediatamente:" - -#: web/ejabberd_web_admin.erl:2077 -msgid "Import users data from a PIEFXIS file (XEP-0227):" -msgstr "Importar usuarios desde un fichero PIEFXIS" - -#: web/ejabberd_web_admin.erl:2084 -msgid "Export data of all users in the server to PIEFXIS files (XEP-0227):" -msgstr "" -"Exportar datos de todos os usuarios do servidor a ficheros PIEFXIS " -"(XEP-0227):" - -#: web/ejabberd_web_admin.erl:2091 -msgid "Export data of users in a host to PIEFXIS files (XEP-0227):" -msgstr "" -"Exportar datos de todos os usuarios do servidor a ficheros PIEFXIS " -"(XEP-0227):" - -#: web/ejabberd_web_admin.erl:2099 -msgid "Import user data from jabberd14 spool file:" -msgstr "Importar usuario de fichero spool de jabberd14:" - -#: web/ejabberd_web_admin.erl:2106 -msgid "Import users data from jabberd14 spool directory:" -msgstr "Importar usuarios do directorio spool de jabberd14:" - -#: web/ejabberd_web_admin.erl:2132 -msgid "Listened Ports at " -msgstr "Portos de escoita en " - -#: web/ejabberd_web_admin.erl:2157 -msgid "Modules at " -msgstr "Módulos en " - -#: web/ejabberd_web_admin.erl:2183 -msgid "Statistics of ~p" -msgstr "Estatísticas de ~p" - -#: web/ejabberd_web_admin.erl:2186 -msgid "Uptime:" -msgstr "Tempo desde o inicio:" - -#: web/ejabberd_web_admin.erl:2189 -msgid "CPU Time:" -msgstr "Tempo consumido de CPU:" - -#: web/ejabberd_web_admin.erl:2195 -msgid "Transactions Committed:" -msgstr "Transaccións finalizadas:" - -#: web/ejabberd_web_admin.erl:2198 -msgid "Transactions Aborted:" -msgstr "Transaccións abortadas:" - -#: web/ejabberd_web_admin.erl:2201 -msgid "Transactions Restarted:" -msgstr "Transaccións reiniciadas:" - -#: web/ejabberd_web_admin.erl:2204 -msgid "Transactions Logged:" -msgstr "Transaccións rexistradas:" - -#: web/ejabberd_web_admin.erl:2246 -msgid "Update " -msgstr "Actualizar" - -#: web/ejabberd_web_admin.erl:2254 -msgid "Update plan" -msgstr "Plan de actualización" - -#: web/ejabberd_web_admin.erl:2255 -msgid "Modified modules" -msgstr "Módulos Modificados" - -#: web/ejabberd_web_admin.erl:2256 -msgid "Update script" -msgstr "Script de actualización" - -#: web/ejabberd_web_admin.erl:2257 -msgid "Low level update script" -msgstr "Script de actualización a baixo nivel" - -#: web/ejabberd_web_admin.erl:2258 -msgid "Script check" -msgstr "Comprobación de script" - -#: web/ejabberd_web_admin.erl:2426 -msgid "Port" -msgstr "Porto" - -#: web/ejabberd_web_admin.erl:2427 -msgid "IP" -msgstr "IP" - -#: web/ejabberd_web_admin.erl:2428 -msgid "Protocol" -msgstr "Protocolo" - -#: web/ejabberd_web_admin.erl:2429 web/ejabberd_web_admin.erl:2556 -msgid "Module" -msgstr "Módulo" - -#: web/ejabberd_web_admin.erl:2430 web/ejabberd_web_admin.erl:2557 -msgid "Options" -msgstr "Opcións" - -#: web/ejabberd_web_admin.erl:2450 -msgid "Delete" -msgstr "Eliminar" - -#: web/ejabberd_web_admin.erl:2579 -msgid "Start" -msgstr "Iniciar" - -#: web/mod_register_web.erl:104 -msgid "Your Jabber account was successfully created." -msgstr "" - -#: web/mod_register_web.erl:107 -msgid "There was an error creating the account: " -msgstr "" - -#: web/mod_register_web.erl:115 -msgid "Your Jabber account was successfully deleted." -msgstr "" - -#: web/mod_register_web.erl:118 -msgid "There was an error deleting the account: " -msgstr "" - -#: web/mod_register_web.erl:128 -msgid "The password of your Jabber account was successfully changed." -msgstr "" - -#: web/mod_register_web.erl:131 -msgid "There was an error changing the password: " -msgstr "" - -#: web/mod_register_web.erl:163 web/mod_register_web.erl:172 -msgid "Jabber Account Registration" -msgstr "" - -#: web/mod_register_web.erl:174 web/mod_register_web.erl:192 -#: web/mod_register_web.erl:201 -msgid "Register a Jabber account" -msgstr "" - -#: web/mod_register_web.erl:176 web/mod_register_web.erl:493 -#: web/mod_register_web.erl:502 -msgid "Unregister a Jabber account" -msgstr "" - -#: web/mod_register_web.erl:203 -msgid "" -"This page allows to create a Jabber account in this Jabber server. Your JID " -"(Jabber IDentifier) will be of the form: username@server. Please read " -"carefully the instructions to fill correctly the fields." -msgstr "" - -#: web/mod_register_web.erl:212 web/mod_register_web.erl:386 -#: web/mod_register_web.erl:509 -#, fuzzy -msgid "Username:" -msgstr "Nome de usuario en IRC" - -#: web/mod_register_web.erl:217 -msgid "This is case insensitive: macbeth is the same that MacBeth and Macbeth." -msgstr "" - -#: web/mod_register_web.erl:218 -msgid "Characters not allowed:" -msgstr "" - -#: web/mod_register_web.erl:222 web/mod_register_web.erl:391 -#: web/mod_register_web.erl:514 -#, fuzzy -msgid "Server:" -msgstr "Servidor ~b" - -#: web/mod_register_web.erl:232 -msgid "" -"Don't tell your password to anybody, not even the administrators of the " -"Jabber server." -msgstr "" - -#: web/mod_register_web.erl:234 -msgid "You can later change your password using a Jabber client." -msgstr "" - -#: web/mod_register_web.erl:235 -msgid "" -"Some Jabber clients can store your password in your computer. Use that " -"feature only if you trust your computer is safe." -msgstr "" - -#: web/mod_register_web.erl:237 -msgid "" -"Memorize your password, or write it in a paper placed in a safe place. In " -"Jabber there isn't an automated way to recover your password if you forget " -"it." -msgstr "" - -#: web/mod_register_web.erl:242 web/mod_register_web.erl:406 -#, fuzzy -msgid "Password Verification:" -msgstr "Verificación da contrasinal" - -#: web/mod_register_web.erl:250 -#, fuzzy -msgid "Register" -msgstr "Lista de contactos" - -#: web/mod_register_web.erl:396 -#, fuzzy -msgid "Old Password:" -msgstr "Contrasinal:" - -#: web/mod_register_web.erl:401 -#, fuzzy -msgid "New Password:" -msgstr "Contrasinal:" - -#: web/mod_register_web.erl:504 -msgid "This page allows to unregister a Jabber account in this Jabber server." -msgstr "" - -#: web/mod_register_web.erl:524 -msgid "Unregister" -msgstr "" - -#, fuzzy -#~ msgid "CAPTCHA test failed" -#~ msgstr "O CAPTCHA é válido." - -#~ msgid "Encodings" -#~ msgstr "Codificaciones" - -#~ msgid "(Raw)" -#~ msgstr "(Cru)" - -#~ msgid "Specified nickname is already registered" -#~ msgstr "O alcume especificado xa está rexistrado, terás que buscar outro" - -#~ msgid "Size" -#~ msgstr "Tamaño" diff --git a/priv/msgs/he.msg b/priv/msgs/he.msg index 7e2e73218..1dabbd028 100644 --- a/priv/msgs/he.msg +++ b/priv/msgs/he.msg @@ -1,283 +1,352 @@ -{"Access Configuration","כניסה אל תצורה"}. -{"Access Control List Configuration","כניסה אל תצורת בקרת רשימות"}. -{"Access control lists","כניסה אל בקרת רשימות"}. -{"Access Control Lists","כניסה אל בקרת רשימות"}. -{"Add Jabber ID","הוספת JID"}. -{"Add User","הוספת משתמש"}. +%% Generated automatically +%% DO NOT EDIT: run `make translations` instead +%% To improve translations please read: +%% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ + +{" has set the subject to: "," הגדיר/ה את הנושא אל: "}. +{"A friendly name for the node","שם ידידותי עבור הצומת"}. +{"A password is required to enter this room","נדרשת סיסמה כדי להיכנס אל חדר זה"}. +{"Accept","קבל"}. +{"Access denied by service policy","גישה נדחתה על ידי פוליסת שירות"}. +{"Action on user","פעולה על משתמש"}. +{"Add User","הוסף משתמש"}. +{"Administration of ","ניהול של "}. {"Administration","הנהלה"}. {"Administrator privileges required","נדרשות הרשאות מנהל"}. -{"A friendly name for the node","שם ידידותי עבור הממסר"}. {"All activity","כל פעילות"}. -{"Allow this Jabber ID to subscribe to this pubsub node?","האם להתיר לכתובת JID זו להירשם אל ממסר PubSub זה?"}. +{"All Users","כל המשתמשים"}. +{"Allow this Jabber ID to subscribe to this pubsub node?","להתיר למזהה Jabber זה להירשם לצומת PubSub זה?"}. {"Allow users to change the subject","התר למשתמשים לשנות את הנושא"}. {"Allow users to query other users","התר למשתמשים לתשאל משתמשים אחרים"}. {"Allow users to send invites","התר למשתמשים לשלוח הזמנות"}. {"Allow users to send private messages","התר למשתמשים לשלוח הודעות פרטיות"}. -{"Allow visitors to change nickname","התר למבקרים לשנות כינויים"}. -{"Allow visitors to send status text in presence updates","התר למבקרים לשלוח תמליל מצב בעדכוני נוכחות"}. -{"All Users","כל המשתמשים"}. -{"Announcements","מודעות"}. -{"anyone","לכל אחד"}. -{"A password is required to enter this room","נדרשת מילת־מעבר כדי להיכנס אל חדר זה"}. +{"Allow visitors to change nickname","התר למבקרים לשנות שם כינוי"}. +{"Allow visitors to send private messages to","התר למבקרים לשלוח הודעות פרטיות אל"}. +{"Allow visitors to send status text in presence updates","התר למבקרים לשלוח טקסט מצב בתוך עדכוני נוכחות"}. +{"Allow visitors to send voice requests","התר למבקרים לשלוח בקשות ביטוי"}. +{"Announcements","בשורות"}. {"April","אפריל"}. {"August","אוגוסט"}. +{"Automatic node creation is not enabled","יצירה אוטומטית של צומת אינה מאופשרת"}. {"Backup Management","ניהול גיבוי"}. -{"Backup of ","גיבוי של "}. -{"Backup to File at ","גיבוי אל קובץ אצל "}. +{"Backup of ~p","גיבוי של ~p"}. +{"Backup to File at ","גבה לקובץ אצל "}. {"Backup","גיבוי"}. -{"Bad format","פורמט פגום"}. +{"Bad format","פורמט רע"}. {"Birthday","יום הולדת"}. -{"Change Password","שינוי סיסמה"}. -{"Change User Password","שינוי סיסמת משתמש"}. -{"Choose a username and password to register with this server","נא לבחור שם משתמש וסיסמה להירשם עם שרת זה"}. -{"Choose modules to stop","בחירת מודולים להפסקה"}. -{"Choose storage type of tables","נא לבחור צורת אחסון של טבלאות"}. -{"Choose whether to approve this entity's subscription.","נא לבחור האם לאשר את המנוי של ישות זו."}. +{"Cannot remove active list","לא ניתן להסיר רשימה פעילה"}. +{"Cannot remove default list","לא ניתן להסיר רשימה שגרתית"}. +{"CAPTCHA web page","עמוד רשת CAPTCHA"}. +{"Change Password","שנה סיסמה"}. +{"Change User Password","שנה סיסמת משתמש"}. +{"Changing password is not allowed","שינוי סיסמה אינו מותר"}. +{"Changing role/affiliation is not allowed","שינוי תפקיד/שיוך אינו מותר"}. +{"Characters not allowed:","תווים לא מורשים:"}. +{"Chatroom configuration modified","תצורת חדר שיחה שונתה"}. +{"Chatroom is created","חדר שיחה נוצר כעת"}. +{"Chatroom is destroyed","חדר שיחה הינו הרוס"}. +{"Chatroom is started","חדר שיחה מותחל כעת"}. +{"Chatroom is stopped","חדר שיחה הינו מופסק"}. +{"Chatrooms","חדרי שיחה"}. +{"Choose a username and password to register with this server","בחר שם משתמש וסיסמה כדי להירשם בעזרת שרת זה"}. +{"Choose storage type of tables","בחר טיפוס אחסון של טבלאות"}. +{"Choose whether to approve this entity's subscription.","בחר האם לאשר את ההרשמה של ישות זו."}. {"City","עיר"}. {"Commands","פקודות"}. +{"Conference room does not exist","חדר ועידה לא קיים"}. {"Configuration of room ~s","תצורת חדר ~s"}. {"Configuration","תצורה"}. -{"Connected Resources:","משאבים מחוברים:"}. -{"Country","מדינה"}. -{"CPU Time:","זמן מחשב (CPU):"}. +{"Country","ארץ"}. +{"Database failure","כשל מסד נתונים"}. +{"Database Tables Configuration at ","תצורת טבלאות מסד נתונים אצל "}. {"Database","מסד נתונים"}. {"December","דצמבר"}. -{"Default users as participants","משתמשים משתמטים כמשתתפים"}. -{"Delete message of the day on all hosts","מחיקת הודעת היום אצל כל המארחים"}. -{"Delete message of the day","מחיקת הודעת היום"}. -{"Delete User","מחיקת משתמש"}. -{"Delete","מחיקה"}. -{"Deliver event notifications","מסירת התראות אירוע"}. -{"Deliver payloads with event notifications","מסירת מטען ייעוד (מטע״ד) יחד עם התראות אירוע"}. -{"Description:","תיאור:"}. -{"Dump Backup to Text File at ","השלכת גיבוי אל קובץ תמליל אצל "}. -{"Dump to Text File","השלכה אל קובץ תמליל"}. -{"Edit Properties","עריכת מאפיינים"}. +{"Default users as participants","משתמשים שגרתיים כמשתתפים"}. +{"Delete message of the day on all hosts","מחק את בשורת היום בכל המארחים"}. +{"Delete message of the day","מחק את בשורת היום"}. +{"Delete User","מחק משתמש"}. +{"Deliver event notifications","מסור התראות אירוע"}. +{"Deliver payloads with event notifications","מסור מטעני ייעוד (מטע״ד) יחד עם התראות אירוע"}. +{"Disc only copy","העתק של תקליטור בלבד"}. +{"Dump Backup to Text File at ","השלך גיבוי לקובץ טקסט אצל "}. +{"Dump to Text File","השלך לקובץ טקסט"}. +{"Edit Properties","ערוך מאפיינים"}. +{"Either approve or decline the voice request.","אשר או דחה בקשת ביטוי."}. {"ejabberd MUC module","מודול MUC של ejabberd"}. +{"ejabberd Multicast service","שירות שידור מרובב של ejabberd"}. {"ejabberd Publish-Subscribe module","מודול Publish-Subscribe של ejabberd"}. {"ejabberd SOCKS5 Bytestreams module","מודול SOCKS5 Bytestreams של ejabberd"}. {"ejabberd vCard module","מודול vCard של ejabberd"}. {"ejabberd Web Admin","מנהל רשת ejabberd"}. -{"Elements","אלמנטים"}. {"Email","דוא״ל"}. -{"Enable logging","אפשור רישום פעילות"}. -{"End User Session","סיום סשן משתמש"}. -{"Enter nickname you want to register","נא להזין כינוי שברצונך לרושמו"}. -{"Enter path to backup file","נא להזין נתיב אל קובץ גיבוי"}. -{"Enter path to jabberd14 spool dir","נא להזין נתיב אל מדור סליל (spool dir) של jabberd14"}. -{"Enter path to jabberd14 spool file","נא להזין נתיב אל קובץ סליל (spool file) של jabberd14"}. -{"Enter path to text file","נא להזין נתיב אל קובץ תמליל"}. -{"Enter the text you see","נא להזין את התמליל אותו הינך רואה"}. -{"Error","שגיאה"}. -{"Export data of all users in the server to PIEFXIS files (XEP-0227):","ייצוא מידע של כל המשתמשים שבשרת אל קבצי PIEFXIS (‫XEP-0227):"}. -{"Export data of users in a host to PIEFXIS files (XEP-0227):","ייצוא מידע של כל המשתמשים אצל מארח אל קבצי PIEFXIS (‫XEP-0227):"}. +{"Enable logging","אפשר רישום פעילות"}. +{"Enable message archiving","אפשר אחסון הודעות"}. +{"End User Session","סיים סשן משתמש"}. +{"Enter nickname you want to register","הזן שם כינוי אשר ברצונך לרשום"}. +{"Enter path to backup file","הזן נתיב לקובץ גיבוי"}. +{"Enter path to jabberd14 spool dir","הזן נתיב למדור סליל (spool dir) של jabberd14"}. +{"Enter path to jabberd14 spool file","הזן נתיב לקובץ סליל (spool file) של jabberd14"}. +{"Enter path to text file","הזן נתיב לקובץ טקסט"}. +{"Enter the text you see","הזן את הכיתוב שאתה רואה"}. +{"Exclude Jabber IDs from CAPTCHA challenge","הוצא כתובות Jabber מתוך אתגר CAPTCHA"}. +{"Export all tables as SQL queries to a file:","יצא את כל הטבלאות בתור שאילתות SQL לתוך קובץ:"}. +{"Export data of all users in the server to PIEFXIS files (XEP-0227):","יצא מידע של כל המשתמשים שבתוך שרת זה לתוך קבצי PIEFXIS ‏(XEP-0227):"}. +{"Export data of users in a host to PIEFXIS files (XEP-0227):","יצא מידע של כל המשתמשים שבתוך מארח לתוך קבצי PIEFXIS ‏(XEP-0227):"}. +{"Failed to activate bytestream","נכשל להפעיל bytestream"}. +{"Failed to extract JID from your voice request approval","נכשל לחלץ JID מתוך אישור בקשת הביטוי שלך"}. +{"Failed to parse HTTP response","נכשל לפענח תגובת HTTP"}. +{"Failed to process option '~s'","נכשל לעבד אפשרות '~s'"}. {"Family Name","שם משפחה"}. {"February","פברואר"}. -{"Fill in fields to search for any matching Jabber User","נא למלא שדות אלו כדי לחפש עבור כל משתמש Jabber מבוקש"}. -{"Fill in the form to search for any matching Jabber User (Add * to the end of field to match substring)","נא למלא התבניות כדי לחפש עבור כל משתמש Jabber מבוקש (באפשרותך להוסיף * בסוף שדה כדי להתאים אל מחרוזת-משנה)"}. +{"File larger than ~w bytes","קובץ גדול יותר משיעור של ~w בייטים"}. {"Friday","יום שישי"}. -{"From ~s","מאת ~s"}. -{"From","מן"}. {"Full Name","שם מלא"}. -{"Get Number of Online Users","השגת מספר של משתמשים מקוונים"}. -{"Get Number of Registered Users","השגת מספר של משתמשים רשומים"}. -{"Get User Last Login Time","השגת זמן התחברות אחרון של משתמש"}. -{"Get User Password","השגת סיסמת משתמש"}. -{"Get User Statistics","השגת סטטיסטיקת משתמש"}. -{"Groups","קבוצות"}. -{"Group ","קבוצה "}. +{"Get Number of Online Users","השג מספר של משתמשים מקוונים"}. +{"Get Number of Registered Users","השג מספר של משתמשים רשומים"}. +{"Get User Last Login Time","השג זמן כניסה אחרון של משתמש"}. +{"Get User Statistics","השג סטטיסטיקת משתמש"}. +{"Given Name","שם פרטי"}. +{"Grant voice to this person?","להעניק ביטוי לאישיות זו?"}. {"has been banned","נאסר/ה"}. -{"has been kicked because of an affiliation change","נבעט/ה משום שינוי שיוך"}. {"has been kicked because of a system shutdown","נבעט/ה משום כיבוי מערכת"}. +{"has been kicked because of an affiliation change","נבעט/ה משום שינוי סינוף"}. {"has been kicked because the room has been changed to members-only","נבעט/ה משום שהחדר שונה אל חברים-בלבד"}. {"has been kicked","נבעט/ה"}. -{" has set the subject to: "," הגדיר/ה את הנושא אל: "}. -{"Host","מארח"}. +{"Host unknown","מארח לא ידוע"}. +{"If you don't see the CAPTCHA image here, visit the web page.","אם אינך רואה תמונת CAPTCHA כאן, בקר בעמוד רשת."}. {"Import Directory","ייבוא מדור"}. {"Import File","ייבוא קובץ"}. -{"Import user data from jabberd14 spool file:","ייבוא נתוני משתמש מן קובץ סליל (spool file) של jabberd14:"}. -{"Import User from File at ","ייבוא משתמש מן קובץ אצל "}. -{"Import users data from a PIEFXIS file (XEP-0227):","ייבוא מידע משתמשים מן קובץ PIEFXIS (‫XEP-0227):"}. -{"Import users data from jabberd14 spool directory:","ייבוא נתוני משתמשים מן מדור סליל (spool directory) של jabberd14:"}. -{"Import Users from Dir at ","ייבוא משתמשים מן מדור אצל "}. -{"Import Users From jabberd14 Spool Files","ייבוא משתמשים מן קבצי סליל (Spool Files) של jabberd14"}. -{"Improper message type","צורת הודעה לא מתאימה"}. -{"Incorrect password","מילת־מעבר שגויה"}. -{"Invalid affiliation: ~s","שיוך שגוי: ~s"}. -{"Invalid role: ~s","תפקיד שגוי: ~s"}. +{"Import user data from jabberd14 spool file:","יבא נתוני משתמש מתוך קובץ סליל (spool file) של jabberd14:"}. +{"Import User from File at ","ייבוא משתמש מתוך קובץ אצל "}. +{"Import users data from a PIEFXIS file (XEP-0227):","יבא מידע משתמשים מתוך קובץ PIEFXIS ‏(XEP-0227):"}. +{"Import users data from jabberd14 spool directory:","יבא נתוני משתמשים מתוך מדור סליל (spool directory) של jabberd14:"}. +{"Import Users from Dir at ","ייבוא משתמשים מתוך מדור אצל "}. +{"Import Users From jabberd14 Spool Files","יבא משתמשים מתוך קבצי סליל (Spool Files) של jabberd14"}. +{"Improper message type","טיפוס הודעה לא מתאים"}. +{"Incorrect CAPTCHA submit","נשלחה CAPTCHA שגויה"}. +{"Incorrect data form","טופס מידע לא תקין"}. +{"Incorrect password","מילת מעבר שגויה"}. +{"Insufficient privilege","הרשאה לא מספיקה"}. +{"Invitations are not allowed in this conference","הזמנות אינן מותרות בועידה זו"}. {"IP addresses","כתובות IP"}. -{"IP","‫IP"}. {"is now known as","ידועה כעת בכינוי"}. -{"It is not allowed to send private messages of type \"groupchat\"","אין זה מותר לשלוח הודעות פרטיות מן סוג של \"groupchat\""}. -{"It is not allowed to send private messages","אין זה מותר לשלוח הודעות פרטיות"}. -{"Jabber ID","‫JID"}. -{"Jabber ID ~s is invalid","כתובת JID ‫~s הינה שגויה"}. +{"It is not allowed to send error messages to the room. The participant (~s) has sent an error message (~s) and got kicked from the room","אין זה מותר לשלוח הודעות שגיאה לחדר. משתתף זה (~s) שלח הודעת שגיאה (~s) ונבעט מתוך החדר"}. +{"It is not allowed to send private messages of type \"groupchat\"","אין זה מותר לשלוח הודעות פרטיות מן טיפוס \"groupchat\""}. +{"It is not allowed to send private messages to the conference","אין זה מותר לשלוח הודעות פרטיות לועידה"}. +{"Jabber ID","מזהה Jabber"}. {"January","ינואר"}. {"joins the room","נכנס/ת אל החדר"}. {"July","יולי"}. {"June","יוני"}. {"Last Activity","פעילות אחרונה"}. -{"Last login","התחברות אחרונה"}. +{"Last login","כניסה אחרונה"}. {"Last month","חודש אחרון"}. {"Last year","שנה אחרונה"}. -{"leaves the room","עוזב/ת אל החדר"}. -{"List of modules to start","רשימה של מודולים להפעלה"}. -{"Make participants list public","הפיכת רשימת משתתפים אל פומבית"}. -{"Make room members-only","הפיכת חדר אל חברים-בלבד"}. -{"Make room moderated","הפיכת חדר אל מבוקר"}. -{"Make room password protected","הפיכת חדר אל מוגן במילת־מעבר"}. -{"Make room persistent","הפיכת חדר אל קבוע"}. -{"Make room public searchable","הפיכת חדר אל בר־חיפוש פומבי"}. +{"leaves the room","עוזב/ת את החדר"}. +{"Make participants list public","הפוך רשימת משתתפים לפומבית"}. +{"Make room CAPTCHA protected","הפוך חדר לחדר מוגן CAPTCHA"}. +{"Make room members-only","הפוך חדר לחדר עבור חברים-בלבד"}. +{"Make room moderated","הפוך חדר לחדר מבוקר"}. +{"Make room password protected","הפוך חדר לחדר מוגן במילת מעבר"}. +{"Make room persistent","הפוך חדר לחדר קבוע"}. +{"Make room public searchable","הפוך חדר לחדר שנתון לחיפוש פומבי"}. +{"Malformed username","שם משתמש פגום"}. {"March","מרץ"}. +{"Max payload size in bytes","גודל מרבי של מטען ייעוד (payload) ביחידות מידה של byte"}. {"Maximum Number of Occupants","מספר מרבי של נוכחים"}. -{"Max # of items to persist","מספר מרבי של פריטים לקיבוע"}. -{"Max payload size in bytes","גודל מרבי של מטען הייעוד בבייטים (bytes)"}. {"May","מאי"}. {"Membership is required to enter this room","נדרשת חברות כדי להיכנס אל חדר זה"}. -{"Members:","חברים:"}. -{"Memory","זיכרון"}. {"Message body","גוף הודעה"}. {"Middle Name","שם אמצעי"}. +{"Minimum interval between voice requests (in seconds)","תדירות מינימלית בין בקשות ביטוי (בשניות)"}. {"Moderator privileges required","נדרשות הרשאות אחראי"}. -{"moderators only","לאחראים בלבד"}. -{"Modified modules","מודולים שהותאמו"}. -{"Modules at ","מודולים אצל "}. -{"Modules","מודולים"}. -{"Module","מודול"}. +{"Moderator","אחראי"}. +{"Module failed to handle the query","מודול נכשל לטפל בשאילתא"}. {"Monday","יום שני"}. -{"Name:","שם:"}. +{"Multicast","שידור מרובב"}. +{"Multi-User Chat","שיחה מרובת משתמשים"}. {"Name","שם"}. -{"Nickname Registration at ","הרשמת כינוי אצל "}. -{"Nickname ~s does not exist in the room","כינוי ~s לא קיים בחדר"}. -{"Nickname","כינוי"}. +{"Never","אף פעם"}. +{"New Password:","סיסמה חדשה:"}. +{"Nickname Registration at ","רישום שם כינוי אצל "}. +{"Nickname","שם כינוי"}. +{"No available resource found","לא נמצא משאב זמין"}. +{"No body provided for announce message","לא סופק גוף עבור הודעת בשורה"}. {"No Data","אין מידע"}. -{"Node ID","ממסר (NID)"}. -{"Node not found","ממסר לא נמצא"}. -{"Nodes","ממסרים"}. -{"Node ","ממסר"}. +{"No features available","אין תכונות זמינות"}. +{"No items found in this query","לא נמצאו פריטים בתוך שאילתא זו"}. {"No limit","ללא הגבלה"}. +{"No module is handling this query","אין מודול אשר מטפל בשאילתא זו"}. +{"No node specified","לא צויין צומת"}. +{"No pending subscriptions found","לא נמצאו הרשמות ממתינות"}. +{"No privacy list with this name found","לא נמצאה רשימת פרטיות בשם זה"}. +{"No private data found in this query","לא נמצא מידע פרטי בתוך שאילתא זו"}. +{"No running node found","לא נמצא צומת מורץ"}. +{"No services available","אין שירות זמין"}. +{"No statistics found for this item","לא נמצאה סטטיסטיקה לגבי פריט זה"}. +{"Node already exists","צומת כבר קיים"}. +{"Node ID","מזהה צומת (NID)"}. +{"Node index not found","מפתח צומת לא נמצא"}. +{"Node not found","צומת לא נמצא"}. +{"Node ~p","צומת ~p"}. +{"Nodeprep has failed","‏Nodeprep נכשל"}. +{"Nodes","צמתים"}. +{"None","אין"}. {"Not Found","לא נמצא"}. -{"Notify subscribers when items are removed from the node","הודע מנויים כאשר פריטים מוסרים מן הממסר"}. -{"Notify subscribers when the node configuration changes","הודע מנויים כאשר תצורת הממסר משתנה"}. -{"Notify subscribers when the node is deleted","הודע מנויים כאשר הממסר נמחק"}. +{"Not subscribed","לא רשום"}. +{"Notify subscribers when items are removed from the node","הודע מנויים כאשר פריטים מוסרים מתוך הצומת"}. +{"Notify subscribers when the node configuration changes","הודע מנויים כאשר תצורת הצומת משתנה"}. +{"Notify subscribers when the node is deleted","הודע מנויים כאשר הצומת נמחק"}. {"November","נובמבר"}. {"Number of occupants","מספר של נוכחים"}. {"Number of online users","מספר של משתמשים מקוונים"}. {"Number of registered users","מספר של משתמשים רשומים"}. {"October","אוקטובר"}. -{"Offline Messages:","הודעות לא מקוונות:"}. -{"Offline Messages","הודעות לא מקוונות"}. {"OK","אישור"}. -{"Online Users:","משתמשים מקוונים:"}. +{"Old Password:","סיסמה ישנה:"}. {"Online Users","משתמשים מקוונים"}. -{"Only deliver notifications to available users","מסירת התראות אל משתמשים זמינים בלבד"}. +{"Online","מקוון"}. +{"Only deliver notifications to available users","מסור התראות למשתמשים זמינים בלבד"}. +{"Only or tags are allowed","רק תגיות או הינן מורשות"}. +{"Only members may query archives of this room","רק חברים רשאים לתשאל ארכיונים של חדר זה"}. {"Only moderators and participants are allowed to change the subject in this room","רק אחראים ומשתתפים רשאים לשנות את הנושא בחדר זה"}. {"Only moderators are allowed to change the subject in this room","רק אחראים רשאים לשנות את הנושא בחדר זה"}. -{"Options","אפשרויות"}. +{"Only moderators can approve voice requests","רק אחראים יכולים לאשר בקשות ביטוי"}. +{"Only occupants are allowed to send messages to the conference","רק נוכחים רשאים לשלוח הודעות אל הועידה"}. +{"Only occupants are allowed to send queries to the conference","רק נוכחים רשאים לשלוח שאילתות אל הועידה"}. +{"Only service administrators are allowed to send service messages","רק מנהלי שירות רשאים לשלוח הודעות שירות"}. {"Organization Name","שם ארגון"}. {"Organization Unit","יחידת איגוד"}. -{"Outgoing s2s Connections:","חיבורי s2s יוצאים:"}. {"Outgoing s2s Connections","חיבורי s2s יוצאים"}. -{"Outgoing s2s Servers:","שרתי s2s יוצאים:"}. {"Owner privileges required","נדרשות הרשאות בעלים"}. +{"Participant","משתתף"}. {"Password Verification","אימות סיסמה"}. -{"Password:","סיסמה:"}. +{"Password Verification:","אימות סיסמה:"}. {"Password","סיסמה"}. -{"Path to Dir","נתיב אל מדור"}. -{"Path to File","נתיב אל קובץ"}. +{"Password:","סיסמה:"}. +{"Path to Dir","נתיב למדור"}. +{"Path to File","נתיב לקובץ"}. {"Period: ","משך זמן: "}. {"Persist items to storage","פריטים קבועים לאחסון"}. +{"Ping query is incorrect","שאילתת פינג הינה שגויה"}. {"Ping","פינג"}. -{"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","נא לשים לב שלאפשרויות אלו יגבו את מסד הנתונים המובנה Mnesia בלבד. אם הינך עושה שימוש במודול ODBC, עליך גם לגבות את מסד הנתונים SQL שברשותך לחוד."}. +{"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","אנא שים לב כי אפשרויות אלו יגבו את מסד הנתונים המובנה Mnesia בלבד. אם הינך עושה שימוש במודול ODBC, עליך גם לגבות את מסד הנתונים SQL אשר מצוי ברשותך בנפרד."}. +{"Please, wait for a while before sending new voice request","אנא, המתן לזמן מה לפני שליחת בקשת ביטוי חדשה"}. {"Pong","פונג"}. -{"Port","פורט"}. -{"Present real Jabber IDs to","הצגת כתובות JID ממשיות"}. -{"Protocol","פרוטוקול"}. +{"Present real Jabber IDs to","הצג כתובות Jabber ממשיות"}. +{"private, ","פרטי, "}. {"Publish-Subscribe","‫Publish-Subscribe"}. {"PubSub subscriber request","בקשת מנוי PubSub"}. -{"Purge all items when the relevant publisher goes offline","טיהור כל הפריטים כאשר המפרסם הרלוונטי "}. -{"Really delete message of the day?","האם באמת למחוק את הודעת היום?"}. -{"Registered Users:","משתמשים רשומים:"}. -{"Registered Users","משתמשים רשומים"}. -{"Remote copy","עותק מרוחק"}. -{"Remove All Offline Messages","הסרת כל ההודעות הלא מקוונות"}. -{"Remove User","הסרת משתמש"}. +{"Purge all items when the relevant publisher goes offline","טהר את כל הפריטים כאשר המפרסם הרלוונטי הופך לבלתי מקוון"}. +{"Queries to the conference members are not allowed in this room","שאילתות אל חברי הועידה אינן מותרות בחדר זה"}. +{"RAM and disc copy","העתק RAM וגם תקליטור"}. +{"RAM copy","העתק RAM"}. +{"Really delete message of the day?","באמת למחוק את בשורת היום?"}. +{"Recipient is not in the conference room","מקבל אינו מצוי בחדר הועידה"}. +{"Register","הרשם"}. +{"Remote copy","העתק מרוחק"}. +{"Remove User","הסר משתמש"}. +{"Replaced by new connection","הוחלף בחיבור חדש"}. {"Resources","משאבים"}. -{"Restart Service","אתחול שירות"}. -{"Restart","אתחול"}. -{"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:","שחזור גיבוי תמליל גלוי (plain text) לאלתר:"}. -{"Restore","שחזור"}. +{"Restart Service","אתחל שירות"}. +{"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:","שחזר גיבוי טקסט גלוי (plain text) לאלתר:"}. +{"Restore","שחזר"}. +{"Roles for which Presence is Broadcasted","תפקידים להם נוכחות הינה משודרת"}. {"Room Configuration","תצורת חדר"}. +{"Room creation is denied by service policy","יצירת חדר נדחתה על ידי פוליסת שירות"}. {"Room description","תיאור חדר"}. {"Room Occupants","נוכחי חדר"}. {"Room title","כותרת חדר"}. -{"Roster size","גודל רשימה (Roster)"}. -{"Roster","רשימה"}. -{"RPC Call Error","שגיאת קריאת RPC"}. -{"Running Nodes","ממסרים שמורצים כעת"}. +{"Roster groups allowed to subscribe","קבוצות רשימה מורשות להירשם"}. +{"Roster size","גודל רשימה"}. +{"Running Nodes","צמתים מורצים"}. {"Saturday","יום שבת"}. {"Search Results for ","תוצאות חיפוש עבור "}. {"Search users in ","חיפוש משתמשים אצל "}. -{"Send announcement to all online users on all hosts","שליחת מודעות אל כל המשתמשים המקוונים אצל כל המארחים"}. -{"Send announcement to all online users","שליחת מודעות אל כל המשתמשים המקוונים"}. -{"Send announcement to all users on all hosts","שליחת מודעות אל כל המשתמשים אצל כל המארחים"}. -{"Send announcement to all users","שליחת מודעות אל כל המשתמשים"}. +{"Send announcement to all online users on all hosts","שלח בשורה לכל המשתמשים המקוונים בכל המארחים"}. +{"Send announcement to all online users","שלח בשורה לכל המשתמשים המקוונים"}. +{"Send announcement to all users on all hosts","שלח בשורה לכל המשתמשים בכל המארחים"}. +{"Send announcement to all users","שלח בשורה לכל המשתמשים"}. {"September","ספטמבר"}. -{"Set message of the day and send to online users","שליחת הודעת היום ושליחה אל משתמשים מקוונים"}. -{"Set message of the day on all hosts and send to online users","שליחת הודעת היום אצל כל המארחים ושליחה אל משתמשים מקוונים"}. -{"Show Integral Table","הצגת טבלה אינטגרלית"}. -{"Show Ordinary Table","הצגת טבלה רגילה"}. -{"Shut Down Service","שירות כיבוי"}. -{"~s invites you to the room ~s","‫~s מזמינך אל החדר ~s"}. -{"Specify the access model","נא לציין את מודל הגישה"}. -{"Specify the event message type","נא לציין את סוג הודעת האירוע"}. -{"Specify the publisher model","נא לציין את מודל הפרסום"}. -{"Start Modules","הפעלת מודולים"}. -{"Statistics of ~p","סטטיסטיקות עבור ~p"}. -{"Statistics","סטטיסטיקה"}. -{"Stop Modules","עצירת מודולים"}. -{"Stopped Nodes","ממסרים שנעצרו"}. -{"Storage Type","צורת אחסון"}. -{"Store binary backup:","אחסון גיבוי בינארי:"}. -{"Store plain text backup:","אחסון גיבוי תמליל גלוי (plain text):"}. +{"Server:","שרת:"}. +{"Set message of the day and send to online users","קבע את בשורת היום ושלח למשתמשים מקוונים"}. +{"Set message of the day on all hosts and send to online users","קבע את בשורת היום בכל המארחים ושלח למשתמשים מקוונים"}. +{"Shared Roster Groups","קבוצות רשימה משותפות"}. +{"Show Integral Table","הצג טבלה אינטגרלית"}. +{"Show Ordinary Table","הצג טבלה רגילה"}. +{"Shut Down Service","כבה שירות"}. +{"Specify the access model","ציין מודל גישה"}. +{"Specify the event message type","ציין טיפוס הודעת אירוע"}. +{"Specify the publisher model","ציין מודל פרסום"}. +{"Stopped Nodes","צמתים שנפסקו"}. +{"Store binary backup:","אחסן גיבוי בינארי:"}. +{"Store plain text backup:","אחסן גיבוי טקסט גלוי (plain text):"}. {"Subject","נושא"}. -{"Submit","שליחה"}. +{"Submitted","נשלח"}. {"Subscriber Address","כתובת מנוי"}. -{"Subscription","מִנּוּי"}. +{"Subscriptions are not allowed","הרשמות אינן מורשות"}. {"Sunday","יום ראשון"}. -{"That nickname is already in use by another occupant","כינוי זה כבר מצוי בשימוש על ידי נוכח אחר"}. -{"That nickname is registered by another person","כינוי זה הינו רשום על ידי מישהו אחר"}. +{"That nickname is already in use by another occupant","שם כינוי זה כבר מצוי בשימוש על ידי נוכח אחר"}. +{"That nickname is registered by another person","שם כינוי זה הינו רשום על ידי מישהו אחר"}. +{"The CAPTCHA is valid.","‏CAPTCHA הינה תקפה."}. +{"The CAPTCHA verification has failed","אימות CAPTCHA נכשל"}. +{"The collections with which a node is affiliated","האוספים עמם צומת מסונף"}. +{"The password is too weak","הסיסמה חלשה מדי"}. {"the password is","הסיסמה היא"}. -{"This participant is kicked from the room because he sent an error message to another participant","משתתף זה נבעט מן החדר משום שהוא שלח הודעת שגיאה אל משתתף אחר"}. -{"This participant is kicked from the room because he sent an error message","משתתף זה נבעט מן החדר משום שהוא שלח הודעת שגיאה"}. -{"This participant is kicked from the room because he sent an error presence","משתתף זה נבעט מן החדר משום שהוא שלח נוכחות שגויה"}. +{"There was an error creating the account: ","אירעה שגיאה ביצירת החשבון: "}. +{"There was an error deleting the account: ","אירעה שגיאה במחיקת החשבון: "}. {"This room is not anonymous","חדר זה אינו אנונימי"}. {"Thursday","יום חמישי"}. -{"Time","זמן"}. -{"To ~s","אל ~s"}. -{"To","אל"}. +{"Time delay","זמן שיהוי"}. +{"To register, visit ~s","כדי להירשם, בקרו ~s"}. +{"Token TTL","סימן TTL"}. +{"Too many active bytestreams","יותר מדי יחידות bytestream פעילות"}. +{"Too many CAPTCHA requests","יותר מדי בקשות CAPTCHA"}. +{"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","יותר מדי (~p) אימותים כושלים מתוך כתובת IP זו (~s). הכתובת תורשה לקבל גישה בשעה ~s UTC"}. +{"Too many unacked stanzas","יותר מדי סטנזות בלי אישורי קבלה"}. +{"Too many users in this conference","יותר מדי משתמשים בועידה זו"}. {"Traffic rate limit is exceeded","מגבלת שיעור תעבורה נחצתה"}. {"Tuesday","יום שלישי"}. +{"Unable to generate a CAPTCHA","אין אפשרות להפיק CAPTCHA"}. {"Unauthorized","לא מורשה"}. -{"Update message of the day (don't send)","עדכון הודעת היום (אל תשלח)"}. -{"Update message of the day on all hosts (don't send)","עדכון הודעת היום אצל כל המארחים (אל תשלח)"}. -{"Uptime:","זמן פעילות:"}. +{"Unexpected action","פעולה לא צפויה"}. +{"Unregister","בטל רישום"}. +{"Update message of the day (don't send)","עדכן את בשורת היום (אל תשלח)"}. +{"Update message of the day on all hosts (don't send)","עדכן את בשורת היום בכל המארחים (אל תשלח)"}. +{"User already exists","משתמש כבר קיים"}. +{"User JID","‏JID משתמש"}. +{"User (jid)","משתמש (jid)"}. {"User Management","ניהול משתמשים"}. +{"User session not found","סשן משתמש לא נמצא"}. +{"User session terminated","סשן משתמש הסתיים"}. +{"Username:","שם משתמש:"}. +{"Users are not allowed to register accounts so quickly","משתמשים אינם מורשים לרשום חשבונות כל כך במהירות"}. {"Users Last Activity","פעילות משתמשים אחרונה"}. {"Users","משתמשים"}. -{"User ","משתמש"}. {"User","משתמש"}. +{"Value of '~s' should be boolean","ערך של '~s' צריך להיות boolean"}. +{"Value of '~s' should be datetime string","ערך של '~s' צריך להיות מחרוזת datetime"}. +{"Value of '~s' should be integer","ערך של '~s' צריך להיות integer"}. +{"vCard User Search","חיפוש משתמש vCard"}. {"Virtual Hosts","מארחים מדומים"}. -{"Visitors are not allowed to change their nicknames in this room","מבקרים אינם מורשים לשנות את כינויַם בחדר זה"}. +{"Visitors are not allowed to change their nicknames in this room","מבקרים אינם מורשים לשנות את שמות הכינויים שלהם בחדר זה"}. {"Visitors are not allowed to send messages to all occupants","מבקרים אינם מורשים לשלוח הודעות אל כל הנוכחים"}. +{"Visitor","מבקר"}. +{"Voice requests are disabled in this conference","בקשות ביטוי מנוטרלות בועידה זו"}. +{"Voice request","בקשת ביטוי"}. {"Wednesday","יום רביעי"}. -{"When to send the last published item","מתי לשלוח פריט מפורסם אחרון"}. -{"Whether to allow subscriptions","האם להתיר מנויים"}. +{"When to send the last published item","מתי לשלוח את הפריט המפורסם האחרון"}. +{"Whether to allow subscriptions","האם להתיר הרשמות"}. {"You have been banned from this room","נאסרת מן חדר זה"}. -{"You must fill in field \"Nickname\" in the form","עליך למלא את השדה \"כינוי\" בתוך התבנית"}. -{"You need an x:data capable client to configure room","עליך לעשות שימוש בלקוח שביכולתו להבין x:data בכדי להגדיר חדר"}. -{"You need an x:data capable client to search","עליך לעשות שימוש בלקוח שביכולתו להבין x:data בכדי לחפש"}. -{"Your messages to ~s are being blocked. To unblock them, visit ~s","הודעותייך אל ~s הינן חסומות. כדי למנוע את חסימתן, נא לבקר ~s"}. +{"You have joined too many conferences","הצטרפת ליותר מדי ועידות"}. +{"You must fill in field \"Nickname\" in the form","עליך למלא את השדה \"שם כינוי\" בתוך התבנית"}. +{"You need a client that supports x:data and CAPTCHA to register","עליך להשתמש בלקוח אשר תומך x:data וגם CAPTCHA כדי להירשם"}. +{"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 subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","ההודעות שלך לערוץ ~s הינן חסומות. כדי לבטל את חסימתן, בקר בכתובת ~s"}. +{"You're not allowed to create nodes","אינך מורשה ליצור צמתים"}. diff --git a/priv/msgs/he.po b/priv/msgs/he.po deleted file mode 100644 index f013b93b8..000000000 --- a/priv/msgs/he.po +++ /dev/null @@ -1,1953 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: ejabberd 2.1.x\n" -"POT-Creation-Date: \n" -"PO-Revision-Date: \n" -"Last-Translator: Isratine Citizen \n" -"Language-Team: Rahut \n" -"Language: he\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Language: Hebrew\n" -"X-Poedit-Language: Hebrew (עברית)\n" -"X-Generator: Poedit 1.5.4\n" - -#: ejabberd_c2s.erl:424 ejabberd_c2s.erl:727 -msgid "Use of STARTTLS required" -msgstr "נדרש שימוש של STARTTLS" - -#: ejabberd_c2s.erl:503 -msgid "No resource provided" -msgstr "לא סופק משאב" - -#: ejabberd_c2s.erl:1197 -msgid "Replaced by new connection" -msgstr "הוחלף בחיבור חדש" - -#: ejabberd_c2s.erl:1885 -msgid "Your active privacy list has denied the routing of this stanza." -msgstr "רשימת הפרטיות הפעילה שלך אסרה את הניתוב של סטנזה זו." - -# תמליל -# כפי שעינייך רואות -#: ejabberd_captcha.erl:96 ejabberd_captcha.erl:152 ejabberd_captcha.erl:178 -msgid "Enter the text you see" -msgstr "הזן את הטקסט אותו הינך רואה" - -# כדי שלא לחסומן -#: ejabberd_captcha.erl:101 -msgid "Your messages to ~s are being blocked. To unblock them, visit ~s" -msgstr "ההודעות שלך לערוץ ~s הינן חסומות. כדי למנוע את חסימתן, בקר בכתובת ~s" - -# בקר את עמוד -#: ejabberd_captcha.erl:134 -msgid "If you don't see the CAPTCHA image here, visit the web page." -msgstr "במידה ואינך רואה תמונת CAPTCHA כאן, בקר בעמוד הרשת." - -#: ejabberd_captcha.erl:146 -msgid "CAPTCHA web page" -msgstr "עמוד רשת CAPTCHA" - -#: ejabberd_captcha.erl:307 -msgid "The CAPTCHA is valid." -msgstr "‏CAPTCHA הינה בתוקף." - -#: mod_adhoc.erl:95 mod_adhoc.erl:125 mod_adhoc.erl:143 mod_adhoc.erl:161 -msgid "Commands" -msgstr "פקודות" - -#: mod_adhoc.erl:149 mod_adhoc.erl:243 -msgid "Ping" -msgstr "פינג" - -#: mod_adhoc.erl:260 -msgid "Pong" -msgstr "פונג" - -#: mod_announce.erl:507 mod_announce_odbc.erl:499 -msgid "Really delete message of the day?" -msgstr "באמת למחוק את הודעת היום?" - -#: mod_announce.erl:515 mod_announce_odbc.erl:507 mod_configure.erl:1083 -#: mod_configure.erl:1128 -msgid "Subject" -msgstr "נושא" - -#: mod_announce.erl:520 mod_announce_odbc.erl:512 mod_configure.erl:1088 -#: mod_configure.erl:1133 -msgid "Message body" -msgstr "גוף הודעה" - -#: mod_announce.erl:600 mod_announce_odbc.erl:592 -#, fuzzy -msgid "No body provided for announce message" -msgstr "לא סופק גוף עבור announce message" - -#: mod_announce.erl:635 mod_announce_odbc.erl:627 -msgid "Announcements" -msgstr "מודעות" - -#: mod_announce.erl:637 mod_announce_odbc.erl:629 -msgid "Send announcement to all users" -msgstr "שלח מודעות אל כל המשתמשים" - -# אצל כל -#: mod_announce.erl:639 mod_announce_odbc.erl:631 -msgid "Send announcement to all users on all hosts" -msgstr "שלח מודעות אל כל המשתמשים בכל המארחים" - -#: mod_announce.erl:641 mod_announce_odbc.erl:633 -msgid "Send announcement to all online users" -msgstr "שלח מודעות אל כל המשתמשים המקוונים" - -#: mod_announce.erl:643 mod_announce_odbc.erl:635 mod_configure.erl:1078 -#: mod_configure.erl:1123 -msgid "Send announcement to all online users on all hosts" -msgstr "שלח מודעות אל כל המשתמשים המקוונים בכל המארחים" - -#: mod_announce.erl:645 mod_announce_odbc.erl:637 -msgid "Set message of the day and send to online users" -msgstr "קבע הודעת היום ושלח אל משתמשים מקוונים" - -#: mod_announce.erl:647 mod_announce_odbc.erl:639 -msgid "Set message of the day on all hosts and send to online users" -msgstr "קבע הודעת היום בכל המארחים ושלח אל משתמשים מקוונים" - -#: mod_announce.erl:649 mod_announce_odbc.erl:641 -msgid "Update message of the day (don't send)" -msgstr "עדכן הודעת היום (אל תשלח)" - -#: mod_announce.erl:651 mod_announce_odbc.erl:643 -msgid "Update message of the day on all hosts (don't send)" -msgstr "עדכן הודעת היום בכל המארחים (אל תשלח)" - -#: mod_announce.erl:653 mod_announce_odbc.erl:645 -msgid "Delete message of the day" -msgstr "מחק הודעת היום" - -#: mod_announce.erl:655 mod_announce_odbc.erl:647 -msgid "Delete message of the day on all hosts" -msgstr "מחק הודעת היום בכל המארחים" - -#: mod_configure.erl:114 mod_configure.erl:274 mod_configure.erl:296 -#: mod_configure.erl:498 -msgid "Configuration" -msgstr "תצורה" - -#: mod_configure.erl:125 mod_configure.erl:576 web/ejabberd_web_admin.erl:1936 -msgid "Database" -msgstr "מסד נתונים" - -#: mod_configure.erl:127 mod_configure.erl:595 -msgid "Start Modules" -msgstr "התחל מודולים" - -#: mod_configure.erl:129 mod_configure.erl:596 -msgid "Stop Modules" -msgstr "הפסק מודולים" - -# גיבוי -#: mod_configure.erl:131 mod_configure.erl:604 web/ejabberd_web_admin.erl:1937 -msgid "Backup" -msgstr "גבה" - -# שחזור -#: mod_configure.erl:133 mod_configure.erl:605 -msgid "Restore" -msgstr "שחזר" - -# הטל אל קובץ תמליל -#: mod_configure.erl:135 mod_configure.erl:606 -msgid "Dump to Text File" -msgstr "השלך אל קובץ טקסט" - -#: mod_configure.erl:137 mod_configure.erl:615 -msgid "Import File" -msgstr "ייבוא קובץ" - -#: mod_configure.erl:139 mod_configure.erl:616 -msgid "Import Directory" -msgstr "ייבוא מדור" - -#: mod_configure.erl:141 mod_configure.erl:581 mod_configure.erl:1057 -msgid "Restart Service" -msgstr "אתחל שירות" - -# שירות כיבוי -#: mod_configure.erl:143 mod_configure.erl:582 mod_configure.erl:1102 -msgid "Shut Down Service" -msgstr "כבה שירות" - -#: mod_configure.erl:145 mod_configure.erl:518 mod_configure.erl:1197 -#: web/ejabberd_web_admin.erl:1527 -msgid "Add User" -msgstr "הוסף משתמש" - -#: mod_configure.erl:147 mod_configure.erl:519 mod_configure.erl:1219 -msgid "Delete User" -msgstr "מחק משתמש" - -#: mod_configure.erl:149 mod_configure.erl:520 mod_configure.erl:1231 -msgid "End User Session" -msgstr "סיים סשן משתמש" - -#: mod_configure.erl:151 mod_configure.erl:521 mod_configure.erl:1243 -#: mod_configure.erl:1255 -msgid "Get User Password" -msgstr "השג סיסמת משתמש" - -#: mod_configure.erl:153 mod_configure.erl:522 -msgid "Change User Password" -msgstr "שנה סיסמת משתמש" - -# התחברות -#: mod_configure.erl:155 mod_configure.erl:523 mod_configure.erl:1272 -msgid "Get User Last Login Time" -msgstr "השג זמן כניסה אחרון של משתמש" - -#: mod_configure.erl:157 mod_configure.erl:524 mod_configure.erl:1284 -msgid "Get User Statistics" -msgstr "השג סטטיסטיקת משתמש" - -#: mod_configure.erl:159 mod_configure.erl:525 -msgid "Get Number of Registered Users" -msgstr "השג מספר של משתמשים רשומים" - -#: mod_configure.erl:161 mod_configure.erl:526 -msgid "Get Number of Online Users" -msgstr "השג מספר של משתמשים מקוונים" - -# כניסה אל בקרת רשימות -#: mod_configure.erl:163 mod_configure.erl:509 web/ejabberd_web_admin.erl:831 -#: web/ejabberd_web_admin.erl:872 -msgid "Access Control Lists" -msgstr "רשימות בקרת גישה" - -# חוקי -#: mod_configure.erl:165 mod_configure.erl:510 web/ejabberd_web_admin.erl:940 -#: web/ejabberd_web_admin.erl:976 -msgid "Access Rules" -msgstr "כללי גישה" - -# הנהלת -#: mod_configure.erl:297 mod_configure.erl:499 -msgid "User Management" -msgstr "ניהול משתמשים" - -#: mod_configure.erl:500 web/ejabberd_web_admin.erl:1058 -#: web/ejabberd_web_admin.erl:1462 -msgid "Online Users" -msgstr "משתמשים מקוונים" - -#: mod_configure.erl:501 -msgid "All Users" -msgstr "כל המשתמשים" - -#: mod_configure.erl:502 -msgid "Outgoing s2s Connections" -msgstr "חיבורי s2s יוצאים" - -#: mod_configure.erl:503 web/ejabberd_web_admin.erl:1907 -msgid "Running Nodes" -msgstr "ממסרים שמורצים כעת" - -#: mod_configure.erl:504 web/ejabberd_web_admin.erl:1909 -msgid "Stopped Nodes" -msgstr "ממסרים שנפסקו" - -#: mod_configure.erl:577 mod_configure.erl:587 web/ejabberd_web_admin.erl:1953 -msgid "Modules" -msgstr "מודולים" - -#: mod_configure.erl:578 -msgid "Backup Management" -msgstr "ניהול גיבוי" - -# help is needed with spool -#: mod_configure.erl:579 -msgid "Import Users From jabberd14 Spool Files" -msgstr "ייבוא משתמשים מתוך קבצי סליל (Spool Files) של jabberd14" - -#: mod_configure.erl:699 -msgid "To ~s" -msgstr "אל ~s" - -#: mod_configure.erl:717 -msgid "From ~s" -msgstr "מאת ~s" - -#: mod_configure.erl:913 -msgid "Database Tables Configuration at " -msgstr "תצורת טבלאות מסד נתונים אצל " - -#: mod_configure.erl:918 -msgid "Choose storage type of tables" -msgstr "בחר טיפוס אחסון של טבלאות" - -# Typo: Disk (unsure) -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Disc only copy" -msgstr "העתק של תקליטור בלבד" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM and disc copy" -msgstr "העתק RAM וגם תקליטור" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM copy" -msgstr "העתק RAM" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Remote copy" -msgstr "עותק מרוחק" - -# at (time)? -# בשעה -#: mod_configure.erl:950 -#, fuzzy -msgid "Stop Modules at " -msgstr "הפסק מודולים at " - -#: mod_configure.erl:954 -msgid "Choose modules to stop" -msgstr "בחר מודולים להפסקה" - -#: mod_configure.erl:969 -#, fuzzy -msgid "Start Modules at " -msgstr "התחל מודולים at " - -#: mod_configure.erl:973 -msgid "Enter list of {Module, [Options]}" -msgstr "הזן רשימה של {מודול, [אפשרויות]}" - -#: mod_configure.erl:974 -msgid "List of modules to start" -msgstr "רשימה של מודולים להפעלה" - -#: mod_configure.erl:983 -msgid "Backup to File at " -msgstr "גבה אל קובץ אצל " - -#: mod_configure.erl:987 mod_configure.erl:1001 -msgid "Enter path to backup file" -msgstr "הזן נתיב אל קובץ גיבוי" - -#: mod_configure.erl:988 mod_configure.erl:1002 mod_configure.erl:1016 -#: mod_configure.erl:1030 -msgid "Path to File" -msgstr "נתיב אל קובץ" - -#: mod_configure.erl:997 -msgid "Restore Backup from File at " -msgstr "שחזר גיבוי מתוך קובץ אצל " - -#: mod_configure.erl:1011 -msgid "Dump Backup to Text File at " -msgstr "השלך גיבוי אל קובץ טקסט אצל " - -#: mod_configure.erl:1015 -msgid "Enter path to text file" -msgstr "הזן נתיב אל קובץ טקסט" - -#: mod_configure.erl:1025 -msgid "Import User from File at " -msgstr "ייבוא משתמש מתוך קובץ אצל " - -#: mod_configure.erl:1029 -msgid "Enter path to jabberd14 spool file" -msgstr "הזן נתיב אל קובץ סליל (spool file) של jabberd14" - -#: mod_configure.erl:1039 -msgid "Import Users from Dir at " -msgstr "ייבוא משתמשים מתוך מדור אצל " - -#: mod_configure.erl:1043 -msgid "Enter path to jabberd14 spool dir" -msgstr "הזן נתיב אל מדור סליל (spool dir) של jabberd14" - -#: mod_configure.erl:1044 -msgid "Path to Dir" -msgstr "נתיב אל מדור" - -#: mod_configure.erl:1060 mod_configure.erl:1105 -msgid "Time delay" -msgstr "זמן שיהוי" - -# כניסה אל תצורת בקרת רשימה -#: mod_configure.erl:1143 -msgid "Access Control List Configuration" -msgstr "תצורת רשימת בקרת גישה" - -#: mod_configure.erl:1147 -msgid "Access control lists" -msgstr "רשימות בקרת גישה" - -# כניסה אל תצורה -#: mod_configure.erl:1171 -msgid "Access Configuration" -msgstr "תצורת גישה" - -#: mod_configure.erl:1175 -msgid "Access rules" -msgstr "כללי גישה" - -#: mod_configure.erl:1200 mod_configure.erl:1222 mod_configure.erl:1234 -#: mod_configure.erl:1246 mod_configure.erl:1258 mod_configure.erl:1275 -#: mod_configure.erl:1287 mod_configure.erl:1650 mod_configure.erl:1700 -#: mod_configure.erl:1721 mod_roster.erl:943 mod_roster_odbc.erl:1060 -#: mod_vcard.erl:472 mod_vcard_ldap.erl:554 mod_vcard_odbc.erl:448 -msgid "Jabber ID" -msgstr "מזהה Jabber" - -#: mod_configure.erl:1205 mod_configure.erl:1263 mod_configure.erl:1651 -#: mod_configure.erl:1863 mod_muc/mod_muc_room.erl:3224 mod_register.erl:235 -#: web/ejabberd_web_admin.erl:1520 -msgid "Password" -msgstr "סיסמה" - -#: mod_configure.erl:1210 -msgid "Password Verification" -msgstr "אימות סיסמה" - -#: mod_configure.erl:1301 -msgid "Number of registered users" -msgstr "מספר של משתמשים רשומים" - -#: mod_configure.erl:1315 -msgid "Number of online users" -msgstr "מספר של משתמשים מקוונים" - -#: mod_configure.erl:1682 web/ejabberd_web_admin.erl:1588 -#: web/ejabberd_web_admin.erl:1743 -msgid "Never" -msgstr "אף פעם" - -#: mod_configure.erl:1696 web/ejabberd_web_admin.erl:1601 -#: web/ejabberd_web_admin.erl:1756 -msgid "Online" -msgstr "מקוון" - -#: mod_configure.erl:1701 -msgid "Last login" -msgstr "כניסה אחרונה" - -#: mod_configure.erl:1722 -msgid "Roster size" -msgstr "גודל רשימה" - -#: mod_configure.erl:1723 -msgid "IP addresses" -msgstr "כתובות IP" - -#: mod_configure.erl:1724 -msgid "Resources" -msgstr "משאבים" - -#: mod_configure.erl:1850 -msgid "Administration of " -msgstr "ניהול של " - -#: mod_configure.erl:1853 -msgid "Action on user" -msgstr "פעולה על משתמש" - -#: mod_configure.erl:1857 -msgid "Edit Properties" -msgstr "ערוך מאפיינים" - -#: mod_configure.erl:1860 web/ejabberd_web_admin.erl:1769 -msgid "Remove User" -msgstr "הסר משתמש" - -#: mod_irc/mod_irc.erl:201 mod_irc/mod_irc_odbc.erl:196 -#: mod_muc/mod_muc.erl:336 mod_muc/mod_muc_odbc.erl:342 -msgid "Access denied by service policy" -msgstr "גישה נדחתה על ידי פוליסת שירות" - -#: mod_irc/mod_irc.erl:401 mod_irc/mod_irc_odbc.erl:398 -msgid "IRC Transport" -msgstr "מוביל IRC" - -#: mod_irc/mod_irc.erl:428 mod_irc/mod_irc_odbc.erl:425 -msgid "ejabberd IRC module" -msgstr "מודול IRC של ejabberd" - -#: mod_irc/mod_irc.erl:559 mod_irc/mod_irc_odbc.erl:568 -msgid "You need an x:data capable client to configure mod_irc settings" -msgstr "עליך להשתמש בלקוח אשר מסוגל להבין x:data בכדי להגדיר הגדרות של mod_irc" - -#: mod_irc/mod_irc.erl:566 mod_irc/mod_irc_odbc.erl:575 -msgid "Registration in mod_irc for " -msgstr "רישום בתוך mod_irc עבור " - -#: mod_irc/mod_irc.erl:571 mod_irc/mod_irc_odbc.erl:580 -msgid "" -"Enter username, encodings, ports and passwords you wish to use for " -"connecting to IRC servers" -msgstr "" -"הזן שם משתמש, קידודים, פורטים וסיסמאות בהם ברצונך להשתמש לשם התחברות אל " -"שרתים של IRC" - -#: mod_irc/mod_irc.erl:576 mod_irc/mod_irc_odbc.erl:585 -msgid "IRC Username" -msgstr "שם משתמש IRC" - -#: mod_irc/mod_irc.erl:586 mod_irc/mod_irc_odbc.erl:595 -msgid "" -"If you want to specify different ports, passwords, encodings for IRC " -"servers, fill this list with values in format '{\"irc server\", \"encoding" -"\", port, \"password\"}'. By default this service use \"~s\" encoding, port " -"~p, empty password." -msgstr "" -"אם ברצונך לציין פורטים, סיסמאות, קידודים אחרים עבור שרתים של IRC, מלא את " -"רשימה זו עם ערכים בפורמט '{\"irc server\", \"encoding\", port, \"password" -"\"}'. באופן משתמט שירות זה משתמש בקידוד \"~s\", פורט ~p, סיסמה ריקה." - -#: mod_irc/mod_irc.erl:598 mod_irc/mod_irc_odbc.erl:607 -msgid "" -"Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef." -"net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]." -msgstr "" -"דוגמא: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef." -"net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]." - -# פרמטרי חיבור -#: mod_irc/mod_irc.erl:603 mod_irc/mod_irc_odbc.erl:612 -msgid "Connections parameters" -msgstr "פרמטרים של חיבור" - -#: mod_irc/mod_irc.erl:728 mod_irc/mod_irc_odbc.erl:757 -msgid "Join IRC channel" -msgstr "הצטרף אל ערוץ IRC" - -# לא לשים סימן # ראשון -# לא לשים את סימן # הראשון -#: mod_irc/mod_irc.erl:732 mod_irc/mod_irc_odbc.erl:761 -msgid "IRC channel (don't put the first #)" -msgstr "ערוץ IRC (אל תשים סימן # ראשון)" - -#: mod_irc/mod_irc.erl:737 mod_irc/mod_irc_odbc.erl:766 -msgid "IRC server" -msgstr "שרת IRC" - -#: mod_irc/mod_irc.erl:770 mod_irc/mod_irc.erl:774 -#: mod_irc/mod_irc_odbc.erl:799 mod_irc/mod_irc_odbc.erl:803 -msgid "Join the IRC channel here." -msgstr "הצטרף אל ערוץ IRC כאן." - -#: mod_irc/mod_irc.erl:778 mod_irc/mod_irc_odbc.erl:807 -msgid "Join the IRC channel in this Jabber ID: ~s" -msgstr "הצטרף אל ערוץ IRC במזהה Jabber זה: ~s" - -#: mod_irc/mod_irc.erl:863 mod_irc/mod_irc_odbc.erl:904 -msgid "IRC settings" -msgstr "הגדרות IRC" - -# השלם -#: mod_irc/mod_irc.erl:868 mod_irc/mod_irc_odbc.erl:909 -msgid "" -"Enter username and encodings you wish to use for connecting to IRC servers. " -"Press 'Next' to get more fields to fill in. Press 'Complete' to save " -"settings." -msgstr "" -"הזן שם משתמש וקידודים בהם ברצונך להשתמש לשם התחברות אל שרתים של IRC. לחץ " -"'הבא' כדי להשיג עוד שדות למילוי. לחץ 'סיים' כדי לשמור הגדרות." - -#: mod_irc/mod_irc.erl:874 mod_irc/mod_irc_odbc.erl:915 -msgid "IRC username" -msgstr "שם משתמש IRC" - -#: mod_irc/mod_irc.erl:923 mod_irc/mod_irc_odbc.erl:964 -msgid "Password ~b" -msgstr "סיסמה ~b" - -#: mod_irc/mod_irc.erl:928 mod_irc/mod_irc_odbc.erl:969 -msgid "Port ~b" -msgstr "פורט ~b" - -#: mod_irc/mod_irc.erl:933 mod_irc/mod_irc_odbc.erl:974 -msgid "Encoding for server ~b" -msgstr "קידוד עבור שרת ~b" - -#: mod_irc/mod_irc.erl:942 mod_irc/mod_irc_odbc.erl:983 -msgid "Server ~b" -msgstr "שרת ~b" - -#: mod_muc/mod_muc.erl:449 mod_muc/mod_muc_odbc.erl:456 -msgid "Only service administrators are allowed to send service messages" -msgstr "רק מנהלי שירות רשאים לשלוח הודעות שירות" - -#: mod_muc/mod_muc.erl:493 mod_muc/mod_muc_odbc.erl:500 -msgid "Room creation is denied by service policy" -msgstr "יצירת חדר נדחתה על ידי פוליסת שירות" - -#: mod_muc/mod_muc.erl:500 mod_muc/mod_muc_odbc.erl:507 -msgid "Conference room does not exist" -msgstr "חדר ועידה לא קיים" - -#: mod_muc/mod_muc.erl:582 mod_muc/mod_muc_odbc.erl:595 -msgid "Chatrooms" -msgstr "חדרי שיחה" - -# to register nickname -#: mod_muc/mod_muc.erl:712 mod_muc/mod_muc_odbc.erl:729 -msgid "You need a client that supports x:data to register the nickname" -msgstr "עליך להשתמש בלקוח אשר תומך x:data בכדי לרשום את שם הכינוי" - -#: mod_muc/mod_muc.erl:718 mod_muc/mod_muc_odbc.erl:735 -msgid "Nickname Registration at " -msgstr "רישום שם כינוי אצל " - -# אותו ברצונך לרשום -#: mod_muc/mod_muc.erl:722 mod_muc/mod_muc_odbc.erl:739 -msgid "Enter nickname you want to register" -msgstr "הזן שם כינוי אשר ברצונך לרושמו" - -#: mod_muc/mod_muc.erl:723 mod_muc/mod_muc_odbc.erl:740 -#: mod_muc/mod_muc_room.erl:3810 mod_roster.erl:944 mod_roster_odbc.erl:1061 -#: mod_vcard.erl:364 mod_vcard.erl:477 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:453 -msgid "Nickname" -msgstr "שם כינוי" - -# note: another person > someone else -#: mod_muc/mod_muc.erl:762 mod_muc/mod_muc_odbc.erl:784 -#: mod_muc/mod_muc_room.erl:1064 mod_muc/mod_muc_room.erl:1787 -msgid "That nickname is registered by another person" -msgstr "שם כינוי זה הינו רשום על ידי מישהו אחר" - -#: mod_muc/mod_muc.erl:788 mod_muc/mod_muc_odbc.erl:811 -msgid "You must fill in field \"Nickname\" in the form" -msgstr "עליך למלא את השדה \"שם כינוי\" בתוך התבנית" - -#: mod_muc/mod_muc.erl:808 mod_muc/mod_muc_odbc.erl:831 -msgid "ejabberd MUC module" -msgstr "מודול MUC של ejabberd" - -#: mod_muc/mod_muc_log.erl:374 mod_muc/mod_muc_log.erl:381 -msgid "Chatroom configuration modified" -msgstr "תצורת חדר שיחה שונתה" - -#: mod_muc/mod_muc_log.erl:384 -msgid "joins the room" -msgstr "נכנס/ת אל החדר" - -#: mod_muc/mod_muc_log.erl:387 mod_muc/mod_muc_log.erl:390 -msgid "leaves the room" -msgstr "עוזב/ת אל החדר" - -#: mod_muc/mod_muc_log.erl:393 mod_muc/mod_muc_log.erl:396 -msgid "has been banned" -msgstr "נאסר/ה" - -#: mod_muc/mod_muc_log.erl:399 mod_muc/mod_muc_log.erl:402 -msgid "has been kicked" -msgstr "נבעט/ה" - -#: mod_muc/mod_muc_log.erl:405 -msgid "has been kicked because of an affiliation change" -msgstr "נבעט/ה משום שינוי סינוף" - -#: mod_muc/mod_muc_log.erl:408 -msgid "has been kicked because the room has been changed to members-only" -msgstr "נבעט/ה משום שהחדר שונה אל חברים-בלבד" - -#: mod_muc/mod_muc_log.erl:411 -msgid "has been kicked because of a system shutdown" -msgstr "נבעט/ה משום כיבוי מערכת" - -#: mod_muc/mod_muc_log.erl:414 -msgid "is now known as" -msgstr "ידועה כעת בכינוי" - -#: mod_muc/mod_muc_log.erl:417 mod_muc/mod_muc_log.erl:688 -#: mod_muc/mod_muc_room.erl:2393 -msgid " has set the subject to: " -msgstr " הגדיר/ה את הנושא אל: " - -#: mod_muc/mod_muc_log.erl:452 -msgid "Chatroom is created" -msgstr "חדר שיחה נוצר" - -#: mod_muc/mod_muc_log.erl:453 -msgid "Chatroom is destroyed" -msgstr "חדר שיחה הרוס" - -#: mod_muc/mod_muc_log.erl:454 -msgid "Chatroom is started" -msgstr "חדר שיחה מותחל" - -#: mod_muc/mod_muc_log.erl:455 -msgid "Chatroom is stopped" -msgstr "חדר שיחה הופסק" - -#: mod_muc/mod_muc_log.erl:459 -msgid "Monday" -msgstr "יום שני" - -#: mod_muc/mod_muc_log.erl:460 -msgid "Tuesday" -msgstr "יום שלישי" - -#: mod_muc/mod_muc_log.erl:461 -msgid "Wednesday" -msgstr "יום רביעי" - -#: mod_muc/mod_muc_log.erl:462 -msgid "Thursday" -msgstr "יום חמישי" - -#: mod_muc/mod_muc_log.erl:463 -msgid "Friday" -msgstr "יום שישי" - -#: mod_muc/mod_muc_log.erl:464 -msgid "Saturday" -msgstr "יום שבת" - -#: mod_muc/mod_muc_log.erl:465 -msgid "Sunday" -msgstr "יום ראשון" - -#: mod_muc/mod_muc_log.erl:469 -msgid "January" -msgstr "ינואר" - -#: mod_muc/mod_muc_log.erl:470 -msgid "February" -msgstr "פברואר" - -#: mod_muc/mod_muc_log.erl:471 -msgid "March" -msgstr "מרץ" - -#: mod_muc/mod_muc_log.erl:472 -msgid "April" -msgstr "אפריל" - -#: mod_muc/mod_muc_log.erl:473 -msgid "May" -msgstr "מאי" - -#: mod_muc/mod_muc_log.erl:474 -msgid "June" -msgstr "יוני" - -#: mod_muc/mod_muc_log.erl:475 -msgid "July" -msgstr "יולי" - -#: mod_muc/mod_muc_log.erl:476 -msgid "August" -msgstr "אוגוסט" - -#: mod_muc/mod_muc_log.erl:477 -msgid "September" -msgstr "ספטמבר" - -#: mod_muc/mod_muc_log.erl:478 -msgid "October" -msgstr "אוקטובר" - -#: mod_muc/mod_muc_log.erl:479 -msgid "November" -msgstr "נובמבר" - -#: mod_muc/mod_muc_log.erl:480 -msgid "December" -msgstr "דצמבר" - -#: mod_muc/mod_muc_log.erl:750 -msgid "Room Configuration" -msgstr "תצורת חדר" - -#: mod_muc/mod_muc_log.erl:759 -msgid "Room Occupants" -msgstr "נוכחי חדר" - -# נעברה -#: mod_muc/mod_muc_room.erl:174 -msgid "Traffic rate limit is exceeded" -msgstr "מגבלת שיעור תעבורה נחצתה" - -#: mod_muc/mod_muc_room.erl:246 -msgid "" -"This participant is kicked from the room because he sent an error message" -msgstr "משתתף זה נבעט מתוך החדר מכיוון שהוא שלח הודעת שגיאה" - -#: mod_muc/mod_muc_room.erl:255 -msgid "It is not allowed to send private messages to the conference" -msgstr "אין זה מותר לשלוח הודעות פרטיות אל הועידה" - -#: mod_muc/mod_muc_room.erl:332 -msgid "Please, wait for a while before sending new voice request" -msgstr "אנא, המתן לזמן מה לפני שליחת בקשת ביטוי חדשה" - -#: mod_muc/mod_muc_room.erl:347 -msgid "Voice requests are disabled in this conference" -msgstr "בקשות ביטוי מנוטרלות בועידה זו" - -#: mod_muc/mod_muc_room.erl:364 -msgid "Failed to extract JID from your voice request approval" -msgstr "נכשל לחלץ JID מתוך אישור בקשת הביטוי שלך" - -#: mod_muc/mod_muc_room.erl:393 -msgid "Only moderators can approve voice requests" -msgstr "רק אחראים יכולים לאשר בקשות ביטוי" - -# הולם -#: mod_muc/mod_muc_room.erl:408 -msgid "Improper message type" -msgstr "טיפוס הודעה לא מתאים" - -#: mod_muc/mod_muc_room.erl:518 -msgid "" -"This participant is kicked from the room because he sent an error message to " -"another participant" -msgstr "משתתף זה נבעט מתוך החדר משום שהוא שלח הודעת שגיאה אל משתתף אחר" - -#: mod_muc/mod_muc_room.erl:531 -msgid "It is not allowed to send private messages of type \"groupchat\"" -msgstr "אין זה מותר לשלוח הודעות פרטיות מן טיפוס \"groupchat\"" - -#: mod_muc/mod_muc_room.erl:543 mod_muc/mod_muc_room.erl:611 -msgid "Recipient is not in the conference room" -msgstr "מקבל אינו מצוי בחדר הועידה" - -# אסור -#: mod_muc/mod_muc_room.erl:564 mod_muc/mod_muc_room.erl:585 -msgid "It is not allowed to send private messages" -msgstr "אין זה מותר לשלוח הודעות פרטיות" - -#: mod_muc/mod_muc_room.erl:576 mod_muc/mod_muc_room.erl:956 -#: mod_muc/mod_muc_room.erl:4040 -msgid "Only occupants are allowed to send messages to the conference" -msgstr "רק נוכחים רשאים לשלוח הודעות אל הועידה" - -#: mod_muc/mod_muc_room.erl:634 -msgid "Only occupants are allowed to send queries to the conference" -msgstr "רק נוכחים רשאים לשלוח שאילתות אל הועידה" - -#: mod_muc/mod_muc_room.erl:646 -msgid "Queries to the conference members are not allowed in this room" -msgstr "שאילתות אל חברי הועידה אינן מותרות בחדר זה" - -#: mod_muc/mod_muc_room.erl:932 -msgid "" -"Only moderators and participants are allowed to change the subject in this " -"room" -msgstr "רק אחראים ומשתתפים רשאים לשנות את הנושא בחדר זה" - -#: mod_muc/mod_muc_room.erl:937 -msgid "Only moderators are allowed to change the subject in this room" -msgstr "רק אחראים רשאים לשנות את הנושא בחדר זה" - -# רשאים -#: mod_muc/mod_muc_room.erl:947 -msgid "Visitors are not allowed to send messages to all occupants" -msgstr "מבקרים אינם מורשים לשלוח הודעות אל כל הנוכחים" - -# שגיאת נוכחות -# נוכחות שגויה -#: mod_muc/mod_muc_room.erl:1021 -msgid "" -"This participant is kicked from the room because he sent an error presence" -msgstr "משתתף זה נבעט מתוך החדר משום שהוא שלח נוכחות שגיאה" - -#: mod_muc/mod_muc_room.erl:1040 -msgid "Visitors are not allowed to change their nicknames in this room" -msgstr "מבקרים אינם מורשים לשנות את שמות הכינויים שלהם בחדר זה" - -#: mod_muc/mod_muc_room.erl:1053 mod_muc/mod_muc_room.erl:1779 -msgid "That nickname is already in use by another occupant" -msgstr "שם כינוי זה כבר מצוי בשימוש על ידי נוכח אחר" - -#: mod_muc/mod_muc_room.erl:1768 -msgid "You have been banned from this room" -msgstr "נאסרת מן חדר זה" - -#: mod_muc/mod_muc_room.erl:1771 -msgid "Membership is required to enter this room" -msgstr "נדרשת חברות כדי להיכנס אל חדר זה" - -#: mod_muc/mod_muc_room.erl:1807 -msgid "This room is not anonymous" -msgstr "חדר זה אינו אנונימי" - -# מילת־מעבר -#: mod_muc/mod_muc_room.erl:1833 -msgid "A password is required to enter this room" -msgstr "נדרשת סיסמה כדי להיכנס אל חדר זה" - -#: mod_muc/mod_muc_room.erl:1855 mod_register.erl:246 -msgid "Too many CAPTCHA requests" -msgstr "יותר מדי בקשות CAPTCHA" - -#: mod_muc/mod_muc_room.erl:1864 mod_register.erl:251 -msgid "Unable to generate a CAPTCHA" -msgstr "אין אפשרות לחולל CAPTCHA" - -#: mod_muc/mod_muc_room.erl:1874 -msgid "Incorrect password" -msgstr "מילת מעבר שגויה" - -#: mod_muc/mod_muc_room.erl:2448 -msgid "Administrator privileges required" -msgstr "נדרשות הרשאות מנהל" - -#: mod_muc/mod_muc_room.erl:2463 -msgid "Moderator privileges required" -msgstr "נדרשות הרשאות אחראי" - -#: mod_muc/mod_muc_room.erl:2619 -msgid "Jabber ID ~s is invalid" -msgstr "מזהה Jabber ‏~s הינו שגוי" - -#: mod_muc/mod_muc_room.erl:2633 -msgid "Nickname ~s does not exist in the room" -msgstr "שם כינוי ~s לא קיים בחדר" - -#: mod_muc/mod_muc_room.erl:2659 mod_muc/mod_muc_room.erl:3049 -msgid "Invalid affiliation: ~s" -msgstr "סינוף שגוי: ~s" - -#: mod_muc/mod_muc_room.erl:2713 -msgid "Invalid role: ~s" -msgstr "תפקיד שגוי: ~s" - -#: mod_muc/mod_muc_room.erl:3026 mod_muc/mod_muc_room.erl:3062 -msgid "Owner privileges required" -msgstr "נדרשות הרשאות בעלים" - -# תצורה של חדר -#: mod_muc/mod_muc_room.erl:3195 -msgid "Configuration of room ~s" -msgstr "תצורת חדר ~s" - -#: mod_muc/mod_muc_room.erl:3200 -msgid "Room title" -msgstr "כותרת חדר" - -#: mod_muc/mod_muc_room.erl:3203 mod_muc/mod_muc_room.erl:3692 -msgid "Room description" -msgstr "תיאור חדר" - -#: mod_muc/mod_muc_room.erl:3210 -msgid "Make room persistent" -msgstr "הפוך חדר אל קבוע" - -#: mod_muc/mod_muc_room.erl:3215 -msgid "Make room public searchable" -msgstr "הפוך חדר אל בר חיפוש פומבי" - -#: mod_muc/mod_muc_room.erl:3218 -msgid "Make participants list public" -msgstr "הפוך רשימת משתתפים אל פומבית" - -#: mod_muc/mod_muc_room.erl:3221 -msgid "Make room password protected" -msgstr "הפוך חדר אל מוגן במילת מעבר" - -#: mod_muc/mod_muc_room.erl:3232 -msgid "Maximum Number of Occupants" -msgstr "מספר מרבי של נוכחים" - -#: mod_muc/mod_muc_room.erl:3239 -msgid "No limit" -msgstr "ללא הגבלה" - -# הצג כתובות JID ממשיות ל -#: mod_muc/mod_muc_room.erl:3250 -msgid "Present real Jabber IDs to" -msgstr "הצג כתובות JID ממשיות" - -#: mod_muc/mod_muc_room.erl:3258 mod_muc/mod_muc_room.erl:3292 -msgid "moderators only" -msgstr "לאחראים בלבד" - -#: mod_muc/mod_muc_room.erl:3260 mod_muc/mod_muc_room.erl:3294 -msgid "anyone" -msgstr "לכל אחד" - -#: mod_muc/mod_muc_room.erl:3262 -msgid "Make room members-only" -msgstr "הפוך חדר אל חברים-בלבד" - -# חדר מבוקר חדר תחת ביקורת -#: mod_muc/mod_muc_room.erl:3265 -msgid "Make room moderated" -msgstr "הפוך חדר אל מבוקר" - -#: mod_muc/mod_muc_room.erl:3268 -msgid "Default users as participants" -msgstr "משתמשים משתמטים כמשתתפים" - -# התרה -#: mod_muc/mod_muc_room.erl:3271 -msgid "Allow users to change the subject" -msgstr "התר למשתמשים לשנות את הנושא" - -#: mod_muc/mod_muc_room.erl:3274 -msgid "Allow users to send private messages" -msgstr "התר למשתמשים לשלוח הודעות פרטיות" - -#: mod_muc/mod_muc_room.erl:3279 -msgid "Allow visitors to send private messages to" -msgstr "התר למבקרים לשלוח הודעות פרטיות אל" - -#: mod_muc/mod_muc_room.erl:3290 -msgid "nobody" -msgstr "אף אחד" - -#: mod_muc/mod_muc_room.erl:3296 -msgid "Allow users to query other users" -msgstr "התר למשתמשים לתשאל משתמשים אחרים" - -#: mod_muc/mod_muc_room.erl:3299 -msgid "Allow users to send invites" -msgstr "התר למשתמשים לשלוח הזמנות" - -#: mod_muc/mod_muc_room.erl:3302 -msgid "Allow visitors to send status text in presence updates" -msgstr "התר למבקרים לשלוח טקסט מצב בעדכוני נוכחות" - -#: mod_muc/mod_muc_room.erl:3305 -msgid "Allow visitors to change nickname" -msgstr "התר למבקרים לשנות שם כינוי" - -#: mod_muc/mod_muc_room.erl:3308 -msgid "Allow visitors to send voice requests" -msgstr "התר למבקרים לשלוח בקשות ביטוי" - -#: mod_muc/mod_muc_room.erl:3311 -msgid "Minimum interval between voice requests (in seconds)" -msgstr "תדירות מינימלית בין בקשות ביטוי (בשניות)" - -#: mod_muc/mod_muc_room.erl:3317 -msgid "Make room CAPTCHA protected" -msgstr "הפוך חדר אל מוגן CAPTCHA" - -# זהויות -#: mod_muc/mod_muc_room.erl:3322 -msgid "Exclude Jabber IDs from CAPTCHA challenge" -msgstr "הוצא כתובות של Jabber מתוך אתגר CAPTCHA" - -#: mod_muc/mod_muc_room.erl:3329 -msgid "Enable logging" -msgstr "אפשור רישום פעילות" - -#: mod_muc/mod_muc_room.erl:3337 -msgid "You need an x:data capable client to configure room" -msgstr "עליך להשתמש בלקוח אשר מסוגל להבין x:data בכדי להגדיר חדר" - -#: mod_muc/mod_muc_room.erl:3694 -msgid "Number of occupants" -msgstr "מספר של נוכחים" - -# private what? (fe/male) -#: mod_muc/mod_muc_room.erl:3750 -#, fuzzy -msgid "private, " -msgstr "פרטי, " - -#: mod_muc/mod_muc_room.erl:3799 -msgid "Voice request" -msgstr "בקשת ביטוי" - -# אשר או דחה -#: mod_muc/mod_muc_room.erl:3803 -msgid "Either approve or decline the voice request." -msgstr "או שתאשר או שתדחה את בקשת הביטוי." - -#: mod_muc/mod_muc_room.erl:3809 -msgid "User JID" -msgstr "‏JID משתמש" - -#: mod_muc/mod_muc_room.erl:3811 -msgid "Grant voice to this person?" -msgstr "להעניק ביטוי לאישיות זו?" - -#: mod_muc/mod_muc_room.erl:3960 -msgid "~s invites you to the room ~s" -msgstr "‫~s מזמינך אל החדר ~s" - -#: mod_muc/mod_muc_room.erl:3969 -msgid "the password is" -msgstr "הסיסמה היא" - -# תור הודעות לא מקוונות של הקשר שלך הינו -#: mod_offline.erl:510 mod_offline_odbc.erl:352 -msgid "" -"Your contact offline message queue is full. The message has been discarded." -msgstr "תור הודעות קשר לא מקוונות הינו מלא. ההודעה סולקה." - -#: mod_offline.erl:560 mod_offline_odbc.erl:408 -msgid "~s's Offline Messages Queue" -msgstr "תור הודעות לא מקוונות של ~s" - -#: mod_offline.erl:563 mod_offline_odbc.erl:411 mod_roster.erl:987 -#: mod_roster_odbc.erl:1104 mod_shared_roster.erl:901 -#: mod_shared_roster.erl:1010 mod_shared_roster_odbc.erl:930 -#: mod_shared_roster_odbc.erl:1039 web/ejabberd_web_admin.erl:833 -#: web/ejabberd_web_admin.erl:874 web/ejabberd_web_admin.erl:942 -#: web/ejabberd_web_admin.erl:978 web/ejabberd_web_admin.erl:1019 -#: web/ejabberd_web_admin.erl:1508 web/ejabberd_web_admin.erl:1760 -#: web/ejabberd_web_admin.erl:1931 web/ejabberd_web_admin.erl:1963 -#: web/ejabberd_web_admin.erl:2031 web/ejabberd_web_admin.erl:2135 -#: web/ejabberd_web_admin.erl:2160 web/ejabberd_web_admin.erl:2248 -msgid "Submitted" -msgstr "" - -#: mod_offline.erl:571 -msgid "Time" -msgstr "זמן" - -# מאת -#: mod_offline.erl:572 -msgid "From" -msgstr "מן" - -#: mod_offline.erl:573 -msgid "To" -msgstr "אל" - -#: mod_offline.erl:574 mod_offline_odbc.erl:419 -#, fuzzy -msgid "Packet" -msgstr "חבילת מידע" - -# נבחרים -#: mod_offline.erl:587 mod_offline_odbc.erl:432 mod_shared_roster.erl:908 -#: mod_shared_roster_odbc.erl:937 web/ejabberd_web_admin.erl:882 -#: web/ejabberd_web_admin.erl:986 -msgid "Delete Selected" -msgstr "מחק נבחרות" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -msgid "Offline Messages:" -msgstr "הודעות לא מקוונות:" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -msgid "Remove All Offline Messages" -msgstr "הסר את כל ההודעות הלא מקוונות" - -#: mod_proxy65/mod_proxy65_service.erl:213 -msgid "ejabberd SOCKS5 Bytestreams module" -msgstr "מודול SOCKS5 Bytestreams של ejabberd" - -#: mod_pubsub/mod_pubsub.erl:1120 mod_pubsub/mod_pubsub_odbc.erl:929 -msgid "Publish-Subscribe" -msgstr "‫Publish-Subscribe" - -#: mod_pubsub/mod_pubsub.erl:1214 mod_pubsub/mod_pubsub_odbc.erl:1025 -msgid "ejabberd Publish-Subscribe module" -msgstr "מודול Publish-Subscribe של ejabberd" - -#: mod_pubsub/mod_pubsub.erl:1497 mod_pubsub/mod_pubsub_odbc.erl:1312 -msgid "PubSub subscriber request" -msgstr "בקשת מנוי PubSub" - -# ההרשמה -#: mod_pubsub/mod_pubsub.erl:1499 mod_pubsub/mod_pubsub_odbc.erl:1314 -msgid "Choose whether to approve this entity's subscription." -msgstr "בחר האם לאשר את המנוי של ישות זו." - -#: mod_pubsub/mod_pubsub.erl:1505 mod_pubsub/mod_pubsub_odbc.erl:1320 -msgid "Node ID" -msgstr "מזהה ממסר (NID)" - -#: mod_pubsub/mod_pubsub.erl:1510 mod_pubsub/mod_pubsub_odbc.erl:1325 -msgid "Subscriber Address" -msgstr "כתובת מנוי" - -#: mod_pubsub/mod_pubsub.erl:1516 mod_pubsub/mod_pubsub_odbc.erl:1331 -msgid "Allow this Jabber ID to subscribe to this pubsub node?" -msgstr "להתיר לכתובת JID זו להירשם אל ממסר PubSub זה?" - -#: mod_pubsub/mod_pubsub.erl:3474 mod_pubsub/mod_pubsub_odbc.erl:3287 -msgid "Deliver payloads with event notifications" -msgstr "מסירת מטען ייעוד (מטע״ד) יחד עם התראות אירוע" - -#: mod_pubsub/mod_pubsub.erl:3475 mod_pubsub/mod_pubsub_odbc.erl:3288 -msgid "Deliver event notifications" -msgstr "מסירת התראות אירוע" - -# משתמשים רשומים -#: mod_pubsub/mod_pubsub.erl:3476 mod_pubsub/mod_pubsub_odbc.erl:3289 -msgid "Notify subscribers when the node configuration changes" -msgstr "הודע מנויים כאשר תצורת הממסר משתנה" - -#: mod_pubsub/mod_pubsub.erl:3477 mod_pubsub/mod_pubsub_odbc.erl:3290 -msgid "Notify subscribers when the node is deleted" -msgstr "הודע מנויים כאשר הממסר נמחק" - -#: mod_pubsub/mod_pubsub.erl:3478 mod_pubsub/mod_pubsub_odbc.erl:3291 -msgid "Notify subscribers when items are removed from the node" -msgstr "הודע מנויים כאשר פריטים מוסרים מתוך הממסר" - -# Typo: store -#: mod_pubsub/mod_pubsub.erl:3479 mod_pubsub/mod_pubsub_odbc.erl:3292 -msgid "Persist items to storage" -msgstr "פריטים קבועים לאחסון" - -#: mod_pubsub/mod_pubsub.erl:3480 mod_pubsub/mod_pubsub_odbc.erl:3293 -msgid "A friendly name for the node" -msgstr "שם ידידותי עבור הממסר" - -#: mod_pubsub/mod_pubsub.erl:3481 mod_pubsub/mod_pubsub_odbc.erl:3294 -msgid "Max # of items to persist" -msgstr "מספר מרבי של פריטים לקיבוע" - -# בין אם -#: mod_pubsub/mod_pubsub.erl:3482 mod_pubsub/mod_pubsub_odbc.erl:3295 -msgid "Whether to allow subscriptions" -msgstr "האם להתיר מנויים" - -# ציין מודל גישה -#: mod_pubsub/mod_pubsub.erl:3483 mod_pubsub/mod_pubsub_odbc.erl:3296 -msgid "Specify the access model" -msgstr "ציין את מודל הגישה" - -#: mod_pubsub/mod_pubsub.erl:3486 mod_pubsub/mod_pubsub_odbc.erl:3299 -msgid "Roster groups allowed to subscribe" -msgstr "קבוצות רשימה מורשות להירשם" - -# ציין מודל פרסום -#: mod_pubsub/mod_pubsub.erl:3487 mod_pubsub/mod_pubsub_odbc.erl:3300 -msgid "Specify the publisher model" -msgstr "ציין את מודל הפרסום" - -# טהר -#: mod_pubsub/mod_pubsub.erl:3489 mod_pubsub/mod_pubsub_odbc.erl:3302 -msgid "Purge all items when the relevant publisher goes offline" -msgstr "טיהור כל הפריטים כאשר המפרסם הרלוונטי " - -# ציין טיפוס הודעת אירוע -#: mod_pubsub/mod_pubsub.erl:3490 mod_pubsub/mod_pubsub_odbc.erl:3303 -msgid "Specify the event message type" -msgstr "ציין את טיפוס הודעת האירוע" - -# בבתים בבייטים (bytes) -#: mod_pubsub/mod_pubsub.erl:3492 mod_pubsub/mod_pubsub_odbc.erl:3305 -msgid "Max payload size in bytes" -msgstr "גודל מרבי של מטען הייעוד ביחידות מידה של byte" - -#: mod_pubsub/mod_pubsub.erl:3493 mod_pubsub/mod_pubsub_odbc.erl:3306 -msgid "When to send the last published item" -msgstr "מתי לשלוח פריט מפורסם אחרון" - -#: mod_pubsub/mod_pubsub.erl:3495 mod_pubsub/mod_pubsub_odbc.erl:3308 -msgid "Only deliver notifications to available users" -msgstr "מסור התראות אל משתמשים זמינים בלבד" - -# מסונף -#: mod_pubsub/mod_pubsub.erl:3496 mod_pubsub/mod_pubsub_odbc.erl:3309 -msgid "The collections with which a node is affiliated" -msgstr "" - -#: mod_register.erl:193 -msgid "The CAPTCHA verification has failed" -msgstr "אימות CAPTCHA נכשל" - -#: mod_register.erl:220 -msgid "You need a client that supports x:data and CAPTCHA to register" -msgstr "עליך להשתמש בלקוח אשר תומך x:data וגם CAPTCHA בכדי להירשם" - -#: mod_register.erl:226 mod_register.erl:265 -msgid "Choose a username and password to register with this server" -msgstr "בחר שם משתמש וסיסמה להירשם עם שרת זה" - -#: mod_register.erl:230 mod_vcard.erl:364 mod_vcard_odbc.erl:342 -#: web/ejabberd_web_admin.erl:1515 web/ejabberd_web_admin.erl:1572 -msgid "User" -msgstr "משתמש" - -#: mod_register.erl:316 mod_register.erl:361 -msgid "The password is too weak" -msgstr "הסיסמה חלשה מדי" - -# כה מהר -#: mod_register.erl:365 -msgid "Users are not allowed to register accounts so quickly" -msgstr "משתמשים אינם מורשים לרשום חשבונות כל כך במהירות" - -#: mod_roster.erl:938 mod_roster_odbc.erl:1055 web/ejabberd_web_admin.erl:1701 -#: web/ejabberd_web_admin.erl:1886 web/ejabberd_web_admin.erl:1897 -#: web/ejabberd_web_admin.erl:2219 -#, fuzzy -msgid "None" -msgstr "ללא" - -#: mod_roster.erl:945 mod_roster_odbc.erl:1062 -msgid "Subscription" -msgstr "מִנּוּי" - -# ממתינות -#: mod_roster.erl:946 mod_roster_odbc.erl:1063 -#, fuzzy -msgid "Pending" -msgstr "ממתינים" - -#: mod_roster.erl:947 mod_roster_odbc.erl:1064 -msgid "Groups" -msgstr "קבוצות" - -#: mod_roster.erl:974 mod_roster_odbc.erl:1091 -msgid "Validate" -msgstr "הענק תוקף" - -#: mod_roster.erl:982 mod_roster_odbc.erl:1099 -msgid "Remove" -msgstr "הסר" - -#: mod_roster.erl:985 mod_roster_odbc.erl:1102 -msgid "Roster of " -msgstr "רשימה של " - -# פגום -#: mod_roster.erl:988 mod_roster_odbc.erl:1105 mod_shared_roster.erl:902 -#: mod_shared_roster.erl:1011 mod_shared_roster_odbc.erl:931 -#: mod_shared_roster_odbc.erl:1040 web/ejabberd_web_admin.erl:834 -#: web/ejabberd_web_admin.erl:875 web/ejabberd_web_admin.erl:943 -#: web/ejabberd_web_admin.erl:979 web/ejabberd_web_admin.erl:1020 -#: web/ejabberd_web_admin.erl:1509 web/ejabberd_web_admin.erl:1761 -#: web/ejabberd_web_admin.erl:1932 web/ejabberd_web_admin.erl:2136 -#: web/ejabberd_web_admin.erl:2161 -msgid "Bad format" -msgstr "פורמט רע" - -#: mod_roster.erl:995 mod_roster_odbc.erl:1112 -msgid "Add Jabber ID" -msgstr "הוסף JID" - -#: mod_roster.erl:1094 mod_roster_odbc.erl:1211 -msgid "Roster" -msgstr "רשימה" - -#: mod_shared_roster.erl:857 mod_shared_roster.erl:899 -#: mod_shared_roster.erl:1007 mod_shared_roster_odbc.erl:886 -#: mod_shared_roster_odbc.erl:928 mod_shared_roster_odbc.erl:1036 -msgid "Shared Roster Groups" -msgstr "קבוצות רשימה משותפות" - -#: mod_shared_roster.erl:895 mod_shared_roster_odbc.erl:924 -#: web/ejabberd_web_admin.erl:1365 web/ejabberd_web_admin.erl:2461 -msgid "Add New" -msgstr "הוסף חדש" - -#: mod_shared_roster.erl:978 mod_shared_roster_odbc.erl:1007 -msgid "Name:" -msgstr "שם:" - -#: mod_shared_roster.erl:983 mod_shared_roster_odbc.erl:1012 -msgid "Description:" -msgstr "תיאור:" - -#: mod_shared_roster.erl:991 mod_shared_roster_odbc.erl:1020 -msgid "Members:" -msgstr "חברים:" - -#: mod_shared_roster.erl:999 mod_shared_roster_odbc.erl:1028 -msgid "Displayed Groups:" -msgstr "קבוצות מוצגות:" - -#: mod_shared_roster.erl:1008 mod_shared_roster_odbc.erl:1037 -msgid "Group " -msgstr "קבוצה " - -#: mod_shared_roster.erl:1017 mod_shared_roster_odbc.erl:1046 -#: web/ejabberd_web_admin.erl:840 web/ejabberd_web_admin.erl:884 -#: web/ejabberd_web_admin.erl:949 web/ejabberd_web_admin.erl:1026 -#: web/ejabberd_web_admin.erl:2017 -msgid "Submit" -msgstr "שליחה" - -#: mod_vcard.erl:165 mod_vcard_ldap.erl:238 mod_vcard_odbc.erl:129 -#, fuzzy -msgid "Erlang Jabber Server" -msgstr "שרת ג׳אבּר Erlang" - -#: mod_vcard.erl:364 mod_vcard.erl:478 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:454 -msgid "Birthday" -msgstr "יום הולדת" - -#: mod_vcard.erl:364 mod_vcard.erl:480 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:456 -msgid "City" -msgstr "עיר" - -#: mod_vcard.erl:364 mod_vcard.erl:479 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:455 -msgid "Country" -msgstr "ארץ" - -#: mod_vcard.erl:364 mod_vcard.erl:481 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:457 -msgid "Email" -msgstr "דוא״ל" - -#: mod_vcard.erl:364 mod_vcard.erl:476 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:452 -msgid "Family Name" -msgstr "שם משפחה" - -# נא למלא -# שקול לתאום -# note: matching > wanted -#: mod_vcard.erl:364 mod_vcard_odbc.erl:342 -msgid "" -"Fill in the form to search for any matching Jabber User (Add * to the end of " -"field to match substring)" -msgstr "" -"מלא את התבניות כדי לחפש עבור כל משתמש Jabber מבוקש (באפשרותך להוסיף * בסוף " -"שדה כדי להתאים אל מחרוזת-משנה)" - -#: mod_vcard.erl:364 mod_vcard.erl:473 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:449 -msgid "Full Name" -msgstr "שם מלא" - -#: mod_vcard.erl:364 mod_vcard.erl:475 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:451 -msgid "Middle Name" -msgstr "שם אמצעי" - -#: mod_vcard.erl:364 mod_vcard.erl:474 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:450 web/ejabberd_web_admin.erl:2006 -msgid "Name" -msgstr "שם" - -#: mod_vcard.erl:364 mod_vcard.erl:482 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:458 -msgid "Organization Name" -msgstr "שם ארגון" - -#: mod_vcard.erl:364 mod_vcard.erl:483 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:459 -msgid "Organization Unit" -msgstr "יחידת איגוד" - -# בקרב -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "Search users in " -msgstr "חיפוש משתמשים אצל " - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "You need an x:data capable client to search" -msgstr "עליך להשתמש בלקוח אשר מסוגל להבין x:data בכדי לחפש" - -#: mod_vcard.erl:389 mod_vcard_ldap.erl:487 mod_vcard_odbc.erl:367 -msgid "vCard User Search" -msgstr "חיפוש משתמש vCard" - -#: mod_vcard.erl:445 mod_vcard_ldap.erl:541 mod_vcard_odbc.erl:421 -msgid "ejabberd vCard module" -msgstr "מודול vCard של ejabberd" - -#: mod_vcard.erl:469 mod_vcard_ldap.erl:551 mod_vcard_odbc.erl:445 -msgid "Search Results for " -msgstr "תוצאות חיפוש עבור " - -# שקול -#: mod_vcard_ldap.erl:462 -msgid "Fill in fields to search for any matching Jabber User" -msgstr "מלא את שדות אלו כדי לחפש עבור כל משתמש Jabber מבוקש" - -#: web/ejabberd_web_admin.erl:193 web/ejabberd_web_admin.erl:203 -#: web/ejabberd_web_admin.erl:219 web/ejabberd_web_admin.erl:229 -msgid "Unauthorized" -msgstr "לא מורשה" - -#: web/ejabberd_web_admin.erl:286 web/ejabberd_web_admin.erl:303 -msgid "ejabberd Web Admin" -msgstr "מנהל רשת ejabberd" - -#: web/ejabberd_web_admin.erl:769 web/ejabberd_web_admin.erl:780 -msgid "Administration" -msgstr "הנהלה" - -#: web/ejabberd_web_admin.erl:878 web/ejabberd_web_admin.erl:982 -msgid "Raw" -msgstr "" - -#: web/ejabberd_web_admin.erl:1017 -msgid "~s access rule configuration" -msgstr "~s תצורת כללי גישה" - -# וירטואליים -#: web/ejabberd_web_admin.erl:1035 -msgid "Virtual Hosts" -msgstr "מארחים מדומים" - -#: web/ejabberd_web_admin.erl:1043 web/ejabberd_web_admin.erl:1050 -msgid "Users" -msgstr "משתמשים" - -#: web/ejabberd_web_admin.erl:1078 -msgid "Users Last Activity" -msgstr "פעילות משתמשים אחרונה" - -#: web/ejabberd_web_admin.erl:1080 -msgid "Period: " -msgstr "משך זמן: " - -#: web/ejabberd_web_admin.erl:1090 -msgid "Last month" -msgstr "חודש אחרון" - -#: web/ejabberd_web_admin.erl:1091 -msgid "Last year" -msgstr "שנה אחרונה" - -#: web/ejabberd_web_admin.erl:1092 -msgid "All activity" -msgstr "כל פעילות" - -#: web/ejabberd_web_admin.erl:1094 -msgid "Show Ordinary Table" -msgstr "הצג טבלה רגילה" - -#: web/ejabberd_web_admin.erl:1096 -msgid "Show Integral Table" -msgstr "הצג טבלה אינטגרלית" - -#: web/ejabberd_web_admin.erl:1105 web/ejabberd_web_admin.erl:1939 -msgid "Statistics" -msgstr "סטטיסטיקה" - -#: web/ejabberd_web_admin.erl:1117 -msgid "Not Found" -msgstr "לא נמצא" - -#: web/ejabberd_web_admin.erl:1134 -msgid "Node not found" -msgstr "ממסר לא נמצא" - -#: web/ejabberd_web_admin.erl:1460 -msgid "Host" -msgstr "מארח" - -#: web/ejabberd_web_admin.erl:1461 -msgid "Registered Users" -msgstr "משתמשים רשומים" - -#: web/ejabberd_web_admin.erl:1573 -msgid "Offline Messages" -msgstr "הודעות לא מקוונות" - -#: web/ejabberd_web_admin.erl:1574 web/ejabberd_web_admin.erl:1767 -msgid "Last Activity" -msgstr "פעילות אחרונה" - -#: web/ejabberd_web_admin.erl:1659 web/ejabberd_web_admin.erl:1675 -msgid "Registered Users:" -msgstr "משתמשים רשומים:" - -#: web/ejabberd_web_admin.erl:1661 web/ejabberd_web_admin.erl:1677 -#: web/ejabberd_web_admin.erl:2192 -msgid "Online Users:" -msgstr "משתמשים מקוונים:" - -#: web/ejabberd_web_admin.erl:1663 -msgid "Outgoing s2s Connections:" -msgstr "חיבורי s2s יוצאים:" - -#: web/ejabberd_web_admin.erl:1665 -msgid "Outgoing s2s Servers:" -msgstr "שרתי s2s יוצאים:" - -#: web/ejabberd_web_admin.erl:1734 web/mod_register_web.erl:175 -#: web/mod_register_web.erl:372 web/mod_register_web.erl:381 -#: web/mod_register_web.erl:411 -msgid "Change Password" -msgstr "שנה סיסמה" - -#: web/ejabberd_web_admin.erl:1758 -msgid "User " -msgstr "משתמש " - -#: web/ejabberd_web_admin.erl:1765 -msgid "Connected Resources:" -msgstr "משאבים מחוברים:" - -#: web/ejabberd_web_admin.erl:1766 web/mod_register_web.erl:227 -#: web/mod_register_web.erl:519 -msgid "Password:" -msgstr "סיסמה:" - -#: web/ejabberd_web_admin.erl:1828 -msgid "No Data" -msgstr "אין מידע" - -#: web/ejabberd_web_admin.erl:1906 -msgid "Nodes" -msgstr "ממסרים" - -#: web/ejabberd_web_admin.erl:1929 web/ejabberd_web_admin.erl:1951 -msgid "Node " -msgstr "ממסר" - -#: web/ejabberd_web_admin.erl:1938 -#, fuzzy -msgid "Listened Ports" -msgstr "פורטים מואזנים" - -#: web/ejabberd_web_admin.erl:1940 web/ejabberd_web_admin.erl:2260 -#: web/ejabberd_web_admin.erl:2448 -msgid "Update" -msgstr "עדכן" - -#: web/ejabberd_web_admin.erl:1943 web/ejabberd_web_admin.erl:2569 -msgid "Restart" -msgstr "אתחל" - -#: web/ejabberd_web_admin.erl:1945 web/ejabberd_web_admin.erl:2571 -msgid "Stop" -msgstr "הפסק" - -#: web/ejabberd_web_admin.erl:1959 -msgid "RPC Call Error" -msgstr "שגיאת קריאת RPC" - -#: web/ejabberd_web_admin.erl:2000 -msgid "Database Tables at " -msgstr "טבלאות מסד נתונים אצל " - -#: web/ejabberd_web_admin.erl:2007 -msgid "Storage Type" -msgstr "טיפוס אחסון" - -#: web/ejabberd_web_admin.erl:2008 -msgid "Elements" -msgstr "אלמנטים" - -#: web/ejabberd_web_admin.erl:2009 -msgid "Memory" -msgstr "זיכרון" - -#: web/ejabberd_web_admin.erl:2032 web/ejabberd_web_admin.erl:2137 -msgid "Error" -msgstr "שגיאה" - -#: web/ejabberd_web_admin.erl:2034 -msgid "Backup of " -msgstr "גיבוי של " - -# האינטגרלי לחוד -#: web/ejabberd_web_admin.erl:2036 -msgid "" -"Please note that these options will only backup the builtin Mnesia database. " -"If you are using the ODBC module, you also need to backup your SQL database " -"separately." -msgstr "" -"נא לשים לב כי אפשרויות אלו יגבו את מסד הנתונים המובנה Mnesia בלבד. אם הינך " -"עושה שימוש במודול ODBC, עליך גם לגבות את מסד הנתונים SQL אשר מצוי ברשותך " -"בנפרד." - -#: web/ejabberd_web_admin.erl:2041 -msgid "Store binary backup:" -msgstr "אחסן גיבוי בינארי:" - -#: web/ejabberd_web_admin.erl:2045 web/ejabberd_web_admin.erl:2052 -#: web/ejabberd_web_admin.erl:2060 web/ejabberd_web_admin.erl:2067 -#: web/ejabberd_web_admin.erl:2074 web/ejabberd_web_admin.erl:2081 -#: web/ejabberd_web_admin.erl:2088 web/ejabberd_web_admin.erl:2096 -#: web/ejabberd_web_admin.erl:2103 web/ejabberd_web_admin.erl:2110 -msgid "OK" -msgstr "אישור" - -# ללא דיחוי -#: web/ejabberd_web_admin.erl:2048 -msgid "Restore binary backup immediately:" -msgstr "שחזר גיבוי בינארי לאלתר:" - -# לאחר אתחול בא של -#: web/ejabberd_web_admin.erl:2056 -msgid "" -"Restore binary backup after next ejabberd restart (requires less memory):" -msgstr "שחזר גיבוי בינארי לאחר האתחול הבא של ejabberd (מצריך פחות זיכרון):" - -# תמליל ברור -#: web/ejabberd_web_admin.erl:2063 -msgid "Store plain text backup:" -msgstr "אחסן גיבוי טקסט גלוי (plain text):" - -#: web/ejabberd_web_admin.erl:2070 -msgid "Restore plain text backup immediately:" -msgstr "שחזר גיבוי טקסט גלוי (plain text) לאלתר:" - -#: web/ejabberd_web_admin.erl:2077 -msgid "Import users data from a PIEFXIS file (XEP-0227):" -msgstr "ייבוא מידע משתמשים מתוך קובץ PIEFXIS ‏(XEP-0227):" - -#: web/ejabberd_web_admin.erl:2084 -msgid "Export data of all users in the server to PIEFXIS files (XEP-0227):" -msgstr "" -"ייצוא מידע של כל המשתמשים אשר מצויים בשרת זה אל קבצי PIEFXIS (‫XEP-0227):" - -#: web/ejabberd_web_admin.erl:2091 -msgid "Export data of users in a host to PIEFXIS files (XEP-0227):" -msgstr "ייצוא מידע של כל המשתמשים בתוך מארח אל קבצי PIEFXIS (‫XEP-0227):" - -#: web/ejabberd_web_admin.erl:2099 -msgid "Import user data from jabberd14 spool file:" -msgstr "ייבוא נתוני משתמש מתוך קובץ סליל (spool file) של jabberd14:" - -#: web/ejabberd_web_admin.erl:2106 -msgid "Import users data from jabberd14 spool directory:" -msgstr "ייבוא נתוני משתמשים מתוך מדור סליל (spool directory) של jabberd14:" - -#: web/ejabberd_web_admin.erl:2132 -#, fuzzy -msgid "Listened Ports at " -msgstr "פורטים מואזנים אצל " - -#: web/ejabberd_web_admin.erl:2157 -msgid "Modules at " -msgstr "מודולים אצל " - -#: web/ejabberd_web_admin.erl:2183 -msgid "Statistics of ~p" -msgstr "סטטיסטיקות עבור ~p" - -#: web/ejabberd_web_admin.erl:2186 -msgid "Uptime:" -msgstr "זמן פעילות:" - -#: web/ejabberd_web_admin.erl:2189 -msgid "CPU Time:" -msgstr "זמן מחשב (CPU):" - -# זיכרון דברים (דיני חוזים) -#: web/ejabberd_web_admin.erl:2195 -msgid "Transactions Committed:" -msgstr "טרנזקציות בוצעו:" - -#: web/ejabberd_web_admin.erl:2198 -msgid "Transactions Aborted:" -msgstr "טרנזקציות בוטלו:" - -#: web/ejabberd_web_admin.erl:2201 -msgid "Transactions Restarted:" -msgstr "טרנזקציות הותחלו מחדש:" - -#: web/ejabberd_web_admin.erl:2204 -msgid "Transactions Logged:" -msgstr "טרנזקציות נרשמו:" - -#: web/ejabberd_web_admin.erl:2246 -msgid "Update " -msgstr "" - -#: web/ejabberd_web_admin.erl:2254 -msgid "Update plan" -msgstr "" - -# adjusted -#: web/ejabberd_web_admin.erl:2255 -msgid "Modified modules" -msgstr "מודולים שהותאמו" - -# תסריט עדכון -#: web/ejabberd_web_admin.erl:2256 -#, fuzzy -msgid "Update script" -msgstr "עדכן תסריט" - -#: web/ejabberd_web_admin.erl:2257 -#, fuzzy -msgid "Low level update script" -msgstr "תסריט עדכון Low level" - -#: web/ejabberd_web_admin.erl:2258 -#, fuzzy -msgid "Script check" -msgstr "תסריט בדיקה" - -#: web/ejabberd_web_admin.erl:2426 -msgid "Port" -msgstr "פורט" - -#: web/ejabberd_web_admin.erl:2427 -msgid "IP" -msgstr "‫IP" - -#: web/ejabberd_web_admin.erl:2428 -msgid "Protocol" -msgstr "פרוטוקול" - -#: web/ejabberd_web_admin.erl:2429 web/ejabberd_web_admin.erl:2556 -msgid "Module" -msgstr "מודול" - -#: web/ejabberd_web_admin.erl:2430 web/ejabberd_web_admin.erl:2557 -msgid "Options" -msgstr "אפשרויות" - -#: web/ejabberd_web_admin.erl:2450 -msgid "Delete" -msgstr "מחק" - -#: web/ejabberd_web_admin.erl:2579 -msgid "Start" -msgstr "התחל" - -#: web/mod_register_web.erl:104 -msgid "Your Jabber account was successfully created." -msgstr "חשבון Jabber נוצר בהצלחה." - -#: web/mod_register_web.erl:107 -msgid "There was an error creating the account: " -msgstr "אירעה שגיאה ביצירת החשבון: " - -#: web/mod_register_web.erl:115 -msgid "Your Jabber account was successfully deleted." -msgstr "חשבון Jabber נמחק בהצלחה." - -#: web/mod_register_web.erl:118 -msgid "There was an error deleting the account: " -msgstr "אירעה שגיאה במחיקת החשבון: " - -#: web/mod_register_web.erl:128 -msgid "The password of your Jabber account was successfully changed." -msgstr "סיסמת חשבון Jabber שונתה בהצלחה." - -#: web/mod_register_web.erl:131 -msgid "There was an error changing the password: " -msgstr "אירעה שגיאה בשינוי הסיסמה: " - -#: web/mod_register_web.erl:163 web/mod_register_web.erl:172 -msgid "Jabber Account Registration" -msgstr "רישום חשבון Jabber" - -# Why masculine form matters. -#: web/mod_register_web.erl:174 web/mod_register_web.erl:192 -#: web/mod_register_web.erl:201 -msgid "Register a Jabber account" -msgstr "רשום חשבון Jabber" - -#: web/mod_register_web.erl:176 web/mod_register_web.erl:493 -#: web/mod_register_web.erl:502 -msgid "Unregister a Jabber account" -msgstr "בטל רישום חשבון Jabber" - -#: web/mod_register_web.erl:203 -msgid "" -"This page allows to create a Jabber account in this Jabber server. Your JID " -"(Jabber IDentifier) will be of the form: username@server. Please read " -"carefully the instructions to fill correctly the fields." -msgstr "" -"עמוד זה מתיר ליצור חשבון Jabber בשרת Jabber זה. כתובת JID ‏(Jabber " -"IDentifier) תגובש באופן של: username@server. נא לקרוא בזהירות את ההוראות " -"למילוי השדות באופן נכון." - -#: web/mod_register_web.erl:212 web/mod_register_web.erl:386 -#: web/mod_register_web.erl:509 -msgid "Username:" -msgstr "שם משתמש:" - -# כמו -#: web/mod_register_web.erl:217 -#, fuzzy -msgid "This is case insensitive: macbeth is the same that MacBeth and Macbeth." -msgstr "This is case insensitive: macbeth הינה זהה כשם MacBeth וגם Macbeth." - -#: web/mod_register_web.erl:218 -msgid "Characters not allowed:" -msgstr "תווים לא מורשים:" - -#: web/mod_register_web.erl:222 web/mod_register_web.erl:391 -#: web/mod_register_web.erl:514 -msgid "Server:" -msgstr "שרת:" - -# אל נא לומר -#: web/mod_register_web.erl:232 -msgid "" -"Don't tell your password to anybody, not even the administrators of the " -"Jabber server." -msgstr "אל תגלה את הסיסמה שלך לאף אחד, אפילו לא למנהלים של שרת Jabber" - -#: web/mod_register_web.erl:234 -msgid "You can later change your password using a Jabber client." -msgstr "ביכולתך לשנות את הסיסמה שלך מאוחר יותר באמצעות לקוח Jabber." - -# בוטח -# trust that your -#: web/mod_register_web.erl:235 -msgid "" -"Some Jabber clients can store your password in your computer. Use that " -"feature only if you trust your computer is safe." -msgstr "" -"לקוחות Jabber מסוימים יכולים לאחסן את הסיסמה שלך על המחשב שלך. השתמש בתכונה " -"זו רק אם אתה סמוך כי המחשב שלך הינו מוגן." - -# תישכח -#: web/mod_register_web.erl:237 -msgid "" -"Memorize your password, or write it in a paper placed in a safe place. In " -"Jabber there isn't an automated way to recover your password if you forget " -"it." -msgstr "" -"שנן את הסיסמה שלך, או רשום אותה בנייר שמור במקום בטוח. אצל Jabber אין דרך " -"אוטומטית לשחזר את הסיסמה שלך במידה וזו תישמט מתוך זיכרונך." - -#: web/mod_register_web.erl:242 web/mod_register_web.erl:406 -msgid "Password Verification:" -msgstr "אימות סיסמה:" - -# רשום -#: web/mod_register_web.erl:250 -msgid "Register" -msgstr "הרשם" - -#: web/mod_register_web.erl:396 -msgid "Old Password:" -msgstr "סיסמה ישנה:" - -#: web/mod_register_web.erl:401 -msgid "New Password:" -msgstr "סיסמה חדשה:" - -#: web/mod_register_web.erl:504 -msgid "This page allows to unregister a Jabber account in this Jabber server." -msgstr "עמוד זה מתיר לך לבטל רישום של חשבון Jabber בשרת Jabber זה." - -#: web/mod_register_web.erl:524 -msgid "Unregister" -msgstr "בטל רישום" - -#~ msgid "Rooms" -#~ msgstr "חדרים" - -#~ msgid "Multicast" -#~ msgstr "שידור מרובב" - -#~ msgid "ejabberd Multicast service" -#~ msgstr "שירות שידור מרובב של ejabberd" - -#~ msgid "Notify owners about new subscribers and unsubscribers" -#~ msgstr "הודע בעלים אודות מנויים חדשים ומנויים שביטלו מנוי" - -#~ msgid "" -#~ "The type of node data, usually specified by the namespace of the payload " -#~ "(if any)" -#~ msgstr "סוג מידע ממסר, לרוב מצוין לפי מרחב־שמות של מטען הייעוד (אם בכלל)" - -#~ msgid "Group ID:" -#~ msgstr "קבוצה (GID):" - -#~ msgid "vCard" -#~ msgstr "‫vCard" - -#~ msgid "vCard Photo:" -#~ msgstr "תצלום vCard:" - -#~ msgid "Remove vCard" -#~ msgstr "הסרת vCard" - -#~ msgid "vCard size (characters):" -#~ msgstr "גודל vCard (תווים):" diff --git a/priv/msgs/hu.msg b/priv/msgs/hu.msg new file mode 100644 index 000000000..32e3174d0 --- /dev/null +++ b/priv/msgs/hu.msg @@ -0,0 +1,415 @@ +%% Generated automatically +%% DO NOT EDIT: run `make translations` instead +%% To improve translations please read: +%% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ + +{" (Add * to the end of field to match substring)"," (adjon * karaktert a mező végéhez a részkarakterláncra illesztéshez)"}. +{" has set the subject to: "," beállította a tárgyat erre: "}. +{"A password is required to enter this room","Jelszó szükséges a szobába történő belépéshez"}. +{"Accept","Elfogadás"}. +{"Access denied by service policy","Hozzáférés megtagadva a szolgáltatási irányelv miatt"}. +{"Account doesn't exist","A fiók nem létezik"}. +{"Action on user","Művelet a felhasználón"}. +{"Add User","Felhasználó hozzáadása"}. +{"Administration of ","Adminisztrációja ennek: "}. +{"Administration","Adminisztráció"}. +{"Administrator privileges required","Adminisztrátori jogosultságok szükségesek"}. +{"All activity","Összes tevékenység"}. +{"All Users","Összes felhasználó"}. +{"Allow users to change the subject","Lehetővé tenni a felhasználóknak a tárgy megváltoztatását"}. +{"Allow users to query other users","Lehetővé tenni a felhasználóknak más felhasználók lekérdezését"}. +{"Allow users to send invites","Lehetővé tenni a felhasználóknak meghívók küldését"}. +{"Allow users to send private messages","Lehetővé tenni a felhasználóknak személyes üzenetek küldését"}. +{"Allow visitors to change nickname","Lehetővé tenni a látogatóknak a becenév megváltoztatását"}. +{"Allow visitors to send private messages to","Lehetővé tenni a látogatóknak személyes üzenetek küldését"}. +{"Allow visitors to send status text in presence updates","Lehetővé tenni a látogatóknak állapotszöveg küldését a jelenlét frissítéseiben"}. +{"Announcements","Közlemények"}. +{"April","április"}. +{"Attribute 'channel' is required for this request","A „channel” attribútum kötelező ennél a kérésnél"}. +{"Attribute 'id' is mandatory for MIX messages","Az „id” attribútum kötelező a MIX üzeneteknél"}. +{"Attribute 'jid' is not allowed here","A „jid” attribútum itt nem engedélyezett"}. +{"Attribute 'node' is not allowed here","A „node” attribútum itt nem engedélyezett"}. +{"August","augusztus"}. +{"Automatic node creation is not enabled","Automatikus csomópont-létrehozás nincs engedélyezve"}. +{"Backup Management","Biztonságimentés-kezelés"}. +{"Backup of ~p","~p biztonsági mentése"}. +{"Backup to File at ","Biztonsági mentés fájlba ekkor: "}. +{"Backup","Biztonsági mentés"}. +{"Bad format","Hibás formátum"}. +{"Birthday","Születésnap"}. +{"Both the username and the resource are required","A felhasználónév és az erőforrás is szükséges"}. +{"Bytestream already activated","A bájtfolyam már be van kapcsolva"}. +{"Cannot remove active list","Nem lehet eltávolítani az aktív listát"}. +{"Cannot remove default list","Nem lehet eltávolítani az alapértelmezett listát"}. +{"Change Password","Jelszó megváltoztatása"}. +{"Change User Password","Felhasználó jelszavának megváltoztatása"}. +{"Changing password is not allowed","A jelszó megváltoztatása nem engedélyezett"}. +{"Changing role/affiliation is not allowed","A szerep vagy a hovatartozás megváltoztatása nem engedélyezett"}. +{"Channel already exists","A csatorna már létezik"}. +{"Channel does not exist","A csatorna nem létezik"}. +{"Channels","Csatornák"}. +{"Characters not allowed:","Nem engedélyezett karakterek:"}. +{"Chatroom configuration modified","Csevegőszoba beállítása módosítva"}. +{"Chatroom is created","Csevegőszoba létrehozva"}. +{"Chatroom is destroyed","Csevegőszoba megszüntetve"}. +{"Chatroom is started","Csevegőszoba elindítva"}. +{"Chatroom is stopped","Csevegőszoba leállítva"}. +{"Chatrooms","Csevegőszobák"}. +{"Choose a username and password to register with this server","Válasszon felhasználónevet és jelszót a kiszolgálóra történő regisztráláshoz"}. +{"Choose storage type of tables","Táblák tárolótípusának kiválasztása"}. +{"Choose whether to approve this entity's subscription.","Annak kiválasztása, hogy elfogadja-e ennek a bejegyzésnek a feliratkozását."}. +{"City","Település"}. +{"Client acknowledged more stanzas than sent by server","Az ügyfél több stanzát nyugtázott, mint amennyit a kiszolgáló küldött"}. +{"Commands","Parancsok"}. +{"Conference room does not exist","A konferenciaszoba nem létezik"}. +{"Configuration of room ~s","A(z) ~s szoba beállítása"}. +{"Configuration","Beállítás"}. +{"Country","Ország"}. +{"Database failure","Adatbázishiba"}. +{"Database Tables Configuration at ","Adatbázistáblák beállítása itt: "}. +{"Database","Adatbázis"}. +{"December","december"}. +{"Default users as participants","Alapértelmezett felhasználók mint résztvevők"}. +{"Delete message of the day on all hosts","Napi üzenet törlése az összes gépen"}. +{"Delete message of the day","Napi üzenet törlése"}. +{"Delete User","Felhasználó törlése"}. +{"Disc only copy","Csak lemez másolása"}. +{"Dump Backup to Text File at ","Biztonsági mentés kiírása szövegfájlba itt: "}. +{"Dump to Text File","Kiírás szövegfájlba"}. +{"Edit Properties","Tulajdonságok szerkesztése"}. +{"Either approve or decline the voice request.","Hagyja jóvá vagy utasítsa el a hangkérelmet."}. +{"ejabberd HTTP Upload service","ejabberd HTTP feltöltési szolgáltatás"}. +{"ejabberd MUC module","ejabberd MUC modul"}. +{"ejabberd Multicast service","ejabberd üzenetszórási szolgáltatás"}. +{"ejabberd Publish-Subscribe module","ejabberd publikálás-feliratkozás modul"}. +{"ejabberd SOCKS5 Bytestreams module","ejabberd SOCKS5 bájtfolyam modul"}. +{"ejabberd vCard module","ejabberd vCard modul"}. +{"ejabberd Web Admin","ejabberd webes adminisztráció"}. +{"ejabberd","ejabberd"}. +{"Email","E-mail"}. +{"Enable logging","Naplózás engedélyezése"}. +{"Enabling push without 'node' attribute is not supported","A „node” attribútum nélküli felküldés engedélyezése nem támogatott"}. +{"End User Session","Felhasználói munkamenet befejezése"}. +{"Enter nickname you want to register","Adja meg a becenevet, amelyet regisztrálni szeretne"}. +{"Enter path to backup file","Adja meg a biztonsági mentés fájl útvonalát"}. +{"Enter path to jabberd14 spool dir","Adja meg a jabberd14 tárolókönyvtár útvonalát"}. +{"Enter path to jabberd14 spool file","Adja meg a jabberd14 tárolófájl útvonalát"}. +{"Enter path to text file","Adja meg a szövegfájl útvonalát"}. +{"Enter the text you see","Írja be a látott szöveget"}. +{"Export all tables as SQL queries to a file:","Összes tábla exportálása SQL-lekérdezésekként egy fájlba:"}. +{"Export data of all users in the server to PIEFXIS files (XEP-0227):","A kiszolgálón lévő összes felhasználó adatainak exportálása PIEFXIS-fájlokba (XEP-0227):"}. +{"Export data of users in a host to PIEFXIS files (XEP-0227):","Egy gépen lévő felhasználók adatainak exportálása PIEFXIS-fájlokba (XEP-0227):"}. +{"External component failure","Külső összetevő hiba"}. +{"External component timeout","Külső összetevő időtúllépés"}. +{"Failed to activate bytestream","Nem sikerült bekapcsolni a bájtfolyamot"}. +{"Failed to extract JID from your voice request approval","Nem sikerült kinyerni a Jabber-azonosítót a hangkérelem jóváhagyásból"}. +{"Failed to map delegated namespace to external component","Nem sikerült leképezni a delegált névteret külső összetevőre"}. +{"Failed to parse HTTP response","Nem sikerült feldolgozni a HTTP választ"}. +{"Family Name","Családnév"}. +{"February","február"}. +{"File larger than ~w bytes","A fájl nagyobb ~w bájtnál"}. +{"Friday","péntek"}. +{"From ~ts","Feladó: ~ts"}. +{"Full Name","Teljes név"}. +{"Get Number of Online Users","Elérhető felhasználók számának lekérése"}. +{"Get Number of Registered Users","Regisztrált felhasználók számának lekérése"}. +{"Get Pending","Függőben lévő lekérése"}. +{"Get User Last Login Time","Felhasználó legutolsó bejelentkezési idejének lekérése"}. +{"Get User Statistics","Felhasználói statisztikák lekérése"}. +{"Given Name","Keresztnév"}. +{"has been banned","ki lett tiltva"}. +{"has been kicked because of a system shutdown","ki lett rúgva egy rendszerleállítás miatt"}. +{"has been kicked because of an affiliation change","ki lett rúgva egy hovatartozás megváltozása miatt"}. +{"has been kicked because the room has been changed to members-only","ki lett rúgva, mert a szobát megváltoztatták csak tagok részére"}. +{"has been kicked","ki lett rúgva"}. +{"Host unknown","Gép ismeretlen"}. +{"HTTP File Upload","HTTP fájlfeltöltés"}. +{"Idle connection","Tétlen kapcsolat"}. +{"If you don't see the CAPTCHA image here, visit the web page.","Ha nem látja itt a CAPTCHA képet, akkor látogassa meg a weboldalt."}. +{"Import Directory","Könyvtár importálása"}. +{"Import File","Fájl importálása"}. +{"Import user data from jabberd14 spool file:","Felhasználóadatok importálása jabberd14 tárolófájlból:"}. +{"Import User from File at ","Felhasználó importálása fájlból itt: "}. +{"Import users data from a PIEFXIS file (XEP-0227):","Felhasználók adatainak importálása PIEFXIS-fájlból (XEP-0227):"}. +{"Import users data from jabberd14 spool directory:","Felhasználók adatainak importálása jabberd14 tárolókönyvtárból:"}. +{"Import Users from Dir at ","Felhasználók importálása könyvtárból itt: "}. +{"Import Users From jabberd14 Spool Files","Felhasználók importálása jabberd14 tárolófájlokból"}. +{"Improper domain part of 'from' attribute","A „from” attribútum tartományrésze helytelen"}. +{"Improper message type","Helytelen üzenettípus"}. +{"Incorrect CAPTCHA submit","Hibás CAPTCHA beküldés"}. +{"Incorrect data form","Hibás adatűrlap"}. +{"Incorrect password","Hibás jelszó"}. +{"Incorrect value of 'action' attribute","Az „action” attribútum értéke hibás"}. +{"Incorrect value of 'action' in data form","Az „action” értéke hibás az adatűrlapon"}. +{"Incorrect value of 'path' in data form","A „path” értéke hibás az adatűrlapon"}. +{"Insufficient privilege","Nincs elegendő jogosultság"}. +{"Internal server error","Belső kiszolgálóhiba"}. +{"Invalid 'from' attribute in forwarded message","Érvénytelen „from” attribútum a továbbított üzenetben"}. +{"Invalid node name","Érvénytelen csomópontnév"}. +{"Invalid 'previd' value","Érvénytelen „previd” érték"}. +{"Invitations are not allowed in this conference","Meghívások nem engedélyezettek ebben a konferenciában"}. +{"IP addresses","IP-címek"}. +{"is now known as","mostantól úgy ismert mint"}. +{"It is not allowed to send error messages to the room. The participant (~s) has sent an error message (~s) and got kicked from the room","Nem engedélyezett hibaüzeneteket küldeni a szobába. A résztvevő (~s) hibaüzenetet (~s) küldött, és ki lett rúgva a szobából"}. +{"It is not allowed to send private messages of type \"groupchat\"","Nem engedélyezett „groupchat” típusú személyes üzeneteket küldeni"}. +{"It is not allowed to send private messages to the conference","Nem engedélyezett személyes üzeneteket küldeni a konferenciába"}. +{"Jabber ID","Jabber-azonosító"}. +{"January","január"}. +{"JID normalization denied by service policy","A Jabber-azonosító normalizálása megtagadva a szolgáltatási irányelv miatt"}. +{"JID normalization failed","A Jabber-azonosító normalizálása nem sikerült"}. +{"joins the room","belépett a szobába"}. +{"July","július"}. +{"June","június"}. +{"Last Activity","Utolsó tevékenység"}. +{"Last login","Utolsó belépés"}. +{"Last month","Múlt hónap"}. +{"Last year","Múlt év"}. +{"leaves the room","elhagyta a szobát"}. +{"Make participants list public","Résztvevőlista nyilvánossá tétele"}. +{"Make room CAPTCHA protected","Szoba CAPTCHA-védetté tétele"}. +{"Make room members-only","Szoba beállítása csak tagoknak"}. +{"Make room moderated","Szoba moderálttá tétele"}. +{"Make room password protected","Szoba jelszóval védetté tétele"}. +{"Make room persistent","Szoba állandóvá tétele"}. +{"Make room public searchable","Szoba nyilvánosan kereshetővé tétele"}. +{"Malformed username","Helytelenül formázott felhasználónév"}. +{"MAM preference modification denied by service policy","MAM beállítások módosítása megtagadva a szolgáltatási irányelv miatt"}. +{"March","március"}. +{"Maximum Number of Occupants","Résztvevők legnagyobb száma"}. +{"May","május"}. +{"Membership is required to enter this room","Tagság szükséges a szobába lépéshez"}. +{"Message body","Üzenettörzs"}. +{"Message not found in forwarded payload","Nem található üzenet a továbbított adatokban"}. +{"Messages from strangers are rejected","Idegenektől származó üzenetek vissza vannak utasítva"}. +{"Middle Name","Középső név"}. +{"Moderator privileges required","Moderátori jogosultságok szükségesek"}. +{"Module failed to handle the query","A modul nem tudta kezelni a lekérdezést"}. +{"Monday","hétfő"}. +{"Multicast","Csoportcímzés"}. +{"Multi-User Chat","Többfelhasználós csevegés"}. +{"Name","Név"}. +{"Neither 'jid' nor 'nick' attribute found","Sem a „jid”, sem a „nick” attribútum nem található"}. +{"Neither 'role' nor 'affiliation' attribute found","Sem a „role”, sem az „affiliation” attribútum nem található"}. +{"Never","Soha"}. +{"New Password:","Új jelszó:"}. +{"Nickname can't be empty","A becenév nem lehet üres"}. +{"Nickname Registration at ","Becenév regisztrációja itt: "}. +{"Nickname","Becenév"}. +{"No address elements found","Nem találhatók cím elemek"}. +{"No addresses element found","Nem található címek elem"}. +{"No 'affiliation' attribute found","Nem található „affiliation” attribútum"}. +{"No available resource found","Nem található elérhető erőforrás"}. +{"No body provided for announce message","Nincs törzs megadva a közleményüzenethez"}. +{"No child elements found","Nem találhatók gyermekelemek"}. +{"No data form found","Nem található adatűrlap"}. +{"No Data","Nincs adat"}. +{"No features available","Nincsenek elérhető funkciók"}. +{"No element found","Nem található elem"}. +{"No hook has processed this command","Egyetlen horog sem dolgozta fel ezt a parancsot"}. +{"No info about last activity found","Nem található információ a legutolsó tevékenységgel kapcsolatban"}. +{"No 'item' element found","Nem található „item” elem"}. +{"No items found in this query","Nem találhatók elemek ebben a lekérdezésben"}. +{"No limit","Nincs korlát"}. +{"No module is handling this query","Egyetlen modul sem kezeli ezt a lekérdezést"}. +{"No node specified","Nincs csomópont megadva"}. +{"No 'password' found in data form","Nem található „password” az adatűrlapon"}. +{"No 'password' found in this query","Nem található „password” ebben a lekérdezésben"}. +{"No 'path' found in data form","Nem található „path” az adatűrlapon"}. +{"No pending subscriptions found","Nem találhatók függőben lévő feliratkozások"}. +{"No privacy list with this name found","Nem található ilyen nevű adatvédelmi lista"}. +{"No private data found in this query","Nem található személyes adat ebben a lekérdezésben"}. +{"No running node found","Nem található futó csomópont"}. +{"No services available","Nincsenek elérhető szolgáltatások"}. +{"No statistics found for this item","Nem találhatók statisztikák ehhez az elemhez"}. +{"No 'to' attribute found in the invitation","Nem található „to” attribútum a meghívásban"}. +{"Node already exists","A csomópont már létezik"}. +{"Node index not found","A csomópontindex nem található"}. +{"Node not found","A csomópont nem található"}. +{"Node ~p","~p csomópont"}. +{"Nodeprep has failed","A csomópont-előkészítés sikertelen"}. +{"Nodes","Csomópontok"}. +{"None","Nincs"}. +{"Not allowed","Nem engedélyezett"}. +{"Not Found","Nem található"}. +{"Not subscribed","Nincs feliratkozva"}. +{"November","november"}. +{"Number of online users","Elérhető felhasználók száma"}. +{"Number of registered users","Regisztrált felhasználók száma"}. +{"October","október"}. +{"OK","Rendben"}. +{"Old Password:","Régi jelszó:"}. +{"Online Users","Elérhető felhasználók"}. +{"Online","Elérhető"}. +{"Only or tags are allowed","Csak az vagy címkék engedélyezettek"}. +{"Only element is allowed in this query","Csak a elem engedélyezett ebben a lekérdezésben"}. +{"Only members may query archives of this room","Csak tagok kérdezhetik le ennek a szobának az archívumát"}. +{"Only moderators and participants are allowed to change the subject in this room","Csak moderátoroknak és résztvevőknek engedélyezett megváltoztatni a tárgyat ebben a szobában"}. +{"Only moderators are allowed to change the subject in this room","Csak moderátoroknak engedélyezett megváltoztatni a tárgyat ebben a szobában"}. +{"Only moderators can approve voice requests","Csak moderátorok hagyhatnak jóvá hangkérelmeket"}. +{"Only occupants are allowed to send messages to the conference","Csak résztvevőknek engedélyezett üzeneteket küldeni a konferenciába"}. +{"Only occupants are allowed to send queries to the conference","Csak résztvevőknek engedélyezett lekérdezéseket küldeni a konferenciába"}. +{"Only service administrators are allowed to send service messages","Csak szolgáltatás-adminisztrátoroknak engedélyezett szolgáltatási üzeneteket küldeni"}. +{"Organization Name","Szervezet neve"}. +{"Organization Unit","Szervezeti egység"}. +{"Outgoing s2s Connections","Kimenő s2s kapcsolatok"}. +{"Owner privileges required","Tulajdonosi jogosultságok szükségesek"}. +{"Packet relay is denied by service policy","Csomagátjátszás megtagadva a szolgáltatási irányelv miatt"}. +{"Password Verification","Jelszó ellenőrzése"}. +{"Password Verification:","Jelszó ellenőrzése:"}. +{"Password","Jelszó"}. +{"Password:","Jelszó:"}. +{"Path to Dir","Útvonal a könyvtárhoz"}. +{"Path to File","Útvonal a fájlhoz"}. +{"Period: ","Időszak: "}. +{"Ping query is incorrect","A lekérdezés pingelése hibás"}. +{"Ping","Ping"}. +{"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Ne feledje, hogy ezek a beállítások csak a beépített Mnesia adatbázisról készítenek biztonsági mentést. Ha az ODBC modult használja, akkor az SQL adatbázisról is különálló biztonsági mentést kell készítenie."}. +{"Please, wait for a while before sending new voice request","Várjon egy kicsit az új hangkérelem küldése előtt"}. +{"Pong","Pong"}. +{"Previous session not found","Az előző munkamenet nem található"}. +{"Previous session PID has been killed","Az előző munkamenet folyamat-azonosítója ki lett lőve"}. +{"Previous session PID has exited","Az előző munkamenet folyamat-azonosítója kilépett"}. +{"Previous session PID is dead","Az előző munkamenet folyamat-azonosítója halott"}. +{"Previous session timed out","Az előző munkamenet túllépte az időkorlátot"}. +{"private, ","személyes, "}. +{"Publish-Subscribe","Publikálás-feliratkozás"}. +{"PubSub subscriber request","Publikálás-feliratkozás feliratkozási kérelem"}. +{"Push record not found","Leküldési rekord nem található"}. +{"Queries to the conference members are not allowed in this room","A konferenciatagok lekérdezései nem engedélyezettek ebben a szobában"}. +{"Query to another users is forbidden","Egy másik felhasználó lekérdezése tiltva van"}. +{"RAM and disc copy","RAM és lemezmásolás"}. +{"RAM copy","RAM másolás"}. +{"Really delete message of the day?","Valóban törli a napi üzenetet?"}. +{"Recipient is not in the conference room","A címzett nincs a konferenciaszobában"}. +{"Register","Regisztráció"}. +{"Remote copy","Távoli másolás"}. +{"Remove User","Felhasználó eltávolítása"}. +{"Replaced by new connection","Kicserélve egy új kapcsolattal"}. +{"Request has timed out","A kérés túllépte az időkorlátot"}. +{"Request is ignored","A kérés mellőzve lett"}. +{"Resources","Erőforrások"}. +{"Restart Service","Szolgáltatás újraindítása"}. +{"Restore Backup from File at ","Biztonsági mentés visszaállítása fájlból itt: "}. +{"Restore binary backup after next ejabberd restart (requires less memory):","Bináris biztonsági mentés visszaállítása az ejabberd következő újraindítása után (kevesebb memóriát igényel):"}. +{"Restore binary backup immediately:","Bináris biztonsági mentés visszaállítása azonnal:"}. +{"Restore plain text backup immediately:","Egyszerű szöveges biztonsági mentés visszaállítása azonnal:"}. +{"Restore","Visszaállítás"}. +{"Room Configuration","Szoba beállítása"}. +{"Room creation is denied by service policy","Szobalétrehozás megtagadva a szolgáltatási irányelv miatt"}. +{"Room description","Szoba leírása"}. +{"Room Occupants","Szoba résztvevői"}. +{"Room terminates","Szoba megszűnik"}. +{"Room title","Szoba címe"}. +{"Roster size","Névsor mérete"}. +{"Running Nodes","Futó csomópontok"}. +{"Saturday","szombat"}. +{"Search Results for ","Keresési eredménye ennek: "}. +{"Search users in ","Felhasználók keresése ebben: "}. +{"Send announcement to all online users on all hosts","Közlemény küldése az összes elérhető felhasználónak az összes gépen"}. +{"Send announcement to all online users","Közlemény küldése az összes elérhető felhasználónak"}. +{"Send announcement to all users on all hosts","Közlemény küldése az összes felhasználónak az összes gépen"}. +{"Send announcement to all users","Közlemény küldése az összes felhasználónak"}. +{"September","szeptember"}. +{"Server:","Kiszolgáló:"}. +{"Session state copying timed out","A munkamenet állapotának másolása túllépte az időkorlátot"}. +{"Set message of the day and send to online users","Napi üzenet beállítása és küldés az elérhető felhasználóknak"}. +{"Set message of the day on all hosts and send to online users","Napi üzenet beállítása az összes gépen és küldés az elérhető felhasználóknak"}. +{"Shared Roster Groups","Megosztott névsorcsoportok"}. +{"Show Integral Table","Integráltáblázat megjelenítése"}. +{"Show Ordinary Table","Szokásos táblázat megjelenítése"}. +{"Shut Down Service","Szolgáltatás leállítása"}. +{"SOCKS5 Bytestreams","SOCKS5 bájtfolyamok"}. +{"Stopped Nodes","Leállított csomópontok"}. +{"Store binary backup:","Bináris biztonsági mentés tárolása:"}. +{"Store plain text backup:","Egyszerű szöveges biztonsági mentés tárolása:"}. +{"Stream management is already enabled","A folyamkezelés már engedélyezve van"}. +{"Stream management is not enabled","A folyamkezelés nincs engedélyezve"}. +{"Subject","Tárgy"}. +{"Submitted","Elküldve"}. +{"Subscriptions are not allowed","Feliratkozások nem engedélyezettek"}. +{"Sunday","vasárnap"}. +{"That nickname is already in use by another occupant","Ezt a becenevet már használja egy másik résztvevő"}. +{"That nickname is registered by another person","Ezt a becenevet egy másik személy regisztrálta"}. +{"The account already exists","A fiók már létezik"}. +{"The CAPTCHA is valid.","A CAPTCHA érvényes."}. +{"The CAPTCHA verification has failed","A CAPTCHA ellenőrzése nem sikerült"}. +{"The captcha you entered is wrong","A beírt CAPTCHA hibás"}. +{"The feature requested is not supported by the conference","A kért funkciót nem támogatja a konferencia"}. +{"The password contains unacceptable characters","A jelszó elfogadhatatlan karaktereket tartalmaz"}. +{"The password is too weak","A jelszó túl gyenge"}. +{"the password is","a jelszó"}. +{"The password was not changed","A jelszó nem lett megváltoztatva"}. +{"The passwords are different","A jelszavak különböznek"}. +{"The query is only allowed from local users","A lekérdezés csak helyi felhasználóktól engedélyezett"}. +{"The query must not contain elements","A lekérdezés nem tartalmazhat elemeket"}. +{"The stanza MUST contain only one element, one element, or one element","A stanzának csak egyetlen elemet, egyetlen elemet vagy egyetlen elemet KELL tartalmaznia"}. +{"There was an error creating the account: ","Hiba történt a fiók létrehozásakor: "}. +{"There was an error deleting the account: ","Hiba történt a fiók törlésekor: "}. +{"This room is not anonymous","Ez a szoba nem névtelen"}. +{"This service can not process the address: ~s","Ez a szolgáltatás nem tudja feldolgozni a címet: ~s"}. +{"Thursday","csütörtök"}. +{"Time delay","Időkésleltetés"}. +{"Timed out waiting for stream resumption","Időtúllépés a folyam újrakezdésére várakozásnál"}. +{"To register, visit ~s","Regisztráláshoz látogassa meg ezt az oldalt: ~s"}. +{"To ~ts","Címzett: ~ts"}. +{"Token TTL","Token élettartama"}. +{"Too many active bytestreams","Túl sok aktív bájtfolyam"}. +{"Too many CAPTCHA requests","Túl sok CAPTCHA kérés"}. +{"Too many child elements","Túl sok gyermekelem"}. +{"Too many elements","Túl sok elem"}. +{"Too many elements","Túl sok elem"}. +{"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","Túl sok (~p) sikertelen hitelesítés erről az IP-címről (~s) A cím ~s-kor lesz feloldva UTC szerint"}. +{"Too many receiver fields were specified","Túl sok fogadómező lett meghatározva"}. +{"Too many unacked stanzas","Túl sok nyugtázatlan stanza"}. +{"Too many users in this conference","Túl sok felhasználó ebben a konferenciában"}. +{"Traffic rate limit is exceeded","Forgalom sebességkorlátja elérve"}. +{"~ts's Offline Messages Queue","~ts kapcsolat nélküli üzeneteinek tárolója"}. +{"Tuesday","kedd"}. +{"Unable to generate a CAPTCHA","Nem lehet előállítani CAPTCHA-t"}. +{"Unable to register route on existing local domain","Nem lehet útvonalat regisztrálni egy meglévő helyi tartományon"}. +{"Unauthorized","Nem engedélyezett"}. +{"Unexpected action","Váratlan művelet"}. +{"Unexpected error condition: ~p","Váratlan hibafeltétel: ~p"}. +{"Unregister","Regisztráció törlése"}. +{"Unsupported element","Nem támogatott elem"}. +{"Unsupported version","Nem támogatott verzió"}. +{"Update message of the day (don't send)","Napi üzenet frissítése (ne küldje el)"}. +{"Update message of the day on all hosts (don't send)","Napi üzenet frissítése az összes gépen (ne küldje el)"}. +{"User already exists","A felhasználó már létezik"}. +{"User (jid)","Felhasználó (JID)"}. +{"User Management","Felhasználó-kezelés"}. +{"User removed","Felhasználó eltávolítva"}. +{"User session not found","Felhasználói munkamenet nem található"}. +{"User session terminated","Felhasználói munkamenet befejeződött"}. +{"User ~ts","~ts felhasználó"}. +{"User","Felhasználó"}. +{"Username:","Felhasználónév:"}. +{"Users are not allowed to register accounts so quickly","A felhasználóknak nem engedélyezett fiókokat regisztrálni ilyen gyorsan"}. +{"Users Last Activity","Felhasználók utolsó tevékenysége"}. +{"Users","Felhasználók"}. +{"Value 'get' of 'type' attribute is not allowed","A „type” attribútum „get” értéke nem engedélyezett"}. +{"Value of '~s' should be boolean","A(z) „~s” értéke csak logikai lehet"}. +{"Value of '~s' should be datetime string","A(z) „~s” értéke csak dátum és idő karakterlánc lehet"}. +{"Value of '~s' should be integer","A(z) „~s” értéke csak egész szám lehet"}. +{"Value 'set' of 'type' attribute is not allowed","A „type” attribútum „set” értéke nem engedélyezett"}. +{"vCard User Search","vCard felhasználó-keresés"}. +{"Virtual Hosts","Virtuális gépek"}. +{"Visitors are not allowed to change their nicknames in this room","A látogatóknak nem engedélyezett megváltoztatni a beceneveiket ebben a szobában"}. +{"Visitors are not allowed to send messages to all occupants","A látogatóknak nem engedélyezett üzeneteket küldeni az összes résztvevőnek"}. +{"Voice request","Hangkérelem"}. +{"Voice requests are disabled in this conference","A hangkérelmek le vannak tiltva ebben a konferenciában"}. +{"Wednesday","szerda"}. +{"Wrong parameters in the web formulary","Hibás paraméterek a webes modelldokumentumban"}. +{"Wrong xmlns","Hibás xmlns"}. +{"You are being removed from the room because of a system shutdown","El lett távolítva a szobából egy rendszerleállítás miatt"}. +{"You are not joined to the channel","Nincs csatlakozva a csatornához"}. +{"You have been banned from this room","Ki lett tiltva ebből a szobából"}. +{"You have joined too many conferences","Túl sok konferenciához csatlakozott"}. +{"You must fill in field \"Nickname\" in the form","Ki kell töltenie a „becenév” mezőt az űrlapon"}. +{"You need a client that supports x:data and CAPTCHA to register","Olyan programra van szüksége, amelynek x:data és CAPTCHA támogatása van a regisztráláshoz"}. +{"You need a client that supports x:data to register the nickname","Olyan programra van szüksége, amelynek x:data támogatása van a becenév regisztráláshoz"}. +{"You need an x:data capable client to search","Egy x:data támogatású programra van szüksége a kereséshez"}. +{"Your active privacy list has denied the routing of this stanza.","Az aktív adatvédelmi listája megtagadta ennek a stanzának az útválasztását."}. +{"Your contact offline message queue is full. The message has been discarded.","A partnere kapcsolat nélküli üzenettárolója megtelt. Az üzenet el lett dobva."}. +{"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","A feliratkozási kérelme és/vagy ~s számára küldött üzenetei blokkolva lettek. A feliratkozási kérelmének feloldásához látogassa meg ezt az oldalt: ~s"}. +{"You're not allowed to create nodes","Önnek nincs engedélye csomópontokat létrehozni"}. diff --git a/priv/msgs/id.msg b/priv/msgs/id.msg index d5a8520e7..ac6db281c 100644 --- a/priv/msgs/id.msg +++ b/priv/msgs/id.msg @@ -1,41 +1,74 @@ -{"Access Configuration","Akses Konfigurasi"}. -{"Access Control List Configuration","Konfigurasi Daftar Akses Pengendalian"}. -{"Access Control Lists","Akses Daftar Pengendalian"}. -{"Access control lists","Daftar Pengendalian Akses"}. -{"Access denied by service policy","Akses ditolak oleh kebijakan layanan"}. -{"Access rules","Akses peraturan"}. -{"Access Rules","Aturan Akses"}. -{"Action on user","Tindakan pada pengguna"}. -{"Add Jabber ID","Tambah Jabber ID"}. -{"Add New","Tambah Baru"}. -{"Add User","Tambah Pengguna"}. -{"Administration","Administrasi"}. -{"Administration of ","Administrasi"}. -{"Administrator privileges required","Hak istimewa Administrator dibutuhkan"}. +%% Generated automatically +%% DO NOT EDIT: run `make translations` instead +%% To improve translations please read: +%% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ + +{" (Add * to the end of field to match substring)"," Isi formulir untuk pencarian pengguna Jabber yang cocok (Tambahkan * ke mengakhiri pengisian untuk menyamakan kata)"}. +{" has set the subject to: "," telah menetapkan topik yaitu: "}. +{"# participants","# pengguna"}. +{"A description of the node","Deskripsi node"}. {"A friendly name for the node","Nama yang dikenal untuk node"}. +{"A password is required to enter this room","Diperlukan kata sandi untuk masuk ruangan ini"}. +{"A Web Page","Halaman web"}. +{"Accept","Diterima"}. +{"Access denied by service policy","Akses ditolak oleh kebijakan layanan"}. +{"Access model","Model akses"}. +{"Account doesn't exist","Akun tidak ada"}. +{"Action on user","Tindakan pada pengguna"}. +{"Add User","Tambah Pengguna"}. +{"Administration of ","Administrasi "}. +{"Administration","Administrasi"}. +{"Administrator privileges required","Hak istimewa Administrator dibutuhkan"}. {"All activity","Semua aktifitas"}. +{"All Users","Semua Pengguna"}. +{"Allow subscription","Ijinkan berlangganan"}. {"Allow this Jabber ID to subscribe to this pubsub node?","Izinkan ID Jabber ini untuk berlangganan pada node pubsub ini?"}. +{"Allow this person to register with the room?","Ijinkan orang ini mendaftar masuk kamar?"}. {"Allow users to change the subject","Perbolehkan pengguna untuk mengganti topik"}. {"Allow users to query other users","Perbolehkan pengguna untuk mengetahui pengguna lain"}. {"Allow users to send invites","Perbolehkan pengguna mengirimkan undangan"}. {"Allow users to send private messages","perbolehkan pengguna mengirimkan pesan ke pengguna lain secara pribadi"}. {"Allow visitors to change nickname","Perbolehkan visitor mengganti nama julukan"}. +{"Allow visitors to send private messages to","Izinkan pengunjung mengirimkan pesan privat ke"}. {"Allow visitors to send status text in presence updates","Izinkan pengunjung untuk mengirim teks status terbaru"}. -{"All Users","Semua Pengguna"}. +{"Allow visitors to send voice requests","Izinkan pengunjung mengirim permintaan suara"}. {"Announcements","Pengumuman"}. -{"anyone","Siapapun"}. -{"A password is required to enter this room","Diperlukan kata sandi untuk masuk ruangan ini"}. +{"Answer associated with a picture","Jawaban yang berhubungan dengan gambar"}. +{"Answer associated with a video","Jawaban yang berhubungan dengan video"}. +{"Answer associated with speech","Jawaban yang berhubungan dengan ucapan"}. +{"Answer to a question","Jawaban pertanyaan"}. +{"Anyone in the specified roster group(s) may subscribe and retrieve items","Siapapun dalam keanggotaan grup tertentu dapat berlangganan dan mengambil item"}. +{"Anyone may publish","Siapapun dapat mempublikasi"}. +{"Anyone may subscribe and retrieve items","Siapapun dapat berlangganan dan mengambil item"}. +{"Anyone with Voice","Siapapun dengan fungsi suara"}. +{"Anyone","Siapapun"}. {"April","April"}. +{"Attribute 'channel' is required for this request","Atribut 'channel' diperlukan untuk permintaan ini"}. +{"Attribute 'id' is mandatory for MIX messages","Atribut 'id' harus ada untuk pesan MIX"}. +{"Attribute 'jid' is not allowed here","Atribut 'jid' tidak diijinkan disini"}. +{"Attribute 'node' is not allowed here","Atribut 'node' tidak diijinkan disini"}. +{"Attribute 'to' of stanza that triggered challenge","Atribut 'to' dari stanza yang memicu respon"}. {"August","Agustus"}. -{"Backup","Backup"}. +{"Automatic node creation is not enabled","Pembuatan node otomatis tidak diijinkan"}. {"Backup Management","Manajemen Backup"}. -{"Backup of ","Cadangan dari"}. -{"Backup to File at ","Backup ke File pada"}. +{"Backup of ~p","Cadangan dari ~p"}. +{"Backup to File at ","Backup ke File di lokasi "}. +{"Backup","Backup"}. {"Bad format","Format yang buruk"}. {"Birthday","Hari Lahir"}. +{"Both the username and the resource are required","Baik nama pengguna dan sumber daya diperlukan"}. +{"Bytestream already activated","Bytestream telah aktif"}. +{"Cannot remove active list","Tidak bisa menghapus daftar aktif"}. +{"Cannot remove default list","Tidak bisa menghapus daftar standar"}. {"CAPTCHA web page","CAPTCHA laman web"}. +{"Challenge ID","ID tantangan"}. {"Change Password","Ubah Kata Sandi"}. {"Change User Password","Ubah User Password"}. +{"Changing password is not allowed","Tidak diijinkan mengubah kata sandi"}. +{"Changing role/affiliation is not allowed","Tidak diijinkan mengubah peran/afiliasi"}. +{"Channel already exists","Channel sudah ada"}. +{"Channel does not exist","Channel tidak ada"}. +{"Channels","Channel"}. {"Characters not allowed:","Karakter tidak diperbolehkan:"}. {"Chatroom configuration modified","Konfigurasi ruang chat diubah"}. {"Chatroom is created","Ruang chat telah dibuat"}. @@ -44,118 +77,115 @@ {"Chatroom is stopped","Ruang chat dihentikan"}. {"Chatrooms","Ruangan Chat"}. {"Choose a username and password to register with this server","Pilih nama pengguna dan kata sandi untuk mendaftar dengan layanan ini"}. -{"Choose modules to stop","Pilih Modul untuk berhenti"}. {"Choose storage type of tables","Pilih jenis penyimpanan tabel"}. {"Choose whether to approve this entity's subscription.","Pilih apakah akan menyetujui hubungan pertemanan ini."}. {"City","Kota"}. +{"Client acknowledged more stanzas than sent by server","Klien menerima lebih banyak stanza daripada yang dikirim oleh server"}. {"Commands","Perintah"}. {"Conference room does not exist","Ruang Konferensi tidak ada"}. {"Configuration of room ~s","Pengaturan ruangan ~s"}. {"Configuration","Pengaturan"}. -{"Connected Resources:","Sumber Daya Terhubung:"}. -{"Connections parameters","Parameter Koneksi"}. +{"Contact Addresses (normally, room owner or owners)","Alamat Kontak (biasanya, pemilik atau pemilik kamar)"}. {"Country","Negara"}. -{"CPU Time:","Waktu CPU:"}. +{"Current Discussion Topic","Topik diskusi saat ini"}. +{"Database failure","Kegagalan database"}. +{"Database Tables Configuration at ","Konfigurasi Tabel Database pada "}. {"Database","Database"}. -{"Database Tables at ","Tabel Database pada"}. -{"Database Tables Configuration at ","Database Tabel Konfigurasi pada"}. {"December","Desember"}. {"Default users as participants","pengguna pertama kali masuk sebagai participant"}. -{"Delete","Hapus"}. -{"Delete message of the day","Hapus pesan harian"}. {"Delete message of the day on all hosts","Hapus pesan harian pada semua host"}. -{"Delete Selected","Hapus Yang Terpilih"}. +{"Delete message of the day","Hapus pesan harian"}. {"Delete User","Hapus Pengguna"}. {"Deliver event notifications","Memberikan pemberitahuan acara"}. {"Deliver payloads with event notifications","Memberikan muatan dengan pemberitahuan acara"}. -{"Description:","Keterangan:"}. {"Disc only copy","Hanya salinan dari disc"}. -{"Displayed Groups:","Tampilkan Grup:"}. -{"Don't tell your password to anybody, not even the administrators of the Jabber server.","Jangan memberitahukan kata sandi Anda ke siapapun, bahkan para administrator dari layanan Jabber."}. -{"Dump Backup to Text File at ","Dump Backup ke File Teks di"}. +{"Don't tell your password to anybody, not even the administrators of the XMPP server.","Jangan beritahukan kata sandi Anda ke siapapun, bahkan ke administrator layanan XMPP."}. +{"Dump Backup to Text File at ","Dump Backup menjadi File Teks di "}. {"Dump to Text File","Dump menjadi File Teks"}. +{"Duplicated groups are not allowed by RFC6121","Grup duplikat tidak diperbolehkan oleh RFC6121"}. {"Edit Properties","Ganti Properti"}. -{"ejabberd IRC module","ejabberd IRC modul"}. -{"ejabberd MUC module","ejabberd MUC Module"}. +{"Either approve or decline the voice request.","Antara terima atau tolak permintaan suara."}. +{"ejabberd HTTP Upload service","Layanan HTTP Upload ejabberd"}. +{"ejabberd MUC module","Modul MUC ejabberd"}. +{"ejabberd Multicast service","Layanan Multicast ejabberd"}. {"ejabberd Publish-Subscribe module","Modul ejabberd Setujui-Pertemanan"}. {"ejabberd SOCKS5 Bytestreams module","modul ejabberd SOCKS5 Bytestreams"}. {"ejabberd vCard module","Modul ejabberd vCard"}. {"ejabberd Web Admin","Admin Web ejabberd"}. -{"Elements","Elemen-elemen"}. +{"ejabberd","ejabberd"}. +{"Email Address","Alamat email"}. {"Email","Email"}. -{"Enable logging","Aktifkan catatan"}. -{"Encoding for server ~b","Pengkodean untuk layanan ~b"}. +{"Enable logging","Aktifkan log"}. +{"Enable message archiving","Aktifkan pengarsipan pesan"}. +{"Enabling push without 'node' attribute is not supported","Aktivasi push tanpa atribut 'node' tidak didukung"}. {"End User Session","Akhir Sesi Pengguna"}. -{"Enter list of {Module, [Options]}","Masukkan daftar {Modul, [Options]}"}. {"Enter nickname you want to register","Masukkan nama julukan Anda jika ingin mendaftar"}. {"Enter path to backup file","Masukkan path untuk file cadangan"}. {"Enter path to jabberd14 spool dir","Masukkan path ke direktori spool jabberd14"}. {"Enter path to jabberd14 spool file","Masukkan path ke file jabberd14 spool"}. {"Enter path to text file","Masukkan path ke file teks"}. {"Enter the text you see","Masukkan teks yang Anda lihat"}. -{"Enter username and encodings you wish to use for connecting to IRC servers. Press 'Next' to get more fields to fill in. Press 'Complete' to save settings.","Masukkan username dan pengkodean yang ingin Anda gunakan untuk menghubungkan ke layanan IRC. Tekan 'Selanjutnya' untuk mendapatkan lagi formulir kemudian Tekan 'Lengkap' untuk menyimpan pengaturan."}. -{"Enter username, encodings, ports and passwords you wish to use for connecting to IRC servers","Masukkan username, pengkodean, port dan sandi yang ingin Anda gunakan untuk menghubungkan ke layanan IRC"}. -{"Erlang Jabber Server","Layanan Erlang Jabber"}. -{"Error","Kesalahan"}. -{"Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef.net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}].","Contoh: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef.net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]."}. +{"Erlang XMPP Server","Server Erlang XMPP"}. +{"Exclude Jabber IDs from CAPTCHA challenge","Kecualikan Jabber IDs dari tantangan CAPTCHA"}. +{"Export all tables as SQL queries to a file:","Ekspor semua tabel sebagai kueri SQL ke file:"}. {"Export data of all users in the server to PIEFXIS files (XEP-0227):","Ekspor data dari semua pengguna pada layanan ke berkas PIEFXIS (XEP-0227):"}. {"Export data of users in a host to PIEFXIS files (XEP-0227):","Ekspor data pengguna pada sebuah host ke berkas PIEFXIS (XEP-0227):"}. +{"External component failure","Kegagalan komponen eksternal"}. +{"External component timeout","Komponen eksternal kehabisan waktu"}. +{"Failed to activate bytestream","Gagal mengaktifkan bytestream"}. +{"Failed to extract JID from your voice request approval","Gagal mendapatkan JID dari permintaan akses suara"}. +{"Failed to parse HTTP response","Gagal mengurai respon HTTP"}. +{"Failed to process option '~s'","Gagal memproses dengan opsi '~s'"}. {"Family Name","Nama Keluarga (marga)"}. +{"FAQ Entry","Entri FAQ"}. {"February","Februari"}. -{"Fill in fields to search for any matching Jabber User","Isi kolom untuk mencari pengguna Jabber yang sama"}. -{"Fill in the form to search for any matching Jabber User (Add * to the end of field to match substring)","Isi formulir untuk pencarian pengguna Jabber yang cocok (Tambahkan * ke mengakhiri pengisian untuk menyamakan kata)"}. +{"File larger than ~w bytes","File lebih besar dari ~w bytes"}. +{"Fill in the form to search for any matching XMPP User","Isi kolom untuk mencari pengguna XMPP"}. {"Friday","Jumat"}. -{"From","Dari"}. -{"From ~s","Dari ~s"}. +{"From ~ts","Dari ~ts"}. +{"Full List of Room Admins","Daftar Lengkap Admin Kamar"}. +{"Full List of Room Owners","Daftar Lengkap Pemilik Kamar"}. {"Full Name","Nama Lengkap"}. {"Get Number of Online Users","Dapatkan Jumlah User Yang Online"}. {"Get Number of Registered Users","Dapatkan Jumlah Pengguna Yang Terdaftar"}. -{"Get User Last Login Time","Dapatkan Waktu Login Terakhir Pengguna "}. -{"Get User Password","Dapatkan User Password"}. +{"Get Pending","Lihat yang tertunda"}. +{"Get User Last Login Time","Lihat Waktu Login Terakhir Pengguna"}. {"Get User Statistics","Dapatkan Statistik Pengguna"}. -{"Group ","Grup"}. -{"Groups","Grup"}. +{"Given Name","Nama"}. +{"Grant voice to this person?","Ijinkan akses suara kepadanya?"}. {"has been banned","telah dibanned"}. -{"has been kicked because of an affiliation change","telah dikick karena perubahan afiliasi"}. {"has been kicked because of a system shutdown","telah dikick karena sistem shutdown"}. +{"has been kicked because of an affiliation change","telah dikick karena perubahan afiliasi"}. {"has been kicked because the room has been changed to members-only","telah dikick karena ruangan telah diubah menjadi hanya untuk member"}. {"has been kicked","telah dikick"}. -{" has set the subject to: "," telah menetapkan topik yaitu: "}. -{"Host","Host"}. +{"Host unknown","Host tidak dikenal"}. +{"HTTP File Upload","Unggah Berkas HTTP"}. +{"Idle connection","Koneksi menganggur"}. {"If you don't see the CAPTCHA image here, visit the web page.","Jika Anda tidak melihat gambar CAPTCHA disini, silahkan kunjungi halaman web."}. -{"If you want to specify different ports, passwords, encodings for IRC servers, fill this list with values in format '{\"irc server\", \"encoding\", port, \"password\"}'. By default this service use \"~s\" encoding, port ~p, empty password.","Jika Anda ingin menentukan port yang berbeda, sandi, pengkodean untuk layanan IRC, isi daftar ini dengan nilai-nilai dalam format '{\"server irc \", \"encoding \", port, \"sandi \"}'. Secara default ini menggunakan layanan \"~s \" pengkodean, port ~p, kata sandi kosong."}. {"Import Directory","Impor Direktori"}. {"Import File","Impor File"}. {"Import user data from jabberd14 spool file:","Impor data pengguna dari sekumpulan berkas jabberd14:"}. -{"Import User from File at ","Impor Pengguna dari File pada"}. +{"Import User from File at ","Impor Pengguna dari File pada "}. {"Import users data from a PIEFXIS file (XEP-0227):","impor data-data pengguna dari sebuah PIEFXIS (XEP-0227):"}. {"Import users data from jabberd14 spool directory:","Импорт пользовательских данных из буферной директории jabberd14:"}. -{"Import Users from Dir at ","Impor Pengguna dari Dir di"}. +{"Import Users from Dir at ","Impor Pengguna dari Dir di "}. {"Import Users From jabberd14 Spool Files","Impor Pengguna Dari jabberd14 Spool File"}. {"Improper message type","Jenis pesan yang tidak benar"}. +{"Incorrect CAPTCHA submit","Isian CAPTCHA salah"}. +{"Incorrect data form","Formulir data salah"}. {"Incorrect password","Kata sandi salah"}. -{"Invalid affiliation: ~s","Afiliasi tidak valid: ~s"}. -{"Invalid role: ~s","Peran tidak valid: ~s"}. +{"Incorrect value of 'action' attribute","Nilai atribut 'aksi' salah"}. +{"Incorrect value of 'action' in data form","Nilai 'tindakan' yang salah dalam formulir data"}. +{"Insufficient privilege","Hak tidak mencukupi"}. +{"Internal server error","Galat server internal"}. +{"Invalid node name","Nama node tidak valid"}. {"IP addresses","Alamat IP"}. -{"IP","IP"}. -{"IRC channel (don't put the first #)","Channel IRC (tidak perlu menempatkan # sebelumnya)"}. -{"IRC server","Layanan IRC"}. -{"IRC settings","Pengaturan IRC"}. -{"IRC Transport","IRC Transport"}. -{"IRC username","Nama Pengguna IRC"}. -{"IRC Username","Nama Pengguna IRC"}. {"is now known as","sekarang dikenal sebagai"}. -{"It is not allowed to send private messages","Hal ini tidak diperbolehkan untuk mengirim pesan pribadi"}. {"It is not allowed to send private messages of type \"groupchat\"","Hal ini tidak diperbolehkan untuk mengirim pesan pribadi jenis \"groupchat \""}. {"It is not allowed to send private messages to the conference","Hal ini tidak diperbolehkan untuk mengirim pesan pribadi ke konferensi"}. -{"Jabber Account Registration","Pendaftaran Akun Jabber"}. {"Jabber ID","Jabber ID"}. -{"Jabber ID ~s is invalid","Jabber ID ~s tidak valid"}. {"January","Januari"}. -{"Join IRC channel","Gabung channel IRC"}. {"joins the room","bergabung ke ruangan"}. -{"Join the IRC channel here.","Gabung ke channel IRC disini"}. -{"Join the IRC channel in this Jabber ID: ~s","Gabung ke channel IRC dengan Jabber ID: ~s"}. {"July","Juli"}. {"June","Juni"}. {"Last Activity","Aktifitas Terakhir"}. @@ -163,10 +193,6 @@ {"Last month","Akhir bulan"}. {"Last year","Akhir tahun"}. {"leaves the room","meninggalkan ruangan"}. -{"Listened Ports at ","Mendeteksi Port-port di"}. -{"Listened Ports","Port Terdeteksi"}. -{"List of modules to start","Daftar modul untuk memulai"}. -{"Low level update script","Perbaruan naskah tingkat rendah"}. {"Make participants list public","Buat daftar participant diketahui oleh public"}. {"Make room CAPTCHA protected","Buat ruangan dilindungi dengan CAPTCHA"}. {"Make room members-only","Buat ruangan hanya untuk member saja"}. @@ -175,39 +201,29 @@ {"Make room persistent","Buat ruangan menjadi permanent"}. {"Make room public searchable","Buat ruangan dapat dicari"}. {"March","Maret"}. -{"Maximum Number of Occupants","Maksimum Jumlah Penghuni"}. -{"Max # of items to persist","Max item untuk bertahan"}. {"Max payload size in bytes","Max kapasitas ukuran dalam bytes"}. +{"Maximum Number of Occupants","Maksimum Jumlah Penghuni"}. {"May","Mei"}. -{"Members:","Anggota:"}. {"Membership is required to enter this room","Hanya Member yang dapat masuk ruangan ini"}. -{"Memorize your password, or write it in a paper placed in a safe place. In Jabber there isn't an automated way to recover your password if you forget it.","Hafalkan kata sandi Anda, atau dicatat dan letakkan di tempat yang aman. Didalam Jabber tidak ada cara otomatis untuk mendapatkan kembali password Anda jika Anda lupa."}. -{"Memory","Memori"}. {"Message body","Isi Pesan"}. {"Middle Name","Nama Tengah"}. {"Moderator privileges required","Hak istimewa moderator dibutuhkan"}. -{"moderators only","Hanya moderator"}. -{"Modified modules","Modifikasi modul-modul"}. -{"Module","Modul"}. -{"Modules at ","modul-modul di"}. -{"Modules","Modul"}. {"Monday","Senin"}. -{"Name:","Nama:"}. +{"Multiple elements are not allowed by RFC6121","Beberapa elemen tidak diizinkan oleh RFC6121"}. {"Name","Nama"}. {"Never","Tidak Pernah"}. {"New Password:","Password Baru:"}. -{"Nickname","Nama Julukan"}. -{"Nickname Registration at ","Pendaftaran Julukan pada"}. +{"Nickname Registration at ","Pendaftaran Julukan pada "}. {"Nickname ~s does not exist in the room","Nama Julukan ~s tidak berada di dalam ruangan"}. +{"Nickname","Nama Julukan"}. {"No body provided for announce message","Tidak ada isi pesan yang disediakan untuk mengirimkan pesan"}. {"No Data","Tidak Ada Data"}. +{"No element found","Tidak ada elemen yang ditemukan"}. +{"No limit","Tidak terbatas"}. {"Node ID","ID Node"}. -{"Node ","Node"}. {"Node not found","Node tidak ditemukan"}. {"Nodes","Node-node"}. -{"No limit","Tidak terbatas"}. {"None","Tak satupun"}. -{"No resource provided","Tidak ada sumber daya yang disediakan"}. {"Not Found","Tidak Ditemukan"}. {"Notify subscribers when items are removed from the node","Beritahu pelanggan ketika item tersebut dikeluarkan dari node"}. {"Notify subscribers when the node configuration changes","Beritahu pelanggan ketika ada perubahan konfigurasi node"}. @@ -217,94 +233,93 @@ {"Number of online users","Jumlah pengguna online"}. {"Number of registered users","Jumlah pengguna terdaftar"}. {"October","Oktober"}. -{"Offline Messages:","Pesan Offline:"}. -{"Offline Messages","Pesan Offline"}. {"OK","YA"}. {"Old Password:","Password Lama:"}. -{"Online","Online"}. -{"Online Users:","Pengguna Online:"}. {"Online Users","Pengguna Yang Online"}. +{"Online","Online"}. {"Only deliver notifications to available users","Hanya mengirimkan pemberitahuan kepada pengguna yang tersedia"}. {"Only moderators and participants are allowed to change the subject in this room","Hanya moderator dan peserta yang diizinkan untuk mengganti topik pembicaraan di ruangan ini"}. {"Only moderators are allowed to change the subject in this room","Hanya moderator yang diperbolehkan untuk mengubah topik dalam ruangan ini"}. {"Only occupants are allowed to send messages to the conference","Hanya penghuni yang diizinkan untuk mengirim pesan ke konferensi"}. {"Only occupants are allowed to send queries to the conference","Hanya penghuni diizinkan untuk mengirim permintaan ke konferensi"}. {"Only service administrators are allowed to send service messages","Layanan hanya diperuntukan kepada administrator yang diizinkan untuk mengirim layanan pesan"}. -{"Options","Pilihan-pilihan"}. {"Organization Name","Nama Organisasi"}. {"Organization Unit","Unit Organisasi"}. {"Outgoing s2s Connections","Koneksi Keluar s2s"}. -{"Outgoing s2s Connections:","Koneksi s2s yang keluar:"}. -{"Outgoing s2s Servers:","Layanan s2s yang keluar:"}. {"Owner privileges required","Hak istimewa owner dibutuhkan"}. -{"Packet","Paket"}. -{"Password ~b","Kata Sandi ~b"}. -{"Password:","Kata Sandi:"}. -{"Password","Sandi"}. +{"Participant","Partisipan"}. {"Password Verification:","Verifikasi Kata Sandi:"}. {"Password Verification","Verifikasi Sandi"}. +{"Password:","Kata Sandi:"}. +{"Password","Sandi"}. {"Path to Dir","Jalur ke Dir"}. {"Path to File","Jalur ke File"}. -{"Pending","Tertunda"}. -{"Period: ","Periode:"}. +{"Period: ","Periode: "}. {"Persist items to storage","Pertahankan item ke penyimpanan"}. +{"Persistent","Persisten"}. +{"Ping query is incorrect","Kueri ping salah"}. {"Ping","Ping"}. {"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Harap dicatat bahwa pilihan ini hanya akan membuat cadangan builtin Mnesia database. Jika Anda menggunakan modul ODBC, anda juga perlu untuk membuat cadangan database SQL Anda secara terpisah."}. {"Pong","Pong"}. -{"Port ~b","Port ~b"}. -{"Port","Port"}. {"Present real Jabber IDs to","Tampilkan Jabber ID secara lengkap"}. +{"Previous session not found","Sesi sebelumnya tidak ditemukan"}. +{"Previous session PID has been killed","Sesi PID sebelumnya telah dimatikan"}. +{"Previous session PID has exited","Sesi PID sebelumnya telah keluar"}. +{"Previous session PID is dead","Sesi PID sebelumnya mati"}. +{"Previous session timed out","Sesi sebelumnya habis waktu"}. {"private, ","pribadi, "}. -{"Protocol","Protocol"}. +{"Public","Publik"}. +{"Publish model","Model penerbitan"}. {"Publish-Subscribe","Setujui-Pertemanan"}. {"PubSub subscriber request","Permintaan pertemanan PubSub"}. {"Purge all items when the relevant publisher goes offline","Bersihkan semua item ketika penerbit yang relevan telah offline"}. {"Queries to the conference members are not allowed in this room","Permintaan untuk para anggota konferensi tidak diperbolehkan di ruangan ini"}. +{"Query to another users is forbidden","Kueri ke pengguna lain dilarang"}. {"RAM and disc copy","RAM dan disc salinan"}. {"RAM copy","Salinan RAM"}. -{"Raw","mentah"}. {"Really delete message of the day?","Benar-benar ingin menghapus pesan harian?"}. +{"Receive notification from all descendent nodes","Terima notifikasi dari semua node turunan"}. +{"Receive notification from direct child nodes only","Terima notifikasi dari child node saja"}. +{"Receive notification of new items only","Terima notifikasi dari item baru saja"}. +{"Receive notification of new nodes only","Terima notifikasi dari node baru saja"}. {"Recipient is not in the conference room","Penerima tidak berada di ruangan konferensi"}. -{"Register a Jabber account","Daftarkan sebuah akun jabber"}. -{"Registered Users:","Pengguna Terdaftar:"}. -{"Registered Users","Pengguna Terdaftar"}. +{"Register an XMPP account","Daftarkan sebuah akun XMPP"}. {"Register","Mendaftar"}. -{"Registration in mod_irc for ","Pendaftaran di mod_irc untuk"}. {"Remote copy","Salinan Remote"}. -{"Remove All Offline Messages","Hapus Semua Pesan Offline"}. -{"Remove","Menghapus"}. {"Remove User","Hapus Pengguna"}. {"Replaced by new connection","Diganti dengan koneksi baru"}. +{"Request has timed out","Waktu permintaan telah habis"}. +{"Request is ignored","Permintaan diabaikan"}. +{"Requested role","Peran yang diminta"}. {"Resources","Sumber daya"}. -{"Restart","Jalankan Ulang"}. {"Restart Service","Restart Layanan"}. -{"Restore Backup from File at ","Kembalikan Backup dari File pada"}. +{"Restore Backup from File at ","Kembalikan Backup dari File pada "}. {"Restore binary backup after next ejabberd restart (requires less memory):","Mengembalikan cadangan yang berpasanagn setelah ejabberd berikutnya dijalankan ulang (memerlukan memori lebih sedikit):"}. {"Restore binary backup immediately:","Segera mengembalikan cadangan yang berpasangan:"}. -{"Restore","Mengembalikan"}. {"Restore plain text backup immediately:","Segera mengembalikan cadangan teks biasa:"}. +{"Restore","Mengembalikan"}. +{"Roles that May Send Private Messages","Peran yang Dapat Mengirim Pesan Pribadi"}. {"Room Configuration","Konfigurasi Ruangan"}. {"Room creation is denied by service policy","Pembuatan Ruangan ditolak oleh kebijakan layanan"}. {"Room description","Keterangan ruangan"}. {"Room Occupants","Penghuni Ruangan"}. +{"Room terminates","Ruang dihentikan"}. {"Room title","Nama Ruangan"}. {"Roster groups allowed to subscribe","Kelompok kontak yang diizinkan untuk berlangganan"}. -{"Roster","Kontak"}. -{"Roster of ","Kontak dari"}. {"Roster size","Ukuran Daftar Kontak"}. -{"RPC Call Error","Panggilan Kesalahan RPC"}. {"Running Nodes","Menjalankan Node"}. -{"~s access rule configuration","~s aturan akses konfigurasi"}. +{"~s invites you to the room ~s","~s mengundang anda masuk kamar ~s"}. {"Saturday","Sabtu"}. -{"Script check","Periksa naskah"}. -{"Search Results for ","Hasil Pencarian untuk"}. -{"Search users in ","Pencarian pengguna dalam"}. -{"Send announcement to all online users","Kirim pengumuman untuk semua pengguna yang online"}. +{"Search from the date","Cari dari tanggal"}. +{"Search Results for ","Hasil Pencarian untuk "}. +{"Search the text","Cari teks"}. +{"Search until the date","Cari sampai tanggal"}. +{"Search users in ","Pencarian pengguna dalam "}. {"Send announcement to all online users on all hosts","Kirim pengumuman untuk semua pengguna yang online pada semua host"}. -{"Send announcement to all users","Kirim pengumuman untuk semua pengguna"}. +{"Send announcement to all online users","Kirim pengumuman untuk semua pengguna yang online"}. {"Send announcement to all users on all hosts","Kirim pengumuman untuk semua pengguna pada semua host"}. +{"Send announcement to all users","Kirim pengumuman untuk semua pengguna"}. {"September","September"}. -{"Server ~b","Layanan ~b"}. {"Server:","Layanan:"}. {"Set message of the day and send to online users","Mengatur pesan harian dan mengirimkan ke pengguna yang online"}. {"Set message of the day on all hosts and send to online users","Mengatur pesan harian pada semua host dan kirimkan ke pengguna yang online"}. @@ -312,96 +327,139 @@ {"Show Integral Table","Tampilkan Tabel Terpisah"}. {"Show Ordinary Table","Tampilkan Tabel Normal"}. {"Shut Down Service","Shut Down Layanan"}. -{"~s invites you to the room ~s","~s mengundang anda ke ruangan ~s"}. -{"Some Jabber clients can store your password in your computer. Use that feature only if you trust your computer is safe.","Beberapa klien Jabber dapat menyimpan password di komputer Anda. Gunakan fitur itu hanya jika Anda mempercayai komputer Anda aman."}. +{"SOCKS5 Bytestreams","SOCKS5 Bytestreams"}. {"Specify the access model","Tentukan model akses"}. {"Specify the event message type","Tentukan jenis acara pesan"}. {"Specify the publisher model","Tentukan model penerbitan"}. -{"~s's Offline Messages Queue","Antrian Pesan Offline ~s"}. -{"Start Modules at ","Mulai Modul pada"}. -{"Start Modules","Memulai Modul"}. -{"Start","Mulai"}. -{"Statistics of ~p","statistik dari ~p"}. -{"Statistics","Statistik"}. -{"Stop","Hentikan"}. -{"Stop Modules at ","Hentikan Modul pada"}. -{"Stop Modules","Hentikan Modul"}. +{"Stanza ID","ID Stanza"}. {"Stopped Nodes","Menghentikan node"}. -{"Storage Type","Jenis Penyimpanan"}. {"Store binary backup:","Penyimpanan cadangan yang berpasangan:"}. {"Store plain text backup:","Simpan cadangan teks biasa:"}. +{"Stream management is already enabled","Manajemen stream sudah diaktifkan"}. +{"Stream management is not enabled","Manajemen stream tidak diaktifkan"}. {"Subject","Subyek"}. -{"Submit","Serahkan"}. {"Submitted","Ulangi masukan"}. {"Subscriber Address","Alamat Pertemanan"}. -{"Subscription","Berlangganan"}. +{"Subscribers may publish","Pelanggan dapat mempublikasikan"}. +{"Subscriptions are not allowed","Langganan tidak diperbolehkan"}. {"Sunday","Minggu"}. +{"Text associated with a picture","Teks yang terkait dengan gambar"}. +{"Text associated with a sound","Teks yang terkait dengan suara"}. +{"Text associated with a video","Teks yang terkait dengan video"}. +{"Text associated with speech","Teks yang terkait dengan ucapan"}. {"That nickname is already in use by another occupant","Julukan itu sudah digunakan oleh penghuni lain"}. {"That nickname is registered by another person","Julukan tersebut telah didaftarkan oleh orang lain"}. +{"The account was not unregistered","Akun tidak terdaftar"}. {"The CAPTCHA is valid.","Captcha ini benar."}. {"The CAPTCHA verification has failed","Verifikasi CAPTCHA telah gagal"}. +{"The captcha you entered is wrong","Isian captcha yang anda masukkan salah"}. {"The collections with which a node is affiliated","Koleksi dengan yang berafiliasi dengan sebuah node"}. -{"the password is","kata sandi yaitu:"}. +{"The JID of the node creator","JID dari pembuat node"}. +{"The JIDs of those to contact with questions","JID dari mereka untuk dihubungi dengan pertanyaan"}. +{"The JIDs of those with an affiliation of owner","JID dari mereka yang memiliki afiliasi pemilik"}. +{"The JIDs of those with an affiliation of publisher","JID dari mereka yang memiliki afiliasi penerbit"}. +{"The name of the node","Nama node"}. +{"The node is a collection node","Node adalah node koleksi"}. +{"The node is a leaf node (default)","Node adalah leaf node (default)"}. +{"The NodeID of the relevant node","NodeID dari node yang relevan"}. +{"The number of subscribers to the node","Jumlah pendaftar di node"}. +{"The number of unread or undelivered messages","Jumlah pesan yang belum dibaca atau tidak terkirim"}. +{"The password contains unacceptable characters","Kata sandi mengandung karakter yang tidak dapat diterima"}. {"The password is too weak","Kata sandi terlalu lemah"}. -{"The password of your Jabber account was successfully changed.","Kata sandi pada akun Jabber Anda telah berhasil diubah."}. -{"There was an error changing the password: ","Ada kesalahan dalam mengubah password:"}. -{"There was an error creating the account: ","Ada kesalahan saat membuat akun:"}. -{"There was an error deleting the account: ","Ada kesalahan saat menghapus akun:"}. -{"This is case insensitive: macbeth is the same that MacBeth and Macbeth.","Pada bagian ini huruf besar dan kecil tidak dibedakan: Misalnya macbeth adalah sama dengan MacBeth juga Macbeth."}. -{"This page allows to create a Jabber account in this Jabber server. Your JID (Jabber IDentifier) will be of the form: username@server. Please read carefully the instructions to fill correctly the fields.","Halaman ini memungkinkan untuk membuat akun Jabber di layanan Jabber ini. JID Anda (Jabber Pengenal) akan berbentuk: namapengguna@layanan. Harap baca dengan seksama petunjuk-petunjuk untuk mengisi kolom dengan benar."}. -{"This page allows to unregister a Jabber account in this Jabber server.","Pada bagian ini memungkinkan Anda untuk membatalkan pendaftaran akun Jabber pada layanan Jabber ini."}. -{"This participant is kicked from the room because he sent an error message","Peserta ini dikick dari ruangan karena dia mengirim pesan kesalahan"}. -{"This participant is kicked from the room because he sent an error message to another participant","Participant ini dikick dari ruangan karena ia mengirim pesan kesalahan ke participant lain"}. -{"This participant is kicked from the room because he sent an error presence","Participant ini dikick dari ruangan karena ia mengirim kehadiran kesalahan"}. +{"the password is","kata sandinya"}. +{"The password was not changed","Kata sandi belum berubah"}. +{"The passwords are different","Kata sandi berbeda"}. +{"There was an error changing the password: ","Ada kesalahan saat merubah kata kunci: "}. +{"There was an error creating the account: ","Ada kesalahan saat membuat akun: "}. +{"There was an error deleting the account: ","Ada kesalahan saat menghapus akun: "}. {"This room is not anonymous","Ruangan ini tidak dikenal"}. +{"This service can not process the address: ~s","Layanan ini tidak dapat memproses alamat: ~s"}. {"Thursday","Kamis"}. {"Time delay","Waktu tunda"}. -{"Time","Waktu"}. -{"To","Kepada"}. -{"To ~s","Kepada ~s"}. -{"Traffic rate limit is exceeded","Lalu lintas melebihi batas"}. -{"Transactions Aborted:","Transaksi yang dibatalkan:"}. -{"Transactions Committed:","Transaksi yang dilakukan:"}. -{"Transactions Logged:","Transaksi yang ditempuh:"}. -{"Transactions Restarted:","Transaksi yang dijalankan ulang:"}. +{"To register, visit ~s","Untuk mendaftar, kunjungi ~s"}. +{"To ~ts","Kepada ~ts"}. +{"Token TTL","TTL Token"}. +{"Too many active bytestreams","Terlalu banyak bytestream aktif"}. +{"Too many CAPTCHA requests","Terlalu banyak permintaan CAPTCHA"}. +{"Too many child elements","Terlalu banyak elemen turunan"}. +{"Too many elements","Terlalu banyak elemen"}. +{"Too many elements","Terlalu banyak elemen"}. +{"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","Terlalu banyak (~p) percobaan otentifikasi yang gagal dari alamat IP (~s). Alamat akan di unblok pada ~s UTC"}. +{"Too many receiver fields were specified","Terlalu banyak bidang penerima yang ditentukan"}. +{"Too many users in this conference","Terlalu banyak pengguna di grup ini"}. +{"Traffic rate limit is exceeded","Batas tingkat lalu lintas terlampaui"}. +{"~ts's Offline Messages Queue","~ts's antrian Pesan Offline"}. {"Tuesday","Selasa"}. {"Unable to generate a CAPTCHA","Tidak dapat menghasilkan CAPTCHA"}. +{"Unable to register route on existing local domain","Tidak dapat mendaftarkan rute di domain lokal yang ada"}. {"Unauthorized","Ditolak"}. -{"Unregister a Jabber account","Nonaktifkan akun jabber"}. +{"Unexpected action","Aksi yang tidak diharapkan"}. +{"Unexpected error condition: ~p","Kondisi kerusakan yang tidak diduga: ~p"}. +{"Unregister an XMPP account","Nonaktifkan akun XMPP"}. {"Unregister","Nonaktifkan"}. -{"Update ","Memperbarui "}. -{"Update","Memperbarui"}. +{"Unsupported element","Elemen tidak didukung"}. +{"Unsupported version","Versi tidak didukung"}. {"Update message of the day (don't send)","Rubah pesan harian (tidak dikirim)"}. {"Update message of the day on all hosts (don't send)","Rubah pesan harian pada semua host (tidak dikirim)"}. -{"Update plan","Rencana Perubahan"}. -{"Update script","Perbarui naskah"}. -{"Uptime:","Sampai saat:"}. -{"Use of STARTTLS required","Penggunaan STARTTLS diperlukan"}. +{"User already exists","Pengguna sudah ada"}. +{"User (jid)","Pengguna (jid)"}. +{"User JID","Pengguna JID"}. {"User Management","Manajemen Pengguna"}. +{"User removed","Pengguna dipindahkan"}. +{"User session not found","Sesi pengguna tidak ditemukan"}. +{"User session terminated","Sesi pengguna dihentikan"}. +{"User ~ts","Pengguna ~ts"}. {"Username:","Nama Pengguna:"}. -{"User ","Pengguna"}. {"User","Pengguna"}. {"Users are not allowed to register accounts so quickly","Pengguna tidak diperkenankan untuk mendaftar akun begitu cepat"}. {"Users Last Activity","Aktifitas terakhir para pengguna"}. {"Users","Pengguna"}. -{"Validate","Mengesahkan"}. +{"Value 'get' of 'type' attribute is not allowed","Nilai 'get' dari 'type' atribut tidak diperbolehkan"}. +{"Value of '~s' should be boolean","Nilai '~ s' harus boolean"}. +{"Value of '~s' should be datetime string","Nilai '~s' harus string datetime"}. +{"Value of '~s' should be integer","Nilai '~ s' harus integer"}. +{"Value 'set' of 'type' attribute is not allowed","Nilai 'set' dari 'type' atribut tidak diperbolehkan"}. {"vCard User Search","vCard Pencarian Pengguna"}. -{"Virtual Hosts","Virtual Hosts"}. -{"Visitors are not allowed to change their nicknames in this room","Visitor tidak diperbolehkan untuk mengubah nama julukan di ruangan ini"}. -{"Visitors are not allowed to send messages to all occupants","Visitor tidak diperbolehkan untuk mengirim pesan ke semua penghuni"}. +{"Virtual Hosts","Host Virtual"}. +{"Visitors are not allowed to change their nicknames in this room","Tamu tidak diperbolehkan untuk mengubah nama panggilan di ruangan ini"}. +{"Visitors are not allowed to send messages to all occupants","Tamu tidak diperbolehkan untuk mengirim pesan ke semua penghuni"}. +{"Visitor","Tamu"}. +{"Voice request","Permintaan suara"}. +{"Voice requests are disabled in this conference","Permintaan suara dinonaktifkan dalam konferensi ini"}. {"Wednesday","Rabu"}. +{"When a new subscription is processed and whenever a subscriber comes online","Saat langganan baru diproses dan tiap kali pelanggan online"}. +{"When a new subscription is processed","Saat langganan baru diproses"}. {"When to send the last published item","Ketika untuk mengirim item terakhir yang dipublikasikan"}. +{"Whether an entity wants to receive an XMPP message body in addition to the payload format","Apakah entitas ingin menerima isi pesan XMPP selain format payload"}. +{"Whether an entity wants to receive digests (aggregations) of notifications or all notifications individually","Apakah entitas ingin menerima ringkasan(agregasi) pemberitahuan atau semua pemberitahuan satu per satu"}. +{"Whether an entity wants to receive or disable notifications","Apakah entitas ingin menerima atau menonaktifkan pemberitahuan"}. +{"Whether owners or publisher should receive replies to items","Apakah pemilik atau penerbit harus menerima balasan dari item"}. +{"Whether the node is a leaf (default) or a collection","Apakah node adalah leaf (default) atau koleksi"}. {"Whether to allow subscriptions","Apakah diperbolehkan untuk berlangganan"}. -{"You can later change your password using a Jabber client.","Anda dapat mengubah kata sandi anda dilain waktu dengan menggunakan klien Jabber."}. +{"Whether to make all subscriptions temporary, based on subscriber presence","Apakah akan menjadikan semua langganan sementara, berdasarkan keberadaan pelanggan"}. +{"Whether to notify owners about new subscribers and unsubscribes","Apakah akan memberi tahu pemilik tentang pelanggan baru dan berhenti berlangganan"}. +{"Who may associate leaf nodes with a collection","Siapa yang dapat mengaitkan leaf node dengan koleksi"}. +{"Wrong parameters in the web formulary","Parameter yang salah di formula web"}. +{"Wrong xmlns","xmlns salah"}. +{"XMPP Account Registration","Pendaftaran Akun XMPP"}. +{"XMPP Domains","Domain XMPP"}. +{"XMPP Show Value of Away","XMPP menunjukkan status Away"}. +{"XMPP Show Value of Chat","XMPP menunjukkan status Chat"}. +{"XMPP Show Value of DND (Do Not Disturb)","XMPP menunjukkan status DND (Do Not Disturb)"}. +{"XMPP Show Value of XA (Extended Away)","XMPP menunjukkan status XA (Extended Away)"}. +{"XMPP URI of Associated Publish-Subscribe Node","XMPP URI dari node Associated Publish-Subscribe"}. +{"You are being removed from the room because of a system shutdown","Anda sedang dikeluarkan dari kamar karena sistem shutdown"}. +{"You are not joined to the channel","Anda tidak bergabung ke channel"}. +{"You can later change your password using an XMPP client.","Anda dapat mengubah kata sandi menggunakan aplikasi XMPP."}. {"You have been banned from this room","Anda telah diblokir dari ruangan ini"}. -{"You must fill in field \"Nickname\" in the form","Anda harus mengisi kolom \"Julukan\" dalam formulir"}. +{"You have joined too many conferences","Anda telah mengikuti terlalu banyak grup"}. +{"You must fill in field \"Nickname\" in the form","Anda harus mengisi kolom \"Panggilan\" dalam formulir"}. {"You need a client that supports x:data and CAPTCHA to register","Anda memerlukan klien yang mendukung x:data dan CAPTCHA untuk mendaftar"}. {"You need a client that supports x:data to register the nickname","Anda memerlukan klien yang mendukung x:data untuk mendaftar julukan"}. -{"You need an x:data capable client to configure mod_irc settings","Anda memerlukan x:data klien untuk mampu mengkonfigurasi pengaturan mod_irc"}. -{"You need an x:data capable client to configure room","Anda memerlukan x:data klien untuk dapat mengkonfigurasi ruangan"}. {"You need an x:data capable client to search","Anda memerlukan x:data klien untuk melakukan pencarian"}. -{"Your active privacy list has denied the routing of this stanza.","Daftar privasi aktif Anda telah menolak routing ztanza ini"}. +{"Your active privacy list has denied the routing of this stanza.","Daftar privasi aktif Anda telah menolak routing stanza ini."}. {"Your contact offline message queue is full. The message has been discarded.","Kontak offline Anda pada antrian pesan sudah penuh. Pesan telah dibuang."}. -{"Your Jabber account was successfully created.","Jabber akun Anda telah sukses dibuat"}. -{"Your Jabber account was successfully deleted.","Jabber akun Anda berhasil dihapus."}. -{"Your messages to ~s are being blocked. To unblock them, visit ~s","Pesan Anda untuk ~s sedang diblokir. Untuk membuka blokir tersebut, kunjungi ~s"}. +{"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","Pesan Anda untuk ~s sedang diblokir. Untuk membuka blokir tersebut, kunjungi ~s"}. +{"Your XMPP account was successfully registered.","Akun XMPP Anda berhasil didaftarkan."}. +{"Your XMPP account was successfully unregistered.","Akun XMPP Anda berhasil dihapus."}. +{"You're not allowed to create nodes","Anda tidak diizinkan membuat node"}. diff --git a/priv/msgs/id.po b/priv/msgs/id.po deleted file mode 100644 index 6dd2c5c6c..000000000 --- a/priv/msgs/id.po +++ /dev/null @@ -1,1866 +0,0 @@ -# , 2010. -msgid "" -msgstr "" -"Project-Id-Version: 2.1.0-alpha\n" -"PO-Revision-Date: 2011-02-15 07:09+0800\n" -"Last-Translator: Irfan Mahfudz Guntur \n" -"Language-Team: SmartCommunity \n" -"Language: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Language: Indonesian (Bahasa Indonesia)\n" - -#: ejabberd_c2s.erl:424 ejabberd_c2s.erl:727 -msgid "Use of STARTTLS required" -msgstr "Penggunaan STARTTLS diperlukan" - -#: ejabberd_c2s.erl:503 -msgid "No resource provided" -msgstr "Tidak ada sumber daya yang disediakan" - -#: ejabberd_c2s.erl:1197 -msgid "Replaced by new connection" -msgstr "Diganti dengan koneksi baru" - -#: ejabberd_c2s.erl:1885 -msgid "Your active privacy list has denied the routing of this stanza." -msgstr "Daftar privasi aktif Anda telah menolak routing ztanza ini" - -#: ejabberd_captcha.erl:96 ejabberd_captcha.erl:152 ejabberd_captcha.erl:178 -msgid "Enter the text you see" -msgstr "Masukkan teks yang Anda lihat" - -#: ejabberd_captcha.erl:101 -msgid "Your messages to ~s are being blocked. To unblock them, visit ~s" -msgstr "" -"Pesan Anda untuk ~s sedang diblokir. Untuk membuka blokir tersebut, kunjungi " -"~s" - -#: ejabberd_captcha.erl:134 -msgid "If you don't see the CAPTCHA image here, visit the web page." -msgstr "" -"Jika Anda tidak melihat gambar CAPTCHA disini, silahkan kunjungi halaman web." - -#: ejabberd_captcha.erl:146 -msgid "CAPTCHA web page" -msgstr "CAPTCHA laman web" - -#: ejabberd_captcha.erl:307 -msgid "The CAPTCHA is valid." -msgstr "Captcha ini benar." - -#: mod_adhoc.erl:95 mod_adhoc.erl:125 mod_adhoc.erl:143 mod_adhoc.erl:161 -msgid "Commands" -msgstr "Perintah" - -#: mod_adhoc.erl:149 mod_adhoc.erl:243 -msgid "Ping" -msgstr "Ping" - -#: mod_adhoc.erl:260 -msgid "Pong" -msgstr "Pong" - -#: mod_announce.erl:507 mod_announce_odbc.erl:499 -msgid "Really delete message of the day?" -msgstr "Benar-benar ingin menghapus pesan harian?" - -#: mod_announce.erl:515 mod_announce_odbc.erl:507 mod_configure.erl:1083 -#: mod_configure.erl:1128 -msgid "Subject" -msgstr "Subyek" - -#: mod_announce.erl:520 mod_announce_odbc.erl:512 mod_configure.erl:1088 -#: mod_configure.erl:1133 -msgid "Message body" -msgstr "Isi Pesan" - -#: mod_announce.erl:600 mod_announce_odbc.erl:592 -msgid "No body provided for announce message" -msgstr "Tidak ada isi pesan yang disediakan untuk mengirimkan pesan" - -#: mod_announce.erl:635 mod_announce_odbc.erl:627 -msgid "Announcements" -msgstr "Pengumuman" - -#: mod_announce.erl:637 mod_announce_odbc.erl:629 -msgid "Send announcement to all users" -msgstr "Kirim pengumuman untuk semua pengguna" - -#: mod_announce.erl:639 mod_announce_odbc.erl:631 -msgid "Send announcement to all users on all hosts" -msgstr "Kirim pengumuman untuk semua pengguna pada semua host" - -#: mod_announce.erl:641 mod_announce_odbc.erl:633 -msgid "Send announcement to all online users" -msgstr "Kirim pengumuman untuk semua pengguna yang online" - -#: mod_announce.erl:643 mod_announce_odbc.erl:635 mod_configure.erl:1078 -#: mod_configure.erl:1123 -msgid "Send announcement to all online users on all hosts" -msgstr "Kirim pengumuman untuk semua pengguna yang online pada semua host" - -#: mod_announce.erl:645 mod_announce_odbc.erl:637 -msgid "Set message of the day and send to online users" -msgstr "Mengatur pesan harian dan mengirimkan ke pengguna yang online" - -#: mod_announce.erl:647 mod_announce_odbc.erl:639 -msgid "Set message of the day on all hosts and send to online users" -msgstr "" -"Mengatur pesan harian pada semua host dan kirimkan ke pengguna yang online" - -#: mod_announce.erl:649 mod_announce_odbc.erl:641 -msgid "Update message of the day (don't send)" -msgstr "Rubah pesan harian (tidak dikirim)" - -#: mod_announce.erl:651 mod_announce_odbc.erl:643 -msgid "Update message of the day on all hosts (don't send)" -msgstr "Rubah pesan harian pada semua host (tidak dikirim)" - -#: mod_announce.erl:653 mod_announce_odbc.erl:645 -msgid "Delete message of the day" -msgstr "Hapus pesan harian" - -#: mod_announce.erl:655 mod_announce_odbc.erl:647 -msgid "Delete message of the day on all hosts" -msgstr "Hapus pesan harian pada semua host" - -#: mod_configure.erl:114 mod_configure.erl:274 mod_configure.erl:296 -#: mod_configure.erl:498 -msgid "Configuration" -msgstr "Pengaturan" - -#: mod_configure.erl:125 mod_configure.erl:576 web/ejabberd_web_admin.erl:1936 -msgid "Database" -msgstr "Database" - -#: mod_configure.erl:127 mod_configure.erl:595 -msgid "Start Modules" -msgstr "Memulai Modul" - -#: mod_configure.erl:129 mod_configure.erl:596 -msgid "Stop Modules" -msgstr "Hentikan Modul" - -#: mod_configure.erl:131 mod_configure.erl:604 web/ejabberd_web_admin.erl:1937 -msgid "Backup" -msgstr "Backup" - -#: mod_configure.erl:133 mod_configure.erl:605 -msgid "Restore" -msgstr "Mengembalikan" - -#: mod_configure.erl:135 mod_configure.erl:606 -msgid "Dump to Text File" -msgstr "Dump menjadi File Teks" - -#: mod_configure.erl:137 mod_configure.erl:615 -msgid "Import File" -msgstr "Impor File" - -#: mod_configure.erl:139 mod_configure.erl:616 -msgid "Import Directory" -msgstr "Impor Direktori" - -#: mod_configure.erl:141 mod_configure.erl:581 mod_configure.erl:1057 -msgid "Restart Service" -msgstr "Restart Layanan" - -#: mod_configure.erl:143 mod_configure.erl:582 mod_configure.erl:1102 -msgid "Shut Down Service" -msgstr "Shut Down Layanan" - -#: mod_configure.erl:145 mod_configure.erl:518 mod_configure.erl:1197 -#: web/ejabberd_web_admin.erl:1527 -msgid "Add User" -msgstr "Tambah Pengguna" - -#: mod_configure.erl:147 mod_configure.erl:519 mod_configure.erl:1219 -msgid "Delete User" -msgstr "Hapus Pengguna" - -#: mod_configure.erl:149 mod_configure.erl:520 mod_configure.erl:1231 -msgid "End User Session" -msgstr "Akhir Sesi Pengguna" - -#: mod_configure.erl:151 mod_configure.erl:521 mod_configure.erl:1243 -#: mod_configure.erl:1255 -msgid "Get User Password" -msgstr "Dapatkan User Password" - -#: mod_configure.erl:153 mod_configure.erl:522 -msgid "Change User Password" -msgstr "Ubah User Password" - -#: mod_configure.erl:155 mod_configure.erl:523 mod_configure.erl:1272 -msgid "Get User Last Login Time" -msgstr "Dapatkan Waktu Login Terakhir Pengguna " - -#: mod_configure.erl:157 mod_configure.erl:524 mod_configure.erl:1284 -msgid "Get User Statistics" -msgstr "Dapatkan Statistik Pengguna" - -#: mod_configure.erl:159 mod_configure.erl:525 -msgid "Get Number of Registered Users" -msgstr "Dapatkan Jumlah Pengguna Yang Terdaftar" - -#: mod_configure.erl:161 mod_configure.erl:526 -msgid "Get Number of Online Users" -msgstr "Dapatkan Jumlah User Yang Online" - -#: mod_configure.erl:163 mod_configure.erl:509 web/ejabberd_web_admin.erl:831 -#: web/ejabberd_web_admin.erl:872 -msgid "Access Control Lists" -msgstr "Akses Daftar Pengendalian" - -#: mod_configure.erl:165 mod_configure.erl:510 web/ejabberd_web_admin.erl:940 -#: web/ejabberd_web_admin.erl:976 -msgid "Access Rules" -msgstr "Aturan Akses" - -#: mod_configure.erl:297 mod_configure.erl:499 -msgid "User Management" -msgstr "Manajemen Pengguna" - -#: mod_configure.erl:500 web/ejabberd_web_admin.erl:1058 -#: web/ejabberd_web_admin.erl:1462 -msgid "Online Users" -msgstr "Pengguna Yang Online" - -#: mod_configure.erl:501 -msgid "All Users" -msgstr "Semua Pengguna" - -#: mod_configure.erl:502 -msgid "Outgoing s2s Connections" -msgstr "Koneksi Keluar s2s" - -#: mod_configure.erl:503 web/ejabberd_web_admin.erl:1907 -msgid "Running Nodes" -msgstr "Menjalankan Node" - -#: mod_configure.erl:504 web/ejabberd_web_admin.erl:1909 -msgid "Stopped Nodes" -msgstr "Menghentikan node" - -#: mod_configure.erl:577 mod_configure.erl:587 web/ejabberd_web_admin.erl:1953 -msgid "Modules" -msgstr "Modul" - -#: mod_configure.erl:578 -msgid "Backup Management" -msgstr "Manajemen Backup" - -#: mod_configure.erl:579 -msgid "Import Users From jabberd14 Spool Files" -msgstr "Impor Pengguna Dari jabberd14 Spool File" - -#: mod_configure.erl:699 -msgid "To ~s" -msgstr "Kepada ~s" - -#: mod_configure.erl:717 -msgid "From ~s" -msgstr "Dari ~s" - -#: mod_configure.erl:913 -msgid "Database Tables Configuration at " -msgstr "Database Tabel Konfigurasi pada" - -#: mod_configure.erl:918 -msgid "Choose storage type of tables" -msgstr "Pilih jenis penyimpanan tabel" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Disc only copy" -msgstr "Hanya salinan dari disc" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM and disc copy" -msgstr "RAM dan disc salinan" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM copy" -msgstr "Salinan RAM" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Remote copy" -msgstr "Salinan Remote" - -#: mod_configure.erl:950 -msgid "Stop Modules at " -msgstr "Hentikan Modul pada" - -#: mod_configure.erl:954 -msgid "Choose modules to stop" -msgstr "Pilih Modul untuk berhenti" - -#: mod_configure.erl:969 -msgid "Start Modules at " -msgstr "Mulai Modul pada" - -#: mod_configure.erl:973 -msgid "Enter list of {Module, [Options]}" -msgstr "Masukkan daftar {Modul, [Options]}" - -#: mod_configure.erl:974 -msgid "List of modules to start" -msgstr "Daftar modul untuk memulai" - -#: mod_configure.erl:983 -msgid "Backup to File at " -msgstr "Backup ke File pada" - -#: mod_configure.erl:987 mod_configure.erl:1001 -msgid "Enter path to backup file" -msgstr "Masukkan path untuk file cadangan" - -#: mod_configure.erl:988 mod_configure.erl:1002 mod_configure.erl:1016 -#: mod_configure.erl:1030 -msgid "Path to File" -msgstr "Jalur ke File" - -#: mod_configure.erl:997 -msgid "Restore Backup from File at " -msgstr "Kembalikan Backup dari File pada" - -#: mod_configure.erl:1011 -msgid "Dump Backup to Text File at " -msgstr "Dump Backup ke File Teks di" - -#: mod_configure.erl:1015 -msgid "Enter path to text file" -msgstr "Masukkan path ke file teks" - -#: mod_configure.erl:1025 -msgid "Import User from File at " -msgstr "Impor Pengguna dari File pada" - -#: mod_configure.erl:1029 -msgid "Enter path to jabberd14 spool file" -msgstr "Masukkan path ke file jabberd14 spool" - -#: mod_configure.erl:1039 -msgid "Import Users from Dir at " -msgstr "Impor Pengguna dari Dir di" - -#: mod_configure.erl:1043 -msgid "Enter path to jabberd14 spool dir" -msgstr "Masukkan path ke direktori spool jabberd14" - -#: mod_configure.erl:1044 -msgid "Path to Dir" -msgstr "Jalur ke Dir" - -#: mod_configure.erl:1060 mod_configure.erl:1105 -msgid "Time delay" -msgstr "Waktu tunda" - -#: mod_configure.erl:1143 -msgid "Access Control List Configuration" -msgstr "Konfigurasi Daftar Akses Pengendalian" - -#: mod_configure.erl:1147 -msgid "Access control lists" -msgstr "Daftar Pengendalian Akses" - -#: mod_configure.erl:1171 -msgid "Access Configuration" -msgstr "Akses Konfigurasi" - -#: mod_configure.erl:1175 -msgid "Access rules" -msgstr "Akses peraturan" - -#: mod_configure.erl:1200 mod_configure.erl:1222 mod_configure.erl:1234 -#: mod_configure.erl:1246 mod_configure.erl:1258 mod_configure.erl:1275 -#: mod_configure.erl:1287 mod_configure.erl:1650 mod_configure.erl:1700 -#: mod_configure.erl:1721 mod_roster.erl:943 mod_roster_odbc.erl:1060 -#: mod_vcard.erl:472 mod_vcard_ldap.erl:554 mod_vcard_odbc.erl:448 -msgid "Jabber ID" -msgstr "Jabber ID" - -#: mod_configure.erl:1205 mod_configure.erl:1263 mod_configure.erl:1651 -#: mod_configure.erl:1863 mod_muc/mod_muc_room.erl:3224 mod_register.erl:235 -#: web/ejabberd_web_admin.erl:1520 -msgid "Password" -msgstr "Sandi" - -#: mod_configure.erl:1210 -msgid "Password Verification" -msgstr "Verifikasi Sandi" - -#: mod_configure.erl:1301 -msgid "Number of registered users" -msgstr "Jumlah pengguna terdaftar" - -#: mod_configure.erl:1315 -msgid "Number of online users" -msgstr "Jumlah pengguna online" - -#: mod_configure.erl:1682 web/ejabberd_web_admin.erl:1588 -#: web/ejabberd_web_admin.erl:1743 -msgid "Never" -msgstr "Tidak Pernah" - -#: mod_configure.erl:1696 web/ejabberd_web_admin.erl:1601 -#: web/ejabberd_web_admin.erl:1756 -msgid "Online" -msgstr "Online" - -#: mod_configure.erl:1701 -msgid "Last login" -msgstr "Terakhir Login" - -#: mod_configure.erl:1722 -msgid "Roster size" -msgstr "Ukuran Daftar Kontak" - -#: mod_configure.erl:1723 -msgid "IP addresses" -msgstr "Alamat IP" - -#: mod_configure.erl:1724 -msgid "Resources" -msgstr "Sumber daya" - -#: mod_configure.erl:1850 -msgid "Administration of " -msgstr "Administrasi" - -#: mod_configure.erl:1853 -msgid "Action on user" -msgstr "Tindakan pada pengguna" - -#: mod_configure.erl:1857 -msgid "Edit Properties" -msgstr "Ganti Properti" - -#: mod_configure.erl:1860 web/ejabberd_web_admin.erl:1769 -msgid "Remove User" -msgstr "Hapus Pengguna" - -#: mod_irc/mod_irc.erl:201 mod_irc/mod_irc_odbc.erl:196 -#: mod_muc/mod_muc.erl:336 mod_muc/mod_muc_odbc.erl:342 -msgid "Access denied by service policy" -msgstr "Akses ditolak oleh kebijakan layanan" - -#: mod_irc/mod_irc.erl:401 mod_irc/mod_irc_odbc.erl:398 -msgid "IRC Transport" -msgstr "IRC Transport" - -#: mod_irc/mod_irc.erl:428 mod_irc/mod_irc_odbc.erl:425 -msgid "ejabberd IRC module" -msgstr "ejabberd IRC modul" - -#: mod_irc/mod_irc.erl:559 mod_irc/mod_irc_odbc.erl:568 -msgid "You need an x:data capable client to configure mod_irc settings" -msgstr "" -"Anda memerlukan x:data klien untuk mampu mengkonfigurasi pengaturan mod_irc" - -#: mod_irc/mod_irc.erl:566 mod_irc/mod_irc_odbc.erl:575 -msgid "Registration in mod_irc for " -msgstr "Pendaftaran di mod_irc untuk" - -#: mod_irc/mod_irc.erl:571 mod_irc/mod_irc_odbc.erl:580 -msgid "" -"Enter username, encodings, ports and passwords you wish to use for " -"connecting to IRC servers" -msgstr "" -"Masukkan username, pengkodean, port dan sandi yang ingin Anda gunakan untuk " -"menghubungkan ke layanan IRC" - -#: mod_irc/mod_irc.erl:576 mod_irc/mod_irc_odbc.erl:585 -msgid "IRC Username" -msgstr "Nama Pengguna IRC" - -#: mod_irc/mod_irc.erl:586 mod_irc/mod_irc_odbc.erl:595 -msgid "" -"If you want to specify different ports, passwords, encodings for IRC " -"servers, fill this list with values in format '{\"irc server\", \"encoding" -"\", port, \"password\"}'. By default this service use \"~s\" encoding, port " -"~p, empty password." -msgstr "" -"Jika Anda ingin menentukan port yang berbeda, sandi, pengkodean untuk " -"layanan IRC, isi daftar ini dengan nilai-nilai dalam format '{\"server irc " -"\", \"encoding \", port, \"sandi \"}'. Secara default ini menggunakan " -"layanan \"~s \" pengkodean, port ~p, kata sandi kosong." - -#: mod_irc/mod_irc.erl:598 mod_irc/mod_irc_odbc.erl:607 -msgid "" -"Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef." -"net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]." -msgstr "" -"Contoh: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef." -"net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]." - -#: mod_irc/mod_irc.erl:603 mod_irc/mod_irc_odbc.erl:612 -msgid "Connections parameters" -msgstr "Parameter Koneksi" - -#: mod_irc/mod_irc.erl:728 mod_irc/mod_irc_odbc.erl:757 -msgid "Join IRC channel" -msgstr "Gabung channel IRC" - -#: mod_irc/mod_irc.erl:732 mod_irc/mod_irc_odbc.erl:761 -msgid "IRC channel (don't put the first #)" -msgstr "Channel IRC (tidak perlu menempatkan # sebelumnya)" - -#: mod_irc/mod_irc.erl:737 mod_irc/mod_irc_odbc.erl:766 -msgid "IRC server" -msgstr "Layanan IRC" - -#: mod_irc/mod_irc.erl:770 mod_irc/mod_irc.erl:774 -#: mod_irc/mod_irc_odbc.erl:799 mod_irc/mod_irc_odbc.erl:803 -msgid "Join the IRC channel here." -msgstr "Gabung ke channel IRC disini" - -#: mod_irc/mod_irc.erl:778 mod_irc/mod_irc_odbc.erl:807 -msgid "Join the IRC channel in this Jabber ID: ~s" -msgstr "Gabung ke channel IRC dengan Jabber ID: ~s" - -#: mod_irc/mod_irc.erl:863 mod_irc/mod_irc_odbc.erl:904 -msgid "IRC settings" -msgstr "Pengaturan IRC" - -#: mod_irc/mod_irc.erl:868 mod_irc/mod_irc_odbc.erl:909 -msgid "" -"Enter username and encodings you wish to use for connecting to IRC servers. " -"Press 'Next' to get more fields to fill in. Press 'Complete' to save " -"settings." -msgstr "" -"Masukkan username dan pengkodean yang ingin Anda gunakan untuk menghubungkan " -"ke layanan IRC. Tekan 'Selanjutnya' untuk mendapatkan lagi formulir kemudian " -"Tekan 'Lengkap' untuk menyimpan pengaturan." - -#: mod_irc/mod_irc.erl:874 mod_irc/mod_irc_odbc.erl:915 -msgid "IRC username" -msgstr "Nama Pengguna IRC" - -#: mod_irc/mod_irc.erl:923 mod_irc/mod_irc_odbc.erl:964 -msgid "Password ~b" -msgstr "Kata Sandi ~b" - -#: mod_irc/mod_irc.erl:928 mod_irc/mod_irc_odbc.erl:969 -msgid "Port ~b" -msgstr "Port ~b" - -#: mod_irc/mod_irc.erl:933 mod_irc/mod_irc_odbc.erl:974 -msgid "Encoding for server ~b" -msgstr "Pengkodean untuk layanan ~b" - -#: mod_irc/mod_irc.erl:942 mod_irc/mod_irc_odbc.erl:983 -msgid "Server ~b" -msgstr "Layanan ~b" - -#: mod_muc/mod_muc.erl:449 mod_muc/mod_muc_odbc.erl:456 -msgid "Only service administrators are allowed to send service messages" -msgstr "" -"Layanan hanya diperuntukan kepada administrator yang diizinkan untuk " -"mengirim layanan pesan" - -#: mod_muc/mod_muc.erl:493 mod_muc/mod_muc_odbc.erl:500 -msgid "Room creation is denied by service policy" -msgstr "Pembuatan Ruangan ditolak oleh kebijakan layanan" - -#: mod_muc/mod_muc.erl:500 mod_muc/mod_muc_odbc.erl:507 -msgid "Conference room does not exist" -msgstr "Ruang Konferensi tidak ada" - -#: mod_muc/mod_muc.erl:582 mod_muc/mod_muc_odbc.erl:595 -msgid "Chatrooms" -msgstr "Ruangan Chat" - -#: mod_muc/mod_muc.erl:712 mod_muc/mod_muc_odbc.erl:729 -msgid "You need a client that supports x:data to register the nickname" -msgstr "Anda memerlukan klien yang mendukung x:data untuk mendaftar julukan" - -#: mod_muc/mod_muc.erl:718 mod_muc/mod_muc_odbc.erl:735 -msgid "Nickname Registration at " -msgstr "Pendaftaran Julukan pada" - -#: mod_muc/mod_muc.erl:722 mod_muc/mod_muc_odbc.erl:739 -msgid "Enter nickname you want to register" -msgstr "Masukkan nama julukan Anda jika ingin mendaftar" - -#: mod_muc/mod_muc.erl:723 mod_muc/mod_muc_odbc.erl:740 -#: mod_muc/mod_muc_room.erl:3810 mod_roster.erl:944 mod_roster_odbc.erl:1061 -#: mod_vcard.erl:364 mod_vcard.erl:477 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:453 -msgid "Nickname" -msgstr "Nama Julukan" - -#: mod_muc/mod_muc.erl:762 mod_muc/mod_muc_odbc.erl:784 -#: mod_muc/mod_muc_room.erl:1064 mod_muc/mod_muc_room.erl:1787 -msgid "That nickname is registered by another person" -msgstr "Julukan tersebut telah didaftarkan oleh orang lain" - -#: mod_muc/mod_muc.erl:788 mod_muc/mod_muc_odbc.erl:811 -msgid "You must fill in field \"Nickname\" in the form" -msgstr "Anda harus mengisi kolom \"Julukan\" dalam formulir" - -#: mod_muc/mod_muc.erl:808 mod_muc/mod_muc_odbc.erl:831 -msgid "ejabberd MUC module" -msgstr "ejabberd MUC Module" - -#: mod_muc/mod_muc_log.erl:374 mod_muc/mod_muc_log.erl:381 -msgid "Chatroom configuration modified" -msgstr "Konfigurasi ruang chat diubah" - -#: mod_muc/mod_muc_log.erl:384 -msgid "joins the room" -msgstr "bergabung ke ruangan" - -#: mod_muc/mod_muc_log.erl:387 mod_muc/mod_muc_log.erl:390 -msgid "leaves the room" -msgstr "meninggalkan ruangan" - -#: mod_muc/mod_muc_log.erl:393 mod_muc/mod_muc_log.erl:396 -msgid "has been banned" -msgstr "telah dibanned" - -#: mod_muc/mod_muc_log.erl:399 mod_muc/mod_muc_log.erl:402 -msgid "has been kicked" -msgstr "telah dikick" - -#: mod_muc/mod_muc_log.erl:405 -msgid "has been kicked because of an affiliation change" -msgstr "telah dikick karena perubahan afiliasi" - -#: mod_muc/mod_muc_log.erl:408 -msgid "has been kicked because the room has been changed to members-only" -msgstr "telah dikick karena ruangan telah diubah menjadi hanya untuk member" - -#: mod_muc/mod_muc_log.erl:411 -msgid "has been kicked because of a system shutdown" -msgstr "telah dikick karena sistem shutdown" - -#: mod_muc/mod_muc_log.erl:414 -msgid "is now known as" -msgstr "sekarang dikenal sebagai" - -#: mod_muc/mod_muc_log.erl:417 mod_muc/mod_muc_log.erl:688 -#: mod_muc/mod_muc_room.erl:2393 -msgid " has set the subject to: " -msgstr "telah menetapkan topik yaitu:" - -#: mod_muc/mod_muc_log.erl:452 -msgid "Chatroom is created" -msgstr "Ruang chat telah dibuat" - -#: mod_muc/mod_muc_log.erl:453 -msgid "Chatroom is destroyed" -msgstr "Ruang chat dilenyapkan" - -#: mod_muc/mod_muc_log.erl:454 -msgid "Chatroom is started" -msgstr "Ruang chat dimulai" - -#: mod_muc/mod_muc_log.erl:455 -msgid "Chatroom is stopped" -msgstr "Ruang chat dihentikan" - -#: mod_muc/mod_muc_log.erl:459 -msgid "Monday" -msgstr "Senin" - -#: mod_muc/mod_muc_log.erl:460 -msgid "Tuesday" -msgstr "Selasa" - -#: mod_muc/mod_muc_log.erl:461 -msgid "Wednesday" -msgstr "Rabu" - -#: mod_muc/mod_muc_log.erl:462 -msgid "Thursday" -msgstr "Kamis" - -#: mod_muc/mod_muc_log.erl:463 -msgid "Friday" -msgstr "Jumat" - -#: mod_muc/mod_muc_log.erl:464 -msgid "Saturday" -msgstr "Sabtu" - -#: mod_muc/mod_muc_log.erl:465 -msgid "Sunday" -msgstr "Minggu" - -#: mod_muc/mod_muc_log.erl:469 -msgid "January" -msgstr "Januari" - -#: mod_muc/mod_muc_log.erl:470 -msgid "February" -msgstr "Februari" - -#: mod_muc/mod_muc_log.erl:471 -msgid "March" -msgstr "Maret" - -#: mod_muc/mod_muc_log.erl:472 -msgid "April" -msgstr "April" - -#: mod_muc/mod_muc_log.erl:473 -msgid "May" -msgstr "Mei" - -#: mod_muc/mod_muc_log.erl:474 -msgid "June" -msgstr "Juni" - -#: mod_muc/mod_muc_log.erl:475 -msgid "July" -msgstr "Juli" - -#: mod_muc/mod_muc_log.erl:476 -msgid "August" -msgstr "Agustus" - -#: mod_muc/mod_muc_log.erl:477 -msgid "September" -msgstr "September" - -#: mod_muc/mod_muc_log.erl:478 -msgid "October" -msgstr "Oktober" - -#: mod_muc/mod_muc_log.erl:479 -msgid "November" -msgstr "Nopember" - -#: mod_muc/mod_muc_log.erl:480 -msgid "December" -msgstr "Desember" - -#: mod_muc/mod_muc_log.erl:750 -msgid "Room Configuration" -msgstr "Konfigurasi Ruangan" - -#: mod_muc/mod_muc_log.erl:759 -msgid "Room Occupants" -msgstr "Penghuni Ruangan" - -#: mod_muc/mod_muc_room.erl:174 -msgid "Traffic rate limit is exceeded" -msgstr "Lalu lintas melebihi batas" - -#: mod_muc/mod_muc_room.erl:246 -msgid "" -"This participant is kicked from the room because he sent an error message" -msgstr "Peserta ini dikick dari ruangan karena dia mengirim pesan kesalahan" - -#: mod_muc/mod_muc_room.erl:255 -msgid "It is not allowed to send private messages to the conference" -msgstr "Hal ini tidak diperbolehkan untuk mengirim pesan pribadi ke konferensi" - -#: mod_muc/mod_muc_room.erl:332 -msgid "Please, wait for a while before sending new voice request" -msgstr "" - -#: mod_muc/mod_muc_room.erl:347 -msgid "Voice requests are disabled in this conference" -msgstr "" - -#: mod_muc/mod_muc_room.erl:364 -msgid "Failed to extract JID from your voice request approval" -msgstr "" - -#: mod_muc/mod_muc_room.erl:393 -msgid "Only moderators can approve voice requests" -msgstr "" - -#: mod_muc/mod_muc_room.erl:408 -msgid "Improper message type" -msgstr "Jenis pesan yang tidak benar" - -#: mod_muc/mod_muc_room.erl:518 -msgid "" -"This participant is kicked from the room because he sent an error message to " -"another participant" -msgstr "" -"Participant ini dikick dari ruangan karena ia mengirim pesan kesalahan ke " -"participant lain" - -#: mod_muc/mod_muc_room.erl:531 -msgid "It is not allowed to send private messages of type \"groupchat\"" -msgstr "" -"Hal ini tidak diperbolehkan untuk mengirim pesan pribadi jenis \"groupchat \"" - -#: mod_muc/mod_muc_room.erl:543 mod_muc/mod_muc_room.erl:611 -msgid "Recipient is not in the conference room" -msgstr "Penerima tidak berada di ruangan konferensi" - -#: mod_muc/mod_muc_room.erl:564 mod_muc/mod_muc_room.erl:585 -msgid "It is not allowed to send private messages" -msgstr "Hal ini tidak diperbolehkan untuk mengirim pesan pribadi" - -#: mod_muc/mod_muc_room.erl:576 mod_muc/mod_muc_room.erl:956 -#: mod_muc/mod_muc_room.erl:4040 -msgid "Only occupants are allowed to send messages to the conference" -msgstr "Hanya penghuni yang diizinkan untuk mengirim pesan ke konferensi" - -#: mod_muc/mod_muc_room.erl:634 -msgid "Only occupants are allowed to send queries to the conference" -msgstr "Hanya penghuni diizinkan untuk mengirim permintaan ke konferensi" - -#: mod_muc/mod_muc_room.erl:646 -msgid "Queries to the conference members are not allowed in this room" -msgstr "" -"Permintaan untuk para anggota konferensi tidak diperbolehkan di ruangan ini" - -#: mod_muc/mod_muc_room.erl:932 -msgid "" -"Only moderators and participants are allowed to change the subject in this " -"room" -msgstr "" -"Hanya moderator dan peserta yang diizinkan untuk mengganti topik pembicaraan " -"di ruangan ini" - -#: mod_muc/mod_muc_room.erl:937 -msgid "Only moderators are allowed to change the subject in this room" -msgstr "" -"Hanya moderator yang diperbolehkan untuk mengubah topik dalam ruangan ini" - -#: mod_muc/mod_muc_room.erl:947 -msgid "Visitors are not allowed to send messages to all occupants" -msgstr "Visitor tidak diperbolehkan untuk mengirim pesan ke semua penghuni" - -#: mod_muc/mod_muc_room.erl:1021 -msgid "" -"This participant is kicked from the room because he sent an error presence" -msgstr "" -"Participant ini dikick dari ruangan karena ia mengirim kehadiran kesalahan" - -#: mod_muc/mod_muc_room.erl:1040 -msgid "Visitors are not allowed to change their nicknames in this room" -msgstr "Visitor tidak diperbolehkan untuk mengubah nama julukan di ruangan ini" - -#: mod_muc/mod_muc_room.erl:1053 mod_muc/mod_muc_room.erl:1779 -msgid "That nickname is already in use by another occupant" -msgstr "Julukan itu sudah digunakan oleh penghuni lain" - -#: mod_muc/mod_muc_room.erl:1768 -msgid "You have been banned from this room" -msgstr "Anda telah diblokir dari ruangan ini" - -#: mod_muc/mod_muc_room.erl:1771 -msgid "Membership is required to enter this room" -msgstr "Hanya Member yang dapat masuk ruangan ini" - -#: mod_muc/mod_muc_room.erl:1807 -msgid "This room is not anonymous" -msgstr "Ruangan ini tidak dikenal" - -#: mod_muc/mod_muc_room.erl:1833 -msgid "A password is required to enter this room" -msgstr "Diperlukan kata sandi untuk masuk ruangan ini" - -#: mod_muc/mod_muc_room.erl:1855 mod_register.erl:246 -msgid "Too many CAPTCHA requests" -msgstr "" - -#: mod_muc/mod_muc_room.erl:1864 mod_register.erl:251 -msgid "Unable to generate a CAPTCHA" -msgstr "Tidak dapat menghasilkan CAPTCHA" - -#: mod_muc/mod_muc_room.erl:1874 -msgid "Incorrect password" -msgstr "Kata sandi salah" - -#: mod_muc/mod_muc_room.erl:2448 -msgid "Administrator privileges required" -msgstr "Hak istimewa Administrator dibutuhkan" - -#: mod_muc/mod_muc_room.erl:2463 -msgid "Moderator privileges required" -msgstr "Hak istimewa moderator dibutuhkan" - -#: mod_muc/mod_muc_room.erl:2619 -msgid "Jabber ID ~s is invalid" -msgstr "Jabber ID ~s tidak valid" - -#: mod_muc/mod_muc_room.erl:2633 -msgid "Nickname ~s does not exist in the room" -msgstr "Nama Julukan ~s tidak berada di dalam ruangan" - -#: mod_muc/mod_muc_room.erl:2659 mod_muc/mod_muc_room.erl:3049 -msgid "Invalid affiliation: ~s" -msgstr "Afiliasi tidak valid: ~s" - -#: mod_muc/mod_muc_room.erl:2713 -msgid "Invalid role: ~s" -msgstr "Peran tidak valid: ~s" - -#: mod_muc/mod_muc_room.erl:3026 mod_muc/mod_muc_room.erl:3062 -msgid "Owner privileges required" -msgstr "Hak istimewa owner dibutuhkan" - -#: mod_muc/mod_muc_room.erl:3195 -msgid "Configuration of room ~s" -msgstr "Pengaturan ruangan ~s" - -#: mod_muc/mod_muc_room.erl:3200 -msgid "Room title" -msgstr "Nama Ruangan" - -#: mod_muc/mod_muc_room.erl:3203 mod_muc/mod_muc_room.erl:3692 -msgid "Room description" -msgstr "Keterangan ruangan" - -#: mod_muc/mod_muc_room.erl:3210 -msgid "Make room persistent" -msgstr "Buat ruangan menjadi permanent" - -#: mod_muc/mod_muc_room.erl:3215 -msgid "Make room public searchable" -msgstr "Buat ruangan dapat dicari" - -#: mod_muc/mod_muc_room.erl:3218 -msgid "Make participants list public" -msgstr "Buat daftar participant diketahui oleh public" - -#: mod_muc/mod_muc_room.erl:3221 -msgid "Make room password protected" -msgstr "Buat ruangan yang dilindungi dengan kata sandi" - -#: mod_muc/mod_muc_room.erl:3232 -msgid "Maximum Number of Occupants" -msgstr "Maksimum Jumlah Penghuni" - -#: mod_muc/mod_muc_room.erl:3239 -msgid "No limit" -msgstr "Tidak terbatas" - -#: mod_muc/mod_muc_room.erl:3250 -msgid "Present real Jabber IDs to" -msgstr "Tampilkan Jabber ID secara lengkap" - -#: mod_muc/mod_muc_room.erl:3258 mod_muc/mod_muc_room.erl:3292 -msgid "moderators only" -msgstr "Hanya moderator" - -#: mod_muc/mod_muc_room.erl:3260 mod_muc/mod_muc_room.erl:3294 -msgid "anyone" -msgstr "Siapapun" - -#: mod_muc/mod_muc_room.erl:3262 -msgid "Make room members-only" -msgstr "Buat ruangan hanya untuk member saja" - -#: mod_muc/mod_muc_room.erl:3265 -msgid "Make room moderated" -msgstr "Buat ruangan hanya untuk moderator saja" - -#: mod_muc/mod_muc_room.erl:3268 -msgid "Default users as participants" -msgstr "pengguna pertama kali masuk sebagai participant" - -#: mod_muc/mod_muc_room.erl:3271 -msgid "Allow users to change the subject" -msgstr "Perbolehkan pengguna untuk mengganti topik" - -#: mod_muc/mod_muc_room.erl:3274 -msgid "Allow users to send private messages" -msgstr "perbolehkan pengguna mengirimkan pesan ke pengguna lain secara pribadi" - -#: mod_muc/mod_muc_room.erl:3279 -#, fuzzy -msgid "Allow visitors to send private messages to" -msgstr "perbolehkan pengguna mengirimkan pesan ke pengguna lain secara pribadi" - -#: mod_muc/mod_muc_room.erl:3290 -msgid "nobody" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3296 -msgid "Allow users to query other users" -msgstr "Perbolehkan pengguna untuk mengetahui pengguna lain" - -#: mod_muc/mod_muc_room.erl:3299 -msgid "Allow users to send invites" -msgstr "Perbolehkan pengguna mengirimkan undangan" - -#: mod_muc/mod_muc_room.erl:3302 -msgid "Allow visitors to send status text in presence updates" -msgstr "Izinkan pengunjung untuk mengirim teks status terbaru" - -#: mod_muc/mod_muc_room.erl:3305 -msgid "Allow visitors to change nickname" -msgstr "Perbolehkan visitor mengganti nama julukan" - -#: mod_muc/mod_muc_room.erl:3308 -#, fuzzy -msgid "Allow visitors to send voice requests" -msgstr "Perbolehkan pengguna mengirimkan undangan" - -#: mod_muc/mod_muc_room.erl:3311 -msgid "Minimum interval between voice requests (in seconds)" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3317 -msgid "Make room CAPTCHA protected" -msgstr "Buat ruangan dilindungi dengan CAPTCHA" - -#: mod_muc/mod_muc_room.erl:3322 -msgid "Exclude Jabber IDs from CAPTCHA challenge" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3329 -msgid "Enable logging" -msgstr "Aktifkan catatan" - -#: mod_muc/mod_muc_room.erl:3337 -msgid "You need an x:data capable client to configure room" -msgstr "Anda memerlukan x:data klien untuk dapat mengkonfigurasi ruangan" - -#: mod_muc/mod_muc_room.erl:3694 -msgid "Number of occupants" -msgstr "Jumlah Penghuni" - -#: mod_muc/mod_muc_room.erl:3750 -msgid "private, " -msgstr "pribadi, " - -#: mod_muc/mod_muc_room.erl:3799 -msgid "Voice request" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3803 -msgid "Either approve or decline the voice request." -msgstr "" - -#: mod_muc/mod_muc_room.erl:3809 -#, fuzzy -msgid "User JID" -msgstr "Pengguna" - -#: mod_muc/mod_muc_room.erl:3811 -msgid "Grant voice to this person?" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3960 -msgid "~s invites you to the room ~s" -msgstr "~s mengundang anda ke ruangan ~s" - -#: mod_muc/mod_muc_room.erl:3969 -msgid "the password is" -msgstr "kata sandi yaitu:" - -#: mod_offline.erl:510 mod_offline_odbc.erl:352 -msgid "" -"Your contact offline message queue is full. The message has been discarded." -msgstr "" -"Kontak offline Anda pada antrian pesan sudah penuh. Pesan telah dibuang." - -#: mod_offline.erl:560 mod_offline_odbc.erl:408 -msgid "~s's Offline Messages Queue" -msgstr "Antrian Pesan Offline ~s" - -#: mod_offline.erl:563 mod_offline_odbc.erl:411 mod_roster.erl:987 -#: mod_roster_odbc.erl:1104 mod_shared_roster.erl:901 -#: mod_shared_roster.erl:1010 mod_shared_roster_odbc.erl:930 -#: mod_shared_roster_odbc.erl:1039 web/ejabberd_web_admin.erl:833 -#: web/ejabberd_web_admin.erl:874 web/ejabberd_web_admin.erl:942 -#: web/ejabberd_web_admin.erl:978 web/ejabberd_web_admin.erl:1019 -#: web/ejabberd_web_admin.erl:1508 web/ejabberd_web_admin.erl:1760 -#: web/ejabberd_web_admin.erl:1931 web/ejabberd_web_admin.erl:1963 -#: web/ejabberd_web_admin.erl:2031 web/ejabberd_web_admin.erl:2135 -#: web/ejabberd_web_admin.erl:2160 web/ejabberd_web_admin.erl:2248 -msgid "Submitted" -msgstr "Ulangi masukan" - -#: mod_offline.erl:571 -msgid "Time" -msgstr "Waktu" - -#: mod_offline.erl:572 -msgid "From" -msgstr "Dari" - -#: mod_offline.erl:573 -msgid "To" -msgstr "Kepada" - -#: mod_offline.erl:574 mod_offline_odbc.erl:419 -msgid "Packet" -msgstr "Paket" - -#: mod_offline.erl:587 mod_offline_odbc.erl:432 mod_shared_roster.erl:908 -#: mod_shared_roster_odbc.erl:937 web/ejabberd_web_admin.erl:882 -#: web/ejabberd_web_admin.erl:986 -msgid "Delete Selected" -msgstr "Hapus Yang Terpilih" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -msgid "Offline Messages:" -msgstr "Pesan Offline:" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -msgid "Remove All Offline Messages" -msgstr "Hapus Semua Pesan Offline" - -#: mod_proxy65/mod_proxy65_service.erl:213 -msgid "ejabberd SOCKS5 Bytestreams module" -msgstr "modul ejabberd SOCKS5 Bytestreams" - -#: mod_pubsub/mod_pubsub.erl:1120 mod_pubsub/mod_pubsub_odbc.erl:929 -msgid "Publish-Subscribe" -msgstr "Setujui-Pertemanan" - -#: mod_pubsub/mod_pubsub.erl:1214 mod_pubsub/mod_pubsub_odbc.erl:1025 -msgid "ejabberd Publish-Subscribe module" -msgstr "Modul ejabberd Setujui-Pertemanan" - -#: mod_pubsub/mod_pubsub.erl:1497 mod_pubsub/mod_pubsub_odbc.erl:1312 -msgid "PubSub subscriber request" -msgstr "Permintaan pertemanan PubSub" - -#: mod_pubsub/mod_pubsub.erl:1499 mod_pubsub/mod_pubsub_odbc.erl:1314 -msgid "Choose whether to approve this entity's subscription." -msgstr "Pilih apakah akan menyetujui hubungan pertemanan ini." - -#: mod_pubsub/mod_pubsub.erl:1505 mod_pubsub/mod_pubsub_odbc.erl:1320 -msgid "Node ID" -msgstr "ID Node" - -#: mod_pubsub/mod_pubsub.erl:1510 mod_pubsub/mod_pubsub_odbc.erl:1325 -msgid "Subscriber Address" -msgstr "Alamat Pertemanan" - -#: mod_pubsub/mod_pubsub.erl:1516 mod_pubsub/mod_pubsub_odbc.erl:1331 -msgid "Allow this Jabber ID to subscribe to this pubsub node?" -msgstr "Izinkan ID Jabber ini untuk berlangganan pada node pubsub ini?" - -#: mod_pubsub/mod_pubsub.erl:3474 mod_pubsub/mod_pubsub_odbc.erl:3287 -msgid "Deliver payloads with event notifications" -msgstr "Memberikan muatan dengan pemberitahuan acara" - -#: mod_pubsub/mod_pubsub.erl:3475 mod_pubsub/mod_pubsub_odbc.erl:3288 -msgid "Deliver event notifications" -msgstr "Memberikan pemberitahuan acara" - -#: mod_pubsub/mod_pubsub.erl:3476 mod_pubsub/mod_pubsub_odbc.erl:3289 -msgid "Notify subscribers when the node configuration changes" -msgstr "Beritahu pelanggan ketika ada perubahan konfigurasi node" - -#: mod_pubsub/mod_pubsub.erl:3477 mod_pubsub/mod_pubsub_odbc.erl:3290 -msgid "Notify subscribers when the node is deleted" -msgstr "Beritahu pelanggan ketika node dihapus" - -#: mod_pubsub/mod_pubsub.erl:3478 mod_pubsub/mod_pubsub_odbc.erl:3291 -msgid "Notify subscribers when items are removed from the node" -msgstr "Beritahu pelanggan ketika item tersebut dikeluarkan dari node" - -#: mod_pubsub/mod_pubsub.erl:3479 mod_pubsub/mod_pubsub_odbc.erl:3292 -msgid "Persist items to storage" -msgstr "Pertahankan item ke penyimpanan" - -#: mod_pubsub/mod_pubsub.erl:3480 mod_pubsub/mod_pubsub_odbc.erl:3293 -msgid "A friendly name for the node" -msgstr "Nama yang dikenal untuk node" - -#: mod_pubsub/mod_pubsub.erl:3481 mod_pubsub/mod_pubsub_odbc.erl:3294 -msgid "Max # of items to persist" -msgstr "Max item untuk bertahan" - -#: mod_pubsub/mod_pubsub.erl:3482 mod_pubsub/mod_pubsub_odbc.erl:3295 -msgid "Whether to allow subscriptions" -msgstr "Apakah diperbolehkan untuk berlangganan" - -#: mod_pubsub/mod_pubsub.erl:3483 mod_pubsub/mod_pubsub_odbc.erl:3296 -msgid "Specify the access model" -msgstr "Tentukan model akses" - -#: mod_pubsub/mod_pubsub.erl:3486 mod_pubsub/mod_pubsub_odbc.erl:3299 -msgid "Roster groups allowed to subscribe" -msgstr "Kelompok kontak yang diizinkan untuk berlangganan" - -#: mod_pubsub/mod_pubsub.erl:3487 mod_pubsub/mod_pubsub_odbc.erl:3300 -msgid "Specify the publisher model" -msgstr "Tentukan model penerbitan" - -#: mod_pubsub/mod_pubsub.erl:3489 mod_pubsub/mod_pubsub_odbc.erl:3302 -msgid "Purge all items when the relevant publisher goes offline" -msgstr "Bersihkan semua item ketika penerbit yang relevan telah offline" - -#: mod_pubsub/mod_pubsub.erl:3490 mod_pubsub/mod_pubsub_odbc.erl:3303 -msgid "Specify the event message type" -msgstr "Tentukan jenis acara pesan" - -#: mod_pubsub/mod_pubsub.erl:3492 mod_pubsub/mod_pubsub_odbc.erl:3305 -msgid "Max payload size in bytes" -msgstr "Max kapasitas ukuran dalam bytes" - -#: mod_pubsub/mod_pubsub.erl:3493 mod_pubsub/mod_pubsub_odbc.erl:3306 -msgid "When to send the last published item" -msgstr "Ketika untuk mengirim item terakhir yang dipublikasikan" - -#: mod_pubsub/mod_pubsub.erl:3495 mod_pubsub/mod_pubsub_odbc.erl:3308 -msgid "Only deliver notifications to available users" -msgstr "Hanya mengirimkan pemberitahuan kepada pengguna yang tersedia" - -#: mod_pubsub/mod_pubsub.erl:3496 mod_pubsub/mod_pubsub_odbc.erl:3309 -msgid "The collections with which a node is affiliated" -msgstr "Koleksi dengan yang berafiliasi dengan sebuah node" - -#: mod_register.erl:193 -msgid "The CAPTCHA verification has failed" -msgstr "Verifikasi CAPTCHA telah gagal" - -#: mod_register.erl:220 -msgid "You need a client that supports x:data and CAPTCHA to register" -msgstr "" -"Anda memerlukan klien yang mendukung x:data dan CAPTCHA untuk mendaftar" - -#: mod_register.erl:226 mod_register.erl:265 -msgid "Choose a username and password to register with this server" -msgstr "Pilih nama pengguna dan kata sandi untuk mendaftar dengan layanan ini" - -#: mod_register.erl:230 mod_vcard.erl:364 mod_vcard_odbc.erl:342 -#: web/ejabberd_web_admin.erl:1515 web/ejabberd_web_admin.erl:1572 -msgid "User" -msgstr "Pengguna" - -#: mod_register.erl:316 mod_register.erl:361 -msgid "The password is too weak" -msgstr "Kata sandi terlalu lemah" - -#: mod_register.erl:365 -msgid "Users are not allowed to register accounts so quickly" -msgstr "Pengguna tidak diperkenankan untuk mendaftar akun begitu cepat" - -#: mod_roster.erl:938 mod_roster_odbc.erl:1055 web/ejabberd_web_admin.erl:1701 -#: web/ejabberd_web_admin.erl:1886 web/ejabberd_web_admin.erl:1897 -#: web/ejabberd_web_admin.erl:2219 -msgid "None" -msgstr "Tak satupun" - -#: mod_roster.erl:945 mod_roster_odbc.erl:1062 -msgid "Subscription" -msgstr "Berlangganan" - -#: mod_roster.erl:946 mod_roster_odbc.erl:1063 -msgid "Pending" -msgstr "Tertunda" - -#: mod_roster.erl:947 mod_roster_odbc.erl:1064 -msgid "Groups" -msgstr "Grup" - -#: mod_roster.erl:974 mod_roster_odbc.erl:1091 -msgid "Validate" -msgstr "Mengesahkan" - -#: mod_roster.erl:982 mod_roster_odbc.erl:1099 -msgid "Remove" -msgstr "Menghapus" - -#: mod_roster.erl:985 mod_roster_odbc.erl:1102 -msgid "Roster of " -msgstr "Kontak dari" - -#: mod_roster.erl:988 mod_roster_odbc.erl:1105 mod_shared_roster.erl:902 -#: mod_shared_roster.erl:1011 mod_shared_roster_odbc.erl:931 -#: mod_shared_roster_odbc.erl:1040 web/ejabberd_web_admin.erl:834 -#: web/ejabberd_web_admin.erl:875 web/ejabberd_web_admin.erl:943 -#: web/ejabberd_web_admin.erl:979 web/ejabberd_web_admin.erl:1020 -#: web/ejabberd_web_admin.erl:1509 web/ejabberd_web_admin.erl:1761 -#: web/ejabberd_web_admin.erl:1932 web/ejabberd_web_admin.erl:2136 -#: web/ejabberd_web_admin.erl:2161 -msgid "Bad format" -msgstr "Format yang buruk" - -#: mod_roster.erl:995 mod_roster_odbc.erl:1112 -msgid "Add Jabber ID" -msgstr "Tambah Jabber ID" - -#: mod_roster.erl:1094 mod_roster_odbc.erl:1211 -msgid "Roster" -msgstr "Kontak" - -#: mod_shared_roster.erl:857 mod_shared_roster.erl:899 -#: mod_shared_roster.erl:1007 mod_shared_roster_odbc.erl:886 -#: mod_shared_roster_odbc.erl:928 mod_shared_roster_odbc.erl:1036 -msgid "Shared Roster Groups" -msgstr "Berbagi grup kontak" - -#: mod_shared_roster.erl:895 mod_shared_roster_odbc.erl:924 -#: web/ejabberd_web_admin.erl:1365 web/ejabberd_web_admin.erl:2461 -msgid "Add New" -msgstr "Tambah Baru" - -#: mod_shared_roster.erl:978 mod_shared_roster_odbc.erl:1007 -msgid "Name:" -msgstr "Nama:" - -#: mod_shared_roster.erl:983 mod_shared_roster_odbc.erl:1012 -msgid "Description:" -msgstr "Keterangan:" - -#: mod_shared_roster.erl:991 mod_shared_roster_odbc.erl:1020 -msgid "Members:" -msgstr "Anggota:" - -#: mod_shared_roster.erl:999 mod_shared_roster_odbc.erl:1028 -msgid "Displayed Groups:" -msgstr "Tampilkan Grup:" - -#: mod_shared_roster.erl:1008 mod_shared_roster_odbc.erl:1037 -msgid "Group " -msgstr "Grup" - -#: mod_shared_roster.erl:1017 mod_shared_roster_odbc.erl:1046 -#: web/ejabberd_web_admin.erl:840 web/ejabberd_web_admin.erl:884 -#: web/ejabberd_web_admin.erl:949 web/ejabberd_web_admin.erl:1026 -#: web/ejabberd_web_admin.erl:2017 -msgid "Submit" -msgstr "Serahkan" - -#: mod_vcard.erl:165 mod_vcard_ldap.erl:238 mod_vcard_odbc.erl:129 -msgid "Erlang Jabber Server" -msgstr "Layanan Erlang Jabber" - -#: mod_vcard.erl:364 mod_vcard.erl:478 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:454 -msgid "Birthday" -msgstr "Hari Lahir" - -#: mod_vcard.erl:364 mod_vcard.erl:480 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:456 -msgid "City" -msgstr "Kota" - -#: mod_vcard.erl:364 mod_vcard.erl:479 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:455 -msgid "Country" -msgstr "Negara" - -#: mod_vcard.erl:364 mod_vcard.erl:481 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:457 -msgid "Email" -msgstr "Email" - -#: mod_vcard.erl:364 mod_vcard.erl:476 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:452 -msgid "Family Name" -msgstr "Nama Keluarga (marga)" - -#: mod_vcard.erl:364 mod_vcard_odbc.erl:342 -msgid "" -"Fill in the form to search for any matching Jabber User (Add * to the end of " -"field to match substring)" -msgstr "" -"Isi formulir untuk pencarian pengguna Jabber yang cocok (Tambahkan * ke " -"mengakhiri pengisian untuk menyamakan kata)" - -#: mod_vcard.erl:364 mod_vcard.erl:473 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:449 -msgid "Full Name" -msgstr "Nama Lengkap" - -#: mod_vcard.erl:364 mod_vcard.erl:475 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:451 -msgid "Middle Name" -msgstr "Nama Tengah" - -#: mod_vcard.erl:364 mod_vcard.erl:474 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:450 web/ejabberd_web_admin.erl:2006 -msgid "Name" -msgstr "Nama" - -#: mod_vcard.erl:364 mod_vcard.erl:482 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:458 -msgid "Organization Name" -msgstr "Nama Organisasi" - -#: mod_vcard.erl:364 mod_vcard.erl:483 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:459 -msgid "Organization Unit" -msgstr "Unit Organisasi" - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "Search users in " -msgstr "Pencarian pengguna dalam" - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "You need an x:data capable client to search" -msgstr "Anda memerlukan x:data klien untuk melakukan pencarian" - -#: mod_vcard.erl:389 mod_vcard_ldap.erl:487 mod_vcard_odbc.erl:367 -msgid "vCard User Search" -msgstr "vCard Pencarian Pengguna" - -#: mod_vcard.erl:445 mod_vcard_ldap.erl:541 mod_vcard_odbc.erl:421 -msgid "ejabberd vCard module" -msgstr "Modul ejabberd vCard" - -#: mod_vcard.erl:469 mod_vcard_ldap.erl:551 mod_vcard_odbc.erl:445 -msgid "Search Results for " -msgstr "Hasil Pencarian untuk" - -#: mod_vcard_ldap.erl:462 -msgid "Fill in fields to search for any matching Jabber User" -msgstr "Isi kolom untuk mencari pengguna Jabber yang sama" - -#: web/ejabberd_web_admin.erl:193 web/ejabberd_web_admin.erl:203 -#: web/ejabberd_web_admin.erl:219 web/ejabberd_web_admin.erl:229 -msgid "Unauthorized" -msgstr "Ditolak" - -#: web/ejabberd_web_admin.erl:286 web/ejabberd_web_admin.erl:303 -msgid "ejabberd Web Admin" -msgstr "Admin Web ejabberd" - -#: web/ejabberd_web_admin.erl:769 web/ejabberd_web_admin.erl:780 -msgid "Administration" -msgstr "Administrasi" - -#: web/ejabberd_web_admin.erl:878 web/ejabberd_web_admin.erl:982 -msgid "Raw" -msgstr "mentah" - -#: web/ejabberd_web_admin.erl:1017 -msgid "~s access rule configuration" -msgstr "~s aturan akses konfigurasi" - -#: web/ejabberd_web_admin.erl:1035 -msgid "Virtual Hosts" -msgstr "Virtual Hosts" - -#: web/ejabberd_web_admin.erl:1043 web/ejabberd_web_admin.erl:1050 -msgid "Users" -msgstr "Pengguna" - -#: web/ejabberd_web_admin.erl:1078 -msgid "Users Last Activity" -msgstr "Aktifitas terakhir para pengguna" - -#: web/ejabberd_web_admin.erl:1080 -msgid "Period: " -msgstr "Periode:" - -#: web/ejabberd_web_admin.erl:1090 -msgid "Last month" -msgstr "Akhir bulan" - -#: web/ejabberd_web_admin.erl:1091 -msgid "Last year" -msgstr "Akhir tahun" - -#: web/ejabberd_web_admin.erl:1092 -msgid "All activity" -msgstr "Semua aktifitas" - -#: web/ejabberd_web_admin.erl:1094 -msgid "Show Ordinary Table" -msgstr "Tampilkan Tabel Normal" - -#: web/ejabberd_web_admin.erl:1096 -msgid "Show Integral Table" -msgstr "Tampilkan Tabel Terpisah" - -#: web/ejabberd_web_admin.erl:1105 web/ejabberd_web_admin.erl:1939 -msgid "Statistics" -msgstr "Statistik" - -#: web/ejabberd_web_admin.erl:1117 -msgid "Not Found" -msgstr "Tidak Ditemukan" - -#: web/ejabberd_web_admin.erl:1134 -msgid "Node not found" -msgstr "Node tidak ditemukan" - -#: web/ejabberd_web_admin.erl:1460 -msgid "Host" -msgstr "Host" - -#: web/ejabberd_web_admin.erl:1461 -msgid "Registered Users" -msgstr "Pengguna Terdaftar" - -#: web/ejabberd_web_admin.erl:1573 -msgid "Offline Messages" -msgstr "Pesan Offline" - -#: web/ejabberd_web_admin.erl:1574 web/ejabberd_web_admin.erl:1767 -msgid "Last Activity" -msgstr "Aktifitas Terakhir" - -#: web/ejabberd_web_admin.erl:1659 web/ejabberd_web_admin.erl:1675 -msgid "Registered Users:" -msgstr "Pengguna Terdaftar:" - -#: web/ejabberd_web_admin.erl:1661 web/ejabberd_web_admin.erl:1677 -#: web/ejabberd_web_admin.erl:2192 -msgid "Online Users:" -msgstr "Pengguna Online:" - -#: web/ejabberd_web_admin.erl:1663 -msgid "Outgoing s2s Connections:" -msgstr "Koneksi s2s yang keluar:" - -#: web/ejabberd_web_admin.erl:1665 -msgid "Outgoing s2s Servers:" -msgstr "Layanan s2s yang keluar:" - -#: web/ejabberd_web_admin.erl:1734 web/mod_register_web.erl:175 -#: web/mod_register_web.erl:372 web/mod_register_web.erl:381 -#: web/mod_register_web.erl:411 -msgid "Change Password" -msgstr "Ubah Kata Sandi" - -#: web/ejabberd_web_admin.erl:1758 -msgid "User " -msgstr "Pengguna" - -#: web/ejabberd_web_admin.erl:1765 -msgid "Connected Resources:" -msgstr "Sumber Daya Terhubung:" - -#: web/ejabberd_web_admin.erl:1766 web/mod_register_web.erl:227 -#: web/mod_register_web.erl:519 -msgid "Password:" -msgstr "Kata Sandi:" - -#: web/ejabberd_web_admin.erl:1828 -msgid "No Data" -msgstr "Tidak Ada Data" - -#: web/ejabberd_web_admin.erl:1906 -msgid "Nodes" -msgstr "Node-node" - -#: web/ejabberd_web_admin.erl:1929 web/ejabberd_web_admin.erl:1951 -msgid "Node " -msgstr "Node" - -#: web/ejabberd_web_admin.erl:1938 -msgid "Listened Ports" -msgstr "Port Terdeteksi" - -#: web/ejabberd_web_admin.erl:1940 web/ejabberd_web_admin.erl:2260 -#: web/ejabberd_web_admin.erl:2448 -msgid "Update" -msgstr "Memperbarui" - -#: web/ejabberd_web_admin.erl:1943 web/ejabberd_web_admin.erl:2569 -msgid "Restart" -msgstr "Jalankan Ulang" - -#: web/ejabberd_web_admin.erl:1945 web/ejabberd_web_admin.erl:2571 -msgid "Stop" -msgstr "Hentikan" - -#: web/ejabberd_web_admin.erl:1959 -msgid "RPC Call Error" -msgstr "Panggilan Kesalahan RPC" - -#: web/ejabberd_web_admin.erl:2000 -msgid "Database Tables at " -msgstr "Tabel Database pada" - -#: web/ejabberd_web_admin.erl:2007 -msgid "Storage Type" -msgstr "Jenis Penyimpanan" - -#: web/ejabberd_web_admin.erl:2008 -msgid "Elements" -msgstr "Elemen-elemen" - -#: web/ejabberd_web_admin.erl:2009 -msgid "Memory" -msgstr "Memori" - -#: web/ejabberd_web_admin.erl:2032 web/ejabberd_web_admin.erl:2137 -msgid "Error" -msgstr "Kesalahan" - -#: web/ejabberd_web_admin.erl:2034 -msgid "Backup of " -msgstr "Cadangan dari" - -#: web/ejabberd_web_admin.erl:2036 -msgid "" -"Please note that these options will only backup the builtin Mnesia database. " -"If you are using the ODBC module, you also need to backup your SQL database " -"separately." -msgstr "" -"Harap dicatat bahwa pilihan ini hanya akan membuat cadangan builtin Mnesia " -"database. Jika Anda menggunakan modul ODBC, anda juga perlu untuk membuat " -"cadangan database SQL Anda secara terpisah." - -#: web/ejabberd_web_admin.erl:2041 -msgid "Store binary backup:" -msgstr "Penyimpanan cadangan yang berpasangan:" - -#: web/ejabberd_web_admin.erl:2045 web/ejabberd_web_admin.erl:2052 -#: web/ejabberd_web_admin.erl:2060 web/ejabberd_web_admin.erl:2067 -#: web/ejabberd_web_admin.erl:2074 web/ejabberd_web_admin.erl:2081 -#: web/ejabberd_web_admin.erl:2088 web/ejabberd_web_admin.erl:2096 -#: web/ejabberd_web_admin.erl:2103 web/ejabberd_web_admin.erl:2110 -msgid "OK" -msgstr "YA" - -#: web/ejabberd_web_admin.erl:2048 -msgid "Restore binary backup immediately:" -msgstr "Segera mengembalikan cadangan yang berpasangan:" - -#: web/ejabberd_web_admin.erl:2056 -msgid "" -"Restore binary backup after next ejabberd restart (requires less memory):" -msgstr "" -"Mengembalikan cadangan yang berpasanagn setelah ejabberd berikutnya " -"dijalankan ulang (memerlukan memori lebih sedikit):" - -#: web/ejabberd_web_admin.erl:2063 -msgid "Store plain text backup:" -msgstr "Simpan cadangan teks biasa:" - -#: web/ejabberd_web_admin.erl:2070 -msgid "Restore plain text backup immediately:" -msgstr "Segera mengembalikan cadangan teks biasa:" - -#: web/ejabberd_web_admin.erl:2077 -msgid "Import users data from a PIEFXIS file (XEP-0227):" -msgstr "impor data-data pengguna dari sebuah PIEFXIS (XEP-0227):" - -#: web/ejabberd_web_admin.erl:2084 -msgid "Export data of all users in the server to PIEFXIS files (XEP-0227):" -msgstr "" -"Ekspor data dari semua pengguna pada layanan ke berkas PIEFXIS (XEP-0227):" - -#: web/ejabberd_web_admin.erl:2091 -msgid "Export data of users in a host to PIEFXIS files (XEP-0227):" -msgstr "Ekspor data pengguna pada sebuah host ke berkas PIEFXIS (XEP-0227):" - -#: web/ejabberd_web_admin.erl:2099 -msgid "Import user data from jabberd14 spool file:" -msgstr "Impor data pengguna dari sekumpulan berkas jabberd14:" - -#: web/ejabberd_web_admin.erl:2106 -msgid "Import users data from jabberd14 spool directory:" -msgstr "Импорт пользовательских данных из буферной директории jabberd14:" - -#: web/ejabberd_web_admin.erl:2132 -msgid "Listened Ports at " -msgstr "Mendeteksi Port-port di" - -#: web/ejabberd_web_admin.erl:2157 -msgid "Modules at " -msgstr "modul-modul di" - -#: web/ejabberd_web_admin.erl:2183 -msgid "Statistics of ~p" -msgstr "statistik dari ~p" - -#: web/ejabberd_web_admin.erl:2186 -msgid "Uptime:" -msgstr "Sampai saat:" - -#: web/ejabberd_web_admin.erl:2189 -msgid "CPU Time:" -msgstr "Waktu CPU:" - -#: web/ejabberd_web_admin.erl:2195 -msgid "Transactions Committed:" -msgstr "Transaksi yang dilakukan:" - -#: web/ejabberd_web_admin.erl:2198 -msgid "Transactions Aborted:" -msgstr "Transaksi yang dibatalkan:" - -#: web/ejabberd_web_admin.erl:2201 -msgid "Transactions Restarted:" -msgstr "Transaksi yang dijalankan ulang:" - -#: web/ejabberd_web_admin.erl:2204 -msgid "Transactions Logged:" -msgstr "Transaksi yang ditempuh:" - -#: web/ejabberd_web_admin.erl:2246 -msgid "Update " -msgstr "Memperbarui " - -#: web/ejabberd_web_admin.erl:2254 -msgid "Update plan" -msgstr "Rencana Perubahan" - -#: web/ejabberd_web_admin.erl:2255 -msgid "Modified modules" -msgstr "Modifikasi modul-modul" - -#: web/ejabberd_web_admin.erl:2256 -msgid "Update script" -msgstr "Perbarui naskah" - -#: web/ejabberd_web_admin.erl:2257 -msgid "Low level update script" -msgstr "Perbaruan naskah tingkat rendah" - -#: web/ejabberd_web_admin.erl:2258 -msgid "Script check" -msgstr "Periksa naskah" - -#: web/ejabberd_web_admin.erl:2426 -msgid "Port" -msgstr "Port" - -#: web/ejabberd_web_admin.erl:2427 -msgid "IP" -msgstr "IP" - -#: web/ejabberd_web_admin.erl:2428 -msgid "Protocol" -msgstr "Protocol" - -#: web/ejabberd_web_admin.erl:2429 web/ejabberd_web_admin.erl:2556 -msgid "Module" -msgstr "Modul" - -#: web/ejabberd_web_admin.erl:2430 web/ejabberd_web_admin.erl:2557 -msgid "Options" -msgstr "Pilihan-pilihan" - -#: web/ejabberd_web_admin.erl:2450 -msgid "Delete" -msgstr "Hapus" - -#: web/ejabberd_web_admin.erl:2579 -msgid "Start" -msgstr "Mulai" - -#: web/mod_register_web.erl:104 -msgid "Your Jabber account was successfully created." -msgstr "Jabber akun Anda telah sukses dibuat" - -#: web/mod_register_web.erl:107 -msgid "There was an error creating the account: " -msgstr "Ada kesalahan saat membuat akun:" - -#: web/mod_register_web.erl:115 -msgid "Your Jabber account was successfully deleted." -msgstr "Jabber akun Anda berhasil dihapus." - -#: web/mod_register_web.erl:118 -msgid "There was an error deleting the account: " -msgstr "Ada kesalahan saat menghapus akun:" - -#: web/mod_register_web.erl:128 -msgid "The password of your Jabber account was successfully changed." -msgstr "Kata sandi pada akun Jabber Anda telah berhasil diubah." - -#: web/mod_register_web.erl:131 -msgid "There was an error changing the password: " -msgstr "Ada kesalahan dalam mengubah password:" - -#: web/mod_register_web.erl:163 web/mod_register_web.erl:172 -msgid "Jabber Account Registration" -msgstr "Pendaftaran Akun Jabber" - -#: web/mod_register_web.erl:174 web/mod_register_web.erl:192 -#: web/mod_register_web.erl:201 -msgid "Register a Jabber account" -msgstr "Daftarkan sebuah akun jabber" - -#: web/mod_register_web.erl:176 web/mod_register_web.erl:493 -#: web/mod_register_web.erl:502 -msgid "Unregister a Jabber account" -msgstr "Nonaktifkan akun jabber" - -#: web/mod_register_web.erl:203 -msgid "" -"This page allows to create a Jabber account in this Jabber server. Your JID " -"(Jabber IDentifier) will be of the form: username@server. Please read " -"carefully the instructions to fill correctly the fields." -msgstr "" -"Halaman ini memungkinkan untuk membuat akun Jabber di layanan Jabber ini. " -"JID Anda (Jabber Pengenal) akan berbentuk: namapengguna@layanan. Harap baca " -"dengan seksama petunjuk-petunjuk untuk mengisi kolom dengan benar." - -#: web/mod_register_web.erl:212 web/mod_register_web.erl:386 -#: web/mod_register_web.erl:509 -msgid "Username:" -msgstr "Nama Pengguna:" - -#: web/mod_register_web.erl:217 -msgid "This is case insensitive: macbeth is the same that MacBeth and Macbeth." -msgstr "" -"Pada bagian ini huruf besar dan kecil tidak dibedakan: Misalnya macbeth " -"adalah sama dengan MacBeth juga Macbeth." - -#: web/mod_register_web.erl:218 -msgid "Characters not allowed:" -msgstr "Karakter tidak diperbolehkan:" - -#: web/mod_register_web.erl:222 web/mod_register_web.erl:391 -#: web/mod_register_web.erl:514 -msgid "Server:" -msgstr "Layanan:" - -#: web/mod_register_web.erl:232 -msgid "" -"Don't tell your password to anybody, not even the administrators of the " -"Jabber server." -msgstr "" -"Jangan memberitahukan kata sandi Anda ke siapapun, bahkan para administrator " -"dari layanan Jabber." - -#: web/mod_register_web.erl:234 -msgid "You can later change your password using a Jabber client." -msgstr "" -"Anda dapat mengubah kata sandi anda dilain waktu dengan menggunakan klien " -"Jabber." - -#: web/mod_register_web.erl:235 -msgid "" -"Some Jabber clients can store your password in your computer. Use that " -"feature only if you trust your computer is safe." -msgstr "" -"Beberapa klien Jabber dapat menyimpan password di komputer Anda. Gunakan " -"fitur itu hanya jika Anda mempercayai komputer Anda aman." - -#: web/mod_register_web.erl:237 -msgid "" -"Memorize your password, or write it in a paper placed in a safe place. In " -"Jabber there isn't an automated way to recover your password if you forget " -"it." -msgstr "" -"Hafalkan kata sandi Anda, atau dicatat dan letakkan di tempat yang aman. " -"Didalam Jabber tidak ada cara otomatis untuk mendapatkan kembali password " -"Anda jika Anda lupa." - -#: web/mod_register_web.erl:242 web/mod_register_web.erl:406 -msgid "Password Verification:" -msgstr "Verifikasi Kata Sandi:" - -#: web/mod_register_web.erl:250 -msgid "Register" -msgstr "Mendaftar" - -#: web/mod_register_web.erl:396 -msgid "Old Password:" -msgstr "Password Lama:" - -#: web/mod_register_web.erl:401 -msgid "New Password:" -msgstr "Password Baru:" - -#: web/mod_register_web.erl:504 -msgid "This page allows to unregister a Jabber account in this Jabber server." -msgstr "" -"Pada bagian ini memungkinkan Anda untuk membatalkan pendaftaran akun Jabber " -"pada layanan Jabber ini." - -#: web/mod_register_web.erl:524 -msgid "Unregister" -msgstr "Nonaktifkan" - -#, fuzzy -#~ msgid "Captcha test failed" -#~ msgstr "Проверка капчи прошла успешно." diff --git a/priv/msgs/it.msg b/priv/msgs/it.msg index 7961dc170..926d16687 100644 --- a/priv/msgs/it.msg +++ b/priv/msgs/it.msg @@ -1,43 +1,79 @@ -{"Access Configuration","Configurazione dell'accesso"}. -{"Access Control List Configuration","Configurazione dei diritti di accesso (ACL)"}. -{"Access control lists","Diritti di accesso (ACL)"}. -{"Access Control Lists","Diritti di accesso (ACL)"}. -{"Access denied by service policy","Accesso impedito dalle politiche del servizio"}. -{"Access rules","Regole di accesso"}. -{"Access Rules","Regole di accesso"}. -{"Action on user","Azione sull'utente"}. -{"Add Jabber ID","Aggiungere un Jabber ID (Jabber ID)"}. -{"Add New","Aggiungere nuovo"}. -{"Add User","Aggiungere un utente"}. -{"Administration","Amministrazione"}. -{"Administration of ","Amministrazione di "}. -{"Administrator privileges required","Necessari i privilegi di amministratore"}. +%% Generated automatically +%% DO NOT EDIT: run `make translations` instead +%% To improve translations please read: +%% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ + +{" (Add * to the end of field to match substring)"," (Aggiungere * alla fine del campo per far corrispondere la sottostringa)"}. +{" has set the subject to: "," ha modificato l'oggetto in: "}. +{"# participants","# partecipanti"}. +{"A description of the node","Una descrizione del nodo"}. {"A friendly name for the node","Un nome comodo per il nodo"}. +{"A password is required to enter this room","Per entrare in questa stanza è necessaria una password"}. +{"A Web Page","Una pagina web"}. +{"Accept","Accettare"}. +{"Access denied by service policy","Accesso negato dalle politiche del servizio"}. +{"Access model","Modello di accesso"}. +{"Account doesn't exist","L'account non esiste"}. +{"Action on user","Azione sull'utente"}. +{"Add a hat to a user","Aggiungere un cappello a un utente"}. +{"Add User","Aggiungere un utente"}. +{"Administration of ","Amministrazione di "}. +{"Administration","Amministrazione"}. +{"Administrator privileges required","Sono richiesti privilegi di amministratore"}. {"All activity","Tutta l'attività"}. -{"Allow this Jabber ID to subscribe to this pubsub node?","Consentire a questo Jabber ID l'iscrizione a questo nodo pubsub?"}. +{"All Users","Tutti gli utenti"}. +{"Allow subscription","Consenti iscrizione"}. +{"Allow this Jabber ID to subscribe to this pubsub node?","Consentire a questo ID Jabber di iscriversi a questo nodo pubsub?"}. +{"Allow this person to register with the room?","Permettere a questa persona di registrarsi con la stanza?"}. {"Allow users to change the subject","Consentire agli utenti di cambiare l'oggetto"}. {"Allow users to query other users","Consentire agli utenti query verso altri utenti"}. -{"Allow users to send invites","Consentire agli utenti l'invio di inviti"}. -{"Allow users to send private messages","Consentire agli utenti l'invio di messaggi privati"}. +{"Allow users to send invites","Consenti agli utenti di inviare inviti"}. +{"Allow users to send private messages","Consenti agli utenti di inviare messaggi privati"}. {"Allow visitors to change nickname","Consentire ai visitatori di cambiare il nickname"}. {"Allow visitors to send private messages to","Consentire agli ospiti l'invio di messaggi privati a"}. {"Allow visitors to send status text in presence updates","Consentire ai visitatori l'invio di testo sullo stato in aggiornamenti sulla presenza"}. {"Allow visitors to send voice requests","Consentire agli ospiti l'invio di richieste di parola"}. -{"All Users","Tutti gli utenti"}. +{"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 gruppo LDAP associato che definisce l'appartenenza alla sala; deve trattarsi di un nome distinto LDAP in base a una definizione di gruppo specifica dell'implementazione o della distribuzione."}. {"Announcements","Annunci"}. -{"anyone","tutti"}. -{"A password is required to enter this room","Per entrare in questa stanza è prevista una password"}. +{"Answer associated with a picture","Risposta associata ad un'immagine"}. +{"Answer associated with a video","Risposta associata a un video"}. +{"Answer associated with speech","Risposta associata al discorso"}. +{"Answer to a question","Risposta a una domanda"}. +{"Anyone in the specified roster group(s) may subscribe and retrieve items","Chiunque nei gruppi di elenco specificati può iscriversi e recuperare elementi"}. +{"Anyone may associate leaf nodes with the collection","Chiunque può associare i nodi foglia alla raccolta"}. +{"Anyone may publish","Chiunque può pubblicare"}. +{"Anyone may subscribe and retrieve items","Chiunque può iscriversi e recuperare elementi"}. +{"Anyone with a presence subscription of both or from may subscribe and retrieve items","Chiunque abbia un abbonamento di presenza di entrambi o di può sottoscrivere e recuperare elementi"}. +{"Anyone with Voice","Chiunque abbia la Voce"}. +{"Anyone","Chiunque"}. {"April","Aprile"}. +{"Attribute 'channel' is required for this request","Per questa richiesta è richiesto l'attributo 'canale'"}. +{"Attribute 'id' is mandatory for MIX messages","L'attributo 'id' è obbligatorio per i messaggi MIX"}. +{"Attribute 'jid' is not allowed here","L'attributo \"jid\" non è consentito qui"}. +{"Attribute 'node' is not allowed here","L'attributo \"nodo\" non è consentito qui"}. +{"Attribute 'to' of stanza that triggered challenge","Attributo \"a\" della strofa che ha innescato la sfida"}. {"August","Agosto"}. -{"Backup Management","Gestione dei salvataggi"}. -{"Backup of ","Salvataggio di "}. -{"Backup","Salvare"}. -{"Backup to File at ","Salvataggio sul file "}. +{"Automatic node creation is not enabled","La creazione automatica del nodo non è abilitata"}. +{"Backup Management","Gestione dei Backup"}. +{"Backup of ~p","Backup di ~p"}. +{"Backup to File at ","Backup su file in "}. +{"Backup","Backup"}. {"Bad format","Formato non valido"}. {"Birthday","Compleanno"}. +{"Both the username and the resource are required","Sono richiesti sia il nome utente che la risorsa"}. +{"Bytestream already activated","Bytestream già attivato"}. +{"Cannot remove active list","Impossibile rimuovere l'elenco attivo"}. +{"Cannot remove default list","Impossibile rimuovere l'elenco predefinito"}. {"CAPTCHA web page","Pagina web CAPTCHA"}. -{"Change Password","Modificare la password"}. +{"Challenge ID","ID Sfida"}. +{"Change Password","Cambiare la password"}. {"Change User Password","Cambiare la password dell'utente"}. +{"Changing password is not allowed","Non è consentito modificare la password"}. +{"Changing role/affiliation is not allowed","Non è consentito cambiare ruolo/affiliazione"}. +{"Channel already exists","Canale già esistente"}. +{"Channel does not exist","Il canale non esiste"}. +{"Channel JID","Canale JID"}. +{"Channels","Canali"}. {"Characters not allowed:","Caratteri non consentiti:"}. {"Chatroom configuration modified","Configurazione della stanza modificata"}. {"Chatroom is created","La stanza è creata"}. @@ -46,90 +82,100 @@ {"Chatroom is stopped","La stanza è arrestata"}. {"Chatrooms","Stanze"}. {"Choose a username and password to register with this server","Scegliere un nome utente e una password per la registrazione con questo server"}. -{"Choose modules to stop","Selezionare i moduli da arrestare"}. {"Choose storage type of tables","Selezionare una modalità di conservazione delle tabelle"}. -{"Choose whether to approve this entity's subscription.","Scegliere se approvare l'iscrizione per questa entità"}. +{"Choose whether to approve this entity's subscription.","Scegliere se approvare l'iscrizione per questa entità."}. {"City","Città"}. +{"Client acknowledged more stanzas than sent by server","Il client ha riconosciuto più stanze di quelle inviate dal server"}. {"Commands","Comandi"}. {"Conference room does not exist","La stanza per conferenze non esiste"}. -{"Configuration","Configurazione"}. {"Configuration of room ~s","Configurazione per la stanza ~s"}. -{"Connected Resources:","Risorse connesse:"}. -{"Connections parameters","Parametri delle connessioni"}. +{"Configuration","Configurazione"}. +{"Contact Addresses (normally, room owner or owners)","Indirizzi di contatto (normalmente, proprietario o proprietari della stanza)"}. {"Country","Paese"}. -{"CPU Time:","Tempo CPU:"}. -{"Database","Database"}. -{"Database Tables at ","Tabelle del database su "}. +{"Current Discussion Topic","Argomento di discussione attuale"}. +{"Database failure","Errore del database"}. {"Database Tables Configuration at ","Configurazione delle tabelle del database su "}. +{"Database","Database"}. {"December","Dicembre"}. {"Default users as participants","Definire per default gli utenti come partecipanti"}. -{"Delete","Eliminare"}. -{"Delete message of the day","Eliminare il messaggio del giorno (MOTD)"}. {"Delete message of the day on all hosts","Eliminare il messaggio del giorno (MOTD) su tutti gli host"}. -{"Delete Selected","Eliminare gli elementi selezionati"}. +{"Delete message of the day","Eliminare il messaggio del giorno (MOTD)"}. {"Delete User","Eliminare l'utente"}. {"Deliver event notifications","Inviare notifiche degli eventi"}. {"Deliver payloads with event notifications","Inviare il contenuto del messaggio con la notifica dell'evento"}. -{"Description:","Descrizione:"}. -{"Disc only copy","Copia su disco soltanto"}. -{"Displayed Groups:","Gruppi visualizzati:"}. -{"Don't tell your password to anybody, not even the administrators of the Jabber server.","Non comunicare la tua password a nessuno, neppure agli amministratori del server Jabber."}. +{"Disc only copy","Copia solo su disco"}. +{"Don't tell your password to anybody, not even the administrators of the XMPP server.","Non rivelare la tua password a nessuno, nemmeno agli amministratori del server XMPP."}. {"Dump Backup to Text File at ","Trascrivere il salvataggio sul file di testo "}. {"Dump to Text File","Trascrivere su file di testo"}. +{"Duplicated groups are not allowed by RFC6121","I gruppi duplicati non sono consentiti da RFC6121"}. +{"Dynamically specify a replyto of the item publisher","Specifica dinamicamente una risposta dell'editore dell'elemento"}. {"Edit Properties","Modificare le proprietà"}. {"Either approve or decline the voice request.","Approva oppure respingi la richiesta di parola."}. -{"ejabberd IRC module","Modulo IRC per ejabberd"}. +{"ejabberd HTTP Upload service","Servizio di Caricamento HTTP di ejabberd"}. {"ejabberd MUC module","Modulo MUC per ejabberd"}. +{"ejabberd Multicast service","Servizio Multicast ejabberd"}. {"ejabberd Publish-Subscribe module","Modulo Pubblicazione/Iscrizione (PubSub) per ejabberd"}. {"ejabberd SOCKS5 Bytestreams module","Modulo SOCKS5 Bytestreams per ejabberd"}. {"ejabberd vCard module","Modulo vCard per ejabberd"}. {"ejabberd Web Admin","Amministrazione web ejabberd"}. -{"Elements","Elementi"}. +{"ejabberd","ejabberd"}. +{"Email Address","Indirizzo di Posta Elettronica"}. {"Email","E-mail"}. -{"Enable logging","Abilitare i log"}. -{"Encoding for server ~b","Codifica per il server ~b"}. +{"Enable hats","Abilitare i cappelli"}. +{"Enable logging","Abilitare la registrazione"}. +{"Enable message archiving","Abilita l'archiviazione dei messaggi"}. +{"Enabling push without 'node' attribute is not supported","L'abilitazione del push senza l'attributo 'nodo' non è supportata"}. {"End User Session","Terminare la sessione dell'utente"}. -{"Enter list of {Module, [Options]}","Immettere un elenco di {Modulo, [Opzioni]}"}. {"Enter nickname you want to register","Immettere il nickname che si vuole registrare"}. {"Enter path to backup file","Immettere il percorso del file di salvataggio"}. {"Enter path to jabberd14 spool dir","Immettere il percorso della directory di spool di jabberd14"}. {"Enter path to jabberd14 spool file","Immettere il percorso del file di spool di jabberd14"}. {"Enter path to text file","Immettere il percorso del file di testo"}. {"Enter the text you see","Immettere il testo visibile"}. -{"Enter username and encodings you wish to use for connecting to IRC servers. Press 'Next' to get more fields to fill in. Press 'Complete' to save settings.","Immettere il nome utente e le codifiche che si desidera utilizzare per la connessione ai server IRC. Premere \"Avanti\" per vedere i successivi campi da compilare. Premere \"Fatto\" per salvare le impostazioni."}. -{"Enter username, encodings, ports and passwords you wish to use for connecting to IRC servers","Immettere il nome utente, le codifiche, le porte e le password che si desidera utilizzare per la connessione ai server IRC"}. -{"Erlang Jabber Server","Erlang Jabber Server"}. -{"Error","Errore"}. -{"Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef.net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}].","Esempio: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"segreto\"}, {\"vendetta.fef.net\", \"iso8859-1\", 7000}, {\"irc.serverdiprova.net\", \"utf-8\"}]."}. +{"Erlang XMPP Server","Server XMPP Erlang"}. {"Exclude Jabber IDs from CAPTCHA challenge","Escludi degli ID Jabber dal passaggio CAPTCHA"}. +{"Export all tables as SQL queries to a file:","Esporta tutte le tabelle come query SQL in un file:"}. {"Export data of all users in the server to PIEFXIS files (XEP-0227):","Esportare i dati di tutti gli utenti nel server in file PIEFXIS (XEP-0227):"}. {"Export data of users in a host to PIEFXIS files (XEP-0227):","Esportare i dati degli utenti di un host in file PIEFXIS (XEP-0227):"}. +{"External component failure","Guasto del componente esterno"}. +{"External component timeout","Timeout del componente esterno"}. +{"Failed to activate bytestream","Impossibile attivare il flusso di byte"}. {"Failed to extract JID from your voice request approval","Impossibile estrarre il JID dall'approvazione della richiesta di parola"}. +{"Failed to map delegated namespace to external component","Impossibile mappare lo spazio dei nomi delegato al componente esterno"}. +{"Failed to parse HTTP response","Impossibile analizzare la risposta HTTP"}. +{"Failed to process option '~s'","Impossibile elaborare l'opzione '~s'"}. {"Family Name","Cognome"}. +{"FAQ Entry","Inserimento delle domande frequenti"}. {"February","Febbraio"}. -{"Fill in fields to search for any matching Jabber User","Riempire i campi per la ricerca di utenti Jabber corrispondenti ai criteri"}. -{"Fill in the form to search for any matching Jabber User (Add * to the end of field to match substring)","Riempire il modulo per la ricerca di utenti Jabber corrispondenti ai criteri (Aggiungere * alla fine del campo per la ricerca di una sottostringa"}. +{"File larger than ~w bytes","File più grande di ~w byte"}. +{"Fill in the form to search for any matching XMPP User","Compila il modulo per cercare qualsiasi utente XMPP corrispondente"}. {"Friday","Venerdì"}. -{"From","Da"}. -{"From ~s","Da ~s"}. +{"From ~ts","Da ~ts"}. +{"Full List of Room Admins","Elenco Completo degli Amministratori delle Stanze"}. +{"Full List of Room Owners","Elenco Completo dei Proprietari delle Stanze"}. {"Full Name","Nome completo"}. +{"Get List of Online Users","Ottieni L'elenco degli Utenti Online"}. +{"Get List of Registered Users","Ottieni L'elenco degli Utenti Registrati"}. {"Get Number of Online Users","Ottenere il numero di utenti online"}. {"Get Number of Registered Users","Ottenere il numero di utenti registrati"}. +{"Get Pending","Ottieni in sospeso"}. {"Get User Last Login Time","Ottenere la data di ultimo accesso dell'utente"}. -{"Get User Password","Ottenere la password dell'utente"}. {"Get User Statistics","Ottenere le statistiche dell'utente"}. +{"Given Name","Nome di battesimo"}. {"Grant voice to this person?","Dare parola a questa persona?"}. -{"Group ","Gruppo "}. -{"Groups","Gruppi"}. {"has been banned","è stata/o bandita/o"}. -{"has been kicked because of an affiliation change","è stato espulso a causa di un cambiamento di appartenenza"}. {"has been kicked because of a system shutdown","è stato espulso a causa dello spegnimento del sistema"}. +{"has been kicked because of an affiliation change","è stato espulso a causa di un cambiamento di appartenenza"}. {"has been kicked because the room has been changed to members-only","è stato espulso per la limitazione della stanza ai soli membri"}. {"has been kicked","è stata/o espulsa/o"}. -{" has set the subject to: "," ha modificato l'oggetto in: "}. -{"Host","Host"}. +{"Hash of the vCard-temp avatar of this room","Hash dell'avatar vCard-temp di questa stanza"}. +{"Hat title","Titolo del Cappello"}. +{"Hat URI","URI Cappello"}. +{"Hats limit exceeded","Limite di cappelli superato"}. +{"Host unknown","Host sconosciuto"}. +{"HTTP File Upload","Caricamento file HTTP"}. +{"Idle connection","Connessione inattiva"}. {"If you don't see the CAPTCHA image here, visit the web page.","Se qui non vedi l'immagine CAPTCHA, visita la pagina web."}. -{"If you want to specify different ports, passwords, encodings for IRC servers, fill this list with values in format '{\"irc server\", \"encoding\", port, \"password\"}'. By default this service use \"~s\" encoding, port ~p, empty password.","Se si vogliono specificare differenti porte, password, codifiche per i server IRC, si riempia questo elenco con valori nel formato '{\"server IRC\", \"codifica\", porta, \"password\"}'. Per default questo servizio utilizza la codifica \"~s\", la porta ~p, la password vuota."}. {"Import Directory","Importare una directory"}. {"Import File","Importare un file"}. {"Import user data from jabberd14 spool file:","Importare i dati utente da file di spool di jabberd14:"}. @@ -138,41 +184,48 @@ {"Import users data from jabberd14 spool directory:","Importare i dati utenti da directory di spool di jabberd14:"}. {"Import Users from Dir at ","Importare utenti dalla directory "}. {"Import Users From jabberd14 Spool Files","Importare utenti da file di spool di jabberd14"}. +{"Improper domain part of 'from' attribute","Parte del dominio non corretta dell'attributo 'da'"}. {"Improper message type","Tipo di messaggio non corretto"}. +{"Incorrect CAPTCHA submit","Invio CAPTCHA errato"}. +{"Incorrect data form","Modulo dati errato"}. {"Incorrect password","Password non esatta"}. -{"Invalid affiliation: ~s","Affiliazione non valida: ~s"}. -{"Invalid role: ~s","Ruolo non valido: ~s"}. +{"Incorrect value of 'action' attribute","Valore errato dell'attributo 'azione'"}. +{"Incorrect value of 'action' in data form","Valore errato di 'azione' nel modulo dati"}. +{"Incorrect value of 'path' in data form","Valore errato di 'percorso' nel modulo dati"}. +{"Installed Modules:","Moduli installati:"}. +{"Install","Installare"}. +{"Insufficient privilege","Privilegio insufficiente"}. +{"Internal server error","Errore interno del server"}. +{"Invalid 'from' attribute in forwarded message","Attributo 'da' non valido nel messaggio inoltrato"}. +{"Invalid node name","Nome del nodo non valido"}. +{"Invalid 'previd' value","Valore 'previd' non valido"}. +{"Invitations are not allowed in this conference","Non sono ammessi inviti a questa conferenza"}. {"IP addresses","Indirizzi IP"}. -{"IP","IP"}. -{"IRC channel (don't put the first #)","Canale IRC (senza il # iniziale)"}. -{"IRC server","Server IRC"}. -{"IRC settings","Impostazioni IRC"}. -{"IRC Transport","Transport IRC"}. -{"IRC username","Nome utente IRC"}. -{"IRC Username","Nome utente IRC"}. {"is now known as","è ora conosciuta/o come"}. -{"It is not allowed to send private messages","Non è consentito l'invio di messaggi privati"}. +{"It is not allowed to send error messages to the room. The participant (~s) has sent an error message (~s) and got kicked from the room","Non è consentito inviare messaggi di errore alla stanza. Il partecipante (~s) ha inviato un messaggio di errore (~s) ed è stato espulso dalla stanza"}. {"It is not allowed to send private messages of type \"groupchat\"","Non è consentito l'invio di messaggi privati di tipo \"groupchat\""}. {"It is not allowed to send private messages to the conference","Non è consentito l'invio di messaggi privati alla conferenza"}. -{"Jabber Account Registration","Registrazione account Jabber"}. {"Jabber ID","Jabber ID (Jabber ID)"}. -{"Jabber ID ~s is invalid","Il Jabber ID ~s non è valido"}. {"January","Gennaio"}. -{"Join IRC channel","Entra nel canale IRC"}. +{"JID normalization denied by service policy","Normalizzazione JID negata dalla politica del servizio"}. +{"JID normalization failed","La normalizzazione JID non è riuscita"}. +{"Joined MIX channels of ~ts","Canali MIX uniti di ~ts"}. +{"Joined MIX channels:","Canali MIX uniti:"}. {"joins the room","entra nella stanza"}. -{"Join the IRC channel here.","Entra nel canale IRC qui."}. -{"Join the IRC channel in this Jabber ID: ~s","Entra nel canale IRC in questo ID Jabber: ~s"}. {"July","Luglio"}. {"June","Giugno"}. +{"Just created","Appena creato"}. {"Last Activity","Ultima attività"}. {"Last login","Ultimo accesso"}. +{"Last message","Ultimo messaggio"}. {"Last month","Ultimo mese"}. {"Last year","Ultimo anno"}. +{"Least significant bits of SHA-256 hash of text should equal hexadecimal label","I bit meno significativi dell'hash di testo SHA-256 devono corrispondere all'etichetta esadecimale"}. {"leaves the room","esce dalla stanza"}. -{"Listened Ports at ","Porte in ascolto su "}. -{"Listened Ports","Porte in ascolto"}. -{"List of modules to start","Elenco dei moduli da avviare"}. -{"Low level update script","Script di aggiornamento di basso livello"}. +{"List of users with hats","Elenco degli utenti con cappelli"}. +{"List users with hats","Elenca gli utenti con cappelli"}. +{"Logged Out","Disconnesso"}. +{"Logging","Registrazione"}. {"Make participants list public","Rendere pubblica la lista dei partecipanti"}. {"Make room CAPTCHA protected","Rendere la stanza protetta da CAPTCHA"}. {"Make room members-only","Rendere la stanza riservata ai membri"}. @@ -180,242 +233,393 @@ {"Make room password protected","Rendere la stanza protetta da password"}. {"Make room persistent","Rendere la stanza persistente"}. {"Make room public searchable","Rendere la sala visibile al pubblico"}. +{"Malformed username","Nome utente malformato"}. +{"MAM preference modification denied by service policy","Modifica delle preferenze MAM negata dalla policy del servizio"}. {"March","Marzo"}. -{"Maximum Number of Occupants","Numero massimo di occupanti"}. -{"Max # of items to persist","Numero massimo di elementi da conservare persistentemente"}. +{"Max # of items to persist, or `max` for no specific limit other than a server imposed maximum","Numero massimo di elementi da persistere o `max` per nessun limite specifico diverso da quello massimo imposto dal server"}. {"Max payload size in bytes","Dimensione massima del contenuto del messaggio in byte"}. +{"Maximum file size","Dimensione massima del file"}. +{"Maximum Number of History Messages Returned by Room","Numero Massimo di Messaggi di Cronologia Restituiti dalla Stanza"}. +{"Maximum number of items to persist","Numero massimo di elementi da persistere"}. +{"Maximum Number of Occupants","Numero massimo di occupanti"}. {"May","Maggio"}. {"Membership is required to enter this room","Per entrare in questa stanza è necessario essere membro"}. -{"Members:","Membri:"}. -{"Memorize your password, or write it in a paper placed in a safe place. In Jabber there isn't an automated way to recover your password if you forget it.","Memorizza la password, o scrivila su un foglio di carta da conservare in un luogo sicuro. Jabber non prevede una modalità automatica per il recupero di una password dimenticata."}. -{"Memory","Memoria"}. +{"Memorize your password, or write it in a paper placed in a safe place. In XMPP there isn't an automated way to recover your password if you forget it.","Memorizza la tua password, oppure scrivila su un foglio riposto in un luogo sicuro. In XMPP non esiste un modo automatico per recuperare la password se la dimentichi."}. +{"Mere Availability in XMPP (No Show Value)","Mera disponibilità in XMPP (Nessun Valore Mostrato)"}. {"Message body","Corpo del messaggio"}. +{"Message not found in forwarded payload","Messaggio non trovato nel payload inoltrato"}. +{"Messages from strangers are rejected","I messaggi provenienti da sconosciuti vengono rifiutati"}. +{"Messages of type headline","Messaggi di tipo headline"}. +{"Messages of type normal","Messaggi di tipo normale"}. {"Middle Name","Altro nome"}. {"Minimum interval between voice requests (in seconds)","Intervallo minimo fra due richieste di parola (in secondi)"}. {"Moderator privileges required","Necessari i privilegi di moderatore"}. -{"moderators only","moderatori soltanto"}. -{"Modified modules","Moduli modificati"}. -{"Module","Modulo"}. -{"Modules at ","Moduli su "}. -{"Modules","Moduli"}. +{"Moderator","Moderatore/Moderatrice"}. +{"Moderators Only","Solo i Moderatori"}. +{"Module failed to handle the query","Il modulo non è riuscito a gestire la query"}. {"Monday","Lunedì"}. -{"Name:","Nome:"}. +{"Multicast","Multicast"}. +{"Multiple elements are not allowed by RFC6121","Più elementi non sono consentiti da RFC6121"}. +{"Multi-User Chat","Chat Multiutente"}. {"Name","Nome"}. +{"Natural Language for Room Discussions","Linguaggio Naturale per le Discussioni in Sala"}. +{"Natural-Language Room Name","Nome della Stanza in Linguaggio Naturale"}. +{"Neither 'jid' nor 'nick' attribute found","Né l'attributo 'jid' né quello 'nick' sono stati trovati"}. +{"Neither 'role' nor 'affiliation' attribute found","Non sono stati trovati né gli attributi 'ruolo' né 'affiliazione'"}. {"Never","Mai"}. {"New Password:","Nuova password:"}. -{"Nickname","Nickname"}. +{"Nickname can't be empty","Il soprannome non può essere vuoto"}. {"Nickname Registration at ","Registrazione di un nickname su "}. {"Nickname ~s does not exist in the room","Il nickname ~s non esiste nella stanza"}. -{"nobody","nessuno"}. +{"Nickname","Soprannome"}. +{"No address elements found","Nessun elemento dell'indirizzo trovato"}. +{"No addresses element found","Nessun elemento degli indirizzi trovato"}. +{"No 'affiliation' attribute found","Nessun attributo 'affiliazione' trovato"}. +{"No available resource found","Nessuna risorsa disponibile trovata"}. {"No body provided for announce message","Nessun corpo fornito per il messaggio di annuncio"}. +{"No child elements found","Non sono stati trovati elementi figlio"}. +{"No data form found","Nessun modulo dati trovato"}. {"No Data","Nessuna informazione"}. -{"Node ID","ID del nodo"}. -{"Node ","Nodo "}. -{"Node not found","Nodo non trovato"}. -{"Nodes","Nodi"}. +{"No features available","Nessuna funzionalità disponibile"}. +{"No element found","Nessun elemento trovato"}. +{"No hook has processed this command","Nessun hook ha elaborato questo comando"}. +{"No info about last activity found","Nessuna informazione sull'ultima attività trovata"}. +{"No 'item' element found","Nessun elemento 'item' trovato"}. +{"No items found in this query","Nessun elemento trovato in questa query"}. {"No limit","Nessun limite"}. -{"None","Nessuno"}. -{"No resource provided","Nessuna risorsa fornita"}. +{"No module is handling this query","Nessun modulo gestisce questa query"}. +{"No node specified","Nessun nodo specificato"}. +{"No 'password' found in data form","Nessuna 'password' trovata nel modulo dati"}. +{"No 'password' found in this query","Nessuna \"password\" trovata in questa query"}. +{"No 'path' found in data form","Nessun 'percorso' trovato nel modulo dati"}. +{"No pending subscriptions found","Nessuna sottoscrizione in attesa trovata"}. +{"No privacy list with this name found","Nessun elenco di privacy con questo nome trovato"}. +{"No private data found in this query","Non sono stati trovati dati privati in questa query"}. +{"No running node found","Nessun nodo in esecuzione trovato"}. +{"No services available","Nessun servizio disponibile"}. +{"No statistics found for this item","Nessuna statistica trovata per questa voce"}. +{"No 'to' attribute found in the invitation","Nessun attributo 'a' trovato nell'invito"}. +{"Nobody","Nessuno"}. +{"Node already exists","Il nodo esiste già"}. +{"Node ID","ID del Nodo"}. +{"Node index not found","Indice del nodo non trovato"}. +{"Node not found","Nodo non trovato"}. +{"Node ~p","Nodo ~p"}. +{"Node","Nodo"}. +{"Nodeprep has failed","Nodeprep non è riuscito"}. +{"Nodes","Nodi"}. +{"None","Niente"}. +{"Not allowed","Non consentito"}. {"Not Found","Non trovato"}. +{"Not subscribed","Non sottoscritto"}. {"Notify subscribers when items are removed from the node","Notificare gli iscritti quando sono eliminati degli elementi dal nodo"}. {"Notify subscribers when the node configuration changes","Notificare gli iscritti quando la configurazione del nodo cambia"}. {"Notify subscribers when the node is deleted","Notificare gli iscritti quando il nodo è cancellato"}. {"November","Novembre"}. +{"Number of answers required","Numero di risposte richieste"}. {"Number of occupants","Numero di presenti"}. +{"Number of Offline Messages","Numero di messaggi offline"}. {"Number of online users","Numero di utenti online"}. {"Number of registered users","Numero di utenti registrati"}. +{"Number of seconds after which to automatically purge items, or `max` for no specific limit other than a server imposed maximum","Numero di secondi dopo i quali eliminare automaticamente gli elementi o `max` per nessun limite specifico diverso da quello massimo imposto dal server"}. +{"Occupants are allowed to invite others","Gli occupanti possono invitare altri"}. +{"Occupants are allowed to query others","Gli occupanti possono interrogare gli altri"}. +{"Occupants May Change the Subject","Gli Occupanti Possono Cambiare il Soggetto"}. {"October","Ottobre"}. -{"Offline Messages:","Messaggi offline:"}. -{"Offline Messages","Messaggi offline"}. {"OK","OK"}. {"Old Password:","Vecchia password:"}. -{"Online","Online"}. -{"Online Users:","Utenti connessi:"}. {"Online Users","Utenti online"}. +{"Online","Online"}. +{"Only collection node owners may associate leaf nodes with the collection","Solo i proprietari dei nodi di raccolta possono associare i nodi foglia alla collezione"}. {"Only deliver notifications to available users","Inviare le notifiche solamente agli utenti disponibili"}. +{"Only or tags are allowed","Sono consentiti solo i tag o "}. +{"Only element is allowed in this query","In questa query è consentito solo l'elemento "}. +{"Only members may query archives of this room","Solo i membri possono interrogare gli archivi di questa stanza"}. {"Only moderators and participants are allowed to change the subject in this room","La modifica dell'oggetto di questa stanza è consentita soltanto ai moderatori e ai partecipanti"}. {"Only moderators are allowed to change the subject in this room","La modifica dell'oggetto di questa stanza è consentita soltanto ai moderatori"}. +{"Only moderators are allowed to retract messages","Solo i moderatori possono ritirare i messaggi"}. {"Only moderators can approve voice requests","Soltanto i moderatori possono approvare richieste di parola"}. {"Only occupants are allowed to send messages to the conference","L'invio di messaggi alla conferenza è consentito soltanto ai presenti"}. {"Only occupants are allowed to send queries to the conference","L'invio di query alla conferenza è consentito ai soli presenti"}. +{"Only publishers may publish","Solo gli editori possono pubblicare"}. {"Only service administrators are allowed to send service messages","L'invio di messaggi di servizio è consentito solamente agli amministratori del servizio"}. -{"Options","Opzioni"}. +{"Only those on a whitelist may associate leaf nodes with the collection","Solo chi fa parte di una whitelist può associare i nodi foglia alla collezione"}. +{"Only those on a whitelist may subscribe and retrieve items","Solo chi fa parte di una whitelist può sottoscrivere e recuperare le voci"}. {"Organization Name","Nome dell'organizzazione"}. {"Organization Unit","Unità dell'organizzazione"}. -{"Outgoing s2s Connections:","Connessioni s2s in uscita:"}. +{"Other Modules Available:","Altri Moduli Disponibili:"}. {"Outgoing s2s Connections","Connessioni s2s in uscita"}. -{"Outgoing s2s Servers:","Server s2s in uscita"}. {"Owner privileges required","Necessari i privilegi di proprietario"}. -{"Packet","Pacchetto"}. -{"Password ~b","Password ~b"}. -{"Password:","Password:"}. -{"Password","Password"}. -{"Password Verification:","Verifica della password:"}. +{"Packet relay is denied by service policy","Il relay dei pacchetti è negato dalla politica di servizio"}. +{"Participant ID","ID Partecipante"}. +{"Participant","Partecipante"}. {"Password Verification","Verifica della password"}. +{"Password Verification:","Verifica della password:"}. +{"Password","Password"}. +{"Password:","Password:"}. {"Path to Dir","Percorso della directory"}. {"Path to File","Percorso del file"}. -{"Pending","Pendente"}. -{"Period: ","Periodo:"}. +{"Payload semantic type information","Informazioni sul tipo semantico del payload"}. +{"Period: ","Periodo: "}. {"Persist items to storage","Conservazione persistente degli elementi"}. +{"Persistent","Persistente"}. +{"Ping query is incorrect","La query ping non è corretta"}. {"Ping","Ping"}. {"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","N.B.: Queste opzioni comportano il salvataggio solamente del database interno Mnesia. Se si sta utilizzando il modulo ODBC, è necessario salvare anche il proprio database SQL separatamente."}. {"Please, wait for a while before sending new voice request","Attendi qualche istante prima di inviare una nuova richiesta di parola"}. {"Pong","Pong"}. -{"Port ~b","Porta ~b"}. -{"Port","Porta"}. +{"Possessing 'ask' attribute is not allowed by RFC6121","Il possesso dell'attributo 'chiedi' non è consentito da RFC6121"}. {"Present real Jabber IDs to","Rendere visibile il Jabber ID reale a"}. +{"Previous session not found","Sessione precedente non trovata"}. +{"Previous session PID has been killed","Il PID della sessione precedente è stato ucciso"}. +{"Previous session PID has exited","Il PID della sessione precedente è terminato"}. +{"Previous session PID is dead","Il PID della sessione precedente è morto"}. +{"Previous session timed out","La sessione precedente è scaduta"}. {"private, ","privato, "}. -{"Protocol","Protocollo"}. +{"Public","Pubblico"}. +{"Publish model","Pubblica modello"}. {"Publish-Subscribe","Pubblicazione-Iscrizione"}. {"PubSub subscriber request","Richiesta di iscrizione per PubSub"}. {"Purge all items when the relevant publisher goes offline","Cancella tutti gli elementi quando chi li ha pubblicati non è più online"}. +{"Push record not found","Record push non trovato"}. {"Queries to the conference members are not allowed in this room","In questa stanza non sono consentite query ai membri della conferenza"}. +{"Query to another users is forbidden","La richiesta ad altri utenti è vietata"}. {"RAM and disc copy","Copia in memoria (RAM) e su disco"}. {"RAM copy","Copia in memoria (RAM)"}. -{"Raw","Grezzo"}. {"Really delete message of the day?","Si conferma l'eliminazione del messaggio del giorno (MOTD)?"}. +{"Receive notification from all descendent nodes","Ricevere una notifica da tutti i nodi discendenti"}. +{"Receive notification from direct child nodes only","Ricevere notifiche solo dai nodi figli diretti"}. +{"Receive notification of new items only","Ricevere una notifica solo per le nuove voce"}. +{"Receive notification of new nodes only","Ricevere solo la notifica di nuovi nodi"}. {"Recipient is not in the conference room","Il destinatario non è nella stanza per conferenze"}. -{"Register a Jabber account","Registra un account Jabber"}. -{"Registered Users:","Utenti registrati:"}. -{"Registered Users","Utenti registrati"}. +{"Register an XMPP account","Registra un account XMPP"}. {"Register","Registra"}. -{"Registration in mod_irc for ","Registrazione in mod_irc per "}. {"Remote copy","Copia remota"}. -{"Remove All Offline Messages","Eliminare tutti i messaggi offline"}. -{"Remove","Eliminare"}. -{"Remove User","Eliminare l'utente"}. +{"Remove a hat from a user","Rimuovere un cappello da un utente"}. +{"Remove User","Rimuovere l'utente"}. {"Replaced by new connection","Sostituito da una nuova connessione"}. +{"Request has timed out","La richiesta è scaduta"}. +{"Request is ignored","La richiesta viene ignorata"}. +{"Requested role","Ruolo richiesto"}. {"Resources","Risorse"}. -{"Restart","Riavviare"}. {"Restart Service","Riavviare il servizio"}. {"Restore Backup from File at ","Recuperare il salvataggio dal file "}. {"Restore binary backup after next ejabberd restart (requires less memory):","Recuperare un salvataggio binario dopo il prossimo riavvio di ejabberd (necessita di meno memoria):"}. {"Restore binary backup immediately:","Recuperare un salvataggio binario adesso:"}. {"Restore plain text backup immediately:","Recuperare un salvataggio come semplice testo adesso:"}. {"Restore","Recuperare"}. +{"Roles and Affiliations that May Retrieve Member List","Ruoli e Affiliazioni che Possono Recuperare L'elenco dei Membri"}. +{"Roles for which Presence is Broadcasted","Ruoli per i quali viene Trasmessa la Presenza"}. +{"Roles that May Send Private Messages","Ruoli che possono inviare messaggi privati"}. {"Room Configuration","Configurazione della stanza"}. {"Room creation is denied by service policy","La creazione di stanze è impedita dalle politiche del servizio"}. {"Room description","Descrizione della stanza"}. {"Room Occupants","Presenti nella stanza"}. +{"Room terminates","La stanza termina"}. {"Room title","Titolo della stanza"}. {"Roster groups allowed to subscribe","Gruppi roster abilitati alla registrazione"}. -{"Roster","Lista dei contatti"}. -{"Roster of ","Lista dei contatti di "}. {"Roster size","Dimensione della lista dei contatti"}. -{"RPC Call Error","Errore di chiamata RPC"}. {"Running Nodes","Nodi attivi"}. -{"~s access rule configuration","Configurazione delle regole di accesso per ~s"}. +{"~s invites you to the room ~s","~s ti invita nella stanza ~s"}. {"Saturday","Sabato"}. -{"Script check","Verifica dello script"}. +{"Search from the date","Cerca dalla data"}. {"Search Results for ","Risultati della ricerca per "}. +{"Search the text","Cerca nel testo"}. +{"Search until the date","Cerca fino alla data"}. {"Search users in ","Cercare utenti in "}. -{"Send announcement to all online users","Inviare l'annuncio a tutti gli utenti online"}. {"Send announcement to all online users on all hosts","Inviare l'annuncio a tutti gli utenti online su tutti gli host"}. -{"Send announcement to all users","Inviare l'annuncio a tutti gli utenti"}. +{"Send announcement to all online users","Inviare l'annuncio a tutti gli utenti online"}. {"Send announcement to all users on all hosts","Inviare l'annuncio a tutti gli utenti su tutti gli host"}. +{"Send announcement to all users","Inviare l'annuncio a tutti gli utenti"}. {"September","Settembre"}. -{"Server ~b","Server ~b"}. {"Server:","Server:"}. +{"Service list retrieval timed out","Il recupero dell'elenco dei servizi è scaduto"}. +{"Session state copying timed out","La copia dello stato della sessione è scaduta"}. {"Set message of the day and send to online users","Impostare il messaggio del giorno (MOTD) ed inviarlo agli utenti online"}. {"Set message of the day on all hosts and send to online users","Impostare il messaggio del giorno (MOTD) su tutti gli host e inviarlo agli utenti online"}. {"Shared Roster Groups","Gruppi di liste di contatti comuni"}. {"Show Integral Table","Mostrare la tabella integrale"}. +{"Show Occupants Join/Leave","Mostra gli occupanti che si uniscono/escono"}. {"Show Ordinary Table","Mostrare la tabella normale"}. {"Shut Down Service","Terminare il servizio"}. -{"~s invites you to the room ~s","~s ti invita nella stanza ~s"}. -{"Some Jabber clients can store your password in your computer. Use that feature only if you trust your computer is safe.","Alcuni client Jabber possono conservare la password nel tuo computer. Utilizza tale funzione soltanto se ritieni che il tuo computer sia sicuro."}. +{"SOCKS5 Bytestreams","SOCKS5 flussi di byte"}. +{"Some XMPP clients can store your password in the computer, but you should do this only in your personal computer for safety reasons.","Alcuni client XMPP possono memorizzare la tua password nel computer, ma dovresti farlo solo sul tuo computer personale per motivi di sicurezza."}. +{"Sources Specs:","Sorgenti Specifiche:"}. {"Specify the access model","Specificare il modello di accesso"}. {"Specify the event message type","Specificare il tipo di messaggio di evento"}. {"Specify the publisher model","Definire il modello di pubblicazione"}. -{"~s's Offline Messages Queue","Coda di ~s messaggi offline"}. -{"Start","Avviare"}. -{"Start Modules at ","Avviare moduli su "}. -{"Start Modules","Avviare moduli"}. -{"Statistics of ~p","Statistiche di ~p"}. -{"Statistics","Statistiche"}. -{"Stop","Arrestare"}. -{"Stop Modules","Arrestare moduli"}. -{"Stop Modules at ","Arrestare moduli su "}. +{"Stanza id is not valid","L'id della stanza non è valido"}. +{"Stanza ID","Stanza ID"}. +{"Statically specify a replyto of the node owner(s)","Specificare staticamente una risposta del proprietario(i) del nodo"}. {"Stopped Nodes","Nodi arrestati"}. -{"Storage Type","Tipo di conservazione"}. {"Store binary backup:","Conservare un salvataggio binario:"}. {"Store plain text backup:","Conservare un salvataggio come semplice testo:"}. +{"Stream management is already enabled","La gestione del flusso è già abilitata"}. +{"Stream management is not enabled","La gestione del flusso non è abilitata"}. {"Subject","Oggetto"}. -{"Submit","Inviare"}. {"Submitted","Inviato"}. {"Subscriber Address","Indirizzo dell'iscritta/o"}. -{"Subscription","Iscrizione"}. +{"Subscribers may publish","I sottoscrittori possono pubblicare"}. +{"Subscription requests must be approved and only subscribers may retrieve items","Le richieste di sottoscrizione devono essere approvate e solo i sottoscrittori possono recuperare le voci"}. +{"Subscriptions are not allowed","Le sottoscrizioni non sono consentite"}. {"Sunday","Domenica"}. +{"Text associated with a picture","Testo associato a un'immagine"}. +{"Text associated with a sound","Testo associato a un suono"}. +{"Text associated with a video","Testo associato a un video"}. +{"Text associated with speech","Testo associato al parlato"}. {"That nickname is already in use by another occupant","Il nickname è già in uso all'interno della conferenza"}. {"That nickname is registered by another person","Questo nickname è registrato da un'altra persona"}. +{"The account already exists","L'account esiste già"}. +{"The account was not unregistered","L'account non era non registrato"}. +{"The body text of the last received message","Il corpo del testo dell'ultimo messaggio ricevuto"}. {"The CAPTCHA is valid.","Il CAPTCHA è valido."}. {"The CAPTCHA verification has failed","La verifica del CAPTCHA ha avuto esito negativo"}. +{"The captcha you entered is wrong","Il captcha che hai inserito è sbagliato"}. +{"The child nodes (leaf or collection) associated with a collection","I nodi figlio (foglia o raccolta) associati a una raccolta"}. {"The collections with which a node is affiliated","Le collezioni a cui è affiliato un nodo"}. -{"the password is","la password è"}. +{"The DateTime at which a leased subscription will end or has ended","Il DateTime in cui un abbonamento in leasing terminerà o è terminato"}. +{"The datetime when the node was created","La dataora in cui è stato creato il nodo"}. +{"The default language of the node","La lingua predefinita del nodo"}. +{"The feature requested is not supported by the conference","La funzionalità richiesta non è supportata dalla conferenza"}. +{"The JID of the node creator","Il JID del creatore del nodo"}. +{"The JIDs of those to contact with questions","I JID di coloro da contattare per domande"}. +{"The JIDs of those with an affiliation of owner","I JID di coloro che hanno un'affiliazione di proprietario"}. +{"The JIDs of those with an affiliation of publisher","I JID di coloro che hanno un'affiliazione di editore"}. +{"The list of all online users","L'elenco di tutti gli utenti online"}. +{"The list of all users","L'elenco di tutti gli utenti"}. +{"The list of JIDs that may associate leaf nodes with a collection","L'elenco dei JID che possono associare i nodi foglia a una collezione"}. +{"The maximum number of child nodes that can be associated with a collection, or `max` for no specific limit other than a server imposed maximum","Il numero massimo di nodi figli che possono essere associati a una collezione, o `max` per non avere un limite specifico, se non quello imposto dal server"}. +{"The minimum number of milliseconds between sending any two notification digests","Il numero minimo di millisecondi tra l'invio di due digest di notifica qualsiasi"}. +{"The name of the node","Il nome del nodo"}. +{"The node is a collection node","Il nodo è un nodo di raccolta"}. +{"The node is a leaf node (default)","Il nodo è un nodo foglia (predefinito)"}. +{"The NodeID of the relevant node","Il NodeID del nodo rilevante"}. +{"The number of pending incoming presence subscription requests","Il numero di richieste di sottoscrizione di presenza in entrata in sospeso"}. +{"The number of subscribers to the node","Il numero di abbonati al nodo"}. +{"The number of unread or undelivered messages","Il numero di messaggi non letti o non consegnati"}. +{"The password contains unacceptable characters","La password contiene caratteri non accettabili"}. {"The password is too weak","La password è troppo debole"}. -{"The password of your Jabber account was successfully changed.","Il cambio di password del tuo account Jabber è andato a buon fine."}. -{"There was an error changing the password: ","Si è verificato un errore nel cambio di password: "}. +{"the password is","la password è"}. +{"The password of your XMPP account was successfully changed.","La password del tuo account XMPP è stata modificata con successo."}. +{"The password was not changed","La password non è stata modificata"}. +{"The passwords are different","Le password sono diverse"}. +{"The presence states for which an entity wants to receive notifications","Gli stati di presenza per i quali un'entità desidera ricevere le notifiche"}. +{"The query is only allowed from local users","La query è consentita solo da utenti locali"}. +{"The query must not contain elements","La query non deve contenere elementi "}. +{"The room subject can be modified by participants","L'oggetto della stanza potrà essere modificato dai partecipanti"}. +{"The semantic type information of data in the node, usually specified by the namespace of the payload (if any)","Le informazioni sul tipo semantico dei dati nel nodo, solitamente specificate dallo spazio dei nomi del payload (se presente)"}. +{"The sender of the last received message","Il mittente dell'ultimo messaggio ricevuto"}. +{"The stanza MUST contain only one element, one element, or one element","La stanza DEVE contenere solo un elemento , un elemento o un elemento "}. +{"The subscription identifier associated with the subscription request","L'identificatore di sottoscrizione associato alla richiesta di sottoscrizione"}. +{"The URL of an XSL transformation which can be applied to payloads in order to generate an appropriate message body element.","L'URL di una trasformazione XSL che può essere applicata ai payload per generare un elemento del corpo del messaggio appropriato."}. +{"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","L'URL di una trasformazione XSL che può essere applicata al formato del payload per generare un risultato valido di Data Forms che il client può visualizzare utilizzando un motore di rendering Data Forms generico"}. +{"There was an error changing the password: ","Si è verificato un errore durante la modifica della password: "}. {"There was an error creating the account: ","Si è verificato un errore nella creazione dell'account: "}. {"There was an error deleting the account: ","Si è verificato un errore nella cancellazione dell'account: "}. -{"This is case insensitive: macbeth is the same that MacBeth and Macbeth.","Non fa differenza fra minuscolo e maiuscolo: macbeth, MacBeth e Macbeth si equivalgono."}. -{"This page allows to create a Jabber account in this Jabber server. Your JID (Jabber IDentifier) will be of the form: username@server. Please read carefully the instructions to fill correctly the fields.","Questa pagina consente di creare un account Jabber in questo server Jabber. Il tuo JID (Jabber IDentifier) avrà la forma: nome_utente@server. Leggi attentamente le istruzioni per compilare i campi correttamente."}. -{"This page allows to unregister a Jabber account in this Jabber server.","Questa pagina consente di eliminare un account Jabber da questo server Jabber."}. -{"This participant is kicked from the room because he sent an error message","Partecipante espulso dalla stanza perché ha inviato un messaggio non valido"}. -{"This participant is kicked from the room because he sent an error message to another participant","Partecipante espulso dalla stanza perché ha inviato un messaggio non valido a un altro partecipante"}. -{"This participant is kicked from the room because he sent an error presence","Partecipante espulso dalla stanza perché ha inviato una presenza non valido"}. +{"This is case insensitive: macbeth is the same that MacBeth and Macbeth.","Questo è insensibile alle maiuscole: macbeth è lo stesso che MacBeth e 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.","Questa pagina consente di registrare un account XMPP in questo server XMPP. Il tuo JID (Jabber ID) sarà nel formato: nomeutente@server. Si prega di leggere attentamente le istruzioni per compilare correttamente i campi."}. +{"This page allows to unregister an XMPP account in this XMPP server.","Questa pagina consente di annullare la registrazione di un account XMPP in questo server XMPP."}. {"This room is not anonymous","Questa stanza non è anonima"}. +{"This service can not process the address: ~s","Questo servizio non può elaborare l'indirizzo: ~s"}. {"Thursday","Giovedì"}. {"Time delay","Ritardo"}. -{"Time","Ora"}. -{"To","A"}. +{"Timed out waiting for stream resumption","Timed out in attesa della ripresa dello stream"}. +{"To register, visit ~s","Per registrarsi, visita ~s"}. +{"To ~ts","A ~ts"}. +{"Token TTL","Gettone TTL"}. +{"Too many active bytestreams","Troppi bytestream attivi"}. {"Too many CAPTCHA requests","Troppe richieste CAPTCHA"}. -{"To ~s","A ~s"}. +{"Too many child elements","Troppi elementi figlio"}. +{"Too many elements","Troppi elementi "}. +{"Too many elements","Troppi elementi "}. +{"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","Troppe (~p) autenticazioni non riuscite da questo indirizzo IP (~s). L'indirizzo verrà sbloccato alle ~s UTC"}. +{"Too many receiver fields were specified","Sono stati specificati troppi campi del ricevitore"}. +{"Too many unacked stanzas","Troppe stanze non riconosciute"}. +{"Too many users in this conference","Troppi utenti in questa conferenza"}. {"Traffic rate limit is exceeded","Limite di traffico superato"}. -{"Transactions Aborted:","Transazioni abortite:"}. -{"Transactions Committed:","Transazioni avvenute:"}. -{"Transactions Logged:","Transazioni con log:"}. -{"Transactions Restarted:","Transazioni riavviate:"}. +{"~ts's MAM Archive","Archivio MAM di ~ts"}. +{"~ts's Offline Messages Queue","La Coda dei Messaggi Offline di ~ts"}. {"Tuesday","Martedì"}. {"Unable to generate a CAPTCHA","Impossibile generare un CAPTCHA"}. +{"Unable to register route on existing local domain","Impossibile registrare il percorso sul dominio locale esistente"}. {"Unauthorized","Non autorizzato"}. -{"Unregister a Jabber account","Elimina un account Jabber"}. +{"Unexpected action","Azione inaspettata"}. +{"Unexpected error condition: ~p","Condizione di errore imprevisto: ~p"}. +{"Uninstall","Disinstallare"}. +{"Unregister an XMPP account","Disregistrare un account XMPP"}. {"Unregister","Elimina"}. -{"Update ","Aggiornare "}. -{"Update","Aggiornare"}. +{"Unsupported element","Elemento non supportato"}. +{"Unsupported version","Versione non supportata"}. {"Update message of the day (don't send)","Aggiornare il messaggio del giorno (MOTD) (non inviarlo)"}. {"Update message of the day on all hosts (don't send)","Aggiornare il messaggio del giorno (MOTD) su tutti gli host (non inviarlo)"}. -{"Update plan","Piano di aggiornamento"}. -{"Update script","Script di aggiornamento"}. -{"Uptime:","Tempo dall'avvio:"}. -{"Use of STARTTLS required","Utilizzo di STARTTLS obbligatorio"}. +{"Update specs to get modules source, then install desired ones.","Aggiorna le specifiche per ottenere il sorgente dei moduli, quindi installa quelli desiderati."}. +{"Update Specs","Aggiorna Specifiche"}. +{"Updating the vCard is not supported by the vCard storage backend","L'aggiornamento della vCard non è supportato dal backend di archiviazione vCard"}. +{"Upgrade","Aggiornamento"}. +{"URL for Archived Discussion Logs","URL per i Registri delle Discussioni Archiviati"}. +{"User already exists","L'utente esiste già"}. {"User JID","JID utente"}. +{"User (jid)","Utente (jid)"}. {"User Management","Gestione degli utenti"}. +{"User not allowed to perform an IQ set on another user's vCard.","L'utente non è autorizzato a eseguire un set IQ sulla vCard di un altro utente."}. +{"User removed","Utente rimosso"}. +{"User session not found","Sessione utente non trovata"}. +{"User session terminated","Sessione utente terminata"}. +{"User ~ts","Utente ~ts"}. {"Username:","Nome utente:"}. {"Users are not allowed to register accounts so quickly","Non è consentito agli utenti registrare account così rapidamente"}. {"Users Last Activity","Ultima attività degli utenti"}. {"Users","Utenti"}. -{"User ","Utente "}. {"User","Utente"}. -{"Validate","Validare"}. +{"Value 'get' of 'type' attribute is not allowed","Il valore 'get' dell'attributo 'type' non è consentito"}. +{"Value of '~s' should be boolean","Il valore di '~s' dovrebbe essere booleano"}. +{"Value of '~s' should be datetime string","Il valore di '~s' deve essere una stringa dataora"}. +{"Value of '~s' should be integer","Il valore di '~s' dovrebbe essere un intero"}. +{"Value 'set' of 'type' attribute is not allowed","Il valore 'set' dell'attributo 'type' non è consentito"}. {"vCard User Search","Ricerca di utenti per vCard"}. +{"View joined MIX channels","Visualizza i canali MIX uniti"}. {"Virtual Hosts","Host Virtuali"}. {"Visitors are not allowed to change their nicknames in this room","Non è consentito ai visitatori cambiare il nickname in questa stanza"}. {"Visitors are not allowed to send messages to all occupants","Non è consentito ai visitatori l'invio di messaggi a tutti i presenti"}. +{"Visitor","Visitatore"}. {"Voice request","Richiesta di parola"}. {"Voice requests are disabled in this conference","In questa conferenza le richieste di parola sono escluse"}. {"Wednesday","Mercoledì"}. +{"When a new subscription is processed and whenever a subscriber comes online","Quando viene elaborato una nuova sottoscrizione e ogni volta che una sottoscrizione entra in linea"}. +{"When a new subscription is processed","Quando viene elaborata una nuova sottoscrizione"}. {"When to send the last published item","Quando inviare l'ultimo elemento pubblicato"}. -{"Whether to allow subscriptions","Consentire iscrizioni?"}. -{"You can later change your password using a Jabber client.","Potrai in seguito cambiare la password utilizzando un client Jabber."}. +{"Whether an entity wants to receive an XMPP message body in addition to the payload format","Se un'entità vuole ricevere un corpo del messaggio XMPP inoltre al formato del payload"}. +{"Whether an entity wants to receive digests (aggregations) of notifications or all notifications individually","Se un'entità desidera ricevere i digest (aggregazioni) di notifiche o tutte le notifiche individualmente"}. +{"Whether an entity wants to receive or disable notifications","Se un'entità desidera ricevere o disabilitare le notifiche"}. +{"Whether owners or publisher should receive replies to items","Se i proprietari o l'editore dovrebbero ricevere le risposte alle voci"}. +{"Whether the node is a leaf (default) or a collection","Se il nodo è una foglia (impostazione predefinita) o una collezione"}. +{"Whether to allow subscriptions","Se consentire le iscrizioni"}. +{"Whether to make all subscriptions temporary, based on subscriber presence","Se rendere temporanee tutte le sottoscrizioni, in base alla presenza della sottoscrizione"}. +{"Whether to notify owners about new subscribers and unsubscribes","Se notificare ai proprietari le nuove sottoscrizioni e le cancellazioni"}. +{"Who can send private messages","Chi può inviare messaggi privati"}. +{"Who may associate leaf nodes with a collection","Chi può associare i nodi foglia a una collezione"}. +{"Wrong parameters in the web formulary","Parametri errati nel formulario web"}. +{"Wrong xmlns","xmlns errati"}. +{"XMPP Account Registration","Registrazione dell'account XMPP"}. +{"XMPP Domains","Domini XMPP"}. +{"XMPP Show Value of Away","XMPP Mostra Valore di Assenza"}. +{"XMPP Show Value of Chat","XMPP Mostra il Valore della Chat"}. +{"XMPP Show Value of DND (Do Not Disturb)","XMPP Mostra Valore di DND (Non Disturbare)"}. +{"XMPP Show Value of XA (Extended Away)","XMPP Mostra Valore di XA (Extended Away)"}. +{"XMPP URI of Associated Publish-Subscribe Node","URI XMPP del Nodo di Pubblicazione-Sottoscrizione Associato"}. +{"You are being removed from the room because of a system shutdown","Stai per essere rimosso dalla stanza a causa di un arresto del sistema"}. +{"You are not allowed to send private messages","Non ti è consentito inviare messaggi privati"}. +{"You are not joined to the channel","Non si è connessi al canale"}. +{"You can later change your password using an XMPP client.","È possibile modificare successivamente la tua password utilizzando un client XMPP."}. {"You have been banned from this room","Sei stata/o bandita/o da questa stanza"}. +{"You have joined too many conferences","Hai partecipato a troppe conferenze"}. {"You must fill in field \"Nickname\" in the form","Si deve riempire il campo \"Nickname\" nel modulo"}. {"You need a client that supports x:data and CAPTCHA to register","La registrazione richiede un client che supporti x:data e CAPTCHA"}. {"You need a client that supports x:data to register the nickname","Per registrare il nickname è necessario un client che supporti x:data"}. -{"You need an x:data capable client to configure mod_irc settings","Per la configurazione del modulo IRC è necessario un client che supporti x:data"}. -{"You need an x:data capable client to configure room","Per la configurazione della stanza è necessario un client che supporti x:data"}. {"You need an x:data capable client to search","Per effettuare ricerche è necessario un client che supporti x:data"}. {"Your active privacy list has denied the routing of this stanza.","In base alla tua attuale lista privacy questa stanza è stata esclusa dalla navigazione."}. -{"Your contact offline message queue is full. The message has been discarded.","La coda dei messaggi offline del contatto è piena. Il messaggio è stato scartato"}. -{"Your Jabber account was successfully created.","La creazione del tuo account Jabber è andata a buon fine."}. -{"Your Jabber account was successfully deleted.","La cancellazione del tuo account Jabber è andata a buon fine."}. -{"Your messages to ~s are being blocked. To unblock them, visit ~s","I messaggi verso ~s sono bloccati. Per sbloccarli, visitare ~s"}. +{"Your contact offline message queue is full. The message has been discarded.","La coda dei messaggi offline del contatto è piena. Il messaggio è stato scartato."}. +{"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","I messaggi verso ~s sono bloccati. Per sbloccarli, visitare ~s"}. +{"Your XMPP account was successfully registered.","Il tuo account XMPP è stato registrato con successo."}. +{"Your XMPP account was successfully unregistered.","L'account XMPP è stato disregistrato con successo."}. +{"You're not allowed to create nodes","Non ti è consentito creare nodi"}. diff --git a/priv/msgs/it.po b/priv/msgs/it.po deleted file mode 100644 index f15ad8ba9..000000000 --- a/priv/msgs/it.po +++ /dev/null @@ -1,1875 +0,0 @@ -# Luca Brivio , 2012. -msgid "" -msgstr "" -"Project-Id-Version: 2.1.0-alpha\n" -"PO-Revision-Date: 2012-04-24 16:48+0200\n" -"Last-Translator: Luca Brivio \n" -"Language-Team: Italian \n" -"Language: it\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Language: Italian (italiano)\n" -"X-Additional-Translator: Gabriele Stilli \n" -"X-Additional-Translator: Smart2128\n" -"X-Generator: Lokalize 1.2\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#: ejabberd_c2s.erl:424 ejabberd_c2s.erl:727 -msgid "Use of STARTTLS required" -msgstr "Utilizzo di STARTTLS obbligatorio" - -#: ejabberd_c2s.erl:503 -msgid "No resource provided" -msgstr "Nessuna risorsa fornita" - -#: ejabberd_c2s.erl:1197 -msgid "Replaced by new connection" -msgstr "Sostituito da una nuova connessione" - -#: ejabberd_c2s.erl:1885 -msgid "Your active privacy list has denied the routing of this stanza." -msgstr "" -"In base alla tua attuale lista privacy questa stanza è stata esclusa dalla " -"navigazione." - -#: ejabberd_captcha.erl:96 ejabberd_captcha.erl:152 ejabberd_captcha.erl:178 -msgid "Enter the text you see" -msgstr "Immettere il testo visibile" - -#: ejabberd_captcha.erl:101 -msgid "Your messages to ~s are being blocked. To unblock them, visit ~s" -msgstr "I messaggi verso ~s sono bloccati. Per sbloccarli, visitare ~s" - -#: ejabberd_captcha.erl:134 -msgid "If you don't see the CAPTCHA image here, visit the web page." -msgstr "Se qui non vedi l'immagine CAPTCHA, visita la pagina web." - -#: ejabberd_captcha.erl:146 -msgid "CAPTCHA web page" -msgstr "Pagina web CAPTCHA" - -#: ejabberd_captcha.erl:307 -msgid "The CAPTCHA is valid." -msgstr "Il CAPTCHA è valido." - -#: mod_adhoc.erl:95 mod_adhoc.erl:125 mod_adhoc.erl:143 mod_adhoc.erl:161 -msgid "Commands" -msgstr "Comandi" - -#: mod_adhoc.erl:149 mod_adhoc.erl:243 -msgid "Ping" -msgstr "Ping" - -#: mod_adhoc.erl:260 -msgid "Pong" -msgstr "Pong" - -#: mod_announce.erl:507 mod_announce_odbc.erl:499 -msgid "Really delete message of the day?" -msgstr "Si conferma l'eliminazione del messaggio del giorno (MOTD)?" - -#: mod_announce.erl:515 mod_announce_odbc.erl:507 mod_configure.erl:1083 -#: mod_configure.erl:1128 -msgid "Subject" -msgstr "Oggetto" - -#: mod_announce.erl:520 mod_announce_odbc.erl:512 mod_configure.erl:1088 -#: mod_configure.erl:1133 -msgid "Message body" -msgstr "Corpo del messaggio" - -#: mod_announce.erl:600 mod_announce_odbc.erl:592 -msgid "No body provided for announce message" -msgstr "Nessun corpo fornito per il messaggio di annuncio" - -#: mod_announce.erl:635 mod_announce_odbc.erl:627 -msgid "Announcements" -msgstr "Annunci" - -#: mod_announce.erl:637 mod_announce_odbc.erl:629 -msgid "Send announcement to all users" -msgstr "Inviare l'annuncio a tutti gli utenti" - -#: mod_announce.erl:639 mod_announce_odbc.erl:631 -msgid "Send announcement to all users on all hosts" -msgstr "Inviare l'annuncio a tutti gli utenti su tutti gli host" - -#: mod_announce.erl:641 mod_announce_odbc.erl:633 -msgid "Send announcement to all online users" -msgstr "Inviare l'annuncio a tutti gli utenti online" - -#: mod_announce.erl:643 mod_announce_odbc.erl:635 mod_configure.erl:1078 -#: mod_configure.erl:1123 -msgid "Send announcement to all online users on all hosts" -msgstr "Inviare l'annuncio a tutti gli utenti online su tutti gli host" - -#: mod_announce.erl:645 mod_announce_odbc.erl:637 -msgid "Set message of the day and send to online users" -msgstr "" -"Impostare il messaggio del giorno (MOTD) ed inviarlo agli utenti online" - -#: mod_announce.erl:647 mod_announce_odbc.erl:639 -msgid "Set message of the day on all hosts and send to online users" -msgstr "" -"Impostare il messaggio del giorno (MOTD) su tutti gli host e inviarlo agli " -"utenti online" - -#: mod_announce.erl:649 mod_announce_odbc.erl:641 -msgid "Update message of the day (don't send)" -msgstr "Aggiornare il messaggio del giorno (MOTD) (non inviarlo)" - -#: mod_announce.erl:651 mod_announce_odbc.erl:643 -msgid "Update message of the day on all hosts (don't send)" -msgstr "" -"Aggiornare il messaggio del giorno (MOTD) su tutti gli host (non inviarlo)" - -#: mod_announce.erl:653 mod_announce_odbc.erl:645 -msgid "Delete message of the day" -msgstr "Eliminare il messaggio del giorno (MOTD)" - -#: mod_announce.erl:655 mod_announce_odbc.erl:647 -msgid "Delete message of the day on all hosts" -msgstr "Eliminare il messaggio del giorno (MOTD) su tutti gli host" - -#: mod_configure.erl:114 mod_configure.erl:274 mod_configure.erl:296 -#: mod_configure.erl:498 -msgid "Configuration" -msgstr "Configurazione" - -#: mod_configure.erl:125 mod_configure.erl:576 web/ejabberd_web_admin.erl:1936 -msgid "Database" -msgstr "Database" - -#: mod_configure.erl:127 mod_configure.erl:595 -msgid "Start Modules" -msgstr "Avviare moduli" - -#: mod_configure.erl:129 mod_configure.erl:596 -msgid "Stop Modules" -msgstr "Arrestare moduli" - -#: mod_configure.erl:131 mod_configure.erl:604 web/ejabberd_web_admin.erl:1937 -msgid "Backup" -msgstr "Salvare" - -#: mod_configure.erl:133 mod_configure.erl:605 -msgid "Restore" -msgstr "Recuperare" - -#: mod_configure.erl:135 mod_configure.erl:606 -msgid "Dump to Text File" -msgstr "Trascrivere su file di testo" - -#: mod_configure.erl:137 mod_configure.erl:615 -msgid "Import File" -msgstr "Importare un file" - -#: mod_configure.erl:139 mod_configure.erl:616 -msgid "Import Directory" -msgstr "Importare una directory" - -#: mod_configure.erl:141 mod_configure.erl:581 mod_configure.erl:1057 -msgid "Restart Service" -msgstr "Riavviare il servizio" - -#: mod_configure.erl:143 mod_configure.erl:582 mod_configure.erl:1102 -msgid "Shut Down Service" -msgstr "Terminare il servizio" - -#: mod_configure.erl:145 mod_configure.erl:518 mod_configure.erl:1197 -#: web/ejabberd_web_admin.erl:1527 -msgid "Add User" -msgstr "Aggiungere un utente" - -#: mod_configure.erl:147 mod_configure.erl:519 mod_configure.erl:1219 -msgid "Delete User" -msgstr "Eliminare l'utente" - -#: mod_configure.erl:149 mod_configure.erl:520 mod_configure.erl:1231 -msgid "End User Session" -msgstr "Terminare la sessione dell'utente" - -#: mod_configure.erl:151 mod_configure.erl:521 mod_configure.erl:1243 -#: mod_configure.erl:1255 -msgid "Get User Password" -msgstr "Ottenere la password dell'utente" - -#: mod_configure.erl:153 mod_configure.erl:522 -msgid "Change User Password" -msgstr "Cambiare la password dell'utente" - -#: mod_configure.erl:155 mod_configure.erl:523 mod_configure.erl:1272 -msgid "Get User Last Login Time" -msgstr "Ottenere la data di ultimo accesso dell'utente" - -#: mod_configure.erl:157 mod_configure.erl:524 mod_configure.erl:1284 -msgid "Get User Statistics" -msgstr "Ottenere le statistiche dell'utente" - -#: mod_configure.erl:159 mod_configure.erl:525 -msgid "Get Number of Registered Users" -msgstr "Ottenere il numero di utenti registrati" - -#: mod_configure.erl:161 mod_configure.erl:526 -msgid "Get Number of Online Users" -msgstr "Ottenere il numero di utenti online" - -#: mod_configure.erl:163 mod_configure.erl:509 web/ejabberd_web_admin.erl:831 -#: web/ejabberd_web_admin.erl:872 -msgid "Access Control Lists" -msgstr "Diritti di accesso (ACL)" - -#: mod_configure.erl:165 mod_configure.erl:510 web/ejabberd_web_admin.erl:940 -#: web/ejabberd_web_admin.erl:976 -msgid "Access Rules" -msgstr "Regole di accesso" - -#: mod_configure.erl:297 mod_configure.erl:499 -msgid "User Management" -msgstr "Gestione degli utenti" - -#: mod_configure.erl:500 web/ejabberd_web_admin.erl:1058 -#: web/ejabberd_web_admin.erl:1462 -msgid "Online Users" -msgstr "Utenti online" - -#: mod_configure.erl:501 -msgid "All Users" -msgstr "Tutti gli utenti" - -#: mod_configure.erl:502 -msgid "Outgoing s2s Connections" -msgstr "Connessioni s2s in uscita" - -#: mod_configure.erl:503 web/ejabberd_web_admin.erl:1907 -msgid "Running Nodes" -msgstr "Nodi attivi" - -#: mod_configure.erl:504 web/ejabberd_web_admin.erl:1909 -msgid "Stopped Nodes" -msgstr "Nodi arrestati" - -#: mod_configure.erl:577 mod_configure.erl:587 web/ejabberd_web_admin.erl:1953 -msgid "Modules" -msgstr "Moduli" - -#: mod_configure.erl:578 -msgid "Backup Management" -msgstr "Gestione dei salvataggi" - -#: mod_configure.erl:579 -msgid "Import Users From jabberd14 Spool Files" -msgstr "Importare utenti da file di spool di jabberd14" - -#: mod_configure.erl:699 -msgid "To ~s" -msgstr "A ~s" - -#: mod_configure.erl:717 -msgid "From ~s" -msgstr "Da ~s" - -#: mod_configure.erl:913 -msgid "Database Tables Configuration at " -msgstr "Configurazione delle tabelle del database su " - -#: mod_configure.erl:918 -msgid "Choose storage type of tables" -msgstr "Selezionare una modalità di conservazione delle tabelle" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Disc only copy" -msgstr "Copia su disco soltanto" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM and disc copy" -msgstr "Copia in memoria (RAM) e su disco" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM copy" -msgstr "Copia in memoria (RAM)" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Remote copy" -msgstr "Copia remota" - -#: mod_configure.erl:950 -msgid "Stop Modules at " -msgstr "Arrestare moduli su " - -#: mod_configure.erl:954 -msgid "Choose modules to stop" -msgstr "Selezionare i moduli da arrestare" - -#: mod_configure.erl:969 -msgid "Start Modules at " -msgstr "Avviare moduli su " - -#: mod_configure.erl:973 -msgid "Enter list of {Module, [Options]}" -msgstr "Immettere un elenco di {Modulo, [Opzioni]}" - -#: mod_configure.erl:974 -msgid "List of modules to start" -msgstr "Elenco dei moduli da avviare" - -#: mod_configure.erl:983 -msgid "Backup to File at " -msgstr "Salvataggio sul file " - -#: mod_configure.erl:987 mod_configure.erl:1001 -msgid "Enter path to backup file" -msgstr "Immettere il percorso del file di salvataggio" - -#: mod_configure.erl:988 mod_configure.erl:1002 mod_configure.erl:1016 -#: mod_configure.erl:1030 -msgid "Path to File" -msgstr "Percorso del file" - -#: mod_configure.erl:997 -msgid "Restore Backup from File at " -msgstr "Recuperare il salvataggio dal file " - -#: mod_configure.erl:1011 -msgid "Dump Backup to Text File at " -msgstr "Trascrivere il salvataggio sul file di testo " - -#: mod_configure.erl:1015 -msgid "Enter path to text file" -msgstr "Immettere il percorso del file di testo" - -#: mod_configure.erl:1025 -msgid "Import User from File at " -msgstr "Importare un utente dal file " - -#: mod_configure.erl:1029 -msgid "Enter path to jabberd14 spool file" -msgstr "Immettere il percorso del file di spool di jabberd14" - -#: mod_configure.erl:1039 -msgid "Import Users from Dir at " -msgstr "Importare utenti dalla directory " - -#: mod_configure.erl:1043 -msgid "Enter path to jabberd14 spool dir" -msgstr "Immettere il percorso della directory di spool di jabberd14" - -#: mod_configure.erl:1044 -msgid "Path to Dir" -msgstr "Percorso della directory" - -#: mod_configure.erl:1060 mod_configure.erl:1105 -msgid "Time delay" -msgstr "Ritardo" - -#: mod_configure.erl:1143 -msgid "Access Control List Configuration" -msgstr "Configurazione dei diritti di accesso (ACL)" - -#: mod_configure.erl:1147 -msgid "Access control lists" -msgstr "Diritti di accesso (ACL)" - -#: mod_configure.erl:1171 -msgid "Access Configuration" -msgstr "Configurazione dell'accesso" - -#: mod_configure.erl:1175 -msgid "Access rules" -msgstr "Regole di accesso" - -#: mod_configure.erl:1200 mod_configure.erl:1222 mod_configure.erl:1234 -#: mod_configure.erl:1246 mod_configure.erl:1258 mod_configure.erl:1275 -#: mod_configure.erl:1287 mod_configure.erl:1650 mod_configure.erl:1700 -#: mod_configure.erl:1721 mod_roster.erl:943 mod_roster_odbc.erl:1060 -#: mod_vcard.erl:472 mod_vcard_ldap.erl:554 mod_vcard_odbc.erl:448 -msgid "Jabber ID" -msgstr "Jabber ID (Jabber ID)" - -#: mod_configure.erl:1205 mod_configure.erl:1263 mod_configure.erl:1651 -#: mod_configure.erl:1863 mod_muc/mod_muc_room.erl:3224 mod_register.erl:235 -#: web/ejabberd_web_admin.erl:1520 -msgid "Password" -msgstr "Password" - -#: mod_configure.erl:1210 -msgid "Password Verification" -msgstr "Verifica della password" - -#: mod_configure.erl:1301 -msgid "Number of registered users" -msgstr "Numero di utenti registrati" - -#: mod_configure.erl:1315 -msgid "Number of online users" -msgstr "Numero di utenti online" - -#: mod_configure.erl:1682 web/ejabberd_web_admin.erl:1588 -#: web/ejabberd_web_admin.erl:1743 -msgid "Never" -msgstr "Mai" - -#: mod_configure.erl:1696 web/ejabberd_web_admin.erl:1601 -#: web/ejabberd_web_admin.erl:1756 -msgid "Online" -msgstr "Online" - -#: mod_configure.erl:1701 -msgid "Last login" -msgstr "Ultimo accesso" - -#: mod_configure.erl:1722 -msgid "Roster size" -msgstr "Dimensione della lista dei contatti" - -#: mod_configure.erl:1723 -msgid "IP addresses" -msgstr "Indirizzi IP" - -#: mod_configure.erl:1724 -msgid "Resources" -msgstr "Risorse" - -#: mod_configure.erl:1850 -msgid "Administration of " -msgstr "Amministrazione di " - -#: mod_configure.erl:1853 -msgid "Action on user" -msgstr "Azione sull'utente" - -#: mod_configure.erl:1857 -msgid "Edit Properties" -msgstr "Modificare le proprietà" - -#: mod_configure.erl:1860 web/ejabberd_web_admin.erl:1769 -msgid "Remove User" -msgstr "Eliminare l'utente" - -#: mod_irc/mod_irc.erl:201 mod_irc/mod_irc_odbc.erl:196 -#: mod_muc/mod_muc.erl:336 mod_muc/mod_muc_odbc.erl:342 -msgid "Access denied by service policy" -msgstr "Accesso impedito dalle politiche del servizio" - -#: mod_irc/mod_irc.erl:401 mod_irc/mod_irc_odbc.erl:398 -msgid "IRC Transport" -msgstr "Transport IRC" - -#: mod_irc/mod_irc.erl:428 mod_irc/mod_irc_odbc.erl:425 -msgid "ejabberd IRC module" -msgstr "Modulo IRC per ejabberd" - -#: mod_irc/mod_irc.erl:559 mod_irc/mod_irc_odbc.erl:568 -msgid "You need an x:data capable client to configure mod_irc settings" -msgstr "" -"Per la configurazione del modulo IRC è necessario un client che supporti x:" -"data" - -#: mod_irc/mod_irc.erl:566 mod_irc/mod_irc_odbc.erl:575 -msgid "Registration in mod_irc for " -msgstr "Registrazione in mod_irc per " - -#: mod_irc/mod_irc.erl:571 mod_irc/mod_irc_odbc.erl:580 -msgid "" -"Enter username, encodings, ports and passwords you wish to use for " -"connecting to IRC servers" -msgstr "" -"Immettere il nome utente, le codifiche, le porte e le password che si " -"desidera utilizzare per la connessione ai server IRC" - -#: mod_irc/mod_irc.erl:576 mod_irc/mod_irc_odbc.erl:585 -msgid "IRC Username" -msgstr "Nome utente IRC" - -#: mod_irc/mod_irc.erl:586 mod_irc/mod_irc_odbc.erl:595 -msgid "" -"If you want to specify different ports, passwords, encodings for IRC " -"servers, fill this list with values in format '{\"irc server\", \"encoding" -"\", port, \"password\"}'. By default this service use \"~s\" encoding, port " -"~p, empty password." -msgstr "" -"Se si vogliono specificare differenti porte, password, codifiche per i " -"server IRC, si riempia questo elenco con valori nel formato '{\"server IRC" -"\", \"codifica\", porta, \"password\"}'. Per default questo servizio " -"utilizza la codifica \"~s\", la porta ~p, la password vuota." - -#: mod_irc/mod_irc.erl:598 mod_irc/mod_irc_odbc.erl:607 -msgid "" -"Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef." -"net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]." -msgstr "" -"Esempio: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"segreto\"}, {\"vendetta." -"fef.net\", \"iso8859-1\", 7000}, {\"irc.serverdiprova.net\", \"utf-8\"}]." - -#: mod_irc/mod_irc.erl:603 mod_irc/mod_irc_odbc.erl:612 -msgid "Connections parameters" -msgstr "Parametri delle connessioni" - -#: mod_irc/mod_irc.erl:728 mod_irc/mod_irc_odbc.erl:757 -msgid "Join IRC channel" -msgstr "Entra nel canale IRC" - -#: mod_irc/mod_irc.erl:732 mod_irc/mod_irc_odbc.erl:761 -msgid "IRC channel (don't put the first #)" -msgstr "Canale IRC (senza il # iniziale)" - -#: mod_irc/mod_irc.erl:737 mod_irc/mod_irc_odbc.erl:766 -msgid "IRC server" -msgstr "Server IRC" - -#: mod_irc/mod_irc.erl:770 mod_irc/mod_irc.erl:774 -#: mod_irc/mod_irc_odbc.erl:799 mod_irc/mod_irc_odbc.erl:803 -msgid "Join the IRC channel here." -msgstr "Entra nel canale IRC qui." - -#: mod_irc/mod_irc.erl:778 mod_irc/mod_irc_odbc.erl:807 -msgid "Join the IRC channel in this Jabber ID: ~s" -msgstr "Entra nel canale IRC in questo ID Jabber: ~s" - -#: mod_irc/mod_irc.erl:863 mod_irc/mod_irc_odbc.erl:904 -msgid "IRC settings" -msgstr "Impostazioni IRC" - -#: mod_irc/mod_irc.erl:868 mod_irc/mod_irc_odbc.erl:909 -msgid "" -"Enter username and encodings you wish to use for connecting to IRC servers. " -"Press 'Next' to get more fields to fill in. Press 'Complete' to save " -"settings." -msgstr "" -"Immettere il nome utente e le codifiche che si desidera utilizzare per la " -"connessione ai server IRC. Premere \"Avanti\" per vedere i successivi campi " -"da compilare. Premere \"Fatto\" per salvare le impostazioni." - -#: mod_irc/mod_irc.erl:874 mod_irc/mod_irc_odbc.erl:915 -msgid "IRC username" -msgstr "Nome utente IRC" - -#: mod_irc/mod_irc.erl:923 mod_irc/mod_irc_odbc.erl:964 -msgid "Password ~b" -msgstr "Password ~b" - -#: mod_irc/mod_irc.erl:928 mod_irc/mod_irc_odbc.erl:969 -msgid "Port ~b" -msgstr "Porta ~b" - -#: mod_irc/mod_irc.erl:933 mod_irc/mod_irc_odbc.erl:974 -msgid "Encoding for server ~b" -msgstr "Codifica per il server ~b" - -#: mod_irc/mod_irc.erl:942 mod_irc/mod_irc_odbc.erl:983 -msgid "Server ~b" -msgstr "Server ~b" - -#: mod_muc/mod_muc.erl:449 mod_muc/mod_muc_odbc.erl:456 -msgid "Only service administrators are allowed to send service messages" -msgstr "" -"L'invio di messaggi di servizio è consentito solamente agli amministratori " -"del servizio" - -#: mod_muc/mod_muc.erl:493 mod_muc/mod_muc_odbc.erl:500 -msgid "Room creation is denied by service policy" -msgstr "La creazione di stanze è impedita dalle politiche del servizio" - -#: mod_muc/mod_muc.erl:500 mod_muc/mod_muc_odbc.erl:507 -msgid "Conference room does not exist" -msgstr "La stanza per conferenze non esiste" - -#: mod_muc/mod_muc.erl:582 mod_muc/mod_muc_odbc.erl:595 -msgid "Chatrooms" -msgstr "Stanze" - -#: mod_muc/mod_muc.erl:712 mod_muc/mod_muc_odbc.erl:729 -msgid "You need a client that supports x:data to register the nickname" -msgstr "Per registrare il nickname è necessario un client che supporti x:data" - -#: mod_muc/mod_muc.erl:718 mod_muc/mod_muc_odbc.erl:735 -msgid "Nickname Registration at " -msgstr "Registrazione di un nickname su " - -#: mod_muc/mod_muc.erl:722 mod_muc/mod_muc_odbc.erl:739 -msgid "Enter nickname you want to register" -msgstr "Immettere il nickname che si vuole registrare" - -#: mod_muc/mod_muc.erl:723 mod_muc/mod_muc_odbc.erl:740 -#: mod_muc/mod_muc_room.erl:3810 mod_roster.erl:944 mod_roster_odbc.erl:1061 -#: mod_vcard.erl:364 mod_vcard.erl:477 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:453 -msgid "Nickname" -msgstr "Nickname" - -#: mod_muc/mod_muc.erl:762 mod_muc/mod_muc_odbc.erl:784 -#: mod_muc/mod_muc_room.erl:1064 mod_muc/mod_muc_room.erl:1787 -msgid "That nickname is registered by another person" -msgstr "Questo nickname è registrato da un'altra persona" - -#: mod_muc/mod_muc.erl:788 mod_muc/mod_muc_odbc.erl:811 -msgid "You must fill in field \"Nickname\" in the form" -msgstr "Si deve riempire il campo \"Nickname\" nel modulo" - -#: mod_muc/mod_muc.erl:808 mod_muc/mod_muc_odbc.erl:831 -msgid "ejabberd MUC module" -msgstr "Modulo MUC per ejabberd" - -#: mod_muc/mod_muc_log.erl:374 mod_muc/mod_muc_log.erl:381 -msgid "Chatroom configuration modified" -msgstr "Configurazione della stanza modificata" - -#: mod_muc/mod_muc_log.erl:384 -msgid "joins the room" -msgstr "entra nella stanza" - -#: mod_muc/mod_muc_log.erl:387 mod_muc/mod_muc_log.erl:390 -msgid "leaves the room" -msgstr "esce dalla stanza" - -#: mod_muc/mod_muc_log.erl:393 mod_muc/mod_muc_log.erl:396 -msgid "has been banned" -msgstr "è stata/o bandita/o" - -#: mod_muc/mod_muc_log.erl:399 mod_muc/mod_muc_log.erl:402 -msgid "has been kicked" -msgstr "è stata/o espulsa/o" - -#: mod_muc/mod_muc_log.erl:405 -msgid "has been kicked because of an affiliation change" -msgstr "è stato espulso a causa di un cambiamento di appartenenza" - -#: mod_muc/mod_muc_log.erl:408 -msgid "has been kicked because the room has been changed to members-only" -msgstr "è stato espulso per la limitazione della stanza ai soli membri" - -#: mod_muc/mod_muc_log.erl:411 -msgid "has been kicked because of a system shutdown" -msgstr "è stato espulso a causa dello spegnimento del sistema" - -#: mod_muc/mod_muc_log.erl:414 -msgid "is now known as" -msgstr "è ora conosciuta/o come" - -#: mod_muc/mod_muc_log.erl:417 mod_muc/mod_muc_log.erl:688 -#: mod_muc/mod_muc_room.erl:2393 -msgid " has set the subject to: " -msgstr " ha modificato l'oggetto in: " - -#: mod_muc/mod_muc_log.erl:452 -msgid "Chatroom is created" -msgstr "La stanza è creata" - -#: mod_muc/mod_muc_log.erl:453 -msgid "Chatroom is destroyed" -msgstr "La stanza è eliminata" - -#: mod_muc/mod_muc_log.erl:454 -msgid "Chatroom is started" -msgstr "La stanza è avviata" - -#: mod_muc/mod_muc_log.erl:455 -msgid "Chatroom is stopped" -msgstr "La stanza è arrestata" - -#: mod_muc/mod_muc_log.erl:459 -msgid "Monday" -msgstr "Lunedì" - -#: mod_muc/mod_muc_log.erl:460 -msgid "Tuesday" -msgstr "Martedì" - -#: mod_muc/mod_muc_log.erl:461 -msgid "Wednesday" -msgstr "Mercoledì" - -#: mod_muc/mod_muc_log.erl:462 -msgid "Thursday" -msgstr "Giovedì" - -#: mod_muc/mod_muc_log.erl:463 -msgid "Friday" -msgstr "Venerdì" - -#: mod_muc/mod_muc_log.erl:464 -msgid "Saturday" -msgstr "Sabato" - -#: mod_muc/mod_muc_log.erl:465 -msgid "Sunday" -msgstr "Domenica" - -#: mod_muc/mod_muc_log.erl:469 -msgid "January" -msgstr "Gennaio" - -#: mod_muc/mod_muc_log.erl:470 -msgid "February" -msgstr "Febbraio" - -#: mod_muc/mod_muc_log.erl:471 -msgid "March" -msgstr "Marzo" - -#: mod_muc/mod_muc_log.erl:472 -msgid "April" -msgstr "Aprile" - -#: mod_muc/mod_muc_log.erl:473 -msgid "May" -msgstr "Maggio" - -#: mod_muc/mod_muc_log.erl:474 -msgid "June" -msgstr "Giugno" - -#: mod_muc/mod_muc_log.erl:475 -msgid "July" -msgstr "Luglio" - -#: mod_muc/mod_muc_log.erl:476 -msgid "August" -msgstr "Agosto" - -#: mod_muc/mod_muc_log.erl:477 -msgid "September" -msgstr "Settembre" - -#: mod_muc/mod_muc_log.erl:478 -msgid "October" -msgstr "Ottobre" - -#: mod_muc/mod_muc_log.erl:479 -msgid "November" -msgstr "Novembre" - -#: mod_muc/mod_muc_log.erl:480 -msgid "December" -msgstr "Dicembre" - -#: mod_muc/mod_muc_log.erl:750 -msgid "Room Configuration" -msgstr "Configurazione della stanza" - -#: mod_muc/mod_muc_log.erl:759 -msgid "Room Occupants" -msgstr "Presenti nella stanza" - -#: mod_muc/mod_muc_room.erl:174 -msgid "Traffic rate limit is exceeded" -msgstr "Limite di traffico superato" - -#: mod_muc/mod_muc_room.erl:246 -msgid "" -"This participant is kicked from the room because he sent an error message" -msgstr "" -"Partecipante espulso dalla stanza perché ha inviato un messaggio non valido" - -#: mod_muc/mod_muc_room.erl:255 -msgid "It is not allowed to send private messages to the conference" -msgstr "Non è consentito l'invio di messaggi privati alla conferenza" - -#: mod_muc/mod_muc_room.erl:332 -msgid "Please, wait for a while before sending new voice request" -msgstr "Attendi qualche istante prima di inviare una nuova richiesta di parola" - -#: mod_muc/mod_muc_room.erl:347 -msgid "Voice requests are disabled in this conference" -msgstr "In questa conferenza le richieste di parola sono escluse" - -#: mod_muc/mod_muc_room.erl:364 -msgid "Failed to extract JID from your voice request approval" -msgstr "" -"Impossibile estrarre il JID dall'approvazione della richiesta di parola" - -#: mod_muc/mod_muc_room.erl:393 -msgid "Only moderators can approve voice requests" -msgstr "Soltanto i moderatori possono approvare richieste di parola" - -#: mod_muc/mod_muc_room.erl:408 -msgid "Improper message type" -msgstr "Tipo di messaggio non corretto" - -#: mod_muc/mod_muc_room.erl:518 -msgid "" -"This participant is kicked from the room because he sent an error message to " -"another participant" -msgstr "" -"Partecipante espulso dalla stanza perché ha inviato un messaggio non valido " -"a un altro partecipante" - -#: mod_muc/mod_muc_room.erl:531 -msgid "It is not allowed to send private messages of type \"groupchat\"" -msgstr "Non è consentito l'invio di messaggi privati di tipo \"groupchat\"" - -#: mod_muc/mod_muc_room.erl:543 mod_muc/mod_muc_room.erl:611 -msgid "Recipient is not in the conference room" -msgstr "Il destinatario non è nella stanza per conferenze" - -#: mod_muc/mod_muc_room.erl:564 mod_muc/mod_muc_room.erl:585 -msgid "It is not allowed to send private messages" -msgstr "Non è consentito l'invio di messaggi privati" - -#: mod_muc/mod_muc_room.erl:576 mod_muc/mod_muc_room.erl:956 -#: mod_muc/mod_muc_room.erl:4040 -msgid "Only occupants are allowed to send messages to the conference" -msgstr "L'invio di messaggi alla conferenza è consentito soltanto ai presenti" - -#: mod_muc/mod_muc_room.erl:634 -msgid "Only occupants are allowed to send queries to the conference" -msgstr "L'invio di query alla conferenza è consentito ai soli presenti" - -#: mod_muc/mod_muc_room.erl:646 -msgid "Queries to the conference members are not allowed in this room" -msgstr "In questa stanza non sono consentite query ai membri della conferenza" - -#: mod_muc/mod_muc_room.erl:932 -msgid "" -"Only moderators and participants are allowed to change the subject in this " -"room" -msgstr "" -"La modifica dell'oggetto di questa stanza è consentita soltanto ai " -"moderatori e ai partecipanti" - -#: mod_muc/mod_muc_room.erl:937 -msgid "Only moderators are allowed to change the subject in this room" -msgstr "" -"La modifica dell'oggetto di questa stanza è consentita soltanto ai moderatori" - -#: mod_muc/mod_muc_room.erl:947 -msgid "Visitors are not allowed to send messages to all occupants" -msgstr "Non è consentito ai visitatori l'invio di messaggi a tutti i presenti" - -#: mod_muc/mod_muc_room.erl:1021 -msgid "" -"This participant is kicked from the room because he sent an error presence" -msgstr "" -"Partecipante espulso dalla stanza perché ha inviato una presenza non valido" - -#: mod_muc/mod_muc_room.erl:1040 -msgid "Visitors are not allowed to change their nicknames in this room" -msgstr "Non è consentito ai visitatori cambiare il nickname in questa stanza" - -#: mod_muc/mod_muc_room.erl:1053 mod_muc/mod_muc_room.erl:1779 -msgid "That nickname is already in use by another occupant" -msgstr "Il nickname è già in uso all'interno della conferenza" - -#: mod_muc/mod_muc_room.erl:1768 -msgid "You have been banned from this room" -msgstr "Sei stata/o bandita/o da questa stanza" - -#: mod_muc/mod_muc_room.erl:1771 -msgid "Membership is required to enter this room" -msgstr "Per entrare in questa stanza è necessario essere membro" - -#: mod_muc/mod_muc_room.erl:1807 -msgid "This room is not anonymous" -msgstr "Questa stanza non è anonima" - -#: mod_muc/mod_muc_room.erl:1833 -msgid "A password is required to enter this room" -msgstr "Per entrare in questa stanza è prevista una password" - -#: mod_muc/mod_muc_room.erl:1855 mod_register.erl:246 -msgid "Too many CAPTCHA requests" -msgstr "Troppe richieste CAPTCHA" - -#: mod_muc/mod_muc_room.erl:1864 mod_register.erl:251 -msgid "Unable to generate a CAPTCHA" -msgstr "Impossibile generare un CAPTCHA" - -#: mod_muc/mod_muc_room.erl:1874 -msgid "Incorrect password" -msgstr "Password non esatta" - -#: mod_muc/mod_muc_room.erl:2448 -msgid "Administrator privileges required" -msgstr "Necessari i privilegi di amministratore" - -#: mod_muc/mod_muc_room.erl:2463 -msgid "Moderator privileges required" -msgstr "Necessari i privilegi di moderatore" - -#: mod_muc/mod_muc_room.erl:2619 -msgid "Jabber ID ~s is invalid" -msgstr "Il Jabber ID ~s non è valido" - -#: mod_muc/mod_muc_room.erl:2633 -msgid "Nickname ~s does not exist in the room" -msgstr "Il nickname ~s non esiste nella stanza" - -#: mod_muc/mod_muc_room.erl:2659 mod_muc/mod_muc_room.erl:3049 -msgid "Invalid affiliation: ~s" -msgstr "Affiliazione non valida: ~s" - -#: mod_muc/mod_muc_room.erl:2713 -msgid "Invalid role: ~s" -msgstr "Ruolo non valido: ~s" - -#: mod_muc/mod_muc_room.erl:3026 mod_muc/mod_muc_room.erl:3062 -msgid "Owner privileges required" -msgstr "Necessari i privilegi di proprietario" - -#: mod_muc/mod_muc_room.erl:3195 -msgid "Configuration of room ~s" -msgstr "Configurazione per la stanza ~s" - -#: mod_muc/mod_muc_room.erl:3200 -msgid "Room title" -msgstr "Titolo della stanza" - -#: mod_muc/mod_muc_room.erl:3203 mod_muc/mod_muc_room.erl:3692 -msgid "Room description" -msgstr "Descrizione della stanza" - -#: mod_muc/mod_muc_room.erl:3210 -msgid "Make room persistent" -msgstr "Rendere la stanza persistente" - -#: mod_muc/mod_muc_room.erl:3215 -msgid "Make room public searchable" -msgstr "Rendere la sala visibile al pubblico" - -#: mod_muc/mod_muc_room.erl:3218 -msgid "Make participants list public" -msgstr "Rendere pubblica la lista dei partecipanti" - -#: mod_muc/mod_muc_room.erl:3221 -msgid "Make room password protected" -msgstr "Rendere la stanza protetta da password" - -#: mod_muc/mod_muc_room.erl:3232 -msgid "Maximum Number of Occupants" -msgstr "Numero massimo di occupanti" - -#: mod_muc/mod_muc_room.erl:3239 -msgid "No limit" -msgstr "Nessun limite" - -#: mod_muc/mod_muc_room.erl:3250 -msgid "Present real Jabber IDs to" -msgstr "Rendere visibile il Jabber ID reale a" - -#: mod_muc/mod_muc_room.erl:3258 mod_muc/mod_muc_room.erl:3292 -msgid "moderators only" -msgstr "moderatori soltanto" - -#: mod_muc/mod_muc_room.erl:3260 mod_muc/mod_muc_room.erl:3294 -msgid "anyone" -msgstr "tutti" - -#: mod_muc/mod_muc_room.erl:3262 -msgid "Make room members-only" -msgstr "Rendere la stanza riservata ai membri" - -#: mod_muc/mod_muc_room.erl:3265 -msgid "Make room moderated" -msgstr "Rendere la stanza moderata" - -#: mod_muc/mod_muc_room.erl:3268 -msgid "Default users as participants" -msgstr "Definire per default gli utenti come partecipanti" - -#: mod_muc/mod_muc_room.erl:3271 -msgid "Allow users to change the subject" -msgstr "Consentire agli utenti di cambiare l'oggetto" - -#: mod_muc/mod_muc_room.erl:3274 -msgid "Allow users to send private messages" -msgstr "Consentire agli utenti l'invio di messaggi privati" - -#: mod_muc/mod_muc_room.erl:3279 -msgid "Allow visitors to send private messages to" -msgstr "Consentire agli ospiti l'invio di messaggi privati a" - -#: mod_muc/mod_muc_room.erl:3290 -msgid "nobody" -msgstr "nessuno" - -#: mod_muc/mod_muc_room.erl:3296 -msgid "Allow users to query other users" -msgstr "Consentire agli utenti query verso altri utenti" - -#: mod_muc/mod_muc_room.erl:3299 -msgid "Allow users to send invites" -msgstr "Consentire agli utenti l'invio di inviti" - -#: mod_muc/mod_muc_room.erl:3302 -msgid "Allow visitors to send status text in presence updates" -msgstr "" -"Consentire ai visitatori l'invio di testo sullo stato in aggiornamenti sulla " -"presenza" - -#: mod_muc/mod_muc_room.erl:3305 -msgid "Allow visitors to change nickname" -msgstr "Consentire ai visitatori di cambiare il nickname" - -#: mod_muc/mod_muc_room.erl:3308 -msgid "Allow visitors to send voice requests" -msgstr "Consentire agli ospiti l'invio di richieste di parola" - -#: mod_muc/mod_muc_room.erl:3311 -msgid "Minimum interval between voice requests (in seconds)" -msgstr "Intervallo minimo fra due richieste di parola (in secondi)" - -#: mod_muc/mod_muc_room.erl:3317 -msgid "Make room CAPTCHA protected" -msgstr "Rendere la stanza protetta da CAPTCHA" - -#: mod_muc/mod_muc_room.erl:3322 -msgid "Exclude Jabber IDs from CAPTCHA challenge" -msgstr "Escludi degli ID Jabber dal passaggio CAPTCHA" - -#: mod_muc/mod_muc_room.erl:3329 -msgid "Enable logging" -msgstr "Abilitare i log" - -#: mod_muc/mod_muc_room.erl:3337 -msgid "You need an x:data capable client to configure room" -msgstr "" -"Per la configurazione della stanza è necessario un client che supporti x:data" - -#: mod_muc/mod_muc_room.erl:3694 -msgid "Number of occupants" -msgstr "Numero di presenti" - -#: mod_muc/mod_muc_room.erl:3750 -msgid "private, " -msgstr "privato, " - -#: mod_muc/mod_muc_room.erl:3799 -msgid "Voice request" -msgstr "Richiesta di parola" - -#: mod_muc/mod_muc_room.erl:3803 -msgid "Either approve or decline the voice request." -msgstr "Approva oppure respingi la richiesta di parola." - -#: mod_muc/mod_muc_room.erl:3809 -msgid "User JID" -msgstr "JID utente" - -#: mod_muc/mod_muc_room.erl:3811 -msgid "Grant voice to this person?" -msgstr "Dare parola a questa persona?" - -#: mod_muc/mod_muc_room.erl:3960 -msgid "~s invites you to the room ~s" -msgstr "~s ti invita nella stanza ~s" - -#: mod_muc/mod_muc_room.erl:3969 -msgid "the password is" -msgstr "la password è" - -#: mod_offline.erl:510 mod_offline_odbc.erl:352 -msgid "" -"Your contact offline message queue is full. The message has been discarded." -msgstr "" -"La coda dei messaggi offline del contatto è piena. Il messaggio è stato " -"scartato" - -#: mod_offline.erl:560 mod_offline_odbc.erl:408 -msgid "~s's Offline Messages Queue" -msgstr "Coda di ~s messaggi offline" - -#: mod_offline.erl:563 mod_offline_odbc.erl:411 mod_roster.erl:987 -#: mod_roster_odbc.erl:1104 mod_shared_roster.erl:901 -#: mod_shared_roster.erl:1010 mod_shared_roster_odbc.erl:930 -#: mod_shared_roster_odbc.erl:1039 web/ejabberd_web_admin.erl:833 -#: web/ejabberd_web_admin.erl:874 web/ejabberd_web_admin.erl:942 -#: web/ejabberd_web_admin.erl:978 web/ejabberd_web_admin.erl:1019 -#: web/ejabberd_web_admin.erl:1508 web/ejabberd_web_admin.erl:1760 -#: web/ejabberd_web_admin.erl:1931 web/ejabberd_web_admin.erl:1963 -#: web/ejabberd_web_admin.erl:2031 web/ejabberd_web_admin.erl:2135 -#: web/ejabberd_web_admin.erl:2160 web/ejabberd_web_admin.erl:2248 -msgid "Submitted" -msgstr "Inviato" - -#: mod_offline.erl:571 -msgid "Time" -msgstr "Ora" - -#: mod_offline.erl:572 -msgid "From" -msgstr "Da" - -#: mod_offline.erl:573 -msgid "To" -msgstr "A" - -#: mod_offline.erl:574 mod_offline_odbc.erl:419 -msgid "Packet" -msgstr "Pacchetto" - -#: mod_offline.erl:587 mod_offline_odbc.erl:432 mod_shared_roster.erl:908 -#: mod_shared_roster_odbc.erl:937 web/ejabberd_web_admin.erl:882 -#: web/ejabberd_web_admin.erl:986 -msgid "Delete Selected" -msgstr "Eliminare gli elementi selezionati" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -msgid "Offline Messages:" -msgstr "Messaggi offline:" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -msgid "Remove All Offline Messages" -msgstr "Eliminare tutti i messaggi offline" - -#: mod_proxy65/mod_proxy65_service.erl:213 -msgid "ejabberd SOCKS5 Bytestreams module" -msgstr "Modulo SOCKS5 Bytestreams per ejabberd" - -#: mod_pubsub/mod_pubsub.erl:1120 mod_pubsub/mod_pubsub_odbc.erl:929 -msgid "Publish-Subscribe" -msgstr "Pubblicazione-Iscrizione" - -#: mod_pubsub/mod_pubsub.erl:1214 mod_pubsub/mod_pubsub_odbc.erl:1025 -msgid "ejabberd Publish-Subscribe module" -msgstr "Modulo Pubblicazione/Iscrizione (PubSub) per ejabberd" - -#: mod_pubsub/mod_pubsub.erl:1497 mod_pubsub/mod_pubsub_odbc.erl:1312 -msgid "PubSub subscriber request" -msgstr "Richiesta di iscrizione per PubSub" - -#: mod_pubsub/mod_pubsub.erl:1499 mod_pubsub/mod_pubsub_odbc.erl:1314 -msgid "Choose whether to approve this entity's subscription." -msgstr "Scegliere se approvare l'iscrizione per questa entità" - -#: mod_pubsub/mod_pubsub.erl:1505 mod_pubsub/mod_pubsub_odbc.erl:1320 -msgid "Node ID" -msgstr "ID del nodo" - -#: mod_pubsub/mod_pubsub.erl:1510 mod_pubsub/mod_pubsub_odbc.erl:1325 -msgid "Subscriber Address" -msgstr "Indirizzo dell'iscritta/o" - -#: mod_pubsub/mod_pubsub.erl:1516 mod_pubsub/mod_pubsub_odbc.erl:1331 -msgid "Allow this Jabber ID to subscribe to this pubsub node?" -msgstr "Consentire a questo Jabber ID l'iscrizione a questo nodo pubsub?" - -#: mod_pubsub/mod_pubsub.erl:3474 mod_pubsub/mod_pubsub_odbc.erl:3287 -msgid "Deliver payloads with event notifications" -msgstr "Inviare il contenuto del messaggio con la notifica dell'evento" - -#: mod_pubsub/mod_pubsub.erl:3475 mod_pubsub/mod_pubsub_odbc.erl:3288 -msgid "Deliver event notifications" -msgstr "Inviare notifiche degli eventi" - -#: mod_pubsub/mod_pubsub.erl:3476 mod_pubsub/mod_pubsub_odbc.erl:3289 -msgid "Notify subscribers when the node configuration changes" -msgstr "Notificare gli iscritti quando la configurazione del nodo cambia" - -#: mod_pubsub/mod_pubsub.erl:3477 mod_pubsub/mod_pubsub_odbc.erl:3290 -msgid "Notify subscribers when the node is deleted" -msgstr "Notificare gli iscritti quando il nodo è cancellato" - -#: mod_pubsub/mod_pubsub.erl:3478 mod_pubsub/mod_pubsub_odbc.erl:3291 -msgid "Notify subscribers when items are removed from the node" -msgstr "Notificare gli iscritti quando sono eliminati degli elementi dal nodo" - -#: mod_pubsub/mod_pubsub.erl:3479 mod_pubsub/mod_pubsub_odbc.erl:3292 -msgid "Persist items to storage" -msgstr "Conservazione persistente degli elementi" - -#: mod_pubsub/mod_pubsub.erl:3480 mod_pubsub/mod_pubsub_odbc.erl:3293 -msgid "A friendly name for the node" -msgstr "Un nome comodo per il nodo" - -#: mod_pubsub/mod_pubsub.erl:3481 mod_pubsub/mod_pubsub_odbc.erl:3294 -msgid "Max # of items to persist" -msgstr "Numero massimo di elementi da conservare persistentemente" - -#: mod_pubsub/mod_pubsub.erl:3482 mod_pubsub/mod_pubsub_odbc.erl:3295 -msgid "Whether to allow subscriptions" -msgstr "Consentire iscrizioni?" - -#: mod_pubsub/mod_pubsub.erl:3483 mod_pubsub/mod_pubsub_odbc.erl:3296 -msgid "Specify the access model" -msgstr "Specificare il modello di accesso" - -#: mod_pubsub/mod_pubsub.erl:3486 mod_pubsub/mod_pubsub_odbc.erl:3299 -msgid "Roster groups allowed to subscribe" -msgstr "Gruppi roster abilitati alla registrazione" - -#: mod_pubsub/mod_pubsub.erl:3487 mod_pubsub/mod_pubsub_odbc.erl:3300 -msgid "Specify the publisher model" -msgstr "Definire il modello di pubblicazione" - -#: mod_pubsub/mod_pubsub.erl:3489 mod_pubsub/mod_pubsub_odbc.erl:3302 -msgid "Purge all items when the relevant publisher goes offline" -msgstr "" -"Cancella tutti gli elementi quando chi li ha pubblicati non è più online" - -#: mod_pubsub/mod_pubsub.erl:3490 mod_pubsub/mod_pubsub_odbc.erl:3303 -msgid "Specify the event message type" -msgstr "Specificare il tipo di messaggio di evento" - -#: mod_pubsub/mod_pubsub.erl:3492 mod_pubsub/mod_pubsub_odbc.erl:3305 -msgid "Max payload size in bytes" -msgstr "Dimensione massima del contenuto del messaggio in byte" - -#: mod_pubsub/mod_pubsub.erl:3493 mod_pubsub/mod_pubsub_odbc.erl:3306 -msgid "When to send the last published item" -msgstr "Quando inviare l'ultimo elemento pubblicato" - -#: mod_pubsub/mod_pubsub.erl:3495 mod_pubsub/mod_pubsub_odbc.erl:3308 -msgid "Only deliver notifications to available users" -msgstr "Inviare le notifiche solamente agli utenti disponibili" - -#: mod_pubsub/mod_pubsub.erl:3496 mod_pubsub/mod_pubsub_odbc.erl:3309 -msgid "The collections with which a node is affiliated" -msgstr "Le collezioni a cui è affiliato un nodo" - -#: mod_register.erl:193 -msgid "The CAPTCHA verification has failed" -msgstr "La verifica del CAPTCHA ha avuto esito negativo" - -#: mod_register.erl:220 -msgid "You need a client that supports x:data and CAPTCHA to register" -msgstr "La registrazione richiede un client che supporti x:data e CAPTCHA" - -#: mod_register.erl:226 mod_register.erl:265 -msgid "Choose a username and password to register with this server" -msgstr "" -"Scegliere un nome utente e una password per la registrazione con questo " -"server" - -#: mod_register.erl:230 mod_vcard.erl:364 mod_vcard_odbc.erl:342 -#: web/ejabberd_web_admin.erl:1515 web/ejabberd_web_admin.erl:1572 -msgid "User" -msgstr "Utente" - -#: mod_register.erl:316 mod_register.erl:361 -msgid "The password is too weak" -msgstr "La password è troppo debole" - -#: mod_register.erl:365 -msgid "Users are not allowed to register accounts so quickly" -msgstr "Non è consentito agli utenti registrare account così rapidamente" - -#: mod_roster.erl:938 mod_roster_odbc.erl:1055 web/ejabberd_web_admin.erl:1701 -#: web/ejabberd_web_admin.erl:1886 web/ejabberd_web_admin.erl:1897 -#: web/ejabberd_web_admin.erl:2219 -msgid "None" -msgstr "Nessuno" - -#: mod_roster.erl:945 mod_roster_odbc.erl:1062 -msgid "Subscription" -msgstr "Iscrizione" - -#: mod_roster.erl:946 mod_roster_odbc.erl:1063 -msgid "Pending" -msgstr "Pendente" - -#: mod_roster.erl:947 mod_roster_odbc.erl:1064 -msgid "Groups" -msgstr "Gruppi" - -#: mod_roster.erl:974 mod_roster_odbc.erl:1091 -msgid "Validate" -msgstr "Validare" - -#: mod_roster.erl:982 mod_roster_odbc.erl:1099 -msgid "Remove" -msgstr "Eliminare" - -#: mod_roster.erl:985 mod_roster_odbc.erl:1102 -msgid "Roster of " -msgstr "Lista dei contatti di " - -#: mod_roster.erl:988 mod_roster_odbc.erl:1105 mod_shared_roster.erl:902 -#: mod_shared_roster.erl:1011 mod_shared_roster_odbc.erl:931 -#: mod_shared_roster_odbc.erl:1040 web/ejabberd_web_admin.erl:834 -#: web/ejabberd_web_admin.erl:875 web/ejabberd_web_admin.erl:943 -#: web/ejabberd_web_admin.erl:979 web/ejabberd_web_admin.erl:1020 -#: web/ejabberd_web_admin.erl:1509 web/ejabberd_web_admin.erl:1761 -#: web/ejabberd_web_admin.erl:1932 web/ejabberd_web_admin.erl:2136 -#: web/ejabberd_web_admin.erl:2161 -msgid "Bad format" -msgstr "Formato non valido" - -#: mod_roster.erl:995 mod_roster_odbc.erl:1112 -msgid "Add Jabber ID" -msgstr "Aggiungere un Jabber ID (Jabber ID)" - -#: mod_roster.erl:1094 mod_roster_odbc.erl:1211 -msgid "Roster" -msgstr "Lista dei contatti" - -#: mod_shared_roster.erl:857 mod_shared_roster.erl:899 -#: mod_shared_roster.erl:1007 mod_shared_roster_odbc.erl:886 -#: mod_shared_roster_odbc.erl:928 mod_shared_roster_odbc.erl:1036 -msgid "Shared Roster Groups" -msgstr "Gruppi di liste di contatti comuni" - -#: mod_shared_roster.erl:895 mod_shared_roster_odbc.erl:924 -#: web/ejabberd_web_admin.erl:1365 web/ejabberd_web_admin.erl:2461 -msgid "Add New" -msgstr "Aggiungere nuovo" - -#: mod_shared_roster.erl:978 mod_shared_roster_odbc.erl:1007 -msgid "Name:" -msgstr "Nome:" - -#: mod_shared_roster.erl:983 mod_shared_roster_odbc.erl:1012 -msgid "Description:" -msgstr "Descrizione:" - -#: mod_shared_roster.erl:991 mod_shared_roster_odbc.erl:1020 -msgid "Members:" -msgstr "Membri:" - -#: mod_shared_roster.erl:999 mod_shared_roster_odbc.erl:1028 -msgid "Displayed Groups:" -msgstr "Gruppi visualizzati:" - -#: mod_shared_roster.erl:1008 mod_shared_roster_odbc.erl:1037 -msgid "Group " -msgstr "Gruppo " - -#: mod_shared_roster.erl:1017 mod_shared_roster_odbc.erl:1046 -#: web/ejabberd_web_admin.erl:840 web/ejabberd_web_admin.erl:884 -#: web/ejabberd_web_admin.erl:949 web/ejabberd_web_admin.erl:1026 -#: web/ejabberd_web_admin.erl:2017 -msgid "Submit" -msgstr "Inviare" - -#: mod_vcard.erl:165 mod_vcard_ldap.erl:238 mod_vcard_odbc.erl:129 -msgid "Erlang Jabber Server" -msgstr "Erlang Jabber Server" - -#: mod_vcard.erl:364 mod_vcard.erl:478 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:454 -msgid "Birthday" -msgstr "Compleanno" - -#: mod_vcard.erl:364 mod_vcard.erl:480 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:456 -msgid "City" -msgstr "Città" - -#: mod_vcard.erl:364 mod_vcard.erl:479 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:455 -msgid "Country" -msgstr "Paese" - -#: mod_vcard.erl:364 mod_vcard.erl:481 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:457 -msgid "Email" -msgstr "E-mail" - -#: mod_vcard.erl:364 mod_vcard.erl:476 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:452 -msgid "Family Name" -msgstr "Cognome" - -#: mod_vcard.erl:364 mod_vcard_odbc.erl:342 -msgid "" -"Fill in the form to search for any matching Jabber User (Add * to the end of " -"field to match substring)" -msgstr "" -"Riempire il modulo per la ricerca di utenti Jabber corrispondenti ai criteri " -"(Aggiungere * alla fine del campo per la ricerca di una sottostringa" - -#: mod_vcard.erl:364 mod_vcard.erl:473 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:449 -msgid "Full Name" -msgstr "Nome completo" - -#: mod_vcard.erl:364 mod_vcard.erl:475 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:451 -msgid "Middle Name" -msgstr "Altro nome" - -#: mod_vcard.erl:364 mod_vcard.erl:474 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:450 web/ejabberd_web_admin.erl:2006 -msgid "Name" -msgstr "Nome" - -#: mod_vcard.erl:364 mod_vcard.erl:482 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:458 -msgid "Organization Name" -msgstr "Nome dell'organizzazione" - -#: mod_vcard.erl:364 mod_vcard.erl:483 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:459 -msgid "Organization Unit" -msgstr "Unità dell'organizzazione" - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "Search users in " -msgstr "Cercare utenti in " - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "You need an x:data capable client to search" -msgstr "Per effettuare ricerche è necessario un client che supporti x:data" - -#: mod_vcard.erl:389 mod_vcard_ldap.erl:487 mod_vcard_odbc.erl:367 -msgid "vCard User Search" -msgstr "Ricerca di utenti per vCard" - -#: mod_vcard.erl:445 mod_vcard_ldap.erl:541 mod_vcard_odbc.erl:421 -msgid "ejabberd vCard module" -msgstr "Modulo vCard per ejabberd" - -#: mod_vcard.erl:469 mod_vcard_ldap.erl:551 mod_vcard_odbc.erl:445 -msgid "Search Results for " -msgstr "Risultati della ricerca per " - -#: mod_vcard_ldap.erl:462 -msgid "Fill in fields to search for any matching Jabber User" -msgstr "" -"Riempire i campi per la ricerca di utenti Jabber corrispondenti ai criteri" - -#: web/ejabberd_web_admin.erl:193 web/ejabberd_web_admin.erl:203 -#: web/ejabberd_web_admin.erl:219 web/ejabberd_web_admin.erl:229 -msgid "Unauthorized" -msgstr "Non autorizzato" - -#: web/ejabberd_web_admin.erl:286 web/ejabberd_web_admin.erl:303 -msgid "ejabberd Web Admin" -msgstr "Amministrazione web ejabberd" - -#: web/ejabberd_web_admin.erl:769 web/ejabberd_web_admin.erl:780 -msgid "Administration" -msgstr "Amministrazione" - -#: web/ejabberd_web_admin.erl:878 web/ejabberd_web_admin.erl:982 -msgid "Raw" -msgstr "Grezzo" - -#: web/ejabberd_web_admin.erl:1017 -msgid "~s access rule configuration" -msgstr "Configurazione delle regole di accesso per ~s" - -#: web/ejabberd_web_admin.erl:1035 -msgid "Virtual Hosts" -msgstr "Host Virtuali" - -#: web/ejabberd_web_admin.erl:1043 web/ejabberd_web_admin.erl:1050 -msgid "Users" -msgstr "Utenti" - -#: web/ejabberd_web_admin.erl:1078 -msgid "Users Last Activity" -msgstr "Ultima attività degli utenti" - -#: web/ejabberd_web_admin.erl:1080 -msgid "Period: " -msgstr "Periodo:" - -#: web/ejabberd_web_admin.erl:1090 -msgid "Last month" -msgstr "Ultimo mese" - -#: web/ejabberd_web_admin.erl:1091 -msgid "Last year" -msgstr "Ultimo anno" - -#: web/ejabberd_web_admin.erl:1092 -msgid "All activity" -msgstr "Tutta l'attività" - -#: web/ejabberd_web_admin.erl:1094 -msgid "Show Ordinary Table" -msgstr "Mostrare la tabella normale" - -#: web/ejabberd_web_admin.erl:1096 -msgid "Show Integral Table" -msgstr "Mostrare la tabella integrale" - -#: web/ejabberd_web_admin.erl:1105 web/ejabberd_web_admin.erl:1939 -msgid "Statistics" -msgstr "Statistiche" - -#: web/ejabberd_web_admin.erl:1117 -msgid "Not Found" -msgstr "Non trovato" - -#: web/ejabberd_web_admin.erl:1134 -msgid "Node not found" -msgstr "Nodo non trovato" - -#: web/ejabberd_web_admin.erl:1460 -msgid "Host" -msgstr "Host" - -#: web/ejabberd_web_admin.erl:1461 -msgid "Registered Users" -msgstr "Utenti registrati" - -#: web/ejabberd_web_admin.erl:1573 -msgid "Offline Messages" -msgstr "Messaggi offline" - -#: web/ejabberd_web_admin.erl:1574 web/ejabberd_web_admin.erl:1767 -msgid "Last Activity" -msgstr "Ultima attività" - -#: web/ejabberd_web_admin.erl:1659 web/ejabberd_web_admin.erl:1675 -msgid "Registered Users:" -msgstr "Utenti registrati:" - -#: web/ejabberd_web_admin.erl:1661 web/ejabberd_web_admin.erl:1677 -#: web/ejabberd_web_admin.erl:2192 -msgid "Online Users:" -msgstr "Utenti connessi:" - -#: web/ejabberd_web_admin.erl:1663 -msgid "Outgoing s2s Connections:" -msgstr "Connessioni s2s in uscita:" - -#: web/ejabberd_web_admin.erl:1665 -msgid "Outgoing s2s Servers:" -msgstr "Server s2s in uscita" - -#: web/ejabberd_web_admin.erl:1734 web/mod_register_web.erl:175 -#: web/mod_register_web.erl:372 web/mod_register_web.erl:381 -#: web/mod_register_web.erl:411 -msgid "Change Password" -msgstr "Modificare la password" - -#: web/ejabberd_web_admin.erl:1758 -msgid "User " -msgstr "Utente " - -#: web/ejabberd_web_admin.erl:1765 -msgid "Connected Resources:" -msgstr "Risorse connesse:" - -#: web/ejabberd_web_admin.erl:1766 web/mod_register_web.erl:227 -#: web/mod_register_web.erl:519 -msgid "Password:" -msgstr "Password:" - -#: web/ejabberd_web_admin.erl:1828 -msgid "No Data" -msgstr "Nessuna informazione" - -#: web/ejabberd_web_admin.erl:1906 -msgid "Nodes" -msgstr "Nodi" - -#: web/ejabberd_web_admin.erl:1929 web/ejabberd_web_admin.erl:1951 -msgid "Node " -msgstr "Nodo " - -#: web/ejabberd_web_admin.erl:1938 -msgid "Listened Ports" -msgstr "Porte in ascolto" - -#: web/ejabberd_web_admin.erl:1940 web/ejabberd_web_admin.erl:2260 -#: web/ejabberd_web_admin.erl:2448 -msgid "Update" -msgstr "Aggiornare" - -#: web/ejabberd_web_admin.erl:1943 web/ejabberd_web_admin.erl:2569 -msgid "Restart" -msgstr "Riavviare" - -#: web/ejabberd_web_admin.erl:1945 web/ejabberd_web_admin.erl:2571 -msgid "Stop" -msgstr "Arrestare" - -#: web/ejabberd_web_admin.erl:1959 -msgid "RPC Call Error" -msgstr "Errore di chiamata RPC" - -#: web/ejabberd_web_admin.erl:2000 -msgid "Database Tables at " -msgstr "Tabelle del database su " - -#: web/ejabberd_web_admin.erl:2007 -msgid "Storage Type" -msgstr "Tipo di conservazione" - -#: web/ejabberd_web_admin.erl:2008 -msgid "Elements" -msgstr "Elementi" - -#: web/ejabberd_web_admin.erl:2009 -msgid "Memory" -msgstr "Memoria" - -#: web/ejabberd_web_admin.erl:2032 web/ejabberd_web_admin.erl:2137 -msgid "Error" -msgstr "Errore" - -#: web/ejabberd_web_admin.erl:2034 -msgid "Backup of " -msgstr "Salvataggio di " - -#: web/ejabberd_web_admin.erl:2036 -msgid "" -"Please note that these options will only backup the builtin Mnesia database. " -"If you are using the ODBC module, you also need to backup your SQL database " -"separately." -msgstr "" -"N.B.: Queste opzioni comportano il salvataggio solamente del database " -"interno Mnesia. Se si sta utilizzando il modulo ODBC, è necessario salvare " -"anche il proprio database SQL separatamente." - -#: web/ejabberd_web_admin.erl:2041 -msgid "Store binary backup:" -msgstr "Conservare un salvataggio binario:" - -#: web/ejabberd_web_admin.erl:2045 web/ejabberd_web_admin.erl:2052 -#: web/ejabberd_web_admin.erl:2060 web/ejabberd_web_admin.erl:2067 -#: web/ejabberd_web_admin.erl:2074 web/ejabberd_web_admin.erl:2081 -#: web/ejabberd_web_admin.erl:2088 web/ejabberd_web_admin.erl:2096 -#: web/ejabberd_web_admin.erl:2103 web/ejabberd_web_admin.erl:2110 -msgid "OK" -msgstr "OK" - -#: web/ejabberd_web_admin.erl:2048 -msgid "Restore binary backup immediately:" -msgstr "Recuperare un salvataggio binario adesso:" - -#: web/ejabberd_web_admin.erl:2056 -msgid "" -"Restore binary backup after next ejabberd restart (requires less memory):" -msgstr "" -"Recuperare un salvataggio binario dopo il prossimo riavvio di ejabberd " -"(necessita di meno memoria):" - -#: web/ejabberd_web_admin.erl:2063 -msgid "Store plain text backup:" -msgstr "Conservare un salvataggio come semplice testo:" - -#: web/ejabberd_web_admin.erl:2070 -msgid "Restore plain text backup immediately:" -msgstr "Recuperare un salvataggio come semplice testo adesso:" - -#: web/ejabberd_web_admin.erl:2077 -msgid "Import users data from a PIEFXIS file (XEP-0227):" -msgstr "Importare i dati utenti da un file PIEFXIS (XEP-0227):" - -#: web/ejabberd_web_admin.erl:2084 -msgid "Export data of all users in the server to PIEFXIS files (XEP-0227):" -msgstr "" -"Esportare i dati di tutti gli utenti nel server in file PIEFXIS (XEP-0227):" - -#: web/ejabberd_web_admin.erl:2091 -msgid "Export data of users in a host to PIEFXIS files (XEP-0227):" -msgstr "Esportare i dati degli utenti di un host in file PIEFXIS (XEP-0227):" - -#: web/ejabberd_web_admin.erl:2099 -msgid "Import user data from jabberd14 spool file:" -msgstr "Importare i dati utente da file di spool di jabberd14:" - -#: web/ejabberd_web_admin.erl:2106 -msgid "Import users data from jabberd14 spool directory:" -msgstr "Importare i dati utenti da directory di spool di jabberd14:" - -#: web/ejabberd_web_admin.erl:2132 -msgid "Listened Ports at " -msgstr "Porte in ascolto su " - -#: web/ejabberd_web_admin.erl:2157 -msgid "Modules at " -msgstr "Moduli su " - -#: web/ejabberd_web_admin.erl:2183 -msgid "Statistics of ~p" -msgstr "Statistiche di ~p" - -#: web/ejabberd_web_admin.erl:2186 -msgid "Uptime:" -msgstr "Tempo dall'avvio:" - -#: web/ejabberd_web_admin.erl:2189 -msgid "CPU Time:" -msgstr "Tempo CPU:" - -#: web/ejabberd_web_admin.erl:2195 -msgid "Transactions Committed:" -msgstr "Transazioni avvenute:" - -#: web/ejabberd_web_admin.erl:2198 -msgid "Transactions Aborted:" -msgstr "Transazioni abortite:" - -#: web/ejabberd_web_admin.erl:2201 -msgid "Transactions Restarted:" -msgstr "Transazioni riavviate:" - -#: web/ejabberd_web_admin.erl:2204 -msgid "Transactions Logged:" -msgstr "Transazioni con log:" - -#: web/ejabberd_web_admin.erl:2246 -msgid "Update " -msgstr "Aggiornare " - -#: web/ejabberd_web_admin.erl:2254 -msgid "Update plan" -msgstr "Piano di aggiornamento" - -#: web/ejabberd_web_admin.erl:2255 -msgid "Modified modules" -msgstr "Moduli modificati" - -#: web/ejabberd_web_admin.erl:2256 -msgid "Update script" -msgstr "Script di aggiornamento" - -#: web/ejabberd_web_admin.erl:2257 -msgid "Low level update script" -msgstr "Script di aggiornamento di basso livello" - -#: web/ejabberd_web_admin.erl:2258 -msgid "Script check" -msgstr "Verifica dello script" - -#: web/ejabberd_web_admin.erl:2426 -msgid "Port" -msgstr "Porta" - -#: web/ejabberd_web_admin.erl:2427 -msgid "IP" -msgstr "IP" - -#: web/ejabberd_web_admin.erl:2428 -msgid "Protocol" -msgstr "Protocollo" - -#: web/ejabberd_web_admin.erl:2429 web/ejabberd_web_admin.erl:2556 -msgid "Module" -msgstr "Modulo" - -#: web/ejabberd_web_admin.erl:2430 web/ejabberd_web_admin.erl:2557 -msgid "Options" -msgstr "Opzioni" - -#: web/ejabberd_web_admin.erl:2450 -msgid "Delete" -msgstr "Eliminare" - -#: web/ejabberd_web_admin.erl:2579 -msgid "Start" -msgstr "Avviare" - -#: web/mod_register_web.erl:104 -msgid "Your Jabber account was successfully created." -msgstr "La creazione del tuo account Jabber è andata a buon fine." - -#: web/mod_register_web.erl:107 -msgid "There was an error creating the account: " -msgstr "Si è verificato un errore nella creazione dell'account: " - -#: web/mod_register_web.erl:115 -msgid "Your Jabber account was successfully deleted." -msgstr "La cancellazione del tuo account Jabber è andata a buon fine." - -#: web/mod_register_web.erl:118 -msgid "There was an error deleting the account: " -msgstr "Si è verificato un errore nella cancellazione dell'account: " - -#: web/mod_register_web.erl:128 -msgid "The password of your Jabber account was successfully changed." -msgstr "Il cambio di password del tuo account Jabber è andato a buon fine." - -#: web/mod_register_web.erl:131 -msgid "There was an error changing the password: " -msgstr "Si è verificato un errore nel cambio di password: " - -#: web/mod_register_web.erl:163 web/mod_register_web.erl:172 -msgid "Jabber Account Registration" -msgstr "Registrazione account Jabber" - -#: web/mod_register_web.erl:174 web/mod_register_web.erl:192 -#: web/mod_register_web.erl:201 -msgid "Register a Jabber account" -msgstr "Registra un account Jabber" - -#: web/mod_register_web.erl:176 web/mod_register_web.erl:493 -#: web/mod_register_web.erl:502 -msgid "Unregister a Jabber account" -msgstr "Elimina un account Jabber" - -#: web/mod_register_web.erl:203 -msgid "" -"This page allows to create a Jabber account in this Jabber server. Your JID " -"(Jabber IDentifier) will be of the form: username@server. Please read " -"carefully the instructions to fill correctly the fields." -msgstr "" -"Questa pagina consente di creare un account Jabber in questo server Jabber. " -"Il tuo JID (Jabber IDentifier) avrà la forma: nome_utente@server. Leggi " -"attentamente le istruzioni per compilare i campi correttamente." - -#: web/mod_register_web.erl:212 web/mod_register_web.erl:386 -#: web/mod_register_web.erl:509 -msgid "Username:" -msgstr "Nome utente:" - -#: web/mod_register_web.erl:217 -msgid "This is case insensitive: macbeth is the same that MacBeth and Macbeth." -msgstr "" -"Non fa differenza fra minuscolo e maiuscolo: macbeth, MacBeth e Macbeth si " -"equivalgono." - -#: web/mod_register_web.erl:218 -msgid "Characters not allowed:" -msgstr "Caratteri non consentiti:" - -#: web/mod_register_web.erl:222 web/mod_register_web.erl:391 -#: web/mod_register_web.erl:514 -msgid "Server:" -msgstr "Server:" - -#: web/mod_register_web.erl:232 -msgid "" -"Don't tell your password to anybody, not even the administrators of the " -"Jabber server." -msgstr "" -"Non comunicare la tua password a nessuno, neppure agli amministratori del " -"server Jabber." - -#: web/mod_register_web.erl:234 -msgid "You can later change your password using a Jabber client." -msgstr "Potrai in seguito cambiare la password utilizzando un client Jabber." - -#: web/mod_register_web.erl:235 -msgid "" -"Some Jabber clients can store your password in your computer. Use that " -"feature only if you trust your computer is safe." -msgstr "" -"Alcuni client Jabber possono conservare la password nel tuo computer. " -"Utilizza tale funzione soltanto se ritieni che il tuo computer sia sicuro." - -#: web/mod_register_web.erl:237 -msgid "" -"Memorize your password, or write it in a paper placed in a safe place. In " -"Jabber there isn't an automated way to recover your password if you forget " -"it." -msgstr "" -"Memorizza la password, o scrivila su un foglio di carta da conservare in un " -"luogo sicuro. Jabber non prevede una modalità automatica per il recupero di " -"una password dimenticata." - -#: web/mod_register_web.erl:242 web/mod_register_web.erl:406 -msgid "Password Verification:" -msgstr "Verifica della password:" - -#: web/mod_register_web.erl:250 -msgid "Register" -msgstr "Registra" - -#: web/mod_register_web.erl:396 -msgid "Old Password:" -msgstr "Vecchia password:" - -#: web/mod_register_web.erl:401 -msgid "New Password:" -msgstr "Nuova password:" - -#: web/mod_register_web.erl:504 -msgid "This page allows to unregister a Jabber account in this Jabber server." -msgstr "" -"Questa pagina consente di eliminare un account Jabber da questo server " -"Jabber." - -#: web/mod_register_web.erl:524 -msgid "Unregister" -msgstr "Elimina" - -#, fuzzy -#~ msgid "CAPTCHA test failed" -#~ msgstr "Il CAPTCHA è valido." diff --git a/priv/msgs/ja.msg b/priv/msgs/ja.msg index 851e305aa..bf1401f54 100644 --- a/priv/msgs/ja.msg +++ b/priv/msgs/ja.msg @@ -1,21 +1,28 @@ -{"Access Configuration","アクセス設定"}. -{"Access Control List Configuration","アクセスコントロールリスト設定"}. -{"Access control lists","アクセスコントロールリスト"}. -{"Access Control Lists","アクセスコントロールリスト"}. +%% Generated automatically +%% DO NOT EDIT: run `make translations` instead +%% To improve translations please read: +%% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ + +{" (Add * to the end of field to match substring)"," (* を最後に付けると部分文字列にマッチします)"}. +{" has set the subject to: "," は題を設定しました: "}. +{"A description of the node","ノードの説明"}. +{"A friendly name for the node","ノードのフレンドリネーム"}. +{"A password is required to enter this room","このチャットルームに入るにはパスワードが必要です"}. +{"A Web Page","ウェブページ"}. +{"Accept","許可"}. {"Access denied by service policy","サービスポリシーによってアクセスが禁止されました"}. -{"Access rules","アクセスルール"}. -{"Access Rules","アクセスルール"}. +{"Access model","アクセスモデル"}. +{"Account doesn't exist","アカウントは存在しません"}. {"Action on user","ユーザー操作"}. -{"Add Jabber ID","Jabber ID を追加"}. -{"Add New","新規追加"}. {"Add User","ユーザーを追加"}. {"Administration of ","管理: "}. {"Administration","管理"}. {"Administrator privileges required","管理者権限が必要です"}. -{"A friendly name for the node","ノードのフレンドリネーム"}. {"All activity","すべて"}. +{"All Users","全ユーザー"}. +{"Allow subscription","購読を認可"}. {"Allow this Jabber ID to subscribe to this pubsub node?","この Jabber ID に、この pubsubノードの購読を許可しますか ?"}. -{"Allow users to change the subject","ユーザーによる題の変更を許可"}. +{"Allow users to change the subject","ユーザーによる件名の変更を許可"}. {"Allow users to query other users","ユーザーによる他のユーザーへのクエリーを許可"}. {"Allow users to send invites","ユーザーによる招待を許可"}. {"Allow users to send private messages","ユーザーによるプライベートメッセージの送信を許可"}. @@ -23,113 +30,116 @@ {"Allow visitors to send private messages to","傍聴者によるプライベートメッセージの送信を次の相手に許可"}. {"Allow visitors to send status text in presence updates","傍聴者によるプレゼンス更新のステータス文の送信を許可"}. {"Allow visitors to send voice requests","傍聴者による発言権の要求を許可"}. -{"All Users","全ユーザー"}. {"Announcements","アナウンス"}. -{"anyone","誰にでも"}. -{"A password is required to enter this room","この談話室に入るにはパスワードが必要です"}. +{"Anyone","誰にでも"}. {"April","4月"}. {"August","8月"}. -{"Backup","バックアップ"}. +{"Automatic node creation is not enabled","ノードの自動作成は有効になっていません"}. {"Backup Management","バックアップ管理"}. -{"Backup of ","バックアップ: "}. +{"Backup of ~p","バックアップ: ~p"}. {"Backup to File at ","ファイルにバックアップ: "}. +{"Backup","バックアップ"}. {"Bad format","不正なフォーマット"}. {"Birthday","誕生日"}. +{"Both the username and the resource are required","ユーザー名とリソースの両方が必要"}. {"CAPTCHA web page","CAPTCHA ウェブページ"}. +{"Challenge ID","チャレンジ ID"}. {"Change Password","パスワードを変更"}. {"Change User Password","パスワードを変更"}. +{"Changing password is not allowed","パスワード変更の権限がありません"}. +{"Channel already exists","チャンネルは既に存在します"}. +{"Channel does not exist","チャンネルは存在しません"}. +{"Channel JID","チャンネル ID"}. +{"Channels","チャンネル"}. {"Characters not allowed:","使用できない文字:"}. -{"Chatroom configuration modified","談話室の設定が変更されました"}. -{"Chatroom is created","談話室を作りました"}. -{"Chatroom is destroyed","談話室を削除しました"}. -{"Chatroom is started","談話室を開始しました"}. -{"Chatroom is stopped","談話室を停止しました"}. -{"Chatrooms","談話室"}. +{"Chatroom configuration modified","チャットルームの設定が変更されました"}. +{"Chatroom is created","チャットルームを作りました"}. +{"Chatroom is destroyed","チャットルームを終了しました"}. +{"Chatroom is started","チャットルームを開始しました"}. +{"Chatroom is stopped","チャットルームを停止しました"}. +{"Chatrooms","チャットルーム"}. {"Choose a username and password to register with this server","サーバーに登録するユーザー名とパスワードを選択してください"}. -{"Choose modules to stop","停止するモジュールを選択"}. {"Choose storage type of tables","テーブルのストレージタイプを選択"}. {"Choose whether to approve this entity's subscription.","このエントリを承認するかどうかを選択してください"}. {"City","都道府県"}. {"Commands","コマンド"}. {"Conference room does not exist","会議室は存在しません"}. -{"Configuration of room ~s","談話室 ~s の設定"}. +{"Configuration of room ~s","チャットルーム ~s の設定"}. {"Configuration","設定"}. -{"Connected Resources:","接続リソース:"}. -{"Connections parameters","接続パラメーター"}. +{"Contact Addresses (normally, room owner or owners)","連絡先 (通常は会議室の主宰者またはその複数)"}. {"Country","国"}. -{"CPU Time:","CPU時間:"}. -{"Database","データーベース"}. -{"Database Tables at ","データーベーステーブル: "}. +{"Current Discussion Topic","現在の話題"}. +{"Database failure","データーベース障害"}. {"Database Tables Configuration at ","データーベーステーブル設定 "}. +{"Database","データーベース"}. {"December","12月"}. {"Default users as participants","デフォルトのユーザーは参加者"}. {"Delete message of the day on all hosts","全ホストのお知らせメッセージを削除"}. {"Delete message of the day","お知らせメッセージを削除"}. -{"Delete Selected","選択した項目を削除"}. {"Delete User","ユーザーを削除"}. -{"Delete","削除"}. {"Deliver event notifications","イベント通知を配送する"}. {"Deliver payloads with event notifications","イベント通知と同時にペイロードを配送する"}. -{"Description:","説明:"}. {"Disc only copy","ディスクだけのコピー"}. -{"Displayed Groups:","表示グループ"}. -{"Don't tell your password to anybody, not even the administrators of the Jabber server.","パスワードは誰にも教えないようにしてください。Jabber サーバーの管理者があなたにパスワードを尋ねることはありません。"}. +{"Don't tell your password to anybody, not even the administrators of the XMPP server.","パスワードは誰にも(たとえ XMPP サーバーの管理者でも)教えないようにしてください。"}. {"Dump Backup to Text File at ","テキストファイルにバックアップ: "}. {"Dump to Text File","テキストファイルに出力"}. +{"Duplicated groups are not allowed by RFC6121","RFC6121 によりグループの重複は許されません"}. {"Edit Properties","プロパティを編集"}. {"Either approve or decline the voice request.","発言権の要求を承認または却下します。"}. -{"ejabberd IRC module","ejabberd IRC module"}. -{"ejabberd MUC module","ejabberd MUC module"}. +{"ejabberd HTTP Upload service","ejabberd HTTP ファイルアップロード"}. +{"ejabberd MUC module","ejabberd MUCモジュール"}. +{"ejabberd Multicast service","ejabberdマルチキャストサービス"}. {"ejabberd Publish-Subscribe module","ejabberd Publish-Subscribe モジュール"}. {"ejabberd SOCKS5 Bytestreams module","ejabberd SOCKS5 Bytestreams モジュール"}. {"ejabberd vCard module","ejabberd vCard モジュール"}. {"ejabberd Web Admin","ejabberd ウェブ管理"}. -{"Elements","要素"}. -{"Email","メールアドレス"}. +{"ejabberd","ejabberd"}. +{"Email Address","メールアドレス"}. +{"Email","メール"}. {"Enable logging","ロギングを有効"}. -{"Encoding for server ~b","サーバーのエンコーディング ~b"}. +{"Enable message archiving","メッセージアーカイブを有効化"}. {"End User Session","エンドユーザーセッション"}. -{"Enter list of {Module, [Options]}","{モジュール, [オプション]}のリストを入力してください"}. {"Enter nickname you want to register","登録するニックネームを入力してください"}. {"Enter path to backup file","バックアップファイルのパスを入力してください"}. {"Enter path to jabberd14 spool dir","jabberd14 spool ディレクトリのディレクトリを入力してください"}. {"Enter path to jabberd14 spool file","jabberd14 spool ファイルのパスを入力してください"}. {"Enter path to text file","テキストファイルのパスを入力してください"}. {"Enter the text you see","見えているテキストを入力してください"}. -{"Enter username and encodings you wish to use for connecting to IRC servers. Press 'Next' to get more fields to fill in. Press 'Complete' to save settings.","IRC サーバーに接続先するためのユーザー名と文字エンコーディングを入力してください。'Next' を押して次の項目に進みます。'Complete' を押すと設定が保存されます。"}. -{"Enter username, encodings, ports and passwords you wish to use for connecting to IRC servers","IRC サーバーに接続先するために使用するユーザー名、文字エンコーディング、ポート、パスワードを入力してください"}. -{"Erlang Jabber Server","Erlang Jabber Server"}. -{"Error","エラー"}. -{"Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef.net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}].","例: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef.net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]."}. -{"Exclude Jabber IDs from CAPTCHA challenge","CAPTCHA 試験を免除する Jabber ID"}. +{"Erlang XMPP Server","Erlang XMPP サーバー"}. +{"Exclude Jabber IDs from CAPTCHA challenge","CAPTCHA 入力を免除する Jabber ID"}. +{"Export all tables as SQL queries to a file:","すべてのテーブルをSQL形式でファイルにエクスポート: "}. {"Export data of all users in the server to PIEFXIS files (XEP-0227):","サーバーにあるすべてのユーザーデータを PIEFXIS ファイルにエクスポート (XEP-0227):"}. {"Export data of users in a host to PIEFXIS files (XEP-0227):","ホストのユーザーデータを PIEFXIS ファイルにエクスポート (XEP-0227):"}. +{"External component failure","外部コンポーネントの障害"}. +{"External component timeout","外部コンポーネントのタイムアウト"}. {"Failed to extract JID from your voice request approval","発言権要求の承認から JID を取り出すことに失敗しました"}. +{"Failed to parse HTTP response","HTTP 応答のパースに失敗しました"}. +{"Failed to process option '~s'","オプション '~s' の処理に失敗しました"}. {"Family Name","姓"}. {"February","2月"}. -{"Fill in fields to search for any matching Jabber User","欄を埋めて Jabber User を検索してください"}. -{"Fill in the form to search for any matching Jabber User (Add * to the end of field to match substring)","欄を埋めて Jabber User を検索してください (* を使用すると部分文字列にマッチします)"}. +{"Fill in the form to search for any matching XMPP User","XMPP ユーザーを検索するには欄に入力してください"}. {"Friday","金曜日"}. -{"From ~s","差出人 ~s"}. -{"From","差出人"}. +{"From ~ts","From ~ts"}. +{"Full List of Room Admins","チャットルーム管理者の一覧"}. +{"Full List of Room Owners","チャットルーム主宰者の一覧"}. {"Full Name","氏名"}. +{"Get List of Online Users","オンラインユーザーの一覧を取得"}. +{"Get List of Registered Users","登録ユーザーの一覧を取得"}. {"Get Number of Online Users","オンラインユーザー数を取得"}. {"Get Number of Registered Users","登録ユーザー数を取得"}. +{"Get Pending","保留中の取得"}. {"Get User Last Login Time","最終ログイン時間を取得"}. -{"Get User Password","パスワードを取得"}. {"Get User Statistics","ユーザー統計を取得"}. +{"Given Name","名"}. {"Grant voice to this person?","この人に発言権を与えますか ?"}. -{"Group ","グループ"}. -{"Groups","グループ"}. {"has been banned","はバンされました"}. -{"has been kicked","はキックされました"}. -{"has been kicked because of an affiliation change","は分掌が変更されたためキックされました"}. {"has been kicked because of a system shutdown","はシステムシャットダウンのためキックされました"}. -{"has been kicked because the room has been changed to members-only","は談話室がメンバー制に変更されたためキックされました"}. -{" has set the subject to: "," は題を設定しました: "}. -{"Host","ホスト"}. +{"has been kicked because of an affiliation change","は分掌が変更されたためキックされました"}. +{"has been kicked because the room has been changed to members-only","はチャットルームがメンバー制に変更されたためキックされました"}. +{"has been kicked","はキックされました"}. +{"Host unknown","不明なホスト"}. +{"HTTP File Upload","HTTP ファイルアップロード"}. {"If you don't see the CAPTCHA image here, visit the web page.","ここに CAPTCHA 画像が表示されない場合、ウェブページを参照してください。"}. -{"If you want to specify different ports, passwords, encodings for IRC servers, fill this list with values in format '{\"irc server\", \"encoding\", port, \"password\"}'. By default this service use \"~s\" encoding, port ~p, empty password.","別のポートやパスワード、文字エンコーディングを使用したい場合、'{\"irc server\", \"encoding\", port, \"password\"}' という形式のリストを入力してください。デフォルトでエンコーディングは \"~s\" を使用し、ポートは ~p、パスワードは空になっています。"}. {"Import Directory","ディレクトリインポート"}. {"Import File","ファイルからインポート"}. {"Import user data from jabberd14 spool file:","ユーザーデータを jabberd14 Spool ファイルからインポート:"}. @@ -139,174 +149,163 @@ {"Import Users from Dir at ","ディレクトリからユーザーをインポート: "}. {"Import Users From jabberd14 Spool Files","jabberd14 Spool ファイルからユーザーをインポート"}. {"Improper message type","誤ったメッセージタイプです"}. +{"Incorrect data form","データ形式が違います"}. {"Incorrect password","パスワードが違います"}. -{"Invalid affiliation: ~s","無効な分掌です: ~s"}. -{"Invalid role: ~s","無効な役です: ~s"}. +{"Installed Modules:","インストールされているモジュール:"}. +{"Install","インストール"}. +{"Insufficient privilege","権限が不十分です"}. +{"Internal server error","内部サーバーエラー"}. +{"Invalid node name","無効なノード名です"}. +{"Invalid 'previd' value","無効な 'previd' の値です"}. +{"Invitations are not allowed in this conference","この会議では、招待はできません"}. {"IP addresses","IP アドレス"}. -{"IP","IP"}. -{"IRC channel (don't put the first #)","IRC チャンネル (先頭に#は不要)"}. -{"IRC server","IRC サーバー"}. -{"IRC settings","IRC 設定"}. -{"IRC Transport","IRC トランスポート"}. -{"IRC username","IRC ユーザー名"}. -{"IRC Username","IRC ユーザー名"}. {"is now known as","は名前を変更しました: "}. +{"It is not allowed to send error messages to the room. The participant (~s) has sent an error message (~s) and got kicked from the room","このルームにエラーメッセージを送ることは許可されていません。参加者(~s)はエラーメッセージを(~s)を送信してルームからキックされました。"}. {"It is not allowed to send private messages of type \"groupchat\"","種別が\"groupchat\" であるプライベートメッセージを送信することはできません"}. {"It is not allowed to send private messages to the conference","この会議にプライベートメッセージを送信することはできません"}. -{"It is not allowed to send private messages","プライベートメッセージを送信することはできません"}. -{"Jabber Account Registration","Jabber アカウント登録"}. {"Jabber ID","Jabber ID"}. -{"Jabber ID ~s is invalid","Jabber ID ~s は無効です"}. {"January","1月"}. -{"Join IRC channel","IRC チャンネルに参加"}. -{"joins the room","が談話室に参加しました"}. -{"Join the IRC channel here.","この IRC チャンネルに参加します。"}. -{"Join the IRC channel in this Jabber ID: ~s","Jabber ID: ~s でこの IRC チャンネルに参加"}. +{"JID normalization failed","JID の正規化に失敗しました"}. +{"joins the room","がチャットルームに参加しました"}. {"July","7月"}. {"June","6月"}. +{"Just created","作成しました"}. {"Last Activity","活動履歴"}. {"Last login","最終ログイン"}. +{"Last message","最終メッセージ"}. {"Last month","先月"}. {"Last year","去年"}. -{"leaves the room","が談話室から退出しました"}. -{"Listened Ports at ","Listen ポート "}. -{"Listened Ports","Listen ポート"}. -{"List of modules to start","起動モジュールの一覧"}. -{"Low level update script","低レベル更新スクリプト"}. +{"leaves the room","がチャットルームから退出しました"}. {"Make participants list public","参加者一覧を公開"}. -{"Make room CAPTCHA protected","談話室を CAPTCHA で保護"}. -{"Make room members-only","談話室をメンバーのみに制限"}. -{"Make room moderated","談話室をモデレート化"}. -{"Make room password protected","談話室をパスワードで保護"}. -{"Make room persistent","談話室を永続化"}. -{"Make room public searchable","談話室を検索可"}. +{"Make room CAPTCHA protected","チャットルームを CAPTCHA で保護"}. +{"Make room members-only","チャットルームをメンバーのみに制限"}. +{"Make room moderated","チャットルームをモデレート化"}. +{"Make room password protected","チャットルームをパスワードで保護"}. +{"Make room persistent","チャットルームを永続化"}. +{"Make room public searchable","チャットルームを検索可"}. +{"Malformed username","不正な形式のユーザー名"}. {"March","3月"}. -{"Maximum Number of Occupants","最大在室者数"}. -{"Max # of items to persist","アイテムの最大保存数"}. {"Max payload size in bytes","最大ぺイロードサイズ (byte)"}. +{"Maximum file size","最大ファイルサイズ"}. +{"Maximum Number of Occupants","最大在室者数"}. {"May","5月"}. -{"Members:","メンバー:"}. -{"Membership is required to enter this room","この談話室に入るにはメンバーでなければなりません"}. -{"Memorize your password, or write it in a paper placed in a safe place. In Jabber there isn't an automated way to recover your password if you forget it.","パスワードは記憶するか、紙に書いて安全な場所に保管してください。もしあなたがパスワードを忘れてしまった場合、Jabber ではパスワードのリカバリを自動的に行うことはできません。"}. -{"Memory","メモリ"}. +{"Membership is required to enter this room","このチャットルームに入るにはメンバーでなければなりません"}. +{"Memorize your password, or write it in a paper placed in a safe place. In XMPP there isn't an automated way to recover your password if you forget it.","パスワードは記憶するか、紙に書いて安全な場所に保管してください。もしあなたがパスワードを忘れてしまった場合、XMPP ではパスワードのリカバリを自動的に行うことはできません。"}. {"Message body","本文"}. {"Middle Name","ミドルネーム"}. {"Minimum interval between voice requests (in seconds)","発言権の要求の最小時間間隔 (秒)"}. {"Moderator privileges required","モデレーター権限が必要です"}. -{"moderators only","モデレーターにのみ"}. -{"Modified modules","変更されたモジュール"}. -{"Module","モジュール"}. -{"Modules","モジュール"}. -{"Modules at ","モジュール "}. +{"Moderator","モデレーター"}. {"Monday","月曜日"}. +{"Multicast","マルチキャスト"}. +{"Multi-User Chat","マルチユーザーチャット"}. {"Name","名"}. -{"Name:","名前:"}. +{"Natural-Language Room Name","自然言語での会議室名"}. {"Never","なし"}. {"New Password:","新しいパスワード:"}. -{"Nickname","ニックネーム"}. +{"Nickname can't be empty","ニックネームは空にできません"}. {"Nickname Registration at ","ニックネーム登録: "}. -{"Nickname ~s does not exist in the room","ニックネーム ~s はこの談話室にいません"}. +{"Nickname ~s does not exist in the room","ニックネーム ~s はこのチャットルームにいません"}. +{"Nickname","ニックネーム"}. +{"No address elements found","アドレス要素が見つかりません"}. +{"No addresses element found","アドレス要素が見つかりません"}. {"No body provided for announce message","アナウンスメッセージはありませんでした"}. -{"nobody","誰にも許可しない"}. +{"No child elements found","子要素が見つかりません"}. {"No Data","データなし"}. -{"Node ","ノード "}. +{"No element found"," 要素が見つかりません"}. +{"No 'item' element found","'item' 要素が見つかりません"}. +{"No limit","制限なし"}. +{"No 'password' found in this query","このクエリに 'password' が見つかりません"}. +{"No services available","利用できるサービスはありません"}. +{"Node already exists","ノードは既に存在しています"}. {"Node ID","ノードID"}. {"Node not found","ノードが見つかりません"}. +{"Node ~p","ノード ~p"}. {"Nodes","ノード"}. -{"No limit","制限なし"}. +{"Node","ノード"}. {"None","なし"}. -{"No resource provided","リソースが提供されませんでした"}. {"Not Found","見つかりません"}. {"Notify subscribers when items are removed from the node","アイテムがノードから消された時に購読者へ通知する"}. {"Notify subscribers when the node configuration changes","ノード設定に変更があった時に購読者へ通知する"}. {"Notify subscribers when the node is deleted","ノードが削除された時に購読者へ通知する"}. {"November","11月"}. {"Number of occupants","在室者の数"}. +{"Number of Offline Messages","オフラインメッセージ数"}. {"Number of online users","オンラインユーザー数"}. {"Number of registered users","登録ユーザー数"}. +{"Occupants are allowed to invite others","在室者は誰かを招待することができます"}. +{"Occupants May Change the Subject","ユーザーによる件名の変更を許可"}. {"October","10月"}. -{"Offline Messages:","オフラインメッセージ:"}. -{"Offline Messages","オフラインメッセージ"}. {"OK","OK"}. {"Old Password:","古いパスワード:"}. -{"Online","オンライン"}. -{"Online Users:","オンラインユーザー:"}. {"Online Users","オンラインユーザー"}. +{"Online","オンライン"}. {"Only deliver notifications to available users","有効なユーザーにのみ告知を送信する"}. -{"Only moderators and participants are allowed to change the subject in this room","モデレーターと参加者のみが談話室の題を変更できます"}. -{"Only moderators are allowed to change the subject in this room","モデレーターのみが談話室の題を変更できます"}. +{"Only or tags are allowed"," タグまたは タグのみが許可されます"}. +{"Only element is allowed in this query","このクエリでは 要素のみが許可されます"}. +{"Only members may query archives of this room","メンバーのみがこのルームのアーカイブを取得できます"}. +{"Only moderators and participants are allowed to change the subject in this room","モデレーターと参加者のみがチャットルームの件名を変更できます"}. +{"Only moderators are allowed to change the subject in this room","モデレーターのみがチャットルームの件名を変更できます"}. {"Only moderators can approve voice requests","モデレーターだけが発言権の要求を承認できます"}. {"Only occupants are allowed to send messages to the conference","在室者のみがこの会議にメッセージを送ることができます"}. {"Only occupants are allowed to send queries to the conference","在室者のみが会議にクエリーを送信することができます"}. {"Only service administrators are allowed to send service messages","サービス管理者のみがサービスメッセージを送信できます"}. -{"Options","オプション"}. {"Organization Name","会社名"}. {"Organization Unit","部署名"}. -{"Outgoing s2s Connections:","外向き s2s コネクション:"}. {"Outgoing s2s Connections","外向き s2s コネクション"}. -{"Outgoing s2s Servers:","外向き s2s サービス:"}. {"Owner privileges required","主宰者の権限が必要です"}. -{"Packet","パケット"}. -{"Password:","パスワード"}. -{"Password","パスワード"}. -{"Password ~b","パスワード ~b"}. -{"Password Verification:","パスワード (確認):"}. +{"Participant ID","参加者 ID"}. +{"Participant","参加者"}. {"Password Verification","パスワード (確認)"}. +{"Password Verification:","パスワード (確認):"}. +{"Password","パスワード"}. +{"Password:","パスワード:"}. {"Path to Dir","ディレクトリのパス"}. {"Path to File","ファイルのパス"}. -{"Pending","保留"}. {"Period: ","期間: "}. {"Persist items to storage","アイテムをストレージに保存する"}. +{"Persistent","チャットルームを永続化"}. {"Ping","Ping"}. {"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","これらのオプションは組み込みの Mnesia データーベースのバックアップのみを行うことに注意してください。もし ODBC モジュールを使用している場合は、SQL データーベースのバックアップを別に行う必要があります。"}. {"Please, wait for a while before sending new voice request","新しい発言権の要求を送るまで少し間をおいてください"}. {"Pong","Pong"}. -{"Port","ポート"}. -{"Port ~b","ポート ~b"}. {"Present real Jabber IDs to","本当の Jabber ID を公開"}. +{"Previous session not found","前のセッションが見つかりません"}. {"private, ","プライベート、"}. -{"Protocol","プロトコル"}. +{"Public","公開"}. {"Publish-Subscribe","Publish-Subscribe"}. {"PubSub subscriber request","PubSub 購読者のリクエスト"}. {"Purge all items when the relevant publisher goes offline","公開者がオフラインになるときに、すべてのアイテムを削除"}. -{"Queries to the conference members are not allowed in this room","この談話室では、会議のメンバーへのクエリーは禁止されています"}. +{"Queries to the conference members are not allowed in this room","このチャットルームでは、会議のメンバーへのクエリーは禁止されています"}. +{"Query to another users is forbidden","他のユーザーへのクエリは禁止されています"}. {"RAM and disc copy","RAM, ディスクコピー"}. {"RAM copy","RAM コピー"}. -{"Raw","Raw"}. {"Really delete message of the day?","本当にお知らせメッセージを削除しますか ?"}. {"Recipient is not in the conference room","受信者はこの会議室にいません"}. -{"Register a Jabber account","Jabber アカウントを登録"}. -{"Registered Users:","登録ユーザー:"}. -{"Registered Users","登録ユーザー"}. +{"Register an XMPP account","XMPP アカウントを登録"}. {"Register","登録"}. -{"Registration in mod_irc for ","mod_irc での登録: "}. {"Remote copy","リモートコピー"}. -{"Remove All Offline Messages","すべてのオフラインメッセージを削除"}. {"Remove User","ユーザーを削除"}. -{"Remove","削除"}. {"Replaced by new connection","新しいコネクションによって置き換えられました"}. +{"Request is ignored","リクエストは無視されます"}. {"Resources","リソース"}. {"Restart Service","サービスを再起動"}. -{"Restart","再起動"}. -{"Restore","リストア"}. {"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:","直ちにプレーンテキストバックアップからリストア:"}. -{"Room Configuration","談話室の設定"}. -{"Room creation is denied by service policy","サービスポリシーによって談話室の作成が禁止されています"}. -{"Room description","談話室の説明"}. +{"Restore","リストア"}. +{"Roles for which Presence is Broadcasted","プレゼンスをブロードキャストするロール"}. +{"Room Configuration","チャットルームの設定"}. +{"Room creation is denied by service policy","サービスポリシーによってチャットルームの作成が禁止されています"}. +{"Room description","チャットルームの説明"}. {"Room Occupants","在室者"}. -{"Room title","談話室のタイトル"}. +{"Room title","チャットルームのタイトル"}. {"Roster groups allowed to subscribe","名簿グループは購読を許可しました"}. -{"Roster of ","名簿: "}. {"Roster size","名簿サイズ"}. -{"Roster","名簿"}. -{"RPC Call Error","RPC 呼び出しエラー"}. {"Running Nodes","起動ノード"}. -{"~s access rule configuration","~s アクセスルール設定"}. +{"~s invites you to the room ~s","~s はあなたをチャットルーム ~s に招待しています"}. {"Saturday","土曜日"}. -{"Script check","スクリプトチェック"}. {"Search Results for ","検索結果: "}. {"Search users in ","ユーザーの検索: "}. {"Send announcement to all online users on all hosts","全ホストのオンラインユーザーにアナウンスを送信"}. @@ -315,107 +314,109 @@ {"Send announcement to all users","すべてのユーザーにアナウンスを送信"}. {"September","9月"}. {"Server:","サーバー:"}. -{"Server ~b","サーバー ~b"}. {"Set message of the day and send to online users","お知らせメッセージを設定し、オンラインユーザーに送信"}. {"Set message of the day on all hosts and send to online users","全ホストのお知らせメッセージを設定し、オンラインユーザーに送信"}. {"Shared Roster Groups","共有名簿グループ"}. {"Show Integral Table","累積の表を表示"}. {"Show Ordinary Table","通常の表を表示"}. {"Shut Down Service","サービスを停止"}. -{"~s invites you to the room ~s","~s はあなたを談話室 ~s に招待しています"}. -{"Some Jabber clients can store your password in your computer. Use that feature only if you trust your computer is safe.","Jabber クライアントはコンピューターにパスワードを記憶できます。コンピューターが安全であると信頼できる場合にのみ、この機能を使用してください。"}. +{"SOCKS5 Bytestreams","SOCKS5 Bytestreams"}. +{"Some XMPP clients can store your password in the computer, but you should do this only in your personal computer for safety reasons.","XMPP クライアントはコンピューターにパスワードを記憶できます。コンピューターが安全であると信頼できる場合にのみ、この機能を使用してください。"}. {"Specify the access model","アクセスモデルを設定する"}. {"Specify the event message type","イベントメッセージ種別を設定"}. {"Specify the publisher model","公開モデルを指定する"}. -{"~s's Offline Messages Queue","~s' のオフラインメッセージキュー"}. -{"Start Modules at ","モジュールを開始: "}. -{"Start Modules","モジュールを起動"}. -{"Start","開始"}. -{"Statistics of ~p","~p の統計"}. -{"Statistics","統計"}. -{"Stop Modules at ","モジュールを停止: "}. -{"Stop Modules","モジュールを停止"}. +{"Stanza ID","スタンザ ID"}. {"Stopped Nodes","停止ノード"}. -{"Stop","停止"}. -{"Storage Type","ストレージタイプ"}. {"Store binary backup:","バイナリバックアップを保存:"}. {"Store plain text backup:","プレーンテキストバックアップを保存:"}. -{"Subject","題"}. +{"Subject","件名"}. {"Submitted","送信完了"}. -{"Submit","送信"}. {"Subscriber Address","購読者のアドレス"}. -{"Subscription","認可"}. {"Sunday","日曜日"}. {"That nickname is already in use by another occupant","そのニックネームは既にほかの在室者によって使用されています"}. {"That nickname is registered by another person","ニックネームはほかの人によって登録されています"}. +{"The account already exists","アカウントは既に存在しています"}. +{"The account was not unregistered","アカウントは削除されませんでした"}. {"The CAPTCHA is valid.","CAPTCHA は有効です。"}. {"The CAPTCHA verification has failed","CAPTCHA 検証は失敗しました"}. +{"The captcha you entered is wrong","入力した CAPTCHA は間違っています"}. {"The collections with which a node is affiliated","提携されたノードの集合です"}. -{"the password is","パスワードは"}. +{"The default language of the node","ノードのデフォルトの言語"}. +{"The JID of the node creator","ノード作成者の JID"}. +{"The name of the node","ノード名"}. +{"The password contains unacceptable characters","パスワードに使用できない文字が含まれています"}. {"The password is too weak","このパスワードは単純過ぎます"}. -{"The password of your Jabber account was successfully changed.","Jabber アカウントのパスワード変更に成功しました。"}. -{"There was an error changing the password: ","パスワードの変更中にエラーが発生しました: "}. +{"the password is","パスワードは"}. +{"The password of your XMPP account was successfully changed.","XMPP アカウントのパスワード変更に成功しました。"}. +{"The password was not changed","このパスワードは変更されませんでした"}. +{"The passwords are different","このパスワードが違います"}. {"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 create a Jabber account in this Jabber server. Your JID (Jabber IDentifier) will be of the form: username@server. Please read carefully the instructions to fill correctly the fields.","ここはこの Jabber サーバーにアカウントを作成するページです。あなたの JID (JabberID) は username@server のような形式になります。注意事項どおり、正しく項目を記入してください。"}. -{"This page allows to unregister a Jabber account in this Jabber server.","ここはこの Jabber サーバーのアカウントを削除するページです。"}. -{"This participant is kicked from the room because he sent an error message to another participant","他の参加者にエラーメッセージを送信したため、この参加者はキックされました"}. -{"This participant is kicked from the room because he sent an error message","エラーメッセージを送信したため、この参加者はキックされました"}. -{"This participant is kicked from the room because he sent an error presence","エラープレゼンスを送信したため、この参加者はキックされました"}. -{"This room is not anonymous","この談話室は非匿名です"}. +{"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 (JabberID) は username@server のような形式になります。注意事項どおり、正しく項目を記入してください。"}. +{"This page allows to unregister an XMPP account in this XMPP server.","このページはサーバー上のXMPPアカウントを削除するページです。"}. +{"This room is not anonymous","このチャットルームは非匿名です"}. {"Thursday","木曜日"}. {"Time delay","遅延時間"}. -{"Time","時間"}. {"Too many CAPTCHA requests","CAPTCHA 要求が多すぎます"}. -{"To ~s","宛先 ~s"}. -{"To","宛先"}. +{"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","~p回の認証に失敗しました。このIPアドレス(~s)は~s UTCまでブロックされます。"}. +{"Too many unacked stanzas","多くのスタンザが応答していません"}. +{"Too many users in this conference","この会議にはユーザーが多すぎます"}. {"Traffic rate limit is exceeded","トラフィックレートの制限を超えました"}. -{"Transactions Aborted:","トランザクションの失敗:"}. -{"Transactions Committed:","トランザクションのコミット:"}. -{"Transactions Logged:","トランザクションのログ: "}. -{"Transactions Restarted:","トランザクションの再起動:"}. +{"~ts's Offline Messages Queue","~ts のオフラインメッセージキュー"}. {"Tuesday","火曜日"}. {"Unable to generate a CAPTCHA","CAPTCHA を生成できません"}. {"Unauthorized","認証されていません"}. -{"Unregister a Jabber account","Jabber アカウントを削除"}. +{"Unexpected action","予期しないアクション"}. +{"Unexpected error condition: ~p","予期しないエラー状態: ~p"}. +{"Uninstall","アンインストール"}. +{"Unregister an XMPP account","XMPP アカウントを削除"}. {"Unregister","削除"}. +{"Unsupported version","対応していないバージョン"}. {"Update message of the day (don't send)","お知らせメッセージを更新 (送信しない)"}. {"Update message of the day on all hosts (don't send)","全ホストのお知らせメッセージを更新 (送信しない)"}. -{"Update plan","更新計画"}. -{"Update script","スクリプトの更新"}. -{"Update ","更新 "}. -{"Update","更新"}. -{"Uptime:","起動時間:"}. -{"Use of STARTTLS required","STARTTLS の使用が必要です"}. -{"User ","ユーザー "}. -{"User","ユーザー"}. +{"Upgrade","アップグレード"}. +{"User already exists","ユーザーは既に存在しています"}. +{"User (jid)","ユーザー (JID)"}. {"User JID","ユーザー JID"}. {"User Management","ユーザー管理"}. +{"User removed","ユーザーを削除しました"}. +{"User session not found","ユーザーセッションが見つかりません"}. +{"User session terminated","ユーザーセッションが終了しました"}. +{"User ~ts","ユーザー ~ts"}. {"Username:","ユーザー名:"}. -{"Users","ユーザー"}. {"Users are not allowed to register accounts so quickly","それほど速くアカウントを登録することはできません"}. {"Users Last Activity","ユーザーの活動履歴"}. -{"Validate","検証"}. -{"vCard User Search","vCard ユーザー検索"}. -{"Virtual Hosts","ヴァーチャルホスト"}. -{"Visitors are not allowed to change their nicknames in this room","傍聴者はこの談話室でニックネームを変更することはできません"}. +{"Users","ユーザー"}. +{"User","ユーザー"}. +{"Value of '~s' should be boolean","'~s' の値はブール値である必要があります"}. +{"Value of '~s' should be integer","'~s' の値は整数である必要があります"}. +{"vCard User Search","vCard検索"}. +{"Virtual Hosts","バーチャルホスト"}. +{"Visitors are not allowed to change their nicknames in this room","傍聴者はこのチャットルームでニックネームを変更することはできません"}. {"Visitors are not allowed to send messages to all occupants","傍聴者はすべての在室者にメッセージを送信することはできません"}. +{"Visitor","傍聴者"}. {"Voice requests are disabled in this conference","この会議では、発言権の要求はできません"}. {"Voice request","発言権を要求"}. {"Wednesday","水曜日"}. +{"When a new subscription is processed","新しい購読が処理されるとき"}. {"When to send the last published item","最後の公開アイテムを送信するタイミングで"}. {"Whether to allow subscriptions","購読を許可するかどうか"}. -{"You can later change your password using a Jabber client.","あなたは後で Jabber クライアントを使用してパスワードを変更できます。"}. -{"You have been banned from this room","あなたはこの談話室からバンされています"}. +{"Who can send private messages","誰がプライベートメッセージを送れるか"}. +{"XMPP Account Registration","XMPP アカウント登録"}. +{"XMPP Domains","XMPP ドメイン"}. +{"You are being removed from the room because of a system shutdown","システムシャットダウンのためチャットルームから削除されました"}. +{"You are not allowed to send private messages","プライベートメッセージを送信する権限がありません"}. +{"You are not joined to the channel","チャンネルに参加していません"}. +{"You can later change your password using an XMPP client.","あなたは後で XMPP クライアントを使用してパスワードを変更できます。"}. +{"You have been banned from this room","あなたはこのチャットルームから締め出されています"}. {"You must fill in field \"Nickname\" in the form","フォームの\"ニックネーム\"欄を入力する必要があります"}. {"You need a client that supports x:data and CAPTCHA to register","登録を行うには x:data と CAPTCHA をサポートするクライアントが必要です"}. {"You need a client that supports x:data to register the nickname","ニックネームを登録するには x:data をサポートするクライアントが必要です"}. -{"You need an x:data capable client to configure mod_irc settings","mod_irc の設定には x:data をサポートするクライアントが必要です"}. -{"You need an x:data capable client to configure room","談話室を設定するには 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 Jabber account was successfully created.","Jabber アカウントの作成に成功しました。"}. -{"Your Jabber account was successfully deleted.","Jabber アカウントの削除に成功しました。"}. -{"Your messages to ~s are being blocked. To unblock them, visit ~s","~s 宛のメッセージはブロックされています。解除するにはこちらを見てください ~s"}. +{"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 アカウントの削除に成功しました。"}. +{"You're not allowed to create nodes","ノードを作成する権限がありません"}. diff --git a/priv/msgs/ja.po b/priv/msgs/ja.po deleted file mode 100644 index f7785c230..000000000 --- a/priv/msgs/ja.po +++ /dev/null @@ -1,1857 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: ejabberd 2.1.x\n" -"PO-Revision-Date: 2012-04-16 15:48+0900\n" -"Last-Translator: Mako N \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Language: Japanese (日本語)\n" -"X-Additional-Translator: Tsukasa Hamano \n" - -#: ejabberd_c2s.erl:424 ejabberd_c2s.erl:727 -msgid "Use of STARTTLS required" -msgstr "STARTTLS の使用が必要です" - -#: ejabberd_c2s.erl:503 -msgid "No resource provided" -msgstr "リソースが提供されませんでした" - -#: ejabberd_c2s.erl:1197 -msgid "Replaced by new connection" -msgstr "新しいコネクションによって置き換えられました" - -#: ejabberd_c2s.erl:1885 -msgid "Your active privacy list has denied the routing of this stanza." -msgstr "あなたのプライバシーリストはこのスタンザのルーティングを拒否しました。" - -#: ejabberd_captcha.erl:96 ejabberd_captcha.erl:152 ejabberd_captcha.erl:178 -msgid "Enter the text you see" -msgstr "見えているテキストを入力してください" - -#: ejabberd_captcha.erl:101 -msgid "Your messages to ~s are being blocked. To unblock them, visit ~s" -msgstr "" -"~s 宛のメッセージはブロックされています。解除するにはこちらを見てください ~s" - -#: ejabberd_captcha.erl:134 -msgid "If you don't see the CAPTCHA image here, visit the web page." -msgstr "" -"ここに CAPTCHA 画像が表示されない場合、ウェブページを参照してください。" - -#: ejabberd_captcha.erl:146 -msgid "CAPTCHA web page" -msgstr "CAPTCHA ウェブページ" - -#: ejabberd_captcha.erl:307 -msgid "The CAPTCHA is valid." -msgstr "CAPTCHA は有効です。" - -#: mod_adhoc.erl:95 mod_adhoc.erl:125 mod_adhoc.erl:143 mod_adhoc.erl:161 -msgid "Commands" -msgstr "コマンド" - -#: mod_adhoc.erl:149 mod_adhoc.erl:243 -msgid "Ping" -msgstr "Ping" - -#: mod_adhoc.erl:260 -msgid "Pong" -msgstr "Pong" - -#: mod_announce.erl:507 mod_announce_odbc.erl:499 -msgid "Really delete message of the day?" -msgstr "本当にお知らせメッセージを削除しますか ?" - -#: mod_announce.erl:515 mod_announce_odbc.erl:507 mod_configure.erl:1083 -#: mod_configure.erl:1128 -msgid "Subject" -msgstr "題" - -#: mod_announce.erl:520 mod_announce_odbc.erl:512 mod_configure.erl:1088 -#: mod_configure.erl:1133 -msgid "Message body" -msgstr "本文" - -#: mod_announce.erl:600 mod_announce_odbc.erl:592 -msgid "No body provided for announce message" -msgstr "アナウンスメッセージはありませんでした" - -#: mod_announce.erl:635 mod_announce_odbc.erl:627 -msgid "Announcements" -msgstr "アナウンス" - -#: mod_announce.erl:637 mod_announce_odbc.erl:629 -msgid "Send announcement to all users" -msgstr "すべてのユーザーにアナウンスを送信" - -#: mod_announce.erl:639 mod_announce_odbc.erl:631 -msgid "Send announcement to all users on all hosts" -msgstr "全ホストのユーザーにアナウンスを送信" - -#: mod_announce.erl:641 mod_announce_odbc.erl:633 -msgid "Send announcement to all online users" -msgstr "すべてのオンラインユーザーにアナウンスを送信" - -#: mod_announce.erl:643 mod_announce_odbc.erl:635 mod_configure.erl:1078 -#: mod_configure.erl:1123 -msgid "Send announcement to all online users on all hosts" -msgstr "全ホストのオンラインユーザーにアナウンスを送信" - -#: mod_announce.erl:645 mod_announce_odbc.erl:637 -msgid "Set message of the day and send to online users" -msgstr "お知らせメッセージを設定し、オンラインユーザーに送信" - -#: mod_announce.erl:647 mod_announce_odbc.erl:639 -msgid "Set message of the day on all hosts and send to online users" -msgstr "全ホストのお知らせメッセージを設定し、オンラインユーザーに送信" - -#: mod_announce.erl:649 mod_announce_odbc.erl:641 -msgid "Update message of the day (don't send)" -msgstr "お知らせメッセージを更新 (送信しない)" - -#: mod_announce.erl:651 mod_announce_odbc.erl:643 -msgid "Update message of the day on all hosts (don't send)" -msgstr "全ホストのお知らせメッセージを更新 (送信しない)" - -#: mod_announce.erl:653 mod_announce_odbc.erl:645 -msgid "Delete message of the day" -msgstr "お知らせメッセージを削除" - -#: mod_announce.erl:655 mod_announce_odbc.erl:647 -msgid "Delete message of the day on all hosts" -msgstr "全ホストのお知らせメッセージを削除" - -#: mod_configure.erl:114 mod_configure.erl:274 mod_configure.erl:296 -#: mod_configure.erl:498 -msgid "Configuration" -msgstr "設定" - -#: mod_configure.erl:125 mod_configure.erl:576 web/ejabberd_web_admin.erl:1936 -msgid "Database" -msgstr "データーベース" - -#: mod_configure.erl:127 mod_configure.erl:595 -msgid "Start Modules" -msgstr "モジュールを起動" - -#: mod_configure.erl:129 mod_configure.erl:596 -msgid "Stop Modules" -msgstr "モジュールを停止" - -#: mod_configure.erl:131 mod_configure.erl:604 web/ejabberd_web_admin.erl:1937 -msgid "Backup" -msgstr "バックアップ" - -#: mod_configure.erl:133 mod_configure.erl:605 -msgid "Restore" -msgstr "リストア" - -#: mod_configure.erl:135 mod_configure.erl:606 -msgid "Dump to Text File" -msgstr "テキストファイルに出力" - -#: mod_configure.erl:137 mod_configure.erl:615 -msgid "Import File" -msgstr "ファイルからインポート" - -#: mod_configure.erl:139 mod_configure.erl:616 -msgid "Import Directory" -msgstr "ディレクトリインポート" - -#: mod_configure.erl:141 mod_configure.erl:581 mod_configure.erl:1057 -msgid "Restart Service" -msgstr "サービスを再起動" - -#: mod_configure.erl:143 mod_configure.erl:582 mod_configure.erl:1102 -msgid "Shut Down Service" -msgstr "サービスを停止" - -#: mod_configure.erl:145 mod_configure.erl:518 mod_configure.erl:1197 -#: web/ejabberd_web_admin.erl:1527 -msgid "Add User" -msgstr "ユーザーを追加" - -#: mod_configure.erl:147 mod_configure.erl:519 mod_configure.erl:1219 -msgid "Delete User" -msgstr "ユーザーを削除" - -#: mod_configure.erl:149 mod_configure.erl:520 mod_configure.erl:1231 -msgid "End User Session" -msgstr "エンドユーザーセッション" - -#: mod_configure.erl:151 mod_configure.erl:521 mod_configure.erl:1243 -#: mod_configure.erl:1255 -msgid "Get User Password" -msgstr "パスワードを取得" - -#: mod_configure.erl:153 mod_configure.erl:522 -msgid "Change User Password" -msgstr "パスワードを変更" - -#: mod_configure.erl:155 mod_configure.erl:523 mod_configure.erl:1272 -msgid "Get User Last Login Time" -msgstr "最終ログイン時間を取得" - -#: mod_configure.erl:157 mod_configure.erl:524 mod_configure.erl:1284 -msgid "Get User Statistics" -msgstr "ユーザー統計を取得" - -#: mod_configure.erl:159 mod_configure.erl:525 -msgid "Get Number of Registered Users" -msgstr "登録ユーザー数を取得" - -#: mod_configure.erl:161 mod_configure.erl:526 -msgid "Get Number of Online Users" -msgstr "オンラインユーザー数を取得" - -#: mod_configure.erl:163 mod_configure.erl:509 web/ejabberd_web_admin.erl:831 -#: web/ejabberd_web_admin.erl:872 -msgid "Access Control Lists" -msgstr "アクセスコントロールリスト" - -#: mod_configure.erl:165 mod_configure.erl:510 web/ejabberd_web_admin.erl:940 -#: web/ejabberd_web_admin.erl:976 -msgid "Access Rules" -msgstr "アクセスルール" - -#: mod_configure.erl:297 mod_configure.erl:499 -msgid "User Management" -msgstr "ユーザー管理" - -#: mod_configure.erl:500 web/ejabberd_web_admin.erl:1058 -#: web/ejabberd_web_admin.erl:1462 -msgid "Online Users" -msgstr "オンラインユーザー" - -#: mod_configure.erl:501 -msgid "All Users" -msgstr "全ユーザー" - -#: mod_configure.erl:502 -msgid "Outgoing s2s Connections" -msgstr "外向き s2s コネクション" - -#: mod_configure.erl:503 web/ejabberd_web_admin.erl:1907 -msgid "Running Nodes" -msgstr "起動ノード" - -#: mod_configure.erl:504 web/ejabberd_web_admin.erl:1909 -msgid "Stopped Nodes" -msgstr "停止ノード" - -#: mod_configure.erl:577 mod_configure.erl:587 web/ejabberd_web_admin.erl:1953 -msgid "Modules" -msgstr "モジュール" - -#: mod_configure.erl:578 -msgid "Backup Management" -msgstr "バックアップ管理" - -#: mod_configure.erl:579 -msgid "Import Users From jabberd14 Spool Files" -msgstr "jabberd14 Spool ファイルからユーザーをインポート" - -#: mod_configure.erl:699 -msgid "To ~s" -msgstr "宛先 ~s" - -#: mod_configure.erl:717 -msgid "From ~s" -msgstr "差出人 ~s" - -#: mod_configure.erl:913 -msgid "Database Tables Configuration at " -msgstr "データーベーステーブル設定 " - -#: mod_configure.erl:918 -msgid "Choose storage type of tables" -msgstr "テーブルのストレージタイプを選択" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Disc only copy" -msgstr "ディスクだけのコピー" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM and disc copy" -msgstr "RAM, ディスクコピー" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM copy" -msgstr "RAM コピー" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Remote copy" -msgstr "リモートコピー" - -#: mod_configure.erl:950 -msgid "Stop Modules at " -msgstr "モジュールを停止: " - -#: mod_configure.erl:954 -msgid "Choose modules to stop" -msgstr "停止するモジュールを選択" - -#: mod_configure.erl:969 -msgid "Start Modules at " -msgstr "モジュールを開始: " - -#: mod_configure.erl:973 -msgid "Enter list of {Module, [Options]}" -msgstr "{モジュール, [オプション]}のリストを入力してください" - -#: mod_configure.erl:974 -msgid "List of modules to start" -msgstr "起動モジュールの一覧" - -#: mod_configure.erl:983 -msgid "Backup to File at " -msgstr "ファイルにバックアップ: " - -#: mod_configure.erl:987 mod_configure.erl:1001 -msgid "Enter path to backup file" -msgstr "バックアップファイルのパスを入力してください" - -#: mod_configure.erl:988 mod_configure.erl:1002 mod_configure.erl:1016 -#: mod_configure.erl:1030 -msgid "Path to File" -msgstr "ファイルのパス" - -#: mod_configure.erl:997 -msgid "Restore Backup from File at " -msgstr "ファイルからバックアップをリストア: " - -#: mod_configure.erl:1011 -msgid "Dump Backup to Text File at " -msgstr "テキストファイルにバックアップ: " - -#: mod_configure.erl:1015 -msgid "Enter path to text file" -msgstr "テキストファイルのパスを入力してください" - -#: mod_configure.erl:1025 -msgid "Import User from File at " -msgstr "ファイルからユーザーをインポート: " - -#: mod_configure.erl:1029 -msgid "Enter path to jabberd14 spool file" -msgstr "jabberd14 spool ファイルのパスを入力してください" - -#: mod_configure.erl:1039 -msgid "Import Users from Dir at " -msgstr "ディレクトリからユーザーをインポート: " - -#: mod_configure.erl:1043 -msgid "Enter path to jabberd14 spool dir" -msgstr "jabberd14 spool ディレクトリのディレクトリを入力してください" - -#: mod_configure.erl:1044 -msgid "Path to Dir" -msgstr "ディレクトリのパス" - -#: mod_configure.erl:1060 mod_configure.erl:1105 -msgid "Time delay" -msgstr "遅延時間" - -#: mod_configure.erl:1143 -msgid "Access Control List Configuration" -msgstr "アクセスコントロールリスト設定" - -#: mod_configure.erl:1147 -msgid "Access control lists" -msgstr "アクセスコントロールリスト" - -#: mod_configure.erl:1171 -msgid "Access Configuration" -msgstr "アクセス設定" - -#: mod_configure.erl:1175 -msgid "Access rules" -msgstr "アクセスルール" - -#: mod_configure.erl:1200 mod_configure.erl:1222 mod_configure.erl:1234 -#: mod_configure.erl:1246 mod_configure.erl:1258 mod_configure.erl:1275 -#: mod_configure.erl:1287 mod_configure.erl:1650 mod_configure.erl:1700 -#: mod_configure.erl:1721 mod_roster.erl:943 mod_roster_odbc.erl:1060 -#: mod_vcard.erl:472 mod_vcard_ldap.erl:554 mod_vcard_odbc.erl:448 -msgid "Jabber ID" -msgstr "Jabber ID" - -#: mod_configure.erl:1205 mod_configure.erl:1263 mod_configure.erl:1651 -#: mod_configure.erl:1863 mod_muc/mod_muc_room.erl:3224 mod_register.erl:235 -#: web/ejabberd_web_admin.erl:1520 -msgid "Password" -msgstr "パスワード" - -#: mod_configure.erl:1210 -msgid "Password Verification" -msgstr "パスワード (確認)" - -#: mod_configure.erl:1301 -msgid "Number of registered users" -msgstr "登録ユーザー数" - -#: mod_configure.erl:1315 -msgid "Number of online users" -msgstr "オンラインユーザー数" - -#: mod_configure.erl:1682 web/ejabberd_web_admin.erl:1588 -#: web/ejabberd_web_admin.erl:1743 -msgid "Never" -msgstr "なし" - -#: mod_configure.erl:1696 web/ejabberd_web_admin.erl:1601 -#: web/ejabberd_web_admin.erl:1756 -msgid "Online" -msgstr "オンライン" - -#: mod_configure.erl:1701 -msgid "Last login" -msgstr "最終ログイン" - -#: mod_configure.erl:1722 -msgid "Roster size" -msgstr "名簿サイズ" - -#: mod_configure.erl:1723 -msgid "IP addresses" -msgstr "IP アドレス" - -#: mod_configure.erl:1724 -msgid "Resources" -msgstr "リソース" - -#: mod_configure.erl:1850 -msgid "Administration of " -msgstr "管理: " - -#: mod_configure.erl:1853 -msgid "Action on user" -msgstr "ユーザー操作" - -#: mod_configure.erl:1857 -msgid "Edit Properties" -msgstr "プロパティを編集" - -#: mod_configure.erl:1860 web/ejabberd_web_admin.erl:1769 -msgid "Remove User" -msgstr "ユーザーを削除" - -#: mod_irc/mod_irc.erl:201 mod_irc/mod_irc_odbc.erl:196 -#: mod_muc/mod_muc.erl:336 mod_muc/mod_muc_odbc.erl:342 -msgid "Access denied by service policy" -msgstr "サービスポリシーによってアクセスが禁止されました" - -#: mod_irc/mod_irc.erl:401 mod_irc/mod_irc_odbc.erl:398 -msgid "IRC Transport" -msgstr "IRC トランスポート" - -#: mod_irc/mod_irc.erl:428 mod_irc/mod_irc_odbc.erl:425 -msgid "ejabberd IRC module" -msgstr "ejabberd IRC module" - -#: mod_irc/mod_irc.erl:559 mod_irc/mod_irc_odbc.erl:568 -msgid "You need an x:data capable client to configure mod_irc settings" -msgstr "mod_irc の設定には x:data をサポートするクライアントが必要です" - -#: mod_irc/mod_irc.erl:566 mod_irc/mod_irc_odbc.erl:575 -msgid "Registration in mod_irc for " -msgstr "mod_irc での登録: " - -#: mod_irc/mod_irc.erl:571 mod_irc/mod_irc_odbc.erl:580 -msgid "" -"Enter username, encodings, ports and passwords you wish to use for " -"connecting to IRC servers" -msgstr "" -"IRC サーバーに接続先するために使用するユーザー名、文字エンコーディング、ポー" -"ト、パスワードを入力してください" - -#: mod_irc/mod_irc.erl:576 mod_irc/mod_irc_odbc.erl:585 -msgid "IRC Username" -msgstr "IRC ユーザー名" - -#: mod_irc/mod_irc.erl:586 mod_irc/mod_irc_odbc.erl:595 -msgid "" -"If you want to specify different ports, passwords, encodings for IRC " -"servers, fill this list with values in format '{\"irc server\", \"encoding" -"\", port, \"password\"}'. By default this service use \"~s\" encoding, port " -"~p, empty password." -msgstr "" -"別のポートやパスワード、文字エンコーディングを使用したい場合、'{\"irc server" -"\", \"encoding\", port, \"password\"}' という形式のリストを入力してください。" -"デフォルトでエンコーディングは \"~s\" を使用し、ポートは ~p、パスワードは空に" -"なっています。" - -#: mod_irc/mod_irc.erl:598 mod_irc/mod_irc_odbc.erl:607 -msgid "" -"Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef." -"net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]." -msgstr "" -"例: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef.net" -"\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]." - -#: mod_irc/mod_irc.erl:603 mod_irc/mod_irc_odbc.erl:612 -msgid "Connections parameters" -msgstr "接続パラメーター" - -#: mod_irc/mod_irc.erl:728 mod_irc/mod_irc_odbc.erl:757 -msgid "Join IRC channel" -msgstr "IRC チャンネルに参加" - -#: mod_irc/mod_irc.erl:732 mod_irc/mod_irc_odbc.erl:761 -msgid "IRC channel (don't put the first #)" -msgstr "IRC チャンネル (先頭に#は不要)" - -#: mod_irc/mod_irc.erl:737 mod_irc/mod_irc_odbc.erl:766 -msgid "IRC server" -msgstr "IRC サーバー" - -#: mod_irc/mod_irc.erl:770 mod_irc/mod_irc.erl:774 -#: mod_irc/mod_irc_odbc.erl:799 mod_irc/mod_irc_odbc.erl:803 -msgid "Join the IRC channel here." -msgstr "この IRC チャンネルに参加します。" - -#: mod_irc/mod_irc.erl:778 mod_irc/mod_irc_odbc.erl:807 -msgid "Join the IRC channel in this Jabber ID: ~s" -msgstr "Jabber ID: ~s でこの IRC チャンネルに参加" - -#: mod_irc/mod_irc.erl:863 mod_irc/mod_irc_odbc.erl:904 -msgid "IRC settings" -msgstr "IRC 設定" - -#: mod_irc/mod_irc.erl:868 mod_irc/mod_irc_odbc.erl:909 -msgid "" -"Enter username and encodings you wish to use for connecting to IRC servers. " -"Press 'Next' to get more fields to fill in. Press 'Complete' to save " -"settings." -msgstr "" -"IRC サーバーに接続先するためのユーザー名と文字エンコーディングを入力してくだ" -"さい。'Next' を押して次の項目に進みます。'Complete' を押すと設定が保存されま" -"す。" - -#: mod_irc/mod_irc.erl:874 mod_irc/mod_irc_odbc.erl:915 -msgid "IRC username" -msgstr "IRC ユーザー名" - -#: mod_irc/mod_irc.erl:923 mod_irc/mod_irc_odbc.erl:964 -msgid "Password ~b" -msgstr "パスワード ~b" - -#: mod_irc/mod_irc.erl:928 mod_irc/mod_irc_odbc.erl:969 -msgid "Port ~b" -msgstr "ポート ~b" - -#: mod_irc/mod_irc.erl:933 mod_irc/mod_irc_odbc.erl:974 -msgid "Encoding for server ~b" -msgstr "サーバーのエンコーディング ~b" - -#: mod_irc/mod_irc.erl:942 mod_irc/mod_irc_odbc.erl:983 -msgid "Server ~b" -msgstr "サーバー ~b" - -#: mod_muc/mod_muc.erl:449 mod_muc/mod_muc_odbc.erl:456 -msgid "Only service administrators are allowed to send service messages" -msgstr "サービス管理者のみがサービスメッセージを送信できます" - -#: mod_muc/mod_muc.erl:493 mod_muc/mod_muc_odbc.erl:500 -msgid "Room creation is denied by service policy" -msgstr "サービスポリシーによって談話室の作成が禁止されています" - -#: mod_muc/mod_muc.erl:500 mod_muc/mod_muc_odbc.erl:507 -msgid "Conference room does not exist" -msgstr "会議室は存在しません" - -#: mod_muc/mod_muc.erl:582 mod_muc/mod_muc_odbc.erl:595 -msgid "Chatrooms" -msgstr "談話室" - -#: mod_muc/mod_muc.erl:712 mod_muc/mod_muc_odbc.erl:729 -msgid "You need a client that supports x:data to register the nickname" -msgstr "ニックネームを登録するには x:data をサポートするクライアントが必要です" - -#: mod_muc/mod_muc.erl:718 mod_muc/mod_muc_odbc.erl:735 -msgid "Nickname Registration at " -msgstr "ニックネーム登録: " - -#: mod_muc/mod_muc.erl:722 mod_muc/mod_muc_odbc.erl:739 -msgid "Enter nickname you want to register" -msgstr "登録するニックネームを入力してください" - -#: mod_muc/mod_muc.erl:723 mod_muc/mod_muc_odbc.erl:740 -#: mod_muc/mod_muc_room.erl:3810 mod_roster.erl:944 mod_roster_odbc.erl:1061 -#: mod_vcard.erl:364 mod_vcard.erl:477 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:453 -msgid "Nickname" -msgstr "ニックネーム" - -#: mod_muc/mod_muc.erl:762 mod_muc/mod_muc_odbc.erl:784 -#: mod_muc/mod_muc_room.erl:1064 mod_muc/mod_muc_room.erl:1787 -msgid "That nickname is registered by another person" -msgstr "ニックネームはほかの人によって登録されています" - -#: mod_muc/mod_muc.erl:788 mod_muc/mod_muc_odbc.erl:811 -msgid "You must fill in field \"Nickname\" in the form" -msgstr "フォームの\"ニックネーム\"欄を入力する必要があります" - -#: mod_muc/mod_muc.erl:808 mod_muc/mod_muc_odbc.erl:831 -msgid "ejabberd MUC module" -msgstr "ejabberd MUC module" - -#: mod_muc/mod_muc_log.erl:374 mod_muc/mod_muc_log.erl:381 -msgid "Chatroom configuration modified" -msgstr "談話室の設定が変更されました" - -#: mod_muc/mod_muc_log.erl:384 -msgid "joins the room" -msgstr "が談話室に参加しました" - -#: mod_muc/mod_muc_log.erl:387 mod_muc/mod_muc_log.erl:390 -msgid "leaves the room" -msgstr "が談話室から退出しました" - -#: mod_muc/mod_muc_log.erl:393 mod_muc/mod_muc_log.erl:396 -msgid "has been banned" -msgstr "はバンされました" - -#: mod_muc/mod_muc_log.erl:399 mod_muc/mod_muc_log.erl:402 -msgid "has been kicked" -msgstr "はキックされました" - -#: mod_muc/mod_muc_log.erl:405 -msgid "has been kicked because of an affiliation change" -msgstr "は分掌が変更されたためキックされました" - -#: mod_muc/mod_muc_log.erl:408 -msgid "has been kicked because the room has been changed to members-only" -msgstr "は談話室がメンバー制に変更されたためキックされました" - -#: mod_muc/mod_muc_log.erl:411 -msgid "has been kicked because of a system shutdown" -msgstr "はシステムシャットダウンのためキックされました" - -#: mod_muc/mod_muc_log.erl:414 -msgid "is now known as" -msgstr "は名前を変更しました: " - -#: mod_muc/mod_muc_log.erl:417 mod_muc/mod_muc_log.erl:688 -#: mod_muc/mod_muc_room.erl:2393 -msgid " has set the subject to: " -msgstr " は題を設定しました: " - -#: mod_muc/mod_muc_log.erl:452 -msgid "Chatroom is created" -msgstr "談話室を作りました" - -#: mod_muc/mod_muc_log.erl:453 -msgid "Chatroom is destroyed" -msgstr "談話室を削除しました" - -#: mod_muc/mod_muc_log.erl:454 -msgid "Chatroom is started" -msgstr "談話室を開始しました" - -#: mod_muc/mod_muc_log.erl:455 -msgid "Chatroom is stopped" -msgstr "談話室を停止しました" - -#: mod_muc/mod_muc_log.erl:459 -msgid "Monday" -msgstr "月曜日" - -#: mod_muc/mod_muc_log.erl:460 -msgid "Tuesday" -msgstr "火曜日" - -#: mod_muc/mod_muc_log.erl:461 -msgid "Wednesday" -msgstr "水曜日" - -#: mod_muc/mod_muc_log.erl:462 -msgid "Thursday" -msgstr "木曜日" - -#: mod_muc/mod_muc_log.erl:463 -msgid "Friday" -msgstr "金曜日" - -#: mod_muc/mod_muc_log.erl:464 -msgid "Saturday" -msgstr "土曜日" - -#: mod_muc/mod_muc_log.erl:465 -msgid "Sunday" -msgstr "日曜日" - -#: mod_muc/mod_muc_log.erl:469 -msgid "January" -msgstr "1月" - -#: mod_muc/mod_muc_log.erl:470 -msgid "February" -msgstr "2月" - -#: mod_muc/mod_muc_log.erl:471 -msgid "March" -msgstr "3月" - -#: mod_muc/mod_muc_log.erl:472 -msgid "April" -msgstr "4月" - -#: mod_muc/mod_muc_log.erl:473 -msgid "May" -msgstr "5月" - -#: mod_muc/mod_muc_log.erl:474 -msgid "June" -msgstr "6月" - -#: mod_muc/mod_muc_log.erl:475 -msgid "July" -msgstr "7月" - -#: mod_muc/mod_muc_log.erl:476 -msgid "August" -msgstr "8月" - -#: mod_muc/mod_muc_log.erl:477 -msgid "September" -msgstr "9月" - -#: mod_muc/mod_muc_log.erl:478 -msgid "October" -msgstr "10月" - -#: mod_muc/mod_muc_log.erl:479 -msgid "November" -msgstr "11月" - -#: mod_muc/mod_muc_log.erl:480 -msgid "December" -msgstr "12月" - -#: mod_muc/mod_muc_log.erl:750 -msgid "Room Configuration" -msgstr "談話室の設定" - -#: mod_muc/mod_muc_log.erl:759 -msgid "Room Occupants" -msgstr "在室者" - -#: mod_muc/mod_muc_room.erl:174 -msgid "Traffic rate limit is exceeded" -msgstr "トラフィックレートの制限を超えました" - -#: mod_muc/mod_muc_room.erl:246 -msgid "" -"This participant is kicked from the room because he sent an error message" -msgstr "エラーメッセージを送信したため、この参加者はキックされました" - -#: mod_muc/mod_muc_room.erl:255 -msgid "It is not allowed to send private messages to the conference" -msgstr "この会議にプライベートメッセージを送信することはできません" - -#: mod_muc/mod_muc_room.erl:332 -msgid "Please, wait for a while before sending new voice request" -msgstr "新しい発言権の要求を送るまで少し間をおいてください" - -#: mod_muc/mod_muc_room.erl:347 -msgid "Voice requests are disabled in this conference" -msgstr "この会議では、発言権の要求はできません" - -#: mod_muc/mod_muc_room.erl:364 -msgid "Failed to extract JID from your voice request approval" -msgstr "発言権要求の承認から JID を取り出すことに失敗しました" - -#: mod_muc/mod_muc_room.erl:393 -msgid "Only moderators can approve voice requests" -msgstr "モデレーターだけが発言権の要求を承認できます" - -#: mod_muc/mod_muc_room.erl:408 -msgid "Improper message type" -msgstr "誤ったメッセージタイプです" - -#: mod_muc/mod_muc_room.erl:518 -msgid "" -"This participant is kicked from the room because he sent an error message to " -"another participant" -msgstr "" -"他の参加者にエラーメッセージを送信したため、この参加者はキックされました" - -#: mod_muc/mod_muc_room.erl:531 -msgid "It is not allowed to send private messages of type \"groupchat\"" -msgstr "" -"種別が\"groupchat\" であるプライベートメッセージを送信することはできません" - -#: mod_muc/mod_muc_room.erl:543 mod_muc/mod_muc_room.erl:611 -msgid "Recipient is not in the conference room" -msgstr "受信者はこの会議室にいません" - -#: mod_muc/mod_muc_room.erl:564 mod_muc/mod_muc_room.erl:585 -msgid "It is not allowed to send private messages" -msgstr "プライベートメッセージを送信することはできません" - -#: mod_muc/mod_muc_room.erl:576 mod_muc/mod_muc_room.erl:956 -#: mod_muc/mod_muc_room.erl:4040 -msgid "Only occupants are allowed to send messages to the conference" -msgstr "在室者のみがこの会議にメッセージを送ることができます" - -#: mod_muc/mod_muc_room.erl:634 -msgid "Only occupants are allowed to send queries to the conference" -msgstr "在室者のみが会議にクエリーを送信することができます" - -#: mod_muc/mod_muc_room.erl:646 -msgid "Queries to the conference members are not allowed in this room" -msgstr "この談話室では、会議のメンバーへのクエリーは禁止されています" - -#: mod_muc/mod_muc_room.erl:932 -msgid "" -"Only moderators and participants are allowed to change the subject in this " -"room" -msgstr "モデレーターと参加者のみが談話室の題を変更できます" - -#: mod_muc/mod_muc_room.erl:937 -msgid "Only moderators are allowed to change the subject in this room" -msgstr "モデレーターのみが談話室の題を変更できます" - -#: mod_muc/mod_muc_room.erl:947 -msgid "Visitors are not allowed to send messages to all occupants" -msgstr "傍聴者はすべての在室者にメッセージを送信することはできません" - -#: mod_muc/mod_muc_room.erl:1021 -msgid "" -"This participant is kicked from the room because he sent an error presence" -msgstr "エラープレゼンスを送信したため、この参加者はキックされました" - -#: mod_muc/mod_muc_room.erl:1040 -msgid "Visitors are not allowed to change their nicknames in this room" -msgstr "傍聴者はこの談話室でニックネームを変更することはできません" - -#: mod_muc/mod_muc_room.erl:1053 mod_muc/mod_muc_room.erl:1779 -msgid "That nickname is already in use by another occupant" -msgstr "そのニックネームは既にほかの在室者によって使用されています" - -#: mod_muc/mod_muc_room.erl:1768 -msgid "You have been banned from this room" -msgstr "あなたはこの談話室からバンされています" - -#: mod_muc/mod_muc_room.erl:1771 -msgid "Membership is required to enter this room" -msgstr "この談話室に入るにはメンバーでなければなりません" - -#: mod_muc/mod_muc_room.erl:1807 -msgid "This room is not anonymous" -msgstr "この談話室は非匿名です" - -#: mod_muc/mod_muc_room.erl:1833 -msgid "A password is required to enter this room" -msgstr "この談話室に入るにはパスワードが必要です" - -#: mod_muc/mod_muc_room.erl:1855 mod_register.erl:246 -msgid "Too many CAPTCHA requests" -msgstr "CAPTCHA 要求が多すぎます" - -#: mod_muc/mod_muc_room.erl:1864 mod_register.erl:251 -msgid "Unable to generate a CAPTCHA" -msgstr "CAPTCHA を生成できません" - -#: mod_muc/mod_muc_room.erl:1874 -msgid "Incorrect password" -msgstr "パスワードが違います" - -#: mod_muc/mod_muc_room.erl:2448 -msgid "Administrator privileges required" -msgstr "管理者権限が必要です" - -#: mod_muc/mod_muc_room.erl:2463 -msgid "Moderator privileges required" -msgstr "モデレーター権限が必要です" - -#: mod_muc/mod_muc_room.erl:2619 -msgid "Jabber ID ~s is invalid" -msgstr "Jabber ID ~s は無効です" - -#: mod_muc/mod_muc_room.erl:2633 -msgid "Nickname ~s does not exist in the room" -msgstr "ニックネーム ~s はこの談話室にいません" - -#: mod_muc/mod_muc_room.erl:2659 mod_muc/mod_muc_room.erl:3049 -msgid "Invalid affiliation: ~s" -msgstr "無効な分掌です: ~s" - -#: mod_muc/mod_muc_room.erl:2713 -msgid "Invalid role: ~s" -msgstr "無効な役です: ~s" - -#: mod_muc/mod_muc_room.erl:3026 mod_muc/mod_muc_room.erl:3062 -msgid "Owner privileges required" -msgstr "主宰者の権限が必要です" - -#: mod_muc/mod_muc_room.erl:3195 -msgid "Configuration of room ~s" -msgstr "談話室 ~s の設定" - -#: mod_muc/mod_muc_room.erl:3200 -msgid "Room title" -msgstr "談話室のタイトル" - -#: mod_muc/mod_muc_room.erl:3203 mod_muc/mod_muc_room.erl:3692 -msgid "Room description" -msgstr "談話室の説明" - -#: mod_muc/mod_muc_room.erl:3210 -msgid "Make room persistent" -msgstr "談話室を永続化" - -#: mod_muc/mod_muc_room.erl:3215 -msgid "Make room public searchable" -msgstr "談話室を検索可" - -#: mod_muc/mod_muc_room.erl:3218 -msgid "Make participants list public" -msgstr "参加者一覧を公開" - -#: mod_muc/mod_muc_room.erl:3221 -msgid "Make room password protected" -msgstr "談話室をパスワードで保護" - -#: mod_muc/mod_muc_room.erl:3232 -msgid "Maximum Number of Occupants" -msgstr "最大在室者数" - -#: mod_muc/mod_muc_room.erl:3239 -msgid "No limit" -msgstr "制限なし" - -#: mod_muc/mod_muc_room.erl:3250 -msgid "Present real Jabber IDs to" -msgstr "本当の Jabber ID を公開" - -#: mod_muc/mod_muc_room.erl:3258 mod_muc/mod_muc_room.erl:3292 -msgid "moderators only" -msgstr "モデレーターにのみ" - -#: mod_muc/mod_muc_room.erl:3260 mod_muc/mod_muc_room.erl:3294 -msgid "anyone" -msgstr "誰にでも" - -#: mod_muc/mod_muc_room.erl:3262 -msgid "Make room members-only" -msgstr "談話室をメンバーのみに制限" - -#: mod_muc/mod_muc_room.erl:3265 -msgid "Make room moderated" -msgstr "談話室をモデレート化" - -#: mod_muc/mod_muc_room.erl:3268 -msgid "Default users as participants" -msgstr "デフォルトのユーザーは参加者" - -#: mod_muc/mod_muc_room.erl:3271 -msgid "Allow users to change the subject" -msgstr "ユーザーによる題の変更を許可" - -#: mod_muc/mod_muc_room.erl:3274 -msgid "Allow users to send private messages" -msgstr "ユーザーによるプライベートメッセージの送信を許可" - -#: mod_muc/mod_muc_room.erl:3279 -msgid "Allow visitors to send private messages to" -msgstr "傍聴者によるプライベートメッセージの送信を次の相手に許可" - -#: mod_muc/mod_muc_room.erl:3290 -msgid "nobody" -msgstr "誰にも許可しない" - -#: mod_muc/mod_muc_room.erl:3296 -msgid "Allow users to query other users" -msgstr "ユーザーによる他のユーザーへのクエリーを許可" - -#: mod_muc/mod_muc_room.erl:3299 -msgid "Allow users to send invites" -msgstr "ユーザーによる招待を許可" - -#: mod_muc/mod_muc_room.erl:3302 -msgid "Allow visitors to send status text in presence updates" -msgstr "傍聴者によるプレゼンス更新のステータス文の送信を許可" - -#: mod_muc/mod_muc_room.erl:3305 -msgid "Allow visitors to change nickname" -msgstr "傍聴者のニックネームの変更を許可" - -#: mod_muc/mod_muc_room.erl:3308 -msgid "Allow visitors to send voice requests" -msgstr "傍聴者による発言権の要求を許可" - -#: mod_muc/mod_muc_room.erl:3311 -msgid "Minimum interval between voice requests (in seconds)" -msgstr "発言権の要求の最小時間間隔 (秒)" - -#: mod_muc/mod_muc_room.erl:3317 -msgid "Make room CAPTCHA protected" -msgstr "談話室を CAPTCHA で保護" - -#: mod_muc/mod_muc_room.erl:3322 -msgid "Exclude Jabber IDs from CAPTCHA challenge" -msgstr "CAPTCHA 試験を免除する Jabber ID" - -#: mod_muc/mod_muc_room.erl:3329 -msgid "Enable logging" -msgstr "ロギングを有効" - -#: mod_muc/mod_muc_room.erl:3337 -msgid "You need an x:data capable client to configure room" -msgstr "談話室を設定するには x:data をサポートするクライアントが必要です" - -#: mod_muc/mod_muc_room.erl:3694 -msgid "Number of occupants" -msgstr "在室者の数" - -#: mod_muc/mod_muc_room.erl:3750 -msgid "private, " -msgstr "プライベート、" - -#: mod_muc/mod_muc_room.erl:3799 -msgid "Voice request" -msgstr "発言権を要求" - -#: mod_muc/mod_muc_room.erl:3803 -msgid "Either approve or decline the voice request." -msgstr "発言権の要求を承認または却下します。" - -#: mod_muc/mod_muc_room.erl:3809 -msgid "User JID" -msgstr "ユーザー JID" - -#: mod_muc/mod_muc_room.erl:3811 -msgid "Grant voice to this person?" -msgstr "この人に発言権を与えますか ?" - -#: mod_muc/mod_muc_room.erl:3960 -msgid "~s invites you to the room ~s" -msgstr "~s はあなたを談話室 ~s に招待しています" - -#: mod_muc/mod_muc_room.erl:3969 -msgid "the password is" -msgstr "パスワードは" - -#: mod_offline.erl:510 mod_offline_odbc.erl:352 -msgid "" -"Your contact offline message queue is full. The message has been discarded." -msgstr "" -"相手先のオフラインメッセージキューが一杯です。このメッセージは破棄されます。" - -#: mod_offline.erl:560 mod_offline_odbc.erl:408 -msgid "~s's Offline Messages Queue" -msgstr "~s' のオフラインメッセージキュー" - -#: mod_offline.erl:563 mod_offline_odbc.erl:411 mod_roster.erl:987 -#: mod_roster_odbc.erl:1104 mod_shared_roster.erl:901 -#: mod_shared_roster.erl:1010 mod_shared_roster_odbc.erl:930 -#: mod_shared_roster_odbc.erl:1039 web/ejabberd_web_admin.erl:833 -#: web/ejabberd_web_admin.erl:874 web/ejabberd_web_admin.erl:942 -#: web/ejabberd_web_admin.erl:978 web/ejabberd_web_admin.erl:1019 -#: web/ejabberd_web_admin.erl:1508 web/ejabberd_web_admin.erl:1760 -#: web/ejabberd_web_admin.erl:1931 web/ejabberd_web_admin.erl:1963 -#: web/ejabberd_web_admin.erl:2031 web/ejabberd_web_admin.erl:2135 -#: web/ejabberd_web_admin.erl:2160 web/ejabberd_web_admin.erl:2248 -msgid "Submitted" -msgstr "送信完了" - -#: mod_offline.erl:571 -msgid "Time" -msgstr "時間" - -#: mod_offline.erl:572 -msgid "From" -msgstr "差出人" - -#: mod_offline.erl:573 -msgid "To" -msgstr "宛先" - -#: mod_offline.erl:574 mod_offline_odbc.erl:419 -msgid "Packet" -msgstr "パケット" - -#: mod_offline.erl:587 mod_offline_odbc.erl:432 mod_shared_roster.erl:908 -#: mod_shared_roster_odbc.erl:937 web/ejabberd_web_admin.erl:882 -#: web/ejabberd_web_admin.erl:986 -msgid "Delete Selected" -msgstr "選択した項目を削除" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -msgid "Offline Messages:" -msgstr "オフラインメッセージ:" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -msgid "Remove All Offline Messages" -msgstr "すべてのオフラインメッセージを削除" - -#: mod_proxy65/mod_proxy65_service.erl:213 -msgid "ejabberd SOCKS5 Bytestreams module" -msgstr "ejabberd SOCKS5 Bytestreams モジュール" - -#: mod_pubsub/mod_pubsub.erl:1120 mod_pubsub/mod_pubsub_odbc.erl:929 -msgid "Publish-Subscribe" -msgstr "Publish-Subscribe" - -#: mod_pubsub/mod_pubsub.erl:1214 mod_pubsub/mod_pubsub_odbc.erl:1025 -msgid "ejabberd Publish-Subscribe module" -msgstr "ejabberd Publish-Subscribe モジュール" - -#: mod_pubsub/mod_pubsub.erl:1497 mod_pubsub/mod_pubsub_odbc.erl:1312 -msgid "PubSub subscriber request" -msgstr "PubSub 購読者のリクエスト" - -#: mod_pubsub/mod_pubsub.erl:1499 mod_pubsub/mod_pubsub_odbc.erl:1314 -msgid "Choose whether to approve this entity's subscription." -msgstr "このエントリを承認するかどうかを選択してください" - -#: mod_pubsub/mod_pubsub.erl:1505 mod_pubsub/mod_pubsub_odbc.erl:1320 -msgid "Node ID" -msgstr "ノードID" - -#: mod_pubsub/mod_pubsub.erl:1510 mod_pubsub/mod_pubsub_odbc.erl:1325 -msgid "Subscriber Address" -msgstr "購読者のアドレス" - -#: mod_pubsub/mod_pubsub.erl:1516 mod_pubsub/mod_pubsub_odbc.erl:1331 -msgid "Allow this Jabber ID to subscribe to this pubsub node?" -msgstr "この Jabber ID に、この pubsubノードの購読を許可しますか ?" - -#: mod_pubsub/mod_pubsub.erl:3474 mod_pubsub/mod_pubsub_odbc.erl:3287 -msgid "Deliver payloads with event notifications" -msgstr "イベント通知と同時にペイロードを配送する" - -#: mod_pubsub/mod_pubsub.erl:3475 mod_pubsub/mod_pubsub_odbc.erl:3288 -msgid "Deliver event notifications" -msgstr "イベント通知を配送する" - -#: mod_pubsub/mod_pubsub.erl:3476 mod_pubsub/mod_pubsub_odbc.erl:3289 -msgid "Notify subscribers when the node configuration changes" -msgstr "ノード設定に変更があった時に購読者へ通知する" - -#: mod_pubsub/mod_pubsub.erl:3477 mod_pubsub/mod_pubsub_odbc.erl:3290 -msgid "Notify subscribers when the node is deleted" -msgstr "ノードが削除された時に購読者へ通知する" - -#: mod_pubsub/mod_pubsub.erl:3478 mod_pubsub/mod_pubsub_odbc.erl:3291 -msgid "Notify subscribers when items are removed from the node" -msgstr "アイテムがノードから消された時に購読者へ通知する" - -#: mod_pubsub/mod_pubsub.erl:3479 mod_pubsub/mod_pubsub_odbc.erl:3292 -msgid "Persist items to storage" -msgstr "アイテムをストレージに保存する" - -#: mod_pubsub/mod_pubsub.erl:3480 mod_pubsub/mod_pubsub_odbc.erl:3293 -msgid "A friendly name for the node" -msgstr "ノードのフレンドリネーム" - -#: mod_pubsub/mod_pubsub.erl:3481 mod_pubsub/mod_pubsub_odbc.erl:3294 -msgid "Max # of items to persist" -msgstr "アイテムの最大保存数" - -#: mod_pubsub/mod_pubsub.erl:3482 mod_pubsub/mod_pubsub_odbc.erl:3295 -msgid "Whether to allow subscriptions" -msgstr "購読を許可するかどうか" - -#: mod_pubsub/mod_pubsub.erl:3483 mod_pubsub/mod_pubsub_odbc.erl:3296 -msgid "Specify the access model" -msgstr "アクセスモデルを設定する" - -#: mod_pubsub/mod_pubsub.erl:3486 mod_pubsub/mod_pubsub_odbc.erl:3299 -msgid "Roster groups allowed to subscribe" -msgstr "名簿グループは購読を許可しました" - -#: mod_pubsub/mod_pubsub.erl:3487 mod_pubsub/mod_pubsub_odbc.erl:3300 -msgid "Specify the publisher model" -msgstr "公開モデルを指定する" - -#: mod_pubsub/mod_pubsub.erl:3489 mod_pubsub/mod_pubsub_odbc.erl:3302 -msgid "Purge all items when the relevant publisher goes offline" -msgstr "公開者がオフラインになるときに、すべてのアイテムを削除" - -#: mod_pubsub/mod_pubsub.erl:3490 mod_pubsub/mod_pubsub_odbc.erl:3303 -msgid "Specify the event message type" -msgstr "イベントメッセージ種別を設定" - -#: mod_pubsub/mod_pubsub.erl:3492 mod_pubsub/mod_pubsub_odbc.erl:3305 -msgid "Max payload size in bytes" -msgstr "最大ぺイロードサイズ (byte)" - -#: mod_pubsub/mod_pubsub.erl:3493 mod_pubsub/mod_pubsub_odbc.erl:3306 -msgid "When to send the last published item" -msgstr "最後の公開アイテムを送信するタイミングで" - -#: mod_pubsub/mod_pubsub.erl:3495 mod_pubsub/mod_pubsub_odbc.erl:3308 -msgid "Only deliver notifications to available users" -msgstr "有効なユーザーにのみ告知を送信する" - -#: mod_pubsub/mod_pubsub.erl:3496 mod_pubsub/mod_pubsub_odbc.erl:3309 -msgid "The collections with which a node is affiliated" -msgstr "提携されたノードの集合です" - -#: mod_register.erl:193 -msgid "The CAPTCHA verification has failed" -msgstr "CAPTCHA 検証は失敗しました" - -#: mod_register.erl:220 -msgid "You need a client that supports x:data and CAPTCHA to register" -msgstr "登録を行うには x:data と CAPTCHA をサポートするクライアントが必要です" - -#: mod_register.erl:226 mod_register.erl:265 -msgid "Choose a username and password to register with this server" -msgstr "サーバーに登録するユーザー名とパスワードを選択してください" - -#: mod_register.erl:230 mod_vcard.erl:364 mod_vcard_odbc.erl:342 -#: web/ejabberd_web_admin.erl:1515 web/ejabberd_web_admin.erl:1572 -msgid "User" -msgstr "ユーザー" - -#: mod_register.erl:316 mod_register.erl:361 -msgid "The password is too weak" -msgstr "このパスワードは単純過ぎます" - -#: mod_register.erl:365 -msgid "Users are not allowed to register accounts so quickly" -msgstr "それほど速くアカウントを登録することはできません" - -#: mod_roster.erl:938 mod_roster_odbc.erl:1055 web/ejabberd_web_admin.erl:1701 -#: web/ejabberd_web_admin.erl:1886 web/ejabberd_web_admin.erl:1897 -#: web/ejabberd_web_admin.erl:2219 -msgid "None" -msgstr "なし" - -#: mod_roster.erl:945 mod_roster_odbc.erl:1062 -msgid "Subscription" -msgstr "認可" - -#: mod_roster.erl:946 mod_roster_odbc.erl:1063 -msgid "Pending" -msgstr "保留" - -#: mod_roster.erl:947 mod_roster_odbc.erl:1064 -msgid "Groups" -msgstr "グループ" - -#: mod_roster.erl:974 mod_roster_odbc.erl:1091 -msgid "Validate" -msgstr "検証" - -#: mod_roster.erl:982 mod_roster_odbc.erl:1099 -msgid "Remove" -msgstr "削除" - -#: mod_roster.erl:985 mod_roster_odbc.erl:1102 -msgid "Roster of " -msgstr "名簿: " - -#: mod_roster.erl:988 mod_roster_odbc.erl:1105 mod_shared_roster.erl:902 -#: mod_shared_roster.erl:1011 mod_shared_roster_odbc.erl:931 -#: mod_shared_roster_odbc.erl:1040 web/ejabberd_web_admin.erl:834 -#: web/ejabberd_web_admin.erl:875 web/ejabberd_web_admin.erl:943 -#: web/ejabberd_web_admin.erl:979 web/ejabberd_web_admin.erl:1020 -#: web/ejabberd_web_admin.erl:1509 web/ejabberd_web_admin.erl:1761 -#: web/ejabberd_web_admin.erl:1932 web/ejabberd_web_admin.erl:2136 -#: web/ejabberd_web_admin.erl:2161 -msgid "Bad format" -msgstr "不正なフォーマット" - -#: mod_roster.erl:995 mod_roster_odbc.erl:1112 -msgid "Add Jabber ID" -msgstr "Jabber ID を追加" - -#: mod_roster.erl:1094 mod_roster_odbc.erl:1211 -msgid "Roster" -msgstr "名簿" - -#: mod_shared_roster.erl:857 mod_shared_roster.erl:899 -#: mod_shared_roster.erl:1007 mod_shared_roster_odbc.erl:886 -#: mod_shared_roster_odbc.erl:928 mod_shared_roster_odbc.erl:1036 -msgid "Shared Roster Groups" -msgstr "共有名簿グループ" - -#: mod_shared_roster.erl:895 mod_shared_roster_odbc.erl:924 -#: web/ejabberd_web_admin.erl:1365 web/ejabberd_web_admin.erl:2461 -msgid "Add New" -msgstr "新規追加" - -#: mod_shared_roster.erl:978 mod_shared_roster_odbc.erl:1007 -msgid "Name:" -msgstr "名前:" - -#: mod_shared_roster.erl:983 mod_shared_roster_odbc.erl:1012 -msgid "Description:" -msgstr "説明:" - -#: mod_shared_roster.erl:991 mod_shared_roster_odbc.erl:1020 -msgid "Members:" -msgstr "メンバー:" - -#: mod_shared_roster.erl:999 mod_shared_roster_odbc.erl:1028 -msgid "Displayed Groups:" -msgstr "表示グループ" - -#: mod_shared_roster.erl:1008 mod_shared_roster_odbc.erl:1037 -msgid "Group " -msgstr "グループ" - -#: mod_shared_roster.erl:1017 mod_shared_roster_odbc.erl:1046 -#: web/ejabberd_web_admin.erl:840 web/ejabberd_web_admin.erl:884 -#: web/ejabberd_web_admin.erl:949 web/ejabberd_web_admin.erl:1026 -#: web/ejabberd_web_admin.erl:2017 -msgid "Submit" -msgstr "送信" - -#: mod_vcard.erl:165 mod_vcard_ldap.erl:238 mod_vcard_odbc.erl:129 -msgid "Erlang Jabber Server" -msgstr "Erlang Jabber Server" - -#: mod_vcard.erl:364 mod_vcard.erl:478 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:454 -msgid "Birthday" -msgstr "誕生日" - -#: mod_vcard.erl:364 mod_vcard.erl:480 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:456 -msgid "City" -msgstr "都道府県" - -#: mod_vcard.erl:364 mod_vcard.erl:479 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:455 -msgid "Country" -msgstr "国" - -#: mod_vcard.erl:364 mod_vcard.erl:481 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:457 -msgid "Email" -msgstr "メールアドレス" - -#: mod_vcard.erl:364 mod_vcard.erl:476 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:452 -msgid "Family Name" -msgstr "姓" - -#: mod_vcard.erl:364 mod_vcard_odbc.erl:342 -msgid "" -"Fill in the form to search for any matching Jabber User (Add * to the end of " -"field to match substring)" -msgstr "" -"欄を埋めて Jabber User を検索してください (* を使用すると部分文字列にマッチし" -"ます)" - -#: mod_vcard.erl:364 mod_vcard.erl:473 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:449 -msgid "Full Name" -msgstr "氏名" - -#: mod_vcard.erl:364 mod_vcard.erl:475 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:451 -msgid "Middle Name" -msgstr "ミドルネーム" - -#: mod_vcard.erl:364 mod_vcard.erl:474 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:450 web/ejabberd_web_admin.erl:2006 -msgid "Name" -msgstr "名" - -#: mod_vcard.erl:364 mod_vcard.erl:482 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:458 -msgid "Organization Name" -msgstr "会社名" - -#: mod_vcard.erl:364 mod_vcard.erl:483 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:459 -msgid "Organization Unit" -msgstr "部署名" - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "Search users in " -msgstr "ユーザーの検索: " - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "You need an x:data capable client to search" -msgstr "検索を行うためには x:data をサポートするクライアントが必要です" - -#: mod_vcard.erl:389 mod_vcard_ldap.erl:487 mod_vcard_odbc.erl:367 -msgid "vCard User Search" -msgstr "vCard ユーザー検索" - -#: mod_vcard.erl:445 mod_vcard_ldap.erl:541 mod_vcard_odbc.erl:421 -msgid "ejabberd vCard module" -msgstr "ejabberd vCard モジュール" - -#: mod_vcard.erl:469 mod_vcard_ldap.erl:551 mod_vcard_odbc.erl:445 -msgid "Search Results for " -msgstr "検索結果: " - -#: mod_vcard_ldap.erl:462 -msgid "Fill in fields to search for any matching Jabber User" -msgstr "欄を埋めて Jabber User を検索してください" - -#: web/ejabberd_web_admin.erl:193 web/ejabberd_web_admin.erl:203 -#: web/ejabberd_web_admin.erl:219 web/ejabberd_web_admin.erl:229 -msgid "Unauthorized" -msgstr "認証されていません" - -#: web/ejabberd_web_admin.erl:286 web/ejabberd_web_admin.erl:303 -msgid "ejabberd Web Admin" -msgstr "ejabberd ウェブ管理" - -#: web/ejabberd_web_admin.erl:769 web/ejabberd_web_admin.erl:780 -msgid "Administration" -msgstr "管理" - -#: web/ejabberd_web_admin.erl:878 web/ejabberd_web_admin.erl:982 -msgid "Raw" -msgstr "Raw" - -#: web/ejabberd_web_admin.erl:1017 -msgid "~s access rule configuration" -msgstr "~s アクセスルール設定" - -#: web/ejabberd_web_admin.erl:1035 -msgid "Virtual Hosts" -msgstr "ヴァーチャルホスト" - -#: web/ejabberd_web_admin.erl:1043 web/ejabberd_web_admin.erl:1050 -msgid "Users" -msgstr "ユーザー" - -#: web/ejabberd_web_admin.erl:1078 -msgid "Users Last Activity" -msgstr "ユーザーの活動履歴" - -#: web/ejabberd_web_admin.erl:1080 -msgid "Period: " -msgstr "期間: " - -#: web/ejabberd_web_admin.erl:1090 -msgid "Last month" -msgstr "先月" - -#: web/ejabberd_web_admin.erl:1091 -msgid "Last year" -msgstr "去年" - -#: web/ejabberd_web_admin.erl:1092 -msgid "All activity" -msgstr "すべて" - -#: web/ejabberd_web_admin.erl:1094 -msgid "Show Ordinary Table" -msgstr "通常の表を表示" - -#: web/ejabberd_web_admin.erl:1096 -msgid "Show Integral Table" -msgstr "累積の表を表示" - -#: web/ejabberd_web_admin.erl:1105 web/ejabberd_web_admin.erl:1939 -msgid "Statistics" -msgstr "統計" - -#: web/ejabberd_web_admin.erl:1117 -msgid "Not Found" -msgstr "見つかりません" - -#: web/ejabberd_web_admin.erl:1134 -msgid "Node not found" -msgstr "ノードが見つかりません" - -#: web/ejabberd_web_admin.erl:1460 -msgid "Host" -msgstr "ホスト" - -#: web/ejabberd_web_admin.erl:1461 -msgid "Registered Users" -msgstr "登録ユーザー" - -#: web/ejabberd_web_admin.erl:1573 -msgid "Offline Messages" -msgstr "オフラインメッセージ" - -#: web/ejabberd_web_admin.erl:1574 web/ejabberd_web_admin.erl:1767 -msgid "Last Activity" -msgstr "活動履歴" - -#: web/ejabberd_web_admin.erl:1659 web/ejabberd_web_admin.erl:1675 -msgid "Registered Users:" -msgstr "登録ユーザー:" - -#: web/ejabberd_web_admin.erl:1661 web/ejabberd_web_admin.erl:1677 -#: web/ejabberd_web_admin.erl:2192 -msgid "Online Users:" -msgstr "オンラインユーザー:" - -#: web/ejabberd_web_admin.erl:1663 -msgid "Outgoing s2s Connections:" -msgstr "外向き s2s コネクション:" - -#: web/ejabberd_web_admin.erl:1665 -msgid "Outgoing s2s Servers:" -msgstr "外向き s2s サービス:" - -#: web/ejabberd_web_admin.erl:1734 web/mod_register_web.erl:175 -#: web/mod_register_web.erl:372 web/mod_register_web.erl:381 -#: web/mod_register_web.erl:411 -msgid "Change Password" -msgstr "パスワードを変更" - -#: web/ejabberd_web_admin.erl:1758 -msgid "User " -msgstr "ユーザー " - -#: web/ejabberd_web_admin.erl:1765 -msgid "Connected Resources:" -msgstr "接続リソース:" - -#: web/ejabberd_web_admin.erl:1766 web/mod_register_web.erl:227 -#: web/mod_register_web.erl:519 -msgid "Password:" -msgstr "パスワード" - -#: web/ejabberd_web_admin.erl:1828 -msgid "No Data" -msgstr "データなし" - -#: web/ejabberd_web_admin.erl:1906 -msgid "Nodes" -msgstr "ノード" - -#: web/ejabberd_web_admin.erl:1929 web/ejabberd_web_admin.erl:1951 -msgid "Node " -msgstr "ノード " - -#: web/ejabberd_web_admin.erl:1938 -msgid "Listened Ports" -msgstr "Listen ポート" - -#: web/ejabberd_web_admin.erl:1940 web/ejabberd_web_admin.erl:2260 -#: web/ejabberd_web_admin.erl:2448 -msgid "Update" -msgstr "更新" - -#: web/ejabberd_web_admin.erl:1943 web/ejabberd_web_admin.erl:2569 -msgid "Restart" -msgstr "再起動" - -#: web/ejabberd_web_admin.erl:1945 web/ejabberd_web_admin.erl:2571 -msgid "Stop" -msgstr "停止" - -#: web/ejabberd_web_admin.erl:1959 -msgid "RPC Call Error" -msgstr "RPC 呼び出しエラー" - -#: web/ejabberd_web_admin.erl:2000 -msgid "Database Tables at " -msgstr "データーベーステーブル: " - -#: web/ejabberd_web_admin.erl:2007 -msgid "Storage Type" -msgstr "ストレージタイプ" - -#: web/ejabberd_web_admin.erl:2008 -msgid "Elements" -msgstr "要素" - -#: web/ejabberd_web_admin.erl:2009 -msgid "Memory" -msgstr "メモリ" - -#: web/ejabberd_web_admin.erl:2032 web/ejabberd_web_admin.erl:2137 -msgid "Error" -msgstr "エラー" - -#: web/ejabberd_web_admin.erl:2034 -msgid "Backup of " -msgstr "バックアップ: " - -#: web/ejabberd_web_admin.erl:2036 -msgid "" -"Please note that these options will only backup the builtin Mnesia database. " -"If you are using the ODBC module, you also need to backup your SQL database " -"separately." -msgstr "" -"これらのオプションは組み込みの Mnesia データーベースのバックアップのみを行う" -"ことに注意してください。もし ODBC モジュールを使用している場合は、SQL デー" -"ターベースのバックアップを別に行う必要があります。" - -#: web/ejabberd_web_admin.erl:2041 -msgid "Store binary backup:" -msgstr "バイナリバックアップを保存:" - -#: web/ejabberd_web_admin.erl:2045 web/ejabberd_web_admin.erl:2052 -#: web/ejabberd_web_admin.erl:2060 web/ejabberd_web_admin.erl:2067 -#: web/ejabberd_web_admin.erl:2074 web/ejabberd_web_admin.erl:2081 -#: web/ejabberd_web_admin.erl:2088 web/ejabberd_web_admin.erl:2096 -#: web/ejabberd_web_admin.erl:2103 web/ejabberd_web_admin.erl:2110 -msgid "OK" -msgstr "OK" - -#: web/ejabberd_web_admin.erl:2048 -msgid "Restore binary backup immediately:" -msgstr "直ちにバイナリバックアップからリストア:" - -#: web/ejabberd_web_admin.erl:2056 -msgid "" -"Restore binary backup after next ejabberd restart (requires less memory):" -msgstr "ejabberd の再起動時にバイナリバックアップからリストア (メモリ少):" - -#: web/ejabberd_web_admin.erl:2063 -msgid "Store plain text backup:" -msgstr "プレーンテキストバックアップを保存:" - -#: web/ejabberd_web_admin.erl:2070 -msgid "Restore plain text backup immediately:" -msgstr "直ちにプレーンテキストバックアップからリストア:" - -#: web/ejabberd_web_admin.erl:2077 -msgid "Import users data from a PIEFXIS file (XEP-0227):" -msgstr "ユーザーデータを PIEFXIS ファイルからインポート (XEP-0227):" - -#: web/ejabberd_web_admin.erl:2084 -msgid "Export data of all users in the server to PIEFXIS files (XEP-0227):" -msgstr "" -"サーバーにあるすべてのユーザーデータを PIEFXIS ファイルにエクスポート " -"(XEP-0227):" - -#: web/ejabberd_web_admin.erl:2091 -msgid "Export data of users in a host to PIEFXIS files (XEP-0227):" -msgstr "ホストのユーザーデータを PIEFXIS ファイルにエクスポート (XEP-0227):" - -#: web/ejabberd_web_admin.erl:2099 -msgid "Import user data from jabberd14 spool file:" -msgstr "ユーザーデータを jabberd14 Spool ファイルからインポート:" - -#: web/ejabberd_web_admin.erl:2106 -msgid "Import users data from jabberd14 spool directory:" -msgstr "ユーザーデータを jabberd14 Spool ディレクトリからインポート:" - -#: web/ejabberd_web_admin.erl:2132 -msgid "Listened Ports at " -msgstr "Listen ポート " - -#: web/ejabberd_web_admin.erl:2157 -msgid "Modules at " -msgstr "モジュール " - -#: web/ejabberd_web_admin.erl:2183 -msgid "Statistics of ~p" -msgstr "~p の統計" - -#: web/ejabberd_web_admin.erl:2186 -msgid "Uptime:" -msgstr "起動時間:" - -#: web/ejabberd_web_admin.erl:2189 -msgid "CPU Time:" -msgstr "CPU時間:" - -#: web/ejabberd_web_admin.erl:2195 -msgid "Transactions Committed:" -msgstr "トランザクションのコミット:" - -#: web/ejabberd_web_admin.erl:2198 -msgid "Transactions Aborted:" -msgstr "トランザクションの失敗:" - -#: web/ejabberd_web_admin.erl:2201 -msgid "Transactions Restarted:" -msgstr "トランザクションの再起動:" - -#: web/ejabberd_web_admin.erl:2204 -msgid "Transactions Logged:" -msgstr "トランザクションのログ: " - -#: web/ejabberd_web_admin.erl:2246 -msgid "Update " -msgstr "更新 " - -#: web/ejabberd_web_admin.erl:2254 -msgid "Update plan" -msgstr "更新計画" - -#: web/ejabberd_web_admin.erl:2255 -msgid "Modified modules" -msgstr "変更されたモジュール" - -#: web/ejabberd_web_admin.erl:2256 -msgid "Update script" -msgstr "スクリプトの更新" - -#: web/ejabberd_web_admin.erl:2257 -msgid "Low level update script" -msgstr "低レベル更新スクリプト" - -#: web/ejabberd_web_admin.erl:2258 -msgid "Script check" -msgstr "スクリプトチェック" - -#: web/ejabberd_web_admin.erl:2426 -msgid "Port" -msgstr "ポート" - -#: web/ejabberd_web_admin.erl:2427 -msgid "IP" -msgstr "IP" - -#: web/ejabberd_web_admin.erl:2428 -msgid "Protocol" -msgstr "プロトコル" - -#: web/ejabberd_web_admin.erl:2429 web/ejabberd_web_admin.erl:2556 -msgid "Module" -msgstr "モジュール" - -#: web/ejabberd_web_admin.erl:2430 web/ejabberd_web_admin.erl:2557 -msgid "Options" -msgstr "オプション" - -#: web/ejabberd_web_admin.erl:2450 -msgid "Delete" -msgstr "削除" - -#: web/ejabberd_web_admin.erl:2579 -msgid "Start" -msgstr "開始" - -#: web/mod_register_web.erl:104 -msgid "Your Jabber account was successfully created." -msgstr "Jabber アカウントの作成に成功しました。" - -#: web/mod_register_web.erl:107 -msgid "There was an error creating the account: " -msgstr "アカウントの作成中にエラーが発生しました: " - -#: web/mod_register_web.erl:115 -msgid "Your Jabber account was successfully deleted." -msgstr "Jabber アカウントの削除に成功しました。" - -#: web/mod_register_web.erl:118 -msgid "There was an error deleting the account: " -msgstr "アカウントの削除中にエラーが発生しました: " - -#: web/mod_register_web.erl:128 -msgid "The password of your Jabber account was successfully changed." -msgstr "Jabber アカウントのパスワード変更に成功しました。" - -#: web/mod_register_web.erl:131 -msgid "There was an error changing the password: " -msgstr "パスワードの変更中にエラーが発生しました: " - -#: web/mod_register_web.erl:163 web/mod_register_web.erl:172 -msgid "Jabber Account Registration" -msgstr "Jabber アカウント登録" - -#: web/mod_register_web.erl:174 web/mod_register_web.erl:192 -#: web/mod_register_web.erl:201 -msgid "Register a Jabber account" -msgstr "Jabber アカウントを登録" - -#: web/mod_register_web.erl:176 web/mod_register_web.erl:493 -#: web/mod_register_web.erl:502 -msgid "Unregister a Jabber account" -msgstr "Jabber アカウントを削除" - -#: web/mod_register_web.erl:203 -msgid "" -"This page allows to create a Jabber account in this Jabber server. Your JID " -"(Jabber IDentifier) will be of the form: username@server. Please read " -"carefully the instructions to fill correctly the fields." -msgstr "" -"ここはこの Jabber サーバーにアカウントを作成するページです。あなたの JID " -"(JabberID) は username@server のような形式になります。注意事項どおり、正しく" -"項目を記入してください。" - -#: web/mod_register_web.erl:212 web/mod_register_web.erl:386 -#: web/mod_register_web.erl:509 -msgid "Username:" -msgstr "ユーザー名:" - -#: web/mod_register_web.erl:217 -msgid "This is case insensitive: macbeth is the same that MacBeth and Macbeth." -msgstr "" -"大文字と小文字は区別しません: macbeth は MacBeth や Macbeth と同じです。" - -#: web/mod_register_web.erl:218 -msgid "Characters not allowed:" -msgstr "使用できない文字:" - -#: web/mod_register_web.erl:222 web/mod_register_web.erl:391 -#: web/mod_register_web.erl:514 -msgid "Server:" -msgstr "サーバー:" - -#: web/mod_register_web.erl:232 -msgid "" -"Don't tell your password to anybody, not even the administrators of the " -"Jabber server." -msgstr "" -"パスワードは誰にも教えないようにしてください。Jabber サーバーの管理者があなた" -"にパスワードを尋ねることはありません。" - -#: web/mod_register_web.erl:234 -msgid "You can later change your password using a Jabber client." -msgstr "あなたは後で Jabber クライアントを使用してパスワードを変更できます。" - -#: web/mod_register_web.erl:235 -msgid "" -"Some Jabber clients can store your password in your computer. Use that " -"feature only if you trust your computer is safe." -msgstr "" -"Jabber クライアントはコンピューターにパスワードを記憶できます。コンピューター" -"が安全であると信頼できる場合にのみ、この機能を使用してください。" - -#: web/mod_register_web.erl:237 -msgid "" -"Memorize your password, or write it in a paper placed in a safe place. In " -"Jabber there isn't an automated way to recover your password if you forget " -"it." -msgstr "" -"パスワードは記憶するか、紙に書いて安全な場所に保管してください。もしあなたが" -"パスワードを忘れてしまった場合、Jabber ではパスワードのリカバリを自動的に行う" -"ことはできません。" - -#: web/mod_register_web.erl:242 web/mod_register_web.erl:406 -msgid "Password Verification:" -msgstr "パスワード (確認):" - -#: web/mod_register_web.erl:250 -msgid "Register" -msgstr "登録" - -#: web/mod_register_web.erl:396 -msgid "Old Password:" -msgstr "古いパスワード:" - -#: web/mod_register_web.erl:401 -msgid "New Password:" -msgstr "新しいパスワード:" - -#: web/mod_register_web.erl:504 -msgid "This page allows to unregister a Jabber account in this Jabber server." -msgstr "ここはこの Jabber サーバーのアカウントを削除するページです。" - -#: web/mod_register_web.erl:524 -msgid "Unregister" -msgstr "削除" - -#~ msgid "ejabberd virtual hosts" -#~ msgstr "ejabberd ヴァーチャルホスト" - -#~ msgid "Captcha test failed" -#~ msgstr "キャプチャのテストに失敗しました" - -#~ msgid "Encodings" -#~ msgstr "エンコーディング" - -#~ msgid "(Raw)" -#~ msgstr "(Raw)" - -#~ msgid "Specified nickname is already registered" -#~ msgstr "指定されたニックネームは既に登録されています" - -#~ msgid "Size" -#~ msgstr "サイズ" diff --git a/priv/msgs/nl.msg b/priv/msgs/nl.msg index 70e739fe7..d7799f9c5 100644 --- a/priv/msgs/nl.msg +++ b/priv/msgs/nl.msg @@ -1,19 +1,20 @@ -{"Access Configuration","Toegangsinstellingen"}. -{"Access Control List Configuration","Instellingen van access control lists"}. -{"Access control lists","Access control lists"}. -{"Access Control Lists","Access control lists"}. -{"Access denied by service policy","De toegang werd geweigerd door het beleid van deze dienst"}. -{"Access rules","Access rules"}. -{"Access Rules","Access rules"}. -{"Action on user","Actie op gebruiker"}. -{"Add Jabber ID","Jabber ID toevoegen"}. -{"Add New","Toevoegen"}. -{"Add User","Gebruiker toevoegen"}. -{"Administration","Beheer"}. -{"Administration of ","Beheer van "}. -{"Administrator privileges required","U hebt beheerdersprivileges nodig"}. +%% Generated automatically +%% DO NOT EDIT: run `make translations` instead +%% To improve translations please read: +%% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ + +{" (Add * to the end of field to match substring)"," Gebruik de velden om te zoeken (Voeg achteraan het teken * toe om te zoeken naar alles wat met het eerste deel begint.)."}. +{" has set the subject to: "," veranderde het onderwerp in: "}. {"A friendly name for the node","Bijnaam voor deze knoop"}. +{"A password is required to enter this room","U hebt een wachtwoord nodig om deze chatruimte te kunnen betreden"}. +{"Access denied by service policy","De toegang werd geweigerd door het beleid van deze dienst"}. +{"Action on user","Actie op gebruiker"}. +{"Add User","Gebruiker toevoegen"}. +{"Administration of ","Beheer van "}. +{"Administration","Beheer"}. +{"Administrator privileges required","U hebt beheerdersprivileges nodig"}. {"All activity","Alle activiteit"}. +{"All Users","Alle gebruikers"}. {"Allow this Jabber ID to subscribe to this pubsub node?","Deze gebruiker toestaan te abonneren op deze pubsub node?"}. {"Allow users to change the subject","Sta gebruikers toe het onderwerp te veranderen"}. {"Allow users to query other users","Gebruikers mogen naar andere gebruikers verzoeken verzenden"}. @@ -23,17 +24,14 @@ {"Allow visitors to send private messages to","Gebruikers mogen privéberichten verzenden aan"}. {"Allow visitors to send status text in presence updates","Sta bezoekers toe hun statusbericht in te stellen"}. {"Allow visitors to send voice requests","Gebruikers mogen stemaanvragen verzenden"}. -{"All Users","Alle gebruikers"}. {"Announcements","Mededelingen"}. -{"anyone","iedereen"}. -{"A password is required to enter this room","U hebt een wachtwoord nodig om deze chatruimte te kunnen betreden"}. {"April","April"}. {"August","Augustus"}. -{"Backup","Backup"}. {"Backup Management","Backup"}. -{"Backup of ","Backup maken van "}. +{"Backup of ~p","Backup maken van ~p"}. {"Backup to File at ","Binaire backup maken op "}. -{"Bad format","Slecht formaat"}. +{"Backup","Backup"}. +{"Bad format","Verkeerd formaat"}. {"Birthday","Geboortedatum"}. {"CAPTCHA web page","CAPTCHA webpagina."}. {"Change Password","Wachtwoord wijzigen"}. @@ -46,90 +44,64 @@ {"Chatroom is stopped","Gespreksruimte gestopt"}. {"Chatrooms","Groepsgesprekken"}. {"Choose a username and password to register with this server","Kies een gebruikersnaam en een wachtwoord om u te registreren op deze server"}. -{"Choose modules to stop","Selecteer de modules die u wilt stoppen"}. {"Choose storage type of tables","Opslagmethode voor tabellen kiezen"}. {"Choose whether to approve this entity's subscription.","Beslis of dit verzoek tot abonneren zal worden goedgekeurd"}. {"City","Plaats"}. {"Commands","Commando's"}. {"Conference room does not exist","De chatruimte bestaat niet"}. -{"Configuration","Instellingen"}. {"Configuration of room ~s","Instellingen van chatruimte ~s"}. -{"Connected Resources:","Verbonden bronnen:"}. -{"Connections parameters","Verbindingsparameters"}. +{"Configuration","Instellingen"}. {"Country","Land"}. -{"CPU Time:","Processortijd:"}. -{"Database","Database"}. -{"Database Tables at ","Databasetabellen van "}. {"Database Tables Configuration at ","Instellingen van databasetabellen op "}. +{"Database","Database"}. {"December","December"}. {"Default users as participants","Gebruikers standaard instellen als deelnemers"}. -{"Delete message of the day","Bericht van de dag verwijderen"}. {"Delete message of the day on all hosts","Verwijder bericht-van-de-dag op alle hosts"}. -{"Delete Selected","Geselecteerde verwijderen"}. +{"Delete message of the day","Bericht van de dag verwijderen"}. {"Delete User","Verwijder Gebruiker"}. -{"Delete","Verwijderen"}. {"Deliver event notifications","Gebeurtenisbevestigingen Sturen"}. {"Deliver payloads with event notifications","Berichten bezorgen samen met gebeurtenisnotificaties"}. -{"Description:","Beschrijving:"}. {"Disc only copy","Harde schijf"}. -{"Displayed Groups:","Weergegeven groepen:"}. -{"Don't tell your password to anybody, not even the administrators of the Jabber server.","Geef Uw wachtwoord aan niemand, zelfs niet aan de beheerders van deze Jabber-server."}. {"Dump Backup to Text File at ","Backup naar een tekstbestand schrijven op "}. {"Dump to Text File","Backup naar een tekstbestand schrijven"}. {"Edit Properties","Eigenschappen bewerken"}. {"Either approve or decline the voice request.","Keur stemaanvraag goed of af."}. -{"ejabberd IRC module","ejabberd's IRC-module"}. {"ejabberd MUC module","ejabberd's MUC module"}. +{"ejabberd Multicast service","ejabberd Multicast service"}. {"ejabberd Publish-Subscribe module","ejabberd Publish-Subscribe module"}. {"ejabberd SOCKS5 Bytestreams module","ejabberd SOCKS5 Bytestreams module"}. {"ejabberd vCard module","ejabberd's vCard-module"}. {"ejabberd Web Admin","ejabberd Webbeheer"}. -{"Elements","Elementen"}. {"Email","E-mail"}. {"Enable logging","Logs aanzetten"}. -{"Encoding for server ~b","Karakterset voor server ~b"}. +{"Enable message archiving","Zet bericht-archivering aan"}. {"End User Session","Verwijder Gebruikers-sessie"}. -{"Enter list of {Module, [Options]}","Voer lijst met op te starten modules als volgt in: {Module, [Opties]}"}. {"Enter nickname you want to register","Voer de bijnaam in die u wilt registreren"}. {"Enter path to backup file","Voer pad naar backupbestand in"}. {"Enter path to jabberd14 spool dir","Voer pad naar jabberd14-spool-directory in"}. {"Enter path to jabberd14 spool file","Voer pad naar jabberd14-spool-bestand in"}. {"Enter path to text file","Voer pad naar backupbestand in"}. {"Enter the text you see","Voer de getoonde tekst in"}. -{"Enter username and encodings you wish to use for connecting to IRC servers. Press 'Next' to get more fields to fill in. Press 'Complete' to save settings.","Voer de gebruikersnaam en de coderingen in die u wilt gebruiken voor verbindingen met IRC-servers. Klik op 'Volgende' om meer velden aan te maken. Klik op \"Voltooi' om de instellingen op te slaan."}. -{"Enter username, encodings, ports and passwords you wish to use for connecting to IRC servers","Voer de gebruikersnaam, coderingen, poorten en wachtwoorden in die U wilt gebruiken voor het verbinden met IRC-servers"}. -{"Erlang Jabber Server","Erlang Jabber Server"}. -{"Error","Fout"}. -{"Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef.net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}].","Voorbeeld: [{\"irc.example.org\", \"koi8-r\", 6667, \"geheim\"}, {\"vendetta.example.net\", \"iso8859-1\", 7000}, {irc,testserver.nl\", \"utf-8\"}]."}. {"Exclude Jabber IDs from CAPTCHA challenge","Geen CAPTCHA test voor Jabber IDs"}. +{"Export all tables as SQL queries to a file:","Exporteer alle tabellen als SQL-queries naar een bestand:"}. {"Export data of all users in the server to PIEFXIS files (XEP-0227):","Exporteer data van alle gebruikers in de server naar PIEFXIS-bestanden (XEP-0227):"}. {"Export data of users in a host to PIEFXIS files (XEP-0227):","Exporteer data van alle gebruikers van een host naar PIEXFIS-bestanden (XEP-0227):"}. {"Failed to extract JID from your voice request approval","Er kon geen JID worden ontleend uit deze stemaanvraag"}. {"Family Name","Achternaam"}. {"February","Februari"}. -{"Fill in fields to search for any matching Jabber User","Vul de velden in om te zoeken naar Jabber-gebruikers op deze server"}. -{"Fill in the form to search for any matching Jabber User (Add * to the end of field to match substring)","Gebruik de velden om te zoeken (Voeg achteraan het teken * toe om te zoeken naar alles wat met het eerste deel begint.)."}. {"Friday","Vrijdag"}. -{"From ~s","Van ~s"}. -{"From","Van"}. {"Full Name","Volledige naam"}. {"Get Number of Online Users","Aantal Aanwezige Gebruikers Opvragen"}. {"Get Number of Registered Users","Aantal Geregistreerde Gebruikers Opvragen"}. {"Get User Last Login Time","Tijd van Laatste Aanmelding Opvragen"}. -{"Get User Password","Gebruikerswachtwoord Opvragen"}. {"Get User Statistics","Gebruikers-statistieken Opvragen"}. {"Grant voice to this person?","Stemaanvraag honoreren voor deze persoon?"}. -{"Group ","Groep "}. -{"Groups","Groepen"}. -{"has been banned","werd verbannen"}. -{"has been kicked because of an affiliation change","is weggestuurd vanwege een affiliatieverandering"}. +{"has been banned","is verbannen"}. {"has been kicked because of a system shutdown","is weggestuurd omdat het systeem gestopt wordt"}. +{"has been kicked because of an affiliation change","is weggestuurd vanwege een affiliatieverandering"}. {"has been kicked because the room has been changed to members-only","is weggestuurd omdat de chatruimte vanaf heden alleen toegankelijk is voor leden"}. -{"has been kicked","werd gekicked"}. -{" has set the subject to: "," veranderde het onderwerp in: "}. -{"Host","Host"}. +{"has been kicked","is weggestuurd"}. {"If you don't see the CAPTCHA image here, visit the web page.","Als U het CAPTCHA-plaatje niet ziet, bezoek dan de webpagina."}. -{"If you want to specify different ports, passwords, encodings for IRC servers, fill this list with values in format '{\"irc server\", \"encoding\", port, \"password\"}'. By default this service use \"~s\" encoding, port ~p, empty password.","Als u verschillende poorten, wachtwoorden en coderingen wilt opgeven voor elke IRC-server, vul dan deze lijst met het volgende formaat: '{\"IRC-server\", \"codering\", poort, \"wachtwoord\"}'. Standaard gebruikt deze service de codering \"~s\", poort ~p, leeg wachtwoord."}. {"Import Directory","Directory importeren"}. {"Import File","Bestand importeren"}. {"Import user data from jabberd14 spool file:","Importeer gebruikersdata via spool-bestanden van jabberd14"}. @@ -140,28 +112,13 @@ {"Import Users From jabberd14 Spool Files","Importeer gebruikers via spool-bestanden van jabberd14"}. {"Improper message type","Onjuist berichttype"}. {"Incorrect password","Foutief wachtwoord"}. -{"Invalid affiliation: ~s","Ongeldige affiliatie: ~s"}. -{"Invalid role: ~s","Ongeldige rol: ~s"}. {"IP addresses","IP-adres"}. -{"IP","IP"}. -{"IRC channel (don't put the first #)","IRC kanaal (zonder eerste #)"}. -{"IRC server","IRC-server"}. -{"IRC settings","IRC instellingen"}. -{"IRC Transport","IRC Transport"}. -{"IRC username","Gebruikersnaam voor IRC"}. -{"IRC Username","Gebruikersnaam voor IRC:"}. {"is now known as","heet nu"}. -{"It is not allowed to send private messages","Het is niet toegestaan priveberichten te sturen"}. {"It is not allowed to send private messages of type \"groupchat\"","Er mogen geen privéberichten van het type \"groupchat\" worden verzonden"}. {"It is not allowed to send private messages to the conference","Er mogen geen privéberichten naar de chatruimte worden verzonden"}. -{"Jabber Account Registration","Jabber-account registratie"}. {"Jabber ID","Jabber ID"}. -{"Jabber ID ~s is invalid","De Jabber ID ~s is ongeldig"}. {"January","Januari"}. -{"Join IRC channel","Ga IRC kanaal binnen"}. {"joins the room","betrad de chatruimte"}. -{"Join the IRC channel here.","Ga het IRC kanaal binnen"}. -{"Join the IRC channel in this Jabber ID: ~s","Ga het IRC kanaal van deze Jabber ID binnen: ~s"}. {"July","Juli"}. {"June","Juni"}. {"Last Activity","Laatste activiteit"}. @@ -169,10 +126,6 @@ {"Last month","Afgelopen maand"}. {"Last year","Afgelopen jaar"}. {"leaves the room","verliet de chatruimte"}. -{"Listened Ports at ","Openstaande poorten op "}. -{"Listened Ports","Openstaande poorten"}. -{"List of modules to start","Lijst met op te starten modules"}. -{"Low level update script","Lowlevel script voor de opwaardering"}. {"Make participants list public","Deelnemerslijst publiek maken"}. {"Make room CAPTCHA protected","Chatruimte beveiligen met een geautomatiseerde Turing test"}. {"Make room members-only","Chatruimte enkel toegankelijk maken voor leden"}. @@ -181,41 +134,31 @@ {"Make room persistent","Chatruimte blijvend maken"}. {"Make room public searchable","Chatruimte doorzoekbaar maken"}. {"March","Maart"}. -{"Maximum Number of Occupants","Maximum aantal aanwezigen"}. -{"Max # of items to persist","Maximum aantal in het geheugen te bewaren items"}. {"Max payload size in bytes","Maximumgrootte van bericht in bytes"}. +{"Maximum Number of Occupants","Maximum aantal aanwezigen"}. {"May","Mei"}. -{"Members:","Groepsleden:"}. {"Membership is required to enter this room","U moet lid zijn om deze chatruimte te kunnen betreden"}. -{"Memorize your password, or write it in a paper placed in a safe place. In Jabber there isn't an automated way to recover your password if you forget it.","Onthou het wachtwoord, of schrijf het op en bewaar het op een veilige plaats. Met Jabber is er geen geautomatiseerde manier om het wachtwoord terug te halen als U het vergeet."}. -{"Memory","Geheugen"}. {"Message body","Bericht"}. {"Middle Name","Tussennaam"}. {"Minimum interval between voice requests (in seconds)","Minimale interval tussen stemaanvragen (in seconden)"}. {"Moderator privileges required","U hebt moderatorprivileges nodig"}. -{"moderators only","moderators"}. -{"Modified modules","Gewijzigde modules"}. -{"Module","Module"}. -{"Modules at ","Modules op "}. -{"Modules","Modules"}. {"Monday","Maandag"}. -{"Name:","Naam:"}. +{"Multicast","Multicast"}. +{"Multi-User Chat","Groepschat"}. {"Name","Naam"}. {"Never","Nooit"}. {"New Password:","Nieuw Wachtwoord:"}. -{"Nickname","Bijnaam"}. {"Nickname Registration at ","Registratie van een bijnaam op "}. {"Nickname ~s does not exist in the room","De bijnaam ~s bestaat niet in deze chatruimte"}. -{"nobody","niemand"}. +{"Nickname","Bijnaam"}. {"No body provided for announce message","De mededeling bevat geen bericht"}. {"No Data","Geen gegevens"}. -{"Node ID","Node ID"}. -{"Node ","Node "}. -{"Node not found","Node niet gevonden"}. -{"Nodes","Nodes"}. {"No limit","Geen limiet"}. +{"Node ID","Node ID"}. +{"Node not found","Node niet gevonden"}. +{"Node ~p","Node ~p"}. +{"Nodes","Nodes"}. {"None","Geen"}. -{"No resource provided","Geen bron opgegeven"}. {"Not Found","Niet gevonden"}. {"Notify subscribers when items are removed from the node","Abonnees informeren wanneer items verwijderd worden uit de node"}. {"Notify subscribers when the node configuration changes","Abonnees informeren wanneer de instellingen van de node veranderen"}. @@ -225,13 +168,10 @@ {"Number of online users","Aantal Aanwezige Gebruikers"}. {"Number of registered users","Aantal Geregistreerde Gebruikers"}. {"October","Oktober"}. -{"Offline Messages:","Offline berichten:"}. -{"Offline Messages","Offline berichten"}. {"OK","OK"}. {"Old Password:","Oud Wachtwoord:"}. -{"Online","Online"}. -{"Online Users:","Online gebruikers:"}. {"Online Users","Online gebruikers"}. +{"Online","Online"}. {"Only deliver notifications to available users","Notificaties alleen verzenden naar online gebruikers"}. {"Only moderators and participants are allowed to change the subject in this room","Alleen moderators en deelnemers mogen het onderwerp van deze chatruimte veranderen"}. {"Only moderators are allowed to change the subject in this room","Alleen moderators mogen het onderwerp van deze chatruimte veranderen"}. @@ -239,82 +179,59 @@ {"Only occupants are allowed to send messages to the conference","Alleen aanwezigen mogen berichten naar de chatruimte verzenden"}. {"Only occupants are allowed to send queries to the conference","Alleen aanwezigen mogen verzoeken verzenden naar de chatruimte"}. {"Only service administrators are allowed to send service messages","Alleen beheerders van deze dienst mogen mededelingen verzenden naar alle chatruimtes"}. -{"Options","Opties"}. {"Organization Name","Organisatie"}. {"Organization Unit","Afdeling"}. -{"Outgoing s2s Connections:","Uitgaande s2s-verbindingen:"}. {"Outgoing s2s Connections","Uitgaande s2s-verbindingen"}. -{"Outgoing s2s Servers:","Uitgaande s2s-verbindingen:"}. {"Owner privileges required","U hebt eigenaarsprivileges nodig"}. -{"Packet","Pakket"}. -{"Password ~b","Wachtwoord ~b"}. -{"Password Verification:","Wachtwoord Bevestiging:"}. {"Password Verification","Wachtwoord Bevestiging"}. -{"Password:","Wachtwoord:"}. +{"Password Verification:","Wachtwoord Bevestiging:"}. {"Password","Wachtwoord"}. +{"Password:","Wachtwoord:"}. {"Path to Dir","Pad naar directory"}. {"Path to File","Pad naar bestand"}. -{"Pending","Bezig"}. {"Period: ","Periode: "}. {"Persist items to storage","Items in het geheugen bewaren"}. {"Ping","Ping"}. {"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Merk op dat volgende opties enkel backups maken van de ingebouwde database Mnesia. Als U de ODBC module gebruikt dan moeten daarvan afzonderlijke backups gemaakt worden."}. {"Please, wait for a while before sending new voice request","Wacht s.v.p. met het maken van een nieuwe stemaanvraag."}. {"Pong","Pong"}. -{"Port ~b","Poort ~b"}. -{"Port","Poort"}. {"Present real Jabber IDs to","Jabber ID's kunnen achterhaald worden door"}. {"private, ","privé, "}. -{"Protocol","Protocol"}. {"Publish-Subscribe","Publish-Subscribe"}. {"PubSub subscriber request","PubSub abonnee verzoek"}. {"Purge all items when the relevant publisher goes offline","Verwijder alle items wanneer de gerelateerde publiceerder offline gaat"}. {"Queries to the conference members are not allowed in this room","Er mogen geen verzoeken verzenden worden naar deelnemers in deze chatruimte"}. {"RAM and disc copy","RAM en harde schijf"}. {"RAM copy","RAM"}. -{"Raw","Ruw"}. {"Really delete message of the day?","Wilt u het bericht van de dag verwijderen?"}. {"Recipient is not in the conference room","De ontvanger is niet in de chatruimte"}. -{"Register a Jabber account","Registreer een Jabber-account"}. -{"Registered Users:","Geregistreerde gebruikers:"}. -{"Registered Users","Geregistreerde gebruikers"}. {"Register","Registreer"}. -{"Registration in mod_irc for ","Registratie van "}. {"Remote copy","Op andere nodes in de cluster"}. -{"Remove All Offline Messages","Verwijder alle offline berichten"}. {"Remove User","Gebruiker verwijderen"}. -{"Remove","Verwijderen"}. {"Replaced by new connection","Vervangen door een nieuwe verbinding"}. {"Resources","Bronnen"}. -{"Restart","Herstarten"}. {"Restart Service","Herstart Service"}. {"Restore Backup from File at ","Binaire backup direct herstellen op "}. -{"Restore","Binaire backup direct herstellen"}. {"Restore binary backup after next ejabberd restart (requires less memory):","Binaire backup herstellen na herstart van ejabberd (vereist minder geheugen):"}. {"Restore binary backup immediately:","Binaire backup direct herstellen:"}. {"Restore plain text backup immediately:","Backup in een tekstbestand direct herstellen:"}. +{"Restore","Binaire backup direct herstellen"}. {"Room Configuration","Instellingen van de chatruimte"}. {"Room creation is denied by service policy","De aanmaak van de chatruimte is verhinderd door de instellingen van deze server"}. {"Room description","Beschrijving"}. {"Room Occupants","Aantal aanwezigen"}. {"Room title","Naam van de chatruimte"}. {"Roster groups allowed to subscribe","Contactlijst-groepen die mogen abonneren"}. -{"Roster of ","Roster van "}. -{"Roster","Roster"}. {"Roster size","Contactlijst Groote"}. -{"RPC Call Error","RPC-oproepfout"}. {"Running Nodes","Draaiende nodes"}. -{"~s access rule configuration","Access rules op ~s"}. {"Saturday","Zaterdag"}. -{"Script check","Controle van script"}. {"Search Results for ","Zoekresultaten voor "}. {"Search users in ","Gebruikers zoeken in "}. -{"Send announcement to all online users","Mededeling verzenden naar alle online gebruikers"}. {"Send announcement to all online users on all hosts","Mededeling verzenden naar alle online gebruikers op alle virtuele hosts"}. -{"Send announcement to all users","Mededeling verzenden naar alle gebruikers"}. +{"Send announcement to all online users","Mededeling verzenden naar alle online gebruikers"}. {"Send announcement to all users on all hosts","Stuur aankondiging aan alle gebruikers op alle hosts"}. +{"Send announcement to all users","Mededeling verzenden naar alle gebruikers"}. {"September","September"}. -{"Server ~b","Server ~b"}. {"Server:","Server:"}. {"Set message of the day and send to online users","Bericht van de dag instellen en verzenden naar online gebruikers"}. {"Set message of the day on all hosts and send to online users","Stel bericht-van-de-dag in op alle hosts en stuur naar aanwezige gebruikers"}. @@ -322,81 +239,45 @@ {"Show Integral Table","Volledige tabel laten zien"}. {"Show Ordinary Table","Deel van tabel laten zien"}. {"Shut Down Service","Stop Service"}. -{"~s invites you to the room ~s","~s nodigt je uit voor het groepsgesprek ~s"}. -{"Some Jabber clients can store your password in your computer. Use that feature only if you trust your computer is safe.","Sommige Jabber-clienten kunnen het wachtwoord opslaan op Uw computer. Gebruik deze mogelijkheid alleen als U vertrouwd dat Uw computer afdoende beveiligd is."}. {"Specify the access model","Geef toegangsmodel"}. {"Specify the event message type","Geef type van eventbericht"}. {"Specify the publisher model","Publicatietype opgeven"}. -{"~s's Offline Messages Queue","offline berichten van ~s"}. -{"Start Modules at ","Modules starten op "}. -{"Start Modules","Modules starten"}. -{"Start","Starten"}. -{"Statistics of ~p","Statistieken van ~p"}. -{"Statistics","Statistieken"}. -{"Stop Modules at ","Modules stoppen op "}. -{"Stop Modules","Modules stoppen"}. {"Stopped Nodes","Gestopte nodes"}. -{"Stop","Stoppen"}. -{"Storage Type","Opslagmethode"}. {"Store binary backup:","Binaire backup maken:"}. {"Store plain text backup:","Backup naar een tekstbestand schrijven:"}. {"Subject","Onderwerp"}. {"Submitted","Verzonden"}. -{"Submit","Verzenden"}. {"Subscriber Address","Abonnee Adres"}. -{"Subscription","Inschrijving"}. {"Sunday","Zondag"}. {"That nickname is already in use by another occupant","Deze bijnaam is al in gebruik door een andere aanwezige"}. {"That nickname is registered by another person","Deze bijnaam is al geregistreerd door iemand anders"}. {"The CAPTCHA is valid.","De geautomatiseerde Turing-test is geslaagd."}. {"The CAPTCHA verification has failed","De CAPTCHA-verificatie is mislukt"}. {"The collections with which a node is affiliated","De collecties waar een node mee is gerelateerd"}. -{"the password is","het wachtwoord is"}. {"The password is too weak","Het wachtwoord is te zwak"}. -{"The password of your Jabber account was successfully changed.","Het wachtwoord van Uw Jabber-account is succesvol veranderd."}. -{"There was an error changing the password: ","Er was een fout bij het veranderen van het wachtwoord:"}. +{"the password is","het wachtwoord is"}. {"There was an error creating the account: ","Er was een fout bij het creeern van de account:"}. {"There was an error deleting the account: ","Er was een fout bij het verwijderen van de account."}. -{"This is case insensitive: macbeth is the same that MacBeth and Macbeth.","Dit is niet hoofdlettergevoelig: macbeth is hetzelfde als MacBeth en Macbeth."}. -{"This page allows to create a Jabber account in this Jabber server. Your JID (Jabber IDentifier) will be of the form: username@server. Please read carefully the instructions to fill correctly the fields.","Deze pagina maakt het mogelijk een Jabber-account te registreren op deze server. Uw JID (Jabber IDentiteit) zal er als volg uit zien: gebruikersnaam@server. Lees de instructies zorgvuldig teneinde de velden correct in te vullen."}. -{"This page allows to unregister a Jabber account in this Jabber server.","Deze pagina maakt het mogelijk een Jabber-account op deze server op te heffen."}. -{"This participant is kicked from the room because he sent an error message","Deze deelnemer wordt weggestuurd vanwege het sturen van een foutmeldingsbericht"}. -{"This participant is kicked from the room because he sent an error message to another participant","Deze deelnemer wordt weggestuurd vanwege het sturen van een foutmeldingsbericht aan een andere deelnemer"}. -{"This participant is kicked from the room because he sent an error presence","Deze deelnemer wordt weggestuurd vanwege het sturen van een foutmelding-aanwezigheid"}. {"This room is not anonymous","Deze chatruimte is niet anoniem"}. {"Thursday","Donderdag"}. {"Time delay","Vertraging"}. -{"Time","Tijd"}. -{"To","Aan"}. {"Too many CAPTCHA requests","Te veel CAPTCHA-aanvragen"}. -{"To ~s","Naar ~s"}. +{"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","Te veel (~p) mislukte authenticatie-pogingen van dit IP-adres (~s). Dit adres zal worden gedeblokkeerd om ~s UTC"}. +{"Too many unacked stanzas","Te veel niet-bevestigde stanzas"}. {"Traffic rate limit is exceeded","Dataverkeerslimiet overschreden"}. -{"Transactions Aborted:","Afgebroken transacties:"}. -{"Transactions Committed:","Bevestigde transacties:"}. -{"Transactions Logged:","Gelogde transacties:"}. -{"Transactions Restarted:","Herstarte transacties:"}. {"Tuesday","Dinsdag"}. {"Unable to generate a CAPTCHA","Het generen van een CAPTCHA is mislukt"}. {"Unauthorized","Niet geautoriseerd"}. -{"Unregister a Jabber account","Opheffen van Jabber-account"}. {"Unregister","Opheffen"}. -{"Update","Bijwerken"}. {"Update message of the day (don't send)","Bericht van de dag bijwerken (niet verzenden)"}. {"Update message of the day on all hosts (don't send)","Verander bericht-van-de-dag op alle hosts (niet versturen)"}. -{"Update ","Opwaarderen van "}. -{"Update plan","Plan voor de opwaardering"}. -{"Update script","Script voor de opwaardering"}. -{"Uptime:","Uptime:"}. -{"Use of STARTTLS required","Gebruik van STARTTLS is vereist"}. -{"User ","Gebruiker "}. -{"User","Gebruiker"}. {"User JID","JID Gebruiker"}. {"User Management","Gebruikersbeheer"}. +{"User","Gebruiker"}. {"Username:","Gebruikersnaam:"}. {"Users are not allowed to register accounts so quickly","Het is gebruikers niet toegestaan zo snel achter elkaar te registreren"}. -{"Users","Gebruikers"}. {"Users Last Activity","Laatste activiteit van gebruikers"}. -{"Validate","Bevestigen"}. +{"Users","Gebruikers"}. {"vCard User Search","Gebruikers zoeken"}. {"Virtual Hosts","Virtuele hosts"}. {"Visitors are not allowed to change their nicknames in this room","Het is bezoekers niet toegestaan hun naam te veranderen in dit kanaal"}. @@ -406,16 +287,11 @@ {"Wednesday","Woensdag"}. {"When to send the last published item","Wanneer het laatst gepubliceerde item verzonden moet worden"}. {"Whether to allow subscriptions","Abonnementsaanvraag toestaan"}. -{"You can later change your password using a Jabber client.","U can het wachtwoord later veranderen met een Jabber-client."}. {"You have been banned from this room","U werd verbannen uit deze chatruimte"}. {"You must fill in field \"Nickname\" in the form","U moet het veld \"bijnaam\" invullen"}. {"You need a client that supports x:data and CAPTCHA to register","U hebt een client nodig die x:data en CAPTCHA ondersteunt om een bijnaam te registreren"}. {"You need a client that supports x:data to register the nickname","U hebt een client nodig die x:data ondersteunt om een bijnaam te registreren"}. -{"You need an x:data capable client to configure mod_irc settings","U hebt een client nodig die x:data ondersteunt om dit IRC-transport in te stellen"}. -{"You need an x:data capable client to configure room","U hebt een client nodig die x:data ondersteunt om deze chatruimte in te stellen"}. {"You need an x:data capable client to search","U hebt een client nodig die x:data ondersteunt om te zoeken"}. {"Your active privacy list has denied the routing of this stanza.","Uw actieve privacy-lijst verbied het routeren van dit stanza."}. {"Your contact offline message queue is full. The message has been discarded.","Te veel offline berichten voor dit contactpersoon. Het bericht is niet opgeslagen."}. -{"Your Jabber account was successfully created.","Uw Jabber-account is succesvol gecreeerd."}. -{"Your Jabber account was successfully deleted.","Uw Jabber-account is succesvol verwijderd."}. -{"Your messages to ~s are being blocked. To unblock them, visit ~s","Uw berichten aan ~s worden geblokkeerd. Om ze te deblokkeren, ga naar ~s"}. +{"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","Uw berichten aan ~s worden geblokkeerd. Om ze te deblokkeren, ga naar ~s"}. diff --git a/priv/msgs/nl.po b/priv/msgs/nl.po deleted file mode 100644 index 5135d89bf..000000000 --- a/priv/msgs/nl.po +++ /dev/null @@ -1,1877 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: 2.1.0-alpha\n" -"POT-Creation-Date: \n" -"PO-Revision-Date: \n" -"Last-Translator: Andreas van Cranenburgh \n" -"Language-Team: \n" -"Language: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Language: Dutch (nederlands)\n" -"X-Additional-Translator: Sander Devrieze\n" - -#: ejabberd_c2s.erl:424 ejabberd_c2s.erl:727 -msgid "Use of STARTTLS required" -msgstr "Gebruik van STARTTLS is vereist" - -#: ejabberd_c2s.erl:503 -msgid "No resource provided" -msgstr "Geen bron opgegeven" - -#: ejabberd_c2s.erl:1197 -msgid "Replaced by new connection" -msgstr "Vervangen door een nieuwe verbinding" - -#: ejabberd_c2s.erl:1885 -msgid "Your active privacy list has denied the routing of this stanza." -msgstr "Uw actieve privacy-lijst verbied het routeren van dit stanza." - -#: ejabberd_captcha.erl:96 ejabberd_captcha.erl:152 ejabberd_captcha.erl:178 -msgid "Enter the text you see" -msgstr "Voer de getoonde tekst in" - -#: ejabberd_captcha.erl:101 -msgid "Your messages to ~s are being blocked. To unblock them, visit ~s" -msgstr "" -"Uw berichten aan ~s worden geblokkeerd. Om ze te deblokkeren, ga naar ~s" - -#: ejabberd_captcha.erl:134 -msgid "If you don't see the CAPTCHA image here, visit the web page." -msgstr "Als U het CAPTCHA-plaatje niet ziet, bezoek dan de webpagina." - -#: ejabberd_captcha.erl:146 -msgid "CAPTCHA web page" -msgstr "CAPTCHA webpagina." - -#: ejabberd_captcha.erl:307 -msgid "The CAPTCHA is valid." -msgstr "De geautomatiseerde Turing-test is geslaagd." - -#: mod_adhoc.erl:95 mod_adhoc.erl:125 mod_adhoc.erl:143 mod_adhoc.erl:161 -msgid "Commands" -msgstr "Commando's" - -#: mod_adhoc.erl:149 mod_adhoc.erl:243 -msgid "Ping" -msgstr "Ping" - -#: mod_adhoc.erl:260 -msgid "Pong" -msgstr "Pong" - -#: mod_announce.erl:507 mod_announce_odbc.erl:499 -msgid "Really delete message of the day?" -msgstr "Wilt u het bericht van de dag verwijderen?" - -#: mod_announce.erl:515 mod_announce_odbc.erl:507 mod_configure.erl:1083 -#: mod_configure.erl:1128 -msgid "Subject" -msgstr "Onderwerp" - -#: mod_announce.erl:520 mod_announce_odbc.erl:512 mod_configure.erl:1088 -#: mod_configure.erl:1133 -msgid "Message body" -msgstr "Bericht" - -#: mod_announce.erl:600 mod_announce_odbc.erl:592 -msgid "No body provided for announce message" -msgstr "De mededeling bevat geen bericht" - -#: mod_announce.erl:635 mod_announce_odbc.erl:627 -msgid "Announcements" -msgstr "Mededelingen" - -#: mod_announce.erl:637 mod_announce_odbc.erl:629 -msgid "Send announcement to all users" -msgstr "Mededeling verzenden naar alle gebruikers" - -#: mod_announce.erl:639 mod_announce_odbc.erl:631 -msgid "Send announcement to all users on all hosts" -msgstr "Stuur aankondiging aan alle gebruikers op alle hosts" - -#: mod_announce.erl:641 mod_announce_odbc.erl:633 -msgid "Send announcement to all online users" -msgstr "Mededeling verzenden naar alle online gebruikers" - -#: mod_announce.erl:643 mod_announce_odbc.erl:635 mod_configure.erl:1078 -#: mod_configure.erl:1123 -msgid "Send announcement to all online users on all hosts" -msgstr "" -"Mededeling verzenden naar alle online gebruikers op alle virtuele hosts" - -#: mod_announce.erl:645 mod_announce_odbc.erl:637 -msgid "Set message of the day and send to online users" -msgstr "Bericht van de dag instellen en verzenden naar online gebruikers" - -#: mod_announce.erl:647 mod_announce_odbc.erl:639 -msgid "Set message of the day on all hosts and send to online users" -msgstr "" -"Stel bericht-van-de-dag in op alle hosts en stuur naar aanwezige gebruikers" - -#: mod_announce.erl:649 mod_announce_odbc.erl:641 -msgid "Update message of the day (don't send)" -msgstr "Bericht van de dag bijwerken (niet verzenden)" - -#: mod_announce.erl:651 mod_announce_odbc.erl:643 -msgid "Update message of the day on all hosts (don't send)" -msgstr "Verander bericht-van-de-dag op alle hosts (niet versturen)" - -#: mod_announce.erl:653 mod_announce_odbc.erl:645 -msgid "Delete message of the day" -msgstr "Bericht van de dag verwijderen" - -#: mod_announce.erl:655 mod_announce_odbc.erl:647 -msgid "Delete message of the day on all hosts" -msgstr "Verwijder bericht-van-de-dag op alle hosts" - -#: mod_configure.erl:114 mod_configure.erl:274 mod_configure.erl:296 -#: mod_configure.erl:498 -msgid "Configuration" -msgstr "Instellingen" - -#: mod_configure.erl:125 mod_configure.erl:576 web/ejabberd_web_admin.erl:1936 -msgid "Database" -msgstr "Database" - -#: mod_configure.erl:127 mod_configure.erl:595 -msgid "Start Modules" -msgstr "Modules starten" - -#: mod_configure.erl:129 mod_configure.erl:596 -msgid "Stop Modules" -msgstr "Modules stoppen" - -#: mod_configure.erl:131 mod_configure.erl:604 web/ejabberd_web_admin.erl:1937 -msgid "Backup" -msgstr "Backup" - -#: mod_configure.erl:133 mod_configure.erl:605 -msgid "Restore" -msgstr "Binaire backup direct herstellen" - -#: mod_configure.erl:135 mod_configure.erl:606 -msgid "Dump to Text File" -msgstr "Backup naar een tekstbestand schrijven" - -#: mod_configure.erl:137 mod_configure.erl:615 -msgid "Import File" -msgstr "Bestand importeren" - -#: mod_configure.erl:139 mod_configure.erl:616 -msgid "Import Directory" -msgstr "Directory importeren" - -#: mod_configure.erl:141 mod_configure.erl:581 mod_configure.erl:1057 -msgid "Restart Service" -msgstr "Herstart Service" - -#: mod_configure.erl:143 mod_configure.erl:582 mod_configure.erl:1102 -msgid "Shut Down Service" -msgstr "Stop Service" - -#: mod_configure.erl:145 mod_configure.erl:518 mod_configure.erl:1197 -#: web/ejabberd_web_admin.erl:1527 -msgid "Add User" -msgstr "Gebruiker toevoegen" - -#: mod_configure.erl:147 mod_configure.erl:519 mod_configure.erl:1219 -msgid "Delete User" -msgstr "Verwijder Gebruiker" - -#: mod_configure.erl:149 mod_configure.erl:520 mod_configure.erl:1231 -msgid "End User Session" -msgstr "Verwijder Gebruikers-sessie" - -#: mod_configure.erl:151 mod_configure.erl:521 mod_configure.erl:1243 -#: mod_configure.erl:1255 -msgid "Get User Password" -msgstr "Gebruikerswachtwoord Opvragen" - -#: mod_configure.erl:153 mod_configure.erl:522 -msgid "Change User Password" -msgstr "Verander Gebruikerswachtwoord" - -#: mod_configure.erl:155 mod_configure.erl:523 mod_configure.erl:1272 -msgid "Get User Last Login Time" -msgstr "Tijd van Laatste Aanmelding Opvragen" - -#: mod_configure.erl:157 mod_configure.erl:524 mod_configure.erl:1284 -msgid "Get User Statistics" -msgstr "Gebruikers-statistieken Opvragen" - -#: mod_configure.erl:159 mod_configure.erl:525 -msgid "Get Number of Registered Users" -msgstr "Aantal Geregistreerde Gebruikers Opvragen" - -#: mod_configure.erl:161 mod_configure.erl:526 -msgid "Get Number of Online Users" -msgstr "Aantal Aanwezige Gebruikers Opvragen" - -#: mod_configure.erl:163 mod_configure.erl:509 web/ejabberd_web_admin.erl:831 -#: web/ejabberd_web_admin.erl:872 -msgid "Access Control Lists" -msgstr "Access control lists" - -#: mod_configure.erl:165 mod_configure.erl:510 web/ejabberd_web_admin.erl:940 -#: web/ejabberd_web_admin.erl:976 -msgid "Access Rules" -msgstr "Access rules" - -#: mod_configure.erl:297 mod_configure.erl:499 -msgid "User Management" -msgstr "Gebruikersbeheer" - -#: mod_configure.erl:500 web/ejabberd_web_admin.erl:1058 -#: web/ejabberd_web_admin.erl:1462 -msgid "Online Users" -msgstr "Online gebruikers" - -#: mod_configure.erl:501 -msgid "All Users" -msgstr "Alle gebruikers" - -#: mod_configure.erl:502 -msgid "Outgoing s2s Connections" -msgstr "Uitgaande s2s-verbindingen" - -#: mod_configure.erl:503 web/ejabberd_web_admin.erl:1907 -msgid "Running Nodes" -msgstr "Draaiende nodes" - -#: mod_configure.erl:504 web/ejabberd_web_admin.erl:1909 -msgid "Stopped Nodes" -msgstr "Gestopte nodes" - -#: mod_configure.erl:577 mod_configure.erl:587 web/ejabberd_web_admin.erl:1953 -msgid "Modules" -msgstr "Modules" - -#: mod_configure.erl:578 -msgid "Backup Management" -msgstr "Backup" - -#: mod_configure.erl:579 -msgid "Import Users From jabberd14 Spool Files" -msgstr "Importeer gebruikers via spool-bestanden van jabberd14" - -#: mod_configure.erl:699 -msgid "To ~s" -msgstr "Naar ~s" - -#: mod_configure.erl:717 -msgid "From ~s" -msgstr "Van ~s" - -#: mod_configure.erl:913 -msgid "Database Tables Configuration at " -msgstr "Instellingen van databasetabellen op " - -#: mod_configure.erl:918 -msgid "Choose storage type of tables" -msgstr "Opslagmethode voor tabellen kiezen" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Disc only copy" -msgstr "Harde schijf" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM and disc copy" -msgstr "RAM en harde schijf" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM copy" -msgstr "RAM" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Remote copy" -msgstr "Op andere nodes in de cluster" - -#: mod_configure.erl:950 -msgid "Stop Modules at " -msgstr "Modules stoppen op " - -#: mod_configure.erl:954 -msgid "Choose modules to stop" -msgstr "Selecteer de modules die u wilt stoppen" - -#: mod_configure.erl:969 -msgid "Start Modules at " -msgstr "Modules starten op " - -#: mod_configure.erl:973 -msgid "Enter list of {Module, [Options]}" -msgstr "Voer lijst met op te starten modules als volgt in: {Module, [Opties]}" - -#: mod_configure.erl:974 -msgid "List of modules to start" -msgstr "Lijst met op te starten modules" - -#: mod_configure.erl:983 -msgid "Backup to File at " -msgstr "Binaire backup maken op " - -#: mod_configure.erl:987 mod_configure.erl:1001 -msgid "Enter path to backup file" -msgstr "Voer pad naar backupbestand in" - -#: mod_configure.erl:988 mod_configure.erl:1002 mod_configure.erl:1016 -#: mod_configure.erl:1030 -msgid "Path to File" -msgstr "Pad naar bestand" - -#: mod_configure.erl:997 -msgid "Restore Backup from File at " -msgstr "Binaire backup direct herstellen op " - -#: mod_configure.erl:1011 -msgid "Dump Backup to Text File at " -msgstr "Backup naar een tekstbestand schrijven op " - -#: mod_configure.erl:1015 -msgid "Enter path to text file" -msgstr "Voer pad naar backupbestand in" - -#: mod_configure.erl:1025 -msgid "Import User from File at " -msgstr "Importeer gebruiker via bestand op " - -#: mod_configure.erl:1029 -msgid "Enter path to jabberd14 spool file" -msgstr "Voer pad naar jabberd14-spool-bestand in" - -#: mod_configure.erl:1039 -msgid "Import Users from Dir at " -msgstr "Gebruikers importeren vanaf directory op " - -#: mod_configure.erl:1043 -msgid "Enter path to jabberd14 spool dir" -msgstr "Voer pad naar jabberd14-spool-directory in" - -#: mod_configure.erl:1044 -msgid "Path to Dir" -msgstr "Pad naar directory" - -#: mod_configure.erl:1060 mod_configure.erl:1105 -msgid "Time delay" -msgstr "Vertraging" - -#: mod_configure.erl:1143 -msgid "Access Control List Configuration" -msgstr "Instellingen van access control lists" - -#: mod_configure.erl:1147 -msgid "Access control lists" -msgstr "Access control lists" - -#: mod_configure.erl:1171 -msgid "Access Configuration" -msgstr "Toegangsinstellingen" - -#: mod_configure.erl:1175 -msgid "Access rules" -msgstr "Access rules" - -#: mod_configure.erl:1200 mod_configure.erl:1222 mod_configure.erl:1234 -#: mod_configure.erl:1246 mod_configure.erl:1258 mod_configure.erl:1275 -#: mod_configure.erl:1287 mod_configure.erl:1650 mod_configure.erl:1700 -#: mod_configure.erl:1721 mod_roster.erl:943 mod_roster_odbc.erl:1060 -#: mod_vcard.erl:472 mod_vcard_ldap.erl:554 mod_vcard_odbc.erl:448 -msgid "Jabber ID" -msgstr "Jabber ID" - -#: mod_configure.erl:1205 mod_configure.erl:1263 mod_configure.erl:1651 -#: mod_configure.erl:1863 mod_muc/mod_muc_room.erl:3224 mod_register.erl:235 -#: web/ejabberd_web_admin.erl:1520 -msgid "Password" -msgstr "Wachtwoord" - -#: mod_configure.erl:1210 -msgid "Password Verification" -msgstr "Wachtwoord Bevestiging" - -#: mod_configure.erl:1301 -msgid "Number of registered users" -msgstr "Aantal Geregistreerde Gebruikers" - -#: mod_configure.erl:1315 -msgid "Number of online users" -msgstr "Aantal Aanwezige Gebruikers" - -#: mod_configure.erl:1682 web/ejabberd_web_admin.erl:1588 -#: web/ejabberd_web_admin.erl:1743 -msgid "Never" -msgstr "Nooit" - -#: mod_configure.erl:1696 web/ejabberd_web_admin.erl:1601 -#: web/ejabberd_web_admin.erl:1756 -msgid "Online" -msgstr "Online" - -#: mod_configure.erl:1701 -msgid "Last login" -msgstr "Laatste Aanmelding" - -#: mod_configure.erl:1722 -msgid "Roster size" -msgstr "Contactlijst Groote" - -#: mod_configure.erl:1723 -msgid "IP addresses" -msgstr "IP-adres" - -#: mod_configure.erl:1724 -msgid "Resources" -msgstr "Bronnen" - -#: mod_configure.erl:1850 -msgid "Administration of " -msgstr "Beheer van " - -#: mod_configure.erl:1853 -msgid "Action on user" -msgstr "Actie op gebruiker" - -#: mod_configure.erl:1857 -msgid "Edit Properties" -msgstr "Eigenschappen bewerken" - -#: mod_configure.erl:1860 web/ejabberd_web_admin.erl:1769 -msgid "Remove User" -msgstr "Gebruiker verwijderen" - -#: mod_irc/mod_irc.erl:201 mod_irc/mod_irc_odbc.erl:196 -#: mod_muc/mod_muc.erl:336 mod_muc/mod_muc_odbc.erl:342 -msgid "Access denied by service policy" -msgstr "De toegang werd geweigerd door het beleid van deze dienst" - -#: mod_irc/mod_irc.erl:401 mod_irc/mod_irc_odbc.erl:398 -msgid "IRC Transport" -msgstr "IRC Transport" - -#: mod_irc/mod_irc.erl:428 mod_irc/mod_irc_odbc.erl:425 -msgid "ejabberd IRC module" -msgstr "ejabberd's IRC-module" - -#: mod_irc/mod_irc.erl:559 mod_irc/mod_irc_odbc.erl:568 -msgid "You need an x:data capable client to configure mod_irc settings" -msgstr "" -"U hebt een client nodig die x:data ondersteunt om dit IRC-transport in te " -"stellen" - -#: mod_irc/mod_irc.erl:566 mod_irc/mod_irc_odbc.erl:575 -msgid "Registration in mod_irc for " -msgstr "Registratie van " - -#: mod_irc/mod_irc.erl:571 mod_irc/mod_irc_odbc.erl:580 -msgid "" -"Enter username, encodings, ports and passwords you wish to use for " -"connecting to IRC servers" -msgstr "" -"Voer de gebruikersnaam, coderingen, poorten en wachtwoorden in die U wilt " -"gebruiken voor het verbinden met IRC-servers" - -#: mod_irc/mod_irc.erl:576 mod_irc/mod_irc_odbc.erl:585 -msgid "IRC Username" -msgstr "Gebruikersnaam voor IRC:" - -#: mod_irc/mod_irc.erl:586 mod_irc/mod_irc_odbc.erl:595 -msgid "" -"If you want to specify different ports, passwords, encodings for IRC " -"servers, fill this list with values in format '{\"irc server\", \"encoding" -"\", port, \"password\"}'. By default this service use \"~s\" encoding, port " -"~p, empty password." -msgstr "" -"Als u verschillende poorten, wachtwoorden en coderingen wilt opgeven voor " -"elke IRC-server, vul dan deze lijst met het volgende formaat: '{\"IRC-server" -"\", \"codering\", poort, \"wachtwoord\"}'. Standaard gebruikt deze service " -"de codering \"~s\", poort ~p, leeg wachtwoord." - -#: mod_irc/mod_irc.erl:598 mod_irc/mod_irc_odbc.erl:607 -msgid "" -"Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef." -"net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]." -msgstr "" -"Voorbeeld: [{\"irc.example.org\", \"koi8-r\", 6667, \"geheim\"}, {\"vendetta." -"example.net\", \"iso8859-1\", 7000}, {irc,testserver.nl\", \"utf-8\"}]." - -#: mod_irc/mod_irc.erl:603 mod_irc/mod_irc_odbc.erl:612 -msgid "Connections parameters" -msgstr "Verbindingsparameters" - -#: mod_irc/mod_irc.erl:728 mod_irc/mod_irc_odbc.erl:757 -msgid "Join IRC channel" -msgstr "Ga IRC kanaal binnen" - -#: mod_irc/mod_irc.erl:732 mod_irc/mod_irc_odbc.erl:761 -msgid "IRC channel (don't put the first #)" -msgstr "IRC kanaal (zonder eerste #)" - -#: mod_irc/mod_irc.erl:737 mod_irc/mod_irc_odbc.erl:766 -msgid "IRC server" -msgstr "IRC-server" - -#: mod_irc/mod_irc.erl:770 mod_irc/mod_irc.erl:774 -#: mod_irc/mod_irc_odbc.erl:799 mod_irc/mod_irc_odbc.erl:803 -msgid "Join the IRC channel here." -msgstr "Ga het IRC kanaal binnen" - -#: mod_irc/mod_irc.erl:778 mod_irc/mod_irc_odbc.erl:807 -msgid "Join the IRC channel in this Jabber ID: ~s" -msgstr "Ga het IRC kanaal van deze Jabber ID binnen: ~s" - -#: mod_irc/mod_irc.erl:863 mod_irc/mod_irc_odbc.erl:904 -msgid "IRC settings" -msgstr "IRC instellingen" - -#: mod_irc/mod_irc.erl:868 mod_irc/mod_irc_odbc.erl:909 -msgid "" -"Enter username and encodings you wish to use for connecting to IRC servers. " -"Press 'Next' to get more fields to fill in. Press 'Complete' to save " -"settings." -msgstr "" -"Voer de gebruikersnaam en de coderingen in die u wilt gebruiken voor " -"verbindingen met IRC-servers. Klik op 'Volgende' om meer velden aan te " -"maken. Klik op \"Voltooi' om de instellingen op te slaan." - -#: mod_irc/mod_irc.erl:874 mod_irc/mod_irc_odbc.erl:915 -msgid "IRC username" -msgstr "Gebruikersnaam voor IRC" - -#: mod_irc/mod_irc.erl:923 mod_irc/mod_irc_odbc.erl:964 -msgid "Password ~b" -msgstr "Wachtwoord ~b" - -#: mod_irc/mod_irc.erl:928 mod_irc/mod_irc_odbc.erl:969 -msgid "Port ~b" -msgstr "Poort ~b" - -#: mod_irc/mod_irc.erl:933 mod_irc/mod_irc_odbc.erl:974 -msgid "Encoding for server ~b" -msgstr "Karakterset voor server ~b" - -#: mod_irc/mod_irc.erl:942 mod_irc/mod_irc_odbc.erl:983 -msgid "Server ~b" -msgstr "Server ~b" - -#: mod_muc/mod_muc.erl:449 mod_muc/mod_muc_odbc.erl:456 -msgid "Only service administrators are allowed to send service messages" -msgstr "" -"Alleen beheerders van deze dienst mogen mededelingen verzenden naar alle " -"chatruimtes" - -#: mod_muc/mod_muc.erl:493 mod_muc/mod_muc_odbc.erl:500 -msgid "Room creation is denied by service policy" -msgstr "" -"De aanmaak van de chatruimte is verhinderd door de instellingen van deze " -"server" - -#: mod_muc/mod_muc.erl:500 mod_muc/mod_muc_odbc.erl:507 -msgid "Conference room does not exist" -msgstr "De chatruimte bestaat niet" - -#: mod_muc/mod_muc.erl:582 mod_muc/mod_muc_odbc.erl:595 -msgid "Chatrooms" -msgstr "Groepsgesprekken" - -#: mod_muc/mod_muc.erl:712 mod_muc/mod_muc_odbc.erl:729 -msgid "You need a client that supports x:data to register the nickname" -msgstr "" -"U hebt een client nodig die x:data ondersteunt om een bijnaam te registreren" - -#: mod_muc/mod_muc.erl:718 mod_muc/mod_muc_odbc.erl:735 -msgid "Nickname Registration at " -msgstr "Registratie van een bijnaam op " - -#: mod_muc/mod_muc.erl:722 mod_muc/mod_muc_odbc.erl:739 -msgid "Enter nickname you want to register" -msgstr "Voer de bijnaam in die u wilt registreren" - -#: mod_muc/mod_muc.erl:723 mod_muc/mod_muc_odbc.erl:740 -#: mod_muc/mod_muc_room.erl:3810 mod_roster.erl:944 mod_roster_odbc.erl:1061 -#: mod_vcard.erl:364 mod_vcard.erl:477 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:453 -msgid "Nickname" -msgstr "Bijnaam" - -#: mod_muc/mod_muc.erl:762 mod_muc/mod_muc_odbc.erl:784 -#: mod_muc/mod_muc_room.erl:1064 mod_muc/mod_muc_room.erl:1787 -msgid "That nickname is registered by another person" -msgstr "Deze bijnaam is al geregistreerd door iemand anders" - -#: mod_muc/mod_muc.erl:788 mod_muc/mod_muc_odbc.erl:811 -msgid "You must fill in field \"Nickname\" in the form" -msgstr "U moet het veld \"bijnaam\" invullen" - -#: mod_muc/mod_muc.erl:808 mod_muc/mod_muc_odbc.erl:831 -msgid "ejabberd MUC module" -msgstr "ejabberd's MUC module" - -#: mod_muc/mod_muc_log.erl:374 mod_muc/mod_muc_log.erl:381 -msgid "Chatroom configuration modified" -msgstr "De instellingen van de chatruimte werden veranderd" - -#: mod_muc/mod_muc_log.erl:384 -msgid "joins the room" -msgstr "betrad de chatruimte" - -#: mod_muc/mod_muc_log.erl:387 mod_muc/mod_muc_log.erl:390 -msgid "leaves the room" -msgstr "verliet de chatruimte" - -#: mod_muc/mod_muc_log.erl:393 mod_muc/mod_muc_log.erl:396 -msgid "has been banned" -msgstr "werd verbannen" - -#: mod_muc/mod_muc_log.erl:399 mod_muc/mod_muc_log.erl:402 -msgid "has been kicked" -msgstr "werd gekicked" - -#: mod_muc/mod_muc_log.erl:405 -msgid "has been kicked because of an affiliation change" -msgstr "is weggestuurd vanwege een affiliatieverandering" - -#: mod_muc/mod_muc_log.erl:408 -msgid "has been kicked because the room has been changed to members-only" -msgstr "" -"is weggestuurd omdat de chatruimte vanaf heden alleen toegankelijk is voor " -"leden" - -#: mod_muc/mod_muc_log.erl:411 -msgid "has been kicked because of a system shutdown" -msgstr "is weggestuurd omdat het systeem gestopt wordt" - -#: mod_muc/mod_muc_log.erl:414 -msgid "is now known as" -msgstr "heet nu" - -#: mod_muc/mod_muc_log.erl:417 mod_muc/mod_muc_log.erl:688 -#: mod_muc/mod_muc_room.erl:2393 -msgid " has set the subject to: " -msgstr " veranderde het onderwerp in: " - -#: mod_muc/mod_muc_log.erl:452 -msgid "Chatroom is created" -msgstr "Gespreksruimte gecreëerd" - -#: mod_muc/mod_muc_log.erl:453 -msgid "Chatroom is destroyed" -msgstr "Gespreksruimte vernietigd" - -#: mod_muc/mod_muc_log.erl:454 -msgid "Chatroom is started" -msgstr "Gespreksruimte gestart" - -#: mod_muc/mod_muc_log.erl:455 -msgid "Chatroom is stopped" -msgstr "Gespreksruimte gestopt" - -#: mod_muc/mod_muc_log.erl:459 -msgid "Monday" -msgstr "Maandag" - -#: mod_muc/mod_muc_log.erl:460 -msgid "Tuesday" -msgstr "Dinsdag" - -#: mod_muc/mod_muc_log.erl:461 -msgid "Wednesday" -msgstr "Woensdag" - -#: mod_muc/mod_muc_log.erl:462 -msgid "Thursday" -msgstr "Donderdag" - -#: mod_muc/mod_muc_log.erl:463 -msgid "Friday" -msgstr "Vrijdag" - -#: mod_muc/mod_muc_log.erl:464 -msgid "Saturday" -msgstr "Zaterdag" - -#: mod_muc/mod_muc_log.erl:465 -msgid "Sunday" -msgstr "Zondag" - -#: mod_muc/mod_muc_log.erl:469 -msgid "January" -msgstr "Januari" - -#: mod_muc/mod_muc_log.erl:470 -msgid "February" -msgstr "Februari" - -#: mod_muc/mod_muc_log.erl:471 -msgid "March" -msgstr "Maart" - -#: mod_muc/mod_muc_log.erl:472 -msgid "April" -msgstr "April" - -#: mod_muc/mod_muc_log.erl:473 -msgid "May" -msgstr "Mei" - -#: mod_muc/mod_muc_log.erl:474 -msgid "June" -msgstr "Juni" - -#: mod_muc/mod_muc_log.erl:475 -msgid "July" -msgstr "Juli" - -#: mod_muc/mod_muc_log.erl:476 -msgid "August" -msgstr "Augustus" - -#: mod_muc/mod_muc_log.erl:477 -msgid "September" -msgstr "September" - -#: mod_muc/mod_muc_log.erl:478 -msgid "October" -msgstr "Oktober" - -#: mod_muc/mod_muc_log.erl:479 -msgid "November" -msgstr "November" - -#: mod_muc/mod_muc_log.erl:480 -msgid "December" -msgstr "December" - -#: mod_muc/mod_muc_log.erl:750 -msgid "Room Configuration" -msgstr "Instellingen van de chatruimte" - -#: mod_muc/mod_muc_log.erl:759 -msgid "Room Occupants" -msgstr "Aantal aanwezigen" - -#: mod_muc/mod_muc_room.erl:174 -msgid "Traffic rate limit is exceeded" -msgstr "Dataverkeerslimiet overschreden" - -#: mod_muc/mod_muc_room.erl:246 -msgid "" -"This participant is kicked from the room because he sent an error message" -msgstr "" -"Deze deelnemer wordt weggestuurd vanwege het sturen van een " -"foutmeldingsbericht" - -#: mod_muc/mod_muc_room.erl:255 -msgid "It is not allowed to send private messages to the conference" -msgstr "Er mogen geen privéberichten naar de chatruimte worden verzonden" - -#: mod_muc/mod_muc_room.erl:332 -msgid "Please, wait for a while before sending new voice request" -msgstr "Wacht s.v.p. met het maken van een nieuwe stemaanvraag." - -#: mod_muc/mod_muc_room.erl:347 -msgid "Voice requests are disabled in this conference" -msgstr "Stemaanvragen zijn uitgeschakeld voor deze chatruimte" - -#: mod_muc/mod_muc_room.erl:364 -msgid "Failed to extract JID from your voice request approval" -msgstr "Er kon geen JID worden ontleend uit deze stemaanvraag" - -#: mod_muc/mod_muc_room.erl:393 -msgid "Only moderators can approve voice requests" -msgstr "Alleen moderators kunnen stemaanvragen goedkeuren" - -#: mod_muc/mod_muc_room.erl:408 -msgid "Improper message type" -msgstr "Onjuist berichttype" - -#: mod_muc/mod_muc_room.erl:518 -msgid "" -"This participant is kicked from the room because he sent an error message to " -"another participant" -msgstr "" -"Deze deelnemer wordt weggestuurd vanwege het sturen van een " -"foutmeldingsbericht aan een andere deelnemer" - -#: mod_muc/mod_muc_room.erl:531 -msgid "It is not allowed to send private messages of type \"groupchat\"" -msgstr "" -"Er mogen geen privéberichten van het type \"groupchat\" worden verzonden" - -#: mod_muc/mod_muc_room.erl:543 mod_muc/mod_muc_room.erl:611 -msgid "Recipient is not in the conference room" -msgstr "De ontvanger is niet in de chatruimte" - -#: mod_muc/mod_muc_room.erl:564 mod_muc/mod_muc_room.erl:585 -msgid "It is not allowed to send private messages" -msgstr "Het is niet toegestaan priveberichten te sturen" - -#: mod_muc/mod_muc_room.erl:576 mod_muc/mod_muc_room.erl:956 -#: mod_muc/mod_muc_room.erl:4040 -msgid "Only occupants are allowed to send messages to the conference" -msgstr "Alleen aanwezigen mogen berichten naar de chatruimte verzenden" - -#: mod_muc/mod_muc_room.erl:634 -msgid "Only occupants are allowed to send queries to the conference" -msgstr "Alleen aanwezigen mogen verzoeken verzenden naar de chatruimte" - -#: mod_muc/mod_muc_room.erl:646 -msgid "Queries to the conference members are not allowed in this room" -msgstr "" -"Er mogen geen verzoeken verzenden worden naar deelnemers in deze chatruimte" - -#: mod_muc/mod_muc_room.erl:932 -msgid "" -"Only moderators and participants are allowed to change the subject in this " -"room" -msgstr "" -"Alleen moderators en deelnemers mogen het onderwerp van deze chatruimte " -"veranderen" - -#: mod_muc/mod_muc_room.erl:937 -msgid "Only moderators are allowed to change the subject in this room" -msgstr "Alleen moderators mogen het onderwerp van deze chatruimte veranderen" - -#: mod_muc/mod_muc_room.erl:947 -msgid "Visitors are not allowed to send messages to all occupants" -msgstr "Bezoekers mogen geen berichten verzenden naar alle aanwezigen" - -#: mod_muc/mod_muc_room.erl:1021 -msgid "" -"This participant is kicked from the room because he sent an error presence" -msgstr "" -"Deze deelnemer wordt weggestuurd vanwege het sturen van een foutmelding-" -"aanwezigheid" - -#: mod_muc/mod_muc_room.erl:1040 -msgid "Visitors are not allowed to change their nicknames in this room" -msgstr "Het is bezoekers niet toegestaan hun naam te veranderen in dit kanaal" - -#: mod_muc/mod_muc_room.erl:1053 mod_muc/mod_muc_room.erl:1779 -msgid "That nickname is already in use by another occupant" -msgstr "Deze bijnaam is al in gebruik door een andere aanwezige" - -#: mod_muc/mod_muc_room.erl:1768 -msgid "You have been banned from this room" -msgstr "U werd verbannen uit deze chatruimte" - -#: mod_muc/mod_muc_room.erl:1771 -msgid "Membership is required to enter this room" -msgstr "U moet lid zijn om deze chatruimte te kunnen betreden" - -#: mod_muc/mod_muc_room.erl:1807 -msgid "This room is not anonymous" -msgstr "Deze chatruimte is niet anoniem" - -#: mod_muc/mod_muc_room.erl:1833 -msgid "A password is required to enter this room" -msgstr "U hebt een wachtwoord nodig om deze chatruimte te kunnen betreden" - -#: mod_muc/mod_muc_room.erl:1855 mod_register.erl:246 -msgid "Too many CAPTCHA requests" -msgstr "Te veel CAPTCHA-aanvragen" - -#: mod_muc/mod_muc_room.erl:1864 mod_register.erl:251 -msgid "Unable to generate a CAPTCHA" -msgstr "Het generen van een CAPTCHA is mislukt" - -#: mod_muc/mod_muc_room.erl:1874 -msgid "Incorrect password" -msgstr "Foutief wachtwoord" - -#: mod_muc/mod_muc_room.erl:2448 -msgid "Administrator privileges required" -msgstr "U hebt beheerdersprivileges nodig" - -#: mod_muc/mod_muc_room.erl:2463 -msgid "Moderator privileges required" -msgstr "U hebt moderatorprivileges nodig" - -#: mod_muc/mod_muc_room.erl:2619 -msgid "Jabber ID ~s is invalid" -msgstr "De Jabber ID ~s is ongeldig" - -#: mod_muc/mod_muc_room.erl:2633 -msgid "Nickname ~s does not exist in the room" -msgstr "De bijnaam ~s bestaat niet in deze chatruimte" - -#: mod_muc/mod_muc_room.erl:2659 mod_muc/mod_muc_room.erl:3049 -msgid "Invalid affiliation: ~s" -msgstr "Ongeldige affiliatie: ~s" - -#: mod_muc/mod_muc_room.erl:2713 -msgid "Invalid role: ~s" -msgstr "Ongeldige rol: ~s" - -#: mod_muc/mod_muc_room.erl:3026 mod_muc/mod_muc_room.erl:3062 -msgid "Owner privileges required" -msgstr "U hebt eigenaarsprivileges nodig" - -#: mod_muc/mod_muc_room.erl:3195 -msgid "Configuration of room ~s" -msgstr "Instellingen van chatruimte ~s" - -#: mod_muc/mod_muc_room.erl:3200 -msgid "Room title" -msgstr "Naam van de chatruimte" - -#: mod_muc/mod_muc_room.erl:3203 mod_muc/mod_muc_room.erl:3692 -msgid "Room description" -msgstr "Beschrijving" - -#: mod_muc/mod_muc_room.erl:3210 -msgid "Make room persistent" -msgstr "Chatruimte blijvend maken" - -#: mod_muc/mod_muc_room.erl:3215 -msgid "Make room public searchable" -msgstr "Chatruimte doorzoekbaar maken" - -#: mod_muc/mod_muc_room.erl:3218 -msgid "Make participants list public" -msgstr "Deelnemerslijst publiek maken" - -#: mod_muc/mod_muc_room.erl:3221 -msgid "Make room password protected" -msgstr "Chatruimte beveiligen met een wachtwoord" - -#: mod_muc/mod_muc_room.erl:3232 -msgid "Maximum Number of Occupants" -msgstr "Maximum aantal aanwezigen" - -#: mod_muc/mod_muc_room.erl:3239 -msgid "No limit" -msgstr "Geen limiet" - -#: mod_muc/mod_muc_room.erl:3250 -msgid "Present real Jabber IDs to" -msgstr "Jabber ID's kunnen achterhaald worden door" - -#: mod_muc/mod_muc_room.erl:3258 mod_muc/mod_muc_room.erl:3292 -msgid "moderators only" -msgstr "moderators" - -#: mod_muc/mod_muc_room.erl:3260 mod_muc/mod_muc_room.erl:3294 -msgid "anyone" -msgstr "iedereen" - -#: mod_muc/mod_muc_room.erl:3262 -msgid "Make room members-only" -msgstr "Chatruimte enkel toegankelijk maken voor leden" - -#: mod_muc/mod_muc_room.erl:3265 -msgid "Make room moderated" -msgstr "Chatruimte gemodereerd maken" - -#: mod_muc/mod_muc_room.erl:3268 -msgid "Default users as participants" -msgstr "Gebruikers standaard instellen als deelnemers" - -#: mod_muc/mod_muc_room.erl:3271 -msgid "Allow users to change the subject" -msgstr "Sta gebruikers toe het onderwerp te veranderen" - -#: mod_muc/mod_muc_room.erl:3274 -msgid "Allow users to send private messages" -msgstr "Gebruikers mogen privéberichten verzenden" - -#: mod_muc/mod_muc_room.erl:3279 -msgid "Allow visitors to send private messages to" -msgstr "Gebruikers mogen privéberichten verzenden aan" - -#: mod_muc/mod_muc_room.erl:3290 -msgid "nobody" -msgstr "niemand" - -#: mod_muc/mod_muc_room.erl:3296 -msgid "Allow users to query other users" -msgstr "Gebruikers mogen naar andere gebruikers verzoeken verzenden" - -#: mod_muc/mod_muc_room.erl:3299 -msgid "Allow users to send invites" -msgstr "Gebruikers mogen uitnodigingen verzenden" - -#: mod_muc/mod_muc_room.erl:3302 -msgid "Allow visitors to send status text in presence updates" -msgstr "Sta bezoekers toe hun statusbericht in te stellen" - -#: mod_muc/mod_muc_room.erl:3305 -msgid "Allow visitors to change nickname" -msgstr "Sta bezoekers toe hun naam te veranderen" - -#: mod_muc/mod_muc_room.erl:3308 -msgid "Allow visitors to send voice requests" -msgstr "Gebruikers mogen stemaanvragen verzenden" - -#: mod_muc/mod_muc_room.erl:3311 -msgid "Minimum interval between voice requests (in seconds)" -msgstr "Minimale interval tussen stemaanvragen (in seconden)" - -#: mod_muc/mod_muc_room.erl:3317 -msgid "Make room CAPTCHA protected" -msgstr "Chatruimte beveiligen met een geautomatiseerde Turing test" - -#: mod_muc/mod_muc_room.erl:3322 -msgid "Exclude Jabber IDs from CAPTCHA challenge" -msgstr "Geen CAPTCHA test voor Jabber IDs" - -#: mod_muc/mod_muc_room.erl:3329 -msgid "Enable logging" -msgstr "Logs aanzetten" - -#: mod_muc/mod_muc_room.erl:3337 -msgid "You need an x:data capable client to configure room" -msgstr "" -"U hebt een client nodig die x:data ondersteunt om deze chatruimte in te " -"stellen" - -#: mod_muc/mod_muc_room.erl:3694 -msgid "Number of occupants" -msgstr "Aantal aanwezigen" - -#: mod_muc/mod_muc_room.erl:3750 -msgid "private, " -msgstr "privé, " - -#: mod_muc/mod_muc_room.erl:3799 -msgid "Voice request" -msgstr "Stemaanvraag" - -#: mod_muc/mod_muc_room.erl:3803 -msgid "Either approve or decline the voice request." -msgstr "Keur stemaanvraag goed of af." - -#: mod_muc/mod_muc_room.erl:3809 -msgid "User JID" -msgstr "JID Gebruiker" - -#: mod_muc/mod_muc_room.erl:3811 -msgid "Grant voice to this person?" -msgstr "Stemaanvraag honoreren voor deze persoon?" - -#: mod_muc/mod_muc_room.erl:3960 -msgid "~s invites you to the room ~s" -msgstr "~s nodigt je uit voor het groepsgesprek ~s" - -#: mod_muc/mod_muc_room.erl:3969 -msgid "the password is" -msgstr "het wachtwoord is" - -#: mod_offline.erl:510 mod_offline_odbc.erl:352 -msgid "" -"Your contact offline message queue is full. The message has been discarded." -msgstr "" -"Te veel offline berichten voor dit contactpersoon. Het bericht is niet " -"opgeslagen." - -#: mod_offline.erl:560 mod_offline_odbc.erl:408 -msgid "~s's Offline Messages Queue" -msgstr "offline berichten van ~s" - -#: mod_offline.erl:563 mod_offline_odbc.erl:411 mod_roster.erl:987 -#: mod_roster_odbc.erl:1104 mod_shared_roster.erl:901 -#: mod_shared_roster.erl:1010 mod_shared_roster_odbc.erl:930 -#: mod_shared_roster_odbc.erl:1039 web/ejabberd_web_admin.erl:833 -#: web/ejabberd_web_admin.erl:874 web/ejabberd_web_admin.erl:942 -#: web/ejabberd_web_admin.erl:978 web/ejabberd_web_admin.erl:1019 -#: web/ejabberd_web_admin.erl:1508 web/ejabberd_web_admin.erl:1760 -#: web/ejabberd_web_admin.erl:1931 web/ejabberd_web_admin.erl:1963 -#: web/ejabberd_web_admin.erl:2031 web/ejabberd_web_admin.erl:2135 -#: web/ejabberd_web_admin.erl:2160 web/ejabberd_web_admin.erl:2248 -msgid "Submitted" -msgstr "Verzonden" - -#: mod_offline.erl:571 -msgid "Time" -msgstr "Tijd" - -#: mod_offline.erl:572 -msgid "From" -msgstr "Van" - -#: mod_offline.erl:573 -msgid "To" -msgstr "Aan" - -#: mod_offline.erl:574 mod_offline_odbc.erl:419 -msgid "Packet" -msgstr "Pakket" - -#: mod_offline.erl:587 mod_offline_odbc.erl:432 mod_shared_roster.erl:908 -#: mod_shared_roster_odbc.erl:937 web/ejabberd_web_admin.erl:882 -#: web/ejabberd_web_admin.erl:986 -msgid "Delete Selected" -msgstr "Geselecteerde verwijderen" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -msgid "Offline Messages:" -msgstr "Offline berichten:" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -msgid "Remove All Offline Messages" -msgstr "Verwijder alle offline berichten" - -#: mod_proxy65/mod_proxy65_service.erl:213 -msgid "ejabberd SOCKS5 Bytestreams module" -msgstr "ejabberd SOCKS5 Bytestreams module" - -#: mod_pubsub/mod_pubsub.erl:1120 mod_pubsub/mod_pubsub_odbc.erl:929 -msgid "Publish-Subscribe" -msgstr "Publish-Subscribe" - -#: mod_pubsub/mod_pubsub.erl:1214 mod_pubsub/mod_pubsub_odbc.erl:1025 -msgid "ejabberd Publish-Subscribe module" -msgstr "ejabberd Publish-Subscribe module" - -#: mod_pubsub/mod_pubsub.erl:1497 mod_pubsub/mod_pubsub_odbc.erl:1312 -msgid "PubSub subscriber request" -msgstr "PubSub abonnee verzoek" - -#: mod_pubsub/mod_pubsub.erl:1499 mod_pubsub/mod_pubsub_odbc.erl:1314 -msgid "Choose whether to approve this entity's subscription." -msgstr "Beslis of dit verzoek tot abonneren zal worden goedgekeurd" - -#: mod_pubsub/mod_pubsub.erl:1505 mod_pubsub/mod_pubsub_odbc.erl:1320 -msgid "Node ID" -msgstr "Node ID" - -#: mod_pubsub/mod_pubsub.erl:1510 mod_pubsub/mod_pubsub_odbc.erl:1325 -msgid "Subscriber Address" -msgstr "Abonnee Adres" - -#: mod_pubsub/mod_pubsub.erl:1516 mod_pubsub/mod_pubsub_odbc.erl:1331 -msgid "Allow this Jabber ID to subscribe to this pubsub node?" -msgstr "Deze gebruiker toestaan te abonneren op deze pubsub node?" - -#: mod_pubsub/mod_pubsub.erl:3474 mod_pubsub/mod_pubsub_odbc.erl:3287 -msgid "Deliver payloads with event notifications" -msgstr "Berichten bezorgen samen met gebeurtenisnotificaties" - -#: mod_pubsub/mod_pubsub.erl:3475 mod_pubsub/mod_pubsub_odbc.erl:3288 -msgid "Deliver event notifications" -msgstr "Gebeurtenisbevestigingen Sturen" - -#: mod_pubsub/mod_pubsub.erl:3476 mod_pubsub/mod_pubsub_odbc.erl:3289 -msgid "Notify subscribers when the node configuration changes" -msgstr "Abonnees informeren wanneer de instellingen van de node veranderen" - -#: mod_pubsub/mod_pubsub.erl:3477 mod_pubsub/mod_pubsub_odbc.erl:3290 -msgid "Notify subscribers when the node is deleted" -msgstr "Abonnees informeren wanneer de node verwijderd word" - -#: mod_pubsub/mod_pubsub.erl:3478 mod_pubsub/mod_pubsub_odbc.erl:3291 -msgid "Notify subscribers when items are removed from the node" -msgstr "Abonnees informeren wanneer items verwijderd worden uit de node" - -#: mod_pubsub/mod_pubsub.erl:3479 mod_pubsub/mod_pubsub_odbc.erl:3292 -msgid "Persist items to storage" -msgstr "Items in het geheugen bewaren" - -#: mod_pubsub/mod_pubsub.erl:3480 mod_pubsub/mod_pubsub_odbc.erl:3293 -msgid "A friendly name for the node" -msgstr "Bijnaam voor deze knoop" - -#: mod_pubsub/mod_pubsub.erl:3481 mod_pubsub/mod_pubsub_odbc.erl:3294 -msgid "Max # of items to persist" -msgstr "Maximum aantal in het geheugen te bewaren items" - -#: mod_pubsub/mod_pubsub.erl:3482 mod_pubsub/mod_pubsub_odbc.erl:3295 -msgid "Whether to allow subscriptions" -msgstr "Abonnementsaanvraag toestaan" - -#: mod_pubsub/mod_pubsub.erl:3483 mod_pubsub/mod_pubsub_odbc.erl:3296 -msgid "Specify the access model" -msgstr "Geef toegangsmodel" - -#: mod_pubsub/mod_pubsub.erl:3486 mod_pubsub/mod_pubsub_odbc.erl:3299 -msgid "Roster groups allowed to subscribe" -msgstr "Contactlijst-groepen die mogen abonneren" - -#: mod_pubsub/mod_pubsub.erl:3487 mod_pubsub/mod_pubsub_odbc.erl:3300 -msgid "Specify the publisher model" -msgstr "Publicatietype opgeven" - -#: mod_pubsub/mod_pubsub.erl:3489 mod_pubsub/mod_pubsub_odbc.erl:3302 -msgid "Purge all items when the relevant publisher goes offline" -msgstr "Verwijder alle items wanneer de gerelateerde publiceerder offline gaat" - -#: mod_pubsub/mod_pubsub.erl:3490 mod_pubsub/mod_pubsub_odbc.erl:3303 -msgid "Specify the event message type" -msgstr "Geef type van eventbericht" - -#: mod_pubsub/mod_pubsub.erl:3492 mod_pubsub/mod_pubsub_odbc.erl:3305 -msgid "Max payload size in bytes" -msgstr "Maximumgrootte van bericht in bytes" - -#: mod_pubsub/mod_pubsub.erl:3493 mod_pubsub/mod_pubsub_odbc.erl:3306 -msgid "When to send the last published item" -msgstr "Wanneer het laatst gepubliceerde item verzonden moet worden" - -#: mod_pubsub/mod_pubsub.erl:3495 mod_pubsub/mod_pubsub_odbc.erl:3308 -msgid "Only deliver notifications to available users" -msgstr "Notificaties alleen verzenden naar online gebruikers" - -#: mod_pubsub/mod_pubsub.erl:3496 mod_pubsub/mod_pubsub_odbc.erl:3309 -msgid "The collections with which a node is affiliated" -msgstr "De collecties waar een node mee is gerelateerd" - -#: mod_register.erl:193 -msgid "The CAPTCHA verification has failed" -msgstr "De CAPTCHA-verificatie is mislukt" - -#: mod_register.erl:220 -msgid "You need a client that supports x:data and CAPTCHA to register" -msgstr "" -"U hebt een client nodig die x:data en CAPTCHA ondersteunt om een bijnaam te " -"registreren" - -#: mod_register.erl:226 mod_register.erl:265 -msgid "Choose a username and password to register with this server" -msgstr "" -"Kies een gebruikersnaam en een wachtwoord om u te registreren op deze server" - -#: mod_register.erl:230 mod_vcard.erl:364 mod_vcard_odbc.erl:342 -#: web/ejabberd_web_admin.erl:1515 web/ejabberd_web_admin.erl:1572 -msgid "User" -msgstr "Gebruiker" - -#: mod_register.erl:316 mod_register.erl:361 -msgid "The password is too weak" -msgstr "Het wachtwoord is te zwak" - -#: mod_register.erl:365 -msgid "Users are not allowed to register accounts so quickly" -msgstr "Het is gebruikers niet toegestaan zo snel achter elkaar te registreren" - -#: mod_roster.erl:938 mod_roster_odbc.erl:1055 web/ejabberd_web_admin.erl:1701 -#: web/ejabberd_web_admin.erl:1886 web/ejabberd_web_admin.erl:1897 -#: web/ejabberd_web_admin.erl:2219 -msgid "None" -msgstr "Geen" - -#: mod_roster.erl:945 mod_roster_odbc.erl:1062 -msgid "Subscription" -msgstr "Inschrijving" - -#: mod_roster.erl:946 mod_roster_odbc.erl:1063 -msgid "Pending" -msgstr "Bezig" - -#: mod_roster.erl:947 mod_roster_odbc.erl:1064 -msgid "Groups" -msgstr "Groepen" - -#: mod_roster.erl:974 mod_roster_odbc.erl:1091 -msgid "Validate" -msgstr "Bevestigen" - -#: mod_roster.erl:982 mod_roster_odbc.erl:1099 -msgid "Remove" -msgstr "Verwijderen" - -#: mod_roster.erl:985 mod_roster_odbc.erl:1102 -msgid "Roster of " -msgstr "Roster van " - -#: mod_roster.erl:988 mod_roster_odbc.erl:1105 mod_shared_roster.erl:902 -#: mod_shared_roster.erl:1011 mod_shared_roster_odbc.erl:931 -#: mod_shared_roster_odbc.erl:1040 web/ejabberd_web_admin.erl:834 -#: web/ejabberd_web_admin.erl:875 web/ejabberd_web_admin.erl:943 -#: web/ejabberd_web_admin.erl:979 web/ejabberd_web_admin.erl:1020 -#: web/ejabberd_web_admin.erl:1509 web/ejabberd_web_admin.erl:1761 -#: web/ejabberd_web_admin.erl:1932 web/ejabberd_web_admin.erl:2136 -#: web/ejabberd_web_admin.erl:2161 -msgid "Bad format" -msgstr "Slecht formaat" - -#: mod_roster.erl:995 mod_roster_odbc.erl:1112 -msgid "Add Jabber ID" -msgstr "Jabber ID toevoegen" - -#: mod_roster.erl:1094 mod_roster_odbc.erl:1211 -msgid "Roster" -msgstr "Roster" - -#: mod_shared_roster.erl:857 mod_shared_roster.erl:899 -#: mod_shared_roster.erl:1007 mod_shared_roster_odbc.erl:886 -#: mod_shared_roster_odbc.erl:928 mod_shared_roster_odbc.erl:1036 -msgid "Shared Roster Groups" -msgstr "Gedeelde rostergroepen" - -#: mod_shared_roster.erl:895 mod_shared_roster_odbc.erl:924 -#: web/ejabberd_web_admin.erl:1365 web/ejabberd_web_admin.erl:2461 -msgid "Add New" -msgstr "Toevoegen" - -#: mod_shared_roster.erl:978 mod_shared_roster_odbc.erl:1007 -msgid "Name:" -msgstr "Naam:" - -#: mod_shared_roster.erl:983 mod_shared_roster_odbc.erl:1012 -msgid "Description:" -msgstr "Beschrijving:" - -#: mod_shared_roster.erl:991 mod_shared_roster_odbc.erl:1020 -msgid "Members:" -msgstr "Groepsleden:" - -#: mod_shared_roster.erl:999 mod_shared_roster_odbc.erl:1028 -msgid "Displayed Groups:" -msgstr "Weergegeven groepen:" - -#: mod_shared_roster.erl:1008 mod_shared_roster_odbc.erl:1037 -msgid "Group " -msgstr "Groep " - -#: mod_shared_roster.erl:1017 mod_shared_roster_odbc.erl:1046 -#: web/ejabberd_web_admin.erl:840 web/ejabberd_web_admin.erl:884 -#: web/ejabberd_web_admin.erl:949 web/ejabberd_web_admin.erl:1026 -#: web/ejabberd_web_admin.erl:2017 -msgid "Submit" -msgstr "Verzenden" - -#: mod_vcard.erl:165 mod_vcard_ldap.erl:238 mod_vcard_odbc.erl:129 -msgid "Erlang Jabber Server" -msgstr "Erlang Jabber Server" - -#: mod_vcard.erl:364 mod_vcard.erl:478 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:454 -msgid "Birthday" -msgstr "Geboortedatum" - -#: mod_vcard.erl:364 mod_vcard.erl:480 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:456 -msgid "City" -msgstr "Plaats" - -#: mod_vcard.erl:364 mod_vcard.erl:479 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:455 -msgid "Country" -msgstr "Land" - -#: mod_vcard.erl:364 mod_vcard.erl:481 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:457 -msgid "Email" -msgstr "E-mail" - -#: mod_vcard.erl:364 mod_vcard.erl:476 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:452 -msgid "Family Name" -msgstr "Achternaam" - -#: mod_vcard.erl:364 mod_vcard_odbc.erl:342 -msgid "" -"Fill in the form to search for any matching Jabber User (Add * to the end of " -"field to match substring)" -msgstr "" -"Gebruik de velden om te zoeken (Voeg achteraan het teken * toe om te zoeken " -"naar alles wat met het eerste deel begint.)." - -#: mod_vcard.erl:364 mod_vcard.erl:473 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:449 -msgid "Full Name" -msgstr "Volledige naam" - -#: mod_vcard.erl:364 mod_vcard.erl:475 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:451 -msgid "Middle Name" -msgstr "Tussennaam" - -#: mod_vcard.erl:364 mod_vcard.erl:474 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:450 web/ejabberd_web_admin.erl:2006 -msgid "Name" -msgstr "Naam" - -#: mod_vcard.erl:364 mod_vcard.erl:482 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:458 -msgid "Organization Name" -msgstr "Organisatie" - -#: mod_vcard.erl:364 mod_vcard.erl:483 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:459 -msgid "Organization Unit" -msgstr "Afdeling" - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "Search users in " -msgstr "Gebruikers zoeken in " - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "You need an x:data capable client to search" -msgstr "U hebt een client nodig die x:data ondersteunt om te zoeken" - -#: mod_vcard.erl:389 mod_vcard_ldap.erl:487 mod_vcard_odbc.erl:367 -msgid "vCard User Search" -msgstr "Gebruikers zoeken" - -#: mod_vcard.erl:445 mod_vcard_ldap.erl:541 mod_vcard_odbc.erl:421 -msgid "ejabberd vCard module" -msgstr "ejabberd's vCard-module" - -#: mod_vcard.erl:469 mod_vcard_ldap.erl:551 mod_vcard_odbc.erl:445 -msgid "Search Results for " -msgstr "Zoekresultaten voor " - -#: mod_vcard_ldap.erl:462 -msgid "Fill in fields to search for any matching Jabber User" -msgstr "Vul de velden in om te zoeken naar Jabber-gebruikers op deze server" - -#: web/ejabberd_web_admin.erl:193 web/ejabberd_web_admin.erl:203 -#: web/ejabberd_web_admin.erl:219 web/ejabberd_web_admin.erl:229 -msgid "Unauthorized" -msgstr "Niet geautoriseerd" - -#: web/ejabberd_web_admin.erl:286 web/ejabberd_web_admin.erl:303 -msgid "ejabberd Web Admin" -msgstr "ejabberd Webbeheer" - -#: web/ejabberd_web_admin.erl:769 web/ejabberd_web_admin.erl:780 -msgid "Administration" -msgstr "Beheer" - -#: web/ejabberd_web_admin.erl:878 web/ejabberd_web_admin.erl:982 -msgid "Raw" -msgstr "Ruw" - -#: web/ejabberd_web_admin.erl:1017 -msgid "~s access rule configuration" -msgstr "Access rules op ~s" - -#: web/ejabberd_web_admin.erl:1035 -msgid "Virtual Hosts" -msgstr "Virtuele hosts" - -#: web/ejabberd_web_admin.erl:1043 web/ejabberd_web_admin.erl:1050 -msgid "Users" -msgstr "Gebruikers" - -#: web/ejabberd_web_admin.erl:1078 -msgid "Users Last Activity" -msgstr "Laatste activiteit van gebruikers" - -#: web/ejabberd_web_admin.erl:1080 -msgid "Period: " -msgstr "Periode: " - -#: web/ejabberd_web_admin.erl:1090 -msgid "Last month" -msgstr "Afgelopen maand" - -#: web/ejabberd_web_admin.erl:1091 -msgid "Last year" -msgstr "Afgelopen jaar" - -#: web/ejabberd_web_admin.erl:1092 -msgid "All activity" -msgstr "Alle activiteit" - -#: web/ejabberd_web_admin.erl:1094 -msgid "Show Ordinary Table" -msgstr "Deel van tabel laten zien" - -#: web/ejabberd_web_admin.erl:1096 -msgid "Show Integral Table" -msgstr "Volledige tabel laten zien" - -#: web/ejabberd_web_admin.erl:1105 web/ejabberd_web_admin.erl:1939 -msgid "Statistics" -msgstr "Statistieken" - -#: web/ejabberd_web_admin.erl:1117 -msgid "Not Found" -msgstr "Niet gevonden" - -#: web/ejabberd_web_admin.erl:1134 -msgid "Node not found" -msgstr "Node niet gevonden" - -#: web/ejabberd_web_admin.erl:1460 -msgid "Host" -msgstr "Host" - -#: web/ejabberd_web_admin.erl:1461 -msgid "Registered Users" -msgstr "Geregistreerde gebruikers" - -#: web/ejabberd_web_admin.erl:1573 -msgid "Offline Messages" -msgstr "Offline berichten" - -#: web/ejabberd_web_admin.erl:1574 web/ejabberd_web_admin.erl:1767 -msgid "Last Activity" -msgstr "Laatste activiteit" - -#: web/ejabberd_web_admin.erl:1659 web/ejabberd_web_admin.erl:1675 -msgid "Registered Users:" -msgstr "Geregistreerde gebruikers:" - -#: web/ejabberd_web_admin.erl:1661 web/ejabberd_web_admin.erl:1677 -#: web/ejabberd_web_admin.erl:2192 -msgid "Online Users:" -msgstr "Online gebruikers:" - -#: web/ejabberd_web_admin.erl:1663 -msgid "Outgoing s2s Connections:" -msgstr "Uitgaande s2s-verbindingen:" - -#: web/ejabberd_web_admin.erl:1665 -msgid "Outgoing s2s Servers:" -msgstr "Uitgaande s2s-verbindingen:" - -#: web/ejabberd_web_admin.erl:1734 web/mod_register_web.erl:175 -#: web/mod_register_web.erl:372 web/mod_register_web.erl:381 -#: web/mod_register_web.erl:411 -msgid "Change Password" -msgstr "Wachtwoord wijzigen" - -#: web/ejabberd_web_admin.erl:1758 -msgid "User " -msgstr "Gebruiker " - -#: web/ejabberd_web_admin.erl:1765 -msgid "Connected Resources:" -msgstr "Verbonden bronnen:" - -#: web/ejabberd_web_admin.erl:1766 web/mod_register_web.erl:227 -#: web/mod_register_web.erl:519 -msgid "Password:" -msgstr "Wachtwoord:" - -#: web/ejabberd_web_admin.erl:1828 -msgid "No Data" -msgstr "Geen gegevens" - -#: web/ejabberd_web_admin.erl:1906 -msgid "Nodes" -msgstr "Nodes" - -#: web/ejabberd_web_admin.erl:1929 web/ejabberd_web_admin.erl:1951 -msgid "Node " -msgstr "Node " - -#: web/ejabberd_web_admin.erl:1938 -msgid "Listened Ports" -msgstr "Openstaande poorten" - -#: web/ejabberd_web_admin.erl:1940 web/ejabberd_web_admin.erl:2260 -#: web/ejabberd_web_admin.erl:2448 -msgid "Update" -msgstr "Bijwerken" - -#: web/ejabberd_web_admin.erl:1943 web/ejabberd_web_admin.erl:2569 -msgid "Restart" -msgstr "Herstarten" - -#: web/ejabberd_web_admin.erl:1945 web/ejabberd_web_admin.erl:2571 -msgid "Stop" -msgstr "Stoppen" - -#: web/ejabberd_web_admin.erl:1959 -msgid "RPC Call Error" -msgstr "RPC-oproepfout" - -#: web/ejabberd_web_admin.erl:2000 -msgid "Database Tables at " -msgstr "Databasetabellen van " - -#: web/ejabberd_web_admin.erl:2007 -msgid "Storage Type" -msgstr "Opslagmethode" - -#: web/ejabberd_web_admin.erl:2008 -msgid "Elements" -msgstr "Elementen" - -#: web/ejabberd_web_admin.erl:2009 -msgid "Memory" -msgstr "Geheugen" - -#: web/ejabberd_web_admin.erl:2032 web/ejabberd_web_admin.erl:2137 -msgid "Error" -msgstr "Fout" - -#: web/ejabberd_web_admin.erl:2034 -msgid "Backup of " -msgstr "Backup maken van " - -#: web/ejabberd_web_admin.erl:2036 -msgid "" -"Please note that these options will only backup the builtin Mnesia database. " -"If you are using the ODBC module, you also need to backup your SQL database " -"separately." -msgstr "" -"Merk op dat volgende opties enkel backups maken van de ingebouwde database " -"Mnesia. Als U de ODBC module gebruikt dan moeten daarvan afzonderlijke " -"backups gemaakt worden." - -#: web/ejabberd_web_admin.erl:2041 -msgid "Store binary backup:" -msgstr "Binaire backup maken:" - -#: web/ejabberd_web_admin.erl:2045 web/ejabberd_web_admin.erl:2052 -#: web/ejabberd_web_admin.erl:2060 web/ejabberd_web_admin.erl:2067 -#: web/ejabberd_web_admin.erl:2074 web/ejabberd_web_admin.erl:2081 -#: web/ejabberd_web_admin.erl:2088 web/ejabberd_web_admin.erl:2096 -#: web/ejabberd_web_admin.erl:2103 web/ejabberd_web_admin.erl:2110 -msgid "OK" -msgstr "OK" - -#: web/ejabberd_web_admin.erl:2048 -msgid "Restore binary backup immediately:" -msgstr "Binaire backup direct herstellen:" - -#: web/ejabberd_web_admin.erl:2056 -msgid "" -"Restore binary backup after next ejabberd restart (requires less memory):" -msgstr "" -"Binaire backup herstellen na herstart van ejabberd (vereist minder geheugen):" - -#: web/ejabberd_web_admin.erl:2063 -msgid "Store plain text backup:" -msgstr "Backup naar een tekstbestand schrijven:" - -#: web/ejabberd_web_admin.erl:2070 -msgid "Restore plain text backup immediately:" -msgstr "Backup in een tekstbestand direct herstellen:" - -#: web/ejabberd_web_admin.erl:2077 -msgid "Import users data from a PIEFXIS file (XEP-0227):" -msgstr "Importeer gebruikersdata van een PIEFXIS-bestand (XEP-0227):" - -#: web/ejabberd_web_admin.erl:2084 -msgid "Export data of all users in the server to PIEFXIS files (XEP-0227):" -msgstr "" -"Exporteer data van alle gebruikers in de server naar PIEFXIS-bestanden " -"(XEP-0227):" - -#: web/ejabberd_web_admin.erl:2091 -msgid "Export data of users in a host to PIEFXIS files (XEP-0227):" -msgstr "" -"Exporteer data van alle gebruikers van een host naar PIEXFIS-bestanden " -"(XEP-0227):" - -#: web/ejabberd_web_admin.erl:2099 -msgid "Import user data from jabberd14 spool file:" -msgstr "Importeer gebruikersdata via spool-bestanden van jabberd14" - -#: web/ejabberd_web_admin.erl:2106 -msgid "Import users data from jabberd14 spool directory:" -msgstr "Importeer gebruikersdata via spool-bestanden van jabberd14" - -#: web/ejabberd_web_admin.erl:2132 -msgid "Listened Ports at " -msgstr "Openstaande poorten op " - -#: web/ejabberd_web_admin.erl:2157 -msgid "Modules at " -msgstr "Modules op " - -#: web/ejabberd_web_admin.erl:2183 -msgid "Statistics of ~p" -msgstr "Statistieken van ~p" - -#: web/ejabberd_web_admin.erl:2186 -msgid "Uptime:" -msgstr "Uptime:" - -#: web/ejabberd_web_admin.erl:2189 -msgid "CPU Time:" -msgstr "Processortijd:" - -#: web/ejabberd_web_admin.erl:2195 -msgid "Transactions Committed:" -msgstr "Bevestigde transacties:" - -#: web/ejabberd_web_admin.erl:2198 -msgid "Transactions Aborted:" -msgstr "Afgebroken transacties:" - -#: web/ejabberd_web_admin.erl:2201 -msgid "Transactions Restarted:" -msgstr "Herstarte transacties:" - -#: web/ejabberd_web_admin.erl:2204 -msgid "Transactions Logged:" -msgstr "Gelogde transacties:" - -#: web/ejabberd_web_admin.erl:2246 -msgid "Update " -msgstr "Opwaarderen van " - -#: web/ejabberd_web_admin.erl:2254 -msgid "Update plan" -msgstr "Plan voor de opwaardering" - -#: web/ejabberd_web_admin.erl:2255 -msgid "Modified modules" -msgstr "Gewijzigde modules" - -#: web/ejabberd_web_admin.erl:2256 -msgid "Update script" -msgstr "Script voor de opwaardering" - -#: web/ejabberd_web_admin.erl:2257 -msgid "Low level update script" -msgstr "Lowlevel script voor de opwaardering" - -#: web/ejabberd_web_admin.erl:2258 -msgid "Script check" -msgstr "Controle van script" - -#: web/ejabberd_web_admin.erl:2426 -msgid "Port" -msgstr "Poort" - -#: web/ejabberd_web_admin.erl:2427 -msgid "IP" -msgstr "IP" - -#: web/ejabberd_web_admin.erl:2428 -msgid "Protocol" -msgstr "Protocol" - -#: web/ejabberd_web_admin.erl:2429 web/ejabberd_web_admin.erl:2556 -msgid "Module" -msgstr "Module" - -#: web/ejabberd_web_admin.erl:2430 web/ejabberd_web_admin.erl:2557 -msgid "Options" -msgstr "Opties" - -#: web/ejabberd_web_admin.erl:2450 -msgid "Delete" -msgstr "Verwijderen" - -#: web/ejabberd_web_admin.erl:2579 -msgid "Start" -msgstr "Starten" - -#: web/mod_register_web.erl:104 -msgid "Your Jabber account was successfully created." -msgstr "Uw Jabber-account is succesvol gecreeerd." - -#: web/mod_register_web.erl:107 -msgid "There was an error creating the account: " -msgstr "Er was een fout bij het creeern van de account:" - -#: web/mod_register_web.erl:115 -msgid "Your Jabber account was successfully deleted." -msgstr "Uw Jabber-account is succesvol verwijderd." - -#: web/mod_register_web.erl:118 -msgid "There was an error deleting the account: " -msgstr "Er was een fout bij het verwijderen van de account." - -#: web/mod_register_web.erl:128 -msgid "The password of your Jabber account was successfully changed." -msgstr "Het wachtwoord van Uw Jabber-account is succesvol veranderd." - -#: web/mod_register_web.erl:131 -msgid "There was an error changing the password: " -msgstr "Er was een fout bij het veranderen van het wachtwoord:" - -#: web/mod_register_web.erl:163 web/mod_register_web.erl:172 -msgid "Jabber Account Registration" -msgstr "Jabber-account registratie" - -#: web/mod_register_web.erl:174 web/mod_register_web.erl:192 -#: web/mod_register_web.erl:201 -msgid "Register a Jabber account" -msgstr "Registreer een Jabber-account" - -#: web/mod_register_web.erl:176 web/mod_register_web.erl:493 -#: web/mod_register_web.erl:502 -msgid "Unregister a Jabber account" -msgstr "Opheffen van Jabber-account" - -#: web/mod_register_web.erl:203 -msgid "" -"This page allows to create a Jabber account in this Jabber server. Your JID " -"(Jabber IDentifier) will be of the form: username@server. Please read " -"carefully the instructions to fill correctly the fields." -msgstr "" -"Deze pagina maakt het mogelijk een Jabber-account te registreren op deze " -"server. Uw JID (Jabber IDentiteit) zal er als volg uit zien: " -"gebruikersnaam@server. Lees de instructies zorgvuldig teneinde de velden " -"correct in te vullen." - -#: web/mod_register_web.erl:212 web/mod_register_web.erl:386 -#: web/mod_register_web.erl:509 -msgid "Username:" -msgstr "Gebruikersnaam:" - -#: web/mod_register_web.erl:217 -msgid "This is case insensitive: macbeth is the same that MacBeth and Macbeth." -msgstr "" -"Dit is niet hoofdlettergevoelig: macbeth is hetzelfde als MacBeth en Macbeth." - -#: web/mod_register_web.erl:218 -msgid "Characters not allowed:" -msgstr "Niet-toegestane karakters:" - -#: web/mod_register_web.erl:222 web/mod_register_web.erl:391 -#: web/mod_register_web.erl:514 -msgid "Server:" -msgstr "Server:" - -#: web/mod_register_web.erl:232 -msgid "" -"Don't tell your password to anybody, not even the administrators of the " -"Jabber server." -msgstr "" -"Geef Uw wachtwoord aan niemand, zelfs niet aan de beheerders van deze Jabber-" -"server." - -#: web/mod_register_web.erl:234 -msgid "You can later change your password using a Jabber client." -msgstr "U can het wachtwoord later veranderen met een Jabber-client." - -#: web/mod_register_web.erl:235 -msgid "" -"Some Jabber clients can store your password in your computer. Use that " -"feature only if you trust your computer is safe." -msgstr "" -"Sommige Jabber-clienten kunnen het wachtwoord opslaan op Uw computer. " -"Gebruik deze mogelijkheid alleen als U vertrouwd dat Uw computer afdoende " -"beveiligd is." - -#: web/mod_register_web.erl:237 -msgid "" -"Memorize your password, or write it in a paper placed in a safe place. In " -"Jabber there isn't an automated way to recover your password if you forget " -"it." -msgstr "" -"Onthou het wachtwoord, of schrijf het op en bewaar het op een veilige " -"plaats. Met Jabber is er geen geautomatiseerde manier om het wachtwoord " -"terug te halen als U het vergeet." - -#: web/mod_register_web.erl:242 web/mod_register_web.erl:406 -msgid "Password Verification:" -msgstr "Wachtwoord Bevestiging:" - -#: web/mod_register_web.erl:250 -msgid "Register" -msgstr "Registreer" - -#: web/mod_register_web.erl:396 -msgid "Old Password:" -msgstr "Oud Wachtwoord:" - -#: web/mod_register_web.erl:401 -msgid "New Password:" -msgstr "Nieuw Wachtwoord:" - -#: web/mod_register_web.erl:504 -msgid "This page allows to unregister a Jabber account in this Jabber server." -msgstr "" -"Deze pagina maakt het mogelijk een Jabber-account op deze server op te " -"heffen." - -#: web/mod_register_web.erl:524 -msgid "Unregister" -msgstr "Opheffen" - -#, fuzzy -#~ msgid "Captcha test failed" -#~ msgstr "De geautomatiseerde Turing-test is geslaagd." diff --git a/priv/msgs/no.msg b/priv/msgs/no.msg index febe781b8..b24b9eca9 100644 --- a/priv/msgs/no.msg +++ b/priv/msgs/no.msg @@ -1,421 +1,337 @@ -{"Access Configuration","Tilgangskonfigurasjon"}. -{"Access Control List Configuration","Konfigurasjon for Tilgangskontroll lister"}. -{"Access Control Lists","Tilgangskontrollister"}. -{"Access control lists","Tilgangskontroll lister"}. -{"Access denied by service policy","Tilgang nektes på grunn av en tjeneste regel"}. -{"Access rules","Tilgangsregler"}. -{"Access Rules","Tilgangsregler"}. -{"Action on user","Handling på bruker"}. -{"Add Jabber ID","Legg til Jabber ID"}. -{"Add New","Legg til ny"}. -{"Add User","Legg til Bruker"}. -{"Administration","Administrasjon"}. -{"Administration of ","Administrasjon av "}. -{"Administrator privileges required","Administratorprivilegier kreves"}. +%% Generated automatically +%% DO NOT EDIT: run `make translations` instead +%% To improve translations please read: +%% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ + +{" has set the subject to: "," har satt emnet til: "}. {"A friendly name for the node","Et vennlig navn for noden"}. +{"A password is required to enter this room","Et passord kreves for tilgang til samtalerommet"}. +{"Accept","Godta"}. +{"Access denied by service policy","Tilgang nektes på grunn av en tjenesteregel"}. +{"Action on user","Handling på bruker"}. +{"Add User","Legg til bruker"}. +{"Administration of ","Administrasjon av "}. +{"Administration","Administrasjon"}. +{"Administrator privileges required","Administratorprivilegier kreves"}. {"All activity","All aktivitet"}. -{"Allow this Jabber ID to subscribe to this pubsub node?","Tillat denne Jabber ID å abonnere på denne pubsub "}. -{"Allow users to change the subject","Tillat brukere å endre emne"}. -{"Allow users to query other users","Tillat brukere å sende forespørsel til andre brukere"}. +{"All Users","Alle brukere"}. +{"Allow this Jabber ID to subscribe to this pubsub node?","Tillat denne Jabber-ID-en å abonnere på denne pubsub -noden?"}. +{"Allow users to change the subject","Tillat brukere å endre emnet"}. +{"Allow users to query other users","Tillat brukere å sende forespørsler til andre brukere"}. {"Allow users to send invites","Tillat brukere å sende invitasjoner"}. {"Allow users to send private messages","Tillat brukere å sende private meldinger"}. {"Allow visitors to change nickname","Tillat besøkende å endre kallenavn"}. {"Allow visitors to send private messages to","Tillat brukere å sende private meldinger til"}. -{"Allow visitors to send status text in presence updates","Tillat besøkende å sende status tekst i "}. -{"Allow visitors to send voice requests","Tillat brukere å sende lyd forespørsler"}. -{"All Users","Alle Brukere"}. +{"Allow visitors to send status text in presence updates","Tillat besøkende å sende statustekst i tilstedeværelsesoppdateringer"}. +{"Allow visitors to send voice requests","Tillat brukere å sende lydforespørsler"}. {"Announcements","Kunngjøringer"}. -{"anyone","hvem som helst"}. -{"A password is required to enter this room","Et passord kreves for tilgang til samtalerommet"}. +{"Answer to a question","Svar på spørsmål"}. {"April","april"}. {"August","august"}. -{"Backup Management","Håndtere Sikkerehetskopiering"}. -{"Backup of ","Sikkerhetskopi av "}. -{"Backup","Sikkerhetskopier"}. -{"Backup to File at ","Sikkerhetskopiere til Fil på "}. -{"Bad format","Feil format"}. -{"Birthday","Fødselsdag"}. -{"CAPTCHA web page","CAPTCHA web side"}. -{"Change Password","Endre Passord"}. -{"Change User Password","Endre Brukers Passord"}. +{"Backup Management","Håndtering av sikkerehetskopiering"}. +{"Backup of ~p","Sikkerhetskopi av ~p"}. +{"Backup to File at ","Sikkerhetskopier til fil på "}. +{"Backup","Sikkerhetskopiering"}. +{"Bad format","Feilaktig format"}. +{"Birthday","Geburtsdag"}. +{"Both the username and the resource are required","Både brukernavn og ressurs kreves"}. +{"Cannot remove active list","Kan ikke fjerne aktiv liste"}. +{"Cannot remove default list","Kan ikke fjerne forvalgt liste"}. +{"CAPTCHA web page","CAPTCHA-nettside"}. +{"Challenge ID","Utfordrings-ID"}. +{"Change Password","Endre passord"}. +{"Change User Password","Endre brukerpassord"}. +{"Channel already exists","Kanalen finnes allerede"}. +{"Channels","Kanaler"}. {"Characters not allowed:","Ikke godtatte tegn:"}. -{"Chatroom configuration modified","Samtalerommets konfigurasjon er endret"}. -{"Chatroom is created","Samtalerom er opprettet"}. +{"Chatroom configuration modified","Samtalerommets oppsett er endret"}. +{"Chatroom is created","Samtalerom opprettet"}. {"Chatroom is destroyed","Samtalerom er fjernet"}. -{"Chatroom is started","Samtalerom er startet"}. -{"Chatroom is stopped","Samtalerom er stoppet"}. +{"Chatroom is started","Samtalerom startet"}. +{"Chatroom is stopped","Samtalerom stoppet"}. {"Chatrooms","Samtalerom"}. -{"Choose a username and password to register with this server","Velg et brukernavn og passord for å registrere på "}. -{"Choose modules to stop","Velg hvilke moduler som skal stoppes"}. +{"Choose a username and password to register with this server","Velg et brukernavn og passord for å registrere deg på denne tjeneren"}. {"Choose storage type of tables","Velg lagringstype for tabeller"}. -{"Choose whether to approve this entity's subscription.","Velg om du vil godkjenne denne eksistensens abonement"}. {"City","By"}. {"Commands","Kommandoer"}. {"Conference room does not exist","Konferanserommet finnes ikke"}. -{"Configuration","Konfigurasjon"}. -{"Configuration of room ~s","Konfigurasjon for rom ~s"}. -{"Connected Resources:","Tilkoblede Ressurser:"}. -{"Connections parameters","Tilkoblings parametere"}. +{"Configuration of room ~s","Oppsett for rom ~s"}. +{"Configuration","Oppsett"}. {"Country","Land"}. -{"CPU Time:","CPU Tid:"}. +{"Current Discussion Topic","Nåværende diskusjonstema"}. +{"Database Tables Configuration at ","Database-tabelloppsett på "}. {"Database","Database"}. -{"Database Tables at ","Database Tabeller på "}. -{"Database Tables Configuration at ","Database Tabell Konfigurasjon på "}. {"December","desember"}. {"Default users as participants","Standard brukere som deltakere"}. -{"Delete message of the day on all hosts","Slett melding for dagen på alle maskiner"}. {"Delete message of the day","Slett melding for dagen"}. -{"Delete Selected","Slett valgte"}. -{"Delete","Slett"}. -{"Delete User","Slett Bruker"}. -{"Deliver event notifications","Lever begivenhets kunngjøringer"}. -{"Deliver payloads with event notifications","Send innhold sammen med kunngjøringer"}. -{"Description:","Beskrivelse:"}. +{"Delete User","Slett bruker"}. +{"Deliver event notifications","Lever begivenhetskunngjøringer"}. +{"Deliver payloads with event notifications","Send innhold sammen med hendelsesmerknader"}. {"Disc only copy","Kun diskkopi"}. -{"Displayed Groups:","Viste grupper:"}. -{"Don't tell your password to anybody, not even the administrators of the Jabber server.","Ikke fortell passordet til noen, ikke en gang til administratoren av Jabber serveren."}. -{"Dump Backup to Text File at ","Dump Sikkerhetskopi til Tekstfil på "}. -{"Dump to Text File","Dump til Tekstfil"}. -{"Edit Properties","Redigere Egenskaper"}. -{"Either approve or decline the voice request.","Enten godkjenn eller forby lyd forespørselen"}. -{"ejabberd IRC module","ejabberd IRC modul"}. -{"ejabberd MUC module","ejabberd MUC modul"}. -{"ejabberd Publish-Subscribe module","ejabberd Publish-Subscribe modul"}. -{"ejabberd SOCKS5 Bytestreams module","ejabberd SOCKS5 Bytestreams modul"}. -{"ejabberd vCard module","ejabberd vCard modul"}. -{"ejabberd Web Admin","ejabberd Web Admin"}. -{"Elements","Elementer"}. -{"Email","Epost"}. -{"Enable logging","Slå på logging"}. -{"Encoding for server ~b","Tekstkoding for server ~b"}. -{"End User Session","Avslutt Bruker Sesjon"}. -{"Enter list of {Module, [Options]}","Skriv inn en liste av {Module, [Options]}"}. +{"Dump Backup to Text File at ","Dump sikkerhetskopi til tekstfil på "}. +{"Dump to Text File","Dump til tekstfil"}. +{"Edit Properties","Rediger egenskaper"}. +{"Either approve or decline the voice request.","Enten godkjenn eller forby lydforespørselen."}. +{"ejabberd HTTP Upload service","ejabberd-HTTP-opplastingstjeneste"}. +{"ejabberd MUC module","ejabberd-MUC-modul"}. +{"ejabberd Multicast service","ejabberd-multikastingstjeneste"}. +{"ejabberd Publish-Subscribe module","ejabberd-Publish-Subscribe-modul"}. +{"Email","E-post"}. +{"Enable logging","Skru på loggføring"}. +{"Enable message archiving","Skru på meldingsarkivering"}. +{"Enabling push without 'node' attribute is not supported","Å skru på dytting uten «node»-attributt støttes ikke"}. +{"End User Session","Avslutt brukerøkt"}. {"Enter nickname you want to register","Skriv inn kallenavnet du ønsker å registrere"}. -{"Enter path to backup file","Skriv inn sti til sikkerhetskopi filen"}. +{"Enter path to backup file","Skriv inn sti til sikkerhetskopifilen"}. {"Enter path to jabberd14 spool dir","Skriv inn sti til jabberd14 spoolkatalog"}. -{"Enter path to jabberd14 spool file","Skriv inn sti til jabberd14 spoolfil"}. {"Enter path to text file","Skriv inn sti til tekstfil"}. {"Enter the text you see","Skriv inn teksten du ser"}. -{"Enter username and encodings you wish to use for connecting to IRC servers. Press 'Next' to get more fields to fill in. Press 'Complete' to save settings.","Angi brukernavn og kodinger du ønsker å bruke for å koble til IRC servere. Trykk 'Neste' for å få flere felt for å fylle i. Trykk 'Fullfør' for å lagre innstillingene."}. -{"Enter username, encodings, ports and passwords you wish to use for connecting to IRC servers","Skriv brukernavn, tekstkoding, porter og passord du ønsker å bruke for tilkobling til IRC servere"}. -{"Erlang Jabber Server","Erlang Jabber Server"}. -{"Error","Feil"}. -{"Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef.net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}].","Eksempel: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef.net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]."}. -{"Exclude Jabber IDs from CAPTCHA challenge","Ekskluder Jabber IDer fra CAPTCHA utfordring"}. -{"Export data of all users in the server to PIEFXIS files (XEP-0227):","Eksporter data om alle brukere i en server til PIEFXIS filer"}. -{"Export data of users in a host to PIEFXIS files (XEP-0227):","Eksporter data om alle brukere i en host til PIEFXIS filer (XEP-0227):"}. -{"Failed to extract JID from your voice request approval","Feilet i forsøk på å hente JID fra din lyd forespørsel godkjenning"}. +{"Exclude Jabber IDs from CAPTCHA challenge","Ekskluder Jabber-ID-er fra CAPTCHA-utfordring"}. +{"Export all tables as SQL queries to a file:","Eksporter alle tabeller som SQL-spørringer til en fil:"}. +{"Export data of all users in the server to PIEFXIS files (XEP-0227):","Eksporter data om alle brukere på en tjener til PIEFXIS-filer (XEP-0227):"}. +{"Export data of users in a host to PIEFXIS files (XEP-0227):","Eksporter data om alle brukere på en vert til PIEFXIS-filer (XEP-0227):"}. +{"External component failure","Feil med ekstern komponent"}. +{"External component timeout","Tidsavbrudd for ekstern komponent"}. {"Family Name","Etternavn"}. +{"FAQ Entry","O-S-S -oppføring"}. {"February","februar"}. -{"Fill in fields to search for any matching Jabber User","Fyll inn felt for å søke etter Jabber brukere"}. -{"Fill in the form to search for any matching Jabber User (Add * to the end of field to match substring)","Fyll inn skjemaet for å søke etter Jabber bruker (Legg til * på slutten av feltet for å treffe alle som starter slik)"}. +{"File larger than ~w bytes","Fil større enn ~w byte"}. {"Friday","fredag"}. -{"From","Fra"}. -{"From ~s","Fra ~s"}. -{"Full Name","Fullstendig Navn"}. -{"Get Number of Online Users","Vis Antall Tilkoblede Brukere"}. -{"Get Number of Registered Users","Vis Antall Registrerte Brukere"}. -{"Get User Last Login Time","Vis Brukers Siste Påloggings Tidspunkt"}. -{"Get User Password","Hent Brukers Passord"}. -{"Get User Statistics","Vis Bruker Statistikk"}. -{"Grant voice to this person?","Gi lyd til denne personen?"}. -{"Group ","Gruppe "}. -{"Groups","Grupper"}. +{"Full List of Room Admins","Full liste over romadministratorer"}. +{"Full List of Room Owners","Full liste over romeiere"}. +{"Full Name","Fullt navn"}. +{"Get Number of Online Users","Vis antall tilkoblede brukere"}. +{"Get Number of Registered Users","Vis antall registrerte brukere"}. +{"Get User Last Login Time","Vis brukers siste innloggingstidspunkt"}. +{"Get User Statistics","Vis brukerstatistikk"}. {"has been banned","har blitt bannlyst"}. -{"has been kicked because of an affiliation change","har blitt kastet ut på grunn av en tilknytnings endring"}. -{"has been kicked because of a system shutdown","har blitt kastet ut på grunn av at systemet avslutter"}. -{"has been kicked because the room has been changed to members-only","har blitt kastet ut på grunn av at rommet er endret til kun-for-medlemmer"}. +{"has been kicked because of a system shutdown","har blitt kastet ut på grunn av systemavstenging"}. +{"has been kicked because of an affiliation change","har blitt kastet ut på grunn av en tilknytningsendring"}. {"has been kicked","har blitt kastet ut"}. -{" has set the subject to: "," har satt emnet til: "}. -{"Host","Maskin"}. -{"If you don't see the CAPTCHA image here, visit the web page.","Dersom du ikke ser CAPTCHA bilde her, besøk web siden. "}. -{"If you want to specify different ports, passwords, encodings for IRC servers, fill this list with values in format '{\"irc server\", \"encoding\", port, \"password\"}'. By default this service use \"~s\" encoding, port ~p, empty password.","Om du ønsker å spesifisere tekstkoding for IRC tjenere, fyller du ut en liste med verdier i formatet '{\"irc server\", \"encoding\", port, \"password\"}'. Denne tjenesten bruker \"~s\" som standard, port ~p, empty password."}. -{"Import Directory","Importer Katalog"}. -{"Import File","Importer File"}. -{"Import user data from jabberd14 spool file:","Importer bruker data fra jabberd14 spoolfiler:"}. -{"Import User from File at ","Importer Bruker fra Fil på "}. -{"Import users data from a PIEFXIS file (XEP-0227):","Importer brukeres data fra en PIEFXIS fil (XEP-0227):"}. -{"Import users data from jabberd14 spool directory:","Importer brukeres data fra jabberd14 spoolfil katalog:"}. -{"Import Users from Dir at ","Importer Brukere fra Katalog på "}. -{"Import Users From jabberd14 Spool Files","Importer Brukere Fra jabberd14 Spoolfiler"}. +{"Host unknown","Ukjent vert"}. +{"HTTP File Upload","HTTP-filopplasting"}. +{"If you don't see the CAPTCHA image here, visit the web page.","Dersom du ikke ser et CAPTCHA-bilde her, besøk nettsiden."}. +{"Import Directory","Importer mappe"}. +{"Import File","Importer file"}. +{"Import User from File at ","Importer bruker fra fil på "}. +{"Import users data from a PIEFXIS file (XEP-0227):","Importer data fra bruker fra en PIEFXIS-fil (XEP-0227):"}. +{"Import Users from Dir at ","Importer brukere fra mappe på "}. {"Improper message type","Feilaktig meldingstype"}. {"Incorrect password","Feil passord"}. -{"Invalid affiliation: ~s","Ugyldig rang: ~s"}. -{"Invalid role: ~s","Ugyldig rolle: ~s"}. -{"IP addresses","IP adresser"}. -{"IP","IP"}. -{"IRC channel (don't put the first #)","IRC kanal (ikke skriv den første #)"}. -{"IRC server","IRC server"}. -{"IRC settings","IRC instillinger"}. -{"IRC Transport","IRC Transport"}. -{"IRC username","IRC brukernavn"}. -{"IRC Username","IRC Brukernavn"}. +{"Insufficient privilege","Mangler tilstrekkelige rettigheter"}. +{"Internal server error","Intern tjenerfeil"}. +{"Invalid 'from' attribute in forwarded message","Ugyldig «fra»-attributt i videresendt melding"}. +{"IP addresses","IP-adresser"}. {"is now known as","er nå kjent som"}. -{"It is not allowed to send private messages","Det er ikke tillatt å sende private meldinger"}. -{"It is not allowed to send private messages of type \"groupchat\"","Det er ikke tillatt å sende private meldinger med typen "}. -{"It is not allowed to send private messages to the conference","Det er ikke tillatt å sende private meldinger til "}. -{"Jabber Account Registration","Jabber Konto Registrering"}. -{"Jabber ID","Jabber ID"}. -{"Jabber ID ~s is invalid","Ugyldig Jabber ID ~s"}. +{"It is not allowed to send private messages to the conference","Det er ikke tillatt å sende private meldinger til konferansen"}. +{"Jabber ID","Jabber-ID"}. {"January","januar"}. -{"Join IRC channel","Bli med i IRC kanal"}. -{"joins the room","kommer inn i rommet"}. -{"Join the IRC channel here.","Bli med i IRC kanalen her. "}. -{"Join the IRC channel in this Jabber ID: ~s","Bli med i IRC kanalen med denne Jabber ID: ~s"}. +{"JID normalization failed","JID-normalisering mislyktes"}. +{"joins the room","tar del i rommet"}. {"July","juli"}. {"June","juni"}. -{"Last Activity","Siste Aktivitet"}. -{"Last login","Siste pålogging"}. +{"Just created","Akkurat opprettet"}. +{"Last Activity","Siste aktivitet"}. +{"Last login","Siste innlogging"}. {"Last month","Siste måned"}. -{"Last year","Siste året"}. +{"Last year","Siste år"}. +{"Least significant bits of SHA-256 hash of text should equal hexadecimal label","De minst viktige bit-ene av SHA-256-sjekksummen for tekst skal tilsvare heksadesimal etikett"}. {"leaves the room","forlater rommet"}. -{"Listened Ports at ","Lyttende Porter på "}. -{"Listened Ports","Lyttende Porter"}. -{"List of modules to start","Liste over moduler som skal startes"}. -{"Low level update script","Lavnivå oppdaterings skript"}. +{"Logging","Loggføring"}. {"Make participants list public","Gjør deltakerlisten offentlig"}. -{"Make room CAPTCHA protected","Gjør rommet CAPTCHA beskyttet"}. {"Make room members-only","Gjør rommet tilgjengelig kun for medlemmer"}. -{"Make room moderated","Gjør rommet redaktørstyrt"}. {"Make room password protected","Passordbeskytt rommet"}. -{"Make room persistent","Gjør rommet permanent"}. +{"Make room persistent","Gjør rommet vedvarende"}. {"Make room public searchable","Gjør rommet offentlig søkbart"}. {"March","mars"}. -{"Maximum Number of Occupants","Maksimum Antall Deltakere"}. -{"Max # of items to persist","Høyeste # elementer som skal lagres"}. -{"Max payload size in bytes","Største innholdsstørrelse i byte"}. +{"Maximum file size","Maksimal filstørrelse"}. +{"Maximum Number of History Messages Returned by Room","Maksimalt antall historikkmeldinger tilbudt av rommet"}. {"May","mai"}. -{"Membership is required to enter this room","Medlemskap kreves for tilgang til samtalerommet"}. -{"Members:","Medlemmer:"}. -{"Memorize your password, or write it in a paper placed in a safe place. In Jabber there isn't an automated way to recover your password if you forget it.","Husk passordet, eller skriv det ned på et papir lagret på et trygt sted. I Jabber er det ingen automatisert måte å gjenskape passordet om du glemmer det. "}. -{"Memory","Minne"}. +{"Membership is required to enter this room","Medlemskap kreves for tilgang til dette rommet"}. {"Message body","Meldingskropp"}. +{"Message not found in forwarded payload","Fant ikke melding i videresendt nyttelast"}. +{"Messages from strangers are rejected","Meldinger fra ukjente avvises"}. +{"Messages of type headline","Meldinger av typen overskrift"}. +{"Messages of type normal","Meldinger av normal type"}. {"Middle Name","Mellomnavn"}. -{"Minimum interval between voice requests (in seconds)","Minimums interval mellom lyd forespørsler (i sekunder)"}. -{"Moderator privileges required","Redaktørprivilegier kreves"}. -{"moderators only","kun for redaktører"}. -{"Modified modules","Endrede moduler"}. -{"Module","Modul"}. -{"Modules at ","Moduler på "}. -{"Modules","Moduler"}. +{"Minimum interval between voice requests (in seconds)","Minimumsintervall mellom lydforespørsler (i sekunder)"}. {"Monday","mandag"}. -{"Name:","Navn:"}. +{"Multicast","Multikasting"}. +{"Multi-User Chat","Multibrukersludring"}. {"Name","Navn"}. {"Never","Aldri"}. -{"New Password:","Nytt Passord:"}. +{"New Password:","Nytt passord:"}. +{"Nickname can't be empty","Kallenavn kan ikke stå tomt"}. +{"Nickname Registration at ","Registrer kallenavn på "}. {"Nickname","Kallenavn"}. -{"Nickname Registration at ","Registrer Kallenavn på "}. -{"Nickname ~s does not exist in the room","Kallenavn ~s eksisterer ikke i dette rommet"}. -{"nobody","ingen"}. -{"No body provided for announce message","Ingen meldingskropp gitt for kunngjørings melding"}. -{"No Data","Ingen Data"}. -{"Node ID","Node ID"}. -{"Node ","Node "}. +{"No available resource found","Fant ingen tilgjengelig ressurs"}. +{"No body provided for announce message","Ingen meldingskropp angitt for kunngjøringsmelding"}. +{"No Data","Ingen data"}. +{"No features available","Ingen tilgjengelige funksjoner"}. +{"No element found","Fant ikke noe -element"}. +{"No limit","Ingen grense"}. +{"Node already exists","Node finnes allerede"}. +{"Node ID","Node-ID"}. {"Node not found","Noden finnes ikke"}. {"Nodes","Noder"}. -{"No limit","Ingen grense"}. {"None","Ingen"}. -{"No resource provided","Ingen ressurs angitt"}. -{"Not Found","Finnes Ikke"}. -{"Notify subscribers when items are removed from the node","Informer abonnenter når elementer fjernes fra noden"}. -{"Notify subscribers when the node configuration changes","Informer abonnenter når node konfigurasjonen endres"}. +{"Not Found","Finnes ikke"}. +{"Notify subscribers when the node configuration changes","Informer abonnenter når nodeoppsettet endres"}. {"Notify subscribers when the node is deleted","Informer abonnenter når noden slettes"}. {"November","november"}. {"Number of occupants","Antall deltakere"}. {"Number of online users","Antall tilkoblede brukere"}. {"Number of registered users","Antall registrerte brukere"}. {"October","oktober"}. -{"Offline Messages:","Frakoblede Meldinger:"}. -{"Offline Messages","Frakoblede Meldinger"}. {"OK","OK"}. -{"Old Password:","Gammelt Passord:"}. +{"Old Password:","Gammelt passord:"}. +{"Online Users","Tilkoblede brukere"}. {"Online","Tilkoblet"}. -{"Online Users:","Tilkoblede Brukere:"}. -{"Online Users","Tilkoblede Brukere"}. -{"Only deliver notifications to available users","Send kunngjøringer bare til tilgjengelige brukere"}. -{"Only moderators and participants are allowed to change the subject in this room","Bare redaktører og deltakere kan endre emnet i dette rommet"}. -{"Only moderators are allowed to change the subject in this room","Bare ordstyrer tillates å endre emnet i dette rommet"}. -{"Only moderators can approve voice requests","Bare ordstyrer kan godkjenne lyd forespørsler"}. +{"Only deliver notifications to available users","Kun send kunngjøringer til tilgjengelige brukere"}. {"Only occupants are allowed to send messages to the conference","Bare deltakere får sende normale meldinger til konferansen"}. -{"Only occupants are allowed to send queries to the conference","Bare deltakere er tillatt å sende forespørsler til "}. -{"Only service administrators are allowed to send service messages","Bare tjeneste administratorer er tilatt å sende tjeneste "}. -{"Options","Alternativer"}. +{"Only occupants are allowed to send queries to the conference","Kun deltakere tillates å sende forespørsler til konferansen"}. +{"Only publishers may publish","Kun publiserere kan publisere"}. +{"Only service administrators are allowed to send service messages","Bare tjenesteadministratorer tillates å sende tjenestemeldinger"}. {"Organization Name","Organisasjonsnavn"}. {"Organization Unit","Organisasjonsenhet"}. -{"Outgoing s2s Connections:","Utgående s2s Koblinger"}. -{"Outgoing s2s Connections","Utgående s2s Koblinger"}. -{"Outgoing s2s Servers:","Utgående s2s Tjenere"}. +{"Outgoing s2s Connections","Utgående s2s-koblinger"}. {"Owner privileges required","Eierprivilegier kreves"}. -{"Packet","Pakke"}. -{"Password ~b","Passord ~b"}. -{"Password:","Passord:"}. +{"Participant","Deltager"}. +{"Password Verification","Passordbekreftelse"}. +{"Password Verification:","Passordbekreftelse:"}. {"Password","Passord"}. -{"Password Verification:","Passord Bekreftelse:"}. -{"Password Verification","Passord Bekreftelse"}. -{"Path to Dir","Sti til Katalog"}. -{"Path to File","Sti til Fil"}. -{"Pending","Ventende"}. +{"Password:","Passord:"}. +{"Path to Dir","Sti til mappe"}. +{"Path to File","Sti til fil"}. {"Period: ","Periode: "}. {"Persist items to storage","Vedvarende elementer til lagring"}. -{"Ping","Ping"}. -{"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Merk at disse valgene vil bare sikkerhetskopiere den innebygde Mnesia databasen. Dersom du bruker ODBC modulen må du også ta backup av din SQL database."}. -{"Please, wait for a while before sending new voice request","Vennligst vent en stund før du sender en ny lyd forespørsel"}. -{"Pong","Pong"}. -{"Port ~b","Port ~b"}. -{"Port","Port"}. -{"Present real Jabber IDs to","Presenter ekte Jabber IDer til"}. +{"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Merk at disse valgene kun vil sikkerhetskopiere den innebygde Mnesia-databasen. Dersom du bruker ODBC-modulen må du også ta sikkerhetskopi av din SQL-database."}. +{"Previous session PID has exited","Forrige økt-ID har avsluttet"}. +{"Previous session PID is dead","Forrige økt-PID er død"}. +{"Previous session timed out","Tidsavbrudd for forrige økt"}. {"private, ","privat, "}. -{"Protocol","Protokoll"}. -{"Publish-Subscribe","Publish-Subscribe"}. -{"PubSub subscriber request","PubSub abonements forespørsel"}. +{"Public","Offentlig"}. +{"PubSub subscriber request","PubSub-abonementsforespørsel"}. {"Purge all items when the relevant publisher goes offline","Rydd alle elementer når den aktuelle utgiveren logger av"}. -{"Queries to the conference members are not allowed in this room","Forespørsler til konferanse medlemmene er ikke tillat i dette rommet"}. -{"RAM and disc copy","RAM og diskkopi"}. -{"RAM copy","RAM kopi"}. -{"Raw","Rå"}. -{"Really delete message of the day?","Virkelig slette melding for dagen?"}. +{"Queries to the conference members are not allowed in this room","Forespørsler til konferansemedlemmerer tillates ikke i dette rommet"}. +{"Query to another users is forbidden","Spørringer til andre brukere er forbudt"}. +{"RAM and disc copy","Minne og diskkopi"}. +{"RAM copy","Minnekopi"}. +{"Really delete message of the day?","Vil du virkelig slette melding for dagen?"}. {"Recipient is not in the conference room","Mottakeren er ikke i konferanserommet"}. -{"Register a Jabber account","Registrer en Jabber konto"}. -{"Registered Users:","Registrerte Brukere:"}. -{"Registered Users","Registrerte Brukere"}. {"Register","Registrer"}. -{"Registration in mod_irc for ","Registrering i mod_irc for "}. -{"Remote copy","Lagres ikke lokalt"}. -{"Remove All Offline Messages","Fjern Alle Frakoblede Meldinger"}. -{"Remove","Fjern"}. -{"Remove User","Fjern Bruker"}. +{"Remove User","Fjern bruker"}. {"Replaced by new connection","Erstattet av en ny tilkobling"}. +{"Request has timed out","Tidsavbrudd for forespørsel"}. +{"Request is ignored","Forespørsel ignorert"}. +{"Requested role","Forespurt rolle"}. {"Resources","Ressurser"}. -{"Restart Service","Start Tjeneste på Nytt"}. -{"Restart","Starte på nytt"}. -{"Restore Backup from File at ","Gjenopprett fra Sikkerhetsopifil på "}. -{"Restore binary backup after next ejabberd restart (requires less memory):","Gjenopprette binær backup etter neste ejabberd omstart (krever mindre minne):"}. -{"Restore binary backup immediately:","Gjenopprette binær backup umiddelbart:"}. -{"Restore","Gjenopprett"}. +{"Restart Service","Omstart av tjeneste"}. +{"Restore Backup from File at ","Gjenopprett fra sikkerhetskopifil på "}. +{"Restore binary backup after next ejabberd restart (requires less memory):","Gjenopprett binær sikkerhetskopi etter neste ejabberd-omstart (krever mindre minne):"}. +{"Restore binary backup immediately:","Gjenopprett binær sikkerhetskopi umiddelbart:"}. {"Restore plain text backup immediately:","Gjenopprette rentekst sikkerhetskopi umiddelbart:"}. -{"Room Configuration","Rom Konfigurasjon"}. -{"Room creation is denied by service policy","Oppretting av rom nektes av en tjenste regel"}. -{"Room description","Rom beskrivelse"}. -{"Room Occupants","Samtalerom Deltakere"}. -{"Room title","Romtittel"}. -{"Roster groups allowed to subscribe","Kontaktliste grupper som tillates å abonnere"}. -{"Roster","Kontaktliste"}. -{"Roster of ","Kontaktliste for "}. -{"Roster size","Kontaktliste størrelse"}. -{"RPC Call Error","RPC Kall Feil"}. -{"Running Nodes","Kjørende Noder"}. -{"~s access rule configuration","tilgangsregel konfigurasjon for ~s"}. +{"Restore","Gjenopprett"}. +{"Room Configuration","Rom-oppsett"}. +{"Room creation is denied by service policy","Oppretting av rom nektes av en tjensteregel"}. +{"Room description","Rom-beskrivelse"}. +{"Room Occupants","Samtaleromsdeltagere"}. +{"Room title","Rom-tittel"}. +{"Roster groups allowed to subscribe","Kontaktlistegrupper som tillates å abonnere"}. +{"Roster size","Kontaktlistestørrelse"}. +{"Running Nodes","Kjørende noder"}. {"Saturday","lørdag"}. -{"Script check","Skript sjekk"}. -{"Search Results for ","Søke Resultater for "}. +{"Search Results for ","Søkeresultater for "}. {"Search users in ","Søk etter brukere i "}. -{"Send announcement to all online users on all hosts","Send kunngjøring til alle tilkoblede brukere på alle "}. +{"Send announcement to all online users on all hosts","Send kunngjøring til alle tilkoblede brukere på alle verter"}. {"Send announcement to all online users","Send kunngjøring alle tilkoblede brukere"}. -{"Send announcement to all users on all hosts","Send kunngjøring til alle brukere på alle maskiner"}. +{"Send announcement to all users on all hosts","Send kunngjøring til alle brukere på alle verter"}. {"Send announcement to all users","Send kunngjøring til alle brukere"}. {"September","september"}. -{"Server ~b","Server ~b"}. -{"Server:","Server:"}. +{"Server:","Tjener:"}. {"Set message of the day and send to online users","Angi melding for dagen og send til tilkoblede brukere"}. -{"Set message of the day on all hosts and send to online users","Angi melding for dagen på alle maskiner og send til "}. -{"Shared Roster Groups","Delte Kontaktgrupper"}. -{"Show Integral Table","Vis Integral Tabell"}. -{"Show Ordinary Table","Vis Ordinær Tabell"}. -{"Shut Down Service","Avslutt Tjeneste"}. -{"~s invites you to the room ~s","~s inviterer deg til rommet ~s"}. -{"Some Jabber clients can store your password in your computer. Use that feature only if you trust your computer is safe.","Noen Jabber klienter kan lagre passordet på datamaskinen. Bruk bare den funksjonen dersom du er sikker på at maskinen er trygg."}. -{"Specify the access model","Spesifiser aksess modellen"}. -{"Specify the event message type","Spesifiser hendelsesbeskjed type"}. -{"Specify the publisher model","Angi publiserings modell"}. -{"~s's Offline Messages Queue","~ss kø for Frakoblede Meldinger"}. -{"Start Modules at ","Start Moduler på "}. -{"Start Modules","Start Moduler"}. -{"Start","Start"}. -{"Statistics of ~p","Statistikk for ~p"}. -{"Statistics","Statistikk"}. -{"Stop Modules at ","Stopp Moduler på "}. -{"Stop Modules","Stop Moduler"}. -{"Stopped Nodes","Stoppede Noder"}. -{"Stop","Stoppe"}. -{"Storage Type","Lagringstype"}. +{"Shared Roster Groups","Delte kontaktgrupper"}. +{"Show Ordinary Table","Vis ordinær tabell"}. +{"Shut Down Service","Avslutt tjeneste"}. +{"Specify the access model","Spesifiser tilgangsmodellen"}. +{"Specify the event message type","Spesifiser hendelsesbeskjedtypen"}. +{"Specify the publisher model","Angi publiseringsmodell"}. +{"Stopped Nodes","Stoppede noder"}. {"Store binary backup:","Lagre binær sikkerhetskopi:"}. -{"Store plain text backup:","Lagre rentekst sikkerhetskopi:"}. -{"Subject","Tittel"}. -{"Submit","Send"}. +{"Store plain text backup:","Lagre klartekst-sikkerhetskopi:"}. +{"Subject","Emne"}. {"Submitted","Innsendt"}. -{"Subscriber Address","Abonnements Adresse"}. -{"Subscription","Abonnement"}. +{"Subscriber Address","Abonnementsadresse"}. +{"Subscribers may publish","Abonnenter kan publisere"}. +{"Subscriptions are not allowed","Abonnementer tillates ikke"}. {"Sunday","søndag"}. +{"Text associated with a picture","Tekst tilknyttet et bilde"}. +{"Text associated with a sound","Tekst tilknyttet en lyd"}. +{"Text associated with a video","Tekst tilknyttet en video"}. +{"Text associated with speech","Tekst tilknyttet tale"}. {"That nickname is already in use by another occupant","Det kallenavnet er allerede i bruk av en annen deltaker"}. {"That nickname is registered by another person","Det kallenavnet er registrert av en annen person"}. -{"The CAPTCHA is valid.","Captchaen er ikke gyldig"}. -{"The CAPTCHA verification has failed","CAPTCHA godkjenningen har feilet"}. -{"The collections with which a node is affiliated","Samlingene som en node er assosiert med"}. -{"the password is","passordet er"}. +{"The account already exists","Kontoen finnes allerede"}. +{"The account was not unregistered","Kontoen ble ikke avregistrert"}. +{"The body text of the last received message","Brødteksten i sist mottatte melding"}. +{"The CAPTCHA is valid.","CAPTCHA-en er gyldig."}. +{"The CAPTCHA verification has failed","CAPTCHA-godkjenning mislyktes"}. +{"The captcha you entered is wrong","CAPTCHA-en du skrev inn er feil"}. +{"The number of unread or undelivered messages","Antallet uleste eller uleverte meldinger"}. +{"The password contains unacceptable characters","Passordet inneholder ulovlige tegn"}. {"The password is too weak","Passordet er for svakt"}. -{"The password of your Jabber account was successfully changed.","Passordet for din Jabber konto ble endret."}. -{"There was an error changing the password: ","En feil skjedde under endring av passordet:"}. -{"There was an error creating the account: ","En feil skjedde under oppretting av kontoen:"}. -{"There was an error deleting the account: ","En feil skjedde under sletting av kontoen: "}. -{"This is case insensitive: macbeth is the same that MacBeth and Macbeth.","Denne er ufølsom for små og store bokstaver: macbeth er det samme som MacBeth og Macbeth. "}. -{"This page allows to create a Jabber account in this Jabber server. Your JID (Jabber IDentifier) will be of the form: username@server. Please read carefully the instructions to fill correctly the fields.","Denne siden lar deg lage en Jabber konto på denne Jabber serveren. Din JID (Jabber ID) vil være i formatet: brukernavn@server. Vennligst les instruksjonene nøye slik at du fyller ut skjemaet riktig."}. -{"This page allows to unregister a Jabber account in this Jabber server.","Denne siden lar deg avregistrere en Jabber konto på denne Jabber serveren."}. -{"This participant is kicked from the room because he sent an error message","Denne deltakeren er kastet ut av rommet fordi han sendte en feilmelding"}. -{"This participant is kicked from the room because he sent an error message to another participant","Denne deltakeren er kastet ut av rommet fordi han sendte en feilmelding til en annen deltaker"}. -{"This participant is kicked from the room because he sent an error presence","Denne deltakeren er kastet ut av rommet fordi han sendte feil tilstederværelse"}. +{"the password is","passordet er"}. +{"The presence states for which an entity wants to receive notifications","Tilstedeværelsestilstandene en eksistens ønsker å motta merknader for"}. +{"The query is only allowed from local users","Spørringen tillates kun fra lokale brukere"}. +{"The query must not contain elements","Spørringen kan ikke inneholde -elementer"}. +{"The room subject can be modified by participants","Romemnet kan endres av dets deltagere"}. +{"The sender of the last received message","Avsender for sist mottatte melding"}. +{"There was an error creating the account: ","En feil inntraff under oppretting av kontoen: "}. +{"There was an error deleting the account: ","En feil inntraff under sletting av kontoen: "}. {"This room is not anonymous","Dette rommet er ikke anonymt"}. {"Thursday","torsdag"}. -{"Time delay","Tids forsinkelse"}. -{"Time","Tid"}. -{"Too many CAPTCHA requests","For mange CAPTCHA forespørsler"}. -{"To ~s","Til ~s"}. -{"To","Til"}. -{"Traffic rate limit is exceeded","Trafikkmengde grense overskredet"}. -{"Transactions Aborted:","Avbrutte Transasksjoner:"}. -{"Transactions Committed:","Sendte Transaksjoner:"}. -{"Transactions Logged:","Loggede Transasksjoner:"}. -{"Transactions Restarted:","Omstartede Transasksjoner:"}. +{"Time delay","Tidsforsinkelse"}. +{"To register, visit ~s","Besøk ~s for registrering"}. +{"Too many CAPTCHA requests","For mange CAPTCHA-forespørsler"}. +{"Too many elements","For mange -elementer"}. +{"Too many elements","For mange -elementer"}. +{"Traffic rate limit is exceeded","Grense for tillatt trafikkmengde overskredet"}. {"Tuesday","tirsdag"}. -{"Unable to generate a CAPTCHA","Umulig å generere en CAPTCHA"}. +{"Unable to generate a CAPTCHA","Kunne ikke generere CAPTCHA"}. {"Unauthorized","Uautorisert"}. -{"Unregister a Jabber account","Avregistrer en Jabber konto"}. {"Unregister","Avregistrer"}. +{"Unsupported element","Ustøttet -element"}. +{"Unsupported version","Ustøttet versjon"}. {"Update message of the day (don't send)","Oppdater melding for dagen (ikke send)"}. -{"Update message of the day on all hosts (don't send)","Oppdater melding for dagen på alle maskiner (ikke send)"}. -{"Update ","Oppdater "}. -{"Update","Oppdatere"}. -{"Update plan","Oppdaterings plan"}. -{"Update script","Oppdaterings skript"}. -{"Uptime:","Oppetid:"}. -{"Use of STARTTLS required","Bruk av STARTTLS kreves"}. -{"User ","Bruker "}. +{"Update message of the day on all hosts (don't send)","Oppdater melding for dagen på alle verter (ikke send)"}. +{"User already exists","Brukeren finnes allerede"}. +{"User Management","Brukerhåndtering"}. +{"User removed","Fjernet bruker"}. +{"User ~ts","Bruker ~ts"}. {"User","Bruker"}. -{"User JID","Bruker JID"}. -{"User Management","Bruker Behandling"}. {"Username:","Brukernavn:"}. -{"Users are not allowed to register accounts so quickly","Brukere får ikke lov til registrere kontoer så fort"}. +{"Users are not allowed to register accounts so quickly","Brukere har ikke lov til registrere kontoer så fort"}. +{"Users Last Activity","Brukers siste aktivitet"}. {"Users","Brukere"}. -{"Users Last Activity","Brukers Siste Aktivitet"}. -{"Validate","Bekrefte gyldighet"}. -{"vCard User Search","vCard Bruker Søk"}. -{"Virtual Hosts","Virtuella Maskiner"}. -{"Visitors are not allowed to change their nicknames in this room","Besøkende får ikke lov å endre kallenavn i dette "}. +{"vCard User Search","vCard-brukersøk"}. +{"Virtual Hosts","Virtuelle maskiner"}. +{"Visitor","Besøker"}. +{"Visitors are not allowed to change their nicknames in this room","Besøkende får ikke lov å endre kallenavn i dette rommet"}. {"Visitors are not allowed to send messages to all occupants","Besøkende får ikke sende meldinger til alle deltakere"}. -{"Voice request","Lyd forespørsel"}. -{"Voice requests are disabled in this conference","Lyd forespørsler er blokkert i denne konferansen"}. +{"Voice requests are disabled in this conference","Stemmeforespørsler er blokkert i denne konferansen"}. +{"Voice request","Stemmeforespørsel"}. {"Wednesday","onsdag"}. {"When to send the last published item","Når skal siste publiserte artikkel sendes"}. -{"Whether to allow subscriptions","Om man skal tillate abonnenter"}. -{"You can later change your password using a Jabber client.","Du kan når som helst endre passordet via en Jabber klient."}. -{"You have been banned from this room","Du har blitt bannlyst i dette rommet."}. -{"You must fill in field \"Nickname\" in the form","Du må fylle inn feltet \"Nickname\" i skjemaet"}. -{"You need a client that supports x:data and CAPTCHA to register","Du trenger en klient som støtter x:data og CAPTCHA for registrering "}. +{"Whether an entity wants to receive an XMPP message body in addition to the payload format","Hvorvidt en eksistens ønsker å motta en XMPP-meldingsbrødtekst i tillegg til nyttedata-formatet"}. +{"Whether an entity wants to receive or disable notifications","Hvorvidt en eksistens ønsker å motta eller skru av merknader"}. +{"Whether owners or publisher should receive replies to items","Hvorvidt elere eller publisererer skal motta svar på elementer"}. +{"Whether to allow subscriptions","Hvorvidt abonnementer skal tillates"}. +{"XMPP Domains","XMPP-domener"}. +{"You have been banned from this room","Du har blitt bannlyst fra dette rommet."}. +{"You have joined too many conferences","Du har tilknyttet deg for mange konferanser"}. +{"You must fill in field \"Nickname\" in the form","Du må fylle inn feltet «Kallenavn» i skjemaet"}. +{"You need a client that supports x:data and CAPTCHA to register","Du trenger en klient som støtter x:data og CAPTCHA for registrering"}. {"You need a client that supports x:data to register the nickname","Du trenger en klient som støtter x:data for å registrere kallenavnet"}. -{"You need an x:data capable client to configure mod_irc settings","Du trenger en x:data kompatibel klient for å konfigurere mod_irc instillinger"}. -{"You need an x:data capable client to configure room","Du trenger en klient som støtter x:data for å "}. -{"You need an x:data capable client to search","Du tregner en klient som støtter x:data for å kunne "}. -{"Your active privacy list has denied the routing of this stanza.","Din aktive privat liste har blokkert rutingen av denne strofen."}. +{"You need an x:data capable client to search","Du trenger en klient som støtter x:data for å søke"}. {"Your contact offline message queue is full. The message has been discarded.","Kontaktens frakoblede meldingskø er full. Meldingen har blitt kassert."}. -{"Your Jabber account was successfully created.","Din Jabber konto ble opprettet"}. -{"Your Jabber account was successfully deleted.","Dni Jabber konto er blitt sltettet."}. -{"Your messages to ~s are being blocked. To unblock them, visit ~s","Dine meldinger til ~s blir blokkert. For å åpne igjen, besøk ~s"}. +{"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","Din abonnementsforespørsel og/eller meldinger til ~s har blitt blokkert. For å avblokkere din abonnementsforespørsel, besøk ~s"}. diff --git a/priv/msgs/no.po b/priv/msgs/no.po deleted file mode 100644 index a2d0b98d4..000000000 --- a/priv/msgs/no.po +++ /dev/null @@ -1,1847 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: 2.1.0-alpha\n" -"POT-Creation-Date: \n" -"PO-Revision-Date: \n" -"Last-Translator: Stian B. Barmen \n" -"Language-Team: \n" -"Language: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Language: Norwegian (bokmål)\n" - -#: ejabberd_c2s.erl:424 ejabberd_c2s.erl:727 -msgid "Use of STARTTLS required" -msgstr "Bruk av STARTTLS kreves" - -#: ejabberd_c2s.erl:503 -msgid "No resource provided" -msgstr "Ingen ressurs angitt" - -#: ejabberd_c2s.erl:1197 -msgid "Replaced by new connection" -msgstr "Erstattet av en ny tilkobling" - -#: ejabberd_c2s.erl:1885 -msgid "Your active privacy list has denied the routing of this stanza." -msgstr "Din aktive privat liste har blokkert rutingen av denne strofen." - -#: ejabberd_captcha.erl:96 ejabberd_captcha.erl:152 ejabberd_captcha.erl:178 -msgid "Enter the text you see" -msgstr "Skriv inn teksten du ser" - -#: ejabberd_captcha.erl:101 -msgid "Your messages to ~s are being blocked. To unblock them, visit ~s" -msgstr "Dine meldinger til ~s blir blokkert. For å åpne igjen, besøk ~s" - -#: ejabberd_captcha.erl:134 -msgid "If you don't see the CAPTCHA image here, visit the web page." -msgstr "Dersom du ikke ser CAPTCHA bilde her, besøk web siden. " - -#: ejabberd_captcha.erl:146 -msgid "CAPTCHA web page" -msgstr "CAPTCHA web side" - -#: ejabberd_captcha.erl:307 -msgid "The CAPTCHA is valid." -msgstr "Captchaen er ikke gyldig" - -#: mod_adhoc.erl:95 mod_adhoc.erl:125 mod_adhoc.erl:143 mod_adhoc.erl:161 -msgid "Commands" -msgstr "Kommandoer" - -#: mod_adhoc.erl:149 mod_adhoc.erl:243 -msgid "Ping" -msgstr "Ping" - -#: mod_adhoc.erl:260 -msgid "Pong" -msgstr "Pong" - -#: mod_announce.erl:507 mod_announce_odbc.erl:499 -msgid "Really delete message of the day?" -msgstr "Virkelig slette melding for dagen?" - -#: mod_announce.erl:515 mod_announce_odbc.erl:507 mod_configure.erl:1083 -#: mod_configure.erl:1128 -msgid "Subject" -msgstr "Tittel" - -#: mod_announce.erl:520 mod_announce_odbc.erl:512 mod_configure.erl:1088 -#: mod_configure.erl:1133 -msgid "Message body" -msgstr "Meldingskropp" - -#: mod_announce.erl:600 mod_announce_odbc.erl:592 -msgid "No body provided for announce message" -msgstr "Ingen meldingskropp gitt for kunngjørings melding" - -#: mod_announce.erl:635 mod_announce_odbc.erl:627 -msgid "Announcements" -msgstr "Kunngjøringer" - -#: mod_announce.erl:637 mod_announce_odbc.erl:629 -msgid "Send announcement to all users" -msgstr "Send kunngjøring til alle brukere" - -#: mod_announce.erl:639 mod_announce_odbc.erl:631 -msgid "Send announcement to all users on all hosts" -msgstr "Send kunngjøring til alle brukere på alle maskiner" - -#: mod_announce.erl:641 mod_announce_odbc.erl:633 -msgid "Send announcement to all online users" -msgstr "Send kunngjøring alle tilkoblede brukere" - -#: mod_announce.erl:643 mod_announce_odbc.erl:635 mod_configure.erl:1078 -#: mod_configure.erl:1123 -msgid "Send announcement to all online users on all hosts" -msgstr "Send kunngjøring til alle tilkoblede brukere på alle " - -#: mod_announce.erl:645 mod_announce_odbc.erl:637 -msgid "Set message of the day and send to online users" -msgstr "Angi melding for dagen og send til tilkoblede brukere" - -#: mod_announce.erl:647 mod_announce_odbc.erl:639 -msgid "Set message of the day on all hosts and send to online users" -msgstr "Angi melding for dagen på alle maskiner og send til " - -#: mod_announce.erl:649 mod_announce_odbc.erl:641 -msgid "Update message of the day (don't send)" -msgstr "Oppdater melding for dagen (ikke send)" - -#: mod_announce.erl:651 mod_announce_odbc.erl:643 -msgid "Update message of the day on all hosts (don't send)" -msgstr "Oppdater melding for dagen på alle maskiner (ikke send)" - -#: mod_announce.erl:653 mod_announce_odbc.erl:645 -msgid "Delete message of the day" -msgstr "Slett melding for dagen" - -#: mod_announce.erl:655 mod_announce_odbc.erl:647 -msgid "Delete message of the day on all hosts" -msgstr "Slett melding for dagen på alle maskiner" - -#: mod_configure.erl:114 mod_configure.erl:274 mod_configure.erl:296 -#: mod_configure.erl:498 -msgid "Configuration" -msgstr "Konfigurasjon" - -#: mod_configure.erl:125 mod_configure.erl:576 web/ejabberd_web_admin.erl:1936 -msgid "Database" -msgstr "Database" - -#: mod_configure.erl:127 mod_configure.erl:595 -msgid "Start Modules" -msgstr "Start Moduler" - -#: mod_configure.erl:129 mod_configure.erl:596 -msgid "Stop Modules" -msgstr "Stop Moduler" - -#: mod_configure.erl:131 mod_configure.erl:604 web/ejabberd_web_admin.erl:1937 -msgid "Backup" -msgstr "Sikkerhetskopier" - -#: mod_configure.erl:133 mod_configure.erl:605 -msgid "Restore" -msgstr "Gjenopprett" - -#: mod_configure.erl:135 mod_configure.erl:606 -msgid "Dump to Text File" -msgstr "Dump til Tekstfil" - -#: mod_configure.erl:137 mod_configure.erl:615 -msgid "Import File" -msgstr "Importer File" - -#: mod_configure.erl:139 mod_configure.erl:616 -msgid "Import Directory" -msgstr "Importer Katalog" - -#: mod_configure.erl:141 mod_configure.erl:581 mod_configure.erl:1057 -msgid "Restart Service" -msgstr "Start Tjeneste på Nytt" - -#: mod_configure.erl:143 mod_configure.erl:582 mod_configure.erl:1102 -msgid "Shut Down Service" -msgstr "Avslutt Tjeneste" - -#: mod_configure.erl:145 mod_configure.erl:518 mod_configure.erl:1197 -#: web/ejabberd_web_admin.erl:1527 -msgid "Add User" -msgstr "Legg til Bruker" - -#: mod_configure.erl:147 mod_configure.erl:519 mod_configure.erl:1219 -msgid "Delete User" -msgstr "Slett Bruker" - -#: mod_configure.erl:149 mod_configure.erl:520 mod_configure.erl:1231 -msgid "End User Session" -msgstr "Avslutt Bruker Sesjon" - -#: mod_configure.erl:151 mod_configure.erl:521 mod_configure.erl:1243 -#: mod_configure.erl:1255 -msgid "Get User Password" -msgstr "Hent Brukers Passord" - -#: mod_configure.erl:153 mod_configure.erl:522 -msgid "Change User Password" -msgstr "Endre Brukers Passord" - -#: mod_configure.erl:155 mod_configure.erl:523 mod_configure.erl:1272 -msgid "Get User Last Login Time" -msgstr "Vis Brukers Siste Påloggings Tidspunkt" - -#: mod_configure.erl:157 mod_configure.erl:524 mod_configure.erl:1284 -msgid "Get User Statistics" -msgstr "Vis Bruker Statistikk" - -#: mod_configure.erl:159 mod_configure.erl:525 -msgid "Get Number of Registered Users" -msgstr "Vis Antall Registrerte Brukere" - -#: mod_configure.erl:161 mod_configure.erl:526 -msgid "Get Number of Online Users" -msgstr "Vis Antall Tilkoblede Brukere" - -#: mod_configure.erl:163 mod_configure.erl:509 web/ejabberd_web_admin.erl:831 -#: web/ejabberd_web_admin.erl:872 -msgid "Access Control Lists" -msgstr "Tilgangskontrollister" - -#: mod_configure.erl:165 mod_configure.erl:510 web/ejabberd_web_admin.erl:940 -#: web/ejabberd_web_admin.erl:976 -msgid "Access Rules" -msgstr "Tilgangsregler" - -#: mod_configure.erl:297 mod_configure.erl:499 -msgid "User Management" -msgstr "Bruker Behandling" - -#: mod_configure.erl:500 web/ejabberd_web_admin.erl:1058 -#: web/ejabberd_web_admin.erl:1462 -msgid "Online Users" -msgstr "Tilkoblede Brukere" - -#: mod_configure.erl:501 -msgid "All Users" -msgstr "Alle Brukere" - -#: mod_configure.erl:502 -msgid "Outgoing s2s Connections" -msgstr "Utgående s2s Koblinger" - -#: mod_configure.erl:503 web/ejabberd_web_admin.erl:1907 -msgid "Running Nodes" -msgstr "Kjørende Noder" - -#: mod_configure.erl:504 web/ejabberd_web_admin.erl:1909 -msgid "Stopped Nodes" -msgstr "Stoppede Noder" - -#: mod_configure.erl:577 mod_configure.erl:587 web/ejabberd_web_admin.erl:1953 -msgid "Modules" -msgstr "Moduler" - -#: mod_configure.erl:578 -msgid "Backup Management" -msgstr "Håndtere Sikkerehetskopiering" - -#: mod_configure.erl:579 -msgid "Import Users From jabberd14 Spool Files" -msgstr "Importer Brukere Fra jabberd14 Spoolfiler" - -#: mod_configure.erl:699 -msgid "To ~s" -msgstr "Til ~s" - -#: mod_configure.erl:717 -msgid "From ~s" -msgstr "Fra ~s" - -#: mod_configure.erl:913 -msgid "Database Tables Configuration at " -msgstr "Database Tabell Konfigurasjon på " - -#: mod_configure.erl:918 -msgid "Choose storage type of tables" -msgstr "Velg lagringstype for tabeller" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Disc only copy" -msgstr "Kun diskkopi" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM and disc copy" -msgstr "RAM og diskkopi" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM copy" -msgstr "RAM kopi" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Remote copy" -msgstr "Lagres ikke lokalt" - -#: mod_configure.erl:950 -msgid "Stop Modules at " -msgstr "Stopp Moduler på " - -#: mod_configure.erl:954 -msgid "Choose modules to stop" -msgstr "Velg hvilke moduler som skal stoppes" - -#: mod_configure.erl:969 -msgid "Start Modules at " -msgstr "Start Moduler på " - -#: mod_configure.erl:973 -msgid "Enter list of {Module, [Options]}" -msgstr "Skriv inn en liste av {Module, [Options]}" - -#: mod_configure.erl:974 -msgid "List of modules to start" -msgstr "Liste over moduler som skal startes" - -#: mod_configure.erl:983 -msgid "Backup to File at " -msgstr "Sikkerhetskopiere til Fil på " - -#: mod_configure.erl:987 mod_configure.erl:1001 -msgid "Enter path to backup file" -msgstr "Skriv inn sti til sikkerhetskopi filen" - -#: mod_configure.erl:988 mod_configure.erl:1002 mod_configure.erl:1016 -#: mod_configure.erl:1030 -msgid "Path to File" -msgstr "Sti til Fil" - -#: mod_configure.erl:997 -msgid "Restore Backup from File at " -msgstr "Gjenopprett fra Sikkerhetsopifil på " - -#: mod_configure.erl:1011 -msgid "Dump Backup to Text File at " -msgstr "Dump Sikkerhetskopi til Tekstfil på " - -#: mod_configure.erl:1015 -msgid "Enter path to text file" -msgstr "Skriv inn sti til tekstfil" - -#: mod_configure.erl:1025 -msgid "Import User from File at " -msgstr "Importer Bruker fra Fil på " - -#: mod_configure.erl:1029 -msgid "Enter path to jabberd14 spool file" -msgstr "Skriv inn sti til jabberd14 spoolfil" - -#: mod_configure.erl:1039 -msgid "Import Users from Dir at " -msgstr "Importer Brukere fra Katalog på " - -#: mod_configure.erl:1043 -msgid "Enter path to jabberd14 spool dir" -msgstr "Skriv inn sti til jabberd14 spoolkatalog" - -#: mod_configure.erl:1044 -msgid "Path to Dir" -msgstr "Sti til Katalog" - -#: mod_configure.erl:1060 mod_configure.erl:1105 -msgid "Time delay" -msgstr "Tids forsinkelse" - -#: mod_configure.erl:1143 -msgid "Access Control List Configuration" -msgstr "Konfigurasjon for Tilgangskontroll lister" - -#: mod_configure.erl:1147 -msgid "Access control lists" -msgstr "Tilgangskontroll lister" - -#: mod_configure.erl:1171 -msgid "Access Configuration" -msgstr "Tilgangskonfigurasjon" - -#: mod_configure.erl:1175 -msgid "Access rules" -msgstr "Tilgangsregler" - -#: mod_configure.erl:1200 mod_configure.erl:1222 mod_configure.erl:1234 -#: mod_configure.erl:1246 mod_configure.erl:1258 mod_configure.erl:1275 -#: mod_configure.erl:1287 mod_configure.erl:1650 mod_configure.erl:1700 -#: mod_configure.erl:1721 mod_roster.erl:943 mod_roster_odbc.erl:1060 -#: mod_vcard.erl:472 mod_vcard_ldap.erl:554 mod_vcard_odbc.erl:448 -msgid "Jabber ID" -msgstr "Jabber ID" - -#: mod_configure.erl:1205 mod_configure.erl:1263 mod_configure.erl:1651 -#: mod_configure.erl:1863 mod_muc/mod_muc_room.erl:3224 mod_register.erl:235 -#: web/ejabberd_web_admin.erl:1520 -msgid "Password" -msgstr "Passord" - -#: mod_configure.erl:1210 -msgid "Password Verification" -msgstr "Passord Bekreftelse" - -#: mod_configure.erl:1301 -msgid "Number of registered users" -msgstr "Antall registrerte brukere" - -#: mod_configure.erl:1315 -msgid "Number of online users" -msgstr "Antall tilkoblede brukere" - -#: mod_configure.erl:1682 web/ejabberd_web_admin.erl:1588 -#: web/ejabberd_web_admin.erl:1743 -msgid "Never" -msgstr "Aldri" - -#: mod_configure.erl:1696 web/ejabberd_web_admin.erl:1601 -#: web/ejabberd_web_admin.erl:1756 -msgid "Online" -msgstr "Tilkoblet" - -#: mod_configure.erl:1701 -msgid "Last login" -msgstr "Siste pålogging" - -#: mod_configure.erl:1722 -msgid "Roster size" -msgstr "Kontaktliste størrelse" - -#: mod_configure.erl:1723 -msgid "IP addresses" -msgstr "IP adresser" - -#: mod_configure.erl:1724 -msgid "Resources" -msgstr "Ressurser" - -#: mod_configure.erl:1850 -msgid "Administration of " -msgstr "Administrasjon av " - -#: mod_configure.erl:1853 -msgid "Action on user" -msgstr "Handling på bruker" - -#: mod_configure.erl:1857 -msgid "Edit Properties" -msgstr "Redigere Egenskaper" - -#: mod_configure.erl:1860 web/ejabberd_web_admin.erl:1769 -msgid "Remove User" -msgstr "Fjern Bruker" - -#: mod_irc/mod_irc.erl:201 mod_irc/mod_irc_odbc.erl:196 -#: mod_muc/mod_muc.erl:336 mod_muc/mod_muc_odbc.erl:342 -msgid "Access denied by service policy" -msgstr "Tilgang nektes på grunn av en tjeneste regel" - -#: mod_irc/mod_irc.erl:401 mod_irc/mod_irc_odbc.erl:398 -msgid "IRC Transport" -msgstr "IRC Transport" - -#: mod_irc/mod_irc.erl:428 mod_irc/mod_irc_odbc.erl:425 -msgid "ejabberd IRC module" -msgstr "ejabberd IRC modul" - -#: mod_irc/mod_irc.erl:559 mod_irc/mod_irc_odbc.erl:568 -msgid "You need an x:data capable client to configure mod_irc settings" -msgstr "" -"Du trenger en x:data kompatibel klient for å konfigurere mod_irc instillinger" - -#: mod_irc/mod_irc.erl:566 mod_irc/mod_irc_odbc.erl:575 -msgid "Registration in mod_irc for " -msgstr "Registrering i mod_irc for " - -#: mod_irc/mod_irc.erl:571 mod_irc/mod_irc_odbc.erl:580 -msgid "" -"Enter username, encodings, ports and passwords you wish to use for " -"connecting to IRC servers" -msgstr "" -"Skriv brukernavn, tekstkoding, porter og passord du ønsker å bruke for " -"tilkobling til IRC servere" - -#: mod_irc/mod_irc.erl:576 mod_irc/mod_irc_odbc.erl:585 -msgid "IRC Username" -msgstr "IRC Brukernavn" - -#: mod_irc/mod_irc.erl:586 mod_irc/mod_irc_odbc.erl:595 -msgid "" -"If you want to specify different ports, passwords, encodings for IRC " -"servers, fill this list with values in format '{\"irc server\", \"encoding" -"\", port, \"password\"}'. By default this service use \"~s\" encoding, port " -"~p, empty password." -msgstr "" -"Om du ønsker å spesifisere tekstkoding for IRC tjenere, fyller du ut en " -"liste med verdier i formatet '{\"irc server\", \"encoding\", port, \"password" -"\"}'. Denne tjenesten bruker \"~s\" som standard, port ~p, empty password." - -#: mod_irc/mod_irc.erl:598 mod_irc/mod_irc_odbc.erl:607 -msgid "" -"Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef." -"net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]." -msgstr "" -"Eksempel: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta." -"fef.net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]." - -#: mod_irc/mod_irc.erl:603 mod_irc/mod_irc_odbc.erl:612 -msgid "Connections parameters" -msgstr "Tilkoblings parametere" - -#: mod_irc/mod_irc.erl:728 mod_irc/mod_irc_odbc.erl:757 -msgid "Join IRC channel" -msgstr "Bli med i IRC kanal" - -#: mod_irc/mod_irc.erl:732 mod_irc/mod_irc_odbc.erl:761 -msgid "IRC channel (don't put the first #)" -msgstr "IRC kanal (ikke skriv den første #)" - -#: mod_irc/mod_irc.erl:737 mod_irc/mod_irc_odbc.erl:766 -msgid "IRC server" -msgstr "IRC server" - -#: mod_irc/mod_irc.erl:770 mod_irc/mod_irc.erl:774 -#: mod_irc/mod_irc_odbc.erl:799 mod_irc/mod_irc_odbc.erl:803 -msgid "Join the IRC channel here." -msgstr "Bli med i IRC kanalen her. " - -#: mod_irc/mod_irc.erl:778 mod_irc/mod_irc_odbc.erl:807 -msgid "Join the IRC channel in this Jabber ID: ~s" -msgstr "Bli med i IRC kanalen med denne Jabber ID: ~s" - -#: mod_irc/mod_irc.erl:863 mod_irc/mod_irc_odbc.erl:904 -msgid "IRC settings" -msgstr "IRC instillinger" - -#: mod_irc/mod_irc.erl:868 mod_irc/mod_irc_odbc.erl:909 -msgid "" -"Enter username and encodings you wish to use for connecting to IRC servers. " -"Press 'Next' to get more fields to fill in. Press 'Complete' to save " -"settings." -msgstr "" -"Angi brukernavn og kodinger du ønsker å bruke for å koble til IRC servere. " -"Trykk 'Neste' for å få flere felt for å fylle i. Trykk 'Fullfør' for å lagre " -"innstillingene." - -#: mod_irc/mod_irc.erl:874 mod_irc/mod_irc_odbc.erl:915 -msgid "IRC username" -msgstr "IRC brukernavn" - -#: mod_irc/mod_irc.erl:923 mod_irc/mod_irc_odbc.erl:964 -msgid "Password ~b" -msgstr "Passord ~b" - -#: mod_irc/mod_irc.erl:928 mod_irc/mod_irc_odbc.erl:969 -msgid "Port ~b" -msgstr "Port ~b" - -#: mod_irc/mod_irc.erl:933 mod_irc/mod_irc_odbc.erl:974 -msgid "Encoding for server ~b" -msgstr "Tekstkoding for server ~b" - -#: mod_irc/mod_irc.erl:942 mod_irc/mod_irc_odbc.erl:983 -msgid "Server ~b" -msgstr "Server ~b" - -#: mod_muc/mod_muc.erl:449 mod_muc/mod_muc_odbc.erl:456 -msgid "Only service administrators are allowed to send service messages" -msgstr "Bare tjeneste administratorer er tilatt å sende tjeneste " - -#: mod_muc/mod_muc.erl:493 mod_muc/mod_muc_odbc.erl:500 -msgid "Room creation is denied by service policy" -msgstr "Oppretting av rom nektes av en tjenste regel" - -#: mod_muc/mod_muc.erl:500 mod_muc/mod_muc_odbc.erl:507 -msgid "Conference room does not exist" -msgstr "Konferanserommet finnes ikke" - -#: mod_muc/mod_muc.erl:582 mod_muc/mod_muc_odbc.erl:595 -msgid "Chatrooms" -msgstr "Samtalerom" - -#: mod_muc/mod_muc.erl:712 mod_muc/mod_muc_odbc.erl:729 -msgid "You need a client that supports x:data to register the nickname" -msgstr "Du trenger en klient som støtter x:data for å registrere kallenavnet" - -#: mod_muc/mod_muc.erl:718 mod_muc/mod_muc_odbc.erl:735 -msgid "Nickname Registration at " -msgstr "Registrer Kallenavn på " - -#: mod_muc/mod_muc.erl:722 mod_muc/mod_muc_odbc.erl:739 -msgid "Enter nickname you want to register" -msgstr "Skriv inn kallenavnet du ønsker å registrere" - -#: mod_muc/mod_muc.erl:723 mod_muc/mod_muc_odbc.erl:740 -#: mod_muc/mod_muc_room.erl:3810 mod_roster.erl:944 mod_roster_odbc.erl:1061 -#: mod_vcard.erl:364 mod_vcard.erl:477 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:453 -msgid "Nickname" -msgstr "Kallenavn" - -#: mod_muc/mod_muc.erl:762 mod_muc/mod_muc_odbc.erl:784 -#: mod_muc/mod_muc_room.erl:1064 mod_muc/mod_muc_room.erl:1787 -msgid "That nickname is registered by another person" -msgstr "Det kallenavnet er registrert av en annen person" - -#: mod_muc/mod_muc.erl:788 mod_muc/mod_muc_odbc.erl:811 -msgid "You must fill in field \"Nickname\" in the form" -msgstr "Du må fylle inn feltet \"Nickname\" i skjemaet" - -#: mod_muc/mod_muc.erl:808 mod_muc/mod_muc_odbc.erl:831 -msgid "ejabberd MUC module" -msgstr "ejabberd MUC modul" - -#: mod_muc/mod_muc_log.erl:374 mod_muc/mod_muc_log.erl:381 -msgid "Chatroom configuration modified" -msgstr "Samtalerommets konfigurasjon er endret" - -#: mod_muc/mod_muc_log.erl:384 -msgid "joins the room" -msgstr "kommer inn i rommet" - -#: mod_muc/mod_muc_log.erl:387 mod_muc/mod_muc_log.erl:390 -msgid "leaves the room" -msgstr "forlater rommet" - -#: mod_muc/mod_muc_log.erl:393 mod_muc/mod_muc_log.erl:396 -msgid "has been banned" -msgstr "har blitt bannlyst" - -#: mod_muc/mod_muc_log.erl:399 mod_muc/mod_muc_log.erl:402 -msgid "has been kicked" -msgstr "har blitt kastet ut" - -#: mod_muc/mod_muc_log.erl:405 -msgid "has been kicked because of an affiliation change" -msgstr "har blitt kastet ut på grunn av en tilknytnings endring" - -#: mod_muc/mod_muc_log.erl:408 -msgid "has been kicked because the room has been changed to members-only" -msgstr "" -"har blitt kastet ut på grunn av at rommet er endret til kun-for-medlemmer" - -#: mod_muc/mod_muc_log.erl:411 -msgid "has been kicked because of a system shutdown" -msgstr "har blitt kastet ut på grunn av at systemet avslutter" - -#: mod_muc/mod_muc_log.erl:414 -msgid "is now known as" -msgstr "er nå kjent som" - -#: mod_muc/mod_muc_log.erl:417 mod_muc/mod_muc_log.erl:688 -#: mod_muc/mod_muc_room.erl:2393 -msgid " has set the subject to: " -msgstr " har satt emnet til: " - -#: mod_muc/mod_muc_log.erl:452 -msgid "Chatroom is created" -msgstr "Samtalerom er opprettet" - -#: mod_muc/mod_muc_log.erl:453 -msgid "Chatroom is destroyed" -msgstr "Samtalerom er fjernet" - -#: mod_muc/mod_muc_log.erl:454 -msgid "Chatroom is started" -msgstr "Samtalerom er startet" - -#: mod_muc/mod_muc_log.erl:455 -msgid "Chatroom is stopped" -msgstr "Samtalerom er stoppet" - -#: mod_muc/mod_muc_log.erl:459 -msgid "Monday" -msgstr "mandag" - -#: mod_muc/mod_muc_log.erl:460 -msgid "Tuesday" -msgstr "tirsdag" - -#: mod_muc/mod_muc_log.erl:461 -msgid "Wednesday" -msgstr "onsdag" - -#: mod_muc/mod_muc_log.erl:462 -msgid "Thursday" -msgstr "torsdag" - -#: mod_muc/mod_muc_log.erl:463 -msgid "Friday" -msgstr "fredag" - -#: mod_muc/mod_muc_log.erl:464 -msgid "Saturday" -msgstr "lørdag" - -#: mod_muc/mod_muc_log.erl:465 -msgid "Sunday" -msgstr "søndag" - -#: mod_muc/mod_muc_log.erl:469 -msgid "January" -msgstr "januar" - -#: mod_muc/mod_muc_log.erl:470 -msgid "February" -msgstr "februar" - -#: mod_muc/mod_muc_log.erl:471 -msgid "March" -msgstr "mars" - -#: mod_muc/mod_muc_log.erl:472 -msgid "April" -msgstr "april" - -#: mod_muc/mod_muc_log.erl:473 -msgid "May" -msgstr "mai" - -#: mod_muc/mod_muc_log.erl:474 -msgid "June" -msgstr "juni" - -#: mod_muc/mod_muc_log.erl:475 -msgid "July" -msgstr "juli" - -#: mod_muc/mod_muc_log.erl:476 -msgid "August" -msgstr "august" - -#: mod_muc/mod_muc_log.erl:477 -msgid "September" -msgstr "september" - -#: mod_muc/mod_muc_log.erl:478 -msgid "October" -msgstr "oktober" - -#: mod_muc/mod_muc_log.erl:479 -msgid "November" -msgstr "november" - -#: mod_muc/mod_muc_log.erl:480 -msgid "December" -msgstr "desember" - -#: mod_muc/mod_muc_log.erl:750 -msgid "Room Configuration" -msgstr "Rom Konfigurasjon" - -#: mod_muc/mod_muc_log.erl:759 -msgid "Room Occupants" -msgstr "Samtalerom Deltakere" - -#: mod_muc/mod_muc_room.erl:174 -msgid "Traffic rate limit is exceeded" -msgstr "Trafikkmengde grense overskredet" - -#: mod_muc/mod_muc_room.erl:246 -msgid "" -"This participant is kicked from the room because he sent an error message" -msgstr "" -"Denne deltakeren er kastet ut av rommet fordi han sendte en feilmelding" - -#: mod_muc/mod_muc_room.erl:255 -msgid "It is not allowed to send private messages to the conference" -msgstr "Det er ikke tillatt å sende private meldinger til " - -#: mod_muc/mod_muc_room.erl:332 -msgid "Please, wait for a while before sending new voice request" -msgstr "Vennligst vent en stund før du sender en ny lyd forespørsel" - -#: mod_muc/mod_muc_room.erl:347 -msgid "Voice requests are disabled in this conference" -msgstr "Lyd forespørsler er blokkert i denne konferansen" - -#: mod_muc/mod_muc_room.erl:364 -msgid "Failed to extract JID from your voice request approval" -msgstr "Feilet i forsøk på å hente JID fra din lyd forespørsel godkjenning" - -#: mod_muc/mod_muc_room.erl:393 -msgid "Only moderators can approve voice requests" -msgstr "Bare ordstyrer kan godkjenne lyd forespørsler" - -#: mod_muc/mod_muc_room.erl:408 -msgid "Improper message type" -msgstr "Feilaktig meldingstype" - -#: mod_muc/mod_muc_room.erl:518 -msgid "" -"This participant is kicked from the room because he sent an error message to " -"another participant" -msgstr "" -"Denne deltakeren er kastet ut av rommet fordi han sendte en feilmelding til " -"en annen deltaker" - -#: mod_muc/mod_muc_room.erl:531 -msgid "It is not allowed to send private messages of type \"groupchat\"" -msgstr "Det er ikke tillatt å sende private meldinger med typen " - -#: mod_muc/mod_muc_room.erl:543 mod_muc/mod_muc_room.erl:611 -msgid "Recipient is not in the conference room" -msgstr "Mottakeren er ikke i konferanserommet" - -#: mod_muc/mod_muc_room.erl:564 mod_muc/mod_muc_room.erl:585 -msgid "It is not allowed to send private messages" -msgstr "Det er ikke tillatt å sende private meldinger" - -#: mod_muc/mod_muc_room.erl:576 mod_muc/mod_muc_room.erl:956 -#: mod_muc/mod_muc_room.erl:4040 -msgid "Only occupants are allowed to send messages to the conference" -msgstr "Bare deltakere får sende normale meldinger til konferansen" - -#: mod_muc/mod_muc_room.erl:634 -msgid "Only occupants are allowed to send queries to the conference" -msgstr "Bare deltakere er tillatt å sende forespørsler til " - -#: mod_muc/mod_muc_room.erl:646 -msgid "Queries to the conference members are not allowed in this room" -msgstr "Forespørsler til konferanse medlemmene er ikke tillat i dette rommet" - -#: mod_muc/mod_muc_room.erl:932 -msgid "" -"Only moderators and participants are allowed to change the subject in this " -"room" -msgstr "Bare redaktører og deltakere kan endre emnet i dette rommet" - -#: mod_muc/mod_muc_room.erl:937 -msgid "Only moderators are allowed to change the subject in this room" -msgstr "Bare ordstyrer tillates å endre emnet i dette rommet" - -#: mod_muc/mod_muc_room.erl:947 -msgid "Visitors are not allowed to send messages to all occupants" -msgstr "Besøkende får ikke sende meldinger til alle deltakere" - -#: mod_muc/mod_muc_room.erl:1021 -msgid "" -"This participant is kicked from the room because he sent an error presence" -msgstr "" -"Denne deltakeren er kastet ut av rommet fordi han sendte feil " -"tilstederværelse" - -#: mod_muc/mod_muc_room.erl:1040 -msgid "Visitors are not allowed to change their nicknames in this room" -msgstr "Besøkende får ikke lov å endre kallenavn i dette " - -#: mod_muc/mod_muc_room.erl:1053 mod_muc/mod_muc_room.erl:1779 -msgid "That nickname is already in use by another occupant" -msgstr "Det kallenavnet er allerede i bruk av en annen deltaker" - -#: mod_muc/mod_muc_room.erl:1768 -msgid "You have been banned from this room" -msgstr "Du har blitt bannlyst i dette rommet." - -#: mod_muc/mod_muc_room.erl:1771 -msgid "Membership is required to enter this room" -msgstr "Medlemskap kreves for tilgang til samtalerommet" - -#: mod_muc/mod_muc_room.erl:1807 -msgid "This room is not anonymous" -msgstr "Dette rommet er ikke anonymt" - -#: mod_muc/mod_muc_room.erl:1833 -msgid "A password is required to enter this room" -msgstr "Et passord kreves for tilgang til samtalerommet" - -#: mod_muc/mod_muc_room.erl:1855 mod_register.erl:246 -msgid "Too many CAPTCHA requests" -msgstr "For mange CAPTCHA forespørsler" - -#: mod_muc/mod_muc_room.erl:1864 mod_register.erl:251 -msgid "Unable to generate a CAPTCHA" -msgstr "Umulig å generere en CAPTCHA" - -#: mod_muc/mod_muc_room.erl:1874 -msgid "Incorrect password" -msgstr "Feil passord" - -#: mod_muc/mod_muc_room.erl:2448 -msgid "Administrator privileges required" -msgstr "Administratorprivilegier kreves" - -#: mod_muc/mod_muc_room.erl:2463 -msgid "Moderator privileges required" -msgstr "Redaktørprivilegier kreves" - -#: mod_muc/mod_muc_room.erl:2619 -msgid "Jabber ID ~s is invalid" -msgstr "Ugyldig Jabber ID ~s" - -#: mod_muc/mod_muc_room.erl:2633 -msgid "Nickname ~s does not exist in the room" -msgstr "Kallenavn ~s eksisterer ikke i dette rommet" - -#: mod_muc/mod_muc_room.erl:2659 mod_muc/mod_muc_room.erl:3049 -msgid "Invalid affiliation: ~s" -msgstr "Ugyldig rang: ~s" - -#: mod_muc/mod_muc_room.erl:2713 -msgid "Invalid role: ~s" -msgstr "Ugyldig rolle: ~s" - -#: mod_muc/mod_muc_room.erl:3026 mod_muc/mod_muc_room.erl:3062 -msgid "Owner privileges required" -msgstr "Eierprivilegier kreves" - -#: mod_muc/mod_muc_room.erl:3195 -msgid "Configuration of room ~s" -msgstr "Konfigurasjon for rom ~s" - -#: mod_muc/mod_muc_room.erl:3200 -msgid "Room title" -msgstr "Romtittel" - -#: mod_muc/mod_muc_room.erl:3203 mod_muc/mod_muc_room.erl:3692 -msgid "Room description" -msgstr "Rom beskrivelse" - -#: mod_muc/mod_muc_room.erl:3210 -msgid "Make room persistent" -msgstr "Gjør rommet permanent" - -#: mod_muc/mod_muc_room.erl:3215 -msgid "Make room public searchable" -msgstr "Gjør rommet offentlig søkbart" - -#: mod_muc/mod_muc_room.erl:3218 -msgid "Make participants list public" -msgstr "Gjør deltakerlisten offentlig" - -#: mod_muc/mod_muc_room.erl:3221 -msgid "Make room password protected" -msgstr "Passordbeskytt rommet" - -#: mod_muc/mod_muc_room.erl:3232 -msgid "Maximum Number of Occupants" -msgstr "Maksimum Antall Deltakere" - -#: mod_muc/mod_muc_room.erl:3239 -msgid "No limit" -msgstr "Ingen grense" - -#: mod_muc/mod_muc_room.erl:3250 -msgid "Present real Jabber IDs to" -msgstr "Presenter ekte Jabber IDer til" - -#: mod_muc/mod_muc_room.erl:3258 mod_muc/mod_muc_room.erl:3292 -msgid "moderators only" -msgstr "kun for redaktører" - -#: mod_muc/mod_muc_room.erl:3260 mod_muc/mod_muc_room.erl:3294 -msgid "anyone" -msgstr "hvem som helst" - -#: mod_muc/mod_muc_room.erl:3262 -msgid "Make room members-only" -msgstr "Gjør rommet tilgjengelig kun for medlemmer" - -#: mod_muc/mod_muc_room.erl:3265 -msgid "Make room moderated" -msgstr "Gjør rommet redaktørstyrt" - -#: mod_muc/mod_muc_room.erl:3268 -msgid "Default users as participants" -msgstr "Standard brukere som deltakere" - -#: mod_muc/mod_muc_room.erl:3271 -msgid "Allow users to change the subject" -msgstr "Tillat brukere å endre emne" - -#: mod_muc/mod_muc_room.erl:3274 -msgid "Allow users to send private messages" -msgstr "Tillat brukere å sende private meldinger" - -#: mod_muc/mod_muc_room.erl:3279 -msgid "Allow visitors to send private messages to" -msgstr "Tillat brukere å sende private meldinger til" - -#: mod_muc/mod_muc_room.erl:3290 -msgid "nobody" -msgstr "ingen" - -#: mod_muc/mod_muc_room.erl:3296 -msgid "Allow users to query other users" -msgstr "Tillat brukere å sende forespørsel til andre brukere" - -#: mod_muc/mod_muc_room.erl:3299 -msgid "Allow users to send invites" -msgstr "Tillat brukere å sende invitasjoner" - -#: mod_muc/mod_muc_room.erl:3302 -msgid "Allow visitors to send status text in presence updates" -msgstr "Tillat besøkende å sende status tekst i " - -#: mod_muc/mod_muc_room.erl:3305 -msgid "Allow visitors to change nickname" -msgstr "Tillat besøkende å endre kallenavn" - -#: mod_muc/mod_muc_room.erl:3308 -msgid "Allow visitors to send voice requests" -msgstr "Tillat brukere å sende lyd forespørsler" - -#: mod_muc/mod_muc_room.erl:3311 -msgid "Minimum interval between voice requests (in seconds)" -msgstr "Minimums interval mellom lyd forespørsler (i sekunder)" - -#: mod_muc/mod_muc_room.erl:3317 -msgid "Make room CAPTCHA protected" -msgstr "Gjør rommet CAPTCHA beskyttet" - -#: mod_muc/mod_muc_room.erl:3322 -msgid "Exclude Jabber IDs from CAPTCHA challenge" -msgstr "Ekskluder Jabber IDer fra CAPTCHA utfordring" - -#: mod_muc/mod_muc_room.erl:3329 -msgid "Enable logging" -msgstr "Slå på logging" - -#: mod_muc/mod_muc_room.erl:3337 -msgid "You need an x:data capable client to configure room" -msgstr "Du trenger en klient som støtter x:data for å " - -#: mod_muc/mod_muc_room.erl:3694 -msgid "Number of occupants" -msgstr "Antall deltakere" - -#: mod_muc/mod_muc_room.erl:3750 -msgid "private, " -msgstr "privat, " - -#: mod_muc/mod_muc_room.erl:3799 -msgid "Voice request" -msgstr "Lyd forespørsel" - -#: mod_muc/mod_muc_room.erl:3803 -msgid "Either approve or decline the voice request." -msgstr "Enten godkjenn eller forby lyd forespørselen" - -#: mod_muc/mod_muc_room.erl:3809 -msgid "User JID" -msgstr "Bruker JID" - -#: mod_muc/mod_muc_room.erl:3811 -msgid "Grant voice to this person?" -msgstr "Gi lyd til denne personen?" - -#: mod_muc/mod_muc_room.erl:3960 -msgid "~s invites you to the room ~s" -msgstr "~s inviterer deg til rommet ~s" - -#: mod_muc/mod_muc_room.erl:3969 -msgid "the password is" -msgstr "passordet er" - -#: mod_offline.erl:510 mod_offline_odbc.erl:352 -msgid "" -"Your contact offline message queue is full. The message has been discarded." -msgstr "Kontaktens frakoblede meldingskø er full. Meldingen har blitt kassert." - -#: mod_offline.erl:560 mod_offline_odbc.erl:408 -msgid "~s's Offline Messages Queue" -msgstr "~ss kø for Frakoblede Meldinger" - -#: mod_offline.erl:563 mod_offline_odbc.erl:411 mod_roster.erl:987 -#: mod_roster_odbc.erl:1104 mod_shared_roster.erl:901 -#: mod_shared_roster.erl:1010 mod_shared_roster_odbc.erl:930 -#: mod_shared_roster_odbc.erl:1039 web/ejabberd_web_admin.erl:833 -#: web/ejabberd_web_admin.erl:874 web/ejabberd_web_admin.erl:942 -#: web/ejabberd_web_admin.erl:978 web/ejabberd_web_admin.erl:1019 -#: web/ejabberd_web_admin.erl:1508 web/ejabberd_web_admin.erl:1760 -#: web/ejabberd_web_admin.erl:1931 web/ejabberd_web_admin.erl:1963 -#: web/ejabberd_web_admin.erl:2031 web/ejabberd_web_admin.erl:2135 -#: web/ejabberd_web_admin.erl:2160 web/ejabberd_web_admin.erl:2248 -msgid "Submitted" -msgstr "Innsendt" - -#: mod_offline.erl:571 -msgid "Time" -msgstr "Tid" - -#: mod_offline.erl:572 -msgid "From" -msgstr "Fra" - -#: mod_offline.erl:573 -msgid "To" -msgstr "Til" - -#: mod_offline.erl:574 mod_offline_odbc.erl:419 -msgid "Packet" -msgstr "Pakke" - -#: mod_offline.erl:587 mod_offline_odbc.erl:432 mod_shared_roster.erl:908 -#: mod_shared_roster_odbc.erl:937 web/ejabberd_web_admin.erl:882 -#: web/ejabberd_web_admin.erl:986 -msgid "Delete Selected" -msgstr "Slett valgte" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -msgid "Offline Messages:" -msgstr "Frakoblede Meldinger:" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -msgid "Remove All Offline Messages" -msgstr "Fjern Alle Frakoblede Meldinger" - -#: mod_proxy65/mod_proxy65_service.erl:213 -msgid "ejabberd SOCKS5 Bytestreams module" -msgstr "ejabberd SOCKS5 Bytestreams modul" - -#: mod_pubsub/mod_pubsub.erl:1120 mod_pubsub/mod_pubsub_odbc.erl:929 -msgid "Publish-Subscribe" -msgstr "Publish-Subscribe" - -#: mod_pubsub/mod_pubsub.erl:1214 mod_pubsub/mod_pubsub_odbc.erl:1025 -msgid "ejabberd Publish-Subscribe module" -msgstr "ejabberd Publish-Subscribe modul" - -#: mod_pubsub/mod_pubsub.erl:1497 mod_pubsub/mod_pubsub_odbc.erl:1312 -msgid "PubSub subscriber request" -msgstr "PubSub abonements forespørsel" - -#: mod_pubsub/mod_pubsub.erl:1499 mod_pubsub/mod_pubsub_odbc.erl:1314 -msgid "Choose whether to approve this entity's subscription." -msgstr "Velg om du vil godkjenne denne eksistensens abonement" - -#: mod_pubsub/mod_pubsub.erl:1505 mod_pubsub/mod_pubsub_odbc.erl:1320 -msgid "Node ID" -msgstr "Node ID" - -#: mod_pubsub/mod_pubsub.erl:1510 mod_pubsub/mod_pubsub_odbc.erl:1325 -msgid "Subscriber Address" -msgstr "Abonnements Adresse" - -#: mod_pubsub/mod_pubsub.erl:1516 mod_pubsub/mod_pubsub_odbc.erl:1331 -msgid "Allow this Jabber ID to subscribe to this pubsub node?" -msgstr "Tillat denne Jabber ID å abonnere på denne pubsub " - -#: mod_pubsub/mod_pubsub.erl:3474 mod_pubsub/mod_pubsub_odbc.erl:3287 -msgid "Deliver payloads with event notifications" -msgstr "Send innhold sammen med kunngjøringer" - -#: mod_pubsub/mod_pubsub.erl:3475 mod_pubsub/mod_pubsub_odbc.erl:3288 -msgid "Deliver event notifications" -msgstr "Lever begivenhets kunngjøringer" - -#: mod_pubsub/mod_pubsub.erl:3476 mod_pubsub/mod_pubsub_odbc.erl:3289 -msgid "Notify subscribers when the node configuration changes" -msgstr "Informer abonnenter når node konfigurasjonen endres" - -#: mod_pubsub/mod_pubsub.erl:3477 mod_pubsub/mod_pubsub_odbc.erl:3290 -msgid "Notify subscribers when the node is deleted" -msgstr "Informer abonnenter når noden slettes" - -#: mod_pubsub/mod_pubsub.erl:3478 mod_pubsub/mod_pubsub_odbc.erl:3291 -msgid "Notify subscribers when items are removed from the node" -msgstr "Informer abonnenter når elementer fjernes fra noden" - -#: mod_pubsub/mod_pubsub.erl:3479 mod_pubsub/mod_pubsub_odbc.erl:3292 -msgid "Persist items to storage" -msgstr "Vedvarende elementer til lagring" - -#: mod_pubsub/mod_pubsub.erl:3480 mod_pubsub/mod_pubsub_odbc.erl:3293 -msgid "A friendly name for the node" -msgstr "Et vennlig navn for noden" - -#: mod_pubsub/mod_pubsub.erl:3481 mod_pubsub/mod_pubsub_odbc.erl:3294 -msgid "Max # of items to persist" -msgstr "Høyeste # elementer som skal lagres" - -#: mod_pubsub/mod_pubsub.erl:3482 mod_pubsub/mod_pubsub_odbc.erl:3295 -msgid "Whether to allow subscriptions" -msgstr "Om man skal tillate abonnenter" - -#: mod_pubsub/mod_pubsub.erl:3483 mod_pubsub/mod_pubsub_odbc.erl:3296 -msgid "Specify the access model" -msgstr "Spesifiser aksess modellen" - -#: mod_pubsub/mod_pubsub.erl:3486 mod_pubsub/mod_pubsub_odbc.erl:3299 -msgid "Roster groups allowed to subscribe" -msgstr "Kontaktliste grupper som tillates å abonnere" - -#: mod_pubsub/mod_pubsub.erl:3487 mod_pubsub/mod_pubsub_odbc.erl:3300 -msgid "Specify the publisher model" -msgstr "Angi publiserings modell" - -#: mod_pubsub/mod_pubsub.erl:3489 mod_pubsub/mod_pubsub_odbc.erl:3302 -msgid "Purge all items when the relevant publisher goes offline" -msgstr "Rydd alle elementer når den aktuelle utgiveren logger av" - -#: mod_pubsub/mod_pubsub.erl:3490 mod_pubsub/mod_pubsub_odbc.erl:3303 -msgid "Specify the event message type" -msgstr "Spesifiser hendelsesbeskjed type" - -#: mod_pubsub/mod_pubsub.erl:3492 mod_pubsub/mod_pubsub_odbc.erl:3305 -msgid "Max payload size in bytes" -msgstr "Største innholdsstørrelse i byte" - -#: mod_pubsub/mod_pubsub.erl:3493 mod_pubsub/mod_pubsub_odbc.erl:3306 -msgid "When to send the last published item" -msgstr "Når skal siste publiserte artikkel sendes" - -#: mod_pubsub/mod_pubsub.erl:3495 mod_pubsub/mod_pubsub_odbc.erl:3308 -msgid "Only deliver notifications to available users" -msgstr "Send kunngjøringer bare til tilgjengelige brukere" - -#: mod_pubsub/mod_pubsub.erl:3496 mod_pubsub/mod_pubsub_odbc.erl:3309 -msgid "The collections with which a node is affiliated" -msgstr "Samlingene som en node er assosiert med" - -#: mod_register.erl:193 -msgid "The CAPTCHA verification has failed" -msgstr "CAPTCHA godkjenningen har feilet" - -#: mod_register.erl:220 -msgid "You need a client that supports x:data and CAPTCHA to register" -msgstr "Du trenger en klient som støtter x:data og CAPTCHA for registrering " - -#: mod_register.erl:226 mod_register.erl:265 -msgid "Choose a username and password to register with this server" -msgstr "Velg et brukernavn og passord for å registrere på " - -#: mod_register.erl:230 mod_vcard.erl:364 mod_vcard_odbc.erl:342 -#: web/ejabberd_web_admin.erl:1515 web/ejabberd_web_admin.erl:1572 -msgid "User" -msgstr "Bruker" - -#: mod_register.erl:316 mod_register.erl:361 -msgid "The password is too weak" -msgstr "Passordet er for svakt" - -#: mod_register.erl:365 -msgid "Users are not allowed to register accounts so quickly" -msgstr "Brukere får ikke lov til registrere kontoer så fort" - -#: mod_roster.erl:938 mod_roster_odbc.erl:1055 web/ejabberd_web_admin.erl:1701 -#: web/ejabberd_web_admin.erl:1886 web/ejabberd_web_admin.erl:1897 -#: web/ejabberd_web_admin.erl:2219 -msgid "None" -msgstr "Ingen" - -#: mod_roster.erl:945 mod_roster_odbc.erl:1062 -msgid "Subscription" -msgstr "Abonnement" - -#: mod_roster.erl:946 mod_roster_odbc.erl:1063 -msgid "Pending" -msgstr "Ventende" - -#: mod_roster.erl:947 mod_roster_odbc.erl:1064 -msgid "Groups" -msgstr "Grupper" - -#: mod_roster.erl:974 mod_roster_odbc.erl:1091 -msgid "Validate" -msgstr "Bekrefte gyldighet" - -#: mod_roster.erl:982 mod_roster_odbc.erl:1099 -msgid "Remove" -msgstr "Fjern" - -#: mod_roster.erl:985 mod_roster_odbc.erl:1102 -msgid "Roster of " -msgstr "Kontaktliste for " - -#: mod_roster.erl:988 mod_roster_odbc.erl:1105 mod_shared_roster.erl:902 -#: mod_shared_roster.erl:1011 mod_shared_roster_odbc.erl:931 -#: mod_shared_roster_odbc.erl:1040 web/ejabberd_web_admin.erl:834 -#: web/ejabberd_web_admin.erl:875 web/ejabberd_web_admin.erl:943 -#: web/ejabberd_web_admin.erl:979 web/ejabberd_web_admin.erl:1020 -#: web/ejabberd_web_admin.erl:1509 web/ejabberd_web_admin.erl:1761 -#: web/ejabberd_web_admin.erl:1932 web/ejabberd_web_admin.erl:2136 -#: web/ejabberd_web_admin.erl:2161 -msgid "Bad format" -msgstr "Feil format" - -#: mod_roster.erl:995 mod_roster_odbc.erl:1112 -msgid "Add Jabber ID" -msgstr "Legg til Jabber ID" - -#: mod_roster.erl:1094 mod_roster_odbc.erl:1211 -msgid "Roster" -msgstr "Kontaktliste" - -#: mod_shared_roster.erl:857 mod_shared_roster.erl:899 -#: mod_shared_roster.erl:1007 mod_shared_roster_odbc.erl:886 -#: mod_shared_roster_odbc.erl:928 mod_shared_roster_odbc.erl:1036 -msgid "Shared Roster Groups" -msgstr "Delte Kontaktgrupper" - -#: mod_shared_roster.erl:895 mod_shared_roster_odbc.erl:924 -#: web/ejabberd_web_admin.erl:1365 web/ejabberd_web_admin.erl:2461 -msgid "Add New" -msgstr "Legg til ny" - -#: mod_shared_roster.erl:978 mod_shared_roster_odbc.erl:1007 -msgid "Name:" -msgstr "Navn:" - -#: mod_shared_roster.erl:983 mod_shared_roster_odbc.erl:1012 -msgid "Description:" -msgstr "Beskrivelse:" - -#: mod_shared_roster.erl:991 mod_shared_roster_odbc.erl:1020 -msgid "Members:" -msgstr "Medlemmer:" - -#: mod_shared_roster.erl:999 mod_shared_roster_odbc.erl:1028 -msgid "Displayed Groups:" -msgstr "Viste grupper:" - -#: mod_shared_roster.erl:1008 mod_shared_roster_odbc.erl:1037 -msgid "Group " -msgstr "Gruppe " - -#: mod_shared_roster.erl:1017 mod_shared_roster_odbc.erl:1046 -#: web/ejabberd_web_admin.erl:840 web/ejabberd_web_admin.erl:884 -#: web/ejabberd_web_admin.erl:949 web/ejabberd_web_admin.erl:1026 -#: web/ejabberd_web_admin.erl:2017 -msgid "Submit" -msgstr "Send" - -#: mod_vcard.erl:165 mod_vcard_ldap.erl:238 mod_vcard_odbc.erl:129 -msgid "Erlang Jabber Server" -msgstr "Erlang Jabber Server" - -#: mod_vcard.erl:364 mod_vcard.erl:478 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:454 -msgid "Birthday" -msgstr "Fødselsdag" - -#: mod_vcard.erl:364 mod_vcard.erl:480 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:456 -msgid "City" -msgstr "By" - -#: mod_vcard.erl:364 mod_vcard.erl:479 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:455 -msgid "Country" -msgstr "Land" - -#: mod_vcard.erl:364 mod_vcard.erl:481 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:457 -msgid "Email" -msgstr "Epost" - -#: mod_vcard.erl:364 mod_vcard.erl:476 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:452 -msgid "Family Name" -msgstr "Etternavn" - -#: mod_vcard.erl:364 mod_vcard_odbc.erl:342 -msgid "" -"Fill in the form to search for any matching Jabber User (Add * to the end of " -"field to match substring)" -msgstr "" -"Fyll inn skjemaet for å søke etter Jabber bruker (Legg til * på slutten av " -"feltet for å treffe alle som starter slik)" - -#: mod_vcard.erl:364 mod_vcard.erl:473 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:449 -msgid "Full Name" -msgstr "Fullstendig Navn" - -#: mod_vcard.erl:364 mod_vcard.erl:475 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:451 -msgid "Middle Name" -msgstr "Mellomnavn" - -#: mod_vcard.erl:364 mod_vcard.erl:474 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:450 web/ejabberd_web_admin.erl:2006 -msgid "Name" -msgstr "Navn" - -#: mod_vcard.erl:364 mod_vcard.erl:482 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:458 -msgid "Organization Name" -msgstr "Organisasjonsnavn" - -#: mod_vcard.erl:364 mod_vcard.erl:483 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:459 -msgid "Organization Unit" -msgstr "Organisasjonsenhet" - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "Search users in " -msgstr "Søk etter brukere i " - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "You need an x:data capable client to search" -msgstr "Du tregner en klient som støtter x:data for å kunne " - -#: mod_vcard.erl:389 mod_vcard_ldap.erl:487 mod_vcard_odbc.erl:367 -msgid "vCard User Search" -msgstr "vCard Bruker Søk" - -#: mod_vcard.erl:445 mod_vcard_ldap.erl:541 mod_vcard_odbc.erl:421 -msgid "ejabberd vCard module" -msgstr "ejabberd vCard modul" - -#: mod_vcard.erl:469 mod_vcard_ldap.erl:551 mod_vcard_odbc.erl:445 -msgid "Search Results for " -msgstr "Søke Resultater for " - -#: mod_vcard_ldap.erl:462 -msgid "Fill in fields to search for any matching Jabber User" -msgstr "Fyll inn felt for å søke etter Jabber brukere" - -#: web/ejabberd_web_admin.erl:193 web/ejabberd_web_admin.erl:203 -#: web/ejabberd_web_admin.erl:219 web/ejabberd_web_admin.erl:229 -msgid "Unauthorized" -msgstr "Uautorisert" - -#: web/ejabberd_web_admin.erl:286 web/ejabberd_web_admin.erl:303 -msgid "ejabberd Web Admin" -msgstr "ejabberd Web Admin" - -#: web/ejabberd_web_admin.erl:769 web/ejabberd_web_admin.erl:780 -msgid "Administration" -msgstr "Administrasjon" - -#: web/ejabberd_web_admin.erl:878 web/ejabberd_web_admin.erl:982 -msgid "Raw" -msgstr "Rå" - -#: web/ejabberd_web_admin.erl:1017 -msgid "~s access rule configuration" -msgstr "tilgangsregel konfigurasjon for ~s" - -#: web/ejabberd_web_admin.erl:1035 -msgid "Virtual Hosts" -msgstr "Virtuella Maskiner" - -#: web/ejabberd_web_admin.erl:1043 web/ejabberd_web_admin.erl:1050 -msgid "Users" -msgstr "Brukere" - -#: web/ejabberd_web_admin.erl:1078 -msgid "Users Last Activity" -msgstr "Brukers Siste Aktivitet" - -#: web/ejabberd_web_admin.erl:1080 -msgid "Period: " -msgstr "Periode: " - -#: web/ejabberd_web_admin.erl:1090 -msgid "Last month" -msgstr "Siste måned" - -#: web/ejabberd_web_admin.erl:1091 -msgid "Last year" -msgstr "Siste året" - -#: web/ejabberd_web_admin.erl:1092 -msgid "All activity" -msgstr "All aktivitet" - -#: web/ejabberd_web_admin.erl:1094 -msgid "Show Ordinary Table" -msgstr "Vis Ordinær Tabell" - -#: web/ejabberd_web_admin.erl:1096 -msgid "Show Integral Table" -msgstr "Vis Integral Tabell" - -#: web/ejabberd_web_admin.erl:1105 web/ejabberd_web_admin.erl:1939 -msgid "Statistics" -msgstr "Statistikk" - -#: web/ejabberd_web_admin.erl:1117 -msgid "Not Found" -msgstr "Finnes Ikke" - -#: web/ejabberd_web_admin.erl:1134 -msgid "Node not found" -msgstr "Noden finnes ikke" - -#: web/ejabberd_web_admin.erl:1460 -msgid "Host" -msgstr "Maskin" - -#: web/ejabberd_web_admin.erl:1461 -msgid "Registered Users" -msgstr "Registrerte Brukere" - -#: web/ejabberd_web_admin.erl:1573 -msgid "Offline Messages" -msgstr "Frakoblede Meldinger" - -#: web/ejabberd_web_admin.erl:1574 web/ejabberd_web_admin.erl:1767 -msgid "Last Activity" -msgstr "Siste Aktivitet" - -#: web/ejabberd_web_admin.erl:1659 web/ejabberd_web_admin.erl:1675 -msgid "Registered Users:" -msgstr "Registrerte Brukere:" - -#: web/ejabberd_web_admin.erl:1661 web/ejabberd_web_admin.erl:1677 -#: web/ejabberd_web_admin.erl:2192 -msgid "Online Users:" -msgstr "Tilkoblede Brukere:" - -#: web/ejabberd_web_admin.erl:1663 -msgid "Outgoing s2s Connections:" -msgstr "Utgående s2s Koblinger" - -#: web/ejabberd_web_admin.erl:1665 -msgid "Outgoing s2s Servers:" -msgstr "Utgående s2s Tjenere" - -#: web/ejabberd_web_admin.erl:1734 web/mod_register_web.erl:175 -#: web/mod_register_web.erl:372 web/mod_register_web.erl:381 -#: web/mod_register_web.erl:411 -msgid "Change Password" -msgstr "Endre Passord" - -#: web/ejabberd_web_admin.erl:1758 -msgid "User " -msgstr "Bruker " - -#: web/ejabberd_web_admin.erl:1765 -msgid "Connected Resources:" -msgstr "Tilkoblede Ressurser:" - -#: web/ejabberd_web_admin.erl:1766 web/mod_register_web.erl:227 -#: web/mod_register_web.erl:519 -msgid "Password:" -msgstr "Passord:" - -#: web/ejabberd_web_admin.erl:1828 -msgid "No Data" -msgstr "Ingen Data" - -#: web/ejabberd_web_admin.erl:1906 -msgid "Nodes" -msgstr "Noder" - -#: web/ejabberd_web_admin.erl:1929 web/ejabberd_web_admin.erl:1951 -msgid "Node " -msgstr "Node " - -#: web/ejabberd_web_admin.erl:1938 -msgid "Listened Ports" -msgstr "Lyttende Porter" - -#: web/ejabberd_web_admin.erl:1940 web/ejabberd_web_admin.erl:2260 -#: web/ejabberd_web_admin.erl:2448 -msgid "Update" -msgstr "Oppdatere" - -#: web/ejabberd_web_admin.erl:1943 web/ejabberd_web_admin.erl:2569 -msgid "Restart" -msgstr "Starte på nytt" - -#: web/ejabberd_web_admin.erl:1945 web/ejabberd_web_admin.erl:2571 -msgid "Stop" -msgstr "Stoppe" - -#: web/ejabberd_web_admin.erl:1959 -msgid "RPC Call Error" -msgstr "RPC Kall Feil" - -#: web/ejabberd_web_admin.erl:2000 -msgid "Database Tables at " -msgstr "Database Tabeller på " - -#: web/ejabberd_web_admin.erl:2007 -msgid "Storage Type" -msgstr "Lagringstype" - -#: web/ejabberd_web_admin.erl:2008 -msgid "Elements" -msgstr "Elementer" - -#: web/ejabberd_web_admin.erl:2009 -msgid "Memory" -msgstr "Minne" - -#: web/ejabberd_web_admin.erl:2032 web/ejabberd_web_admin.erl:2137 -msgid "Error" -msgstr "Feil" - -#: web/ejabberd_web_admin.erl:2034 -msgid "Backup of " -msgstr "Sikkerhetskopi av " - -#: web/ejabberd_web_admin.erl:2036 -msgid "" -"Please note that these options will only backup the builtin Mnesia database. " -"If you are using the ODBC module, you also need to backup your SQL database " -"separately." -msgstr "" -"Merk at disse valgene vil bare sikkerhetskopiere den innebygde Mnesia " -"databasen. Dersom du bruker ODBC modulen må du også ta backup av din SQL " -"database." - -#: web/ejabberd_web_admin.erl:2041 -msgid "Store binary backup:" -msgstr "Lagre binær sikkerhetskopi:" - -#: web/ejabberd_web_admin.erl:2045 web/ejabberd_web_admin.erl:2052 -#: web/ejabberd_web_admin.erl:2060 web/ejabberd_web_admin.erl:2067 -#: web/ejabberd_web_admin.erl:2074 web/ejabberd_web_admin.erl:2081 -#: web/ejabberd_web_admin.erl:2088 web/ejabberd_web_admin.erl:2096 -#: web/ejabberd_web_admin.erl:2103 web/ejabberd_web_admin.erl:2110 -msgid "OK" -msgstr "OK" - -#: web/ejabberd_web_admin.erl:2048 -msgid "Restore binary backup immediately:" -msgstr "Gjenopprette binær backup umiddelbart:" - -#: web/ejabberd_web_admin.erl:2056 -msgid "" -"Restore binary backup after next ejabberd restart (requires less memory):" -msgstr "" -"Gjenopprette binær backup etter neste ejabberd omstart (krever mindre minne):" - -#: web/ejabberd_web_admin.erl:2063 -msgid "Store plain text backup:" -msgstr "Lagre rentekst sikkerhetskopi:" - -#: web/ejabberd_web_admin.erl:2070 -msgid "Restore plain text backup immediately:" -msgstr "Gjenopprette rentekst sikkerhetskopi umiddelbart:" - -#: web/ejabberd_web_admin.erl:2077 -msgid "Import users data from a PIEFXIS file (XEP-0227):" -msgstr "Importer brukeres data fra en PIEFXIS fil (XEP-0227):" - -#: web/ejabberd_web_admin.erl:2084 -msgid "Export data of all users in the server to PIEFXIS files (XEP-0227):" -msgstr "Eksporter data om alle brukere i en server til PIEFXIS filer" - -#: web/ejabberd_web_admin.erl:2091 -msgid "Export data of users in a host to PIEFXIS files (XEP-0227):" -msgstr "Eksporter data om alle brukere i en host til PIEFXIS filer (XEP-0227):" - -#: web/ejabberd_web_admin.erl:2099 -msgid "Import user data from jabberd14 spool file:" -msgstr "Importer bruker data fra jabberd14 spoolfiler:" - -#: web/ejabberd_web_admin.erl:2106 -msgid "Import users data from jabberd14 spool directory:" -msgstr "Importer brukeres data fra jabberd14 spoolfil katalog:" - -#: web/ejabberd_web_admin.erl:2132 -msgid "Listened Ports at " -msgstr "Lyttende Porter på " - -#: web/ejabberd_web_admin.erl:2157 -msgid "Modules at " -msgstr "Moduler på " - -#: web/ejabberd_web_admin.erl:2183 -msgid "Statistics of ~p" -msgstr "Statistikk for ~p" - -#: web/ejabberd_web_admin.erl:2186 -msgid "Uptime:" -msgstr "Oppetid:" - -#: web/ejabberd_web_admin.erl:2189 -msgid "CPU Time:" -msgstr "CPU Tid:" - -#: web/ejabberd_web_admin.erl:2195 -msgid "Transactions Committed:" -msgstr "Sendte Transaksjoner:" - -#: web/ejabberd_web_admin.erl:2198 -msgid "Transactions Aborted:" -msgstr "Avbrutte Transasksjoner:" - -#: web/ejabberd_web_admin.erl:2201 -msgid "Transactions Restarted:" -msgstr "Omstartede Transasksjoner:" - -#: web/ejabberd_web_admin.erl:2204 -msgid "Transactions Logged:" -msgstr "Loggede Transasksjoner:" - -#: web/ejabberd_web_admin.erl:2246 -msgid "Update " -msgstr "Oppdater " - -#: web/ejabberd_web_admin.erl:2254 -msgid "Update plan" -msgstr "Oppdaterings plan" - -#: web/ejabberd_web_admin.erl:2255 -msgid "Modified modules" -msgstr "Endrede moduler" - -#: web/ejabberd_web_admin.erl:2256 -msgid "Update script" -msgstr "Oppdaterings skript" - -#: web/ejabberd_web_admin.erl:2257 -msgid "Low level update script" -msgstr "Lavnivå oppdaterings skript" - -#: web/ejabberd_web_admin.erl:2258 -msgid "Script check" -msgstr "Skript sjekk" - -#: web/ejabberd_web_admin.erl:2426 -msgid "Port" -msgstr "Port" - -#: web/ejabberd_web_admin.erl:2427 -msgid "IP" -msgstr "IP" - -#: web/ejabberd_web_admin.erl:2428 -msgid "Protocol" -msgstr "Protokoll" - -#: web/ejabberd_web_admin.erl:2429 web/ejabberd_web_admin.erl:2556 -msgid "Module" -msgstr "Modul" - -#: web/ejabberd_web_admin.erl:2430 web/ejabberd_web_admin.erl:2557 -msgid "Options" -msgstr "Alternativer" - -#: web/ejabberd_web_admin.erl:2450 -msgid "Delete" -msgstr "Slett" - -#: web/ejabberd_web_admin.erl:2579 -msgid "Start" -msgstr "Start" - -#: web/mod_register_web.erl:104 -msgid "Your Jabber account was successfully created." -msgstr "Din Jabber konto ble opprettet" - -#: web/mod_register_web.erl:107 -msgid "There was an error creating the account: " -msgstr "En feil skjedde under oppretting av kontoen:" - -#: web/mod_register_web.erl:115 -msgid "Your Jabber account was successfully deleted." -msgstr "Dni Jabber konto er blitt sltettet." - -#: web/mod_register_web.erl:118 -msgid "There was an error deleting the account: " -msgstr "En feil skjedde under sletting av kontoen: " - -#: web/mod_register_web.erl:128 -msgid "The password of your Jabber account was successfully changed." -msgstr "Passordet for din Jabber konto ble endret." - -#: web/mod_register_web.erl:131 -msgid "There was an error changing the password: " -msgstr "En feil skjedde under endring av passordet:" - -#: web/mod_register_web.erl:163 web/mod_register_web.erl:172 -msgid "Jabber Account Registration" -msgstr "Jabber Konto Registrering" - -#: web/mod_register_web.erl:174 web/mod_register_web.erl:192 -#: web/mod_register_web.erl:201 -msgid "Register a Jabber account" -msgstr "Registrer en Jabber konto" - -#: web/mod_register_web.erl:176 web/mod_register_web.erl:493 -#: web/mod_register_web.erl:502 -msgid "Unregister a Jabber account" -msgstr "Avregistrer en Jabber konto" - -#: web/mod_register_web.erl:203 -msgid "" -"This page allows to create a Jabber account in this Jabber server. Your JID " -"(Jabber IDentifier) will be of the form: username@server. Please read " -"carefully the instructions to fill correctly the fields." -msgstr "" -"Denne siden lar deg lage en Jabber konto på denne Jabber serveren. Din JID " -"(Jabber ID) vil være i formatet: brukernavn@server. Vennligst les " -"instruksjonene nøye slik at du fyller ut skjemaet riktig." - -#: web/mod_register_web.erl:212 web/mod_register_web.erl:386 -#: web/mod_register_web.erl:509 -msgid "Username:" -msgstr "Brukernavn:" - -#: web/mod_register_web.erl:217 -msgid "This is case insensitive: macbeth is the same that MacBeth and Macbeth." -msgstr "" -"Denne er ufølsom for små og store bokstaver: macbeth er det samme som " -"MacBeth og Macbeth. " - -#: web/mod_register_web.erl:218 -msgid "Characters not allowed:" -msgstr "Ikke godtatte tegn:" - -#: web/mod_register_web.erl:222 web/mod_register_web.erl:391 -#: web/mod_register_web.erl:514 -msgid "Server:" -msgstr "Server:" - -#: web/mod_register_web.erl:232 -msgid "" -"Don't tell your password to anybody, not even the administrators of the " -"Jabber server." -msgstr "" -"Ikke fortell passordet til noen, ikke en gang til administratoren av Jabber " -"serveren." - -#: web/mod_register_web.erl:234 -msgid "You can later change your password using a Jabber client." -msgstr "Du kan når som helst endre passordet via en Jabber klient." - -#: web/mod_register_web.erl:235 -msgid "" -"Some Jabber clients can store your password in your computer. Use that " -"feature only if you trust your computer is safe." -msgstr "" -"Noen Jabber klienter kan lagre passordet på datamaskinen. Bruk bare den " -"funksjonen dersom du er sikker på at maskinen er trygg." - -#: web/mod_register_web.erl:237 -msgid "" -"Memorize your password, or write it in a paper placed in a safe place. In " -"Jabber there isn't an automated way to recover your password if you forget " -"it." -msgstr "" -"Husk passordet, eller skriv det ned på et papir lagret på et trygt sted. I " -"Jabber er det ingen automatisert måte å gjenskape passordet om du glemmer " -"det. " - -#: web/mod_register_web.erl:242 web/mod_register_web.erl:406 -msgid "Password Verification:" -msgstr "Passord Bekreftelse:" - -#: web/mod_register_web.erl:250 -msgid "Register" -msgstr "Registrer" - -#: web/mod_register_web.erl:396 -msgid "Old Password:" -msgstr "Gammelt Passord:" - -#: web/mod_register_web.erl:401 -msgid "New Password:" -msgstr "Nytt Passord:" - -#: web/mod_register_web.erl:504 -msgid "This page allows to unregister a Jabber account in this Jabber server." -msgstr "" -"Denne siden lar deg avregistrere en Jabber konto på denne Jabber serveren." - -#: web/mod_register_web.erl:524 -msgid "Unregister" -msgstr "Avregistrer" - -#, fuzzy -#~ msgid "Captcha test failed" -#~ msgstr "Captchaen er ikke gyldig" diff --git a/priv/msgs/pl.msg b/priv/msgs/pl.msg index 4bc206306..e4015091c 100644 --- a/priv/msgs/pl.msg +++ b/priv/msgs/pl.msg @@ -1,20 +1,24 @@ -{"Access Configuration","Konfiguracja dostępu"}. -{"Access Control List Configuration","Konfiguracja listy dostępowej"}. -{"Access Control Lists","Lista dostępowa"}. -{"Access control lists","Listy dostępowe"}. -{"Access denied by service policy","Dostęp zabroniony zgodnie z zasadami usługi"}. -{"Access rules","Reguły dostępu"}. -{"Access Rules","Zasady dostępu"}. -{"Action on user","Wykonaj na użytkowniku"}. -{"Add Jabber ID","Dodaj Jabber ID"}. -{"Add New","Dodaj nowe"}. -{"Add User","Dodaj użytkownika"}. -{"Administration","Administracja"}. -{"Administration of ","Zarządzanie "}. -{"Administrator privileges required","Wymagane uprawnienia administratora"}. +%% Generated automatically +%% DO NOT EDIT: run `make translations` instead +%% To improve translations please read: +%% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ + +{" has set the subject to: "," zmienił temat na: "}. {"A friendly name for the node","Przyjazna nazwa węzła"}. +{"A password is required to enter this room","Aby wejść do pokoju wymagane jest hasło"}. +{"A Web Page","Strona sieci Web"}. +{"Accept","Zaakceptuj"}. +{"Access denied by service policy","Dostęp zabroniony zgodnie z zasadami usługi"}. +{"Action on user","Wykonaj na użytkowniku"}. +{"Add a hat to a user","Dodaj kapelusz do użytkownika"}. +{"Add User","Dodaj użytkownika"}. +{"Administration of ","Zarządzanie "}. +{"Administration","Administracja"}. +{"Administrator privileges required","Wymagane uprawnienia administratora"}. {"All activity","Cała aktywność"}. +{"All Users","Wszyscy użytkownicy"}. {"Allow this Jabber ID to subscribe to this pubsub node?","Pozwól temu Jabber ID na zapisanie się do tego węzła PubSub"}. +{"Allow this person to register with the room?","Pozwolić tej osobie zarejestrować się w tym pokoju?"}. {"Allow users to change the subject","Pozwól użytkownikom zmieniać temat"}. {"Allow users to query other users","Pozwól użytkownikom pobierać informacje o innych użytkownikach"}. {"Allow users to send invites","Pozwól użytkownikom wysyłać zaproszenia"}. @@ -23,21 +27,25 @@ {"Allow visitors to send private messages to","Pozwól użytkownikom wysyłać prywatne wiadomości"}. {"Allow visitors to send status text in presence updates","Pozwól uczestnikom na wysyłanie statusów opisowych"}. {"Allow visitors to send voice requests","Pozwól użytkownikom wysyłać zaproszenia"}. -{"All Users","Wszyscy użytkownicy"}. {"Announcements","Powiadomienia"}. -{"anyone","wszystkich"}. -{"A password is required to enter this room","Aby wejść do pokoju wymagane jest hasło"}. {"April","Kwiecień"}. {"August","Sierpień"}. +{"Automatic node creation is not enabled","Automatyczne tworzenie węzłów nie zostało włączone"}. {"Backup Management","Zarządzanie kopiami zapasowymi"}. -{"Backup of ","Kopia zapasowa "}. +{"Backup of ~p","Kopia zapasowa ~p"}. {"Backup to File at ","Zapisz kopię w pliku na "}. -{"Backup","Wykonaj kopie"}. +{"Backup","Kopia zapasowa"}. {"Bad format","Błędny format"}. {"Birthday","Data urodzenia"}. +{"Both the username and the resource are required","Wymagana jest zarówno nazwa użytkownika jak i zasób"}. +{"Bytestream already activated","Strumień danych został już aktywowany"}. +{"Cannot remove active list","Nie można usunąć aktywnej listy"}. +{"Cannot remove default list","Nie można usunąć domyślnej listy"}. {"CAPTCHA web page","Strona internetowa CAPTCHA"}. {"Change Password","Zmień hasło"}. {"Change User Password","Zmień hasło użytkownika"}. +{"Changing password is not allowed","Zmiana hasła jest niedopuszczalna"}. +{"Changing role/affiliation is not allowed","Zmiana roli jest niedopuszczalna"}. {"Characters not allowed:","Te znaki są niedozwolone:"}. {"Chatroom configuration modified","Konfiguracja pokoju zmodyfikowana"}. {"Chatroom is created","Pokój został stworzony"}. @@ -46,122 +54,102 @@ {"Chatroom is stopped","Pokój został zatrzymany"}. {"Chatrooms","Pokoje rozmów"}. {"Choose a username and password to register with this server","Wybierz nazwę użytkownika i hasło aby zarejestrować się na tym serwerze"}. -{"Choose modules to stop","Wybierz moduły do zatrzymania"}. {"Choose storage type of tables","Wybierz typ bazy dla tablel"}. {"Choose whether to approve this entity's subscription.","Wybierz, czy akceptować subskrypcję tej jednostki"}. {"City","Miasto"}. {"Commands","Polecenia"}. {"Conference room does not exist","Pokój konferencyjny nie istnieje"}. -{"Configuration","Konfiguracja"}. {"Configuration of room ~s","Konfiguracja pokoju ~s"}. -{"Connected Resources:","Zasoby zalogowane:"}. -{"Connections parameters","Parametry połączeń"}. +{"Configuration","Konfiguracja"}. {"Country","Państwo"}. -{"CPU Time:","Czas CPU:"}. -{"Database","Baza danych"}. -{"Database Tables at ","Tabele bazy na "}. +{"Database failure","Błąd bazy danych"}. {"Database Tables Configuration at ","Konfiguracja tabel bazy na "}. +{"Database","Baza danych"}. {"December","Grudzień"}. {"Default users as participants","Domyślni użytkownicy jako uczestnicy"}. {"Delete message of the day on all hosts","Usuń wiadomość dnia ze wszystkich hostów"}. {"Delete message of the day","Usuń wiadomość dnia"}. -{"Delete Selected","Usuń zaznaczone"}. {"Delete User","Usuń użytkownika"}. -{"Delete","Usuń"}. {"Deliver event notifications","Dostarczaj powiadomienia o zdarzeniach"}. {"Deliver payloads with event notifications","Dostarczaj zawartość publikacji wraz z powiadomieniami o zdarzeniach"}. -{"Description:","Opis:"}. {"Disc only copy","Kopia tylko na dysku"}. -{"Displayed Groups:","Wyświetlane grupy:"}. -{"Don't tell your password to anybody, not even the administrators of the Jabber server.","Nie podawaj swojego hasła nikomu, nawet administratorowi serwera Jabber."}. {"Dump Backup to Text File at ","Zapisz kopię zapasową w pliku tekstowym na "}. {"Dump to Text File","Wykonaj kopie do pliku tekstowego"}. {"Edit Properties","Edytuj właściwości"}. -{"Either approve or decline the voice request.","Zatwierdź lub odrzuć żądanie głosowe"}. -{"ejabberd IRC module","Moduł IRC ejabberd"}. +{"Either approve or decline the voice request.","Zatwierdź lub odrzuć żądanie głosowe."}. {"ejabberd MUC module","Moduł MUC"}. +{"ejabberd Multicast service","Serwis multicast ejabbera"}. {"ejabberd Publish-Subscribe module","Moduł Publish-Subscribe"}. {"ejabberd SOCKS5 Bytestreams module","Moduł SOCKS5 Bytestreams"}. {"ejabberd vCard module","Moduł vCard ejabberd"}. {"ejabberd Web Admin","ejabberd: Panel Administracyjny"}. -{"Elements","Elementy"}. {"Email","Email"}. {"Enable logging","Włącz logowanie"}. -{"Encoding for server ~b","Kodowanie znaków dla serwera ~b"}. +{"Enable message archiving","Włącz archiwizowanie rozmów"}. +{"Enabling push without 'node' attribute is not supported","Aktywacja 'push' bez węzła jest nie dostępna"}. {"End User Session","Zakończ sesję uzytkownika"}. -{"Enter list of {Module, [Options]}","Wprowadź listę {Moduł, [Opcje]}"}. {"Enter nickname you want to register","Wprowadz nazwę użytkownika którego chcesz zarejestrować"}. {"Enter path to backup file","Wprowadź scieżkę do pliku kopii zapasowej"}. {"Enter path to jabberd14 spool dir","Wprowadź ścieżkę do roboczego katalogu serwera jabberd14"}. {"Enter path to jabberd14 spool file","Wprowadź ścieżkę do roboczego pliku serwera jabberd14"}. {"Enter path to text file","Wprowadź scieżkę do pliku tekstowego"}. {"Enter the text you see","Przepisz tekst z obrazka"}. -{"Enter username and encodings you wish to use for connecting to IRC servers. Press 'Next' to get more fields to fill in. Press 'Complete' to save settings.","Wprowadź nazwę użytkownika i kodowania których chcesz używać do łączenia z serwerami IRC. Wciśnij \"Dalej\" aby ustawić więcej parametrów połączenia. Wciśnij \"Zakończ\" aby zapisać ustawienia."}. -{"Enter username, encodings, ports and passwords you wish to use for connecting to IRC servers","Wprowadź nazwę użytkownika, port i kodowanie, których chcesz używać do łączenia z serwerami IRC"}. -{"Erlang Jabber Server","Erlang Jabber Server"}. -{"Error","Błąd"}. -{"Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef.net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}].","Przykład: [{\"wroclaw.irc.pl\",\"utf-8\"}, {\"warszawa.irc.pl\", \"iso8859-2\"}]."}. {"Exclude Jabber IDs from CAPTCHA challenge","Pomiń Jabber ID z żądania CAPTCHA"}. +{"Export all tables as SQL queries to a file:","Wyeksportuj wszystkie tabele jako zapytania SQL do pliku:"}. {"Export data of all users in the server to PIEFXIS files (XEP-0227):","Eksportuj dane wszystkich użytkowników serwera do plików w formacie PIEFXIS (XEP-0227):"}. {"Export data of users in a host to PIEFXIS files (XEP-0227):","Eksportuj dane użytkowników z hosta do plików w formacie PIEFXIS (XEP-0227):"}. +{"External component failure","Błąd zewnętrznego komponentu"}. +{"External component timeout","Upłynął limit czasu zewnętrznego komponentu"}. +{"Failed to activate bytestream","Nie udało się aktywować strumienia danych"}. {"Failed to extract JID from your voice request approval","Nie udało się wydobyć JID-u z twojego żądania"}. +{"Failed to map delegated namespace to external component","Nie udało się znaleźć zewnętrznego komponentu na podstawie nazwy"}. +{"Failed to parse HTTP response","Nie udało się zanalizować odpowiedzi HTTP"}. +{"Failed to process option '~s'","Nie udało się przetworzyć opcji '~s'"}. {"Family Name","Nazwisko"}. {"February","Luty"}. -{"Fill in fields to search for any matching Jabber User","Wypełnij pola aby znaleźć pasujących użytkowników Jabbera"}. -{"Fill in the form to search for any matching Jabber User (Add * to the end of field to match substring)","Wypełnij formularz aby wyszukać użytkowników Jabbera (dodaj * na koniec zapytania aby wyszukać po fragmencie)"}. +{"File larger than ~w bytes","Plik jest większy niż ~w bajtów"}. {"Friday","Piątek"}. -{"From","Od"}. -{"From ~s","Od ~s"}. {"Full Name","Pełna nazwa"}. {"Get Number of Online Users","Pokaż liczbę zalogowanych użytkowników"}. {"Get Number of Registered Users","Pokaż liczbę zarejestrowanych użytkowników"}. {"Get User Last Login Time","Pokaż czas ostatniego zalogowania uzytkownika"}. -{"Get User Password","Pobierz hasło użytkownika"}. {"Get User Statistics","Pobierz statystyki użytkownika"}. +{"Given Name","Imię"}. {"Grant voice to this person?","Udzielić głosu tej osobie?"}. -{"Group ","Grupa "}. -{"Groups","Grupy"}. {"has been banned","został wykluczony"}. -{"has been kicked because of an affiliation change","został wyrzucony z powodu zmiany przynależności"}. {"has been kicked because of a system shutdown","został wyrzucony z powodu wyłączenia systemu"}. +{"has been kicked because of an affiliation change","został wyrzucony z powodu zmiany przynależności"}. {"has been kicked because the room has been changed to members-only","został wyrzucony z powodu zmiany pokoju na \"Tylko dla Członków\""}. {"has been kicked","został wyrzucony"}. -{" has set the subject to: "," zmienił temat na: "}. -{"Host","Host"}. +{"Host unknown","Nieznany host"}. {"If you don't see the CAPTCHA image here, visit the web page.","Jeśli nie widzisz obrazka CAPTCHA, odwiedź stronę internetową."}. -{"If you want to specify different ports, passwords, encodings for IRC servers, fill this list with values in format '{\"irc server\", \"encoding\", port, \"password\"}'. By default this service use \"~s\" encoding, port ~p, empty password.","Jeśli chcesz ustawić inne hasła, porty lub kodowania dla poszczególnych serwerów IRC, wypełnij tą listę wartościami w formacie '{\"irc server\",\"encoding\", port, \"password\"}'. Domyślne ta usługa używa kodowania \"~s\", portu ~p, bez hasła."}. {"Import Directory","Importuj katalog"}. {"Import File","Importuj plik"}. {"Import user data from jabberd14 spool file:","Importuj dane użytkownika z pliku roboczego serwera jabberd14:"}. {"Import User from File at ","Importuj użytkownika z pliku na "}. {"Import users data from a PIEFXIS file (XEP-0227):","Importuj dane użytkowników z pliku w formacie PIEFXIS (XEP-0227):"}. -{"Import users data from jabberd14 spool directory:","Importuj użytkowników z katalogu roboczego serwera jabberd14"}. +{"Import users data from jabberd14 spool directory:","Importuj użytkowników z katalogu roboczego serwera jabberd14:"}. {"Import Users from Dir at ","Importuj użytkowników z katalogu na "}. {"Import Users From jabberd14 Spool Files","Importuj użytkowników z plików roboczych serwera jabberd14"}. +{"Improper domain part of 'from' attribute","Nieprawidłowa domena atrybutu 'from'"}. {"Improper message type","Nieprawidłowy typ wiadomości"}. +{"Incorrect CAPTCHA submit","Nieprawidłowa odpowiedz dla CAPTCHA"}. +{"Incorrect data form","Nieprawidłowe dane w formatce"}. {"Incorrect password","Nieprawidłowe hasło"}. -{"Invalid affiliation: ~s","Nieprawidłowa przynależność: ~s"}. -{"Invalid role: ~s","Nieprawidłowa rola: ~s"}. +{"Incorrect value of 'action' attribute","Nieprawidłowe dane atrybutu 'action'"}. +{"Incorrect value of 'action' in data form","Nieprawidłowe dane atrybutu 'action'"}. +{"Incorrect value of 'path' in data form","Nieprawidłowe dane atrybutu 'path'"}. +{"Insufficient privilege","Niewystarczające uprawnienia"}. +{"Invalid 'from' attribute in forwarded message","Nieprawidłowy atrybut 'from' w przesyłanej dalej wiadomości"}. +{"Invitations are not allowed in this conference","Zaproszenia są wyłączone w tym pokoju"}. {"IP addresses","Adresy IP"}. -{"IP","IP"}. -{"IRC channel (don't put the first #)","Kanał IRC (nie używaj #)"}. -{"IRC server","Serwer IRC"}. -{"IRC settings","Ustawienia IRC"}. -{"IRC Transport","Transport IRC"}. -{"IRC username","Nazwa użytkownika IRC"}. -{"IRC Username","Nazwa użytkownika IRC"}. {"is now known as","jest teraz znany jako"}. +{"It is not allowed to send error messages to the room. The participant (~s) has sent an error message (~s) and got kicked from the room","Użytkownik nie może wysyłać wiadomości o błędach do pokoju. Użytkownik (~s) wysłał błąd (~s) i został wyrzucony z pokoju"}. {"It is not allowed to send private messages of type \"groupchat\"","Nie można wysyłać prywatnych wiadomości typu \"groupchat\""}. {"It is not allowed to send private messages to the conference","Nie wolno wysyłac prywatnych wiadomości na konferencję"}. -{"It is not allowed to send private messages","Wysyłanie prywatnych wiadomości jest zabronione"}. -{"Jabber Account Registration","Zakładanie konta Jabber"}. {"Jabber ID","Jabber ID"}. -{"Jabber ID ~s is invalid","Jabber ID ~s jest niepoprawny"}. {"January","Styczeń"}. -{"Join IRC channel","Dołącz do kanału IRC"}. {"joins the room","dołącza do pokoju"}. -{"Join the IRC channel here.","Dołącz do kanału IRC."}. -{"Join the IRC channel in this Jabber ID: ~s","Dołącz do kanału IRC pod tym Jabber ID: ~s"}. {"July","Lipiec"}. {"June","Czerwiec"}. {"Last Activity","Ostatnia aktywność"}. @@ -169,10 +157,6 @@ {"Last month","Miniony miesiąc"}. {"Last year","Miniony rok"}. {"leaves the room","opuszcza pokój"}. -{"Listened Ports at ","Porty nasłuchujące na "}. -{"Listened Ports","Porty nasłuchujące"}. -{"List of modules to start","Lista modułów do uruchomienia"}. -{"Low level update script","Skrypt aktualizacji niskiego poziomu"}. {"Make participants list public","Upublicznij listę uczestników"}. {"Make room CAPTCHA protected","Pokój zabezpieczony captchą"}. {"Make room members-only","Pokój tylko dla członków"}. @@ -180,43 +164,62 @@ {"Make room password protected","Pokój zabezpieczony hasłem"}. {"Make room persistent","Utwórz pokój na stałe"}. {"Make room public searchable","Pozwól wyszukiwać pokój"}. +{"Malformed username","Nieprawidłowa nazwa użytkownika"}. {"March","Marzec"}. -{"Maximum Number of Occupants","Maksymalna liczba uczestników"}. -{"Max # of items to persist","Maksymalna liczba przechowywanych przedmiotów"}. {"Max payload size in bytes","Maksymalna wielkość powiadomienia w bajtach"}. +{"Maximum Number of Occupants","Maksymalna liczba uczestników"}. {"May","Maj"}. -{"Members:","Członkowie:"}. {"Membership is required to enter this room","Musisz być na liście członków tego pokoju aby do niego wejść"}. -{"Memorize your password, or write it in a paper placed in a safe place. In Jabber there isn't an automated way to recover your password if you forget it.","Zapamiętaj swoje hasło lub zapisz je na kartce i zachowaj w bezpiecznym miejscu. Na Jabberze nie ma zautomatyzowanego systemu odzyskiwania haseł."}. -{"Memory","Pamięć"}. {"Message body","Treść wiadomości"}. +{"Message not found in forwarded payload","Nie znaleziona wiadomości w przesyłanych dalej danych"}. {"Middle Name","Drugie imię"}. {"Minimum interval between voice requests (in seconds)","Minimalny odstęp między żądaniami głosowymi (w sekundach)"}. {"Moderator privileges required","Wymagane uprawnienia moderatora"}. -{"moderators only","tylko moderatorzy"}. -{"Modified modules","Zmodyfikowane moduły"}. -{"Module","Moduł"}. -{"Modules at ","Moduły na "}. -{"Modules","Moduły"}. +{"Moderator","Moderatorzy"}. +{"Module failed to handle the query","Moduł nie był wstanie przetworzyć zapytania"}. {"Monday","Poniedziałek"}. +{"Multicast","Multicast"}. +{"Multi-User Chat","Wieloosobowa rozmowa"}. {"Name","Imię"}. -{"Name:","Nazwa:"}. +{"Neither 'jid' nor 'nick' attribute found","Brak zarówno atrybutu 'jid' jak i 'nick'"}. +{"Neither 'role' nor 'affiliation' attribute found","Brak zarówno atrybutu 'role' jak i 'affiliation'"}. {"Never","Nigdy"}. {"New Password:","Nowe hasło:"}. -{"Nickname","Nazwa użytkownika"}. {"Nickname Registration at ","Rejestracja nazwy użytkownika na "}. -{"Nickname ~s does not exist in the room","Nie ma nicka ~s w tym pokoju"}. -{"nobody","nikt"}. +{"Nickname","Nazwa użytkownika"}. +{"No 'affiliation' attribute found","Brak wartości dla 'access'"}. +{"No available resource found","Brak dostępnych zasobów"}. {"No body provided for announce message","Brak treści powiadomienia"}. +{"No data form found","Brak danych dla formatki"}. {"No Data","Brak danych"}. -{"Node ID","ID węzła"}. -{"Node not found","Węzeł nie został znaleziony"}. -{"Nodes","Węzły"}. -{"Node ","Węzeł "}. +{"No features available","Brak dostępnych funkcji"}. +{"No hook has processed this command","Żadna funkcja nie przetworzyła tej komendy"}. +{"No info about last activity found","Nie znaleziono informacji o ostatniej aktywności"}. +{"No 'item' element found","Brak wartości dla 'item'"}. +{"No items found in this query","Nie znaleziono żadnych pozycji w tym zapytaniu"}. {"No limit","Bez limitu"}. +{"No module is handling this query","Żaden moduł nie obsługuje tego zapytania"}. +{"No node specified","Nie podano węzła"}. +{"No 'password' found in data form","Brak wartości dla 'password'"}. +{"No 'password' found in this query","Brak wartości dla 'password'"}. +{"No 'path' found in data form","Brak wartości dla 'path'"}. +{"No pending subscriptions found","Nie ma żadnych oczekujących subskrypcji"}. +{"No privacy list with this name found","Nie znaleziona żadnych list prywatności z tą nazwą"}. +{"No private data found in this query","Nie znaleziono danych prywatnych w tym zapytaniu"}. +{"No running node found","Brak uruchomionych węzłów"}. +{"No services available","Usługa nie jest dostępna"}. +{"No statistics found for this item","Nie znaleziono statystyk dla tego elementu"}. +{"No 'to' attribute found in the invitation","Brak wartości dla 'to' w zaproszeniu"}. +{"Node already exists","Węzeł już istnieje"}. +{"Node ID","ID węzła"}. +{"Node index not found","Indeks węzła już istnieje"}. +{"Node not found","Węzeł nie został znaleziony"}. +{"Node ~p","Węzeł ~p"}. +{"Nodeprep has failed","Weryfikacja nazwy nie powiodła się"}. +{"Nodes","Węzły"}. {"None","Brak"}. -{"No resource provided","Nie podano zasobu"}. {"Not Found","Nie znaleziono"}. +{"Not subscribed","Nie zasubskrybowano"}. {"Notify subscribers when items are removed from the node","Informuj subskrybentów o usunięciu elementów węzła"}. {"Notify subscribers when the node configuration changes","Informuj subskrybentów o zmianach konfiguracji węzła"}. {"Notify subscribers when the node is deleted","Informuj subskrybentów o usunięciu węzła"}. @@ -225,88 +228,70 @@ {"Number of online users","Liczba zalogowanych użytkowników"}. {"Number of registered users","Liczba zarejestrowanych użytkowników"}. {"October","Październik"}. -{"Offline Messages:","Wiadomości offline:"}. -{"Offline Messages","Wiadomości offline"}. {"OK","OK"}. {"Old Password:","Stare hasło:"}. -{"Online","Dostępny"}. -{"Online Users:","Użytkownicy zalogowani:"}. {"Online Users","Użytkownicy zalogowani"}. +{"Online","Dostępny"}. {"Only deliver notifications to available users","Dostarczaj powiadomienia tylko dostępnym użytkownikom"}. +{"Only or tags are allowed","Dozwolone są wyłącznie elementy lub "}. +{"Only element is allowed in this query","Wyłącznie elementy są dozwolone w tym zapytaniu"}. +{"Only members may query archives of this room","Tylko moderatorzy mogą przeglądać archiwa tego pokoju"}. {"Only moderators and participants are allowed to change the subject in this room","Tylko moderatorzy i uczestnicy mogą zmienić temat tego pokoju"}. {"Only moderators are allowed to change the subject in this room","Tylko moderatorzy mogą zmienić temat tego pokoju"}. {"Only moderators can approve voice requests","Tylko moderatorzy mogą zatwierdzać żądania głosowe"}. {"Only occupants are allowed to send messages to the conference","Tylko uczestnicy mogą wysyłać wiadomości na konferencję"}. {"Only occupants are allowed to send queries to the conference","Tylko uczestnicy mogą wysyłać zapytania do konferencji"}. {"Only service administrators are allowed to send service messages","Tylko administratorzy mogą wysyłać wiadomości"}. -{"Options","Opcje"}. {"Organization Name","Nazwa organizacji"}. {"Organization Unit","Dział"}. -{"Outgoing s2s Connections:","Wychodzące połączenia s2s:"}. {"Outgoing s2s Connections","Wychodzące połączenia s2s"}. -{"Outgoing s2s Servers:","Serwery zewnętrzne s2s:"}. {"Owner privileges required","Wymagane uprawnienia właściciela"}. -{"Packet","Pakiet"}. -{"Password ~b","Hasło ~b"}. -{"Password:","Hasło:"}. -{"Password","Hasło"}. -{"Password Verification:","Weryfikacja hasła:"}. +{"Participant","Uczestnicy"}. {"Password Verification","Weryfikacja hasła"}. +{"Password Verification:","Weryfikacja hasła:"}. +{"Password","Hasło"}. +{"Password:","Hasło:"}. {"Path to Dir","Ścieżka do katalogu"}. {"Path to File","Scieżka do pliku"}. -{"Pending","Oczekuje"}. {"Period: ","Przedział czasu: "}. {"Persist items to storage","Przechowuj na stałe dane PubSub"}. +{"Ping query is incorrect","Żądanie 'ping' nie jest prawidłowe"}. {"Ping","Ping"}. {"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Te opcje kopii zapasowych dotyczą tylko wbudowanej bazy danych typu Mnesia. Jeśli korzystasz z modułu ODBC, musisz wykonać kopie bazy we własnym zakresie."}. {"Please, wait for a while before sending new voice request","Proszę poczekać chwile, zanim wyślesz nowe żądanie głosowe"}. {"Pong","Pong"}. -{"Port ~b","Port ~b"}. -{"Port","Port"}. {"Present real Jabber IDs to","Prawdziwe Jabber ID widoczne dla"}. {"private, ","prywatny, "}. -{"Protocol","Protokół"}. {"Publish-Subscribe","PubSub"}. {"PubSub subscriber request","Żądanie subskrybcji PubSub"}. {"Purge all items when the relevant publisher goes offline","Usuń wszystkie elementy w momencie kiedy publikujący rozłączy się"}. {"Queries to the conference members are not allowed in this room","Informacje o członkach konferencji nie są dostępne w tym pokoju"}. +{"Query to another users is forbidden","Zapytanie do innych użytkowników nie są dozwolone"}. {"RAM and disc copy","Kopia na dysku i w pamięci RAM"}. {"RAM copy","Kopia w pamięci RAM"}. -{"Raw","Żródło"}. {"Really delete message of the day?","Na pewno usunąć wiadomość dnia?"}. {"Recipient is not in the conference room","Odbiorcy nie ma w pokoju"}. -{"Register a Jabber account","Załóż konto Jabber"}. -{"Registered Users:","Użytkownicy zarejestrowani:"}. -{"Registered Users","Użytkownicy zarejestrowani"}. {"Register","Zarejestruj"}. -{"Registration in mod_irc for ","Rejestracja w mod_irc dla "}. {"Remote copy","Kopia zdalna"}. -{"Remove All Offline Messages","Usuń wszystkie wiadomości typu 'Offline'"}. {"Remove User","Usuń użytkownika"}. -{"Remove","Usuń"}. {"Replaced by new connection","Połączenie zostało zastąpione"}. {"Resources","Zasoby"}. {"Restart Service","Restart usługi"}. -{"Restart","Uruchom ponownie"}. {"Restore Backup from File at ","Odtwórz bazę danych z kopii zapasowej na "}. {"Restore binary backup after next ejabberd restart (requires less memory):","Odtwórz kopię binarną podczas następnego uruchomienia ejabberd (wymaga mniej zasobów):"}. {"Restore binary backup immediately:","Natychmiast odtwórz kopię binarną:"}. {"Restore plain text backup immediately:","Natychmiast odtwórz kopię z postaci tekstowej:"}. {"Restore","Przywróć z kopii"}. +{"Roles for which Presence is Broadcasted","Role dla których wysyłane są statusy"}. {"Room Configuration","Konfiguracja pokoju"}. {"Room creation is denied by service policy","Zasady serwera zabraniają tworzyć nowe pokoje"}. {"Room description","Opis pokoju"}. {"Room Occupants","Lista uczestników"}. {"Room title","Tytuł pokoju"}. {"Roster groups allowed to subscribe","Grupy kontaktów uprawnione do subskrypcji"}. -{"Roster","Lista kontaktów"}. -{"Roster of ","Lista kontaktów "}. {"Roster size","Rozmiar listy kontaktów"}. -{"RPC Call Error","Błąd żądania RPC"}. {"Running Nodes","Uruchomione węzły"}. -{"~s access rule configuration","~s konfiguracja zasad dostępu"}. {"Saturday","Sobota"}. -{"Script check","Sprawdź skrypt"}. {"Search Results for ","Wyniki wyszukiwania dla "}. {"Search users in ","Wyszukaj użytkowników w "}. {"Send announcement to all online users on all hosts","Wyślij powiadomienie do wszystkich zalogowanych użytkowników na wszystkich hostach"}. @@ -314,7 +299,6 @@ {"Send announcement to all users on all hosts","Wyślij powiadomienie do wszystkich użytkowników na wszystkich hostach"}. {"Send announcement to all users","Wyślij powiadomienie do wszystkich użytkowników"}. {"September","Wrzesień"}. -{"Server ~b","Serwer ~b"}. {"Server:","Serwer:"}. {"Set message of the day and send to online users","Wyślij wiadomość dnia do wszystkich zalogowanych użytkowników"}. {"Set message of the day on all hosts and send to online users","Ustaw wiadomość dnia dla wszystkich hostów i wyślij do zalogowanych uzytkowników"}. @@ -322,83 +306,72 @@ {"Show Integral Table","Pokaż tabelę całkowitą"}. {"Show Ordinary Table","Pokaż zwykłą tabelę"}. {"Shut Down Service","Wyłącz usługę"}. -{"~s invites you to the room ~s","~s zaprasza Cię do pokoju ~s"}. -{"Some Jabber clients can store your password in your computer. Use that feature only if you trust your computer is safe.","Niektóre klienty Jabber mogą zapisywać Twoje hasło na komputerze. Używaj tej opcji tylko jeśli ufasz komputerowi na którym pracujesz."}. {"Specify the access model","Określ model dostępu"}. {"Specify the event message type","Określ typ wiadomości"}. {"Specify the publisher model","Określ model publikującego"}. -{"~s's Offline Messages Queue","Kolejka wiadomości offline użytkownika ~s"}. -{"Start Modules at ","Uruchom moduły na "}. -{"Start Modules","Uruchom moduły"}. -{"Start","Uruchom"}. -{"Statistics of ~p","Statystyki ~p"}. -{"Statistics","Statystyki"}. -{"Stop Modules at ","Zatrzymaj moduły na "}. -{"Stop Modules","Zatrzymaj moduły"}. {"Stopped Nodes","Zatrzymane węzły"}. -{"Stop","Zatrzymaj"}. -{"Storage Type","Typ bazy"}. {"Store binary backup:","Zachowaj kopię binarną:"}. {"Store plain text backup:","Zachowaj kopię w postaci tekstowej:"}. {"Subject","Temat"}. {"Submitted","Wprowadzone"}. -{"Submit","Wyślij"}. {"Subscriber Address","Adres subskrybenta"}. -{"Subscription","Subskrypcja"}. +{"Subscriptions are not allowed","Subskrypcje nie są dozwolone"}. {"Sunday","Niedziela"}. {"That nickname is already in use by another occupant","Ta nazwa użytkownika jest używana przez kogoś innego"}. {"That nickname is registered by another person","Ta nazwa użytkownika jest już zarejestrowana przez inną osobę"}. {"The CAPTCHA is valid.","Captcha jest poprawna."}. -{"The CAPTCHA verification has failed","Weryfikacja CAPTCHA nie powiodła się."}. +{"The CAPTCHA verification has failed","Weryfikacja CAPTCHA nie powiodła się"}. {"The collections with which a node is affiliated","Grupy, do których należy węzeł"}. -{"the password is","hasło to:"}. +{"The feature requested is not supported by the conference","Żądana czynność nie jest obsługiwana przez konferencje"}. +{"The password contains unacceptable characters","Hasło zawiera niedopuszczalne znaki"}. {"The password is too weak","Hasło nie jest wystarczająco trudne"}. -{"The password of your Jabber account was successfully changed.","Hasło do Twojego konta zostało zmienione."}. -{"There was an error changing the password: ","Podczas próby zmiany hasła wystąpił błąd:"}. -{"There was an error creating the account: ","Wystąpił błąd podczas tworzenia konta:"}. -{"There was an error deleting the account: ","Podczas usuwania konta wystąpił błąd:"}. -{"This is case insensitive: macbeth is the same that MacBeth and Macbeth.","Pole nie rozróżnia wielkości liter: słowo Hanna jest takie samo jak hAnna lub haNNa."}. -{"This page allows to create a Jabber account in this Jabber server. Your JID (Jabber IDentifier) will be of the form: username@server. Please read carefully the instructions to fill correctly the fields.","Niniejsza strona pozwala na założenie konta Jabber na tym serwerze. Twój JID (Jabber IDentyfikator) będzie miał postać: nazwa_użytkownika@serwer. Przeczytaj dokładnie instrukcję i wypełnij pola."}. -{"This page allows to unregister a Jabber account in this Jabber server.","Ta strona pozwala usunąć konto Jabber z tego serwera."}. -{"This participant is kicked from the room because he sent an error message","Ten uczestnik został wyrzucony z pokoju ponieważ wysłał komunikat błędu"}. -{"This participant is kicked from the room because he sent an error message to another participant","Ten uczestnik został wyrzucony z pokoju ponieważ wysłał komunikat błędu do innego uczestnika"}. -{"This participant is kicked from the room because he sent an error presence","Ten uczestnik został wyrzucony z pokoju ponieważ jego informacja o statusie zawierała błędy"}. +{"the password is","hasło to:"}. +{"The query is only allowed from local users","To żądanie jest dopuszczalne wyłącznie dla lokalnych użytkowników"}. +{"The query must not contain elements","Żądanie nie może zawierać elementów "}. +{"The stanza MUST contain only one element, one element, or one element","Żądanie może zawierać wyłącznie jeden z elementów , lub "}. +{"There was an error creating the account: ","Wystąpił błąd podczas tworzenia konta: "}. +{"There was an error deleting the account: ","Podczas usuwania konta wystąpił błąd: "}. {"This room is not anonymous","Ten pokój nie jest anonimowy"}. {"Thursday","Czwartek"}. -{"Time","Czas"}. {"Time delay","Opóźnienie"}. -{"To","Do"}. +{"To register, visit ~s","Żeby się zarejestrować odwiedź ~s"}. +{"Token TTL","Limit czasu tokenu"}. +{"Too many active bytestreams","Zbyt wiele strumieni danych"}. {"Too many CAPTCHA requests","Za dużo żądań CAPTCHA"}. -{"To ~s","Do ~s"}. +{"Too many elements","Zbyt wiele elementów "}. +{"Too many elements","Zbyt wiele elementów "}. +{"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","Zbyt wiele (~p) nieudanych prób logowanie z tego adresu IP (~s). Ten adres zostanie odblokowany o ~s UTC"}. +{"Too many unacked stanzas","Zbyt wiele niepotwierdzonych pakietów"}. +{"Too many users in this conference","Zbyt wielu użytkowników konferencji"}. {"Traffic rate limit is exceeded","Limit transferu przekroczony"}. -{"Transactions Aborted:","Transakcje anulowane:"}. -{"Transactions Committed:","Transakcje zakończone:"}. -{"Transactions Logged:","Transakcje zalogowane:"}. -{"Transactions Restarted:","Transakcje uruchomione ponownie:"}. {"Tuesday","Wtorek"}. {"Unable to generate a CAPTCHA","Nie można wygenerować CAPTCHA"}. +{"Unable to register route on existing local domain","Nie można zarejestrować trasy dla lokalnej domeny"}. {"Unauthorized","Nie autoryzowano"}. -{"Unregister a Jabber account","Usuń konto Jabber"}. +{"Unexpected action","Nieoczekiwana akcja"}. {"Unregister","Wyrejestruj"}. -{"Update","Aktualizuj"}. +{"Unsupported element","Nieobsługiwany element "}. {"Update message of the day (don't send)","Aktualizuj wiadomość dnia (bez wysyłania)"}. {"Update message of the day on all hosts (don't send)","Aktualizuj wiadomość dnia na wszystkich hostach (bez wysyłania)"}. -{"Update plan","Plan aktualizacji"}. -{"Update script","Skrypt aktualizacji"}. -{"Update ","Uaktualnij "}. -{"Uptime:","Czas pracy:"}. -{"Use of STARTTLS required","Wymagane jest użycie STARTTLS"}. +{"User already exists","Użytkownik już istnieje"}. {"User JID","Użytkownik "}. +{"User (jid)","Użytkownik (jid)"}. {"User Management","Zarządzanie użytkownikami"}. +{"User session not found","Sesja użytkownika nie została znaleziona"}. +{"User session terminated","Sesja użytkownika została zakończona"}. {"Username:","Nazwa użytkownika:"}. {"Users are not allowed to register accounts so quickly","Użytkowncy nie mogą tak szybko rejestrować nowych kont"}. {"Users Last Activity","Ostatnia aktywność użytkowników"}. {"Users","Użytkownicy"}. -{"User ","Użytkownik "}. {"User","Użytkownik"}. -{"Validate","Potwierdź"}. +{"Value 'get' of 'type' attribute is not allowed","Wartość 'get' dla atrybutu 'type' jest niedozwolona"}. +{"Value of '~s' should be boolean","Wartość '~s' powinna być typu logicznego"}. +{"Value of '~s' should be datetime string","Wartość '~s' powinna być typu daty"}. +{"Value of '~s' should be integer","Wartość '~s' powinna być liczbą"}. +{"Value 'set' of 'type' attribute is not allowed","Wartość 'set' dla atrybutu 'type' jest niedozwolona"}. {"vCard User Search","Wyszukiwanie vCard użytkowników"}. {"Virtual Hosts","Wirtualne Hosty"}. +{"Visitor","Odwiedzający"}. {"Visitors are not allowed to change their nicknames in this room","Uczestnicy tego pokoju nie mogą zmieniać swoich nicków"}. {"Visitors are not allowed to send messages to all occupants","Odwiedzający nie mogą wysyłać wiadomości do wszystkich obecnych"}. {"Voice requests are disabled in this conference","Głosowe żądania są wyłączone w tym pokoju"}. @@ -406,16 +379,13 @@ {"Wednesday","Środa"}. {"When to send the last published item","Kiedy wysłać ostatnio opublikowaną rzecz"}. {"Whether to allow subscriptions","Czy pozwolić na subskrypcje"}. -{"You can later change your password using a Jabber client.","Możesz później zmienić swoje hasło za pomocą dowolnego klienta Jabber."}. {"You have been banned from this room","Zostałeś wykluczony z tego pokoju"}. +{"You have joined too many conferences","Dołączyłeś do zbyt wielu konferencji"}. {"You must fill in field \"Nickname\" in the form","Musisz wypełnić pole \"Nazwa użytkownika\" w formularzu"}. {"You need a client that supports x:data and CAPTCHA to register","Potrzebujesz klienta obsługującego x:data aby zarejestrować nick"}. {"You need a client that supports x:data to register the nickname","Potrzebujesz klienta obsługującego x:data aby zarejestrować nick"}. -{"You need an x:data capable client to configure mod_irc settings","Potrzebujesz klienta obsługującego x:data aby skonfigurować mod_irc"}. -{"You need an x:data capable client to configure room","Potrzebujesz klienta obsługującego x:data aby skonfigurować pokój"}. {"You need an x:data capable client to search","Potrzebujesz klienta obsługującego x:data aby wyszukiwać"}. -{"Your active privacy list has denied the routing of this stanza.","Aktualna lista prywatności zabrania przesyłania tej stanzy"}. +{"Your active privacy list has denied the routing of this stanza.","Aktualna lista prywatności zabrania przesyłania tej stanzy."}. {"Your contact offline message queue is full. The message has been discarded.","Kolejka wiadomości offline adresata jest pełna. Wiadomość została odrzucona."}. -{"Your Jabber account was successfully created.","Twoje konto zostało stworzone."}. -{"Your Jabber account was successfully deleted.","Twoje konto zostało usunięte."}. -{"Your messages to ~s are being blocked. To unblock them, visit ~s","Twoje wiadomości do ~s są blokowane. Aby je odblokować, odwiedź ~s"}. +{"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","Twoje wiadomości do ~s są blokowane. Aby je odblokować, odwiedź ~s"}. +{"You're not allowed to create nodes","Nie masz uprawnień do tworzenia węzłów"}. diff --git a/priv/msgs/pl.po b/priv/msgs/pl.po deleted file mode 100644 index f0dc11bed..000000000 --- a/priv/msgs/pl.po +++ /dev/null @@ -1,1857 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: 2.1.0-alpha\n" -"POT-Creation-Date: \n" -"PO-Revision-Date: \n" -"Last-Translator: Zbyszek Żółkiewski \n" -"Language-Team: \n" -"Language: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Language: Polish (polski)\n" -"X-Additional-Translator: Janusz B. Wiśniewski\n" -"X-Additional-Translator: Marcin Owsiany\n" -"X-Additional-Translator: Andrzej Smyk\n" -"X-Additional-Translator: Mateusz Gajewski\n" - -#: ejabberd_c2s.erl:424 ejabberd_c2s.erl:727 -msgid "Use of STARTTLS required" -msgstr "Wymagane jest użycie STARTTLS" - -#: ejabberd_c2s.erl:503 -msgid "No resource provided" -msgstr "Nie podano zasobu" - -#: ejabberd_c2s.erl:1197 -msgid "Replaced by new connection" -msgstr "Połączenie zostało zastąpione" - -#: ejabberd_c2s.erl:1885 -msgid "Your active privacy list has denied the routing of this stanza." -msgstr "Aktualna lista prywatności zabrania przesyłania tej stanzy" - -#: ejabberd_captcha.erl:96 ejabberd_captcha.erl:152 ejabberd_captcha.erl:178 -msgid "Enter the text you see" -msgstr "Przepisz tekst z obrazka" - -#: ejabberd_captcha.erl:101 -msgid "Your messages to ~s are being blocked. To unblock them, visit ~s" -msgstr "Twoje wiadomości do ~s są blokowane. Aby je odblokować, odwiedź ~s" - -#: ejabberd_captcha.erl:134 -msgid "If you don't see the CAPTCHA image here, visit the web page." -msgstr "Jeśli nie widzisz obrazka CAPTCHA, odwiedź stronę internetową." - -#: ejabberd_captcha.erl:146 -msgid "CAPTCHA web page" -msgstr "Strona internetowa CAPTCHA" - -#: ejabberd_captcha.erl:307 -msgid "The CAPTCHA is valid." -msgstr "Captcha jest poprawna." - -#: mod_adhoc.erl:95 mod_adhoc.erl:125 mod_adhoc.erl:143 mod_adhoc.erl:161 -msgid "Commands" -msgstr "Polecenia" - -#: mod_adhoc.erl:149 mod_adhoc.erl:243 -msgid "Ping" -msgstr "Ping" - -#: mod_adhoc.erl:260 -msgid "Pong" -msgstr "Pong" - -#: mod_announce.erl:507 mod_announce_odbc.erl:499 -msgid "Really delete message of the day?" -msgstr "Na pewno usunąć wiadomość dnia?" - -#: mod_announce.erl:515 mod_announce_odbc.erl:507 mod_configure.erl:1083 -#: mod_configure.erl:1128 -msgid "Subject" -msgstr "Temat" - -#: mod_announce.erl:520 mod_announce_odbc.erl:512 mod_configure.erl:1088 -#: mod_configure.erl:1133 -msgid "Message body" -msgstr "Treść wiadomości" - -#: mod_announce.erl:600 mod_announce_odbc.erl:592 -msgid "No body provided for announce message" -msgstr "Brak treści powiadomienia" - -#: mod_announce.erl:635 mod_announce_odbc.erl:627 -msgid "Announcements" -msgstr "Powiadomienia" - -#: mod_announce.erl:637 mod_announce_odbc.erl:629 -msgid "Send announcement to all users" -msgstr "Wyślij powiadomienie do wszystkich użytkowników" - -#: mod_announce.erl:639 mod_announce_odbc.erl:631 -msgid "Send announcement to all users on all hosts" -msgstr "Wyślij powiadomienie do wszystkich użytkowników na wszystkich hostach" - -#: mod_announce.erl:641 mod_announce_odbc.erl:633 -msgid "Send announcement to all online users" -msgstr "Wyślij powiadomienie do wszystkich zalogowanych użytkowników" - -#: mod_announce.erl:643 mod_announce_odbc.erl:635 mod_configure.erl:1078 -#: mod_configure.erl:1123 -msgid "Send announcement to all online users on all hosts" -msgstr "" -"Wyślij powiadomienie do wszystkich zalogowanych użytkowników na wszystkich " -"hostach" - -#: mod_announce.erl:645 mod_announce_odbc.erl:637 -msgid "Set message of the day and send to online users" -msgstr "Wyślij wiadomość dnia do wszystkich zalogowanych użytkowników" - -#: mod_announce.erl:647 mod_announce_odbc.erl:639 -msgid "Set message of the day on all hosts and send to online users" -msgstr "" -"Ustaw wiadomość dnia dla wszystkich hostów i wyślij do zalogowanych " -"uzytkowników" - -#: mod_announce.erl:649 mod_announce_odbc.erl:641 -msgid "Update message of the day (don't send)" -msgstr "Aktualizuj wiadomość dnia (bez wysyłania)" - -#: mod_announce.erl:651 mod_announce_odbc.erl:643 -msgid "Update message of the day on all hosts (don't send)" -msgstr "Aktualizuj wiadomość dnia na wszystkich hostach (bez wysyłania)" - -#: mod_announce.erl:653 mod_announce_odbc.erl:645 -msgid "Delete message of the day" -msgstr "Usuń wiadomość dnia" - -#: mod_announce.erl:655 mod_announce_odbc.erl:647 -msgid "Delete message of the day on all hosts" -msgstr "Usuń wiadomość dnia ze wszystkich hostów" - -#: mod_configure.erl:114 mod_configure.erl:274 mod_configure.erl:296 -#: mod_configure.erl:498 -msgid "Configuration" -msgstr "Konfiguracja" - -#: mod_configure.erl:125 mod_configure.erl:576 web/ejabberd_web_admin.erl:1936 -msgid "Database" -msgstr "Baza danych" - -#: mod_configure.erl:127 mod_configure.erl:595 -msgid "Start Modules" -msgstr "Uruchom moduły" - -#: mod_configure.erl:129 mod_configure.erl:596 -msgid "Stop Modules" -msgstr "Zatrzymaj moduły" - -#: mod_configure.erl:131 mod_configure.erl:604 web/ejabberd_web_admin.erl:1937 -msgid "Backup" -msgstr "Wykonaj kopie" - -#: mod_configure.erl:133 mod_configure.erl:605 -msgid "Restore" -msgstr "Przywróć z kopii" - -#: mod_configure.erl:135 mod_configure.erl:606 -msgid "Dump to Text File" -msgstr "Wykonaj kopie do pliku tekstowego" - -#: mod_configure.erl:137 mod_configure.erl:615 -msgid "Import File" -msgstr "Importuj plik" - -#: mod_configure.erl:139 mod_configure.erl:616 -msgid "Import Directory" -msgstr "Importuj katalog" - -#: mod_configure.erl:141 mod_configure.erl:581 mod_configure.erl:1057 -msgid "Restart Service" -msgstr "Restart usługi" - -#: mod_configure.erl:143 mod_configure.erl:582 mod_configure.erl:1102 -msgid "Shut Down Service" -msgstr "Wyłącz usługę" - -#: mod_configure.erl:145 mod_configure.erl:518 mod_configure.erl:1197 -#: web/ejabberd_web_admin.erl:1527 -msgid "Add User" -msgstr "Dodaj użytkownika" - -#: mod_configure.erl:147 mod_configure.erl:519 mod_configure.erl:1219 -msgid "Delete User" -msgstr "Usuń użytkownika" - -#: mod_configure.erl:149 mod_configure.erl:520 mod_configure.erl:1231 -msgid "End User Session" -msgstr "Zakończ sesję uzytkownika" - -#: mod_configure.erl:151 mod_configure.erl:521 mod_configure.erl:1243 -#: mod_configure.erl:1255 -msgid "Get User Password" -msgstr "Pobierz hasło użytkownika" - -#: mod_configure.erl:153 mod_configure.erl:522 -msgid "Change User Password" -msgstr "Zmień hasło użytkownika" - -#: mod_configure.erl:155 mod_configure.erl:523 mod_configure.erl:1272 -msgid "Get User Last Login Time" -msgstr "Pokaż czas ostatniego zalogowania uzytkownika" - -#: mod_configure.erl:157 mod_configure.erl:524 mod_configure.erl:1284 -msgid "Get User Statistics" -msgstr "Pobierz statystyki użytkownika" - -#: mod_configure.erl:159 mod_configure.erl:525 -msgid "Get Number of Registered Users" -msgstr "Pokaż liczbę zarejestrowanych użytkowników" - -#: mod_configure.erl:161 mod_configure.erl:526 -msgid "Get Number of Online Users" -msgstr "Pokaż liczbę zalogowanych użytkowników" - -#: mod_configure.erl:163 mod_configure.erl:509 web/ejabberd_web_admin.erl:831 -#: web/ejabberd_web_admin.erl:872 -msgid "Access Control Lists" -msgstr "Lista dostępowa" - -#: mod_configure.erl:165 mod_configure.erl:510 web/ejabberd_web_admin.erl:940 -#: web/ejabberd_web_admin.erl:976 -msgid "Access Rules" -msgstr "Zasady dostępu" - -#: mod_configure.erl:297 mod_configure.erl:499 -msgid "User Management" -msgstr "Zarządzanie użytkownikami" - -#: mod_configure.erl:500 web/ejabberd_web_admin.erl:1058 -#: web/ejabberd_web_admin.erl:1462 -msgid "Online Users" -msgstr "Użytkownicy zalogowani" - -#: mod_configure.erl:501 -msgid "All Users" -msgstr "Wszyscy użytkownicy" - -#: mod_configure.erl:502 -msgid "Outgoing s2s Connections" -msgstr "Wychodzące połączenia s2s" - -#: mod_configure.erl:503 web/ejabberd_web_admin.erl:1907 -msgid "Running Nodes" -msgstr "Uruchomione węzły" - -#: mod_configure.erl:504 web/ejabberd_web_admin.erl:1909 -msgid "Stopped Nodes" -msgstr "Zatrzymane węzły" - -#: mod_configure.erl:577 mod_configure.erl:587 web/ejabberd_web_admin.erl:1953 -msgid "Modules" -msgstr "Moduły" - -#: mod_configure.erl:578 -msgid "Backup Management" -msgstr "Zarządzanie kopiami zapasowymi" - -#: mod_configure.erl:579 -msgid "Import Users From jabberd14 Spool Files" -msgstr "Importuj użytkowników z plików roboczych serwera jabberd14" - -#: mod_configure.erl:699 -msgid "To ~s" -msgstr "Do ~s" - -#: mod_configure.erl:717 -msgid "From ~s" -msgstr "Od ~s" - -#: mod_configure.erl:913 -msgid "Database Tables Configuration at " -msgstr "Konfiguracja tabel bazy na " - -#: mod_configure.erl:918 -msgid "Choose storage type of tables" -msgstr "Wybierz typ bazy dla tablel" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Disc only copy" -msgstr "Kopia tylko na dysku" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM and disc copy" -msgstr "Kopia na dysku i w pamięci RAM" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM copy" -msgstr "Kopia w pamięci RAM" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Remote copy" -msgstr "Kopia zdalna" - -#: mod_configure.erl:950 -msgid "Stop Modules at " -msgstr "Zatrzymaj moduły na " - -#: mod_configure.erl:954 -msgid "Choose modules to stop" -msgstr "Wybierz moduły do zatrzymania" - -#: mod_configure.erl:969 -msgid "Start Modules at " -msgstr "Uruchom moduły na " - -#: mod_configure.erl:973 -msgid "Enter list of {Module, [Options]}" -msgstr "Wprowadź listę {Moduł, [Opcje]}" - -#: mod_configure.erl:974 -msgid "List of modules to start" -msgstr "Lista modułów do uruchomienia" - -#: mod_configure.erl:983 -msgid "Backup to File at " -msgstr "Zapisz kopię w pliku na " - -#: mod_configure.erl:987 mod_configure.erl:1001 -msgid "Enter path to backup file" -msgstr "Wprowadź scieżkę do pliku kopii zapasowej" - -#: mod_configure.erl:988 mod_configure.erl:1002 mod_configure.erl:1016 -#: mod_configure.erl:1030 -msgid "Path to File" -msgstr "Scieżka do pliku" - -#: mod_configure.erl:997 -msgid "Restore Backup from File at " -msgstr "Odtwórz bazę danych z kopii zapasowej na " - -#: mod_configure.erl:1011 -msgid "Dump Backup to Text File at " -msgstr "Zapisz kopię zapasową w pliku tekstowym na " - -#: mod_configure.erl:1015 -msgid "Enter path to text file" -msgstr "Wprowadź scieżkę do pliku tekstowego" - -#: mod_configure.erl:1025 -msgid "Import User from File at " -msgstr "Importuj użytkownika z pliku na " - -#: mod_configure.erl:1029 -msgid "Enter path to jabberd14 spool file" -msgstr "Wprowadź ścieżkę do roboczego pliku serwera jabberd14" - -#: mod_configure.erl:1039 -msgid "Import Users from Dir at " -msgstr "Importuj użytkowników z katalogu na " - -#: mod_configure.erl:1043 -msgid "Enter path to jabberd14 spool dir" -msgstr "Wprowadź ścieżkę do roboczego katalogu serwera jabberd14" - -#: mod_configure.erl:1044 -msgid "Path to Dir" -msgstr "Ścieżka do katalogu" - -#: mod_configure.erl:1060 mod_configure.erl:1105 -msgid "Time delay" -msgstr "Opóźnienie" - -#: mod_configure.erl:1143 -msgid "Access Control List Configuration" -msgstr "Konfiguracja listy dostępowej" - -#: mod_configure.erl:1147 -msgid "Access control lists" -msgstr "Listy dostępowe" - -#: mod_configure.erl:1171 -msgid "Access Configuration" -msgstr "Konfiguracja dostępu" - -#: mod_configure.erl:1175 -msgid "Access rules" -msgstr "Reguły dostępu" - -#: mod_configure.erl:1200 mod_configure.erl:1222 mod_configure.erl:1234 -#: mod_configure.erl:1246 mod_configure.erl:1258 mod_configure.erl:1275 -#: mod_configure.erl:1287 mod_configure.erl:1650 mod_configure.erl:1700 -#: mod_configure.erl:1721 mod_roster.erl:943 mod_roster_odbc.erl:1060 -#: mod_vcard.erl:472 mod_vcard_ldap.erl:554 mod_vcard_odbc.erl:448 -msgid "Jabber ID" -msgstr "Jabber ID" - -#: mod_configure.erl:1205 mod_configure.erl:1263 mod_configure.erl:1651 -#: mod_configure.erl:1863 mod_muc/mod_muc_room.erl:3224 mod_register.erl:235 -#: web/ejabberd_web_admin.erl:1520 -msgid "Password" -msgstr "Hasło" - -#: mod_configure.erl:1210 -msgid "Password Verification" -msgstr "Weryfikacja hasła" - -#: mod_configure.erl:1301 -msgid "Number of registered users" -msgstr "Liczba zarejestrowanych użytkowników" - -#: mod_configure.erl:1315 -msgid "Number of online users" -msgstr "Liczba zalogowanych użytkowników" - -#: mod_configure.erl:1682 web/ejabberd_web_admin.erl:1588 -#: web/ejabberd_web_admin.erl:1743 -msgid "Never" -msgstr "Nigdy" - -#: mod_configure.erl:1696 web/ejabberd_web_admin.erl:1601 -#: web/ejabberd_web_admin.erl:1756 -msgid "Online" -msgstr "Dostępny" - -#: mod_configure.erl:1701 -msgid "Last login" -msgstr "Ostatnie logowanie" - -#: mod_configure.erl:1722 -msgid "Roster size" -msgstr "Rozmiar listy kontaktów" - -#: mod_configure.erl:1723 -msgid "IP addresses" -msgstr "Adresy IP" - -#: mod_configure.erl:1724 -msgid "Resources" -msgstr "Zasoby" - -#: mod_configure.erl:1850 -msgid "Administration of " -msgstr "Zarządzanie " - -#: mod_configure.erl:1853 -msgid "Action on user" -msgstr "Wykonaj na użytkowniku" - -#: mod_configure.erl:1857 -msgid "Edit Properties" -msgstr "Edytuj właściwości" - -#: mod_configure.erl:1860 web/ejabberd_web_admin.erl:1769 -msgid "Remove User" -msgstr "Usuń użytkownika" - -#: mod_irc/mod_irc.erl:201 mod_irc/mod_irc_odbc.erl:196 -#: mod_muc/mod_muc.erl:336 mod_muc/mod_muc_odbc.erl:342 -msgid "Access denied by service policy" -msgstr "Dostęp zabroniony zgodnie z zasadami usługi" - -#: mod_irc/mod_irc.erl:401 mod_irc/mod_irc_odbc.erl:398 -msgid "IRC Transport" -msgstr "Transport IRC" - -#: mod_irc/mod_irc.erl:428 mod_irc/mod_irc_odbc.erl:425 -msgid "ejabberd IRC module" -msgstr "Moduł IRC ejabberd" - -#: mod_irc/mod_irc.erl:559 mod_irc/mod_irc_odbc.erl:568 -msgid "You need an x:data capable client to configure mod_irc settings" -msgstr "Potrzebujesz klienta obsługującego x:data aby skonfigurować mod_irc" - -#: mod_irc/mod_irc.erl:566 mod_irc/mod_irc_odbc.erl:575 -msgid "Registration in mod_irc for " -msgstr "Rejestracja w mod_irc dla " - -#: mod_irc/mod_irc.erl:571 mod_irc/mod_irc_odbc.erl:580 -msgid "" -"Enter username, encodings, ports and passwords you wish to use for " -"connecting to IRC servers" -msgstr "" -"Wprowadź nazwę użytkownika, port i kodowanie, których chcesz używać do " -"łączenia z serwerami IRC" - -#: mod_irc/mod_irc.erl:576 mod_irc/mod_irc_odbc.erl:585 -msgid "IRC Username" -msgstr "Nazwa użytkownika IRC" - -#: mod_irc/mod_irc.erl:586 mod_irc/mod_irc_odbc.erl:595 -msgid "" -"If you want to specify different ports, passwords, encodings for IRC " -"servers, fill this list with values in format '{\"irc server\", \"encoding" -"\", port, \"password\"}'. By default this service use \"~s\" encoding, port " -"~p, empty password." -msgstr "" -"Jeśli chcesz ustawić inne hasła, porty lub kodowania dla poszczególnych " -"serwerów IRC, wypełnij tą listę wartościami w formacie '{\"irc server\"," -"\"encoding\", port, \"password\"}'. Domyślne ta usługa używa kodowania \"~s" -"\", portu ~p, bez hasła." - -#: mod_irc/mod_irc.erl:598 mod_irc/mod_irc_odbc.erl:607 -msgid "" -"Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef." -"net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]." -msgstr "" -"Przykład: [{\"wroclaw.irc.pl\",\"utf-8\"}, {\"warszawa.irc.pl\", " -"\"iso8859-2\"}]." - -#: mod_irc/mod_irc.erl:603 mod_irc/mod_irc_odbc.erl:612 -msgid "Connections parameters" -msgstr "Parametry połączeń" - -#: mod_irc/mod_irc.erl:728 mod_irc/mod_irc_odbc.erl:757 -msgid "Join IRC channel" -msgstr "Dołącz do kanału IRC" - -#: mod_irc/mod_irc.erl:732 mod_irc/mod_irc_odbc.erl:761 -msgid "IRC channel (don't put the first #)" -msgstr "Kanał IRC (nie używaj #)" - -#: mod_irc/mod_irc.erl:737 mod_irc/mod_irc_odbc.erl:766 -msgid "IRC server" -msgstr "Serwer IRC" - -#: mod_irc/mod_irc.erl:770 mod_irc/mod_irc.erl:774 -#: mod_irc/mod_irc_odbc.erl:799 mod_irc/mod_irc_odbc.erl:803 -msgid "Join the IRC channel here." -msgstr "Dołącz do kanału IRC." - -#: mod_irc/mod_irc.erl:778 mod_irc/mod_irc_odbc.erl:807 -msgid "Join the IRC channel in this Jabber ID: ~s" -msgstr "Dołącz do kanału IRC pod tym Jabber ID: ~s" - -#: mod_irc/mod_irc.erl:863 mod_irc/mod_irc_odbc.erl:904 -msgid "IRC settings" -msgstr "Ustawienia IRC" - -#: mod_irc/mod_irc.erl:868 mod_irc/mod_irc_odbc.erl:909 -msgid "" -"Enter username and encodings you wish to use for connecting to IRC servers. " -"Press 'Next' to get more fields to fill in. Press 'Complete' to save " -"settings." -msgstr "" -"Wprowadź nazwę użytkownika i kodowania których chcesz używać do łączenia z " -"serwerami IRC. Wciśnij \"Dalej\" aby ustawić więcej parametrów połączenia. " -"Wciśnij \"Zakończ\" aby zapisać ustawienia." - -#: mod_irc/mod_irc.erl:874 mod_irc/mod_irc_odbc.erl:915 -msgid "IRC username" -msgstr "Nazwa użytkownika IRC" - -#: mod_irc/mod_irc.erl:923 mod_irc/mod_irc_odbc.erl:964 -msgid "Password ~b" -msgstr "Hasło ~b" - -#: mod_irc/mod_irc.erl:928 mod_irc/mod_irc_odbc.erl:969 -msgid "Port ~b" -msgstr "Port ~b" - -#: mod_irc/mod_irc.erl:933 mod_irc/mod_irc_odbc.erl:974 -msgid "Encoding for server ~b" -msgstr "Kodowanie znaków dla serwera ~b" - -#: mod_irc/mod_irc.erl:942 mod_irc/mod_irc_odbc.erl:983 -msgid "Server ~b" -msgstr "Serwer ~b" - -#: mod_muc/mod_muc.erl:449 mod_muc/mod_muc_odbc.erl:456 -msgid "Only service administrators are allowed to send service messages" -msgstr "Tylko administratorzy mogą wysyłać wiadomości" - -#: mod_muc/mod_muc.erl:493 mod_muc/mod_muc_odbc.erl:500 -msgid "Room creation is denied by service policy" -msgstr "Zasady serwera zabraniają tworzyć nowe pokoje" - -#: mod_muc/mod_muc.erl:500 mod_muc/mod_muc_odbc.erl:507 -msgid "Conference room does not exist" -msgstr "Pokój konferencyjny nie istnieje" - -#: mod_muc/mod_muc.erl:582 mod_muc/mod_muc_odbc.erl:595 -msgid "Chatrooms" -msgstr "Pokoje rozmów" - -#: mod_muc/mod_muc.erl:712 mod_muc/mod_muc_odbc.erl:729 -msgid "You need a client that supports x:data to register the nickname" -msgstr "Potrzebujesz klienta obsługującego x:data aby zarejestrować nick" - -#: mod_muc/mod_muc.erl:718 mod_muc/mod_muc_odbc.erl:735 -msgid "Nickname Registration at " -msgstr "Rejestracja nazwy użytkownika na " - -#: mod_muc/mod_muc.erl:722 mod_muc/mod_muc_odbc.erl:739 -msgid "Enter nickname you want to register" -msgstr "Wprowadz nazwę użytkownika którego chcesz zarejestrować" - -#: mod_muc/mod_muc.erl:723 mod_muc/mod_muc_odbc.erl:740 -#: mod_muc/mod_muc_room.erl:3810 mod_roster.erl:944 mod_roster_odbc.erl:1061 -#: mod_vcard.erl:364 mod_vcard.erl:477 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:453 -msgid "Nickname" -msgstr "Nazwa użytkownika" - -#: mod_muc/mod_muc.erl:762 mod_muc/mod_muc_odbc.erl:784 -#: mod_muc/mod_muc_room.erl:1064 mod_muc/mod_muc_room.erl:1787 -msgid "That nickname is registered by another person" -msgstr "Ta nazwa użytkownika jest już zarejestrowana przez inną osobę" - -#: mod_muc/mod_muc.erl:788 mod_muc/mod_muc_odbc.erl:811 -msgid "You must fill in field \"Nickname\" in the form" -msgstr "Musisz wypełnić pole \"Nazwa użytkownika\" w formularzu" - -#: mod_muc/mod_muc.erl:808 mod_muc/mod_muc_odbc.erl:831 -msgid "ejabberd MUC module" -msgstr "Moduł MUC" - -#: mod_muc/mod_muc_log.erl:374 mod_muc/mod_muc_log.erl:381 -msgid "Chatroom configuration modified" -msgstr "Konfiguracja pokoju zmodyfikowana" - -#: mod_muc/mod_muc_log.erl:384 -msgid "joins the room" -msgstr "dołącza do pokoju" - -#: mod_muc/mod_muc_log.erl:387 mod_muc/mod_muc_log.erl:390 -msgid "leaves the room" -msgstr "opuszcza pokój" - -#: mod_muc/mod_muc_log.erl:393 mod_muc/mod_muc_log.erl:396 -msgid "has been banned" -msgstr "został wykluczony" - -#: mod_muc/mod_muc_log.erl:399 mod_muc/mod_muc_log.erl:402 -msgid "has been kicked" -msgstr "został wyrzucony" - -#: mod_muc/mod_muc_log.erl:405 -msgid "has been kicked because of an affiliation change" -msgstr "został wyrzucony z powodu zmiany przynależności" - -#: mod_muc/mod_muc_log.erl:408 -msgid "has been kicked because the room has been changed to members-only" -msgstr "został wyrzucony z powodu zmiany pokoju na \"Tylko dla Członków\"" - -#: mod_muc/mod_muc_log.erl:411 -msgid "has been kicked because of a system shutdown" -msgstr "został wyrzucony z powodu wyłączenia systemu" - -#: mod_muc/mod_muc_log.erl:414 -msgid "is now known as" -msgstr "jest teraz znany jako" - -#: mod_muc/mod_muc_log.erl:417 mod_muc/mod_muc_log.erl:688 -#: mod_muc/mod_muc_room.erl:2393 -msgid " has set the subject to: " -msgstr " zmienił temat na: " - -#: mod_muc/mod_muc_log.erl:452 -msgid "Chatroom is created" -msgstr "Pokój został stworzony" - -#: mod_muc/mod_muc_log.erl:453 -msgid "Chatroom is destroyed" -msgstr "Pokój został usunięty" - -#: mod_muc/mod_muc_log.erl:454 -msgid "Chatroom is started" -msgstr "Pokój został uruchomiony" - -#: mod_muc/mod_muc_log.erl:455 -msgid "Chatroom is stopped" -msgstr "Pokój został zatrzymany" - -#: mod_muc/mod_muc_log.erl:459 -msgid "Monday" -msgstr "Poniedziałek" - -#: mod_muc/mod_muc_log.erl:460 -msgid "Tuesday" -msgstr "Wtorek" - -#: mod_muc/mod_muc_log.erl:461 -msgid "Wednesday" -msgstr "Środa" - -#: mod_muc/mod_muc_log.erl:462 -msgid "Thursday" -msgstr "Czwartek" - -#: mod_muc/mod_muc_log.erl:463 -msgid "Friday" -msgstr "Piątek" - -#: mod_muc/mod_muc_log.erl:464 -msgid "Saturday" -msgstr "Sobota" - -#: mod_muc/mod_muc_log.erl:465 -msgid "Sunday" -msgstr "Niedziela" - -#: mod_muc/mod_muc_log.erl:469 -msgid "January" -msgstr "Styczeń" - -#: mod_muc/mod_muc_log.erl:470 -msgid "February" -msgstr "Luty" - -#: mod_muc/mod_muc_log.erl:471 -msgid "March" -msgstr "Marzec" - -#: mod_muc/mod_muc_log.erl:472 -msgid "April" -msgstr "Kwiecień" - -#: mod_muc/mod_muc_log.erl:473 -msgid "May" -msgstr "Maj" - -#: mod_muc/mod_muc_log.erl:474 -msgid "June" -msgstr "Czerwiec" - -#: mod_muc/mod_muc_log.erl:475 -msgid "July" -msgstr "Lipiec" - -#: mod_muc/mod_muc_log.erl:476 -msgid "August" -msgstr "Sierpień" - -#: mod_muc/mod_muc_log.erl:477 -msgid "September" -msgstr "Wrzesień" - -#: mod_muc/mod_muc_log.erl:478 -msgid "October" -msgstr "Październik" - -#: mod_muc/mod_muc_log.erl:479 -msgid "November" -msgstr "Listopad" - -#: mod_muc/mod_muc_log.erl:480 -msgid "December" -msgstr "Grudzień" - -#: mod_muc/mod_muc_log.erl:750 -msgid "Room Configuration" -msgstr "Konfiguracja pokoju" - -#: mod_muc/mod_muc_log.erl:759 -msgid "Room Occupants" -msgstr "Lista uczestników" - -#: mod_muc/mod_muc_room.erl:174 -msgid "Traffic rate limit is exceeded" -msgstr "Limit transferu przekroczony" - -#: mod_muc/mod_muc_room.erl:246 -msgid "" -"This participant is kicked from the room because he sent an error message" -msgstr "" -"Ten uczestnik został wyrzucony z pokoju ponieważ wysłał komunikat błędu" - -#: mod_muc/mod_muc_room.erl:255 -msgid "It is not allowed to send private messages to the conference" -msgstr "Nie wolno wysyłac prywatnych wiadomości na konferencję" - -#: mod_muc/mod_muc_room.erl:332 -msgid "Please, wait for a while before sending new voice request" -msgstr "Proszę poczekać chwile, zanim wyślesz nowe żądanie głosowe" - -#: mod_muc/mod_muc_room.erl:347 -msgid "Voice requests are disabled in this conference" -msgstr "Głosowe żądania są wyłączone w tym pokoju" - -#: mod_muc/mod_muc_room.erl:364 -msgid "Failed to extract JID from your voice request approval" -msgstr "Nie udało się wydobyć JID-u z twojego żądania" - -#: mod_muc/mod_muc_room.erl:393 -msgid "Only moderators can approve voice requests" -msgstr "Tylko moderatorzy mogą zatwierdzać żądania głosowe" - -#: mod_muc/mod_muc_room.erl:408 -msgid "Improper message type" -msgstr "Nieprawidłowy typ wiadomości" - -#: mod_muc/mod_muc_room.erl:518 -msgid "" -"This participant is kicked from the room because he sent an error message to " -"another participant" -msgstr "" -"Ten uczestnik został wyrzucony z pokoju ponieważ wysłał komunikat błędu do " -"innego uczestnika" - -#: mod_muc/mod_muc_room.erl:531 -msgid "It is not allowed to send private messages of type \"groupchat\"" -msgstr "Nie można wysyłać prywatnych wiadomości typu \"groupchat\"" - -#: mod_muc/mod_muc_room.erl:543 mod_muc/mod_muc_room.erl:611 -msgid "Recipient is not in the conference room" -msgstr "Odbiorcy nie ma w pokoju" - -#: mod_muc/mod_muc_room.erl:564 mod_muc/mod_muc_room.erl:585 -msgid "It is not allowed to send private messages" -msgstr "Wysyłanie prywatnych wiadomości jest zabronione" - -#: mod_muc/mod_muc_room.erl:576 mod_muc/mod_muc_room.erl:956 -#: mod_muc/mod_muc_room.erl:4040 -msgid "Only occupants are allowed to send messages to the conference" -msgstr "Tylko uczestnicy mogą wysyłać wiadomości na konferencję" - -#: mod_muc/mod_muc_room.erl:634 -msgid "Only occupants are allowed to send queries to the conference" -msgstr "Tylko uczestnicy mogą wysyłać zapytania do konferencji" - -#: mod_muc/mod_muc_room.erl:646 -msgid "Queries to the conference members are not allowed in this room" -msgstr "Informacje o członkach konferencji nie są dostępne w tym pokoju" - -#: mod_muc/mod_muc_room.erl:932 -msgid "" -"Only moderators and participants are allowed to change the subject in this " -"room" -msgstr "Tylko moderatorzy i uczestnicy mogą zmienić temat tego pokoju" - -#: mod_muc/mod_muc_room.erl:937 -msgid "Only moderators are allowed to change the subject in this room" -msgstr "Tylko moderatorzy mogą zmienić temat tego pokoju" - -#: mod_muc/mod_muc_room.erl:947 -msgid "Visitors are not allowed to send messages to all occupants" -msgstr "Odwiedzający nie mogą wysyłać wiadomości do wszystkich obecnych" - -#: mod_muc/mod_muc_room.erl:1021 -msgid "" -"This participant is kicked from the room because he sent an error presence" -msgstr "" -"Ten uczestnik został wyrzucony z pokoju ponieważ jego informacja o statusie " -"zawierała błędy" - -#: mod_muc/mod_muc_room.erl:1040 -msgid "Visitors are not allowed to change their nicknames in this room" -msgstr "Uczestnicy tego pokoju nie mogą zmieniać swoich nicków" - -#: mod_muc/mod_muc_room.erl:1053 mod_muc/mod_muc_room.erl:1779 -msgid "That nickname is already in use by another occupant" -msgstr "Ta nazwa użytkownika jest używana przez kogoś innego" - -#: mod_muc/mod_muc_room.erl:1768 -msgid "You have been banned from this room" -msgstr "Zostałeś wykluczony z tego pokoju" - -#: mod_muc/mod_muc_room.erl:1771 -msgid "Membership is required to enter this room" -msgstr "Musisz być na liście członków tego pokoju aby do niego wejść" - -#: mod_muc/mod_muc_room.erl:1807 -msgid "This room is not anonymous" -msgstr "Ten pokój nie jest anonimowy" - -#: mod_muc/mod_muc_room.erl:1833 -msgid "A password is required to enter this room" -msgstr "Aby wejść do pokoju wymagane jest hasło" - -#: mod_muc/mod_muc_room.erl:1855 mod_register.erl:246 -msgid "Too many CAPTCHA requests" -msgstr "Za dużo żądań CAPTCHA" - -#: mod_muc/mod_muc_room.erl:1864 mod_register.erl:251 -msgid "Unable to generate a CAPTCHA" -msgstr "Nie można wygenerować CAPTCHA" - -#: mod_muc/mod_muc_room.erl:1874 -msgid "Incorrect password" -msgstr "Nieprawidłowe hasło" - -#: mod_muc/mod_muc_room.erl:2448 -msgid "Administrator privileges required" -msgstr "Wymagane uprawnienia administratora" - -#: mod_muc/mod_muc_room.erl:2463 -msgid "Moderator privileges required" -msgstr "Wymagane uprawnienia moderatora" - -#: mod_muc/mod_muc_room.erl:2619 -msgid "Jabber ID ~s is invalid" -msgstr "Jabber ID ~s jest niepoprawny" - -#: mod_muc/mod_muc_room.erl:2633 -msgid "Nickname ~s does not exist in the room" -msgstr "Nie ma nicka ~s w tym pokoju" - -#: mod_muc/mod_muc_room.erl:2659 mod_muc/mod_muc_room.erl:3049 -msgid "Invalid affiliation: ~s" -msgstr "Nieprawidłowa przynależność: ~s" - -#: mod_muc/mod_muc_room.erl:2713 -msgid "Invalid role: ~s" -msgstr "Nieprawidłowa rola: ~s" - -#: mod_muc/mod_muc_room.erl:3026 mod_muc/mod_muc_room.erl:3062 -msgid "Owner privileges required" -msgstr "Wymagane uprawnienia właściciela" - -#: mod_muc/mod_muc_room.erl:3195 -msgid "Configuration of room ~s" -msgstr "Konfiguracja pokoju ~s" - -#: mod_muc/mod_muc_room.erl:3200 -msgid "Room title" -msgstr "Tytuł pokoju" - -#: mod_muc/mod_muc_room.erl:3203 mod_muc/mod_muc_room.erl:3692 -msgid "Room description" -msgstr "Opis pokoju" - -#: mod_muc/mod_muc_room.erl:3210 -msgid "Make room persistent" -msgstr "Utwórz pokój na stałe" - -#: mod_muc/mod_muc_room.erl:3215 -msgid "Make room public searchable" -msgstr "Pozwól wyszukiwać pokój" - -#: mod_muc/mod_muc_room.erl:3218 -msgid "Make participants list public" -msgstr "Upublicznij listę uczestników" - -#: mod_muc/mod_muc_room.erl:3221 -msgid "Make room password protected" -msgstr "Pokój zabezpieczony hasłem" - -#: mod_muc/mod_muc_room.erl:3232 -msgid "Maximum Number of Occupants" -msgstr "Maksymalna liczba uczestników" - -#: mod_muc/mod_muc_room.erl:3239 -msgid "No limit" -msgstr "Bez limitu" - -#: mod_muc/mod_muc_room.erl:3250 -msgid "Present real Jabber IDs to" -msgstr "Prawdziwe Jabber ID widoczne dla" - -#: mod_muc/mod_muc_room.erl:3258 mod_muc/mod_muc_room.erl:3292 -msgid "moderators only" -msgstr "tylko moderatorzy" - -#: mod_muc/mod_muc_room.erl:3260 mod_muc/mod_muc_room.erl:3294 -msgid "anyone" -msgstr "wszystkich" - -#: mod_muc/mod_muc_room.erl:3262 -msgid "Make room members-only" -msgstr "Pokój tylko dla członków" - -#: mod_muc/mod_muc_room.erl:3265 -msgid "Make room moderated" -msgstr "Pokój moderowany" - -#: mod_muc/mod_muc_room.erl:3268 -msgid "Default users as participants" -msgstr "Domyślni użytkownicy jako uczestnicy" - -#: mod_muc/mod_muc_room.erl:3271 -msgid "Allow users to change the subject" -msgstr "Pozwól użytkownikom zmieniać temat" - -#: mod_muc/mod_muc_room.erl:3274 -msgid "Allow users to send private messages" -msgstr "Pozwól użytkownikom wysyłać prywatne wiadomości" - -#: mod_muc/mod_muc_room.erl:3279 -msgid "Allow visitors to send private messages to" -msgstr "Pozwól użytkownikom wysyłać prywatne wiadomości" - -#: mod_muc/mod_muc_room.erl:3290 -msgid "nobody" -msgstr "nikt" - -#: mod_muc/mod_muc_room.erl:3296 -msgid "Allow users to query other users" -msgstr "Pozwól użytkownikom pobierać informacje o innych użytkownikach" - -#: mod_muc/mod_muc_room.erl:3299 -msgid "Allow users to send invites" -msgstr "Pozwól użytkownikom wysyłać zaproszenia" - -#: mod_muc/mod_muc_room.erl:3302 -msgid "Allow visitors to send status text in presence updates" -msgstr "Pozwól uczestnikom na wysyłanie statusów opisowych" - -#: mod_muc/mod_muc_room.erl:3305 -msgid "Allow visitors to change nickname" -msgstr "Pozwól uczestnikom na zmianę nicka" - -#: mod_muc/mod_muc_room.erl:3308 -msgid "Allow visitors to send voice requests" -msgstr "Pozwól użytkownikom wysyłać zaproszenia" - -#: mod_muc/mod_muc_room.erl:3311 -msgid "Minimum interval between voice requests (in seconds)" -msgstr "Minimalny odstęp między żądaniami głosowymi (w sekundach)" - -#: mod_muc/mod_muc_room.erl:3317 -msgid "Make room CAPTCHA protected" -msgstr "Pokój zabezpieczony captchą" - -#: mod_muc/mod_muc_room.erl:3322 -msgid "Exclude Jabber IDs from CAPTCHA challenge" -msgstr "Pomiń Jabber ID z żądania CAPTCHA" - -#: mod_muc/mod_muc_room.erl:3329 -msgid "Enable logging" -msgstr "Włącz logowanie" - -#: mod_muc/mod_muc_room.erl:3337 -msgid "You need an x:data capable client to configure room" -msgstr "Potrzebujesz klienta obsługującego x:data aby skonfigurować pokój" - -#: mod_muc/mod_muc_room.erl:3694 -msgid "Number of occupants" -msgstr "Liczba uczestników" - -#: mod_muc/mod_muc_room.erl:3750 -msgid "private, " -msgstr "prywatny, " - -#: mod_muc/mod_muc_room.erl:3799 -msgid "Voice request" -msgstr "Żądanie głosowe" - -#: mod_muc/mod_muc_room.erl:3803 -msgid "Either approve or decline the voice request." -msgstr "Zatwierdź lub odrzuć żądanie głosowe" - -#: mod_muc/mod_muc_room.erl:3809 -msgid "User JID" -msgstr "Użytkownik " - -#: mod_muc/mod_muc_room.erl:3811 -msgid "Grant voice to this person?" -msgstr "Udzielić głosu tej osobie?" - -#: mod_muc/mod_muc_room.erl:3960 -msgid "~s invites you to the room ~s" -msgstr "~s zaprasza Cię do pokoju ~s" - -#: mod_muc/mod_muc_room.erl:3969 -msgid "the password is" -msgstr "hasło to:" - -#: mod_offline.erl:510 mod_offline_odbc.erl:352 -msgid "" -"Your contact offline message queue is full. The message has been discarded." -msgstr "" -"Kolejka wiadomości offline adresata jest pełna. Wiadomość została odrzucona." - -#: mod_offline.erl:560 mod_offline_odbc.erl:408 -msgid "~s's Offline Messages Queue" -msgstr "Kolejka wiadomości offline użytkownika ~s" - -#: mod_offline.erl:563 mod_offline_odbc.erl:411 mod_roster.erl:987 -#: mod_roster_odbc.erl:1104 mod_shared_roster.erl:901 -#: mod_shared_roster.erl:1010 mod_shared_roster_odbc.erl:930 -#: mod_shared_roster_odbc.erl:1039 web/ejabberd_web_admin.erl:833 -#: web/ejabberd_web_admin.erl:874 web/ejabberd_web_admin.erl:942 -#: web/ejabberd_web_admin.erl:978 web/ejabberd_web_admin.erl:1019 -#: web/ejabberd_web_admin.erl:1508 web/ejabberd_web_admin.erl:1760 -#: web/ejabberd_web_admin.erl:1931 web/ejabberd_web_admin.erl:1963 -#: web/ejabberd_web_admin.erl:2031 web/ejabberd_web_admin.erl:2135 -#: web/ejabberd_web_admin.erl:2160 web/ejabberd_web_admin.erl:2248 -msgid "Submitted" -msgstr "Wprowadzone" - -#: mod_offline.erl:571 -msgid "Time" -msgstr "Czas" - -#: mod_offline.erl:572 -msgid "From" -msgstr "Od" - -#: mod_offline.erl:573 -msgid "To" -msgstr "Do" - -#: mod_offline.erl:574 mod_offline_odbc.erl:419 -msgid "Packet" -msgstr "Pakiet" - -#: mod_offline.erl:587 mod_offline_odbc.erl:432 mod_shared_roster.erl:908 -#: mod_shared_roster_odbc.erl:937 web/ejabberd_web_admin.erl:882 -#: web/ejabberd_web_admin.erl:986 -msgid "Delete Selected" -msgstr "Usuń zaznaczone" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -msgid "Offline Messages:" -msgstr "Wiadomości offline:" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -msgid "Remove All Offline Messages" -msgstr "Usuń wszystkie wiadomości typu 'Offline'" - -#: mod_proxy65/mod_proxy65_service.erl:213 -msgid "ejabberd SOCKS5 Bytestreams module" -msgstr "Moduł SOCKS5 Bytestreams" - -#: mod_pubsub/mod_pubsub.erl:1120 mod_pubsub/mod_pubsub_odbc.erl:929 -msgid "Publish-Subscribe" -msgstr "PubSub" - -#: mod_pubsub/mod_pubsub.erl:1214 mod_pubsub/mod_pubsub_odbc.erl:1025 -msgid "ejabberd Publish-Subscribe module" -msgstr "Moduł Publish-Subscribe" - -#: mod_pubsub/mod_pubsub.erl:1497 mod_pubsub/mod_pubsub_odbc.erl:1312 -msgid "PubSub subscriber request" -msgstr "Żądanie subskrybcji PubSub" - -#: mod_pubsub/mod_pubsub.erl:1499 mod_pubsub/mod_pubsub_odbc.erl:1314 -msgid "Choose whether to approve this entity's subscription." -msgstr "Wybierz, czy akceptować subskrypcję tej jednostki" - -#: mod_pubsub/mod_pubsub.erl:1505 mod_pubsub/mod_pubsub_odbc.erl:1320 -msgid "Node ID" -msgstr "ID węzła" - -#: mod_pubsub/mod_pubsub.erl:1510 mod_pubsub/mod_pubsub_odbc.erl:1325 -msgid "Subscriber Address" -msgstr "Adres subskrybenta" - -#: mod_pubsub/mod_pubsub.erl:1516 mod_pubsub/mod_pubsub_odbc.erl:1331 -msgid "Allow this Jabber ID to subscribe to this pubsub node?" -msgstr "Pozwól temu Jabber ID na zapisanie się do tego węzła PubSub" - -#: mod_pubsub/mod_pubsub.erl:3474 mod_pubsub/mod_pubsub_odbc.erl:3287 -msgid "Deliver payloads with event notifications" -msgstr "Dostarczaj zawartość publikacji wraz z powiadomieniami o zdarzeniach" - -#: mod_pubsub/mod_pubsub.erl:3475 mod_pubsub/mod_pubsub_odbc.erl:3288 -msgid "Deliver event notifications" -msgstr "Dostarczaj powiadomienia o zdarzeniach" - -#: mod_pubsub/mod_pubsub.erl:3476 mod_pubsub/mod_pubsub_odbc.erl:3289 -msgid "Notify subscribers when the node configuration changes" -msgstr "Informuj subskrybentów o zmianach konfiguracji węzła" - -#: mod_pubsub/mod_pubsub.erl:3477 mod_pubsub/mod_pubsub_odbc.erl:3290 -msgid "Notify subscribers when the node is deleted" -msgstr "Informuj subskrybentów o usunięciu węzła" - -#: mod_pubsub/mod_pubsub.erl:3478 mod_pubsub/mod_pubsub_odbc.erl:3291 -msgid "Notify subscribers when items are removed from the node" -msgstr "Informuj subskrybentów o usunięciu elementów węzła" - -#: mod_pubsub/mod_pubsub.erl:3479 mod_pubsub/mod_pubsub_odbc.erl:3292 -msgid "Persist items to storage" -msgstr "Przechowuj na stałe dane PubSub" - -#: mod_pubsub/mod_pubsub.erl:3480 mod_pubsub/mod_pubsub_odbc.erl:3293 -msgid "A friendly name for the node" -msgstr "Przyjazna nazwa węzła" - -#: mod_pubsub/mod_pubsub.erl:3481 mod_pubsub/mod_pubsub_odbc.erl:3294 -msgid "Max # of items to persist" -msgstr "Maksymalna liczba przechowywanych przedmiotów" - -#: mod_pubsub/mod_pubsub.erl:3482 mod_pubsub/mod_pubsub_odbc.erl:3295 -msgid "Whether to allow subscriptions" -msgstr "Czy pozwolić na subskrypcje" - -#: mod_pubsub/mod_pubsub.erl:3483 mod_pubsub/mod_pubsub_odbc.erl:3296 -msgid "Specify the access model" -msgstr "Określ model dostępu" - -#: mod_pubsub/mod_pubsub.erl:3486 mod_pubsub/mod_pubsub_odbc.erl:3299 -msgid "Roster groups allowed to subscribe" -msgstr "Grupy kontaktów uprawnione do subskrypcji" - -#: mod_pubsub/mod_pubsub.erl:3487 mod_pubsub/mod_pubsub_odbc.erl:3300 -msgid "Specify the publisher model" -msgstr "Określ model publikującego" - -#: mod_pubsub/mod_pubsub.erl:3489 mod_pubsub/mod_pubsub_odbc.erl:3302 -msgid "Purge all items when the relevant publisher goes offline" -msgstr "Usuń wszystkie elementy w momencie kiedy publikujący rozłączy się" - -#: mod_pubsub/mod_pubsub.erl:3490 mod_pubsub/mod_pubsub_odbc.erl:3303 -msgid "Specify the event message type" -msgstr "Określ typ wiadomości" - -#: mod_pubsub/mod_pubsub.erl:3492 mod_pubsub/mod_pubsub_odbc.erl:3305 -msgid "Max payload size in bytes" -msgstr "Maksymalna wielkość powiadomienia w bajtach" - -#: mod_pubsub/mod_pubsub.erl:3493 mod_pubsub/mod_pubsub_odbc.erl:3306 -msgid "When to send the last published item" -msgstr "Kiedy wysłać ostatnio opublikowaną rzecz" - -#: mod_pubsub/mod_pubsub.erl:3495 mod_pubsub/mod_pubsub_odbc.erl:3308 -msgid "Only deliver notifications to available users" -msgstr "Dostarczaj powiadomienia tylko dostępnym użytkownikom" - -#: mod_pubsub/mod_pubsub.erl:3496 mod_pubsub/mod_pubsub_odbc.erl:3309 -msgid "The collections with which a node is affiliated" -msgstr "Grupy, do których należy węzeł" - -#: mod_register.erl:193 -msgid "The CAPTCHA verification has failed" -msgstr "Weryfikacja CAPTCHA nie powiodła się." - -#: mod_register.erl:220 -msgid "You need a client that supports x:data and CAPTCHA to register" -msgstr "Potrzebujesz klienta obsługującego x:data aby zarejestrować nick" - -#: mod_register.erl:226 mod_register.erl:265 -msgid "Choose a username and password to register with this server" -msgstr "" -"Wybierz nazwę użytkownika i hasło aby zarejestrować się na tym serwerze" - -#: mod_register.erl:230 mod_vcard.erl:364 mod_vcard_odbc.erl:342 -#: web/ejabberd_web_admin.erl:1515 web/ejabberd_web_admin.erl:1572 -msgid "User" -msgstr "Użytkownik" - -#: mod_register.erl:316 mod_register.erl:361 -msgid "The password is too weak" -msgstr "Hasło nie jest wystarczająco trudne" - -#: mod_register.erl:365 -msgid "Users are not allowed to register accounts so quickly" -msgstr "Użytkowncy nie mogą tak szybko rejestrować nowych kont" - -#: mod_roster.erl:938 mod_roster_odbc.erl:1055 web/ejabberd_web_admin.erl:1701 -#: web/ejabberd_web_admin.erl:1886 web/ejabberd_web_admin.erl:1897 -#: web/ejabberd_web_admin.erl:2219 -msgid "None" -msgstr "Brak" - -#: mod_roster.erl:945 mod_roster_odbc.erl:1062 -msgid "Subscription" -msgstr "Subskrypcja" - -#: mod_roster.erl:946 mod_roster_odbc.erl:1063 -msgid "Pending" -msgstr "Oczekuje" - -#: mod_roster.erl:947 mod_roster_odbc.erl:1064 -msgid "Groups" -msgstr "Grupy" - -#: mod_roster.erl:974 mod_roster_odbc.erl:1091 -msgid "Validate" -msgstr "Potwierdź" - -#: mod_roster.erl:982 mod_roster_odbc.erl:1099 -msgid "Remove" -msgstr "Usuń" - -#: mod_roster.erl:985 mod_roster_odbc.erl:1102 -msgid "Roster of " -msgstr "Lista kontaktów " - -#: mod_roster.erl:988 mod_roster_odbc.erl:1105 mod_shared_roster.erl:902 -#: mod_shared_roster.erl:1011 mod_shared_roster_odbc.erl:931 -#: mod_shared_roster_odbc.erl:1040 web/ejabberd_web_admin.erl:834 -#: web/ejabberd_web_admin.erl:875 web/ejabberd_web_admin.erl:943 -#: web/ejabberd_web_admin.erl:979 web/ejabberd_web_admin.erl:1020 -#: web/ejabberd_web_admin.erl:1509 web/ejabberd_web_admin.erl:1761 -#: web/ejabberd_web_admin.erl:1932 web/ejabberd_web_admin.erl:2136 -#: web/ejabberd_web_admin.erl:2161 -msgid "Bad format" -msgstr "Błędny format" - -#: mod_roster.erl:995 mod_roster_odbc.erl:1112 -msgid "Add Jabber ID" -msgstr "Dodaj Jabber ID" - -#: mod_roster.erl:1094 mod_roster_odbc.erl:1211 -msgid "Roster" -msgstr "Lista kontaktów" - -#: mod_shared_roster.erl:857 mod_shared_roster.erl:899 -#: mod_shared_roster.erl:1007 mod_shared_roster_odbc.erl:886 -#: mod_shared_roster_odbc.erl:928 mod_shared_roster_odbc.erl:1036 -msgid "Shared Roster Groups" -msgstr "Wspólne grupy kontaktów" - -#: mod_shared_roster.erl:895 mod_shared_roster_odbc.erl:924 -#: web/ejabberd_web_admin.erl:1365 web/ejabberd_web_admin.erl:2461 -msgid "Add New" -msgstr "Dodaj nowe" - -#: mod_shared_roster.erl:978 mod_shared_roster_odbc.erl:1007 -msgid "Name:" -msgstr "Nazwa:" - -#: mod_shared_roster.erl:983 mod_shared_roster_odbc.erl:1012 -msgid "Description:" -msgstr "Opis:" - -#: mod_shared_roster.erl:991 mod_shared_roster_odbc.erl:1020 -msgid "Members:" -msgstr "Członkowie:" - -#: mod_shared_roster.erl:999 mod_shared_roster_odbc.erl:1028 -msgid "Displayed Groups:" -msgstr "Wyświetlane grupy:" - -#: mod_shared_roster.erl:1008 mod_shared_roster_odbc.erl:1037 -msgid "Group " -msgstr "Grupa " - -#: mod_shared_roster.erl:1017 mod_shared_roster_odbc.erl:1046 -#: web/ejabberd_web_admin.erl:840 web/ejabberd_web_admin.erl:884 -#: web/ejabberd_web_admin.erl:949 web/ejabberd_web_admin.erl:1026 -#: web/ejabberd_web_admin.erl:2017 -msgid "Submit" -msgstr "Wyślij" - -#: mod_vcard.erl:165 mod_vcard_ldap.erl:238 mod_vcard_odbc.erl:129 -msgid "Erlang Jabber Server" -msgstr "Erlang Jabber Server" - -#: mod_vcard.erl:364 mod_vcard.erl:478 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:454 -msgid "Birthday" -msgstr "Data urodzenia" - -#: mod_vcard.erl:364 mod_vcard.erl:480 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:456 -msgid "City" -msgstr "Miasto" - -#: mod_vcard.erl:364 mod_vcard.erl:479 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:455 -msgid "Country" -msgstr "Państwo" - -#: mod_vcard.erl:364 mod_vcard.erl:481 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:457 -msgid "Email" -msgstr "Email" - -#: mod_vcard.erl:364 mod_vcard.erl:476 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:452 -msgid "Family Name" -msgstr "Nazwisko" - -#: mod_vcard.erl:364 mod_vcard_odbc.erl:342 -msgid "" -"Fill in the form to search for any matching Jabber User (Add * to the end of " -"field to match substring)" -msgstr "" -"Wypełnij formularz aby wyszukać użytkowników Jabbera (dodaj * na koniec " -"zapytania aby wyszukać po fragmencie)" - -#: mod_vcard.erl:364 mod_vcard.erl:473 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:449 -msgid "Full Name" -msgstr "Pełna nazwa" - -#: mod_vcard.erl:364 mod_vcard.erl:475 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:451 -msgid "Middle Name" -msgstr "Drugie imię" - -#: mod_vcard.erl:364 mod_vcard.erl:474 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:450 web/ejabberd_web_admin.erl:2006 -msgid "Name" -msgstr "Imię" - -#: mod_vcard.erl:364 mod_vcard.erl:482 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:458 -msgid "Organization Name" -msgstr "Nazwa organizacji" - -#: mod_vcard.erl:364 mod_vcard.erl:483 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:459 -msgid "Organization Unit" -msgstr "Dział" - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "Search users in " -msgstr "Wyszukaj użytkowników w " - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "You need an x:data capable client to search" -msgstr "Potrzebujesz klienta obsługującego x:data aby wyszukiwać" - -#: mod_vcard.erl:389 mod_vcard_ldap.erl:487 mod_vcard_odbc.erl:367 -msgid "vCard User Search" -msgstr "Wyszukiwanie vCard użytkowników" - -#: mod_vcard.erl:445 mod_vcard_ldap.erl:541 mod_vcard_odbc.erl:421 -msgid "ejabberd vCard module" -msgstr "Moduł vCard ejabberd" - -#: mod_vcard.erl:469 mod_vcard_ldap.erl:551 mod_vcard_odbc.erl:445 -msgid "Search Results for " -msgstr "Wyniki wyszukiwania dla " - -#: mod_vcard_ldap.erl:462 -msgid "Fill in fields to search for any matching Jabber User" -msgstr "Wypełnij pola aby znaleźć pasujących użytkowników Jabbera" - -#: web/ejabberd_web_admin.erl:193 web/ejabberd_web_admin.erl:203 -#: web/ejabberd_web_admin.erl:219 web/ejabberd_web_admin.erl:229 -msgid "Unauthorized" -msgstr "Nie autoryzowano" - -#: web/ejabberd_web_admin.erl:286 web/ejabberd_web_admin.erl:303 -msgid "ejabberd Web Admin" -msgstr "ejabberd: Panel Administracyjny" - -#: web/ejabberd_web_admin.erl:769 web/ejabberd_web_admin.erl:780 -msgid "Administration" -msgstr "Administracja" - -#: web/ejabberd_web_admin.erl:878 web/ejabberd_web_admin.erl:982 -msgid "Raw" -msgstr "Żródło" - -#: web/ejabberd_web_admin.erl:1017 -msgid "~s access rule configuration" -msgstr "~s konfiguracja zasad dostępu" - -#: web/ejabberd_web_admin.erl:1035 -msgid "Virtual Hosts" -msgstr "Wirtualne Hosty" - -#: web/ejabberd_web_admin.erl:1043 web/ejabberd_web_admin.erl:1050 -msgid "Users" -msgstr "Użytkownicy" - -#: web/ejabberd_web_admin.erl:1078 -msgid "Users Last Activity" -msgstr "Ostatnia aktywność użytkowników" - -#: web/ejabberd_web_admin.erl:1080 -msgid "Period: " -msgstr "Przedział czasu: " - -#: web/ejabberd_web_admin.erl:1090 -msgid "Last month" -msgstr "Miniony miesiąc" - -#: web/ejabberd_web_admin.erl:1091 -msgid "Last year" -msgstr "Miniony rok" - -#: web/ejabberd_web_admin.erl:1092 -msgid "All activity" -msgstr "Cała aktywność" - -#: web/ejabberd_web_admin.erl:1094 -msgid "Show Ordinary Table" -msgstr "Pokaż zwykłą tabelę" - -#: web/ejabberd_web_admin.erl:1096 -msgid "Show Integral Table" -msgstr "Pokaż tabelę całkowitą" - -#: web/ejabberd_web_admin.erl:1105 web/ejabberd_web_admin.erl:1939 -msgid "Statistics" -msgstr "Statystyki" - -#: web/ejabberd_web_admin.erl:1117 -msgid "Not Found" -msgstr "Nie znaleziono" - -#: web/ejabberd_web_admin.erl:1134 -msgid "Node not found" -msgstr "Węzeł nie został znaleziony" - -#: web/ejabberd_web_admin.erl:1460 -msgid "Host" -msgstr "Host" - -#: web/ejabberd_web_admin.erl:1461 -msgid "Registered Users" -msgstr "Użytkownicy zarejestrowani" - -#: web/ejabberd_web_admin.erl:1573 -msgid "Offline Messages" -msgstr "Wiadomości offline" - -#: web/ejabberd_web_admin.erl:1574 web/ejabberd_web_admin.erl:1767 -msgid "Last Activity" -msgstr "Ostatnia aktywność" - -#: web/ejabberd_web_admin.erl:1659 web/ejabberd_web_admin.erl:1675 -msgid "Registered Users:" -msgstr "Użytkownicy zarejestrowani:" - -#: web/ejabberd_web_admin.erl:1661 web/ejabberd_web_admin.erl:1677 -#: web/ejabberd_web_admin.erl:2192 -msgid "Online Users:" -msgstr "Użytkownicy zalogowani:" - -#: web/ejabberd_web_admin.erl:1663 -msgid "Outgoing s2s Connections:" -msgstr "Wychodzące połączenia s2s:" - -#: web/ejabberd_web_admin.erl:1665 -msgid "Outgoing s2s Servers:" -msgstr "Serwery zewnętrzne s2s:" - -#: web/ejabberd_web_admin.erl:1734 web/mod_register_web.erl:175 -#: web/mod_register_web.erl:372 web/mod_register_web.erl:381 -#: web/mod_register_web.erl:411 -msgid "Change Password" -msgstr "Zmień hasło" - -#: web/ejabberd_web_admin.erl:1758 -msgid "User " -msgstr "Użytkownik " - -#: web/ejabberd_web_admin.erl:1765 -msgid "Connected Resources:" -msgstr "Zasoby zalogowane:" - -#: web/ejabberd_web_admin.erl:1766 web/mod_register_web.erl:227 -#: web/mod_register_web.erl:519 -msgid "Password:" -msgstr "Hasło:" - -#: web/ejabberd_web_admin.erl:1828 -msgid "No Data" -msgstr "Brak danych" - -#: web/ejabberd_web_admin.erl:1906 -msgid "Nodes" -msgstr "Węzły" - -#: web/ejabberd_web_admin.erl:1929 web/ejabberd_web_admin.erl:1951 -msgid "Node " -msgstr "Węzeł " - -#: web/ejabberd_web_admin.erl:1938 -msgid "Listened Ports" -msgstr "Porty nasłuchujące" - -#: web/ejabberd_web_admin.erl:1940 web/ejabberd_web_admin.erl:2260 -#: web/ejabberd_web_admin.erl:2448 -msgid "Update" -msgstr "Aktualizuj" - -#: web/ejabberd_web_admin.erl:1943 web/ejabberd_web_admin.erl:2569 -msgid "Restart" -msgstr "Uruchom ponownie" - -#: web/ejabberd_web_admin.erl:1945 web/ejabberd_web_admin.erl:2571 -msgid "Stop" -msgstr "Zatrzymaj" - -#: web/ejabberd_web_admin.erl:1959 -msgid "RPC Call Error" -msgstr "Błąd żądania RPC" - -#: web/ejabberd_web_admin.erl:2000 -msgid "Database Tables at " -msgstr "Tabele bazy na " - -#: web/ejabberd_web_admin.erl:2007 -msgid "Storage Type" -msgstr "Typ bazy" - -#: web/ejabberd_web_admin.erl:2008 -msgid "Elements" -msgstr "Elementy" - -#: web/ejabberd_web_admin.erl:2009 -msgid "Memory" -msgstr "Pamięć" - -#: web/ejabberd_web_admin.erl:2032 web/ejabberd_web_admin.erl:2137 -msgid "Error" -msgstr "Błąd" - -#: web/ejabberd_web_admin.erl:2034 -msgid "Backup of " -msgstr "Kopia zapasowa " - -#: web/ejabberd_web_admin.erl:2036 -msgid "" -"Please note that these options will only backup the builtin Mnesia database. " -"If you are using the ODBC module, you also need to backup your SQL database " -"separately." -msgstr "" -"Te opcje kopii zapasowych dotyczą tylko wbudowanej bazy danych typu Mnesia. " -"Jeśli korzystasz z modułu ODBC, musisz wykonać kopie bazy we własnym " -"zakresie." - -#: web/ejabberd_web_admin.erl:2041 -msgid "Store binary backup:" -msgstr "Zachowaj kopię binarną:" - -#: web/ejabberd_web_admin.erl:2045 web/ejabberd_web_admin.erl:2052 -#: web/ejabberd_web_admin.erl:2060 web/ejabberd_web_admin.erl:2067 -#: web/ejabberd_web_admin.erl:2074 web/ejabberd_web_admin.erl:2081 -#: web/ejabberd_web_admin.erl:2088 web/ejabberd_web_admin.erl:2096 -#: web/ejabberd_web_admin.erl:2103 web/ejabberd_web_admin.erl:2110 -msgid "OK" -msgstr "OK" - -#: web/ejabberd_web_admin.erl:2048 -msgid "Restore binary backup immediately:" -msgstr "Natychmiast odtwórz kopię binarną:" - -#: web/ejabberd_web_admin.erl:2056 -msgid "" -"Restore binary backup after next ejabberd restart (requires less memory):" -msgstr "" -"Odtwórz kopię binarną podczas następnego uruchomienia ejabberd (wymaga mniej " -"zasobów):" - -#: web/ejabberd_web_admin.erl:2063 -msgid "Store plain text backup:" -msgstr "Zachowaj kopię w postaci tekstowej:" - -#: web/ejabberd_web_admin.erl:2070 -msgid "Restore plain text backup immediately:" -msgstr "Natychmiast odtwórz kopię z postaci tekstowej:" - -#: web/ejabberd_web_admin.erl:2077 -msgid "Import users data from a PIEFXIS file (XEP-0227):" -msgstr "Importuj dane użytkowników z pliku w formacie PIEFXIS (XEP-0227):" - -#: web/ejabberd_web_admin.erl:2084 -msgid "Export data of all users in the server to PIEFXIS files (XEP-0227):" -msgstr "" -"Eksportuj dane wszystkich użytkowników serwera do plików w formacie PIEFXIS " -"(XEP-0227):" - -#: web/ejabberd_web_admin.erl:2091 -msgid "Export data of users in a host to PIEFXIS files (XEP-0227):" -msgstr "" -"Eksportuj dane użytkowników z hosta do plików w formacie PIEFXIS (XEP-0227):" - -#: web/ejabberd_web_admin.erl:2099 -msgid "Import user data from jabberd14 spool file:" -msgstr "Importuj dane użytkownika z pliku roboczego serwera jabberd14:" - -#: web/ejabberd_web_admin.erl:2106 -msgid "Import users data from jabberd14 spool directory:" -msgstr "Importuj użytkowników z katalogu roboczego serwera jabberd14" - -#: web/ejabberd_web_admin.erl:2132 -msgid "Listened Ports at " -msgstr "Porty nasłuchujące na " - -#: web/ejabberd_web_admin.erl:2157 -msgid "Modules at " -msgstr "Moduły na " - -#: web/ejabberd_web_admin.erl:2183 -msgid "Statistics of ~p" -msgstr "Statystyki ~p" - -#: web/ejabberd_web_admin.erl:2186 -msgid "Uptime:" -msgstr "Czas pracy:" - -#: web/ejabberd_web_admin.erl:2189 -msgid "CPU Time:" -msgstr "Czas CPU:" - -#: web/ejabberd_web_admin.erl:2195 -msgid "Transactions Committed:" -msgstr "Transakcje zakończone:" - -#: web/ejabberd_web_admin.erl:2198 -msgid "Transactions Aborted:" -msgstr "Transakcje anulowane:" - -#: web/ejabberd_web_admin.erl:2201 -msgid "Transactions Restarted:" -msgstr "Transakcje uruchomione ponownie:" - -#: web/ejabberd_web_admin.erl:2204 -msgid "Transactions Logged:" -msgstr "Transakcje zalogowane:" - -#: web/ejabberd_web_admin.erl:2246 -msgid "Update " -msgstr "Uaktualnij " - -#: web/ejabberd_web_admin.erl:2254 -msgid "Update plan" -msgstr "Plan aktualizacji" - -#: web/ejabberd_web_admin.erl:2255 -msgid "Modified modules" -msgstr "Zmodyfikowane moduły" - -#: web/ejabberd_web_admin.erl:2256 -msgid "Update script" -msgstr "Skrypt aktualizacji" - -#: web/ejabberd_web_admin.erl:2257 -msgid "Low level update script" -msgstr "Skrypt aktualizacji niskiego poziomu" - -#: web/ejabberd_web_admin.erl:2258 -msgid "Script check" -msgstr "Sprawdź skrypt" - -#: web/ejabberd_web_admin.erl:2426 -msgid "Port" -msgstr "Port" - -#: web/ejabberd_web_admin.erl:2427 -msgid "IP" -msgstr "IP" - -#: web/ejabberd_web_admin.erl:2428 -msgid "Protocol" -msgstr "Protokół" - -#: web/ejabberd_web_admin.erl:2429 web/ejabberd_web_admin.erl:2556 -msgid "Module" -msgstr "Moduł" - -#: web/ejabberd_web_admin.erl:2430 web/ejabberd_web_admin.erl:2557 -msgid "Options" -msgstr "Opcje" - -#: web/ejabberd_web_admin.erl:2450 -msgid "Delete" -msgstr "Usuń" - -#: web/ejabberd_web_admin.erl:2579 -msgid "Start" -msgstr "Uruchom" - -#: web/mod_register_web.erl:104 -msgid "Your Jabber account was successfully created." -msgstr "Twoje konto zostało stworzone." - -#: web/mod_register_web.erl:107 -msgid "There was an error creating the account: " -msgstr "Wystąpił błąd podczas tworzenia konta:" - -#: web/mod_register_web.erl:115 -msgid "Your Jabber account was successfully deleted." -msgstr "Twoje konto zostało usunięte." - -#: web/mod_register_web.erl:118 -msgid "There was an error deleting the account: " -msgstr "Podczas usuwania konta wystąpił błąd:" - -#: web/mod_register_web.erl:128 -msgid "The password of your Jabber account was successfully changed." -msgstr "Hasło do Twojego konta zostało zmienione." - -#: web/mod_register_web.erl:131 -msgid "There was an error changing the password: " -msgstr "Podczas próby zmiany hasła wystąpił błąd:" - -#: web/mod_register_web.erl:163 web/mod_register_web.erl:172 -msgid "Jabber Account Registration" -msgstr "Zakładanie konta Jabber" - -#: web/mod_register_web.erl:174 web/mod_register_web.erl:192 -#: web/mod_register_web.erl:201 -msgid "Register a Jabber account" -msgstr "Załóż konto Jabber" - -#: web/mod_register_web.erl:176 web/mod_register_web.erl:493 -#: web/mod_register_web.erl:502 -msgid "Unregister a Jabber account" -msgstr "Usuń konto Jabber" - -#: web/mod_register_web.erl:203 -msgid "" -"This page allows to create a Jabber account in this Jabber server. Your JID " -"(Jabber IDentifier) will be of the form: username@server. Please read " -"carefully the instructions to fill correctly the fields." -msgstr "" -"Niniejsza strona pozwala na założenie konta Jabber na tym serwerze. Twój JID " -"(Jabber IDentyfikator) będzie miał postać: nazwa_użytkownika@serwer. " -"Przeczytaj dokładnie instrukcję i wypełnij pola." - -#: web/mod_register_web.erl:212 web/mod_register_web.erl:386 -#: web/mod_register_web.erl:509 -msgid "Username:" -msgstr "Nazwa użytkownika:" - -#: web/mod_register_web.erl:217 -msgid "This is case insensitive: macbeth is the same that MacBeth and Macbeth." -msgstr "" -"Pole nie rozróżnia wielkości liter: słowo Hanna jest takie samo jak hAnna " -"lub haNNa." - -#: web/mod_register_web.erl:218 -msgid "Characters not allowed:" -msgstr "Te znaki są niedozwolone:" - -#: web/mod_register_web.erl:222 web/mod_register_web.erl:391 -#: web/mod_register_web.erl:514 -msgid "Server:" -msgstr "Serwer:" - -#: web/mod_register_web.erl:232 -msgid "" -"Don't tell your password to anybody, not even the administrators of the " -"Jabber server." -msgstr "" -"Nie podawaj swojego hasła nikomu, nawet administratorowi serwera Jabber." - -#: web/mod_register_web.erl:234 -msgid "You can later change your password using a Jabber client." -msgstr "Możesz później zmienić swoje hasło za pomocą dowolnego klienta Jabber." - -#: web/mod_register_web.erl:235 -msgid "" -"Some Jabber clients can store your password in your computer. Use that " -"feature only if you trust your computer is safe." -msgstr "" -"Niektóre klienty Jabber mogą zapisywać Twoje hasło na komputerze. Używaj tej " -"opcji tylko jeśli ufasz komputerowi na którym pracujesz." - -#: web/mod_register_web.erl:237 -msgid "" -"Memorize your password, or write it in a paper placed in a safe place. In " -"Jabber there isn't an automated way to recover your password if you forget " -"it." -msgstr "" -"Zapamiętaj swoje hasło lub zapisz je na kartce i zachowaj w bezpiecznym " -"miejscu. Na Jabberze nie ma zautomatyzowanego systemu odzyskiwania haseł." - -#: web/mod_register_web.erl:242 web/mod_register_web.erl:406 -msgid "Password Verification:" -msgstr "Weryfikacja hasła:" - -#: web/mod_register_web.erl:250 -msgid "Register" -msgstr "Zarejestruj" - -#: web/mod_register_web.erl:396 -msgid "Old Password:" -msgstr "Stare hasło:" - -#: web/mod_register_web.erl:401 -msgid "New Password:" -msgstr "Nowe hasło:" - -#: web/mod_register_web.erl:504 -msgid "This page allows to unregister a Jabber account in this Jabber server." -msgstr "Ta strona pozwala usunąć konto Jabber z tego serwera." - -#: web/mod_register_web.erl:524 -msgid "Unregister" -msgstr "Wyrejestruj" - -#, fuzzy -#~ msgid "Captcha test failed" -#~ msgstr "Captcha jest poprawna." diff --git a/priv/msgs/pt-br.msg b/priv/msgs/pt-br.msg index c2cdc2359..9fbbb0096 100644 --- a/priv/msgs/pt-br.msg +++ b/priv/msgs/pt-br.msg @@ -1,405 +1,630 @@ -{"Access Configuration","Configuração de Acesso"}. -{"Access Control List Configuration","Configuração da Lista de Controle de Acesso"}. -{"Access control lists","Listas de Controle de Acesso"}. -{"Access Control Lists","Listas de Controle de Acesso"}. -{"Access denied by service policy","Aceso denegado por la política do serviço"}. -{"Access rules","Regras de acesso"}. -{"Access Rules","Regras de Acesso"}. -{"Action on user","Ação no usuário"}. -{"Add Jabber ID","Adicionar ID jabber"}. -{"Add New","Adicionar novo"}. -{"Add User","Adicionar usuário"}. -{"Administration","Administração"}. -{"Administration of ","Administração de "}. -{"Administrator privileges required","Se necessita privilégios de administrador"}. +%% Generated automatically +%% DO NOT EDIT: run `make translations` instead +%% To improve translations please read: +%% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ + +{" (Add * to the end of field to match substring)"," (Adicione * no final do campo para combinar com a substring)"}. +{" has set the subject to: "," mudou o assunto para: "}. +{"# participants","# participantes"}. +{"A description of the node","Uma descrição do nó"}. {"A friendly name for the node","Um nome familiar para o nó"}. +{"A password is required to enter this room","Se necessita senha para entrar nesta sala"}. +{"A Web Page","Uma página da web"}. +{"Accept","Aceito"}. +{"Access denied by service policy","Acesso negado pela política do serviço"}. +{"Access model","Modelo de acesso"}. +{"Account doesn't exist","A conta não existe"}. +{"Action on user","Ação no usuário"}. +{"Add a hat to a user","Adiciona um chapéu num usuário"}. +{"Add User","Adicionar usuário"}. +{"Administration of ","Administração de "}. +{"Administration","Administração"}. +{"Administrator privileges required","Se necessita privilégios de administrador"}. {"All activity","Todas atividades"}. +{"All Users","Todos os usuários"}. +{"Allow subscription","Permitir a assinatura"}. {"Allow this Jabber ID to subscribe to this pubsub node?","Autorizar este Jabber ID para a inscrição neste tópico pubsub?"}. +{"Allow this person to register with the room?","Permita que esta pessoa se registe na sala?"}. {"Allow users to change the subject","Permitir a usuários modificar o assunto"}. {"Allow users to query other users","Permitir a usuários pesquisar informações sobre os demais"}. {"Allow users to send invites","Permitir a usuários envio de convites"}. {"Allow users to send private messages","Permitir a usuários enviarem mensagens privadas"}. {"Allow visitors to change nickname","Permitir mudança de apelido aos visitantes"}. +{"Allow visitors to send private messages to","Permitir visitantes enviar mensagem privada para"}. {"Allow visitors to send status text in presence updates","Permitir atualizações de status aos visitantes"}. -{"All Users","Todos os usuários"}. +{"Allow visitors to send voice requests","Permitir aos visitantes o envio de requisições de voz"}. +{"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.","Um grupo LDAP associado que define a adesão à sala; este deve ser um Nome Distinto LDAP de acordo com uma definição específica da implementação ou da implantação específica de um grupo."}. {"Announcements","Anúncios"}. -{"anyone","qualquer um"}. -{"A password is required to enter this room","Se necessita senha para entrar em esta sala"}. +{"Answer associated with a picture","Resposta associada com uma foto"}. +{"Answer associated with a video","Resposta associada com um vídeo"}. +{"Answer associated with speech","Resposta associada com a fala"}. +{"Answer to a question","Resposta para uma pergunta"}. +{"Anyone in the specified roster group(s) may subscribe and retrieve items","Qualquer pessoa do(s) grupo(s) informado(s) podem se inscrever e recuperar itens"}. +{"Anyone may associate leaf nodes with the collection","Qualquer pessoa pode associar nós das páginas à coleção"}. +{"Anyone may publish","Qualquer pessoa pode publicar"}. +{"Anyone may subscribe and retrieve items","Qualquer pessoa pode se inscrever e recuperar os itens"}. +{"Anyone with a presence subscription of both or from may subscribe and retrieve items","Qualquer pessoa com uma assinatura presente dos dois ou de ambos pode se inscrever e recuperar os itens"}. +{"Anyone with Voice","Qualquer pessoa com voz"}. +{"Anyone","Qualquer pessoa"}. +{"API Commands","Comandos API"}. {"April","Abril"}. +{"Arguments","Argumentos"}. +{"Attribute 'channel' is required for this request","O atributo 'canal' é necessário para esta solicitação"}. +{"Attribute 'id' is mandatory for MIX messages","O atributo 'id' é obrigatório para mensagens MIX"}. +{"Attribute 'jid' is not allowed here","O atributo 'jid' não é permitido aqui"}. +{"Attribute 'node' is not allowed here","O Atributo 'nó' não é permitido aqui"}. +{"Attribute 'to' of stanza that triggered challenge","O atributo 'para' da estrofe que desencadeou o desafio"}. {"August","Agosto"}. +{"Automatic node creation is not enabled","Criação automatizada de nós está desabilitada"}. {"Backup Management","Gestão de Backup"}. -{"Backup of ","Backup de "}. -{"Backup","Salvar cópia de segurança"}. +{"Backup of ~p","Backup de ~p"}. {"Backup to File at ","Salvar backup para arquivo em "}. +{"Backup","Salvar cópia de segurança"}. {"Bad format","Formato incorreto"}. {"Birthday","Aniversário"}. +{"Both the username and the resource are required","Nome de usuário e recurso são necessários"}. +{"Bytestream already activated","Bytestream já foi ativado"}. +{"Cannot remove active list","Não é possível remover uma lista ativa"}. +{"Cannot remove default list","Não é possível remover uma lista padrão"}. +{"CAPTCHA web page","CAPTCHA web page"}. +{"Challenge ID","ID do desafio"}. {"Change Password","Mudar senha"}. {"Change User Password","Alterar Senha do Usuário"}. +{"Changing password is not allowed","Não é permitida a alteração da senha"}. +{"Changing role/affiliation is not allowed","Não é permitida a alteração da função/afiliação"}. +{"Channel already exists","O canal já existe"}. +{"Channel does not exist","O canal não existe"}. +{"Channel JID","Canal JID"}. +{"Channels","Canais"}. {"Characters not allowed:","Caracteres não aceitos:"}. {"Chatroom configuration modified","Configuração da sala de bate-papo modificada"}. {"Chatroom is created","A sala de chat está criada"}. {"Chatroom is destroyed","A sala de chat está destruída"}. -{"Chatroom is started","A sala de chat está inciada"}. +{"Chatroom is started","A sala de chat está iniciada"}. {"Chatroom is stopped","A sala de chat está parada"}. {"Chatrooms","Salas de Chat"}. {"Choose a username and password to register with this server","Escolha um nome de usuário e senha para registrar-se neste servidor"}. -{"Choose modules to stop","Selecione módulos a parar"}. {"Choose storage type of tables","Selecione o tipo de armazenamento das tabelas"}. {"Choose whether to approve this entity's subscription.","Aprovar esta assinatura."}. {"City","Cidade"}. +{"Client acknowledged more stanzas than sent by server","O cliente reconheceu mais estrofes do que as enviadas pelo servidor"}. +{"Clustering","Agrupamento"}. {"Commands","Comandos"}. -{"Conference room does not exist","La sala de conferencias não existe"}. -{"Configuration","Configuração"}. +{"Conference room does not exist","A sala de conferência não existe"}. {"Configuration of room ~s","Configuração para ~s"}. -{"Connected Resources:","Recursos conectados:"}. -{"Connections parameters","Parâmetros para as Conexões"}. +{"Configuration","Configuração"}. +{"Contact Addresses (normally, room owner or owners)","Endereços de contato (normalmente, o proprietário ou os proprietários da sala)"}. {"Country","País"}. -{"CPU Time:","Tempo de CPU"}. -{"Database","Base de dados"}. -{"Database Tables at ","Tabelas de base de dados em "}. +{"Current Discussion Topic","Assunto em discussão"}. +{"Database failure","Falha no banco de dados"}. {"Database Tables Configuration at ","Configuração de Tabelas de Base de dados em "}. +{"Database","Base de dados"}. {"December","Dezembro"}. {"Default users as participants","Usuários padrões como participantes"}. -{"Delete","Eliminar"}. -{"Delete message of the day","Apagar mensagem do dia"}. {"Delete message of the day on all hosts","Apagar a mensagem do dia em todos os hosts"}. -{"Delete Selected","Remover os selecionados"}. +{"Delete message of the day","Apagar mensagem do dia"}. {"Delete User","Deletar Usuário"}. {"Deliver event notifications","Entregar as notificações de evento"}. {"Deliver payloads with event notifications","Enviar payloads junto com as notificações de eventos"}. -{"Description:","Descrição:"}. -{"Disc only copy","Somente copia em disco"}. -{"Don't tell your password to anybody, not even the administrators of the Jabber server.","Não revele o seu computador a ninguém, mesmo para o administrador deste servidor Jabber."}. +{"Disc only copy","Somente cópia em disco"}. +{"Don't tell your password to anybody, not even the administrators of the XMPP server.","Não revele a sua senha para ninguém, nem mesmo para o administrador deste servidor XMPP."}. {"Dump Backup to Text File at ","Exportar backup para texto em "}. -{"Dump to Text File","Exportar para arquivo de texto"}. +{"Dump to Text File","Exportar para arquivo texto"}. +{"Duplicated groups are not allowed by RFC6121","Os grupos duplicados não são permitidos pela RFC6121"}. +{"Dynamically specify a replyto of the item publisher","Definir de forma dinâmica uma resposta da editora do item"}. {"Edit Properties","Editar propriedades"}. {"Either approve or decline the voice request.","Você deve aprovar/desaprovar a requisição de voz."}. -{"ejabberd IRC module","Módulo de IRC para ejabberd"}. +{"ejabberd HTTP Upload service","serviço HTTP de upload ejabberd"}. {"ejabberd MUC module","Módulo de MUC para ejabberd"}. +{"ejabberd Multicast service","Serviço multicast ejabberd"}. {"ejabberd Publish-Subscribe module","Módulo para Publicar Tópicos do ejabberd"}. {"ejabberd SOCKS5 Bytestreams module","Modulo ejabberd SOCKS5 Bytestreams"}. {"ejabberd vCard module","Módulo vCard para ejabberd"}. {"ejabberd Web Admin","ejabberd Web Admin"}. -{"Elements","Elementos"}. +{"ejabberd","ejabberd"}. +{"Email Address","Endereço de e-mail"}. {"Email","Email"}. +{"Enable hats","Ativa chapéus"}. {"Enable logging","Permitir criação de logs"}. -{"Encoding for server ~b","Codificação para o servidor ~b"}. +{"Enable message archiving","Habilitar arquivamento de mensagens"}. +{"Enabling push without 'node' attribute is not supported","Abilitar push sem o atributo 'node' não é suportado"}. {"End User Session","Terminar Sessão do Usuário"}. -{"Enter list of {Module, [Options]}","Introduza lista de {módulo, [opções]}"}. {"Enter nickname you want to register","Introduza o apelido que quer registrar"}. {"Enter path to backup file","Introduza o caminho do arquivo de backup"}. {"Enter path to jabberd14 spool dir","Introduza o caminho para o diretório de fila do jabberd14"}. {"Enter path to jabberd14 spool file","Insira o caminho para a fila (arquivo) do jabberd14"}. {"Enter path to text file","Introduza caminho para o arquivo texto"}. {"Enter the text you see","Insira o texto que você vê"}. -{"Enter username and encodings you wish to use for connecting to IRC servers. Press 'Next' to get more fields to fill in. Press 'Complete' to save settings.","Insira o nome de usuário e codificações que você deseja usar para conectar-se aos servidores de IRC. Depois, presione 'Next' ('Próximo') para exibir mais campos que devem ser preenchidos. Ao final, pressione 'Complete' ('Completar') para salvar a configuração."}. -{"Enter username, encodings, ports and passwords you wish to use for connecting to IRC servers","Insira o nome de usuário, codificações, portas e senhas que vocêdeseja para usar nos servidores IRC"}. -{"Erlang Jabber Server","Servidor Jabber em Erlang"}. -{"Error","Erro"}. -{"Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef.net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}].","Exemplo: [{\"irc.teste.net\", \"koi8-r\"}, 6667, \"senha\"}, {\"dominio.foo.net\", \"iso8859-1\", 7000}, {\"irc.servidordeteste.net\", \"utf-8\"}]."}. +{"Erlang XMPP Server","Servidor XMPP Erlang"}. {"Exclude Jabber IDs from CAPTCHA challenge","Excluir IDs Jabber de serem submetidos ao CAPTCHA"}. +{"Export all tables as SQL queries to a file:","Exportar todas as tabelas como SQL para um arquivo:"}. {"Export data of all users in the server to PIEFXIS files (XEP-0227):","Exportar todos os dados de todos os usuários no servidor, para arquivos formato PIEFXIS (XEP-0227):"}. {"Export data of users in a host to PIEFXIS files (XEP-0227):","Exportar dados dos usuários em um host, para arquivos PIEFXIS (XEP-0227):"}. +{"External component failure","Falha de componente externo"}. +{"External component timeout","Tempo esgotado à espera de componente externo"}. +{"Failed to activate bytestream","Falha ao ativar bytestream"}. {"Failed to extract JID from your voice request approval","Não foi possível extrair o JID (Jabber ID) da requisição de voz"}. +{"Failed to map delegated namespace to external component","Falha ao mapear namespace delegado ao componente externo"}. +{"Failed to parse HTTP response","Falha ao analisar resposta HTTP"}. +{"Failed to process option '~s'","Falha ao processar opção '~s'"}. {"Family Name","Sobrenome"}. +{"FAQ Entry","Registro das perguntas frequentes"}. {"February","Fevereiro"}. -{"Fill in fields to search for any matching Jabber User","Preencha campos para buscar usuários Jabber que concordem"}. -{"Fill in the form to search for any matching Jabber User (Add * to the end of field to match substring)","Preencha o formulário para buscar usuários Jabber. Agrega * ao final de um campo para buscar sub-palavras."}. +{"File larger than ~w bytes","Arquivo maior que ~w bytes"}. +{"Fill in the form to search for any matching XMPP User","Preencha campos para procurar por quaisquer usuários XMPP"}. {"Friday","Sexta"}. -{"From","De"}. -{"From ~s","De ~s"}. +{"From ~ts","De ~s"}. +{"Full List of Room Admins","Lista completa dos administradores das salas"}. +{"Full List of Room Owners","Lista completa dos proprietários das salas"}. {"Full Name","Nome completo"}. +{"Get List of Online Users","Obter a lista de usuários online"}. +{"Get List of Registered Users","Obter a lista de usuários registrados"}. {"Get Number of Online Users","Obter Número de Usuários Online"}. {"Get Number of Registered Users","Obter Número de Usuários Registrados"}. +{"Get Pending","Obter os pendentes"}. {"Get User Last Login Time","Obter a Data do Último Login"}. -{"Get User Password","Obter Senha do Usuário"}. {"Get User Statistics","Obter Estatísticas do Usuário"}. -{"Group ","Grupo "}. -{"Groups","Grupos"}. +{"Given Name","Prenome"}. +{"Grant voice to this person?","Dar voz a esta pessoa?"}. {"has been banned","foi banido"}. -{"has been kicked because of an affiliation change","foi desconectado porque por afiliação inválida"}. {"has been kicked because of a system shutdown","foi desconectado porque o sistema foi desligado"}. +{"has been kicked because of an affiliation change","foi desconectado porque por afiliação inválida"}. {"has been kicked because the room has been changed to members-only","foi desconectado porque a política da sala mudou, só membros são permitidos"}. {"has been kicked","foi removido"}. -{" has set the subject to: "," a posto o assunto: "}. -{"Host","Máquina"}. -{"If you want to specify different ports, passwords, encodings for IRC servers, fill this list with values in format '{\"irc server\", \"encoding\", port, \"password\"}'. By default this service use \"~s\" encoding, port ~p, empty password.","Se você deseja especificar portas diferentes, senhas ou codifações para servidores de IRC, complete esta lista com os valores no formato: '{\"servidor IRC\", \"codificação\", porta, \"senha\"}'. Por padrão, este serviço usa a codificação \"~s\", porta \"~p\", e senha em branco (vazia)"}. +{"Hash of the vCard-temp avatar of this room","Hash do avatar do vCard-temp desta sala"}. +{"Hat title","Título do chapéu"}. +{"Hat URI","URI do chapéu"}. +{"Hats limit exceeded","O limite dos chapéus foi excedido"}. +{"Host unknown","Máquina desconhecida"}. +{"HTTP File Upload","Upload de arquivo HTTP"}. +{"Idle connection","Conexão inativa"}. +{"If you don't see the CAPTCHA image here, visit the web page.","Se você não conseguir ver o CAPTCHA aqui, visite a web page."}. {"Import Directory","Importar diretório"}. {"Import File","Importar arquivo"}. {"Import user data from jabberd14 spool file:","Importar dados dos usuários de uma fila jabberd14:"}. {"Import User from File at ","Importar usuário a partir do arquivo em "}. -{"Import users data from a PIEFXIS file (XEP-0227):","Importar usuários de um arquivo PIEFXIS (XEP-0227): "}. +{"Import users data from a PIEFXIS file (XEP-0227):","Importe os usuários de um arquivo PIEFXIS (XEP-0227):"}. {"Import users data from jabberd14 spool directory:","Importar dados dos usuários de um diretório-fila jabberd14:"}. {"Import Users from Dir at ","Importar usuários a partir do diretório em "}. -{"Import Users From jabberd14 Spool Files","Importar usuários de arquivos jabberd14"}. +{"Import Users From jabberd14 Spool Files","Importar usuários de arquivos jabberd14 (spool files)"}. +{"Improper domain part of 'from' attribute","Atributo 'from' contém domínio incorreto"}. {"Improper message type","Tipo de mensagem incorreto"}. +{"Incorrect CAPTCHA submit","CAPTCHA submetido incorretamente"}. +{"Incorrect data form","Formulário dos dados incorreto"}. {"Incorrect password","Senha incorreta"}. -{"Invalid affiliation: ~s","Afiliação não válida: ~s"}. -{"Invalid role: ~s","Cargo (role) é não válido: ~s"}. +{"Incorrect value of 'action' attribute","Valor incorreto do atributo 'action'"}. +{"Incorrect value of 'action' in data form","Valor incorreto de 'action' no formulário de dados"}. +{"Incorrect value of 'path' in data form","Valor incorreto de 'path' no formulário de dados"}. +{"Installed Modules:","Módulos instalados:"}. +{"Install","Instalar"}. +{"Insufficient privilege","Privilégio insuficiente"}. +{"Internal server error","Erro interno do servidor"}. +{"Invalid 'from' attribute in forwarded message","Atributo 'from' inválido na mensagem reenviada"}. +{"Invalid node name","Nome do nó inválido"}. +{"Invalid 'previd' value","Valor 'previd' inválido"}. +{"Invitations are not allowed in this conference","Os convites não são permitidos nesta conferência"}. {"IP addresses","Endereços IP"}. -{"IP","IP"}. -{"IRC channel (don't put the first #)","Canal IRC (não coloque o #)"}. -{"IRC server","Servidor IRC"}. -{"IRC settings","Configurações do IRC"}. -{"IRC Transport","Transporte IRC"}. -{"IRC username","Usuário IRC"}. -{"IRC Username","Usuário IRC"}. {"is now known as","é agora conhecido como"}. -{"It is not allowed to send private messages","Não é permitido enviar mensagens privadas"}. -{"It is not allowed to send private messages of type \"groupchat\"","No está permitido enviar mensagens privados do tipo \"groupchat\""}. -{"It is not allowed to send private messages to the conference","Impedir o envio de mensagens privadas para a sala"}. -{"Jabber Account Registration","Registros de Contas Jabber"}. +{"It is not allowed to send error messages to the room. The participant (~s) has sent an error message (~s) and got kicked from the room","Não é permitido o envio de mensagens de erro para a sala. O membro (~s) enviou uma mensagem de erro (~s) e foi expulso da sala"}. +{"It is not allowed to send private messages of type \"groupchat\"","Não é permitido enviar mensagens privadas do tipo \"groupchat\""}. +{"It is not allowed to send private messages to the conference","Não é permitido enviar mensagens privadas para a sala de conferência"}. {"Jabber ID","ID Jabber"}. -{"Jabber ID ~s is invalid","O Jabber ID ~s não es válido"}. {"January","Janeiro"}. -{"Join IRC channel","Juntar-se ao canal IRC"}. +{"JID normalization denied by service policy","Normalização JID negada por causa da política de serviços"}. +{"JID normalization failed","A normalização JID falhou"}. +{"Joined MIX channels of ~ts","Entrou no canais MIX do ~ts"}. +{"Joined MIX channels:","Uniu-se aos canais MIX:"}. {"joins the room","Entrar na sala"}. -{"Join the IRC channel here.","Aqui! Juntar-se ao canal IRC."}. -{"Join the IRC channel in this Jabber ID: ~s","Entrar no canal IRC, neste ID Jabber: ~s"}. {"July","Julho"}. {"June","Junho"}. +{"Just created","Acabou de ser criado"}. {"Last Activity","Última atividade"}. {"Last login","Último login"}. +{"Last message","Última mensagem"}. {"Last month","Último mês"}. {"Last year","Último ano"}. +{"Least significant bits of SHA-256 hash of text should equal hexadecimal label","Bits menos significativos do hash sha-256 do texto devem ser iguais ao rótulo hexadecimal"}. {"leaves the room","Sair da sala"}. -{"Listened Ports at ","Portas ouvintes em "}. -{"Listened Ports","Portas escutadas"}. -{"List of modules to start","Listas de módulos para inicializar"}. -{"Low level update script","Script de atualização low level"}. +{"List of users with hats","Lista dos usuários com chapéus"}. +{"List users with hats","Lista os usuários com chapéus"}. +{"Logged Out","Desconectado"}. +{"Logging","Registrando no log"}. {"Make participants list public","Tornar pública a lista de participantes"}. {"Make room CAPTCHA protected","Tornar protegida a senha da sala"}. {"Make room members-only","Tornar sala apenas para membros"}. {"Make room moderated","Tornar a sala moderada"}. -{"Make room password protected","Tornar protegida a senha da sala"}. +{"Make room password protected","Tornar sala protegida à senha"}. {"Make room persistent","Tornar sala persistente"}. {"Make room public searchable","Tornar sala pública possível de ser encontrada"}. +{"Malformed username","Nome de usuário inválido"}. +{"MAM preference modification denied by service policy","Modificação de preferência MAM negada por causa da política de serviços"}. {"March","Março"}. -{"Maximum Number of Occupants","Número máximo de participantes"}. -{"Max # of items to persist","Máximo # de elementos que persistem"}. +{"Max # of items to persist, or `max` for no specific limit other than a server imposed maximum","Máximo # de itens para persistir ou `max` para nenhum limite específico que não seja um servidor imposto como máximo"}. {"Max payload size in bytes","Máximo tamanho do payload em bytes"}. +{"Maximum file size","Tamanho máximo do arquivo"}. +{"Maximum Number of History Messages Returned by Room","Quantidade máxima das mensagens do histórico que foram devolvidas por sala"}. +{"Maximum number of items to persist","Quantidade máxima dos itens para manter"}. +{"Maximum Number of Occupants","Número máximo de participantes"}. {"May","Maio"}. -{"Membership is required to enter this room","Necessitas ser membro de esta sala para poder entrar"}. -{"Members:","Miembros:"}. -{"Memorize your password, or write it in a paper placed in a safe place. In Jabber there isn't an automated way to recover your password if you forget it.","Memorize a sua senha, ou escreva-a em um papel e guarde-o em um lugar seguro. Jabber não é uma maneira automatizada para recuperar a sua senha, se você a esquecer eventualmente."}. -{"Memory","Memória"}. +{"Membership is required to enter this room","É necessário ser membro desta sala para poder entrar"}. +{"Memorize your password, or write it in a paper placed in a safe place. In XMPP there isn't an automated way to recover your password if you forget it.","Memorize a sua senha ou anote-a em um papel guardado em um local seguro. No XMPP, não há uma maneira automatizada de recuperar a sua senha caso a esqueça."}. +{"Mere Availability in XMPP (No Show Value)","Mera disponibilidade no XMPP (Sem valor para ser exibido)"}. {"Message body","Corpo da mensagem"}. +{"Message not found in forwarded payload","Mensagem não encontrada em conteúdo encaminhado"}. +{"Messages from strangers are rejected","As mensagens vindas de estranhos são rejeitadas"}. +{"Messages of type headline","Mensagens do tipo do título"}. +{"Messages of type normal","Mensagens do tipo normal"}. {"Middle Name","Nome do meio"}. {"Minimum interval between voice requests (in seconds)","O intervalo mínimo entre requisições de voz (em segundos)"}. {"Moderator privileges required","Se necessita privilégios de moderador"}. -{"moderators only","apenas moderadores"}. -{"Modified modules","Módulos atualizados"}. -{"Module","Módulo"}. -{"Modules at ","Módulos em "}. -{"Modules","Módulos"}. +{"Moderator","Moderador"}. +{"Moderators Only","Somente moderadores"}. +{"Module failed to handle the query","Módulo falhou ao processar a consulta"}. {"Monday","Segunda"}. -{"Name:","Nome:"}. +{"Multicast","Multicast"}. +{"Multiple elements are not allowed by RFC6121","Vários elementos não são permitidos pela RFC6121"}. +{"Multi-User Chat","Chat multi-usuário"}. {"Name","Nome"}. +{"Natural Language for Room Discussions","Idioma nativo para as discussões na sala"}. +{"Natural-Language Room Name","Nome da sala no idioma nativo"}. +{"Neither 'jid' nor 'nick' attribute found","Nem o atributo 'jid' nem 'nick' foram encontrados"}. +{"Neither 'role' nor 'affiliation' attribute found","Nem o atributo 'role' nem 'affiliation' foram encontrados"}. {"Never","Nunca"}. -{"Nickname","Apelido"}. +{"New Password:","Nova Senha:"}. +{"Nickname can't be empty","O apelido não pode ser vazio"}. {"Nickname Registration at ","Registro do apelido em "}. -{"Nickname ~s does not exist in the room","O nick ~s não existe em la sala"}. -{"nobody","ninguém"}. +{"Nickname ~s does not exist in the room","O apelido ~s não existe na sala"}. +{"Nickname","Apelido"}. +{"No address elements found","Nenhum elemento endereço foi encontrado"}. +{"No addresses element found","Nenhum elemento endereços foi encontrado"}. +{"No 'affiliation' attribute found","Atributo 'affiliation' não foi encontrado"}. +{"No available resource found","Nenhum recurso disponível foi encontrado"}. {"No body provided for announce message","Nenhum corpo de texto fornecido para anunciar mensagem"}. +{"No child elements found","Nenhum elemento filho foi encontrado"}. +{"No data form found","Nenhum formulário de dados foi encontrado"}. {"No Data","Nenhum dado"}. -{"Node ID","ID do Tópico"}. -{"Node ","Nó"}. -{"Node not found","Nó não encontrado"}. -{"Nodes","Nós"}. +{"No features available","Nenhuma funcionalidade disponível"}. +{"No element found","Nenhum elemento foi encontrado"}. +{"No hook has processed this command","Nenhum hook processou este comando"}. +{"No info about last activity found","Não foi encontrada informação sobre última atividade"}. +{"No 'item' element found","O elemento 'item' não foi encontrado"}. +{"No items found in this query","Nenhum item encontrado nesta consulta"}. {"No limit","Ilimitado"}. +{"No module is handling this query","Nenhum módulo está processando esta consulta"}. +{"No node specified","Nenhum nó especificado"}. +{"No 'password' found in data form","'password' não foi encontrado em formulário de dados"}. +{"No 'password' found in this query","'password' não foi encontrado nesta consulta"}. +{"No 'path' found in data form","'path' não foi encontrado em formulário de dados"}. +{"No pending subscriptions found","Não foram encontradas subscrições"}. +{"No privacy list with this name found","Nenhuma lista de privacidade encontrada com este nome"}. +{"No private data found in this query","Nenhum dado privado encontrado nesta consulta"}. +{"No running node found","Nenhum nó em execução foi encontrado"}. +{"No services available","Não há serviços disponíveis"}. +{"No statistics found for this item","Não foram encontradas estatísticas para este item"}. +{"No 'to' attribute found in the invitation","Atributo 'to' não foi encontrado no convite"}. +{"Nobody","Ninguém"}. +{"Node already exists","Nó já existe"}. +{"Node ID","ID do Tópico"}. +{"Node index not found","O índice do nó não foi encontrado"}. +{"Node not found","Nó não encontrado"}. +{"Node ~p","Nó ~p"}. +{"Node","Nó"}. +{"Nodeprep has failed","Processo de identificação de nó falhou (nodeprep)"}. +{"Nodes","Nós"}. {"None","Nenhum"}. -{"No resource provided","Nenhum recurso foi informado"}. +{"Not allowed","Não é permitido"}. {"Not Found","Não encontrado"}. -{"Notify subscribers when items are removed from the node","Notificar subscritores quando os elementos se eliminem do nodo"}. -{"Notify subscribers when the node configuration changes","Notificar subscritores quando cambia la configuração do nodo"}. -{"Notify subscribers when the node is deleted","Notificar subscritores quando o nodo se elimine"}. +{"Not subscribed","Não subscrito"}. +{"Notify subscribers when items are removed from the node","Notificar assinantes quando itens forem eliminados do nó"}. +{"Notify subscribers when the node configuration changes","Notificar assinantes a configuração do nó mudar"}. +{"Notify subscribers when the node is deleted","Notificar assinantes quando o nó for eliminado se elimine"}. {"November","Novembro"}. -{"Number of occupants","Número de participantes"}. +{"Number of answers required","Quantidade de respostas necessárias"}. +{"Number of occupants","Quantidade de ocupantes"}. +{"Number of Offline Messages","Quantidade das mensagens offline"}. {"Number of online users","Número de usuários online"}. {"Number of registered users","Número de usuários registrados"}. +{"Number of seconds after which to automatically purge items, or `max` for no specific limit other than a server imposed maximum","Quantidade de segundos após limpar automaticamente os itens ou `max` para nenhum limite específico que não seja um servidor imposto máximo"}. +{"Occupants are allowed to invite others","As pessoas estão autorizadas a convidar outras pessoas"}. +{"Occupants are allowed to query others","Os ocupantes estão autorizados a consultar os outros"}. +{"Occupants May Change the Subject","As pessoas talvez possam alterar o assunto"}. {"October","Outubro"}. -{"Offline Messages:","Mensagens offline"}. -{"Offline Messages","Mensagens offline"}. {"OK","OK"}. -{"Online","Conectado"}. +{"Old Password:","Senha Antiga:"}. {"Online Users","Usuários conectados"}. -{"Online Users:","Usuários online"}. -{"Only deliver notifications to available users","Solo enviar notificações aos usuários disponíveis"}. +{"Online","Conectado"}. +{"Only collection node owners may associate leaf nodes with the collection","Apenas um grupo dos proprietários dos nós podem associar as páginas na coleção"}. +{"Only deliver notifications to available users","Somente enviar notificações aos usuários disponíveis"}. +{"Only or tags are allowed","Apenas tags ou são permitidas"}. +{"Only element is allowed in this query","Apenas elemento é permitido nesta consulta"}. +{"Only members may query archives of this room","Somente os membros podem procurar nos arquivos desta sala"}. {"Only moderators and participants are allowed to change the subject in this room","Somente os moderadores e os participamentes podem alterar o assunto desta sala"}. {"Only moderators are allowed to change the subject in this room","Somente os moderadores podem alterar o assunto desta sala"}. +{"Only moderators are allowed to retract messages","Apenas moderadores estão autorizados a retirar mensagens"}. {"Only moderators can approve voice requests","Somente moderadores podem aprovar requisições de voz"}. -{"Only occupants are allowed to send messages to the conference","Solo os ocupantes podem enviar mensagens a la sala"}. -{"Only occupants are allowed to send queries to the conference","Solo os ocupantes podem enviar consultas a la sala"}. +{"Only occupants are allowed to send messages to the conference","Somente os ocupantes podem enviar mensagens à sala de conferência"}. +{"Only occupants are allowed to send queries to the conference","Somente os ocupantes podem enviar consultas à sala de conferência"}. +{"Only publishers may publish","Apenas os editores podem publicar"}. {"Only service administrators are allowed to send service messages","Apenas administradores possuem permissão para enviar mensagens de serviço"}. -{"Options","Opções"}. +{"Only those on a whitelist may associate leaf nodes with the collection","Apenas aqueles presentes em uma lista branca podem associar páginas na coleção"}. +{"Only those on a whitelist may subscribe and retrieve items","Apenas aqueles presentes em uma lista branca podem se inscrever e recuperar os itens"}. {"Organization Name","Nome da organização"}. {"Organization Unit","Departamento/Unidade"}. -{"Outgoing s2s Connections","Conexões que partam de s2s"}. -{"Outgoing s2s Connections:","Conexões que partem de s2s"}. -{"Outgoing s2s Servers:","Servidores que partem de s2s"}. -{"Owner privileges required","Se requere privilégios de proprietário da sala"}. -{"Packet","Pacote"}. -{"Password ~b","Senha ~b"}. -{"Password:","Senha:"}. -{"Password","Senha"}. +{"Other Modules Available:","Outros módulos disponíveis:"}. +{"Outgoing s2s Connections","Conexões s2s de Saída"}. +{"Owner privileges required","Se requer privilégios de proprietário da sala"}. +{"Packet relay is denied by service policy","A retransmissão de pacote é negada por causa da política de serviço"}. +{"Participant ID","ID do participante"}. +{"Participant","Participante"}. +{"Password Verification:","Verificação da Senha:"}. {"Password Verification","Verificação de Senha"}. +{"Password","Senha"}. +{"Password:","Senha:"}. {"Path to Dir","Caminho para o diretório"}. {"Path to File","Caminho do arquivo"}. -{"Pending","Pendente"}. +{"Payload semantic type information","Informações de tipo semântico de carga útil"}. {"Period: ","Período: "}. {"Persist items to storage","Persistir elementos ao armazenar"}. +{"Persistent","Persistente"}. +{"Ping query is incorrect","A consulta ping está incorreta"}. {"Ping","Ping"}. {"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Observe que tais opções farão backup apenas da base de dados Mnesia. Caso você esteja utilizando o modulo ODBC, você precisará fazer backup de sua base de dados SQL separadamente."}. {"Please, wait for a while before sending new voice request","Por favor, espere antes de enviar uma nova requisição de voz"}. {"Pong","Pong"}. -{"Port ~b","Porta ~b"}. -{"Port","Porta"}. +{"Possessing 'ask' attribute is not allowed by RFC6121","Possuir o atributo 'ask' não é permitido pela RFC6121"}. {"Present real Jabber IDs to","Tornar o Jabber ID real visível por"}. +{"Previous session not found","A sessão anterior não foi encontrada"}. +{"Previous session PID has been killed","O PID da sessão anterior foi excluído"}. +{"Previous session PID has exited","O PID da sessão anterior foi encerrado"}. +{"Previous session PID is dead","O PID da sessão anterior está morto"}. +{"Previous session timed out","A sessão anterior expirou"}. {"private, ","privado, "}. -{"Protocol","Porta"}. +{"Public","Público"}. +{"Publish model","Publicar o modelo"}. {"Publish-Subscribe","Publicação de Tópico"}. {"PubSub subscriber request","PubSub requisição de assinante"}. {"Purge all items when the relevant publisher goes offline","Descartar todos os itens quando o publicante principal estiver offline"}. -{"Queries to the conference members are not allowed in this room","Nesta sala não se permite consultas aos membros da sala"}. -{"RAM and disc copy","Copias na RAM e disco rígido"}. -{"RAM copy","Copia em RAM"}. -{"Raw","Intocado"}. +{"Push record not found","O registro push não foi encontrado"}. +{"Queries to the conference members are not allowed in this room","Nesta sala de conferência, consultas aos membros não são permitidas"}. +{"Query to another users is forbidden","Consultar a outro usuário é proibido"}. +{"RAM and disc copy","Cópias na RAM e disco rígido"}. +{"RAM copy","Cópia em RAM"}. {"Really delete message of the day?","Deletar realmente a mensagem do dia?"}. -{"Recipient is not in the conference room","O receptor não está em la sala de conferencia"}. -{"Register a Jabber account","Registrar uma conta Jabber"}. -{"Registered Users:","Usuários registrados"}. -{"Registered Users","Usuários Registrados"}. -{"Registration in mod_irc for ","Registro em mod_irc para "}. -{"Remote copy","Copia remota"}. -{"Remove All Offline Messages","Remover Todas as Mensagens Offline"}. -{"Remove","Remover"}. +{"Receive notification from all descendent nodes","Receba a notificação de todos os nós descendentes"}. +{"Receive notification from direct child nodes only","Receba apenas as notificações dos nós relacionados"}. +{"Receive notification of new items only","Receba apenas as notificações dos itens novos"}. +{"Receive notification of new nodes only","Receba apenas as notificações dos nós novos"}. +{"Recipient is not in the conference room","O receptor não está na sala de conferência"}. +{"Register an XMPP account","Registre uma conta XMPP"}. +{"Register","Registrar"}. +{"Remote copy","Cópia remota"}. +{"Remove a hat from a user","Remove um chapéu de um usuário"}. {"Remove User","Remover usuário"}. {"Replaced by new connection","Substituído por nova conexão"}. +{"Request has timed out","O pedido expirou"}. +{"Request is ignored","O pedido foi ignorado"}. +{"Requested role","Função solicitada"}. {"Resources","Recursos"}. -{"Restart","Reiniciar"}. {"Restart Service","Reiniciar Serviço"}. {"Restore Backup from File at ","Restaurar backup a partir do arquivo em "}. -{"Restore binary backup after next ejabberd restart (requires less memory):","Restaurar backup binário após próximo reinicialização do ejabberd (requer menos memória):"}. -{"Restore binary backup immediately:","Restaurar backup binário imediatamente"}. +{"Restore binary backup after next ejabberd restart (requires less memory):","Restaurar backup binário após reinicialização do ejabberd (requer menos memória):"}. +{"Restore binary backup immediately:","Restaurar imediatamente o backup binário:"}. {"Restore plain text backup immediately:","Restaurar backup formato texto imediatamente:"}. {"Restore","Restaurar"}. +{"Result","Resultado"}. +{"Roles and Affiliations that May Retrieve Member List","As funções e as afiliações que podem recuperar a lista dos membros"}. +{"Roles for which Presence is Broadcasted","Para quem a presença será notificada"}. +{"Roles that May Send Private Messages","Atribuições que talvez possam enviar mensagens privadas"}. {"Room Configuration","Configuração de salas"}. -{"Room creation is denied by service policy","Se te a denegado criar la sala por política do serviço"}. +{"Room creation is denied by service policy","Sala não pode ser criada devido à política do serviço"}. {"Room description","Descrição da Sala"}. -{"Room Occupants","Número de participantes"}. +{"Room Occupants","Ocupantes do quarto"}. +{"Room terminates","Terminação da sala"}. {"Room title","Título da sala"}. {"Roster groups allowed to subscribe","Listar grupos autorizados"}. -{"Roster","Lista de contatos"}. -{"Roster of ","Lista de contatos de "}. {"Roster size","Tamanho da Lista"}. -{"RPC Call Error","Erro de chamada RPC"}. -{"Running Nodes","Nos em execução"}. -{"~s access rule configuration","Configuração da Regra de Acesso ~s"}. +{"Running Nodes","Nós em execução"}. +{"~s invites you to the room ~s","~s convidaram você para a sala ~s"}. {"Saturday","Sábado"}. -{"Script check","Verificação de Script"}. +{"Search from the date","Pesquise a partir da data"}. {"Search Results for ","Resultados de pesquisa para "}. +{"Search the text","Pesquise o texto"}. +{"Search until the date","Pesquise até a data"}. {"Search users in ","Procurar usuários em "}. -{"Send announcement to all online users","Enviar anúncio a todos os usuárions online"}. {"Send announcement to all online users on all hosts","Enviar anúncio a todos usuários online em todas as máquinas"}. -{"Send announcement to all users","Enviar anúncio a todos os usuários"}. +{"Send announcement to all online users","Enviar anúncio a todos os usuárions online"}. {"Send announcement to all users on all hosts","Enviar aviso para todos os usuários em todos os hosts"}. +{"Send announcement to all users","Enviar anúncio a todos os usuários"}. {"September","Setembro"}. -{"Server ~b","Servidor ~b"}. +{"Server:","Servidor:"}. +{"Service list retrieval timed out","A recuperação da lista dos serviços expirou"}. +{"Session state copying timed out","A cópia do estado da sessão expirou"}. {"Set message of the day and send to online users","Definir mensagem do dia e enviar a todos usuários online"}. {"Set message of the day on all hosts and send to online users","Definir mensagem do dia em todos os hosts e enviar para os usuários online"}. {"Shared Roster Groups","Grupos Shared Roster"}. {"Show Integral Table","Mostrar Tabela Integral"}. +{"Show Occupants Join/Leave","Mostrar a entrada e a saída de ocupantes"}. {"Show Ordinary Table","Mostrar Tabela Ordinária"}. {"Shut Down Service","Parar Serviço"}. -{"~s invites you to the room ~s","~s convidou você para a sala ~s"}. -{"Some Jabber clients can store your password in your computer. Use that feature only if you trust your computer is safe.","Alguns clientes jabber podem salvar a sua senha no seu computador. Use recurso somente se você considera este computador seguro o suficiente."}. +{"SOCKS5 Bytestreams","Bytestreams SOCKS5"}. +{"Some XMPP clients can store your password in the computer, but you should do this only in your personal computer for safety reasons.","Alguns clientes XMPP podem armazenar a sua senha no seu computador, só faça isso no seu computador particular por questões de segurança."}. +{"Sources Specs:","Especificações das fontes:"}. {"Specify the access model","Especificar os modelos de acesso"}. {"Specify the event message type","Especificar o tipo de mensagem para o evento"}. {"Specify the publisher model","Especificar o modelo do publicante"}. -{"~s's Offline Messages Queue","~s's Fila de Mensagens Offline"}. -{"Start","Iniciar"}. -{"Start Modules at ","Iniciar módulos em "}. -{"Start Modules","Iniciar módulos"}. -{"Statistics","Estatísticas"}. -{"Statistics of ~p","Estatísticas de ~p"}. -{"Stop Modules at ","Parar módulos em "}. -{"Stop Modules","Parar módulos"}. -{"Stop","Parar"}. -{"Stopped Nodes","Nos parados"}. -{"Storage Type","Tipo de armazenamento"}. +{"Stanza id is not valid","A Stanza ID não é válido"}. +{"Stanza ID","ID da estrofe"}. +{"Statically specify a replyto of the node owner(s)","Defina uma resposta fixa do(s) proprietário(s) do nó"}. +{"Stopped Nodes","Nós parados"}. {"Store binary backup:","Armazenar backup binário:"}. {"Store plain text backup:","Armazenar backup em texto:"}. +{"Stream management is already enabled","A gestão do fluxo já está ativada"}. +{"Stream management is not enabled","O gerenciamento do fluxo não está ativado"}. {"Subject","Assunto"}. -{"Submit","Enviar"}. {"Submitted","Submetido"}. {"Subscriber Address","Endereço dos Assinantes"}. -{"Subscription","Subscrição"}. +{"Subscribers may publish","Os assinantes podem publicar"}. +{"Subscription requests must be approved and only subscribers may retrieve items","Os pedidos de assinatura devem ser aprovados e apenas os assinantes podem recuperar os itens"}. +{"Subscriptions are not allowed","Subscrições não estão permitidas"}. {"Sunday","Domingo"}. +{"Text associated with a picture","Um texto associado a uma imagem"}. +{"Text associated with a sound","Um texto associado a um som"}. +{"Text associated with a video","Um texto associado a um vídeo"}. +{"Text associated with speech","Um texto associado à fala"}. {"That nickname is already in use by another occupant","O apelido (nick) já está sendo utilizado"}. -{"That nickname is registered by another person","O nick já está registrado por outra pessoa"}. +{"That nickname is registered by another person","O apelido já está registrado por outra pessoa"}. +{"The account already exists","A conta já existe"}. +{"The account was not unregistered","A conta não estava não registrada"}. +{"The body text of the last received message","O corpo do texto da última mensagem que foi recebida"}. {"The CAPTCHA is valid.","O CAPTCHA é inválido."}. {"The CAPTCHA verification has failed","A verificação do CAPTCHA falhou"}. +{"The captcha you entered is wrong","O captcha que você digitou está errado"}. +{"The child nodes (leaf or collection) associated with a collection","Os nós relacionados (página ou coleção) associados com uma coleção"}. {"The collections with which a node is affiliated","As coleções com as quais o nó está relacionado"}. +{"The DateTime at which a leased subscription will end or has ended","A data e a hora que uma assinatura alugada terminará ou terá terminado"}. +{"The datetime when the node was created","A data em que o nó foi criado"}. +{"The default language of the node","O idioma padrão do nó"}. +{"The feature requested is not supported by the conference","A funcionalidade solicitada não é suportada pela sala de conferência"}. +{"The JID of the node creator","O JID do criador do nó"}. +{"The JIDs of those to contact with questions","Os JIDs daqueles para entrar em contato com perguntas"}. +{"The JIDs of those with an affiliation of owner","Os JIDs daqueles com uma afiliação de proprietário"}. +{"The JIDs of those with an affiliation of publisher","Os JIDs daqueles com uma afiliação de editor"}. +{"The list of all online users","A lista de todos os usuários online"}. +{"The list of all users","A lista de todos os usuários"}. +{"The list of JIDs that may associate leaf nodes with a collection","A lista dos JIDs que podem associar as páginas dos nós em uma coleção"}. +{"The maximum number of child nodes that can be associated with a collection, or `max` for no specific limit other than a server imposed maximum","A quantidade máxima de nós relacionados que podem ser associados a uma coleção ou `máximo` para nenhum limite específico que não seja um servidor imposto no máximo"}. +{"The minimum number of milliseconds between sending any two notification digests","O número mínimo de milissegundos entre o envio do resumo das duas notificações"}. +{"The name of the node","O nome do nó"}. +{"The node is a collection node","O nó é um nó da coleção"}. +{"The node is a leaf node (default)","O nó é uma página do nó (padrão)"}. +{"The NodeID of the relevant node","O NodeID do nó relevante"}. +{"The number of pending incoming presence subscription requests","A quantidade pendente dos pedidos da presença da assinatura"}. +{"The number of subscribers to the node","A quantidade dos assinantes para o nó"}. +{"The number of unread or undelivered messages","A quantidade das mensagens que não foram lidas ou não foram entregues"}. +{"The password contains unacceptable characters","A senha contém caracteres proibidos"}. +{"The password is too weak","Senha considerada muito fraca"}. {"the password is","a senha é"}. -{"The password of your Jabber account was successfully changed.","A senha da sua conta Jabber foi mudada com sucesso."}. -{"There was an error changing the password: ","Houveram erros ao mudar a senha: "}. -{"There was an error creating the account: ","Houveram erras ao criar esta conta: "}. -{"There was an error deleting the account: ","Erro ao deletar esta conta: "}. -{"This is case insensitive: macbeth is the same that MacBeth and Macbeth.","Não é 'case insensitive': macbeth é o mesmo que MacBeth e ainda Macbeth. "}. -{"This page allows to create a Jabber account in this Jabber server. Your JID (Jabber IDentifier) will be of the form: username@server. Please read carefully the instructions to fill correctly the fields.","Esta pagina aceita criações de novas contas Jabber neste servidor. A sua JID (Identificador Jabber) será da seguinte forma: 'usuário@servidor'. Por favor, leia cuidadosamente as instruções para preencher corretamente os campos."}. -{"This page allows to unregister a Jabber account in this Jabber server.","Esta página aceita para deletar uma conta Jabber neste servidor."}. -{"This participant is kicked from the room because he sent an error message","Este participante foi desconectado da sala de chat por ter enviado uma mensagem de erro."}. -{"This participant is kicked from the room because he sent an error message to another participant","Este participante foi desconectado da sala de chat por ter enviado uma mensagem de erro para outro usuário."}. -{"This participant is kicked from the room because he sent an error presence","Este participante foi desconectado da sala de chat por ter enviado uma notificação errônea de presença."}. +{"The password of your XMPP account was successfully changed.","A senha da sua conta XMPP foi alterada com sucesso."}. +{"The password was not changed","A senha não foi alterada"}. +{"The passwords are different","As senhas não batem"}. +{"The presence states for which an entity wants to receive notifications","As condições da presença para os quais uma entidade queira receber as notificações"}. +{"The query is only allowed from local users","Esta consulta só é permitida a partir de usuários locais"}. +{"The query must not contain elements","A consulta não pode conter elementos "}. +{"The room subject can be modified by participants","O tema da sala pode ser alterada pelos próprios participantes"}. +{"The semantic type information of data in the node, usually specified by the namespace of the payload (if any)","Informações de tipo semântico dos dados no nó, geralmente especificadas pelo espaço de nomes da carga útil (se houver)"}. +{"The sender of the last received message","O remetente da última mensagem que foi recebida"}. +{"The stanza MUST contain only one element, one element, or one element","A instância DEVE conter apenas um elemento , um elemento , ou um elemento "}. +{"The subscription identifier associated with the subscription request","O identificador da assinatura associado à solicitação da assinatura"}. +{"The URL of an XSL transformation which can be applied to payloads in order to generate an appropriate message body element.","O URL da transformação XSL que pode ser aplicada nas cargas úteis para gerar um elemento apropriado no corpo da mensagem."}. +{"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","A URL de uma transformação XSL que pode ser aplicada ao formato de carga útil para gerar um Formulário de Dados válido onde o cliente possa exibir usando um mecanismo genérico de renderização do Formulários de Dados"}. +{"There was an error changing the password: ","Houve um erro ao alterar a senha: "}. +{"There was an error creating the account: ","Houve um erro ao criar esta conta: "}. +{"There was an error deleting the account: ","Houve um erro ao deletar esta conta: "}. +{"This is case insensitive: macbeth is the same that MacBeth and Macbeth.","O tamanho da caixa não importa: macbeth é o mesmo que MacBeth e 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.","Esta pagina permite a criação de novas contas XMPP neste servidor. O seu JID (Identificador Jabber) será da seguinte maneira: usuário@servidor. Por favor, leia cuidadosamente as instruções para preencher todos os campos corretamente."}. +{"This page allows to unregister an XMPP account in this XMPP server.","Esta página permite a exclusão de uma conta XMPP neste servidor."}. {"This room is not anonymous","Essa sala não é anônima"}. +{"This service can not process the address: ~s","Este serviço não pode processar o endereço: ~s"}. {"Thursday","Quinta"}. {"Time delay","Intervalo (Tempo)"}. -{"Time","Fecha"}. +{"Timed out waiting for stream resumption","Tempo limite expirou durante à espera da retomada da transmissão"}. +{"To register, visit ~s","Para registrar, visite ~s"}. +{"To ~ts","Para ~s"}. +{"Token TTL","Token TTL"}. +{"Too many active bytestreams","Quantidade excessiva de bytestreams ativos"}. {"Too many CAPTCHA requests","Número excessivo de requisições para o CAPTCHA"}. -{"To","Para"}. -{"To ~s","Para ~s"}. +{"Too many child elements","Quantidade excessiva de elementos filho"}. +{"Too many elements","Número excessivo de elementos "}. +{"Too many elements","Número excessivo de elementos "}. +{"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","Número excessivo (~p) de tentativas falhas de autenticação (~s). O endereço será desbloqueado às ~s UTC"}. +{"Too many receiver fields were specified","Foram definidos receptores demais nos campos"}. +{"Too many unacked stanzas","Número excessivo de instâncias sem confirmação"}. +{"Too many users in this conference","Há uma quantidade excessiva de usuários nesta conferência"}. {"Traffic rate limit is exceeded","Limite de banda excedido"}. -{"Transactions Aborted:","Transações abortadas:"}. -{"Transactions Committed:","Transações salvas:"}. -{"Transactions Logged:","Transações de log:"}. -{"Transactions Restarted:","Transações reiniciadas:"}. +{"~ts's MAM Archive","Arquivo ~ts's MAM"}. +{"~ts's Offline Messages Queue","~s's Fila de Mensagens Offline"}. {"Tuesday","Terça"}. {"Unable to generate a CAPTCHA","Impossível gerar um CAPTCHA"}. +{"Unable to register route on existing local domain","Não foi possível registrar rota no domínio local existente"}. {"Unauthorized","Não Autorizado"}. -{"Unregister a Jabber account","Deletar conta Jabber"}. +{"Unexpected action","Ação inesperada"}. +{"Unexpected error condition: ~p","Condição de erro inesperada: ~p"}. +{"Uninstall","Desinstalar"}. +{"Unregister an XMPP account","Excluir uma conta XMPP"}. {"Unregister","Deletar registro"}. -{"Update ","Atualizar "}. -{"Update","Atualizar"}. +{"Unsupported element","Elemento não suportado"}. +{"Unsupported version","Versão sem suporte"}. {"Update message of the day (don't send)","Atualizar mensagem do dia (não enviar)"}. {"Update message of the day on all hosts (don't send)","Atualizar a mensagem do dia em todos os host (não enviar)"}. -{"Update plan","Plano de Atualização"}. -{"Update script","Script de atualização"}. -{"Uptime:","Uptime:"}. -{"Use of STARTTLS required","É obrigatório uso de STARTTLS"}. +{"Update specs to get modules source, then install desired ones.","Atualize as especificações para obter a fonte dos módulos e instale os que desejar."}. +{"Update Specs","Atualizar as especificações"}. +{"Updating the vCard is not supported by the vCard storage backend","A atualização do vCard não é compatível com o back-end de armazenamento do vCard"}. +{"Upgrade","Atualização"}. +{"URL for Archived Discussion Logs","A URL para o arquivamento dos registros da discussão"}. +{"User already exists","Usuário já existe"}. +{"User (jid)","Usuário (jid)"}. +{"User JID","Usuário JID"}. {"User Management","Gerenciamento de Usuários"}. +{"User not allowed to perform an IQ set on another user's vCard.","O usuário não tem permissão para executar um conjunto de QI no vCard de outro usuário."}. +{"User removed","O usuário foi removido"}. +{"User session not found","A sessão do usuário não foi encontrada"}. +{"User session terminated","Sessão de usuário terminada"}. +{"User ~ts","Usuário ~s"}. +{"Username:","Usuário:"}. {"Users are not allowed to register accounts so quickly","Usuários não estão autorizados a registrar contas imediatamente"}. -{"Users Last Activity","Ultimas atividades dos usuários"}. +{"Users Last Activity","Últimas atividades dos usuários"}. {"Users","Usuários"}. -{"User ","Usuário "}. {"User","Usuário"}. -{"Validate","Validar"}. +{"Value 'get' of 'type' attribute is not allowed","Valor 'get' não permitido para atributo 'type'"}. +{"Value of '~s' should be boolean","Value de '~s' deveria ser um booleano"}. +{"Value of '~s' should be datetime string","Valor de '~s' deveria ser data e hora"}. +{"Value of '~s' should be integer","Valor de '~s' deveria ser um inteiro"}. +{"Value 'set' of 'type' attribute is not allowed","Valor 'set' não permitido para atributo 'type'"}. {"vCard User Search","Busca de Usuário vCard"}. +{"View joined MIX channels","Exibir os canais MIX aderidos"}. {"Virtual Hosts","Hosts virtuais"}. -{"Visitors are not allowed to change their nicknames in this room","Nesta sala, os visitantes não pode mudar seus apelidos"}. +{"Visitors are not allowed to change their nicknames in this room","Nesta sala, os visitantes não podem mudar seus apelidos"}. {"Visitors are not allowed to send messages to all occupants","Os visitantes não podem enviar mensagens a todos os ocupantes"}. +{"Visitor","Visitante"}. {"Voice request","Requisição de voz"}. -{"Voice requests are disabled in this conference","Requisições de voz estào desabilitadas nesta conferência"}. +{"Voice requests are disabled in this conference","Requisições de voz estão desabilitadas nesta sala de conferência"}. +{"Web client which allows to join the room anonymously","Cliente da web que permite entrar na sala de forma anônima"}. {"Wednesday","Quarta"}. +{"When a new subscription is processed and whenever a subscriber comes online","Quando uma nova assinatura é processada e sempre que um assinante fica online"}. +{"When a new subscription is processed","Quando uma nova assinatura é processada"}. {"When to send the last published item","Quando enviar o último tópico publicado"}. +{"Whether an entity wants to receive an XMPP message body in addition to the payload format","Caso uma entidade queira receber o corpo de uma mensagem XMPP além do formato de carga útil"}. +{"Whether an entity wants to receive digests (aggregations) of notifications or all notifications individually","Caso uma entidade queira receber os resumos (as agregações) das notificações ou todas as notificações individualmente"}. +{"Whether an entity wants to receive or disable notifications","Caso uma entidade queira receber ou desativar as notificações"}. +{"Whether owners or publisher should receive replies to items","Caso os proprietários ou a editora devam receber as respostas nos itens"}. +{"Whether the node is a leaf (default) or a collection","Caso o nó seja uma folha (padrão) ou uma coleção"}. {"Whether to allow subscriptions","Permitir subscrições"}. -{"You can later change your password using a Jabber client.","Mais tarde você pode alterar a sua senha usando um cliente Jabber."}. -{"You have been banned from this room","As sido bloqueado em esta sala"}. +{"Whether to make all subscriptions temporary, based on subscriber presence","Caso todas as assinaturas devam ser temporárias, com base na presença do assinante"}. +{"Whether to notify owners about new subscribers and unsubscribes","Caso deva notificar os proprietários sobre os novos assinantes e aqueles que cancelaram a assinatura"}. +{"Who can send private messages","Quem pode enviar mensagens privadas"}. +{"Who may associate leaf nodes with a collection","Quem pode associar as folhas dos nós em uma coleção"}. +{"Wrong parameters in the web formulary","O formulário web está com os parâmetros errados"}. +{"Wrong xmlns","Xmlns errado"}. +{"XMPP Account Registration","Registo da Conta XMPP"}. +{"XMPP Domains","Domínios XMPP"}. +{"XMPP Show Value of Away","XMPP Exiba o valor da ausência"}. +{"XMPP Show Value of Chat","XMPP Exiba o valor do chat"}. +{"XMPP Show Value of DND (Do Not Disturb)","XMPP Exiba o valor do DND (Não Perturbe)"}. +{"XMPP Show Value of XA (Extended Away)","XMPP Exiba o valor do XA (Ausência Estendida)"}. +{"XMPP URI of Associated Publish-Subscribe Node","XMPP URI da publicação do nó associado da assinatura"}. +{"You are being removed from the room because of a system shutdown","Você está sendo removido da sala por causa do desligamento do sistema"}. +{"You are not allowed to send private messages","Você não tem permissão para enviar mensagens privadas"}. +{"You are not joined to the channel","Você não está inscrito no canal"}. +{"You can later change your password using an XMPP client.","Você pode alterar a sua senha mais tarde usando um cliente XMPP."}. +{"You have been banned from this room","Você foi banido desta sala"}. +{"You have joined too many conferences","Você entrou em um número excessivo de salas de conferência"}. {"You must fill in field \"Nickname\" in the form","Você deve completar o campo \"Apelido\" no formulário"}. -{"You need an x:data capable client to configure mod_irc settings","Necessitas um cliente com suporte de x:data para configurar las opções de mod_irc"}. -{"You need an x:data capable client to configure room","Necessitas um cliente com suporte de x:data para configurar la sala"}. +{"You need a client that supports x:data and CAPTCHA to register","Você precisa de um cliente com suporte de x:data para poder registrar o apelido"}. +{"You need a client that supports x:data to register the nickname","Você precisa de um cliente com suporte a x:data para registrar o seu apelido"}. {"You need an x:data capable client to search","Necessitas um cliente com suporte de x:data para poder buscar"}. -{"Your active privacy list has denied the routing of this stanza.","Sua lista de privacidade ativa negou o roteamento deste."}. -{"Your contact offline message queue is full. The message has been discarded.","Sua fila de mensagens offline esta cheia. Sua mensagem foi descartada"}. -{"Your Jabber account was successfully created.","Sua conta jabber foi criada corretamente."}. -{"Your Jabber account was successfully deleted.","Sua conta Jabber foi deletada com sucesso."}. -{"Your messages to ~s are being blocked. To unblock them, visit ~s","Suas mensagens para ~s estão bloqueadas. Para desbloquea-las, visite: ~s"}. +{"Your active privacy list has denied the routing of this stanza.","Sua lista de privacidade ativa negou o roteamento desta instância."}. +{"Your contact offline message queue is full. The message has been discarded.","A fila de contatos offline esta cheia. A sua mensagem foi descartada."}. +{"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","Suas mensagens para ~s estão bloqueadas. Para desbloqueá-las, visite: ~s"}. +{"Your XMPP account was successfully registered.","A sua conta XMPP foi registrada com sucesso."}. +{"Your XMPP account was successfully unregistered.","Sua conta XMPP foi excluída com sucesso."}. +{"You're not allowed to create nodes","Você não tem autorização para criar nós"}. diff --git a/priv/msgs/pt-br.po b/priv/msgs/pt-br.po deleted file mode 100644 index 674d92c92..000000000 --- a/priv/msgs/pt-br.po +++ /dev/null @@ -1,1874 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: 2.1.0-alpha\n" -"Last-Translator: Otávio Fernandes\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Language: Portuguese (Brazil)\n" -"X-Additional-Translator: Renato Botelho\n" -"X-Additional-Translator: Lucius Curado\n" -"X-Additional-Translator: Felipe Brito Vasconcellos\n" -"X-Additional-Translator: Victor Hugo dos Santos\n" - -#: ejabberd_c2s.erl:424 ejabberd_c2s.erl:727 -msgid "Use of STARTTLS required" -msgstr "É obrigatório uso de STARTTLS" - -#: ejabberd_c2s.erl:503 -msgid "No resource provided" -msgstr "Nenhum recurso foi informado" - -#: ejabberd_c2s.erl:1197 -msgid "Replaced by new connection" -msgstr "Substituído por nova conexão" - -#: ejabberd_c2s.erl:1885 -msgid "Your active privacy list has denied the routing of this stanza." -msgstr "Sua lista de privacidade ativa negou o roteamento deste." - -#: ejabberd_captcha.erl:96 ejabberd_captcha.erl:152 ejabberd_captcha.erl:178 -msgid "Enter the text you see" -msgstr "Insira o texto que você vê" - -#: ejabberd_captcha.erl:101 -msgid "Your messages to ~s are being blocked. To unblock them, visit ~s" -msgstr "" -"Suas mensagens para ~s estão bloqueadas. Para desbloquea-las, visite: ~s" - -#: ejabberd_captcha.erl:134 -msgid "If you don't see the CAPTCHA image here, visit the web page." -msgstr "" - -#: ejabberd_captcha.erl:146 -msgid "CAPTCHA web page" -msgstr "" - -#: ejabberd_captcha.erl:307 -msgid "The CAPTCHA is valid." -msgstr "O CAPTCHA é inválido." - -#: mod_adhoc.erl:95 mod_adhoc.erl:125 mod_adhoc.erl:143 mod_adhoc.erl:161 -msgid "Commands" -msgstr "Comandos" - -#: mod_adhoc.erl:149 mod_adhoc.erl:243 -msgid "Ping" -msgstr "Ping" - -#: mod_adhoc.erl:260 -msgid "Pong" -msgstr "Pong" - -#: mod_announce.erl:507 mod_announce_odbc.erl:499 -msgid "Really delete message of the day?" -msgstr "Deletar realmente a mensagem do dia?" - -#: mod_announce.erl:515 mod_announce_odbc.erl:507 mod_configure.erl:1083 -#: mod_configure.erl:1128 -msgid "Subject" -msgstr "Assunto" - -#: mod_announce.erl:520 mod_announce_odbc.erl:512 mod_configure.erl:1088 -#: mod_configure.erl:1133 -msgid "Message body" -msgstr "Corpo da mensagem" - -#: mod_announce.erl:600 mod_announce_odbc.erl:592 -msgid "No body provided for announce message" -msgstr "Nenhum corpo de texto fornecido para anunciar mensagem" - -#: mod_announce.erl:635 mod_announce_odbc.erl:627 -msgid "Announcements" -msgstr "Anúncios" - -#: mod_announce.erl:637 mod_announce_odbc.erl:629 -msgid "Send announcement to all users" -msgstr "Enviar anúncio a todos os usuários" - -#: mod_announce.erl:639 mod_announce_odbc.erl:631 -msgid "Send announcement to all users on all hosts" -msgstr "Enviar aviso para todos os usuários em todos os hosts" - -#: mod_announce.erl:641 mod_announce_odbc.erl:633 -msgid "Send announcement to all online users" -msgstr "Enviar anúncio a todos os usuárions online" - -#: mod_announce.erl:643 mod_announce_odbc.erl:635 mod_configure.erl:1078 -#: mod_configure.erl:1123 -msgid "Send announcement to all online users on all hosts" -msgstr "Enviar anúncio a todos usuários online em todas as máquinas" - -#: mod_announce.erl:645 mod_announce_odbc.erl:637 -msgid "Set message of the day and send to online users" -msgstr "Definir mensagem do dia e enviar a todos usuários online" - -#: mod_announce.erl:647 mod_announce_odbc.erl:639 -msgid "Set message of the day on all hosts and send to online users" -msgstr "" -"Definir mensagem do dia em todos os hosts e enviar para os usuários online" - -#: mod_announce.erl:649 mod_announce_odbc.erl:641 -msgid "Update message of the day (don't send)" -msgstr "Atualizar mensagem do dia (não enviar)" - -#: mod_announce.erl:651 mod_announce_odbc.erl:643 -msgid "Update message of the day on all hosts (don't send)" -msgstr "Atualizar a mensagem do dia em todos os host (não enviar)" - -#: mod_announce.erl:653 mod_announce_odbc.erl:645 -msgid "Delete message of the day" -msgstr "Apagar mensagem do dia" - -#: mod_announce.erl:655 mod_announce_odbc.erl:647 -msgid "Delete message of the day on all hosts" -msgstr "Apagar a mensagem do dia em todos os hosts" - -#: mod_configure.erl:114 mod_configure.erl:274 mod_configure.erl:296 -#: mod_configure.erl:498 -msgid "Configuration" -msgstr "Configuração" - -#: mod_configure.erl:125 mod_configure.erl:576 web/ejabberd_web_admin.erl:1936 -msgid "Database" -msgstr "Base de dados" - -#: mod_configure.erl:127 mod_configure.erl:595 -msgid "Start Modules" -msgstr "Iniciar módulos" - -#: mod_configure.erl:129 mod_configure.erl:596 -msgid "Stop Modules" -msgstr "Parar módulos" - -#: mod_configure.erl:131 mod_configure.erl:604 web/ejabberd_web_admin.erl:1937 -msgid "Backup" -msgstr "Salvar cópia de segurança" - -#: mod_configure.erl:133 mod_configure.erl:605 -msgid "Restore" -msgstr "Restaurar" - -#: mod_configure.erl:135 mod_configure.erl:606 -msgid "Dump to Text File" -msgstr "Exportar para arquivo de texto" - -#: mod_configure.erl:137 mod_configure.erl:615 -msgid "Import File" -msgstr "Importar arquivo" - -#: mod_configure.erl:139 mod_configure.erl:616 -msgid "Import Directory" -msgstr "Importar diretório" - -#: mod_configure.erl:141 mod_configure.erl:581 mod_configure.erl:1057 -msgid "Restart Service" -msgstr "Reiniciar Serviço" - -#: mod_configure.erl:143 mod_configure.erl:582 mod_configure.erl:1102 -msgid "Shut Down Service" -msgstr "Parar Serviço" - -#: mod_configure.erl:145 mod_configure.erl:518 mod_configure.erl:1197 -#: web/ejabberd_web_admin.erl:1527 -msgid "Add User" -msgstr "Adicionar usuário" - -#: mod_configure.erl:147 mod_configure.erl:519 mod_configure.erl:1219 -msgid "Delete User" -msgstr "Deletar Usuário" - -#: mod_configure.erl:149 mod_configure.erl:520 mod_configure.erl:1231 -msgid "End User Session" -msgstr "Terminar Sessão do Usuário" - -#: mod_configure.erl:151 mod_configure.erl:521 mod_configure.erl:1243 -#: mod_configure.erl:1255 -msgid "Get User Password" -msgstr "Obter Senha do Usuário" - -#: mod_configure.erl:153 mod_configure.erl:522 -msgid "Change User Password" -msgstr "Alterar Senha do Usuário" - -#: mod_configure.erl:155 mod_configure.erl:523 mod_configure.erl:1272 -msgid "Get User Last Login Time" -msgstr "Obter a Data do Último Login" - -#: mod_configure.erl:157 mod_configure.erl:524 mod_configure.erl:1284 -msgid "Get User Statistics" -msgstr "Obter Estatísticas do Usuário" - -#: mod_configure.erl:159 mod_configure.erl:525 -msgid "Get Number of Registered Users" -msgstr "Obter Número de Usuários Registrados" - -#: mod_configure.erl:161 mod_configure.erl:526 -msgid "Get Number of Online Users" -msgstr "Obter Número de Usuários Online" - -#: mod_configure.erl:163 mod_configure.erl:509 web/ejabberd_web_admin.erl:831 -#: web/ejabberd_web_admin.erl:872 -msgid "Access Control Lists" -msgstr "Listas de Controle de Acesso" - -#: mod_configure.erl:165 mod_configure.erl:510 web/ejabberd_web_admin.erl:940 -#: web/ejabberd_web_admin.erl:976 -msgid "Access Rules" -msgstr "Regras de Acesso" - -#: mod_configure.erl:297 mod_configure.erl:499 -msgid "User Management" -msgstr "Gerenciamento de Usuários" - -#: mod_configure.erl:500 web/ejabberd_web_admin.erl:1058 -#: web/ejabberd_web_admin.erl:1462 -msgid "Online Users" -msgstr "Usuários conectados" - -#: mod_configure.erl:501 -msgid "All Users" -msgstr "Todos os usuários" - -#: mod_configure.erl:502 -msgid "Outgoing s2s Connections" -msgstr "Conexões que partam de s2s" - -#: mod_configure.erl:503 web/ejabberd_web_admin.erl:1907 -msgid "Running Nodes" -msgstr "Nos em execução" - -#: mod_configure.erl:504 web/ejabberd_web_admin.erl:1909 -msgid "Stopped Nodes" -msgstr "Nos parados" - -#: mod_configure.erl:577 mod_configure.erl:587 web/ejabberd_web_admin.erl:1953 -msgid "Modules" -msgstr "Módulos" - -#: mod_configure.erl:578 -msgid "Backup Management" -msgstr "Gestão de Backup" - -#: mod_configure.erl:579 -msgid "Import Users From jabberd14 Spool Files" -msgstr "Importar usuários de arquivos jabberd14" - -#: mod_configure.erl:699 -msgid "To ~s" -msgstr "Para ~s" - -#: mod_configure.erl:717 -msgid "From ~s" -msgstr "De ~s" - -#: mod_configure.erl:913 -msgid "Database Tables Configuration at " -msgstr "Configuração de Tabelas de Base de dados em " - -#: mod_configure.erl:918 -msgid "Choose storage type of tables" -msgstr "Selecione o tipo de armazenamento das tabelas" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Disc only copy" -msgstr "Somente copia em disco" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM and disc copy" -msgstr "Copias na RAM e disco rígido" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM copy" -msgstr "Copia em RAM" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Remote copy" -msgstr "Copia remota" - -#: mod_configure.erl:950 -msgid "Stop Modules at " -msgstr "Parar módulos em " - -#: mod_configure.erl:954 -msgid "Choose modules to stop" -msgstr "Selecione módulos a parar" - -#: mod_configure.erl:969 -msgid "Start Modules at " -msgstr "Iniciar módulos em " - -#: mod_configure.erl:973 -msgid "Enter list of {Module, [Options]}" -msgstr "Introduza lista de {módulo, [opções]}" - -#: mod_configure.erl:974 -msgid "List of modules to start" -msgstr "Listas de módulos para inicializar" - -#: mod_configure.erl:983 -msgid "Backup to File at " -msgstr "Salvar backup para arquivo em " - -#: mod_configure.erl:987 mod_configure.erl:1001 -msgid "Enter path to backup file" -msgstr "Introduza o caminho do arquivo de backup" - -#: mod_configure.erl:988 mod_configure.erl:1002 mod_configure.erl:1016 -#: mod_configure.erl:1030 -msgid "Path to File" -msgstr "Caminho do arquivo" - -#: mod_configure.erl:997 -msgid "Restore Backup from File at " -msgstr "Restaurar backup a partir do arquivo em " - -#: mod_configure.erl:1011 -msgid "Dump Backup to Text File at " -msgstr "Exportar backup para texto em " - -#: mod_configure.erl:1015 -msgid "Enter path to text file" -msgstr "Introduza caminho para o arquivo texto" - -#: mod_configure.erl:1025 -msgid "Import User from File at " -msgstr "Importar usuário a partir do arquivo em " - -#: mod_configure.erl:1029 -msgid "Enter path to jabberd14 spool file" -msgstr "Insira o caminho para a fila (arquivo) do jabberd14" - -#: mod_configure.erl:1039 -msgid "Import Users from Dir at " -msgstr "Importar usuários a partir do diretório em " - -#: mod_configure.erl:1043 -msgid "Enter path to jabberd14 spool dir" -msgstr "Introduza o caminho para o diretório de fila do jabberd14" - -#: mod_configure.erl:1044 -msgid "Path to Dir" -msgstr "Caminho para o diretório" - -#: mod_configure.erl:1060 mod_configure.erl:1105 -msgid "Time delay" -msgstr "Intervalo (Tempo)" - -#: mod_configure.erl:1143 -msgid "Access Control List Configuration" -msgstr "Configuração da Lista de Controle de Acesso" - -#: mod_configure.erl:1147 -msgid "Access control lists" -msgstr "Listas de Controle de Acesso" - -#: mod_configure.erl:1171 -msgid "Access Configuration" -msgstr "Configuração de Acesso" - -#: mod_configure.erl:1175 -msgid "Access rules" -msgstr "Regras de acesso" - -#: mod_configure.erl:1200 mod_configure.erl:1222 mod_configure.erl:1234 -#: mod_configure.erl:1246 mod_configure.erl:1258 mod_configure.erl:1275 -#: mod_configure.erl:1287 mod_configure.erl:1650 mod_configure.erl:1700 -#: mod_configure.erl:1721 mod_roster.erl:943 mod_roster_odbc.erl:1060 -#: mod_vcard.erl:472 mod_vcard_ldap.erl:554 mod_vcard_odbc.erl:448 -msgid "Jabber ID" -msgstr "ID Jabber" - -#: mod_configure.erl:1205 mod_configure.erl:1263 mod_configure.erl:1651 -#: mod_configure.erl:1863 mod_muc/mod_muc_room.erl:3224 mod_register.erl:235 -#: web/ejabberd_web_admin.erl:1520 -msgid "Password" -msgstr "Senha" - -#: mod_configure.erl:1210 -msgid "Password Verification" -msgstr "Verificação de Senha" - -#: mod_configure.erl:1301 -msgid "Number of registered users" -msgstr "Número de usuários registrados" - -#: mod_configure.erl:1315 -msgid "Number of online users" -msgstr "Número de usuários online" - -#: mod_configure.erl:1682 web/ejabberd_web_admin.erl:1588 -#: web/ejabberd_web_admin.erl:1743 -msgid "Never" -msgstr "Nunca" - -#: mod_configure.erl:1696 web/ejabberd_web_admin.erl:1601 -#: web/ejabberd_web_admin.erl:1756 -msgid "Online" -msgstr "Conectado" - -#: mod_configure.erl:1701 -msgid "Last login" -msgstr "Último login" - -#: mod_configure.erl:1722 -msgid "Roster size" -msgstr "Tamanho da Lista" - -#: mod_configure.erl:1723 -msgid "IP addresses" -msgstr "Endereços IP" - -#: mod_configure.erl:1724 -msgid "Resources" -msgstr "Recursos" - -#: mod_configure.erl:1850 -msgid "Administration of " -msgstr "Administração de " - -#: mod_configure.erl:1853 -msgid "Action on user" -msgstr "Ação no usuário" - -#: mod_configure.erl:1857 -msgid "Edit Properties" -msgstr "Editar propriedades" - -#: mod_configure.erl:1860 web/ejabberd_web_admin.erl:1769 -msgid "Remove User" -msgstr "Remover usuário" - -#: mod_irc/mod_irc.erl:201 mod_irc/mod_irc_odbc.erl:196 -#: mod_muc/mod_muc.erl:336 mod_muc/mod_muc_odbc.erl:342 -msgid "Access denied by service policy" -msgstr "Aceso denegado por la política do serviço" - -#: mod_irc/mod_irc.erl:401 mod_irc/mod_irc_odbc.erl:398 -msgid "IRC Transport" -msgstr "Transporte IRC" - -#: mod_irc/mod_irc.erl:428 mod_irc/mod_irc_odbc.erl:425 -msgid "ejabberd IRC module" -msgstr "Módulo de IRC para ejabberd" - -#: mod_irc/mod_irc.erl:559 mod_irc/mod_irc_odbc.erl:568 -msgid "You need an x:data capable client to configure mod_irc settings" -msgstr "" -"Necessitas um cliente com suporte de x:data para configurar las opções de " -"mod_irc" - -#: mod_irc/mod_irc.erl:566 mod_irc/mod_irc_odbc.erl:575 -msgid "Registration in mod_irc for " -msgstr "Registro em mod_irc para " - -#: mod_irc/mod_irc.erl:571 mod_irc/mod_irc_odbc.erl:580 -msgid "" -"Enter username, encodings, ports and passwords you wish to use for " -"connecting to IRC servers" -msgstr "" -"Insira o nome de usuário, codificações, portas e senhas que vocêdeseja para " -"usar nos servidores IRC" - -#: mod_irc/mod_irc.erl:576 mod_irc/mod_irc_odbc.erl:585 -msgid "IRC Username" -msgstr "Usuário IRC" - -#: mod_irc/mod_irc.erl:586 mod_irc/mod_irc_odbc.erl:595 -msgid "" -"If you want to specify different ports, passwords, encodings for IRC " -"servers, fill this list with values in format '{\"irc server\", \"encoding" -"\", port, \"password\"}'. By default this service use \"~s\" encoding, port " -"~p, empty password." -msgstr "" -"Se você deseja especificar portas diferentes, senhas ou codifações para " -"servidores de IRC, complete esta lista com os valores no formato: " -"'{\"servidor IRC\", \"codificação\", porta, \"senha\"}'. Por padrão, este " -"serviço usa a codificação \"~s\", porta \"~p\", e senha em branco (vazia)" - -#: mod_irc/mod_irc.erl:598 mod_irc/mod_irc_odbc.erl:607 -msgid "" -"Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef." -"net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]." -msgstr "" -"Exemplo: [{\"irc.teste.net\", \"koi8-r\"}, 6667, \"senha\"}, {\"dominio.foo." -"net\", \"iso8859-1\", 7000}, {\"irc.servidordeteste.net\", \"utf-8\"}]." - -#: mod_irc/mod_irc.erl:603 mod_irc/mod_irc_odbc.erl:612 -msgid "Connections parameters" -msgstr "Parâmetros para as Conexões" - -#: mod_irc/mod_irc.erl:728 mod_irc/mod_irc_odbc.erl:757 -msgid "Join IRC channel" -msgstr "Juntar-se ao canal IRC" - -#: mod_irc/mod_irc.erl:732 mod_irc/mod_irc_odbc.erl:761 -msgid "IRC channel (don't put the first #)" -msgstr "Canal IRC (não coloque o #)" - -#: mod_irc/mod_irc.erl:737 mod_irc/mod_irc_odbc.erl:766 -msgid "IRC server" -msgstr "Servidor IRC" - -#: mod_irc/mod_irc.erl:770 mod_irc/mod_irc.erl:774 -#: mod_irc/mod_irc_odbc.erl:799 mod_irc/mod_irc_odbc.erl:803 -msgid "Join the IRC channel here." -msgstr "Aqui! Juntar-se ao canal IRC." - -#: mod_irc/mod_irc.erl:778 mod_irc/mod_irc_odbc.erl:807 -msgid "Join the IRC channel in this Jabber ID: ~s" -msgstr "Entrar no canal IRC, neste ID Jabber: ~s" - -#: mod_irc/mod_irc.erl:863 mod_irc/mod_irc_odbc.erl:904 -msgid "IRC settings" -msgstr "Configurações do IRC" - -#: mod_irc/mod_irc.erl:868 mod_irc/mod_irc_odbc.erl:909 -msgid "" -"Enter username and encodings you wish to use for connecting to IRC servers. " -"Press 'Next' to get more fields to fill in. Press 'Complete' to save " -"settings." -msgstr "" -"Insira o nome de usuário e codificações que você deseja usar para conectar-" -"se aos servidores de IRC. Depois, presione 'Next' ('Próximo') para exibir " -"mais campos que devem ser preenchidos. Ao final, pressione " -"'Complete' ('Completar') para salvar a configuração." - -#: mod_irc/mod_irc.erl:874 mod_irc/mod_irc_odbc.erl:915 -msgid "IRC username" -msgstr "Usuário IRC" - -#: mod_irc/mod_irc.erl:923 mod_irc/mod_irc_odbc.erl:964 -msgid "Password ~b" -msgstr "Senha ~b" - -#: mod_irc/mod_irc.erl:928 mod_irc/mod_irc_odbc.erl:969 -msgid "Port ~b" -msgstr "Porta ~b" - -#: mod_irc/mod_irc.erl:933 mod_irc/mod_irc_odbc.erl:974 -msgid "Encoding for server ~b" -msgstr "Codificação para o servidor ~b" - -#: mod_irc/mod_irc.erl:942 mod_irc/mod_irc_odbc.erl:983 -msgid "Server ~b" -msgstr "Servidor ~b" - -#: mod_muc/mod_muc.erl:449 mod_muc/mod_muc_odbc.erl:456 -msgid "Only service administrators are allowed to send service messages" -msgstr "" -"Apenas administradores possuem permissão para enviar mensagens de serviço" - -#: mod_muc/mod_muc.erl:493 mod_muc/mod_muc_odbc.erl:500 -msgid "Room creation is denied by service policy" -msgstr "Se te a denegado criar la sala por política do serviço" - -#: mod_muc/mod_muc.erl:500 mod_muc/mod_muc_odbc.erl:507 -msgid "Conference room does not exist" -msgstr "La sala de conferencias não existe" - -#: mod_muc/mod_muc.erl:582 mod_muc/mod_muc_odbc.erl:595 -msgid "Chatrooms" -msgstr "Salas de Chat" - -#: mod_muc/mod_muc.erl:712 mod_muc/mod_muc_odbc.erl:729 -#, fuzzy -msgid "You need a client that supports x:data to register the nickname" -msgstr "" -"Você precisa de um cliente com suporte de x:data para poder registrar o nick" - -#: mod_muc/mod_muc.erl:718 mod_muc/mod_muc_odbc.erl:735 -msgid "Nickname Registration at " -msgstr "Registro do apelido em " - -#: mod_muc/mod_muc.erl:722 mod_muc/mod_muc_odbc.erl:739 -msgid "Enter nickname you want to register" -msgstr "Introduza o apelido que quer registrar" - -#: mod_muc/mod_muc.erl:723 mod_muc/mod_muc_odbc.erl:740 -#: mod_muc/mod_muc_room.erl:3810 mod_roster.erl:944 mod_roster_odbc.erl:1061 -#: mod_vcard.erl:364 mod_vcard.erl:477 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:453 -msgid "Nickname" -msgstr "Apelido" - -#: mod_muc/mod_muc.erl:762 mod_muc/mod_muc_odbc.erl:784 -#: mod_muc/mod_muc_room.erl:1064 mod_muc/mod_muc_room.erl:1787 -msgid "That nickname is registered by another person" -msgstr "O nick já está registrado por outra pessoa" - -#: mod_muc/mod_muc.erl:788 mod_muc/mod_muc_odbc.erl:811 -msgid "You must fill in field \"Nickname\" in the form" -msgstr "Você deve completar o campo \"Apelido\" no formulário" - -#: mod_muc/mod_muc.erl:808 mod_muc/mod_muc_odbc.erl:831 -msgid "ejabberd MUC module" -msgstr "Módulo de MUC para ejabberd" - -#: mod_muc/mod_muc_log.erl:374 mod_muc/mod_muc_log.erl:381 -msgid "Chatroom configuration modified" -msgstr "Configuração da sala de bate-papo modificada" - -#: mod_muc/mod_muc_log.erl:384 -msgid "joins the room" -msgstr "Entrar na sala" - -#: mod_muc/mod_muc_log.erl:387 mod_muc/mod_muc_log.erl:390 -msgid "leaves the room" -msgstr "Sair da sala" - -#: mod_muc/mod_muc_log.erl:393 mod_muc/mod_muc_log.erl:396 -msgid "has been banned" -msgstr "foi banido" - -#: mod_muc/mod_muc_log.erl:399 mod_muc/mod_muc_log.erl:402 -msgid "has been kicked" -msgstr "foi removido" - -#: mod_muc/mod_muc_log.erl:405 -msgid "has been kicked because of an affiliation change" -msgstr "foi desconectado porque por afiliação inválida" - -#: mod_muc/mod_muc_log.erl:408 -msgid "has been kicked because the room has been changed to members-only" -msgstr "" -"foi desconectado porque a política da sala mudou, só membros são permitidos" - -#: mod_muc/mod_muc_log.erl:411 -msgid "has been kicked because of a system shutdown" -msgstr "foi desconectado porque o sistema foi desligado" - -#: mod_muc/mod_muc_log.erl:414 -msgid "is now known as" -msgstr "é agora conhecido como" - -#: mod_muc/mod_muc_log.erl:417 mod_muc/mod_muc_log.erl:688 -#: mod_muc/mod_muc_room.erl:2393 -msgid " has set the subject to: " -msgstr " a posto o assunto: " - -#: mod_muc/mod_muc_log.erl:452 -msgid "Chatroom is created" -msgstr "A sala de chat está criada" - -#: mod_muc/mod_muc_log.erl:453 -msgid "Chatroom is destroyed" -msgstr "A sala de chat está destruída" - -#: mod_muc/mod_muc_log.erl:454 -msgid "Chatroom is started" -msgstr "A sala de chat está inciada" - -#: mod_muc/mod_muc_log.erl:455 -msgid "Chatroom is stopped" -msgstr "A sala de chat está parada" - -#: mod_muc/mod_muc_log.erl:459 -msgid "Monday" -msgstr "Segunda" - -#: mod_muc/mod_muc_log.erl:460 -msgid "Tuesday" -msgstr "Terça" - -#: mod_muc/mod_muc_log.erl:461 -msgid "Wednesday" -msgstr "Quarta" - -#: mod_muc/mod_muc_log.erl:462 -msgid "Thursday" -msgstr "Quinta" - -#: mod_muc/mod_muc_log.erl:463 -msgid "Friday" -msgstr "Sexta" - -#: mod_muc/mod_muc_log.erl:464 -msgid "Saturday" -msgstr "Sábado" - -#: mod_muc/mod_muc_log.erl:465 -msgid "Sunday" -msgstr "Domingo" - -#: mod_muc/mod_muc_log.erl:469 -msgid "January" -msgstr "Janeiro" - -#: mod_muc/mod_muc_log.erl:470 -msgid "February" -msgstr "Fevereiro" - -#: mod_muc/mod_muc_log.erl:471 -msgid "March" -msgstr "Março" - -#: mod_muc/mod_muc_log.erl:472 -msgid "April" -msgstr "Abril" - -#: mod_muc/mod_muc_log.erl:473 -msgid "May" -msgstr "Maio" - -#: mod_muc/mod_muc_log.erl:474 -msgid "June" -msgstr "Junho" - -#: mod_muc/mod_muc_log.erl:475 -msgid "July" -msgstr "Julho" - -#: mod_muc/mod_muc_log.erl:476 -msgid "August" -msgstr "Agosto" - -#: mod_muc/mod_muc_log.erl:477 -msgid "September" -msgstr "Setembro" - -#: mod_muc/mod_muc_log.erl:478 -msgid "October" -msgstr "Outubro" - -#: mod_muc/mod_muc_log.erl:479 -msgid "November" -msgstr "Novembro" - -#: mod_muc/mod_muc_log.erl:480 -msgid "December" -msgstr "Dezembro" - -#: mod_muc/mod_muc_log.erl:750 -msgid "Room Configuration" -msgstr "Configuração de salas" - -#: mod_muc/mod_muc_log.erl:759 -msgid "Room Occupants" -msgstr "Número de participantes" - -#: mod_muc/mod_muc_room.erl:174 -msgid "Traffic rate limit is exceeded" -msgstr "Limite de banda excedido" - -#: mod_muc/mod_muc_room.erl:246 -msgid "" -"This participant is kicked from the room because he sent an error message" -msgstr "" -"Este participante foi desconectado da sala de chat por ter enviado uma " -"mensagem de erro." - -#: mod_muc/mod_muc_room.erl:255 -msgid "It is not allowed to send private messages to the conference" -msgstr "Impedir o envio de mensagens privadas para a sala" - -#: mod_muc/mod_muc_room.erl:332 -msgid "Please, wait for a while before sending new voice request" -msgstr "Por favor, espere antes de enviar uma nova requisição de voz" - -#: mod_muc/mod_muc_room.erl:347 -msgid "Voice requests are disabled in this conference" -msgstr "Requisições de voz estào desabilitadas nesta conferência" - -#: mod_muc/mod_muc_room.erl:364 -msgid "Failed to extract JID from your voice request approval" -msgstr "Não foi possível extrair o JID (Jabber ID) da requisição de voz" - -#: mod_muc/mod_muc_room.erl:393 -msgid "Only moderators can approve voice requests" -msgstr "Somente moderadores podem aprovar requisições de voz" - -#: mod_muc/mod_muc_room.erl:408 -msgid "Improper message type" -msgstr "Tipo de mensagem incorreto" - -#: mod_muc/mod_muc_room.erl:518 -msgid "" -"This participant is kicked from the room because he sent an error message to " -"another participant" -msgstr "" -"Este participante foi desconectado da sala de chat por ter enviado uma " -"mensagem de erro para outro usuário." - -#: mod_muc/mod_muc_room.erl:531 -msgid "It is not allowed to send private messages of type \"groupchat\"" -msgstr "No está permitido enviar mensagens privados do tipo \"groupchat\"" - -#: mod_muc/mod_muc_room.erl:543 mod_muc/mod_muc_room.erl:611 -msgid "Recipient is not in the conference room" -msgstr "O receptor não está em la sala de conferencia" - -#: mod_muc/mod_muc_room.erl:564 mod_muc/mod_muc_room.erl:585 -msgid "It is not allowed to send private messages" -msgstr "Não é permitido enviar mensagens privadas" - -#: mod_muc/mod_muc_room.erl:576 mod_muc/mod_muc_room.erl:956 -#: mod_muc/mod_muc_room.erl:4040 -msgid "Only occupants are allowed to send messages to the conference" -msgstr "Solo os ocupantes podem enviar mensagens a la sala" - -#: mod_muc/mod_muc_room.erl:634 -msgid "Only occupants are allowed to send queries to the conference" -msgstr "Solo os ocupantes podem enviar consultas a la sala" - -#: mod_muc/mod_muc_room.erl:646 -msgid "Queries to the conference members are not allowed in this room" -msgstr "Nesta sala não se permite consultas aos membros da sala" - -#: mod_muc/mod_muc_room.erl:932 -msgid "" -"Only moderators and participants are allowed to change the subject in this " -"room" -msgstr "" -"Somente os moderadores e os participamentes podem alterar o assunto desta " -"sala" - -#: mod_muc/mod_muc_room.erl:937 -msgid "Only moderators are allowed to change the subject in this room" -msgstr "Somente os moderadores podem alterar o assunto desta sala" - -#: mod_muc/mod_muc_room.erl:947 -msgid "Visitors are not allowed to send messages to all occupants" -msgstr "Os visitantes não podem enviar mensagens a todos os ocupantes" - -#: mod_muc/mod_muc_room.erl:1021 -msgid "" -"This participant is kicked from the room because he sent an error presence" -msgstr "" -"Este participante foi desconectado da sala de chat por ter enviado uma " -"notificação errônea de presença." - -#: mod_muc/mod_muc_room.erl:1040 -msgid "Visitors are not allowed to change their nicknames in this room" -msgstr "Nesta sala, os visitantes não pode mudar seus apelidos" - -#: mod_muc/mod_muc_room.erl:1053 mod_muc/mod_muc_room.erl:1779 -msgid "That nickname is already in use by another occupant" -msgstr "O apelido (nick) já está sendo utilizado" - -#: mod_muc/mod_muc_room.erl:1768 -msgid "You have been banned from this room" -msgstr "As sido bloqueado em esta sala" - -#: mod_muc/mod_muc_room.erl:1771 -msgid "Membership is required to enter this room" -msgstr "Necessitas ser membro de esta sala para poder entrar" - -#: mod_muc/mod_muc_room.erl:1807 -msgid "This room is not anonymous" -msgstr "Essa sala não é anônima" - -#: mod_muc/mod_muc_room.erl:1833 -msgid "A password is required to enter this room" -msgstr "Se necessita senha para entrar em esta sala" - -#: mod_muc/mod_muc_room.erl:1855 mod_register.erl:246 -msgid "Too many CAPTCHA requests" -msgstr "Número excessivo de requisições para o CAPTCHA" - -#: mod_muc/mod_muc_room.erl:1864 mod_register.erl:251 -msgid "Unable to generate a CAPTCHA" -msgstr "Impossível gerar um CAPTCHA" - -#: mod_muc/mod_muc_room.erl:1874 -msgid "Incorrect password" -msgstr "Senha incorreta" - -#: mod_muc/mod_muc_room.erl:2448 -msgid "Administrator privileges required" -msgstr "Se necessita privilégios de administrador" - -#: mod_muc/mod_muc_room.erl:2463 -msgid "Moderator privileges required" -msgstr "Se necessita privilégios de moderador" - -#: mod_muc/mod_muc_room.erl:2619 -msgid "Jabber ID ~s is invalid" -msgstr "O Jabber ID ~s não es válido" - -#: mod_muc/mod_muc_room.erl:2633 -msgid "Nickname ~s does not exist in the room" -msgstr "O nick ~s não existe em la sala" - -#: mod_muc/mod_muc_room.erl:2659 mod_muc/mod_muc_room.erl:3049 -msgid "Invalid affiliation: ~s" -msgstr "Afiliação não válida: ~s" - -#: mod_muc/mod_muc_room.erl:2713 -msgid "Invalid role: ~s" -msgstr "Cargo (role) é não válido: ~s" - -#: mod_muc/mod_muc_room.erl:3026 mod_muc/mod_muc_room.erl:3062 -msgid "Owner privileges required" -msgstr "Se requere privilégios de proprietário da sala" - -#: mod_muc/mod_muc_room.erl:3195 -msgid "Configuration of room ~s" -msgstr "Configuração para ~s" - -#: mod_muc/mod_muc_room.erl:3200 -msgid "Room title" -msgstr "Título da sala" - -#: mod_muc/mod_muc_room.erl:3203 mod_muc/mod_muc_room.erl:3692 -msgid "Room description" -msgstr "Descrição da Sala" - -#: mod_muc/mod_muc_room.erl:3210 -msgid "Make room persistent" -msgstr "Tornar sala persistente" - -#: mod_muc/mod_muc_room.erl:3215 -msgid "Make room public searchable" -msgstr "Tornar sala pública possível de ser encontrada" - -#: mod_muc/mod_muc_room.erl:3218 -msgid "Make participants list public" -msgstr "Tornar pública a lista de participantes" - -#: mod_muc/mod_muc_room.erl:3221 -msgid "Make room password protected" -msgstr "Tornar protegida a senha da sala" - -#: mod_muc/mod_muc_room.erl:3232 -msgid "Maximum Number of Occupants" -msgstr "Número máximo de participantes" - -#: mod_muc/mod_muc_room.erl:3239 -msgid "No limit" -msgstr "Ilimitado" - -#: mod_muc/mod_muc_room.erl:3250 -msgid "Present real Jabber IDs to" -msgstr "Tornar o Jabber ID real visível por" - -#: mod_muc/mod_muc_room.erl:3258 mod_muc/mod_muc_room.erl:3292 -msgid "moderators only" -msgstr "apenas moderadores" - -#: mod_muc/mod_muc_room.erl:3260 mod_muc/mod_muc_room.erl:3294 -msgid "anyone" -msgstr "qualquer um" - -#: mod_muc/mod_muc_room.erl:3262 -msgid "Make room members-only" -msgstr "Tornar sala apenas para membros" - -#: mod_muc/mod_muc_room.erl:3265 -msgid "Make room moderated" -msgstr "Tornar a sala moderada" - -#: mod_muc/mod_muc_room.erl:3268 -msgid "Default users as participants" -msgstr "Usuários padrões como participantes" - -#: mod_muc/mod_muc_room.erl:3271 -msgid "Allow users to change the subject" -msgstr "Permitir a usuários modificar o assunto" - -#: mod_muc/mod_muc_room.erl:3274 -msgid "Allow users to send private messages" -msgstr "Permitir a usuários enviarem mensagens privadas" - -#: mod_muc/mod_muc_room.erl:3279 -#, fuzzy -msgid "Allow visitors to send private messages to" -msgstr "Permitir a usuários enviarem mensagens privadas" - -#: mod_muc/mod_muc_room.erl:3290 -msgid "nobody" -msgstr "ninguém" - -#: mod_muc/mod_muc_room.erl:3296 -msgid "Allow users to query other users" -msgstr "Permitir a usuários pesquisar informações sobre os demais" - -#: mod_muc/mod_muc_room.erl:3299 -msgid "Allow users to send invites" -msgstr "Permitir a usuários envio de convites" - -#: mod_muc/mod_muc_room.erl:3302 -msgid "Allow visitors to send status text in presence updates" -msgstr "Permitir atualizações de status aos visitantes" - -#: mod_muc/mod_muc_room.erl:3305 -msgid "Allow visitors to change nickname" -msgstr "Permitir mudança de apelido aos visitantes" - -#: mod_muc/mod_muc_room.erl:3308 -#, fuzzy -msgid "Allow visitors to send voice requests" -msgstr "Permitir a usuários envio de convites" - -#: mod_muc/mod_muc_room.erl:3311 -msgid "Minimum interval between voice requests (in seconds)" -msgstr "O intervalo mínimo entre requisições de voz (em segundos)" - -#: mod_muc/mod_muc_room.erl:3317 -msgid "Make room CAPTCHA protected" -msgstr "Tornar protegida a senha da sala" - -#: mod_muc/mod_muc_room.erl:3322 -msgid "Exclude Jabber IDs from CAPTCHA challenge" -msgstr "Excluir IDs Jabber de serem submetidos ao CAPTCHA" - -#: mod_muc/mod_muc_room.erl:3329 -msgid "Enable logging" -msgstr "Permitir criação de logs" - -#: mod_muc/mod_muc_room.erl:3337 -msgid "You need an x:data capable client to configure room" -msgstr "Necessitas um cliente com suporte de x:data para configurar la sala" - -#: mod_muc/mod_muc_room.erl:3694 -msgid "Number of occupants" -msgstr "Número de participantes" - -#: mod_muc/mod_muc_room.erl:3750 -msgid "private, " -msgstr "privado, " - -#: mod_muc/mod_muc_room.erl:3799 -msgid "Voice request" -msgstr "Requisição de voz" - -#: mod_muc/mod_muc_room.erl:3803 -msgid "Either approve or decline the voice request." -msgstr "Você deve aprovar/desaprovar a requisição de voz." - -#: mod_muc/mod_muc_room.erl:3809 -#, fuzzy -msgid "User JID" -msgstr "Usuário " - -#: mod_muc/mod_muc_room.erl:3811 -msgid "Grant voice to this person?" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3960 -msgid "~s invites you to the room ~s" -msgstr "~s convidou você para a sala ~s" - -#: mod_muc/mod_muc_room.erl:3969 -msgid "the password is" -msgstr "a senha é" - -#: mod_offline.erl:510 mod_offline_odbc.erl:352 -msgid "" -"Your contact offline message queue is full. The message has been discarded." -msgstr "Sua fila de mensagens offline esta cheia. Sua mensagem foi descartada" - -#: mod_offline.erl:560 mod_offline_odbc.erl:408 -msgid "~s's Offline Messages Queue" -msgstr "~s's Fila de Mensagens Offline" - -#: mod_offline.erl:563 mod_offline_odbc.erl:411 mod_roster.erl:987 -#: mod_roster_odbc.erl:1104 mod_shared_roster.erl:901 -#: mod_shared_roster.erl:1010 mod_shared_roster_odbc.erl:930 -#: mod_shared_roster_odbc.erl:1039 web/ejabberd_web_admin.erl:833 -#: web/ejabberd_web_admin.erl:874 web/ejabberd_web_admin.erl:942 -#: web/ejabberd_web_admin.erl:978 web/ejabberd_web_admin.erl:1019 -#: web/ejabberd_web_admin.erl:1508 web/ejabberd_web_admin.erl:1760 -#: web/ejabberd_web_admin.erl:1931 web/ejabberd_web_admin.erl:1963 -#: web/ejabberd_web_admin.erl:2031 web/ejabberd_web_admin.erl:2135 -#: web/ejabberd_web_admin.erl:2160 web/ejabberd_web_admin.erl:2248 -msgid "Submitted" -msgstr "Submetido" - -#: mod_offline.erl:571 -msgid "Time" -msgstr "Fecha" - -#: mod_offline.erl:572 -msgid "From" -msgstr "De" - -#: mod_offline.erl:573 -msgid "To" -msgstr "Para" - -#: mod_offline.erl:574 mod_offline_odbc.erl:419 -msgid "Packet" -msgstr "Pacote" - -#: mod_offline.erl:587 mod_offline_odbc.erl:432 mod_shared_roster.erl:908 -#: mod_shared_roster_odbc.erl:937 web/ejabberd_web_admin.erl:882 -#: web/ejabberd_web_admin.erl:986 -msgid "Delete Selected" -msgstr "Remover os selecionados" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -msgid "Offline Messages:" -msgstr "Mensagens offline" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -msgid "Remove All Offline Messages" -msgstr "Remover Todas as Mensagens Offline" - -#: mod_proxy65/mod_proxy65_service.erl:213 -msgid "ejabberd SOCKS5 Bytestreams module" -msgstr "Modulo ejabberd SOCKS5 Bytestreams" - -#: mod_pubsub/mod_pubsub.erl:1120 mod_pubsub/mod_pubsub_odbc.erl:929 -msgid "Publish-Subscribe" -msgstr "Publicação de Tópico" - -#: mod_pubsub/mod_pubsub.erl:1214 mod_pubsub/mod_pubsub_odbc.erl:1025 -msgid "ejabberd Publish-Subscribe module" -msgstr "Módulo para Publicar Tópicos do ejabberd" - -#: mod_pubsub/mod_pubsub.erl:1497 mod_pubsub/mod_pubsub_odbc.erl:1312 -msgid "PubSub subscriber request" -msgstr "PubSub requisição de assinante" - -#: mod_pubsub/mod_pubsub.erl:1499 mod_pubsub/mod_pubsub_odbc.erl:1314 -msgid "Choose whether to approve this entity's subscription." -msgstr "Aprovar esta assinatura." - -#: mod_pubsub/mod_pubsub.erl:1505 mod_pubsub/mod_pubsub_odbc.erl:1320 -msgid "Node ID" -msgstr "ID do Tópico" - -#: mod_pubsub/mod_pubsub.erl:1510 mod_pubsub/mod_pubsub_odbc.erl:1325 -msgid "Subscriber Address" -msgstr "Endereço dos Assinantes" - -#: mod_pubsub/mod_pubsub.erl:1516 mod_pubsub/mod_pubsub_odbc.erl:1331 -msgid "Allow this Jabber ID to subscribe to this pubsub node?" -msgstr "Autorizar este Jabber ID para a inscrição neste tópico pubsub?" - -#: mod_pubsub/mod_pubsub.erl:3474 mod_pubsub/mod_pubsub_odbc.erl:3287 -msgid "Deliver payloads with event notifications" -msgstr "Enviar payloads junto com as notificações de eventos" - -#: mod_pubsub/mod_pubsub.erl:3475 mod_pubsub/mod_pubsub_odbc.erl:3288 -msgid "Deliver event notifications" -msgstr "Entregar as notificações de evento" - -#: mod_pubsub/mod_pubsub.erl:3476 mod_pubsub/mod_pubsub_odbc.erl:3289 -msgid "Notify subscribers when the node configuration changes" -msgstr "Notificar subscritores quando cambia la configuração do nodo" - -#: mod_pubsub/mod_pubsub.erl:3477 mod_pubsub/mod_pubsub_odbc.erl:3290 -msgid "Notify subscribers when the node is deleted" -msgstr "Notificar subscritores quando o nodo se elimine" - -#: mod_pubsub/mod_pubsub.erl:3478 mod_pubsub/mod_pubsub_odbc.erl:3291 -msgid "Notify subscribers when items are removed from the node" -msgstr "Notificar subscritores quando os elementos se eliminem do nodo" - -#: mod_pubsub/mod_pubsub.erl:3479 mod_pubsub/mod_pubsub_odbc.erl:3292 -msgid "Persist items to storage" -msgstr "Persistir elementos ao armazenar" - -#: mod_pubsub/mod_pubsub.erl:3480 mod_pubsub/mod_pubsub_odbc.erl:3293 -msgid "A friendly name for the node" -msgstr "Um nome familiar para o nó" - -#: mod_pubsub/mod_pubsub.erl:3481 mod_pubsub/mod_pubsub_odbc.erl:3294 -msgid "Max # of items to persist" -msgstr "Máximo # de elementos que persistem" - -#: mod_pubsub/mod_pubsub.erl:3482 mod_pubsub/mod_pubsub_odbc.erl:3295 -msgid "Whether to allow subscriptions" -msgstr "Permitir subscrições" - -#: mod_pubsub/mod_pubsub.erl:3483 mod_pubsub/mod_pubsub_odbc.erl:3296 -msgid "Specify the access model" -msgstr "Especificar os modelos de acesso" - -#: mod_pubsub/mod_pubsub.erl:3486 mod_pubsub/mod_pubsub_odbc.erl:3299 -msgid "Roster groups allowed to subscribe" -msgstr "Listar grupos autorizados" - -#: mod_pubsub/mod_pubsub.erl:3487 mod_pubsub/mod_pubsub_odbc.erl:3300 -msgid "Specify the publisher model" -msgstr "Especificar o modelo do publicante" - -#: mod_pubsub/mod_pubsub.erl:3489 mod_pubsub/mod_pubsub_odbc.erl:3302 -msgid "Purge all items when the relevant publisher goes offline" -msgstr "Descartar todos os itens quando o publicante principal estiver offline" - -#: mod_pubsub/mod_pubsub.erl:3490 mod_pubsub/mod_pubsub_odbc.erl:3303 -msgid "Specify the event message type" -msgstr "Especificar o tipo de mensagem para o evento" - -#: mod_pubsub/mod_pubsub.erl:3492 mod_pubsub/mod_pubsub_odbc.erl:3305 -msgid "Max payload size in bytes" -msgstr "Máximo tamanho do payload em bytes" - -#: mod_pubsub/mod_pubsub.erl:3493 mod_pubsub/mod_pubsub_odbc.erl:3306 -msgid "When to send the last published item" -msgstr "Quando enviar o último tópico publicado" - -#: mod_pubsub/mod_pubsub.erl:3495 mod_pubsub/mod_pubsub_odbc.erl:3308 -msgid "Only deliver notifications to available users" -msgstr "Solo enviar notificações aos usuários disponíveis" - -#: mod_pubsub/mod_pubsub.erl:3496 mod_pubsub/mod_pubsub_odbc.erl:3309 -msgid "The collections with which a node is affiliated" -msgstr "As coleções com as quais o nó está relacionado" - -#: mod_register.erl:193 -msgid "The CAPTCHA verification has failed" -msgstr "A verificação do CAPTCHA falhou" - -#: mod_register.erl:220 -#, fuzzy -msgid "You need a client that supports x:data and CAPTCHA to register" -msgstr "" -"Você precisa de um cliente com suporte de x:data para poder registrar o nick" - -#: mod_register.erl:226 mod_register.erl:265 -msgid "Choose a username and password to register with this server" -msgstr "Escolha um nome de usuário e senha para registrar-se neste servidor" - -#: mod_register.erl:230 mod_vcard.erl:364 mod_vcard_odbc.erl:342 -#: web/ejabberd_web_admin.erl:1515 web/ejabberd_web_admin.erl:1572 -msgid "User" -msgstr "Usuário" - -#: mod_register.erl:316 mod_register.erl:361 -#, fuzzy -msgid "The password is too weak" -msgstr "Senha considerada fraca'" - -#: mod_register.erl:365 -msgid "Users are not allowed to register accounts so quickly" -msgstr "Usuários não estão autorizados a registrar contas imediatamente" - -#: mod_roster.erl:938 mod_roster_odbc.erl:1055 web/ejabberd_web_admin.erl:1701 -#: web/ejabberd_web_admin.erl:1886 web/ejabberd_web_admin.erl:1897 -#: web/ejabberd_web_admin.erl:2219 -msgid "None" -msgstr "Nenhum" - -#: mod_roster.erl:945 mod_roster_odbc.erl:1062 -msgid "Subscription" -msgstr "Subscrição" - -#: mod_roster.erl:946 mod_roster_odbc.erl:1063 -msgid "Pending" -msgstr "Pendente" - -#: mod_roster.erl:947 mod_roster_odbc.erl:1064 -msgid "Groups" -msgstr "Grupos" - -#: mod_roster.erl:974 mod_roster_odbc.erl:1091 -msgid "Validate" -msgstr "Validar" - -#: mod_roster.erl:982 mod_roster_odbc.erl:1099 -msgid "Remove" -msgstr "Remover" - -#: mod_roster.erl:985 mod_roster_odbc.erl:1102 -msgid "Roster of " -msgstr "Lista de contatos de " - -#: mod_roster.erl:988 mod_roster_odbc.erl:1105 mod_shared_roster.erl:902 -#: mod_shared_roster.erl:1011 mod_shared_roster_odbc.erl:931 -#: mod_shared_roster_odbc.erl:1040 web/ejabberd_web_admin.erl:834 -#: web/ejabberd_web_admin.erl:875 web/ejabberd_web_admin.erl:943 -#: web/ejabberd_web_admin.erl:979 web/ejabberd_web_admin.erl:1020 -#: web/ejabberd_web_admin.erl:1509 web/ejabberd_web_admin.erl:1761 -#: web/ejabberd_web_admin.erl:1932 web/ejabberd_web_admin.erl:2136 -#: web/ejabberd_web_admin.erl:2161 -msgid "Bad format" -msgstr "Formato incorreto" - -#: mod_roster.erl:995 mod_roster_odbc.erl:1112 -msgid "Add Jabber ID" -msgstr "Adicionar ID jabber" - -#: mod_roster.erl:1094 mod_roster_odbc.erl:1211 -msgid "Roster" -msgstr "Lista de contatos" - -#: mod_shared_roster.erl:857 mod_shared_roster.erl:899 -#: mod_shared_roster.erl:1007 mod_shared_roster_odbc.erl:886 -#: mod_shared_roster_odbc.erl:928 mod_shared_roster_odbc.erl:1036 -msgid "Shared Roster Groups" -msgstr "Grupos Shared Roster" - -#: mod_shared_roster.erl:895 mod_shared_roster_odbc.erl:924 -#: web/ejabberd_web_admin.erl:1365 web/ejabberd_web_admin.erl:2461 -msgid "Add New" -msgstr "Adicionar novo" - -#: mod_shared_roster.erl:978 mod_shared_roster_odbc.erl:1007 -msgid "Name:" -msgstr "Nome:" - -#: mod_shared_roster.erl:983 mod_shared_roster_odbc.erl:1012 -msgid "Description:" -msgstr "Descrição:" - -#: mod_shared_roster.erl:991 mod_shared_roster_odbc.erl:1020 -msgid "Members:" -msgstr "Miembros:" - -#: mod_shared_roster.erl:999 mod_shared_roster_odbc.erl:1028 -#, fuzzy -msgid "Displayed Groups:" -msgstr "Grupos Indicados:" - -#: mod_shared_roster.erl:1008 mod_shared_roster_odbc.erl:1037 -msgid "Group " -msgstr "Grupo " - -#: mod_shared_roster.erl:1017 mod_shared_roster_odbc.erl:1046 -#: web/ejabberd_web_admin.erl:840 web/ejabberd_web_admin.erl:884 -#: web/ejabberd_web_admin.erl:949 web/ejabberd_web_admin.erl:1026 -#: web/ejabberd_web_admin.erl:2017 -msgid "Submit" -msgstr "Enviar" - -#: mod_vcard.erl:165 mod_vcard_ldap.erl:238 mod_vcard_odbc.erl:129 -msgid "Erlang Jabber Server" -msgstr "Servidor Jabber em Erlang" - -#: mod_vcard.erl:364 mod_vcard.erl:478 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:454 -msgid "Birthday" -msgstr "Aniversário" - -#: mod_vcard.erl:364 mod_vcard.erl:480 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:456 -msgid "City" -msgstr "Cidade" - -#: mod_vcard.erl:364 mod_vcard.erl:479 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:455 -msgid "Country" -msgstr "País" - -#: mod_vcard.erl:364 mod_vcard.erl:481 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:457 -msgid "Email" -msgstr "Email" - -#: mod_vcard.erl:364 mod_vcard.erl:476 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:452 -msgid "Family Name" -msgstr "Sobrenome" - -#: mod_vcard.erl:364 mod_vcard_odbc.erl:342 -msgid "" -"Fill in the form to search for any matching Jabber User (Add * to the end of " -"field to match substring)" -msgstr "" -"Preencha o formulário para buscar usuários Jabber. Agrega * ao final de um " -"campo para buscar sub-palavras." - -#: mod_vcard.erl:364 mod_vcard.erl:473 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:449 -msgid "Full Name" -msgstr "Nome completo" - -#: mod_vcard.erl:364 mod_vcard.erl:475 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:451 -msgid "Middle Name" -msgstr "Nome do meio" - -#: mod_vcard.erl:364 mod_vcard.erl:474 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:450 web/ejabberd_web_admin.erl:2006 -msgid "Name" -msgstr "Nome" - -#: mod_vcard.erl:364 mod_vcard.erl:482 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:458 -msgid "Organization Name" -msgstr "Nome da organização" - -#: mod_vcard.erl:364 mod_vcard.erl:483 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:459 -msgid "Organization Unit" -msgstr "Departamento/Unidade" - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "Search users in " -msgstr "Procurar usuários em " - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "You need an x:data capable client to search" -msgstr "Necessitas um cliente com suporte de x:data para poder buscar" - -#: mod_vcard.erl:389 mod_vcard_ldap.erl:487 mod_vcard_odbc.erl:367 -msgid "vCard User Search" -msgstr "Busca de Usuário vCard" - -#: mod_vcard.erl:445 mod_vcard_ldap.erl:541 mod_vcard_odbc.erl:421 -msgid "ejabberd vCard module" -msgstr "Módulo vCard para ejabberd" - -#: mod_vcard.erl:469 mod_vcard_ldap.erl:551 mod_vcard_odbc.erl:445 -msgid "Search Results for " -msgstr "Resultados de pesquisa para " - -#: mod_vcard_ldap.erl:462 -msgid "Fill in fields to search for any matching Jabber User" -msgstr "Preencha campos para buscar usuários Jabber que concordem" - -#: web/ejabberd_web_admin.erl:193 web/ejabberd_web_admin.erl:203 -#: web/ejabberd_web_admin.erl:219 web/ejabberd_web_admin.erl:229 -msgid "Unauthorized" -msgstr "Não Autorizado" - -#: web/ejabberd_web_admin.erl:286 web/ejabberd_web_admin.erl:303 -msgid "ejabberd Web Admin" -msgstr "ejabberd Web Admin" - -#: web/ejabberd_web_admin.erl:769 web/ejabberd_web_admin.erl:780 -msgid "Administration" -msgstr "Administração" - -#: web/ejabberd_web_admin.erl:878 web/ejabberd_web_admin.erl:982 -msgid "Raw" -msgstr "Intocado" - -#: web/ejabberd_web_admin.erl:1017 -msgid "~s access rule configuration" -msgstr "Configuração da Regra de Acesso ~s" - -#: web/ejabberd_web_admin.erl:1035 -msgid "Virtual Hosts" -msgstr "Hosts virtuais" - -#: web/ejabberd_web_admin.erl:1043 web/ejabberd_web_admin.erl:1050 -msgid "Users" -msgstr "Usuários" - -#: web/ejabberd_web_admin.erl:1078 -msgid "Users Last Activity" -msgstr "Ultimas atividades dos usuários" - -#: web/ejabberd_web_admin.erl:1080 -msgid "Period: " -msgstr "Período: " - -#: web/ejabberd_web_admin.erl:1090 -msgid "Last month" -msgstr "Último mês" - -#: web/ejabberd_web_admin.erl:1091 -msgid "Last year" -msgstr "Último ano" - -#: web/ejabberd_web_admin.erl:1092 -msgid "All activity" -msgstr "Todas atividades" - -#: web/ejabberd_web_admin.erl:1094 -msgid "Show Ordinary Table" -msgstr "Mostrar Tabela Ordinária" - -#: web/ejabberd_web_admin.erl:1096 -msgid "Show Integral Table" -msgstr "Mostrar Tabela Integral" - -#: web/ejabberd_web_admin.erl:1105 web/ejabberd_web_admin.erl:1939 -msgid "Statistics" -msgstr "Estatísticas" - -#: web/ejabberd_web_admin.erl:1117 -msgid "Not Found" -msgstr "Não encontrado" - -#: web/ejabberd_web_admin.erl:1134 -msgid "Node not found" -msgstr "Nó não encontrado" - -#: web/ejabberd_web_admin.erl:1460 -msgid "Host" -msgstr "Máquina" - -#: web/ejabberd_web_admin.erl:1461 -msgid "Registered Users" -msgstr "Usuários Registrados" - -#: web/ejabberd_web_admin.erl:1573 -msgid "Offline Messages" -msgstr "Mensagens offline" - -#: web/ejabberd_web_admin.erl:1574 web/ejabberd_web_admin.erl:1767 -msgid "Last Activity" -msgstr "Última atividade" - -#: web/ejabberd_web_admin.erl:1659 web/ejabberd_web_admin.erl:1675 -msgid "Registered Users:" -msgstr "Usuários registrados" - -#: web/ejabberd_web_admin.erl:1661 web/ejabberd_web_admin.erl:1677 -#: web/ejabberd_web_admin.erl:2192 -msgid "Online Users:" -msgstr "Usuários online" - -#: web/ejabberd_web_admin.erl:1663 -msgid "Outgoing s2s Connections:" -msgstr "Conexões que partem de s2s" - -#: web/ejabberd_web_admin.erl:1665 -msgid "Outgoing s2s Servers:" -msgstr "Servidores que partem de s2s" - -#: web/ejabberd_web_admin.erl:1734 web/mod_register_web.erl:175 -#: web/mod_register_web.erl:372 web/mod_register_web.erl:381 -#: web/mod_register_web.erl:411 -msgid "Change Password" -msgstr "Mudar senha" - -#: web/ejabberd_web_admin.erl:1758 -msgid "User " -msgstr "Usuário " - -#: web/ejabberd_web_admin.erl:1765 -msgid "Connected Resources:" -msgstr "Recursos conectados:" - -#: web/ejabberd_web_admin.erl:1766 web/mod_register_web.erl:227 -#: web/mod_register_web.erl:519 -msgid "Password:" -msgstr "Senha:" - -#: web/ejabberd_web_admin.erl:1828 -msgid "No Data" -msgstr "Nenhum dado" - -#: web/ejabberd_web_admin.erl:1906 -msgid "Nodes" -msgstr "Nós" - -#: web/ejabberd_web_admin.erl:1929 web/ejabberd_web_admin.erl:1951 -msgid "Node " -msgstr "Nó" - -#: web/ejabberd_web_admin.erl:1938 -msgid "Listened Ports" -msgstr "Portas escutadas" - -#: web/ejabberd_web_admin.erl:1940 web/ejabberd_web_admin.erl:2260 -#: web/ejabberd_web_admin.erl:2448 -msgid "Update" -msgstr "Atualizar" - -#: web/ejabberd_web_admin.erl:1943 web/ejabberd_web_admin.erl:2569 -msgid "Restart" -msgstr "Reiniciar" - -#: web/ejabberd_web_admin.erl:1945 web/ejabberd_web_admin.erl:2571 -msgid "Stop" -msgstr "Parar" - -#: web/ejabberd_web_admin.erl:1959 -msgid "RPC Call Error" -msgstr "Erro de chamada RPC" - -#: web/ejabberd_web_admin.erl:2000 -msgid "Database Tables at " -msgstr "Tabelas de base de dados em " - -#: web/ejabberd_web_admin.erl:2007 -msgid "Storage Type" -msgstr "Tipo de armazenamento" - -#: web/ejabberd_web_admin.erl:2008 -msgid "Elements" -msgstr "Elementos" - -#: web/ejabberd_web_admin.erl:2009 -msgid "Memory" -msgstr "Memória" - -#: web/ejabberd_web_admin.erl:2032 web/ejabberd_web_admin.erl:2137 -msgid "Error" -msgstr "Erro" - -#: web/ejabberd_web_admin.erl:2034 -msgid "Backup of " -msgstr "Backup de " - -#: web/ejabberd_web_admin.erl:2036 -msgid "" -"Please note that these options will only backup the builtin Mnesia database. " -"If you are using the ODBC module, you also need to backup your SQL database " -"separately." -msgstr "" -"Observe que tais opções farão backup apenas da base de dados Mnesia. Caso " -"você esteja utilizando o modulo ODBC, você precisará fazer backup de sua " -"base de dados SQL separadamente." - -#: web/ejabberd_web_admin.erl:2041 -msgid "Store binary backup:" -msgstr "Armazenar backup binário:" - -#: web/ejabberd_web_admin.erl:2045 web/ejabberd_web_admin.erl:2052 -#: web/ejabberd_web_admin.erl:2060 web/ejabberd_web_admin.erl:2067 -#: web/ejabberd_web_admin.erl:2074 web/ejabberd_web_admin.erl:2081 -#: web/ejabberd_web_admin.erl:2088 web/ejabberd_web_admin.erl:2096 -#: web/ejabberd_web_admin.erl:2103 web/ejabberd_web_admin.erl:2110 -msgid "OK" -msgstr "OK" - -#: web/ejabberd_web_admin.erl:2048 -msgid "Restore binary backup immediately:" -msgstr "Restaurar backup binário imediatamente" - -#: web/ejabberd_web_admin.erl:2056 -msgid "" -"Restore binary backup after next ejabberd restart (requires less memory):" -msgstr "" -"Restaurar backup binário após próximo reinicialização do ejabberd (requer " -"menos memória):" - -#: web/ejabberd_web_admin.erl:2063 -msgid "Store plain text backup:" -msgstr "Armazenar backup em texto:" - -#: web/ejabberd_web_admin.erl:2070 -msgid "Restore plain text backup immediately:" -msgstr "Restaurar backup formato texto imediatamente:" - -#: web/ejabberd_web_admin.erl:2077 -msgid "Import users data from a PIEFXIS file (XEP-0227):" -msgstr "Importar usuários de um arquivo PIEFXIS (XEP-0227): " - -#: web/ejabberd_web_admin.erl:2084 -msgid "Export data of all users in the server to PIEFXIS files (XEP-0227):" -msgstr "" -"Exportar todos os dados de todos os usuários no servidor, para arquivos " -"formato PIEFXIS (XEP-0227):" - -#: web/ejabberd_web_admin.erl:2091 -msgid "Export data of users in a host to PIEFXIS files (XEP-0227):" -msgstr "" -"Exportar dados dos usuários em um host, para arquivos PIEFXIS (XEP-0227):" - -#: web/ejabberd_web_admin.erl:2099 -msgid "Import user data from jabberd14 spool file:" -msgstr "Importar dados dos usuários de uma fila jabberd14:" - -#: web/ejabberd_web_admin.erl:2106 -msgid "Import users data from jabberd14 spool directory:" -msgstr "Importar dados dos usuários de um diretório-fila jabberd14:" - -#: web/ejabberd_web_admin.erl:2132 -msgid "Listened Ports at " -msgstr "Portas ouvintes em " - -#: web/ejabberd_web_admin.erl:2157 -msgid "Modules at " -msgstr "Módulos em " - -#: web/ejabberd_web_admin.erl:2183 -msgid "Statistics of ~p" -msgstr "Estatísticas de ~p" - -#: web/ejabberd_web_admin.erl:2186 -msgid "Uptime:" -msgstr "Uptime:" - -#: web/ejabberd_web_admin.erl:2189 -msgid "CPU Time:" -msgstr "Tempo de CPU" - -#: web/ejabberd_web_admin.erl:2195 -msgid "Transactions Committed:" -msgstr "Transações salvas:" - -#: web/ejabberd_web_admin.erl:2198 -msgid "Transactions Aborted:" -msgstr "Transações abortadas:" - -#: web/ejabberd_web_admin.erl:2201 -msgid "Transactions Restarted:" -msgstr "Transações reiniciadas:" - -#: web/ejabberd_web_admin.erl:2204 -msgid "Transactions Logged:" -msgstr "Transações de log:" - -#: web/ejabberd_web_admin.erl:2246 -msgid "Update " -msgstr "Atualizar " - -#: web/ejabberd_web_admin.erl:2254 -msgid "Update plan" -msgstr "Plano de Atualização" - -#: web/ejabberd_web_admin.erl:2255 -msgid "Modified modules" -msgstr "Módulos atualizados" - -#: web/ejabberd_web_admin.erl:2256 -msgid "Update script" -msgstr "Script de atualização" - -#: web/ejabberd_web_admin.erl:2257 -msgid "Low level update script" -msgstr "Script de atualização low level" - -#: web/ejabberd_web_admin.erl:2258 -msgid "Script check" -msgstr "Verificação de Script" - -#: web/ejabberd_web_admin.erl:2426 -msgid "Port" -msgstr "Porta" - -#: web/ejabberd_web_admin.erl:2427 -msgid "IP" -msgstr "IP" - -#: web/ejabberd_web_admin.erl:2428 -msgid "Protocol" -msgstr "Porta" - -#: web/ejabberd_web_admin.erl:2429 web/ejabberd_web_admin.erl:2556 -msgid "Module" -msgstr "Módulo" - -#: web/ejabberd_web_admin.erl:2430 web/ejabberd_web_admin.erl:2557 -msgid "Options" -msgstr "Opções" - -#: web/ejabberd_web_admin.erl:2450 -msgid "Delete" -msgstr "Eliminar" - -#: web/ejabberd_web_admin.erl:2579 -msgid "Start" -msgstr "Iniciar" - -#: web/mod_register_web.erl:104 -msgid "Your Jabber account was successfully created." -msgstr "Sua conta jabber foi criada corretamente." - -#: web/mod_register_web.erl:107 -msgid "There was an error creating the account: " -msgstr "Houveram erras ao criar esta conta: " - -#: web/mod_register_web.erl:115 -msgid "Your Jabber account was successfully deleted." -msgstr "Sua conta Jabber foi deletada com sucesso." - -#: web/mod_register_web.erl:118 -msgid "There was an error deleting the account: " -msgstr "Erro ao deletar esta conta: " - -#: web/mod_register_web.erl:128 -msgid "The password of your Jabber account was successfully changed." -msgstr "A senha da sua conta Jabber foi mudada com sucesso." - -#: web/mod_register_web.erl:131 -msgid "There was an error changing the password: " -msgstr "Houveram erros ao mudar a senha: " - -#: web/mod_register_web.erl:163 web/mod_register_web.erl:172 -msgid "Jabber Account Registration" -msgstr "Registros de Contas Jabber" - -#: web/mod_register_web.erl:174 web/mod_register_web.erl:192 -#: web/mod_register_web.erl:201 -msgid "Register a Jabber account" -msgstr "Registrar uma conta Jabber" - -#: web/mod_register_web.erl:176 web/mod_register_web.erl:493 -#: web/mod_register_web.erl:502 -msgid "Unregister a Jabber account" -msgstr "Deletar conta Jabber" - -#: web/mod_register_web.erl:203 -msgid "" -"This page allows to create a Jabber account in this Jabber server. Your JID " -"(Jabber IDentifier) will be of the form: username@server. Please read " -"carefully the instructions to fill correctly the fields." -msgstr "" -"Esta pagina aceita criações de novas contas Jabber neste servidor. A sua JID " -"(Identificador Jabber) será da seguinte forma: 'usuário@servidor'. Por " -"favor, leia cuidadosamente as instruções para preencher corretamente os " -"campos." - -#: web/mod_register_web.erl:212 web/mod_register_web.erl:386 -#: web/mod_register_web.erl:509 -#, fuzzy -msgid "Username:" -msgstr "Nome de usuário no IRC" - -#: web/mod_register_web.erl:217 -msgid "This is case insensitive: macbeth is the same that MacBeth and Macbeth." -msgstr "" -"Não é 'case insensitive': macbeth é o mesmo que MacBeth e ainda Macbeth. " - -#: web/mod_register_web.erl:218 -msgid "Characters not allowed:" -msgstr "Caracteres não aceitos:" - -#: web/mod_register_web.erl:222 web/mod_register_web.erl:391 -#: web/mod_register_web.erl:514 -#, fuzzy -msgid "Server:" -msgstr "Servidor ~b" - -#: web/mod_register_web.erl:232 -msgid "" -"Don't tell your password to anybody, not even the administrators of the " -"Jabber server." -msgstr "" -"Não revele o seu computador a ninguém, mesmo para o administrador deste " -"servidor Jabber." - -#: web/mod_register_web.erl:234 -msgid "You can later change your password using a Jabber client." -msgstr "Mais tarde você pode alterar a sua senha usando um cliente Jabber." - -#: web/mod_register_web.erl:235 -msgid "" -"Some Jabber clients can store your password in your computer. Use that " -"feature only if you trust your computer is safe." -msgstr "" -"Alguns clientes jabber podem salvar a sua senha no seu computador. Use " -"recurso somente se você considera este computador seguro o suficiente." - -#: web/mod_register_web.erl:237 -msgid "" -"Memorize your password, or write it in a paper placed in a safe place. In " -"Jabber there isn't an automated way to recover your password if you forget " -"it." -msgstr "" -"Memorize a sua senha, ou escreva-a em um papel e guarde-o em um lugar " -"seguro. Jabber não é uma maneira automatizada para recuperar a sua senha, se " -"você a esquecer eventualmente." - -#: web/mod_register_web.erl:242 web/mod_register_web.erl:406 -#, fuzzy -msgid "Password Verification:" -msgstr "Verificação de Senha" - -#: web/mod_register_web.erl:250 -#, fuzzy -msgid "Register" -msgstr "Lista de contatos" - -#: web/mod_register_web.erl:396 -#, fuzzy -msgid "Old Password:" -msgstr "Senha Antiga:" - -#: web/mod_register_web.erl:401 -#, fuzzy -msgid "New Password:" -msgstr "Nova Senha:" - -#: web/mod_register_web.erl:504 -msgid "This page allows to unregister a Jabber account in this Jabber server." -msgstr "Esta página aceita para deletar uma conta Jabber neste servidor." - -#: web/mod_register_web.erl:524 -msgid "Unregister" -msgstr "Deletar registro" - -#, fuzzy -#~ msgid "Captcha test failed" -#~ msgstr "O CAPTCHA é inválido." diff --git a/priv/msgs/pt.msg b/priv/msgs/pt.msg index e4e3a4fb7..358eb4858 100644 --- a/priv/msgs/pt.msg +++ b/priv/msgs/pt.msg @@ -1,137 +1,630 @@ -{"Access Configuration","Configuração de acessos"}. -{"Access Control List Configuration","Configuração da Lista de Controlo de Acesso"}. -{"Access control lists","Listas de Controlo de Acesso"}. -{"Access Control Lists","Listas de Controlo de Acesso"}. +%% Generated automatically +%% DO NOT EDIT: run `make translations` instead +%% To improve translations please read: +%% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ + +{" (Add * to the end of field to match substring)"," (Adicione * no final do campo para combinar com a sub-cadeia)"}. +{" has set the subject to: "," colocou o tópico: "}. +{"# participants","# participantes"}. +{"A description of the node","Uma descrição do nó"}. +{"A friendly name for the node","Um nome familiar para o nó"}. +{"A password is required to enter this room","Se necessita palavra-passe para entrar nesta sala"}. +{"A Web Page","Uma página da web"}. +{"Accept","Aceito"}. {"Access denied by service policy","Acesso negado pela política de serviço"}. -{"Access rules","Regras de acesso"}. -{"Access Rules","Regras de Acesso"}. +{"Access model","Modelo de acesso"}. +{"Account doesn't exist","A conta não existe"}. {"Action on user","Acção no utilizador"}. -{"Add New","Adicionar novo"}. +{"Add a hat to a user","Adiciona um chapéu num utilizador"}. {"Add User","Adicionar utilizador"}. {"Administration of ","Administração de "}. +{"Administration","Administração"}. {"Administrator privileges required","São necessários privilégios de administrador"}. +{"All activity","Todas atividades"}. {"All Users","Todos os utilizadores"}. -{"Backup","Guardar cópia de segurança"}. +{"Allow subscription","Permitir a assinatura"}. +{"Allow this Jabber ID to subscribe to this pubsub node?","Autorizar este Jabber ID para a inscrição neste tópico pubsub?"}. +{"Allow this person to register with the room?","Permita que esta pessoa se registe na sala?"}. +{"Allow users to change the subject","Permitir a utilizadores modificar o assunto"}. +{"Allow users to query other users","Permitir a utilizadores pesquisar informações sobre os demais"}. +{"Allow users to send invites","Permitir a utilizadores envio de convites"}. +{"Allow users to send private messages","Permitir a utilizadores enviarem mensagens privadas"}. +{"Allow visitors to change nickname","Permitir mudança de apelido aos visitantes"}. +{"Allow visitors to send private messages to","Permitir visitantes enviar mensagem privada para"}. +{"Allow visitors to send status text in presence updates","Permitir atualizações de estado aos visitantes"}. +{"Allow visitors to send voice requests","Permitir aos visitantes o envio de requisições de voz"}. +{"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.","Um grupo LDAP associado que define a adesão à sala; este deve ser um Nome Distinto LDAP de acordo com uma definição específica da implementação ou da implantação específica de um grupo."}. +{"Announcements","Anúncios"}. +{"Answer associated with a picture","Resposta associada com uma foto"}. +{"Answer associated with a video","Resposta associada com um vídeo"}. +{"Answer associated with speech","Resposta associada com a fala"}. +{"Answer to a question","Resposta para uma pergunta"}. +{"Anyone in the specified roster group(s) may subscribe and retrieve items","Qualquer pessoa do(s) grupo(s) informado(s) podem se inscrever e recuperar itens"}. +{"Anyone may associate leaf nodes with the collection","Qualquer pessoa pode associar nós das páginas à coleção"}. +{"Anyone may publish","Qualquer pessoa pode publicar"}. +{"Anyone may subscribe and retrieve items","Qualquer pessoa pode se inscrever e recuperar os itens"}. +{"Anyone with a presence subscription of both or from may subscribe and retrieve items","Qualquer pessoa com uma assinatura presente dos dois ou de ambos pode se inscrever e recuperar os itens"}. +{"Anyone with Voice","Qualquer pessoa com voz"}. +{"Anyone","Qualquer pessoa"}. +{"API Commands","Comandos API"}. +{"April","Abril"}. +{"Arguments","Argumentos"}. +{"Attribute 'channel' is required for this request","O atributo 'canal' é necessário para esta solicitação"}. +{"Attribute 'id' is mandatory for MIX messages","O atributo 'id' é obrigatório para mensagens MIX"}. +{"Attribute 'jid' is not allowed here","O atributo 'jid' não é permitido aqui"}. +{"Attribute 'node' is not allowed here","O Atributo 'nó' não é permitido aqui"}. +{"Attribute 'to' of stanza that triggered challenge","O atributo 'para' da estrofe que desencadeou o desafio"}. +{"August","Agosto"}. +{"Automatic node creation is not enabled","Criação automatizada de nós está desativada"}. {"Backup Management","Gestão de cópias de segurança"}. +{"Backup of ~p","Backup de ~p"}. {"Backup to File at ","Guardar cópia de segurança para ficheiro em "}. +{"Backup","Guardar cópia de segurança"}. +{"Bad format","Formato incorreto"}. {"Birthday","Data de nascimento"}. +{"Both the username and the resource are required","Nome de utilizador e recurso são necessários"}. +{"Bytestream already activated","Bytestream já foi ativado"}. +{"Cannot remove active list","Não é possível remover uma lista ativa"}. +{"Cannot remove default list","Não é possível remover uma lista padrão"}. +{"CAPTCHA web page","CAPTCHA web page"}. +{"Challenge ID","ID do desafio"}. {"Change Password","Mudar palavra-chave"}. +{"Change User Password","Alterar Palavra-passe do Utilizador"}. +{"Changing password is not allowed","Não é permitida a alteração da palavra-passe"}. +{"Changing role/affiliation is not allowed","Não é permitida a alteração da função/afiliação"}. +{"Channel already exists","O canal já existe"}. +{"Channel does not exist","O canal não existe"}. +{"Channel JID","Canal JID"}. +{"Channels","Canais"}. +{"Characters not allowed:","Caracteres não aceitos:"}. +{"Chatroom configuration modified","Configuração da sala de bate-papo modificada"}. +{"Chatroom is created","A sala de chat está criada"}. +{"Chatroom is destroyed","A sala de chat está destruída"}. +{"Chatroom is started","A sala de chat está iniciada"}. +{"Chatroom is stopped","A sala de chat está parada"}. +{"Chatrooms","Salas de Chat"}. {"Choose a username and password to register with this server","Escolha um nome de utilizador e palavra-chave para se registar neste servidor"}. -{"Choose modules to stop","Seleccione os módulos a parar"}. {"Choose storage type of tables","Seleccione o tipo de armazenagem das tabelas"}. +{"Choose whether to approve this entity's subscription.","Aprovar esta assinatura."}. {"City","Cidade"}. +{"Client acknowledged more stanzas than sent by server","O cliente reconheceu mais estrofes do que as enviadas pelo servidor"}. +{"Clustering","Agrupamento"}. +{"Commands","Comandos"}. {"Conference room does not exist","A sala não existe"}. +{"Configuration of room ~s","Configuração para ~s"}. {"Configuration","Configuração"}. -{"Connected Resources:","Recursos conectados:"}. +{"Contact Addresses (normally, room owner or owners)","Endereços de contato (normalmente, o proprietário ou os proprietários da sala)"}. {"Country","País"}. -{"Delete","Eliminar"}. -{"Delete Selected","Eliminar os seleccionados"}. +{"Current Discussion Topic","Assunto em discussão"}. +{"Database failure","Falha no banco de dados"}. +{"Database Tables Configuration at ","Configuração de Tabelas de Base de dados em "}. +{"Database","Base de dados"}. +{"December","Dezembro"}. +{"Default users as participants","Utilizadores padrões como participantes"}. +{"Delete message of the day on all hosts","Apagar a mensagem do dia em todos os hosts"}. +{"Delete message of the day","Apagar mensagem do dia"}. +{"Delete User","Deletar Utilizador"}. +{"Deliver event notifications","Entregar as notificações de evento"}. +{"Deliver payloads with event notifications","Enviar payloads junto com as notificações de eventos"}. {"Disc only copy","Cópia apenas em disco"}. +{"Don't tell your password to anybody, not even the administrators of the XMPP server.","Não revele a sua palavra-passe a ninguém, nem mesmo para o administrador deste servidor XMPP."}. {"Dump Backup to Text File at ","Exporta cópia de segurança para ficheiro de texto em "}. {"Dump to Text File","Exportar para ficheiro de texto"}. +{"Duplicated groups are not allowed by RFC6121","Os grupos duplicados não são permitidos pela RFC6121"}. +{"Dynamically specify a replyto of the item publisher","Definir de forma dinâmica uma resposta da editora do item"}. {"Edit Properties","Editar propriedades"}. -{"ejabberd IRC module","Módulo de IRC ejabberd"}. +{"Either approve or decline the voice request.","Deve aprovar/desaprovar a requisição de voz."}. +{"ejabberd HTTP Upload service","serviço HTTP de upload ejabberd"}. {"ejabberd MUC module","Módulo MUC de ejabberd"}. +{"ejabberd Multicast service","Serviço multicast ejabberd"}. +{"ejabberd Publish-Subscribe module","Módulo para Publicar Tópicos do ejabberd"}. +{"ejabberd SOCKS5 Bytestreams module","Modulo ejabberd SOCKS5 Bytestreams"}. {"ejabberd vCard module","Módulo vCard de ejabberd"}. -{"Enter list of {Module, [Options]}","Introduza lista de {módulos, [opções]}"}. +{"ejabberd Web Admin","ejabberd Web Admin"}. +{"ejabberd","ejabberd"}. +{"Email Address","Endereço de e-mail"}. +{"Email","Email"}. +{"Enable hats","Ativa chapéus"}. +{"Enable logging","Permitir criação de logs"}. +{"Enable message archiving","Ativar arquivamento de mensagens"}. +{"Enabling push without 'node' attribute is not supported","Abilitar push sem o atributo 'node' não é suportado"}. +{"End User Session","Terminar Sessão do Utilizador"}. {"Enter nickname you want to register","Introduza a alcunha que quer registar"}. {"Enter path to backup file","Introduza o caminho do ficheiro de cópia de segurança"}. {"Enter path to jabberd14 spool dir","Introduza o caminho para o directório de spools do jabberd14"}. {"Enter path to jabberd14 spool file","Introduza o caminho para o ficheiro de spool do jabberd14"}. {"Enter path to text file","Introduza caminho para o ficheiro de texto"}. -{"Erlang Jabber Server","Servidor Jabber em Erlang"}. +{"Enter the text you see","Insira o texto que vê"}. +{"Erlang XMPP Server","Servidor XMPP Erlang"}. +{"Exclude Jabber IDs from CAPTCHA challenge","Excluir IDs Jabber de serem submetidos ao CAPTCHA"}. +{"Export all tables as SQL queries to a file:","Exportar todas as tabelas como SQL para um ficheiro:"}. +{"Export data of all users in the server to PIEFXIS files (XEP-0227):","Exportar todos os dados de todos os utilizadores no servidor, para ficheiros de formato PIEFXIS (XEP-0227):"}. +{"Export data of users in a host to PIEFXIS files (XEP-0227):","Exportar dados dos utilizadores num host, para ficheiros de PIEFXIS (XEP-0227):"}. +{"External component failure","Falha de componente externo"}. +{"External component timeout","Tempo esgotado à espera de componente externo"}. +{"Failed to activate bytestream","Falha ao ativar bytestream"}. +{"Failed to extract JID from your voice request approval","Não foi possível extrair o JID (Jabber ID) da requisição de voz"}. +{"Failed to map delegated namespace to external component","Falha ao mapear namespace delegado ao componente externo"}. +{"Failed to parse HTTP response","Falha ao analisar resposta HTTP"}. +{"Failed to process option '~s'","Falha ao processar opção '~s'"}. {"Family Name","Apelido"}. -{"Fill in fields to search for any matching Jabber User","Preencha os campos para procurar utilizadores Jabber coincidentes"}. -{"From","De"}. -{"From ~s","De ~s"}. +{"FAQ Entry","Registo das perguntas frequentes"}. +{"February","Fevereiro"}. +{"File larger than ~w bytes","Ficheiro é maior que ~w bytes"}. +{"Fill in the form to search for any matching XMPP User","Preencha campos para procurar por quaisquer utilizadores XMPP"}. +{"Friday","Sexta"}. +{"From ~ts","De ~s"}. +{"Full List of Room Admins","Lista completa dos administradores das salas"}. +{"Full List of Room Owners","Lista completa dos proprietários das salas"}. {"Full Name","Nome completo"}. -{"Groups","Grupos"}. -{" has set the subject to: "," colocou o tópico: "}. +{"Get List of Online Users","Obter a lista de utilizadores online"}. +{"Get List of Registered Users","Obter a lista de utilizadores registados"}. +{"Get Number of Online Users","Obter quantidade de utilizadores online"}. +{"Get Number of Registered Users","Obter quantidade de utilizadores registados"}. +{"Get Pending","Obter os pendentes"}. +{"Get User Last Login Time","Obter a data do último login"}. +{"Get User Statistics","Obter estatísticas do utilizador"}. +{"Given Name","Sobrenome"}. +{"Grant voice to this person?","Dar voz a esta pessoa?"}. +{"has been banned","foi banido"}. +{"has been kicked because of a system shutdown","foi desconectado porque o sistema foi desligado"}. +{"has been kicked because of an affiliation change","foi desconectado porque por afiliação inválida"}. +{"has been kicked because the room has been changed to members-only","foi desconectado porque a política da sala mudou, só membros são permitidos"}. +{"has been kicked","foi removido"}. +{"Hash of the vCard-temp avatar of this room","Hash do avatar do vCard-temp desta sala"}. +{"Hat title","Título do chapéu"}. +{"Hat URI","URI do chapéu"}. +{"Hats limit exceeded","O limite dos chapéus foi excedido"}. +{"Host unknown","Máquina desconhecida"}. +{"HTTP File Upload","Upload de ficheiros por HTTP"}. +{"Idle connection","Conexão inativa"}. +{"If you don't see the CAPTCHA image here, visit the web page.","Se não conseguir ver o CAPTCHA aqui, visite a web page."}. {"Import Directory","Importar directório"}. {"Import File","Importar ficheiro"}. +{"Import user data from jabberd14 spool file:","Importar dados dos utilizadores de uma fila jabberd14:"}. {"Import User from File at ","Importar utilizador a partir do ficheiro em "}. +{"Import users data from a PIEFXIS file (XEP-0227):","Importe os utilizadores de um ficheiro PIEFXIS (XEP-0227):"}. +{"Import users data from jabberd14 spool directory:","Importar dados dos utilizadores de um diretório-fila jabberd14:"}. {"Import Users from Dir at ","Importar utilizadores a partir do directório em "}. +{"Import Users From jabberd14 Spool Files","Importar utilizadores de ficheiros de jabberd14 (spool files)"}. +{"Improper domain part of 'from' attribute","Atributo 'from' contém domínio incorreto"}. {"Improper message type","Tipo de mensagem incorrecto"}. +{"Incorrect CAPTCHA submit","CAPTCHA submetido incorretamente"}. +{"Incorrect data form","Formulário dos dados incorreto"}. {"Incorrect password","Palavra-chave incorrecta"}. -{"Invalid affiliation: ~s","Afiliação inválida: ~s"}. -{"Invalid role: ~s","Papel inválido: ~s"}. -{"IRC Username","Nome do utilizador de IRC"}. +{"Incorrect value of 'action' attribute","Valor incorreto do atributo 'action'"}. +{"Incorrect value of 'action' in data form","Valor incorreto de 'action' no formulário de dados"}. +{"Incorrect value of 'path' in data form","Valor incorreto de 'path' no formulário de dados"}. +{"Installed Modules:","Módulos instalados:"}. +{"Install","Instalar"}. +{"Insufficient privilege","Privilégio insuficiente"}. +{"Internal server error","Erro interno do servidor"}. +{"Invalid 'from' attribute in forwarded message","Atributo 'from' inválido na mensagem reenviada"}. +{"Invalid node name","Nome do nó inválido"}. +{"Invalid 'previd' value","Valor 'previd' inválido"}. +{"Invitations are not allowed in this conference","Os convites não são permitidos nesta conferência"}. +{"IP addresses","Endereços IP"}. +{"is now known as","é agora conhecido como"}. +{"It is not allowed to send error messages to the room. The participant (~s) has sent an error message (~s) and got kicked from the room","Não é permitido o envio de mensagens de erro para a sala. O membro (~s) enviou uma mensagem de erro (~s) e foi expulso da sala"}. {"It is not allowed to send private messages of type \"groupchat\"","Não é permitido enviar mensagens privadas do tipo \"groupchat\""}. {"It is not allowed to send private messages to the conference","Impedir o envio de mensagens privadas para a sala"}. -{"Jabber ID ~s is invalid","O Jabber ID ~s não é válido"}. +{"Jabber ID","ID Jabber"}. +{"January","Janeiro"}. +{"JID normalization denied by service policy","Normalização JID negada por causa da política de serviços"}. +{"JID normalization failed","A normalização JID falhou"}. +{"Joined MIX channels of ~ts","Entrou no canais MIX do ~ts"}. +{"Joined MIX channels:","Uniu-se aos canais MIX:"}. +{"joins the room","Entrar na sala"}. +{"July","Julho"}. +{"June","Junho"}. +{"Just created","Acabou de ser criado"}. {"Last Activity","Última actividade"}. -{"Listened Ports at ","Portas em escuta em "}. -{"List of modules to start","Lista de módulos a iniciar"}. +{"Last login","Último login"}. +{"Last message","Última mensagem"}. +{"Last month","Último mês"}. +{"Last year","Último ano"}. +{"Least significant bits of SHA-256 hash of text should equal hexadecimal label","Bits menos significativos do hash sha-256 do texto devem ser iguais ao rótulo hexadecimal"}. +{"leaves the room","Sair da sala"}. +{"List of users with hats","Lista os utilizadores com chapéus"}. +{"List users with hats","Lista os utilizadores com chapéus"}. +{"Logged Out","Desconectado"}. +{"Logging","Registando no log"}. +{"Make participants list public","Tornar pública a lista de participantes"}. +{"Make room CAPTCHA protected","Tornar protegida a palavra-passe da sala"}. +{"Make room members-only","Tornar sala apenas para membros"}. {"Make room moderated","Tornar a sala moderada"}. -{"Memory","Memória"}. +{"Make room password protected","Tornar sala protegida à palavra-passe"}. +{"Make room persistent","Tornar sala persistente"}. +{"Make room public searchable","Tornar sala pública possível de ser encontrada"}. +{"Malformed username","Nome de utilizador inválido"}. +{"MAM preference modification denied by service policy","Modificação de preferência MAM negada por causa da política de serviços"}. +{"March","Março"}. +{"Max # of items to persist, or `max` for no specific limit other than a server imposed maximum","Máximo # de itens para persistir ou `max` para nenhum limite específico que não seja um servidor imposto como máximo"}. +{"Max payload size in bytes","Máximo tamanho do payload em bytes"}. +{"Maximum file size","Tamanho máximo do ficheiro"}. +{"Maximum Number of History Messages Returned by Room","Quantidade máxima das mensagens do histórico que foram devolvidas por sala"}. +{"Maximum number of items to persist","Quantidade máxima dos itens para manter"}. +{"Maximum Number of Occupants","Quantidate máxima de participantes"}. +{"May","Maio"}. +{"Membership is required to enter this room","É necessário ser membro desta sala para poder entrar"}. +{"Memorize your password, or write it in a paper placed in a safe place. In XMPP there isn't an automated way to recover your password if you forget it.","Memorize a sua palavra-passe ou anote-a num papel guardado num local seguro. No XMPP, não há uma maneira automatizada de recuperar a sua palavra-passe caso a esqueça."}. +{"Mere Availability in XMPP (No Show Value)","Mera disponibilidade no XMPP (Sem valor para ser exibido)"}. +{"Message body","Corpo da mensagem"}. +{"Message not found in forwarded payload","Mensagem não encontrada em conteúdo encaminhado"}. +{"Messages from strangers are rejected","As mensagens vindas de estranhos são rejeitadas"}. +{"Messages of type headline","Mensagens do tipo do título"}. +{"Messages of type normal","Mensagens do tipo normal"}. {"Middle Name","Segundo nome"}. +{"Minimum interval between voice requests (in seconds)","O intervalo mínimo entre requisições de voz (em segundos)"}. {"Moderator privileges required","São necessários privilégios de moderador"}. -{"Module","Módulo"}. -{"Modules","Módulos"}. +{"Moderator","Moderador"}. +{"Moderators Only","Somente moderadores"}. +{"Module failed to handle the query","Módulo falhou ao processar a consulta"}. +{"Monday","Segunda"}. +{"Multicast","Multicast"}. +{"Multiple elements are not allowed by RFC6121","Vários elementos não são permitidos pela RFC6121"}. +{"Multi-User Chat","Chat multi-utilizador"}. {"Name","Nome"}. +{"Natural Language for Room Discussions","Idioma nativo para as discussões na sala"}. +{"Natural-Language Room Name","Nome da sala no idioma nativo"}. +{"Neither 'jid' nor 'nick' attribute found","Nem o atributo 'jid' nem 'nick' foram encontrados"}. +{"Neither 'role' nor 'affiliation' attribute found","Nem o atributo 'role' nem 'affiliation' foram encontrados"}. {"Never","Nunca"}. -{"Nickname","Alcunha"}. +{"New Password:","Nova Palavra-passe:"}. +{"Nickname can't be empty","O apelido não pode ser vazio"}. {"Nickname Registration at ","Registo da alcunha em "}. {"Nickname ~s does not exist in the room","A alcunha ~s não existe na sala"}. -{"Node ","Nodo"}. +{"Nickname","Alcunha"}. +{"No address elements found","Nenhum elemento endereço foi encontrado"}. +{"No addresses element found","Nenhum elemento endereços foi encontrado"}. +{"No 'affiliation' attribute found","Atributo 'affiliation' não foi encontrado"}. +{"No available resource found","Nenhum recurso disponível foi encontrado"}. +{"No body provided for announce message","Nenhum corpo de texto fornecido para anunciar mensagem"}. +{"No child elements found","Nenhum elemento filho foi encontrado"}. +{"No data form found","Nenhum formulário de dados foi encontrado"}. +{"No Data","Nenhum dado"}. +{"No features available","Nenhuma funcionalidade disponível"}. +{"No element found","Nenhum elemento foi encontrado"}. +{"No hook has processed this command","Nenhum hook processou este comando"}. +{"No info about last activity found","Não foi encontrada informação sobre última atividade"}. +{"No 'item' element found","O elemento 'item' não foi encontrado"}. +{"No items found in this query","Nenhum item encontrado nesta consulta"}. +{"No limit","Ilimitado"}. +{"No module is handling this query","Nenhum módulo está processando esta consulta"}. +{"No node specified","Nenhum nó especificado"}. +{"No 'password' found in data form","'password' não foi encontrado em formulário de dados"}. +{"No 'password' found in this query","Nenhuma 'palavra-passe' foi encontrado nesta consulta"}. +{"No 'path' found in data form","'path' não foi encontrado em formulário de dados"}. +{"No pending subscriptions found","Não foram encontradas subscrições"}. +{"No privacy list with this name found","Nenhuma lista de privacidade encontrada com este nome"}. +{"No private data found in this query","Nenhum dado privado encontrado nesta consulta"}. +{"No running node found","Nenhum nó em execução foi encontrado"}. +{"No services available","Não há serviços disponíveis"}. +{"No statistics found for this item","Não foram encontradas estatísticas para este item"}. +{"No 'to' attribute found in the invitation","Atributo 'to' não foi encontrado no convite"}. +{"Nobody","Ninguém"}. +{"Node already exists","Nó já existe"}. +{"Node ID","ID do Tópico"}. +{"Node index not found","O índice do nó não foi encontrado"}. {"Node not found","Nodo não encontrado"}. +{"Node ~p","Nó ~p"}. +{"Node","Nó"}. +{"Nodeprep has failed","Processo de identificação de nó falhou (nodeprep)"}. {"Nodes","Nodos"}. {"None","Nenhum"}. -{"No resource provided","Não foi passado nenhum recurso"}. +{"Not allowed","Não é permitido"}. +{"Not Found","Não encontrado"}. +{"Not subscribed","Não subscrito"}. +{"Notify subscribers when items are removed from the node","Notificar assinantes quando itens forem eliminados do nó"}. +{"Notify subscribers when the node configuration changes","Notificar assinantes a configuração do nó mudar"}. +{"Notify subscribers when the node is deleted","Notificar assinantes quando o nó for eliminado se elimine"}. +{"November","Novembro"}. +{"Number of answers required","Quantidade de respostas necessárias"}. +{"Number of occupants","Quantidade de participantes"}. +{"Number of Offline Messages","Quantidade das mensagens offline"}. +{"Number of online users","Quantidade de utilizadores online"}. +{"Number of registered users","Quantidade de utilizadores registados"}. +{"Number of seconds after which to automatically purge items, or `max` for no specific limit other than a server imposed maximum","Quantidade de segundos após limpar automaticamente os itens ou `max` para nenhum limite específico que não seja um servidor imposto máximo"}. +{"Occupants are allowed to invite others","As pessoas estão autorizadas a convidar outras pessoas"}. +{"Occupants are allowed to query others","Os ocupantes estão autorizados a consultar os outros"}. +{"Occupants May Change the Subject","As pessoas talvez possam alterar o assunto"}. +{"October","Outubro"}. {"OK","OK"}. -{"Online","Ligado"}. +{"Old Password:","Palavra-passe Antiga:"}. {"Online Users","Utilizadores ligados"}. +{"Online","Ligado"}. +{"Only collection node owners may associate leaf nodes with the collection","Apenas um grupo dos proprietários dos nós podem associar as páginas na coleção"}. +{"Only deliver notifications to available users","Somente enviar notificações aos utilizadores disponíveis"}. +{"Only or tags are allowed","Apenas tags ou são permitidas"}. +{"Only element is allowed in this query","Apenas elemento é permitido nesta consulta"}. +{"Only members may query archives of this room","Somente os membros podem procurar nos arquivos desta sala"}. +{"Only moderators and participants are allowed to change the subject in this room","Somente os moderadores e os participamentes podem alterar o assunto desta sala"}. +{"Only moderators are allowed to change the subject in this room","Somente os moderadores podem alterar o assunto desta sala"}. +{"Only moderators are allowed to retract messages","Apenas moderadores estão autorizados de retirar mensagens"}. +{"Only moderators can approve voice requests","Somente moderadores podem aprovar requisições de voz"}. {"Only occupants are allowed to send messages to the conference","Só os ocupantes podem enviar mensagens para a sala"}. {"Only occupants are allowed to send queries to the conference","Só os ocupantes podem enviar consultas para a sala"}. +{"Only publishers may publish","Apenas os editores podem publicar"}. {"Only service administrators are allowed to send service messages","Só os administradores do serviço têm permissão para enviar mensagens de serviço"}. -{"Options","Opções"}. +{"Only those on a whitelist may associate leaf nodes with the collection","Apenas aqueles presentes numa lista branca podem associar páginas na coleção"}. +{"Only those on a whitelist may subscribe and retrieve items","Apenas aqueles presentes numa lista branca podem se inscrever e recuperar os itens"}. {"Organization Name","Nome da organização"}. {"Organization Unit","Unidade da organização"}. +{"Other Modules Available:","Outros módulos disponíveis:"}. +{"Outgoing s2s Connections","Conexões s2s de Saída"}. {"Owner privileges required","São necessários privilégios de dono"}. -{"Packet","Pacote"}. -{"Password:","Palavra-chave:"}. +{"Packet relay is denied by service policy","A retransmissão de pacote é negada por causa da política de serviço"}. +{"Participant ID","ID do participante"}. +{"Participant","Participante"}. +{"Password Verification:","Verificação da Palavra-passe:"}. +{"Password Verification","Verificação de Palavra-passe"}. {"Password","Palavra-chave"}. +{"Password:","Palavra-chave:"}. {"Path to Dir","Caminho para o directório"}. {"Path to File","Caminho do ficheiro"}. -{"Pending","Pendente"}. -{"Port","Porta"}. -{"private, ","privado"}. +{"Payload semantic type information","Informações de tipo semântico de carga útil"}. +{"Period: ","Período: "}. +{"Persist items to storage","Persistir elementos ao armazenar"}. +{"Persistent","Persistente"}. +{"Ping query is incorrect","A consulta ping está incorreta"}. +{"Ping","Ping"}. +{"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Observe que tais opções farão backup apenas da base de dados Mnesia. Caso esteja a utilizar o modulo ODBC, precisará fazer backup da sua base de dados SQL separadamente."}. +{"Please, wait for a while before sending new voice request","Por favor, espere antes de enviar uma nova requisição de voz"}. +{"Pong","Pong"}. +{"Possessing 'ask' attribute is not allowed by RFC6121","Possuir o atributo 'ask' não é permitido pela RFC6121"}. +{"Present real Jabber IDs to","Tornar o Jabber ID real visível por"}. +{"Previous session not found","A sessão anterior não foi encontrada"}. +{"Previous session PID has been killed","O PID da sessão anterior foi excluído"}. +{"Previous session PID has exited","O PID da sessão anterior foi encerrado"}. +{"Previous session PID is dead","O PID da sessão anterior está morto"}. +{"Previous session timed out","A sessão anterior expirou"}. +{"private, ","privado, "}. +{"Public","Público"}. +{"Publish model","Publicar o modelo"}. +{"Publish-Subscribe","Publicação de Tópico"}. +{"PubSub subscriber request","PubSub requisição de assinante"}. +{"Purge all items when the relevant publisher goes offline","Descartar todos os itens quando o publicante principal estiver offline"}. +{"Push record not found","O registo push não foi encontrado"}. {"Queries to the conference members are not allowed in this room","Nesta sala não são permitidas consultas aos seus membros"}. +{"Query to another users is forbidden","Consultar a outro utilizador é proibido"}. {"RAM and disc copy","Cópia em RAM e em disco"}. {"RAM copy","Cópia em RAM"}. +{"Really delete message of the day?","Deletar realmente a mensagem do dia?"}. +{"Receive notification from all descendent nodes","Receba a notificação de todos os nós descendentes"}. +{"Receive notification from direct child nodes only","Receba apenas as notificações dos nós relacionados"}. +{"Receive notification of new items only","Receba apenas as notificações dos itens novos"}. +{"Receive notification of new nodes only","Receba apenas as notificações dos nós novos"}. {"Recipient is not in the conference room","O destinatário não está na sala"}. -{"Registration in mod_irc for ","Registo no mod_irc para"}. +{"Register an XMPP account","Registe uma conta XMPP"}. +{"Register","Registar"}. {"Remote copy","Cópia remota"}. -{"Remove","Remover"}. +{"Remove a hat from a user","Remove um chapéu de um utilizador"}. {"Remove User","Eliminar utilizador"}. -{"Restart","Reiniciar"}. +{"Replaced by new connection","Substituído por nova conexão"}. +{"Request has timed out","O pedido expirou"}. +{"Request is ignored","O pedido foi ignorado"}. +{"Requested role","Função solicitada"}. +{"Resources","Recursos"}. +{"Restart Service","Reiniciar Serviço"}. {"Restore Backup from File at ","Restaura cópia de segurança a partir do ficheiro em "}. +{"Restore binary backup after next ejabberd restart (requires less memory):","Restaurar backup binário após reinicialização do ejabberd (requer menos memória):"}. +{"Restore binary backup immediately:","Restaurar imediatamente o backup binário:"}. +{"Restore plain text backup immediately:","Restaurar backup formato texto imediatamente:"}. {"Restore","Restaurar"}. +{"Result","Resultado"}. +{"Roles and Affiliations that May Retrieve Member List","As funções e as afiliações que podem recuperar a lista dos membros"}. +{"Roles for which Presence is Broadcasted","Para quem a presença será notificada"}. +{"Roles that May Send Private Messages","Atribuições que talvez possam enviar mensagens privadas"}. +{"Room Configuration","Configuração de salas"}. +{"Room creation is denied by service policy","Sala não pode ser criada devido à política do serviço"}. +{"Room description","Descrição da Sala"}. +{"Room Occupants","Quantidade de participantes"}. +{"Room terminates","Terminação da sala"}. {"Room title","Título da sala"}. -{"Roster","Lista de contactos"}. -{"Roster of ","Lista de contactos de "}. +{"Roster groups allowed to subscribe","Listar grupos autorizados"}. +{"Roster size","Tamanho da Lista"}. {"Running Nodes","Nodos a correr"}. -{"~s access rule configuration","Configuração das Regra de Acesso ~s"}. +{"~s invites you to the room ~s","~s convidaram-o à sala ~s"}. +{"Saturday","Sábado"}. +{"Search from the date","Pesquise a partir da data"}. +{"Search Results for ","Resultados de pesquisa para "}. +{"Search the text","Pesquise o texto"}. +{"Search until the date","Pesquise até a data"}. {"Search users in ","Procurar utilizadores em "}. -{"Start Modules at ","Iniciar os módulos em "}. -{"Start Modules","Iniciar módulos"}. -{"Statistics","Estatísticas"}. -{"Stop Modules at ","Parar módulos em "}. -{"Stop Modules","Parar módulos"}. -{"Stop","Parar"}. +{"Send announcement to all online users on all hosts","Enviar anúncio a todos utilizadores online em todas as máquinas"}. +{"Send announcement to all online users","Enviar anúncio a todos os utilizadorns online"}. +{"Send announcement to all users on all hosts","Enviar aviso para todos os utilizadores em todos os hosts"}. +{"Send announcement to all users","Enviar anúncio a todos os utilizadores"}. +{"September","Setembro"}. +{"Server:","Servidor:"}. +{"Service list retrieval timed out","A recuperação da lista dos serviços expirou"}. +{"Session state copying timed out","A cópia do estado da sessão expirou"}. +{"Set message of the day and send to online users","Definir mensagem do dia e enviar a todos utilizadores online"}. +{"Set message of the day on all hosts and send to online users","Definir mensagem do dia em todos os hosts e enviar para os utilizadores online"}. +{"Shared Roster Groups","Grupos Shared Roster"}. +{"Show Integral Table","Mostrar Tabela Integral"}. +{"Show Occupants Join/Leave","Mostrar a entrada e a saída de ocupantes"}. +{"Show Ordinary Table","Mostrar Tabela Ordinária"}. +{"Shut Down Service","Parar Serviço"}. +{"SOCKS5 Bytestreams","Bytestreams SOCKS5"}. +{"Some XMPP clients can store your password in the computer, but you should do this only in your personal computer for safety reasons.","Alguns clientes XMPP podem armazenar a sua palavra-passe no seu computador, só faça isso no seu computador particular por questões de segurança."}. +{"Sources Specs:","Especificações das fontes:"}. +{"Specify the access model","Especificar os modelos de acesso"}. +{"Specify the event message type","Especificar o tipo de mensagem para o evento"}. +{"Specify the publisher model","Especificar o modelo do publicante"}. +{"Stanza id is not valid","A Stanza ID é inválido"}. +{"Stanza ID","ID da estrofe"}. +{"Statically specify a replyto of the node owner(s)","Defina uma resposta fixa do(s) proprietário(s) do nó"}. {"Stopped Nodes","Nodos parados"}. -{"Storage Type","Tipo de armazenagem"}. -{"Submit","Enviar"}. -{"Subscription","Subscrição"}. -{"Time","Data"}. -{"To","Para"}. -{"To ~s","A ~s"}. -{"Update","Actualizar"}. +{"Store binary backup:","Armazenar backup binário:"}. +{"Store plain text backup:","Armazenar backup em texto:"}. +{"Stream management is already enabled","A gestão do fluxo já está ativada"}. +{"Stream management is not enabled","A gestão do fluxo não está ativada"}. +{"Subject","Assunto"}. +{"Submitted","Submetido"}. +{"Subscriber Address","Endereço dos Assinantes"}. +{"Subscribers may publish","Os assinantes podem publicar"}. +{"Subscription requests must be approved and only subscribers may retrieve items","Os pedidos de assinatura devem ser aprovados e apenas os assinantes podem recuperar os itens"}. +{"Subscriptions are not allowed","Subscrições não estão permitidas"}. +{"Sunday","Domingo"}. +{"Text associated with a picture","Um texto associado a uma imagem"}. +{"Text associated with a sound","Um texto associado a um som"}. +{"Text associated with a video","Um texto associado a um vídeo"}. +{"Text associated with speech","Um texto associado à fala"}. +{"That nickname is already in use by another occupant","O apelido (nick) já está a ser utilizado"}. +{"That nickname is registered by another person","O apelido já está registado por outra pessoa"}. +{"The account already exists","A conta já existe"}. +{"The account was not unregistered","A conta não estava não registada"}. +{"The body text of the last received message","O corpo do texto da última mensagem que foi recebida"}. +{"The CAPTCHA is valid.","O CAPTCHA é inválido."}. +{"The CAPTCHA verification has failed","A verificação do CAPTCHA falhou"}. +{"The captcha you entered is wrong","O captcha que digitou está errado"}. +{"The child nodes (leaf or collection) associated with a collection","Os nós relacionados (página ou coleção) associados com uma coleção"}. +{"The collections with which a node is affiliated","As coleções com as quais o nó está relacionado"}. +{"The DateTime at which a leased subscription will end or has ended","A data e a hora que uma assinatura alugada terminará ou terá terminado"}. +{"The datetime when the node was created","A data em que o nó foi criado"}. +{"The default language of the node","O idioma padrão do nó"}. +{"The feature requested is not supported by the conference","A funcionalidade solicitada não é suportada pela sala de conferência"}. +{"The JID of the node creator","O JID do criador do nó"}. +{"The JIDs of those to contact with questions","Os JIDs daqueles para entrar em contato com perguntas"}. +{"The JIDs of those with an affiliation of owner","Os JIDs daqueles com uma afiliação de proprietário"}. +{"The JIDs of those with an affiliation of publisher","Os JIDs daqueles com uma afiliação de editor"}. +{"The list of all online users","A lista de todos os utilizadores online"}. +{"The list of all users","A lista de todos os utilizadores"}. +{"The list of JIDs that may associate leaf nodes with a collection","A lista dos JIDs que podem associar as páginas dos nós numa coleção"}. +{"The maximum number of child nodes that can be associated with a collection, or `max` for no specific limit other than a server imposed maximum","A quantidade máxima de nós relacionados que podem ser associados a uma coleção ou `máximo` para nenhum limite específico que não seja um servidor imposto no máximo"}. +{"The minimum number of milliseconds between sending any two notification digests","A quantidade mínima de milissegundos entre o envio do resumo das duas notificações"}. +{"The name of the node","O nome do nó"}. +{"The node is a collection node","O nó é um nó da coleção"}. +{"The node is a leaf node (default)","O nó é uma página do nó (padrão)"}. +{"The NodeID of the relevant node","O NodeID do nó relevante"}. +{"The number of pending incoming presence subscription requests","A quantidade pendente dos pedidos da presença da assinatura"}. +{"The number of subscribers to the node","A quantidade dos assinantes para o nó"}. +{"The number of unread or undelivered messages","A quantidade das mensagens que não foram lidas ou não foram entregues"}. +{"The password contains unacceptable characters","A palavra-passe contém caracteres proibidos"}. +{"The password is too weak","Palavra-passe considerada muito fraca"}. +{"the password is","a palavra-passe é"}. +{"The password of your XMPP account was successfully changed.","A palavra-passe da sua conta XMPP foi alterada com sucesso."}. +{"The password was not changed","A palavra-passe não foi alterada"}. +{"The passwords are different","As palavras-passe não batem"}. +{"The presence states for which an entity wants to receive notifications","As condições da presença para os quais uma entidade queira receber as notificações"}. +{"The query is only allowed from local users","Esta consulta só é permitida a partir de utilizadores locais"}. +{"The query must not contain elements","A consulta não pode conter elementos "}. +{"The room subject can be modified by participants","O tema da sala pode ser alterada pelos próprios participantes"}. +{"The semantic type information of data in the node, usually specified by the namespace of the payload (if any)","Informações de tipo semântico dos dados no nó, geralmente especificadas pelo espaço de nomes da carga útil (se houver)"}. +{"The sender of the last received message","O remetente da última mensagem que foi recebida"}. +{"The stanza MUST contain only one element, one element, or one element","A instância DEVE conter apenas um elemento , um elemento , ou um elemento "}. +{"The subscription identifier associated with the subscription request","O identificador da assinatura associado à solicitação da assinatura"}. +{"The URL of an XSL transformation which can be applied to payloads in order to generate an appropriate message body element.","O URL da transformação XSL que pode ser aplicada nas cargas úteis para gerar um elemento apropriado no corpo da mensagem."}. +{"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","A URL de uma transformação XSL que pode ser aplicada ao formato de carga útil para gerar um Formulário de Dados válido onde o cliente possa exibir usando um mecanismo genérico de renderização do Formulários de Dados"}. +{"There was an error changing the password: ","Houve um erro ao alterar a palavra-passe: "}. +{"There was an error creating the account: ","Houve um erro ao criar esta conta: "}. +{"There was an error deleting the account: ","Houve um erro ao deletar esta conta: "}. +{"This is case insensitive: macbeth is the same that MacBeth and Macbeth.","O tamanho da caixa não importa: macbeth é o mesmo que MacBeth e 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.","Esta pagina permite a criação de novas contas XMPP neste servidor. O seu JID (Identificador Jabber) será da seguinte maneira: utilizador@servidor. Por favor, leia cuidadosamente as instruções para preencher todos os campos corretamente."}. +{"This page allows to unregister an XMPP account in this XMPP server.","Esta página permite a exclusão de uma conta XMPP neste servidor."}. +{"This room is not anonymous","Essa sala não é anônima"}. +{"This service can not process the address: ~s","Este serviço não pode processar o endereço: ~s"}. +{"Thursday","Quinta"}. +{"Time delay","Intervalo (Tempo)"}. +{"Timed out waiting for stream resumption","Tempo limite expirou durante à espera da retomada da transmissão"}. +{"To register, visit ~s","Para registar, visite ~s"}. +{"To ~ts","Para ~s"}. +{"Token TTL","Token TTL"}. +{"Too many active bytestreams","Quantidade excessiva de bytestreams ativos"}. +{"Too many CAPTCHA requests","Quantidade excessiva de requisições para o CAPTCHA"}. +{"Too many child elements","Quantidade excessiva de elementos filho"}. +{"Too many elements","Quantidade excessiva de elementos "}. +{"Too many elements","Quantidade excessiva de elementos "}. +{"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","Tentativas excessivas (~p) com falha de autenticação (~s). O endereço será desbloqueado às ~s UTC"}. +{"Too many receiver fields were specified","Foram definidos receptores demais nos campos"}. +{"Too many unacked stanzas","Quantidade excessiva de instâncias sem confirmação"}. +{"Too many users in this conference","Há uma quantidade excessiva de utilizadores nesta conferência"}. +{"Traffic rate limit is exceeded","Limite de banda excedido"}. +{"~ts's MAM Archive","Arquivo ~ts's MAM"}. +{"~ts's Offline Messages Queue","~s's Fila de Mensagens Offline"}. +{"Tuesday","Terça"}. +{"Unable to generate a CAPTCHA","Impossível gerar um CAPTCHA"}. +{"Unable to register route on existing local domain","Não foi possível registar rota no domínio local existente"}. +{"Unauthorized","Não Autorizado"}. +{"Unexpected action","Ação inesperada"}. +{"Unexpected error condition: ~p","Condição de erro inesperada: ~p"}. +{"Uninstall","Desinstalar"}. +{"Unregister an XMPP account","Excluir uma conta XMPP"}. +{"Unregister","Deletar registo"}. +{"Unsupported element","Elemento não suportado"}. +{"Unsupported version","Versão sem suporte"}. +{"Update message of the day (don't send)","Atualizar mensagem do dia (não enviar)"}. +{"Update message of the day on all hosts (don't send)","Atualizar a mensagem do dia em todos os host (não enviar)"}. +{"Update specs to get modules source, then install desired ones.","Atualize as especificações para obter a fonte dos módulos e instale os que desejar."}. +{"Update Specs","Atualizar as especificações"}. +{"Updating the vCard is not supported by the vCard storage backend","A atualização do vCard não é compatível com o back-end de armazenamento do vCard"}. +{"Upgrade","Atualização"}. +{"URL for Archived Discussion Logs","A URL para o arquivamento dos registos da discussão"}. +{"User already exists","Utilizador já existe"}. +{"User (jid)","Utilizador (jid)"}. +{"User JID","Utilizador JID"}. +{"User Management","Gestão de utilizadores"}. +{"User not allowed to perform an IQ set on another user's vCard.","O utilizador não tem permissão para executar um conjunto de QI no vCard de outro utilizador."}. +{"User removed","O utilizador foi removido"}. +{"User session not found","A sessão do utilizador não foi encontrada"}. +{"User session terminated","Sessão de utilizador terminada"}. +{"User ~ts","Utilizador ~s"}. +{"Username:","Utilizador:"}. +{"Users are not allowed to register accounts so quickly","Utilizadores não estão autorizados a registar contas imediatamente"}. +{"Users Last Activity","Últimas atividades dos utilizadores"}. {"Users","Utilizadores"}. -{"User ","Utilizador"}. {"User","Utilizador"}. +{"Value 'get' of 'type' attribute is not allowed","Valor 'get' não permitido para atributo 'type'"}. +{"Value of '~s' should be boolean","Value de '~s' deveria ser um booleano"}. +{"Value of '~s' should be datetime string","Valor de '~s' deveria ser data e hora"}. +{"Value of '~s' should be integer","Valor de '~s' deveria ser um inteiro"}. +{"Value 'set' of 'type' attribute is not allowed","Valor 'set' não permitido para atributo 'type'"}. +{"vCard User Search","Busca de Utilizador vCard"}. +{"View joined MIX channels","Exibir os canais MIX aderidos"}. +{"Virtual Hosts","Hosts virtuais"}. +{"Visitors are not allowed to change their nicknames in this room","Nesta sala, os visitantes não podem mudar os apelidos deles"}. {"Visitors are not allowed to send messages to all occupants","Os visitantes não podem enviar mensagens para todos os ocupantes"}. +{"Visitor","Visitante"}. +{"Voice request","Requisição de voz"}. +{"Voice requests are disabled in this conference","Requisições de voz estão desativadas nesta sala de conferência"}. +{"Web client which allows to join the room anonymously","Cliente da web que permite entrar na sala de forma anônima"}. +{"Wednesday","Quarta"}. +{"When a new subscription is processed and whenever a subscriber comes online","Quando uma nova assinatura é processada e sempre que um assinante fica online"}. +{"When a new subscription is processed","Quando uma nova assinatura é processada"}. +{"When to send the last published item","Quando enviar o último tópico publicado"}. +{"Whether an entity wants to receive an XMPP message body in addition to the payload format","Caso uma entidade queira receber o corpo de uma mensagem XMPP além do formato de carga útil"}. +{"Whether an entity wants to receive digests (aggregations) of notifications or all notifications individually","Caso uma entidade queira receber os resumos (as agregações) das notificações ou todas as notificações individualmente"}. +{"Whether an entity wants to receive or disable notifications","Caso uma entidade queira receber ou desativar as notificações"}. +{"Whether owners or publisher should receive replies to items","Caso os proprietários ou a editora devam receber as respostas nos itens"}. +{"Whether the node is a leaf (default) or a collection","Caso o nó seja uma folha (padrão) ou uma coleção"}. +{"Whether to allow subscriptions","Permitir subscrições"}. +{"Whether to make all subscriptions temporary, based on subscriber presence","Caso todas as assinaturas devam ser temporárias, com base na presença do assinante"}. +{"Whether to notify owners about new subscribers and unsubscribes","Caso deva notificar os proprietários sobre os novos assinantes e aqueles que cancelaram a assinatura"}. +{"Who can send private messages","Quem pode enviar mensagens privadas"}. +{"Who may associate leaf nodes with a collection","Quem pode associar as folhas dos nós numa coleção"}. +{"Wrong parameters in the web formulary","O formulário web está com os parâmetros errados"}. +{"Wrong xmlns","Xmlns errado"}. +{"XMPP Account Registration","Registo da Conta XMPP"}. +{"XMPP Domains","Domínios XMPP"}. +{"XMPP Show Value of Away","XMPP Exiba o valor da ausência"}. +{"XMPP Show Value of Chat","XMPP Exiba o valor do chat"}. +{"XMPP Show Value of DND (Do Not Disturb)","XMPP Exiba o valor do DND (Não Perturbe)"}. +{"XMPP Show Value of XA (Extended Away)","XMPP Exiba o valor do XA (Ausência Estendida)"}. +{"XMPP URI of Associated Publish-Subscribe Node","XMPP URI da publicação do nó associado da assinatura"}. +{"You are being removed from the room because of a system shutdown","Está a ser removido da sala devido ao desligamento do sistema"}. +{"You are not allowed to send private messages","Não tem permissão de enviar mensagens privadas"}. +{"You are not joined to the channel","Não está inscrito no canal"}. +{"You can later change your password using an XMPP client.","Pode alterar a sua palavra-passe mais tarde usando um cliente XMPP."}. {"You have been banned from this room","Foi banido desta sala"}. -{"You need an x:data capable client to configure mod_irc settings","É necessário um cliente com suporte de x:data para configurar as opções do mod_irc"}. -{"You need an x:data capable client to configure room","É necessário um cliente com suporte de x:data para configurar a sala"}. +{"You have joined too many conferences","Entrou em demais salas de conferência"}. +{"You must fill in field \"Nickname\" in the form","Deve completar o campo \"Apelido\" no formulário"}. +{"You need a client that supports x:data and CAPTCHA to register","Precisa de um cliente com suporte de x:data para poder registar o apelido"}. +{"You need a client that supports x:data to register the nickname","Precisa de um cliente com suporte a x:data para registar o seu apelido"}. {"You need an x:data capable client to search","É necessário um cliente com suporte de x:data para poder procurar"}. +{"Your active privacy list has denied the routing of this stanza.","A sua lista de privacidade ativa negou o roteamento desta instância."}. +{"Your contact offline message queue is full. The message has been discarded.","A fila de contatos offline esta cheia. A sua mensagem foi descartada."}. +{"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","As suas mensagens para ~s estão bloqueadas. Para desbloqueá-las, visite: ~s"}. +{"Your XMPP account was successfully registered.","A sua conta XMPP foi registada com sucesso."}. +{"Your XMPP account was successfully unregistered.","A sua conta XMPP foi excluída com sucesso."}. +{"You're not allowed to create nodes","Não tem autorização para criar nós"}. diff --git a/priv/msgs/pt.po b/priv/msgs/pt.po deleted file mode 100644 index 194bc0dd5..000000000 --- a/priv/msgs/pt.po +++ /dev/null @@ -1,2008 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: 2.1.0-alpha\n" -"Last-Translator: Iceburn\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Language: Portuguese (português)\n" - -#: ejabberd_c2s.erl:424 ejabberd_c2s.erl:727 -msgid "Use of STARTTLS required" -msgstr "" - -#: ejabberd_c2s.erl:503 -msgid "No resource provided" -msgstr "Não foi passado nenhum recurso" - -#: ejabberd_c2s.erl:1197 -msgid "Replaced by new connection" -msgstr "" - -#: ejabberd_c2s.erl:1885 -msgid "Your active privacy list has denied the routing of this stanza." -msgstr "" - -#: ejabberd_captcha.erl:96 ejabberd_captcha.erl:152 ejabberd_captcha.erl:178 -#, fuzzy -msgid "Enter the text you see" -msgstr "Introduza caminho para o ficheiro de texto" - -#: ejabberd_captcha.erl:101 -msgid "Your messages to ~s are being blocked. To unblock them, visit ~s" -msgstr "" - -#: ejabberd_captcha.erl:134 -msgid "If you don't see the CAPTCHA image here, visit the web page." -msgstr "" - -#: ejabberd_captcha.erl:146 -msgid "CAPTCHA web page" -msgstr "" - -#: ejabberd_captcha.erl:307 -msgid "The CAPTCHA is valid." -msgstr "" - -#: mod_adhoc.erl:95 mod_adhoc.erl:125 mod_adhoc.erl:143 mod_adhoc.erl:161 -msgid "Commands" -msgstr "" - -#: mod_adhoc.erl:149 mod_adhoc.erl:243 -#, fuzzy -msgid "Ping" -msgstr "Pendente" - -#: mod_adhoc.erl:260 -msgid "Pong" -msgstr "" - -#: mod_announce.erl:507 mod_announce_odbc.erl:499 -msgid "Really delete message of the day?" -msgstr "" - -#: mod_announce.erl:515 mod_announce_odbc.erl:507 mod_configure.erl:1083 -#: mod_configure.erl:1128 -#, fuzzy -msgid "Subject" -msgstr "Enviar" - -#: mod_announce.erl:520 mod_announce_odbc.erl:512 mod_configure.erl:1088 -#: mod_configure.erl:1133 -msgid "Message body" -msgstr "" - -#: mod_announce.erl:600 mod_announce_odbc.erl:592 -msgid "No body provided for announce message" -msgstr "" - -#: mod_announce.erl:635 mod_announce_odbc.erl:627 -msgid "Announcements" -msgstr "" - -#: mod_announce.erl:637 mod_announce_odbc.erl:629 -msgid "Send announcement to all users" -msgstr "" - -#: mod_announce.erl:639 mod_announce_odbc.erl:631 -msgid "Send announcement to all users on all hosts" -msgstr "" - -#: mod_announce.erl:641 mod_announce_odbc.erl:633 -msgid "Send announcement to all online users" -msgstr "" - -#: mod_announce.erl:643 mod_announce_odbc.erl:635 mod_configure.erl:1078 -#: mod_configure.erl:1123 -msgid "Send announcement to all online users on all hosts" -msgstr "" - -#: mod_announce.erl:645 mod_announce_odbc.erl:637 -msgid "Set message of the day and send to online users" -msgstr "" - -#: mod_announce.erl:647 mod_announce_odbc.erl:639 -msgid "Set message of the day on all hosts and send to online users" -msgstr "" - -#: mod_announce.erl:649 mod_announce_odbc.erl:641 -msgid "Update message of the day (don't send)" -msgstr "" - -#: mod_announce.erl:651 mod_announce_odbc.erl:643 -msgid "Update message of the day on all hosts (don't send)" -msgstr "" - -#: mod_announce.erl:653 mod_announce_odbc.erl:645 -#, fuzzy -msgid "Delete message of the day" -msgstr "Eliminar os seleccionados" - -#: mod_announce.erl:655 mod_announce_odbc.erl:647 -msgid "Delete message of the day on all hosts" -msgstr "" - -#: mod_configure.erl:114 mod_configure.erl:274 mod_configure.erl:296 -#: mod_configure.erl:498 -msgid "Configuration" -msgstr "Configuração" - -#: mod_configure.erl:125 mod_configure.erl:576 web/ejabberd_web_admin.erl:1936 -msgid "Database" -msgstr "" - -#: mod_configure.erl:127 mod_configure.erl:595 -msgid "Start Modules" -msgstr "Iniciar módulos" - -#: mod_configure.erl:129 mod_configure.erl:596 -msgid "Stop Modules" -msgstr "Parar módulos" - -#: mod_configure.erl:131 mod_configure.erl:604 web/ejabberd_web_admin.erl:1937 -msgid "Backup" -msgstr "Guardar cópia de segurança" - -#: mod_configure.erl:133 mod_configure.erl:605 -msgid "Restore" -msgstr "Restaurar" - -#: mod_configure.erl:135 mod_configure.erl:606 -msgid "Dump to Text File" -msgstr "Exportar para ficheiro de texto" - -#: mod_configure.erl:137 mod_configure.erl:615 -msgid "Import File" -msgstr "Importar ficheiro" - -#: mod_configure.erl:139 mod_configure.erl:616 -msgid "Import Directory" -msgstr "Importar directório" - -#: mod_configure.erl:141 mod_configure.erl:581 mod_configure.erl:1057 -#, fuzzy -msgid "Restart Service" -msgstr "Reiniciar" - -#: mod_configure.erl:143 mod_configure.erl:582 mod_configure.erl:1102 -msgid "Shut Down Service" -msgstr "" - -#: mod_configure.erl:145 mod_configure.erl:518 mod_configure.erl:1197 -#: web/ejabberd_web_admin.erl:1527 -msgid "Add User" -msgstr "Adicionar utilizador" - -#: mod_configure.erl:147 mod_configure.erl:519 mod_configure.erl:1219 -#, fuzzy -msgid "Delete User" -msgstr "Eliminar" - -#: mod_configure.erl:149 mod_configure.erl:520 mod_configure.erl:1231 -msgid "End User Session" -msgstr "" - -#: mod_configure.erl:151 mod_configure.erl:521 mod_configure.erl:1243 -#: mod_configure.erl:1255 -#, fuzzy -msgid "Get User Password" -msgstr "Palavra-chave" - -#: mod_configure.erl:153 mod_configure.erl:522 -#, fuzzy -msgid "Change User Password" -msgstr "Mudar palavra-chave" - -#: mod_configure.erl:155 mod_configure.erl:523 mod_configure.erl:1272 -msgid "Get User Last Login Time" -msgstr "" - -#: mod_configure.erl:157 mod_configure.erl:524 mod_configure.erl:1284 -#, fuzzy -msgid "Get User Statistics" -msgstr "Estatísticas" - -#: mod_configure.erl:159 mod_configure.erl:525 -#, fuzzy -msgid "Get Number of Registered Users" -msgstr "Utilizadores registados" - -#: mod_configure.erl:161 mod_configure.erl:526 -#, fuzzy -msgid "Get Number of Online Users" -msgstr "Utilizadores ligados" - -#: mod_configure.erl:163 mod_configure.erl:509 web/ejabberd_web_admin.erl:831 -#: web/ejabberd_web_admin.erl:872 -msgid "Access Control Lists" -msgstr "Listas de Controlo de Acesso" - -#: mod_configure.erl:165 mod_configure.erl:510 web/ejabberd_web_admin.erl:940 -#: web/ejabberd_web_admin.erl:976 -msgid "Access Rules" -msgstr "Regras de Acesso" - -#: mod_configure.erl:297 mod_configure.erl:499 -#, fuzzy -msgid "User Management" -msgstr "Gestão da BD" - -#: mod_configure.erl:500 web/ejabberd_web_admin.erl:1058 -#: web/ejabberd_web_admin.erl:1462 -msgid "Online Users" -msgstr "Utilizadores ligados" - -#: mod_configure.erl:501 -msgid "All Users" -msgstr "Todos os utilizadores" - -#: mod_configure.erl:502 -#, fuzzy -msgid "Outgoing s2s Connections" -msgstr "Conexões S2S para fora" - -#: mod_configure.erl:503 web/ejabberd_web_admin.erl:1907 -msgid "Running Nodes" -msgstr "Nodos a correr" - -#: mod_configure.erl:504 web/ejabberd_web_admin.erl:1909 -msgid "Stopped Nodes" -msgstr "Nodos parados" - -#: mod_configure.erl:577 mod_configure.erl:587 web/ejabberd_web_admin.erl:1953 -msgid "Modules" -msgstr "Módulos" - -#: mod_configure.erl:578 -msgid "Backup Management" -msgstr "Gestão de cópias de segurança" - -#: mod_configure.erl:579 -#, fuzzy -msgid "Import Users From jabberd14 Spool Files" -msgstr "Importar utilizadores a partir de ficheiros da spool do jabberd14" - -#: mod_configure.erl:699 -msgid "To ~s" -msgstr "A ~s" - -#: mod_configure.erl:717 -msgid "From ~s" -msgstr "De ~s" - -#: mod_configure.erl:913 -#, fuzzy -msgid "Database Tables Configuration at " -msgstr "Configuração de tabelas da BD em " - -#: mod_configure.erl:918 -msgid "Choose storage type of tables" -msgstr "Seleccione o tipo de armazenagem das tabelas" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Disc only copy" -msgstr "Cópia apenas em disco" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM and disc copy" -msgstr "Cópia em RAM e em disco" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM copy" -msgstr "Cópia em RAM" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Remote copy" -msgstr "Cópia remota" - -#: mod_configure.erl:950 -msgid "Stop Modules at " -msgstr "Parar módulos em " - -#: mod_configure.erl:954 -msgid "Choose modules to stop" -msgstr "Seleccione os módulos a parar" - -#: mod_configure.erl:969 -msgid "Start Modules at " -msgstr "Iniciar os módulos em " - -#: mod_configure.erl:973 -msgid "Enter list of {Module, [Options]}" -msgstr "Introduza lista de {módulos, [opções]}" - -#: mod_configure.erl:974 -msgid "List of modules to start" -msgstr "Lista de módulos a iniciar" - -#: mod_configure.erl:983 -msgid "Backup to File at " -msgstr "Guardar cópia de segurança para ficheiro em " - -#: mod_configure.erl:987 mod_configure.erl:1001 -msgid "Enter path to backup file" -msgstr "Introduza o caminho do ficheiro de cópia de segurança" - -#: mod_configure.erl:988 mod_configure.erl:1002 mod_configure.erl:1016 -#: mod_configure.erl:1030 -msgid "Path to File" -msgstr "Caminho do ficheiro" - -#: mod_configure.erl:997 -msgid "Restore Backup from File at " -msgstr "Restaura cópia de segurança a partir do ficheiro em " - -#: mod_configure.erl:1011 -msgid "Dump Backup to Text File at " -msgstr "Exporta cópia de segurança para ficheiro de texto em " - -#: mod_configure.erl:1015 -msgid "Enter path to text file" -msgstr "Introduza caminho para o ficheiro de texto" - -#: mod_configure.erl:1025 -msgid "Import User from File at " -msgstr "Importar utilizador a partir do ficheiro em " - -#: mod_configure.erl:1029 -msgid "Enter path to jabberd14 spool file" -msgstr "Introduza o caminho para o ficheiro de spool do jabberd14" - -#: mod_configure.erl:1039 -msgid "Import Users from Dir at " -msgstr "Importar utilizadores a partir do directório em " - -#: mod_configure.erl:1043 -msgid "Enter path to jabberd14 spool dir" -msgstr "Introduza o caminho para o directório de spools do jabberd14" - -#: mod_configure.erl:1044 -msgid "Path to Dir" -msgstr "Caminho para o directório" - -#: mod_configure.erl:1060 mod_configure.erl:1105 -msgid "Time delay" -msgstr "" - -#: mod_configure.erl:1143 -msgid "Access Control List Configuration" -msgstr "Configuração da Lista de Controlo de Acesso" - -#: mod_configure.erl:1147 -msgid "Access control lists" -msgstr "Listas de Controlo de Acesso" - -#: mod_configure.erl:1171 -msgid "Access Configuration" -msgstr "Configuração de acessos" - -#: mod_configure.erl:1175 -msgid "Access rules" -msgstr "Regras de acesso" - -#: mod_configure.erl:1200 mod_configure.erl:1222 mod_configure.erl:1234 -#: mod_configure.erl:1246 mod_configure.erl:1258 mod_configure.erl:1275 -#: mod_configure.erl:1287 mod_configure.erl:1650 mod_configure.erl:1700 -#: mod_configure.erl:1721 mod_roster.erl:943 mod_roster_odbc.erl:1060 -#: mod_vcard.erl:472 mod_vcard_ldap.erl:554 mod_vcard_odbc.erl:448 -msgid "Jabber ID" -msgstr "" - -#: mod_configure.erl:1205 mod_configure.erl:1263 mod_configure.erl:1651 -#: mod_configure.erl:1863 mod_muc/mod_muc_room.erl:3224 mod_register.erl:235 -#: web/ejabberd_web_admin.erl:1520 -msgid "Password" -msgstr "Palavra-chave" - -#: mod_configure.erl:1210 -msgid "Password Verification" -msgstr "" - -#: mod_configure.erl:1301 -#, fuzzy -msgid "Number of registered users" -msgstr "Utilizadores registados" - -#: mod_configure.erl:1315 -#, fuzzy -msgid "Number of online users" -msgstr "Utilizadores ligados" - -#: mod_configure.erl:1682 web/ejabberd_web_admin.erl:1588 -#: web/ejabberd_web_admin.erl:1743 -msgid "Never" -msgstr "Nunca" - -#: mod_configure.erl:1696 web/ejabberd_web_admin.erl:1601 -#: web/ejabberd_web_admin.erl:1756 -msgid "Online" -msgstr "Ligado" - -#: mod_configure.erl:1701 -msgid "Last login" -msgstr "" - -#: mod_configure.erl:1722 -#, fuzzy -msgid "Roster size" -msgstr "Lista de contactos" - -#: mod_configure.erl:1723 -msgid "IP addresses" -msgstr "" - -#: mod_configure.erl:1724 -#, fuzzy -msgid "Resources" -msgstr "Restaurar" - -#: mod_configure.erl:1850 -msgid "Administration of " -msgstr "Administração de " - -#: mod_configure.erl:1853 -msgid "Action on user" -msgstr "Acção no utilizador" - -#: mod_configure.erl:1857 -msgid "Edit Properties" -msgstr "Editar propriedades" - -#: mod_configure.erl:1860 web/ejabberd_web_admin.erl:1769 -msgid "Remove User" -msgstr "Eliminar utilizador" - -#: mod_irc/mod_irc.erl:201 mod_irc/mod_irc_odbc.erl:196 -#: mod_muc/mod_muc.erl:336 mod_muc/mod_muc_odbc.erl:342 -msgid "Access denied by service policy" -msgstr "Acesso negado pela política de serviço" - -#: mod_irc/mod_irc.erl:401 mod_irc/mod_irc_odbc.erl:398 -msgid "IRC Transport" -msgstr "" - -#: mod_irc/mod_irc.erl:428 mod_irc/mod_irc_odbc.erl:425 -msgid "ejabberd IRC module" -msgstr "Módulo de IRC ejabberd" - -#: mod_irc/mod_irc.erl:559 mod_irc/mod_irc_odbc.erl:568 -msgid "You need an x:data capable client to configure mod_irc settings" -msgstr "" -"É necessário um cliente com suporte de x:data para configurar as opções do " -"mod_irc" - -#: mod_irc/mod_irc.erl:566 mod_irc/mod_irc_odbc.erl:575 -msgid "Registration in mod_irc for " -msgstr "Registo no mod_irc para" - -#: mod_irc/mod_irc.erl:571 mod_irc/mod_irc_odbc.erl:580 -#, fuzzy -msgid "" -"Enter username, encodings, ports and passwords you wish to use for " -"connecting to IRC servers" -msgstr "" -"Introduza o nome de utilizador e codificações de caracteres que quer usar ao " -"conectar-se aos servidores de IRC" - -#: mod_irc/mod_irc.erl:576 mod_irc/mod_irc_odbc.erl:585 -msgid "IRC Username" -msgstr "Nome do utilizador de IRC" - -#: mod_irc/mod_irc.erl:586 mod_irc/mod_irc_odbc.erl:595 -#, fuzzy -msgid "" -"If you want to specify different ports, passwords, encodings for IRC " -"servers, fill this list with values in format '{\"irc server\", \"encoding" -"\", port, \"password\"}'. By default this service use \"~s\" encoding, port " -"~p, empty password." -msgstr "" -"Se deseja especificar codificações de caracteres diferentes para cada " -"servidor IRC preencha esta lista con valores no formato '{\"servidor irc\", " -"\"codificação\"}'. Este serviço usa por omissão a codificação \"~s\"." - -#: mod_irc/mod_irc.erl:598 mod_irc/mod_irc_odbc.erl:607 -msgid "" -"Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef." -"net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]." -msgstr "" - -#: mod_irc/mod_irc.erl:603 mod_irc/mod_irc_odbc.erl:612 -msgid "Connections parameters" -msgstr "" - -#: mod_irc/mod_irc.erl:728 mod_irc/mod_irc_odbc.erl:757 -msgid "Join IRC channel" -msgstr "" - -#: mod_irc/mod_irc.erl:732 mod_irc/mod_irc_odbc.erl:761 -msgid "IRC channel (don't put the first #)" -msgstr "" - -#: mod_irc/mod_irc.erl:737 mod_irc/mod_irc_odbc.erl:766 -#, fuzzy -msgid "IRC server" -msgstr "Nome do utilizador de IRC" - -#: mod_irc/mod_irc.erl:770 mod_irc/mod_irc.erl:774 -#: mod_irc/mod_irc_odbc.erl:799 mod_irc/mod_irc_odbc.erl:803 -msgid "Join the IRC channel here." -msgstr "" - -#: mod_irc/mod_irc.erl:778 mod_irc/mod_irc_odbc.erl:807 -msgid "Join the IRC channel in this Jabber ID: ~s" -msgstr "" - -#: mod_irc/mod_irc.erl:863 mod_irc/mod_irc_odbc.erl:904 -msgid "IRC settings" -msgstr "" - -#: mod_irc/mod_irc.erl:868 mod_irc/mod_irc_odbc.erl:909 -#, fuzzy -msgid "" -"Enter username and encodings you wish to use for connecting to IRC servers. " -"Press 'Next' to get more fields to fill in. Press 'Complete' to save " -"settings." -msgstr "" -"Introduza o nome de utilizador e codificações de caracteres que quer usar ao " -"conectar-se aos servidores de IRC" - -#: mod_irc/mod_irc.erl:874 mod_irc/mod_irc_odbc.erl:915 -#, fuzzy -msgid "IRC username" -msgstr "Nome do utilizador de IRC" - -#: mod_irc/mod_irc.erl:923 mod_irc/mod_irc_odbc.erl:964 -#, fuzzy -msgid "Password ~b" -msgstr "Palavra-chave" - -#: mod_irc/mod_irc.erl:928 mod_irc/mod_irc_odbc.erl:969 -#, fuzzy -msgid "Port ~b" -msgstr "Porta" - -#: mod_irc/mod_irc.erl:933 mod_irc/mod_irc_odbc.erl:974 -msgid "Encoding for server ~b" -msgstr "" - -#: mod_irc/mod_irc.erl:942 mod_irc/mod_irc_odbc.erl:983 -msgid "Server ~b" -msgstr "" - -#: mod_muc/mod_muc.erl:449 mod_muc/mod_muc_odbc.erl:456 -msgid "Only service administrators are allowed to send service messages" -msgstr "" -"Só os administradores do serviço têm permissão para enviar mensagens de " -"serviço" - -#: mod_muc/mod_muc.erl:493 mod_muc/mod_muc_odbc.erl:500 -#, fuzzy -msgid "Room creation is denied by service policy" -msgstr "Acesso negado pela política de serviço" - -#: mod_muc/mod_muc.erl:500 mod_muc/mod_muc_odbc.erl:507 -msgid "Conference room does not exist" -msgstr "A sala não existe" - -#: mod_muc/mod_muc.erl:582 mod_muc/mod_muc_odbc.erl:595 -msgid "Chatrooms" -msgstr "" - -#: mod_muc/mod_muc.erl:712 mod_muc/mod_muc_odbc.erl:729 -#, fuzzy -msgid "You need a client that supports x:data to register the nickname" -msgstr "" -"É necessário um cliente com suporte de x:data para poder registar a alcunha" - -#: mod_muc/mod_muc.erl:718 mod_muc/mod_muc_odbc.erl:735 -msgid "Nickname Registration at " -msgstr "Registo da alcunha em " - -#: mod_muc/mod_muc.erl:722 mod_muc/mod_muc_odbc.erl:739 -msgid "Enter nickname you want to register" -msgstr "Introduza a alcunha que quer registar" - -#: mod_muc/mod_muc.erl:723 mod_muc/mod_muc_odbc.erl:740 -#: mod_muc/mod_muc_room.erl:3810 mod_roster.erl:944 mod_roster_odbc.erl:1061 -#: mod_vcard.erl:364 mod_vcard.erl:477 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:453 -msgid "Nickname" -msgstr "Alcunha" - -#: mod_muc/mod_muc.erl:762 mod_muc/mod_muc_odbc.erl:784 -#: mod_muc/mod_muc_room.erl:1064 mod_muc/mod_muc_room.erl:1787 -#, fuzzy -msgid "That nickname is registered by another person" -msgstr "A alcunha já está registada por outra pessoa" - -#: mod_muc/mod_muc.erl:788 mod_muc/mod_muc_odbc.erl:811 -#, fuzzy -msgid "You must fill in field \"Nickname\" in the form" -msgstr "Deve preencher o campo \"alcunha\" no formulário" - -#: mod_muc/mod_muc.erl:808 mod_muc/mod_muc_odbc.erl:831 -msgid "ejabberd MUC module" -msgstr "Módulo MUC de ejabberd" - -#: mod_muc/mod_muc_log.erl:374 mod_muc/mod_muc_log.erl:381 -#, fuzzy -msgid "Chatroom configuration modified" -msgstr "Configuração para " - -#: mod_muc/mod_muc_log.erl:384 -msgid "joins the room" -msgstr "" - -#: mod_muc/mod_muc_log.erl:387 mod_muc/mod_muc_log.erl:390 -msgid "leaves the room" -msgstr "" - -#: mod_muc/mod_muc_log.erl:393 mod_muc/mod_muc_log.erl:396 -msgid "has been banned" -msgstr "" - -#: mod_muc/mod_muc_log.erl:399 mod_muc/mod_muc_log.erl:402 -msgid "has been kicked" -msgstr "" - -#: mod_muc/mod_muc_log.erl:405 -msgid "has been kicked because of an affiliation change" -msgstr "" - -#: mod_muc/mod_muc_log.erl:408 -msgid "has been kicked because the room has been changed to members-only" -msgstr "" - -#: mod_muc/mod_muc_log.erl:411 -msgid "has been kicked because of a system shutdown" -msgstr "" - -#: mod_muc/mod_muc_log.erl:414 -msgid "is now known as" -msgstr "" - -#: mod_muc/mod_muc_log.erl:417 mod_muc/mod_muc_log.erl:688 -#: mod_muc/mod_muc_room.erl:2393 -msgid " has set the subject to: " -msgstr " colocou o tópico: " - -#: mod_muc/mod_muc_log.erl:452 -#, fuzzy -msgid "Chatroom is created" -msgstr "Configuração para " - -#: mod_muc/mod_muc_log.erl:453 -msgid "Chatroom is destroyed" -msgstr "" - -#: mod_muc/mod_muc_log.erl:454 -msgid "Chatroom is started" -msgstr "" - -#: mod_muc/mod_muc_log.erl:455 -msgid "Chatroom is stopped" -msgstr "" - -#: mod_muc/mod_muc_log.erl:459 -msgid "Monday" -msgstr "" - -#: mod_muc/mod_muc_log.erl:460 -msgid "Tuesday" -msgstr "" - -#: mod_muc/mod_muc_log.erl:461 -msgid "Wednesday" -msgstr "" - -#: mod_muc/mod_muc_log.erl:462 -msgid "Thursday" -msgstr "" - -#: mod_muc/mod_muc_log.erl:463 -msgid "Friday" -msgstr "" - -#: mod_muc/mod_muc_log.erl:464 -msgid "Saturday" -msgstr "" - -#: mod_muc/mod_muc_log.erl:465 -msgid "Sunday" -msgstr "" - -#: mod_muc/mod_muc_log.erl:469 -msgid "January" -msgstr "" - -#: mod_muc/mod_muc_log.erl:470 -msgid "February" -msgstr "" - -#: mod_muc/mod_muc_log.erl:471 -msgid "March" -msgstr "" - -#: mod_muc/mod_muc_log.erl:472 -msgid "April" -msgstr "" - -#: mod_muc/mod_muc_log.erl:473 -msgid "May" -msgstr "" - -#: mod_muc/mod_muc_log.erl:474 -msgid "June" -msgstr "" - -#: mod_muc/mod_muc_log.erl:475 -msgid "July" -msgstr "" - -#: mod_muc/mod_muc_log.erl:476 -msgid "August" -msgstr "" - -#: mod_muc/mod_muc_log.erl:477 -msgid "September" -msgstr "" - -#: mod_muc/mod_muc_log.erl:478 -msgid "October" -msgstr "" - -#: mod_muc/mod_muc_log.erl:479 -#, fuzzy -msgid "November" -msgstr "Nunca" - -#: mod_muc/mod_muc_log.erl:480 -msgid "December" -msgstr "" - -#: mod_muc/mod_muc_log.erl:750 -#, fuzzy -msgid "Room Configuration" -msgstr "Configuração" - -#: mod_muc/mod_muc_log.erl:759 -msgid "Room Occupants" -msgstr "" - -#: mod_muc/mod_muc_room.erl:174 -msgid "Traffic rate limit is exceeded" -msgstr "" - -#: mod_muc/mod_muc_room.erl:246 -msgid "" -"This participant is kicked from the room because he sent an error message" -msgstr "" - -#: mod_muc/mod_muc_room.erl:255 -msgid "It is not allowed to send private messages to the conference" -msgstr "Impedir o envio de mensagens privadas para a sala" - -#: mod_muc/mod_muc_room.erl:332 -msgid "Please, wait for a while before sending new voice request" -msgstr "" - -#: mod_muc/mod_muc_room.erl:347 -msgid "Voice requests are disabled in this conference" -msgstr "" - -#: mod_muc/mod_muc_room.erl:364 -msgid "Failed to extract JID from your voice request approval" -msgstr "" - -#: mod_muc/mod_muc_room.erl:393 -msgid "Only moderators can approve voice requests" -msgstr "" - -#: mod_muc/mod_muc_room.erl:408 -msgid "Improper message type" -msgstr "Tipo de mensagem incorrecto" - -#: mod_muc/mod_muc_room.erl:518 -msgid "" -"This participant is kicked from the room because he sent an error message to " -"another participant" -msgstr "" - -#: mod_muc/mod_muc_room.erl:531 -msgid "It is not allowed to send private messages of type \"groupchat\"" -msgstr "Não é permitido enviar mensagens privadas do tipo \"groupchat\"" - -#: mod_muc/mod_muc_room.erl:543 mod_muc/mod_muc_room.erl:611 -msgid "Recipient is not in the conference room" -msgstr "O destinatário não está na sala" - -#: mod_muc/mod_muc_room.erl:564 mod_muc/mod_muc_room.erl:585 -#, fuzzy -msgid "It is not allowed to send private messages" -msgstr "Impedir o envio de mensagens privadas para a sala" - -#: mod_muc/mod_muc_room.erl:576 mod_muc/mod_muc_room.erl:956 -#: mod_muc/mod_muc_room.erl:4040 -msgid "Only occupants are allowed to send messages to the conference" -msgstr "Só os ocupantes podem enviar mensagens para a sala" - -#: mod_muc/mod_muc_room.erl:634 -msgid "Only occupants are allowed to send queries to the conference" -msgstr "Só os ocupantes podem enviar consultas para a sala" - -#: mod_muc/mod_muc_room.erl:646 -msgid "Queries to the conference members are not allowed in this room" -msgstr "Nesta sala não são permitidas consultas aos seus membros" - -#: mod_muc/mod_muc_room.erl:932 -#, fuzzy -msgid "" -"Only moderators and participants are allowed to change the subject in this " -"room" -msgstr "Só os moderadores e os participantes podem mudar o tópico desta sala" - -#: mod_muc/mod_muc_room.erl:937 -#, fuzzy -msgid "Only moderators are allowed to change the subject in this room" -msgstr "Só os moderadores podem mudar o tópico desta sala" - -#: mod_muc/mod_muc_room.erl:947 -msgid "Visitors are not allowed to send messages to all occupants" -msgstr "Os visitantes não podem enviar mensagens para todos os ocupantes" - -#: mod_muc/mod_muc_room.erl:1021 -msgid "" -"This participant is kicked from the room because he sent an error presence" -msgstr "" - -#: mod_muc/mod_muc_room.erl:1040 -#, fuzzy -msgid "Visitors are not allowed to change their nicknames in this room" -msgstr "Só os moderadores podem mudar o tópico desta sala" - -#: mod_muc/mod_muc_room.erl:1053 mod_muc/mod_muc_room.erl:1779 -#, fuzzy -msgid "That nickname is already in use by another occupant" -msgstr "A alcunha já está a ser usado por outro ocupante" - -#: mod_muc/mod_muc_room.erl:1768 -msgid "You have been banned from this room" -msgstr "Foi banido desta sala" - -#: mod_muc/mod_muc_room.erl:1771 -#, fuzzy -msgid "Membership is required to enter this room" -msgstr "É necessário ser membro desta sala para poder entrar" - -#: mod_muc/mod_muc_room.erl:1807 -#, fuzzy -msgid "This room is not anonymous" -msgstr "Tornar a sala anónima?" - -#: mod_muc/mod_muc_room.erl:1833 -#, fuzzy -msgid "A password is required to enter this room" -msgstr "É necessária a palavra-chave para poder entrar nesta sala" - -#: mod_muc/mod_muc_room.erl:1855 mod_register.erl:246 -msgid "Too many CAPTCHA requests" -msgstr "" - -#: mod_muc/mod_muc_room.erl:1864 mod_register.erl:251 -msgid "Unable to generate a CAPTCHA" -msgstr "" - -#: mod_muc/mod_muc_room.erl:1874 -msgid "Incorrect password" -msgstr "Palavra-chave incorrecta" - -#: mod_muc/mod_muc_room.erl:2448 -msgid "Administrator privileges required" -msgstr "São necessários privilégios de administrador" - -#: mod_muc/mod_muc_room.erl:2463 -msgid "Moderator privileges required" -msgstr "São necessários privilégios de moderador" - -#: mod_muc/mod_muc_room.erl:2619 -msgid "Jabber ID ~s is invalid" -msgstr "O Jabber ID ~s não é válido" - -#: mod_muc/mod_muc_room.erl:2633 -msgid "Nickname ~s does not exist in the room" -msgstr "A alcunha ~s não existe na sala" - -#: mod_muc/mod_muc_room.erl:2659 mod_muc/mod_muc_room.erl:3049 -msgid "Invalid affiliation: ~s" -msgstr "Afiliação inválida: ~s" - -#: mod_muc/mod_muc_room.erl:2713 -msgid "Invalid role: ~s" -msgstr "Papel inválido: ~s" - -#: mod_muc/mod_muc_room.erl:3026 mod_muc/mod_muc_room.erl:3062 -msgid "Owner privileges required" -msgstr "São necessários privilégios de dono" - -#: mod_muc/mod_muc_room.erl:3195 -#, fuzzy -msgid "Configuration of room ~s" -msgstr "Configuração para " - -#: mod_muc/mod_muc_room.erl:3200 -msgid "Room title" -msgstr "Título da sala" - -#: mod_muc/mod_muc_room.erl:3203 mod_muc/mod_muc_room.erl:3692 -#, fuzzy -msgid "Room description" -msgstr "Subscrição" - -#: mod_muc/mod_muc_room.erl:3210 -#, fuzzy -msgid "Make room persistent" -msgstr "Tornar a sala permanente?" - -#: mod_muc/mod_muc_room.erl:3215 -#, fuzzy -msgid "Make room public searchable" -msgstr "Tornar a sala publicamente visível?" - -#: mod_muc/mod_muc_room.erl:3218 -#, fuzzy -msgid "Make participants list public" -msgstr "Tornar pública a lista de participantes?" - -#: mod_muc/mod_muc_room.erl:3221 -#, fuzzy -msgid "Make room password protected" -msgstr "Proteger a sala com palavra-chave?" - -#: mod_muc/mod_muc_room.erl:3232 -msgid "Maximum Number of Occupants" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3239 -msgid "No limit" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3250 -msgid "Present real Jabber IDs to" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3258 mod_muc/mod_muc_room.erl:3292 -msgid "moderators only" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3260 mod_muc/mod_muc_room.erl:3294 -#, fuzzy -msgid "anyone" -msgstr "Nenhum" - -#: mod_muc/mod_muc_room.erl:3262 -#, fuzzy -msgid "Make room members-only" -msgstr "Tornar a sala exclusiva a membros?" - -#: mod_muc/mod_muc_room.erl:3265 -msgid "Make room moderated" -msgstr "Tornar a sala moderada" - -#: mod_muc/mod_muc_room.erl:3268 -#, fuzzy -msgid "Default users as participants" -msgstr "Os utilizadores são membros por omissão?" - -#: mod_muc/mod_muc_room.erl:3271 -#, fuzzy -msgid "Allow users to change the subject" -msgstr "Permitir aos utilizadores mudar o tópico?" - -#: mod_muc/mod_muc_room.erl:3274 -#, fuzzy -msgid "Allow users to send private messages" -msgstr "Permitir que os utilizadores enviem mensagens privadas?" - -#: mod_muc/mod_muc_room.erl:3279 -#, fuzzy -msgid "Allow visitors to send private messages to" -msgstr "Permitir que os utilizadores enviem mensagens privadas?" - -#: mod_muc/mod_muc_room.erl:3290 -msgid "nobody" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3296 -#, fuzzy -msgid "Allow users to query other users" -msgstr "Permitir aos utilizadores consultar outros utilizadores?" - -#: mod_muc/mod_muc_room.erl:3299 -#, fuzzy -msgid "Allow users to send invites" -msgstr "Permitir que os utilizadores enviem convites?" - -#: mod_muc/mod_muc_room.erl:3302 -msgid "Allow visitors to send status text in presence updates" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3305 -#, fuzzy -msgid "Allow visitors to change nickname" -msgstr "Permitir aos utilizadores mudar o tópico?" - -#: mod_muc/mod_muc_room.erl:3308 -#, fuzzy -msgid "Allow visitors to send voice requests" -msgstr "Permitir que os utilizadores enviem convites?" - -#: mod_muc/mod_muc_room.erl:3311 -msgid "Minimum interval between voice requests (in seconds)" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3317 -#, fuzzy -msgid "Make room CAPTCHA protected" -msgstr "Proteger a sala com palavra-chave?" - -#: mod_muc/mod_muc_room.erl:3322 -msgid "Exclude Jabber IDs from CAPTCHA challenge" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3329 -#, fuzzy -msgid "Enable logging" -msgstr "Guardar históricos?" - -#: mod_muc/mod_muc_room.erl:3337 -msgid "You need an x:data capable client to configure room" -msgstr "É necessário um cliente com suporte de x:data para configurar a sala" - -#: mod_muc/mod_muc_room.erl:3694 -msgid "Number of occupants" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3750 -msgid "private, " -msgstr "privado" - -#: mod_muc/mod_muc_room.erl:3799 -msgid "Voice request" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3803 -msgid "Either approve or decline the voice request." -msgstr "" - -#: mod_muc/mod_muc_room.erl:3809 -#, fuzzy -msgid "User JID" -msgstr "Utilizador" - -#: mod_muc/mod_muc_room.erl:3811 -msgid "Grant voice to this person?" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3960 -msgid "~s invites you to the room ~s" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3969 -#, fuzzy -msgid "the password is" -msgstr "Mudar palavra-chave" - -#: mod_offline.erl:510 mod_offline_odbc.erl:352 -msgid "" -"Your contact offline message queue is full. The message has been discarded." -msgstr "" - -#: mod_offline.erl:560 mod_offline_odbc.erl:408 -#, fuzzy -msgid "~s's Offline Messages Queue" -msgstr "~s fila de mensagens diferidas" - -#: mod_offline.erl:563 mod_offline_odbc.erl:411 mod_roster.erl:987 -#: mod_roster_odbc.erl:1104 mod_shared_roster.erl:901 -#: mod_shared_roster.erl:1010 mod_shared_roster_odbc.erl:930 -#: mod_shared_roster_odbc.erl:1039 web/ejabberd_web_admin.erl:833 -#: web/ejabberd_web_admin.erl:874 web/ejabberd_web_admin.erl:942 -#: web/ejabberd_web_admin.erl:978 web/ejabberd_web_admin.erl:1019 -#: web/ejabberd_web_admin.erl:1508 web/ejabberd_web_admin.erl:1760 -#: web/ejabberd_web_admin.erl:1931 web/ejabberd_web_admin.erl:1963 -#: web/ejabberd_web_admin.erl:2031 web/ejabberd_web_admin.erl:2135 -#: web/ejabberd_web_admin.erl:2160 web/ejabberd_web_admin.erl:2248 -#, fuzzy -msgid "Submitted" -msgstr "enviado" - -#: mod_offline.erl:571 -msgid "Time" -msgstr "Data" - -#: mod_offline.erl:572 -msgid "From" -msgstr "De" - -#: mod_offline.erl:573 -msgid "To" -msgstr "Para" - -#: mod_offline.erl:574 mod_offline_odbc.erl:419 -msgid "Packet" -msgstr "Pacote" - -#: mod_offline.erl:587 mod_offline_odbc.erl:432 mod_shared_roster.erl:908 -#: mod_shared_roster_odbc.erl:937 web/ejabberd_web_admin.erl:882 -#: web/ejabberd_web_admin.erl:986 -msgid "Delete Selected" -msgstr "Eliminar os seleccionados" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -#, fuzzy -msgid "Offline Messages:" -msgstr "Mensagens diferidas:" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -#, fuzzy -msgid "Remove All Offline Messages" -msgstr "Mensagens diferidas" - -#: mod_proxy65/mod_proxy65_service.erl:213 -#, fuzzy -msgid "ejabberd SOCKS5 Bytestreams module" -msgstr "Módulo vCard de ejabberd" - -#: mod_pubsub/mod_pubsub.erl:1120 mod_pubsub/mod_pubsub_odbc.erl:929 -msgid "Publish-Subscribe" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:1214 mod_pubsub/mod_pubsub_odbc.erl:1025 -#, fuzzy -msgid "ejabberd Publish-Subscribe module" -msgstr "Módulo pub/sub de ejabberd" - -#: mod_pubsub/mod_pubsub.erl:1497 mod_pubsub/mod_pubsub_odbc.erl:1312 -msgid "PubSub subscriber request" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:1499 mod_pubsub/mod_pubsub_odbc.erl:1314 -msgid "Choose whether to approve this entity's subscription." -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:1505 mod_pubsub/mod_pubsub_odbc.erl:1320 -#, fuzzy -msgid "Node ID" -msgstr "Nodo" - -#: mod_pubsub/mod_pubsub.erl:1510 mod_pubsub/mod_pubsub_odbc.erl:1325 -msgid "Subscriber Address" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:1516 mod_pubsub/mod_pubsub_odbc.erl:1331 -msgid "Allow this Jabber ID to subscribe to this pubsub node?" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:3474 mod_pubsub/mod_pubsub_odbc.erl:3287 -msgid "Deliver payloads with event notifications" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:3475 mod_pubsub/mod_pubsub_odbc.erl:3288 -msgid "Deliver event notifications" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:3476 mod_pubsub/mod_pubsub_odbc.erl:3289 -msgid "Notify subscribers when the node configuration changes" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:3477 mod_pubsub/mod_pubsub_odbc.erl:3290 -msgid "Notify subscribers when the node is deleted" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:3478 mod_pubsub/mod_pubsub_odbc.erl:3291 -msgid "Notify subscribers when items are removed from the node" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:3479 mod_pubsub/mod_pubsub_odbc.erl:3292 -msgid "Persist items to storage" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:3480 mod_pubsub/mod_pubsub_odbc.erl:3293 -msgid "A friendly name for the node" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:3481 mod_pubsub/mod_pubsub_odbc.erl:3294 -msgid "Max # of items to persist" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:3482 mod_pubsub/mod_pubsub_odbc.erl:3295 -msgid "Whether to allow subscriptions" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:3483 mod_pubsub/mod_pubsub_odbc.erl:3296 -msgid "Specify the access model" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:3486 mod_pubsub/mod_pubsub_odbc.erl:3299 -msgid "Roster groups allowed to subscribe" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:3487 mod_pubsub/mod_pubsub_odbc.erl:3300 -msgid "Specify the publisher model" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:3489 mod_pubsub/mod_pubsub_odbc.erl:3302 -msgid "Purge all items when the relevant publisher goes offline" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:3490 mod_pubsub/mod_pubsub_odbc.erl:3303 -msgid "Specify the event message type" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:3492 mod_pubsub/mod_pubsub_odbc.erl:3305 -msgid "Max payload size in bytes" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:3493 mod_pubsub/mod_pubsub_odbc.erl:3306 -msgid "When to send the last published item" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:3495 mod_pubsub/mod_pubsub_odbc.erl:3308 -msgid "Only deliver notifications to available users" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:3496 mod_pubsub/mod_pubsub_odbc.erl:3309 -msgid "The collections with which a node is affiliated" -msgstr "" - -#: mod_register.erl:193 -msgid "The CAPTCHA verification has failed" -msgstr "" - -#: mod_register.erl:220 -#, fuzzy -msgid "You need a client that supports x:data and CAPTCHA to register" -msgstr "" -"É necessário um cliente com suporte de x:data para poder registar a alcunha" - -#: mod_register.erl:226 mod_register.erl:265 -msgid "Choose a username and password to register with this server" -msgstr "" -"Escolha um nome de utilizador e palavra-chave para se registar neste servidor" - -#: mod_register.erl:230 mod_vcard.erl:364 mod_vcard_odbc.erl:342 -#: web/ejabberd_web_admin.erl:1515 web/ejabberd_web_admin.erl:1572 -msgid "User" -msgstr "Utilizador" - -#: mod_register.erl:316 mod_register.erl:361 -#, fuzzy -msgid "The password is too weak" -msgstr "Mudar palavra-chave" - -#: mod_register.erl:365 -#, fuzzy -msgid "Users are not allowed to register accounts so quickly" -msgstr "Os visitantes não podem enviar mensagens para todos os ocupantes" - -#: mod_roster.erl:938 mod_roster_odbc.erl:1055 web/ejabberd_web_admin.erl:1701 -#: web/ejabberd_web_admin.erl:1886 web/ejabberd_web_admin.erl:1897 -#: web/ejabberd_web_admin.erl:2219 -msgid "None" -msgstr "Nenhum" - -#: mod_roster.erl:945 mod_roster_odbc.erl:1062 -msgid "Subscription" -msgstr "Subscrição" - -#: mod_roster.erl:946 mod_roster_odbc.erl:1063 -msgid "Pending" -msgstr "Pendente" - -#: mod_roster.erl:947 mod_roster_odbc.erl:1064 -msgid "Groups" -msgstr "Grupos" - -#: mod_roster.erl:974 mod_roster_odbc.erl:1091 -msgid "Validate" -msgstr "" - -#: mod_roster.erl:982 mod_roster_odbc.erl:1099 -msgid "Remove" -msgstr "Remover" - -#: mod_roster.erl:985 mod_roster_odbc.erl:1102 -msgid "Roster of " -msgstr "Lista de contactos de " - -#: mod_roster.erl:988 mod_roster_odbc.erl:1105 mod_shared_roster.erl:902 -#: mod_shared_roster.erl:1011 mod_shared_roster_odbc.erl:931 -#: mod_shared_roster_odbc.erl:1040 web/ejabberd_web_admin.erl:834 -#: web/ejabberd_web_admin.erl:875 web/ejabberd_web_admin.erl:943 -#: web/ejabberd_web_admin.erl:979 web/ejabberd_web_admin.erl:1020 -#: web/ejabberd_web_admin.erl:1509 web/ejabberd_web_admin.erl:1761 -#: web/ejabberd_web_admin.erl:1932 web/ejabberd_web_admin.erl:2136 -#: web/ejabberd_web_admin.erl:2161 -#, fuzzy -msgid "Bad format" -msgstr "formato inválido" - -#: mod_roster.erl:995 mod_roster_odbc.erl:1112 -#, fuzzy -msgid "Add Jabber ID" -msgstr "Adicionar Jabber ID" - -#: mod_roster.erl:1094 mod_roster_odbc.erl:1211 -msgid "Roster" -msgstr "Lista de contactos" - -#: mod_shared_roster.erl:857 mod_shared_roster.erl:899 -#: mod_shared_roster.erl:1007 mod_shared_roster_odbc.erl:886 -#: mod_shared_roster_odbc.erl:928 mod_shared_roster_odbc.erl:1036 -#, fuzzy -msgid "Shared Roster Groups" -msgstr "Lista de contactos partilhada" - -#: mod_shared_roster.erl:895 mod_shared_roster_odbc.erl:924 -#: web/ejabberd_web_admin.erl:1365 web/ejabberd_web_admin.erl:2461 -msgid "Add New" -msgstr "Adicionar novo" - -#: mod_shared_roster.erl:978 mod_shared_roster_odbc.erl:1007 -#, fuzzy -msgid "Name:" -msgstr "Nome" - -#: mod_shared_roster.erl:983 mod_shared_roster_odbc.erl:1012 -#, fuzzy -msgid "Description:" -msgstr "Subscrição" - -#: mod_shared_roster.erl:991 mod_shared_roster_odbc.erl:1020 -msgid "Members:" -msgstr "" - -#: mod_shared_roster.erl:999 mod_shared_roster_odbc.erl:1028 -msgid "Displayed Groups:" -msgstr "" - -#: mod_shared_roster.erl:1008 mod_shared_roster_odbc.erl:1037 -#, fuzzy -msgid "Group " -msgstr "Grupos" - -#: mod_shared_roster.erl:1017 mod_shared_roster_odbc.erl:1046 -#: web/ejabberd_web_admin.erl:840 web/ejabberd_web_admin.erl:884 -#: web/ejabberd_web_admin.erl:949 web/ejabberd_web_admin.erl:1026 -#: web/ejabberd_web_admin.erl:2017 -msgid "Submit" -msgstr "Enviar" - -#: mod_vcard.erl:165 mod_vcard_ldap.erl:238 mod_vcard_odbc.erl:129 -msgid "Erlang Jabber Server" -msgstr "Servidor Jabber em Erlang" - -#: mod_vcard.erl:364 mod_vcard.erl:478 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:454 -msgid "Birthday" -msgstr "Data de nascimento" - -#: mod_vcard.erl:364 mod_vcard.erl:480 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:456 -msgid "City" -msgstr "Cidade" - -#: mod_vcard.erl:364 mod_vcard.erl:479 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:455 -msgid "Country" -msgstr "País" - -#: mod_vcard.erl:364 mod_vcard.erl:481 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:457 -#, fuzzy -msgid "Email" -msgstr "email" - -#: mod_vcard.erl:364 mod_vcard.erl:476 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:452 -msgid "Family Name" -msgstr "Apelido" - -#: mod_vcard.erl:364 mod_vcard_odbc.erl:342 -#, fuzzy -msgid "" -"Fill in the form to search for any matching Jabber User (Add * to the end of " -"field to match substring)" -msgstr "Preencha os campos para procurar utilizadores Jabber coincidentes" - -#: mod_vcard.erl:364 mod_vcard.erl:473 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:449 -msgid "Full Name" -msgstr "Nome completo" - -#: mod_vcard.erl:364 mod_vcard.erl:475 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:451 -msgid "Middle Name" -msgstr "Segundo nome" - -#: mod_vcard.erl:364 mod_vcard.erl:474 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:450 web/ejabberd_web_admin.erl:2006 -msgid "Name" -msgstr "Nome" - -#: mod_vcard.erl:364 mod_vcard.erl:482 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:458 -msgid "Organization Name" -msgstr "Nome da organização" - -#: mod_vcard.erl:364 mod_vcard.erl:483 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:459 -msgid "Organization Unit" -msgstr "Unidade da organização" - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "Search users in " -msgstr "Procurar utilizadores em " - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "You need an x:data capable client to search" -msgstr "É necessário um cliente com suporte de x:data para poder procurar" - -#: mod_vcard.erl:389 mod_vcard_ldap.erl:487 mod_vcard_odbc.erl:367 -msgid "vCard User Search" -msgstr "" - -#: mod_vcard.erl:445 mod_vcard_ldap.erl:541 mod_vcard_odbc.erl:421 -msgid "ejabberd vCard module" -msgstr "Módulo vCard de ejabberd" - -#: mod_vcard.erl:469 mod_vcard_ldap.erl:551 mod_vcard_odbc.erl:445 -#, fuzzy -msgid "Search Results for " -msgstr "Procurar utilizadores em " - -#: mod_vcard_ldap.erl:462 -msgid "Fill in fields to search for any matching Jabber User" -msgstr "Preencha os campos para procurar utilizadores Jabber coincidentes" - -#: web/ejabberd_web_admin.erl:193 web/ejabberd_web_admin.erl:203 -#: web/ejabberd_web_admin.erl:219 web/ejabberd_web_admin.erl:229 -msgid "Unauthorized" -msgstr "" - -#: web/ejabberd_web_admin.erl:286 web/ejabberd_web_admin.erl:303 -#, fuzzy -msgid "ejabberd Web Admin" -msgstr "Administração do ejabberd" - -#: web/ejabberd_web_admin.erl:769 web/ejabberd_web_admin.erl:780 -#, fuzzy -msgid "Administration" -msgstr "Administração de " - -#: web/ejabberd_web_admin.erl:878 web/ejabberd_web_admin.erl:982 -#, fuzzy -msgid "Raw" -msgstr "modo texto" - -#: web/ejabberd_web_admin.erl:1017 -msgid "~s access rule configuration" -msgstr "Configuração das Regra de Acesso ~s" - -#: web/ejabberd_web_admin.erl:1035 -#, fuzzy -msgid "Virtual Hosts" -msgstr "Servidores virtuales" - -#: web/ejabberd_web_admin.erl:1043 web/ejabberd_web_admin.erl:1050 -msgid "Users" -msgstr "Utilizadores" - -#: web/ejabberd_web_admin.erl:1078 -#, fuzzy -msgid "Users Last Activity" -msgstr "Última actividade" - -#: web/ejabberd_web_admin.erl:1080 -msgid "Period: " -msgstr "" - -#: web/ejabberd_web_admin.erl:1090 -msgid "Last month" -msgstr "" - -#: web/ejabberd_web_admin.erl:1091 -msgid "Last year" -msgstr "" - -#: web/ejabberd_web_admin.erl:1092 -#, fuzzy -msgid "All activity" -msgstr "Última actividade" - -#: web/ejabberd_web_admin.erl:1094 -msgid "Show Ordinary Table" -msgstr "" - -#: web/ejabberd_web_admin.erl:1096 -msgid "Show Integral Table" -msgstr "" - -#: web/ejabberd_web_admin.erl:1105 web/ejabberd_web_admin.erl:1939 -msgid "Statistics" -msgstr "Estatísticas" - -#: web/ejabberd_web_admin.erl:1117 -#, fuzzy -msgid "Not Found" -msgstr "Nodo não encontrado" - -#: web/ejabberd_web_admin.erl:1134 -msgid "Node not found" -msgstr "Nodo não encontrado" - -#: web/ejabberd_web_admin.erl:1460 -#, fuzzy -msgid "Host" -msgstr "Nome do servidor" - -#: web/ejabberd_web_admin.erl:1461 -#, fuzzy -msgid "Registered Users" -msgstr "Utilizadores registados" - -#: web/ejabberd_web_admin.erl:1573 -#, fuzzy -msgid "Offline Messages" -msgstr "Mensagens diferidas" - -#: web/ejabberd_web_admin.erl:1574 web/ejabberd_web_admin.erl:1767 -msgid "Last Activity" -msgstr "Última actividade" - -#: web/ejabberd_web_admin.erl:1659 web/ejabberd_web_admin.erl:1675 -#, fuzzy -msgid "Registered Users:" -msgstr "Utilizadores registados" - -#: web/ejabberd_web_admin.erl:1661 web/ejabberd_web_admin.erl:1677 -#: web/ejabberd_web_admin.erl:2192 -#, fuzzy -msgid "Online Users:" -msgstr "Utilizadores ligados" - -#: web/ejabberd_web_admin.erl:1663 -#, fuzzy -msgid "Outgoing s2s Connections:" -msgstr "Conexões S2S para fora" - -#: web/ejabberd_web_admin.erl:1665 -#, fuzzy -msgid "Outgoing s2s Servers:" -msgstr "Servidores S2S de saída" - -#: web/ejabberd_web_admin.erl:1734 web/mod_register_web.erl:175 -#: web/mod_register_web.erl:372 web/mod_register_web.erl:381 -#: web/mod_register_web.erl:411 -msgid "Change Password" -msgstr "Mudar palavra-chave" - -#: web/ejabberd_web_admin.erl:1758 -msgid "User " -msgstr "Utilizador" - -#: web/ejabberd_web_admin.erl:1765 -msgid "Connected Resources:" -msgstr "Recursos conectados:" - -#: web/ejabberd_web_admin.erl:1766 web/mod_register_web.erl:227 -#: web/mod_register_web.erl:519 -msgid "Password:" -msgstr "Palavra-chave:" - -#: web/ejabberd_web_admin.erl:1828 -msgid "No Data" -msgstr "" - -#: web/ejabberd_web_admin.erl:1906 -msgid "Nodes" -msgstr "Nodos" - -#: web/ejabberd_web_admin.erl:1929 web/ejabberd_web_admin.erl:1951 -msgid "Node " -msgstr "Nodo" - -#: web/ejabberd_web_admin.erl:1938 -#, fuzzy -msgid "Listened Ports" -msgstr "Portas em escuta em " - -#: web/ejabberd_web_admin.erl:1940 web/ejabberd_web_admin.erl:2260 -#: web/ejabberd_web_admin.erl:2448 -msgid "Update" -msgstr "Actualizar" - -#: web/ejabberd_web_admin.erl:1943 web/ejabberd_web_admin.erl:2569 -msgid "Restart" -msgstr "Reiniciar" - -#: web/ejabberd_web_admin.erl:1945 web/ejabberd_web_admin.erl:2571 -msgid "Stop" -msgstr "Parar" - -#: web/ejabberd_web_admin.erl:1959 -#, fuzzy -msgid "RPC Call Error" -msgstr "Erro na chamada RPC" - -#: web/ejabberd_web_admin.erl:2000 -#, fuzzy -msgid "Database Tables at " -msgstr "Tabelas da BD em " - -#: web/ejabberd_web_admin.erl:2007 -msgid "Storage Type" -msgstr "Tipo de armazenagem" - -#: web/ejabberd_web_admin.erl:2008 -msgid "Elements" -msgstr "" - -#: web/ejabberd_web_admin.erl:2009 -msgid "Memory" -msgstr "Memória" - -#: web/ejabberd_web_admin.erl:2032 web/ejabberd_web_admin.erl:2137 -msgid "Error" -msgstr "" - -#: web/ejabberd_web_admin.erl:2034 -#, fuzzy -msgid "Backup of " -msgstr "Guardar cópia de segurança" - -#: web/ejabberd_web_admin.erl:2036 -msgid "" -"Please note that these options will only backup the builtin Mnesia database. " -"If you are using the ODBC module, you also need to backup your SQL database " -"separately." -msgstr "" - -#: web/ejabberd_web_admin.erl:2041 -#, fuzzy -msgid "Store binary backup:" -msgstr "Armazenar uma cópia de segurança no ficheiro" - -#: web/ejabberd_web_admin.erl:2045 web/ejabberd_web_admin.erl:2052 -#: web/ejabberd_web_admin.erl:2060 web/ejabberd_web_admin.erl:2067 -#: web/ejabberd_web_admin.erl:2074 web/ejabberd_web_admin.erl:2081 -#: web/ejabberd_web_admin.erl:2088 web/ejabberd_web_admin.erl:2096 -#: web/ejabberd_web_admin.erl:2103 web/ejabberd_web_admin.erl:2110 -msgid "OK" -msgstr "OK" - -#: web/ejabberd_web_admin.erl:2048 -#, fuzzy -msgid "Restore binary backup immediately:" -msgstr "Recuperar uma cópia de segurança a partir de ficheiro" - -#: web/ejabberd_web_admin.erl:2056 -msgid "" -"Restore binary backup after next ejabberd restart (requires less memory):" -msgstr "" - -#: web/ejabberd_web_admin.erl:2063 -msgid "Store plain text backup:" -msgstr "" - -#: web/ejabberd_web_admin.erl:2070 -#, fuzzy -msgid "Restore plain text backup immediately:" -msgstr "Recuperar uma cópia de segurança a partir de ficheiro" - -#: web/ejabberd_web_admin.erl:2077 -msgid "Import users data from a PIEFXIS file (XEP-0227):" -msgstr "" - -#: web/ejabberd_web_admin.erl:2084 -msgid "Export data of all users in the server to PIEFXIS files (XEP-0227):" -msgstr "" - -#: web/ejabberd_web_admin.erl:2091 -msgid "Export data of users in a host to PIEFXIS files (XEP-0227):" -msgstr "" - -#: web/ejabberd_web_admin.erl:2099 -#, fuzzy -msgid "Import user data from jabberd14 spool file:" -msgstr "Importar utilizadores a partir de ficheiros da spool do jabberd14" - -#: web/ejabberd_web_admin.erl:2106 -#, fuzzy -msgid "Import users data from jabberd14 spool directory:" -msgstr "Importar utilizadores a partir de ficheiros da spool do jabberd14" - -#: web/ejabberd_web_admin.erl:2132 -msgid "Listened Ports at " -msgstr "Portas em escuta em " - -#: web/ejabberd_web_admin.erl:2157 -#, fuzzy -msgid "Modules at " -msgstr "Parar módulos em " - -#: web/ejabberd_web_admin.erl:2183 -#, fuzzy -msgid "Statistics of ~p" -msgstr "Estatísticas" - -#: web/ejabberd_web_admin.erl:2186 -#, fuzzy -msgid "Uptime:" -msgstr "Tempo de funcionamento" - -#: web/ejabberd_web_admin.erl:2189 -#, fuzzy -msgid "CPU Time:" -msgstr "Tempo de processador consumido" - -#: web/ejabberd_web_admin.erl:2195 -#, fuzzy -msgid "Transactions Committed:" -msgstr "Transacções realizadas" - -#: web/ejabberd_web_admin.erl:2198 -#, fuzzy -msgid "Transactions Aborted:" -msgstr "Transacções abortadas" - -#: web/ejabberd_web_admin.erl:2201 -#, fuzzy -msgid "Transactions Restarted:" -msgstr "Transacções reiniciadas" - -#: web/ejabberd_web_admin.erl:2204 -#, fuzzy -msgid "Transactions Logged:" -msgstr "Transacções armazenadas" - -#: web/ejabberd_web_admin.erl:2246 -#, fuzzy -msgid "Update " -msgstr "Actualizar" - -#: web/ejabberd_web_admin.erl:2254 -#, fuzzy -msgid "Update plan" -msgstr "Actualizar" - -#: web/ejabberd_web_admin.erl:2255 -#, fuzzy -msgid "Modified modules" -msgstr "Iniciar módulos" - -#: web/ejabberd_web_admin.erl:2256 -#, fuzzy -msgid "Update script" -msgstr "Actualizar" - -#: web/ejabberd_web_admin.erl:2257 -msgid "Low level update script" -msgstr "" - -#: web/ejabberd_web_admin.erl:2258 -msgid "Script check" -msgstr "" - -#: web/ejabberd_web_admin.erl:2426 -msgid "Port" -msgstr "Porta" - -#: web/ejabberd_web_admin.erl:2427 -msgid "IP" -msgstr "" - -#: web/ejabberd_web_admin.erl:2428 -#, fuzzy -msgid "Protocol" -msgstr "Porta" - -#: web/ejabberd_web_admin.erl:2429 web/ejabberd_web_admin.erl:2556 -msgid "Module" -msgstr "Módulo" - -#: web/ejabberd_web_admin.erl:2430 web/ejabberd_web_admin.erl:2557 -msgid "Options" -msgstr "Opções" - -#: web/ejabberd_web_admin.erl:2450 -msgid "Delete" -msgstr "Eliminar" - -#: web/ejabberd_web_admin.erl:2579 -#, fuzzy -msgid "Start" -msgstr "Reiniciar" - -#: web/mod_register_web.erl:104 -msgid "Your Jabber account was successfully created." -msgstr "" - -#: web/mod_register_web.erl:107 -msgid "There was an error creating the account: " -msgstr "" - -#: web/mod_register_web.erl:115 -msgid "Your Jabber account was successfully deleted." -msgstr "" - -#: web/mod_register_web.erl:118 -msgid "There was an error deleting the account: " -msgstr "" - -#: web/mod_register_web.erl:128 -msgid "The password of your Jabber account was successfully changed." -msgstr "" - -#: web/mod_register_web.erl:131 -msgid "There was an error changing the password: " -msgstr "" - -#: web/mod_register_web.erl:163 web/mod_register_web.erl:172 -#, fuzzy -msgid "Jabber Account Registration" -msgstr "Configuração das Listas de Controlo de Acesso do ejabberd" - -#: web/mod_register_web.erl:174 web/mod_register_web.erl:192 -#: web/mod_register_web.erl:201 -msgid "Register a Jabber account" -msgstr "" - -#: web/mod_register_web.erl:176 web/mod_register_web.erl:493 -#: web/mod_register_web.erl:502 -msgid "Unregister a Jabber account" -msgstr "" - -#: web/mod_register_web.erl:203 -msgid "" -"This page allows to create a Jabber account in this Jabber server. Your JID " -"(Jabber IDentifier) will be of the form: username@server. Please read " -"carefully the instructions to fill correctly the fields." -msgstr "" - -#: web/mod_register_web.erl:212 web/mod_register_web.erl:386 -#: web/mod_register_web.erl:509 -#, fuzzy -msgid "Username:" -msgstr "Nome do utilizador de IRC" - -#: web/mod_register_web.erl:217 -msgid "This is case insensitive: macbeth is the same that MacBeth and Macbeth." -msgstr "" - -#: web/mod_register_web.erl:218 -msgid "Characters not allowed:" -msgstr "" - -#: web/mod_register_web.erl:222 web/mod_register_web.erl:391 -#: web/mod_register_web.erl:514 -#, fuzzy -msgid "Server:" -msgstr "Nunca" - -#: web/mod_register_web.erl:232 -msgid "" -"Don't tell your password to anybody, not even the administrators of the " -"Jabber server." -msgstr "" - -#: web/mod_register_web.erl:234 -msgid "You can later change your password using a Jabber client." -msgstr "" - -#: web/mod_register_web.erl:235 -msgid "" -"Some Jabber clients can store your password in your computer. Use that " -"feature only if you trust your computer is safe." -msgstr "" - -#: web/mod_register_web.erl:237 -msgid "" -"Memorize your password, or write it in a paper placed in a safe place. In " -"Jabber there isn't an automated way to recover your password if you forget " -"it." -msgstr "" - -#: web/mod_register_web.erl:242 web/mod_register_web.erl:406 -msgid "Password Verification:" -msgstr "" - -#: web/mod_register_web.erl:250 -#, fuzzy -msgid "Register" -msgstr "Lista de contactos" - -#: web/mod_register_web.erl:396 -#, fuzzy -msgid "Old Password:" -msgstr "Palavra-chave:" - -#: web/mod_register_web.erl:401 -#, fuzzy -msgid "New Password:" -msgstr "Palavra-chave:" - -#: web/mod_register_web.erl:504 -msgid "This page allows to unregister a Jabber account in this Jabber server." -msgstr "" - -#: web/mod_register_web.erl:524 -msgid "Unregister" -msgstr "" - -#~ msgid "Encodings" -#~ msgstr "Codificações" - -#, fuzzy -#~ msgid "(Raw)" -#~ msgstr "(modo texto)" - -#~ msgid "Specified nickname is already registered" -#~ msgstr "A alcunha especificada já está registada" - -#~ msgid "Size" -#~ msgstr "Tamanho" - -#~ msgid "Backup Management at " -#~ msgstr "Gestão da cópia de segurança em " - -#~ msgid "Choose host name" -#~ msgstr "Introduza o nome do servidor" - -#~ msgid "Choose users to remove" -#~ msgstr "Seleccione utilizadores a eliminar" - -#~ msgid "DB" -#~ msgstr "BD" - -#~ msgid "Dump a database in a text file" -#~ msgstr "Exportar uma Base de Dados para um ficheiro de texto" - -#~ msgid "Host name" -#~ msgstr "Nome do servidor" - -#~ msgid "Hostname Configuration" -#~ msgstr "Configuração do nome do servidor" - -#~ msgid "Install a database fallback from a file" -#~ msgstr "Instalar uma recuperação de BD desde um ficheiro" - -#~ msgid "It is not allowed to send normal messages to the conference" -#~ msgstr "Impedir o envio de mensagens normais para a sala" - -#~ msgid "Listened Ports Management" -#~ msgstr "Gestão das portas em escuta" - -#~ msgid "Make room moderated?" -#~ msgstr "Tornar a sala moderada?" - -#~ msgid "Remove Users" -#~ msgstr "Eliminar utilizadores" - -#~ msgid "Restore a database from a text file" -#~ msgstr "Restaurar uma Base de Dados a partir de ficheiro de texto" - -#~ msgid "Results of search in " -#~ msgstr "Resultados da procura em " - -#~ msgid "ejabberd (c) 2002-2005 Alexey Shchepin, 2005 Process One" -#~ msgstr "ejabberd (c) 2002-2005 Alexey Shchepin, 2005 Process One" - -#~ msgid "ejabberd access rules configuration" -#~ msgstr "Configuração das Regras de Acesso do ejabberd" - -#~ msgid "ejabberd users" -#~ msgstr "Utilizadores do ejabberd" - -#~ msgid "~p statistics" -#~ msgstr "Estatísticas de ~p" diff --git a/priv/msgs/ru.msg b/priv/msgs/ru.msg index ece734849..ca23d2167 100644 --- a/priv/msgs/ru.msg +++ b/priv/msgs/ru.msg @@ -1,20 +1,25 @@ -{"Access Configuration","Конфигурация доступа"}. -{"Access Control List Configuration","Конфигурация списков управления доступом"}. -{"Access control lists","Списки управления доступом"}. -{"Access Control Lists","Списки управления доступом"}. +%% Generated automatically +%% DO NOT EDIT: run `make translations` instead +%% To improve translations please read: +%% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ + +{" (Add * to the end of field to match substring)"," (Добавьте * в конец поля для поиска подстроки)"}. +{" has set the subject to: "," установил(а) тему: "}. +{"A friendly name for the node","Легко запоминаемое имя для узла"}. +{"A password is required to enter this room","Чтобы войти в эту конференцию, нужен пароль"}. +{"A Web Page","Веб-страница"}. +{"Accept","Принять"}. {"Access denied by service policy","Доступ запрещён политикой службы"}. -{"Access rules","Правила доступа"}. -{"Access Rules","Правила доступа"}. +{"Account doesn't exist","Учётная запись не существует"}. {"Action on user","Действие над пользователем"}. -{"Add Jabber ID","Добавить Jabber ID"}. -{"Add New","Добавить"}. {"Add User","Добавить пользователя"}. {"Administration of ","Администрирование "}. {"Administration","Администрирование"}. {"Administrator privileges required","Требуются права администратора"}. -{"A friendly name for the node","Легко запоминаемое имя для узла"}. {"All activity","Вся статистика"}. +{"All Users","Все пользователи"}. {"Allow this Jabber ID to subscribe to this pubsub node?","Разрешить этому Jabber ID подписаться на данный узел?"}. +{"Allow this person to register with the room?","Разрешить пользователю зарегистрироваться в комнате?"}. {"Allow users to change the subject","Разрешить пользователям изменять тему"}. {"Allow users to query other users","Разрешить iq-запросы к пользователям"}. {"Allow users to send invites","Разрешить пользователям посылать приглашения"}. @@ -23,21 +28,32 @@ {"Allow visitors to send private messages to","Разрешить посетителям посылать приватные сообщения"}. {"Allow visitors to send status text in presence updates","Разрешить посетителям вставлять текcт статуса в сообщения о присутствии"}. {"Allow visitors to send voice requests","Разрешить посетителям запрашивать право голоса"}. -{"All Users","Все пользователи"}. {"Announcements","Объявления"}. -{"anyone","всем участникам"}. -{"A password is required to enter this room","Чтобы войти в эту конференцию, нужен пароль"}. {"April","апреля"}. +{"Attribute 'channel' is required for this request","Атрибут 'channel' является обязательным для этого запроса"}. +{"Attribute 'id' is mandatory for MIX messages","Атрибут 'id' является обязательным для MIX сообщений"}. +{"Attribute 'jid' is not allowed here","Атрибут 'jid' здесь недопустим"}. +{"Attribute 'node' is not allowed here","Атрибут 'node' здесь недопустим"}. {"August","августа"}. +{"Automatic node creation is not enabled","Автоматическое создание узлов недоступно"}. {"Backup Management","Управление резервным копированием"}. -{"Backup of ","Резервное копирование "}. +{"Backup of ~p","Резервное копирование ~p"}. {"Backup to File at ","Резервное копирование в файл на "}. {"Backup","Резервное копирование"}. {"Bad format","Неправильный формат"}. {"Birthday","День рождения"}. +{"Both the username and the resource are required","Требуются имя пользователя и ресурс"}. +{"Bytestream already activated","Поток данных уже активирован"}. +{"Cannot remove active list","Невозможно удалить активный список"}. +{"Cannot remove default list","Невозможно удалить список по умолчанию"}. {"CAPTCHA web page","Ссылка на капчу"}. {"Change Password","Сменить пароль"}. {"Change User Password","Изменить пароль пользователя"}. +{"Changing password is not allowed","Изменение пароля не разрешено"}. +{"Changing role/affiliation is not allowed","Изменение роли/ранга не разрешено"}. +{"Channel already exists","Канал уже существует"}. +{"Channel does not exist","Канал не существует"}. +{"Channels","Каналы"}. {"Characters not allowed:","Недопустимые символы:"}. {"Chatroom configuration modified","Конфигурация комнаты изменилась"}. {"Chatroom is created","Комната создана"}. @@ -46,90 +62,80 @@ {"Chatroom is stopped","Комната остановлена"}. {"Chatrooms","Комнаты"}. {"Choose a username and password to register with this server","Выберите имя пользователя и пароль для регистрации на этом сервере"}. -{"Choose modules to stop","Выберите модули, которые следует остановить"}. {"Choose storage type of tables","Выберите тип хранения таблиц"}. {"Choose whether to approve this entity's subscription.","Решите: предоставить ли подписку этому объекту."}. {"City","Город"}. +{"Client acknowledged more stanzas than sent by server","Клиент подтвердил больше сообщений чем было отправлено сервером"}. {"Commands","Команды"}. {"Conference room does not exist","Конференция не существует"}. {"Configuration of room ~s","Конфигурация комнаты ~s"}. {"Configuration","Конфигурация"}. -{"Connected Resources:","Подключённые ресурсы:"}. -{"Connections parameters","Параметры соединения"}. {"Country","Страна"}. -{"CPU Time:","Процессорное время:"}. -{"Database Tables at ","Таблицы базы данных на "}. +{"Database failure","Ошибка базы данных"}. {"Database Tables Configuration at ","Конфигурация таблиц базы данных на "}. {"Database","База данных"}. {"December","декабря"}. {"Default users as participants","Сделать пользователей участниками по умолчанию"}. {"Delete message of the day on all hosts","Удалить сообщение дня со всех виртуальных серверов"}. {"Delete message of the day","Удалить сообщение дня"}. -{"Delete Selected","Удалить выделенные"}. {"Delete User","Удалить пользователя"}. -{"Delete","Удалить"}. {"Deliver event notifications","Доставлять уведомления о событиях"}. {"Deliver payloads with event notifications","Доставлять вместе с уведомлениями o публикациях сами публикации"}. -{"Description:","Описание:"}. {"Disc only copy","только диск"}. -{"Displayed Groups:","Видимые группы:"}. -{"Don't tell your password to anybody, not even the administrators of the Jabber server.","Не говорите никому свой пароль, даже администраторам сервера."}. {"Dump Backup to Text File at ","Копирование в текстовый файл на "}. {"Dump to Text File","Копирование в текстовый файл"}. {"Edit Properties","Изменить параметры"}. {"Either approve or decline the voice request.","Подтвердите или отклоните право голоса."}. -{"ejabberd IRC module","ejabberd IRC модуль"}. {"ejabberd MUC module","ejabberd MUC модуль"}. +{"ejabberd Multicast service","ejabberd Multicast сервис"}. {"ejabberd Publish-Subscribe module","Модуль ejabberd Публикации-Подписки"}. {"ejabberd SOCKS5 Bytestreams module","ejabberd SOCKS5 Bytestreams модуль"}. {"ejabberd vCard module","ejabberd vCard модуль"}. {"ejabberd Web Admin","Web-интерфейс администрирования ejabberd"}. -{"Elements","Элементы"}. +{"ejabberd","ejabberd"}. {"Email","Электронная почта"}. {"Enable logging","Включить журналирование"}. -{"Encoding for server ~b","Кодировка сервера ~b"}. +{"Enable message archiving","Включить хранение сообщений"}. +{"Enabling push without 'node' attribute is not supported","Включение push-режима без атрибута 'node' не поддерживается"}. {"End User Session","Завершить сеанс пользователя"}. -{"Enter list of {Module, [Options]}","Введите список вида {Module, [Options]}"}. {"Enter nickname you want to register","Введите псевдоним, который Вы хотели бы зарегистрировать"}. {"Enter path to backup file","Введите путь к резервному файлу"}. {"Enter path to jabberd14 spool dir","Введите путь к директории спула jabberd14"}. {"Enter path to jabberd14 spool file","Введите путь к файлу из спула jabberd14"}. {"Enter path to text file","Введите путь к текстовому файлу"}. {"Enter the text you see","Введите увиденный текст"}. -{"Enter username and encodings you wish to use for connecting to IRC servers. Press 'Next' to get more fields to fill in. Press 'Complete' to save settings.","Введите имя пользователя и кодировки, которые будут использоваться при подключении к IRC-серверам. Нажмите 'Далее' для получения дополнительных полей для заполнения. Нажмите 'Завершить' для сохранения настроек."}. -{"Enter username, encodings, ports and passwords you wish to use for connecting to IRC servers","Введите имя пользователя, кодировки, порты и пароли, которые будут использоваться при подключении к IRC-серверам"}. -{"Erlang Jabber Server","Erlang Jabber Server"}. -{"Error","Ошибка"}. -{"Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef.net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}].","Пример: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef.net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]."}. {"Exclude Jabber IDs from CAPTCHA challenge","Исключить показ капчи для списка Jabber ID"}. +{"Export all tables as SQL queries to a file:","Экспортировать все таблицы в виде SQL запросов в файл:"}. {"Export data of all users in the server to PIEFXIS files (XEP-0227):","Экспорт данных всех пользователей сервера в файлы формата PIEFXIS (XEP-0227):"}. {"Export data of users in a host to PIEFXIS files (XEP-0227):","Экспорт пользовательских данных домена в файлы формата PIEFXIS (XEP-0227):"}. +{"External component failure","Ошибка внешнего сервиса"}. +{"External component timeout","Таймаут внешнего сервиса"}. +{"Failed to activate bytestream","Ошибка при активировании потока данных"}. {"Failed to extract JID from your voice request approval","Ошибка обработки JID из вашего запроса на право голоса"}. +{"Failed to map delegated namespace to external component","Не получилось найти внешний сервис, делегирующий это пространство имён"}. +{"Failed to parse HTTP response","Ошибка разбора HTTP ответа"}. +{"Failed to process option '~s'","Ошибка обработки опции '~s'"}. {"Family Name","Фамилия"}. {"February","февраля"}. -{"Fill in fields to search for any matching Jabber User","Заполните форму для поиска пользователя Jabber"}. -{"Fill in the form to search for any matching Jabber User (Add * to the end of field to match substring)","Заполните форму для поиска пользователя Jabber (Если добавить * в конец поля, то происходит поиск подстроки)"}. +{"File larger than ~w bytes","Файл больше ~w байт"}. {"Friday","Пятница"}. -{"From ~s","От ~s"}. -{"From","От кого"}. {"Full Name","Полное имя"}. {"Get Number of Online Users","Получить количество подключённых пользователей"}. {"Get Number of Registered Users","Получить количество зарегистрированных пользователей"}. +{"Get Pending","Получить отложенные"}. {"Get User Last Login Time","Получить время последнего подключения пользователя"}. -{"Get User Password","Получить пароль пользователя"}. {"Get User Statistics","Получить статистику по пользователю"}. +{"Given Name","Имя"}. {"Grant voice to this person?","Предоставить голос?"}. -{"Groups","Группы"}. -{"Group ","Группа "}. {"has been banned","запретили входить в комнату"}. -{"has been kicked because of an affiliation change","выгнали из комнаты вследствие смены ранга"}. {"has been kicked because of a system shutdown","выгнали из комнаты из-за останова системы"}. +{"has been kicked because of an affiliation change","выгнали из комнаты вследствие смены ранга"}. {"has been kicked because the room has been changed to members-only","выгнали из комнаты потому что она стала только для членов"}. {"has been kicked","выгнали из комнаты"}. -{" has set the subject to: "," установил(а) тему: "}. -{"Host","Хост"}. +{"Host unknown","Неизвестное имя сервера"}. +{"HTTP File Upload","Передача файлов по HTTP"}. +{"Idle connection","Неиспользуемое соединение"}. {"If you don't see the CAPTCHA image here, visit the web page.","Если вы не видите изображение капчи, перейдите по ссылке."}. -{"If you want to specify different ports, passwords, encodings for IRC servers, fill this list with values in format '{\"irc server\", \"encoding\", port, \"password\"}'. By default this service use \"~s\" encoding, port ~p, empty password.","Чтобы указать различные порты, пароли, кодировки для разных серверов IRC, заполните список значениями в формате '{\"сервер IRC\", \"кодировка\", порт, \"пароль\"}'. По умолчанию сервис использует кодировку \"~s\", порт ~p, пустой пароль."}. {"Import Directory","Импорт из директории"}. {"Import File","Импорт из файла"}. {"Import user data from jabberd14 spool file:","Импорт пользовательских данных из буферного файла jabberd14:"}. @@ -138,30 +144,28 @@ {"Import users data from jabberd14 spool directory:","Импорт пользовательских данных из буферной директории jabberd14:"}. {"Import Users from Dir at ","Импорт пользователей из директории на "}. {"Import Users From jabberd14 Spool Files","Импорт пользователей из спула jabberd14"}. +{"Improper domain part of 'from' attribute","Неправильная доменная часть атрибута 'from'"}. {"Improper message type","Неправильный тип сообщения"}. +{"Incorrect CAPTCHA submit","Неверный ввод капчи"}. +{"Incorrect data form","Некорректная форма данных"}. {"Incorrect password","Неправильный пароль"}. -{"Invalid affiliation: ~s","Недопустимый ранг: ~s"}. -{"Invalid role: ~s","Недопустимая роль: ~s"}. +{"Incorrect value of 'action' attribute","Некорректное значение атрибута 'action'"}. +{"Incorrect value of 'action' in data form","Некорректное значение 'action' в форме данных"}. +{"Incorrect value of 'path' in data form","Некорректное значение 'path' в форме данных"}. +{"Insufficient privilege","Недостаточно прав"}. +{"Internal server error","Внутренняя ошибка сервера"}. +{"Invalid 'from' attribute in forwarded message","Некорректный атрибут 'from' в пересланном сообщении"}. +{"Invalid node name","Недопустимое имя узла"}. +{"Invalid 'previd' value","Недопустимое значение 'previd'"}. +{"Invitations are not allowed in this conference","Рассылка приглашений отключена в этой конференции"}. {"IP addresses","IP адреса"}. -{"IP","IP"}. -{"IRC channel (don't put the first #)","Канал IRC (без символа #)"}. -{"IRC server","Сервер IRC"}. -{"IRC settings","Настройки IRC"}. -{"IRC Transport","IRC Транспорт"}. -{"IRC username","Имя пользователя IRC"}. -{"IRC Username","Имя пользователя IRC"}. {"is now known as","изменил(а) имя на"}. +{"It is not allowed to send error messages to the room. The participant (~s) has sent an error message (~s) and got kicked from the room","Запрещено посылать сообщения об ошибках в эту комнату. Участник (~s) послал сообщение об ошибке (~s) и был выкинут из комнаты"}. {"It is not allowed to send private messages of type \"groupchat\"","Нельзя посылать частные сообщения типа \"groupchat\""}. {"It is not allowed to send private messages to the conference","Не разрешается посылать частные сообщения прямо в конференцию"}. -{"It is not allowed to send private messages","Запрещено посылать приватные сообщения"}. -{"Jabber Account Registration","Регистрация Jabber-аккаунта"}. {"Jabber ID","Jabber ID"}. -{"Jabber ID ~s is invalid","Jabber ID ~s недопустимый"}. {"January","января"}. -{"Join IRC channel","Присоединиться к каналу IRC"}. {"joins the room","вошёл(а) в комнату"}. -{"Join the IRC channel here.","Присоединяйтесь к каналу IRC"}. -{"Join the IRC channel in this Jabber ID: ~s","Присоединиться к каналу IRC с Jabber ID: ~s"}. {"July","июля"}. {"June","июня"}. {"Last Activity","Последнее подключение"}. @@ -169,10 +173,6 @@ {"Last month","За последний месяц"}. {"Last year","За последний год"}. {"leaves the room","вышел(а) из комнаты"}. -{"Listened Ports at ","Прослушиваемые порты на "}. -{"Listened Ports","Прослушиваемые порты"}. -{"List of modules to start","Список запускаемых модулей"}. -{"Low level update script","Низкоуровневый сценарий обновления"}. {"Make participants list public","Сделать список участников видимым всем"}. {"Make room CAPTCHA protected","Сделать комнату защищённой капчей"}. {"Make room members-only","Комната только для зарегистрированных участников"}. @@ -180,43 +180,70 @@ {"Make room password protected","Сделать комнату защищённой паролем"}. {"Make room persistent","Сделать комнату постоянной"}. {"Make room public searchable","Сделать комнату видимой всем"}. +{"Malformed username","Недопустимое имя пользователя"}. +{"MAM preference modification denied by service policy","Изменение настроек архива сообщений запрещено политикой службы"}. {"March","марта"}. -{"Maximum Number of Occupants","Максимальное количество участников"}. -{"Max # of items to persist","Максимальное число сохраняемых публикаций"}. {"Max payload size in bytes","Максимальный размер полезной нагрузки в байтах"}. +{"Maximum Number of Occupants","Максимальное количество участников"}. {"May","мая"}. {"Membership is required to enter this room","В эту конференцию могут входить только её члены"}. -{"Members:","Члены:"}. -{"Memorize your password, or write it in a paper placed in a safe place. In Jabber there isn't an automated way to recover your password if you forget it.","Запомните пароль или запишите его на бумаге, которую сохраните в безопасном месте. В Jabber'е нет автоматизированного средства восстановления пароля в том случае, если Вы его забудете."}. -{"Memory","Память"}. {"Message body","Тело сообщения"}. +{"Message not found in forwarded payload","Сообщение не найдено в пересылаемом вложении"}. +{"Messages from strangers are rejected","Сообщения от незнакомцев запрещены"}. {"Middle Name","Отчество"}. {"Minimum interval between voice requests (in seconds)","Минимальный интервал между запросами на право голоса"}. {"Moderator privileges required","Требуются права модератора"}. -{"moderators only","только модераторам"}. -{"Modified modules","Изменённые модули"}. -{"Modules at ","Модули на "}. -{"Modules","Модули"}. -{"Module","Модуль"}. +{"Module failed to handle the query","Ошибка модуля при обработке запроса"}. {"Monday","Понедельник"}. -{"Name:","Название:"}. +{"Multicast","Мультикаст"}. +{"Multi-User Chat","Конференция"}. {"Name","Название"}. +{"Neither 'jid' nor 'nick' attribute found","Не найден атрибут 'jid' или 'nick'"}. +{"Neither 'role' nor 'affiliation' attribute found","Не найден атрибут 'role' или 'affiliation'"}. {"Never","Никогда"}. {"New Password:","Новый пароль:"}. +{"Nickname can't be empty","Псевдоним не может быть пустым значением"}. {"Nickname Registration at ","Регистрация псевдонима на "}. {"Nickname ~s does not exist in the room","Псевдоним ~s в комнате отсутствует"}. {"Nickname","Псевдоним"}. +{"No address elements found","Не найден элемент
"}. +{"No addresses element found","Не найден элемент "}. +{"No 'affiliation' attribute found","Не найден атрибут 'affiliation'"}. +{"No available resource found","Нет доступных ресурсов"}. {"No body provided for announce message","Тело объявления не должно быть пустым"}. -{"nobody","никто"}. +{"No child elements found","Нет дочерних элементов"}. +{"No data form found","Форма данных не найдена"}. {"No Data","Нет данных"}. -{"Node ID","ID узла"}. -{"Node not found","Узел не найден"}. -{"Nodes","Узлы"}. -{"Node ","Узел "}. +{"No features available","Свойства недоступны"}. +{"No element found","Не найден элемент "}. +{"No hook has processed this command","Ни один из хуков не выполнил эту команду"}. +{"No info about last activity found","Не найдено информации о последней активности"}. +{"No 'item' element found","Элемент 'item' не найден"}. +{"No items found in this query","Не найдены элементы в этом запросе"}. {"No limit","Не ограничено"}. +{"No module is handling this query","Нет модуля который бы обработал этот запрос"}. +{"No node specified","Узел не указан"}. +{"No 'password' found in data form","Не найден 'password' в форме данных"}. +{"No 'password' found in this query","Не найден 'password' в этом запросе"}. +{"No 'path' found in data form","Не найден 'path' в форме данных"}. +{"No pending subscriptions found","Не найдено ожидающих решения подписок"}. +{"No privacy list with this name found","Списка приватности с таким именем не найдено"}. +{"No private data found in this query","Приватные данные не найдены в этом запросе"}. +{"No running node found","Нет работающих узлов"}. +{"No services available","Нет доступных сервисов"}. +{"No statistics found for this item","Не найдено статистики для этого элемента"}. +{"No 'to' attribute found in the invitation","Не найден атрибут 'to' в этом приглашении"}. +{"Node already exists","Узел уже существует"}. +{"Node ID","ID узла"}. +{"Node index not found","Индекс узла не найден"}. +{"Node not found","Узел не найден"}. +{"Node ~p","Узел ~p"}. +{"Nodeprep has failed","Ошибка применения профиля Nodeprep"}. +{"Nodes","Узлы"}. {"None","Нет"}. -{"No resource provided","Не указан ресурс"}. +{"Not allowed","Недопустимо"}. {"Not Found","Не Найдено"}. +{"Not subscribed","Нет подписки"}. {"Notify subscribers when items are removed from the node","Уведомлять подписчиков об удалении публикаций из сборника"}. {"Notify subscribers when the node configuration changes","Уведомлять подписчиков об изменении конфигурации сборника"}. {"Notify subscribers when the node is deleted","Уведомлять подписчиков об удалении сборника"}. @@ -225,69 +252,62 @@ {"Number of online users","Количество подключённых пользователей"}. {"Number of registered users","Количество зарегистрированных пользователей"}. {"October","октября"}. -{"Offline Messages:","Офлайновые сообщения:"}. -{"Offline Messages","Офлайновые сообщения"}. {"OK","Продолжить"}. {"Old Password:","Старый пароль:"}. -{"Online Users:","Подключённые пользователи:"}. {"Online Users","Подключённые пользователи"}. {"Online","Подключён"}. {"Only deliver notifications to available users","Доставлять уведомления только доступным пользователям"}. +{"Only or tags are allowed","Допустимы только тэги или "}. +{"Only element is allowed in this query","Только элемент допустим в этом запросе"}. +{"Only members may query archives of this room","Только члены могут запрашивать архивы этой комнаты"}. {"Only moderators and participants are allowed to change the subject in this room","Только модераторы и участники могут изменять тему в этой комнате"}. {"Only moderators are allowed to change the subject in this room","Только модераторы могут изменять тему в этой комнате"}. {"Only moderators can approve voice requests","Только модераторы могут утверждать запросы на право голоса"}. {"Only occupants are allowed to send messages to the conference","Только присутствующим разрешается посылать сообщения в конференцию"}. {"Only occupants are allowed to send queries to the conference","Только присутствующим разрешается посылать запросы в конференцию"}. {"Only service administrators are allowed to send service messages","Только администратор службы может посылать служебные сообщения"}. -{"Options","Параметры"}. {"Organization Name","Название организации"}. {"Organization Unit","Отдел организации"}. -{"Outgoing s2s Connections:","Исходящие s2s-серверы:"}. {"Outgoing s2s Connections","Исходящие s2s-соединения"}. -{"Outgoing s2s Servers:","Исходящие s2s-серверы:"}. {"Owner privileges required","Требуются права владельца"}. -{"Packet","Пакет"}. -{"Password ~b","Пароль ~b"}. -{"Password Verification:","Проверка пароля:"}. +{"Packet relay is denied by service policy","Пересылка пакетов запрещена политикой службы"}. {"Password Verification","Проверка пароля"}. -{"Password:","Пароль:"}. +{"Password Verification:","Проверка пароля:"}. {"Password","Пароль"}. +{"Password:","Пароль:"}. {"Path to Dir","Путь к директории"}. {"Path to File","Путь к файлу"}. -{"Pending","Ожидание"}. {"Period: ","Период"}. {"Persist items to storage","Сохранять публикации в хранилище"}. +{"Ping query is incorrect","Некорректный пинг-запрос"}. {"Ping","Пинг"}. {"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Заметьте, что здесь производится резервное копирование только встроенной базы данных Mnesia. Если Вы также используете другое хранилище данных (например с помощью модуля ODBC), то его резервное копирование следует осуществлять отдельно."}. {"Please, wait for a while before sending new voice request","Пожалуйста, подождите перед тем как подать новый запрос на право голоса"}. {"Pong","Понг"}. -{"Port ~b","Порт ~b"}. -{"Port","Порт"}. {"Present real Jabber IDs to","Сделать реальные Jabber ID участников видимыми"}. +{"Previous session not found","Предыдущая сессия не найдена"}. +{"Previous session PID has been killed","Предыдущая сессия была убита"}. +{"Previous session PID has exited","Процесс предыдущей сессии завершён"}. +{"Previous session PID is dead","Предыдущая сессия мертва"}. +{"Previous session timed out","Предыдущая сессия не отвечает"}. {"private, ","приватная, "}. -{"Protocol","Протокол"}. {"Publish-Subscribe","Публикация-Подписка"}. {"PubSub subscriber request","Запрос подписчика PubSub"}. {"Purge all items when the relevant publisher goes offline","Очищать все записи автора публикации когда он отключается"}. +{"Push record not found","Push-запись не найдена"}. {"Queries to the conference members are not allowed in this room","Запросы к пользователям в этой конференции запрещены"}. +{"Query to another users is forbidden","Запрос к другим пользователям запрещён"}. {"RAM and disc copy","ОЗУ и диск"}. {"RAM copy","ОЗУ"}. -{"Raw","Необработанный формат"}. {"Really delete message of the day?","Действительно удалить сообщение дня?"}. {"Recipient is not in the conference room","Адресата нет в конференции"}. -{"Register a Jabber account","Зарегистрировать Jabber-аккаунт"}. -{"Registered Users:","Зарегистрированные пользователи:"}. -{"Registered Users","Зарегистрированные пользователи"}. {"Register","Зарегистрировать"}. -{"Registration in mod_irc for ","Регистрация в mod_irc для "}. {"Remote copy","не хранится локально"}. -{"Remove All Offline Messages","Удалить все офлайновые сообщения"}. {"Remove User","Удалить пользователя"}. -{"Remove","Удалить"}. {"Replaced by new connection","Заменено новым соединением"}. +{"Request has timed out","Истекло время ожидания запроса"}. {"Resources","Ресурсы"}. {"Restart Service","Перезапустить службу"}. -{"Restart","Перезапустить"}. {"Restore Backup from File at ","Восстановление из резервной копии на "}. {"Restore binary backup after next ejabberd restart (requires less memory):","Восстановить из бинарной резервной копии при следующем запуске (требует меньше памяти):"}. {"Restore binary backup immediately:","Восстановить из бинарной резервной копии немедленно:"}. @@ -297,16 +317,12 @@ {"Room creation is denied by service policy","Cоздавать конференцию запрещено политикой службы"}. {"Room description","Описание комнаты"}. {"Room Occupants","Участники комнаты"}. +{"Room terminates","Комната остановлена"}. {"Room title","Название комнаты"}. {"Roster groups allowed to subscribe","Группы списка контактов, которым разрешена подписка"}. -{"Roster of ","Ростер пользователя "}. {"Roster size","Размер списка контактов"}. -{"Roster","Ростер"}. -{"RPC Call Error","Ошибка вызова RPC"}. {"Running Nodes","Работающие узлы"}. -{"~s access rule configuration","Конфигурация правила доступа ~s"}. {"Saturday","Суббота"}. -{"Script check","Проверка сценария"}. {"Search Results for ","Результаты поиска в "}. {"Search users in ","Поиск пользователей в "}. {"Send announcement to all online users on all hosts","Разослать объявление всем подключённым пользователям на всех виртуальных серверах"}. @@ -314,89 +330,91 @@ {"Send announcement to all users on all hosts","Разослать объявление всем пользователям на всех виртуальных серверах"}. {"Send announcement to all users","Разослать объявление всем пользователям"}. {"September","сентября"}. -{"Server ~b","Сервер ~b"}. {"Server:","Сервер:"}. +{"Session state copying timed out","Таймаут копирования состояния сессии"}. {"Set message of the day and send to online users","Установить сообщение дня и разослать его подключённым пользователям"}. {"Set message of the day on all hosts and send to online users","Установить сообщение дня на всех виртуальных серверах и разослать его подключённым пользователям"}. {"Shared Roster Groups","Группы общих контактов"}. {"Show Integral Table","Показать интегральную таблицу"}. {"Show Ordinary Table","Показать обычную таблицу"}. {"Shut Down Service","Остановить службу"}. -{"~s invites you to the room ~s","~s приглашает вас в комнату ~s"}. -{"Some Jabber clients can store your password in your computer. Use that feature only if you trust your computer is safe.","Некоторые Jabber-клиенты могут сохранять пароль на Вашем компьютере. Используйте эту функцию только в том случае, если считаете это безопасным."}. +{"SOCKS5 Bytestreams","Передача файлов через SOCKS5"}. {"Specify the access model","Укажите механизм управления доступом"}. {"Specify the event message type","Укажите тип сообщения о событии"}. {"Specify the publisher model","Условия публикации"}. -{"~s's Offline Messages Queue","Oчередь офлайновых сообщений ~s"}. -{"Start Modules at ","Запуск модулей на "}. -{"Start Modules","Запуск модулей"}. -{"Start","Запустить"}. -{"Statistics of ~p","статистика узла ~p"}. -{"Statistics","Статистика"}. -{"Stop Modules at ","Остановка модулей на "}. -{"Stop Modules","Остановка модулей"}. {"Stopped Nodes","Остановленные узлы"}. -{"Stop","Остановить"}. -{"Storage Type","Тип таблицы"}. {"Store binary backup:","Сохранить бинарную резервную копию:"}. {"Store plain text backup:","Сохранить текстовую резервную копию:"}. +{"Stream management is already enabled","Управление потоком уже активировано"}. +{"Stream management is not enabled","Управление потоком не активировано"}. {"Subject","Тема"}. {"Submitted","Отправлено"}. -{"Submit","Отправить"}. {"Subscriber Address","Адрес подписчика"}. -{"Subscription","Подписка"}. +{"Subscriptions are not allowed","Подписки недопустимы"}. {"Sunday","Воскресенье"}. {"That nickname is already in use by another occupant","Этот псевдоним уже занят другим участником"}. {"That nickname is registered by another person","Этот псевдоним зарегистрирован кем-то другим"}. +{"The account already exists","Учётная запись уже существует"}. {"The CAPTCHA is valid.","Проверка капчи прошла успешно."}. {"The CAPTCHA verification has failed","Проверка капчи не пройдена"}. +{"The captcha you entered is wrong","Неправильно введённое значение капчи"}. {"The collections with which a node is affiliated","Имя коллекции, в которую входит узел"}. +{"The feature requested is not supported by the conference","Запрашиваемое свойство не поддерживается этой конференцией"}. +{"The password contains unacceptable characters","Пароль содержит недопустимые символы"}. {"The password is too weak","Слишком слабый пароль"}. {"the password is","пароль:"}. -{"The password of your Jabber account was successfully changed.","Пароль Вашего Jabber-аккаунта был успешно изменен."}. -{"There was an error changing the password: ","Ошибка при смене пароля:"}. +{"The password was not changed","Пароль не был изменён"}. +{"The passwords are different","Пароли не совпадают"}. +{"The query is only allowed from local users","Запрос доступен только для локальных пользователей"}. +{"The query must not contain elements","Запрос не должен содержать элементов "}. +{"The stanza MUST contain only one element, one element, or one element","Строфа может содержать только один элемент , один элемент или один элемент "}. {"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.","Регистр не имеет значения: \"маша\" и \"МАША\" будет считаться одним и тем же именем."}. -{"This page allows to create a Jabber account in this Jabber server. Your JID (Jabber IDentifier) will be of the form: username@server. Please read carefully the instructions to fill correctly the fields.","Здесь Вы можете создать Jabber-аккаунт на этом Jabber-сервере. Ваш JID (Jabber-идентификатор) будет в виде: \"пользователь@сервер\". Пожалуйста, внимательно читайте инструкции для правильного заполнения полей."}. -{"This page allows to unregister a Jabber account in this Jabber server.","Здесь Вы можете удалить Jabber-аккаунт с этого сервера."}. -{"This participant is kicked from the room because he sent an error message to another participant","Этого участника выгнали из комнаты за то, что он послал сообщение об ошибке другому участнику"}. -{"This participant is kicked from the room because he sent an error message","Этого участника выгнали из комнаты за то, что он послал сообщение об ошибке"}. -{"This participant is kicked from the room because he sent an error presence","Этого участника выгнали из комнаты за то, что он послал присутствие с ошибкой"}. {"This room is not anonymous","Эта комната не анонимная"}. +{"This service can not process the address: ~s","Сервер не может обработать адрес: ~s"}. {"Thursday","Четверг"}. {"Time delay","По истечение"}. -{"Time","Время"}. +{"Timed out waiting for stream resumption","Истекло время ожидания возобновления потока"}. +{"To register, visit ~s","Для регистрации посетите ~s"}. +{"Token TTL","Токен TTL"}. +{"Too many active bytestreams","Слишком много активных потоков данных"}. {"Too many CAPTCHA requests","Слишком много запросов капчи"}. -{"To ~s","К ~s"}. -{"To","Кому"}. +{"Too many child elements","Слишком много дочерних элементов"}. +{"Too many elements","Слишком много элементов "}. +{"Too many elements","Слишком много элементов "}. +{"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","Слишком много (~p) неудачных попыток аутентификации с этого IP-адреса (~s). Адрес будет разблокирован в ~s UTC"}. +{"Too many receiver fields were specified","Указано слишком много получателей"}. +{"Too many unacked stanzas","Слишком много неподтверждённых пакетов"}. +{"Too many users in this conference","Слишком много пользователей в этой конференции"}. {"Traffic rate limit is exceeded","Превышен лимит скорости посылки информации"}. -{"Transactions Aborted:","Транзакции отмененные:"}. -{"Transactions Committed:","Транзакции завершенные:"}. -{"Transactions Logged:","Транзакции запротоколированные:"}. -{"Transactions Restarted:","Транзакции перезапущенные:"}. {"Tuesday","Вторник"}. {"Unable to generate a CAPTCHA","Не получилось создать капчу"}. +{"Unable to register route on existing local domain","Нельзя регистрировать маршруты на существующие локальные домены"}. {"Unauthorized","Не авторизован"}. -{"Unregister a Jabber account","Удалить Jabber-аккаунт"}. +{"Unexpected action","Неожиданное действие"}. +{"Unexpected error condition: ~p","Неожиданная ошибка: ~p"}. {"Unregister","Удалить"}. +{"Unsupported element","Элемент не поддерживается"}. +{"Unsupported version","Неподдерживаемая версия"}. {"Update message of the day (don't send)","Обновить сообщение дня (не рассылать)"}. {"Update message of the day on all hosts (don't send)","Обновить сообщение дня на всех виртуальных серверах (не рассылать)"}. -{"Update plan","План обновления"}. -{"Update script","Сценарий обновления"}. -{"Update","Обновить"}. -{"Update ","Обновление "}. -{"Uptime:","Время работы:"}. -{"Use of STARTTLS required","Вы обязаны использовать STARTTLS"}. +{"User already exists","Пользователь уже существует"}. {"User JID","JID пользователя"}. +{"User (jid)","Пользователь (XMPP адрес)"}. {"User Management","Управление пользователями"}. +{"User removed","Пользователь удалён"}. +{"User session not found","Сессия пользователя не найдена"}. +{"User session terminated","Сессия пользователя завершена"}. {"Username:","Имя пользователя:"}. {"Users are not allowed to register accounts so quickly","Пользователи не могут регистрировать учётные записи так быстро"}. {"Users Last Activity","Статистика последнего подключения пользователей"}. {"Users","Пользователи"}. -{"User ","Пользователь "}. {"User","Пользователь"}. -{"Validate","Утвердить"}. +{"Value 'get' of 'type' attribute is not allowed","Значение 'get' атрибута 'type' недопустимо"}. +{"Value of '~s' should be boolean","Значение '~s' должно быть булевым"}. +{"Value of '~s' should be datetime string","Значение '~s' должно быть датой"}. +{"Value of '~s' should be integer","Значение '~s' должно быть целочисленным"}. +{"Value 'set' of 'type' attribute is not allowed","Значение 'set' атрибута 'type' недопустимо"}. {"vCard User Search","Поиск пользователей по vCard"}. {"Virtual Hosts","Виртуальные хосты"}. {"Visitors are not allowed to change their nicknames in this room","Посетителям запрещено изменять свои псевдонимы в этой комнате"}. @@ -406,16 +424,17 @@ {"Wednesday","Среда"}. {"When to send the last published item","Когда посылать последний опубликованный элемент"}. {"Whether to allow subscriptions","Разрешить подписку"}. -{"You can later change your password using a Jabber client.","Позже Вы можете изменить пароль через Jabber-клиент."}. +{"Wrong parameters in the web formulary","Недопустимые параметры веб-формы"}. +{"Wrong xmlns","Неправильный xmlns"}. +{"You are being removed from the room because of a system shutdown","Вы покинули комнату из-за останова системы"}. +{"You are not joined to the channel","Вы не присоединены к каналу"}. {"You have been banned from this room","Вам запрещено входить в эту конференцию"}. +{"You have joined too many conferences","Вы присоединены к слишком большому количеству конференций"}. {"You must fill in field \"Nickname\" in the form","Вы должны заполнить поле \"Псевдоним\" в форме"}. {"You need a client that supports x:data and CAPTCHA to register","Чтобы зарегистрироваться, требуется x:data-совместимый клиент"}. {"You need a client that supports x:data to register the nickname","Чтобы зарегистрировать псевдоним, требуется x:data-совместимый клиент"}. -{"You need an x:data capable client to configure mod_irc settings","Чтобы настроить параметры mod_irc, требуется x:data-совместимый клиент"}. -{"You need an x:data capable client to configure room","Чтобы сконфигурировать комнату, требуется 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 Jabber account was successfully created.","Ваш Jabber-аккаунт был успешно создан."}. -{"Your Jabber account was successfully deleted.","Ваш Jabber-аккаунт был успешно удален."}. -{"Your messages to ~s are being blocked. To unblock them, visit ~s","Ваши сообщения к ~s блокируются. Для снятия блокировки перейдите по ссылке ~s"}. +{"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","Ваши запросы на добавление в контакт-лист, а также сообщения к ~s блокируются. Для снятия блокировки перейдите по ссылке ~s"}. +{"You're not allowed to create nodes","Вам не разрешается создавать узлы"}. diff --git a/priv/msgs/ru.po b/priv/msgs/ru.po deleted file mode 100644 index 27f6a40c6..000000000 --- a/priv/msgs/ru.po +++ /dev/null @@ -1,1862 +0,0 @@ -# , 2010. -msgid "" -msgstr "" -"Project-Id-Version: 2.1.0-alpha\n" -"POT-Creation-Date: \n" -"PO-Revision-Date: 2012-04-15 13:38+0900\n" -"Last-Translator: Evgeniy Khramtsov \n" -"Language-Team: Russian \n" -"Language: ru\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Language: Russian (русский)\n" -"X-Additional-Translator: Konstantin Khomoutov\n" -"X-Additional-Translator: Sergei Golovan\n" -"X-Generator: Lokalize 1.0\n" -"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" -"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" - -#: ejabberd_c2s.erl:424 ejabberd_c2s.erl:727 -msgid "Use of STARTTLS required" -msgstr "Вы обязаны использовать STARTTLS" - -#: ejabberd_c2s.erl:503 -msgid "No resource provided" -msgstr "Не указан ресурс" - -#: ejabberd_c2s.erl:1197 -msgid "Replaced by new connection" -msgstr "Заменено новым соединением" - -#: ejabberd_c2s.erl:1885 -msgid "Your active privacy list has denied the routing of this stanza." -msgstr "" -"Маршрутизация этой строфы запрещена вашим активным списком приватности." - -#: ejabberd_captcha.erl:96 ejabberd_captcha.erl:152 ejabberd_captcha.erl:178 -msgid "Enter the text you see" -msgstr "Введите увиденный текст" - -#: ejabberd_captcha.erl:101 -msgid "Your messages to ~s are being blocked. To unblock them, visit ~s" -msgstr "" -"Ваши сообщения к ~s блокируются. Для снятия блокировки перейдите по ссылке ~s" - -#: ejabberd_captcha.erl:134 -msgid "If you don't see the CAPTCHA image here, visit the web page." -msgstr "Если вы не видите изображение капчи, перейдите по ссылке." - -#: ejabberd_captcha.erl:146 -msgid "CAPTCHA web page" -msgstr "Ссылка на капчу" - -#: ejabberd_captcha.erl:307 -msgid "The CAPTCHA is valid." -msgstr "Проверка капчи прошла успешно." - -#: mod_adhoc.erl:95 mod_adhoc.erl:125 mod_adhoc.erl:143 mod_adhoc.erl:161 -msgid "Commands" -msgstr "Команды" - -#: mod_adhoc.erl:149 mod_adhoc.erl:243 -msgid "Ping" -msgstr "Пинг" - -#: mod_adhoc.erl:260 -msgid "Pong" -msgstr "Понг" - -#: mod_announce.erl:507 mod_announce_odbc.erl:499 -msgid "Really delete message of the day?" -msgstr "Действительно удалить сообщение дня?" - -#: mod_announce.erl:515 mod_announce_odbc.erl:507 mod_configure.erl:1083 -#: mod_configure.erl:1128 -msgid "Subject" -msgstr "Тема" - -#: mod_announce.erl:520 mod_announce_odbc.erl:512 mod_configure.erl:1088 -#: mod_configure.erl:1133 -msgid "Message body" -msgstr "Тело сообщения" - -#: mod_announce.erl:600 mod_announce_odbc.erl:592 -msgid "No body provided for announce message" -msgstr "Тело объявления не должно быть пустым" - -#: mod_announce.erl:635 mod_announce_odbc.erl:627 -msgid "Announcements" -msgstr "Объявления" - -#: mod_announce.erl:637 mod_announce_odbc.erl:629 -msgid "Send announcement to all users" -msgstr "Разослать объявление всем пользователям" - -#: mod_announce.erl:639 mod_announce_odbc.erl:631 -msgid "Send announcement to all users on all hosts" -msgstr "Разослать объявление всем пользователям на всех виртуальных серверах" - -#: mod_announce.erl:641 mod_announce_odbc.erl:633 -msgid "Send announcement to all online users" -msgstr "Разослать объявление всем подключённым пользователям" - -#: mod_announce.erl:643 mod_announce_odbc.erl:635 mod_configure.erl:1078 -#: mod_configure.erl:1123 -msgid "Send announcement to all online users on all hosts" -msgstr "" -"Разослать объявление всем подключённым пользователям на всех виртуальных " -"серверах" - -#: mod_announce.erl:645 mod_announce_odbc.erl:637 -msgid "Set message of the day and send to online users" -msgstr "Установить сообщение дня и разослать его подключённым пользователям" - -#: mod_announce.erl:647 mod_announce_odbc.erl:639 -msgid "Set message of the day on all hosts and send to online users" -msgstr "" -"Установить сообщение дня на всех виртуальных серверах и разослать его " -"подключённым пользователям" - -#: mod_announce.erl:649 mod_announce_odbc.erl:641 -msgid "Update message of the day (don't send)" -msgstr "Обновить сообщение дня (не рассылать)" - -#: mod_announce.erl:651 mod_announce_odbc.erl:643 -msgid "Update message of the day on all hosts (don't send)" -msgstr "Обновить сообщение дня на всех виртуальных серверах (не рассылать)" - -#: mod_announce.erl:653 mod_announce_odbc.erl:645 -msgid "Delete message of the day" -msgstr "Удалить сообщение дня" - -#: mod_announce.erl:655 mod_announce_odbc.erl:647 -msgid "Delete message of the day on all hosts" -msgstr "Удалить сообщение дня со всех виртуальных серверов" - -#: mod_configure.erl:114 mod_configure.erl:274 mod_configure.erl:296 -#: mod_configure.erl:498 -msgid "Configuration" -msgstr "Конфигурация" - -#: mod_configure.erl:125 mod_configure.erl:576 web/ejabberd_web_admin.erl:1936 -msgid "Database" -msgstr "База данных" - -#: mod_configure.erl:127 mod_configure.erl:595 -msgid "Start Modules" -msgstr "Запуск модулей" - -#: mod_configure.erl:129 mod_configure.erl:596 -msgid "Stop Modules" -msgstr "Остановка модулей" - -#: mod_configure.erl:131 mod_configure.erl:604 web/ejabberd_web_admin.erl:1937 -msgid "Backup" -msgstr "Резервное копирование" - -#: mod_configure.erl:133 mod_configure.erl:605 -msgid "Restore" -msgstr "Восстановление из резервной копии" - -#: mod_configure.erl:135 mod_configure.erl:606 -msgid "Dump to Text File" -msgstr "Копирование в текстовый файл" - -#: mod_configure.erl:137 mod_configure.erl:615 -msgid "Import File" -msgstr "Импорт из файла" - -#: mod_configure.erl:139 mod_configure.erl:616 -msgid "Import Directory" -msgstr "Импорт из директории" - -#: mod_configure.erl:141 mod_configure.erl:581 mod_configure.erl:1057 -msgid "Restart Service" -msgstr "Перезапустить службу" - -#: mod_configure.erl:143 mod_configure.erl:582 mod_configure.erl:1102 -msgid "Shut Down Service" -msgstr "Остановить службу" - -#: mod_configure.erl:145 mod_configure.erl:518 mod_configure.erl:1197 -#: web/ejabberd_web_admin.erl:1527 -msgid "Add User" -msgstr "Добавить пользователя" - -#: mod_configure.erl:147 mod_configure.erl:519 mod_configure.erl:1219 -msgid "Delete User" -msgstr "Удалить пользователя" - -#: mod_configure.erl:149 mod_configure.erl:520 mod_configure.erl:1231 -msgid "End User Session" -msgstr "Завершить сеанс пользователя" - -#: mod_configure.erl:151 mod_configure.erl:521 mod_configure.erl:1243 -#: mod_configure.erl:1255 -msgid "Get User Password" -msgstr "Получить пароль пользователя" - -#: mod_configure.erl:153 mod_configure.erl:522 -msgid "Change User Password" -msgstr "Изменить пароль пользователя" - -#: mod_configure.erl:155 mod_configure.erl:523 mod_configure.erl:1272 -msgid "Get User Last Login Time" -msgstr "Получить время последнего подключения пользователя" - -#: mod_configure.erl:157 mod_configure.erl:524 mod_configure.erl:1284 -msgid "Get User Statistics" -msgstr "Получить статистику по пользователю" - -#: mod_configure.erl:159 mod_configure.erl:525 -msgid "Get Number of Registered Users" -msgstr "Получить количество зарегистрированных пользователей" - -#: mod_configure.erl:161 mod_configure.erl:526 -msgid "Get Number of Online Users" -msgstr "Получить количество подключённых пользователей" - -#: mod_configure.erl:163 mod_configure.erl:509 web/ejabberd_web_admin.erl:831 -#: web/ejabberd_web_admin.erl:872 -msgid "Access Control Lists" -msgstr "Списки управления доступом" - -#: mod_configure.erl:165 mod_configure.erl:510 web/ejabberd_web_admin.erl:940 -#: web/ejabberd_web_admin.erl:976 -msgid "Access Rules" -msgstr "Правила доступа" - -#: mod_configure.erl:297 mod_configure.erl:499 -msgid "User Management" -msgstr "Управление пользователями" - -#: mod_configure.erl:500 web/ejabberd_web_admin.erl:1058 -#: web/ejabberd_web_admin.erl:1462 -msgid "Online Users" -msgstr "Подключённые пользователи" - -#: mod_configure.erl:501 -msgid "All Users" -msgstr "Все пользователи" - -#: mod_configure.erl:502 -msgid "Outgoing s2s Connections" -msgstr "Исходящие s2s-соединения" - -#: mod_configure.erl:503 web/ejabberd_web_admin.erl:1907 -msgid "Running Nodes" -msgstr "Работающие узлы" - -#: mod_configure.erl:504 web/ejabberd_web_admin.erl:1909 -msgid "Stopped Nodes" -msgstr "Остановленные узлы" - -#: mod_configure.erl:577 mod_configure.erl:587 web/ejabberd_web_admin.erl:1953 -msgid "Modules" -msgstr "Модули" - -#: mod_configure.erl:578 -msgid "Backup Management" -msgstr "Управление резервным копированием" - -#: mod_configure.erl:579 -msgid "Import Users From jabberd14 Spool Files" -msgstr "Импорт пользователей из спула jabberd14" - -#: mod_configure.erl:699 -msgid "To ~s" -msgstr "К ~s" - -#: mod_configure.erl:717 -msgid "From ~s" -msgstr "От ~s" - -#: mod_configure.erl:913 -msgid "Database Tables Configuration at " -msgstr "Конфигурация таблиц базы данных на " - -#: mod_configure.erl:918 -msgid "Choose storage type of tables" -msgstr "Выберите тип хранения таблиц" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Disc only copy" -msgstr "только диск" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM and disc copy" -msgstr "ОЗУ и диск" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM copy" -msgstr "ОЗУ" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Remote copy" -msgstr "не хранится локально" - -#: mod_configure.erl:950 -msgid "Stop Modules at " -msgstr "Остановка модулей на " - -#: mod_configure.erl:954 -msgid "Choose modules to stop" -msgstr "Выберите модули, которые следует остановить" - -#: mod_configure.erl:969 -msgid "Start Modules at " -msgstr "Запуск модулей на " - -#: mod_configure.erl:973 -msgid "Enter list of {Module, [Options]}" -msgstr "Введите список вида {Module, [Options]}" - -#: mod_configure.erl:974 -msgid "List of modules to start" -msgstr "Список запускаемых модулей" - -#: mod_configure.erl:983 -msgid "Backup to File at " -msgstr "Резервное копирование в файл на " - -#: mod_configure.erl:987 mod_configure.erl:1001 -msgid "Enter path to backup file" -msgstr "Введите путь к резервному файлу" - -#: mod_configure.erl:988 mod_configure.erl:1002 mod_configure.erl:1016 -#: mod_configure.erl:1030 -msgid "Path to File" -msgstr "Путь к файлу" - -#: mod_configure.erl:997 -msgid "Restore Backup from File at " -msgstr "Восстановление из резервной копии на " - -#: mod_configure.erl:1011 -msgid "Dump Backup to Text File at " -msgstr "Копирование в текстовый файл на " - -#: mod_configure.erl:1015 -msgid "Enter path to text file" -msgstr "Введите путь к текстовому файлу" - -#: mod_configure.erl:1025 -msgid "Import User from File at " -msgstr "Импорт пользователя из файла на " - -#: mod_configure.erl:1029 -msgid "Enter path to jabberd14 spool file" -msgstr "Введите путь к файлу из спула jabberd14" - -#: mod_configure.erl:1039 -msgid "Import Users from Dir at " -msgstr "Импорт пользователей из директории на " - -#: mod_configure.erl:1043 -msgid "Enter path to jabberd14 spool dir" -msgstr "Введите путь к директории спула jabberd14" - -#: mod_configure.erl:1044 -msgid "Path to Dir" -msgstr "Путь к директории" - -#: mod_configure.erl:1060 mod_configure.erl:1105 -msgid "Time delay" -msgstr "По истечение" - -#: mod_configure.erl:1143 -msgid "Access Control List Configuration" -msgstr "Конфигурация списков управления доступом" - -#: mod_configure.erl:1147 -msgid "Access control lists" -msgstr "Списки управления доступом" - -#: mod_configure.erl:1171 -msgid "Access Configuration" -msgstr "Конфигурация доступа" - -#: mod_configure.erl:1175 -msgid "Access rules" -msgstr "Правила доступа" - -#: mod_configure.erl:1200 mod_configure.erl:1222 mod_configure.erl:1234 -#: mod_configure.erl:1246 mod_configure.erl:1258 mod_configure.erl:1275 -#: mod_configure.erl:1287 mod_configure.erl:1650 mod_configure.erl:1700 -#: mod_configure.erl:1721 mod_roster.erl:943 mod_roster_odbc.erl:1060 -#: mod_vcard.erl:472 mod_vcard_ldap.erl:554 mod_vcard_odbc.erl:448 -msgid "Jabber ID" -msgstr "Jabber ID" - -#: mod_configure.erl:1205 mod_configure.erl:1263 mod_configure.erl:1651 -#: mod_configure.erl:1863 mod_muc/mod_muc_room.erl:3224 mod_register.erl:235 -#: web/ejabberd_web_admin.erl:1520 -msgid "Password" -msgstr "Пароль" - -#: mod_configure.erl:1210 -msgid "Password Verification" -msgstr "Проверка пароля" - -#: mod_configure.erl:1301 -msgid "Number of registered users" -msgstr "Количество зарегистрированных пользователей" - -#: mod_configure.erl:1315 -msgid "Number of online users" -msgstr "Количество подключённых пользователей" - -#: mod_configure.erl:1682 web/ejabberd_web_admin.erl:1588 -#: web/ejabberd_web_admin.erl:1743 -msgid "Never" -msgstr "Никогда" - -#: mod_configure.erl:1696 web/ejabberd_web_admin.erl:1601 -#: web/ejabberd_web_admin.erl:1756 -msgid "Online" -msgstr "Подключён" - -#: mod_configure.erl:1701 -msgid "Last login" -msgstr "Время последнего подключения" - -#: mod_configure.erl:1722 -msgid "Roster size" -msgstr "Размер списка контактов" - -#: mod_configure.erl:1723 -msgid "IP addresses" -msgstr "IP адреса" - -#: mod_configure.erl:1724 -msgid "Resources" -msgstr "Ресурсы" - -#: mod_configure.erl:1850 -msgid "Administration of " -msgstr "Администрирование " - -#: mod_configure.erl:1853 -msgid "Action on user" -msgstr "Действие над пользователем" - -#: mod_configure.erl:1857 -msgid "Edit Properties" -msgstr "Изменить параметры" - -#: mod_configure.erl:1860 web/ejabberd_web_admin.erl:1769 -msgid "Remove User" -msgstr "Удалить пользователя" - -#: mod_irc/mod_irc.erl:201 mod_irc/mod_irc_odbc.erl:196 -#: mod_muc/mod_muc.erl:336 mod_muc/mod_muc_odbc.erl:342 -msgid "Access denied by service policy" -msgstr "Доступ запрещён политикой службы" - -#: mod_irc/mod_irc.erl:401 mod_irc/mod_irc_odbc.erl:398 -msgid "IRC Transport" -msgstr "IRC Транспорт" - -#: mod_irc/mod_irc.erl:428 mod_irc/mod_irc_odbc.erl:425 -msgid "ejabberd IRC module" -msgstr "ejabberd IRC модуль" - -#: mod_irc/mod_irc.erl:559 mod_irc/mod_irc_odbc.erl:568 -msgid "You need an x:data capable client to configure mod_irc settings" -msgstr "Чтобы настроить параметры mod_irc, требуется x:data-совместимый клиент" - -#: mod_irc/mod_irc.erl:566 mod_irc/mod_irc_odbc.erl:575 -msgid "Registration in mod_irc for " -msgstr "Регистрация в mod_irc для " - -#: mod_irc/mod_irc.erl:571 mod_irc/mod_irc_odbc.erl:580 -msgid "" -"Enter username, encodings, ports and passwords you wish to use for " -"connecting to IRC servers" -msgstr "" -"Введите имя пользователя, кодировки, порты и пароли, которые будут " -"использоваться при подключении к IRC-серверам" - -#: mod_irc/mod_irc.erl:576 mod_irc/mod_irc_odbc.erl:585 -msgid "IRC Username" -msgstr "Имя пользователя IRC" - -#: mod_irc/mod_irc.erl:586 mod_irc/mod_irc_odbc.erl:595 -msgid "" -"If you want to specify different ports, passwords, encodings for IRC " -"servers, fill this list with values in format '{\"irc server\", \"encoding" -"\", port, \"password\"}'. By default this service use \"~s\" encoding, port " -"~p, empty password." -msgstr "" -"Чтобы указать различные порты, пароли, кодировки для разных серверов IRC, " -"заполните список значениями в формате '{\"сервер IRC\", \"кодировка\", порт, " -"\"пароль\"}'. По умолчанию сервис использует кодировку \"~s\", порт ~p, " -"пустой пароль." - -#: mod_irc/mod_irc.erl:598 mod_irc/mod_irc_odbc.erl:607 -msgid "" -"Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef." -"net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]." -msgstr "" -"Пример: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef." -"net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]." - -#: mod_irc/mod_irc.erl:603 mod_irc/mod_irc_odbc.erl:612 -msgid "Connections parameters" -msgstr "Параметры соединения" - -#: mod_irc/mod_irc.erl:728 mod_irc/mod_irc_odbc.erl:757 -msgid "Join IRC channel" -msgstr "Присоединиться к каналу IRC" - -#: mod_irc/mod_irc.erl:732 mod_irc/mod_irc_odbc.erl:761 -msgid "IRC channel (don't put the first #)" -msgstr "Канал IRC (без символа #)" - -#: mod_irc/mod_irc.erl:737 mod_irc/mod_irc_odbc.erl:766 -msgid "IRC server" -msgstr "Сервер IRC" - -#: mod_irc/mod_irc.erl:770 mod_irc/mod_irc.erl:774 -#: mod_irc/mod_irc_odbc.erl:799 mod_irc/mod_irc_odbc.erl:803 -msgid "Join the IRC channel here." -msgstr "Присоединяйтесь к каналу IRC" - -#: mod_irc/mod_irc.erl:778 mod_irc/mod_irc_odbc.erl:807 -msgid "Join the IRC channel in this Jabber ID: ~s" -msgstr "Присоединиться к каналу IRC с Jabber ID: ~s" - -#: mod_irc/mod_irc.erl:863 mod_irc/mod_irc_odbc.erl:904 -msgid "IRC settings" -msgstr "Настройки IRC" - -#: mod_irc/mod_irc.erl:868 mod_irc/mod_irc_odbc.erl:909 -msgid "" -"Enter username and encodings you wish to use for connecting to IRC servers. " -"Press 'Next' to get more fields to fill in. Press 'Complete' to save " -"settings." -msgstr "" -"Введите имя пользователя и кодировки, которые будут использоваться при " -"подключении к IRC-серверам. Нажмите 'Далее' для получения дополнительных " -"полей для заполнения. Нажмите 'Завершить' для сохранения настроек." - -#: mod_irc/mod_irc.erl:874 mod_irc/mod_irc_odbc.erl:915 -msgid "IRC username" -msgstr "Имя пользователя IRC" - -#: mod_irc/mod_irc.erl:923 mod_irc/mod_irc_odbc.erl:964 -msgid "Password ~b" -msgstr "Пароль ~b" - -#: mod_irc/mod_irc.erl:928 mod_irc/mod_irc_odbc.erl:969 -msgid "Port ~b" -msgstr "Порт ~b" - -#: mod_irc/mod_irc.erl:933 mod_irc/mod_irc_odbc.erl:974 -msgid "Encoding for server ~b" -msgstr "Кодировка сервера ~b" - -#: mod_irc/mod_irc.erl:942 mod_irc/mod_irc_odbc.erl:983 -msgid "Server ~b" -msgstr "Сервер ~b" - -#: mod_muc/mod_muc.erl:449 mod_muc/mod_muc_odbc.erl:456 -msgid "Only service administrators are allowed to send service messages" -msgstr "Только администратор службы может посылать служебные сообщения" - -#: mod_muc/mod_muc.erl:493 mod_muc/mod_muc_odbc.erl:500 -msgid "Room creation is denied by service policy" -msgstr "Cоздавать конференцию запрещено политикой службы" - -#: mod_muc/mod_muc.erl:500 mod_muc/mod_muc_odbc.erl:507 -msgid "Conference room does not exist" -msgstr "Конференция не существует" - -#: mod_muc/mod_muc.erl:582 mod_muc/mod_muc_odbc.erl:595 -msgid "Chatrooms" -msgstr "Комнаты" - -#: mod_muc/mod_muc.erl:712 mod_muc/mod_muc_odbc.erl:729 -msgid "You need a client that supports x:data to register the nickname" -msgstr "Чтобы зарегистрировать псевдоним, требуется x:data-совместимый клиент" - -#: mod_muc/mod_muc.erl:718 mod_muc/mod_muc_odbc.erl:735 -msgid "Nickname Registration at " -msgstr "Регистрация псевдонима на " - -#: mod_muc/mod_muc.erl:722 mod_muc/mod_muc_odbc.erl:739 -msgid "Enter nickname you want to register" -msgstr "Введите псевдоним, который Вы хотели бы зарегистрировать" - -#: mod_muc/mod_muc.erl:723 mod_muc/mod_muc_odbc.erl:740 -#: mod_muc/mod_muc_room.erl:3810 mod_roster.erl:944 mod_roster_odbc.erl:1061 -#: mod_vcard.erl:364 mod_vcard.erl:477 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:453 -msgid "Nickname" -msgstr "Псевдоним" - -#: mod_muc/mod_muc.erl:762 mod_muc/mod_muc_odbc.erl:784 -#: mod_muc/mod_muc_room.erl:1064 mod_muc/mod_muc_room.erl:1787 -msgid "That nickname is registered by another person" -msgstr "Этот псевдоним зарегистрирован кем-то другим" - -#: mod_muc/mod_muc.erl:788 mod_muc/mod_muc_odbc.erl:811 -msgid "You must fill in field \"Nickname\" in the form" -msgstr "Вы должны заполнить поле \"Псевдоним\" в форме" - -#: mod_muc/mod_muc.erl:808 mod_muc/mod_muc_odbc.erl:831 -msgid "ejabberd MUC module" -msgstr "ejabberd MUC модуль" - -#: mod_muc/mod_muc_log.erl:374 mod_muc/mod_muc_log.erl:381 -msgid "Chatroom configuration modified" -msgstr "Конфигурация комнаты изменилась" - -#: mod_muc/mod_muc_log.erl:384 -msgid "joins the room" -msgstr "вошёл(а) в комнату" - -#: mod_muc/mod_muc_log.erl:387 mod_muc/mod_muc_log.erl:390 -msgid "leaves the room" -msgstr "вышел(а) из комнаты" - -#: mod_muc/mod_muc_log.erl:393 mod_muc/mod_muc_log.erl:396 -msgid "has been banned" -msgstr "запретили входить в комнату" - -#: mod_muc/mod_muc_log.erl:399 mod_muc/mod_muc_log.erl:402 -msgid "has been kicked" -msgstr "выгнали из комнаты" - -#: mod_muc/mod_muc_log.erl:405 -msgid "has been kicked because of an affiliation change" -msgstr "выгнали из комнаты вследствие смены ранга" - -#: mod_muc/mod_muc_log.erl:408 -msgid "has been kicked because the room has been changed to members-only" -msgstr "выгнали из комнаты потому что она стала только для членов" - -#: mod_muc/mod_muc_log.erl:411 -msgid "has been kicked because of a system shutdown" -msgstr "выгнали из комнаты из-за останова системы" - -#: mod_muc/mod_muc_log.erl:414 -msgid "is now known as" -msgstr "изменил(а) имя на" - -#: mod_muc/mod_muc_log.erl:417 mod_muc/mod_muc_log.erl:688 -#: mod_muc/mod_muc_room.erl:2393 -msgid " has set the subject to: " -msgstr " установил(а) тему: " - -#: mod_muc/mod_muc_log.erl:452 -msgid "Chatroom is created" -msgstr "Комната создана" - -#: mod_muc/mod_muc_log.erl:453 -msgid "Chatroom is destroyed" -msgstr "Комната уничтожена" - -#: mod_muc/mod_muc_log.erl:454 -msgid "Chatroom is started" -msgstr "Комната запущена" - -#: mod_muc/mod_muc_log.erl:455 -msgid "Chatroom is stopped" -msgstr "Комната остановлена" - -#: mod_muc/mod_muc_log.erl:459 -msgid "Monday" -msgstr "Понедельник" - -#: mod_muc/mod_muc_log.erl:460 -msgid "Tuesday" -msgstr "Вторник" - -#: mod_muc/mod_muc_log.erl:461 -msgid "Wednesday" -msgstr "Среда" - -#: mod_muc/mod_muc_log.erl:462 -msgid "Thursday" -msgstr "Четверг" - -#: mod_muc/mod_muc_log.erl:463 -msgid "Friday" -msgstr "Пятница" - -#: mod_muc/mod_muc_log.erl:464 -msgid "Saturday" -msgstr "Суббота" - -#: mod_muc/mod_muc_log.erl:465 -msgid "Sunday" -msgstr "Воскресенье" - -#: mod_muc/mod_muc_log.erl:469 -msgid "January" -msgstr "января" - -#: mod_muc/mod_muc_log.erl:470 -msgid "February" -msgstr "февраля" - -#: mod_muc/mod_muc_log.erl:471 -msgid "March" -msgstr "марта" - -#: mod_muc/mod_muc_log.erl:472 -msgid "April" -msgstr "апреля" - -#: mod_muc/mod_muc_log.erl:473 -msgid "May" -msgstr "мая" - -#: mod_muc/mod_muc_log.erl:474 -msgid "June" -msgstr "июня" - -#: mod_muc/mod_muc_log.erl:475 -msgid "July" -msgstr "июля" - -#: mod_muc/mod_muc_log.erl:476 -msgid "August" -msgstr "августа" - -#: mod_muc/mod_muc_log.erl:477 -msgid "September" -msgstr "сентября" - -#: mod_muc/mod_muc_log.erl:478 -msgid "October" -msgstr "октября" - -#: mod_muc/mod_muc_log.erl:479 -msgid "November" -msgstr "ноября" - -#: mod_muc/mod_muc_log.erl:480 -msgid "December" -msgstr "декабря" - -#: mod_muc/mod_muc_log.erl:750 -msgid "Room Configuration" -msgstr "Конфигурация комнаты" - -#: mod_muc/mod_muc_log.erl:759 -msgid "Room Occupants" -msgstr "Участники комнаты" - -#: mod_muc/mod_muc_room.erl:174 -msgid "Traffic rate limit is exceeded" -msgstr "Превышен лимит скорости посылки информации" - -#: mod_muc/mod_muc_room.erl:246 -msgid "" -"This participant is kicked from the room because he sent an error message" -msgstr "" -"Этого участника выгнали из комнаты за то, что он послал сообщение об ошибке" - -#: mod_muc/mod_muc_room.erl:255 -msgid "It is not allowed to send private messages to the conference" -msgstr "Не разрешается посылать частные сообщения прямо в конференцию" - -#: mod_muc/mod_muc_room.erl:332 -msgid "Please, wait for a while before sending new voice request" -msgstr "" -"Пожалуйста, подождите перед тем как подать новый запрос на право голоса" - -#: mod_muc/mod_muc_room.erl:347 -msgid "Voice requests are disabled in this conference" -msgstr "Запросы на право голоса отключены в этой конференции" - -#: mod_muc/mod_muc_room.erl:364 -msgid "Failed to extract JID from your voice request approval" -msgstr "Ошибка обработки JID из вашего запроса на право голоса" - -#: mod_muc/mod_muc_room.erl:393 -msgid "Only moderators can approve voice requests" -msgstr "Только модераторы могут утверждать запросы на право голоса" - -#: mod_muc/mod_muc_room.erl:408 -msgid "Improper message type" -msgstr "Неправильный тип сообщения" - -#: mod_muc/mod_muc_room.erl:518 -msgid "" -"This participant is kicked from the room because he sent an error message to " -"another participant" -msgstr "" -"Этого участника выгнали из комнаты за то, что он послал сообщение об ошибке " -"другому участнику" - -#: mod_muc/mod_muc_room.erl:531 -msgid "It is not allowed to send private messages of type \"groupchat\"" -msgstr "Нельзя посылать частные сообщения типа \"groupchat\"" - -#: mod_muc/mod_muc_room.erl:543 mod_muc/mod_muc_room.erl:611 -msgid "Recipient is not in the conference room" -msgstr "Адресата нет в конференции" - -#: mod_muc/mod_muc_room.erl:564 mod_muc/mod_muc_room.erl:585 -msgid "It is not allowed to send private messages" -msgstr "Запрещено посылать приватные сообщения" - -#: mod_muc/mod_muc_room.erl:576 mod_muc/mod_muc_room.erl:956 -#: mod_muc/mod_muc_room.erl:4040 -msgid "Only occupants are allowed to send messages to the conference" -msgstr "Только присутствующим разрешается посылать сообщения в конференцию" - -#: mod_muc/mod_muc_room.erl:634 -msgid "Only occupants are allowed to send queries to the conference" -msgstr "Только присутствующим разрешается посылать запросы в конференцию" - -#: mod_muc/mod_muc_room.erl:646 -msgid "Queries to the conference members are not allowed in this room" -msgstr "Запросы к пользователям в этой конференции запрещены" - -#: mod_muc/mod_muc_room.erl:932 -msgid "" -"Only moderators and participants are allowed to change the subject in this " -"room" -msgstr "Только модераторы и участники могут изменять тему в этой комнате" - -#: mod_muc/mod_muc_room.erl:937 -msgid "Only moderators are allowed to change the subject in this room" -msgstr "Только модераторы могут изменять тему в этой комнате" - -#: mod_muc/mod_muc_room.erl:947 -msgid "Visitors are not allowed to send messages to all occupants" -msgstr "Посетителям не разрешается посылать сообщения всем присутствующим" - -#: mod_muc/mod_muc_room.erl:1021 -msgid "" -"This participant is kicked from the room because he sent an error presence" -msgstr "" -"Этого участника выгнали из комнаты за то, что он послал присутствие с ошибкой" - -#: mod_muc/mod_muc_room.erl:1040 -msgid "Visitors are not allowed to change their nicknames in this room" -msgstr "Посетителям запрещено изменять свои псевдонимы в этой комнате" - -#: mod_muc/mod_muc_room.erl:1053 mod_muc/mod_muc_room.erl:1779 -msgid "That nickname is already in use by another occupant" -msgstr "Этот псевдоним уже занят другим участником" - -#: mod_muc/mod_muc_room.erl:1768 -msgid "You have been banned from this room" -msgstr "Вам запрещено входить в эту конференцию" - -#: mod_muc/mod_muc_room.erl:1771 -msgid "Membership is required to enter this room" -msgstr "В эту конференцию могут входить только её члены" - -#: mod_muc/mod_muc_room.erl:1807 -msgid "This room is not anonymous" -msgstr "Эта комната не анонимная" - -#: mod_muc/mod_muc_room.erl:1833 -msgid "A password is required to enter this room" -msgstr "Чтобы войти в эту конференцию, нужен пароль" - -#: mod_muc/mod_muc_room.erl:1855 mod_register.erl:246 -msgid "Too many CAPTCHA requests" -msgstr "Слишком много запросов капчи" - -#: mod_muc/mod_muc_room.erl:1864 mod_register.erl:251 -msgid "Unable to generate a CAPTCHA" -msgstr "Не получилось создать капчу" - -#: mod_muc/mod_muc_room.erl:1874 -msgid "Incorrect password" -msgstr "Неправильный пароль" - -#: mod_muc/mod_muc_room.erl:2448 -msgid "Administrator privileges required" -msgstr "Требуются права администратора" - -#: mod_muc/mod_muc_room.erl:2463 -msgid "Moderator privileges required" -msgstr "Требуются права модератора" - -#: mod_muc/mod_muc_room.erl:2619 -msgid "Jabber ID ~s is invalid" -msgstr "Jabber ID ~s недопустимый" - -#: mod_muc/mod_muc_room.erl:2633 -msgid "Nickname ~s does not exist in the room" -msgstr "Псевдоним ~s в комнате отсутствует" - -#: mod_muc/mod_muc_room.erl:2659 mod_muc/mod_muc_room.erl:3049 -msgid "Invalid affiliation: ~s" -msgstr "Недопустимый ранг: ~s" - -#: mod_muc/mod_muc_room.erl:2713 -msgid "Invalid role: ~s" -msgstr "Недопустимая роль: ~s" - -#: mod_muc/mod_muc_room.erl:3026 mod_muc/mod_muc_room.erl:3062 -msgid "Owner privileges required" -msgstr "Требуются права владельца" - -#: mod_muc/mod_muc_room.erl:3195 -msgid "Configuration of room ~s" -msgstr "Конфигурация комнаты ~s" - -#: mod_muc/mod_muc_room.erl:3200 -msgid "Room title" -msgstr "Название комнаты" - -#: mod_muc/mod_muc_room.erl:3203 mod_muc/mod_muc_room.erl:3692 -msgid "Room description" -msgstr "Описание комнаты" - -#: mod_muc/mod_muc_room.erl:3210 -msgid "Make room persistent" -msgstr "Сделать комнату постоянной" - -#: mod_muc/mod_muc_room.erl:3215 -msgid "Make room public searchable" -msgstr "Сделать комнату видимой всем" - -#: mod_muc/mod_muc_room.erl:3218 -msgid "Make participants list public" -msgstr "Сделать список участников видимым всем" - -#: mod_muc/mod_muc_room.erl:3221 -msgid "Make room password protected" -msgstr "Сделать комнату защищённой паролем" - -#: mod_muc/mod_muc_room.erl:3232 -msgid "Maximum Number of Occupants" -msgstr "Максимальное количество участников" - -#: mod_muc/mod_muc_room.erl:3239 -msgid "No limit" -msgstr "Не ограничено" - -#: mod_muc/mod_muc_room.erl:3250 -msgid "Present real Jabber IDs to" -msgstr "Сделать реальные Jabber ID участников видимыми" - -#: mod_muc/mod_muc_room.erl:3258 mod_muc/mod_muc_room.erl:3292 -msgid "moderators only" -msgstr "только модераторам" - -#: mod_muc/mod_muc_room.erl:3260 mod_muc/mod_muc_room.erl:3294 -msgid "anyone" -msgstr "всем участникам" - -#: mod_muc/mod_muc_room.erl:3262 -msgid "Make room members-only" -msgstr "Комната только для зарегистрированных участников" - -#: mod_muc/mod_muc_room.erl:3265 -msgid "Make room moderated" -msgstr "Сделать комнату модерируемой" - -#: mod_muc/mod_muc_room.erl:3268 -msgid "Default users as participants" -msgstr "Сделать пользователей участниками по умолчанию" - -#: mod_muc/mod_muc_room.erl:3271 -msgid "Allow users to change the subject" -msgstr "Разрешить пользователям изменять тему" - -#: mod_muc/mod_muc_room.erl:3274 -msgid "Allow users to send private messages" -msgstr "Разрешить приватные сообщения" - -#: mod_muc/mod_muc_room.erl:3279 -msgid "Allow visitors to send private messages to" -msgstr "Разрешить посетителям посылать приватные сообщения" - -#: mod_muc/mod_muc_room.erl:3290 -msgid "nobody" -msgstr "никто" - -#: mod_muc/mod_muc_room.erl:3296 -msgid "Allow users to query other users" -msgstr "Разрешить iq-запросы к пользователям" - -#: mod_muc/mod_muc_room.erl:3299 -msgid "Allow users to send invites" -msgstr "Разрешить пользователям посылать приглашения" - -#: mod_muc/mod_muc_room.erl:3302 -msgid "Allow visitors to send status text in presence updates" -msgstr "" -"Разрешить посетителям вставлять текcт статуса в сообщения о присутствии" - -#: mod_muc/mod_muc_room.erl:3305 -msgid "Allow visitors to change nickname" -msgstr "Разрешить посетителям изменять псевдоним" - -#: mod_muc/mod_muc_room.erl:3308 -msgid "Allow visitors to send voice requests" -msgstr "Разрешить посетителям запрашивать право голоса" - -#: mod_muc/mod_muc_room.erl:3311 -msgid "Minimum interval between voice requests (in seconds)" -msgstr "Минимальный интервал между запросами на право голоса" - -#: mod_muc/mod_muc_room.erl:3317 -msgid "Make room CAPTCHA protected" -msgstr "Сделать комнату защищённой капчей" - -#: mod_muc/mod_muc_room.erl:3322 -msgid "Exclude Jabber IDs from CAPTCHA challenge" -msgstr "Исключить показ капчи для списка Jabber ID" - -#: mod_muc/mod_muc_room.erl:3329 -msgid "Enable logging" -msgstr "Включить журналирование" - -#: mod_muc/mod_muc_room.erl:3337 -msgid "You need an x:data capable client to configure room" -msgstr "Чтобы сконфигурировать комнату, требуется x:data-совместимый клиент" - -#: mod_muc/mod_muc_room.erl:3694 -msgid "Number of occupants" -msgstr "Число присутствующих" - -#: mod_muc/mod_muc_room.erl:3750 -msgid "private, " -msgstr "приватная, " - -#: mod_muc/mod_muc_room.erl:3799 -msgid "Voice request" -msgstr "Запрос на право голоса" - -#: mod_muc/mod_muc_room.erl:3803 -msgid "Either approve or decline the voice request." -msgstr "Подтвердите или отклоните право голоса." - -#: mod_muc/mod_muc_room.erl:3809 -msgid "User JID" -msgstr "JID пользователя" - -#: mod_muc/mod_muc_room.erl:3811 -msgid "Grant voice to this person?" -msgstr "Предоставить голос?" - -#: mod_muc/mod_muc_room.erl:3960 -msgid "~s invites you to the room ~s" -msgstr "~s приглашает вас в комнату ~s" - -#: mod_muc/mod_muc_room.erl:3969 -msgid "the password is" -msgstr "пароль:" - -#: mod_offline.erl:510 mod_offline_odbc.erl:352 -msgid "" -"Your contact offline message queue is full. The message has been discarded." -msgstr "" -"Очередь недоставленных сообщений Вашего адресата переполнена. Сообщение не " -"было сохранено." - -#: mod_offline.erl:560 mod_offline_odbc.erl:408 -msgid "~s's Offline Messages Queue" -msgstr "Oчередь офлайновых сообщений ~s" - -#: mod_offline.erl:563 mod_offline_odbc.erl:411 mod_roster.erl:987 -#: mod_roster_odbc.erl:1104 mod_shared_roster.erl:901 -#: mod_shared_roster.erl:1010 mod_shared_roster_odbc.erl:930 -#: mod_shared_roster_odbc.erl:1039 web/ejabberd_web_admin.erl:833 -#: web/ejabberd_web_admin.erl:874 web/ejabberd_web_admin.erl:942 -#: web/ejabberd_web_admin.erl:978 web/ejabberd_web_admin.erl:1019 -#: web/ejabberd_web_admin.erl:1508 web/ejabberd_web_admin.erl:1760 -#: web/ejabberd_web_admin.erl:1931 web/ejabberd_web_admin.erl:1963 -#: web/ejabberd_web_admin.erl:2031 web/ejabberd_web_admin.erl:2135 -#: web/ejabberd_web_admin.erl:2160 web/ejabberd_web_admin.erl:2248 -msgid "Submitted" -msgstr "Отправлено" - -#: mod_offline.erl:571 -msgid "Time" -msgstr "Время" - -#: mod_offline.erl:572 -msgid "From" -msgstr "От кого" - -#: mod_offline.erl:573 -msgid "To" -msgstr "Кому" - -#: mod_offline.erl:574 mod_offline_odbc.erl:419 -msgid "Packet" -msgstr "Пакет" - -#: mod_offline.erl:587 mod_offline_odbc.erl:432 mod_shared_roster.erl:908 -#: mod_shared_roster_odbc.erl:937 web/ejabberd_web_admin.erl:882 -#: web/ejabberd_web_admin.erl:986 -msgid "Delete Selected" -msgstr "Удалить выделенные" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -msgid "Offline Messages:" -msgstr "Офлайновые сообщения:" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -msgid "Remove All Offline Messages" -msgstr "Удалить все офлайновые сообщения" - -#: mod_proxy65/mod_proxy65_service.erl:213 -msgid "ejabberd SOCKS5 Bytestreams module" -msgstr "ejabberd SOCKS5 Bytestreams модуль" - -#: mod_pubsub/mod_pubsub.erl:1120 mod_pubsub/mod_pubsub_odbc.erl:929 -msgid "Publish-Subscribe" -msgstr "Публикация-Подписка" - -#: mod_pubsub/mod_pubsub.erl:1214 mod_pubsub/mod_pubsub_odbc.erl:1025 -msgid "ejabberd Publish-Subscribe module" -msgstr "Модуль ejabberd Публикации-Подписки" - -#: mod_pubsub/mod_pubsub.erl:1497 mod_pubsub/mod_pubsub_odbc.erl:1312 -msgid "PubSub subscriber request" -msgstr "Запрос подписчика PubSub" - -#: mod_pubsub/mod_pubsub.erl:1499 mod_pubsub/mod_pubsub_odbc.erl:1314 -msgid "Choose whether to approve this entity's subscription." -msgstr "Решите: предоставить ли подписку этому объекту." - -#: mod_pubsub/mod_pubsub.erl:1505 mod_pubsub/mod_pubsub_odbc.erl:1320 -msgid "Node ID" -msgstr "ID узла" - -#: mod_pubsub/mod_pubsub.erl:1510 mod_pubsub/mod_pubsub_odbc.erl:1325 -msgid "Subscriber Address" -msgstr "Адрес подписчика" - -#: mod_pubsub/mod_pubsub.erl:1516 mod_pubsub/mod_pubsub_odbc.erl:1331 -msgid "Allow this Jabber ID to subscribe to this pubsub node?" -msgstr "Разрешить этому Jabber ID подписаться на данный узел?" - -#: mod_pubsub/mod_pubsub.erl:3474 mod_pubsub/mod_pubsub_odbc.erl:3287 -msgid "Deliver payloads with event notifications" -msgstr "Доставлять вместе с уведомлениями o публикациях сами публикации" - -#: mod_pubsub/mod_pubsub.erl:3475 mod_pubsub/mod_pubsub_odbc.erl:3288 -msgid "Deliver event notifications" -msgstr "Доставлять уведомления о событиях" - -#: mod_pubsub/mod_pubsub.erl:3476 mod_pubsub/mod_pubsub_odbc.erl:3289 -msgid "Notify subscribers when the node configuration changes" -msgstr "Уведомлять подписчиков об изменении конфигурации сборника" - -#: mod_pubsub/mod_pubsub.erl:3477 mod_pubsub/mod_pubsub_odbc.erl:3290 -msgid "Notify subscribers when the node is deleted" -msgstr "Уведомлять подписчиков об удалении сборника" - -#: mod_pubsub/mod_pubsub.erl:3478 mod_pubsub/mod_pubsub_odbc.erl:3291 -msgid "Notify subscribers when items are removed from the node" -msgstr "Уведомлять подписчиков об удалении публикаций из сборника" - -#: mod_pubsub/mod_pubsub.erl:3479 mod_pubsub/mod_pubsub_odbc.erl:3292 -msgid "Persist items to storage" -msgstr "Сохранять публикации в хранилище" - -#: mod_pubsub/mod_pubsub.erl:3480 mod_pubsub/mod_pubsub_odbc.erl:3293 -msgid "A friendly name for the node" -msgstr "Легко запоминаемое имя для узла" - -#: mod_pubsub/mod_pubsub.erl:3481 mod_pubsub/mod_pubsub_odbc.erl:3294 -msgid "Max # of items to persist" -msgstr "Максимальное число сохраняемых публикаций" - -#: mod_pubsub/mod_pubsub.erl:3482 mod_pubsub/mod_pubsub_odbc.erl:3295 -msgid "Whether to allow subscriptions" -msgstr "Разрешить подписку" - -#: mod_pubsub/mod_pubsub.erl:3483 mod_pubsub/mod_pubsub_odbc.erl:3296 -msgid "Specify the access model" -msgstr "Укажите механизм управления доступом" - -#: mod_pubsub/mod_pubsub.erl:3486 mod_pubsub/mod_pubsub_odbc.erl:3299 -msgid "Roster groups allowed to subscribe" -msgstr "Группы списка контактов, которым разрешена подписка" - -#: mod_pubsub/mod_pubsub.erl:3487 mod_pubsub/mod_pubsub_odbc.erl:3300 -msgid "Specify the publisher model" -msgstr "Условия публикации" - -#: mod_pubsub/mod_pubsub.erl:3489 mod_pubsub/mod_pubsub_odbc.erl:3302 -msgid "Purge all items when the relevant publisher goes offline" -msgstr "Очищать все записи автора публикации когда он отключается" - -#: mod_pubsub/mod_pubsub.erl:3490 mod_pubsub/mod_pubsub_odbc.erl:3303 -msgid "Specify the event message type" -msgstr "Укажите тип сообщения о событии" - -#: mod_pubsub/mod_pubsub.erl:3492 mod_pubsub/mod_pubsub_odbc.erl:3305 -msgid "Max payload size in bytes" -msgstr "Максимальный размер полезной нагрузки в байтах" - -#: mod_pubsub/mod_pubsub.erl:3493 mod_pubsub/mod_pubsub_odbc.erl:3306 -msgid "When to send the last published item" -msgstr "Когда посылать последний опубликованный элемент" - -#: mod_pubsub/mod_pubsub.erl:3495 mod_pubsub/mod_pubsub_odbc.erl:3308 -msgid "Only deliver notifications to available users" -msgstr "Доставлять уведомления только доступным пользователям" - -#: mod_pubsub/mod_pubsub.erl:3496 mod_pubsub/mod_pubsub_odbc.erl:3309 -msgid "The collections with which a node is affiliated" -msgstr "Имя коллекции, в которую входит узел" - -#: mod_register.erl:193 -msgid "The CAPTCHA verification has failed" -msgstr "Проверка капчи не пройдена" - -#: mod_register.erl:220 -msgid "You need a client that supports x:data and CAPTCHA to register" -msgstr "Чтобы зарегистрироваться, требуется x:data-совместимый клиент" - -#: mod_register.erl:226 mod_register.erl:265 -msgid "Choose a username and password to register with this server" -msgstr "Выберите имя пользователя и пароль для регистрации на этом сервере" - -#: mod_register.erl:230 mod_vcard.erl:364 mod_vcard_odbc.erl:342 -#: web/ejabberd_web_admin.erl:1515 web/ejabberd_web_admin.erl:1572 -msgid "User" -msgstr "Пользователь" - -#: mod_register.erl:316 mod_register.erl:361 -msgid "The password is too weak" -msgstr "Слишком слабый пароль" - -#: mod_register.erl:365 -msgid "Users are not allowed to register accounts so quickly" -msgstr "Пользователи не могут регистрировать учётные записи так быстро" - -#: mod_roster.erl:938 mod_roster_odbc.erl:1055 web/ejabberd_web_admin.erl:1701 -#: web/ejabberd_web_admin.erl:1886 web/ejabberd_web_admin.erl:1897 -#: web/ejabberd_web_admin.erl:2219 -msgid "None" -msgstr "Нет" - -#: mod_roster.erl:945 mod_roster_odbc.erl:1062 -msgid "Subscription" -msgstr "Подписка" - -#: mod_roster.erl:946 mod_roster_odbc.erl:1063 -msgid "Pending" -msgstr "Ожидание" - -#: mod_roster.erl:947 mod_roster_odbc.erl:1064 -msgid "Groups" -msgstr "Группы" - -#: mod_roster.erl:974 mod_roster_odbc.erl:1091 -msgid "Validate" -msgstr "Утвердить" - -#: mod_roster.erl:982 mod_roster_odbc.erl:1099 -msgid "Remove" -msgstr "Удалить" - -#: mod_roster.erl:985 mod_roster_odbc.erl:1102 -msgid "Roster of " -msgstr "Ростер пользователя " - -#: mod_roster.erl:988 mod_roster_odbc.erl:1105 mod_shared_roster.erl:902 -#: mod_shared_roster.erl:1011 mod_shared_roster_odbc.erl:931 -#: mod_shared_roster_odbc.erl:1040 web/ejabberd_web_admin.erl:834 -#: web/ejabberd_web_admin.erl:875 web/ejabberd_web_admin.erl:943 -#: web/ejabberd_web_admin.erl:979 web/ejabberd_web_admin.erl:1020 -#: web/ejabberd_web_admin.erl:1509 web/ejabberd_web_admin.erl:1761 -#: web/ejabberd_web_admin.erl:1932 web/ejabberd_web_admin.erl:2136 -#: web/ejabberd_web_admin.erl:2161 -msgid "Bad format" -msgstr "Неправильный формат" - -#: mod_roster.erl:995 mod_roster_odbc.erl:1112 -msgid "Add Jabber ID" -msgstr "Добавить Jabber ID" - -#: mod_roster.erl:1094 mod_roster_odbc.erl:1211 -msgid "Roster" -msgstr "Ростер" - -#: mod_shared_roster.erl:857 mod_shared_roster.erl:899 -#: mod_shared_roster.erl:1007 mod_shared_roster_odbc.erl:886 -#: mod_shared_roster_odbc.erl:928 mod_shared_roster_odbc.erl:1036 -msgid "Shared Roster Groups" -msgstr "Группы общих контактов" - -#: mod_shared_roster.erl:895 mod_shared_roster_odbc.erl:924 -#: web/ejabberd_web_admin.erl:1365 web/ejabberd_web_admin.erl:2461 -msgid "Add New" -msgstr "Добавить" - -#: mod_shared_roster.erl:978 mod_shared_roster_odbc.erl:1007 -msgid "Name:" -msgstr "Название:" - -#: mod_shared_roster.erl:983 mod_shared_roster_odbc.erl:1012 -msgid "Description:" -msgstr "Описание:" - -#: mod_shared_roster.erl:991 mod_shared_roster_odbc.erl:1020 -msgid "Members:" -msgstr "Члены:" - -#: mod_shared_roster.erl:999 mod_shared_roster_odbc.erl:1028 -msgid "Displayed Groups:" -msgstr "Видимые группы:" - -#: mod_shared_roster.erl:1008 mod_shared_roster_odbc.erl:1037 -msgid "Group " -msgstr "Группа " - -#: mod_shared_roster.erl:1017 mod_shared_roster_odbc.erl:1046 -#: web/ejabberd_web_admin.erl:840 web/ejabberd_web_admin.erl:884 -#: web/ejabberd_web_admin.erl:949 web/ejabberd_web_admin.erl:1026 -#: web/ejabberd_web_admin.erl:2017 -msgid "Submit" -msgstr "Отправить" - -#: mod_vcard.erl:165 mod_vcard_ldap.erl:238 mod_vcard_odbc.erl:129 -msgid "Erlang Jabber Server" -msgstr "Erlang Jabber Server" - -#: mod_vcard.erl:364 mod_vcard.erl:478 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:454 -msgid "Birthday" -msgstr "День рождения" - -#: mod_vcard.erl:364 mod_vcard.erl:480 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:456 -msgid "City" -msgstr "Город" - -#: mod_vcard.erl:364 mod_vcard.erl:479 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:455 -msgid "Country" -msgstr "Страна" - -#: mod_vcard.erl:364 mod_vcard.erl:481 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:457 -msgid "Email" -msgstr "Электронная почта" - -#: mod_vcard.erl:364 mod_vcard.erl:476 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:452 -msgid "Family Name" -msgstr "Фамилия" - -#: mod_vcard.erl:364 mod_vcard_odbc.erl:342 -msgid "" -"Fill in the form to search for any matching Jabber User (Add * to the end of " -"field to match substring)" -msgstr "" -"Заполните форму для поиска пользователя Jabber (Если добавить * в конец " -"поля, то происходит поиск подстроки)" - -#: mod_vcard.erl:364 mod_vcard.erl:473 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:449 -msgid "Full Name" -msgstr "Полное имя" - -#: mod_vcard.erl:364 mod_vcard.erl:475 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:451 -msgid "Middle Name" -msgstr "Отчество" - -#: mod_vcard.erl:364 mod_vcard.erl:474 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:450 web/ejabberd_web_admin.erl:2006 -msgid "Name" -msgstr "Название" - -#: mod_vcard.erl:364 mod_vcard.erl:482 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:458 -msgid "Organization Name" -msgstr "Название организации" - -#: mod_vcard.erl:364 mod_vcard.erl:483 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:459 -msgid "Organization Unit" -msgstr "Отдел организации" - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "Search users in " -msgstr "Поиск пользователей в " - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "You need an x:data capable client to search" -msgstr "Чтобы воспользоваться поиском, требуется x:data-совместимый клиент" - -#: mod_vcard.erl:389 mod_vcard_ldap.erl:487 mod_vcard_odbc.erl:367 -msgid "vCard User Search" -msgstr "Поиск пользователей по vCard" - -#: mod_vcard.erl:445 mod_vcard_ldap.erl:541 mod_vcard_odbc.erl:421 -msgid "ejabberd vCard module" -msgstr "ejabberd vCard модуль" - -#: mod_vcard.erl:469 mod_vcard_ldap.erl:551 mod_vcard_odbc.erl:445 -msgid "Search Results for " -msgstr "Результаты поиска в " - -#: mod_vcard_ldap.erl:462 -msgid "Fill in fields to search for any matching Jabber User" -msgstr "Заполните форму для поиска пользователя Jabber" - -#: web/ejabberd_web_admin.erl:193 web/ejabberd_web_admin.erl:203 -#: web/ejabberd_web_admin.erl:219 web/ejabberd_web_admin.erl:229 -msgid "Unauthorized" -msgstr "Не авторизован" - -#: web/ejabberd_web_admin.erl:286 web/ejabberd_web_admin.erl:303 -msgid "ejabberd Web Admin" -msgstr "Web-интерфейс администрирования ejabberd" - -#: web/ejabberd_web_admin.erl:769 web/ejabberd_web_admin.erl:780 -msgid "Administration" -msgstr "Администрирование" - -#: web/ejabberd_web_admin.erl:878 web/ejabberd_web_admin.erl:982 -msgid "Raw" -msgstr "Необработанный формат" - -#: web/ejabberd_web_admin.erl:1017 -msgid "~s access rule configuration" -msgstr "Конфигурация правила доступа ~s" - -#: web/ejabberd_web_admin.erl:1035 -msgid "Virtual Hosts" -msgstr "Виртуальные хосты" - -#: web/ejabberd_web_admin.erl:1043 web/ejabberd_web_admin.erl:1050 -msgid "Users" -msgstr "Пользователи" - -#: web/ejabberd_web_admin.erl:1078 -msgid "Users Last Activity" -msgstr "Статистика последнего подключения пользователей" - -#: web/ejabberd_web_admin.erl:1080 -msgid "Period: " -msgstr "Период" - -#: web/ejabberd_web_admin.erl:1090 -msgid "Last month" -msgstr "За последний месяц" - -#: web/ejabberd_web_admin.erl:1091 -msgid "Last year" -msgstr "За последний год" - -#: web/ejabberd_web_admin.erl:1092 -msgid "All activity" -msgstr "Вся статистика" - -#: web/ejabberd_web_admin.erl:1094 -msgid "Show Ordinary Table" -msgstr "Показать обычную таблицу" - -#: web/ejabberd_web_admin.erl:1096 -msgid "Show Integral Table" -msgstr "Показать интегральную таблицу" - -#: web/ejabberd_web_admin.erl:1105 web/ejabberd_web_admin.erl:1939 -msgid "Statistics" -msgstr "Статистика" - -#: web/ejabberd_web_admin.erl:1117 -msgid "Not Found" -msgstr "Не Найдено" - -#: web/ejabberd_web_admin.erl:1134 -msgid "Node not found" -msgstr "Узел не найден" - -#: web/ejabberd_web_admin.erl:1460 -msgid "Host" -msgstr "Хост" - -#: web/ejabberd_web_admin.erl:1461 -msgid "Registered Users" -msgstr "Зарегистрированные пользователи" - -#: web/ejabberd_web_admin.erl:1573 -msgid "Offline Messages" -msgstr "Офлайновые сообщения" - -#: web/ejabberd_web_admin.erl:1574 web/ejabberd_web_admin.erl:1767 -msgid "Last Activity" -msgstr "Последнее подключение" - -#: web/ejabberd_web_admin.erl:1659 web/ejabberd_web_admin.erl:1675 -msgid "Registered Users:" -msgstr "Зарегистрированные пользователи:" - -#: web/ejabberd_web_admin.erl:1661 web/ejabberd_web_admin.erl:1677 -#: web/ejabberd_web_admin.erl:2192 -msgid "Online Users:" -msgstr "Подключённые пользователи:" - -#: web/ejabberd_web_admin.erl:1663 -msgid "Outgoing s2s Connections:" -msgstr "Исходящие s2s-серверы:" - -#: web/ejabberd_web_admin.erl:1665 -msgid "Outgoing s2s Servers:" -msgstr "Исходящие s2s-серверы:" - -#: web/ejabberd_web_admin.erl:1734 web/mod_register_web.erl:175 -#: web/mod_register_web.erl:372 web/mod_register_web.erl:381 -#: web/mod_register_web.erl:411 -msgid "Change Password" -msgstr "Сменить пароль" - -#: web/ejabberd_web_admin.erl:1758 -msgid "User " -msgstr "Пользователь " - -#: web/ejabberd_web_admin.erl:1765 -msgid "Connected Resources:" -msgstr "Подключённые ресурсы:" - -#: web/ejabberd_web_admin.erl:1766 web/mod_register_web.erl:227 -#: web/mod_register_web.erl:519 -msgid "Password:" -msgstr "Пароль:" - -#: web/ejabberd_web_admin.erl:1828 -msgid "No Data" -msgstr "Нет данных" - -#: web/ejabberd_web_admin.erl:1906 -msgid "Nodes" -msgstr "Узлы" - -#: web/ejabberd_web_admin.erl:1929 web/ejabberd_web_admin.erl:1951 -msgid "Node " -msgstr "Узел " - -#: web/ejabberd_web_admin.erl:1938 -msgid "Listened Ports" -msgstr "Прослушиваемые порты" - -#: web/ejabberd_web_admin.erl:1940 web/ejabberd_web_admin.erl:2260 -#: web/ejabberd_web_admin.erl:2448 -msgid "Update" -msgstr "Обновить" - -#: web/ejabberd_web_admin.erl:1943 web/ejabberd_web_admin.erl:2569 -msgid "Restart" -msgstr "Перезапустить" - -#: web/ejabberd_web_admin.erl:1945 web/ejabberd_web_admin.erl:2571 -msgid "Stop" -msgstr "Остановить" - -#: web/ejabberd_web_admin.erl:1959 -msgid "RPC Call Error" -msgstr "Ошибка вызова RPC" - -#: web/ejabberd_web_admin.erl:2000 -msgid "Database Tables at " -msgstr "Таблицы базы данных на " - -#: web/ejabberd_web_admin.erl:2007 -msgid "Storage Type" -msgstr "Тип таблицы" - -#: web/ejabberd_web_admin.erl:2008 -msgid "Elements" -msgstr "Элементы" - -#: web/ejabberd_web_admin.erl:2009 -msgid "Memory" -msgstr "Память" - -#: web/ejabberd_web_admin.erl:2032 web/ejabberd_web_admin.erl:2137 -msgid "Error" -msgstr "Ошибка" - -#: web/ejabberd_web_admin.erl:2034 -msgid "Backup of " -msgstr "Резервное копирование " - -#: web/ejabberd_web_admin.erl:2036 -msgid "" -"Please note that these options will only backup the builtin Mnesia database. " -"If you are using the ODBC module, you also need to backup your SQL database " -"separately." -msgstr "" -"Заметьте, что здесь производится резервное копирование только встроенной " -"базы данных Mnesia. Если Вы также используете другое хранилище данных " -"(например с помощью модуля ODBC), то его резервное копирование следует " -"осуществлять отдельно." - -#: web/ejabberd_web_admin.erl:2041 -msgid "Store binary backup:" -msgstr "Сохранить бинарную резервную копию:" - -#: web/ejabberd_web_admin.erl:2045 web/ejabberd_web_admin.erl:2052 -#: web/ejabberd_web_admin.erl:2060 web/ejabberd_web_admin.erl:2067 -#: web/ejabberd_web_admin.erl:2074 web/ejabberd_web_admin.erl:2081 -#: web/ejabberd_web_admin.erl:2088 web/ejabberd_web_admin.erl:2096 -#: web/ejabberd_web_admin.erl:2103 web/ejabberd_web_admin.erl:2110 -msgid "OK" -msgstr "Продолжить" - -#: web/ejabberd_web_admin.erl:2048 -msgid "Restore binary backup immediately:" -msgstr "Восстановить из бинарной резервной копии немедленно:" - -#: web/ejabberd_web_admin.erl:2056 -msgid "" -"Restore binary backup after next ejabberd restart (requires less memory):" -msgstr "" -"Восстановить из бинарной резервной копии при следующем запуске (требует " -"меньше памяти):" - -#: web/ejabberd_web_admin.erl:2063 -msgid "Store plain text backup:" -msgstr "Сохранить текстовую резервную копию:" - -#: web/ejabberd_web_admin.erl:2070 -msgid "Restore plain text backup immediately:" -msgstr "Восстановить из текстовой резервной копии немедленно:" - -#: web/ejabberd_web_admin.erl:2077 -msgid "Import users data from a PIEFXIS file (XEP-0227):" -msgstr "Импорт пользовательских данных из файла формата PIEFXIS (XEP-0227):" - -#: web/ejabberd_web_admin.erl:2084 -msgid "Export data of all users in the server to PIEFXIS files (XEP-0227):" -msgstr "" -"Экспорт данных всех пользователей сервера в файлы формата PIEFXIS (XEP-0227):" - -#: web/ejabberd_web_admin.erl:2091 -msgid "Export data of users in a host to PIEFXIS files (XEP-0227):" -msgstr "" -"Экспорт пользовательских данных домена в файлы формата PIEFXIS (XEP-0227):" - -#: web/ejabberd_web_admin.erl:2099 -msgid "Import user data from jabberd14 spool file:" -msgstr "Импорт пользовательских данных из буферного файла jabberd14:" - -#: web/ejabberd_web_admin.erl:2106 -msgid "Import users data from jabberd14 spool directory:" -msgstr "Импорт пользовательских данных из буферной директории jabberd14:" - -#: web/ejabberd_web_admin.erl:2132 -msgid "Listened Ports at " -msgstr "Прослушиваемые порты на " - -#: web/ejabberd_web_admin.erl:2157 -msgid "Modules at " -msgstr "Модули на " - -#: web/ejabberd_web_admin.erl:2183 -msgid "Statistics of ~p" -msgstr "статистика узла ~p" - -#: web/ejabberd_web_admin.erl:2186 -msgid "Uptime:" -msgstr "Время работы:" - -#: web/ejabberd_web_admin.erl:2189 -msgid "CPU Time:" -msgstr "Процессорное время:" - -#: web/ejabberd_web_admin.erl:2195 -msgid "Transactions Committed:" -msgstr "Транзакции завершенные:" - -#: web/ejabberd_web_admin.erl:2198 -msgid "Transactions Aborted:" -msgstr "Транзакции отмененные:" - -#: web/ejabberd_web_admin.erl:2201 -msgid "Transactions Restarted:" -msgstr "Транзакции перезапущенные:" - -#: web/ejabberd_web_admin.erl:2204 -msgid "Transactions Logged:" -msgstr "Транзакции запротоколированные:" - -#: web/ejabberd_web_admin.erl:2246 -msgid "Update " -msgstr "Обновление " - -#: web/ejabberd_web_admin.erl:2254 -msgid "Update plan" -msgstr "План обновления" - -#: web/ejabberd_web_admin.erl:2255 -msgid "Modified modules" -msgstr "Изменённые модули" - -#: web/ejabberd_web_admin.erl:2256 -msgid "Update script" -msgstr "Сценарий обновления" - -#: web/ejabberd_web_admin.erl:2257 -msgid "Low level update script" -msgstr "Низкоуровневый сценарий обновления" - -#: web/ejabberd_web_admin.erl:2258 -msgid "Script check" -msgstr "Проверка сценария" - -#: web/ejabberd_web_admin.erl:2426 -msgid "Port" -msgstr "Порт" - -#: web/ejabberd_web_admin.erl:2427 -msgid "IP" -msgstr "IP" - -#: web/ejabberd_web_admin.erl:2428 -msgid "Protocol" -msgstr "Протокол" - -#: web/ejabberd_web_admin.erl:2429 web/ejabberd_web_admin.erl:2556 -msgid "Module" -msgstr "Модуль" - -#: web/ejabberd_web_admin.erl:2430 web/ejabberd_web_admin.erl:2557 -msgid "Options" -msgstr "Параметры" - -#: web/ejabberd_web_admin.erl:2450 -msgid "Delete" -msgstr "Удалить" - -#: web/ejabberd_web_admin.erl:2579 -msgid "Start" -msgstr "Запустить" - -#: web/mod_register_web.erl:104 -msgid "Your Jabber account was successfully created." -msgstr "Ваш Jabber-аккаунт был успешно создан." - -#: web/mod_register_web.erl:107 -msgid "There was an error creating the account: " -msgstr "Ошибка при создании аккаунта:" - -#: web/mod_register_web.erl:115 -msgid "Your Jabber account was successfully deleted." -msgstr "Ваш Jabber-аккаунт был успешно удален." - -#: web/mod_register_web.erl:118 -msgid "There was an error deleting the account: " -msgstr "Ошибка при удалении аккаунта:" - -#: web/mod_register_web.erl:128 -msgid "The password of your Jabber account was successfully changed." -msgstr "Пароль Вашего Jabber-аккаунта был успешно изменен." - -#: web/mod_register_web.erl:131 -msgid "There was an error changing the password: " -msgstr "Ошибка при смене пароля:" - -#: web/mod_register_web.erl:163 web/mod_register_web.erl:172 -msgid "Jabber Account Registration" -msgstr "Регистрация Jabber-аккаунта" - -#: web/mod_register_web.erl:174 web/mod_register_web.erl:192 -#: web/mod_register_web.erl:201 -msgid "Register a Jabber account" -msgstr "Зарегистрировать Jabber-аккаунт" - -#: web/mod_register_web.erl:176 web/mod_register_web.erl:493 -#: web/mod_register_web.erl:502 -msgid "Unregister a Jabber account" -msgstr "Удалить Jabber-аккаунт" - -#: web/mod_register_web.erl:203 -msgid "" -"This page allows to create a Jabber account in this Jabber server. Your JID " -"(Jabber IDentifier) will be of the form: username@server. Please read " -"carefully the instructions to fill correctly the fields." -msgstr "" -"Здесь Вы можете создать Jabber-аккаунт на этом Jabber-сервере. Ваш JID " -"(Jabber-идентификатор) будет в виде: \"пользователь@сервер\". Пожалуйста, " -"внимательно читайте инструкции для правильного заполнения полей." - -#: web/mod_register_web.erl:212 web/mod_register_web.erl:386 -#: web/mod_register_web.erl:509 -msgid "Username:" -msgstr "Имя пользователя:" - -#: web/mod_register_web.erl:217 -msgid "This is case insensitive: macbeth is the same that MacBeth and Macbeth." -msgstr "" -"Регистр не имеет значения: \"маша\" и \"МАША\" будет считаться одним и тем " -"же именем." - -#: web/mod_register_web.erl:218 -msgid "Characters not allowed:" -msgstr "Недопустимые символы:" - -#: web/mod_register_web.erl:222 web/mod_register_web.erl:391 -#: web/mod_register_web.erl:514 -msgid "Server:" -msgstr "Сервер:" - -#: web/mod_register_web.erl:232 -msgid "" -"Don't tell your password to anybody, not even the administrators of the " -"Jabber server." -msgstr "Не говорите никому свой пароль, даже администраторам сервера." - -#: web/mod_register_web.erl:234 -msgid "You can later change your password using a Jabber client." -msgstr "Позже Вы можете изменить пароль через Jabber-клиент." - -#: web/mod_register_web.erl:235 -msgid "" -"Some Jabber clients can store your password in your computer. Use that " -"feature only if you trust your computer is safe." -msgstr "" -"Некоторые Jabber-клиенты могут сохранять пароль на Вашем компьютере. " -"Используйте эту функцию только в том случае, если считаете это безопасным." - -#: web/mod_register_web.erl:237 -msgid "" -"Memorize your password, or write it in a paper placed in a safe place. In " -"Jabber there isn't an automated way to recover your password if you forget " -"it." -msgstr "" -"Запомните пароль или запишите его на бумаге, которую сохраните в безопасном " -"месте. В Jabber'е нет автоматизированного средства восстановления пароля в " -"том случае, если Вы его забудете." - -#: web/mod_register_web.erl:242 web/mod_register_web.erl:406 -msgid "Password Verification:" -msgstr "Проверка пароля:" - -#: web/mod_register_web.erl:250 -msgid "Register" -msgstr "Зарегистрировать" - -#: web/mod_register_web.erl:396 -msgid "Old Password:" -msgstr "Старый пароль:" - -#: web/mod_register_web.erl:401 -msgid "New Password:" -msgstr "Новый пароль:" - -#: web/mod_register_web.erl:504 -msgid "This page allows to unregister a Jabber account in this Jabber server." -msgstr "Здесь Вы можете удалить Jabber-аккаунт с этого сервера." - -#: web/mod_register_web.erl:524 -msgid "Unregister" -msgstr "Удалить" - -#, fuzzy -#~ msgid "Captcha test failed" -#~ msgstr "Проверка капчи прошла успешно." diff --git a/priv/msgs/sk.msg b/priv/msgs/sk.msg index d707000d7..54f711610 100644 --- a/priv/msgs/sk.msg +++ b/priv/msgs/sk.msg @@ -1,40 +1,40 @@ -{"Access Configuration","Konfigurácia prístupu"}. -{"Access Control List Configuration","Konfigurácia zoznamu prístupových oprávnení (ACL)"}. -{"Access control lists","Zoznamy prístupových oprávnení (ACL)"}. -{"Access Control Lists","Zoznamy prístupových oprávnení (ACL)"}. -{"Access denied by service policy","Prístup bol zamietnutý nastavením služby"}. -{"Access rules","Prístupové pravidlá"}. -{"Access Rules","Prístupové pravidlá"}. -{"Action on user","Operácia aplikovaná na užívateľa"}. -{"Add Jabber ID","Pridať Jabber ID"}. -{"Add New","Pridať nový"}. -{"Add User","Pridať používateľa"}. -{"Administration","Administrácia"}. -{"Administration of ","Administrácia "}. -{"Administrator privileges required","Sú potrebné práva administrátora"}. +%% Generated automatically +%% DO NOT EDIT: run `make translations` instead +%% To improve translations please read: +%% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ + +{" has set the subject to: ","zmenil(a) tému na: "}. {"A friendly name for the node","Prístupný názov pre uzol"}. +{"A password is required to enter this room","Pre vstup do miestnosti je potrebné heslo"}. +{"Access denied by service policy","Prístup bol zamietnutý nastavením služby"}. +{"Action on user","Operácia aplikovaná na užívateľa"}. +{"Add User","Pridať používateľa"}. +{"Administration of ","Administrácia "}. +{"Administration","Administrácia"}. +{"Administrator privileges required","Sú potrebné práva administrátora"}. {"All activity","Všetky aktivity"}. +{"All Users","Všetci užívatelia"}. {"Allow this Jabber ID to subscribe to this pubsub node?","Dovoliť tomuto Jabber ID odoberať PubSub uzol?"}. {"Allow users to change the subject","Povoliť užívateľom meniť tému"}. {"Allow users to query other users","Povoliť užívateľom dotazovať sa informácie o iných užívateľoch"}. {"Allow users to send invites","Povoliť používateľom posielanie pozvánok"}. {"Allow users to send private messages","Povoliť užívateľom odosielať súkromné správy"}. {"Allow visitors to change nickname","Návštevníci môžu meniť prezývky"}. +{"Allow visitors to send private messages to","Povoliť užívateľom odosielať súkromné správy"}. {"Allow visitors to send status text in presence updates","Návštevníci môžu posielať textové informácie v stavových správach"}. -{"All Users","Všetci užívatelia"}. +{"Allow visitors to send voice requests","Povoliť používateľom posielanie pozvánok"}. {"Announcements","Oznámenia"}. -{"anyone","všetkým"}. -{"A password is required to enter this room","Pre vstup do miestnosti je potrebné heslo"}. {"April","Apríl"}. {"August","August"}. {"Backup Management","Správa zálohovania"}. -{"Backup of ","Záloha "}. {"Backup to File at ","Záloha do súboru na "}. {"Backup","Zálohovať"}. {"Bad format","Zlý formát"}. {"Birthday","Dátum narodenia"}. +{"CAPTCHA web page","Webová stránka CAPTCHA"}. {"Change Password","Zmeniť heslo"}. {"Change User Password","Zmeniť heslo užívateľa"}. +{"Characters not allowed:","Nepovolené znaky:"}. {"Chatroom configuration modified","Nastavenie diskusnej miestnosti bolo zmenené"}. {"Chatroom is created","Diskusná miestnosť je vytvorená"}. {"Chatroom is destroyed","Diskusná miestnosť je zrušená"}. @@ -42,84 +42,61 @@ {"Chatroom is stopped","Diskusná miestnosť je pozastavená"}. {"Chatrooms","Diskusné miestnosti"}. {"Choose a username and password to register with this server","Zvolte meno užívateľa a heslo pre registráciu na tomto servere"}. -{"Choose modules to stop","Vyberte moduly, ktoré majú byť zastavené"}. {"Choose storage type of tables","Vyberte typ úložiska pre tabuľky"}. {"Choose whether to approve this entity's subscription.","Zvolte, či chcete povoliť toto odoberanie"}. {"City","Mesto"}. {"Commands","Príkazy"}. {"Conference room does not exist","Diskusná miestnosť neexistuje"}. -{"Configuration","Konfigurácia"}. {"Configuration of room ~s","Konfigurácia miestnosti ~s"}. -{"Connected Resources:","Pripojené zdroje:"}. -{"Connections parameters","Parametre spojenia"}. +{"Configuration","Konfigurácia"}. {"Country","Krajina"}. -{"CPU Time:","Čas procesoru"}. -{"Database","Databáza"}. -{"Database Tables at ","Databázové tabuľky na "}. {"Database Tables Configuration at ","Konfigurácia databázových tabuliek "}. +{"Database","Databáza"}. {"December","December"}. {"Default users as participants","Užívatelia sú implicitne členmi"}. {"Delete message of the day on all hosts","Zmazať správu dňa na všetkých serveroch"}. {"Delete message of the day","Zmazať správu dňa"}. -{"Delete Selected","Zmazať vybrané"}. {"Delete User","Vymazať užívateľa"}. -{"Delete","Zmazať"}. {"Deliver event notifications","Doručiť oznamy o udalosti"}. {"Deliver payloads with event notifications","Doručiť náklad s upozornením na udalosť"}. -{"Description:","Popis:"}. {"Disc only copy","Len kópia disku"}. -{"Displayed Groups:","Zobrazené skupiny:"}. {"Dump Backup to Text File at ","Uložiť zálohu do textového súboru na "}. {"Dump to Text File","Uložiť do textového súboru"}. {"Edit Properties","Editovať vlastnosti"}. -{"ejabberd IRC module","ejabberd IRC modul"}. +{"Either approve or decline the voice request.","Povolte alebo zamietnite žiadosť o Voice."}. {"ejabberd MUC module","ejabberd MUC modul"}. {"ejabberd Publish-Subscribe module","ejabberd Publish-Subscribe modul"}. {"ejabberd SOCKS5 Bytestreams module","ejabberd SOCKS5 Bytestreams modul"}. {"ejabberd vCard module","ejabberd vCard modul"}. {"ejabberd Web Admin","ejabberd Web Admin"}. -{"Elements","Prvky"}. {"Email","E-mail"}. {"Enable logging","Zapnúť zaznamenávanie histórie"}. -{"Encoding for server ~b","Kódovanie pre server ~b"}. {"End User Session","Ukončiť reláciu užívateľa"}. -{"Enter list of {Module, [Options]}","Vložte zoznam modulov {Modul, [Parametre]}"}. {"Enter nickname you want to register","Zadajte prezývku, ktorú chcete registrovať"}. {"Enter path to backup file","Zadajte cestu k súboru so zálohou"}. {"Enter path to jabberd14 spool dir","Zadajte cestu k jabberd14 spool adresáru"}. {"Enter path to jabberd14 spool file","Zadajte cestu k spool súboru jabberd14"}. {"Enter path to text file","Zadajte cestu k textovému súboru"}. {"Enter the text you see","Zadajte zobrazený text"}. -{"Enter username and encodings you wish to use for connecting to IRC servers. Press 'Next' to get more fields to fill in. Press 'Complete' to save settings.","Vložte meno používateľa a kódovanie, ktoré chcete používať pri pripojení na IRC servery. Kliknutím na tlačítko 'Ďalej' môžete zadať niektoré ďalšie hodnoty. Pomocou 'Ukončiť ' uložíte nastavenia."}. -{"Enter username, encodings, ports and passwords you wish to use for connecting to IRC servers","Vložte meno používateľa, kódovanie, porty a heslo ktoré chcete používať pri pripojení na IRC server"}. -{"Erlang Jabber Server","Erlang Jabber Server"}. -{"Error","Chyba"}. -{"Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef.net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}].","Príklad: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef.net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]."}. +{"Exclude Jabber IDs from CAPTCHA challenge","Nepoužívať CAPTCHA pre nasledujúce Jabber ID"}. {"Export data of all users in the server to PIEFXIS files (XEP-0227):","Exportovať dáta všetkých uživateľov na serveri do súborov PIEFXIS (XEP-0227):"}. {"Export data of users in a host to PIEFXIS files (XEP-0227):","Exportovať dáta uživateľov na hostitelovi do súborov PIEFXIS (XEP-0227):"}. +{"Failed to extract JID from your voice request approval","Nepodarilo sa nájsť JID v súhlase o Voice."}. {"Family Name","Priezvisko"}. {"February","Február"}. -{"Fill in fields to search for any matching Jabber User","Vyplnte políčka pre vyhľadávanie Jabber užívateľa"}. -{"Fill in the form to search for any matching Jabber User (Add * to the end of field to match substring)","Pre vyhľadanie Jabber používateľa vyplňte formulár (pridajte znak * na koniec, pre vyhľadanie podreťazca)"}. {"Friday","Piatok"}. -{"From","Od"}. -{"From ~s","Od ~s"}. {"Full Name","Celé meno: "}. {"Get Number of Online Users","Zobraziť počet pripojených užívateľov"}. {"Get Number of Registered Users","Zobraziť počet registrovaných užívateľov"}. {"Get User Last Login Time","Zobraziť čas posledného prihlásenia"}. -{"Get User Password","Zobraziť heslo užívateľa"}. {"Get User Statistics","Zobraziť štatistiku užívateľa"}. -{"Group ","Skupina "}. -{"Groups","Skupiny"}. +{"Grant voice to this person?","Prideltiť Voice tejto osobe?"}. {"has been banned","bol(a) zablokovaný(á)"}. -{"has been kicked because of an affiliation change","bol vyhodený(á) kvôli zmene priradenia"}. {"has been kicked because of a system shutdown","bol vyhodený(á) kvôli reštartu systému"}. +{"has been kicked because of an affiliation change","bol vyhodený(á) kvôli zmene priradenia"}. {"has been kicked because the room has been changed to members-only","bol vyhodený(á), pretože miestnosť bola vyhradená len pre členov"}. {"has been kicked","bol(a) vyhodený(á) z miestnosti"}. -{" has set the subject to: ","zmenil(a) tému na: "}. -{"Host","Server"}. -{"If you want to specify different ports, passwords, encodings for IRC servers, fill this list with values in format '{\"irc server\", \"encoding\", port, \"password\"}'. By default this service use \"~s\" encoding, port ~p, empty password.","Ak chcete zadať iné porty, heslá a kódovania pre IRC servery, vyplnte zoznam s hodnotami vo formáte '{\"irc server\",\"kódovanie\", \"port\", \"heslo\"}'. Predvolenéi hodnoty pre túto službu sú: kódovanie \"~s\", port ~p a žiadne heslo."}. +{"If you don't see the CAPTCHA image here, visit the web page.","Pokiaľ nevidíte obrázok CAPTCHA, navštívte webovú stránku."}. {"Import Directory","Import adresára"}. {"Import File","Import súboru"}. {"Import user data from jabberd14 spool file:","Importovať dáta užívateľov z jabberd14 spool súboru:"}. @@ -130,27 +107,13 @@ {"Import Users From jabberd14 Spool Files","Importovať užívateľov z jabberd14 spool súborov"}. {"Improper message type","Nesprávny typ správy"}. {"Incorrect password","Nesprávne heslo"}. -{"Invalid affiliation: ~s","Neplatné priradenie: ~s"}. -{"Invalid role: ~s","Neplatná rola: ~s"}. {"IP addresses","IP adresa"}. -{"IP","IP"}. -{"IRC channel (don't put the first #)","IRC kanál (bez počiatočnej #)"}. -{"IRC server","IRC server"}. -{"IRC settings","Nastavania IRC"}. -{"IRC Transport","IRC Transport"}. -{"IRC username","IRC prezývka"}. -{"IRC Username","IRC prezývka"}. {"is now known as","sa premenoval(a) na"}. -{"It is not allowed to send private messages","Nieje povolené posielať súkromné správy"}. {"It is not allowed to send private messages of type \"groupchat\"","Nie je dovolené odoslanie súkromnej správy typu \"Skupinová správa\" "}. {"It is not allowed to send private messages to the conference","Nie je povolené odosielať súkromné správy do konferencie"}. {"Jabber ID","Jabber ID"}. -{"Jabber ID ~s is invalid","Jabber ID ~s je neplatné"}. {"January","Január"}. -{"Join IRC channel","Pripojit IRC kanál"}. {"joins the room","vstúpil(a) do miestnosti"}. -{"Join the IRC channel here.","Propojiť IRC kanál sem."}. -{"Join the IRC channel in this Jabber ID: ~s","Pripojit IRC kanál k tomuto Jabber ID: ~s"}. {"July","Júl"}. {"June","Jún"}. {"Last Activity","Posledná aktivita"}. @@ -158,10 +121,6 @@ {"Last month","Posledný mesiac"}. {"Last year","Posledný rok"}. {"leaves the room","odišiel(a) z miestnosti"}. -{"Listened Ports at ","Otvorené porty na "}. -{"Listened Ports","Otvorené portov"}. -{"List of modules to start","Zoznam modulov, ktoré majú byť spustené"}. -{"Low level update script","Nízkoúrovňový aktualizačný skript"}. {"Make participants list public","Nastaviť zoznam zúčastnených ako verejný"}. {"Make room CAPTCHA protected","Chrániť miestnosť systémom CAPTCHA"}. {"Make room members-only","Nastaviť miestnosť len pre členov"}. @@ -170,37 +129,28 @@ {"Make room persistent","Nastaviť miestnosť ako trvalú"}. {"Make room public searchable","Nastaviť miestnosť ako verejne prehľadávateľnú"}. {"March","Marec"}. -{"Maximum Number of Occupants","Počet účastníkov"}. -{"Max # of items to persist","Maximálny počet položiek, ktoré je možné natrvalo uložiť"}. {"Max payload size in bytes","Maximálny náklad v bajtoch"}. +{"Maximum Number of Occupants","Počet účastníkov"}. {"May","Máj"}. -{"Members:","Členovia:"}. {"Membership is required to enter this room","Pre vstup do miestnosti je potrebné byť členom"}. -{"Memory","Pamäť"}. {"Message body","Telo správy"}. {"Middle Name","Prostredné meno: "}. +{"Minimum interval between voice requests (in seconds)","Minimum interval between voice requests (in seconds)"}. {"Moderator privileges required","Sú potrebné práva moderátora"}. -{"moderators only","moderátorom"}. -{"Modified modules","Modifikované moduly"}. -{"Module","Modul"}. -{"Modules at ","Moduly na "}. -{"Modules","Moduly"}. {"Monday","Pondelok"}. -{"Name:","Meno:"}. {"Name","Meno"}. {"Never","Nikdy"}. -{"Nickname","Prezývka"}. +{"New Password:","Nové heslo:"}. {"Nickname Registration at ","Registrácia prezývky na "}. {"Nickname ~s does not exist in the room","Prezývka ~s v miestnosti neexistuje"}. +{"Nickname","Prezývka"}. {"No body provided for announce message","Správa neobsahuje text"}. {"No Data","Žiadne dáta"}. +{"No limit","Bez limitu"}. {"Node ID","ID uzlu"}. {"Node not found","Uzol nenájdený"}. {"Nodes","Uzly"}. -{"Node ","Uzol "}. -{"No limit","Bez limitu"}. {"None","Nič"}. -{"No resource provided","Nebol poskytnutý žiadny zdroj"}. {"Not Found","Nebol nájdený"}. {"Notify subscribers when items are removed from the node","Upozorniť prihlásených používateľov na odstránenie položiek z uzlu"}. {"Notify subscribers when the node configuration changes","Upozorniť prihlásených používateľov na zmenu nastavenia uzlu"}. @@ -210,166 +160,127 @@ {"Number of online users","Počet online užívateľov"}. {"Number of registered users","Počet registrovaných užívateľov"}. {"October","Október"}. -{"Offline Messages:","Offline správy"}. -{"Offline Messages","Offline správy"}. {"OK","OK"}. -{"Online","Online"}. -{"Online Users:","Online používatelia:"}. +{"Old Password:","Staré heslo:"}. {"Online Users","Online užívatelia"}. +{"Online","Online"}. {"Only deliver notifications to available users","Doručovať upozornenia len aktuálne prihláseným používateľom"}. {"Only moderators and participants are allowed to change the subject in this room","Len moderátori a zúčastnený majú povolené meniť tému tejto miestnosti"}. {"Only moderators are allowed to change the subject in this room","Len moderátori majú povolené meniť tému miestnosti"}. +{"Only moderators can approve voice requests","Len moderátori môžu schváliť žiadosť o Voice"}. {"Only occupants are allowed to send messages to the conference","Len členovia majú povolené zasielať správy do konferencie"}. {"Only occupants are allowed to send queries to the conference","Len členovia majú povolené dotazovať sa o konferencii"}. {"Only service administrators are allowed to send service messages","Iba správcovia služby majú povolené odosielanie servisných správ"}. -{"Options","Nastavenia"}. {"Organization Name","Meno organizácie: "}. {"Organization Unit","Organizačná jednotka: "}. -{"Outgoing s2s Connections:","Odchádzajúce s2s spojenia:"}. {"Outgoing s2s Connections","Odchádzajúce s2s spojenia"}. -{"Outgoing s2s Servers:","Odchádzajúce s2s servery:"}. {"Owner privileges required","Sú vyžadované práva vlastníka"}. -{"Packet","Paket"}. -{"Password ~b","Heslo ~b"}. -{"Password:","Heslo:"}. -{"Password","Heslo"}. {"Password Verification","Overenie hesla"}. +{"Password Verification:","Overenie hesla"}. +{"Password","Heslo"}. +{"Password:","Heslo:"}. {"Path to Dir","Cesta k adresáru"}. {"Path to File","Cesta k súboru"}. -{"Pending","Čakajúce"}. {"Period: ","Čas:"}. {"Persist items to storage","Uložiť položky natrvalo do úložiska"}. {"Ping","Ping"}. {"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Prosím, berte na vedomie, že tieto nastavenia zázálohujú iba zabudovnú Mnesia databázu. Ak používate ODBC modul, musíte zálohovať vašu SQL databázu separátne."}. +{"Please, wait for a while before sending new voice request","Prosím počkate, predtým než pošlete novú žiadosť o Voice"}. {"Pong","Pong"}. -{"Port ~b","Port ~b"}. -{"Port","Port"}. {"Present real Jabber IDs to","Zobrazovať skutočné Jabber ID"}. {"private, ","súkromná, "}. -{"Protocol","Protokol"}. {"Publish-Subscribe","Publish-Subscribe"}. {"PubSub subscriber request","Žiadosť odberateľa PubSub"}. {"Purge all items when the relevant publisher goes offline","Odstrániť všetky relevantné položky, keď užívateľ prejde do módu offline"}. {"Queries to the conference members are not allowed in this room","Dotazovať sa o členoch nie je v tejto miestnosti povolené"}. {"RAM and disc copy","Kópia RAM a disku"}. {"RAM copy","Kópia RAM"}. -{"Raw","Raw"}. {"Really delete message of the day?","Skutočne zmazať správu dňa?"}. {"Recipient is not in the conference room","Príjemca sa nenachádza v konferenčnej miestnosti"}. -{"Registered Users:","Registrovaní používatelia:"}. -{"Registered Users","Registrovaní používatelia"}. -{"Registration in mod_irc for ","Registrácia do mod_irc na "}. +{"Register","Zoznam kontaktov"}. {"Remote copy","Vzdialená kópia"}. -{"Remove All Offline Messages","Odstrániť všetky offline správy"}. -{"Remove","Odstrániť"}. {"Remove User","Odstrániť užívateľa"}. {"Replaced by new connection","Nahradené novým spojením"}. {"Resources","Zdroje"}. -{"Restart","Reštart"}. {"Restart Service","Reštartovať službu"}. {"Restore Backup from File at ","Obnoviť zálohu zo súboru na "}. {"Restore binary backup after next ejabberd restart (requires less memory):","Obnoviť binárnu zálohu pri nasledujúcom reštarte ejabberd (vyžaduje menej pamäte)"}. {"Restore binary backup immediately:","Okamžite obnoviť binárnu zálohu:"}. -{"Restore","Obnoviť"}. {"Restore plain text backup immediately:","Okamžite obnoviť zálohu z textového súboru:"}. +{"Restore","Obnoviť"}. {"Room Configuration","Nastavenia miestnosti"}. {"Room creation is denied by service policy","Vytváranie miestnosti nie je povolené"}. {"Room description","Popis miestnosti"}. {"Room Occupants","Ľudí v miestnosti"}. {"Room title","Názov miestnosti"}. {"Roster groups allowed to subscribe","Skupiny kontaktov, ktoré môžu odoberať"}. -{"Roster of ","Zoznam kontaktov "}. {"Roster size","Počet kontaktov v zozname"}. -{"Roster","Zoznam kontaktov"}. -{"RPC Call Error","Chyba RPC volania"}. {"Running Nodes","Bežiace uzly"}. -{"~s access rule configuration","~s konfigurácia prístupového pravidla"}. {"Saturday","Sobota"}. -{"Script check","Kontrola skriptu"}. {"Search Results for ","Hľadať výsledky pre "}. {"Search users in ","Hľadať užívateľov v "}. -{"Send announcement to all online users","Odoslať zoznam všetkým online používateľom"}. {"Send announcement to all online users on all hosts","Odoslať oznam všetkým online používateľom na všetkých serveroch"}. -{"Send announcement to all users","Odoslať oznam všetkým používateľom"}. +{"Send announcement to all online users","Odoslať zoznam všetkým online používateľom"}. {"Send announcement to all users on all hosts","Poslať oznámenie všetkým užívateľom na všetkých serveroch"}. +{"Send announcement to all users","Odoslať oznam všetkým používateľom"}. {"September","September"}. -{"Server ~b","Server ~b"}. {"Set message of the day and send to online users","Nastaviť správu dňa a odoslať ju online používateľom"}. {"Set message of the day on all hosts and send to online users","Nastaviť správu dňa na všetkých serveroch a poslať ju online užívateľom"}. {"Shared Roster Groups","Skupiny pre zdieľaný zoznam kontaktov"}. {"Show Integral Table","Zobraziť kompletnú tabuľku"}. {"Show Ordinary Table","Zobraziť bežnú tabuľku"}. {"Shut Down Service","Vypnúť službu"}. -{"~s invites you to the room ~s","~s Vás pozýva do miestnosti ~s"}. {"Specify the access model","Uveďte model prístupu"}. {"Specify the event message type","Uveďte typ pre správu o udalosti"}. {"Specify the publisher model","Špecifikovať model publikovania"}. -{"~s's Offline Messages Queue","~s Offline správy"}. -{"Start Modules at ","Spustiť moduly na "}. -{"Start Modules","Spustiť moduly"}. -{"Start","Štart"}. -{"Statistics of ~p","Štatistiky ~p"}. -{"Statistics","Štatistiky"}. -{"Stop Modules at ","Zastaviť moduly na "}. -{"Stop Modules","Zastaviť moduly"}. {"Stopped Nodes","Zastavené uzly"}. -{"Stop","Zastaviť"}. -{"Storage Type","Typ úložiska"}. {"Store binary backup:","Uložiť binárnu zálohu:"}. {"Store plain text backup:","Uložiť zálohu do textového súboru:"}. {"Subject","Predmet"}. -{"Submit","Odoslať"}. {"Submitted","Odoslané"}. {"Subscriber Address","Adresa odberateľa"}. -{"Subscription","Prihlásenie"}. {"Sunday","Nedeľa"}. {"That nickname is already in use by another occupant","Prezývka je už používaná iným členom"}. {"That nickname is registered by another person","Prezývka je už zaregistrovaná inou osobou"}. {"The CAPTCHA is valid.","Platná CAPTCHA."}. +{"The CAPTCHA verification has failed","Overenie pomocou CAPTCHA zlihalo"}. {"The collections with which a node is affiliated","Kolekcie asociované s uzlom"}. +{"The password is too weak","heslo je"}. {"the password is","heslo je"}. -{"This participant is kicked from the room because he sent an error message to another participant","Účastník bol vyhodený z miestnosti, pretože poslal chybovú správu inému účastníkovi"}. -{"This participant is kicked from the room because he sent an error message","Účastník bol vyhodený z miestnosti, pretože poslal chybovú správu"}. -{"This participant is kicked from the room because he sent an error presence","Účastník bol vyhodený z miestnosti, pretože poslal chybovú správu o stave"}. +{"There was an error creating the account: ","Pri vytváraní účtu nastala chyba: "}. +{"There was an error deleting the account: ","Pri rušení účtu nastala chyba:"}. {"This room is not anonymous","Táto miestnosť nie je anonymná"}. {"Thursday","Štvrtok"}. -{"Time","Čas"}. {"Time delay","Časový posun"}. -{"To","Pre"}. -{"To ~s","Pre ~s"}. +{"Too many CAPTCHA requests","Príliš veľa žiadostí o CAPTCHA"}. {"Traffic rate limit is exceeded","Bol prekročený prenosový limit"}. -{"Transactions Aborted:","Transakcie zrušená"}. -{"Transactions Committed:","Transakcie potvrdená"}. -{"Transactions Logged:","Transakcie zaznamenaná"}. -{"Transactions Restarted:","Transakcie reštartovaná"}. {"Tuesday","Utorok"}. +{"Unable to generate a CAPTCHA","Nepodarilo sa vygenerovat CAPTCHA"}. {"Unauthorized","Neautorizovaný"}. -{"Update ","Aktualizovať "}. -{"Update","Aktualizovať"}. +{"Unregister","Zrušiť účet"}. {"Update message of the day (don't send)","Aktualizovať správu dňa (neodosielať)"}. {"Update message of the day on all hosts (don't send)","Upraviť správu dňa na všetkých serveroch"}. -{"Update plan","Aktualizovať plán"}. -{"Update script","Aktualizované skripty"}. -{"Uptime:","Uptime:"}. -{"Use of STARTTLS required","Použitie STARTTLS je vyžadované"}. +{"User JID","Používateľ "}. {"User Management","Správa užívateľov"}. -{"User ","Používateľ "}. +{"Username:","IRC prezývka"}. {"Users are not allowed to register accounts so quickly","Nieje dovolené vytvárať účty tak rýchlo po sebe"}. {"Users Last Activity","Posledná aktivita používateľa"}. {"Users","Používatelia"}. {"User","Užívateľ"}. -{"Validate","Overiť"}. {"vCard User Search","Hľadať užívateľov vo vCard"}. {"Virtual Hosts","Virtuálne servery"}. {"Visitors are not allowed to change their nicknames in this room","V tejto miestnosti nieje povolené meniť prezývky"}. {"Visitors are not allowed to send messages to all occupants","Návštevníci nemajú povolené zasielať správy všetkým prihláseným do konferencie"}. +{"Voice requests are disabled in this conference","Žiadosti o Voice nie sú povolené v tejto konferencii"}. +{"Voice request","Žiadosť o Voice"}. {"Wednesday","Streda"}. {"When to send the last published item","Kedy odoslať posledne publikovanú položku"}. {"Whether to allow subscriptions","Povoliť prihlasovanie"}. {"You have been banned from this room","Boli ste vylúčený z tejto miestnosti"}. {"You must fill in field \"Nickname\" in the form","Musíte vyplniť políčko \"Prezývka\" vo formulári"}. -{"You need an x:data capable client to configure mod_irc settings","Pre konfiguráciu mod_irc potrebujete klienta podporujúceho x:data"}. -{"You need an x:data capable client to configure room","Na konfiguráciu miestnosti potrebujete klienta podporujúceho x:data"}. +{"You need a client that supports x:data and CAPTCHA to register","Na registráciu prezývky potrebujete klienta podporujúceho z x:data"}. +{"You need a client that supports x:data to register the nickname","Na registráciu prezývky potrebujete klienta podporujúceho z x:data"}. {"You need an x:data capable client to search","Na vyhľadávanie potrebujete klienta podporujúceho x:data"}. +{"Your active privacy list has denied the routing of this stanza.","Aktívny list súkromia zbránil v smerovaní tejto stanzy."}. {"Your contact offline message queue is full. The message has been discarded.","Fronta offline správ tohoto kontaktu je plná. Správa bola zahodená."}. -{"Your messages to ~s are being blocked. To unblock them, visit ~s","Správa určená pre ~s bola zablokovaná. Oblokovať ju môžete na ~s"}. +{"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","Správa určená pre ~s bola zablokovaná. Oblokovať ju môžete na ~s"}. diff --git a/priv/msgs/sk.po b/priv/msgs/sk.po deleted file mode 100644 index 6429aa696..000000000 --- a/priv/msgs/sk.po +++ /dev/null @@ -1,1834 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: 2.1.x\n" -"PO-Revision-Date: 2012-04-29 18:25+0000\n" -"Last-Translator: Marek Bečka \n" -"Language: sk\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Language: Slovak (slovenčina)\n" -"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2\n" -"X-Additional-Translator: Juraj Michalek\n" -"X-Additional-Translator: SkLUG\n" - -#: ejabberd_c2s.erl:424 ejabberd_c2s.erl:727 -msgid "Use of STARTTLS required" -msgstr "Je vyžadované použitie STARTTLS " - -#: ejabberd_c2s.erl:503 -msgid "No resource provided" -msgstr "Nebol poskytnutý žiadny zdroj" - -#: ejabberd_c2s.erl:1197 -msgid "Replaced by new connection" -msgstr "Nahradené novým spojením" - -#: ejabberd_c2s.erl:1885 -msgid "Your active privacy list has denied the routing of this stanza." -msgstr "Aktívny list súkromia zbránil v smerovaní tejto stanzy." - -#: ejabberd_captcha.erl:96 ejabberd_captcha.erl:152 ejabberd_captcha.erl:178 -msgid "Enter the text you see" -msgstr "Zadajte zobrazený text" - -#: ejabberd_captcha.erl:101 -msgid "Your messages to ~s are being blocked. To unblock them, visit ~s" -msgstr "Správa určená pre ~s bola zablokovaná. Oblokovať ju môžete na ~s" - -#: ejabberd_captcha.erl:134 -msgid "If you don't see the CAPTCHA image here, visit the web page." -msgstr "Pokiaľ nevidíte obrázok CAPTCHA, navštívte webovú stránku." - -#: ejabberd_captcha.erl:146 -msgid "CAPTCHA web page" -msgstr "Webová stránka CAPTCHA" - -#: ejabberd_captcha.erl:307 -msgid "The CAPTCHA is valid." -msgstr "Platná CAPTCHA." - -#: mod_adhoc.erl:95 mod_adhoc.erl:125 mod_adhoc.erl:143 mod_adhoc.erl:161 -msgid "Commands" -msgstr "Príkazy" - -#: mod_adhoc.erl:149 mod_adhoc.erl:243 -msgid "Ping" -msgstr "Ping" - -#: mod_adhoc.erl:260 -msgid "Pong" -msgstr "Pong" - -#: mod_announce.erl:516 -msgid "Really delete message of the day?" -msgstr "Skutočne zmazať správu dňa?" - -#: mod_announce.erl:524 mod_configure.erl:1083 mod_configure.erl:1128 -msgid "Subject" -msgstr "Predmet" - -#: mod_announce.erl:529 mod_configure.erl:1088 mod_configure.erl:1133 -msgid "Message body" -msgstr "Telo správy" - -#: mod_announce.erl:609 -msgid "No body provided for announce message" -msgstr "Správa neobsahuje text" - -#: mod_announce.erl:644 -msgid "Announcements" -msgstr "Oznámenia" - -#: mod_announce.erl:646 -msgid "Send announcement to all users" -msgstr "Odoslať oznam všetkým používateľom" - -#: mod_announce.erl:648 -msgid "Send announcement to all users on all hosts" -msgstr "Poslať oznámenie všetkým užívateľom na všetkých serveroch" - -#: mod_announce.erl:650 -msgid "Send announcement to all online users" -msgstr "Odoslať zoznam všetkým online používateľom" - -#: mod_announce.erl:652 mod_configure.erl:1078 mod_configure.erl:1123 -msgid "Send announcement to all online users on all hosts" -msgstr "Odoslať oznam všetkým online používateľom na všetkých serveroch" - -#: mod_announce.erl:654 -msgid "Set message of the day and send to online users" -msgstr "Nastaviť správu dňa a odoslať ju online používateľom" - -#: mod_announce.erl:656 -msgid "Set message of the day on all hosts and send to online users" -msgstr "" -"Nastaviť správu dňa na všetkých serveroch a poslať ju online užívateľom" - -#: mod_announce.erl:658 -msgid "Update message of the day (don't send)" -msgstr "Aktualizovať správu dňa (neodosielať)" - -#: mod_announce.erl:660 -msgid "Update message of the day on all hosts (don't send)" -msgstr "Upraviť správu dňa na všetkých serveroch" - -#: mod_announce.erl:662 -msgid "Delete message of the day" -msgstr "Zmazať správu dňa" - -#: mod_announce.erl:664 -msgid "Delete message of the day on all hosts" -msgstr "Zmazať správu dňa na všetkých serveroch" - -#: mod_configure.erl:114 mod_configure.erl:274 mod_configure.erl:296 -#: mod_configure.erl:498 -msgid "Configuration" -msgstr "Konfigurácia" - -#: mod_configure.erl:125 mod_configure.erl:576 web/ejabberd_web_admin.erl:1929 -msgid "Database" -msgstr "Databáza" - -#: mod_configure.erl:127 mod_configure.erl:595 -msgid "Start Modules" -msgstr "Spustiť moduly" - -#: mod_configure.erl:129 mod_configure.erl:596 -msgid "Stop Modules" -msgstr "Zastaviť moduly" - -#: mod_configure.erl:131 mod_configure.erl:604 web/ejabberd_web_admin.erl:1930 -msgid "Backup" -msgstr "Zálohovať" - -#: mod_configure.erl:133 mod_configure.erl:605 -msgid "Restore" -msgstr "Obnoviť" - -#: mod_configure.erl:135 mod_configure.erl:606 -msgid "Dump to Text File" -msgstr "Uložiť do textového súboru" - -#: mod_configure.erl:137 mod_configure.erl:615 -msgid "Import File" -msgstr "Import súboru" - -#: mod_configure.erl:139 mod_configure.erl:616 -msgid "Import Directory" -msgstr "Import adresára" - -#: mod_configure.erl:141 mod_configure.erl:581 mod_configure.erl:1057 -msgid "Restart Service" -msgstr "Reštartovať službu" - -#: mod_configure.erl:143 mod_configure.erl:582 mod_configure.erl:1102 -msgid "Shut Down Service" -msgstr "Vypnúť službu" - -#: mod_configure.erl:145 mod_configure.erl:518 mod_configure.erl:1197 -#: web/ejabberd_web_admin.erl:1527 -msgid "Add User" -msgstr "Pridať používateľa" - -#: mod_configure.erl:147 mod_configure.erl:519 mod_configure.erl:1219 -msgid "Delete User" -msgstr "Vymazať užívateľa" - -#: mod_configure.erl:149 mod_configure.erl:520 mod_configure.erl:1231 -msgid "End User Session" -msgstr "Ukončiť reláciu užívateľa" - -#: mod_configure.erl:151 mod_configure.erl:521 mod_configure.erl:1243 -#: mod_configure.erl:1255 -msgid "Get User Password" -msgstr "Zobraziť heslo užívateľa" - -#: mod_configure.erl:153 mod_configure.erl:522 -msgid "Change User Password" -msgstr "Zmeniť heslo užívateľa" - -#: mod_configure.erl:155 mod_configure.erl:523 mod_configure.erl:1272 -msgid "Get User Last Login Time" -msgstr "Zobraziť čas posledného prihlásenia" - -#: mod_configure.erl:157 mod_configure.erl:524 mod_configure.erl:1284 -msgid "Get User Statistics" -msgstr "Zobraziť štatistiku užívateľa" - -#: mod_configure.erl:159 mod_configure.erl:525 -msgid "Get Number of Registered Users" -msgstr "Zobraziť počet registrovaných užívateľov" - -#: mod_configure.erl:161 mod_configure.erl:526 -msgid "Get Number of Online Users" -msgstr "Zobraziť počet pripojených užívateľov" - -#: mod_configure.erl:163 mod_configure.erl:509 web/ejabberd_web_admin.erl:831 -#: web/ejabberd_web_admin.erl:872 -msgid "Access Control Lists" -msgstr "Zoznamy prístupových oprávnení (ACL)" - -#: mod_configure.erl:165 mod_configure.erl:510 web/ejabberd_web_admin.erl:940 -#: web/ejabberd_web_admin.erl:976 -msgid "Access Rules" -msgstr "Prístupové pravidlá" - -#: mod_configure.erl:297 mod_configure.erl:499 -msgid "User Management" -msgstr "Správa užívateľov" - -#: mod_configure.erl:500 web/ejabberd_web_admin.erl:1058 -#: web/ejabberd_web_admin.erl:1462 -msgid "Online Users" -msgstr "Online užívatelia" - -#: mod_configure.erl:501 -msgid "All Users" -msgstr "Všetci užívatelia" - -#: mod_configure.erl:502 -msgid "Outgoing s2s Connections" -msgstr "Odchádzajúce s2s spojenia" - -#: mod_configure.erl:503 web/ejabberd_web_admin.erl:1900 -msgid "Running Nodes" -msgstr "Bežiace uzly" - -#: mod_configure.erl:504 web/ejabberd_web_admin.erl:1902 -msgid "Stopped Nodes" -msgstr "Zastavené uzly" - -#: mod_configure.erl:577 mod_configure.erl:587 web/ejabberd_web_admin.erl:1946 -msgid "Modules" -msgstr "Moduly" - -#: mod_configure.erl:578 -msgid "Backup Management" -msgstr "Správa zálohovania" - -#: mod_configure.erl:579 -msgid "Import Users From jabberd14 Spool Files" -msgstr "Importovať užívateľov z jabberd14 spool súborov" - -#: mod_configure.erl:699 -msgid "To ~s" -msgstr "Pre ~s" - -#: mod_configure.erl:717 -msgid "From ~s" -msgstr "Od ~s" - -#: mod_configure.erl:913 -msgid "Database Tables Configuration at " -msgstr "Konfigurácia databázových tabuliek " - -#: mod_configure.erl:918 -msgid "Choose storage type of tables" -msgstr "Vyberte typ úložiska pre tabuľky" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Disc only copy" -msgstr "Len kópia disku" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM and disc copy" -msgstr "Kópia RAM a disku" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM copy" -msgstr "Kópia RAM" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Remote copy" -msgstr "Vzdialená kópia" - -#: mod_configure.erl:950 -msgid "Stop Modules at " -msgstr "Zastaviť moduly na " - -#: mod_configure.erl:954 -msgid "Choose modules to stop" -msgstr "Vyberte moduly, ktoré majú byť zastavené" - -#: mod_configure.erl:969 -msgid "Start Modules at " -msgstr "Spustiť moduly na " - -#: mod_configure.erl:973 -msgid "Enter list of {Module, [Options]}" -msgstr "Vložte zoznam modulov {Modul, [Parametre]}" - -#: mod_configure.erl:974 -msgid "List of modules to start" -msgstr "Zoznam modulov, ktoré majú byť spustené" - -#: mod_configure.erl:983 -msgid "Backup to File at " -msgstr "Záloha do súboru na " - -#: mod_configure.erl:987 mod_configure.erl:1001 -msgid "Enter path to backup file" -msgstr "Zadajte cestu k súboru so zálohou" - -#: mod_configure.erl:988 mod_configure.erl:1002 mod_configure.erl:1016 -#: mod_configure.erl:1030 -msgid "Path to File" -msgstr "Cesta k súboru" - -#: mod_configure.erl:997 -msgid "Restore Backup from File at " -msgstr "Obnoviť zálohu zo súboru na " - -#: mod_configure.erl:1011 -msgid "Dump Backup to Text File at " -msgstr "Uložiť zálohu do textového súboru na " - -#: mod_configure.erl:1015 -msgid "Enter path to text file" -msgstr "Zadajte cestu k textovému súboru" - -#: mod_configure.erl:1025 -msgid "Import User from File at " -msgstr "Importovať užívateľa zo súboru na " - -#: mod_configure.erl:1029 -msgid "Enter path to jabberd14 spool file" -msgstr "Zadajte cestu k spool súboru jabberd14" - -#: mod_configure.erl:1039 -msgid "Import Users from Dir at " -msgstr "Importovať užívateľov z adresára na " - -#: mod_configure.erl:1043 -msgid "Enter path to jabberd14 spool dir" -msgstr "Zadajte cestu k jabberd14 spool adresáru" - -#: mod_configure.erl:1044 -msgid "Path to Dir" -msgstr "Cesta k adresáru" - -#: mod_configure.erl:1060 mod_configure.erl:1105 -msgid "Time delay" -msgstr "Časový posun" - -#: mod_configure.erl:1143 -msgid "Access Control List Configuration" -msgstr "Konfigurácia zoznamu prístupových oprávnení (ACL)" - -#: mod_configure.erl:1147 -msgid "Access control lists" -msgstr "Zoznamy prístupových oprávnení (ACL)" - -#: mod_configure.erl:1171 -msgid "Access Configuration" -msgstr "Konfigurácia prístupu" - -#: mod_configure.erl:1175 -msgid "Access rules" -msgstr "Prístupové pravidlá" - -#: mod_configure.erl:1200 mod_configure.erl:1222 mod_configure.erl:1234 -#: mod_configure.erl:1246 mod_configure.erl:1258 mod_configure.erl:1275 -#: mod_configure.erl:1287 mod_configure.erl:1650 mod_configure.erl:1700 -#: mod_configure.erl:1721 mod_roster.erl:1345 mod_vcard.erl:543 -#: mod_vcard_ldap.erl:554 -msgid "Jabber ID" -msgstr "Jabber ID" - -#: mod_configure.erl:1205 mod_configure.erl:1263 mod_configure.erl:1651 -#: mod_configure.erl:1861 mod_muc/mod_muc_room.erl:3218 mod_register.erl:235 -#: web/ejabberd_web_admin.erl:1520 -msgid "Password" -msgstr "Heslo" - -#: mod_configure.erl:1210 -msgid "Password Verification" -msgstr "Overenie hesla" - -#: mod_configure.erl:1301 -msgid "Number of registered users" -msgstr "Počet registrovaných užívateľov" - -#: mod_configure.erl:1315 -msgid "Number of online users" -msgstr "Počet online užívateľov" - -#: mod_configure.erl:1682 web/ejabberd_web_admin.erl:1587 -#: web/ejabberd_web_admin.erl:1736 -msgid "Never" -msgstr "Nikdy" - -#: mod_configure.erl:1696 web/ejabberd_web_admin.erl:1600 -#: web/ejabberd_web_admin.erl:1749 -msgid "Online" -msgstr "Online" - -#: mod_configure.erl:1701 -msgid "Last login" -msgstr "Posledné prihlásenie" - -#: mod_configure.erl:1722 -msgid "Roster size" -msgstr "Počet kontaktov v zozname" - -#: mod_configure.erl:1723 -msgid "IP addresses" -msgstr "IP adresa" - -#: mod_configure.erl:1724 -msgid "Resources" -msgstr "Zdroje" - -#: mod_configure.erl:1848 -msgid "Administration of " -msgstr "Administrácia " - -#: mod_configure.erl:1851 -msgid "Action on user" -msgstr "Operácia aplikovaná na užívateľa" - -#: mod_configure.erl:1855 -msgid "Edit Properties" -msgstr "Editovať vlastnosti" - -#: mod_configure.erl:1858 web/ejabberd_web_admin.erl:1762 -msgid "Remove User" -msgstr "Odstrániť užívateľa" - -#: mod_irc/mod_irc.erl:207 mod_muc/mod_muc.erl:406 -msgid "Access denied by service policy" -msgstr "Prístup bol zamietnutý nastavením služby" - -#: mod_irc/mod_irc.erl:409 -msgid "IRC Transport" -msgstr "IRC Transport" - -#: mod_irc/mod_irc.erl:436 -msgid "ejabberd IRC module" -msgstr "ejabberd IRC modul" - -#: mod_irc/mod_irc.erl:600 -msgid "You need an x:data capable client to configure mod_irc settings" -msgstr "Pre konfiguráciu mod_irc potrebujete klienta podporujúceho x:data" - -#: mod_irc/mod_irc.erl:607 -msgid "Registration in mod_irc for " -msgstr "Registrácia do mod_irc na" - -#: mod_irc/mod_irc.erl:612 -msgid "" -"Enter username, encodings, ports and passwords you wish to use for " -"connecting to IRC servers" -msgstr "" -"Vložte meno používateľa, kódovanie, porty a heslo ktoré chcete používať pri " -"pripojení na IRC server" - -#: mod_irc/mod_irc.erl:617 -msgid "IRC Username" -msgstr "IRC prezývka" - -#: mod_irc/mod_irc.erl:627 -msgid "" -"If you want to specify different ports, passwords, encodings for IRC " -"servers, fill this list with values in format '{\"irc server\", \"encoding" -"\", port, \"password\"}'. By default this service use \"~s\" encoding, port " -"~p, empty password." -msgstr "" -"Ak chcete zadať iné porty, heslá a kódovania pre IRC servery, vyplnte zoznam " -"s hodnotami vo formáte '{\"irc server\",\"kódovanie\", \"port\", \"heslo" -"\"}'. Predvolenéi hodnoty pre túto službu sú: kódovanie \"~s\", port ~p a " -"žiadne heslo." - -#: mod_irc/mod_irc.erl:639 -msgid "" -"Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef." -"net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]." -msgstr "" -"Príklad: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef." -"net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]." - -#: mod_irc/mod_irc.erl:644 -msgid "Connections parameters" -msgstr "Parametre spojenia" - -#: mod_irc/mod_irc.erl:787 -msgid "Join IRC channel" -msgstr "Pripojit IRC kanál" - -#: mod_irc/mod_irc.erl:791 -msgid "IRC channel (don't put the first #)" -msgstr "IRC kanál (bez počiatočnej #)" - -#: mod_irc/mod_irc.erl:796 -msgid "IRC server" -msgstr "IRC server" - -#: mod_irc/mod_irc.erl:829 mod_irc/mod_irc.erl:833 -msgid "Join the IRC channel here." -msgstr "Propojiť IRC kanál sem." - -#: mod_irc/mod_irc.erl:837 -msgid "Join the IRC channel in this Jabber ID: ~s" -msgstr "Pripojit IRC kanál k tomuto Jabber ID: ~s" - -#: mod_irc/mod_irc.erl:915 -msgid "IRC settings" -msgstr "Nastavania IRC" - -#: mod_irc/mod_irc.erl:920 -msgid "" -"Enter username and encodings you wish to use for connecting to IRC servers. " -"Press 'Next' to get more fields to fill in. Press 'Complete' to save " -"settings." -msgstr "" -"Vložte meno používateľa a kódovanie, ktoré chcete používať pri pripojení na " -"IRC servery. Kliknutím na tlačítko 'Ďalej' môžete zadať niektoré ďalšie " -"hodnoty. Pomocou 'Ukončiť ' uložíte nastavenia." - -#: mod_irc/mod_irc.erl:926 -msgid "IRC username" -msgstr "IRC prezývka" - -#: mod_irc/mod_irc.erl:975 -msgid "Password ~b" -msgstr "Heslo ~b" - -#: mod_irc/mod_irc.erl:980 -msgid "Port ~b" -msgstr "Port ~b" - -#: mod_irc/mod_irc.erl:985 -msgid "Encoding for server ~b" -msgstr "Kódovanie pre server ~b" - -#: mod_irc/mod_irc.erl:994 -msgid "Server ~b" -msgstr "Server ~b" - -#: mod_muc/mod_muc.erl:520 -msgid "Only service administrators are allowed to send service messages" -msgstr "Iba správcovia služby majú povolené odosielanie servisných správ" - -#: mod_muc/mod_muc.erl:564 -msgid "Room creation is denied by service policy" -msgstr "Vytváranie miestnosti nie je povolené" - -#: mod_muc/mod_muc.erl:571 -msgid "Conference room does not exist" -msgstr "Diskusná miestnosť neexistuje" - -#: mod_muc/mod_muc.erl:673 -msgid "Chatrooms" -msgstr "Diskusné miestnosti" - -#: mod_muc/mod_muc.erl:829 -msgid "You need a client that supports x:data to register the nickname" -msgstr "Na registráciu prezývky potrebujete klienta podporujúceho z x:data" - -#: mod_muc/mod_muc.erl:835 -msgid "Nickname Registration at " -msgstr "Registrácia prezývky na " - -#: mod_muc/mod_muc.erl:839 -msgid "Enter nickname you want to register" -msgstr "Zadajte prezývku, ktorú chcete registrovať" - -#: mod_muc/mod_muc.erl:840 mod_muc/mod_muc_room.erl:3804 mod_roster.erl:1346 -#: mod_vcard.erl:435 mod_vcard.erl:548 -msgid "Nickname" -msgstr "Prezývka" - -#: mod_muc/mod_muc.erl:924 mod_muc/mod_muc_room.erl:1059 -#: mod_muc/mod_muc_room.erl:1783 -msgid "That nickname is registered by another person" -msgstr "Prezývka je už zaregistrovaná inou osobou" - -#: mod_muc/mod_muc.erl:951 -msgid "You must fill in field \"Nickname\" in the form" -msgstr "Musíte vyplniť políčko \"Prezývka\" vo formulári" - -#: mod_muc/mod_muc.erl:971 -msgid "ejabberd MUC module" -msgstr "ejabberd MUC modul" - -#: mod_muc/mod_muc_log.erl:374 mod_muc/mod_muc_log.erl:381 -msgid "Chatroom configuration modified" -msgstr "Nastavenie diskusnej miestnosti bolo zmenené" - -#: mod_muc/mod_muc_log.erl:384 -msgid "joins the room" -msgstr "vstúpil(a) do miestnosti" - -#: mod_muc/mod_muc_log.erl:387 mod_muc/mod_muc_log.erl:390 -msgid "leaves the room" -msgstr "odišiel(a) z miestnosti" - -#: mod_muc/mod_muc_log.erl:393 mod_muc/mod_muc_log.erl:396 -msgid "has been banned" -msgstr "bol(a) zablokovaný(á)" - -#: mod_muc/mod_muc_log.erl:399 mod_muc/mod_muc_log.erl:402 -msgid "has been kicked" -msgstr "bol(a) vyhodený(á) z miestnosti" - -#: mod_muc/mod_muc_log.erl:405 -msgid "has been kicked because of an affiliation change" -msgstr "bol vyhodený(á) kvôli zmene priradenia" - -#: mod_muc/mod_muc_log.erl:408 -msgid "has been kicked because the room has been changed to members-only" -msgstr "bol vyhodený(á), pretože miestnosť bola vyhradená len pre členov" - -#: mod_muc/mod_muc_log.erl:411 -msgid "has been kicked because of a system shutdown" -msgstr "bol vyhodený(á) kvôli reštartu systému" - -#: mod_muc/mod_muc_log.erl:414 -msgid "is now known as" -msgstr "sa premenoval(a) na" - -#: mod_muc/mod_muc_log.erl:417 mod_muc/mod_muc_log.erl:688 -#: mod_muc/mod_muc_room.erl:2389 -msgid " has set the subject to: " -msgstr "zmenil(a) tému na: " - -#: mod_muc/mod_muc_log.erl:452 -msgid "Chatroom is created" -msgstr "Diskusná miestnosť je vytvorená" - -#: mod_muc/mod_muc_log.erl:453 -msgid "Chatroom is destroyed" -msgstr "Diskusná miestnosť je zrušená" - -#: mod_muc/mod_muc_log.erl:454 -msgid "Chatroom is started" -msgstr "Diskusná miestnosť je obnovená" - -#: mod_muc/mod_muc_log.erl:455 -msgid "Chatroom is stopped" -msgstr "Diskusná miestnosť je pozastavená" - -#: mod_muc/mod_muc_log.erl:459 -msgid "Monday" -msgstr "Pondelok" - -#: mod_muc/mod_muc_log.erl:460 -msgid "Tuesday" -msgstr "Utorok" - -#: mod_muc/mod_muc_log.erl:461 -msgid "Wednesday" -msgstr "Streda" - -#: mod_muc/mod_muc_log.erl:462 -msgid "Thursday" -msgstr "Štvrtok" - -#: mod_muc/mod_muc_log.erl:463 -msgid "Friday" -msgstr "Piatok" - -#: mod_muc/mod_muc_log.erl:464 -msgid "Saturday" -msgstr "Sobota" - -#: mod_muc/mod_muc_log.erl:465 -msgid "Sunday" -msgstr "Nedeľa" - -#: mod_muc/mod_muc_log.erl:469 -msgid "January" -msgstr "Január" - -#: mod_muc/mod_muc_log.erl:470 -msgid "February" -msgstr "Február" - -#: mod_muc/mod_muc_log.erl:471 -msgid "March" -msgstr "Marec" - -#: mod_muc/mod_muc_log.erl:472 -msgid "April" -msgstr "Apríl" - -#: mod_muc/mod_muc_log.erl:473 -msgid "May" -msgstr "Máj" - -#: mod_muc/mod_muc_log.erl:474 -msgid "June" -msgstr "Jún" - -#: mod_muc/mod_muc_log.erl:475 -msgid "July" -msgstr "Júl" - -#: mod_muc/mod_muc_log.erl:476 -msgid "August" -msgstr "August" - -#: mod_muc/mod_muc_log.erl:477 -msgid "September" -msgstr "September" - -#: mod_muc/mod_muc_log.erl:478 -msgid "October" -msgstr "Október" - -#: mod_muc/mod_muc_log.erl:479 -msgid "November" -msgstr "November" - -#: mod_muc/mod_muc_log.erl:480 -msgid "December" -msgstr "December" - -#: mod_muc/mod_muc_log.erl:750 -msgid "Room Configuration" -msgstr "Nastavenia miestnosti" - -#: mod_muc/mod_muc_log.erl:759 -msgid "Room Occupants" -msgstr "Ľudí v miestnosti" - -#: mod_muc/mod_muc_room.erl:170 -msgid "Traffic rate limit is exceeded" -msgstr "Bol prekročený prenosový limit" - -#: mod_muc/mod_muc_room.erl:242 -msgid "" -"This participant is kicked from the room because he sent an error message" -msgstr "Účastník bol vyhodený z miestnosti, pretože poslal chybovú správu" - -#: mod_muc/mod_muc_room.erl:251 -msgid "It is not allowed to send private messages to the conference" -msgstr "Nie je povolené odosielať súkromné správy do konferencie" - -#: mod_muc/mod_muc_room.erl:328 -msgid "Please, wait for a while before sending new voice request" -msgstr "Prosím počkate, predtým než pošlete novú žiadosť o Voice" - -#: mod_muc/mod_muc_room.erl:343 -msgid "Voice requests are disabled in this conference" -msgstr "Žiadosti o Voice nie sú povolené v tejto konferencii" - -#: mod_muc/mod_muc_room.erl:360 -msgid "Failed to extract JID from your voice request approval" -msgstr "Nepodarilo sa nájsť JID v súhlase o Voice." - -#: mod_muc/mod_muc_room.erl:389 -msgid "Only moderators can approve voice requests" -msgstr "Len moderátori môžu schváliť žiadosť o Voice" - -#: mod_muc/mod_muc_room.erl:404 -msgid "Improper message type" -msgstr "Nesprávny typ správy" - -#: mod_muc/mod_muc_room.erl:514 -msgid "" -"This participant is kicked from the room because he sent an error message to " -"another participant" -msgstr "" -"Účastník bol vyhodený z miestnosti, pretože poslal chybovú správu inému " -"účastníkovi" - -#: mod_muc/mod_muc_room.erl:527 -msgid "It is not allowed to send private messages of type \"groupchat\"" -msgstr "Nie je dovolené odoslanie súkromnej správy typu \"Skupinová správa\" " - -#: mod_muc/mod_muc_room.erl:539 mod_muc/mod_muc_room.erl:607 -msgid "Recipient is not in the conference room" -msgstr "Príjemca sa nenachádza v konferenčnej miestnosti" - -#: mod_muc/mod_muc_room.erl:560 mod_muc/mod_muc_room.erl:581 -msgid "It is not allowed to send private messages" -msgstr "Nieje povolené posielať súkromné správy" - -#: mod_muc/mod_muc_room.erl:572 mod_muc/mod_muc_room.erl:951 -#: mod_muc/mod_muc_room.erl:4034 -msgid "Only occupants are allowed to send messages to the conference" -msgstr "Len členovia majú povolené zasielať správy do konferencie" - -#: mod_muc/mod_muc_room.erl:630 -msgid "Only occupants are allowed to send queries to the conference" -msgstr "Len členovia majú povolené dotazovať sa o konferencii" - -#: mod_muc/mod_muc_room.erl:642 -msgid "Queries to the conference members are not allowed in this room" -msgstr "Dotazovať sa o členoch nie je v tejto miestnosti povolené" - -#: mod_muc/mod_muc_room.erl:927 -msgid "" -"Only moderators and participants are allowed to change the subject in this " -"room" -msgstr "Len moderátori a zúčastnený majú povolené meniť tému tejto miestnosti" - -#: mod_muc/mod_muc_room.erl:932 -msgid "Only moderators are allowed to change the subject in this room" -msgstr "Len moderátori majú povolené meniť tému miestnosti" - -#: mod_muc/mod_muc_room.erl:942 -msgid "Visitors are not allowed to send messages to all occupants" -msgstr "" -"Návštevníci nemajú povolené zasielať správy všetkým prihláseným do " -"konferencie" - -#: mod_muc/mod_muc_room.erl:1016 -msgid "" -"This participant is kicked from the room because he sent an error presence" -msgstr "" -"Účastník bol vyhodený z miestnosti, pretože poslal chybovú správu o stave" - -#: mod_muc/mod_muc_room.erl:1035 -msgid "Visitors are not allowed to change their nicknames in this room" -msgstr "V tejto miestnosti nieje povolené meniť prezývky" - -#: mod_muc/mod_muc_room.erl:1048 mod_muc/mod_muc_room.erl:1775 -msgid "That nickname is already in use by another occupant" -msgstr "Prezývka je už používaná iným členom" - -#: mod_muc/mod_muc_room.erl:1764 -msgid "You have been banned from this room" -msgstr "Boli ste vylúčený z tejto miestnosti" - -#: mod_muc/mod_muc_room.erl:1767 -msgid "Membership is required to enter this room" -msgstr "Pre vstup do miestnosti je potrebné byť členom" - -#: mod_muc/mod_muc_room.erl:1803 -msgid "This room is not anonymous" -msgstr "Táto miestnosť nie je anonymná" - -#: mod_muc/mod_muc_room.erl:1829 -msgid "A password is required to enter this room" -msgstr "Pre vstup do miestnosti je potrebné heslo" - -#: mod_muc/mod_muc_room.erl:1851 mod_register.erl:246 -msgid "Too many CAPTCHA requests" -msgstr "Príliš veľa žiadostí o CAPTCHA" - -#: mod_muc/mod_muc_room.erl:1860 mod_register.erl:251 -msgid "Unable to generate a CAPTCHA" -msgstr "Nepodarilo sa vygenerovat CAPTCHA" - -#: mod_muc/mod_muc_room.erl:1870 -msgid "Incorrect password" -msgstr "Nesprávne heslo" - -#: mod_muc/mod_muc_room.erl:2444 -msgid "Administrator privileges required" -msgstr "Sú potrebné práva administrátora" - -#: mod_muc/mod_muc_room.erl:2459 -msgid "Moderator privileges required" -msgstr "Sú potrebné práva moderátora" - -#: mod_muc/mod_muc_room.erl:2615 -msgid "Jabber ID ~s is invalid" -msgstr "Jabber ID ~s je neplatné" - -#: mod_muc/mod_muc_room.erl:2629 -msgid "Nickname ~s does not exist in the room" -msgstr "Prezývka ~s v miestnosti neexistuje" - -#: mod_muc/mod_muc_room.erl:2655 mod_muc/mod_muc_room.erl:3045 -msgid "Invalid affiliation: ~s" -msgstr "Neplatné priradenie: ~s" - -#: mod_muc/mod_muc_room.erl:2709 -msgid "Invalid role: ~s" -msgstr "Neplatná rola: ~s" - -#: mod_muc/mod_muc_room.erl:3022 mod_muc/mod_muc_room.erl:3058 -msgid "Owner privileges required" -msgstr "Sú vyžadované práva vlastníka" - -#: mod_muc/mod_muc_room.erl:3189 -msgid "Configuration of room ~s" -msgstr "Konfigurácia miestnosti ~s" - -#: mod_muc/mod_muc_room.erl:3194 -msgid "Room title" -msgstr "Názov miestnosti" - -#: mod_muc/mod_muc_room.erl:3197 mod_muc/mod_muc_room.erl:3686 -msgid "Room description" -msgstr "Popis miestnosti" - -#: mod_muc/mod_muc_room.erl:3204 -msgid "Make room persistent" -msgstr "Nastaviť miestnosť ako trvalú" - -#: mod_muc/mod_muc_room.erl:3209 -msgid "Make room public searchable" -msgstr "Nastaviť miestnosť ako verejne prehľadávateľnú" - -#: mod_muc/mod_muc_room.erl:3212 -msgid "Make participants list public" -msgstr "Nastaviť zoznam zúčastnených ako verejný" - -#: mod_muc/mod_muc_room.erl:3215 -msgid "Make room password protected" -msgstr "Chrániť miestnosť heslom" - -#: mod_muc/mod_muc_room.erl:3226 -msgid "Maximum Number of Occupants" -msgstr "Počet účastníkov" - -#: mod_muc/mod_muc_room.erl:3233 -msgid "No limit" -msgstr "Bez limitu" - -#: mod_muc/mod_muc_room.erl:3244 -msgid "Present real Jabber IDs to" -msgstr "Zobrazovať skutočné Jabber ID" - -#: mod_muc/mod_muc_room.erl:3252 mod_muc/mod_muc_room.erl:3286 -msgid "moderators only" -msgstr "moderátorom" - -#: mod_muc/mod_muc_room.erl:3254 mod_muc/mod_muc_room.erl:3288 -msgid "anyone" -msgstr "všetkým" - -#: mod_muc/mod_muc_room.erl:3256 -msgid "Make room members-only" -msgstr "Nastaviť miestnosť len pre členov" - -#: mod_muc/mod_muc_room.erl:3259 -msgid "Make room moderated" -msgstr "Nastaviť miestnosť ako moderovanú" - -#: mod_muc/mod_muc_room.erl:3262 -msgid "Default users as participants" -msgstr "Užívatelia sú implicitne členmi" - -#: mod_muc/mod_muc_room.erl:3265 -msgid "Allow users to change the subject" -msgstr "Povoliť užívateľom meniť tému" - -#: mod_muc/mod_muc_room.erl:3268 -msgid "Allow users to send private messages" -msgstr "Povoliť užívateľom odosielať súkromné správy" - -#: mod_muc/mod_muc_room.erl:3273 -msgid "Allow visitors to send private messages to" -msgstr "Povoliť užívateľom odosielať súkromné správy" - -#: mod_muc/mod_muc_room.erl:3284 -msgid "nobody" -msgstr "nikto" - -#: mod_muc/mod_muc_room.erl:3290 -msgid "Allow users to query other users" -msgstr "Povoliť užívateľom dotazovať sa informácie o iných užívateľoch" - -#: mod_muc/mod_muc_room.erl:3293 -msgid "Allow users to send invites" -msgstr "Povoliť používateľom posielanie pozvánok" - -#: mod_muc/mod_muc_room.erl:3296 -msgid "Allow visitors to send status text in presence updates" -msgstr "Návštevníci môžu posielať textové informácie v stavových správach" - -#: mod_muc/mod_muc_room.erl:3299 -msgid "Allow visitors to change nickname" -msgstr "Návštevníci môžu meniť prezývky" - -#: mod_muc/mod_muc_room.erl:3302 -msgid "Allow visitors to send voice requests" -msgstr "Povoliť používateľom posielanie pozvánok" - -#: mod_muc/mod_muc_room.erl:3305 -msgid "Minimum interval between voice requests (in seconds)" -msgstr "Minimum interval between voice requests (in seconds)" - -#: mod_muc/mod_muc_room.erl:3311 -msgid "Make room CAPTCHA protected" -msgstr "Chrániť miestnosť systémom CAPTCHA" - -#: mod_muc/mod_muc_room.erl:3316 -msgid "Exclude Jabber IDs from CAPTCHA challenge" -msgstr "Nepoužívať CAPTCHA pre nasledujúce Jabber ID" - -#: mod_muc/mod_muc_room.erl:3323 -msgid "Enable logging" -msgstr "Zapnúť zaznamenávanie histórie" - -#: mod_muc/mod_muc_room.erl:3331 -msgid "You need an x:data capable client to configure room" -msgstr "Na konfiguráciu miestnosti potrebujete klienta podporujúceho x:data" - -#: mod_muc/mod_muc_room.erl:3688 -msgid "Number of occupants" -msgstr "Počet zúčastnených" - -#: mod_muc/mod_muc_room.erl:3744 -msgid "private, " -msgstr "súkromná, " - -#: mod_muc/mod_muc_room.erl:3793 -msgid "Voice request" -msgstr "Žiadosť o Voice" - -#: mod_muc/mod_muc_room.erl:3797 -msgid "Either approve or decline the voice request." -msgstr "Povolte alebo zamietnite žiadosť o Voice." - -#: mod_muc/mod_muc_room.erl:3803 -msgid "User JID" -msgstr "Používateľ " - -#: mod_muc/mod_muc_room.erl:3805 -msgid "Grant voice to this person?" -msgstr "Prideltiť Voice tejto osobe?" - -#: mod_muc/mod_muc_room.erl:3954 -msgid "~s invites you to the room ~s" -msgstr "~s Vás pozýva do miestnosti ~s" - -#: mod_muc/mod_muc_room.erl:3963 -msgid "the password is" -msgstr "heslo je" - -#: mod_offline.erl:623 -msgid "" -"Your contact offline message queue is full. The message has been discarded." -msgstr "Fronta offline správ tohoto kontaktu je plná. Správa bola zahodená." - -#: mod_offline.erl:713 -msgid "~s's Offline Messages Queue" -msgstr "~s Offline správy" - -#: mod_offline.erl:716 mod_roster.erl:1389 mod_shared_roster.erl:1060 -#: mod_shared_roster.erl:1169 web/ejabberd_web_admin.erl:833 -#: web/ejabberd_web_admin.erl:874 web/ejabberd_web_admin.erl:942 -#: web/ejabberd_web_admin.erl:978 web/ejabberd_web_admin.erl:1019 -#: web/ejabberd_web_admin.erl:1508 web/ejabberd_web_admin.erl:1753 -#: web/ejabberd_web_admin.erl:1924 web/ejabberd_web_admin.erl:1956 -#: web/ejabberd_web_admin.erl:2024 web/ejabberd_web_admin.erl:2128 -#: web/ejabberd_web_admin.erl:2153 web/ejabberd_web_admin.erl:2241 -msgid "Submitted" -msgstr "Odoslané" - -#: mod_offline.erl:724 -msgid "Time" -msgstr "Čas" - -#: mod_offline.erl:725 -msgid "From" -msgstr "Od" - -#: mod_offline.erl:726 -msgid "To" -msgstr "Pre" - -#: mod_offline.erl:727 -msgid "Packet" -msgstr "Paket" - -#: mod_offline.erl:740 mod_shared_roster.erl:1067 -#: web/ejabberd_web_admin.erl:882 web/ejabberd_web_admin.erl:986 -msgid "Delete Selected" -msgstr "Zmazať vybrané" - -#: mod_offline.erl:864 -msgid "Offline Messages:" -msgstr "Offline správy" - -#: mod_offline.erl:864 -msgid "Remove All Offline Messages" -msgstr "Odstrániť všetky offline správy" - -#: mod_proxy65/mod_proxy65_service.erl:213 -msgid "ejabberd SOCKS5 Bytestreams module" -msgstr "ejabberd SOCKS5 Bytestreams modul" - -#: mod_pubsub/mod_pubsub.erl:1120 mod_pubsub/mod_pubsub_odbc.erl:929 -msgid "Publish-Subscribe" -msgstr "Publish-Subscribe" - -#: mod_pubsub/mod_pubsub.erl:1214 mod_pubsub/mod_pubsub_odbc.erl:1025 -msgid "ejabberd Publish-Subscribe module" -msgstr "ejabberd Publish-Subscribe modul" - -#: mod_pubsub/mod_pubsub.erl:1497 mod_pubsub/mod_pubsub_odbc.erl:1312 -msgid "PubSub subscriber request" -msgstr "Žiadosť odberateľa PubSub" - -#: mod_pubsub/mod_pubsub.erl:1499 mod_pubsub/mod_pubsub_odbc.erl:1314 -msgid "Choose whether to approve this entity's subscription." -msgstr "Zvolte, či chcete povoliť toto odoberanie" - -#: mod_pubsub/mod_pubsub.erl:1505 mod_pubsub/mod_pubsub_odbc.erl:1320 -msgid "Node ID" -msgstr "ID uzlu" - -#: mod_pubsub/mod_pubsub.erl:1510 mod_pubsub/mod_pubsub_odbc.erl:1325 -msgid "Subscriber Address" -msgstr "Adresa odberateľa" - -#: mod_pubsub/mod_pubsub.erl:1516 mod_pubsub/mod_pubsub_odbc.erl:1331 -msgid "Allow this Jabber ID to subscribe to this pubsub node?" -msgstr "Dovoliť tomuto Jabber ID odoberať PubSub uzol?" - -#: mod_pubsub/mod_pubsub.erl:3474 mod_pubsub/mod_pubsub_odbc.erl:3287 -msgid "Deliver payloads with event notifications" -msgstr "Doručiť náklad s upozornením na udalosť" - -#: mod_pubsub/mod_pubsub.erl:3475 mod_pubsub/mod_pubsub_odbc.erl:3288 -msgid "Deliver event notifications" -msgstr "Doručiť oznamy o udalosti" - -#: mod_pubsub/mod_pubsub.erl:3476 mod_pubsub/mod_pubsub_odbc.erl:3289 -msgid "Notify subscribers when the node configuration changes" -msgstr "Upozorniť prihlásených používateľov na zmenu nastavenia uzlu" - -#: mod_pubsub/mod_pubsub.erl:3477 mod_pubsub/mod_pubsub_odbc.erl:3290 -msgid "Notify subscribers when the node is deleted" -msgstr "Upozorniť prihlásených používateľov na zmazanie uzlu" - -#: mod_pubsub/mod_pubsub.erl:3478 mod_pubsub/mod_pubsub_odbc.erl:3291 -msgid "Notify subscribers when items are removed from the node" -msgstr "Upozorniť prihlásených používateľov na odstránenie položiek z uzlu" - -#: mod_pubsub/mod_pubsub.erl:3479 mod_pubsub/mod_pubsub_odbc.erl:3292 -msgid "Persist items to storage" -msgstr "Uložiť položky natrvalo do úložiska" - -#: mod_pubsub/mod_pubsub.erl:3480 mod_pubsub/mod_pubsub_odbc.erl:3293 -msgid "A friendly name for the node" -msgstr "Prístupný názov pre uzol" - -#: mod_pubsub/mod_pubsub.erl:3481 mod_pubsub/mod_pubsub_odbc.erl:3294 -msgid "Max # of items to persist" -msgstr "Maximálny počet položiek, ktoré je možné natrvalo uložiť" - -#: mod_pubsub/mod_pubsub.erl:3482 mod_pubsub/mod_pubsub_odbc.erl:3295 -msgid "Whether to allow subscriptions" -msgstr "Povoliť prihlasovanie" - -#: mod_pubsub/mod_pubsub.erl:3483 mod_pubsub/mod_pubsub_odbc.erl:3296 -msgid "Specify the access model" -msgstr "Uveďte model prístupu" - -#: mod_pubsub/mod_pubsub.erl:3486 mod_pubsub/mod_pubsub_odbc.erl:3299 -msgid "Roster groups allowed to subscribe" -msgstr "Skupiny kontaktov, ktoré môžu odoberať" - -#: mod_pubsub/mod_pubsub.erl:3487 mod_pubsub/mod_pubsub_odbc.erl:3300 -msgid "Specify the publisher model" -msgstr "Špecifikovať model publikovania" - -#: mod_pubsub/mod_pubsub.erl:3489 mod_pubsub/mod_pubsub_odbc.erl:3302 -msgid "Purge all items when the relevant publisher goes offline" -msgstr "" -"Odstrániť všetky relevantné položky, keď užívateľ prejde do módu offline" - -#: mod_pubsub/mod_pubsub.erl:3490 mod_pubsub/mod_pubsub_odbc.erl:3303 -msgid "Specify the event message type" -msgstr "Uveďte typ pre správu o udalosti" - -#: mod_pubsub/mod_pubsub.erl:3492 mod_pubsub/mod_pubsub_odbc.erl:3305 -msgid "Max payload size in bytes" -msgstr "Maximálny náklad v bajtoch" - -#: mod_pubsub/mod_pubsub.erl:3493 mod_pubsub/mod_pubsub_odbc.erl:3306 -msgid "When to send the last published item" -msgstr "Kedy odoslať posledne publikovanú položku" - -#: mod_pubsub/mod_pubsub.erl:3495 mod_pubsub/mod_pubsub_odbc.erl:3308 -msgid "Only deliver notifications to available users" -msgstr "Doručovať upozornenia len aktuálne prihláseným používateľom" - -#: mod_pubsub/mod_pubsub.erl:3496 mod_pubsub/mod_pubsub_odbc.erl:3309 -msgid "The collections with which a node is affiliated" -msgstr "Kolekcie asociované s uzlom" - -#: mod_register.erl:193 -msgid "The CAPTCHA verification has failed" -msgstr "Overenie pomocou CAPTCHA zlihalo" - -#: mod_register.erl:220 -msgid "You need a client that supports x:data and CAPTCHA to register" -msgstr "Na registráciu prezývky potrebujete klienta podporujúceho z x:data" - -#: mod_register.erl:226 mod_register.erl:265 -msgid "Choose a username and password to register with this server" -msgstr "Zvolte meno užívateľa a heslo pre registráciu na tomto servere" - -#: mod_register.erl:230 mod_vcard.erl:435 web/ejabberd_web_admin.erl:1515 -#: web/ejabberd_web_admin.erl:1571 -msgid "User" -msgstr "Užívateľ" - -#: mod_register.erl:316 mod_register.erl:361 -msgid "The password is too weak" -msgstr "heslo je" - -#: mod_register.erl:365 -msgid "Users are not allowed to register accounts so quickly" -msgstr "Nieje dovolené vytvárať účty tak rýchlo po sebe" - -#: mod_roster.erl:1340 web/ejabberd_web_admin.erl:1695 -#: web/ejabberd_web_admin.erl:1879 web/ejabberd_web_admin.erl:1890 -#: web/ejabberd_web_admin.erl:2212 -msgid "None" -msgstr "Nič" - -#: mod_roster.erl:1347 -msgid "Subscription" -msgstr "Prihlásenie" - -#: mod_roster.erl:1348 -msgid "Pending" -msgstr "Čakajúce" - -#: mod_roster.erl:1349 -msgid "Groups" -msgstr "Skupiny" - -#: mod_roster.erl:1376 -msgid "Validate" -msgstr "Overiť" - -#: mod_roster.erl:1384 -msgid "Remove" -msgstr "Odstrániť" - -#: mod_roster.erl:1387 -msgid "Roster of " -msgstr "Zoznam kontaktov " - -#: mod_roster.erl:1390 mod_shared_roster.erl:1061 mod_shared_roster.erl:1170 -#: web/ejabberd_web_admin.erl:834 web/ejabberd_web_admin.erl:875 -#: web/ejabberd_web_admin.erl:943 web/ejabberd_web_admin.erl:979 -#: web/ejabberd_web_admin.erl:1020 web/ejabberd_web_admin.erl:1509 -#: web/ejabberd_web_admin.erl:1754 web/ejabberd_web_admin.erl:1925 -#: web/ejabberd_web_admin.erl:2129 web/ejabberd_web_admin.erl:2154 -msgid "Bad format" -msgstr "Zlý formát" - -#: mod_roster.erl:1397 -msgid "Add Jabber ID" -msgstr "Pridať Jabber ID" - -#: mod_roster.erl:1496 -msgid "Roster" -msgstr "Zoznam kontaktov" - -#: mod_shared_roster.erl:1016 mod_shared_roster.erl:1058 -#: mod_shared_roster.erl:1166 -msgid "Shared Roster Groups" -msgstr "Skupiny pre zdieľaný zoznam kontaktov" - -#: mod_shared_roster.erl:1054 web/ejabberd_web_admin.erl:1365 -#: web/ejabberd_web_admin.erl:2454 -msgid "Add New" -msgstr "Pridať nový" - -#: mod_shared_roster.erl:1137 -msgid "Name:" -msgstr "Meno:" - -#: mod_shared_roster.erl:1142 -msgid "Description:" -msgstr "Popis:" - -#: mod_shared_roster.erl:1150 -msgid "Members:" -msgstr "Členovia:" - -#: mod_shared_roster.erl:1158 -msgid "Displayed Groups:" -msgstr "Zobrazené skupiny:" - -#: mod_shared_roster.erl:1167 -msgid "Group " -msgstr "Skupina " - -#: mod_shared_roster.erl:1176 web/ejabberd_web_admin.erl:840 -#: web/ejabberd_web_admin.erl:884 web/ejabberd_web_admin.erl:949 -#: web/ejabberd_web_admin.erl:1026 web/ejabberd_web_admin.erl:2010 -msgid "Submit" -msgstr "Odoslať" - -#: mod_vcard.erl:172 mod_vcard_ldap.erl:238 -msgid "Erlang Jabber Server" -msgstr "Erlang Jabber Server" - -#: mod_vcard.erl:435 mod_vcard.erl:549 -msgid "Birthday" -msgstr "Dátum narodenia" - -#: mod_vcard.erl:435 mod_vcard.erl:551 -msgid "City" -msgstr "Mesto" - -#: mod_vcard.erl:435 mod_vcard.erl:550 -msgid "Country" -msgstr "Krajina" - -#: mod_vcard.erl:435 mod_vcard.erl:552 -msgid "Email" -msgstr "E-mail" - -#: mod_vcard.erl:435 mod_vcard.erl:547 -msgid "Family Name" -msgstr "Priezvisko" - -#: mod_vcard.erl:435 -msgid "" -"Fill in the form to search for any matching Jabber User (Add * to the end of " -"field to match substring)" -msgstr "" -"Pre vyhľadanie Jabber používateľa vyplňte formulár (pridajte znak * na " -"koniec, pre vyhľadanie podreťazca)" - -#: mod_vcard.erl:435 mod_vcard.erl:544 -msgid "Full Name" -msgstr "Celé meno: " - -#: mod_vcard.erl:435 mod_vcard.erl:546 -msgid "Middle Name" -msgstr "Prostredné meno: " - -#: mod_vcard.erl:435 mod_vcard.erl:545 web/ejabberd_web_admin.erl:1999 -msgid "Name" -msgstr "Meno" - -#: mod_vcard.erl:435 mod_vcard.erl:553 -msgid "Organization Name" -msgstr "Meno organizácie: " - -#: mod_vcard.erl:435 mod_vcard.erl:554 -msgid "Organization Unit" -msgstr "Organizačná jednotka: " - -#: mod_vcard.erl:435 mod_vcard_ldap.erl:462 -msgid "Search users in " -msgstr "Hľadať užívateľov v " - -#: mod_vcard.erl:435 mod_vcard_ldap.erl:462 -msgid "You need an x:data capable client to search" -msgstr "Na vyhľadávanie potrebujete klienta podporujúceho x:data" - -#: mod_vcard.erl:460 mod_vcard_ldap.erl:487 -msgid "vCard User Search" -msgstr "Hľadať užívateľov vo vCard" - -#: mod_vcard.erl:516 mod_vcard_ldap.erl:541 -msgid "ejabberd vCard module" -msgstr "ejabberd vCard modul" - -#: mod_vcard.erl:540 mod_vcard_ldap.erl:551 -msgid "Search Results for " -msgstr "Hľadať výsledky pre " - -#: mod_vcard_ldap.erl:462 -msgid "Fill in fields to search for any matching Jabber User" -msgstr "Vyplnte políčka pre vyhľadávanie Jabber užívateľa" - -#: web/ejabberd_web_admin.erl:193 web/ejabberd_web_admin.erl:203 -#: web/ejabberd_web_admin.erl:219 web/ejabberd_web_admin.erl:229 -msgid "Unauthorized" -msgstr "Neautorizovaný" - -#: web/ejabberd_web_admin.erl:286 web/ejabberd_web_admin.erl:303 -msgid "ejabberd Web Admin" -msgstr "ejabberd Web Admin" - -#: web/ejabberd_web_admin.erl:769 web/ejabberd_web_admin.erl:780 -msgid "Administration" -msgstr "Administrácia" - -#: web/ejabberd_web_admin.erl:878 web/ejabberd_web_admin.erl:982 -msgid "Raw" -msgstr "Surové dáta" - -#: web/ejabberd_web_admin.erl:1017 -msgid "~s access rule configuration" -msgstr "~s konfigurácia prístupového pravidla" - -#: web/ejabberd_web_admin.erl:1035 -msgid "Virtual Hosts" -msgstr "Virtuálne servery" - -#: web/ejabberd_web_admin.erl:1043 web/ejabberd_web_admin.erl:1050 -msgid "Users" -msgstr "Používatelia" - -#: web/ejabberd_web_admin.erl:1078 -msgid "Users Last Activity" -msgstr "Posledná aktivita používateľa" - -#: web/ejabberd_web_admin.erl:1080 -msgid "Period: " -msgstr "Čas:" - -#: web/ejabberd_web_admin.erl:1090 -msgid "Last month" -msgstr "Posledný mesiac" - -#: web/ejabberd_web_admin.erl:1091 -msgid "Last year" -msgstr "Posledný rok" - -#: web/ejabberd_web_admin.erl:1092 -msgid "All activity" -msgstr "Všetky aktivity" - -#: web/ejabberd_web_admin.erl:1094 -msgid "Show Ordinary Table" -msgstr "Zobraziť bežnú tabuľku" - -#: web/ejabberd_web_admin.erl:1096 -msgid "Show Integral Table" -msgstr "Zobraziť kompletnú tabuľku" - -#: web/ejabberd_web_admin.erl:1105 web/ejabberd_web_admin.erl:1932 -msgid "Statistics" -msgstr "Štatistiky" - -#: web/ejabberd_web_admin.erl:1117 -msgid "Not Found" -msgstr "Nebol nájdený" - -#: web/ejabberd_web_admin.erl:1134 -msgid "Node not found" -msgstr "Uzol nenájdený" - -#: web/ejabberd_web_admin.erl:1460 -msgid "Host" -msgstr "Server" - -#: web/ejabberd_web_admin.erl:1461 -msgid "Registered Users" -msgstr "Registrovaní používatelia" - -#: web/ejabberd_web_admin.erl:1572 -msgid "Offline Messages" -msgstr "Offline správy" - -#: web/ejabberd_web_admin.erl:1573 web/ejabberd_web_admin.erl:1760 -msgid "Last Activity" -msgstr "Posledná aktivita" - -#: web/ejabberd_web_admin.erl:1653 web/ejabberd_web_admin.erl:1669 -msgid "Registered Users:" -msgstr "Registrovaní používatelia:" - -#: web/ejabberd_web_admin.erl:1655 web/ejabberd_web_admin.erl:1671 -#: web/ejabberd_web_admin.erl:2185 -msgid "Online Users:" -msgstr "Online používatelia:" - -#: web/ejabberd_web_admin.erl:1657 -msgid "Outgoing s2s Connections:" -msgstr "Odchádzajúce s2s spojenia:" - -#: web/ejabberd_web_admin.erl:1659 -msgid "Outgoing s2s Servers:" -msgstr "Odchádzajúce s2s servery:" - -#: web/ejabberd_web_admin.erl:1728 web/mod_register_web.erl:175 -#: web/mod_register_web.erl:372 web/mod_register_web.erl:381 -#: web/mod_register_web.erl:411 -msgid "Change Password" -msgstr "Zmeniť heslo" - -#: web/ejabberd_web_admin.erl:1751 -msgid "User " -msgstr "Používateľ " - -#: web/ejabberd_web_admin.erl:1758 -msgid "Connected Resources:" -msgstr "Pripojené zdroje:" - -#: web/ejabberd_web_admin.erl:1759 web/mod_register_web.erl:227 -#: web/mod_register_web.erl:519 -msgid "Password:" -msgstr "Heslo:" - -#: web/ejabberd_web_admin.erl:1821 -msgid "No Data" -msgstr "Žiadne dáta" - -#: web/ejabberd_web_admin.erl:1899 -msgid "Nodes" -msgstr "Uzly" - -#: web/ejabberd_web_admin.erl:1922 web/ejabberd_web_admin.erl:1944 -msgid "Node " -msgstr "Uzol" - -#: web/ejabberd_web_admin.erl:1931 -msgid "Listened Ports" -msgstr "Otvorené portov" - -#: web/ejabberd_web_admin.erl:1933 web/ejabberd_web_admin.erl:2253 -#: web/ejabberd_web_admin.erl:2441 -msgid "Update" -msgstr "Aktualizovať" - -#: web/ejabberd_web_admin.erl:1936 web/ejabberd_web_admin.erl:2562 -msgid "Restart" -msgstr "Reštart" - -#: web/ejabberd_web_admin.erl:1938 web/ejabberd_web_admin.erl:2564 -msgid "Stop" -msgstr "Zastaviť" - -#: web/ejabberd_web_admin.erl:1952 -msgid "RPC Call Error" -msgstr "Chyba RPC volania" - -#: web/ejabberd_web_admin.erl:1993 -msgid "Database Tables at " -msgstr "Databázové tabuľky na " - -#: web/ejabberd_web_admin.erl:2000 -msgid "Storage Type" -msgstr "Typ úložiska" - -#: web/ejabberd_web_admin.erl:2001 -msgid "Elements" -msgstr "Prvky" - -#: web/ejabberd_web_admin.erl:2002 -msgid "Memory" -msgstr "Pamäť" - -#: web/ejabberd_web_admin.erl:2025 web/ejabberd_web_admin.erl:2130 -msgid "Error" -msgstr "Chyba" - -#: web/ejabberd_web_admin.erl:2027 -msgid "Backup of " -msgstr "Záloha " - -#: web/ejabberd_web_admin.erl:2029 -msgid "" -"Please note that these options will only backup the builtin Mnesia database. " -"If you are using the ODBC module, you also need to backup your SQL database " -"separately." -msgstr "" -"Prosím, berte na vedomie, že tieto nastavenia zázálohujú iba zabudovnú " -"Mnesia databázu. Ak používate ODBC modul, musíte zálohovať vašu SQL databázu " -"separátne." - -#: web/ejabberd_web_admin.erl:2034 -msgid "Store binary backup:" -msgstr "Uložiť binárnu zálohu:" - -#: web/ejabberd_web_admin.erl:2038 web/ejabberd_web_admin.erl:2045 -#: web/ejabberd_web_admin.erl:2053 web/ejabberd_web_admin.erl:2060 -#: web/ejabberd_web_admin.erl:2067 web/ejabberd_web_admin.erl:2074 -#: web/ejabberd_web_admin.erl:2081 web/ejabberd_web_admin.erl:2089 -#: web/ejabberd_web_admin.erl:2096 web/ejabberd_web_admin.erl:2103 -msgid "OK" -msgstr "OK" - -#: web/ejabberd_web_admin.erl:2041 -msgid "Restore binary backup immediately:" -msgstr "Okamžite obnoviť binárnu zálohu:" - -#: web/ejabberd_web_admin.erl:2049 -msgid "" -"Restore binary backup after next ejabberd restart (requires less memory):" -msgstr "" -"Obnoviť binárnu zálohu pri nasledujúcom reštarte ejabberd (vyžaduje menej " -"pamäte)" - -#: web/ejabberd_web_admin.erl:2056 -msgid "Store plain text backup:" -msgstr "Uložiť zálohu do textového súboru:" - -#: web/ejabberd_web_admin.erl:2063 -msgid "Restore plain text backup immediately:" -msgstr "Okamžite obnoviť zálohu z textového súboru:" - -#: web/ejabberd_web_admin.erl:2070 -msgid "Import users data from a PIEFXIS file (XEP-0227):" -msgstr "Importovat dáta užívateľov zo súboru PIEFXIS (XEP-0227):" - -#: web/ejabberd_web_admin.erl:2077 -msgid "Export data of all users in the server to PIEFXIS files (XEP-0227):" -msgstr "" -"Exportovať dáta všetkých uživateľov na serveri do súborov PIEFXIS (XEP-0227):" - -#: web/ejabberd_web_admin.erl:2084 -msgid "Export data of users in a host to PIEFXIS files (XEP-0227):" -msgstr "" -"Exportovať dáta uživateľov na hostitelovi do súborov PIEFXIS (XEP-0227):" - -#: web/ejabberd_web_admin.erl:2092 -msgid "Import user data from jabberd14 spool file:" -msgstr "Importovať dáta užívateľov z jabberd14 spool súboru:" - -#: web/ejabberd_web_admin.erl:2099 -msgid "Import users data from jabberd14 spool directory:" -msgstr "Importovať dáta užívateľov z jabberd14 spool adresára:" - -#: web/ejabberd_web_admin.erl:2125 -msgid "Listened Ports at " -msgstr "Otvorené porty na " - -#: web/ejabberd_web_admin.erl:2150 -msgid "Modules at " -msgstr "Moduly na " - -#: web/ejabberd_web_admin.erl:2176 -msgid "Statistics of ~p" -msgstr "Štatistiky ~p" - -#: web/ejabberd_web_admin.erl:2179 -msgid "Uptime:" -msgstr "Uptime:" - -#: web/ejabberd_web_admin.erl:2182 -msgid "CPU Time:" -msgstr "Čas procesoru" - -#: web/ejabberd_web_admin.erl:2188 -msgid "Transactions Committed:" -msgstr "Transakcie potvrdená" - -#: web/ejabberd_web_admin.erl:2191 -msgid "Transactions Aborted:" -msgstr "Transakcie zrušená" - -#: web/ejabberd_web_admin.erl:2194 -msgid "Transactions Restarted:" -msgstr "Transakcie reštartovaná" - -#: web/ejabberd_web_admin.erl:2197 -msgid "Transactions Logged:" -msgstr "Transakcie zaznamenaná" - -#: web/ejabberd_web_admin.erl:2239 -msgid "Update " -msgstr "Aktualizovať " - -#: web/ejabberd_web_admin.erl:2247 -msgid "Update plan" -msgstr "Aktualizovať plán" - -#: web/ejabberd_web_admin.erl:2248 -msgid "Modified modules" -msgstr "Modifikované moduly" - -#: web/ejabberd_web_admin.erl:2249 -msgid "Update script" -msgstr "Aktualizované skripty" - -#: web/ejabberd_web_admin.erl:2250 -msgid "Low level update script" -msgstr "Nízkoúrovňový aktualizačný skript" - -#: web/ejabberd_web_admin.erl:2251 -msgid "Script check" -msgstr "Kontrola skriptu" - -#: web/ejabberd_web_admin.erl:2419 -msgid "Port" -msgstr "Port" - -#: web/ejabberd_web_admin.erl:2420 -msgid "IP" -msgstr "IP" - -#: web/ejabberd_web_admin.erl:2421 -msgid "Protocol" -msgstr "Protokol" - -#: web/ejabberd_web_admin.erl:2422 web/ejabberd_web_admin.erl:2549 -msgid "Module" -msgstr "Modul" - -#: web/ejabberd_web_admin.erl:2423 web/ejabberd_web_admin.erl:2550 -msgid "Options" -msgstr "Nastavenia" - -#: web/ejabberd_web_admin.erl:2443 -msgid "Delete" -msgstr "Zmazať" - -#: web/ejabberd_web_admin.erl:2572 -msgid "Start" -msgstr "Štart" - -#: web/mod_register_web.erl:104 -msgid "Your Jabber account was successfully created." -msgstr "Jabber účet bol úspešne vytvorený." - -#: web/mod_register_web.erl:107 -msgid "There was an error creating the account: " -msgstr "Pri vytváraní účtu nastala chyba: " - -#: web/mod_register_web.erl:115 -msgid "Your Jabber account was successfully deleted." -msgstr "Váš Jabber účet bol úspešne odstránený." - -#: web/mod_register_web.erl:118 -msgid "There was an error deleting the account: " -msgstr "Pri rušení účtu nastala chyba:" - -#: web/mod_register_web.erl:128 -msgid "The password of your Jabber account was successfully changed." -msgstr "Heslo k Jabber účtu bolo úspešne zmenené." - -#: web/mod_register_web.erl:131 -msgid "There was an error changing the password: " -msgstr "Pri zmene hesla nastala chyba: " - -#: web/mod_register_web.erl:163 web/mod_register_web.erl:172 -msgid "Jabber Account Registration" -msgstr "Registrácia jabber účtu" - -#: web/mod_register_web.erl:174 web/mod_register_web.erl:192 -#: web/mod_register_web.erl:201 -msgid "Register a Jabber account" -msgstr "Zaregistrovať Jabber účet" - -#: web/mod_register_web.erl:176 web/mod_register_web.erl:493 -#: web/mod_register_web.erl:502 -msgid "Unregister a Jabber account" -msgstr "Zrušiť Jabber účet" - -#: web/mod_register_web.erl:203 -msgid "" -"This page allows to create a Jabber account in this Jabber server. Your JID " -"(Jabber IDentifier) will be of the form: username@server. Please read " -"carefully the instructions to fill correctly the fields." -msgstr "" -"Táto stránka umožňuje refistrovať Jabber účet na tomto serveri. Vaše JID " -"(Jabber IDentifikátor) bude vo formáte: užívateľ@server. Pozorne sledujte " -"inštrukcie, aby ste údaje vypnili správne." - -#: web/mod_register_web.erl:212 web/mod_register_web.erl:386 -#: web/mod_register_web.erl:509 -msgid "Username:" -msgstr "IRC prezývka" - -#: web/mod_register_web.erl:217 -msgid "This is case insensitive: macbeth is the same that MacBeth and Macbeth." -msgstr "" -"Veľké a malé písmená sa nerozlišujú: macbeth je to isté ako MacBeth a " -"Macbeth." - -#: web/mod_register_web.erl:218 -msgid "Characters not allowed:" -msgstr "Nepovolené znaky:" - -#: web/mod_register_web.erl:232 -msgid "" -"Don't tell your password to anybody, not even the administrators of the " -"Jabber server." -msgstr "Nevyzrádzajte heslo nikomu, ani administrátorom tohoto Jabber servera." - -#: web/mod_register_web.erl:234 -msgid "You can later change your password using a Jabber client." -msgstr "Neskôr si heslo môžete zmeniť pomocou Jabber klienta." - -#: web/mod_register_web.erl:235 -msgid "" -"Some Jabber clients can store your password in your computer. Use that " -"feature only if you trust your computer is safe." -msgstr "" -"Niektorí Jabber klenti môžu ukladať heslá v počítači. Používajte túto " -"funkciu len ak veríte, že sú tam v bezpečí. " - -#: web/mod_register_web.erl:237 -msgid "" -"Memorize your password, or write it in a paper placed in a safe place. In " -"Jabber there isn't an automated way to recover your password if you forget " -"it." -msgstr "" -"Zapamätajte si heslo alebo si ho zapíšte na papier. Jabber neposkytuje " -"automatickú funkciu ako zistiť zabudnuté heslo. " - -#: web/mod_register_web.erl:242 web/mod_register_web.erl:406 -msgid "Password Verification:" -msgstr "Overenie hesla" - -#: web/mod_register_web.erl:250 -msgid "Register" -msgstr "Zoznam kontaktov" - -#: web/mod_register_web.erl:396 -msgid "Old Password:" -msgstr "Staré heslo:" - -#: web/mod_register_web.erl:401 -msgid "New Password:" -msgstr "Nové heslo:" - -#: web/mod_register_web.erl:504 -msgid "This page allows to unregister a Jabber account in this Jabber server." -msgstr "" -"Na tejto stránke si môžete zrušiť Jabber účet registrovaný na tomto serveri." - -#: web/mod_register_web.erl:524 -msgid "Unregister" -msgstr "Zrušiť účet" - -#~ msgid "Captcha test failed" -#~ msgstr "Platná CAPTCHA." - -#~ msgid "Encodings" -#~ msgstr "Kódovania" - -#~ msgid "(Raw)" -#~ msgstr "(Raw)" - -#~ msgid "Specified nickname is already registered" -#~ msgstr "Zadaná prezývka je už registrovaná" - -#~ msgid "Size" -#~ msgstr "Veľkosť" - -#~ msgid "You must fill in field \"nick\" in the form" -#~ msgstr "Musíte vyplniť políčko \"prezývka\" vo formulári" diff --git a/priv/msgs/sq.msg b/priv/msgs/sq.msg new file mode 100644 index 000000000..de51a9614 --- /dev/null +++ b/priv/msgs/sq.msg @@ -0,0 +1,345 @@ +%% Generated automatically +%% DO NOT EDIT: run `make translations` instead +%% To improve translations please read: +%% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ + +{" has set the subject to: "," ka caktuar si subjekt: "}. +{"# participants","# pjesëmarrës"}. +{"A description of the node","Përshkrim i nyjës"}. +{"A friendly name for the node","Emër miqësor për nyjën"}. +{"A password is required to enter this room","Lypset fjalëkalim për të hyrë në këtë dhomë"}. +{"A Web Page","Faqe Web"}. +{"Accept","Pranoje"}. +{"Account doesn't exist","Llogaria s’ekziston"}. +{"Add User","Shtoni Përdorues"}. +{"Administration of ","Administrim i "}. +{"Administration","Administrim"}. +{"Administrator privileges required","Lyp privilegje përgjegjësi"}. +{"All activity","Krejt veprimtaria"}. +{"All Users","Krejt Përdoruesit"}. +{"Allow subscription","Lejo pajtim"}. +{"Allow users to query other users","Lejojuni përdoruesve të kërkojnë për përdorues të tjerë"}. +{"Allow users to send invites","Lejojuni përdoruesve të dërgojnë ftesa"}. +{"Allow users to send private messages","Lejojuni përdoruesve të dërgojnë mesazhe private"}. +{"Allow visitors to change nickname","Lejojuni përdoruesve të ndryshojnë nofkë"}. +{"Allow visitors to send private messages to","Lejojuni përdoruesve të dërgojnë mesazhe private te"}. +{"Announcements","Lajmërime"}. +{"Answer to a question","Përgjigjuni një pyetje"}. +{"Anyone may publish","Gjithkush mund të publikojë"}. +{"Anyone with Voice","Cilido me Zë"}. +{"Anyone","Cilido"}. +{"API Commands","Urdhra API"}. +{"April","Prill"}. +{"Arguments","Argumente"}. +{"Attribute 'channel' is required for this request","Atributi 'channel' është i domosdoshëm për këtë kërkesë"}. +{"Attribute 'jid' is not allowed here","Atributi 'jid' s’lejohet këtu"}. +{"Attribute 'node' is not allowed here","Atributi 'node' s’lejohet këtu"}. +{"August","Gusht"}. +{"Automatic node creation is not enabled","S’është aktivizuar krijimi automatik i nyjes"}. +{"Backup Management","Administrim Kopjeruajtjesh"}. +{"Backup of ~p","Kopjeruajtje e ~p"}. +{"Backup to File at ","Kopjeruaje te Kartelë në "}. +{"Backup","Kopjeruajtje"}. +{"Bad format","Format i gabuar"}. +{"Birthday","Datëlindje"}. +{"Both the username and the resource are required","Janë të domosdoshëm të dy, emri i përdoruesit dhe burimi"}. +{"Cannot remove active list","S’hiqet dot lista aktive"}. +{"Cannot remove default list","S’hiqet dot lista parazgjedhje"}. +{"CAPTCHA web page","Faqe web e CAPTCHA-s"}. +{"Change Password","Ndryshoni Fjalëkalimin"}. +{"Change User Password","Ndryshoni Fjalëkalim Përdoruesi"}. +{"Changing password is not allowed","Nuk lejohet ndryshimi i fjalëkalimit"}. +{"Changing role/affiliation is not allowed","Nuk lejohet ndryshim roli/përkatësie"}. +{"Channel already exists","Kanali ekziston tashmë"}. +{"Channel does not exist","Kanali s’ekziston"}. +{"Channel JID","JID Kanali"}. +{"Channels","Kanale"}. +{"Characters not allowed:","Shenja të palejuara:"}. +{"Chatroom configuration modified","Ndryshoi formësimi i dhomës së fjalosjeve"}. +{"Chatroom is created","Dhoma e fjalosjes u krijua"}. +{"Chatroom is destroyed","Dhoma e fjalosjes u asgjësua"}. +{"Chatroom is started","Dhoma e fjalosjes u nis"}. +{"Chatroom is stopped","Dhoma e fjalosjes u ndal"}. +{"Chatrooms","Dhoma fjalosjeje"}. +{"Choose a username and password to register with this server","Zgjidhni një emër përdoruesi dhe fjalëkalim për ta regjistruar me këtë shërbyes"}. +{"Choose storage type of tables","Zgjidhni lloj depozitimi tableash"}. +{"City","Qytet"}. +{"Commands","Urdhra"}. +{"Conference room does not exist","Dhoma e konferencës s’ekziston"}. +{"Configuration of room ~s","Formësim i dhomë ~s"}. +{"Configuration","Formësim"}. +{"Country","Vend"}. +{"Current Discussion Topic","Tema e Tanishme e Diskutimit"}. +{"Database failure","Dështim baze të dhënash"}. +{"Database Tables Configuration at ","Formësim Tabelash Baze të Dhënash te "}. +{"Database","Bazë të dhënash"}. +{"December","Dhjetor"}. +{"Delete message of the day","Fshini mesazhin e ditës"}. +{"Delete User","Fshi Përdorues"}. +{"Deliver event notifications","Dërgo njoftime aktesh"}. +{"Disc only copy","Kopje vetëm në disk"}. +{"Duplicated groups are not allowed by RFC6121","Grupe të përsëdytur s’lejohen nga RFC6121"}. +{"Edit Properties","Përpunoni Veti"}. +{"ejabberd","ejabberd"}. +{"Email Address","Adresë Email"}. +{"Email","Email"}. +{"Enable logging","Aktivizo regjistrim"}. +{"Enable message archiving","Aktivizoni arkivim mesazhesh"}. +{"Enter path to backup file","Jepni shteg për te kartelë kopjeruajtje"}. +{"Enter path to text file","Jepni shteg për te kartelë tekst"}. +{"Enter the text you see","Jepni tekstin që shihni"}. +{"External component failure","Dështim përbërësi të jashtëm"}. +{"External component timeout","Mbarim kohe për përbërës të jashtëm"}. +{"Failed to parse HTTP response","S’u arrit të përtypet përgjigje HTTP"}. +{"Failed to process option '~s'","S’u arrit të përpunohej mundësia '~s'"}. +{"Family Name","Mbiemër"}. +{"FAQ Entry","Zë PBR-sh"}. +{"February","Shkurt"}. +{"File larger than ~w bytes","Kartelë më e madhe se ~w bajte"}. +{"Fill in the form to search for any matching XMPP User","Plotësoni formularin që të kërkohet për çfarëdo përdoruesi XMPP me përputhje"}. +{"Friday","E premte"}. +{"From ~ts","Nga ~ts"}. +{"Full List of Room Admins","Listë e Plotë Përgjegjësish Dhome"}. +{"Full List of Room Owners","Listë e Plotë të Zotësh Dhome"}. +{"Full Name","Emër i Plotë"}. +{"Get List of Online Users","Merr Listë Përdoruesish Në Linjë"}. +{"Get List of Registered Users","Merr Listë Përdoruesish të Regjistruar"}. +{"Get Number of Online Users","Merr Numër Përdoruesish Në Linjë"}. +{"Get Number of Registered Users","Merr Numër Përdoruesish të Regjistruar"}. +{"Get User Statistics","Merr Statistika Përdoruesi"}. +{"Given Name","Emër"}. +{"Grant voice to this person?","T’i akordohet zë këtij personi?"}. +{"has been banned","është dëbuar"}. +{"has been kicked","është përzënë"}. +{"Hat title","Titull kapeleje"}. +{"Host unknown","Strehë e panjohur"}. +{"HTTP File Upload","Ngarkim Kartelash HTTP"}. +{"Idle connection","Lidhje e plogësht"}. +{"Import Directory","Importoni Drejtori"}. +{"Import File","Importoni Kartelë"}. +{"Import User from File at ","Importo Përdorues prej Kartele te "}. +{"Import Users from Dir at ","Importo Përdorues nga Drejtori te "}. +{"Improper message type","Lloj i pasaktë mesazhesh"}. +{"Incorrect CAPTCHA submit","Parashtim Captcha-je të pasaktë"}. +{"Incorrect password","Fjalëkalim i pasaktë"}. +{"Incorrect value of 'action' attribute","Vlerë e pavlefshme atributi 'action'"}. +{"Insufficient privilege","Privilegj i pamjaftueshëm"}. +{"Internal server error","Gabim i brendshëm shërbyesi"}. +{"Invalid node name","Emër i pavlefshëm nyjeje"}. +{"Invalid 'previd' value","Vlerë e pavlefshme 'previd'"}. +{"Invitations are not allowed in this conference","Në këtë konferencë nuk lejohen ftesa"}. +{"IP addresses","Adresa IP"}. +{"is now known as","tani njihet si"}. +{"It is not allowed to send private messages to the conference","Nuk lejohet të dërgohen mesazhe private te konferenca"}. +{"Jabber ID","ID Jabber"}. +{"January","Janar"}. +{"JID normalization failed","Normalizimi JID dështoi"}. +{"joins the room","hyn te dhoma"}. +{"July","Korrik"}. +{"June","Qershor"}. +{"Just created","Të sapokrijuara"}. +{"Last Activity","Veprimtaria e Fundit"}. +{"Last login","Hyrja e fundit"}. +{"Last message","Mesazhi i fundit"}. +{"Last month","Muaji i fundit"}. +{"Last year","Viti i shkuar"}. +{"leaves the room","del nga dhoma"}. +{"Logging","Regjistrim"}. +{"Make participants list public","Bëje publike listën e pjesëmarrësve"}. +{"Make room CAPTCHA protected","Bëje dhomën të mbrojtur me CAPTCHA"}. +{"Make room members-only","Bëje dhomën vetëm për anëtarët"}. +{"Make room moderated","Bëje dhomën të moderuar"}. +{"Make room password protected","Bëje dhomën të mbrojtur me fjalëkalim"}. +{"Make room persistent","Bëje dhomën të qëndrueshme"}. +{"Make room public searchable","Bëje dhomën të kërkueshme publikisht"}. +{"Malformed username","Faqerojtës i keqformuar"}. +{"March","Mars"}. +{"Max payload size in bytes","Madhësi maksimum ngarkese në bajte"}. +{"Maximum file size","Madhësi maksimum kartelash"}. +{"Maximum Number of Occupants","Numër Maksimum të Pranishmish"}. +{"May","Maj"}. +{"Membership is required to enter this room","Lypset anëtarësim për të hyrë në këtë dhomë"}. +{"Message body","Lëndë mesazhi"}. +{"Messages from strangers are rejected","Mesazhet prej të panjohurish hidhen tej"}. +{"Messages of type headline","Mesazhe të llojit titull"}. +{"Messages of type normal","Mesazhe të llojit normal"}. +{"Middle Name","Emër i Dytë"}. +{"Moderator privileges required","Lypset privilegj moderatori"}. +{"Moderator","Moderator"}. +{"Moderators Only","Vetëm Moderatorët"}. +{"Module failed to handle the query","Moduli s’arrii të trajtonte kërkesën"}. +{"Monday","E hënë"}. +{"Multicast","Multikast"}. +{"Multi-User Chat","Fjalosje Me Shumë Përdorues Njëherësh"}. +{"Name","Emër"}. +{"Natural-Language Room Name","Emër Dhome Në Gjuhë Natyrale"}. +{"Never","Kurrë"}. +{"New Password:","Fjalëkalim i Ri:"}. +{"Nickname can't be empty","Nofka s’mund të jetë e zbrazët"}. +{"Nickname Registration at ","Regjistrim Nofke te "}. +{"Nickname ~s does not exist in the room","Në këtë dhomë s’ekziston nofka ~s"}. +{"Nickname","Nofkë"}. +{"No address elements found","S’u gjetën elementë adrese"}. +{"No addresses element found","S’u gjetën elementë adresash"}. +{"No child elements found","S’u gjetën elementë pjella"}. +{"No Data","S’ka të Dhëna"}. +{"No items found in this query","S’u gjetën objekte në këtë kërkesë"}. +{"No limit","Pa kufi"}. +{"No node specified","S’u përcaktua nyjë"}. +{"No pending subscriptions found","S’u gjetën pajtime pezull"}. +{"No privacy list with this name found","S’u gjet listë privatësie me atë emër"}. +{"No running node found","S’u gjet nyjë në funksionim"}. +{"No services available","S’ka shërbime të gatshme"}. +{"No statistics found for this item","S’u gjetën statistika për këtë objekt"}. +{"Nobody","Askush"}. +{"Node already exists","Nyja ekziston tashmë"}. +{"Node ID","ID Nyjeje"}. +{"Node index not found","S’u gjet tregues nyje"}. +{"Node not found","S’u gjet nyjë"}. +{"Node ~p","Nyjë ~p"}. +{"Node","Nyjë"}. +{"Nodes","Nyja"}. +{"None","Asnjë"}. +{"Not allowed","E palejuar"}. +{"Not Found","S’u Gjet"}. +{"Not subscribed","Jo i pajtuar"}. +{"November","Nëntor"}. +{"Number of answers required","Numër përgjigjesh të domosdoshme"}. +{"Number of occupants","Numër të pranishmish"}. +{"Number of Offline Messages","Numër Mesazhesh Jo Në Linjë"}. +{"Number of online users","Numër përdoruesish në linjë"}. +{"Number of registered users","Numër përdoruesish të regjistruar"}. +{"Occupants are allowed to invite others","Të pranishmëve u është lejuar të ftojnë të tjerë"}. +{"Occupants are allowed to query others","Të pranishmëve u është lejuar t’u bëjnë kërkim të tjerëve"}. +{"Occupants May Change the Subject","Të pranishmit Mund të Ndryshojnë Subjektin"}. +{"October","Tetor"}. +{"OK","OK"}. +{"Old Password:","Fjalëkalimi i Vjetër:"}. +{"Online Users","Përdorues Në Linjë"}. +{"Online","Në linjë"}. +{"Only deliver notifications to available users","Dorëzo njoftime vetëm te përdoruesit e pranishëm"}. +{"Only moderators are allowed to retract messages","Vetëm të moderatorëve u lejohet të tërheqin mbrapsht mesazhe"}. +{"Only occupants are allowed to send messages to the conference","Vetëm të pranishmëve u lejohet të dërgojnë mesazhe te konferenca"}. +{"Only publishers may publish","Vetëm botuesit mund të botojnë"}. +{"Organization Name","Emër Enti"}. +{"Organization Unit","Njësi Organizative"}. +{"Outgoing s2s Connections","Lidhje s2s Ikëse"}. +{"Owner privileges required","Lypset privilegje të zoti"}. +{"Participant ID","ID Pjesëmarrësi"}. +{"Participant","Pjesëmarrës"}. +{"Password Verification","Verifikim Fjalëkalimi"}. +{"Password Verification:","Verifikim Fjalëkalimi:"}. +{"Password","Fjalëkalim"}. +{"Password:","Fjalëkalim:"}. +{"Path to Dir","Shteg për te Drejtori"}. +{"Path to File","Shteg për te Kartelë"}. +{"Period: ","Periudhë: "}. +{"Ping","Ping"}. +{"Pong","Pong"}. +{"Previous session not found","S’u gjet sesion i mëparshëm"}. +{"Previous session PID is dead","PID e sesionit të mëparshëm është e asgjësuar"}. +{"Previous session timed out","Sesionit të mëparshëm i mbaroi koha"}. +{"private, ","private, "}. +{"RAM and disc copy","RAM dhe kopje në disk"}. +{"RAM copy","Kopje në RAM"}. +{"Really delete message of the day?","Të fshihet vërtet mesazhi i ditës?"}. +{"Recipient is not in the conference room","Pjesëmarrësi s’është në dhomën e konferencës"}. +{"Register an XMPP account","Regjistroni një llogari XMPP"}. +{"Register","Regjistrohuni"}. +{"Remote copy","Kopje e largët"}. +{"Remove User","Hiqeni Përdoruesin"}. +{"Replaced by new connection","Zëvendësuar nga lidhje e re"}. +{"Request has timed out","Kërkesës i mbaroi koha"}. +{"Request is ignored","Kërkesa u shpërfill"}. +{"Requested role","Rol i domosdoshëm"}. +{"Resources","Burime"}. +{"Restart Service","Rinise Shërbimin"}. +{"Restore","Riktheje"}. +{"Roles that May Send Private Messages","Role që Mund të Dërgojnë Mesazhe Private"}. +{"Room Configuration","Formësim Dhome"}. +{"Room description","Përshkrim i dhomës"}. +{"Room Occupants","Të pranishëm Në Dhomë"}. +{"Room title","Titull dhome"}. +{"Running Nodes","Nyje Në Punë"}. +{"Saturday","E shtunë"}. +{"Search from the date","Kërko nga data"}. +{"Search Results for ","Përfundime Kërkimi për "}. +{"Search the text","Kërkoni për tekst"}. +{"Search until the date","Kërko deri më datën"}. +{"Search users in ","Kërko përdorues te "}. +{"Send announcement to all users","Dërgo njoftim krejt përdoruesve"}. +{"September","Shtator"}. +{"Server:","Shërbyes:"}. +{"Show Integral Table","Shfaq Tabelë të Plotë"}. +{"Show Ordinary Table","Shfaq Tabelë të Rëndomtë"}. +{"Shut Down Service","Fike Shërbimin"}. +{"Specify the access model","Specifikoni model hyrjeje"}. +{"Specify the event message type","Përcaktoni llojin e mesazhit për aktin"}. +{"Specify the publisher model","Përcaktoni model botuesi"}. +{"Stanza id is not valid","ID Stanza s’është i vlefshëm"}. +{"Stopped Nodes","Nyja të Ndalura"}. +{"Subject","Subjekti"}. +{"Submitted","Parashtruar"}. +{"Subscriber Address","Adresë e Pajtimtarit"}. +{"Sunday","E diel"}. +{"The account already exists","Ka tashmë një llogari të tillë"}. +{"The account was not unregistered","Llogaria s’qe çregjistruar"}. +{"The CAPTCHA is valid.","Kaptça është e vlefshme."}. +{"The default language of the node","Gjuha parazgjedhje e nyjës"}. +{"The feature requested is not supported by the conference","Veçoria e kërkuar nuk mbulohen nga konferenca"}. +{"The JID of the node creator","JID i krijjuesit të nyjës"}. +{"The list of all online users","Lista e krejt përdoruesve në linjë"}. +{"The name of the node","Emri i nyjës"}. +{"The number of subscribers to the node","Numri i pajtimtarëve te nyja"}. +{"The number of unread or undelivered messages","Numri i mesazheve të palexuar ose të padorëzuar"}. +{"The password is too weak","Fjalëkalimi është shumë i dobët"}. +{"the password is","fjalëkalimi është"}. +{"The password of your XMPP account was successfully changed.","Fjalëkalimi i llogarisë tuaj XMPP u ndryshua me sukses."}. +{"The password was not changed","Fjalëkalimi s’u ndryshua"}. +{"The passwords are different","Fjalëkalimet janë të ndryshëm"}. +{"The sender of the last received message","Dërguesi i mesazhit të fundit të marrë"}. +{"There was an error changing the password: ","Pati një gabim në ndryshimin e fjalëkalimit: "}. +{"There was an error creating the account: ","Pati një gabim në krijimin e llogarisë: "}. +{"This room is not anonymous","Kjo dhomë s’është anonime"}. +{"Thursday","E enjte"}. +{"Time delay","Vonesë kohore"}. +{"Too many CAPTCHA requests","Shumë kërkesa ndaj CAPTCHA-s"}. +{"Too many child elements","Shumë elementë pjella"}. +{"Too many elements","Shumë elementë "}. +{"Too many elements","Shumë elementë "}. +{"Too many users in this conference","Shumë përdorues në këtë konferencë"}. +{"Tuesday","E martë"}. +{"Unable to generate a CAPTCHA","S’arrihet të prodhohet një CAPTCHA"}. +{"Unauthorized","E paautorizuar"}. +{"Unexpected action","Veprim i papritur"}. +{"Unregister an XMPP account","Çregjistroni një llogari XMPP"}. +{"Unregister","Çregjistrohuni"}. +{"Unsupported version","Version i pambuluar"}. +{"Updating the vCard is not supported by the vCard storage backend","Përditësimi i vCard-it nuk mbulohet nga mekanizmi i depozitimit të vCard-ve"}. +{"User already exists","Ka tashmë një përdorues të tillë"}. +{"User JID","JID përdoruesi"}. +{"User (jid)","Përdorues (jid)"}. +{"User Management","Administrim Përdoruesish"}. +{"User removed","Përdoruesi u hoq"}. +{"User session not found","S’u gjet sesion përdoruesi"}. +{"User session terminated","Sesioni i përdoruesit përfundoi"}. +{"Username:","Emër përdoruesi:"}. +{"User","Përdorues"}. +{"Users Last Activity","Veprimtaria e Fundit Nga Përdorues"}. +{"Users","Përdorues"}. +{"Virtual Hosts","Streha Virtuale"}. +{"Visitor","Vizitor"}. +{"Wednesday","E mërkurë"}. +{"When a new subscription is processed","Kur përpunohet një pajtim i ri"}. +{"Whether to allow subscriptions","Nëse duhen lejuar apo jo pajtime"}. +{"Who can send private messages","Cilët mund të dërgojnë mesazhe private"}. +{"Wrong parameters in the web formulary","Parametër i gabuar në formular web"}. +{"XMPP Account Registration","Regjistrim Llogarish XMPP"}. +{"XMPP Domains","Përkatësi XMPP"}. +{"You are not allowed to send private messages","S’keni leje të dërgoni mesazhe private"}. +{"You are not joined to the channel","S’keni hyrë te kanali"}. +{"You have been banned from this room","Jeni dëbuar prej kësaj dhome"}. +{"You have joined too many conferences","Keni hyrë në shumë konferenca"}. +{"Your XMPP account was successfully registered.","Llogaria juaj XMPP u regjistrua me sukses."}. +{"Your XMPP account was successfully unregistered.","Llogaria juaj XMPP u çregjistrua me sukses."}. +{"You're not allowed to create nodes","S’keni leje të krijoni nyja"}. diff --git a/priv/msgs/sv.msg b/priv/msgs/sv.msg index 02fa18b4a..8e454464e 100644 --- a/priv/msgs/sv.msg +++ b/priv/msgs/sv.msg @@ -1,19 +1,18 @@ -{"Access Configuration","Åtkomstkonfiguration"}. -{"Access Control List Configuration","Konfiguera ACL"}. -{"Access control lists","ACL"}. -{"Access Control Lists","ACL"}. -{"Access denied by service policy","Åtkomst nekad enligt lokal policy"}. -{"Access rules","Åtkomstregler"}. -{"Access Rules","Åtkomstregler"}. -{"Action on user","Handling mot användare"}. -{"Add Jabber ID","Lägg till Jabber ID"}. -{"Add New","Lägg till ny"}. -{"Add User","Lägg till användare"}. -{"Administration","Administration"}. -{"Administration of ","Administration av "}. -{"Administrator privileges required","Administrationsprivilegier krävs"}. +%% Generated automatically +%% DO NOT EDIT: run `make translations` instead +%% To improve translations please read: +%% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ + +{" has set the subject to: "," har satt ämnet till: "}. {"A friendly name for the node","Ett vänligt namn for noden"}. +{"Access denied by service policy","Åtkomst nekad enligt lokal policy"}. +{"Action on user","Handling mot användare"}. +{"Add User","Lägg till användare"}. +{"Administration of ","Administration av "}. +{"Administration","Administration"}. +{"Administrator privileges required","Administrationsprivilegier krävs"}. {"All activity","All aktivitet"}. +{"All Users","Alla användare"}. {"Allow this Jabber ID to subscribe to this pubsub node?","Tillåt denna Jabber ID att prenumerera på denna pubsub node"}. {"Allow users to change the subject","Tillåt användare att byta ämne"}. {"Allow users to query other users","Tillåt användare att söka efter andra användare"}. @@ -21,15 +20,12 @@ {"Allow users to send private messages","Tillåt användare att skicka privata meddelanden"}. {"Allow visitors to change nickname","Tillåt gäster att kunna ändra smeknamn"}. {"Allow visitors to send status text in presence updates","Tillåt gäster att skicka statustext som uppdatering"}. -{"All Users","Alla användare"}. {"Announcements","Meddelanden"}. -{"anyone","Vemsomhelst"}. {"April","April"}. {"August","Augusti"}. {"Backup Management","Hantera säkerhetskopior"}. -{"Backup of ","Backup av"}. -{"Backup","Säkerhetskopiera"}. {"Backup to File at ","Säkerhetskopiera till fil på "}. +{"Backup","Säkerhetskopiera"}. {"Bad format","Dåligt format"}. {"Birthday","Födelsedag"}. {"Change Password","Ändra lösenord"}. @@ -37,83 +33,55 @@ {"Chatroom configuration modified","Chattrum konfiguration modifierad"}. {"Chatrooms","Chattrum"}. {"Choose a username and password to register with this server","Välj ett användarnamn och lösenord för att registrera mot denna server"}. -{"Choose modules to stop","Välj vilka moduler som skall stoppas"}. {"Choose storage type of tables","Välj lagringstyp för tabeller"}. -{"Choose whether to approve this entity's subscription.","Välj om du vill godkänna hela denna prenumertion."}. {"City","Stad"}. {"Commands","Kommandon"}. {"Conference room does not exist","Rummet finns inte"}. -{"Configuration","Konfiguration"}. {"Configuration of room ~s","Konfiguration för ~s"}. -{"Connected Resources:","Anslutna resurser:"}. -{"Connections parameters","Uppkopplingsparametrar"}. +{"Configuration","Konfiguration"}. {"Country","Land"}. -{"CPU Time:","CPU tid"}. -{"Database","Databas"}. -{"Database Tables at ","Databas tabell pa"}. {"Database Tables Configuration at ","Databastabellers konfiguration"}. +{"Database","Databas"}. {"December","December"}. {"Default users as participants","Gör om användare till deltagare"}. {"Delete message of the day on all hosts","Ta bort dagens meddelande på alla värdar"}. {"Delete message of the day","Ta bort dagens meddelande"}. -{"Delete Selected","Tabort valda"}. -{"Delete","Ta bort"}. {"Delete User","Ta bort användare"}. {"Deliver event notifications","Skicka eventnotifikation"}. {"Deliver payloads with event notifications","Skicka innehåll tillsammans med notifikationer"}. -{"Description:","Beskrivning:"}. {"Disc only copy","Endast diskkopia"}. -{"Displayed Groups:","Visade grupper:"}. {"Dump Backup to Text File at ","Dumpa säkerhetskopia till textfil på "}. {"Dump to Text File","Dumpa till textfil"}. {"Edit Properties","Redigera egenskaper"}. -{"ejabberd IRC module","ejabberd IRC-modul"}. {"ejabberd MUC module","ejabberd MUC modul"}. {"ejabberd Publish-Subscribe module","ejabberd publikprenumerations modul"}. {"ejabberd SOCKS5 Bytestreams module","ejabberd SOCKS5 Bytestrem modul"}. {"ejabberd vCard module","ejabberd vCard-modul"}. {"ejabberd Web Admin","ejabberd Web Admin"}. -{"Elements","Elements"}. {"Email","Email"}. {"Enable logging","Möjliggör login"}. -{"Encoding for server ~b","Encoding för server ~b"}. {"End User Session","Avsluta användarsession"}. -{"Enter list of {Module, [Options]}","Skriv in en lista av {Module, [Options]}"}. {"Enter nickname you want to register","Skriv in smeknamnet du vill registrera"}. {"Enter path to backup file","Skriv in sökväg till fil för säkerhetskopia"}. {"Enter path to jabberd14 spool dir","Skriv in sökväg till spoolkatalog från jabberd14"}. {"Enter path to jabberd14 spool file","Skriv in sökväg till spoolfil från jabberd14"}. {"Enter path to text file","Skriv in sökväg till textfil"}. {"Enter the text you see","Skriv in sökväg till textfil"}. -{"Enter username, encodings, ports and passwords you wish to use for connecting to IRC servers","Skriv in användarnamn och textkodning du vill använda för att ansluta till IRC-servrar"}. -{"Erlang Jabber Server","Erlang Jabber Server"}. -{"Error","Fel"}. -{"Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef.net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}].","Exempel: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef.net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]."}. {"Export data of all users in the server to PIEFXIS files (XEP-0227):","Exportera data av alla användare i servern till en PIEFXIS fil (XEP-0227):"}. {"Export data of users in a host to PIEFXIS files (XEP-0227):","Exportera data av användare i en host till PIEFXIS fil (XEP-0227):"}. {"Family Name","Efternamn"}. {"February","Februari"}. -{"Fill in fields to search for any matching Jabber User","Fyll i fält för att söka efter jabberanvändare"}. -{"Fill in the form to search for any matching Jabber User (Add * to the end of field to match substring)","Fyll i formuläret för att söka efter en användare (lägg till * på slutet av fältet för att hitta alla som börjar så)"}. {"Friday","Fredag"}. -{"From","Från"}. -{"From ~s","Från ~s"}. {"Full Name","Fullständigt namn"}. {"Get Number of Online Users","Hämta antal inloggade användare"}. {"Get Number of Registered Users","Hämta antal registrerade användare"}. {"Get User Last Login Time","Hämta användarens senast inloggade tid"}. -{"Get User Password","Hämta användarlösenord"}. {"Get User Statistics","Hämta användarstatistik"}. -{"Group ","Grupp "}. -{"Groups","Grupper"}. {"has been banned","har blivit bannad"}. -{"has been kicked because of an affiliation change","har blivit kickad p.g.a en ändring av tillhörighet"}. {"has been kicked because of a system shutdown","har blivit kickad p.g.a en systemnerstängning"}. +{"has been kicked because of an affiliation change","har blivit kickad p.g.a en ändring av tillhörighet"}. {"has been kicked because the room has been changed to members-only","har blivit kickad p.g.a att rummet har ändrats till endast användare"}. {"has been kicked","har blivit kickad"}. -{" has set the subject to: "," har satt ämnet till: "}. -{"Host","Server"}. -{"If you want to specify different ports, passwords, encodings for IRC servers, fill this list with values in format '{\"irc server\", \"encoding\", port, \"password\"}'. By default this service use \"~s\" encoding, port ~p, empty password.","Om du vill specifiera textkodning för IRC-servrar, fyll i listan med värden i formatet '{\"irc server\", \"encoding\", port, \"password\"}'. Som standard används \"~s\", port ~p, no password."}. {"Import Directory","Importera katalog"}. {"Import File","Importera fil"}. {"Import user data from jabberd14 spool file:","Importera användare från jabberd14 Spool filer"}. @@ -124,27 +92,13 @@ {"Import Users From jabberd14 Spool Files","Importera användare från jabberd14 Spool filer"}. {"Improper message type","Felaktig medelandetyp"}. {"Incorrect password","Fel lösenord"}. -{"Invalid affiliation: ~s","Ogiltlig rang: ~s"}. -{"Invalid role: ~s","Ogiltlig roll: ~s"}. {"IP addresses","IP adresser"}. -{"IP","IP"}. -{"IRC channel (don't put the first #)","IRC kanal (skriv inte första #)"}. -{"IRC server","IRC-användarnamn"}. -{"IRC settings","IRC Inställningar"}. -{"IRC Transport","IRC transport"}. -{"IRC username","IRC-användarnamn"}. -{"IRC Username","IRC-användarnamn"}. {"is now known as","är känd som"}. -{"It is not allowed to send private messages","Det ar inte tillåtet att skicka privata meddelanden"}. {"It is not allowed to send private messages of type \"groupchat\"","Det är inte tillåtet att skicka privata medelanden med typen \"groupchat\""}. {"It is not allowed to send private messages to the conference","Det är inte tillåtet att skicka privata medelanden till den här konferensen"}. {"Jabber ID","Jabber ID"}. -{"Jabber ID ~s is invalid","Otillåtet Jabber ID ~s"}. {"January","Januari"}. -{"Join IRC channel","Lägg till IRC kanal"}. {"joins the room","joinar rummet"}. -{"Join the IRC channel here.","Lägg till IRC kanal här."}. -{"Join the IRC channel in this Jabber ID: ~s","Lägg till IRC kanal till detta Jabber ID: ~s"}. {"July","Juli"}. {"June","Juni"}. {"Last Activity","Senast aktivitet"}. @@ -152,10 +106,6 @@ {"Last month","Senaste månaden"}. {"Last year","Senaste året"}. {"leaves the room","lämnar rummet"}. -{"Listened Ports at ","Lyssnande portar på "}. -{"Listened Ports","Lyssnarport"}. -{"List of modules to start","Lista av moduler som skall startas"}. -{"Low level update script","Uppdaterade laglevel skript"}. {"Make participants list public","Gör deltagarlistan publik"}. {"Make room members-only","Gör om rummet till endast medlemmar"}. {"Make room moderated","Gör rummet modererat"}. @@ -163,23 +113,14 @@ {"Make room persistent","Gör rummet permanent"}. {"Make room public searchable","Gör rummet publikt sökbart"}. {"March","Mars"}. -{"Maximum Number of Occupants","Maximalt antal av användare"}. -{"Max # of items to persist","Högsta antal dataposter som sparas"}. {"Max payload size in bytes","Högsta innehållsstorlek i bytes"}. +{"Maximum Number of Occupants","Maximalt antal av användare"}. {"May","Maj"}. {"Membership is required to enter this room","Du måste vara medlem för att komma in i det här rummet"}. -{"Members:","Medlemmar:"}. -{"Memory","Minne"}. {"Message body","Meddelande kropp"}. {"Middle Name","Mellannamn"}. {"Moderator privileges required","Moderatorprivilegier krävs"}. -{"moderators only","endast moderatorer"}. -{"Modified modules","Uppdaterade moduler"}. -{"Module","Modul"}. -{"Modules at ","Moduler på"}. -{"Modules","Moduler"}. {"Monday","Måndag"}. -{"Name:","Namn:"}. {"Name","Namn"}. {"Never","Aldrig"}. {"Nickname Registration at ","Registrera smeknamn på "}. @@ -187,13 +128,11 @@ {"Nickname","Smeknamn"}. {"No body provided for announce message","Ingen kropp behövs för dessa meddelanden"}. {"No Data","Ingen data"}. +{"No limit","Ingen gräns"}. {"Node ID","Node ID"}. -{"Node ","Nod "}. {"Node not found","Noden finns inte"}. {"Nodes","Noder"}. -{"No limit","Ingen gräns"}. {"None","Inga"}. -{"No resource provided","Ingen resurs angiven"}. {"Not Found","Noden finns inte"}. {"Notify subscribers when items are removed from the node","Meddela prenumeranter när dataposter tas bort från noden"}. {"Notify subscribers when the node configuration changes","Meddela prenumeranter när nodens konfiguration ändras"}. @@ -203,148 +142,92 @@ {"Number of online users","Antal inloggade användare"}. {"Number of registered users","Antal registrerade användare"}. {"October","Oktober"}. -{"Offline Messages:","Offline meddelanden:"}. -{"Offline Messages","Offline meddelanden"}. {"OK","OK"}. -{"Online","Ansluten"}. {"Online Users","Anslutna användare"}. -{"Online Users:","Inloggade användare"}. +{"Online","Ansluten"}. {"Only deliver notifications to available users","Skicka notifikationer bara till uppkopplade användare"}. {"Only moderators and participants are allowed to change the subject in this room","Endast moderatorer och deltagare har tillåtelse att ändra ämnet i det här rummet"}. {"Only occupants are allowed to send messages to the conference","Utomstående får inte skicka medelanden till den här konferensen"}. {"Only occupants are allowed to send queries to the conference","Utomstående får inte skicka iq-queries till den här konferensen"}. {"Only service administrators are allowed to send service messages","Endast administratörer får skicka tjänstmeddelanden"}. -{"Options","Parametrar"}. {"Organization Name","Organisationsnamn"}. {"Organization Unit","Organisationsenhet"}. {"Outgoing s2s Connections","Utgaende s2s anslutning"}. -{"Outgoing s2s Connections:","Utgående s2s anslutning"}. -{"Outgoing s2s Servers:","Utgående s2s server"}. {"Owner privileges required","Ägarprivilegier krävs"}. -{"Packet","Paket"}. -{"Password ~b","Lösenord ~b"}. -{"Password:","Lösenord:"}. -{"Password","Lösenord"}. {"Password Verification","Lösenordsverifikation"}. +{"Password","Lösenord"}. +{"Password:","Lösenord:"}. {"Path to Dir","Sökväg till katalog"}. {"Path to File","Sökväg till fil"}. -{"Pending","Ännu inte godkända"}. {"Period: ","Period: "}. {"Persist items to storage","Spara dataposter permanent"}. {"Ping","Ping"}. {"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Kom ihåg att dessa inställningar endast tar backup pa builtin Mnesias databas. Om du använder ODBC modul så måste du ta backup på SQLs databas enskilt"}. {"Pong","Pong"}. -{"Port ~b","Port ~b"}. -{"Port","Port"}. {"Present real Jabber IDs to","Nuvarande äkta Jabber IDs till"}. {"private, ","privat, "}. -{"Protocol","Protocol"}. {"Publish-Subscribe","Publikprenumeration"}. {"PubSub subscriber request","Pubsub prenumerationsforfrågan"}. {"Queries to the conference members are not allowed in this room","Det är förbjudet att skicka iq-queries till konferensdeltagare"}. {"RAM and disc copy","RAM- och diskkopia"}. {"RAM copy","RAM-kopia"}. -{"Raw","Ra"}. {"Really delete message of the day?","Verkligen ta bort dagens meddelanden?"}. {"Recipient is not in the conference room","Mottagaren finns inte i rummet"}. -{"Registered Users:","Registrerade användare"}. -{"Registered Users","Registrerade användare"}. -{"Registration in mod_irc for ","mod_irc-registrering för "}. {"Remote copy","Sparas inte lokalt"}. -{"Remove","Ta bort"}. {"Remove User","Ta bort användare"}. {"Replaced by new connection","Ersatt av ny anslutning"}. {"Resources","Resurser"}. -{"Restart","Omstart"}. {"Restart Service","Starta om servicen"}. -{"Restore","Återställ"}. {"Restore Backup from File at ","Återställ säkerhetskopia från fil på "}. {"Restore binary backup after next ejabberd restart (requires less memory):","återställ den binära backupen efter nästa ejabberd omstart"}. {"Restore binary backup immediately:","återställ den binära backupen omedelbart"}. {"Restore plain text backup immediately:","återställ textbackup omedelbart"}. +{"Restore","Återställ"}. {"Room Configuration","Rumkonfiguration"}. {"Room creation is denied by service policy","Skapandet av rum är förbjudet enligt lokal policy"}. {"Room Occupants","Antal besökare"}. {"Room title","Rumstitel"}. {"Roster groups allowed to subscribe","Rostergrupper tillåts att prenumerera"}. -{"Roster","Kontaktlista"}. -{"Roster of ","Kontaktlista för "}. {"Roster size","Roster storlek"}. -{"RPC Call Error","RPC Uppringningserror"}. {"Running Nodes","Körande noder"}. -{"~s access rule configuration","Åtkomstregelkonfiguration för ~s"}. {"Saturday","Lördag"}. -{"Script check","Skript kollat"}. {"Search Results for ","Sökresultat för"}. {"Search users in ","Sök efter användare på "}. {"Send announcement to all online users on all hosts","Sänd meddelanden till alla inloggade användare på alla värdar"}. {"Send announcement to all online users","Sänd meddelanden till alla inloggade användare"}. -{"Send announcement to all users on all hosts","Sänd meddelanden till alla användare på alla värdar"}. {"Send announcement to all users","Sänd meddelanden till alla användare"}. {"September","September"}. -{"Server ~b","Server ~b"}. {"Set message of the day and send to online users","Sätt dagens status meddelande och skicka till alla användare"}. {"Set message of the day on all hosts and send to online users","Sätt dagens status meddelande pa alla värdar och skicka till alla användare"}. {"Shared Roster Groups","Delade Rostergrupper"}. {"Show Integral Table","Visa kumulativ tabell"}. {"Show Ordinary Table","Visa normal tabell"}. {"Shut Down Service","Stäng ner servicen"}. -{"~s invites you to the room ~s","~s bjöd in dig till rummet ~s"}. {"Specify the access model","Specificera accessmodellen"}. {"Specify the publisher model","Ange publiceringsmodell"}. -{"~s's Offline Messages Queue","~s's offline meddelandekö"}. -{"Start Modules at ","Starta moduler på "}. -{"Start Modules","Starta moduler"}. -{"Start","Starta"}. -{"Statistics of ~p","Statistik på ~p"}. -{"Statistics","Statistik"}. -{"Stop Modules at ","Stoppa moduler på "}. -{"Stop Modules","Stanna moduler"}. {"Stopped Nodes","Stannade noder"}. -{"Stop","Stoppa"}. -{"Storage Type","Lagringstyp"}. {"Store binary backup:","Lagra den binära backupen"}. {"Store plain text backup:","Lagra textbackup"}. {"Subject","Ämne"}. -{"Submit","Skicka"}. {"Submitted","Skicka in"}. {"Subscriber Address","Prenumerationsadress"}. -{"Subscription","Prenumeration"}. {"Sunday","Söndag"}. {"That nickname is registered by another person","Smeknamnet är reserverat"}. {"The CAPTCHA is valid.","Din CAPTCHA är godkänd."}. {"the password is","Lösenordet är"}. -{"This participant is kicked from the room because he sent an error message","Deltagaren har blivit kickad fran rummet p.g.a att han skickade ett errormeddelande"}. -{"This participant is kicked from the room because he sent an error message to another participant","Deltagaren har blivit kickad från rummet p.g.a att han skickade ett errormeddelande till en annan deltagare"}. -{"This participant is kicked from the room because he sent an error presence","Denna deltagaren är kickad från rummet p.g.a att han skickade en errorstatus"}. {"This room is not anonymous","Detta rum är inte anonymt"}. {"Thursday","Torsdag"}. {"Time delay","Tidsförsening"}. -{"Time","Tid"}. -{"To ~s","Till ~s"}. -{"To","Till"}. {"Traffic rate limit is exceeded","Trafikgränsen har överstigits"}. -{"Transactions Aborted:","Transaktioner borttagna"}. -{"Transactions Committed:","Transaktioner kommittade"}. -{"Transactions Logged:","Transaktioner loggade "}. -{"Transactions Restarted:","Transaktioner omstartade"}. {"Tuesday","Tisdag"}. {"Unauthorized","Ej auktoriserad"}. {"Update message of the day (don't send)","Uppdatera dagens status meddelande (skicka inte)"}. {"Update message of the day on all hosts (don't send)","Uppdatera dagens status meddelande på alla värdar (skicka inte)"}. -{"Update plan","Uppdateringsplan"}. -{"Update script","Uppdatera skript"}. -{"Update ","Uppdatera"}. -{"Update","Uppdatera"}. -{"Uptime:","Tid upp"}. -{"Use of STARTTLS required","Du måste använda STARTTLS"}. -{"User ","Användare "}. -{"User","Användarnamn"}. {"User Management","Användarmanagement"}. -{"Users","Användare"}. +{"User","Användarnamn"}. {"Users are not allowed to register accounts so quickly","Det är inte tillåtet för användare att skapa konton så fort"}. {"Users Last Activity","Användarens senaste aktivitet"}. -{"Validate","Validera"}. +{"Users","Användare"}. {"vCard User Search","vCard användare sök"}. {"Virtual Hosts","Virtuella servrar"}. {"Visitors are not allowed to change their nicknames in this room","Det är inte tillåtet for gäster att ändra sina smeknamn i detta rummet"}. @@ -354,8 +237,6 @@ {"Whether to allow subscriptions","Tillåta prenumerationer?"}. {"You have been banned from this room","Du har blivit bannlyst från det här rummet"}. {"You must fill in field \"Nickname\" in the form","Du måste fylla i fält \"smeknamn\" i formen"}. -{"You need an x:data capable client to configure mod_irc settings","Du behöer en klient som stöjer x:data för att konfigurera mod_irc"}. -{"You need an x:data capable client to configure room","Du behöver en klient som stödjer x:data för att konfiguera detta rum"}. {"You need an x:data capable client to search","Du behöver en klient som stödjer x:data, för att kunna söka"}. {"Your contact offline message queue is full. The message has been discarded.","Din kontaktkö for offlinekontakter ar full"}. -{"Your messages to ~s are being blocked. To unblock them, visit ~s","Dina meddelanden till ~s är blockerade. För att avblockera dem, gå till ~s"}. +{"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","Dina meddelanden till ~s är blockerade. För att avblockera dem, gå till ~s"}. diff --git a/priv/msgs/sv.po b/priv/msgs/sv.po deleted file mode 100644 index 7fa7d6c06..000000000 --- a/priv/msgs/sv.po +++ /dev/null @@ -1,1862 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: 2.1.0-alpha\n" -"Last-Translator: Gustaf Alströmer\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Language: Swedish (svenska)\n" -"X-Additional-Translator: Thore Alstromer\n" -"X-Additional-Translator: Heysan\n" -"X-Additional-Translator: Magnus Henoch\n" -"X-Additional-Translator: Jonas Ådahl\n" - -#: ejabberd_c2s.erl:424 ejabberd_c2s.erl:727 -msgid "Use of STARTTLS required" -msgstr "Du måste använda STARTTLS" - -#: ejabberd_c2s.erl:503 -msgid "No resource provided" -msgstr "Ingen resurs angiven" - -#: ejabberd_c2s.erl:1197 -msgid "Replaced by new connection" -msgstr "Ersatt av ny anslutning" - -#: ejabberd_c2s.erl:1885 -msgid "Your active privacy list has denied the routing of this stanza." -msgstr "" - -#: ejabberd_captcha.erl:96 ejabberd_captcha.erl:152 ejabberd_captcha.erl:178 -msgid "Enter the text you see" -msgstr "Skriv in sökväg till textfil" - -#: ejabberd_captcha.erl:101 -msgid "Your messages to ~s are being blocked. To unblock them, visit ~s" -msgstr "" -"Dina meddelanden till ~s är blockerade. För att avblockera dem, gå till ~s" - -#: ejabberd_captcha.erl:134 -msgid "If you don't see the CAPTCHA image here, visit the web page." -msgstr "" - -#: ejabberd_captcha.erl:146 -msgid "CAPTCHA web page" -msgstr "" - -#: ejabberd_captcha.erl:307 -msgid "The CAPTCHA is valid." -msgstr "Din CAPTCHA är godkänd." - -#: mod_adhoc.erl:95 mod_adhoc.erl:125 mod_adhoc.erl:143 mod_adhoc.erl:161 -msgid "Commands" -msgstr "Kommandon" - -#: mod_adhoc.erl:149 mod_adhoc.erl:243 -msgid "Ping" -msgstr "Ping" - -#: mod_adhoc.erl:260 -msgid "Pong" -msgstr "Pong" - -#: mod_announce.erl:507 mod_announce_odbc.erl:499 -msgid "Really delete message of the day?" -msgstr "Verkligen ta bort dagens meddelanden?" - -#: mod_announce.erl:515 mod_announce_odbc.erl:507 mod_configure.erl:1083 -#: mod_configure.erl:1128 -msgid "Subject" -msgstr "Ämne" - -#: mod_announce.erl:520 mod_announce_odbc.erl:512 mod_configure.erl:1088 -#: mod_configure.erl:1133 -msgid "Message body" -msgstr "Meddelande kropp" - -#: mod_announce.erl:600 mod_announce_odbc.erl:592 -msgid "No body provided for announce message" -msgstr "Ingen kropp behövs för dessa meddelanden" - -#: mod_announce.erl:635 mod_announce_odbc.erl:627 -msgid "Announcements" -msgstr "Meddelanden" - -#: mod_announce.erl:637 mod_announce_odbc.erl:629 -msgid "Send announcement to all users" -msgstr "Sänd meddelanden till alla användare" - -#: mod_announce.erl:639 mod_announce_odbc.erl:631 -msgid "Send announcement to all users on all hosts" -msgstr "Sänd meddelanden till alla användare på alla värdar" - -#: mod_announce.erl:641 mod_announce_odbc.erl:633 -msgid "Send announcement to all online users" -msgstr "Sänd meddelanden till alla inloggade användare" - -#: mod_announce.erl:643 mod_announce_odbc.erl:635 mod_configure.erl:1078 -#: mod_configure.erl:1123 -msgid "Send announcement to all online users on all hosts" -msgstr "Sänd meddelanden till alla inloggade användare på alla värdar" - -#: mod_announce.erl:645 mod_announce_odbc.erl:637 -msgid "Set message of the day and send to online users" -msgstr "Sätt dagens status meddelande och skicka till alla användare" - -#: mod_announce.erl:647 mod_announce_odbc.erl:639 -msgid "Set message of the day on all hosts and send to online users" -msgstr "" -"Sätt dagens status meddelande pa alla värdar och skicka till alla användare" - -#: mod_announce.erl:649 mod_announce_odbc.erl:641 -msgid "Update message of the day (don't send)" -msgstr "Uppdatera dagens status meddelande (skicka inte)" - -#: mod_announce.erl:651 mod_announce_odbc.erl:643 -msgid "Update message of the day on all hosts (don't send)" -msgstr "Uppdatera dagens status meddelande på alla värdar (skicka inte)" - -#: mod_announce.erl:653 mod_announce_odbc.erl:645 -msgid "Delete message of the day" -msgstr "Ta bort dagens meddelande" - -#: mod_announce.erl:655 mod_announce_odbc.erl:647 -msgid "Delete message of the day on all hosts" -msgstr "Ta bort dagens meddelande på alla värdar" - -#: mod_configure.erl:114 mod_configure.erl:274 mod_configure.erl:296 -#: mod_configure.erl:498 -msgid "Configuration" -msgstr "Konfiguration" - -#: mod_configure.erl:125 mod_configure.erl:576 web/ejabberd_web_admin.erl:1936 -msgid "Database" -msgstr "Databas" - -#: mod_configure.erl:127 mod_configure.erl:595 -msgid "Start Modules" -msgstr "Starta moduler" - -#: mod_configure.erl:129 mod_configure.erl:596 -msgid "Stop Modules" -msgstr "Stanna moduler" - -#: mod_configure.erl:131 mod_configure.erl:604 web/ejabberd_web_admin.erl:1937 -msgid "Backup" -msgstr "Säkerhetskopiera" - -#: mod_configure.erl:133 mod_configure.erl:605 -msgid "Restore" -msgstr "Återställ" - -#: mod_configure.erl:135 mod_configure.erl:606 -msgid "Dump to Text File" -msgstr "Dumpa till textfil" - -#: mod_configure.erl:137 mod_configure.erl:615 -msgid "Import File" -msgstr "Importera fil" - -#: mod_configure.erl:139 mod_configure.erl:616 -msgid "Import Directory" -msgstr "Importera katalog" - -#: mod_configure.erl:141 mod_configure.erl:581 mod_configure.erl:1057 -msgid "Restart Service" -msgstr "Starta om servicen" - -#: mod_configure.erl:143 mod_configure.erl:582 mod_configure.erl:1102 -msgid "Shut Down Service" -msgstr "Stäng ner servicen" - -#: mod_configure.erl:145 mod_configure.erl:518 mod_configure.erl:1197 -#: web/ejabberd_web_admin.erl:1527 -msgid "Add User" -msgstr "Lägg till användare" - -#: mod_configure.erl:147 mod_configure.erl:519 mod_configure.erl:1219 -msgid "Delete User" -msgstr "Ta bort användare" - -#: mod_configure.erl:149 mod_configure.erl:520 mod_configure.erl:1231 -msgid "End User Session" -msgstr "Avsluta användarsession" - -#: mod_configure.erl:151 mod_configure.erl:521 mod_configure.erl:1243 -#: mod_configure.erl:1255 -msgid "Get User Password" -msgstr "Hämta användarlösenord" - -#: mod_configure.erl:153 mod_configure.erl:522 -msgid "Change User Password" -msgstr "Andra användarlösenord" - -#: mod_configure.erl:155 mod_configure.erl:523 mod_configure.erl:1272 -msgid "Get User Last Login Time" -msgstr "Hämta användarens senast inloggade tid" - -#: mod_configure.erl:157 mod_configure.erl:524 mod_configure.erl:1284 -msgid "Get User Statistics" -msgstr "Hämta användarstatistik" - -#: mod_configure.erl:159 mod_configure.erl:525 -msgid "Get Number of Registered Users" -msgstr "Hämta antal registrerade användare" - -#: mod_configure.erl:161 mod_configure.erl:526 -msgid "Get Number of Online Users" -msgstr "Hämta antal inloggade användare" - -#: mod_configure.erl:163 mod_configure.erl:509 web/ejabberd_web_admin.erl:831 -#: web/ejabberd_web_admin.erl:872 -msgid "Access Control Lists" -msgstr "ACL" - -#: mod_configure.erl:165 mod_configure.erl:510 web/ejabberd_web_admin.erl:940 -#: web/ejabberd_web_admin.erl:976 -msgid "Access Rules" -msgstr "Åtkomstregler" - -#: mod_configure.erl:297 mod_configure.erl:499 -msgid "User Management" -msgstr "Användarmanagement" - -#: mod_configure.erl:500 web/ejabberd_web_admin.erl:1058 -#: web/ejabberd_web_admin.erl:1462 -msgid "Online Users" -msgstr "Anslutna användare" - -#: mod_configure.erl:501 -msgid "All Users" -msgstr "Alla användare" - -#: mod_configure.erl:502 -msgid "Outgoing s2s Connections" -msgstr "Utgaende s2s anslutning" - -#: mod_configure.erl:503 web/ejabberd_web_admin.erl:1907 -msgid "Running Nodes" -msgstr "Körande noder" - -#: mod_configure.erl:504 web/ejabberd_web_admin.erl:1909 -msgid "Stopped Nodes" -msgstr "Stannade noder" - -#: mod_configure.erl:577 mod_configure.erl:587 web/ejabberd_web_admin.erl:1953 -msgid "Modules" -msgstr "Moduler" - -#: mod_configure.erl:578 -msgid "Backup Management" -msgstr "Hantera säkerhetskopior" - -#: mod_configure.erl:579 -msgid "Import Users From jabberd14 Spool Files" -msgstr "Importera användare från jabberd14 Spool filer" - -#: mod_configure.erl:699 -msgid "To ~s" -msgstr "Till ~s" - -#: mod_configure.erl:717 -msgid "From ~s" -msgstr "Från ~s" - -#: mod_configure.erl:913 -msgid "Database Tables Configuration at " -msgstr "Databastabellers konfiguration" - -#: mod_configure.erl:918 -msgid "Choose storage type of tables" -msgstr "Välj lagringstyp för tabeller" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Disc only copy" -msgstr "Endast diskkopia" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM and disc copy" -msgstr "RAM- och diskkopia" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM copy" -msgstr "RAM-kopia" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Remote copy" -msgstr "Sparas inte lokalt" - -#: mod_configure.erl:950 -msgid "Stop Modules at " -msgstr "Stoppa moduler på " - -#: mod_configure.erl:954 -msgid "Choose modules to stop" -msgstr "Välj vilka moduler som skall stoppas" - -#: mod_configure.erl:969 -msgid "Start Modules at " -msgstr "Starta moduler på " - -#: mod_configure.erl:973 -msgid "Enter list of {Module, [Options]}" -msgstr "Skriv in en lista av {Module, [Options]}" - -#: mod_configure.erl:974 -msgid "List of modules to start" -msgstr "Lista av moduler som skall startas" - -#: mod_configure.erl:983 -msgid "Backup to File at " -msgstr "Säkerhetskopiera till fil på " - -#: mod_configure.erl:987 mod_configure.erl:1001 -msgid "Enter path to backup file" -msgstr "Skriv in sökväg till fil för säkerhetskopia" - -#: mod_configure.erl:988 mod_configure.erl:1002 mod_configure.erl:1016 -#: mod_configure.erl:1030 -msgid "Path to File" -msgstr "Sökväg till fil" - -#: mod_configure.erl:997 -msgid "Restore Backup from File at " -msgstr "Återställ säkerhetskopia från fil på " - -#: mod_configure.erl:1011 -msgid "Dump Backup to Text File at " -msgstr "Dumpa säkerhetskopia till textfil på " - -#: mod_configure.erl:1015 -msgid "Enter path to text file" -msgstr "Skriv in sökväg till textfil" - -#: mod_configure.erl:1025 -msgid "Import User from File at " -msgstr "Importera användare från fil på " - -#: mod_configure.erl:1029 -msgid "Enter path to jabberd14 spool file" -msgstr "Skriv in sökväg till spoolfil från jabberd14" - -#: mod_configure.erl:1039 -msgid "Import Users from Dir at " -msgstr "Importera användare från katalog på " - -#: mod_configure.erl:1043 -msgid "Enter path to jabberd14 spool dir" -msgstr "Skriv in sökväg till spoolkatalog från jabberd14" - -#: mod_configure.erl:1044 -msgid "Path to Dir" -msgstr "Sökväg till katalog" - -#: mod_configure.erl:1060 mod_configure.erl:1105 -msgid "Time delay" -msgstr "Tidsförsening" - -#: mod_configure.erl:1143 -msgid "Access Control List Configuration" -msgstr "Konfiguera ACL" - -#: mod_configure.erl:1147 -msgid "Access control lists" -msgstr "ACL" - -#: mod_configure.erl:1171 -msgid "Access Configuration" -msgstr "Åtkomstkonfiguration" - -#: mod_configure.erl:1175 -msgid "Access rules" -msgstr "Åtkomstregler" - -#: mod_configure.erl:1200 mod_configure.erl:1222 mod_configure.erl:1234 -#: mod_configure.erl:1246 mod_configure.erl:1258 mod_configure.erl:1275 -#: mod_configure.erl:1287 mod_configure.erl:1650 mod_configure.erl:1700 -#: mod_configure.erl:1721 mod_roster.erl:943 mod_roster_odbc.erl:1060 -#: mod_vcard.erl:472 mod_vcard_ldap.erl:554 mod_vcard_odbc.erl:448 -msgid "Jabber ID" -msgstr "Jabber ID" - -#: mod_configure.erl:1205 mod_configure.erl:1263 mod_configure.erl:1651 -#: mod_configure.erl:1863 mod_muc/mod_muc_room.erl:3224 mod_register.erl:235 -#: web/ejabberd_web_admin.erl:1520 -msgid "Password" -msgstr "Lösenord" - -#: mod_configure.erl:1210 -msgid "Password Verification" -msgstr "Lösenordsverifikation" - -#: mod_configure.erl:1301 -msgid "Number of registered users" -msgstr "Antal registrerade användare" - -#: mod_configure.erl:1315 -msgid "Number of online users" -msgstr "Antal inloggade användare" - -#: mod_configure.erl:1682 web/ejabberd_web_admin.erl:1588 -#: web/ejabberd_web_admin.erl:1743 -msgid "Never" -msgstr "Aldrig" - -#: mod_configure.erl:1696 web/ejabberd_web_admin.erl:1601 -#: web/ejabberd_web_admin.erl:1756 -msgid "Online" -msgstr "Ansluten" - -#: mod_configure.erl:1701 -msgid "Last login" -msgstr "Senaste login" - -#: mod_configure.erl:1722 -msgid "Roster size" -msgstr "Roster storlek" - -#: mod_configure.erl:1723 -msgid "IP addresses" -msgstr "IP adresser" - -#: mod_configure.erl:1724 -msgid "Resources" -msgstr "Resurser" - -#: mod_configure.erl:1850 -msgid "Administration of " -msgstr "Administration av " - -#: mod_configure.erl:1853 -msgid "Action on user" -msgstr "Handling mot användare" - -#: mod_configure.erl:1857 -msgid "Edit Properties" -msgstr "Redigera egenskaper" - -#: mod_configure.erl:1860 web/ejabberd_web_admin.erl:1769 -msgid "Remove User" -msgstr "Ta bort användare" - -#: mod_irc/mod_irc.erl:201 mod_irc/mod_irc_odbc.erl:196 -#: mod_muc/mod_muc.erl:336 mod_muc/mod_muc_odbc.erl:342 -msgid "Access denied by service policy" -msgstr "Åtkomst nekad enligt lokal policy" - -#: mod_irc/mod_irc.erl:401 mod_irc/mod_irc_odbc.erl:398 -msgid "IRC Transport" -msgstr "IRC transport" - -#: mod_irc/mod_irc.erl:428 mod_irc/mod_irc_odbc.erl:425 -msgid "ejabberd IRC module" -msgstr "ejabberd IRC-modul" - -#: mod_irc/mod_irc.erl:559 mod_irc/mod_irc_odbc.erl:568 -msgid "You need an x:data capable client to configure mod_irc settings" -msgstr "Du behöer en klient som stöjer x:data för att konfigurera mod_irc" - -#: mod_irc/mod_irc.erl:566 mod_irc/mod_irc_odbc.erl:575 -msgid "Registration in mod_irc for " -msgstr "mod_irc-registrering för " - -#: mod_irc/mod_irc.erl:571 mod_irc/mod_irc_odbc.erl:580 -msgid "" -"Enter username, encodings, ports and passwords you wish to use for " -"connecting to IRC servers" -msgstr "" -"Skriv in användarnamn och textkodning du vill använda för att ansluta till " -"IRC-servrar" - -#: mod_irc/mod_irc.erl:576 mod_irc/mod_irc_odbc.erl:585 -msgid "IRC Username" -msgstr "IRC-användarnamn" - -#: mod_irc/mod_irc.erl:586 mod_irc/mod_irc_odbc.erl:595 -msgid "" -"If you want to specify different ports, passwords, encodings for IRC " -"servers, fill this list with values in format '{\"irc server\", \"encoding" -"\", port, \"password\"}'. By default this service use \"~s\" encoding, port " -"~p, empty password." -msgstr "" -"Om du vill specifiera textkodning för IRC-servrar, fyll i listan med värden " -"i formatet '{\"irc server\", \"encoding\", port, \"password\"}'. Som " -"standard används \"~s\", port ~p, no password." - -#: mod_irc/mod_irc.erl:598 mod_irc/mod_irc_odbc.erl:607 -msgid "" -"Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef." -"net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]." -msgstr "" -"Exempel: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef." -"net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]." - -#: mod_irc/mod_irc.erl:603 mod_irc/mod_irc_odbc.erl:612 -msgid "Connections parameters" -msgstr "Uppkopplingsparametrar" - -#: mod_irc/mod_irc.erl:728 mod_irc/mod_irc_odbc.erl:757 -msgid "Join IRC channel" -msgstr "Lägg till IRC kanal" - -#: mod_irc/mod_irc.erl:732 mod_irc/mod_irc_odbc.erl:761 -msgid "IRC channel (don't put the first #)" -msgstr "IRC kanal (skriv inte första #)" - -#: mod_irc/mod_irc.erl:737 mod_irc/mod_irc_odbc.erl:766 -msgid "IRC server" -msgstr "IRC-användarnamn" - -#: mod_irc/mod_irc.erl:770 mod_irc/mod_irc.erl:774 -#: mod_irc/mod_irc_odbc.erl:799 mod_irc/mod_irc_odbc.erl:803 -msgid "Join the IRC channel here." -msgstr "Lägg till IRC kanal här." - -#: mod_irc/mod_irc.erl:778 mod_irc/mod_irc_odbc.erl:807 -msgid "Join the IRC channel in this Jabber ID: ~s" -msgstr "Lägg till IRC kanal till detta Jabber ID: ~s" - -#: mod_irc/mod_irc.erl:863 mod_irc/mod_irc_odbc.erl:904 -msgid "IRC settings" -msgstr "IRC Inställningar" - -#: mod_irc/mod_irc.erl:868 mod_irc/mod_irc_odbc.erl:909 -#, fuzzy -msgid "" -"Enter username and encodings you wish to use for connecting to IRC servers. " -"Press 'Next' to get more fields to fill in. Press 'Complete' to save " -"settings." -msgstr "" -"Skriv in användarnamn och textkodning du vill använda för att ansluta till " -"IRC-servrar" - -#: mod_irc/mod_irc.erl:874 mod_irc/mod_irc_odbc.erl:915 -msgid "IRC username" -msgstr "IRC-användarnamn" - -#: mod_irc/mod_irc.erl:923 mod_irc/mod_irc_odbc.erl:964 -msgid "Password ~b" -msgstr "Lösenord ~b" - -#: mod_irc/mod_irc.erl:928 mod_irc/mod_irc_odbc.erl:969 -msgid "Port ~b" -msgstr "Port ~b" - -#: mod_irc/mod_irc.erl:933 mod_irc/mod_irc_odbc.erl:974 -msgid "Encoding for server ~b" -msgstr "Encoding för server ~b" - -#: mod_irc/mod_irc.erl:942 mod_irc/mod_irc_odbc.erl:983 -msgid "Server ~b" -msgstr "Server ~b" - -#: mod_muc/mod_muc.erl:449 mod_muc/mod_muc_odbc.erl:456 -msgid "Only service administrators are allowed to send service messages" -msgstr "Endast administratörer får skicka tjänstmeddelanden" - -#: mod_muc/mod_muc.erl:493 mod_muc/mod_muc_odbc.erl:500 -msgid "Room creation is denied by service policy" -msgstr "Skapandet av rum är förbjudet enligt lokal policy" - -#: mod_muc/mod_muc.erl:500 mod_muc/mod_muc_odbc.erl:507 -msgid "Conference room does not exist" -msgstr "Rummet finns inte" - -#: mod_muc/mod_muc.erl:582 mod_muc/mod_muc_odbc.erl:595 -msgid "Chatrooms" -msgstr "Chattrum" - -#: mod_muc/mod_muc.erl:712 mod_muc/mod_muc_odbc.erl:729 -#, fuzzy -msgid "You need a client that supports x:data to register the nickname" -msgstr "Du behöver en klient som stödjer x:data för att registrera smeknamn" - -#: mod_muc/mod_muc.erl:718 mod_muc/mod_muc_odbc.erl:735 -msgid "Nickname Registration at " -msgstr "Registrera smeknamn på " - -#: mod_muc/mod_muc.erl:722 mod_muc/mod_muc_odbc.erl:739 -msgid "Enter nickname you want to register" -msgstr "Skriv in smeknamnet du vill registrera" - -#: mod_muc/mod_muc.erl:723 mod_muc/mod_muc_odbc.erl:740 -#: mod_muc/mod_muc_room.erl:3810 mod_roster.erl:944 mod_roster_odbc.erl:1061 -#: mod_vcard.erl:364 mod_vcard.erl:477 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:453 -msgid "Nickname" -msgstr "Smeknamn" - -#: mod_muc/mod_muc.erl:762 mod_muc/mod_muc_odbc.erl:784 -#: mod_muc/mod_muc_room.erl:1064 mod_muc/mod_muc_room.erl:1787 -msgid "That nickname is registered by another person" -msgstr "Smeknamnet är reserverat" - -#: mod_muc/mod_muc.erl:788 mod_muc/mod_muc_odbc.erl:811 -msgid "You must fill in field \"Nickname\" in the form" -msgstr "Du måste fylla i fält \"smeknamn\" i formen" - -#: mod_muc/mod_muc.erl:808 mod_muc/mod_muc_odbc.erl:831 -msgid "ejabberd MUC module" -msgstr "ejabberd MUC modul" - -#: mod_muc/mod_muc_log.erl:374 mod_muc/mod_muc_log.erl:381 -msgid "Chatroom configuration modified" -msgstr "Chattrum konfiguration modifierad" - -#: mod_muc/mod_muc_log.erl:384 -msgid "joins the room" -msgstr "joinar rummet" - -#: mod_muc/mod_muc_log.erl:387 mod_muc/mod_muc_log.erl:390 -msgid "leaves the room" -msgstr "lämnar rummet" - -#: mod_muc/mod_muc_log.erl:393 mod_muc/mod_muc_log.erl:396 -msgid "has been banned" -msgstr "har blivit bannad" - -#: mod_muc/mod_muc_log.erl:399 mod_muc/mod_muc_log.erl:402 -msgid "has been kicked" -msgstr "har blivit kickad" - -#: mod_muc/mod_muc_log.erl:405 -msgid "has been kicked because of an affiliation change" -msgstr "har blivit kickad p.g.a en ändring av tillhörighet" - -#: mod_muc/mod_muc_log.erl:408 -msgid "has been kicked because the room has been changed to members-only" -msgstr "har blivit kickad p.g.a att rummet har ändrats till endast användare" - -#: mod_muc/mod_muc_log.erl:411 -msgid "has been kicked because of a system shutdown" -msgstr "har blivit kickad p.g.a en systemnerstängning" - -#: mod_muc/mod_muc_log.erl:414 -msgid "is now known as" -msgstr "är känd som" - -#: mod_muc/mod_muc_log.erl:417 mod_muc/mod_muc_log.erl:688 -#: mod_muc/mod_muc_room.erl:2393 -msgid " has set the subject to: " -msgstr " har satt ämnet till: " - -#: mod_muc/mod_muc_log.erl:452 -#, fuzzy -msgid "Chatroom is created" -msgstr "Chattrum" - -#: mod_muc/mod_muc_log.erl:453 -#, fuzzy -msgid "Chatroom is destroyed" -msgstr "Chattrum" - -#: mod_muc/mod_muc_log.erl:454 -#, fuzzy -msgid "Chatroom is started" -msgstr "Chattrum" - -#: mod_muc/mod_muc_log.erl:455 -#, fuzzy -msgid "Chatroom is stopped" -msgstr "Chattrum" - -#: mod_muc/mod_muc_log.erl:459 -msgid "Monday" -msgstr "Måndag" - -#: mod_muc/mod_muc_log.erl:460 -msgid "Tuesday" -msgstr "Tisdag" - -#: mod_muc/mod_muc_log.erl:461 -msgid "Wednesday" -msgstr "Onsdag" - -#: mod_muc/mod_muc_log.erl:462 -msgid "Thursday" -msgstr "Torsdag" - -#: mod_muc/mod_muc_log.erl:463 -msgid "Friday" -msgstr "Fredag" - -#: mod_muc/mod_muc_log.erl:464 -msgid "Saturday" -msgstr "Lördag" - -#: mod_muc/mod_muc_log.erl:465 -msgid "Sunday" -msgstr "Söndag" - -#: mod_muc/mod_muc_log.erl:469 -msgid "January" -msgstr "Januari" - -#: mod_muc/mod_muc_log.erl:470 -msgid "February" -msgstr "Februari" - -#: mod_muc/mod_muc_log.erl:471 -msgid "March" -msgstr "Mars" - -#: mod_muc/mod_muc_log.erl:472 -msgid "April" -msgstr "April" - -#: mod_muc/mod_muc_log.erl:473 -msgid "May" -msgstr "Maj" - -#: mod_muc/mod_muc_log.erl:474 -msgid "June" -msgstr "Juni" - -#: mod_muc/mod_muc_log.erl:475 -msgid "July" -msgstr "Juli" - -#: mod_muc/mod_muc_log.erl:476 -msgid "August" -msgstr "Augusti" - -#: mod_muc/mod_muc_log.erl:477 -msgid "September" -msgstr "September" - -#: mod_muc/mod_muc_log.erl:478 -msgid "October" -msgstr "Oktober" - -#: mod_muc/mod_muc_log.erl:479 -msgid "November" -msgstr "November" - -#: mod_muc/mod_muc_log.erl:480 -msgid "December" -msgstr "December" - -#: mod_muc/mod_muc_log.erl:750 -msgid "Room Configuration" -msgstr "Rumkonfiguration" - -#: mod_muc/mod_muc_log.erl:759 -msgid "Room Occupants" -msgstr "Antal besökare" - -#: mod_muc/mod_muc_room.erl:174 -msgid "Traffic rate limit is exceeded" -msgstr "Trafikgränsen har överstigits" - -#: mod_muc/mod_muc_room.erl:246 -msgid "" -"This participant is kicked from the room because he sent an error message" -msgstr "" -"Deltagaren har blivit kickad fran rummet p.g.a att han skickade ett " -"errormeddelande" - -#: mod_muc/mod_muc_room.erl:255 -msgid "It is not allowed to send private messages to the conference" -msgstr "" -"Det är inte tillåtet att skicka privata medelanden till den här konferensen" - -#: mod_muc/mod_muc_room.erl:332 -msgid "Please, wait for a while before sending new voice request" -msgstr "" - -#: mod_muc/mod_muc_room.erl:347 -msgid "Voice requests are disabled in this conference" -msgstr "" - -#: mod_muc/mod_muc_room.erl:364 -msgid "Failed to extract JID from your voice request approval" -msgstr "" - -#: mod_muc/mod_muc_room.erl:393 -msgid "Only moderators can approve voice requests" -msgstr "" - -#: mod_muc/mod_muc_room.erl:408 -msgid "Improper message type" -msgstr "Felaktig medelandetyp" - -#: mod_muc/mod_muc_room.erl:518 -msgid "" -"This participant is kicked from the room because he sent an error message to " -"another participant" -msgstr "" -"Deltagaren har blivit kickad från rummet p.g.a att han skickade ett " -"errormeddelande till en annan deltagare" - -#: mod_muc/mod_muc_room.erl:531 -msgid "It is not allowed to send private messages of type \"groupchat\"" -msgstr "" -"Det är inte tillåtet att skicka privata medelanden med typen \"groupchat\"" - -#: mod_muc/mod_muc_room.erl:543 mod_muc/mod_muc_room.erl:611 -msgid "Recipient is not in the conference room" -msgstr "Mottagaren finns inte i rummet" - -#: mod_muc/mod_muc_room.erl:564 mod_muc/mod_muc_room.erl:585 -msgid "It is not allowed to send private messages" -msgstr "Det ar inte tillåtet att skicka privata meddelanden" - -#: mod_muc/mod_muc_room.erl:576 mod_muc/mod_muc_room.erl:956 -#: mod_muc/mod_muc_room.erl:4040 -msgid "Only occupants are allowed to send messages to the conference" -msgstr "Utomstående får inte skicka medelanden till den här konferensen" - -#: mod_muc/mod_muc_room.erl:634 -msgid "Only occupants are allowed to send queries to the conference" -msgstr "Utomstående får inte skicka iq-queries till den här konferensen" - -#: mod_muc/mod_muc_room.erl:646 -msgid "Queries to the conference members are not allowed in this room" -msgstr "Det är förbjudet att skicka iq-queries till konferensdeltagare" - -#: mod_muc/mod_muc_room.erl:932 -msgid "" -"Only moderators and participants are allowed to change the subject in this " -"room" -msgstr "" -"Endast moderatorer och deltagare har tillåtelse att ändra ämnet i det här " -"rummet" - -#: mod_muc/mod_muc_room.erl:937 -#, fuzzy -msgid "Only moderators are allowed to change the subject in this room" -msgstr "Endast moderatorer får ändra ämnet i det här rummet" - -#: mod_muc/mod_muc_room.erl:947 -msgid "Visitors are not allowed to send messages to all occupants" -msgstr "Besökare får inte skicka medelande till alla" - -#: mod_muc/mod_muc_room.erl:1021 -msgid "" -"This participant is kicked from the room because he sent an error presence" -msgstr "" -"Denna deltagaren är kickad från rummet p.g.a att han skickade en errorstatus" - -#: mod_muc/mod_muc_room.erl:1040 -msgid "Visitors are not allowed to change their nicknames in this room" -msgstr "Det är inte tillåtet for gäster att ändra sina smeknamn i detta rummet" - -#: mod_muc/mod_muc_room.erl:1053 mod_muc/mod_muc_room.erl:1779 -#, fuzzy -msgid "That nickname is already in use by another occupant" -msgstr "Smeknamnet används redan" - -#: mod_muc/mod_muc_room.erl:1768 -msgid "You have been banned from this room" -msgstr "Du har blivit bannlyst från det här rummet" - -#: mod_muc/mod_muc_room.erl:1771 -msgid "Membership is required to enter this room" -msgstr "Du måste vara medlem för att komma in i det här rummet" - -#: mod_muc/mod_muc_room.erl:1807 -msgid "This room is not anonymous" -msgstr "Detta rum är inte anonymt" - -#: mod_muc/mod_muc_room.erl:1833 -#, fuzzy -msgid "A password is required to enter this room" -msgstr "Lösenord erfordras" - -#: mod_muc/mod_muc_room.erl:1855 mod_register.erl:246 -msgid "Too many CAPTCHA requests" -msgstr "" - -#: mod_muc/mod_muc_room.erl:1864 mod_register.erl:251 -#, fuzzy -msgid "Unable to generate a CAPTCHA" -msgstr "Kunde inte generera ett CAPTCHA" - -#: mod_muc/mod_muc_room.erl:1874 -msgid "Incorrect password" -msgstr "Fel lösenord" - -#: mod_muc/mod_muc_room.erl:2448 -msgid "Administrator privileges required" -msgstr "Administrationsprivilegier krävs" - -#: mod_muc/mod_muc_room.erl:2463 -msgid "Moderator privileges required" -msgstr "Moderatorprivilegier krävs" - -#: mod_muc/mod_muc_room.erl:2619 -msgid "Jabber ID ~s is invalid" -msgstr "Otillåtet Jabber ID ~s" - -#: mod_muc/mod_muc_room.erl:2633 -msgid "Nickname ~s does not exist in the room" -msgstr "Smeknamnet ~s existerar inte i det här rummet" - -#: mod_muc/mod_muc_room.erl:2659 mod_muc/mod_muc_room.erl:3049 -msgid "Invalid affiliation: ~s" -msgstr "Ogiltlig rang: ~s" - -#: mod_muc/mod_muc_room.erl:2713 -msgid "Invalid role: ~s" -msgstr "Ogiltlig roll: ~s" - -#: mod_muc/mod_muc_room.erl:3026 mod_muc/mod_muc_room.erl:3062 -msgid "Owner privileges required" -msgstr "Ägarprivilegier krävs" - -#: mod_muc/mod_muc_room.erl:3195 -msgid "Configuration of room ~s" -msgstr "Konfiguration för ~s" - -#: mod_muc/mod_muc_room.erl:3200 -msgid "Room title" -msgstr "Rumstitel" - -#: mod_muc/mod_muc_room.erl:3203 mod_muc/mod_muc_room.erl:3692 -#, fuzzy -msgid "Room description" -msgstr "Beskrivning:" - -#: mod_muc/mod_muc_room.erl:3210 -msgid "Make room persistent" -msgstr "Gör rummet permanent" - -#: mod_muc/mod_muc_room.erl:3215 -msgid "Make room public searchable" -msgstr "Gör rummet publikt sökbart" - -#: mod_muc/mod_muc_room.erl:3218 -msgid "Make participants list public" -msgstr "Gör deltagarlistan publik" - -#: mod_muc/mod_muc_room.erl:3221 -msgid "Make room password protected" -msgstr "Gör losenorden i rummet publika" - -#: mod_muc/mod_muc_room.erl:3232 -msgid "Maximum Number of Occupants" -msgstr "Maximalt antal av användare" - -#: mod_muc/mod_muc_room.erl:3239 -msgid "No limit" -msgstr "Ingen gräns" - -#: mod_muc/mod_muc_room.erl:3250 -msgid "Present real Jabber IDs to" -msgstr "Nuvarande äkta Jabber IDs till" - -#: mod_muc/mod_muc_room.erl:3258 mod_muc/mod_muc_room.erl:3292 -msgid "moderators only" -msgstr "endast moderatorer" - -#: mod_muc/mod_muc_room.erl:3260 mod_muc/mod_muc_room.erl:3294 -msgid "anyone" -msgstr "Vemsomhelst" - -#: mod_muc/mod_muc_room.erl:3262 -msgid "Make room members-only" -msgstr "Gör om rummet till endast medlemmar" - -#: mod_muc/mod_muc_room.erl:3265 -msgid "Make room moderated" -msgstr "Gör rummet modererat" - -#: mod_muc/mod_muc_room.erl:3268 -msgid "Default users as participants" -msgstr "Gör om användare till deltagare" - -#: mod_muc/mod_muc_room.erl:3271 -msgid "Allow users to change the subject" -msgstr "Tillåt användare att byta ämne" - -#: mod_muc/mod_muc_room.erl:3274 -msgid "Allow users to send private messages" -msgstr "Tillåt användare att skicka privata meddelanden" - -#: mod_muc/mod_muc_room.erl:3279 -#, fuzzy -msgid "Allow visitors to send private messages to" -msgstr "Tillåt användare att skicka privata meddelanden" - -#: mod_muc/mod_muc_room.erl:3290 -msgid "nobody" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3296 -msgid "Allow users to query other users" -msgstr "Tillåt användare att söka efter andra användare" - -#: mod_muc/mod_muc_room.erl:3299 -msgid "Allow users to send invites" -msgstr "Tillåt användare att skicka inbjudningar" - -#: mod_muc/mod_muc_room.erl:3302 -msgid "Allow visitors to send status text in presence updates" -msgstr "Tillåt gäster att skicka statustext som uppdatering" - -#: mod_muc/mod_muc_room.erl:3305 -msgid "Allow visitors to change nickname" -msgstr "Tillåt gäster att kunna ändra smeknamn" - -#: mod_muc/mod_muc_room.erl:3308 -#, fuzzy -msgid "Allow visitors to send voice requests" -msgstr "Tillåt användare att skicka inbjudningar" - -#: mod_muc/mod_muc_room.erl:3311 -msgid "Minimum interval between voice requests (in seconds)" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3317 -#, fuzzy -msgid "Make room CAPTCHA protected" -msgstr "Gör losenorden i rummet publika" - -#: mod_muc/mod_muc_room.erl:3322 -msgid "Exclude Jabber IDs from CAPTCHA challenge" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3329 -msgid "Enable logging" -msgstr "Möjliggör login" - -#: mod_muc/mod_muc_room.erl:3337 -msgid "You need an x:data capable client to configure room" -msgstr "Du behöver en klient som stödjer x:data för att konfiguera detta rum" - -#: mod_muc/mod_muc_room.erl:3694 -msgid "Number of occupants" -msgstr "Antal besökare" - -#: mod_muc/mod_muc_room.erl:3750 -msgid "private, " -msgstr "privat, " - -#: mod_muc/mod_muc_room.erl:3799 -msgid "Voice request" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3803 -msgid "Either approve or decline the voice request." -msgstr "" - -#: mod_muc/mod_muc_room.erl:3809 -#, fuzzy -msgid "User JID" -msgstr "Användare " - -#: mod_muc/mod_muc_room.erl:3811 -msgid "Grant voice to this person?" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3960 -msgid "~s invites you to the room ~s" -msgstr "~s bjöd in dig till rummet ~s" - -#: mod_muc/mod_muc_room.erl:3969 -msgid "the password is" -msgstr "Lösenordet är" - -#: mod_offline.erl:510 mod_offline_odbc.erl:352 -msgid "" -"Your contact offline message queue is full. The message has been discarded." -msgstr "Din kontaktkö for offlinekontakter ar full" - -#: mod_offline.erl:560 mod_offline_odbc.erl:408 -msgid "~s's Offline Messages Queue" -msgstr "~s's offline meddelandekö" - -#: mod_offline.erl:563 mod_offline_odbc.erl:411 mod_roster.erl:987 -#: mod_roster_odbc.erl:1104 mod_shared_roster.erl:901 -#: mod_shared_roster.erl:1010 mod_shared_roster_odbc.erl:930 -#: mod_shared_roster_odbc.erl:1039 web/ejabberd_web_admin.erl:833 -#: web/ejabberd_web_admin.erl:874 web/ejabberd_web_admin.erl:942 -#: web/ejabberd_web_admin.erl:978 web/ejabberd_web_admin.erl:1019 -#: web/ejabberd_web_admin.erl:1508 web/ejabberd_web_admin.erl:1760 -#: web/ejabberd_web_admin.erl:1931 web/ejabberd_web_admin.erl:1963 -#: web/ejabberd_web_admin.erl:2031 web/ejabberd_web_admin.erl:2135 -#: web/ejabberd_web_admin.erl:2160 web/ejabberd_web_admin.erl:2248 -msgid "Submitted" -msgstr "Skicka in" - -#: mod_offline.erl:571 -msgid "Time" -msgstr "Tid" - -#: mod_offline.erl:572 -msgid "From" -msgstr "Från" - -#: mod_offline.erl:573 -msgid "To" -msgstr "Till" - -#: mod_offline.erl:574 mod_offline_odbc.erl:419 -msgid "Packet" -msgstr "Paket" - -#: mod_offline.erl:587 mod_offline_odbc.erl:432 mod_shared_roster.erl:908 -#: mod_shared_roster_odbc.erl:937 web/ejabberd_web_admin.erl:882 -#: web/ejabberd_web_admin.erl:986 -msgid "Delete Selected" -msgstr "Tabort valda" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -msgid "Offline Messages:" -msgstr "Offline meddelanden:" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -#, fuzzy -msgid "Remove All Offline Messages" -msgstr "Offline meddelanden" - -#: mod_proxy65/mod_proxy65_service.erl:213 -msgid "ejabberd SOCKS5 Bytestreams module" -msgstr "ejabberd SOCKS5 Bytestrem modul" - -#: mod_pubsub/mod_pubsub.erl:1120 mod_pubsub/mod_pubsub_odbc.erl:929 -msgid "Publish-Subscribe" -msgstr "Publikprenumeration" - -#: mod_pubsub/mod_pubsub.erl:1214 mod_pubsub/mod_pubsub_odbc.erl:1025 -msgid "ejabberd Publish-Subscribe module" -msgstr "ejabberd publikprenumerations modul" - -#: mod_pubsub/mod_pubsub.erl:1497 mod_pubsub/mod_pubsub_odbc.erl:1312 -msgid "PubSub subscriber request" -msgstr "Pubsub prenumerationsforfrågan" - -#: mod_pubsub/mod_pubsub.erl:1499 mod_pubsub/mod_pubsub_odbc.erl:1314 -msgid "Choose whether to approve this entity's subscription." -msgstr "Välj om du vill godkänna hela denna prenumertion." - -#: mod_pubsub/mod_pubsub.erl:1505 mod_pubsub/mod_pubsub_odbc.erl:1320 -msgid "Node ID" -msgstr "Node ID" - -#: mod_pubsub/mod_pubsub.erl:1510 mod_pubsub/mod_pubsub_odbc.erl:1325 -msgid "Subscriber Address" -msgstr "Prenumerationsadress" - -#: mod_pubsub/mod_pubsub.erl:1516 mod_pubsub/mod_pubsub_odbc.erl:1331 -msgid "Allow this Jabber ID to subscribe to this pubsub node?" -msgstr "Tillåt denna Jabber ID att prenumerera på denna pubsub node" - -#: mod_pubsub/mod_pubsub.erl:3474 mod_pubsub/mod_pubsub_odbc.erl:3287 -msgid "Deliver payloads with event notifications" -msgstr "Skicka innehåll tillsammans med notifikationer" - -#: mod_pubsub/mod_pubsub.erl:3475 mod_pubsub/mod_pubsub_odbc.erl:3288 -msgid "Deliver event notifications" -msgstr "Skicka eventnotifikation" - -#: mod_pubsub/mod_pubsub.erl:3476 mod_pubsub/mod_pubsub_odbc.erl:3289 -msgid "Notify subscribers when the node configuration changes" -msgstr "Meddela prenumeranter när nodens konfiguration ändras" - -#: mod_pubsub/mod_pubsub.erl:3477 mod_pubsub/mod_pubsub_odbc.erl:3290 -msgid "Notify subscribers when the node is deleted" -msgstr "Meddela prenumeranter när noden tas bort" - -#: mod_pubsub/mod_pubsub.erl:3478 mod_pubsub/mod_pubsub_odbc.erl:3291 -msgid "Notify subscribers when items are removed from the node" -msgstr "Meddela prenumeranter när dataposter tas bort från noden" - -#: mod_pubsub/mod_pubsub.erl:3479 mod_pubsub/mod_pubsub_odbc.erl:3292 -msgid "Persist items to storage" -msgstr "Spara dataposter permanent" - -#: mod_pubsub/mod_pubsub.erl:3480 mod_pubsub/mod_pubsub_odbc.erl:3293 -msgid "A friendly name for the node" -msgstr "Ett vänligt namn for noden" - -#: mod_pubsub/mod_pubsub.erl:3481 mod_pubsub/mod_pubsub_odbc.erl:3294 -msgid "Max # of items to persist" -msgstr "Högsta antal dataposter som sparas" - -#: mod_pubsub/mod_pubsub.erl:3482 mod_pubsub/mod_pubsub_odbc.erl:3295 -msgid "Whether to allow subscriptions" -msgstr "Tillåta prenumerationer?" - -#: mod_pubsub/mod_pubsub.erl:3483 mod_pubsub/mod_pubsub_odbc.erl:3296 -msgid "Specify the access model" -msgstr "Specificera accessmodellen" - -#: mod_pubsub/mod_pubsub.erl:3486 mod_pubsub/mod_pubsub_odbc.erl:3299 -msgid "Roster groups allowed to subscribe" -msgstr "Rostergrupper tillåts att prenumerera" - -#: mod_pubsub/mod_pubsub.erl:3487 mod_pubsub/mod_pubsub_odbc.erl:3300 -msgid "Specify the publisher model" -msgstr "Ange publiceringsmodell" - -#: mod_pubsub/mod_pubsub.erl:3489 mod_pubsub/mod_pubsub_odbc.erl:3302 -msgid "Purge all items when the relevant publisher goes offline" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:3490 mod_pubsub/mod_pubsub_odbc.erl:3303 -#, fuzzy -msgid "Specify the event message type" -msgstr "Specificera accessmodellen" - -#: mod_pubsub/mod_pubsub.erl:3492 mod_pubsub/mod_pubsub_odbc.erl:3305 -msgid "Max payload size in bytes" -msgstr "Högsta innehållsstorlek i bytes" - -#: mod_pubsub/mod_pubsub.erl:3493 mod_pubsub/mod_pubsub_odbc.erl:3306 -msgid "When to send the last published item" -msgstr "När att skicka senast publicerade ämne" - -#: mod_pubsub/mod_pubsub.erl:3495 mod_pubsub/mod_pubsub_odbc.erl:3308 -msgid "Only deliver notifications to available users" -msgstr "Skicka notifikationer bara till uppkopplade användare" - -#: mod_pubsub/mod_pubsub.erl:3496 mod_pubsub/mod_pubsub_odbc.erl:3309 -msgid "The collections with which a node is affiliated" -msgstr "" - -#: mod_register.erl:193 -msgid "The CAPTCHA verification has failed" -msgstr "" - -#: mod_register.erl:220 -#, fuzzy -msgid "You need a client that supports x:data and CAPTCHA to register" -msgstr "Du behöver en klient som stödjer x:data för att registrera smeknamn" - -#: mod_register.erl:226 mod_register.erl:265 -msgid "Choose a username and password to register with this server" -msgstr "Välj ett användarnamn och lösenord för att registrera mot denna server" - -#: mod_register.erl:230 mod_vcard.erl:364 mod_vcard_odbc.erl:342 -#: web/ejabberd_web_admin.erl:1515 web/ejabberd_web_admin.erl:1572 -msgid "User" -msgstr "Användarnamn" - -#: mod_register.erl:316 mod_register.erl:361 -#, fuzzy -msgid "The password is too weak" -msgstr "Lösenordet är" - -#: mod_register.erl:365 -msgid "Users are not allowed to register accounts so quickly" -msgstr "Det är inte tillåtet för användare att skapa konton så fort" - -#: mod_roster.erl:938 mod_roster_odbc.erl:1055 web/ejabberd_web_admin.erl:1701 -#: web/ejabberd_web_admin.erl:1886 web/ejabberd_web_admin.erl:1897 -#: web/ejabberd_web_admin.erl:2219 -msgid "None" -msgstr "Inga" - -#: mod_roster.erl:945 mod_roster_odbc.erl:1062 -msgid "Subscription" -msgstr "Prenumeration" - -#: mod_roster.erl:946 mod_roster_odbc.erl:1063 -msgid "Pending" -msgstr "Ännu inte godkända" - -#: mod_roster.erl:947 mod_roster_odbc.erl:1064 -msgid "Groups" -msgstr "Grupper" - -#: mod_roster.erl:974 mod_roster_odbc.erl:1091 -msgid "Validate" -msgstr "Validera" - -#: mod_roster.erl:982 mod_roster_odbc.erl:1099 -msgid "Remove" -msgstr "Ta bort" - -#: mod_roster.erl:985 mod_roster_odbc.erl:1102 -msgid "Roster of " -msgstr "Kontaktlista för " - -#: mod_roster.erl:988 mod_roster_odbc.erl:1105 mod_shared_roster.erl:902 -#: mod_shared_roster.erl:1011 mod_shared_roster_odbc.erl:931 -#: mod_shared_roster_odbc.erl:1040 web/ejabberd_web_admin.erl:834 -#: web/ejabberd_web_admin.erl:875 web/ejabberd_web_admin.erl:943 -#: web/ejabberd_web_admin.erl:979 web/ejabberd_web_admin.erl:1020 -#: web/ejabberd_web_admin.erl:1509 web/ejabberd_web_admin.erl:1761 -#: web/ejabberd_web_admin.erl:1932 web/ejabberd_web_admin.erl:2136 -#: web/ejabberd_web_admin.erl:2161 -msgid "Bad format" -msgstr "Dåligt format" - -#: mod_roster.erl:995 mod_roster_odbc.erl:1112 -msgid "Add Jabber ID" -msgstr "Lägg till Jabber ID" - -#: mod_roster.erl:1094 mod_roster_odbc.erl:1211 -msgid "Roster" -msgstr "Kontaktlista" - -#: mod_shared_roster.erl:857 mod_shared_roster.erl:899 -#: mod_shared_roster.erl:1007 mod_shared_roster_odbc.erl:886 -#: mod_shared_roster_odbc.erl:928 mod_shared_roster_odbc.erl:1036 -msgid "Shared Roster Groups" -msgstr "Delade Rostergrupper" - -#: mod_shared_roster.erl:895 mod_shared_roster_odbc.erl:924 -#: web/ejabberd_web_admin.erl:1365 web/ejabberd_web_admin.erl:2461 -msgid "Add New" -msgstr "Lägg till ny" - -#: mod_shared_roster.erl:978 mod_shared_roster_odbc.erl:1007 -msgid "Name:" -msgstr "Namn:" - -#: mod_shared_roster.erl:983 mod_shared_roster_odbc.erl:1012 -msgid "Description:" -msgstr "Beskrivning:" - -#: mod_shared_roster.erl:991 mod_shared_roster_odbc.erl:1020 -msgid "Members:" -msgstr "Medlemmar:" - -#: mod_shared_roster.erl:999 mod_shared_roster_odbc.erl:1028 -msgid "Displayed Groups:" -msgstr "Visade grupper:" - -#: mod_shared_roster.erl:1008 mod_shared_roster_odbc.erl:1037 -msgid "Group " -msgstr "Grupp " - -#: mod_shared_roster.erl:1017 mod_shared_roster_odbc.erl:1046 -#: web/ejabberd_web_admin.erl:840 web/ejabberd_web_admin.erl:884 -#: web/ejabberd_web_admin.erl:949 web/ejabberd_web_admin.erl:1026 -#: web/ejabberd_web_admin.erl:2017 -msgid "Submit" -msgstr "Skicka" - -#: mod_vcard.erl:165 mod_vcard_ldap.erl:238 mod_vcard_odbc.erl:129 -msgid "Erlang Jabber Server" -msgstr "Erlang Jabber Server" - -#: mod_vcard.erl:364 mod_vcard.erl:478 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:454 -msgid "Birthday" -msgstr "Födelsedag" - -#: mod_vcard.erl:364 mod_vcard.erl:480 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:456 -msgid "City" -msgstr "Stad" - -#: mod_vcard.erl:364 mod_vcard.erl:479 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:455 -msgid "Country" -msgstr "Land" - -#: mod_vcard.erl:364 mod_vcard.erl:481 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:457 -msgid "Email" -msgstr "Email" - -#: mod_vcard.erl:364 mod_vcard.erl:476 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:452 -msgid "Family Name" -msgstr "Efternamn" - -#: mod_vcard.erl:364 mod_vcard_odbc.erl:342 -msgid "" -"Fill in the form to search for any matching Jabber User (Add * to the end of " -"field to match substring)" -msgstr "" -"Fyll i formuläret för att söka efter en användare (lägg till * på slutet av " -"fältet för att hitta alla som börjar så)" - -#: mod_vcard.erl:364 mod_vcard.erl:473 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:449 -msgid "Full Name" -msgstr "Fullständigt namn" - -#: mod_vcard.erl:364 mod_vcard.erl:475 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:451 -msgid "Middle Name" -msgstr "Mellannamn" - -#: mod_vcard.erl:364 mod_vcard.erl:474 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:450 web/ejabberd_web_admin.erl:2006 -msgid "Name" -msgstr "Namn" - -#: mod_vcard.erl:364 mod_vcard.erl:482 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:458 -msgid "Organization Name" -msgstr "Organisationsnamn" - -#: mod_vcard.erl:364 mod_vcard.erl:483 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:459 -msgid "Organization Unit" -msgstr "Organisationsenhet" - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "Search users in " -msgstr "Sök efter användare på " - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "You need an x:data capable client to search" -msgstr "Du behöver en klient som stödjer x:data, för att kunna söka" - -#: mod_vcard.erl:389 mod_vcard_ldap.erl:487 mod_vcard_odbc.erl:367 -msgid "vCard User Search" -msgstr "vCard användare sök" - -#: mod_vcard.erl:445 mod_vcard_ldap.erl:541 mod_vcard_odbc.erl:421 -msgid "ejabberd vCard module" -msgstr "ejabberd vCard-modul" - -#: mod_vcard.erl:469 mod_vcard_ldap.erl:551 mod_vcard_odbc.erl:445 -msgid "Search Results for " -msgstr "Sökresultat för" - -#: mod_vcard_ldap.erl:462 -msgid "Fill in fields to search for any matching Jabber User" -msgstr "Fyll i fält för att söka efter jabberanvändare" - -#: web/ejabberd_web_admin.erl:193 web/ejabberd_web_admin.erl:203 -#: web/ejabberd_web_admin.erl:219 web/ejabberd_web_admin.erl:229 -msgid "Unauthorized" -msgstr "Ej auktoriserad" - -#: web/ejabberd_web_admin.erl:286 web/ejabberd_web_admin.erl:303 -msgid "ejabberd Web Admin" -msgstr "ejabberd Web Admin" - -#: web/ejabberd_web_admin.erl:769 web/ejabberd_web_admin.erl:780 -msgid "Administration" -msgstr "Administration" - -#: web/ejabberd_web_admin.erl:878 web/ejabberd_web_admin.erl:982 -msgid "Raw" -msgstr "Ra" - -#: web/ejabberd_web_admin.erl:1017 -msgid "~s access rule configuration" -msgstr "Åtkomstregelkonfiguration för ~s" - -#: web/ejabberd_web_admin.erl:1035 -msgid "Virtual Hosts" -msgstr "Virtuella servrar" - -#: web/ejabberd_web_admin.erl:1043 web/ejabberd_web_admin.erl:1050 -msgid "Users" -msgstr "Användare" - -#: web/ejabberd_web_admin.erl:1078 -msgid "Users Last Activity" -msgstr "Användarens senaste aktivitet" - -#: web/ejabberd_web_admin.erl:1080 -msgid "Period: " -msgstr "Period: " - -#: web/ejabberd_web_admin.erl:1090 -msgid "Last month" -msgstr "Senaste månaden" - -#: web/ejabberd_web_admin.erl:1091 -msgid "Last year" -msgstr "Senaste året" - -#: web/ejabberd_web_admin.erl:1092 -msgid "All activity" -msgstr "All aktivitet" - -#: web/ejabberd_web_admin.erl:1094 -msgid "Show Ordinary Table" -msgstr "Visa normal tabell" - -#: web/ejabberd_web_admin.erl:1096 -msgid "Show Integral Table" -msgstr "Visa kumulativ tabell" - -#: web/ejabberd_web_admin.erl:1105 web/ejabberd_web_admin.erl:1939 -msgid "Statistics" -msgstr "Statistik" - -#: web/ejabberd_web_admin.erl:1117 -msgid "Not Found" -msgstr "Noden finns inte" - -#: web/ejabberd_web_admin.erl:1134 -msgid "Node not found" -msgstr "Noden finns inte" - -#: web/ejabberd_web_admin.erl:1460 -msgid "Host" -msgstr "Server" - -#: web/ejabberd_web_admin.erl:1461 -msgid "Registered Users" -msgstr "Registrerade användare" - -#: web/ejabberd_web_admin.erl:1573 -msgid "Offline Messages" -msgstr "Offline meddelanden" - -#: web/ejabberd_web_admin.erl:1574 web/ejabberd_web_admin.erl:1767 -msgid "Last Activity" -msgstr "Senast aktivitet" - -#: web/ejabberd_web_admin.erl:1659 web/ejabberd_web_admin.erl:1675 -msgid "Registered Users:" -msgstr "Registrerade användare" - -#: web/ejabberd_web_admin.erl:1661 web/ejabberd_web_admin.erl:1677 -#: web/ejabberd_web_admin.erl:2192 -msgid "Online Users:" -msgstr "Inloggade användare" - -#: web/ejabberd_web_admin.erl:1663 -msgid "Outgoing s2s Connections:" -msgstr "Utgående s2s anslutning" - -#: web/ejabberd_web_admin.erl:1665 -msgid "Outgoing s2s Servers:" -msgstr "Utgående s2s server" - -#: web/ejabberd_web_admin.erl:1734 web/mod_register_web.erl:175 -#: web/mod_register_web.erl:372 web/mod_register_web.erl:381 -#: web/mod_register_web.erl:411 -msgid "Change Password" -msgstr "Ändra lösenord" - -#: web/ejabberd_web_admin.erl:1758 -msgid "User " -msgstr "Användare " - -#: web/ejabberd_web_admin.erl:1765 -msgid "Connected Resources:" -msgstr "Anslutna resurser:" - -#: web/ejabberd_web_admin.erl:1766 web/mod_register_web.erl:227 -#: web/mod_register_web.erl:519 -msgid "Password:" -msgstr "Lösenord:" - -#: web/ejabberd_web_admin.erl:1828 -msgid "No Data" -msgstr "Ingen data" - -#: web/ejabberd_web_admin.erl:1906 -msgid "Nodes" -msgstr "Noder" - -#: web/ejabberd_web_admin.erl:1929 web/ejabberd_web_admin.erl:1951 -msgid "Node " -msgstr "Nod " - -#: web/ejabberd_web_admin.erl:1938 -msgid "Listened Ports" -msgstr "Lyssnarport" - -#: web/ejabberd_web_admin.erl:1940 web/ejabberd_web_admin.erl:2260 -#: web/ejabberd_web_admin.erl:2448 -msgid "Update" -msgstr "Uppdatera" - -#: web/ejabberd_web_admin.erl:1943 web/ejabberd_web_admin.erl:2569 -msgid "Restart" -msgstr "Omstart" - -#: web/ejabberd_web_admin.erl:1945 web/ejabberd_web_admin.erl:2571 -msgid "Stop" -msgstr "Stoppa" - -#: web/ejabberd_web_admin.erl:1959 -msgid "RPC Call Error" -msgstr "RPC Uppringningserror" - -#: web/ejabberd_web_admin.erl:2000 -msgid "Database Tables at " -msgstr "Databas tabell pa" - -#: web/ejabberd_web_admin.erl:2007 -msgid "Storage Type" -msgstr "Lagringstyp" - -#: web/ejabberd_web_admin.erl:2008 -msgid "Elements" -msgstr "Elements" - -#: web/ejabberd_web_admin.erl:2009 -msgid "Memory" -msgstr "Minne" - -#: web/ejabberd_web_admin.erl:2032 web/ejabberd_web_admin.erl:2137 -msgid "Error" -msgstr "Fel" - -#: web/ejabberd_web_admin.erl:2034 -msgid "Backup of " -msgstr "Backup av" - -#: web/ejabberd_web_admin.erl:2036 -msgid "" -"Please note that these options will only backup the builtin Mnesia database. " -"If you are using the ODBC module, you also need to backup your SQL database " -"separately." -msgstr "" -"Kom ihåg att dessa inställningar endast tar backup pa builtin Mnesias " -"databas. Om du använder ODBC modul så måste du ta backup på SQLs databas " -"enskilt" - -#: web/ejabberd_web_admin.erl:2041 -msgid "Store binary backup:" -msgstr "Lagra den binära backupen" - -#: web/ejabberd_web_admin.erl:2045 web/ejabberd_web_admin.erl:2052 -#: web/ejabberd_web_admin.erl:2060 web/ejabberd_web_admin.erl:2067 -#: web/ejabberd_web_admin.erl:2074 web/ejabberd_web_admin.erl:2081 -#: web/ejabberd_web_admin.erl:2088 web/ejabberd_web_admin.erl:2096 -#: web/ejabberd_web_admin.erl:2103 web/ejabberd_web_admin.erl:2110 -msgid "OK" -msgstr "OK" - -#: web/ejabberd_web_admin.erl:2048 -msgid "Restore binary backup immediately:" -msgstr "återställ den binära backupen omedelbart" - -#: web/ejabberd_web_admin.erl:2056 -msgid "" -"Restore binary backup after next ejabberd restart (requires less memory):" -msgstr "återställ den binära backupen efter nästa ejabberd omstart" - -#: web/ejabberd_web_admin.erl:2063 -msgid "Store plain text backup:" -msgstr "Lagra textbackup" - -#: web/ejabberd_web_admin.erl:2070 -msgid "Restore plain text backup immediately:" -msgstr "återställ textbackup omedelbart" - -#: web/ejabberd_web_admin.erl:2077 -msgid "Import users data from a PIEFXIS file (XEP-0227):" -msgstr "Importera användardata från en PIEFXIS fil (XEP-0227):" - -#: web/ejabberd_web_admin.erl:2084 -msgid "Export data of all users in the server to PIEFXIS files (XEP-0227):" -msgstr "" -"Exportera data av alla användare i servern till en PIEFXIS fil (XEP-0227):" - -#: web/ejabberd_web_admin.erl:2091 -msgid "Export data of users in a host to PIEFXIS files (XEP-0227):" -msgstr "Exportera data av användare i en host till PIEFXIS fil (XEP-0227):" - -#: web/ejabberd_web_admin.erl:2099 -msgid "Import user data from jabberd14 spool file:" -msgstr "Importera användare från jabberd14 Spool filer" - -#: web/ejabberd_web_admin.erl:2106 -msgid "Import users data from jabberd14 spool directory:" -msgstr "Importera användare från jabberd14 Spool directory:" - -#: web/ejabberd_web_admin.erl:2132 -msgid "Listened Ports at " -msgstr "Lyssnande portar på " - -#: web/ejabberd_web_admin.erl:2157 -msgid "Modules at " -msgstr "Moduler på" - -#: web/ejabberd_web_admin.erl:2183 -msgid "Statistics of ~p" -msgstr "Statistik på ~p" - -#: web/ejabberd_web_admin.erl:2186 -msgid "Uptime:" -msgstr "Tid upp" - -#: web/ejabberd_web_admin.erl:2189 -msgid "CPU Time:" -msgstr "CPU tid" - -#: web/ejabberd_web_admin.erl:2195 -msgid "Transactions Committed:" -msgstr "Transaktioner kommittade" - -#: web/ejabberd_web_admin.erl:2198 -msgid "Transactions Aborted:" -msgstr "Transaktioner borttagna" - -#: web/ejabberd_web_admin.erl:2201 -msgid "Transactions Restarted:" -msgstr "Transaktioner omstartade" - -#: web/ejabberd_web_admin.erl:2204 -msgid "Transactions Logged:" -msgstr "Transaktioner loggade " - -#: web/ejabberd_web_admin.erl:2246 -msgid "Update " -msgstr "Uppdatera" - -#: web/ejabberd_web_admin.erl:2254 -msgid "Update plan" -msgstr "Uppdateringsplan" - -#: web/ejabberd_web_admin.erl:2255 -msgid "Modified modules" -msgstr "Uppdaterade moduler" - -#: web/ejabberd_web_admin.erl:2256 -msgid "Update script" -msgstr "Uppdatera skript" - -#: web/ejabberd_web_admin.erl:2257 -msgid "Low level update script" -msgstr "Uppdaterade laglevel skript" - -#: web/ejabberd_web_admin.erl:2258 -msgid "Script check" -msgstr "Skript kollat" - -#: web/ejabberd_web_admin.erl:2426 -msgid "Port" -msgstr "Port" - -#: web/ejabberd_web_admin.erl:2427 -msgid "IP" -msgstr "IP" - -#: web/ejabberd_web_admin.erl:2428 -msgid "Protocol" -msgstr "Protocol" - -#: web/ejabberd_web_admin.erl:2429 web/ejabberd_web_admin.erl:2556 -msgid "Module" -msgstr "Modul" - -#: web/ejabberd_web_admin.erl:2430 web/ejabberd_web_admin.erl:2557 -msgid "Options" -msgstr "Parametrar" - -#: web/ejabberd_web_admin.erl:2450 -msgid "Delete" -msgstr "Ta bort" - -#: web/ejabberd_web_admin.erl:2579 -msgid "Start" -msgstr "Starta" - -#: web/mod_register_web.erl:104 -msgid "Your Jabber account was successfully created." -msgstr "" - -#: web/mod_register_web.erl:107 -msgid "There was an error creating the account: " -msgstr "" - -#: web/mod_register_web.erl:115 -msgid "Your Jabber account was successfully deleted." -msgstr "" - -#: web/mod_register_web.erl:118 -msgid "There was an error deleting the account: " -msgstr "" - -#: web/mod_register_web.erl:128 -msgid "The password of your Jabber account was successfully changed." -msgstr "" - -#: web/mod_register_web.erl:131 -msgid "There was an error changing the password: " -msgstr "" - -#: web/mod_register_web.erl:163 web/mod_register_web.erl:172 -msgid "Jabber Account Registration" -msgstr "" - -#: web/mod_register_web.erl:174 web/mod_register_web.erl:192 -#: web/mod_register_web.erl:201 -msgid "Register a Jabber account" -msgstr "" - -#: web/mod_register_web.erl:176 web/mod_register_web.erl:493 -#: web/mod_register_web.erl:502 -msgid "Unregister a Jabber account" -msgstr "" - -#: web/mod_register_web.erl:203 -msgid "" -"This page allows to create a Jabber account in this Jabber server. Your JID " -"(Jabber IDentifier) will be of the form: username@server. Please read " -"carefully the instructions to fill correctly the fields." -msgstr "" - -#: web/mod_register_web.erl:212 web/mod_register_web.erl:386 -#: web/mod_register_web.erl:509 -#, fuzzy -msgid "Username:" -msgstr "IRC-användarnamn" - -#: web/mod_register_web.erl:217 -msgid "This is case insensitive: macbeth is the same that MacBeth and Macbeth." -msgstr "" - -#: web/mod_register_web.erl:218 -msgid "Characters not allowed:" -msgstr "" - -#: web/mod_register_web.erl:222 web/mod_register_web.erl:391 -#: web/mod_register_web.erl:514 -#, fuzzy -msgid "Server:" -msgstr "Server ~b" - -#: web/mod_register_web.erl:232 -msgid "" -"Don't tell your password to anybody, not even the administrators of the " -"Jabber server." -msgstr "" - -#: web/mod_register_web.erl:234 -msgid "You can later change your password using a Jabber client." -msgstr "" - -#: web/mod_register_web.erl:235 -msgid "" -"Some Jabber clients can store your password in your computer. Use that " -"feature only if you trust your computer is safe." -msgstr "" - -#: web/mod_register_web.erl:237 -msgid "" -"Memorize your password, or write it in a paper placed in a safe place. In " -"Jabber there isn't an automated way to recover your password if you forget " -"it." -msgstr "" - -#: web/mod_register_web.erl:242 web/mod_register_web.erl:406 -#, fuzzy -msgid "Password Verification:" -msgstr "Lösenordsverifikation" - -#: web/mod_register_web.erl:250 -#, fuzzy -msgid "Register" -msgstr "Kontaktlista" - -#: web/mod_register_web.erl:396 -#, fuzzy -msgid "Old Password:" -msgstr "Lösenord:" - -#: web/mod_register_web.erl:401 -#, fuzzy -msgid "New Password:" -msgstr "Lösenord:" - -#: web/mod_register_web.erl:504 -msgid "This page allows to unregister a Jabber account in this Jabber server." -msgstr "" - -#: web/mod_register_web.erl:524 -msgid "Unregister" -msgstr "" - -#, fuzzy -#~ msgid "Captcha test failed" -#~ msgstr "Din CAPTCHA är godkänd." diff --git a/priv/msgs/ta.msg b/priv/msgs/ta.msg new file mode 100644 index 000000000..d76425610 --- /dev/null +++ b/priv/msgs/ta.msg @@ -0,0 +1,630 @@ +%% Generated automatically +%% DO NOT EDIT: run `make translations` instead +%% To improve translations please read: +%% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ + +{" (Add * to the end of field to match substring)"," (அடி மூலக்கூறுடன் பொருந்தக்கூடிய புலத்தின் முடிவில் * சேர்க்கவும்)"}. +{" has set the subject to: "," இதற்கு பொருள் அமைத்துள்ளது: "}. +{"# participants","# பங்கேற்பாளர்கள்"}. +{"A description of the node","முனையின் விளக்கம்"}. +{"A friendly name for the node","முனைக்கு ஒரு நட்பு பெயர்"}. +{"A password is required to enter this room","இந்த அறைக்குள் நுழைய கடவுச்சொல் தேவை"}. +{"A Web Page","ஒரு வலைப்பக்கம்"}. +{"Accept","ஏற்றுக்கொள்"}. +{"Access denied by service policy","பணி கொள்கையால் மறுக்கப்பட்டது"}. +{"Access model","அணுகல் மாதிரி"}. +{"Account doesn't exist","கணக்கு இல்லை"}. +{"Action on user","பயனரின் செயல்"}. +{"Add a hat to a user","ஒரு பயனருக்கு ஒரு தொப்பியைச் சேர்க்கவும்"}. +{"Add User","பயனரைச் சேர்க்கவும்"}. +{"Administration of ","நிர்வாகம் "}. +{"Administration","நிர்வாகம்"}. +{"Administrator privileges required","நிர்வாகி சலுகைகள் தேவை"}. +{"All activity","அனைத்து செயல்பாடுகளும்"}. +{"All Users","அனைத்து பயனர்களும்"}. +{"Allow subscription","சந்தாவை அனுமதிக்கவும்"}. +{"Allow this Jabber ID to subscribe to this pubsub node?","இந்த பப்சப் முனைக்கு குழுசேர இந்த சாபர் ஐடியை அனுமதிக்கவா?"}. +{"Allow this person to register with the room?","இந்த நபரை அறையில் பதிவு செய்ய அனுமதிக்கவா?"}. +{"Allow users to change the subject","இந்த விசயத்தை மாற்ற பயனர்களை அனுமதிக்கவும்"}. +{"Allow users to query other users","பயனர்களை மற்ற பயனர்களை வினவ அனுமதிக்கவும்"}. +{"Allow users to send invites","அழைப்புகளை அனுப்ப பயனர்களை அனுமதிக்கவும்"}. +{"Allow users to send private messages","தனிப்பட்ட செய்திகளை அனுப்ப பயனர்களை அனுமதிக்கவும்"}. +{"Allow visitors to change nickname","பார்வையாளர்களை புனைப்பெயரை மாற்ற அனுமதிக்கவும்"}. +{"Allow visitors to send private messages to","தனிப்பட்ட செய்திகளை அனுப்ப பார்வையாளர்களை அனுமதிக்கவும்"}. +{"Allow visitors to send status text in presence updates","முன்னிலையில் புதுப்பிப்புகளில் நிலை உரையை அனுப்ப பார்வையாளர்களை அனுமதிக்கவும்"}. +{"Allow visitors to send voice requests","குரல் கோரிக்கைகளை அனுப்ப பார்வையாளர்களை அனுமதிக்கவும்"}. +{"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.","அறை உறுப்பினர்களை வரையறுக்கும் தொடர்புடைய எல்.டி.ஏ.பி குழு; இது ஒரு குழுவின் செயல்படுத்தல்-குறிப்பிட்ட அல்லது வரிசைப்படுத்தல்-குறிப்பிட்ட வரையறையின்படி எல்.டி.ஏ.பி புகழ்பெற்ற பெயராக இருக்க வேண்டும்."}. +{"Announcements","அறிவிப்புகள்"}. +{"Answer associated with a picture","ஒரு படத்துடன் தொடர்புடைய பதில்"}. +{"Answer associated with a video","வீடியோவுடன் தொடர்புடைய பதில்"}. +{"Answer associated with speech","பேச்சுடன் தொடர்புடைய பதில்"}. +{"Answer to a question","ஒரு கேள்விக்கு பதில்"}. +{"Anyone in the specified roster group(s) may subscribe and retrieve items","குறிப்பிட்ட ரோச்டர் குழு (கள்) இல் உள்ள எவரும் உருப்படிகளை குழுசேரலாம் மற்றும் மீட்டெடுக்கலாம்"}. +{"Anyone may associate leaf nodes with the collection","எவரும் இலை முனைகளை சேகரிப்புடன் தொடர்புபடுத்தலாம்"}. +{"Anyone may publish","எவரும் வெளியிடலாம்"}. +{"Anyone may subscribe and retrieve items","எவரும் பொருட்களை குழுசேர் மற்றும் மீட்டெடுக்கலாம்"}. +{"Anyone with a presence subscription of both or from may subscribe and retrieve items","அல்லது இரண்டின் இருப்பு சந்தா உள்ள எவரும் உருப்படிகளை குழுசேர் மற்றும் மீட்டெடுக்கலாம்"}. +{"Anyone with Voice","குரல் உள்ள எவரும்"}. +{"Anyone","யாரும்"}. +{"API Commands","பநிஇ கட்டளைகள்"}. +{"April","ப-சித்திரை"}. +{"Arguments","வாதங்கள்"}. +{"Attribute 'channel' is required for this request","இந்த கோரிக்கைக்கு 'சேனல்' என்ற பண்புக்கூறு தேவை"}. +{"Attribute 'id' is mandatory for MIX messages","கலவை செய்திகளுக்கு 'ஐடி' கட்டாயமாகும்"}. +{"Attribute 'jid' is not allowed here","'சிட்' என்ற பண்புக்கூறு இங்கே அனுமதிக்கப்படவில்லை"}. +{"Attribute 'node' is not allowed here","'முனை' என்ற பண்புக்கூறு இங்கே அனுமதிக்கப்படவில்லை"}. +{"Attribute 'to' of stanza that triggered challenge","சவாலைத் தூண்டும் சரணத்தின் '' 'என்ற பண்புக்கூறு"}. +{"August","ஆ-ஆவணி"}. +{"Automatic node creation is not enabled","தானியங்கி முனை உருவாக்கம் இயக்கப்படவில்லை"}. +{"Backup Management","காப்பு மேலாண்மை"}. +{"Backup of ~p","~p இன் காப்புப்பிரதி"}. +{"Backup to File at ","தாக்கல் செய்ய காப்புப்பிரதி "}. +{"Backup","காப்புப்பிரதி"}. +{"Bad format","மோசமான வடிவம்"}. +{"Birthday","பிறந்த நாள்"}. +{"Both the username and the resource are required","பயனர்பெயர் மற்றும் சான்று இரண்டும் தேவை"}. +{"Bytestream already activated","பைட்டெச்ட்ரீம் ஏற்கனவே செயல்படுத்தப்பட்டது"}. +{"Cannot remove active list","செயலில் உள்ள பட்டியலை அகற்ற முடியாது"}. +{"Cannot remove default list","இயல்புநிலை பட்டியலை அகற்ற முடியாது"}. +{"CAPTCHA web page","கேப்ட்சா வலைப்பக்கம்"}. +{"Challenge ID","அறைகூவல் ஐடி"}. +{"Change Password","கடவுச்சொல்லை மாற்றவும்"}. +{"Change User Password","பயனர் கடவுச்சொல்லை மாற்றவும்"}. +{"Changing password is not allowed","கடவுச்சொல்லை மாற்ற அனுமதிக்கப்படவில்லை"}. +{"Changing role/affiliation is not allowed","பங்கு/இணைப்பை மாற்ற அனுமதிக்கப்படவில்லை"}. +{"Channel already exists","சேனல் ஏற்கனவே உள்ளது"}. +{"Channel does not exist","சேனல் இல்லை"}. +{"Channel JID","சேனல் சிட்"}. +{"Channels","சேனல்கள்"}. +{"Characters not allowed:","எழுத்துக்கள் அனுமதிக்கப்படவில்லை:"}. +{"Chatroom configuration modified","அரட்டை உள்ளமைவு மாற்றப்பட்டது"}. +{"Chatroom is created","அரட்டை அறை உருவாக்கப்பட்டது"}. +{"Chatroom is destroyed","அரட்டை அறை அழிக்கப்படுகிறது"}. +{"Chatroom is started","அரட்டை அறை தொடங்கப்பட்டது"}. +{"Chatroom is stopped","அரட்டை அறை நிறுத்தப்பட்டது"}. +{"Chatrooms","அரட்டை அறைகள்"}. +{"Choose a username and password to register with this server","இந்த சேவையகத்துடன் பதிவு செய்ய பயனர்பெயர் மற்றும் கடவுச்சொல்லைத் தேர்வுசெய்க"}. +{"Choose storage type of tables","அட்டவணைகளின் சேமிப்பக வகை என்பதைத் தேர்வுசெய்க"}. +{"Choose whether to approve this entity's subscription.","இந்த நிறுவனத்தின் சந்தாவை அங்கீகரிக்க வேண்டுமா என்பதைத் தேர்வுசெய்க."}. +{"City","நகரம்"}. +{"Client acknowledged more stanzas than sent by server","சேவையகத்தால் அனுப்பப்பட்டதை விட கிளையன்ட் அதிக சரணத்தை ஒப்புக் கொண்டார்"}. +{"Clustering","கிளச்டரிங்"}. +{"Commands","கட்டளைகள்"}. +{"Conference room does not exist","மாநாட்டு அறை இல்லை"}. +{"Configuration of room ~s","அறையின் உள்ளமைவு ~s"}. +{"Configuration","உள்ளமைவு"}. +{"Contact Addresses (normally, room owner or owners)","தொடர்பு முகவரிகள் (பொதுவாக, அறை உரிமையாளர் அல்லது உரிமையாளர்கள்)"}. +{"Country","நாடு"}. +{"Current Discussion Topic","தற்போதைய கலந்துரையாடல் தலைப்பு"}. +{"Database failure","தரவுத்தள தோல்வி"}. +{"Database Tables Configuration at ","தரவுத்தள அட்டவணைகள் உள்ளமைவு "}. +{"Database","தரவுத்தளம்"}. +{"December","கா-மார்கழி"}. +{"Default users as participants","பங்கேற்பாளர்களாக இயல்புநிலை பயனர்கள்"}. +{"Delete message of the day on all hosts","அனைத்து புரவலர்களிலும் அன்றைய செய்தியை நீக்கு"}. +{"Delete message of the day","அன்றைய செய்தியை நீக்கு"}. +{"Delete User","பயனரை நீக்கு"}. +{"Deliver event notifications","நிகழ்வு அறிவிப்புகளை வழங்கவும்"}. +{"Deliver payloads with event notifications","நிகழ்வு அறிவிப்புகளுடன் பேலோடுகளை வழங்கவும்"}. +{"Disc only copy","வட்டு மட்டுமே நகலெடுக்கவும்"}. +{"Don't tell your password to anybody, not even the administrators of the XMPP server.","உங்கள் கடவுச்சொல்லை யாரிடமும் சொல்லாதீர்கள், எக்ச்எம்பி.பி சேவையகத்தின் நிர்வாகிகள் கூட இல்லை."}. +{"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","உருப்படி வெளியீட்டாளரின் பதிலை மாறும் வகையில் குறிப்பிடவும்"}. +{"Edit Properties","பண்புகளைத் திருத்து"}. +{"Either approve or decline the voice request.","குரல் கோரிக்கையை அங்கீகரிக்கவும் அல்லது நிராகரிக்கவும்."}. +{"ejabberd HTTP Upload service","EJABBERD HTTP பதிவேற்ற பணி"}. +{"ejabberd MUC module","EJABBERD MUC தொகுதி"}. +{"ejabberd Multicast service","எசாபர்ட் மல்டிகாச்ட் பணி"}. +{"ejabberd Publish-Subscribe module","EJABBERD வெளியீட்டு-சந்தா தொகுதி"}. +{"ejabberd SOCKS5 Bytestreams module","EJABBERD SOCKS5 BYTESTREAMS தொகுதி"}. +{"ejabberd vCard module","EJABBERD VCARD தொகுதி"}. +{"ejabberd Web Admin","எசாபர்ட் வலை நிர்வாகி"}. +{"ejabberd","எசாபர்ட்"}. +{"Email Address","மின்னஞ்சல் முகவரி"}. +{"Email","மின்னஞ்சல்"}. +{"Enable hats","தொப்பிகளை இயக்கவும்"}. +{"Enable logging","பதிவை இயக்கவும்"}. +{"Enable message archiving","செய்தி காப்பகத்தை இயக்கவும்"}. +{"Enabling push without 'node' attribute is not supported","'முனை' பண்புக்கூறு இல்லாமல் உந்துதலை இயக்குவது ஆதரிக்கப்படவில்லை"}. +{"End User Session","இறுதி பயனர் அமர்வு"}. +{"Enter nickname you want to register","நீங்கள் பதிவு செய்ய விரும்பும் புனைப்பெயரை உள்ளிடவும்"}. +{"Enter path to backup file","காப்புப்பிரதி கோப்பிற்கு பாதையை உள்ளிடவும்"}. +{"Enter path to jabberd14 spool dir","Jabberd14 Spool அடைவு க்கு பாதையை உள்ளிடவும்"}. +{"Enter path to jabberd14 spool file","சாபர்ட் 14 ச்பூல் கோப்புக்கு பாதையை உள்ளிடவும்"}. +{"Enter path to text file","உரை கோப்புக்கு பாதையை உள்ளிடவும்"}. +{"Enter the text you see","நீங்கள் பார்க்கும் உரையை உள்ளிடவும்"}. +{"Erlang XMPP Server","எர்லாங் எக்ச்எம்பிபி சேவையகம்"}. +{"Exclude Jabber IDs from CAPTCHA challenge","கேப்ட்சா சேலஞ்சில் இருந்து சாபர் ஐடிகளை விலக்குங்கள்"}. +{"Export all tables as SQL queries to a file:","அனைத்து அட்டவணைகளையும் கவிமொ வினவல்களாக ஒரு கோப்பில் ஏற்றுமதி செய்யுங்கள்:"}. +{"Export data of all users in the server to PIEFXIS files (XEP-0227):","சேவையகத்தில் உள்ள அனைத்து பயனர்களின் தரவை Piefxis கோப்புகளுக்கு (XEP-0227) ஏற்றுமதி செய்யுங்கள்:"}. +{"Export data of users in a host to PIEFXIS files (XEP-0227):","ஓச்டில் பயனர்களின் தரவை Piefxis கோப்புகளுக்கு ஏற்றுமதி செய்யுங்கள் (XEP-0227):"}. +{"External component failure","வெளிப்புற கூறு தோல்வி"}. +{"External component timeout","வெளிப்புற கூறு நேரம் முடிந்தது"}. +{"Failed to activate bytestream","பைட்டெச்ட்ரீமை செயல்படுத்தத் தவறிவிட்டது"}. +{"Failed to extract JID from your voice request approval","உங்கள் குரல் கோரிக்கை ஒப்புதலிலிருந்து சிட் பிரித்தெடுப்பதில் தோல்வி"}. +{"Failed to map delegated namespace to external component","வெளிப்புற கூறுகளுக்கு பிரதிநிதித்துவ பெயர்வெளியை வரைபடமாக்குவதில் தோல்வி"}. +{"Failed to parse HTTP response","HTTP பதிலை அலசத் தவறிவிட்டது"}. +{"Failed to process option '~s'","விருப்பத்தை '~s' செயலாக்குவதில் தோல்வி"}. +{"Family Name","குடும்ப பெயர்"}. +{"FAQ Entry","கேள்விகள் நுழைவு"}. +{"February","தை-மாசி"}. +{"File larger than ~w bytes","~w பைட்டுகளை விட பெரியது"}. +{"Fill in the form to search for any matching XMPP User","பொருந்தக்கூடிய எக்ச்எம்பிபி பயனரைத் தேட படிவத்தை நிரப்பவும்"}. +{"Friday","வெள்ளிக்கிழமை"}. +{"From ~ts","~ts இலிருந்து"}. +{"Full List of Room Admins","அறை நிர்வாகிகளின் முழு பட்டியல்"}. +{"Full List of Room Owners","அறை உரிமையாளர்களின் முழு பட்டியல்"}. +{"Full Name","முழு பெயர்"}. +{"Get List of Online Users","நிகழ்நிலை பயனர்களின் பட்டியலைப் பெறுங்கள்"}. +{"Get List of Registered Users","பதிவுசெய்யப்பட்ட பயனர்களின் பட்டியலைப் பெறுங்கள்"}. +{"Get Number of Online Users","நிகழ்நிலை பயனர்களின் எண்ணிக்கையைப் பெறுங்கள்"}. +{"Get Number of Registered Users","பதிவுசெய்யப்பட்ட பயனர்களின் எண்ணிக்கையைப் பெறுங்கள்"}. +{"Get Pending","நிலுவையில் செல்லுங்கள்"}. +{"Get User Last Login Time","பயனர் கடைசி உள்நுழைவு நேரத்தைப் பெறுங்கள்"}. +{"Get User Statistics","பயனர் புள்ளிவிவரங்களைப் பெறுங்கள்"}. +{"Given Name","கொடுக்கப்பட்ட பெயர்"}. +{"Grant voice to this person?","இந்த நபருக்கு குரல் கொடுக்கவா?"}. +{"has been banned","தடைசெய்யப்பட்டுள்ளது"}. +{"has been kicked because of a system shutdown","கணினி பணிநிறுத்தம் காரணமாக உதைக்கப்பட்டுள்ளது"}. +{"has been kicked because of an affiliation change","இணைப்பு மாற்றம் காரணமாக உதைக்கப்பட்டுள்ளது"}. +{"has been kicked because the room has been changed to members-only","அறை உறுப்பினர்களுக்கு மட்டுமே மாற்றப்பட்டதால் உதைக்கப்பட்டுள்ளது"}. +{"has been kicked","உதைக்கப்பட்டுள்ளது"}. +{"Hash of the vCard-temp avatar of this room","இந்த அறையின் vcard-temp அவதாரத்தின் ஆச்"}. +{"Hat title","தொப்பி தலைப்பு"}. +{"Hat URI","தொப்பி யூரி"}. +{"Hats limit exceeded","தொப்பிகள் வரம்பு மீறியது"}. +{"Host unknown","புரவலன் தெரியவில்லை"}. +{"HTTP File Upload","HTTP கோப்பு பதிவேற்றம்"}. +{"Idle connection","செயலற்ற இணைப்பு"}. +{"If you don't see the CAPTCHA image here, visit the web page.","நீங்கள் இங்கே கேப்ட்சா படத்தைக் காணவில்லை என்றால், வலைப்பக்கத்தைப் பார்வையிடவும்."}. +{"Import Directory","இறக்குமதி அடைவு"}. +{"Import File","கோப்பு இறக்குமதி"}. +{"Import user data from jabberd14 spool file:","சாபர்ட் 14 ச்பூல் கோப்பிலிருந்து பயனர் தரவை இறக்குமதி செய்யுங்கள்:"}. +{"Import User from File at ","கோப்பிலிருந்து பயனரை இறக்குமதி செய்யுங்கள் "}. +{"Import users data from a PIEFXIS file (XEP-0227):","PIEFXIS கோப்பிலிருந்து (XEP-0227) பயனர்களின் தரவை இறக்குமதி செய்யுங்கள்:"}. +{"Import users data from jabberd14 spool directory:","சாபர்ட் 14 ச்பூல் கோப்பகத்திலிருந்து பயனர்களின் தரவை இறக்குமதி செய்யுங்கள்:"}. +{"Import Users from Dir at ","அடைவு இலிருந்து பயனர்களை இறக்குமதி செய்யுங்கள் "}. +{"Import Users From jabberd14 Spool Files","சாபர்ட் 14 ச்பூல் கோப்புகளிலிருந்து பயனர்களை இறக்குமதி செய்யுங்கள்"}. +{"Improper domain part of 'from' attribute","'ஃப்ரம்' பண்புக்கூறின் முறையற்ற டொமைன் பகுதி"}. +{"Improper message type","முறையற்ற செய்தி வகை"}. +{"Incorrect CAPTCHA submit","தவறான கேப்ட்சா சமர்ப்பிக்கவும்"}. +{"Incorrect data form","தவறான தரவு வடிவம்"}. +{"Incorrect password","தவறான கடவுச்சொல்"}. +{"Incorrect value of 'action' attribute","'செயல்' பண்புக்கூறின் தவறான மதிப்பு"}. +{"Incorrect value of 'action' in data form","தரவு வடிவத்தில் 'செயல்' இன் தவறான மதிப்பு"}. +{"Incorrect value of 'path' in data form","தரவு வடிவத்தில் 'பாதை' இன் தவறான மதிப்பு"}. +{"Installed Modules:","நிறுவப்பட்ட தொகுதிகள்:"}. +{"Install","நிறுவவும்"}. +{"Insufficient privilege","போதிய சலுகை"}. +{"Internal server error","உள் சேவையக பிழை"}. +{"Invalid 'from' attribute in forwarded message","அனுப்பப்பட்ட செய்தியில் 'இலிருந்து' பண்புக்கூறு"}. +{"Invalid node name","தவறான முனை பெயர்"}. +{"Invalid 'previd' value","தவறான 'முந்தைய' மதிப்பு"}. +{"Invitations are not allowed in this conference","இந்த மாநாட்டில் அழைப்புகள் அனுமதிக்கப்படவில்லை"}. +{"IP addresses","ஐபி முகவரிகள்"}. +{"is now known as","இப்போது அழைக்கப்படுகிறது"}. +{"It is not allowed to send error messages to the room. The participant (~s) has sent an error message (~s) and got kicked from the room","பிழை செய்திகளை அறைக்கு அனுப்ப அனுமதிக்கப்படவில்லை. பங்கேற்பாளர் (~s) ஒரு பிழை செய்தியை (~s) அனுப்பி அறையிலிருந்து உதைத்துள்ளார்"}. +{"It is not allowed to send private messages of type \"groupchat\"","\"குரூப்சாட்\" என்ற வகை தனிப்பட்ட செய்திகளை அனுப்ப அனுமதிக்கப்படவில்லை"}. +{"It is not allowed to send private messages to the conference","தனிப்பட்ட செய்திகளை மாநாட்டிற்கு அனுப்ப அனுமதிக்கப்படவில்லை"}. +{"Jabber ID","சாபர் ஐடி"}. +{"January","மா-தை"}. +{"JID normalization denied by service policy","பணி கொள்கையால் மறுக்கப்பட்ட சிட் இயல்பாக்கம்"}. +{"JID normalization failed","சிட் இயல்பாக்கம் தோல்வியடைந்தது"}. +{"Joined MIX channels of ~ts","~ts இன் கலவை சேனல்களில் சேர்ந்தது"}. +{"Joined MIX channels:","இணைந்த கலவை சேனல்கள்:"}. +{"joins the room","அறையில் இணைகிறது"}. +{"July","ஆ-ஆடி"}. +{"June","வை-ஆனி"}. +{"Just created","இப்போது உருவாக்கப்பட்டது"}. +{"Last Activity","கடைசி செயல்பாடு"}. +{"Last login","கடைசி உள்நுழைவு"}. +{"Last message","கடைசி செய்தி"}. +{"Last month","கடந்த மாதம்"}. +{"Last year","கடந்த ஆண்டு"}. +{"Least significant bits of SHA-256 hash of text should equal hexadecimal label","உரையின் SHA-256 ஆசின் குறைந்த குறிப்பிடத்தக்க பிட்கள் எக்சாடெசிமல் சிட்டை சமமாக இருக்க வேண்டும்"}. +{"leaves the room","அறையை விட்டு வெளியேறுகிறது"}. +{"List of users with hats","தொப்பிகளைக் கொண்ட பயனர்களின் பட்டியல்"}. +{"List users with hats","தொப்பிகளுடன் பயனர்களை பட்டியலிடுங்கள்"}. +{"Logged Out","வெளியேறியது"}. +{"Logging","பதிவு"}. +{"Make participants list public","பங்கேற்பாளர்கள் பட்டியலிடுங்கள்"}. +{"Make room CAPTCHA protected","அறை கேப்ட்சா பாதுகாக்கவும்"}. +{"Make room members-only","அறை உறுப்பினர்களை மட்டும் செய்யுங்கள்"}. +{"Make room moderated","அறை மிதமானதாக்குங்கள்"}. +{"Make room password protected","அறை கடவுச்சொல்லைப் பாதுகாக்கவும்"}. +{"Make room persistent","அறையை தொடர்ந்து செய்யுங்கள்"}. +{"Make room public searchable","அறையை பொதுவில் தேடலாம்"}. +{"Malformed username","தவறான பயனர்பெயர்"}. +{"MAM preference modification denied by service policy","பணி கொள்கையால் மறுக்கப்பட்ட மாம் விருப்பத்தேர்வு மாற்றம்"}. +{"March","மா-பங்குனி"}. +{"Max # of items to persist, or `max` for no specific limit other than a server imposed maximum","அதிகபட்சம் # தொடர்ச்சியாக இருக்க வேண்டும், அல்லது அதிகபட்சமாக விதிக்கப்பட்ட சேவையகத்தைத் தவிர வேறு எந்த குறிப்பிட்ட வரம்பும் இல்லாமல் `அதிகபட்சம்"}. +{"Max payload size in bytes","பைட்டுகளில் அதிகபட்ச பேலோட் அளவு"}. +{"Maximum file size","அதிகபட்ச கோப்பு அளவு"}. +{"Maximum Number of History Messages Returned by Room","அறையால் திரும்பிய அதிகபட்ச வரலாற்று செய்திகளின் எண்ணிக்கை"}. +{"Maximum number of items to persist","தொடர்ந்து அதிகபட்ச உருப்படிகளின் எண்ணிக்கை"}. +{"Maximum Number of Occupants","அதிகபட்ச குடியிருப்பாளர்களின் எண்ணிக்கை"}. +{"May","சி-வைகாசி"}. +{"Membership is required to enter this room","இந்த அறைக்குள் நுழைய உறுப்பினர் தேவை"}. +{"Memorize your password, or write it in a paper placed in a safe place. In XMPP there isn't an automated way to recover your password if you forget it.","உங்கள் கடவுச்சொல்லை மனப்பாடம் செய்யுங்கள், அல்லது பாதுகாப்பான இடத்தில் வைக்கப்பட்டுள்ள ஒரு காகிதத்தில் எழுதுங்கள். உங்கள் கடவுச்சொல்லை மறந்துவிட்டால் அதை மீட்டெடுக்க எக்ச்எம்பிபியில் தானியங்கு வழி இல்லை."}. +{"Mere Availability in XMPP (No Show Value)","எக்ச்எம்பிபியில் வெறும் கிடைக்கும் (காட்சி மதிப்பு இல்லை)"}. +{"Message body","செய்தி உடல்"}. +{"Message not found in forwarded payload","அனுப்பப்பட்ட பேலோடில் செய்தி காணப்படவில்லை"}. +{"Messages from strangers are rejected","அந்நியர்களிடமிருந்து செய்திகள் நிராகரிக்கப்படுகின்றன"}. +{"Messages of type headline","வகை தலைப்பின் செய்திகள்"}. +{"Messages of type normal","வகை இயல்பான செய்திகள்"}. +{"Middle Name","நடுத்தர பெயர்"}. +{"Minimum interval between voice requests (in seconds)","குரல் கோரிக்கைகளுக்கு இடையில் குறைந்தபட்ச இடைவெளி (நொடிகளில்)"}. +{"Moderator privileges required","மதிப்பீட்டாளர் சலுகைகள் தேவை"}. +{"Moderators Only","மதிப்பீட்டாளர்கள் மட்டுமே"}. +{"Moderator","மதிப்பீட்டாளர்"}. +{"Module failed to handle the query","தொகுதி வினவலைக் கையாளத் தவறிவிட்டது"}. +{"Monday","திங்கள்"}. +{"Multicast","மல்டிகாச்ட்"}. +{"Multiple elements are not allowed by RFC6121","பல கூறுகள் RFC6121 ஆல் அனுமதிக்கப்படாது"}. +{"Multi-User Chat","பல பயனர் அரட்டை"}. +{"Name","பெயர்"}. +{"Natural Language for Room Discussions","அறை விவாதங்களுக்கு இயற்கை மொழி"}. +{"Natural-Language Room Name","இயற்கை மொழி அறை பெயர்"}. +{"Neither 'jid' nor 'nick' attribute found","'சிட்' அல்லது 'நிக்' பண்புக்கூறு இல்லை"}. +{"Neither 'role' nor 'affiliation' attribute found","'பங்கு' அல்லது 'இணைப்பு' பண்புக்கூறு இல்லை"}. +{"Never","ஒருபோதும்"}. +{"New Password:","புதிய கடவுச்சொல்:"}. +{"Nickname can't be empty","புனைப்பெயர் காலியாக இருக்க முடியாது"}. +{"Nickname Registration at ","இல் புனைப்பெயர் பதிவு "}. +{"Nickname ~s does not exist in the room","அறையில் ~s புனைப்பெயர் இல்லை"}. +{"Nickname","புனைப்பெயர்"}. +{"No address elements found","முகவரி கூறுகள் எதுவும் கிடைக்கவில்லை"}. +{"No addresses element found","முகவரிகள் உறுப்பு இல்லை"}. +{"No 'affiliation' attribute found","'இணைப்பு' பண்புக்கூறு இல்லை"}. +{"No available resource found","கிடைக்கக்கூடிய வளங்கள் எதுவும் கிடைக்கவில்லை"}. +{"No body provided for announce message","அறிவிப்பு செய்திக்கு எந்த உடலும் வழங்கப்படவில்லை"}. +{"No child elements found","குழந்தை கூறுகள் எதுவும் கிடைக்கவில்லை"}. +{"No data form found","தரவு படிவம் எதுவும் கிடைக்கவில்லை"}. +{"No Data","தரவு இல்லை"}. +{"No features available","நற்பொருத்தங்கள் எதுவும் கிடைக்கவில்லை"}. +{"No element found","இல்லை உறுப்பு காணப்பட்டது"}. +{"No hook has processed this command","இந்த கட்டளையை எந்த ஊக்கும் செயலாக்கவில்லை"}. +{"No info about last activity found","கடைசி செயல்பாட்டைப் பற்றிய எந்த தகவலும் கிடைக்கவில்லை"}. +{"No 'item' element found","'உருப்படி' உறுப்பு இல்லை"}. +{"No items found in this query","இந்த வினவலில் எந்த உருப்படிகளும் காணப்படவில்லை"}. +{"No limit","வரம்பு இல்லை"}. +{"No module is handling this query","இந்த வினவலை எந்த தொகுதியும் கையாளவில்லை"}. +{"No node specified","எந்த முனையும் குறிப்பிடப்படவில்லை"}. +{"No 'password' found in data form","தரவு வடிவத்தில் 'கடவுச்சொல்' காணப்படவில்லை"}. +{"No 'password' found in this query","இந்த வினவலில் 'கடவுச்சொல்' இல்லை"}. +{"No 'path' found in data form","தரவு வடிவத்தில் 'பாதை' இல்லை"}. +{"No pending subscriptions found","நிலுவையில் உள்ள சந்தாக்கள் எதுவும் கிடைக்கவில்லை"}. +{"No privacy list with this name found","இந்த பெயருடன் தனியுரிமை பட்டியல் எதுவும் இல்லை"}. +{"No private data found in this query","இந்த வினவலில் தனிப்பட்ட தரவு எதுவும் காணப்படவில்லை"}. +{"No running node found","இயங்கும் முனை எதுவும் கிடைக்கவில்லை"}. +{"No services available","சேவைகள் எதுவும் கிடைக்கவில்லை"}. +{"No statistics found for this item","இந்த உருப்படிக்கு புள்ளிவிவரங்கள் எதுவும் கிடைக்கவில்லை"}. +{"No 'to' attribute found in the invitation","அழைப்பில் காணப்படும் 'to' பண்புக்கூறு"}. +{"Nobody","யாரும்"}. +{"Node already exists","முனை ஏற்கனவே உள்ளது"}. +{"Node ID","முனை ஐடி"}. +{"Node index not found","முனை குறியீடு கிடைக்கவில்லை"}. +{"Node not found","முனை கிடைக்கவில்லை"}. +{"Node ~p","முனை ~p"}. +{"Nodeprep has failed","நோடெப்ரெப் தோல்வியுற்றது"}. +{"Nodes","முனைகள்"}. +{"Node","கணு"}. +{"None","எதுவுமில்லை"}. +{"Not allowed","அனுமதிக்கப்படவில்லை"}. +{"Not Found","கண்டுபிடிக்கப்படவில்லை"}. +{"Not subscribed","குழுசேரவில்லை"}. +{"Notify subscribers when items are removed from the node","முனையிலிருந்து உருப்படிகள் அகற்றப்படும்போது சந்தாதாரர்களுக்கு அறிவிக்கவும்"}. +{"Notify subscribers when the node configuration changes","முனை உள்ளமைவு மாறும்போது சந்தாதாரர்களுக்கு அறிவிக்கவும்"}. +{"Notify subscribers when the node is deleted","முனை நீக்கப்படும் போது சந்தாதாரர்களுக்கு அறிவிக்கவும்"}. +{"November","ஐ-கார்த்திகை"}. +{"Number of answers required","தேவையான பதில்களின் எண்ணிக்கை"}. +{"Number of occupants","குடியிருப்பாளர்களின் எண்ணிக்கை"}. +{"Number of Offline Messages","இணைப்பில்லாத செய்திகளின் எண்ணிக்கை"}. +{"Number of online users","நிகழ்நிலை பயனர்களின் எண்ணிக்கை"}. +{"Number of registered users","பதிவுசெய்யப்பட்ட பயனர்களின் எண்ணிக்கை"}. +{"Number of seconds after which to automatically purge items, or `max` for no specific limit other than a server imposed maximum","உருப்படிகளை தானாக தூய்மைப்படுத்துவதற்கான விநாடிகளின் எண்ணிக்கை, அல்லது அதிகபட்சம் விதிக்கப்பட்ட சேவையகத்தைத் தவிர வேறு எந்த குறிப்பிட்ட வரம்பையும் `அதிகபட்சம்`"}. +{"Occupants are allowed to invite others","குடியிருப்பாளர்கள் மற்றவர்களை அழைக்க அனுமதிக்கப்படுகிறார்கள்"}. +{"Occupants are allowed to query others","குடியிருப்பாளர்கள் மற்றவர்களை வினவ அனுமதிக்கப்படுகிறார்கள்"}. +{"Occupants May Change the Subject","குடியிருப்பாளர்கள் இந்த விசயத்தை மாற்றலாம்"}. +{"October","பு-ஐப்பசி"}. +{"OK","சரி"}. +{"Old Password:","பழைய கடவுச்சொல்:"}. +{"Online Users","நிகழ்நிலை பயனர்கள்"}. +{"Online","ஆன்லைனில்"}. +{"Only collection node owners may associate leaf nodes with the collection","சேகரிப்பு முனை உரிமையாளர்கள் மட்டுமே இலை முனைகளை சேகரிப்புடன் தொடர்புபடுத்தலாம்"}. +{"Only deliver notifications to available users","கிடைக்கக்கூடிய பயனர்களுக்கு மட்டுமே அறிவிப்புகளை வழங்கவும்"}. +{"Only or tags are allowed","அல்லது குறிச்சொற்கள் மட்டுமே அனுமதிக்கப்படுகின்றன"}. +{"Only element is allowed in this query","இந்த வினவலில் உறுப்பு மட்டுமே அனுமதிக்கப்படுகிறது"}. +{"Only members may query archives of this room","உறுப்பினர்கள் மட்டுமே இந்த அறையின் காப்பகங்களை வினவலாம்"}. +{"Only moderators and participants are allowed to change the subject in this room","இந்த அறையில் உள்ள விசயத்தை மாற்ற மதிப்பீட்டாளர்கள் மற்றும் பங்கேற்பாளர்கள் மட்டுமே அனுமதிக்கப்படுகிறார்கள்"}. +{"Only moderators are allowed to change the subject in this room","இந்த அறையில் உள்ள விசயத்தை மாற்ற மதிப்பீட்டாளர்கள் மட்டுமே அனுமதிக்கப்படுகிறார்கள்"}. +{"Only moderators are allowed to retract messages","மதிப்பீட்டாளர்கள் மட்டுமே செய்திகளைத் திரும்பப் பெற அனுமதிக்கப்படுகிறார்கள்"}. +{"Only moderators can approve voice requests","மதிப்பீட்டாளர்கள் மட்டுமே குரல் கோரிக்கைகளை அங்கீகரிக்க முடியும்"}. +{"Only occupants are allowed to send messages to the conference","குடியிருப்பாளர்கள் மட்டுமே மாநாட்டிற்கு செய்திகளை அனுப்ப அனுமதிக்கப்படுகிறார்கள்"}. +{"Only occupants are allowed to send queries to the conference","மாநாட்டிற்கு வினவல்களை அனுப்ப குடியிருப்பாளர்கள் மட்டுமே அனுமதிக்கப்படுகிறார்கள்"}. +{"Only publishers may publish","வெளியீட்டாளர்கள் மட்டுமே வெளியிடலாம்"}. +{"Only service administrators are allowed to send service messages","பணி செய்திகளை அனுப்ப பணி நிர்வாகிகள் மட்டுமே அனுமதிக்கப்படுகிறார்கள்"}. +{"Only those on a whitelist may associate leaf nodes with the collection","அனுமதிப்பட்டியலில் இருப்பவர்கள் மட்டுமே இலை முனைகளை சேகரிப்புடன் தொடர்புபடுத்தலாம்"}. +{"Only those on a whitelist may subscribe and retrieve items","அனுமதிப்பட்டியலில் இருப்பவர்கள் மட்டுமே உருப்படிகளை குழுசேரலாம் மற்றும் மீட்டெடுக்கலாம்"}. +{"Organization Name","அமைப்பு பெயர்"}. +{"Organization Unit","அமைப்பு பிரிவு"}. +{"Other Modules Available:","பிற தொகுதிகள் கிடைக்கின்றன:"}. +{"Outgoing s2s Connections","வெளிச்செல்லும் எச் 2 எச் இணைப்புகள்"}. +{"Owner privileges required","உரிமையாளர் சலுகைகள் தேவை"}. +{"Packet relay is denied by service policy","பணி கொள்கையால் பாக்கெட் ரிலே மறுக்கப்படுகிறது"}. +{"Participant ID","பங்கேற்பாளர் ஐடி"}. +{"Participant","பங்கேற்பாளர்"}. +{"Password Verification","கடவுச்சொல் சரிபார்ப்பு"}. +{"Password Verification:","கடவுச்சொல் சரிபார்ப்பு:"}. +{"Password","கடவுச்சொல்"}. +{"Password:","கடவுச்சொல்:"}. +{"Path to Dir","அடைவு க்கு பாதை"}. +{"Path to File","தாக்கல் செய்வதற்கான பாதை"}. +{"Payload semantic type information","பேலோட் சொற்பொருள் வகை செய்தி"}. +{"Period: ","காலம்: "}. +{"Persist items to storage","சேமிப்பகத்திற்கு உருப்படிகளைத் தொடருங்கள்"}. +{"Persistent","விடாமுயற்சி"}. +{"Ping query is incorrect","பிங் வினவல் தவறானது"}. +{"Ping","பிங்"}. +{"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","இந்த விருப்பங்கள் பில்டின் மென்சியா தரவுத்தளத்தை மட்டுமே காப்புப் பிரதி எடுக்கும் என்பதை நினைவில் கொள்க. நீங்கள் ODBC தொகுதியைப் பயன்படுத்துகிறீர்கள் என்றால், உங்கள் கவிமொ தரவுத்தளத்தையும் தனித்தனியாக காப்புப் பிரதி எடுக்க வேண்டும்."}. +{"Please, wait for a while before sending new voice request","தயவுசெய்து, புதிய குரல் கோரிக்கையை அனுப்புவதற்கு முன் சிறிது நேரம் காத்திருங்கள்"}. +{"Pong","பாங்"}. +{"Possessing 'ask' attribute is not allowed by RFC6121","'கேளுங்கள்' பண்புக்கூறு வைத்திருப்பது RFC6121 ஆல் அனுமதிக்கப்படவில்லை"}. +{"Present real Jabber IDs to","உண்மையான சாபர் ஐடிகளை வழங்கவும்"}. +{"Previous session not found","முந்தைய அமர்வு காணப்படவில்லை"}. +{"Previous session PID has been killed","முந்தைய அமர்வு பிஐடி கொல்லப்பட்டுள்ளது"}. +{"Previous session PID has exited","முந்தைய அமர்வு பிஐடி வெளியேறியது"}. +{"Previous session PID is dead","முந்தைய அமர்வு பிஐடி இறந்துவிட்டது"}. +{"Previous session timed out","முந்தைய அமர்வு நேரம் முடிந்தது"}. +{"private, ","தனிப்பட்ட, "}. +{"Public","பொது"}. +{"Publish model","மாதிரி வெளியிடு"}. +{"Publish-Subscribe","வெளியீட்டு-சந்தா"}. +{"PubSub subscriber request","பப்சப் சந்தாதாரர் கோரிக்கை"}. +{"Purge all items when the relevant publisher goes offline","தொடர்புடைய வெளியீட்டாளர் ஆஃப்லைனில் செல்லும்போது எல்லா பொருட்களையும் தூய்மைப்படுத்துங்கள்"}. +{"Push record not found","புச் பதிவு கிடைக்கவில்லை"}. +{"Queries to the conference members are not allowed in this room","இந்த அறையில் மாநாட்டு உறுப்பினர்களுக்கு வினவல்கள் அனுமதிக்கப்படவில்லை"}. +{"Query to another users is forbidden","மற்றொரு பயனர்களுக்கு வினவல் தடைசெய்யப்பட்டுள்ளது"}. +{"RAM and disc copy","ராம் மற்றும் வட்டு நகல்"}. +{"RAM copy","ரேம் நகல்"}. +{"Really delete message of the day?","அன்றைய செய்தியை உண்மையில் நீக்கவா?"}. +{"Receive notification from all descendent nodes","அனைத்து வழித்தோன்றல்களிலிருந்தும் அறிவிப்பைப் பெறுங்கள்"}. +{"Receive notification from direct child nodes only","நேரடி குழந்தை முனைகளிலிருந்து மட்டுமே அறிவிப்பைப் பெறுங்கள்"}. +{"Receive notification of new items only","புதிய பொருட்களின் அறிவிப்பைப் பெறுங்கள்"}. +{"Receive notification of new nodes only","புதிய முனைகளின் அறிவிப்பைப் பெறுங்கள்"}. +{"Recipient is not in the conference room","பெறுநர் மாநாட்டு அறையில் இல்லை"}. +{"Register an XMPP account","எக்ச்எம்பி.பி கணக்கை பதிவு செய்யுங்கள்"}. +{"Register","பதிவு செய்யுங்கள்"}. +{"Remote copy","தொலை நகல்"}. +{"Remove a hat from a user","ஒரு பயனரிடமிருந்து ஒரு தொப்பியை அகற்றவும்"}. +{"Remove User","பயனரை அகற்று"}. +{"Replaced by new connection","புதிய இணைப்பு மூலம் மாற்றப்பட்டது"}. +{"Request has timed out","கோரிக்கை நேரம் முடிந்துவிட்டது"}. +{"Request is ignored","கோரிக்கை புறக்கணிக்கப்படுகிறது"}. +{"Requested role","கோரப்பட்ட பங்கு"}. +{"Resources","வளங்கள்"}. +{"Restart Service","சேவையை மறுதொடக்கம் செய்யுங்கள்"}. +{"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:","எளிய உரை காப்புப்பிரதியை உடனடியாக மீட்டெடுக்கவும்:"}. +{"Restore","மீட்டமை"}. +{"Result","விளைவு"}. +{"Roles and Affiliations that May Retrieve Member List","உறுப்பினர் பட்டியலை மீட்டெடுக்கக்கூடிய பாத்திரங்கள் மற்றும் இணைப்புகள்"}. +{"Roles for which Presence is Broadcasted","இருப்பு ஒளிபரப்பப்படும் பாத்திரங்கள்"}. +{"Roles that May Send Private Messages","தனிப்பட்ட செய்திகளை அனுப்பக்கூடிய பாத்திரங்கள்"}. +{"Room Configuration","அறை உள்ளமைவு"}. +{"Room creation is denied by service policy","பணி கொள்கையால் அறை உருவாக்கம் மறுக்கப்படுகிறது"}. +{"Room description","அறை விளக்கம்"}. +{"Room Occupants","அறை குடியிருப்பாளர்கள்"}. +{"Room terminates","அறை முடிவடைகிறது"}. +{"Room title","அறை தலைப்பு"}. +{"Roster groups allowed to subscribe","பட்டியல் குழுக்கள் குழுசேர அனுமதிக்கப்பட்டன"}. +{"Roster size","பட்டியல் அளவு"}. +{"Running Nodes","இயங்கும் முனைகள்"}. +{"~s invites you to the room ~s","~s எச் உங்களை அறைக்கு அழைக்கிறது ~s கள்"}. +{"Saturday","காரிக்கிழமை"}. +{"Search from the date","தேதியிலிருந்து தேடுங்கள்"}. +{"Search Results for ","தேடல் முடிவுகள் "}. +{"Search the text","உரையைத் தேடுங்கள்"}. +{"Search until the date","தேதி வரை தேடுங்கள்"}. +{"Search users in ","பயனர்களைத் தேடுங்கள் "}. +{"Send announcement to all online users on all hosts","அனைத்து ஓச்ட்களிலும் அனைத்து நிகழ்நிலை பயனர்களுக்கும் அறிவிப்பை அனுப்பவும்"}. +{"Send announcement to all online users","அனைத்து நிகழ்நிலை பயனர்களுக்கும் அறிவிப்பை அனுப்பவும்"}. +{"Send announcement to all users on all hosts","அனைத்து ஓச்ட்களிலும் உள்ள அனைத்து பயனர்களுக்கும் அறிவிப்பை அனுப்பவும்"}. +{"Send announcement to all users","அனைத்து பயனர்களுக்கும் அறிவிப்பை அனுப்பவும்"}. +{"September","ஆ-புரட்டாசி"}. +{"Server:","சேவையகம்:"}. +{"Service list retrieval timed out","பணி பட்டியல் மீட்டெடுப்பு நேரம் முடிந்தது"}. +{"Session state copying timed out","அமர்வு நிலை நகலெடுக்கும் நேரம் முடிந்தது"}. +{"Set message of the day and send to online users","அன்றைய செய்தியை அமைத்து நிகழ்நிலை பயனர்களுக்கு அனுப்பவும்"}. +{"Set message of the day on all hosts and send to online users","அனைத்து ஓச்ட்களிலும் அன்றைய செய்தியை அமைத்து நிகழ்நிலை பயனர்களுக்கு அனுப்பவும்"}. +{"Shared Roster Groups","பகிரப்பட்ட பட்டியல் குழுக்கள்"}. +{"Show Integral Table","ஒருங்கிணைந்த அட்டவணையைக் காட்டு"}. +{"Show Occupants Join/Leave","காட்டு குடியிருப்பாளர்கள் சேர/விடுகிறார்கள்"}. +{"Show Ordinary Table","சாதாரண அட்டவணையைக் காட்டு"}. +{"Shut Down Service","சேவையை மூடு"}. +{"SOCKS5 Bytestreams","SOCKS5 BYTESTREAMS"}. +{"Some XMPP clients can store your password in the computer, but you should do this only in your personal computer for safety reasons.","சில எக்ச்எம்பிபி வாடிக்கையாளர்கள் உங்கள் கடவுச்சொல்லை கணினியில் சேமிக்க முடியும், ஆனால் பாதுகாப்பு காரணங்களுக்காக இதை உங்கள் தனிப்பட்ட கணினியில் மட்டுமே செய்ய வேண்டும்."}. +{"Sources Specs:","ஆதார விவரக்குறிப்புகள்:"}. +{"Specify the access model","அணுகல் மாதிரியைக் குறிப்பிடவும்"}. +{"Specify the event message type","நிகழ்வு செய்தி வகையைக் குறிப்பிடவும்"}. +{"Specify the publisher model","வெளியீட்டாளர் மாதிரியைக் குறிப்பிடவும்"}. +{"Stanza id is not valid","முடிப்பு ஐடி செல்லுபடியாகாது"}. +{"Stanza ID","முடிப்பு"}. +{"Statically specify a replyto of the node owner(s)","முனை உரிமையாளரின் (கள்) ஒரு பதிலை நிலையான முறையில் குறிப்பிடவும்"}. +{"Stopped Nodes","நிறுத்தப்பட்ட முனைகள்"}. +{"Store binary backup:","பைனரி காப்புப்பிரதியை சேமிக்கவும்:"}. +{"Store plain text backup:","எளிய உரை காப்புப்பிரதியை சேமிக்கவும்:"}. +{"Stream management is already enabled","ச்ட்ரீம் மேலாண்மை ஏற்கனவே இயக்கப்பட்டது"}. +{"Stream management is not enabled","ச்ட்ரீம் மேலாண்மை இயக்கப்படவில்லை"}. +{"Subject","பொருள்"}. +{"Submitted","சமர்ப்பிக்கப்பட்டது"}. +{"Subscriber Address","சந்தாதாரர் முகவரி"}. +{"Subscribers may publish","சந்தாதாரர்கள் வெளியிடலாம்"}. +{"Subscription requests must be approved and only subscribers may retrieve items","சந்தா கோரிக்கைகள் அங்கீகரிக்கப்பட வேண்டும் மற்றும் சந்தாதாரர்கள் மட்டுமே உருப்படிகளை மீட்டெடுக்க முடியும்"}. +{"Subscriptions are not allowed","சந்தாக்கள் அனுமதிக்கப்படவில்லை"}. +{"Sunday","ஞாயிற்றுக்கிழமை"}. +{"Text associated with a picture","ஒரு படத்துடன் தொடர்புடைய உரை"}. +{"Text associated with a sound","ஒலி ஒரு ஒலியுடன் தொடர்புடையது"}. +{"Text associated with a video","வீடியோவுடன் தொடர்புடைய உரை"}. +{"Text associated with speech","பேச்சுடன் தொடர்புடைய உரை"}. +{"That nickname is already in use by another occupant","அந்த புனைப்பெயர் ஏற்கனவே மற்றொரு குடியிருப்பாளரால் பயன்பாட்டில் உள்ளது"}. +{"That nickname is registered by another person","அந்த புனைப்பெயர் மற்றொரு நபரால் பதிவு செய்யப்பட்டுள்ளது"}. +{"The account already exists","கணக்கு ஏற்கனவே உள்ளது"}. +{"The account was not unregistered","கணக்கு பதிவு செய்யப்படவில்லை"}. +{"The body text of the last received message","கடைசியாக பெறப்பட்ட செய்தியின் உடல் உரை"}. +{"The CAPTCHA is valid.","கேப்ட்சா செல்லுபடியாகும்."}. +{"The CAPTCHA verification has failed","கேப்ட்சா சரிபார்ப்பு தோல்வியுற்றது"}. +{"The captcha you entered is wrong","நீங்கள் உள்ளிட்ட கேப்ட்சா தவறு"}. +{"The child nodes (leaf or collection) associated with a collection","சேகரிப்புடன் தொடர்புடைய குழந்தை முனைகள் (இலை அல்லது சேகரிப்பு)"}. +{"The collections with which a node is affiliated","ஒரு முனை இணைக்கப்பட்ட தொகுப்புகள்"}. +{"The DateTime at which a leased subscription will end or has ended","குத்தகைக்கு விடப்பட்ட சந்தா முடிவடையும் அல்லது முடிவடைந்த தேதிநேரம்"}. +{"The datetime when the node was created","முனை உருவாக்கப்பட்ட தேதிநேரம்"}. +{"The default language of the node","முனையின் இயல்புநிலை மொழி"}. +{"The feature requested is not supported by the conference","கோரப்பட்ட நற்பொருத்தம் மாநாட்டால் ஆதரிக்கப்படவில்லை"}. +{"The JID of the node creator","முனை படைப்பாளரின் சிட்"}. +{"The JIDs of those to contact with questions","கேள்விகளுடன் தொடர்பு கொள்ளுபவர்களின் குழந்தைகள்"}. +{"The JIDs of those with an affiliation of owner","உரிமையாளரின் இணைப்பு உள்ளவர்களின் குழந்தைகள்"}. +{"The JIDs of those with an affiliation of publisher","வெளியீட்டாளரின் இணைப்பு உள்ளவர்களின் குழந்தைகள்"}. +{"The list of all online users","அனைத்து நிகழ்நிலை பயனர்களின் பட்டியல்"}. +{"The list of all users","அனைத்து பயனர்களின் பட்டியல்"}. +{"The list of JIDs that may associate leaf nodes with a collection","இலை முனைகளை ஒரு தொகுப்போடு தொடர்புபடுத்தக்கூடிய JIDS இன் பட்டியல்"}. +{"The maximum number of child nodes that can be associated with a collection, or `max` for no specific limit other than a server imposed maximum","ஒரு சேகரிப்புடன் தொடர்புடைய அதிகபட்ச குழந்தை முனைகளின் எண்ணிக்கை, அல்லது அதிகபட்சமாக விதிக்கப்பட்ட சேவையகத்தைத் தவிர வேறு எந்த குறிப்பிட்ட வரம்பும் இல்லாமல் `அதிகபட்சம் '"}. +{"The minimum number of milliseconds between sending any two notification digests","இரண்டு அறிவிப்பு செரிமானங்களையும் அனுப்புவதற்கு இடையில் குறைந்தபட்ச மில்லி விநாடிகளின் எண்ணிக்கை"}. +{"The name of the node","முனையின் பெயர்"}. +{"The node is a collection node","முனை ஒரு சேகரிப்பு முனை"}. +{"The node is a leaf node (default)","முனை ஒரு இலை முனை (இயல்புநிலை)"}. +{"The NodeID of the relevant node","தொடர்புடைய முனையின் நோடிட்"}. +{"The number of pending incoming presence subscription requests","நிலுவையில் உள்ள உள்வரும் இருப்பு சந்தா கோரிக்கைகளின் எண்ணிக்கை"}. +{"The number of subscribers to the node","முனைக்கு சந்தாதாரர்களின் எண்ணிக்கை"}. +{"The number of unread or undelivered messages","படிக்காத அல்லது வழங்கப்படாத செய்திகளின் எண்ணிக்கை"}. +{"The password contains unacceptable characters","கடவுச்சொல்லில் ஏற்றுக்கொள்ள முடியாத எழுத்துக்கள் உள்ளன"}. +{"The password is too weak","கடவுச்சொல் மிகவும் பலவீனமாக உள்ளது"}. +{"the password is","கடவுச்சொல்"}. +{"The password of your XMPP account was successfully changed.","உங்கள் எக்ச்எம்பிபி கணக்கின் கடவுச்சொல் வெற்றிகரமாக மாற்றப்பட்டது."}. +{"The password was not changed","கடவுச்சொல் மாற்றப்படவில்லை"}. +{"The passwords are different","கடவுச்சொற்கள் வேறுபட்டவை"}. +{"The presence states for which an entity wants to receive notifications","ஒரு நிறுவனம் அறிவிப்புகளைப் பெற விரும்பும் இருப்பு நிலைகள்"}. +{"The query is only allowed from local users","வினவல் உள்ளக பயனர்களிடமிருந்து மட்டுமே அனுமதிக்கப்படுகிறது"}. +{"The query must not contain elements","வினவலில் கூறுகள் இருக்கக் கூடாது"}. +{"The room subject can be modified by participants","அறை பொருள் பங்கேற்பாளர்களால் மாற்றப்படலாம்"}. +{"The semantic type information of data in the node, usually specified by the namespace of the payload (if any)","முனையில் உள்ள தரவின் சொற்பொருள் வகை செய்தி, பொதுவாக பேலோடின் பெயர்வெளியால் குறிப்பிடப்படுகிறது (ஏதேனும் இருந்தால்)"}. +{"The sender of the last received message","கடைசியாக பெறப்பட்ட செய்தியை அனுப்பு"}. +{"The stanza MUST contain only one element, one element, or one element","ச்டான்சாவில் ஒரே உறுப்பு, ஒரு உறுப்பு அல்லது ஒரு உறுப்பு மட்டுமே இருக்க வேண்டும்"}. +{"The subscription identifier associated with the subscription request","சந்தா கோரிக்கையுடன் தொடர்புடைய சந்தா அடையாளங்காட்டி"}. +{"The URL of an XSL transformation which can be applied to payloads in order to generate an appropriate message body element.","பொருத்தமான செய்தி உடல் உறுப்பை உருவாக்குவதற்காக பேலோடுகளுக்கு பயன்படுத்தக்கூடிய எக்ச்எச்எல் மாற்றத்தின் முகவரி."}. +{"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","செல்லுபடியாகும் தரவு படிவங்களை உருவாக்குவதற்காக பேலோட் வடிவத்தில் பயன்படுத்தக்கூடிய ஒரு எக்ச்எச்எல் உருமாற்றத்தின் முகவரி, கிளையன்ட் பொதுவான தரவு படிவங்கள் வழங்குதல் எஞ்சின் பயன்படுத்தி காண்பிக்க முடியும்"}. +{"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.","இது வழக்கு உணர்வற்றது: மாக்பெத் மற்றும் மாக்பெத் தான்."}. +{"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 சேவையகத்தில் ஒரு XMPP கணக்கை பதிவு செய்ய இந்த பக்கம் அனுமதிக்கிறது. உங்கள் சிட் (சாபர் ஐடி) படிவமாக இருக்கும்: பயனர்பெயர்@சேவையகம். புலங்களை சரியாக நிரப்ப வழிமுறைகளை கவனமாகப் படியுங்கள்."}. +{"This page allows to unregister an XMPP account in this XMPP server.","இந்த எக்ச்எம்பி.பி சேவையகத்தில் எக்ச்எம்பி.பி கணக்கை பதிவு செய்ய இந்த பக்கம் அனுமதிக்கிறது."}. +{"This room is not anonymous","இந்த அறை அநாமதேயமானது அல்ல"}. +{"This service can not process the address: ~s","இந்த பணி முகவரியை செயலாக்க முடியாது: ~s"}. +{"Thursday","வியாழக்கிழமை"}. +{"Time delay","நேர நேரந்தவறுகை"}. +{"Timed out waiting for stream resumption","ச்ட்ரீம் மறுதொடக்கத்திற்காக காத்திருக்கும் நேரம்"}. +{"To register, visit ~s","பதிவு செய்ய, ~s பார்வையிடவும்"}. +{"To ~ts","~ts K"}. +{"Token TTL","கிள்ளாக்கு டி.டி.எல்"}. +{"Too many active bytestreams","பல செயலில் உள்ள பைட்டிரீம்கள்"}. +{"Too many CAPTCHA requests","பல கேப்ட்சா கோரிக்கைகள்"}. +{"Too many child elements","பல குழந்தை கூறுகள்"}. +{"Too many elements","பல கூறுகள்"}. +{"Too many elements","பல கூறுகள்"}. +{"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","இந்த ஐபி முகவரியிலிருந்து (~p) பல (~s) தோல்வியுற்ற அங்கீகாரங்கள் ~s. முகவரி UTC இல் தடைசெய்யப்படும்"}. +{"Too many receiver fields were specified","அதிகமான ரிசீவர் புலங்கள் குறிப்பிடப்பட்டன"}. +{"Too many unacked stanzas","பல அறியப்படாத சரணங்கள்"}. +{"Too many users in this conference","இந்த மாநாட்டில் அதிகமான பயனர்கள்"}. +{"Traffic rate limit is exceeded","போக்குவரத்து வீத வரம்பு மீறப்பட்டது"}. +{"~ts's MAM Archive","~ts இன் மாம் காப்பகம்"}. +{"~ts's Offline Messages Queue","~ts இன் இணைப்பில்லாத செய்திகள் வரிசை"}. +{"Tuesday","செவ்வாய்க்கிழமை"}. +{"Unable to generate a CAPTCHA","கேப்ட்சாவை உருவாக்க முடியவில்லை"}. +{"Unable to register route on existing local domain","தற்போதுள்ள உள்ளக களத்தில் வழியை பதிவு செய்ய முடியவில்லை"}. +{"Unauthorized","அங்கீகரிக்கப்படாதது"}. +{"Unexpected action","எதிர்பாராத நடவடிக்கை"}. +{"Unexpected error condition: ~p","எதிர்பாராத பிழை நிலை: ~p"}. +{"Uninstall","நிறுவல் நீக்க"}. +{"Unregister an XMPP account","ஒரு எக்ச்எம்பிபி கணக்கை பதிவு செய்யவும்"}. +{"Unregister","பதிவு செய்யப்படாதது"}. +{"Unsupported element","ஆதரிக்கப்படாத உறுப்பு"}. +{"Unsupported version","ஆதரிக்கப்படாத பதிப்பு"}. +{"Update message of the day (don't send)","அன்றைய செய்தியைப் புதுப்பிக்கவும் (அனுப்ப வேண்டாம்)"}. +{"Update message of the day on all hosts (don't send)","எல்லா ஓச்ட்களிலும் அன்றைய செய்தியைப் புதுப்பிக்கவும் (அனுப்ப வேண்டாம்)"}. +{"Update specs to get modules source, then install desired ones.","தொகுதிகள் மூலத்தைப் பெற விவரக்குறிப்புகளைப் புதுப்பிக்கவும், பின்னர் விரும்பியவற்றை நிறுவவும்."}. +{"Update Specs","விவரக்குறிப்புகளைப் புதுப்பிக்கவும்"}. +{"Updating the vCard is not supported by the vCard storage backend","VCARD ஐப் புதுப்பிப்பது VCARD சேமிப்பக பின்தளத்தில் ஆதரிக்கப்படவில்லை"}. +{"Upgrade","மேம்படுத்தல்"}. +{"URL for Archived Discussion Logs","காப்பகப்படுத்தப்பட்ட கலந்துரையாடல் பதிவுகளுக்கான முகவரி"}. +{"User already exists","பயனர் ஏற்கனவே உள்ளது"}. +{"User (jid)","பயனர் (சிட்)"}. +{"User JID","பயனர் சிட்"}. +{"User Management","பயனர் மேலாண்மை"}. +{"User not allowed to perform an IQ set on another user's vCard.","மற்றொரு பயனரின் VCARD இல் IQ தொகுப்பை செய்ய பயனர் அனுமதிக்கப்படவில்லை."}. +{"User removed","பயனர் அகற்றப்பட்டார்"}. +{"User session not found","பயனர் அமர்வு காணப்படவில்லை"}. +{"User session terminated","பயனர் அமர்வு நிறுத்தப்பட்டது"}. +{"User ~ts","பயனர் ~ts"}. +{"Username:","பயனர்பெயர்:"}. +{"Users are not allowed to register accounts so quickly","பயனர்கள் கணக்குகளை விரைவாக பதிவு செய்ய அனுமதிக்கப்படுவதில்லை"}. +{"Users Last Activity","பயனர்கள் கடைசி செயல்பாடு"}. +{"Users","பயனர்கள்"}. +{"User","பயனர்"}. +{"Value 'get' of 'type' attribute is not allowed","'வகை' பண்புக்கூறின் மதிப்பு 'பெறுதல்' அனுமதிக்கப்படவில்லை"}. +{"Value of '~s' should be boolean","'~s' மதிப்பு பூலியனாக இருக்க வேண்டும்"}. +{"Value of '~s' should be datetime string","'~s' இன் மதிப்பு தேதிநேர சரமாக இருக்க வேண்டும்"}. +{"Value of '~s' should be integer","'~s' இன் மதிப்பு முழு எண்ணாக இருக்க வேண்டும்"}. +{"Value 'set' of 'type' attribute is not allowed","'வகை' பண்புக்கூறின் 'தொகுப்பு' மதிப்பு அனுமதிக்கப்படவில்லை"}. +{"vCard User Search","VCARD பயனர் தேடல்"}. +{"View joined MIX channels","கலப்பு சேனல்களில் சேர்ந்தார்"}. +{"Virtual Hosts","மெய்நிகர் ஓச்ட்கள்"}. +{"Visitors are not allowed to change their nicknames in this room","இந்த அறையில் பார்வையாளர்கள் தங்கள் புனைப்பெயர்களை மாற்ற அனுமதிக்கப்படுவதில்லை"}. +{"Visitors are not allowed to send messages to all occupants","பார்வையாளர்கள் அனைத்து குடியிருப்பாளர்களுக்கும் செய்திகளை அனுப்ப அனுமதிக்கப்படுவதில்லை"}. +{"Visitor","பார்வையாளர்"}. +{"Voice requests are disabled in this conference","இந்த மாநாட்டில் குரல் கோரிக்கைகள் முடக்கப்பட்டுள்ளன"}. +{"Voice request","குரல் கோரிக்கை"}. +{"Web client which allows to join the room anonymously","அநாமதேயமாக அறையில் சேர அனுமதிக்கும் வலை கிளையண்ட்"}. +{"Wednesday","புதன்கிழமை"}. +{"When a new subscription is processed and whenever a subscriber comes online","புதிய சந்தா செயலாக்கப்படும் போது, சந்தாதாரர் ஆன்லைனில் வரும்போதெல்லாம்"}. +{"When a new subscription is processed","புதிய சந்தா செயலாக்கப்படும் போது"}. +{"When to send the last published item","கடைசியாக வெளியிடப்பட்ட உருப்படியை எப்போது அனுப்ப வேண்டும்"}. +{"Whether an entity wants to receive an XMPP message body in addition to the payload format","ஒரு நிறுவனம் பேலோட் வடிவமைப்பிற்கு கூடுதலாக ஒரு எக்ச்எம்பிபி செய்தி உடலைப் பெற விரும்புகிறதா என்பதை"}. +{"Whether an entity wants to receive digests (aggregations) of notifications or all notifications individually","ஒரு நிறுவனம் அறிவிப்புகளின் செரிமானங்களை (திரட்டல்கள்) பெற விரும்புகிறதா அல்லது அனைத்து அறிவிப்புகளையும் தனித்தனியாகப் பெற விரும்புகிறதா"}. +{"Whether an entity wants to receive or disable notifications","ஒரு நிறுவனம் அறிவிப்புகளைப் பெற விரும்புகிறதா அல்லது முடக்க விரும்புகிறதா"}. +{"Whether owners or publisher should receive replies to items","உரிமையாளர்கள் அல்லது வெளியீட்டாளர் உருப்படிகளுக்கான பதில்களைப் பெற வேண்டுமா"}. +{"Whether the node is a leaf (default) or a collection","முனை ஒரு இலை (இயல்புநிலை) அல்லது தொகுப்பாக இருந்தாலும் சரி"}. +{"Whether to allow subscriptions","சந்தாக்களை அனுமதிக்க வேண்டுமா"}. +{"Whether to make all subscriptions temporary, based on subscriber presence","அனைத்து சந்தாக்களையும் தற்காலிகமாக மாற்றலாமா, சந்தாதாரர் இருப்பின் அடிப்படையில்"}. +{"Whether to notify owners about new subscribers and unsubscribes","புதிய சந்தாதாரர்கள் மற்றும் குழுவிலகங்களைப் பற்றி உரிமையாளர்களுக்கு அறிவிக்க வேண்டுமா"}. +{"Who can send private messages","தனிப்பட்ட செய்திகளை யார் அனுப்ப முடியும்"}. +{"Who may associate leaf nodes with a collection","இலை முனைகளை யார் சேகரிப்புடன் தொடர்புபடுத்தலாம்"}. +{"Wrong parameters in the web formulary","வலை சூத்திரத்தில் தவறான அளவுருக்கள்"}. +{"Wrong xmlns","தவறான xmlns"}. +{"XMPP Account Registration","எக்ச்எம்பிபி கணக்கு பதிவு"}. +{"XMPP Domains","எக்ச்எம்பிபி களங்கள்"}. +{"XMPP Show Value of Away","எக்ச்எம்பிபி சோ மதிப்பைக் காட்டுகிறது"}. +{"XMPP Show Value of Chat","எக்ச்எம்பிபி அரட்டையின் மதிப்பைக் காட்டுகிறது"}. +{"XMPP Show Value of DND (Do Not Disturb)","எக்ச்எம்பிபி டி.என்.டி யின் மதிப்பைக் காட்டுகிறது (தொந்தரவு செய்யாதீர்கள்)"}. +{"XMPP Show Value of XA (Extended Away)","XMPP XA இன் மதிப்பைக் காட்டுகிறது (நீட்டிக்கப்பட்டது)"}. +{"XMPP URI of Associated Publish-Subscribe Node","அசோசியேட்டட் பப்ளிச்-சப்ச்கிரிப்ட் முனையின் எக்ச்எம்பிபி யுஆர்ஐ"}. +{"You are being removed from the room because of a system shutdown","கணினி பணிநிறுத்தம் காரணமாக நீங்கள் அறையிலிருந்து அகற்றப்படுகிறீர்கள்"}. +{"You are not allowed to send private messages","தனிப்பட்ட செய்திகளை அனுப்ப உங்களுக்கு இசைவு இல்லை"}. +{"You are not joined to the channel","நீங்கள் சேனலுடன் சேரவில்லை"}. +{"You can later change your password using an XMPP client.","எக்ச்எம்பிபி கிளையண்டைப் பயன்படுத்தி உங்கள் கடவுச்சொல்லை பின்னர் மாற்றலாம்."}. +{"You have been banned from this room","இந்த அறையிலிருந்து நீங்கள் தடை செய்யப்பட்டுள்ளீர்கள்"}. +{"You have joined too many conferences","நீங்கள் பல மாநாடுகளில் சேர்ந்துள்ளீர்கள்"}. +{"You must fill in field \"Nickname\" in the form","நீங்கள் வடிவத்தில் புலம் \"புனைப்பெயரை\" நிரப்ப வேண்டும்"}. +{"You need a client that supports x:data and CAPTCHA to register","எக்ச்: தரவு மற்றும் கேப்ட்சா பதிவு செய்ய உங்களுக்கு ஒரு வாங்கி தேவை"}. +{"You need a client that supports x:data to register the nickname","புனைப்பெயரை பதிவு செய்ய எக்ச்: தரவை ஆதரிக்கும் வாங்கி உங்களுக்குத் தேவை"}. +{"You need an x:data capable client to search","தேட உங்களுக்கு ஒரு ஃச் தேவை: தேட தரவு திறன் கொண்ட வாங்கி"}. +{"Your active privacy list has denied the routing of this stanza.","உங்கள் செயலில் உள்ள தனியுரிமை பட்டியல் இந்த சரணத்தை வழிநடத்துவதை மறுத்துள்ளது."}. +{"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.","உங்கள் எக்ச்எம்பிபி கணக்கு வெற்றிகரமாக பதிவு செய்யப்பட்டது."}. +{"Your XMPP account was successfully unregistered.","உங்கள் எக்ச்எம்பிபி கணக்கு வெற்றிகரமாக பதிவு செய்யப்படவில்லை."}. +{"You're not allowed to create nodes","முனைகளை உருவாக்க உங்களுக்கு இசைவு இல்லை"}. diff --git a/priv/msgs/th.msg b/priv/msgs/th.msg index 892fe48bb..0def6490d 100644 --- a/priv/msgs/th.msg +++ b/priv/msgs/th.msg @@ -1,31 +1,27 @@ -{"Access Configuration","การกำหนดค่าการเข้าถึง"}. -{"Access Control List Configuration","การกำหนดค่ารายการควบคุมการเข้าถึง"}. -{"Access control lists","รายการควบคุมการเข้าถึง"}. -{"Access Control Lists","รายการควบคุมการเข้าถึง"}. +%% Generated automatically +%% DO NOT EDIT: run `make translations` instead +%% To improve translations please read: +%% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ + +{" has set the subject to: "," ตั้งหัวข้อว่า: "}. {"Access denied by service policy","การเข้าถึงถูกปฏิเสธโดยนโยบายการบริการ"}. -{"Access rules","กฎการเข้าถึง"}. -{"Access Rules","กฎการเข้าถึง"}. {"Action on user","การดำเนินการกับผู้ใช้"}. -{"Add Jabber ID","เพิ่ม Jabber ID"}. -{"Add New","เพิ่มผู้ใช้ใหม่"}. {"Add User","เพิ่มผู้ใช้"}. -{"Administration","การดูแล"}. {"Administration of ","การดูแล "}. +{"Administration","การดูแล"}. {"Administrator privileges required","ต้องมีสิทธิพิเศษของผู้ดูแลระบบ"}. {"All activity","กิจกรรมทั้งหมด"}. +{"All Users","ผู้ใช้ทั้งหมด"}. {"Allow this Jabber ID to subscribe to this pubsub node?","อนุญาตให้ Jabber ID นี้เข้าร่วมเป็นสมาชิกของโหนด pubsub หรือไม่"}. {"Allow users to query other users","อนุญาตให้ผู้ใช้ถามคำถามกับผู้ใช้คนอื่นๆ ได้"}. {"Allow users to send invites","อนุญาตให้ผู้ใช้ส่งคำเชิญถึงกันได้"}. {"Allow users to send private messages","อนุญาตให้ผู้ใช้ส่งข้อความส่วนตัว"}. -{"All Users","ผู้ใช้ทั้งหมด"}. {"Announcements","ประกาศ"}. -{"anyone","ทุกคน"}. {"April","เมษายน"}. {"August","สิงหาคม"}. -{"Backup","การสำรองข้อมูล "}. {"Backup Management","การจัดการข้อมูลสำรอง"}. -{"Backup of ","การสำรองข้อมูล"}. {"Backup to File at ","สำรองไฟล์ข้อมูลที่"}. +{"Backup","การสำรองข้อมูล "}. {"Bad format","รูปแบบที่ไม่ถูกต้อง"}. {"Birthday","วันเกิด"}. {"Change Password","เปลี่ยนรหัสผ่าน"}. @@ -33,35 +29,26 @@ {"Chatroom configuration modified","มีการปรับเปลี่ยนการกำหนดค่าของห้องสนทนา"}. {"Chatrooms","ห้องสนทนา"}. {"Choose a username and password to register with this server","เลือกชื่อผู้ใช้และรหัสผ่านเพื่อลงทะเบียนกับเซิร์ฟเวอร์นี้"}. -{"Choose modules to stop","เลือกโมดูลเพื่อหยุดการทำงาน"}. {"Choose storage type of tables","เลือกชนิดการจัดเก็บของตาราง"}. {"Choose whether to approve this entity's subscription.","เลือกว่าจะอนุมัติการสมัครเข้าใช้งานของเอนทิตี้นี้หรือไม่"}. {"City","เมือง"}. {"Commands","คำสั่ง"}. {"Conference room does not exist","ไม่มีห้องประชุม"}. {"Configuration","การกำหนดค่า"}. -{"Connected Resources:","ทรัพยากรที่เชื่อมต่อ:"}. {"Country","ประเทศ"}. -{"CPU Time:","เวลาการทำงานของ CPU:"}. -{"Database","ฐานข้อมูล"}. -{"Database Tables at ","ตารางฐานข้อมูลที่"}. {"Database Tables Configuration at ","การกำหนดค่าตารางฐานข้อมูลที่"}. +{"Database","ฐานข้อมูล"}. {"December","ธันวาคม"}. {"Default users as participants","ผู้ใช้เริ่มต้นเป็นผู้เข้าร่วม"}. -{"Delete","ลบ"}. -{"Delete message of the day","ลบข้อความของวัน"}. {"Delete message of the day on all hosts","ลบข้อความของวันบนโฮสต์ทั้งหมด"}. -{"Delete Selected","ลบข้อความที่เลือก"}. +{"Delete message of the day","ลบข้อความของวัน"}. {"Delete User","ลบผู้ใช้"}. {"Deliver event notifications","ส่งการแจ้งเตือนเหตุการณ์"}. {"Deliver payloads with event notifications","ส่งส่วนของข้อมูล (payload) พร้อมกับการแจ้งเตือนเหตุการณ์"}. -{"Description:","รายละเอียด:"}. {"Disc only copy","คัดลอกเฉพาะดิสก์"}. -{"Displayed Groups:","กลุ่มที่แสดง:"}. {"Dump Backup to Text File at ","ถ่ายโอนการสำรองข้อมูลไปยังไฟล์ข้อความที่"}. {"Dump to Text File","ถ่ายโอนข้อมูลไปยังไฟล์ข้อความ"}. {"Edit Properties","แก้ไขคุณสมบัติ"}. -{"ejabberd IRC module","ejabberd IRC module"}. {"ejabberd MUC module","ejabberd MUC module"}. {"ejabberd Publish-Subscribe module","ejabberd Publish-Subscribe module"}. {"ejabberd SOCKS5 Bytestreams module","ejabberd SOCKS5 Bytestreams module"}. @@ -69,32 +56,21 @@ {"Email","อีเมล"}. {"Enable logging","เปิดใช้งานการบันทึก"}. {"End User Session","สิ้นสุดเซสชันของผู้ใช้"}. -{"Enter list of {Module, [Options]}","ป้อนรายการของ {โมดูล, [ตัวเลือก]}"}. {"Enter nickname you want to register","ป้อนชื่อเล่นที่คุณต้องการลงทะเบียน"}. {"Enter path to backup file","ป้อนพาธเพื่อสำรองไฟล์ข้อมูล"}. {"Enter path to jabberd14 spool dir","ป้อนพาธไปยัง jabberd14 spool dir"}. {"Enter path to jabberd14 spool file","ป้อนพาธไปยังไฟล์เก็บพักข้อมูล jabberd14"}. {"Enter path to text file","ป้อนพาธของไฟล์ข้อความ"}. -{"Erlang Jabber Server","Erlang Jabber Server"}. {"Family Name","นามสกุล"}. {"February","กุมภาพันธ์"}. -{"Fill in fields to search for any matching Jabber User","กรอกข้อมูลลงในฟิลด์เพื่อค้นหาผู้ใช้ Jabber ที่ตรงกัน"}. -{"Fill in the form to search for any matching Jabber User (Add * to the end of field to match substring)","กรอกข้อมูลในแบบฟอร์มเพื่อค้นหาผู้ใช้ Jabber ที่ตรงกัน (ใส่เครื่องหมาย * ที่ท้ายสุดของฟิลด์เพื่อจับคู่กับสตริงย่อย)"}. {"Friday","วันศุกร์"}. -{"From","จาก"}. -{"From ~s","จาก ~s"}. {"Full Name","ชื่อเต็ม"}. {"Get Number of Online Users","แสดงจำนวนผู้ใช้ออนไลน์"}. {"Get Number of Registered Users","แสดงจำนวนผู้ใช้ที่ลงทะเบียน"}. {"Get User Last Login Time","แสดงเวลาเข้าสู่ระบบครั้งล่าสุดของผู้ใช้"}. -{"Get User Password","ขอรับรหัสผ่านของผู้ใช้"}. {"Get User Statistics","แสดงสถิติของผู้ใช้"}. -{"Group ","กลุ่ม"}. -{"Groups","กลุ่ม"}. {"has been banned","ถูกสั่งห้าม"}. {"has been kicked","ถูกไล่ออก"}. -{" has set the subject to: "," ตั้งหัวข้อว่า: "}. -{"Host","โฮสต์"}. {"Import Directory","อิมพอร์ตไดเร็กทอรี"}. {"Import File","อิมพอร์ตไฟล์"}. {"Import User from File at ","อิมพอร์ตผู้ใช้จากไฟล์ที่"}. @@ -102,16 +78,11 @@ {"Import Users From jabberd14 Spool Files","อิมพอร์ตผู้ใช้จากไฟล์เก็บพักข้อมูล jabberd14"}. {"Improper message type","ประเภทข้อความไม่เหมาะสม"}. {"Incorrect password","รหัสผ่านไม่ถูกต้อง"}. -{"Invalid affiliation: ~s","การเข้าร่วมที่ไม่ถูกต้อง: ~s"}. -{"Invalid role: ~s","บทบาทไม่ถูกต้อง: ~s"}. {"IP addresses","ที่อยู่ IP"}. -{"IRC Transport","การส่ง IRC"}. -{"IRC Username","ชื่อผู้ใช้ IRC"}. {"is now known as","ซึ่งรู้จักกันในชื่อ"}. {"It is not allowed to send private messages of type \"groupchat\"","ไม่อนุญาตให้ส่งข้อความส่วนตัวไปยัง \"กลุ่มสนทนา\""}. {"It is not allowed to send private messages to the conference","ไม่อนุญาตให้ส่งข้อความส่วนตัวไปยังห้องประชุม"}. {"Jabber ID","Jabber ID"}. -{"Jabber ID ~s is invalid","Jabber ID ~s ไม่ถูกต้อง"}. {"January","มกราคม"}. {"joins the room","เข้าห้องสนทนานี้"}. {"July","กรกฎาคม"}. @@ -121,45 +92,30 @@ {"Last month","เดือนที่แล้ว"}. {"Last year","ปีที่แล้ว"}. {"leaves the room","ออกจากห้อง"}. -{"Listened Ports","พอร์ทฟัง"}. -{"Listened Ports at ","พอร์ทฟังที่"}. -{"List of modules to start","รายการของโมดูลที่จะเริ่มการทำงาน"}. -{"Low level update script","อัพเดตสคริปต์ระดับต่ำ"}. {"Make participants list public","สร้างรายการผู้เข้าร่วมสำหรับใช้งานโดยบุคคลทั่วไป"}. {"Make room members-only","สร้างห้องสำหรับสมาชิกเท่านั้น"}. {"Make room password protected","สร้างห้องที่มีการป้องกันด้วยรหัสผ่าน"}. {"Make room persistent","สร้างเป็นห้องถาวร"}. {"Make room public searchable","สร้างเป็นห้องที่บุคคลทั่วไปสามารถค้นหาได้"}. {"March","มีนาคม"}. -{"Maximum Number of Occupants","จำนวนผู้ครอบครองห้องสูงสุด"}. -{"Max # of items to persist","จำนวนสูงสุดของรายการที่ยืนยัน"}. {"Max payload size in bytes","ขนาดสูงสุดของส่วนของข้อมูล (payload) มีหน่วยเป็นไบต์"}. +{"Maximum Number of Occupants","จำนวนผู้ครอบครองห้องสูงสุด"}. {"May","พฤษภาคม"}. -{"Members:","สมาชิก:"}. -{"Memory","หน่วยความจำ"}. {"Message body","เนื้อหาของข้อความ"}. {"Middle Name","ชื่อกลาง"}. {"Moderator privileges required","ต้องมีสิทธิพิเศษของผู้ดูแลการสนทนา"}. -{"moderators only","สำหรับผู้ดูแลการสนทนาเท่านั้น"}. -{"Module","โมดูล"}. -{"Modules","โมดูล"}. -{"Modules at ","โมดูลที่ "}. {"Monday","วันจันทร์"}. -{"Name:","ชื่อ:"}. {"Name","ชื่อ"}. {"Never","ไม่เคย"}. -{"Nickname","ชื่อเล่น"}. {"Nickname Registration at ","การลงทะเบียนชื่อเล่นที่ "}. -{"Nickname ~s does not exist in the room","ไม่มีชื่อเล่น ~s อยู่ในห้องนี้"}. +{"Nickname","ชื่อเล่น"}. {"No body provided for announce message","ไม่ได้ป้อนเนื้อหาสำหรับข้อความที่ประกาศ"}. {"No Data","ไม่มีข้อมูล"}. -{"Node ","โหนด "}. +{"No limit","ไม่จำกัด"}. {"Node ID","ID โหนด"}. {"Node not found","ไม่พบโหนด"}. {"Nodes","โหนด"}. -{"No limit","ไม่จำกัด"}. {"None","ไม่มี"}. -{"No resource provided","ไม่ได้ระบุข้อมูล"}. {"Notify subscribers when items are removed from the node","แจ้งเตือนผู้สมัครสมาชิกเมื่อรายการถูกลบออกจากโหนด"}. {"Notify subscribers when the node configuration changes","แจ้งเตือนผู้สมัครสมาชิกเมื่อการกำหนดค่าโหนดเปลี่ยนแปลง"}. {"Notify subscribers when the node is deleted","แจ้งเตือนผู้สมัครสมาชิกเมื่อโหนดถูกลบ"}. @@ -168,35 +124,26 @@ {"Number of online users","จำนวนผู้ใช้ออนไลน์"}. {"Number of registered users","จำนวนผู้ใช้ที่ลงทะเบียน"}. {"October","ตุลาคม"}. -{"Offline Messages:","ข้อความออฟไลน์:"}. -{"Offline Messages","ข้อความออฟไลน์"}. {"OK","ตกลง"}. -{"Online","ออนไลน์"}. -{"Online Users:","ผู้ใช้ออนไลน์:"}. {"Online Users","ผู้ใช้ออนไลน์"}. +{"Online","ออนไลน์"}. {"Only deliver notifications to available users","ส่งการแจ้งเตือนถึงผู้ใช้ที่สามารถติดต่อได้เท่านั้น"}. {"Only occupants are allowed to send messages to the conference","ผู้ครอบครองห้องเท่านั้นที่ได้รับอนุญาตให้ส่งข้อความไปยังห้องประชุม"}. {"Only occupants are allowed to send queries to the conference","ผู้ครอบครองห้องเท่านั้นที่ได้รับอนุญาตให้ส่งกระทู้ถามไปยังห้องประชุม"}. {"Only service administrators are allowed to send service messages","ผู้ดูแลด้านการบริการเท่านั้นที่ได้รับอนุญาตให้ส่งข้อความการบริการ"}. -{"Options","ตัวเลือก"}. {"Organization Name","ชื่อองค์กร"}. {"Organization Unit","หน่วยขององค์กร"}. -{"Outgoing s2s Connections:","การเชื่อมต่อ s2s ขาออก:"}. {"Outgoing s2s Connections","การเชื่อมต่อ s2s ขาออก"}. -{"Outgoing s2s Servers:","เซิร์ฟเวอร์ s2s ขาออก:"}. {"Owner privileges required","ต้องมีสิทธิพิเศษของเจ้าของ"}. -{"Packet","แพ็กเก็ต"}. -{"Password:","รหัสผ่าน:"}. -{"Password","รหัสผ่าน"}. {"Password Verification","การตรวจสอบรหัสผ่าน"}. +{"Password","รหัสผ่าน"}. +{"Password:","รหัสผ่าน:"}. {"Path to Dir","พาธไปยัง Dir"}. {"Path to File","พาธของไฟล์ข้อมูล"}. -{"Pending","ค้างอยู่"}. {"Period: ","ระยะเวลา:"}. {"Persist items to storage","ยืนยันรายการที่จะจัดเก็บ"}. {"Ping","Ping"}. {"Pong","Pong"}. -{"Port","พอร์ท"}. {"Present real Jabber IDs to","แสดง Jabber IDs ที่ถูกต้องแก่"}. {"private, ","ส่วนตัว, "}. {"Publish-Subscribe","เผยแพร่-สมัครเข้าใช้งาน"}. @@ -204,41 +151,30 @@ {"Queries to the conference members are not allowed in this room","ห้องนี้ไม่อนุญาตให้ส่งกระทู้ถามถึงสมาชิกในห้องประชุม"}. {"RAM and disc copy","คัดลอก RAM และดิสก์"}. {"RAM copy","คัดลอก RAM"}. -{"Raw","ข้อมูลดิบ"}. {"Really delete message of the day?","แน่ใจว่าต้องการลบข้อความของวันหรือไม่"}. {"Recipient is not in the conference room","ผู้รับไม่ได้อยู่ในห้องประชุม"}. -{"Registered Users:","ผู้ใช้ที่ลงทะเบียน:"}. -{"Registered Users","ผู้ใช้ที่ลงทะเบียน"}. -{"Registration in mod_irc for ","การลงทะเบียนใน mod_irc สำหรับ"}. {"Remote copy","คัดลอกระยะไกล"}. -{"Remove","ลบ"}. {"Remove User","ลบผู้ใช้"}. {"Replaced by new connection","แทนที่ด้วยการเชื่อมต่อใหม่"}. {"Resources","ทรัพยากร"}. -{"Restart","เริ่มต้นใหม่"}. {"Restart Service","เริ่มต้นการบริการใหม่อีกครั้ง"}. -{"Restore","การคืนค่า"}. {"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:","คืนค่าข้อมูลสำรองที่เป็นข้อความธรรมดาโดยทันที:"}. +{"Restore","การคืนค่า"}. {"Room Configuration","การกำหนดค่าห้องสนทนา"}. {"Room creation is denied by service policy","การสร้างห้องสนทนาถูกปฏิเสธโดยนโยบายการบริการ"}. {"Room title","ชื่อห้อง"}. -{"Roster","บัญชีรายชื่อ"}. -{"Roster of ","บัญชีรายชื่อของ "}. {"Roster size","ขนาดของบัญชีรายชื่อ"}. -{"RPC Call Error","ข้อผิดพลาดจากการเรียกใช้ RPC"}. {"Running Nodes","โหนดที่ทำงาน"}. -{"~s access rule configuration","~s การกำหนดค่ากฎการเข้าถึง"}. {"Saturday","วันเสาร์"}. -{"Script check","ตรวจสอบคริปต์"}. {"Search Results for ","ผลการค้นหาสำหรับ "}. {"Search users in ","ค้นหาผู้ใช้ใน "}. -{"Send announcement to all online users","ส่งประกาศถึงผู้ใช้ออนไลน์ทั้งหมด"}. {"Send announcement to all online users on all hosts","ส่งประกาศถึงผู้ใช้ออนไลน์ทั้งหมดบนโฮสต์ทั้งหมด"}. -{"Send announcement to all users","ส่งประกาศถึงผู้ใช้ทั้งหมด"}. +{"Send announcement to all online users","ส่งประกาศถึงผู้ใช้ออนไลน์ทั้งหมด"}. {"Send announcement to all users on all hosts","ส่งประกาศถึงผู้ใช้ทั้งหมดบนโฮสต์ทั้งหมด"}. +{"Send announcement to all users","ส่งประกาศถึงผู้ใช้ทั้งหมด"}. {"September","กันยายน"}. {"Set message of the day and send to online users","ตั้งค่าข้อความของวันและส่งถึงผู้ใช้ออนไลน์"}. {"Set message of the day on all hosts and send to online users","ตั้งค่าข้อความของวันบนโฮสต์ทั้งหมดและส่งถึงผู้ใช้ออนไลน์"}. @@ -246,55 +182,27 @@ {"Show Integral Table","แสดงตารางรวม"}. {"Show Ordinary Table","แสดงตารางทั่วไป"}. {"Shut Down Service","ปิดการบริการ"}. -{"~s invites you to the room ~s","~s เชิญคุณเข้าร่วมสนทนาในห้อง ~s"}. {"Specify the access model","ระบุโมเดลการเข้าถึง"}. {"Specify the publisher model","ระบุโมเดลผู้เผยแพร่"}. -{"~s's Offline Messages Queue","~s's ลำดับข้อความออฟไลน์"}. -{"Start","เริ่ม"}. -{"Start Modules","เริ่มโมดูล"}. -{"Start Modules at ","เริ่มโมดูลที่"}. -{"Statistics","สถิติ"}. -{"Statistics of ~p","สถิติของ ~p"}. -{"Stop","หยุด"}. -{"Stop Modules","หยุดโมดูล"}. -{"Stop Modules at ","หยุดโมดูลที่"}. {"Stopped Nodes","โหนดที่หยุด"}. -{"Storage Type","ชนิดที่เก็บข้อมูล"}. {"Store binary backup:","จัดเก็บข้อมูลสำรองแบบไบนารี:"}. {"Store plain text backup:","จัดเก็บข้อมูลสำรองที่เป็นข้อความธรรมดา:"}. {"Subject","หัวเรื่อง"}. -{"Submit","ส่ง"}. {"Submitted","ส่งแล้ว"}. {"Subscriber Address","ที่อยู่ของผู้สมัคร"}. -{"Subscription","การสมัครสมาชิก"}. {"Sunday","วันอาทิตย์"}. {"the password is","รหัสผ่านคือ"}. {"This room is not anonymous","ห้องนี้ไม่ปิดบังชื่อ"}. {"Thursday","วันพฤหัสบดี"}. -{"Time","เวลา"}. {"Time delay","การหน่วงเวลา"}. -{"To","ถึง"}. -{"To ~s","ถึง ~s"}. {"Traffic rate limit is exceeded","อัตราของปริมาณการเข้าใช้เกินขีดจำกัด"}. -{"Transactions Aborted:","ทรานแซกชันที่ถูกยกเลิก:"}. -{"Transactions Committed:","ทรานแซกชันที่ได้รับมอบหมาย:"}. -{"Transactions Logged:","ทรานแซกชันที่บันทึก:"}. -{"Transactions Restarted:","ทรานแซกชันที่เริ่มทำงานใหม่อีกครั้ง:"}. {"Tuesday","วันอังคาร"}. -{"Update ","อัพเดต "}. -{"Update","อัพเดต"}. {"Update message of the day (don't send)","อัพเดตข้อความของวัน (ไม่ต้องส่ง)"}. {"Update message of the day on all hosts (don't send)","อัพเดตข้อความของวันบนโฮสต์ทั้งหมด (ไม่ต้องส่ง) "}. -{"Update plan","แผนการอัพเดต"}. -{"Update script","อัพเดตสคริปต์"}. -{"Uptime:","เวลาการทำงานต่อเนื่อง:"}. -{"Use of STARTTLS required","ต้องใช้ STARTTLS"}. -{"User ","ผู้ใช้"}. -{"User","ผู้ใช้"}. {"User Management","การจัดการผู้ใช้"}. -{"Users","ผู้ใช้"}. {"Users Last Activity","กิจกรรมล่าสุดของผู้ใช้"}. -{"Validate","ตรวจสอบ"}. +{"Users","ผู้ใช้"}. +{"User","ผู้ใช้"}. {"vCard User Search","ค้นหาผู้ใช้ vCard "}. {"Virtual Hosts","โฮสต์เสมือน"}. {"Visitors are not allowed to send messages to all occupants","ผู้เยี่ยมเยือนไม่ได้รับอนุญาตให้ส่งข้อความถึงผู้ครอบครองห้องทั้งหมด"}. @@ -303,7 +211,5 @@ {"Whether to allow subscriptions","อนุญาตให้เข้าร่วมเป็นสมาชิกหรือไม่"}. {"You have been banned from this room","คุณถูกสั่งห้ามไมให้เข้าห้องนี้"}. {"You must fill in field \"Nickname\" in the form","คุณต้องกรอกฟิลด์ \"Nickname\" ในแบบฟอร์ม"}. -{"You need an x:data capable client to configure mod_irc settings","คุณต้องใช้ไคลเอ็นต์ที่รองรับ x:data เพื่อกำหนดการตั้งค่า mod_irc"}. -{"You need an x:data capable client to configure room","คุณต้องใช้ไคลเอ็นต์ที่รองรับ x:data เพื่อกำหนดค่าห้องสนทนา "}. {"You need an x:data capable client to search","คุณต้องใช้ไคลเอ็นต์ที่รองรับ x:data เพื่อค้นหา"}. {"Your contact offline message queue is full. The message has been discarded.","ลำดับข้อความออฟไลน์ของผู้ที่ติดต่อของคุณเต็มแล้ว ข้อความถูกลบทิ้งแล้ว"}. diff --git a/priv/msgs/th.po b/priv/msgs/th.po deleted file mode 100644 index 0c1825cf9..000000000 --- a/priv/msgs/th.po +++ /dev/null @@ -1,1879 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: 2.1.0-alpha\n" -"Last-Translator: EQHO Communications (Thailand) Ltd. - http://www.eqho.com\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Language: Thai (ภาษาไทย)\n" - -#: ejabberd_c2s.erl:424 ejabberd_c2s.erl:727 -msgid "Use of STARTTLS required" -msgstr "ต้องใช้ STARTTLS" - -#: ejabberd_c2s.erl:503 -msgid "No resource provided" -msgstr "ไม่ได้ระบุข้อมูล" - -#: ejabberd_c2s.erl:1197 -msgid "Replaced by new connection" -msgstr "แทนที่ด้วยการเชื่อมต่อใหม่" - -#: ejabberd_c2s.erl:1885 -msgid "Your active privacy list has denied the routing of this stanza." -msgstr "" - -#: ejabberd_captcha.erl:96 ejabberd_captcha.erl:152 ejabberd_captcha.erl:178 -#, fuzzy -msgid "Enter the text you see" -msgstr "ป้อนพาธของไฟล์ข้อความ" - -#: ejabberd_captcha.erl:101 -msgid "Your messages to ~s are being blocked. To unblock them, visit ~s" -msgstr "" - -#: ejabberd_captcha.erl:134 -msgid "If you don't see the CAPTCHA image here, visit the web page." -msgstr "" - -#: ejabberd_captcha.erl:146 -msgid "CAPTCHA web page" -msgstr "" - -#: ejabberd_captcha.erl:307 -msgid "The CAPTCHA is valid." -msgstr "" - -#: mod_adhoc.erl:95 mod_adhoc.erl:125 mod_adhoc.erl:143 mod_adhoc.erl:161 -msgid "Commands" -msgstr "คำสั่ง" - -#: mod_adhoc.erl:149 mod_adhoc.erl:243 -msgid "Ping" -msgstr "Ping" - -#: mod_adhoc.erl:260 -msgid "Pong" -msgstr "Pong" - -#: mod_announce.erl:507 mod_announce_odbc.erl:499 -msgid "Really delete message of the day?" -msgstr "แน่ใจว่าต้องการลบข้อความของวันหรือไม่" - -#: mod_announce.erl:515 mod_announce_odbc.erl:507 mod_configure.erl:1083 -#: mod_configure.erl:1128 -msgid "Subject" -msgstr "หัวเรื่อง" - -#: mod_announce.erl:520 mod_announce_odbc.erl:512 mod_configure.erl:1088 -#: mod_configure.erl:1133 -msgid "Message body" -msgstr "เนื้อหาของข้อความ" - -#: mod_announce.erl:600 mod_announce_odbc.erl:592 -msgid "No body provided for announce message" -msgstr "ไม่ได้ป้อนเนื้อหาสำหรับข้อความที่ประกาศ" - -#: mod_announce.erl:635 mod_announce_odbc.erl:627 -msgid "Announcements" -msgstr "ประกาศ" - -#: mod_announce.erl:637 mod_announce_odbc.erl:629 -msgid "Send announcement to all users" -msgstr "ส่งประกาศถึงผู้ใช้ทั้งหมด" - -#: mod_announce.erl:639 mod_announce_odbc.erl:631 -msgid "Send announcement to all users on all hosts" -msgstr "ส่งประกาศถึงผู้ใช้ทั้งหมดบนโฮสต์ทั้งหมด" - -#: mod_announce.erl:641 mod_announce_odbc.erl:633 -msgid "Send announcement to all online users" -msgstr "ส่งประกาศถึงผู้ใช้ออนไลน์ทั้งหมด" - -#: mod_announce.erl:643 mod_announce_odbc.erl:635 mod_configure.erl:1078 -#: mod_configure.erl:1123 -msgid "Send announcement to all online users on all hosts" -msgstr "ส่งประกาศถึงผู้ใช้ออนไลน์ทั้งหมดบนโฮสต์ทั้งหมด" - -#: mod_announce.erl:645 mod_announce_odbc.erl:637 -msgid "Set message of the day and send to online users" -msgstr "ตั้งค่าข้อความของวันและส่งถึงผู้ใช้ออนไลน์" - -#: mod_announce.erl:647 mod_announce_odbc.erl:639 -msgid "Set message of the day on all hosts and send to online users" -msgstr "ตั้งค่าข้อความของวันบนโฮสต์ทั้งหมดและส่งถึงผู้ใช้ออนไลน์" - -#: mod_announce.erl:649 mod_announce_odbc.erl:641 -msgid "Update message of the day (don't send)" -msgstr "อัพเดตข้อความของวัน (ไม่ต้องส่ง)" - -#: mod_announce.erl:651 mod_announce_odbc.erl:643 -msgid "Update message of the day on all hosts (don't send)" -msgstr "อัพเดตข้อความของวันบนโฮสต์ทั้งหมด (ไม่ต้องส่ง) " - -#: mod_announce.erl:653 mod_announce_odbc.erl:645 -msgid "Delete message of the day" -msgstr "ลบข้อความของวัน" - -#: mod_announce.erl:655 mod_announce_odbc.erl:647 -msgid "Delete message of the day on all hosts" -msgstr "ลบข้อความของวันบนโฮสต์ทั้งหมด" - -#: mod_configure.erl:114 mod_configure.erl:274 mod_configure.erl:296 -#: mod_configure.erl:498 -msgid "Configuration" -msgstr "การกำหนดค่า" - -#: mod_configure.erl:125 mod_configure.erl:576 web/ejabberd_web_admin.erl:1936 -msgid "Database" -msgstr "ฐานข้อมูล" - -#: mod_configure.erl:127 mod_configure.erl:595 -msgid "Start Modules" -msgstr "เริ่มโมดูล" - -#: mod_configure.erl:129 mod_configure.erl:596 -msgid "Stop Modules" -msgstr "หยุดโมดูล" - -#: mod_configure.erl:131 mod_configure.erl:604 web/ejabberd_web_admin.erl:1937 -msgid "Backup" -msgstr "การสำรองข้อมูล " - -#: mod_configure.erl:133 mod_configure.erl:605 -msgid "Restore" -msgstr "การคืนค่า" - -#: mod_configure.erl:135 mod_configure.erl:606 -msgid "Dump to Text File" -msgstr "ถ่ายโอนข้อมูลไปยังไฟล์ข้อความ" - -#: mod_configure.erl:137 mod_configure.erl:615 -msgid "Import File" -msgstr "อิมพอร์ตไฟล์" - -#: mod_configure.erl:139 mod_configure.erl:616 -msgid "Import Directory" -msgstr "อิมพอร์ตไดเร็กทอรี" - -#: mod_configure.erl:141 mod_configure.erl:581 mod_configure.erl:1057 -msgid "Restart Service" -msgstr "เริ่มต้นการบริการใหม่อีกครั้ง" - -#: mod_configure.erl:143 mod_configure.erl:582 mod_configure.erl:1102 -msgid "Shut Down Service" -msgstr "ปิดการบริการ" - -#: mod_configure.erl:145 mod_configure.erl:518 mod_configure.erl:1197 -#: web/ejabberd_web_admin.erl:1527 -msgid "Add User" -msgstr "เพิ่มผู้ใช้" - -#: mod_configure.erl:147 mod_configure.erl:519 mod_configure.erl:1219 -msgid "Delete User" -msgstr "ลบผู้ใช้" - -#: mod_configure.erl:149 mod_configure.erl:520 mod_configure.erl:1231 -msgid "End User Session" -msgstr "สิ้นสุดเซสชันของผู้ใช้" - -#: mod_configure.erl:151 mod_configure.erl:521 mod_configure.erl:1243 -#: mod_configure.erl:1255 -msgid "Get User Password" -msgstr "ขอรับรหัสผ่านของผู้ใช้" - -#: mod_configure.erl:153 mod_configure.erl:522 -msgid "Change User Password" -msgstr "เปลี่ยนรหัสผ่านของผู้ใช้" - -#: mod_configure.erl:155 mod_configure.erl:523 mod_configure.erl:1272 -msgid "Get User Last Login Time" -msgstr "แสดงเวลาเข้าสู่ระบบครั้งล่าสุดของผู้ใช้" - -#: mod_configure.erl:157 mod_configure.erl:524 mod_configure.erl:1284 -msgid "Get User Statistics" -msgstr "แสดงสถิติของผู้ใช้" - -#: mod_configure.erl:159 mod_configure.erl:525 -msgid "Get Number of Registered Users" -msgstr "แสดงจำนวนผู้ใช้ที่ลงทะเบียน" - -#: mod_configure.erl:161 mod_configure.erl:526 -msgid "Get Number of Online Users" -msgstr "แสดงจำนวนผู้ใช้ออนไลน์" - -#: mod_configure.erl:163 mod_configure.erl:509 web/ejabberd_web_admin.erl:831 -#: web/ejabberd_web_admin.erl:872 -msgid "Access Control Lists" -msgstr "รายการควบคุมการเข้าถึง" - -#: mod_configure.erl:165 mod_configure.erl:510 web/ejabberd_web_admin.erl:940 -#: web/ejabberd_web_admin.erl:976 -msgid "Access Rules" -msgstr "กฎการเข้าถึง" - -#: mod_configure.erl:297 mod_configure.erl:499 -msgid "User Management" -msgstr "การจัดการผู้ใช้" - -#: mod_configure.erl:500 web/ejabberd_web_admin.erl:1058 -#: web/ejabberd_web_admin.erl:1462 -msgid "Online Users" -msgstr "ผู้ใช้ออนไลน์" - -#: mod_configure.erl:501 -msgid "All Users" -msgstr "ผู้ใช้ทั้งหมด" - -#: mod_configure.erl:502 -msgid "Outgoing s2s Connections" -msgstr "การเชื่อมต่อ s2s ขาออก" - -#: mod_configure.erl:503 web/ejabberd_web_admin.erl:1907 -msgid "Running Nodes" -msgstr "โหนดที่ทำงาน" - -#: mod_configure.erl:504 web/ejabberd_web_admin.erl:1909 -msgid "Stopped Nodes" -msgstr "โหนดที่หยุด" - -#: mod_configure.erl:577 mod_configure.erl:587 web/ejabberd_web_admin.erl:1953 -msgid "Modules" -msgstr "โมดูล" - -#: mod_configure.erl:578 -msgid "Backup Management" -msgstr "การจัดการข้อมูลสำรอง" - -#: mod_configure.erl:579 -msgid "Import Users From jabberd14 Spool Files" -msgstr "อิมพอร์ตผู้ใช้จากไฟล์เก็บพักข้อมูล jabberd14" - -#: mod_configure.erl:699 -msgid "To ~s" -msgstr "ถึง ~s" - -#: mod_configure.erl:717 -msgid "From ~s" -msgstr "จาก ~s" - -#: mod_configure.erl:913 -msgid "Database Tables Configuration at " -msgstr "การกำหนดค่าตารางฐานข้อมูลที่" - -#: mod_configure.erl:918 -msgid "Choose storage type of tables" -msgstr "เลือกชนิดการจัดเก็บของตาราง" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Disc only copy" -msgstr "คัดลอกเฉพาะดิสก์" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM and disc copy" -msgstr "คัดลอก RAM และดิสก์" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM copy" -msgstr "คัดลอก RAM" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Remote copy" -msgstr "คัดลอกระยะไกล" - -#: mod_configure.erl:950 -msgid "Stop Modules at " -msgstr "หยุดโมดูลที่" - -#: mod_configure.erl:954 -msgid "Choose modules to stop" -msgstr "เลือกโมดูลเพื่อหยุดการทำงาน" - -#: mod_configure.erl:969 -msgid "Start Modules at " -msgstr "เริ่มโมดูลที่" - -#: mod_configure.erl:973 -msgid "Enter list of {Module, [Options]}" -msgstr "ป้อนรายการของ {โมดูล, [ตัวเลือก]}" - -#: mod_configure.erl:974 -msgid "List of modules to start" -msgstr "รายการของโมดูลที่จะเริ่มการทำงาน" - -#: mod_configure.erl:983 -msgid "Backup to File at " -msgstr "สำรองไฟล์ข้อมูลที่" - -#: mod_configure.erl:987 mod_configure.erl:1001 -msgid "Enter path to backup file" -msgstr "ป้อนพาธเพื่อสำรองไฟล์ข้อมูล" - -#: mod_configure.erl:988 mod_configure.erl:1002 mod_configure.erl:1016 -#: mod_configure.erl:1030 -msgid "Path to File" -msgstr "พาธของไฟล์ข้อมูล" - -#: mod_configure.erl:997 -msgid "Restore Backup from File at " -msgstr "คืนค่าการสำรองข้อมูลจากไฟล์ที่" - -#: mod_configure.erl:1011 -msgid "Dump Backup to Text File at " -msgstr "ถ่ายโอนการสำรองข้อมูลไปยังไฟล์ข้อความที่" - -#: mod_configure.erl:1015 -msgid "Enter path to text file" -msgstr "ป้อนพาธของไฟล์ข้อความ" - -#: mod_configure.erl:1025 -msgid "Import User from File at " -msgstr "อิมพอร์ตผู้ใช้จากไฟล์ที่" - -#: mod_configure.erl:1029 -msgid "Enter path to jabberd14 spool file" -msgstr "ป้อนพาธไปยังไฟล์เก็บพักข้อมูล jabberd14" - -#: mod_configure.erl:1039 -msgid "Import Users from Dir at " -msgstr "อิมพอร์ตผู้ใช้จาก Dir ที่" - -#: mod_configure.erl:1043 -msgid "Enter path to jabberd14 spool dir" -msgstr "ป้อนพาธไปยัง jabberd14 spool dir" - -#: mod_configure.erl:1044 -msgid "Path to Dir" -msgstr "พาธไปยัง Dir" - -#: mod_configure.erl:1060 mod_configure.erl:1105 -msgid "Time delay" -msgstr "การหน่วงเวลา" - -#: mod_configure.erl:1143 -msgid "Access Control List Configuration" -msgstr "การกำหนดค่ารายการควบคุมการเข้าถึง" - -#: mod_configure.erl:1147 -msgid "Access control lists" -msgstr "รายการควบคุมการเข้าถึง" - -#: mod_configure.erl:1171 -msgid "Access Configuration" -msgstr "การกำหนดค่าการเข้าถึง" - -#: mod_configure.erl:1175 -msgid "Access rules" -msgstr "กฎการเข้าถึง" - -#: mod_configure.erl:1200 mod_configure.erl:1222 mod_configure.erl:1234 -#: mod_configure.erl:1246 mod_configure.erl:1258 mod_configure.erl:1275 -#: mod_configure.erl:1287 mod_configure.erl:1650 mod_configure.erl:1700 -#: mod_configure.erl:1721 mod_roster.erl:943 mod_roster_odbc.erl:1060 -#: mod_vcard.erl:472 mod_vcard_ldap.erl:554 mod_vcard_odbc.erl:448 -msgid "Jabber ID" -msgstr "Jabber ID" - -#: mod_configure.erl:1205 mod_configure.erl:1263 mod_configure.erl:1651 -#: mod_configure.erl:1863 mod_muc/mod_muc_room.erl:3224 mod_register.erl:235 -#: web/ejabberd_web_admin.erl:1520 -msgid "Password" -msgstr "รหัสผ่าน" - -#: mod_configure.erl:1210 -msgid "Password Verification" -msgstr "การตรวจสอบรหัสผ่าน" - -#: mod_configure.erl:1301 -msgid "Number of registered users" -msgstr "จำนวนผู้ใช้ที่ลงทะเบียน" - -#: mod_configure.erl:1315 -msgid "Number of online users" -msgstr "จำนวนผู้ใช้ออนไลน์" - -#: mod_configure.erl:1682 web/ejabberd_web_admin.erl:1588 -#: web/ejabberd_web_admin.erl:1743 -msgid "Never" -msgstr "ไม่เคย" - -#: mod_configure.erl:1696 web/ejabberd_web_admin.erl:1601 -#: web/ejabberd_web_admin.erl:1756 -msgid "Online" -msgstr "ออนไลน์" - -#: mod_configure.erl:1701 -msgid "Last login" -msgstr "การเข้าสู่ระบบครั้งล่าสุด" - -#: mod_configure.erl:1722 -msgid "Roster size" -msgstr "ขนาดของบัญชีรายชื่อ" - -#: mod_configure.erl:1723 -msgid "IP addresses" -msgstr "ที่อยู่ IP" - -#: mod_configure.erl:1724 -msgid "Resources" -msgstr "ทรัพยากร" - -#: mod_configure.erl:1850 -msgid "Administration of " -msgstr "การดูแล " - -#: mod_configure.erl:1853 -msgid "Action on user" -msgstr "การดำเนินการกับผู้ใช้" - -#: mod_configure.erl:1857 -msgid "Edit Properties" -msgstr "แก้ไขคุณสมบัติ" - -#: mod_configure.erl:1860 web/ejabberd_web_admin.erl:1769 -msgid "Remove User" -msgstr "ลบผู้ใช้" - -#: mod_irc/mod_irc.erl:201 mod_irc/mod_irc_odbc.erl:196 -#: mod_muc/mod_muc.erl:336 mod_muc/mod_muc_odbc.erl:342 -msgid "Access denied by service policy" -msgstr "การเข้าถึงถูกปฏิเสธโดยนโยบายการบริการ" - -#: mod_irc/mod_irc.erl:401 mod_irc/mod_irc_odbc.erl:398 -msgid "IRC Transport" -msgstr "การส่ง IRC" - -#: mod_irc/mod_irc.erl:428 mod_irc/mod_irc_odbc.erl:425 -msgid "ejabberd IRC module" -msgstr "ejabberd IRC module" - -#: mod_irc/mod_irc.erl:559 mod_irc/mod_irc_odbc.erl:568 -msgid "You need an x:data capable client to configure mod_irc settings" -msgstr "คุณต้องใช้ไคลเอ็นต์ที่รองรับ x:data เพื่อกำหนดการตั้งค่า mod_irc" - -#: mod_irc/mod_irc.erl:566 mod_irc/mod_irc_odbc.erl:575 -msgid "Registration in mod_irc for " -msgstr "การลงทะเบียนใน mod_irc สำหรับ" - -#: mod_irc/mod_irc.erl:571 mod_irc/mod_irc_odbc.erl:580 -#, fuzzy -msgid "" -"Enter username, encodings, ports and passwords you wish to use for " -"connecting to IRC servers" -msgstr "ป้อนชื่อผู้ใช้และการเข้ารหัสที่คุณต้องการใช้สำหรับเชื่อมต่อกับเซิร์ฟเวอร์ IRC" - -#: mod_irc/mod_irc.erl:576 mod_irc/mod_irc_odbc.erl:585 -msgid "IRC Username" -msgstr "ชื่อผู้ใช้ IRC" - -#: mod_irc/mod_irc.erl:586 mod_irc/mod_irc_odbc.erl:595 -#, fuzzy -msgid "" -"If you want to specify different ports, passwords, encodings for IRC " -"servers, fill this list with values in format '{\"irc server\", \"encoding" -"\", port, \"password\"}'. By default this service use \"~s\" encoding, port " -"~p, empty password." -msgstr "" -"ถ้าคุณต้องการระบุการเข้ารหัสที่ต่างกันสำหรับเซิร์ฟเวอร์ IRC ให้กรอกค่าโดยใช้รูปแบบ '{\"irc " -"server\", \"encoding\"}' ลงในรายการ การบริการนี้ใช้การเข้ารหัสในรูปแบบ \"~s\" " -"โดยค่าดีฟอลต์ " - -#: mod_irc/mod_irc.erl:598 mod_irc/mod_irc_odbc.erl:607 -#, fuzzy -msgid "" -"Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef." -"net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]." -msgstr "" -"ตัวอย่าง: [{\"irc.lucky.net\", \"koi8-r\"}, {\"vendetta.fef.net\", " -"\"iso8859-1\"}]." - -#: mod_irc/mod_irc.erl:603 mod_irc/mod_irc_odbc.erl:612 -msgid "Connections parameters" -msgstr "" - -#: mod_irc/mod_irc.erl:728 mod_irc/mod_irc_odbc.erl:757 -msgid "Join IRC channel" -msgstr "" - -#: mod_irc/mod_irc.erl:732 mod_irc/mod_irc_odbc.erl:761 -msgid "IRC channel (don't put the first #)" -msgstr "" - -#: mod_irc/mod_irc.erl:737 mod_irc/mod_irc_odbc.erl:766 -#, fuzzy -msgid "IRC server" -msgstr "ชื่อผู้ใช้ IRC" - -#: mod_irc/mod_irc.erl:770 mod_irc/mod_irc.erl:774 -#: mod_irc/mod_irc_odbc.erl:799 mod_irc/mod_irc_odbc.erl:803 -msgid "Join the IRC channel here." -msgstr "" - -#: mod_irc/mod_irc.erl:778 mod_irc/mod_irc_odbc.erl:807 -msgid "Join the IRC channel in this Jabber ID: ~s" -msgstr "" - -#: mod_irc/mod_irc.erl:863 mod_irc/mod_irc_odbc.erl:904 -msgid "IRC settings" -msgstr "" - -#: mod_irc/mod_irc.erl:868 mod_irc/mod_irc_odbc.erl:909 -#, fuzzy -msgid "" -"Enter username and encodings you wish to use for connecting to IRC servers. " -"Press 'Next' to get more fields to fill in. Press 'Complete' to save " -"settings." -msgstr "ป้อนชื่อผู้ใช้และการเข้ารหัสที่คุณต้องการใช้สำหรับเชื่อมต่อกับเซิร์ฟเวอร์ IRC" - -#: mod_irc/mod_irc.erl:874 mod_irc/mod_irc_odbc.erl:915 -#, fuzzy -msgid "IRC username" -msgstr "ชื่อผู้ใช้ IRC" - -#: mod_irc/mod_irc.erl:923 mod_irc/mod_irc_odbc.erl:964 -#, fuzzy -msgid "Password ~b" -msgstr "รหัสผ่าน" - -#: mod_irc/mod_irc.erl:928 mod_irc/mod_irc_odbc.erl:969 -#, fuzzy -msgid "Port ~b" -msgstr "พอร์ท" - -#: mod_irc/mod_irc.erl:933 mod_irc/mod_irc_odbc.erl:974 -msgid "Encoding for server ~b" -msgstr "" - -#: mod_irc/mod_irc.erl:942 mod_irc/mod_irc_odbc.erl:983 -msgid "Server ~b" -msgstr "" - -#: mod_muc/mod_muc.erl:449 mod_muc/mod_muc_odbc.erl:456 -msgid "Only service administrators are allowed to send service messages" -msgstr "ผู้ดูแลด้านการบริการเท่านั้นที่ได้รับอนุญาตให้ส่งข้อความการบริการ" - -#: mod_muc/mod_muc.erl:493 mod_muc/mod_muc_odbc.erl:500 -msgid "Room creation is denied by service policy" -msgstr "การสร้างห้องสนทนาถูกปฏิเสธโดยนโยบายการบริการ" - -#: mod_muc/mod_muc.erl:500 mod_muc/mod_muc_odbc.erl:507 -msgid "Conference room does not exist" -msgstr "ไม่มีห้องประชุม" - -#: mod_muc/mod_muc.erl:582 mod_muc/mod_muc_odbc.erl:595 -msgid "Chatrooms" -msgstr "ห้องสนทนา" - -#: mod_muc/mod_muc.erl:712 mod_muc/mod_muc_odbc.erl:729 -#, fuzzy -msgid "You need a client that supports x:data to register the nickname" -msgstr "คุณต้องใช้ไคลเอ็นต์ที่รองรับ x:data เพื่อลงทะเบียนชื่อเล่น" - -#: mod_muc/mod_muc.erl:718 mod_muc/mod_muc_odbc.erl:735 -msgid "Nickname Registration at " -msgstr "การลงทะเบียนชื่อเล่นที่ " - -#: mod_muc/mod_muc.erl:722 mod_muc/mod_muc_odbc.erl:739 -msgid "Enter nickname you want to register" -msgstr "ป้อนชื่อเล่นที่คุณต้องการลงทะเบียน" - -#: mod_muc/mod_muc.erl:723 mod_muc/mod_muc_odbc.erl:740 -#: mod_muc/mod_muc_room.erl:3810 mod_roster.erl:944 mod_roster_odbc.erl:1061 -#: mod_vcard.erl:364 mod_vcard.erl:477 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:453 -msgid "Nickname" -msgstr "ชื่อเล่น" - -#: mod_muc/mod_muc.erl:762 mod_muc/mod_muc_odbc.erl:784 -#: mod_muc/mod_muc_room.erl:1064 mod_muc/mod_muc_room.erl:1787 -#, fuzzy -msgid "That nickname is registered by another person" -msgstr "ชื่อเล่นถูกลงทะเบียนใช้งานโดยบุคคลอื่น" - -#: mod_muc/mod_muc.erl:788 mod_muc/mod_muc_odbc.erl:811 -msgid "You must fill in field \"Nickname\" in the form" -msgstr "คุณต้องกรอกฟิลด์ \"Nickname\" ในแบบฟอร์ม" - -#: mod_muc/mod_muc.erl:808 mod_muc/mod_muc_odbc.erl:831 -msgid "ejabberd MUC module" -msgstr "ejabberd MUC module" - -#: mod_muc/mod_muc_log.erl:374 mod_muc/mod_muc_log.erl:381 -msgid "Chatroom configuration modified" -msgstr "มีการปรับเปลี่ยนการกำหนดค่าของห้องสนทนา" - -#: mod_muc/mod_muc_log.erl:384 -msgid "joins the room" -msgstr "เข้าห้องสนทนานี้" - -#: mod_muc/mod_muc_log.erl:387 mod_muc/mod_muc_log.erl:390 -msgid "leaves the room" -msgstr "ออกจากห้อง" - -#: mod_muc/mod_muc_log.erl:393 mod_muc/mod_muc_log.erl:396 -msgid "has been banned" -msgstr "ถูกสั่งห้าม" - -#: mod_muc/mod_muc_log.erl:399 mod_muc/mod_muc_log.erl:402 -msgid "has been kicked" -msgstr "ถูกไล่ออก" - -#: mod_muc/mod_muc_log.erl:405 -msgid "has been kicked because of an affiliation change" -msgstr "" - -#: mod_muc/mod_muc_log.erl:408 -msgid "has been kicked because the room has been changed to members-only" -msgstr "" - -#: mod_muc/mod_muc_log.erl:411 -msgid "has been kicked because of a system shutdown" -msgstr "" - -#: mod_muc/mod_muc_log.erl:414 -msgid "is now known as" -msgstr "ซึ่งรู้จักกันในชื่อ" - -#: mod_muc/mod_muc_log.erl:417 mod_muc/mod_muc_log.erl:688 -#: mod_muc/mod_muc_room.erl:2393 -msgid " has set the subject to: " -msgstr " ตั้งหัวข้อว่า: " - -#: mod_muc/mod_muc_log.erl:452 -#, fuzzy -msgid "Chatroom is created" -msgstr "ห้องสนทนา" - -#: mod_muc/mod_muc_log.erl:453 -#, fuzzy -msgid "Chatroom is destroyed" -msgstr "ห้องสนทนา" - -#: mod_muc/mod_muc_log.erl:454 -#, fuzzy -msgid "Chatroom is started" -msgstr "ห้องสนทนา" - -#: mod_muc/mod_muc_log.erl:455 -#, fuzzy -msgid "Chatroom is stopped" -msgstr "ห้องสนทนา" - -#: mod_muc/mod_muc_log.erl:459 -msgid "Monday" -msgstr "วันจันทร์" - -#: mod_muc/mod_muc_log.erl:460 -msgid "Tuesday" -msgstr "วันอังคาร" - -#: mod_muc/mod_muc_log.erl:461 -msgid "Wednesday" -msgstr "วันพุธ" - -#: mod_muc/mod_muc_log.erl:462 -msgid "Thursday" -msgstr "วันพฤหัสบดี" - -#: mod_muc/mod_muc_log.erl:463 -msgid "Friday" -msgstr "วันศุกร์" - -#: mod_muc/mod_muc_log.erl:464 -msgid "Saturday" -msgstr "วันเสาร์" - -#: mod_muc/mod_muc_log.erl:465 -msgid "Sunday" -msgstr "วันอาทิตย์" - -#: mod_muc/mod_muc_log.erl:469 -msgid "January" -msgstr "มกราคม" - -#: mod_muc/mod_muc_log.erl:470 -msgid "February" -msgstr "กุมภาพันธ์" - -#: mod_muc/mod_muc_log.erl:471 -msgid "March" -msgstr "มีนาคม" - -#: mod_muc/mod_muc_log.erl:472 -msgid "April" -msgstr "เมษายน" - -#: mod_muc/mod_muc_log.erl:473 -msgid "May" -msgstr "พฤษภาคม" - -#: mod_muc/mod_muc_log.erl:474 -msgid "June" -msgstr "มิถุนายน" - -#: mod_muc/mod_muc_log.erl:475 -msgid "July" -msgstr "กรกฎาคม" - -#: mod_muc/mod_muc_log.erl:476 -msgid "August" -msgstr "สิงหาคม" - -#: mod_muc/mod_muc_log.erl:477 -msgid "September" -msgstr "กันยายน" - -#: mod_muc/mod_muc_log.erl:478 -msgid "October" -msgstr "ตุลาคม" - -#: mod_muc/mod_muc_log.erl:479 -msgid "November" -msgstr "พฤศจิกายน" - -#: mod_muc/mod_muc_log.erl:480 -msgid "December" -msgstr "ธันวาคม" - -#: mod_muc/mod_muc_log.erl:750 -msgid "Room Configuration" -msgstr "การกำหนดค่าห้องสนทนา" - -#: mod_muc/mod_muc_log.erl:759 -#, fuzzy -msgid "Room Occupants" -msgstr "จำนวนผู้ครอบครองห้อง" - -#: mod_muc/mod_muc_room.erl:174 -msgid "Traffic rate limit is exceeded" -msgstr "อัตราของปริมาณการเข้าใช้เกินขีดจำกัด" - -#: mod_muc/mod_muc_room.erl:246 -msgid "" -"This participant is kicked from the room because he sent an error message" -msgstr "" - -#: mod_muc/mod_muc_room.erl:255 -msgid "It is not allowed to send private messages to the conference" -msgstr "ไม่อนุญาตให้ส่งข้อความส่วนตัวไปยังห้องประชุม" - -#: mod_muc/mod_muc_room.erl:332 -msgid "Please, wait for a while before sending new voice request" -msgstr "" - -#: mod_muc/mod_muc_room.erl:347 -msgid "Voice requests are disabled in this conference" -msgstr "" - -#: mod_muc/mod_muc_room.erl:364 -msgid "Failed to extract JID from your voice request approval" -msgstr "" - -#: mod_muc/mod_muc_room.erl:393 -msgid "Only moderators can approve voice requests" -msgstr "" - -#: mod_muc/mod_muc_room.erl:408 -msgid "Improper message type" -msgstr "ประเภทข้อความไม่เหมาะสม" - -#: mod_muc/mod_muc_room.erl:518 -msgid "" -"This participant is kicked from the room because he sent an error message to " -"another participant" -msgstr "" - -#: mod_muc/mod_muc_room.erl:531 -msgid "It is not allowed to send private messages of type \"groupchat\"" -msgstr "ไม่อนุญาตให้ส่งข้อความส่วนตัวไปยัง \"กลุ่มสนทนา\"" - -#: mod_muc/mod_muc_room.erl:543 mod_muc/mod_muc_room.erl:611 -msgid "Recipient is not in the conference room" -msgstr "ผู้รับไม่ได้อยู่ในห้องประชุม" - -#: mod_muc/mod_muc_room.erl:564 mod_muc/mod_muc_room.erl:585 -#, fuzzy -msgid "It is not allowed to send private messages" -msgstr "ไม่อนุญาตให้ส่งข้อความส่วนตัวไปยังห้องประชุม" - -#: mod_muc/mod_muc_room.erl:576 mod_muc/mod_muc_room.erl:956 -#: mod_muc/mod_muc_room.erl:4040 -msgid "Only occupants are allowed to send messages to the conference" -msgstr "ผู้ครอบครองห้องเท่านั้นที่ได้รับอนุญาตให้ส่งข้อความไปยังห้องประชุม" - -#: mod_muc/mod_muc_room.erl:634 -msgid "Only occupants are allowed to send queries to the conference" -msgstr "ผู้ครอบครองห้องเท่านั้นที่ได้รับอนุญาตให้ส่งกระทู้ถามไปยังห้องประชุม" - -#: mod_muc/mod_muc_room.erl:646 -msgid "Queries to the conference members are not allowed in this room" -msgstr "ห้องนี้ไม่อนุญาตให้ส่งกระทู้ถามถึงสมาชิกในห้องประชุม" - -#: mod_muc/mod_muc_room.erl:932 -#, fuzzy -msgid "" -"Only moderators and participants are allowed to change the subject in this " -"room" -msgstr "ผู้ดูแลการสนทนาและผู้เข้าร่วมเท่านั้นที่ได้รับอนุญาตให้เปลี่ยนหัวข้อในห้องนี้" - -#: mod_muc/mod_muc_room.erl:937 -#, fuzzy -msgid "Only moderators are allowed to change the subject in this room" -msgstr "ผู้ดูแลการสนทนาเท่านั้นที่ได้รับอนุญาตให้เปลี่ยนหัวข้อในห้องนี้" - -#: mod_muc/mod_muc_room.erl:947 -msgid "Visitors are not allowed to send messages to all occupants" -msgstr "ผู้เยี่ยมเยือนไม่ได้รับอนุญาตให้ส่งข้อความถึงผู้ครอบครองห้องทั้งหมด" - -#: mod_muc/mod_muc_room.erl:1021 -msgid "" -"This participant is kicked from the room because he sent an error presence" -msgstr "" - -#: mod_muc/mod_muc_room.erl:1040 -#, fuzzy -msgid "Visitors are not allowed to change their nicknames in this room" -msgstr "ผู้ดูแลการสนทนาเท่านั้นที่ได้รับอนุญาตให้เปลี่ยนหัวข้อในห้องนี้" - -#: mod_muc/mod_muc_room.erl:1053 mod_muc/mod_muc_room.erl:1779 -#, fuzzy -msgid "That nickname is already in use by another occupant" -msgstr "ชื่อเล่นถูกใช้งานอยู่โดยผู้ครอบครองห้อง" - -#: mod_muc/mod_muc_room.erl:1768 -msgid "You have been banned from this room" -msgstr "คุณถูกสั่งห้ามไมให้เข้าห้องนี้" - -#: mod_muc/mod_muc_room.erl:1771 -#, fuzzy -msgid "Membership is required to enter this room" -msgstr "ต้องเป็นสมาชิกจึงจะสามารถเข้าห้องนี้ได้" - -#: mod_muc/mod_muc_room.erl:1807 -msgid "This room is not anonymous" -msgstr "ห้องนี้ไม่ปิดบังชื่อ" - -#: mod_muc/mod_muc_room.erl:1833 -#, fuzzy -msgid "A password is required to enter this room" -msgstr "ต้องใส่รหัสผ่านเพื่อเข้าห้องนี้" - -#: mod_muc/mod_muc_room.erl:1855 mod_register.erl:246 -msgid "Too many CAPTCHA requests" -msgstr "" - -#: mod_muc/mod_muc_room.erl:1864 mod_register.erl:251 -msgid "Unable to generate a CAPTCHA" -msgstr "" - -#: mod_muc/mod_muc_room.erl:1874 -msgid "Incorrect password" -msgstr "รหัสผ่านไม่ถูกต้อง" - -#: mod_muc/mod_muc_room.erl:2448 -msgid "Administrator privileges required" -msgstr "ต้องมีสิทธิพิเศษของผู้ดูแลระบบ" - -#: mod_muc/mod_muc_room.erl:2463 -msgid "Moderator privileges required" -msgstr "ต้องมีสิทธิพิเศษของผู้ดูแลการสนทนา" - -#: mod_muc/mod_muc_room.erl:2619 -msgid "Jabber ID ~s is invalid" -msgstr "Jabber ID ~s ไม่ถูกต้อง" - -#: mod_muc/mod_muc_room.erl:2633 -msgid "Nickname ~s does not exist in the room" -msgstr "ไม่มีชื่อเล่น ~s อยู่ในห้องนี้" - -#: mod_muc/mod_muc_room.erl:2659 mod_muc/mod_muc_room.erl:3049 -msgid "Invalid affiliation: ~s" -msgstr "การเข้าร่วมที่ไม่ถูกต้อง: ~s" - -#: mod_muc/mod_muc_room.erl:2713 -msgid "Invalid role: ~s" -msgstr "บทบาทไม่ถูกต้อง: ~s" - -#: mod_muc/mod_muc_room.erl:3026 mod_muc/mod_muc_room.erl:3062 -msgid "Owner privileges required" -msgstr "ต้องมีสิทธิพิเศษของเจ้าของ" - -#: mod_muc/mod_muc_room.erl:3195 -#, fuzzy -msgid "Configuration of room ~s" -msgstr "การกำหนดค่าสำหรับ " - -#: mod_muc/mod_muc_room.erl:3200 -msgid "Room title" -msgstr "ชื่อห้อง" - -#: mod_muc/mod_muc_room.erl:3203 mod_muc/mod_muc_room.erl:3692 -#, fuzzy -msgid "Room description" -msgstr "รายละเอียด:" - -#: mod_muc/mod_muc_room.erl:3210 -msgid "Make room persistent" -msgstr "สร้างเป็นห้องถาวร" - -#: mod_muc/mod_muc_room.erl:3215 -msgid "Make room public searchable" -msgstr "สร้างเป็นห้องที่บุคคลทั่วไปสามารถค้นหาได้" - -#: mod_muc/mod_muc_room.erl:3218 -msgid "Make participants list public" -msgstr "สร้างรายการผู้เข้าร่วมสำหรับใช้งานโดยบุคคลทั่วไป" - -#: mod_muc/mod_muc_room.erl:3221 -msgid "Make room password protected" -msgstr "สร้างห้องที่มีการป้องกันด้วยรหัสผ่าน" - -#: mod_muc/mod_muc_room.erl:3232 -msgid "Maximum Number of Occupants" -msgstr "จำนวนผู้ครอบครองห้องสูงสุด" - -#: mod_muc/mod_muc_room.erl:3239 -msgid "No limit" -msgstr "ไม่จำกัด" - -#: mod_muc/mod_muc_room.erl:3250 -msgid "Present real Jabber IDs to" -msgstr "แสดง Jabber IDs ที่ถูกต้องแก่" - -#: mod_muc/mod_muc_room.erl:3258 mod_muc/mod_muc_room.erl:3292 -msgid "moderators only" -msgstr "สำหรับผู้ดูแลการสนทนาเท่านั้น" - -#: mod_muc/mod_muc_room.erl:3260 mod_muc/mod_muc_room.erl:3294 -msgid "anyone" -msgstr "ทุกคน" - -#: mod_muc/mod_muc_room.erl:3262 -msgid "Make room members-only" -msgstr "สร้างห้องสำหรับสมาชิกเท่านั้น" - -#: mod_muc/mod_muc_room.erl:3265 -#, fuzzy -msgid "Make room moderated" -msgstr "สร้างเป็นห้องถาวร" - -#: mod_muc/mod_muc_room.erl:3268 -msgid "Default users as participants" -msgstr "ผู้ใช้เริ่มต้นเป็นผู้เข้าร่วม" - -#: mod_muc/mod_muc_room.erl:3271 -#, fuzzy -msgid "Allow users to change the subject" -msgstr "อนุญาตให้ผู้ใช้เปลี่ยนหัวข้อได้" - -#: mod_muc/mod_muc_room.erl:3274 -msgid "Allow users to send private messages" -msgstr "อนุญาตให้ผู้ใช้ส่งข้อความส่วนตัว" - -#: mod_muc/mod_muc_room.erl:3279 -#, fuzzy -msgid "Allow visitors to send private messages to" -msgstr "อนุญาตให้ผู้ใช้ส่งข้อความส่วนตัว" - -#: mod_muc/mod_muc_room.erl:3290 -msgid "nobody" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3296 -msgid "Allow users to query other users" -msgstr "อนุญาตให้ผู้ใช้ถามคำถามกับผู้ใช้คนอื่นๆ ได้" - -#: mod_muc/mod_muc_room.erl:3299 -msgid "Allow users to send invites" -msgstr "อนุญาตให้ผู้ใช้ส่งคำเชิญถึงกันได้" - -#: mod_muc/mod_muc_room.erl:3302 -#, fuzzy -msgid "Allow visitors to send status text in presence updates" -msgstr "อนุญาตให้ผู้ใช้ส่งข้อความส่วนตัว" - -#: mod_muc/mod_muc_room.erl:3305 -#, fuzzy -msgid "Allow visitors to change nickname" -msgstr "อนุญาตให้ผู้ใช้เปลี่ยนหัวข้อได้" - -#: mod_muc/mod_muc_room.erl:3308 -#, fuzzy -msgid "Allow visitors to send voice requests" -msgstr "อนุญาตให้ผู้ใช้ส่งคำเชิญถึงกันได้" - -#: mod_muc/mod_muc_room.erl:3311 -msgid "Minimum interval between voice requests (in seconds)" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3317 -#, fuzzy -msgid "Make room CAPTCHA protected" -msgstr "สร้างห้องที่มีการป้องกันด้วยรหัสผ่าน" - -#: mod_muc/mod_muc_room.erl:3322 -msgid "Exclude Jabber IDs from CAPTCHA challenge" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3329 -msgid "Enable logging" -msgstr "เปิดใช้งานการบันทึก" - -#: mod_muc/mod_muc_room.erl:3337 -msgid "You need an x:data capable client to configure room" -msgstr "คุณต้องใช้ไคลเอ็นต์ที่รองรับ x:data เพื่อกำหนดค่าห้องสนทนา " - -#: mod_muc/mod_muc_room.erl:3694 -msgid "Number of occupants" -msgstr "จำนวนผู้ครอบครองห้อง" - -#: mod_muc/mod_muc_room.erl:3750 -msgid "private, " -msgstr "ส่วนตัว, " - -#: mod_muc/mod_muc_room.erl:3799 -msgid "Voice request" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3803 -msgid "Either approve or decline the voice request." -msgstr "" - -#: mod_muc/mod_muc_room.erl:3809 -#, fuzzy -msgid "User JID" -msgstr "ผู้ใช้" - -#: mod_muc/mod_muc_room.erl:3811 -msgid "Grant voice to this person?" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3960 -msgid "~s invites you to the room ~s" -msgstr "~s เชิญคุณเข้าร่วมสนทนาในห้อง ~s" - -#: mod_muc/mod_muc_room.erl:3969 -msgid "the password is" -msgstr "รหัสผ่านคือ" - -#: mod_offline.erl:510 mod_offline_odbc.erl:352 -msgid "" -"Your contact offline message queue is full. The message has been discarded." -msgstr "ลำดับข้อความออฟไลน์ของผู้ที่ติดต่อของคุณเต็มแล้ว ข้อความถูกลบทิ้งแล้ว" - -#: mod_offline.erl:560 mod_offline_odbc.erl:408 -msgid "~s's Offline Messages Queue" -msgstr "~s's ลำดับข้อความออฟไลน์" - -#: mod_offline.erl:563 mod_offline_odbc.erl:411 mod_roster.erl:987 -#: mod_roster_odbc.erl:1104 mod_shared_roster.erl:901 -#: mod_shared_roster.erl:1010 mod_shared_roster_odbc.erl:930 -#: mod_shared_roster_odbc.erl:1039 web/ejabberd_web_admin.erl:833 -#: web/ejabberd_web_admin.erl:874 web/ejabberd_web_admin.erl:942 -#: web/ejabberd_web_admin.erl:978 web/ejabberd_web_admin.erl:1019 -#: web/ejabberd_web_admin.erl:1508 web/ejabberd_web_admin.erl:1760 -#: web/ejabberd_web_admin.erl:1931 web/ejabberd_web_admin.erl:1963 -#: web/ejabberd_web_admin.erl:2031 web/ejabberd_web_admin.erl:2135 -#: web/ejabberd_web_admin.erl:2160 web/ejabberd_web_admin.erl:2248 -msgid "Submitted" -msgstr "ส่งแล้ว" - -#: mod_offline.erl:571 -msgid "Time" -msgstr "เวลา" - -#: mod_offline.erl:572 -msgid "From" -msgstr "จาก" - -#: mod_offline.erl:573 -msgid "To" -msgstr "ถึง" - -#: mod_offline.erl:574 mod_offline_odbc.erl:419 -msgid "Packet" -msgstr "แพ็กเก็ต" - -#: mod_offline.erl:587 mod_offline_odbc.erl:432 mod_shared_roster.erl:908 -#: mod_shared_roster_odbc.erl:937 web/ejabberd_web_admin.erl:882 -#: web/ejabberd_web_admin.erl:986 -msgid "Delete Selected" -msgstr "ลบข้อความที่เลือก" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -msgid "Offline Messages:" -msgstr "ข้อความออฟไลน์:" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -#, fuzzy -msgid "Remove All Offline Messages" -msgstr "ข้อความออฟไลน์" - -#: mod_proxy65/mod_proxy65_service.erl:213 -msgid "ejabberd SOCKS5 Bytestreams module" -msgstr "ejabberd SOCKS5 Bytestreams module" - -#: mod_pubsub/mod_pubsub.erl:1120 mod_pubsub/mod_pubsub_odbc.erl:929 -msgid "Publish-Subscribe" -msgstr "เผยแพร่-สมัครเข้าใช้งาน" - -#: mod_pubsub/mod_pubsub.erl:1214 mod_pubsub/mod_pubsub_odbc.erl:1025 -msgid "ejabberd Publish-Subscribe module" -msgstr "ejabberd Publish-Subscribe module" - -#: mod_pubsub/mod_pubsub.erl:1497 mod_pubsub/mod_pubsub_odbc.erl:1312 -msgid "PubSub subscriber request" -msgstr "คำร้องขอของผู้สมัครเข้าใช้งาน PubSub" - -#: mod_pubsub/mod_pubsub.erl:1499 mod_pubsub/mod_pubsub_odbc.erl:1314 -msgid "Choose whether to approve this entity's subscription." -msgstr "เลือกว่าจะอนุมัติการสมัครเข้าใช้งานของเอนทิตี้นี้หรือไม่" - -#: mod_pubsub/mod_pubsub.erl:1505 mod_pubsub/mod_pubsub_odbc.erl:1320 -msgid "Node ID" -msgstr "ID โหนด" - -#: mod_pubsub/mod_pubsub.erl:1510 mod_pubsub/mod_pubsub_odbc.erl:1325 -msgid "Subscriber Address" -msgstr "ที่อยู่ของผู้สมัคร" - -#: mod_pubsub/mod_pubsub.erl:1516 mod_pubsub/mod_pubsub_odbc.erl:1331 -msgid "Allow this Jabber ID to subscribe to this pubsub node?" -msgstr "อนุญาตให้ Jabber ID นี้เข้าร่วมเป็นสมาชิกของโหนด pubsub หรือไม่" - -#: mod_pubsub/mod_pubsub.erl:3474 mod_pubsub/mod_pubsub_odbc.erl:3287 -msgid "Deliver payloads with event notifications" -msgstr "ส่งส่วนของข้อมูล (payload) พร้อมกับการแจ้งเตือนเหตุการณ์" - -#: mod_pubsub/mod_pubsub.erl:3475 mod_pubsub/mod_pubsub_odbc.erl:3288 -msgid "Deliver event notifications" -msgstr "ส่งการแจ้งเตือนเหตุการณ์" - -#: mod_pubsub/mod_pubsub.erl:3476 mod_pubsub/mod_pubsub_odbc.erl:3289 -msgid "Notify subscribers when the node configuration changes" -msgstr "แจ้งเตือนผู้สมัครสมาชิกเมื่อการกำหนดค่าโหนดเปลี่ยนแปลง" - -#: mod_pubsub/mod_pubsub.erl:3477 mod_pubsub/mod_pubsub_odbc.erl:3290 -msgid "Notify subscribers when the node is deleted" -msgstr "แจ้งเตือนผู้สมัครสมาชิกเมื่อโหนดถูกลบ" - -#: mod_pubsub/mod_pubsub.erl:3478 mod_pubsub/mod_pubsub_odbc.erl:3291 -msgid "Notify subscribers when items are removed from the node" -msgstr "แจ้งเตือนผู้สมัครสมาชิกเมื่อรายการถูกลบออกจากโหนด" - -#: mod_pubsub/mod_pubsub.erl:3479 mod_pubsub/mod_pubsub_odbc.erl:3292 -msgid "Persist items to storage" -msgstr "ยืนยันรายการที่จะจัดเก็บ" - -#: mod_pubsub/mod_pubsub.erl:3480 mod_pubsub/mod_pubsub_odbc.erl:3293 -msgid "A friendly name for the node" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:3481 mod_pubsub/mod_pubsub_odbc.erl:3294 -msgid "Max # of items to persist" -msgstr "จำนวนสูงสุดของรายการที่ยืนยัน" - -#: mod_pubsub/mod_pubsub.erl:3482 mod_pubsub/mod_pubsub_odbc.erl:3295 -msgid "Whether to allow subscriptions" -msgstr "อนุญาตให้เข้าร่วมเป็นสมาชิกหรือไม่" - -#: mod_pubsub/mod_pubsub.erl:3483 mod_pubsub/mod_pubsub_odbc.erl:3296 -msgid "Specify the access model" -msgstr "ระบุโมเดลการเข้าถึง" - -#: mod_pubsub/mod_pubsub.erl:3486 mod_pubsub/mod_pubsub_odbc.erl:3299 -msgid "Roster groups allowed to subscribe" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:3487 mod_pubsub/mod_pubsub_odbc.erl:3300 -msgid "Specify the publisher model" -msgstr "ระบุโมเดลผู้เผยแพร่" - -#: mod_pubsub/mod_pubsub.erl:3489 mod_pubsub/mod_pubsub_odbc.erl:3302 -msgid "Purge all items when the relevant publisher goes offline" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:3490 mod_pubsub/mod_pubsub_odbc.erl:3303 -#, fuzzy -msgid "Specify the event message type" -msgstr "ระบุโมเดลการเข้าถึง" - -#: mod_pubsub/mod_pubsub.erl:3492 mod_pubsub/mod_pubsub_odbc.erl:3305 -msgid "Max payload size in bytes" -msgstr "ขนาดสูงสุดของส่วนของข้อมูล (payload) มีหน่วยเป็นไบต์" - -#: mod_pubsub/mod_pubsub.erl:3493 mod_pubsub/mod_pubsub_odbc.erl:3306 -msgid "When to send the last published item" -msgstr "เวลาที่ส่งรายการที่เผยแพร่ครั้งล่าสุด" - -#: mod_pubsub/mod_pubsub.erl:3495 mod_pubsub/mod_pubsub_odbc.erl:3308 -msgid "Only deliver notifications to available users" -msgstr "ส่งการแจ้งเตือนถึงผู้ใช้ที่สามารถติดต่อได้เท่านั้น" - -#: mod_pubsub/mod_pubsub.erl:3496 mod_pubsub/mod_pubsub_odbc.erl:3309 -msgid "The collections with which a node is affiliated" -msgstr "" - -#: mod_register.erl:193 -msgid "The CAPTCHA verification has failed" -msgstr "" - -#: mod_register.erl:220 -#, fuzzy -msgid "You need a client that supports x:data and CAPTCHA to register" -msgstr "คุณต้องใช้ไคลเอ็นต์ที่รองรับ x:data เพื่อลงทะเบียนชื่อเล่น" - -#: mod_register.erl:226 mod_register.erl:265 -msgid "Choose a username and password to register with this server" -msgstr "เลือกชื่อผู้ใช้และรหัสผ่านเพื่อลงทะเบียนกับเซิร์ฟเวอร์นี้" - -#: mod_register.erl:230 mod_vcard.erl:364 mod_vcard_odbc.erl:342 -#: web/ejabberd_web_admin.erl:1515 web/ejabberd_web_admin.erl:1572 -msgid "User" -msgstr "ผู้ใช้" - -#: mod_register.erl:316 mod_register.erl:361 -#, fuzzy -msgid "The password is too weak" -msgstr "รหัสผ่านคือ" - -#: mod_register.erl:365 -#, fuzzy -msgid "Users are not allowed to register accounts so quickly" -msgstr "ผู้เยี่ยมเยือนไม่ได้รับอนุญาตให้ส่งข้อความถึงผู้ครอบครองห้องทั้งหมด" - -#: mod_roster.erl:938 mod_roster_odbc.erl:1055 web/ejabberd_web_admin.erl:1701 -#: web/ejabberd_web_admin.erl:1886 web/ejabberd_web_admin.erl:1897 -#: web/ejabberd_web_admin.erl:2219 -msgid "None" -msgstr "ไม่มี" - -#: mod_roster.erl:945 mod_roster_odbc.erl:1062 -msgid "Subscription" -msgstr "การสมัครสมาชิก" - -#: mod_roster.erl:946 mod_roster_odbc.erl:1063 -msgid "Pending" -msgstr "ค้างอยู่" - -#: mod_roster.erl:947 mod_roster_odbc.erl:1064 -msgid "Groups" -msgstr "กลุ่ม" - -#: mod_roster.erl:974 mod_roster_odbc.erl:1091 -msgid "Validate" -msgstr "ตรวจสอบ" - -#: mod_roster.erl:982 mod_roster_odbc.erl:1099 -msgid "Remove" -msgstr "ลบ" - -#: mod_roster.erl:985 mod_roster_odbc.erl:1102 -msgid "Roster of " -msgstr "บัญชีรายชื่อของ " - -#: mod_roster.erl:988 mod_roster_odbc.erl:1105 mod_shared_roster.erl:902 -#: mod_shared_roster.erl:1011 mod_shared_roster_odbc.erl:931 -#: mod_shared_roster_odbc.erl:1040 web/ejabberd_web_admin.erl:834 -#: web/ejabberd_web_admin.erl:875 web/ejabberd_web_admin.erl:943 -#: web/ejabberd_web_admin.erl:979 web/ejabberd_web_admin.erl:1020 -#: web/ejabberd_web_admin.erl:1509 web/ejabberd_web_admin.erl:1761 -#: web/ejabberd_web_admin.erl:1932 web/ejabberd_web_admin.erl:2136 -#: web/ejabberd_web_admin.erl:2161 -msgid "Bad format" -msgstr "รูปแบบที่ไม่ถูกต้อง" - -#: mod_roster.erl:995 mod_roster_odbc.erl:1112 -msgid "Add Jabber ID" -msgstr "เพิ่ม Jabber ID" - -#: mod_roster.erl:1094 mod_roster_odbc.erl:1211 -msgid "Roster" -msgstr "บัญชีรายชื่อ" - -#: mod_shared_roster.erl:857 mod_shared_roster.erl:899 -#: mod_shared_roster.erl:1007 mod_shared_roster_odbc.erl:886 -#: mod_shared_roster_odbc.erl:928 mod_shared_roster_odbc.erl:1036 -msgid "Shared Roster Groups" -msgstr "กลุ่มบัญชีรายชื่อที่ใช้งานร่วมกัน" - -#: mod_shared_roster.erl:895 mod_shared_roster_odbc.erl:924 -#: web/ejabberd_web_admin.erl:1365 web/ejabberd_web_admin.erl:2461 -msgid "Add New" -msgstr "เพิ่มผู้ใช้ใหม่" - -#: mod_shared_roster.erl:978 mod_shared_roster_odbc.erl:1007 -msgid "Name:" -msgstr "ชื่อ:" - -#: mod_shared_roster.erl:983 mod_shared_roster_odbc.erl:1012 -msgid "Description:" -msgstr "รายละเอียด:" - -#: mod_shared_roster.erl:991 mod_shared_roster_odbc.erl:1020 -msgid "Members:" -msgstr "สมาชิก:" - -#: mod_shared_roster.erl:999 mod_shared_roster_odbc.erl:1028 -msgid "Displayed Groups:" -msgstr "กลุ่มที่แสดง:" - -#: mod_shared_roster.erl:1008 mod_shared_roster_odbc.erl:1037 -msgid "Group " -msgstr "กลุ่ม" - -#: mod_shared_roster.erl:1017 mod_shared_roster_odbc.erl:1046 -#: web/ejabberd_web_admin.erl:840 web/ejabberd_web_admin.erl:884 -#: web/ejabberd_web_admin.erl:949 web/ejabberd_web_admin.erl:1026 -#: web/ejabberd_web_admin.erl:2017 -msgid "Submit" -msgstr "ส่ง" - -#: mod_vcard.erl:165 mod_vcard_ldap.erl:238 mod_vcard_odbc.erl:129 -msgid "Erlang Jabber Server" -msgstr "Erlang Jabber Server" - -#: mod_vcard.erl:364 mod_vcard.erl:478 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:454 -msgid "Birthday" -msgstr "วันเกิด" - -#: mod_vcard.erl:364 mod_vcard.erl:480 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:456 -msgid "City" -msgstr "เมือง" - -#: mod_vcard.erl:364 mod_vcard.erl:479 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:455 -msgid "Country" -msgstr "ประเทศ" - -#: mod_vcard.erl:364 mod_vcard.erl:481 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:457 -msgid "Email" -msgstr "อีเมล" - -#: mod_vcard.erl:364 mod_vcard.erl:476 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:452 -msgid "Family Name" -msgstr "นามสกุล" - -#: mod_vcard.erl:364 mod_vcard_odbc.erl:342 -msgid "" -"Fill in the form to search for any matching Jabber User (Add * to the end of " -"field to match substring)" -msgstr "" -"กรอกข้อมูลในแบบฟอร์มเพื่อค้นหาผู้ใช้ Jabber ที่ตรงกัน (ใส่เครื่องหมาย * " -"ที่ท้ายสุดของฟิลด์เพื่อจับคู่กับสตริงย่อย)" - -#: mod_vcard.erl:364 mod_vcard.erl:473 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:449 -msgid "Full Name" -msgstr "ชื่อเต็ม" - -#: mod_vcard.erl:364 mod_vcard.erl:475 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:451 -msgid "Middle Name" -msgstr "ชื่อกลาง" - -#: mod_vcard.erl:364 mod_vcard.erl:474 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:450 web/ejabberd_web_admin.erl:2006 -msgid "Name" -msgstr "ชื่อ" - -#: mod_vcard.erl:364 mod_vcard.erl:482 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:458 -msgid "Organization Name" -msgstr "ชื่อองค์กร" - -#: mod_vcard.erl:364 mod_vcard.erl:483 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:459 -msgid "Organization Unit" -msgstr "หน่วยขององค์กร" - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "Search users in " -msgstr "ค้นหาผู้ใช้ใน " - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "You need an x:data capable client to search" -msgstr "คุณต้องใช้ไคลเอ็นต์ที่รองรับ x:data เพื่อค้นหา" - -#: mod_vcard.erl:389 mod_vcard_ldap.erl:487 mod_vcard_odbc.erl:367 -msgid "vCard User Search" -msgstr "ค้นหาผู้ใช้ vCard " - -#: mod_vcard.erl:445 mod_vcard_ldap.erl:541 mod_vcard_odbc.erl:421 -msgid "ejabberd vCard module" -msgstr "ejabberd vCard module" - -#: mod_vcard.erl:469 mod_vcard_ldap.erl:551 mod_vcard_odbc.erl:445 -msgid "Search Results for " -msgstr "ผลการค้นหาสำหรับ " - -#: mod_vcard_ldap.erl:462 -msgid "Fill in fields to search for any matching Jabber User" -msgstr "กรอกข้อมูลลงในฟิลด์เพื่อค้นหาผู้ใช้ Jabber ที่ตรงกัน" - -#: web/ejabberd_web_admin.erl:193 web/ejabberd_web_admin.erl:203 -#: web/ejabberd_web_admin.erl:219 web/ejabberd_web_admin.erl:229 -msgid "Unauthorized" -msgstr "" - -#: web/ejabberd_web_admin.erl:286 web/ejabberd_web_admin.erl:303 -#, fuzzy -msgid "ejabberd Web Admin" -msgstr "เว็บอินเทอร์เฟซของ ejabberd" - -#: web/ejabberd_web_admin.erl:769 web/ejabberd_web_admin.erl:780 -msgid "Administration" -msgstr "การดูแล" - -#: web/ejabberd_web_admin.erl:878 web/ejabberd_web_admin.erl:982 -msgid "Raw" -msgstr "ข้อมูลดิบ" - -#: web/ejabberd_web_admin.erl:1017 -msgid "~s access rule configuration" -msgstr "~s การกำหนดค่ากฎการเข้าถึง" - -#: web/ejabberd_web_admin.erl:1035 -msgid "Virtual Hosts" -msgstr "โฮสต์เสมือน" - -#: web/ejabberd_web_admin.erl:1043 web/ejabberd_web_admin.erl:1050 -msgid "Users" -msgstr "ผู้ใช้" - -#: web/ejabberd_web_admin.erl:1078 -msgid "Users Last Activity" -msgstr "กิจกรรมล่าสุดของผู้ใช้" - -#: web/ejabberd_web_admin.erl:1080 -msgid "Period: " -msgstr "ระยะเวลา:" - -#: web/ejabberd_web_admin.erl:1090 -msgid "Last month" -msgstr "เดือนที่แล้ว" - -#: web/ejabberd_web_admin.erl:1091 -msgid "Last year" -msgstr "ปีที่แล้ว" - -#: web/ejabberd_web_admin.erl:1092 -msgid "All activity" -msgstr "กิจกรรมทั้งหมด" - -#: web/ejabberd_web_admin.erl:1094 -msgid "Show Ordinary Table" -msgstr "แสดงตารางทั่วไป" - -#: web/ejabberd_web_admin.erl:1096 -msgid "Show Integral Table" -msgstr "แสดงตารางรวม" - -#: web/ejabberd_web_admin.erl:1105 web/ejabberd_web_admin.erl:1939 -msgid "Statistics" -msgstr "สถิติ" - -#: web/ejabberd_web_admin.erl:1117 -#, fuzzy -msgid "Not Found" -msgstr "ไม่พบโหนด" - -#: web/ejabberd_web_admin.erl:1134 -msgid "Node not found" -msgstr "ไม่พบโหนด" - -#: web/ejabberd_web_admin.erl:1460 -msgid "Host" -msgstr "โฮสต์" - -#: web/ejabberd_web_admin.erl:1461 -msgid "Registered Users" -msgstr "ผู้ใช้ที่ลงทะเบียน" - -#: web/ejabberd_web_admin.erl:1573 -msgid "Offline Messages" -msgstr "ข้อความออฟไลน์" - -#: web/ejabberd_web_admin.erl:1574 web/ejabberd_web_admin.erl:1767 -msgid "Last Activity" -msgstr "กิจกรรมล่าสุด" - -#: web/ejabberd_web_admin.erl:1659 web/ejabberd_web_admin.erl:1675 -msgid "Registered Users:" -msgstr "ผู้ใช้ที่ลงทะเบียน:" - -#: web/ejabberd_web_admin.erl:1661 web/ejabberd_web_admin.erl:1677 -#: web/ejabberd_web_admin.erl:2192 -msgid "Online Users:" -msgstr "ผู้ใช้ออนไลน์:" - -#: web/ejabberd_web_admin.erl:1663 -msgid "Outgoing s2s Connections:" -msgstr "การเชื่อมต่อ s2s ขาออก:" - -#: web/ejabberd_web_admin.erl:1665 -msgid "Outgoing s2s Servers:" -msgstr "เซิร์ฟเวอร์ s2s ขาออก:" - -#: web/ejabberd_web_admin.erl:1734 web/mod_register_web.erl:175 -#: web/mod_register_web.erl:372 web/mod_register_web.erl:381 -#: web/mod_register_web.erl:411 -msgid "Change Password" -msgstr "เปลี่ยนรหัสผ่าน" - -#: web/ejabberd_web_admin.erl:1758 -msgid "User " -msgstr "ผู้ใช้" - -#: web/ejabberd_web_admin.erl:1765 -msgid "Connected Resources:" -msgstr "ทรัพยากรที่เชื่อมต่อ:" - -#: web/ejabberd_web_admin.erl:1766 web/mod_register_web.erl:227 -#: web/mod_register_web.erl:519 -msgid "Password:" -msgstr "รหัสผ่าน:" - -#: web/ejabberd_web_admin.erl:1828 -msgid "No Data" -msgstr "ไม่มีข้อมูล" - -#: web/ejabberd_web_admin.erl:1906 -msgid "Nodes" -msgstr "โหนด" - -#: web/ejabberd_web_admin.erl:1929 web/ejabberd_web_admin.erl:1951 -msgid "Node " -msgstr "โหนด " - -#: web/ejabberd_web_admin.erl:1938 -msgid "Listened Ports" -msgstr "พอร์ทฟัง" - -#: web/ejabberd_web_admin.erl:1940 web/ejabberd_web_admin.erl:2260 -#: web/ejabberd_web_admin.erl:2448 -msgid "Update" -msgstr "อัพเดต" - -#: web/ejabberd_web_admin.erl:1943 web/ejabberd_web_admin.erl:2569 -msgid "Restart" -msgstr "เริ่มต้นใหม่" - -#: web/ejabberd_web_admin.erl:1945 web/ejabberd_web_admin.erl:2571 -msgid "Stop" -msgstr "หยุด" - -#: web/ejabberd_web_admin.erl:1959 -msgid "RPC Call Error" -msgstr "ข้อผิดพลาดจากการเรียกใช้ RPC" - -#: web/ejabberd_web_admin.erl:2000 -msgid "Database Tables at " -msgstr "ตารางฐานข้อมูลที่" - -#: web/ejabberd_web_admin.erl:2007 -msgid "Storage Type" -msgstr "ชนิดที่เก็บข้อมูล" - -#: web/ejabberd_web_admin.erl:2008 -msgid "Elements" -msgstr "" - -#: web/ejabberd_web_admin.erl:2009 -msgid "Memory" -msgstr "หน่วยความจำ" - -#: web/ejabberd_web_admin.erl:2032 web/ejabberd_web_admin.erl:2137 -msgid "Error" -msgstr "" - -#: web/ejabberd_web_admin.erl:2034 -msgid "Backup of " -msgstr "การสำรองข้อมูล" - -#: web/ejabberd_web_admin.erl:2036 -#, fuzzy -msgid "" -"Please note that these options will only backup the builtin Mnesia database. " -"If you are using the ODBC module, you also need to backup your SQL database " -"separately." -msgstr "" -"โปรดทราบว่าตัวเลือกเหล่านี้จะสำรองข้อมูลในฐานข้อมูล builtin Mnesia เท่านั้น หากคุณใช้โมดูล " -"ODBC คุณต้องสำรองข้อมูลของฐานข้อมูล SQL แยกต่างหากด้วย" - -#: web/ejabberd_web_admin.erl:2041 -msgid "Store binary backup:" -msgstr "จัดเก็บข้อมูลสำรองแบบไบนารี:" - -#: web/ejabberd_web_admin.erl:2045 web/ejabberd_web_admin.erl:2052 -#: web/ejabberd_web_admin.erl:2060 web/ejabberd_web_admin.erl:2067 -#: web/ejabberd_web_admin.erl:2074 web/ejabberd_web_admin.erl:2081 -#: web/ejabberd_web_admin.erl:2088 web/ejabberd_web_admin.erl:2096 -#: web/ejabberd_web_admin.erl:2103 web/ejabberd_web_admin.erl:2110 -msgid "OK" -msgstr "ตกลง" - -#: web/ejabberd_web_admin.erl:2048 -msgid "Restore binary backup immediately:" -msgstr "คืนค่าข้อมูลสำรองแบบไบนารีโดยทันที:" - -#: web/ejabberd_web_admin.erl:2056 -msgid "" -"Restore binary backup after next ejabberd restart (requires less memory):" -msgstr "" -"คืนค่าข้อมูลสำรองแบบไบนารีหลังจากที่ ejabberd ถัดไปเริ่มการทำงานใหม่ (ใช้หน่วยความจำน้อยลง):" - -#: web/ejabberd_web_admin.erl:2063 -msgid "Store plain text backup:" -msgstr "จัดเก็บข้อมูลสำรองที่เป็นข้อความธรรมดา:" - -#: web/ejabberd_web_admin.erl:2070 -msgid "Restore plain text backup immediately:" -msgstr "คืนค่าข้อมูลสำรองที่เป็นข้อความธรรมดาโดยทันที:" - -#: web/ejabberd_web_admin.erl:2077 -msgid "Import users data from a PIEFXIS file (XEP-0227):" -msgstr "" - -#: web/ejabberd_web_admin.erl:2084 -msgid "Export data of all users in the server to PIEFXIS files (XEP-0227):" -msgstr "" - -#: web/ejabberd_web_admin.erl:2091 -msgid "Export data of users in a host to PIEFXIS files (XEP-0227):" -msgstr "" - -#: web/ejabberd_web_admin.erl:2099 -#, fuzzy -msgid "Import user data from jabberd14 spool file:" -msgstr "อิมพอร์ตผู้ใช้จากไฟล์เก็บพักข้อมูล jabberd14" - -#: web/ejabberd_web_admin.erl:2106 -#, fuzzy -msgid "Import users data from jabberd14 spool directory:" -msgstr "อิมพอร์ตผู้ใช้จากไฟล์เก็บพักข้อมูล jabberd14" - -#: web/ejabberd_web_admin.erl:2132 -msgid "Listened Ports at " -msgstr "พอร์ทฟังที่" - -#: web/ejabberd_web_admin.erl:2157 -msgid "Modules at " -msgstr "โมดูลที่ " - -#: web/ejabberd_web_admin.erl:2183 -msgid "Statistics of ~p" -msgstr "สถิติของ ~p" - -#: web/ejabberd_web_admin.erl:2186 -msgid "Uptime:" -msgstr "เวลาการทำงานต่อเนื่อง:" - -#: web/ejabberd_web_admin.erl:2189 -msgid "CPU Time:" -msgstr "เวลาการทำงานของ CPU:" - -#: web/ejabberd_web_admin.erl:2195 -msgid "Transactions Committed:" -msgstr "ทรานแซกชันที่ได้รับมอบหมาย:" - -#: web/ejabberd_web_admin.erl:2198 -msgid "Transactions Aborted:" -msgstr "ทรานแซกชันที่ถูกยกเลิก:" - -#: web/ejabberd_web_admin.erl:2201 -msgid "Transactions Restarted:" -msgstr "ทรานแซกชันที่เริ่มทำงานใหม่อีกครั้ง:" - -#: web/ejabberd_web_admin.erl:2204 -msgid "Transactions Logged:" -msgstr "ทรานแซกชันที่บันทึก:" - -#: web/ejabberd_web_admin.erl:2246 -msgid "Update " -msgstr "อัพเดต " - -#: web/ejabberd_web_admin.erl:2254 -msgid "Update plan" -msgstr "แผนการอัพเดต" - -#: web/ejabberd_web_admin.erl:2255 -#, fuzzy -msgid "Modified modules" -msgstr "โมดูลที่อัพเดต" - -#: web/ejabberd_web_admin.erl:2256 -msgid "Update script" -msgstr "อัพเดตสคริปต์" - -#: web/ejabberd_web_admin.erl:2257 -msgid "Low level update script" -msgstr "อัพเดตสคริปต์ระดับต่ำ" - -#: web/ejabberd_web_admin.erl:2258 -msgid "Script check" -msgstr "ตรวจสอบคริปต์" - -#: web/ejabberd_web_admin.erl:2426 -msgid "Port" -msgstr "พอร์ท" - -#: web/ejabberd_web_admin.erl:2427 -msgid "IP" -msgstr "" - -#: web/ejabberd_web_admin.erl:2428 -#, fuzzy -msgid "Protocol" -msgstr "พอร์ท" - -#: web/ejabberd_web_admin.erl:2429 web/ejabberd_web_admin.erl:2556 -msgid "Module" -msgstr "โมดูล" - -#: web/ejabberd_web_admin.erl:2430 web/ejabberd_web_admin.erl:2557 -msgid "Options" -msgstr "ตัวเลือก" - -#: web/ejabberd_web_admin.erl:2450 -msgid "Delete" -msgstr "ลบ" - -#: web/ejabberd_web_admin.erl:2579 -msgid "Start" -msgstr "เริ่ม" - -#: web/mod_register_web.erl:104 -msgid "Your Jabber account was successfully created." -msgstr "" - -#: web/mod_register_web.erl:107 -msgid "There was an error creating the account: " -msgstr "" - -#: web/mod_register_web.erl:115 -msgid "Your Jabber account was successfully deleted." -msgstr "" - -#: web/mod_register_web.erl:118 -msgid "There was an error deleting the account: " -msgstr "" - -#: web/mod_register_web.erl:128 -msgid "The password of your Jabber account was successfully changed." -msgstr "" - -#: web/mod_register_web.erl:131 -msgid "There was an error changing the password: " -msgstr "" - -#: web/mod_register_web.erl:163 web/mod_register_web.erl:172 -msgid "Jabber Account Registration" -msgstr "" - -#: web/mod_register_web.erl:174 web/mod_register_web.erl:192 -#: web/mod_register_web.erl:201 -msgid "Register a Jabber account" -msgstr "" - -#: web/mod_register_web.erl:176 web/mod_register_web.erl:493 -#: web/mod_register_web.erl:502 -msgid "Unregister a Jabber account" -msgstr "" - -#: web/mod_register_web.erl:203 -msgid "" -"This page allows to create a Jabber account in this Jabber server. Your JID " -"(Jabber IDentifier) will be of the form: username@server. Please read " -"carefully the instructions to fill correctly the fields." -msgstr "" - -#: web/mod_register_web.erl:212 web/mod_register_web.erl:386 -#: web/mod_register_web.erl:509 -#, fuzzy -msgid "Username:" -msgstr "ชื่อผู้ใช้ IRC" - -#: web/mod_register_web.erl:217 -msgid "This is case insensitive: macbeth is the same that MacBeth and Macbeth." -msgstr "" - -#: web/mod_register_web.erl:218 -msgid "Characters not allowed:" -msgstr "" - -#: web/mod_register_web.erl:222 web/mod_register_web.erl:391 -#: web/mod_register_web.erl:514 -#, fuzzy -msgid "Server:" -msgstr "ไม่เคย" - -#: web/mod_register_web.erl:232 -msgid "" -"Don't tell your password to anybody, not even the administrators of the " -"Jabber server." -msgstr "" - -#: web/mod_register_web.erl:234 -msgid "You can later change your password using a Jabber client." -msgstr "" - -#: web/mod_register_web.erl:235 -msgid "" -"Some Jabber clients can store your password in your computer. Use that " -"feature only if you trust your computer is safe." -msgstr "" - -#: web/mod_register_web.erl:237 -msgid "" -"Memorize your password, or write it in a paper placed in a safe place. In " -"Jabber there isn't an automated way to recover your password if you forget " -"it." -msgstr "" - -#: web/mod_register_web.erl:242 web/mod_register_web.erl:406 -#, fuzzy -msgid "Password Verification:" -msgstr "การตรวจสอบรหัสผ่าน" - -#: web/mod_register_web.erl:250 -#, fuzzy -msgid "Register" -msgstr "บัญชีรายชื่อ" - -#: web/mod_register_web.erl:396 -#, fuzzy -msgid "Old Password:" -msgstr "รหัสผ่าน:" - -#: web/mod_register_web.erl:401 -#, fuzzy -msgid "New Password:" -msgstr "รหัสผ่าน:" - -#: web/mod_register_web.erl:504 -msgid "This page allows to unregister a Jabber account in this Jabber server." -msgstr "" - -#: web/mod_register_web.erl:524 -msgid "Unregister" -msgstr "" - -#~ msgid "Encodings" -#~ msgstr "การเข้ารหัส" - -#~ msgid "(Raw)" -#~ msgstr "(ข้อมูลดิบ)" - -#~ msgid "Specified nickname is already registered" -#~ msgstr "ชื่อเล่นที่ระบุได้รับการลงได้ทะเบียนแล้ว" - -#~ msgid "Size" -#~ msgstr "ขนาด" - -#~ msgid "Roster groups that may subscribe (if access model is roster)" -#~ msgstr "กลุ่มบัญชีรายชื่อที่อาจจะสมัครเป็นสมาชิก (ถ้าโมเดลการเข้าถึงคือบัญชีรายชื่อ)" diff --git a/priv/msgs/tr.msg b/priv/msgs/tr.msg index 06de13813..af108d514 100644 --- a/priv/msgs/tr.msg +++ b/priv/msgs/tr.msg @@ -1,19 +1,19 @@ -{"Access Configuration","Erişim Ayarları"}. -{"Access Control List Configuration","Erişim Kontrol Listelerinin Ayarlanması (ACL)"}. -{"Access control lists","Erişim kontrol listeleri (ACL)"}. -{"Access Control Lists","Erişim Kontrol Listeleri (ACL)"}. +%% Generated automatically +%% DO NOT EDIT: run `make translations` instead +%% To improve translations please read: +%% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ + +{" has set the subject to: "," konuyu değiştirdi: "}. +{"A friendly name for the node","Düğüm için dostane bir isim"}. +{"A password is required to enter this room","Bu odaya girmek için parola gerekiyor"}. {"Access denied by service policy","Servis politikası gereği erişim engellendi"}. -{"Access rules","Erişim kuralları"}. -{"Access Rules","Erişim Kuralları"}. {"Action on user","Kullanıcıya uygulanacak eylem"}. -{"Add Jabber ID","Jabber ID'si Ekle"}. -{"Add New","Yeni Ekle"}. {"Add User","Kullanıcı Ekle"}. {"Administration of ","Yönetim : "}. {"Administration","Yönetim"}. {"Administrator privileges required","Yönetim yetkileri gerekli"}. -{"A friendly name for the node","Düğüm için dostane bir isim"}. {"All activity","Tüm aktivite"}. +{"All Users","Tüm Kullanıcılar"}. {"Allow this Jabber ID to subscribe to this pubsub node?","Bu Jabber ID bu pubsub düğümüne üye olmasına izin verilsin mi?"}. {"Allow users to change the subject","Kullanıcıların konu değiştirmesine izin ver"}. {"Allow users to query other users","Kullanıcıların diğer kullanıcıları sorgulamalarına izin ver"}. @@ -23,14 +23,10 @@ {"Allow visitors to send private messages to","Ziyaretçilerin özel mesaj göndermelerine izin ver"}. {"Allow visitors to send status text in presence updates","Ziyaretçilerin varlık (presence) güncellemelerinde durum metni göndermelerine izin ver"}. {"Allow visitors to send voice requests","Ziyaretçilerin ses isteğine göndermelerine izin ver"}. -{"All Users","Tüm Kullanıcılar"}. {"Announcements","Duyurular"}. -{"anyone","herkes"}. -{"A password is required to enter this room","Bu odaya girmek için parola gerekiyor"}. {"April","Nisan"}. {"August","Ağustos"}. {"Backup Management","Yedek Yönetimi"}. -{"Backup of ","Yedek : "}. {"Backup to File at ","Dosyaya Yedekle : "}. {"Backup","Yedekle"}. {"Bad format","Kötü biçem"}. @@ -46,90 +42,61 @@ {"Chatroom is stopped","Sohbet odası durduruldu"}. {"Chatrooms","Sohbet Odaları"}. {"Choose a username and password to register with this server","Bu sunucuya kayıt olmak için bir kullanıcı ismi ve parola seçiniz"}. -{"Choose modules to stop","Durdurulacak modülleri seçiniz"}. {"Choose storage type of tables","Tabloların veri depolama tipini seçiniz"}. {"Choose whether to approve this entity's subscription.","Bu varlığın üyeliğini onaylayıp onaylamamayı seçiniz."}. {"City","İl"}. {"Commands","Komutlar"}. {"Conference room does not exist","Konferans odası bulunamadı"}. -{"Configuration","Ayarlar"}. {"Configuration of room ~s","~s odasının ayarları"}. -{"Connected Resources:","Bağlı Kaynaklar:"}. -{"Connections parameters","Bağlantı parametreleri"}. +{"Configuration","Ayarlar"}. {"Country","Ülke"}. -{"CPU Time:","İşlemci Zamanı:"}. -{"Database Tables at ","Veritabanı Tabloları : "}. {"Database Tables Configuration at ","Veritabanı Tablo Ayarları : "}. {"Database","Veritabanı"}. {"December","Aralık"}. {"Default users as participants","Kullanıcılar öntanımlı olarak katılımcı olsun"}. -{"Delete message of the day","Günün mesajını sil"}. {"Delete message of the day on all hosts","Tüm sunuculardaki günün mesajını sil"}. -{"Delete Selected","Seçilenleri Sil"}. -{"Delete","Sil"}. +{"Delete message of the day","Günün mesajını sil"}. {"Delete User","Kullanıcıyı Sil"}. {"Deliver event notifications","Olay uyarıları gönderilsin"}. {"Deliver payloads with event notifications","Yükleri (payload) olay uyarıları ile beraber gönder"}. -{"Description:","Tanım:"}. {"Disc only copy","Sadece disk kopyala"}. -{"Displayed Groups:","Gösterilen Gruplar:"}. -{"Don't tell your password to anybody, not even the administrators of the Jabber server.","Parolanızı kimseye söylemeyin, Jabber sunucusunun yöneticilerine bile."}. {"Dump Backup to Text File at ","Metin Dosyasına Döküm Alarak Yedekle : "}. {"Dump to Text File","Metin Dosyasına Döküm Al"}. {"Edit Properties","Özellikleri Düzenle"}. {"Either approve or decline the voice request.","Ses isteğini kabul edin ya da reddedin"}. -{"ejabberd IRC module","ejabberd IRC modülü"}. {"ejabberd MUC module","ejabberd MUC modülü"}. {"ejabberd Publish-Subscribe module","ejabberd Publish-Subscribe modülü"}. {"ejabberd SOCKS5 Bytestreams module","ejabberd SOCKS5 Bytestreams modülü"}. {"ejabberd vCard module","ejabberd vCard modülü"}. {"ejabberd Web Admin","ejabberd Web Yöneticisi"}. -{"Elements","Elementler"}. {"Email","E-posta"}. {"Enable logging","Kayıt tutma özelliğini aç"}. -{"Encoding for server ~b","Sunucu için kodlama ~b"}. {"End User Session","Kullanıcı Oturumunu Kapat"}. -{"Enter list of {Module, [Options]}","{Module, [Options]} listesi giriniz"}. {"Enter nickname you want to register","Kaydettirmek istediğiniz takma ismi giriniz"}. {"Enter path to backup file","Yedek dosyasının yolunu giriniz"}. {"Enter path to jabberd14 spool dir","jabberd14 spool dosyası için yol giriniz"}. {"Enter path to jabberd14 spool file","jabberd14 spool dosyası için yol giriniz"}. {"Enter path to text file","Metin dosyasının yolunu giriniz"}. {"Enter the text you see","Gördüğünüz metni giriniz"}. -{"Enter username and encodings you wish to use for connecting to IRC servers. Press 'Next' to get more fields to fill in. Press 'Complete' to save settings.","IRC sunuculara bağlanmak için kullanmak istediğiniz kullanıcı isimleri ve kodlamaları giriniz. 'İleri' tuşuna basınca karşınıza dolduracak daha fazla alan çıkacak. 'Tamamla' tuşuna basarak ayarları kaydedin."}. -{"Enter username, encodings, ports and passwords you wish to use for connecting to IRC servers","IRC sunuculara bağlanmak için kullanmak istediğiniz kullanıcı ismi, kodlamalar, kapılar (portlar) ve parolaları giriniz"}. -{"Erlang Jabber Server","Erlang Jabber Sunucusu"}. -{"Error","Hata"}. -{"Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef.net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}].","Örnek: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"gizli\"}, {\"vendetta.fef.net\", \"iso8859-1\", 7000}], {\"irc.sometestserver.net\", \"utf-8\"}]"}. {"Exclude Jabber IDs from CAPTCHA challenge","CAPTCHA doğrulamasını şu Jabber ID'ler için yapma"}. {"Export data of all users in the server to PIEFXIS files (XEP-0227):","Sunucudaki tüm kullanıcıların verisini PIEFXIS dosyalarına (XEP-0227) dışa aktar:"}. {"Export data of users in a host to PIEFXIS files (XEP-0227):","Bir sunucudaki kullanıcıların verisini PIEFXIS dosyalarına (XEP-0227) dışa aktar:"}. {"Failed to extract JID from your voice request approval","Ses isteği onayınızdan JID bilginize ulaşılamadı"}. {"Family Name","Soyisim"}. {"February","Şubat"}. -{"Fill in fields to search for any matching Jabber User","Eşleşen jabber kullanıcılarını aramak için alanları doldurunuz"}. -{"Fill in the form to search for any matching Jabber User (Add * to the end of field to match substring)","Eşleşen jabber kullanıcılarını aramak için formu doldurunuz (Alt dizgi eşlemek için alanın sonuna * ekleyin)"}. {"Friday","Cuma"}. -{"From","Kimden"}. -{"From ~s","Kimden ~s"}. {"Full Name","Tam İsim"}. {"Get Number of Online Users","Bağlı Kullanıcı Sayısını Al"}. {"Get Number of Registered Users","Kayıtlı Kullanıcı Sayısını Al"}. {"Get User Last Login Time","Kullanıcı Son Giriş Zamanınlarını Al"}. -{"Get User Password","Kullanıcı Parolasını Al"}. {"Get User Statistics","Kullanıcı İstatistiklerini Al"}. {"Grant voice to this person?","Bu kişiye ses verelim mi?"}. -{"Group ","Group "}. -{"Groups","Gruplar"}. {"has been banned","odaya girmesi yasaklandı"}. -{"has been kicked because of an affiliation change","ilişki değişikliğinden dolayı atıldı"}. {"has been kicked because of a system shutdown","sistem kapandığından dolayı atıldı"}. +{"has been kicked because of an affiliation change","ilişki değişikliğinden dolayı atıldı"}. {"has been kicked because the room has been changed to members-only","oda üyelere-özel hale getirildiğinden dolayı atıldı"}. {"has been kicked","odadan atıldı"}. -{" has set the subject to: "," konuyu değiştirdi: "}. -{"Host","Sunucu"}. {"If you don't see the CAPTCHA image here, visit the web page.","Eğer burada CAPTCHA resmini göremiyorsanız, web sayfasını ziyaret edin."}. -{"If you want to specify different ports, passwords, encodings for IRC servers, fill this list with values in format '{\"irc server\", \"encoding\", port, \"password\"}'. By default this service use \"~s\" encoding, port ~p, empty password.","IRC sunucuları için farklı kapılar (portlar), parolalar, kodlamalar belirtmek istiyorsanız, '{\"irc sunucusu\", \"kodlama\",\"kapı\",\"parola\"}' biçeminde değerlerle bu listeyi doldurunuz. Öntanımlı olarak bu servis \"~s\" kodlamasını, ~p \"kapısını\", \"boş\" parolasını kullanıyor."}. {"Import Directory","Dizini İçe Aktar"}. {"Import File","Dosyayı İçe Aktar"}. {"Import user data from jabberd14 spool file:","Jabberd 1.4 Spool Dosyalarından Kullanıcıları İçe Aktar:"}. @@ -140,28 +107,13 @@ {"Import Users From jabberd14 Spool Files","Jabberd 1.4 Spool Dosyalarından Kullanıcıları İçeri Aktar"}. {"Improper message type","Uygunsuz mesaj tipi"}. {"Incorrect password","Yanlış parola"}. -{"Invalid affiliation: ~s","Geçersiz ilişki: ~s"}. -{"Invalid role: ~s","Geçersiz rol: ~s"}. {"IP addresses","IP adresleri"}. -{"IP","IP"}. -{"IRC channel (don't put the first #)","IRC kanalı (ilk # işaretini koymayın)"}. -{"IRC server","IRC sunucusu"}. -{"IRC settings","IRC ayarları"}. -{"IRC Transport","IRC Nakli (Transport)"}. -{"IRC username","IRC kullanıcı ismi"}. -{"IRC Username","IRC Kullanıcı İsmi"}. {"is now known as","isim değiştirdi :"}. {"It is not allowed to send private messages of type \"groupchat\"","\"groupchat\" tipinde özel mesajlar gönderilmesine izin verilmiyor"}. -{"It is not allowed to send private messages","Özel mesaj gönderilmesine izin verilmiyor"}. {"It is not allowed to send private messages to the conference","Konferansa özel mesajlar gönderilmesine izin verilmiyor"}. -{"Jabber Account Registration","Jabber Hesap Kaydı"}. {"Jabber ID","Jabber ID"}. -{"Jabber ID ~s is invalid","Jabber ID ~s geçersiz"}. {"January","Ocak"}. -{"Join IRC channel","IRC kanalına katıl"}. {"joins the room","odaya katıldı"}. -{"Join the IRC channel here.","Buradaki IRC kanalına katıl."}. -{"Join the IRC channel in this Jabber ID: ~s","IRC kanalına bu Jabber ID'si ile katıl: ~s"}. {"July","Temmuz"}. {"June","Haziran"}. {"Last Activity","Son Aktivite"}. @@ -169,10 +121,6 @@ {"Last month","Geçen ay"}. {"Last year","Geçen yıl"}. {"leaves the room","odadan ayrıldı"}. -{"Listened Ports at ","Dinlenen Kapılar (Portlar) : "}. -{"Listened Ports","Dinlenen Kapılar (Portlar)"}. -{"List of modules to start","Başlatılacak modüllerin listesi"}. -{"Low level update script","Düşük seviye güncelleme betiği"}. {"Make participants list public","Katılımcı listesini herkese açık hale getir"}. {"Make room CAPTCHA protected","Odayı insan doğrulaması (captcha) korumalı hale getir"}. {"Make room members-only","Odayı sadece üyelere açık hale getir"}. @@ -181,41 +129,28 @@ {"Make room persistent","Odayı kalıcı hale getir"}. {"Make room public searchable","Odayı herkes tarafından aranabilir hale getir"}. {"March","Mart"}. -{"Maximum Number of Occupants","Odada En Fazla Bulunabilecek Kişi Sayısı"}. -{"Max # of items to persist","Kalıcı hale getirilecek en fazla öğe sayısı"}. {"Max payload size in bytes","En fazla yük (payload) boyutu (bayt olarak)"}. +{"Maximum Number of Occupants","Odada En Fazla Bulunabilecek Kişi Sayısı"}. {"May","Mayıs"}. {"Membership is required to enter this room","Bu odaya girmek için üyelik gerekiyor"}. -{"Members:","Üyeler:"}. -{"Memorize your password, or write it in a paper placed in a safe place. In Jabber there isn't an automated way to recover your password if you forget it.","Parolanızı ezberleyin ya da bir kağıda yazarak güvenli bir yerde saklayın. Jabber'da parolanızı unutursanız, otomatik kurtarmak için bir yöntem bulunmuyor."}. -{"Memory","Bellek"}. {"Message body","Mesajın gövdesi"}. {"Middle Name","Ortanca İsim"}. {"Minimum interval between voice requests (in seconds)","Ses istekleri arasında olabilecek en az aralık (saniye olarak)"}. {"Moderator privileges required","Moderatör yetkileri gerekli"}. -{"moderators only","sadece moderatörler"}. -{"Modified modules","Değişen modüller"}. -{"Module","Modül"}. -{"Modules at ","Modüller : "}. -{"Modules","Modüller"}. {"Monday","Pazartesi"}. -{"Name:","İsim:"}. {"Name","İsim"}. {"Never","Asla"}. {"New Password:","Yeni Parola:"}. {"Nickname Registration at ","Takma İsim Kaydı : "}. {"Nickname ~s does not exist in the room","~s takma ismi odada yok"}. {"Nickname","Takma isim"}. -{"nobody","hiç kimse"}. {"No body provided for announce message","Duyuru mesajının gövdesi yok"}. {"No Data","Veri Yok"}. -{"Node ","Düğüm "}. +{"No limit","Sınırsız"}. {"Node ID","Düğüm ID"}. {"Node not found","Düğüm bulunamadı"}. {"Nodes","Düğümler"}. -{"No limit","Sınırsız"}. {"None","Hiçbiri"}. -{"No resource provided","Hiç kaynak sağlanmadı"}. {"Not Found","Bulunamadı"}. {"Notify subscribers when items are removed from the node","Düğümden öğeler kaldırıldığında üyeleri uyar"}. {"Notify subscribers when the node configuration changes","Düğüm ayarları değiştiğinde üyeleri uyar"}. @@ -225,13 +160,10 @@ {"Number of online users","Bağlı kullanıcı sayısı"}. {"Number of registered users","Kayıtlı kullanıcı sayısı"}. {"October","Ekim"}. -{"Offline Messages:","Çevirim-dışı Mesajlar:"}. -{"Offline Messages","Çevirim-dışı Mesajlar"}. {"OK","Tamam"}. {"Old Password:","Eski Parola:"}. -{"Online","Bağlı"}. -{"Online Users:","Bağlı Kullanıcılar:"}. {"Online Users","Bağlı Kullanıcılar"}. +{"Online","Bağlı"}. {"Only deliver notifications to available users","Uyarıları sadece durumu uygun kullanıcılara ulaştır"}. {"Only moderators and participants are allowed to change the subject in this room","Sadece moderatörlerin ve katılımcıların bu odanın konusunu değiştirmesine izin veriliyor"}. {"Only moderators are allowed to change the subject in this room","Sadece moderatörlerin bu odanın konusunu değiştirmesine izin veriliyor"}. @@ -239,55 +171,38 @@ {"Only occupants are allowed to send messages to the conference","Sadece oda sakinlerinin konferansa mesaj göndermesine izin veriliyor"}. {"Only occupants are allowed to send queries to the conference","Sadece oda sakinlerinin konferansa sorgu göndermesine izin veriliyor"}. {"Only service administrators are allowed to send service messages","Sadece servis yöneticileri servis mesajı gönderebilirler"}. -{"Options","Seçenekler"}. {"Organization Name","Kurum İsmi"}. {"Organization Unit","Kurumun İlgili Birimi"}. -{"Outgoing s2s Connections:","Giden s2s Bağlantıları:"}. {"Outgoing s2s Connections","Giden s2s Bağlantıları"}. -{"Outgoing s2s Servers:","Giden s2s Sunucuları"}. {"Owner privileges required","Sahip yetkileri gerekli"}. -{"Packet","Paket"}. -{"Password ~b","Parola ~b"}. -{"Password:","Parola:"}. -{"Password","Parola"}. -{"Password Verification:","Parola Doğrulaması:"}. {"Password Verification","Parola Doğrulaması"}. +{"Password Verification:","Parola Doğrulaması:"}. +{"Password","Parola"}. +{"Password:","Parola:"}. {"Path to Dir","Dizinin Yolu"}. {"Path to File","Dosyanın Yolu"}. -{"Pending","Sıra Bekleyen"}. {"Period: ","Periyot:"}. {"Persist items to storage","Öğeleri depoda kalıcı hale getir"}. {"Ping","Ping"}. {"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Bu seçeneklerin sadece gömülü Mnesia veritabanını yedekleyeceğine dikkat edin. Eğer ODBC modülünü kullanıyorsanız, SQL veritabanınızı da ayrıca yedeklemeniz gerekiyor."}. {"Please, wait for a while before sending new voice request","Lütfen yeni bir ses isteği göndermeden önce biraz bekleyin"}. {"Pong","Pong"}. -{"Port ~b","Kapı (Port) ~b"}. -{"Port","Kapı (Port)"}. {"Present real Jabber IDs to","Gerçek Jabber ID'lerini göster :"}. {"private, ","özel"}. -{"Protocol","Protokol"}. {"Publish-Subscribe","Yayınla-Üye Ol"}. {"PubSub subscriber request","PubSub üye isteği"}. {"Purge all items when the relevant publisher goes offline","İlgili yayıncı çevirimdışı olunca tüm onunla ilgili olanları sil"}. {"Queries to the conference members are not allowed in this room","Bu odada konferans üyelerine sorgu yapılmasına izin verilmiyor"}. {"RAM and disc copy","RAM ve disk kopyala"}. {"RAM copy","RAM kopyala"}. -{"Raw","Ham"}. {"Really delete message of the day?","Günün mesajını silmek istediğinize emin misiniz?"}. {"Recipient is not in the conference room","Alıcı konferans odasında değil"}. -{"Register a Jabber account","Bir Jabber hesabı kaydet"}. -{"Registered Users:","Kayıtlı Kullanıcılar:"}. -{"Registered Users","Kayıtlı Kullanıcılar"}. {"Register","Kayıt Ol"}. -{"Registration in mod_irc for ","mod_irc'ye kayıt : "}. {"Remote copy","Uzak kopyala"}. -{"Remove All Offline Messages","Tüm Çevirim-dışı Mesajları Kaldır"}. -{"Remove","Kaldır"}. {"Remove User","Kullanıcıyı Kaldır"}. {"Replaced by new connection","Eski bağlantı yenisi ile değiştirildi"}. {"Resources","Kaynaklar"}. {"Restart Service","Servisi Tekrar Başlat"}. -{"Restart","Tekrar Başlat"}. {"Restore Backup from File at ","Dosyadaki Yedekten Geri Al : "}. {"Restore binary backup after next ejabberd restart (requires less memory):","ejabberd'nin bir sonraki tekrar başlatılışında ikili yedekten geri al (daha az bellek gerektirir)"}. {"Restore binary backup immediately:","Hemen ikili yedekten geri al:"}. @@ -299,22 +214,16 @@ {"Room Occupants","Oda Sakini Sayısı"}. {"Room title","Oda başlığı"}. {"Roster groups allowed to subscribe","Üye olunmasına izin verilen kontak listesi grupları"}. -{"Roster","Kontak Listesi"}. -{"Roster of ","Kontak Listesi : "}. {"Roster size","İsim listesi boyutu"}. -{"RPC Call Error","RPC Çağrı Hatası"}. {"Running Nodes","Çalışan Düğümler"}. -{"~s access rule configuration","~s erişim kuralları ayarları"}. {"Saturday","Cumartesi"}. -{"Script check","Betik kontrolü"}. {"Search Results for ","Arama sonuçları : "}. {"Search users in ","Kullanıcılarda arama yap : "}. -{"Send announcement to all online users","Duyuruyu tüm bağlı kullanıcılara yolla"}. {"Send announcement to all online users on all hosts","Duyuruyu tüm sunuculardaki tüm bağlı kullanıcılara yolla"}. -{"Send announcement to all users","Duyuruyu tüm kullanıcılara yolla"}. +{"Send announcement to all online users","Duyuruyu tüm bağlı kullanıcılara yolla"}. {"Send announcement to all users on all hosts","Tüm sunuculardaki tüm kullanıcılara duyuru yolla"}. +{"Send announcement to all users","Duyuruyu tüm kullanıcılara yolla"}. {"September","Eylül"}. -{"Server ~b","Sunucu ~b"}. {"Server:","Sunucu:"}. {"Set message of the day and send to online users","Günün mesajını belirle"}. {"Set message of the day on all hosts and send to online users","Tüm sunucularda günün mesajını belirle ve bağlı tüm kullanıcılara gönder"}. @@ -322,81 +231,43 @@ {"Show Integral Table","Önemli Tabloyu Göster"}. {"Show Ordinary Table","Sıradan Tabloyu Göster"}. {"Shut Down Service","Servisi Kapat"}. -{"~s invites you to the room ~s","~s sizi ~s odasına davet ediyor"}. -{"Some Jabber clients can store your password in your computer. Use that feature only if you trust your computer is safe.","Bazı Jabber istemcileri parolanızı bilgisayarınızda saklayabilir. Bu özelliği ancak bilgisayarın güvenli olduğuna güveniyorsanız kullanın."}. {"Specify the access model","Erişim modelini belirtiniz"}. {"Specify the event message type","Olay mesaj tipini belirtiniz"}. {"Specify the publisher model","Yayıncı modelini belirtiniz"}. -{"~s's Offline Messages Queue","~s Kullanıcısının Mesaj Kuyruğu"}. -{"Start","Başlat"}. -{"Start Modules at ","Modülleri Başlat : "}. -{"Start Modules","Modülleri Başlat"}. -{"Statistics","İstatistikler"}. -{"Statistics of ~p","~p istatistikleri"}. -{"Stop","Durdur"}. -{"Stop Modules at ","Modülleri Durdur : "}. -{"Stop Modules","Modülleri Durdur"}. {"Stopped Nodes","Durdurulmuş Düğümler"}. -{"Storage Type","Depolama Tipi"}. {"Store binary backup:","İkili yedeği sakla:"}. {"Store plain text backup:","Düz metin yedeği sakla:"}. {"Subject","Konu"}. -{"Submit","Gönder"}. {"Submitted","Gönderilenler"}. {"Subscriber Address","Üye Olanın Adresi"}. -{"Subscription","Üyelik"}. {"Sunday","Pazar"}. {"That nickname is already in use by another occupant","Takma isim odanın başka bir sakini tarafından halihazırda kullanımda"}. {"That nickname is registered by another person","O takma isim başka biri tarafından kaydettirilmiş"}. {"The CAPTCHA is valid.","İnsan doğrulaması (captcha) geçerli."}. {"The CAPTCHA verification has failed","CAPTCHA doğrulaması başarısız oldu"}. {"The collections with which a node is affiliated","Bir düğüm ile bağlantılı koleksiyonlar"}. -{"the password is","parola :"}. {"The password is too weak","Parola çok zayıf"}. -{"The password of your Jabber account was successfully changed.","Jabber hesabınızın parolası başarıyla değiştirildi."}. -{"There was an error changing the password: ","Parolanın değiştirilmesi sırasında bir hata oluştu:"}. +{"the password is","parola :"}. {"There was an error creating the account: ","Hesap oluşturulurken bir hata oluştu:"}. {"There was an error deleting the account: ","Hesabın silinmesi sırasında bir hata oluştu:"}. -{"This is case insensitive: macbeth is the same that MacBeth and Macbeth.","Burada büyük küçük harfi yapılmaz: macbeth ile MacBeth ve Macbeth aynıdır."}. -{"This page allows to create a Jabber account in this Jabber server. Your JID (Jabber IDentifier) will be of the form: username@server. Please read carefully the instructions to fill correctly the fields.","Bu sayfa bu Jabber sunucusunda bir Jabber hesabı oluşturulmasına olanak tanıyor. Sizin JID'iniz (Jabber Tanımlayıcısı) şu biçemde olacaktır: kullanici_adi@sunucu. Lütfen alanları doğru doldurabilmek için yönergeleri dikkatle okuyunuz."}. -{"This page allows to unregister a Jabber account in this Jabber server.","Bu sayfa bu Jabber sunucusundan bir Jabber hesabının kaydını silmeye olanak tanıyor."}. -{"This participant is kicked from the room because he sent an error message","Bu katılımcı bir hata mesajı gönderdiği için odadan atıldı"}. -{"This participant is kicked from the room because he sent an error message to another participant","Bu katılımcı başka bir katılımcıya bir hata mesajı gönderdiği için odadan atıldı"}. -{"This participant is kicked from the room because he sent an error presence","Bu katılımcı bir hata varlığı (presence) gönderdiği için odadan atıldı"}. {"This room is not anonymous","Bu oda anonim değil"}. {"Thursday","Perşembe"}. {"Time delay","Zaman gecikmesi"}. -{"Time","Zaman"}. -{"To","Kime"}. {"Too many CAPTCHA requests","Çok fazla CAPTCHA isteği"}. -{"To ~s","Kime ~s"}. {"Traffic rate limit is exceeded","Trafik oran sınırı aşıldı"}. -{"Transactions Aborted:","İptal Edilen Hareketler (Transactions):"}. -{"Transactions Committed:","Tamamlanan Hareketler (Transactions Committed):"}. -{"Transactions Logged:","Kaydı Tutulan Hareketler (Transactions):"}. -{"Transactions Restarted:","Tekrar Başlatılan Hareketler (Transactions):"}. {"Tuesday","Salı"}. {"Unable to generate a CAPTCHA","İnsan doğrulaması (CAPTCHA) oluşturulamadı"}. {"Unauthorized","Yetkisiz"}. -{"Unregister a Jabber account","Bir Jabber hesabı kaydı sil"}. {"Unregister","Kaydı Sil"}. -{"Update ","Güncelle "}. -{"Update","GÜncelle"}. {"Update message of the day (don't send)","Günün mesajını güncelle (gönderme)"}. {"Update message of the day on all hosts (don't send)","Tüm sunuculardaki günün mesajını güncelle (gönderme)"}. -{"Update plan","Planı güncelle"}. -{"Update script","Betiği Güncelle"}. -{"Uptime:","Hizmet Süresi:"}. -{"Use of STARTTLS required","STARTTLS kullanımı gereklidir"}. {"User JID","Kullanıcı JID"}. -{"User ","Kullanıcı "}. -{"User","Kullanıcı"}. {"User Management","Kullanıcı Yönetimi"}. +{"User","Kullanıcı"}. {"Username:","Kullanıcı adı:"}. {"Users are not allowed to register accounts so quickly","Kullanıcıların bu kadar hızlı hesap açmalarına izin verilmiyor"}. -{"Users","Kullanıcılar"}. {"Users Last Activity","Kullanıcıların Son Aktiviteleri"}. -{"Validate","Geçerli"}. +{"Users","Kullanıcılar"}. {"vCard User Search","vCard Kullanıcı Araması"}. {"Virtual Hosts","Sanal Sunucuları"}. {"Visitors are not allowed to change their nicknames in this room","Bu odada ziyaretçilerin takma isimlerini değiştirmesine izin verilmiyor"}. @@ -406,16 +277,11 @@ {"Wednesday","Çarşamba"}. {"When to send the last published item","Son yayınlanan öğe ne zaman gönderilsin"}. {"Whether to allow subscriptions","Üyeliklere izin verilsin mi"}. -{"You can later change your password using a Jabber client.","Parolanızı daha sonra bir Jabber istemci kullanarak değiştirebilirsiniz."}. {"You have been banned from this room","Bu odaya girmeniz yasaklandı"}. {"You must fill in field \"Nickname\" in the form","Formda \"Takma isim\" alanını doldurmanız gerekiyor"}. {"You need a client that supports x:data and CAPTCHA to register","Takma isminizi kaydettirmek için x:data ve CAPTCHA destekleyen bir istemciye gereksinimiz var"}. {"You need a client that supports x:data to register the nickname","Takma isminizi kaydettirmek için x:data destekleyen bir istemciye gereksinimiz var"}. -{"You need an x:data capable client to configure mod_irc settings","mod_irc ayarlarını düzenlemek için x:data becerisine sahip bir istemciye gereksinimiz var"}. -{"You need an x:data capable client to configure room","Odayı ayarlamak için x:data becerisine sahip bir istemciye gereksinimiz var"}. {"You need an x:data capable client to search","Arama yapabilmek için x:data becerisine sahip bir istemciye gereksinimiz var"}. {"Your active privacy list has denied the routing of this stanza.","Etkin mahremiyet listeniz bu bölümün yönlendirilmesini engelledi."}. {"Your contact offline message queue is full. The message has been discarded.","Çevirim-dışı mesaj kuyruğunuz dolu. Mesajını dikkate alınmadı."}. -{"Your Jabber account was successfully created.","Jabber hesabınız başarıyla oluşturuldu."}. -{"Your Jabber account was successfully deleted.","Jabber hesabınız başarıyla silindi."}. -{"Your messages to ~s are being blocked. To unblock them, visit ~s","~s kullanıcısına mesajlarınız engelleniyor. Durumu düzeltmek için ~s adresini ziyaret ediniz."}. +{"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","~s kullanıcısına mesajlarınız engelleniyor. Durumu düzeltmek için ~s adresini ziyaret ediniz."}. diff --git a/priv/msgs/tr.po b/priv/msgs/tr.po deleted file mode 100644 index 91d2219e5..000000000 --- a/priv/msgs/tr.po +++ /dev/null @@ -1,1864 +0,0 @@ -# translation of tr.po to Turkish -# Doruk Fisek , 2009, 2012. -msgid "" -msgstr "" -"Project-Id-Version: tr\n" -"PO-Revision-Date: 2012-04-17 11:18+0300\n" -"Last-Translator: Doruk Fisek \n" -"Language-Team: Turkish \n" -"Language: tr\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Language: Turkish (türkçe)\n" -"X-Generator: KBabel 1.11.4\n" - -#: ejabberd_c2s.erl:424 ejabberd_c2s.erl:727 -msgid "Use of STARTTLS required" -msgstr "STARTTLS kullanımı gereklidir" - -#: ejabberd_c2s.erl:503 -msgid "No resource provided" -msgstr "Hiç kaynak sağlanmadı" - -#: ejabberd_c2s.erl:1197 -msgid "Replaced by new connection" -msgstr "Eski bağlantı yenisi ile değiştirildi" - -#: ejabberd_c2s.erl:1885 -msgid "Your active privacy list has denied the routing of this stanza." -msgstr "Etkin mahremiyet listeniz bu bölümün yönlendirilmesini engelledi." - -#: ejabberd_captcha.erl:96 ejabberd_captcha.erl:152 ejabberd_captcha.erl:178 -msgid "Enter the text you see" -msgstr "Gördüğünüz metni giriniz" - -#: ejabberd_captcha.erl:101 -msgid "Your messages to ~s are being blocked. To unblock them, visit ~s" -msgstr "" -"~s kullanıcısına mesajlarınız engelleniyor. Durumu düzeltmek için ~s " -"adresini ziyaret ediniz." - -#: ejabberd_captcha.erl:134 -msgid "If you don't see the CAPTCHA image here, visit the web page." -msgstr "" -"Eğer burada CAPTCHA resmini göremiyorsanız, web sayfasını ziyaret edin." - -#: ejabberd_captcha.erl:146 -msgid "CAPTCHA web page" -msgstr "CAPTCHA web sayfası" - -#: ejabberd_captcha.erl:307 -msgid "The CAPTCHA is valid." -msgstr "İnsan doğrulaması (captcha) geçerli." - -#: mod_adhoc.erl:95 mod_adhoc.erl:125 mod_adhoc.erl:143 mod_adhoc.erl:161 -msgid "Commands" -msgstr "Komutlar" - -#: mod_adhoc.erl:149 mod_adhoc.erl:243 -msgid "Ping" -msgstr "Ping" - -#: mod_adhoc.erl:260 -msgid "Pong" -msgstr "Pong" - -#: mod_announce.erl:507 mod_announce_odbc.erl:499 -msgid "Really delete message of the day?" -msgstr "Günün mesajını silmek istediğinize emin misiniz?" - -#: mod_announce.erl:515 mod_announce_odbc.erl:507 mod_configure.erl:1083 -#: mod_configure.erl:1128 -msgid "Subject" -msgstr "Konu" - -#: mod_announce.erl:520 mod_announce_odbc.erl:512 mod_configure.erl:1088 -#: mod_configure.erl:1133 -msgid "Message body" -msgstr "Mesajın gövdesi" - -#: mod_announce.erl:600 mod_announce_odbc.erl:592 -msgid "No body provided for announce message" -msgstr "Duyuru mesajının gövdesi yok" - -#: mod_announce.erl:635 mod_announce_odbc.erl:627 -msgid "Announcements" -msgstr "Duyurular" - -#: mod_announce.erl:637 mod_announce_odbc.erl:629 -msgid "Send announcement to all users" -msgstr "Duyuruyu tüm kullanıcılara yolla" - -#: mod_announce.erl:639 mod_announce_odbc.erl:631 -msgid "Send announcement to all users on all hosts" -msgstr "Tüm sunuculardaki tüm kullanıcılara duyuru yolla" - -#: mod_announce.erl:641 mod_announce_odbc.erl:633 -msgid "Send announcement to all online users" -msgstr "Duyuruyu tüm bağlı kullanıcılara yolla" - -#: mod_announce.erl:643 mod_announce_odbc.erl:635 mod_configure.erl:1078 -#: mod_configure.erl:1123 -msgid "Send announcement to all online users on all hosts" -msgstr "Duyuruyu tüm sunuculardaki tüm bağlı kullanıcılara yolla" - -#: mod_announce.erl:645 mod_announce_odbc.erl:637 -msgid "Set message of the day and send to online users" -msgstr "Günün mesajını belirle" - -#: mod_announce.erl:647 mod_announce_odbc.erl:639 -msgid "Set message of the day on all hosts and send to online users" -msgstr "" -"Tüm sunucularda günün mesajını belirle ve bağlı tüm kullanıcılara gönder" - -#: mod_announce.erl:649 mod_announce_odbc.erl:641 -msgid "Update message of the day (don't send)" -msgstr "Günün mesajını güncelle (gönderme)" - -#: mod_announce.erl:651 mod_announce_odbc.erl:643 -msgid "Update message of the day on all hosts (don't send)" -msgstr "Tüm sunuculardaki günün mesajını güncelle (gönderme)" - -#: mod_announce.erl:653 mod_announce_odbc.erl:645 -msgid "Delete message of the day" -msgstr "Günün mesajını sil" - -#: mod_announce.erl:655 mod_announce_odbc.erl:647 -msgid "Delete message of the day on all hosts" -msgstr "Tüm sunuculardaki günün mesajını sil" - -#: mod_configure.erl:114 mod_configure.erl:274 mod_configure.erl:296 -#: mod_configure.erl:498 -msgid "Configuration" -msgstr "Ayarlar" - -#: mod_configure.erl:125 mod_configure.erl:576 web/ejabberd_web_admin.erl:1936 -msgid "Database" -msgstr "Veritabanı" - -#: mod_configure.erl:127 mod_configure.erl:595 -msgid "Start Modules" -msgstr "Modülleri Başlat" - -#: mod_configure.erl:129 mod_configure.erl:596 -msgid "Stop Modules" -msgstr "Modülleri Durdur" - -#: mod_configure.erl:131 mod_configure.erl:604 web/ejabberd_web_admin.erl:1937 -msgid "Backup" -msgstr "Yedekle" - -#: mod_configure.erl:133 mod_configure.erl:605 -msgid "Restore" -msgstr "Yedekten Geri Al" - -#: mod_configure.erl:135 mod_configure.erl:606 -msgid "Dump to Text File" -msgstr "Metin Dosyasına Döküm Al" - -#: mod_configure.erl:137 mod_configure.erl:615 -msgid "Import File" -msgstr "Dosyayı İçe Aktar" - -#: mod_configure.erl:139 mod_configure.erl:616 -msgid "Import Directory" -msgstr "Dizini İçe Aktar" - -#: mod_configure.erl:141 mod_configure.erl:581 mod_configure.erl:1057 -msgid "Restart Service" -msgstr "Servisi Tekrar Başlat" - -#: mod_configure.erl:143 mod_configure.erl:582 mod_configure.erl:1102 -msgid "Shut Down Service" -msgstr "Servisi Kapat" - -#: mod_configure.erl:145 mod_configure.erl:518 mod_configure.erl:1197 -#: web/ejabberd_web_admin.erl:1527 -msgid "Add User" -msgstr "Kullanıcı Ekle" - -#: mod_configure.erl:147 mod_configure.erl:519 mod_configure.erl:1219 -msgid "Delete User" -msgstr "Kullanıcıyı Sil" - -#: mod_configure.erl:149 mod_configure.erl:520 mod_configure.erl:1231 -msgid "End User Session" -msgstr "Kullanıcı Oturumunu Kapat" - -#: mod_configure.erl:151 mod_configure.erl:521 mod_configure.erl:1243 -#: mod_configure.erl:1255 -msgid "Get User Password" -msgstr "Kullanıcı Parolasını Al" - -#: mod_configure.erl:153 mod_configure.erl:522 -msgid "Change User Password" -msgstr "Kullanıcı Parolasını Değiştir" - -#: mod_configure.erl:155 mod_configure.erl:523 mod_configure.erl:1272 -msgid "Get User Last Login Time" -msgstr "Kullanıcı Son Giriş Zamanınlarını Al" - -#: mod_configure.erl:157 mod_configure.erl:524 mod_configure.erl:1284 -msgid "Get User Statistics" -msgstr "Kullanıcı İstatistiklerini Al" - -#: mod_configure.erl:159 mod_configure.erl:525 -msgid "Get Number of Registered Users" -msgstr "Kayıtlı Kullanıcı Sayısını Al" - -#: mod_configure.erl:161 mod_configure.erl:526 -msgid "Get Number of Online Users" -msgstr "Bağlı Kullanıcı Sayısını Al" - -#: mod_configure.erl:163 mod_configure.erl:509 web/ejabberd_web_admin.erl:831 -#: web/ejabberd_web_admin.erl:872 -msgid "Access Control Lists" -msgstr "Erişim Kontrol Listeleri (ACL)" - -#: mod_configure.erl:165 mod_configure.erl:510 web/ejabberd_web_admin.erl:940 -#: web/ejabberd_web_admin.erl:976 -msgid "Access Rules" -msgstr "Erişim Kuralları" - -#: mod_configure.erl:297 mod_configure.erl:499 -msgid "User Management" -msgstr "Kullanıcı Yönetimi" - -#: mod_configure.erl:500 web/ejabberd_web_admin.erl:1058 -#: web/ejabberd_web_admin.erl:1462 -msgid "Online Users" -msgstr "Bağlı Kullanıcılar" - -#: mod_configure.erl:501 -msgid "All Users" -msgstr "Tüm Kullanıcılar" - -#: mod_configure.erl:502 -msgid "Outgoing s2s Connections" -msgstr "Giden s2s Bağlantıları" - -#: mod_configure.erl:503 web/ejabberd_web_admin.erl:1907 -msgid "Running Nodes" -msgstr "Çalışan Düğümler" - -#: mod_configure.erl:504 web/ejabberd_web_admin.erl:1909 -msgid "Stopped Nodes" -msgstr "Durdurulmuş Düğümler" - -#: mod_configure.erl:577 mod_configure.erl:587 web/ejabberd_web_admin.erl:1953 -msgid "Modules" -msgstr "Modüller" - -#: mod_configure.erl:578 -msgid "Backup Management" -msgstr "Yedek Yönetimi" - -#: mod_configure.erl:579 -msgid "Import Users From jabberd14 Spool Files" -msgstr "Jabberd 1.4 Spool Dosyalarından Kullanıcıları İçeri Aktar" - -#: mod_configure.erl:699 -msgid "To ~s" -msgstr "Kime ~s" - -#: mod_configure.erl:717 -msgid "From ~s" -msgstr "Kimden ~s" - -#: mod_configure.erl:913 -msgid "Database Tables Configuration at " -msgstr "Veritabanı Tablo Ayarları : " - -#: mod_configure.erl:918 -msgid "Choose storage type of tables" -msgstr "Tabloların veri depolama tipini seçiniz" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Disc only copy" -msgstr "Sadece disk kopyala" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM and disc copy" -msgstr "RAM ve disk kopyala" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM copy" -msgstr "RAM kopyala" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Remote copy" -msgstr "Uzak kopyala" - -#: mod_configure.erl:950 -msgid "Stop Modules at " -msgstr "Modülleri Durdur : " - -#: mod_configure.erl:954 -msgid "Choose modules to stop" -msgstr "Durdurulacak modülleri seçiniz" - -#: mod_configure.erl:969 -msgid "Start Modules at " -msgstr "Modülleri Başlat : " - -#: mod_configure.erl:973 -msgid "Enter list of {Module, [Options]}" -msgstr "{Module, [Options]} listesi giriniz" - -#: mod_configure.erl:974 -msgid "List of modules to start" -msgstr "Başlatılacak modüllerin listesi" - -#: mod_configure.erl:983 -msgid "Backup to File at " -msgstr "Dosyaya Yedekle : " - -#: mod_configure.erl:987 mod_configure.erl:1001 -msgid "Enter path to backup file" -msgstr "Yedek dosyasının yolunu giriniz" - -#: mod_configure.erl:988 mod_configure.erl:1002 mod_configure.erl:1016 -#: mod_configure.erl:1030 -msgid "Path to File" -msgstr "Dosyanın Yolu" - -#: mod_configure.erl:997 -msgid "Restore Backup from File at " -msgstr "Dosyadaki Yedekten Geri Al : " - -#: mod_configure.erl:1011 -msgid "Dump Backup to Text File at " -msgstr "Metin Dosyasına Döküm Alarak Yedekle : " - -#: mod_configure.erl:1015 -msgid "Enter path to text file" -msgstr "Metin dosyasının yolunu giriniz" - -#: mod_configure.erl:1025 -msgid "Import User from File at " -msgstr "Dosyadan Kullanıcıları İçe Aktar : " - -#: mod_configure.erl:1029 -msgid "Enter path to jabberd14 spool file" -msgstr "jabberd14 spool dosyası için yol giriniz" - -#: mod_configure.erl:1039 -msgid "Import Users from Dir at " -msgstr "Dizinden Kullanıcıları İçe Aktar : " - -#: mod_configure.erl:1043 -msgid "Enter path to jabberd14 spool dir" -msgstr "jabberd14 spool dosyası için yol giriniz" - -#: mod_configure.erl:1044 -msgid "Path to Dir" -msgstr "Dizinin Yolu" - -#: mod_configure.erl:1060 mod_configure.erl:1105 -msgid "Time delay" -msgstr "Zaman gecikmesi" - -#: mod_configure.erl:1143 -msgid "Access Control List Configuration" -msgstr "Erişim Kontrol Listelerinin Ayarlanması (ACL)" - -#: mod_configure.erl:1147 -msgid "Access control lists" -msgstr "Erişim kontrol listeleri (ACL)" - -#: mod_configure.erl:1171 -msgid "Access Configuration" -msgstr "Erişim Ayarları" - -#: mod_configure.erl:1175 -msgid "Access rules" -msgstr "Erişim kuralları" - -#: mod_configure.erl:1200 mod_configure.erl:1222 mod_configure.erl:1234 -#: mod_configure.erl:1246 mod_configure.erl:1258 mod_configure.erl:1275 -#: mod_configure.erl:1287 mod_configure.erl:1650 mod_configure.erl:1700 -#: mod_configure.erl:1721 mod_roster.erl:943 mod_roster_odbc.erl:1060 -#: mod_vcard.erl:472 mod_vcard_ldap.erl:554 mod_vcard_odbc.erl:448 -msgid "Jabber ID" -msgstr "Jabber ID" - -#: mod_configure.erl:1205 mod_configure.erl:1263 mod_configure.erl:1651 -#: mod_configure.erl:1863 mod_muc/mod_muc_room.erl:3224 mod_register.erl:235 -#: web/ejabberd_web_admin.erl:1520 -msgid "Password" -msgstr "Parola" - -#: mod_configure.erl:1210 -msgid "Password Verification" -msgstr "Parola Doğrulaması" - -#: mod_configure.erl:1301 -msgid "Number of registered users" -msgstr "Kayıtlı kullanıcı sayısı" - -#: mod_configure.erl:1315 -msgid "Number of online users" -msgstr "Bağlı kullanıcı sayısı" - -#: mod_configure.erl:1682 web/ejabberd_web_admin.erl:1588 -#: web/ejabberd_web_admin.erl:1743 -msgid "Never" -msgstr "Asla" - -#: mod_configure.erl:1696 web/ejabberd_web_admin.erl:1601 -#: web/ejabberd_web_admin.erl:1756 -msgid "Online" -msgstr "Bağlı" - -#: mod_configure.erl:1701 -msgid "Last login" -msgstr "Son giriş" - -#: mod_configure.erl:1722 -msgid "Roster size" -msgstr "İsim listesi boyutu" - -#: mod_configure.erl:1723 -msgid "IP addresses" -msgstr "IP adresleri" - -#: mod_configure.erl:1724 -msgid "Resources" -msgstr "Kaynaklar" - -#: mod_configure.erl:1850 -msgid "Administration of " -msgstr "Yönetim : " - -#: mod_configure.erl:1853 -msgid "Action on user" -msgstr "Kullanıcıya uygulanacak eylem" - -#: mod_configure.erl:1857 -msgid "Edit Properties" -msgstr "Özellikleri Düzenle" - -#: mod_configure.erl:1860 web/ejabberd_web_admin.erl:1769 -msgid "Remove User" -msgstr "Kullanıcıyı Kaldır" - -#: mod_irc/mod_irc.erl:201 mod_irc/mod_irc_odbc.erl:196 -#: mod_muc/mod_muc.erl:336 mod_muc/mod_muc_odbc.erl:342 -msgid "Access denied by service policy" -msgstr "Servis politikası gereği erişim engellendi" - -#: mod_irc/mod_irc.erl:401 mod_irc/mod_irc_odbc.erl:398 -msgid "IRC Transport" -msgstr "IRC Nakli (Transport)" - -#: mod_irc/mod_irc.erl:428 mod_irc/mod_irc_odbc.erl:425 -msgid "ejabberd IRC module" -msgstr "ejabberd IRC modülü" - -#: mod_irc/mod_irc.erl:559 mod_irc/mod_irc_odbc.erl:568 -msgid "You need an x:data capable client to configure mod_irc settings" -msgstr "" -"mod_irc ayarlarını düzenlemek için x:data becerisine sahip bir istemciye " -"gereksinimiz var" - -#: mod_irc/mod_irc.erl:566 mod_irc/mod_irc_odbc.erl:575 -msgid "Registration in mod_irc for " -msgstr "mod_irc'ye kayıt : " - -#: mod_irc/mod_irc.erl:571 mod_irc/mod_irc_odbc.erl:580 -msgid "" -"Enter username, encodings, ports and passwords you wish to use for " -"connecting to IRC servers" -msgstr "" -"IRC sunuculara bağlanmak için kullanmak istediğiniz kullanıcı ismi, " -"kodlamalar, kapılar (portlar) ve parolaları giriniz" - -#: mod_irc/mod_irc.erl:576 mod_irc/mod_irc_odbc.erl:585 -msgid "IRC Username" -msgstr "IRC Kullanıcı İsmi" - -#: mod_irc/mod_irc.erl:586 mod_irc/mod_irc_odbc.erl:595 -msgid "" -"If you want to specify different ports, passwords, encodings for IRC " -"servers, fill this list with values in format '{\"irc server\", \"encoding" -"\", port, \"password\"}'. By default this service use \"~s\" encoding, port " -"~p, empty password." -msgstr "" -"IRC sunucuları için farklı kapılar (portlar), parolalar, kodlamalar " -"belirtmek istiyorsanız, '{\"irc sunucusu\", \"kodlama\",\"kapı\",\"parola" -"\"}' biçeminde değerlerle bu listeyi doldurunuz. Öntanımlı olarak bu servis " -"\"~s\" kodlamasını, ~p \"kapısını\", \"boş\" parolasını kullanıyor." - -#: mod_irc/mod_irc.erl:598 mod_irc/mod_irc_odbc.erl:607 -msgid "" -"Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef." -"net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]." -msgstr "" -"Örnek: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"gizli\"}, {\"vendetta.fef.net" -"\", \"iso8859-1\", 7000}], {\"irc.sometestserver.net\", \"utf-8\"}]" - -#: mod_irc/mod_irc.erl:603 mod_irc/mod_irc_odbc.erl:612 -msgid "Connections parameters" -msgstr "Bağlantı parametreleri" - -#: mod_irc/mod_irc.erl:728 mod_irc/mod_irc_odbc.erl:757 -msgid "Join IRC channel" -msgstr "IRC kanalına katıl" - -#: mod_irc/mod_irc.erl:732 mod_irc/mod_irc_odbc.erl:761 -msgid "IRC channel (don't put the first #)" -msgstr "IRC kanalı (ilk # işaretini koymayın)" - -#: mod_irc/mod_irc.erl:737 mod_irc/mod_irc_odbc.erl:766 -msgid "IRC server" -msgstr "IRC sunucusu" - -#: mod_irc/mod_irc.erl:770 mod_irc/mod_irc.erl:774 -#: mod_irc/mod_irc_odbc.erl:799 mod_irc/mod_irc_odbc.erl:803 -msgid "Join the IRC channel here." -msgstr "Buradaki IRC kanalına katıl." - -#: mod_irc/mod_irc.erl:778 mod_irc/mod_irc_odbc.erl:807 -msgid "Join the IRC channel in this Jabber ID: ~s" -msgstr "IRC kanalına bu Jabber ID'si ile katıl: ~s" - -#: mod_irc/mod_irc.erl:863 mod_irc/mod_irc_odbc.erl:904 -msgid "IRC settings" -msgstr "IRC ayarları" - -#: mod_irc/mod_irc.erl:868 mod_irc/mod_irc_odbc.erl:909 -msgid "" -"Enter username and encodings you wish to use for connecting to IRC servers. " -"Press 'Next' to get more fields to fill in. Press 'Complete' to save " -"settings." -msgstr "" -"IRC sunuculara bağlanmak için kullanmak istediğiniz kullanıcı isimleri ve " -"kodlamaları giriniz. 'İleri' tuşuna basınca karşınıza dolduracak daha fazla " -"alan çıkacak. 'Tamamla' tuşuna basarak ayarları kaydedin." - -#: mod_irc/mod_irc.erl:874 mod_irc/mod_irc_odbc.erl:915 -msgid "IRC username" -msgstr "IRC kullanıcı ismi" - -#: mod_irc/mod_irc.erl:923 mod_irc/mod_irc_odbc.erl:964 -msgid "Password ~b" -msgstr "Parola ~b" - -#: mod_irc/mod_irc.erl:928 mod_irc/mod_irc_odbc.erl:969 -msgid "Port ~b" -msgstr "Kapı (Port) ~b" - -#: mod_irc/mod_irc.erl:933 mod_irc/mod_irc_odbc.erl:974 -msgid "Encoding for server ~b" -msgstr "Sunucu için kodlama ~b" - -#: mod_irc/mod_irc.erl:942 mod_irc/mod_irc_odbc.erl:983 -msgid "Server ~b" -msgstr "Sunucu ~b" - -#: mod_muc/mod_muc.erl:449 mod_muc/mod_muc_odbc.erl:456 -msgid "Only service administrators are allowed to send service messages" -msgstr "Sadece servis yöneticileri servis mesajı gönderebilirler" - -#: mod_muc/mod_muc.erl:493 mod_muc/mod_muc_odbc.erl:500 -msgid "Room creation is denied by service policy" -msgstr "Odanın oluşturulması servis politikası gereği reddedildi" - -#: mod_muc/mod_muc.erl:500 mod_muc/mod_muc_odbc.erl:507 -msgid "Conference room does not exist" -msgstr "Konferans odası bulunamadı" - -#: mod_muc/mod_muc.erl:582 mod_muc/mod_muc_odbc.erl:595 -msgid "Chatrooms" -msgstr "Sohbet Odaları" - -#: mod_muc/mod_muc.erl:712 mod_muc/mod_muc_odbc.erl:729 -msgid "You need a client that supports x:data to register the nickname" -msgstr "" -"Takma isminizi kaydettirmek için x:data destekleyen bir istemciye " -"gereksinimiz var" - -#: mod_muc/mod_muc.erl:718 mod_muc/mod_muc_odbc.erl:735 -msgid "Nickname Registration at " -msgstr "Takma İsim Kaydı : " - -#: mod_muc/mod_muc.erl:722 mod_muc/mod_muc_odbc.erl:739 -msgid "Enter nickname you want to register" -msgstr "Kaydettirmek istediğiniz takma ismi giriniz" - -#: mod_muc/mod_muc.erl:723 mod_muc/mod_muc_odbc.erl:740 -#: mod_muc/mod_muc_room.erl:3810 mod_roster.erl:944 mod_roster_odbc.erl:1061 -#: mod_vcard.erl:364 mod_vcard.erl:477 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:453 -msgid "Nickname" -msgstr "Takma isim" - -#: mod_muc/mod_muc.erl:762 mod_muc/mod_muc_odbc.erl:784 -#: mod_muc/mod_muc_room.erl:1064 mod_muc/mod_muc_room.erl:1787 -msgid "That nickname is registered by another person" -msgstr "O takma isim başka biri tarafından kaydettirilmiş" - -#: mod_muc/mod_muc.erl:788 mod_muc/mod_muc_odbc.erl:811 -msgid "You must fill in field \"Nickname\" in the form" -msgstr "Formda \"Takma isim\" alanını doldurmanız gerekiyor" - -#: mod_muc/mod_muc.erl:808 mod_muc/mod_muc_odbc.erl:831 -msgid "ejabberd MUC module" -msgstr "ejabberd MUC modülü" - -#: mod_muc/mod_muc_log.erl:374 mod_muc/mod_muc_log.erl:381 -msgid "Chatroom configuration modified" -msgstr "Sohbet odası ayarı değiştirildi" - -#: mod_muc/mod_muc_log.erl:384 -msgid "joins the room" -msgstr "odaya katıldı" - -#: mod_muc/mod_muc_log.erl:387 mod_muc/mod_muc_log.erl:390 -msgid "leaves the room" -msgstr "odadan ayrıldı" - -#: mod_muc/mod_muc_log.erl:393 mod_muc/mod_muc_log.erl:396 -msgid "has been banned" -msgstr "odaya girmesi yasaklandı" - -#: mod_muc/mod_muc_log.erl:399 mod_muc/mod_muc_log.erl:402 -msgid "has been kicked" -msgstr "odadan atıldı" - -#: mod_muc/mod_muc_log.erl:405 -msgid "has been kicked because of an affiliation change" -msgstr "ilişki değişikliğinden dolayı atıldı" - -#: mod_muc/mod_muc_log.erl:408 -msgid "has been kicked because the room has been changed to members-only" -msgstr "oda üyelere-özel hale getirildiğinden dolayı atıldı" - -#: mod_muc/mod_muc_log.erl:411 -msgid "has been kicked because of a system shutdown" -msgstr "sistem kapandığından dolayı atıldı" - -#: mod_muc/mod_muc_log.erl:414 -msgid "is now known as" -msgstr "isim değiştirdi :" - -#: mod_muc/mod_muc_log.erl:417 mod_muc/mod_muc_log.erl:688 -#: mod_muc/mod_muc_room.erl:2393 -msgid " has set the subject to: " -msgstr " konuyu değiştirdi: " - -#: mod_muc/mod_muc_log.erl:452 -msgid "Chatroom is created" -msgstr "Sohbet odası oluşturuldu" - -#: mod_muc/mod_muc_log.erl:453 -msgid "Chatroom is destroyed" -msgstr "Sohbet odası kaldırıldı" - -#: mod_muc/mod_muc_log.erl:454 -msgid "Chatroom is started" -msgstr "Sohbet odası başlatıldı" - -#: mod_muc/mod_muc_log.erl:455 -msgid "Chatroom is stopped" -msgstr "Sohbet odası durduruldu" - -#: mod_muc/mod_muc_log.erl:459 -msgid "Monday" -msgstr "Pazartesi" - -#: mod_muc/mod_muc_log.erl:460 -msgid "Tuesday" -msgstr "Salı" - -#: mod_muc/mod_muc_log.erl:461 -msgid "Wednesday" -msgstr "Çarşamba" - -#: mod_muc/mod_muc_log.erl:462 -msgid "Thursday" -msgstr "Perşembe" - -#: mod_muc/mod_muc_log.erl:463 -msgid "Friday" -msgstr "Cuma" - -#: mod_muc/mod_muc_log.erl:464 -msgid "Saturday" -msgstr "Cumartesi" - -#: mod_muc/mod_muc_log.erl:465 -msgid "Sunday" -msgstr "Pazar" - -#: mod_muc/mod_muc_log.erl:469 -msgid "January" -msgstr "Ocak" - -#: mod_muc/mod_muc_log.erl:470 -msgid "February" -msgstr "Şubat" - -#: mod_muc/mod_muc_log.erl:471 -msgid "March" -msgstr "Mart" - -#: mod_muc/mod_muc_log.erl:472 -msgid "April" -msgstr "Nisan" - -#: mod_muc/mod_muc_log.erl:473 -msgid "May" -msgstr "Mayıs" - -#: mod_muc/mod_muc_log.erl:474 -msgid "June" -msgstr "Haziran" - -#: mod_muc/mod_muc_log.erl:475 -msgid "July" -msgstr "Temmuz" - -#: mod_muc/mod_muc_log.erl:476 -msgid "August" -msgstr "Ağustos" - -#: mod_muc/mod_muc_log.erl:477 -msgid "September" -msgstr "Eylül" - -#: mod_muc/mod_muc_log.erl:478 -msgid "October" -msgstr "Ekim" - -#: mod_muc/mod_muc_log.erl:479 -msgid "November" -msgstr "Kasım" - -#: mod_muc/mod_muc_log.erl:480 -msgid "December" -msgstr "Aralık" - -#: mod_muc/mod_muc_log.erl:750 -msgid "Room Configuration" -msgstr "Oda Ayarları" - -#: mod_muc/mod_muc_log.erl:759 -msgid "Room Occupants" -msgstr "Oda Sakini Sayısı" - -#: mod_muc/mod_muc_room.erl:174 -msgid "Traffic rate limit is exceeded" -msgstr "Trafik oran sınırı aşıldı" - -#: mod_muc/mod_muc_room.erl:246 -msgid "" -"This participant is kicked from the room because he sent an error message" -msgstr "Bu katılımcı bir hata mesajı gönderdiği için odadan atıldı" - -#: mod_muc/mod_muc_room.erl:255 -msgid "It is not allowed to send private messages to the conference" -msgstr "Konferansa özel mesajlar gönderilmesine izin verilmiyor" - -#: mod_muc/mod_muc_room.erl:332 -msgid "Please, wait for a while before sending new voice request" -msgstr "Lütfen yeni bir ses isteği göndermeden önce biraz bekleyin" - -#: mod_muc/mod_muc_room.erl:347 -msgid "Voice requests are disabled in this conference" -msgstr "Bu konferansta ses istekleri etkisizleştirilmiş durumda." - -#: mod_muc/mod_muc_room.erl:364 -msgid "Failed to extract JID from your voice request approval" -msgstr "Ses isteği onayınızdan JID bilginize ulaşılamadı" - -#: mod_muc/mod_muc_room.erl:393 -msgid "Only moderators can approve voice requests" -msgstr "Yalnız moderatörler ses isteklerini onaylayabilir" - -#: mod_muc/mod_muc_room.erl:408 -msgid "Improper message type" -msgstr "Uygunsuz mesaj tipi" - -#: mod_muc/mod_muc_room.erl:518 -msgid "" -"This participant is kicked from the room because he sent an error message to " -"another participant" -msgstr "" -"Bu katılımcı başka bir katılımcıya bir hata mesajı gönderdiği için odadan " -"atıldı" - -#: mod_muc/mod_muc_room.erl:531 -msgid "It is not allowed to send private messages of type \"groupchat\"" -msgstr "\"groupchat\" tipinde özel mesajlar gönderilmesine izin verilmiyor" - -#: mod_muc/mod_muc_room.erl:543 mod_muc/mod_muc_room.erl:611 -msgid "Recipient is not in the conference room" -msgstr "Alıcı konferans odasında değil" - -#: mod_muc/mod_muc_room.erl:564 mod_muc/mod_muc_room.erl:585 -msgid "It is not allowed to send private messages" -msgstr "Özel mesaj gönderilmesine izin verilmiyor" - -#: mod_muc/mod_muc_room.erl:576 mod_muc/mod_muc_room.erl:956 -#: mod_muc/mod_muc_room.erl:4040 -msgid "Only occupants are allowed to send messages to the conference" -msgstr "Sadece oda sakinlerinin konferansa mesaj göndermesine izin veriliyor" - -#: mod_muc/mod_muc_room.erl:634 -msgid "Only occupants are allowed to send queries to the conference" -msgstr "Sadece oda sakinlerinin konferansa sorgu göndermesine izin veriliyor" - -#: mod_muc/mod_muc_room.erl:646 -msgid "Queries to the conference members are not allowed in this room" -msgstr "Bu odada konferans üyelerine sorgu yapılmasına izin verilmiyor" - -#: mod_muc/mod_muc_room.erl:932 -msgid "" -"Only moderators and participants are allowed to change the subject in this " -"room" -msgstr "" -"Sadece moderatörlerin ve katılımcıların bu odanın konusunu değiştirmesine " -"izin veriliyor" - -#: mod_muc/mod_muc_room.erl:937 -msgid "Only moderators are allowed to change the subject in this room" -msgstr "Sadece moderatörlerin bu odanın konusunu değiştirmesine izin veriliyor" - -#: mod_muc/mod_muc_room.erl:947 -msgid "Visitors are not allowed to send messages to all occupants" -msgstr "" -"Ziyaretçilerin odadaki tüm sakinlere mesaj göndermesine izin verilmiyor" - -#: mod_muc/mod_muc_room.erl:1021 -msgid "" -"This participant is kicked from the room because he sent an error presence" -msgstr "Bu katılımcı bir hata varlığı (presence) gönderdiği için odadan atıldı" - -#: mod_muc/mod_muc_room.erl:1040 -msgid "Visitors are not allowed to change their nicknames in this room" -msgstr "" -"Bu odada ziyaretçilerin takma isimlerini değiştirmesine izin verilmiyor" - -#: mod_muc/mod_muc_room.erl:1053 mod_muc/mod_muc_room.erl:1779 -msgid "That nickname is already in use by another occupant" -msgstr "Takma isim odanın başka bir sakini tarafından halihazırda kullanımda" - -#: mod_muc/mod_muc_room.erl:1768 -msgid "You have been banned from this room" -msgstr "Bu odaya girmeniz yasaklandı" - -#: mod_muc/mod_muc_room.erl:1771 -msgid "Membership is required to enter this room" -msgstr "Bu odaya girmek için üyelik gerekiyor" - -#: mod_muc/mod_muc_room.erl:1807 -msgid "This room is not anonymous" -msgstr "Bu oda anonim değil" - -#: mod_muc/mod_muc_room.erl:1833 -msgid "A password is required to enter this room" -msgstr "Bu odaya girmek için parola gerekiyor" - -#: mod_muc/mod_muc_room.erl:1855 mod_register.erl:246 -msgid "Too many CAPTCHA requests" -msgstr "Çok fazla CAPTCHA isteği" - -#: mod_muc/mod_muc_room.erl:1864 mod_register.erl:251 -msgid "Unable to generate a CAPTCHA" -msgstr "İnsan doğrulaması (CAPTCHA) oluşturulamadı" - -#: mod_muc/mod_muc_room.erl:1874 -msgid "Incorrect password" -msgstr "Yanlış parola" - -#: mod_muc/mod_muc_room.erl:2448 -msgid "Administrator privileges required" -msgstr "Yönetim yetkileri gerekli" - -#: mod_muc/mod_muc_room.erl:2463 -msgid "Moderator privileges required" -msgstr "Moderatör yetkileri gerekli" - -#: mod_muc/mod_muc_room.erl:2619 -msgid "Jabber ID ~s is invalid" -msgstr "Jabber ID ~s geçersiz" - -#: mod_muc/mod_muc_room.erl:2633 -msgid "Nickname ~s does not exist in the room" -msgstr "~s takma ismi odada yok" - -#: mod_muc/mod_muc_room.erl:2659 mod_muc/mod_muc_room.erl:3049 -msgid "Invalid affiliation: ~s" -msgstr "Geçersiz ilişki: ~s" - -#: mod_muc/mod_muc_room.erl:2713 -msgid "Invalid role: ~s" -msgstr "Geçersiz rol: ~s" - -#: mod_muc/mod_muc_room.erl:3026 mod_muc/mod_muc_room.erl:3062 -msgid "Owner privileges required" -msgstr "Sahip yetkileri gerekli" - -#: mod_muc/mod_muc_room.erl:3195 -msgid "Configuration of room ~s" -msgstr "~s odasının ayarları" - -#: mod_muc/mod_muc_room.erl:3200 -msgid "Room title" -msgstr "Oda başlığı" - -#: mod_muc/mod_muc_room.erl:3203 mod_muc/mod_muc_room.erl:3692 -msgid "Room description" -msgstr "Oda tanımı" - -#: mod_muc/mod_muc_room.erl:3210 -msgid "Make room persistent" -msgstr "Odayı kalıcı hale getir" - -#: mod_muc/mod_muc_room.erl:3215 -msgid "Make room public searchable" -msgstr "Odayı herkes tarafından aranabilir hale getir" - -#: mod_muc/mod_muc_room.erl:3218 -msgid "Make participants list public" -msgstr "Katılımcı listesini herkese açık hale getir" - -#: mod_muc/mod_muc_room.erl:3221 -msgid "Make room password protected" -msgstr "Odayı parola korumalı hale getir" - -#: mod_muc/mod_muc_room.erl:3232 -msgid "Maximum Number of Occupants" -msgstr "Odada En Fazla Bulunabilecek Kişi Sayısı" - -#: mod_muc/mod_muc_room.erl:3239 -msgid "No limit" -msgstr "Sınırsız" - -#: mod_muc/mod_muc_room.erl:3250 -msgid "Present real Jabber IDs to" -msgstr "Gerçek Jabber ID'lerini göster :" - -#: mod_muc/mod_muc_room.erl:3258 mod_muc/mod_muc_room.erl:3292 -msgid "moderators only" -msgstr "sadece moderatörler" - -#: mod_muc/mod_muc_room.erl:3260 mod_muc/mod_muc_room.erl:3294 -msgid "anyone" -msgstr "herkes" - -#: mod_muc/mod_muc_room.erl:3262 -msgid "Make room members-only" -msgstr "Odayı sadece üyelere açık hale getir" - -#: mod_muc/mod_muc_room.erl:3265 -msgid "Make room moderated" -msgstr "Odayı moderasyonlu hale getir" - -#: mod_muc/mod_muc_room.erl:3268 -msgid "Default users as participants" -msgstr "Kullanıcılar öntanımlı olarak katılımcı olsun" - -#: mod_muc/mod_muc_room.erl:3271 -msgid "Allow users to change the subject" -msgstr "Kullanıcıların konu değiştirmesine izin ver" - -#: mod_muc/mod_muc_room.erl:3274 -msgid "Allow users to send private messages" -msgstr "Kullanıcıların özel mesaj göndermelerine izin ver" - -#: mod_muc/mod_muc_room.erl:3279 -msgid "Allow visitors to send private messages to" -msgstr "Ziyaretçilerin özel mesaj göndermelerine izin ver" - -#: mod_muc/mod_muc_room.erl:3290 -msgid "nobody" -msgstr "hiç kimse" - -#: mod_muc/mod_muc_room.erl:3296 -msgid "Allow users to query other users" -msgstr "Kullanıcıların diğer kullanıcıları sorgulamalarına izin ver" - -#: mod_muc/mod_muc_room.erl:3299 -msgid "Allow users to send invites" -msgstr "Kullanıcıların davetiye göndermelerine izin ver" - -#: mod_muc/mod_muc_room.erl:3302 -msgid "Allow visitors to send status text in presence updates" -msgstr "" -"Ziyaretçilerin varlık (presence) güncellemelerinde durum metni " -"göndermelerine izin ver" - -#: mod_muc/mod_muc_room.erl:3305 -msgid "Allow visitors to change nickname" -msgstr "Ziyaretçilerin takma isim değiştirmelerine izin ver" - -#: mod_muc/mod_muc_room.erl:3308 -msgid "Allow visitors to send voice requests" -msgstr "Ziyaretçilerin ses isteğine göndermelerine izin ver" - -#: mod_muc/mod_muc_room.erl:3311 -msgid "Minimum interval between voice requests (in seconds)" -msgstr "Ses istekleri arasında olabilecek en az aralık (saniye olarak)" - -#: mod_muc/mod_muc_room.erl:3317 -msgid "Make room CAPTCHA protected" -msgstr "Odayı insan doğrulaması (captcha) korumalı hale getir" - -#: mod_muc/mod_muc_room.erl:3322 -msgid "Exclude Jabber IDs from CAPTCHA challenge" -msgstr "CAPTCHA doğrulamasını şu Jabber ID'ler için yapma" - -#: mod_muc/mod_muc_room.erl:3329 -msgid "Enable logging" -msgstr "Kayıt tutma özelliğini aç" - -#: mod_muc/mod_muc_room.erl:3337 -msgid "You need an x:data capable client to configure room" -msgstr "" -"Odayı ayarlamak için x:data becerisine sahip bir istemciye gereksinimiz var" - -#: mod_muc/mod_muc_room.erl:3694 -msgid "Number of occupants" -msgstr "Oda sakini sayısı" - -#: mod_muc/mod_muc_room.erl:3750 -msgid "private, " -msgstr "özel" - -#: mod_muc/mod_muc_room.erl:3799 -msgid "Voice request" -msgstr "Ses isteği" - -#: mod_muc/mod_muc_room.erl:3803 -msgid "Either approve or decline the voice request." -msgstr "Ses isteğini kabul edin ya da reddedin" - -#: mod_muc/mod_muc_room.erl:3809 -msgid "User JID" -msgstr "Kullanıcı JID" - -#: mod_muc/mod_muc_room.erl:3811 -msgid "Grant voice to this person?" -msgstr "Bu kişiye ses verelim mi?" - -#: mod_muc/mod_muc_room.erl:3960 -msgid "~s invites you to the room ~s" -msgstr "~s sizi ~s odasına davet ediyor" - -#: mod_muc/mod_muc_room.erl:3969 -msgid "the password is" -msgstr "parola :" - -#: mod_offline.erl:510 mod_offline_odbc.erl:352 -msgid "" -"Your contact offline message queue is full. The message has been discarded." -msgstr "Çevirim-dışı mesaj kuyruğunuz dolu. Mesajını dikkate alınmadı." - -#: mod_offline.erl:560 mod_offline_odbc.erl:408 -msgid "~s's Offline Messages Queue" -msgstr "~s Kullanıcısının Mesaj Kuyruğu" - -#: mod_offline.erl:563 mod_offline_odbc.erl:411 mod_roster.erl:987 -#: mod_roster_odbc.erl:1104 mod_shared_roster.erl:901 -#: mod_shared_roster.erl:1010 mod_shared_roster_odbc.erl:930 -#: mod_shared_roster_odbc.erl:1039 web/ejabberd_web_admin.erl:833 -#: web/ejabberd_web_admin.erl:874 web/ejabberd_web_admin.erl:942 -#: web/ejabberd_web_admin.erl:978 web/ejabberd_web_admin.erl:1019 -#: web/ejabberd_web_admin.erl:1508 web/ejabberd_web_admin.erl:1760 -#: web/ejabberd_web_admin.erl:1931 web/ejabberd_web_admin.erl:1963 -#: web/ejabberd_web_admin.erl:2031 web/ejabberd_web_admin.erl:2135 -#: web/ejabberd_web_admin.erl:2160 web/ejabberd_web_admin.erl:2248 -msgid "Submitted" -msgstr "Gönderilenler" - -#: mod_offline.erl:571 -msgid "Time" -msgstr "Zaman" - -#: mod_offline.erl:572 -msgid "From" -msgstr "Kimden" - -#: mod_offline.erl:573 -msgid "To" -msgstr "Kime" - -#: mod_offline.erl:574 mod_offline_odbc.erl:419 -msgid "Packet" -msgstr "Paket" - -#: mod_offline.erl:587 mod_offline_odbc.erl:432 mod_shared_roster.erl:908 -#: mod_shared_roster_odbc.erl:937 web/ejabberd_web_admin.erl:882 -#: web/ejabberd_web_admin.erl:986 -msgid "Delete Selected" -msgstr "Seçilenleri Sil" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -msgid "Offline Messages:" -msgstr "Çevirim-dışı Mesajlar:" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -msgid "Remove All Offline Messages" -msgstr "Tüm Çevirim-dışı Mesajları Kaldır" - -#: mod_proxy65/mod_proxy65_service.erl:213 -msgid "ejabberd SOCKS5 Bytestreams module" -msgstr "ejabberd SOCKS5 Bytestreams modülü" - -#: mod_pubsub/mod_pubsub.erl:1120 mod_pubsub/mod_pubsub_odbc.erl:929 -msgid "Publish-Subscribe" -msgstr "Yayınla-Üye Ol" - -#: mod_pubsub/mod_pubsub.erl:1214 mod_pubsub/mod_pubsub_odbc.erl:1025 -msgid "ejabberd Publish-Subscribe module" -msgstr "ejabberd Publish-Subscribe modülü" - -#: mod_pubsub/mod_pubsub.erl:1497 mod_pubsub/mod_pubsub_odbc.erl:1312 -msgid "PubSub subscriber request" -msgstr "PubSub üye isteği" - -#: mod_pubsub/mod_pubsub.erl:1499 mod_pubsub/mod_pubsub_odbc.erl:1314 -msgid "Choose whether to approve this entity's subscription." -msgstr "Bu varlığın üyeliğini onaylayıp onaylamamayı seçiniz." - -#: mod_pubsub/mod_pubsub.erl:1505 mod_pubsub/mod_pubsub_odbc.erl:1320 -msgid "Node ID" -msgstr "Düğüm ID" - -#: mod_pubsub/mod_pubsub.erl:1510 mod_pubsub/mod_pubsub_odbc.erl:1325 -msgid "Subscriber Address" -msgstr "Üye Olanın Adresi" - -#: mod_pubsub/mod_pubsub.erl:1516 mod_pubsub/mod_pubsub_odbc.erl:1331 -msgid "Allow this Jabber ID to subscribe to this pubsub node?" -msgstr "Bu Jabber ID bu pubsub düğümüne üye olmasına izin verilsin mi?" - -#: mod_pubsub/mod_pubsub.erl:3474 mod_pubsub/mod_pubsub_odbc.erl:3287 -msgid "Deliver payloads with event notifications" -msgstr "Yükleri (payload) olay uyarıları ile beraber gönder" - -#: mod_pubsub/mod_pubsub.erl:3475 mod_pubsub/mod_pubsub_odbc.erl:3288 -msgid "Deliver event notifications" -msgstr "Olay uyarıları gönderilsin" - -#: mod_pubsub/mod_pubsub.erl:3476 mod_pubsub/mod_pubsub_odbc.erl:3289 -msgid "Notify subscribers when the node configuration changes" -msgstr "Düğüm ayarları değiştiğinde üyeleri uyar" - -#: mod_pubsub/mod_pubsub.erl:3477 mod_pubsub/mod_pubsub_odbc.erl:3290 -msgid "Notify subscribers when the node is deleted" -msgstr "Bir düğüm silindiğinde üyeleri uyar" - -#: mod_pubsub/mod_pubsub.erl:3478 mod_pubsub/mod_pubsub_odbc.erl:3291 -msgid "Notify subscribers when items are removed from the node" -msgstr "Düğümden öğeler kaldırıldığında üyeleri uyar" - -#: mod_pubsub/mod_pubsub.erl:3479 mod_pubsub/mod_pubsub_odbc.erl:3292 -msgid "Persist items to storage" -msgstr "Öğeleri depoda kalıcı hale getir" - -#: mod_pubsub/mod_pubsub.erl:3480 mod_pubsub/mod_pubsub_odbc.erl:3293 -msgid "A friendly name for the node" -msgstr "Düğüm için dostane bir isim" - -#: mod_pubsub/mod_pubsub.erl:3481 mod_pubsub/mod_pubsub_odbc.erl:3294 -msgid "Max # of items to persist" -msgstr "Kalıcı hale getirilecek en fazla öğe sayısı" - -#: mod_pubsub/mod_pubsub.erl:3482 mod_pubsub/mod_pubsub_odbc.erl:3295 -msgid "Whether to allow subscriptions" -msgstr "Üyeliklere izin verilsin mi" - -#: mod_pubsub/mod_pubsub.erl:3483 mod_pubsub/mod_pubsub_odbc.erl:3296 -msgid "Specify the access model" -msgstr "Erişim modelini belirtiniz" - -#: mod_pubsub/mod_pubsub.erl:3486 mod_pubsub/mod_pubsub_odbc.erl:3299 -msgid "Roster groups allowed to subscribe" -msgstr "Üye olunmasına izin verilen kontak listesi grupları" - -#: mod_pubsub/mod_pubsub.erl:3487 mod_pubsub/mod_pubsub_odbc.erl:3300 -msgid "Specify the publisher model" -msgstr "Yayıncı modelini belirtiniz" - -#: mod_pubsub/mod_pubsub.erl:3489 mod_pubsub/mod_pubsub_odbc.erl:3302 -msgid "Purge all items when the relevant publisher goes offline" -msgstr "İlgili yayıncı çevirimdışı olunca tüm onunla ilgili olanları sil" - -#: mod_pubsub/mod_pubsub.erl:3490 mod_pubsub/mod_pubsub_odbc.erl:3303 -msgid "Specify the event message type" -msgstr "Olay mesaj tipini belirtiniz" - -#: mod_pubsub/mod_pubsub.erl:3492 mod_pubsub/mod_pubsub_odbc.erl:3305 -msgid "Max payload size in bytes" -msgstr "En fazla yük (payload) boyutu (bayt olarak)" - -#: mod_pubsub/mod_pubsub.erl:3493 mod_pubsub/mod_pubsub_odbc.erl:3306 -msgid "When to send the last published item" -msgstr "Son yayınlanan öğe ne zaman gönderilsin" - -#: mod_pubsub/mod_pubsub.erl:3495 mod_pubsub/mod_pubsub_odbc.erl:3308 -msgid "Only deliver notifications to available users" -msgstr "Uyarıları sadece durumu uygun kullanıcılara ulaştır" - -#: mod_pubsub/mod_pubsub.erl:3496 mod_pubsub/mod_pubsub_odbc.erl:3309 -msgid "The collections with which a node is affiliated" -msgstr "Bir düğüm ile bağlantılı koleksiyonlar" - -#: mod_register.erl:193 -msgid "The CAPTCHA verification has failed" -msgstr "CAPTCHA doğrulaması başarısız oldu" - -#: mod_register.erl:220 -msgid "You need a client that supports x:data and CAPTCHA to register" -msgstr "" -"Takma isminizi kaydettirmek için x:data ve CAPTCHA destekleyen bir istemciye " -"gereksinimiz var" - -#: mod_register.erl:226 mod_register.erl:265 -msgid "Choose a username and password to register with this server" -msgstr "Bu sunucuya kayıt olmak için bir kullanıcı ismi ve parola seçiniz" - -#: mod_register.erl:230 mod_vcard.erl:364 mod_vcard_odbc.erl:342 -#: web/ejabberd_web_admin.erl:1515 web/ejabberd_web_admin.erl:1572 -msgid "User" -msgstr "Kullanıcı" - -#: mod_register.erl:316 mod_register.erl:361 -msgid "The password is too weak" -msgstr "Parola çok zayıf" - -#: mod_register.erl:365 -msgid "Users are not allowed to register accounts so quickly" -msgstr "Kullanıcıların bu kadar hızlı hesap açmalarına izin verilmiyor" - -#: mod_roster.erl:938 mod_roster_odbc.erl:1055 web/ejabberd_web_admin.erl:1701 -#: web/ejabberd_web_admin.erl:1886 web/ejabberd_web_admin.erl:1897 -#: web/ejabberd_web_admin.erl:2219 -msgid "None" -msgstr "Hiçbiri" - -#: mod_roster.erl:945 mod_roster_odbc.erl:1062 -msgid "Subscription" -msgstr "Üyelik" - -#: mod_roster.erl:946 mod_roster_odbc.erl:1063 -msgid "Pending" -msgstr "Sıra Bekleyen" - -#: mod_roster.erl:947 mod_roster_odbc.erl:1064 -msgid "Groups" -msgstr "Gruplar" - -#: mod_roster.erl:974 mod_roster_odbc.erl:1091 -msgid "Validate" -msgstr "Geçerli" - -#: mod_roster.erl:982 mod_roster_odbc.erl:1099 -msgid "Remove" -msgstr "Kaldır" - -#: mod_roster.erl:985 mod_roster_odbc.erl:1102 -msgid "Roster of " -msgstr "Kontak Listesi : " - -#: mod_roster.erl:988 mod_roster_odbc.erl:1105 mod_shared_roster.erl:902 -#: mod_shared_roster.erl:1011 mod_shared_roster_odbc.erl:931 -#: mod_shared_roster_odbc.erl:1040 web/ejabberd_web_admin.erl:834 -#: web/ejabberd_web_admin.erl:875 web/ejabberd_web_admin.erl:943 -#: web/ejabberd_web_admin.erl:979 web/ejabberd_web_admin.erl:1020 -#: web/ejabberd_web_admin.erl:1509 web/ejabberd_web_admin.erl:1761 -#: web/ejabberd_web_admin.erl:1932 web/ejabberd_web_admin.erl:2136 -#: web/ejabberd_web_admin.erl:2161 -msgid "Bad format" -msgstr "Kötü biçem" - -#: mod_roster.erl:995 mod_roster_odbc.erl:1112 -msgid "Add Jabber ID" -msgstr "Jabber ID'si Ekle" - -#: mod_roster.erl:1094 mod_roster_odbc.erl:1211 -msgid "Roster" -msgstr "Kontak Listesi" - -#: mod_shared_roster.erl:857 mod_shared_roster.erl:899 -#: mod_shared_roster.erl:1007 mod_shared_roster_odbc.erl:886 -#: mod_shared_roster_odbc.erl:928 mod_shared_roster_odbc.erl:1036 -msgid "Shared Roster Groups" -msgstr "Paylaşımlı Kontak Listesi Grupları" - -#: mod_shared_roster.erl:895 mod_shared_roster_odbc.erl:924 -#: web/ejabberd_web_admin.erl:1365 web/ejabberd_web_admin.erl:2461 -msgid "Add New" -msgstr "Yeni Ekle" - -#: mod_shared_roster.erl:978 mod_shared_roster_odbc.erl:1007 -msgid "Name:" -msgstr "İsim:" - -#: mod_shared_roster.erl:983 mod_shared_roster_odbc.erl:1012 -msgid "Description:" -msgstr "Tanım:" - -#: mod_shared_roster.erl:991 mod_shared_roster_odbc.erl:1020 -msgid "Members:" -msgstr "Üyeler:" - -#: mod_shared_roster.erl:999 mod_shared_roster_odbc.erl:1028 -msgid "Displayed Groups:" -msgstr "Gösterilen Gruplar:" - -#: mod_shared_roster.erl:1008 mod_shared_roster_odbc.erl:1037 -msgid "Group " -msgstr "Group " - -#: mod_shared_roster.erl:1017 mod_shared_roster_odbc.erl:1046 -#: web/ejabberd_web_admin.erl:840 web/ejabberd_web_admin.erl:884 -#: web/ejabberd_web_admin.erl:949 web/ejabberd_web_admin.erl:1026 -#: web/ejabberd_web_admin.erl:2017 -msgid "Submit" -msgstr "Gönder" - -#: mod_vcard.erl:165 mod_vcard_ldap.erl:238 mod_vcard_odbc.erl:129 -msgid "Erlang Jabber Server" -msgstr "Erlang Jabber Sunucusu" - -#: mod_vcard.erl:364 mod_vcard.erl:478 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:454 -msgid "Birthday" -msgstr "Doğumgünü" - -#: mod_vcard.erl:364 mod_vcard.erl:480 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:456 -msgid "City" -msgstr "İl" - -#: mod_vcard.erl:364 mod_vcard.erl:479 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:455 -msgid "Country" -msgstr "Ülke" - -#: mod_vcard.erl:364 mod_vcard.erl:481 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:457 -msgid "Email" -msgstr "E-posta" - -#: mod_vcard.erl:364 mod_vcard.erl:476 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:452 -msgid "Family Name" -msgstr "Soyisim" - -#: mod_vcard.erl:364 mod_vcard_odbc.erl:342 -msgid "" -"Fill in the form to search for any matching Jabber User (Add * to the end of " -"field to match substring)" -msgstr "" -"Eşleşen jabber kullanıcılarını aramak için formu doldurunuz (Alt dizgi " -"eşlemek için alanın sonuna * ekleyin)" - -#: mod_vcard.erl:364 mod_vcard.erl:473 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:449 -msgid "Full Name" -msgstr "Tam İsim" - -#: mod_vcard.erl:364 mod_vcard.erl:475 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:451 -msgid "Middle Name" -msgstr "Ortanca İsim" - -#: mod_vcard.erl:364 mod_vcard.erl:474 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:450 web/ejabberd_web_admin.erl:2006 -msgid "Name" -msgstr "İsim" - -#: mod_vcard.erl:364 mod_vcard.erl:482 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:458 -msgid "Organization Name" -msgstr "Kurum İsmi" - -#: mod_vcard.erl:364 mod_vcard.erl:483 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:459 -msgid "Organization Unit" -msgstr "Kurumun İlgili Birimi" - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "Search users in " -msgstr "Kullanıcılarda arama yap : " - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "You need an x:data capable client to search" -msgstr "" -"Arama yapabilmek için x:data becerisine sahip bir istemciye gereksinimiz var" - -#: mod_vcard.erl:389 mod_vcard_ldap.erl:487 mod_vcard_odbc.erl:367 -msgid "vCard User Search" -msgstr "vCard Kullanıcı Araması" - -#: mod_vcard.erl:445 mod_vcard_ldap.erl:541 mod_vcard_odbc.erl:421 -msgid "ejabberd vCard module" -msgstr "ejabberd vCard modülü" - -#: mod_vcard.erl:469 mod_vcard_ldap.erl:551 mod_vcard_odbc.erl:445 -msgid "Search Results for " -msgstr "Arama sonuçları : " - -#: mod_vcard_ldap.erl:462 -msgid "Fill in fields to search for any matching Jabber User" -msgstr "Eşleşen jabber kullanıcılarını aramak için alanları doldurunuz" - -#: web/ejabberd_web_admin.erl:193 web/ejabberd_web_admin.erl:203 -#: web/ejabberd_web_admin.erl:219 web/ejabberd_web_admin.erl:229 -msgid "Unauthorized" -msgstr "Yetkisiz" - -#: web/ejabberd_web_admin.erl:286 web/ejabberd_web_admin.erl:303 -msgid "ejabberd Web Admin" -msgstr "ejabberd Web Yöneticisi" - -#: web/ejabberd_web_admin.erl:769 web/ejabberd_web_admin.erl:780 -msgid "Administration" -msgstr "Yönetim" - -#: web/ejabberd_web_admin.erl:878 web/ejabberd_web_admin.erl:982 -msgid "Raw" -msgstr "Ham" - -#: web/ejabberd_web_admin.erl:1017 -msgid "~s access rule configuration" -msgstr "~s erişim kuralları ayarları" - -#: web/ejabberd_web_admin.erl:1035 -msgid "Virtual Hosts" -msgstr "Sanal Sunucuları" - -#: web/ejabberd_web_admin.erl:1043 web/ejabberd_web_admin.erl:1050 -msgid "Users" -msgstr "Kullanıcılar" - -#: web/ejabberd_web_admin.erl:1078 -msgid "Users Last Activity" -msgstr "Kullanıcıların Son Aktiviteleri" - -#: web/ejabberd_web_admin.erl:1080 -msgid "Period: " -msgstr "Periyot:" - -#: web/ejabberd_web_admin.erl:1090 -msgid "Last month" -msgstr "Geçen ay" - -#: web/ejabberd_web_admin.erl:1091 -msgid "Last year" -msgstr "Geçen yıl" - -#: web/ejabberd_web_admin.erl:1092 -msgid "All activity" -msgstr "Tüm aktivite" - -#: web/ejabberd_web_admin.erl:1094 -msgid "Show Ordinary Table" -msgstr "Sıradan Tabloyu Göster" - -#: web/ejabberd_web_admin.erl:1096 -msgid "Show Integral Table" -msgstr "Önemli Tabloyu Göster" - -#: web/ejabberd_web_admin.erl:1105 web/ejabberd_web_admin.erl:1939 -msgid "Statistics" -msgstr "İstatistikler" - -#: web/ejabberd_web_admin.erl:1117 -msgid "Not Found" -msgstr "Bulunamadı" - -#: web/ejabberd_web_admin.erl:1134 -msgid "Node not found" -msgstr "Düğüm bulunamadı" - -#: web/ejabberd_web_admin.erl:1460 -msgid "Host" -msgstr "Sunucu" - -#: web/ejabberd_web_admin.erl:1461 -msgid "Registered Users" -msgstr "Kayıtlı Kullanıcılar" - -#: web/ejabberd_web_admin.erl:1573 -msgid "Offline Messages" -msgstr "Çevirim-dışı Mesajlar" - -#: web/ejabberd_web_admin.erl:1574 web/ejabberd_web_admin.erl:1767 -msgid "Last Activity" -msgstr "Son Aktivite" - -#: web/ejabberd_web_admin.erl:1659 web/ejabberd_web_admin.erl:1675 -msgid "Registered Users:" -msgstr "Kayıtlı Kullanıcılar:" - -#: web/ejabberd_web_admin.erl:1661 web/ejabberd_web_admin.erl:1677 -#: web/ejabberd_web_admin.erl:2192 -msgid "Online Users:" -msgstr "Bağlı Kullanıcılar:" - -#: web/ejabberd_web_admin.erl:1663 -msgid "Outgoing s2s Connections:" -msgstr "Giden s2s Bağlantıları:" - -#: web/ejabberd_web_admin.erl:1665 -msgid "Outgoing s2s Servers:" -msgstr "Giden s2s Sunucuları" - -#: web/ejabberd_web_admin.erl:1734 web/mod_register_web.erl:175 -#: web/mod_register_web.erl:372 web/mod_register_web.erl:381 -#: web/mod_register_web.erl:411 -msgid "Change Password" -msgstr "Parola Değiştir" - -#: web/ejabberd_web_admin.erl:1758 -msgid "User " -msgstr "Kullanıcı " - -#: web/ejabberd_web_admin.erl:1765 -msgid "Connected Resources:" -msgstr "Bağlı Kaynaklar:" - -#: web/ejabberd_web_admin.erl:1766 web/mod_register_web.erl:227 -#: web/mod_register_web.erl:519 -msgid "Password:" -msgstr "Parola:" - -#: web/ejabberd_web_admin.erl:1828 -msgid "No Data" -msgstr "Veri Yok" - -#: web/ejabberd_web_admin.erl:1906 -msgid "Nodes" -msgstr "Düğümler" - -#: web/ejabberd_web_admin.erl:1929 web/ejabberd_web_admin.erl:1951 -msgid "Node " -msgstr "Düğüm " - -#: web/ejabberd_web_admin.erl:1938 -msgid "Listened Ports" -msgstr "Dinlenen Kapılar (Portlar)" - -#: web/ejabberd_web_admin.erl:1940 web/ejabberd_web_admin.erl:2260 -#: web/ejabberd_web_admin.erl:2448 -msgid "Update" -msgstr "GÜncelle" - -#: web/ejabberd_web_admin.erl:1943 web/ejabberd_web_admin.erl:2569 -msgid "Restart" -msgstr "Tekrar Başlat" - -#: web/ejabberd_web_admin.erl:1945 web/ejabberd_web_admin.erl:2571 -msgid "Stop" -msgstr "Durdur" - -#: web/ejabberd_web_admin.erl:1959 -msgid "RPC Call Error" -msgstr "RPC Çağrı Hatası" - -#: web/ejabberd_web_admin.erl:2000 -msgid "Database Tables at " -msgstr "Veritabanı Tabloları : " - -#: web/ejabberd_web_admin.erl:2007 -msgid "Storage Type" -msgstr "Depolama Tipi" - -#: web/ejabberd_web_admin.erl:2008 -msgid "Elements" -msgstr "Elementler" - -#: web/ejabberd_web_admin.erl:2009 -msgid "Memory" -msgstr "Bellek" - -#: web/ejabberd_web_admin.erl:2032 web/ejabberd_web_admin.erl:2137 -msgid "Error" -msgstr "Hata" - -#: web/ejabberd_web_admin.erl:2034 -msgid "Backup of " -msgstr "Yedek : " - -#: web/ejabberd_web_admin.erl:2036 -msgid "" -"Please note that these options will only backup the builtin Mnesia database. " -"If you are using the ODBC module, you also need to backup your SQL database " -"separately." -msgstr "" -"Bu seçeneklerin sadece gömülü Mnesia veritabanını yedekleyeceğine dikkat " -"edin. Eğer ODBC modülünü kullanıyorsanız, SQL veritabanınızı da ayrıca " -"yedeklemeniz gerekiyor." - -#: web/ejabberd_web_admin.erl:2041 -msgid "Store binary backup:" -msgstr "İkili yedeği sakla:" - -#: web/ejabberd_web_admin.erl:2045 web/ejabberd_web_admin.erl:2052 -#: web/ejabberd_web_admin.erl:2060 web/ejabberd_web_admin.erl:2067 -#: web/ejabberd_web_admin.erl:2074 web/ejabberd_web_admin.erl:2081 -#: web/ejabberd_web_admin.erl:2088 web/ejabberd_web_admin.erl:2096 -#: web/ejabberd_web_admin.erl:2103 web/ejabberd_web_admin.erl:2110 -msgid "OK" -msgstr "Tamam" - -#: web/ejabberd_web_admin.erl:2048 -msgid "Restore binary backup immediately:" -msgstr "Hemen ikili yedekten geri al:" - -#: web/ejabberd_web_admin.erl:2056 -msgid "" -"Restore binary backup after next ejabberd restart (requires less memory):" -msgstr "" -"ejabberd'nin bir sonraki tekrar başlatılışında ikili yedekten geri al (daha " -"az bellek gerektirir)" - -#: web/ejabberd_web_admin.erl:2063 -msgid "Store plain text backup:" -msgstr "Düz metin yedeği sakla:" - -#: web/ejabberd_web_admin.erl:2070 -msgid "Restore plain text backup immediately:" -msgstr "Hemen düz metin yedekten geri al" - -#: web/ejabberd_web_admin.erl:2077 -msgid "Import users data from a PIEFXIS file (XEP-0227):" -msgstr "Kullanıcıları bir PIEFXIS dosyasından (XEP-0227) içe aktar:" - -#: web/ejabberd_web_admin.erl:2084 -msgid "Export data of all users in the server to PIEFXIS files (XEP-0227):" -msgstr "" -"Sunucudaki tüm kullanıcıların verisini PIEFXIS dosyalarına (XEP-0227) dışa " -"aktar:" - -#: web/ejabberd_web_admin.erl:2091 -msgid "Export data of users in a host to PIEFXIS files (XEP-0227):" -msgstr "" -"Bir sunucudaki kullanıcıların verisini PIEFXIS dosyalarına (XEP-0227) dışa " -"aktar:" - -#: web/ejabberd_web_admin.erl:2099 -msgid "Import user data from jabberd14 spool file:" -msgstr "Jabberd 1.4 Spool Dosyalarından Kullanıcıları İçe Aktar:" - -#: web/ejabberd_web_admin.erl:2106 -msgid "Import users data from jabberd14 spool directory:" -msgstr "Jabberd 1.4 Spool Dizininden Kullanıcıları İçe Aktar:" - -#: web/ejabberd_web_admin.erl:2132 -msgid "Listened Ports at " -msgstr "Dinlenen Kapılar (Portlar) : " - -#: web/ejabberd_web_admin.erl:2157 -msgid "Modules at " -msgstr "Modüller : " - -#: web/ejabberd_web_admin.erl:2183 -msgid "Statistics of ~p" -msgstr "~p istatistikleri" - -#: web/ejabberd_web_admin.erl:2186 -msgid "Uptime:" -msgstr "Hizmet Süresi:" - -#: web/ejabberd_web_admin.erl:2189 -msgid "CPU Time:" -msgstr "İşlemci Zamanı:" - -#: web/ejabberd_web_admin.erl:2195 -msgid "Transactions Committed:" -msgstr "Tamamlanan Hareketler (Transactions Committed):" - -#: web/ejabberd_web_admin.erl:2198 -msgid "Transactions Aborted:" -msgstr "İptal Edilen Hareketler (Transactions):" - -#: web/ejabberd_web_admin.erl:2201 -msgid "Transactions Restarted:" -msgstr "Tekrar Başlatılan Hareketler (Transactions):" - -#: web/ejabberd_web_admin.erl:2204 -msgid "Transactions Logged:" -msgstr "Kaydı Tutulan Hareketler (Transactions):" - -#: web/ejabberd_web_admin.erl:2246 -msgid "Update " -msgstr "Güncelle " - -#: web/ejabberd_web_admin.erl:2254 -msgid "Update plan" -msgstr "Planı güncelle" - -#: web/ejabberd_web_admin.erl:2255 -msgid "Modified modules" -msgstr "Değişen modüller" - -#: web/ejabberd_web_admin.erl:2256 -msgid "Update script" -msgstr "Betiği Güncelle" - -#: web/ejabberd_web_admin.erl:2257 -msgid "Low level update script" -msgstr "Düşük seviye güncelleme betiği" - -#: web/ejabberd_web_admin.erl:2258 -msgid "Script check" -msgstr "Betik kontrolü" - -#: web/ejabberd_web_admin.erl:2426 -msgid "Port" -msgstr "Kapı (Port)" - -#: web/ejabberd_web_admin.erl:2427 -msgid "IP" -msgstr "IP" - -#: web/ejabberd_web_admin.erl:2428 -msgid "Protocol" -msgstr "Protokol" - -#: web/ejabberd_web_admin.erl:2429 web/ejabberd_web_admin.erl:2556 -msgid "Module" -msgstr "Modül" - -#: web/ejabberd_web_admin.erl:2430 web/ejabberd_web_admin.erl:2557 -msgid "Options" -msgstr "Seçenekler" - -#: web/ejabberd_web_admin.erl:2450 -msgid "Delete" -msgstr "Sil" - -#: web/ejabberd_web_admin.erl:2579 -msgid "Start" -msgstr "Başlat" - -#: web/mod_register_web.erl:104 -msgid "Your Jabber account was successfully created." -msgstr "Jabber hesabınız başarıyla oluşturuldu." - -#: web/mod_register_web.erl:107 -msgid "There was an error creating the account: " -msgstr "Hesap oluşturulurken bir hata oluştu:" - -#: web/mod_register_web.erl:115 -msgid "Your Jabber account was successfully deleted." -msgstr "Jabber hesabınız başarıyla silindi." - -#: web/mod_register_web.erl:118 -msgid "There was an error deleting the account: " -msgstr "Hesabın silinmesi sırasında bir hata oluştu:" - -#: web/mod_register_web.erl:128 -msgid "The password of your Jabber account was successfully changed." -msgstr "Jabber hesabınızın parolası başarıyla değiştirildi." - -#: web/mod_register_web.erl:131 -msgid "There was an error changing the password: " -msgstr "Parolanın değiştirilmesi sırasında bir hata oluştu:" - -#: web/mod_register_web.erl:163 web/mod_register_web.erl:172 -msgid "Jabber Account Registration" -msgstr "Jabber Hesap Kaydı" - -#: web/mod_register_web.erl:174 web/mod_register_web.erl:192 -#: web/mod_register_web.erl:201 -msgid "Register a Jabber account" -msgstr "Bir Jabber hesabı kaydet" - -#: web/mod_register_web.erl:176 web/mod_register_web.erl:493 -#: web/mod_register_web.erl:502 -msgid "Unregister a Jabber account" -msgstr "Bir Jabber hesabı kaydı sil" - -#: web/mod_register_web.erl:203 -msgid "" -"This page allows to create a Jabber account in this Jabber server. Your JID " -"(Jabber IDentifier) will be of the form: username@server. Please read " -"carefully the instructions to fill correctly the fields." -msgstr "" -"Bu sayfa bu Jabber sunucusunda bir Jabber hesabı oluşturulmasına olanak " -"tanıyor. Sizin JID'iniz (Jabber Tanımlayıcısı) şu biçemde olacaktır: " -"kullanici_adi@sunucu. Lütfen alanları doğru doldurabilmek için yönergeleri " -"dikkatle okuyunuz." - -#: web/mod_register_web.erl:212 web/mod_register_web.erl:386 -#: web/mod_register_web.erl:509 -msgid "Username:" -msgstr "Kullanıcı adı:" - -#: web/mod_register_web.erl:217 -msgid "This is case insensitive: macbeth is the same that MacBeth and Macbeth." -msgstr "" -"Burada büyük küçük harfi yapılmaz: macbeth ile MacBeth ve Macbeth aynıdır." - -#: web/mod_register_web.erl:218 -msgid "Characters not allowed:" -msgstr "İzin verilmeyen karakterler:" - -#: web/mod_register_web.erl:222 web/mod_register_web.erl:391 -#: web/mod_register_web.erl:514 -msgid "Server:" -msgstr "Sunucu:" - -#: web/mod_register_web.erl:232 -msgid "" -"Don't tell your password to anybody, not even the administrators of the " -"Jabber server." -msgstr "Parolanızı kimseye söylemeyin, Jabber sunucusunun yöneticilerine bile." - -#: web/mod_register_web.erl:234 -msgid "You can later change your password using a Jabber client." -msgstr "" -"Parolanızı daha sonra bir Jabber istemci kullanarak değiştirebilirsiniz." - -#: web/mod_register_web.erl:235 -msgid "" -"Some Jabber clients can store your password in your computer. Use that " -"feature only if you trust your computer is safe." -msgstr "" -"Bazı Jabber istemcileri parolanızı bilgisayarınızda saklayabilir. Bu " -"özelliği ancak bilgisayarın güvenli olduğuna güveniyorsanız kullanın." - -#: web/mod_register_web.erl:237 -msgid "" -"Memorize your password, or write it in a paper placed in a safe place. In " -"Jabber there isn't an automated way to recover your password if you forget " -"it." -msgstr "" -"Parolanızı ezberleyin ya da bir kağıda yazarak güvenli bir yerde saklayın. " -"Jabber'da parolanızı unutursanız, otomatik kurtarmak için bir yöntem " -"bulunmuyor." - -#: web/mod_register_web.erl:242 web/mod_register_web.erl:406 -msgid "Password Verification:" -msgstr "Parola Doğrulaması:" - -#: web/mod_register_web.erl:250 -msgid "Register" -msgstr "Kayıt Ol" - -#: web/mod_register_web.erl:396 -msgid "Old Password:" -msgstr "Eski Parola:" - -#: web/mod_register_web.erl:401 -msgid "New Password:" -msgstr "Yeni Parola:" - -#: web/mod_register_web.erl:504 -msgid "This page allows to unregister a Jabber account in this Jabber server." -msgstr "" -"Bu sayfa bu Jabber sunucusundan bir Jabber hesabının kaydını silmeye olanak " -"tanıyor." - -#: web/mod_register_web.erl:524 -msgid "Unregister" -msgstr "Kaydı Sil" diff --git a/priv/msgs/uk.msg b/priv/msgs/uk.msg index 6e21c909a..cf950ac73 100644 --- a/priv/msgs/uk.msg +++ b/priv/msgs/uk.msg @@ -1,130 +1,184 @@ -{"Access Configuration","Конфігурація доступу"}. -{"Access Control List Configuration","Конфігурація списків керування доступом"}. -{"Access control lists","Списки керування доступом"}. -{"Access Control Lists","Списки керування доступом"}. +%% Generated automatically +%% DO NOT EDIT: run `make translations` instead +%% To improve translations please read: +%% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ + +{" (Add * to the end of field to match substring)"," Заповніть поля для пошуку користувача Jabber (Додайте * в кінець поля для пошуку підрядка)"}. +{" has set the subject to: "," встановив(ла) тему: "}. +{"# participants","# учасників"}. +{"A description of the node","Опис вузла"}. +{"A friendly name for the node","Псевдонім для вузла"}. +{"A password is required to enter this room","Щоб зайти в цю конференцію, необхідно ввести пароль"}. +{"A Web Page","Веб-сторінка"}. +{"Accept","Прийняти"}. {"Access denied by service policy","Доступ заборонений політикою служби"}. -{"Access rules","Правила доступу"}. -{"Access Rules","Правила доступу"}. +{"Access model","Права доступу"}. +{"Account doesn't exist","Обліковий запис не існує"}. {"Action on user","Дія над користувачем"}. -{"Add Jabber ID","Додати Jabber ID"}. -{"Add New","Додати"}. +{"Add a hat to a user","Додати капелюх користувачу"}. {"Add User","Додати користувача"}. {"Administration of ","Адміністрування "}. {"Administration","Адміністрування"}. {"Administrator privileges required","Необхідні права адміністратора"}. -{"A friendly name for the node","Псевдонім для вузла"}. {"All activity","Вся статистика"}. -{"Allow this Jabber ID to subscribe to this pubsub node?","Чи дозволити цьому Jabber ID підписатись новини наданого вузла"}. +{"All Users","Всі користувачі"}. +{"Allow subscription","Дозволити підписку"}. +{"Allow this Jabber ID to subscribe to this pubsub node?","Дозволити цьому Jabber ID підписатись на даний pubsub-вузол?"}. +{"Allow this person to register with the room?","Дозволити цій людині зареєструватися в кімнаті?"}. {"Allow users to change the subject","Дозволити користувачам змінювати тему"}. {"Allow users to query other users","Дозволити iq-запити до користувачів"}. {"Allow users to send invites","Дозволити користувачам надсилати запрошення"}. {"Allow users to send private messages","Дозволити приватні повідомлення"}. {"Allow visitors to change nickname","Дозволити відвідувачам змінювати псевдонім"}. +{"Allow visitors to send private messages to","Дозволити відвідувачам відсилати приватні повідомлення"}. {"Allow visitors to send status text in presence updates","Дозволити відвідувачам відсилати текст статусу в оновленнях присутності"}. -{"All Users","Всі користувачі"}. +{"Allow visitors to send voice requests","Дозволити відвідувачам надсилати голосові запрошення"}. +{"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.","Асоційована група LDAP, яка визначає членство в кімнаті; це повинно бути відмінне ім'я LDAP відповідно до специфічного для реалізації або специфічного для розгортання визначення групи."}. {"Announcements","Сповіщення"}. -{"anyone","всім учасникам"}. -{"A password is required to enter this room","Щоб зайти в цю конференцію, необхідно ввести пароль"}. -{"April","квітня"}. -{"August","серпня"}. +{"Answer associated with a picture","Відповідь, пов’язана зі зображенням"}. +{"Answer associated with a video","Відповідь, пов'язана з відео"}. +{"Answer associated with speech","Відповідь, пов'язана з мовленням"}. +{"Answer to a question","Відповідь на запитання"}. +{"Anyone in the specified roster group(s) may subscribe and retrieve items","Будь-хто в зазначеній групі (групах) реєстру може підписатися та отримувати матеріали"}. +{"Anyone may associate leaf nodes with the collection","Будь-хто може асоціювати листові вузли з колекцією"}. +{"Anyone may publish","Будь-хто може публікувати"}. +{"Anyone may subscribe and retrieve items","Будь-хто може підписатися та отримувати публікації"}. +{"Anyone with a presence subscription of both or from may subscribe and retrieve items","Будь-хто, хто має підписку на отримання інформації про присутність в обох випадках або може підписуватись та отримувати матеріали"}. +{"Anyone with Voice","Будь-хто, хто має голос"}. +{"Anyone","Будь-хто"}. +{"API Commands","Команди API"}. +{"April","Квітень"}. +{"Arguments","Аргументи"}. +{"Attribute 'channel' is required for this request","Атрибут \"канал\" є обов'язковим для цього запиту"}. +{"Attribute 'id' is mandatory for MIX messages","Атрибут 'id' обов'язковий для MIX повідомлень"}. +{"Attribute 'jid' is not allowed here","Атрибут 'jid' заборонений"}. +{"Attribute 'node' is not allowed here","Атрибут \"вузол\" заборонений"}. +{"Attribute 'to' of stanza that triggered challenge","Атрибут \"до\" рядка, який спровокував виклик"}. +{"August","Серпень"}. +{"Automatic node creation is not enabled","Автоматичне створення вузлів не ввімкнено"}. {"Backup Management","Керування резервним копіюванням"}. -{"Backup of ","Резервне копіювання "}. +{"Backup of ~p","Резервне копіювання ~p"}. {"Backup to File at ","Резервне копіювання в файл на "}. {"Backup","Резервне копіювання"}. -{"Bad format","Неправильний формат"}. +{"Bad format","Невірний формат"}. {"Birthday","День народження"}. -{"CAPTCHA web page","Адреса капчі"}. +{"Both the username and the resource are required","Обов'язково потрібне ім'я користувача та джерело"}. +{"Bytestream already activated","Потік байтів вже активовано"}. +{"Cannot remove active list","Неможливо видалити активний список"}. +{"Cannot remove default list","Неможливо видалити список за замовчуванням"}. +{"CAPTCHA web page","Веб-сторінка CAPTCHA"}. +{"Challenge ID","ID виклику"}. {"Change Password","Змінити пароль"}. -{"Change User Password","Змінити Пароль Користувача"}. +{"Change User Password","Змінити пароль користувача"}. +{"Changing password is not allowed","Зміна пароля заборонена"}. +{"Changing role/affiliation is not allowed","Зміна ролі/рангу заборонена"}. +{"Channel already exists","Канал вже існує"}. +{"Channel does not exist","Каналу не існує"}. +{"Channel JID","Канали JID"}. +{"Channels","Канали"}. {"Characters not allowed:","Заборонені символи:"}. -{"Chatroom configuration modified","Конфігурація кімнати змінилась"}. -{"Chatroom is created","Створено кімнату"}. -{"Chatroom is destroyed","Знищено кімнату"}. -{"Chatroom is started","Запущено кімнату"}. -{"Chatroom is stopped","Зупинено кімнату"}. +{"Chatroom configuration modified","Конфігурацію кімнати змінено"}. +{"Chatroom is created","Кімнату створено"}. +{"Chatroom is destroyed","Кімнату видалено"}. +{"Chatroom is started","Кімнату запущено"}. +{"Chatroom is stopped","Кімнату зупинено"}. {"Chatrooms","Кімнати"}. -{"Choose a username and password to register with this server","Виберіть назву користувача та пароль для реєстрації на цьому сервері"}. -{"Choose modules to stop","Виберіть модулі, які необхідно зупинити"}. +{"Choose a username and password to register with this server","Виберіть логін і пароль для реєстрації на цьому сервері"}. {"Choose storage type of tables","Оберіть тип збереження таблиць"}. -{"Choose whether to approve this entity's subscription.","Вирішіть, чи задовольнити запит цього об'єкту на підписку"}. +{"Choose whether to approve this entity's subscription.","Виберіть, чи підтверджувати підписку."}. {"City","Місто"}. +{"Client acknowledged more stanzas than sent by server","Клієнт підтвердив більше повідомлень, ніж було відправлено сервером"}. +{"Clustering","Кластеризація"}. {"Commands","Команди"}. -{"Conference room does not exist","Конференція не існує"}. +{"Conference room does not exist","Кімната для переговорів відсутня"}. {"Configuration of room ~s","Конфігурація кімнати ~s"}. {"Configuration","Конфігурація"}. -{"Connected Resources:","Підключені ресурси:"}. -{"Connections parameters","Параметри з'єднання"}. +{"Contact Addresses (normally, room owner or owners)","Контактні адреси (зазвичай, власника або власників кімнати)"}. {"Country","Країна"}. -{"CPU Time:","Процесорний час:"}. -{"Database Tables at ","Таблиці бази даних на "}. +{"Current Discussion Topic","Поточна тема обговорення"}. +{"Database failure","Збій бази даних"}. {"Database Tables Configuration at ","Конфігурація таблиць бази даних на "}. {"Database","База даних"}. -{"December","грудня"}. -{"Default users as participants","Зробити користувачів учасниками за замовчуванням"}. +{"December","Грудень"}. +{"Default users as participants","Користувачі за замовчуванням як учасники"}. {"Delete message of the day on all hosts","Видалити повідомлення дня на усіх хостах"}. {"Delete message of the day","Видалити повідомлення дня"}. -{"Delete Selected","Видалити виділені"}. -{"Delete User","Видалити Користувача"}. -{"Delete","Видалити"}. +{"Delete User","Видалити користувача"}. {"Deliver event notifications","Доставляти сповіщення про події"}. {"Deliver payloads with event notifications","Доставляти разом з повідомленнями про публікації самі публікації"}. -{"Description:","Опис:"}. {"Disc only copy","Тільки диск"}. -{"Displayed Groups:","Видимі групи:"}. -{"Don't tell your password to anybody, not even the administrators of the Jabber server.","Нікому не кажіть свій пароль, навіть адміністраторам сервера."}. +{"Don't tell your password to anybody, not even the administrators of the XMPP server.","Нікому не кажіть свій пароль, навіть адміністраторам XMPP-сервера."}. {"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","Динамічно вказуйте відповідь видавця елемента"}. {"Edit Properties","Змінити параметри"}. -{"ejabberd IRC module","ejabberd IRC модуль"}. +{"Either approve or decline the voice request.","Підтвердіть або відхиліть голосовий запит."}. +{"ejabberd HTTP Upload service","Служба відвантаження по HTTP для ejabberd"}. {"ejabberd MUC module","ejabberd MUC модуль"}. +{"ejabberd Multicast service","Мультікаст ejabberd сервіс"}. {"ejabberd Publish-Subscribe module","Модуль ejabberd Публікації-Підписки"}. {"ejabberd SOCKS5 Bytestreams module","ejabberd SOCKS5 Bytestreams модуль"}. {"ejabberd vCard module","ejabberd vCard модуль"}. {"ejabberd Web Admin","Веб-інтерфейс Адміністрування ejabberd"}. -{"Elements","Елементи"}. +{"ejabberd","ejabberd"}. +{"Email Address","Адреса ел. пошти"}. {"Email","Електронна пошта"}. -{"Enable logging","Включити журнал роботи"}. -{"Encoding for server ~b","Кодування для сервера ~b"}. +{"Enable hats","Увімкнути капелюхи"}. +{"Enable logging","Увімкнути журнал роботи"}. +{"Enable message archiving","Ввімкнути архівацію повідомлень"}. +{"Enabling push without 'node' attribute is not supported","Увімкнення push без атрибута node не підтримується"}. {"End User Session","Закінчити Сеанс Користувача"}. -{"Enter list of {Module, [Options]}","Введіть перелік такого виду {Module, [Options]}"}. {"Enter nickname you want to register","Введіть псевдонім, який ви хочете зареєструвати"}. {"Enter path to backup file","Введіть шлях до резервного файла"}. {"Enter path to jabberd14 spool dir","Введіть шлях до директорії спула jabberd14"}. {"Enter path to jabberd14 spool file","Введіть шлях до файла зі спула jabberd14"}. {"Enter path to text file","Введіть шлях до текстового файла"}. {"Enter the text you see","Введіть текст, що ви бачите"}. -{"Enter username and encodings you wish to use for connecting to IRC servers. Press 'Next' to get more fields to fill in. Press 'Complete' to save settings.","Введіть ім'я користувача та кодування, які будуть використовуватися при підключенні до IRC-серверів Натисніть 'Далі' для заповнення додаткових полів. Натисніть 'Завершити' для збереження параметрів."}. -{"Enter username, encodings, ports and passwords you wish to use for connecting to IRC servers","Введіть ім'я користувача, кодування, порти та паролі, що будуть використовуватися при підключенні до IRC-серверів"}. -{"Erlang Jabber Server","Erlang Jabber Server"}. -{"Error","Помилка"}. -{"Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef.net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}].","Приклад: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef.net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]."}. +{"Erlang XMPP Server","Ерланґ XMPP Сервер"}. {"Exclude Jabber IDs from CAPTCHA challenge","Пропускати ці Jabber ID без CAPTCHA-запиту"}. +{"Export all tables as SQL queries to a file:","Експортувати всі таблиці у файл як SQL запити:"}. {"Export data of all users in the server to PIEFXIS files (XEP-0227):","Експорт даних всіх користувачів сервера до файлу PIEFXIS (XEP-0227):"}. {"Export data of users in a host to PIEFXIS files (XEP-0227):","Експорт даних користувачів домена до файлу PIEFXIS (XEP-0227):"}. +{"External component failure","Помилка зовнішнього компонента"}. +{"External component timeout","Тайм-аут зовнішнього компонента"}. +{"Failed to activate bytestream","Не вдалося активувати потік байтів"}. +{"Failed to extract JID from your voice request approval","Помилка витягнення JID з вашого схвалення голосового запиту"}. +{"Failed to map delegated namespace to external component","Не вдалося зіставити делегований простір імен із зовнішнім компонентом"}. +{"Failed to parse HTTP response","Не вдалося розібрати HTTP-відповідь"}. +{"Failed to process option '~s'","Не вдалося обробити параметр \"~s\""}. {"Family Name","Прізвище"}. -{"February","лютого"}. -{"Fill in fields to search for any matching Jabber User","Заповніть поля для пошуку користувача Jabber"}. -{"Fill in the form to search for any matching Jabber User (Add * to the end of field to match substring)","Заповніть поля для пошуку користувача Jabber (Додайте * в кінець поля для пошуку підрядка)"}. +{"FAQ Entry","Запис в ЧаПи"}. +{"February","Лютого"}. +{"File larger than ~w bytes","Файл більший, ніж ~w байт"}. +{"Fill in the form to search for any matching XMPP User","Заповніть форму для пошуку будь-якого відповідного користувача XMPP"}. {"Friday","П'ятниця"}. -{"From ~s","Від ~s"}. -{"From","Від кого"}. +{"From ~ts","Від ~ts"}. +{"Full List of Room Admins","Повний перелік адміністраторів кімнати"}. +{"Full List of Room Owners","Повний перелік власників кімнати"}. {"Full Name","Повне ім'я"}. +{"Get List of Online Users","Отримати кількість користувачів в мережі"}. +{"Get List of Registered Users","Отримати кількість зареєстрованих користувачів"}. {"Get Number of Online Users","Отримати Кількість Підключених Користувачів"}. {"Get Number of Registered Users","Отримати Кількість Зареєстрованих Користувачів"}. +{"Get Pending","Очікування"}. {"Get User Last Login Time","Отримати Час Останнього Підключення Користувача"}. -{"Get User Password","Отримати Пароль Користувача"}. {"Get User Statistics","Отримати Статистику по Користувачу"}. -{"Groups","Групи"}. -{"Group ","Група "}. +{"Given Name","По-батькові"}. +{"Grant voice to this person?","Надати голос персоні?"}. {"has been banned","заборонили вхід в кімнату"}. -{"has been kicked because of an affiliation change","вигнано з кімнати внаслідок зміни рангу"}. {"has been kicked because of a system shutdown","вигнано з кімнати внаслідок зупинки системи"}. +{"has been kicked because of an affiliation change","вигнано з кімнати внаслідок зміни рангу"}. {"has been kicked because the room has been changed to members-only","вигнано з кімнати тому, що вона стала тільки для учасників"}. {"has been kicked","вигнали з кімнати"}. -{" has set the subject to: "," встановив(ла) тему: "}. -{"Host","Хост"}. -{"If you don't see the CAPTCHA image here, visit the web page.","Якщо ви не бачите зображення капчі, перейдіть за за цією адресою."}. -{"If you want to specify different ports, passwords, encodings for IRC servers, fill this list with values in format '{\"irc server\", \"encoding\", port, \"password\"}'. By default this service use \"~s\" encoding, port ~p, empty password.","Щоб вказати різні порти, паролі та кодування для різних серверів IRC, заповніть список значеннями в форматі '{\"irc server\", \"encoding\", port, \"password\"}'. За замовчуванням ця служба використовує \"~s\" кодування, порт ~p, пустий пароль."}. +{"Hash of the vCard-temp avatar of this room","Хеш тимчасового аватара vCard цієї кімнати"}. +{"Hat title","Назва кімнати"}. +{"Hat URI","Назва URI"}. +{"Hats limit exceeded","Перевищено швидкість передачі інформації"}. +{"Host unknown","Невідоме ім'я сервера"}. +{"HTTP File Upload","Відвантаження файлів по HTTP"}. +{"Idle connection","Неактивне підключення"}. +{"If you don't see the CAPTCHA image here, visit the web page.","Якщо ви не бачите зображення CAPTCHA, перейдіть за адресою."}. {"Import Directory","Імпорт з директорії"}. {"Import File","Імпорт з файла"}. {"Import user data from jabberd14 spool file:","Імпорт користувачів з файла спула jabberd14:"}. @@ -132,278 +186,445 @@ {"Import users data from a PIEFXIS file (XEP-0227):","Імпорт даних користовучів з файлу PIEFXIS (XEP-0227):"}. {"Import users data from jabberd14 spool directory:","Імпорт користувачів з діректорії спула jabberd14:"}. {"Import Users from Dir at ","Імпортування користувача з директорії на "}. -{"Import Users From jabberd14 Spool Files","Імпорт користувачів зі спулу jabberd14"}. +{"Import Users From jabberd14 Spool Files","Імпорт користувачів з jabberd14 файлів \"Spool\""}. +{"Improper domain part of 'from' attribute","Неправильна доменна частина атрибута \"from\""}. {"Improper message type","Неправильний тип повідомлення"}. +{"Incorrect CAPTCHA submit","Неправильний ввід CAPTCHA"}. +{"Incorrect data form","Неправильна форма даних"}. {"Incorrect password","Неправильний пароль"}. -{"Invalid affiliation: ~s","Недопустимий ранг: ~s"}. -{"Invalid role: ~s","Недопустима роль: ~s"}. +{"Incorrect value of 'action' attribute","Неправильне значення атрибута \"action\""}. +{"Incorrect value of 'action' in data form","Неправильне значення \"action\" у формі даних"}. +{"Incorrect value of 'path' in data form","Неправильне значення \"path\" у формі даних"}. +{"Installed Modules:","Запуск модулів:"}. +{"Install","Встановлення"}. +{"Insufficient privilege","Недостатньо привілеїв"}. +{"Internal server error","Внутрішня помилка сервера"}. +{"Invalid 'from' attribute in forwarded message","Неприйнятний атрибут \"from\" у пересланому повідомленні"}. +{"Invalid node name","Неприйнятне ім'я вузла"}. +{"Invalid 'previd' value","Неприйнятне значення \"previd\""}. +{"Invitations are not allowed in this conference","Запрошення на цю конференцію не допускаються"}. {"IP addresses","IP адреси"}. -{"IP","IP"}. -{"IRC channel (don't put the first #)","Канал IRC (не включаючи #)"}. -{"IRC server","IRC-сервер"}. -{"IRC settings","Парметри IRC"}. -{"IRC Transport","IRC Транспорт"}. -{"IRC username","Ім'я користувача IRC"}. -{"IRC Username","Ім'я користувача IRC"}. {"is now known as","змінив(ла) псевдонім на"}. +{"It is not allowed to send error messages to the room. The participant (~s) has sent an error message (~s) and got kicked from the room","Не дозволяється відправляти помилкові повідомлення в кімнату. Учасник (~s) відправив помилкове повідомлення (~s), та був виганий з кімнати"}. {"It is not allowed to send private messages of type \"groupchat\"","Не дозволяється надсилати приватні повідомлення типу \"groupchat\""}. -{"It is not allowed to send private messages to the conference","Не дозволяється надсилати приватні повідомлення в конференцію"}. -{"It is not allowed to send private messages","Приватні повідомлення не дозволені"}. -{"Jabber Account Registration","Реєстрація Jabber-акаунту"}. +{"It is not allowed to send private messages to the conference","Не дозволяється надсилати приватні повідомлення в конференцію"}. {"Jabber ID","Jabber ID"}. -{"Jabber ID ~s is invalid","Jabber ID ~s недопустимий"}. -{"January","січня"}. -{"Join IRC channel","Приєднатися до каналу IRC"}. +{"January","Січня"}. +{"JID normalization denied by service policy","Створювати конференцію заборонено політикою служби"}. +{"JID normalization failed","Помилка нормалізації JID"}. +{"Joined MIX channels of ~ts","Приєднався до каналів MIX ~ts"}. +{"Joined MIX channels:","Приєднався до каналів MIX:"}. {"joins the room","увійшов(ла) в кімнату"}. -{"Join the IRC channel here.","Приєднатися до каналу IRC"}. -{"Join the IRC channel in this Jabber ID: ~s","Приєднатися до каналу IRC з Jabber ID: ~s"}. -{"July","липня"}. -{"June","червня"}. +{"July","Липня"}. +{"June","Червня"}. +{"Just created","Щойно створено"}. {"Last Activity","Останнє підключення"}. {"Last login","Останнє підключення"}. +{"Last message","Останнє повідомлення"}. {"Last month","За останній місяць"}. {"Last year","За останній рік"}. +{"Least significant bits of SHA-256 hash of text should equal hexadecimal label","Найменш значущі біти хеш-функції SHA-256 від тексту мають відповідати шістнадцятковій мітці."}. {"leaves the room","вийшов(ла) з кімнати"}. -{"Listened Ports at ","Відкриті порти на "}. -{"Listened Ports","Відкриті порти"}. -{"List of modules to start","Список завантажуваних модулів"}. -{"Low level update script","Низькорівневий сценарій поновлення"}. +{"List of users with hats","Список користувачів із капелюхами"}. +{"List users with hats","Список користувачів із капелюхами"}. +{"Logged Out","Вийшов із системи"}. +{"Logging","Журналювання"}. {"Make participants list public","Зробити список учасників видимим всім"}. {"Make room CAPTCHA protected","Зробити кімнату захищеною капчею"}. -{"Make room members-only","Кімната тільки для зареєтрованых учасників"}. +{"Make room members-only","Кімната тільки для зареєтрованих учасників"}. {"Make room moderated","Зробити кімнату модерованою"}. {"Make room password protected","Зробити кімнату захищеною паролем"}. {"Make room persistent","Зробити кімнату постійною"}. {"Make room public searchable","Зробити кімнату видимою всім"}. +{"Malformed username","Неправильне ім’я користувача"}. +{"MAM preference modification denied by service policy","Зміна налаштувань MAM відхилена через політики сервісу"}. {"March","березня"}. -{"Maximum Number of Occupants","Максимальна кількість учасників"}. -{"Max # of items to persist","Максимальне число збережених публікацій"}. +{"Max # of items to persist, or `max` for no specific limit other than a server imposed maximum","Максимальна кількість елементів для зберігання, або max для встановлення не конкретизованого ліміту, окрім максимального, накладеного сервером"}. {"Max payload size in bytes","Максимальний розмір корисного навантаження в байтах"}. +{"Maximum file size","Макс. розмір файлу"}. +{"Maximum Number of History Messages Returned by Room","Максимальна кількість повідомлень історії на кімнату"}. +{"Maximum number of items to persist","Максимальна кількість елементів для збереження"}. +{"Maximum Number of Occupants","Максимальна кількість учасників"}. {"May","травня"}. {"Membership is required to enter this room","В цю конференцію можуть входити тільки її члени"}. -{"Members:","Члени:"}. -{"Memorize your password, or write it in a paper placed in a safe place. In Jabber there isn't an automated way to recover your password if you forget it.","Запам'ятайте пароль, або запишіть його на папері, який треба зберегти у безпечному місці. У Jabber'і немає автоматизованих засобів відновлення пароля на той випадок, якщо ви його забудете."}. -{"Memory","Пам'ять"}. +{"Memorize your password, or write it in a paper placed in a safe place. In XMPP there isn't an automated way to recover your password if you forget it.","Запам'ятайте свій пароль або запишіть його на папері та зберігайте у безпечному місці. У XMPP не існує автоматизованого способу відновлення паролю, якщо ви його забудете."}. +{"Mere Availability in XMPP (No Show Value)","Проста доступність у XMPP (без показу статусу)"}. {"Message body","Тіло повідомлення"}. +{"Message not found in forwarded payload","Повідомлення не знайдено в пересланому вмісті"}. +{"Messages from strangers are rejected","Повідомлення від незнайомців відхиляються"}. +{"Messages of type headline","Повідомлення типу \"заголовок\""}. +{"Messages of type normal","Повідомлення типу \"звичайні\""}. {"Middle Name","По-батькові"}. +{"Minimum interval between voice requests (in seconds)","Мінімальний інтервал між голосовими запитами (в секундах)"}. {"Moderator privileges required","Необхідні права модератора"}. -{"moderators only","тільки модераторам"}. -{"Modified modules","Змінені модулі"}. -{"Modules at ","Модулі на "}. -{"Modules","Модулі"}. -{"Module","Модуль"}. +{"Moderators Only","Тільки модераторам"}. +{"Moderator","Модератор"}. +{"Module failed to handle the query","Модулю не вдалося обробити запит"}. {"Monday","Понеділок"}. -{"Name:","Назва:"}. +{"Multicast","Мультікаст"}. +{"Multiple elements are not allowed by RFC6121","Кілька елементів не дозволені RFC6121"}. +{"Multi-User Chat","Багато-користувальницький чат"}. {"Name","Назва"}. +{"Natural Language for Room Discussions","Розмовна мова для обговорень у кімнаті"}. +{"Natural-Language Room Name","Назва кімнати розмовною мовою"}. +{"Neither 'jid' nor 'nick' attribute found","Не знайдено ні атрибута \"jid\", ні \"nick\""}. +{"Neither 'role' nor 'affiliation' attribute found","Не знайдено ні атрибута \"role\", ні \"affiliation\""}. {"Never","Ніколи"}. {"New Password:","Новий Пароль:"}. +{"Nickname can't be empty","Псевдонім не може бути порожнім"}. {"Nickname Registration at ","Реєстрація псевдоніма на "}. {"Nickname ~s does not exist in the room","Псевдонім ~s в кімнаті відсутній"}. {"Nickname","Псевдонім"}. +{"No address elements found","Не знайдено елементів адреси"}. +{"No addresses element found","Не знайдено елемента адрес"}. +{"No 'affiliation' attribute found","Не знайдено атрибут \"affiliation\""}. +{"No available resource found","Не знайдено доступного ресурсу"}. {"No body provided for announce message","Тіло оголошення має бути непустим"}. +{"No child elements found","Не знайдено дочірніх елементів"}. +{"No data form found","Не знайдено форми даних"}. {"No Data","Немає даних"}. -{"Node ID","ID вузла"}. -{"Node not found","Вузол не знайдено"}. -{"Nodes","Вузли"}. -{"Node ","Вузол "}. +{"No features available","Немає доступних функцій"}. +{"No element found","Вузол не знайдено"}. +{"No hook has processed this command","Жоден хук не обробив цю команду"}. +{"No info about last activity found","Не знайдено інформації про останню діяльність"}. +{"No 'item' element found","Елемент \"item\" не знайдено"}. +{"No items found in this query","У цьому запиті не знайдено жодного елемента"}. {"No limit","Без обмежень"}. +{"No module is handling this query","Жоден модуль не може обробити цей запит"}. +{"No node specified","Вузол не вказано"}. +{"No 'password' found in data form","Не знайдено \"пароль\" у формі даних"}. +{"No 'password' found in this query","Не знайдено \"пароль\" у цьому запиті"}. +{"No 'path' found in data form","Не знайдено \"path\" у формі даних"}. +{"No pending subscriptions found","Не знайдено очікуваних рішення підписок"}. +{"No privacy list with this name found","Немає списку конфіденційності з такою назвою"}. +{"No private data found in this query","Приватних даних у цьому запиті не знайдено"}. +{"No running node found","Не знайдено запущеного вузла"}. +{"No services available","Немає доступних сервісів"}. +{"No statistics found for this item","Для цього елемента статистичні дані не знайдено"}. +{"No 'to' attribute found in the invitation","У запрошенні не знайдено атрибут \"до\""}. +{"Nobody","Ніхто"}. +{"Node already exists","Вузол уже існує"}. +{"Node ID","ID вузла"}. +{"Node index not found","Індекс вузла не знайдено"}. +{"Node not found","Вузол не знайдено"}. +{"Node ~p","Вузол ~p"}. +{"Nodeprep has failed","Не вдалося виконати Nodeprep"}. +{"Nodes","Вузли"}. +{"Node","Вузол"}. {"None","Немає"}. -{"No resource provided","Не вказаний ресурс"}. -{"Not Found","не знайдено"}. +{"Not allowed","Не дозволяється"}. +{"Not Found","Не знайдено"}. +{"Not subscribed","Не підписаний"}. {"Notify subscribers when items are removed from the node","Повідомляти абонентів про видалення публікацій із збірника"}. {"Notify subscribers when the node configuration changes","Повідомляти абонентів про зміни в конфігурації збірника"}. {"Notify subscribers when the node is deleted","Повідомляти абонентів про видалення збірника"}. -{"November","листопада"}. +{"November","Листопада"}. +{"Number of answers required","Кількість необхідних відповідей"}. {"Number of occupants","Кількість присутніх"}. +{"Number of Offline Messages","Кількість автономних повідомлень"}. {"Number of online users","Кількість підключених користувачів"}. {"Number of registered users","Кількість зареєстрованих користувачів"}. +{"Number of seconds after which to automatically purge items, or `max` for no specific limit other than a server imposed maximum","Кількість секунд, після яких автоматично видаляти елементи, або `max` для встановлення невизначеного ліміту, окрім максимального, накладеного сервером."}. +{"Occupants are allowed to invite others","Учасникам дозволено запрошувати інших"}. +{"Occupants are allowed to query others","Учасникам дозволено ставити запитання іншим"}. +{"Occupants May Change the Subject","Учасникам дозволено змінювати тему"}. {"October","грудня"}. -{"Offline Messages:","Офлайнові повідомлення:"}. -{"Offline Messages","Офлайнові повідомлення"}. {"OK","Продовжити"}. {"Old Password:","Старий пароль:"}. -{"Online Users:","Підключені користувачі:"}. {"Online Users","Підключені користувачі"}. {"Online","Підключений"}. +{"Only collection node owners may associate leaf nodes with the collection","Лише власники вузлів колекції можуть асоціювати листові вузли з колекцією"}. {"Only deliver notifications to available users","Доставляти повідомленнями тільки доступним користувачам"}. +{"Only or tags are allowed","Дозволені лише теги або "}. +{"Only element is allowed in this query","У цьому запиті дозволено лише елемент "}. +{"Only members may query archives of this room","Тільки модератори можуть запитувати архіви цієї кімнати"}. {"Only moderators and participants are allowed to change the subject in this room","Тільки модератори та учасники можуть змінювати тему в цій кімнаті"}. {"Only moderators are allowed to change the subject in this room","Тільки модератори можуть змінювати тему в цій кімнаті"}. +{"Only moderators are allowed to retract messages","Тільки модераторам дозволено відкликати повідомлення"}. +{"Only moderators can approve voice requests","Тільки модератори можуть схвалювати голосові запити"}. {"Only occupants are allowed to send messages to the conference","Тільки присутнім дозволяється надсилати повідомленняя в конференцію"}. {"Only occupants are allowed to send queries to the conference","Тільки присутнім дозволяється відправляти запити в конференцію"}. +{"Only publishers may publish","Тільки видавці можуть публікувати"}. {"Only service administrators are allowed to send service messages","Тільки адміністратор сервісу може надсилати службові повідомлення"}. -{"Options","Параметри"}. +{"Only those on a whitelist may associate leaf nodes with the collection","Лише ті, хто входить до білого списку, можуть асоціювати листові вузли з колекцією"}. +{"Only those on a whitelist may subscribe and retrieve items","Тільки ті, хто є в білому списку, можуть підписуватися та отримувати елементи"}. {"Organization Name","Назва організації"}. {"Organization Unit","Відділ організації"}. -{"Outgoing s2s Connections:","Вихідні s2s-з'єднання:"}. +{"Other Modules Available:","Інші доступні модулі:"}. {"Outgoing s2s Connections","Вихідні s2s-з'єднання"}. -{"Outgoing s2s Servers:","Вихідні s2s-сервери:"}. {"Owner privileges required","Необхідні права власника"}. -{"Packet","Пакет"}. -{"Password ~b","Пароль ~b"}. -{"Password Verification:","Перевірка Пароля:"}. +{"Packet relay is denied by service policy","Пересилання пакетів заборонене політикою сервісу"}. +{"Participant ID","ID учасника"}. +{"Participant","Учасник"}. {"Password Verification","Перевірка Пароля"}. -{"Password:","Пароль:"}. +{"Password Verification:","Перевірка Пароля:"}. {"Password","Пароль"}. +{"Password:","Пароль:"}. {"Path to Dir","Шлях до директорії"}. {"Path to File","Шлях до файла"}. -{"Pending","Очікування"}. -{"Period: ","Період"}. -{"Persist items to storage","Зберегати публікації до сховища"}. -{"Ping","Пінг"}. -{"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Зауважте, що ця опція відповідає за резервне копіювання тільки вбудованної бази даних Mnesia. Якщо Ви також використовуєте інше сховище для даних (наприклад за допомогою модуля ODBC), то його резервне копіювання потрібно робити окремо."}. -{"Pong","Понг"}. -{"Port ~b","Порт ~b"}. -{"Port","Порт"}. -{"Present real Jabber IDs to","Зробити реальні Jabber ID учасників видимими"}. +{"Payload semantic type information","Інформація про тип вмісту даних"}. +{"Period: ","Період: "}. +{"Persist items to storage","Зберігати елементи в сховищі"}. +{"Persistent","Тривалий"}. +{"Ping query is incorrect","Запит ping некоректний"}. +{"Ping","Затримка (пінг)"}. +{"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Зверніть увагу, що ці налаштування створюють резервну копію лише вбудованої бази даних Mnesia. Якщо ви використовуєте модуль ODBC, вам також потрібно окремо створити резервну копію вашої SQL-бази даних."}. +{"Please, wait for a while before sending new voice request","Будь ласка, зачекайте трохи перед тим, як відправляти новий голосовий запит"}. +{"Pong","Pong (відповідь на Ping, підтвердження взаємодії)"}. +{"Possessing 'ask' attribute is not allowed by RFC6121","Наявність атрибуту 'ask' не дозволена згідно з RFC6121"}. +{"Present real Jabber IDs to","Показувати реальні Jabber ID для"}. +{"Previous session not found","Попередню сесію не знайдено"}. +{"Previous session PID has been killed","PID попередньої сесії був зупинений"}. +{"Previous session PID has exited","PID попередньої сесії завершив роботу"}. +{"Previous session PID is dead","PID попередньої сесії більше не активний"}. +{"Previous session timed out","Час попередньої сесії вичерпано"}. {"private, ","приватна, "}. -{"Protocol","Протокол"}. +{"Public","Публічний"}. +{"Publish model","Модель публікації"}. {"Publish-Subscribe","Публікація-Підписка"}. -{"PubSub subscriber request","Запит на підписку PubSub"}. -{"Purge all items when the relevant publisher goes offline","Видалити всі елементи, коли особа, що їх опублікувала, вимикається від мережі"}. -{"Queries to the conference members are not allowed in this room","Запити до користувачів в цій конференції заборонені"}. -{"RAM and disc copy","ОЗП та диск"}. -{"RAM copy","ОЗП"}. -{"Raw","необроблений формат"}. -{"Really delete message of the day?","Насправді видалити повідомлення дня?"}. +{"PubSub subscriber request","Запит підписника PubSub"}. +{"Purge all items when the relevant publisher goes offline","Видалити всі елементи, коли автор публікації виходить офлайн"}. +{"Push record not found","Push-запис не знайдено"}. +{"Queries to the conference members are not allowed in this room","Запити до учасників конференції не дозволені в цій кімнаті"}. +{"Query to another users is forbidden","Запит до інших користувачів заборонено"}. +{"RAM and disc copy","Копія оперативної пам'яті та диску"}. +{"RAM copy","Копія оперативної пам'яті"}. +{"Really delete message of the day?","Дійсно видалити повідомлення дня?"}. +{"Receive notification from all descendent nodes","Отримувати сповіщення від усіх підпорядкованих вузлів"}. +{"Receive notification from direct child nodes only","Отримувати сповіщення лише від прямих дочірніх вузлів"}. +{"Receive notification of new items only","Отримувати сповіщення лише про нові товари"}. +{"Receive notification of new nodes only","Отримувати сповіщення лише про нові вузли"}. {"Recipient is not in the conference room","Адресата немає в конференції"}. -{"Register a Jabber account","Зареєструвати Jabber-акаунт"}. -{"Registered Users:","Зареєстровані користувачі:"}. -{"Registered Users","Зареєстровані користувачі"}. +{"Register an XMPP account","Зареєструвати XMPP-запис"}. {"Register","Реєстрація"}. -{"Registration in mod_irc for ","Реєстрація в mod_irc для "}. -{"Remote copy","не зберігаеться локально"}. -{"Remove All Offline Messages","Видалити всі офлайнові повідомлення"}. +{"Remote copy","Віддалене копіювання"}. +{"Remove a hat from a user","Зняти шапку з користувача"}. {"Remove User","Видалити користувача"}. -{"Remove","Видалити"}. {"Replaced by new connection","Замінено новим з'єднанням"}. +{"Request has timed out","Час очікування запиту минув"}. +{"Request is ignored","Запит ігнорується"}. +{"Requested role","Запитана роль"}. {"Resources","Ресурси"}. {"Restart Service","Перезапустити Сервіс"}. -{"Restart","Перезапустити"}. {"Restore Backup from File at ","Відновлення з резервної копії на "}. {"Restore binary backup after next ejabberd restart (requires less memory):","Відновити з бінарної резервної копії при наступному запуску (потребує менше пам'яті):"}. {"Restore binary backup immediately:","Відновити з бінарної резервної копії негайно:"}. {"Restore plain text backup immediately:","Відновити з текстової резервної копії негайно:"}. {"Restore","Відновлення з резервної копії"}. +{"Result","Результат"}. +{"Roles and Affiliations that May Retrieve Member List","Ролі та зв’язки, які можуть отримати список учасників"}. +{"Roles for which Presence is Broadcasted","Ролі для яких поширюється наявність"}. +{"Roles that May Send Private Messages","Ролі, що можуть надсилати приватні повідомлення"}. {"Room Configuration","Конфігурація кімнати"}. {"Room creation is denied by service policy","Створювати конференцію заборонено політикою служби"}. {"Room description","Опис кімнати"}. {"Room Occupants","Учасники кімнати"}. +{"Room terminates","Кімната припиняється"}. {"Room title","Назва кімнати"}. {"Roster groups allowed to subscribe","Дозволені для підписки групи ростера"}. -{"Roster of ","Ростер користувача "}. {"Roster size","Кількість контактів"}. -{"Roster","Ростер"}. -{"RPC Call Error","Помилка виклику RPC"}. {"Running Nodes","Працюючі вузли"}. -{"~s access rule configuration","Конфігурація правила доступу ~s"}. +{"~s invites you to the room ~s","~s запрошує вас до кімнати ~s"}. {"Saturday","Субота"}. -{"Script check","Перевірка сценарію"}. +{"Search from the date","Пошук від дати"}. {"Search Results for ","Результати пошуку в "}. +{"Search the text","Знайдіть текст"}. +{"Search until the date","Пошук до дати"}. {"Search users in ","Пошук користувачів в "}. {"Send announcement to all online users on all hosts","Надіслати сповіщення всім підключеним користувачам на всіх віртуальних серверах"}. {"Send announcement to all online users","Надіслати сповіщення всім підключеним користувачам"}. {"Send announcement to all users on all hosts","Надіслати сповіщення до усіх користувачів на усіх хостах"}. {"Send announcement to all users","Надіслати сповіщення всім користувачам"}. -{"September","вересня"}. -{"Server ~b","Сервер ~b"}. +{"September","Вересня"}. {"Server:","Сервер:"}. +{"Service list retrieval timed out","Час очікування отримання списку послуг минув"}. +{"Session state copying timed out","Час очікування копіювання стану сеансу минув"}. {"Set message of the day and send to online users","Встановити повідомлення дня та надіслати його підключеним користувачам"}. {"Set message of the day on all hosts and send to online users","Встановити повідомлення дня на всіх хостах та надійслати його підключеним користувачам"}. {"Shared Roster Groups","Спільні групи контактів"}. {"Show Integral Table","Показати інтегральну таблицю"}. +{"Show Occupants Join/Leave","Показати мешканцям приєднатися/вийти"}. {"Show Ordinary Table","Показати звичайну таблицю"}. {"Shut Down Service","Вимкнути Сервіс"}. -{"~s invites you to the room ~s","~s запрошує вас до кімнати ~s"}. -{"Some Jabber clients can store your password in your computer. Use that feature only if you trust your computer is safe.","Деякі Jabber-клієнти можуть зберігати пароль на вашому комп'ютері. Користуйтесь цією функцією тільки у тому випадку, якщо вважаєте її безпечною."}. +{"SOCKS5 Bytestreams","Потоки байтів SOCKS5"}. +{"Some XMPP clients can store your password in the computer, but you should do this only in your personal computer for safety reasons.","Деякі клієнти XMPP можуть зберігати ваш пароль на комп’ютері, але ви повинні робити це лише на своєму персональному комп’ютері з міркувань безпеки."}. +{"Sources Specs:","Специфікації джерел:"}. {"Specify the access model","Визначити модель доступу"}. {"Specify the event message type","Вкажіть тип повідомлень зі сповіщеннями про події"}. {"Specify the publisher model","Умови публікації"}. -{"~s's Offline Messages Queue","Черга офлайнових повідомлень ~s"}. -{"Start Modules at ","Запуск модулів на "}. -{"Start Modules","Запуск модулів"}. -{"Start","Запустити"}. -{"Statistics of ~p","Статистика вузла ~p"}. -{"Statistics","Статистика"}. -{"Stop Modules at ","Зупинка модулів на "}. -{"Stop Modules","Зупинка модулів"}. +{"Stanza id is not valid","Ідентифікатор строфи недійсний"}. +{"Stanza ID","ID кімнати"}. +{"Statically specify a replyto of the node owner(s)","Статично вкажіть відповідь власника(ів) вузла"}. {"Stopped Nodes","Зупинені вузли"}. -{"Stop","Зупинити"}. -{"Storage Type","Тип таблиці"}. {"Store binary backup:","Зберегти бінарну резервну копію:"}. {"Store plain text backup:","Зберегти текстову резервну копію:"}. +{"Stream management is already enabled","Керування потоком уже ввімкнено"}. +{"Stream management is not enabled","Керування потоком не ввімкнено"}. {"Subject","Тема"}. {"Submitted","Відправлено"}. -{"Submit","Відправити"}. {"Subscriber Address","Адреса абонента"}. -{"Subscription","Підписка"}. +{"Subscribers may publish","Підписники можуть публікувати"}. +{"Subscription requests must be approved and only subscribers may retrieve items","Запити на підписку мають бути схвалені, і лише передплатники можуть отримувати елементи"}. +{"Subscriptions are not allowed","Підписка заборонена"}. {"Sunday","Неділя"}. +{"Text associated with a picture","Текст, пов'язаний із зображенням"}. +{"Text associated with a sound","Текст, пов'язаний зі звуком"}. +{"Text associated with a video","Текст, пов’язаний із відео"}. +{"Text associated with speech","Текст, пов'язаний з мовленням"}. {"That nickname is already in use by another occupant","Псевдонім зайнято кимось з присутніх"}. {"That nickname is registered by another person","Псевдонім зареєстровано кимось іншим"}. -{"The CAPTCHA is valid.","Перевірку капчею закінчено успішно"}. +{"The account already exists","Обліковий запис уже існує"}. +{"The account was not unregistered","Обліковий запис не було видалено"}. +{"The body text of the last received message","Основний текст останнього отриманого повідомлення"}. +{"The CAPTCHA is valid.","Перевірку CAPTCHA успішно завершено."}. {"The CAPTCHA verification has failed","Перевірку капчею не пройдено"}. +{"The captcha you entered is wrong","Captcha, яку ви ввели, неправильна"}. +{"The child nodes (leaf or collection) associated with a collection","Дочірні вузли (лист або колекція), пов’язані з колекцією"}. {"The collections with which a node is affiliated","Колекція, до якої входить вузол"}. +{"The DateTime at which a leased subscription will end or has ended","DateTime, коли орендована підписка закінчиться або закінчилася"}. +{"The datetime when the node was created","Дата і час створення вузла"}. +{"The default language of the node","Мова вузла за замовчуванням"}. +{"The feature requested is not supported by the conference","Запитана функція не підтримується конференцією"}. +{"The JID of the node creator","JID творця вузла"}. +{"The JIDs of those to contact with questions","JID тих, до кого можна звернутися із запитаннями"}. +{"The JIDs of those with an affiliation of owner","JID тих, хто є афілійованим власником"}. +{"The JIDs of those with an affiliation of publisher","JID тих, хто пов’язаний із видавцем"}. +{"The list of all online users","Список усіх онлайн-користувачів"}. +{"The list of all users","Список усіх користувачів"}. +{"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, or `max` for no specific limit other than a server imposed maximum","Максимальна кількість дочірніх вузлів, які можна пов’язати з колекцією, або `max` для відсутності конкретного обмеження, окрім максимального накладеного сервером"}. +{"The minimum number of milliseconds between sending any two notification digests","Мінімальна кількість мілісекунд між надсиланням будь-яких двох дайджестів сповіщень"}. +{"The name of the node","Ім'я вузла"}. +{"The node is a collection node","Вузол є вузлом колекції"}. +{"The node is a leaf node (default)","Вузол є листовим вузлом (за замовчуванням)"}. +{"The NodeID of the relevant node","NodeID відповідного вузла"}. +{"The number of pending incoming presence subscription requests","Кількість вхідних запитів на підписку про присутність, що очікують на розгляд"}. +{"The number of subscribers to the node","Кількість передплатників вузла"}. +{"The number of unread or undelivered messages","Кількість непрочитаних або недоставлених повідомлень"}. +{"The password contains unacceptable characters","Пароль містить неприйнятні символи"}. {"The password is too weak","Пароль надто простий"}. -{"the password is","пароль:"}. -{"The password of your Jabber account was successfully changed.","Пароль вашого Jabber-акаунту був успішно змінений."}. +{"the password is","паролем є"}. +{"The password of your XMPP account was successfully changed.","Пароль вашого облікового запису XMPP успішно змінено."}. +{"The password was not changed","Пароль не змінено"}. +{"The passwords are different","Паролі різні"}. +{"The presence states for which an entity wants to receive notifications","Стан присутності, для якого сутність хоче отримувати сповіщення"}. +{"The query is only allowed from local users","Запит дозволено лише від локальних користувачів"}. +{"The query must not contain elements","Запит не повинен містити елементів "}. +{"The room subject can be modified by participants","Тема кімнати може бути змінена учасниками"}. +{"The semantic type information of data in the node, usually specified by the namespace of the payload (if any)","Інформація про семантичний тип даних у вузлі, зазвичай визначена простором імен корисного навантаження (якщо є)"}. +{"The sender of the last received message","Відправник останнього отриманого повідомлення"}. +{"The stanza MUST contain only one element, one element, or one element","Строфа ПОВИННА містити лише один елемент , один елемент або один елемент "}. +{"The subscription identifier associated with the subscription request","Ідентифікатор підписки, пов’язаний із запитом на підписку"}. +{"The URL of an XSL transformation which can be applied to payloads in order to generate an appropriate message body element.","URL-адреса перетворення XSL, яке можна застосувати до корисних даних, щоб створити відповідний елемент тіла повідомлення."}. +{"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","URL-адреса трансформації XSL, яку можна застосувати до формату корисного навантаження, щоб створити дійсний результат форм даних, який клієнт міг би відобразити за допомогою загального механізму візуалізації форм даних"}. {"There was an error changing the password: ","Помилка при зміні пароля: "}. -{"There was an error creating the account: ","Помилка при створенні акаунту:"}. +{"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.","Регістр не має значення: \"МАША\" та \"маша\" буде сприйматися як одне й те саме ім'я."}. -{"This page allows to create a Jabber account in this Jabber server. Your JID (Jabber IDentifier) will be of the form: username@server. Please read carefully the instructions to fill correctly the fields.","Тут ви можете зареєструвати обліковий запис Jabber на цьому сервері. Ваш JID (ідентифікатор Jabber) матиме вигляд \"користувач@сервер\". Щоб вірно заповнити поля нижче, будь ласка, уважно читайте інструкції до них."}. -{"This page allows to unregister a Jabber account in this Jabber server.","Ця сторінка дозволяє видалити свій акаунт з Jabber-сервера."}. -{"This participant is kicked from the room because he sent an error message to another participant","Цього учасника було відключено від кімнати через те, що він надіслав помилкове повідомлення іншому учаснику"}. -{"This participant is kicked from the room because he sent an error message","Цього учасника було відключено від кімнати через те, що він надіслав помилкове повідомлення"}. -{"This participant is kicked from the room because he sent an error presence","Цього учасника було відключено від кімнати через те, що він надіслав помилковий статус присутності"}. +{"This is case insensitive: macbeth is the same that MacBeth and 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 на цьому сервері XMPP. Ваш JID (Jabber ID) матиме такий вигляд: ім’я користувача@сервер. Уважно прочитайте інструкції, щоб правильно заповнити поля."}. +{"This page allows to unregister an XMPP account in this XMPP server.","Ця сторінка дозволяє видалити свій обліковий запис з XMPP-сервера."}. {"This room is not anonymous","Ця кімната не анонімна"}. +{"This service can not process the address: ~s","Ця служба не може обробити адресу: ~s"}. {"Thursday","Четвер"}. {"Time delay","Час затримки"}. -{"Time","Час"}. +{"Timed out waiting for stream resumption","Час очікування на відновлення потоку закінчився"}. +{"To register, visit ~s","Щоб зареєструватися, відвідайте ~s"}. +{"To ~ts","До ~ts"}. +{"Token TTL","Токен TTL"}. +{"Too many active bytestreams","Надто багато активних потоків байтів"}. {"Too many CAPTCHA requests","Надто багато CAPTCHA-запитів"}. -{"To ~s","До ~s"}. -{"To","Кому"}. +{"Too many child elements","Надто багато дочірніх елементів"}. +{"Too many elements","Надто багато елементів "}. +{"Too many elements","Надто багато елементів "}. +{"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","Забагато (~p) помилок авторизації з цієї IP адреси (~s). Адресу буде розблоковано о ~s UTC"}. +{"Too many receiver fields were specified","Вказано забагато одержувачів"}. +{"Too many unacked stanzas","Занадто багато пакетів без відповідей"}. +{"Too many users in this conference","Надто багато користувачів у цій конференції"}. {"Traffic rate limit is exceeded","Швидкість передачі інформації було перевищено"}. -{"Transactions Aborted:","Транзакції відмінені:"}. -{"Transactions Committed:","Транзакції завершені:"}. -{"Transactions Logged:","Транзакції запротокольовані:"}. -{"Transactions Restarted:","Транзакції перезапущені:"}. +{"~ts's MAM Archive","Архів МАМ ~ts"}. +{"~ts's Offline Messages Queue","Черга автономних повідомлень ~ts"}. {"Tuesday","Вівторок"}. {"Unable to generate a CAPTCHA","Нема можливості згенерувати капчу"}. +{"Unable to register route on existing local domain","Неможливо зареєструвати маршрут на наявному локальному домені"}. {"Unauthorized","Не авторизовано"}. -{"Unregister a Jabber account","Видалити Jabber-акаунт"}. -{"Unregister","Видалити"}. +{"Unexpected action","Несподівана дія"}. +{"Unexpected error condition: ~p","Умова несподіваної помилки: ~p"}. +{"Uninstall","Видалити"}. +{"Unregister an XMPP account","Видалити обліковий запис XMPP"}. +{"Unregister","Скасувати реєстрацію"}. +{"Unsupported element","Непідтримуваний елемент "}. +{"Unsupported version","Непідтримувана версія"}. {"Update message of the day (don't send)","Оновити повідомлення дня (не надсилати)"}. {"Update message of the day on all hosts (don't send)","Оновити повідомлення дня на всіх хостах (не надсилати)"}. -{"Update plan","План поновлення"}. -{"Update script","Сценарій поновлення"}. -{"Update","Обновити"}. -{"Update ","Поновлення "}. -{"Uptime:","Час роботи:"}. -{"Use of STARTTLS required","Ви мусите використовувати STARTTLS"}. +{"Update specs to get modules source, then install desired ones.","Оновіть специфікації, щоб отримати джерело модулів, а потім встановіть потрібні."}. +{"Update Specs","Оновити характеристики"}. +{"Updating the vCard is not supported by the vCard storage backend","Оновлення vCard не підтримується системою зберігання vCard"}. +{"Upgrade","Оновлення"}. +{"URL for Archived Discussion Logs","URL-адреса для журналів архівних обговорень"}. +{"User already exists","Користувач уже існує"}. +{"User JID","JID Користувача"}. +{"User (jid)","Користувач (jid)"}. {"User Management","Управління Користувачами"}. +{"User not allowed to perform an IQ set on another user's vCard.","Користувачеві заборонено виконувати тест IQ на vCard іншого користувача."}. +{"User removed","Користувача видалено"}. +{"User session not found","Сеанс користувача не знайдено"}. +{"User session terminated","Сеанс користувача припинено"}. +{"User ~ts","Користувач ~ts"}. {"Username:","Ім'я користувача:"}. {"Users are not allowed to register accounts so quickly","Користувачам не дозволено так часто реєструвати облікові записи"}. {"Users Last Activity","Статистика останнього підключення користувачів"}. {"Users","Користувачі"}. -{"User ","Користувач "}. {"User","Користувач"}. -{"Validate","Затвердити"}. +{"Value 'get' of 'type' attribute is not allowed","Значення 'get' атрибута 'type' не дозволене"}. +{"Value of '~s' should be boolean","Значення \"~s\" має бути логічним"}. +{"Value of '~s' should be datetime string","Значення \"~s\" має бути рядком дати і часу"}. +{"Value of '~s' should be integer","Значення \"~s\" має бути цілим числом"}. +{"Value 'set' of 'type' attribute is not allowed","Значення 'set' атрибута 'type' не допускається"}. {"vCard User Search","Пошук користувачів по vCard"}. -{"Virtual Hosts","віртуальні хости"}. +{"View joined MIX channels","Перегляд приєднаних каналів MIX"}. +{"Virtual Hosts","Віртуальні хости"}. {"Visitors are not allowed to change their nicknames in this room","Відвідувачам не дозволяється змінювати псевдонім в цій кімнаті"}. {"Visitors are not allowed to send messages to all occupants","Відвідувачам не дозволяється надсилати повідомлення всім присутнім"}. +{"Visitor","Відвідувач"}. +{"Voice requests are disabled in this conference","Голосові запити відключені в цій конференції"}. +{"Voice request","Голосовий запит"}. +{"Web client which allows to join the room anonymously","Веб-клієнт, який дозволяє анонімно приєднатися до кімнати"}. {"Wednesday","Середа"}. +{"When a new subscription is processed and whenever a subscriber comes online","Коли обробляється нова підписка та щоразу, коли абонент виходить в Інтернет"}. +{"When a new subscription is processed","Під час обробки нової підписки"}. {"When to send the last published item","Коли надсилати останній опублікований елемент"}. +{"Whether an entity wants to receive an XMPP message body in addition to the payload format","Чи бажає суб’єкт отримувати тіло повідомлення XMPP на додаток до формату корисного навантаження"}. +{"Whether an entity wants to receive digests (aggregations) of notifications or all notifications individually","Чи бажає організація отримувати дайджести (агрегації) сповіщень чи всі сповіщення окремо"}. +{"Whether an entity wants to receive or disable notifications","Чи бажає суб’єкт отримувати або вимикати сповіщення"}. +{"Whether owners or publisher should receive replies to items","Чи повинні власники або видавець отримувати відповіді на елементи"}. +{"Whether the node is a leaf (default) or a collection","Чи є вузол листом (типово) чи колекцією"}. {"Whether to allow subscriptions","Дозволяти підписку"}. -{"You can later change your password using a Jabber client.","Пізніше можна змінити пароль через Jabber-клієнт."}. +{"Whether to make all subscriptions temporary, based on subscriber presence","Чи робити всі підписки тимчасовими, залежно від присутності читача"}. +{"Whether to notify owners about new subscribers and unsubscribes","Чи повідомляти власників про нових читачів та їх втрату"}. +{"Who can send private messages","Хто може надсилати приватні повідомлення"}. +{"Who may associate leaf nodes with a collection","Хто може пов’язувати листові вузли з колекцією"}. +{"Wrong parameters in the web formulary","Неправильні параметри у веб-формі"}. +{"Wrong xmlns","Неправильний xmlns"}. +{"XMPP Account Registration","Реєстрація облікового запису XMPP"}. +{"XMPP Domains","Домени XMPP"}. +{"XMPP Show Value of Away","XMPP Показати значення Away"}. +{"XMPP Show Value of Chat","XMPP Показати значення чату"}. +{"XMPP Show Value of DND (Do Not Disturb)","XMPP Показати значення DND (Не турбувати)"}. +{"XMPP Show Value of XA (Extended Away)","XMPP Показати значення XA (розширено)"}. +{"XMPP URI of Associated Publish-Subscribe Node","XMPP URI-адреса асоційованого вузла публікацій-підписок"}. +{"You are being removed from the room because of a system shutdown","Ви будете видалені з кімнати через завершення роботи системи"}. +{"You are not allowed to send private messages","Ви не можете надсилати приватні повідомлення"}. +{"You are not joined to the channel","Ви не приєднані до каналу"}. +{"You can later change your password using an XMPP client.","Пізніше ви можете змінити пароль за допомогою XMPP-клієнта."}. {"You have been banned from this room","Вам заборонено входити в цю конференцію"}. +{"You have joined too many conferences","Ви приєднані до надто великої кількості конференцій"}. {"You must fill in field \"Nickname\" in the form","Вам необхідно заповнити поле \"Псевдонім\" у формі"}. {"You need a client that supports x:data and CAPTCHA to register","Для реєстрації псевдоніму необхідно використовувати клієнт з підтримкою x:data"}. {"You need a client that supports x:data to register the nickname","Для реєстрації псевдоніму необхідно використовувати клієнт з підтримкою x:data"}. -{"You need an x:data capable client to configure mod_irc settings","Для налагодження параметрів mod_irc необхідно використовувати клієнт, що має підтримку x:data"}. -{"You need an x:data capable client to configure room","Для конфігурування кімнати потрібно використовувати клієнт з підтримкою 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 Jabber account was successfully created.","Ваш Jabber-акаунт було успішно створено."}. -{"Your Jabber account was successfully deleted.","Ваш Jabber-акаунт було успішно видалено."}. -{"Your messages to ~s are being blocked. To unblock them, visit ~s","Ваші повідомлення до ~s блокуються. Для розблокування відвідайте ~s"}. +{"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 успішно видалено."}. +{"You're not allowed to create nodes","Вам заборонено створювати вузли"}. diff --git a/priv/msgs/uk.po b/priv/msgs/uk.po deleted file mode 100644 index 923861503..000000000 --- a/priv/msgs/uk.po +++ /dev/null @@ -1,1867 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: 2.1.0-alpha\n" -"POT-Creation-Date: \n" -"PO-Revision-Date: \n" -"Last-Translator: Konstantin Khomoutov \n" -"Language-Team: \n" -"Language: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Language: Ukrainian (українська)\n" -"X-Additional-Translator: Ruslan Rakhmanin\n" -"X-Additional-Translator: Stoune\n" -"X-Additional-Translator: Sergei Golovan\n" - -#: ejabberd_c2s.erl:424 ejabberd_c2s.erl:727 -msgid "Use of STARTTLS required" -msgstr "Ви мусите використовувати STARTTLS" - -#: ejabberd_c2s.erl:503 -msgid "No resource provided" -msgstr "Не вказаний ресурс" - -#: ejabberd_c2s.erl:1197 -msgid "Replaced by new connection" -msgstr "Замінено новим з'єднанням" - -#: ejabberd_c2s.erl:1885 -msgid "Your active privacy list has denied the routing of this stanza." -msgstr "Маршрутизація цієї строфи була відмінена активним списком приватності." - -#: ejabberd_captcha.erl:96 ejabberd_captcha.erl:152 ejabberd_captcha.erl:178 -msgid "Enter the text you see" -msgstr "Введіть текст, що ви бачите" - -#: ejabberd_captcha.erl:101 -msgid "Your messages to ~s are being blocked. To unblock them, visit ~s" -msgstr "Ваші повідомлення до ~s блокуються. Для розблокування відвідайте ~s" - -#: ejabberd_captcha.erl:134 -msgid "If you don't see the CAPTCHA image here, visit the web page." -msgstr "Якщо ви не бачите зображення капчі, перейдіть за за цією адресою." - -#: ejabberd_captcha.erl:146 -msgid "CAPTCHA web page" -msgstr "Адреса капчі" - -#: ejabberd_captcha.erl:307 -msgid "The CAPTCHA is valid." -msgstr "Перевірку капчею закінчено успішно" - -#: mod_adhoc.erl:95 mod_adhoc.erl:125 mod_adhoc.erl:143 mod_adhoc.erl:161 -msgid "Commands" -msgstr "Команди" - -#: mod_adhoc.erl:149 mod_adhoc.erl:243 -msgid "Ping" -msgstr "Пінг" - -#: mod_adhoc.erl:260 -msgid "Pong" -msgstr "Понг" - -#: mod_announce.erl:507 mod_announce_odbc.erl:499 -msgid "Really delete message of the day?" -msgstr "Насправді видалити повідомлення дня?" - -#: mod_announce.erl:515 mod_announce_odbc.erl:507 mod_configure.erl:1083 -#: mod_configure.erl:1128 -msgid "Subject" -msgstr "Тема" - -#: mod_announce.erl:520 mod_announce_odbc.erl:512 mod_configure.erl:1088 -#: mod_configure.erl:1133 -msgid "Message body" -msgstr "Тіло повідомлення" - -#: mod_announce.erl:600 mod_announce_odbc.erl:592 -msgid "No body provided for announce message" -msgstr "Тіло оголошення має бути непустим" - -#: mod_announce.erl:635 mod_announce_odbc.erl:627 -msgid "Announcements" -msgstr "Сповіщення" - -#: mod_announce.erl:637 mod_announce_odbc.erl:629 -msgid "Send announcement to all users" -msgstr "Надіслати сповіщення всім користувачам" - -#: mod_announce.erl:639 mod_announce_odbc.erl:631 -msgid "Send announcement to all users on all hosts" -msgstr "Надіслати сповіщення до усіх користувачів на усіх хостах" - -#: mod_announce.erl:641 mod_announce_odbc.erl:633 -msgid "Send announcement to all online users" -msgstr "Надіслати сповіщення всім підключеним користувачам" - -#: mod_announce.erl:643 mod_announce_odbc.erl:635 mod_configure.erl:1078 -#: mod_configure.erl:1123 -msgid "Send announcement to all online users on all hosts" -msgstr "" -"Надіслати сповіщення всім підключеним користувачам на всіх віртуальних " -"серверах" - -#: mod_announce.erl:645 mod_announce_odbc.erl:637 -msgid "Set message of the day and send to online users" -msgstr "Встановити повідомлення дня та надіслати його підключеним користувачам" - -#: mod_announce.erl:647 mod_announce_odbc.erl:639 -msgid "Set message of the day on all hosts and send to online users" -msgstr "" -"Встановити повідомлення дня на всіх хостах та надійслати його підключеним " -"користувачам" - -#: mod_announce.erl:649 mod_announce_odbc.erl:641 -msgid "Update message of the day (don't send)" -msgstr "Оновити повідомлення дня (не надсилати)" - -#: mod_announce.erl:651 mod_announce_odbc.erl:643 -msgid "Update message of the day on all hosts (don't send)" -msgstr "Оновити повідомлення дня на всіх хостах (не надсилати)" - -#: mod_announce.erl:653 mod_announce_odbc.erl:645 -msgid "Delete message of the day" -msgstr "Видалити повідомлення дня" - -#: mod_announce.erl:655 mod_announce_odbc.erl:647 -msgid "Delete message of the day on all hosts" -msgstr "Видалити повідомлення дня на усіх хостах" - -#: mod_configure.erl:114 mod_configure.erl:274 mod_configure.erl:296 -#: mod_configure.erl:498 -msgid "Configuration" -msgstr "Конфігурація" - -#: mod_configure.erl:125 mod_configure.erl:576 web/ejabberd_web_admin.erl:1936 -msgid "Database" -msgstr "База даних" - -#: mod_configure.erl:127 mod_configure.erl:595 -msgid "Start Modules" -msgstr "Запуск модулів" - -#: mod_configure.erl:129 mod_configure.erl:596 -msgid "Stop Modules" -msgstr "Зупинка модулів" - -#: mod_configure.erl:131 mod_configure.erl:604 web/ejabberd_web_admin.erl:1937 -msgid "Backup" -msgstr "Резервне копіювання" - -#: mod_configure.erl:133 mod_configure.erl:605 -msgid "Restore" -msgstr "Відновлення з резервної копії" - -#: mod_configure.erl:135 mod_configure.erl:606 -msgid "Dump to Text File" -msgstr "Копіювання в текстовий файл" - -#: mod_configure.erl:137 mod_configure.erl:615 -msgid "Import File" -msgstr "Імпорт з файла" - -#: mod_configure.erl:139 mod_configure.erl:616 -msgid "Import Directory" -msgstr "Імпорт з директорії" - -#: mod_configure.erl:141 mod_configure.erl:581 mod_configure.erl:1057 -msgid "Restart Service" -msgstr "Перезапустити Сервіс" - -#: mod_configure.erl:143 mod_configure.erl:582 mod_configure.erl:1102 -msgid "Shut Down Service" -msgstr "Вимкнути Сервіс" - -#: mod_configure.erl:145 mod_configure.erl:518 mod_configure.erl:1197 -#: web/ejabberd_web_admin.erl:1527 -msgid "Add User" -msgstr "Додати користувача" - -#: mod_configure.erl:147 mod_configure.erl:519 mod_configure.erl:1219 -msgid "Delete User" -msgstr "Видалити Користувача" - -#: mod_configure.erl:149 mod_configure.erl:520 mod_configure.erl:1231 -msgid "End User Session" -msgstr "Закінчити Сеанс Користувача" - -#: mod_configure.erl:151 mod_configure.erl:521 mod_configure.erl:1243 -#: mod_configure.erl:1255 -msgid "Get User Password" -msgstr "Отримати Пароль Користувача" - -#: mod_configure.erl:153 mod_configure.erl:522 -msgid "Change User Password" -msgstr "Змінити Пароль Користувача" - -#: mod_configure.erl:155 mod_configure.erl:523 mod_configure.erl:1272 -msgid "Get User Last Login Time" -msgstr "Отримати Час Останнього Підключення Користувача" - -#: mod_configure.erl:157 mod_configure.erl:524 mod_configure.erl:1284 -msgid "Get User Statistics" -msgstr "Отримати Статистику по Користувачу" - -#: mod_configure.erl:159 mod_configure.erl:525 -msgid "Get Number of Registered Users" -msgstr "Отримати Кількість Зареєстрованих Користувачів" - -#: mod_configure.erl:161 mod_configure.erl:526 -msgid "Get Number of Online Users" -msgstr "Отримати Кількість Підключених Користувачів" - -#: mod_configure.erl:163 mod_configure.erl:509 web/ejabberd_web_admin.erl:831 -#: web/ejabberd_web_admin.erl:872 -msgid "Access Control Lists" -msgstr "Списки керування доступом" - -#: mod_configure.erl:165 mod_configure.erl:510 web/ejabberd_web_admin.erl:940 -#: web/ejabberd_web_admin.erl:976 -msgid "Access Rules" -msgstr "Правила доступу" - -#: mod_configure.erl:297 mod_configure.erl:499 -msgid "User Management" -msgstr "Управління Користувачами" - -#: mod_configure.erl:500 web/ejabberd_web_admin.erl:1058 -#: web/ejabberd_web_admin.erl:1462 -msgid "Online Users" -msgstr "Підключені користувачі" - -#: mod_configure.erl:501 -msgid "All Users" -msgstr "Всі користувачі" - -#: mod_configure.erl:502 -msgid "Outgoing s2s Connections" -msgstr "Вихідні s2s-з'єднання" - -#: mod_configure.erl:503 web/ejabberd_web_admin.erl:1907 -msgid "Running Nodes" -msgstr "Працюючі вузли" - -#: mod_configure.erl:504 web/ejabberd_web_admin.erl:1909 -msgid "Stopped Nodes" -msgstr "Зупинені вузли" - -#: mod_configure.erl:577 mod_configure.erl:587 web/ejabberd_web_admin.erl:1953 -msgid "Modules" -msgstr "Модулі" - -#: mod_configure.erl:578 -msgid "Backup Management" -msgstr "Керування резервним копіюванням" - -#: mod_configure.erl:579 -msgid "Import Users From jabberd14 Spool Files" -msgstr "Імпорт користувачів зі спулу jabberd14" - -#: mod_configure.erl:699 -msgid "To ~s" -msgstr "До ~s" - -#: mod_configure.erl:717 -msgid "From ~s" -msgstr "Від ~s" - -#: mod_configure.erl:913 -msgid "Database Tables Configuration at " -msgstr "Конфігурація таблиць бази даних на " - -#: mod_configure.erl:918 -msgid "Choose storage type of tables" -msgstr "Оберіть тип збереження таблиць" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Disc only copy" -msgstr "Тільки диск" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM and disc copy" -msgstr "ОЗП та диск" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM copy" -msgstr "ОЗП" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Remote copy" -msgstr "не зберігаеться локально" - -#: mod_configure.erl:950 -msgid "Stop Modules at " -msgstr "Зупинка модулів на " - -#: mod_configure.erl:954 -msgid "Choose modules to stop" -msgstr "Виберіть модулі, які необхідно зупинити" - -#: mod_configure.erl:969 -msgid "Start Modules at " -msgstr "Запуск модулів на " - -#: mod_configure.erl:973 -msgid "Enter list of {Module, [Options]}" -msgstr "Введіть перелік такого виду {Module, [Options]}" - -#: mod_configure.erl:974 -msgid "List of modules to start" -msgstr "Список завантажуваних модулів" - -#: mod_configure.erl:983 -msgid "Backup to File at " -msgstr "Резервне копіювання в файл на " - -#: mod_configure.erl:987 mod_configure.erl:1001 -msgid "Enter path to backup file" -msgstr "Введіть шлях до резервного файла" - -#: mod_configure.erl:988 mod_configure.erl:1002 mod_configure.erl:1016 -#: mod_configure.erl:1030 -msgid "Path to File" -msgstr "Шлях до файла" - -#: mod_configure.erl:997 -msgid "Restore Backup from File at " -msgstr "Відновлення з резервної копії на " - -#: mod_configure.erl:1011 -msgid "Dump Backup to Text File at " -msgstr "Копіювання в текстовий файл на " - -#: mod_configure.erl:1015 -msgid "Enter path to text file" -msgstr "Введіть шлях до текстового файла" - -#: mod_configure.erl:1025 -msgid "Import User from File at " -msgstr "Імпортування користувача з файла на " - -#: mod_configure.erl:1029 -msgid "Enter path to jabberd14 spool file" -msgstr "Введіть шлях до файла зі спула jabberd14" - -#: mod_configure.erl:1039 -msgid "Import Users from Dir at " -msgstr "Імпортування користувача з директорії на " - -#: mod_configure.erl:1043 -msgid "Enter path to jabberd14 spool dir" -msgstr "Введіть шлях до директорії спула jabberd14" - -#: mod_configure.erl:1044 -msgid "Path to Dir" -msgstr "Шлях до директорії" - -#: mod_configure.erl:1060 mod_configure.erl:1105 -msgid "Time delay" -msgstr "Час затримки" - -#: mod_configure.erl:1143 -msgid "Access Control List Configuration" -msgstr "Конфігурація списків керування доступом" - -#: mod_configure.erl:1147 -msgid "Access control lists" -msgstr "Списки керування доступом" - -#: mod_configure.erl:1171 -msgid "Access Configuration" -msgstr "Конфігурація доступу" - -#: mod_configure.erl:1175 -msgid "Access rules" -msgstr "Правила доступу" - -#: mod_configure.erl:1200 mod_configure.erl:1222 mod_configure.erl:1234 -#: mod_configure.erl:1246 mod_configure.erl:1258 mod_configure.erl:1275 -#: mod_configure.erl:1287 mod_configure.erl:1650 mod_configure.erl:1700 -#: mod_configure.erl:1721 mod_roster.erl:943 mod_roster_odbc.erl:1060 -#: mod_vcard.erl:472 mod_vcard_ldap.erl:554 mod_vcard_odbc.erl:448 -msgid "Jabber ID" -msgstr "Jabber ID" - -#: mod_configure.erl:1205 mod_configure.erl:1263 mod_configure.erl:1651 -#: mod_configure.erl:1863 mod_muc/mod_muc_room.erl:3224 mod_register.erl:235 -#: web/ejabberd_web_admin.erl:1520 -msgid "Password" -msgstr "Пароль" - -#: mod_configure.erl:1210 -msgid "Password Verification" -msgstr "Перевірка Пароля" - -#: mod_configure.erl:1301 -msgid "Number of registered users" -msgstr "Кількість зареєстрованих користувачів" - -#: mod_configure.erl:1315 -msgid "Number of online users" -msgstr "Кількість підключених користувачів" - -#: mod_configure.erl:1682 web/ejabberd_web_admin.erl:1588 -#: web/ejabberd_web_admin.erl:1743 -msgid "Never" -msgstr "Ніколи" - -#: mod_configure.erl:1696 web/ejabberd_web_admin.erl:1601 -#: web/ejabberd_web_admin.erl:1756 -msgid "Online" -msgstr "Підключений" - -#: mod_configure.erl:1701 -msgid "Last login" -msgstr "Останнє підключення" - -#: mod_configure.erl:1722 -msgid "Roster size" -msgstr "Кількість контактів" - -#: mod_configure.erl:1723 -msgid "IP addresses" -msgstr "IP адреси" - -#: mod_configure.erl:1724 -msgid "Resources" -msgstr "Ресурси" - -#: mod_configure.erl:1850 -msgid "Administration of " -msgstr "Адміністрування " - -#: mod_configure.erl:1853 -msgid "Action on user" -msgstr "Дія над користувачем" - -#: mod_configure.erl:1857 -msgid "Edit Properties" -msgstr "Змінити параметри" - -#: mod_configure.erl:1860 web/ejabberd_web_admin.erl:1769 -msgid "Remove User" -msgstr "Видалити користувача" - -#: mod_irc/mod_irc.erl:201 mod_irc/mod_irc_odbc.erl:196 -#: mod_muc/mod_muc.erl:336 mod_muc/mod_muc_odbc.erl:342 -msgid "Access denied by service policy" -msgstr "Доступ заборонений політикою служби" - -#: mod_irc/mod_irc.erl:401 mod_irc/mod_irc_odbc.erl:398 -msgid "IRC Transport" -msgstr "IRC Транспорт" - -#: mod_irc/mod_irc.erl:428 mod_irc/mod_irc_odbc.erl:425 -msgid "ejabberd IRC module" -msgstr "ejabberd IRC модуль" - -#: mod_irc/mod_irc.erl:559 mod_irc/mod_irc_odbc.erl:568 -msgid "You need an x:data capable client to configure mod_irc settings" -msgstr "" -"Для налагодження параметрів mod_irc необхідно використовувати клієнт, що має " -"підтримку x:data" - -#: mod_irc/mod_irc.erl:566 mod_irc/mod_irc_odbc.erl:575 -msgid "Registration in mod_irc for " -msgstr "Реєстрація в mod_irc для " - -#: mod_irc/mod_irc.erl:571 mod_irc/mod_irc_odbc.erl:580 -msgid "" -"Enter username, encodings, ports and passwords you wish to use for " -"connecting to IRC servers" -msgstr "" -"Введіть ім'я користувача, кодування, порти та паролі, що будуть " -"використовуватися при підключенні до IRC-серверів" - -#: mod_irc/mod_irc.erl:576 mod_irc/mod_irc_odbc.erl:585 -msgid "IRC Username" -msgstr "Ім'я користувача IRC" - -#: mod_irc/mod_irc.erl:586 mod_irc/mod_irc_odbc.erl:595 -msgid "" -"If you want to specify different ports, passwords, encodings for IRC " -"servers, fill this list with values in format '{\"irc server\", \"encoding" -"\", port, \"password\"}'. By default this service use \"~s\" encoding, port " -"~p, empty password." -msgstr "" -"Щоб вказати різні порти, паролі та кодування для різних серверів IRC, " -"заповніть список значеннями в форматі '{\"irc server\", \"encoding\", port, " -"\"password\"}'. За замовчуванням ця служба використовує \"~s\" кодування, " -"порт ~p, пустий пароль." - -#: mod_irc/mod_irc.erl:598 mod_irc/mod_irc_odbc.erl:607 -msgid "" -"Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef." -"net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]." -msgstr "" -"Приклад: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta." -"fef.net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]." - -#: mod_irc/mod_irc.erl:603 mod_irc/mod_irc_odbc.erl:612 -msgid "Connections parameters" -msgstr "Параметри з'єднання" - -#: mod_irc/mod_irc.erl:728 mod_irc/mod_irc_odbc.erl:757 -msgid "Join IRC channel" -msgstr "Приєднатися до каналу IRC" - -#: mod_irc/mod_irc.erl:732 mod_irc/mod_irc_odbc.erl:761 -msgid "IRC channel (don't put the first #)" -msgstr "Канал IRC (не включаючи #)" - -#: mod_irc/mod_irc.erl:737 mod_irc/mod_irc_odbc.erl:766 -msgid "IRC server" -msgstr "IRC-сервер" - -#: mod_irc/mod_irc.erl:770 mod_irc/mod_irc.erl:774 -#: mod_irc/mod_irc_odbc.erl:799 mod_irc/mod_irc_odbc.erl:803 -msgid "Join the IRC channel here." -msgstr "Приєднатися до каналу IRC" - -#: mod_irc/mod_irc.erl:778 mod_irc/mod_irc_odbc.erl:807 -msgid "Join the IRC channel in this Jabber ID: ~s" -msgstr "Приєднатися до каналу IRC з Jabber ID: ~s" - -#: mod_irc/mod_irc.erl:863 mod_irc/mod_irc_odbc.erl:904 -msgid "IRC settings" -msgstr "Парметри IRC" - -#: mod_irc/mod_irc.erl:868 mod_irc/mod_irc_odbc.erl:909 -msgid "" -"Enter username and encodings you wish to use for connecting to IRC servers. " -"Press 'Next' to get more fields to fill in. Press 'Complete' to save " -"settings." -msgstr "" -"Введіть ім'я користувача та кодування, які будуть використовуватися при " -"підключенні до IRC-серверів Натисніть 'Далі' для заповнення додаткових " -"полів. Натисніть 'Завершити' для збереження параметрів." - -#: mod_irc/mod_irc.erl:874 mod_irc/mod_irc_odbc.erl:915 -msgid "IRC username" -msgstr "Ім'я користувача IRC" - -#: mod_irc/mod_irc.erl:923 mod_irc/mod_irc_odbc.erl:964 -msgid "Password ~b" -msgstr "Пароль ~b" - -#: mod_irc/mod_irc.erl:928 mod_irc/mod_irc_odbc.erl:969 -msgid "Port ~b" -msgstr "Порт ~b" - -#: mod_irc/mod_irc.erl:933 mod_irc/mod_irc_odbc.erl:974 -msgid "Encoding for server ~b" -msgstr "Кодування для сервера ~b" - -#: mod_irc/mod_irc.erl:942 mod_irc/mod_irc_odbc.erl:983 -msgid "Server ~b" -msgstr "Сервер ~b" - -#: mod_muc/mod_muc.erl:449 mod_muc/mod_muc_odbc.erl:456 -msgid "Only service administrators are allowed to send service messages" -msgstr "Тільки адміністратор сервісу може надсилати службові повідомлення" - -#: mod_muc/mod_muc.erl:493 mod_muc/mod_muc_odbc.erl:500 -msgid "Room creation is denied by service policy" -msgstr "Створювати конференцію заборонено політикою служби" - -#: mod_muc/mod_muc.erl:500 mod_muc/mod_muc_odbc.erl:507 -msgid "Conference room does not exist" -msgstr "Конференція не існує" - -#: mod_muc/mod_muc.erl:582 mod_muc/mod_muc_odbc.erl:595 -msgid "Chatrooms" -msgstr "Кімнати" - -#: mod_muc/mod_muc.erl:712 mod_muc/mod_muc_odbc.erl:729 -msgid "You need a client that supports x:data to register the nickname" -msgstr "" -"Для реєстрації псевдоніму необхідно використовувати клієнт з підтримкою x:" -"data" - -#: mod_muc/mod_muc.erl:718 mod_muc/mod_muc_odbc.erl:735 -msgid "Nickname Registration at " -msgstr "Реєстрація псевдоніма на " - -#: mod_muc/mod_muc.erl:722 mod_muc/mod_muc_odbc.erl:739 -msgid "Enter nickname you want to register" -msgstr "Введіть псевдонім, який ви хочете зареєструвати" - -#: mod_muc/mod_muc.erl:723 mod_muc/mod_muc_odbc.erl:740 -#: mod_muc/mod_muc_room.erl:3810 mod_roster.erl:944 mod_roster_odbc.erl:1061 -#: mod_vcard.erl:364 mod_vcard.erl:477 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:453 -msgid "Nickname" -msgstr "Псевдонім" - -#: mod_muc/mod_muc.erl:762 mod_muc/mod_muc_odbc.erl:784 -#: mod_muc/mod_muc_room.erl:1064 mod_muc/mod_muc_room.erl:1787 -msgid "That nickname is registered by another person" -msgstr "Псевдонім зареєстровано кимось іншим" - -#: mod_muc/mod_muc.erl:788 mod_muc/mod_muc_odbc.erl:811 -msgid "You must fill in field \"Nickname\" in the form" -msgstr "Вам необхідно заповнити поле \"Псевдонім\" у формі" - -#: mod_muc/mod_muc.erl:808 mod_muc/mod_muc_odbc.erl:831 -msgid "ejabberd MUC module" -msgstr "ejabberd MUC модуль" - -#: mod_muc/mod_muc_log.erl:374 mod_muc/mod_muc_log.erl:381 -msgid "Chatroom configuration modified" -msgstr "Конфігурація кімнати змінилась" - -#: mod_muc/mod_muc_log.erl:384 -msgid "joins the room" -msgstr "увійшов(ла) в кімнату" - -#: mod_muc/mod_muc_log.erl:387 mod_muc/mod_muc_log.erl:390 -msgid "leaves the room" -msgstr "вийшов(ла) з кімнати" - -#: mod_muc/mod_muc_log.erl:393 mod_muc/mod_muc_log.erl:396 -msgid "has been banned" -msgstr "заборонили вхід в кімнату" - -#: mod_muc/mod_muc_log.erl:399 mod_muc/mod_muc_log.erl:402 -msgid "has been kicked" -msgstr "вигнали з кімнати" - -#: mod_muc/mod_muc_log.erl:405 -msgid "has been kicked because of an affiliation change" -msgstr "вигнано з кімнати внаслідок зміни рангу" - -#: mod_muc/mod_muc_log.erl:408 -msgid "has been kicked because the room has been changed to members-only" -msgstr "вигнано з кімнати тому, що вона стала тільки для учасників" - -#: mod_muc/mod_muc_log.erl:411 -msgid "has been kicked because of a system shutdown" -msgstr "вигнано з кімнати внаслідок зупинки системи" - -#: mod_muc/mod_muc_log.erl:414 -msgid "is now known as" -msgstr "змінив(ла) псевдонім на" - -#: mod_muc/mod_muc_log.erl:417 mod_muc/mod_muc_log.erl:688 -#: mod_muc/mod_muc_room.erl:2393 -msgid " has set the subject to: " -msgstr " встановив(ла) тему: " - -#: mod_muc/mod_muc_log.erl:452 -msgid "Chatroom is created" -msgstr "Створено кімнату" - -#: mod_muc/mod_muc_log.erl:453 -msgid "Chatroom is destroyed" -msgstr "Знищено кімнату" - -#: mod_muc/mod_muc_log.erl:454 -msgid "Chatroom is started" -msgstr "Запущено кімнату" - -#: mod_muc/mod_muc_log.erl:455 -msgid "Chatroom is stopped" -msgstr "Зупинено кімнату" - -#: mod_muc/mod_muc_log.erl:459 -msgid "Monday" -msgstr "Понеділок" - -#: mod_muc/mod_muc_log.erl:460 -msgid "Tuesday" -msgstr "Вівторок" - -#: mod_muc/mod_muc_log.erl:461 -msgid "Wednesday" -msgstr "Середа" - -#: mod_muc/mod_muc_log.erl:462 -msgid "Thursday" -msgstr "Четвер" - -#: mod_muc/mod_muc_log.erl:463 -msgid "Friday" -msgstr "П'ятниця" - -#: mod_muc/mod_muc_log.erl:464 -msgid "Saturday" -msgstr "Субота" - -#: mod_muc/mod_muc_log.erl:465 -msgid "Sunday" -msgstr "Неділя" - -#: mod_muc/mod_muc_log.erl:469 -msgid "January" -msgstr "січня" - -#: mod_muc/mod_muc_log.erl:470 -msgid "February" -msgstr "лютого" - -#: mod_muc/mod_muc_log.erl:471 -msgid "March" -msgstr "березня" - -#: mod_muc/mod_muc_log.erl:472 -msgid "April" -msgstr "квітня" - -#: mod_muc/mod_muc_log.erl:473 -msgid "May" -msgstr "травня" - -#: mod_muc/mod_muc_log.erl:474 -msgid "June" -msgstr "червня" - -#: mod_muc/mod_muc_log.erl:475 -msgid "July" -msgstr "липня" - -#: mod_muc/mod_muc_log.erl:476 -msgid "August" -msgstr "серпня" - -#: mod_muc/mod_muc_log.erl:477 -msgid "September" -msgstr "вересня" - -#: mod_muc/mod_muc_log.erl:478 -msgid "October" -msgstr "грудня" - -#: mod_muc/mod_muc_log.erl:479 -msgid "November" -msgstr "листопада" - -#: mod_muc/mod_muc_log.erl:480 -msgid "December" -msgstr "грудня" - -#: mod_muc/mod_muc_log.erl:750 -msgid "Room Configuration" -msgstr "Конфігурація кімнати" - -#: mod_muc/mod_muc_log.erl:759 -msgid "Room Occupants" -msgstr "Учасники кімнати" - -#: mod_muc/mod_muc_room.erl:174 -msgid "Traffic rate limit is exceeded" -msgstr "Швидкість передачі інформації було перевищено" - -#: mod_muc/mod_muc_room.erl:246 -msgid "" -"This participant is kicked from the room because he sent an error message" -msgstr "" -"Цього учасника було відключено від кімнати через те, що він надіслав " -"помилкове повідомлення" - -#: mod_muc/mod_muc_room.erl:255 -msgid "It is not allowed to send private messages to the conference" -msgstr "Не дозволяється надсилати приватні повідомлення в конференцію" - -#: mod_muc/mod_muc_room.erl:332 -msgid "Please, wait for a while before sending new voice request" -msgstr "" - -#: mod_muc/mod_muc_room.erl:347 -msgid "Voice requests are disabled in this conference" -msgstr "" - -#: mod_muc/mod_muc_room.erl:364 -msgid "Failed to extract JID from your voice request approval" -msgstr "" - -#: mod_muc/mod_muc_room.erl:393 -msgid "Only moderators can approve voice requests" -msgstr "" - -#: mod_muc/mod_muc_room.erl:408 -msgid "Improper message type" -msgstr "Неправильний тип повідомлення" - -#: mod_muc/mod_muc_room.erl:518 -msgid "" -"This participant is kicked from the room because he sent an error message to " -"another participant" -msgstr "" -"Цього учасника було відключено від кімнати через те, що він надіслав " -"помилкове повідомлення іншому учаснику" - -#: mod_muc/mod_muc_room.erl:531 -msgid "It is not allowed to send private messages of type \"groupchat\"" -msgstr "Не дозволяється надсилати приватні повідомлення типу \"groupchat\"" - -#: mod_muc/mod_muc_room.erl:543 mod_muc/mod_muc_room.erl:611 -msgid "Recipient is not in the conference room" -msgstr "Адресата немає в конференції" - -#: mod_muc/mod_muc_room.erl:564 mod_muc/mod_muc_room.erl:585 -msgid "It is not allowed to send private messages" -msgstr "Приватні повідомлення не дозволені" - -#: mod_muc/mod_muc_room.erl:576 mod_muc/mod_muc_room.erl:956 -#: mod_muc/mod_muc_room.erl:4040 -msgid "Only occupants are allowed to send messages to the conference" -msgstr "Тільки присутнім дозволяється надсилати повідомленняя в конференцію" - -#: mod_muc/mod_muc_room.erl:634 -msgid "Only occupants are allowed to send queries to the conference" -msgstr "Тільки присутнім дозволяється відправляти запити в конференцію" - -#: mod_muc/mod_muc_room.erl:646 -msgid "Queries to the conference members are not allowed in this room" -msgstr "Запити до користувачів в цій конференції заборонені" - -#: mod_muc/mod_muc_room.erl:932 -msgid "" -"Only moderators and participants are allowed to change the subject in this " -"room" -msgstr "Тільки модератори та учасники можуть змінювати тему в цій кімнаті" - -#: mod_muc/mod_muc_room.erl:937 -msgid "Only moderators are allowed to change the subject in this room" -msgstr "Тільки модератори можуть змінювати тему в цій кімнаті" - -#: mod_muc/mod_muc_room.erl:947 -msgid "Visitors are not allowed to send messages to all occupants" -msgstr "Відвідувачам не дозволяється надсилати повідомлення всім присутнім" - -#: mod_muc/mod_muc_room.erl:1021 -msgid "" -"This participant is kicked from the room because he sent an error presence" -msgstr "" -"Цього учасника було відключено від кімнати через те, що він надіслав " -"помилковий статус присутності" - -#: mod_muc/mod_muc_room.erl:1040 -msgid "Visitors are not allowed to change their nicknames in this room" -msgstr "Відвідувачам не дозволяється змінювати псевдонім в цій кімнаті" - -#: mod_muc/mod_muc_room.erl:1053 mod_muc/mod_muc_room.erl:1779 -msgid "That nickname is already in use by another occupant" -msgstr "Псевдонім зайнято кимось з присутніх" - -#: mod_muc/mod_muc_room.erl:1768 -msgid "You have been banned from this room" -msgstr "Вам заборонено входити в цю конференцію" - -#: mod_muc/mod_muc_room.erl:1771 -msgid "Membership is required to enter this room" -msgstr "В цю конференцію можуть входити тільки її члени" - -#: mod_muc/mod_muc_room.erl:1807 -msgid "This room is not anonymous" -msgstr "Ця кімната не анонімна" - -#: mod_muc/mod_muc_room.erl:1833 -msgid "A password is required to enter this room" -msgstr "Щоб зайти в цю конференцію, необхідно ввести пароль" - -#: mod_muc/mod_muc_room.erl:1855 mod_register.erl:246 -msgid "Too many CAPTCHA requests" -msgstr "Надто багато CAPTCHA-запитів" - -#: mod_muc/mod_muc_room.erl:1864 mod_register.erl:251 -msgid "Unable to generate a CAPTCHA" -msgstr "Нема можливості згенерувати капчу" - -#: mod_muc/mod_muc_room.erl:1874 -msgid "Incorrect password" -msgstr "Неправильний пароль" - -#: mod_muc/mod_muc_room.erl:2448 -msgid "Administrator privileges required" -msgstr "Необхідні права адміністратора" - -#: mod_muc/mod_muc_room.erl:2463 -msgid "Moderator privileges required" -msgstr "Необхідні права модератора" - -#: mod_muc/mod_muc_room.erl:2619 -msgid "Jabber ID ~s is invalid" -msgstr "Jabber ID ~s недопустимий" - -#: mod_muc/mod_muc_room.erl:2633 -msgid "Nickname ~s does not exist in the room" -msgstr "Псевдонім ~s в кімнаті відсутній" - -#: mod_muc/mod_muc_room.erl:2659 mod_muc/mod_muc_room.erl:3049 -msgid "Invalid affiliation: ~s" -msgstr "Недопустимий ранг: ~s" - -#: mod_muc/mod_muc_room.erl:2713 -msgid "Invalid role: ~s" -msgstr "Недопустима роль: ~s" - -#: mod_muc/mod_muc_room.erl:3026 mod_muc/mod_muc_room.erl:3062 -msgid "Owner privileges required" -msgstr "Необхідні права власника" - -#: mod_muc/mod_muc_room.erl:3195 -msgid "Configuration of room ~s" -msgstr "Конфігурація кімнати ~s" - -#: mod_muc/mod_muc_room.erl:3200 -msgid "Room title" -msgstr "Назва кімнати" - -#: mod_muc/mod_muc_room.erl:3203 mod_muc/mod_muc_room.erl:3692 -msgid "Room description" -msgstr "Опис кімнати" - -#: mod_muc/mod_muc_room.erl:3210 -msgid "Make room persistent" -msgstr "Зробити кімнату постійною" - -#: mod_muc/mod_muc_room.erl:3215 -msgid "Make room public searchable" -msgstr "Зробити кімнату видимою всім" - -#: mod_muc/mod_muc_room.erl:3218 -msgid "Make participants list public" -msgstr "Зробити список учасників видимим всім" - -#: mod_muc/mod_muc_room.erl:3221 -msgid "Make room password protected" -msgstr "Зробити кімнату захищеною паролем" - -#: mod_muc/mod_muc_room.erl:3232 -msgid "Maximum Number of Occupants" -msgstr "Максимальна кількість учасників" - -#: mod_muc/mod_muc_room.erl:3239 -msgid "No limit" -msgstr "Без обмежень" - -#: mod_muc/mod_muc_room.erl:3250 -msgid "Present real Jabber IDs to" -msgstr "Зробити реальні Jabber ID учасників видимими" - -#: mod_muc/mod_muc_room.erl:3258 mod_muc/mod_muc_room.erl:3292 -msgid "moderators only" -msgstr "тільки модераторам" - -#: mod_muc/mod_muc_room.erl:3260 mod_muc/mod_muc_room.erl:3294 -msgid "anyone" -msgstr "всім учасникам" - -#: mod_muc/mod_muc_room.erl:3262 -msgid "Make room members-only" -msgstr "Кімната тільки для зареєтрованых учасників" - -#: mod_muc/mod_muc_room.erl:3265 -msgid "Make room moderated" -msgstr "Зробити кімнату модерованою" - -#: mod_muc/mod_muc_room.erl:3268 -msgid "Default users as participants" -msgstr "Зробити користувачів учасниками за замовчуванням" - -#: mod_muc/mod_muc_room.erl:3271 -msgid "Allow users to change the subject" -msgstr "Дозволити користувачам змінювати тему" - -#: mod_muc/mod_muc_room.erl:3274 -msgid "Allow users to send private messages" -msgstr "Дозволити приватні повідомлення" - -#: mod_muc/mod_muc_room.erl:3279 -#, fuzzy -msgid "Allow visitors to send private messages to" -msgstr "Дозволити приватні повідомлення" - -#: mod_muc/mod_muc_room.erl:3290 -msgid "nobody" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3296 -msgid "Allow users to query other users" -msgstr "Дозволити iq-запити до користувачів" - -#: mod_muc/mod_muc_room.erl:3299 -msgid "Allow users to send invites" -msgstr "Дозволити користувачам надсилати запрошення" - -#: mod_muc/mod_muc_room.erl:3302 -msgid "Allow visitors to send status text in presence updates" -msgstr "" -"Дозволити відвідувачам відсилати текст статусу в оновленнях присутності" - -#: mod_muc/mod_muc_room.erl:3305 -msgid "Allow visitors to change nickname" -msgstr "Дозволити відвідувачам змінювати псевдонім" - -#: mod_muc/mod_muc_room.erl:3308 -#, fuzzy -msgid "Allow visitors to send voice requests" -msgstr "Дозволити користувачам надсилати запрошення" - -#: mod_muc/mod_muc_room.erl:3311 -msgid "Minimum interval between voice requests (in seconds)" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3317 -msgid "Make room CAPTCHA protected" -msgstr "Зробити кімнату захищеною капчею" - -#: mod_muc/mod_muc_room.erl:3322 -msgid "Exclude Jabber IDs from CAPTCHA challenge" -msgstr "Пропускати ці Jabber ID без CAPTCHA-запиту" - -#: mod_muc/mod_muc_room.erl:3329 -msgid "Enable logging" -msgstr "Включити журнал роботи" - -#: mod_muc/mod_muc_room.erl:3337 -msgid "You need an x:data capable client to configure room" -msgstr "" -"Для конфігурування кімнати потрібно використовувати клієнт з підтримкою x:" -"data" - -#: mod_muc/mod_muc_room.erl:3694 -msgid "Number of occupants" -msgstr "Кількість присутніх" - -#: mod_muc/mod_muc_room.erl:3750 -msgid "private, " -msgstr "приватна, " - -#: mod_muc/mod_muc_room.erl:3799 -msgid "Voice request" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3803 -msgid "Either approve or decline the voice request." -msgstr "" - -#: mod_muc/mod_muc_room.erl:3809 -#, fuzzy -msgid "User JID" -msgstr "Користувач " - -#: mod_muc/mod_muc_room.erl:3811 -msgid "Grant voice to this person?" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3960 -msgid "~s invites you to the room ~s" -msgstr "~s запрошує вас до кімнати ~s" - -#: mod_muc/mod_muc_room.erl:3969 -msgid "the password is" -msgstr "пароль:" - -#: mod_offline.erl:510 mod_offline_odbc.erl:352 -msgid "" -"Your contact offline message queue is full. The message has been discarded." -msgstr "" -"Черга повідомлень, що не були доставлені, переповнена. Повідомлення не було " -"збережено." - -#: mod_offline.erl:560 mod_offline_odbc.erl:408 -msgid "~s's Offline Messages Queue" -msgstr "Черга офлайнових повідомлень ~s" - -#: mod_offline.erl:563 mod_offline_odbc.erl:411 mod_roster.erl:987 -#: mod_roster_odbc.erl:1104 mod_shared_roster.erl:901 -#: mod_shared_roster.erl:1010 mod_shared_roster_odbc.erl:930 -#: mod_shared_roster_odbc.erl:1039 web/ejabberd_web_admin.erl:833 -#: web/ejabberd_web_admin.erl:874 web/ejabberd_web_admin.erl:942 -#: web/ejabberd_web_admin.erl:978 web/ejabberd_web_admin.erl:1019 -#: web/ejabberd_web_admin.erl:1508 web/ejabberd_web_admin.erl:1760 -#: web/ejabberd_web_admin.erl:1931 web/ejabberd_web_admin.erl:1963 -#: web/ejabberd_web_admin.erl:2031 web/ejabberd_web_admin.erl:2135 -#: web/ejabberd_web_admin.erl:2160 web/ejabberd_web_admin.erl:2248 -msgid "Submitted" -msgstr "Відправлено" - -#: mod_offline.erl:571 -msgid "Time" -msgstr "Час" - -#: mod_offline.erl:572 -msgid "From" -msgstr "Від кого" - -#: mod_offline.erl:573 -msgid "To" -msgstr "Кому" - -#: mod_offline.erl:574 mod_offline_odbc.erl:419 -msgid "Packet" -msgstr "Пакет" - -#: mod_offline.erl:587 mod_offline_odbc.erl:432 mod_shared_roster.erl:908 -#: mod_shared_roster_odbc.erl:937 web/ejabberd_web_admin.erl:882 -#: web/ejabberd_web_admin.erl:986 -msgid "Delete Selected" -msgstr "Видалити виділені" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -msgid "Offline Messages:" -msgstr "Офлайнові повідомлення:" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -msgid "Remove All Offline Messages" -msgstr "Видалити всі офлайнові повідомлення" - -#: mod_proxy65/mod_proxy65_service.erl:213 -msgid "ejabberd SOCKS5 Bytestreams module" -msgstr "ejabberd SOCKS5 Bytestreams модуль" - -#: mod_pubsub/mod_pubsub.erl:1120 mod_pubsub/mod_pubsub_odbc.erl:929 -msgid "Publish-Subscribe" -msgstr "Публікація-Підписка" - -#: mod_pubsub/mod_pubsub.erl:1214 mod_pubsub/mod_pubsub_odbc.erl:1025 -msgid "ejabberd Publish-Subscribe module" -msgstr "Модуль ejabberd Публікації-Підписки" - -#: mod_pubsub/mod_pubsub.erl:1497 mod_pubsub/mod_pubsub_odbc.erl:1312 -msgid "PubSub subscriber request" -msgstr "Запит на підписку PubSub" - -#: mod_pubsub/mod_pubsub.erl:1499 mod_pubsub/mod_pubsub_odbc.erl:1314 -msgid "Choose whether to approve this entity's subscription." -msgstr "Вирішіть, чи задовольнити запит цього об'єкту на підписку" - -#: mod_pubsub/mod_pubsub.erl:1505 mod_pubsub/mod_pubsub_odbc.erl:1320 -msgid "Node ID" -msgstr "ID вузла" - -#: mod_pubsub/mod_pubsub.erl:1510 mod_pubsub/mod_pubsub_odbc.erl:1325 -msgid "Subscriber Address" -msgstr "Адреса абонента" - -#: mod_pubsub/mod_pubsub.erl:1516 mod_pubsub/mod_pubsub_odbc.erl:1331 -msgid "Allow this Jabber ID to subscribe to this pubsub node?" -msgstr "Чи дозволити цьому Jabber ID підписатись новини наданого вузла" - -#: mod_pubsub/mod_pubsub.erl:3474 mod_pubsub/mod_pubsub_odbc.erl:3287 -msgid "Deliver payloads with event notifications" -msgstr "Доставляти разом з повідомленнями про публікації самі публікації" - -#: mod_pubsub/mod_pubsub.erl:3475 mod_pubsub/mod_pubsub_odbc.erl:3288 -msgid "Deliver event notifications" -msgstr "Доставляти сповіщення про події" - -#: mod_pubsub/mod_pubsub.erl:3476 mod_pubsub/mod_pubsub_odbc.erl:3289 -msgid "Notify subscribers when the node configuration changes" -msgstr "Повідомляти абонентів про зміни в конфігурації збірника" - -#: mod_pubsub/mod_pubsub.erl:3477 mod_pubsub/mod_pubsub_odbc.erl:3290 -msgid "Notify subscribers when the node is deleted" -msgstr "Повідомляти абонентів про видалення збірника" - -#: mod_pubsub/mod_pubsub.erl:3478 mod_pubsub/mod_pubsub_odbc.erl:3291 -msgid "Notify subscribers when items are removed from the node" -msgstr "Повідомляти абонентів про видалення публікацій із збірника" - -#: mod_pubsub/mod_pubsub.erl:3479 mod_pubsub/mod_pubsub_odbc.erl:3292 -msgid "Persist items to storage" -msgstr "Зберегати публікації до сховища" - -#: mod_pubsub/mod_pubsub.erl:3480 mod_pubsub/mod_pubsub_odbc.erl:3293 -msgid "A friendly name for the node" -msgstr "Псевдонім для вузла" - -#: mod_pubsub/mod_pubsub.erl:3481 mod_pubsub/mod_pubsub_odbc.erl:3294 -msgid "Max # of items to persist" -msgstr "Максимальне число збережених публікацій" - -#: mod_pubsub/mod_pubsub.erl:3482 mod_pubsub/mod_pubsub_odbc.erl:3295 -msgid "Whether to allow subscriptions" -msgstr "Дозволяти підписку" - -#: mod_pubsub/mod_pubsub.erl:3483 mod_pubsub/mod_pubsub_odbc.erl:3296 -msgid "Specify the access model" -msgstr "Визначити модель доступу" - -#: mod_pubsub/mod_pubsub.erl:3486 mod_pubsub/mod_pubsub_odbc.erl:3299 -msgid "Roster groups allowed to subscribe" -msgstr "Дозволені для підписки групи ростера" - -#: mod_pubsub/mod_pubsub.erl:3487 mod_pubsub/mod_pubsub_odbc.erl:3300 -msgid "Specify the publisher model" -msgstr "Умови публікації" - -#: mod_pubsub/mod_pubsub.erl:3489 mod_pubsub/mod_pubsub_odbc.erl:3302 -msgid "Purge all items when the relevant publisher goes offline" -msgstr "" -"Видалити всі елементи, коли особа, що їх опублікувала, вимикається від мережі" - -#: mod_pubsub/mod_pubsub.erl:3490 mod_pubsub/mod_pubsub_odbc.erl:3303 -msgid "Specify the event message type" -msgstr "Вкажіть тип повідомлень зі сповіщеннями про події" - -#: mod_pubsub/mod_pubsub.erl:3492 mod_pubsub/mod_pubsub_odbc.erl:3305 -msgid "Max payload size in bytes" -msgstr "Максимальний розмір корисного навантаження в байтах" - -#: mod_pubsub/mod_pubsub.erl:3493 mod_pubsub/mod_pubsub_odbc.erl:3306 -msgid "When to send the last published item" -msgstr "Коли надсилати останній опублікований елемент" - -#: mod_pubsub/mod_pubsub.erl:3495 mod_pubsub/mod_pubsub_odbc.erl:3308 -msgid "Only deliver notifications to available users" -msgstr "Доставляти повідомленнями тільки доступним користувачам" - -#: mod_pubsub/mod_pubsub.erl:3496 mod_pubsub/mod_pubsub_odbc.erl:3309 -msgid "The collections with which a node is affiliated" -msgstr "Колекція, до якої входить вузол" - -#: mod_register.erl:193 -msgid "The CAPTCHA verification has failed" -msgstr "Перевірку капчею не пройдено" - -#: mod_register.erl:220 -msgid "You need a client that supports x:data and CAPTCHA to register" -msgstr "" -"Для реєстрації псевдоніму необхідно використовувати клієнт з підтримкою x:" -"data" - -#: mod_register.erl:226 mod_register.erl:265 -msgid "Choose a username and password to register with this server" -msgstr "Виберіть назву користувача та пароль для реєстрації на цьому сервері" - -#: mod_register.erl:230 mod_vcard.erl:364 mod_vcard_odbc.erl:342 -#: web/ejabberd_web_admin.erl:1515 web/ejabberd_web_admin.erl:1572 -msgid "User" -msgstr "Користувач" - -#: mod_register.erl:316 mod_register.erl:361 -msgid "The password is too weak" -msgstr "Пароль надто простий" - -#: mod_register.erl:365 -msgid "Users are not allowed to register accounts so quickly" -msgstr "Користувачам не дозволено так часто реєструвати облікові записи" - -#: mod_roster.erl:938 mod_roster_odbc.erl:1055 web/ejabberd_web_admin.erl:1701 -#: web/ejabberd_web_admin.erl:1886 web/ejabberd_web_admin.erl:1897 -#: web/ejabberd_web_admin.erl:2219 -msgid "None" -msgstr "Немає" - -#: mod_roster.erl:945 mod_roster_odbc.erl:1062 -msgid "Subscription" -msgstr "Підписка" - -#: mod_roster.erl:946 mod_roster_odbc.erl:1063 -msgid "Pending" -msgstr "Очікування" - -#: mod_roster.erl:947 mod_roster_odbc.erl:1064 -msgid "Groups" -msgstr "Групи" - -#: mod_roster.erl:974 mod_roster_odbc.erl:1091 -msgid "Validate" -msgstr "Затвердити" - -#: mod_roster.erl:982 mod_roster_odbc.erl:1099 -msgid "Remove" -msgstr "Видалити" - -#: mod_roster.erl:985 mod_roster_odbc.erl:1102 -msgid "Roster of " -msgstr "Ростер користувача " - -#: mod_roster.erl:988 mod_roster_odbc.erl:1105 mod_shared_roster.erl:902 -#: mod_shared_roster.erl:1011 mod_shared_roster_odbc.erl:931 -#: mod_shared_roster_odbc.erl:1040 web/ejabberd_web_admin.erl:834 -#: web/ejabberd_web_admin.erl:875 web/ejabberd_web_admin.erl:943 -#: web/ejabberd_web_admin.erl:979 web/ejabberd_web_admin.erl:1020 -#: web/ejabberd_web_admin.erl:1509 web/ejabberd_web_admin.erl:1761 -#: web/ejabberd_web_admin.erl:1932 web/ejabberd_web_admin.erl:2136 -#: web/ejabberd_web_admin.erl:2161 -msgid "Bad format" -msgstr "Неправильний формат" - -#: mod_roster.erl:995 mod_roster_odbc.erl:1112 -msgid "Add Jabber ID" -msgstr "Додати Jabber ID" - -#: mod_roster.erl:1094 mod_roster_odbc.erl:1211 -msgid "Roster" -msgstr "Ростер" - -#: mod_shared_roster.erl:857 mod_shared_roster.erl:899 -#: mod_shared_roster.erl:1007 mod_shared_roster_odbc.erl:886 -#: mod_shared_roster_odbc.erl:928 mod_shared_roster_odbc.erl:1036 -msgid "Shared Roster Groups" -msgstr "Спільні групи контактів" - -#: mod_shared_roster.erl:895 mod_shared_roster_odbc.erl:924 -#: web/ejabberd_web_admin.erl:1365 web/ejabberd_web_admin.erl:2461 -msgid "Add New" -msgstr "Додати" - -#: mod_shared_roster.erl:978 mod_shared_roster_odbc.erl:1007 -msgid "Name:" -msgstr "Назва:" - -#: mod_shared_roster.erl:983 mod_shared_roster_odbc.erl:1012 -msgid "Description:" -msgstr "Опис:" - -#: mod_shared_roster.erl:991 mod_shared_roster_odbc.erl:1020 -msgid "Members:" -msgstr "Члени:" - -#: mod_shared_roster.erl:999 mod_shared_roster_odbc.erl:1028 -msgid "Displayed Groups:" -msgstr "Видимі групи:" - -#: mod_shared_roster.erl:1008 mod_shared_roster_odbc.erl:1037 -msgid "Group " -msgstr "Група " - -#: mod_shared_roster.erl:1017 mod_shared_roster_odbc.erl:1046 -#: web/ejabberd_web_admin.erl:840 web/ejabberd_web_admin.erl:884 -#: web/ejabberd_web_admin.erl:949 web/ejabberd_web_admin.erl:1026 -#: web/ejabberd_web_admin.erl:2017 -msgid "Submit" -msgstr "Відправити" - -#: mod_vcard.erl:165 mod_vcard_ldap.erl:238 mod_vcard_odbc.erl:129 -msgid "Erlang Jabber Server" -msgstr "Erlang Jabber Server" - -#: mod_vcard.erl:364 mod_vcard.erl:478 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:454 -msgid "Birthday" -msgstr "День народження" - -#: mod_vcard.erl:364 mod_vcard.erl:480 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:456 -msgid "City" -msgstr "Місто" - -#: mod_vcard.erl:364 mod_vcard.erl:479 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:455 -msgid "Country" -msgstr "Країна" - -#: mod_vcard.erl:364 mod_vcard.erl:481 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:457 -msgid "Email" -msgstr "Електронна пошта" - -#: mod_vcard.erl:364 mod_vcard.erl:476 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:452 -msgid "Family Name" -msgstr "Прізвище" - -#: mod_vcard.erl:364 mod_vcard_odbc.erl:342 -msgid "" -"Fill in the form to search for any matching Jabber User (Add * to the end of " -"field to match substring)" -msgstr "" -"Заповніть поля для пошуку користувача Jabber (Додайте * в кінець поля для " -"пошуку підрядка)" - -#: mod_vcard.erl:364 mod_vcard.erl:473 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:449 -msgid "Full Name" -msgstr "Повне ім'я" - -#: mod_vcard.erl:364 mod_vcard.erl:475 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:451 -msgid "Middle Name" -msgstr "По-батькові" - -#: mod_vcard.erl:364 mod_vcard.erl:474 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:450 web/ejabberd_web_admin.erl:2006 -msgid "Name" -msgstr "Назва" - -#: mod_vcard.erl:364 mod_vcard.erl:482 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:458 -msgid "Organization Name" -msgstr "Назва організації" - -#: mod_vcard.erl:364 mod_vcard.erl:483 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:459 -msgid "Organization Unit" -msgstr "Відділ організації" - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "Search users in " -msgstr "Пошук користувачів в " - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "You need an x:data capable client to search" -msgstr "Для пошуку необхідний клієнт із підтримкою x:data" - -#: mod_vcard.erl:389 mod_vcard_ldap.erl:487 mod_vcard_odbc.erl:367 -msgid "vCard User Search" -msgstr "Пошук користувачів по vCard" - -#: mod_vcard.erl:445 mod_vcard_ldap.erl:541 mod_vcard_odbc.erl:421 -msgid "ejabberd vCard module" -msgstr "ejabberd vCard модуль" - -#: mod_vcard.erl:469 mod_vcard_ldap.erl:551 mod_vcard_odbc.erl:445 -msgid "Search Results for " -msgstr "Результати пошуку в " - -#: mod_vcard_ldap.erl:462 -msgid "Fill in fields to search for any matching Jabber User" -msgstr "Заповніть поля для пошуку користувача Jabber" - -#: web/ejabberd_web_admin.erl:193 web/ejabberd_web_admin.erl:203 -#: web/ejabberd_web_admin.erl:219 web/ejabberd_web_admin.erl:229 -msgid "Unauthorized" -msgstr "Не авторизовано" - -#: web/ejabberd_web_admin.erl:286 web/ejabberd_web_admin.erl:303 -msgid "ejabberd Web Admin" -msgstr "Веб-інтерфейс Адміністрування ejabberd" - -#: web/ejabberd_web_admin.erl:769 web/ejabberd_web_admin.erl:780 -msgid "Administration" -msgstr "Адміністрування" - -#: web/ejabberd_web_admin.erl:878 web/ejabberd_web_admin.erl:982 -msgid "Raw" -msgstr "необроблений формат" - -#: web/ejabberd_web_admin.erl:1017 -msgid "~s access rule configuration" -msgstr "Конфігурація правила доступу ~s" - -#: web/ejabberd_web_admin.erl:1035 -msgid "Virtual Hosts" -msgstr "віртуальні хости" - -#: web/ejabberd_web_admin.erl:1043 web/ejabberd_web_admin.erl:1050 -msgid "Users" -msgstr "Користувачі" - -#: web/ejabberd_web_admin.erl:1078 -msgid "Users Last Activity" -msgstr "Статистика останнього підключення користувачів" - -#: web/ejabberd_web_admin.erl:1080 -msgid "Period: " -msgstr "Період" - -#: web/ejabberd_web_admin.erl:1090 -msgid "Last month" -msgstr "За останній місяць" - -#: web/ejabberd_web_admin.erl:1091 -msgid "Last year" -msgstr "За останній рік" - -#: web/ejabberd_web_admin.erl:1092 -msgid "All activity" -msgstr "Вся статистика" - -#: web/ejabberd_web_admin.erl:1094 -msgid "Show Ordinary Table" -msgstr "Показати звичайну таблицю" - -#: web/ejabberd_web_admin.erl:1096 -msgid "Show Integral Table" -msgstr "Показати інтегральну таблицю" - -#: web/ejabberd_web_admin.erl:1105 web/ejabberd_web_admin.erl:1939 -msgid "Statistics" -msgstr "Статистика" - -#: web/ejabberd_web_admin.erl:1117 -msgid "Not Found" -msgstr "не знайдено" - -#: web/ejabberd_web_admin.erl:1134 -msgid "Node not found" -msgstr "Вузол не знайдено" - -#: web/ejabberd_web_admin.erl:1460 -msgid "Host" -msgstr "Хост" - -#: web/ejabberd_web_admin.erl:1461 -msgid "Registered Users" -msgstr "Зареєстровані користувачі" - -#: web/ejabberd_web_admin.erl:1573 -msgid "Offline Messages" -msgstr "Офлайнові повідомлення" - -#: web/ejabberd_web_admin.erl:1574 web/ejabberd_web_admin.erl:1767 -msgid "Last Activity" -msgstr "Останнє підключення" - -#: web/ejabberd_web_admin.erl:1659 web/ejabberd_web_admin.erl:1675 -msgid "Registered Users:" -msgstr "Зареєстровані користувачі:" - -#: web/ejabberd_web_admin.erl:1661 web/ejabberd_web_admin.erl:1677 -#: web/ejabberd_web_admin.erl:2192 -msgid "Online Users:" -msgstr "Підключені користувачі:" - -#: web/ejabberd_web_admin.erl:1663 -msgid "Outgoing s2s Connections:" -msgstr "Вихідні s2s-з'єднання:" - -#: web/ejabberd_web_admin.erl:1665 -msgid "Outgoing s2s Servers:" -msgstr "Вихідні s2s-сервери:" - -#: web/ejabberd_web_admin.erl:1734 web/mod_register_web.erl:175 -#: web/mod_register_web.erl:372 web/mod_register_web.erl:381 -#: web/mod_register_web.erl:411 -msgid "Change Password" -msgstr "Змінити пароль" - -#: web/ejabberd_web_admin.erl:1758 -msgid "User " -msgstr "Користувач " - -#: web/ejabberd_web_admin.erl:1765 -msgid "Connected Resources:" -msgstr "Підключені ресурси:" - -#: web/ejabberd_web_admin.erl:1766 web/mod_register_web.erl:227 -#: web/mod_register_web.erl:519 -msgid "Password:" -msgstr "Пароль:" - -#: web/ejabberd_web_admin.erl:1828 -msgid "No Data" -msgstr "Немає даних" - -#: web/ejabberd_web_admin.erl:1906 -msgid "Nodes" -msgstr "Вузли" - -#: web/ejabberd_web_admin.erl:1929 web/ejabberd_web_admin.erl:1951 -msgid "Node " -msgstr "Вузол " - -#: web/ejabberd_web_admin.erl:1938 -msgid "Listened Ports" -msgstr "Відкриті порти" - -#: web/ejabberd_web_admin.erl:1940 web/ejabberd_web_admin.erl:2260 -#: web/ejabberd_web_admin.erl:2448 -msgid "Update" -msgstr "Обновити" - -#: web/ejabberd_web_admin.erl:1943 web/ejabberd_web_admin.erl:2569 -msgid "Restart" -msgstr "Перезапустити" - -#: web/ejabberd_web_admin.erl:1945 web/ejabberd_web_admin.erl:2571 -msgid "Stop" -msgstr "Зупинити" - -#: web/ejabberd_web_admin.erl:1959 -msgid "RPC Call Error" -msgstr "Помилка виклику RPC" - -#: web/ejabberd_web_admin.erl:2000 -msgid "Database Tables at " -msgstr "Таблиці бази даних на " - -#: web/ejabberd_web_admin.erl:2007 -msgid "Storage Type" -msgstr "Тип таблиці" - -#: web/ejabberd_web_admin.erl:2008 -msgid "Elements" -msgstr "Елементи" - -#: web/ejabberd_web_admin.erl:2009 -msgid "Memory" -msgstr "Пам'ять" - -#: web/ejabberd_web_admin.erl:2032 web/ejabberd_web_admin.erl:2137 -msgid "Error" -msgstr "Помилка" - -#: web/ejabberd_web_admin.erl:2034 -msgid "Backup of " -msgstr "Резервне копіювання " - -#: web/ejabberd_web_admin.erl:2036 -msgid "" -"Please note that these options will only backup the builtin Mnesia database. " -"If you are using the ODBC module, you also need to backup your SQL database " -"separately." -msgstr "" -"Зауважте, що ця опція відповідає за резервне копіювання тільки вбудованної " -"бази даних Mnesia. Якщо Ви також використовуєте інше сховище для даних " -"(наприклад за допомогою модуля ODBC), то його резервне копіювання потрібно " -"робити окремо." - -#: web/ejabberd_web_admin.erl:2041 -msgid "Store binary backup:" -msgstr "Зберегти бінарну резервну копію:" - -#: web/ejabberd_web_admin.erl:2045 web/ejabberd_web_admin.erl:2052 -#: web/ejabberd_web_admin.erl:2060 web/ejabberd_web_admin.erl:2067 -#: web/ejabberd_web_admin.erl:2074 web/ejabberd_web_admin.erl:2081 -#: web/ejabberd_web_admin.erl:2088 web/ejabberd_web_admin.erl:2096 -#: web/ejabberd_web_admin.erl:2103 web/ejabberd_web_admin.erl:2110 -msgid "OK" -msgstr "Продовжити" - -#: web/ejabberd_web_admin.erl:2048 -msgid "Restore binary backup immediately:" -msgstr "Відновити з бінарної резервної копії негайно:" - -#: web/ejabberd_web_admin.erl:2056 -msgid "" -"Restore binary backup after next ejabberd restart (requires less memory):" -msgstr "" -"Відновити з бінарної резервної копії при наступному запуску (потребує менше " -"пам'яті):" - -#: web/ejabberd_web_admin.erl:2063 -msgid "Store plain text backup:" -msgstr "Зберегти текстову резервну копію:" - -#: web/ejabberd_web_admin.erl:2070 -msgid "Restore plain text backup immediately:" -msgstr "Відновити з текстової резервної копії негайно:" - -#: web/ejabberd_web_admin.erl:2077 -msgid "Import users data from a PIEFXIS file (XEP-0227):" -msgstr "Імпорт даних користовучів з файлу PIEFXIS (XEP-0227):" - -#: web/ejabberd_web_admin.erl:2084 -msgid "Export data of all users in the server to PIEFXIS files (XEP-0227):" -msgstr "Експорт даних всіх користувачів сервера до файлу PIEFXIS (XEP-0227):" - -#: web/ejabberd_web_admin.erl:2091 -msgid "Export data of users in a host to PIEFXIS files (XEP-0227):" -msgstr "Експорт даних користувачів домена до файлу PIEFXIS (XEP-0227):" - -#: web/ejabberd_web_admin.erl:2099 -msgid "Import user data from jabberd14 spool file:" -msgstr "Імпорт користувачів з файла спула jabberd14:" - -#: web/ejabberd_web_admin.erl:2106 -msgid "Import users data from jabberd14 spool directory:" -msgstr "Імпорт користувачів з діректорії спула jabberd14:" - -#: web/ejabberd_web_admin.erl:2132 -msgid "Listened Ports at " -msgstr "Відкриті порти на " - -#: web/ejabberd_web_admin.erl:2157 -msgid "Modules at " -msgstr "Модулі на " - -#: web/ejabberd_web_admin.erl:2183 -msgid "Statistics of ~p" -msgstr "Статистика вузла ~p" - -#: web/ejabberd_web_admin.erl:2186 -msgid "Uptime:" -msgstr "Час роботи:" - -#: web/ejabberd_web_admin.erl:2189 -msgid "CPU Time:" -msgstr "Процесорний час:" - -#: web/ejabberd_web_admin.erl:2195 -msgid "Transactions Committed:" -msgstr "Транзакції завершені:" - -#: web/ejabberd_web_admin.erl:2198 -msgid "Transactions Aborted:" -msgstr "Транзакції відмінені:" - -#: web/ejabberd_web_admin.erl:2201 -msgid "Transactions Restarted:" -msgstr "Транзакції перезапущені:" - -#: web/ejabberd_web_admin.erl:2204 -msgid "Transactions Logged:" -msgstr "Транзакції запротокольовані:" - -#: web/ejabberd_web_admin.erl:2246 -msgid "Update " -msgstr "Поновлення " - -#: web/ejabberd_web_admin.erl:2254 -msgid "Update plan" -msgstr "План поновлення" - -#: web/ejabberd_web_admin.erl:2255 -msgid "Modified modules" -msgstr "Змінені модулі" - -#: web/ejabberd_web_admin.erl:2256 -msgid "Update script" -msgstr "Сценарій поновлення" - -#: web/ejabberd_web_admin.erl:2257 -msgid "Low level update script" -msgstr "Низькорівневий сценарій поновлення" - -#: web/ejabberd_web_admin.erl:2258 -msgid "Script check" -msgstr "Перевірка сценарію" - -#: web/ejabberd_web_admin.erl:2426 -msgid "Port" -msgstr "Порт" - -#: web/ejabberd_web_admin.erl:2427 -msgid "IP" -msgstr "IP" - -#: web/ejabberd_web_admin.erl:2428 -msgid "Protocol" -msgstr "Протокол" - -#: web/ejabberd_web_admin.erl:2429 web/ejabberd_web_admin.erl:2556 -msgid "Module" -msgstr "Модуль" - -#: web/ejabberd_web_admin.erl:2430 web/ejabberd_web_admin.erl:2557 -msgid "Options" -msgstr "Параметри" - -#: web/ejabberd_web_admin.erl:2450 -msgid "Delete" -msgstr "Видалити" - -#: web/ejabberd_web_admin.erl:2579 -msgid "Start" -msgstr "Запустити" - -#: web/mod_register_web.erl:104 -msgid "Your Jabber account was successfully created." -msgstr "Ваш Jabber-акаунт було успішно створено." - -#: web/mod_register_web.erl:107 -msgid "There was an error creating the account: " -msgstr "Помилка при створенні акаунту:" - -#: web/mod_register_web.erl:115 -msgid "Your Jabber account was successfully deleted." -msgstr "Ваш Jabber-акаунт було успішно видалено." - -#: web/mod_register_web.erl:118 -msgid "There was an error deleting the account: " -msgstr "Помилка при видаленні акаунту: " - -#: web/mod_register_web.erl:128 -msgid "The password of your Jabber account was successfully changed." -msgstr "Пароль вашого Jabber-акаунту був успішно змінений." - -#: web/mod_register_web.erl:131 -msgid "There was an error changing the password: " -msgstr "Помилка при зміні пароля: " - -#: web/mod_register_web.erl:163 web/mod_register_web.erl:172 -msgid "Jabber Account Registration" -msgstr "Реєстрація Jabber-акаунту" - -#: web/mod_register_web.erl:174 web/mod_register_web.erl:192 -#: web/mod_register_web.erl:201 -msgid "Register a Jabber account" -msgstr "Зареєструвати Jabber-акаунт" - -#: web/mod_register_web.erl:176 web/mod_register_web.erl:493 -#: web/mod_register_web.erl:502 -msgid "Unregister a Jabber account" -msgstr "Видалити Jabber-акаунт" - -#: web/mod_register_web.erl:203 -msgid "" -"This page allows to create a Jabber account in this Jabber server. Your JID " -"(Jabber IDentifier) will be of the form: username@server. Please read " -"carefully the instructions to fill correctly the fields." -msgstr "" -"Тут ви можете зареєструвати обліковий запис Jabber на цьому сервері. Ваш JID " -"(ідентифікатор Jabber) матиме вигляд \"користувач@сервер\". Щоб вірно " -"заповнити поля нижче, будь ласка, уважно читайте інструкції до них." - -#: web/mod_register_web.erl:212 web/mod_register_web.erl:386 -#: web/mod_register_web.erl:509 -msgid "Username:" -msgstr "Ім'я користувача:" - -#: web/mod_register_web.erl:217 -msgid "This is case insensitive: macbeth is the same that MacBeth and Macbeth." -msgstr "" -"Регістр не має значення: \"МАША\" та \"маша\" буде сприйматися як одне й те " -"саме ім'я." - -#: web/mod_register_web.erl:218 -msgid "Characters not allowed:" -msgstr "Заборонені символи:" - -#: web/mod_register_web.erl:222 web/mod_register_web.erl:391 -#: web/mod_register_web.erl:514 -msgid "Server:" -msgstr "Сервер:" - -#: web/mod_register_web.erl:232 -msgid "" -"Don't tell your password to anybody, not even the administrators of the " -"Jabber server." -msgstr "Нікому не кажіть свій пароль, навіть адміністраторам сервера." - -#: web/mod_register_web.erl:234 -msgid "You can later change your password using a Jabber client." -msgstr "Пізніше можна змінити пароль через Jabber-клієнт." - -#: web/mod_register_web.erl:235 -msgid "" -"Some Jabber clients can store your password in your computer. Use that " -"feature only if you trust your computer is safe." -msgstr "" -"Деякі Jabber-клієнти можуть зберігати пароль на вашому комп'ютері. " -"Користуйтесь цією функцією тільки у тому випадку, якщо вважаєте її безпечною." - -#: web/mod_register_web.erl:237 -msgid "" -"Memorize your password, or write it in a paper placed in a safe place. In " -"Jabber there isn't an automated way to recover your password if you forget " -"it." -msgstr "" -"Запам'ятайте пароль, або запишіть його на папері, який треба зберегти у " -"безпечному місці. У Jabber'і немає автоматизованих засобів відновлення " -"пароля на той випадок, якщо ви його забудете." - -#: web/mod_register_web.erl:242 web/mod_register_web.erl:406 -msgid "Password Verification:" -msgstr "Перевірка Пароля:" - -#: web/mod_register_web.erl:250 -msgid "Register" -msgstr "Реєстрація" - -#: web/mod_register_web.erl:396 -msgid "Old Password:" -msgstr "Старий пароль:" - -#: web/mod_register_web.erl:401 -msgid "New Password:" -msgstr "Новий Пароль:" - -#: web/mod_register_web.erl:504 -msgid "This page allows to unregister a Jabber account in this Jabber server." -msgstr "Ця сторінка дозволяє видалити свій акаунт з Jabber-сервера." - -#: web/mod_register_web.erl:524 -msgid "Unregister" -msgstr "Видалити" - -#~ msgid "Captcha test failed" -#~ msgstr "Перевірка капчею закінчилась невдало" diff --git a/priv/msgs/vi.msg b/priv/msgs/vi.msg index da3101110..186eb20e7 100644 --- a/priv/msgs/vi.msg +++ b/priv/msgs/vi.msg @@ -1,31 +1,27 @@ -{"Access Configuration","Cấu Hình Truy Cập"}. -{"Access Control List Configuration","Cấu Hình Danh Sách Kiểm Soát Truy Cập"}. -{"Access control lists","Danh sách kiểm soát truy cập"}. -{"Access Control Lists","Danh Sách Kiểm Soát Truy Cập"}. +%% Generated automatically +%% DO NOT EDIT: run `make translations` instead +%% To improve translations please read: +%% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ + +{" has set the subject to: "," đã đặt chủ đề thành: "}. {"Access denied by service policy","Sự truy cập bị chặn theo chính sách phục vụ"}. -{"Access rules","Quy tắc Truy Cập"}. -{"Access Rules","Quy Tắc Truy Cập"}. {"Action on user","Hành động đối với người sử dụng"}. -{"Add Jabber ID","Thêm Jabber ID"}. -{"Add New","Thêm Mới"}. {"Add User","Thêm Người Sử Dụng"}. {"Administration of ","Quản trị về "}. {"Administration","Quản trị"}. {"Administrator privileges required","Yêu cầu đặc quyền của nhà quản trị"}. {"All activity","Tất cả hoạt động"}. +{"All Users","Tất Cả Người Sử Dụng"}. {"Allow this Jabber ID to subscribe to this pubsub node?","Cho phép Jabber ID đăng ký nút môđun xuất bản đăng ký này không?"}. {"Allow users to query other users","Cho phép người sử dụng hỏi người sử dụng khác"}. {"Allow users to send invites","Cho phép người sử dụng gửi lời mời"}. {"Allow users to send private messages","Cho phép người sử dụng gửi thư riêng"}. -{"All Users","Tất Cả Người Sử Dụng"}. {"Announcements","Thông báo"}. -{"anyone","bất kỳ ai"}. {"April","Tháng Tư"}. {"August","Tháng Tám"}. {"Backup Management","Quản lý Sao Lưu Dự Phòng"}. -{"Backup of ","Sao lưu dự phòng về"}. -{"Backup","Sao lưu dự phòng"}. {"Backup to File at ","Sao lưu dự phòng ra Tập Tin tại"}. +{"Backup","Sao lưu dự phòng"}. {"Bad format","Định dạng hỏng"}. {"Birthday","Ngày sinh"}. {"Change Password","Thay Đổi Mật Khẩu"}. @@ -33,35 +29,26 @@ {"Chatroom configuration modified","Cấu hình phòng trò chuyện được chỉnh sửa"}. {"Chatrooms","Phòng trò chuyện"}. {"Choose a username and password to register with this server","Chọn một tên truy cập và mật khẩu để đăng ký với máy chủ này"}. -{"Choose modules to stop","Chọn môđun để dừng"}. {"Choose storage type of tables","Chọn loại bảng lưu trữ"}. {"Choose whether to approve this entity's subscription.","Chọn có nên chấp nhận sự đăng ký của đối tượng này không"}. {"City","Thành phố"}. {"Commands","Lệnh"}. {"Conference room does not exist","Phòng họp không tồn tại"}. {"Configuration","Cấu hình"}. -{"Connected Resources:","Tài Nguyên Được Kết Nối:"}. {"Country","Quốc gia"}. -{"CPU Time:","Thời Gian CPU:"}. -{"Database","Cơ sở dữ liệu"}. -{"Database Tables at ","Bảng Cơ Sở Dữ Liệu tại"}. {"Database Tables Configuration at ","Cấu Hình Bảng Cơ Sở Dữ Liệu tại"}. +{"Database","Cơ sở dữ liệu"}. {"December","Tháng Mười Hai"}. {"Default users as participants","Người sử dụng mặc định là người tham dự"}. {"Delete message of the day on all hosts","Xóa thư trong ngày trên tất cả các máy chủ"}. {"Delete message of the day","Xóa thư trong ngày"}. -{"Delete Selected","Tùy chọn Xóa được Chọn"}. {"Delete User","Xóa Người Sử Dụng"}. -{"Delete","Xóa"}. {"Deliver event notifications","Đưa ra các thông báo sự kiện"}. {"Deliver payloads with event notifications","Đưa ra thông tin dung lượng với các thông báo sự kiện"}. -{"Description:","Miêu tả:"}. {"Disc only copy","Chỉ sao chép vào đĩa"}. -{"Displayed Groups:","Nhóm được hiển thị:"}. {"Dump Backup to Text File at ","Kết Xuất Sao Lưu ra Tập Tin Văn Bản tại"}. {"Dump to Text File","Kết xuất ra Tập Tin Văn Bản"}. {"Edit Properties","Chỉnh Sửa Thuộc Tính"}. -{"ejabberd IRC module","Môdun ejabberd IRC Bản quyền"}. {"ejabberd MUC module","Môdun ejabberd MUC Bản quyền"}. {"ejabberd Publish-Subscribe module","Môdun ejabberd Xuất Bản-Đăng Ký Bản quyền"}. {"ejabberd SOCKS5 Bytestreams module","Môdun SOCKS5 Bytestreams Bản quyền"}. @@ -69,32 +56,21 @@ {"Email","Email"}. {"Enable logging","Cho phép ghi nhật ký"}. {"End User Session","Kết Thúc Phiên Giao Dịch Người Sử Dụng"}. -{"Enter list of {Module, [Options]}","Nhập danh sách {Môđun, [Các Tùy Chọn]}"}. {"Enter nickname you want to register","Nhập bí danh bạn muốn đăng ký"}. {"Enter path to backup file","Nhập đường dẫn đến tập tin sao lưu dự phòng"}. {"Enter path to jabberd14 spool dir","Nhập đường dẫn đến thư mục spool jabberd14"}. {"Enter path to jabberd14 spool file","Nhập đường dẫn đến tập tin spool jabberd14"}. {"Enter path to text file","Nhập đường dẫn đến tập tin văn bản"}. -{"Erlang Jabber Server","Erlang Jabber Server Bản quyền"}. {"Family Name","Họ"}. {"February","Tháng Hai"}. -{"Fill in fields to search for any matching Jabber User","Điền vào các ô để tìm kiếm bất kỳ các thông tin nào khớp với Người sử dụng Jabber"}. -{"Fill in the form to search for any matching Jabber User (Add * to the end of field to match substring)","Điền vào mẫu này để tìm kiếm bất kỳ thông tin nào khớp với Người sử dụng Jabber (Thêm dấu * vào cuối ô để thông tin khớp với chuỗi bên trong)"}. {"Friday","Thứ Sáu"}. -{"From ~s","Nhận từ ~s"}. -{"From","Từ"}. {"Full Name","Tên Đầy Đủ"}. {"Get Number of Online Users","Nhận Số Người Sử Dụng Trực Tuyến"}. {"Get Number of Registered Users","Nhận Số Người Sử Dụng Đã Đăng Ký"}. {"Get User Last Login Time","Nhận Thời Gian Đăng Nhập Cuối Cùng Của Người Sử Dụng"}. -{"Get User Password","Nhận Mật Khẩu Người Sử Dụng"}. {"Get User Statistics","Nhận Thông Tin Thống Kê Người Sử Dụng"}. -{"Group ","Nhóm "}. -{"Groups","Nhóm"}. {"has been banned","đã bị cấm"}. {"has been kicked","đã bị đẩy ra khỏi"}. -{" has set the subject to: "," đã đặt chủ đề thành: "}. -{"Host","Máy chủ"}. {"Import Directory","Nhập Thư Mục"}. {"Import File","Nhập Tập Tin"}. {"Import User from File at ","Nhập Người Sử Dụng từ Tập Tin tại"}. @@ -102,16 +78,11 @@ {"Import Users From jabberd14 Spool Files","Nhập Người Sử Dụng Từ Các Tập Tin Spool jabberd14"}. {"Improper message type","Loại thư không phù hợp"}. {"Incorrect password","Mật khẩu sai"}. -{"Invalid affiliation: ~s","Tư cách không hợp lệ: ~s"}. -{"Invalid role: ~s","Vai trò không hợp lệ: ~s"}. {"IP addresses","Địa chỉ IP"}. -{"IRC Transport","Truyền tải IRC"}. -{"IRC Username","Tên truy cập IRC"}. {"is now known as","bây giờ được biết như"}. {"It is not allowed to send private messages of type \"groupchat\"","Không được phép gửi những thư riêng loại \"groupchat\""}. {"It is not allowed to send private messages to the conference","Không được phép gửi những thư riêng đến phòng họp"}. {"Jabber ID","Jabber ID"}. -{"Jabber ID ~s is invalid","Jabber ID ~s không hợp lệ"}. {"January","Tháng Một"}. {"joins the room","tham gia phòng này"}. {"July","Tháng Bảy"}. @@ -121,45 +92,31 @@ {"Last month","Tháng trước"}. {"Last year","Năm trước"}. {"leaves the room","rời khỏi phòng này"}. -{"Listened Ports at ","Cổng Liên Lạc tại"}. -{"Listened Ports","Cổng Kết Nối"}. -{"List of modules to start","Danh sách các môđun khởi động"}. -{"Low level update script","Lệnh cập nhật mức độ thấp"}. {"Make participants list public","Tạo danh sách người tham dự công khai"}. {"Make room members-only","Tạo phòng chỉ cho phép tư cách thành viên tham gia"}. {"Make room password protected","Tạo phòng được bảo vệ bằng mật khẩu"}. {"Make room persistent","Tạo phòng bền vững"}. {"Make room public searchable","Tạo phòng có thể tìm kiếm công khai"}. {"March","Tháng Ba"}. -{"Maximum Number of Occupants","Số Lượng Người Tham Dự Tối Đa"}. -{"Max # of items to persist","Số mục tối đa để lưu trữ"}. {"Max payload size in bytes","Kích thước dung lượng byte tối đa"}. +{"Maximum Number of Occupants","Số Lượng Người Tham Dự Tối Đa"}. {"May","Tháng Năm"}. -{"Members:","Thành viên:"}. -{"Memory","Bộ Nhớ"}. {"Message body","Thân thư"}. {"Middle Name","Họ Đệm"}. {"Moderator privileges required","Yêu cầu đặc quyền của nhà điều phối"}. -{"moderators only","nhà điều phối duy nhất"}. -{"Module","Môđun"}. -{"Modules at ","Môđun tại "}. -{"Modules","Môđun"}. {"Monday","Thứ Hai"}. -{"Name:","Tên:"}. {"Name","Tên"}. {"Never","Không bao giờ"}. -{"Nickname","Bí danh"}. {"Nickname Registration at ","Đăng Ký Bí Danh tại"}. {"Nickname ~s does not exist in the room","Bí danh ~s không tồn tại trong phòng này"}. +{"Nickname","Bí danh"}. {"No body provided for announce message","Không có nội dung trong thư thông báo"}. {"No Data","Không Dữ Liệu"}. +{"No limit","Không giới hạn"}. {"Node ID","ID Nút"}. {"Node not found","Nút không tìm thấy"}. -{"Node ","Nút "}. {"Nodes","Nút"}. -{"No limit","Không giới hạn"}. {"None","Không có"}. -{"No resource provided","Không có nguồn lực cung cấp"}. {"Notify subscribers when items are removed from the node","Thông báo cho người đăng ký khi nào các mục chọn bị gỡ bỏ khỏi nút"}. {"Notify subscribers when the node configuration changes","Thông báo cho người đăng ký khi nào cấu hình nút thay đổi"}. {"Notify subscribers when the node is deleted","Thông báo cho người đăng ký khi nào nút bị xóa bỏ"}. @@ -168,35 +125,26 @@ {"Number of online users","Số người sử dụng trực tuyến"}. {"Number of registered users","Số người sử dụng đã đăng ký"}. {"October","Tháng Mười"}. -{"Offline Messages:","Thư Ngoại Tuyến:"}. -{"Offline Messages","Thư Ngoại Tuyến"}. {"OK","OK"}. -{"Online","Trực tuyến"}. -{"Online Users:","Người Sử Dụng Trực Tuyến:"}. {"Online Users","Người Sử Dụng Trực Tuyến"}. +{"Online","Trực tuyến"}. {"Only deliver notifications to available users","Chỉ gửi thông báo đến những người sử dụng hiện có"}. {"Only occupants are allowed to send messages to the conference","Chỉ có những đối tượng tham gia mới được phép gửi thư đến phòng họp"}. {"Only occupants are allowed to send queries to the conference","Chỉ có những đối tượng tham gia mới được phép gửi yêu cầu đến phòng họp"}. {"Only service administrators are allowed to send service messages","Chỉ có người quản trị dịch vụ mới được phép gửi những thư dịch vụ"}. -{"Options","Tùy chọn"}. {"Organization Name","Tên Tổ Chức"}. {"Organization Unit","Bộ Phận"}. -{"Outgoing s2s Connections:","Kết Nối Bên Ngoài s2s:"}. {"Outgoing s2s Connections","Kết Nối Bên Ngoài s2s"}. -{"Outgoing s2s Servers:","Máy chủ Bên Ngoài s2s:"}. {"Owner privileges required","Yêu cầu đặc quyền của người sở hữu"}. -{"Packet","Gói thông tin"}. -{"Password:","Mật Khẩu:"}. -{"Password","Mật Khẩu"}. {"Password Verification","Kiểm Tra Mật Khẩu"}. +{"Password","Mật Khẩu"}. +{"Password:","Mật Khẩu:"}. {"Path to Dir","Đường Dẫn đến Thư Mục"}. {"Path to File","Đường dẫn đến Tập Tin"}. -{"Pending","Chờ"}. {"Period: ","Giai đoạn: "}. {"Persist items to storage","Những mục cần để lưu trữ"}. {"Ping","Ping"}. {"Pong","Pong"}. -{"Port","Cổng"}. {"Present real Jabber IDs to","Jabber ID thực tế hiện hành đến"}. {"private, ","riêng,"}. {"Publish-Subscribe","Xuất Bản-Đăng Ký"}. @@ -204,41 +152,30 @@ {"Queries to the conference members are not allowed in this room","Không được phép gửi các yêu cầu gửi đến các thành viên trong phòng họp này"}. {"RAM and disc copy","Sao chép vào RAM và đĩa"}. {"RAM copy","Sao chép vào RAM"}. -{"Raw","Thô"}. {"Really delete message of the day?","Có thực sự xóa thư trong ngày này không?"}. {"Recipient is not in the conference room","Người nhận không có trong phòng họp"}. -{"Registered Users:","Người Sử Dụng Đã Đăng Ký:"}. -{"Registered Users","Người Sử Dụng Đã Đăng Ký"}. -{"Registration in mod_irc for ","Đăng ký trong mod_irc cho "}. {"Remote copy","Sao chép từ xa"}. -{"Remove","Gỡ bỏ"}. {"Remove User","Gỡ Bỏ Người Sử Dụng"}. {"Replaced by new connection","Được thay thế bởi kết nối mới"}. {"Resources","Nguồn tài nguyên"}. -{"Restart","Khởi động lại"}. {"Restart Service","Khởi Động Lại Dịch Vụ"}. {"Restore Backup from File at ","Phục hồi Sao Lưu từ Tập Tin tại "}. {"Restore binary backup after next ejabberd restart (requires less memory):","Khôi phục bản sao lưu dự phòng dạng nhị phân sau lần khởi động ejabberd kế tiếp (yêu cầu ít bộ nhớ hơn):"}. {"Restore binary backup immediately:","Khôi phục bản sao lưu dự phòng dạng nhị phận ngay lập tức:"}. -{"Restore","Khôi phục"}. {"Restore plain text backup immediately:","Khôi phục bản sao lưu dự phòng thuần văn bản ngay lập tức:"}. +{"Restore","Khôi phục"}. {"Room Configuration","Cấu Hình Phòng"}. {"Room creation is denied by service policy","Việc tạo phòng bị ngăn lại theo chính sách dịch vụ"}. {"Room title","Tên phòng"}. -{"Roster","Bảng phân công"}. -{"Roster of ","Bảng phân công của "}. {"Roster size","Kích thước bảng phân công"}. -{"RPC Call Error","Lỗi Gọi RPC"}. {"Running Nodes","Nút Hoạt Động"}. -{"~s access rule configuration","~s cấu hình quy tắc truy cập"}. {"Saturday","Thứ Bảy"}. -{"Script check","Lệnh kiểm tra"}. {"Search Results for ","Kết Quả Tìm Kiếm cho "}. {"Search users in ","Tìm kiếm người sử dụng trong"}. -{"Send announcement to all online users","Gửi thông báo đến tất cả người sử dụng trực tuyến"}. {"Send announcement to all online users on all hosts","Gửi thông báo đến tất cả người sử dụng trực tuyến trên tất cả các máy chủ"}. -{"Send announcement to all users","Gửi thông báo đến tất cả người sử dụng"}. +{"Send announcement to all online users","Gửi thông báo đến tất cả người sử dụng trực tuyến"}. {"Send announcement to all users on all hosts","Gửi thông báo đến tất cả người sử dụng trên tất cả các máy chủ"}. +{"Send announcement to all users","Gửi thông báo đến tất cả người sử dụng"}. {"September","Tháng Chín"}. {"Set message of the day and send to online users","Tạo lập thư trong ngày và gửi đến những người sử dụng trực tuyến"}. {"Set message of the day on all hosts and send to online users","Tạo lập thư trong ngày trên tất cả các máy chủ và gửi đến những người sử dụng trực tuyến"}. @@ -246,55 +183,27 @@ {"Show Integral Table","Hiển Thị Bảng Đầy Đủ"}. {"Show Ordinary Table","Hiển Thị Bảng Thường"}. {"Shut Down Service","Tắt Dịch Vụ"}. -{"~s invites you to the room ~s","~s mời bạn vào phòng ~s"}. {"Specify the access model","Xác định mô hình truy cập"}. {"Specify the publisher model","Xác định mô hình nhà xuất bản"}. -{"~s's Offline Messages Queue","~s's Danh Sách Chờ Thư Ngoại Tuyến"}. -{"Start","Khởi động"}. -{"Start Modules at ","Môđun Khởi Động tại "}. -{"Start Modules","Môđun Khởi Động"}. -{"Statistics of ~p","Thống kê về ~p"}. -{"Statistics","Số liệu thống kê"}. -{"Stop","Dừng"}. -{"Stop Modules at ","Môđun Dừng tại"}. -{"Stop Modules","Môđun Dừng"}. {"Stopped Nodes","Nút Dừng"}. -{"Storage Type","Loại Lưu Trữ"}. {"Store binary backup:","Lưu dữ liệu sao lưu dạng nhị phân:"}. {"Store plain text backup:","Khôi phục bản sao lưu dự phòng thuần văn bản"}. {"Subject","Tiêu đề"}. -{"Submit","Gửi"}. {"Submitted","Đã gửi"}. {"Subscriber Address","Địa Chỉ Người Đăng Ký"}. -{"Subscription","Đăng ký"}. {"Sunday","Chủ Nhật"}. {"the password is","mật khẩu là"}. {"This room is not anonymous","Phòng này không nặc danh"}. {"Thursday","Thứ Năm"}. {"Time delay","Thời gian trì hoãn"}. -{"Time","Thời Gian"}. -{"To","Đến"}. -{"To ~s","Gửi đến ~s"}. {"Traffic rate limit is exceeded","Quá giới hạn tỷ lệ lưu lượng truyền tải"}. -{"Transactions Aborted:","Giao Dịch Hủy Bỏ:"}. -{"Transactions Committed:","Giao Dịch Được Cam Kết:"}. -{"Transactions Logged:","Giao Dịch Được Ghi Nhận:"}. -{"Transactions Restarted:","Giao Dịch Khởi Động Lại:"}. {"Tuesday","Thứ Ba"}. -{"Update ","Cập Nhật "}. -{"Update","Cập Nhật"}. {"Update message of the day (don't send)","Cập nhật thư trong ngày (không gửi)"}. {"Update message of the day on all hosts (don't send)","Cập nhật thư trong ngày trên tất cả các máy chủ (không gửi)"}. -{"Update plan","Kế hoạch cập nhật"}. -{"Update script","Cập nhận lệnh"}. -{"Uptime:","Thời gian tải lên:"}. -{"Use of STARTTLS required","Yêu cầu sử dụng STARTTLS"}. {"User Management","Quản Lý Người Sử Dụng"}. -{"User ","Người sử dụng "}. {"User","Người sử dụng"}. {"Users Last Activity","Hoạt Động Cuối Cùng Của Người Sử Dụng"}. {"Users","Người sử dụng"}. -{"Validate","Xác nhận hợp lệ"}. {"vCard User Search","Tìm Kiếm Người Sử Dụng vCard"}. {"Virtual Hosts","Máy Chủ Ảo"}. {"Visitors are not allowed to send messages to all occupants","Người ghé thăm không được phép gửi thư đến tất cả các người tham dự"}. @@ -303,7 +212,5 @@ {"Whether to allow subscriptions","Xác định nên cho phép đăng ký không"}. {"You have been banned from this room","Bạn bị cấm tham gia phòng này"}. {"You must fill in field \"Nickname\" in the form","Bạn phải điền thông tin vào ô \"Nickname\" trong biểu mẫu này"}. -{"You need an x:data capable client to configure mod_irc settings","Bạn cần có một trình ứng dụng khách hỗ trợ định dạng dữ liệu x: để xác định các thiết lập mod_irc"}. -{"You need an x:data capable client to configure room","Bạn cần có một trình ứng dụng khách hỗ trợ định dạng dữ liệu x: để xác định cấu hình phòng họp"}. {"You need an x:data capable client to search","Bạn cần có một trình ứng dụng khách hỗ trợ định dạng dữ liệu x: để tìm kiếm"}. {"Your contact offline message queue is full. The message has been discarded.","Danh sách chờ thư liên lạc ngoại tuyến của bạn đã đầy. Thư này đã bị loại bỏ."}. diff --git a/priv/msgs/vi.po b/priv/msgs/vi.po deleted file mode 100644 index 4c3492148..000000000 --- a/priv/msgs/vi.po +++ /dev/null @@ -1,1905 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: 2.1.0-alpha\n" -"Last-Translator: EQHO Communications (Thailand) Ltd. - http://www.eqho.com\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Language: Vietnamese (tiếng việt)\n" - -#: ejabberd_c2s.erl:424 ejabberd_c2s.erl:727 -msgid "Use of STARTTLS required" -msgstr "Yêu cầu sử dụng STARTTLS" - -#: ejabberd_c2s.erl:503 -msgid "No resource provided" -msgstr "Không có nguồn lực cung cấp" - -#: ejabberd_c2s.erl:1197 -msgid "Replaced by new connection" -msgstr "Được thay thế bởi kết nối mới" - -#: ejabberd_c2s.erl:1885 -msgid "Your active privacy list has denied the routing of this stanza." -msgstr "" - -#: ejabberd_captcha.erl:96 ejabberd_captcha.erl:152 ejabberd_captcha.erl:178 -#, fuzzy -msgid "Enter the text you see" -msgstr "Nhập đường dẫn đến tập tin văn bản" - -#: ejabberd_captcha.erl:101 -msgid "Your messages to ~s are being blocked. To unblock them, visit ~s" -msgstr "" - -#: ejabberd_captcha.erl:134 -msgid "If you don't see the CAPTCHA image here, visit the web page." -msgstr "" - -#: ejabberd_captcha.erl:146 -msgid "CAPTCHA web page" -msgstr "" - -#: ejabberd_captcha.erl:307 -msgid "The CAPTCHA is valid." -msgstr "" - -#: mod_adhoc.erl:95 mod_adhoc.erl:125 mod_adhoc.erl:143 mod_adhoc.erl:161 -msgid "Commands" -msgstr "Lệnh" - -#: mod_adhoc.erl:149 mod_adhoc.erl:243 -msgid "Ping" -msgstr "Ping" - -#: mod_adhoc.erl:260 -msgid "Pong" -msgstr "Pong" - -#: mod_announce.erl:507 mod_announce_odbc.erl:499 -msgid "Really delete message of the day?" -msgstr "Có thực sự xóa thư trong ngày này không?" - -#: mod_announce.erl:515 mod_announce_odbc.erl:507 mod_configure.erl:1083 -#: mod_configure.erl:1128 -msgid "Subject" -msgstr "Tiêu đề" - -#: mod_announce.erl:520 mod_announce_odbc.erl:512 mod_configure.erl:1088 -#: mod_configure.erl:1133 -msgid "Message body" -msgstr "Thân thư" - -#: mod_announce.erl:600 mod_announce_odbc.erl:592 -msgid "No body provided for announce message" -msgstr "Không có nội dung trong thư thông báo" - -#: mod_announce.erl:635 mod_announce_odbc.erl:627 -msgid "Announcements" -msgstr "Thông báo" - -#: mod_announce.erl:637 mod_announce_odbc.erl:629 -msgid "Send announcement to all users" -msgstr "Gửi thông báo đến tất cả người sử dụng" - -#: mod_announce.erl:639 mod_announce_odbc.erl:631 -msgid "Send announcement to all users on all hosts" -msgstr "Gửi thông báo đến tất cả người sử dụng trên tất cả các máy chủ" - -#: mod_announce.erl:641 mod_announce_odbc.erl:633 -msgid "Send announcement to all online users" -msgstr "Gửi thông báo đến tất cả người sử dụng trực tuyến" - -#: mod_announce.erl:643 mod_announce_odbc.erl:635 mod_configure.erl:1078 -#: mod_configure.erl:1123 -msgid "Send announcement to all online users on all hosts" -msgstr "" -"Gửi thông báo đến tất cả người sử dụng trực tuyến trên tất cả các máy chủ" - -#: mod_announce.erl:645 mod_announce_odbc.erl:637 -msgid "Set message of the day and send to online users" -msgstr "Tạo lập thư trong ngày và gửi đến những người sử dụng trực tuyến" - -#: mod_announce.erl:647 mod_announce_odbc.erl:639 -msgid "Set message of the day on all hosts and send to online users" -msgstr "" -"Tạo lập thư trong ngày trên tất cả các máy chủ và gửi đến những người sử " -"dụng trực tuyến" - -#: mod_announce.erl:649 mod_announce_odbc.erl:641 -msgid "Update message of the day (don't send)" -msgstr "Cập nhật thư trong ngày (không gửi)" - -#: mod_announce.erl:651 mod_announce_odbc.erl:643 -msgid "Update message of the day on all hosts (don't send)" -msgstr "Cập nhật thư trong ngày trên tất cả các máy chủ (không gửi)" - -#: mod_announce.erl:653 mod_announce_odbc.erl:645 -msgid "Delete message of the day" -msgstr "Xóa thư trong ngày" - -#: mod_announce.erl:655 mod_announce_odbc.erl:647 -msgid "Delete message of the day on all hosts" -msgstr "Xóa thư trong ngày trên tất cả các máy chủ" - -#: mod_configure.erl:114 mod_configure.erl:274 mod_configure.erl:296 -#: mod_configure.erl:498 -msgid "Configuration" -msgstr "Cấu hình" - -#: mod_configure.erl:125 mod_configure.erl:576 web/ejabberd_web_admin.erl:1936 -msgid "Database" -msgstr "Cơ sở dữ liệu" - -#: mod_configure.erl:127 mod_configure.erl:595 -msgid "Start Modules" -msgstr "Môđun Khởi Động" - -#: mod_configure.erl:129 mod_configure.erl:596 -msgid "Stop Modules" -msgstr "Môđun Dừng" - -#: mod_configure.erl:131 mod_configure.erl:604 web/ejabberd_web_admin.erl:1937 -msgid "Backup" -msgstr "Sao lưu dự phòng" - -#: mod_configure.erl:133 mod_configure.erl:605 -msgid "Restore" -msgstr "Khôi phục" - -#: mod_configure.erl:135 mod_configure.erl:606 -msgid "Dump to Text File" -msgstr "Kết xuất ra Tập Tin Văn Bản" - -#: mod_configure.erl:137 mod_configure.erl:615 -msgid "Import File" -msgstr "Nhập Tập Tin" - -#: mod_configure.erl:139 mod_configure.erl:616 -msgid "Import Directory" -msgstr "Nhập Thư Mục" - -#: mod_configure.erl:141 mod_configure.erl:581 mod_configure.erl:1057 -msgid "Restart Service" -msgstr "Khởi Động Lại Dịch Vụ" - -#: mod_configure.erl:143 mod_configure.erl:582 mod_configure.erl:1102 -msgid "Shut Down Service" -msgstr "Tắt Dịch Vụ" - -#: mod_configure.erl:145 mod_configure.erl:518 mod_configure.erl:1197 -#: web/ejabberd_web_admin.erl:1527 -msgid "Add User" -msgstr "Thêm Người Sử Dụng" - -#: mod_configure.erl:147 mod_configure.erl:519 mod_configure.erl:1219 -msgid "Delete User" -msgstr "Xóa Người Sử Dụng" - -#: mod_configure.erl:149 mod_configure.erl:520 mod_configure.erl:1231 -msgid "End User Session" -msgstr "Kết Thúc Phiên Giao Dịch Người Sử Dụng" - -#: mod_configure.erl:151 mod_configure.erl:521 mod_configure.erl:1243 -#: mod_configure.erl:1255 -msgid "Get User Password" -msgstr "Nhận Mật Khẩu Người Sử Dụng" - -#: mod_configure.erl:153 mod_configure.erl:522 -msgid "Change User Password" -msgstr "Thay Đổi Mật Khẩu Người Sử Dụng" - -#: mod_configure.erl:155 mod_configure.erl:523 mod_configure.erl:1272 -msgid "Get User Last Login Time" -msgstr "Nhận Thời Gian Đăng Nhập Cuối Cùng Của Người Sử Dụng" - -#: mod_configure.erl:157 mod_configure.erl:524 mod_configure.erl:1284 -msgid "Get User Statistics" -msgstr "Nhận Thông Tin Thống Kê Người Sử Dụng" - -#: mod_configure.erl:159 mod_configure.erl:525 -msgid "Get Number of Registered Users" -msgstr "Nhận Số Người Sử Dụng Đã Đăng Ký" - -#: mod_configure.erl:161 mod_configure.erl:526 -msgid "Get Number of Online Users" -msgstr "Nhận Số Người Sử Dụng Trực Tuyến" - -#: mod_configure.erl:163 mod_configure.erl:509 web/ejabberd_web_admin.erl:831 -#: web/ejabberd_web_admin.erl:872 -msgid "Access Control Lists" -msgstr "Danh Sách Kiểm Soát Truy Cập" - -#: mod_configure.erl:165 mod_configure.erl:510 web/ejabberd_web_admin.erl:940 -#: web/ejabberd_web_admin.erl:976 -msgid "Access Rules" -msgstr "Quy Tắc Truy Cập" - -#: mod_configure.erl:297 mod_configure.erl:499 -msgid "User Management" -msgstr "Quản Lý Người Sử Dụng" - -#: mod_configure.erl:500 web/ejabberd_web_admin.erl:1058 -#: web/ejabberd_web_admin.erl:1462 -msgid "Online Users" -msgstr "Người Sử Dụng Trực Tuyến" - -#: mod_configure.erl:501 -msgid "All Users" -msgstr "Tất Cả Người Sử Dụng" - -#: mod_configure.erl:502 -msgid "Outgoing s2s Connections" -msgstr "Kết Nối Bên Ngoài s2s" - -#: mod_configure.erl:503 web/ejabberd_web_admin.erl:1907 -msgid "Running Nodes" -msgstr "Nút Hoạt Động" - -#: mod_configure.erl:504 web/ejabberd_web_admin.erl:1909 -msgid "Stopped Nodes" -msgstr "Nút Dừng" - -#: mod_configure.erl:577 mod_configure.erl:587 web/ejabberd_web_admin.erl:1953 -msgid "Modules" -msgstr "Môđun" - -#: mod_configure.erl:578 -msgid "Backup Management" -msgstr "Quản lý Sao Lưu Dự Phòng" - -#: mod_configure.erl:579 -msgid "Import Users From jabberd14 Spool Files" -msgstr "Nhập Người Sử Dụng Từ Các Tập Tin Spool jabberd14" - -#: mod_configure.erl:699 -msgid "To ~s" -msgstr "Gửi đến ~s" - -#: mod_configure.erl:717 -msgid "From ~s" -msgstr "Nhận từ ~s" - -#: mod_configure.erl:913 -msgid "Database Tables Configuration at " -msgstr "Cấu Hình Bảng Cơ Sở Dữ Liệu tại" - -#: mod_configure.erl:918 -msgid "Choose storage type of tables" -msgstr "Chọn loại bảng lưu trữ" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Disc only copy" -msgstr "Chỉ sao chép vào đĩa" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM and disc copy" -msgstr "Sao chép vào RAM và đĩa" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM copy" -msgstr "Sao chép vào RAM" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Remote copy" -msgstr "Sao chép từ xa" - -#: mod_configure.erl:950 -msgid "Stop Modules at " -msgstr "Môđun Dừng tại" - -#: mod_configure.erl:954 -msgid "Choose modules to stop" -msgstr "Chọn môđun để dừng" - -#: mod_configure.erl:969 -msgid "Start Modules at " -msgstr "Môđun Khởi Động tại " - -#: mod_configure.erl:973 -msgid "Enter list of {Module, [Options]}" -msgstr "Nhập danh sách {Môđun, [Các Tùy Chọn]}" - -#: mod_configure.erl:974 -msgid "List of modules to start" -msgstr "Danh sách các môđun khởi động" - -#: mod_configure.erl:983 -msgid "Backup to File at " -msgstr "Sao lưu dự phòng ra Tập Tin tại" - -#: mod_configure.erl:987 mod_configure.erl:1001 -msgid "Enter path to backup file" -msgstr "Nhập đường dẫn đến tập tin sao lưu dự phòng" - -#: mod_configure.erl:988 mod_configure.erl:1002 mod_configure.erl:1016 -#: mod_configure.erl:1030 -msgid "Path to File" -msgstr "Đường dẫn đến Tập Tin" - -#: mod_configure.erl:997 -msgid "Restore Backup from File at " -msgstr "Phục hồi Sao Lưu từ Tập Tin tại " - -#: mod_configure.erl:1011 -msgid "Dump Backup to Text File at " -msgstr "Kết Xuất Sao Lưu ra Tập Tin Văn Bản tại" - -#: mod_configure.erl:1015 -msgid "Enter path to text file" -msgstr "Nhập đường dẫn đến tập tin văn bản" - -#: mod_configure.erl:1025 -msgid "Import User from File at " -msgstr "Nhập Người Sử Dụng từ Tập Tin tại" - -#: mod_configure.erl:1029 -msgid "Enter path to jabberd14 spool file" -msgstr "Nhập đường dẫn đến tập tin spool jabberd14" - -#: mod_configure.erl:1039 -msgid "Import Users from Dir at " -msgstr "Nhập Người Sử Dụng từ Thư Mục tại" - -#: mod_configure.erl:1043 -msgid "Enter path to jabberd14 spool dir" -msgstr "Nhập đường dẫn đến thư mục spool jabberd14" - -#: mod_configure.erl:1044 -msgid "Path to Dir" -msgstr "Đường Dẫn đến Thư Mục" - -#: mod_configure.erl:1060 mod_configure.erl:1105 -msgid "Time delay" -msgstr "Thời gian trì hoãn" - -#: mod_configure.erl:1143 -msgid "Access Control List Configuration" -msgstr "Cấu Hình Danh Sách Kiểm Soát Truy Cập" - -#: mod_configure.erl:1147 -msgid "Access control lists" -msgstr "Danh sách kiểm soát truy cập" - -#: mod_configure.erl:1171 -msgid "Access Configuration" -msgstr "Cấu Hình Truy Cập" - -#: mod_configure.erl:1175 -msgid "Access rules" -msgstr "Quy tắc Truy Cập" - -#: mod_configure.erl:1200 mod_configure.erl:1222 mod_configure.erl:1234 -#: mod_configure.erl:1246 mod_configure.erl:1258 mod_configure.erl:1275 -#: mod_configure.erl:1287 mod_configure.erl:1650 mod_configure.erl:1700 -#: mod_configure.erl:1721 mod_roster.erl:943 mod_roster_odbc.erl:1060 -#: mod_vcard.erl:472 mod_vcard_ldap.erl:554 mod_vcard_odbc.erl:448 -msgid "Jabber ID" -msgstr "Jabber ID" - -#: mod_configure.erl:1205 mod_configure.erl:1263 mod_configure.erl:1651 -#: mod_configure.erl:1863 mod_muc/mod_muc_room.erl:3224 mod_register.erl:235 -#: web/ejabberd_web_admin.erl:1520 -msgid "Password" -msgstr "Mật Khẩu" - -#: mod_configure.erl:1210 -msgid "Password Verification" -msgstr "Kiểm Tra Mật Khẩu" - -#: mod_configure.erl:1301 -msgid "Number of registered users" -msgstr "Số người sử dụng đã đăng ký" - -#: mod_configure.erl:1315 -msgid "Number of online users" -msgstr "Số người sử dụng trực tuyến" - -#: mod_configure.erl:1682 web/ejabberd_web_admin.erl:1588 -#: web/ejabberd_web_admin.erl:1743 -msgid "Never" -msgstr "Không bao giờ" - -#: mod_configure.erl:1696 web/ejabberd_web_admin.erl:1601 -#: web/ejabberd_web_admin.erl:1756 -msgid "Online" -msgstr "Trực tuyến" - -#: mod_configure.erl:1701 -msgid "Last login" -msgstr "Đăng nhập lần cuối" - -#: mod_configure.erl:1722 -msgid "Roster size" -msgstr "Kích thước bảng phân công" - -#: mod_configure.erl:1723 -msgid "IP addresses" -msgstr "Địa chỉ IP" - -#: mod_configure.erl:1724 -msgid "Resources" -msgstr "Nguồn tài nguyên" - -#: mod_configure.erl:1850 -msgid "Administration of " -msgstr "Quản trị về " - -#: mod_configure.erl:1853 -msgid "Action on user" -msgstr "Hành động đối với người sử dụng" - -#: mod_configure.erl:1857 -msgid "Edit Properties" -msgstr "Chỉnh Sửa Thuộc Tính" - -#: mod_configure.erl:1860 web/ejabberd_web_admin.erl:1769 -msgid "Remove User" -msgstr "Gỡ Bỏ Người Sử Dụng" - -#: mod_irc/mod_irc.erl:201 mod_irc/mod_irc_odbc.erl:196 -#: mod_muc/mod_muc.erl:336 mod_muc/mod_muc_odbc.erl:342 -msgid "Access denied by service policy" -msgstr "Sự truy cập bị chặn theo chính sách phục vụ" - -#: mod_irc/mod_irc.erl:401 mod_irc/mod_irc_odbc.erl:398 -msgid "IRC Transport" -msgstr "Truyền tải IRC" - -#: mod_irc/mod_irc.erl:428 mod_irc/mod_irc_odbc.erl:425 -msgid "ejabberd IRC module" -msgstr "Môdun ejabberd IRC Bản quyền" - -#: mod_irc/mod_irc.erl:559 mod_irc/mod_irc_odbc.erl:568 -msgid "You need an x:data capable client to configure mod_irc settings" -msgstr "" -"Bạn cần có một trình ứng dụng khách hỗ trợ định dạng dữ liệu x: để xác định " -"các thiết lập mod_irc" - -#: mod_irc/mod_irc.erl:566 mod_irc/mod_irc_odbc.erl:575 -msgid "Registration in mod_irc for " -msgstr "Đăng ký trong mod_irc cho " - -#: mod_irc/mod_irc.erl:571 mod_irc/mod_irc_odbc.erl:580 -#, fuzzy -msgid "" -"Enter username, encodings, ports and passwords you wish to use for " -"connecting to IRC servers" -msgstr "" -"Nhập tên truy cập và mã hóa mà bạn muốn sử dụng khi kết nối với các máy chủ " -"IRC" - -#: mod_irc/mod_irc.erl:576 mod_irc/mod_irc_odbc.erl:585 -msgid "IRC Username" -msgstr "Tên truy cập IRC" - -#: mod_irc/mod_irc.erl:586 mod_irc/mod_irc_odbc.erl:595 -#, fuzzy -msgid "" -"If you want to specify different ports, passwords, encodings for IRC " -"servers, fill this list with values in format '{\"irc server\", \"encoding" -"\", port, \"password\"}'. By default this service use \"~s\" encoding, port " -"~p, empty password." -msgstr "" -"Nếu bạn muốn xác định các cách thức mã hóa khác nhau cho các máy chủ IRC, " -"hãy điền vào danh sách này những giá trị theo định dạng '{\"máy chủ irc\", " -"\"mã hóa\"}'. Dịch vụ này mặc định sử dụng định dạng mã hóa \"~s\"." - -#: mod_irc/mod_irc.erl:598 mod_irc/mod_irc_odbc.erl:607 -#, fuzzy -msgid "" -"Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef." -"net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]." -msgstr "" -"Ví dụ: [{\"irc.lucky.net\", \"koi8-r\"}, {\"vendetta.fef.net\", " -"\"iso8859-1\"}]" - -#: mod_irc/mod_irc.erl:603 mod_irc/mod_irc_odbc.erl:612 -msgid "Connections parameters" -msgstr "" - -#: mod_irc/mod_irc.erl:728 mod_irc/mod_irc_odbc.erl:757 -msgid "Join IRC channel" -msgstr "" - -#: mod_irc/mod_irc.erl:732 mod_irc/mod_irc_odbc.erl:761 -msgid "IRC channel (don't put the first #)" -msgstr "" - -#: mod_irc/mod_irc.erl:737 mod_irc/mod_irc_odbc.erl:766 -#, fuzzy -msgid "IRC server" -msgstr "Tên truy cập IRC" - -#: mod_irc/mod_irc.erl:770 mod_irc/mod_irc.erl:774 -#: mod_irc/mod_irc_odbc.erl:799 mod_irc/mod_irc_odbc.erl:803 -msgid "Join the IRC channel here." -msgstr "" - -#: mod_irc/mod_irc.erl:778 mod_irc/mod_irc_odbc.erl:807 -msgid "Join the IRC channel in this Jabber ID: ~s" -msgstr "" - -#: mod_irc/mod_irc.erl:863 mod_irc/mod_irc_odbc.erl:904 -msgid "IRC settings" -msgstr "" - -#: mod_irc/mod_irc.erl:868 mod_irc/mod_irc_odbc.erl:909 -#, fuzzy -msgid "" -"Enter username and encodings you wish to use for connecting to IRC servers. " -"Press 'Next' to get more fields to fill in. Press 'Complete' to save " -"settings." -msgstr "" -"Nhập tên truy cập và mã hóa mà bạn muốn sử dụng khi kết nối với các máy chủ " -"IRC" - -#: mod_irc/mod_irc.erl:874 mod_irc/mod_irc_odbc.erl:915 -#, fuzzy -msgid "IRC username" -msgstr "Tên truy cập IRC" - -#: mod_irc/mod_irc.erl:923 mod_irc/mod_irc_odbc.erl:964 -#, fuzzy -msgid "Password ~b" -msgstr "Mật Khẩu" - -#: mod_irc/mod_irc.erl:928 mod_irc/mod_irc_odbc.erl:969 -#, fuzzy -msgid "Port ~b" -msgstr "Cổng" - -#: mod_irc/mod_irc.erl:933 mod_irc/mod_irc_odbc.erl:974 -msgid "Encoding for server ~b" -msgstr "" - -#: mod_irc/mod_irc.erl:942 mod_irc/mod_irc_odbc.erl:983 -msgid "Server ~b" -msgstr "" - -#: mod_muc/mod_muc.erl:449 mod_muc/mod_muc_odbc.erl:456 -msgid "Only service administrators are allowed to send service messages" -msgstr "Chỉ có người quản trị dịch vụ mới được phép gửi những thư dịch vụ" - -#: mod_muc/mod_muc.erl:493 mod_muc/mod_muc_odbc.erl:500 -msgid "Room creation is denied by service policy" -msgstr "Việc tạo phòng bị ngăn lại theo chính sách dịch vụ" - -#: mod_muc/mod_muc.erl:500 mod_muc/mod_muc_odbc.erl:507 -msgid "Conference room does not exist" -msgstr "Phòng họp không tồn tại" - -#: mod_muc/mod_muc.erl:582 mod_muc/mod_muc_odbc.erl:595 -msgid "Chatrooms" -msgstr "Phòng trò chuyện" - -#: mod_muc/mod_muc.erl:712 mod_muc/mod_muc_odbc.erl:729 -#, fuzzy -msgid "You need a client that supports x:data to register the nickname" -msgstr "" -"Bạn cần có một trình ứng dụng khách hỗ trợ định dạng dữ liệu x: để đăng ký " -"bí danh" - -#: mod_muc/mod_muc.erl:718 mod_muc/mod_muc_odbc.erl:735 -msgid "Nickname Registration at " -msgstr "Đăng Ký Bí Danh tại" - -#: mod_muc/mod_muc.erl:722 mod_muc/mod_muc_odbc.erl:739 -msgid "Enter nickname you want to register" -msgstr "Nhập bí danh bạn muốn đăng ký" - -#: mod_muc/mod_muc.erl:723 mod_muc/mod_muc_odbc.erl:740 -#: mod_muc/mod_muc_room.erl:3810 mod_roster.erl:944 mod_roster_odbc.erl:1061 -#: mod_vcard.erl:364 mod_vcard.erl:477 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:453 -msgid "Nickname" -msgstr "Bí danh" - -#: mod_muc/mod_muc.erl:762 mod_muc/mod_muc_odbc.erl:784 -#: mod_muc/mod_muc_room.erl:1064 mod_muc/mod_muc_room.erl:1787 -#, fuzzy -msgid "That nickname is registered by another person" -msgstr "Một người khác đã đăng ký bí danh này rồi" - -#: mod_muc/mod_muc.erl:788 mod_muc/mod_muc_odbc.erl:811 -msgid "You must fill in field \"Nickname\" in the form" -msgstr "Bạn phải điền thông tin vào ô \"Nickname\" trong biểu mẫu này" - -#: mod_muc/mod_muc.erl:808 mod_muc/mod_muc_odbc.erl:831 -msgid "ejabberd MUC module" -msgstr "Môdun ejabberd MUC Bản quyền" - -#: mod_muc/mod_muc_log.erl:374 mod_muc/mod_muc_log.erl:381 -msgid "Chatroom configuration modified" -msgstr "Cấu hình phòng trò chuyện được chỉnh sửa" - -#: mod_muc/mod_muc_log.erl:384 -msgid "joins the room" -msgstr "tham gia phòng này" - -#: mod_muc/mod_muc_log.erl:387 mod_muc/mod_muc_log.erl:390 -msgid "leaves the room" -msgstr "rời khỏi phòng này" - -#: mod_muc/mod_muc_log.erl:393 mod_muc/mod_muc_log.erl:396 -msgid "has been banned" -msgstr "đã bị cấm" - -#: mod_muc/mod_muc_log.erl:399 mod_muc/mod_muc_log.erl:402 -msgid "has been kicked" -msgstr "đã bị đẩy ra khỏi" - -#: mod_muc/mod_muc_log.erl:405 -msgid "has been kicked because of an affiliation change" -msgstr "" - -#: mod_muc/mod_muc_log.erl:408 -msgid "has been kicked because the room has been changed to members-only" -msgstr "" - -#: mod_muc/mod_muc_log.erl:411 -msgid "has been kicked because of a system shutdown" -msgstr "" - -#: mod_muc/mod_muc_log.erl:414 -msgid "is now known as" -msgstr "bây giờ được biết như" - -#: mod_muc/mod_muc_log.erl:417 mod_muc/mod_muc_log.erl:688 -#: mod_muc/mod_muc_room.erl:2393 -msgid " has set the subject to: " -msgstr " đã đặt chủ đề thành: " - -#: mod_muc/mod_muc_log.erl:452 -#, fuzzy -msgid "Chatroom is created" -msgstr "Phòng trò chuyện" - -#: mod_muc/mod_muc_log.erl:453 -#, fuzzy -msgid "Chatroom is destroyed" -msgstr "Phòng trò chuyện" - -#: mod_muc/mod_muc_log.erl:454 -#, fuzzy -msgid "Chatroom is started" -msgstr "Phòng trò chuyện" - -#: mod_muc/mod_muc_log.erl:455 -#, fuzzy -msgid "Chatroom is stopped" -msgstr "Phòng trò chuyện" - -#: mod_muc/mod_muc_log.erl:459 -msgid "Monday" -msgstr "Thứ Hai" - -#: mod_muc/mod_muc_log.erl:460 -msgid "Tuesday" -msgstr "Thứ Ba" - -#: mod_muc/mod_muc_log.erl:461 -msgid "Wednesday" -msgstr "Thứ Tư" - -#: mod_muc/mod_muc_log.erl:462 -msgid "Thursday" -msgstr "Thứ Năm" - -#: mod_muc/mod_muc_log.erl:463 -msgid "Friday" -msgstr "Thứ Sáu" - -#: mod_muc/mod_muc_log.erl:464 -msgid "Saturday" -msgstr "Thứ Bảy" - -#: mod_muc/mod_muc_log.erl:465 -msgid "Sunday" -msgstr "Chủ Nhật" - -#: mod_muc/mod_muc_log.erl:469 -msgid "January" -msgstr "Tháng Một" - -#: mod_muc/mod_muc_log.erl:470 -msgid "February" -msgstr "Tháng Hai" - -#: mod_muc/mod_muc_log.erl:471 -msgid "March" -msgstr "Tháng Ba" - -#: mod_muc/mod_muc_log.erl:472 -msgid "April" -msgstr "Tháng Tư" - -#: mod_muc/mod_muc_log.erl:473 -msgid "May" -msgstr "Tháng Năm" - -#: mod_muc/mod_muc_log.erl:474 -msgid "June" -msgstr "Tháng Sáu" - -#: mod_muc/mod_muc_log.erl:475 -msgid "July" -msgstr "Tháng Bảy" - -#: mod_muc/mod_muc_log.erl:476 -msgid "August" -msgstr "Tháng Tám" - -#: mod_muc/mod_muc_log.erl:477 -msgid "September" -msgstr "Tháng Chín" - -#: mod_muc/mod_muc_log.erl:478 -msgid "October" -msgstr "Tháng Mười" - -#: mod_muc/mod_muc_log.erl:479 -msgid "November" -msgstr "Tháng Mười Một" - -#: mod_muc/mod_muc_log.erl:480 -msgid "December" -msgstr "Tháng Mười Hai" - -#: mod_muc/mod_muc_log.erl:750 -msgid "Room Configuration" -msgstr "Cấu Hình Phòng" - -#: mod_muc/mod_muc_log.erl:759 -#, fuzzy -msgid "Room Occupants" -msgstr "Số người tham dự" - -#: mod_muc/mod_muc_room.erl:174 -msgid "Traffic rate limit is exceeded" -msgstr "Quá giới hạn tỷ lệ lưu lượng truyền tải" - -#: mod_muc/mod_muc_room.erl:246 -msgid "" -"This participant is kicked from the room because he sent an error message" -msgstr "" - -#: mod_muc/mod_muc_room.erl:255 -msgid "It is not allowed to send private messages to the conference" -msgstr "Không được phép gửi những thư riêng đến phòng họp" - -#: mod_muc/mod_muc_room.erl:332 -msgid "Please, wait for a while before sending new voice request" -msgstr "" - -#: mod_muc/mod_muc_room.erl:347 -msgid "Voice requests are disabled in this conference" -msgstr "" - -#: mod_muc/mod_muc_room.erl:364 -msgid "Failed to extract JID from your voice request approval" -msgstr "" - -#: mod_muc/mod_muc_room.erl:393 -msgid "Only moderators can approve voice requests" -msgstr "" - -#: mod_muc/mod_muc_room.erl:408 -msgid "Improper message type" -msgstr "Loại thư không phù hợp" - -#: mod_muc/mod_muc_room.erl:518 -msgid "" -"This participant is kicked from the room because he sent an error message to " -"another participant" -msgstr "" - -#: mod_muc/mod_muc_room.erl:531 -msgid "It is not allowed to send private messages of type \"groupchat\"" -msgstr "Không được phép gửi những thư riêng loại \"groupchat\"" - -#: mod_muc/mod_muc_room.erl:543 mod_muc/mod_muc_room.erl:611 -msgid "Recipient is not in the conference room" -msgstr "Người nhận không có trong phòng họp" - -#: mod_muc/mod_muc_room.erl:564 mod_muc/mod_muc_room.erl:585 -#, fuzzy -msgid "It is not allowed to send private messages" -msgstr "Không được phép gửi những thư riêng đến phòng họp" - -#: mod_muc/mod_muc_room.erl:576 mod_muc/mod_muc_room.erl:956 -#: mod_muc/mod_muc_room.erl:4040 -msgid "Only occupants are allowed to send messages to the conference" -msgstr "Chỉ có những đối tượng tham gia mới được phép gửi thư đến phòng họp" - -#: mod_muc/mod_muc_room.erl:634 -msgid "Only occupants are allowed to send queries to the conference" -msgstr "" -"Chỉ có những đối tượng tham gia mới được phép gửi yêu cầu đến phòng họp" - -#: mod_muc/mod_muc_room.erl:646 -msgid "Queries to the conference members are not allowed in this room" -msgstr "" -"Không được phép gửi các yêu cầu gửi đến các thành viên trong phòng họp này" - -#: mod_muc/mod_muc_room.erl:932 -#, fuzzy -msgid "" -"Only moderators and participants are allowed to change the subject in this " -"room" -msgstr "" -"Chỉ có những người điều phối và những người tham gia được phép thay đổi chủ " -"đề trong phòng này" - -#: mod_muc/mod_muc_room.erl:937 -#, fuzzy -msgid "Only moderators are allowed to change the subject in this room" -msgstr "Chỉ có những người điều phối được phép thay đổi chủ đề trong phòng này" - -#: mod_muc/mod_muc_room.erl:947 -msgid "Visitors are not allowed to send messages to all occupants" -msgstr "Người ghé thăm không được phép gửi thư đến tất cả các người tham dự" - -#: mod_muc/mod_muc_room.erl:1021 -msgid "" -"This participant is kicked from the room because he sent an error presence" -msgstr "" - -#: mod_muc/mod_muc_room.erl:1040 -#, fuzzy -msgid "Visitors are not allowed to change their nicknames in this room" -msgstr "Chỉ có những người điều phối được phép thay đổi chủ đề trong phòng này" - -#: mod_muc/mod_muc_room.erl:1053 mod_muc/mod_muc_room.erl:1779 -#, fuzzy -msgid "That nickname is already in use by another occupant" -msgstr "Bí danh đang do một người tham dự khác sử dụng" - -#: mod_muc/mod_muc_room.erl:1768 -msgid "You have been banned from this room" -msgstr "Bạn bị cấm tham gia phòng này" - -#: mod_muc/mod_muc_room.erl:1771 -#, fuzzy -msgid "Membership is required to enter this room" -msgstr "Yêu cầu tư cách thành viên khi tham gia vào phòng này" - -#: mod_muc/mod_muc_room.erl:1807 -msgid "This room is not anonymous" -msgstr "Phòng này không nặc danh" - -#: mod_muc/mod_muc_room.erl:1833 -#, fuzzy -msgid "A password is required to enter this room" -msgstr "Yêu cầu nhập mật khẩu để vào phòng này" - -#: mod_muc/mod_muc_room.erl:1855 mod_register.erl:246 -msgid "Too many CAPTCHA requests" -msgstr "" - -#: mod_muc/mod_muc_room.erl:1864 mod_register.erl:251 -msgid "Unable to generate a CAPTCHA" -msgstr "" - -#: mod_muc/mod_muc_room.erl:1874 -msgid "Incorrect password" -msgstr "Mật khẩu sai" - -#: mod_muc/mod_muc_room.erl:2448 -msgid "Administrator privileges required" -msgstr "Yêu cầu đặc quyền của nhà quản trị" - -#: mod_muc/mod_muc_room.erl:2463 -msgid "Moderator privileges required" -msgstr "Yêu cầu đặc quyền của nhà điều phối" - -#: mod_muc/mod_muc_room.erl:2619 -msgid "Jabber ID ~s is invalid" -msgstr "Jabber ID ~s không hợp lệ" - -#: mod_muc/mod_muc_room.erl:2633 -msgid "Nickname ~s does not exist in the room" -msgstr "Bí danh ~s không tồn tại trong phòng này" - -#: mod_muc/mod_muc_room.erl:2659 mod_muc/mod_muc_room.erl:3049 -msgid "Invalid affiliation: ~s" -msgstr "Tư cách không hợp lệ: ~s" - -#: mod_muc/mod_muc_room.erl:2713 -msgid "Invalid role: ~s" -msgstr "Vai trò không hợp lệ: ~s" - -#: mod_muc/mod_muc_room.erl:3026 mod_muc/mod_muc_room.erl:3062 -msgid "Owner privileges required" -msgstr "Yêu cầu đặc quyền của người sở hữu" - -#: mod_muc/mod_muc_room.erl:3195 -#, fuzzy -msgid "Configuration of room ~s" -msgstr "Cấu hình cho " - -#: mod_muc/mod_muc_room.erl:3200 -msgid "Room title" -msgstr "Tên phòng" - -#: mod_muc/mod_muc_room.erl:3203 mod_muc/mod_muc_room.erl:3692 -#, fuzzy -msgid "Room description" -msgstr "Miêu tả:" - -#: mod_muc/mod_muc_room.erl:3210 -msgid "Make room persistent" -msgstr "Tạo phòng bền vững" - -#: mod_muc/mod_muc_room.erl:3215 -msgid "Make room public searchable" -msgstr "Tạo phòng có thể tìm kiếm công khai" - -#: mod_muc/mod_muc_room.erl:3218 -msgid "Make participants list public" -msgstr "Tạo danh sách người tham dự công khai" - -#: mod_muc/mod_muc_room.erl:3221 -msgid "Make room password protected" -msgstr "Tạo phòng được bảo vệ bằng mật khẩu" - -#: mod_muc/mod_muc_room.erl:3232 -msgid "Maximum Number of Occupants" -msgstr "Số Lượng Người Tham Dự Tối Đa" - -#: mod_muc/mod_muc_room.erl:3239 -msgid "No limit" -msgstr "Không giới hạn" - -#: mod_muc/mod_muc_room.erl:3250 -msgid "Present real Jabber IDs to" -msgstr "Jabber ID thực tế hiện hành đến" - -#: mod_muc/mod_muc_room.erl:3258 mod_muc/mod_muc_room.erl:3292 -msgid "moderators only" -msgstr "nhà điều phối duy nhất" - -#: mod_muc/mod_muc_room.erl:3260 mod_muc/mod_muc_room.erl:3294 -msgid "anyone" -msgstr "bất kỳ ai" - -#: mod_muc/mod_muc_room.erl:3262 -msgid "Make room members-only" -msgstr "Tạo phòng chỉ cho phép tư cách thành viên tham gia" - -#: mod_muc/mod_muc_room.erl:3265 -#, fuzzy -msgid "Make room moderated" -msgstr "Tạo phòng bền vững" - -#: mod_muc/mod_muc_room.erl:3268 -msgid "Default users as participants" -msgstr "Người sử dụng mặc định là người tham dự" - -#: mod_muc/mod_muc_room.erl:3271 -#, fuzzy -msgid "Allow users to change the subject" -msgstr "Cho phép người sử dụng thay đổi chủ đề" - -#: mod_muc/mod_muc_room.erl:3274 -msgid "Allow users to send private messages" -msgstr "Cho phép người sử dụng gửi thư riêng" - -#: mod_muc/mod_muc_room.erl:3279 -#, fuzzy -msgid "Allow visitors to send private messages to" -msgstr "Cho phép người sử dụng gửi thư riêng" - -#: mod_muc/mod_muc_room.erl:3290 -msgid "nobody" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3296 -msgid "Allow users to query other users" -msgstr "Cho phép người sử dụng hỏi người sử dụng khác" - -#: mod_muc/mod_muc_room.erl:3299 -msgid "Allow users to send invites" -msgstr "Cho phép người sử dụng gửi lời mời" - -#: mod_muc/mod_muc_room.erl:3302 -#, fuzzy -msgid "Allow visitors to send status text in presence updates" -msgstr "Cho phép người sử dụng gửi thư riêng" - -#: mod_muc/mod_muc_room.erl:3305 -#, fuzzy -msgid "Allow visitors to change nickname" -msgstr "Cho phép người sử dụng thay đổi chủ đề" - -#: mod_muc/mod_muc_room.erl:3308 -#, fuzzy -msgid "Allow visitors to send voice requests" -msgstr "Cho phép người sử dụng gửi lời mời" - -#: mod_muc/mod_muc_room.erl:3311 -msgid "Minimum interval between voice requests (in seconds)" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3317 -#, fuzzy -msgid "Make room CAPTCHA protected" -msgstr "Tạo phòng được bảo vệ bằng mật khẩu" - -#: mod_muc/mod_muc_room.erl:3322 -msgid "Exclude Jabber IDs from CAPTCHA challenge" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3329 -msgid "Enable logging" -msgstr "Cho phép ghi nhật ký" - -#: mod_muc/mod_muc_room.erl:3337 -msgid "You need an x:data capable client to configure room" -msgstr "" -"Bạn cần có một trình ứng dụng khách hỗ trợ định dạng dữ liệu x: để xác định " -"cấu hình phòng họp" - -#: mod_muc/mod_muc_room.erl:3694 -msgid "Number of occupants" -msgstr "Số người tham dự" - -#: mod_muc/mod_muc_room.erl:3750 -msgid "private, " -msgstr "riêng," - -#: mod_muc/mod_muc_room.erl:3799 -msgid "Voice request" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3803 -msgid "Either approve or decline the voice request." -msgstr "" - -#: mod_muc/mod_muc_room.erl:3809 -#, fuzzy -msgid "User JID" -msgstr "Người sử dụng " - -#: mod_muc/mod_muc_room.erl:3811 -msgid "Grant voice to this person?" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3960 -msgid "~s invites you to the room ~s" -msgstr "~s mời bạn vào phòng ~s" - -#: mod_muc/mod_muc_room.erl:3969 -msgid "the password is" -msgstr "mật khẩu là" - -#: mod_offline.erl:510 mod_offline_odbc.erl:352 -msgid "" -"Your contact offline message queue is full. The message has been discarded." -msgstr "" -"Danh sách chờ thư liên lạc ngoại tuyến của bạn đã đầy. Thư này đã bị loại bỏ." - -#: mod_offline.erl:560 mod_offline_odbc.erl:408 -msgid "~s's Offline Messages Queue" -msgstr "~s's Danh Sách Chờ Thư Ngoại Tuyến" - -#: mod_offline.erl:563 mod_offline_odbc.erl:411 mod_roster.erl:987 -#: mod_roster_odbc.erl:1104 mod_shared_roster.erl:901 -#: mod_shared_roster.erl:1010 mod_shared_roster_odbc.erl:930 -#: mod_shared_roster_odbc.erl:1039 web/ejabberd_web_admin.erl:833 -#: web/ejabberd_web_admin.erl:874 web/ejabberd_web_admin.erl:942 -#: web/ejabberd_web_admin.erl:978 web/ejabberd_web_admin.erl:1019 -#: web/ejabberd_web_admin.erl:1508 web/ejabberd_web_admin.erl:1760 -#: web/ejabberd_web_admin.erl:1931 web/ejabberd_web_admin.erl:1963 -#: web/ejabberd_web_admin.erl:2031 web/ejabberd_web_admin.erl:2135 -#: web/ejabberd_web_admin.erl:2160 web/ejabberd_web_admin.erl:2248 -msgid "Submitted" -msgstr "Đã gửi" - -#: mod_offline.erl:571 -msgid "Time" -msgstr "Thời Gian" - -#: mod_offline.erl:572 -msgid "From" -msgstr "Từ" - -#: mod_offline.erl:573 -msgid "To" -msgstr "Đến" - -#: mod_offline.erl:574 mod_offline_odbc.erl:419 -msgid "Packet" -msgstr "Gói thông tin" - -#: mod_offline.erl:587 mod_offline_odbc.erl:432 mod_shared_roster.erl:908 -#: mod_shared_roster_odbc.erl:937 web/ejabberd_web_admin.erl:882 -#: web/ejabberd_web_admin.erl:986 -msgid "Delete Selected" -msgstr "Tùy chọn Xóa được Chọn" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -msgid "Offline Messages:" -msgstr "Thư Ngoại Tuyến:" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -#, fuzzy -msgid "Remove All Offline Messages" -msgstr "Thư Ngoại Tuyến" - -#: mod_proxy65/mod_proxy65_service.erl:213 -msgid "ejabberd SOCKS5 Bytestreams module" -msgstr "Môdun SOCKS5 Bytestreams Bản quyền" - -#: mod_pubsub/mod_pubsub.erl:1120 mod_pubsub/mod_pubsub_odbc.erl:929 -msgid "Publish-Subscribe" -msgstr "Xuất Bản-Đăng Ký" - -#: mod_pubsub/mod_pubsub.erl:1214 mod_pubsub/mod_pubsub_odbc.erl:1025 -msgid "ejabberd Publish-Subscribe module" -msgstr "Môdun ejabberd Xuất Bản-Đăng Ký Bản quyền" - -#: mod_pubsub/mod_pubsub.erl:1497 mod_pubsub/mod_pubsub_odbc.erl:1312 -msgid "PubSub subscriber request" -msgstr "Yêu cầu người đăng ký môđun Xuất Bản Đăng Ký" - -#: mod_pubsub/mod_pubsub.erl:1499 mod_pubsub/mod_pubsub_odbc.erl:1314 -msgid "Choose whether to approve this entity's subscription." -msgstr "Chọn có nên chấp nhận sự đăng ký của đối tượng này không" - -#: mod_pubsub/mod_pubsub.erl:1505 mod_pubsub/mod_pubsub_odbc.erl:1320 -msgid "Node ID" -msgstr "ID Nút" - -#: mod_pubsub/mod_pubsub.erl:1510 mod_pubsub/mod_pubsub_odbc.erl:1325 -msgid "Subscriber Address" -msgstr "Địa Chỉ Người Đăng Ký" - -#: mod_pubsub/mod_pubsub.erl:1516 mod_pubsub/mod_pubsub_odbc.erl:1331 -msgid "Allow this Jabber ID to subscribe to this pubsub node?" -msgstr "Cho phép Jabber ID đăng ký nút môđun xuất bản đăng ký này không?" - -#: mod_pubsub/mod_pubsub.erl:3474 mod_pubsub/mod_pubsub_odbc.erl:3287 -msgid "Deliver payloads with event notifications" -msgstr "Đưa ra thông tin dung lượng với các thông báo sự kiện" - -#: mod_pubsub/mod_pubsub.erl:3475 mod_pubsub/mod_pubsub_odbc.erl:3288 -msgid "Deliver event notifications" -msgstr "Đưa ra các thông báo sự kiện" - -#: mod_pubsub/mod_pubsub.erl:3476 mod_pubsub/mod_pubsub_odbc.erl:3289 -msgid "Notify subscribers when the node configuration changes" -msgstr "Thông báo cho người đăng ký khi nào cấu hình nút thay đổi" - -#: mod_pubsub/mod_pubsub.erl:3477 mod_pubsub/mod_pubsub_odbc.erl:3290 -msgid "Notify subscribers when the node is deleted" -msgstr "Thông báo cho người đăng ký khi nào nút bị xóa bỏ" - -#: mod_pubsub/mod_pubsub.erl:3478 mod_pubsub/mod_pubsub_odbc.erl:3291 -msgid "Notify subscribers when items are removed from the node" -msgstr "Thông báo cho người đăng ký khi nào các mục chọn bị gỡ bỏ khỏi nút" - -#: mod_pubsub/mod_pubsub.erl:3479 mod_pubsub/mod_pubsub_odbc.erl:3292 -msgid "Persist items to storage" -msgstr "Những mục cần để lưu trữ" - -#: mod_pubsub/mod_pubsub.erl:3480 mod_pubsub/mod_pubsub_odbc.erl:3293 -msgid "A friendly name for the node" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:3481 mod_pubsub/mod_pubsub_odbc.erl:3294 -msgid "Max # of items to persist" -msgstr "Số mục tối đa để lưu trữ" - -#: mod_pubsub/mod_pubsub.erl:3482 mod_pubsub/mod_pubsub_odbc.erl:3295 -msgid "Whether to allow subscriptions" -msgstr "Xác định nên cho phép đăng ký không" - -#: mod_pubsub/mod_pubsub.erl:3483 mod_pubsub/mod_pubsub_odbc.erl:3296 -msgid "Specify the access model" -msgstr "Xác định mô hình truy cập" - -#: mod_pubsub/mod_pubsub.erl:3486 mod_pubsub/mod_pubsub_odbc.erl:3299 -msgid "Roster groups allowed to subscribe" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:3487 mod_pubsub/mod_pubsub_odbc.erl:3300 -msgid "Specify the publisher model" -msgstr "Xác định mô hình nhà xuất bản" - -#: mod_pubsub/mod_pubsub.erl:3489 mod_pubsub/mod_pubsub_odbc.erl:3302 -msgid "Purge all items when the relevant publisher goes offline" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:3490 mod_pubsub/mod_pubsub_odbc.erl:3303 -#, fuzzy -msgid "Specify the event message type" -msgstr "Xác định mô hình truy cập" - -#: mod_pubsub/mod_pubsub.erl:3492 mod_pubsub/mod_pubsub_odbc.erl:3305 -msgid "Max payload size in bytes" -msgstr "Kích thước dung lượng byte tối đa" - -#: mod_pubsub/mod_pubsub.erl:3493 mod_pubsub/mod_pubsub_odbc.erl:3306 -msgid "When to send the last published item" -msgstr "Khi cần gửi mục được xuất bản cuối cùng" - -#: mod_pubsub/mod_pubsub.erl:3495 mod_pubsub/mod_pubsub_odbc.erl:3308 -msgid "Only deliver notifications to available users" -msgstr "Chỉ gửi thông báo đến những người sử dụng hiện có" - -#: mod_pubsub/mod_pubsub.erl:3496 mod_pubsub/mod_pubsub_odbc.erl:3309 -msgid "The collections with which a node is affiliated" -msgstr "" - -#: mod_register.erl:193 -msgid "The CAPTCHA verification has failed" -msgstr "" - -#: mod_register.erl:220 -#, fuzzy -msgid "You need a client that supports x:data and CAPTCHA to register" -msgstr "" -"Bạn cần có một trình ứng dụng khách hỗ trợ định dạng dữ liệu x: để đăng ký " -"bí danh" - -#: mod_register.erl:226 mod_register.erl:265 -msgid "Choose a username and password to register with this server" -msgstr "Chọn một tên truy cập và mật khẩu để đăng ký với máy chủ này" - -#: mod_register.erl:230 mod_vcard.erl:364 mod_vcard_odbc.erl:342 -#: web/ejabberd_web_admin.erl:1515 web/ejabberd_web_admin.erl:1572 -msgid "User" -msgstr "Người sử dụng" - -#: mod_register.erl:316 mod_register.erl:361 -#, fuzzy -msgid "The password is too weak" -msgstr "mật khẩu là" - -#: mod_register.erl:365 -#, fuzzy -msgid "Users are not allowed to register accounts so quickly" -msgstr "Người ghé thăm không được phép gửi thư đến tất cả các người tham dự" - -#: mod_roster.erl:938 mod_roster_odbc.erl:1055 web/ejabberd_web_admin.erl:1701 -#: web/ejabberd_web_admin.erl:1886 web/ejabberd_web_admin.erl:1897 -#: web/ejabberd_web_admin.erl:2219 -msgid "None" -msgstr "Không có" - -#: mod_roster.erl:945 mod_roster_odbc.erl:1062 -msgid "Subscription" -msgstr "Đăng ký" - -#: mod_roster.erl:946 mod_roster_odbc.erl:1063 -msgid "Pending" -msgstr "Chờ" - -#: mod_roster.erl:947 mod_roster_odbc.erl:1064 -msgid "Groups" -msgstr "Nhóm" - -#: mod_roster.erl:974 mod_roster_odbc.erl:1091 -msgid "Validate" -msgstr "Xác nhận hợp lệ" - -#: mod_roster.erl:982 mod_roster_odbc.erl:1099 -msgid "Remove" -msgstr "Gỡ bỏ" - -#: mod_roster.erl:985 mod_roster_odbc.erl:1102 -msgid "Roster of " -msgstr "Bảng phân công của " - -#: mod_roster.erl:988 mod_roster_odbc.erl:1105 mod_shared_roster.erl:902 -#: mod_shared_roster.erl:1011 mod_shared_roster_odbc.erl:931 -#: mod_shared_roster_odbc.erl:1040 web/ejabberd_web_admin.erl:834 -#: web/ejabberd_web_admin.erl:875 web/ejabberd_web_admin.erl:943 -#: web/ejabberd_web_admin.erl:979 web/ejabberd_web_admin.erl:1020 -#: web/ejabberd_web_admin.erl:1509 web/ejabberd_web_admin.erl:1761 -#: web/ejabberd_web_admin.erl:1932 web/ejabberd_web_admin.erl:2136 -#: web/ejabberd_web_admin.erl:2161 -msgid "Bad format" -msgstr "Định dạng hỏng" - -#: mod_roster.erl:995 mod_roster_odbc.erl:1112 -msgid "Add Jabber ID" -msgstr "Thêm Jabber ID" - -#: mod_roster.erl:1094 mod_roster_odbc.erl:1211 -msgid "Roster" -msgstr "Bảng phân công" - -#: mod_shared_roster.erl:857 mod_shared_roster.erl:899 -#: mod_shared_roster.erl:1007 mod_shared_roster_odbc.erl:886 -#: mod_shared_roster_odbc.erl:928 mod_shared_roster_odbc.erl:1036 -msgid "Shared Roster Groups" -msgstr "Nhóm Phân Công Chia Sẻ" - -#: mod_shared_roster.erl:895 mod_shared_roster_odbc.erl:924 -#: web/ejabberd_web_admin.erl:1365 web/ejabberd_web_admin.erl:2461 -msgid "Add New" -msgstr "Thêm Mới" - -#: mod_shared_roster.erl:978 mod_shared_roster_odbc.erl:1007 -msgid "Name:" -msgstr "Tên:" - -#: mod_shared_roster.erl:983 mod_shared_roster_odbc.erl:1012 -msgid "Description:" -msgstr "Miêu tả:" - -#: mod_shared_roster.erl:991 mod_shared_roster_odbc.erl:1020 -msgid "Members:" -msgstr "Thành viên:" - -#: mod_shared_roster.erl:999 mod_shared_roster_odbc.erl:1028 -msgid "Displayed Groups:" -msgstr "Nhóm được hiển thị:" - -#: mod_shared_roster.erl:1008 mod_shared_roster_odbc.erl:1037 -msgid "Group " -msgstr "Nhóm " - -#: mod_shared_roster.erl:1017 mod_shared_roster_odbc.erl:1046 -#: web/ejabberd_web_admin.erl:840 web/ejabberd_web_admin.erl:884 -#: web/ejabberd_web_admin.erl:949 web/ejabberd_web_admin.erl:1026 -#: web/ejabberd_web_admin.erl:2017 -msgid "Submit" -msgstr "Gửi" - -#: mod_vcard.erl:165 mod_vcard_ldap.erl:238 mod_vcard_odbc.erl:129 -msgid "Erlang Jabber Server" -msgstr "Erlang Jabber Server Bản quyền" - -#: mod_vcard.erl:364 mod_vcard.erl:478 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:454 -msgid "Birthday" -msgstr "Ngày sinh" - -#: mod_vcard.erl:364 mod_vcard.erl:480 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:456 -msgid "City" -msgstr "Thành phố" - -#: mod_vcard.erl:364 mod_vcard.erl:479 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:455 -msgid "Country" -msgstr "Quốc gia" - -#: mod_vcard.erl:364 mod_vcard.erl:481 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:457 -msgid "Email" -msgstr "Email" - -#: mod_vcard.erl:364 mod_vcard.erl:476 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:452 -msgid "Family Name" -msgstr "Họ" - -#: mod_vcard.erl:364 mod_vcard_odbc.erl:342 -msgid "" -"Fill in the form to search for any matching Jabber User (Add * to the end of " -"field to match substring)" -msgstr "" -"Điền vào mẫu này để tìm kiếm bất kỳ thông tin nào khớp với Người sử dụng " -"Jabber (Thêm dấu * vào cuối ô để thông tin khớp với chuỗi bên trong)" - -#: mod_vcard.erl:364 mod_vcard.erl:473 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:449 -msgid "Full Name" -msgstr "Tên Đầy Đủ" - -#: mod_vcard.erl:364 mod_vcard.erl:475 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:451 -msgid "Middle Name" -msgstr "Họ Đệm" - -#: mod_vcard.erl:364 mod_vcard.erl:474 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:450 web/ejabberd_web_admin.erl:2006 -msgid "Name" -msgstr "Tên" - -#: mod_vcard.erl:364 mod_vcard.erl:482 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:458 -msgid "Organization Name" -msgstr "Tên Tổ Chức" - -#: mod_vcard.erl:364 mod_vcard.erl:483 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:459 -msgid "Organization Unit" -msgstr "Bộ Phận" - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "Search users in " -msgstr "Tìm kiếm người sử dụng trong" - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "You need an x:data capable client to search" -msgstr "" -"Bạn cần có một trình ứng dụng khách hỗ trợ định dạng dữ liệu x: để tìm kiếm" - -#: mod_vcard.erl:389 mod_vcard_ldap.erl:487 mod_vcard_odbc.erl:367 -msgid "vCard User Search" -msgstr "Tìm Kiếm Người Sử Dụng vCard" - -#: mod_vcard.erl:445 mod_vcard_ldap.erl:541 mod_vcard_odbc.erl:421 -msgid "ejabberd vCard module" -msgstr "Môdun ejabberd vCard Bản quyền" - -#: mod_vcard.erl:469 mod_vcard_ldap.erl:551 mod_vcard_odbc.erl:445 -msgid "Search Results for " -msgstr "Kết Quả Tìm Kiếm cho " - -#: mod_vcard_ldap.erl:462 -msgid "Fill in fields to search for any matching Jabber User" -msgstr "" -"Điền vào các ô để tìm kiếm bất kỳ các thông tin nào khớp với Người sử dụng " -"Jabber" - -#: web/ejabberd_web_admin.erl:193 web/ejabberd_web_admin.erl:203 -#: web/ejabberd_web_admin.erl:219 web/ejabberd_web_admin.erl:229 -msgid "Unauthorized" -msgstr "" - -#: web/ejabberd_web_admin.erl:286 web/ejabberd_web_admin.erl:303 -#, fuzzy -msgid "ejabberd Web Admin" -msgstr "Giao diện Web ejabberd" - -#: web/ejabberd_web_admin.erl:769 web/ejabberd_web_admin.erl:780 -msgid "Administration" -msgstr "Quản trị" - -#: web/ejabberd_web_admin.erl:878 web/ejabberd_web_admin.erl:982 -msgid "Raw" -msgstr "Thô" - -#: web/ejabberd_web_admin.erl:1017 -msgid "~s access rule configuration" -msgstr "~s cấu hình quy tắc truy cập" - -#: web/ejabberd_web_admin.erl:1035 -msgid "Virtual Hosts" -msgstr "Máy Chủ Ảo" - -#: web/ejabberd_web_admin.erl:1043 web/ejabberd_web_admin.erl:1050 -msgid "Users" -msgstr "Người sử dụng" - -#: web/ejabberd_web_admin.erl:1078 -msgid "Users Last Activity" -msgstr "Hoạt Động Cuối Cùng Của Người Sử Dụng" - -#: web/ejabberd_web_admin.erl:1080 -msgid "Period: " -msgstr "Giai đoạn: " - -#: web/ejabberd_web_admin.erl:1090 -msgid "Last month" -msgstr "Tháng trước" - -#: web/ejabberd_web_admin.erl:1091 -msgid "Last year" -msgstr "Năm trước" - -#: web/ejabberd_web_admin.erl:1092 -msgid "All activity" -msgstr "Tất cả hoạt động" - -#: web/ejabberd_web_admin.erl:1094 -msgid "Show Ordinary Table" -msgstr "Hiển Thị Bảng Thường" - -#: web/ejabberd_web_admin.erl:1096 -msgid "Show Integral Table" -msgstr "Hiển Thị Bảng Đầy Đủ" - -#: web/ejabberd_web_admin.erl:1105 web/ejabberd_web_admin.erl:1939 -msgid "Statistics" -msgstr "Số liệu thống kê" - -#: web/ejabberd_web_admin.erl:1117 -#, fuzzy -msgid "Not Found" -msgstr "Nút không tìm thấy" - -#: web/ejabberd_web_admin.erl:1134 -msgid "Node not found" -msgstr "Nút không tìm thấy" - -#: web/ejabberd_web_admin.erl:1460 -msgid "Host" -msgstr "Máy chủ" - -#: web/ejabberd_web_admin.erl:1461 -msgid "Registered Users" -msgstr "Người Sử Dụng Đã Đăng Ký" - -#: web/ejabberd_web_admin.erl:1573 -msgid "Offline Messages" -msgstr "Thư Ngoại Tuyến" - -#: web/ejabberd_web_admin.erl:1574 web/ejabberd_web_admin.erl:1767 -msgid "Last Activity" -msgstr "Hoạt Động Cuối Cùng" - -#: web/ejabberd_web_admin.erl:1659 web/ejabberd_web_admin.erl:1675 -msgid "Registered Users:" -msgstr "Người Sử Dụng Đã Đăng Ký:" - -#: web/ejabberd_web_admin.erl:1661 web/ejabberd_web_admin.erl:1677 -#: web/ejabberd_web_admin.erl:2192 -msgid "Online Users:" -msgstr "Người Sử Dụng Trực Tuyến:" - -#: web/ejabberd_web_admin.erl:1663 -msgid "Outgoing s2s Connections:" -msgstr "Kết Nối Bên Ngoài s2s:" - -#: web/ejabberd_web_admin.erl:1665 -msgid "Outgoing s2s Servers:" -msgstr "Máy chủ Bên Ngoài s2s:" - -#: web/ejabberd_web_admin.erl:1734 web/mod_register_web.erl:175 -#: web/mod_register_web.erl:372 web/mod_register_web.erl:381 -#: web/mod_register_web.erl:411 -msgid "Change Password" -msgstr "Thay Đổi Mật Khẩu" - -#: web/ejabberd_web_admin.erl:1758 -msgid "User " -msgstr "Người sử dụng " - -#: web/ejabberd_web_admin.erl:1765 -msgid "Connected Resources:" -msgstr "Tài Nguyên Được Kết Nối:" - -#: web/ejabberd_web_admin.erl:1766 web/mod_register_web.erl:227 -#: web/mod_register_web.erl:519 -msgid "Password:" -msgstr "Mật Khẩu:" - -#: web/ejabberd_web_admin.erl:1828 -msgid "No Data" -msgstr "Không Dữ Liệu" - -#: web/ejabberd_web_admin.erl:1906 -msgid "Nodes" -msgstr "Nút" - -#: web/ejabberd_web_admin.erl:1929 web/ejabberd_web_admin.erl:1951 -msgid "Node " -msgstr "Nút " - -#: web/ejabberd_web_admin.erl:1938 -msgid "Listened Ports" -msgstr "Cổng Kết Nối" - -#: web/ejabberd_web_admin.erl:1940 web/ejabberd_web_admin.erl:2260 -#: web/ejabberd_web_admin.erl:2448 -msgid "Update" -msgstr "Cập Nhật" - -#: web/ejabberd_web_admin.erl:1943 web/ejabberd_web_admin.erl:2569 -msgid "Restart" -msgstr "Khởi động lại" - -#: web/ejabberd_web_admin.erl:1945 web/ejabberd_web_admin.erl:2571 -msgid "Stop" -msgstr "Dừng" - -#: web/ejabberd_web_admin.erl:1959 -msgid "RPC Call Error" -msgstr "Lỗi Gọi RPC" - -#: web/ejabberd_web_admin.erl:2000 -msgid "Database Tables at " -msgstr "Bảng Cơ Sở Dữ Liệu tại" - -#: web/ejabberd_web_admin.erl:2007 -msgid "Storage Type" -msgstr "Loại Lưu Trữ" - -#: web/ejabberd_web_admin.erl:2008 -msgid "Elements" -msgstr "" - -#: web/ejabberd_web_admin.erl:2009 -msgid "Memory" -msgstr "Bộ Nhớ" - -#: web/ejabberd_web_admin.erl:2032 web/ejabberd_web_admin.erl:2137 -msgid "Error" -msgstr "" - -#: web/ejabberd_web_admin.erl:2034 -msgid "Backup of " -msgstr "Sao lưu dự phòng về" - -#: web/ejabberd_web_admin.erl:2036 -#, fuzzy -msgid "" -"Please note that these options will only backup the builtin Mnesia database. " -"If you are using the ODBC module, you also need to backup your SQL database " -"separately." -msgstr "" -"Lưu ý rằng những tùy chọn này sẽ chỉ được sao lưu cơ sở dữ liệu bên trong " -"Mnesia. Nếu bạn đang sử dụng môđun ODBC, bạn cũng cần sao lưu cơ sở dữ liệu " -"SQL của bạn riêng biệt." - -#: web/ejabberd_web_admin.erl:2041 -msgid "Store binary backup:" -msgstr "Lưu dữ liệu sao lưu dạng nhị phân:" - -#: web/ejabberd_web_admin.erl:2045 web/ejabberd_web_admin.erl:2052 -#: web/ejabberd_web_admin.erl:2060 web/ejabberd_web_admin.erl:2067 -#: web/ejabberd_web_admin.erl:2074 web/ejabberd_web_admin.erl:2081 -#: web/ejabberd_web_admin.erl:2088 web/ejabberd_web_admin.erl:2096 -#: web/ejabberd_web_admin.erl:2103 web/ejabberd_web_admin.erl:2110 -msgid "OK" -msgstr "OK" - -#: web/ejabberd_web_admin.erl:2048 -msgid "Restore binary backup immediately:" -msgstr "Khôi phục bản sao lưu dự phòng dạng nhị phận ngay lập tức:" - -#: web/ejabberd_web_admin.erl:2056 -msgid "" -"Restore binary backup after next ejabberd restart (requires less memory):" -msgstr "" -"Khôi phục bản sao lưu dự phòng dạng nhị phân sau lần khởi động ejabberd kế " -"tiếp (yêu cầu ít bộ nhớ hơn):" - -#: web/ejabberd_web_admin.erl:2063 -msgid "Store plain text backup:" -msgstr "Khôi phục bản sao lưu dự phòng thuần văn bản" - -#: web/ejabberd_web_admin.erl:2070 -msgid "Restore plain text backup immediately:" -msgstr "Khôi phục bản sao lưu dự phòng thuần văn bản ngay lập tức:" - -#: web/ejabberd_web_admin.erl:2077 -msgid "Import users data from a PIEFXIS file (XEP-0227):" -msgstr "" - -#: web/ejabberd_web_admin.erl:2084 -msgid "Export data of all users in the server to PIEFXIS files (XEP-0227):" -msgstr "" - -#: web/ejabberd_web_admin.erl:2091 -msgid "Export data of users in a host to PIEFXIS files (XEP-0227):" -msgstr "" - -#: web/ejabberd_web_admin.erl:2099 -#, fuzzy -msgid "Import user data from jabberd14 spool file:" -msgstr "Nhập Người Sử Dụng Từ Các Tập Tin Spool jabberd14" - -#: web/ejabberd_web_admin.erl:2106 -#, fuzzy -msgid "Import users data from jabberd14 spool directory:" -msgstr "Nhập Người Sử Dụng Từ Các Tập Tin Spool jabberd14" - -#: web/ejabberd_web_admin.erl:2132 -msgid "Listened Ports at " -msgstr "Cổng Liên Lạc tại" - -#: web/ejabberd_web_admin.erl:2157 -msgid "Modules at " -msgstr "Môđun tại " - -#: web/ejabberd_web_admin.erl:2183 -msgid "Statistics of ~p" -msgstr "Thống kê về ~p" - -#: web/ejabberd_web_admin.erl:2186 -msgid "Uptime:" -msgstr "Thời gian tải lên:" - -#: web/ejabberd_web_admin.erl:2189 -msgid "CPU Time:" -msgstr "Thời Gian CPU:" - -#: web/ejabberd_web_admin.erl:2195 -msgid "Transactions Committed:" -msgstr "Giao Dịch Được Cam Kết:" - -#: web/ejabberd_web_admin.erl:2198 -msgid "Transactions Aborted:" -msgstr "Giao Dịch Hủy Bỏ:" - -#: web/ejabberd_web_admin.erl:2201 -msgid "Transactions Restarted:" -msgstr "Giao Dịch Khởi Động Lại:" - -#: web/ejabberd_web_admin.erl:2204 -msgid "Transactions Logged:" -msgstr "Giao Dịch Được Ghi Nhận:" - -#: web/ejabberd_web_admin.erl:2246 -msgid "Update " -msgstr "Cập Nhật " - -#: web/ejabberd_web_admin.erl:2254 -msgid "Update plan" -msgstr "Kế hoạch cập nhật" - -#: web/ejabberd_web_admin.erl:2255 -#, fuzzy -msgid "Modified modules" -msgstr "Môđun cập nhật" - -#: web/ejabberd_web_admin.erl:2256 -msgid "Update script" -msgstr "Cập nhận lệnh" - -#: web/ejabberd_web_admin.erl:2257 -msgid "Low level update script" -msgstr "Lệnh cập nhật mức độ thấp" - -#: web/ejabberd_web_admin.erl:2258 -msgid "Script check" -msgstr "Lệnh kiểm tra" - -#: web/ejabberd_web_admin.erl:2426 -msgid "Port" -msgstr "Cổng" - -#: web/ejabberd_web_admin.erl:2427 -msgid "IP" -msgstr "" - -#: web/ejabberd_web_admin.erl:2428 -#, fuzzy -msgid "Protocol" -msgstr "Cổng" - -#: web/ejabberd_web_admin.erl:2429 web/ejabberd_web_admin.erl:2556 -msgid "Module" -msgstr "Môđun" - -#: web/ejabberd_web_admin.erl:2430 web/ejabberd_web_admin.erl:2557 -msgid "Options" -msgstr "Tùy chọn" - -#: web/ejabberd_web_admin.erl:2450 -msgid "Delete" -msgstr "Xóa" - -#: web/ejabberd_web_admin.erl:2579 -msgid "Start" -msgstr "Khởi động" - -#: web/mod_register_web.erl:104 -msgid "Your Jabber account was successfully created." -msgstr "" - -#: web/mod_register_web.erl:107 -msgid "There was an error creating the account: " -msgstr "" - -#: web/mod_register_web.erl:115 -msgid "Your Jabber account was successfully deleted." -msgstr "" - -#: web/mod_register_web.erl:118 -msgid "There was an error deleting the account: " -msgstr "" - -#: web/mod_register_web.erl:128 -msgid "The password of your Jabber account was successfully changed." -msgstr "" - -#: web/mod_register_web.erl:131 -msgid "There was an error changing the password: " -msgstr "" - -#: web/mod_register_web.erl:163 web/mod_register_web.erl:172 -msgid "Jabber Account Registration" -msgstr "" - -#: web/mod_register_web.erl:174 web/mod_register_web.erl:192 -#: web/mod_register_web.erl:201 -msgid "Register a Jabber account" -msgstr "" - -#: web/mod_register_web.erl:176 web/mod_register_web.erl:493 -#: web/mod_register_web.erl:502 -msgid "Unregister a Jabber account" -msgstr "" - -#: web/mod_register_web.erl:203 -msgid "" -"This page allows to create a Jabber account in this Jabber server. Your JID " -"(Jabber IDentifier) will be of the form: username@server. Please read " -"carefully the instructions to fill correctly the fields." -msgstr "" - -#: web/mod_register_web.erl:212 web/mod_register_web.erl:386 -#: web/mod_register_web.erl:509 -#, fuzzy -msgid "Username:" -msgstr "Tên truy cập IRC" - -#: web/mod_register_web.erl:217 -msgid "This is case insensitive: macbeth is the same that MacBeth and Macbeth." -msgstr "" - -#: web/mod_register_web.erl:218 -msgid "Characters not allowed:" -msgstr "" - -#: web/mod_register_web.erl:222 web/mod_register_web.erl:391 -#: web/mod_register_web.erl:514 -#, fuzzy -msgid "Server:" -msgstr "Không bao giờ" - -#: web/mod_register_web.erl:232 -msgid "" -"Don't tell your password to anybody, not even the administrators of the " -"Jabber server." -msgstr "" - -#: web/mod_register_web.erl:234 -msgid "You can later change your password using a Jabber client." -msgstr "" - -#: web/mod_register_web.erl:235 -msgid "" -"Some Jabber clients can store your password in your computer. Use that " -"feature only if you trust your computer is safe." -msgstr "" - -#: web/mod_register_web.erl:237 -msgid "" -"Memorize your password, or write it in a paper placed in a safe place. In " -"Jabber there isn't an automated way to recover your password if you forget " -"it." -msgstr "" - -#: web/mod_register_web.erl:242 web/mod_register_web.erl:406 -#, fuzzy -msgid "Password Verification:" -msgstr "Kiểm Tra Mật Khẩu" - -#: web/mod_register_web.erl:250 -#, fuzzy -msgid "Register" -msgstr "Bảng phân công" - -#: web/mod_register_web.erl:396 -#, fuzzy -msgid "Old Password:" -msgstr "Mật Khẩu:" - -#: web/mod_register_web.erl:401 -#, fuzzy -msgid "New Password:" -msgstr "Mật Khẩu:" - -#: web/mod_register_web.erl:504 -msgid "This page allows to unregister a Jabber account in this Jabber server." -msgstr "" - -#: web/mod_register_web.erl:524 -msgid "Unregister" -msgstr "" - -#~ msgid "Encodings" -#~ msgstr "Mã hóa" - -#~ msgid "(Raw)" -#~ msgstr "(Thô)" - -#~ msgid "Specified nickname is already registered" -#~ msgstr "Bí danh xác định đã đăng ký rồi" - -#~ msgid "Size" -#~ msgstr "Kích thước" - -#~ msgid "Roster groups that may subscribe (if access model is roster)" -#~ msgstr "" -#~ "Các nhóm phân công có thể đăng ký (nếu mô hình truy cập là dạng phân công)" diff --git a/priv/msgs/wa.msg b/priv/msgs/wa.msg index 199a6eaf8..23fcf49f5 100644 --- a/priv/msgs/wa.msg +++ b/priv/msgs/wa.msg @@ -1,125 +1,123 @@ -{"Access Configuration","Apontiaedje des accès"}. -{"Access Control List Configuration","Apontiaedje des droets (ACL)"}. -{"Access control lists","Droets (ACL)"}. -{"Access Control Lists","Droets (ACL)"}. -{"Access denied by service policy","L' accès a stî rfuzé pal politike do siervice"}. -{"Access rules","Rîles d' accès"}. -{"Access Rules","Rîles d' accès"}. -{"Action on user","Accion so l' uzeu"}. -{"Add Jabber ID","Radjouter èn ID Jabber"}. -{"Add New","Radjouter"}. -{"Add User","Radjouter èn uzeu"}. -{"Administration","Manaedjaedje"}. -{"Administration of ","Manaedjaedje di "}. -{"Administrator privileges required","I fåt des priviledjes di manaedjeu"}. +%% Generated automatically +%% DO NOT EDIT: run `make translations` instead +%% To improve translations please read: +%% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ + +{" has set the subject to: "," a candjî l' tite a: "}. {"A friendly name for the node","On no uzeu-ahessåve pol nuk"}. +{"A password is required to enter this room","I fåt dner on scret po poleur intrer dins cisse såle ci"}. +{"Accept","Accepter"}. +{"Access denied by service policy","L' accès a stî rfuzé pal politike do siervice"}. +{"Action on user","Accion so l' uzeu"}. +{"Add User","Radjouter èn uzeu"}. +{"Administration of ","Manaedjaedje di "}. +{"Administration","Manaedjaedje"}. +{"Administrator privileges required","I fåt des priviledjes di manaedjeu"}. {"All activity","Dispoy todi"}. +{"All Users","Tos les uzeus"}. {"Allow this Jabber ID to subscribe to this pubsub node?","Permete ki ci Jabber ID ci si poye abouner a ç' nuk eplaidaedje-abounmint ci?"}. +{"Allow users to change the subject","Les uzeus polèt candjî l' tite"}. {"Allow users to query other users","Les uzeus polèt cweri ls ôtes uzeus"}. {"Allow users to send invites","Les uzeus polèt evoyî priyaedjes"}. {"Allow users to send private messages","Les uzeus polèt evoyî des messaedjes privés"}. {"Allow visitors to change nickname","Permete ki les viziteus candjexhe leus metous nos"}. +{"Allow visitors to send private messages to","Les uzeus polèt evoyî des messaedjes privés"}. {"Allow visitors to send status text in presence updates","Permete ki les viziteus evoyexhe des tecse d' estat dins leus messaedjes di prezince"}. -{"All Users","Tos les uzeus"}. +{"Allow visitors to send voice requests","Les uzeus polèt evoyî des dmandes di vwès"}. {"Announcements","Anonces"}. -{"anyone","tot l' minme kî"}. {"April","avri"}. {"August","awousse"}. -{"Backup","Copeye di såvrité"}. {"Backup Management","Manaedjaedje des copeyes di såvrité"}. -{"Backup of ","Copeye di såvrité po "}. +{"Backup of ~p","Copeye di såvrité po ~p"}. {"Backup to File at ","Fé ene copeye di såvrité dins on fitchî so "}. +{"Backup","Copeye di såvrité"}. {"Bad format","Mwais fôrmat"}. {"Birthday","Date d' askepiaedje"}. +{"CAPTCHA web page","Pådje web CAPTCHA"}. {"Change Password","Candjî l' sicret"}. {"Change User Password","Candjî l' sicret d' l' uzeu"}. +{"Characters not allowed:","Caracteres nén permetous:"}. {"Chatroom configuration modified","L' apontiaedje del såle di berdelaedje a candjî"}. +{"Chatroom is created","Li såle di berdelaedje est ahivêye"}. +{"Chatroom is destroyed","Li såle di berdelaedje est distrûte"}. +{"Chatroom is started","Li såle di berdelaedje est enondêye"}. +{"Chatroom is stopped","Li såle di berdelaedje est ahotêye"}. {"Chatrooms","Såles di berdelaedje"}. {"Choose a username and password to register with this server","Tchoezixhoz on no d' uzeu eyet on scret po vs edjîstrer so ç' sierveu ci"}. -{"Choose modules to stop","Tchoezixhoz les modules a-z arester"}. {"Choose storage type of tables","Tchoezi l' sôre di wårdaedje po les tåves"}. {"Choose whether to approve this entity's subscription.","Tchoezi s' i fåt aprover ou nén l' abounmint di ciste intité."}. {"City","Veye"}. {"Commands","Comandes"}. {"Conference room does not exist","Li såle di conferince n' egzistêye nén"}. +{"Configuration of room ~s","Apontiaedje del såle ~s"}. {"Configuration","Apontiaedjes"}. -{"Connected Resources:","Raloyî avou les rsoûces:"}. {"Country","Payis"}. -{"CPU Time:","Tins CPU:"}. -{"Database","Båze di dnêyes"}. -{"Database Tables at ","Tåves del båze di dnêyes so "}. {"Database Tables Configuration at ","Apontiaedje des tåves del båze di dnêyes so "}. +{"Database","Båze di dnêyes"}. {"December","decimbe"}. {"Default users as participants","Les uzeus sont des pårticipants come prémetowe dujhance"}. -{"Delete","Disfacer"}. -{"Delete message of the day","Disfacer l' messaedje do djoû"}. {"Delete message of the day on all hosts","Disfacer l' messaedje do djoû so tos les lodjoes"}. -{"Delete Selected","Disfacer les elemints tchoezis"}. +{"Delete message of the day","Disfacer l' messaedje do djoû"}. {"Delete User","Disfacer èn uzeu"}. {"Deliver event notifications","Evoyî des notifiaedjes d' evenmints"}. {"Deliver payloads with event notifications","Evoyî l' contnou avou les notifiaedjes d' evenmints"}. -{"Description:","Discrijhaedje:"}. {"Disc only copy","Copeye seulmint sol deure plake"}. -{"Displayed Groups:","Groupes håynés:"}. {"Dump Backup to Text File at ","Copeye di såvritè viè on fitchî tecse so "}. {"Dump to Text File","Schaper en on fitchî tecse"}. {"Edit Properties","Candjî les prôpietés"}. -{"ejabberd IRC module","Module IRC po ejabberd"}. +{"Either approve or decline the voice request.","Aprover oudonbén rifuzer li dmande di vwès."}. {"ejabberd MUC module","Module MUC (såles di berdelaedje) po ejabberd"}. +{"ejabberd Multicast service","siervice multicast d' ejabberd"}. {"ejabberd Publish-Subscribe module","Module d' eplaidaedje-abounmint po ejabberd"}. {"ejabberd SOCKS5 Bytestreams module","Module SOCKS5 Bytestreams po ejabberd"}. {"ejabberd vCard module","Module vCard ejabberd"}. {"ejabberd Web Admin","Manaedjeu waibe ejabberd"}. {"Email","Emile"}. {"Enable logging","Mete en alaedje li djournå"}. +{"Enable message archiving","Mete en alaedje l' årtchivaedje des messaedjes"}. {"End User Session","Fini l' session d' l' uzeu"}. -{"Enter list of {Module, [Options]}","Dinez ene djivêye del cogne {Module, [Tchuzes]}"}. {"Enter nickname you want to register","Dinez l' metou no ki vos vloz edjîstrer"}. {"Enter path to backup file","Dinez l' tchimin viè l' fitchî copeye di såvrité"}. {"Enter path to jabberd14 spool dir","Dinez l' tchimin viè l' ridant di spool jabberd14"}. {"Enter path to jabberd14 spool file","Dinez l' tchimin viè l' fitchî di spool jabberd14"}. {"Enter path to text file","Dinez l' tchimin viè l' fitchî tecse"}. -{"Erlang Jabber Server","Sierveu Jabber Erlang"}. +{"Enter the text you see","Tapez l' tecse ki vos voeyoz"}. +{"Exclude Jabber IDs from CAPTCHA challenge","Esclure les IDs Jabber des kesses CAPTCHA"}. +{"Export all tables as SQL queries to a file:","Espoirter totes les tåves, come des cmandes SQL, viè on fitchî"}. +{"Export data of all users in the server to PIEFXIS files (XEP-0227):","Espoirter les dnêyes di tos les uzeus do sierveu viè des fitchîs PIEFXIS (XEP-0227):"}. +{"Export data of users in a host to PIEFXIS files (XEP-0227):","Espoirter les dnêyes di tos les uzeus do sierveu viè des fitchîs PIEFXIS (XEP-0227):"}. +{"Failed to extract JID from your voice request approval","Nén moyén di rsaetchî on JID foû d' l' aprovaedje di vosse dimande di vwès"}. {"Family Name","No d' famile"}. {"February","fevrî"}. -{"Fill in fields to search for any matching Jabber User","Rimplixhoz les tchamps po cweri èn uzeu Jabber"}. -{"Fill in the form to search for any matching Jabber User (Add * to the end of field to match substring)","Rimplixhoz les tchamps do formulaire po cweri èn uzeu Jabber (radjouter «*» al fén do tchamp po cweri tot l' minme kéne fén d' tchinne"}. {"Friday","vénrdi"}. -{"From","Di"}. -{"From ~s","Dispoy ~s"}. {"Full Name","No etir"}. {"Get Number of Online Users","Riçure li nombe d' uzeus raloyîs"}. {"Get Number of Registered Users","Riçure li nombe d' uzeus edjîstrés"}. {"Get User Last Login Time","Riçure li date/eure do dierin elodjaedje di l' uzeu"}. -{"Get User Password","Riçure sicret d' l' uzeu"}. {"Get User Statistics","Riçure les statistikes di l' uzeu"}. -{"Group ","Groupe "}. -{"Groups","Groupes"}. +{"Grant voice to this person?","Permete li vwès po cisse djin ci?"}. {"has been banned","a stî bani"}. -{"has been kicked","a stî pité evoye"}. -{"has been kicked because of an affiliation change","a stî pité evoye cåze d' on candjmint d' afiyaedje"}. {"has been kicked because of a system shutdown","a stî pité evoye cåze d' èn arestaedje do sistinme"}. +{"has been kicked because of an affiliation change","a stî pité evoye cåze d' on candjmint d' afiyaedje"}. {"has been kicked because the room has been changed to members-only","a stî pité evoye cåze ki l' såle a stî ristrindowe åzès mimbes seulmint"}. -{" has set the subject to: "," a candjî l' tite a: "}. -{"Host","Sierveu"}. +{"has been kicked","a stî pité evoye"}. +{"If you don't see the CAPTCHA image here, visit the web page.","Si vos n' voeyoz nole imådje CAPTCHA chal, vizitez l' pådje waibe."}. {"Import Directory","Sititchî d' on ridant"}. {"Import File","Sititchî d' on fitchî"}. +{"Import user data from jabberd14 spool file:","Sititchî des dnêyes uzeus foû d' on fitchî spoûle jabberd14:"}. {"Import User from File at ","Sititchî uzeu d' on fitchî so "}. +{"Import users data from a PIEFXIS file (XEP-0227):","Sititchî des dnêyes uzeus foû d' on fitchî PIEFXIS (XEP-0227):"}. +{"Import users data from jabberd14 spool directory:","Sititchî des dnêyes uzeus foû d' on ridant spoûle jabberd14:"}. {"Import Users from Dir at ","Sitichî des uzeus d' on ridant so "}. {"Import Users From jabberd14 Spool Files","Sititchî des uzeus Jabberd 1.4"}. {"Improper message type","Sôre di messaedje nén valide"}. {"Incorrect password","Sicret nén corek"}. -{"Invalid affiliation: ~s","Afiyaedje nén valide: ~s"}. -{"Invalid role: ~s","Role nén valide: ~s"}. {"IP addresses","Adresses IP"}. -{"IRC Transport","Transpoirt IRC"}. -{"IRC Username","No d' uzeu IRC"}. {"is now known as","est asteure kinoxhou come"}. -{"It is not allowed to send private messages","Ci n' est nén permetou d' evoyî des messaedjes privés"}. +{"It is not allowed to send error messages to the room. The participant (~s) has sent an error message (~s) and got kicked from the room","On n' pout nén evoyî des messaedjes d' aroke sol såle. Li pårticipan (~s) a-st evoyî on messaedje d' aroke (~s) ey a stî tapé foû."}. {"It is not allowed to send private messages of type \"groupchat\"","C' est nén possibe d' evoyî des messaedjes privés del sôre «groupchat»"}. {"It is not allowed to send private messages to the conference","On n' pout nén evoyî des messaedjes privés dins cisse conferince ci"}. {"Jabber ID","ID Jabber"}. -{"Jabber ID ~s is invalid","Li Jabber ID ~s n' est nén valide"}. {"January","djanvî"}. {"joins the room","arive sol såle"}. {"July","djulete"}. @@ -129,46 +127,41 @@ {"Last month","Dierin moes"}. {"Last year","Dierinne anêye"}. {"leaves the room","cwite li såle"}. -{"Listened Ports at ","Pôrts drovous so "}. -{"Listened Ports","Pôrts drovous"}. -{"List of modules to start","Djivêye di modules a-z enonder"}. -{"Low level update script","Sicripe di metaedje a djoû d' bas livea"}. {"Make participants list public","Rinde publike li djivêye des pårticipants"}. +{"Make room CAPTCHA protected","Rinde li såle di berdelaedje protedjeye pa CAPTCHA"}. {"Make room members-only","Rinde li såle di berdelaedje ristrindowe ås mimbes seulmint"}. {"Make room moderated","Rinde li såle di berdelaedje moderêye"}. {"Make room password protected","Rinde li såle di berdelaedje protedjeye pa scret"}. {"Make room persistent","Rinde li såle permaninte"}. {"Make room public searchable","Rinde li såle di berdelaedje cweråve publicmint"}. {"March","måss"}. -{"Maximum Number of Occupants","Nombe macsimom di prezints"}. -{"Max # of items to persist","Nombe macsimoms di cayets permanints"}. {"Max payload size in bytes","Contnou macsimom en octets"}. +{"Maximum Number of Occupants","Nombe macsimom di prezints"}. {"May","may"}. -{"Members:","Mimbes:"}. -{"Memory","Memwere"}. +{"Membership is required to enter this room","I fåt esse mimbe po poleur intrer dins cisse såle ci"}. {"Message body","Coir do messaedje"}. {"Middle Name","No do mitan"}. +{"Minimum interval between voice requests (in seconds)","Tins minimom etur deus dmandes di vwès (e segondes)"}. {"Moderator privileges required","I fåt des priviledjes di moderateu"}. -{"moderators only","les moderateus seulmint"}. -{"Module","Module"}. -{"Modules at ","Modules so "}. -{"Modules","Modules"}. +{"Moderator","Moderateu"}. {"Monday","londi"}. +{"Multicast","Multicast"}. +{"Multi-User Chat","Berdelaedje a sacwants"}. {"Name","No"}. -{"Name:","Pitit no:"}. {"Never","Måy"}. -{"Nickname","Metou no"}. +{"New Password:","Novea scret:"}. {"Nickname Registration at ","Edjîstraedje di metou no amon "}. {"Nickname ~s does not exist in the room","Li metou no ~s n' egzistêye nén dins l' såle"}. +{"Nickname","Metou no"}. {"No body provided for announce message","I n' a nou coir do messaedje po ciste anonce la"}. {"No Data","Nole dinêye disponibe"}. +{"No limit","Pont d' limite"}. {"Node ID","ID d' nuk"}. {"Node not found","Nuk nén trové"}. -{"Node ","Nuk "}. +{"Node ~p","Nuk ~p"}. {"Nodes","Nuks"}. -{"No limit","Pont d' limite"}. {"None","Nole"}. -{"No resource provided","Nole rissoûce di dnêye"}. +{"Not Found","Nén trové"}. {"Notify subscribers when items are removed from the node","Notifyî åzès abounés cwand des cayets sont oisté foû do nuk"}. {"Notify subscribers when the node configuration changes","Notifyî åzès abounés cwand l' apontiaedje do nuk candje"}. {"Notify subscribers when the node is deleted","Notifyî åzès abounés cwand l' nuk est disfacé"}. @@ -177,147 +170,134 @@ {"Number of online users","Nombe d' uzeus raloyîs"}. {"Number of registered users","Nombe d' uzeus edjîstrés"}. {"October","octôbe"}. -{"Offline Messages:","Messaedjes ki ratindèt:"}. -{"Offline Messages","Messaedjes ki ratindèt"}. {"OK","'l est bon"}. -{"Online","Raloyî"}. -{"Online Users:","Uzeus raloyîs:"}. +{"Old Password:","Vî scret:"}. {"Online Users","Uzeus raloyîs"}. +{"Online","Raloyî"}. {"Only deliver notifications to available users","Seulmint evoyî des notifiaedje åzès uzeus disponibes"}. +{"Only members may query archives of this room","Seulmint les mimbes polèt cweri les årtchives dins cisse såle ci"}. +{"Only moderators and participants are allowed to change the subject in this room","Seulmint les moderateus et les pårticipants polèt candjî l' sudjet dins cisse såle ci"}. +{"Only moderators are allowed to change the subject in this room","Seulmint les moderateus polèt candjî l' sudjet dins cisse såle ci"}. +{"Only moderators can approve voice requests","Seulmint les moderateus polèt aprover des dmandes di vwès"}. {"Only occupants are allowed to send messages to the conference","Seulmint les prezints polèt evoyî des messaedjes al conferince"}. {"Only occupants are allowed to send queries to the conference","Seulmint les prezints polèt evoyî des cweraedjes sol conferince"}. {"Only service administrators are allowed to send service messages","Seulmint les manaedjeus d' siervices polèt evoyî des messaedjes di siervice"}. -{"Options","Tchuzes"}. {"Organization Name","No d' l' organizåcion"}. {"Organization Unit","Unité d' l' organizåcion"}. -{"Outgoing s2s Connections:","Raloyaedjes s2s e rexhowe:"}. {"Outgoing s2s Connections","Raloyaedjes s2s e rexhowe"}. -{"Outgoing s2s Servers:","Sierveus s2s e rexhowe:"}. {"Owner privileges required","I fåt des priviledjes di prôpietaire"}. -{"Packet","Paket"}. -{"Password:","Sicret:"}. -{"Password","Sicret"}. +{"Participant","Pårticipant"}. {"Password Verification","Acertinaedje do scret"}. +{"Password Verification:","Acertinaedje do scret:"}. +{"Password","Sicret"}. +{"Password:","Sicret:"}. {"Path to Dir","Tchimin viè l' ridant"}. {"Path to File","Tchimin viè l' fitchî"}. -{"Pending","Ratindant"}. {"Period: ","Termene:"}. {"Persist items to storage","Cayets permanints a wårder"}. {"Ping","Ping"}. +{"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Notez ki ces tchuzes la vont seulmint fé ene copeye di såvrité del båze di dnêyes Mnesia costrûte å dvins do programe. Si vos eployîz ene difoûtrinne båze di dnêyes avou l' module ODBC, vos dvoz fé ene copeye di såvrité del båze SQL da vosse sepårumint."}. +{"Please, wait for a while before sending new voice request","Ratindez ene miete s' i vs plait divant d' rivoyî ene nouve dimande di vwès"}. {"Pong","Pong"}. -{"Port","Pôrt"}. {"Present real Jabber IDs to","Mostrer les vraiys Jabber IDs a"}. {"private, ","privé, "}. {"Publish-Subscribe","Eplaidaedje-abounmint"}. {"PubSub subscriber request","Dimande d' eplaidaedje-abounmint d' èn abouné"}. +{"Purge all items when the relevant publisher goes offline","Purdjî tos les cayets cwand l' eplaideu aloyî va foû raloyaedje"}. {"Queries to the conference members are not allowed in this room","Les cweraedjes des mimbes del conferince ni sont nén permetous dins cisse såle ci"}. {"RAM and disc copy","Copeye e memwere (RAM) et sol deure plake"}. {"RAM copy","Copeye e memwere (RAM)"}. -{"Raw","Dinêyes brutes"}. {"Really delete message of the day?","Voloz vs vormint disfacer l' messaedje do djoû?"}. {"Recipient is not in the conference room","Li riçuveu n' est nén dins l' såle di conferince"}. -{"Registered Users:","Uzeus edjistrés:"}. -{"Registered Users","Uzeus edjistrés"}. -{"Registration in mod_irc for ","Edjîstraedje dins mod_irc po "}. +{"Register","Edjîstrer"}. {"Remote copy","Copeye å lon"}. -{"Remove","Oister"}. {"Remove User","Disfacer l' uzeu"}. {"Replaced by new connection","Replaecî pa on novea raloyaedje"}. {"Resources","Rissoûces"}. -{"Restart","Renonder"}. {"Restart Service","Renonder siervice"}. {"Restore Backup from File at ","Rapexhî dispoy li fitchî copeye di såvrité so "}. {"Restore binary backup after next ejabberd restart (requires less memory):","Rapexhî l' copeye di såvrité binaire après l' renondaedje ki vént d' ejabberd (çoula prind moens d' memwere del fé insi):"}. {"Restore binary backup immediately:","Rapexhî do côp foû d' ene copeye di såvrité binaire:"}. {"Restore plain text backup immediately:","Rapexhî do côp foû d' ene copeye di såvrité tecse:"}. {"Restore","Rapexhî"}. +{"Roles for which Presence is Broadcasted","Roles ki leu prezince est difuzêye"}. {"Room Configuration","Apontiaedje del såle"}. {"Room creation is denied by service policy","L' ahivaedje del såle est rfuzé pal politike do siervice"}. +{"Room description","Discrijhaedje del såle"}. +{"Room Occupants","Prezints el såle"}. {"Room title","Tite del såle"}. -{"Roster","Djivêye des soçons"}. {"Roster groups allowed to subscribe","Pårtaedjîs groupes di soçons k' on s' î pout abouner"}. -{"Roster of ","Djivêye des soçons da "}. {"Roster size","Grandeu del djivêye des soçons"}. -{"RPC Call Error","Aroke di houcaedje RPC"}. {"Running Nodes","Nuks en alaedje"}. -{"~s access rule configuration","Apontiaedje des rîles d' accès a ~s"}. {"Saturday","semdi"}. -{"Script check","Acertinaedje do scripe"}. {"Search Results for ","Rizultats do cweraedje po "}. {"Search users in ","Cweri des uzeus dins "}. -{"Send announcement to all online users","Evoyî l' anonce a tos les uzeus raloyîs"}. {"Send announcement to all online users on all hosts","Evoyî l' anonce a tos les uzeus raloyîs so tos les lodjoes"}. -{"Send announcement to all users","Evoyî l' anonce a tos les uzeus"}. +{"Send announcement to all online users","Evoyî l' anonce a tos les uzeus raloyîs"}. {"Send announcement to all users on all hosts","Evoyî l' anonce a tos les uzeus so tos les lodjoes"}. +{"Send announcement to all users","Evoyî l' anonce a tos les uzeus"}. {"September","setimbe"}. +{"Server:","Sierveu:"}. {"Set message of the day and send to online users","Defini l' messaedje do djoû et l' evoyî åzès uzeus raloyîs"}. {"Set message of the day on all hosts and send to online users","Defini l' messaedje do djoû so tos les lodjoes et l' evoyî åzès uzeus raloyîs"}. {"Shared Roster Groups","Pårtaedjîs groupes ezès djivêyes di soçons"}. {"Show Integral Table","Mostrer totå"}. {"Show Ordinary Table","Mostrer crexhince"}. {"Shut Down Service","Arester siervice"}. -{"~s invites you to the room ~s","~s vos preye sol såle ~s"}. {"Specify the access model","Sipecifyî l' modele d' accès"}. +{"Specify the event message type","Sipecifyî l' sôre do messaedje d' evenmint"}. {"Specify the publisher model","Dinez l' modele d' eplaideu"}. -{"~s's Offline Messages Queue","messaedjes ki ratindèt el cawêye po ~s"}. -{"Start","Enonder"}. -{"Start Modules at ","Renonder les modules so "}. -{"Start Modules","Enonder des modules"}. -{"Statistics of ~p","Sitatistikes di ~p"}. -{"Statistics","Sitatistikes"}. -{"Stop","Arester"}. -{"Stop Modules","Arester des modules"}. -{"Stop Modules at ","Arester les modules so "}. {"Stopped Nodes","Nuks essoctés"}. -{"Storage Type","Sôre di wårdaedje"}. {"Store binary backup:","Copeye di såvrité binaire:"}. {"Store plain text backup:","Copeye di såvrité tecse:"}. {"Subject","Sudjet"}. -{"Submit","Evoyî"}. {"Submitted","Candjmints evoyîs"}. {"Subscriber Address","Adresse di l' abouné"}. -{"Subscription","Abounmimnt"}. {"Sunday","dimegne"}. +{"That nickname is already in use by another occupant","Li metou no est ddja eployî pa ene ôte sakî sol såle"}. +{"That nickname is registered by another person","Li metou no est ddja edjîstré pa ene ôte sakî"}. +{"The CAPTCHA is valid.","Li CAPTCHA est valide."}. +{"The CAPTCHA verification has failed","Li verifiaedje CAPTCHA a fwait berwete"}. +{"The collections with which a node is affiliated","Les ramexhnêyes k' on nuk est afiyî avou"}. +{"The password is too weak","li scret est trop flåw"}. {"the password is","li scret est"}. -{"This participant is kicked from the room because he sent an error message","Ci pårticipant ci a stî pité evoye del såle cåze k' il a-st evoyî on messaedje d' aroke"}. -{"This participant is kicked from the room because he sent an error message to another participant","Ci pårticipant ci a stî pité evoye del såle cåze k' il a-st evoyî on messaedje d' aroke a èn ôte pårticipant"}. -{"This participant is kicked from the room because he sent an error presence","Ci pårticipant ci a stî pité evoye del såle cåze k' il a-st evoyî ene aroke di prezince"}. +{"There was an error creating the account: ","Åk n' a nén stî tot ahivant l' conte: "}. +{"There was an error deleting the account: ","Åk n' a nén stî tot disfaçant l' conte: "}. {"This room is not anonymous","Cisse såle ci n' est nén anonime"}. {"Thursday","djudi"}. -{"Time","Date"}. {"Time delay","Tårdjaedje"}. -{"To","Po"}. -{"To ~s","Viè ~s"}. +{"Too many CAPTCHA requests","Pår trop di dmandes CAPTCHA"}. +{"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","I gn a-st avou pår trop (~p) d' otintifiaedjes k' ont fwait berwete vinant di ciste adresse IP la (~s). L' adresse serè disblokêye a ~s UTC"}. +{"Too many unacked stanzas","Pår trop di messaedjes sins acertinaedje di rçuvaedje"}. {"Traffic rate limit is exceeded","Li limite pol volume di trafik a stî passêye"}. -{"Transactions Aborted:","Transaccions arestêyes:"}. -{"Transactions Committed:","Transaccions evoyeyes:"}. -{"Transactions Logged:","Transaccions wårdêyes e djournå:"}. -{"Transactions Restarted:","Transaccions renondêyes:"}. {"Tuesday","mårdi"}. +{"Unable to generate a CAPTCHA","Nén moyén di djenerer on CAPTCHA"}. +{"Unauthorized","Nén otorijhî"}. +{"Unregister","Disdjîstrer"}. {"Update message of the day (don't send)","Mete a djoû l' messaedje do djoû (nén l' evoyî)"}. {"Update message of the day on all hosts (don't send)","Mete a djoû l' messaedje do djoû so tos les lodjoes (nén l' evoyî)"}. -{"Update ","Metaedje a djoû "}. -{"Update","Mete a djoû"}. -{"Update plan","Plan d' metaedje a djoû"}. -{"Update script","Sicripe di metaedje a djoû"}. -{"Uptime:","Tins dispoy l' enondaedje:"}. -{"Use of STARTTLS required","L' eployaedje di STARTTL est oblidjî"}. +{"User JID","JID d' l' uzeu"}. {"User Management","Manaedjaedje des uzeus"}. +{"Username:","No d' uzeu:"}. +{"Users are not allowed to register accounts so quickly","Les noveas uzeus n' si polèt nén edjîstrer si raddimint"}. {"Users Last Activity","Dierinne activité des uzeus"}. {"Users","Uzeus"}. -{"User ","Uzeu "}. {"User","Uzeu"}. -{"Validate","Valider"}. {"vCard User Search","Calpin des uzeus"}. {"Virtual Hosts","Forveyous sierveus"}. {"Visitors are not allowed to change their nicknames in this room","Les viziteus èn polèt nén candjî leus metous no po ç' såle ci"}. {"Visitors are not allowed to send messages to all occupants","Les viziteus n' polèt nén evoyî des messaedjes a tos les prezints"}. +{"Visitor","Viziteu"}. +{"Voice request","Dimande di vwès"}. +{"Voice requests are disabled in this conference","Les dmandes di vwès sont dismetowes e cisse conferince ci"}. {"Wednesday","mierkidi"}. {"When to send the last published item","Cwand evoyî l' dierin cayet eplaidî"}. {"Whether to allow subscriptions","Si on permete les abounmints"}. {"You have been banned from this room","Vos avoz stî bani di cisse såle ci"}. {"You must fill in field \"Nickname\" in the form","Vos dvoz rimpli l' tchamp «Metou no» dins l' formiulaire"}. -{"You need an x:data capable client to configure mod_irc settings","Vos avoz mezåjhe d' on cliyint ki sopoite x:data po candjî ls apontiaedjes di mod_irc"}. -{"You need an x:data capable client to configure room","I vs fåt on cliyint ki sopoite x:data por vos poleur apontyî l' såle"}. +{"You need a client that supports x:data and CAPTCHA to register","Vos avoz mezåjhe d' on cliyint ki sopoite x:data eyet CAPTCHA po vs edjîstrer"}. +{"You need a client that supports x:data to register the nickname","Vos avoz mezåjhe d' on cliyint ki sopoite x:data po-z edjîstrer l' metou no"}. {"You need an x:data capable client to search","Vos avoz mezåjhe d' on cliyint ki sopoite x:data po fé on cweraedje"}. +{"Your active privacy list has denied the routing of this stanza.","Vosse djivêye di privaceye active a rfuzé l' evoyaedje di ç' messaedje ci."}. {"Your contact offline message queue is full. The message has been discarded.","Li cawêye di messaedjes e môde disraloyî di vosse soçon est plinne. Li messaedje a stî tapé å diale."}. +{"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","Vos messaedjes po ~s sont blokés. Po les disbloker, alez vey ~s"}. diff --git a/priv/msgs/wa.po b/priv/msgs/wa.po deleted file mode 100644 index 462b22445..000000000 --- a/priv/msgs/wa.po +++ /dev/null @@ -1,1900 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: 2.1.0-alpha\n" -"Last-Translator: Pablo Saratxaga\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Language: Walon (Walloon)\n" - -#: ejabberd_c2s.erl:424 ejabberd_c2s.erl:727 -msgid "Use of STARTTLS required" -msgstr "L' eployaedje di STARTTL est oblidjî" - -#: ejabberd_c2s.erl:503 -msgid "No resource provided" -msgstr "Nole rissoûce di dnêye" - -#: ejabberd_c2s.erl:1197 -msgid "Replaced by new connection" -msgstr "Replaecî pa on novea raloyaedje" - -#: ejabberd_c2s.erl:1885 -msgid "Your active privacy list has denied the routing of this stanza." -msgstr "" - -#: ejabberd_captcha.erl:96 ejabberd_captcha.erl:152 ejabberd_captcha.erl:178 -#, fuzzy -msgid "Enter the text you see" -msgstr "Dinez l' tchimin viè l' fitchî tecse" - -#: ejabberd_captcha.erl:101 -msgid "Your messages to ~s are being blocked. To unblock them, visit ~s" -msgstr "" - -#: ejabberd_captcha.erl:134 -msgid "If you don't see the CAPTCHA image here, visit the web page." -msgstr "" - -#: ejabberd_captcha.erl:146 -msgid "CAPTCHA web page" -msgstr "" - -#: ejabberd_captcha.erl:307 -msgid "The CAPTCHA is valid." -msgstr "" - -#: mod_adhoc.erl:95 mod_adhoc.erl:125 mod_adhoc.erl:143 mod_adhoc.erl:161 -msgid "Commands" -msgstr "Comandes" - -#: mod_adhoc.erl:149 mod_adhoc.erl:243 -msgid "Ping" -msgstr "Ping" - -#: mod_adhoc.erl:260 -msgid "Pong" -msgstr "Pong" - -#: mod_announce.erl:507 mod_announce_odbc.erl:499 -msgid "Really delete message of the day?" -msgstr "Voloz vs vormint disfacer l' messaedje do djoû?" - -#: mod_announce.erl:515 mod_announce_odbc.erl:507 mod_configure.erl:1083 -#: mod_configure.erl:1128 -msgid "Subject" -msgstr "Sudjet" - -#: mod_announce.erl:520 mod_announce_odbc.erl:512 mod_configure.erl:1088 -#: mod_configure.erl:1133 -msgid "Message body" -msgstr "Coir do messaedje" - -#: mod_announce.erl:600 mod_announce_odbc.erl:592 -msgid "No body provided for announce message" -msgstr "I n' a nou coir do messaedje po ciste anonce la" - -#: mod_announce.erl:635 mod_announce_odbc.erl:627 -msgid "Announcements" -msgstr "Anonces" - -#: mod_announce.erl:637 mod_announce_odbc.erl:629 -msgid "Send announcement to all users" -msgstr "Evoyî l' anonce a tos les uzeus" - -#: mod_announce.erl:639 mod_announce_odbc.erl:631 -msgid "Send announcement to all users on all hosts" -msgstr "Evoyî l' anonce a tos les uzeus so tos les lodjoes" - -#: mod_announce.erl:641 mod_announce_odbc.erl:633 -msgid "Send announcement to all online users" -msgstr "Evoyî l' anonce a tos les uzeus raloyîs" - -#: mod_announce.erl:643 mod_announce_odbc.erl:635 mod_configure.erl:1078 -#: mod_configure.erl:1123 -msgid "Send announcement to all online users on all hosts" -msgstr "Evoyî l' anonce a tos les uzeus raloyîs so tos les lodjoes" - -#: mod_announce.erl:645 mod_announce_odbc.erl:637 -msgid "Set message of the day and send to online users" -msgstr "Defini l' messaedje do djoû et l' evoyî åzès uzeus raloyîs" - -#: mod_announce.erl:647 mod_announce_odbc.erl:639 -msgid "Set message of the day on all hosts and send to online users" -msgstr "" -"Defini l' messaedje do djoû so tos les lodjoes et l' evoyî åzès uzeus raloyîs" - -#: mod_announce.erl:649 mod_announce_odbc.erl:641 -msgid "Update message of the day (don't send)" -msgstr "Mete a djoû l' messaedje do djoû (nén l' evoyî)" - -#: mod_announce.erl:651 mod_announce_odbc.erl:643 -msgid "Update message of the day on all hosts (don't send)" -msgstr "Mete a djoû l' messaedje do djoû so tos les lodjoes (nén l' evoyî)" - -#: mod_announce.erl:653 mod_announce_odbc.erl:645 -msgid "Delete message of the day" -msgstr "Disfacer l' messaedje do djoû" - -#: mod_announce.erl:655 mod_announce_odbc.erl:647 -msgid "Delete message of the day on all hosts" -msgstr "Disfacer l' messaedje do djoû so tos les lodjoes" - -#: mod_configure.erl:114 mod_configure.erl:274 mod_configure.erl:296 -#: mod_configure.erl:498 -msgid "Configuration" -msgstr "Apontiaedjes" - -#: mod_configure.erl:125 mod_configure.erl:576 web/ejabberd_web_admin.erl:1936 -msgid "Database" -msgstr "Båze di dnêyes" - -#: mod_configure.erl:127 mod_configure.erl:595 -msgid "Start Modules" -msgstr "Enonder des modules" - -#: mod_configure.erl:129 mod_configure.erl:596 -msgid "Stop Modules" -msgstr "Arester des modules" - -#: mod_configure.erl:131 mod_configure.erl:604 web/ejabberd_web_admin.erl:1937 -msgid "Backup" -msgstr "Copeye di såvrité" - -#: mod_configure.erl:133 mod_configure.erl:605 -msgid "Restore" -msgstr "Rapexhî" - -#: mod_configure.erl:135 mod_configure.erl:606 -msgid "Dump to Text File" -msgstr "Schaper en on fitchî tecse" - -#: mod_configure.erl:137 mod_configure.erl:615 -msgid "Import File" -msgstr "Sititchî d' on fitchî" - -#: mod_configure.erl:139 mod_configure.erl:616 -msgid "Import Directory" -msgstr "Sititchî d' on ridant" - -#: mod_configure.erl:141 mod_configure.erl:581 mod_configure.erl:1057 -msgid "Restart Service" -msgstr "Renonder siervice" - -#: mod_configure.erl:143 mod_configure.erl:582 mod_configure.erl:1102 -msgid "Shut Down Service" -msgstr "Arester siervice" - -#: mod_configure.erl:145 mod_configure.erl:518 mod_configure.erl:1197 -#: web/ejabberd_web_admin.erl:1527 -msgid "Add User" -msgstr "Radjouter èn uzeu" - -#: mod_configure.erl:147 mod_configure.erl:519 mod_configure.erl:1219 -msgid "Delete User" -msgstr "Disfacer èn uzeu" - -#: mod_configure.erl:149 mod_configure.erl:520 mod_configure.erl:1231 -msgid "End User Session" -msgstr "Fini l' session d' l' uzeu" - -#: mod_configure.erl:151 mod_configure.erl:521 mod_configure.erl:1243 -#: mod_configure.erl:1255 -msgid "Get User Password" -msgstr "Riçure sicret d' l' uzeu" - -#: mod_configure.erl:153 mod_configure.erl:522 -msgid "Change User Password" -msgstr "Candjî l' sicret d' l' uzeu" - -#: mod_configure.erl:155 mod_configure.erl:523 mod_configure.erl:1272 -msgid "Get User Last Login Time" -msgstr "Riçure li date/eure do dierin elodjaedje di l' uzeu" - -#: mod_configure.erl:157 mod_configure.erl:524 mod_configure.erl:1284 -msgid "Get User Statistics" -msgstr "Riçure les statistikes di l' uzeu" - -#: mod_configure.erl:159 mod_configure.erl:525 -msgid "Get Number of Registered Users" -msgstr "Riçure li nombe d' uzeus edjîstrés" - -#: mod_configure.erl:161 mod_configure.erl:526 -msgid "Get Number of Online Users" -msgstr "Riçure li nombe d' uzeus raloyîs" - -#: mod_configure.erl:163 mod_configure.erl:509 web/ejabberd_web_admin.erl:831 -#: web/ejabberd_web_admin.erl:872 -msgid "Access Control Lists" -msgstr "Droets (ACL)" - -#: mod_configure.erl:165 mod_configure.erl:510 web/ejabberd_web_admin.erl:940 -#: web/ejabberd_web_admin.erl:976 -msgid "Access Rules" -msgstr "Rîles d' accès" - -#: mod_configure.erl:297 mod_configure.erl:499 -msgid "User Management" -msgstr "Manaedjaedje des uzeus" - -#: mod_configure.erl:500 web/ejabberd_web_admin.erl:1058 -#: web/ejabberd_web_admin.erl:1462 -msgid "Online Users" -msgstr "Uzeus raloyîs" - -#: mod_configure.erl:501 -msgid "All Users" -msgstr "Tos les uzeus" - -#: mod_configure.erl:502 -msgid "Outgoing s2s Connections" -msgstr "Raloyaedjes s2s e rexhowe" - -#: mod_configure.erl:503 web/ejabberd_web_admin.erl:1907 -msgid "Running Nodes" -msgstr "Nuks en alaedje" - -#: mod_configure.erl:504 web/ejabberd_web_admin.erl:1909 -msgid "Stopped Nodes" -msgstr "Nuks essoctés" - -#: mod_configure.erl:577 mod_configure.erl:587 web/ejabberd_web_admin.erl:1953 -msgid "Modules" -msgstr "Modules" - -#: mod_configure.erl:578 -msgid "Backup Management" -msgstr "Manaedjaedje des copeyes di såvrité" - -#: mod_configure.erl:579 -msgid "Import Users From jabberd14 Spool Files" -msgstr "Sititchî des uzeus Jabberd 1.4" - -#: mod_configure.erl:699 -msgid "To ~s" -msgstr "Viè ~s" - -#: mod_configure.erl:717 -msgid "From ~s" -msgstr "Dispoy ~s" - -#: mod_configure.erl:913 -msgid "Database Tables Configuration at " -msgstr "Apontiaedje des tåves del båze di dnêyes so " - -#: mod_configure.erl:918 -msgid "Choose storage type of tables" -msgstr "Tchoezi l' sôre di wårdaedje po les tåves" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Disc only copy" -msgstr "Copeye seulmint sol deure plake" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM and disc copy" -msgstr "Copeye e memwere (RAM) et sol deure plake" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM copy" -msgstr "Copeye e memwere (RAM)" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Remote copy" -msgstr "Copeye å lon" - -#: mod_configure.erl:950 -msgid "Stop Modules at " -msgstr "Arester les modules so " - -#: mod_configure.erl:954 -msgid "Choose modules to stop" -msgstr "Tchoezixhoz les modules a-z arester" - -#: mod_configure.erl:969 -msgid "Start Modules at " -msgstr "Renonder les modules so " - -#: mod_configure.erl:973 -msgid "Enter list of {Module, [Options]}" -msgstr "Dinez ene djivêye del cogne {Module, [Tchuzes]}" - -#: mod_configure.erl:974 -msgid "List of modules to start" -msgstr "Djivêye di modules a-z enonder" - -#: mod_configure.erl:983 -msgid "Backup to File at " -msgstr "Fé ene copeye di såvrité dins on fitchî so " - -#: mod_configure.erl:987 mod_configure.erl:1001 -msgid "Enter path to backup file" -msgstr "Dinez l' tchimin viè l' fitchî copeye di såvrité" - -#: mod_configure.erl:988 mod_configure.erl:1002 mod_configure.erl:1016 -#: mod_configure.erl:1030 -msgid "Path to File" -msgstr "Tchimin viè l' fitchî" - -#: mod_configure.erl:997 -msgid "Restore Backup from File at " -msgstr "Rapexhî dispoy li fitchî copeye di såvrité so " - -#: mod_configure.erl:1011 -msgid "Dump Backup to Text File at " -msgstr "Copeye di såvritè viè on fitchî tecse so " - -#: mod_configure.erl:1015 -msgid "Enter path to text file" -msgstr "Dinez l' tchimin viè l' fitchî tecse" - -#: mod_configure.erl:1025 -msgid "Import User from File at " -msgstr "Sititchî uzeu d' on fitchî so " - -#: mod_configure.erl:1029 -msgid "Enter path to jabberd14 spool file" -msgstr "Dinez l' tchimin viè l' fitchî di spool jabberd14" - -#: mod_configure.erl:1039 -msgid "Import Users from Dir at " -msgstr "Sitichî des uzeus d' on ridant so " - -#: mod_configure.erl:1043 -msgid "Enter path to jabberd14 spool dir" -msgstr "Dinez l' tchimin viè l' ridant di spool jabberd14" - -#: mod_configure.erl:1044 -msgid "Path to Dir" -msgstr "Tchimin viè l' ridant" - -#: mod_configure.erl:1060 mod_configure.erl:1105 -msgid "Time delay" -msgstr "Tårdjaedje" - -#: mod_configure.erl:1143 -msgid "Access Control List Configuration" -msgstr "Apontiaedje des droets (ACL)" - -#: mod_configure.erl:1147 -msgid "Access control lists" -msgstr "Droets (ACL)" - -#: mod_configure.erl:1171 -msgid "Access Configuration" -msgstr "Apontiaedje des accès" - -#: mod_configure.erl:1175 -msgid "Access rules" -msgstr "Rîles d' accès" - -#: mod_configure.erl:1200 mod_configure.erl:1222 mod_configure.erl:1234 -#: mod_configure.erl:1246 mod_configure.erl:1258 mod_configure.erl:1275 -#: mod_configure.erl:1287 mod_configure.erl:1650 mod_configure.erl:1700 -#: mod_configure.erl:1721 mod_roster.erl:943 mod_roster_odbc.erl:1060 -#: mod_vcard.erl:472 mod_vcard_ldap.erl:554 mod_vcard_odbc.erl:448 -msgid "Jabber ID" -msgstr "ID Jabber" - -#: mod_configure.erl:1205 mod_configure.erl:1263 mod_configure.erl:1651 -#: mod_configure.erl:1863 mod_muc/mod_muc_room.erl:3224 mod_register.erl:235 -#: web/ejabberd_web_admin.erl:1520 -msgid "Password" -msgstr "Sicret" - -#: mod_configure.erl:1210 -msgid "Password Verification" -msgstr "Acertinaedje do scret" - -#: mod_configure.erl:1301 -msgid "Number of registered users" -msgstr "Nombe d' uzeus edjîstrés" - -#: mod_configure.erl:1315 -msgid "Number of online users" -msgstr "Nombe d' uzeus raloyîs" - -#: mod_configure.erl:1682 web/ejabberd_web_admin.erl:1588 -#: web/ejabberd_web_admin.erl:1743 -msgid "Never" -msgstr "Måy" - -#: mod_configure.erl:1696 web/ejabberd_web_admin.erl:1601 -#: web/ejabberd_web_admin.erl:1756 -msgid "Online" -msgstr "Raloyî" - -#: mod_configure.erl:1701 -msgid "Last login" -msgstr "Dierin elodjaedje" - -#: mod_configure.erl:1722 -msgid "Roster size" -msgstr "Grandeu del djivêye des soçons" - -#: mod_configure.erl:1723 -msgid "IP addresses" -msgstr "Adresses IP" - -#: mod_configure.erl:1724 -msgid "Resources" -msgstr "Rissoûces" - -#: mod_configure.erl:1850 -msgid "Administration of " -msgstr "Manaedjaedje di " - -#: mod_configure.erl:1853 -msgid "Action on user" -msgstr "Accion so l' uzeu" - -#: mod_configure.erl:1857 -msgid "Edit Properties" -msgstr "Candjî les prôpietés" - -#: mod_configure.erl:1860 web/ejabberd_web_admin.erl:1769 -msgid "Remove User" -msgstr "Disfacer l' uzeu" - -#: mod_irc/mod_irc.erl:201 mod_irc/mod_irc_odbc.erl:196 -#: mod_muc/mod_muc.erl:336 mod_muc/mod_muc_odbc.erl:342 -msgid "Access denied by service policy" -msgstr "L' accès a stî rfuzé pal politike do siervice" - -#: mod_irc/mod_irc.erl:401 mod_irc/mod_irc_odbc.erl:398 -msgid "IRC Transport" -msgstr "Transpoirt IRC" - -#: mod_irc/mod_irc.erl:428 mod_irc/mod_irc_odbc.erl:425 -msgid "ejabberd IRC module" -msgstr "Module IRC po ejabberd" - -#: mod_irc/mod_irc.erl:559 mod_irc/mod_irc_odbc.erl:568 -msgid "You need an x:data capable client to configure mod_irc settings" -msgstr "" -"Vos avoz mezåjhe d' on cliyint ki sopoite x:data po candjî ls apontiaedjes " -"di mod_irc" - -#: mod_irc/mod_irc.erl:566 mod_irc/mod_irc_odbc.erl:575 -msgid "Registration in mod_irc for " -msgstr "Edjîstraedje dins mod_irc po " - -#: mod_irc/mod_irc.erl:571 mod_irc/mod_irc_odbc.erl:580 -#, fuzzy -msgid "" -"Enter username, encodings, ports and passwords you wish to use for " -"connecting to IRC servers" -msgstr "" -"Dinez les nos d' uzeu et ls ecôdaedjes ki vos vloz eployî po vs raloyî åzès " -"sierveus IRC" - -#: mod_irc/mod_irc.erl:576 mod_irc/mod_irc_odbc.erl:585 -msgid "IRC Username" -msgstr "No d' uzeu IRC" - -#: mod_irc/mod_irc.erl:586 mod_irc/mod_irc_odbc.erl:595 -#, fuzzy -msgid "" -"If you want to specify different ports, passwords, encodings for IRC " -"servers, fill this list with values in format '{\"irc server\", \"encoding" -"\", port, \"password\"}'. By default this service use \"~s\" encoding, port " -"~p, empty password." -msgstr "" -"Si vos vloz dner des ecôdaedjes diferins po les sierveus IRC, rimplixhoz " -"cisse djivêye ci avou des valixhances del cogne «{\"sierveu irc\", " -"\"ecôdaedje\"}». Li prémetou ecôdaedje do siervice c' est «~s»." - -#: mod_irc/mod_irc.erl:598 mod_irc/mod_irc_odbc.erl:607 -#, fuzzy -msgid "" -"Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef." -"net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]." -msgstr "" -"Egzimpe: [{\"irc.lucky.net\", \"koi8-r\"}, {\"vendetta.fef.net\", " -"\"iso8859-1\"}]." - -#: mod_irc/mod_irc.erl:603 mod_irc/mod_irc_odbc.erl:612 -msgid "Connections parameters" -msgstr "" - -#: mod_irc/mod_irc.erl:728 mod_irc/mod_irc_odbc.erl:757 -msgid "Join IRC channel" -msgstr "" - -#: mod_irc/mod_irc.erl:732 mod_irc/mod_irc_odbc.erl:761 -msgid "IRC channel (don't put the first #)" -msgstr "" - -#: mod_irc/mod_irc.erl:737 mod_irc/mod_irc_odbc.erl:766 -#, fuzzy -msgid "IRC server" -msgstr "No d' uzeu IRC" - -#: mod_irc/mod_irc.erl:770 mod_irc/mod_irc.erl:774 -#: mod_irc/mod_irc_odbc.erl:799 mod_irc/mod_irc_odbc.erl:803 -msgid "Join the IRC channel here." -msgstr "" - -#: mod_irc/mod_irc.erl:778 mod_irc/mod_irc_odbc.erl:807 -msgid "Join the IRC channel in this Jabber ID: ~s" -msgstr "" - -#: mod_irc/mod_irc.erl:863 mod_irc/mod_irc_odbc.erl:904 -msgid "IRC settings" -msgstr "" - -#: mod_irc/mod_irc.erl:868 mod_irc/mod_irc_odbc.erl:909 -#, fuzzy -msgid "" -"Enter username and encodings you wish to use for connecting to IRC servers. " -"Press 'Next' to get more fields to fill in. Press 'Complete' to save " -"settings." -msgstr "" -"Dinez les nos d' uzeu et ls ecôdaedjes ki vos vloz eployî po vs raloyî åzès " -"sierveus IRC" - -#: mod_irc/mod_irc.erl:874 mod_irc/mod_irc_odbc.erl:915 -#, fuzzy -msgid "IRC username" -msgstr "No d' uzeu IRC" - -#: mod_irc/mod_irc.erl:923 mod_irc/mod_irc_odbc.erl:964 -#, fuzzy -msgid "Password ~b" -msgstr "Sicret" - -#: mod_irc/mod_irc.erl:928 mod_irc/mod_irc_odbc.erl:969 -#, fuzzy -msgid "Port ~b" -msgstr "Pôrt" - -#: mod_irc/mod_irc.erl:933 mod_irc/mod_irc_odbc.erl:974 -msgid "Encoding for server ~b" -msgstr "" - -#: mod_irc/mod_irc.erl:942 mod_irc/mod_irc_odbc.erl:983 -msgid "Server ~b" -msgstr "" - -#: mod_muc/mod_muc.erl:449 mod_muc/mod_muc_odbc.erl:456 -msgid "Only service administrators are allowed to send service messages" -msgstr "" -"Seulmint les manaedjeus d' siervices polèt evoyî des messaedjes di siervice" - -#: mod_muc/mod_muc.erl:493 mod_muc/mod_muc_odbc.erl:500 -msgid "Room creation is denied by service policy" -msgstr "L' ahivaedje del såle est rfuzé pal politike do siervice" - -#: mod_muc/mod_muc.erl:500 mod_muc/mod_muc_odbc.erl:507 -msgid "Conference room does not exist" -msgstr "Li såle di conferince n' egzistêye nén" - -#: mod_muc/mod_muc.erl:582 mod_muc/mod_muc_odbc.erl:595 -msgid "Chatrooms" -msgstr "Såles di berdelaedje" - -#: mod_muc/mod_muc.erl:712 mod_muc/mod_muc_odbc.erl:729 -#, fuzzy -msgid "You need a client that supports x:data to register the nickname" -msgstr "" -"Vos avoz mezåjhe d' on cliyint ki sopoite x:data po-z edjîstrer on metou no" - -#: mod_muc/mod_muc.erl:718 mod_muc/mod_muc_odbc.erl:735 -msgid "Nickname Registration at " -msgstr "Edjîstraedje di metou no amon " - -#: mod_muc/mod_muc.erl:722 mod_muc/mod_muc_odbc.erl:739 -msgid "Enter nickname you want to register" -msgstr "Dinez l' metou no ki vos vloz edjîstrer" - -#: mod_muc/mod_muc.erl:723 mod_muc/mod_muc_odbc.erl:740 -#: mod_muc/mod_muc_room.erl:3810 mod_roster.erl:944 mod_roster_odbc.erl:1061 -#: mod_vcard.erl:364 mod_vcard.erl:477 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:453 -msgid "Nickname" -msgstr "Metou no" - -#: mod_muc/mod_muc.erl:762 mod_muc/mod_muc_odbc.erl:784 -#: mod_muc/mod_muc_room.erl:1064 mod_muc/mod_muc_room.erl:1787 -#, fuzzy -msgid "That nickname is registered by another person" -msgstr "Li metou no est ddja edjîstré pa ene ôte sakî" - -#: mod_muc/mod_muc.erl:788 mod_muc/mod_muc_odbc.erl:811 -msgid "You must fill in field \"Nickname\" in the form" -msgstr "Vos dvoz rimpli l' tchamp «Metou no» dins l' formiulaire" - -#: mod_muc/mod_muc.erl:808 mod_muc/mod_muc_odbc.erl:831 -msgid "ejabberd MUC module" -msgstr "Module MUC (såles di berdelaedje) po ejabberd" - -#: mod_muc/mod_muc_log.erl:374 mod_muc/mod_muc_log.erl:381 -msgid "Chatroom configuration modified" -msgstr "L' apontiaedje del såle di berdelaedje a candjî" - -#: mod_muc/mod_muc_log.erl:384 -msgid "joins the room" -msgstr "arive sol såle" - -#: mod_muc/mod_muc_log.erl:387 mod_muc/mod_muc_log.erl:390 -msgid "leaves the room" -msgstr "cwite li såle" - -#: mod_muc/mod_muc_log.erl:393 mod_muc/mod_muc_log.erl:396 -msgid "has been banned" -msgstr "a stî bani" - -#: mod_muc/mod_muc_log.erl:399 mod_muc/mod_muc_log.erl:402 -msgid "has been kicked" -msgstr "a stî pité evoye" - -#: mod_muc/mod_muc_log.erl:405 -msgid "has been kicked because of an affiliation change" -msgstr "a stî pité evoye cåze d' on candjmint d' afiyaedje" - -#: mod_muc/mod_muc_log.erl:408 -msgid "has been kicked because the room has been changed to members-only" -msgstr "" -"a stî pité evoye cåze ki l' såle a stî ristrindowe åzès mimbes seulmint" - -#: mod_muc/mod_muc_log.erl:411 -msgid "has been kicked because of a system shutdown" -msgstr "a stî pité evoye cåze d' èn arestaedje do sistinme" - -#: mod_muc/mod_muc_log.erl:414 -msgid "is now known as" -msgstr "est asteure kinoxhou come" - -#: mod_muc/mod_muc_log.erl:417 mod_muc/mod_muc_log.erl:688 -#: mod_muc/mod_muc_room.erl:2393 -msgid " has set the subject to: " -msgstr " a candjî l' tite a: " - -#: mod_muc/mod_muc_log.erl:452 -#, fuzzy -msgid "Chatroom is created" -msgstr "Såles di berdelaedje" - -#: mod_muc/mod_muc_log.erl:453 -#, fuzzy -msgid "Chatroom is destroyed" -msgstr "Såles di berdelaedje" - -#: mod_muc/mod_muc_log.erl:454 -#, fuzzy -msgid "Chatroom is started" -msgstr "Såles di berdelaedje" - -#: mod_muc/mod_muc_log.erl:455 -#, fuzzy -msgid "Chatroom is stopped" -msgstr "Såles di berdelaedje" - -#: mod_muc/mod_muc_log.erl:459 -msgid "Monday" -msgstr "londi" - -#: mod_muc/mod_muc_log.erl:460 -msgid "Tuesday" -msgstr "mårdi" - -#: mod_muc/mod_muc_log.erl:461 -msgid "Wednesday" -msgstr "mierkidi" - -#: mod_muc/mod_muc_log.erl:462 -msgid "Thursday" -msgstr "djudi" - -#: mod_muc/mod_muc_log.erl:463 -msgid "Friday" -msgstr "vénrdi" - -#: mod_muc/mod_muc_log.erl:464 -msgid "Saturday" -msgstr "semdi" - -#: mod_muc/mod_muc_log.erl:465 -msgid "Sunday" -msgstr "dimegne" - -#: mod_muc/mod_muc_log.erl:469 -msgid "January" -msgstr "djanvî" - -#: mod_muc/mod_muc_log.erl:470 -msgid "February" -msgstr "fevrî" - -#: mod_muc/mod_muc_log.erl:471 -msgid "March" -msgstr "måss" - -#: mod_muc/mod_muc_log.erl:472 -msgid "April" -msgstr "avri" - -#: mod_muc/mod_muc_log.erl:473 -msgid "May" -msgstr "may" - -#: mod_muc/mod_muc_log.erl:474 -msgid "June" -msgstr "djun" - -#: mod_muc/mod_muc_log.erl:475 -msgid "July" -msgstr "djulete" - -#: mod_muc/mod_muc_log.erl:476 -msgid "August" -msgstr "awousse" - -#: mod_muc/mod_muc_log.erl:477 -msgid "September" -msgstr "setimbe" - -#: mod_muc/mod_muc_log.erl:478 -msgid "October" -msgstr "octôbe" - -#: mod_muc/mod_muc_log.erl:479 -msgid "November" -msgstr "nôvimbe" - -#: mod_muc/mod_muc_log.erl:480 -msgid "December" -msgstr "decimbe" - -#: mod_muc/mod_muc_log.erl:750 -msgid "Room Configuration" -msgstr "Apontiaedje del såle" - -#: mod_muc/mod_muc_log.erl:759 -#, fuzzy -msgid "Room Occupants" -msgstr "Nombe di prezints" - -#: mod_muc/mod_muc_room.erl:174 -msgid "Traffic rate limit is exceeded" -msgstr "Li limite pol volume di trafik a stî passêye" - -#: mod_muc/mod_muc_room.erl:246 -msgid "" -"This participant is kicked from the room because he sent an error message" -msgstr "" -"Ci pårticipant ci a stî pité evoye del såle cåze k' il a-st evoyî on " -"messaedje d' aroke" - -#: mod_muc/mod_muc_room.erl:255 -msgid "It is not allowed to send private messages to the conference" -msgstr "On n' pout nén evoyî des messaedjes privés dins cisse conferince ci" - -#: mod_muc/mod_muc_room.erl:332 -msgid "Please, wait for a while before sending new voice request" -msgstr "" - -#: mod_muc/mod_muc_room.erl:347 -msgid "Voice requests are disabled in this conference" -msgstr "" - -#: mod_muc/mod_muc_room.erl:364 -msgid "Failed to extract JID from your voice request approval" -msgstr "" - -#: mod_muc/mod_muc_room.erl:393 -msgid "Only moderators can approve voice requests" -msgstr "" - -#: mod_muc/mod_muc_room.erl:408 -msgid "Improper message type" -msgstr "Sôre di messaedje nén valide" - -#: mod_muc/mod_muc_room.erl:518 -msgid "" -"This participant is kicked from the room because he sent an error message to " -"another participant" -msgstr "" -"Ci pårticipant ci a stî pité evoye del såle cåze k' il a-st evoyî on " -"messaedje d' aroke a èn ôte pårticipant" - -#: mod_muc/mod_muc_room.erl:531 -msgid "It is not allowed to send private messages of type \"groupchat\"" -msgstr "C' est nén possibe d' evoyî des messaedjes privés del sôre «groupchat»" - -#: mod_muc/mod_muc_room.erl:543 mod_muc/mod_muc_room.erl:611 -msgid "Recipient is not in the conference room" -msgstr "Li riçuveu n' est nén dins l' såle di conferince" - -#: mod_muc/mod_muc_room.erl:564 mod_muc/mod_muc_room.erl:585 -msgid "It is not allowed to send private messages" -msgstr "Ci n' est nén permetou d' evoyî des messaedjes privés" - -#: mod_muc/mod_muc_room.erl:576 mod_muc/mod_muc_room.erl:956 -#: mod_muc/mod_muc_room.erl:4040 -msgid "Only occupants are allowed to send messages to the conference" -msgstr "Seulmint les prezints polèt evoyî des messaedjes al conferince" - -#: mod_muc/mod_muc_room.erl:634 -msgid "Only occupants are allowed to send queries to the conference" -msgstr "Seulmint les prezints polèt evoyî des cweraedjes sol conferince" - -#: mod_muc/mod_muc_room.erl:646 -msgid "Queries to the conference members are not allowed in this room" -msgstr "" -"Les cweraedjes des mimbes del conferince ni sont nén permetous dins cisse " -"såle ci" - -#: mod_muc/mod_muc_room.erl:932 -#, fuzzy -msgid "" -"Only moderators and participants are allowed to change the subject in this " -"room" -msgstr "" -"Seulmint les moderateus et les pårticipants polèt candjî l' sudjet dins " -"cisse såle ci" - -#: mod_muc/mod_muc_room.erl:937 -#, fuzzy -msgid "Only moderators are allowed to change the subject in this room" -msgstr "Seulmint les moderateus polèt candjî l' sudjet dins cisse såle ci" - -#: mod_muc/mod_muc_room.erl:947 -msgid "Visitors are not allowed to send messages to all occupants" -msgstr "Les viziteus n' polèt nén evoyî des messaedjes a tos les prezints" - -#: mod_muc/mod_muc_room.erl:1021 -msgid "" -"This participant is kicked from the room because he sent an error presence" -msgstr "" -"Ci pårticipant ci a stî pité evoye del såle cåze k' il a-st evoyî ene aroke " -"di prezince" - -#: mod_muc/mod_muc_room.erl:1040 -msgid "Visitors are not allowed to change their nicknames in this room" -msgstr "Les viziteus èn polèt nén candjî leus metous no po ç' såle ci" - -#: mod_muc/mod_muc_room.erl:1053 mod_muc/mod_muc_room.erl:1779 -#, fuzzy -msgid "That nickname is already in use by another occupant" -msgstr "Li metou no est ddja eployî pa ene ôte sakî sol såle" - -#: mod_muc/mod_muc_room.erl:1768 -msgid "You have been banned from this room" -msgstr "Vos avoz stî bani di cisse såle ci" - -#: mod_muc/mod_muc_room.erl:1771 -#, fuzzy -msgid "Membership is required to enter this room" -msgstr "I fåt esse mimbe po poleur intrer dins cisse såle ci" - -#: mod_muc/mod_muc_room.erl:1807 -msgid "This room is not anonymous" -msgstr "Cisse såle ci n' est nén anonime" - -#: mod_muc/mod_muc_room.erl:1833 -#, fuzzy -msgid "A password is required to enter this room" -msgstr "I fåt dner on scret po poleur intrer dins cisse såle ci" - -#: mod_muc/mod_muc_room.erl:1855 mod_register.erl:246 -msgid "Too many CAPTCHA requests" -msgstr "" - -#: mod_muc/mod_muc_room.erl:1864 mod_register.erl:251 -msgid "Unable to generate a CAPTCHA" -msgstr "" - -#: mod_muc/mod_muc_room.erl:1874 -msgid "Incorrect password" -msgstr "Sicret nén corek" - -#: mod_muc/mod_muc_room.erl:2448 -msgid "Administrator privileges required" -msgstr "I fåt des priviledjes di manaedjeu" - -#: mod_muc/mod_muc_room.erl:2463 -msgid "Moderator privileges required" -msgstr "I fåt des priviledjes di moderateu" - -#: mod_muc/mod_muc_room.erl:2619 -msgid "Jabber ID ~s is invalid" -msgstr "Li Jabber ID ~s n' est nén valide" - -#: mod_muc/mod_muc_room.erl:2633 -msgid "Nickname ~s does not exist in the room" -msgstr "Li metou no ~s n' egzistêye nén dins l' såle" - -#: mod_muc/mod_muc_room.erl:2659 mod_muc/mod_muc_room.erl:3049 -msgid "Invalid affiliation: ~s" -msgstr "Afiyaedje nén valide: ~s" - -#: mod_muc/mod_muc_room.erl:2713 -msgid "Invalid role: ~s" -msgstr "Role nén valide: ~s" - -#: mod_muc/mod_muc_room.erl:3026 mod_muc/mod_muc_room.erl:3062 -msgid "Owner privileges required" -msgstr "I fåt des priviledjes di prôpietaire" - -#: mod_muc/mod_muc_room.erl:3195 -#, fuzzy -msgid "Configuration of room ~s" -msgstr "Apontiaedje po " - -#: mod_muc/mod_muc_room.erl:3200 -msgid "Room title" -msgstr "Tite del såle" - -#: mod_muc/mod_muc_room.erl:3203 mod_muc/mod_muc_room.erl:3692 -#, fuzzy -msgid "Room description" -msgstr "Discrijhaedje:" - -#: mod_muc/mod_muc_room.erl:3210 -msgid "Make room persistent" -msgstr "Rinde li såle permaninte" - -#: mod_muc/mod_muc_room.erl:3215 -msgid "Make room public searchable" -msgstr "Rinde li såle di berdelaedje cweråve publicmint" - -#: mod_muc/mod_muc_room.erl:3218 -msgid "Make participants list public" -msgstr "Rinde publike li djivêye des pårticipants" - -#: mod_muc/mod_muc_room.erl:3221 -msgid "Make room password protected" -msgstr "Rinde li såle di berdelaedje protedjeye pa scret" - -#: mod_muc/mod_muc_room.erl:3232 -msgid "Maximum Number of Occupants" -msgstr "Nombe macsimom di prezints" - -#: mod_muc/mod_muc_room.erl:3239 -msgid "No limit" -msgstr "Pont d' limite" - -#: mod_muc/mod_muc_room.erl:3250 -msgid "Present real Jabber IDs to" -msgstr "Mostrer les vraiys Jabber IDs a" - -#: mod_muc/mod_muc_room.erl:3258 mod_muc/mod_muc_room.erl:3292 -msgid "moderators only" -msgstr "les moderateus seulmint" - -#: mod_muc/mod_muc_room.erl:3260 mod_muc/mod_muc_room.erl:3294 -msgid "anyone" -msgstr "tot l' minme kî" - -#: mod_muc/mod_muc_room.erl:3262 -msgid "Make room members-only" -msgstr "Rinde li såle di berdelaedje ristrindowe ås mimbes seulmint" - -#: mod_muc/mod_muc_room.erl:3265 -msgid "Make room moderated" -msgstr "Rinde li såle di berdelaedje moderêye" - -#: mod_muc/mod_muc_room.erl:3268 -msgid "Default users as participants" -msgstr "Les uzeus sont des pårticipants come prémetowe dujhance" - -#: mod_muc/mod_muc_room.erl:3271 -#, fuzzy -msgid "Allow users to change the subject" -msgstr "Les uzeus polèt candjî l' tite" - -#: mod_muc/mod_muc_room.erl:3274 -msgid "Allow users to send private messages" -msgstr "Les uzeus polèt evoyî des messaedjes privés" - -#: mod_muc/mod_muc_room.erl:3279 -#, fuzzy -msgid "Allow visitors to send private messages to" -msgstr "Les uzeus polèt evoyî des messaedjes privés" - -#: mod_muc/mod_muc_room.erl:3290 -msgid "nobody" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3296 -msgid "Allow users to query other users" -msgstr "Les uzeus polèt cweri ls ôtes uzeus" - -#: mod_muc/mod_muc_room.erl:3299 -msgid "Allow users to send invites" -msgstr "Les uzeus polèt evoyî priyaedjes" - -#: mod_muc/mod_muc_room.erl:3302 -msgid "Allow visitors to send status text in presence updates" -msgstr "" -"Permete ki les viziteus evoyexhe des tecse d' estat dins leus messaedjes di " -"prezince" - -#: mod_muc/mod_muc_room.erl:3305 -msgid "Allow visitors to change nickname" -msgstr "Permete ki les viziteus candjexhe leus metous nos" - -#: mod_muc/mod_muc_room.erl:3308 -#, fuzzy -msgid "Allow visitors to send voice requests" -msgstr "Les uzeus polèt evoyî priyaedjes" - -#: mod_muc/mod_muc_room.erl:3311 -msgid "Minimum interval between voice requests (in seconds)" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3317 -#, fuzzy -msgid "Make room CAPTCHA protected" -msgstr "Rinde li såle di berdelaedje protedjeye pa scret" - -#: mod_muc/mod_muc_room.erl:3322 -msgid "Exclude Jabber IDs from CAPTCHA challenge" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3329 -msgid "Enable logging" -msgstr "Mete en alaedje li djournå" - -#: mod_muc/mod_muc_room.erl:3337 -msgid "You need an x:data capable client to configure room" -msgstr "I vs fåt on cliyint ki sopoite x:data por vos poleur apontyî l' såle" - -#: mod_muc/mod_muc_room.erl:3694 -msgid "Number of occupants" -msgstr "Nombe di prezints" - -#: mod_muc/mod_muc_room.erl:3750 -msgid "private, " -msgstr "privé, " - -#: mod_muc/mod_muc_room.erl:3799 -msgid "Voice request" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3803 -msgid "Either approve or decline the voice request." -msgstr "" - -#: mod_muc/mod_muc_room.erl:3809 -#, fuzzy -msgid "User JID" -msgstr "Uzeu " - -#: mod_muc/mod_muc_room.erl:3811 -msgid "Grant voice to this person?" -msgstr "" - -#: mod_muc/mod_muc_room.erl:3960 -msgid "~s invites you to the room ~s" -msgstr "~s vos preye sol såle ~s" - -#: mod_muc/mod_muc_room.erl:3969 -msgid "the password is" -msgstr "li scret est" - -#: mod_offline.erl:510 mod_offline_odbc.erl:352 -msgid "" -"Your contact offline message queue is full. The message has been discarded." -msgstr "" -"Li cawêye di messaedjes e môde disraloyî di vosse soçon est plinne. Li " -"messaedje a stî tapé å diale." - -#: mod_offline.erl:560 mod_offline_odbc.erl:408 -msgid "~s's Offline Messages Queue" -msgstr "messaedjes ki ratindèt el cawêye po ~s" - -#: mod_offline.erl:563 mod_offline_odbc.erl:411 mod_roster.erl:987 -#: mod_roster_odbc.erl:1104 mod_shared_roster.erl:901 -#: mod_shared_roster.erl:1010 mod_shared_roster_odbc.erl:930 -#: mod_shared_roster_odbc.erl:1039 web/ejabberd_web_admin.erl:833 -#: web/ejabberd_web_admin.erl:874 web/ejabberd_web_admin.erl:942 -#: web/ejabberd_web_admin.erl:978 web/ejabberd_web_admin.erl:1019 -#: web/ejabberd_web_admin.erl:1508 web/ejabberd_web_admin.erl:1760 -#: web/ejabberd_web_admin.erl:1931 web/ejabberd_web_admin.erl:1963 -#: web/ejabberd_web_admin.erl:2031 web/ejabberd_web_admin.erl:2135 -#: web/ejabberd_web_admin.erl:2160 web/ejabberd_web_admin.erl:2248 -msgid "Submitted" -msgstr "Candjmints evoyîs" - -#: mod_offline.erl:571 -msgid "Time" -msgstr "Date" - -#: mod_offline.erl:572 -msgid "From" -msgstr "Di" - -#: mod_offline.erl:573 -msgid "To" -msgstr "Po" - -#: mod_offline.erl:574 mod_offline_odbc.erl:419 -msgid "Packet" -msgstr "Paket" - -#: mod_offline.erl:587 mod_offline_odbc.erl:432 mod_shared_roster.erl:908 -#: mod_shared_roster_odbc.erl:937 web/ejabberd_web_admin.erl:882 -#: web/ejabberd_web_admin.erl:986 -msgid "Delete Selected" -msgstr "Disfacer les elemints tchoezis" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -msgid "Offline Messages:" -msgstr "Messaedjes ki ratindèt:" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -#, fuzzy -msgid "Remove All Offline Messages" -msgstr "Messaedjes ki ratindèt" - -#: mod_proxy65/mod_proxy65_service.erl:213 -msgid "ejabberd SOCKS5 Bytestreams module" -msgstr "Module SOCKS5 Bytestreams po ejabberd" - -#: mod_pubsub/mod_pubsub.erl:1120 mod_pubsub/mod_pubsub_odbc.erl:929 -msgid "Publish-Subscribe" -msgstr "Eplaidaedje-abounmint" - -#: mod_pubsub/mod_pubsub.erl:1214 mod_pubsub/mod_pubsub_odbc.erl:1025 -msgid "ejabberd Publish-Subscribe module" -msgstr "Module d' eplaidaedje-abounmint po ejabberd" - -#: mod_pubsub/mod_pubsub.erl:1497 mod_pubsub/mod_pubsub_odbc.erl:1312 -msgid "PubSub subscriber request" -msgstr "Dimande d' eplaidaedje-abounmint d' èn abouné" - -#: mod_pubsub/mod_pubsub.erl:1499 mod_pubsub/mod_pubsub_odbc.erl:1314 -msgid "Choose whether to approve this entity's subscription." -msgstr "Tchoezi s' i fåt aprover ou nén l' abounmint di ciste intité." - -#: mod_pubsub/mod_pubsub.erl:1505 mod_pubsub/mod_pubsub_odbc.erl:1320 -msgid "Node ID" -msgstr "ID d' nuk" - -#: mod_pubsub/mod_pubsub.erl:1510 mod_pubsub/mod_pubsub_odbc.erl:1325 -msgid "Subscriber Address" -msgstr "Adresse di l' abouné" - -#: mod_pubsub/mod_pubsub.erl:1516 mod_pubsub/mod_pubsub_odbc.erl:1331 -msgid "Allow this Jabber ID to subscribe to this pubsub node?" -msgstr "" -"Permete ki ci Jabber ID ci si poye abouner a ç' nuk eplaidaedje-abounmint ci?" - -#: mod_pubsub/mod_pubsub.erl:3474 mod_pubsub/mod_pubsub_odbc.erl:3287 -msgid "Deliver payloads with event notifications" -msgstr "Evoyî l' contnou avou les notifiaedjes d' evenmints" - -#: mod_pubsub/mod_pubsub.erl:3475 mod_pubsub/mod_pubsub_odbc.erl:3288 -msgid "Deliver event notifications" -msgstr "Evoyî des notifiaedjes d' evenmints" - -#: mod_pubsub/mod_pubsub.erl:3476 mod_pubsub/mod_pubsub_odbc.erl:3289 -msgid "Notify subscribers when the node configuration changes" -msgstr "Notifyî åzès abounés cwand l' apontiaedje do nuk candje" - -#: mod_pubsub/mod_pubsub.erl:3477 mod_pubsub/mod_pubsub_odbc.erl:3290 -msgid "Notify subscribers when the node is deleted" -msgstr "Notifyî åzès abounés cwand l' nuk est disfacé" - -#: mod_pubsub/mod_pubsub.erl:3478 mod_pubsub/mod_pubsub_odbc.erl:3291 -msgid "Notify subscribers when items are removed from the node" -msgstr "Notifyî åzès abounés cwand des cayets sont oisté foû do nuk" - -#: mod_pubsub/mod_pubsub.erl:3479 mod_pubsub/mod_pubsub_odbc.erl:3292 -msgid "Persist items to storage" -msgstr "Cayets permanints a wårder" - -#: mod_pubsub/mod_pubsub.erl:3480 mod_pubsub/mod_pubsub_odbc.erl:3293 -msgid "A friendly name for the node" -msgstr "On no uzeu-ahessåve pol nuk" - -#: mod_pubsub/mod_pubsub.erl:3481 mod_pubsub/mod_pubsub_odbc.erl:3294 -msgid "Max # of items to persist" -msgstr "Nombe macsimoms di cayets permanints" - -#: mod_pubsub/mod_pubsub.erl:3482 mod_pubsub/mod_pubsub_odbc.erl:3295 -msgid "Whether to allow subscriptions" -msgstr "Si on permete les abounmints" - -#: mod_pubsub/mod_pubsub.erl:3483 mod_pubsub/mod_pubsub_odbc.erl:3296 -msgid "Specify the access model" -msgstr "Sipecifyî l' modele d' accès" - -#: mod_pubsub/mod_pubsub.erl:3486 mod_pubsub/mod_pubsub_odbc.erl:3299 -msgid "Roster groups allowed to subscribe" -msgstr "Pårtaedjîs groupes di soçons k' on s' î pout abouner" - -#: mod_pubsub/mod_pubsub.erl:3487 mod_pubsub/mod_pubsub_odbc.erl:3300 -msgid "Specify the publisher model" -msgstr "Dinez l' modele d' eplaideu" - -#: mod_pubsub/mod_pubsub.erl:3489 mod_pubsub/mod_pubsub_odbc.erl:3302 -msgid "Purge all items when the relevant publisher goes offline" -msgstr "" - -#: mod_pubsub/mod_pubsub.erl:3490 mod_pubsub/mod_pubsub_odbc.erl:3303 -#, fuzzy -msgid "Specify the event message type" -msgstr "Sipecifyî l' modele d' accès" - -#: mod_pubsub/mod_pubsub.erl:3492 mod_pubsub/mod_pubsub_odbc.erl:3305 -msgid "Max payload size in bytes" -msgstr "Contnou macsimom en octets" - -#: mod_pubsub/mod_pubsub.erl:3493 mod_pubsub/mod_pubsub_odbc.erl:3306 -msgid "When to send the last published item" -msgstr "Cwand evoyî l' dierin cayet eplaidî" - -#: mod_pubsub/mod_pubsub.erl:3495 mod_pubsub/mod_pubsub_odbc.erl:3308 -msgid "Only deliver notifications to available users" -msgstr "Seulmint evoyî des notifiaedje åzès uzeus disponibes" - -#: mod_pubsub/mod_pubsub.erl:3496 mod_pubsub/mod_pubsub_odbc.erl:3309 -msgid "The collections with which a node is affiliated" -msgstr "" - -#: mod_register.erl:193 -msgid "The CAPTCHA verification has failed" -msgstr "" - -#: mod_register.erl:220 -#, fuzzy -msgid "You need a client that supports x:data and CAPTCHA to register" -msgstr "" -"Vos avoz mezåjhe d' on cliyint ki sopoite x:data po-z edjîstrer on metou no" - -#: mod_register.erl:226 mod_register.erl:265 -msgid "Choose a username and password to register with this server" -msgstr "" -"Tchoezixhoz on no d' uzeu eyet on scret po vs edjîstrer so ç' sierveu ci" - -#: mod_register.erl:230 mod_vcard.erl:364 mod_vcard_odbc.erl:342 -#: web/ejabberd_web_admin.erl:1515 web/ejabberd_web_admin.erl:1572 -msgid "User" -msgstr "Uzeu" - -#: mod_register.erl:316 mod_register.erl:361 -#, fuzzy -msgid "The password is too weak" -msgstr "li scret est" - -#: mod_register.erl:365 -#, fuzzy -msgid "Users are not allowed to register accounts so quickly" -msgstr "Les noveas uzeus n' si polèt nén edjîstrer si raddimint" - -#: mod_roster.erl:938 mod_roster_odbc.erl:1055 web/ejabberd_web_admin.erl:1701 -#: web/ejabberd_web_admin.erl:1886 web/ejabberd_web_admin.erl:1897 -#: web/ejabberd_web_admin.erl:2219 -msgid "None" -msgstr "Nole" - -#: mod_roster.erl:945 mod_roster_odbc.erl:1062 -msgid "Subscription" -msgstr "Abounmimnt" - -#: mod_roster.erl:946 mod_roster_odbc.erl:1063 -msgid "Pending" -msgstr "Ratindant" - -#: mod_roster.erl:947 mod_roster_odbc.erl:1064 -msgid "Groups" -msgstr "Groupes" - -#: mod_roster.erl:974 mod_roster_odbc.erl:1091 -msgid "Validate" -msgstr "Valider" - -#: mod_roster.erl:982 mod_roster_odbc.erl:1099 -msgid "Remove" -msgstr "Oister" - -#: mod_roster.erl:985 mod_roster_odbc.erl:1102 -msgid "Roster of " -msgstr "Djivêye des soçons da " - -#: mod_roster.erl:988 mod_roster_odbc.erl:1105 mod_shared_roster.erl:902 -#: mod_shared_roster.erl:1011 mod_shared_roster_odbc.erl:931 -#: mod_shared_roster_odbc.erl:1040 web/ejabberd_web_admin.erl:834 -#: web/ejabberd_web_admin.erl:875 web/ejabberd_web_admin.erl:943 -#: web/ejabberd_web_admin.erl:979 web/ejabberd_web_admin.erl:1020 -#: web/ejabberd_web_admin.erl:1509 web/ejabberd_web_admin.erl:1761 -#: web/ejabberd_web_admin.erl:1932 web/ejabberd_web_admin.erl:2136 -#: web/ejabberd_web_admin.erl:2161 -msgid "Bad format" -msgstr "Mwais fôrmat" - -#: mod_roster.erl:995 mod_roster_odbc.erl:1112 -msgid "Add Jabber ID" -msgstr "Radjouter èn ID Jabber" - -#: mod_roster.erl:1094 mod_roster_odbc.erl:1211 -msgid "Roster" -msgstr "Djivêye des soçons" - -#: mod_shared_roster.erl:857 mod_shared_roster.erl:899 -#: mod_shared_roster.erl:1007 mod_shared_roster_odbc.erl:886 -#: mod_shared_roster_odbc.erl:928 mod_shared_roster_odbc.erl:1036 -msgid "Shared Roster Groups" -msgstr "Pårtaedjîs groupes ezès djivêyes di soçons" - -#: mod_shared_roster.erl:895 mod_shared_roster_odbc.erl:924 -#: web/ejabberd_web_admin.erl:1365 web/ejabberd_web_admin.erl:2461 -msgid "Add New" -msgstr "Radjouter" - -#: mod_shared_roster.erl:978 mod_shared_roster_odbc.erl:1007 -msgid "Name:" -msgstr "Pitit no:" - -#: mod_shared_roster.erl:983 mod_shared_roster_odbc.erl:1012 -msgid "Description:" -msgstr "Discrijhaedje:" - -#: mod_shared_roster.erl:991 mod_shared_roster_odbc.erl:1020 -msgid "Members:" -msgstr "Mimbes:" - -#: mod_shared_roster.erl:999 mod_shared_roster_odbc.erl:1028 -msgid "Displayed Groups:" -msgstr "Groupes håynés:" - -#: mod_shared_roster.erl:1008 mod_shared_roster_odbc.erl:1037 -msgid "Group " -msgstr "Groupe " - -#: mod_shared_roster.erl:1017 mod_shared_roster_odbc.erl:1046 -#: web/ejabberd_web_admin.erl:840 web/ejabberd_web_admin.erl:884 -#: web/ejabberd_web_admin.erl:949 web/ejabberd_web_admin.erl:1026 -#: web/ejabberd_web_admin.erl:2017 -msgid "Submit" -msgstr "Evoyî" - -#: mod_vcard.erl:165 mod_vcard_ldap.erl:238 mod_vcard_odbc.erl:129 -msgid "Erlang Jabber Server" -msgstr "Sierveu Jabber Erlang" - -#: mod_vcard.erl:364 mod_vcard.erl:478 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:454 -msgid "Birthday" -msgstr "Date d' askepiaedje" - -#: mod_vcard.erl:364 mod_vcard.erl:480 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:456 -msgid "City" -msgstr "Veye" - -#: mod_vcard.erl:364 mod_vcard.erl:479 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:455 -msgid "Country" -msgstr "Payis" - -#: mod_vcard.erl:364 mod_vcard.erl:481 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:457 -msgid "Email" -msgstr "Emile" - -#: mod_vcard.erl:364 mod_vcard.erl:476 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:452 -msgid "Family Name" -msgstr "No d' famile" - -#: mod_vcard.erl:364 mod_vcard_odbc.erl:342 -msgid "" -"Fill in the form to search for any matching Jabber User (Add * to the end of " -"field to match substring)" -msgstr "" -"Rimplixhoz les tchamps do formulaire po cweri èn uzeu Jabber (radjouter «*» " -"al fén do tchamp po cweri tot l' minme kéne fén d' tchinne" - -#: mod_vcard.erl:364 mod_vcard.erl:473 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:449 -msgid "Full Name" -msgstr "No etir" - -#: mod_vcard.erl:364 mod_vcard.erl:475 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:451 -msgid "Middle Name" -msgstr "No do mitan" - -#: mod_vcard.erl:364 mod_vcard.erl:474 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:450 web/ejabberd_web_admin.erl:2006 -msgid "Name" -msgstr "No" - -#: mod_vcard.erl:364 mod_vcard.erl:482 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:458 -msgid "Organization Name" -msgstr "No d' l' organizåcion" - -#: mod_vcard.erl:364 mod_vcard.erl:483 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:459 -msgid "Organization Unit" -msgstr "Unité d' l' organizåcion" - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "Search users in " -msgstr "Cweri des uzeus dins " - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "You need an x:data capable client to search" -msgstr "Vos avoz mezåjhe d' on cliyint ki sopoite x:data po fé on cweraedje" - -#: mod_vcard.erl:389 mod_vcard_ldap.erl:487 mod_vcard_odbc.erl:367 -msgid "vCard User Search" -msgstr "Calpin des uzeus" - -#: mod_vcard.erl:445 mod_vcard_ldap.erl:541 mod_vcard_odbc.erl:421 -msgid "ejabberd vCard module" -msgstr "Module vCard ejabberd" - -#: mod_vcard.erl:469 mod_vcard_ldap.erl:551 mod_vcard_odbc.erl:445 -msgid "Search Results for " -msgstr "Rizultats do cweraedje po " - -#: mod_vcard_ldap.erl:462 -msgid "Fill in fields to search for any matching Jabber User" -msgstr "Rimplixhoz les tchamps po cweri èn uzeu Jabber" - -#: web/ejabberd_web_admin.erl:193 web/ejabberd_web_admin.erl:203 -#: web/ejabberd_web_admin.erl:219 web/ejabberd_web_admin.erl:229 -msgid "Unauthorized" -msgstr "" - -#: web/ejabberd_web_admin.erl:286 web/ejabberd_web_admin.erl:303 -msgid "ejabberd Web Admin" -msgstr "Manaedjeu waibe ejabberd" - -#: web/ejabberd_web_admin.erl:769 web/ejabberd_web_admin.erl:780 -msgid "Administration" -msgstr "Manaedjaedje" - -#: web/ejabberd_web_admin.erl:878 web/ejabberd_web_admin.erl:982 -msgid "Raw" -msgstr "Dinêyes brutes" - -#: web/ejabberd_web_admin.erl:1017 -msgid "~s access rule configuration" -msgstr "Apontiaedje des rîles d' accès a ~s" - -#: web/ejabberd_web_admin.erl:1035 -msgid "Virtual Hosts" -msgstr "Forveyous sierveus" - -#: web/ejabberd_web_admin.erl:1043 web/ejabberd_web_admin.erl:1050 -msgid "Users" -msgstr "Uzeus" - -#: web/ejabberd_web_admin.erl:1078 -msgid "Users Last Activity" -msgstr "Dierinne activité des uzeus" - -#: web/ejabberd_web_admin.erl:1080 -msgid "Period: " -msgstr "Termene:" - -#: web/ejabberd_web_admin.erl:1090 -msgid "Last month" -msgstr "Dierin moes" - -#: web/ejabberd_web_admin.erl:1091 -msgid "Last year" -msgstr "Dierinne anêye" - -#: web/ejabberd_web_admin.erl:1092 -msgid "All activity" -msgstr "Dispoy todi" - -#: web/ejabberd_web_admin.erl:1094 -msgid "Show Ordinary Table" -msgstr "Mostrer crexhince" - -#: web/ejabberd_web_admin.erl:1096 -msgid "Show Integral Table" -msgstr "Mostrer totå" - -#: web/ejabberd_web_admin.erl:1105 web/ejabberd_web_admin.erl:1939 -msgid "Statistics" -msgstr "Sitatistikes" - -#: web/ejabberd_web_admin.erl:1117 -#, fuzzy -msgid "Not Found" -msgstr "Nuk nén trové" - -#: web/ejabberd_web_admin.erl:1134 -msgid "Node not found" -msgstr "Nuk nén trové" - -#: web/ejabberd_web_admin.erl:1460 -msgid "Host" -msgstr "Sierveu" - -#: web/ejabberd_web_admin.erl:1461 -msgid "Registered Users" -msgstr "Uzeus edjistrés" - -#: web/ejabberd_web_admin.erl:1573 -msgid "Offline Messages" -msgstr "Messaedjes ki ratindèt" - -#: web/ejabberd_web_admin.erl:1574 web/ejabberd_web_admin.erl:1767 -msgid "Last Activity" -msgstr "Dierinne activité" - -#: web/ejabberd_web_admin.erl:1659 web/ejabberd_web_admin.erl:1675 -msgid "Registered Users:" -msgstr "Uzeus edjistrés:" - -#: web/ejabberd_web_admin.erl:1661 web/ejabberd_web_admin.erl:1677 -#: web/ejabberd_web_admin.erl:2192 -msgid "Online Users:" -msgstr "Uzeus raloyîs:" - -#: web/ejabberd_web_admin.erl:1663 -msgid "Outgoing s2s Connections:" -msgstr "Raloyaedjes s2s e rexhowe:" - -#: web/ejabberd_web_admin.erl:1665 -msgid "Outgoing s2s Servers:" -msgstr "Sierveus s2s e rexhowe:" - -#: web/ejabberd_web_admin.erl:1734 web/mod_register_web.erl:175 -#: web/mod_register_web.erl:372 web/mod_register_web.erl:381 -#: web/mod_register_web.erl:411 -msgid "Change Password" -msgstr "Candjî l' sicret" - -#: web/ejabberd_web_admin.erl:1758 -msgid "User " -msgstr "Uzeu " - -#: web/ejabberd_web_admin.erl:1765 -msgid "Connected Resources:" -msgstr "Raloyî avou les rsoûces:" - -#: web/ejabberd_web_admin.erl:1766 web/mod_register_web.erl:227 -#: web/mod_register_web.erl:519 -msgid "Password:" -msgstr "Sicret:" - -#: web/ejabberd_web_admin.erl:1828 -msgid "No Data" -msgstr "Nole dinêye disponibe" - -#: web/ejabberd_web_admin.erl:1906 -msgid "Nodes" -msgstr "Nuks" - -#: web/ejabberd_web_admin.erl:1929 web/ejabberd_web_admin.erl:1951 -msgid "Node " -msgstr "Nuk " - -#: web/ejabberd_web_admin.erl:1938 -msgid "Listened Ports" -msgstr "Pôrts drovous" - -#: web/ejabberd_web_admin.erl:1940 web/ejabberd_web_admin.erl:2260 -#: web/ejabberd_web_admin.erl:2448 -msgid "Update" -msgstr "Mete a djoû" - -#: web/ejabberd_web_admin.erl:1943 web/ejabberd_web_admin.erl:2569 -msgid "Restart" -msgstr "Renonder" - -#: web/ejabberd_web_admin.erl:1945 web/ejabberd_web_admin.erl:2571 -msgid "Stop" -msgstr "Arester" - -#: web/ejabberd_web_admin.erl:1959 -msgid "RPC Call Error" -msgstr "Aroke di houcaedje RPC" - -#: web/ejabberd_web_admin.erl:2000 -msgid "Database Tables at " -msgstr "Tåves del båze di dnêyes so " - -#: web/ejabberd_web_admin.erl:2007 -msgid "Storage Type" -msgstr "Sôre di wårdaedje" - -#: web/ejabberd_web_admin.erl:2008 -msgid "Elements" -msgstr "" - -#: web/ejabberd_web_admin.erl:2009 -msgid "Memory" -msgstr "Memwere" - -#: web/ejabberd_web_admin.erl:2032 web/ejabberd_web_admin.erl:2137 -msgid "Error" -msgstr "" - -#: web/ejabberd_web_admin.erl:2034 -msgid "Backup of " -msgstr "Copeye di såvrité po " - -#: web/ejabberd_web_admin.erl:2036 -#, fuzzy -msgid "" -"Please note that these options will only backup the builtin Mnesia database. " -"If you are using the ODBC module, you also need to backup your SQL database " -"separately." -msgstr "" -"Notez ki ces tchuzes la vont seulmint fé ene copeye di såvrité del båze di " -"dnêyes Mnesia costrûte å dvins do programe. Si vos eployîz ene difoûtrinne " -"båze di dnêyes avou l' module ODBC, vos dvoz fé ene copeye di såvrité del " -"båze SQL da vosse sepårumint." - -#: web/ejabberd_web_admin.erl:2041 -msgid "Store binary backup:" -msgstr "Copeye di såvrité binaire:" - -#: web/ejabberd_web_admin.erl:2045 web/ejabberd_web_admin.erl:2052 -#: web/ejabberd_web_admin.erl:2060 web/ejabberd_web_admin.erl:2067 -#: web/ejabberd_web_admin.erl:2074 web/ejabberd_web_admin.erl:2081 -#: web/ejabberd_web_admin.erl:2088 web/ejabberd_web_admin.erl:2096 -#: web/ejabberd_web_admin.erl:2103 web/ejabberd_web_admin.erl:2110 -msgid "OK" -msgstr "'l est bon" - -#: web/ejabberd_web_admin.erl:2048 -msgid "Restore binary backup immediately:" -msgstr "Rapexhî do côp foû d' ene copeye di såvrité binaire:" - -#: web/ejabberd_web_admin.erl:2056 -msgid "" -"Restore binary backup after next ejabberd restart (requires less memory):" -msgstr "" -"Rapexhî l' copeye di såvrité binaire après l' renondaedje ki vént " -"d' ejabberd (çoula prind moens d' memwere del fé insi):" - -#: web/ejabberd_web_admin.erl:2063 -msgid "Store plain text backup:" -msgstr "Copeye di såvrité tecse:" - -#: web/ejabberd_web_admin.erl:2070 -msgid "Restore plain text backup immediately:" -msgstr "Rapexhî do côp foû d' ene copeye di såvrité tecse:" - -#: web/ejabberd_web_admin.erl:2077 -msgid "Import users data from a PIEFXIS file (XEP-0227):" -msgstr "" - -#: web/ejabberd_web_admin.erl:2084 -msgid "Export data of all users in the server to PIEFXIS files (XEP-0227):" -msgstr "" - -#: web/ejabberd_web_admin.erl:2091 -msgid "Export data of users in a host to PIEFXIS files (XEP-0227):" -msgstr "" - -#: web/ejabberd_web_admin.erl:2099 -#, fuzzy -msgid "Import user data from jabberd14 spool file:" -msgstr "Sititchî des uzeus Jabberd 1.4" - -#: web/ejabberd_web_admin.erl:2106 -#, fuzzy -msgid "Import users data from jabberd14 spool directory:" -msgstr "Sititchî des uzeus Jabberd 1.4" - -#: web/ejabberd_web_admin.erl:2132 -msgid "Listened Ports at " -msgstr "Pôrts drovous so " - -#: web/ejabberd_web_admin.erl:2157 -msgid "Modules at " -msgstr "Modules so " - -#: web/ejabberd_web_admin.erl:2183 -msgid "Statistics of ~p" -msgstr "Sitatistikes di ~p" - -#: web/ejabberd_web_admin.erl:2186 -msgid "Uptime:" -msgstr "Tins dispoy l' enondaedje:" - -#: web/ejabberd_web_admin.erl:2189 -msgid "CPU Time:" -msgstr "Tins CPU:" - -#: web/ejabberd_web_admin.erl:2195 -msgid "Transactions Committed:" -msgstr "Transaccions evoyeyes:" - -#: web/ejabberd_web_admin.erl:2198 -msgid "Transactions Aborted:" -msgstr "Transaccions arestêyes:" - -#: web/ejabberd_web_admin.erl:2201 -msgid "Transactions Restarted:" -msgstr "Transaccions renondêyes:" - -#: web/ejabberd_web_admin.erl:2204 -msgid "Transactions Logged:" -msgstr "Transaccions wårdêyes e djournå:" - -#: web/ejabberd_web_admin.erl:2246 -msgid "Update " -msgstr "Metaedje a djoû " - -#: web/ejabberd_web_admin.erl:2254 -msgid "Update plan" -msgstr "Plan d' metaedje a djoû" - -#: web/ejabberd_web_admin.erl:2255 -#, fuzzy -msgid "Modified modules" -msgstr "Modules metous a djoû" - -#: web/ejabberd_web_admin.erl:2256 -msgid "Update script" -msgstr "Sicripe di metaedje a djoû" - -#: web/ejabberd_web_admin.erl:2257 -msgid "Low level update script" -msgstr "Sicripe di metaedje a djoû d' bas livea" - -#: web/ejabberd_web_admin.erl:2258 -msgid "Script check" -msgstr "Acertinaedje do scripe" - -#: web/ejabberd_web_admin.erl:2426 -msgid "Port" -msgstr "Pôrt" - -#: web/ejabberd_web_admin.erl:2427 -msgid "IP" -msgstr "" - -#: web/ejabberd_web_admin.erl:2428 -#, fuzzy -msgid "Protocol" -msgstr "Pôrt" - -#: web/ejabberd_web_admin.erl:2429 web/ejabberd_web_admin.erl:2556 -msgid "Module" -msgstr "Module" - -#: web/ejabberd_web_admin.erl:2430 web/ejabberd_web_admin.erl:2557 -msgid "Options" -msgstr "Tchuzes" - -#: web/ejabberd_web_admin.erl:2450 -msgid "Delete" -msgstr "Disfacer" - -#: web/ejabberd_web_admin.erl:2579 -msgid "Start" -msgstr "Enonder" - -#: web/mod_register_web.erl:104 -msgid "Your Jabber account was successfully created." -msgstr "" - -#: web/mod_register_web.erl:107 -msgid "There was an error creating the account: " -msgstr "" - -#: web/mod_register_web.erl:115 -msgid "Your Jabber account was successfully deleted." -msgstr "" - -#: web/mod_register_web.erl:118 -msgid "There was an error deleting the account: " -msgstr "" - -#: web/mod_register_web.erl:128 -msgid "The password of your Jabber account was successfully changed." -msgstr "" - -#: web/mod_register_web.erl:131 -msgid "There was an error changing the password: " -msgstr "" - -#: web/mod_register_web.erl:163 web/mod_register_web.erl:172 -msgid "Jabber Account Registration" -msgstr "" - -#: web/mod_register_web.erl:174 web/mod_register_web.erl:192 -#: web/mod_register_web.erl:201 -msgid "Register a Jabber account" -msgstr "" - -#: web/mod_register_web.erl:176 web/mod_register_web.erl:493 -#: web/mod_register_web.erl:502 -msgid "Unregister a Jabber account" -msgstr "" - -#: web/mod_register_web.erl:203 -msgid "" -"This page allows to create a Jabber account in this Jabber server. Your JID " -"(Jabber IDentifier) will be of the form: username@server. Please read " -"carefully the instructions to fill correctly the fields." -msgstr "" - -#: web/mod_register_web.erl:212 web/mod_register_web.erl:386 -#: web/mod_register_web.erl:509 -#, fuzzy -msgid "Username:" -msgstr "No d' uzeu IRC" - -#: web/mod_register_web.erl:217 -msgid "This is case insensitive: macbeth is the same that MacBeth and Macbeth." -msgstr "" - -#: web/mod_register_web.erl:218 -msgid "Characters not allowed:" -msgstr "" - -#: web/mod_register_web.erl:222 web/mod_register_web.erl:391 -#: web/mod_register_web.erl:514 -#, fuzzy -msgid "Server:" -msgstr "Måy" - -#: web/mod_register_web.erl:232 -msgid "" -"Don't tell your password to anybody, not even the administrators of the " -"Jabber server." -msgstr "" - -#: web/mod_register_web.erl:234 -msgid "You can later change your password using a Jabber client." -msgstr "" - -#: web/mod_register_web.erl:235 -msgid "" -"Some Jabber clients can store your password in your computer. Use that " -"feature only if you trust your computer is safe." -msgstr "" - -#: web/mod_register_web.erl:237 -msgid "" -"Memorize your password, or write it in a paper placed in a safe place. In " -"Jabber there isn't an automated way to recover your password if you forget " -"it." -msgstr "" - -#: web/mod_register_web.erl:242 web/mod_register_web.erl:406 -#, fuzzy -msgid "Password Verification:" -msgstr "Acertinaedje do scret" - -#: web/mod_register_web.erl:250 -#, fuzzy -msgid "Register" -msgstr "Djivêye des soçons" - -#: web/mod_register_web.erl:396 -#, fuzzy -msgid "Old Password:" -msgstr "Sicret:" - -#: web/mod_register_web.erl:401 -#, fuzzy -msgid "New Password:" -msgstr "Sicret:" - -#: web/mod_register_web.erl:504 -msgid "This page allows to unregister a Jabber account in this Jabber server." -msgstr "" - -#: web/mod_register_web.erl:524 -msgid "Unregister" -msgstr "" - -#~ msgid "Encodings" -#~ msgstr "Ecôdaedjes" - -#~ msgid "(Raw)" -#~ msgstr "(Dinêyes brutes)" - -#~ msgid "Specified nickname is already registered" -#~ msgstr "Li no metou dné est ddja edjîstré" - -#~ msgid "Size" -#~ msgstr "Grandeu" diff --git a/priv/msgs/zh.msg b/priv/msgs/zh.msg index 934ff6729..4f5688244 100644 --- a/priv/msgs/zh.msg +++ b/priv/msgs/zh.msg @@ -1,421 +1,630 @@ -{"Access Configuration","访问配置"}. -{"Access Control List Configuration","访问控制列表(ACL)配置"}. -{"Access control lists","访问控制列表(ACL)"}. -{"Access Control Lists","访问控制列表(ACL)"}. -{"Access denied by service policy","访问被服务策略拒绝"}. -{"Access rules","访问规则"}. -{"Access Rules","访问规则"}. -{"Action on user","对用户的动作"}. -{"Add Jabber ID","添加 Jabber ID"}. -{"Add New","添加新用户"}. +%% Generated automatically +%% DO NOT EDIT: run `make translations` instead +%% To improve translations please read: +%% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ + +{" (Add * to the end of field to match substring)"," (在字段末尾添加 * 以匹配子字符串)"}. +{" has set the subject to: "," 已将主题设置为: "}. +{"# participants","# 参与者"}. +{"A description of the node","节点的描述"}. +{"A friendly name for the node","节点的友好名称"}. +{"A password is required to enter this room","需要密码才能进入此房间"}. +{"A Web Page","网页"}. +{"Accept","接受"}. +{"Access denied by service policy","服务策略拒绝访问"}. +{"Access model","访问模型"}. +{"Account doesn't exist","账号不存在"}. +{"Action on user","对用户的操作"}. +{"Add a hat to a user","给用户添加头衔"}. {"Add User","添加用户"}. -{"Administration of ","管理"}. +{"Administration of ","管理 "}. {"Administration","管理"}. {"Administrator privileges required","需要管理员权限"}. -{"A friendly name for the node","该节点的友好名称"}. {"All activity","所有活动"}. -{"Allow this Jabber ID to subscribe to this pubsub node?","允许该 Jabber ID 订阅该 pubsub 节点?"}. -{"Allow users to change the subject","允许用户更改主题"}. -{"Allow users to query other users","允许用户查询其它用户"}. -{"Allow users to send invites","允许用户发送邀请"}. -{"Allow users to send private messages","允许用户发送私聊消息"}. -{"Allow visitors to change nickname","允许用户更改昵称"}. -{"Allow visitors to send private messages to","允许访客发送私聊消息至"}. -{"Allow visitors to send status text in presence updates","更新在线状态时允许用户发送状态文本"}. -{"Allow visitors to send voice requests","允许访客发送声音请求"}. {"All Users","所有用户"}. -{"Announcements","通知"}. -{"anyone","任何人"}. -{"A password is required to enter this room","进入此房间需要密码"}. +{"Allow subscription","允许订阅"}. +{"Allow this Jabber ID to subscribe to this pubsub node?","是否允许此 Jabber ID 订阅此 pubsub 节点?"}. +{"Allow this person to register with the room?","是否允许此用户在房间注册?"}. +{"Allow users to change the subject","允许用户更改主题"}. +{"Allow users to query other users","允许用户查询其他用户"}. +{"Allow users to send invites","允许用户发送邀请"}. +{"Allow users to send private messages","允许用户发送私信"}. +{"Allow visitors to change nickname","允许参观者更改昵称"}. +{"Allow visitors to send private messages to","允许参观者发送私信至"}. +{"Allow visitors to send status text in presence updates","允许参观者在在线状态更新中发送状态文本"}. +{"Allow visitors to send voice requests","允许参观者发送发言权请求"}. +{"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.","与定义房间成员资格相关联的 LDAP 组;根据组的特定实施或特定部署的定义,使用 LDAP 专有名称。"}. +{"Announcements","公告"}. +{"Answer associated with a picture","与图片相关的答案"}. +{"Answer associated with a video","与视频相关的答案"}. +{"Answer associated with speech","与讲话相关的答案"}. +{"Answer to a question","问题的答案"}. +{"Anyone in the specified roster group(s) may subscribe and retrieve items","指定名册组中的任何人都可以订阅和检索项目"}. +{"Anyone may associate leaf nodes with the collection","任何人都可以将叶节点与集合关联"}. +{"Anyone may publish","任何人都可以发布"}. +{"Anyone may subscribe and retrieve items","任何人都可以订阅和检索项目"}. +{"Anyone with a presence subscription of both or from may subscribe and retrieve items","任何拥有 both 或 from 的在线状态订阅的用户都可以订阅和检索项目"}. +{"Anyone with Voice","任何有发言权的人"}. +{"Anyone","任何人"}. +{"API Commands","API 命令"}. {"April","四月"}. +{"Arguments","参数"}. +{"Attribute 'channel' is required for this request","此请求要求“channel”属性"}. +{"Attribute 'id' is mandatory for MIX messages","对于 MIX 消息,“id”属性是必需的"}. +{"Attribute 'jid' is not allowed here","此处不允许“jid”属性"}. +{"Attribute 'node' is not allowed here","此处不允许“node”属性"}. +{"Attribute 'to' of stanza that triggered challenge","触发挑战节的“to”属性"}. {"August","八月"}. +{"Automatic node creation is not enabled","未启用自动节点创建"}. {"Backup Management","备份管理"}. -{"Backup of ","备份来源 "}. -{"Backup to File at ","备份文件位于"}. +{"Backup of ~p","~p 的备份"}. +{"Backup to File at ","备份文件位于 "}. {"Backup","备份"}. {"Bad format","格式错误"}. -{"Birthday","出生日期"}. +{"Birthday","生日"}. +{"Both the username and the resource are required","用户名和资源均为必填项"}. +{"Bytestream already activated","字节流已激活"}. +{"Cannot remove active list","无法移除活动列表"}. +{"Cannot remove default list","无法移除默认列表"}. {"CAPTCHA web page","验证码网页"}. +{"Challenge ID","挑战 ID"}. {"Change Password","更改密码"}. {"Change User Password","更改用户密码"}. -{"Characters not allowed:","禁用字符:"}. +{"Changing password is not allowed","不允许更改密码"}. +{"Changing role/affiliation is not allowed","不允许更改角色/从属关系"}. +{"Channel already exists","频道已存在"}. +{"Channel does not exist","频道不存在"}. +{"Channel JID","频道 JID"}. +{"Channels","频道"}. +{"Characters not allowed:","不允许字符:"}. {"Chatroom configuration modified","聊天室配置已修改"}. -{"Chatroom is created","聊天室已被创建"}. -{"Chatroom is destroyed","聊天室已被销毁"}. -{"Chatroom is started","聊天室已被启动"}. -{"Chatroom is stopped","聊天室已被停用"}. +{"Chatroom is created","已创建聊天室"}. +{"Chatroom is destroyed","已解散聊天室"}. +{"Chatroom is started","已启动聊天室"}. +{"Chatroom is stopped","已停止聊天室"}. {"Chatrooms","聊天室"}. -{"Choose a username and password to register with this server","请选择在此服务器上注册所需的用户名和密码"}. -{"Choose modules to stop","请选择要停止的模块"}. -{"Choose storage type of tables","请选择表格的存储类型"}. -{"Choose whether to approve this entity's subscription.","选择是否允许该实体的订阅"}. +{"Choose a username and password to register with this server","请选择要在此服务器中注册的用户名和密码"}. +{"Choose storage type of tables","选择表的存储类型"}. +{"Choose whether to approve this entity's subscription.","选择是否批准此实体的订阅。"}. {"City","城市"}. +{"Client acknowledged more stanzas than sent by server","客户端确认的节数多于服务器发送的节数"}. +{"Clustering","集群"}. {"Commands","命令"}. {"Conference room does not exist","会议室不存在"}. -{"Configuration of room ~s","房间 ~s 的配置 "}. +{"Configuration of room ~s","房间 ~s 的配置"}. {"Configuration","配置"}. -{"Connected Resources:","已连接资源:"}. -{"Connections parameters","连接参数"}. -{"Country","国家"}. -{"CPU Time:","CPU 时间:"}. -{"Database Tables at ","数据库列表位于 "}. -{"Database Tables Configuration at ","数据库表格配置位于"}. +{"Contact Addresses (normally, room owner or owners)","联系地址(通常为房间所有者)"}. +{"Country","国家/地区"}. +{"Current Discussion Topic","当前讨论话题"}. +{"Database failure","数据库失败"}. +{"Database Tables Configuration at ","数据库表配置在 "}. {"Database","数据库"}. {"December","十二月"}. -{"Default users as participants","用户默认被视为参与人"}. +{"Default users as participants","默认用户为参与者"}. {"Delete message of the day on all hosts","删除所有主机上的每日消息"}. {"Delete message of the day","删除每日消息"}. -{"Delete Selected","删除已选内容"}. {"Delete User","删除用户"}. -{"Delete","删除"}. {"Deliver event notifications","传递事件通知"}. -{"Deliver payloads with event notifications","用事件通告传输有效负载"}. -{"Description:","描述:"}. -{"Disc only copy","仅磁盘复制"}. -{"Displayed Groups:","已显示的组:"}. -{"Don't tell your password to anybody, not even the administrators of the Jabber server.","不要将密码告诉任何人, 就算是 Jabber 服务器的管理员也不可以."}. -{"Dump Backup to Text File at ","转储备份到文本文件于"}. +{"Deliver payloads with event notifications","用事件通知传递有效负载"}. +{"Disc only copy","仅磁盘副本"}. +{"Don't tell your password to anybody, not even the administrators of the XMPP server.","不要将您的密码告诉任何人,甚至是 XMPP 服务器的管理员。"}. +{"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.","接受或拒绝声音请求"}. -{"ejabberd IRC module","ejabberd IRC 模块"}. +{"Either approve or decline the voice request.","批准或拒绝发言权请求。"}. +{"ejabberd HTTP Upload service","ejabberd HTTP 上传服务"}. {"ejabberd MUC module","ejabberd MUC 模块"}. -{"ejabberd Publish-Subscribe module","ejabberd 发行-订阅模块"}. +{"ejabberd Multicast service","ejabberd 多播服务"}. +{"ejabberd Publish-Subscribe module","ejabberd 发布–订阅模块"}. {"ejabberd SOCKS5 Bytestreams module","ejabberd SOCKS5 字节流模块"}. {"ejabberd vCard module","ejabberd vCard 模块"}. -{"ejabberd Web Admin","ejabberd 网页管理"}. -{"Elements","元素"}. +{"ejabberd Web Admin","ejabberd Web 管理"}. +{"ejabberd","ejabberd"}. +{"Email Address","电子邮件地址"}. {"Email","电子邮件"}. -{"Enable logging","启用服务器端聊天记录"}. -{"Encoding for server ~b","服务器 ~b 的编码"}. +{"Enable hats","启用头衔"}. +{"Enable logging","启用日志记录"}. +{"Enable message archiving","启用消息归档"}. +{"Enabling push without 'node' attribute is not supported","不支持没有“node”属性就启用推送"}. {"End User Session","结束用户会话"}. -{"Enter list of {Module, [Options]}","请输入{模块, [选项]}列表"}. -{"Enter nickname you want to register","请输入您想要注册的昵称"}. +{"Enter nickname you want to register","请输入要注册的昵称"}. {"Enter path to backup file","请输入备份文件的路径"}. {"Enter path to jabberd14 spool dir","请输入 jabberd14 spool 目录的路径"}. {"Enter path to jabberd14 spool file","请输入 jabberd14 spool 文件的路径"}. {"Enter path to text file","请输入文本文件的路径"}. -{"Enter the text you see","请输入您所看到的文本"}. -{"Enter username and encodings you wish to use for connecting to IRC servers. Press 'Next' to get more fields to fill in. Press 'Complete' to save settings.","请输入您想使用的用来连接到 IRC 服务器的用户名和编码. 按 '下一步' 获取更多待填字段. 按 '完成' 保存设置."}. -{"Enter username, encodings, ports and passwords you wish to use for connecting to IRC servers","请输入您想使用的用来连接到IRC服务器的用户名, 编码, 端口和密码."}. -{"Erlang Jabber Server","Erlang Jabber 服务器"}. -{"Error","错误"}. -{"Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef.net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}].","例如: [{\"irc.lucky.net\", \"koi8-r\"}, 6667, \"secret\"}, {\"vendetta.fef.net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]."}. -{"Exclude Jabber IDs from CAPTCHA challenge","从验证码挑战中排除 Jabber ID"}. -{"Export data of all users in the server to PIEFXIS files (XEP-0227):","将服务器上所有用户的数据导出到 PIEFXIS 文件 (XEP-0227):"}. -{"Export data of users in a host to PIEFXIS files (XEP-0227):","将某主机的用户数据导出到 PIEFXIS 文件 (XEP-0227):"}. -{"Failed to extract JID from your voice request approval","无法从你的声音请求确认信息中提取JID"}. +{"Enter the text you see","请输入您看到的文本"}. +{"Erlang XMPP Server","Erlang XMPP 服务器"}. +{"Exclude Jabber IDs from CAPTCHA challenge","从验证码挑战中排除的 Jabber ID"}. +{"Export all tables as SQL queries to a file:","将所有表以 SQL 查询导出到文件:"}. +{"Export data of all users in the server to PIEFXIS files (XEP-0227):","将服务器中所有用户的数据导出到 PIEFXIS 文件(XEP-0227):"}. +{"Export data of users in a host to PIEFXIS files (XEP-0227):","将主机中用户的数据导出到 PIEFXIS 文件(XEP-0227):"}. +{"External component failure","外部组件故障"}. +{"External component timeout","外部组件超时"}. +{"Failed to activate bytestream","无法激活字节流"}. +{"Failed to extract JID from your voice request approval","无法从您的发言权请求批准中提取 JID"}. +{"Failed to map delegated namespace to external component","无法将委托命名空间映射到外部组件"}. +{"Failed to parse HTTP response","无法解析 HTTP 响应"}. +{"Failed to process option '~s'","无法处理选项“~s”"}. {"Family Name","姓氏"}. +{"FAQ Entry","常见问题条目"}. {"February","二月"}. -{"Fill in fields to search for any matching Jabber User","填充字段以搜索任何匹配的 Jabber 用户"}. -{"Fill in the form to search for any matching Jabber User (Add * to the end of field to match substring)","填充表单以搜索任何匹配的 Jabber 用户(在字段末添加*来匹配子串)"}. -{"Friday","星期五"}. -{"From ~s","来自~s"}. -{"From","从"}. +{"File larger than ~w bytes","文件大于 ~w 字节"}. +{"Fill in the form to search for any matching XMPP User","填写表单以搜索任何匹配的 XMPP 用户"}. +{"Friday","周五"}. +{"From ~ts","来自 ~ts"}. +{"Full List of Room Admins","房间管理员的完整列表"}. +{"Full List of Room Owners","房间所有者的完整列表"}. {"Full Name","全名"}. +{"Get List of Online Users","获取在线用户列表"}. +{"Get List of Registered Users","获取注册用户列表"}. {"Get Number of Online Users","获取在线用户数"}. {"Get Number of Registered Users","获取注册用户数"}. -{"Get User Last Login Time","获取用户上次登陆时间"}. -{"Get User Password","获取用户密码"}. -{"Get User Statistics","获取用户统计"}. -{"Grant voice to this person?","为此人授权声音?"}. -{"Groups","组"}. -{"Group ","组"}. -{"has been banned","已被禁止"}. -{"has been kicked because of an affiliation change","因联属关系改变而被踢出"}. -{"has been kicked because of a system shutdown","因系统关机而被踢出"}. -{"has been kicked because the room has been changed to members-only","因该房间改为只对会员开放而被踢出"}. +{"Get Pending","获取待处理"}. +{"Get User Last Login Time","获取用户上次登录时间"}. +{"Get User Statistics","获取用户统计数据"}. +{"Given Name","名字"}. +{"Grant voice to this person?","是否授予此用户发言权?"}. +{"has been banned","已被封禁"}. +{"has been kicked because of a system shutdown","因系统关闭而被踢出"}. +{"has been kicked because of an affiliation change","由于从属关系的更改而被踢出"}. +{"has been kicked because the room has been changed to members-only","被踢出,因为房间已更改为仅成员"}. {"has been kicked","已被踢出"}. -{" has set the subject to: ","已将标题设置为: "}. -{"Host","主机"}. -{"If you don't see the CAPTCHA image here, visit the web page.","如果您在这里没有看到验证码图片, 请访问网页."}. -{"If you want to specify different ports, passwords, encodings for IRC servers, fill this list with values in format '{\"irc server\", \"encoding\", port, \"password\"}'. By default this service use \"~s\" encoding, port ~p, empty password.","如果您想为 IRC 服务器指定不同的端口, 密码, 编码, 请用 '{\"irc 服务器\", \"编码\", 端口, \"密码\"}' 格式的值填充此表单. 默认情况下此服务使用\"~s\"编码, ~p 端口, 密码为空."}. +{"Hash of the vCard-temp avatar of this room","此房间的 vCard-temp 头像的散列"}. +{"Hat title","头衔标题"}. +{"Hat URI","头衔 URI"}. +{"Hats limit exceeded","已超过头衔限制"}. +{"Host unknown","主机未知"}. +{"HTTP File Upload","HTTP 文件上传"}. +{"Idle connection","空闲连接"}. +{"If you don't see the CAPTCHA image here, visit the web page.","如果您在此处没有看到验证码图片,请访问网页。"}. {"Import Directory","导入目录"}. {"Import File","导入文件"}. -{"Import user data from jabberd14 spool file:","从 jabberd14 Spool 文件导入用户数据:"}. -{"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 user data from jabberd14 spool file:","从 jabberd14 Spool 文件导入用户数据:"}. +{"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 jabberd14 Spool Files","从 jabberd14 Spool 文件导入用户"}. -{"Improper message type","不恰当的消息类型"}. +{"Improper domain part of 'from' attribute","“from”属性域名部分不正确"}. +{"Improper message type","消息类型不正确"}. +{"Incorrect CAPTCHA submit","提交的验证码不正确"}. +{"Incorrect data form","数据表单不正确"}. {"Incorrect password","密码不正确"}. -{"Invalid affiliation: ~s","无效加入: ~s"}. -{"Invalid role: ~s","无效角色: ~s"}. +{"Incorrect value of 'action' attribute","“action”属性的值不正确"}. +{"Incorrect value of 'action' in data form","数据表单中“action”的值不正确"}. +{"Incorrect value of 'path' in data form","数据表单中“path”的值不正确"}. +{"Installed Modules:","已安装的模块:"}. +{"Install","安装"}. +{"Insufficient privilege","权限不足"}. +{"Internal server error","内部服务器错误"}. +{"Invalid 'from' attribute in forwarded message","转发消息中的“from”属性无效"}. +{"Invalid node name","节点名称无效"}. +{"Invalid 'previd' value","“previd”值无效"}. +{"Invitations are not allowed in this conference","此会议不允许邀请"}. {"IP addresses","IP 地址"}. -{"IP","IP"}. -{"IRC channel (don't put the first #)","IRC 频道 (不要输入第一个#号)"}. -{"IRC server","IRC 服务器"}. -{"IRC settings","IRC 设置"}. -{"IRC Transport","IRC 传输"}. -{"IRC username","IRC 用户名"}. -{"IRC Username","IRC 用户名"}. -{"is now known as","现在称呼为"}. -{"It is not allowed to send private messages of type \"groupchat\"","\"群组聊天\"类型不允许发送私聊消息"}. -{"It is not allowed to send private messages to the conference","不允许向会议发送私聊消息"}. -{"It is not allowed to send private messages","不可以发送私聊消息"}. -{"Jabber Account Registration","Jabber 帐户注册"}. +{"is now known as","现在昵称为"}. +{"It is not allowed to send error messages to the room. The participant (~s) has sent an error message (~s) and got kicked from the room","不允许向此房间发送错误消息。参与者(~s)发送了错误消息(~s),被踢出了房间"}. +{"It is not allowed to send private messages of type \"groupchat\"","不允许发送“groupchat”类型的私信"}. +{"It is not allowed to send private messages to the conference","不允许向会议发送私信"}. {"Jabber ID","Jabber ID"}. -{"Jabber ID ~s is invalid","Jabber ID ~s 无效"}. {"January","一月"}. -{"Join IRC channel","加入 IRC 频道"}. +{"JID normalization denied by service policy","服务策略拒绝 JID 规范化"}. +{"JID normalization failed","JID 规范化失败"}. +{"Joined MIX channels of ~ts","加入了 ~ts 的 MIX 频道"}. +{"Joined MIX channels:","加入了 MIX 频道:"}. {"joins the room","加入房间"}. -{"Join the IRC channel here.","在这里加入 IRC 频道."}. -{"Join the IRC channel in this Jabber ID: ~s","用此 Jabber ID: ~s 加入 IRC 频道"}. {"July","七月"}. {"June","六月"}. +{"Just created","刚刚创建"}. {"Last Activity","上次活动"}. -{"Last login","上次登陆"}. +{"Last login","上次登录"}. +{"Last message","最后一条消息"}. {"Last month","上个月"}. -{"Last year","上一年"}. +{"Last year","去年"}. +{"Least significant bits of SHA-256 hash of text should equal hexadecimal label","文本的 SHA-256 散列的最低有效位应等于十六进制标签"}. {"leaves the room","离开房间"}. -{"Listened Ports at ","监听的端口位于 "}. -{"Listened Ports","被监听的端口"}. -{"List of modules to start","要启动的模块列表"}. -{"Low level update script","低级别更新脚本"}. -{"Make participants list public","公开参与人列表"}. -{"Make room CAPTCHA protected","保护房间验证码"}. -{"Make room members-only","设置房间只接收会员"}. -{"Make room moderated","设置房间只接收主持人"}. -{"Make room password protected","进入此房间需要密码"}. -{"Make room persistent","永久保存该房间"}. -{"Make room public searchable","使房间可被公开搜索"}. +{"List of users with hats","有头衔的用户列表"}. +{"List users with hats","列出有头衔的用户"}. +{"Logged Out","已登出"}. +{"Logging","日志记录"}. +{"Make participants list public","公开参与者列表"}. +{"Make room CAPTCHA protected","启用房间验证码保护"}. +{"Make room members-only","将房间设为仅成员"}. +{"Make room moderated","启用房间发言审核"}. +{"Make room password protected","启用房间密码保护"}. +{"Make room persistent","将房间设为持久"}. +{"Make room public searchable","将房间设为可公开搜索"}. +{"Malformed username","用户名格式不正确"}. +{"MAM preference modification denied by service policy","服务策略拒绝修改 MAM 首选项"}. {"March","三月"}. -{"Maximum Number of Occupants","允许的与会人最大数"}. -{"Max # of items to persist","允许持久化的最大内容条目数"}. -{"Max payload size in bytes","最大有效负载字节数"}. +{"Max # of items to persist, or `max` for no specific limit other than a server imposed maximum","要保留的最大项目数 #,或 `max` 表示除服务器强制规定的最大值外无其他特定限制"}. +{"Max payload size in bytes","最大有效负载大小(字节)"}. +{"Maximum file size","最大文件大小"}. +{"Maximum Number of History Messages Returned by Room","房间返回的最大历史消息数"}. +{"Maximum number of items to persist","要保留的最大项目数"}. +{"Maximum Number of Occupants","最大使用者数"}. {"May","五月"}. -{"Membership is required to enter this room","进入此房间需要会员身份"}. -{"Members:","会员:"}. -{"Memorize your password, or write it in a paper placed in a safe place. In Jabber there isn't an automated way to recover your password if you forget it.","记住你的密码, 或将其记到纸上并放于安全位置. 如果你忘记了密码, Jabber 也没有自动恢复密码的方式."}. -{"Memory","内存"}. -{"Message body","消息主体"}. +{"Membership is required to enter this room","进入此房间需要成员资格"}. +{"Memorize your password, or write it in a paper placed in a safe place. In XMPP there isn't an automated way to recover your password if you forget it.","请记住您的密码,或写在放在安全地方的纸上。在 XMPP 中,如果您忘记密码,没有自动恢复密码的方法。"}. +{"Mere Availability in XMPP (No Show Value)","XMPP 中的可用性(无显示值)"}. +{"Message body","消息正文"}. +{"Message not found in forwarded payload","在转发的有效负载中未找到消息"}. +{"Messages from strangers are rejected","拒绝来自陌生人的消息"}. +{"Messages of type headline","标题类型的消息"}. +{"Messages of type normal","普通类型的消息"}. {"Middle Name","中间名"}. -{"Minimum interval between voice requests (in seconds)","声音请求的最小间隔(以秒为单位)"}. +{"Minimum interval between voice requests (in seconds)","发言权请求的最短间隔时间(秒)"}. {"Moderator privileges required","需要主持人权限"}. -{"moderators only","仅主持人"}. -{"Modified modules","被修改模块"}. -{"Modules at ","模块位于 "}. -{"Modules","模块"}. -{"Module","模块"}. -{"Monday","星期一"}. -{"Name:","姓名:"}. -{"Name","姓名"}. -{"Never","从未"}. -{"New Password:","新密码: "}. +{"Moderators Only","仅主持人"}. +{"Moderator","主持人"}. +{"Module failed to handle the query","模块无法处理查询"}. +{"Monday","周一"}. +{"Multicast","多播"}. +{"Multiple elements are not allowed by RFC6121","按照 RFC6121,多个 元素是不允许的"}. +{"Multi-User Chat","多用户聊天"}. +{"Name","名称"}. +{"Natural Language for Room Discussions","房间讨论的自然语言"}. +{"Natural-Language Room Name","自然语言房间名称"}. +{"Neither 'jid' nor 'nick' attribute found","未找到“jid”和“nick”属性"}. +{"Neither 'role' nor 'affiliation' attribute found","未找到“role”或“affiliation”属性"}. +{"Never","从不"}. +{"New Password:","新密码:"}. +{"Nickname can't be empty","昵称不能为空"}. {"Nickname Registration at ","昵称注册于 "}. -{"Nickname ~s does not exist in the room","昵称 ~s 不在该房间"}. +{"Nickname ~s does not exist in the room","昵称 ~s 在房间中不存在"}. {"Nickname","昵称"}. -{"No body provided for announce message","通知消息无正文内容"}. -{"nobody","没有人"}. -{"No Data","没有数据"}. +{"No address elements found","未找到地址元素"}. +{"No addresses element found","未找到地址元素"}. +{"No 'affiliation' attribute found","未找到“affiliation”属性"}. +{"No available resource found","未找到可用资源"}. +{"No body provided for announce message","未提供公告消息正文"}. +{"No child elements found","未找到子元素"}. +{"No data form found","未找到数据表单"}. +{"No Data","无数据"}. +{"No features available","无可用功能"}. +{"No element found","未找到 元素"}. +{"No hook has processed this command","没有钩子处理此命令"}. +{"No info about last activity found","未找到有关上次活动的信息"}. +{"No 'item' element found","未找到“item”元素"}. +{"No items found in this query","在此查询中未找到任何项目"}. +{"No limit","无限制"}. +{"No module is handling this query","没有模块正在处理此查询"}. +{"No node specified","未指定节点"}. +{"No 'password' found in data form","在数据表单中未找到“password”"}. +{"No 'password' found in this query","在此查询中未找到“password”"}. +{"No 'path' found in data form","在数据表单中未找到“path”"}. +{"No pending subscriptions found","未找到待处理的订阅"}. +{"No privacy list with this name found","未找到具有此名称的隐私列表"}. +{"No private data found in this query","在此查询中未找到专用数据"}. +{"No running node found","未找到正在运行的节点"}. +{"No services available","无可用服务"}. +{"No statistics found for this item","未找到此项目的统计数据"}. +{"No 'to' attribute found in the invitation","邀请中未找到“to”属性"}. +{"Nobody","没有人"}. +{"Node already exists","节点已存在"}. {"Node ID","节点 ID"}. -{"Node not found","没有找到节点"}. +{"Node index not found","未找到节点索引"}. +{"Node not found","未找到节点"}. +{"Node ~p","节点 ~p"}. +{"Nodeprep has failed","Nodeprep 失败了"}. {"Nodes","节点"}. -{"Node ","节点 "}. -{"No limit","不限"}. +{"Node","节点"}. {"None","无"}. -{"No resource provided","无资源提供"}. -{"Not Found","没有找到"}. -{"Notify subscribers when items are removed from the node","当从节点删除内容条目时通知订阅人"}. -{"Notify subscribers when the node configuration changes","当节点设置改变时通知订阅人"}. -{"Notify subscribers when the node is deleted","当节点被删除时通知订阅人"}. +{"Not allowed","不允许"}. +{"Not Found","未找到"}. +{"Not subscribed","未订阅"}. +{"Notify subscribers when items are removed from the node","从节点中移除项目时通知订阅者"}. +{"Notify subscribers when the node configuration changes","节点配置更改时通知订阅者"}. +{"Notify subscribers when the node is deleted","删除节点时通知订阅者"}. {"November","十一月"}. -{"Number of occupants","驻留人数"}. +{"Number of answers required","所需答案数"}. +{"Number of occupants","使用者数"}. +{"Number of Offline Messages","离线消息数"}. {"Number of online users","在线用户数"}. {"Number of registered users","注册用户数"}. +{"Number of seconds after which to automatically purge items, or `max` for no specific limit other than a server imposed maximum","自动清除项目前的秒数,`max` 表示除服务器强制规定的最大值外无其他特定限制"}. +{"Occupants are allowed to invite others","允许使用者邀请他人"}. +{"Occupants are allowed to query others","允许使用者查询他人"}. +{"Occupants May Change the Subject","使用者可以更改主题"}. {"October","十月"}. -{"Offline Messages:","离线消息:"}. -{"Offline Messages","离线消息"}. {"OK","确定"}. -{"Old Password:","旧密码: "}. -{"Online Users:","在线用户:"}. +{"Old Password:","旧密码:"}. {"Online Users","在线用户"}. {"Online","在线"}. -{"Only deliver notifications to available users","仅将通知发送给可发送的用户"}. -{"Only moderators and participants are allowed to change the subject in this room","只有主持人和参与人可以在此房间里更改主题"}. -{"Only moderators are allowed to change the subject in this room","只有主持人可以在此房间里更改主题"}. -{"Only moderators can approve voice requests","仅主持人能确认声音请求"}. -{"Only occupants are allowed to send messages to the conference","只有与会人可以向大会发送消息"}. -{"Only occupants are allowed to send queries to the conference","只有与会人可以向大会发出查询请求"}. -{"Only service administrators are allowed to send service messages","只有服务管理员可以发送服务消息"}. -{"Options","选项"}. +{"Only collection node owners may associate leaf nodes with the collection","只有集合节点所有者可以将叶节点与集合关联"}. +{"Only deliver notifications to available users","仅向在线用户发送通知"}. +{"Only or tags are allowed","仅允许 标签"}. +{"Only element is allowed in this query","此查询中只允许 元素"}. +{"Only members may query archives of this room","只有成员才能查询此房间的归档"}. +{"Only moderators and participants are allowed to change the subject in this room","只允许主持人和参与者更改此房间的主题"}. +{"Only moderators are allowed to change the subject in this room","只允许主持人更改此房间的主题"}. +{"Only moderators are allowed to retract messages","只允许主持人撤回消息"}. +{"Only moderators can approve voice requests","只有主持人可以批准发言权请求"}. +{"Only occupants are allowed to send messages to the conference","只允许使用者向会议发送消息"}. +{"Only occupants are allowed to send queries to the conference","只允许使用者向会议发送查询"}. +{"Only publishers may publish","只有发布者才能发布"}. +{"Only service administrators are allowed to send service messages","只允许服务管理员发送服务消息"}. +{"Only those on a whitelist may associate leaf nodes with the collection","只有白名单上的那些可以将叶节点与集合关联"}. +{"Only those on a whitelist may subscribe and retrieve items","只有白名单上的那些才可以订阅和检索项目"}. {"Organization Name","组织名称"}. {"Organization Unit","组织单位"}. -{"Outgoing s2s Connections:","出站 s2s 连接:"}. -{"Outgoing s2s Connections","出站 s2s 连接"}. -{"Outgoing s2s Servers:","出站 s2s 服务器"}. -{"Owner privileges required","需要持有人权限"}. -{"Packet","数据包"}. -{"Password ~b","~b 的密码"}. -{"Password Verification:","密码确认:"}. -{"Password Verification","确认密码"}. -{"Password:","密码:"}. +{"Other Modules Available:","其他可用模块:"}. +{"Outgoing s2s Connections","传出 s2s 连接"}. +{"Owner privileges required","需要所有者权限"}. +{"Packet relay is denied by service policy","服务策略拒绝数据包中继"}. +{"Participant ID","参与者 ID"}. +{"Participant","参与者"}. +{"Password Verification","密码验证"}. +{"Password Verification:","密码验证:"}. {"Password","密码"}. -{"Path to Dir","目录的路径"}. +{"Password:","密码:"}. +{"Path to Dir","目录路径"}. {"Path to File","文件路径"}. -{"Pending","挂起"}. -{"Period: ","持续时间: "}. -{"Persist items to storage","持久化内容条目"}. +{"Payload semantic type information","有效负载语义类型信息"}. +{"Period: ","时段: "}. +{"Persist items to storage","将项目保留到存储"}. +{"Persistent","持久"}. +{"Ping query is incorrect","Ping 查询不正确"}. {"Ping","Ping"}. -{"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","注意:这些选项仅将备份内置的 Mnesia 数据库. 如果您正在使用 ODBC 模块, 您还需要分别备份您的数据库."}. -{"Please, wait for a while before sending new voice request","请稍后再发送新的声音请求"}. +{"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","注意:这些选项只会备份内置的 Mnesia 数据库。如果使用 ODBC 模块,还需要单独备份 SQL 数据库。"}. +{"Please, wait for a while before sending new voice request","请稍候再发送新的发言权请求"}. {"Pong","Pong"}. -{"Port ~b","~b 的端口"}. -{"Port","端口"}. +{"Possessing 'ask' attribute is not allowed by RFC6121","按照 RFC6121, 不允许有“ask”属性"}. {"Present real Jabber IDs to","将真实 Jabber ID 显示给"}. -{"private, ","保密, "}. -{"Protocol","协议"}. -{"Publish-Subscribe","发行-订阅"}. -{"PubSub subscriber request","PubSub 订阅人请求"}. -{"Purge all items when the relevant publisher goes offline","相关发布人离线后清除所有选项"}. -{"Queries to the conference members are not allowed in this room","本房间不可以查询会议成员信息"}. -{"RAM and disc copy","内存与磁盘复制"}. -{"RAM copy","内存(RAM)复制"}. -{"Raw","原始格式"}. -{"Really delete message of the day?","确实要删除每日消息吗?"}. -{"Recipient is not in the conference room","接收人不在会议室"}. -{"Register a Jabber account","注册 Jabber 帐户"}. -{"Registered Users:","注册用户:"}. -{"Registered Users","注册用户"}. +{"Previous session not found","未找到上一个会话"}. +{"Previous session PID has been killed","上一个会话 PID 已终止"}. +{"Previous session PID has exited","上一个会话 PID 已退出"}. +{"Previous session PID is dead","上一个会话 PID 已失效"}. +{"Previous session timed out","上一个会话超时"}. +{"private, ","私人, "}. +{"Public","公开"}. +{"Publish model","发布模型"}. +{"Publish-Subscribe","发布–订阅"}. +{"PubSub subscriber request","PubSub 订阅者请求"}. +{"Purge all items when the relevant publisher goes offline","相关发布者离线后清除所有项目"}. +{"Push record not found","未找到推送记录"}. +{"Queries to the conference members are not allowed in this room","此房间不允许向会议成员查询"}. +{"Query to another users is forbidden","禁止查询其他用户"}. +{"RAM and disc copy","RAM 和磁盘副本"}. +{"RAM copy","RAM 副本"}. +{"Really delete message of the day?","是否确定删除每日消息?"}. +{"Receive notification from all descendent nodes","接收所有后代节点的通知"}. +{"Receive notification from direct child nodes only","仅接收直接子节点的通知"}. +{"Receive notification of new items only","仅接收新项目的通知"}. +{"Receive notification of new nodes only","仅接收新节点的通知"}. +{"Recipient is not in the conference room","接收者不在会议室"}. +{"Register an XMPP account","注册 XMPP 账号"}. {"Register","注册"}. -{"Registration in mod_irc for ","mod_irc 中的注册是为 "}. -{"Remote copy","远程复制"}. -{"Remove All Offline Messages","移除所有离线消息"}. -{"Remove User","删除用户"}. -{"Remove","移除"}. -{"Replaced by new connection","被新的连接替换"}. +{"Remote copy","远程副本"}. +{"Remove a hat from a user","移除用户头衔"}. +{"Remove User","移除用户"}. +{"Replaced by new connection","替换为新连接"}. +{"Request has timed out","请求已超时"}. +{"Request is ignored","请求被忽略"}. +{"Requested role","请求的角色"}. {"Resources","资源"}. {"Restart Service","重启服务"}. -{"Restart","重启"}. -{"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:","立即恢复普通文本备份:"}. +{"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:","立即恢复纯文本备份:"}. {"Restore","恢复"}. +{"Result","结果"}. +{"Roles and Affiliations that May Retrieve Member List","可以检索成员列表的角色和从属关系"}. +{"Roles for which Presence is Broadcasted","广播在线状态的角色"}. +{"Roles that May Send Private Messages","可以发送私信的角色"}. {"Room Configuration","房间配置"}. -{"Room creation is denied by service policy","创建房间被服务策略拒绝"}. +{"Room creation is denied by service policy","服务策略拒绝创建房间"}. {"Room description","房间描述"}. -{"Room Occupants","房间人数"}. +{"Room Occupants","房间使用者"}. +{"Room terminates","房间终止"}. {"Room title","房间标题"}. -{"Roster groups allowed to subscribe","允许订阅的花名册组"}. -{"Roster of ","花名册属于 "}. -{"Roster size","花名册大小"}. -{"Roster","花名册"}. -{"RPC Call Error","RPC 调用错误"}. -{"Running Nodes","运行中的节点"}. -{"~s access rule configuration","~s 访问规则配置"}. -{"Saturday","星期六"}. -{"Script check","脚本检查"}. -{"Search Results for ","搜索结果属于关键词 "}. -{"Search users in ","搜索用户于 "}. -{"Send announcement to all online users on all hosts","发送通知给所有主机的在线用户"}. -{"Send announcement to all online users","发送通知给所有在线用户"}. -{"Send announcement to all users on all hosts","发送通知给所有主机上的所有用户"}. -{"Send announcement to all users","发送通知给所有用户"}. +{"Roster groups allowed to subscribe","允许订阅的名册组"}. +{"Roster size","名册大小"}. +{"Running Nodes","正在运行的节点"}. +{"~s invites you to the room ~s","~s 邀请您加入房间 ~s"}. +{"Saturday","周六"}. +{"Search from the date","从日期搜索"}. +{"Search Results for ","搜索结果 "}. +{"Search the text","搜索文本"}. +{"Search until the date","搜索截至日期"}. +{"Search users in ","在以下位置搜索用户 "}. +{"Send announcement to all online users on all hosts","向所有主机上的所有在线用户发送公告"}. +{"Send announcement to all online users","向所有在线用户发送公告"}. +{"Send announcement to all users on all hosts","向所有主机上的所有用户发送公告"}. +{"Send announcement to all users","向所有用户发送公告"}. {"September","九月"}. -{"Server ~b","服务器 ~b"}. -{"Server:","服务器:"}. -{"Set message of the day and send to online users","设定每日消息并发送给所有在线用户"}. -{"Set message of the day on all hosts and send to online users","设置所有主机上的每日消息并发送给在线用户"}. -{"Shared Roster Groups","共享的花名册组群"}. -{"Show Integral Table","显示完整列表"}. -{"Show Ordinary Table","显示普通列表"}. +{"Server:","服务器:"}. +{"Service list retrieval timed out","服务列表检索超时"}. +{"Session state copying timed out","会话状态复制超时"}. +{"Set message of the day and send to online users","设置每日消息并发送给在线用户"}. +{"Set message of the day on all hosts and send to online users","在所有主机上设置每日消息并发送给在线用户"}. +{"Shared Roster Groups","共享名册组"}. +{"Show Integral Table","显示完整表"}. +{"Show Occupants Join/Leave","显示使用者加入/离开"}. +{"Show Ordinary Table","显示普通表"}. {"Shut Down Service","关闭服务"}. -{"~s invites you to the room ~s","~s 邀请你到 ~s 房间"}. -{"Some Jabber clients can store your password in your computer. Use that feature only if you trust your computer is safe.","某些 Jabber 客户端可以在你的计算机里存储密码. 请仅在你确认你的计算机安全的情况下使用该功能."}. -{"Specify the access model","指定访问范例"}. +{"SOCKS5 Bytestreams","SOCKS5 字节流"}. +{"Some XMPP clients can store your password in the computer, but you should do this only in your personal computer for safety reasons.","某些 XMPP 客户端可以将您的密码存储在计算机中,但出于安全考虑,您应该仅在个人计算机中存储密码。"}. +{"Sources Specs:","源规格:"}. +{"Specify the access model","指定访问模型"}. {"Specify the event message type","指定事件消息类型"}. -{"Specify the publisher model","指定发布人范例"}. -{"~s's Offline Messages Queue","~s 的离线消息队列"}. -{"Start Modules at ","要启动的模块位于 "}. -{"Start Modules","启动模块"}. -{"Start","开始"}. -{"Statistics of ~p","~p 的统计"}. -{"Statistics","统计"}. -{"Stop Modules at ","要停止的模块位于 "}. -{"Stop Modules","停止模块"}. -{"Stopped Nodes","已经停止的节点"}. -{"Stop","停止"}. -{"Storage Type","存储类型"}. -{"Store binary backup:","存储为二进制备份:"}. -{"Store plain text backup:","存储为普通文本备份:"}. -{"Subject","标题"}. +{"Specify the publisher model","指定发布者模型"}. +{"Stanza id is not valid","节 ID 无效"}. +{"Stanza ID","节 ID"}. +{"Statically specify a replyto of the node owner(s)","静态指定节点所有者的 replyto"}. +{"Stopped Nodes","已停止的节点"}. +{"Store binary backup:","存储二进制备份:"}. +{"Store plain text backup:","存储纯文本备份:"}. +{"Stream management is already enabled","已启用流管理"}. +{"Stream management is not enabled","未启用流管理"}. +{"Subject","主题"}. {"Submitted","已提交"}. -{"Submit","提交"}. -{"Subscriber Address","订阅人地址"}. -{"Subscription","订阅"}. -{"Sunday","星期天"}. -{"That nickname is already in use by another occupant","该昵称已被另一用户使用"}. -{"That nickname is registered by another person","该昵称已被另一个人注册了"}. -{"The CAPTCHA is valid.","验证码有效."}. -{"The CAPTCHA verification has failed","验证码检查失败"}. -{"The collections with which a node is affiliated","加入结点的集合"}. -{"The password is too weak","密码强度太弱"}. +{"Subscriber Address","订阅者地址"}. +{"Subscribers may publish","订阅者可以发布"}. +{"Subscription requests must be approved and only subscribers may retrieve items","订阅请求必须得到批准,只有订阅者才能检索项目"}. +{"Subscriptions are not allowed","不允许订阅"}. +{"Sunday","周日"}. +{"Text associated with a picture","与图片相关的文字"}. +{"Text associated with a sound","与声音相关的文字"}. +{"Text associated with a video","与视频相关的文字"}. +{"Text associated with speech","与语音相关的文字"}. +{"That nickname is already in use by another occupant","该昵称已被其他使用者使用"}. +{"That nickname is registered by another person","该昵称已被另一用户注册了"}. +{"The account already exists","此账号已存在"}. +{"The account was not unregistered","此账号未注销"}. +{"The body text of the last received message","最后收到的消息的正文"}. +{"The CAPTCHA is valid.","验证码有效。"}. +{"The CAPTCHA verification has failed","验证码验证失败"}. +{"The captcha you entered is wrong","您输入的验证码错误"}. +{"The child nodes (leaf or collection) associated with a collection","与集合关联的子节点(叶或集合)"}. +{"The collections with which a node is affiliated","节点所属的集合"}. +{"The DateTime at which a leased subscription will end or has ended","租赁订阅将结束或已结束的日期时间"}. +{"The datetime when the node was created","创建节点的日期时间"}. +{"The default language of the node","节点的默认语言"}. +{"The feature requested is not supported by the conference","会议不支持所请求的功能"}. +{"The JID of the node creator","节点创建者的 JID"}. +{"The JIDs of those to contact with questions","有疑问时需联系的人员的 JID"}. +{"The JIDs of those with an affiliation of owner","有所有者从属关系的人员的 JID"}. +{"The JIDs of those with an affiliation of publisher","有发布者从属关系的人员的 JID"}. +{"The list of all online users","所有在线用户的列表"}. +{"The list of all users","所有用户的列表"}. +{"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, or `max` for no specific limit other than a server imposed maximum","可以与集合关联的子节点的最大数量,或 `max` 表示除服务器强制规定的最大值外无其他特定限制"}. +{"The minimum number of milliseconds between sending any two notification digests","发送任意两个通知摘要之间的最小毫秒数"}. +{"The name of the node","节点的名称"}. +{"The node is a collection node","节点是集合节点"}. +{"The node is a leaf node (default)","节点是叶节点(默认)"}. +{"The NodeID of the relevant node","相关节点的 NodeID"}. +{"The number of pending incoming presence subscription requests","待处理的传入在线状态订阅请求数"}. +{"The number of subscribers to the node","节点的订阅者数"}. +{"The number of unread or undelivered messages","未读或未传递的消息数"}. +{"The password contains unacceptable characters","密码包含不可接受的字符"}. +{"The password is too weak","密码太弱"}. {"the password is","密码是"}. -{"The password of your Jabber account was successfully changed.","你的 Jabber 帐户密码已成功更新."}. -{"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 create a Jabber account in this Jabber server. Your JID (Jabber IDentifier) will be of the form: username@server. Please read carefully the instructions to fill correctly the fields.","本页面允许在此服务器上创建 Jabber 帐户. 你的 JID (Jabber ID) 的形式如下: 用户名@服务器. 请仔细阅读说明并正确填写相应字段."}. -{"This page allows to unregister a Jabber account in this Jabber server.","此页面允许在此 Jabber 服务器上注销 Jabber 帐户"}. -{"This participant is kicked from the room because he sent an error message to another participant","该参与人由于给其他人发送了出错消息而被踢出了聊天室"}. -{"This participant is kicked from the room because he sent an error message","该参与人由于发送了错误消息而被踢出了聊天室"}. -{"This participant is kicked from the room because he sent an error presence","该用户由于发送了错误状态而被踢出了聊天室"}. -{"This room is not anonymous","此房间不是匿名房间"}. -{"Thursday","星期四"}. +{"The password of your XMPP account was successfully changed.","您的 XMPP 账号密码已成功更改。"}. +{"The password was not changed","密码未更改"}. +{"The passwords are different","密码不同"}. +{"The presence states for which an entity wants to receive notifications","实体要接收通知的在线状态"}. +{"The query is only allowed from local users","仅允许来自本地用户的查询"}. +{"The query must not contain elements","查询不能包含 元素"}. +{"The room subject can be modified by participants","参与者可以修改房间主题"}. +{"The semantic type information of data in the node, usually specified by the namespace of the payload (if any)","节点中数据的语义类型信息,通常由有效负载的命名空间指定(如果有)"}. +{"The sender of the last received message","最后收到的消息的发送者"}. +{"The stanza MUST contain only one element, one element, or one element","节必须仅包含一个 元素、一个 元素或一个 元素"}. +{"The subscription identifier associated with the subscription request","与订阅请求关联的订阅标识符"}. +{"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,可以将其应用于有效负载格式,以生成有效的数据表单结果,客户端可以使用通用数据表单呈现引擎来显示该结果"}. +{"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 room is not anonymous","此房间是非匿名的"}. +{"This service can not process the address: ~s","此服务无法处理地址:~s"}. +{"Thursday","周四"}. {"Time delay","时间延迟"}. -{"Time","时间"}. +{"Timed out waiting for stream resumption","等待流恢复超时"}. +{"To register, visit ~s","要注册,请访问 ~s"}. +{"To ~ts","到 ~ts"}. +{"Token TTL","令牌 TTL"}. +{"Too many active bytestreams","活动字节流太多"}. {"Too many CAPTCHA requests","验证码请求太多"}. -{"To ~s","发送给~s"}. -{"To","到"}. -{"Traffic rate limit is exceeded","已经超过传输率限制"}. -{"Transactions Aborted:","取消的事务:"}. -{"Transactions Committed:","提交的事务:"}. -{"Transactions Logged:","记入日志的事务:"}. -{"Transactions Restarted:","重启的事务:"}. -{"Tuesday","星期二"}. +{"Too many child elements","子元素太多"}. +{"Too many elements"," 元素太多"}. +{"Too many elements"," 元素太多"}. +{"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","有太多(~p)失败的身份验证来自此 IP 地址(~s),将在 UTC 时间 ~s 取消对该地址的屏蔽"}. +{"Too many receiver fields were specified","指定的接收者字段太多"}. +{"Too many unacked stanzas","未确认的节太多"}. +{"Too many users in this conference","此会议中的用户太多"}. +{"Traffic rate limit is exceeded","超过流量速率限制"}. +{"~ts's MAM Archive","~ts 的 MAM 归档"}. +{"~ts's Offline Messages Queue","~ts 的离线消息队列"}. +{"Tuesday","周二"}. {"Unable to generate a CAPTCHA","无法生成验证码"}. -{"Unauthorized","未认证的"}. -{"Unregister a Jabber account","注销 Jabber 帐户"}. -{"Unregister","取消注册"}. -{"Update message of the day (don't send)","更新每日消息(不发送)"}. -{"Update message of the day on all hosts (don't send)","更新所有主机上的每日消息(不发送)"}. -{"Update plan","更新计划"}. -{"Update script","更新脚本"}. -{"Update ","更新 "}. -{"Update","更新"}. -{"Uptime:","正常运行时间:"}. -{"Use of STARTTLS required","要求使用 STARTTLS"}. +{"Unable to register route on existing local domain","无法在现有本地域上注册路由"}. +{"Unauthorized","未经授权"}. +{"Unexpected action","意外操作"}. +{"Unexpected error condition: ~p","意外错误条件:~p"}. +{"Uninstall","卸载"}. +{"Unregister an XMPP account","注销 XMPP 账号"}. +{"Unregister","注销"}. +{"Unsupported element","不支持的 元素"}. +{"Unsupported version","不支持的版本"}. +{"Update message of the day (don't send)","更新每日消息(不发送)"}. +{"Update message of the day on all hosts (don't send)","更新所有主机上的每日消息(不发送)"}. +{"Update specs to get modules source, then install desired ones.","更新规格以获取模块源,然后安装所需的模块。"}. +{"Update Specs","更新规格"}. +{"Updating the vCard is not supported by the vCard storage backend","vCard 存储后端不支持更新 vCard"}. +{"Upgrade","升级"}. +{"URL for Archived Discussion Logs","已归档的讨论日志 URL"}. +{"User already exists","用户已存在"}. {"User JID","用户 JID"}. +{"User (jid)","用户(JID)"}. {"User Management","用户管理"}. -{"Username:","用户名:"}. -{"Users are not allowed to register accounts so quickly","不允许用户太频繁地注册帐户"}. +{"User not allowed to perform an IQ set on another user's vCard.","不允许用户在其他用户的 vCard 上执行 IQ 设置。"}. +{"User removed","用户已移除"}. +{"User session not found","未找到用户会话"}. +{"User session terminated","用户会话已终止"}. +{"User ~ts","用户 ~ts"}. +{"Username:","用户名:"}. +{"Users are not allowed to register accounts so quickly","不允许用户太频繁地注册账号"}. {"Users Last Activity","用户上次活动"}. {"Users","用户"}. -{"User ","用户 "}. {"User","用户"}. -{"Validate","确认"}. +{"Value 'get' of 'type' attribute is not allowed","不允许“type”属性的“get”值"}. +{"Value of '~s' should be boolean","“~s”的值应为布尔值"}. +{"Value of '~s' should be datetime string","“~s”的值应为日期时间字符串"}. +{"Value of '~s' should be integer","“~s”的值应为整数"}. +{"Value 'set' of 'type' attribute is not allowed","不允许“type”属性的“set”值"}. {"vCard User Search","vCard 用户搜索"}. +{"View joined MIX channels","查看已加入的 MIX 频道"}. {"Virtual Hosts","虚拟主机"}. -{"Visitors are not allowed to change their nicknames in this room","此房间不允许用户更改昵称"}. -{"Visitors are not allowed to send messages to all occupants","不允许访客给所有占有者发送消息"}. -{"Voice requests are disabled in this conference","该会议的声音请求以被禁用"}. -{"Voice request","声音请求"}. -{"Wednesday","星期三"}. -{"When to send the last published item","何时发送最新发布的内容条目"}. +{"Visitors are not allowed to change their nicknames in this room","不允许参观者在此房间中更改其昵称"}. +{"Visitors are not allowed to send messages to all occupants","不允许参观者向所有使用者发送消息"}. +{"Visitor","参观者"}. +{"Voice requests are disabled in this conference","此会议中禁用了发言权请求"}. +{"Voice request","发言权请求"}. +{"Web client which allows to join the room anonymously","允许匿名加入房间的 Web 客户端"}. +{"Wednesday","周三"}. +{"When a new subscription is processed and whenever a subscriber comes online","处理新订阅时和订阅者上线时"}. +{"When a new subscription is processed","处理新订阅时"}. +{"When to send the last published item","何时发送最后发布的项目"}. +{"Whether an entity wants to receive an XMPP message body in addition to the payload format","除有效负载格式外,实体是否还希望接收 XMPP 消息正文"}. +{"Whether an entity wants to receive digests (aggregations) of notifications or all notifications individually","实体是要接收通知摘要(汇总) 还是要单独接收所有通知"}. +{"Whether an entity wants to receive or disable notifications","实体是否要接收或禁用通知"}. +{"Whether owners or publisher should receive replies to items","所有者或发布者是否应收到对项目的回复"}. +{"Whether the node is a leaf (default) or a collection","节点是叶(默认)还是集合"}. {"Whether to allow subscriptions","是否允许订阅"}. -{"You can later change your password using a Jabber client.","你可以稍后用 Jabber 客户端修改你的密码."}. -{"You have been banned from this room","您已被禁止进入该房间"}. -{"You must fill in field \"Nickname\" in the form","您必须填充表单中\"昵称\"项"}. -{"You need a client that supports x:data and CAPTCHA to register","您需要一个支持 x:data 和验证码的客户端进行注册"}. -{"You need a client that supports x:data to register the nickname","您需要一个支持 x:data 的客户端来注册昵称"}. -{"You need an x:data capable client to configure mod_irc settings","您需要一个兼容 x:data 的客户端来配置 mod_irc 设置"}. -{"You need an x:data capable client to configure room","您需要一个兼容 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 Jabber account was successfully created.","你的 Jabber 帐户已成功创建."}. -{"Your Jabber account was successfully deleted.","你的 Jabber 帐户已成功删除."}. -{"Your messages to ~s are being blocked. To unblock them, visit ~s","您发送给 ~s 的消息已被阻止. 要解除阻止, 请访问 ~s"}. +{"Whether to make all subscriptions temporary, based on subscriber presence","是否根据订阅者的在线状态将所有订阅设为临时订阅"}. +{"Whether to notify owners about new subscribers and unsubscribes","是否通知所有者新的订阅者和退订者"}. +{"Who can send private messages","谁可以发送私信"}. +{"Who may associate leaf nodes with a collection","谁可以将叶节点与集合关联"}. +{"Wrong parameters in the web formulary","Web 表单集中的参数错误"}. +{"Wrong xmlns","错误的 xmlns"}. +{"XMPP Account Registration","XMPP 账号注册"}. +{"XMPP Domains","XMPP 域"}. +{"XMPP Show Value of Away","XMPP 的离开显示值"}. +{"XMPP Show Value of Chat","XMPP 的聊天显示值"}. +{"XMPP Show Value of DND (Do Not Disturb)","XMPP 的 DND(请勿打扰)显示值"}. +{"XMPP Show Value of XA (Extended Away)","XMPP 的 XA(延长离开)显示值"}. +{"XMPP URI of Associated Publish-Subscribe Node","关联发布–订阅节点的 XMPP URI"}. +{"You are being removed from the room because of a system shutdown","由于系统关闭,您将被移出房间"}. +{"You are not allowed to send private messages","不允许您发送私信"}. +{"You are not joined to the channel","您未加入频道"}. +{"You can later change your password using an XMPP client.","您之后可以使用 XMPP 客户端更改密码。"}. +{"You have been banned from this room","禁止您进入此房间"}. +{"You have joined too many conferences","您加入了太多会议"}. +{"You must fill in field \"Nickname\" in the form","您必须在表单中填写“昵称”字段"}. +{"You need a client that supports x:data and CAPTCHA to register","您需要支持 x:data 和验证码的客户端来注册"}. +{"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 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 账号注销成功。"}. +{"You're not allowed to create nodes","不允许您创建节点"}. diff --git a/priv/msgs/zh.po b/priv/msgs/zh.po deleted file mode 100644 index d1a76a2ee..000000000 --- a/priv/msgs/zh.po +++ /dev/null @@ -1,1836 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: 2.1.0-alpha\n" -"Last-Translator: Shelley Shyan - shelleyshyan AT gmail DOT com\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Language: Chinese (中文)\n" -"X-Additional-Translator: Zhan Caibao - zhancaibao AT gmail DOT com\n" -"X-Additional-Translator: Mike Wang\n" - -#: ejabberd_c2s.erl:424 ejabberd_c2s.erl:727 -msgid "Use of STARTTLS required" -msgstr "要求使用 STARTTLS" - -#: ejabberd_c2s.erl:503 -msgid "No resource provided" -msgstr "无资源提供" - -#: ejabberd_c2s.erl:1197 -msgid "Replaced by new connection" -msgstr "被新的连接替换" - -#: ejabberd_c2s.erl:1885 -msgid "Your active privacy list has denied the routing of this stanza." -msgstr "你的活跃私聊列表拒绝了在此房间进行路由分发." - -#: ejabberd_captcha.erl:96 ejabberd_captcha.erl:152 ejabberd_captcha.erl:178 -msgid "Enter the text you see" -msgstr "请输入您所看到的文本" - -#: ejabberd_captcha.erl:101 -msgid "Your messages to ~s are being blocked. To unblock them, visit ~s" -msgstr "您发送给 ~s 的消息已被阻止. 要解除阻止, 请访问 ~s" - -#: ejabberd_captcha.erl:134 -msgid "If you don't see the CAPTCHA image here, visit the web page." -msgstr "如果您在这里没有看到验证码图片, 请访问网页." - -#: ejabberd_captcha.erl:146 -msgid "CAPTCHA web page" -msgstr "验证码网页" - -#: ejabberd_captcha.erl:307 -msgid "The CAPTCHA is valid." -msgstr "验证码有效." - -#: mod_adhoc.erl:95 mod_adhoc.erl:125 mod_adhoc.erl:143 mod_adhoc.erl:161 -msgid "Commands" -msgstr "命令" - -#: mod_adhoc.erl:149 mod_adhoc.erl:243 -msgid "Ping" -msgstr "Ping" - -#: mod_adhoc.erl:260 -msgid "Pong" -msgstr "Pong" - -#: mod_announce.erl:507 mod_announce_odbc.erl:499 -msgid "Really delete message of the day?" -msgstr "确实要删除每日消息吗?" - -#: mod_announce.erl:515 mod_announce_odbc.erl:507 mod_configure.erl:1083 -#: mod_configure.erl:1128 -msgid "Subject" -msgstr "标题" - -#: mod_announce.erl:520 mod_announce_odbc.erl:512 mod_configure.erl:1088 -#: mod_configure.erl:1133 -msgid "Message body" -msgstr "消息主体" - -#: mod_announce.erl:600 mod_announce_odbc.erl:592 -msgid "No body provided for announce message" -msgstr "通知消息无正文内容" - -#: mod_announce.erl:635 mod_announce_odbc.erl:627 -msgid "Announcements" -msgstr "通知" - -#: mod_announce.erl:637 mod_announce_odbc.erl:629 -msgid "Send announcement to all users" -msgstr "发送通知给所有用户" - -#: mod_announce.erl:639 mod_announce_odbc.erl:631 -msgid "Send announcement to all users on all hosts" -msgstr "发送通知给所有主机上的所有用户" - -#: mod_announce.erl:641 mod_announce_odbc.erl:633 -msgid "Send announcement to all online users" -msgstr "发送通知给所有在线用户" - -#: mod_announce.erl:643 mod_announce_odbc.erl:635 mod_configure.erl:1078 -#: mod_configure.erl:1123 -msgid "Send announcement to all online users on all hosts" -msgstr "发送通知给所有主机的在线用户" - -#: mod_announce.erl:645 mod_announce_odbc.erl:637 -msgid "Set message of the day and send to online users" -msgstr "设定每日消息并发送给所有在线用户" - -#: mod_announce.erl:647 mod_announce_odbc.erl:639 -msgid "Set message of the day on all hosts and send to online users" -msgstr "设置所有主机上的每日消息并发送给在线用户" - -#: mod_announce.erl:649 mod_announce_odbc.erl:641 -msgid "Update message of the day (don't send)" -msgstr "更新每日消息(不发送)" - -#: mod_announce.erl:651 mod_announce_odbc.erl:643 -msgid "Update message of the day on all hosts (don't send)" -msgstr "更新所有主机上的每日消息(不发送)" - -#: mod_announce.erl:653 mod_announce_odbc.erl:645 -msgid "Delete message of the day" -msgstr "删除每日消息" - -#: mod_announce.erl:655 mod_announce_odbc.erl:647 -msgid "Delete message of the day on all hosts" -msgstr "删除所有主机上的每日消息" - -#: mod_configure.erl:114 mod_configure.erl:274 mod_configure.erl:296 -#: mod_configure.erl:498 -msgid "Configuration" -msgstr "配置" - -#: mod_configure.erl:125 mod_configure.erl:576 web/ejabberd_web_admin.erl:1936 -msgid "Database" -msgstr "数据库" - -#: mod_configure.erl:127 mod_configure.erl:595 -msgid "Start Modules" -msgstr "启动模块" - -#: mod_configure.erl:129 mod_configure.erl:596 -msgid "Stop Modules" -msgstr "停止模块" - -#: mod_configure.erl:131 mod_configure.erl:604 web/ejabberd_web_admin.erl:1937 -msgid "Backup" -msgstr "备份" - -#: mod_configure.erl:133 mod_configure.erl:605 -msgid "Restore" -msgstr "恢复" - -#: mod_configure.erl:135 mod_configure.erl:606 -msgid "Dump to Text File" -msgstr "转储到文本文件" - -#: mod_configure.erl:137 mod_configure.erl:615 -msgid "Import File" -msgstr "导入文件" - -#: mod_configure.erl:139 mod_configure.erl:616 -msgid "Import Directory" -msgstr "导入目录" - -#: mod_configure.erl:141 mod_configure.erl:581 mod_configure.erl:1057 -msgid "Restart Service" -msgstr "重启服务" - -#: mod_configure.erl:143 mod_configure.erl:582 mod_configure.erl:1102 -msgid "Shut Down Service" -msgstr "关闭服务" - -#: mod_configure.erl:145 mod_configure.erl:518 mod_configure.erl:1197 -#: web/ejabberd_web_admin.erl:1527 -msgid "Add User" -msgstr "添加用户" - -#: mod_configure.erl:147 mod_configure.erl:519 mod_configure.erl:1219 -msgid "Delete User" -msgstr "删除用户" - -#: mod_configure.erl:149 mod_configure.erl:520 mod_configure.erl:1231 -msgid "End User Session" -msgstr "结束用户会话" - -#: mod_configure.erl:151 mod_configure.erl:521 mod_configure.erl:1243 -#: mod_configure.erl:1255 -msgid "Get User Password" -msgstr "获取用户密码" - -#: mod_configure.erl:153 mod_configure.erl:522 -msgid "Change User Password" -msgstr "更改用户密码" - -#: mod_configure.erl:155 mod_configure.erl:523 mod_configure.erl:1272 -msgid "Get User Last Login Time" -msgstr "获取用户上次登陆时间" - -#: mod_configure.erl:157 mod_configure.erl:524 mod_configure.erl:1284 -msgid "Get User Statistics" -msgstr "获取用户统计" - -#: mod_configure.erl:159 mod_configure.erl:525 -msgid "Get Number of Registered Users" -msgstr "获取注册用户数" - -#: mod_configure.erl:161 mod_configure.erl:526 -msgid "Get Number of Online Users" -msgstr "获取在线用户数" - -#: mod_configure.erl:163 mod_configure.erl:509 web/ejabberd_web_admin.erl:831 -#: web/ejabberd_web_admin.erl:872 -msgid "Access Control Lists" -msgstr "访问控制列表(ACL)" - -#: mod_configure.erl:165 mod_configure.erl:510 web/ejabberd_web_admin.erl:940 -#: web/ejabberd_web_admin.erl:976 -msgid "Access Rules" -msgstr "访问规则" - -#: mod_configure.erl:297 mod_configure.erl:499 -msgid "User Management" -msgstr "用户管理" - -#: mod_configure.erl:500 web/ejabberd_web_admin.erl:1058 -#: web/ejabberd_web_admin.erl:1462 -msgid "Online Users" -msgstr "在线用户" - -#: mod_configure.erl:501 -msgid "All Users" -msgstr "所有用户" - -#: mod_configure.erl:502 -msgid "Outgoing s2s Connections" -msgstr "出站 s2s 连接" - -#: mod_configure.erl:503 web/ejabberd_web_admin.erl:1907 -msgid "Running Nodes" -msgstr "运行中的节点" - -#: mod_configure.erl:504 web/ejabberd_web_admin.erl:1909 -msgid "Stopped Nodes" -msgstr "已经停止的节点" - -#: mod_configure.erl:577 mod_configure.erl:587 web/ejabberd_web_admin.erl:1953 -msgid "Modules" -msgstr "模块" - -#: mod_configure.erl:578 -msgid "Backup Management" -msgstr "备份管理" - -#: mod_configure.erl:579 -msgid "Import Users From jabberd14 Spool Files" -msgstr "从 jabberd14 Spool 文件导入用户" - -#: mod_configure.erl:699 -msgid "To ~s" -msgstr "发送给~s" - -#: mod_configure.erl:717 -msgid "From ~s" -msgstr "来自~s" - -#: mod_configure.erl:913 -msgid "Database Tables Configuration at " -msgstr "数据库表格配置位于" - -#: mod_configure.erl:918 -msgid "Choose storage type of tables" -msgstr "请选择表格的存储类型" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Disc only copy" -msgstr "仅磁盘复制" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM and disc copy" -msgstr "内存与磁盘复制" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "RAM copy" -msgstr "内存(RAM)复制" - -#: mod_configure.erl:926 mod_configure.erl:928 -msgid "Remote copy" -msgstr "远程复制" - -#: mod_configure.erl:950 -msgid "Stop Modules at " -msgstr "要停止的模块位于 " - -#: mod_configure.erl:954 -msgid "Choose modules to stop" -msgstr "请选择要停止的模块" - -#: mod_configure.erl:969 -msgid "Start Modules at " -msgstr "要启动的模块位于 " - -#: mod_configure.erl:973 -msgid "Enter list of {Module, [Options]}" -msgstr "请输入{模块, [选项]}列表" - -#: mod_configure.erl:974 -msgid "List of modules to start" -msgstr "要启动的模块列表" - -#: mod_configure.erl:983 -msgid "Backup to File at " -msgstr "备份文件位于" - -#: mod_configure.erl:987 mod_configure.erl:1001 -msgid "Enter path to backup file" -msgstr "请输入备份文件的路径" - -#: mod_configure.erl:988 mod_configure.erl:1002 mod_configure.erl:1016 -#: mod_configure.erl:1030 -msgid "Path to File" -msgstr "文件路径" - -#: mod_configure.erl:997 -msgid "Restore Backup from File at " -msgstr "要恢复的备份文件位于" - -#: mod_configure.erl:1011 -msgid "Dump Backup to Text File at " -msgstr "转储备份到文本文件于" - -#: mod_configure.erl:1015 -msgid "Enter path to text file" -msgstr "请输入文本文件的路径" - -#: mod_configure.erl:1025 -msgid "Import User from File at " -msgstr "导入用户的文件位于 " - -#: mod_configure.erl:1029 -msgid "Enter path to jabberd14 spool file" -msgstr "请输入 jabberd14 spool 文件的路径" - -#: mod_configure.erl:1039 -msgid "Import Users from Dir at " -msgstr "导入用户的目录位于 " - -#: mod_configure.erl:1043 -msgid "Enter path to jabberd14 spool dir" -msgstr "请输入 jabberd14 spool 目录的路径" - -#: mod_configure.erl:1044 -msgid "Path to Dir" -msgstr "目录的路径" - -#: mod_configure.erl:1060 mod_configure.erl:1105 -msgid "Time delay" -msgstr "时间延迟" - -#: mod_configure.erl:1143 -msgid "Access Control List Configuration" -msgstr "访问控制列表(ACL)配置" - -#: mod_configure.erl:1147 -msgid "Access control lists" -msgstr "访问控制列表(ACL)" - -#: mod_configure.erl:1171 -msgid "Access Configuration" -msgstr "访问配置" - -#: mod_configure.erl:1175 -msgid "Access rules" -msgstr "访问规则" - -#: mod_configure.erl:1200 mod_configure.erl:1222 mod_configure.erl:1234 -#: mod_configure.erl:1246 mod_configure.erl:1258 mod_configure.erl:1275 -#: mod_configure.erl:1287 mod_configure.erl:1650 mod_configure.erl:1700 -#: mod_configure.erl:1721 mod_roster.erl:943 mod_roster_odbc.erl:1060 -#: mod_vcard.erl:472 mod_vcard_ldap.erl:554 mod_vcard_odbc.erl:448 -msgid "Jabber ID" -msgstr "Jabber ID" - -#: mod_configure.erl:1205 mod_configure.erl:1263 mod_configure.erl:1651 -#: mod_configure.erl:1863 mod_muc/mod_muc_room.erl:3224 mod_register.erl:235 -#: web/ejabberd_web_admin.erl:1520 -msgid "Password" -msgstr "密码" - -#: mod_configure.erl:1210 -msgid "Password Verification" -msgstr "确认密码" - -#: mod_configure.erl:1301 -msgid "Number of registered users" -msgstr "注册用户数" - -#: mod_configure.erl:1315 -msgid "Number of online users" -msgstr "在线用户数" - -#: mod_configure.erl:1682 web/ejabberd_web_admin.erl:1588 -#: web/ejabberd_web_admin.erl:1743 -msgid "Never" -msgstr "从未" - -#: mod_configure.erl:1696 web/ejabberd_web_admin.erl:1601 -#: web/ejabberd_web_admin.erl:1756 -msgid "Online" -msgstr "在线" - -#: mod_configure.erl:1701 -msgid "Last login" -msgstr "上次登陆" - -#: mod_configure.erl:1722 -msgid "Roster size" -msgstr "花名册大小" - -#: mod_configure.erl:1723 -msgid "IP addresses" -msgstr "IP 地址" - -#: mod_configure.erl:1724 -msgid "Resources" -msgstr "资源" - -#: mod_configure.erl:1850 -msgid "Administration of " -msgstr "管理" - -#: mod_configure.erl:1853 -msgid "Action on user" -msgstr "对用户的动作" - -#: mod_configure.erl:1857 -msgid "Edit Properties" -msgstr "编辑属性" - -#: mod_configure.erl:1860 web/ejabberd_web_admin.erl:1769 -msgid "Remove User" -msgstr "删除用户" - -#: mod_irc/mod_irc.erl:201 mod_irc/mod_irc_odbc.erl:196 -#: mod_muc/mod_muc.erl:336 mod_muc/mod_muc_odbc.erl:342 -msgid "Access denied by service policy" -msgstr "访问被服务策略拒绝" - -#: mod_irc/mod_irc.erl:401 mod_irc/mod_irc_odbc.erl:398 -msgid "IRC Transport" -msgstr "IRC 传输" - -#: mod_irc/mod_irc.erl:428 mod_irc/mod_irc_odbc.erl:425 -msgid "ejabberd IRC module" -msgstr "ejabberd IRC 模块" - -#: mod_irc/mod_irc.erl:559 mod_irc/mod_irc_odbc.erl:568 -msgid "You need an x:data capable client to configure mod_irc settings" -msgstr "您需要一个兼容 x:data 的客户端来配置 mod_irc 设置" - -#: mod_irc/mod_irc.erl:566 mod_irc/mod_irc_odbc.erl:575 -msgid "Registration in mod_irc for " -msgstr "mod_irc 中的注册是为 " - -#: mod_irc/mod_irc.erl:571 mod_irc/mod_irc_odbc.erl:580 -msgid "" -"Enter username, encodings, ports and passwords you wish to use for " -"connecting to IRC servers" -msgstr "请输入您想使用的用来连接到IRC服务器的用户名, 编码, 端口和密码." - -#: mod_irc/mod_irc.erl:576 mod_irc/mod_irc_odbc.erl:585 -msgid "IRC Username" -msgstr "IRC 用户名" - -#: mod_irc/mod_irc.erl:586 mod_irc/mod_irc_odbc.erl:595 -msgid "" -"If you want to specify different ports, passwords, encodings for IRC " -"servers, fill this list with values in format '{\"irc server\", \"encoding" -"\", port, \"password\"}'. By default this service use \"~s\" encoding, port " -"~p, empty password." -msgstr "" -"如果您想为 IRC 服务器指定不同的端口, 密码, 编码, 请用 '{\"irc 服务器\", \"编" -"码\", 端口, \"密码\"}' 格式的值填充此表单. 默认情况下此服务使用\"~s\"编码, " -"~p 端口, 密码为空." - -#: mod_irc/mod_irc.erl:598 mod_irc/mod_irc_odbc.erl:607 -msgid "" -"Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, {\"vendetta.fef." -"net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]." -msgstr "" -"例如: [{\"irc.lucky.net\", \"koi8-r\"}, 6667, \"secret\"}, {\"vendetta.fef." -"net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]." - -#: mod_irc/mod_irc.erl:603 mod_irc/mod_irc_odbc.erl:612 -msgid "Connections parameters" -msgstr "连接参数" - -#: mod_irc/mod_irc.erl:728 mod_irc/mod_irc_odbc.erl:757 -msgid "Join IRC channel" -msgstr "加入 IRC 频道" - -#: mod_irc/mod_irc.erl:732 mod_irc/mod_irc_odbc.erl:761 -msgid "IRC channel (don't put the first #)" -msgstr "IRC 频道 (不要输入第一个#号)" - -#: mod_irc/mod_irc.erl:737 mod_irc/mod_irc_odbc.erl:766 -msgid "IRC server" -msgstr "IRC 服务器" - -#: mod_irc/mod_irc.erl:770 mod_irc/mod_irc.erl:774 -#: mod_irc/mod_irc_odbc.erl:799 mod_irc/mod_irc_odbc.erl:803 -msgid "Join the IRC channel here." -msgstr "在这里加入 IRC 频道." - -#: mod_irc/mod_irc.erl:778 mod_irc/mod_irc_odbc.erl:807 -msgid "Join the IRC channel in this Jabber ID: ~s" -msgstr "用此 Jabber ID: ~s 加入 IRC 频道" - -#: mod_irc/mod_irc.erl:863 mod_irc/mod_irc_odbc.erl:904 -msgid "IRC settings" -msgstr "IRC 设置" - -#: mod_irc/mod_irc.erl:868 mod_irc/mod_irc_odbc.erl:909 -msgid "" -"Enter username and encodings you wish to use for connecting to IRC servers. " -"Press 'Next' to get more fields to fill in. Press 'Complete' to save " -"settings." -msgstr "" -"请输入您想使用的用来连接到 IRC 服务器的用户名和编码. 按 '下一步' 获取更多待填" -"字段. 按 '完成' 保存设置." - -#: mod_irc/mod_irc.erl:874 mod_irc/mod_irc_odbc.erl:915 -msgid "IRC username" -msgstr "IRC 用户名" - -#: mod_irc/mod_irc.erl:923 mod_irc/mod_irc_odbc.erl:964 -msgid "Password ~b" -msgstr "~b 的密码" - -#: mod_irc/mod_irc.erl:928 mod_irc/mod_irc_odbc.erl:969 -msgid "Port ~b" -msgstr "~b 的端口" - -#: mod_irc/mod_irc.erl:933 mod_irc/mod_irc_odbc.erl:974 -msgid "Encoding for server ~b" -msgstr "服务器 ~b 的编码" - -#: mod_irc/mod_irc.erl:942 mod_irc/mod_irc_odbc.erl:983 -msgid "Server ~b" -msgstr "服务器 ~b" - -#: mod_muc/mod_muc.erl:449 mod_muc/mod_muc_odbc.erl:456 -msgid "Only service administrators are allowed to send service messages" -msgstr "只有服务管理员可以发送服务消息" - -#: mod_muc/mod_muc.erl:493 mod_muc/mod_muc_odbc.erl:500 -msgid "Room creation is denied by service policy" -msgstr "创建房间被服务策略拒绝" - -#: mod_muc/mod_muc.erl:500 mod_muc/mod_muc_odbc.erl:507 -msgid "Conference room does not exist" -msgstr "会议室不存在" - -#: mod_muc/mod_muc.erl:582 mod_muc/mod_muc_odbc.erl:595 -msgid "Chatrooms" -msgstr "聊天室" - -#: mod_muc/mod_muc.erl:712 mod_muc/mod_muc_odbc.erl:729 -msgid "You need a client that supports x:data to register the nickname" -msgstr "您需要一个支持 x:data 的客户端来注册昵称" - -#: mod_muc/mod_muc.erl:718 mod_muc/mod_muc_odbc.erl:735 -msgid "Nickname Registration at " -msgstr "昵称注册于 " - -#: mod_muc/mod_muc.erl:722 mod_muc/mod_muc_odbc.erl:739 -msgid "Enter nickname you want to register" -msgstr "请输入您想要注册的昵称" - -#: mod_muc/mod_muc.erl:723 mod_muc/mod_muc_odbc.erl:740 -#: mod_muc/mod_muc_room.erl:3810 mod_roster.erl:944 mod_roster_odbc.erl:1061 -#: mod_vcard.erl:364 mod_vcard.erl:477 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:453 -msgid "Nickname" -msgstr "昵称" - -#: mod_muc/mod_muc.erl:762 mod_muc/mod_muc_odbc.erl:784 -#: mod_muc/mod_muc_room.erl:1064 mod_muc/mod_muc_room.erl:1787 -msgid "That nickname is registered by another person" -msgstr "该昵称已被另一个人注册了" - -#: mod_muc/mod_muc.erl:788 mod_muc/mod_muc_odbc.erl:811 -msgid "You must fill in field \"Nickname\" in the form" -msgstr "您必须填充表单中\"昵称\"项" - -#: mod_muc/mod_muc.erl:808 mod_muc/mod_muc_odbc.erl:831 -msgid "ejabberd MUC module" -msgstr "ejabberd MUC 模块" - -#: mod_muc/mod_muc_log.erl:374 mod_muc/mod_muc_log.erl:381 -msgid "Chatroom configuration modified" -msgstr "聊天室配置已修改" - -#: mod_muc/mod_muc_log.erl:384 -msgid "joins the room" -msgstr "加入房间" - -#: mod_muc/mod_muc_log.erl:387 mod_muc/mod_muc_log.erl:390 -msgid "leaves the room" -msgstr "离开房间" - -#: mod_muc/mod_muc_log.erl:393 mod_muc/mod_muc_log.erl:396 -msgid "has been banned" -msgstr "已被禁止" - -#: mod_muc/mod_muc_log.erl:399 mod_muc/mod_muc_log.erl:402 -msgid "has been kicked" -msgstr "已被踢出" - -#: mod_muc/mod_muc_log.erl:405 -msgid "has been kicked because of an affiliation change" -msgstr "因联属关系改变而被踢出" - -#: mod_muc/mod_muc_log.erl:408 -msgid "has been kicked because the room has been changed to members-only" -msgstr "因该房间改为只对会员开放而被踢出" - -#: mod_muc/mod_muc_log.erl:411 -msgid "has been kicked because of a system shutdown" -msgstr "因系统关机而被踢出" - -#: mod_muc/mod_muc_log.erl:414 -msgid "is now known as" -msgstr "现在称呼为" - -#: mod_muc/mod_muc_log.erl:417 mod_muc/mod_muc_log.erl:688 -#: mod_muc/mod_muc_room.erl:2393 -msgid " has set the subject to: " -msgstr "已将标题设置为: " - -#: mod_muc/mod_muc_log.erl:452 -msgid "Chatroom is created" -msgstr "聊天室已被创建" - -#: mod_muc/mod_muc_log.erl:453 -msgid "Chatroom is destroyed" -msgstr "聊天室已被销毁" - -#: mod_muc/mod_muc_log.erl:454 -msgid "Chatroom is started" -msgstr "聊天室已被启动" - -#: mod_muc/mod_muc_log.erl:455 -msgid "Chatroom is stopped" -msgstr "聊天室已被停用" - -#: mod_muc/mod_muc_log.erl:459 -msgid "Monday" -msgstr "星期一" - -#: mod_muc/mod_muc_log.erl:460 -msgid "Tuesday" -msgstr "星期二" - -#: mod_muc/mod_muc_log.erl:461 -msgid "Wednesday" -msgstr "星期三" - -#: mod_muc/mod_muc_log.erl:462 -msgid "Thursday" -msgstr "星期四" - -#: mod_muc/mod_muc_log.erl:463 -msgid "Friday" -msgstr "星期五" - -#: mod_muc/mod_muc_log.erl:464 -msgid "Saturday" -msgstr "星期六" - -#: mod_muc/mod_muc_log.erl:465 -msgid "Sunday" -msgstr "星期天" - -#: mod_muc/mod_muc_log.erl:469 -msgid "January" -msgstr "一月" - -#: mod_muc/mod_muc_log.erl:470 -msgid "February" -msgstr "二月" - -#: mod_muc/mod_muc_log.erl:471 -msgid "March" -msgstr "三月" - -#: mod_muc/mod_muc_log.erl:472 -msgid "April" -msgstr "四月" - -#: mod_muc/mod_muc_log.erl:473 -msgid "May" -msgstr "五月" - -#: mod_muc/mod_muc_log.erl:474 -msgid "June" -msgstr "六月" - -#: mod_muc/mod_muc_log.erl:475 -msgid "July" -msgstr "七月" - -#: mod_muc/mod_muc_log.erl:476 -msgid "August" -msgstr "八月" - -#: mod_muc/mod_muc_log.erl:477 -msgid "September" -msgstr "九月" - -#: mod_muc/mod_muc_log.erl:478 -msgid "October" -msgstr "十月" - -#: mod_muc/mod_muc_log.erl:479 -msgid "November" -msgstr "十一月" - -#: mod_muc/mod_muc_log.erl:480 -msgid "December" -msgstr "十二月" - -#: mod_muc/mod_muc_log.erl:750 -msgid "Room Configuration" -msgstr "房间配置" - -#: mod_muc/mod_muc_log.erl:759 -msgid "Room Occupants" -msgstr "房间人数" - -#: mod_muc/mod_muc_room.erl:174 -msgid "Traffic rate limit is exceeded" -msgstr "已经超过传输率限制" - -#: mod_muc/mod_muc_room.erl:246 -msgid "" -"This participant is kicked from the room because he sent an error message" -msgstr "该参与人由于发送了错误消息而被踢出了聊天室" - -#: mod_muc/mod_muc_room.erl:255 -msgid "It is not allowed to send private messages to the conference" -msgstr "不允许向会议发送私聊消息" - -#: mod_muc/mod_muc_room.erl:332 -msgid "Please, wait for a while before sending new voice request" -msgstr "请稍后再发送新的声音请求" - -#: mod_muc/mod_muc_room.erl:347 -msgid "Voice requests are disabled in this conference" -msgstr "该会议的声音请求以被禁用" - -#: mod_muc/mod_muc_room.erl:364 -msgid "Failed to extract JID from your voice request approval" -msgstr "无法从你的声音请求确认信息中提取JID" - -#: mod_muc/mod_muc_room.erl:393 -msgid "Only moderators can approve voice requests" -msgstr "仅主持人能确认声音请求" - -#: mod_muc/mod_muc_room.erl:408 -msgid "Improper message type" -msgstr "不恰当的消息类型" - -#: mod_muc/mod_muc_room.erl:518 -msgid "" -"This participant is kicked from the room because he sent an error message to " -"another participant" -msgstr "该参与人由于给其他人发送了出错消息而被踢出了聊天室" - -#: mod_muc/mod_muc_room.erl:531 -msgid "It is not allowed to send private messages of type \"groupchat\"" -msgstr "\"群组聊天\"类型不允许发送私聊消息" - -#: mod_muc/mod_muc_room.erl:543 mod_muc/mod_muc_room.erl:611 -msgid "Recipient is not in the conference room" -msgstr "接收人不在会议室" - -#: mod_muc/mod_muc_room.erl:564 mod_muc/mod_muc_room.erl:585 -msgid "It is not allowed to send private messages" -msgstr "不可以发送私聊消息" - -#: mod_muc/mod_muc_room.erl:576 mod_muc/mod_muc_room.erl:956 -#: mod_muc/mod_muc_room.erl:4040 -msgid "Only occupants are allowed to send messages to the conference" -msgstr "只有与会人可以向大会发送消息" - -#: mod_muc/mod_muc_room.erl:634 -msgid "Only occupants are allowed to send queries to the conference" -msgstr "只有与会人可以向大会发出查询请求" - -#: mod_muc/mod_muc_room.erl:646 -msgid "Queries to the conference members are not allowed in this room" -msgstr "本房间不可以查询会议成员信息" - -#: mod_muc/mod_muc_room.erl:932 -msgid "" -"Only moderators and participants are allowed to change the subject in this " -"room" -msgstr "只有主持人和参与人可以在此房间里更改主题" - -#: mod_muc/mod_muc_room.erl:937 -msgid "Only moderators are allowed to change the subject in this room" -msgstr "只有主持人可以在此房间里更改主题" - -#: mod_muc/mod_muc_room.erl:947 -msgid "Visitors are not allowed to send messages to all occupants" -msgstr "不允许访客给所有占有者发送消息" - -#: mod_muc/mod_muc_room.erl:1021 -msgid "" -"This participant is kicked from the room because he sent an error presence" -msgstr "该用户由于发送了错误状态而被踢出了聊天室" - -#: mod_muc/mod_muc_room.erl:1040 -msgid "Visitors are not allowed to change their nicknames in this room" -msgstr "此房间不允许用户更改昵称" - -#: mod_muc/mod_muc_room.erl:1053 mod_muc/mod_muc_room.erl:1779 -msgid "That nickname is already in use by another occupant" -msgstr "该昵称已被另一用户使用" - -#: mod_muc/mod_muc_room.erl:1768 -msgid "You have been banned from this room" -msgstr "您已被禁止进入该房间" - -#: mod_muc/mod_muc_room.erl:1771 -msgid "Membership is required to enter this room" -msgstr "进入此房间需要会员身份" - -#: mod_muc/mod_muc_room.erl:1807 -msgid "This room is not anonymous" -msgstr "此房间不是匿名房间" - -#: mod_muc/mod_muc_room.erl:1833 -msgid "A password is required to enter this room" -msgstr "进入此房间需要密码" - -#: mod_muc/mod_muc_room.erl:1855 mod_register.erl:246 -msgid "Too many CAPTCHA requests" -msgstr "验证码请求太多" - -#: mod_muc/mod_muc_room.erl:1864 mod_register.erl:251 -msgid "Unable to generate a CAPTCHA" -msgstr "无法生成验证码" - -#: mod_muc/mod_muc_room.erl:1874 -msgid "Incorrect password" -msgstr "密码不正确" - -#: mod_muc/mod_muc_room.erl:2448 -msgid "Administrator privileges required" -msgstr "需要管理员权限" - -#: mod_muc/mod_muc_room.erl:2463 -msgid "Moderator privileges required" -msgstr "需要主持人权限" - -#: mod_muc/mod_muc_room.erl:2619 -msgid "Jabber ID ~s is invalid" -msgstr "Jabber ID ~s 无效" - -#: mod_muc/mod_muc_room.erl:2633 -msgid "Nickname ~s does not exist in the room" -msgstr "昵称 ~s 不在该房间" - -#: mod_muc/mod_muc_room.erl:2659 mod_muc/mod_muc_room.erl:3049 -msgid "Invalid affiliation: ~s" -msgstr "无效加入: ~s" - -#: mod_muc/mod_muc_room.erl:2713 -msgid "Invalid role: ~s" -msgstr "无效角色: ~s" - -#: mod_muc/mod_muc_room.erl:3026 mod_muc/mod_muc_room.erl:3062 -msgid "Owner privileges required" -msgstr "需要持有人权限" - -#: mod_muc/mod_muc_room.erl:3195 -msgid "Configuration of room ~s" -msgstr "房间 ~s 的配置 " - -#: mod_muc/mod_muc_room.erl:3200 -msgid "Room title" -msgstr "房间标题" - -#: mod_muc/mod_muc_room.erl:3203 mod_muc/mod_muc_room.erl:3692 -msgid "Room description" -msgstr "房间描述" - -#: mod_muc/mod_muc_room.erl:3210 -msgid "Make room persistent" -msgstr "永久保存该房间" - -#: mod_muc/mod_muc_room.erl:3215 -msgid "Make room public searchable" -msgstr "使房间可被公开搜索" - -#: mod_muc/mod_muc_room.erl:3218 -msgid "Make participants list public" -msgstr "公开参与人列表" - -#: mod_muc/mod_muc_room.erl:3221 -msgid "Make room password protected" -msgstr "进入此房间需要密码" - -#: mod_muc/mod_muc_room.erl:3232 -msgid "Maximum Number of Occupants" -msgstr "允许的与会人最大数" - -#: mod_muc/mod_muc_room.erl:3239 -msgid "No limit" -msgstr "不限" - -#: mod_muc/mod_muc_room.erl:3250 -msgid "Present real Jabber IDs to" -msgstr "将真实 Jabber ID 显示给" - -#: mod_muc/mod_muc_room.erl:3258 mod_muc/mod_muc_room.erl:3292 -msgid "moderators only" -msgstr "仅主持人" - -#: mod_muc/mod_muc_room.erl:3260 mod_muc/mod_muc_room.erl:3294 -msgid "anyone" -msgstr "任何人" - -#: mod_muc/mod_muc_room.erl:3262 -msgid "Make room members-only" -msgstr "设置房间只接收会员" - -#: mod_muc/mod_muc_room.erl:3265 -msgid "Make room moderated" -msgstr "设置房间只接收主持人" - -#: mod_muc/mod_muc_room.erl:3268 -msgid "Default users as participants" -msgstr "用户默认被视为参与人" - -#: mod_muc/mod_muc_room.erl:3271 -msgid "Allow users to change the subject" -msgstr "允许用户更改主题" - -#: mod_muc/mod_muc_room.erl:3274 -msgid "Allow users to send private messages" -msgstr "允许用户发送私聊消息" - -#: mod_muc/mod_muc_room.erl:3279 -msgid "Allow visitors to send private messages to" -msgstr "允许访客发送私聊消息至" - -#: mod_muc/mod_muc_room.erl:3290 -msgid "nobody" -msgstr "没有人" - -#: mod_muc/mod_muc_room.erl:3296 -msgid "Allow users to query other users" -msgstr "允许用户查询其它用户" - -#: mod_muc/mod_muc_room.erl:3299 -msgid "Allow users to send invites" -msgstr "允许用户发送邀请" - -#: mod_muc/mod_muc_room.erl:3302 -msgid "Allow visitors to send status text in presence updates" -msgstr "更新在线状态时允许用户发送状态文本" - -#: mod_muc/mod_muc_room.erl:3305 -msgid "Allow visitors to change nickname" -msgstr "允许用户更改昵称" - -#: mod_muc/mod_muc_room.erl:3308 -msgid "Allow visitors to send voice requests" -msgstr "允许访客发送声音请求" - -#: mod_muc/mod_muc_room.erl:3311 -msgid "Minimum interval between voice requests (in seconds)" -msgstr "声音请求的最小间隔(以秒为单位)" - -#: mod_muc/mod_muc_room.erl:3317 -msgid "Make room CAPTCHA protected" -msgstr "保护房间验证码" - -#: mod_muc/mod_muc_room.erl:3322 -msgid "Exclude Jabber IDs from CAPTCHA challenge" -msgstr "从验证码挑战中排除 Jabber ID" - -#: mod_muc/mod_muc_room.erl:3329 -msgid "Enable logging" -msgstr "启用服务器端聊天记录" - -#: mod_muc/mod_muc_room.erl:3337 -msgid "You need an x:data capable client to configure room" -msgstr "您需要一个兼容 x:data 的客户端来配置房间" - -#: mod_muc/mod_muc_room.erl:3694 -msgid "Number of occupants" -msgstr "驻留人数" - -#: mod_muc/mod_muc_room.erl:3750 -msgid "private, " -msgstr "保密, " - -#: mod_muc/mod_muc_room.erl:3799 -msgid "Voice request" -msgstr "声音请求" - -#: mod_muc/mod_muc_room.erl:3803 -msgid "Either approve or decline the voice request." -msgstr "接受或拒绝声音请求" - -#: mod_muc/mod_muc_room.erl:3809 -msgid "User JID" -msgstr "用户 JID" - -#: mod_muc/mod_muc_room.erl:3811 -msgid "Grant voice to this person?" -msgstr "为此人授权声音?" - -#: mod_muc/mod_muc_room.erl:3960 -msgid "~s invites you to the room ~s" -msgstr "~s 邀请你到 ~s 房间" - -#: mod_muc/mod_muc_room.erl:3969 -msgid "the password is" -msgstr "密码是" - -#: mod_offline.erl:510 mod_offline_odbc.erl:352 -msgid "" -"Your contact offline message queue is full. The message has been discarded." -msgstr "您的联系人离线消息队列已满. 消息已被丢弃" - -#: mod_offline.erl:560 mod_offline_odbc.erl:408 -msgid "~s's Offline Messages Queue" -msgstr "~s 的离线消息队列" - -#: mod_offline.erl:563 mod_offline_odbc.erl:411 mod_roster.erl:987 -#: mod_roster_odbc.erl:1104 mod_shared_roster.erl:901 -#: mod_shared_roster.erl:1010 mod_shared_roster_odbc.erl:930 -#: mod_shared_roster_odbc.erl:1039 web/ejabberd_web_admin.erl:833 -#: web/ejabberd_web_admin.erl:874 web/ejabberd_web_admin.erl:942 -#: web/ejabberd_web_admin.erl:978 web/ejabberd_web_admin.erl:1019 -#: web/ejabberd_web_admin.erl:1508 web/ejabberd_web_admin.erl:1760 -#: web/ejabberd_web_admin.erl:1931 web/ejabberd_web_admin.erl:1963 -#: web/ejabberd_web_admin.erl:2031 web/ejabberd_web_admin.erl:2135 -#: web/ejabberd_web_admin.erl:2160 web/ejabberd_web_admin.erl:2248 -msgid "Submitted" -msgstr "已提交" - -#: mod_offline.erl:571 -msgid "Time" -msgstr "时间" - -#: mod_offline.erl:572 -msgid "From" -msgstr "从" - -#: mod_offline.erl:573 -msgid "To" -msgstr "到" - -#: mod_offline.erl:574 mod_offline_odbc.erl:419 -msgid "Packet" -msgstr "数据包" - -#: mod_offline.erl:587 mod_offline_odbc.erl:432 mod_shared_roster.erl:908 -#: mod_shared_roster_odbc.erl:937 web/ejabberd_web_admin.erl:882 -#: web/ejabberd_web_admin.erl:986 -msgid "Delete Selected" -msgstr "删除已选内容" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -msgid "Offline Messages:" -msgstr "离线消息:" - -#: mod_offline.erl:645 mod_offline_odbc.erl:519 -msgid "Remove All Offline Messages" -msgstr "移除所有离线消息" - -#: mod_proxy65/mod_proxy65_service.erl:213 -msgid "ejabberd SOCKS5 Bytestreams module" -msgstr "ejabberd SOCKS5 字节流模块" - -#: mod_pubsub/mod_pubsub.erl:1120 mod_pubsub/mod_pubsub_odbc.erl:929 -msgid "Publish-Subscribe" -msgstr "发行-订阅" - -#: mod_pubsub/mod_pubsub.erl:1214 mod_pubsub/mod_pubsub_odbc.erl:1025 -msgid "ejabberd Publish-Subscribe module" -msgstr "ejabberd 发行-订阅模块" - -#: mod_pubsub/mod_pubsub.erl:1497 mod_pubsub/mod_pubsub_odbc.erl:1312 -msgid "PubSub subscriber request" -msgstr "PubSub 订阅人请求" - -#: mod_pubsub/mod_pubsub.erl:1499 mod_pubsub/mod_pubsub_odbc.erl:1314 -msgid "Choose whether to approve this entity's subscription." -msgstr "选择是否允许该实体的订阅" - -#: mod_pubsub/mod_pubsub.erl:1505 mod_pubsub/mod_pubsub_odbc.erl:1320 -msgid "Node ID" -msgstr "节点 ID" - -#: mod_pubsub/mod_pubsub.erl:1510 mod_pubsub/mod_pubsub_odbc.erl:1325 -msgid "Subscriber Address" -msgstr "订阅人地址" - -#: mod_pubsub/mod_pubsub.erl:1516 mod_pubsub/mod_pubsub_odbc.erl:1331 -msgid "Allow this Jabber ID to subscribe to this pubsub node?" -msgstr "允许该 Jabber ID 订阅该 pubsub 节点?" - -#: mod_pubsub/mod_pubsub.erl:3474 mod_pubsub/mod_pubsub_odbc.erl:3287 -msgid "Deliver payloads with event notifications" -msgstr "用事件通告传输有效负载" - -#: mod_pubsub/mod_pubsub.erl:3475 mod_pubsub/mod_pubsub_odbc.erl:3288 -msgid "Deliver event notifications" -msgstr "传递事件通知" - -#: mod_pubsub/mod_pubsub.erl:3476 mod_pubsub/mod_pubsub_odbc.erl:3289 -msgid "Notify subscribers when the node configuration changes" -msgstr "当节点设置改变时通知订阅人" - -#: mod_pubsub/mod_pubsub.erl:3477 mod_pubsub/mod_pubsub_odbc.erl:3290 -msgid "Notify subscribers when the node is deleted" -msgstr "当节点被删除时通知订阅人" - -#: mod_pubsub/mod_pubsub.erl:3478 mod_pubsub/mod_pubsub_odbc.erl:3291 -msgid "Notify subscribers when items are removed from the node" -msgstr "当从节点删除内容条目时通知订阅人" - -#: mod_pubsub/mod_pubsub.erl:3479 mod_pubsub/mod_pubsub_odbc.erl:3292 -msgid "Persist items to storage" -msgstr "持久化内容条目" - -#: mod_pubsub/mod_pubsub.erl:3480 mod_pubsub/mod_pubsub_odbc.erl:3293 -msgid "A friendly name for the node" -msgstr "该节点的友好名称" - -#: mod_pubsub/mod_pubsub.erl:3481 mod_pubsub/mod_pubsub_odbc.erl:3294 -msgid "Max # of items to persist" -msgstr "允许持久化的最大内容条目数" - -#: mod_pubsub/mod_pubsub.erl:3482 mod_pubsub/mod_pubsub_odbc.erl:3295 -msgid "Whether to allow subscriptions" -msgstr "是否允许订阅" - -#: mod_pubsub/mod_pubsub.erl:3483 mod_pubsub/mod_pubsub_odbc.erl:3296 -msgid "Specify the access model" -msgstr "指定访问范例" - -#: mod_pubsub/mod_pubsub.erl:3486 mod_pubsub/mod_pubsub_odbc.erl:3299 -msgid "Roster groups allowed to subscribe" -msgstr "允许订阅的花名册组" - -#: mod_pubsub/mod_pubsub.erl:3487 mod_pubsub/mod_pubsub_odbc.erl:3300 -msgid "Specify the publisher model" -msgstr "指定发布人范例" - -#: mod_pubsub/mod_pubsub.erl:3489 mod_pubsub/mod_pubsub_odbc.erl:3302 -msgid "Purge all items when the relevant publisher goes offline" -msgstr "相关发布人离线后清除所有选项" - -#: mod_pubsub/mod_pubsub.erl:3490 mod_pubsub/mod_pubsub_odbc.erl:3303 -msgid "Specify the event message type" -msgstr "指定事件消息类型" - -# bytes was translated as 'bits'. It's corrected now. -#: mod_pubsub/mod_pubsub.erl:3492 mod_pubsub/mod_pubsub_odbc.erl:3305 -msgid "Max payload size in bytes" -msgstr "最大有效负载字节数" - -#: mod_pubsub/mod_pubsub.erl:3493 mod_pubsub/mod_pubsub_odbc.erl:3306 -msgid "When to send the last published item" -msgstr "何时发送最新发布的内容条目" - -#: mod_pubsub/mod_pubsub.erl:3495 mod_pubsub/mod_pubsub_odbc.erl:3308 -msgid "Only deliver notifications to available users" -msgstr "仅将通知发送给可发送的用户" - -#: mod_pubsub/mod_pubsub.erl:3496 mod_pubsub/mod_pubsub_odbc.erl:3309 -msgid "The collections with which a node is affiliated" -msgstr "加入结点的集合" - -#: mod_register.erl:193 -msgid "The CAPTCHA verification has failed" -msgstr "验证码检查失败" - -#: mod_register.erl:220 -msgid "You need a client that supports x:data and CAPTCHA to register" -msgstr "您需要一个支持 x:data 和验证码的客户端进行注册" - -#: mod_register.erl:226 mod_register.erl:265 -msgid "Choose a username and password to register with this server" -msgstr "请选择在此服务器上注册所需的用户名和密码" - -#: mod_register.erl:230 mod_vcard.erl:364 mod_vcard_odbc.erl:342 -#: web/ejabberd_web_admin.erl:1515 web/ejabberd_web_admin.erl:1572 -msgid "User" -msgstr "用户" - -#: mod_register.erl:316 mod_register.erl:361 -msgid "The password is too weak" -msgstr "密码强度太弱" - -#: mod_register.erl:365 -msgid "Users are not allowed to register accounts so quickly" -msgstr "不允许用户太频繁地注册帐户" - -#: mod_roster.erl:938 mod_roster_odbc.erl:1055 web/ejabberd_web_admin.erl:1701 -#: web/ejabberd_web_admin.erl:1886 web/ejabberd_web_admin.erl:1897 -#: web/ejabberd_web_admin.erl:2219 -msgid "None" -msgstr "无" - -#: mod_roster.erl:945 mod_roster_odbc.erl:1062 -msgid "Subscription" -msgstr "订阅" - -#: mod_roster.erl:946 mod_roster_odbc.erl:1063 -msgid "Pending" -msgstr "挂起" - -#: mod_roster.erl:947 mod_roster_odbc.erl:1064 -msgid "Groups" -msgstr "组" - -#: mod_roster.erl:974 mod_roster_odbc.erl:1091 -msgid "Validate" -msgstr "确认" - -#: mod_roster.erl:982 mod_roster_odbc.erl:1099 -msgid "Remove" -msgstr "移除" - -#: mod_roster.erl:985 mod_roster_odbc.erl:1102 -msgid "Roster of " -msgstr "花名册属于 " - -#: mod_roster.erl:988 mod_roster_odbc.erl:1105 mod_shared_roster.erl:902 -#: mod_shared_roster.erl:1011 mod_shared_roster_odbc.erl:931 -#: mod_shared_roster_odbc.erl:1040 web/ejabberd_web_admin.erl:834 -#: web/ejabberd_web_admin.erl:875 web/ejabberd_web_admin.erl:943 -#: web/ejabberd_web_admin.erl:979 web/ejabberd_web_admin.erl:1020 -#: web/ejabberd_web_admin.erl:1509 web/ejabberd_web_admin.erl:1761 -#: web/ejabberd_web_admin.erl:1932 web/ejabberd_web_admin.erl:2136 -#: web/ejabberd_web_admin.erl:2161 -msgid "Bad format" -msgstr "格式错误" - -#: mod_roster.erl:995 mod_roster_odbc.erl:1112 -msgid "Add Jabber ID" -msgstr "添加 Jabber ID" - -#: mod_roster.erl:1094 mod_roster_odbc.erl:1211 -msgid "Roster" -msgstr "花名册" - -#: mod_shared_roster.erl:857 mod_shared_roster.erl:899 -#: mod_shared_roster.erl:1007 mod_shared_roster_odbc.erl:886 -#: mod_shared_roster_odbc.erl:928 mod_shared_roster_odbc.erl:1036 -msgid "Shared Roster Groups" -msgstr "共享的花名册组群" - -#: mod_shared_roster.erl:895 mod_shared_roster_odbc.erl:924 -#: web/ejabberd_web_admin.erl:1365 web/ejabberd_web_admin.erl:2461 -msgid "Add New" -msgstr "添加新用户" - -#: mod_shared_roster.erl:978 mod_shared_roster_odbc.erl:1007 -msgid "Name:" -msgstr "姓名:" - -#: mod_shared_roster.erl:983 mod_shared_roster_odbc.erl:1012 -msgid "Description:" -msgstr "描述:" - -#: mod_shared_roster.erl:991 mod_shared_roster_odbc.erl:1020 -msgid "Members:" -msgstr "会员:" - -#: mod_shared_roster.erl:999 mod_shared_roster_odbc.erl:1028 -msgid "Displayed Groups:" -msgstr "已显示的组:" - -#: mod_shared_roster.erl:1008 mod_shared_roster_odbc.erl:1037 -msgid "Group " -msgstr "组" - -#: mod_shared_roster.erl:1017 mod_shared_roster_odbc.erl:1046 -#: web/ejabberd_web_admin.erl:840 web/ejabberd_web_admin.erl:884 -#: web/ejabberd_web_admin.erl:949 web/ejabberd_web_admin.erl:1026 -#: web/ejabberd_web_admin.erl:2017 -msgid "Submit" -msgstr "提交" - -#: mod_vcard.erl:165 mod_vcard_ldap.erl:238 mod_vcard_odbc.erl:129 -msgid "Erlang Jabber Server" -msgstr "Erlang Jabber 服务器" - -#: mod_vcard.erl:364 mod_vcard.erl:478 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:454 -msgid "Birthday" -msgstr "出生日期" - -#: mod_vcard.erl:364 mod_vcard.erl:480 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:456 -msgid "City" -msgstr "城市" - -#: mod_vcard.erl:364 mod_vcard.erl:479 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:455 -msgid "Country" -msgstr "国家" - -#: mod_vcard.erl:364 mod_vcard.erl:481 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:457 -msgid "Email" -msgstr "电子邮件" - -#: mod_vcard.erl:364 mod_vcard.erl:476 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:452 -msgid "Family Name" -msgstr "姓氏" - -#: mod_vcard.erl:364 mod_vcard_odbc.erl:342 -msgid "" -"Fill in the form to search for any matching Jabber User (Add * to the end of " -"field to match substring)" -msgstr "填充表单以搜索任何匹配的 Jabber 用户(在字段末添加*来匹配子串)" - -#: mod_vcard.erl:364 mod_vcard.erl:473 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:449 -msgid "Full Name" -msgstr "全名" - -#: mod_vcard.erl:364 mod_vcard.erl:475 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:451 -msgid "Middle Name" -msgstr "中间名" - -#: mod_vcard.erl:364 mod_vcard.erl:474 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:450 web/ejabberd_web_admin.erl:2006 -msgid "Name" -msgstr "姓名" - -#: mod_vcard.erl:364 mod_vcard.erl:482 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:458 -msgid "Organization Name" -msgstr "组织名称" - -#: mod_vcard.erl:364 mod_vcard.erl:483 mod_vcard_odbc.erl:342 -#: mod_vcard_odbc.erl:459 -msgid "Organization Unit" -msgstr "组织单位" - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "Search users in " -msgstr "搜索用户于 " - -#: mod_vcard.erl:364 mod_vcard_ldap.erl:462 mod_vcard_odbc.erl:342 -msgid "You need an x:data capable client to search" -msgstr "您需要一个兼容 x:data 的客户端来搜索" - -#: mod_vcard.erl:389 mod_vcard_ldap.erl:487 mod_vcard_odbc.erl:367 -msgid "vCard User Search" -msgstr "vCard 用户搜索" - -#: mod_vcard.erl:445 mod_vcard_ldap.erl:541 mod_vcard_odbc.erl:421 -msgid "ejabberd vCard module" -msgstr "ejabberd vCard 模块" - -#: mod_vcard.erl:469 mod_vcard_ldap.erl:551 mod_vcard_odbc.erl:445 -msgid "Search Results for " -msgstr "搜索结果属于关键词 " - -#: mod_vcard_ldap.erl:462 -msgid "Fill in fields to search for any matching Jabber User" -msgstr "填充字段以搜索任何匹配的 Jabber 用户" - -#: web/ejabberd_web_admin.erl:193 web/ejabberd_web_admin.erl:203 -#: web/ejabberd_web_admin.erl:219 web/ejabberd_web_admin.erl:229 -msgid "Unauthorized" -msgstr "未认证的" - -#: web/ejabberd_web_admin.erl:286 web/ejabberd_web_admin.erl:303 -msgid "ejabberd Web Admin" -msgstr "ejabberd 网页管理" - -#: web/ejabberd_web_admin.erl:769 web/ejabberd_web_admin.erl:780 -msgid "Administration" -msgstr "管理" - -#: web/ejabberd_web_admin.erl:878 web/ejabberd_web_admin.erl:982 -msgid "Raw" -msgstr "原始格式" - -#: web/ejabberd_web_admin.erl:1017 -msgid "~s access rule configuration" -msgstr "~s 访问规则配置" - -#: web/ejabberd_web_admin.erl:1035 -msgid "Virtual Hosts" -msgstr "虚拟主机" - -#: web/ejabberd_web_admin.erl:1043 web/ejabberd_web_admin.erl:1050 -msgid "Users" -msgstr "用户" - -#: web/ejabberd_web_admin.erl:1078 -msgid "Users Last Activity" -msgstr "用户上次活动" - -#: web/ejabberd_web_admin.erl:1080 -msgid "Period: " -msgstr "持续时间: " - -#: web/ejabberd_web_admin.erl:1090 -msgid "Last month" -msgstr "上个月" - -#: web/ejabberd_web_admin.erl:1091 -msgid "Last year" -msgstr "上一年" - -#: web/ejabberd_web_admin.erl:1092 -msgid "All activity" -msgstr "所有活动" - -#: web/ejabberd_web_admin.erl:1094 -msgid "Show Ordinary Table" -msgstr "显示普通列表" - -#: web/ejabberd_web_admin.erl:1096 -msgid "Show Integral Table" -msgstr "显示完整列表" - -#: web/ejabberd_web_admin.erl:1105 web/ejabberd_web_admin.erl:1939 -msgid "Statistics" -msgstr "统计" - -#: web/ejabberd_web_admin.erl:1117 -msgid "Not Found" -msgstr "没有找到" - -#: web/ejabberd_web_admin.erl:1134 -msgid "Node not found" -msgstr "没有找到节点" - -#: web/ejabberd_web_admin.erl:1460 -msgid "Host" -msgstr "主机" - -#: web/ejabberd_web_admin.erl:1461 -msgid "Registered Users" -msgstr "注册用户" - -#: web/ejabberd_web_admin.erl:1573 -msgid "Offline Messages" -msgstr "离线消息" - -#: web/ejabberd_web_admin.erl:1574 web/ejabberd_web_admin.erl:1767 -msgid "Last Activity" -msgstr "上次活动" - -#: web/ejabberd_web_admin.erl:1659 web/ejabberd_web_admin.erl:1675 -msgid "Registered Users:" -msgstr "注册用户:" - -#: web/ejabberd_web_admin.erl:1661 web/ejabberd_web_admin.erl:1677 -#: web/ejabberd_web_admin.erl:2192 -msgid "Online Users:" -msgstr "在线用户:" - -#: web/ejabberd_web_admin.erl:1663 -msgid "Outgoing s2s Connections:" -msgstr "出站 s2s 连接:" - -#: web/ejabberd_web_admin.erl:1665 -msgid "Outgoing s2s Servers:" -msgstr "出站 s2s 服务器" - -#: web/ejabberd_web_admin.erl:1734 web/mod_register_web.erl:175 -#: web/mod_register_web.erl:372 web/mod_register_web.erl:381 -#: web/mod_register_web.erl:411 -msgid "Change Password" -msgstr "更改密码" - -#: web/ejabberd_web_admin.erl:1758 -msgid "User " -msgstr "用户 " - -#: web/ejabberd_web_admin.erl:1765 -msgid "Connected Resources:" -msgstr "已连接资源:" - -#: web/ejabberd_web_admin.erl:1766 web/mod_register_web.erl:227 -#: web/mod_register_web.erl:519 -msgid "Password:" -msgstr "密码:" - -#: web/ejabberd_web_admin.erl:1828 -msgid "No Data" -msgstr "没有数据" - -#: web/ejabberd_web_admin.erl:1906 -msgid "Nodes" -msgstr "节点" - -#: web/ejabberd_web_admin.erl:1929 web/ejabberd_web_admin.erl:1951 -msgid "Node " -msgstr "节点 " - -#: web/ejabberd_web_admin.erl:1938 -msgid "Listened Ports" -msgstr "被监听的端口" - -#: web/ejabberd_web_admin.erl:1940 web/ejabberd_web_admin.erl:2260 -#: web/ejabberd_web_admin.erl:2448 -msgid "Update" -msgstr "更新" - -#: web/ejabberd_web_admin.erl:1943 web/ejabberd_web_admin.erl:2569 -msgid "Restart" -msgstr "重启" - -#: web/ejabberd_web_admin.erl:1945 web/ejabberd_web_admin.erl:2571 -msgid "Stop" -msgstr "停止" - -#: web/ejabberd_web_admin.erl:1959 -msgid "RPC Call Error" -msgstr "RPC 调用错误" - -#: web/ejabberd_web_admin.erl:2000 -msgid "Database Tables at " -msgstr "数据库列表位于 " - -#: web/ejabberd_web_admin.erl:2007 -msgid "Storage Type" -msgstr "存储类型" - -#: web/ejabberd_web_admin.erl:2008 -msgid "Elements" -msgstr "元素" - -#: web/ejabberd_web_admin.erl:2009 -msgid "Memory" -msgstr "内存" - -#: web/ejabberd_web_admin.erl:2032 web/ejabberd_web_admin.erl:2137 -msgid "Error" -msgstr "错误" - -#: web/ejabberd_web_admin.erl:2034 -msgid "Backup of " -msgstr "备份来源 " - -#: web/ejabberd_web_admin.erl:2036 -msgid "" -"Please note that these options will only backup the builtin Mnesia database. " -"If you are using the ODBC module, you also need to backup your SQL database " -"separately." -msgstr "" -"注意:这些选项仅将备份内置的 Mnesia 数据库. 如果您正在使用 ODBC 模块, 您还需" -"要分别备份您的数据库." - -#: web/ejabberd_web_admin.erl:2041 -msgid "Store binary backup:" -msgstr "存储为二进制备份:" - -#: web/ejabberd_web_admin.erl:2045 web/ejabberd_web_admin.erl:2052 -#: web/ejabberd_web_admin.erl:2060 web/ejabberd_web_admin.erl:2067 -#: web/ejabberd_web_admin.erl:2074 web/ejabberd_web_admin.erl:2081 -#: web/ejabberd_web_admin.erl:2088 web/ejabberd_web_admin.erl:2096 -#: web/ejabberd_web_admin.erl:2103 web/ejabberd_web_admin.erl:2110 -msgid "OK" -msgstr "确定" - -#: web/ejabberd_web_admin.erl:2048 -msgid "Restore binary backup immediately:" -msgstr "立即恢复二进制备份:" - -#: web/ejabberd_web_admin.erl:2056 -msgid "" -"Restore binary backup after next ejabberd restart (requires less memory):" -msgstr "在下次 ejabberd 重启后恢复二进制备份(需要的内存更少):" - -#: web/ejabberd_web_admin.erl:2063 -msgid "Store plain text backup:" -msgstr "存储为普通文本备份:" - -#: web/ejabberd_web_admin.erl:2070 -msgid "Restore plain text backup immediately:" -msgstr "立即恢复普通文本备份:" - -#: web/ejabberd_web_admin.erl:2077 -msgid "Import users data from a PIEFXIS file (XEP-0227):" -msgstr "从 PIEFXIS 文件 (XEP-0227) 导入用户数据:" - -#: web/ejabberd_web_admin.erl:2084 -msgid "Export data of all users in the server to PIEFXIS files (XEP-0227):" -msgstr "将服务器上所有用户的数据导出到 PIEFXIS 文件 (XEP-0227):" - -#: web/ejabberd_web_admin.erl:2091 -msgid "Export data of users in a host to PIEFXIS files (XEP-0227):" -msgstr "将某主机的用户数据导出到 PIEFXIS 文件 (XEP-0227):" - -#: web/ejabberd_web_admin.erl:2099 -msgid "Import user data from jabberd14 spool file:" -msgstr "从 jabberd14 Spool 文件导入用户数据:" - -#: web/ejabberd_web_admin.erl:2106 -msgid "Import users data from jabberd14 spool directory:" -msgstr "从 jabberd14 Spool 目录导入用户数据:" - -#: web/ejabberd_web_admin.erl:2132 -msgid "Listened Ports at " -msgstr "监听的端口位于 " - -#: web/ejabberd_web_admin.erl:2157 -msgid "Modules at " -msgstr "模块位于 " - -#: web/ejabberd_web_admin.erl:2183 -msgid "Statistics of ~p" -msgstr "~p 的统计" - -#: web/ejabberd_web_admin.erl:2186 -msgid "Uptime:" -msgstr "正常运行时间:" - -#: web/ejabberd_web_admin.erl:2189 -msgid "CPU Time:" -msgstr "CPU 时间:" - -#: web/ejabberd_web_admin.erl:2195 -msgid "Transactions Committed:" -msgstr "提交的事务:" - -#: web/ejabberd_web_admin.erl:2198 -msgid "Transactions Aborted:" -msgstr "取消的事务:" - -#: web/ejabberd_web_admin.erl:2201 -msgid "Transactions Restarted:" -msgstr "重启的事务:" - -#: web/ejabberd_web_admin.erl:2204 -msgid "Transactions Logged:" -msgstr "记入日志的事务:" - -#: web/ejabberd_web_admin.erl:2246 -msgid "Update " -msgstr "更新 " - -#: web/ejabberd_web_admin.erl:2254 -msgid "Update plan" -msgstr "更新计划" - -#: web/ejabberd_web_admin.erl:2255 -msgid "Modified modules" -msgstr "被修改模块" - -#: web/ejabberd_web_admin.erl:2256 -msgid "Update script" -msgstr "更新脚本" - -#: web/ejabberd_web_admin.erl:2257 -msgid "Low level update script" -msgstr "低级别更新脚本" - -#: web/ejabberd_web_admin.erl:2258 -msgid "Script check" -msgstr "脚本检查" - -#: web/ejabberd_web_admin.erl:2426 -msgid "Port" -msgstr "端口" - -#: web/ejabberd_web_admin.erl:2427 -msgid "IP" -msgstr "IP" - -#: web/ejabberd_web_admin.erl:2428 -msgid "Protocol" -msgstr "协议" - -#: web/ejabberd_web_admin.erl:2429 web/ejabberd_web_admin.erl:2556 -msgid "Module" -msgstr "模块" - -#: web/ejabberd_web_admin.erl:2430 web/ejabberd_web_admin.erl:2557 -msgid "Options" -msgstr "选项" - -#: web/ejabberd_web_admin.erl:2450 -msgid "Delete" -msgstr "删除" - -#: web/ejabberd_web_admin.erl:2579 -msgid "Start" -msgstr "开始" - -#: web/mod_register_web.erl:104 -msgid "Your Jabber account was successfully created." -msgstr "你的 Jabber 帐户已成功创建." - -#: web/mod_register_web.erl:107 -msgid "There was an error creating the account: " -msgstr "帐户创建出错: " - -#: web/mod_register_web.erl:115 -msgid "Your Jabber account was successfully deleted." -msgstr "你的 Jabber 帐户已成功删除." - -#: web/mod_register_web.erl:118 -msgid "There was an error deleting the account: " -msgstr "帐户删除失败: " - -#: web/mod_register_web.erl:128 -msgid "The password of your Jabber account was successfully changed." -msgstr "你的 Jabber 帐户密码已成功更新." - -#: web/mod_register_web.erl:131 -msgid "There was an error changing the password: " -msgstr "修改密码出错: " - -#: web/mod_register_web.erl:163 web/mod_register_web.erl:172 -msgid "Jabber Account Registration" -msgstr "Jabber 帐户注册" - -#: web/mod_register_web.erl:174 web/mod_register_web.erl:192 -#: web/mod_register_web.erl:201 -msgid "Register a Jabber account" -msgstr "注册 Jabber 帐户" - -#: web/mod_register_web.erl:176 web/mod_register_web.erl:493 -#: web/mod_register_web.erl:502 -msgid "Unregister a Jabber account" -msgstr "注销 Jabber 帐户" - -#: web/mod_register_web.erl:203 -msgid "" -"This page allows to create a Jabber account in this Jabber server. Your JID " -"(Jabber IDentifier) will be of the form: username@server. Please read " -"carefully the instructions to fill correctly the fields." -msgstr "" -"本页面允许在此服务器上创建 Jabber 帐户. 你的 JID (Jabber ID) 的形式如下: 用户" -"名@服务器. 请仔细阅读说明并正确填写相应字段." - -#: web/mod_register_web.erl:212 web/mod_register_web.erl:386 -#: web/mod_register_web.erl:509 -msgid "Username:" -msgstr "用户名:" - -#: web/mod_register_web.erl:217 -msgid "This is case insensitive: macbeth is the same that MacBeth and Macbeth." -msgstr "此处不区分大小写: macbeth 与 MacBeth 和 Macbeth 是一样的." - -#: web/mod_register_web.erl:218 -msgid "Characters not allowed:" -msgstr "禁用字符:" - -#: web/mod_register_web.erl:222 web/mod_register_web.erl:391 -#: web/mod_register_web.erl:514 -msgid "Server:" -msgstr "服务器:" - -#: web/mod_register_web.erl:232 -msgid "" -"Don't tell your password to anybody, not even the administrators of the " -"Jabber server." -msgstr "不要将密码告诉任何人, 就算是 Jabber 服务器的管理员也不可以." - -#: web/mod_register_web.erl:234 -msgid "You can later change your password using a Jabber client." -msgstr "你可以稍后用 Jabber 客户端修改你的密码." - -#: web/mod_register_web.erl:235 -msgid "" -"Some Jabber clients can store your password in your computer. Use that " -"feature only if you trust your computer is safe." -msgstr "" -"某些 Jabber 客户端可以在你的计算机里存储密码. 请仅在你确认你的计算机安全的情" -"况下使用该功能." - -#: web/mod_register_web.erl:237 -msgid "" -"Memorize your password, or write it in a paper placed in a safe place. In " -"Jabber there isn't an automated way to recover your password if you forget " -"it." -msgstr "" -"记住你的密码, 或将其记到纸上并放于安全位置. 如果你忘记了密码, Jabber 也没有自" -"动恢复密码的方式." - -#: web/mod_register_web.erl:242 web/mod_register_web.erl:406 -msgid "Password Verification:" -msgstr "密码确认:" - -#: web/mod_register_web.erl:250 -msgid "Register" -msgstr "注册" - -#: web/mod_register_web.erl:396 -msgid "Old Password:" -msgstr "旧密码: " - -#: web/mod_register_web.erl:401 -msgid "New Password:" -msgstr "新密码: " - -#: web/mod_register_web.erl:504 -msgid "This page allows to unregister a Jabber account in this Jabber server." -msgstr "此页面允许在此 Jabber 服务器上注销 Jabber 帐户" - -#: web/mod_register_web.erl:524 -msgid "Unregister" -msgstr "取消注册" - -#~ msgid "Captcha test failed" -#~ msgstr "验证码检测失败." - -#~ msgid "Encodings" -#~ msgstr "编码" - -#~ msgid "(Raw)" -#~ msgstr "(原始格式)" - -#~ msgid "Specified nickname is already registered" -#~ msgstr "指定的昵称已被注册" - -#~ msgid "Size" -#~ msgstr "大小" diff --git a/rebar b/rebar index 8553bb177..3f6203bcf 100755 Binary files a/rebar and b/rebar differ diff --git a/rebar.config b/rebar.config new file mode 100644 index 000000000..3d85402e3 --- /dev/null +++ b/rebar.config @@ -0,0 +1,319 @@ +%%%---------------------------------------------------------------------- +%%% +%%% ejabberd, Copyright (C) 2002-2025 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. +%%% +%%%---------------------------------------------------------------------- + +%%% +%%% Dependencies +%%% + +{deps, [{if_not_rebar3, + {if_version_below, "24", + {base64url, "~> 1.0", {git, "https://github.com/dvv/base64url", {tag, "1.0.1"}}} + }}, + {cache_tab, "~> 1.0.33", {git, "https://github.com/processone/cache_tab", {tag, "1.0.33"}}}, + {eimp, "~> 1.0.26", {git, "https://github.com/processone/eimp", {tag, "1.0.26"}}}, + {if_var_true, pam, + {epam, "~> 1.0.14", {git, "https://github.com/processone/epam", {tag, "1.0.14"}}}}, + {if_var_true, redis, + {if_not_rebar3, + {eredis, "~> 1.2.0", {git, "https://github.com/wooga/eredis/", {tag, "v1.2.0"}}} + }}, + {if_var_true, redis, + {if_rebar3, + {if_version_below, "21", + {eredis, "1.2.0", {git, "https://github.com/wooga/eredis/", {tag, "v1.2.0"}}}, + {eredis, "~> 1.7.1", {git, "https://github.com/Nordix/eredis/", {tag, "v1.7.1"}}} + }}}, + {if_var_true, sip, + {esip, "~> 1.0.59", {git, "https://github.com/processone/esip", {tag, "1.0.59"}}}}, + {if_var_true, zlib, + {ezlib, "~> 1.0.15", {git, "https://github.com/processone/ezlib", {tag, "1.0.15"}}}}, + {fast_tls, "~> 1.1.25", {git, "https://github.com/processone/fast_tls", {tag, "1.1.25"}}}, + {fast_xml, "~> 1.1.57", {git, "https://github.com/processone/fast_xml", {tag, "1.1.57"}}}, + {fast_yaml, "~> 1.0.39", {git, "https://github.com/processone/fast_yaml", {tag, "1.0.39"}}}, + {idna, "~> 6.0", {git, "https://github.com/benoitc/erlang-idna", {tag, "6.0.0"}}}, + {if_version_below, "27", + {jiffy, "~> 1.1.1", {git, "https://github.com/davisp/jiffy", {tag, "1.1.1"}}} + }, + {if_version_above, "23", + {jose, "~> 1.11.10", {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.10"}}}, + {jose, "1.11.1", {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.1"}}} + }, + {if_version_below, "22", + {lager, "~> 3.9.1", {git, "https://github.com/erlang-lager/lager", {tag, "3.9.1"}}} + }, + {if_var_true, lua, + {if_version_below, "21", + {luerl, "1.0.0", {git, "https://github.com/rvirding/luerl", {tag, "1.0"}}}, + {luerl, "~> 1.2.0", {git, "https://github.com/rvirding/luerl", {tag, "1.2"}}} + }}, + {mqtree, "~> 1.0.19", {git, "https://github.com/processone/mqtree", {tag, "1.0.19"}}}, + {p1_acme, "~> 1.0.28", {git, "https://github.com/processone/p1_acme", {tag, "1.0.28"}}}, + {if_var_true, mysql, + {p1_mysql, "~> 1.0.26", {git, "https://github.com/processone/p1_mysql", {tag, "1.0.26"}}}}, + {p1_oauth2, "~> 0.6.14", {git, "https://github.com/processone/p1_oauth2", {tag, "0.6.14"}}}, + {if_var_true, pgsql, + {p1_pgsql, "~> 1.1.35", {git, "https://github.com/processone/p1_pgsql", {tag, "1.1.35"}}}}, + {p1_utils, "~> 1.0.28", {git, "https://github.com/processone/p1_utils", {tag, "1.0.28"}}}, + {pkix, "~> 1.0.10", {git, "https://github.com/processone/pkix", {tag, "1.0.10"}}}, + {if_var_true, sqlite, + {sqlite3, "~> 1.1.15", {git, "https://github.com/processone/erlang-sqlite3", {tag, "1.1.15"}}}}, + {stringprep, "~> 1.0.33", {git, "https://github.com/processone/stringprep", {tag, "1.0.33"}}}, + {if_var_true, stun, + {stun, "~> 1.2.21", {git, "https://github.com/processone/stun", {tag, "1.2.21"}}}}, + {xmpp, ".*", {git, "https://github.com/processone/xmpp", "e9d901ea84fd3910ad32b715853397eb1155b41c"}}, + {yconf, ".*", {git, "https://github.com/processone/yconf", "95692795a8a8d950ba560e5b07e6b80660557259"}} + ]}. + +{gitonly_deps, [ejabberd_po]}. + +{if_var_true, latest_deps, + {floating_deps, [cache_tab, + eimp, + epam, + esip, + ezlib, + fast_tls, + fast_xml, + fast_yaml, + mqtree, + p1_acme, + p1_mysql, + p1_oauth2, + p1_pgsql, + p1_utils, + pkix, + sqlite3, + stringprep, + stun, + xmpp, + yconf]}}. + +%%% +%%% Compile +%%% + +{recursive_cmds, ['configure-deps']}. + +{post_hook_configure, [{"eimp", []}, + {if_var_true, pam, {"epam", []}}, + {if_var_true, sip, {"esip", []}}, + {if_var_true, zlib, {"ezlib", []}}, + {"fast_tls", []}, + {"fast_xml", [{if_var_true, full_xml, "--enable-full-xml"}]}, + {"fast_yaml", []}, + {"stringprep", []}]}. + +{erl_first_files, ["src/ejabberd_sql_pt.erl", "src/ejabberd_config.erl", + "src/gen_mod.erl", "src/mod_muc_room.erl", + "src/mod_push.erl", "src/xmpp_socket.erl"]}. + +{erl_opts, [nowarn_deprecated_function, + {i, "include"}, + {if_version_above, "20", {d, 'HAVE_ERL_ERROR'}}, + {if_version_above, "20", {d, 'HAVE_URI_STRING'}}, + {if_version_below, "21", {d, 'USE_OLD_HTTP_URI'}}, + {if_version_below, "22", {d, 'LAGER'}}, + {if_version_below, "21", {d, 'NO_CUSTOMIZE_HOSTNAME_CHECK'}}, + {if_version_below, "23", {d, 'USE_OLD_CRYPTO_HMAC'}}, + {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_version_below, "24", {d, 'OTP_BELOW_24'}}, + {if_version_below, "25", {d, 'OTP_BELOW_25'}}, + {if_version_below, "26", {d, 'OTP_BELOW_26'}}, + {if_version_below, "27", {d, 'OTP_BELOW_27'}}, + {if_version_below, "28", {d, 'OTP_BELOW_28'}}, + {if_var_false, debug, no_debug_info}, + {if_var_true, debug, debug_info}, + {if_var_true, elixir, {d, 'ELIXIR_ENABLED'}}, + {if_var_true, new_sql_schema, {d, 'NEW_SQL_SCHEMA'}}, + {if_var_true, roster_gateway_workaround, {d, 'ROSTER_GATEWAY_WORKAROUND'}}, + {if_var_true, sip, {d, 'SIP'}}, + {if_var_true, stun, {d, 'STUN'}}, + {src_dirs, [src, + {if_rebar3, sql}, + {if_var_true, tools, tools}]}]}. + +{if_rebar3, {plugins, [{if_version_below, "21", {rebar3_hex, "7.0.7"}}, + {if_version_above, "20", {rebar3_hex, "~> 7.0.8"}}, + {provider_asn1, "0.4.1"}, + %% Protocol consolidation doesn't work correctly in upstream rebar_mix, see + %% https://github.com/Supersonido/rebar_mix/issues/27#issuecomment-894873335 + %% Let's use this fixed rebar_mix fork, see its PR: + %% https://github.com/Supersonido/rebar_mix/pull/31 + {if_var_true, elixir, {rebar_mix, ".*", + {git, "https://github.com/bsanyi/rebar_mix.git", + {branch, "consolidation_fix"}}} + }]}}. +{if_rebar3, {project_plugins, [configure_deps, + {if_var_true, tools, rebar3_format}, + {if_var_true, tools, {rebar3_lint, "4.1.1"}} + ]}}. +{if_not_rebar3, {plugins, [ + deps_erl_opts, override_deps_versions2, override_opts, configure_deps + ]}}. + +{if_rebar3, {if_var_true, elixir, + {provider_hooks, [ + {post, [{compile, {mix, consolidate_protocols}}]} + ]}}}. + +%% Compiling Jose 1.11.10 with Erlang/OTP 27.0 throws warnings on public_key deprecated functions +{if_rebar3, {overrides, [{del, jose, [{erl_opts, [warnings_as_errors]}]}]}}. + +{sub_dirs, ["rel"]}. + +{keep_build_info, true}. + +%%% +%%% Test +%%% + +{xref_warnings, false}. + +{if_rebar3, + {xref_checks, + [deprecated_function_calls, deprecated_functions, locals_not_used, + undefined_function_calls, undefined_functions]} +}. +{if_not_rebar3, + {xref_checks, + [deprecated_function_calls, deprecated_functions, + undefined_function_calls, undefined_functions]} +}. + +{xref_exclusions, [ + "(\"gen_transport\":_/_)", + "(\"eprof\":_/_)", + {if_var_false, elixir, "(\"Elixir.*\":_/_)"}, + {if_var_false, http, "(\"lhttpc\":_/_)"}, + {if_var_false, mysql, "(\".*mysql.*\":_/_)"}, + {if_var_false, odbc, "(\"odbc\":_/_)"}, + {if_var_false, pam, "(\"epam\":_/_)"}, + {if_var_false, pgsql, "(\".*pgsql.*\":_/_)"}, + {if_var_false, redis, "(\"eredis\":_/_)"}, + {if_var_false, sqlite, "(\"sqlite3\":_/_)"}, + {if_var_false, zlib, "(\"ezlib\":_/_)"}]}. + +{xref_ignores, [{eldap_filter_yecc, return_error, 2}, + {http_uri, encode, 1}]}. + +{eunit_compile_opts, [{i, "tools"}, + {i, "include"}]}. + +{dialyzer, [{get_warnings, false}, % Show warnings of dependencies + {if_version_above, "25", + {plt_extra_apps, + [asn1, odbc, public_key, stdlib, syntax_tools, + idna, jose, + cache_tab, eimp, fast_tls, fast_xml, fast_yaml, + mqtree, p1_acme, p1_oauth2, p1_utils, pkix, + stringprep, xmpp, yconf, + {if_version_below, "27", jiffy}, + {if_var_true, pam, epam}, + {if_var_true, redis, eredis}, + {if_var_true, sip, esip}, + {if_var_true, zlib, ezlib}, + {if_var_true, lua, luerl}, + {if_var_true, mysql, p1_mysql}, + {if_var_true, pgsql, p1_pgsql}, + {if_var_true, stun, stun}, + {if_var_true, sqlite, sqlite3}]}, + {plt_extra_apps, % For Erlang/OTP 25 and older + [cache_tab, eimp, fast_tls, fast_xml, fast_yaml, + mqtree, p1_acme, p1_oauth2, p1_utils, pkix, stringprep, xmpp, yconf, + {if_var_true, pam, epam}, + {if_var_true, redis, eredis}, + {if_var_true, sip, esip}, + {if_var_true, zlib, ezlib}, + {if_var_true, lua, luerl}, + {if_var_true, mysql, p1_mysql}, + {if_var_true, pgsql, p1_pgsql}, + {if_var_true, stun, stun}, + {if_var_true, sqlite, sqlite3}]} + } ]}. + +{ct_opts, [{keep_logs, 20}]}. + +{cover_enabled, true}. +{cover_export_enabled, true}. +{cover_excl_mods, [eldap_filter_yecc]}. +{coveralls_coverdata, "_build/test/cover/ct.coverdata"}. +{coveralls_service_name, "github"}. + +%%% +%%% OTP Release +%%% + +{relx, [{release, {ejabberd, {cmd, "grep {vsn, vars.config | sed 's|{vsn, \"||;s|\"}.||' | tr -d '\012'"}}, + [ejabberd]}, + {sys_config, "./rel/sys.config"}, + {vm_args, "./rel/vm.args"}, + {overlay_vars, "vars.config"}, + {overlay, [{mkdir, "logs"}, + {mkdir, "database"}, + {mkdir, "conf"}, + {copy, "rel/files/erl", "erts-\{\{erts_vsn\}\}/bin/erl"}, + {template, "ejabberdctl.template", "bin/ejabberdctl"}, + {copy, "_build/default/lib/ejabberd/ebin/Elixir.*", "lib/ejabberd-{{release_version}}/ebin/"}, + {copy, "{{base_dir}}/consolidated/*", "lib/ejabberd-{{release_version}}/ebin/"}, + {copy, "rel/overlays/iex", "releases/{{release_version}}/"}, + {if_var_true, elixir, + {template, "rel/overlays/elixir", "releases/{{release_version}}/elixir"} + }, + {copy, "inetrc", "conf/inetrc"}, + {copy, "tools/captcha*.sh", "lib/ejabberd-\{\{release_version\}\}/priv/bin/"}, + {copy, "rel/files/install_upgrade.escript", "bin/install_upgrade.escript"}]} + ]}. + +{profiles, [{prod, [{relx, [{debug_info, strip}, + {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", "conf/ejabberdctl.cfg"}, + {copy, "ejabberd.yml.example", "conf/ejabberd.yml"}]}]}]}, + {dev, [{post_hooks, [{release, "rel/setup-dev.sh rebar3"}]}, + {deps, [{if_version_above, "20", sync}]}, + {relx, [{debug_info, keep}, + {dev_mode, true}, + {include_erts, true}, + {include_src, false}, + {generate_start_script, true}, + {extended_start_script, true}, + {overlay, [{copy, "ejabberdctl.cfg.example", "conf/ejabberdctl.cfg.example"}, + {copy, "ejabberd.yml.example", "conf/ejabberd.yml.example"}, + {copy, "test/ejabberd_SUITE_data/ca.pem", "conf/"}, + {copy, "test/ejabberd_SUITE_data/cert.pem", "conf/"}]}]}]}, + {translations, [{deps, [{ejabberd_po, ".*", {git, "https://github.com/processone/ejabberd-po", {branch, "main"}}}]}]}, + {test, [{erl_opts, [nowarn_export_all]}]}]}. + +{alias, [{relive, [{shell, "--apps ejabberd \ + --config rel/relive.config \ + --eval sync:go(). \ + --script rel/relive.escript \ + --name ejabberd@localhost"}]} +]}. + +%% Local Variables: +%% mode: erlang +%% End: +%% vim: set filetype=erlang tabstop=8: diff --git a/rebar.config.script b/rebar.config.script index 64398f33e..e476df448 100644 --- a/rebar.config.script +++ b/rebar.config.script @@ -1,174 +1,459 @@ -%%%------------------------------------------------------------------- -%%% @author Evgeniy Khramtsov -%%% @copyright (C) 2013, Evgeniy Khramtsov -%%% @doc +%%%---------------------------------------------------------------------- %%% -%%% @end -%%% Created : 1 May 2013 by Evgeniy Khramtsov -%%%------------------------------------------------------------------- -Cfg = case file:consult("vars.config") of - {ok, Terms} -> - Terms; - _Err -> - [] - end, +%%% ejabberd, Copyright (C) 2002-2025 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. +%%% +%%%---------------------------------------------------------------------- +Vars = case file:consult(filename:join([filename:dirname(SCRIPT),"vars.config"])) of + {ok, Terms} -> + Terms; + _Err -> + [] + end ++ [{cflags, "-g -O2 -Wall"}, {cppflags, "-g -O2 -Wall"}, + {ldflags, ""}, {system_deps, false}], +{cflags, CFlags} = lists:keyfind(cflags, 1, Vars), +{cppflags, CPPFlags} = lists:keyfind(cppflags, 1, Vars), +{ldflags, LDFlags} = lists:keyfind(ldflags, 1, Vars), +{system_deps, SystemDeps} = lists:keyfind(system_deps, 1, Vars), -Macros = lists:flatmap( - fun({roster_gateway_workaround, true}) -> - [{d, 'ROSTER_GATEWAY_WORKAROUND'}]; - ({transient_supervisors, false}) -> - [{d, 'NO_TRANSIENT_SUPERVISORS'}]; - ({nif, true}) -> - [{d, 'NIF'}]; - ({db_type, mssql}) -> - [{d, 'mssql'}]; - ({lager, true}) -> - [{d, 'LAGER'}]; - (_) -> - [] - end, Cfg), +GetCfg = fun GetCfg(Cfg, [Key | Tail], Default) -> + Val = case lists:keyfind(Key, 1, Cfg) of + {Key, V1} -> V1; + false -> Default + end, + case Tail of + [] -> + Val; + _ -> + GetCfg(Val, Tail, Default) + end + end, +ModCfg = fun ModCfg(Cfg, [Key | Tail], Op, Default) -> + {OldVal, PartCfg} = case lists:keytake(Key, 1, Cfg) of + {value, {_, V1}, V2} -> {V1, V2}; + false -> {if Tail == [] -> Default; true -> [] end, Cfg} + end, + case Tail of + [] -> + [{Key, Op(OldVal)} | PartCfg]; + _ -> + [{Key, ModCfg(OldVal, Tail, Op, Default)} | PartCfg] + end + end, -DebugInfo = case lists:keysearch(debug, 1, Cfg) of - {value, {debug, true}} -> - []; - _ -> - [no_debug_info] - end, +FilterConfig = fun FilterConfig(Cfg, [{Path, true, ModFun, Default} | Tail]) -> + FilterConfig(ModCfg(Cfg, Path, ModFun, Default), Tail); + FilterConfig(Cfg, [{Path, SourcePath, true, ModFun, Default, SourceDefault} | Tail]) -> + SourceVal = GetCfg(Cfg, SourcePath, SourceDefault), + ModFun2 = fun(V) -> ModFun(V, SourceVal) end, + FilterConfig(ModCfg(Cfg, Path, ModFun2, Default), Tail); + FilterConfig(Cfg, [_ | Tail]) -> + FilterConfig(Cfg, Tail); + FilterConfig(Cfg, []) -> + Cfg + end, -HiPE = case lists:keysearch(hipe, 1, Cfg) of - {value, {hipe, true}} -> - [native]; - _ -> - [] - end, +IsRebar3 = case application:get_key(rebar, vsn) of + {ok, VSN} -> + [VSN1 | _] = string:tokens(VSN, "-"), + [Maj|_] = string:tokens(VSN1, "."), + (list_to_integer(Maj) >= 3); + undefined -> + lists:keymember(mix, 1, application:loaded_applications()) + end, -SrcDirs = lists:foldl( - fun({tools, true}, Acc) -> - [tools|Acc]; - (_, Acc) -> - Acc - end, [], Cfg), +SysVer = erlang:system_info(otp_release), -Deps = [{p1_cache_tab, ".*", {git, "git://github.com/processone/cache_tab"}}, - {p1_tls, ".*", {git, "git://github.com/processone/tls"}}, - {p1_stringprep, ".*", {git, "git://github.com/processone/stringprep"}}, - {p1_xml, ".*", {git, "git://github.com/processone/xml"}}, - {esip, ".*", {git, "git://github.com/processone/p1_sip"}}, - {p1_stun, ".*", {git, "git://github.com/processone/stun"}}, - {p1_yaml, ".*", {git, "git://github.com/processone/p1_yaml"}}, - {ehyperloglog, ".*", {git, "https://github.com/vaxelfel/eHyperLogLog.git"}}, - {p1_utils, ".*", {git, "git://github.com/processone/p1_utils"}}], +ProcessSingleVar = fun(F, Var, Tail) -> + case F([Var], []) of + [] -> Tail; + [Val] -> [Val | Tail] + end + end, -ConfigureCmd = fun(Pkg, Flags) -> - {'get-deps', - "sh -c 'cd deps/" ++ Pkg ++ - " && ./configure" ++ Flags ++ "'"} - end, +ProcessVars = fun F([], Acc) -> + lists:reverse(Acc); + F([{Type, Ver, Value} | Tail], Acc) when + Type == if_version_above orelse + Type == if_version_below -> + SysVer = erlang:system_info(otp_release), + Include = if Type == if_version_above -> + SysVer > Ver; + true -> + SysVer < Ver + end, + if Include -> + F(Tail, ProcessSingleVar(F, Value, Acc)); + true -> + F(Tail, Acc) + end; + F([{Type, Ver, Value, ElseValue} | Tail], Acc) when + Type == if_version_above orelse + Type == if_version_below -> + Include = if Type == if_version_above -> + SysVer > Ver; + true -> + SysVer < Ver + end, + if Include -> + F(Tail, ProcessSingleVar(F, Value, Acc)); + true -> + F(Tail, ProcessSingleVar(F, ElseValue, Acc)) + end; + F([{Type, Var, Value} | Tail], Acc) when + Type == if_var_true orelse + Type == if_var_false -> + Flag = Type == if_var_true, + case proplists:get_bool(Var, Vars) of + V when V == Flag -> + F(Tail, ProcessSingleVar(F, Value, Acc)); + _ -> + F(Tail, Acc) + end; + F([{Type, Value} | Tail], Acc) when + Type == if_rebar3 orelse + Type == if_not_rebar3 -> + Flag = Type == if_rebar3, + case IsRebar3 == Flag of + true -> + F(Tail, ProcessSingleVar(F, Value, Acc)); + _ -> + F(Tail, Acc) + end; + F([{Type, Var, Match, Value} | Tail], Acc) when + Type == if_var_match orelse + Type == if_var_no_match -> + case proplists:get_value(Var, Vars) of + V when V == Match -> + F(Tail, ProcessSingleVar(F, Value, Acc)); + _ -> + F(Tail, Acc) + end; + F([{if_have_fun, MFA, Value} | Tail], Acc) -> + {Mod, Fun, Arity} = MFA, + code:ensure_loaded(Mod), + case erlang:function_exported(Mod, Fun, Arity) of + true -> + F(Tail, ProcessSingleVar(F, Value, Acc)); + false -> + F(Tail, Acc) + end; + F([{Type, {Mod, TypeDef}, Value} | Tail], Acc) when + Type == if_type_exported orelse + Type == if_type_not_exported -> + try + {ok, Concrete} = dialyzer_utils:get_core_from_beam(code:which(Mod)), + {ok, Types} = dialyzer_utils:get_record_and_type_info(Concrete), + maps:get(TypeDef, Types, undefined) + of + undefined when Type == if_type_not_exported -> + F(Tail, ProcessSingleVar(F, Value, Acc)); + undefined -> + F(Tail, Acc); + _ when Type == if_type_exported -> + F(Tail, ProcessSingleVar(F, Value, Acc)); + _ -> + F(Tail, Acc) + catch _:_ -> + if + Type == if_type_not_exported -> + F(Tail, ProcessSingleVar(F, Value, Acc)); + true -> + F(Tail, Acc) + end + end; + F([Other1 | Tail1], Acc) -> + F(Tail1, [F(Other1, []) | Acc]); + F(Val, Acc) when is_tuple(Val) -> + list_to_tuple(F(tuple_to_list(Val), Acc)); + F(Other2, _Acc) -> + Other2 + end, -XMLFlags = lists:foldl( - fun({nif, true}, Acc) -> - Acc ++ " --enable-nif"; - ({full_xml, true}, Acc) -> - Acc ++ " --enable-full-xml"; - (_, Acc) -> - Acc - end, "", Cfg), +MaybeApply = fun(Val) when is_function(Val) -> + Val(); + (Val) -> + Val + end, +MaybeApply2 = fun(Val, Arg) when is_function(Val) -> + Val(Arg); + (Val, _) -> + Val + end, -PostHooks = [ConfigureCmd("p1_tls", ""), - ConfigureCmd("p1_stringprep", ""), - ConfigureCmd("p1_yaml", ""), - ConfigureCmd("esip", ""), - ConfigureCmd("p1_xml", XMLFlags)], +AppendStr = fun(Append) -> + fun("") -> + lists:flatten(MaybeApply(Append)); + (Val) -> + lists:flatten([Val, " ", MaybeApply(Append)]) + end + end, +AppendList = fun(Append) -> + fun(Val) -> + Val ++ MaybeApply(Append) + end + end, +AppendStr2 = fun(Append) -> + fun("", Arg) -> + lists:flatten(MaybeApply2(Append, Arg)); + (Val, Arg) -> + lists:flatten([Val, " ", MaybeApply2(Append, Arg)]) + end + end, +AppendList2 = fun(Append) -> + fun(Val, Arg) -> + Val ++ MaybeApply2(Append, Arg) + end + end, -CfgDeps = lists:flatmap( - fun({mysql, true}) -> - [{p1_mysql, ".*", {git, "git://github.com/processone/mysql"}}]; - ({pgsql, true}) -> - [{p1_pgsql, ".*", {git, "git://github.com/processone/pgsql"}}]; - ({pam, true}) -> - [{p1_pam, ".*", {git, "git://github.com/processone/epam"}}]; - ({zlib, true}) -> - [{p1_zlib, ".*", {git, "git://github.com/processone/zlib"}}]; - ({riak, true}) -> - [{riakc, ".*", - {git, "git://github.com/basho/riak-erlang-client", - {tag, "1.4.2"}}}]; - ({json, true}) -> - [{jiffy, ".*", {git, "git://github.com/davisp/jiffy"}}]; - ({elixir, true}) -> - [{rebar_elixir_plugin, ".*", {git, "git://github.com/yrashk/rebar_elixir_plugin"}}, - {elixir, "1.1.*", {git, "git://github.com/elixir-lang/elixir"}}]; - ({iconv, true}) -> - [{p1_iconv, ".*", {git, "git://github.com/processone/eiconv"}}]; - ({lager, true}) -> - [{lager, ".*", {git, "git://github.com/basho/lager"}}]; - ({lager, false}) -> - [{p1_logger, ".*", {git, "git://github.com/processone/p1_logger"}}]; - (_) -> - [] - end, Cfg), +% Convert our rich deps syntax to rebar2 format: +% https://github.com/rebar/rebar/wiki/Dependency-management +Rebar2DepsFilter = +fun(DepsList, GitOnlyDeps) -> + lists:map(fun({DepName, _HexVersion, Source}) -> + {DepName, ".*", Source} + end, DepsList) +end, -CfgPostHooks = lists:flatmap( - fun({pam, true}) -> - [ConfigureCmd("p1_pam", "")]; - ({zlib, true}) -> - [ConfigureCmd("p1_zlib", "")]; - ({iconv, true}) -> - [ConfigureCmd("p1_iconv", "")]; - (_) -> - [] - end, Cfg), +% Convert our rich deps syntax to rebar3 version definition format: +% https://rebar3.org/docs/configuration/dependencies/#dependency-version-handling +% https://hexdocs.pm/elixir/Version.html +Rebar3DepsFilter = +fun(DepsList, GitOnlyDeps) -> + lists:map(fun({DepName, HexVersion, {git, _, {tag, GitVersion}} = Source}) -> + case {lists:member(DepName, GitOnlyDeps), HexVersion == ".*"} of + {true, _} -> + {DepName, ".*", Source}; + {false, true} -> + {DepName, GitVersion}; + {false, false} -> + {DepName, HexVersion} + end; + ({DepName, _HexVersion, Source}) -> + {DepName, ".*", Source} + end, DepsList) +end, -CfgXrefs = lists:flatmap( - fun({mysql, false}) -> - ["(\".*mysql.*\":_/_)"]; - ({pgsql, false}) -> - ["(\".*pgsql.*\":_/_)"]; - ({pam, false}) -> - ["(\"epam\":_/_)"]; - ({riak, false}) -> - ["(\"riak.*\":_/_)"]; - ({riak, true}) -> - % used in map-reduce function called from riak vm - ["(\"riak_object\":_/_)"]; - ({json, false}) -> - ["(\"jiffy\":_/_)"]; - ({zlib, false}) -> - ["(\"ezlib\":_/_)"]; - ({http, false}) -> - ["(\"lhttpc\":_/_)"]; - ({iconv, false}) -> - ["(\"iconv\":_/_)"]; - ({odbc, false}) -> - ["(\"odbc\":_/_)"]; - (_) -> - [] - end, Cfg), -ElixirConfig = case lists:keysearch(elixir, 1, Cfg) of - {value, {elixir, true}} -> - [{plugins, [rebar_elixir_compiler, rebar_exunit] }, - {lib_dirs, ["deps/elixir/lib"]}]; - _ -> - [] - end, +DepAlts = fun("esip") -> ["esip", "p1_sip"]; + ("xmpp") -> ["xmpp", "p1_xmpp"]; + ("fast_xml") -> ["fast_xml", "p1_xml"]; + (Val) -> [Val] + end, + +LibDirInt = fun F([Dep|Rest], Suffix) -> + case code:lib_dir(Dep) of + {error, _} -> + F(Rest, Suffix); + V -> V ++ Suffix + end; + F([], _) -> + error + end, + +LibDir = fun(Name, Suffix) -> + LibDirInt(DepAlts(Name), Suffix) + end, + +GlobalDepsFilter = +fun(Deps) -> + DepNames = lists:map(fun({DepName, _, _}) -> DepName; + ({DepName, _}) -> DepName; + (DepName) -> DepName + end, Deps), + lists:filtermap(fun(Dep) -> + case LibDir(atom_to_list(Dep), "") of + error -> + exit("Unable to locate dep '" ++ atom_to_list(Dep) ++ "' in system deps."); + _ -> + false + end + end, DepNames) +end, {ok, Cwd} = file:get_cwd(), +TestConfigFile = filename:join([Cwd, "test", "config.ctc"]), +TestConfig = case file:read_file_info(TestConfigFile) of + {ok, _} -> + [" -userconfig ct_config_plain ", TestConfigFile, " "]; + _ -> + "" + end, + +ResolveDepPath = case {SystemDeps, IsRebar3} of + {true, _} -> + fun("deps/" ++ Rest) -> + Slash = string:str(Rest, "/"), + case LibDir(string:sub_string(Rest, 1, Slash -1), string:sub_string(Rest, Slash)) of + error -> Rest; + V -> V + end; + (Path) -> + Path + end; + {_, true} -> + fun("deps/" ++ Rest) -> + Slash = string:str(Rest, "/"), + "_build/default/lib/" ++ + string:sub_string(Rest, 1, Slash - 1) ++ + string:sub_string(Rest, Slash); + (Path) -> + Path + end; + _ -> + fun(P) -> + P + end + end, + +CtParams = fun(CompileOpts) -> + ["-ct_hooks cth_surefire ", + lists:map(fun({i, IncPath}) -> + [" -include ", filename:absname(ResolveDepPath(IncPath), Cwd)] + end, CompileOpts), + TestConfig] + end, + +GenDepConfigureLine = +fun(DepPath, Flags) -> + ["sh -c 'if test ! -f config.status -o ", + "../../config.status -nt config.status; ", + "then (", + "CFLAGS=\"", CFlags,"\" ", + "CPPFLAGS=\"", CPPFlags, "\" " + "LDFLAGS=\"", LDFlags, "\"", + " ./configure ", string:join(Flags, " "), + "); fi'"] +end, + +GenDepsConfigure = +fun(Hooks) -> + lists:map(fun({Pkg, Flags}) -> + DepPath = ResolveDepPath("deps/" ++ Pkg ++ "/"), + Line = lists:flatten(GenDepConfigureLine(DepPath, Flags)), + {add, list_to_atom(Pkg), [{pre_hooks, [{{pc, compile}, Line}, {'compile', Line}, {'configure-deps', Line}]}]} + end, Hooks) +end, + +ProcessErlOpt = fun(Vals) -> + R = lists:map( + fun({i, Path}) -> + {i, ResolveDepPath(Path)}; + (ErlOpt) -> + ErlOpt + end, Vals), + M = lists:filter(fun({d, M}) -> true; (_) -> false end, R), + [{d, 'ALL_DEFS', M} | R] + end, + +ProcssXrefExclusions = fun(Items) -> + [{lists:flatten(["(XC - UC) || (XU - X - B ", + [[" - ", V] || V <- Items], ")"]), + []}] + end, + +ProcessFloatingDeps = +fun(Deps, FDeps) -> + lists:map(fun({DepName, _Ver, {git, Repo, _Commit}} = Dep) -> + case lists:member(DepName, FDeps) of + true -> + {DepName, ".*", {git, Repo}}; + _ -> + Dep + end; + (Dep2) -> + Dep2 + end, Deps) +end, + + +VarsApps = case file:consult(filename:join([filename:dirname(SCRIPT),"vars.config"])) of + {ok, TermsV} -> + case proplists:get_bool(odbc, TermsV) of + true -> [odbc]; + false -> [] + end; + _-> + [] + end, + +ProcessRelx = fun(Relx, Deps) -> + {value, {release, NameVersion, DefaultApps}, RelxTail} = lists:keytake(release, 1, Relx), + DepApps = lists:map(fun({DepName, _, _}) -> DepName; + ({DepName, _}) -> DepName; + (DepName) -> DepName + end, Deps), + [{release, NameVersion, DefaultApps ++ VarsApps ++ DepApps} | RelxTail] + end, + +GithubConfig = case {os:getenv("GITHUB_ACTIONS"), os:getenv("GITHUB_TOKEN")} of + {"true", Token} when is_list(Token) -> + CONFIG1 = [{coveralls_repo_token, Token}, + {coveralls_service_job_id, os:getenv("GITHUB_RUN_ID")}, + {coveralls_commit_sha, os:getenv("GITHUB_SHA")}, + {coveralls_service_number, os:getenv("GITHUB_RUN_NUMBER")}], + case os:getenv("GITHUB_EVENT_NAME") =:= "pull_request" + andalso string:tokens(os:getenv("GITHUB_REF"), "/") of + [_, "pull", PRNO, _] -> + [{coveralls_service_pull_request, PRNO} | CONFIG1]; + _ -> + CONFIG1 + end; + _ -> + [] +end, + +Rules = [ + {[plugins], IsRebar3, + AppendList([{pc, "~> 1.15.0"}]), []}, + {[provider_hooks], IsRebar3, + AppendList([{pre, [ + {compile, {asn, compile}}, + {clean, {asn, clean}} + ]}]), []}, + {[plugins], IsRebar3 and (os:getenv("GITHUB_ACTIONS") == "true"), + AppendList([{coveralls, {git, + "https://github.com/processone/coveralls-erl.git", + {branch, "addjsonfile"}}} ]), []}, + {[overrides], [post_hook_configure], SystemDeps == false, + AppendList2(GenDepsConfigure), [], []}, + {[ct_extra_params], [eunit_compile_opts], true, + AppendStr2(CtParams), "", []}, + {[erl_opts], true, + ProcessErlOpt, []}, + {[xref_queries], [xref_exclusions], true, + AppendList2(ProcssXrefExclusions), [], []}, + {[relx], [deps], IsRebar3, + ProcessRelx, [], []}, + {[deps], [floating_deps], true, + ProcessFloatingDeps, [], []}, + {[deps], [gitonly_deps], (not IsRebar3), + Rebar2DepsFilter, [], []}, + {[deps], [gitonly_deps], IsRebar3, + Rebar3DepsFilter, [], []}, + {[deps], SystemDeps /= false, + GlobalDepsFilter, []} + ], + +Config = [{plugin_dir, filename:join([filename:dirname(SCRIPT),"plugins"])}]++ +FilterConfig(ProcessVars(CONFIG, []), Rules)++ +GithubConfig, + +%io:format("ejabberd configuration:~n ~p~n", [Config]), -Config = [{erl_opts, Macros ++ HiPE ++ DebugInfo ++ - [{src_dirs, [asn1, src | SrcDirs]}]}, - {sub_dirs, ["rel"]}, - {keep_build_info, true}, - {ct_extra_params, "-include " - ++ filename:join([Cwd, "tools"])}, - {xref_warnings, false}, - {xref_checks, []}, - {xref_queries, - [{"(XC - UC) || (XU - X - B - " - ++ string:join(CfgXrefs, " - ") ++ ")", []}]}, - {post_hooks, PostHooks ++ CfgPostHooks}, - {deps, Deps ++ CfgDeps}] ++ ElixirConfig, -%%io:format("ejabberd configuration:~n ~p~n", [Config]), Config. %% Local Variables: diff --git a/rebar.lock b/rebar.lock new file mode 100644 index 000000000..d69fcec1e --- /dev/null +++ b/rebar.lock @@ -0,0 +1,88 @@ +{"1.2.0", +[{<<"base64url">>,{pkg,<<"base64url">>,<<"1.0.1">>},1}, + {<<"cache_tab">>,{pkg,<<"cache_tab">>,<<"1.0.33">>},0}, + {<<"eimp">>,{pkg,<<"eimp">>,<<"1.0.26">>},0}, + {<<"epam">>,{pkg,<<"epam">>,<<"1.0.14">>},0}, + {<<"eredis">>,{pkg,<<"eredis">>,<<"1.7.1">>},0}, + {<<"esip">>,{pkg,<<"esip">>,<<"1.0.59">>},0}, + {<<"ezlib">>,{pkg,<<"ezlib">>,<<"1.0.15">>},0}, + {<<"fast_tls">>,{pkg,<<"fast_tls">>,<<"1.1.25">>},0}, + {<<"fast_xml">>,{pkg,<<"fast_xml">>,<<"1.1.57">>},0}, + {<<"fast_yaml">>,{pkg,<<"fast_yaml">>,<<"1.0.39">>},0}, + {<<"idna">>,{pkg,<<"idna">>,<<"6.1.1">>},0}, + {<<"jiffy">>,{pkg,<<"jiffy">>,<<"1.1.2">>},1}, + {<<"jose">>,{pkg,<<"jose">>,<<"1.11.10">>},0}, + {<<"luerl">>,{pkg,<<"luerl">>,<<"1.2.3">>},0}, + {<<"mqtree">>,{pkg,<<"mqtree">>,<<"1.0.19">>},0}, + {<<"p1_acme">>,{pkg,<<"p1_acme">>,<<"1.0.28">>},0}, + {<<"p1_mysql">>,{pkg,<<"p1_mysql">>,<<"1.0.26">>},0}, + {<<"p1_oauth2">>,{pkg,<<"p1_oauth2">>,<<"0.6.14">>},0}, + {<<"p1_pgsql">>,{pkg,<<"p1_pgsql">>,<<"1.1.35">>},0}, + {<<"p1_utils">>,{pkg,<<"p1_utils">>,<<"1.0.28">>},0}, + {<<"pkix">>,{pkg,<<"pkix">>,<<"1.0.10">>},0}, + {<<"sqlite3">>,{pkg,<<"sqlite3">>,<<"1.1.15">>},0}, + {<<"stringprep">>,{pkg,<<"stringprep">>,<<"1.0.33">>},0}, + {<<"stun">>,{pkg,<<"stun">>,<<"1.2.21">>},0}, + {<<"unicode_util_compat">>,{pkg,<<"unicode_util_compat">>,<<"0.7.1">>},1}, + {<<"xmpp">>, + {git,"https://github.com/processone/xmpp", + {ref,"e9d901ea84fd3910ad32b715853397eb1155b41c"}}, + 0}, + {<<"yconf">>, + {git,"https://github.com/processone/yconf", + {ref,"95692795a8a8d950ba560e5b07e6b80660557259"}}, + 0}]}. +[ +{pkg_hash,[ + {<<"base64url">>, <<"F8C7F2DA04CA9A5D0F5F50258F055E1D699F0E8BF4CFDB30B750865368403CF6">>}, + {<<"cache_tab">>, <<"E2542AFB34F17EE3CA19D2B0F546A074922C2B99FB6B2ACFB38160D7D0336EC3">>}, + {<<"eimp">>, <<"C0B05F32E35629C4D9BCFB832FF879A92B0F92B19844BC7835E0A45635F2899A">>}, + {<<"epam">>, <<"AA0B85D27F4EF3A756AE995179DF952A0721237E83C6B79D644347B75016681A">>}, + {<<"eredis">>, <<"39E31AA02ADCD651C657F39AAFD4D31A9B2F63C6C700DC9CECE98D4BC3C897AB">>}, + {<<"esip">>, <<"EB202F8C62928193588091DFEDBC545FE3274C34ECD209961F86DCB6C9EBCE88">>}, + {<<"ezlib">>, <<"D74F5DF191784744726A5B1AE9062522C606334F11086363385EB3B772D91357">>}, + {<<"fast_tls">>, <<"DA8ED6F05A2452121B087158B17234749F36704C1F2B74DC51DB99A1E27ED5E8">>}, + {<<"fast_xml">>, <<"31EFC0F9BCEDA92069704F7A25830407DA5DC3DAD1272B810D6F2E13E73CC11A">>}, + {<<"fast_yaml">>, <<"2E71168091949BAB0E5F583B340A99072B4D22D93EB86624E7850A12B1517BE4">>}, + {<<"idna">>, <<"8A63070E9F7D0C62EB9D9FCB360A7DE382448200FBBD1B106CC96D3D8099DF8D">>}, + {<<"jiffy">>, <<"A9B6C9A7EC268E7CF493D028F0A4C9144F59CCB878B1AFE42841597800840A1B">>}, + {<<"jose">>, <<"A903F5227417BD2A08C8A00A0CBCC458118BE84480955E8D251297A425723F83">>}, + {<<"luerl">>, <<"DF25F41944E57A7C4D9EF09D238BC3E850276C46039CFC12B8BB42ECCF36FCB1">>}, + {<<"mqtree">>, <<"D769C25F898810725FC7DB0DBFFE5F72098647048B1BE2E6D772F1C2F31D8476">>}, + {<<"p1_acme">>, <<"64D9C17F5412AA92D75B29206B2B984D734A4FE1B7EACB66C3D7A7C697AC612C">>}, + {<<"p1_mysql">>, <<"574D07C9936C53B1EC3556DB3CF064CC14A6C39039835B3D940471BFA5AC8E2B">>}, + {<<"p1_oauth2">>, <<"1C5F82535574DE87E2059695AC4B91F8F9AEBACBC1C80287DAE6F02552D47AEA">>}, + {<<"p1_pgsql">>, <<"E13D89F14D717553E85C88A152CE77461916B013D88FCB851E354A0B332D4218">>}, + {<<"p1_utils">>, <<"9A7088A98D788B4C4880FD3C82D0C135650DB13F2E4EF7E10DB179791BC94D59">>}, + {<<"pkix">>, <<"D3BFADF7B7CFE2A3636F1B256C9CCE5F646A07CE31E57EE527668502850765A0">>}, + {<<"sqlite3">>, <<"E819DEFD280145C328457D7AF897D2E45E8E5270E18812EE30B607C99CDD21AF">>}, + {<<"stringprep">>, <<"22F42866B4F6F3C238EA2B9CB6241791184DDEDBAB55E94A025511F46325F3CA">>}, + {<<"stun">>, <<"735855314AD22CB7816B88597D2F5CA22E24AA5E4D6010A0EF3AFFB33CEED6A5">>}, + {<<"unicode_util_compat">>, <<"A48703A25C170EEDADCA83B11E88985AF08D35F37C6F664D6DCFB106A97782FC">>}]}, +{pkg_hash_ext,[ + {<<"base64url">>, <<"F9B3ADD4731A02A9B0410398B475B33E7566A695365237A6BDEE1BB447719F5C">>}, + {<<"cache_tab">>, <<"4258009EB050B22AABE0C848E230BBA58401A6895C58C2FF74DFB635E3C35900">>}, + {<<"eimp">>, <<"D96D4E8572B9DFC40F271E47F0CB1D8849373BC98A21223268781765ED52044C">>}, + {<<"epam">>, <<"2F3449E72885A72A6C2A843F561ADD0FC2F70D7A21F61456930A547473D4D989">>}, + {<<"eredis">>, <<"7C2B54C566FED55FEEF3341CA79B0100A6348FD3F162184B7ED5118D258C3CC1">>}, + {<<"esip">>, <<"0BDF2E3C349DC0B144F173150329E675C6A51AC473D7A0B2E362245FAAD3FBE6">>}, + {<<"ezlib">>, <<"DD14BA6C12521AF5CFE6923E73E3D545F4A0897DC66BFAB5287FBB7AE3962EAB">>}, + {<<"fast_tls">>, <<"59E183B5740E670E02B8AA6BE673B5E7779E5FE5BFCC679FE2D4993D1949A821">>}, + {<<"fast_xml">>, <<"EEC34E90ADACAFE467D5DDAB635A014DED73B98B4061554B2D1972173D929C39">>}, + {<<"fast_yaml">>, <<"24C7B9AB9E2B9269D64E45F4A2A1280966ADB17D31E63365CFD3EE277FB0A78D">>}, + {<<"idna">>, <<"92376EB7894412ED19AC475E4A86F7B413C1B9FBB5BD16DCCD57934157944CEA">>}, + {<<"jiffy">>, <<"BB61BC42A720BBD33CB09A410E48BB79A61012C74CB8B3E75F26D988485CF381">>}, + {<<"jose">>, <<"0D6CD36FF8BA174DB29148FC112B5842186B68A90CE9FC2B3EC3AFE76593E614">>}, + {<<"luerl">>, <<"1B4B9D0CA5D7D280D1D2787A6A5EE9F5A212641B62BFF91556BAA53805DF3AED">>}, + {<<"mqtree">>, <<"C81065715C49A1882812F80A5AE2D842E80DD3F2D130530DF35990248BF8CE3C">>}, + {<<"p1_acme">>, <<"CE686986DE3F9D5FD285AFE87523CB45329A349C6C6BE7ACC1ED916725D46423">>}, + {<<"p1_mysql">>, <<"EA138083F2C54719B9CF549DBF5802A288B0019EA3E5449B354C74CC03FAFDEC">>}, + {<<"p1_oauth2">>, <<"1FD3AC474E43722D9D5A87C6DF8D36F698ED87AF7BB81CBBB66361451D99AE8F">>}, + {<<"p1_pgsql">>, <<"E99594446C411C660696795B062336F5C4BD800451D8F620BB4D4CE304E255C2">>}, + {<<"p1_utils">>, <<"C49BD44BC4A40AD996691AF826DD7E0AA56D4D0CD730817190A1F84D1A7F0033">>}, + {<<"pkix">>, <<"E02164F83094CB124C41B1AB28988A615D54B9ADC38575F00F19A597A3AC5D0E">>}, + {<<"sqlite3">>, <<"3C0BA4E13322C2AD49DE4E2DDD28311366ADDE54BEAE8DBA9D9E3888F69D2857">>}, + {<<"stringprep">>, <<"96F8B30BC50887F605B33B46BCA1D248C19A879319B8C482790E3B4DA5DA98C0">>}, + {<<"stun">>, <<"3D7FE8EFB9D05B240A6AA9A6BF8B8B7BFF2D802895D170443C588987DC1E12D9">>}, + {<<"unicode_util_compat">>, <<"B3A917854CE3AE233619744AD1E0102E05673136776FB2FA76234F3E03B23642">>}]} +]. diff --git a/rebar3 b/rebar3 new file mode 100755 index 000000000..deaea1f90 Binary files /dev/null and b/rebar3 differ diff --git a/rel/relive.config b/rel/relive.config new file mode 100644 index 000000000..49da88b79 --- /dev/null +++ b/rel/relive.config @@ -0,0 +1,4 @@ +[{mnesia, [{dir, "_build/relive/database"}]}, + {sync,[{src_dirs, {replace, [{"ejabberd/src", []}]}}]}, + {ejabberd, [{config, "_build/relive/conf/ejabberd.yml"}, + {log_path, "_build/relive/logs/ejabberd.log"}]}]. diff --git a/rel/relive.escript b/rel/relive.escript new file mode 100644 index 000000000..3ee2de0f3 --- /dev/null +++ b/rel/relive.escript @@ -0,0 +1,26 @@ +#!/usr/bin/env escript + +main(_) -> + Base = "_build/relive", + prepare(Base, "", none), + prepare(Base, "conf", {os, cmd, "rel/setup-relive.sh"}), + prepare(Base, "database", none), + prepare(Base, "logs", none), + c:erlangrc([os:cmd("echo -n $HOME")]), + ok. + +prepare(BaseDir, SuffixDir, MFA) -> + Dir = filename:join(BaseDir, SuffixDir), + case file:make_dir(Dir) of + ok -> + io:format("Preparing relive dir ~s...~n", [Dir]), + case MFA of + none -> ok; + {M, F, A} -> M:F(A) + end; + {error, eexist} -> + ok; + {error, LogsError} -> + io:format("Error creating dir ~s: ~p~n", [Dir, LogsError]), + halt(1) + end. diff --git a/rel/reltool.config.script b/rel/reltool.config.script index 09dd83657..4f142efe2 100644 --- a/rel/reltool.config.script +++ b/rel/reltool.config.script @@ -1,12 +1,35 @@ %%%------------------------------------------------------------------- %%% @author Evgeniy Khramtsov -%%% @copyright (C) 2013, Evgeniy Khramtsov +%%% @copyright (C) 2013-2025, Evgeniy Khramtsov %%% @doc %%% %%% @end %%% Created : 8 May 2013 by Evgeniy Khramtsov %%%------------------------------------------------------------------- -Vars = case file:consult(filename:join(["..", "vars.config"])) of + +TopDir = filename:join(filename:dirname(SCRIPT), ".."), + +GetDeps = fun(Config, GetDepsFun) -> + case catch rebar_config:consult_file(Config) of + {ok, Data} -> + case lists:keyfind(deps, 1, Data) of + {deps, Deps} -> + lists:map(fun({Dep, _, _}) -> + [Dep, GetDepsFun(filename:join([TopDir, + "deps", + Dep, + "rebar.config"]), + GetDepsFun)] + end, Deps); + _ -> + [] + end; + _ -> + [] + end + end, + +Vars = case file:consult(filename:join([TopDir, "vars.config"])) of {ok, Terms} -> Terms; _Err -> @@ -15,11 +38,12 @@ Vars = case file:consult(filename:join(["..", "vars.config"])) of RequiredOTPApps = [sasl, crypto, public_key, ssl, mnesia, inets, compiler, asn1, + observer, tools, syntax_tools, os_mon, xmerl], ConfiguredOTPApps = lists:flatmap( fun({tools, true}) -> - [tools, runtime_tools]; + [runtime_tools]; ({odbc, true}) -> [odbc]; (_) -> @@ -28,22 +52,9 @@ ConfiguredOTPApps = lists:flatmap( OTPApps = RequiredOTPApps ++ ConfiguredOTPApps, -DepRequiredApps = [p1_cache_tab, p1_tls, p1_stringprep, p1_xml, p1_yaml, p1_utils], +DepApps = lists:usort(lists:flatten(GetDeps(filename:join(TopDir, "rebar.config"), GetDeps))), -DepConfiguredApps = lists:flatmap( - fun({mysql, true}) -> [p1_mysql]; - ({pgsql, true}) -> [p1_pgsql]; - ({pam, true}) -> [p1_pam]; - ({zlib, true}) -> [p1_zlib]; - ({stun, true}) -> [p1_stun]; - ({json, true}) -> [jiffy]; - ({iconv, true}) -> [p1_iconv]; - ({lager, true}) -> [lager, goldrush]; - ({lager, false}) -> [p1_logger]; - (_) -> [] - end, Vars), - -DepApps = DepRequiredApps ++ DepConfiguredApps, +SysVer = erlang:system_info(otp_release), Sys = [{lib_dirs, []}, {erts, [{mod_cond, derived}, {app_file, strip}]}, @@ -62,13 +73,17 @@ Sys = [{lib_dirs, []}, {boot_rel, "ejabberd"}, {profile, embedded}, {incl_cond, exclude}, - {excl_archive_filters, [".*"]}, %% Do not archive built libs {excl_sys_filters, ["^bin/.*", "^erts.*/bin/(dialyzer|typer)", "^erts.*/(doc|info|include|lib|man|src)"]}, {excl_app_filters, ["\.gitignore"]}, {app, stdlib, [{incl_cond, include}]}, {app, kernel, [{incl_cond, include}]}, {app, ejabberd, [{incl_cond, include}, {lib_dir, ".."}]}] +++ if SysVer < "26" -> + [{excl_archive_filters, [".*"]}]; %% Do not archive built libs + true -> + [] + end ++ lists:map( fun(App) -> {app, App, [{incl_cond, include}, @@ -80,16 +95,15 @@ Sys = [{lib_dirs, []}, end, OTPApps). Overlay = [ - {mkdir, "var/log/ejabberd"}, - {mkdir, "var/lock"}, - {mkdir, "var/lib/ejabberd"}, - {mkdir, "etc/ejabberd"}, + {mkdir, "logs"}, + {mkdir, "database"}, + {mkdir, "conf"}, {mkdir, "doc"}, {template, "files/erl", "\{\{erts_vsn\}\}/bin/erl"}, {template, "../ejabberdctl.template", "bin/ejabberdctl"}, - {copy, "../ejabberdctl.cfg.example", "etc/ejabberd/ejabberdctl.cfg"}, - {copy, "../ejabberd.yml.example", "etc/ejabberd/ejabberd.yml"}, - {copy, "../inetrc", "etc/ejabberd/inetrc"}, + {copy, "../ejabberdctl.cfg.example", "conf/ejabberdctl.cfg"}, + {copy, "../ejabberd.yml.example", "conf/ejabberd.yml"}, + {copy, "../inetrc", "conf/inetrc"}, {copy, "files/install_upgrade.escript", "bin/install_upgrade.escript"} ], diff --git a/rel/setup-dev.sh b/rel/setup-dev.sh new file mode 100755 index 000000000..af3875cf0 --- /dev/null +++ b/rel/setup-dev.sh @@ -0,0 +1,36 @@ +printf "===> Preparing dev configuration files: " + +PWD_DIR=$(pwd) +REL_DIR=$PWD_DIR/_build/dev/rel/ejabberd/ +CON_DIR=$REL_DIR/conf/ + +[ -z "$REL_DIR_TEMP" ] && REL_DIR_TEMP=$REL_DIR +CON_DIR_TEMP=$REL_DIR_TEMP/conf/ + +cd $CON_DIR_TEMP || exit + +sed -i "s|# certfiles:|certfiles:\n - $CON_DIR/cert.pem|g" ejabberd.yml.example +sed -i "s|certfiles:|ca_file: $CON_DIR/ca.pem\ncertfiles:|g" ejabberd.yml.example +sed -i 's|^acl:$|acl:\n admin: [user: admin]|g' ejabberd.yml.example +[ ! -f "$CON_DIR/ejabberd.yml" ] \ + && printf "ejabberd.yml " \ + && mv ejabberd.yml.example ejabberd.yml + +sed -i "s|#' POLL|EJABBERD_BYPASS_WARNINGS=true\n\n#' POLL|g" ejabberdctl.cfg.example +[ ! -f "$CON_DIR/ejabberdctl.cfg" ] \ + && printf "ejabberdctl.cfg " \ + && mv ejabberdctl.cfg.example ejabberdctl.cfg + +echo "" +echo "===> Some example ways to start this ejabberd dev:" +echo " _build/dev/rel/ejabberd/bin/ejabberdctl live" +case "$1" in + "rebar3") + echo " _build/dev/rel/ejabberd/bin/ejabberd console" + ;; + "mix") + echo " RELEASE_NODE=ejabberd@localhost _build/dev/rel/ejabberd/bin/ejabberd start" + ;; + "*") + ;; +esac diff --git a/rel/setup-relive.sh b/rel/setup-relive.sh new file mode 100755 index 000000000..a4c88f6c5 --- /dev/null +++ b/rel/setup-relive.sh @@ -0,0 +1,31 @@ +PWD_DIR=$(pwd) +REL_DIR=$PWD_DIR/_build/relive/ +CON_DIR=$REL_DIR/conf/ + +[ -z "$REL_DIR_TEMP" ] && REL_DIR_TEMP=$REL_DIR +CON_DIR_TEMP=$REL_DIR_TEMP/conf/ + +make ejabberdctl.relive +chmod +x ejabberdctl.relive +mv ejabberdctl.relive $REL_DIR/ejabberdctl + +cp inetrc $CON_DIR/ +cp ejabberdctl.cfg.example $CON_DIR/ejabberdctl.cfg.example +cp ejabberd.yml.example $CON_DIR/ejabberd.yml.example +cp test/ejabberd_SUITE_data/ca.pem $CON_DIR +cp test/ejabberd_SUITE_data/cert.pem $CON_DIR + +cd $CON_DIR_TEMP || exit + +sed -i "s|# certfiles:|certfiles:\n - $CON_DIR/cert.pem|g" ejabberd.yml.example +sed -i "s|certfiles:|ca_file: $CON_DIR/ca.pem\ncertfiles:|g" ejabberd.yml.example +sed -i 's|^acl:$|acl:\n admin: [user: admin]|g' ejabberd.yml.example +[ ! -f "$CON_DIR/ejabberd.yml" ] \ + && printf "ejabberd.yml " \ + && mv ejabberd.yml.example ejabberd.yml + +sed -i "s|#' POLL|EJABBERD_BYPASS_WARNINGS=true\n\n#' POLL|g" ejabberdctl.cfg.example +[ ! -f "$CON_DIR/ejabberdctl.cfg" ] \ + && printf "ejabberdctl.cfg " \ + && mv ejabberdctl.cfg.example ejabberdctl.cfg \ + || printf diff --git a/rel/sys.config b/rel/sys.config new file mode 100644 index 000000000..26b0d0c61 --- /dev/null +++ b/rel/sys.config @@ -0,0 +1,2 @@ +[{ejabberd, [{config, "conf/ejabberd.yml"}, + {log_path, "logs/ejabberd.log"}]}]. diff --git a/rel/vm.args b/rel/vm.args new file mode 100644 index 000000000..6301f464d --- /dev/null +++ b/rel/vm.args @@ -0,0 +1,32 @@ +## Name of the node +-sname ejabberd@localhost + +## Cookie for distributed erlang +#-setcookie ejabberd + +-mnesia dir \"database\" + +## Heartbeat management; auto-restarts VM if it dies or becomes unresponsive +## (Disabled by default..use with caution!) +##-heart + +## Enable kernel poll and a few async threads +##+K true +##+A 5 + +## Increase number of concurrent ports/sockets +##-env ERL_MAX_PORTS 4096 + +## Tweak GC to run more often +##-env ERL_FULLSWEEP_AFTER 10 + +# +B [c | d | i] +# Option c makes Ctrl-C interrupt the current shell instead of invoking the emulator break +# handler. Option d (same as specifying +B without an extra option) disables the break handler. # Option i makes the emulator ignore any break signal. +# If option c is used with oldshell on Unix, Ctrl-C will restart the shell process rather than +# interrupt it. +# Disable the emulator break handler +# it easy to accidentally type ctrl-c when trying +# to reach for ctrl-d. ctrl-c on a live node can +# have very undesirable results +##+Bi diff --git a/sql/lite.new.sql b/sql/lite.new.sql new file mode 100644 index 000000000..42f289fb3 --- /dev/null +++ b/sql/lite.new.sql @@ -0,0 +1,491 @@ +-- +-- ejabberd, Copyright (C) 2002-2025 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. +-- + +CREATE TABLE users ( + username text NOT NULL, + type smallint, + server_host text NOT NULL, + password text NOT NULL, + serverkey text NOT NULL DEFAULT '', + salt text NOT NULL DEFAULT '', + iterationcount integer NOT NULL DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (server_host, username, type) +); + + +CREATE TABLE last ( + username text NOT NULL, + server_host text NOT NULL, + seconds text NOT NULL, + state text NOT NULL, + PRIMARY KEY (server_host, username) +); + + +CREATE TABLE rosterusers ( + username text NOT NULL, + server_host text NOT NULL, + jid text NOT NULL, + nick text NOT NULL, + subscription character(1) NOT NULL, + ask character(1) NOT NULL, + askmessage text NOT NULL, + server character(1) NOT NULL, + subscribe text NOT NULL, + type text, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE UNIQUE INDEX i_rosteru_sh_user_jid ON rosterusers (server_host, username, jid); +CREATE INDEX i_rosteru_sh_jid ON rosterusers (server_host, jid); + + +CREATE TABLE rostergroups ( + username text NOT NULL, + server_host text NOT NULL, + jid text NOT NULL, + grp text NOT NULL +); + +CREATE INDEX i_rosterg_sh_user_jid ON rostergroups (server_host, username, jid); + +CREATE TABLE sr_group ( + name text NOT NULL, + server_host text NOT NULL, + opts text NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (server_host, name) +); + +CREATE UNIQUE INDEX i_sr_group_sh_name ON sr_group (server_host, name); + +CREATE TABLE sr_user ( + jid text NOT NULL, + server_host text NOT NULL, + grp text NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (server_host, jid, grp) +); + +CREATE UNIQUE INDEX i_sr_user_sh_jid_grp ON sr_user (server_host, jid, grp); +CREATE INDEX i_sr_user_sh_grp ON sr_user (server_host, grp); + +CREATE TABLE spool ( + username text NOT NULL, + server_host text NOT NULL, + xml text NOT NULL, + seq INTEGER PRIMARY KEY AUTOINCREMENT, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX i_spool_sh_username ON spool (server_host, username); + +CREATE TABLE archive ( + username text NOT NULL, + server_host text NOT NULL, + timestamp BIGINT UNSIGNED NOT NULL, + peer text NOT NULL, + bare_peer text NOT NULL, + xml text NOT NULL, + txt text, + id INTEGER PRIMARY KEY AUTOINCREMENT, + kind text, + nick text, + origin_id text, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX i_archive_sh_username_timestamp ON archive (server_host, username, timestamp); +CREATE INDEX i_archive_sh_username_peer ON archive (server_host, username, peer); +CREATE INDEX i_archive_sh_username_bare_peer ON archive (server_host, username, bare_peer); +CREATE INDEX i_archive_sh_timestamp ON archive (server_host, timestamp); +CREATE INDEX i_archive_sh_username_origin_id ON archive (server_host, username, origin_id); + +-- To update 'archive' from ejabberd <= 23.10: +-- ALTER TABLE archive ADD COLUMN origin_id text NOT NULL DEFAULT ''; +-- CREATE INDEX i_archive_sh_username_origin_id ON archive (server_host, username, origin_id); + +CREATE TABLE archive_prefs ( + username text NOT NULL, + server_host text NOT NULL, + def text NOT NULL, + always text NOT NULL, + never text NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (server_host, username) +); + +CREATE TABLE vcard ( + username text NOT NULL, + server_host text NOT NULL, + vcard text NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (server_host, username) +); + +CREATE TABLE vcard_search ( + username text NOT NULL, + lusername text NOT NULL, + server_host text NOT NULL, + fn text NOT NULL, + lfn text NOT NULL, + family text NOT NULL, + lfamily text NOT NULL, + given text NOT NULL, + lgiven text NOT NULL, + middle text NOT NULL, + lmiddle text NOT NULL, + nickname text NOT NULL, + lnickname text NOT NULL, + bday text NOT NULL, + lbday text NOT NULL, + ctry text NOT NULL, + lctry text NOT NULL, + locality text NOT NULL, + llocality text NOT NULL, + email text NOT NULL, + lemail text NOT NULL, + orgname text NOT NULL, + lorgname text NOT NULL, + orgunit text NOT NULL, + lorgunit text NOT NULL, + PRIMARY KEY (server_host, lusername) +); + +CREATE INDEX i_vcard_search_sh_lfn ON vcard_search(server_host, lfn); +CREATE INDEX i_vcard_search_sh_lfamily ON vcard_search(server_host, lfamily); +CREATE INDEX i_vcard_search_sh_lgiven ON vcard_search(server_host, lgiven); +CREATE INDEX i_vcard_search_sh_lmiddle ON vcard_search(server_host, lmiddle); +CREATE INDEX i_vcard_search_sh_lnickname ON vcard_search(server_host, lnickname); +CREATE INDEX i_vcard_search_sh_lbday ON vcard_search(server_host, lbday); +CREATE INDEX i_vcard_search_sh_lctry ON vcard_search(server_host, lctry); +CREATE INDEX i_vcard_search_sh_llocality ON vcard_search(server_host, llocality); +CREATE INDEX i_vcard_search_sh_lemail ON vcard_search(server_host, lemail); +CREATE INDEX i_vcard_search_sh_lorgname ON vcard_search(server_host, lorgname); +CREATE INDEX i_vcard_search_sh_lorgunit ON vcard_search(server_host, lorgunit); + +CREATE TABLE privacy_default_list ( + username text NOT NULL, + server_host text NOT NULL, + name text NOT NULL, + PRIMARY KEY (server_host, username) +); + +CREATE TABLE privacy_list ( + username text NOT NULL, + server_host text NOT NULL, + name text NOT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE UNIQUE INDEX i_privacy_list_sh_username_name ON privacy_list (server_host, username, name); + +CREATE TABLE privacy_list_data ( + id bigint REFERENCES privacy_list(id) ON DELETE CASCADE, + t character(1) NOT NULL, + value text NOT NULL, + action character(1) NOT NULL, + ord NUMERIC NOT NULL, + match_all boolean NOT NULL, + match_iq boolean NOT NULL, + match_message boolean NOT NULL, + match_presence_in boolean NOT NULL, + match_presence_out boolean NOT NULL +); + +CREATE TABLE private_storage ( + username text NOT NULL, + server_host text NOT NULL, + namespace text NOT NULL, + data text NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (server_host, username, namespace) +); + +CREATE TABLE roster_version ( + username text NOT NULL, + server_host text NOT NULL, + version text NOT NULL, + PRIMARY KEY (server_host, username) +); + +CREATE TABLE pubsub_node ( + host text NOT NULL, + node text NOT NULL, + parent text NOT NULL DEFAULT '', + plugin text NOT NULL, + nodeid INTEGER PRIMARY KEY AUTOINCREMENT +); +CREATE INDEX i_pubsub_node_parent ON pubsub_node (parent); +CREATE UNIQUE INDEX i_pubsub_node_tuple ON pubsub_node (host, node); + +CREATE TABLE pubsub_node_option ( + nodeid bigint REFERENCES pubsub_node(nodeid) ON DELETE CASCADE, + name text NOT NULL, + val text NOT NULL +); +CREATE INDEX i_pubsub_node_option_nodeid ON pubsub_node_option (nodeid); + +CREATE TABLE pubsub_node_owner ( + nodeid bigint REFERENCES pubsub_node(nodeid) ON DELETE CASCADE, + owner text NOT NULL +); +CREATE INDEX i_pubsub_node_owner_nodeid ON pubsub_node_owner (nodeid); + +CREATE TABLE pubsub_state ( + nodeid bigint REFERENCES pubsub_node(nodeid) ON DELETE CASCADE, + jid text NOT NULL, + affiliation character(1), + subscriptions text NOT NULL DEFAULT '', + stateid INTEGER PRIMARY KEY AUTOINCREMENT +); +CREATE INDEX i_pubsub_state_jid ON pubsub_state (jid); +CREATE UNIQUE INDEX i_pubsub_state_tuple ON pubsub_state (nodeid, jid); + +CREATE TABLE pubsub_item ( + nodeid bigint REFERENCES pubsub_node(nodeid) ON DELETE CASCADE, + itemid text NOT NULL, + publisher text NOT NULL, + creation varchar(32) NOT NULL, + modification varchar(32) NOT NULL, + payload text NOT NULL DEFAULT '' +); +CREATE INDEX i_pubsub_item_itemid ON pubsub_item (itemid); +CREATE UNIQUE INDEX i_pubsub_item_tuple ON pubsub_item (nodeid, itemid); + + CREATE TABLE pubsub_subscription_opt ( + subid text NOT NULL, + opt_name varchar(32), + opt_value text NOT NULL +); +CREATE UNIQUE INDEX i_pubsub_subscription_opt ON pubsub_subscription_opt (subid, opt_name); + +CREATE TABLE muc_room ( + name text NOT NULL, + server_host text NOT NULL, + host text NOT NULL, + opts text NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE UNIQUE INDEX i_muc_room_name_host ON muc_room (name, host); +CREATE INDEX i_muc_room_host_created_at ON muc_room (host, created_at); + +CREATE TABLE muc_registered ( + jid text NOT NULL, + host text NOT NULL, + server_host text NOT NULL, + nick text NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX i_muc_registered_nick ON muc_registered (nick); +CREATE UNIQUE INDEX i_muc_registered_jid_host ON muc_registered (jid, host); + +CREATE TABLE muc_online_room ( + name text NOT NULL, + host text NOT NULL, + server_host text NOT NULL, + node text NOT NULL, + pid text NOT NULL +); + +CREATE UNIQUE INDEX i_muc_online_room_name_host ON muc_online_room (name, host); + +CREATE TABLE muc_online_users ( + username text NOT NULL, + server text NOT NULL, + resource text NOT NULL, + name text NOT NULL, + host text NOT NULL, + server_host text NOT NULL, + node text NOT NULL +); + +CREATE UNIQUE INDEX i_muc_online_users ON muc_online_users (username, server, resource, name, host); + +CREATE TABLE muc_room_subscribers ( + room text NOT NULL, + host text NOT NULL, + jid text NOT NULL, + nick text NOT NULL, + nodes text NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +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 ( + username text NOT NULL, + server_host text NOT NULL, + xml text, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (server_host, username) +); + +CREATE TABLE caps_features ( + node text NOT NULL, + subnode text NOT NULL, + feature text, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX i_caps_features_node_subnode ON caps_features (node, subnode); + +CREATE TABLE sm ( + usec bigint NOT NULL, + pid text NOT NULL, + node text NOT NULL, + username text NOT NULL, + server_host text NOT NULL, + resource text NOT NULL, + priority text NOT NULL, + info text NOT NULL, + PRIMARY KEY (usec, pid) +); + +CREATE INDEX i_sm_node ON sm(node); +CREATE INDEX i_sm_sh_username ON sm (server_host, username); + +CREATE TABLE oauth_token ( + token text NOT NULL PRIMARY KEY, + jid text NOT NULL, + scope text NOT NULL, + expire bigint NOT NULL +); + +CREATE TABLE oauth_client ( + client_id text PRIMARY KEY, + client_name text NOT NULL, + grant_type text NOT NULL, + options text NOT NULL +); + +CREATE TABLE route ( + domain text NOT NULL, + server_host text NOT NULL, + node text NOT NULL, + pid text NOT NULL, + local_hint text NOT NULL +); + +CREATE UNIQUE INDEX i_route ON route(domain, server_host, node, pid); + +CREATE TABLE bosh ( + sid text NOT NULL, + node text NOT NULL, + pid text NOT NULL +); + +CREATE UNIQUE INDEX i_bosh_sid ON bosh(sid); + +CREATE TABLE proxy65 ( + sid text NOT NULL, + pid_t text NOT NULL, + pid_i text NOT NULL, + node_t text NOT NULL, + node_i text NOT NULL, + jid_i text NOT NULL +); + +CREATE UNIQUE INDEX i_proxy65_sid ON proxy65 (sid); +CREATE INDEX i_proxy65_jid ON proxy65 (jid_i); + +CREATE TABLE push_session ( + username text NOT NULL, + server_host text NOT NULL, + timestamp bigint NOT NULL, + service text NOT NULL, + node text NOT NULL, + xml text NOT NULL, + PRIMARY KEY (server_host, username, timestamp) +); + +CREATE UNIQUE INDEX i_push_session_susn ON push_session (server_host, username, service, node); +CREATE INDEX i_push_session_sh_username_timestamp ON push_session (server_host, username, timestamp); + +CREATE TABLE mix_channel ( + channel text NOT NULL, + service text NOT NULL, + username text NOT NULL, + domain text NOT NULL, + jid text NOT NULL, + hidden boolean NOT NULL, + hmac_key text NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE UNIQUE INDEX i_mix_channel ON mix_channel (channel, service); +CREATE INDEX i_mix_channel_serv ON mix_channel (service); + +CREATE TABLE mix_participant ( + channel text NOT NULL, + service text NOT NULL, + username text NOT NULL, + domain text NOT NULL, + jid text NOT NULL, + id text NOT NULL, + nick text NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE UNIQUE INDEX i_mix_participant ON mix_participant (channel, service, username, domain); + +CREATE TABLE mix_subscription ( + channel text NOT NULL, + service text NOT NULL, + username text NOT NULL, + domain text NOT NULL, + node text NOT NULL, + jid text NOT NULL +); + +CREATE UNIQUE INDEX i_mix_subscription ON mix_subscription (channel, service, username, domain, node); +CREATE INDEX i_mix_subscription_chan_serv_node ON mix_subscription (channel, service, node); + +CREATE TABLE mix_pam ( + username text NOT NULL, + server_host text NOT NULL, + channel text NOT NULL, + service text NOT NULL, + id text NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE UNIQUE INDEX i_mix_pam ON mix_pam (username, server_host, channel, service); + +CREATE TABLE mqtt_pub ( + username text NOT NULL, + server_host text NOT NULL, + resource text NOT NULL, + topic text NOT NULL, + qos smallint NOT NULL, + payload blob NOT NULL, + payload_format smallint NOT NULL, + content_type text NOT NULL, + response_topic text NOT NULL, + correlation_data blob NOT NULL, + user_properties blob NOT NULL, + expiry bigint NOT NULL +); + +CREATE UNIQUE INDEX i_mqtt_topic_server ON mqtt_pub (topic, server_host); diff --git a/sql/lite.sql b/sql/lite.sql new file mode 100644 index 000000000..b31e02b79 --- /dev/null +++ b/sql/lite.sql @@ -0,0 +1,459 @@ +-- +-- ejabberd, Copyright (C) 2002-2025 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. +-- + +CREATE TABLE users ( + username text, + type smallint, + password text NOT NULL, + serverkey text NOT NULL DEFAULT '', + salt text NOT NULL DEFAULT '', + iterationcount integer NOT NULL DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + primary key (username, type) +); + + +CREATE TABLE last ( + username text PRIMARY KEY, + seconds text NOT NULL, + state text NOT NULL +); + + +CREATE TABLE rosterusers ( + username text NOT NULL, + jid text NOT NULL, + nick text NOT NULL, + subscription character(1) NOT NULL, + ask character(1) NOT NULL, + askmessage text NOT NULL, + server character(1) NOT NULL, + subscribe text NOT NULL, + type text, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE UNIQUE INDEX i_rosteru_user_jid ON rosterusers (username, jid); +CREATE INDEX i_rosteru_jid ON rosterusers (jid); + + +CREATE TABLE rostergroups ( + username text NOT NULL, + jid text NOT NULL, + grp text NOT NULL +); + +CREATE INDEX pk_rosterg_user_jid ON rostergroups (username, jid); + +CREATE TABLE sr_group ( + name text NOT NULL, + opts text NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE UNIQUE INDEX i_sr_group_name ON sr_group (name); + +CREATE TABLE sr_user ( + jid text NOT NULL, + grp text NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE UNIQUE INDEX i_sr_user_jid_grp ON sr_user (jid, grp); +CREATE INDEX i_sr_user_grp ON sr_user (grp); + +CREATE TABLE spool ( + username text NOT NULL, + xml text NOT NULL, + seq INTEGER PRIMARY KEY AUTOINCREMENT, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX i_despool ON spool (username); + +CREATE TABLE archive ( + username text NOT NULL, + timestamp BIGINT UNSIGNED NOT NULL, + peer text NOT NULL, + bare_peer text NOT NULL, + xml text NOT NULL, + txt text, + id INTEGER PRIMARY KEY AUTOINCREMENT, + kind text, + nick text, + origin_id text, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX i_username_timestamp ON archive(username, timestamp); +CREATE INDEX i_archive_username_peer ON archive (username, peer); +CREATE INDEX i_archive_username_bare_peer ON archive (username, bare_peer); +CREATE INDEX i_timestamp ON archive(timestamp); +CREATE INDEX i_archive_username_origin_id ON archive (username, origin_id); + +-- To update 'archive' from ejabberd <= 23.10: +-- ALTER TABLE archive ADD COLUMN origin_id text NOT NULL DEFAULT ''; +-- CREATE INDEX i_archive_username_origin_id ON archive (username, origin_id); + +CREATE TABLE archive_prefs ( + username text NOT NULL PRIMARY KEY, + def text NOT NULL, + always text NOT NULL, + never text NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE vcard ( + username text PRIMARY KEY, + vcard text NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE vcard_search ( + username text NOT NULL, + lusername text PRIMARY KEY, + fn text NOT NULL, + lfn text NOT NULL, + family text NOT NULL, + lfamily text NOT NULL, + given text NOT NULL, + lgiven text NOT NULL, + middle text NOT NULL, + lmiddle text NOT NULL, + nickname text NOT NULL, + lnickname text NOT NULL, + bday text NOT NULL, + lbday text NOT NULL, + ctry text NOT NULL, + lctry text NOT NULL, + locality text NOT NULL, + llocality text NOT NULL, + email text NOT NULL, + lemail text NOT NULL, + orgname text NOT NULL, + lorgname text NOT NULL, + orgunit text NOT NULL, + lorgunit text NOT NULL +); + +CREATE INDEX i_vcard_search_lfn ON vcard_search(lfn); +CREATE INDEX i_vcard_search_lfamily ON vcard_search(lfamily); +CREATE INDEX i_vcard_search_lgiven ON vcard_search(lgiven); +CREATE INDEX i_vcard_search_lmiddle ON vcard_search(lmiddle); +CREATE INDEX i_vcard_search_lnickname ON vcard_search(lnickname); +CREATE INDEX i_vcard_search_lbday ON vcard_search(lbday); +CREATE INDEX i_vcard_search_lctry ON vcard_search(lctry); +CREATE INDEX i_vcard_search_llocality ON vcard_search(llocality); +CREATE INDEX i_vcard_search_lemail ON vcard_search(lemail); +CREATE INDEX i_vcard_search_lorgname ON vcard_search(lorgname); +CREATE INDEX i_vcard_search_lorgunit ON vcard_search(lorgunit); + +CREATE TABLE privacy_default_list ( + username text PRIMARY KEY, + name text NOT NULL +); + +CREATE TABLE privacy_list ( + username text NOT NULL, + name text NOT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE UNIQUE INDEX i_privacy_list_username_name ON privacy_list (username, name); + +CREATE TABLE privacy_list_data ( + id bigint REFERENCES privacy_list(id) ON DELETE CASCADE, + t character(1) NOT NULL, + value text NOT NULL, + action character(1) NOT NULL, + ord NUMERIC NOT NULL, + match_all boolean NOT NULL, + match_iq boolean NOT NULL, + match_message boolean NOT NULL, + match_presence_in boolean NOT NULL, + match_presence_out boolean NOT NULL +); + +CREATE TABLE private_storage ( + username text NOT NULL, + namespace text NOT NULL, + data text NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE UNIQUE INDEX i_private_storage_username_namespace ON private_storage (username, namespace); + + +CREATE TABLE roster_version ( + username text PRIMARY KEY, + version text NOT NULL +); + +CREATE TABLE pubsub_node ( + host text NOT NULL, + node text NOT NULL, + parent text NOT NULL DEFAULT '', + plugin text NOT NULL, + nodeid INTEGER PRIMARY KEY AUTOINCREMENT +); +CREATE INDEX i_pubsub_node_parent ON pubsub_node (parent); +CREATE UNIQUE INDEX i_pubsub_node_tuple ON pubsub_node (host, node); + +CREATE TABLE pubsub_node_option ( + nodeid bigint REFERENCES pubsub_node(nodeid) ON DELETE CASCADE, + name text NOT NULL, + val text NOT NULL +); +CREATE INDEX i_pubsub_node_option_nodeid ON pubsub_node_option (nodeid); + +CREATE TABLE pubsub_node_owner ( + nodeid bigint REFERENCES pubsub_node(nodeid) ON DELETE CASCADE, + owner text NOT NULL +); +CREATE INDEX i_pubsub_node_owner_nodeid ON pubsub_node_owner (nodeid); + +CREATE TABLE pubsub_state ( + nodeid bigint REFERENCES pubsub_node(nodeid) ON DELETE CASCADE, + jid text NOT NULL, + affiliation character(1), + subscriptions text NOT NULL DEFAULT '', + stateid INTEGER PRIMARY KEY AUTOINCREMENT +); +CREATE INDEX i_pubsub_state_jid ON pubsub_state (jid); +CREATE UNIQUE INDEX i_pubsub_state_tuple ON pubsub_state (nodeid, jid); + +CREATE TABLE pubsub_item ( + nodeid bigint REFERENCES pubsub_node(nodeid) ON DELETE CASCADE, + itemid text NOT NULL, + publisher text NOT NULL, + creation varchar(32) NOT NULL, + modification varchar(32) NOT NULL, + payload text NOT NULL DEFAULT '' +); +CREATE INDEX i_pubsub_item_itemid ON pubsub_item (itemid); +CREATE UNIQUE INDEX i_pubsub_item_tuple ON pubsub_item (nodeid, itemid); + + CREATE TABLE pubsub_subscription_opt ( + subid text NOT NULL, + opt_name varchar(32), + opt_value text NOT NULL +); +CREATE UNIQUE INDEX i_pubsub_subscription_opt ON pubsub_subscription_opt (subid, opt_name); + +CREATE TABLE muc_room ( + name text NOT NULL, + host text NOT NULL, + opts text NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE UNIQUE INDEX i_muc_room_name_host ON muc_room (name, host); +CREATE INDEX i_muc_room_host_created_at ON muc_room (host, created_at); + +CREATE TABLE muc_registered ( + jid text NOT NULL, + host text NOT NULL, + nick text NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX i_muc_registered_nick ON muc_registered (nick); +CREATE UNIQUE INDEX i_muc_registered_jid_host ON muc_registered (jid, host); + +CREATE TABLE muc_online_room ( + name text NOT NULL, + host text NOT NULL, + node text NOT NULL, + pid text NOT NULL +); + +CREATE UNIQUE INDEX i_muc_online_room_name_host ON muc_online_room (name, host); + +CREATE TABLE muc_online_users ( + username text NOT NULL, + server text NOT NULL, + resource text NOT NULL, + name text NOT NULL, + host text NOT NULL, + node text NOT NULL +); + +CREATE UNIQUE INDEX i_muc_online_users ON muc_online_users (username, server, resource, name, host); + +CREATE TABLE muc_room_subscribers ( + room text NOT NULL, + host text NOT NULL, + jid text NOT NULL, + nick text NOT NULL, + nodes text NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +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 ( + username text PRIMARY KEY, + xml text, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE caps_features ( + node text NOT NULL, + subnode text NOT NULL, + feature text, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX i_caps_features_node_subnode ON caps_features (node, subnode); + +CREATE TABLE sm ( + usec bigint NOT NULL, + pid text NOT NULL, + node text NOT NULL, + username text NOT NULL, + resource text NOT NULL, + priority text NOT NULL, + info text NOT NULL +); + +CREATE UNIQUE INDEX i_sm_sid ON sm(usec, pid); +CREATE INDEX i_sm_node ON sm(node); +CREATE INDEX i_sm_username ON sm(username); + +CREATE TABLE oauth_token ( + token text NOT NULL PRIMARY KEY, + jid text NOT NULL, + scope text NOT NULL, + expire bigint NOT NULL +); + +CREATE TABLE oauth_client ( + client_id text PRIMARY KEY, + client_name text NOT NULL, + grant_type text NOT NULL, + options text NOT NULL +); + +CREATE TABLE route ( + domain text NOT NULL, + server_host text NOT NULL, + node text NOT NULL, + pid text NOT NULL, + local_hint text NOT NULL +); + +CREATE UNIQUE INDEX i_route ON route(domain, server_host, node, pid); + +CREATE TABLE bosh ( + sid text NOT NULL, + node text NOT NULL, + pid text NOT NULL +); + +CREATE UNIQUE INDEX i_bosh_sid ON bosh(sid); + +CREATE TABLE proxy65 ( + sid text NOT NULL, + pid_t text NOT NULL, + pid_i text NOT NULL, + node_t text NOT NULL, + node_i text NOT NULL, + jid_i text NOT NULL +); + +CREATE UNIQUE INDEX i_proxy65_sid ON proxy65 (sid); +CREATE INDEX i_proxy65_jid ON proxy65 (jid_i); + +CREATE TABLE push_session ( + username text NOT NULL, + timestamp bigint NOT NULL, + service text NOT NULL, + node text NOT NULL, + xml text NOT NULL +); + +CREATE UNIQUE INDEX i_push_usn ON push_session (username, service, node); +CREATE UNIQUE INDEX i_push_ut ON push_session (username, timestamp); + +CREATE TABLE mix_channel ( + channel text NOT NULL, + service text NOT NULL, + username text NOT NULL, + domain text NOT NULL, + jid text NOT NULL, + hidden boolean NOT NULL, + hmac_key text NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE UNIQUE INDEX i_mix_channel ON mix_channel (channel, service); +CREATE INDEX i_mix_channel_serv ON mix_channel (service); + +CREATE TABLE mix_participant ( + channel text NOT NULL, + service text NOT NULL, + username text NOT NULL, + domain text NOT NULL, + jid text NOT NULL, + id text NOT NULL, + nick text NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE UNIQUE INDEX i_mix_participant ON mix_participant (channel, service, username, domain); + +CREATE TABLE mix_subscription ( + channel text NOT NULL, + service text NOT NULL, + username text NOT NULL, + domain text NOT NULL, + node text NOT NULL, + jid text NOT NULL +); + +CREATE UNIQUE INDEX i_mix_subscription ON mix_subscription (channel, service, username, domain, node); +CREATE INDEX i_mix_subscription_chan_serv_node ON mix_subscription (channel, service, node); + +CREATE TABLE mix_pam ( + username text NOT NULL, + channel text NOT NULL, + service text NOT NULL, + id text NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE UNIQUE INDEX i_mix_pam ON mix_pam (username, channel, service); + +CREATE TABLE mqtt_pub ( + username text NOT NULL, + resource text NOT NULL, + topic text NOT NULL, + qos smallint NOT NULL, + payload blob NOT NULL, + payload_format smallint NOT NULL, + content_type text NOT NULL, + response_topic text NOT NULL, + correlation_data blob NOT NULL, + user_properties blob NOT NULL, + expiry bigint NOT NULL +); + +CREATE UNIQUE INDEX i_mqtt_topic ON mqtt_pub (topic); diff --git a/sql/mssql.new.sql b/sql/mssql.new.sql new file mode 100644 index 000000000..f67033eed --- /dev/null +++ b/sql/mssql.new.sql @@ -0,0 +1,652 @@ +-- +-- ejabberd, Copyright (C) 2002-2025 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. +-- + +SET ANSI_PADDING OFF; +SET ANSI_NULLS ON; +SET QUOTED_IDENTIFIER ON; +SET ANSI_PADDING ON; + +CREATE TABLE [dbo].[archive] ( + [username] [varchar] (250) NOT NULL, + [server_host] [varchar] (250) NOT NULL, + [timestamp] [bigint] NOT NULL, + [peer] [varchar] (250) NOT NULL, + [bare_peer] [varchar] (250) NOT NULL, + [xml] [ntext] NOT NULL, + [txt] [ntext] NULL, + [id] [bigint] IDENTITY(1,1) NOT NULL, + [kind] [varchar] (10) NULL, + [nick] [varchar] (250) NULL, + [origin_id] [varchar] (250) NOT NULL, + [created_at] [datetime] NOT NULL DEFAULT GETDATE(), + CONSTRAINT [archive_PK] PRIMARY KEY CLUSTERED +( + [id] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) +) TEXTIMAGE_ON [PRIMARY]; + +CREATE INDEX [archive_sh_username_timestamp] ON [archive] (server_host, username, timestamp) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [archive_sh_username_peer] ON [archive] (server_host, username, peer) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [archive_sh_username_bare_peer] ON [archive] (server_host, username, bare_peer) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [archive_sh_timestamp] ON [archive] (server_host, timestamp) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [archive_sh_username_origin_id] ON [archive] (server_host, username, origin_id) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[archive_prefs] ( + [username] [varchar] (250) NOT NULL, + [server_host] [varchar] (250) NOT NULL, + [def] [text] NOT NULL, + [always] [text] NOT NULL, + [never] [text] NOT NULL, + [created_at] [datetime] NOT NULL DEFAULT GETDATE(), + CONSTRAINT [archive_prefs_PRIMARY] PRIMARY KEY CLUSTERED +( + [server_host] ASC, + [username] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) +) TEXTIMAGE_ON [PRIMARY]; + +CREATE TABLE [dbo].[caps_features] ( + [node] [varchar] (250) NOT NULL, + [subnode] [varchar] (250) NOT NULL, + [feature] [text] NULL, + [created_at] [datetime] NOT NULL DEFAULT GETDATE() +) TEXTIMAGE_ON [PRIMARY]; + +CREATE CLUSTERED INDEX [caps_features_node_subnode] ON [caps_features] (node, subnode) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[last] ( + [username] [varchar] (250) NOT NULL, + [server_host] [varchar] (250) NOT NULL, + [seconds] [text] NOT NULL, + [state] [text] NOT NULL, + CONSTRAINT [last_PRIMARY] PRIMARY KEY CLUSTERED +( + [server_host] ASC, + [username] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) +) TEXTIMAGE_ON [PRIMARY]; + +CREATE TABLE [dbo].[motd] ( + [username] [varchar] (250) NOT NULL, + [server_host] [varchar] (250) NOT NULL, + [xml] [text] NULL, + [created_at] [datetime] NOT NULL DEFAULT GETDATE(), + CONSTRAINT [motd_PRIMARY] PRIMARY KEY CLUSTERED +( + [server_host] ASC, + [username] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) +) TEXTIMAGE_ON [PRIMARY]; + +CREATE TABLE [dbo].[muc_registered] ( + [jid] [varchar] (255) NOT NULL, + [host] [varchar] (255) NOT NULL, + [server_host] [varchar] (250) NOT NULL, + [nick] [varchar] (255) NOT NULL, + [created_at] [datetime] NOT NULL DEFAULT GETDATE() +); + +CREATE INDEX [muc_registered_nick] ON [muc_registered] (nick) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE UNIQUE CLUSTERED INDEX [muc_registered_jid_host] ON [muc_registered] (jid, host) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[muc_room] ( + [name] [varchar] (250) NOT NULL, + [host] [varchar] (250) NOT NULL, + [server_host] [varchar] (250) NOT NULL, + [opts] [text] NOT NULL, + [created_at] [datetime] NOT NULL DEFAULT GETDATE() +) TEXTIMAGE_ON [PRIMARY]; + +CREATE UNIQUE CLUSTERED INDEX [muc_room_name_host] ON [muc_room] (name, host) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); +CREATE INDEX [muc_room_host_created_at] ON [muc_registered] (host, nick) + WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[muc_online_room] ( + [name] [varchar] (250) NOT NULL, + [host] [varchar] (250) NOT NULL, + [server_host] [varchar] (250) NOT NULL, + [node] [varchar] (250) NOT NULL, + [pid] [varchar] (100) NOT NULL +); + +CREATE UNIQUE CLUSTERED INDEX [muc_online_room_name_host] ON [muc_online_room] (name, host) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[muc_online_users] ( + [username] [varchar] (250) NOT NULL, + [server] [varchar] (250) NOT NULL, + [resource] [varchar] (250) NOT NULL, + [name] [varchar] (250) NOT NULL, + [host] [varchar] (250) NOT NULL, + [server_host] [varchar] (250) NOT NULL, + [node] [varchar] (250) NOT NULL +); + +CREATE UNIQUE INDEX [muc_online_users_i] ON [muc_online_users] (username, server, resource, name, host) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[muc_room_subscribers] ( + [room] [varchar] (191) NOT NULL, + [host] [varchar] (191) NOT NULL, + [jid] [varchar] (191) NOT NULL, + [nick] [text] NOT NULL, + [nodes] [text] NOT NULL, + [created_at] [datetime] NOT NULL DEFAULT GETDATE() +); + +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, + [server_host] [varchar] (250) NOT NULL, + [name] [varchar] (250) NOT NULL, + CONSTRAINT [privacy_default_list_PRIMARY] PRIMARY KEY CLUSTERED +( + [server_host] ASC, + [username] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) +); + +CREATE TABLE [dbo].[privacy_list] ( + [username] [varchar] (250) NOT NULL, + [server_host] [varchar] (250) NOT NULL, + [name] [varchar] (250) NOT NULL, + [id] [bigint] IDENTITY(1,1) NOT NULL, + [created_at] [datetime] NOT NULL DEFAULT GETDATE(), + CONSTRAINT [privacy_list_PK] PRIMARY KEY CLUSTERED +( + [id] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) +); + +CREATE UNIQUE INDEX [privacy_list_sh_username_name] ON [privacy_list] (server_host, username, name) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[privacy_list_data] ( + [id] [bigint] NULL, + [t] [char] (1) NOT NULL, + [value] [text] NOT NULL, + [action] [char] (1) NOT NULL, + [ord] [smallint] NOT NULL, + [match_all] [smallint] NOT NULL, + [match_iq] [smallint] NOT NULL, + [match_message] [smallint] NOT NULL, + [match_presence_in] [smallint] NOT NULL, + [match_presence_out] [smallint] NOT NULL +) TEXTIMAGE_ON [PRIMARY]; + +CREATE CLUSTERED INDEX [privacy_list_data_id] ON [privacy_list_data] (id) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[private_storage] ( + [username] [varchar] (250) NOT NULL, + [server_host] [varchar] (250) NOT NULL, + [namespace] [varchar] (250) NOT NULL, + [data] [text] NOT NULL, + [created_at] [datetime] NOT NULL DEFAULT GETDATE() +) TEXTIMAGE_ON [PRIMARY]; + +CREATE UNIQUE CLUSTERED INDEX [private_storage_sh_username_namespace] ON [private_storage] (server_host, username, namespace) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[pubsub_item] ( + [nodeid] [bigint] NULL, + [itemid] [varchar] (255) NOT NULL, + [publisher] [varchar] (250) NOT NULL, + [creation] [varchar] (32) NOT NULL, + [modification] [varchar] (32) NOT NULL, + [payload] [text] NOT NULL DEFAULT '' +) TEXTIMAGE_ON [PRIMARY]; + +CREATE INDEX [pubsub_item_itemid] ON [pubsub_item] (itemid) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE UNIQUE CLUSTERED INDEX [pubsub_item_nodeid_itemid] ON [pubsub_item] (nodeid, itemid) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[pubsub_node_option] ( + [nodeid] [bigint] NULL, + [name] [varchar] (250) NOT NULL, + [val] [varchar] (250) NOT NULL +); + +CREATE CLUSTERED INDEX [pubsub_node_option_nodeid] ON [pubsub_node_option] (nodeid) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[pubsub_node_owner] ( + [nodeid] [bigint] NULL, + [owner] [text] NOT NULL +) TEXTIMAGE_ON [PRIMARY]; + +CREATE CLUSTERED INDEX [pubsub_node_owner_nodeid] ON [pubsub_node_owner] (nodeid) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[pubsub_state] ( + [nodeid] [bigint] NULL, + [jid] [varchar] (255) NOT NULL, + [affiliation] [char] (1) NOT NULL, + [subscriptions] [text] NOT NULL DEFAULT '', + [stateid] [bigint] IDENTITY(1,1) NOT NULL, + CONSTRAINT [pubsub_state_PRIMARY] PRIMARY KEY CLUSTERED +( + [stateid] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) +) TEXTIMAGE_ON [PRIMARY]; + +CREATE INDEX [pubsub_state_jid] ON [pubsub_state] (jid) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE UNIQUE INDEX [pubsub_state_nodeid_jid] ON [pubsub_state] (nodeid, jid) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[pubsub_subscription_opt] ( + [subid] [varchar] (255) NOT NULL, + [opt_name] [varchar] (32) NOT NULL, + [opt_value] [text] NOT NULL +) TEXTIMAGE_ON [PRIMARY]; + +CREATE UNIQUE CLUSTERED INDEX [pubsub_subscription_opt_subid_opt_name] ON [pubsub_subscription_opt] (subid, opt_name) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[pubsub_node] ( + [host] [varchar] (255) NOT NULL, + [node] [varchar] (255) NOT NULL, + [parent] [varchar] (255) NOT NULL DEFAULT '', + [plugin] [varchar] (32) NOT NULL, + [nodeid] [bigint] IDENTITY(1,1) NOT NULL, + CONSTRAINT [pubsub_node_PRIMARY] PRIMARY KEY CLUSTERED +( + [nodeid] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) +); + +CREATE INDEX [pubsub_node_parent] ON [pubsub_node] (parent) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE UNIQUE INDEX [pubsub_node_host_node] ON [pubsub_node] (host, node) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[roster_version] ( + [username] [varchar] (250) NOT NULL, + [server_host] [varchar] (250) NOT NULL, + [version] [text] NOT NULL, + CONSTRAINT [roster_version_PRIMARY] PRIMARY KEY CLUSTERED +( + [server_host] ASC, + [username] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) +) TEXTIMAGE_ON [PRIMARY]; + +CREATE TABLE [dbo].[rostergroups] ( + [username] [varchar] (250) NOT NULL, + [server_host] [varchar] (250) NOT NULL, + [jid] [varchar] (250) NOT NULL, + [grp] [text] NOT NULL +) TEXTIMAGE_ON [PRIMARY]; + +CREATE CLUSTERED INDEX [rostergroups_sh_username_jid] ON [rostergroups] ([server_host], [username], [jid]) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[rosterusers] ( + [username] [varchar] (250) NOT NULL, + [server_host] [varchar] (250) NOT NULL, + [jid] [varchar] (250) NOT NULL, + [nick] [text] NOT NULL, + [subscription] [char] (1) NOT NULL, + [ask] [char] (1) NOT NULL, + [askmessage] [text] NOT NULL, + [server] [char] (1) NOT NULL, + [subscribe] [text] NOT NULL, + [type] [text] NULL, + [created_at] [datetime] NOT NULL DEFAULT GETDATE() +) TEXTIMAGE_ON [PRIMARY]; + +CREATE UNIQUE CLUSTERED INDEX [rosterusers_sh_username_jid] ON [rosterusers] ([server_host], [username], [jid]) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [rosterusers_sh_jid] ON [rosterusers] ([server_host], [jid]) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[sm] ( + [usec] [bigint] NOT NULL, + [pid] [varchar] (100) NOT NULL, + [node] [varchar] (255) NOT NULL, + [username] [varchar] (255) NOT NULL, + [server_host] [varchar] (250) NOT NULL, + [resource] [varchar] (255) NOT NULL, + [priority] [text] NOT NULL, + [info] [text] NOT NULL +) TEXTIMAGE_ON [PRIMARY]; + +CREATE UNIQUE CLUSTERED INDEX [sm_sid] ON [sm] (usec, pid) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [sm_node] ON [sm] (node) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [sm_sh_username] ON [sm] (server_host, username) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[spool] ( + [username] [varchar] (250) NOT NULL, + [server_host] [varchar] (250) NOT NULL, + [xml] [text] NOT NULL, + [seq] [bigint] IDENTITY(1,1) NOT NULL, + [created_at] [datetime] NOT NULL DEFAULT GETDATE(), + CONSTRAINT [spool_PK] PRIMARY KEY CLUSTERED +( + [seq] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) +) TEXTIMAGE_ON [PRIMARY]; + +CREATE INDEX [spool_sh_username] ON [spool] (server_host, username) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [spool_created_at] ON [spool] (created_at) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) +; + +CREATE TABLE [dbo].[sr_group] ( + [name] [varchar] (250) NOT NULL, + [server_host] [varchar] (250) NOT NULL, + [opts] [text] NOT NULL, + [created_at] [datetime] NOT NULL DEFAULT GETDATE() +) TEXTIMAGE_ON [PRIMARY]; + +CREATE UNIQUE CLUSTERED INDEX [sr_group_sh_name] ON [sr_group] ([server_host], [name]) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[sr_user] ( + [jid] [varchar] (250) NOT NULL, + [server_host] [varchar] (250) NOT NULL, + [grp] [varchar] (250) NOT NULL, + [created_at] [datetime] NOT NULL DEFAULT GETDATE() +); + +CREATE UNIQUE CLUSTERED INDEX [sr_user_sh_jid_group] ON [sr_user] ([server_host], [jid], [grp]) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [sr_user_sh_grp] ON [sr_user] ([server_host], [grp]) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[users] ( + [username] [varchar] (250) NOT NULL, + [server_host] [varchar] (250) NOT NULL, + [type] [smallint] NOT NULL, + [password] [text] NOT NULL, + [serverkey] [text] NOT NULL DEFAULT '', + [salt] [text] NOT NULL DEFAULT '', + [iterationcount] [smallint] NOT NULL DEFAULT 0, + [created_at] [datetime] NOT NULL DEFAULT GETDATE(), + CONSTRAINT [users_PRIMARY] PRIMARY KEY CLUSTERED +( + [server_host] ASC, + [username] ASC, + [type] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) +) TEXTIMAGE_ON [PRIMARY]; + +CREATE TABLE [dbo].[vcard] ( + [username] [varchar] (250) NOT NULL, + [server_host] [varchar] (250) NOT NULL, + [vcard] [text] NOT NULL, + [created_at] [datetime] NOT NULL DEFAULT GETDATE(), + CONSTRAINT [vcard_PRIMARY] PRIMARY KEY CLUSTERED +( + [server_host] ASC, + [username] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) +) TEXTIMAGE_ON [PRIMARY]; + +CREATE TABLE [dbo].[vcard_search] ( + [username] [varchar] (250) NOT NULL, + [lusername] [varchar] (250) NOT NULL, + [server_host] [varchar] (250) NOT NULL, + [fn] [text] NOT NULL, + [lfn] [varchar] (250) NOT NULL, + [family] [text] NOT NULL, + [lfamily] [varchar] (250) NOT NULL, + [given] [text] NOT NULL, + [lgiven] [varchar] (250) NOT NULL, + [middle] [text] NOT NULL, + [lmiddle] [varchar] (250) NOT NULL, + [nickname] [text] NOT NULL, + [lnickname] [varchar] (250) NOT NULL, + [bday] [text] NOT NULL, + [lbday] [varchar] (250) NOT NULL, + [ctry] [text] NOT NULL, + [lctry] [varchar] (250) NOT NULL, + [locality] [text] NOT NULL, + [llocality] [varchar] (250) NOT NULL, + [email] [text] NOT NULL, + [lemail] [varchar] (250) NOT NULL, + [orgname] [text] NOT NULL, + [lorgname] [varchar] (250) NOT NULL, + [orgunit] [text] NOT NULL, + [lorgunit] [varchar] (250) NOT NULL, + CONSTRAINT [vcard_search_PRIMARY] PRIMARY KEY CLUSTERED +( + [server_host] ASC, + [lusername] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) +) TEXTIMAGE_ON [PRIMARY]; + +CREATE INDEX [vcard_search_sh_lfn] ON [vcard_search] (server_host, lfn) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [vcard_search_sh_lfamily] ON [vcard_search] (server_host, lfamily) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [vcard_search_sh_lgiven] ON [vcard_search] (server_host, lgiven) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [vcard_search_sh_lmiddle] ON [vcard_search] (server_host, lmiddle) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [vcard_search_sh_lnickname] ON [vcard_search] (server_host, lnickname) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [vcard_search_sh_lbday] ON [vcard_search] (server_host, lbday) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [vcard_search_sh_lctry] ON [vcard_search] (server_host, lctry) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [vcard_search_sh_llocality] ON [vcard_search] (server_host, llocality) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [vcard_search_sh_lemail] ON [vcard_search] (server_host, lemail) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [vcard_search_sh_lorgname] ON [vcard_search] (server_host, lorgname) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [vcard_search_sh_lorgunit] ON [vcard_search] (server_host, lorgunit) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +ALTER TABLE [dbo].[pubsub_item] WITH CHECK ADD CONSTRAINT [pubsub_item_ibfk_1] FOREIGN KEY([nodeid]) +REFERENCES [dbo].[pubsub_node] ([nodeid]) +ON DELETE CASCADE; + +ALTER TABLE [dbo].[pubsub_item] CHECK CONSTRAINT [pubsub_item_ibfk_1]; + +ALTER TABLE [dbo].[pubsub_node_option] WITH CHECK ADD CONSTRAINT [pubsub_node_option_ibfk_1] FOREIGN KEY([nodeid]) +REFERENCES [dbo].[pubsub_node] ([nodeid]) +ON DELETE CASCADE; + +ALTER TABLE [dbo].[pubsub_node_option] CHECK CONSTRAINT [pubsub_node_option_ibfk_1]; + +ALTER TABLE [dbo].[pubsub_node_owner] WITH CHECK ADD CONSTRAINT [pubsub_node_owner_ibfk_1] FOREIGN KEY([nodeid]) +REFERENCES [dbo].[pubsub_node] ([nodeid]) +ON DELETE CASCADE; + +ALTER TABLE [dbo].[pubsub_node_owner] CHECK CONSTRAINT [pubsub_node_owner_ibfk_1]; + +ALTER TABLE [dbo].[pubsub_state] WITH CHECK ADD CONSTRAINT [pubsub_state_ibfk_1] FOREIGN KEY([nodeid]) +REFERENCES [dbo].[pubsub_node] ([nodeid]) +ON DELETE CASCADE; + +ALTER TABLE [dbo].[pubsub_state] CHECK CONSTRAINT [pubsub_state_ibfk_1]; + +CREATE TABLE [dbo].[oauth_token] ( + [token] [varchar] (250) NOT NULL, + [jid] [text] NOT NULL, + [scope] [text] NOT NULL, + [expire] [bigint] NOT NULL, + CONSTRAINT [oauth_token_PRIMARY] PRIMARY KEY CLUSTERED +( + [token] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) +) TEXTIMAGE_ON [PRIMARY]; + +CREATE TABLE [dbo].[route] ( + [domain] [varchar] (255) NOT NULL, + [server_host] [varchar] (255) NOT NULL, + [node] [varchar] (255) NOT NULL, + [pid] [varchar](100) NOT NULL, + [local_hint] [text] NOT NULL +); + +CREATE UNIQUE CLUSTERED INDEX [route_i] ON [route] (domain, server_host, node, pid) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[bosh] ( + [sid] [varchar] (255) NOT NULL, + [node] [varchar] (255) NOT NULL, + [pid] [varchar](100) NOT NULL + CONSTRAINT [bosh_PRIMARY] PRIMARY KEY CLUSTERED +( + [sid] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) +); + +CREATE TABLE [dbo].[push_session] ( + [username] [varchar] (255) NOT NULL, + [server_host] [varchar] (250) NOT NULL, + [timestamp] [bigint] NOT NULL, + [service] [varchar] (255) NOT NULL, + [node] [varchar] (255) NOT NULL, + [xml] [varchar] (255) NOT NULL +); + +CREATE UNIQUE NONCLUSTERED INDEX [push_session_susn] ON [push_session] (server_host, username, service, node) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [push_session_sh_username_timestamp] ON [push_session] (server_host, username, timestamp) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[mix_channel] ( + [channel] [varchar] (250) NOT NULL, + [service] [varchar] (250) NOT NULL, + [username] [varchar] (250) NOT NULL, + [domain] [varchar] (250) NOT NULL, + [jid] [varchar] (250) NOT NULL, + [hidden] [smallint] NOT NULL, + [hmac_key] [text] NOT NULL, + [created_at] [datetime] NOT NULL DEFAULT GETDATE() +) TEXTIMAGE_ON [PRIMARY]; + +CREATE UNIQUE CLUSTERED INDEX [mix_channel] ON [mix_channel] (channel, service) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [mix_channel_serv] ON [mix_channel] (service) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[mix_participant] ( + [channel] [varchar] (250) NOT NULL, + [service] [varchar] (250) NOT NULL, + [username] [varchar] (250) NOT NULL, + [domain] [varchar] (250) NOT NULL, + [jid] [varchar] (250) NOT NULL, + [id] [text] NOT NULL, + [nick] [text] NOT NULL, + [created_at] [datetime] NOT NULL DEFAULT GETDATE() +) TEXTIMAGE_ON [PRIMARY]; + +CREATE UNIQUE INDEX [mix_participant] ON [mix_participant] (channel, service, username, domain) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [mix_participant_chan_serv] ON [mix_participant] (channel, service) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[mix_subscription] ( + [channel] [varchar] (250) NOT NULL, + [service] [varchar] (250) NOT NULL, + [username] [varchar] (250) NOT NULL, + [domain] [varchar] (250) NOT NULL, + [node] [varchar] (250) NOT NULL, + [jid] [varchar] (250) NOT NULL +); + +CREATE UNIQUE INDEX [mix_subscription] ON [mix_subscription] (channel, service, username, domain, node) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [mix_subscription_chan_serv_ud] ON [mix_subscription] (channel, service, username, domain) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [mix_subscription_chan_serv_node] ON [mix_subscription] (channel, service, node) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [mix_subscription_chan_serv] ON [mix_subscription] (channel, service) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[mix_pam] ( + [username] [varchar] (250) NOT NULL, + [server_host] [varchar] (250) NOT NULL, + [channel] [varchar] (250) NOT NULL, + [service] [varchar] (250) NOT NULL, + [id] [text] NOT NULL, + [created_at] [datetime] NOT NULL DEFAULT GETDATE() +) TEXTIMAGE_ON [PRIMARY]; + +CREATE UNIQUE NONCLUSTERED INDEX [mix_pam] ON [mix_pam] (username, server_host, channel, service) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[mqtt_pub] ( + [username] [varchar] (250) NOT NULL, + [server_host] [varchar] (250) NOT NULL, + [resource] [varchar] (250) NOT NULL, + [topic] [varchar] (250) NOT NULL, + [qos] [tinyint] NOT NULL, + [payload] [varbinary](max) NOT NULL, + [payload_format] [tinyint] NOT NULL, + [content_type] [text] NOT NULL, + [response_topic] [text] NOT NULL, + [correlation_data] [varbinary](max) NOT NULL, + [user_properties] [varbinary](max) NOT NULL, + [expiry] [int] NOT NULL +) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]; + +CREATE UNIQUE CLUSTERED INDEX [mqtt_topic_server] ON [mqtt_pub] (topic, server_host) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); diff --git a/sql/mssql.sql b/sql/mssql.sql new file mode 100644 index 000000000..ab5596d48 --- /dev/null +++ b/sql/mssql.sql @@ -0,0 +1,620 @@ +-- +-- ejabberd, Copyright (C) 2002-2025 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. +-- + +SET ANSI_PADDING OFF; +SET ANSI_NULLS ON; +SET QUOTED_IDENTIFIER ON; +SET ANSI_PADDING ON; + +CREATE TABLE [dbo].[archive] ( + [username] [varchar] (250) NOT NULL, + [timestamp] [bigint] NOT NULL, + [peer] [varchar] (250) NOT NULL, + [bare_peer] [varchar] (250) NOT NULL, + [xml] [ntext] NOT NULL, + [txt] [ntext] NULL, + [id] [bigint] IDENTITY(1,1) NOT NULL, + [kind] [varchar] (10) NULL, + [nick] [varchar] (250) NULL, + [origin_id] [varchar] (250) NOT NULL, + [created_at] [datetime] NOT NULL DEFAULT GETDATE(), + CONSTRAINT [archive_PK] PRIMARY KEY CLUSTERED +( + [id] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) +) TEXTIMAGE_ON [PRIMARY]; + +CREATE INDEX [archive_username_timestamp] ON [archive] (username, timestamp) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [archive_username_peer] ON [archive] (username, peer) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [archive_username_bare_peer] ON [archive] (username, bare_peer) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [archive_timestamp] ON [archive] (timestamp) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [archive_username_origin_id] ON [archive] (username, origin_id) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[archive_prefs] ( + [username] [varchar] (250) NOT NULL, + [def] [text] NOT NULL, + [always] [text] NOT NULL, + [never] [text] NOT NULL, + [created_at] [datetime] NOT NULL DEFAULT GETDATE(), + CONSTRAINT [archive_prefs_PRIMARY] PRIMARY KEY CLUSTERED +( + [username] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) +) TEXTIMAGE_ON [PRIMARY]; + +CREATE TABLE [dbo].[caps_features] ( + [node] [varchar] (250) NOT NULL, + [subnode] [varchar] (250) NOT NULL, + [feature] [text] NULL, + [created_at] [datetime] NOT NULL DEFAULT GETDATE() +) TEXTIMAGE_ON [PRIMARY]; + +CREATE CLUSTERED INDEX [caps_features_node_subnode] ON [caps_features] (node, subnode) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[last] ( + [username] [varchar] (250) NOT NULL, + [seconds] [text] NOT NULL, + [state] [text] NOT NULL, + CONSTRAINT [last_PRIMARY] PRIMARY KEY CLUSTERED +( + [username] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) +) TEXTIMAGE_ON [PRIMARY]; + +CREATE TABLE [dbo].[motd] ( + [username] [varchar] (250) NOT NULL, + [xml] [text] NULL, + [created_at] [datetime] NOT NULL DEFAULT GETDATE(), + CONSTRAINT [motd_PRIMARY] PRIMARY KEY CLUSTERED +( + [username] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) +) TEXTIMAGE_ON [PRIMARY]; + +CREATE TABLE [dbo].[muc_registered] ( + [jid] [varchar] (255) NOT NULL, + [host] [varchar] (255) NOT NULL, + [nick] [varchar] (255) NOT NULL, + [created_at] [datetime] NOT NULL DEFAULT GETDATE() +); + +CREATE INDEX [muc_registered_nick] ON [muc_registered] (nick) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE UNIQUE CLUSTERED INDEX [muc_registered_jid_host] ON [muc_registered] (jid, host) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[muc_room] ( + [name] [varchar] (250) NOT NULL, + [host] [varchar] (250) NOT NULL, + [opts] [text] NOT NULL, + [created_at] [datetime] NOT NULL DEFAULT GETDATE() +) TEXTIMAGE_ON [PRIMARY]; + +CREATE UNIQUE CLUSTERED INDEX [muc_room_name_host] ON [muc_room] (name, host) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); +CREATE INDEX [muc_room_host_created_at] ON [muc_registered] (host, nick) + WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[muc_online_room] ( + [name] [varchar] (250) NOT NULL, + [host] [varchar] (250) NOT NULL, + [node] [varchar] (250) NOT NULL, + [pid] [varchar] (100) NOT NULL +); + +CREATE UNIQUE CLUSTERED INDEX [muc_online_room_name_host] ON [muc_online_room] (name, host) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[muc_online_users] ( + [username] [varchar] (250) NOT NULL, + [server] [varchar] (250) NOT NULL, + [resource] [varchar] (250) NOT NULL, + [name] [varchar] (250) NOT NULL, + [host] [varchar] (250) NOT NULL, + [node] [varchar] (250) NOT NULL +); + +CREATE UNIQUE INDEX [muc_online_users_i] ON [muc_online_users] (username, server, resource, name, host) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[muc_room_subscribers] ( + [room] [varchar] (191) NOT NULL, + [host] [varchar] (191) NOT NULL, + [jid] [varchar] (191) NOT NULL, + [nick] [text] NOT NULL, + [nodes] [text] NOT NULL, + [created_at] [datetime] NOT NULL DEFAULT GETDATE() +); + +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, + [name] [varchar] (250) NOT NULL, + CONSTRAINT [privacy_default_list_PRIMARY] PRIMARY KEY CLUSTERED +( + [username] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) +); + +CREATE TABLE [dbo].[privacy_list] ( + [username] [varchar] (250) NOT NULL, + [name] [varchar] (250) NOT NULL, + [id] [bigint] IDENTITY(1,1) NOT NULL, + [created_at] [datetime] NOT NULL DEFAULT GETDATE(), + CONSTRAINT [privacy_list_PK] PRIMARY KEY CLUSTERED +( + [id] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) +); + +CREATE UNIQUE INDEX [privacy_list_username_name] ON [privacy_list] (username, name) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[privacy_list_data] ( + [id] [bigint] NULL, + [t] [char] (1) NOT NULL, + [value] [text] NOT NULL, + [action] [char] (1) NOT NULL, + [ord] [smallint] NOT NULL, + [match_all] [smallint] NOT NULL, + [match_iq] [smallint] NOT NULL, + [match_message] [smallint] NOT NULL, + [match_presence_in] [smallint] NOT NULL, + [match_presence_out] [smallint] NOT NULL +) TEXTIMAGE_ON [PRIMARY]; + +CREATE CLUSTERED INDEX [privacy_list_data_id] ON [privacy_list_data] (id) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[private_storage] ( + [username] [varchar] (250) NOT NULL, + [namespace] [varchar] (250) NOT NULL, + [data] [text] NOT NULL, + [created_at] [datetime] NOT NULL DEFAULT GETDATE() +) TEXTIMAGE_ON [PRIMARY]; + +CREATE UNIQUE CLUSTERED INDEX [private_storage_username_namespace] ON [private_storage] (username, namespace) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[pubsub_item] ( + [nodeid] [bigint] NULL, + [itemid] [varchar] (255) NOT NULL, + [publisher] [varchar] (250) NOT NULL, + [creation] [varchar] (32) NOT NULL, + [modification] [varchar] (32) NOT NULL, + [payload] [text] NOT NULL DEFAULT '' +) TEXTIMAGE_ON [PRIMARY]; + +CREATE INDEX [pubsub_item_itemid] ON [pubsub_item] (itemid) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE UNIQUE CLUSTERED INDEX [pubsub_item_nodeid_itemid] ON [pubsub_item] (nodeid, itemid) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[pubsub_node_option] ( + [nodeid] [bigint] NULL, + [name] [varchar] (250) NOT NULL, + [val] [varchar] (250) NOT NULL +); + +CREATE CLUSTERED INDEX [pubsub_node_option_nodeid] ON [pubsub_node_option] (nodeid) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[pubsub_node_owner] ( + [nodeid] [bigint] NULL, + [owner] [text] NOT NULL +) TEXTIMAGE_ON [PRIMARY]; + +CREATE CLUSTERED INDEX [pubsub_node_owner_nodeid] ON [pubsub_node_owner] (nodeid) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[pubsub_state] ( + [nodeid] [bigint] NULL, + [jid] [varchar] (255) NOT NULL, + [affiliation] [char] (1) NOT NULL, + [subscriptions] [text] NOT NULL DEFAULT '', + [stateid] [bigint] IDENTITY(1,1) NOT NULL, + CONSTRAINT [pubsub_state_PRIMARY] PRIMARY KEY CLUSTERED +( + [stateid] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) +) TEXTIMAGE_ON [PRIMARY]; + +CREATE INDEX [pubsub_state_jid] ON [pubsub_state] (jid) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE UNIQUE INDEX [pubsub_state_nodeid_jid] ON [pubsub_state] (nodeid, jid) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[pubsub_subscription_opt] ( + [subid] [varchar] (255) NOT NULL, + [opt_name] [varchar] (32) NOT NULL, + [opt_value] [text] NOT NULL +) TEXTIMAGE_ON [PRIMARY]; + +CREATE UNIQUE CLUSTERED INDEX [pubsub_subscription_opt_subid_opt_name] ON [pubsub_subscription_opt] (subid, opt_name) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[pubsub_node] ( + [host] [varchar] (255) NOT NULL, + [node] [varchar] (255) NOT NULL, + [parent] [varchar] (255) NOT NULL DEFAULT '', + [plugin] [varchar] (32) NOT NULL, + [nodeid] [bigint] IDENTITY(1,1) NOT NULL, + CONSTRAINT [pubsub_node_PRIMARY] PRIMARY KEY CLUSTERED +( + [nodeid] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) +); + +CREATE INDEX [pubsub_node_parent] ON [pubsub_node] (parent) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE UNIQUE INDEX [pubsub_node_host_node] ON [pubsub_node] (host, node) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[roster_version] ( + [username] [varchar] (250) NOT NULL, + [version] [text] NOT NULL, + CONSTRAINT [roster_version_PRIMARY] PRIMARY KEY CLUSTERED +( + [username] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) +) TEXTIMAGE_ON [PRIMARY]; + +CREATE TABLE [dbo].[rostergroups] ( + [username] [varchar] (250) NOT NULL, + [jid] [varchar] (250) NOT NULL, + [grp] [text] NOT NULL +) TEXTIMAGE_ON [PRIMARY]; + +CREATE CLUSTERED INDEX [rostergroups_username_jid] ON [rostergroups] ([username], [jid]) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[rosterusers] ( + [username] [varchar] (250) NOT NULL, + [jid] [varchar] (250) NOT NULL, + [nick] [text] NOT NULL, + [subscription] [char] (1) NOT NULL, + [ask] [char] (1) NOT NULL, + [askmessage] [text] NOT NULL, + [server] [char] (1) NOT NULL, + [subscribe] [text] NOT NULL, + [type] [text] NULL, + [created_at] [datetime] NOT NULL DEFAULT GETDATE() +) TEXTIMAGE_ON [PRIMARY]; + +CREATE UNIQUE CLUSTERED INDEX [rosterusers_username_jid] ON [rosterusers] ([username], [jid]) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [rosterusers_jid] ON [rosterusers] ([jid]) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[sm] ( + [usec] [bigint] NOT NULL, + [pid] [varchar] (100) NOT NULL, + [node] [varchar] (255) NOT NULL, + [username] [varchar] (255) NOT NULL, + [resource] [varchar] (255) NOT NULL, + [priority] [text] NOT NULL, + [info] [text] NOT NULL +) TEXTIMAGE_ON [PRIMARY]; + +CREATE UNIQUE CLUSTERED INDEX [sm_sid] ON [sm] (usec, pid) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [sm_node] ON [sm] (node) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [sm_username] ON [sm] (username) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[spool] ( + [username] [varchar] (250) NOT NULL, + [xml] [text] NOT NULL, + [seq] [bigint] IDENTITY(1,1) NOT NULL, + [created_at] [datetime] NOT NULL DEFAULT GETDATE(), + CONSTRAINT [spool_PK] PRIMARY KEY CLUSTERED +( + [seq] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) +) TEXTIMAGE_ON [PRIMARY]; + +CREATE INDEX [spool_username] ON [spool] (username) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [spool_created_at] ON [spool] (created_at) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) +; + +CREATE TABLE [dbo].[sr_group] ( + [name] [varchar] (250) NOT NULL, + [opts] [text] NOT NULL, + [created_at] [datetime] NOT NULL DEFAULT GETDATE() +) TEXTIMAGE_ON [PRIMARY]; + +CREATE UNIQUE CLUSTERED INDEX [sr_group_name] ON [sr_group] ([name]) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[sr_user] ( + [jid] [varchar] (250) NOT NULL, + [grp] [varchar] (250) NOT NULL, + [created_at] [datetime] NOT NULL DEFAULT GETDATE() +); + +CREATE UNIQUE CLUSTERED INDEX [sr_user_jid_group] ON [sr_user] ([jid], [grp]) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [sr_user_grp] ON [sr_user] ([grp]) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[users] ( + [username] [varchar] (250) NOT NULL, + [type] [smallint] NOT NULL, + [password] [text] NOT NULL, + [serverkey] [text] NOT NULL DEFAULT '', + [salt] [text] NOT NULL DEFAULT '', + [iterationcount] [smallint] NOT NULL DEFAULT 0, + [created_at] [datetime] NOT NULL DEFAULT GETDATE(), + CONSTRAINT [users_PRIMARY] PRIMARY KEY CLUSTERED +( + [username] ASC, + [type] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) +) TEXTIMAGE_ON [PRIMARY]; + +CREATE TABLE [dbo].[vcard] ( + [username] [varchar] (250) NOT NULL, + [vcard] [text] NOT NULL, + [created_at] [datetime] NOT NULL DEFAULT GETDATE(), + CONSTRAINT [vcard_PRIMARY] PRIMARY KEY CLUSTERED +( + [username] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) +) TEXTIMAGE_ON [PRIMARY]; + +CREATE TABLE [dbo].[vcard_search] ( + [username] [varchar] (250) NOT NULL, + [lusername] [varchar] (250) NOT NULL, + [fn] [text] NOT NULL, + [lfn] [varchar] (250) NOT NULL, + [family] [text] NOT NULL, + [lfamily] [varchar] (250) NOT NULL, + [given] [text] NOT NULL, + [lgiven] [varchar] (250) NOT NULL, + [middle] [text] NOT NULL, + [lmiddle] [varchar] (250) NOT NULL, + [nickname] [text] NOT NULL, + [lnickname] [varchar] (250) NOT NULL, + [bday] [text] NOT NULL, + [lbday] [varchar] (250) NOT NULL, + [ctry] [text] NOT NULL, + [lctry] [varchar] (250) NOT NULL, + [locality] [text] NOT NULL, + [llocality] [varchar] (250) NOT NULL, + [email] [text] NOT NULL, + [lemail] [varchar] (250) NOT NULL, + [orgname] [text] NOT NULL, + [lorgname] [varchar] (250) NOT NULL, + [orgunit] [text] NOT NULL, + [lorgunit] [varchar] (250) NOT NULL, + CONSTRAINT [vcard_search_PRIMARY] PRIMARY KEY CLUSTERED +( + [lusername] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) +) TEXTIMAGE_ON [PRIMARY]; + +CREATE INDEX [vcard_search_lfn] ON [vcard_search] (lfn) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [vcard_search_lfamily] ON [vcard_search] (lfamily) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [vcard_search_lgiven] ON [vcard_search] (lgiven) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [vcard_search_lmiddle] ON [vcard_search] (lmiddle) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [vcard_search_lnickname] ON [vcard_search] (lnickname) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [vcard_search_lbday] ON [vcard_search] (lbday) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [vcard_search_lctry] ON [vcard_search] (lctry) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [vcard_search_llocality] ON [vcard_search] (llocality) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [vcard_search_lemail] ON [vcard_search] (lemail) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [vcard_search_lorgname] ON [vcard_search] (lorgname) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [vcard_search_lorgunit] ON [vcard_search] (lorgunit) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +ALTER TABLE [dbo].[pubsub_item] WITH CHECK ADD CONSTRAINT [pubsub_item_ibfk_1] FOREIGN KEY([nodeid]) +REFERENCES [dbo].[pubsub_node] ([nodeid]) +ON DELETE CASCADE; + +ALTER TABLE [dbo].[pubsub_item] CHECK CONSTRAINT [pubsub_item_ibfk_1]; + +ALTER TABLE [dbo].[pubsub_node_option] WITH CHECK ADD CONSTRAINT [pubsub_node_option_ibfk_1] FOREIGN KEY([nodeid]) +REFERENCES [dbo].[pubsub_node] ([nodeid]) +ON DELETE CASCADE; + +ALTER TABLE [dbo].[pubsub_node_option] CHECK CONSTRAINT [pubsub_node_option_ibfk_1]; + +ALTER TABLE [dbo].[pubsub_node_owner] WITH CHECK ADD CONSTRAINT [pubsub_node_owner_ibfk_1] FOREIGN KEY([nodeid]) +REFERENCES [dbo].[pubsub_node] ([nodeid]) +ON DELETE CASCADE; + +ALTER TABLE [dbo].[pubsub_node_owner] CHECK CONSTRAINT [pubsub_node_owner_ibfk_1]; + +ALTER TABLE [dbo].[pubsub_state] WITH CHECK ADD CONSTRAINT [pubsub_state_ibfk_1] FOREIGN KEY([nodeid]) +REFERENCES [dbo].[pubsub_node] ([nodeid]) +ON DELETE CASCADE; + +ALTER TABLE [dbo].[pubsub_state] CHECK CONSTRAINT [pubsub_state_ibfk_1]; + +CREATE TABLE [dbo].[oauth_token] ( + [token] [varchar] (250) NOT NULL, + [jid] [text] NOT NULL, + [scope] [text] NOT NULL, + [expire] [bigint] NOT NULL, + CONSTRAINT [oauth_token_PRIMARY] PRIMARY KEY CLUSTERED +( + [token] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) +) TEXTIMAGE_ON [PRIMARY]; + +CREATE TABLE [dbo].[route] ( + [domain] [varchar] (255) NOT NULL, + [server_host] [varchar] (255) NOT NULL, + [node] [varchar] (255) NOT NULL, + [pid] [varchar](100) NOT NULL, + [local_hint] [text] NOT NULL +); + +CREATE UNIQUE CLUSTERED INDEX [route_i] ON [route] (domain, server_host, node, pid) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[bosh] ( + [sid] [varchar] (255) NOT NULL, + [node] [varchar] (255) NOT NULL, + [pid] [varchar](100) NOT NULL + CONSTRAINT [bosh_PRIMARY] PRIMARY KEY CLUSTERED +( + [sid] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) +); + +CREATE TABLE [dbo].[push_session] ( + [username] [varchar] (255) NOT NULL, + [timestamp] [bigint] NOT NULL, + [service] [varchar] (255) NOT NULL, + [node] [varchar] (255) NOT NULL, + [xml] [varchar] (255) NOT NULL +); + +CREATE UNIQUE CLUSTERED INDEX [i_push_usn] ON [push_session] (username, service, node) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [i_push_ut] ON [push_session] (username, timestamp) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[mix_channel] ( + [channel] [varchar] (250) NOT NULL, + [service] [varchar] (250) NOT NULL, + [username] [varchar] (250) NOT NULL, + [domain] [varchar] (250) NOT NULL, + [jid] [varchar] (250) NOT NULL, + [hidden] [smallint] NOT NULL, + [hmac_key] [text] NOT NULL, + [created_at] [datetime] NOT NULL DEFAULT GETDATE() +) TEXTIMAGE_ON [PRIMARY]; + +CREATE UNIQUE CLUSTERED INDEX [mix_channel] ON [mix_channel] (channel, service) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [mix_channel_serv] ON [mix_channel] (service) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[mix_participant] ( + [channel] [varchar] (250) NOT NULL, + [service] [varchar] (250) NOT NULL, + [username] [varchar] (250) NOT NULL, + [domain] [varchar] (250) NOT NULL, + [jid] [varchar] (250) NOT NULL, + [id] [text] NOT NULL, + [nick] [text] NOT NULL, + [created_at] [datetime] NOT NULL DEFAULT GETDATE() +) TEXTIMAGE_ON [PRIMARY]; + +CREATE UNIQUE INDEX [mix_participant] ON [mix_participant] (channel, service, username, domain) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [mix_participant_chan_serv] ON [mix_participant] (channel, service) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[mix_subscription] ( + [channel] [varchar] (250) NOT NULL, + [service] [varchar] (250) NOT NULL, + [username] [varchar] (250) NOT NULL, + [domain] [varchar] (250) NOT NULL, + [node] [varchar] (250) NOT NULL, + [jid] [varchar] (250) NOT NULL +); + +CREATE UNIQUE INDEX [mix_subscription] ON [mix_subscription] (channel, service, username, domain, node) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [mix_subscription_chan_serv_ud] ON [mix_subscription] (channel, service, username, domain) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [mix_subscription_chan_serv_node] ON [mix_subscription] (channel, service, node) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE INDEX [mix_subscription_chan_serv] ON [mix_subscription] (channel, service) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[mix_pam] ( + [username] [varchar] (250) NOT NULL, + [channel] [varchar] (250) NOT NULL, + [service] [varchar] (250) NOT NULL, + [id] [text] NOT NULL, + [created_at] [datetime] NOT NULL DEFAULT GETDATE() +) TEXTIMAGE_ON [PRIMARY]; + +CREATE UNIQUE CLUSTERED INDEX [mix_pam] ON [mix_pam] (username, channel, service) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); + +CREATE TABLE [dbo].[mqtt_pub] ( + [username] [varchar] (250) NOT NULL, + [resource] [varchar] (250) NOT NULL, + [topic] [varchar] (250) NOT NULL, + [qos] [tinyint] NOT NULL, + [payload] [varbinary](max) NOT NULL, + [payload_format] [tinyint] NOT NULL, + [content_type] [text] NOT NULL, + [response_topic] [text] NOT NULL, + [correlation_data] [varbinary](max) NOT NULL, + [user_properties] [varbinary](max) NOT NULL, + [expiry] [int] NOT NULL +) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]; + +CREATE UNIQUE CLUSTERED INDEX [mqtt_topic] ON [mqtt_pub] (topic) +WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); diff --git a/sql/mssql2000.sql b/sql/mssql2000.sql deleted file mode 100644 index a717b0fb0..000000000 --- a/sql/mssql2000.sql +++ /dev/null @@ -1,1095 +0,0 @@ -/* - * ejabberd, Copyright (C) 2002-2015 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. - * - */ - -SET ANSI_NULLS ON -GO -SET QUOTED_IDENTIFIER ON -GO - -exec sp_dboption N'ejabberd', N'autoclose', N'false' -GO - -exec sp_dboption N'ejabberd', N'bulkcopy', N'true' -GO - -exec sp_dboption N'ejabberd', N'trunc. log', N'false' -GO - -exec sp_dboption N'ejabberd', N'torn page detection', N'true' -GO - -exec sp_dboption N'ejabberd', N'read only', N'false' -GO - -exec sp_dboption N'ejabberd', N'dbo use', N'false' -GO - -exec sp_dboption N'ejabberd', N'single', N'false' -GO - -exec sp_dboption N'ejabberd', N'autoshrink', N'false' -GO - -exec sp_dboption N'ejabberd', N'ANSI null default', N'false' -GO - -exec sp_dboption N'ejabberd', N'recursive triggers', N'false' -GO - -exec sp_dboption N'ejabberd', N'ANSI nulls', N'false' -GO - -exec sp_dboption N'ejabberd', N'concat null yields null', N'false' -GO - -exec sp_dboption N'ejabberd', N'cursor close on commit', N'false' -GO - -exec sp_dboption N'ejabberd', N'default to local cursor', N'false' -GO - -exec sp_dboption N'ejabberd', N'quoted identifier', N'false' -GO - -exec sp_dboption N'ejabberd', N'ANSI warnings', N'false' -GO - -exec sp_dboption N'ejabberd', N'auto create statistics', N'true' -GO - -exec sp_dboption N'ejabberd', N'auto update statistics', N'true' -GO - -use [ejabberd] -GO - -if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[last]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) -drop table [dbo].[last] -GO - -if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[rostergroups]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) -drop table [dbo].[rostergroups] -GO - -if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[rosterusers]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) -drop table [dbo].[rosterusers] -GO - -if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[spool]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) -drop table [dbo].[spool] -GO - -if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[users]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) -drop table [dbo].[users] -GO - -if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[vcard]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) -drop table [dbo].[vcard] -GO - -if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[private_storage]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) -drop table [dbo].[private_storage] -GO - -CREATE TABLE [dbo].[last] ( - [username] [varchar] (250) NOT NULL , - [seconds] [varchar] (50) NOT NULL , - [state] [varchar] (100) NOT NULL , -) ON [PRIMARY] -GO - -CREATE TABLE [dbo].[rostergroups] ( - [username] [varchar] (250) NOT NULL , - [jid] [varchar] (250) NOT NULL , - [grp] [varchar] (100) NOT NULL -) ON [PRIMARY] -GO - -CREATE TABLE [dbo].[rosterusers] ( - [username] [varchar] (250) NOT NULL , - [jid] [varchar] (250) NOT NULL , - [nick] [varchar] (50) NOT NULL , - [subscription] [char] (1) NOT NULL , - [ask] [char] (1) NOT NULL , - [askmessage] [varchar] (250) NOT NULL , - [server] [char] (1) NOT NULL , - [subscribe] [varchar] (200) NULL , - [type] [varchar] (50) NULL -) ON [PRIMARY] -GO - -CREATE TABLE [dbo].[spool] ( - [id] [numeric](19, 0) IDENTITY (1, 1) NOT NULL , - [username] [varchar] (250) NOT NULL , - [xml] [text] NOT NULL , - [notifyprocessed] [bit] NULL , - [created] [datetime] NULL , -) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] -GO - -CREATE TABLE [dbo].[users] ( - [username] [varchar] (250) NOT NULL , - [password] [varchar] (50) NOT NULL , - [created] [datetime] NULL -) ON [PRIMARY] -GO - -CREATE TABLE [dbo].[vcard] ( - [username] [varchar] (250) NOT NULL , - [full_name] [varchar] (250) NULL , - [first_name] [varchar] (50) NULL , - [last_name] [varchar] (50) NULL , - [nick_name] [varchar] (50) NULL , - [url] [varchar] (1024) NULL , - [address1] [varchar] (50) NULL , - [address2] [varchar] (50) NULL , - [locality] [varchar] (50) NULL , - [region] [varchar] (50) NULL , - [pcode] [varchar] (50) NULL , - [country] [varchar] (50) NULL , - [telephone] [varchar] (50) NULL , - [email] [varchar] (250) NULL , - [orgname] [varchar] (50) NULL , - [orgunit] [varchar] (50) NULL , - [title] [varchar] (50) NULL , - [role] [varchar] (50) NULL , - [b_day] [datetime] NULL , - [descr] [varchar] (500) NULL -) ON [PRIMARY] -GO - -CREATE TABLE [dbo].[private_storage] ( - [username] [varchar] (250) NOT NULL , - [namespace] [varchar] (250) NOT NULL , - [data] [text] NOT NULL -) ON [PRIMARY] -GO - -CREATE TABLE [dbo].[privacy_default_list] ( - [username] [varchar] (250) NOT NULL, - [name] [varchar] (250) NOT NULL -) ON [PRIMARY] -GO - -CREATE TABLE [dbo].[privacy_list] ( - [username] [varchar] (250) NOT NULL, - [name] [varchar] (250) NOT NULL, - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE -) ON [PRIMARY] -GO - -CREATE TABLE [dbo].[privacy_list_data] ( - [id] [bigint] NOT NULL, - [t] [character] (1) NOT NULL, - [value] [text] NOT NULL, - [action] [character] (1) NOT NULL, - [ord] [NUMERIC] NOT NULL, - [match_all] [boolean] NOT NULL, - [match_iq] [boolean] NOT NULL, - [match_message] [boolean] NOT NULL, - [match_presence_in] [boolean] NOT NULL, - [match_presence_out] [boolean] NOT NULL -) ON [PRIMARY] -GO - -/* Not tested on mssql */ -CREATE TABLE [dbo].[roster_version] ( - [username] [varchar] (250) NOT NULL , - [version] [varchar] (64) NOT NULL -) ON [PRIMARY] -GO - - -/* Constraints to add: -- id in privacy_list is a SERIAL autogenerated number -- id in privacy_list_data must exist in the table privacy_list */ - -ALTER TABLE [dbo].[last] WITH NOCHECK ADD - CONSTRAINT [PK_last] PRIMARY KEY CLUSTERED - ( - [username] - ) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - -ALTER TABLE [dbo].[rostergroups] WITH NOCHECK ADD - CONSTRAINT [PK_rostergroups] PRIMARY KEY CLUSTERED - ( - [username], - [jid], - [grp] - ) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - -ALTER TABLE [dbo].[spool] WITH NOCHECK ADD - CONSTRAINT [PK_spool] PRIMARY KEY CLUSTERED - ( - [username], - [id] - ) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - -ALTER TABLE [dbo].[users] WITH NOCHECK ADD - CONSTRAINT [PK_users] PRIMARY KEY CLUSTERED - ( - [username] - ) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - -ALTER TABLE [dbo].[roster_version] WITH NOCHECK ADD - CONSTRAINT [PK_roster_version] PRIMARY KEY CLUSTERED - ( - [username] - ) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - -ALTER TABLE [dbo].[vcard] WITH NOCHECK ADD - CONSTRAINT [PK_vcard] PRIMARY KEY CLUSTERED - ( - [username] - ) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - -CREATE CLUSTERED INDEX [IX_rosterusers_user] ON [dbo].[rosterusers]([username]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - -ALTER TABLE [dbo].[last] WITH NOCHECK ADD - CONSTRAINT [DF_last_updated] DEFAULT (getdate()) FOR [Modify_Date] -GO - -ALTER TABLE [dbo].[spool] WITH NOCHECK ADD - CONSTRAINT [DF_spool_notifyprocessed] DEFAULT (0) FOR [notifyprocessed], - CONSTRAINT [DF_spool_created] DEFAULT (getdate()) FOR [created], - CONSTRAINT [DF_spool_MustDelete] DEFAULT (0) FOR [MustDelete] -GO - -ALTER TABLE [dbo].[users] WITH NOCHECK ADD - CONSTRAINT [DF_users_created] DEFAULT (getdate()) FOR [created] -GO - -ALTER TABLE [dbo].[privacy_default_list] WITH NOCHECK ADD - CONSTRAINT [PK_privacy_defaut_list] PRIMARY KEY CLUSTERED - ( - [username] - ) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - - - CREATE INDEX [IX_rostergroups_jid] ON [dbo].[rostergroups]([jid]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - CREATE INDEX [IX_rostergroups_user] ON [dbo].[rostergroups]([username]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - CREATE INDEX [IX_rosterusers_jid] ON [dbo].[rosterusers]([username], [jid]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - CREATE INDEX [IX_spool_user] ON [dbo].[spool]([username]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - CREATE INDEX [IX_spool_process] ON [dbo].[spool]([created], [notifyprocessed]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - CREATE INDEX [IK_Spool_Del] ON [dbo].[spool]([MustDelete]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - CREATE INDEX [IK_Spool_Created] ON [dbo].[spool]([created]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - CREATE INDEX [IX_private_user] ON [dbo].[private_storage]([username]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - CREATE INDEX [IX_private_user_ns] ON [dbo].[private_storage]([username], [namespace]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - CREATE INDEX [IX_privacy_list_username] ON [dbo].[privacy_list]([username]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - CREATE INDEX [IX_privacy_list_username_name] ON [dbo].[privacy_list]([username], [name]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - -/*********************************************************/ -/** These store procedures are for use with ejabberd **/ -/** 1.1 and Microsoft Sql Server 2000 **/ -/** **/ -/** The stored procedures reduce the need to sql **/ -/** compilation of the database and also allow for also **/ -/** provide each of database integration. The stored **/ -/** procedure have been optimized to increase database **/ -/** performance and a reduction of 80% in CPU was **/ -/** achieved over the use of standard sql. **/ -/*********************************************************/ - -/****** Object: StoredProcedure [dbo].[add_roster] ******/ -/** Add or update user entries in the roster **/ -/*********************************************************/ -CREATE PROCEDURE [dbo].[add_roster] - @Username varchar(250), - @JID varchar(250), - @Nick varchar(50), - @Subscription char(1), - @Ask char(1), - @AskMessage varchar(250), - @Server char(1), - @Subscribe varchar(200), - @Type varchar(50), - @Grp varchar(100) -AS -BEGIN - BEGIN TRANSACTION - --- Update Roster if user exist else add roster item - IF EXISTS (SELECT username FROM rosterusers WITH (NOLOCK) WHERE rosterusers.username=@Username AND rosterusers.jid=@JID) - BEGIN - UPDATE rosterusers - SET rosterusers.username=@Username, - rosterusers.jid=@JID, - rosterusers.nick=@Nick, - rosterusers.subscription=@Subscription, - rosterusers.ask=@Ask, - rosterusers.askmessage=@AskMessage, - rosterusers.server=@Server, - rosterusers.subscribe=@Subscribe, - rosterusers.type=@Type - WHERE (rosterusers.username=@Username) AND (rosterusers.jid=@JID); - END - ELSE - BEGIN - INSERT INTO rosterusers - ( rosterusers.username, - rosterusers.jid, - rosterusers.nick, - rosterusers.subscription, - rosterusers.ask, - rosterusers.askmessage, - rosterusers.server, - rosterusers.subscribe, - rosterusers.type - ) - VALUES - ( @Username, - @JID, - @Nick, - @Subscription, - @Ask, - @AskMessage, - @Server, - @Subscribe, - @Type - ); - END - - --- Update Roster Groups if exist else add group entry - IF NOT EXISTS (SELECT username FROM rostergroups WITH (NOLOCK) WHERE rostergroups.username=@Username AND rostergroups.jid=@JID AND rostergroups.grp=@Grp) - BEGIN - INSERT INTO rostergroups - ( rostergroups.username, - rostergroups.jid, - rostergroups.grp - ) - VALUES - ( @Username, - @JID, - @Grp - ); - END - - COMMIT -END -GO - -/***************************************************************/ -/****** Object: StoredProcedure [dbo].[add_roster_group] ******/ -/** Add or update user group entries in the roster groups **/ -/***************************************************************/ -CREATE PROCEDURE [dbo].[add_roster_group] - @Username varchar(250), - @JID varchar(250), - @Grp varchar(100) -AS -BEGIN - --- Update Roster Groups if exist else add group - IF NOT EXISTS (SELECT username FROM rostergroups WHERE rostergroups.username=@Username AND rostergroups.jid=@JID AND rostergroups.grp=@Grp) - BEGIN - INSERT INTO rostergroups - ( rostergroups.username, - rostergroups.jid, - rostergroups.grp - ) - VALUES - ( @Username, - @JID, - @Grp - ) - END -END -GO - -/***************************************************************/ -/****** Object: StoredProcedure [dbo].[add_roster_user] ******/ -/** Add or update user entries in the roster **/ -/***************************************************************/ -CREATE PROCEDURE [dbo].[add_roster_user] - @Username varchar(250), - @JID varchar(250), - @Nick varchar(50), - @Subscription char(1), - @Ask char(1), - @AskMessage varchar(250), - @Server char(1), - @Subscribe varchar(200), - @Type varchar(50), - @Grp varchar(100) = Null -AS -BEGIN - BEGIN TRANSACTION - --- Update Roster Users if exist of add new user - IF EXISTS (SELECT username FROM rosterusers WHERE rosterusers.username=@Username AND rosterusers.jid=@JID) - BEGIN - UPDATE rosterusers - SET rosterusers.username=@Username, - rosterusers.jid=@JID, - rosterusers.nick=@Nick, - rosterusers.subscription=@Subscription, - rosterusers.ask=@Ask, - rosterusers.askmessage=@AskMessage, - rosterusers.server=@Server, - rosterusers.subscribe=@Subscribe, - rosterusers.type=@Type - WHERE (rosterusers.username=@Username) AND (rosterusers.jid=@JID); - END - ELSE - BEGIN - INSERT INTO rosterusers - ( rosterusers.username, - rosterusers.jid, - rosterusers.nick, - rosterusers.subscription, - rosterusers.ask, - rosterusers.askmessage, - rosterusers.server, - rosterusers.subscribe, - rosterusers.type - ) - VALUES - ( @Username, - @JID, - @Nick, - @Subscription, - @Ask, - @AskMessage, - @Server, - @Subscribe, - @Type - ); - END - - --- Update Roster Group if exist of add new group - IF @Grp IS NOT NULL - EXECUTE [dbo].[add_roster_group] @Username, @JID, @Grp - - COMMIT -END -GO - -/***************************************************************/ -/****** Object: StoredProcedure [dbo].[del_roster_groups] ******/ -/** Remove user group entries from the roster groups table **/ -/***************************************************************/ -CREATE PROCEDURE [dbo].[del_roster_groups] - @Username varchar(250), - @JID varchar(250) -AS -BEGIN - DELETE FROM rostergroups - WITH (ROWLOCK) - WHERE (rostergroups.username = @Username) AND (rostergroups.jid = @JID); -END -GO - -/***************************************************************/ -/****** Object: StoredProcedure [dbo].[add_spool] ******/ -/** Add a entry to the spool table **/ -/***************************************************************/ -CREATE PROCEDURE [dbo].[add_spool] - @Username varchar(250), - @XML varchar(8000) -AS -BEGIN - INSERT INTO spool - ( spool.username, - spool.xml - ) - VALUES - ( @Username, - @XML - ) -END -GO - -/***************************************************************/ -/****** Object: StoredProcedure [dbo].[add_user] ******/ -/** Add or update user entries to jabber **/ -/***************************************************************/ -CREATE PROCEDURE [dbo].[add_user] - @Username varchar(200), - @Password varchar(50) -AS -BEGIN - INSERT INTO users - ( [username], - [password] - ) - VALUES - ( @Username, - @Password - ); -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[set_password] **/ -/** Update users password **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[set_password] - @Username varchar(200), - @Password varchar(50) -AS -BEGIN - IF EXISTS (SELECT username FROM users WITH (NOLOCK) WHERE username=@Username) - BEGIN - UPDATE users SET username=@Username, password=@Password WHERE username=@Username; - END - ELSE - BEGIN - INSERT INTO users (username, password) VALUES (@Username, @Password); - END -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[get_password] **/ -/** Retrive the user password **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[get_password] - @Username varchar(200) -AS -BEGIN - SELECT users.password as password - FROM users WITH (NOLOCK) - WHERE username=@Username; -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[set_roster_version] **/ -/** Update users roster_version **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[set_roster_version] - @Username varchar(200), - @Version varchar(50) -AS -BEGIN - IF EXISTS (SELECT username FROM roster_version WITH (NOLOCK) WHERE username=@Username) - BEGIN - UPDATE roster_version SET username=@Username, version=@Version WHERE username=@Username; - END - ELSE - BEGIN - INSERT INTO roster_version (username, version) VALUES (@Username, @Version); - END -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[get_roster_version] **/ -/** Retrive the user roster_version **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[get_roster_version] - @Username varchar(200) -AS -BEGIN - SELECT roster_version.version as version - FROM roster_version WITH (NOLOCK) - WHERE username=@Username; -END -GO - -/***************************************************************/ -/****** Object: StoredProcedure [dbo].[clean_spool_msg] ******/ -/** Delete messages older that 3 days from spool **/ -/***************************************************************/ -CREATE PROCEDURE [dbo].[clean_spool_msg] -AS -DECLARE - @dt datetime, - @myRowCount int -BEGIN - -- Delete small amounts because if locks the database table - SET ROWCOUNT 500 - SET @myRowCount = 1 - - WHILE (@myRowCount) > 0 - BEGIN - BEGIN TRANSACTION - SELECT @dt = DATEADD(d, -3, GETDATE()) - DELETE FROM spool - WITH (ROWLOCK) - WHERE (MustDelete=1) OR (Created < @dt); - - SET @myRowCount = @@RowCount - COMMIT - END -END -GO - -/***************************************************************/ -/****** Object: StoredProcedure [dbo].[del_last] ******/ -/** Delete an entry from the last table **/ -/***************************************************************/ -CREATE PROCEDURE [dbo].[del_last] - @Username varchar(250) -AS -BEGIN - DELETE FROM [last] - WITH (ROWLOCK) - WHERE [last].username=@Username; -END -GO - -/***************************************************************/ -/****** Object: StoredProcedure [dbo].[del_roster] ******/ -/** Delete an entry from the roster **/ -/***************************************************************/ -CREATE PROCEDURE [dbo].[del_roster] - @Username varchar(250), - @JID varchar(250) -AS -BEGIN - BEGIN TRANSACTION - DELETE FROM rosterusers - WITH (ROWLOCK) - WHERE (rosterusers.username = @Username) AND (rosterusers.jid = @JID); - - DELETE FROM rostergroups - WITH (ROWLOCK) - WHERE (rostergroups.username = @Username) AND (rostergroups.jid = @JID); - COMMIT -END -GO - - -/***************************************************************/ -/****** Object: StoredProcedure [dbo].[del_spool_msg] ******/ -/** Delete an entry from the spool table **/ -/***************************************************************/ -CREATE PROCEDURE [dbo].[del_spool_msg] - @Username varchar(250) -AS -BEGIN - DELETE FROM spool - WITH (ROWLOCK) - WHERE spool.username=@Username; -END -GO - -/***************************************************************/ -/****** Object: StoredProcedure [dbo].[del_user] ******/ -/** Delete an entry from the user table **/ -/***************************************************************/ -CREATE PROCEDURE [dbo].[del_user] - @Username varchar(200) -AS -BEGIN - DELETE FROM users - WITH (ROWLOCK) - WHERE username=@Username; -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[del_user_return_password]**/ -/** Delete an entry from the user table and return user password **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[del_user_return_password] - @Username varchar(250) -AS -DECLARE - @Pwd varchar(50) -BEGIN - EXECUTE @Pwd = dbo.get_password @Username - DELETE FROM users - WITH (ROWLOCK) - WHERE username=@Username - - SELECT @Pwd; -END -GO - - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[del_user_roster] **/ -/** Delete the users roster **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[del_user_roster] - @Username varchar(250) -AS -BEGIN - BEGIN TRANSACTION - DELETE FROM rosterusers - WITH (ROWLOCK) - WHERE rosterusers.username = @Username; - - DELETE FROM rostergroups - WITH (ROWLOCK) - WHERE rostergroups.username = @Username; - COMMIT -END -GO - - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[get_and_del_spool_msg] **/ -/** Fetch and delete the users offline messages **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[get_and_del_spool_msg] - @Username varchar(250) -AS -DECLARE - @vSpool table( username varchar(1), - xml varchar(1)) -BEGIN - IF EXISTS (SELECT username FROM spool with (nolock) WHERE spool.username=@Username) - BEGIN - SELECT spool.username AS username, - spool.xml AS xml - FROM spool WITH (NOLOCK) - WHERE spool.username=@Username; - - DELETE spool - WITH (ROWLOCK) - WHERE spool.username=@Username - END - ELSE - BEGIN - SELECT * FROM @vSpool; - END -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[get_last] **/ -/** Retrive the last user login **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[get_last] - @Username varchar(250) -AS -BEGIN - SELECT last.seconds AS seconds, - last.state AS state - FROM last WITH (NOLOCK) - WHERE last.username=@Username; -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[get_roster] **/ -/** Retrive the user roster **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[get_roster] - @Username varchar(250) -AS -DECLARE - @vRosterusers table( username varchar(1), - jid varchar(1), - nick varchar(1), - subscription varchar(1), - ask varchar(1), - askmessage varchar(1), - server varchar(1), - subscribe varchar(1), - type varchar(1)) -BEGIN - IF EXISTS (SELECT username FROM rosterusers with (nolock) WHERE rosterusers.username = @Username) - BEGIN - SELECT rosterusers.username AS username, - rosterusers.jid AS jid, - rosterusers.nick AS nick, - rosterusers.subscription AS subscription, - rosterusers.ask AS ask, - rosterusers.askmessage AS askmessage, - rosterusers.server AS server, - rosterusers.subscribe AS subscribe, - rosterusers.type AS type - FROM rosterusers WITH (NOLOCK) - WHERE rosterusers.username = @Username; - END - ELSE - BEGIN - SELECT * FROM @vRosterusers - END -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[get_roster_by_jid] **/ -/** Retrive the user roster via JID **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[get_roster_by_jid] - @Username varchar(200), - @JID varchar(250) -AS -DECLARE - @vRosterusers table( username varchar(1), - jid varchar(1), - nick varchar(1), - subscription varchar(1), - ask varchar(1), - askmessage varchar(1), - server varchar(1), - subscribe varchar(1), - type varchar(1)) -BEGIN - IF EXISTS (SELECT username FROM rosterusers with (nolock) WHERE (rosterusers.username = @Username) AND (rosterusers.jid = @JID)) - BEGIN - SELECT rosterusers.username AS username, - rosterusers.jid AS jid, - rosterusers.nick AS nick, - rosterusers.subscription AS subscription, - rosterusers.ask AS ask, - rosterusers.askmessage AS askmessage, - rosterusers.server AS server, - rosterusers.subscribe AS subscribe, - rosterusers.type AS type - FROM rosterusers WITH (NOLOCK) - WHERE (rosterusers.username = @Username) AND (rosterusers.jid = @JID); - END - ELSE - BEGIN - SELECT * FROM @vRosterusers - END -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[get_roster_jid_groups] **/ -/** Retrieve the user roster groups **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[get_roster_jid_groups] - @Username varchar(200) -AS -DECLARE - @vrostergroups table( jid varchar(1), - grp varchar(1)) -BEGIN - IF EXISTS (SELECT username FROM rostergroups with (nolock) WHERE rostergroups.username = @Username) - BEGIN - SELECT rostergroups.jid AS jid, - rostergroups.grp AS grp - FROM rostergroups WITH (NOLOCK) - WHERE rostergroups.username = @Username; - END - ELSE - BEGIN - SELECT * FROM @vrostergroups - END -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[get_roster_groups] **/ -/** Retrive the user roster groups **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[get_roster_groups] - @Username varchar(200), - @JID varchar(250) -AS -DECLARE - @vrostergroups table( grp varchar(1)) -BEGIN - IF EXISTS (SELECT username FROM rostergroups with (nolock) WHERE rostergroups.username = @Username) - BEGIN - SELECT rostergroups.grp AS grp - FROM rostergroups WITH (NOLOCK) - WHERE (rostergroups.username = @Username) AND (rostergroups.jid = @JID); - END - ELSE - BEGIN - SELECT * FROM @vrostergroups - END -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[get_rostergroup_by_jid] **/ -/** Retrive the user roster groups via JID **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[get_rostergroup_by_jid] - @Username varchar(250), - @JID varchar(250) -AS -DECLARE - @vrostergroups table(grp varchar(1)) -BEGIN - IF EXISTS (SELECT username FROM rostergroups with (nolock) WHERE rostergroups.username=@Username AND rostergroups.jid=@JID) - BEGIN - SELECT rostergroups.grp AS grp - FROM rostergroups WITH (NOLOCK) - WHERE rostergroups.username=@Username AND rostergroups.jid=@JID; - END - ELSE - BEGIN - SELECT * FROM @vrostergroups - END -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[get_subscription] **/ -/** Retrive the user subscription requests **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[get_subscription] - @Username varchar(250), - @JID varchar(250) -AS -DECLARE - @vrosterusers table( subscription varchar(1)) -BEGIN - IF EXISTS (SELECT username FROM rosterusers with (nolock) WHERE rosterusers.username=@Username AND rosterusers.jid=@JID) - BEGIN - SELECT rosterusers.subscription AS subscription - FROM rosterusers WITH (NOLOCK) - WHERE rosterusers.username=@Username AND rosterusers.jid=@JID; - END - ELSE - BEGIN - SELECT * FROM @vrosterusers - END -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[list_users] **/ -/** Retrieve a list of all users **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[list_users] -AS -BEGIN - SELECT users.username AS username FROM users WITH (NOLOCK); -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[set_last] **/ -/** Update users last login status **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[set_last] - @Username varchar(250), - @Seconds varchar(50), - @State varchar(100) -AS -BEGIN - IF EXISTS (SELECT username FROM [last] WITH (NOLOCK) WHERE username=@Username) - BEGIN - UPDATE [last] - SET [last].username = @Username, - [last].seconds = @Seconds, - [last].state = @State - WHERE last.username=@Username; - END - ELSE - BEGIN - INSERT INTO [last] - ( [last].username, - [last].seconds, - [last].state - ) - VALUES - ( @Username, - @Seconds, - @State - ) - END -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[set_private_data] **/ -/** store user private data by namespace **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[set_private_data] - @Username varchar(250), - @Namespace varchar(250), - @Data varchar(8000) -AS -BEGIN - IF EXISTS (SELECT username FROM private_storage with (nolock) WHERE private_storage.username = @Username AND private_storage.namespace = @Namespace) - BEGIN - UPDATE [private_storage] - SET [private_storage].username = @Username, - [private_storage].namespace = @Namespace, - [private_storage].data = @Data - WHERE private_storage.username = @Username AND private_storage.namespace = @Namespace; - END - ELSE - BEGIN - INSERT INTO [private_storage] - ( [private_storage].username, - [private_storage].namespace, - [private_storage].data - ) - VALUES - ( @Username, - @Namespace, - @Data - ) - END -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[get_private_data] **/ -/** Retrieve user private data by namespace **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[get_private_data] - @Username varchar(250), - @Namespace varchar(250) -AS -BEGIN - SELECT private_storage.data AS data - FROM private_storage WITH (NOLOCK) - WHERE username=@Username and namespace=@Namespace; -END -GO - -/***************************************************************/ -/****** Object: StoredProcedure [dbo].[del_user_storage] ******/ -/** Delete private storage area for a given user **/ -/***************************************************************/ -CREATE PROCEDURE [dbo].[del_user_storage] - @Username varchar(250) -AS -BEGIN - DELETE FROM [private_storage] - WITH (ROWLOCK) - WHERE [private_storage].username=@Username; -END -GO - - - diff --git a/sql/mssql2005.sql b/sql/mssql2005.sql deleted file mode 100644 index de4b1bed0..000000000 --- a/sql/mssql2005.sql +++ /dev/null @@ -1,1802 +0,0 @@ -/* - * ejabberd, Copyright (C) 2002-2015 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. - * - */ - -SET ANSI_NULLS ON -GO -SET QUOTED_IDENTIFIER ON -GO - -exec sp_dboption N'ejabberd', N'autoclose', N'false' -GO - -exec sp_dboption N'ejabberd', N'bulkcopy', N'true' -GO - -exec sp_dboption N'ejabberd', N'trunc. log', N'false' -GO - -exec sp_dboption N'ejabberd', N'torn page detection', N'true' -GO - -exec sp_dboption N'ejabberd', N'read only', N'false' -GO - -exec sp_dboption N'ejabberd', N'dbo use', N'false' -GO - -exec sp_dboption N'ejabberd', N'single', N'false' -GO - -exec sp_dboption N'ejabberd', N'autoshrink', N'false' -GO - -exec sp_dboption N'ejabberd', N'ANSI null default', N'false' -GO - -exec sp_dboption N'ejabberd', N'recursive triggers', N'false' -GO - -exec sp_dboption N'ejabberd', N'ANSI nulls', N'false' -GO - -exec sp_dboption N'ejabberd', N'concat null yields null', N'false' -GO - -exec sp_dboption N'ejabberd', N'cursor close on commit', N'false' -GO - -exec sp_dboption N'ejabberd', N'default to local cursor', N'false' -GO - -exec sp_dboption N'ejabberd', N'quoted identifier', N'false' -GO - -exec sp_dboption N'ejabberd', N'ANSI warnings', N'false' -GO - -exec sp_dboption N'ejabberd', N'auto create statistics', N'true' -GO - -exec sp_dboption N'ejabberd', N'auto update statistics', N'true' -GO - -use [ejabberd] -GO - -if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[last]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) -drop table [dbo].[last] -GO - -if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[rostergroups]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) -drop table [dbo].[rostergroups] -GO - -if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[rosterusers]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) -drop table [dbo].[rosterusers] -GO - -if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[spool]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) -drop table [dbo].[spool] -GO - -if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[users]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) -drop table [dbo].[users] -GO - -if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[vcard]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) -drop table [dbo].[vcard] -GO - -if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[vcard_search]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) -drop table [dbo].[vcard_search] -GO - -if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[private_storage]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) -drop table [dbo].[private_storage] -GO - -if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[privacy_default_list]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) -drop table [dbo].[privacy_default_list] -GO - -if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[privacy_list]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) -drop table [dbo].[privacy_list] -GO - -if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[privacy_list_data]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) -drop table [dbo].[privacy_list_data] -GO - -if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[roster_version]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) -drop table [dbo].[roster_version] -GO - -if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[pubsub_node_option]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) -drop table [dbo].[pubsub_node_option] -GO - -if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[pubsub_node_owner]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) -drop table [dbo].[pubsub_node_owner] -GO - -if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[pubsub_state]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) -drop table [dbo].[pubsub_state] -GO - -if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[pubsub_item]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) -drop table [dbo].[pubsub_item] -GO - -if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[pubsub_subscription_opt]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) -drop table [dbo].[pubsub_subscription_opt] -GO - -if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[pubsub_node]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) -drop table [dbo].[pubsub_node] -GO - -CREATE TABLE [dbo].[last] ( - [username] [varchar] (250) NOT NULL , - [seconds] [varchar] (50) NOT NULL , - [state] [varchar] (100) NOT NULL , - [Modify_Date] [datetime] NOT NULL -) ON [PRIMARY] -GO - -CREATE TABLE [dbo].[rostergroups] ( - [username] [varchar] (250) NOT NULL , - [jid] [varchar] (250) NOT NULL , - [grp] [varchar] (100) NOT NULL -) ON [PRIMARY] -GO - -CREATE TABLE [dbo].[rosterusers] ( - [username] [varchar] (250) NOT NULL , - [jid] [varchar] (250) NOT NULL , - [nick] [varchar] (50) NOT NULL , - [subscription] [char] (1) NOT NULL , - [ask] [char] (1) NOT NULL , - [askmessage] [varchar] (250) NOT NULL , - [server] [char] (1) NOT NULL , - [subscribe] [varchar] (200) NULL , - [type] [varchar] (50) NULL , -CONSTRAINT [PK_rosterusers] PRIMARY KEY NONCLUSTERED -( - [username] ASC, - [jid] ASC -)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] -) ON [PRIMARY] -GO - -CREATE TABLE [dbo].[spool] ( - [id] [numeric](19, 0) IDENTITY (1, 1) NOT NULL , - [username] [varchar] (250) NOT NULL , - [xml] [text] NOT NULL , - [notifyprocessed] [bit] NULL , - [created] [datetime] NULL , - [MustDelete] [bit] NOT NULL -) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] -GO - -CREATE TABLE [dbo].[users] ( - [username] [varchar] (250) NOT NULL , - [password] [varchar] (50) NOT NULL , - [created] [datetime] NULL -) ON [PRIMARY] -GO - -CREATE TABLE [dbo].[vcard] ( - [username] [varchar] (250) NOT NULL , - [vcard] [text] NOT NULL -) ON [PRIMARY] -GO - -CREATE TABLE [dbo].[vcard_search] ( - [username] [varchar] (250) NOT NULL , - [lusername] [varchar] (250) NOT NULL , - [fn] [text] NOT NULL , - [lfn] [varchar] (250) NOT NULL , - [family] [text] NOT NULL , - [lfamily] [varchar] (250) NOT NULL , - [given] [text] NOT NULL , - [lgiven] [varchar] (250) NOT NULL , - [middle] [text] NOT NULL , - [lmiddle] [varchar] (250) NOT NULL , - [nickname] [text] NOT NULL , - [lnickname] [varchar] (250) NOT NULL , - [bday] [text] NOT NULL , - [lbday] [varchar] (250) NOT NULL , - [ctry] [text] NOT NULL , - [lctry] [varchar] (250) NOT NULL , - [locality] [text] NOT NULL , - [llocality] [varchar] (250) NOT NULL , - [email] [text] NOT NULL , - [lemail] [varchar] (250) NOT NULL , - [orgname] [text] NOT NULL , - [lorgname] [varchar] (250) NOT NULL , - [orgunit] [text] NOT NULL , - [lorgunit] [varchar] (250) NOT NULL -) ON [PRIMARY] -GO - -CREATE TABLE [dbo].[private_storage] ( - [username] [varchar] (250) NOT NULL , - [namespace] [varchar] (250) NOT NULL , - [data] [text] NOT NULL -) ON [PRIMARY] -GO - -CREATE TABLE [dbo].[privacy_default_list] ( - [username] [varchar] (250) NOT NULL, - [name] [varchar] (250) NOT NULL -) ON [PRIMARY] -GO - -CREATE TABLE [dbo].[privacy_list]( - [username] [varchar](250) NOT NULL, - [name] [varchar](250) NOT NULL, - [id] [bigint] IDENTITY(1,1) NOT NULL, - CONSTRAINT [PK_privacy_list] PRIMARY KEY CLUSTERED -( - [id] ASC -)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] -) ON [PRIMARY] -GO - -CREATE TABLE [dbo].[privacy_list_data] ( - [id] [bigint] NOT NULL, - [t] [character] (1) NOT NULL, - [value] [text] NOT NULL, - [action] [character] (1) NOT NULL, - [ord] [NUMERIC] NOT NULL, - [match_all] [bit] NOT NULL, - [match_iq] [bit] NOT NULL, - [match_message] [bit] NOT NULL, - [match_presence_in] [bit] NOT NULL, - [match_presence_out] [bit] NOT NULL -) ON [PRIMARY] -GO - -CREATE TABLE [dbo].[roster_version] ( - [username] [varchar](250) PRIMARY KEY, - [version] [text] NOT NULL -) ON [PRIMARY] -GO - -CREATE TABLE [dbo].[pubsub_node] ( - [host] [varchar](250), - [node] [varchar](250), - [parent] [varchar](250), - [type] [varchar](250), - [nodeid] [bigint] IDENTITY(1,1) PRIMARY KEY -) ON [PRIMARY] -GO - -CREATE TABLE [dbo].[pubsub_node_option] ( - [nodeid] [bigint], - [name] [varchar](250), - [val] [varchar](250) -) ON [PRIMARY] -GO - -CREATE TABLE [dbo].[pubsub_node_owner] ( - [nodeid] [bigint], - [owner] [varchar](250) -) ON [PRIMARY] -GO - -CREATE TABLE [dbo].[pubsub_state] ( - [nodeid] [bigint], - [jid] [varchar](250), - [affiliation] [CHAR](1), - [subscriptions] [text], - [stateid] [bigint] IDENTITY(1,1) PRIMARY KEY -) ON [PRIMARY] -GO - -CREATE TABLE [dbo].[pubsub_item] ( - [nodeid] [bigint], - [itemid] [varchar](250), - [publisher] [text], - [creation] [text], - [modification] [text], - [payload] [text] -) ON [PRIMARY] -GO - -CREATE TABLE [dbo].[pubsub_subscription_opt] ( - [subid] [varchar](250), - [opt_name] [varchar](32), - [opt_value] [text] -) ON [PRIMARY] -GO - -/* Constraints to add: -- id in privacy_list is a SERIAL autogenerated number -- id in privacy_list_data must exist in the table privacy_list */ - -ALTER TABLE [dbo].[last] WITH NOCHECK ADD - CONSTRAINT [PK_last] PRIMARY KEY CLUSTERED - ( - [username] - ) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - -ALTER TABLE [dbo].[rostergroups] WITH NOCHECK ADD - CONSTRAINT [PK_rostergroups] PRIMARY KEY CLUSTERED - ( - [username], - [jid], - [grp] - ) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - -ALTER TABLE [dbo].[spool] WITH NOCHECK ADD - CONSTRAINT [PK_spool] PRIMARY KEY CLUSTERED - ( - [username], - [id] - ) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - -ALTER TABLE [dbo].[users] WITH NOCHECK ADD - CONSTRAINT [PK_users] PRIMARY KEY CLUSTERED - ( - [username] - ) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - -ALTER TABLE [dbo].[vcard] WITH NOCHECK ADD - CONSTRAINT [PK_vcard] PRIMARY KEY CLUSTERED - ( - [username] - ) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - -ALTER TABLE [dbo].[pubsub_node_option] WITH NOCHECK ADD - CONSTRAINT [FK_pubsub_node_option] FOREIGN KEY - ( - [nodeid] - ) REFERENCES [dbo].[pubsub_node] - ( - [nodeid] - ) -ON DELETE CASCADE -GO - -ALTER TABLE [dbo].[pubsub_node_owner] WITH NOCHECK ADD - CONSTRAINT [FK_pubsub_node_owner] FOREIGN KEY - ( - [nodeid] - ) REFERENCES [pubsub_node] - ( - [nodeid] - ) -ON DELETE CASCADE -GO - -ALTER TABLE [dbo].[pubsub_state] WITH NOCHECK ADD - CONSTRAINT [FK_pubsub_state] FOREIGN KEY - ( - [nodeid] - ) REFERENCES [pubsub_node] - ( - [nodeid] - ) -ON DELETE CASCADE -GO - -ALTER TABLE [dbo].[pubsub_item] WITH NOCHECK ADD - CONSTRAINT [FK_pubsub_item] FOREIGN KEY - ( - [nodeid] - ) REFERENCES [pubsub_node] - ( - [nodeid] - ) -ON DELETE CASCADE -GO - -CREATE INDEX [IX_vcard_search_lfn] ON [dbo].[vcard_search]([lfn]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO -CREATE INDEX [IX_vcard_search_lfamily] ON [dbo].[vcard_search]([lfamily]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO -CREATE INDEX [IX_vcard_search_lgiven] ON [dbo].[vcard_search]([lgiven]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO -CREATE INDEX [IX_vcard_search_lmiddle] ON [dbo].[vcard_search]([lmiddle]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO -CREATE INDEX [IX_vcard_search_lnickname] ON [dbo].[vcard_search]([lnickname]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO -CREATE INDEX [IX_vcard_search_lbday] ON [dbo].[vcard_search]([lbday]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO -CREATE INDEX [IX_vcard_search_lctry] ON [dbo].[vcard_search]([lctry]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO -CREATE INDEX [IX_vcard_search_llocality] ON [dbo].[vcard_search]([llocality]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO -CREATE INDEX [IX_vcard_search_lemail] ON [dbo].[vcard_search]([lemail]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO -CREATE INDEX [IX_vcard_search_lorgname] ON [dbo].[vcard_search]([lorgname]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO -CREATE INDEX [IX_vcard_search_lorgunit] ON [dbo].[vcard_search]([lorgunit]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - -CREATE CLUSTERED INDEX [IX_rosterusers_user] ON [dbo].[rosterusers]([username]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - -ALTER TABLE [dbo].[last] WITH NOCHECK ADD - CONSTRAINT [DF_last_updated] DEFAULT (getdate()) FOR [Modify_Date] -GO - -ALTER TABLE [dbo].[spool] WITH NOCHECK ADD - CONSTRAINT [DF_spool_notifyprocessed] DEFAULT (0) FOR [notifyprocessed], - CONSTRAINT [DF_spool_created] DEFAULT (getdate()) FOR [created], - CONSTRAINT [DF_spool_MustDelete] DEFAULT (0) FOR [MustDelete] -GO - -ALTER TABLE [dbo].[users] WITH NOCHECK ADD - CONSTRAINT [DF_users_created] DEFAULT (getdate()) FOR [created] -GO - -ALTER TABLE [dbo].[privacy_default_list] WITH NOCHECK ADD - CONSTRAINT [PK_privacy_defaut_list] PRIMARY KEY CLUSTERED - ( - [username] - ) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - CREATE INDEX [IX_rostergroups_jid] ON [dbo].[rostergroups]([jid]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - CREATE INDEX [IX_rostergroups_user] ON [dbo].[rostergroups]([username]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - CREATE INDEX [IX_spool_user] ON [dbo].[spool]([username]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - CREATE INDEX [IX_spool_process] ON [dbo].[spool]([created], [notifyprocessed]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - CREATE INDEX [IK_Spool_Del] ON [dbo].[spool]([MustDelete]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - CREATE INDEX [IK_Spool_Created] ON [dbo].[spool]([created]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - CREATE INDEX [IX_private_user] ON [dbo].[private_storage]([username]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - CREATE INDEX [IX_private_user_ns] ON [dbo].[private_storage]([username], [namespace]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - CREATE INDEX [IX_privacy_list_username] ON [dbo].[privacy_list]([username]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - CREATE INDEX [IX_privacy_list_username_name] ON [dbo].[privacy_list]([username], [name]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - CREATE INDEX [IX_pubsub_node_parent] ON [dbo].[pubsub_node]([parent]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - CREATE INDEX [IX_pubsub_node_tuple] ON [dbo].[pubsub_node]([host], [node]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - CREATE INDEX [IX_pubsub_node_option_nodeid] ON [dbo].[pubsub_node_option]([nodeid]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - CREATE INDEX [IX_pubsub_node_owner_nodeid] ON [dbo].[pubsub_node_owner]([nodeid]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - CREATE INDEX [IX_pubsub_state_jid] ON [dbo].[pubsub_state]([jid]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - CREATE INDEX [IX_pubsub_state_tuple] ON [dbo].[pubsub_state]([nodeid], [jid]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - CREATE INDEX [IX_pubsub_item_itemid] ON [dbo].[pubsub_item]([itemid]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - CREATE INDEX [IX_pubsub_item_tuple] ON [dbo].[pubsub_item]([nodeid], [itemid]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - CREATE INDEX [IX_pubsub_subscription_opt] ON [dbo].[pubsub_subscription_opt]([subid], [opt_name]) WITH FILLFACTOR = 90 ON [PRIMARY] -Go - -/*********************************************************/ -/** These store procedures are for use with ejabberd **/ -/** 1.1 and Microsoft Sql Server 2000 **/ -/** **/ -/** The stored procedures reduce the need to sql **/ -/** compilation of the database and also allow for also **/ -/** provide each of database integration. The stored **/ -/** procedure have been optimized to increase database **/ -/** performance and a reduction of 80% in CPU was **/ -/** achieved over the use of standard sql. **/ -/*********************************************************/ - -/****** Object: StoredProcedure [dbo].[add_roster] ******/ -/** Add or update user entries in the roster **/ -/*********************************************************/ -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[add_roster]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[add_roster] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[add_roster_group]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[add_roster_group] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[add_roster_user]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[add_roster_user] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[del_roster_groups]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[del_roster_groups] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[add_spool]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[add_spool] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[add_user]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[add_user] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[set_password]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[set_password] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[clean_spool_msg]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[clean_spool_msg] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[get_password]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[get_password] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[del_last]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[del_last] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[del_roster]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[del_roster] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[del_spool_msg]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[del_spool_msg] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[del_user]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[del_user] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[del_user_return_password]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[del_user_return_password] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[del_user_roster]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[del_user_roster] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[get_and_del_spool_msg]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[get_and_del_spool_msg] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[get_last]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[get_last] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[get_roster]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[get_roster] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[get_roster_by_jid]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[get_roster_by_jid] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[get_roster_jid_groups]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[get_roster_jid_groups] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[get_roster_groups]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[get_roster_groups] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[get_rostergroup_by_jid]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[get_rostergroup_by_jid] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[get_subscription]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[get_subscription] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[list_users]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[list_users] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[set_last]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[set_last] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[set_private_data]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[set_private_data] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[get_private_data]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[get_private_data] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[del_user_storage]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[del_user_storage] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[set_vcard]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[set_vcard] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[get_vcard]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[get_vcard] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[get_default_privacy_list]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[get_default_privacy_list] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[get_privacy_list_names]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[get_privacy_list_names] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[get_privacy_list_id]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[get_privacy_list_id] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[get_privacy_list_data]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[get_privacy_list_data] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[get_privacy_list_data_by_id]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[get_privacy_list_data_by_id] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[set_default_privacy_list]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[set_default_privacy_list] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[unset_default_privacy_list]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[unset_default_privacy_list] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[remove_privacy_list]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[remove_privacy_list] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[add_privacy_list]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[add_privacy_list] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[set_privacy_list]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[set_privacy_list] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[del_privacy_list_by_id]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[del_privacy_list_by_id] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[del_privacy_lists]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[del_privacy_lists] -GO - -CREATE PROCEDURE [dbo].[add_roster] - @Username varchar(250), - @JID varchar(250), - @Nick varchar(50), - @Subscription char(1), - @Ask char(1), - @AskMessage varchar(250), - @Server char(1), - @Subscribe varchar(200), - @Type varchar(50), - @Grp varchar(100) -AS -BEGIN - BEGIN TRANSACTION - --- Update Roster if user exist else add roster item - IF EXISTS (SELECT username FROM rosterusers WITH (NOLOCK) WHERE rosterusers.username=@Username AND rosterusers.jid=@JID) - BEGIN - UPDATE rosterusers - SET rosterusers.username=@Username, - rosterusers.jid=@JID, - rosterusers.nick=@Nick, - rosterusers.subscription=@Subscription, - rosterusers.ask=@Ask, - rosterusers.askmessage=@AskMessage, - rosterusers.server=@Server, - rosterusers.subscribe=@Subscribe, - rosterusers.type=@Type - WHERE (rosterusers.username=@Username) AND (rosterusers.jid=@JID); - END - ELSE - BEGIN - INSERT INTO rosterusers - ( rosterusers.username, - rosterusers.jid, - rosterusers.nick, - rosterusers.subscription, - rosterusers.ask, - rosterusers.askmessage, - rosterusers.server, - rosterusers.subscribe, - rosterusers.type - ) - VALUES - ( @Username, - @JID, - @Nick, - @Subscription, - @Ask, - @AskMessage, - @Server, - @Subscribe, - @Type - ); - END - - --- Update Roster Groups if exist else add group entry - IF NOT EXISTS (SELECT username FROM rostergroups WITH (NOLOCK) WHERE rostergroups.username=@Username AND rostergroups.jid=@JID AND rostergroups.grp=@Grp) - BEGIN - INSERT INTO rostergroups - ( rostergroups.username, - rostergroups.jid, - rostergroups.grp - ) - VALUES - ( @Username, - @JID, - @Grp - ); - END - - COMMIT -END -GO - -/***************************************************************/ -/****** Object: StoredProcedure [dbo].[add_roster_group] ******/ -/** Add or update user group entries in the roster groups **/ -/***************************************************************/ -CREATE PROCEDURE [dbo].[add_roster_group] - @Username varchar(250), - @JID varchar(250), - @Grp varchar(100) -AS -BEGIN - --- Update Roster Groups if exist else add group - IF NOT EXISTS (SELECT username FROM rostergroups WHERE rostergroups.username=@Username AND rostergroups.jid=@JID AND rostergroups.grp=@Grp) - BEGIN - INSERT INTO rostergroups - ( rostergroups.username, - rostergroups.jid, - rostergroups.grp - ) - VALUES - ( @Username, - @JID, - @Grp - ) - END -END -GO - -/***************************************************************/ -/****** Object: StoredProcedure [dbo].[add_roster_user] ******/ -/** Add or update user entries in the roster **/ -/***************************************************************/ -CREATE PROCEDURE [dbo].[add_roster_user] - @Username varchar(250), - @JID varchar(250), - @Nick varchar(50), - @Subscription char(1), - @Ask char(1), - @AskMessage varchar(250), - @Server char(1), - @Subscribe varchar(200), - @Type varchar(50), - @Grp varchar(100) = Null -AS -BEGIN - BEGIN TRANSACTION - --- Update Roster Users if exist of add new user - IF EXISTS (SELECT username FROM rosterusers WHERE rosterusers.username=@Username AND rosterusers.jid=@JID) - BEGIN - UPDATE rosterusers - SET rosterusers.username=@Username, - rosterusers.jid=@JID, - rosterusers.nick=@Nick, - rosterusers.subscription=@Subscription, - rosterusers.ask=@Ask, - rosterusers.askmessage=@AskMessage, - rosterusers.server=@Server, - rosterusers.subscribe=@Subscribe, - rosterusers.type=@Type - WHERE (rosterusers.username=@Username) AND (rosterusers.jid=@JID); - END - ELSE - BEGIN - INSERT INTO rosterusers - ( rosterusers.username, - rosterusers.jid, - rosterusers.nick, - rosterusers.subscription, - rosterusers.ask, - rosterusers.askmessage, - rosterusers.server, - rosterusers.subscribe, - rosterusers.type - ) - VALUES - ( @Username, - @JID, - @Nick, - @Subscription, - @Ask, - @AskMessage, - @Server, - @Subscribe, - @Type - ); - END - - --- Update Roster Group if exist of add new group - IF @Grp IS NOT NULL - EXECUTE [dbo].[add_roster_group] @Username, @JID, @Grp - - COMMIT -END -GO - -/***************************************************************/ -/****** Object: StoredProcedure [dbo].[del_roster_groups] ******/ -/** Remove user group entries from the roster groups table **/ -/***************************************************************/ -CREATE PROCEDURE [dbo].[del_roster_groups] - @Username varchar(250), - @JID varchar(250) -AS -BEGIN - DELETE FROM rostergroups - WITH (ROWLOCK) - WHERE (rostergroups.username = @Username) AND (rostergroups.jid = @JID); -END -GO - -/***************************************************************/ -/****** Object: StoredProcedure [dbo].[add_spool] ******/ -/** Add a entry to the spool table **/ -/***************************************************************/ -CREATE PROCEDURE [dbo].[add_spool] - @Username varchar(250), - @XML varchar(8000) -AS -BEGIN - INSERT INTO spool - ( spool.username, - spool.xml - ) - VALUES - ( @Username, - @XML - ) -END -GO - -/***************************************************************/ -/****** Object: StoredProcedure [dbo].[add_user] ******/ -/** Add or update user entries to jabber **/ -/***************************************************************/ -CREATE PROCEDURE [dbo].[add_user] - @Username varchar(200), - @Password varchar(50) -AS -BEGIN - INSERT INTO users - ( [username], - [password] - ) - VALUES - ( @Username, - @Password - ); -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[set_password] **/ -/** Update users password **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[set_password] - @Username varchar(200), - @Password varchar(50) -AS -BEGIN - IF EXISTS (SELECT username FROM users WITH (NOLOCK) WHERE username=@Username) - BEGIN - UPDATE users SET username=@Username, password=@Password WHERE username=@Username; - END - ELSE - BEGIN - INSERT INTO users (username, password) VALUES (@Username, @Password); - END -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[get_password] **/ -/** Retrive the user password **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[get_password] - @Username varchar(200) -AS -BEGIN - SELECT users.password as password - FROM users WITH (NOLOCK) - WHERE username=@Username; -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[set_roster_version] **/ -/** Update users roster_version **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[set_roster_version] - @Username varchar(200), - @Version varchar(8000) -AS -BEGIN - IF EXISTS (SELECT username FROM roster_version WITH (NOLOCK) WHERE username=@Username) - BEGIN - UPDATE roster_version SET username=@Username, version=@Version WHERE username=@Username; - END - ELSE - BEGIN - INSERT INTO roster_version (username, version) VALUES (@Username, @Version); - END -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[get_roster_version] **/ -/** Retrive the user roster_version **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[get_roster_version] - @Username varchar(200) -AS -BEGIN - SELECT roster_version.version as version - FROM roster_version WITH (NOLOCK) - WHERE username=@Username; -END -GO - -/***************************************************************/ -/****** Object: StoredProcedure [dbo].[clean_spool_msg] ******/ -/** Delete messages older that 3 days from spool **/ -/***************************************************************/ -CREATE PROCEDURE [dbo].[clean_spool_msg] -AS -DECLARE - @dt datetime, - @myRowCount int -BEGIN - -- Delete small amounts because if locks the database table - SET ROWCOUNT 500 - SET @myRowCount = 1 - - WHILE (@myRowCount) > 0 - BEGIN - BEGIN TRANSACTION - SELECT @dt = DATEADD(d, -3, GETDATE()) - DELETE FROM spool - WITH (ROWLOCK) - WHERE (MustDelete=1) OR (Created < @dt); - - SET @myRowCount = @@RowCount - COMMIT - END -END -GO - -/***************************************************************/ -/****** Object: StoredProcedure [dbo].[del_last] ******/ -/** Delete an entry from the last table **/ -/***************************************************************/ -CREATE PROCEDURE [dbo].[del_last] - @Username varchar(250) -AS -BEGIN - DELETE FROM [last] - WITH (ROWLOCK) - WHERE [last].username=@Username; -END -GO - -/***************************************************************/ -/****** Object: StoredProcedure [dbo].[del_roster] ******/ -/** Delete an entry from the roster **/ -/***************************************************************/ -CREATE PROCEDURE [dbo].[del_roster] - @Username varchar(250), - @JID varchar(250) -AS -BEGIN - BEGIN TRANSACTION - DELETE FROM rosterusers - WITH (ROWLOCK) - WHERE (rosterusers.username = @Username) AND (rosterusers.jid = @JID); - - DELETE FROM rostergroups - WITH (ROWLOCK) - WHERE (rostergroups.username = @Username) AND (rostergroups.jid = @JID); - COMMIT -END -GO - - -/***************************************************************/ -/****** Object: StoredProcedure [dbo].[del_spool_msg] ******/ -/** Delete an entry from the spool table **/ -/***************************************************************/ -CREATE PROCEDURE [dbo].[del_spool_msg] - @Username varchar(250) -AS -BEGIN - DELETE FROM spool - WITH (ROWLOCK) - WHERE spool.username=@Username; -END -GO - -/***************************************************************/ -/****** Object: StoredProcedure [dbo].[del_user] ******/ -/** Delete an entry from the user table **/ -/***************************************************************/ -CREATE PROCEDURE [dbo].[del_user] - @Username varchar(200) -AS -BEGIN - DELETE FROM users - WITH (ROWLOCK) - WHERE username=@Username; -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[del_user_return_password]**/ -/** Delete an entry from the user table and return user password **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[del_user_return_password] - @Username varchar(250) -AS -DECLARE - @Pwd varchar(50) -BEGIN - EXECUTE @Pwd = dbo.get_password @Username - DELETE FROM users - WITH (ROWLOCK) - WHERE username=@Username - - SELECT @Pwd; -END -GO - - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[del_user_roster] **/ -/** Delete the users roster **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[del_user_roster] - @Username varchar(250) -AS -BEGIN - BEGIN TRANSACTION - DELETE FROM rosterusers - WITH (ROWLOCK) - WHERE rosterusers.username = @Username; - - DELETE FROM rostergroups - WITH (ROWLOCK) - WHERE rostergroups.username = @Username; - COMMIT -END -GO - - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[get_and_del_spool_msg] **/ -/** Fetch and delete the users offline messages **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[get_and_del_spool_msg] - @Username varchar(250) -AS -DECLARE - @vSpool table( username varchar(1), - xml varchar(1)) -BEGIN - IF EXISTS (SELECT username FROM spool with (nolock) WHERE spool.username=@Username) - BEGIN - SELECT spool.username AS username, - spool.xml AS xml - FROM spool WITH (NOLOCK) - WHERE spool.username=@Username; - - DELETE spool - WITH (ROWLOCK) - WHERE spool.username=@Username - END - ELSE - BEGIN - SELECT * FROM @vSpool; - END -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[get_last] **/ -/** Retrive the last user login **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[get_last] - @Username varchar(250) -AS -BEGIN - SELECT last.seconds AS seconds, - last.state AS state - FROM last WITH (NOLOCK) - WHERE last.username=@Username; -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[get_roster] **/ -/** Retrive the user roster **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[get_roster] - @Username varchar(250) -AS -DECLARE - @vRosterusers table( username varchar(1), - jid varchar(1), - nick varchar(1), - subscription varchar(1), - ask varchar(1), - askmessage varchar(1), - server varchar(1), - subscribe varchar(1), - type varchar(1)) -BEGIN - IF EXISTS (SELECT username FROM rosterusers with (nolock) WHERE rosterusers.username = @Username) - BEGIN - SELECT rosterusers.username AS username, - rosterusers.jid AS jid, - rosterusers.nick AS nick, - rosterusers.subscription AS subscription, - rosterusers.ask AS ask, - rosterusers.askmessage AS askmessage, - rosterusers.server AS server, - rosterusers.subscribe AS subscribe, - rosterusers.type AS type - FROM rosterusers WITH (NOLOCK) - WHERE rosterusers.username = @Username; - END - ELSE - BEGIN - SELECT * FROM @vRosterusers - END -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[get_roster_by_jid] **/ -/** Retrive the user roster via JID **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[get_roster_by_jid] - @Username varchar(200), - @JID varchar(250) -AS -DECLARE - @vRosterusers table( username varchar(1), - jid varchar(1), - nick varchar(1), - subscription varchar(1), - ask varchar(1), - askmessage varchar(1), - server varchar(1), - subscribe varchar(1), - type varchar(1)) -BEGIN - IF EXISTS (SELECT username FROM rosterusers with (nolock) WHERE (rosterusers.username = @Username) AND (rosterusers.jid = @JID)) - BEGIN - SELECT rosterusers.username AS username, - rosterusers.jid AS jid, - rosterusers.nick AS nick, - rosterusers.subscription AS subscription, - rosterusers.ask AS ask, - rosterusers.askmessage AS askmessage, - rosterusers.server AS server, - rosterusers.subscribe AS subscribe, - rosterusers.type AS type - FROM rosterusers WITH (NOLOCK) - WHERE (rosterusers.username = @Username) AND (rosterusers.jid = @JID); - END - ELSE - BEGIN - SELECT * FROM @vRosterusers - END -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[get_roster_jid_groups] **/ -/** Retrieve the user roster groups **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[get_roster_jid_groups] - @Username varchar(200) -AS -DECLARE - @vrostergroups table( jid varchar(1), - grp varchar(1)) -BEGIN - IF EXISTS (SELECT username FROM rostergroups with (nolock) WHERE rostergroups.username = @Username) - BEGIN - SELECT rostergroups.jid AS jid, - rostergroups.grp AS grp - FROM rostergroups WITH (NOLOCK) - WHERE rostergroups.username = @Username; - END - ELSE - BEGIN - SELECT * FROM @vrostergroups - END -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[get_roster_groups] **/ -/** Retrive the user roster groups **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[get_roster_groups] - @Username varchar(200), - @JID varchar(250) -AS -DECLARE - @vrostergroups table( grp varchar(1)) -BEGIN - IF EXISTS (SELECT username FROM rostergroups with (nolock) WHERE rostergroups.username = @Username) - BEGIN - SELECT rostergroups.grp AS grp - FROM rostergroups WITH (NOLOCK) - WHERE (rostergroups.username = @Username) AND (rostergroups.jid = @JID); - END - ELSE - BEGIN - SELECT * FROM @vrostergroups - END -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[get_rostergroup_by_jid] **/ -/** Retrive the user roster groups via JID **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[get_rostergroup_by_jid] - @Username varchar(250), - @JID varchar(250) -AS -DECLARE - @vrostergroups table(grp varchar(1)) -BEGIN - IF EXISTS (SELECT username FROM rostergroups with (nolock) WHERE rostergroups.username=@Username AND rostergroups.jid=@JID) - BEGIN - SELECT rostergroups.grp AS grp - FROM rostergroups WITH (NOLOCK) - WHERE rostergroups.username=@Username AND rostergroups.jid=@JID; - END - ELSE - BEGIN - SELECT * FROM @vrostergroups - END -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[get_subscription] **/ -/** Retrive the user subscription requests **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[get_subscription] - @Username varchar(250), - @JID varchar(250) -AS -DECLARE - @vrosterusers table( subscription varchar(1)) -BEGIN - IF EXISTS (SELECT username FROM rosterusers with (nolock) WHERE rosterusers.username=@Username AND rosterusers.jid=@JID) - BEGIN - SELECT rosterusers.subscription AS subscription - FROM rosterusers WITH (NOLOCK) - WHERE rosterusers.username=@Username AND rosterusers.jid=@JID; - END - ELSE - BEGIN - SELECT * FROM @vrosterusers - END -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[list_users] **/ -/** Retrieve a list of all users **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[list_users] -AS -BEGIN - SELECT users.username AS username FROM users WITH (NOLOCK); -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[set_last] **/ -/** Update users last login status **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[set_last] - @Username varchar(250), - @Seconds varchar(50), - @State varchar(100) -AS -BEGIN - IF EXISTS (SELECT username FROM [last] WITH (NOLOCK) WHERE username=@Username) - BEGIN - UPDATE [last] - SET [last].username = @Username, - [last].seconds = @Seconds, - [last].state = @State - WHERE last.username=@Username; - END - ELSE - BEGIN - INSERT INTO [last] - ( [last].username, - [last].seconds, - [last].state - ) - VALUES - ( @Username, - @Seconds, - @State - ) - END -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[set_private_data] **/ -/** store user private data by namespace **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[set_private_data] - @Username varchar(250), - @Namespace varchar(250), - @Data varchar(8000) -AS -BEGIN - IF EXISTS (SELECT username FROM private_storage with (nolock) WHERE private_storage.username = @Username AND private_storage.namespace = @Namespace) - BEGIN - UPDATE [private_storage] - SET [private_storage].username = @Username, - [private_storage].namespace = @Namespace, - [private_storage].data = @Data - WHERE private_storage.username = @Username AND private_storage.namespace = @Namespace; - END - ELSE - BEGIN - INSERT INTO [private_storage] - ( [private_storage].username, - [private_storage].namespace, - [private_storage].data - ) - VALUES - ( @Username, - @Namespace, - @Data - ) - END -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[get_private_data] **/ -/** Retrieve user private data by namespace **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[get_private_data] - @Username varchar(250), - @Namespace varchar(250) -AS -BEGIN - SELECT private_storage.data AS data - FROM private_storage WITH (NOLOCK) - WHERE username=@Username and namespace=@Namespace; -END -GO - -/***************************************************************/ -/****** Object: StoredProcedure [dbo].[del_user_storage] ******/ -/** Delete private storage area for a given user **/ -/***************************************************************/ -CREATE PROCEDURE [dbo].[del_user_storage] - @Username varchar(250) -AS -BEGIN - DELETE FROM [private_storage] - WITH (ROWLOCK) - WHERE [private_storage].username=@Username; -END -GO - - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[set_vcard] **/ -/** Set the user's vCard **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[set_vcard] - @VCard varchar(8000), - @Username varchar(250), - @Lusername varchar(250), - @Fn varchar(8000), - @Lfn varchar(250), - @Family varchar(8000), - @Lfamily varchar(250), - @Given varchar(8000), - @Lgiven varchar(250), - @Middle varchar(8000), - @Lmiddle varchar(250), - @Nickname varchar(8000), - @Lnickname varchar(250), - @Bday varchar(8000), - @Lbday varchar(250), - @Ctry varchar(8000), - @Lctry varchar(250), - @Locality varchar(8000), - @Llocality varchar(250), - @Email varchar(8000), - @Lemail varchar(250), - @Orgname varchar(8000), - @Lorgname varchar(250), - @Orgunit varchar(8000), - @Lorgunit varchar(250) -AS -BEGIN - IF EXISTS (SELECT username FROM vcard with (nolock) WHERE vcard.username = @Username) - BEGIN - UPDATE [vcard] - SET [vcard].username = @LUsername, - [vcard].vcard = @Vcard - WHERE vcard.username = @LUsername; - - UPDATE [vcard_search] - SET [vcard_search].username = @Username, - [vcard_search].lusername = @Lusername, - [vcard_search].fn = @Fn, - [vcard_search].lfn = @Lfn, - [vcard_search].family = @Family, - [vcard_search].lfamily = @Lfamily, - [vcard_search].given = @Given, - [vcard_search].lgiven = @Lgiven, - [vcard_search].middle = @Middle, - [vcard_search].lmiddle = @Lmiddle, - [vcard_search].nickname = @Nickname, - [vcard_search].lnickname = @Lnickname, - [vcard_search].bday = @Bday, - [vcard_search].lbday = @Lbday, - [vcard_search].ctry = @Ctry, - [vcard_search].lctry = @Lctry, - [vcard_search].locality = @Locality, - [vcard_search].llocality = @Llocality, - [vcard_search].email = @Email, - [vcard_search].lemail = @Lemail, - [vcard_search].orgname = @Orgname, - [vcard_search].lorgname = @Lorgname, - [vcard_search].orgunit = @Orgunit, - [vcard_search].lorgunit = @Lorgunit - WHERE vcard_search.lusername = @LUsername; - END - ELSE - BEGIN - INSERT INTO [vcard] - ( [vcard].username, - [vcard].vcard - ) - VALUES - ( @lUsername, - @Vcard - ); - - INSERT INTO [vcard_search] - ( - [vcard_search].username , - [vcard_search].lusername , - [vcard_search].fn , - [vcard_search].lfn , - [vcard_search].family , - [vcard_search].lfamily , - [vcard_search].given , - [vcard_search].lgiven , - [vcard_search].middle , - [vcard_search].lmiddle , - [vcard_search].nickname, - [vcard_search].lnickname, - [vcard_search].bday, - [vcard_search].lbday, - [vcard_search].ctry, - [vcard_search].lctry, - [vcard_search].locality, - [vcard_search].llocality, - [vcard_search].email, - [vcard_search].lemail, - [vcard_search].orgname, - [vcard_search].lorgname, - [vcard_search].orgunit, - [vcard_search].lorgunit - ) - VALUES - ( - @Username, - @Lusername, - @Fn, - @Lfn, - @Family, - @Lfamily, - @Given, - @Lgiven, - @Middle, - @Lmiddle, - @Nickname, - @Lnickname, - @Bday, - @Lbday, - @Ctry, - @Lctry, - @Locality, - @Llocality, - @Email, - @Lemail, - @Orgname, - @Lorgname, - @Orgunit, - @Lorgunit - ) - END -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[get_vcard] **/ -/** Retrive the user's vCard **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[get_vcard] - @Username varchar(250) -AS -BEGIN - SELECT vcard.vcard as vcard - FROM vcard WITH (NOLOCK) - WHERE username=@Username; -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[get_default_privacy_list]**/ -/** Retrive the user's default privacy list **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[get_default_privacy_list] - @Username varchar(250) -AS -BEGIN - SELECT list.name - FROM privacy_default_list list WITH (NOLOCK) - WHERE list.username=@Username -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[get_privacy_list_names] **/ -/** Retrive the user's default privacy list names **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[get_privacy_list_names] - @username varchar(250) -AS -BEGIN - SELECT list.name - FROM privacy_list list WITH (NOLOCK) - WHERE list.username=@Username -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[get_privacy_list_id] **/ -/** **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[get_privacy_list_id] - @username varchar(250), - @SName varchar(250) -AS -BEGIN - SELECT id FROM privacy_list - WHERE username=@Username - AND name=@SName -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[get_privacy_list_data] **/ -/** **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[get_privacy_list_data] - @username varchar(250), - @SName varchar(250) -AS -BEGIN - SELECT l_data.t, - l_data.value, - l_data.action, - l_data.ord, - l_data.match_all, - l_data.match_iq, - l_data.match_message, - l_data.match_presence_in, - l_data.match_presence_out - FROM privacy_list_data l_data (NOLOCK) - WHERE l_data.id = (SELECT list.id - FROM privacy_list list - WHERE list.username=@username - AND list.name=@SName) - ORDER BY l_data.ord -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[get_privacy_list_data_by_id]**/ -/** **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[get_privacy_list_data_by_id] - @Id bigint -AS -BEGIN - SELECT l_data.t, - l_data.value, - l_data.action, - l_data.ord, - l_data.match_all, - l_data.match_iq, - l_data.match_message, - l_data.match_presence_in, - l_data.match_presence_out - FROM privacy_list_data l_data (NOLOCK) - WHERE l_data.id=@ID - ORDER BY l_data.ord -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[set_default_privacy_list]**/ -/** **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[set_default_privacy_list] - @username varchar(250), - @Sname varchar(250) -AS -BEGIN - IF EXISTS (SELECT username FROM privacy_default_list with (nolock) WHERE privacy_default_list.username = @Username AND privacy_default_list.name = @Sname) - BEGIN - UPDATE [privacy_default_list] - SET [privacy_default_list].username = @Username, - [privacy_default_list].name = @Sname - WHERE privacy_default_list.username = @Username - END - ELSE - BEGIN - INSERT INTO [privacy_default_list] - ( [privacy_default_list].username, - [privacy_default_list].name - ) - VALUES - ( @Username, - @SName - ) - END -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[unset_default_privacy_list]**/ -/** **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[unset_default_privacy_list] - @username varchar(250) -AS -BEGIN - DELETE - FROM privacy_default_list - WHERE privacy_default_list.username=@username -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[remove_privacy_list] **/ -/** **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[remove_privacy_list] - @username varchar(250), - @SName varchar(250) -AS -BEGIN - DELETE - FROM privacy_list - WHERE privacy_list.username=@username - AND privacy_list.name=@SName -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[add_privacy_list] **/ -/** **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[add_privacy_list] - @username varchar(250), - @SName varchar(250) -AS -BEGIN - INSERT INTO privacy_list(username, name) - VALUES (@username, @SName) -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[set_privacy_list] **/ -/** **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[set_privacy_list] - @Id bigint, - @t char(1), - @value text, - @action char(1), - @ord numeric, - @match_all bit, - @match_iq bit, - @match_message bit, - @match_presence_in bit, - @match_presence_out bit -AS -BEGIN - insert into privacy_list_data ( - id, - t, - value, - action, - ord, - match_all, - match_iq, - match_message, - match_presence_in, - match_presence_out - ) - values (@Id, - @t, - @value, - @action, - @ord, - @match_all, - @match_iq, - @match_message, - @match_presence_in, - @match_presence_out - ) - -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[del_privacy_list_by_id] **/ -/** **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[del_privacy_list_by_id] - @Id bigint -AS -BEGIN - DELETE FROM privacy_list_data - WHERE privacy_list_data.id=@Id -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[del_privacy_lists] **/ -/** **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[del_privacy_lists] - @Server varchar(250), - @username varchar(250) -AS -BEGIN - DELETE FROM privacy_list WHERE username=@username - DELETE FROM privacy_list_data WHERE convert(varchar,value)=@username+'@'+@Server - DELETE FROM privacy_default_list WHERE username=@username -END -GO diff --git a/sql/mssql2012.sql b/sql/mssql2012.sql deleted file mode 100644 index 4bb95f216..000000000 --- a/sql/mssql2012.sql +++ /dev/null @@ -1,1782 +0,0 @@ -/* - * ejabberd, Copyright (C) 2002-2015 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. - * - */ - -SET ANSI_NULLS ON -GO -SET QUOTED_IDENTIFIER ON -GO - -use [ejabber] -GO - -ALTER DATABASE CURRENT SET AUTO_CLOSE OFF - -ALTER DATABASE CURRENT SET RECOVERY BULK_LOGGED - -ALTER DATABASE CURRENT SET RECOVERY FULL - -ALTER DATABASE CURRENT SET TORN_PAGE_DETECTION ON - -ALTER DATABASE CURRENT SET READ_WRITE - -ALTER DATABASE CURRENT SET MULTI_USER - -ALTER DATABASE CURRENT SET MULTI_USER - -ALTER DATABASE CURRENT SET AUTO_SHRINK OFF - -ALTER DATABASE CURRENT SET ANSI_NULL_DEFAULT OFF - -ALTER DATABASE CURRENT SET RECURSIVE_TRIGGERS OFF - -ALTER DATABASE CURRENT SET ANSI_NULLS OFF - -ALTER DATABASE CURRENT SET CONCAT_NULL_YIELDS_NULL OFF - -ALTER DATABASE CURRENT SET CURSOR_CLOSE_ON_COMMIT OFF - -ALTER DATABASE CURRENT SET CURSOR_DEFAULT GLOBAL - -ALTER DATABASE CURRENT SET QUOTED_IDENTIFIER OFF - -ALTER DATABASE CURRENT SET ANSI_WARNINGS OFF - -ALTER DATABASE CURRENT SET AUTO_CREATE_STATISTICS ON - -if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[last]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) -drop table [dbo].[last] -GO - -if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[rostergroups]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) -drop table [dbo].[rostergroups] -GO - -if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[rosterusers]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) -drop table [dbo].[rosterusers] -GO - -if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[spool]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) -drop table [dbo].[spool] -GO - -if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[users]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) -drop table [dbo].[users] -GO - -if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[vcard]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) -drop table [dbo].[vcard] -GO - -if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[vcard_search]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) -drop table [dbo].[vcard_search] -GO - -if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[private_storage]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) -drop table [dbo].[private_storage] -GO - -if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[privacy_default_list]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) -drop table [dbo].[privacy_default_list] -GO - -if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[privacy_list]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) -drop table [dbo].[privacy_list] -GO - -if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[privacy_list_data]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) -drop table [dbo].[privacy_list_data] -GO - -if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[roster_version]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) -drop table [dbo].[roster_version] -GO - -if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[pubsub_node_option]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) -drop table [dbo].[pubsub_node_option] -GO - -if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[pubsub_node_owner]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) -drop table [dbo].[pubsub_node_owner] -GO - -if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[pubsub_state]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) -drop table [dbo].[pubsub_state] -GO - -if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[pubsub_item]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) -drop table [dbo].[pubsub_item] -GO - -if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[pubsub_subscription_opt]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) -drop table [dbo].[pubsub_subscription_opt] -GO - -if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[pubsub_node]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) -drop table [dbo].[pubsub_node] -GO - -CREATE TABLE [dbo].[last] ( - [username] [varchar] (250) NOT NULL , - [seconds] [varchar] (50) NOT NULL , - [state] [varchar] (100) NOT NULL , - [Modify_Date] [datetime] NOT NULL -) ON [PRIMARY] -GO - -CREATE TABLE [dbo].[rostergroups] ( - [username] [varchar] (250) NOT NULL , - [jid] [varchar] (250) NOT NULL , - [grp] [varchar] (100) NOT NULL -) ON [PRIMARY] -GO - -CREATE TABLE [dbo].[rosterusers] ( - [username] [varchar] (250) NOT NULL , - [jid] [varchar] (250) NOT NULL , - [nick] [varchar] (50) NOT NULL , - [subscription] [char] (1) NOT NULL , - [ask] [char] (1) NOT NULL , - [askmessage] [varchar] (250) NOT NULL , - [server] [char] (1) NOT NULL , - [subscribe] [varchar] (200) NULL , - [type] [varchar] (50) NULL , -CONSTRAINT [PK_rosterusers] PRIMARY KEY NONCLUSTERED -( - [username] ASC, - [jid] ASC -)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] -) ON [PRIMARY] -GO - -CREATE TABLE [dbo].[spool] ( - [id] [numeric](19, 0) IDENTITY (1, 1) NOT NULL , - [username] [varchar] (250) NOT NULL , - [xml] [text] NOT NULL , - [notifyprocessed] [bit] NULL , - [created] [datetime] NULL , - [MustDelete] [bit] NOT NULL -) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] -GO - -CREATE TABLE [dbo].[users] ( - [username] [varchar] (250) NOT NULL , - [password] [varchar] (50) NOT NULL , - [created] [datetime] NULL -) ON [PRIMARY] -GO - -CREATE TABLE [dbo].[vcard] ( - [username] [varchar] (250) NOT NULL , - [vcard] [text] NOT NULL -) ON [PRIMARY] -GO - -CREATE TABLE [dbo].[vcard_search] ( - [username] [varchar] (250) NOT NULL , - [lusername] [varchar] (250) NOT NULL , - [fn] [text] NOT NULL , - [lfn] [varchar] (250) NOT NULL , - [family] [text] NOT NULL , - [lfamily] [varchar] (250) NOT NULL , - [given] [text] NOT NULL , - [lgiven] [varchar] (250) NOT NULL , - [middle] [text] NOT NULL , - [lmiddle] [varchar] (250) NOT NULL , - [nickname] [text] NOT NULL , - [lnickname] [varchar] (250) NOT NULL , - [bday] [text] NOT NULL , - [lbday] [varchar] (250) NOT NULL , - [ctry] [text] NOT NULL , - [lctry] [varchar] (250) NOT NULL , - [locality] [text] NOT NULL , - [llocality] [varchar] (250) NOT NULL , - [email] [text] NOT NULL , - [lemail] [varchar] (250) NOT NULL , - [orgname] [text] NOT NULL , - [lorgname] [varchar] (250) NOT NULL , - [orgunit] [text] NOT NULL , - [lorgunit] [varchar] (250) NOT NULL -) ON [PRIMARY] -GO - -CREATE TABLE [dbo].[private_storage] ( - [username] [varchar] (250) NOT NULL , - [namespace] [varchar] (250) NOT NULL , - [data] [text] NOT NULL -) ON [PRIMARY] -GO - -CREATE TABLE [dbo].[privacy_default_list] ( - [username] [varchar] (250) NOT NULL, - [name] [varchar] (250) NOT NULL -) ON [PRIMARY] -GO - -CREATE TABLE [dbo].[privacy_list]( - [username] [varchar](250) NOT NULL, - [name] [varchar](250) NOT NULL, - [id] [bigint] IDENTITY(1,1) NOT NULL, - CONSTRAINT [PK_privacy_list] PRIMARY KEY CLUSTERED -( - [id] ASC -)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] -) ON [PRIMARY] -GO - -CREATE TABLE [dbo].[privacy_list_data] ( - [id] [bigint] NOT NULL, - [t] [character] (1) NOT NULL, - [value] [text] NOT NULL, - [action] [character] (1) NOT NULL, - [ord] [NUMERIC] NOT NULL, - [match_all] [bit] NOT NULL, - [match_iq] [bit] NOT NULL, - [match_message] [bit] NOT NULL, - [match_presence_in] [bit] NOT NULL, - [match_presence_out] [bit] NOT NULL -) ON [PRIMARY] -GO - -CREATE TABLE [dbo].[roster_version] ( - [username] [varchar](250) PRIMARY KEY, - [version] [text] NOT NULL -) ON [PRIMARY] -GO - -CREATE TABLE [dbo].[pubsub_node] ( - [host] [varchar](250), - [node] [varchar](250), - [parent] [varchar](250), - [type] [varchar](250), - [nodeid] [bigint] IDENTITY(1,1) PRIMARY KEY -) ON [PRIMARY] -GO - -CREATE TABLE [dbo].[pubsub_node_option] ( - [nodeid] [bigint], - [name] [varchar](250), - [val] [varchar](250) -) ON [PRIMARY] -GO - -CREATE TABLE [dbo].[pubsub_node_owner] ( - [nodeid] [bigint], - [owner] [varchar](250) -) ON [PRIMARY] -GO - -CREATE TABLE [dbo].[pubsub_state] ( - [nodeid] [bigint], - [jid] [varchar](250), - [affiliation] [CHAR](1), - [subscriptions] [text], - [stateid] [bigint] IDENTITY(1,1) PRIMARY KEY -) ON [PRIMARY] -GO - -CREATE TABLE [dbo].[pubsub_item] ( - [nodeid] [bigint], - [itemid] [varchar](250), - [publisher] [text], - [creation] [text], - [modification] [text], - [payload] [text] -) ON [PRIMARY] -GO - -CREATE TABLE [dbo].[pubsub_subscription_opt] ( - [subid] [varchar](250), - [opt_name] [varchar](32), - [opt_value] [text] -) ON [PRIMARY] -GO - -/* Constraints to add: -- id in privacy_list is a SERIAL autogenerated number -- id in privacy_list_data must exist in the table privacy_list */ - -ALTER TABLE [dbo].[last] WITH NOCHECK ADD - CONSTRAINT [PK_last] PRIMARY KEY CLUSTERED - ( - [username] - ) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - -ALTER TABLE [dbo].[rostergroups] WITH NOCHECK ADD - CONSTRAINT [PK_rostergroups] PRIMARY KEY CLUSTERED - ( - [username], - [jid], - [grp] - ) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - -ALTER TABLE [dbo].[spool] WITH NOCHECK ADD - CONSTRAINT [PK_spool] PRIMARY KEY CLUSTERED - ( - [username], - [id] - ) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - -ALTER TABLE [dbo].[users] WITH NOCHECK ADD - CONSTRAINT [PK_users] PRIMARY KEY CLUSTERED - ( - [username] - ) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - -ALTER TABLE [dbo].[vcard] WITH NOCHECK ADD - CONSTRAINT [PK_vcard] PRIMARY KEY CLUSTERED - ( - [username] - ) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - -ALTER TABLE [dbo].[pubsub_node_option] WITH NOCHECK ADD - CONSTRAINT [FK_pubsub_node_option] FOREIGN KEY - ( - [nodeid] - ) REFERENCES [dbo].[pubsub_node] - ( - [nodeid] - ) -ON DELETE CASCADE -GO - -ALTER TABLE [dbo].[pubsub_node_owner] WITH NOCHECK ADD - CONSTRAINT [FK_pubsub_node_owner] FOREIGN KEY - ( - [nodeid] - ) REFERENCES [pubsub_node] - ( - [nodeid] - ) -ON DELETE CASCADE -GO - -ALTER TABLE [dbo].[pubsub_state] WITH NOCHECK ADD - CONSTRAINT [FK_pubsub_state] FOREIGN KEY - ( - [nodeid] - ) REFERENCES [pubsub_node] - ( - [nodeid] - ) -ON DELETE CASCADE -GO - -ALTER TABLE [dbo].[pubsub_item] WITH NOCHECK ADD - CONSTRAINT [FK_pubsub_item] FOREIGN KEY - ( - [nodeid] - ) REFERENCES [pubsub_node] - ( - [nodeid] - ) -ON DELETE CASCADE -GO - -CREATE INDEX [IX_vcard_search_lfn] ON [dbo].[vcard_search]([lfn]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO -CREATE INDEX [IX_vcard_search_lfamily] ON [dbo].[vcard_search]([lfamily]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO -CREATE INDEX [IX_vcard_search_lgiven] ON [dbo].[vcard_search]([lgiven]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO -CREATE INDEX [IX_vcard_search_lmiddle] ON [dbo].[vcard_search]([lmiddle]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO -CREATE INDEX [IX_vcard_search_lnickname] ON [dbo].[vcard_search]([lnickname]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO -CREATE INDEX [IX_vcard_search_lbday] ON [dbo].[vcard_search]([lbday]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO -CREATE INDEX [IX_vcard_search_lctry] ON [dbo].[vcard_search]([lctry]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO -CREATE INDEX [IX_vcard_search_llocality] ON [dbo].[vcard_search]([llocality]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO -CREATE INDEX [IX_vcard_search_lemail] ON [dbo].[vcard_search]([lemail]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO -CREATE INDEX [IX_vcard_search_lorgname] ON [dbo].[vcard_search]([lorgname]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO -CREATE INDEX [IX_vcard_search_lorgunit] ON [dbo].[vcard_search]([lorgunit]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - -CREATE CLUSTERED INDEX [IX_rosterusers_user] ON [dbo].[rosterusers]([username]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - -ALTER TABLE [dbo].[last] WITH NOCHECK ADD - CONSTRAINT [DF_last_updated] DEFAULT (getdate()) FOR [Modify_Date] -GO - -ALTER TABLE [dbo].[spool] WITH NOCHECK ADD - CONSTRAINT [DF_spool_notifyprocessed] DEFAULT (0) FOR [notifyprocessed], - CONSTRAINT [DF_spool_created] DEFAULT (getdate()) FOR [created], - CONSTRAINT [DF_spool_MustDelete] DEFAULT (0) FOR [MustDelete] -GO - -ALTER TABLE [dbo].[users] WITH NOCHECK ADD - CONSTRAINT [DF_users_created] DEFAULT (getdate()) FOR [created] -GO - -ALTER TABLE [dbo].[privacy_default_list] WITH NOCHECK ADD - CONSTRAINT [PK_privacy_defaut_list] PRIMARY KEY CLUSTERED - ( - [username] - ) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - CREATE INDEX [IX_rostergroups_jid] ON [dbo].[rostergroups]([jid]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - CREATE INDEX [IX_rostergroups_user] ON [dbo].[rostergroups]([username]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - CREATE INDEX [IX_spool_user] ON [dbo].[spool]([username]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - CREATE INDEX [IX_spool_process] ON [dbo].[spool]([created], [notifyprocessed]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - CREATE INDEX [IK_Spool_Del] ON [dbo].[spool]([MustDelete]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - CREATE INDEX [IK_Spool_Created] ON [dbo].[spool]([created]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - CREATE INDEX [IX_private_user] ON [dbo].[private_storage]([username]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - CREATE INDEX [IX_private_user_ns] ON [dbo].[private_storage]([username], [namespace]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - CREATE INDEX [IX_privacy_list_username] ON [dbo].[privacy_list]([username]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - CREATE INDEX [IX_privacy_list_username_name] ON [dbo].[privacy_list]([username], [name]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - CREATE INDEX [IX_pubsub_node_parent] ON [dbo].[pubsub_node]([parent]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - CREATE INDEX [IX_pubsub_node_tuple] ON [dbo].[pubsub_node]([host], [node]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - CREATE INDEX [IX_pubsub_node_option_nodeid] ON [dbo].[pubsub_node_option]([nodeid]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - CREATE INDEX [IX_pubsub_node_owner_nodeid] ON [dbo].[pubsub_node_owner]([nodeid]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - CREATE INDEX [IX_pubsub_state_jid] ON [dbo].[pubsub_state]([jid]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - CREATE INDEX [IX_pubsub_state_tuple] ON [dbo].[pubsub_state]([nodeid], [jid]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - CREATE INDEX [IX_pubsub_item_itemid] ON [dbo].[pubsub_item]([itemid]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - CREATE INDEX [IX_pubsub_item_tuple] ON [dbo].[pubsub_item]([nodeid], [itemid]) WITH FILLFACTOR = 90 ON [PRIMARY] -GO - - CREATE INDEX [IX_pubsub_subscription_opt] ON [dbo].[pubsub_subscription_opt]([subid], [opt_name]) WITH FILLFACTOR = 90 ON [PRIMARY] -Go - -/*********************************************************/ -/** These store procedures are for use with ejabberd **/ -/** 1.1 and Microsoft Sql Server 2000 **/ -/** **/ -/** The stored procedures reduce the need to sql **/ -/** compilation of the database and also allow for also **/ -/** provide each of database integration. The stored **/ -/** procedure have been optimized to increase database **/ -/** performance and a reduction of 80% in CPU was **/ -/** achieved over the use of standard sql. **/ -/*********************************************************/ - -/****** Object: StoredProcedure [dbo].[add_roster] ******/ -/** Add or update user entries in the roster **/ -/*********************************************************/ -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[add_roster]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[add_roster] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[add_roster_group]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[add_roster_group] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[add_roster_user]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[add_roster_user] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[del_roster_groups]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[del_roster_groups] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[add_spool]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[add_spool] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[add_user]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[add_user] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[set_password]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[set_password] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[clean_spool_msg]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[clean_spool_msg] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[get_password]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[get_password] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[del_last]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[del_last] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[del_roster]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[del_roster] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[del_spool_msg]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[del_spool_msg] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[del_user]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[del_user] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[del_user_return_password]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[del_user_return_password] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[del_user_roster]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[del_user_roster] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[get_and_del_spool_msg]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[get_and_del_spool_msg] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[get_last]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[get_last] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[get_roster]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[get_roster] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[get_roster_by_jid]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[get_roster_by_jid] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[get_roster_jid_groups]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[get_roster_jid_groups] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[get_roster_groups]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[get_roster_groups] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[get_rostergroup_by_jid]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[get_rostergroup_by_jid] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[get_subscription]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[get_subscription] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[list_users]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[list_users] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[set_last]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[set_last] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[set_private_data]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[set_private_data] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[get_private_data]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[get_private_data] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[del_user_storage]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[del_user_storage] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[set_vcard]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[set_vcard] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[get_vcard]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[get_vcard] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[get_default_privacy_list]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[get_default_privacy_list] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[get_privacy_list_names]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[get_privacy_list_names] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[get_privacy_list_id]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[get_privacy_list_id] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[get_privacy_list_data]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[get_privacy_list_data] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[get_privacy_list_data_by_id]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[get_privacy_list_data_by_id] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[set_default_privacy_list]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[set_default_privacy_list] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[unset_default_privacy_list]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[unset_default_privacy_list] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[remove_privacy_list]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[remove_privacy_list] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[add_privacy_list]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[add_privacy_list] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[set_privacy_list]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[set_privacy_list] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[del_privacy_list_by_id]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[del_privacy_list_by_id] -GO -IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[del_privacy_lists]') AND type in (N'P', N'PC')) -DROP PROCEDURE [dbo].[del_privacy_lists] -GO - -CREATE PROCEDURE [dbo].[add_roster] - @Username varchar(250), - @JID varchar(250), - @Nick varchar(50), - @Subscription char(1), - @Ask char(1), - @AskMessage varchar(250), - @Server char(1), - @Subscribe varchar(200), - @Type varchar(50), - @Grp varchar(100) -AS -BEGIN - BEGIN TRANSACTION - --- Update Roster if user exist else add roster item - IF EXISTS (SELECT username FROM rosterusers WITH (NOLOCK) WHERE rosterusers.username=@Username AND rosterusers.jid=@JID) - BEGIN - UPDATE rosterusers - SET rosterusers.username=@Username, - rosterusers.jid=@JID, - rosterusers.nick=@Nick, - rosterusers.subscription=@Subscription, - rosterusers.ask=@Ask, - rosterusers.askmessage=@AskMessage, - rosterusers.server=@Server, - rosterusers.subscribe=@Subscribe, - rosterusers.type=@Type - WHERE (rosterusers.username=@Username) AND (rosterusers.jid=@JID); - END - ELSE - BEGIN - INSERT INTO rosterusers - ( rosterusers.username, - rosterusers.jid, - rosterusers.nick, - rosterusers.subscription, - rosterusers.ask, - rosterusers.askmessage, - rosterusers.server, - rosterusers.subscribe, - rosterusers.type - ) - VALUES - ( @Username, - @JID, - @Nick, - @Subscription, - @Ask, - @AskMessage, - @Server, - @Subscribe, - @Type - ); - END - - --- Update Roster Groups if exist else add group entry - IF NOT EXISTS (SELECT username FROM rostergroups WITH (NOLOCK) WHERE rostergroups.username=@Username AND rostergroups.jid=@JID AND rostergroups.grp=@Grp) - BEGIN - INSERT INTO rostergroups - ( rostergroups.username, - rostergroups.jid, - rostergroups.grp - ) - VALUES - ( @Username, - @JID, - @Grp - ); - END - - COMMIT -END -GO - -/***************************************************************/ -/****** Object: StoredProcedure [dbo].[add_roster_group] ******/ -/** Add or update user group entries in the roster groups **/ -/***************************************************************/ -CREATE PROCEDURE [dbo].[add_roster_group] - @Username varchar(250), - @JID varchar(250), - @Grp varchar(100) -AS -BEGIN - --- Update Roster Groups if exist else add group - IF NOT EXISTS (SELECT username FROM rostergroups WHERE rostergroups.username=@Username AND rostergroups.jid=@JID AND rostergroups.grp=@Grp) - BEGIN - INSERT INTO rostergroups - ( rostergroups.username, - rostergroups.jid, - rostergroups.grp - ) - VALUES - ( @Username, - @JID, - @Grp - ) - END -END -GO - -/***************************************************************/ -/****** Object: StoredProcedure [dbo].[add_roster_user] ******/ -/** Add or update user entries in the roster **/ -/***************************************************************/ -CREATE PROCEDURE [dbo].[add_roster_user] - @Username varchar(250), - @JID varchar(250), - @Nick varchar(50), - @Subscription char(1), - @Ask char(1), - @AskMessage varchar(250), - @Server char(1), - @Subscribe varchar(200), - @Type varchar(50), - @Grp varchar(100) = Null -AS -BEGIN - BEGIN TRANSACTION - --- Update Roster Users if exist of add new user - IF EXISTS (SELECT username FROM rosterusers WHERE rosterusers.username=@Username AND rosterusers.jid=@JID) - BEGIN - UPDATE rosterusers - SET rosterusers.username=@Username, - rosterusers.jid=@JID, - rosterusers.nick=@Nick, - rosterusers.subscription=@Subscription, - rosterusers.ask=@Ask, - rosterusers.askmessage=@AskMessage, - rosterusers.server=@Server, - rosterusers.subscribe=@Subscribe, - rosterusers.type=@Type - WHERE (rosterusers.username=@Username) AND (rosterusers.jid=@JID); - END - ELSE - BEGIN - INSERT INTO rosterusers - ( rosterusers.username, - rosterusers.jid, - rosterusers.nick, - rosterusers.subscription, - rosterusers.ask, - rosterusers.askmessage, - rosterusers.server, - rosterusers.subscribe, - rosterusers.type - ) - VALUES - ( @Username, - @JID, - @Nick, - @Subscription, - @Ask, - @AskMessage, - @Server, - @Subscribe, - @Type - ); - END - - --- Update Roster Group if exist of add new group - IF @Grp IS NOT NULL - EXECUTE [dbo].[add_roster_group] @Username, @JID, @Grp - - COMMIT -END -GO - -/***************************************************************/ -/****** Object: StoredProcedure [dbo].[del_roster_groups] ******/ -/** Remove user group entries from the roster groups table **/ -/***************************************************************/ -CREATE PROCEDURE [dbo].[del_roster_groups] - @Username varchar(250), - @JID varchar(250) -AS -BEGIN - DELETE FROM rostergroups - WITH (ROWLOCK) - WHERE (rostergroups.username = @Username) AND (rostergroups.jid = @JID); -END -GO - -/***************************************************************/ -/****** Object: StoredProcedure [dbo].[add_spool] ******/ -/** Add a entry to the spool table **/ -/***************************************************************/ -CREATE PROCEDURE [dbo].[add_spool] - @Username varchar(250), - @XML varchar(8000) -AS -BEGIN - INSERT INTO spool - ( spool.username, - spool.xml - ) - VALUES - ( @Username, - @XML - ) -END -GO - -/***************************************************************/ -/****** Object: StoredProcedure [dbo].[add_user] ******/ -/** Add or update user entries to jabber **/ -/***************************************************************/ -CREATE PROCEDURE [dbo].[add_user] - @Username varchar(200), - @Password varchar(50) -AS -BEGIN - INSERT INTO users - ( [username], - [password] - ) - VALUES - ( @Username, - @Password - ); -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[set_password] **/ -/** Update users password **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[set_password] - @Username varchar(200), - @Password varchar(50) -AS -BEGIN - IF EXISTS (SELECT username FROM users WITH (NOLOCK) WHERE username=@Username) - BEGIN - UPDATE users SET username=@Username, password=@Password WHERE username=@Username; - END - ELSE - BEGIN - INSERT INTO users (username, password) VALUES (@Username, @Password); - END -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[get_password] **/ -/** Retrive the user password **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[get_password] - @Username varchar(200) -AS -BEGIN - SELECT users.password as password - FROM users WITH (NOLOCK) - WHERE username=@Username; -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[set_roster_version] **/ -/** Update users roster_version **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[set_roster_version] - @Username varchar(200), - @Version varchar(8000) -AS -BEGIN - IF EXISTS (SELECT username FROM roster_version WITH (NOLOCK) WHERE username=@Username) - BEGIN - UPDATE roster_version SET username=@Username, version=@Version WHERE username=@Username; - END - ELSE - BEGIN - INSERT INTO roster_version (username, version) VALUES (@Username, @Version); - END -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[get_roster_version] **/ -/** Retrive the user roster_version **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[get_roster_version] - @Username varchar(200) -AS -BEGIN - SELECT roster_version.version as version - FROM roster_version WITH (NOLOCK) - WHERE username=@Username; -END -GO - -/***************************************************************/ -/****** Object: StoredProcedure [dbo].[clean_spool_msg] ******/ -/** Delete messages older that 3 days from spool **/ -/***************************************************************/ -CREATE PROCEDURE [dbo].[clean_spool_msg] -AS -DECLARE - @dt datetime, - @myRowCount int -BEGIN - -- Delete small amounts because if locks the database table - SET ROWCOUNT 500 - SET @myRowCount = 1 - - WHILE (@myRowCount) > 0 - BEGIN - BEGIN TRANSACTION - SELECT @dt = DATEADD(d, -3, GETDATE()) - DELETE FROM spool - WITH (ROWLOCK) - WHERE (MustDelete=1) OR (Created < @dt); - - SET @myRowCount = @@RowCount - COMMIT - END -END -GO - -/***************************************************************/ -/****** Object: StoredProcedure [dbo].[del_last] ******/ -/** Delete an entry from the last table **/ -/***************************************************************/ -CREATE PROCEDURE [dbo].[del_last] - @Username varchar(250) -AS -BEGIN - DELETE FROM [last] - WITH (ROWLOCK) - WHERE [last].username=@Username; -END -GO - -/***************************************************************/ -/****** Object: StoredProcedure [dbo].[del_roster] ******/ -/** Delete an entry from the roster **/ -/***************************************************************/ -CREATE PROCEDURE [dbo].[del_roster] - @Username varchar(250), - @JID varchar(250) -AS -BEGIN - BEGIN TRANSACTION - DELETE FROM rosterusers - WITH (ROWLOCK) - WHERE (rosterusers.username = @Username) AND (rosterusers.jid = @JID); - - DELETE FROM rostergroups - WITH (ROWLOCK) - WHERE (rostergroups.username = @Username) AND (rostergroups.jid = @JID); - COMMIT -END -GO - - -/***************************************************************/ -/****** Object: StoredProcedure [dbo].[del_spool_msg] ******/ -/** Delete an entry from the spool table **/ -/***************************************************************/ -CREATE PROCEDURE [dbo].[del_spool_msg] - @Username varchar(250) -AS -BEGIN - DELETE FROM spool - WITH (ROWLOCK) - WHERE spool.username=@Username; -END -GO - -/***************************************************************/ -/****** Object: StoredProcedure [dbo].[del_user] ******/ -/** Delete an entry from the user table **/ -/***************************************************************/ -CREATE PROCEDURE [dbo].[del_user] - @Username varchar(200) -AS -BEGIN - DELETE FROM users - WITH (ROWLOCK) - WHERE username=@Username; -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[del_user_return_password]**/ -/** Delete an entry from the user table and return user password **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[del_user_return_password] - @Username varchar(250) -AS -DECLARE - @Pwd varchar(50) -BEGIN - EXECUTE @Pwd = dbo.get_password @Username - DELETE FROM users - WITH (ROWLOCK) - WHERE username=@Username - - SELECT @Pwd; -END -GO - - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[del_user_roster] **/ -/** Delete the users roster **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[del_user_roster] - @Username varchar(250) -AS -BEGIN - BEGIN TRANSACTION - DELETE FROM rosterusers - WITH (ROWLOCK) - WHERE rosterusers.username = @Username; - - DELETE FROM rostergroups - WITH (ROWLOCK) - WHERE rostergroups.username = @Username; - COMMIT -END -GO - - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[get_and_del_spool_msg] **/ -/** Fetch and delete the users offline messages **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[get_and_del_spool_msg] - @Username varchar(250) -AS -DECLARE - @vSpool table( username varchar(1), - xml varchar(1)) -BEGIN - IF EXISTS (SELECT username FROM spool with (nolock) WHERE spool.username=@Username) - BEGIN - SELECT spool.username AS username, - spool.xml AS xml - FROM spool WITH (NOLOCK) - WHERE spool.username=@Username; - - DELETE spool - WITH (ROWLOCK) - WHERE spool.username=@Username - END - ELSE - BEGIN - SELECT * FROM @vSpool; - END -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[get_last] **/ -/** Retrive the last user login **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[get_last] - @Username varchar(250) -AS -BEGIN - SELECT last.seconds AS seconds, - last.state AS state - FROM last WITH (NOLOCK) - WHERE last.username=@Username; -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[get_roster] **/ -/** Retrive the user roster **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[get_roster] - @Username varchar(250) -AS -DECLARE - @vRosterusers table( username varchar(1), - jid varchar(1), - nick varchar(1), - subscription varchar(1), - ask varchar(1), - askmessage varchar(1), - server varchar(1), - subscribe varchar(1), - type varchar(1)) -BEGIN - IF EXISTS (SELECT username FROM rosterusers with (nolock) WHERE rosterusers.username = @Username) - BEGIN - SELECT rosterusers.username AS username, - rosterusers.jid AS jid, - rosterusers.nick AS nick, - rosterusers.subscription AS subscription, - rosterusers.ask AS ask, - rosterusers.askmessage AS askmessage, - rosterusers.server AS server, - rosterusers.subscribe AS subscribe, - rosterusers.type AS type - FROM rosterusers WITH (NOLOCK) - WHERE rosterusers.username = @Username; - END - ELSE - BEGIN - SELECT * FROM @vRosterusers - END -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[get_roster_by_jid] **/ -/** Retrive the user roster via JID **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[get_roster_by_jid] - @Username varchar(200), - @JID varchar(250) -AS -DECLARE - @vRosterusers table( username varchar(1), - jid varchar(1), - nick varchar(1), - subscription varchar(1), - ask varchar(1), - askmessage varchar(1), - server varchar(1), - subscribe varchar(1), - type varchar(1)) -BEGIN - IF EXISTS (SELECT username FROM rosterusers with (nolock) WHERE (rosterusers.username = @Username) AND (rosterusers.jid = @JID)) - BEGIN - SELECT rosterusers.username AS username, - rosterusers.jid AS jid, - rosterusers.nick AS nick, - rosterusers.subscription AS subscription, - rosterusers.ask AS ask, - rosterusers.askmessage AS askmessage, - rosterusers.server AS server, - rosterusers.subscribe AS subscribe, - rosterusers.type AS type - FROM rosterusers WITH (NOLOCK) - WHERE (rosterusers.username = @Username) AND (rosterusers.jid = @JID); - END - ELSE - BEGIN - SELECT * FROM @vRosterusers - END -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[get_roster_jid_groups] **/ -/** Retrieve the user roster groups **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[get_roster_jid_groups] - @Username varchar(200) -AS -DECLARE - @vrostergroups table( jid varchar(1), - grp varchar(1)) -BEGIN - IF EXISTS (SELECT username FROM rostergroups with (nolock) WHERE rostergroups.username = @Username) - BEGIN - SELECT rostergroups.jid AS jid, - rostergroups.grp AS grp - FROM rostergroups WITH (NOLOCK) - WHERE rostergroups.username = @Username; - END - ELSE - BEGIN - SELECT * FROM @vrostergroups - END -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[get_roster_groups] **/ -/** Retrive the user roster groups **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[get_roster_groups] - @Username varchar(200), - @JID varchar(250) -AS -DECLARE - @vrostergroups table( grp varchar(1)) -BEGIN - IF EXISTS (SELECT username FROM rostergroups with (nolock) WHERE rostergroups.username = @Username) - BEGIN - SELECT rostergroups.grp AS grp - FROM rostergroups WITH (NOLOCK) - WHERE (rostergroups.username = @Username) AND (rostergroups.jid = @JID); - END - ELSE - BEGIN - SELECT * FROM @vrostergroups - END -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[get_rostergroup_by_jid] **/ -/** Retrive the user roster groups via JID **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[get_rostergroup_by_jid] - @Username varchar(250), - @JID varchar(250) -AS -DECLARE - @vrostergroups table(grp varchar(1)) -BEGIN - IF EXISTS (SELECT username FROM rostergroups with (nolock) WHERE rostergroups.username=@Username AND rostergroups.jid=@JID) - BEGIN - SELECT rostergroups.grp AS grp - FROM rostergroups WITH (NOLOCK) - WHERE rostergroups.username=@Username AND rostergroups.jid=@JID; - END - ELSE - BEGIN - SELECT * FROM @vrostergroups - END -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[get_subscription] **/ -/** Retrive the user subscription requests **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[get_subscription] - @Username varchar(250), - @JID varchar(250) -AS -DECLARE - @vrosterusers table( subscription varchar(1)) -BEGIN - IF EXISTS (SELECT username FROM rosterusers with (nolock) WHERE rosterusers.username=@Username AND rosterusers.jid=@JID) - BEGIN - SELECT rosterusers.subscription AS subscription - FROM rosterusers WITH (NOLOCK) - WHERE rosterusers.username=@Username AND rosterusers.jid=@JID; - END - ELSE - BEGIN - SELECT * FROM @vrosterusers - END -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[list_users] **/ -/** Retrieve a list of all users **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[list_users] -AS -BEGIN - SELECT users.username AS username FROM users WITH (NOLOCK); -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[set_last] **/ -/** Update users last login status **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[set_last] - @Username varchar(250), - @Seconds varchar(50), - @State varchar(100) -AS -BEGIN - IF EXISTS (SELECT username FROM [last] WITH (NOLOCK) WHERE username=@Username) - BEGIN - UPDATE [last] - SET [last].username = @Username, - [last].seconds = @Seconds, - [last].state = @State - WHERE last.username=@Username; - END - ELSE - BEGIN - INSERT INTO [last] - ( [last].username, - [last].seconds, - [last].state - ) - VALUES - ( @Username, - @Seconds, - @State - ) - END -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[set_private_data] **/ -/** store user private data by namespace **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[set_private_data] - @Username varchar(250), - @Namespace varchar(250), - @Data varchar(8000) -AS -BEGIN - IF EXISTS (SELECT username FROM private_storage with (nolock) WHERE private_storage.username = @Username AND private_storage.namespace = @Namespace) - BEGIN - UPDATE [private_storage] - SET [private_storage].username = @Username, - [private_storage].namespace = @Namespace, - [private_storage].data = @Data - WHERE private_storage.username = @Username AND private_storage.namespace = @Namespace; - END - ELSE - BEGIN - INSERT INTO [private_storage] - ( [private_storage].username, - [private_storage].namespace, - [private_storage].data - ) - VALUES - ( @Username, - @Namespace, - @Data - ) - END -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[get_private_data] **/ -/** Retrieve user private data by namespace **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[get_private_data] - @Username varchar(250), - @Namespace varchar(250) -AS -BEGIN - SELECT private_storage.data AS data - FROM private_storage WITH (NOLOCK) - WHERE username=@Username and namespace=@Namespace; -END -GO - -/***************************************************************/ -/****** Object: StoredProcedure [dbo].[del_user_storage] ******/ -/** Delete private storage area for a given user **/ -/***************************************************************/ -CREATE PROCEDURE [dbo].[del_user_storage] - @Username varchar(250) -AS -BEGIN - DELETE FROM [private_storage] - WITH (ROWLOCK) - WHERE [private_storage].username=@Username; -END -GO - - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[set_vcard] **/ -/** Set the user's vCard **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[set_vcard] - @VCard varchar(8000), - @Username varchar(250), - @Lusername varchar(250), - @Fn varchar(8000), - @Lfn varchar(250), - @Family varchar(8000), - @Lfamily varchar(250), - @Given varchar(8000), - @Lgiven varchar(250), - @Middle varchar(8000), - @Lmiddle varchar(250), - @Nickname varchar(8000), - @Lnickname varchar(250), - @Bday varchar(8000), - @Lbday varchar(250), - @Ctry varchar(8000), - @Lctry varchar(250), - @Locality varchar(8000), - @Llocality varchar(250), - @Email varchar(8000), - @Lemail varchar(250), - @Orgname varchar(8000), - @Lorgname varchar(250), - @Orgunit varchar(8000), - @Lorgunit varchar(250) -AS -BEGIN - IF EXISTS (SELECT username FROM vcard with (nolock) WHERE vcard.username = @Username) - BEGIN - UPDATE [vcard] - SET [vcard].username = @LUsername, - [vcard].vcard = @Vcard - WHERE vcard.username = @LUsername; - - UPDATE [vcard_search] - SET [vcard_search].username = @Username, - [vcard_search].lusername = @Lusername, - [vcard_search].fn = @Fn, - [vcard_search].lfn = @Lfn, - [vcard_search].family = @Family, - [vcard_search].lfamily = @Lfamily, - [vcard_search].given = @Given, - [vcard_search].lgiven = @Lgiven, - [vcard_search].middle = @Middle, - [vcard_search].lmiddle = @Lmiddle, - [vcard_search].nickname = @Nickname, - [vcard_search].lnickname = @Lnickname, - [vcard_search].bday = @Bday, - [vcard_search].lbday = @Lbday, - [vcard_search].ctry = @Ctry, - [vcard_search].lctry = @Lctry, - [vcard_search].locality = @Locality, - [vcard_search].llocality = @Llocality, - [vcard_search].email = @Email, - [vcard_search].lemail = @Lemail, - [vcard_search].orgname = @Orgname, - [vcard_search].lorgname = @Lorgname, - [vcard_search].orgunit = @Orgunit, - [vcard_search].lorgunit = @Lorgunit - WHERE vcard_search.lusername = @LUsername; - END - ELSE - BEGIN - INSERT INTO [vcard] - ( [vcard].username, - [vcard].vcard - ) - VALUES - ( @lUsername, - @Vcard - ); - - INSERT INTO [vcard_search] - ( - [vcard_search].username , - [vcard_search].lusername , - [vcard_search].fn , - [vcard_search].lfn , - [vcard_search].family , - [vcard_search].lfamily , - [vcard_search].given , - [vcard_search].lgiven , - [vcard_search].middle , - [vcard_search].lmiddle , - [vcard_search].nickname, - [vcard_search].lnickname, - [vcard_search].bday, - [vcard_search].lbday, - [vcard_search].ctry, - [vcard_search].lctry, - [vcard_search].locality, - [vcard_search].llocality, - [vcard_search].email, - [vcard_search].lemail, - [vcard_search].orgname, - [vcard_search].lorgname, - [vcard_search].orgunit, - [vcard_search].lorgunit - ) - VALUES - ( - @Username, - @Lusername, - @Fn, - @Lfn, - @Family, - @Lfamily, - @Given, - @Lgiven, - @Middle, - @Lmiddle, - @Nickname, - @Lnickname, - @Bday, - @Lbday, - @Ctry, - @Lctry, - @Locality, - @Llocality, - @Email, - @Lemail, - @Orgname, - @Lorgname, - @Orgunit, - @Lorgunit - ) - END -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[get_vcard] **/ -/** Retrive the user's vCard **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[get_vcard] - @Username varchar(250) -AS -BEGIN - SELECT vcard.vcard as vcard - FROM vcard WITH (NOLOCK) - WHERE username=@Username; -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[get_default_privacy_list]**/ -/** Retrive the user's default privacy list **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[get_default_privacy_list] - @Username varchar(250) -AS -BEGIN - SELECT list.name - FROM privacy_default_list list WITH (NOLOCK) - WHERE list.username=@Username -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[get_privacy_list_names] **/ -/** Retrive the user's default privacy list names **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[get_privacy_list_names] - @username varchar(250) -AS -BEGIN - SELECT list.name - FROM privacy_list list WITH (NOLOCK) - WHERE list.username=@Username -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[get_privacy_list_id] **/ -/** **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[get_privacy_list_id] - @username varchar(250), - @SName varchar(250) -AS -BEGIN - SELECT id FROM privacy_list - WHERE username=@Username - AND name=@SName -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[get_privacy_list_data] **/ -/** **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[get_privacy_list_data] - @username varchar(250), - @SName varchar(250) -AS -BEGIN - SELECT l_data.t, - l_data.value, - l_data.action, - l_data.ord, - l_data.match_all, - l_data.match_iq, - l_data.match_message, - l_data.match_presence_in, - l_data.match_presence_out - FROM privacy_list_data l_data (NOLOCK) - WHERE l_data.id = (SELECT list.id - FROM privacy_list list - WHERE list.username=@username - AND list.name=@SName) - ORDER BY l_data.ord -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[get_privacy_list_data_by_id]**/ -/** **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[get_privacy_list_data_by_id] - @Id bigint -AS -BEGIN - SELECT l_data.t, - l_data.value, - l_data.action, - l_data.ord, - l_data.match_all, - l_data.match_iq, - l_data.match_message, - l_data.match_presence_in, - l_data.match_presence_out - FROM privacy_list_data l_data (NOLOCK) - WHERE l_data.id=@ID - ORDER BY l_data.ord -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[set_default_privacy_list]**/ -/** **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[set_default_privacy_list] - @username varchar(250), - @Sname varchar(250) -AS -BEGIN - IF EXISTS (SELECT username FROM privacy_default_list with (nolock) WHERE privacy_default_list.username = @Username AND privacy_default_list.name = @Sname) - BEGIN - UPDATE [privacy_default_list] - SET [privacy_default_list].username = @Username, - [privacy_default_list].name = @Sname - WHERE privacy_default_list.username = @Username - END - ELSE - BEGIN - INSERT INTO [privacy_default_list] - ( [privacy_default_list].username, - [privacy_default_list].name - ) - VALUES - ( @Username, - @SName - ) - END -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[unset_default_privacy_list]**/ -/** **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[unset_default_privacy_list] - @username varchar(250) -AS -BEGIN - DELETE - FROM privacy_default_list - WHERE privacy_default_list.username=@username -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[remove_privacy_list] **/ -/** **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[remove_privacy_list] - @username varchar(250), - @SName varchar(250) -AS -BEGIN - DELETE - FROM privacy_list - WHERE privacy_list.username=@username - AND privacy_list.name=@SName -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[add_privacy_list] **/ -/** **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[add_privacy_list] - @username varchar(250), - @SName varchar(250) -AS -BEGIN - INSERT INTO privacy_list(username, name) - VALUES (@username, @SName) -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[set_privacy_list] **/ -/** **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[set_privacy_list] - @Id bigint, - @t char(1), - @value text, - @action char(1), - @ord numeric, - @match_all bit, - @match_iq bit, - @match_message bit, - @match_presence_in bit, - @match_presence_out bit -AS -BEGIN - insert into privacy_list_data ( - id, - t, - value, - action, - ord, - match_all, - match_iq, - match_message, - match_presence_in, - match_presence_out - ) - values (@Id, - @t, - @value, - @action, - @ord, - @match_all, - @match_iq, - @match_message, - @match_presence_in, - @match_presence_out - ) - -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[del_privacy_list_by_id] **/ -/** **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[del_privacy_list_by_id] - @Id bigint -AS -BEGIN - DELETE FROM privacy_list_data - WHERE privacy_list_data.id=@Id -END -GO - -/******************************************************************/ -/****** Object: StoredProcedure [dbo].[del_privacy_lists] **/ -/** **/ -/******************************************************************/ -CREATE PROCEDURE [dbo].[del_privacy_lists] - @Server varchar(250), - @username varchar(250) -AS -BEGIN - DELETE FROM privacy_list WHERE username=@username - DELETE FROM privacy_list_data WHERE convert(varchar,value)=@username+'@'+@Server - DELETE FROM privacy_default_list WHERE username=@username -END -GO diff --git a/sql/mysql.new.sql b/sql/mysql.new.sql new file mode 100644 index 000000000..cf818ad3d --- /dev/null +++ b/sql/mysql.new.sql @@ -0,0 +1,509 @@ +-- +-- ejabberd, Copyright (C) 2002-2025 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. +-- + +CREATE TABLE users ( + username varchar(191) NOT NULL, + type smallint NOT NULL, + server_host varchar(191) NOT NULL, + password text NOT NULL, + serverkey varchar(128) NOT NULL DEFAULT '', + salt varchar(128) NOT NULL DEFAULT '', + iterationcount integer NOT NULL DEFAULT 0, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (server_host(191), username, type) +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- Add support for SCRAM auth to a database created before ejabberd 16.03: +-- ALTER TABLE users ADD COLUMN serverkey varchar(64) NOT NULL DEFAULT ''; +-- ALTER TABLE users ADD COLUMN salt varchar(64) NOT NULL DEFAULT ''; +-- ALTER TABLE users ADD COLUMN iterationcount integer NOT NULL DEFAULT 0; + +CREATE TABLE last ( + username varchar(191) NOT NULL, + server_host varchar(191) NOT NULL, + seconds text NOT NULL, + state text NOT NULL, + PRIMARY KEY (server_host(191), username) +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + + +CREATE TABLE rosterusers ( + username varchar(191) NOT NULL, + server_host varchar(191) NOT NULL, + jid varchar(191) NOT NULL, + nick text NOT NULL, + subscription character(1) NOT NULL, + ask character(1) NOT NULL, + askmessage text NOT NULL, + server character(1) NOT NULL, + subscribe text NOT NULL, + type text, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE UNIQUE INDEX i_rosteru_sh_user_jid ON rosterusers(server_host(191), username(75), jid(75)); +CREATE INDEX i_rosteru_sh_jid ON rosterusers(server_host(191), jid); + +CREATE TABLE rostergroups ( + username varchar(191) NOT NULL, + server_host varchar(191) NOT NULL, + jid varchar(191) NOT NULL, + grp text NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE INDEX i_rosterg_sh_user_jid ON rostergroups(server_host(191), username(75), jid(75)); + +CREATE TABLE sr_group ( + name varchar(191) NOT NULL, + server_host varchar(191) NOT NULL, + opts text NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (server_host(191), name) +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE UNIQUE INDEX i_sr_group_sh_name ON sr_group(server_host(191), name); + +CREATE TABLE sr_user ( + jid varchar(191) NOT NULL, + server_host varchar(191) NOT NULL, + grp varchar(191) NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (server_host(191), jid, grp) +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE UNIQUE INDEX i_sr_user_sh_jid_grp ON sr_user(server_host(191), jid, grp); +CREATE INDEX i_sr_user_sh_grp ON sr_user(server_host(191), grp); + +CREATE TABLE spool ( + username varchar(191) NOT NULL, + server_host varchar(191) NOT NULL, + xml mediumtext NOT NULL, + seq BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE INDEX i_spool_sh_username USING BTREE ON spool(server_host(191), username); +CREATE INDEX i_spool_created_at USING BTREE ON spool(created_at); + +CREATE TABLE archive ( + username varchar(191) NOT NULL, + server_host varchar(191) NOT NULL, + timestamp BIGINT UNSIGNED NOT NULL, + peer varchar(191) NOT NULL, + bare_peer varchar(191) NOT NULL, + xml mediumtext NOT NULL, + txt mediumtext, + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, + kind varchar(10), + nick varchar(191), + origin_id varchar(191), + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE FULLTEXT INDEX i_text ON archive(txt); +CREATE INDEX i_archive_sh_username_timestamp USING BTREE ON archive(server_host(191), username(191), timestamp); +CREATE INDEX i_archive_sh_username_peer USING BTREE ON archive(server_host(191), username(191), peer(191)); +CREATE INDEX i_archive_sh_username_bare_peer USING BTREE ON archive(server_host(191), username(191), bare_peer(191)); +CREATE INDEX i_archive_sh_timestamp USING BTREE ON archive(server_host(191), timestamp); +CREATE INDEX i_archive_sh_username_origin_id USING BTREE ON archive(server_host(191), username(191), origin_id(191)); + +-- To update 'archive' from ejabberd <= 23.10: +-- ALTER TABLE archive ADD COLUMN origin_id varchar(191) NOT NULL DEFAULT ''; +-- ALTER TABLE archive ALTER COLUMN origin_id DROP DEFAULT; +-- CREATE INDEX i_archive_sh_username_origin_id USING BTREE ON archive(server_host(191), username(191), origin_id(191)); + +CREATE TABLE archive_prefs ( + username varchar(191) NOT NULL, + server_host varchar(191) NOT NULL, + def text NOT NULL, + always text NOT NULL, + never text NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (server_host(191), username) +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE TABLE vcard ( + username varchar(191) NOT NULL, + server_host varchar(191) NOT NULL, + vcard mediumtext NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (server_host(191), username) +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE TABLE vcard_search ( + username varchar(191) NOT NULL, + lusername varchar(191) NOT NULL, + server_host varchar(191) NOT NULL, + fn text NOT NULL, + lfn varchar(191) NOT NULL, + family text NOT NULL, + lfamily varchar(191) NOT NULL, + given text NOT NULL, + lgiven varchar(191) NOT NULL, + middle text NOT NULL, + lmiddle varchar(191) NOT NULL, + nickname text NOT NULL, + lnickname varchar(191) NOT NULL, + bday text NOT NULL, + lbday varchar(191) NOT NULL, + ctry text NOT NULL, + lctry varchar(191) NOT NULL, + locality text NOT NULL, + llocality varchar(191) NOT NULL, + email text NOT NULL, + lemail varchar(191) NOT NULL, + orgname text NOT NULL, + lorgname varchar(191) NOT NULL, + orgunit text NOT NULL, + lorgunit varchar(191) NOT NULL, + PRIMARY KEY (server_host(191), lusername) +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE INDEX i_vcard_search_sh_lfn ON vcard_search(server_host(191), lfn); +CREATE INDEX i_vcard_search_sh_lfamily ON vcard_search(server_host(191), lfamily); +CREATE INDEX i_vcard_search_sh_lgiven ON vcard_search(server_host(191), lgiven); +CREATE INDEX i_vcard_search_sh_lmiddle ON vcard_search(server_host(191), lmiddle); +CREATE INDEX i_vcard_search_sh_lnickname ON vcard_search(server_host(191), lnickname); +CREATE INDEX i_vcard_search_sh_lbday ON vcard_search(server_host(191), lbday); +CREATE INDEX i_vcard_search_sh_lctry ON vcard_search(server_host(191), lctry); +CREATE INDEX i_vcard_search_sh_llocality ON vcard_search(server_host(191), llocality); +CREATE INDEX i_vcard_search_sh_lemail ON vcard_search(server_host(191), lemail); +CREATE INDEX i_vcard_search_sh_lorgname ON vcard_search(server_host(191), lorgname); +CREATE INDEX i_vcard_search_sh_lorgunit ON vcard_search(server_host(191), lorgunit); + +CREATE TABLE privacy_default_list ( + username varchar(191) NOT NULL, + server_host varchar(191) NOT NULL, + name varchar(191) NOT NULL, + PRIMARY KEY (server_host(191), username) +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE TABLE privacy_list ( + username varchar(191) NOT NULL, + server_host varchar(191) NOT NULL, + name varchar(191) NOT NULL, + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE UNIQUE INDEX i_privacy_list_sh_username_name USING BTREE ON privacy_list (server_host(191), username(75), name(75)); + +CREATE TABLE privacy_list_data ( + id bigint, + t character(1) NOT NULL, + value text NOT NULL, + action character(1) NOT NULL, + ord NUMERIC NOT NULL, + match_all boolean NOT NULL, + match_iq boolean NOT NULL, + match_message boolean NOT NULL, + match_presence_in boolean NOT NULL, + match_presence_out boolean NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE INDEX i_privacy_list_data_id ON privacy_list_data(id); + +CREATE TABLE private_storage ( + username varchar(191) NOT NULL, + server_host varchar(191) NOT NULL, + namespace varchar(191) NOT NULL, + data text NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE UNIQUE INDEX i_private_storage_sh_sername_namespace USING BTREE ON private_storage(server_host(191), username, namespace); + +-- Not tested in mysql +CREATE TABLE roster_version ( + username varchar(191) NOT NULL, + server_host varchar(191) NOT NULL, + version text NOT NULL, + PRIMARY KEY (server_host(191), username) +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- To update from 1.x: +-- ALTER TABLE rosterusers ADD COLUMN askmessage text AFTER ask; +-- UPDATE rosterusers SET askmessage = ''; +-- ALTER TABLE rosterusers ALTER COLUMN askmessage SET NOT NULL; + +CREATE TABLE pubsub_node ( + host text NOT NULL, + node text NOT NULL, + parent VARCHAR(191) NOT NULL DEFAULT '', + plugin text NOT NULL, + nodeid bigint auto_increment primary key +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +CREATE INDEX i_pubsub_node_parent ON pubsub_node(parent(120)); +CREATE UNIQUE INDEX i_pubsub_node_tuple ON pubsub_node(host(71), node(120)); + +CREATE TABLE pubsub_node_option ( + nodeid bigint, + name text NOT NULL, + val text NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +CREATE INDEX i_pubsub_node_option_nodeid ON pubsub_node_option(nodeid); +ALTER TABLE `pubsub_node_option` ADD FOREIGN KEY (`nodeid`) REFERENCES `pubsub_node` (`nodeid`) ON DELETE CASCADE; + +CREATE TABLE pubsub_node_owner ( + nodeid bigint, + owner text NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +CREATE INDEX i_pubsub_node_owner_nodeid ON pubsub_node_owner(nodeid); +ALTER TABLE `pubsub_node_owner` ADD FOREIGN KEY (`nodeid`) REFERENCES `pubsub_node` (`nodeid`) ON DELETE CASCADE; + +CREATE TABLE pubsub_state ( + nodeid bigint, + jid text NOT NULL, + affiliation character(1), + subscriptions VARCHAR(191) NOT NULL DEFAULT '', + stateid bigint auto_increment primary key +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +CREATE INDEX i_pubsub_state_jid ON pubsub_state(jid(60)); +CREATE UNIQUE INDEX i_pubsub_state_tuple ON pubsub_state(nodeid, jid(60)); +ALTER TABLE `pubsub_state` ADD FOREIGN KEY (`nodeid`) REFERENCES `pubsub_node` (`nodeid`) ON DELETE CASCADE; + +CREATE TABLE pubsub_item ( + nodeid bigint, + itemid text NOT NULL, + publisher text NOT NULL, + creation varchar(32) NOT NULL, + modification varchar(32) NOT NULL, + payload mediumtext NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +CREATE INDEX i_pubsub_item_itemid ON pubsub_item(itemid(36)); +CREATE UNIQUE INDEX i_pubsub_item_tuple ON pubsub_item(nodeid, itemid(36)); +ALTER TABLE `pubsub_item` ADD FOREIGN KEY (`nodeid`) REFERENCES `pubsub_node` (`nodeid`) ON DELETE CASCADE; + +CREATE TABLE pubsub_subscription_opt ( + subid text NOT NULL, + opt_name varchar(32), + opt_value text NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +CREATE UNIQUE INDEX i_pubsub_subscription_opt ON pubsub_subscription_opt(subid(32), opt_name(32)); + +CREATE TABLE muc_room ( + name text NOT NULL, + host text NOT NULL, + server_host varchar(191) NOT NULL, + opts mediumtext NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE UNIQUE INDEX i_muc_room_name_host USING BTREE ON muc_room(name(75), host(75)); +CREATE INDEX i_muc_room_host_created_at ON muc_room(host(75), created_at); + +CREATE TABLE muc_registered ( + jid text NOT NULL, + host text NOT NULL, + server_host varchar(191) NOT NULL, + nick text NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE INDEX i_muc_registered_nick USING BTREE ON muc_registered(nick(75)); +CREATE UNIQUE INDEX i_muc_registered_jid_host USING BTREE ON muc_registered(jid(75), host(75)); + +CREATE TABLE muc_online_room ( + name text NOT NULL, + host text NOT NULL, + server_host varchar(191) NOT NULL, + node text NOT NULL, + pid text NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE UNIQUE INDEX i_muc_online_room_name_host USING BTREE ON muc_online_room(name(75), host(75)); + +CREATE TABLE muc_online_users ( + username text NOT NULL, + server text NOT NULL, + resource text NOT NULL, + name text NOT NULL, + host text NOT NULL, + server_host varchar(191) NOT NULL, + node text NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE UNIQUE INDEX i_muc_online_users USING BTREE ON muc_online_users(username(75), server(75), resource(75), name(75), host(75)); + +CREATE TABLE muc_room_subscribers ( + room varchar(191) NOT NULL, + host varchar(191) NOT NULL, + jid varchar(191) NOT NULL, + nick text NOT NULL, + nodes text NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY i_muc_room_subscribers_host_room_jid (host, room, jid) +) 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, + server_host varchar(191) NOT NULL, + xml text, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (server_host(191), username) +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE TABLE caps_features ( + node varchar(191) NOT NULL, + subnode varchar(191) NOT NULL, + feature text, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE INDEX i_caps_features_node_subnode ON caps_features(node(75), subnode(75)); + +CREATE TABLE sm ( + usec bigint NOT NULL, + pid text NOT NULL, + node text NOT NULL, + username varchar(191) NOT NULL, + server_host varchar(191) NOT NULL, + resource varchar(191) NOT NULL, + priority text NOT NULL, + info text NOT NULL, + PRIMARY KEY (usec, pid(75)) +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE INDEX i_sm_node ON sm(node(75)); +CREATE INDEX i_sm_sh_username ON sm(server_host(191), username); + +CREATE TABLE oauth_token ( + token varchar(191) NOT NULL PRIMARY KEY, + jid text NOT NULL, + scope text NOT NULL, + expire bigint NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE TABLE oauth_client ( + client_id varchar(191) NOT NULL PRIMARY KEY, + client_name text NOT NULL, + grant_type text NOT NULL, + options text NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE TABLE route ( + domain text NOT NULL, + server_host varchar(191) NOT NULL, + node text NOT NULL, + pid text NOT NULL, + local_hint text NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE UNIQUE INDEX i_route ON route(domain(75), server_host(75), node(75), pid(75)); + +CREATE TABLE bosh ( + sid text NOT NULL, + node text NOT NULL, + pid text NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE UNIQUE INDEX i_bosh_sid ON bosh(sid(75)); + +CREATE TABLE proxy65 ( + sid text NOT NULL, + pid_t text NOT NULL, + pid_i text NOT NULL, + node_t text NOT NULL, + node_i text NOT NULL, + jid_i text NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE UNIQUE INDEX i_proxy65_sid ON proxy65 (sid(191)); +CREATE INDEX i_proxy65_jid ON proxy65 (jid_i(191)); + +CREATE TABLE push_session ( + username text NOT NULL, + server_host varchar(191) NOT NULL, + timestamp bigint NOT NULL, + service text NOT NULL, + node text NOT NULL, + xml text NOT NULL, + PRIMARY KEY (server_host(191), username(191), timestamp) +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE UNIQUE INDEX i_push_session_susn ON push_session (server_host(191), username(191), service(191), node(191)); +CREATE INDEX i_push_session_sh_username_timestamp ON push_session (server_host, username(191), timestamp); + +CREATE TABLE mix_channel ( + channel text NOT NULL, + service text NOT NULL, + username text NOT NULL, + domain text NOT NULL, + jid text NOT NULL, + hidden boolean NOT NULL, + hmac_key text NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE UNIQUE INDEX i_mix_channel ON mix_channel (channel(191), service(191)); +CREATE INDEX i_mix_channel_serv ON mix_channel (service(191)); + +CREATE TABLE mix_participant ( + channel text NOT NULL, + service text NOT NULL, + username text NOT NULL, + domain text NOT NULL, + jid text NOT NULL, + id text NOT NULL, + nick text NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE UNIQUE INDEX i_mix_participant ON mix_participant (channel(191), service(191), username(191), domain(191)); + +CREATE TABLE mix_subscription ( + channel text NOT NULL, + service text NOT NULL, + username text NOT NULL, + domain text NOT NULL, + node text NOT NULL, + jid text NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE UNIQUE INDEX i_mix_subscription ON mix_subscription (channel(153), service(153), username(153), domain(153), node(153)); +CREATE INDEX i_mix_subscription_chan_serv_node ON mix_subscription (channel(191), service(191), node(191)); + +CREATE TABLE mix_pam ( + username text NOT NULL, + server_host varchar(191) NOT NULL, + channel text NOT NULL, + service text NOT NULL, + id text NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE UNIQUE INDEX i_mix_pam ON mix_pam (username(191), server_host(191), channel(191), service(191)); + +CREATE TABLE mqtt_pub ( + username varchar(191) NOT NULL, + server_host varchar(191) NOT NULL, + resource varchar(191) NOT NULL, + topic text NOT NULL, + qos tinyint NOT NULL, + payload blob NOT NULL, + payload_format tinyint NOT NULL, + content_type text NOT NULL, + response_topic text NOT NULL, + correlation_data blob NOT NULL, + user_properties blob NOT NULL, + expiry int unsigned NOT NULL, + UNIQUE KEY i_mqtt_topic_server (topic(191), server_host) +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; diff --git a/sql/mysql.old-to-new.sql b/sql/mysql.old-to-new.sql new file mode 100644 index 000000000..a58a90a46 --- /dev/null +++ b/sql/mysql.old-to-new.sql @@ -0,0 +1,157 @@ +SET @DEFAULT_HOST = ''; -- Please fill with name of your current host name + +BEGIN; +DELIMITER ## +CREATE PROCEDURE update_server_host(IN DEFAULT_HOST TEXT) +BEGIN + START TRANSACTION; + SET FOREIGN_KEY_CHECKS = 0; + SET @DEFAULT_HOST = DEFAULT_HOST; + IF DEFAULT_HOST = '' THEN + SELECT 'Please fill @DEFAULT_HOST parameter' as Error; + ROLLBACK; + ELSE + ALTER TABLE `push_session` DROP INDEX `i_push_usn`; + ALTER TABLE `push_session` DROP INDEX `i_push_ut`; + ALTER TABLE `push_session` ADD COLUMN `server_host` VARCHAR (191) NOT NULL DEFAULT @DEFAULT_HOST AFTER `username`; + ALTER TABLE `push_session` ALTER COLUMN `server_host` DROP DEFAULT; + ALTER TABLE `push_session` ADD PRIMARY KEY (`server_host`, `username`(191), `timestamp`); + ALTER TABLE `push_session` ADD UNIQUE INDEX `i_push_session_susn` (`server_host`, `username`(191), `service`(191), `node`(191)); + ALTER TABLE `push_session` ADD INDEX `i_push_session_sh_username_timestamp` (`server_host`, `username`(191), `timestamp`); + ALTER TABLE `roster_version` DROP PRIMARY KEY; + ALTER TABLE `roster_version` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `username`; + ALTER TABLE `roster_version` ALTER COLUMN `server_host` DROP DEFAULT; + ALTER TABLE `roster_version` ADD PRIMARY KEY (`server_host`, `username`); + ALTER TABLE `muc_online_room` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `host`; + ALTER TABLE `muc_online_room` ALTER COLUMN `server_host` DROP DEFAULT; + ALTER TABLE `motd` DROP PRIMARY KEY; + ALTER TABLE `motd` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `username`; + ALTER TABLE `motd` ALTER COLUMN `server_host` DROP DEFAULT; + ALTER TABLE `motd` ADD PRIMARY KEY (`server_host`, `username`); + ALTER TABLE `rosterusers` DROP INDEX `i_rosteru_username`; + ALTER TABLE `rosterusers` DROP INDEX `i_rosteru_jid`; + ALTER TABLE `rosterusers` DROP INDEX `i_rosteru_user_jid`; + ALTER TABLE `rosterusers` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `username`; + ALTER TABLE `rosterusers` ALTER COLUMN `server_host` DROP DEFAULT; + ALTER TABLE `rosterusers` ADD UNIQUE INDEX `i_rosteru_sh_user_jid` (`server_host`, `username`(75), `jid`(75)); + ALTER TABLE `rosterusers` ADD INDEX `i_rosteru_sh_jid` (`server_host`, `jid`); + ALTER TABLE `private_storage` DROP INDEX `i_private_storage_username_namespace`; + ALTER TABLE `private_storage` DROP INDEX `i_private_storage_username`; + ALTER TABLE `private_storage` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `username`; + ALTER TABLE `private_storage` ALTER COLUMN `server_host` DROP DEFAULT; + ALTER TABLE `private_storage` ADD PRIMARY KEY (`server_host`, `username`, `namespace`); + ALTER TABLE `mqtt_pub` DROP INDEX `i_mqtt_topic`; + ALTER TABLE `mqtt_pub` ADD COLUMN `server_host` VARCHAR (191) NOT NULL DEFAULT @DEFAULT_HOST AFTER `username`; + ALTER TABLE `mqtt_pub` ALTER COLUMN `server_host` DROP DEFAULT; + ALTER TABLE `mqtt_pub` ADD UNIQUE INDEX `i_mqtt_topic_server` (`topic`(191), `server_host`); + ALTER TABLE `vcard_search` DROP PRIMARY KEY; + ALTER TABLE `vcard_search` DROP INDEX `i_vcard_search_lgiven`; + ALTER TABLE `vcard_search` DROP INDEX `i_vcard_search_lmiddle`; + ALTER TABLE `vcard_search` DROP INDEX `i_vcard_search_lnickname`; + ALTER TABLE `vcard_search` DROP INDEX `i_vcard_search_lbday`; + ALTER TABLE `vcard_search` DROP INDEX `i_vcard_search_lctry`; + ALTER TABLE `vcard_search` DROP INDEX `i_vcard_search_lfn`; + ALTER TABLE `vcard_search` DROP INDEX `i_vcard_search_lemail`; + ALTER TABLE `vcard_search` DROP INDEX `i_vcard_search_lorgunit`; + ALTER TABLE `vcard_search` DROP INDEX `i_vcard_search_llocality`; + ALTER TABLE `vcard_search` DROP INDEX `i_vcard_search_lorgname`; + ALTER TABLE `vcard_search` DROP INDEX `i_vcard_search_lfamily`; + ALTER TABLE `vcard_search` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `lusername`; + ALTER TABLE `vcard_search` ALTER COLUMN `server_host` DROP DEFAULT; + ALTER TABLE `vcard_search` ADD INDEX `i_vcard_search_sh_lfn` (`server_host`, `lfn`); + ALTER TABLE `vcard_search` ADD INDEX `i_vcard_search_sh_llocality` (`server_host`, `llocality`); + ALTER TABLE `vcard_search` ADD PRIMARY KEY (`server_host`, `lusername`); + ALTER TABLE `vcard_search` ADD INDEX `i_vcard_search_sh_lnickname` (`server_host`, `lnickname`); + ALTER TABLE `vcard_search` ADD INDEX `i_vcard_search_sh_lctry` (`server_host`, `lctry`); + ALTER TABLE `vcard_search` ADD INDEX `i_vcard_search_sh_lgiven` (`server_host`, `lgiven`); + ALTER TABLE `vcard_search` ADD INDEX `i_vcard_search_sh_lmiddle` (`server_host`, `lmiddle`); + ALTER TABLE `vcard_search` ADD INDEX `i_vcard_search_sh_lorgname` (`server_host`, `lorgname`); + ALTER TABLE `vcard_search` ADD INDEX `i_vcard_search_sh_lfamily` (`server_host`, `lfamily`); + ALTER TABLE `vcard_search` ADD INDEX `i_vcard_search_sh_lbday` (`server_host`, `lbday`); + ALTER TABLE `vcard_search` ADD INDEX `i_vcard_search_sh_lemail` (`server_host`, `lemail`); + ALTER TABLE `vcard_search` ADD INDEX `i_vcard_search_sh_lorgunit` (`server_host`, `lorgunit`); + ALTER TABLE `last` DROP PRIMARY KEY; + ALTER TABLE `last` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `username`; + ALTER TABLE `last` ALTER COLUMN `server_host` DROP DEFAULT; + ALTER TABLE `last` ADD PRIMARY KEY (`server_host`, `username`); + ALTER TABLE `sr_group` DROP INDEX `i_sr_group_name`; + 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 `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; + ALTER TABLE `sm` DROP INDEX `i_node`; + ALTER TABLE `sm` DROP INDEX `i_username`; + ALTER TABLE `sm` DROP INDEX `i_sid`; + ALTER TABLE `sm` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `username`; + ALTER TABLE `sm` ALTER COLUMN `server_host` DROP DEFAULT; + ALTER TABLE `sm` ADD INDEX `i_sm_node` (`node`(75)); + ALTER TABLE `sm` ADD INDEX `i_sm_sh_username` (`server_host`, `username`); + ALTER TABLE `sm` ADD PRIMARY KEY (`usec`, `pid`(75)); + ALTER TABLE `privacy_list` DROP INDEX `i_privacy_list_username_name`; + ALTER TABLE `privacy_list` DROP INDEX `i_privacy_list_username`; + ALTER TABLE `privacy_list` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `username`; + ALTER TABLE `privacy_list` ALTER COLUMN `server_host` DROP DEFAULT; + ALTER TABLE `privacy_list` ADD UNIQUE INDEX `i_privacy_list_sh_username_name` USING BTREE (`server_host`, `username`(75), `name`(75)); + ALTER TABLE `sr_user` DROP INDEX `i_sr_user_jid`; + ALTER TABLE `sr_user` DROP INDEX `i_sr_user_grp`; + 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_grp` (`server_host`, `grp`); + ALTER TABLE `muc_online_users` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `host`; + ALTER TABLE `muc_online_users` ALTER COLUMN `server_host` DROP DEFAULT; + ALTER TABLE `vcard` DROP PRIMARY KEY; + ALTER TABLE `vcard` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `username`; + ALTER TABLE `vcard` ALTER COLUMN `server_host` DROP DEFAULT; + ALTER TABLE `vcard` ADD PRIMARY KEY (`server_host`, `username`); + ALTER TABLE `archive_prefs` DROP PRIMARY KEY; + ALTER TABLE `archive_prefs` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `username`; + ALTER TABLE `archive_prefs` ALTER COLUMN `server_host` DROP DEFAULT; + ALTER TABLE `archive_prefs` ADD PRIMARY KEY (`server_host`, `username`); + ALTER TABLE `mix_pam` DROP INDEX `i_mix_pam`; + ALTER TABLE `mix_pam` DROP INDEX `i_mix_pam_u`; + ALTER TABLE `mix_pam` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `username`; + ALTER TABLE `mix_pam` ALTER COLUMN `server_host` DROP DEFAULT; + ALTER TABLE `mix_pam` ADD UNIQUE INDEX `i_mix_pam` (`username`(191), `server_host`, `channel`(191), `service`(191)); + ALTER TABLE `route` CHANGE COLUMN `server_host` `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL; + ALTER TABLE `users` DROP PRIMARY KEY; + ALTER TABLE `users` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `username`; + ALTER TABLE `users` ALTER COLUMN `server_host` DROP DEFAULT; + ALTER TABLE `users` ADD PRIMARY KEY (`server_host`, `username`); + ALTER TABLE `privacy_default_list` DROP PRIMARY KEY; + ALTER TABLE `privacy_default_list` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `username`; + ALTER TABLE `privacy_default_list` ALTER COLUMN `server_host` DROP DEFAULT; + ALTER TABLE `privacy_default_list` ADD PRIMARY KEY (`server_host`, `username`); + ALTER TABLE `rostergroups` DROP INDEX `pk_rosterg_user_jid`; + ALTER TABLE `rostergroups` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `username`; + ALTER TABLE `rostergroups` ALTER COLUMN `server_host` DROP DEFAULT; + ALTER TABLE `rostergroups` ADD INDEX `i_rosterg_sh_user_jid` (`server_host`, `username`(75), `jid`(75)); + ALTER TABLE `muc_room` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `host`; + ALTER TABLE `muc_room` ALTER COLUMN `server_host` DROP DEFAULT; + ALTER TABLE `spool` DROP INDEX `i_despool`; + ALTER TABLE `spool` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `username`; + ALTER TABLE `spool` ALTER COLUMN `server_host` DROP DEFAULT; + ALTER TABLE `spool` ADD INDEX `i_spool_sh_username` USING BTREE (`server_host`, `username`); + ALTER TABLE `archive` DROP INDEX `i_username_timestamp`; + ALTER TABLE `archive` DROP INDEX `i_username_peer`; + ALTER TABLE `archive` DROP INDEX `i_username_bare_peer`; + ALTER TABLE `archive` DROP INDEX `i_timestamp`; + ALTER TABLE `archive` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `username`; + ALTER TABLE `archive` ALTER COLUMN `server_host` DROP DEFAULT; + ALTER TABLE `archive` ADD INDEX `i_archive_sh_username_bare_peer` USING BTREE (`server_host`, `username`, `bare_peer`); + ALTER TABLE `archive` ADD INDEX `i_archive_sh_timestamp` USING BTREE (`server_host`, `timestamp`); + ALTER TABLE `archive` ADD INDEX `i_archive_sh_username_timestamp` USING BTREE (`server_host`, `username`, `timestamp`); + ALTER TABLE `archive` ADD INDEX `i_archive_sh_username_peer` USING BTREE (`server_host`, `username`, `peer`); + END IF; + SET FOREIGN_KEY_CHECKS = 1; +END; +## +DELIMITER ; + +CALL update_server_host(@DEFAULT_HOST); + +DROP PROCEDURE update_server_host; + +COMMIT; diff --git a/sql/mysql.sql b/sql/mysql.sql index 7f96905c0..630c4a557 100644 --- a/sql/mysql.sql +++ b/sql/mysql.sql @@ -1,5 +1,5 @@ -- --- ejabberd, Copyright (C) 2002-2015 ProcessOne +-- ejabberd, Copyright (C) 2002-2025 ProcessOne -- -- This program is free software; you can redistribute it and/or -- modify it under the terms of the GNU General Public License as @@ -17,22 +17,31 @@ -- CREATE TABLE users ( - username varchar(250) PRIMARY KEY, + username varchar(191) NOT NULL, + type smallint NOT NULL, password text NOT NULL, - created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP -) ENGINE=InnoDB CHARACTER SET utf8; + serverkey varchar(128) NOT NULL DEFAULT '', + salt varchar(128) NOT NULL DEFAULT '', + iterationcount integer NOT NULL DEFAULT 0, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (username, type) +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +-- Add support for SCRAM auth to a database created before ejabberd 16.03: +-- ALTER TABLE users ADD COLUMN serverkey varchar(64) NOT NULL DEFAULT ''; +-- ALTER TABLE users ADD COLUMN salt varchar(64) NOT NULL DEFAULT ''; +-- ALTER TABLE users ADD COLUMN iterationcount integer NOT NULL DEFAULT 0; CREATE TABLE last ( - username varchar(250) PRIMARY KEY, + username varchar(191) PRIMARY KEY, seconds text NOT NULL, state text NOT NULl -) ENGINE=InnoDB CHARACTER SET utf8; +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE TABLE rosterusers ( - username varchar(250) NOT NULL, - jid varchar(250) NOT NULL, + username varchar(191) NOT NULL, + jid varchar(191) NOT NULL, nick text NOT NULL, subscription character(1) NOT NULL, ask character(1) NOT NULL, @@ -41,83 +50,112 @@ CREATE TABLE rosterusers ( subscribe text NOT NULL, type text, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP -) ENGINE=InnoDB CHARACTER SET utf8; +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE UNIQUE INDEX i_rosteru_user_jid ON rosterusers(username(75), jid(75)); -CREATE INDEX i_rosteru_username ON rosterusers(username); CREATE INDEX i_rosteru_jid ON rosterusers(jid); CREATE TABLE rostergroups ( - username varchar(250) NOT NULL, - jid varchar(250) NOT NULL, + username varchar(191) NOT NULL, + jid varchar(191) NOT NULL, grp text NOT NULL -) ENGINE=InnoDB CHARACTER SET utf8; +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE INDEX pk_rosterg_user_jid ON rostergroups(username(75), jid(75)); CREATE TABLE sr_group ( - name varchar(250) NOT NULL, + name varchar(191) NOT NULL, opts text NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP -) ENGINE=InnoDB CHARACTER SET utf8; +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE UNIQUE INDEX i_sr_group_name ON sr_group(name); CREATE TABLE sr_user ( - jid varchar(250) NOT NULL, - grp varchar(250) NOT NULL, + jid varchar(191) NOT NULL, + grp varchar(191) NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP -) ENGINE=InnoDB CHARACTER SET utf8; +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE UNIQUE INDEX i_sr_user_jid_group ON sr_user(jid(75), grp(75)); -CREATE INDEX i_sr_user_jid ON sr_user(jid); CREATE INDEX i_sr_user_grp ON sr_user(grp); CREATE TABLE spool ( - username varchar(250) NOT NULL, - xml text NOT NULL, + username varchar(191) NOT NULL, + xml mediumtext NOT NULL, seq BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP -) ENGINE=InnoDB CHARACTER SET utf8; +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE INDEX i_despool USING BTREE ON spool(username); +CREATE INDEX i_spool_created_at USING BTREE ON spool(created_at); + +CREATE TABLE archive ( + username varchar(191) NOT NULL, + timestamp BIGINT UNSIGNED NOT NULL, + peer varchar(191) NOT NULL, + bare_peer varchar(191) NOT NULL, + xml mediumtext NOT NULL, + txt mediumtext, + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, + kind varchar(10), + nick varchar(191), + origin_id varchar(191), + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE FULLTEXT INDEX i_text ON archive(txt); +CREATE INDEX i_username_timestamp USING BTREE ON archive(username(191), timestamp); +CREATE INDEX i_username_peer USING BTREE ON archive(username(191), peer(191)); +CREATE INDEX i_username_bare_peer USING BTREE ON archive(username(191), bare_peer(191)); +CREATE INDEX i_timestamp USING BTREE ON archive(timestamp); +CREATE INDEX i_archive_username_origin_id USING BTREE ON archive(username(191), origin_id(191)); + +-- To update 'archive' from ejabberd <= 23.10: +-- ALTER TABLE archive ADD COLUMN origin_id varchar(191) NOT NULL DEFAULT ''; +-- ALTER TABLE archive ALTER COLUMN origin_id DROP DEFAULT; +-- CREATE INDEX i_archive_username_origin_id USING BTREE ON archive(username(191), origin_id(191)); + +CREATE TABLE archive_prefs ( + username varchar(191) NOT NULL PRIMARY KEY, + def text NOT NULL, + always text NOT NULL, + never text NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE TABLE vcard ( - username varchar(250) PRIMARY KEY, + username varchar(191) PRIMARY KEY, vcard mediumtext NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP -) ENGINE=InnoDB CHARACTER SET utf8; - -CREATE TABLE vcard_xupdate ( - username varchar(250) PRIMARY KEY, - hash text NOT NULL, - created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP -) ENGINE=InnoDB CHARACTER SET utf8; +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE TABLE vcard_search ( - username varchar(250) NOT NULL, - lusername varchar(250) PRIMARY KEY, + username varchar(191) NOT NULL, + lusername varchar(191) PRIMARY KEY, fn text NOT NULL, - lfn varchar(250) NOT NULL, + lfn varchar(191) NOT NULL, family text NOT NULL, - lfamily varchar(250) NOT NULL, + lfamily varchar(191) NOT NULL, given text NOT NULL, - lgiven varchar(250) NOT NULL, + lgiven varchar(191) NOT NULL, middle text NOT NULL, - lmiddle varchar(250) NOT NULL, + lmiddle varchar(191) NOT NULL, nickname text NOT NULL, - lnickname varchar(250) NOT NULL, + lnickname varchar(191) NOT NULL, bday text NOT NULL, - lbday varchar(250) NOT NULL, + lbday varchar(191) NOT NULL, ctry text NOT NULL, - lctry varchar(250) NOT NULL, + lctry varchar(191) NOT NULL, locality text NOT NULL, - llocality varchar(250) NOT NULL, + llocality varchar(191) NOT NULL, email text NOT NULL, - lemail varchar(250) NOT NULL, + lemail varchar(191) NOT NULL, orgname text NOT NULL, - lorgname varchar(250) NOT NULL, + lorgname varchar(191) NOT NULL, orgunit text NOT NULL, - lorgunit varchar(250) NOT NULL -) ENGINE=InnoDB CHARACTER SET utf8; + lorgunit varchar(191) NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE INDEX i_vcard_search_lfn ON vcard_search(lfn); CREATE INDEX i_vcard_search_lfamily ON vcard_search(lfamily); @@ -132,18 +170,17 @@ CREATE INDEX i_vcard_search_lorgname ON vcard_search(lorgname); CREATE INDEX i_vcard_search_lorgunit ON vcard_search(lorgunit); CREATE TABLE privacy_default_list ( - username varchar(250) PRIMARY KEY, - name varchar(250) NOT NULL -) ENGINE=InnoDB CHARACTER SET utf8; + username varchar(191) PRIMARY KEY, + name varchar(191) NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE TABLE privacy_list ( - username varchar(250) NOT NULL, - name varchar(250) NOT NULL, + username varchar(191) NOT NULL, + name varchar(191) NOT NULL, id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP -) ENGINE=InnoDB CHARACTER SET utf8; +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -CREATE INDEX i_privacy_list_username USING BTREE ON privacy_list(username); CREATE UNIQUE INDEX i_privacy_list_username_name USING BTREE ON privacy_list (username(75), name(75)); CREATE TABLE privacy_list_data ( @@ -157,24 +194,24 @@ CREATE TABLE privacy_list_data ( match_message boolean NOT NULL, match_presence_in boolean NOT NULL, match_presence_out boolean NOT NULL -) ENGINE=InnoDB CHARACTER SET utf8; +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + CREATE INDEX i_privacy_list_data_id ON privacy_list_data(id); CREATE TABLE private_storage ( - username varchar(250) NOT NULL, - namespace varchar(250) NOT NULL, + username varchar(191) NOT NULL, + namespace varchar(191) NOT NULL, data text NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP -) ENGINE=InnoDB CHARACTER SET utf8; +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -CREATE INDEX i_private_storage_username USING BTREE ON private_storage(username); CREATE UNIQUE INDEX i_private_storage_username_namespace USING BTREE ON private_storage(username(75), namespace(75)); -- Not tested in mysql CREATE TABLE roster_version ( - username varchar(250) PRIMARY KEY, + username varchar(191) PRIMARY KEY, version text NOT NULL -) ENGINE=InnoDB CHARACTER SET utf8; +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- To update from 1.x: -- ALTER TABLE rosterusers ADD COLUMN askmessage text AFTER ask; @@ -182,99 +219,257 @@ CREATE TABLE roster_version ( -- ALTER TABLE rosterusers ALTER COLUMN askmessage SET NOT NULL; CREATE TABLE pubsub_node ( - host text, - node text, - parent text, - type text, + host text NOT NULL, + node text NOT NULL, + parent VARCHAR(191) NOT NULL DEFAULT '', + plugin text NOT NULL, nodeid bigint auto_increment primary key -) ENGINE=InnoDB CHARACTER SET utf8; +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE INDEX i_pubsub_node_parent ON pubsub_node(parent(120)); -CREATE UNIQUE INDEX i_pubsub_node_tuple ON pubsub_node(host(20), node(120)); +CREATE UNIQUE INDEX i_pubsub_node_tuple ON pubsub_node(host(71), node(120)); CREATE TABLE pubsub_node_option ( nodeid bigint, - name text, - val text -) ENGINE=InnoDB CHARACTER SET utf8; + name text NOT NULL, + val text NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE INDEX i_pubsub_node_option_nodeid ON pubsub_node_option(nodeid); ALTER TABLE `pubsub_node_option` ADD FOREIGN KEY (`nodeid`) REFERENCES `pubsub_node` (`nodeid`) ON DELETE CASCADE; CREATE TABLE pubsub_node_owner ( nodeid bigint, - owner text -) ENGINE=InnoDB CHARACTER SET utf8; + owner text NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE INDEX i_pubsub_node_owner_nodeid ON pubsub_node_owner(nodeid); ALTER TABLE `pubsub_node_owner` ADD FOREIGN KEY (`nodeid`) REFERENCES `pubsub_node` (`nodeid`) ON DELETE CASCADE; CREATE TABLE pubsub_state ( nodeid bigint, - jid text, + jid text NOT NULL, affiliation character(1), - subscriptions text, + subscriptions VARCHAR(191) NOT NULL DEFAULT '', stateid bigint auto_increment primary key -) ENGINE=InnoDB CHARACTER SET utf8; +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE INDEX i_pubsub_state_jid ON pubsub_state(jid(60)); CREATE UNIQUE INDEX i_pubsub_state_tuple ON pubsub_state(nodeid, jid(60)); ALTER TABLE `pubsub_state` ADD FOREIGN KEY (`nodeid`) REFERENCES `pubsub_node` (`nodeid`) ON DELETE CASCADE; CREATE TABLE pubsub_item ( nodeid bigint, - itemid text, - publisher text, - creation text, - modification text, - payload text -) ENGINE=InnoDB CHARACTER SET utf8; + itemid text NOT NULL, + publisher text NOT NULL, + creation varchar(32) NOT NULL, + modification varchar(32) NOT NULL, + payload mediumtext NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE INDEX i_pubsub_item_itemid ON pubsub_item(itemid(36)); CREATE UNIQUE INDEX i_pubsub_item_tuple ON pubsub_item(nodeid, itemid(36)); ALTER TABLE `pubsub_item` ADD FOREIGN KEY (`nodeid`) REFERENCES `pubsub_node` (`nodeid`) ON DELETE CASCADE; CREATE TABLE pubsub_subscription_opt ( - subid text, + subid text NOT NULL, opt_name varchar(32), - opt_value text -); + opt_value text NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE UNIQUE INDEX i_pubsub_subscription_opt ON pubsub_subscription_opt(subid(32), opt_name(32)); CREATE TABLE muc_room ( name text NOT NULL, host text NOT NULL, - opts text NOT NULL, + opts mediumtext NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP -) ENGINE=InnoDB CHARACTER SET utf8; +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE UNIQUE INDEX i_muc_room_name_host USING BTREE ON muc_room(name(75), host(75)); +CREATE INDEX i_muc_room_host_created_at ON muc_room(host(75), created_at); CREATE TABLE muc_registered ( jid text NOT NULL, host text NOT NULL, nick text NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP -) ENGINE=InnoDB CHARACTER SET utf8; +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE INDEX i_muc_registered_nick USING BTREE ON muc_registered(nick(75)); CREATE UNIQUE INDEX i_muc_registered_jid_host USING BTREE ON muc_registered(jid(75), host(75)); -CREATE TABLE irc_custom ( - jid text NOT NULL, +CREATE TABLE muc_online_room ( + name text NOT NULL, host text NOT NULL, - data text NOT NULL, - created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP -) ENGINE=InnoDB CHARACTER SET utf8; + node text NOT NULL, + pid text NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -CREATE UNIQUE INDEX i_irc_custom_jid_host USING BTREE ON irc_custom(jid(75), host(75)); +CREATE UNIQUE INDEX i_muc_online_room_name_host USING BTREE ON muc_online_room(name(75), host(75)); + +CREATE TABLE muc_online_users ( + username text NOT NULL, + server text NOT NULL, + resource text NOT NULL, + name text NOT NULL, + host text NOT NULL, + node text NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE UNIQUE INDEX i_muc_online_users USING BTREE ON muc_online_users(username(75), server(75), resource(75), name(75), host(75)); + +CREATE TABLE muc_room_subscribers ( + room varchar(191) NOT NULL, + host varchar(191) NOT NULL, + jid varchar(191) NOT NULL, + nick text NOT NULL, + nodes text NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY i_muc_room_subscribers_host_room_jid (host, room, jid) +) 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(250) PRIMARY KEY, + username varchar(191) PRIMARY KEY, xml text, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP -) ENGINE=InnoDB CHARACTER SET utf8; +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE TABLE caps_features ( - node varchar(250) NOT NULL, - subnode varchar(250) NOT NULL, + node varchar(191) NOT NULL, + subnode varchar(191) NOT NULL, feature text, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP -) ENGINE=InnoDB CHARACTER SET utf8; +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE INDEX i_caps_features_node_subnode ON caps_features(node(75), subnode(75)); + +CREATE TABLE sm ( + usec bigint NOT NULL, + pid text NOT NULL, + node text NOT NULL, + username varchar(191) NOT NULL, + resource varchar(191) NOT NULL, + priority text NOT NULL, + info text NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE UNIQUE INDEX i_sid ON sm(usec, pid(75)); +CREATE INDEX i_node ON sm(node(75)); +CREATE INDEX i_username ON sm(username); + +CREATE TABLE oauth_token ( + token varchar(191) NOT NULL PRIMARY KEY, + jid text NOT NULL, + scope text NOT NULL, + expire bigint NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE TABLE oauth_client ( + client_id varchar(191) NOT NULL PRIMARY KEY, + client_name text NOT NULL, + grant_type text NOT NULL, + options text NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE TABLE route ( + domain text NOT NULL, + server_host text NOT NULL, + node text NOT NULL, + pid text NOT NULL, + local_hint text NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE UNIQUE INDEX i_route ON route(domain(75), server_host(75), node(75), pid(75)); + +CREATE TABLE bosh ( + sid text NOT NULL, + node text NOT NULL, + pid text NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE UNIQUE INDEX i_bosh_sid ON bosh(sid(75)); + +CREATE TABLE proxy65 ( + sid text NOT NULL, + pid_t text NOT NULL, + pid_i text NOT NULL, + node_t text NOT NULL, + node_i text NOT NULL, + jid_i text NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE UNIQUE INDEX i_proxy65_sid ON proxy65 (sid(191)); +CREATE INDEX i_proxy65_jid ON proxy65 (jid_i(191)); + +CREATE TABLE push_session ( + username text NOT NULL, + timestamp bigint NOT NULL, + service text NOT NULL, + node text NOT NULL, + xml text NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE UNIQUE INDEX i_push_usn ON push_session (username(191), service(191), node(191)); +CREATE UNIQUE INDEX i_push_ut ON push_session (username(191), timestamp); + +CREATE TABLE mix_channel ( + channel text NOT NULL, + service text NOT NULL, + username text NOT NULL, + domain text NOT NULL, + jid text NOT NULL, + hidden boolean NOT NULL, + hmac_key text NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE UNIQUE INDEX i_mix_channel ON mix_channel (channel(191), service(191)); +CREATE INDEX i_mix_channel_serv ON mix_channel (service(191)); + +CREATE TABLE mix_participant ( + channel text NOT NULL, + service text NOT NULL, + username text NOT NULL, + domain text NOT NULL, + jid text NOT NULL, + id text NOT NULL, + nick text NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE UNIQUE INDEX i_mix_participant ON mix_participant (channel(191), service(191), username(191), domain(191)); + +CREATE TABLE mix_subscription ( + channel text NOT NULL, + service text NOT NULL, + username text NOT NULL, + domain text NOT NULL, + node text NOT NULL, + jid text NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE UNIQUE INDEX i_mix_subscription ON mix_subscription (channel(153), service(153), username(153), domain(153), node(153)); +CREATE INDEX i_mix_subscription_chan_serv_node ON mix_subscription (channel(191), service(191), node(191)); + +CREATE TABLE mix_pam ( + username text NOT NULL, + channel text NOT NULL, + service text NOT NULL, + id text NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE UNIQUE INDEX i_mix_pam ON mix_pam (username(191), channel(191), service(191)); + +CREATE TABLE mqtt_pub ( + username varchar(191) NOT NULL, + resource varchar(191) NOT NULL, + topic text NOT NULL, + qos tinyint NOT NULL, + payload blob NOT NULL, + payload_format tinyint NOT NULL, + content_type text NOT NULL, + response_topic text NOT NULL, + correlation_data blob NOT NULL, + user_properties blob NOT NULL, + expiry int unsigned NOT NULL, + UNIQUE KEY i_mqtt_topic (topic(191)) +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; diff --git a/sql/pg.new.sql b/sql/pg.new.sql new file mode 100644 index 000000000..1e59ec571 --- /dev/null +++ b/sql/pg.new.sql @@ -0,0 +1,664 @@ +-- +-- ejabberd, Copyright (C) 2002-2025 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. +-- + +-- To update from the old schema, replace with the host's domain: + +-- ALTER TABLE users ADD COLUMN server_host text NOT NULL DEFAULT ''; +-- ALTER TABLE users DROP CONSTRAINT users_pkey; +-- ALTER TABLE users ADD PRIMARY KEY (server_host, username, "type"); +-- ALTER TABLE users ALTER COLUMN server_host DROP DEFAULT; + +-- ALTER TABLE last ADD COLUMN server_host text NOT NULL DEFAULT ''; +-- ALTER TABLE last DROP CONSTRAINT last_pkey; +-- ALTER TABLE last ADD PRIMARY KEY (server_host, username); +-- ALTER TABLE last ALTER COLUMN server_host DROP DEFAULT; + +-- ALTER TABLE rosterusers ADD COLUMN server_host text NOT NULL DEFAULT ''; +-- DROP INDEX i_rosteru_user_jid; +-- DROP INDEX i_rosteru_jid; +-- CREATE UNIQUE INDEX i_rosteru_sh_user_jid ON rosterusers USING btree (server_host, username, jid); +-- CREATE INDEX i_rosteru_sh_jid ON rosterusers USING btree (server_host, jid); +-- ALTER TABLE rosterusers ALTER COLUMN server_host DROP DEFAULT; + +-- ALTER TABLE rostergroups ADD COLUMN server_host text NOT NULL DEFAULT ''; +-- DROP INDEX pk_rosterg_user_jid; +-- CREATE INDEX i_rosterg_sh_user_jid ON rostergroups USING btree (server_host, username, jid); +-- ALTER TABLE rostergroups ALTER COLUMN server_host DROP DEFAULT; + +-- ALTER TABLE sr_group ADD COLUMN server_host text NOT NULL DEFAULT ''; +-- DROP INDEX i_sr_group_name; +-- ALTER TABLE sr_group ADD PRIMARY KEY (server_host, name); +-- CREATE UNIQUE INDEX i_sr_group_sh_name ON sr_group USING btree (server_host, name); +-- ALTER TABLE sr_group ALTER COLUMN server_host DROP DEFAULT; + +-- ALTER TABLE sr_user ADD COLUMN server_host text NOT NULL DEFAULT ''; +-- DROP INDEX i_sr_user_jid_grp; +-- DROP INDEX i_sr_user_grp; +-- ALTER TABLE sr_user ADD PRIMARY KEY (server_host, jid, grp); +-- CREATE INDEX i_sr_user_sh_grp ON sr_user USING btree (server_host, grp); +-- ALTER TABLE sr_user ALTER COLUMN server_host DROP DEFAULT; + +-- ALTER TABLE spool ADD COLUMN server_host text NOT NULL DEFAULT ''; +-- DROP INDEX i_despool; +-- CREATE INDEX i_spool_sh_username ON spool USING btree (server_host, username); +-- ALTER TABLE spool ALTER COLUMN server_host DROP DEFAULT; + +-- ALTER TABLE archive ADD COLUMN server_host text NOT NULL DEFAULT ''; +-- DROP INDEX i_username_timestamp; +-- DROP INDEX i_username_peer; +-- DROP INDEX i_username_bare_peer; +-- DROP INDEX i_timestamp; +-- CREATE INDEX i_archive_sh_username_timestamp ON archive USING btree (server_host, username, timestamp); +-- CREATE INDEX i_archive_sh_username_peer ON archive USING btree (server_host, username, peer); +-- CREATE INDEX i_archive_sh_username_bare_peer ON archive USING btree (server_host, username, bare_peer); +-- CREATE INDEX i_archive_sh_timestamp ON archive USING btree (server_host, timestamp); +-- ALTER TABLE archive ALTER COLUMN server_host DROP DEFAULT; + +-- ALTER TABLE archive_prefs ADD COLUMN server_host text NOT NULL DEFAULT ''; +-- ALTER TABLE archive_prefs DROP CONSTRAINT archive_prefs_pkey; +-- ALTER TABLE archive_prefs ADD PRIMARY KEY (server_host, username); +-- ALTER TABLE archive_prefs ALTER COLUMN server_host DROP DEFAULT; + +-- ALTER TABLE vcard ADD COLUMN server_host text NOT NULL DEFAULT ''; +-- ALTER TABLE vcard DROP CONSTRAINT vcard_pkey; +-- ALTER TABLE vcard ADD PRIMARY KEY (server_host, username); +-- ALTER TABLE vcard ALTER COLUMN server_host DROP DEFAULT; + +-- ALTER TABLE vcard_search ADD COLUMN server_host text NOT NULL DEFAULT ''; +-- ALTER TABLE vcard_search DROP CONSTRAINT vcard_search_pkey; +-- DROP INDEX i_vcard_search_lfn; +-- DROP INDEX i_vcard_search_lfamily; +-- DROP INDEX i_vcard_search_lgiven; +-- DROP INDEX i_vcard_search_lmiddle; +-- DROP INDEX i_vcard_search_lnickname; +-- DROP INDEX i_vcard_search_lbday; +-- DROP INDEX i_vcard_search_lctry; +-- DROP INDEX i_vcard_search_llocality; +-- DROP INDEX i_vcard_search_lemail; +-- DROP INDEX i_vcard_search_lorgname; +-- DROP INDEX i_vcard_search_lorgunit; +-- ALTER TABLE vcard_search ADD PRIMARY KEY (server_host, lusername); +-- CREATE INDEX i_vcard_search_sh_lfn ON vcard_search(server_host, lfn); +-- CREATE INDEX i_vcard_search_sh_lfamily ON vcard_search(server_host, lfamily); +-- CREATE INDEX i_vcard_search_sh_lgiven ON vcard_search(server_host, lgiven); +-- CREATE INDEX i_vcard_search_sh_lmiddle ON vcard_search(server_host, lmiddle); +-- CREATE INDEX i_vcard_search_sh_lnickname ON vcard_search(server_host, lnickname); +-- CREATE INDEX i_vcard_search_sh_lbday ON vcard_search(server_host, lbday); +-- CREATE INDEX i_vcard_search_sh_lctry ON vcard_search(server_host, lctry); +-- CREATE INDEX i_vcard_search_sh_llocality ON vcard_search(server_host, llocality); +-- CREATE INDEX i_vcard_search_sh_lemail ON vcard_search(server_host, lemail); +-- CREATE INDEX i_vcard_search_sh_lorgname ON vcard_search(server_host, lorgname); +-- CREATE INDEX i_vcard_search_sh_lorgunit ON vcard_search(server_host, lorgunit); +-- ALTER TABLE vcard_search ALTER COLUMN server_host DROP DEFAULT; + +-- ALTER TABLE privacy_default_list ADD COLUMN server_host text NOT NULL DEFAULT ''; +-- ALTER TABLE privacy_default_list DROP CONSTRAINT privacy_default_list_pkey; +-- ALTER TABLE privacy_default_list ADD PRIMARY KEY (server_host, username); +-- ALTER TABLE privacy_default_list ALTER COLUMN server_host DROP DEFAULT; + +-- ALTER TABLE privacy_list ADD COLUMN server_host text NOT NULL DEFAULT ''; +-- DROP INDEX i_privacy_list_username_name; +-- CREATE UNIQUE INDEX i_privacy_list_sh_username_name ON privacy_list USING btree (server_host, username, name); +-- ALTER TABLE privacy_list ALTER COLUMN server_host DROP DEFAULT; + +-- ALTER TABLE private_storage ADD COLUMN server_host text NOT NULL DEFAULT ''; +-- DROP INDEX i_private_storage_username_namespace; +-- ALTER TABLE private_storage ADD PRIMARY KEY (server_host, username, namespace); +-- ALTER TABLE private_storage ALTER COLUMN server_host DROP DEFAULT; + +-- ALTER TABLE roster_version ADD COLUMN server_host text NOT NULL DEFAULT ''; +-- ALTER TABLE roster_version DROP CONSTRAINT roster_version_pkey; +-- ALTER TABLE roster_version ADD PRIMARY KEY (server_host, username); +-- ALTER TABLE roster_version ALTER COLUMN server_host DROP DEFAULT; + +-- ALTER TABLE muc_room ADD COLUMN server_host text NOT NULL DEFAULT ''; +-- ALTER TABLE muc_room ALTER COLUMN server_host DROP DEFAULT; + +-- ALTER TABLE muc_registered ADD COLUMN server_host text NOT NULL DEFAULT ''; +-- ALTER TABLE muc_registered ALTER COLUMN server_host DROP DEFAULT; + +-- ALTER TABLE muc_online_room ADD COLUMN server_host text NOT NULL DEFAULT ''; +-- ALTER TABLE muc_online_room ALTER COLUMN server_host DROP DEFAULT; + +-- ALTER TABLE muc_online_users ADD COLUMN server_host text NOT NULL DEFAULT ''; +-- ALTER TABLE muc_online_users ALTER COLUMN server_host DROP DEFAULT; + +-- ALTER TABLE motd ADD COLUMN server_host text NOT NULL DEFAULT ''; +-- ALTER TABLE motd DROP CONSTRAINT motd_pkey; +-- ALTER TABLE motd ADD PRIMARY KEY (server_host, username); +-- ALTER TABLE motd ALTER COLUMN server_host DROP DEFAULT; + +-- ALTER TABLE sm ADD COLUMN server_host text NOT NULL DEFAULT ''; +-- DROP INDEX i_sm_sid; +-- DROP INDEX i_sm_username; +-- ALTER TABLE sm ADD PRIMARY KEY (usec, pid); +-- CREATE INDEX i_sm_sh_username ON sm USING btree (server_host, username); +-- ALTER TABLE sm ALTER COLUMN server_host DROP DEFAULT; + +-- ALTER TABLE push_session ADD COLUMN server_host text NOT NULL DEFAULT ''; +-- DROP INDEX i_push_usn; +-- DROP INDEX i_push_ut; +-- ALTER TABLE push_session ADD PRIMARY KEY (server_host, username, timestamp); +-- CREATE UNIQUE INDEX i_push_session_susn ON push_session USING btree (server_host, username, service, node); +-- CREATE INDEX i_push_session_sh_username_timestamp ON push_session USING btree (server_host, username, timestamp); +-- ALTER TABLE push_session ALTER COLUMN server_host DROP DEFAULT; + +-- ALTER TABLE mix_pam ADD COLUMN server_host text NOT NULL DEFAULT ''; +-- DROP INDEX i_mix_pam; +-- CREATE UNIQUE INDEX i_mix_pam ON mix_pam (username, server_host, channel, service); +-- ALTER TABLE mix_pam ALTER COLUMN server_host DROP DEFAULT; + +-- ALTER TABLE mqtt_pub ADD COLUMN server_host text NOT NULL DEFAULT ''; +-- DROP INDEX i_mqtt_topic; +-- CREATE UNIQUE INDEX i_mqtt_topic_server ON mqtt_pub (topic, server_host); +-- ALTER TABLE mqtt_pub ALTER COLUMN server_host DROP DEFAULT; + + +CREATE TABLE users ( + username text NOT NULL, + server_host text NOT NULL, + "type" smallint NOT NULL, + "password" text NOT NULL, + serverkey text NOT NULL DEFAULT '', + salt text NOT NULL DEFAULT '', + iterationcount integer NOT NULL DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT now(), + PRIMARY KEY (server_host, username, "type") +); + +-- Add support for SCRAM auth to a database created before ejabberd 16.03: +-- ALTER TABLE users ADD COLUMN serverkey text NOT NULL DEFAULT ''; +-- ALTER TABLE users ADD COLUMN salt text NOT NULL DEFAULT ''; +-- ALTER TABLE users ADD COLUMN iterationcount integer NOT NULL DEFAULT 0; + +CREATE TABLE last ( + username text NOT NULL, + server_host text NOT NULL, + seconds text NOT NULL, + state text NOT NULL, + PRIMARY KEY (server_host, username) +); + + +CREATE TABLE rosterusers ( + username text NOT NULL, + server_host text NOT NULL, + jid text NOT NULL, + nick text NOT NULL, + subscription character(1) NOT NULL, + ask character(1) NOT NULL, + askmessage text NOT NULL, + server character(1) NOT NULL, + subscribe text NOT NULL, + "type" text, + created_at TIMESTAMP NOT NULL DEFAULT now() +); + +CREATE UNIQUE INDEX i_rosteru_sh_user_jid ON rosterusers USING btree (server_host, username, jid); +CREATE INDEX i_rosteru_sh_jid ON rosterusers USING btree (server_host, jid); + + +CREATE TABLE rostergroups ( + username text NOT NULL, + server_host text NOT NULL, + jid text NOT NULL, + grp text NOT NULL +); + +CREATE INDEX i_rosterg_sh_user_jid ON rostergroups USING btree (server_host, username, jid); + +CREATE TABLE sr_group ( + name text NOT NULL, + server_host text NOT NULL, + opts text NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT now() +); + +CREATE UNIQUE INDEX i_sr_group_sh_name ON sr_group USING btree (server_host, name); + +CREATE TABLE sr_user ( + jid text NOT NULL, + server_host text NOT NULL, + grp text NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT now() +); + +CREATE UNIQUE INDEX i_sr_user_sh_jid_grp ON sr_user USING btree (server_host, jid, grp); +CREATE INDEX i_sr_user_sh_grp ON sr_user USING btree (server_host, grp); + +CREATE TABLE spool ( + username text NOT NULL, + server_host text NOT NULL, + xml text NOT NULL, + seq BIGSERIAL, + created_at TIMESTAMP NOT NULL DEFAULT now() +); + +CREATE INDEX i_spool_sh_username ON spool USING btree (server_host, username); + +CREATE TABLE archive ( + username text NOT NULL, + server_host text NOT NULL, + timestamp BIGINT NOT NULL, + peer text NOT NULL, + bare_peer text NOT NULL, + xml text NOT NULL, + txt text, + id BIGSERIAL, + kind text, + nick text, + origin_id text, + created_at TIMESTAMP NOT NULL DEFAULT now() +); + +CREATE INDEX i_archive_sh_username_timestamp ON archive USING btree (server_host, username, timestamp); +CREATE INDEX i_archive_sh_username_peer ON archive USING btree (server_host, username, peer); +CREATE INDEX i_archive_sh_username_bare_peer ON archive USING btree (server_host, username, bare_peer); +CREATE INDEX i_archive_sh_timestamp ON archive USING btree (server_host, timestamp); +CREATE INDEX i_archive_sh_username_origin_id ON archive USING btree (server_host, username, origin_id); + +-- To update 'archive' from ejabberd <= 23.10: +-- ALTER TABLE archive ADD COLUMN origin_id text NOT NULL DEFAULT ''; +-- ALTER TABLE archive ALTER COLUMN origin_id DROP DEFAULT; +-- CREATE INDEX i_archive_sh_username_origin_id ON archive USING btree (server_host, username, origin_id); + +CREATE TABLE archive_prefs ( + username text NOT NULL, + server_host text NOT NULL, + def text NOT NULL, + always text NOT NULL, + never text NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT now(), + PRIMARY KEY (server_host, username) +); + +CREATE TABLE vcard ( + username text NOT NULL, + server_host text NOT NULL, + vcard text NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT now(), + PRIMARY KEY (server_host, username) +); + +CREATE TABLE vcard_search ( + username text NOT NULL, + lusername text NOT NULL, + server_host text NOT NULL, + fn text NOT NULL, + lfn text NOT NULL, + "family" text NOT NULL, + lfamily text NOT NULL, + given text NOT NULL, + lgiven text NOT NULL, + middle text NOT NULL, + lmiddle text NOT NULL, + nickname text NOT NULL, + lnickname text NOT NULL, + bday text NOT NULL, + lbday text NOT NULL, + ctry text NOT NULL, + lctry text NOT NULL, + locality text NOT NULL, + llocality text NOT NULL, + email text NOT NULL, + lemail text NOT NULL, + orgname text NOT NULL, + lorgname text NOT NULL, + orgunit text NOT NULL, + lorgunit text NOT NULL, + PRIMARY KEY (server_host, lusername) +); + +CREATE INDEX i_vcard_search_sh_lfn ON vcard_search(server_host, lfn); +CREATE INDEX i_vcard_search_sh_lfamily ON vcard_search(server_host, lfamily); +CREATE INDEX i_vcard_search_sh_lgiven ON vcard_search(server_host, lgiven); +CREATE INDEX i_vcard_search_sh_lmiddle ON vcard_search(server_host, lmiddle); +CREATE INDEX i_vcard_search_sh_lnickname ON vcard_search(server_host, lnickname); +CREATE INDEX i_vcard_search_sh_lbday ON vcard_search(server_host, lbday); +CREATE INDEX i_vcard_search_sh_lctry ON vcard_search(server_host, lctry); +CREATE INDEX i_vcard_search_sh_llocality ON vcard_search(server_host, llocality); +CREATE INDEX i_vcard_search_sh_lemail ON vcard_search(server_host, lemail); +CREATE INDEX i_vcard_search_sh_lorgname ON vcard_search(server_host, lorgname); +CREATE INDEX i_vcard_search_sh_lorgunit ON vcard_search(server_host, lorgunit); + +CREATE TABLE privacy_default_list ( + username text NOT NULL, + server_host text NOT NULL, + name text NOT NULL, + PRIMARY KEY (server_host, username) +); + +CREATE TABLE privacy_list ( + username text NOT NULL, + server_host text NOT NULL, + name text NOT NULL, + id BIGSERIAL UNIQUE, + created_at TIMESTAMP NOT NULL DEFAULT now() +); + +CREATE UNIQUE INDEX i_privacy_list_sh_username_name ON privacy_list USING btree (server_host, username, name); + +CREATE TABLE privacy_list_data ( + id bigint REFERENCES privacy_list(id) ON DELETE CASCADE, + t character(1) NOT NULL, + value text NOT NULL, + action character(1) NOT NULL, + ord NUMERIC NOT NULL, + match_all boolean NOT NULL, + match_iq boolean NOT NULL, + match_message boolean NOT NULL, + match_presence_in boolean NOT NULL, + match_presence_out boolean NOT NULL +); + +CREATE INDEX i_privacy_list_data_id ON privacy_list_data USING btree (id); + +CREATE TABLE private_storage ( + username text NOT NULL, + server_host text NOT NULL, + namespace text NOT NULL, + data text NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT now() +); + +CREATE UNIQUE INDEX i_private_storage_sh_username_namespace ON private_storage USING btree (server_host, username, namespace); + +CREATE TABLE roster_version ( + username text NOT NULL, + server_host text NOT NULL, + version text NOT NULL, + PRIMARY KEY (server_host, username) +); + +-- To update from 0.9.8: +-- CREATE SEQUENCE spool_seq_seq; +-- ALTER TABLE spool ADD COLUMN seq integer; +-- ALTER TABLE spool ALTER COLUMN seq SET DEFAULT nextval('spool_seq_seq'); +-- UPDATE spool SET seq = DEFAULT; +-- ALTER TABLE spool ALTER COLUMN seq SET NOT NULL; + +-- To update from 1.x: +-- ALTER TABLE rosterusers ADD COLUMN askmessage text; +-- UPDATE rosterusers SET askmessage = ''; +-- ALTER TABLE rosterusers ALTER COLUMN askmessage SET NOT NULL; + +CREATE TABLE pubsub_node ( + host text NOT NULL, + node text NOT NULL, + parent text NOT NULL DEFAULT '', + plugin text NOT NULL, + nodeid BIGSERIAL UNIQUE +); +CREATE INDEX i_pubsub_node_parent ON pubsub_node USING btree (parent); +CREATE UNIQUE INDEX i_pubsub_node_tuple ON pubsub_node USING btree (host, node); + +CREATE TABLE pubsub_node_option ( + nodeid bigint REFERENCES pubsub_node(nodeid) ON DELETE CASCADE, + name text NOT NULL, + val text NOT NULL +); +CREATE INDEX i_pubsub_node_option_nodeid ON pubsub_node_option USING btree (nodeid); + +CREATE TABLE pubsub_node_owner ( + nodeid bigint REFERENCES pubsub_node(nodeid) ON DELETE CASCADE, + owner text NOT NULL +); +CREATE INDEX i_pubsub_node_owner_nodeid ON pubsub_node_owner USING btree (nodeid); + +CREATE TABLE pubsub_state ( + nodeid bigint REFERENCES pubsub_node(nodeid) ON DELETE CASCADE, + jid text NOT NULL, + affiliation character(1), + subscriptions text NOT NULL DEFAULT '', + stateid BIGSERIAL UNIQUE +); +CREATE INDEX i_pubsub_state_jid ON pubsub_state USING btree (jid); +CREATE UNIQUE INDEX i_pubsub_state_tuple ON pubsub_state USING btree (nodeid, jid); + +CREATE TABLE pubsub_item ( + nodeid bigint REFERENCES pubsub_node(nodeid) ON DELETE CASCADE, + itemid text NOT NULL, + publisher text NOT NULL, + creation varchar(32) NOT NULL, + modification varchar(32) NOT NULL, + payload text NOT NULL DEFAULT '' +); +CREATE INDEX i_pubsub_item_itemid ON pubsub_item USING btree (itemid); +CREATE UNIQUE INDEX i_pubsub_item_tuple ON pubsub_item USING btree (nodeid, itemid); + +CREATE TABLE pubsub_subscription_opt ( + subid text NOT NULL, + opt_name varchar(32), + opt_value text NOT NULL +); +CREATE UNIQUE INDEX i_pubsub_subscription_opt ON pubsub_subscription_opt USING btree (subid, opt_name); + +CREATE TABLE muc_room ( + name text NOT NULL, + host text NOT NULL, + server_host text NOT NULL, + opts text NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT now() +); + +CREATE UNIQUE INDEX i_muc_room_name_host ON muc_room USING btree (name, host); +CREATE INDEX i_muc_room_host_created_at ON muc_room USING btree (host, created_at); + +CREATE TABLE muc_registered ( + jid text NOT NULL, + host text NOT NULL, + server_host text NOT NULL, + nick text NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT now() +); + +CREATE INDEX i_muc_registered_nick ON muc_registered USING btree (nick); +CREATE UNIQUE INDEX i_muc_registered_jid_host ON muc_registered USING btree (jid, host); + +CREATE TABLE muc_online_room ( + name text NOT NULL, + host text NOT NULL, + server_host text NOT NULL, + node text NOT NULL, + pid text NOT NULL +); + +CREATE UNIQUE INDEX i_muc_online_room_name_host ON muc_online_room USING btree (name, host); + +CREATE TABLE muc_online_users ( + username text NOT NULL, + server text NOT NULL, + resource text NOT NULL, + name text NOT NULL, + host text NOT NULL, + server_host text NOT NULL, + node text NOT NULL +); + +CREATE UNIQUE INDEX i_muc_online_users ON muc_online_users USING btree (username, server, resource, name, host); + +CREATE TABLE muc_room_subscribers ( + room text NOT NULL, + host text NOT NULL, + jid text NOT NULL, + nick text NOT NULL, + nodes text NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT now() +); + +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 ( + username text NOT NULL, + server_host text NOT NULL, + xml text, + created_at TIMESTAMP NOT NULL DEFAULT now(), + PRIMARY KEY (server_host, username) +); + +CREATE TABLE caps_features ( + node text NOT NULL, + subnode text NOT NULL, + feature text, + created_at TIMESTAMP NOT NULL DEFAULT now() +); + +CREATE INDEX i_caps_features_node_subnode ON caps_features USING btree (node, subnode); + +CREATE TABLE sm ( + usec bigint NOT NULL, + pid text NOT NULL, + node text NOT NULL, + username text NOT NULL, + server_host text NOT NULL, + resource text NOT NULL, + priority text NOT NULL, + info text NOT NULL, + PRIMARY KEY (usec, pid) +); + +CREATE INDEX i_sm_node ON sm USING btree (node); +CREATE INDEX i_sm_sh_username ON sm USING btree (server_host, username); + +CREATE TABLE oauth_token ( + token text NOT NULL, + jid text NOT NULL, + scope text NOT NULL, + expire bigint NOT NULL +); + +CREATE UNIQUE INDEX i_oauth_token_token ON oauth_token USING btree (token); + +CREATE TABLE oauth_client ( + client_id text PRIMARY KEY, + client_name text NOT NULL, + grant_type text NOT NULL, + options text NOT NULL +); + +CREATE TABLE route ( + domain text NOT NULL, + server_host text NOT NULL, + node text NOT NULL, + pid text NOT NULL, + local_hint text NOT NULL +); + +CREATE UNIQUE INDEX i_route ON route USING btree (domain, server_host, node, pid); + +CREATE TABLE bosh ( + sid text NOT NULL, + node text NOT NULL, + pid text NOT NULL +); + +CREATE UNIQUE INDEX i_bosh_sid ON bosh USING btree (sid); + +CREATE TABLE proxy65 ( + sid text NOT NULL, + pid_t text NOT NULL, + pid_i text NOT NULL, + node_t text NOT NULL, + node_i text NOT NULL, + jid_i text NOT NULL +); + +CREATE UNIQUE INDEX i_proxy65_sid ON proxy65 USING btree (sid); +CREATE INDEX i_proxy65_jid ON proxy65 USING btree (jid_i); + +CREATE TABLE push_session ( + username text NOT NULL, + server_host text NOT NULL, + timestamp bigint NOT NULL, + service text NOT NULL, + node text NOT NULL, + xml text NOT NULL, + PRIMARY KEY (server_host, username, timestamp) +); + +CREATE UNIQUE INDEX i_push_session_susn ON push_session USING btree (server_host, username, service, node); +CREATE INDEX i_push_session_sh_username_timestamp ON push_session USING btree (server_host, username, timestamp); + +CREATE TABLE mix_channel ( + channel text NOT NULL, + service text NOT NULL, + username text NOT NULL, + domain text NOT NULL, + jid text NOT NULL, + hidden boolean NOT NULL, + hmac_key text NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE UNIQUE INDEX i_mix_channel ON mix_channel (channel, service); +CREATE INDEX i_mix_channel_serv ON mix_channel (service); + +CREATE TABLE mix_participant ( + channel text NOT NULL, + service text NOT NULL, + username text NOT NULL, + domain text NOT NULL, + jid text NOT NULL, + id text NOT NULL, + nick text NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE UNIQUE INDEX i_mix_participant ON mix_participant (channel, service, username, domain); + +CREATE TABLE mix_subscription ( + channel text NOT NULL, + service text NOT NULL, + username text NOT NULL, + domain text NOT NULL, + node text NOT NULL, + jid text NOT NULL +); + +CREATE UNIQUE INDEX i_mix_subscription ON mix_subscription (channel, service, username, domain, node); +CREATE INDEX i_mix_subscription_chan_serv_node ON mix_subscription (channel, service, node); + +CREATE TABLE mix_pam ( + username text NOT NULL, + server_host text NOT NULL, + channel text NOT NULL, + service text NOT NULL, + id text NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE UNIQUE INDEX i_mix_pam ON mix_pam (username, server_host, channel, service); + +CREATE TABLE mqtt_pub ( + username text NOT NULL, + server_host text NOT NULL, + resource text NOT NULL, + topic text NOT NULL, + qos smallint NOT NULL, + payload bytea NOT NULL, + payload_format smallint NOT NULL, + content_type text NOT NULL, + response_topic text NOT NULL, + correlation_data bytea NOT NULL, + user_properties bytea NOT NULL, + expiry bigint NOT NULL +); + +CREATE UNIQUE INDEX i_mqtt_topic_server ON mqtt_pub (topic, server_host); diff --git a/sql/pg.sql b/sql/pg.sql index f07712332..dd83e087e 100644 --- a/sql/pg.sql +++ b/sql/pg.sql @@ -1,5 +1,5 @@ -- --- ejabberd, Copyright (C) 2002-2015 ProcessOne +-- ejabberd, Copyright (C) 2002-2025 ProcessOne -- -- This program is free software; you can redistribute it and/or -- modify it under the terms of the GNU General Public License as @@ -10,18 +10,27 @@ -- 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. -- CREATE TABLE users ( - username text PRIMARY KEY, + username text NOT NULL, + "type" smallint NOT NULL, "password" text NOT NULL, - created_at TIMESTAMP NOT NULL DEFAULT now() + serverkey text NOT NULL DEFAULT '', + salt text NOT NULL DEFAULT '', + iterationcount integer NOT NULL DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT now(), + PRIMARY KEY (username, "type") ); +-- Add support for SCRAM auth to a database created before ejabberd 16.03: +-- ALTER TABLE users ADD COLUMN serverkey text NOT NULL DEFAULT ''; +-- ALTER TABLE users ADD COLUMN salt text NOT NULL DEFAULT ''; +-- ALTER TABLE users ADD COLUMN iterationcount integer NOT NULL DEFAULT 0; CREATE TABLE last ( username text PRIMARY KEY, @@ -38,13 +47,12 @@ CREATE TABLE rosterusers ( ask character(1) NOT NULL, askmessage text NOT NULL, server character(1) NOT NULL, - subscribe text, + subscribe text NOT NULL, "type" text, created_at TIMESTAMP NOT NULL DEFAULT now() ); CREATE UNIQUE INDEX i_rosteru_user_jid ON rosterusers USING btree (username, jid); -CREATE INDEX i_rosteru_username ON rosterusers USING btree (username); CREATE INDEX i_rosteru_jid ON rosterusers USING btree (jid); @@ -62,6 +70,8 @@ CREATE TABLE sr_group ( created_at TIMESTAMP NOT NULL DEFAULT now() ); +CREATE UNIQUE INDEX i_sr_group_name ON sr_group USING btree (name); + CREATE TABLE sr_user ( jid text NOT NULL, grp text NOT NULL, @@ -69,18 +79,49 @@ CREATE TABLE sr_user ( ); CREATE UNIQUE INDEX i_sr_user_jid_grp ON sr_user USING btree (jid, grp); -CREATE INDEX i_sr_user_jid ON sr_user USING btree (jid); CREATE INDEX i_sr_user_grp ON sr_user USING btree (grp); CREATE TABLE spool ( username text NOT NULL, xml text NOT NULL, - seq SERIAL, + seq BIGSERIAL, created_at TIMESTAMP NOT NULL DEFAULT now() ); CREATE INDEX i_despool ON spool USING btree (username); +CREATE TABLE archive ( + username text NOT NULL, + timestamp BIGINT NOT NULL, + peer text NOT NULL, + bare_peer text NOT NULL, + xml text NOT NULL, + txt text, + id BIGSERIAL, + kind text, + nick text, + origin_id text, + created_at TIMESTAMP NOT NULL DEFAULT now() +); + +CREATE INDEX i_username_timestamp ON archive USING btree (username, timestamp); +CREATE INDEX i_username_peer ON archive USING btree (username, peer); +CREATE INDEX i_username_bare_peer ON archive USING btree (username, bare_peer); +CREATE INDEX i_timestamp ON archive USING btree (timestamp); +CREATE INDEX i_archive_username_origin_id ON archive USING btree (username, origin_id); + +-- To update 'archive' from ejabberd <= 23.10: +-- ALTER TABLE archive ADD COLUMN origin_id text NOT NULL DEFAULT ''; +-- ALTER TABLE archive ALTER COLUMN origin_id DROP DEFAULT; +-- CREATE INDEX i_archive_username_origin_id ON archive USING btree (username, origin_id); + +CREATE TABLE archive_prefs ( + username text NOT NULL PRIMARY KEY, + def text NOT NULL, + always text NOT NULL, + never text NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT now() +); CREATE TABLE vcard ( username text PRIMARY KEY, @@ -88,18 +129,12 @@ CREATE TABLE vcard ( created_at TIMESTAMP NOT NULL DEFAULT now() ); -CREATE TABLE vcard_xupdate ( - username text PRIMARY KEY, - hash text NOT NULL, - created_at TIMESTAMP NOT NULL DEFAULT now() -); - CREATE TABLE vcard_search ( username text NOT NULL, lusername text PRIMARY KEY, fn text NOT NULL, lfn text NOT NULL, - family text NOT NULL, + "family" text NOT NULL, lfamily text NOT NULL, given text NOT NULL, lgiven text NOT NULL, @@ -141,11 +176,10 @@ CREATE TABLE privacy_default_list ( CREATE TABLE privacy_list ( username text NOT NULL, name text NOT NULL, - id SERIAL UNIQUE, + id BIGSERIAL UNIQUE, created_at TIMESTAMP NOT NULL DEFAULT now() ); -CREATE INDEX i_privacy_list_username ON privacy_list USING btree (username); CREATE UNIQUE INDEX i_privacy_list_username_name ON privacy_list USING btree (username, name); CREATE TABLE privacy_list_data ( @@ -161,6 +195,8 @@ CREATE TABLE privacy_list_data ( match_presence_out boolean NOT NULL ); +CREATE INDEX i_privacy_list_data_id ON privacy_list_data USING btree (id); + CREATE TABLE private_storage ( username text NOT NULL, namespace text NOT NULL, @@ -168,7 +204,6 @@ CREATE TABLE private_storage ( created_at TIMESTAMP NOT NULL DEFAULT now() ); -CREATE INDEX i_private_storage_username ON private_storage USING btree (username); CREATE UNIQUE INDEX i_private_storage_username_namespace ON private_storage USING btree (username, namespace); @@ -190,53 +225,53 @@ CREATE TABLE roster_version ( -- ALTER TABLE rosterusers ALTER COLUMN askmessage SET NOT NULL; CREATE TABLE pubsub_node ( - host text, - node text, - parent text, - "type" text, - nodeid SERIAL UNIQUE + host text NOT NULL, + node text NOT NULL, + parent text NOT NULL DEFAULT '', + plugin text NOT NULL, + nodeid BIGSERIAL UNIQUE ); CREATE INDEX i_pubsub_node_parent ON pubsub_node USING btree (parent); CREATE UNIQUE INDEX i_pubsub_node_tuple ON pubsub_node USING btree (host, node); CREATE TABLE pubsub_node_option ( nodeid bigint REFERENCES pubsub_node(nodeid) ON DELETE CASCADE, - name text, - val text + name text NOT NULL, + val text NOT NULL ); CREATE INDEX i_pubsub_node_option_nodeid ON pubsub_node_option USING btree (nodeid); CREATE TABLE pubsub_node_owner ( nodeid bigint REFERENCES pubsub_node(nodeid) ON DELETE CASCADE, - owner text + owner text NOT NULL ); CREATE INDEX i_pubsub_node_owner_nodeid ON pubsub_node_owner USING btree (nodeid); CREATE TABLE pubsub_state ( nodeid bigint REFERENCES pubsub_node(nodeid) ON DELETE CASCADE, - jid text, + jid text NOT NULL, affiliation character(1), - subscriptions text, - stateid SERIAL UNIQUE + subscriptions text NOT NULL DEFAULT '', + stateid BIGSERIAL UNIQUE ); CREATE INDEX i_pubsub_state_jid ON pubsub_state USING btree (jid); CREATE UNIQUE INDEX i_pubsub_state_tuple ON pubsub_state USING btree (nodeid, jid); CREATE TABLE pubsub_item ( nodeid bigint REFERENCES pubsub_node(nodeid) ON DELETE CASCADE, - itemid text, - publisher text, - creation text, - modification text, - payload text + itemid text NOT NULL, + publisher text NOT NULL, + creation varchar(32) NOT NULL, + modification varchar(32) NOT NULL, + payload text NOT NULL DEFAULT '' ); CREATE INDEX i_pubsub_item_itemid ON pubsub_item USING btree (itemid); CREATE UNIQUE INDEX i_pubsub_item_tuple ON pubsub_item USING btree (nodeid, itemid); CREATE TABLE pubsub_subscription_opt ( - subid text, + subid text NOT NULL, opt_name varchar(32), - opt_value text + opt_value text NOT NULL ); CREATE UNIQUE INDEX i_pubsub_subscription_opt ON pubsub_subscription_opt USING btree (subid, opt_name); @@ -248,6 +283,7 @@ CREATE TABLE muc_room ( ); CREATE UNIQUE INDEX i_muc_room_name_host ON muc_room USING btree (name, host); +CREATE INDEX i_muc_room_host_created_at ON muc_room USING btree (host, created_at); CREATE TABLE muc_registered ( jid text NOT NULL, @@ -259,14 +295,38 @@ CREATE TABLE muc_registered ( CREATE INDEX i_muc_registered_nick ON muc_registered USING btree (nick); CREATE UNIQUE INDEX i_muc_registered_jid_host ON muc_registered USING btree (jid, host); -CREATE TABLE irc_custom ( - jid text NOT NULL, +CREATE TABLE muc_online_room ( + name text NOT NULL, host text NOT NULL, - data text NOT NULL, - created_at TIMESTAMP NOT NULL DEFAULT now() + node text NOT NULL, + pid text NOT NULL ); -CREATE UNIQUE INDEX i_irc_custom_jid_host ON irc_custom USING btree (jid, host); +CREATE UNIQUE INDEX i_muc_online_room_name_host ON muc_online_room USING btree (name, host); + +CREATE TABLE muc_online_users ( + username text NOT NULL, + server text NOT NULL, + resource text NOT NULL, + name text NOT NULL, + host text NOT NULL, + node text NOT NULL +); + +CREATE UNIQUE INDEX i_muc_online_users ON muc_online_users USING btree (username, server, resource, name, host); + +CREATE TABLE muc_room_subscribers ( + room text NOT NULL, + host text NOT NULL, + jid text NOT NULL, + nick text NOT NULL, + nodes text NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT now() +); + +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 ( username text PRIMARY KEY, @@ -282,3 +342,139 @@ CREATE TABLE caps_features ( ); CREATE INDEX i_caps_features_node_subnode ON caps_features USING btree (node, subnode); + +CREATE TABLE sm ( + usec bigint NOT NULL, + pid text NOT NULL, + node text NOT NULL, + username text NOT NULL, + resource text NOT NULL, + priority text NOT NULL, + info text NOT NULL +); + +CREATE UNIQUE INDEX i_sm_sid ON sm USING btree (usec, pid); +CREATE INDEX i_sm_node ON sm USING btree (node); +CREATE INDEX i_sm_username ON sm USING btree (username); + +CREATE TABLE oauth_token ( + token text NOT NULL, + jid text NOT NULL, + scope text NOT NULL, + expire bigint NOT NULL +); + +CREATE UNIQUE INDEX i_oauth_token_token ON oauth_token USING btree (token); + +CREATE TABLE oauth_client ( + client_id text PRIMARY KEY, + client_name text NOT NULL, + grant_type text NOT NULL, + options text NOT NULL +); + +CREATE TABLE route ( + domain text NOT NULL, + server_host text NOT NULL, + node text NOT NULL, + pid text NOT NULL, + local_hint text NOT NULL +); + +CREATE UNIQUE INDEX i_route ON route USING btree (domain, server_host, node, pid); + +CREATE TABLE bosh ( + sid text NOT NULL, + node text NOT NULL, + pid text NOT NULL +); + +CREATE UNIQUE INDEX i_bosh_sid ON bosh USING btree (sid); + +CREATE TABLE proxy65 ( + sid text NOT NULL, + pid_t text NOT NULL, + pid_i text NOT NULL, + node_t text NOT NULL, + node_i text NOT NULL, + jid_i text NOT NULL +); + +CREATE UNIQUE INDEX i_proxy65_sid ON proxy65 USING btree (sid); +CREATE INDEX i_proxy65_jid ON proxy65 USING btree (jid_i); + +CREATE TABLE push_session ( + username text NOT NULL, + timestamp bigint NOT NULL, + service text NOT NULL, + node text NOT NULL, + xml text NOT NULL +); + +CREATE UNIQUE INDEX i_push_usn ON push_session USING btree (username, service, node); +CREATE INDEX i_push_ut ON push_session USING btree (username, timestamp); + +CREATE TABLE mix_channel ( + channel text NOT NULL, + service text NOT NULL, + username text NOT NULL, + domain text NOT NULL, + jid text NOT NULL, + hidden boolean NOT NULL, + hmac_key text NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE UNIQUE INDEX i_mix_channel ON mix_channel (channel, service); +CREATE INDEX i_mix_channel_serv ON mix_channel (service); + +CREATE TABLE mix_participant ( + channel text NOT NULL, + service text NOT NULL, + username text NOT NULL, + domain text NOT NULL, + jid text NOT NULL, + id text NOT NULL, + nick text NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE UNIQUE INDEX i_mix_participant ON mix_participant (channel, service, username, domain); + +CREATE TABLE mix_subscription ( + channel text NOT NULL, + service text NOT NULL, + username text NOT NULL, + domain text NOT NULL, + node text NOT NULL, + jid text NOT NULL +); + +CREATE UNIQUE INDEX i_mix_subscription ON mix_subscription (channel, service, username, domain, node); +CREATE INDEX i_mix_subscription_chan_serv_node ON mix_subscription (channel, service, node); + +CREATE TABLE mix_pam ( + username text NOT NULL, + channel text NOT NULL, + service text NOT NULL, + id text NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE UNIQUE INDEX i_mix_pam ON mix_pam (username, channel, service); + +CREATE TABLE mqtt_pub ( + username text NOT NULL, + resource text NOT NULL, + topic text NOT NULL, + qos smallint NOT NULL, + payload bytea NOT NULL, + payload_format smallint NOT NULL, + content_type text NOT NULL, + response_topic text NOT NULL, + correlation_data bytea NOT NULL, + user_properties bytea NOT NULL, + expiry bigint NOT NULL +); + +CREATE UNIQUE INDEX i_mqtt_topic ON mqtt_pub (topic); diff --git a/src/ELDAPv3.erl b/src/ELDAPv3.erl index 494573164..3c102e7ec 100644 --- a/src/ELDAPv3.erl +++ b/src/ELDAPv3.erl @@ -3,6 +3,7 @@ -module('ELDAPv3'). -compile(nowarn_unused_vars). +-dialyzer(no_match). -include("ELDAPv3.hrl"). -asn1_info([{vsn,'2.0.1'}, {module,'ELDAPv3'}, @@ -349,7 +350,7 @@ encode_tags(TagIn, BytesSoFar, LenSoFar). 'enc_ExtendedRequest'(element(2,Val), [<<119>>]); extendedResp -> 'enc_ExtendedResponse'(element(2,Val), [<<120>>]); - Else -> + Else -> exit({error,{asn1,{invalid_choice_type,Else}}}) end, @@ -361,105 +362,105 @@ Tlv1 = match_tags(Tlv, TagIn), case (case Tlv1 of [CtempTlv1] -> CtempTlv1; _ -> Tlv1 end) of %% 'bindRequest' - {65536, V1} -> + {65536, V1} -> {bindRequest, 'dec_BindRequest'(V1, [])}; %% 'bindResponse' - {65537, V1} -> + {65537, V1} -> {bindResponse, 'dec_BindResponse'(V1, [])}; %% 'unbindRequest' - {65538, V1} -> + {65538, V1} -> {unbindRequest, decode_null(V1,[])}; %% 'searchRequest' - {65539, V1} -> + {65539, V1} -> {searchRequest, 'dec_SearchRequest'(V1, [])}; %% 'searchResEntry' - {65540, V1} -> + {65540, V1} -> {searchResEntry, 'dec_SearchResultEntry'(V1, [])}; %% 'searchResDone' - {65541, V1} -> + {65541, V1} -> {searchResDone, 'dec_SearchResultDone'(V1, [])}; %% 'searchResRef' - {65555, V1} -> + {65555, V1} -> {searchResRef, 'dec_SearchResultReference'(V1, [])}; %% 'modifyRequest' - {65542, V1} -> + {65542, V1} -> {modifyRequest, 'dec_ModifyRequest'(V1, [])}; %% 'modifyResponse' - {65543, V1} -> + {65543, V1} -> {modifyResponse, 'dec_ModifyResponse'(V1, [])}; %% 'addRequest' - {65544, V1} -> + {65544, V1} -> {addRequest, 'dec_AddRequest'(V1, [])}; %% 'addResponse' - {65545, V1} -> + {65545, V1} -> {addResponse, 'dec_AddResponse'(V1, [])}; %% 'delRequest' - {65546, V1} -> + {65546, V1} -> {delRequest, decode_restricted_string(V1,[])}; %% 'delResponse' - {65547, V1} -> + {65547, V1} -> {delResponse, 'dec_DelResponse'(V1, [])}; %% 'modDNRequest' - {65548, V1} -> + {65548, V1} -> {modDNRequest, 'dec_ModifyDNRequest'(V1, [])}; %% 'modDNResponse' - {65549, V1} -> + {65549, V1} -> {modDNResponse, 'dec_ModifyDNResponse'(V1, [])}; %% 'compareRequest' - {65550, V1} -> + {65550, V1} -> {compareRequest, 'dec_CompareRequest'(V1, [])}; %% 'compareResponse' - {65551, V1} -> + {65551, V1} -> {compareResponse, 'dec_CompareResponse'(V1, [])}; %% 'abandonRequest' - {65552, V1} -> + {65552, V1} -> {abandonRequest, decode_integer(V1,{0,2147483647},[])}; %% 'extendedReq' - {65559, V1} -> + {65559, V1} -> {extendedReq, 'dec_ExtendedRequest'(V1, [])}; %% 'extendedResp' - {65560, V1} -> + {65560, V1} -> {extendedResp, 'dec_ExtendedResponse'(V1, [])}; - Else -> + Else -> exit({error,{asn1,{invalid_choice_tag,Else}}}) end . @@ -470,20 +471,20 @@ case (case Tlv1 of [CtempTlv1] -> CtempTlv1; _ -> Tlv1 end) of 'dec_LDAPMessage'(Tlv, TagIn) -> %%------------------------------------------------- - %% decode tag and length + %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), %%------------------------------------------------- %% attribute messageID(1) with type INTEGER %%------------------------------------------------- -[V1|Tlv2] = Tlv1, +[V1|Tlv2] = Tlv1, Term1 = decode_integer(V1,{0,2147483647},[2]), %%------------------------------------------------- %% attribute protocolOp(2) with type CHOICE %%------------------------------------------------- -[V2|Tlv3] = Tlv2, +[V2|Tlv3] = Tlv2, Term2 = 'dec_LDAPMessage_protocolOp'(V2, []), %%------------------------------------------------- @@ -639,7 +640,7 @@ decode_restricted_string(Tlv,TagIn). {EncBytes,EncLen} = 'enc_AttributeDescriptionList_components'(Val,[],0), encode_tags(TagIn, EncBytes, EncLen). -'enc_AttributeDescriptionList_components'([], AccBytes, AccLen) -> +'enc_AttributeDescriptionList_components'([], AccBytes, AccLen) -> {lists:reverse(AccBytes),AccLen}; 'enc_AttributeDescriptionList_components'([H|T],AccBytes, AccLen) -> @@ -653,7 +654,7 @@ decode_restricted_string(Tlv,TagIn). 'dec_AttributeDescriptionList'(Tlv, TagIn) -> %%------------------------------------------------- - %% decode tag and length + %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), [decode_restricted_string(V1,[4]) || V1 <- Tlv1]. @@ -708,20 +709,20 @@ encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_AttributeValueAssertion'(Tlv, TagIn) -> %%------------------------------------------------- - %% decode tag and length + %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), %%------------------------------------------------- %% attribute attributeDesc(1) with type OCTET STRING %%------------------------------------------------- -[V1|Tlv2] = Tlv1, +[V1|Tlv2] = Tlv1, Term1 = decode_restricted_string(V1,[4]), %%------------------------------------------------- %% attribute assertionValue(2) with type OCTET STRING %%------------------------------------------------- -[V2|Tlv3] = Tlv2, +[V2|Tlv3] = Tlv2, Term2 = decode_restricted_string(V2,[4]), case Tlv3 of @@ -781,7 +782,7 @@ encode_tags(TagIn, BytesSoFar, LenSoFar). {EncBytes,EncLen} = 'enc_Attribute_vals_components'(Val,[],0), encode_tags(TagIn, EncBytes, EncLen). -'enc_Attribute_vals_components'([], AccBytes, AccLen) -> +'enc_Attribute_vals_components'([], AccBytes, AccLen) -> {lists:reverse(AccBytes),AccLen}; 'enc_Attribute_vals_components'([H|T],AccBytes, AccLen) -> @@ -790,7 +791,7 @@ encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_Attribute_vals'(Tlv, TagIn) -> %%------------------------------------------------- - %% decode tag and length + %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), [decode_restricted_string(V1,[4]) || V1 <- Tlv1]. @@ -803,20 +804,20 @@ Tlv1 = match_tags(Tlv, TagIn), 'dec_Attribute'(Tlv, TagIn) -> %%------------------------------------------------- - %% decode tag and length + %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), %%------------------------------------------------- %% attribute type(1) with type OCTET STRING %%------------------------------------------------- -[V1|Tlv2] = Tlv1, +[V1|Tlv2] = Tlv1, Term1 = decode_restricted_string(V1,[4]), %%------------------------------------------------- %% attribute vals(2) with type SET OF %%------------------------------------------------- -[V2|Tlv3] = Tlv2, +[V2|Tlv3] = Tlv2, Term2 = 'dec_Attribute_vals'(V2, [17]), case Tlv3 of @@ -928,26 +929,26 @@ encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_LDAPResult'(Tlv, TagIn) -> %%------------------------------------------------- - %% decode tag and length + %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), %%------------------------------------------------- %% attribute resultCode(1) with type ENUMERATED %%------------------------------------------------- -[V1|Tlv2] = Tlv1, +[V1|Tlv2] = Tlv1, Term1 = decode_enumerated(V1,[{success,0},{operationsError,1},{protocolError,2},{timeLimitExceeded,3},{sizeLimitExceeded,4},{compareFalse,5},{compareTrue,6},{authMethodNotSupported,7},{strongAuthRequired,8},{referral,10},{adminLimitExceeded,11},{unavailableCriticalExtension,12},{confidentialityRequired,13},{saslBindInProgress,14},{noSuchAttribute,16},{undefinedAttributeType,17},{inappropriateMatching,18},{constraintViolation,19},{attributeOrValueExists,20},{invalidAttributeSyntax,21},{noSuchObject,32},{aliasProblem,33},{invalidDNSyntax,34},{aliasDereferencingProblem,36},{inappropriateAuthentication,48},{invalidCredentials,49},{insufficientAccessRights,50},{busy,51},{unavailable,52},{unwillingToPerform,53},{loopDetect,54},{namingViolation,64},{objectClassViolation,65},{notAllowedOnNonLeaf,66},{notAllowedOnRDN,67},{entryAlreadyExists,68},{objectClassModsProhibited,69},{affectsMultipleDSAs,71},{other,80}],[10]), %%------------------------------------------------- %% attribute matchedDN(2) with type OCTET STRING %%------------------------------------------------- -[V2|Tlv3] = Tlv2, +[V2|Tlv3] = Tlv2, Term2 = decode_restricted_string(V2,[4]), %%------------------------------------------------- %% attribute errorMessage(3) with type OCTET STRING %%------------------------------------------------- -[V3|Tlv4] = Tlv3, +[V3|Tlv4] = Tlv3, Term3 = decode_restricted_string(V3,[4]), %%------------------------------------------------- @@ -977,7 +978,7 @@ end, {EncBytes,EncLen} = 'enc_Referral_components'(Val,[],0), encode_tags(TagIn, EncBytes, EncLen). -'enc_Referral_components'([], AccBytes, AccLen) -> +'enc_Referral_components'([], AccBytes, AccLen) -> {lists:reverse(AccBytes),AccLen}; 'enc_Referral_components'([H|T],AccBytes, AccLen) -> @@ -991,7 +992,7 @@ end, 'dec_Referral'(Tlv, TagIn) -> %%------------------------------------------------- - %% decode tag and length + %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), [decode_restricted_string(V1,[4]) || V1 <- Tlv1]. @@ -1027,7 +1028,7 @@ decode_restricted_string(Tlv,TagIn). {EncBytes,EncLen} = 'enc_Controls_components'(Val,[],0), encode_tags(TagIn, EncBytes, EncLen). -'enc_Controls_components'([], AccBytes, AccLen) -> +'enc_Controls_components'([], AccBytes, AccLen) -> {lists:reverse(AccBytes),AccLen}; 'enc_Controls_components'([H|T],AccBytes, AccLen) -> @@ -1041,7 +1042,7 @@ decode_restricted_string(Tlv,TagIn). 'dec_Controls'(Tlv, TagIn) -> %%------------------------------------------------- - %% decode tag and length + %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), ['dec_Control'(V1, [16]) || V1 <- Tlv1]. @@ -1092,14 +1093,14 @@ encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_Control'(Tlv, TagIn) -> %%------------------------------------------------- - %% decode tag and length + %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), %%------------------------------------------------- %% attribute controlType(1) with type OCTET STRING %%------------------------------------------------- -[V1|Tlv2] = Tlv1, +[V1|Tlv2] = Tlv1, Term1 = decode_restricted_string(V1,[4]), %%------------------------------------------------- @@ -1163,26 +1164,26 @@ encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_BindRequest'(Tlv, TagIn) -> %%------------------------------------------------- - %% decode tag and length + %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), %%------------------------------------------------- %% attribute version(1) with type INTEGER %%------------------------------------------------- -[V1|Tlv2] = Tlv1, +[V1|Tlv2] = Tlv1, Term1 = decode_integer(V1,{1,127},[2]), %%------------------------------------------------- %% attribute name(2) with type OCTET STRING %%------------------------------------------------- -[V2|Tlv3] = Tlv2, +[V2|Tlv3] = Tlv2, Term2 = decode_restricted_string(V2,[4]), %%------------------------------------------------- %% attribute authentication(3) External ELDAPv3:AuthenticationChoice %%------------------------------------------------- -[V3|Tlv4] = Tlv3, +[V3|Tlv4] = Tlv3, Term3 = 'dec_AuthenticationChoice'(V3, []), case Tlv4 of @@ -1204,7 +1205,7 @@ end, encode_restricted_string(element(2,Val), [<<128>>]); sasl -> 'enc_SaslCredentials'(element(2,Val), [<<163>>]); - Else -> + Else -> exit({error,{asn1,{invalid_choice_type,Else}}}) end, @@ -1221,15 +1222,15 @@ Tlv1 = match_tags(Tlv, TagIn), case (case Tlv1 of [CtempTlv1] -> CtempTlv1; _ -> Tlv1 end) of %% 'simple' - {131072, V1} -> + {131072, V1} -> {simple, decode_restricted_string(V1,[])}; %% 'sasl' - {131075, V1} -> + {131075, V1} -> {sasl, 'dec_SaslCredentials'(V1, [])}; - Else -> + Else -> exit({error,{asn1,{invalid_choice_tag,Else}}}) end . @@ -1268,14 +1269,14 @@ encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_SaslCredentials'(Tlv, TagIn) -> %%------------------------------------------------- - %% decode tag and length + %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), %%------------------------------------------------- %% attribute mechanism(1) with type OCTET STRING %%------------------------------------------------- -[V1|Tlv2] = Tlv1, +[V1|Tlv2] = Tlv1, Term1 = decode_restricted_string(V1,[4]), %%------------------------------------------------- @@ -1388,26 +1389,26 @@ encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_BindResponse'(Tlv, TagIn) -> %%------------------------------------------------- - %% decode tag and length + %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), %%------------------------------------------------- %% attribute resultCode(1) with type ENUMERATED %%------------------------------------------------- -[V1|Tlv2] = Tlv1, +[V1|Tlv2] = Tlv1, Term1 = decode_enumerated(V1,[{success,0},{operationsError,1},{protocolError,2},{timeLimitExceeded,3},{sizeLimitExceeded,4},{compareFalse,5},{compareTrue,6},{authMethodNotSupported,7},{strongAuthRequired,8},{referral,10},{adminLimitExceeded,11},{unavailableCriticalExtension,12},{confidentialityRequired,13},{saslBindInProgress,14},{noSuchAttribute,16},{undefinedAttributeType,17},{inappropriateMatching,18},{constraintViolation,19},{attributeOrValueExists,20},{invalidAttributeSyntax,21},{noSuchObject,32},{aliasProblem,33},{invalidDNSyntax,34},{aliasDereferencingProblem,36},{inappropriateAuthentication,48},{invalidCredentials,49},{insufficientAccessRights,50},{busy,51},{unavailable,52},{unwillingToPerform,53},{loopDetect,54},{namingViolation,64},{objectClassViolation,65},{notAllowedOnNonLeaf,66},{notAllowedOnRDN,67},{entryAlreadyExists,68},{objectClassModsProhibited,69},{affectsMultipleDSAs,71},{other,80}],[10]), %%------------------------------------------------- %% attribute matchedDN(2) with type OCTET STRING %%------------------------------------------------- -[V2|Tlv3] = Tlv2, +[V2|Tlv3] = Tlv2, Term2 = decode_restricted_string(V2,[4]), %%------------------------------------------------- %% attribute errorMessage(3) with type OCTET STRING %%------------------------------------------------- -[V3|Tlv4] = Tlv3, +[V3|Tlv4] = Tlv3, Term3 = decode_restricted_string(V3,[4]), %%------------------------------------------------- @@ -1525,56 +1526,56 @@ encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_SearchRequest'(Tlv, TagIn) -> %%------------------------------------------------- - %% decode tag and length + %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), %%------------------------------------------------- %% attribute baseObject(1) with type OCTET STRING %%------------------------------------------------- -[V1|Tlv2] = Tlv1, +[V1|Tlv2] = Tlv1, Term1 = decode_restricted_string(V1,[4]), %%------------------------------------------------- %% attribute scope(2) with type ENUMERATED %%------------------------------------------------- -[V2|Tlv3] = Tlv2, +[V2|Tlv3] = Tlv2, Term2 = decode_enumerated(V2,[{baseObject,0},{singleLevel,1},{wholeSubtree,2}],[10]), %%------------------------------------------------- %% attribute derefAliases(3) with type ENUMERATED %%------------------------------------------------- -[V3|Tlv4] = Tlv3, +[V3|Tlv4] = Tlv3, Term3 = decode_enumerated(V3,[{neverDerefAliases,0},{derefInSearching,1},{derefFindingBaseObj,2},{derefAlways,3}],[10]), %%------------------------------------------------- %% attribute sizeLimit(4) with type INTEGER %%------------------------------------------------- -[V4|Tlv5] = Tlv4, +[V4|Tlv5] = Tlv4, Term4 = decode_integer(V4,{0,2147483647},[2]), %%------------------------------------------------- %% attribute timeLimit(5) with type INTEGER %%------------------------------------------------- -[V5|Tlv6] = Tlv5, +[V5|Tlv6] = Tlv5, Term5 = decode_integer(V5,{0,2147483647},[2]), %%------------------------------------------------- %% attribute typesOnly(6) with type BOOLEAN %%------------------------------------------------- -[V6|Tlv7] = Tlv6, +[V6|Tlv7] = Tlv6, Term6 = decode_boolean(V6,[1]), %%------------------------------------------------- %% attribute filter(7) External ELDAPv3:Filter %%------------------------------------------------- -[V7|Tlv8] = Tlv7, +[V7|Tlv8] = Tlv7, Term7 = 'dec_Filter'(V7, []), %%------------------------------------------------- %% attribute attributes(8) External ELDAPv3:AttributeDescriptionList %%------------------------------------------------- -[V8|Tlv9] = Tlv8, +[V8|Tlv9] = Tlv8, Term8 = 'dec_AttributeDescriptionList'(V8, [16]), case Tlv9 of @@ -1612,7 +1613,7 @@ end, 'enc_AttributeValueAssertion'(element(2,Val), [<<168>>]); extensibleMatch -> 'enc_MatchingRuleAssertion'(element(2,Val), [<<169>>]); - Else -> + Else -> exit({error,{asn1,{invalid_choice_type,Else}}}) end, @@ -1629,7 +1630,7 @@ encode_tags(TagIn, EncBytes, EncLen). {EncBytes,EncLen} = 'enc_Filter_and_components'(Val,[],0), encode_tags(TagIn, EncBytes, EncLen). -'enc_Filter_and_components'([], AccBytes, AccLen) -> +'enc_Filter_and_components'([], AccBytes, AccLen) -> {lists:reverse(AccBytes),AccLen}; 'enc_Filter_and_components'([H|T],AccBytes, AccLen) -> @@ -1638,7 +1639,7 @@ encode_tags(TagIn, EncBytes, EncLen). 'dec_Filter_and'(Tlv, TagIn) -> %%------------------------------------------------- - %% decode tag and length + %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), ['dec_Filter'(V1, []) || V1 <- Tlv1]. @@ -1654,7 +1655,7 @@ Tlv1 = match_tags(Tlv, TagIn), {EncBytes,EncLen} = 'enc_Filter_or_components'(Val,[],0), encode_tags(TagIn, EncBytes, EncLen). -'enc_Filter_or_components'([], AccBytes, AccLen) -> +'enc_Filter_or_components'([], AccBytes, AccLen) -> {lists:reverse(AccBytes),AccLen}; 'enc_Filter_or_components'([H|T],AccBytes, AccLen) -> @@ -1663,7 +1664,7 @@ Tlv1 = match_tags(Tlv, TagIn), 'dec_Filter_or'(Tlv, TagIn) -> %%------------------------------------------------- - %% decode tag and length + %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), ['dec_Filter'(V1, []) || V1 <- Tlv1]. @@ -1679,55 +1680,55 @@ Tlv1 = match_tags(Tlv, TagIn), case (case Tlv1 of [CtempTlv1] -> CtempTlv1; _ -> Tlv1 end) of %% 'and' - {131072, V1} -> + {131072, V1} -> {'and', 'dec_Filter_and'(V1, [])}; %% 'or' - {131073, V1} -> + {131073, V1} -> {'or', 'dec_Filter_or'(V1, [])}; %% 'not' - {131074, V1} -> + {131074, V1} -> {'not', 'dec_Filter'(V1, [])}; %% 'equalityMatch' - {131075, V1} -> + {131075, V1} -> {equalityMatch, 'dec_AttributeValueAssertion'(V1, [])}; %% 'substrings' - {131076, V1} -> + {131076, V1} -> {substrings, 'dec_SubstringFilter'(V1, [])}; %% 'greaterOrEqual' - {131077, V1} -> + {131077, V1} -> {greaterOrEqual, 'dec_AttributeValueAssertion'(V1, [])}; %% 'lessOrEqual' - {131078, V1} -> + {131078, V1} -> {lessOrEqual, 'dec_AttributeValueAssertion'(V1, [])}; %% 'present' - {131079, V1} -> + {131079, V1} -> {present, decode_restricted_string(V1,[])}; %% 'approxMatch' - {131080, V1} -> + {131080, V1} -> {approxMatch, 'dec_AttributeValueAssertion'(V1, [])}; %% 'extensibleMatch' - {131081, V1} -> + {131081, V1} -> {extensibleMatch, 'dec_MatchingRuleAssertion'(V1, [])}; - Else -> + Else -> exit({error,{asn1,{invalid_choice_tag,Else}}}) end . @@ -1765,7 +1766,7 @@ encode_tags(TagIn, BytesSoFar, LenSoFar). {EncBytes,EncLen} = 'enc_SubstringFilter_substrings_components'(Val,[],0), encode_tags(TagIn, EncBytes, EncLen). -'enc_SubstringFilter_substrings_components'([], AccBytes, AccLen) -> +'enc_SubstringFilter_substrings_components'([], AccBytes, AccLen) -> {lists:reverse(AccBytes),AccLen}; 'enc_SubstringFilter_substrings_components'([H|T],AccBytes, AccLen) -> @@ -1786,7 +1787,7 @@ encode_tags(TagIn, BytesSoFar, LenSoFar). encode_restricted_string(element(2,Val), [<<129>>]); final -> encode_restricted_string(element(2,Val), [<<130>>]); - Else -> + Else -> exit({error,{asn1,{invalid_choice_type,Else}}}) end, @@ -1798,26 +1799,26 @@ Tlv1 = match_tags(Tlv, TagIn), case (case Tlv1 of [CtempTlv1] -> CtempTlv1; _ -> Tlv1 end) of %% 'initial' - {131072, V1} -> + {131072, V1} -> {initial, decode_restricted_string(V1,[])}; %% 'any' - {131073, V1} -> + {131073, V1} -> {any, decode_restricted_string(V1,[])}; %% 'final' - {131074, V1} -> + {131074, V1} -> {final, decode_restricted_string(V1,[])}; - Else -> + Else -> exit({error,{asn1,{invalid_choice_tag,Else}}}) end . 'dec_SubstringFilter_substrings'(Tlv, TagIn) -> %%------------------------------------------------- - %% decode tag and length + %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), ['dec_SubstringFilter_substrings_SEQOF'(V1, []) || V1 <- Tlv1]. @@ -1830,20 +1831,20 @@ Tlv1 = match_tags(Tlv, TagIn), 'dec_SubstringFilter'(Tlv, TagIn) -> %%------------------------------------------------- - %% decode tag and length + %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), %%------------------------------------------------- %% attribute type(1) with type OCTET STRING %%------------------------------------------------- -[V1|Tlv2] = Tlv1, +[V1|Tlv2] = Tlv1, Term1 = decode_restricted_string(V1,[4]), %%------------------------------------------------- %% attribute substrings(2) with type SEQUENCE OF %%------------------------------------------------- -[V2|Tlv3] = Tlv2, +[V2|Tlv3] = Tlv2, Term2 = 'dec_SubstringFilter_substrings'(V2, [16]), case Tlv3 of @@ -1905,7 +1906,7 @@ encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_MatchingRuleAssertion'(Tlv, TagIn) -> %%------------------------------------------------- - %% decode tag and length + %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), @@ -1932,7 +1933,7 @@ end, %%------------------------------------------------- %% attribute matchValue(3) with type OCTET STRING %%------------------------------------------------- -[V3|Tlv4] = Tlv3, +[V3|Tlv4] = Tlv3, Term3 = decode_restricted_string(V3,[131075]), %%------------------------------------------------- @@ -1981,20 +1982,20 @@ encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_SearchResultEntry'(Tlv, TagIn) -> %%------------------------------------------------- - %% decode tag and length + %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), %%------------------------------------------------- %% attribute objectName(1) with type OCTET STRING %%------------------------------------------------- -[V1|Tlv2] = Tlv1, +[V1|Tlv2] = Tlv1, Term1 = decode_restricted_string(V1,[4]), %%------------------------------------------------- %% attribute attributes(2) External ELDAPv3:PartialAttributeList %%------------------------------------------------- -[V2|Tlv3] = Tlv2, +[V2|Tlv3] = Tlv2, Term2 = 'dec_PartialAttributeList'(V2, [16]), case Tlv3 of @@ -2014,7 +2015,7 @@ end, {EncBytes,EncLen} = 'enc_PartialAttributeList_components'(Val,[],0), encode_tags(TagIn, EncBytes, EncLen). -'enc_PartialAttributeList_components'([], AccBytes, AccLen) -> +'enc_PartialAttributeList_components'([], AccBytes, AccLen) -> {lists:reverse(AccBytes),AccLen}; 'enc_PartialAttributeList_components'([H|T],AccBytes, AccLen) -> @@ -2053,7 +2054,7 @@ encode_tags(TagIn, BytesSoFar, LenSoFar). {EncBytes,EncLen} = 'enc_PartialAttributeList_SEQOF_vals_components'(Val,[],0), encode_tags(TagIn, EncBytes, EncLen). -'enc_PartialAttributeList_SEQOF_vals_components'([], AccBytes, AccLen) -> +'enc_PartialAttributeList_SEQOF_vals_components'([], AccBytes, AccLen) -> {lists:reverse(AccBytes),AccLen}; 'enc_PartialAttributeList_SEQOF_vals_components'([H|T],AccBytes, AccLen) -> @@ -2062,7 +2063,7 @@ encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_PartialAttributeList_SEQOF_vals'(Tlv, TagIn) -> %%------------------------------------------------- - %% decode tag and length + %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), [decode_restricted_string(V1,[4]) || V1 <- Tlv1]. @@ -2070,20 +2071,20 @@ Tlv1 = match_tags(Tlv, TagIn), 'dec_PartialAttributeList_SEQOF'(Tlv, TagIn) -> %%------------------------------------------------- - %% decode tag and length + %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), %%------------------------------------------------- %% attribute type(1) with type OCTET STRING %%------------------------------------------------- -[V1|Tlv2] = Tlv1, +[V1|Tlv2] = Tlv1, Term1 = decode_restricted_string(V1,[4]), %%------------------------------------------------- %% attribute vals(2) with type SET OF %%------------------------------------------------- -[V2|Tlv3] = Tlv2, +[V2|Tlv3] = Tlv2, Term2 = 'dec_PartialAttributeList_SEQOF_vals'(V2, [17]), case Tlv3 of @@ -2098,7 +2099,7 @@ end, 'dec_PartialAttributeList'(Tlv, TagIn) -> %%------------------------------------------------- - %% decode tag and length + %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), ['dec_PartialAttributeList_SEQOF'(V1, [16]) || V1 <- Tlv1]. @@ -2116,7 +2117,7 @@ Tlv1 = match_tags(Tlv, TagIn), {EncBytes,EncLen} = 'enc_SearchResultReference_components'(Val,[],0), encode_tags(TagIn, EncBytes, EncLen). -'enc_SearchResultReference_components'([], AccBytes, AccLen) -> +'enc_SearchResultReference_components'([], AccBytes, AccLen) -> {lists:reverse(AccBytes),AccLen}; 'enc_SearchResultReference_components'([H|T],AccBytes, AccLen) -> @@ -2130,7 +2131,7 @@ Tlv1 = match_tags(Tlv, TagIn), 'dec_SearchResultReference'(Tlv, TagIn) -> %%------------------------------------------------- - %% decode tag and length + %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), [decode_restricted_string(V1,[4]) || V1 <- Tlv1]. @@ -2188,7 +2189,7 @@ encode_tags(TagIn, BytesSoFar, LenSoFar). {EncBytes,EncLen} = 'enc_ModifyRequest_modification_components'(Val,[],0), encode_tags(TagIn, EncBytes, EncLen). -'enc_ModifyRequest_modification_components'([], AccBytes, AccLen) -> +'enc_ModifyRequest_modification_components'([], AccBytes, AccLen) -> {lists:reverse(AccBytes),AccLen}; 'enc_ModifyRequest_modification_components'([H|T],AccBytes, AccLen) -> @@ -2224,20 +2225,20 @@ LenSoFar = EncLen1 + EncLen2, encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_ModifyRequest_modification_SEQOF'(Tlv, TagIn) -> %%------------------------------------------------- - %% decode tag and length + %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), %%------------------------------------------------- %% attribute operation(1) with type ENUMERATED %%------------------------------------------------- -[V1|Tlv2] = Tlv1, +[V1|Tlv2] = Tlv1, Term1 = decode_enumerated(V1,[{add,0},{delete,1},{replace,2}],[10]), %%------------------------------------------------- %% attribute modification(2) External ELDAPv3:AttributeTypeAndValues %%------------------------------------------------- -[V2|Tlv3] = Tlv2, +[V2|Tlv3] = Tlv2, Term2 = 'dec_AttributeTypeAndValues'(V2, [16]), case Tlv3 of @@ -2247,7 +2248,7 @@ end, 'dec_ModifyRequest_modification'(Tlv, TagIn) -> %%------------------------------------------------- - %% decode tag and length + %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), ['dec_ModifyRequest_modification_SEQOF'(V1, [16]) || V1 <- Tlv1]. @@ -2260,20 +2261,20 @@ Tlv1 = match_tags(Tlv, TagIn), 'dec_ModifyRequest'(Tlv, TagIn) -> %%------------------------------------------------- - %% decode tag and length + %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), %%------------------------------------------------- %% attribute object(1) with type OCTET STRING %%------------------------------------------------- -[V1|Tlv2] = Tlv1, +[V1|Tlv2] = Tlv1, Term1 = decode_restricted_string(V1,[4]), %%------------------------------------------------- %% attribute modification(2) with type SEQUENCE OF %%------------------------------------------------- -[V2|Tlv3] = Tlv2, +[V2|Tlv3] = Tlv2, Term2 = 'dec_ModifyRequest_modification'(V2, [16]), case Tlv3 of @@ -2315,7 +2316,7 @@ encode_tags(TagIn, BytesSoFar, LenSoFar). {EncBytes,EncLen} = 'enc_AttributeTypeAndValues_vals_components'(Val,[],0), encode_tags(TagIn, EncBytes, EncLen). -'enc_AttributeTypeAndValues_vals_components'([], AccBytes, AccLen) -> +'enc_AttributeTypeAndValues_vals_components'([], AccBytes, AccLen) -> {lists:reverse(AccBytes),AccLen}; 'enc_AttributeTypeAndValues_vals_components'([H|T],AccBytes, AccLen) -> @@ -2324,7 +2325,7 @@ encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_AttributeTypeAndValues_vals'(Tlv, TagIn) -> %%------------------------------------------------- - %% decode tag and length + %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), [decode_restricted_string(V1,[4]) || V1 <- Tlv1]. @@ -2337,20 +2338,20 @@ Tlv1 = match_tags(Tlv, TagIn), 'dec_AttributeTypeAndValues'(Tlv, TagIn) -> %%------------------------------------------------- - %% decode tag and length + %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), %%------------------------------------------------- %% attribute type(1) with type OCTET STRING %%------------------------------------------------- -[V1|Tlv2] = Tlv1, +[V1|Tlv2] = Tlv1, Term1 = decode_restricted_string(V1,[4]), %%------------------------------------------------- %% attribute vals(2) with type SET OF %%------------------------------------------------- -[V2|Tlv3] = Tlv2, +[V2|Tlv3] = Tlv2, Term2 = 'dec_AttributeTypeAndValues_vals'(V2, [17]), case Tlv3 of @@ -2407,20 +2408,20 @@ encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_AddRequest'(Tlv, TagIn) -> %%------------------------------------------------- - %% decode tag and length + %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), %%------------------------------------------------- %% attribute entry(1) with type OCTET STRING %%------------------------------------------------- -[V1|Tlv2] = Tlv1, +[V1|Tlv2] = Tlv1, Term1 = decode_restricted_string(V1,[4]), %%------------------------------------------------- %% attribute attributes(2) External ELDAPv3:AttributeList %%------------------------------------------------- -[V2|Tlv3] = Tlv2, +[V2|Tlv3] = Tlv2, Term2 = 'dec_AttributeList'(V2, [16]), case Tlv3 of @@ -2440,7 +2441,7 @@ end, {EncBytes,EncLen} = 'enc_AttributeList_components'(Val,[],0), encode_tags(TagIn, EncBytes, EncLen). -'enc_AttributeList_components'([], AccBytes, AccLen) -> +'enc_AttributeList_components'([], AccBytes, AccLen) -> {lists:reverse(AccBytes),AccLen}; 'enc_AttributeList_components'([H|T],AccBytes, AccLen) -> @@ -2479,7 +2480,7 @@ encode_tags(TagIn, BytesSoFar, LenSoFar). {EncBytes,EncLen} = 'enc_AttributeList_SEQOF_vals_components'(Val,[],0), encode_tags(TagIn, EncBytes, EncLen). -'enc_AttributeList_SEQOF_vals_components'([], AccBytes, AccLen) -> +'enc_AttributeList_SEQOF_vals_components'([], AccBytes, AccLen) -> {lists:reverse(AccBytes),AccLen}; 'enc_AttributeList_SEQOF_vals_components'([H|T],AccBytes, AccLen) -> @@ -2488,7 +2489,7 @@ encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_AttributeList_SEQOF_vals'(Tlv, TagIn) -> %%------------------------------------------------- - %% decode tag and length + %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), [decode_restricted_string(V1,[4]) || V1 <- Tlv1]. @@ -2496,20 +2497,20 @@ Tlv1 = match_tags(Tlv, TagIn), 'dec_AttributeList_SEQOF'(Tlv, TagIn) -> %%------------------------------------------------- - %% decode tag and length + %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), %%------------------------------------------------- %% attribute type(1) with type OCTET STRING %%------------------------------------------------- -[V1|Tlv2] = Tlv1, +[V1|Tlv2] = Tlv1, Term1 = decode_restricted_string(V1,[4]), %%------------------------------------------------- %% attribute vals(2) with type SET OF %%------------------------------------------------- -[V2|Tlv3] = Tlv2, +[V2|Tlv3] = Tlv2, Term2 = 'dec_AttributeList_SEQOF_vals'(V2, [17]), case Tlv3 of @@ -2524,7 +2525,7 @@ end, 'dec_AttributeList'(Tlv, TagIn) -> %%------------------------------------------------- - %% decode tag and length + %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), ['dec_AttributeList_SEQOF'(V1, [16]) || V1 <- Tlv1]. @@ -2629,26 +2630,26 @@ encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_ModifyDNRequest'(Tlv, TagIn) -> %%------------------------------------------------- - %% decode tag and length + %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), %%------------------------------------------------- %% attribute entry(1) with type OCTET STRING %%------------------------------------------------- -[V1|Tlv2] = Tlv1, +[V1|Tlv2] = Tlv1, Term1 = decode_restricted_string(V1,[4]), %%------------------------------------------------- %% attribute newrdn(2) with type OCTET STRING %%------------------------------------------------- -[V2|Tlv3] = Tlv2, +[V2|Tlv3] = Tlv2, Term2 = decode_restricted_string(V2,[4]), %%------------------------------------------------- %% attribute deleteoldrdn(3) with type BOOLEAN %%------------------------------------------------- -[V3|Tlv4] = Tlv3, +[V3|Tlv4] = Tlv3, Term3 = decode_boolean(V3,[1]), %%------------------------------------------------- @@ -2715,20 +2716,20 @@ encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_CompareRequest'(Tlv, TagIn) -> %%------------------------------------------------- - %% decode tag and length + %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), %%------------------------------------------------- %% attribute entry(1) with type OCTET STRING %%------------------------------------------------- -[V1|Tlv2] = Tlv1, +[V1|Tlv2] = Tlv1, Term1 = decode_restricted_string(V1,[4]), %%------------------------------------------------- %% attribute ava(2) External ELDAPv3:AttributeValueAssertion %%------------------------------------------------- -[V2|Tlv3] = Tlv2, +[V2|Tlv3] = Tlv2, Term2 = 'dec_AttributeValueAssertion'(V2, [16]), case Tlv3 of @@ -2807,14 +2808,14 @@ encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_ExtendedRequest'(Tlv, TagIn) -> %%------------------------------------------------- - %% decode tag and length + %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), %%------------------------------------------------- %% attribute requestName(1) with type OCTET STRING %%------------------------------------------------- -[V1|Tlv2] = Tlv1, +[V1|Tlv2] = Tlv1, Term1 = decode_restricted_string(V1,[131072]), %%------------------------------------------------- @@ -2936,26 +2937,26 @@ encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_ExtendedResponse'(Tlv, TagIn) -> %%------------------------------------------------- - %% decode tag and length + %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), %%------------------------------------------------- %% attribute resultCode(1) with type ENUMERATED %%------------------------------------------------- -[V1|Tlv2] = Tlv1, +[V1|Tlv2] = Tlv1, Term1 = decode_enumerated(V1,[{success,0},{operationsError,1},{protocolError,2},{timeLimitExceeded,3},{sizeLimitExceeded,4},{compareFalse,5},{compareTrue,6},{authMethodNotSupported,7},{strongAuthRequired,8},{referral,10},{adminLimitExceeded,11},{unavailableCriticalExtension,12},{confidentialityRequired,13},{saslBindInProgress,14},{noSuchAttribute,16},{undefinedAttributeType,17},{inappropriateMatching,18},{constraintViolation,19},{attributeOrValueExists,20},{invalidAttributeSyntax,21},{noSuchObject,32},{aliasProblem,33},{invalidDNSyntax,34},{aliasDereferencingProblem,36},{inappropriateAuthentication,48},{invalidCredentials,49},{insufficientAccessRights,50},{busy,51},{unavailable,52},{unwillingToPerform,53},{loopDetect,54},{namingViolation,64},{objectClassViolation,65},{notAllowedOnNonLeaf,66},{notAllowedOnRDN,67},{entryAlreadyExists,68},{objectClassModsProhibited,69},{affectsMultipleDSAs,71},{other,80}],[10]), %%------------------------------------------------- %% attribute matchedDN(2) with type OCTET STRING %%------------------------------------------------- -[V2|Tlv3] = Tlv2, +[V2|Tlv3] = Tlv2, Term2 = decode_restricted_string(V2,[4]), %%------------------------------------------------- %% attribute errorMessage(3) with type OCTET STRING %%------------------------------------------------- -[V3|Tlv4] = Tlv3, +[V3|Tlv4] = Tlv3, Term3 = decode_restricted_string(V3,[4]), %%------------------------------------------------- @@ -3041,7 +3042,7 @@ encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_PasswdModifyRequestValue'(Tlv, TagIn) -> %%------------------------------------------------- - %% decode tag and length + %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), @@ -3110,7 +3111,7 @@ encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_PasswdModifyResponseValue'(Tlv, TagIn) -> %%------------------------------------------------- - %% decode tag and length + %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), diff --git a/src/acl.erl b/src/acl.erl index 8d9692ffb..eaa0aa50f 100644 --- a/src/acl.erl +++ b/src/acl.erl @@ -1,11 +1,5 @@ %%%---------------------------------------------------------------------- -%%% File : acl.erl -%%% Author : Alexey Shchepin -%%% Purpose : ACL support -%%% Created : 18 Jan 2003 by Alexey Shchepin -%%% -%%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -22,457 +16,351 @@ %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- - -module(acl). +-behaviour(gen_server). --author('alexey@process-one.net'). +-export([start_link/0]). +-export([reload_from_config/0]). +-export([match_rule/3, match_acl/3]). +-export([match_rules/4, match_acls/3]). +-export([access_rules_validator/0, access_validator/0]). +-export([validator/1, validators/0]). +-export([loaded_shared_roster_module/1]). +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). --export([start/0, to_record/3, add/3, add_list/3, - add_local/3, add_list_local/3, load_from_config/0, - match_rule/3, match_acl/3, transform_options/1]). - --include("ejabberd.hrl"). -include("logger.hrl"). --include("jlib.hrl"). --record(acl, {aclname, aclspec}). --record(access, {name :: aclname(), - rules = [] :: [access_rule()]}). +-type state() :: #{hosts := [binary()]}. +-type action() :: allow | deny. +-type ip_mask() :: {inet:ip4_address(), 0..32} | {inet:ip6_address(), 0..128}. +-type access_rule() :: {acl, atom()} | acl_rule(). +-type acl_rule() :: {user, {binary(), binary()} | binary()} | + {server, binary()} | + {resource, binary()} | + {user_regexp, {misc:re_mp(), binary()} | misc:re_mp()} | + {server_regexp, misc:re_mp()} | + {resource_regexp, misc:re_mp()} | + {node_regexp, {misc:re_mp(), misc:re_mp()}} | + {user_glob, {misc:re_mp(), binary()} | misc:re_mp()} | + {server_glob, misc:re_mp()} | + {resource_glob, misc:re_mp()} | + {node_glob, {misc:re_mp(), misc:re_mp()}} | + {shared_group, {binary(), binary()} | binary()} | + {ip, ip_mask()}. +-type access() :: [{action(), [access_rule()]}]. +-type acl() :: atom() | access(). +-type match() :: #{ip => inet:ip_address(), + usr => jid:ljid(), + atom() => term()}. --type regexp() :: binary(). --type glob() :: binary(). --type access_name() :: atom(). --type access_rule() :: {atom(), any()}. --type host() :: binary(). --type aclname() :: {atom(), binary() | global}. --type aclspec() :: all | none | - {user, {binary(), host()} | binary()} | - {server, binary()} | - {resource, binary()} | - {user_regexp, {regexp(), host()} | regexp()} | - {shared_group, {binary(), host()} | binary()} | - {user_regexp, {regexp(), host()} | regexp()} | - {server_regexp, regexp()} | - {resource_regexp, regexp()} | - {node_regexp, {regexp(), regexp()}} | - {user_glob, {glob(), host()} | glob()} | - {server_glob, glob()} | - {resource_glob, glob()} | - {ip, {inet:ip_address(), integer()}} | - {node_glob, {glob(), glob()}}. +-export_type([acl/0, acl_rule/0, access/0, access_rule/0, match/0]). --type acl() :: #acl{aclname :: aclname(), - aclspec :: aclspec()}. +%%%=================================================================== +%%% API +%%%=================================================================== +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). --export_type([acl/0]). - -start() -> - case catch mnesia:table_info(acl, storage_type) of - disc_copies -> - mnesia:delete_table(acl); - _ -> - ok - end, - mnesia:create_table(acl, - [{ram_copies, [node()]}, {type, bag}, - {local_content, true}, - {attributes, record_info(fields, acl)}]), - mnesia:create_table(access, - [{ram_copies, [node()]}, - {local_content, true}, - {attributes, record_info(fields, access)}]), - mnesia:add_table_copy(acl, node(), ram_copies), - mnesia:add_table_copy(access, node(), ram_copies), - load_from_config(), - ok. - --spec to_record(binary(), atom(), aclspec()) -> acl(). - -to_record(Host, ACLName, ACLSpec) -> - #acl{aclname = {ACLName, Host}, - aclspec = normalize_spec(ACLSpec)}. - --spec add(binary(), aclname(), aclspec()) -> ok | {error, any()}. - -add(Host, ACLName, ACLSpec) -> - {ResL, BadNodes} = rpc:multicall(mnesia:system_info(running_db_nodes), - ?MODULE, add_local, - [Host, ACLName, ACLSpec]), - case lists:keyfind(aborted, 1, ResL) of - false when BadNodes == [] -> - ok; - false -> - {error, {failed_nodes, BadNodes}}; - Err -> - {error, Err} - end. - -add_local(Host, ACLName, ACLSpec) -> - F = fun () -> - mnesia:write(#acl{aclname = {ACLName, Host}, - aclspec = normalize_spec(ACLSpec)}) - end, - case mnesia:transaction(F) of - {atomic, ok} -> - ok; - Err -> - Err - end. - --spec add_list(binary(), [acl()], boolean()) -> ok | {error, any()}. - -add_list(Host, ACLs, Clear) -> - {ResL, BadNodes} = rpc:multicall(mnesia:system_info(running_db_nodes), - ?MODULE, add_list_local, - [Host, ACLs, Clear]), - case lists:keyfind(aborted, 1, ResL) of - false when BadNodes == [] -> - ok; - false -> - {error, {failed_nodes, BadNodes}}; - Err -> - {error, Err} - end. - -add_list_local(Host, ACLs, Clear) -> - F = fun () -> - if Clear -> - Ks = mnesia:select(acl, - [{{acl, {'$1', Host}, '$2'}, [], - ['$1']}]), - lists:foreach(fun (K) -> mnesia:delete({acl, {K, Host}}) - end, - Ks); - true -> ok - end, - lists:foreach(fun (ACL) -> - case ACL of - #acl{aclname = ACLName, - aclspec = ACLSpec} -> - mnesia:write(#acl{aclname = - {ACLName, - Host}, - aclspec = - normalize_spec(ACLSpec)}) - end - end, - ACLs) - end, - mnesia:transaction(F). - --spec add_access(binary() | global, - access_name(), [access_rule()]) -> ok | {error, any()}. - -add_access(Host, Access, Rules) -> - case mnesia:transaction( - fun() -> - mnesia:write( - #access{name = {Access, Host}, - rules = Rules}) - end) of - {atomic, ok} -> - ok; - Err -> - {error, Err} - end. - --spec load_from_config() -> ok. - -load_from_config() -> - Hosts = [global|?MYHOSTS], - lists:foreach( - fun(Host) -> - ACLs = ejabberd_config:get_option( - {acl, Host}, fun(V) -> V end, []), - AccessRules = ejabberd_config:get_option( - {access, Host}, fun(V) -> V end, []), - lists:foreach( - fun({ACLName, SpecList}) -> - lists:foreach( - fun({ACLType, ACLSpecs}) when is_list(ACLSpecs) -> - lists:foreach( - fun(ACLSpec) -> - add(Host, ACLName, - {ACLType, ACLSpec}) - end, lists:flatten(ACLSpecs)); - ({ACLType, ACLSpecs}) -> - add(Host, ACLName, {ACLType, ACLSpecs}) - end, lists:flatten(SpecList)) - end, ACLs), - lists:foreach( - fun({Access, Rules}) -> - add_access(Host, Access, Rules) - end, AccessRules) - end, Hosts). - -b(S) -> - iolist_to_binary(S). - -nodeprep(S) -> - jlib:nodeprep(b(S)). - -nameprep(S) -> - jlib:nameprep(b(S)). - -resourceprep(S) -> - jlib:resourceprep(b(S)). - -normalize_spec(Spec) -> - case Spec of - all -> all; - none -> none; - {user, {U, S}} -> {user, {nodeprep(U), nameprep(S)}}; - {user, U} -> {user, nodeprep(U)}; - {shared_group, {G, H}} -> {shared_group, {b(G), nameprep(H)}}; - {shared_group, G} -> {shared_group, b(G)}; - {user_regexp, {UR, S}} -> {user_regexp, {b(UR), nameprep(S)}}; - {user_regexp, UR} -> {user_regexp, b(UR)}; - {node_regexp, {UR, SR}} -> {node_regexp, {b(UR), b(SR)}}; - {user_glob, {UR, S}} -> {user_glob, {b(UR), nameprep(S)}}; - {user_glob, UR} -> {user_glob, b(UR)}; - {node_glob, {UR, SR}} -> {node_glob, {b(UR), b(SR)}}; - {server, S} -> {server, nameprep(S)}; - {resource, R} -> {resource, resourceprep(R)}; - {server_regexp, SR} -> {server_regexp, b(SR)}; - {server_glob, S} -> {server_glob, b(S)}; - {resource_glob, R} -> {resource_glob, b(R)}; - {ip, {Net, Mask}} -> - {ip, {Net, Mask}}; - {ip, S} -> - case parse_ip_netmask(b(S)) of - {ok, Net, Mask} -> - {ip, {Net, Mask}}; - error -> - ?INFO_MSG("Invalid network address: ~p", [S]), - none - end - end. - --spec match_rule(global | binary(), access_name(), - jid() | ljid() | inet:ip_address()) -> any(). - -match_rule(_Host, all, _JID) -> +-spec match_rule(global | binary(), atom() | access(), + jid:jid() | jid:ljid() | inet:ip_address() | match()) -> action(). +match_rule(_, all, _) -> allow; -match_rule(_Host, none, _JID) -> +match_rule(_, none, _) -> deny; +match_rule(Host, Access, Match) when is_map(Match) -> + Rules = if is_atom(Access) -> read_access(Access, Host); + true -> Access + end, + match_rules(Host, Rules, Match, deny); +match_rule(Host, Access, IP) when tuple_size(IP) == 4; tuple_size(IP) == 8 -> + match_rule(Host, Access, #{ip => IP}); match_rule(Host, Access, JID) -> - GAccess = ets:lookup(access, {Access, global}), - LAccess = if Host /= global -> - ets:lookup(access, {Access, Host}); - true -> - [] - end, - case GAccess ++ LAccess of - [] -> - deny; - AccessList -> - Rules = lists:flatmap( - fun(#access{rules = Rs}) -> - Rs - end, AccessList), - match_acls(Rules, JID, Host) - end. + match_rule(Host, Access, #{usr => jid:tolower(JID)}). -match_acls([], _, _Host) -> deny; -match_acls([{ACL, Access} | ACLs], JID, Host) -> - case match_acl(ACL, JID, Host) of - true -> Access; - _ -> match_acls(ACLs, JID, Host) - end. - --spec match_acl(atom(), - jid() | ljid() | inet:ip_address(), - binary()) -> boolean(). - -match_acl(all, _JID, _Host) -> +-spec match_acl(global | binary(), access_rule(), match()) -> boolean(). +match_acl(_Host, {acl, all}, _) -> true; -match_acl(none, _JID, _Host) -> +match_acl(_Host, {acl, none}, _) -> false; -match_acl(ACL, IP, Host) when tuple_size(IP) == 4; - tuple_size(IP) == 8 -> +match_acl(Host, {acl, ACLName}, Match) -> lists:any( - fun(#acl{aclspec = {ip, {Net, Mask}}}) -> - is_ip_match(IP, Net, Mask); - (_) -> - false - end, - ets:lookup(acl, {ACL, Host}) ++ - ets:lookup(acl, {ACL, global})); -match_acl(ACL, JID, Host) -> - {User, Server, Resource} = jlib:jid_tolower(JID), - lists:any( - fun(#acl{aclspec = Spec}) -> - case Spec of - all -> true; - {user, {U, S}} -> U == User andalso S == Server; - {user, U} -> - U == User andalso - lists:member(Server, ?MYHOSTS); - {server, S} -> S == Server; - {resource, R} -> R == Resource; - {shared_group, {G, H}} -> - Mod = loaded_shared_roster_module(H), - Mod:is_user_in_group({User, Server}, G, H); - {shared_group, G} -> - Mod = loaded_shared_roster_module(Host), - Mod:is_user_in_group({User, Server}, G, Host); - {user_regexp, {UR, S}} -> - S == Server andalso is_regexp_match(User, UR); - {user_regexp, UR} -> - lists:member(Server, ?MYHOSTS) - andalso is_regexp_match(User, UR); - {server_regexp, SR} -> - is_regexp_match(Server, SR); - {resource_regexp, RR} -> - is_regexp_match(Resource, RR); - {node_regexp, {UR, SR}} -> - is_regexp_match(Server, SR) andalso - is_regexp_match(User, UR); - {user_glob, {UR, S}} -> - S == Server andalso is_glob_match(User, UR); - {user_glob, UR} -> - lists:member(Server, ?MYHOSTS) - andalso is_glob_match(User, UR); - {server_glob, SR} -> is_glob_match(Server, SR); - {resource_glob, RR} -> - is_glob_match(Resource, RR); - {node_glob, {UR, SR}} -> - is_glob_match(Server, SR) andalso - is_glob_match(User, UR); - WrongSpec -> - ?ERROR_MSG("Wrong ACL expression: ~p~nCheck your " - "config file and reload it with the override_a" - "cls option enabled", - [WrongSpec]), - false - end - end, - ets:lookup(acl, {ACL, Host}) ++ - ets:lookup(acl, {ACL, global})). - -is_regexp_match(String, RegExp) -> - case ejabberd_regexp:run(String, RegExp) of - nomatch -> false; - match -> true; - {error, ErrDesc} -> - ?ERROR_MSG("Wrong regexp ~p in ACL: ~p", - [RegExp, ErrDesc]), - false - end. - -is_glob_match(String, Glob) -> - is_regexp_match(String, - ejabberd_regexp:sh_to_awk(Glob)). - -is_ip_match({_, _, _, _} = IP, {_, _, _, _} = Net, Mask) -> - IPInt = ip_to_integer(IP), - NetInt = ip_to_integer(Net), - M = bnot (1 bsl (32 - Mask) - 1), - IPInt band M =:= NetInt band M; -is_ip_match({_, _, _, _, _, _, _, _} = IP, - {_, _, _, _, _, _, _, _} = Net, Mask) -> - IPInt = ip_to_integer(IP), - NetInt = ip_to_integer(Net), - M = bnot (1 bsl (128 - Mask) - 1), - IPInt band M =:= NetInt band M; -is_ip_match(_, _, _) -> + fun(ACL) -> + match_acl(Host, ACL, Match) + end, read_acl(ACLName, Host)); +match_acl(_Host, {ip, {Net, Mask}}, #{ip := {IP, _Port}}) -> + misc:match_ip_mask(IP, Net, Mask); +match_acl(_Host, {ip, {Net, Mask}}, #{ip := IP}) -> + misc:match_ip_mask(IP, Net, Mask); +match_acl(_Host, {user, {U, S}}, #{usr := {U, S, _}}) -> + true; +match_acl(_Host, {user, U}, #{usr := {U, S, _}}) -> + ejabberd_router:is_my_host(S); +match_acl(_Host, {server, S}, #{usr := {_, S, _}}) -> + true; +match_acl(_Host, {resource, R}, #{usr := {_, _, R}}) -> + true; +match_acl(_Host, {shared_group, {G, H}}, #{usr := {U, S, _}}) -> + case loaded_shared_roster_module(H) of + undefined -> false; + Mod -> Mod:is_user_in_group({U, S}, G, H) + end; +match_acl(Host, {shared_group, G}, #{usr := {_, S, _}} = Map) -> + match_acl(Host, {shared_group, {G, S}}, Map); +match_acl(_Host, {user_regexp, {UR, S1}}, #{usr := {U, S2, _}}) -> + S1 == S2 andalso match_regexp(U, UR); +match_acl(_Host, {user_regexp, UR}, #{usr := {U, S, _}}) -> + ejabberd_router:is_my_host(S) andalso match_regexp(U, UR); +match_acl(_Host, {server_regexp, SR}, #{usr := {_, S, _}}) -> + match_regexp(S, SR); +match_acl(_Host, {resource_regexp, RR}, #{usr := {_, _, R}}) -> + match_regexp(R, RR); +match_acl(_Host, {node_regexp, {UR, SR}}, #{usr := {U, S, _}}) -> + match_regexp(U, UR) andalso match_regexp(S, SR); +match_acl(_Host, {user_glob, {UR, S1}}, #{usr := {U, S2, _}}) -> + S1 == S2 andalso match_regexp(U, UR); +match_acl(_Host, {user_glob, UR}, #{usr := {U, S, _}}) -> + ejabberd_router:is_my_host(S) andalso match_regexp(U, UR); +match_acl(_Host, {server_glob, SR}, #{usr := {_, S, _}}) -> + match_regexp(S, SR); +match_acl(_Host, {resource_glob, RR}, #{usr := {_, _, R}}) -> + match_regexp(R, RR); +match_acl(_Host, {node_glob, {UR, SR}}, #{usr := {U, S, _}}) -> + match_regexp(U, UR) andalso match_regexp(S, SR); +match_acl(_, _, _) -> false. -ip_to_integer({IP1, IP2, IP3, IP4}) -> - IP1 bsl 8 bor IP2 bsl 8 bor IP3 bsl 8 bor IP4; -ip_to_integer({IP1, IP2, IP3, IP4, IP5, IP6, IP7, - IP8}) -> - IP1 bsl 16 bor IP2 bsl 16 bor IP3 bsl 16 bor IP4 bsl 16 - bor IP5 - bsl 16 - bor IP6 - bsl 16 - bor IP7 - bsl 16 - bor IP8. +-spec match_rules(global | binary(), [{T, [access_rule()]}], match(), T) -> T. +match_rules(Host, [{Return, Rules} | Rest], Match, Default) -> + case match_acls(Host, Rules, Match) of + false -> + match_rules(Host, Rest, Match, Default); + true -> + Return + end; +match_rules(_Host, [], _Match, Default) -> + Default. +-spec match_acls(global | binary(), [access_rule()], match()) -> boolean(). +match_acls(_Host, [], _Match) -> + false; +match_acls(Host, Rules, Match) -> + lists:all( + fun(Rule) -> + match_acl(Host, Rule, Match) + end, Rules). + +-spec reload_from_config() -> ok. +reload_from_config() -> + gen_server:call(?MODULE, reload_from_config, timer:minutes(1)). + +-spec validator(access_rules | acl) -> econf:validator(). +validator(access_rules) -> + econf:options( + #{'_' => access_rules_validator()}, + [{disallowed, [all, none]}, unique]); +validator(acl) -> + econf:options( + #{'_' => acl_validator()}, + [{disallowed, [all, none]}, unique]). + +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== +-spec init([]) -> {ok, state()}. +init([]) -> + create_tab(acl), + create_tab(access), + Hosts = ejabberd_option:hosts(), + load_from_config(Hosts), + ejabberd_hooks:add(config_reloaded, ?MODULE, reload_from_config, 20), + {ok, #{hosts => Hosts}}. + +-spec handle_call(term(), term(), state()) -> {reply, ok, state()} | {noreply, state()}. +handle_call(reload_from_config, _, State) -> + NewHosts = ejabberd_option:hosts(), + load_from_config(NewHosts), + {reply, ok, State#{hosts => NewHosts}}; +handle_call(Request, From, State) -> + ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), + {noreply, State}. + +-spec handle_cast(term(), state()) -> {noreply, state()}. +handle_cast(Msg, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Msg]), + {noreply, State}. + +-spec handle_info(term(), state()) -> {noreply, state()}. +handle_info(Info, State) -> + ?WARNING_MSG("Unexpected info: ~p", [Info]), + {noreply, State}. + +-spec terminate(any(), state()) -> ok. +terminate(_Reason, _State) -> + ejabberd_hooks:delete(config_reloaded, ?MODULE, reload_from_config, 20). + +-spec code_change(term(), state(), term()) -> {ok, state()}. +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +%%%=================================================================== +%%% Table management +%%%=================================================================== +-spec load_from_config([binary()]) -> ok. +load_from_config(NewHosts) -> + ?DEBUG("Loading access rules from config", []), + load_tab(acl, NewHosts, fun ejabberd_option:acl/1), + load_tab(access, NewHosts, fun ejabberd_option:access_rules/1), + ?DEBUG("Access rules loaded successfully", []). + +-spec create_tab(atom()) -> atom(). +create_tab(Tab) -> + _ = mnesia:delete_table(Tab), + ets:new(Tab, [named_table, set, {read_concurrency, true}]). + +-spec load_tab(atom(), [binary()], fun((global | binary()) -> {atom(), list()})) -> ok. +load_tab(Tab, Hosts, Fun) -> + Old = ets:tab2list(Tab), + New = lists:flatmap( + fun(Host) -> + [{{Name, Host}, List} || {Name, List} <- Fun(Host)] + end, [global|Hosts]), + ets:insert(Tab, New), + lists:foreach( + fun({Key, _}) -> + case lists:keymember(Key, 1, New) of + false -> ets:delete(Tab, Key); + true -> ok + end + end, Old). + +-spec read_access(atom(), global | binary()) -> access(). +read_access(Name, Host) -> + case ets:lookup(access, {Name, Host}) of + [{_, Access}] -> Access; + [] -> [] + end. + +-spec read_acl(atom(), global | binary()) -> [acl_rule()]. +read_acl(Name, Host) -> + case ets:lookup(acl, {Name, Host}) of + [{_, ACL}] -> ACL; + [] -> [] + end. + +%%%=================================================================== +%%% Validators +%%%=================================================================== +validators() -> + #{ip => econf:list_or_single(econf:ip_mask()), + user => user_validator(econf:user(), econf:domain()), + user_regexp => user_validator(econf:re([unicode]), econf:domain()), + user_glob => user_validator(econf:glob([unicode]), econf:domain()), + server => econf:list_or_single(econf:domain()), + server_regexp => econf:list_or_single(econf:re([unicode])), + server_glob => econf:list_or_single(econf:glob([unicode])), + resource => econf:list_or_single(econf:resource()), + resource_regexp => econf:list_or_single(econf:re([unicode])), + resource_glob => econf:list_or_single(econf:glob([unicode])), + node_regexp => node_validator(econf:re([unicode]), econf:re([unicode])), + node_glob => node_validator(econf:glob([unicode]), econf:glob([unicode])), + shared_group => user_validator(econf:binary(), econf:domain()), + acl => econf:atom()}. + +rule_validator() -> + rule_validator(validators()). + +rule_validator(RVs) -> + econf:and_then( + econf:non_empty(econf:options(RVs, [])), + fun(Rules) -> + lists:flatmap( + fun({Type, Rs}) when is_list(Rs) -> + [{Type, R} || R <- Rs]; + (Other) -> + [Other] + end, Rules) + end). + +access_validator() -> + econf:and_then( + fun(L) when is_list(L) -> + lists:map( + fun({K, V}) -> {(econf:atom())(K), V}; + (A) -> {acl, (econf:atom())(A)} + end, lists:flatten(L)); + (A) -> + [{acl, (econf:atom())(A)}] + end, + rule_validator()). + +access_rules_validator() -> + econf:and_then( + fun(L) when is_list(L) -> + lists:map( + fun({K, V}) -> {(econf:atom())(K), V}; + (A) -> {(econf:atom())(A), [{acl, all}]} + end, lists:flatten(L)); + (Bad) -> + Bad + end, + econf:non_empty( + econf:options( + #{allow => access_validator(), + deny => access_validator()}, + []))). + +acl_validator() -> + econf:and_then( + fun(L) when is_list(L) -> lists:flatten(L); + (Bad) -> Bad + end, + rule_validator(maps:remove(acl, validators()))). + +user_validator(UV, SV) -> + econf:and_then( + econf:list_or_single( + fun({U, S}) -> + {UV(U), SV(S)}; + (M) when is_list(M) -> + (econf:map(UV, SV))(M); + (Val) -> + US = (econf:binary())(Val), + case binary:split(US, <<"@">>, [global]) of + [U, S] -> {UV(U), SV(S)}; + [U] -> UV(U); + _ -> econf:fail({bad_user, Val}) + end + end), + fun lists:flatten/1). + +node_validator(UV, SV) -> + econf:and_then( + econf:and_then( + econf:list(econf:any()), + fun lists:flatten/1), + econf:map(UV, SV)). + +%%%=================================================================== +%%% Aux +%%%=================================================================== +-spec match_regexp(iodata(), misc:re_mp()) -> boolean(). +match_regexp(Data, RegExp) -> + re:run(Data, RegExp) /= nomatch. + +-spec loaded_shared_roster_module(global | binary()) -> atom(). +loaded_shared_roster_module(global) -> + loaded_shared_roster_module(ejabberd_config:get_myname()); loaded_shared_roster_module(Host) -> case gen_mod:is_loaded(Host, mod_shared_roster_ldap) of - true -> mod_shared_roster_ldap; - false -> mod_shared_roster + true -> mod_shared_roster_ldap; + false -> + case gen_mod:is_loaded(Host, mod_shared_roster) of + true -> mod_shared_roster; + false -> undefined + end end. - -parse_ip_netmask(S) -> - case str:tokens(S, <<"/">>) of - [IPStr] -> - case inet_parse:address(binary_to_list(IPStr)) of - {ok, {_, _, _, _} = IP} -> {ok, IP, 32}; - {ok, {_, _, _, _, _, _, _, _} = IP} -> {ok, IP, 128}; - _ -> error - end; - [IPStr, MaskStr] -> - case catch jlib:binary_to_integer(MaskStr) of - Mask when is_integer(Mask), Mask >= 0 -> - case inet_parse:address(binary_to_list(IPStr)) of - {ok, {_, _, _, _} = IP} when Mask =< 32 -> - {ok, IP, Mask}; - {ok, {_, _, _, _, _, _, _, _} = IP} when Mask =< 128 -> - {ok, IP, Mask}; - _ -> error - end; - _ -> error - end; - _ -> error - end. - -transform_options(Opts) -> - Opts1 = lists:foldl(fun transform_options/2, [], Opts), - {ACLOpts, Opts2} = lists:mapfoldl( - fun({acl, Os}, Acc) -> - {Os, Acc}; - (O, Acc) -> - {[], [O|Acc]} - end, [], Opts1), - {AccessOpts, Opts3} = lists:mapfoldl( - fun({access, Os}, Acc) -> - {Os, Acc}; - (O, Acc) -> - {[], [O|Acc]} - end, [], Opts2), - ACLOpts1 = ejabberd_config:collect_options(lists:flatten(ACLOpts)), - AccessOpts1 = case ejabberd_config:collect_options( - lists:flatten(AccessOpts)) of - [] -> []; - L1 -> [{access, L1}] - end, - ACLOpts2 = case lists:map( - fun({ACLName, Os}) -> - {ACLName, ejabberd_config:collect_options(Os)} - end, ACLOpts1) of - [] -> []; - L2 -> [{acl, L2}] - end, - ACLOpts2 ++ AccessOpts1 ++ Opts3. - -transform_options({acl, Name, Type}, Opts) -> - T = case Type of - all -> all; - none -> none; - {user, U} -> {user, [b(U)]}; - {user, U, S} -> {user, [[{b(U), b(S)}]]}; - {shared_group, G} -> {shared_group, [b(G)]}; - {shared_group, G, H} -> {shared_group, [[{b(G), b(H)}]]}; - {user_regexp, UR} -> {user_regexp, [b(UR)]}; - {user_regexp, UR, S} -> {user_regexp, [[{b(UR), b(S)}]]}; - {node_regexp, UR, SR} -> {node_regexp, [[{b(UR), b(SR)}]]}; - {user_glob, UR} -> {user_glob, [b(UR)]}; - {user_glob, UR, S} -> {user_glob, [[{b(UR), b(S)}]]}; - {node_glob, UR, SR} -> {node_glob, [[{b(UR), b(SR)}]]}; - {server, S} -> {server, [b(S)]}; - {resource, R} -> {resource, [b(R)]}; - {server_regexp, SR} -> {server_regexp, [b(SR)]}; - {server_glob, S} -> {server_glob, [b(S)]}; - {ip, S} -> {ip, [b(S)]}; - {resource_glob, R} -> {resource_glob, [b(R)]} - end, - [{acl, [{Name, [T]}]}|Opts]; -transform_options({access, Name, Rules}, Opts) -> - NewRules = [{ACL, Action} || {Action, ACL} <- Rules], - [{access, [{Name, NewRules}]}|Opts]; -transform_options(Opt, Opts) -> - [Opt|Opts]. diff --git a/src/adhoc.erl b/src/adhoc.erl deleted file mode 100644 index a68b54d89..000000000 --- a/src/adhoc.erl +++ /dev/null @@ -1,160 +0,0 @@ -%%%---------------------------------------------------------------------- -%%% File : adhoc.erl -%%% Author : Magnus Henoch -%%% Purpose : Provide helper functions for ad-hoc commands (XEP-0050) -%%% Created : 31 Oct 2005 by Magnus Henoch -%%% -%%% -%%% ejabberd, Copyright (C) 2002-2015 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(adhoc). - --author('henoch@dtek.chalmers.se'). - --export([ - parse_request/1, - produce_response/2, - produce_response/1 -]). - --include("ejabberd.hrl"). --include("logger.hrl"). --include("jlib.hrl"). --include("adhoc.hrl"). - -%% Parse an ad-hoc request. Return either an adhoc_request record or -%% an {error, ErrorType} tuple. -%% --spec(parse_request/1 :: -( - IQ :: iq_request()) - -> adhoc_response() - %% - | {error, _} -). - -parse_request(#iq{type = set, lang = Lang, sub_el = SubEl, xmlns = ?NS_COMMANDS}) -> - ?DEBUG("entering parse_request...", []), - Node = xml:get_tag_attr_s(<<"node">>, SubEl), - SessionID = xml:get_tag_attr_s(<<"sessionid">>, SubEl), - Action = xml:get_tag_attr_s(<<"action">>, SubEl), - XData = find_xdata_el(SubEl), - #xmlel{children = AllEls} = SubEl, - Others = case XData of - false -> AllEls; - _ -> lists:delete(XData, AllEls) - end, - #adhoc_request{ - lang = Lang, - node = Node, - sessionid = SessionID, - action = Action, - xdata = XData, - others = Others - }; -parse_request(_) -> {error, ?ERR_BAD_REQUEST}. - -%% Borrowed from mod_vcard.erl -find_xdata_el(#xmlel{children = SubEls}) -> - find_xdata_el1(SubEls). - -find_xdata_el1([]) -> false; -find_xdata_el1([El | Els]) when is_record(El, xmlel) -> - case xml:get_tag_attr_s(<<"xmlns">>, El) of - ?NS_XDATA -> El; - _ -> find_xdata_el1(Els) - end; -find_xdata_el1([_ | Els]) -> find_xdata_el1(Els). - -%% Produce a node to use as response from an adhoc_response -%% record, filling in values for language, node and session id from -%% the request. -%% --spec(produce_response/2 :: -( - Adhoc_Request :: adhoc_request(), - Adhoc_Response :: adhoc_response()) - -> Xmlel::xmlel() -). - -%% Produce a node to use as response from an adhoc_response -%% record. -produce_response(#adhoc_request{lang = Lang, node = Node, sessionid = SessionID}, - Adhoc_Response) -> - produce_response(Adhoc_Response#adhoc_response{ - lang = Lang, node = Node, sessionid = SessionID - }). - -%% --spec(produce_response/1 :: -( - Adhoc_Response::adhoc_response()) - -> Xmlel::xmlel() -). - -produce_response( - #adhoc_response{ - %lang = _Lang, - node = Node, - sessionid = ProvidedSessionID, - status = Status, - defaultaction = DefaultAction, - actions = Actions, - notes = Notes, - elements = Elements - }) -> - SessionID = if is_binary(ProvidedSessionID), - ProvidedSessionID /= <<"">> -> ProvidedSessionID; - true -> jlib:now_to_utc_string(now()) - end, - case Actions of - [] -> - ActionsEls = []; - _ -> - case DefaultAction of - <<"">> -> ActionsElAttrs = []; - _ -> ActionsElAttrs = [{<<"execute">>, DefaultAction}] - end, - ActionsEls = [ - #xmlel{ - name = <<"actions">>, - attrs = ActionsElAttrs, - children = [ - #xmlel{name = Action, attrs = [], children = []} - || Action <- Actions] - } - ] - end, - NotesEls = lists:map(fun({Type, Text}) -> - #xmlel{ - name = <<"note">>, - attrs = [{<<"type">>, Type}], - children = [{xmlcdata, Text}] - } - end, Notes), - #xmlel{ - name = <<"command">>, - attrs = [ - {<<"xmlns">>, ?NS_COMMANDS}, - {<<"sessionid">>, SessionID}, - {<<"node">>, Node}, - {<<"status">>, iolist_to_binary(atom_to_list(Status))} - ], - children = ActionsEls ++ NotesEls ++ Elements - }. diff --git a/src/cyrsasl.erl b/src/cyrsasl.erl deleted file mode 100644 index 764473bab..000000000 --- a/src/cyrsasl.erl +++ /dev/null @@ -1,239 +0,0 @@ -%%%---------------------------------------------------------------------- -%%% File : cyrsasl.erl -%%% Author : Alexey Shchepin -%%% Purpose : Cyrus SASL-like library -%%% Created : 8 Mar 2003 by Alexey Shchepin -%%% -%%% -%%% ejabberd, Copyright (C) 2002-2015 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(cyrsasl). - --author('alexey@process-one.net'). - --export([start/0, register_mechanism/3, listmech/1, - server_new/7, server_start/3, server_step/2]). - --include("ejabberd.hrl"). --include("logger.hrl"). - -%% --export_type([ - mechanism/0, - mechanisms/0, - sasl_mechanism/0 -]). - --record(sasl_mechanism, - {mechanism = <<"">> :: mechanism() | '$1', - module :: atom(), - password_type = plain :: password_type() | '$2'}). - --type(mechanism() :: binary()). --type(mechanisms() :: [mechanism(),...]). --type(password_type() :: plain | digest | scram). --type(props() :: [{username, binary()} | - {authzid, binary()} | - {auth_module, atom()}]). - --type(sasl_mechanism() :: #sasl_mechanism{}). - --record(sasl_state, -{ - service, - myname, - realm, - get_password, - check_password, - check_password_digest, - mech_mod, - mech_state -}). - --callback mech_new(binary(), fun(), fun(), fun()) -> any(). --callback mech_step(any(), binary()) -> {ok, props()} | - {ok, props(), binary()} | - {continue, binary(), any()} | - {error, binary()} | - {error, binary(), binary()}. - -start() -> - ets:new(sasl_mechanism, - [named_table, public, - {keypos, #sasl_mechanism.mechanism}]), - cyrsasl_plain:start([]), - cyrsasl_digest:start([]), - cyrsasl_scram:start([]), - cyrsasl_anonymous:start([]), - ok. - -%% --spec(register_mechanism/3 :: -( - Mechanim :: mechanism(), - Module :: module(), - PasswordType :: password_type()) - -> any() -). - -register_mechanism(Mechanism, Module, PasswordType) -> - case is_disabled(Mechanism) of - false -> - ets:insert(sasl_mechanism, - #sasl_mechanism{mechanism = Mechanism, module = Module, - password_type = PasswordType}); - true -> - ?DEBUG("SASL mechanism ~p is disabled", [Mechanism]), - true - end. - -%%% TODO: use callbacks -%%-include("ejabberd.hrl"). -%%-include("jlib.hrl"). -%%check_authzid(_State, Props) -> -%% AuthzId = xml:get_attr_s(authzid, Props), -%% case jlib:string_to_jid(AuthzId) of -%% error -> -%% {error, "invalid-authzid"}; -%% JID -> -%% LUser = jlib:nodeprep(xml:get_attr_s(username, Props)), -%% {U, S, R} = jlib:jid_tolower(JID), -%% case R of -%% "" -> -%% {error, "invalid-authzid"}; -%% _ -> -%% case {LUser, ?MYNAME} of -%% {U, S} -> -%% ok; -%% _ -> -%% {error, "invalid-authzid"} -%% end -%% end -%% end. - -check_credentials(_State, Props) -> - User = proplists:get_value(username, Props, <<>>), - case jlib:nodeprep(User) of - error -> {error, <<"not-authorized">>}; - <<"">> -> {error, <<"not-authorized">>}; - _LUser -> ok - end. - --spec(listmech/1 :: -( - Host ::binary()) - -> Mechanisms::mechanisms() -). - -listmech(Host) -> - Mechs = ets:select(sasl_mechanism, - [{#sasl_mechanism{mechanism = '$1', - password_type = '$2', _ = '_'}, - case catch ejabberd_auth:store_type(Host) of - external -> [{'==', '$2', plain}]; - scram -> [{'/=', '$2', digest}]; - {'EXIT', {undef, [{Module, store_type, []} | _]}} -> - ?WARNING_MSG("~p doesn't implement the function store_type/0", - [Module]), - []; - _Else -> [] - end, - ['$1']}]), - filter_anonymous(Host, Mechs). - -server_new(Service, ServerFQDN, UserRealm, _SecFlags, - GetPassword, CheckPassword, CheckPasswordDigest) -> - #sasl_state{service = Service, myname = ServerFQDN, - realm = UserRealm, get_password = GetPassword, - check_password = CheckPassword, - check_password_digest = CheckPasswordDigest}. - -server_start(State, Mech, ClientIn) -> - case lists:member(Mech, - listmech(State#sasl_state.myname)) - of - true -> - case ets:lookup(sasl_mechanism, Mech) of - [#sasl_mechanism{module = Module}] -> - {ok, MechState} = - Module:mech_new(State#sasl_state.myname, - State#sasl_state.get_password, - State#sasl_state.check_password, - State#sasl_state.check_password_digest), - server_step(State#sasl_state{mech_mod = Module, - mech_state = MechState}, - ClientIn); - _ -> {error, <<"no-mechanism">>} - end; - false -> {error, <<"no-mechanism">>} - end. - -server_step(State, ClientIn) -> - Module = State#sasl_state.mech_mod, - MechState = State#sasl_state.mech_state, - case Module:mech_step(MechState, ClientIn) of - {ok, Props} -> - case check_credentials(State, Props) of - ok -> {ok, Props}; - {error, Error} -> {error, Error} - end; - {ok, Props, ServerOut} -> - case check_credentials(State, Props) of - ok -> {ok, Props, ServerOut}; - {error, Error} -> {error, Error} - end; - {continue, ServerOut, NewMechState} -> - {continue, ServerOut, State#sasl_state{mech_state = NewMechState}}; - {error, Error, Username} -> - {error, Error, Username}; - {error, Error} -> - {error, Error} - end. - -%% Remove the anonymous mechanism from the list if not enabled for the given -%% host -%% --spec(filter_anonymous/2 :: -( - Host :: binary(), - Mechs :: mechanisms()) - -> mechanisms() -). - -filter_anonymous(Host, Mechs) -> - case ejabberd_auth_anonymous:is_sasl_anonymous_enabled(Host) of - true -> Mechs; - false -> Mechs -- [<<"ANONYMOUS">>] - end. - --spec(is_disabled/1 :: -( - Mechanism :: mechanism()) - -> boolean() -). - -is_disabled(Mechanism) -> - Disabled = ejabberd_config:get_option( - disable_sasl_mechanisms, - fun(V) when is_list(V) -> - lists:map(fun(M) -> str:to_upper(M) end, V); - (V) -> - [str:to_upper(V)] - end, []), - lists:member(Mechanism, Disabled). diff --git a/src/cyrsasl_anonymous.erl b/src/cyrsasl_anonymous.erl deleted file mode 100644 index 51d5db9d8..000000000 --- a/src/cyrsasl_anonymous.erl +++ /dev/null @@ -1,51 +0,0 @@ -%%%---------------------------------------------------------------------- -%%% File : cyrsasl_anonymous.erl -%%% Author : Magnus Henoch -%%% Purpose : ANONYMOUS SASL mechanism -%%% See http://www.ietf.org/internet-drafts/draft-ietf-sasl-anon-05.txt -%%% Created : 23 Aug 2005 by Magnus Henoch -%%% -%%% -%%% ejabberd, Copyright (C) 2002-2015 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(cyrsasl_anonymous). - --export([start/1, stop/0, mech_new/4, mech_step/2]). - --behaviour(cyrsasl). - --record(state, {server = <<"">> :: binary()}). - -start(_Opts) -> - cyrsasl:register_mechanism(<<"ANONYMOUS">>, ?MODULE, plain), - ok. - -stop() -> ok. - -mech_new(Host, _GetPassword, _CheckPassword, _CheckPasswordDigest) -> - {ok, #state{server = Host}}. - -mech_step(#state{server = Server}, _ClientIn) -> - User = iolist_to_binary([randoms:get_string() - | [jlib:integer_to_binary(X) - || X <- tuple_to_list(now())]]), - case ejabberd_auth:is_user_exists(User, Server) of - true -> {error, <<"not-authorized">>}; - false -> {ok, [{username, User}, {auth_module, ejabberd_auth_anonymous}]} - end. diff --git a/src/cyrsasl_digest.erl b/src/cyrsasl_digest.erl deleted file mode 100644 index 10f964130..000000000 --- a/src/cyrsasl_digest.erl +++ /dev/null @@ -1,258 +0,0 @@ -%%%---------------------------------------------------------------------- -%%% File : cyrsasl_digest.erl -%%% Author : Alexey Shchepin -%%% Purpose : DIGEST-MD5 SASL mechanism -%%% Created : 11 Mar 2003 by Alexey Shchepin -%%% -%%% -%%% ejabberd, Copyright (C) 2002-2015 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(cyrsasl_digest). - --author('alexey@sevcom.net'). - --export([start/1, stop/0, mech_new/4, mech_step/2, parse/1]). - --include("ejabberd.hrl"). --include("logger.hrl"). - --behaviour(cyrsasl). - --type get_password_fun() :: fun((binary()) -> {false, any()} | - {binary(), atom()}). - --type check_password_fun() :: fun((binary(), binary(), binary(), - fun((binary()) -> binary())) -> - {boolean(), any()} | - false). - --record(state, {step = 1 :: 1 | 3 | 5, - nonce = <<"">> :: binary(), - username = <<"">> :: binary(), - authzid = <<"">> :: binary(), - get_password = fun(_) -> {false, <<>>} end :: get_password_fun(), - check_password = fun(_, _, _, _) -> false end :: check_password_fun(), - auth_module :: atom(), - host = <<"">> :: binary(), - hostfqdn = <<"">> :: binary()}). - -start(_Opts) -> - Fqdn = get_local_fqdn(), - ?INFO_MSG("FQDN used to check DIGEST-MD5 SASL authentication: ~s", - [Fqdn]), - cyrsasl:register_mechanism(<<"DIGEST-MD5">>, ?MODULE, - digest). - -stop() -> ok. - -mech_new(Host, GetPassword, _CheckPassword, - CheckPasswordDigest) -> - {ok, - #state{step = 1, nonce = randoms:get_string(), - host = Host, hostfqdn = get_local_fqdn(), - get_password = GetPassword, - check_password = CheckPasswordDigest}}. - -mech_step(#state{step = 1, nonce = Nonce} = State, _) -> - {continue, - <<"nonce=\"", Nonce/binary, - "\",qop=\"auth\",charset=utf-8,algorithm=md5-sess">>, - State#state{step = 3}}; -mech_step(#state{step = 3, nonce = Nonce} = State, - ClientIn) -> - case parse(ClientIn) of - bad -> {error, <<"bad-protocol">>}; - KeyVals -> - DigestURI = proplists:get_value(<<"digest-uri">>, KeyVals, <<>>), - %DigestURI = xml:get_attr_s(<<"digest-uri">>, KeyVals), - UserName = proplists:get_value(<<"username">>, KeyVals, <<>>), - %UserName = xml:get_attr_s(<<"username">>, KeyVals), - case is_digesturi_valid(DigestURI, State#state.host, - State#state.hostfqdn) - of - false -> - ?DEBUG("User login not authorized because digest-uri " - "seems invalid: ~p (checking for Host " - "~p, FQDN ~p)", - [DigestURI, State#state.host, State#state.hostfqdn]), - {error, <<"not-authorized">>, UserName}; - true -> - AuthzId = proplists:get_value(<<"authzid">>, KeyVals, <<>>), - %AuthzId = xml:get_attr_s(<<"authzid">>, KeyVals), - case (State#state.get_password)(UserName) of - {false, _} -> {error, <<"not-authorized">>, UserName}; - {Passwd, AuthModule} -> - case (State#state.check_password)(UserName, <<"">>, - proplists:get_value(<<"response">>, KeyVals, <<>>), - %xml:get_attr_s(<<"response">>, KeyVals), - fun (PW) -> - response(KeyVals, - UserName, - PW, - Nonce, - AuthzId, - <<"AUTHENTICATE">>) - end) - of - {true, _} -> - RspAuth = response(KeyVals, UserName, Passwd, Nonce, - AuthzId, <<"">>), - {continue, <<"rspauth=", RspAuth/binary>>, - State#state{step = 5, auth_module = AuthModule, - username = UserName, - authzid = AuthzId}}; - false -> {error, <<"not-authorized">>, UserName}; - {false, _} -> {error, <<"not-authorized">>, UserName} - end - end - end - end; -mech_step(#state{step = 5, auth_module = AuthModule, - username = UserName, authzid = AuthzId}, - <<"">>) -> - {ok, - [{username, UserName}, {authzid, AuthzId}, - {auth_module, AuthModule}]}; -mech_step(A, B) -> - ?DEBUG("SASL DIGEST: A ~p B ~p", [A, B]), - {error, <<"bad-protocol">>}. - -parse(S) -> parse1(binary_to_list(S), "", []). - -parse1([$= | Cs], S, Ts) -> - parse2(Cs, lists:reverse(S), "", Ts); -parse1([$, | Cs], [], Ts) -> parse1(Cs, [], Ts); -parse1([$\s | Cs], [], Ts) -> parse1(Cs, [], Ts); -parse1([C | Cs], S, Ts) -> parse1(Cs, [C | S], Ts); -parse1([], [], T) -> lists:reverse(T); -parse1([], _S, _T) -> bad. - -parse2([$" | Cs], Key, Val, Ts) -> - parse3(Cs, Key, Val, Ts); -parse2([C | Cs], Key, Val, Ts) -> - parse4(Cs, Key, [C | Val], Ts); -parse2([], _, _, _) -> bad. - -parse3([$" | Cs], Key, Val, Ts) -> - parse4(Cs, Key, Val, Ts); -parse3([$\\, C | Cs], Key, Val, Ts) -> - parse3(Cs, Key, [C | Val], Ts); -parse3([C | Cs], Key, Val, Ts) -> - parse3(Cs, Key, [C | Val], Ts); -parse3([], _, _, _) -> bad. - -parse4([$, | Cs], Key, Val, Ts) -> - parse1(Cs, "", [{list_to_binary(Key), list_to_binary(lists:reverse(Val))} | Ts]); -parse4([$\s | Cs], Key, Val, Ts) -> - parse4(Cs, Key, Val, Ts); -parse4([C | Cs], Key, Val, Ts) -> - parse4(Cs, Key, [C | Val], Ts); -parse4([], Key, Val, Ts) -> -%% @doc Check if the digest-uri is valid. -%% RFC-2831 allows to provide the IP address in Host, -%% however ejabberd doesn't allow that. -%% If the service (for example jabber.example.org) -%% is provided by several hosts (being one of them server3.example.org), -%% then acceptable digest-uris would be: -%% xmpp/server3.example.org/jabber.example.org, xmpp/server3.example.org and -%% xmpp/jabber.example.org -%% The last version is not actually allowed by the RFC, but implemented by popular clients - parse1([], "", [{list_to_binary(Key), list_to_binary(lists:reverse(Val))} | Ts]). - -is_digesturi_valid(DigestURICase, JabberDomain, - JabberFQDN) -> - DigestURI = stringprep:tolower(DigestURICase), - case catch str:tokens(DigestURI, <<"/">>) of - [<<"xmpp">>, Host] -> - IsHostFqdn = is_host_fqdn(binary_to_list(Host), binary_to_list(JabberFQDN)), - (Host == JabberDomain) or IsHostFqdn; - [<<"xmpp">>, Host, ServName] -> - IsHostFqdn = is_host_fqdn(binary_to_list(Host), binary_to_list(JabberFQDN)), - (ServName == JabberDomain) and IsHostFqdn; - _ -> - false - end. - -is_host_fqdn(Host, [Letter | _Tail] = Fqdn) when not is_list(Letter) -> - Host == Fqdn; -is_host_fqdn(_Host, []) -> - false; -is_host_fqdn(Host, [Fqdn | _FqdnTail]) when Host == Fqdn -> - true; -is_host_fqdn(Host, [Fqdn | FqdnTail]) when Host /= Fqdn -> - is_host_fqdn(Host, FqdnTail). - -get_local_fqdn() -> - case catch get_local_fqdn2() of - Str when is_binary(Str) -> Str; - _ -> - <<"unknown-fqdn, please configure fqdn " - "option in ejabberd.yml!">> - end. - -get_local_fqdn2() -> - case ejabberd_config:get_option( - fqdn, fun iolist_to_binary/1) of - ConfiguredFqdn when is_binary(ConfiguredFqdn) -> - ConfiguredFqdn; - undefined -> - {ok, Hostname} = inet:gethostname(), - {ok, {hostent, Fqdn, _, _, _, _}} = - inet:gethostbyname(Hostname), - list_to_binary(Fqdn) - end. - -hex(S) -> - p1_sha:to_hexlist(S). - -proplists_get_bin_value(Key, Pairs, Default) -> - case proplists:get_value(Key, Pairs, Default) of - L when is_list(L) -> - list_to_binary(L); - L2 -> - L2 - end. - -response(KeyVals, User, Passwd, Nonce, AuthzId, - A2Prefix) -> - Realm = proplists_get_bin_value(<<"realm">>, KeyVals, <<>>), - CNonce = proplists_get_bin_value(<<"cnonce">>, KeyVals, <<>>), - DigestURI = proplists_get_bin_value(<<"digest-uri">>, KeyVals, <<>>), - NC = proplists_get_bin_value(<<"nc">>, KeyVals, <<>>), - QOP = proplists_get_bin_value(<<"qop">>, KeyVals, <<>>), - MD5Hash = erlang:md5(<>), - A1 = case AuthzId of - <<"">> -> - <>; - _ -> - <> - end, - A2 = case QOP of - <<"auth">> -> - <>; - _ -> - <> - end, - T = <<(hex((erlang:md5(A1))))/binary, ":", Nonce/binary, - ":", NC/binary, ":", CNonce/binary, ":", QOP/binary, - ":", (hex((erlang:md5(A2))))/binary>>, - hex((erlang:md5(T))). diff --git a/src/cyrsasl_plain.erl b/src/cyrsasl_plain.erl deleted file mode 100644 index d2fb373e4..000000000 --- a/src/cyrsasl_plain.erl +++ /dev/null @@ -1,89 +0,0 @@ -%%%---------------------------------------------------------------------- -%%% File : cyrsasl_plain.erl -%%% Author : Alexey Shchepin -%%% Purpose : PLAIN SASL mechanism -%%% Created : 8 Mar 2003 by Alexey Shchepin -%%% -%%% -%%% ejabberd, Copyright (C) 2002-2015 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(cyrsasl_plain). - --author('alexey@process-one.net'). - --export([start/1, stop/0, mech_new/4, mech_step/2, parse/1]). - --behaviour(cyrsasl). - --record(state, {check_password}). - -start(_Opts) -> - cyrsasl:register_mechanism(<<"PLAIN">>, ?MODULE, plain), - ok. - -stop() -> ok. - -mech_new(_Host, _GetPassword, CheckPassword, _CheckPasswordDigest) -> - {ok, #state{check_password = CheckPassword}}. - -mech_step(State, ClientIn) -> - case prepare(ClientIn) of - [AuthzId, User, Password] -> - case (State#state.check_password)(User, Password) of - {true, AuthModule} -> - {ok, - [{username, User}, {authzid, AuthzId}, - {auth_module, AuthModule}]}; - _ -> {error, <<"not-authorized">>, User} - end; - _ -> {error, <<"bad-protocol">>} - end. - -prepare(ClientIn) -> - case parse(ClientIn) of - [<<"">>, UserMaybeDomain, Password] -> - case parse_domain(UserMaybeDomain) of - %% login@domainpwd - [User, _Domain] -> [UserMaybeDomain, User, Password]; - %% loginpwd - [User] -> [<<"">>, User, Password] - end; - %% login@domainloginpwd - [AuthzId, User, Password] -> [AuthzId, User, Password]; - _ -> error - end. - -parse(S) -> parse1(binary_to_list(S), "", []). - -parse1([0 | Cs], S, T) -> - parse1(Cs, "", [list_to_binary(lists:reverse(S)) | T]); -parse1([C | Cs], S, T) -> parse1(Cs, [C | S], T); -%parse1([], [], T) -> -% lists:reverse(T); -parse1([], S, T) -> - lists:reverse([list_to_binary(lists:reverse(S)) | T]). - -parse_domain(S) -> parse_domain1(binary_to_list(S), "", []). - -parse_domain1([$@ | Cs], S, T) -> - parse_domain1(Cs, "", [list_to_binary(lists:reverse(S)) | T]); -parse_domain1([C | Cs], S, T) -> - parse_domain1(Cs, [C | S], T); -parse_domain1([], S, T) -> - lists:reverse([list_to_binary(lists:reverse(S)) | T]). diff --git a/src/cyrsasl_scram.erl b/src/cyrsasl_scram.erl deleted file mode 100644 index 1fd7c1be5..000000000 --- a/src/cyrsasl_scram.erl +++ /dev/null @@ -1,218 +0,0 @@ -%%%---------------------------------------------------------------------- -%%% File : cyrsasl_scram.erl -%%% Author : Stephen Röttger -%%% Purpose : SASL SCRAM authentication -%%% Created : 7 Aug 2011 by Stephen Röttger -%%% -%%% -%%% ejabberd, Copyright (C) 2002-2015 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(cyrsasl_scram). - --author('stephen.roettger@googlemail.com'). - --export([start/1, stop/0, mech_new/4, mech_step/2]). - --include("ejabberd.hrl"). --include("logger.hrl"). - --include("jlib.hrl"). - --behaviour(cyrsasl). - --record(state, - {step = 2 :: 2 | 4, - stored_key = <<"">> :: binary(), - server_key = <<"">> :: binary(), - username = <<"">> :: binary(), - get_password :: fun(), - check_password :: fun(), - auth_message = <<"">> :: binary(), - client_nonce = <<"">> :: binary(), - server_nonce = <<"">> :: binary()}). - --define(SALT_LENGTH, 16). - --define(NONCE_LENGTH, 16). - -start(_Opts) -> - cyrsasl:register_mechanism(<<"SCRAM-SHA-1">>, ?MODULE, - scram). - -stop() -> ok. - -mech_new(_Host, GetPassword, _CheckPassword, - _CheckPasswordDigest) -> - {ok, #state{step = 2, get_password = GetPassword}}. - -mech_step(#state{step = 2} = State, ClientIn) -> - case re:split(ClientIn, <<",">>, [{return, binary}]) of - [_CBind, _AuthorizationIdentity, _UserNameAttribute, _ClientNonceAttribute, ExtensionAttribute | _] - when ExtensionAttribute /= [] -> - {error, <<"protocol-error-extension-not-supported">>}; - [CBind, _AuthorizationIdentity, UserNameAttribute, ClientNonceAttribute | _] - when (CBind == <<"y">>) or (CBind == <<"n">>) -> - case parse_attribute(UserNameAttribute) of - {error, Reason} -> {error, Reason}; - {_, EscapedUserName} -> - case unescape_username(EscapedUserName) of - error -> {error, <<"protocol-error-bad-username">>}; - UserName -> - case parse_attribute(ClientNonceAttribute) of - {$r, ClientNonce} -> - {Ret, _AuthModule} = (State#state.get_password)(UserName), - case {Ret, jlib:resourceprep(Ret)} of - {false, _} -> {error, <<"not-authorized">>, UserName}; - {_, error} when is_binary(Ret) -> ?WARNING_MSG("invalid plain password", []), {error, <<"not-authorized">>, UserName}; - {Ret, _} -> - {StoredKey, ServerKey, Salt, IterationCount} = - if is_tuple(Ret) -> Ret; - true -> - TempSalt = - crypto:rand_bytes(?SALT_LENGTH), - SaltedPassword = - scram:salted_password(Ret, - TempSalt, - ?SCRAM_DEFAULT_ITERATION_COUNT), - {scram:stored_key(scram:client_key(SaltedPassword)), - scram:server_key(SaltedPassword), - TempSalt, - ?SCRAM_DEFAULT_ITERATION_COUNT} - end, - ClientFirstMessageBare = - str:substr(ClientIn, - str:str(ClientIn, <<"n=">>)), - ServerNonce = - jlib:encode_base64(crypto:rand_bytes(?NONCE_LENGTH)), - ServerFirstMessage = - iolist_to_binary( - ["r=", - ClientNonce, - ServerNonce, - ",", "s=", - jlib:encode_base64(Salt), - ",", "i=", - integer_to_list(IterationCount)]), - {continue, ServerFirstMessage, - State#state{step = 4, stored_key = StoredKey, - server_key = ServerKey, - auth_message = - <>, - client_nonce = ClientNonce, - server_nonce = ServerNonce, - username = UserName}} - end; - _Else -> {error, <<"not-supported">>} - end - end - end; - _Else -> {error, <<"bad-protocol">>} - end; -mech_step(#state{step = 4} = State, ClientIn) -> - case str:tokens(ClientIn, <<",">>) of - [GS2ChannelBindingAttribute, NonceAttribute, - ClientProofAttribute] -> - case parse_attribute(GS2ChannelBindingAttribute) of - {$c, CVal} -> - ChannelBindingSupport = binary:at(jlib:decode_base64(CVal), 0), - if (ChannelBindingSupport == $n) - or (ChannelBindingSupport == $y) -> - Nonce = <<(State#state.client_nonce)/binary, - (State#state.server_nonce)/binary>>, - case parse_attribute(NonceAttribute) of - {$r, CompareNonce} when CompareNonce == Nonce -> - case parse_attribute(ClientProofAttribute) of - {$p, ClientProofB64} -> - ClientProof = jlib:decode_base64(ClientProofB64), - AuthMessage = iolist_to_binary( - [State#state.auth_message, - ",", - str:substr(ClientIn, 1, - str:str(ClientIn, <<",p=">>) - - 1)]), - ClientSignature = - scram:client_signature(State#state.stored_key, - AuthMessage), - ClientKey = scram:client_key(ClientProof, - ClientSignature), - CompareStoredKey = scram:stored_key(ClientKey), - if CompareStoredKey == State#state.stored_key -> - ServerSignature = - scram:server_signature(State#state.server_key, - AuthMessage), - {ok, [{username, State#state.username}], - <<"v=", - (jlib:encode_base64(ServerSignature))/binary>>}; - true -> {error, <<"bad-auth">>} - end; - _Else -> {error, <<"bad-protocol">>} - end; - {$r, _} -> {error, <<"bad-nonce">>}; - _Else -> {error, <<"bad-protocol">>} - end; - true -> {error, <<"bad-channel-binding">>} - end; - _Else -> {error, <<"bad-protocol">>} - end; - _Else -> {error, <<"bad-protocol">>} - end. - -parse_attribute(Attribute) -> - AttributeLen = byte_size(Attribute), - if AttributeLen >= 3 -> - AttributeS = binary_to_list(Attribute), - SecondChar = lists:nth(2, AttributeS), - case is_alpha(lists:nth(1, AttributeS)) of - true -> - if SecondChar == $= -> - String = str:substr(Attribute, 3), - {lists:nth(1, AttributeS), String}; - true -> {error, <<"bad-format second char not equal sign">>} - end; - _Else -> {error, <<"bad-format first char not a letter">>} - end; - true -> {error, <<"bad-format attribute too short">>} - end. - -unescape_username(<<"">>) -> <<"">>; -unescape_username(EscapedUsername) -> - Pos = str:str(EscapedUsername, <<"=">>), - if Pos == 0 -> EscapedUsername; - true -> - Start = str:substr(EscapedUsername, 1, Pos - 1), - End = str:substr(EscapedUsername, Pos), - EndLen = byte_size(End), - if EndLen < 3 -> error; - true -> - case str:substr(End, 1, 3) of - <<"=2C">> -> - <>; - <<"=3D">> -> - < + Mods = lists:filter( + fun(M) -> + case atom_to_list(M) of + "mod_" ++ _ -> true; + "Elixir.Mod" ++ _ -> true; + _ -> false + end + end, ejabberd_config:beams(all)), + format("~ts: unknown ~ts: ~ts. Did you mean ~ts?", + [yconf:format_ctx(Ctx), + format_module_type(Ctx), + format_module(Mod), + format_module(misc:best_match(Mod, Mods))]); +format_error({bad_export, {F, A}, Mod}, Ctx) + when Ctx == [listen, module]; + Ctx == [listen, request_handlers]; + Ctx == [modules] -> + Type = format_module_type(Ctx), + Slogan = yconf:format_ctx(Ctx), + case lists:member(Mod, ejabberd_config:beams(local)) of + true -> + format("~ts: '~ts' is not a ~ts", + [Slogan, format_module(Mod), Type]); + false -> + case lists:member(Mod, ejabberd_config:beams(external)) of + true -> + format("~ts: third-party ~ts '~ts' doesn't export " + "function ~ts/~B. If it's really a ~ts, " + "consider to upgrade it", + [Slogan, Type, format_module(Mod),F, A, Type]); + false -> + format("~ts: '~ts' doesn't match any known ~ts", + [Slogan, format_module(Mod), Type]) + end + end; +format_error({unknown_option, [], _} = Why, Ctx) -> + format("~ts. There are no available options", + [yconf:format_error(Why, Ctx)]); +format_error({unknown_option, Known, Opt} = Why, Ctx) -> + format("~ts. Did you mean ~ts? ~ts", + [yconf:format_error(Why, Ctx), + misc:best_match(Opt, Known), + format_known("Available options", Known)]); +format_error({bad_enum, Known, Bad} = Why, Ctx) -> + format("~ts. Did you mean ~ts? ~ts", + [yconf:format_error(Why, Ctx), + misc:best_match(Bad, Known), + format_known("Possible values", Known)]); +format_error({bad_yaml, _, _} = Why, _) -> + format_error(Why); +format_error(Reason, Ctx) -> + yconf:format_ctx(Ctx) ++ ": " ++ format_error(Reason). + +format_error({bad_db_type, _, Atom}) -> + format("unsupported database: ~ts", [Atom]); +format_error({bad_lang, Lang}) -> + format("Invalid language tag: ~ts", [Lang]); +format_error({bad_pem, Why, Path}) -> + format("Failed to read PEM file '~ts': ~ts", + [Path, pkix:format_error(Why)]); +format_error({bad_cert, Why, Path}) -> + format_error({bad_pem, Why, Path}); +format_error({bad_jwt_key, Path}) -> + format("No valid JWT key found in file: ~ts", [Path]); +format_error({bad_jwt_key_set, Path}) -> + format("JWK set contains multiple JWT keys in file: ~ts", [Path]); +format_error({bad_jid, Bad}) -> + format("Invalid XMPP address: ~ts", [Bad]); +format_error({bad_user, Bad}) -> + format("Invalid user part: ~ts", [Bad]); +format_error({bad_domain, Bad}) -> + format("Invalid domain: ~ts", [Bad]); +format_error({bad_resource, Bad}) -> + format("Invalid resource part: ~ts", [Bad]); +format_error({bad_ldap_filter, Bad}) -> + format("Invalid LDAP filter: ~ts", [Bad]); +format_error({bad_sip_uri, Bad}) -> + format("Invalid SIP URI: ~ts", [Bad]); +format_error({route_conflict, R}) -> + format("Failed to reuse route '~ts' because it's " + "already registered on a virtual host", + [R]); +format_error({listener_dup, AddrPort}) -> + format("Overlapping listeners found at ~ts", + [format_addr_port(AddrPort)]); +format_error({listener_conflict, AddrPort1, AddrPort2}) -> + format("Overlapping listeners found at ~ts and ~ts", + [format_addr_port(AddrPort1), + format_addr_port(AddrPort2)]); +format_error({invalid_syntax, Reason}) -> + format("~ts", [Reason]); +format_error({missing_module_dep, Mod, DepMod}) -> + format("module ~ts depends on module ~ts, " + "which is not found in the config", + [Mod, DepMod]); +format_error(eimp_error) -> + format("ejabberd is built without image converter support", []); +format_error({mqtt_codec, Reason}) -> + mqtt_codec:format_error(Reason); +format_error({external_module_error, Module, Error}) -> + try Module:format_error(Error) + catch _:_ -> + format("Invalid value", []) + end; +format_error(Reason) -> + yconf:format_error(Reason). + +-spec format_module(atom() | string()) -> string(). +format_module(Mod) when is_atom(Mod) -> + format_module(atom_to_list(Mod)); +format_module(Mod) -> + case Mod of + "Elixir." ++ M -> M; + M -> M + end. + +format_module_type([listen, module]) -> + "listening module"; +format_module_type([listen, request_handlers]) -> + "HTTP request handler"; +format_module_type([modules]) -> + "ejabberd module". + +format_known(_, Known) when length(Known) > 20 -> + ""; +format_known(Prefix, Known) -> + [Prefix, " are: ", format_join(Known)]. + +format_join([]) -> + "(empty)"; +format_join([H|_] = L) when is_atom(H) -> + format_join([atom_to_binary(A, utf8) || A <- L]); +format_join(L) -> + str:join(lists:sort(L), <<", ">>). + +%% All duplicated options having list-values are grouped +%% into a single option with all list-values being concatenated +-spec group_dups(list(T)) -> list(T). +group_dups(Y1) -> + lists:reverse( + lists:foldl( + fun({Option, Values}, Acc) when is_list(Values) -> + case lists:keyfind(Option, 1, Acc) of + {Option, Vals} when is_list(Vals) -> + lists:keyreplace(Option, 1, Acc, {Option, Vals ++ Values}); + _ -> + [{Option, Values}|Acc] + end; + (Other, Acc) -> + [Other|Acc] + end, [], Y1)). + +%%%=================================================================== +%%% Validators from yconf +%%%=================================================================== +pos_int() -> + yconf:pos_int(). + +pos_int(Inf) -> + yconf:pos_int(Inf). + +non_neg_int() -> + yconf:non_neg_int(). + +non_neg_int(Inf) -> + yconf:non_neg_int(Inf). + +int() -> + yconf:int(). + +int(Min, Max) -> + yconf:int(Min, Max). + +number(Min) -> + yconf:number(Min). + +octal() -> + yconf:octal(). + +binary() -> + yconf:binary(). + +binary(Re) -> + yconf:binary(Re). + +binary(Re, Opts) -> + yconf:binary(Re, Opts). + +enum(L) -> + yconf:enum(L). + +bool() -> + yconf:bool(). + +atom() -> + yconf:atom(). + +string() -> + yconf:string(). + +string(Re) -> + yconf:string(Re). + +string(Re, Opts) -> + yconf:string(Re, Opts). + +any() -> + yconf:any(). + +url() -> + yconf:url(). + +url(Schemes) -> + yconf:url(Schemes). + +file() -> + yconf:file(). + +file(Type) -> + yconf:file(Type). + +directory() -> + yconf:directory(). + +directory(Type) -> + yconf:directory(Type). + +ip() -> + yconf:ip(). + +ipv4() -> + yconf:ipv4(). + +ipv6() -> + yconf:ipv6(). + +ip_mask() -> + yconf:ip_mask(). + +port() -> + yconf:port(). + +re() -> + yconf:re(). + +re(Opts) -> + yconf:re(Opts). + +glob() -> + yconf:glob(). + +glob(Opts) -> + yconf:glob(Opts). + +path() -> + yconf:path(). + +binary_sep(Sep) -> + yconf:binary_sep(Sep). + +timeout(Units) -> + yconf:timeout(Units). + +timeout(Units, Inf) -> + yconf:timeout(Units, Inf). + +base64() -> + yconf:base64(). + +non_empty(F) -> + yconf:non_empty(F). + +list(F) -> + yconf:list(F). + +list(F, Opts) -> + yconf:list(F, Opts). + +list_or_single(F) -> + yconf:list_or_single(F). + +list_or_single(F, Opts) -> + yconf:list_or_single(F, Opts). + +map(F1, F2) -> + yconf:map(F1, F2). + +map(F1, F2, Opts) -> + yconf:map(F1, F2, Opts). + +either(F1, F2) -> + yconf:either(F1, F2). + +and_then(F1, F2) -> + yconf:and_then(F1, F2). + +options(V) -> + yconf:options(V). + +options(V, O) -> + yconf:options(V, O). + +%%%=================================================================== +%%% Custom validators +%%%=================================================================== +beam() -> + beam([]). + +beam(Exports) -> + and_then( + non_empty(binary()), + fun(<<"Elixir.", _/binary>> = Val) -> + (yconf:beam(Exports))(Val); + (<> = Val) when C >= $A, C =< $Z -> + (yconf:beam(Exports))(<<"Elixir.", Val/binary>>); + (Val) -> + (yconf:beam(Exports))(Val) + end). + +acl() -> + either( + atom(), + acl:access_rules_validator()). + +shaper() -> + either( + atom(), + ejabberd_shaper:shaper_rules_validator()). + +-spec url_or_file() -> yconf:validator({file | url, binary()}). +url_or_file() -> + either( + and_then(url(), fun(URL) -> {url, URL} end), + and_then(file(), fun(File) -> {file, File} end)). + +-spec lang() -> yconf:validator(binary()). +lang() -> + and_then( + binary(), + fun(Lang) -> + try xmpp_lang:check(Lang) + catch _:_ -> fail({bad_lang, Lang}) + end + end). + +-spec pem() -> yconf:validator(binary()). +pem() -> + and_then( + path(), + fun(Path) -> + case pkix:is_pem_file(Path) of + true -> Path; + {false, Reason} -> + fail({bad_pem, Reason, Path}) + end + end). + +-spec jid() -> yconf:validator(jid:jid()). +jid() -> + and_then( + binary(), + fun(Val) -> + try jid:decode(Val) + catch _:{bad_jid, _} = Reason -> fail(Reason) + end + end). + +-spec user() -> yconf:validator(binary()). +user() -> + and_then( + binary(), + fun(Val) -> + case jid:nodeprep(Val) of + error -> fail({bad_user, Val}); + U -> U + end + end). + +-spec domain() -> yconf:validator(binary()). +domain() -> + and_then( + non_empty(binary()), + fun(Val) -> + try jid:tolower(jid:decode(Val)) of + {<<"">>, <<"xn--", _/binary>> = Domain, <<"">>} -> + unicode:characters_to_binary(idna:decode(binary_to_list(Domain)), utf8); + {<<"">>, Domain, <<"">>} -> Domain; + _ -> fail({bad_domain, Val}) + catch _:{bad_jid, _} -> + fail({bad_domain, Val}) + end + end). + +-spec resource() -> yconf:validator(binary()). +resource() -> + and_then( + binary(), + fun(Val) -> + case jid:resourceprep(Val) of + error -> fail({bad_resource, Val}); + R -> R + end + end). + +-spec db_type(module()) -> yconf:validator(atom()). +db_type(M) -> + and_then( + atom(), + fun(T) -> + case code:ensure_loaded(db_module(M, T)) of + {module, _} -> T; + {error, _} -> + ElixirModule = "Elixir." ++ atom_to_list(T), + case code:ensure_loaded(list_to_atom(ElixirModule)) of + {module, _} -> list_to_atom(ElixirModule); + {error, _} -> fail({bad_db_type, M, T}) + end + end + end). + +-spec queue_type() -> yconf:validator(ram | file). +queue_type() -> + enum([ram, file]). + +-spec ldap_filter() -> yconf:validator(binary()). +ldap_filter() -> + and_then( + binary(), + fun(Val) -> + case eldap_filter:parse(Val) of + {ok, _} -> Val; + _ -> fail({bad_ldap_filter, Val}) + end + end). + +-ifdef(SIP). +sip_uri() -> + and_then( + binary(), + fun(Val) -> + case esip:decode_uri(Val) of + error -> fail({bad_sip_uri, Val}); + URI -> URI + end + end). +-endif. + +-spec host() -> yconf:validator(binary()). +host() -> + fun(Domain) -> + Hosts = ejabberd_config:get_option(hosts), + Domain3 = (domain())(Domain), + case lists:member(Domain3, Hosts) of + true -> fail({route_conflict, Domain}); + false -> Domain3 + end + end. + +-spec hosts() -> yconf:validator([binary()]). +hosts() -> + list(host(), [unique]). + +-spec vcard_temp() -> yconf:validator(). +vcard_temp() -> + and_then( + vcard_validator( + vcard_temp, undefined, + [{version, undefined, binary()}, + {fn, undefined, binary()}, + {n, undefined, vcard_name()}, + {nickname, undefined, binary()}, + {photo, undefined, vcard_photo()}, + {bday, undefined, binary()}, + {adr, [], list(vcard_adr())}, + {label, [], list(vcard_label())}, + {tel, [], list(vcard_tel())}, + {email, [], list(vcard_email())}, + {jabberid, undefined, binary()}, + {mailer, undefined, binary()}, + {tz, undefined, binary()}, + {geo, undefined, vcard_geo()}, + {title, undefined, binary()}, + {role, undefined, binary()}, + {logo, undefined, vcard_logo()}, + {org, undefined, vcard_org()}, + {categories, [], list(binary())}, + {note, undefined, binary()}, + {prodid, undefined, binary()}, + {rev, undefined, binary()}, + {sort_string, undefined, binary()}, + {sound, undefined, vcard_sound()}, + {uid, undefined, binary()}, + {url, undefined, binary()}, + {class, undefined, enum([confidential, private, public])}, + {key, undefined, vcard_key()}, + {desc, undefined, binary()}]), + fun(Tuple) -> + list_to_tuple(tuple_to_list(Tuple) ++ [[]]) + end). + + +-spec vcard_name() -> yconf:validator(). +vcard_name() -> + vcard_validator( + vcard_name, undefined, + [{family, undefined, binary()}, + {given, undefined, binary()}, + {middle, undefined, binary()}, + {prefix, undefined, binary()}, + {suffix, undefined, binary()}]). + +-spec vcard_photo() -> yconf:validator(). +vcard_photo() -> + vcard_validator( + vcard_photo, undefined, + [{type, undefined, binary()}, + {binval, undefined, base64()}, + {extval, undefined, binary()}]). + +-spec vcard_adr() -> yconf:validator(). +vcard_adr() -> + vcard_validator( + vcard_adr, [], + [{home, false, bool()}, + {work, false, bool()}, + {postal, false, bool()}, + {parcel, false, bool()}, + {dom, false, bool()}, + {intl, false, bool()}, + {pref, false, bool()}, + {pobox, undefined, binary()}, + {extadd, undefined, binary()}, + {street, undefined, binary()}, + {locality, undefined, binary()}, + {region, undefined, binary()}, + {pcode, undefined, binary()}, + {ctry, undefined, binary()}]). + +-spec vcard_label() -> yconf:validator(). +vcard_label() -> + vcard_validator( + vcard_label, [], + [{home, false, bool()}, + {work, false, bool()}, + {postal, false, bool()}, + {parcel, false, bool()}, + {dom, false, bool()}, + {intl, false, bool()}, + {pref, false, bool()}, + {line, [], list(binary())}]). + +-spec vcard_tel() -> yconf:validator(). +vcard_tel() -> + vcard_validator( + vcard_tel, [], + [{home, false, bool()}, + {work, false, bool()}, + {voice, false, bool()}, + {fax, false, bool()}, + {pager, false, bool()}, + {msg, false, bool()}, + {cell, false, bool()}, + {video, false, bool()}, + {bbs, false, bool()}, + {modem, false, bool()}, + {isdn, false, bool()}, + {pcs, false, bool()}, + {pref, false, bool()}, + {number, undefined, binary()}]). + +-spec vcard_email() -> yconf:validator(). +vcard_email() -> + vcard_validator( + vcard_email, [], + [{home, false, bool()}, + {work, false, bool()}, + {internet, false, bool()}, + {pref, false, bool()}, + {x400, false, bool()}, + {userid, undefined, binary()}]). + +-spec vcard_geo() -> yconf:validator(). +vcard_geo() -> + vcard_validator( + vcard_geo, undefined, + [{lat, undefined, binary()}, + {lon, undefined, binary()}]). + +-spec vcard_logo() -> yconf:validator(). +vcard_logo() -> + vcard_validator( + vcard_logo, undefined, + [{type, undefined, binary()}, + {binval, undefined, base64()}, + {extval, undefined, binary()}]). + +-spec vcard_org() -> yconf:validator(). +vcard_org() -> + vcard_validator( + vcard_org, undefined, + [{name, undefined, binary()}, + {units, [], list(binary())}]). + +-spec vcard_sound() -> yconf:validator(). +vcard_sound() -> + vcard_validator( + vcard_sound, undefined, + [{phonetic, undefined, binary()}, + {binval, undefined, base64()}, + {extval, undefined, binary()}]). + +-spec vcard_key() -> yconf:validator(). +vcard_key() -> + vcard_validator( + vcard_key, undefined, + [{type, undefined, binary()}, + {cred, undefined, binary()}]). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +-spec db_module(module(), atom()) -> module(). +db_module(M, Type) -> + try list_to_atom(atom_to_list(M) ++ "_" ++ atom_to_list(Type)) + catch _:system_limit -> + fail({bad_length, 255}) + end. + +format_addr_port({IP, Port}) -> + IPStr = case tuple_size(IP) of + 4 -> inet:ntoa(IP); + 8 -> "[" ++ inet:ntoa(IP) ++ "]" + end, + IPStr ++ ":" ++ integer_to_list(Port). + +-spec format(iolist(), list()) -> string(). +format(Fmt, Args) -> + lists:flatten(io_lib:format(Fmt, Args)). + +-spec vcard_validator(atom(), term(), [{atom(), term(), validator()}]) -> validator(). +vcard_validator(Name, Default, Schema) -> + Defaults = [{Key, Val} || {Key, Val, _} <- Schema], + and_then( + options( + maps:from_list([{Key, Fun} || {Key, _, Fun} <- Schema]), + [{return, map}, {unique, true}]), + fun(Options) -> + merge(Defaults, Options, Name, Default) + end). + +-spec merge([{atom(), term()}], #{atom() => term()}, atom(), T) -> tuple() | T. +merge(_, Options, _, Default) when Options == #{} -> + Default; +merge(Defaults, Options, Name, _) -> + list_to_tuple([Name|[maps:get(Key, Options, Val) || {Key, Val} <- Defaults]]). diff --git a/src/ejabberd.app.src.in b/src/ejabberd.app.src.in deleted file mode 100644 index 4b0864831..000000000 --- a/src/ejabberd.app.src.in +++ /dev/null @@ -1,16 +0,0 @@ -%% $Id$ - -{application, ejabberd, - [{description, "@PACKAGE_NAME@"}, - {vsn, "@PACKAGE_VERSION@"}, - {modules, []}, - {registered, []}, - {applications, [kernel, stdlib]}, - {env, []}, - {mod, {ejabberd_app, []}}]}. - - -%% Local Variables: -%% mode: erlang -%% End: -%% vim: set filetype=erlang tabstop=8: diff --git a/src/ejabberd.app.src.script b/src/ejabberd.app.src.script new file mode 100644 index 000000000..a4e245461 --- /dev/null +++ b/src/ejabberd.app.src.script @@ -0,0 +1,56 @@ +{Vars, ElixirApps} = case file:consult(filename:join([filename:dirname(SCRIPT), "..", "vars.config"])) of + {ok, Terms} -> + Backends = [mssql, mysql, odbc, pgsql, redis, sqlite], + EBs = lists:filter(fun(Backend) -> lists:member({Backend, true}, Terms) end, Backends), + Elixirs = case proplists:get_bool(elixir, Terms) of + true -> [elixir, logger, mix]; + false -> [] + end, + + ProfileEnvironmentVariable = os:getenv("REBAR_PROFILE"), + AsProfiles = case lists:dropwhile(fun("as") -> false; (_) -> true end, + init:get_plain_arguments()) of + ["as", Profiles | _] -> string:split(Profiles, ","); + _ -> [] + end, + Terms2 = case lists:member("dev", [ProfileEnvironmentVariable | AsProfiles]) of + true -> lists:keystore(tools, 1, Terms, {tools, true}); + false -> Terms + end, + Tools = case lists:keyfind(tools, 1, Terms2) of + {tools, true} -> [observer]; + _ -> [] + end, + + {[lists:keyfind(description, 1, Terms), + lists:keyfind(vsn, 1, Terms), + {env, [{enabled_backends, EBs}]} + ], Elixirs ++ Tools}; + _Err -> + {[], []} + end, + +{application, ejabberd, + Vars ++ + [{modules, []}, + {registered, []}, + {applications, [kernel, sasl, ssl, stdlib, syntax_tools]}, + {included_applications, + [compiler, inets, mnesia, os_mon, + cache_tab, + eimp, + fast_tls, + fast_xml, + fast_yaml, + p1_acme, + p1_utils, + pkix, + stringprep, + yconf, + xmpp | ElixirApps]}, + {mod, {ejabberd_app, []}}]}. + +%% Local Variables: +%% mode: erlang +%% End: +%% vim: set filetype=erlang tabstop=8: diff --git a/src/ejabberd.erl b/src/ejabberd.erl index 64ac5a0da..844ef7ea2 100644 --- a/src/ejabberd.erl +++ b/src/ejabberd.erl @@ -5,7 +5,7 @@ %%% Created : 16 Nov 2002 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -25,21 +25,43 @@ -module(ejabberd). -author('alexey@process-one.net'). +-compile({no_auto_import, [{halt, 0}]}). --export([start/0, stop/0, start_app/1, start_app/2, - get_pid_file/0, check_app/1]). +-protocol({rfc, 6122}). +-protocol({rfc, 7590}). +-protocol({xep, 4, '2.9', '0.5.0', "complete", ""}). +-protocol({xep, 59, '1.0', '2.1.0', "complete", ""}). +-protocol({xep, 82, '1.1.1', '2.1.0', "complete", ""}). +-protocol({xep, 86, '1.0', '0.5.0', "complete", ""}). +-protocol({xep, 106, '1.1', '0.5.0', "complete", ""}). +-protocol({xep, 170, '1.0', '17.12', "complete", ""}). +-protocol({xep, 178, '1.1', '17.03', "complete", ""}). +-protocol({xep, 205, '1.0', '1.1.2', "complete", ""}). +-protocol({xep, 368, '1.1.0', '17.09', "complete", ""}). +-protocol({xep, 386, '0.3.0', '24.02', "complete", ""}). +-protocol({xep, 388, '0.4.0', '24.02', "complete", ""}). +-protocol({xep, 440, '0.4.0', '24.02', "complete", ""}). +-protocol({xep, 474, '0.4.0', '24.02', "complete", "0.4.0 since 25.03"}). + +-export([start/0, stop/0, halt/0, start_app/1, start_app/2, + get_pid_file/0, check_apps/0, module_name/1, is_loaded/0]). -include("logger.hrl"). start() -> - %%ejabberd_cover:start(), - application:start(ejabberd). + case application:ensure_all_started(ejabberd) of + {error, Err} -> error_logger:error_msg("Failed to start ejabberd application: ~p", [Err]); + Ok -> Ok + end. stop() -> application:stop(ejabberd). - %%ejabberd_cover:stop(). -%% @spec () -> false | string() +halt() -> + ejabberd_logger:flush(), + erlang:halt(1, [{flush, true}]). + +-spec get_pid_file() -> false | string(). get_pid_file() -> case os:getenv("EJABBERD_PID_PATH") of false -> @@ -57,21 +79,15 @@ start_app(App, Type) -> StartFlag = not is_loaded(), start_app(App, Type, StartFlag). -check_app(App) -> - StartFlag = not is_loaded(), - spawn(fun() -> check_app_modules(App, StartFlag) end), - ok. - is_loaded() -> Apps = application:which_applications(), lists:keymember(ejabberd, 1, Apps). -start_app(App, Type, StartFlag) when not is_list(App) -> +start_app(App, Type, StartFlag) when is_atom(App) -> start_app([App], Type, StartFlag); start_app([App|Apps], Type, StartFlag) -> - case application:start(App) of + case application:start(App,Type) of ok -> - spawn(fun() -> check_app_modules(App, StartFlag) end), start_app(Apps, Type, StartFlag); {error, {already_started, _}} -> start_app(Apps, Type, StartFlag); @@ -79,25 +95,23 @@ start_app([App|Apps], Type, StartFlag) -> case lists:member(DepApp, [App|Apps]) of true -> Reason = io_lib:format( - "failed to start application '~p': " - "circular dependency on '~p' detected", + "Failed to start Erlang application '~ts': " + "circular dependency with '~ts' detected", [App, DepApp]), exit_or_halt(Reason, StartFlag); false -> start_app([DepApp,App|Apps], Type, StartFlag) end; - Err -> - Reason = io_lib:format("failed to start application '~p': ~p", - [App, Err]), + {error, Why} -> + Reason = io_lib:format( + "Failed to start Erlang application '~ts': ~ts. ~ts", + [App, format_error(Why), hint()]), exit_or_halt(Reason, StartFlag) end; start_app([], _Type, _StartFlag) -> ok. check_app_modules(App, StartFlag) -> - {A, B, C} = now(), - random:seed(A, B, C), - sleep(5000), case application:get_key(App, modules) of {ok, Mods} -> lists:foreach( @@ -106,12 +120,12 @@ check_app_modules(App, StartFlag) -> non_existing -> File = get_module_file(App, Mod), Reason = io_lib:format( - "couldn't find module ~s " - "needed for application '~p'", - [File, App]), + "Couldn't find file ~ts needed " + "for Erlang application '~ts'. ~ts", + [File, App, hint()]), exit_or_halt(Reason, StartFlag); _ -> - sleep(10) + ok end end, Mods); _ -> @@ -119,24 +133,81 @@ check_app_modules(App, StartFlag) -> ok end. +check_apps() -> + spawn( + fun() -> + Apps = [ejabberd | + [App || {App, _, _} <- application:which_applications(), + App /= ejabberd, App /= hex]], + ?DEBUG("Checking consistency of applications: ~ts", + [misc:join_atoms(Apps, <<", ">>)]), + misc:peach( + fun(App) -> + check_app_modules(App, true) + end, Apps), + ?DEBUG("All applications are intact", []), + lists:foreach(fun erlang:garbage_collect/1, processes()) + end). + +-spec exit_or_halt(iodata(), boolean()) -> no_return(). exit_or_halt(Reason, StartFlag) -> ?CRITICAL_MSG(Reason, []), if StartFlag -> %% Wait for the critical message is written in the console/log - timer:sleep(1000), - halt(string:substr(lists:flatten(Reason), 1, 199)); + halt(); true -> erlang:error(application_start_failed) end. -sleep(N) -> - timer:sleep(random:uniform(N)). - get_module_file(App, Mod) -> BaseName = atom_to_list(Mod), - case code:lib_dir(App, ebin) of + case code:lib_dir(App) of {error, _} -> BaseName; Dir -> - filename:join([Dir, BaseName ++ ".beam"]) + filename:join([Dir, "ebin", BaseName ++ ".beam"]) end. + +module_name([Dir, _, <> | _] = Mod) when H >= 65, H =< 90 -> + Module = str:join([elixir_name(M) || M<-tl(Mod)], <<>>), + Prefix = case elixir_name(Dir) of + <<"Ejabberd">> -> <<"Elixir.Ejabberd.">>; + Lib -> <<"Elixir.Ejabberd.", Lib/binary, ".">> + end, + misc:binary_to_atom(<>); + +module_name([<<"auth">> | T] = Mod) -> + case hd(T) of + %% T already starts with "Elixir" if an Elixir module is + %% loaded with that name, as per `econf:db_type/1` + <<"Elixir", _/binary>> -> misc:binary_to_atom(hd(T)); + _ -> module_name([<<"ejabberd">>] ++ Mod) + end; + +module_name([<<"ejabberd">> | _] = Mod) -> + Module = str:join([erlang_name(M) || M<-Mod], $_), + misc:binary_to_atom(Module); +module_name(Mod) when is_list(Mod) -> + Module = str:join([erlang_name(M) || M<-tl(Mod)], $_), + misc:binary_to_atom(Module). + +elixir_name(Atom) when is_atom(Atom) -> + elixir_name(misc:atom_to_binary(Atom)); +elixir_name(<>) when H >= 65, H =< 90 -> + <>; +elixir_name(<>) -> + <<(H-32), T/binary>>. + +erlang_name(Atom) when is_atom(Atom) -> + misc:atom_to_binary(Atom); +erlang_name(Bin) when is_binary(Bin) -> + Bin. + +format_error({Reason, File}) when is_list(Reason), is_list(File) -> + Reason ++ ": " ++ File; +format_error(Term) -> + io_lib:format("~p", [Term]). + +hint() -> + "This usually means that ejabberd or Erlang " + "was compiled/installed incorrectly.". diff --git a/src/ejabberd_access_permissions.erl b/src/ejabberd_access_permissions.erl new file mode 100644 index 000000000..57b3637e3 --- /dev/null +++ b/src/ejabberd_access_permissions.erl @@ -0,0 +1,392 @@ +%%%------------------------------------------------------------------- +%%% File : ejabberd_access_permissions.erl +%%% Author : Paweł Chmielowski +%%% Purpose : Administrative functions and commands +%%% Created : 7 Sep 2016 by Paweł Chmielowski +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(ejabberd_access_permissions). +-author("pawel@process-one.net"). + +-include("ejabberd_commands.hrl"). +-include("logger.hrl"). + +-behaviour(gen_server). + +%% API +-export([start_link/0, + can_access/2, + invalidate/0, + validator/0, + show_current_definitions/0]). + +%% gen_server callbacks +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). + +-define(SERVER, ?MODULE). +-define(CACHE_TAB, access_permissions_cache). + +-record(state, + {definitions = none :: none | [definition()]}). + +-type state() :: #state{}. +-type rule() :: {access, acl:access()} | + {acl, all | none | acl:acl_rule()}. +-type what() :: all | none | [atom() | {tag, atom()}]. +-type who() :: rule() | {oauth, {[binary()], [rule()]}}. +-type from() :: atom(). +-type permission() :: {binary(), {[from()], [who()], {what(), what()}}}. +-type definition() :: {binary(), {[from()], [who()], [atom()] | all}}. +-type caller_info() :: #{caller_module => module(), + caller_host => global | binary(), + tag => binary() | none, + extra_permissions => [definition()], + atom() => term()}. + +-export_type([permission/0]). + +%%%=================================================================== +%%% API +%%%=================================================================== +-spec can_access(atom(), caller_info()) -> allow | deny. +can_access(Cmd, CallerInfo) -> + Defs0 = show_current_definitions(), + CallerModule = maps:get(caller_module, CallerInfo, none), + Host = maps:get(caller_host, CallerInfo, global), + Tag = maps:get(tag, CallerInfo, none), + Defs = maps:get(extra_permissions, CallerInfo, []) ++ Defs0, + Res = lists:foldl( + fun({Name, _} = Def, none) -> + case matches_definition(Def, Cmd, CallerModule, Tag, Host, CallerInfo) of + true -> + ?DEBUG("Command '~p' execution allowed by rule " + "'~ts'~n (CallerInfo=~p)", [Cmd, Name, CallerInfo]), + allow; + _ -> + none + end; + (_, Val) -> + Val + end, none, Defs), + case Res of + allow -> allow; + _ -> + ?DEBUG("Command '~p' execution denied~n " + "(CallerInfo=~p)", [Cmd, CallerInfo]), + deny + end. + +-spec invalidate() -> ok. +invalidate() -> + gen_server:cast(?MODULE, invalidate), + ets_cache:delete(?CACHE_TAB, definitions). + +-spec show_current_definitions() -> [definition()]. +show_current_definitions() -> + ets_cache:lookup(?CACHE_TAB, definitions, + fun() -> + {cache, gen_server:call(?MODULE, show_current_definitions)} + end). +start_link() -> + ets_cache:new(?CACHE_TAB, [{max_size, 2}]), + gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). + +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== +-spec init([]) -> {ok, state()}. +init([]) -> + ejabberd_hooks:add(config_reloaded, ?MODULE, invalidate, 90), + ets_cache:new(access_permissions), + {ok, #state{}}. + +-spec handle_call(show_current_definitions | term(), + term(), state()) -> {reply, term(), state()}. +handle_call(show_current_definitions, _From, State) -> + {State2, Defs} = get_definitions(State), + {reply, Defs, State2}; +handle_call(Request, From, State) -> + ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), + {noreply, State}. + +-spec handle_cast(invalidate | term(), state()) -> {noreply, state()}. +handle_cast(invalidate, State) -> + {noreply, State#state{definitions = none}}; +handle_cast(Msg, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Msg]), + {noreply, State}. + +handle_info(Info, State) -> + ?WARNING_MSG("Unexpected info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, _State) -> + ejabberd_hooks:delete(config_reloaded, ?MODULE, invalidate, 90). + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +-spec get_definitions(state()) -> {state(), [definition()]}. +get_definitions(#state{definitions = Defs} = State) when Defs /= none -> + {State, Defs}; +get_definitions(#state{definitions = none} = State) -> + ApiPerms = ejabberd_option:api_permissions(), + AllCommands = ejabberd_commands:get_commands_definition(), + NDefs0 = lists:map( + fun({Name, {From, Who, {Add, Del}}}) -> + Cmds = filter_commands_with_permissions(AllCommands, Add, Del), + {Name, {From, Who, Cmds}} + end, ApiPerms), + NDefs = case lists:keyfind(<<"console commands">>, 1, NDefs0) of + false -> + [{<<"console commands">>, + {[ejabberd_ctl], + [{acl, all}], + filter_commands_with_permissions(AllCommands, all, none)}} | NDefs0]; + _ -> + NDefs0 + end, + {State#state{definitions = NDefs}, NDefs}. + +-spec matches_definition(definition(), atom(), module(), + atom(), global | binary(), caller_info()) -> boolean(). +matches_definition({_Name, {From, Who, What}}, Cmd, Module, Tag, Host, CallerInfo) -> + case What == all orelse lists:member(Cmd, What) of + true -> + {Tags, Modules} = lists:partition(fun({tag, _}) -> true; (_) -> false end, From), + case (Modules == [] orelse lists:member(Module, Modules)) andalso + (Tags == [] orelse lists:member({tag, Tag}, Tags)) of + true -> + Scope = maps:get(oauth_scope, CallerInfo, none), + lists:any( + fun({access, Access}) when Scope == none -> + acl:match_rule(Host, Access, CallerInfo) == allow; + ({acl, Name} = Acl) when Scope == none, is_atom(Name) -> + acl:match_acl(Host, Acl, CallerInfo); + ({acl, Acl}) when Scope == none -> + acl:match_acl(Host, Acl, CallerInfo); + ({oauth, {Scopes, List}}) when Scope /= none -> + case ejabberd_oauth:scope_in_scope_list(Scope, Scopes) of + true -> + lists:any( + fun({access, Access}) -> + acl:match_rule(Host, Access, CallerInfo) == allow; + ({acl, Name} = Acl) when is_atom(Name) -> + acl:match_acl(Host, Acl, CallerInfo); + ({acl, Acl}) -> + acl:match_acl(Host, Acl, CallerInfo) + end, List); + _ -> + false + end; + (_) -> + false + end, Who); + _ -> + false + end; + _ -> + false + end. + +-spec filter_commands_with_permissions([#ejabberd_commands{}], what(), what()) -> [atom()]. +filter_commands_with_permissions(AllCommands, Add, Del) -> + CommandsAdd = filter_commands_with_patterns(AllCommands, Add, []), + CommandsDel = filter_commands_with_patterns(CommandsAdd, Del, []), + lists:map(fun(#ejabberd_commands{name = N}) -> N end, + CommandsAdd -- CommandsDel). + +-spec filter_commands_with_patterns([#ejabberd_commands{}], what(), + [#ejabberd_commands{}]) -> [#ejabberd_commands{}]. +filter_commands_with_patterns([], _Patterns, Acc) -> + Acc; +filter_commands_with_patterns([C | CRest], Patterns, Acc) -> + case command_matches_patterns(C, Patterns) of + true -> + filter_commands_with_patterns(CRest, Patterns, [C | Acc]); + _ -> + filter_commands_with_patterns(CRest, Patterns, Acc) + end. + +-spec command_matches_patterns(#ejabberd_commands{}, what()) -> boolean(). +command_matches_patterns(_, all) -> + true; +command_matches_patterns(_, none) -> + false; +command_matches_patterns(_, []) -> + false; +command_matches_patterns(#ejabberd_commands{tags = Tags} = C, [{tag, Tag} | Tail]) -> + case lists:member(Tag, Tags) of + true -> + true; + _ -> + command_matches_patterns(C, Tail) + end; +command_matches_patterns(#ejabberd_commands{name = Name}, [Name | _Tail]) -> + true; +command_matches_patterns(C, [_ | Tail]) -> + command_matches_patterns(C, Tail). + +%%%=================================================================== +%%% Validators +%%%=================================================================== +-spec parse_what([binary()]) -> {what(), what()}. +parse_what(Defs) -> + {A, D} = + lists:foldl( + fun(Def, {Add, Del}) -> + case parse_single_what(Def) of + {error, Err} -> + econf:fail({invalid_syntax, [Err, ": ", Def]}); + all -> + {case Add of none -> none; _ -> all end, Del}; + {neg, all} -> + {none, all}; + {neg, Value} -> + {Add, case Del of L when is_list(L) -> [Value | L]; L2 -> L2 end}; + Value -> + {case Add of L when is_list(L) -> [Value | L]; L2 -> L2 end, Del} + end + end, {[], []}, Defs), + case {A, D} of + {[], _} -> + {none, all}; + {A2, []} -> + {A2, none}; + V -> + V + end. + +-spec parse_single_what(binary()) -> atom() | {neg, atom()} | {tag, atom()} | {error, string()}. +parse_single_what(<<"*">>) -> + all; +parse_single_what(<<"!*">>) -> + {neg, all}; +parse_single_what(<<"!", Rest/binary>>) -> + case parse_single_what(Rest) of + {neg, _} -> + {error, "double negation"}; + {error, _} = Err -> + Err; + V -> + {neg, V} + end; +parse_single_what(<<"[tag:", Rest/binary>>) -> + case binary:split(Rest, <<"]">>) of + [TagName, <<"">>] -> + case parse_single_what(TagName) of + {error, _} = Err -> + Err; + V when is_atom(V) -> + {tag, V}; + _ -> + {error, "invalid tag"} + end; + _ -> + {error, "invalid tag"} + end; +parse_single_what(B) -> + case re:run(B, "^[a-z0-9_\\-]*$") of + nomatch -> {error, "invalid command"}; + _ -> binary_to_atom(B, latin1) + end. + +validator(Map, Opts) -> + econf:and_then( + fun(L) when is_list(L) -> + lists:map( + fun({K, V}) -> {(econf:atom())(K), V}; + (A) -> {acl, (econf:atom())(A)} + end, lists:flatten(L)); + (A) -> + [{acl, (econf:atom())(A)}] + end, + econf:and_then( + econf:options(maps:merge(acl:validators(), Map), Opts), + fun(Rules) -> + lists:flatmap( + fun({Type, Rs}) when is_list(Rs) -> + case maps:is_key(Type, acl:validators()) of + true -> [{acl, {Type, R}} || R <- Rs]; + false -> [{Type, Rs}] + end; + (Other) -> + [Other] + end, Rules) + end)). + +validator(from) -> + fun(L) when is_list(L) -> + lists:map( + fun({K, V}) -> {(econf:enum([tag]))(K), (econf:binary())(V)}; + (A) -> (econf:enum([ejabberd_ctl, + ejabberd_web_admin, + ejabberd_xmlrpc, + mod_adhoc_api, + mod_cron, + mod_http_api]))(A) + end, lists:flatten(L)); + (A) -> + [(econf:enum([ejabberd_ctl, + ejabberd_web_admin, + ejabberd_xmlrpc, + mod_adhoc_api, + mod_cron, + mod_http_api]))(A)] + end; +validator(what) -> + econf:and_then( + econf:list_or_single(econf:non_empty(econf:binary())), + fun parse_what/1); +validator(who) -> + validator(#{access => econf:acl(), oauth => validator(oauth)}, []); +validator(oauth) -> + econf:and_then( + validator(#{access => econf:acl(), + scope => econf:non_empty( + econf:list_or_single(econf:binary()))}, + [{required, [scope]}]), + fun(Os) -> + {[Scopes], Rest} = proplists:split(Os, [scope]), + {lists:flatten([S || {_, S} <- Scopes]), Rest} + end). + +validator() -> + econf:map( + econf:binary(), + econf:and_then( + econf:options( + #{from => validator(from), + what => validator(what), + who => validator(who)}), + fun(Os) -> + {proplists:get_value(from, Os, []), + proplists:get_value(who, Os, none), + proplists:get_value(what, Os, {none, none})} + end), + [unique]). diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl new file mode 100644 index 000000000..8b16fc727 --- /dev/null +++ b/src/ejabberd_acme.erl @@ -0,0 +1,682 @@ +%%%---------------------------------------------------------------------- +%%% ejabberd, Copyright (C) 2002-2025 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(ejabberd_acme). +-behaviour(gen_server). + +%% API +-export([start_link/0]). +-export([default_directory_url/0]). +%% HTTP API +-export([process/2]). +%% Hooks +-export([ejabberd_started/0, register_certfiles/0, cert_expired/2]). +%% ejabberd commands +-export([get_commands_spec/0, request_certificate/1, + revoke_certificate/1, list_certificates/0]). +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). +%% WebAdmin +-export([webadmin_menu_node/3, webadmin_page_node/3]). + +-include("logger.hrl"). +-include("ejabberd_commands.hrl"). +-include("ejabberd_http.hrl"). +-include("ejabberd_web_admin.hrl"). +-include_lib("public_key/include/public_key.hrl"). +-include_lib("stdlib/include/ms_transform.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). + +-define(CALL_TIMEOUT, timer:minutes(10)). + +-record(state, {}). + +-type state() :: #state{}. +-type priv_key() :: public_key:private_key(). +-type cert() :: #'OTPCertificate'{}. +-type cert_type() :: ec | rsa. +-type io_error() :: file:posix(). +-type issue_result() :: ok | p1_acme:issue_return() | + {error, {file, io_error()} | + {idna_failed, binary()}}. + +%%%=================================================================== +%%% API +%%%=================================================================== +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +-spec register_certfiles() -> ok. +register_certfiles() -> + lists:foreach(fun ejabberd_pkix:add_certfile/1, + list_certfiles()). + +-spec process([binary()], _) -> {integer(), [{binary(), binary()}], binary()}. +process([Token], _) -> + ?DEBUG("Received ACME challenge request for token: ~ts", [Token]), + try ets:lookup_element(acme_challenge, Token, 2) of + Key -> {200, [{<<"Content-Type">>, + <<"application/octet-stream">>}], + Key} + catch _:_ -> + {404, [], <<>>} + end; +process(_, _) -> + {404, [], <<>>}. + +-spec cert_expired(_, pkix:cert_info()) -> ok | stop. +cert_expired(_, #{domains := Domains, files := Files}) -> + CertFiles = list_certfiles(), + case lists:any( + fun({File, _}) -> + lists:member(File, CertFiles) + end, Files) of + true -> + gen_server:cast(?MODULE, {request, Domains}), + stop; + false -> + ok + end. + +-spec ejabberd_started() -> ok. +ejabberd_started() -> + gen_server:cast(?MODULE, ejabberd_started). + +default_directory_url() -> + <<"https://acme-v02.api.letsencrypt.org/directory">>. + +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== +init([]) -> + ets:new(acme_challenge, [named_table, public]), + process_flag(trap_exit, true), + ejabberd:start_app(p1_acme), + delete_obsolete_data(), + ejabberd_hooks:add(cert_expired, ?MODULE, cert_expired, 60), + ejabberd_hooks:add(config_reloaded, ?MODULE, register_certfiles, 40), + ejabberd_hooks:add(ejabberd_started, ?MODULE, ejabberd_started, 110), + ejabberd_hooks:add(config_reloaded, ?MODULE, ejabberd_started, 110), + ejabberd_hooks:add(webadmin_menu_node, ?MODULE, webadmin_menu_node, 110), + ejabberd_hooks:add(webadmin_page_node, ?MODULE, webadmin_page_node, 110), + ejabberd_commands:register_commands(get_commands_spec()), + register_certfiles(), + {ok, #state{}}. + +handle_call({request, [_|_] = Domains}, _From, State) -> + ?INFO_MSG("Requesting new certificate for ~ts from ~ts", + [misc:format_hosts_list(Domains), directory_url()]), + {Ret, State1} = issue_request(State, Domains), + {reply, Ret, State1}; +handle_call({revoke, Cert, Key, Path}, _From, State) -> + ?INFO_MSG("Revoking certificate from file ~ts", [Path]), + {Ret, State1} = revoke_request(State, Cert, Key, Path), + {reply, Ret, State1}; +handle_call(Request, From, State) -> + ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), + {noreply, State}. + +handle_cast(ejabberd_started, State) -> + case request_on_start() of + {true, Domains} -> + ?INFO_MSG("Requesting new certificate for ~ts from ~ts", + [misc:format_hosts_list(Domains), directory_url()]), + {_, State1} = issue_request(State, Domains), + {noreply, State1}; + false -> + {noreply, State} + end; +handle_cast({request, [_|_] = Domains}, State) -> + ?INFO_MSG("Requesting renewal of certificate for ~ts from ~ts", + [misc:format_hosts_list(Domains), directory_url()]), + {_, State1} = issue_request(State, Domains), + {noreply, State1}; +handle_cast(Request, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Request]), + {noreply, State}. + +handle_info(Info, State) -> + ?WARNING_MSG("Unexpected info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, _State) -> + ejabberd_hooks:delete(cert_expired, ?MODULE, cert_expired, 60), + ejabberd_hooks:delete(config_reloaded, ?MODULE, register_certfiles, 40), + ejabberd_hooks:delete(ejabberd_started, ?MODULE, ejabberd_started, 110), + ejabberd_hooks:delete(config_reloaded, ?MODULE, ejabberd_started, 110), + ejabberd_hooks:delete(webadmin_menu_node, ?MODULE, webadmin_menu_node, 110), + ejabberd_hooks:delete(webadmin_page_node, ?MODULE, webadmin_page_node, 110), + ejabberd_commands:unregister_commands(get_commands_spec()). + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +%%%=================================================================== +%%% Challenge callback +%%%=================================================================== +-spec register_challenge(p1_acme:challenge_data(), reference()) -> true. +register_challenge(Auth, Ref) -> + ?DEBUG("Registering ACME challenge ~p -> ~p", [Ref, Auth]), + ejabberd_hooks:run(acme_challenge, [{start, Auth, Ref}]), + ets:insert( + acme_challenge, + lists:map( + fun(#{token := Token, key := Key}) -> + {Token, Key, Ref} + end, Auth)). + +-spec unregister_challenge(reference()) -> non_neg_integer(). +unregister_challenge(Ref) -> + ?DEBUG("Unregistering ACME challenge ~p", [Ref]), + ejabberd_hooks:run(acme_challenge, [{stop, Ref}]), + ets:select_delete( + acme_challenge, + ets:fun2ms( + fun({_, _, Ref1}) -> + Ref1 == Ref + end)). + +%%%=================================================================== +%%% Issuance +%%%=================================================================== +-spec issue_request(state(), [binary(),...]) -> {issue_result(), state()}. +issue_request(State, Domains) -> + case check_idna(Domains) of + {ok, AsciiDomains} -> + case read_account_key() of + {ok, AccKey} -> + Config = ejabberd_option:acme(), + DirURL = maps:get(ca_url, Config, default_directory_url()), + Contact = maps:get(contact, Config, []), + CertType = maps:get(cert_type, Config, rsa), + issue_request(State, DirURL, Domains, AsciiDomains, AccKey, CertType, Contact); + {error, Reason} = Err -> + ?ERROR_MSG("Failed to request certificate for ~ts: ~ts", + [misc:format_hosts_list(Domains), + format_error(Reason)]), + {Err, State} + end; + {error, Reason} = Err -> + ?ERROR_MSG("Failed to request certificate for ~ts: ~ts", + [misc:format_hosts_list(Domains), + format_error(Reason)]), + {Err, State} + end. + +-spec issue_request(state(), binary(), [binary(),...], [string(), ...], priv_key(), + cert_type(), [binary()]) -> {issue_result(), state()}. +issue_request(State, DirURL, Domains, AsciiDomains, AccKey, CertType, Contact) -> + Ref = make_ref(), + ChallengeFun = fun(Auth) -> register_challenge(Auth, Ref) end, + Ret = case p1_acme:issue(DirURL, AsciiDomains, AccKey, + [{cert_type, CertType}, + {contact, Contact}, + {debug_fun, debug_fun()}, + {challenge_fun, ChallengeFun}]) of + {ok, #{cert_key := CertKey, + cert_chain := Certs}} -> + case store_cert(CertKey, Certs, CertType, Domains) of + {ok, Path} -> + ejabberd_pkix:add_certfile(Path), + ejabberd_pkix:commit(), + ?INFO_MSG("Certificate for ~ts has been received, " + "stored and loaded successfully", + [misc:format_hosts_list(Domains)]), + {ok, State}; + {error, Reason} = Err -> + ?ERROR_MSG("Failed to store certificate for ~ts: ~ts", + [misc:format_hosts_list(Domains), + format_error(Reason)]), + {Err, State} + end; + {error, Reason} = Err -> + ?ERROR_MSG("Failed to request certificate for ~ts: ~ts", + [misc:format_hosts_list(Domains), + format_error(Reason)]), + {Err, State} + end, + unregister_challenge(Ref), + Ret. + +%%%=================================================================== +%%% Revocation +%%%=================================================================== +revoke_request(State, Cert, Key, Path) -> + case p1_acme:revoke(directory_url(), Cert, Key, + [{debug_fun, debug_fun()}]) of + ok -> + ?INFO_MSG("Certificate from file ~ts has been " + "revoked successfully", [Path]), + case delete_file(Path) of + ok -> + ejabberd_pkix:del_certfile(Path), + ejabberd_pkix:commit(), + {ok, State}; + Err -> + {Err, State} + end; + {error, Reason} = Err -> + ?ERROR_MSG("Failed to revoke certificate from file ~ts: ~ts", + [Path, format_error(Reason)]), + {Err, State} + end. + +%%%=================================================================== +%%% File management +%%%=================================================================== +-spec acme_dir() -> file:filename_all(). +acme_dir() -> + MnesiaDir = mnesia:system_info(directory), + filename:join(MnesiaDir, "acme"). + +-spec acme_certs_dir(atom()) -> file:filename_all(). +acme_certs_dir(Tag) -> + filename:join(acme_dir(), Tag). + +-spec account_file() -> file:filename_all(). +account_file() -> + filename:join(acme_dir(), "account.key"). + +-spec cert_file(cert_type(), [binary()]) -> file:filename_all(). +cert_file(CertType, Domains) -> + L = [erlang:atom_to_binary(CertType, latin1)|Domains], + Hash = str:sha(str:join(L, <<0>>)), + filename:join(acme_certs_dir(live), Hash). + +-spec prep_path(file:filename_all()) -> binary(). +prep_path(Path) -> + unicode:characters_to_binary(Path). + +-spec list_certfiles() -> [binary()]. +list_certfiles() -> + filelib:fold_files( + acme_certs_dir(live), "^[0-9a-f]{40}$", false, + fun(F, Fs) -> [prep_path(F)|Fs] end, []). + +-spec read_account_key() -> {ok, #'ECPrivateKey'{}} | {error, {file, io_error()}}. +read_account_key() -> + Path = account_file(), + case pkix:read_file(Path) of + {ok, _, KeyMap} -> + case maps:keys(KeyMap) of + [#'ECPrivateKey'{} = Key|_] -> {ok, Key}; + _ -> + ?WARNING_MSG("File ~ts doesn't contain ACME account key. " + "Trying to create a new one...", + [Path]), + create_account_key() + end; + {error, enoent} -> + create_account_key(); + {error, {bad_cert, _, _} = Reason} -> + ?WARNING_MSG("ACME account key from '~ts' is corrupted: ~ts. " + "Trying to create a new one...", + [Path, pkix:format_error(Reason)]), + create_account_key(); + {error, Reason} -> + ?ERROR_MSG("Failed to read ACME account from ~ts: ~ts. " + "Try to fix permissions or delete the file completely", + [Path, pkix:format_error(Reason)]), + {error, {file, Reason}} + end. + +-spec create_account_key() -> {ok, #'ECPrivateKey'{}} | {error, {file, io_error()}}. +create_account_key() -> + Path = account_file(), + ?DEBUG("Creating ACME account key in ~ts", [Path]), + Key = p1_acme:generate_key(ec), + DER = public_key:der_encode(element(1, Key), Key), + PEM = public_key:pem_encode([{element(1, Key), DER, not_encrypted}]), + case write_file(Path, PEM) of + ok -> + ?DEBUG("ACME account key has been created successfully in ~ts", + [Path]), + {ok, Key}; + {error, Reason} -> + {error, {file, Reason}} + end. + +-spec store_cert(priv_key(), [cert()], cert_type(), [binary()]) -> {ok, file:filename_all()} | + {error, {file, io_error()}}. +store_cert(Key, Chain, CertType, Domains) -> + DerKey = public_key:der_encode(element(1, Key), Key), + PemKey = [{element(1, Key), DerKey, not_encrypted}], + PemChain = lists:map( + fun(Cert) -> + DerCert = public_key:pkix_encode( + element(1, Cert), Cert, otp), + {'Certificate', DerCert, not_encrypted} + end, Chain), + PEM = public_key:pem_encode(PemChain ++ PemKey), + Path = cert_file(CertType, Domains), + ?DEBUG("Storing certificate for ~ts in ~ts", + [misc:format_hosts_list(Domains), Path]), + case write_file(Path, PEM) of + ok -> + {ok, Path}; + {error, Reason} -> + {error, {file, Reason}} + end. + +-spec read_cert(file:filename_all()) -> {ok, [cert()], priv_key()} | + {error, {file, io_error()} | + {bad_cert, _, _} | + unexpected_certfile}. +read_cert(Path) -> + ?DEBUG("Reading certificate from ~ts", [Path]), + case pkix:read_file(Path) of + {ok, CertsMap, KeysMap} -> + case {maps:to_list(CertsMap), maps:keys(KeysMap)} of + {[_|_] = Certs, [CertKey]} -> + {ok, [Cert || {Cert, _} <- lists:keysort(2, Certs)], CertKey}; + _ -> + {error, unexpected_certfile} + end; + {error, Why} when is_atom(Why) -> + {error, {file, Why}}; + {error, _} = Err -> + Err + end. + +-spec write_file(file:filename_all(), iodata()) -> ok | {error, io_error()}. +write_file(Path, Data) -> + case ensure_dir(Path) of + ok -> + case file:write_file(Path, Data) of + ok -> + case file:change_mode(Path, 8#600) of + ok -> ok; + {error, Why} -> + ?WARNING_MSG("Failed to change permissions of ~ts: ~ts", + [Path, file:format_error(Why)]) + end; + {error, Why} = Err -> + ?ERROR_MSG("Failed to write file ~ts: ~ts", + [Path, file:format_error(Why)]), + Err + end; + Err -> + Err + end. + +-spec delete_file(file:filename_all()) -> ok | {error, io_error()}. +delete_file(Path) -> + case file:delete(Path) of + ok -> ok; + {error, Why} = Err -> + ?WARNING_MSG("Failed to delete file ~ts: ~ts", + [Path, file:format_error(Why)]), + Err + end. + +-spec ensure_dir(file:filename_all()) -> ok | {error, io_error()}. +ensure_dir(Path) -> + case filelib:ensure_dir(Path) of + ok -> ok; + {error, Why} = Err -> + ?ERROR_MSG("Failed to create directory ~ts: ~ts", + [filename:dirname(Path), + file:format_error(Why)]), + Err + end. + +-spec delete_obsolete_data() -> ok. +delete_obsolete_data() -> + Path = filename:join(ejabberd_pkix:certs_dir(), "acme"), + case filelib:is_dir(Path) of + true -> + ?INFO_MSG("Deleting obsolete directory ~ts", [Path]), + _ = misc:delete_dir(Path), + ok; + false -> + ok + end. + +%%%=================================================================== +%%% ejabberd commands +%%%=================================================================== +get_commands_spec() -> + [#ejabberd_commands{name = request_certificate, tags = [acme], + desc = "Requests certificates for all or some domains", + longdesc = "Domains can be `all`, or a list of domains separared with comma characters", + module = ?MODULE, function = request_certificate, + args_desc = ["Domains for which to acquire a certificate"], + args_example = ["example.com,domain.tld,conference.domain.tld"], + args = [{domains, string}], + result = {res, restuple}}, + #ejabberd_commands{name = list_certificates, tags = [acme], + desc = "Lists all ACME certificates", + module = ?MODULE, function = list_certificates, + args = [], + result = {certificates, + {list, {certificate, + {tuple, [{domain, string}, + {file, string}, + {used, string}]}}}}}, + #ejabberd_commands{name = revoke_certificate, tags = [acme], + desc = "Revokes the selected ACME certificate", + module = ?MODULE, function = revoke_certificate, + args_desc = ["Filename of the certificate"], + args = [{file, string}], + result = {res, restuple}}]. + +-spec request_certificate(iodata()) -> {ok | error, string()}. +request_certificate(Arg) -> + Ret = case lists:filter( + fun(S) -> S /= <<>> end, + re:split(Arg, "[\\h,;]+", [{return, binary}])) of + [<<"all">>] -> + case auto_domains() of + [] -> {error, no_auto_hosts}; + Domains -> + gen_server:call(?MODULE, {request, Domains}, ?CALL_TIMEOUT) + end; + [_|_] = Domains -> + case lists:dropwhile( + fun(D) -> + try ejabberd_router:is_my_route(D) of + true -> not is_ip_or_localhost(D); + false -> false + catch _:{invalid_domain, _} -> false + end + end, Domains) of + [Bad|_] -> + {error, {invalid_host, Bad}}; + [] -> + gen_server:call(?MODULE, {request, Domains}, ?CALL_TIMEOUT) + end; + [] -> + {error, invalid_argument} + end, + case Ret of + ok -> {ok, ""}; + {error, Why} -> {error, format_error(Why)} + end. + +-spec revoke_certificate(iodata()) -> {ok | error, string()}. +revoke_certificate(Path0) -> + Path = prep_path(Path0), + Ret = case read_cert(Path) of + {ok, [Cert|_], Key} -> + gen_server:call(?MODULE, {revoke, Cert, Key, Path}, ?CALL_TIMEOUT); + {error, _} = Err -> + Err + end, + case Ret of + ok -> {ok, ""}; + {error, Reason} -> {error, format_error(Reason)} + end. + +-spec list_certificates() -> [{binary(), binary(), boolean()}]. +list_certificates() -> + Known = lists:flatmap( + fun(Path) -> + try + {ok, [Cert|_], _} = read_cert(Path), + Domains = pkix:extract_domains(Cert), + [{Domain, Path} || Domain <- Domains] + catch _:{badmatch, _} -> + [] + end + end, list_certfiles()), + Used = lists:foldl( + fun(Domain, S) -> + try + {ok, Path} = ejabberd_pkix:get_certfile_no_default(Domain), + {ok, [Cert|_], _} = read_cert(Path), + {ok, #{files := Files}} = pkix:get_cert_info(Cert), + lists:foldl(fun sets:add_element/2, + S, [{Domain, File} || {File, _} <- Files]) + catch _:{badmatch, _} -> + S + end + end, sets:new(), all_domains()), + lists:sort( + lists:map( + fun({Domain, Path} = E) -> + {Domain, Path, sets:is_element(E, Used)} + end, Known)). + +%%%=================================================================== +%%% WebAdmin +%%%=================================================================== + +webadmin_menu_node(Acc, _Node, _Lang) -> + Acc ++ [{<<"acme">>, <<"ACME">>}]. + +webadmin_page_node(_, Node, #request{path = [<<"acme">>]} = R) -> + Head = ?H1GLraw(<<"ACME Certificates">>, <<"admin/configuration/basic/#acme">>, <<"ACME">>), + Set = [ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [request_certificate, R]), + ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [revoke_certificate, R])], + Get = [ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [list_certificates, R])], + {stop, Head ++ Get ++ Set}; +webadmin_page_node(Acc, _, _) -> Acc. + +%%%=================================================================== +%%% Other stuff +%%%=================================================================== +-spec all_domains() -> [binary(),...]. +all_domains() -> + ejabberd_option:hosts() ++ ejabberd_router:get_all_routes(). + +-spec auto_domains() -> [binary()]. +auto_domains() -> + lists:filter( + fun(Host) -> + not is_ip_or_localhost(Host) + end, all_domains()). + +-spec directory_url() -> binary(). +directory_url() -> + maps:get(ca_url, ejabberd_option:acme(), default_directory_url()). + +-spec debug_fun() -> fun((string(), list()) -> ok). +debug_fun() -> + fun(Fmt, Args) -> ?DEBUG(Fmt, Args) end. + +-spec request_on_start() -> false | {true, [binary()]}. +request_on_start() -> + Config = ejabberd_option:acme(), + case maps:get(auto, Config, true) of + false -> false; + true -> + case ejabberd_listener:tls_listeners() of + [] -> false; + _ -> + case lists:filter( + fun(Host) -> + not (have_cert_for_domain(Host) + orelse is_ip_or_localhost(Host)) + end, auto_domains()) of + [] -> false; + Hosts -> + case have_acme_listener() of + true -> {true, Hosts}; + false -> + ?WARNING_MSG( + "No HTTP listeners for ACME challenges " + "are configured, automatic " + "certificate requests are aborted. Hint: " + "configure the listener and restart/reload " + "ejabberd. Or set acme->auto option to " + "`false` to suppress this warning.", + []), + false + end + end + end + end. + +well_known() -> + [<<".well-known">>, <<"acme-challenge">>]. + +-spec have_cert_for_domain(binary()) -> boolean(). +have_cert_for_domain(Host) -> + ejabberd_pkix:get_certfile_no_default(Host) /= error. + +-spec is_ip_or_localhost(binary()) -> boolean(). +is_ip_or_localhost(Host) -> + Parts = binary:split(Host, <<".">>), + TLD = binary_to_list(lists:last(Parts)), + case inet:parse_address(TLD) of + {ok, _} -> true; + _ -> TLD == "localhost" + end. + +-spec have_acme_listener() -> boolean(). +have_acme_listener() -> + lists:any( + fun({_, ejabberd_http, #{tls := false, + request_handlers := Handlers}}) -> + lists:keymember(well_known(), 1, Handlers); + (_) -> + false + end, ejabberd_option:listen()). + +-spec check_idna([binary()]) -> {ok, [string()]} | {error, {idna_failed, binary()}}. +check_idna(Domains) -> + lists:foldl( + fun(D, {ok, Ds}) -> + try {ok, [idna:utf8_to_ascii(D)|Ds]} + catch _:_ -> {error, {idna_failed, D}} + end; + (_, Err) -> + Err + end, {ok, []}, Domains). + +-spec format_error(term()) -> string(). +format_error({file, Reason}) -> + "I/O error: " ++ file:format_error(Reason); +format_error({invalid_host, Domain}) -> + "Unknown or unacceptable virtual host: " ++ binary_to_list(Domain); +format_error(no_auto_hosts) -> + "You have no virtual hosts acceptable for ACME certification"; +format_error(invalid_argument) -> + "Invalid argument"; +format_error(unexpected_certfile) -> + "The certificate file was not obtained using ACME"; +format_error({idna_failed, Domain}) -> + "Not an IDN hostname: " ++ binary_to_list(Domain); +format_error({bad_cert, _, _} = Reason) -> + "Malformed certificate file: " ++ pkix:format_error(Reason); +format_error(Reason) -> + p1_acme:format_error(Reason). diff --git a/src/ejabberd_admin.erl b/src/ejabberd_admin.erl index 8b6e27b82..5ec0ac051 100644 --- a/src/ejabberd_admin.erl +++ b/src/ejabberd_admin.erl @@ -5,7 +5,7 @@ %%% Created : 7 May 2006 by Mickael Remond %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -26,14 +26,26 @@ -module(ejabberd_admin). -author('mickael.remond@process-one.net'). --export([start/0, stop/0, +-behaviour(gen_server). + +-export([start_link/0, %% Server - status/0, reopen_log/0, + status/0, stop/0, restart/0, + reopen_log/0, rotate_log/0, + set_loglevel/1, + evacuate_kindly/2, stop_kindly/2, send_service_message_all_mucs/2, registered_vhosts/0, reload_config/0, + dump_config/1, + convert_to_yaml/2, + %% Cluster + join_cluster/1, leave_cluster/1, + join_cluster_here/1, + list_cluster/0, list_cluster_detailed/0, + get_cluster_node_details3/0, %% Erlang - update_list/0, update/1, + update_list/0, update/1, update/0, %% Accounts register/3, unregister/2, registered_users/1, @@ -42,196 +54,604 @@ %% Purge DB delete_expired_messages/0, delete_old_messages/1, %% Mnesia - export2odbc/2, - set_master/1, + get_master/0, set_master/1, backup_mnesia/1, restore_mnesia/1, dump_mnesia/1, dump_table/2, load_mnesia/1, + mnesia_info/0, mnesia_table_info/1, install_fallback_mnesia/1, dump_to_textfile/1, dump_to_textfile/2, mnesia_change_nodename/4, - restore/1 % Still used by some modules - ]). + restore/1, % Still used by some modules + clear_cache/0, + gc/0, + get_commands_spec/0, + delete_old_messages_batch/4, delete_old_messages_status/1, delete_old_messages_abort/1, + %% Internal + mnesia_list_tables/0, + mnesia_table_details/1, + mnesia_table_change_storage/2, + mnesia_table_clear/1, + mnesia_table_delete/1, + echo/1, echo3/3]). +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). --include("ejabberd.hrl"). --include("logger.hrl"). +-export([web_menu_main/2, web_page_main/2, + web_menu_node/3, web_page_node/3]). + +-include_lib("xmpp/include/xmpp.hrl"). -include("ejabberd_commands.hrl"). +-include("ejabberd_http.hrl"). +-include("ejabberd_web_admin.hrl"). +-include("logger.hrl"). +-include("translate.hrl"). %+++ TODO -start() -> - ejabberd_commands:register_commands(commands()). +-record(state, {}). -stop() -> - ejabberd_commands:unregister_commands(commands()). +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +init([]) -> + process_flag(trap_exit, true), + ejabberd_commands:register_commands(get_commands_spec()), + ejabberd_hooks:add(webadmin_menu_main, ?MODULE, web_menu_main, 50), + ejabberd_hooks:add(webadmin_page_main, ?MODULE, web_page_main, 50), + ejabberd_hooks:add(webadmin_menu_node, ?MODULE, web_menu_node, 50), + ejabberd_hooks:add(webadmin_page_node, ?MODULE, web_page_node, 50), + {ok, #state{}}. + +handle_call(Request, From, State) -> + ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), + {noreply, State}. + +handle_cast(Msg, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Msg]), + {noreply, State}. + +handle_info(Info, State) -> + ?WARNING_MSG("Unexpected info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, _State) -> + ejabberd_hooks:delete(webadmin_menu_main, ?MODULE, web_menu_main, 50), + ejabberd_hooks:delete(webadmin_page_main, ?MODULE, web_page_main, 50), + ejabberd_hooks:delete(webadmin_menu_node, ?MODULE, web_menu_node, 50), + ejabberd_hooks:delete(webadmin_page_node, ?MODULE, web_page_node, 50), + ejabberd_commands:unregister_commands(get_commands_spec()). + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. %%% %%% ejabberd commands %%% -commands() -> +get_commands_spec() -> [ %% The commands status, stop and restart are implemented also in ejabberd_ctl %% They are defined here so that other interfaces can use them too #ejabberd_commands{name = status, tags = [server], desc = "Get status of the ejabberd server", module = ?MODULE, function = status, + result_desc = "Result tuple", + result_example = {ok, <<"The node ejabberd@localhost is started with status: started" + "ejabberd X.X is running in that node">>}, args = [], result = {res, restuple}}, #ejabberd_commands{name = stop, tags = [server], desc = "Stop ejabberd gracefully", - module = init, function = stop, + module = ?MODULE, function = stop, + args = [], result = {res, rescode}}, + #ejabberd_commands{name = halt, tags = [server], + desc = "Halt ejabberd abruptly with status code 1", + note = "added in 23.10", + module = ejabberd, function = halt, args = [], result = {res, rescode}}, #ejabberd_commands{name = restart, tags = [server], desc = "Restart ejabberd gracefully", - module = init, function = restart, + module = ?MODULE, function = restart, args = [], result = {res, rescode}}, - #ejabberd_commands{name = reopen_log, tags = [logs, server], - desc = "Reopen the log files", + #ejabberd_commands{name = reopen_log, tags = [logs], + desc = "Reopen maybe the log files after being renamed", + longdesc = "Has no effect on ejabberd main log files, " + "only on log files generated by some modules.\n" + "This can be useful when an external tool is " + "used for log rotation. See " + "_`../../admin/guide/troubleshooting.md#log-files|Log Files`_.", + policy = admin, module = ?MODULE, function = reopen_log, args = [], result = {res, rescode}}, - #ejabberd_commands{name = stop_kindly, tags = [server], - desc = "Inform users and rooms, wait, and stop the server", - longdesc = "Provide the delay in seconds, and the " - "announcement quoted, for example: \n" - "ejabberdctl stop_kindly 60 " - "\\\"The server will stop in one minute.\\\"", - module = ?MODULE, function = stop_kindly, + #ejabberd_commands{name = rotate_log, tags = [logs], + desc = "Rotate maybe log file of some module", + longdesc = "Has no effect on ejabberd main log files, " + "only on log files generated by some modules.", + module = ?MODULE, function = rotate_log, + args = [], result = {res, rescode}}, + #ejabberd_commands{name = evacuate_kindly, tags = [server], + desc = "Evacuate kindly all users (kick and prevent login)", + longdesc = "Inform users and rooms, don't allow login, wait, " + "restart the server, and don't allow new logins.\n" + "Provide the delay in seconds, and the " + "announcement quoted, for example: \n" + "`ejabberdctl evacuate_kindly 60 " + "\\\"The server will stop in one minute.\\\"`", + note = "added in 24.12", + module = ?MODULE, function = evacuate_kindly, + args_desc = ["Seconds to wait", "Announcement to send, with quotes"], + args_example = [60, <<"Server will stop now.">>], args = [{delay, integer}, {announcement, string}], result = {res, rescode}}, - #ejabberd_commands{name = get_loglevel, tags = [logs, server], + #ejabberd_commands{name = stop_kindly, tags = [server], + desc = "Stop kindly the server (informing users)", + longdesc = "Inform users and rooms, wait, and stop the server.\n" + "Provide the delay in seconds, and the " + "announcement quoted, for example: \n" + "`ejabberdctl stop_kindly 60 " + "\\\"The server will stop in one minute.\\\"`", + module = ?MODULE, function = stop_kindly, + args_desc = ["Seconds to wait", "Announcement to send, with quotes"], + args_example = [60, <<"Server will stop now.">>], + args = [{delay, integer}, {announcement, string}], + result = {res, rescode}}, + #ejabberd_commands{name = get_loglevel, tags = [logs], desc = "Get the current loglevel", module = ejabberd_logger, function = get, + result_desc = "Tuple with the log level number, its keyword and description", + result_example = warning, args = [], - result = {leveltuple, {tuple, [{levelnumber, integer}, - {levelatom, atom}, - {leveldesc, string} - ]}}}, + result = {levelatom, atom}}, + #ejabberd_commands{name = set_loglevel, tags = [logs], + desc = "Set the loglevel", + longdesc = "Possible loglevels: `none`, `emergency`, `alert`, `critical`, + `error`, `warning`, `notice`, `info`, `debug`.", + module = ?MODULE, function = set_loglevel, + args_desc = ["Desired logging level"], + args_example = ["debug"], + args = [{loglevel, string}], + result = {res, rescode}}, #ejabberd_commands{name = update_list, tags = [server], desc = "List modified modules that can be updated", module = ?MODULE, function = update_list, args = [], + result_example = ["mod_configure", "mod_vcard"], result = {modules, {list, {module, string}}}}, #ejabberd_commands{name = update, tags = [server], - desc = "Update the given module, or use the keyword: all", + desc = "Update the given module", + longdesc = "To update all the possible modules, use `all`.", + note = "improved in 24.10", module = ?MODULE, function = update, + args_example = ["all"], args = [{module, string}], + result_example = {ok, <<"Updated modules: mod_configure, mod_vcard">>}, result = {res, restuple}}, #ejabberd_commands{name = register, tags = [accounts], desc = "Register a user", + policy = admin, module = ?MODULE, function = register, + args_desc = ["Username", "Local vhost served by ejabberd", "Password"], + args_example = [<<"bob">>, <<"example.com">>, <<"SomEPass44">>], args = [{user, binary}, {host, binary}, {password, binary}], 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"], + args_example = [<<"bob">>, <<"example.com">>], args = [{user, binary}, {host, binary}], result = {res, restuple}}, #ejabberd_commands{name = registered_users, tags = [accounts], desc = "List all registered users in HOST", module = ?MODULE, function = registered_users, + args_desc = ["Local vhost"], + args_example = [<<"example.com">>], + result_desc = "List of registered accounts usernames", + result_example = [<<"user1">>, <<"user2">>], args = [{host, binary}], result = {users, {list, {username, string}}}}, - #ejabberd_commands{name = registered_vhosts, tags = [server], + #ejabberd_commands{name = registered_vhosts, tags = [server], desc = "List all registered vhosts in SERVER", module = ?MODULE, function = registered_vhosts, + result_desc = "List of available vhosts", + result_example = [<<"example.com">>, <<"anon.example.com">>], args = [], result = {vhosts, {list, {vhost, string}}}}, - #ejabberd_commands{name = reload_config, tags = [server], - desc = "Reload ejabberd configuration file into memory", + #ejabberd_commands{name = reload_config, tags = [config], + desc = "Reload config file in memory", module = ?MODULE, function = reload_config, args = [], result = {res, rescode}}, + #ejabberd_commands{name = join_cluster, tags = [cluster], + desc = "Join our local node into the cluster handled by Node", + longdesc = "This command returns immediately, + even before the joining process has + completed. Consequently, if you are using + `ejabberdctl` (or some `CTL_ON_` container + environment variables) to run more commands + afterwards, you may want to precede them with + the `started` + _`../../admin/guide/managing.md#ejabberdctl-commands|ejabberdctl command`_ + to ensure the + clustering process has completed before + proceeding. For example: `join_cluster + ejabberd@main` > `started` > `list_cluster`.", + note = "improved in 24.06", + module = ?MODULE, function = join_cluster, + args_desc = ["Nodename of the node to join"], + args_example = [<<"ejabberd1@machine7">>], + args = [{node, binary}], + result = {res, restuple}}, + #ejabberd_commands{name = join_cluster_here, tags = [cluster], + desc = "Join a remote Node here, into our cluster", + note = "added in 24.06", + module = ?MODULE, function = join_cluster_here, + args_desc = ["Nodename of the node to join here"], + args_example = [<<"ejabberd1@machine7">>], + args = [{node, binary}], + result = {res, restuple}}, + #ejabberd_commands{name = leave_cluster, tags = [cluster], + desc = "Remove and shutdown Node from the running cluster", + longdesc = "This command can be run from any running " + "node of the cluster, even the node to be removed. " + "In the removed node, this command works only when " + "using ejabberdctl, not _`mod_http_api`_ or other code that " + "runs inside the same ejabberd node that will leave.", + module = ?MODULE, function = leave_cluster, + args_desc = ["Nodename of the node to kick from the cluster"], + args_example = [<<"ejabberd1@machine8">>], + args = [{node, binary}], + result = {res, rescode}}, + + #ejabberd_commands{name = list_cluster, tags = [cluster], + desc = "List running nodes that are part of this cluster", + module = ?MODULE, function = list_cluster, + result_example = [ejabberd1@machine7, ejabberd1@machine8], + args = [], + result = {nodes, {list, {node, atom}}}}, + #ejabberd_commands{name = list_cluster_detailed, tags = [cluster], + desc = "List nodes (both running and known) and some stats", + note = "added in 24.06", + module = ?MODULE, function = list_cluster_detailed, + args = [], + result_example = [{'ejabberd@localhost', "true", + "The node ejabberd is started. Status...", + 7, 348, 60, none}], + result = {nodes, {list, {node, {tuple, [{name, atom}, + {running, string}, + {status, string}, + {online_users, integer}, + {processes, integer}, + {uptime_seconds, integer}, + {master_node, atom} + ]}}}}}, + #ejabberd_commands{name = import_file, tags = [mnesia], desc = "Import user data from jabberd14 spool file", module = ?MODULE, function = import_file, + args_desc = ["Full path to the jabberd14 spool file"], + args_example = ["/var/lib/ejabberd/jabberd14.spool"], args = [{file, string}], result = {res, restuple}}, #ejabberd_commands{name = import_dir, tags = [mnesia], desc = "Import users data from jabberd14 spool dir", module = ?MODULE, function = import_dir, + args_desc = ["Full path to the jabberd14 spool directory"], + args_example = ["/var/lib/ejabberd/jabberd14/"], args = [{file, string}], result = {res, restuple}}, - #ejabberd_commands{name = import_piefxis, tags = [mnesia], desc = "Import users data from a PIEFXIS file (XEP-0227)", module = ejabberd_piefxis, function = import_file, - args = [{file, string}], result = {res, rescode}}, + args_desc = ["Full path to the PIEFXIS file"], + args_example = ["/var/lib/ejabberd/example.com.xml"], + args = [{file, binary}], result = {res, rescode}}, #ejabberd_commands{name = export_piefxis, tags = [mnesia], desc = "Export data of all users in the server to PIEFXIS files (XEP-0227)", module = ejabberd_piefxis, function = export_server, - args = [{dir, string}], result = {res, rescode}}, + args_desc = ["Full path to a directory"], + args_example = ["/var/lib/ejabberd/"], + args = [{dir, binary}], result = {res, rescode}}, #ejabberd_commands{name = export_piefxis_host, tags = [mnesia], desc = "Export data of users in a host to PIEFXIS files (XEP-0227)", module = ejabberd_piefxis, function = export_host, - args = [{dir, string}, {host, string}], result = {res, rescode}}, + args_desc = ["Full path to a directory", "Vhost to export"], + args_example = ["/var/lib/ejabberd/", "example.com"], + args = [{dir, binary}, {host, binary}], result = {res, rescode}}, - #ejabberd_commands{name = export_odbc, tags = [mnesia, odbc], - desc = "Export all tables as SQL queries to a file", - module = ejd2odbc, function = export, - args = [{host, string}, {file, string}], result = {res, rescode}}, + #ejabberd_commands{name = delete_mnesia, tags = [mnesia], + desc = "Delete elements in Mnesia database for a given vhost", + module = ejd2sql, function = delete, + args_desc = ["Vhost which content will be deleted in Mnesia database"], + args_example = ["example.com"], + args = [{host, string}], result = {res, rescode}}, + #ejabberd_commands{name = convert_to_scram, tags = [sql], + desc = "Convert the passwords of users to SCRAM", + module = ejabberd_auth, function = convert_to_scram, + args_desc = ["Vhost which users' passwords will be scrammed"], + args_example = ["example.com"], + args = [{host, binary}], result = {res, rescode}}, + #ejabberd_commands{name = import_prosody, tags = [mnesia, sql], + desc = "Import data from Prosody", + longdesc = "Note: this requires ejabberd to be " + "compiled with `./configure --enable-lua` " + "(which installs the `luerl` library).", + module = prosody2ejabberd, function = from_dir, + args_desc = ["Full path to the Prosody data directory"], + args_example = ["/var/lib/prosody/datadump/"], + args = [{dir, string}], result = {res, rescode}}, #ejabberd_commands{name = convert_to_yaml, tags = [config], desc = "Convert the input file from Erlang to YAML format", - module = ejabberd_config, function = convert_to_yaml, + module = ?MODULE, function = convert_to_yaml, + args_desc = ["Full path to the original configuration file", "And full path to final file"], + args_example = ["/etc/ejabberd/ejabberd.cfg", "/etc/ejabberd/ejabberd.yml"], args = [{in, string}, {out, string}], result = {res, rescode}}, + #ejabberd_commands{name = dump_config, tags = [config], + desc = "Dump configuration in YAML format as seen by ejabberd", + module = ?MODULE, function = dump_config, + args_desc = ["Full path to output file"], + args_example = ["/tmp/ejabberd.yml"], + args = [{out, string}], + result = {res, rescode}}, - #ejabberd_commands{name = delete_expired_messages, tags = [purge], + #ejabberd_commands{name = delete_expired_messages, tags = [offline, purge], desc = "Delete expired offline messages from database", module = ?MODULE, function = delete_expired_messages, args = [], result = {res, rescode}}, - #ejabberd_commands{name = delete_old_messages, tags = [purge], + #ejabberd_commands{name = delete_old_messages, tags = [offline, purge], desc = "Delete offline messages older than DAYS", module = ?MODULE, function = delete_old_messages, + args_desc = ["Number of days"], + args_example = [31], args = [{days, integer}], result = {res, rescode}}, - - #ejabberd_commands{name = rename_default_nodeplugin, tags = [mnesia], - desc = "Update PubSub table from old ejabberd trunk SVN to 2.1.0", - module = mod_pubsub, function = rename_default_nodeplugin, - args = [], result = {res, rescode}}, + #ejabberd_commands{name = delete_old_messages_batch, tags = [offline, purge], + desc = "Delete offline messages older than DAYS", + note = "added in 22.05", + module = ?MODULE, function = delete_old_messages_batch, + args_desc = ["Name of host where messages should be deleted", + "Days to keep messages", + "Number of messages to delete per batch", + "Desired rate of messages to delete per minute"], + args_example = [<<"localhost">>, 31, 1000, 10000], + args = [{host, binary}, {days, integer}, {batch_size, integer}, {rate, integer}], + result = {res, restuple}, + result_desc = "Result tuple", + result_example = {ok, <<"Removal of 5000 messages in progress">>}}, + #ejabberd_commands{name = delete_old_messages_status, tags = [offline, purge], + desc = "Status of delete old offline messages operation", + note = "added in 22.05", + module = ?MODULE, function = delete_old_messages_status, + args_desc = ["Name of host where messages should be deleted"], + args_example = [<<"localhost">>], + args = [{host, binary}], + result = {status, string}, + result_desc = "Status test", + result_example = "Operation in progress, delete 5000 messages"}, + #ejabberd_commands{name = abort_delete_old_messages, tags = [offline, purge], + desc = "Abort currently running delete old offline messages operation", + note = "added in 22.05", + module = ?MODULE, function = delete_old_messages_abort, + args_desc = ["Name of host where operation should be aborted"], + args_example = [<<"localhost">>], + args = [{host, binary}], + result = {status, string}, + result_desc = "Status text", + result_example = "Operation aborted"}, - #ejabberd_commands{name = export2odbc, tags = [mnesia], - desc = "Export virtual host information from Mnesia tables to SQL files", - module = ?MODULE, function = export2odbc, - args = [{host, string}, {directory, string}], + #ejabberd_commands{name = export2sql, tags = [mnesia], + desc = "Export virtual host information from Mnesia tables to SQL file", + longdesc = "Configure the modules to use SQL, then call this command. " + "After correctly exported the database of a vhost, " + "you may want to delete from mnesia with " + "the _`delete_mnesia`_ API.", + module = ejd2sql, function = export, + args_desc = ["Vhost", "Full path to the destination SQL file"], + args_example = ["example.com", "/var/lib/ejabberd/example.com.sql"], + args = [{host, string}, {file, string}], result = {res, rescode}}, - #ejabberd_commands{name = set_master, tags = [mnesia], + #ejabberd_commands{name = get_master, tags = [cluster], + desc = "Get master node of the clustered Mnesia tables", + note = "added in 24.06", + longdesc = "If there is no master, returns `none`.", + module = ?MODULE, function = get_master, + result = {nodename, atom}}, + #ejabberd_commands{name = set_master, tags = [cluster], desc = "Set master node of the clustered Mnesia tables", - longdesc = "If you provide as nodename \"self\", this " + longdesc = "If `nodename` is set to `self`, then this " "node will be set as its own master.", module = ?MODULE, function = set_master, + args_desc = ["Name of the erlang node that will be considered master of this node"], + args_example = ["ejabberd@machine7"], args = [{nodename, string}], result = {res, restuple}}, #ejabberd_commands{name = mnesia_change_nodename, tags = [mnesia], desc = "Change the erlang node name in a backup file", module = ?MODULE, function = mnesia_change_nodename, + args_desc = ["Name of the old erlang node", "Name of the new node", + "Path to old backup file", "Path to the new backup file"], + args_example = ["ejabberd@machine1", "ejabberd@machine2", + "/var/lib/ejabberd/old.backup", "/var/lib/ejabberd/new.backup"], args = [{oldnodename, string}, {newnodename, string}, {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`_ API.", 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 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 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 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`_ API and " + "_`install_fallback`_ API.", module = ?MODULE, function = load_mnesia, + args_desc = ["Full path to the text file"], + args_example = ["/var/lib/ejabberd/database.txt"], args = [{file, string}], result = {res, restuple}}, + #ejabberd_commands{name = mnesia_info, tags = [mnesia], + desc = "Dump info on global Mnesia state", + module = ?MODULE, function = mnesia_info, + args = [], result = {res, string}}, + #ejabberd_commands{name = mnesia_table_info, tags = [mnesia], + desc = "Dump info on Mnesia table state", + module = ?MODULE, function = mnesia_table_info, + args_desc = ["Mnesia table name"], + 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`_ API.", module = ?MODULE, function = install_fallback_mnesia, - args = [{file, string}], result = {res, restuple}} - ]. + args_desc = ["Full path to the fallback file"], + args_example = ["/var/lib/ejabberd/database.fallback"], + args = [{file, string}], result = {res, restuple}}, + #ejabberd_commands{name = clear_cache, tags = [server], + desc = "Clear database cache on all nodes", + module = ?MODULE, function = clear_cache, + args = [], result = {res, rescode}}, + #ejabberd_commands{name = gc, tags = [server], + desc = "Force full garbage collection", + note = "added in 20.01", + module = ?MODULE, function = gc, + args = [], result = {res, rescode}}, + #ejabberd_commands{name = man, tags = [documentation], + desc = "Generate Unix manpage for current ejabberd version", + note = "added in 20.01", + module = ejabberd_doc, function = man, + args = [], result = {res, restuple}}, + #ejabberd_commands{name = webadmin_host_user_queue, tags = [offline, internal], + desc = "Generate WebAdmin offline queue HTML", + module = mod_offline, function = webadmin_host_user_queue, + args = [{user, binary}, {host, binary}, {query, any}, {lang, binary}], + result = {res, any}}, + + #ejabberd_commands{name = webadmin_host_last_activity, tags = [internal], + desc = "Generate WebAdmin Last Activity HTML", + module = ejabberd_web_admin, function = webadmin_host_last_activity, + args = [{host, binary}, {query, any}, {lang, binary}], + result = {res, any}}, + #ejabberd_commands{name = webadmin_host_srg, tags = [internal], + desc = "Generate WebAdmin Shared Roster Group HTML", + module = mod_shared_roster, function = webadmin_host_srg, + args = [{host, binary}, {query, any}, {lang, binary}], + result = {res, any}}, + #ejabberd_commands{name = webadmin_host_srg_group, tags = [internal], + desc = "Generate WebAdmin Shared Roster Group HTML for a group", + module = mod_shared_roster, function = webadmin_host_srg_group, + args = [{host, binary}, {group, binary}, {query, any}, {lang, binary}], + result = {res, any}}, + + #ejabberd_commands{name = webadmin_node_contrib, tags = [internal], + desc = "Generate WebAdmin ejabberd-contrib HTML", + module = ext_mod, function = webadmin_node_contrib, + args = [{node, atom}, {query, any}, {lang, binary}], + result = {res, any}}, + #ejabberd_commands{name = webadmin_node_db, tags = [internal], + desc = "Generate WebAdmin Mnesia database HTML", + module = ejabberd_web_admin, function = webadmin_node_db, + args = [{node, atom}, {query, any}, {lang, binary}], + result = {res, any}}, + #ejabberd_commands{name = webadmin_node_db_table, tags = [internal], + desc = "Generate WebAdmin Mnesia database HTML for a table", + module = ejabberd_web_admin, function = webadmin_node_db_table, + args = [{node, atom}, {table, binary}, {lang, binary}], + result = {res, any}}, + #ejabberd_commands{name = webadmin_node_db_table_page, tags = [internal], + desc = "Generate WebAdmin Mnesia database HTML for a table content", + module = ejabberd_web_admin, function = webadmin_node_db_table_page, + args = [{node, atom}, {table, binary}, {page, integer}], + result = {res, any}}, + + #ejabberd_commands{name = mnesia_list_tables, tags = [mnesia], + desc = "List of Mnesia tables", + note = "added in 25.03", + module = ?MODULE, function = mnesia_list_tables, + result = {tables, {list, {table, {tuple, [{name, atom}, + {storage_type, binary}, + {elements, integer}, + {memory_kb, integer}, + {memory_mb, integer} + ]}}}}}, + #ejabberd_commands{name = mnesia_table_details, tags = [internal, mnesia], + desc = "Get details of a Mnesia table", + module = ?MODULE, function = mnesia_table_details, + args = [{table, binary}], + result = {details, {list, {detail, {tuple, [{name, atom}, + {value, binary} + ]}}}}}, + + #ejabberd_commands{name = mnesia_table_change_storage, tags = [mnesia], + desc = "Change storage type of a Mnesia table", + note = "added in 25.03", + longdesc = "Storage type can be: `ram_copies`, `disc_copies`, `disc_only_copies`, `remote_copy`.", + module = ?MODULE, function = mnesia_table_change_storage, + args = [{table, binary}, {storage_type, binary}], + result = {res, restuple}}, + #ejabberd_commands{name = mnesia_table_clear, tags = [internal, mnesia], + desc = "Delete all content in a Mnesia table", + module = ?MODULE, function = mnesia_table_clear, + args = [{table, binary}], + result = {res, restuple}}, + #ejabberd_commands{name = mnesia_table_destroy, tags = [internal, mnesia], + desc = "Destroy a Mnesia table", + module = ?MODULE, function = mnesia_table_destroy, + args = [{table, binary}], + result = {res, restuple}}, + #ejabberd_commands{name = echo, tags = [internal], + desc = "Return the same sentence that was provided", + module = ?MODULE, function = echo, + args_desc = ["Sentence to echoe"], + args_example = [<<"Test Sentence">>], + args = [{sentence, binary}], + result = {sentence, string}, + result_example = "Test Sentence"}, + #ejabberd_commands{name = echo3, tags = [internal], + desc = "Return the same sentence that was provided", + module = ?MODULE, function = echo3, + args_desc = ["First argument", "Second argument", "Sentence to echoe"], + args_example = [<<"example.com">>, <<"Group1">>, <<"Test Sentence">>], + args = [{first, binary}, {second, binary}, {sentence, binary}], + result = {sentence, string}, + result_example = "Test Sentence"} + ]. %%% %%% Server management @@ -248,61 +668,97 @@ status() -> {value, {_, _, Version}} -> {ok, io_lib:format("ejabberd ~s is running in that node", [Version])} end, - {Is_running, String1 ++ String2}. + {Is_running, String1 ++ "\n" ++String2}. + +stop() -> + _ = supervisor:terminate_child(ejabberd_sup, ejabberd_sm), + timer:sleep(1000), + init:stop(). + +restart() -> + _ = supervisor:terminate_child(ejabberd_sup, ejabberd_sm), + timer:sleep(1000), + init:restart(). reopen_log() -> - ejabberd_hooks:run(reopen_log_hook, []), - ejabberd_logger:reopen_log(). + ejabberd_hooks:run(reopen_log_hook, []). + +rotate_log() -> + ejabberd_hooks:run(rotate_log_hook, []). + +set_loglevel(LogLevel) -> + try binary_to_existing_atom(iolist_to_binary(LogLevel), latin1) of + Level -> + case lists:member(Level, ejabberd_logger:loglevels()) of + true -> + ejabberd_logger:set(Level); + false -> + {error, "Invalid log level"} + end + catch _:_ -> + {error, "Invalid log level"} + end. %%% %%% Stop Kindly %%% +evacuate_kindly(DelaySeconds, AnnouncementTextString) -> + perform_kindly(DelaySeconds, AnnouncementTextString, evacuate). + stop_kindly(DelaySeconds, AnnouncementTextString) -> - Subject = list_to_binary(io_lib:format("Server stop in ~p seconds!", [DelaySeconds])), - WaitingDesc = list_to_binary(io_lib:format("Waiting ~p seconds", [DelaySeconds])), + perform_kindly(DelaySeconds, AnnouncementTextString, stop). + +perform_kindly(DelaySeconds, AnnouncementTextString, Action) -> + Subject = str:format("Server stop in ~p seconds!", [DelaySeconds]), + WaitingDesc = str:format("Waiting ~p seconds", [DelaySeconds]), AnnouncementText = list_to_binary(AnnouncementTextString), - Steps = [ - {"Stopping ejabberd port listeners", - ejabberd_listener, stop_listeners, []}, - {"Sending announcement to connected users", - mod_announce, send_announcement_to_all, - [?MYNAME, Subject, AnnouncementText]}, - {"Sending service message to MUC rooms", - ejabberd_admin, send_service_message_all_mucs, - [Subject, AnnouncementText]}, - {WaitingDesc, timer, sleep, [DelaySeconds * 1000]}, - {"Stopping ejabberd", application, stop, [ejabberd]}, - {"Stopping Mnesia", mnesia, stop, []}, - {"Stopping Erlang node", init, stop, []} - ], + PreSteps = + [{"Stopping ejabberd port listeners", ejabberd_listener, stop_listeners, []}, + {"Sending announcement to connected users", + mod_announce, + send_announcement_to_all, + [ejabberd_config:get_myname(), Subject, AnnouncementText]}, + {"Sending service message to MUC rooms", + ejabberd_admin, + send_service_message_all_mucs, + [Subject, AnnouncementText]}, + {WaitingDesc, timer, sleep, [DelaySeconds * 1000]}, + {"Stopping ejabberd", application, stop, [ejabberd]}], + SpecificSteps = + case Action of + evacuate -> + [{"Starting ejabberd", application, start, [ejabberd]}, + {"Stopping ejabberd port listeners", ejabberd_listener, stop_listeners, []}]; + stop -> + [{"Stopping Mnesia", mnesia, stop, []}, {"Stopping Erlang node", init, stop, []}] + end, + Steps = PreSteps ++ SpecificSteps, NumberLast = length(Steps), TimestampStart = calendar:datetime_to_gregorian_seconds({date(), time()}), - lists:foldl( - fun({Desc, Mod, Func, Args}, NumberThis) -> - SecondsDiff = - calendar:datetime_to_gregorian_seconds({date(), time()}) - - TimestampStart, - io:format("[~p/~p ~ps] ~s... ", - [NumberThis, NumberLast, SecondsDiff, Desc]), - Result = (catch apply(Mod, Func, Args)), - io:format("~p~n", [Result]), - NumberThis+1 - end, - 1, - Steps), + lists:foldl(fun({Desc, Mod, Func, Args}, NumberThis) -> + SecondsDiff = + calendar:datetime_to_gregorian_seconds({date(), time()}) - TimestampStart, + io:format("[~p/~p ~ps] ~ts... ", [NumberThis, NumberLast, SecondsDiff, Desc]), + Result = (catch apply(Mod, Func, Args)), + io:format("~p~n", [Result]), + NumberThis + 1 + end, + 1, + Steps), ok. send_service_message_all_mucs(Subject, AnnouncementText) -> - Message = list_to_binary( - io_lib:format("~s~n~s", [Subject, AnnouncementText])), + Message = str:format("~s~n~s", [Subject, AnnouncementText]), lists:foreach( fun(ServerHost) -> - MUCHost = gen_mod:get_module_opt_host( - ServerHost, mod_muc, <<"conference.@HOST@">>), - mod_muc:broadcast_service_message(MUCHost, Message) + MUCHosts = gen_mod:get_module_opt_hosts(ServerHost, mod_muc), + lists:foreach( + fun(MUCHost) -> + mod_muc:broadcast_service_message(ServerHost, MUCHost, Message) + end, MUCHosts) end, - ?MYHOSTS). + ejabberd_option:hosts()). %%% %%% ejabberd_update @@ -314,8 +770,14 @@ update_list() -> [atom_to_list(Beam) || Beam <- UpdatedBeams]. update("all") -> - [update_module(ModStr) || ModStr <- update_list()], - {ok, []}; + ResList = [{ModStr, update_module(ModStr)} || ModStr <- update_list()], + String = case string:join([Mod || {Mod, {ok, _}} <- ResList], ", ") of + [] -> + "No modules updated"; + ModulesString -> + "Updated modules: " ++ ModulesString + end, + {ok, String}; update(ModStr) -> update_module(ModStr). @@ -324,44 +786,173 @@ update_module(ModuleNameBin) when is_binary(ModuleNameBin) -> update_module(ModuleNameString) -> ModuleName = list_to_atom(ModuleNameString), case ejabberd_update:update([ModuleName]) of - {ok, _Res} -> {ok, []}; - {error, Reason} -> {error, Reason} + {ok, []} -> + {ok, "Not updated: "++ModuleNameString}; + {ok, [ModuleName]} -> + {ok, "Updated: "++ModuleNameString}; + {error, Reason} -> {error, Reason} end. +update() -> + io:format("Compiling ejabberd...~n", []), + os:cmd("make"), + Mods = ejabberd_admin:update_list(), + io:format("Updating modules: ~p~n", [Mods]), + ejabberd_admin:update("all"), + Mods2 = Mods -- ejabberd_admin:update_list(), + io:format("Updated modules: ~p~n", [Mods2]), + ok. + %%% %%% Account management %%% register(User, Host, Password) -> - case ejabberd_auth:try_register(User, Host, Password) of - {atomic, ok} -> - {ok, io_lib:format("User ~s@~s successfully registered", [User, Host])}; - {atomic, exists} -> - String = io_lib:format("User ~s@~s already registered at node ~p", - [User, Host, node()]), - {exists, String}; - {error, Reason} -> - String = io_lib:format("Can't register user ~s@~s at node ~p: ~p", - [User, Host, node(), Reason]), - {cannot_register, String} + case is_my_host(Host) of + true -> + case ejabberd_auth:try_register(User, Host, Password) of + ok -> + {ok, io_lib:format("User ~s@~s successfully registered", [User, Host])}; + {error, exists} -> + Msg = io_lib:format("User ~s@~s already registered", [User, Host]), + {error, conflict, 10090, Msg}; + {error, Reason} -> + String = io_lib:format("Can't register user ~s@~s at node ~p: ~s", + [User, Host, node(), + mod_register:format_error(Reason)]), + {error, cannot_register, 10001, String} + end; + false -> + {error, cannot_register, 10001, "Unknown virtual host"} end. unregister(User, Host) -> - ejabberd_auth:remove_user(User, Host), - {ok, ""}. + case is_my_host(Host) of + true -> + ejabberd_auth:remove_user(User, Host), + {ok, ""}; + false -> + {error, "Unknown virtual host"} + end. registered_users(Host) -> - Users = ejabberd_auth:get_vh_registered_users(Host), - SUsers = lists:sort(Users), - lists:map(fun({U, _S}) -> U end, SUsers). + case is_my_host(Host) of + true -> + Users = ejabberd_auth:get_users(Host), + SUsers = lists:sort(Users), + lists:map(fun({U, _S}) -> U end, SUsers); + false -> + {error, "Unknown virtual host"} + end. registered_vhosts() -> - ?MYHOSTS. + ejabberd_option:hosts(). reload_config() -> - ejabberd_config:reload_file(), - acl:start(), - shaper:start(). + case ejabberd_config:reload() of + ok -> ok; + Err -> + Reason = ejabberd_config:format_error(Err), + {error, Reason} + end. + +dump_config(Path) -> + case ejabberd_config:dump(Path) of + ok -> ok; + Err -> + Reason = ejabberd_config:format_error(Err), + {error, Reason} + end. + +convert_to_yaml(In, Out) -> + case ejabberd_config:convert_to_yaml(In, Out) of + ok -> {ok, ""}; + Err -> + Reason = ejabberd_config:format_error(Err), + {error, Reason} + end. + +%%% +%%% Cluster management +%%% + +join_cluster(NodeBin) when is_binary(NodeBin) -> + join_cluster(list_to_atom(binary_to_list(NodeBin))); +join_cluster(Node) when is_atom(Node) -> + IsNodes = lists:member(Node, ejabberd_cluster:get_nodes()), + IsKnownNodes = lists:member(Node, ejabberd_cluster:get_known_nodes()), + Ping = net_adm:ping(Node), + join_cluster(Node, IsNodes, IsKnownNodes, Ping). + +join_cluster(_Node, true, _IsKnownNodes, _Ping) -> + {error, "This node already joined that running node."}; +join_cluster(_Node, _IsNodes, true, _Ping) -> + {error, "This node already joined that known node."}; +join_cluster(_Node, _IsNodes, _IsKnownNodes, pang) -> + {error, "This node cannot reach that node."}; +join_cluster(Node, false, false, pong) -> + case timer:apply_after(1000, ejabberd_cluster, join, [Node]) of + {ok, _} -> + {ok, "Trying to join that cluster, wait a few seconds and check the list of nodes."}; + Error -> + {error, io_lib:format("Can't join that cluster: ~p", [Error])} + end. + +join_cluster_here(NodeBin) -> + Node = list_to_atom(binary_to_list(NodeBin)), + IsNodes = lists:member(Node, ejabberd_cluster:get_nodes()), + IsKnownNodes = lists:member(Node, ejabberd_cluster:get_known_nodes()), + Ping = net_adm:ping(Node), + join_cluster_here(Node, IsNodes, IsKnownNodes, Ping). + +join_cluster_here(_Node, true, _IsKnownNodes, _Ping) -> + {error, "This node already joined that running node."}; +join_cluster_here(_Node, _IsNodes, true, _Ping) -> + {error, "This node already joined that known node."}; +join_cluster_here(_Node, _IsNodes, _IsKnownNodes, pang) -> + {error, "This node cannot reach that node."}; +join_cluster_here(Node, false, false, pong) -> + case ejabberd_cluster:call(Node, ejabberd_admin, join_cluster, [misc:atom_to_binary(node())]) of + {ok, _} -> + {ok, "Trying to join node to this cluster, wait a few seconds and check the list of nodes."}; + Error -> + {error, io_lib:format("Can't join node to this cluster: ~p", [Error])} + end. + +leave_cluster(NodeBin) when is_binary(NodeBin) -> + leave_cluster(list_to_atom(binary_to_list(NodeBin))); +leave_cluster(Node) -> + ejabberd_cluster:leave(Node). + +list_cluster() -> + ejabberd_cluster:get_nodes(). + +list_cluster_detailed() -> + KnownNodes = ejabberd_cluster:get_known_nodes(), + RunningNodes = ejabberd_cluster:get_nodes(), + [get_cluster_node_details(Node, RunningNodes) || Node <- KnownNodes]. + +get_cluster_node_details(Node, RunningNodes) -> + get_cluster_node_details2(Node, lists:member(Node, RunningNodes)). + +get_cluster_node_details2(Node, false) -> + {Node, "false", "", -1, -1, -1, unknown}; +get_cluster_node_details2(Node, true) -> + try ejabberd_cluster:call(Node, ejabberd_admin, get_cluster_node_details3, []) of + Result -> Result + catch + E:R -> + Status = io_lib:format("~p: ~p", [E, R]), + {Node, "true", Status, -1, -1, -1, unknown} + end. + +get_cluster_node_details3() -> + {ok, StatusString} = status(), + UptimeSeconds = mod_admin_extra:stats(<<"uptimeseconds">>), + Processes = mod_admin_extra:stats(<<"processes">>), + OnlineUsers = mod_admin_extra:stats(<<"onlineusersnode">>), + GetMaster = get_master(), + {node(), "true", StatusString, OnlineUsers, Processes, UptimeSeconds, GetMaster}. %%% %%% Migration management @@ -387,7 +978,6 @@ import_dir(Path) -> {cannot_import_dir, String} end. - %%% %%% Purge DB %%% @@ -396,34 +986,81 @@ delete_expired_messages() -> lists:foreach( fun(Host) -> {atomic, ok} = mod_offline:remove_expired_messages(Host) - end, ?MYHOSTS). + end, ejabberd_option:hosts()). delete_old_messages(Days) -> lists:foreach( fun(Host) -> {atomic, _} = mod_offline:remove_old_messages(Days, Host) - end, ?MYHOSTS). + end, ejabberd_option:hosts()). + +delete_old_messages_batch(Server, Days, BatchSize, Rate) -> + LServer = jid:nameprep(Server), + Mod = gen_mod:db_mod(LServer, mod_offline), + case ejabberd_batch:register_task({spool, LServer}, 0, Rate, {LServer, Days, BatchSize, none}, + fun({L, Da, B, IS} = S) -> + case {erlang:function_exported(Mod, remove_old_messages_batch, 3), + erlang:function_exported(Mod, remove_old_messages_batch, 4)} of + {true, _} -> + case Mod:remove_old_messages_batch(L, Da, B) of + {ok, Count} -> + {ok, S, Count}; + {error, _} = E -> + E + end; + {_, true} -> + case Mod:remove_old_messages_batch(L, Da, B, IS) of + {ok, IS2, Count} -> + {ok, {L, Da, B, IS2}, Count}; + {error, _} = E -> + E + end; + _ -> + {error, not_implemented_for_backend} + end + end) of + ok -> + {ok, ""}; + {error, in_progress} -> + {error, "Operation in progress"} + end. + +delete_old_messages_status(Server) -> + LServer = jid:nameprep(Server), + Msg = case ejabberd_batch:task_status({spool, LServer}) of + not_started -> + "Operation not started"; + {failed, Steps, Error} -> + io_lib:format("Operation failed after deleting ~p messages with error ~p", + [Steps, misc:format_val(Error)]); + {aborted, Steps} -> + io_lib:format("Operation was aborted after deleting ~p messages", + [Steps]); + {working, Steps} -> + io_lib:format("Operation in progress, deleted ~p messages", + [Steps]); + {completed, Steps} -> + io_lib:format("Operation was completed after deleting ~p messages", + [Steps]) + end, + lists:flatten(Msg). + +delete_old_messages_abort(Server) -> + LServer = jid:nameprep(Server), + case ejabberd_batch:abort_task({spool, LServer}) of + aborted -> "Operation aborted"; + not_started -> "No task running" + end. %%% %%% Mnesia management %%% -export2odbc(Host, Directory) -> - Tables = [{export_last, last}, - {export_offline, offline}, - {export_private_storage, private_storage}, - {export_roster, roster}, - {export_vcard, vcard}, - {export_vcard_search, vcard_search}, - {export_passwd, passwd}], - Export = fun({TableFun, Table}) -> - Filename = filename:join([Directory, atom_to_list(Table)++".txt"]), - io:format("Trying to export Mnesia table '~p' on Host '~s' to file '~s'~n", [Table, Host, Filename]), - Res = (catch ejd2odbc:TableFun(Host, Filename)), - io:format(" Result: ~p~n", [Res]) - end, - lists:foreach(Export, Tables), - ok. +get_master() -> + case mnesia:table_info(session, master_nodes) of + [] -> none; + [Node] -> Node + end. set_master("self") -> set_master(node()); @@ -432,7 +1069,7 @@ set_master(NodeString) when is_list(NodeString) -> set_master(Node) when is_atom(Node) -> case mnesia:set_master_nodes([Node]) of ok -> - {ok, ""}; + {ok, "ok"}; {error, Reason} -> String = io_lib:format("Can't set master node ~p at node ~p:~n~p", [Node, node(), Reason]), @@ -453,10 +1090,6 @@ restore_mnesia(Path) -> case ejabberd_admin:restore(Path) of {atomic, _} -> {ok, ""}; - {error, Reason} -> - String = io_lib:format("Can't restore backup from ~p at node ~p: ~p", - [filename:absname(Path), node(), Reason]), - {cannot_restore, String}; {aborted,{no_exists,Table}} -> String = io_lib:format("Can't restore backup from ~p at node ~p: Table ~p does not exist.", [filename:absname(Path), node(), Table]), @@ -469,7 +1102,7 @@ restore_mnesia(Path) -> %% Mnesia database restore %% This function is called from ejabberd_ctl, ejabberd_web_admin and -%% mod_configure/adhoc +%% mod_configure/adhoc restore(Path) -> mnesia:restore(Path, [{keep_tables,keep_tables()}, {default_op, skip_tables}]). @@ -479,20 +1112,19 @@ restore(Path) -> %% Obsolete tables or tables created by module who are no longer used are not %% restored and are ignored. keep_tables() -> - lists:flatten([acl, passwd, config, local_config, + lists:flatten([acl, passwd, config, keep_modules_tables()]). %% Returns the list of modules tables in use, according to the list of actually %% loaded modules -keep_modules_tables() -> +keep_modules_tables() -> lists:map(fun(Module) -> module_tables(Module) end, - gen_mod:loaded_modules(?MYNAME)). + gen_mod:loaded_modules(ejabberd_config:get_myname())). %% TODO: This mapping should probably be moved to a callback function in each %% module. %% Mapping between modules and their tables module_tables(mod_announce) -> [motd, motd_users]; -module_tables(mod_irc) -> [irc_custom]; module_tables(mod_last) -> [last_activity]; module_tables(mod_muc) -> [muc_room, muc_registered]; module_tables(mod_offline) -> [offline_msg]; @@ -572,6 +1204,13 @@ load_mnesia(Path) -> {cannot_load, String} end. +mnesia_info() -> + lists:flatten(io_lib:format("~p", [mnesia:system_info(all)])). + +mnesia_table_info(Table) -> + ATable = list_to_atom(Table), + lists:flatten(io_lib:format("~p", [mnesia:table_info(ATable, all)])). + install_fallback_mnesia(Path) -> case mnesia:install_fallback(Path) of ok -> @@ -627,3 +1266,256 @@ mnesia_change_nodename(FromString, ToString, Source, Target) -> {[Other], Acc} end, mnesia:traverse_backup(Source, Target, Convert, switched). + +clear_cache() -> + Nodes = ejabberd_cluster:get_nodes(), + lists:foreach(fun(T) -> ets_cache:clear(T, Nodes) end, ets_cache:all()). + +gc() -> + lists:foreach(fun erlang:garbage_collect/1, processes()). + +-spec is_my_host(binary()) -> boolean(). +is_my_host(Host) -> + try ejabberd_router:is_my_host(Host) + catch _:{invalid_domain, _} -> false + end. + +%%% +%%% Internal +%%% + +%% @format-begin + +mnesia_table_change_storage(STable, SType) -> + Table = binary_to_existing_atom(STable, latin1), + Type = + case SType of + <<"remote_copy">> -> + remote_copy; + <<"ram_copies">> -> + ram_copies; + <<"disc_copies">> -> + disc_copies; + <<"disc_only_copies">> -> + disc_only_copies; + _ -> + false + end, + Node = node(), + Result = + case Type of + false -> + "Nothing to do"; + remote_copy -> + mnesia:del_table_copy(Table, Node), + "Deleted table copy"; + _ -> + case mnesia:add_table_copy(Table, Node, Type) of + {aborted, _} -> + mnesia:change_table_copy_type(Table, Node, Type), + "Changed table copy type"; + _ -> + "Added table copy" + end + end, + {ok, Result}. + +mnesia_table_clear(STable) -> + Table = binary_to_existing_atom(STable, latin1), + mnesia:clear_table(Table). + +mnesia_table_delete(STable) -> + Table = binary_to_existing_atom(STable, latin1), + mnesia:delete_table(Table). + +mnesia_table_details(STable) -> + Table = binary_to_existing_atom(STable, latin1), + [{Name, iolist_to_binary(str:format("~p", [Value]))} + || {Name, Value} <- mnesia:table_info(Table, all)]. + +mnesia_list_tables() -> + STables = + lists:sort( + mnesia:system_info(tables)), + lists:map(fun(Table) -> + TInfo = mnesia:table_info(Table, all), + {value, {storage_type, Type}} = lists:keysearch(storage_type, 1, TInfo), + {value, {size, Size}} = lists:keysearch(size, 1, TInfo), + {value, {memory, Memory}} = lists:keysearch(memory, 1, TInfo), + MemoryB = Memory * erlang:system_info(wordsize), + MemoryKB = MemoryB div 1024, + MemoryMB = MemoryKB div 1024, + {Table, storage_type_bin(Type), Size, MemoryKB, MemoryMB} + end, + STables). + +storage_type_bin(ram_copies) -> + <<"RAM copy">>; +storage_type_bin(disc_copies) -> + <<"RAM and disc copy">>; +storage_type_bin(disc_only_copies) -> + <<"Disc only copy">>; +storage_type_bin(unknown) -> + <<"Remote copy">>. + +echo(Sentence) -> + Sentence. + +echo3(_, _, Sentence) -> + Sentence. + +%%% +%%% Web Admin: Main +%%% + +web_menu_main(Acc, _Lang) -> + Acc ++ [{<<"purge">>, <<"Purge">>}, {<<"stanza">>, <<"Stanza">>}]. + +web_page_main(_, #request{path = [<<"purge">>]} = R) -> + Types = + [{<<"#erlang">>, <<"Erlang">>}, + {<<"#users">>, <<"Users">>}, + {<<"#offline">>, <<"Offline">>}, + {<<"#mam">>, <<"MAM">>}, + {<<"#pubsub">>, <<"PubSub">>}, + {<<"#push">>, <<"Push">>}], + Head = [?XC(<<"h1">>, <<"Purge">>)], + Set = [?XE(<<"ul">>, [?LI([?AC(MIU, MIN)]) || {MIU, MIN} <- Types]), + ?X(<<"hr">>), + ?XAC(<<"h2">>, [{<<"id">>, <<"erlang">>}], <<"Erlang">>), + ?XE(<<"blockquote">>, + [ejabberd_web_admin:make_command(clear_cache, R), + ejabberd_web_admin:make_command(gc, R)]), + ?X(<<"hr">>), + ?XAC(<<"h2">>, [{<<"id">>, <<"users">>}], <<"Users">>), + ?XE(<<"blockquote">>, [ejabberd_web_admin:make_command(delete_old_users, R)]), + ?X(<<"hr">>), + ?XAC(<<"h2">>, [{<<"id">>, <<"offline">>}], <<"Offline">>), + ?XE(<<"blockquote">>, + [ejabberd_web_admin:make_command(delete_expired_messages, R), + ejabberd_web_admin:make_command(delete_old_messages, R), + ejabberd_web_admin:make_command(delete_old_messages_batch, R), + ejabberd_web_admin:make_command(delete_old_messages_status, R)]), + ?X(<<"hr">>), + ?XAC(<<"h2">>, [{<<"id">>, <<"mam">>}], <<"MAM">>), + ?XE(<<"blockquote">>, + [ejabberd_web_admin:make_command(delete_old_mam_messages, R), + ejabberd_web_admin:make_command(delete_old_mam_messages_batch, R), + ejabberd_web_admin:make_command(delete_old_mam_messages_status, R)]), + ?X(<<"hr">>), + ?XAC(<<"h2">>, [{<<"id">>, <<"pubsub">>}], <<"PubSub">>), + ?XE(<<"blockquote">>, + [ejabberd_web_admin:make_command(delete_expired_pubsub_items, R), + ejabberd_web_admin:make_command(delete_old_pubsub_items, R)]), + ?X(<<"hr">>), + ?XAC(<<"h2">>, [{<<"id">>, <<"push">>}], <<"Push">>), + ?XE(<<"blockquote">>, [ejabberd_web_admin:make_command(delete_old_push_sessions, R)])], + {stop, Head ++ Set}; +web_page_main(_, #request{path = [<<"stanza">>]} = R) -> + Head = [?XC(<<"h1">>, <<"Stanza">>)], + Set = [ejabberd_web_admin:make_command(send_message, R), + ejabberd_web_admin:make_command(send_stanza, R), + ejabberd_web_admin:make_command(send_stanza_c2s, R)], + {stop, Head ++ Set}; +web_page_main(Acc, _) -> + Acc. + +%%% +%%% Web Admin: Node +%%% + +web_menu_node(Acc, _Node, _Lang) -> + Acc + ++ [{<<"cluster">>, <<"Clustering">>}, + {<<"update">>, <<"Code Update">>}, + {<<"config-file">>, <<"Configuration File">>}, + {<<"logs">>, <<"Logs">>}, + {<<"stop">>, <<"Stop Node">>}]. + +web_page_node(_, Node, #request{path = [<<"cluster">>]} = R) -> + {ok, Names} = net_adm:names(), + NodeNames = lists:join(", ", [Name || {Name, _Port} <- Names]), + Hint = + list_to_binary(io_lib:format("Hint: Erlang nodes found in this machine that may be running ejabberd: ~s", + [NodeNames])), + Head = ?H1GLraw(<<"Clustering">>, <<"admin/guide/clustering/">>, <<"Clustering">>), + Set1 = + [ejabberd_cluster:call(Node, + ejabberd_web_admin, + make_command, + [join_cluster_here, R, [], []]), + ?XE(<<"blockquote">>, [?C(Hint)]), + ejabberd_cluster:call(Node, + ejabberd_web_admin, + make_command, + [join_cluster, R, [], [{style, danger}]]), + ?XE(<<"blockquote">>, [?C(Hint)]), + ejabberd_cluster:call(Node, + ejabberd_web_admin, + make_command, + [leave_cluster, R, [], [{style, danger}]])], + Set2 = + [ejabberd_cluster:call(Node, + ejabberd_web_admin, + make_command, + [set_master, R, [], [{style, danger}]])], + timer:sleep(100), % leaving a cluster takes a while, let's delay the get commands + Get1 = + [ejabberd_cluster:call(Node, + ejabberd_web_admin, + make_command, + [list_cluster_detailed, + R, + [], + [{result_links, [{name, node, 3, <<"">>}]}]])], + Get2 = + [ejabberd_cluster:call(Node, + ejabberd_web_admin, + make_command, + [get_master, + R, + [], + [{result_named, true}, + {result_links, [{nodename, node, 3, <<"">>}]}]])], + {stop, Head ++ Get1 ++ Set1 ++ Get2 ++ Set2}; +web_page_node(_, Node, #request{path = [<<"update">>]} = R) -> + Head = [?XC(<<"h1">>, <<"Code Update">>)], + Set = [ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [update, R])], + Get = [ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [update_list, R])], + {stop, Head ++ Get ++ Set}; +web_page_node(_, Node, #request{path = [<<"config-file">>]} = R) -> + Res = ?H1GLraw(<<"Configuration File">>, + <<"admin/configuration/file-format/">>, + <<"File Format">>) + ++ [ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [convert_to_yaml, R]), + ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [dump_config, R]), + ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [reload_config, R])], + {stop, Res}; +web_page_node(_, Node, #request{path = [<<"stop">>]} = R) -> + Res = [?XC(<<"h1">>, <<"Stop This Node">>), + ejabberd_cluster:call(Node, + ejabberd_web_admin, + make_command, + [restart, R, [], [{style, danger}]]), + ejabberd_cluster:call(Node, + ejabberd_web_admin, + make_command, + [stop_kindly, R, [], [{style, danger}]]), + ejabberd_cluster:call(Node, + ejabberd_web_admin, + make_command, + [stop, R, [], [{style, danger}]]), + ejabberd_cluster:call(Node, + ejabberd_web_admin, + make_command, + [halt, R, [], [{style, danger}]])], + {stop, Res}; +web_page_node(_, Node, #request{path = [<<"logs">>]} = R) -> + Res = ?H1GLraw(<<"Logs">>, <<"admin/configuration/basic/#logging">>, <<"Logging">>) + ++ [ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [set_loglevel, R]), + ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [get_loglevel, R]), + ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [reopen_log, R]), + ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [rotate_log, R])], + {stop, Res}; +web_page_node(Acc, _, _) -> + Acc. diff --git a/src/ejabberd_app.erl b/src/ejabberd_app.erl index 957aa5d46..cbe74fb08 100644 --- a/src/ejabberd_app.erl +++ b/src/ejabberd_app.erl @@ -5,7 +5,7 @@ %%% Created : 31 Jan 2003 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -24,158 +24,105 @@ %%%---------------------------------------------------------------------- -module(ejabberd_app). + -author('alexey@process-one.net'). -behaviour(application). --export([start_modules/0,start/2, prep_stop/1, stop/1, init/0]). +-export([start/2, prep_stop/1, stop/1]). --include("ejabberd.hrl"). -include("logger.hrl"). + %%% %%% Application API %%% start(normal, _Args) -> - ejabberd_logger:start(), - write_pid_file(), - start_apps(), - ejabberd:check_app(ejabberd), - randoms:start(), - db_init(), - start(), - translate:start(), - ejabberd_ctl:init(), - ejabberd_commands:init(), - ejabberd_admin:start(), - gen_mod:start(), - ejabberd_config:start(), - set_loglevel_from_config(), - acl:start(), - shaper:start(), - connect_nodes(), - Sup = ejabberd_sup:start_link(), - ejabberd_rdbms:start(), - ejabberd_riak_sup:start(), - ejabberd_auth:start(), - cyrsasl:start(), - % Profiling - %ejabberd_debug:eprof_start(), - %ejabberd_debug:fprof_start(), - maybe_add_nameservers(), - start_modules(), - ejabberd_listener:start_listeners(), - ?INFO_MSG("ejabberd ~s is started in the node ~p", [?VERSION, node()]), - Sup; + try + {T1, _} = statistics(wall_clock), + ejabberd_logger:start(), + write_pid_file(), + start_included_apps(), + misc:warn_unset_home(), + start_elixir_application(), + setup_if_elixir_conf_used(), + case ejabberd_config:load() of + ok -> + ejabberd_mnesia:start(), + file_queue_init(), + maybe_add_nameservers(), + case ejabberd_sup:start_link() of + {ok, SupPid} -> + ejabberd_system_monitor:start(), + register_elixir_config_hooks(), + ejabberd_cluster:wait_for_sync(infinity), + ejabberd_hooks:run(ejabberd_started, []), + ejabberd:check_apps(), + ejabberd_systemd:ready(), + maybe_start_exsync(), + {T2, _} = statistics(wall_clock), + ?INFO_MSG("ejabberd ~ts is started in the node ~p in ~.2fs", + [ejabberd_option:version(), + node(), (T2-T1)/1000]), + maybe_print_elixir_version(), + ?INFO_MSG("~ts", + [erlang:system_info(system_version)]), + {ok, SupPid}; + Err -> + ?CRITICAL_MSG("Failed to start ejabberd application: ~p", [Err]), + ejabberd:halt() + end; + Err -> + ?CRITICAL_MSG("Failed to start ejabberd application: ~ts", + [ejabberd_config:format_error(Err)]), + ejabberd:halt() + end + catch throw:{?MODULE, Error} -> + ?DEBUG("Failed to start ejabberd application: ~p", [Error]), + ejabberd:halt() + end; start(_, _) -> {error, badarg}. +start_included_apps() -> + {ok, Apps} = application:get_key(ejabberd, included_applications), + lists:foreach( + fun(mnesia) -> + ok; + (lager) -> + ok; + (os_mon)-> + ok; + (App) -> + application:ensure_all_started(App) + end, Apps). + %% Prepare the application for termination. %% This function is called when an application is about to be stopped, %% before shutting down the processes of the application. prep_stop(State) -> - ejabberd_listener:stop_listeners(), - stop_modules(), - ejabberd_admin:stop(), - broadcast_c2s_shutdown(), - timer:sleep(5000), + ejabberd_systemd:stopping(), + ejabberd_hooks:run(ejabberd_stopping, []), + ejabberd_listener:stop(), + ejabberd_sm:stop(), + ejabberd_service:stop(), + ejabberd_s2s:stop(), + ejabberd_system_monitor:stop(), + gen_mod:prep_stop(), + gen_mod:stop(), State. %% All the processes were killed when this function is called stop(_State) -> - ?INFO_MSG("ejabberd ~s is stopped in the node ~p", [?VERSION, node()]), - delete_pid_file(), - %%ejabberd_debug:stop(), - ok. - + ?INFO_MSG("ejabberd ~ts is stopped in the node ~p", + [ejabberd_option:version(), node()]), + delete_pid_file(). %%% %%% Internal functions %%% -start() -> - spawn_link(?MODULE, init, []). - -init() -> - register(ejabberd, self()), - loop(). - -loop() -> - receive - _ -> - loop() - end. - -db_init() -> - MyNode = node(), - DbNodes = mnesia:system_info(db_nodes), - case lists:member(MyNode, DbNodes) of - true -> - ok; - false -> - ?CRITICAL_MSG("Node name mismatch: I'm [~s], " - "the database is owned by ~p", [MyNode, DbNodes]), - ?CRITICAL_MSG("Either set ERLANG_NODE in ejabberdctl.cfg " - "or change node name in Mnesia", []), - erlang:error(node_name_mismatch) - end, - case mnesia:system_info(extra_db_nodes) of - [] -> - mnesia:create_schema([node()]); - _ -> - ok - end, - ejabberd:start_app(mnesia, permanent), - mnesia:wait_for_tables(mnesia:system_info(local_tables), infinity). - -%% Start all the modules in all the hosts -start_modules() -> - lists:foreach( - fun(Host) -> - Modules = ejabberd_config:get_option( - {modules, Host}, - fun(Mods) -> - lists:map( - fun({M, A}) when is_atom(M), is_list(A) -> - {M, A} - end, Mods) - end, []), - lists:foreach( - fun({Module, Args}) -> - gen_mod:start_module(Host, Module, Args) - end, Modules) - end, ?MYHOSTS). - -%% Stop all the modules in all the hosts -stop_modules() -> - lists:foreach( - fun(Host) -> - Modules = ejabberd_config:get_option( - {modules, Host}, - fun(Mods) -> - lists:map( - fun({M, A}) when is_atom(M), is_list(A) -> - {M, A} - end, Mods) - end, []), - lists:foreach( - fun({Module, _Args}) -> - gen_mod:stop_module_keep_config(Host, Module) - end, Modules) - end, ?MYHOSTS). - -connect_nodes() -> - Nodes = ejabberd_config:get_option( - cluster_nodes, - fun(Ns) -> - true = lists:all(fun is_atom/1, Ns), - Ns - end, []), - lists:foreach(fun(Node) -> - net_kernel:connect_node(Node) - end, Nodes). - %% If ejabberd is running on some Windows machine, get nameservers and add to Erlang maybe_add_nameservers() -> case os:type() of @@ -188,16 +135,6 @@ add_windows_nameservers() -> ?INFO_MSG("Adding machine's DNS IPs to Erlang system:~n~p", [IPTs]), lists:foreach(fun(IPT) -> inet_db:add_ns(IPT) end, IPTs). - -broadcast_c2s_shutdown() -> - Children = ejabberd_sm:get_all_pids(), - lists:foreach( - fun(C2SPid) when node(C2SPid) == node() -> - C2SPid ! system_shutdown; - (_) -> - ok - end, Children). - %%% %%% PID file %%% @@ -211,13 +148,13 @@ write_pid_file() -> end. write_pid_file(Pid, PidFilename) -> - case file:open(PidFilename, [write]) of - {ok, Fd} -> - io:format(Fd, "~s~n", [Pid]), - file:close(Fd); - {error, Reason} -> - ?ERROR_MSG("Cannot write PID file ~s~nReason: ~p", [PidFilename, Reason]), - throw({cannot_write_pid_file, PidFilename, Reason}) + case file:write_file(PidFilename, io_lib:format("~ts~n", [Pid])) of + ok -> + ok; + {error, Reason} = Err -> + ?CRITICAL_MSG("Cannot write PID file ~ts: ~ts", + [PidFilename, file:format_error(Reason)]), + throw({?MODULE, Err}) end. delete_pid_file() -> @@ -228,19 +165,62 @@ delete_pid_file() -> file:delete(PidFilename) end. -set_loglevel_from_config() -> - Level = ejabberd_config:get_option( - loglevel, - fun(P) when P>=0, P=<5 -> P end, - 4), - ejabberd_logger:set(Level). +file_queue_init() -> + QueueDir = case ejabberd_option:queue_dir() of + undefined -> + MnesiaDir = mnesia:system_info(directory), + filename:join(MnesiaDir, "queue"); + Path -> + Path + end, + case p1_queue:start(QueueDir) of + ok -> ok; + Err -> throw({?MODULE, Err}) + end. -start_apps() -> - ejabberd:start_app(sasl), - ejabberd:start_app(ssl), - ejabberd:start_app(p1_yaml), - ejabberd:start_app(p1_tls), - ejabberd:start_app(p1_xml), - ejabberd:start_app(p1_stringprep), - ejabberd:start_app(p1_zlib), - ejabberd:start_app(p1_cache_tab). +%%% +%%% Elixir +%%% + +-ifdef(ELIXIR_ENABLED). +is_using_elixir_config() -> + Config = ejabberd_config:path(), + try 'Elixir.Ejabberd.ConfigUtil':is_elixir_config(Config) of + B when is_boolean(B) -> B + catch + _:_ -> false + end. + +setup_if_elixir_conf_used() -> + case is_using_elixir_config() of + true -> 'Elixir.Ejabberd.Config.Store':start_link(); + false -> ok + end. + +register_elixir_config_hooks() -> + case is_using_elixir_config() of + true -> 'Elixir.Ejabberd.Config':start_hooks(); + false -> ok + end. + +start_elixir_application() -> + case application:ensure_started(elixir) of + ok -> ok; + {error, _Msg} -> ?ERROR_MSG("Elixir application not started.", []) + end. + +maybe_start_exsync() -> + case os:getenv("RELIVE") of + "true" -> rpc:call(node(), 'Elixir.ExSync.Application', start, []); + _ -> ok + end. + +maybe_print_elixir_version() -> + ?INFO_MSG("Elixir ~ts", [maps:get(build, 'Elixir.System':build_info())]). +-else. +setup_if_elixir_conf_used() -> ok. +register_elixir_config_hooks() -> ok. +start_elixir_application() -> ok. +maybe_start_exsync() -> ok. +maybe_print_elixir_version() -> ok. +-endif. diff --git a/src/ejabberd_auth.erl b/src/ejabberd_auth.erl index 34d4a52b2..0c5d2fc69 100644 --- a/src/ejabberd_auth.erl +++ b/src/ejabberd_auth.erl @@ -1,11 +1,11 @@ %%%---------------------------------------------------------------------- %%% File : ejabberd_auth.erl %%% Author : Alexey Shchepin -%%% Purpose : Authentification +%%% Purpose : Authentication %%% Created : 23 Nov 2002 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -22,31 +22,46 @@ %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- - -%% TODO: Use the functions in ejabberd auth to add and remove users. - -module(ejabberd_auth). +-behaviour(gen_server). + -author('alexey@process-one.net'). +-protocol({rfc, 5802}). + %% External exports --export([start/0, set_password/3, check_password/3, - check_password/5, check_password_with_authmodule/3, - check_password_with_authmodule/5, try_register/3, - dirty_get_registered_users/0, get_vh_registered_users/1, - get_vh_registered_users/2, export/1, import/1, - get_vh_registered_users_number/1, import/3, - get_vh_registered_users_number/2, get_password/2, +-export([start_link/0, host_up/1, host_down/1, config_reloaded/0, + set_password/3, check_password/4, + check_password/6, check_password_with_authmodule/4, + check_password_with_authmodule/6, try_register/3, + get_users/0, get_users/1, password_to_scram/2, + get_users/2, import_info/0, + count_users/1, import/5, import_start/2, + count_users/2, get_password/2, get_password_s/2, get_password_with_authmodule/2, - is_user_exists/2, is_user_exists_in_other_modules/3, + user_exists/2, user_exists_in_other_modules/3, remove_user/2, remove_user/3, plain_password_required/1, - store_type/1, entropy/1]). + store_type/1, entropy/1, backend_type/1, password_format/1, + which_users_exists/1]). +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). --export([auth_modules/1]). +-export([auth_modules/1, convert_to_scram/1, drop_password_type/2, set_password_instance/3]). --include("ejabberd.hrl"). +-include_lib("xmpp/include/scram.hrl"). -include("logger.hrl"). +-define(SALT_LENGTH, 16). + +-record(state, {host_modules = #{} :: host_modules()}). + +-type host_modules() :: #{binary => [module()]}. +-type password() :: binary() | #scram{}. +-type digest_fun() :: fun((binary()) -> binary()). +-export_type([password/0]). + %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- @@ -55,332 +70,563 @@ {offset, integer()}]. -callback start(binary()) -> any(). --callback plain_password_required() -> boolean(). --callback store_type() -> plain | external | scram. --callback set_password(binary(), binary(), binary()) -> ok | {error, atom()}. --callback remove_user(binary(), binary()) -> any(). --callback remove_user(binary(), binary(), binary()) -> any(). --callback is_user_exists(binary(), binary()) -> boolean() | {error, atom()}. --callback check_password(binary(), binary(), binary()) -> boolean(). --callback check_password(binary(), binary(), binary(), binary(), - fun((binary()) -> binary())) -> boolean(). --callback try_register(binary(), binary(), binary()) -> {atomic, atom()} | - {error, atom()}. --callback dirty_get_registered_users() -> [{binary(), binary()}]. --callback get_vh_registered_users(binary()) -> [{binary(), binary()}]. --callback get_vh_registered_users(binary(), opts()) -> [{binary(), binary()}]. --callback get_vh_registered_users_number(binary()) -> number(). --callback get_vh_registered_users_number(binary(), opts()) -> number(). --callback get_password(binary(), binary()) -> false | binary(). --callback get_password_s(binary(), binary()) -> binary(). +-callback stop(binary()) -> any(). +-callback reload(binary()) -> any(). +-callback plain_password_required(binary()) -> boolean(). +-callback store_type(binary()) -> plain | external | scram. +-callback set_password(binary(), binary(), password()) -> + {ets_cache:tag(), {ok, password()} | {error, db_failure | not_allowed}}. +-callback set_password_multiple(binary(), binary(), [password()]) -> + {ets_cache:tag(), {ok, [password()]} | {error, db_failure | not_allowed}}. +-callback set_password_instance(binary(), binary(), password()) -> + ok | {error, db_failure | not_allowed}. +-callback remove_user(binary(), binary()) -> ok | {error, db_failure | not_allowed}. +-callback user_exists(binary(), binary()) -> {ets_cache:tag(), boolean() | {error, db_failure}}. +-callback check_password(binary(), binary(), binary(), binary()) -> {ets_cache:tag(), boolean() | {stop, boolean()}}. +-callback try_register(binary(), binary(), password()) -> + {ets_cache:tag(), {ok, password()} | {error, exists | db_failure | not_allowed}}. +-callback try_register_multiple(binary(), binary(), [password()]) -> + {ets_cache:tag(), {ok, [password()]} | {error, exists | db_failure | not_allowed}}. +-callback get_users(binary(), opts()) -> [{binary(), binary()}]. +-callback count_users(binary(), opts()) -> number(). +-callback get_password(binary(), binary()) -> {ets_cache:tag(), {ok, password() | [password()]} | error}. +-callback drop_password_type(binary(), atom()) -> + ok | {error, db_failure | not_allowed}. +-callback use_cache(binary()) -> boolean(). +-callback cache_nodes(binary()) -> boolean(). -start() -> -%% This is only executed by ejabberd_c2s for non-SASL auth client - lists:foreach(fun (Host) -> - lists:foreach(fun (M) -> M:start(Host) end, - auth_modules(Host)) - end, - ?MYHOSTS). +-optional_callbacks([reload/1, + set_password/3, + set_password_multiple/3, + set_password_instance/3, + remove_user/2, + user_exists/2, + check_password/4, + try_register/3, + try_register_multiple/3, + get_users/2, + count_users/2, + get_password/2, + drop_password_type/2, + use_cache/1, + cache_nodes/1]). +-spec start_link() -> {ok, pid()} | {error, any()}. +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +init([]) -> + ejabberd_hooks:add(host_up, ?MODULE, host_up, 30), + ejabberd_hooks:add(host_down, ?MODULE, host_down, 80), + ejabberd_hooks:add(config_reloaded, ?MODULE, config_reloaded, 40), + HostModules = lists:foldl( + fun(Host, Acc) -> + Modules = auth_modules(Host), + maps:put(Host, Modules, Acc) + end, #{}, ejabberd_option:hosts()), + lists:foreach( + fun({Host, Modules}) -> + start(Host, Modules) + end, maps:to_list(HostModules)), + init_cache(HostModules), + {ok, #state{host_modules = HostModules}}. + +handle_call(Request, From, State) -> + ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), + {noreply, State}. + +handle_cast({host_up, Host}, #state{host_modules = HostModules} = State) -> + Modules = auth_modules(Host), + start(Host, Modules), + NewHostModules = maps:put(Host, Modules, HostModules), + init_cache(NewHostModules), + {noreply, State#state{host_modules = NewHostModules}}; +handle_cast({host_down, Host}, #state{host_modules = HostModules} = State) -> + Modules = maps:get(Host, HostModules, []), + stop(Host, Modules), + NewHostModules = maps:remove(Host, HostModules), + init_cache(NewHostModules), + {noreply, State#state{host_modules = NewHostModules}}; +handle_cast(config_reloaded, #state{host_modules = HostModules} = State) -> + NewHostModules = + lists:foldl( + fun(Host, Acc) -> + OldModules = maps:get(Host, HostModules, []), + NewModules = auth_modules(Host), + start(Host, NewModules -- OldModules), + stop(Host, OldModules -- NewModules), + reload(Host, misc:intersection(OldModules, NewModules)), + maps:put(Host, NewModules, Acc) + end, HostModules, ejabberd_option:hosts()), + init_cache(NewHostModules), + {noreply, State#state{host_modules = NewHostModules}}; +handle_cast(Msg, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Msg]), + {noreply, State}. + +handle_info(Info, State) -> + ?WARNING_MSG("Unexpected info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, State) -> + ejabberd_hooks:delete(host_up, ?MODULE, host_up, 30), + ejabberd_hooks:delete(host_down, ?MODULE, host_down, 80), + ejabberd_hooks:delete(config_reloaded, ?MODULE, config_reloaded, 40), + lists:foreach( + fun({Host, Modules}) -> + stop(Host, Modules) + end, maps:to_list(State#state.host_modules)). + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +start(Host, Modules) -> + lists:foreach(fun(M) -> M:start(Host) end, Modules). + +stop(Host, Modules) -> + lists:foreach(fun(M) -> M:stop(Host) end, Modules). + +reload(Host, Modules) -> + lists:foreach( + fun(M) -> + case erlang:function_exported(M, reload, 1) of + true -> M:reload(Host); + false -> ok + end + end, Modules). + +host_up(Host) -> + gen_server:cast(?MODULE, {host_up, Host}). + +host_down(Host) -> + gen_server:cast(?MODULE, {host_down, Host}). + +config_reloaded() -> + gen_server:cast(?MODULE, config_reloaded). + +-spec plain_password_required(binary()) -> boolean(). plain_password_required(Server) -> - lists:any(fun (M) -> M:plain_password_required() end, + lists:any(fun (M) -> M:plain_password_required(Server) end, auth_modules(Server)). +-spec store_type(binary()) -> plain | scram | external. store_type(Server) -> -%% @doc Check if the user and password can login in server. -%% @spec (User::string(), Server::string(), Password::string()) -> -%% true | false - lists:foldl(fun (_, external) -> external; - (M, scram) -> - case M:store_type() of - external -> external; - _Else -> scram - end; - (M, plain) -> M:store_type() - end, - plain, auth_modules(Server)). - --spec check_password(binary(), binary(), binary()) -> boolean(). - -check_password(User, Server, Password) -> - case check_password_with_authmodule(User, Server, - Password) - of - {true, _AuthModule} -> true; - false -> false + case auth_modules(Server) of + [ejabberd_auth_anonymous] -> external; + Modules -> + lists:foldl( + fun(ejabberd_auth_anonymous, Type) -> Type; + (_, external) -> external; + (M, scram) -> + case M:store_type(Server) of + external -> external; + _ -> scram + end; + (M, plain) -> + M:store_type(Server) + end, plain, Modules) end. -%% @doc Check if the user and password can login in server. -%% @spec (User::string(), Server::string(), Password::string(), -%% Digest::string(), DigestGen::function()) -> -%% true | false --spec check_password(binary(), binary(), binary(), binary(), - fun((binary()) -> binary())) -> boolean(). - -check_password(User, Server, Password, Digest, - DigestGen) -> - case check_password_with_authmodule(User, Server, - Password, Digest, DigestGen) - of - {true, _AuthModule} -> true; - false -> false +-spec check_password(binary(), binary(), binary(), binary()) -> boolean(). +check_password(User, AuthzId, Server, Password) -> + check_password(User, AuthzId, Server, Password, <<"">>, undefined). + +-spec check_password(binary(), binary(), binary(), binary(), binary(), + digest_fun() | undefined) -> boolean(). +check_password(User, AuthzId, Server, Password, Digest, DigestGen) -> + case check_password_with_authmodule( + User, AuthzId, Server, Password, Digest, DigestGen) of + {true, _AuthModule} -> true; + {false, _ErrorAtom, _Reason} -> false; + false -> false end. -%% @doc Check if the user and password can login in server. -%% The user can login if at least an authentication method accepts the user -%% and the password. -%% The first authentication method that accepts the credentials is returned. -%% @spec (User::string(), Server::string(), Password::string()) -> -%% {true, AuthModule} | false -%% where -%% AuthModule = ejabberd_auth_anonymous | ejabberd_auth_external -%% | ejabberd_auth_internal | ejabberd_auth_ldap -%% | ejabberd_auth_odbc | ejabberd_auth_pam --spec check_password_with_authmodule(binary(), binary(), binary()) -> false | - {true, atom()}. +-spec check_password_with_authmodule(binary(), binary(), + binary(), binary()) -> false | {true, atom()}. +check_password_with_authmodule(User, AuthzId, Server, Password) -> + check_password_with_authmodule( + User, AuthzId, Server, Password, <<"">>, undefined). -check_password_with_authmodule(User, Server, - Password) -> - check_password_loop(auth_modules(Server), - [User, Server, Password]). - --spec check_password_with_authmodule(binary(), binary(), binary(), binary(), - fun((binary()) -> binary())) -> false | - {true, atom()}. - -check_password_with_authmodule(User, Server, Password, - Digest, DigestGen) -> - check_password_loop(auth_modules(Server), - [User, Server, Password, Digest, DigestGen]). - -check_password_loop([], _Args) -> false; -check_password_loop([AuthModule | AuthModules], Args) -> - case apply(AuthModule, check_password, Args) of - true -> {true, AuthModule}; - false -> check_password_loop(AuthModules, Args) +-spec check_password_with_authmodule( + binary(), binary(), binary(), binary(), binary(), + digest_fun() | undefined) -> false | {false, atom(), binary()} | {true, atom()}. +check_password_with_authmodule(User, AuthzId, Server, Password, Digest, DigestGen) -> + case validate_credentials(User, Server) of + {ok, LUser, LServer} -> + case {jid:nodeprep(AuthzId), get_is_banned(LUser, LServer)} of + {error, _} -> + false; + {_, {is_banned, BanReason}} -> + {false, 'account-disabled', BanReason}; + {LAuthzId, _} -> + untag_stop( + lists:foldl( + fun(Mod, false) -> + case db_check_password( + LUser, LAuthzId, LServer, Password, + Digest, DigestGen, Mod) of + true -> {true, Mod}; + false -> false; + {stop, true} -> {stop, {true, Mod}}; + {stop, false} -> {stop, false} + end; + (_, Acc) -> + Acc + end, false, auth_modules(LServer))) + end; + _ -> + false end. --spec set_password(binary(), binary(), binary()) -> ok | - {error, atom()}. +convert_password_for_storage(_Server, #scram{} = Password) -> + {Password, [Password]}; +convert_password_for_storage(Server, Password) -> + P = case ejabberd_option:auth_stored_password_types(Server) of + [] -> + case ejabberd_option:auth_password_format(Server) of + plain -> + [Password]; + _ -> + [password_to_scram(Server, Password)] + end; + M -> + lists:sort(lists:map( + fun(scram_sha1) -> + password_to_scram(Server, Password, sha, ?SCRAM_DEFAULT_ITERATION_COUNT); + (scram_sha256) -> + password_to_scram(Server, Password, sha256, ?SCRAM_DEFAULT_ITERATION_COUNT); + (scram_sha512) -> + password_to_scram(Server, Password, sha512, ?SCRAM_DEFAULT_ITERATION_COUNT); + (plain) -> + Password + end, M)) + end, + {Password, P}. -%% @spec (User::string(), Server::string(), Password::string()) -> -%% ok | {error, ErrorType} -%% where ErrorType = empty_password | not_allowed | invalid_jid -set_password(_User, _Server, <<"">>) -> - {error, empty_password}; +-spec set_password(binary(), binary(), password()) -> ok | {error, + db_failure | not_allowed | + invalid_jid | invalid_password}. set_password(User, Server, Password) -> -%% @spec (User, Server, Password) -> {atomic, ok} | {atomic, exists} | {error, not_allowed} - lists:foldl(fun (M, {error, _}) -> - M:set_password(User, Server, Password); - (_M, Res) -> Res - end, - {error, not_allowed}, auth_modules(Server)). - --spec try_register(binary(), binary(), binary()) -> {atomic, atom()} | - {error, atom()}. - -try_register(_User, _Server, <<"">>) -> - {error, not_allowed}; -try_register(User, Server, Password) -> - case is_user_exists(User, Server) of - true -> {atomic, exists}; - false -> - case lists:member(jlib:nameprep(Server), ?MYHOSTS) of - true -> - Res = lists:foldl(fun (_M, {atomic, ok} = Res) -> Res; - (M, _) -> - M:try_register(User, Server, Password) - end, - {error, not_allowed}, auth_modules(Server)), - case Res of - {atomic, ok} -> - ejabberd_hooks:run(register_user, Server, - [User, Server]), - {atomic, ok}; - _ -> Res - end; - false -> {error, not_allowed} - end + case validate_credentials(User, Server, Password) of + {ok, LUser, LServer} -> + {Plain, Passwords} = convert_password_for_storage(Server, Password), + lists:foldl( + fun(M, {error, _}) -> + db_set_password(LUser, LServer, Plain, Passwords, M); + (_, ok) -> + ok + end, {error, not_allowed}, auth_modules(LServer)); + Err -> + Err end. -%% Registered users list do not include anonymous users logged --spec dirty_get_registered_users() -> [{binary(), binary()}]. +set_password_instance(User, Server, Password) -> + case validate_credentials(User, Server, Password) of + {ok, LUser, LServer} -> + lists:foldl( + fun(Mod, {error, _} = Acc) -> + case erlang:function_exported(Mod, set_password_instance, 3) of + true -> + R = Mod:set_password_instance(LUser, LServer, Password), + case use_cache(Mod, LServer) of + true -> + ets_cache:delete(cache_tab(Mod), {LUser, LServer}, + cache_nodes(Mod, LServer)); + _ -> + ok + end, + R; + _ -> + Acc + end; + (_, ok) -> + ok + end, {error, not_allowed}, auth_modules(LServer)); + Err -> + Err + end. -dirty_get_registered_users() -> - lists:flatmap(fun (M) -> M:dirty_get_registered_users() - end, - auth_modules()). +-spec try_register(binary(), binary(), password()) -> ok | {error, + db_failure | not_allowed | exists | + invalid_jid | invalid_password}. +try_register(User, Server, Password) -> + case validate_credentials(User, Server, Password) of + {ok, LUser, LServer} -> + case user_exists(LUser, LServer) of + true -> + {error, exists}; + false -> + case ejabberd_router:is_my_host(LServer) of + true -> + case ejabberd_hooks:run_fold(check_register_user, LServer, true, + [User, Server, Password]) of + true -> + {Plain, Passwords} = convert_password_for_storage(Server, Password), + case lists:foldl( + fun(_, ok) -> + ok; + (Mod, _) -> + db_try_register( + LUser, LServer, Plain, Passwords, Mod) + end, {error, not_allowed}, auth_modules(LServer)) of + ok -> + ejabberd_hooks:run( + register_user, LServer, [LUser, LServer]); + {error, _} = Err -> + Err + end; + false -> + {error, not_allowed} + end; + false -> + {error, not_allowed} + end + end; + Err -> + Err + end. --spec get_vh_registered_users(binary()) -> [{binary(), binary()}]. +-spec get_users() -> [{binary(), binary()}]. +get_users() -> + lists:flatmap( + fun({Host, Mod}) -> + db_get_users(Host, [], Mod) + end, auth_modules()). -%% Registered users list do not include anonymous users logged -get_vh_registered_users(Server) -> - lists:flatmap(fun (M) -> - M:get_vh_registered_users(Server) - end, - auth_modules(Server)). +-spec get_users(binary()) -> [{binary(), binary()}]. +get_users(Server) -> + get_users(Server, []). --spec get_vh_registered_users(binary(), opts()) -> [{binary(), binary()}]. +-spec get_users(binary(), opts()) -> [{binary(), binary()}]. +get_users(Server, Opts) -> + case jid:nameprep(Server) of + error -> []; + LServer -> + lists:flatmap( + fun(M) -> db_get_users(LServer, Opts, M) end, + auth_modules(LServer)) + end. -get_vh_registered_users(Server, Opts) -> - lists:flatmap(fun (M) -> - case erlang:function_exported(M, - get_vh_registered_users, - 2) - of - true -> M:get_vh_registered_users(Server, Opts); - false -> M:get_vh_registered_users(Server) - end - end, - auth_modules(Server)). +-spec count_users(binary()) -> non_neg_integer(). +count_users(Server) -> + count_users(Server, []). -get_vh_registered_users_number(Server) -> - lists:sum(lists:map(fun (M) -> - case erlang:function_exported(M, - get_vh_registered_users_number, - 1) - of - true -> - M:get_vh_registered_users_number(Server); - false -> - length(M:get_vh_registered_users(Server)) - end - end, - auth_modules(Server))). - --spec get_vh_registered_users_number(binary(), opts()) -> number(). - -get_vh_registered_users_number(Server, Opts) -> -%% @doc Get the password of the user. -%% @spec (User::string(), Server::string()) -> Password::string() - lists:sum(lists:map(fun (M) -> - case erlang:function_exported(M, - get_vh_registered_users_number, - 2) - of - true -> - M:get_vh_registered_users_number(Server, - Opts); - false -> - length(M:get_vh_registered_users(Server)) - end - end, - auth_modules(Server))). - --spec get_password(binary(), binary()) -> false | binary(). +-spec count_users(binary(), opts()) -> non_neg_integer(). +count_users(Server, Opts) -> + case jid:nameprep(Server) of + error -> 0; + LServer -> + lists:sum( + lists:map( + fun(M) -> db_count_users(LServer, Opts, M) end, + auth_modules(LServer))) + end. +-spec get_password(binary(), binary()) -> false | [password()]. get_password(User, Server) -> - lists:foldl(fun (M, false) -> - M:get_password(User, Server); - (_M, Password) -> Password - end, - false, auth_modules(Server)). - --spec get_password_s(binary(), binary()) -> binary(). + {Passwords, _} = get_password_with_authmodule(User, Server), + Passwords. +-spec get_password_s(binary(), binary()) -> password(). get_password_s(User, Server) -> case get_password(User, Server) of false -> <<"">>; - Password -> Password + Passwords -> + {_, Pass} = lists:foldl( + fun(Plain, _) when is_binary(Plain) -> {true, Plain}; + (Pass, {false, _}) -> {true, Pass}; + (_, Acc) -> Acc + end, {false, <<"">>}, Passwords), + Pass end. -%% @doc Get the password of the user and the auth module. -%% @spec (User::string(), Server::string()) -> -%% {Password::string(), AuthModule::atom()} | {false, none} --spec get_password_with_authmodule(binary(), binary()) -> {false | binary(), atom()}. - +-spec get_password_with_authmodule(binary(), binary()) -> + {false | {false, atom(), binary()} | [password()], module()}. get_password_with_authmodule(User, Server) -> -%% Returns true if the user exists in the DB or if an anonymous user is logged -%% under the given name - lists:foldl(fun (M, {false, _}) -> - {M:get_password(User, Server), M}; - (_M, {Password, AuthModule}) -> {Password, AuthModule} - end, - {false, none}, auth_modules(Server)). - --spec is_user_exists(binary(), binary()) -> boolean(). - -is_user_exists(_User, <<"">>) -> - false; - -is_user_exists(User, Server) -> -%% Check if the user exists in all authentications module except the module -%% passed as parameter -%% @spec (Module::atom(), User, Server) -> true | false | maybe - lists:any(fun (M) -> - case M:is_user_exists(User, Server) of - {error, Error} -> - ?ERROR_MSG("The authentication module ~p returned " - "an error~nwhen checking user ~p in server " - "~p~nError message: ~p", - [M, User, Server, Error]), - false; - Else -> Else - end - end, - auth_modules(Server)). - --spec is_user_exists_in_other_modules(atom(), binary(), binary()) -> boolean() | maybe. - -is_user_exists_in_other_modules(Module, User, Server) -> - is_user_exists_in_other_modules_loop(auth_modules(Server) - -- [Module], - User, Server). - -is_user_exists_in_other_modules_loop([], _User, - _Server) -> - false; -is_user_exists_in_other_modules_loop([AuthModule - | AuthModules], - User, Server) -> - case AuthModule:is_user_exists(User, Server) of - true -> true; - false -> - is_user_exists_in_other_modules_loop(AuthModules, User, - Server); - {error, Error} -> - ?DEBUG("The authentication module ~p returned " - "an error~nwhen checking user ~p in server " - "~p~nError message: ~p", - [AuthModule, User, Server, Error]), - maybe + case validate_credentials(User, Server) of + {ok, LUser, LServer} -> + case get_is_banned(LUser, LServer) of + {is_banned, BanReason} -> + {{false, 'account-disabled', BanReason}, module_not_consulted}; + not_banned -> + case lists:foldl( + fun(M, {error, _}) -> + {db_get_password(LUser, LServer, M), M}; + (_M, Acc) -> + Acc + end, {error, undefined}, auth_modules(LServer)) of + {{ok, Password}, Module} when is_list(Password) -> + {Password, Module}; + {{ok, Password}, Module} -> + {[Password], Module}; + {error, Module} -> + {false, Module} + end + end; + _ -> + {false, undefined} end. +-spec user_exists(binary(), binary()) -> boolean(). +user_exists(_User, <<"">>) -> + false; +user_exists(User, Server) -> + case validate_credentials(User, Server) of + {ok, LUser, LServer} -> + {Exists, PerformExternalUserCheck} = + lists:foldl( + fun(M, {Exists0, PerformExternalUserCheck0}) -> + case db_user_exists(LUser, LServer, M) of + {{error, _}, Check} -> + {Exists0, PerformExternalUserCheck0 orelse Check}; + {Else, Check2} -> + {Exists0 orelse Else, PerformExternalUserCheck0 orelse Check2} + end + end, {false, false}, auth_modules(LServer)), + case (not Exists) andalso PerformExternalUserCheck andalso + ejabberd_option:auth_external_user_exists_check(Server) andalso + gen_mod:is_loaded(Server, mod_last) of + true -> + case mod_last:get_last_info(User, Server) of + not_found -> + false; + _ -> + true + end; + _ -> + Exists + end; + _ -> + false + end. + +-spec user_exists_in_other_modules(atom(), binary(), binary()) -> boolean() | maybe_exists. +user_exists_in_other_modules(Module, User, Server) -> + user_exists_in_other_modules_loop( + auth_modules(Server) -- [Module], User, Server). + +user_exists_in_other_modules_loop([], _User, _Server) -> + false; +user_exists_in_other_modules_loop([AuthModule | AuthModules], User, Server) -> + case db_user_exists(User, Server, AuthModule) of + {true, _} -> + true; + {false, _} -> + user_exists_in_other_modules_loop(AuthModules, User, Server); + {{error, _}, _} -> + maybe_exists + end. + +drop_password_type(LServer, Type) -> + Hash = case Type of + plain -> plain; + scram_sha1 -> sha; + scram_sha256 -> sha256; + scram_sha512 -> sha512 + end, + lists:foreach( + fun(M) -> + case erlang:function_exported(M, drop_password_type, 2) of + true -> + M:drop_password_type(LServer, Hash), + case use_cache(M, LServer) of + true -> + ets_cache:clear(cache_tab(M), + cache_nodes(M, LServer)); + false -> + ok + end; + _ -> + ok + end + end, auth_modules(LServer)). + +-spec which_users_exists(list({binary(), binary()})) -> list({binary(), binary()}). +which_users_exists(USPairs) -> + ByServer = lists:foldl( + fun({User, Server}, Dict) -> + LServer = jid:nameprep(Server), + LUser = jid:nodeprep(User), + case gb_trees:lookup(LServer, Dict) of + none -> + gb_trees:insert(LServer, gb_sets:singleton(LUser), Dict); + {value, Set} -> + gb_trees:update(LServer, gb_sets:add(LUser, Set), Dict) + end + end, gb_trees:empty(), USPairs), + Set = lists:foldl( + fun({LServer, UsersSet}, Results) -> + UsersList = gb_sets:to_list(UsersSet), + lists:foldl( + fun(M, Results2) -> + try M:which_users_exists(LServer, UsersList) of + {error, _} -> + Results2; + Res -> + gb_sets:union( + gb_sets:from_list([{U, LServer} || U <- Res]), + Results2) + catch + _:undef -> + lists:foldl( + fun(U, R2) -> + case user_exists(U, LServer) of + true -> + gb_sets:add({U, LServer}, R2); + _ -> + R2 + end + end, Results2, UsersList) + end + end, Results, auth_modules(LServer)) + end, gb_sets:empty(), gb_trees:to_list(ByServer)), + gb_sets:to_list(Set). + -spec remove_user(binary(), binary()) -> ok. - -%% @spec (User, Server) -> ok -%% @doc Remove user. -%% Note: it may return ok even if there was some problem removing the user. remove_user(User, Server) -> - lists:foreach(fun (M) -> M:remove_user(User, Server) - end, - auth_modules(Server)), - ejabberd_hooks:run(remove_user, jlib:nameprep(Server), - [User, Server]), - ok. - -%% @spec (User, Server, Password) -> ok | not_exists | not_allowed | bad_request | error -%% @doc Try to remove user if the provided password is correct. -%% The removal is attempted in each auth method provided: -%% when one returns 'ok' the loop stops; -%% if no method returns 'ok' then it returns the error message indicated by the last method attempted. --spec remove_user(binary(), binary(), binary()) -> any(). + case validate_credentials(User, Server) of + {ok, LUser, LServer} -> + lists:foreach( + fun(Mod) -> db_remove_user(LUser, LServer, Mod) end, + auth_modules(LServer)), + ejabberd_hooks:run(remove_user, LServer, [LUser, LServer]); + _Err -> + ok + end. +-spec remove_user(binary(), binary(), password()) -> ok | {error, atom()}. remove_user(User, Server, Password) -> - R = lists:foldl(fun (_M, ok = Res) -> Res; - (M, _) -> M:remove_user(User, Server, Password) - end, - error, auth_modules(Server)), - case R of - ok -> - ejabberd_hooks:run(remove_user, jlib:nameprep(Server), - [User, Server]); - _ -> none - end, - R. + case validate_credentials(User, Server, Password) of + {ok, LUser, LServer} -> + case lists:foldl( + fun (_, ok) -> + ok; + (Mod, _) -> + case db_check_password( + LUser, <<"">>, LServer, Password, + <<"">>, undefined, Mod) of + true -> + db_remove_user(LUser, LServer, Mod); + {stop, true} -> + db_remove_user(LUser, LServer, Mod); + false -> + {error, not_allowed}; + {stop, false} -> + {error, not_allowed} + end + end, {error, not_allowed}, auth_modules(Server)) of + ok -> + ejabberd_hooks:run( + remove_user, LServer, [LUser, LServer]); + Err -> + Err + end; + Err -> + Err + end. -%% @spec (IOList) -> non_negative_float() %% @doc Calculate informational entropy. +-spec entropy(iodata()) -> float(). entropy(B) -> case binary_to_list(B) of "" -> 0.0; @@ -409,43 +655,480 @@ entropy(B) -> length(S) * math:log(lists:sum(Set)) / math:log(2) end. +-spec backend_type(atom()) -> atom(). +backend_type(Mod) -> + case atom_to_list(Mod) of + "ejabberd_auth_" ++ T -> list_to_atom(T); + _ -> Mod + end. + +-spec password_format(binary() | global) -> plain | scram. +password_format(LServer) -> + ejabberd_option:auth_password_format(LServer). + +get_is_banned(User, Server) -> + case mod_admin_extra:get_ban_details(User, Server) of + [] -> + not_banned; + BanDetails -> + {_, ReasonText} = lists:keyfind("reason", 1, BanDetails), + {is_banned, <<"Account is banned: ", ReasonText/binary>>} + end. + +%%%---------------------------------------------------------------------- +%%% Backend calls +%%%---------------------------------------------------------------------- +-spec db_try_register(binary(), binary(), binary(), [password()], module()) -> ok | {error, exists | db_failure | not_allowed}. +db_try_register(User, Server, PlainPassword, Passwords, Mod) -> + Ret = case erlang:function_exported(Mod, try_register_multiple, 3) of + true -> + case use_cache(Mod, Server) of + true -> + ets_cache:update( + cache_tab(Mod), {User, Server}, {ok, Passwords}, + fun() -> Mod:try_register_multiple(User, Server, Passwords) end, + cache_nodes(Mod, Server)); + false -> + ets_cache:untag(Mod:try_register_multiple(User, Server, Passwords)) + end; + _ -> + case erlang:function_exported(Mod, try_register, 3) of + true -> + case use_cache(Mod, Server) of + true -> + ets_cache:update( + cache_tab(Mod), {User, Server}, {ok, [PlainPassword]}, + fun() -> + case Mod:try_register(User, Server, PlainPassword) of + {Tag, {ok, Pass}} -> {Tag, {ok, [Pass]}}; + Other -> Other + end + end, cache_nodes(Mod, Server)); + false -> + case Mod:try_register(User, Server, PlainPassword) of + {_, {ok, Pass}} -> {ok, [Pass]}; + V -> ets_cache:untag(V) + end + end; + false -> + {error, not_allowed} + end + end, + case Ret of + {ok, _} -> ok; + {error, _} = Err -> Err + end. + +-spec db_set_password(binary(), binary(), binary(), [password()], module()) -> ok | {error, db_failure | not_allowed}. +db_set_password(User, Server, PlainPassword, Passwords, Mod) -> + Ret = case erlang:function_exported(Mod, set_password_multiple, 3) of + true -> + case use_cache(Mod, Server) of + true -> + ets_cache:update( + cache_tab(Mod), {User, Server}, {ok, Passwords}, + fun() -> Mod:set_password_multiple(User, Server, Passwords) end, + cache_nodes(Mod, Server)); + false -> + ets_cache:untag(Mod:set_password_multiple(User, Server, Passwords)) + end; + _ -> + case erlang:function_exported(Mod, set_password, 3) of + true -> + case use_cache(Mod, Server) of + true -> + ets_cache:update( + cache_tab(Mod), {User, Server}, {ok, [PlainPassword]}, + fun() -> + case Mod:set_password(User, Server, PlainPassword) of + {Tag, {ok, Pass}} -> {Tag, {ok, [Pass]}}; + Other -> Other + end + end, cache_nodes(Mod, Server)); + false -> + case Mod:set_password(User, Server, PlainPassword) of + {_, {ok, Pass}} -> {ok, [Pass]}; + V -> ets_cache:untag(V) + end + end; + false -> + {error, not_allowed} + end + end, + case Ret of + {ok, _} -> ejabberd_hooks:run(set_password, Server, [User, Server]); + {error, _} = Err -> Err + end. + +db_get_password(User, Server, Mod) -> + UseCache = use_cache(Mod, Server), + case erlang:function_exported(Mod, get_password, 2) of + false when UseCache -> + case ets_cache:lookup(cache_tab(Mod), {User, Server}) of + {ok, exists} -> error; + not_found -> error; + {ok, List} = V when is_list(List) -> V; + {ok, Single} -> {ok, [Single]}; + Other -> Other + end; + false -> + error; + true when UseCache -> + ets_cache:lookup( + cache_tab(Mod), {User, Server}, + fun() -> + case Mod:get_password(User, Server) of + {_, {ok, List}} = V when is_list(List) -> V; + {Tag, {ok, Single}} -> {Tag, {ok, [Single]}}; + Other -> Other + end + end); + true -> + case Mod:get_password(User, Server) of + {_, {ok, List}} when is_list(List) -> {ok, List}; + {_, {ok, Single}} -> {ok, [Single]}; + Other -> ets_cache:untag(Other) + end + end. + +db_user_exists(User, Server, Mod) -> + case db_get_password(User, Server, Mod) of + {ok, _} -> + {true, false}; + not_found -> + {false, false}; + error -> + case {Mod:store_type(Server), use_cache(Mod, Server)} of + {external, true} -> + Val = case ets_cache:lookup(cache_tab(Mod), {User, Server}, error) of + error -> + ets_cache:update(cache_tab(Mod), {User, Server}, {ok, exists}, + fun() -> + case Mod:user_exists(User, Server) of + {CacheTag, true} -> {CacheTag, {ok, exists}}; + {CacheTag, false} -> {CacheTag, not_found}; + {_, {error, _}} = Err -> Err + end + end); + Other -> + Other + end, + case Val of + {ok, _} -> + {true, Mod /= ejabberd_auth_anonymous}; + not_found -> + {false, Mod /= ejabberd_auth_anonymous}; + error -> + {false, Mod /= ejabberd_auth_anonymous}; + {error, _} = Err -> + {Err, Mod /= ejabberd_auth_anonymous} + end; + {external, false} -> + {ets_cache:untag(Mod:user_exists(User, Server)), Mod /= ejabberd_auth_anonymous}; + _ -> + {false, false} + end + end. + +db_check_password(User, AuthzId, Server, ProvidedPassword, + Digest, DigestFun, Mod) -> + case db_get_password(User, Server, Mod) of + {ok, ValidPasswords} -> + match_passwords(ProvidedPassword, ValidPasswords, Digest, DigestFun); + error -> + case {Mod:store_type(Server), use_cache(Mod, Server)} of + {external, true} -> + case ets_cache:update( + cache_tab(Mod), {User, Server}, {ok, ProvidedPassword}, + fun() -> + case Mod:check_password( + User, AuthzId, Server, ProvidedPassword) of + {CacheTag, true} -> {CacheTag, {ok, ProvidedPassword}}; + {CacheTag, {stop, true}} -> {CacheTag, {ok, ProvidedPassword}}; + {CacheTag, false} -> {CacheTag, error}; + {CacheTag, {stop, false}} -> {CacheTag, error} + end + end) of + {ok, _} -> + true; + error -> + false + end; + {external, false} -> + ets_cache:untag( + Mod:check_password(User, AuthzId, Server, ProvidedPassword)); + _ -> + false + end + end. + +db_remove_user(User, Server, Mod) -> + case erlang:function_exported(Mod, remove_user, 2) of + true -> + case Mod:remove_user(User, Server) of + ok -> + case use_cache(Mod, Server) of + true -> + ets_cache:delete(cache_tab(Mod), {User, Server}, + cache_nodes(Mod, Server)); + false -> + ok + end; + {error, _} = Err -> + Err + end; + false -> + {error, not_allowed} + end. + +db_get_users(Server, Opts, Mod) -> + case erlang:function_exported(Mod, get_users, 2) of + true -> + Mod:get_users(Server, Opts); + false -> + case use_cache(Mod, Server) of + true -> + ets_cache:fold( + fun({User, S}, {ok, _}, Users) when S == Server -> + [{User, Server}|Users]; + (_, _, Users) -> + Users + end, [], cache_tab(Mod)); + false -> + [] + end + end. + +db_count_users(Server, Opts, Mod) -> + case erlang:function_exported(Mod, count_users, 2) of + true -> + Mod:count_users(Server, Opts); + false -> + case use_cache(Mod, Server) of + true -> + ets_cache:fold( + fun({_, S}, {ok, _}, Num) when S == Server -> + Num + 1; + (_, _, Num) -> + Num + end, 0, cache_tab(Mod)); + false -> + 0 + end + end. + +%%%---------------------------------------------------------------------- +%%% SCRAM stuff +%%%---------------------------------------------------------------------- +is_password_scram_valid(Password, Scram) -> + case jid:resourceprep(Password) of + error -> + false; + _ -> + IterationCount = Scram#scram.iterationcount, + Hash = Scram#scram.hash, + Salt = base64:decode(Scram#scram.salt), + SaltedPassword = scram:salted_password(Hash, Password, Salt, IterationCount), + StoredKey = scram:stored_key(Hash, scram:client_key(Hash, SaltedPassword)), + base64:decode(Scram#scram.storedkey) == StoredKey + end. + +password_to_scram(Host, Password) -> + password_to_scram(Host, Password, ?SCRAM_DEFAULT_ITERATION_COUNT). + +password_to_scram(_Host, #scram{} = Password, _IterationCount) -> + Password; +password_to_scram(Host, Password, IterationCount) -> + password_to_scram(Host, Password, ejabberd_option:auth_scram_hash(Host), IterationCount). + +password_to_scram(_Host, Password, Hash, IterationCount) -> + Salt = p1_rand:bytes(?SALT_LENGTH), + SaltedPassword = scram:salted_password(Hash, Password, Salt, IterationCount), + StoredKey = scram:stored_key(Hash, scram:client_key(Hash, SaltedPassword)), + ServerKey = scram:server_key(Hash, SaltedPassword), + #scram{storedkey = base64:encode(StoredKey), + serverkey = base64:encode(ServerKey), + salt = base64:encode(Salt), + hash = Hash, + iterationcount = IterationCount}. + +%%%---------------------------------------------------------------------- +%%% Cache stuff +%%%---------------------------------------------------------------------- +-spec init_cache(host_modules()) -> ok. +init_cache(HostModules) -> + CacheOpts = cache_opts(), + {True, False} = use_cache(HostModules), + lists:foreach( + fun(Module) -> + ets_cache:new(cache_tab(Module), CacheOpts) + end, True), + lists:foreach( + fun(Module) -> + ets_cache:delete(cache_tab(Module)) + end, False). + +-spec cache_opts() -> [proplists:property()]. +cache_opts() -> + MaxSize = ejabberd_option:auth_cache_size(), + CacheMissed = ejabberd_option:auth_cache_missed(), + LifeTime = ejabberd_option:auth_cache_life_time(), + [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}]. + +-spec use_cache(host_modules()) -> {True :: [module()], False :: [module()]}. +use_cache(HostModules) -> + {Enabled, Disabled} = + maps:fold( + fun(Host, Modules, Acc) -> + lists:foldl( + fun(Module, {True, False}) -> + case use_cache(Module, Host) of + true -> + {sets:add_element(Module, True), False}; + false -> + {True, sets:add_element(Module, False)} + end + end, Acc, Modules) + end, {sets:new(), sets:new()}, HostModules), + {sets:to_list(Enabled), sets:to_list(sets:subtract(Disabled, Enabled))}. + +-spec use_cache(module(), binary()) -> boolean(). +use_cache(Mod, LServer) -> + case erlang:function_exported(Mod, use_cache, 1) of + true -> Mod:use_cache(LServer); + false -> + ejabberd_option:auth_use_cache(LServer) + end. + +-spec cache_nodes(module(), binary()) -> [node()]. +cache_nodes(Mod, LServer) -> + case erlang:function_exported(Mod, cache_nodes, 1) of + true -> Mod:cache_nodes(LServer); + false -> ejabberd_cluster:get_nodes() + end. + +-spec cache_tab(module()) -> atom(). +cache_tab(Mod) -> + list_to_atom(atom_to_list(Mod) ++ "_cache"). + %%%---------------------------------------------------------------------- %%% Internal functions %%%---------------------------------------------------------------------- -%% Return the lists of all the auth modules actually used in the -%% configuration +-spec auth_modules() -> [{binary(), module()}]. auth_modules() -> - lists:usort(lists:flatmap(fun (Server) -> - auth_modules(Server) - end, - ?MYHOSTS)). + lists:flatmap( + fun(Host) -> + [{Host, Mod} || Mod <- auth_modules(Host)] + end, ejabberd_option:hosts()). --spec auth_modules(binary()) -> [atom()]. - -%% Return the list of authenticated modules for a given host +-spec auth_modules(binary()) -> [module()]. auth_modules(Server) -> - LServer = jlib:nameprep(Server), - Methods = ejabberd_config:get_option( - {auth_method, LServer}, - fun(V) when is_list(V) -> - true = lists:all(fun is_atom/1, V), - V; - (V) when is_atom(V) -> - [V] - end, []), - [jlib:binary_to_atom(<<"ejabberd_auth_", - (jlib:atom_to_binary(M))/binary>>) + LServer = jid:nameprep(Server), + Methods = ejabberd_option:auth_method(LServer), + [ejabberd:module_name([<<"auth">>, + misc:atom_to_binary(M)]) || M <- Methods]. -export(Server) -> - ejabberd_auth_internal:export(Server). +-spec match_passwords(password(), [password()], + binary(), digest_fun() | undefined) -> boolean(). +match_passwords(Provided, Passwords, Digest, DigestFun) -> + lists:any( + fun(Pass) -> + match_password(Provided, Pass, Digest, DigestFun) + end, Passwords). -import(Server) -> - ejabberd_auth_internal:import(Server). +-spec match_password(password(), password(), + binary(), digest_fun() | undefined) -> boolean(). +match_password(Password, #scram{} = Scram, <<"">>, undefined) -> + is_password_scram_valid(Password, Scram); +match_password(Password, #scram{} = Scram, Digest, DigestFun) -> + StoredKey = base64:decode(Scram#scram.storedkey), + DigRes = if Digest /= <<"">> -> + Digest == DigestFun(StoredKey); + true -> false + end, + if DigRes -> + true; + true -> + StoredKey == Password andalso Password /= <<"">> + end; +match_password(ProvidedPassword, ValidPassword, <<"">>, undefined) -> + ProvidedPassword == ValidPassword andalso ProvidedPassword /= <<"">>; +match_password(ProvidedPassword, ValidPassword, Digest, DigestFun) -> + DigRes = if Digest /= <<"">> -> + Digest == DigestFun(ValidPassword); + true -> false + end, + if DigRes -> + true; + true -> + ValidPassword == ProvidedPassword andalso ProvidedPassword /= <<"">> + end. -import(Server, mnesia, Passwd) -> - ejabberd_auth_internal:import(Server, mnesia, Passwd); -import(Server, riak, Passwd) -> - ejabberd_auth_riak:import(Server, riak, Passwd); -import(_, _, _) -> - pass. +-spec validate_credentials(binary(), binary()) -> + {ok, binary(), binary()} | {error, invalid_jid}. +validate_credentials(User, Server) -> + validate_credentials(User, Server, #scram{}). + +-spec validate_credentials(binary(), binary(), password()) -> + {ok, binary(), binary()} | {error, invalid_jid | invalid_password}. +validate_credentials(_User, _Server, <<"">>) -> + {error, invalid_password}; +validate_credentials(User, Server, Password) -> + case jid:nodeprep(User) of + error -> + {error, invalid_jid}; + LUser -> + case jid:nameprep(Server) of + error -> + {error, invalid_jid}; + LServer -> + if is_record(Password, scram) -> + {ok, LUser, LServer}; + true -> + case jid:resourceprep(Password) of + error -> + {error, invalid_password}; + _ -> + {ok, LUser, LServer} + end + end + end + end. + +untag_stop({stop, Val}) -> Val; +untag_stop(Val) -> Val. + +import_info() -> + [{<<"users">>, 3}]. + +import_start(_LServer, mnesia) -> + ejabberd_auth_mnesia:init_db(); +import_start(_LServer, _) -> + ok. + +import(Server, {sql, _}, mnesia, <<"users">>, Fields) -> + ejabberd_auth_mnesia:import(Server, Fields); +import(_LServer, {sql, _}, sql, <<"users">>, _) -> + ok. + +-spec convert_to_scram(binary()) -> {error, any()} | ok. +convert_to_scram(Server) -> + LServer = jid:nameprep(Server), + if + LServer == error; + LServer == <<>> -> + {error, {incorrect_server_name, Server}}; + true -> + lists:foreach( + fun({U, S}) -> + case get_password(U, S) of + [Pass] when is_binary(Pass) -> + SPass = password_to_scram(Server, Pass), + set_password(U, S, SPass); + _ -> + ok + end + end, get_users(LServer)), + ok + end. diff --git a/src/ejabberd_auth_anonymous.erl b/src/ejabberd_auth_anonymous.erl index cb320dea5..8f67b695b 100644 --- a/src/ejabberd_auth_anonymous.erl +++ b/src/ejabberd_auth_anonymous.erl @@ -5,7 +5,7 @@ %%% Created : 17 Feb 2006 by Mickael Remond %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -24,9 +24,15 @@ %%%---------------------------------------------------------------------- -module(ejabberd_auth_anonymous). + +-behaviour(ejabberd_auth). -author('mickael.remond@process-one.net'). +-protocol({xep, 175, '1.2', '1.1.0', "complete", ""}). + -export([start/1, + stop/1, + use_cache/1, allow_anonymous/1, is_sasl_anonymous_enabled/1, is_login_anonymous_enabled/1, @@ -36,39 +42,29 @@ unregister_connection/3 ]). +-export([login/2, check_password/4, user_exists/2, + get_users/2, count_users/2, store_type/1, + plain_password_required/1]). -%% Function used by ejabberd_auth: --export([login/2, set_password/3, check_password/3, - check_password/5, try_register/3, - dirty_get_registered_users/0, get_vh_registered_users/1, - get_vh_registered_users/2, get_vh_registered_users_number/1, - get_vh_registered_users_number/2, get_password_s/2, - get_password/2, get_password/3, is_user_exists/2, - remove_user/2, remove_user/3, store_type/0, - plain_password_required/0]). - --include("ejabberd.hrl"). -include("logger.hrl"). - --include("jlib.hrl"). - -%% Create the anonymous table if at least one virtual host has anonymous features enabled -%% Register to login / logout events --record(anonymous, {us = {<<"">>, <<"">>} :: {binary(), binary()}, - sid = {now(), self()} :: ejabberd_sm:sid()}). +-include_lib("xmpp/include/jid.hrl"). start(Host) -> - %% TODO: Check cluster mode - mnesia:create_table(anonymous, [{ram_copies, [node()]}, - {type, bag}, - {attributes, record_info(fields, anonymous)}]), - %% The hooks are needed to add / remove users from the anonymous tables ejabberd_hooks:add(sm_register_connection_hook, Host, ?MODULE, register_connection, 100), ejabberd_hooks:add(sm_remove_connection_hook, Host, ?MODULE, unregister_connection, 100), ok. +stop(Host) -> + ejabberd_hooks:delete(sm_register_connection_hook, Host, + ?MODULE, register_connection, 100), + ejabberd_hooks:delete(sm_remove_connection_hook, Host, + ?MODULE, unregister_connection, 100). + +use_cache(_) -> + false. + %% Return true if anonymous is allowed for host or false otherwise allow_anonymous(Host) -> lists:member(?MODULE, ejabberd_auth:auth_modules(Host)). @@ -103,93 +99,65 @@ is_login_anonymous_enabled(Host) -> %% Return the anonymous protocol to use: sasl_anon|login_anon|both %% defaults to login_anon anonymous_protocol(Host) -> - ejabberd_config:get_option( - {anonymous_protocol, Host}, - fun(sasl_anon) -> sasl_anon; - (login_anon) -> login_anon; - (both) -> both - end, - sasl_anon). + ejabberd_option:anonymous_protocol(Host). %% Return true if multiple connections have been allowed in the config file %% defaults to false allow_multiple_connections(Host) -> - ejabberd_config:get_option( - {allow_multiple_connections, Host}, - fun(V) when is_boolean(V) -> V end, - false). + ejabberd_option:allow_multiple_connections(Host). -%% Check if user exist in the anonymus database anonymous_user_exist(User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - US = {LUser, LServer}, - case catch mnesia:dirty_read({anonymous, US}) of - [] -> - false; - [_H|_T] -> - true - end. - -%% Remove connection from Mnesia tables -remove_connection(SID, LUser, LServer) -> - US = {LUser, LServer}, - F = fun () -> mnesia:delete_object({anonymous, US, SID}) - end, - mnesia:transaction(F). + lists:any( + fun({_LResource, Info}) -> + proplists:get_value(auth_module, Info) == ?MODULE + end, ejabberd_sm:get_user_info(User, Server)). %% Register connection -register_connection(SID, - #jid{luser = LUser, lserver = LServer}, Info) -> - AuthModule = list_to_atom(binary_to_list(xml:get_attr_s(<<"auth_module">>, Info))), - case AuthModule == (?MODULE) of - true -> - ejabberd_hooks:run(register_user, LServer, - [LUser, LServer]), - US = {LUser, LServer}, - mnesia:sync_dirty(fun () -> - mnesia:write(#anonymous{us = US, - sid = SID}) - end); - false -> ok +-spec register_connection(ejabberd_sm:sid(), jid(), ejabberd_sm:info()) -> ok. +register_connection(_SID, + #jid{luser = LUser, lserver = LServer, lresource = LResource}, Info) -> + case proplists:get_value(auth_module, Info) of + ?MODULE -> + % Register user only if we are first resource + case ejabberd_sm:get_user_resources(LUser, LServer) of + [LResource] -> + ejabberd_hooks:run(register_user, LServer, [LUser, LServer]); + _ -> + ok + end; + _ -> + ok end. %% Remove an anonymous user from the anonymous users table -unregister_connection(SID, - #jid{luser = LUser, lserver = LServer}, _) -> - purge_hook(anonymous_user_exist(LUser, LServer), LUser, - LServer), - remove_connection(SID, LUser, LServer). - -%% Launch the hook to purge user data only for anonymous users -purge_hook(false, _LUser, _LServer) -> - ok; -purge_hook(true, LUser, LServer) -> - ejabberd_hooks:run(anonymous_purge_hook, LServer, - [LUser, LServer]). +-spec unregister_connection(ejabberd_sm:sid(), jid(), ejabberd_sm:info()) -> any(). +unregister_connection(_SID, + #jid{luser = LUser, lserver = LServer}, Info) -> + case proplists:get_value(auth_module, Info) of + ?MODULE -> + % Remove user data only if there is no more resources around + case ejabberd_sm:get_user_resources(LUser, LServer) of + [] -> + ejabberd_hooks:run(remove_user, LServer, [LUser, LServer]); + _ -> + ok + end; + _ -> + ok + end. %% --------------------------------- %% Specific anonymous auth functions %% --------------------------------- - -%% When anonymous login is enabled, check the password for permenant users -%% before allowing access -check_password(User, Server, Password) -> - check_password(User, Server, Password, undefined, - undefined). - -check_password(User, Server, _Password, _Digest, - _DigestGen) -> - case - ejabberd_auth:is_user_exists_in_other_modules(?MODULE, - User, Server) - of - %% If user exists in other module, reject anonnymous authentication - true -> false; - %% If we are not sure whether the user exists in other module, reject anon auth - maybe -> false; - false -> login(User, Server) - end. +check_password(User, _AuthzId, Server, _Password) -> + {nocache, + case ejabberd_auth:user_exists_in_other_modules(?MODULE, User, Server) of + %% If user exists in other module, reject anonnymous authentication + true -> false; + %% If we are not sure whether the user exists in other module, reject anon auth + maybe_exists -> false; + false -> login(User, Server) + end}. login(User, Server) -> case is_login_anonymous_enabled(Server) of @@ -205,67 +173,17 @@ login(User, Server) -> end end. -%% When anonymous login is enabled, check that the user is permanent before -%% changing its password -set_password(User, Server, _Password) -> - case anonymous_user_exist(User, Server) of - true -> ok; - false -> {error, not_allowed} - end. +get_users(Server, _) -> + [{U, S} || {U, S, _R} <- ejabberd_sm:get_vh_session_list(Server)]. -%% When anonymous login is enabled, check if permanent users are allowed on -%% the server: -try_register(_User, _Server, _Password) -> - {error, not_allowed}. +count_users(Server, Opts) -> + length(get_users(Server, Opts)). -dirty_get_registered_users() -> []. +user_exists(User, Server) -> + {nocache, anonymous_user_exist(User, Server)}. -get_vh_registered_users(Server) -> - [{U, S} - || {U, S, _R} - <- ejabberd_sm:get_vh_session_list(Server)]. +plain_password_required(_) -> + false. -get_vh_registered_users(Server, _) -> - get_vh_registered_users(Server). - -get_vh_registered_users_number(Server) -> - length(get_vh_registered_users(Server)). - -get_vh_registered_users_number(Server, _) -> - get_vh_registered_users_number(Server). - -%% Return password of permanent user or false for anonymous users -get_password(User, Server) -> - get_password(User, Server, <<"">>). - -get_password(User, Server, DefaultValue) -> - case anonymous_user_exist(User, Server) or - login(User, Server) - of - %% We return the default value if the user is anonymous - true -> DefaultValue; - %% We return the permanent user password otherwise - false -> false - end. - -get_password_s(User, Server) -> - case get_password(User, Server) of - false -> - <<"">>; - Password -> - Password - end. - -%% Returns true if the user exists in the DB or if an anonymous user is logged -%% under the given name -is_user_exists(User, Server) -> - anonymous_user_exist(User, Server). - -remove_user(_User, _Server) -> {error, not_allowed}. - -remove_user(_User, _Server, _Password) -> not_allowed. - -plain_password_required() -> false. - -store_type() -> - plain. +store_type(_) -> + external. diff --git a/src/ejabberd_auth_external.erl b/src/ejabberd_auth_external.erl index 0aa825f73..1b69a9a10 100644 --- a/src/ejabberd_auth_external.erl +++ b/src/ejabberd_auth_external.erl @@ -1,11 +1,11 @@ %%%---------------------------------------------------------------------- %%% File : ejabberd_auth_external.erl %%% Author : Alexey Shchepin -%%% Purpose : Authentification via LDAP external script +%%% Purpose : Authentication via LDAP external script %%% Created : 12 Dec 2004 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -29,285 +29,77 @@ -behaviour(ejabberd_auth). -%% External exports --export([start/1, set_password/3, check_password/3, - check_password/5, try_register/3, - dirty_get_registered_users/0, get_vh_registered_users/1, - get_vh_registered_users/2, - get_vh_registered_users_number/1, - get_vh_registered_users_number/2, get_password/2, - get_password_s/2, is_user_exists/2, remove_user/2, - remove_user/3, store_type/0, - plain_password_required/0]). +-export([start/1, stop/1, reload/1, set_password/3, check_password/4, + try_register/3, user_exists/2, remove_user/2, + store_type/1, plain_password_required/1]). --include("ejabberd.hrl"). -include("logger.hrl"). %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- start(Host) -> - Cmd = ejabberd_config:get_option( - {extauth_program, Host}, - fun(V) -> - binary_to_list(iolist_to_binary(V)) - end, - "extauth"), - extauth:start(Host, Cmd), - check_cache_last_options(Host), - ejabberd_auth_internal:start(Host). + extauth:start(Host). -check_cache_last_options(Server) -> - case get_cache_option(Server) of - false -> no_cache; - {true, _CacheTime} -> - case get_mod_last_configured(Server) of - no_mod_last -> - ?ERROR_MSG("In host ~p extauth is used, extauth_cache " - "is enabled but mod_last is not enabled.", - [Server]), - no_cache; - _ -> cache - end +stop(Host) -> + extauth:stop(Host). + +reload(Host) -> + extauth:reload(Host). + +plain_password_required(_) -> true. + +store_type(_) -> external. + +check_password(User, AuthzId, Server, Password) -> + if AuthzId /= <<>> andalso AuthzId /= User -> + {nocache, false}; + true -> + check_password_extauth(User, AuthzId, Server, Password) end. -plain_password_required() -> true. - -store_type() -> external. - -check_password(User, Server, Password) -> - case get_cache_option(Server) of - false -> check_password_extauth(User, Server, Password); - {true, CacheTime} -> - check_password_cache(User, Server, Password, CacheTime) - end. - -check_password(User, Server, Password, _Digest, - _DigestGen) -> - check_password(User, Server, Password). - set_password(User, Server, Password) -> case extauth:set_password(User, Server, Password) of - true -> - set_password_internal(User, Server, Password), ok; - _ -> {error, unknown_problem} + Res when is_boolean(Res) -> {cache, {ok, Password}}; + {error, Reason} -> failure(User, Server, set_password, Reason) end. try_register(User, Server, Password) -> - case get_cache_option(Server) of - false -> try_register_extauth(User, Server, Password); - {true, _CacheTime} -> - try_register_external_cache(User, Server, Password) + case extauth:try_register(User, Server, Password) of + true -> {cache, {ok, Password}}; + false -> {cache, {error, not_allowed}}; + {error, Reason} -> failure(User, Server, try_register, Reason) end. -dirty_get_registered_users() -> - ejabberd_auth_internal:dirty_get_registered_users(). - -get_vh_registered_users(Server) -> - ejabberd_auth_internal:get_vh_registered_users(Server). - -get_vh_registered_users(Server, Data) -> - ejabberd_auth_internal:get_vh_registered_users(Server, - Data). - -get_vh_registered_users_number(Server) -> - ejabberd_auth_internal:get_vh_registered_users_number(Server). - -get_vh_registered_users_number(Server, Data) -> - ejabberd_auth_internal:get_vh_registered_users_number(Server, - Data). - -%% The password can only be returned if cache is enabled, cached info exists and is fresh enough. -get_password(User, Server) -> - case get_cache_option(Server) of - false -> false; - {true, CacheTime} -> - get_password_cache(User, Server, CacheTime) - end. - -get_password_s(User, Server) -> - case get_password(User, Server) of - false -> <<"">>; - Other -> Other - end. - -%% @spec (User, Server) -> true | false | {error, Error} -is_user_exists(User, Server) -> - try extauth:is_user_exists(User, Server) of - Res -> Res - catch - _:Error -> {error, Error} +user_exists(User, Server) -> + case extauth:user_exists(User, Server) of + Res when is_boolean(Res) -> {cache, Res}; + {error, Reason} -> failure(User, Server, user_exists, Reason) end. remove_user(User, Server) -> case extauth:remove_user(User, Server) of - false -> false; - true -> - case get_cache_option(Server) of - false -> false; - {true, _CacheTime} -> - ejabberd_auth_internal:remove_user(User, Server) - end + false -> {error, not_allowed}; + true -> ok; + {error, Reason} -> + {_, Err} = failure(User, Server, remove_user, Reason), + Err end. -remove_user(User, Server, Password) -> - case extauth:remove_user(User, Server, Password) of - false -> false; - true -> - case get_cache_option(Server) of - false -> false; - {true, _CacheTime} -> - ejabberd_auth_internal:remove_user(User, Server, - Password) - end +check_password_extauth(User, _AuthzId, Server, Password) -> + if Password /= <<"">> -> + case extauth:check_password(User, Server, Password) of + Res when is_boolean(Res) -> {cache, Res}; + {error, Reason} -> + {Tag, _} = failure(User, Server, check_password, Reason), + {Tag, false} + end; + true -> + {nocache, false} end. -%%% -%%% Extauth cache management -%%% - -%% @spec (Host::string()) -> false | {true, CacheTime::integer()} -get_cache_option(Host) -> - case ejabberd_config:get_option( - {extauth_cache, Host}, - fun(false) -> undefined; - (I) when is_integer(I), I >= 0 -> I - end) of - undefined -> false; - CacheTime -> {true, CacheTime} - end. - -%% @spec (User, Server, Password) -> true | false -check_password_extauth(User, Server, Password) -> - extauth:check_password(User, Server, Password) andalso - Password /= <<"">>. - -%% @spec (User, Server, Password) -> true | false -try_register_extauth(User, Server, Password) -> - extauth:try_register(User, Server, Password). - -check_password_cache(User, Server, Password, 0) -> - check_password_external_cache(User, Server, Password); -check_password_cache(User, Server, Password, - CacheTime) -> - case get_last_access(User, Server) of - online -> - check_password_internal(User, Server, Password); - never -> - check_password_external_cache(User, Server, Password); - mod_last_required -> - ?ERROR_MSG("extauth is used, extauth_cache is enabled " - "but mod_last is not enabled in that " - "host", - []), - check_password_external_cache(User, Server, Password); - TimeStamp -> - case is_fresh_enough(TimeStamp, CacheTime) of - %% If no need to refresh, check password against Mnesia - true -> - case check_password_internal(User, Server, Password) of - %% If password valid in Mnesia, accept it - true -> true; - %% Else (password nonvalid in Mnesia), check in extauth and cache result - false -> - check_password_external_cache(User, Server, Password) - end; - %% Else (need to refresh), check in extauth and cache result - false -> - check_password_external_cache(User, Server, Password) - end - end. - -get_password_internal(User, Server) -> - ejabberd_auth_internal:get_password(User, Server). - -%% @spec (User, Server, CacheTime) -> false | Password::string() -get_password_cache(User, Server, CacheTime) -> - case get_last_access(User, Server) of - online -> get_password_internal(User, Server); - never -> false; - mod_last_required -> - ?ERROR_MSG("extauth is used, extauth_cache is enabled " - "but mod_last is not enabled in that " - "host", - []), - false; - TimeStamp -> - case is_fresh_enough(TimeStamp, CacheTime) of - true -> get_password_internal(User, Server); - false -> false - end - end. - -%% Check the password using extauth; if success then cache it -check_password_external_cache(User, Server, Password) -> - case check_password_extauth(User, Server, Password) of - true -> - set_password_internal(User, Server, Password), true; - false -> false - end. - -%% Try to register using extauth; if success then cache it -try_register_external_cache(User, Server, Password) -> - case try_register_extauth(User, Server, Password) of - {atomic, ok} = R -> - set_password_internal(User, Server, Password), R; - _ -> {error, not_allowed} - end. - -%% @spec (User, Server, Password) -> true | false -check_password_internal(User, Server, Password) -> - ejabberd_auth_internal:check_password(User, Server, - Password). - -%% @spec (User, Server, Password) -> ok | {error, invalid_jid} -set_password_internal(User, Server, Password) -> -%% @spec (TimeLast, CacheTime) -> true | false -%% TimeLast = online | never | integer() -%% CacheTime = integer() | false - ejabberd_auth_internal:set_password(User, Server, - Password). - -is_fresh_enough(TimeStampLast, CacheTime) -> - {MegaSecs, Secs, _MicroSecs} = now(), - Now = MegaSecs * 1000000 + Secs, - TimeStampLast + CacheTime > Now. - -%% @spec (User, Server) -> online | never | mod_last_required | TimeStamp::integer() -%% Code copied from mod_configure.erl -%% Code copied from web/ejabberd_web_admin.erl -%% TODO: Update time format to XEP-0202: Entity Time -get_last_access(User, Server) -> - case ejabberd_sm:get_user_resources(User, Server) of - [] -> - _US = {User, Server}, - case get_last_info(User, Server) of - mod_last_required -> mod_last_required; - not_found -> never; - {ok, Timestamp, _Status} -> Timestamp - end; - _ -> online - end. -%% @spec (User, Server) -> {ok, Timestamp, Status} | not_found | mod_last_required - -get_last_info(User, Server) -> - case get_mod_last_enabled(Server) of - mod_last -> mod_last:get_last_info(User, Server); - no_mod_last -> mod_last_required - end. - -%% @spec (Server) -> mod_last | no_mod_last -get_mod_last_enabled(Server) -> - case gen_mod:is_loaded(Server, mod_last) of - true -> mod_last; - false -> no_mod_last - end. - -get_mod_last_configured(Server) -> - case is_configured(Server, mod_last) of - true -> mod_last; - false -> no_mod_last - end. - -is_configured(Host, Module) -> - gen_mod:is_loaded(Host, Module). +-spec failure(binary(), binary(), atom(), any()) -> {nocache, {error, db_failure}}. +failure(User, Server, Fun, Reason) -> + ?ERROR_MSG("External authentication program failed when calling " + "'~ts' for ~ts@~ts: ~p", [Fun, User, Server, Reason]), + {nocache, {error, db_failure}}. diff --git a/src/ejabberd_auth_internal.erl b/src/ejabberd_auth_internal.erl deleted file mode 100644 index 415c21713..000000000 --- a/src/ejabberd_auth_internal.erl +++ /dev/null @@ -1,483 +0,0 @@ -%%%---------------------------------------------------------------------- -%%% File : ejabberd_auth_internal.erl -%%% Author : Alexey Shchepin -%%% Purpose : Authentification via mnesia -%%% Created : 12 Dec 2004 by Alexey Shchepin -%%% -%%% -%%% ejabberd, Copyright (C) 2002-2015 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(ejabberd_auth_internal). - --author('alexey@process-one.net'). - --behaviour(ejabberd_auth). - -%% External exports --export([start/1, set_password/3, check_password/3, - check_password/5, try_register/3, - dirty_get_registered_users/0, get_vh_registered_users/1, - get_vh_registered_users/2, - get_vh_registered_users_number/1, - get_vh_registered_users_number/2, get_password/2, - get_password_s/2, is_user_exists/2, remove_user/2, - remove_user/3, store_type/0, export/1, import/1, - import/3, plain_password_required/0]). - --include("ejabberd.hrl"). --include("logger.hrl"). - --record(passwd, {us = {<<"">>, <<"">>} :: {binary(), binary()} | '$1', - password = <<"">> :: binary() | scram() | '_'}). - --record(reg_users_counter, {vhost = <<"">> :: binary(), - count = 0 :: integer() | '$1'}). - --define(SALT_LENGTH, 16). - -%%%---------------------------------------------------------------------- -%%% API -%%%---------------------------------------------------------------------- -start(Host) -> - mnesia:create_table(passwd, - [{disc_copies, [node()]}, - {attributes, record_info(fields, passwd)}]), - mnesia:create_table(reg_users_counter, - [{ram_copies, [node()]}, - {attributes, record_info(fields, reg_users_counter)}]), - update_table(), - update_reg_users_counter_table(Host), - maybe_alert_password_scrammed_without_option(), - ok. - -update_reg_users_counter_table(Server) -> - Set = get_vh_registered_users(Server), - Size = length(Set), - LServer = jlib:nameprep(Server), - F = fun () -> - mnesia:write(#reg_users_counter{vhost = LServer, - count = Size}) - end, - mnesia:sync_dirty(F). - -plain_password_required() -> - is_scrammed(). - -store_type() -> - case is_scrammed() of - false -> plain; %% allows: PLAIN DIGEST-MD5 SCRAM - true -> scram %% allows: PLAIN SCRAM - end. - -check_password(User, Server, Password) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - US = {LUser, LServer}, - case catch mnesia:dirty_read({passwd, US}) of - [#passwd{password = Password}] - when is_binary(Password) -> - Password /= <<"">>; - [#passwd{password = Scram}] - when is_record(Scram, scram) -> - is_password_scram_valid(Password, Scram); - _ -> false - end. - -check_password(User, Server, Password, Digest, - DigestGen) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - US = {LUser, LServer}, - case catch mnesia:dirty_read({passwd, US}) of - [#passwd{password = Passwd}] when is_binary(Passwd) -> - DigRes = if Digest /= <<"">> -> - Digest == DigestGen(Passwd); - true -> false - end, - if DigRes -> true; - true -> (Passwd == Password) and (Password /= <<"">>) - end; - [#passwd{password = Scram}] - when is_record(Scram, scram) -> - Passwd = jlib:decode_base64(Scram#scram.storedkey), - DigRes = if Digest /= <<"">> -> - Digest == DigestGen(Passwd); - true -> false - end, - if DigRes -> true; - true -> (Passwd == Password) and (Password /= <<"">>) - end; - _ -> false - end. - -%% @spec (User::string(), Server::string(), Password::string()) -> -%% ok | {error, invalid_jid} -set_password(User, Server, Password) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - US = {LUser, LServer}, - if (LUser == error) or (LServer == error) -> - {error, invalid_jid}; - true -> - F = fun () -> - Password2 = case is_scrammed() and is_binary(Password) - of - true -> password_to_scram(Password); - false -> Password - end, - mnesia:write(#passwd{us = US, password = Password2}) - end, - {atomic, ok} = mnesia:transaction(F), - ok - end. - -%% @spec (User, Server, Password) -> {atomic, ok} | {atomic, exists} | {error, invalid_jid} | {error, not_allowed} | {error, Reason} -try_register(User, Server, PasswordList) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - Password = iolist_to_binary(PasswordList), - US = {LUser, LServer}, - if (LUser == error) or (LServer == error) -> - {error, invalid_jid}; - true -> - F = fun () -> - case mnesia:read({passwd, US}) of - [] -> - Password2 = case is_scrammed() and - is_binary(Password) - of - true -> password_to_scram(Password); - false -> Password - end, - mnesia:write(#passwd{us = US, - password = Password2}), - mnesia:dirty_update_counter(reg_users_counter, - LServer, 1), - ok; - [_E] -> exists - end - end, - mnesia:transaction(F) - end. - -%% Get all registered users in Mnesia -dirty_get_registered_users() -> - mnesia:dirty_all_keys(passwd). - -get_vh_registered_users(Server) -> - LServer = jlib:nameprep(Server), - mnesia:dirty_select(passwd, - [{#passwd{us = '$1', _ = '_'}, - [{'==', {element, 2, '$1'}, LServer}], ['$1']}]). - -get_vh_registered_users(Server, - [{from, Start}, {to, End}]) - when is_integer(Start) and is_integer(End) -> - get_vh_registered_users(Server, - [{limit, End - Start + 1}, {offset, Start}]); -get_vh_registered_users(Server, - [{limit, Limit}, {offset, Offset}]) - when is_integer(Limit) and is_integer(Offset) -> - case get_vh_registered_users(Server) of - [] -> []; - Users -> - Set = lists:keysort(1, Users), - L = length(Set), - Start = if Offset < 1 -> 1; - Offset > L -> L; - true -> Offset - end, - lists:sublist(Set, Start, Limit) - end; -get_vh_registered_users(Server, [{prefix, Prefix}]) - when is_binary(Prefix) -> - Set = [{U, S} - || {U, S} <- get_vh_registered_users(Server), - str:prefix(Prefix, U)], - lists:keysort(1, Set); -get_vh_registered_users(Server, - [{prefix, Prefix}, {from, Start}, {to, End}]) - when is_binary(Prefix) and is_integer(Start) and - is_integer(End) -> - get_vh_registered_users(Server, - [{prefix, Prefix}, {limit, End - Start + 1}, - {offset, Start}]); -get_vh_registered_users(Server, - [{prefix, Prefix}, {limit, Limit}, {offset, Offset}]) - when is_binary(Prefix) and is_integer(Limit) and - is_integer(Offset) -> - case [{U, S} - || {U, S} <- get_vh_registered_users(Server), - str:prefix(Prefix, U)] - of - [] -> []; - Users -> - Set = lists:keysort(1, Users), - L = length(Set), - Start = if Offset < 1 -> 1; - Offset > L -> L; - true -> Offset - end, - lists:sublist(Set, Start, Limit) - end; -get_vh_registered_users(Server, _) -> - get_vh_registered_users(Server). - -get_vh_registered_users_number(Server) -> - LServer = jlib:nameprep(Server), - Query = mnesia:dirty_select(reg_users_counter, - [{#reg_users_counter{vhost = LServer, - count = '$1'}, - [], ['$1']}]), - case Query of - [Count] -> Count; - _ -> 0 - end. - -get_vh_registered_users_number(Server, - [{prefix, Prefix}]) - when is_binary(Prefix) -> - Set = [{U, S} - || {U, S} <- get_vh_registered_users(Server), - str:prefix(Prefix, U)], - length(Set); -get_vh_registered_users_number(Server, _) -> - get_vh_registered_users_number(Server). - -get_password(User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - US = {LUser, LServer}, - case catch mnesia:dirty_read(passwd, US) of - [#passwd{password = Password}] - when is_binary(Password) -> - Password; - [#passwd{password = Scram}] - when is_record(Scram, scram) -> - {jlib:decode_base64(Scram#scram.storedkey), - jlib:decode_base64(Scram#scram.serverkey), - jlib:decode_base64(Scram#scram.salt), - Scram#scram.iterationcount}; - _ -> false - end. - -get_password_s(User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - US = {LUser, LServer}, - case catch mnesia:dirty_read(passwd, US) of - [#passwd{password = Password}] - when is_binary(Password) -> - Password; - [#passwd{password = Scram}] - when is_record(Scram, scram) -> - <<"">>; - _ -> <<"">> - end. - -%% @spec (User, Server) -> true | false | {error, Error} -is_user_exists(User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - US = {LUser, LServer}, - case catch mnesia:dirty_read({passwd, US}) of - [] -> false; - [_] -> true; - Other -> {error, Other} - end. - -%% @spec (User, Server) -> ok -%% @doc Remove user. -%% Note: it returns ok even if there was some problem removing the user. -remove_user(User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - US = {LUser, LServer}, - F = fun () -> - mnesia:delete({passwd, US}), - mnesia:dirty_update_counter(reg_users_counter, LServer, - -1) - end, - mnesia:transaction(F), - ok. - -%% @spec (User, Server, Password) -> ok | not_exists | not_allowed | bad_request -%% @doc Remove user if the provided password is correct. -remove_user(User, Server, Password) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - US = {LUser, LServer}, - F = fun () -> - case mnesia:read({passwd, US}) of - [#passwd{password = Password}] - when is_binary(Password) -> - mnesia:delete({passwd, US}), - mnesia:dirty_update_counter(reg_users_counter, LServer, - -1), - ok; - [#passwd{password = Scram}] - when is_record(Scram, scram) -> - case is_password_scram_valid(Password, Scram) of - true -> - mnesia:delete({passwd, US}), - mnesia:dirty_update_counter(reg_users_counter, - LServer, -1), - ok; - false -> not_allowed - end; - _ -> not_exists - end - end, - case mnesia:transaction(F) of - {atomic, ok} -> ok; - {atomic, Res} -> Res; - _ -> bad_request - end. - -update_table() -> - Fields = record_info(fields, passwd), - case mnesia:table_info(passwd, attributes) of - Fields -> - convert_to_binary(Fields), - maybe_scram_passwords(), - ok; - _ -> - ?INFO_MSG("Recreating passwd table", []), - mnesia:transform_table(passwd, ignore, Fields) - end. - -convert_to_binary(Fields) -> - ejabberd_config:convert_table_to_binary( - passwd, Fields, set, - fun(#passwd{us = {U, _}}) -> U end, - fun(#passwd{us = {U, S}, password = Pass} = R) -> - NewUS = {iolist_to_binary(U), iolist_to_binary(S)}, - NewPass = case Pass of - #scram{storedkey = StoredKey, - serverkey = ServerKey, - salt = Salt} -> - Pass#scram{ - storedkey = iolist_to_binary(StoredKey), - serverkey = iolist_to_binary(ServerKey), - salt = iolist_to_binary(Salt)}; - _ -> - iolist_to_binary(Pass) - end, - R#passwd{us = NewUS, password = NewPass} - end). - -%%% -%%% SCRAM -%%% - -%% The passwords are stored scrammed in the table either if the option says so, -%% or if at least the first password is scrammed. -is_scrammed() -> - OptionScram = is_option_scram(), - FirstElement = mnesia:dirty_read(passwd, - mnesia:dirty_first(passwd)), - case {OptionScram, FirstElement} of - {true, _} -> true; - {false, [#passwd{password = Scram}]} - when is_record(Scram, scram) -> - true; - _ -> false - end. - -is_option_scram() -> - scram == - ejabberd_config:get_option({auth_password_format, ?MYNAME}, - fun(V) -> V end). - -maybe_alert_password_scrammed_without_option() -> - case is_scrammed() andalso not is_option_scram() of - true -> - ?ERROR_MSG("Some passwords were stored in the database " - "as SCRAM, but 'auth_password_format' " - "is not configured 'scram'. The option " - "will now be considered to be 'scram'.", - []); - false -> ok - end. - -maybe_scram_passwords() -> - case is_scrammed() of - true -> scram_passwords(); - false -> ok - end. - -scram_passwords() -> - ?INFO_MSG("Converting the stored passwords into " - "SCRAM bits", - []), - Fun = fun (#passwd{password = Password} = P) -> - Scram = password_to_scram(Password), - P#passwd{password = Scram} - end, - Fields = record_info(fields, passwd), - mnesia:transform_table(passwd, Fun, Fields). - -password_to_scram(Password) -> - password_to_scram(Password, - ?SCRAM_DEFAULT_ITERATION_COUNT). - -password_to_scram(Password, IterationCount) -> - Salt = crypto:rand_bytes(?SALT_LENGTH), - SaltedPassword = scram:salted_password(Password, Salt, - IterationCount), - StoredKey = - scram:stored_key(scram:client_key(SaltedPassword)), - ServerKey = scram:server_key(SaltedPassword), - #scram{storedkey = jlib:encode_base64(StoredKey), - serverkey = jlib:encode_base64(ServerKey), - salt = jlib:encode_base64(Salt), - iterationcount = IterationCount}. - -is_password_scram_valid(Password, Scram) -> - IterationCount = Scram#scram.iterationcount, - Salt = jlib:decode_base64(Scram#scram.salt), - SaltedPassword = scram:salted_password(Password, Salt, - IterationCount), - StoredKey = - scram:stored_key(scram:client_key(SaltedPassword)), - jlib:decode_base64(Scram#scram.storedkey) == StoredKey. - -export(_Server) -> - [{passwd, - fun(Host, #passwd{us = {LUser, LServer}, password = Password}) - when LServer == Host -> - Username = ejabberd_odbc:escape(LUser), - Pass = ejabberd_odbc:escape(Password), - [[<<"delete from users where username='">>, Username, <<"';">>], - [<<"insert into users(username, password) " - "values ('">>, Username, <<"', '">>, Pass, <<"');">>]]; - (_Host, _R) -> - [] - end}]. - -import(LServer) -> - [{<<"select username, password from users;">>, - fun([LUser, Password]) -> - #passwd{us = {LUser, LServer}, password = Password} - end}]. - -import(_LServer, mnesia, #passwd{} = P) -> - mnesia:dirty_write(P); -import(_, _, _) -> - pass. diff --git a/src/ejabberd_auth_jwt.erl b/src/ejabberd_auth_jwt.erl new file mode 100644 index 000000000..7fac3e4f7 --- /dev/null +++ b/src/ejabberd_auth_jwt.erl @@ -0,0 +1,154 @@ +%%%---------------------------------------------------------------------- +%%% File : ejabberd_auth_jwt.erl +%%% Author : Mickael Remond +%%% Purpose : Authentication using JWT tokens +%%% Created : 16 Mar 2019 by Mickael Remond +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(ejabberd_auth_jwt). + +-author('mremond@process-one.net'). + +-behaviour(ejabberd_auth). + +-export([start/1, stop/1, check_password/4, + store_type/1, plain_password_required/1, + user_exists/2, use_cache/1 + ]). +%% 'ejabberd_hooks' callback: +-export([check_decoded_jwt/5]). + +-include_lib("xmpp/include/xmpp.hrl"). +-include("logger.hrl"). + +%%%---------------------------------------------------------------------- +%%% API +%%%---------------------------------------------------------------------- +start(Host) -> + %% We add our default JWT verifier with hook priority 100. + %% So if you need to check or verify your custom JWT before the + %% default verifier, It's better to use this hook with priority + %% little than 100 and return bool() or {stop, bool()} in your own + %% callback function. + ejabberd_hooks:add(check_decoded_jwt, Host, ?MODULE, check_decoded_jwt, 100), + case ejabberd_option:jwt_key(Host) of + undefined -> + ?ERROR_MSG("Option jwt_key is not configured for ~ts: " + "JWT authentication won't work", [Host]); + _ -> + ok + end. + +stop(Host) -> + ejabberd_hooks:delete(check_decoded_jwt, Host, ?MODULE, check_decoded_jwt, 100). + +plain_password_required(_Host) -> true. + +store_type(_Host) -> external. + +-spec check_password(binary(), binary(), binary(), binary()) -> {ets_cache:tag(), boolean() | {stop, boolean()}}. +check_password(User, AuthzId, Server, Token) -> + %% MREMOND: Should we move the AuthzId check at a higher level in + %% the call stack? + if AuthzId /= <<>> andalso AuthzId /= User -> + {nocache, false}; + true -> + if Token == <<"">> -> {nocache, false}; + true -> + Res = check_jwt_token(User, Server, Token), + Rule = ejabberd_option:jwt_auth_only_rule(Server), + case acl:match_rule(Server, Rule, + jid:make(User, Server, <<"">>)) of + deny -> + {nocache, Res}; + allow -> + {nocache, {stop, Res}} + end + end + end. + +user_exists(User, Host) -> + %% Checking that the user has an active session + %% If the session was negociated by the JWT auth method then we define that the user exists + %% Any other cases will return that the user doesn't exist + {nocache, case ejabberd_sm:get_user_info(User, Host) of + [{_, Info}] -> proplists:get_value(auth_module, Info) == ejabberd_auth_jwt; + _ -> false + end}. + +use_cache(_) -> + false. + +%%%---------------------------------------------------------------------- +%%% 'ejabberd_hooks' callback +%%%---------------------------------------------------------------------- +check_decoded_jwt(true, Fields, _Signature, Server, User) -> + JidField = ejabberd_option:jwt_jid_field(Server), + case maps:find(JidField, Fields) of + {ok, SJid} when is_binary(SJid) -> + try + JID = jid:decode(SJid), + JID#jid.luser == User andalso JID#jid.lserver == Server + catch error:{bad_jid, _} -> + false + end; + _ -> % error | {ok, _UnknownType} + false + end; +check_decoded_jwt(Acc, _, _, _, _) -> + Acc. + +%%%---------------------------------------------------------------------- +%%% Internal functions +%%%---------------------------------------------------------------------- +check_jwt_token(User, Server, Token) -> + JWK = ejabberd_option:jwt_key(Server), + try jose_jwt:verify(JWK, Token) of + {true, {jose_jwt, Fields}, Signature} -> + Now = erlang:system_time(second), + ?DEBUG("jwt verify at system timestamp ~p: ~p - ~p~n", [Now, Fields, Signature]), + case maps:find(<<"exp">>, Fields) of + error -> + %% No expiry in token => We consider token invalid: + false; + {ok, Exp} -> + if + Exp > Now -> + ejabberd_hooks:run_fold( + check_decoded_jwt, + Server, + true, + [Fields, Signature, Server, User] + ); + true -> + %% return false, if token has expired + false + end + end; + {false, _, _} -> + false + catch + A:B -> + ?DEBUG("jose_jwt:verify failed ~n for account ~p@~p~n " + " JWK and token: ~p~n with error: ~p", + [User, Server, {JWK, Token}, {A, B}]), + false + end. diff --git a/src/ejabberd_auth_ldap.erl b/src/ejabberd_auth_ldap.erl index 3055d1044..091e567a8 100644 --- a/src/ejabberd_auth_ldap.erl +++ b/src/ejabberd_auth_ldap.erl @@ -1,11 +1,11 @@ %%%---------------------------------------------------------------------- %%% File : ejabberd_auth_ldap.erl %%% Author : Alexey Shchepin -%%% Purpose : Authentification via LDAP +%%% Purpose : Authentication via LDAP %%% Created : 12 Dec 2004 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -34,18 +34,12 @@ -export([init/1, handle_info/2, handle_call/3, handle_cast/2, terminate/2, code_change/3]). -%% External exports -export([start/1, stop/1, start_link/1, set_password/3, - check_password/3, check_password/5, try_register/3, - dirty_get_registered_users/0, get_vh_registered_users/1, - get_vh_registered_users/2, - get_vh_registered_users_number/1, - get_vh_registered_users_number/2, get_password/2, - get_password_s/2, is_user_exists/2, remove_user/2, - remove_user/3, store_type/0, - plain_password_required/0]). + check_password/4, user_exists/2, + get_users/2, count_users/2, + store_type/1, plain_password_required/1, + reload/1]). --include("ejabberd.hrl"). -include("logger.hrl"). -include("eldap.hrl"). @@ -64,16 +58,19 @@ uids = [] :: [{binary()} | {binary(), binary()}], ufilter = <<"">> :: binary(), sfilter = <<"">> :: binary(), - lfilter :: {any(), any()}, deref_aliases = never :: never | searching | finding | always, - dn_filter :: binary(), + dn_filter :: binary() | undefined, dn_filter_attrs = [] :: [binary()]}). -handle_cast(_Request, State) -> {noreply, State}. +handle_cast(Msg, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Msg]), + {noreply, State}. code_change(_OldVsn, State, _Extra) -> {ok, State}. -handle_info(_Info, State) -> {noreply, State}. +handle_info(Info, State) -> + ?WARNING_MSG("Unexpected info: ~p", [Info]), + {noreply, State}. -define(LDAP_SEARCH_TIMEOUT, 5). @@ -85,13 +82,14 @@ start(Host) -> Proc = gen_mod:get_module_proc(Host, ?MODULE), ChildSpec = {Proc, {?MODULE, start_link, [Host]}, transient, 1000, worker, [?MODULE]}, - supervisor:start_child(ejabberd_sup, ChildSpec). + supervisor:start_child(ejabberd_backend_sup, ChildSpec). stop(Host) -> Proc = gen_mod:get_module_proc(Host, ?MODULE), - gen_server:call(Proc, stop), - supervisor:terminate_child(ejabberd_sup, Proc), - supervisor:delete_child(ejabberd_sup, Proc). + case supervisor:terminate_child(ejabberd_backend_sup, Proc) of + ok -> supervisor:delete_child(ejabberd_backend_sup, Proc); + Err -> Err + end. start_link(Host) -> Proc = gen_mod:get_module_proc(Host, ?MODULE), @@ -100,6 +98,7 @@ start_link(Host) -> terminate(_Reason, _State) -> ok. init(Host) -> + process_flag(trap_exit, true), State = parse_options(Host), eldap_pool:start_link(State#state.eldap_id, State#state.servers, State#state.backups, @@ -111,74 +110,53 @@ init(Host) -> State#state.password, State#state.tls_options), {ok, State}. -plain_password_required() -> true. +reload(Host) -> + stop(Host), + start(Host). -store_type() -> external. +plain_password_required(_) -> true. -check_password(User, Server, Password) -> - if Password == <<"">> -> false; +store_type(_) -> external. + +check_password(User, AuthzId, Server, Password) -> + if AuthzId /= <<>> andalso AuthzId /= User -> + {nocache, false}; + Password == <<"">> -> + {nocache, false}; true -> - case catch check_password_ldap(User, Server, Password) - of - {'EXIT', _} -> false; - Result -> Result - end + case catch check_password_ldap(User, Server, Password) of + {'EXIT', _} -> {nocache, false}; + Result -> {cache, Result} + end end. -check_password(User, Server, Password, _Digest, - _DigestGen) -> - check_password(User, Server, Password). - set_password(User, Server, Password) -> {ok, State} = eldap_utils:get_state(Server, ?MODULE), case find_user_dn(User, State) of - false -> {error, user_not_found}; + false -> {cache, {error, db_failure}}; DN -> - eldap_pool:modify_passwd(State#state.eldap_id, DN, - Password) + case eldap_pool:modify_passwd(State#state.eldap_id, DN, + Password) of + ok -> {cache, {ok, Password}}; + _Err -> {nocache, {error, db_failure}} + end end. -%% @spec (User, Server, Password) -> {error, not_allowed} -try_register(_User, _Server, _Password) -> - {error, not_allowed}. - -dirty_get_registered_users() -> - Servers = ejabberd_config:get_vh_by_auth_method(ldap), - lists:flatmap(fun (Server) -> - get_vh_registered_users(Server) - end, - Servers). - -get_vh_registered_users(Server) -> - case catch get_vh_registered_users_ldap(Server) of +get_users(Server, []) -> + case catch get_users_ldap(Server) of {'EXIT', _} -> []; Result -> Result end. -get_vh_registered_users(Server, _) -> - get_vh_registered_users(Server). +count_users(Server, Opts) -> + length(get_users(Server, Opts)). -get_vh_registered_users_number(Server) -> - length(get_vh_registered_users(Server)). - -get_vh_registered_users_number(Server, _) -> - get_vh_registered_users_number(Server). - -get_password(_User, _Server) -> false. - -get_password_s(_User, _Server) -> <<"">>. - -%% @spec (User, Server) -> true | false | {error, Error} -is_user_exists(User, Server) -> - case catch is_user_exists_ldap(User, Server) of - {'EXIT', Error} -> {error, Error}; - Result -> Result +user_exists(User, Server) -> + case catch user_exists_ldap(User, Server) of + {'EXIT', _Error} -> {nocache, {error, db_failure}}; + Result -> {cache, Result} end. -remove_user(_User, _Server) -> {error, not_allowed}. - -remove_user(_User, _Server, _Password) -> not_allowed. - %%%---------------------------------------------------------------------- %%% Internal functions %%%---------------------------------------------------------------------- @@ -195,7 +173,7 @@ check_password_ldap(User, Server, Password) -> end end. -get_vh_registered_users_ldap(Server) -> +get_users_ldap(Server) -> {ok, State} = eldap_utils:get_state(Server, ?MODULE), UIDs = State#state.uids, Eldap_ID = State#state.eldap_id, @@ -227,11 +205,11 @@ get_vh_registered_users_ldap(Server) -> UIDFormat) of {ok, U} -> - case jlib:nodeprep(U) of + case jid:nodeprep(U) of error -> []; LU -> [{LU, - jlib:nameprep(Server)}] + jid:nameprep(Server)}] end; _ -> [] end @@ -244,7 +222,7 @@ get_vh_registered_users_ldap(Server) -> _ -> [] end. -is_user_exists_ldap(User, Server) -> +user_exists_ldap(User, Server) -> {ok, State} = eldap_utils:get_state(Server, ?MODULE), case find_user_dn(User, State) of false -> false; @@ -255,8 +233,9 @@ handle_call(get_state, _From, State) -> {reply, {ok, State}, State}; handle_call(stop, _From, State) -> {stop, normal, ok, State}; -handle_call(_Request, _From, State) -> - {reply, bad_request, State}. +handle_call(Request, From, State) -> + ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), + {noreply, State}. find_user_dn(User, State) -> ResAttrs = result_attrs(State), @@ -273,19 +252,12 @@ find_user_dn(User, State) -> [#eldap_entry{attributes = Attrs, object_name = DN} | _]} -> - dn_filter(DN, Attrs, State); + is_valid_dn(DN, Attrs, State); _ -> false end; _ -> false end. -%% apply the dn filter and the local filter: -dn_filter(DN, Attrs, State) -> - case check_local_filter(Attrs, State) of - false -> false; - true -> is_valid_dn(DN, Attrs, State) - end. - %% Check that the DN is valid, based on the dn filter is_valid_dn(DN, _, #state{dn_filter = undefined}) -> DN; is_valid_dn(DN, Attrs, State) -> @@ -321,30 +293,6 @@ is_valid_dn(DN, Attrs, State) -> _ -> false end. -%% The local filter is used to check an attribute in ejabberd -%% and not in LDAP to limit the load on the LDAP directory. -%% A local rule can be either: -%% {equal, {"accountStatus",["active"]}} -%% {notequal, {"accountStatus",["disabled"]}} -%% {ldap_local_filter, {notequal, {"accountStatus",["disabled"]}}} -check_local_filter(_Attrs, - #state{lfilter = undefined}) -> - true; -check_local_filter(Attrs, - #state{lfilter = LocalFilter}) -> - {Operation, FilterMatch} = LocalFilter, - local_filter(Operation, Attrs, FilterMatch). - -local_filter(equal, Attrs, FilterMatch) -> - {Attr, Value} = FilterMatch, - case lists:keysearch(Attr, 1, Attrs) of - false -> false; - {value, {Attr, Value}} -> true; - _ -> false - end; -local_filter(notequal, Attrs, FilterMatch) -> - not local_filter(equal, Attrs, FilterMatch). - result_attrs(#state{uids = UIDs, dn_filter_attrs = DNFilterAttrs}) -> lists:foldl(fun ({UID}, Acc) -> [UID | Acc]; @@ -356,50 +304,21 @@ result_attrs(#state{uids = UIDs, %%% Auxiliary functions %%%---------------------------------------------------------------------- parse_options(Host) -> - Cfg = eldap_utils:get_config(Host, []), - Eldap_ID = jlib:atom_to_binary(gen_mod:get_module_proc(Host, ?MODULE)), - Bind_Eldap_ID = jlib:atom_to_binary( + Cfg = ?eldap_config(ejabberd_option, Host), + Eldap_ID = misc:atom_to_binary(gen_mod:get_module_proc(Host, ?MODULE)), + Bind_Eldap_ID = misc:atom_to_binary( gen_mod:get_module_proc(Host, bind_ejabberd_auth_ldap)), - UIDsTemp = eldap_utils:get_opt( - {ldap_uids, Host}, [], - fun(Us) -> - lists:map( - fun({U, P}) -> - {iolist_to_binary(U), - iolist_to_binary(P)}; - ({U}) -> - {iolist_to_binary(U)}; - (U) -> - {iolist_to_binary(U)} - end, lists:flatten(Us)) - end, [{<<"uid">>, <<"%u">>}]), + UIDsTemp = ejabberd_option:ldap_uids(Host), UIDs = eldap_utils:uids_domain_subst(Host, UIDsTemp), SubFilter = eldap_utils:generate_subfilter(UIDs), - UserFilter = case eldap_utils:get_opt( - {ldap_filter, Host}, [], - fun check_filter/1, <<"">>) of + UserFilter = case ejabberd_option:ldap_filter(Host) of <<"">> -> SubFilter; F -> <<"(&", SubFilter/binary, F/binary, ")">> end, - SearchFilter = eldap_filter:do_sub(UserFilter, - [{<<"%u">>, <<"*">>}]), - {DNFilter, DNFilterAttrs} = - eldap_utils:get_opt({ldap_dn_filter, Host}, [], - fun([{DNF, DNFA}]) -> - NewDNFA = case DNFA of - undefined -> - []; - _ -> - [iolist_to_binary(A) - || A <- DNFA] - end, - NewDNF = check_filter(DNF), - {NewDNF, NewDNFA} - end, {undefined, []}), - LocalFilter = eldap_utils:get_opt( - {ldap_local_filter, Host}, [], fun(V) -> V end), + SearchFilter = eldap_filter:do_sub(UserFilter, [{<<"%u">>, <<"*">>}]), + {DNFilter, DNFilterAttrs} = ejabberd_option:ldap_dn_filter(Host), #state{host = Host, eldap_id = Eldap_ID, bind_eldap_id = Bind_Eldap_ID, servers = Cfg#eldap_config.servers, @@ -411,10 +330,5 @@ parse_options(Host) -> base = Cfg#eldap_config.base, deref_aliases = Cfg#eldap_config.deref_aliases, uids = UIDs, ufilter = UserFilter, - sfilter = SearchFilter, lfilter = LocalFilter, + sfilter = SearchFilter, dn_filter = DNFilter, dn_filter_attrs = DNFilterAttrs}. - -check_filter(F) -> - NewF = iolist_to_binary(F), - {ok, _} = eldap_filter:parse(NewF), - NewF. diff --git a/src/ejabberd_auth_mnesia.erl b/src/ejabberd_auth_mnesia.erl new file mode 100644 index 000000000..996dd620f --- /dev/null +++ b/src/ejabberd_auth_mnesia.erl @@ -0,0 +1,297 @@ +%%%---------------------------------------------------------------------- +%%% File : ejabberd_auth_mnesia.erl +%%% Author : Alexey Shchepin +%%% Purpose : Authentication via mnesia +%%% Created : 12 Dec 2004 by Alexey Shchepin +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(ejabberd_auth_mnesia). + +-author('alexey@process-one.net'). + +-behaviour(ejabberd_auth). + +-export([start/1, stop/1, set_password_multiple/3, try_register_multiple/3, + get_users/2, init_db/0, + count_users/2, get_password/2, + remove_user/2, store_type/1, import/2, + plain_password_required/1, use_cache/1, drop_password_type/2, set_password_instance/3]). +-export([need_transform/1, transform/1]). + +-include("logger.hrl"). +-include_lib("xmpp/include/scram.hrl"). +-include("ejabberd_auth.hrl"). + +-record(reg_users_counter, {vhost = <<"">> :: binary(), + count = 0 :: integer() | '$1'}). + +%%%---------------------------------------------------------------------- +%%% API +%%%---------------------------------------------------------------------- +start(Host) -> + init_db(), + update_reg_users_counter_table(Host), + ok. + +stop(_Host) -> + ok. + +init_db() -> + ejabberd_mnesia:create(?MODULE, passwd, + [{disc_only_copies, [node()]}, + {attributes, record_info(fields, passwd)}]), + ejabberd_mnesia:create(?MODULE, reg_users_counter, + [{ram_copies, [node()]}, + {attributes, record_info(fields, reg_users_counter)}]). + +update_reg_users_counter_table(Server) -> + Set = get_users(Server, []), + Size = length(Set), + LServer = jid:nameprep(Server), + F = fun () -> + mnesia:write(#reg_users_counter{vhost = LServer, + count = Size}) + end, + mnesia:sync_dirty(F). + +use_cache(Host) -> + case mnesia:table_info(passwd, storage_type) of + disc_only_copies -> + ejabberd_option:auth_use_cache(Host); + _ -> + false + end. + +plain_password_required(Server) -> + store_type(Server) == scram. + +store_type(Server) -> + ejabberd_auth:password_format(Server). + +set_password_multiple(User, Server, Passwords) -> + F = fun() -> + lists:foreach( + fun(#scram{hash = Hash} = Password) -> + mnesia:write(#passwd{us = {User, Server, Hash}, password = Password}); + (Plain) -> + mnesia:write(#passwd{us = {User, Server, plain}, password = Plain}) + end, Passwords) + end, + case mnesia:transaction(F) of + {atomic, ok} -> + {cache, {ok, Passwords}}; + {aborted, Reason} -> + ?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]), + {nocache, {error, db_failure}} + end. + +set_password_instance(User, Server, Password) -> + F = fun() -> + case Password of + #scram{hash = Hash} = Password -> + mnesia:write(#passwd{us = {User, Server, Hash}, password = Password}); + Plain -> + mnesia:write(#passwd{us = {User, Server, plain}, password = Plain}) + end + end, + case mnesia:transaction(F) of + {atomic, ok} -> + ok; + {aborted, Reason} -> + ?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]), + {error, db_failure} + end. + +try_register_multiple(User, Server, Passwords) -> + F = fun() -> + case mnesia:select(passwd, [{{'_', {'$1', '$2', '_'}, '$3'}, + [{'==', '$1', User}, + {'==', '$2', Server}], + ['$3']}]) of + [] -> + lists:foreach( + fun(#scram{hash = Hash} = Password) -> + mnesia:write(#passwd{us = {User, Server, Hash}, password = Password}); + (Plain) -> + mnesia:write(#passwd{us = {User, Server, plain}, password = Plain}) + end, Passwords), + mnesia:dirty_update_counter(reg_users_counter, Server, 1), + {ok, Passwords}; + [_] -> + {error, exists} + end + end, + case mnesia:transaction(F) of + {atomic, Res} -> + {cache, Res}; + {aborted, Reason} -> + ?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]), + {nocache, {error, db_failure}} + end. + +get_users(Server, []) -> + Users = mnesia:dirty_select(passwd, + [{#passwd{us = '$1', _ = '_'}, + [{'==', {element, 2, '$1'}, Server}], ['$1']}]), + misc:lists_uniq([{U, S} || {U, S, _} <- Users]); +get_users(Server, [{from, Start}, {to, End}]) + when is_integer(Start) and is_integer(End) -> + get_users(Server, [{limit, End - Start + 1}, {offset, Start}]); +get_users(Server, [{limit, Limit}, {offset, Offset}]) + when is_integer(Limit) and is_integer(Offset) -> + case get_users(Server, []) of + [] -> + []; + Users -> + Set = lists:keysort(1, Users), + L = length(Set), + Start = if Offset < 1 -> 1; + Offset > L -> L; + true -> Offset + end, + lists:sublist(Set, Start, Limit) + end; +get_users(Server, [{prefix, Prefix}]) when is_binary(Prefix) -> + Set = [{U, S} || {U, S} <- get_users(Server, []), str:prefix(Prefix, U)], + lists:keysort(1, Set); +get_users(Server, [{prefix, Prefix}, {from, Start}, {to, End}]) + when is_binary(Prefix) and is_integer(Start) and is_integer(End) -> + get_users(Server, [{prefix, Prefix}, {limit, End - Start + 1}, + {offset, Start}]); +get_users(Server, [{prefix, Prefix}, {limit, Limit}, {offset, Offset}]) + when is_binary(Prefix) and is_integer(Limit) and is_integer(Offset) -> + case [{U, S} || {U, S} <- get_users(Server, []), str:prefix(Prefix, U)] of + [] -> + []; + Users -> + Set = lists:keysort(1, Users), + L = length(Set), + Start = if Offset < 1 -> 1; + Offset > L -> L; + true -> Offset + end, + lists:sublist(Set, Start, Limit) + end; +get_users(Server, _) -> + get_users(Server, []). + +count_users(Server, []) -> + case mnesia:dirty_select( + reg_users_counter, + [{#reg_users_counter{vhost = Server, count = '$1'}, + [], ['$1']}]) of + [Count] -> Count; + _ -> 0 + end; +count_users(Server, [{prefix, Prefix}]) when is_binary(Prefix) -> + Set = [{U, S} || {U, S} <- get_users(Server, []), str:prefix(Prefix, U)], + length(Set); +count_users(Server, _) -> + count_users(Server, []). + +get_password(User, Server) -> + case mnesia:dirty_select(passwd, [{{'_', {'$1', '$2', '_'}, '$3'}, + [{'==', '$1', User}, + {'==', '$2', Server}], + ['$3']}]) of + [_|_] = List -> + List2 = lists:map( + fun({scram, SK, SEK, Salt, IC}) -> + #scram{storedkey = SK, serverkey = SEK, + salt = Salt, hash = sha, iterationcount = IC}; + (Other) -> Other + end, List), + {cache, {ok, List2}}; + _ -> + {cache, error} + end. + +drop_password_type(Server, Hash) -> + F = fun() -> + Keys = mnesia:select(passwd, [{{'_', '$1', '_'}, + [{'==', {element, 3, '$1'}, Hash}, + {'==', {element, 2, '$1'}, Server}], + ['$1']}]), + lists:foreach(fun(Key) -> mnesia:delete({passwd, Key}) end, Keys), + ok + end, + case mnesia:transaction(F) of + {atomic, ok} -> + ok; + {aborted, Reason} -> + ?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]), + {error, db_failure} + end. + +remove_user(User, Server) -> + F = fun () -> + Keys = mnesia:select(passwd, [{{'_', '$1', '_'}, + [{'==', {element, 1, '$1'}, User}, + {'==', {element, 2, '$1'}, Server}], + ['$1']}]), + lists:foreach(fun(Key) -> mnesia:delete({passwd, Key}) end, Keys), + mnesia:dirty_update_counter(reg_users_counter, Server, -1), + ok + end, + case mnesia:transaction(F) of + {atomic, ok} -> + ok; + {aborted, Reason} -> + ?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]), + {error, db_failure} + end. + +need_transform(#reg_users_counter{}) -> + false; +need_transform({passwd, {_U, _S, _T}, _Pass}) -> + false; +need_transform({passwd, {_U, _S}, _Pass}) -> + true. + +transform({passwd, {U, S}, Pass}) + when is_list(U) orelse is_list(S) orelse is_list(Pass) -> + NewUS = {iolist_to_binary(U), iolist_to_binary(S)}, + NewPass = case Pass of + #scram{storedkey = StoredKey, + serverkey = ServerKey, + salt = Salt} -> + Pass#scram{ + storedkey = iolist_to_binary(StoredKey), + serverkey = iolist_to_binary(ServerKey), + salt = iolist_to_binary(Salt)}; + _ -> + iolist_to_binary(Pass) + end, + transform(#passwd{us = NewUS, password = NewPass}); +transform(#passwd{us = {U, S}, password = Password} = P) + when is_binary(Password) -> + P#passwd{us = {U, S, plain}, password = Password}; +transform({passwd, {U, S}, {scram, SK, SEK, Salt, IC}}) -> + #passwd{us = {U, S, sha}, + password = #scram{storedkey = SK, serverkey = SEK, + salt = Salt, hash = sha, iterationcount = IC}}; +transform(#passwd{us = {U, S}, password = #scram{hash = Hash}} = P) -> + P#passwd{us = {U, S, Hash}}; +transform(Other) -> Other. + +import(LServer, [LUser, Password, _TimeStamp]) -> + mnesia:dirty_write( + #passwd{us = {LUser, LServer}, password = Password}). diff --git a/src/ejabberd_auth_odbc.erl b/src/ejabberd_auth_odbc.erl deleted file mode 100644 index aea039c1b..000000000 --- a/src/ejabberd_auth_odbc.erl +++ /dev/null @@ -1,256 +0,0 @@ -%%%---------------------------------------------------------------------- -%%% File : ejabberd_auth_odbc.erl -%%% Author : Alexey Shchepin -%%% Purpose : Authentification via ODBC -%%% Created : 12 Dec 2004 by Alexey Shchepin -%%% -%%% -%%% ejabberd, Copyright (C) 2002-2015 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(ejabberd_auth_odbc). - --author('alexey@process-one.net'). - --behaviour(ejabberd_auth). - -%% External exports --export([start/1, set_password/3, check_password/3, - check_password/5, try_register/3, - dirty_get_registered_users/0, get_vh_registered_users/1, - get_vh_registered_users/2, - get_vh_registered_users_number/1, - get_vh_registered_users_number/2, get_password/2, - get_password_s/2, is_user_exists/2, remove_user/2, - remove_user/3, store_type/0, - plain_password_required/0]). - --include("ejabberd.hrl"). --include("logger.hrl"). - -%%%---------------------------------------------------------------------- -%%% API -%%%---------------------------------------------------------------------- -start(_Host) -> ok. - -plain_password_required() -> false. - -store_type() -> plain. - -%% @spec (User, Server, Password) -> true | false | {error, Error} -check_password(User, Server, Password) -> - case jlib:nodeprep(User) of - error -> false; - LUser -> - Username = ejabberd_odbc:escape(LUser), - LServer = jlib:nameprep(Server), - try odbc_queries:get_password(LServer, Username) of - {selected, [<<"password">>], [[Password]]} -> - Password /= <<"">>; - {selected, [<<"password">>], [[_Password2]]} -> - false; %% Password is not correct - {selected, [<<"password">>], []} -> - false; %% Account does not exist - {error, _Error} -> - false %% Typical error is that table doesn't exist - catch - _:_ -> - false %% Typical error is database not accessible - end - end. - -%% @spec (User, Server, Password, Digest, DigestGen) -> true | false | {error, Error} -check_password(User, Server, Password, Digest, - DigestGen) -> - case jlib:nodeprep(User) of - error -> false; - LUser -> - Username = ejabberd_odbc:escape(LUser), - LServer = jlib:nameprep(Server), - try odbc_queries:get_password(LServer, Username) of - %% Account exists, check if password is valid - {selected, [<<"password">>], [[Passwd]]} -> - DigRes = if Digest /= <<"">> -> - Digest == DigestGen(Passwd); - true -> false - end, - if DigRes -> true; - true -> (Passwd == Password) and (Password /= <<"">>) - end; - {selected, [<<"password">>], []} -> - false; %% Account does not exist - {error, _Error} -> - false %% Typical error is that table doesn't exist - catch - _:_ -> - false %% Typical error is database not accessible - end - end. - -%% @spec (User::string(), Server::string(), Password::string()) -> -%% ok | {error, invalid_jid} -set_password(User, Server, Password) -> - case jlib:nodeprep(User) of - error -> {error, invalid_jid}; - LUser -> - Username = ejabberd_odbc:escape(LUser), - Pass = ejabberd_odbc:escape(Password), - LServer = jlib:nameprep(Server), - case catch odbc_queries:set_password_t(LServer, - Username, Pass) - of - {atomic, ok} -> ok; - Other -> {error, Other} - end - end. - -%% @spec (User, Server, Password) -> {atomic, ok} | {atomic, exists} | {error, invalid_jid} -try_register(User, Server, Password) -> - case jlib:nodeprep(User) of - error -> {error, invalid_jid}; - LUser -> - Username = ejabberd_odbc:escape(LUser), - Pass = ejabberd_odbc:escape(Password), - LServer = jlib:nameprep(Server), - case catch odbc_queries:add_user(LServer, Username, - Pass) - of - {updated, 1} -> {atomic, ok}; - _ -> {atomic, exists} - end - end. - -dirty_get_registered_users() -> - Servers = ejabberd_config:get_vh_by_auth_method(odbc), - lists:flatmap(fun (Server) -> - get_vh_registered_users(Server) - end, - Servers). - -get_vh_registered_users(Server) -> - LServer = jlib:nameprep(Server), - case catch odbc_queries:list_users(LServer) of - {selected, [<<"username">>], Res} -> - [{U, LServer} || [U] <- Res]; - _ -> [] - end. - -get_vh_registered_users(Server, Opts) -> - LServer = jlib:nameprep(Server), - case catch odbc_queries:list_users(LServer, Opts) of - {selected, [<<"username">>], Res} -> - [{U, LServer} || [U] <- Res]; - _ -> [] - end. - -get_vh_registered_users_number(Server) -> - LServer = jlib:nameprep(Server), - case catch odbc_queries:users_number(LServer) of - {selected, [_], [[Res]]} -> - jlib:binary_to_integer(Res); - _ -> 0 - end. - -get_vh_registered_users_number(Server, Opts) -> - LServer = jlib:nameprep(Server), - case catch odbc_queries:users_number(LServer, Opts) of - {selected, [_], [[Res]]} -> - jlib:binary_to_integer(Res); - _Other -> 0 - end. - -get_password(User, Server) -> - case jlib:nodeprep(User) of - error -> false; - LUser -> - Username = ejabberd_odbc:escape(LUser), - LServer = jlib:nameprep(Server), - case catch odbc_queries:get_password(LServer, Username) - of - {selected, [<<"password">>], [[Password]]} -> Password; - _ -> false - end - end. - -get_password_s(User, Server) -> - case jlib:nodeprep(User) of - error -> <<"">>; - LUser -> - Username = ejabberd_odbc:escape(LUser), - LServer = jlib:nameprep(Server), - case catch odbc_queries:get_password(LServer, Username) - of - {selected, [<<"password">>], [[Password]]} -> Password; - _ -> <<"">> - end - end. - -%% @spec (User, Server) -> true | false | {error, Error} -is_user_exists(User, Server) -> - case jlib:nodeprep(User) of - error -> false; - LUser -> - Username = ejabberd_odbc:escape(LUser), - LServer = jlib:nameprep(Server), - try odbc_queries:get_password(LServer, Username) of - {selected, [<<"password">>], [[_Password]]} -> - true; %% Account exists - {selected, [<<"password">>], []} -> - false; %% Account does not exist - {error, Error} -> {error, Error} - catch - _:B -> {error, B} - end - end. - -%% @spec (User, Server) -> ok | error -%% @doc Remove user. -%% Note: it may return ok even if there was some problem removing the user. -remove_user(User, Server) -> - case jlib:nodeprep(User) of - error -> error; - LUser -> - Username = ejabberd_odbc:escape(LUser), - LServer = jlib:nameprep(Server), - catch odbc_queries:del_user(LServer, Username), - ok - end. - -%% @spec (User, Server, Password) -> ok | error | not_exists | not_allowed -%% @doc Remove user if the provided password is correct. -remove_user(User, Server, Password) -> - case jlib:nodeprep(User) of - error -> error; - LUser -> - Username = ejabberd_odbc:escape(LUser), - Pass = ejabberd_odbc:escape(Password), - LServer = jlib:nameprep(Server), - F = fun () -> - Result = odbc_queries:del_user_return_password(LServer, - Username, - Pass), - case Result of - {selected, [<<"password">>], [[Password]]} -> ok; - {selected, [<<"password">>], []} -> not_exists; - _ -> not_allowed - end - end, - {atomic, Result} = odbc_queries:sql_transaction(LServer, - F), - Result - end. diff --git a/src/ejabberd_auth_pam.erl b/src/ejabberd_auth_pam.erl index f3fdf628d..d795b0d6f 100644 --- a/src/ejabberd_auth_pam.erl +++ b/src/ejabberd_auth_pam.erl @@ -5,7 +5,7 @@ %%% Created : 5 Jul 2007 by Evgeniy Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -28,93 +28,52 @@ -behaviour(ejabberd_auth). -%% External exports -%%==================================================================== -%% API -%%==================================================================== --export([start/1, set_password/3, check_password/3, - check_password/5, try_register/3, - dirty_get_registered_users/0, get_vh_registered_users/1, - get_vh_registered_users/2, get_vh_registered_users_number/1, - get_vh_registered_users_number/2, - get_password/2, get_password_s/2, is_user_exists/2, - remove_user/2, remove_user/3, store_type/0, - plain_password_required/0]). +-export([start/1, stop/1, check_password/4, + user_exists/2, store_type/1, plain_password_required/1]). start(_Host) -> - ejabberd:start_app(p1_pam). + ejabberd:start_app(epam). -set_password(_User, _Server, _Password) -> - {error, not_allowed}. +stop(_Host) -> + ok. -check_password(User, Server, Password, _Digest, - _DigestGen) -> - check_password(User, Server, Password). - -check_password(User, Host, Password) -> - Service = get_pam_service(Host), - UserInfo = case get_pam_userinfotype(Host) of - username -> User; - jid -> <> - end, - case catch epam:authenticate(Service, UserInfo, - Password) - of - true -> true; - _ -> false +check_password(User, AuthzId, Host, Password) -> + if AuthzId /= <<>> andalso AuthzId /= User -> + false; + true -> + Service = get_pam_service(Host), + UserInfo = case get_pam_userinfotype(Host) of + username -> User; + jid -> <> + end, + case catch epam:authenticate(Service, UserInfo, Password) of + true -> {cache, true}; + false -> {cache, false}; + _ -> {nocache, false} + end end. -try_register(_User, _Server, _Password) -> - {error, not_allowed}. - -dirty_get_registered_users() -> []. - -get_vh_registered_users(_Host) -> []. - -get_vh_registered_users(_Host, _) -> []. - -get_vh_registered_users_number(_Host) -> 0. - -get_vh_registered_users_number(_Host, _) -> 0. - -get_password(_User, _Server) -> false. - -get_password_s(_User, _Server) -> <<"">>. - -%% @spec (User, Server) -> true | false | {error, Error} -%% TODO: Improve this function to return an error instead of 'false' when connection to PAM failed -is_user_exists(User, Host) -> +user_exists(User, Host) -> Service = get_pam_service(Host), UserInfo = case get_pam_userinfotype(Host) of username -> User; jid -> <> end, case catch epam:acct_mgmt(Service, UserInfo) of - true -> true; - _ -> false + true -> {cache, true}; + false -> {cache, false}; + _Err -> {nocache, {error, db_failure}} end. -remove_user(_User, _Server) -> {error, not_allowed}. +plain_password_required(_) -> true. -remove_user(_User, _Server, _Password) -> not_allowed. - -plain_password_required() -> true. - -store_type() -> external. +store_type(_) -> external. %%==================================================================== %% Internal functions %%==================================================================== get_pam_service(Host) -> - ejabberd_config:get_option( - {pam_service, Host}, - fun iolist_to_binary/1, - <<"ejabberd">>). + ejabberd_option:pam_service(Host). get_pam_userinfotype(Host) -> - ejabberd_config:get_option( - {pam_userinfotype, Host}, - fun(username) -> username; - (jid) -> jid - end, - username). + ejabberd_option:pam_userinfotype(Host). diff --git a/src/ejabberd_auth_riak.erl b/src/ejabberd_auth_riak.erl deleted file mode 100644 index 081ee6bb8..000000000 --- a/src/ejabberd_auth_riak.erl +++ /dev/null @@ -1,296 +0,0 @@ -%%%---------------------------------------------------------------------- -%%% File : ejabberd_auth_riak.erl -%%% Author : Evgeniy Khramtsov -%%% Purpose : Authentification via Riak -%%% Created : 12 Nov 2012 by Evgeniy Khramtsov -%%% -%%% -%%% ejabberd, Copyright (C) 2002-2015 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., 59 Temple Place, Suite 330, Boston, MA -%%% 02111-1307 USA -%%% -%%%---------------------------------------------------------------------- - --module(ejabberd_auth_riak). - --author('alexey@process-one.net'). - --behaviour(ejabberd_auth). - -%% External exports --export([start/1, set_password/3, check_password/3, - check_password/5, try_register/3, - dirty_get_registered_users/0, get_vh_registered_users/1, - get_vh_registered_users/2, - get_vh_registered_users_number/1, - get_vh_registered_users_number/2, get_password/2, - get_password_s/2, is_user_exists/2, remove_user/2, - remove_user/3, store_type/0, export/1, import/3, - plain_password_required/0]). --export([passwd_schema/0]). - --include("ejabberd.hrl"). - --record(passwd, {us = {<<"">>, <<"">>} :: {binary(), binary()} | '$1', - password = <<"">> :: binary() | scram() | '_'}). - --define(SALT_LENGTH, 16). - -start(_Host) -> - ok. - -plain_password_required() -> - case is_scrammed() of - false -> false; - true -> true - end. - -store_type() -> - case is_scrammed() of - false -> plain; %% allows: PLAIN DIGEST-MD5 SCRAM - true -> scram %% allows: PLAIN SCRAM - end. - -passwd_schema() -> - {record_info(fields, passwd), #passwd{}}. - -check_password(User, Server, Password) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - case ejabberd_riak:get(passwd, passwd_schema(), {LUser, LServer}) of - {ok, #passwd{password = Password}} when is_binary(Password) -> - Password /= <<"">>; - {ok, #passwd{password = Scram}} when is_record(Scram, scram) -> - is_password_scram_valid(Password, Scram); - _ -> - false - end. - -check_password(User, Server, Password, Digest, - DigestGen) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - case ejabberd_riak:get(passwd, passwd_schema(), {LUser, LServer}) of - {ok, #passwd{password = Passwd}} when is_binary(Passwd) -> - DigRes = if Digest /= <<"">> -> - Digest == DigestGen(Passwd); - true -> false - end, - if DigRes -> true; - true -> (Passwd == Password) and (Password /= <<"">>) - end; - {ok, #passwd{password = Scram}} - when is_record(Scram, scram) -> - Passwd = jlib:decode_base64(Scram#scram.storedkey), - DigRes = if Digest /= <<"">> -> - Digest == DigestGen(Passwd); - true -> false - end, - if DigRes -> true; - true -> (Passwd == Password) and (Password /= <<"">>) - end; - _ -> false - end. - -set_password(User, Server, Password) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - US = {LUser, LServer}, - if (LUser == error) or (LServer == error) -> - {error, invalid_jid}; - true -> - Password2 = case is_scrammed() and is_binary(Password) - of - true -> password_to_scram(Password); - false -> Password - end, - ok = ejabberd_riak:put(#passwd{us = US, password = Password2}, - passwd_schema(), - [{'2i', [{<<"host">>, LServer}]}]) - end. - -try_register(User, Server, PasswordList) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - Password = iolist_to_binary(PasswordList), - US = {LUser, LServer}, - if (LUser == error) or (LServer == error) -> - {error, invalid_jid}; - true -> - case ejabberd_riak:get(passwd, passwd_schema(), US) of - {error, notfound} -> - Password2 = case is_scrammed() and - is_binary(Password) - of - true -> password_to_scram(Password); - false -> Password - end, - {atomic, ejabberd_riak:put( - #passwd{us = US, - password = Password2}, - passwd_schema(), - [{'2i', [{<<"host">>, LServer}]}])}; - {ok, _} -> - exists; - Err -> - {atomic, Err} - end - end. - -dirty_get_registered_users() -> - lists:flatmap( - fun(Server) -> - get_vh_registered_users(Server) - end, ejabberd_config:get_vh_by_auth_method(riak)). - -get_vh_registered_users(Server) -> - LServer = jlib:nameprep(Server), - case ejabberd_riak:get_keys_by_index(passwd, <<"host">>, LServer) of - {ok, Users} -> - Users; - _ -> - [] - end. - -get_vh_registered_users(Server, _) -> - get_vh_registered_users(Server). - -get_vh_registered_users_number(Server) -> - LServer = jlib:nameprep(Server), - case ejabberd_riak:count_by_index(passwd, <<"host">>, LServer) of - {ok, N} -> - N; - _ -> - 0 - end. - -get_vh_registered_users_number(Server, _) -> - get_vh_registered_users_number(Server). - -get_password(User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - case ejabberd_riak:get(passwd, passwd_schema(), {LUser, LServer}) of - {ok, #passwd{password = Password}} - when is_binary(Password) -> - Password; - {ok, #passwd{password = Scram}} - when is_record(Scram, scram) -> - {jlib:decode_base64(Scram#scram.storedkey), - jlib:decode_base64(Scram#scram.serverkey), - jlib:decode_base64(Scram#scram.salt), - Scram#scram.iterationcount}; - _ -> false - end. - -get_password_s(User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - case ejabberd_riak:get(passwd, passwd_schema(), {LUser, LServer}) of - {ok, #passwd{password = Password}} - when is_binary(Password) -> - Password; - {ok, #passwd{password = Scram}} - when is_record(Scram, scram) -> - <<"">>; - _ -> <<"">> - end. - -is_user_exists(User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - case ejabberd_riak:get(passwd, passwd_schema(), {LUser, LServer}) of - {error, notfound} -> false; - {ok, _} -> true; - Err -> Err - end. - -remove_user(User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - ejabberd_riak:delete(passwd, {LUser, LServer}), - ok. - -remove_user(User, Server, Password) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - case ejabberd_riak:get(passwd, passwd_schema(), {LUser, LServer}) of - {ok, #passwd{password = Password}} - when is_binary(Password) -> - ejabberd_riak:delete(passwd, {LUser, LServer}), - ok; - {ok, #passwd{password = Scram}} - when is_record(Scram, scram) -> - case is_password_scram_valid(Password, Scram) of - true -> - ejabberd_riak:delete(passwd, {LUser, LServer}), - ok; - false -> not_allowed - end; - _ -> not_exists - end. - -%%% -%%% SCRAM -%%% - -is_scrammed() -> - scram == - ejabberd_config:get_local_option({auth_password_format, ?MYNAME}, - fun(V) -> V end). - -password_to_scram(Password) -> - password_to_scram(Password, - ?SCRAM_DEFAULT_ITERATION_COUNT). - -password_to_scram(Password, IterationCount) -> - Salt = crypto:rand_bytes(?SALT_LENGTH), - SaltedPassword = scram:salted_password(Password, Salt, - IterationCount), - StoredKey = - scram:stored_key(scram:client_key(SaltedPassword)), - ServerKey = scram:server_key(SaltedPassword), - #scram{storedkey = jlib:encode_base64(StoredKey), - serverkey = jlib:encode_base64(ServerKey), - salt = jlib:encode_base64(Salt), - iterationcount = IterationCount}. - -is_password_scram_valid(Password, Scram) -> - IterationCount = Scram#scram.iterationcount, - Salt = jlib:decode_base64(Scram#scram.salt), - SaltedPassword = scram:salted_password(Password, Salt, - IterationCount), - StoredKey = - scram:stored_key(scram:client_key(SaltedPassword)), - jlib:decode_base64(Scram#scram.storedkey) == StoredKey. - -export(_Server) -> - [{passwd, - fun(Host, #passwd{us = {LUser, LServer}, password = Password}) - when LServer == Host -> - Username = ejabberd_odbc:escape(LUser), - Pass = ejabberd_odbc:escape(Password), - [[<<"delete from users where username='">>, Username, <<"';">>], - [<<"insert into users(username, password) " - "values ('">>, Username, <<"', '">>, Pass, <<"');">>]]; - (_Host, _R) -> - [] - end}]. - -import(LServer, riak, #passwd{} = Passwd) -> - ejabberd_riak:put(Passwd, passwd_schema(), [{'2i', [{<<"host">>, LServer}]}]); -import(_, _, _) -> - pass. diff --git a/src/ejabberd_auth_sql.erl b/src/ejabberd_auth_sql.erl new file mode 100644 index 000000000..8ce78bc18 --- /dev/null +++ b/src/ejabberd_auth_sql.erl @@ -0,0 +1,446 @@ +%%%---------------------------------------------------------------------- +%%% File : ejabberd_auth_sql.erl +%%% Author : Alexey Shchepin +%%% Purpose : Authentication via ODBC +%%% Created : 12 Dec 2004 by Alexey Shchepin +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(ejabberd_auth_sql). + + +-author('alexey@process-one.net'). + +-behaviour(ejabberd_auth). + +-export([start/1, stop/1, set_password_multiple/3, try_register_multiple/3, + get_users/2, count_users/2, get_password/2, + remove_user/2, store_type/1, plain_password_required/1, + export/1, which_users_exists/2, drop_password_type/2, set_password_instance/3]). +-export([sql_schemas/0]). + +-include_lib("xmpp/include/scram.hrl"). +-include("logger.hrl"). +-include("ejabberd_sql_pt.hrl"). +-include("ejabberd_auth.hrl"). + +%%%---------------------------------------------------------------------- +%%% API +%%%---------------------------------------------------------------------- +start(Host) -> + ejabberd_sql_schema:update_schema(Host, ?MODULE, sql_schemas()), + ok. + +sql_schemas() -> + [ + #sql_schema{ + version = 2, + tables = + [#sql_table{ + name = <<"users">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"type">>, type = smallint}, + #sql_column{name = <<"password">>, type = text}, + #sql_column{name = <<"serverkey">>, type = {text, 128}, + default = true}, + #sql_column{name = <<"salt">>, type = {text, 128}, + default = true}, + #sql_column{name = <<"iterationcount">>, type = integer, + default = true}, + #sql_column{name = <<"created_at">>, type = timestamp, + default = true}], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"username">>, <<"type">>], + unique = true}]}], + update = [ + {add_column, <<"users">>, <<"type">>}, + {update_primary_key,<<"users">>, + [<<"server_host">>, <<"username">>, <<"type">>]} + ]}, + #sql_schema{ + version = 1, + tables = + [#sql_table{ + name = <<"users">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"password">>, type = text}, + #sql_column{name = <<"serverkey">>, type = {text, 128}, + default = true}, + #sql_column{name = <<"salt">>, type = {text, 128}, + default = true}, + #sql_column{name = <<"iterationcount">>, type = integer, + default = true}, + #sql_column{name = <<"created_at">>, type = timestamp, + default = true}], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"username">>], + unique = true}]}]}]. + +stop(_Host) -> ok. + +plain_password_required(Server) -> + store_type(Server) == scram. + +store_type(Server) -> + ejabberd_auth:password_format(Server). + +hash_to_num(plain) -> 1; +hash_to_num(sha) -> 2; +hash_to_num(sha256) -> 3; +hash_to_num(sha512) -> 4. + +num_to_hash(2) -> sha; +num_to_hash(3) -> sha256; +num_to_hash(4) -> sha512. + +set_password_instance(User, Server, #scram{hash = Hash, storedkey = SK, serverkey = SEK, + salt = Salt, iterationcount = IC}) -> + F = fun() -> + set_password_scram_t(User, Server, Hash, + SK, SEK, Salt, IC) + end, + case ejabberd_sql:sql_transaction(Server, F) of + {atomic, _} -> + ok; + {aborted, _} -> + {error, db_failure} + end; +set_password_instance(User, Server, Plain) -> + F = fun() -> + set_password_t(User, Server, Plain) + end, + case ejabberd_sql:sql_transaction(Server, F) of + {atomic, _} -> + ok; + {aborted, _} -> + {error, db_failure} + end. + +set_password_multiple(User, Server, Passwords) -> + F = + fun() -> + ejabberd_sql:sql_query_t( + ?SQL("delete from users where username=%(User)s and %(Server)H")), + lists:foreach( + fun(#scram{hash = Hash, storedkey = SK, serverkey = SEK, + salt = Salt, iterationcount = IC}) -> + set_password_scram_t( + User, Server, Hash, + SK, SEK, Salt, IC); + (Plain) -> + set_password_t(User, Server, Plain) + end, Passwords) + end, + case ejabberd_sql:sql_transaction(Server, F) of + {atomic, _} -> + {cache, {ok, Passwords}}; + {aborted, _} -> + {nocache, {error, db_failure}} + end. + +try_register_multiple(User, Server, Passwords) -> + F = + fun() -> + case ejabberd_sql:sql_query_t( + ?SQL("select @(count(*))d from users where username=%(User)s and %(Server)H")) of + {selected, [{0}]} -> + lists:foreach( + fun(#scram{hash = Hash, storedkey = SK, serverkey = SEK, + salt = Salt, iterationcount = IC}) -> + set_password_scram_t( + User, Server, Hash, + SK, SEK, Salt, IC); + (Plain) -> + set_password_t(User, Server, Plain) + end, Passwords), + {cache, {ok, Passwords}}; + {selected, _} -> + {nocache, {error, exists}}; + _ -> + {nocache, {error, db_failure}} + end + end, + case ejabberd_sql:sql_transaction(Server, F) of + {atomic, Res} -> + Res; + {aborted, _} -> + {nocache, {error, db_failure}} + end. + +get_users(Server, Opts) -> + case list_users(Server, Opts) of + {selected, Res} -> + [{U, Server} || {U} <- Res]; + _ -> [] + end. + +count_users(Server, Opts) -> + case users_number(Server, Opts) of + {selected, [{Res}]} -> + Res; + _Other -> 0 + end. + +get_password(User, Server) -> + case get_password_scram(Server, User) of + {selected, []} -> + {cache, error}; + {selected, Passwords} -> + Converted = lists:map( + fun({0, Password, <<>>, <<>>, 0}) -> + update_password_type(User, Server, 1), + Password; + ({_, Password, <<>>, <<>>, 0}) -> + Password; + ({0, StoredKey, ServerKey, Salt, IterationCount}) -> + {Hash, SK} = case StoredKey of + <<"sha256:", Rest/binary>> -> + update_password_type(User, Server, 3, Rest), + {sha256, Rest}; + <<"sha512:", Rest/binary>> -> + update_password_type(User, Server, 4, Rest), + {sha512, Rest}; + Other -> + update_password_type(User, Server, 2), + {sha, Other} + end, + #scram{storedkey = SK, + serverkey = ServerKey, + salt = Salt, + hash = Hash, + iterationcount = IterationCount}; + ({Type, StoredKey, ServerKey, Salt, IterationCount}) -> + Hash = num_to_hash(Type), + #scram{storedkey = StoredKey, + serverkey = ServerKey, + salt = Salt, + hash = Hash, + iterationcount = IterationCount} + end, Passwords), + {cache, {ok, Converted}}; + _ -> + {nocache, error} + end. + +remove_user(User, Server) -> + case del_user(Server, User) of + {updated, _} -> + ok; + _ -> + {error, db_failure} + end. + +drop_password_type(LServer, Hash) -> + Type = hash_to_num(Hash), + ejabberd_sql:sql_query( + LServer, + ?SQL("delete from users" + " where type=%(Type)d and %(LServer)H")). + +set_password_scram_t(LUser, LServer, Hash, + StoredKey, ServerKey, Salt, IterationCount) -> + Type = hash_to_num(Hash), + ?SQL_UPSERT_T( + "users", + ["!username=%(LUser)s", + "!server_host=%(LServer)s", + "!type=%(Type)d", + "password=%(StoredKey)s", + "serverkey=%(ServerKey)s", + "salt=%(Salt)s", + "iterationcount=%(IterationCount)d"]). + +set_password_t(LUser, LServer, Password) -> + ?SQL_UPSERT_T( + "users", + ["!username=%(LUser)s", + "!server_host=%(LServer)s", + "!type=1", + "password=%(Password)s", + "serverkey=''", + "salt=''", + "iterationcount=0"]). + +update_password_type(LUser, LServer, Type, Password) -> + ejabberd_sql:sql_query( + LServer, + ?SQL("update users set type=%(Type)d, password=%(Password)s" + " where username=%(LUser)s and type=0 and %(LServer)H")). + +update_password_type(LUser, LServer, Type) -> + ejabberd_sql:sql_query( + LServer, + ?SQL("update users set type=%(Type)d" + " where username=%(LUser)s and type=0 and %(LServer)H")). + +get_password_scram(LServer, LUser) -> + ejabberd_sql:sql_query( + LServer, + ?SQL("select @(type)d, @(password)s, @(serverkey)s, @(salt)s, @(iterationcount)d" + " from users" + " where username=%(LUser)s and %(LServer)H")). + +del_user(LServer, LUser) -> + ejabberd_sql:sql_query( + LServer, + ?SQL("delete from users where username=%(LUser)s and %(LServer)H")). + +list_users(LServer, []) -> + ejabberd_sql:sql_query( + LServer, + ?SQL("select @(distinct username)s from users where %(LServer)H")); +list_users(LServer, [{from, Start}, {to, End}]) + when is_integer(Start) and is_integer(End) -> + list_users(LServer, + [{limit, End - Start + 1}, {offset, Start - 1}]); +list_users(LServer, + [{prefix, Prefix}, {from, Start}, {to, End}]) + when is_binary(Prefix) and is_integer(Start) and + is_integer(End) -> + list_users(LServer, + [{prefix, Prefix}, {limit, End - Start + 1}, + {offset, Start - 1}]); +list_users(LServer, [{limit, Limit}, {offset, Offset}]) + when is_integer(Limit) and is_integer(Offset) -> + ejabberd_sql:sql_query( + LServer, + ?SQL("select @(distinct username)s from users " + "where %(LServer)H " + "order by username " + "limit %(Limit)d offset %(Offset)d")); +list_users(LServer, + [{prefix, Prefix}, {limit, Limit}, {offset, Offset}]) + when is_binary(Prefix) and is_integer(Limit) and + is_integer(Offset) -> + SPrefix = ejabberd_sql:escape_like_arg(Prefix), + SPrefix2 = <>, + ejabberd_sql:sql_query( + LServer, + ?SQL("select @(distinct username)s from users " + "where username like %(SPrefix2)s %ESCAPE and %(LServer)H " + "order by username " + "limit %(Limit)d offset %(Offset)d")). + +users_number(LServer) -> + ejabberd_sql:sql_query( + LServer, + fun(pgsql, _) -> + case + ejabberd_option:pgsql_users_number_estimate(LServer) of + true -> + ejabberd_sql:sql_query_t( + ?SQL("select @(reltuples :: bigint)d from pg_class" + " where oid = 'users'::regclass::oid")); + _ -> + ejabberd_sql:sql_query_t( + ?SQL("select @(count(distinct username))d from users where %(LServer)H")) + end; + (_Type, _) -> + ejabberd_sql:sql_query_t( + ?SQL("select @(count(distinct username))d from users where %(LServer)H")) + end). + +users_number(LServer, [{prefix, Prefix}]) + when is_binary(Prefix) -> + SPrefix = ejabberd_sql:escape_like_arg(Prefix), + SPrefix2 = <>, + ejabberd_sql:sql_query( + LServer, + ?SQL("select @(count(distinct username))d from users " + "where username like %(SPrefix2)s %ESCAPE and %(LServer)H")); +users_number(LServer, []) -> + users_number(LServer). + +which_users_exists(LServer, LUsers) when length(LUsers) =< 100 -> + try ejabberd_sql:sql_query( + LServer, + ?SQL("select @(distinct username)s from users where username in %(LUsers)ls")) of + {selected, Matching} -> + [U || {U} <- Matching]; + {error, _} = E -> + E + catch _:B -> + {error, B} + end; +which_users_exists(LServer, LUsers) -> + {First, Rest} = lists:split(100, LUsers), + case which_users_exists(LServer, First) of + {error, _} = E -> + E; + V -> + case which_users_exists(LServer, Rest) of + {error, _} = E2 -> + E2; + V2 -> + V ++ V2 + end + end. + +export(_Server) -> + [{passwd, + fun(Host, #passwd{us = {LUser, LServer, plain}, password = Password}) + when LServer == Host, + is_binary(Password) -> + [?SQL("delete from users where username=%(LUser)s and type=1 and %(LServer)H;"), + ?SQL_INSERT( + "users", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "type=1", + "password=%(Password)s"])]; + (Host, {passwd, {LUser, LServer, _}, + {scram, StoredKey, ServerKey, Salt, IterationCount}}) + when LServer == Host -> + Hash = sha, + Type = hash_to_num(Hash), + [?SQL("delete from users where username=%(LUser)s and type=%(Type)d and %(LServer)H;"), + ?SQL_INSERT( + "users", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "type=%(Type)d", + "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#scram.storedkey, + ServerKey = Scram#scram.serverkey, + Salt = Scram#scram.salt, + IterationCount = Scram#scram.iterationcount, + Type = hash_to_num(Scram#scram.hash), + [?SQL("delete from users where username=%(LUser)s and type=%(Type)d and %(LServer)H;"), + ?SQL_INSERT( + "users", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "type=%(Type)d", + "password=%(StoredKey)s", + "serverkey=%(ServerKey)s", + "salt=%(Salt)s", + "iterationcount=%(IterationCount)d"])]; + (_Host, _R) -> + [] + end}]. diff --git a/src/ejabberd_backend_sup.erl b/src/ejabberd_backend_sup.erl new file mode 100644 index 000000000..1b3495e36 --- /dev/null +++ b/src/ejabberd_backend_sup.erl @@ -0,0 +1,46 @@ +%%%------------------------------------------------------------------- +%%% Created : 24 Feb 2017 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(ejabberd_backend_sup). + +-behaviour(supervisor). + +%% API +-export([start_link/0]). + +%% Supervisor callbacks +-export([init/1]). + +%%%=================================================================== +%%% API functions +%%%=================================================================== +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +%%%=================================================================== +%%% Supervisor callbacks +%%%=================================================================== +init([]) -> + {ok, {{one_for_one, 10, 1}, []}}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== diff --git a/src/ejabberd_batch.erl b/src/ejabberd_batch.erl new file mode 100644 index 000000000..5a907c74b --- /dev/null +++ b/src/ejabberd_batch.erl @@ -0,0 +1,205 @@ +%%%---------------------------------------------------------------------- +%%% File : ejabberd_batch.erl +%%% Author : Paweł Chmielowski +%%% Purpose : Batch tasks manager +%%% Created : 8 mar 2022 by Paweł Chmielowski +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(ejabberd_batch). +-author("pawel@process-one.net"). + +-behaviour(gen_server). + +-include("logger.hrl"). + +%% API +-export([start_link/0]). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, + code_change/3]). +-export([register_task/5, task_status/1, abort_task/1]). + +-define(SERVER, ?MODULE). + +-record(state, {tasks = #{}}). +-record(task, {state = not_started, pid, steps, done_steps}). + +%%%=================================================================== +%%% API +%%%=================================================================== + +%% @doc Spawns the server and registers the local name (unique) +-spec(start_link() -> + {ok, Pid :: pid()} | ignore | {error, Reason :: term()}). +start_link() -> + gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). + +register_task(Type, Steps, Rate, JobState, JobFun) -> + gen_server:call(?MODULE, {register_task, Type, Steps, Rate, JobState, JobFun}). + +task_status(Type) -> + gen_server:call(?MODULE, {task_status, Type}). + +abort_task(Type) -> + gen_server:call(?MODULE, {abort_task, Type}). + +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== + +%% @private +%% @doc Initializes the server +-spec(init(Args :: term()) -> + {ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} | + {stop, Reason :: term()} | ignore). +init([]) -> + {ok, #state{}}. + +%% @private +%% @doc Handling call messages +-spec(handle_call(Request :: term(), From :: {pid(), Tag :: term()}, + State :: #state{}) -> + {reply, Reply :: term(), NewState :: #state{}} | + {reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} | + {noreply, NewState :: #state{}} | + {noreply, NewState :: #state{}, timeout() | hibernate} | + {stop, Reason :: term(), Reply :: term(), NewState :: #state{}} | + {stop, Reason :: term(), NewState :: #state{}}). +handle_call({register_task, Type, Steps, Rate, JobState, JobFun}, _From, #state{tasks = Tasks} = State) -> + case maps:get(Type, Tasks, #task{}) of + #task{state = S} when S == completed; S == not_started; S == aborted; S == failed -> + Pid = spawn(fun() -> work_loop(Type, JobState, JobFun, Rate, erlang:monotonic_time(second), 0) end), + Tasks2 = maps:put(Type, #task{state = working, pid = Pid, steps = Steps, done_steps = 0}, Tasks), + {reply, ok, #state{tasks = Tasks2}}; + #task{state = working} -> + {reply, {error, in_progress}, State} + end; +handle_call({task_status, Type}, _From, #state{tasks = Tasks} = State) -> + case maps:get(Type, Tasks, none) of + none -> + {reply, not_started, State}; + #task{state = not_started} -> + {reply, not_started, State}; + #task{state = failed, done_steps = Steps, pid = Error} -> + {reply, {failed, Steps, Error}, State}; + #task{state = aborted, done_steps = Steps} -> + {reply, {aborted, Steps}, State}; + #task{state = working, done_steps = Steps} -> + {reply, {working, Steps}, State}; + #task{state = completed, done_steps = Steps} -> + {reply, {completed, Steps}, State} + end; +handle_call({abort_task, Type}, _From, #state{tasks = Tasks} = State) -> + case maps:get(Type, Tasks, none) of + #task{state = working, pid = Pid} = T -> + Pid ! abort, + Tasks2 = maps:put(Type, T#task{state = aborted, pid = none}, Tasks), + {reply, aborted, State#state{tasks = Tasks2}}; + _ -> + {reply, not_started, State} + end; +handle_call(_Request, _From, State = #state{}) -> + {reply, ok, State}. + +%% @private +%% @doc Handling cast messages +-spec(handle_cast(Request :: term(), State :: #state{}) -> + {noreply, NewState :: #state{}} | + {noreply, NewState :: #state{}, timeout() | hibernate} | + {stop, Reason :: term(), NewState :: #state{}}). +handle_cast({task_finished, Type, Pid}, #state{tasks = Tasks} = State) -> + case maps:get(Type, Tasks, none) of + #task{state = working, pid = Pid2} = T when Pid == Pid2 -> + Tasks2 = maps:put(Type, T#task{state = completed, pid = none}, Tasks), + {noreply, State#state{tasks = Tasks2}}; + _ -> + {noreply, State} + end; +handle_cast({task_progress, Type, Pid, Count}, #state{tasks = Tasks} = State) -> + case maps:get(Type, Tasks, none) of + #task{state = working, pid = Pid2, done_steps = Steps} = T when Pid == Pid2 -> + Tasks2 = maps:put(Type, T#task{done_steps = Steps + Count}, Tasks), + {noreply, State#state{tasks = Tasks2}}; + _ -> + {noreply, State} + end; +handle_cast({task_error, Type, Pid, Error}, #state{tasks = Tasks} = State) -> + case maps:get(Type, Tasks, none) of + #task{state = working, pid = Pid2} = T when Pid == Pid2 -> + Tasks2 = maps:put(Type, T#task{state = failed, pid = Error}, Tasks), + {noreply, State#state{tasks = Tasks2}}; + _ -> + {noreply, State} + end; +handle_cast(_Request, State = #state{}) -> + {noreply, State}. + +%% @private +%% @doc Handling all non call/cast messages +-spec(handle_info(Info :: timeout() | term(), State :: #state{}) -> + {noreply, NewState :: #state{}} | + {noreply, NewState :: #state{}, timeout() | hibernate} | + {stop, Reason :: term(), NewState :: #state{}}). +handle_info(_Info, State = #state{}) -> + {noreply, State}. + +%% @private +%% @doc This function is called by a gen_server when it is about to +%% terminate. It should be the opposite of Module:init/1 and do any +%% necessary cleaning up. When it returns, the gen_server terminates +%% with Reason. The return value is ignored. +-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()), + State :: #state{}) -> term()). +terminate(_Reason, _State = #state{}) -> + ok. + +%% @private +%% @doc Convert process state when code is changed +-spec(code_change(OldVsn :: term() | {down, term()}, State :: #state{}, + Extra :: term()) -> + {ok, NewState :: #state{}} | {error, Reason :: term()}). +code_change(_OldVsn, State = #state{}, _Extra) -> + {ok, State}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== + +work_loop(Task, JobState, JobFun, Rate, StartDate, CurrentProgress) -> + try JobFun(JobState) of + {ok, _NewState, 0} -> + gen_server:cast(?MODULE, {task_finished, Task, self()}); + {ok, NewState, Count} -> + gen_server:cast(?MODULE, {task_progress, Task, self(), Count}), + NewProgress = CurrentProgress + Count, + TimeSpent = erlang:monotonic_time(second) - StartDate, + SleepTime = max(0, NewProgress/Rate*60 - TimeSpent), + receive + abort -> ok + after round(SleepTime*1000) -> + work_loop(Task, NewState, JobFun, Rate, StartDate, NewProgress) + end; + {error, Error} -> + gen_server:cast(?MODULE, {task_error, Task, self(), Error}) + catch _:_ -> + gen_server:cast(?MODULE, {task_error, Task, self(), internal_error}) + end. diff --git a/src/ejabberd_bosh.erl b/src/ejabberd_bosh.erl new file mode 100644 index 000000000..8d1dbd595 --- /dev/null +++ b/src/ejabberd_bosh.erl @@ -0,0 +1,1052 @@ +%%%------------------------------------------------------------------- +%%% File : ejabberd_bosh.erl +%%% Author : Evgeniy Khramtsov +%%% Purpose : Manage BOSH sockets +%%% Created : 20 Jul 2011 by Evgeniy Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(ejabberd_bosh). +-behaviour(xmpp_socket). +-behaviour(p1_fsm). +-protocol({xep, 124, '1.11', '16.12', "complete", ""}). +-protocol({xep, 206, '1.4', '16.12', "complete", ""}). + +%% API +-export([start/2, start/3, start_link/3]). + +-export([send_xml/2, setopts/2, controlling_process/2, + reset_stream/1, change_shaper/2, close/1, + sockname/1, peername/1, process_request/3, send/2, + get_transport/1, get_owner/1]). + +%% gen_fsm callbacks +-export([init/1, wait_for_session/2, wait_for_session/3, + active/2, active/3, handle_event/3, print_state/1, + handle_sync_event/4, handle_info/3, terminate/3, + code_change/4]). + +-include("logger.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). +-include("ejabberd_http.hrl"). +-include("bosh.hrl"). + +%%-define(DBGFSM, true). +-ifdef(DBGFSM). + +-define(FSMOPTS, [{debug, [trace]}]). + +-else. + +-define(FSMOPTS, []). + +-endif. + +-define(BOSH_VERSION, <<"1.11">>). + +-define(NS_BOSH, <<"urn:xmpp:xbosh">>). + +-define(NS_HTTP_BIND, + <<"http://jabber.org/protocol/httpbind">>). + +-define(DEFAULT_WAIT, 300). + +-define(DEFAULT_HOLD, 1). + +-define(DEFAULT_POLLING, 2). + +-define(MAX_SHAPED_REQUESTS_QUEUE_LEN, 1000). + +-define(SEND_TIMEOUT, 15000). + +-type bosh_socket() :: {http_bind, pid(), + {inet:ip_address(), + inet:port_number()}}. + +-export_type([bosh_socket/0]). + +-record(state, + {host = <<"">> :: binary(), + sid = <<"">> :: binary(), + el_ibuf :: p1_queue:queue(), + el_obuf :: p1_queue:queue(), + shaper_state = none :: ejabberd_shaper:shaper(), + c2s_pid :: pid() | undefined, + xmpp_ver = <<"">> :: binary(), + inactivity_timer :: reference() | undefined, + wait_timer :: reference() | undefined, + wait_timeout = ?DEFAULT_WAIT :: pos_integer(), + inactivity_timeout :: pos_integer(), + prev_rid = 0 :: non_neg_integer(), + prev_key = <<"">> :: binary(), + prev_poll :: erlang:timestamp() | undefined, + max_concat = unlimited :: unlimited | non_neg_integer(), + responses = gb_trees:empty() :: gb_trees:tree(), + receivers = gb_trees:empty() :: gb_trees:tree(), + shaped_receivers :: p1_queue:queue(), + ip :: inet:ip_address(), + max_requests = 1 :: non_neg_integer()}). + +-record(body, + {http_reason = <<"">> :: binary(), + attrs = [] :: [{any(), any()}], + els = [] :: [fxml_stream:xml_stream_el()], + size = 0 :: non_neg_integer()}). + +start(#body{attrs = Attrs} = Body, IP, SID) -> + XMPPDomain = get_attr(to, Attrs), + SupervisorProc = gen_mod:get_module_proc(XMPPDomain, mod_bosh), + case catch supervisor:start_child(SupervisorProc, + [Body, IP, SID]) + of + {ok, Pid} -> {ok, Pid}; + {'EXIT', {noproc, _}} -> + check_bosh_module(XMPPDomain), + {error, module_not_loaded}; + Err -> + ?ERROR_MSG("Failed to start BOSH session: ~p", [Err]), + {error, Err} + end. + +start(StateName, State) -> + p1_fsm:start_link(?MODULE, [StateName, State], + ?FSMOPTS). + +start_link(Body, IP, SID) -> + p1_fsm:start_link(?MODULE, [Body, IP, SID], + ?FSMOPTS). + +send({http_bind, FsmRef, IP}, Packet) -> + send_xml({http_bind, FsmRef, IP}, Packet). + +send_xml({http_bind, FsmRef, _IP}, Packet) -> + case catch p1_fsm:sync_send_all_state_event(FsmRef, + {send_xml, Packet}, + ?SEND_TIMEOUT) + of + {'EXIT', {timeout, _}} -> {error, timeout}; + {'EXIT', _} -> {error, einval}; + Res -> Res + end. + +setopts({http_bind, FsmRef, _IP}, Opts) -> + case lists:member({active, once}, Opts) of + true -> + p1_fsm:send_all_state_event(FsmRef, + {activate, self()}); + _ -> + case lists:member({active, false}, Opts) of + true -> + case catch p1_fsm:sync_send_all_state_event(FsmRef, + deactivate_socket) + of + {'EXIT', _} -> {error, einval}; + Res -> Res + end; + _ -> ok + end + end. + +controlling_process(_Socket, _Pid) -> ok. + +reset_stream({http_bind, _FsmRef, _IP} = Socket) -> + Socket. + +change_shaper({http_bind, FsmRef, _IP}, Shaper) -> + p1_fsm:send_all_state_event(FsmRef, {change_shaper, Shaper}). + +close({http_bind, FsmRef, _IP}) -> + catch p1_fsm:sync_send_all_state_event(FsmRef, + close). + +sockname(_Socket) -> {ok, {{0, 0, 0, 0}, 0}}. + +peername({http_bind, _FsmRef, IP}) -> {ok, IP}. + +get_transport(_Socket) -> + http_bind. + +get_owner({http_bind, FsmRef, _IP}) -> + FsmRef. + +process_request(Data, IP, Type) -> + Opts1 = ejabberd_c2s_config:get_c2s_limits(), + Opts = case Type of + xml -> + [{xml_socket, true} | Opts1]; + json -> + Opts1 + end, + MaxStanzaSize = case lists:keysearch(max_stanza_size, 1, + Opts) + of + {value, {_, Size}} -> Size; + _ -> infinity + end, + PayloadSize = iolist_size(Data), + if PayloadSize > MaxStanzaSize -> + http_error(403, <<"Request Too Large">>, Type); + true -> + case decode_body(Data, PayloadSize, Type) of + {ok, #body{attrs = Attrs} = Body} -> + SID = get_attr(sid, Attrs), + To = get_attr(to, Attrs), + if SID == <<"">>, To == <<"">> -> + bosh_response_with_msg(#body{http_reason = + <<"Missing 'to' attribute">>, + attrs = + [{type, <<"terminate">>}, + {condition, + <<"improper-addressing">>}]}, + Type, Body); + SID == <<"">> -> + case start(Body, IP, make_sid()) of + {ok, Pid} -> process_request(Pid, Body, IP, Type); + _Err -> + bosh_response_with_msg(#body{http_reason = + <<"Failed to start BOSH session">>, + attrs = + [{type, <<"terminate">>}, + {condition, + <<"internal-server-error">>}]}, + Type, Body) + end; + true -> + case mod_bosh:find_session(SID) of + {ok, Pid} -> process_request(Pid, Body, IP, Type); + error -> + bosh_response_with_msg(#body{http_reason = + <<"Session ID mismatch">>, + attrs = + [{type, <<"terminate">>}, + {condition, + <<"item-not-found">>}]}, + Type, Body) + end + end; + {error, Reason} -> http_error(400, Reason, Type) + end + end. + +process_request(Pid, Req, _IP, Type) -> + case catch p1_fsm:sync_send_event(Pid, Req, + infinity) + of + #body{} = Resp -> bosh_response(Resp, Type); + {'EXIT', {Reason, _}} + when Reason == noproc; Reason == normal -> + bosh_response(#body{http_reason = + <<"BOSH session not found">>, + attrs = + [{type, <<"terminate">>}, + {condition, <<"item-not-found">>}]}, + Type); + {'EXIT', _} -> + bosh_response(#body{http_reason = + <<"Unexpected error">>, + attrs = + [{type, <<"terminate">>}, + {condition, <<"internal-server-error">>}]}, + Type) + end. + +init([#body{attrs = Attrs}, IP, SID]) -> + Opts1 = ejabberd_c2s_config:get_c2s_limits(), + Opts2 = [{xml_socket, true} | Opts1], + Shaper = none, + ShaperState = ejabberd_shaper:new(Shaper), + Socket = make_socket(self(), IP), + XMPPVer = get_attr('xmpp:version', Attrs), + XMPPDomain = get_attr(to, Attrs), + {InBuf, Opts} = case mod_bosh_opt:prebind(XMPPDomain) of + true -> + JID = make_random_jid(XMPPDomain), + {buf_new(XMPPDomain), [{jid, JID} | Opts2]}; + false -> + {buf_in([make_xmlstreamstart(XMPPDomain, XMPPVer)], + buf_new(XMPPDomain)), + Opts2} + end, + case ejabberd_c2s:start(?MODULE, Socket, [{receiver, self()}|Opts]) of + {ok, C2SPid} -> + ejabberd_c2s:accept(C2SPid), + Inactivity = mod_bosh_opt:max_inactivity(XMPPDomain) div 1000, + MaxConcat = mod_bosh_opt:max_concat(XMPPDomain), + ShapedReceivers = buf_new(XMPPDomain, ?MAX_SHAPED_REQUESTS_QUEUE_LEN), + State = #state{host = XMPPDomain, sid = SID, ip = IP, + xmpp_ver = XMPPVer, el_ibuf = InBuf, + max_concat = MaxConcat, el_obuf = buf_new(XMPPDomain), + inactivity_timeout = Inactivity, + shaped_receivers = ShapedReceivers, + shaper_state = ShaperState}, + NewState = restart_inactivity_timer(State), + case mod_bosh:open_session(SID, self()) of + ok -> + {ok, wait_for_session, NewState}; + {error, Reason} -> + {stop, Reason} + end; + {error, Reason} -> + {stop, Reason}; + ignore -> + ignore + end. + +wait_for_session(Event, State) -> + ?ERROR_MSG("Unexpected event in 'wait_for_session': ~p", + [Event]), + {next_state, wait_for_session, State}. + +wait_for_session(#body{attrs = Attrs} = Req, From, + State) -> + RID = get_attr(rid, Attrs), + ?DEBUG("Got request:~n** RequestID: ~p~n** Request: " + "~p~n** From: ~p~n** State: ~p", + [RID, Req, From, State]), + Wait = min(get_attr(wait, Attrs, undefined), + ?DEFAULT_WAIT), + Hold = min(get_attr(hold, Attrs, undefined), + ?DEFAULT_HOLD), + NewKey = get_attr(newkey, Attrs), + Type = get_attr(type, Attrs), + Requests = Hold + 1, + PollTime = if + Wait == 0, Hold == 0 -> erlang:timestamp(); + true -> undefined + end, + MaxPause = mod_bosh_opt:max_pause(State#state.host) div 1000, + Resp = #body{attrs = + [{sid, State#state.sid}, {wait, Wait}, + {ver, ?BOSH_VERSION}, {polling, ?DEFAULT_POLLING}, + {inactivity, State#state.inactivity_timeout}, + {hold, Hold}, {'xmpp:restartlogic', true}, + {requests, Requests}, {secure, true}, + {maxpause, MaxPause}, {'xmlns:xmpp', ?NS_BOSH}, + {'xmlns:stream', ?NS_STREAM}, {from, State#state.host}]}, + {ShaperState, _} = + ejabberd_shaper:update(State#state.shaper_state, Req#body.size), + State1 = State#state{wait_timeout = Wait, + prev_rid = RID, prev_key = NewKey, + prev_poll = PollTime, shaper_state = ShaperState, + max_requests = Requests}, + Els = maybe_add_xmlstreamend(Req#body.els, Type), + State2 = route_els(State1, Els), + {State3, RespEls} = get_response_els(State2), + State4 = stop_inactivity_timer(State3), + case RespEls of + [{xmlstreamstart, _, _} = El1] -> + OutBuf = buf_in([El1], State4#state.el_obuf), + State5 = restart_wait_timer(State4), + Receivers = gb_trees:insert(RID, {From, Resp}, + State5#state.receivers), + {next_state, active, + State5#state{receivers = Receivers, el_obuf = OutBuf}}; + [] -> + State5 = restart_wait_timer(State4), + Receivers = gb_trees:insert(RID, {From, Resp}, + State5#state.receivers), + {next_state, active, + State5#state{receivers = Receivers}}; + _ -> + reply_next_state(State4, Resp#body{els = RespEls}, RID, + From) + end; +wait_for_session(Event, _From, State) -> + ?ERROR_MSG("Unexpected sync event in 'wait_for_session': ~p", + [Event]), + {reply, {error, badarg}, wait_for_session, State}. + +active({#body{} = Body, From}, State) -> + active1(Body, From, State); +active(Event, State) -> + ?ERROR_MSG("Unexpected event in 'active': ~p", + [Event]), + {next_state, active, State}. + +active(#body{attrs = Attrs, size = Size} = Req, From, + State) -> + ?DEBUG("Got request:~n** Request: ~p~n** From: " + "~p~n** State: ~p", + [Req, From, State]), + {ShaperState, Pause} = + ejabberd_shaper:update(State#state.shaper_state, Size), + State1 = State#state{shaper_state = ShaperState}, + if Pause > 0 -> + TRef = start_shaper_timer(Pause), + try p1_queue:in({TRef, From, Req}, + State1#state.shaped_receivers) of + Q -> + State2 = stop_inactivity_timer(State1), + {next_state, active, + State2#state{shaped_receivers = Q}} + catch error:full -> + misc:cancel_timer(TRef), + RID = get_attr(rid, Attrs), + reply_stop(State1, + #body{http_reason = <<"Too many requests">>, + attrs = + [{<<"type">>, <<"terminate">>}, + {<<"condition">>, + <<"policy-violation">>}]}, + From, RID) + end; + true -> active1(Req, From, State1) + end; +active(Event, _From, State) -> + ?ERROR_MSG("Unexpected sync event in 'active': ~p", + [Event]), + {reply, {error, badarg}, active, State}. + +active1(#body{attrs = Attrs} = Req, From, State) -> + RID = get_attr(rid, Attrs), + Key = get_attr(key, Attrs), + IsValidKey = is_valid_key(State#state.prev_key, Key), + IsOveractivity = is_overactivity(State#state.prev_poll), + Type = get_attr(type, Attrs), + if RID > + State#state.prev_rid + State#state.max_requests -> + reply_stop(State, + #body{http_reason = <<"Request ID is out of range">>, + attrs = + [{<<"type">>, <<"terminate">>}, + {<<"condition">>, <<"item-not-found">>}]}, + From, RID); + RID > State#state.prev_rid + 1 -> + State1 = restart_inactivity_timer(State), + Receivers = gb_trees:insert(RID, {From, Req}, + State1#state.receivers), + {next_state, active, + State1#state{receivers = Receivers}}; + RID =< State#state.prev_rid -> + %% TODO: do we need to check 'key' here? It seems so... + case gb_trees:lookup(RID, State#state.responses) of + {value, PrevBody} -> + {next_state, active, + do_reply(State, From, PrevBody, RID)}; + none -> + State1 = drop_holding_receiver(State, RID), + State2 = stop_inactivity_timer(State1), + State3 = restart_wait_timer(State2), + Receivers = gb_trees:insert(RID, {From, Req}, + State3#state.receivers), + {next_state, active, State3#state{receivers = Receivers}} + end; + not IsValidKey -> + reply_stop(State, + #body{http_reason = <<"Session key mismatch">>, + attrs = + [{<<"type">>, <<"terminate">>}, + {<<"condition">>, <<"item-not-found">>}]}, + From, RID); + IsOveractivity -> + reply_stop(State, + #body{http_reason = <<"Too many requests">>, + attrs = + [{<<"type">>, <<"terminate">>}, + {<<"condition">>, <<"policy-violation">>}]}, + From, RID); + true -> + State1 = stop_inactivity_timer(State), + State2 = stop_wait_timer(State1), + Els = case get_attr('xmpp:restart', Attrs, false) of + true -> + XMPPDomain = get_attr(to, Attrs, State#state.host), + XMPPVer = get_attr('xmpp:version', Attrs, + State#state.xmpp_ver), + [make_xmlstreamstart(XMPPDomain, XMPPVer)]; + false -> Req#body.els + end, + State3 = route_els(State2, + maybe_add_xmlstreamend(Els, Type)), + {State4, RespEls} = get_response_els(State3), + NewKey = get_attr(newkey, Attrs, Key), + Pause = get_attr(pause, Attrs, undefined), + NewPoll = case State#state.prev_poll of + undefined -> undefined; + _ -> erlang:timestamp() + end, + State5 = State4#state{prev_poll = NewPoll, + prev_key = NewKey}, + if Type == <<"terminate">> -> + reply_stop(State5, + #body{http_reason = <<"Session close">>, + attrs = [{<<"type">>, <<"terminate">>}], + els = RespEls}, + From, RID); + Pause /= undefined -> + State6 = drop_holding_receiver(State5), + State7 = restart_inactivity_timer(State6, Pause), + InBuf = buf_in(RespEls, State7#state.el_ibuf), + {next_state, active, + State7#state{prev_rid = RID, el_ibuf = InBuf}}; + RespEls == [] -> + State6 = drop_holding_receiver(State5), + State7 = stop_inactivity_timer(State6), + State8 = restart_wait_timer(State7), + Receivers = gb_trees:insert(RID, {From, #body{}}, + State8#state.receivers), + {next_state, active, + State8#state{prev_rid = RID, receivers = Receivers}}; + true -> + State6 = drop_holding_receiver(State5), + reply_next_state(State6#state{prev_rid = RID}, + #body{els = RespEls}, RID, From) + end + end. + +handle_event({activate, C2SPid}, StateName, + State) -> + State1 = route_els(State#state{c2s_pid = C2SPid}), + {next_state, StateName, State1}; +handle_event({change_shaper, Shaper}, StateName, + State) -> + {next_state, StateName, State#state{shaper_state = Shaper}}; +handle_event(Event, StateName, State) -> + ?ERROR_MSG("Unexpected event in '~ts': ~p", + [StateName, Event]), + {next_state, StateName, State}. + +handle_sync_event({send_xml, + {xmlstreamstart, _, _} = El}, + _From, StateName, State) + when State#state.xmpp_ver >= <<"1.0">> -> + OutBuf = buf_in([El], State#state.el_obuf), + {reply, ok, StateName, State#state{el_obuf = OutBuf}}; +handle_sync_event({send_xml, El}, _From, StateName, + State) -> + OutBuf = buf_in([El], State#state.el_obuf), + State1 = State#state{el_obuf = OutBuf}, + case gb_trees:lookup(State1#state.prev_rid, + State1#state.receivers) + of + {value, {From, Body}} -> + {State2, Els} = get_response_els(State1), + {reply, ok, StateName, + reply(State2, Body#body{els = Els}, + State2#state.prev_rid, From)}; + none -> + State2 = case p1_queue:out(State1#state.shaped_receivers) + of + {{value, {TRef, From, Body}}, Q} -> + misc:cancel_timer(TRef), + p1_fsm:send_event(self(), {Body, From}), + State1#state{shaped_receivers = Q}; + _ -> State1 + end, + {reply, ok, StateName, State2} + end; +handle_sync_event(close, _From, _StateName, State) -> + {stop, normal, State}; +handle_sync_event(deactivate_socket, _From, StateName, + StateData) -> + {reply, ok, StateName, + StateData#state{c2s_pid = undefined}}; +handle_sync_event(Event, _From, StateName, State) -> + ?ERROR_MSG("Unexpected sync event in '~ts': ~p", + [StateName, Event]), + {reply, {error, badarg}, StateName, State}. + +handle_info({timeout, TRef, wait_timeout}, StateName, + #state{wait_timer = TRef} = State) -> + State2 = State#state{wait_timer = undefined}, + {next_state, StateName, drop_holding_receiver(State2)}; +handle_info({timeout, TRef, inactive}, _StateName, + #state{inactivity_timer = TRef} = State) -> + {stop, normal, State}; +handle_info({timeout, TRef, shaper_timeout}, StateName, + State) -> + case p1_queue:out(State#state.shaped_receivers) of + {{value, {TRef, From, Req}}, Q} -> + p1_fsm:send_event(self(), {Req, From}), + {next_state, StateName, + State#state{shaped_receivers = Q}}; + {{value, _}, _} -> + ?ERROR_MSG("shaper_timeout mismatch:~n** TRef: ~p~n** " + "State: ~p", + [TRef, State]), + {stop, normal, State}; + _ -> {next_state, StateName, State} + end; +handle_info(Info, StateName, State) -> + ?ERROR_MSG("Unexpected info:~n** Msg: ~p~n** StateName: ~p", + [Info, StateName]), + {next_state, StateName, State}. + +terminate(_Reason, _StateName, State) -> + mod_bosh:close_session(State#state.sid), + case State#state.c2s_pid of + C2SPid when is_pid(C2SPid) -> + p1_fsm:send_event(C2SPid, closed); + _ -> ok + end, + bounce_receivers(State, closed), + bounce_els_from_obuf(State). + +code_change(_OldVsn, StateName, State, _Extra) -> + {ok, StateName, State}. + +print_state(State) -> State. + +route_els(#state{el_ibuf = Buf, c2s_pid = C2SPid} = State) -> + NewBuf = p1_queue:dropwhile( + fun(El) -> + p1_fsm:send_event(C2SPid, El), + true + end, Buf), + State#state{el_ibuf = NewBuf}. + +route_els(State, Els) -> + case State#state.c2s_pid of + C2SPid when is_pid(C2SPid) -> + lists:foreach(fun (El) -> + p1_fsm:send_event(C2SPid, El) + end, + Els), + State; + _ -> + InBuf = buf_in(Els, State#state.el_ibuf), + State#state{el_ibuf = InBuf} + end. + +get_response_els(#state{el_obuf = OutBuf, + max_concat = MaxConcat} = + State) -> + {Els, NewOutBuf} = buf_out(OutBuf, MaxConcat), + {State#state{el_obuf = NewOutBuf}, Els}. + +reply(State, Body, RID, From) -> + State1 = restart_inactivity_timer(State), + Receivers = gb_trees:delete_any(RID, + State1#state.receivers), + State2 = do_reply(State1, From, Body, RID), + case catch gb_trees:take_smallest(Receivers) of + {NextRID, {From1, Req}, Receivers1} + when NextRID == RID + 1 -> + p1_fsm:send_event(self(), {Req, From1}), + State2#state{receivers = Receivers1}; + _ -> State2#state{receivers = Receivers} + end. + +reply_next_state(State, Body, RID, From) -> + State1 = restart_inactivity_timer(State), + Receivers = gb_trees:delete_any(RID, + State1#state.receivers), + State2 = do_reply(State1, From, Body, RID), + case catch gb_trees:take_smallest(Receivers) of + {NextRID, {From1, Req}, Receivers1} + when NextRID == RID + 1 -> + active(Req, From1, + State2#state{receivers = Receivers1}); + _ -> + {next_state, active, + State2#state{receivers = Receivers}} + end. + +reply_stop(State, Body, From, RID) -> + {stop, normal, do_reply(State, From, Body, RID)}. + +drop_holding_receiver(State) -> + drop_holding_receiver(State, State#state.prev_rid). +drop_holding_receiver(State, RID) -> + case gb_trees:lookup(RID, State#state.receivers) of + {value, {From, Body}} -> + State1 = restart_inactivity_timer(State), + Receivers = gb_trees:delete_any(RID, + State1#state.receivers), + State2 = State1#state{receivers = Receivers}, + do_reply(State2, From, Body, RID); + none -> + restart_inactivity_timer(State) + end. + +do_reply(State, From, Body, RID) -> + ?DEBUG("Send reply:~n** RequestID: ~p~n** Reply: " + "~p~n** To: ~p~n** State: ~p", + [RID, Body, From, State]), + p1_fsm:reply(From, Body), + Responses = gb_trees:delete_any(RID, + State#state.responses), + Responses1 = case gb_trees:size(Responses) of + N when N < State#state.max_requests; N == 0 -> + Responses; + _ -> element(3, gb_trees:take_smallest(Responses)) + end, + Responses2 = gb_trees:insert(RID, Body, Responses1), + State#state{responses = Responses2}. + +bounce_receivers(State, _Reason) -> + Receivers = gb_trees:to_list(State#state.receivers), + ShapedReceivers = lists:map(fun ({_, From, + #body{attrs = Attrs} = Body}) -> + RID = get_attr(rid, Attrs), + {RID, {From, Body}} + end, + p1_queue:to_list(State#state.shaped_receivers)), + lists:foldl(fun ({RID, {From, _Body}}, AccState) -> + NewBody = #body{http_reason = + <<"Session closed">>, + attrs = + [{type, <<"terminate">>}, + {condition, + <<"other-request">>}]}, + do_reply(AccState, From, NewBody, RID) + end, + State, Receivers ++ ShapedReceivers). + +bounce_els_from_obuf(State) -> + Opts = ejabberd_config:codec_options(), + p1_queue:foreach( + fun({xmlstreamelement, El}) -> + try xmpp:decode(El, ?NS_CLIENT, Opts) of + Pkt when ?is_stanza(Pkt) -> + case {xmpp:get_from(Pkt), xmpp:get_to(Pkt)} of + {#jid{}, #jid{}} -> + ejabberd_router:route(Pkt); + _ -> + ok + end; + _ -> + ok + catch _:{xmpp_codec, _} -> + ok + end; + (_) -> + ok + end, State#state.el_obuf). + +is_valid_key(<<"">>, <<"">>) -> true; +is_valid_key(PrevKey, Key) -> + str:sha(Key) == PrevKey. + +is_overactivity(undefined) -> false; +is_overactivity(PrevPoll) -> + PollPeriod = timer:now_diff(erlang:timestamp(), PrevPoll) div + 1000000, + if PollPeriod < (?DEFAULT_POLLING) -> true; + true -> false + end. + +make_xmlstreamstart(XMPPDomain, Version) -> + VersionEl = case Version of + <<"">> -> []; + _ -> [{<<"version">>, Version}] + end, + {xmlstreamstart, <<"stream:stream">>, + [{<<"to">>, XMPPDomain}, {<<"xmlns">>, ?NS_CLIENT}, + {<<"xmlns:xmpp">>, ?NS_BOSH}, + {<<"xmlns:stream">>, ?NS_STREAM} + | VersionEl]}. + +maybe_add_xmlstreamend(Els, <<"terminate">>) -> + Els ++ [{xmlstreamend, <<"stream:stream">>}]; +maybe_add_xmlstreamend(Els, _) -> Els. + +encode_body(#body{attrs = Attrs, els = Els}, Type) -> + Attrs1 = lists:map(fun ({K, V}) when is_atom(K) -> + AmK = iolist_to_binary(atom_to_list(K)), + case V of + true -> {AmK, <<"true">>}; + false -> {AmK, <<"false">>}; + I when is_integer(I), I >= 0 -> + {AmK, integer_to_binary(I)}; + _ -> {AmK, V} + end; + ({K, V}) -> {K, V} + end, + Attrs), + Attrs2 = [{<<"xmlns">>, ?NS_HTTP_BIND} | Attrs1], + {Attrs3, XMLs} = lists:foldr(fun ({xmlstreamraw, XML}, + {AttrsAcc, XMLBuf}) -> + {AttrsAcc, [XML | XMLBuf]}; + ({xmlstreamelement, + #xmlel{name = <<"stream:error">>} = El}, + {AttrsAcc, XMLBuf}) -> + {[{<<"type">>, <<"terminate">>}, + {<<"condition">>, + <<"remote-stream-error">>}, + {<<"xmlns:stream">>, ?NS_STREAM} + | AttrsAcc], + [encode_element(El, Type) | XMLBuf]}; + ({xmlstreamelement, + #xmlel{name = <<"stream:features">>} = + El}, + {AttrsAcc, XMLBuf}) -> + {lists:keystore(<<"xmlns:stream">>, 1, + AttrsAcc, + {<<"xmlns:stream">>, + ?NS_STREAM}), + [encode_element(El, Type) | XMLBuf]}; + ({xmlstreamelement, + #xmlel{name = Name, attrs = EAttrs} = El}, + {AttrsAcc, XMLBuf}) + when Name == <<"message">>; + Name == <<"presence">>; + Name == <<"iq">> -> + NewAttrs = lists:keystore( + <<"xmlns">>, 1, EAttrs, + {<<"xmlns">>, ?NS_CLIENT}), + NewEl = El#xmlel{attrs = NewAttrs}, + {AttrsAcc, + [encode_element(NewEl, Type) | XMLBuf]}; + ({xmlstreamelement, El}, + {AttrsAcc, XMLBuf}) -> + {AttrsAcc, + [encode_element(El, Type) | XMLBuf]}; + ({xmlstreamend, _}, {AttrsAcc, XMLBuf}) -> + {[{<<"type">>, <<"terminate">>}, + {<<"condition">>, + <<"remote-stream-error">>} + | AttrsAcc], + XMLBuf}; + ({xmlstreamstart, <<"stream:stream">>, + SAttrs}, + {AttrsAcc, XMLBuf}) -> + StreamID = fxml:get_attr_s(<<"id">>, + SAttrs), + NewAttrs = case + fxml:get_attr_s(<<"version">>, + SAttrs) + of + <<"">> -> + [{<<"authid">>, + StreamID} + | AttrsAcc]; + V -> + lists:keystore(<<"xmlns:xmpp">>, + 1, + [{<<"xmpp:version">>, + V}, + {<<"authid">>, + StreamID} + | AttrsAcc], + {<<"xmlns:xmpp">>, + ?NS_BOSH}) + end, + {NewAttrs, XMLBuf}; + ({xmlstreamerror, _}, + {AttrsAcc, XMLBuf}) -> + {[{<<"type">>, <<"terminate">>}, + {<<"condition">>, + <<"remote-stream-error">>} + | AttrsAcc], + XMLBuf}; + (_, Acc) -> Acc + end, + {Attrs2, []}, Els), + case XMLs of + [] when Type == xml -> + [<<">, attrs_to_list(Attrs3), <<"/>">>]; + _ when Type == xml -> + [<<">, attrs_to_list(Attrs3), $>, XMLs, + <<"">>] + end. + +encode_element(El, xml) -> + fxml:element_to_binary(El); +encode_element(El, json) -> + El. + +decode_body(Data, Size, Type) -> + case decode(Data, Type) of + #xmlel{name = <<"body">>, attrs = Attrs, + children = Els} -> + case attrs_to_body_attrs(Attrs) of + {error, _} = Err -> Err; + BodyAttrs -> + case get_attr(rid, BodyAttrs) of + <<"">> -> {error, <<"Missing \"rid\" attribute">>}; + _ -> + Els1 = lists:flatmap(fun (#xmlel{} = El) -> + [{xmlstreamelement, El}]; + (_) -> [] + end, + Els), + {ok, #body{attrs = BodyAttrs, size = Size, els = Els1}} + end + end; + #xmlel{} -> {error, <<"Unexpected payload">>}; + _ when Type == xml -> + {error, <<"XML is not well-formed">>}; + _ when Type == json -> + {error, <<"JSON is not well-formed">>} + end. + +decode(Data, xml) -> + fxml_stream:parse_element(Data); +decode(Data, json) -> + Data. + +attrs_to_body_attrs(Attrs) -> + lists:foldl(fun (_, {error, Reason}) -> {error, Reason}; + ({Attr, Val}, Acc) -> + try case Attr of + <<"ver">> -> [{ver, Val} | Acc]; + <<"xmpp:version">> -> + [{'xmpp:version', Val} | Acc]; + <<"type">> -> [{type, Val} | Acc]; + <<"key">> -> [{key, Val} | Acc]; + <<"newkey">> -> [{newkey, Val} | Acc]; + <<"xmlns">> -> Val = (?NS_HTTP_BIND), Acc; + <<"secure">> -> [{secure, to_bool(Val)} | Acc]; + <<"xmpp:restart">> -> + [{'xmpp:restart', to_bool(Val)} | Acc]; + <<"to">> -> + [{to, jid:nameprep(Val)} | Acc]; + <<"wait">> -> [{wait, to_int(Val, 0)} | Acc]; + <<"ack">> -> [{ack, to_int(Val, 0)} | Acc]; + <<"sid">> -> [{sid, Val} | Acc]; + <<"hold">> -> [{hold, to_int(Val, 0)} | Acc]; + <<"rid">> -> [{rid, to_int(Val, 0)} | Acc]; + <<"pause">> -> [{pause, to_int(Val, 0)} | Acc]; + _ -> [{Attr, Val} | Acc] + end + catch + _:_ -> + {error, + <<"Invalid \"", Attr/binary, "\" attribute">>} + end + end, + [], Attrs). + +to_int(S, Min) -> + case binary_to_integer(S) of + I when I >= Min -> I; + _ -> erlang:error(badarg) + end. + +to_bool(<<"true">>) -> true; +to_bool(<<"1">>) -> true; +to_bool(<<"false">>) -> false; +to_bool(<<"0">>) -> false. + +attrs_to_list(Attrs) -> [attr_to_list(A) || A <- Attrs]. + +attr_to_list({Name, Value}) -> + [$\s, Name, $=, $', fxml:crypt(Value), $']. + +bosh_response(Body, Type) -> + CType = case Type of + xml -> ?CT_XML; + json -> ?CT_JSON + end, + {200, Body#body.http_reason, ?HEADER(CType), + encode_body(Body, Type)}. + +bosh_response_with_msg(Body, Type, RcvBody) -> + ?DEBUG("Send error reply:~p~n** Receiced body: ~p", + [Body, RcvBody]), + bosh_response(Body, Type). + +http_error(Status, Reason, Type) -> + CType = case Type of + xml -> ?CT_XML; + json -> ?CT_JSON + end, + {Status, Reason, ?HEADER(CType), <<"">>}. + +make_sid() -> str:sha(p1_rand:get_string()). + +-compile({no_auto_import, [{min, 2}]}). + +min(undefined, B) -> B; +min(A, B) -> erlang:min(A, B). + +check_bosh_module(XmppDomain) -> + case gen_mod:is_loaded(XmppDomain, mod_bosh) of + true -> ok; + false -> + ?ERROR_MSG("You are trying to use BOSH (HTTP Bind) " + "in host ~p, but the module mod_bosh " + "is not started in that host. Configure " + "your BOSH client to connect to the correct " + "host, or add your desired host to the " + "configuration, or check your 'modules' " + "section in your ejabberd configuration " + "file.", + [XmppDomain]) + end. + +get_attr(Attr, Attrs) -> get_attr(Attr, Attrs, <<"">>). + +get_attr(Attr, Attrs, Default) -> + case lists:keysearch(Attr, 1, Attrs) of + {value, {_, Val}} -> Val; + _ -> Default + end. + +buf_new(Host) -> + buf_new(Host, unlimited). + +buf_new(Host, Limit) -> + QueueType = mod_bosh_opt:queue_type(Host), + p1_queue:new(QueueType, Limit). + +buf_in(Xs, Buf) -> + lists:foldl(fun p1_queue:in/2, Buf, Xs). + +buf_out(Buf, Num) when is_integer(Num), Num > 0 -> + buf_out(Buf, Num, []); +buf_out(Buf, _) -> {p1_queue:to_list(Buf), p1_queue:clear(Buf)}. + +buf_out(Buf, 0, Els) -> {lists:reverse(Els), Buf}; +buf_out(Buf, I, Els) -> + case p1_queue:out(Buf) of + {{value, El}, NewBuf} -> + buf_out(NewBuf, I - 1, [El | Els]); + {empty, _} -> buf_out(Buf, 0, Els) + end. + +restart_timer(TRef, Timeout, Msg) -> + misc:cancel_timer(TRef), + erlang:start_timer(timer:seconds(Timeout), self(), Msg). + +restart_inactivity_timer(#state{inactivity_timeout = + Timeout} = + State) -> + restart_inactivity_timer(State, Timeout). + +restart_inactivity_timer(#state{inactivity_timer = + TRef} = + State, + Timeout) -> + NewTRef = restart_timer(TRef, Timeout, inactive), + State#state{inactivity_timer = NewTRef}. + +stop_inactivity_timer(#state{inactivity_timer = TRef} = + State) -> + misc:cancel_timer(TRef), + State#state{inactivity_timer = undefined}. + +restart_wait_timer(#state{wait_timer = TRef, + wait_timeout = Timeout} = + State) -> + NewTRef = restart_timer(TRef, Timeout, wait_timeout), + State#state{wait_timer = NewTRef}. + +stop_wait_timer(#state{wait_timer = TRef} = State) -> + misc:cancel_timer(TRef), State#state{wait_timer = undefined}. + +start_shaper_timer(Timeout) -> + erlang:start_timer(Timeout, self(), shaper_timeout). + +make_random_jid(Host) -> + User = p1_rand:get_string(), + jid:make(User, Host, p1_rand:get_string()). + +make_socket(Pid, IP) -> {http_bind, Pid, IP}. diff --git a/src/ejabberd_c2s.erl b/src/ejabberd_c2s.erl index 0855da219..ef9312ef5 100644 --- a/src/ejabberd_c2s.erl +++ b/src/ejabberd_c2s.erl @@ -1,11 +1,8 @@ -%%%---------------------------------------------------------------------- -%%% File : ejabberd_c2s.erl -%%% Author : Alexey Shchepin -%%% Purpose : Serve C2S connection -%%% Created : 16 Nov 2002 by Alexey Shchepin +%%%------------------------------------------------------------------- +%%% Created : 8 Dec 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -21,3123 +18,1147 @@ %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% -%%%---------------------------------------------------------------------- - +%%%------------------------------------------------------------------- -module(ejabberd_c2s). +-behaviour(xmpp_stream_in). +-behaviour(ejabberd_listener). --author('alexey@process-one.net'). +-protocol({rfc, 3920}). +-protocol({rfc, 3921}). +-protocol({rfc, 6120}). +-protocol({rfc, 6121}). +-protocol({xep, 138, '2.1', '1.1.0', "complete", ""}). --update_info({update, 0}). +%% ejabberd_listener callbacks +-export([start/3, start_link/3, accept/1, listen_opt_type/1, listen_options/0]). +%% xmpp_stream_in callbacks +-export([init/1, handle_call/3, handle_cast/2, + handle_info/2, terminate/2, code_change/3]). +-export([tls_options/1, tls_required/1, tls_enabled/1, + allow_unencrypted_sasl2/1, compress_methods/1, bind/2, + sasl_mechanisms/2, get_password_fun/2, check_password_fun/2, + check_password_digest_fun/2, unauthenticated_stream_features/1, + authenticated_stream_features/1, handle_stream_start/2, + handle_stream_end/2, handle_unauthenticated_packet/2, + handle_authenticated_packet/2, handle_auth_success/4, + handle_auth_failure/4, handle_send/3, handle_recv/3, handle_cdata/2, + handle_unbinded_packet/2, inline_stream_features/1, + handle_sasl2_inline/2, handle_sasl2_inline_post/3, + handle_bind2_inline/2, handle_bind2_inline_post/3, sasl_options/1, + handle_sasl2_task_next/4, handle_sasl2_task_data/3, + get_fast_tokens_fun/2, fast_mechanisms/1]). +%% Hooks +-export([handle_unexpected_cast/2, handle_unexpected_call/3, + process_auth_result/3, c2s_handle_bind/1, + reject_unauthenticated_packet/2, process_closed/2, + process_terminated/2, process_info/2]). +%% API +-export([get_presence/1, set_presence/2, resend_presence/1, resend_presence/2, + open_session/1, call/3, cast/2, send/2, close/1, close/2, stop_async/1, + reply/2, copy_state/2, set_timeout/2, route/2, format_reason/2, + host_up/1, host_down/1, send_ws_ping/1, bounce_message_queue/2, + reset_vcard_xupdate_resend_presence/1]). --define(GEN_FSM, p1_fsm). - --behaviour(?GEN_FSM). - -%% External exports --export([start/2, - stop/1, - start_link/2, - send_text/2, - send_element/2, - socket_type/0, - get_presence/1, - get_aux_field/2, - set_aux_field/3, - del_aux_field/2, - get_subscription/2, - send_filtered/5, - broadcast/4, - get_subscribed/1, - transform_listen_option/2]). - -%% gen_fsm callbacks --export([init/1, - wait_for_stream/2, - wait_for_auth/2, - wait_for_feature_request/2, - wait_for_bind/2, - wait_for_session/2, - wait_for_sasl_response/2, - wait_for_resume/2, - session_established/2, - handle_event/3, - handle_sync_event/4, - code_change/4, - handle_info/3, - terminate/3, - print_state/1 - ]). - --include("ejabberd.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). -include("logger.hrl"). - --include("jlib.hrl"). - --include("mod_privacy.hrl"). +-include("mod_roster.hrl"). +-include("translate.hrl"). -define(SETS, gb_sets). --define(DICT, dict). -%% pres_a contains all the presence available send (either through roster mechanism or directed). -%% Directed presence unavailable remove user from pres_a. --record(state, {socket, - sockmod, - socket_monitor, - xml_socket, - streamid, - sasl_state, - access, - shaper, - zlib = false, - tls = false, - tls_required = false, - tls_enabled = false, - tls_options = [], - authenticated = false, - jid, - user = <<"">>, server = <<"">>, resource = <<"">>, - sid, - pres_t = ?SETS:new(), - pres_f = ?SETS:new(), - pres_a = ?SETS:new(), - pres_last, - pres_timestamp, - privacy_list = #userlist{}, - conn = unknown, - auth_module = unknown, - ip, - aux_fields = [], - csi_state = active, - csi_queue = [], - mgmt_state, - mgmt_xmlns, - mgmt_queue, - mgmt_max_queue, - mgmt_pending_since, - mgmt_timeout, - mgmt_resend, - mgmt_stanzas_in = 0, - mgmt_stanzas_out = 0, - lang = <<"">>}). +-type state() :: xmpp_stream_in:state(). +-export_type([state/0]). -%-define(DBGFSM, true). +%%%=================================================================== +%%% ejabberd_listener API +%%%=================================================================== +start(SockMod, Socket, Opts) -> + xmpp_stream_in:start(?MODULE, [{SockMod, Socket}, Opts], + ejabberd_config:fsm_limit_opts(Opts)). --ifdef(DBGFSM). +start_link(SockMod, Socket, Opts) -> + xmpp_stream_in:start_link(?MODULE, [{SockMod, Socket}, Opts], + ejabberd_config:fsm_limit_opts(Opts)). --define(FSMOPTS, [{debug, [trace]}]). +accept(Ref) -> + xmpp_stream_in:accept(Ref). --else. +%%%=================================================================== +%%% Common API +%%%=================================================================== +-spec call(pid(), term(), non_neg_integer() | infinity) -> term(). +call(Ref, Msg, Timeout) -> + xmpp_stream_in:call(Ref, Msg, Timeout). --define(FSMOPTS, []). +-spec cast(pid(), term()) -> ok. +cast(Ref, Msg) -> + xmpp_stream_in:cast(Ref, Msg). --endif. +reply(Ref, Reply) -> + xmpp_stream_in:reply(Ref, Reply). -%% Module start with or without supervisor: --ifdef(NO_TRANSIENT_SUPERVISORS). --define(SUPERVISOR_START, ?GEN_FSM:start(ejabberd_c2s, [SockData, Opts], - fsm_limit_opts(Opts) ++ ?FSMOPTS)). --else. --define(SUPERVISOR_START, supervisor:start_child(ejabberd_c2s_sup, - [SockData, Opts])). --endif. +-spec get_presence(pid()) -> presence(). +get_presence(Ref) -> + call(Ref, get_presence, 1000). -%% This is the timeout to apply between event when starting a new -%% session: --define(C2S_OPEN_TIMEOUT, 60000). +-spec set_presence(pid(), presence()) -> ok. +set_presence(Ref, Pres) -> + call(Ref, {set_presence, Pres}, 1000). --define(C2S_HIBERNATE_TIMEOUT, 90000). +-spec resend_presence(pid()) -> boolean(). +resend_presence(Pid) -> + resend_presence(Pid, undefined). --define(STREAM_HEADER, - <<"">>). +-spec resend_presence(pid(), jid() | undefined) -> boolean(). +resend_presence(Pid, To) -> + route(Pid, {resend_presence, To}). --define(STREAM_TRAILER, <<"">>). +-spec reset_vcard_xupdate_resend_presence(pid()) -> boolean(). +reset_vcard_xupdate_resend_presence(Pid) -> + route(Pid, reset_vcard_xupdate_resend_presence). --define(INVALID_NS_ERR, ?SERR_INVALID_NAMESPACE). +-spec close(pid()) -> ok; + (state()) -> state(). +close(Ref) -> + xmpp_stream_in:close(Ref). --define(INVALID_XML_ERR, ?SERR_XML_NOT_WELL_FORMED). +-spec close(pid(), atom()) -> ok. +close(Ref, Reason) -> + xmpp_stream_in:close(Ref, Reason). --define(HOST_UNKNOWN_ERR, ?SERR_HOST_UNKNOWN). +-spec stop_async(pid()) -> ok. +stop_async(Pid) -> + xmpp_stream_in:stop_async(Pid). --define(POLICY_VIOLATION_ERR(Lang, Text), - ?SERRT_POLICY_VIOLATION(Lang, Text)). - --define(INVALID_FROM, ?SERR_INVALID_FROM). - -%% XEP-0198: - --define(IS_STREAM_MGMT_TAG(Name), - Name == <<"enable">>; - Name == <<"resume">>; - Name == <<"a">>; - Name == <<"r">>). - --define(IS_SUPPORTED_MGMT_XMLNS(Xmlns), - Xmlns == ?NS_STREAM_MGMT_2; - Xmlns == ?NS_STREAM_MGMT_3). - --define(MGMT_FAILED(Condition, Xmlns), - #xmlel{name = <<"failed">>, - attrs = [{<<"xmlns">>, Xmlns}], - children = [#xmlel{name = Condition, - attrs = [{<<"xmlns">>, ?NS_STANZAS}], - children = []}]}). - --define(MGMT_BAD_REQUEST(Xmlns), - ?MGMT_FAILED(<<"bad-request">>, Xmlns)). - --define(MGMT_ITEM_NOT_FOUND(Xmlns), - ?MGMT_FAILED(<<"item-not-found">>, Xmlns)). - --define(MGMT_SERVICE_UNAVAILABLE(Xmlns), - ?MGMT_FAILED(<<"service-unavailable">>, Xmlns)). - --define(MGMT_UNEXPECTED_REQUEST(Xmlns), - ?MGMT_FAILED(<<"unexpected-request">>, Xmlns)). - --define(MGMT_UNSUPPORTED_VERSION(Xmlns), - ?MGMT_FAILED(<<"unsupported-version">>, Xmlns)). - -%%%---------------------------------------------------------------------- -%%% API -%%%---------------------------------------------------------------------- -start(SockData, Opts) -> - ?SUPERVISOR_START. - -start_link(SockData, Opts) -> - ?GEN_FSM:start_link(ejabberd_c2s, [SockData, Opts], - fsm_limit_opts(Opts) ++ ?FSMOPTS). - -socket_type() -> xml_stream. - -%% Return Username, Resource and presence information -get_presence(FsmRef) -> - (?GEN_FSM):sync_send_all_state_event(FsmRef, - {get_presence}, 1000). - -get_aux_field(Key, #state{aux_fields = Opts}) -> - case lists:keysearch(Key, 1, Opts) of - {value, {_, Val}} -> {ok, Val}; - _ -> error +-spec send(pid(), xmpp_element()) -> ok; + (state(), xmpp_element()) -> state(). +send(Pid, Pkt) when is_pid(Pid) -> + xmpp_stream_in:send(Pid, Pkt); +send(#{lserver := LServer} = State, Pkt) -> + Pkt1 = fix_from_to(Pkt, State), + case ejabberd_hooks:run_fold(c2s_filter_send, LServer, {Pkt1, State}, []) of + {drop, State1} -> State1; + {Pkt2, State1} -> xmpp_stream_in:send(State1, Pkt2) end. -set_aux_field(Key, Val, - #state{aux_fields = Opts} = State) -> - Opts1 = lists:keydelete(Key, 1, Opts), - State#state{aux_fields = [{Key, Val} | Opts1]}. - -del_aux_field(Key, #state{aux_fields = Opts} = State) -> - Opts1 = lists:keydelete(Key, 1, Opts), - State#state{aux_fields = Opts1}. - -get_subscription(From = #jid{}, StateData) -> - get_subscription(jlib:jid_tolower(From), StateData); -get_subscription(LFrom, StateData) -> - LBFrom = setelement(3, LFrom, <<"">>), - F = (?SETS):is_element(LFrom, StateData#state.pres_f) - orelse - (?SETS):is_element(LBFrom, StateData#state.pres_f), - T = (?SETS):is_element(LFrom, StateData#state.pres_t) - orelse - (?SETS):is_element(LBFrom, StateData#state.pres_t), - if F and T -> both; - F -> from; - T -> to; - true -> none +-spec send_error(state(), xmpp_element(), stanza_error()) -> state(). +send_error(#{lserver := LServer} = State, Pkt, Err) -> + case ejabberd_hooks:run_fold(c2s_filter_send, LServer, {Pkt, State}, []) of + {drop, State1} -> State1; + {Pkt1, State1} -> xmpp_stream_in:send_error(State1, Pkt1, Err) end. -send_filtered(FsmRef, Feature, From, To, Packet) -> - FsmRef ! {send_filtered, Feature, From, To, Packet}. +-spec send_ws_ping(pid()) -> ok; + (state()) -> state(). +send_ws_ping(Ref) -> + xmpp_stream_in:send_ws_ping(Ref). -broadcast(FsmRef, Type, From, Packet) -> - FsmRef ! {broadcast, Type, From, Packet}. +-spec route(pid(), term()) -> boolean(). +route(Pid, Term) -> + ejabberd_cluster:send(Pid, Term). -stop(FsmRef) -> (?GEN_FSM):send_event(FsmRef, closed). +-spec set_timeout(state(), timeout()) -> state(). +set_timeout(State, Timeout) -> + xmpp_stream_in:set_timeout(State, Timeout). -%%%---------------------------------------------------------------------- -%%% Callback functions from gen_fsm -%%%---------------------------------------------------------------------- +-spec host_up(binary()) -> ok. +host_up(Host) -> + ejabberd_hooks:add(c2s_closed, Host, ?MODULE, process_closed, 100), + ejabberd_hooks:add(c2s_terminated, Host, ?MODULE, + process_terminated, 100), + ejabberd_hooks:add(c2s_handle_bind, Host, ?MODULE, c2s_handle_bind, 100), + ejabberd_hooks:add(c2s_unauthenticated_packet, Host, ?MODULE, + reject_unauthenticated_packet, 100), + ejabberd_hooks:add(c2s_handle_info, Host, ?MODULE, + process_info, 100), + ejabberd_hooks:add(c2s_auth_result, Host, ?MODULE, + process_auth_result, 100), + ejabberd_hooks:add(c2s_handle_cast, Host, ?MODULE, + handle_unexpected_cast, 100), + ejabberd_hooks:add(c2s_handle_call, Host, ?MODULE, + handle_unexpected_call, 100). -%%---------------------------------------------------------------------- -%% Func: init/1 -%% Returns: {ok, StateName, StateData} | -%% {ok, StateName, StateData, Timeout} | -%% ignore | -%% {stop, StopReason} -%%---------------------------------------------------------------------- -init([{SockMod, Socket}, Opts]) -> - Access = case lists:keysearch(access, 1, Opts) of - {value, {_, A}} -> A; - _ -> all - end, - Shaper = case lists:keysearch(shaper, 1, Opts) of - {value, {_, S}} -> S; - _ -> none - end, - XMLSocket = case lists:keysearch(xml_socket, 1, Opts) of - {value, {_, XS}} -> XS; - _ -> false - end, - Zlib = proplists:get_bool(zlib, Opts), - StartTLS = proplists:get_bool(starttls, Opts), - StartTLSRequired = proplists:get_bool(starttls_required, Opts), - TLSEnabled = proplists:get_bool(tls, Opts), - TLS = StartTLS orelse - StartTLSRequired orelse TLSEnabled, - TLSOpts1 = lists:filter(fun ({certfile, _}) -> true; - ({ciphers, _}) -> true; - (_) -> false - end, - Opts), - TLSOpts2 = case lists:keysearch(protocol_options, 1, Opts) of - {value, {_, O}} -> - [_|ProtocolOptions] = lists:foldl( - fun(X, Acc) -> X ++ Acc end, [], - [["|" | binary_to_list(Opt)] || Opt <- O, is_binary(Opt)] - ), - [{protocol_options, iolist_to_binary(ProtocolOptions)} | TLSOpts1]; - _ -> TLSOpts1 - end, - TLSOpts3 = case proplists:get_bool(tls_compression, Opts) of - false -> [compression_none | TLSOpts2]; - true -> TLSOpts2 - end, - TLSOpts = [verify_none | TLSOpts3], - StreamMgmtEnabled = proplists:get_value(stream_management, Opts, true), - StreamMgmtState = if StreamMgmtEnabled -> inactive; - true -> disabled - end, - MaxAckQueue = case proplists:get_value(max_ack_queue, Opts) of - Limit when is_integer(Limit), Limit > 0 -> Limit; - infinity -> infinity; - _ -> 500 - end, - ResumeTimeout = case proplists:get_value(resume_timeout, Opts) of - Timeout when is_integer(Timeout), Timeout >= 0 -> Timeout; - _ -> 300 - end, - ResendOnTimeout = case proplists:get_value(resend_on_timeout, Opts) of - Resend when is_boolean(Resend) -> Resend; - if_offline -> if_offline; - _ -> false - end, - IP = peerip(SockMod, Socket), - Socket1 = if TLSEnabled andalso - SockMod /= ejabberd_frontend_socket -> - SockMod:starttls(Socket, TLSOpts); - true -> Socket - end, - SocketMonitor = SockMod:monitor(Socket1), - StateData = #state{socket = Socket1, sockmod = SockMod, - socket_monitor = SocketMonitor, - xml_socket = XMLSocket, zlib = Zlib, tls = TLS, - tls_required = StartTLSRequired, - tls_enabled = TLSEnabled, tls_options = TLSOpts, - sid = {now(), self()}, streamid = new_id(), - access = Access, shaper = Shaper, ip = IP, - mgmt_state = StreamMgmtState, - mgmt_max_queue = MaxAckQueue, - mgmt_timeout = ResumeTimeout, - mgmt_resend = ResendOnTimeout}, - {ok, wait_for_stream, StateData, ?C2S_OPEN_TIMEOUT}. +-spec host_down(binary()) -> ok. +host_down(Host) -> + ejabberd_hooks:delete(c2s_closed, Host, ?MODULE, process_closed, 100), + ejabberd_hooks:delete(c2s_terminated, Host, ?MODULE, + process_terminated, 100), + ejabberd_hooks:delete(c2s_handle_bind, Host, ?MODULE, c2s_handle_bind, 100), + ejabberd_hooks:delete(c2s_unauthenticated_packet, Host, ?MODULE, + reject_unauthenticated_packet, 100), + ejabberd_hooks:delete(c2s_handle_info, Host, ?MODULE, + process_info, 100), + ejabberd_hooks:delete(c2s_auth_result, Host, ?MODULE, + process_auth_result, 100), + ejabberd_hooks:delete(c2s_handle_cast, Host, ?MODULE, + handle_unexpected_cast, 100), + ejabberd_hooks:delete(c2s_handle_call, Host, ?MODULE, + handle_unexpected_call, 100). -%% Return list of all available resources of contacts, -get_subscribed(FsmRef) -> - (?GEN_FSM):sync_send_all_state_event(FsmRef, - get_subscribed, 1000). - -%%---------------------------------------------------------------------- -%% Func: StateName/2 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} -%%---------------------------------------------------------------------- - -wait_for_stream({xmlstreamstart, _Name, Attrs}, StateData) -> - DefaultLang = ?MYLANG, - case xml:get_attr_s(<<"xmlns:stream">>, Attrs) of - ?NS_STREAM -> - Server = - case StateData#state.server of - <<"">> -> - jlib:nameprep(xml:get_attr_s(<<"to">>, Attrs)); - S -> S - end, - Lang = case xml:get_attr_s(<<"xml:lang">>, Attrs) of - Lang1 when byte_size(Lang1) =< 35 -> - %% As stated in BCP47, 4.4.1: - %% Protocols or specifications that - %% specify limited buffer sizes for - %% language tags MUST allow for - %% language tags of at least 35 characters. - Lang1; - _ -> - %% Do not store long language tag to - %% avoid possible DoS/flood attacks - <<"">> - end, - IsBlacklistedIP = is_ip_blacklisted(StateData#state.ip, Lang), - case lists:member(Server, ?MYHOSTS) of - true when IsBlacklistedIP == false -> - change_shaper(StateData, jlib:make_jid(<<"">>, Server, <<"">>)), - case xml:get_attr_s(<<"version">>, Attrs) of - <<"1.0">> -> - send_header(StateData, Server, <<"1.0">>, DefaultLang), - case StateData#state.authenticated of - false -> - TLS = StateData#state.tls, - TLSEnabled = StateData#state.tls_enabled, - TLSRequired = StateData#state.tls_required, - SASLState = - cyrsasl:server_new( - <<"jabber">>, Server, <<"">>, [], - fun(U) -> - ejabberd_auth:get_password_with_authmodule( - U, Server) - end, - fun(U, P) -> - ejabberd_auth:check_password_with_authmodule( - U, Server, P) - end, - fun(U, P, D, DG) -> - ejabberd_auth:check_password_with_authmodule( - U, Server, P, D, DG) - end), - Mechs = - case TLSEnabled or not TLSRequired of - true -> - Ms = lists:map(fun (S) -> - #xmlel{name = <<"mechanism">>, - attrs = [], - children = [{xmlcdata, S}]} - end, - cyrsasl:listmech(Server)), - [#xmlel{name = <<"mechanisms">>, - attrs = [{<<"xmlns">>, ?NS_SASL}], - children = Ms}]; - false -> - [] - end, - SockMod = - (StateData#state.sockmod):get_sockmod( - StateData#state.socket), - Zlib = StateData#state.zlib, - CompressFeature = - case Zlib andalso - ((SockMod == gen_tcp) orelse - (SockMod == p1_tls)) of - true -> - [#xmlel{name = <<"compression">>, - attrs = [{<<"xmlns">>, ?NS_FEATURE_COMPRESS}], - children = [#xmlel{name = <<"method">>, - attrs = [], - children = [{xmlcdata, <<"zlib">>}]}]}]; - _ -> - [] - end, - TLSFeature = - case (TLS == true) andalso - (TLSEnabled == false) andalso - (SockMod == gen_tcp) of - true -> - case TLSRequired of - true -> - [#xmlel{name = <<"starttls">>, - attrs = [{<<"xmlns">>, ?NS_TLS}], - children = [#xmlel{name = <<"required">>, - attrs = [], - children = []}]}]; - _ -> - [#xmlel{name = <<"starttls">>, - attrs = [{<<"xmlns">>, ?NS_TLS}], - children = []}] - end; - false -> - [] - end, - send_element(StateData, - #xmlel{name = <<"stream:features">>, - attrs = [], - children = - TLSFeature ++ CompressFeature ++ Mechs - ++ - ejabberd_hooks:run_fold(c2s_stream_features, - Server, [], [Server])}), - fsm_next_state(wait_for_feature_request, - StateData#state{ - server = Server, - sasl_state = SASLState, - lang = Lang}); - _ -> - case StateData#state.resource of - <<"">> -> - RosterVersioningFeature = - ejabberd_hooks:run_fold(roster_get_versioning_feature, - Server, [], - [Server]), - StreamManagementFeature = - case stream_mgmt_enabled(StateData) of - true -> - [#xmlel{name = <<"sm">>, - attrs = [{<<"xmlns">>, ?NS_STREAM_MGMT_2}], - children = []}, - #xmlel{name = <<"sm">>, - attrs = [{<<"xmlns">>, ?NS_STREAM_MGMT_3}], - children = []}]; - false -> - [] - end, - StreamFeatures = [#xmlel{name = <<"bind">>, - attrs = [{<<"xmlns">>, ?NS_BIND}], - children = []}, - #xmlel{name = <<"session">>, - attrs = [{<<"xmlns">>, ?NS_SESSION}], - children = []}] - ++ - RosterVersioningFeature ++ - StreamManagementFeature ++ - ejabberd_hooks:run_fold(c2s_post_auth_features, - Server, [], [Server]) ++ - ejabberd_hooks:run_fold(c2s_stream_features, - Server, [], [Server]), - send_element(StateData, - #xmlel{name = <<"stream:features">>, - attrs = [], - children = StreamFeatures}), - fsm_next_state(wait_for_bind, - StateData#state{server = Server, lang = Lang}); - _ -> - send_element(StateData, - #xmlel{name = <<"stream:features">>, - attrs = [], - children = []}), - fsm_next_state(wait_for_session, - StateData#state{server = Server, lang = Lang}) - end - end; - _ -> - send_header(StateData, Server, <<"">>, DefaultLang), - if not StateData#state.tls_enabled and - StateData#state.tls_required -> - send_element(StateData, - ?POLICY_VIOLATION_ERR(Lang, - <<"Use of STARTTLS required">>)), - send_trailer(StateData), - {stop, normal, StateData}; - true -> - fsm_next_state(wait_for_auth, - StateData#state{server = Server, - lang = Lang}) - end - end; - true -> - IP = StateData#state.ip, - {true, LogReason, ReasonT} = IsBlacklistedIP, - ?INFO_MSG("Connection attempt from blacklisted IP ~s: ~s", - [jlib:ip_to_list(IP), LogReason]), - send_header(StateData, Server, <<"">>, DefaultLang), - send_element(StateData, ?POLICY_VIOLATION_ERR(Lang, ReasonT)), - send_trailer(StateData), - {stop, normal, StateData}; - _ -> - send_header(StateData, ?MYNAME, <<"">>, DefaultLang), - send_element(StateData, ?HOST_UNKNOWN_ERR), - send_trailer(StateData), - {stop, normal, StateData} - end; - _ -> - send_header(StateData, ?MYNAME, <<"">>, DefaultLang), - send_element(StateData, ?INVALID_NS_ERR), - send_trailer(StateData), - {stop, normal, StateData} - end; -wait_for_stream(timeout, StateData) -> - {stop, normal, StateData}; -wait_for_stream({xmlstreamelement, _}, StateData) -> - send_element(StateData, ?INVALID_XML_ERR), - send_trailer(StateData), - {stop, normal, StateData}; -wait_for_stream({xmlstreamend, _}, StateData) -> - send_element(StateData, ?INVALID_XML_ERR), - send_trailer(StateData), - {stop, normal, StateData}; -wait_for_stream({xmlstreamerror, _}, StateData) -> - send_header(StateData, ?MYNAME, <<"1.0">>, <<"">>), - send_element(StateData, ?INVALID_XML_ERR), - send_trailer(StateData), - {stop, normal, StateData}; -wait_for_stream(closed, StateData) -> - {stop, normal, StateData}. - -wait_for_auth({xmlstreamelement, #xmlel{name = Name} = El}, StateData) - when ?IS_STREAM_MGMT_TAG(Name) -> - fsm_next_state(wait_for_auth, dispatch_stream_mgmt(El, StateData)); -wait_for_auth({xmlstreamelement, El}, StateData) -> - case is_auth_packet(El) of - {auth, _ID, get, {U, _, _, _}} -> - #xmlel{name = Name, attrs = Attrs} = - jlib:make_result_iq_reply(El), - case U of - <<"">> -> UCdata = []; - _ -> UCdata = [{xmlcdata, U}] - end, - Res = case - ejabberd_auth:plain_password_required(StateData#state.server) - of - false -> - #xmlel{name = Name, attrs = Attrs, - children = - [#xmlel{name = <<"query">>, - attrs = [{<<"xmlns">>, ?NS_AUTH}], - children = - [#xmlel{name = <<"username">>, - attrs = [], - children = UCdata}, - #xmlel{name = <<"password">>, - attrs = [], children = []}, - #xmlel{name = <<"digest">>, - attrs = [], children = []}, - #xmlel{name = <<"resource">>, - attrs = [], - children = []}]}]}; - true -> - #xmlel{name = Name, attrs = Attrs, - children = - [#xmlel{name = <<"query">>, - attrs = [{<<"xmlns">>, ?NS_AUTH}], - children = - [#xmlel{name = <<"username">>, - attrs = [], - children = UCdata}, - #xmlel{name = <<"password">>, - attrs = [], children = []}, - #xmlel{name = <<"resource">>, - attrs = [], - children = []}]}]} - end, - send_element(StateData, Res), - fsm_next_state(wait_for_auth, StateData); - {auth, _ID, set, {_U, _P, _D, <<"">>}} -> - Err = jlib:make_error_reply(El, - ?ERR_AUTH_NO_RESOURCE_PROVIDED((StateData#state.lang))), - send_element(StateData, Err), - fsm_next_state(wait_for_auth, StateData); - {auth, _ID, set, {U, P, D, R}} -> - JID = jlib:make_jid(U, StateData#state.server, R), - case JID /= error andalso - acl:match_rule(StateData#state.server, - StateData#state.access, JID) - == allow - of - true -> - DGen = fun (PW) -> - p1_sha:sha(<<(StateData#state.streamid)/binary, PW/binary>>) - end, - case ejabberd_auth:check_password_with_authmodule(U, - StateData#state.server, - P, D, DGen) - of - {true, AuthModule} -> - ?INFO_MSG("(~w) Accepted legacy authentication for ~s by ~p from ~s", - [StateData#state.socket, - jlib:jid_to_string(JID), AuthModule, - jlib:ip_to_list(StateData#state.ip)]), - ejabberd_hooks:run(c2s_auth_result, StateData#state.server, - [true, U, StateData#state.server, - StateData#state.ip]), - Conn = get_conn_type(StateData), - Info = [{ip, StateData#state.ip}, {conn, Conn}, - {auth_module, AuthModule}], - Res = jlib:make_result_iq_reply( - El#xmlel{children = []}), - send_element(StateData, Res), - ejabberd_sm:open_session(StateData#state.sid, U, - StateData#state.server, R, - Info), - change_shaper(StateData, JID), - {Fs, Ts} = - ejabberd_hooks:run_fold(roster_get_subscription_lists, - StateData#state.server, - {[], []}, - [U, - StateData#state.server]), - LJID = - jlib:jid_tolower(jlib:jid_remove_resource(JID)), - Fs1 = [LJID | Fs], - Ts1 = [LJID | Ts], - PrivList = ejabberd_hooks:run_fold(privacy_get_user_list, - StateData#state.server, - #userlist{}, - [U, StateData#state.server]), - NewStateData = StateData#state{user = U, - resource = R, - jid = JID, - conn = Conn, - auth_module = AuthModule, - pres_f = (?SETS):from_list(Fs1), - pres_t = (?SETS):from_list(Ts1), - privacy_list = PrivList}, - fsm_next_state(session_established, NewStateData); - _ -> - ?INFO_MSG("(~w) Failed legacy authentication for ~s from ~s", - [StateData#state.socket, - jlib:jid_to_string(JID), - jlib:ip_to_list(StateData#state.ip)]), - ejabberd_hooks:run(c2s_auth_result, StateData#state.server, - [false, U, StateData#state.server, - StateData#state.ip]), - Err = jlib:make_error_reply(El, ?ERR_NOT_AUTHORIZED), - send_element(StateData, Err), - fsm_next_state(wait_for_auth, StateData) - end; - _ -> - if JID == error -> - ?INFO_MSG("(~w) Forbidden legacy authentication " - "for username '~s' with resource '~s'", - [StateData#state.socket, U, R]), - Err = jlib:make_error_reply(El, ?ERR_JID_MALFORMED), - send_element(StateData, Err), - fsm_next_state(wait_for_auth, StateData); - true -> - ?INFO_MSG("(~w) Forbidden legacy authentication " - "for ~s from ~s", - [StateData#state.socket, - jlib:jid_to_string(JID), - jlib:ip_to_list(StateData#state.ip)]), - ejabberd_hooks:run(c2s_auth_result, StateData#state.server, - [false, U, StateData#state.server, - StateData#state.ip]), - Err = jlib:make_error_reply(El, ?ERR_NOT_ALLOWED), - send_element(StateData, Err), - fsm_next_state(wait_for_auth, StateData) - end - end; - _ -> - process_unauthenticated_stanza(StateData, El), - fsm_next_state(wait_for_auth, StateData) - end; -wait_for_auth(timeout, StateData) -> - {stop, normal, StateData}; -wait_for_auth({xmlstreamend, _Name}, StateData) -> - send_trailer(StateData), {stop, normal, StateData}; -wait_for_auth({xmlstreamerror, _}, StateData) -> - send_element(StateData, ?INVALID_XML_ERR), - send_trailer(StateData), - {stop, normal, StateData}; -wait_for_auth(closed, StateData) -> - {stop, normal, StateData}. - -wait_for_feature_request({xmlstreamelement, #xmlel{name = Name} = El}, - StateData) - when ?IS_STREAM_MGMT_TAG(Name) -> - fsm_next_state(wait_for_feature_request, - dispatch_stream_mgmt(El, StateData)); -wait_for_feature_request({xmlstreamelement, El}, - StateData) -> - #xmlel{name = Name, attrs = Attrs, children = Els} = El, - Zlib = StateData#state.zlib, - TLS = StateData#state.tls, - TLSEnabled = StateData#state.tls_enabled, - TLSRequired = StateData#state.tls_required, - SockMod = - (StateData#state.sockmod):get_sockmod(StateData#state.socket), - case {xml:get_attr_s(<<"xmlns">>, Attrs), Name} of - {?NS_SASL, <<"auth">>} - when TLSEnabled or not TLSRequired -> - Mech = xml:get_attr_s(<<"mechanism">>, Attrs), - ClientIn = jlib:decode_base64(xml:get_cdata(Els)), - case cyrsasl:server_start(StateData#state.sasl_state, - Mech, ClientIn) - of - {ok, Props} -> - (StateData#state.sockmod):reset_stream(StateData#state.socket), - %U = xml:get_attr_s(username, Props), - U = proplists:get_value(username, Props, <<>>), - %AuthModule = xml:get_attr_s(auth_module, Props), - AuthModule = proplists:get_value(auth_module, Props, undefined), - ?INFO_MSG("(~w) Accepted authentication for ~s " - "by ~p from ~s", - [StateData#state.socket, U, AuthModule, - jlib:ip_to_list(StateData#state.ip)]), - ejabberd_hooks:run(c2s_auth_result, StateData#state.server, - [true, U, StateData#state.server, - StateData#state.ip]), - send_element(StateData, - #xmlel{name = <<"success">>, - attrs = [{<<"xmlns">>, ?NS_SASL}], - children = []}), - fsm_next_state(wait_for_stream, - StateData#state{streamid = new_id(), - authenticated = true, - auth_module = AuthModule, - sasl_state = undefined, - user = U}); - {continue, ServerOut, NewSASLState} -> - send_element(StateData, - #xmlel{name = <<"challenge">>, - attrs = [{<<"xmlns">>, ?NS_SASL}], - children = - [{xmlcdata, - jlib:encode_base64(ServerOut)}]}), - fsm_next_state(wait_for_sasl_response, - StateData#state{sasl_state = NewSASLState}); - {error, Error, Username} -> - ?INFO_MSG("(~w) Failed authentication for ~s@~s from ~s", - [StateData#state.socket, - Username, StateData#state.server, - jlib:ip_to_list(StateData#state.ip)]), - ejabberd_hooks:run(c2s_auth_result, StateData#state.server, - [false, Username, StateData#state.server, - StateData#state.ip]), - send_element(StateData, - #xmlel{name = <<"failure">>, - attrs = [{<<"xmlns">>, ?NS_SASL}], - children = - [#xmlel{name = Error, attrs = [], - children = []}]}), - fsm_next_state(wait_for_feature_request, StateData); - {error, Error} -> - send_element(StateData, - #xmlel{name = <<"failure">>, - attrs = [{<<"xmlns">>, ?NS_SASL}], - children = - [#xmlel{name = Error, attrs = [], - children = []}]}), - fsm_next_state(wait_for_feature_request, StateData) - end; - {?NS_TLS, <<"starttls">>} - when TLS == true, TLSEnabled == false, - SockMod == gen_tcp -> - TLSOpts = case - ejabberd_config:get_option( - {domain_certfile, StateData#state.server}, - fun iolist_to_binary/1) - of - undefined -> StateData#state.tls_options; - CertFile -> - [{certfile, CertFile} | lists:keydelete(certfile, 1, - StateData#state.tls_options)] - end, - Socket = StateData#state.socket, - BProceed = xml:element_to_binary(#xmlel{name = <<"proceed">>, - attrs = [{<<"xmlns">>, ?NS_TLS}]}), - TLSSocket = (StateData#state.sockmod):starttls(Socket, - TLSOpts, - BProceed), - fsm_next_state(wait_for_stream, - StateData#state{socket = TLSSocket, - streamid = new_id(), - tls_enabled = true}); - {?NS_COMPRESS, <<"compress">>} - when Zlib == true, - (SockMod == gen_tcp) or (SockMod == p1_tls) -> - case xml:get_subtag(El, <<"method">>) of - false -> - send_element(StateData, - #xmlel{name = <<"failure">>, - attrs = [{<<"xmlns">>, ?NS_COMPRESS}], - children = - [#xmlel{name = <<"setup-failed">>, - attrs = [], children = []}]}), - fsm_next_state(wait_for_feature_request, StateData); - Method -> - case xml:get_tag_cdata(Method) of - <<"zlib">> -> - Socket = StateData#state.socket, - BCompressed = xml:element_to_binary(#xmlel{name = <<"compressed">>, - attrs = [{<<"xmlns">>, ?NS_COMPRESS}]}), - ZlibSocket = (StateData#state.sockmod):compress(Socket, - BCompressed), - fsm_next_state(wait_for_stream, - StateData#state{socket = ZlibSocket, - streamid = new_id()}); - _ -> - send_element(StateData, - #xmlel{name = <<"failure">>, - attrs = [{<<"xmlns">>, ?NS_COMPRESS}], - children = - [#xmlel{name = - <<"unsupported-method">>, - attrs = [], - children = []}]}), - fsm_next_state(wait_for_feature_request, StateData) - end - end; - _ -> - if TLSRequired and not TLSEnabled -> - Lang = StateData#state.lang, - send_element(StateData, - ?POLICY_VIOLATION_ERR(Lang, - <<"Use of STARTTLS required">>)), - send_trailer(StateData), - {stop, normal, StateData}; - true -> - process_unauthenticated_stanza(StateData, El), - fsm_next_state(wait_for_feature_request, StateData) - end - end; -wait_for_feature_request(timeout, StateData) -> - {stop, normal, StateData}; -wait_for_feature_request({xmlstreamend, _Name}, - StateData) -> - send_trailer(StateData), {stop, normal, StateData}; -wait_for_feature_request({xmlstreamerror, _}, - StateData) -> - send_element(StateData, ?INVALID_XML_ERR), - send_trailer(StateData), - {stop, normal, StateData}; -wait_for_feature_request(closed, StateData) -> - {stop, normal, StateData}. - -wait_for_sasl_response({xmlstreamelement, #xmlel{name = Name} = El}, StateData) - when ?IS_STREAM_MGMT_TAG(Name) -> - fsm_next_state(wait_for_sasl_response, dispatch_stream_mgmt(El, StateData)); -wait_for_sasl_response({xmlstreamelement, El}, - StateData) -> - #xmlel{name = Name, attrs = Attrs, children = Els} = El, - case {xml:get_attr_s(<<"xmlns">>, Attrs), Name} of - {?NS_SASL, <<"response">>} -> - ClientIn = jlib:decode_base64(xml:get_cdata(Els)), - case cyrsasl:server_step(StateData#state.sasl_state, - ClientIn) - of - {ok, Props} -> - catch - (StateData#state.sockmod):reset_stream(StateData#state.socket), -% U = xml:get_attr_s(username, Props), - U = proplists:get_value(username, Props, <<>>), -% AuthModule = xml:get_attr_s(auth_module, Props), - AuthModule = proplists:get_value(auth_module, Props, <<>>), - ?INFO_MSG("(~w) Accepted authentication for ~s " - "by ~p from ~s", - [StateData#state.socket, U, AuthModule, - jlib:ip_to_list(StateData#state.ip)]), - ejabberd_hooks:run(c2s_auth_result, StateData#state.server, - [true, U, StateData#state.server, - StateData#state.ip]), - send_element(StateData, - #xmlel{name = <<"success">>, - attrs = [{<<"xmlns">>, ?NS_SASL}], - children = []}), - fsm_next_state(wait_for_stream, - StateData#state{streamid = new_id(), - authenticated = true, - auth_module = AuthModule, - sasl_state = undefined, - user = U}); - {ok, Props, ServerOut} -> - (StateData#state.sockmod):reset_stream(StateData#state.socket), -% U = xml:get_attr_s(username, Props), - U = proplists:get_value(username, Props, <<>>), -% AuthModule = xml:get_attr_s(auth_module, Props), - AuthModule = proplists:get_value(auth_module, Props, undefined), - ?INFO_MSG("(~w) Accepted authentication for ~s " - "by ~p from ~s", - [StateData#state.socket, U, AuthModule, - jlib:ip_to_list(StateData#state.ip)]), - ejabberd_hooks:run(c2s_auth_result, StateData#state.server, - [true, U, StateData#state.server, - StateData#state.ip]), - send_element(StateData, - #xmlel{name = <<"success">>, - attrs = [{<<"xmlns">>, ?NS_SASL}], - children = - [{xmlcdata, - jlib:encode_base64(ServerOut)}]}), - fsm_next_state(wait_for_stream, - StateData#state{streamid = new_id(), - authenticated = true, - auth_module = AuthModule, - sasl_state = undefined, - user = U}); - {continue, ServerOut, NewSASLState} -> - send_element(StateData, - #xmlel{name = <<"challenge">>, - attrs = [{<<"xmlns">>, ?NS_SASL}], - children = - [{xmlcdata, - jlib:encode_base64(ServerOut)}]}), - fsm_next_state(wait_for_sasl_response, - StateData#state{sasl_state = NewSASLState}); - {error, Error, Username} -> - ?INFO_MSG("(~w) Failed authentication for ~s@~s from ~s", - [StateData#state.socket, - Username, StateData#state.server, - jlib:ip_to_list(StateData#state.ip)]), - ejabberd_hooks:run(c2s_auth_result, StateData#state.server, - [false, Username, StateData#state.server, - StateData#state.ip]), - send_element(StateData, - #xmlel{name = <<"failure">>, - attrs = [{<<"xmlns">>, ?NS_SASL}], - children = - [#xmlel{name = Error, attrs = [], - children = []}]}), - fsm_next_state(wait_for_feature_request, StateData); - {error, Error} -> - send_element(StateData, - #xmlel{name = <<"failure">>, - attrs = [{<<"xmlns">>, ?NS_SASL}], - children = - [#xmlel{name = Error, attrs = [], - children = []}]}), - fsm_next_state(wait_for_feature_request, StateData) - end; - _ -> - process_unauthenticated_stanza(StateData, El), - fsm_next_state(wait_for_feature_request, StateData) - end; -wait_for_sasl_response(timeout, StateData) -> - {stop, normal, StateData}; -wait_for_sasl_response({xmlstreamend, _Name}, - StateData) -> - send_trailer(StateData), {stop, normal, StateData}; -wait_for_sasl_response({xmlstreamerror, _}, - StateData) -> - send_element(StateData, ?INVALID_XML_ERR), - send_trailer(StateData), - {stop, normal, StateData}; -wait_for_sasl_response(closed, StateData) -> - {stop, normal, StateData}. - -resource_conflict_action(U, S, R) -> - OptionRaw = case ejabberd_sm:is_existing_resource(U, S, R) of - true -> - ejabberd_config:get_option( - {resource_conflict, S}, - fun(setresource) -> setresource; - (closeold) -> closeold; - (closenew) -> closenew; - (acceptnew) -> acceptnew - end); - false -> - acceptnew - end, - Option = case OptionRaw of - setresource -> setresource; - closeold -> - acceptnew; %% ejabberd_sm will close old session - closenew -> closenew; - acceptnew -> acceptnew; - _ -> acceptnew %% default ejabberd behavior - end, - case Option of - acceptnew -> {accept_resource, R}; - closenew -> closenew; - setresource -> - Rnew = iolist_to_binary([randoms:get_string() - | [jlib:integer_to_binary(X) - || X <- tuple_to_list(now())]]), - {accept_resource, Rnew} - end. - -wait_for_bind({xmlstreamelement, #xmlel{name = Name, attrs = Attrs} = El}, - StateData) - when ?IS_STREAM_MGMT_TAG(Name) -> - case Name of - <<"resume">> -> - case handle_resume(StateData, Attrs) of - {ok, ResumedState} -> - fsm_next_state(session_established, ResumedState); - error -> - fsm_next_state(wait_for_bind, StateData) - end; - _ -> - fsm_next_state(wait_for_bind, dispatch_stream_mgmt(El, StateData)) - end; -wait_for_bind({xmlstreamelement, El}, StateData) -> - case jlib:iq_query_info(El) of - #iq{type = set, xmlns = ?NS_BIND, sub_el = SubEl} = - IQ -> - U = StateData#state.user, - R1 = xml:get_path_s(SubEl, - [{elem, <<"resource">>}, cdata]), - R = case jlib:resourceprep(R1) of - error -> error; - <<"">> -> - iolist_to_binary([randoms:get_string() - | [jlib:integer_to_binary(X) - || X <- tuple_to_list(now())]]); - Resource -> Resource - end, - case R of - error -> - Err = jlib:make_error_reply(El, ?ERR_BAD_REQUEST), - send_element(StateData, Err), - fsm_next_state(wait_for_bind, StateData); - _ -> - case resource_conflict_action(U, StateData#state.server, - R) - of - closenew -> - Err = jlib:make_error_reply(El, - ?STANZA_ERROR(<<"409">>, - <<"modify">>, - <<"conflict">>)), - send_element(StateData, Err), - fsm_next_state(wait_for_bind, StateData); - {accept_resource, R2} -> - JID = jlib:make_jid(U, StateData#state.server, R2), - Res = IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"bind">>, - attrs = [{<<"xmlns">>, ?NS_BIND}], - children = - [#xmlel{name = <<"jid">>, - attrs = [], - children = - [{xmlcdata, - jlib:jid_to_string(JID)}]}]}]}, - send_element(StateData, jlib:iq_to_xml(Res)), - fsm_next_state(wait_for_session, - StateData#state{resource = R2, jid = JID}) - end - end; - _ -> fsm_next_state(wait_for_bind, StateData) - end; -wait_for_bind(timeout, StateData) -> - {stop, normal, StateData}; -wait_for_bind({xmlstreamend, _Name}, StateData) -> - send_trailer(StateData), {stop, normal, StateData}; -wait_for_bind({xmlstreamerror, _}, StateData) -> - send_element(StateData, ?INVALID_XML_ERR), - send_trailer(StateData), - {stop, normal, StateData}; -wait_for_bind(closed, StateData) -> - {stop, normal, StateData}. - -wait_for_session({xmlstreamelement, #xmlel{name = Name} = El}, StateData) - when ?IS_STREAM_MGMT_TAG(Name) -> - fsm_next_state(wait_for_session, dispatch_stream_mgmt(El, StateData)); -wait_for_session({xmlstreamelement, El}, StateData) -> - NewStateData = update_num_stanzas_in(StateData, El), - case jlib:iq_query_info(El) of - #iq{type = set, xmlns = ?NS_SESSION} -> - U = NewStateData#state.user, - R = NewStateData#state.resource, - JID = NewStateData#state.jid, - case acl:match_rule(NewStateData#state.server, - NewStateData#state.access, JID) of - allow -> - ?INFO_MSG("(~w) Opened session for ~s", - [NewStateData#state.socket, - jlib:jid_to_string(JID)]), - Res = jlib:make_result_iq_reply(El#xmlel{children = []}), - NewState = send_stanza(NewStateData, Res), - change_shaper(NewState, JID), - {Fs, Ts} = ejabberd_hooks:run_fold( - roster_get_subscription_lists, - NewState#state.server, - {[], []}, - [U, NewState#state.server]), - LJID = jlib:jid_tolower(jlib:jid_remove_resource(JID)), - Fs1 = [LJID | Fs], - Ts1 = [LJID | Ts], - PrivList = - ejabberd_hooks:run_fold( - privacy_get_user_list, NewState#state.server, - #userlist{}, - [U, NewState#state.server]), - Conn = get_conn_type(NewState), - Info = [{ip, NewState#state.ip}, {conn, Conn}, - {auth_module, NewState#state.auth_module}], - ejabberd_sm:open_session( - NewState#state.sid, U, NewState#state.server, R, Info), - UpdatedStateData = - NewState#state{ - conn = Conn, - pres_f = ?SETS:from_list(Fs1), - pres_t = ?SETS:from_list(Ts1), - privacy_list = PrivList}, - fsm_next_state_pack(session_established, - UpdatedStateData); - _ -> - ejabberd_hooks:run(forbidden_session_hook, - NewStateData#state.server, [JID]), - ?INFO_MSG("(~w) Forbidden session for ~s", - [NewStateData#state.socket, - jlib:jid_to_string(JID)]), - Err = jlib:make_error_reply(El, ?ERR_NOT_ALLOWED), - send_element(NewStateData, Err), - fsm_next_state(wait_for_session, NewStateData) - end; - _ -> - fsm_next_state(wait_for_session, NewStateData) - end; - -wait_for_session(timeout, StateData) -> - {stop, normal, StateData}; -wait_for_session({xmlstreamend, _Name}, StateData) -> - send_trailer(StateData), {stop, normal, StateData}; -wait_for_session({xmlstreamerror, _}, StateData) -> - send_element(StateData, ?INVALID_XML_ERR), - send_trailer(StateData), - {stop, normal, StateData}; -wait_for_session(closed, StateData) -> - {stop, normal, StateData}. - -session_established({xmlstreamelement, #xmlel{name = Name} = El}, StateData) - when ?IS_STREAM_MGMT_TAG(Name) -> - fsm_next_state(session_established, dispatch_stream_mgmt(El, StateData)); -session_established({xmlstreamelement, - #xmlel{name = <<"active">>, - attrs = [{<<"xmlns">>, ?NS_CLIENT_STATE}]}}, - StateData) -> - NewStateData = csi_queue_flush(StateData), - fsm_next_state(session_established, NewStateData#state{csi_state = active}); -session_established({xmlstreamelement, - #xmlel{name = <<"inactive">>, - attrs = [{<<"xmlns">>, ?NS_CLIENT_STATE}]}}, - StateData) -> - fsm_next_state(session_established, StateData#state{csi_state = inactive}); -session_established({xmlstreamelement, El}, - StateData) -> - FromJID = StateData#state.jid, - case check_from(El, FromJID) of - 'invalid-from' -> - send_element(StateData, ?INVALID_FROM), - send_trailer(StateData), - {stop, normal, StateData}; - _NewEl -> - session_established2(El, StateData) - end; -%% We hibernate the process to reduce memory consumption after a -%% configurable activity timeout -session_established(timeout, StateData) -> - Options = [], - proc_lib:hibernate(?GEN_FSM, enter_loop, - [?MODULE, Options, session_established, StateData]), - fsm_next_state(session_established, StateData); -session_established({xmlstreamend, _Name}, StateData) -> - send_trailer(StateData), {stop, normal, StateData}; -session_established({xmlstreamerror, - <<"XML stanza is too big">> = E}, - StateData) -> - send_element(StateData, - ?POLICY_VIOLATION_ERR((StateData#state.lang), E)), - send_trailer(StateData), - {stop, normal, StateData}; -session_established({xmlstreamerror, _}, StateData) -> - send_element(StateData, ?INVALID_XML_ERR), - send_trailer(StateData), - {stop, normal, StateData}; -session_established(closed, #state{mgmt_state = active} = StateData) -> - fsm_next_state(wait_for_resume, StateData); -session_established(closed, StateData) -> - {stop, normal, StateData}. - -%% Process packets sent by user (coming from user on c2s XMPP -%% connection) -session_established2(El, StateData) -> - #xmlel{name = Name, attrs = Attrs} = El, - NewStateData = update_num_stanzas_in(StateData, El), - User = NewStateData#state.user, - Server = NewStateData#state.server, - FromJID = NewStateData#state.jid, - To = xml:get_attr_s(<<"to">>, Attrs), - ToJID = case To of - <<"">> -> jlib:make_jid(User, Server, <<"">>); - _ -> jlib:string_to_jid(To) - end, - NewEl1 = jlib:remove_attr(<<"xmlns">>, El), - NewEl = case xml:get_attr_s(<<"xml:lang">>, Attrs) of - <<"">> -> - case NewStateData#state.lang of - <<"">> -> NewEl1; - Lang -> - xml:replace_tag_attr(<<"xml:lang">>, Lang, NewEl1) - end; - _ -> NewEl1 - end, - NewState = case ToJID of - error -> - case xml:get_attr_s(<<"type">>, Attrs) of - <<"error">> -> NewStateData; - <<"result">> -> NewStateData; - _ -> - Err = jlib:make_error_reply(NewEl, - ?ERR_JID_MALFORMED), - send_packet(NewStateData, Err) - end; +%% Copies content of one c2s state to another. +%% This is needed for session migration from one pid to another. +-spec copy_state(state(), state()) -> state(). +copy_state(NewState, + #{jid := JID, resource := Resource, auth_module := AuthModule, + lserver := LServer, pres_a := PresA} = OldState) -> + State1 = case OldState of + #{pres_last := Pres, pres_timestamp := PresTS} -> + NewState#{pres_last => Pres, pres_timestamp => PresTS}; _ -> - case Name of - <<"presence">> -> - PresenceEl = - ejabberd_hooks:run_fold(c2s_update_presence, - Server, NewEl, - [User, Server]), - ejabberd_hooks:run(user_send_packet, Server, - [FromJID, ToJID, PresenceEl]), - case ToJID of - #jid{user = User, server = Server, - resource = <<"">>} -> - ?DEBUG("presence_update(~p,~n\t~p,~n\t~p)", - [FromJID, PresenceEl, NewStateData]), - presence_update(FromJID, PresenceEl, - NewStateData); - _ -> - presence_track(FromJID, ToJID, PresenceEl, - NewStateData) - end; - <<"iq">> -> - case jlib:iq_query_info(NewEl) of - #iq{xmlns = Xmlns} = IQ - when Xmlns == (?NS_PRIVACY); - Xmlns == (?NS_BLOCKING) -> - process_privacy_iq(FromJID, ToJID, IQ, - NewStateData); - _ -> - ejabberd_hooks:run(user_send_packet, Server, - [FromJID, ToJID, NewEl]), - check_privacy_route(FromJID, NewStateData, - FromJID, ToJID, NewEl), - NewStateData - end; - <<"message">> -> - ejabberd_hooks:run(user_send_packet, Server, - [FromJID, ToJID, NewEl]), - check_privacy_route(FromJID, NewStateData, FromJID, - ToJID, NewEl), - NewStateData; - _ -> NewStateData - end - end, - ejabberd_hooks:run(c2s_loop_debug, - [{xmlstreamelement, El}]), - fsm_next_state(session_established, NewState). + NewState + end, + Conn = get_conn_type(State1), + State2 = State1#{jid => JID, resource => Resource, + conn => Conn, + auth_module => AuthModule, + pres_a => PresA}, + ejabberd_hooks:run_fold(c2s_copy_session, LServer, State2, [OldState]). -wait_for_resume({xmlstreamelement, _El} = Event, StateData) -> - session_established(Event, StateData), - fsm_next_state(wait_for_resume, StateData); -wait_for_resume(timeout, StateData) -> - ?DEBUG("Timed out waiting for resumption of stream for ~s", - [jlib:jid_to_string(StateData#state.jid)]), - {stop, normal, StateData}; -wait_for_resume(Event, StateData) -> - ?DEBUG("Ignoring event while waiting for resumption: ~p", [Event]), - fsm_next_state(wait_for_resume, StateData). +-spec open_session(state()) -> {ok, state()} | state(). +open_session(#{user := U, server := S, resource := R, + sid := SID, ip := IP, auth_module := AuthModule} = State) -> + JID = jid:make(U, S, R), + State1 = change_shaper(State), + Conn = get_conn_type(State1), + State2 = State1#{conn => Conn, resource => R, jid => JID}, + Prio = case maps:get(pres_last, State, undefined) of + undefined -> undefined; + Pres -> get_priority_from_presence(Pres) + end, + Info = [{ip, IP}, {conn, Conn}, {auth_module, AuthModule}], + case State of + #{bind2_session_id := Tag} -> + ejabberd_sm:open_session(SID, U, S, R, Prio, Info, Tag); + _ -> + ejabberd_sm:open_session(SID, U, S, R, Prio, Info) + end, + xmpp_stream_in:establish(State2). -%%---------------------------------------------------------------------- -%% Func: StateName/3 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {reply, Reply, NextStateName, NextStateData} | -%% {reply, Reply, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} | -%% {stop, Reason, Reply, NewStateData} -%%---------------------------------------------------------------------- -%state_name(Event, From, StateData) -> -% Reply = ok, -% {reply, Reply, state_name, StateData}. - -%%---------------------------------------------------------------------- -%% Func: handle_event/3 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} -%%---------------------------------------------------------------------- -handle_event(_Event, StateName, StateData) -> - fsm_next_state(StateName, StateData). - -%%---------------------------------------------------------------------- -%% Func: handle_sync_event/4 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {reply, Reply, NextStateName, NextStateData} | -%% {reply, Reply, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} | -%% {stop, Reason, Reply, NewStateData} -%%---------------------------------------------------------------------- -handle_sync_event({get_presence}, _From, StateName, - StateData) -> - User = StateData#state.user, - PresLast = StateData#state.pres_last, - Show = get_showtag(PresLast), - Status = get_statustag(PresLast), - Resource = StateData#state.resource, - Reply = {User, Resource, Show, Status}, - fsm_reply(Reply, StateName, StateData); -handle_sync_event(get_subscribed, _From, StateName, - StateData) -> - Subscribed = (?SETS):to_list(StateData#state.pres_f), - {reply, Subscribed, StateName, StateData}; -handle_sync_event({resume_session, Time}, _From, _StateName, - StateData) when element(1, StateData#state.sid) == Time -> - %% The old session should be closed before the new one is opened, so we do - %% this here instead of leaving it to the terminate callback - ejabberd_sm:close_session(StateData#state.sid, - StateData#state.user, - StateData#state.server, - StateData#state.resource), - {stop, normal, {ok, StateData}, StateData#state{mgmt_state = resumed}}; -handle_sync_event({resume_session, _Time}, _From, StateName, - StateData) -> - {reply, {error, <<"Previous session not found">>}, StateName, StateData}; -handle_sync_event(_Event, _From, StateName, - StateData) -> - Reply = ok, fsm_reply(Reply, StateName, StateData). - -code_change(_OldVsn, StateName, StateData, _Extra) -> - {ok, StateName, StateData}. - -%%---------------------------------------------------------------------- -%% Func: handle_info/3 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} -%%---------------------------------------------------------------------- -handle_info({send_text, Text}, StateName, StateData) -> - send_text(StateData, Text), - ejabberd_hooks:run(c2s_loop_debug, [Text]), - fsm_next_state(StateName, StateData); -handle_info(replaced, StateName, StateData) -> - Lang = StateData#state.lang, - Xmlelement = ?SERRT_CONFLICT(Lang, <<"Replaced by new connection">>), - handle_info({kick, replaced, Xmlelement}, StateName, StateData); -handle_info(kick, StateName, StateData) -> - Lang = StateData#state.lang, - Xmlelement = ?SERRT_POLICY_VIOLATION(Lang, <<"has been kicked">>), - handle_info({kick, kicked_by_admin, Xmlelement}, StateName, StateData); -handle_info({kick, Reason, Xmlelement}, _StateName, StateData) -> - send_element(StateData, Xmlelement), - send_trailer(StateData), - {stop, normal, - StateData#state{authenticated = Reason}}; -handle_info({route, _From, _To, {broadcast, Data}}, - StateName, StateData) -> - ?DEBUG("broadcast~n~p~n", [Data]), - case Data of - {item, IJID, ISubscription} -> - fsm_next_state(StateName, - roster_change(IJID, ISubscription, StateData)); - {exit, Reason} -> - Lang = StateData#state.lang, - send_element(StateData, ?SERRT_CONFLICT(Lang, Reason)), - catch send_trailer(StateData), - {stop, normal, StateData}; - {privacy_list, PrivList, PrivListName} -> - case ejabberd_hooks:run_fold(privacy_updated_list, - StateData#state.server, - false, - [StateData#state.privacy_list, - PrivList]) of - false -> - fsm_next_state(StateName, StateData); - NewPL -> - PrivPushIQ = #iq{type = set, - id = <<"push", - (randoms:get_string())/binary>>, - sub_el = - [#xmlel{name = <<"query">>, - attrs = [{<<"xmlns">>, - ?NS_PRIVACY}], - children = - [#xmlel{name = <<"list">>, - attrs = [{<<"name">>, - PrivListName}], - children = []}]}]}, - PrivPushEl = jlib:replace_from_to( - jlib:jid_remove_resource(StateData#state.jid), - StateData#state.jid, - jlib:iq_to_xml(PrivPushIQ)), - NewState = send_stanza(StateData, PrivPushEl), - fsm_next_state(StateName, - NewState#state{privacy_list = NewPL}) - end; - {blocking, What} -> - NewState = route_blocking(What, StateData), - fsm_next_state(StateName, NewState); - _ -> - fsm_next_state(StateName, StateData) - end; -%% Process Packets that are to be send to the user -handle_info({route, From, To, - #xmlel{name = Name, attrs = Attrs, children = Els} = Packet}, - StateName, StateData) -> - {Pass, NewAttrs, NewState} = case Name of - <<"presence">> -> - State = - ejabberd_hooks:run_fold(c2s_presence_in, - StateData#state.server, - StateData, - [{From, To, - Packet}]), - case xml:get_attr_s(<<"type">>, Attrs) of - <<"probe">> -> - LFrom = jlib:jid_tolower(From), - LBFrom = - jlib:jid_remove_resource(LFrom), - NewStateData = case - (?SETS):is_element(LFrom, - State#state.pres_a) - orelse - (?SETS):is_element(LBFrom, - State#state.pres_a) - of - true -> State; - false -> - case - (?SETS):is_element(LFrom, - State#state.pres_f) - of - true -> - A = - (?SETS):add_element(LFrom, - State#state.pres_a), - State#state{pres_a - = - A}; - false -> - case - (?SETS):is_element(LBFrom, - State#state.pres_f) - of - true -> - A = - (?SETS):add_element(LBFrom, - State#state.pres_a), - State#state{pres_a - = - A}; - false -> - State - end - end - end, - process_presence_probe(From, To, - NewStateData), - {false, Attrs, NewStateData}; - <<"error">> -> - NewA = - remove_element(jlib:jid_tolower(From), - State#state.pres_a), - {true, Attrs, - State#state{pres_a = NewA}}; - <<"subscribe">> -> - SRes = is_privacy_allow(State, - From, To, - Packet, - in), - {SRes, Attrs, State}; - <<"subscribed">> -> - SRes = is_privacy_allow(State, - From, To, - Packet, - in), - {SRes, Attrs, State}; - <<"unsubscribe">> -> - SRes = is_privacy_allow(State, - From, To, - Packet, - in), - {SRes, Attrs, State}; - <<"unsubscribed">> -> - SRes = is_privacy_allow(State, - From, To, - Packet, - in), - {SRes, Attrs, State}; - _ -> - case privacy_check_packet(State, - From, To, - Packet, - in) - of - allow -> - LFrom = - jlib:jid_tolower(From), - LBFrom = - jlib:jid_remove_resource(LFrom), - case - (?SETS):is_element(LFrom, - State#state.pres_a) - orelse - (?SETS):is_element(LBFrom, - State#state.pres_a) - of - true -> - {true, Attrs, State}; - false -> - case - (?SETS):is_element(LFrom, - State#state.pres_f) - of - true -> - A = - (?SETS):add_element(LFrom, - State#state.pres_a), - {true, Attrs, - State#state{pres_a - = - A}}; - false -> - case - (?SETS):is_element(LBFrom, - State#state.pres_f) - of - true -> - A = - (?SETS):add_element(LBFrom, - State#state.pres_a), - {true, - Attrs, - State#state{pres_a - = - A}}; - false -> - {true, - Attrs, - State} - end - end - end; - deny -> {false, Attrs, State} - end - end; - <<"iq">> -> - IQ = jlib:iq_query_info(Packet), - case IQ of - #iq{xmlns = ?NS_LAST} -> - LFrom = jlib:jid_tolower(From), - LBFrom = - jlib:jid_remove_resource(LFrom), - HasFromSub = - ((?SETS):is_element(LFrom, - StateData#state.pres_f) - orelse - (?SETS):is_element(LBFrom, - StateData#state.pres_f)) - andalso - is_privacy_allow(StateData, - To, From, - #xmlel{name - = - <<"presence">>, - attrs - = - [], - children - = - []}, - out), - case HasFromSub of - true -> - case - privacy_check_packet(StateData, - From, - To, - Packet, - in) - of - allow -> - {true, Attrs, - StateData}; - deny -> - {false, Attrs, - StateData} - end; - _ -> - Err = - jlib:make_error_reply(Packet, - ?ERR_FORBIDDEN), - ejabberd_router:route(To, - From, - Err), - {false, Attrs, StateData} - end; - IQ - when is_record(IQ, iq) or - (IQ == reply) -> - case - privacy_check_packet(StateData, - From, To, - Packet, in) - of - allow -> - {true, Attrs, StateData}; - deny when is_record(IQ, iq) -> - Err = - jlib:make_error_reply(Packet, - ?ERR_SERVICE_UNAVAILABLE), - ejabberd_router:route(To, - From, - Err), - {false, Attrs, StateData}; - deny when IQ == reply -> - {false, Attrs, StateData} - end; - IQ - when (IQ == invalid) or - (IQ == not_iq) -> - {false, Attrs, StateData} - end; - <<"message">> -> - case privacy_check_packet(StateData, - From, To, - Packet, in) - of - allow -> {true, Attrs, StateData}; - deny -> {false, Attrs, StateData} - end; - _ -> {true, Attrs, StateData} - end, - if Pass == exit -> - %% When Pass==exit, NewState contains a string instead of a #state{} - Lang = StateData#state.lang, - send_element(StateData, ?SERRT_CONFLICT(Lang, NewState)), - send_trailer(StateData), - {stop, normal, StateData}; - Pass -> - Attrs2 = - jlib:replace_from_to_attrs(jlib:jid_to_string(From), - jlib:jid_to_string(To), NewAttrs), - FixedPacket = #xmlel{name = Name, attrs = Attrs2, children = Els}, - FinalState = - case ejabberd_hooks:run_fold(c2s_filter_packet_in, - NewState#state.server, FixedPacket, - [NewState#state.jid, From, To]) - of - drop -> - NewState; - FinalPacket = #xmlel{} -> - SentState = send_packet(NewState, FinalPacket), - ejabberd_hooks:run(user_receive_packet, - SentState#state.server, - [SentState#state.jid, From, To, - FinalPacket]), - SentState - end, - ejabberd_hooks:run(c2s_loop_debug, [{route, From, To, Packet}]), - fsm_next_state(StateName, FinalState); - true -> - ejabberd_hooks:run(c2s_loop_debug, [{route, From, To, Packet}]), - fsm_next_state(StateName, NewState) - end; -handle_info({'DOWN', Monitor, _Type, _Object, _Info}, - _StateName, StateData) - when Monitor == StateData#state.socket_monitor -> - if StateData#state.mgmt_state == active; - StateData#state.mgmt_state == pending -> - fsm_next_state(wait_for_resume, StateData); +%%%=================================================================== +%%% Hooks +%%%=================================================================== +process_info(#{lserver := LServer} = State, {route, Packet}) -> + {Pass, State1} = case Packet of + #presence{} -> + process_presence_in(State, Packet); + #message{} -> + process_message_in(State, Packet); + #iq{} -> + process_iq_in(State, Packet) + end, + if Pass -> + {Packet1, State2} = ejabberd_hooks:run_fold( + user_receive_packet, LServer, + {Packet, State1}, []), + case Packet1 of + drop -> State2; + _ -> send(State2, Packet1) + end; true -> - {stop, normal, StateData} + State1 end; -handle_info(system_shutdown, StateName, StateData) -> - case StateName of - wait_for_stream -> - send_header(StateData, ?MYNAME, <<"1.0">>, <<"en">>), - send_element(StateData, ?SERR_SYSTEM_SHUTDOWN), - send_trailer(StateData), - ok; - _ -> - send_element(StateData, ?SERR_SYSTEM_SHUTDOWN), - send_trailer(StateData), - ok - end, - {stop, normal, StateData}; -handle_info({force_update_presence, LUser}, StateName, - #state{user = LUser, server = LServer} = StateData) -> - NewStateData = case StateData#state.pres_last of - #xmlel{name = <<"presence">>} -> - PresenceEl = - ejabberd_hooks:run_fold(c2s_update_presence, - LServer, - StateData#state.pres_last, - [LUser, LServer]), - StateData2 = StateData#state{pres_last = PresenceEl}, - presence_update(StateData2#state.jid, PresenceEl, - StateData2), - StateData2; - _ -> StateData - end, - fsm_next_state(StateName, NewStateData); -handle_info({send_filtered, Feature, From, To, Packet}, StateName, StateData) -> - Drop = ejabberd_hooks:run_fold(c2s_filter_packet, StateData#state.server, - true, [StateData#state.server, StateData, - Feature, To, Packet]), - NewStateData = if Drop -> - ?DEBUG("Dropping packet from ~p to ~p", - [jlib:jid_to_string(From), - jlib:jid_to_string(To)]), - StateData; - true -> - FinalPacket = jlib:replace_from_to(From, To, Packet), - case StateData#state.jid of - To -> - case privacy_check_packet(StateData, From, To, - FinalPacket, in) of - deny -> - StateData; - allow -> - send_stanza(StateData, FinalPacket) - end; - _ -> - ejabberd_router:route(From, To, FinalPacket), - StateData - end - end, - fsm_next_state(StateName, NewStateData); -handle_info({broadcast, Type, From, Packet}, StateName, StateData) -> - Recipients = ejabberd_hooks:run_fold( - c2s_broadcast_recipients, StateData#state.server, - [], - [StateData#state.server, StateData, Type, From, Packet]), - lists:foreach( - fun(USR) -> - ejabberd_router:route( - From, jlib:make_jid(USR), Packet) - end, lists:usort(Recipients)), - fsm_next_state(StateName, StateData); -handle_info(Info, StateName, StateData) -> - ?ERROR_MSG("Unexpected info: ~p", [Info]), - fsm_next_state(StateName, StateData). +process_info(State, reset_vcard_xupdate_resend_presence) -> + case maps:get(pres_last, State, error) of + error -> State; + Pres -> + Pres2 = xmpp:remove_subtag(Pres, #vcard_xupdate{}), + process_self_presence(State#{pres_last => Pres2}, Pres2) + end; +process_info(#{jid := JID} = State, {resend_presence, To}) -> + case maps:get(pres_last, State, error) of + error -> State; + Pres when To == undefined -> + process_self_presence(State, Pres); + Pres when To#jid.luser == JID#jid.luser andalso + To#jid.lserver == JID#jid.lserver andalso + To#jid.lresource == <<"">> -> + process_self_presence(State, Pres); + Pres -> + process_presence_out(State, xmpp:set_to(Pres, To)) + end; +process_info(State, Info) -> + ?WARNING_MSG("Unexpected info: ~p", [Info]), + State. +handle_unexpected_call(State, From, Msg) -> + ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Msg]), + State. -%%---------------------------------------------------------------------- -%% Func: print_state/1 -%% Purpose: Prepare the state to be printed on error log -%% Returns: State to print -%%---------------------------------------------------------------------- -print_state(State = #state{pres_t = T, pres_f = F, pres_a = A}) -> - State#state{pres_t = {pres_t, ?SETS:size(T)}, - pres_f = {pres_f, ?SETS:size(F)}, - pres_a = {pres_a, ?SETS:size(A)} - }. - -%%---------------------------------------------------------------------- -%% Func: terminate/3 -%% Purpose: Shutdown the fsm -%% Returns: any -%%---------------------------------------------------------------------- -terminate(_Reason, StateName, StateData) -> - case StateData#state.mgmt_state of - resumed -> - ?INFO_MSG("Closing former stream of resumed session for ~s", - [jlib:jid_to_string(StateData#state.jid)]); - _ -> - if StateName == session_established; - StateName == wait_for_resume -> - case StateData#state.authenticated of - replaced -> - ?INFO_MSG("(~w) Replaced session for ~s", - [StateData#state.socket, - jlib:jid_to_string(StateData#state.jid)]), - From = StateData#state.jid, - Packet = #xmlel{name = <<"presence">>, - attrs = [{<<"type">>, <<"unavailable">>}], - children = - [#xmlel{name = <<"status">>, attrs = [], - children = - [{xmlcdata, - <<"Replaced by new connection">>}]}]}, - ejabberd_sm:close_session_unset_presence(StateData#state.sid, - StateData#state.user, - StateData#state.server, - StateData#state.resource, - <<"Replaced by new connection">>), - presence_broadcast(StateData, From, - StateData#state.pres_a, Packet), - handle_unacked_stanzas(StateData); - _ -> - ?INFO_MSG("(~w) Close session for ~s", - [StateData#state.socket, - jlib:jid_to_string(StateData#state.jid)]), - EmptySet = (?SETS):new(), - case StateData of - #state{pres_last = undefined, pres_a = EmptySet} -> - ejabberd_sm:close_session(StateData#state.sid, - StateData#state.user, - StateData#state.server, - StateData#state.resource); - _ -> - From = StateData#state.jid, - Packet = #xmlel{name = <<"presence">>, - attrs = [{<<"type">>, <<"unavailable">>}], - children = []}, - ejabberd_sm:close_session_unset_presence(StateData#state.sid, - StateData#state.user, - StateData#state.server, - StateData#state.resource, - <<"">>), - presence_broadcast(StateData, From, - StateData#state.pres_a, Packet) - end, - handle_unacked_stanzas(StateData) - end, - bounce_messages(); - true -> - ok - end - end, - (StateData#state.sockmod):close(StateData#state.socket), +handle_unexpected_cast(State, Msg) -> + ?WARNING_MSG("Unexpected cast: ~p", [Msg]), + State. + +c2s_handle_bind({<<"">>, {ok, State}}) -> + {new_uniq_id(), {ok, State}}; +c2s_handle_bind(Acc) -> + Acc. + +reject_unauthenticated_packet(State, _Pkt) -> + Err = xmpp:serr_not_authorized(), + send(State, Err). + +process_auth_result(#{sasl_mech := Mech, auth_module := AuthModule, + socket := Socket, ip := IP, lserver := LServer} = State, + true, User) -> + misc:set_proc_label({?MODULE, User, LServer}), + ?INFO_MSG("(~ts) Accepted c2s ~ts authentication for ~ts@~ts by ~ts backend from ~ts", + [xmpp_socket:pp(Socket), Mech, User, LServer, + ejabberd_auth:backend_type(AuthModule), + ejabberd_config:may_hide_data(misc:ip_to_list(IP))]), + State; +process_auth_result(#{sasl_mech := Mech, + socket := Socket, ip := IP, lserver := LServer} = State, + {false, Reason}, User) -> + ?WARNING_MSG("(~ts) Failed c2s ~ts authentication ~tsfrom ~ts: ~ts", + [xmpp_socket:pp(Socket), Mech, + if User /= <<"">> -> ["for ", User, "@", LServer, " "]; + true -> "" + end, + ejabberd_config:may_hide_data(misc:ip_to_list(IP)), Reason]), + State. + +process_closed(State, Reason) -> + stop_async(self()), + State#{stop_reason => Reason}. + +process_terminated(#{sid := SID, jid := JID, user := U, server := S, resource := R} = State, + Reason) -> + Status = format_reason(State, Reason), + ?INFO_MSG("(~ts) Closing c2s session for ~ts: ~ts", + [case maps:find(socket, State) of + {ok, Socket} -> xmpp_socket:pp(Socket); + _ -> <<"unknown">> + end, jid:encode(JID), Status]), + Pres = #presence{type = unavailable, + from = JID, + to = jid:remove_resource(JID)}, + State1 = case maps:is_key(pres_last, State) of + true -> + ejabberd_sm:close_session_unset_presence(SID, U, S, R, + Status), + broadcast_presence_unavailable(State, Pres, true); + false -> + ejabberd_sm:close_session(SID, U, S, R), + broadcast_presence_unavailable(State, Pres, false) + end, + bounce_message_queue(SID, JID), + State1; +process_terminated(#{stop_reason := {tls, _}} = State, Reason) -> + ?WARNING_MSG("(~ts) Failed to secure c2s connection: ~ts", + [case maps:find(socket, State) of + {ok, Socket} -> xmpp_socket:pp(Socket); + _ -> <<"unknown">> + end, format_reason(State, Reason)]), + State; +process_terminated(State, _Reason) -> + State. + +%%%=================================================================== +%%% xmpp_stream_in callbacks +%%%=================================================================== +tls_options(#{lserver := LServer, tls_options := DefaultOpts, + stream_encrypted := Encrypted}) -> + TLSOpts1 = case {Encrypted, proplists:get_value(certfile, DefaultOpts)} of + {true, CertFile} when CertFile /= undefined -> DefaultOpts; + {_, _} -> + case ejabberd_pkix:get_certfile(LServer) of + error -> DefaultOpts; + {ok, CertFile} -> + lists:keystore(certfile, 1, DefaultOpts, + {certfile, CertFile}) + end + end, + TLSOpts2 = case ejabberd_option:c2s_ciphers(LServer) of + undefined -> TLSOpts1; + Ciphers -> lists:keystore(ciphers, 1, TLSOpts1, + {ciphers, Ciphers}) + end, + TLSOpts3 = case ejabberd_option:c2s_protocol_options(LServer) of + undefined -> TLSOpts2; + ProtoOpts -> lists:keystore(protocol_options, 1, TLSOpts2, + {protocol_options, ProtoOpts}) + end, + TLSOpts4 = case ejabberd_option:c2s_dhfile(LServer) of + undefined -> TLSOpts3; + DHFile -> lists:keystore(dhfile, 1, TLSOpts3, + {dhfile, DHFile}) + end, + TLSOpts5 = case ejabberd_option:c2s_cafile(LServer) of + undefined -> TLSOpts4; + CAFile -> lists:keystore(cafile, 1, TLSOpts4, + {cafile, CAFile}) + end, + case ejabberd_option:c2s_tls_compression(LServer) of + undefined -> TLSOpts5; + false -> [compression_none | TLSOpts5]; + true -> lists:delete(compression_none, TLSOpts5) + end. + +tls_required(#{tls_required := TLSRequired}) -> + TLSRequired. + +tls_enabled(#{tls_enabled := TLSEnabled, + tls_required := TLSRequired, + tls_verify := TLSVerify}) -> + TLSEnabled or TLSRequired or TLSVerify. + +allow_unencrypted_sasl2(#{allow_unencrypted_sasl2 := AllowUnencryptedSasl2}) -> + AllowUnencryptedSasl2. + +compress_methods(#{zlib := true}) -> + [<<"zlib">>]; +compress_methods(_) -> + []. + +unauthenticated_stream_features(#{lserver := LServer}) -> + ejabberd_hooks:run_fold(c2s_pre_auth_features, LServer, [], [LServer]). + +authenticated_stream_features(#{lserver := LServer}) -> + ejabberd_hooks:run_fold(c2s_post_auth_features, LServer, [], [LServer]). + +inline_stream_features(#{lserver := LServer} = State) -> + ejabberd_hooks:run_fold(c2s_inline_features, LServer, {[], [], []}, [LServer, State]). + +sasl_mechanisms(Mechs, #{lserver := LServer, stream_encrypted := Encrypted} = State) -> + Type = ejabberd_auth:store_type(LServer), + Mechs1 = ejabberd_option:disable_sasl_mechanisms(LServer), + + {Digest, ShaAv, Sha256Av, Sha512Av} = + case ejabberd_option:auth_stored_password_types(LServer) of + [] -> + ScramHash = ejabberd_option:auth_scram_hash(LServer), + {Type == plain, + Type == plain orelse (Type == scram andalso ScramHash == sha), + Type == plain orelse (Type == scram andalso ScramHash == sha256), + Type == plain orelse (Type == scram andalso ScramHash == sha512)}; + Methods -> + HasPlain = lists:member(plain, Methods), + {HasPlain, + HasPlain orelse lists:member(scram_sha1, Methods), + HasPlain orelse lists:member(scram_sha256, Methods), + HasPlain orelse lists:member(scram_sha512, Methods)} + end, + %% I re-created it from cyrsasl ets magic, but I think it's wrong + %% TODO: need to check before 18.09 release + Mechs2 = lists:filter( + fun(<<"ANONYMOUS">>) -> + ejabberd_auth_anonymous:is_sasl_anonymous_enabled(LServer); + (<<"DIGEST-MD5">>) -> Digest; + (<<"SCRAM-SHA-1">>) -> ShaAv; + (<<"SCRAM-SHA-1-PLUS">>) -> ShaAv andalso Encrypted; + (<<"SCRAM-SHA-256">>) -> Sha256Av; + (<<"SCRAM-SHA-256-PLUS">>) -> Sha256Av andalso Encrypted; + (<<"SCRAM-SHA-512">>) -> Sha512Av; + (<<"SCRAM-SHA-512-PLUS">>) -> Sha512Av andalso Encrypted; + (<<"PLAIN">>) -> true; + (<<"X-OAUTH2">>) -> [ejabberd_auth_anonymous] /= ejabberd_auth:auth_modules(LServer); + (<<"EXTERNAL">>) -> maps:get(tls_verify, State, false); + (_) -> false + end, Mechs -- Mechs1), + case ejabberd_option:auth_password_types_hidden_in_sasl1() of + [] -> Mechs2; + List -> + Mechs3 = lists:foldl( + fun(plain, Acc) -> Acc -- [<<"PLAIN">>]; + (scram_sha1, Acc) -> Acc -- [<<"SCRAM-SHA-1">>, <<"SCRAM-SHA-1-PLUS">>]; + (scram_sha256, Acc) -> Acc -- [<<"SCRAM-SHA-256">>, <<"SCRAM-SHA-256-PLUS">>]; + (scram_sha512, Acc) -> Acc -- [<<"SCRAM-SHA-512">>, <<"SCRAM-SHA-512-PLUS">>] + end, Mechs2, List), + {Mechs3, Mechs2} + end. + +sasl_options(#{lserver := LServer}) -> + case ejabberd_option:disable_sasl_scram_downgrade_protection(LServer) of + true -> [{scram_downgrade_protection, false}]; + _ -> [] + end. + +get_password_fun(_Mech, #{lserver := LServer}) -> + fun(U) -> + ejabberd_auth:get_password_with_authmodule(U, LServer) + end. + +check_password_fun(<<"X-OAUTH2">>, #{lserver := LServer}) -> + fun(User, _AuthzId, Token) -> + case ejabberd_oauth:check_token( + User, LServer, [<<"sasl_auth">>], Token) of + true -> {true, ejabberd_oauth}; + _ -> {false, ejabberd_oauth} + end + end; +check_password_fun(_Mech, #{lserver := LServer}) -> + fun(U, AuthzId, P) -> + ejabberd_auth:check_password_with_authmodule(U, AuthzId, LServer, P) + end. + +check_password_digest_fun(_Mech, #{lserver := LServer}) -> + fun(U, AuthzId, P, D, DG) -> + ejabberd_auth:check_password_with_authmodule(U, AuthzId, LServer, P, D, DG) + end. + +get_fast_tokens_fun(_Mech, #{lserver := LServer}) -> + fun(User, UA) -> + case gen_mod:is_loaded(LServer, mod_auth_fast) of + false -> false; + _ -> mod_auth_fast:get_tokens(LServer, User, UA) + end + end. + +fast_mechanisms(#{lserver := LServer}) -> + case gen_mod:is_loaded(LServer, mod_auth_fast) of + false -> []; + _ -> mod_auth_fast:get_mechanisms(LServer) + end. + +bind( + R, + #{ + user := U, + server := S, + lserver := LServer, + access := Access, + lang := Lang, + socket := Socket, + ip := IP + }=State +) -> + case ejabberd_hooks:run_fold(c2s_handle_bind, LServer, {R, {ok, State}}, []) of + {R2, {ok, State2}} -> + case resource_conflict_action(U, S, R2) of + closenew -> + {error, xmpp:err_conflict(), State2}; + {accept_resource, Resource} -> + JID = jid:make(U, S, Resource), + case acl:match_rule(LServer, Access, #{usr => jid:split(JID), ip => IP}) of + allow -> + State3 = open_session( + State2#{resource => Resource, sid => ejabberd_sm:make_sid()} + ), + State4 = ejabberd_hooks:run_fold( + c2s_session_opened, LServer, State3, [] + ), + ?INFO_MSG( + "(~ts) Opened c2s session for ~ts", [xmpp_socket:pp(Socket), jid:encode(JID)] + ), + {ok, State4}; + deny -> + ejabberd_hooks:run(forbidden_session_hook, LServer, [JID]), + ?WARNING_MSG( + "(~ts) Forbidden c2s session for ~ts", + [xmpp_socket:pp(Socket), jid:encode(JID)] + ), + Txt = ?T("Access denied by service policy"), + {error, xmpp:err_not_allowed(Txt, Lang), State2} + end + end; + {R2, {error, XmppErr, _State2}=Err} -> + case XmppErr of + #stanza_error{reason = 'not-allowed'} -> + JID = jid:make(U, S, R2), + ejabberd_hooks:run(forbidden_session_hook, LServer, [JID]), + ?WARNING_MSG( + "(~ts) Forbidden c2s session for ~ts", + [xmpp_socket:pp(Socket), jid:encode(JID)] + ); + _ -> + ok + end, + Err + end. + +handle_stream_start(StreamStart, #{lserver := LServer} = State) -> + case ejabberd_router:is_my_host(LServer) of + false -> + send(State#{lserver => ejabberd_config:get_myname()}, xmpp:serr_host_unknown()); + true -> + State1 = change_shaper(State), + Opts = ejabberd_config:codec_options(), + State2 = State1#{codec_options => Opts}, + ejabberd_hooks:run_fold( + c2s_stream_started, LServer, State2, [StreamStart]) + end. + +handle_stream_end(Reason, #{lserver := LServer} = State) -> + State1 = State#{stop_reason => Reason}, + ejabberd_hooks:run_fold(c2s_closed, LServer, State1, [Reason]). + +handle_auth_success(User, _Mech, AuthModule, + #{lserver := LServer} = State) -> + State1 = State#{auth_module => AuthModule}, + ejabberd_hooks:run_fold(c2s_auth_result, LServer, State1, [true, User]). + +handle_auth_failure(User, _Mech, Reason, + #{lserver := LServer} = State) -> + ejabberd_hooks:run_fold(c2s_auth_result, LServer, State, [{false, Reason}, User]). + +handle_unbinded_packet(Pkt, #{lserver := LServer} = State) -> + ejabberd_hooks:run_fold(c2s_unbinded_packet, LServer, State, [Pkt]). + +handle_unauthenticated_packet(Pkt, #{lserver := LServer} = State) -> + ejabberd_hooks:run_fold(c2s_unauthenticated_packet, LServer, State, [Pkt]). + +handle_authenticated_packet(Pkt, #{lserver := LServer} = State) when not ?is_stanza(Pkt) -> + ejabberd_hooks:run_fold(c2s_authenticated_packet, + LServer, State, [Pkt]); +handle_authenticated_packet(Pkt, #{lserver := LServer, jid := JID, + ip := {IP, _}} = State) -> + Pkt1 = xmpp:put_meta(Pkt, ip, IP), + State1 = ejabberd_hooks:run_fold(c2s_authenticated_packet, + LServer, State, [Pkt1]), + #jid{luser = LUser} = JID, + {Pkt2, State2} = ejabberd_hooks:run_fold( + user_send_packet, LServer, {Pkt1, State1}, []), + case Pkt2 of + drop -> + State2; + #iq{type = set, sub_els = [_]} -> + try xmpp:try_subtag(Pkt2, #xmpp_session{}) of + #xmpp_session{} -> + % It seems that some client are expecting to have response + % to session request be sent from server jid, let's make + % sure it is that. + Pkt3 = xmpp:set_to(Pkt2, jid:make(<<>>, LServer, <<>>)), + send(State2, xmpp:make_iq_result(Pkt3)); + _ -> + check_privacy_then_route(State2, Pkt2) + catch _:{xmpp_codec, Why} -> + Txt = xmpp:io_format_error(Why), + Lang = maps:get(lang, State), + Err = xmpp:err_bad_request(Txt, Lang), + send_error(State2, Pkt2, Err) + end; + #presence{to = #jid{luser = LUser, lserver = LServer, + lresource = <<"">>}} -> + process_self_presence(State2, Pkt2); + #presence{} -> + process_presence_out(State2, Pkt2); + _ -> + check_privacy_then_route(State2, Pkt2) + end. + +handle_cdata(Data, #{lserver := LServer} = State) -> + ejabberd_hooks:run_fold(c2s_handle_cdata, LServer, + State, [Data]). + +handle_sasl2_inline(Els, #{lserver := LServer} = State) -> + ejabberd_hooks:run_fold(c2s_handle_sasl2_inline, LServer, + {State, Els, []}, []). + +handle_sasl2_inline_post(Els, Results, #{lserver := LServer} = State) -> + ejabberd_hooks:run_fold(c2s_handle_sasl2_inline_post, LServer, + State, [Els, Results]). + +handle_bind2_inline(Els, #{lserver := LServer} = State) -> + ejabberd_hooks:run_fold(c2s_handle_bind2_inline, LServer, + {State, Els, []}, []). + +handle_bind2_inline_post(Els, Results, #{lserver := LServer} = State) -> + ejabberd_hooks:run_fold(c2s_handle_bind2_inline_post, LServer, + State, [Els, Results]). + +handle_sasl2_task_next(Task, Els, InlineEls, #{lserver := LServer} = State) -> + ejabberd_hooks:run_fold(c2s_handle_sasl2_task_next, LServer, + {abort, State}, [Task, Els, InlineEls]). + +handle_sasl2_task_data(Els, InlineEls, #{lserver := LServer} = State) -> + ejabberd_hooks:run_fold(c2s_handle_sasl2_task_data, LServer, + {abort, State}, [Els, InlineEls]). + +handle_recv(El, Pkt, #{lserver := LServer} = State) -> + ejabberd_hooks:run_fold(c2s_handle_recv, LServer, State, [El, Pkt]). + +handle_send(Pkt, Result, #{lserver := LServer} = State) -> + ejabberd_hooks:run_fold(c2s_handle_send, LServer, State, [Pkt, Result]). + +init([State, Opts]) -> + Access = proplists:get_value(access, Opts, all), + Shaper = proplists:get_value(shaper, Opts, none), + TLSOpts1 = lists:filter( + fun({certfile, _}) -> true; + ({ciphers, _}) -> true; + ({dhfile, _}) -> true; + ({cafile, _}) -> true; + ({protocol_options, _}) -> true; + (_) -> false + end, Opts), + TLSOpts2 = case proplists:get_bool(tls_compression, Opts) of + false -> [compression_none | TLSOpts1]; + true -> TLSOpts1 + end, + TLSEnabled = proplists:get_bool(starttls, Opts), + TLSRequired = proplists:get_bool(starttls_required, Opts), + TLSVerify = proplists:get_bool(tls_verify, Opts), + AllowUnencryptedSasl2 = proplists:get_bool(allow_unencrypted_sasl2, Opts), + Zlib = proplists:get_bool(zlib, Opts), + Timeout = ejabberd_option:negotiation_timeout(), + State1 = State#{tls_options => TLSOpts2, + tls_required => TLSRequired, + tls_enabled => TLSEnabled, + tls_verify => TLSVerify, + allow_unencrypted_sasl2 => AllowUnencryptedSasl2, + pres_a => ?SETS:new(), + zlib => Zlib, + lang => ejabberd_option:language(), + server => ejabberd_config:get_myname(), + lserver => ejabberd_config:get_myname(), + access => Access, + shaper => Shaper}, + State2 = xmpp_stream_in:set_timeout(State1, Timeout), + misc:set_proc_label({?MODULE, init_state}), + ejabberd_hooks:run_fold(c2s_init, {ok, State2}, [Opts]). + +handle_call(get_presence, From, #{jid := JID} = State) -> + Pres = case maps:get(pres_last, State, error) of + error -> + BareJID = jid:remove_resource(JID), + #presence{from = JID, to = BareJID, type = unavailable}; + P -> P + end, + reply(From, Pres), + State; +handle_call({set_presence, Pres}, From, State) -> + reply(From, ok), + process_self_presence(State, Pres); +handle_call(Request, From, #{lserver := LServer} = State) -> + ejabberd_hooks:run_fold( + c2s_handle_call, LServer, State, [Request, From]). + +handle_cast(Msg, #{lserver := LServer} = State) -> + ejabberd_hooks:run_fold(c2s_handle_cast, LServer, State, [Msg]). + +handle_info(Info, #{lserver := LServer} = State) -> + ejabberd_hooks:run_fold(c2s_handle_info, LServer, State, [Info]). + +terminate(Reason, #{lserver := LServer} = State) -> + ejabberd_hooks:run_fold(c2s_terminated, LServer, State, [Reason]). + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +-spec process_iq_in(state(), iq()) -> {boolean(), state()}. +process_iq_in(State, #iq{} = IQ) -> + case privacy_check_packet(State, IQ, in) of + allow -> + {true, State}; + deny -> + ejabberd_router:route_error(IQ, xmpp:err_service_unavailable()), + {false, State} + end. + +-spec process_message_in(state(), message()) -> {boolean(), state()}. +process_message_in(State, #message{type = T} = Msg) -> + %% This function should be as simple as process_iq_in/2, + %% however, we don't route errors to MUC rooms in order + %% to avoid kicking us, because having a MUC room's JID blocked + %% most likely means having only some particular participant + %% blocked, i.e. room@conference.server.org/participant. + case privacy_check_packet(State, Msg, in) of + allow -> + {true, State}; + deny when T == groupchat; T == headline -> + {false, State}; + deny -> + case xmpp:has_subtag(Msg, #muc_user{}) of + true -> + ok; + false -> + ejabberd_router:route_error( + Msg, xmpp:err_service_unavailable()) + end, + {false, State} + end. + +-spec process_presence_in(state(), presence()) -> {boolean(), state()}. +process_presence_in(#{lserver := LServer, pres_a := PresA} = State0, + #presence{from = From, type = T} = Pres) -> + State = ejabberd_hooks:run_fold(c2s_presence_in, LServer, State0, [Pres]), + case T of + probe -> + route_probe_reply(From, State), + {false, State}; + error -> + A = ?SETS:del_element(jid:tolower(From), PresA), + {true, State#{pres_a => A}}; + _ -> + case privacy_check_packet(State, Pres, in) of + allow -> + {true, State}; + deny -> + {false, State} + end + end. + +-spec route_probe_reply(jid(), state()) -> ok. +route_probe_reply(From, #{jid := To, + pres_last := LastPres, + pres_timestamp := TS} = State) -> + {LUser, LServer, LResource} = jid:tolower(To), + IsAnotherResource = case jid:tolower(From) of + {LUser, LServer, R} when R /= LResource -> true; + _ -> false + end, + Subscription = get_subscription(To, From), + if IsAnotherResource orelse + Subscription == both orelse Subscription == from -> + Packet = xmpp:set_from_to(LastPres, To, From), + Packet2 = misc:add_delay_info(Packet, To, TS), + case privacy_check_packet(State, Packet2, out) of + deny -> + ok; + allow -> + ejabberd_hooks:run(presence_probe_hook, + LServer, + [From, To, self()]), + ejabberd_router:route(Packet2) + end; + true -> + ok + end; +route_probe_reply(_, _) -> ok. -%%%---------------------------------------------------------------------- -%%% Internal functions -%%%---------------------------------------------------------------------- - -change_shaper(StateData, JID) -> - Shaper = acl:match_rule(StateData#state.server, - StateData#state.shaper, JID), - (StateData#state.sockmod):change_shaper(StateData#state.socket, - Shaper). - -send_text(StateData, Text) when StateData#state.mgmt_state == pending -> - ?DEBUG("Cannot send text while waiting for resumption: ~p", [Text]); -send_text(StateData, Text) when StateData#state.xml_socket -> - ?DEBUG("Send Text on stream = ~p", [Text]), - (StateData#state.sockmod):send_xml(StateData#state.socket, - {xmlstreamraw, Text}); -send_text(StateData, Text) when StateData#state.mgmt_state == active -> - ?DEBUG("Send XML on stream = ~p", [Text]), - case catch (StateData#state.sockmod):send(StateData#state.socket, Text) of - {'EXIT', _} -> - (StateData#state.sockmod):close(StateData#state.socket), - error; - _ -> - ok - end; -send_text(StateData, Text) -> - ?DEBUG("Send XML on stream = ~p", [Text]), - (StateData#state.sockmod):send(StateData#state.socket, Text). - -send_element(StateData, El) when StateData#state.mgmt_state == pending -> - ?DEBUG("Cannot send element while waiting for resumption: ~p", [El]); -send_element(StateData, El) when StateData#state.xml_socket -> - (StateData#state.sockmod):send_xml(StateData#state.socket, - {xmlstreamelement, El}); -send_element(StateData, El) -> - send_text(StateData, xml:element_to_binary(El)). - -send_stanza(StateData, Stanza) when StateData#state.csi_state == inactive -> - csi_filter_stanza(StateData, Stanza); -send_stanza(StateData, Stanza) when StateData#state.mgmt_state == pending -> - mgmt_queue_add(StateData, Stanza); -send_stanza(StateData, Stanza) when StateData#state.mgmt_state == active -> - NewStateData = case send_stanza_and_ack_req(StateData, Stanza) of - ok -> - StateData; - error -> - StateData#state{mgmt_state = pending} - end, - mgmt_queue_add(NewStateData, Stanza); -send_stanza(StateData, Stanza) -> - send_element(StateData, Stanza), - StateData. - -send_packet(StateData, Packet) -> - case is_stanza(Packet) of - true -> - send_stanza(StateData, Packet); - false -> - send_element(StateData, Packet), - StateData - end. - -send_header(StateData, Server, Version, Lang) - when StateData#state.xml_socket -> - VersionAttr = case Version of - <<"">> -> []; - _ -> [{<<"version">>, Version}] - end, - LangAttr = case Lang of - <<"">> -> []; - _ -> [{<<"xml:lang">>, Lang}] - end, - Header = {xmlstreamstart, <<"stream:stream">>, - VersionAttr ++ - LangAttr ++ - [{<<"xmlns">>, <<"jabber:client">>}, - {<<"xmlns:stream">>, - <<"http://etherx.jabber.org/streams">>}, - {<<"id">>, StateData#state.streamid}, - {<<"from">>, Server}]}, - (StateData#state.sockmod):send_xml(StateData#state.socket, - Header); -send_header(StateData, Server, Version, Lang) -> - VersionStr = case Version of - <<"">> -> <<"">>; - _ -> [<<" version='">>, Version, <<"'">>] - end, - LangStr = case Lang of - <<"">> -> <<"">>; - _ -> [<<" xml:lang='">>, Lang, <<"'">>] - end, - Header = io_lib:format(?STREAM_HEADER, - [StateData#state.streamid, Server, VersionStr, - LangStr]), - send_text(StateData, iolist_to_binary(Header)). - -send_trailer(StateData) - when StateData#state.mgmt_state == pending -> - ?DEBUG("Cannot send stream trailer while waiting for resumption", []); -send_trailer(StateData) - when StateData#state.xml_socket -> - (StateData#state.sockmod):send_xml(StateData#state.socket, - {xmlstreamend, <<"stream:stream">>}); -send_trailer(StateData) -> - send_text(StateData, ?STREAM_TRAILER). - -new_id() -> randoms:get_string(). - -is_auth_packet(El) -> - case jlib:iq_query_info(El) of - #iq{id = ID, type = Type, xmlns = ?NS_AUTH, sub_el = SubEl} -> - #xmlel{children = Els} = SubEl, - {auth, ID, Type, - get_auth_tags(Els, <<"">>, <<"">>, <<"">>, <<"">>)}; - _ -> false - end. - -is_stanza(#xmlel{name = Name, attrs = Attrs}) when Name == <<"message">>; - Name == <<"presence">>; - Name == <<"iq">> -> - case xml:get_attr(<<"xmlns">>, Attrs) of - {value, NS} when NS /= <<"jabber:client">>, - NS /= <<"jabber:server">> -> - false; - _ -> - true - end; -is_stanza(_El) -> - false. - -get_auth_tags([#xmlel{name = Name, children = Els} | L], - U, P, D, R) -> - CData = xml:get_cdata(Els), - case Name of - <<"username">> -> get_auth_tags(L, CData, P, D, R); - <<"password">> -> get_auth_tags(L, U, CData, D, R); - <<"digest">> -> get_auth_tags(L, U, P, CData, R); - <<"resource">> -> get_auth_tags(L, U, P, D, CData); - _ -> get_auth_tags(L, U, P, D, R) - end; -get_auth_tags([_ | L], U, P, D, R) -> - get_auth_tags(L, U, P, D, R); -get_auth_tags([], U, P, D, R) -> - {U, P, D, R}. - -%% Copied from ejabberd_socket.erl --record(socket_state, {sockmod, socket, receiver}). - -get_conn_type(StateData) -> - case (StateData#state.sockmod):get_sockmod(StateData#state.socket) of - gen_tcp -> c2s; - p1_tls -> c2s_tls; - ezlib -> - case ezlib:get_sockmod((StateData#state.socket)#socket_state.socket) of - gen_tcp -> c2s_compressed; - p1_tls -> c2s_compressed_tls - end; - ejabberd_http_poll -> http_poll; - ejabberd_http_bind -> http_bind; - _ -> unknown - end. - -process_presence_probe(From, To, StateData) -> - LFrom = jlib:jid_tolower(From), - LBFrom = setelement(3, LFrom, <<"">>), - case StateData#state.pres_last of - undefined -> - ok; - _ -> - Cond = ?SETS:is_element(LFrom, StateData#state.pres_f) - orelse - ((LFrom /= LBFrom) andalso - ?SETS:is_element(LBFrom, StateData#state.pres_f)), - if - Cond -> - %% To is the one sending the presence (the probe target) - Packet = jlib:add_delay_info(StateData#state.pres_last, To, - StateData#state.pres_timestamp), - case privacy_check_packet(StateData, To, From, Packet, out) of - deny -> - ok; - allow -> - Pid=element(2, StateData#state.sid), - ejabberd_hooks:run(presence_probe_hook, StateData#state.server, [From, To, Pid]), - %% Don't route a presence probe to oneself - case From == To of - false -> - ejabberd_router:route(To, From, Packet); - true -> - ok - end +-spec process_presence_out(state(), presence()) -> state(). +process_presence_out(#{lserver := LServer, jid := JID, + lang := Lang, pres_a := PresA} = State0, + #presence{from = From, to = To, type = Type} = Pres) -> + State1 = + if Type == subscribe; Type == subscribed; + Type == unsubscribe; Type == unsubscribed -> + Access = mod_roster_opt:access(LServer), + MyBareJID = jid:remove_resource(JID), + case acl:match_rule(LServer, Access, MyBareJID) of + deny -> + AccessErrTxt = ?T("Access denied by service policy"), + AccessErr = xmpp:err_forbidden(AccessErrTxt, Lang), + send_error(State0, Pres, AccessErr); + allow -> + ejabberd_hooks:run(roster_out_subscription, LServer, [Pres]), + State0 + end; + true -> + State0 + end, + case privacy_check_packet(State1, Pres, out) of + deny -> + PrivErrTxt = ?T("Your active privacy list has denied " + "the routing of this stanza."), + PrivErr = xmpp:err_not_acceptable(PrivErrTxt, Lang), + send_error(State1, Pres, PrivErr); + allow when Type == subscribe; Type == subscribed; + Type == unsubscribe; Type == unsubscribed -> + BareFrom = jid:remove_resource(From), + ejabberd_router:route(xmpp:set_from_to(Pres, BareFrom, To)), + State1; + allow when Type == error; Type == probe -> + ejabberd_router:route(Pres), + State1; + allow -> + ejabberd_router:route(Pres), + LTo = jid:tolower(To), + LBareTo = jid:remove_resource(LTo), + LBareFrom = jid:remove_resource(jid:tolower(From)), + if LBareTo /= LBareFrom -> + Subscription = get_subscription(From, To), + if Subscription /= both andalso Subscription /= from -> + A = case Type of + available -> ?SETS:add_element(LTo, PresA); + unavailable -> ?SETS:del_element(LTo, PresA) + end, + State1#{pres_a => A}; + true -> + State1 end; + true -> + State1 + end + end. + +-spec process_self_presence(state(), presence()) -> state(). +process_self_presence(#{lserver := LServer, sid := SID, + user := U, server := S, resource := R} = State, + #presence{type = unavailable} = Pres) -> + Status = xmpp:get_text(Pres#presence.status), + _ = ejabberd_sm:unset_presence(SID, U, S, R, Status), + {Pres1, State1} = ejabberd_hooks:run_fold( + c2s_self_presence, LServer, {Pres, State}, []), + State2 = broadcast_presence_unavailable(State1, Pres1, true), + maps:remove(pres_last, maps:remove(pres_timestamp, State2)); +process_self_presence(#{lserver := LServer} = State, + #presence{type = available} = Pres) -> + PreviousPres = maps:get(pres_last, State, undefined), + _ = update_priority(State, Pres), + {Pres1, State1} = ejabberd_hooks:run_fold( + c2s_self_presence, LServer, {Pres, State}, []), + State2 = State1#{pres_last => Pres1, + pres_timestamp => erlang:timestamp()}, + FromUnavailable = PreviousPres == undefined, + broadcast_presence_available(State2, Pres1, FromUnavailable); +process_self_presence(State, _Pres) -> + State. + +-spec update_priority(state(), presence()) -> ok | {error, notfound}. +update_priority(#{sid := SID, user := U, server := S, resource := R}, + Pres) -> + Priority = get_priority_from_presence(Pres), + ejabberd_sm:set_presence(SID, U, S, R, Priority, Pres). + +-spec broadcast_presence_unavailable(state(), presence(), boolean()) -> state(). +broadcast_presence_unavailable(#{jid := JID, pres_a := PresA} = State, Pres, + BroadcastToRoster) -> + #jid{luser = LUser, lserver = LServer} = JID, + BareJID = jid:tolower(jid:remove_resource(JID)), + Items1 = case BroadcastToRoster of true -> + Roster = ejabberd_hooks:run_fold(roster_get, LServer, + [], [{LUser, LServer}]), + lists:foldl( + fun(#roster_item{jid = ItemJID, subscription = Sub}, Acc) + when Sub == both; Sub == from -> + maps:put(jid:tolower(ItemJID), 1, Acc); + (_, Acc) -> + Acc + end, #{BareJID => 1}, Roster); + _ -> + #{BareJID => 1} + end, + Items2 = ?SETS:fold( + fun(LJID, Acc) -> + maps:put(LJID, 1, Acc) + end, Items1, PresA), + + JIDs = lists:filtermap( + fun(LJid) -> + To = jid:make(LJid), + P = xmpp:set_to(Pres, To), + case privacy_check_packet(State, P, out) of + allow -> {true, To}; + deny -> false + end + end, maps:keys(Items2)), + route_multiple(State, JIDs, Pres), + State#{pres_a => ?SETS:new()}. + +-spec broadcast_presence_available(state(), presence(), boolean()) -> state(). +broadcast_presence_available(#{jid := JID} = State, + Pres, _FromUnavailable = true) -> + Probe = #presence{from = JID, type = probe}, + #jid{luser = LUser, lserver = LServer} = JID, + BareJID = jid:remove_resource(JID), + Items = ejabberd_hooks:run_fold(roster_get, LServer, + [], [{LUser, LServer}]), + {FJIDs, TJIDs} = + lists:foldl( + fun(#roster_item{jid = To, subscription = Sub}, {F, T}) -> + F1 = if Sub == both orelse Sub == from -> + Pres1 = xmpp:set_to(Pres, To), + case privacy_check_packet(State, Pres1, out) of + allow -> [To|F]; + deny -> F + end; + true -> F + end, + T1 = if Sub == both orelse Sub == to -> + Probe1 = xmpp:set_to(Probe, To), + case privacy_check_packet(State, Probe1, out) of + allow -> [To|T]; + deny -> T + end; + true -> T + end, + {F1, T1} + end, {[BareJID], [BareJID]}, Items), + route_multiple(State, TJIDs, Probe), + route_multiple(State, FJIDs, Pres), + State; +broadcast_presence_available(#{jid := JID} = State, + Pres, _FromUnavailable = false) -> + #jid{luser = LUser, lserver = LServer} = JID, + BareJID = jid:remove_resource(JID), + Items = ejabberd_hooks:run_fold( + roster_get, LServer, [], [{LUser, LServer}]), + JIDs = lists:foldl( + fun(#roster_item{jid = To, subscription = Sub}, Tos) + when Sub == both orelse Sub == from -> + P = xmpp:set_to(Pres, To), + case privacy_check_packet(State, P, out) of + allow -> [To|Tos]; + deny -> Tos + end; + (_, Tos) -> + Tos + end, [BareJID], Items), + route_multiple(State, JIDs, Pres), + State. + +-spec check_privacy_then_route(state(), stanza()) -> state(). +check_privacy_then_route(#{lang := Lang} = State, Pkt) -> + case privacy_check_packet(State, Pkt, out) of + deny -> + ErrText = ?T("Your active privacy list has denied " + "the routing of this stanza."), + Err = xmpp:err_not_acceptable(ErrText, Lang), + send_error(State, Pkt, Err); + allow -> + ejabberd_router:route(Pkt), + State + end. + +-spec privacy_check_packet(state(), stanza(), in | out) -> allow | deny. +privacy_check_packet(#{lserver := LServer} = State, Pkt, Dir) -> + ejabberd_hooks:run_fold(privacy_check_packet, LServer, allow, [State, Pkt, Dir]). + +-spec get_priority_from_presence(presence()) -> integer(). +get_priority_from_presence(#presence{priority = Prio}) -> + case Prio of + undefined -> 0; + _ -> Prio + end. + +-spec route_multiple(state(), [jid()], stanza()) -> ok. +route_multiple(#{lserver := LServer}, JIDs, Pkt) -> + From = xmpp:get_from(Pkt), + ejabberd_router_multicast:route_multicast(From, LServer, JIDs, Pkt, false). + +get_subscription(#jid{luser = LUser, lserver = LServer}, JID) -> + {Subscription, _, _} = ejabberd_hooks:run_fold( + roster_get_jid_info, LServer, {none, none, []}, + [LUser, LServer, JID]), + Subscription. + +-spec resource_conflict_action(binary(), binary(), binary()) -> + {accept_resource, binary()} | closenew. +resource_conflict_action(U, S, R) -> + OptionRaw = case ejabberd_sm:is_existing_resource(U, S, R) of + true -> + ejabberd_option:resource_conflict(S); + false -> + acceptnew + end, + Option = case OptionRaw of + setresource -> setresource; + closeold -> acceptnew; %% ejabberd_sm will close old session + closenew -> closenew; + acceptnew -> acceptnew + end, + case Option of + acceptnew -> {accept_resource, R}; + closenew -> closenew; + setresource -> + Rnew = new_uniq_id(), + {accept_resource, Rnew} + end. + +-spec bounce_message_queue(ejabberd_sm:sid(), jid:jid()) -> ok. +bounce_message_queue({_, Pid} = SID, JID) -> + {U, S, R} = jid:tolower(JID), + SIDs = ejabberd_sm:get_session_sids(U, S, R), + case lists:member(SID, SIDs) of + true -> + ?WARNING_MSG("The session for ~ts@~ts/~ts is supposed to " + "be unregistered, but session identifier ~p " + "still presents in the 'session' table", + [U, S, R, Pid]); + false -> + receive {route, Pkt} -> + ejabberd_router:route(Pkt), + bounce_message_queue(SID, JID) + after 100 -> ok end end. -%% User updates his presence (non-directed presence packet) -presence_update(From, Packet, StateData) -> - #xmlel{attrs = Attrs} = Packet, - case xml:get_attr_s(<<"type">>, Attrs) of - <<"unavailable">> -> - Status = case xml:get_subtag(Packet, <<"status">>) of - false -> <<"">>; - StatusTag -> xml:get_tag_cdata(StatusTag) - end, - Info = [{ip, StateData#state.ip}, - {conn, StateData#state.conn}, - {auth_module, StateData#state.auth_module}], - ejabberd_sm:unset_presence(StateData#state.sid, - StateData#state.user, - StateData#state.server, - StateData#state.resource, Status, Info), - presence_broadcast(StateData, From, - StateData#state.pres_a, Packet), - StateData#state{pres_last = undefined, - pres_timestamp = undefined, pres_a = (?SETS):new()}; - <<"error">> -> StateData; - <<"probe">> -> StateData; - <<"subscribe">> -> StateData; - <<"subscribed">> -> StateData; - <<"unsubscribe">> -> StateData; - <<"unsubscribed">> -> StateData; - _ -> - OldPriority = case StateData#state.pres_last of - undefined -> 0; - OldPresence -> get_priority_from_presence(OldPresence) - end, - NewPriority = get_priority_from_presence(Packet), - update_priority(NewPriority, Packet, StateData), - FromUnavail = (StateData#state.pres_last == undefined), - ?DEBUG("from unavail = ~p~n", [FromUnavail]), - NewStateData = StateData#state{pres_last = Packet, - pres_timestamp = now()}, - NewState = if FromUnavail -> - ejabberd_hooks:run(user_available_hook, - NewStateData#state.server, - [NewStateData#state.jid]), - ResentStateData = if NewPriority >= 0 -> - resend_offline_messages(NewStateData), - resend_subscription_requests(NewStateData); - true -> NewStateData - end, - presence_broadcast_first(From, ResentStateData, - Packet); - true -> - presence_broadcast_to_trusted(NewStateData, From, - NewStateData#state.pres_f, - NewStateData#state.pres_a, - Packet), - if OldPriority < 0, NewPriority >= 0 -> - resend_offline_messages(NewStateData); - true -> ok - end, - NewStateData - end, - NewState +-spec new_uniq_id() -> binary(). +new_uniq_id() -> + iolist_to_binary( + [p1_rand:get_string(), + integer_to_binary(erlang:unique_integer([positive]))]). + +-spec get_conn_type(state()) -> c2s | c2s_tls | c2s_compressed | websocket | + c2s_compressed_tls | http_bind. +get_conn_type(State) -> + case xmpp_stream_in:get_transport(State) of + tcp -> c2s; + tls -> c2s_tls; + tcp_zlib -> c2s_compressed; + tls_zlib -> c2s_compressed_tls; + http_bind -> http_bind; + websocket -> websocket end. -%% User sends a directed presence packet -presence_track(From, To, Packet, StateData) -> - #xmlel{attrs = Attrs} = Packet, - LTo = jlib:jid_tolower(To), - User = StateData#state.user, - Server = StateData#state.server, - case xml:get_attr_s(<<"type">>, Attrs) of - <<"unavailable">> -> - check_privacy_route(From, StateData, From, To, Packet), - A = remove_element(LTo, StateData#state.pres_a), - StateData#state{pres_a = A}; - <<"subscribe">> -> - try_roster_subscribe(subscribe, User, Server, From, To, Packet, StateData), - StateData; - <<"subscribed">> -> - ejabberd_hooks:run(roster_out_subscription, Server, - [User, Server, To, subscribed]), - check_privacy_route(From, StateData, - jlib:jid_remove_resource(From), To, Packet), - StateData; - <<"unsubscribe">> -> - try_roster_subscribe(unsubscribe, User, Server, From, To, Packet, StateData), - StateData; - <<"unsubscribed">> -> - ejabberd_hooks:run(roster_out_subscription, Server, - [User, Server, To, unsubscribed]), - check_privacy_route(From, StateData, - jlib:jid_remove_resource(From), To, Packet), - StateData; - <<"error">> -> - check_privacy_route(From, StateData, From, To, Packet), - StateData; - <<"probe">> -> - check_privacy_route(From, StateData, From, To, Packet), - StateData; - _ -> - check_privacy_route(From, StateData, From, To, Packet), - A = (?SETS):add_element(LTo, StateData#state.pres_a), - StateData#state{pres_a = A} - end. - -check_privacy_route(From, StateData, FromRoute, To, - Packet) -> - case privacy_check_packet(StateData, From, To, Packet, - out) - of - deny -> - Lang = StateData#state.lang, - ErrText = <<"Your active privacy list has denied " - "the routing of this stanza.">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)), - ejabberd_router:route(To, From, Err), - ok; - allow -> ejabberd_router:route(FromRoute, To, Packet) - end. - -%% Check if privacy rules allow this delivery -privacy_check_packet(StateData, From, To, Packet, - Dir) -> - ejabberd_hooks:run_fold(privacy_check_packet, - StateData#state.server, allow, - [StateData#state.user, StateData#state.server, - StateData#state.privacy_list, {From, To, Packet}, - Dir]). - -is_privacy_allow(StateData, From, To, Packet, Dir) -> - allow == - privacy_check_packet(StateData, From, To, Packet, Dir). - -%%% Check ACL before allowing to send a subscription stanza -try_roster_subscribe(Type, User, Server, From, To, Packet, StateData) -> - JID1 = jlib:make_jid(User, Server, <<"">>), - Access = gen_mod:get_module_opt(Server, mod_roster, access, fun(A) when is_atom(A) -> A end, all), - case acl:match_rule(Server, Access, JID1) of - deny -> - %% Silently drop this (un)subscription request - ok; - allow -> - ejabberd_hooks:run(roster_out_subscription, - Server, - [User, Server, To, Type]), - check_privacy_route(From, StateData, jlib:jid_remove_resource(From), - To, Packet) - end. - -%% Send presence when disconnecting -presence_broadcast(StateData, From, JIDSet, Packet) -> - JIDs = ?SETS:to_list(JIDSet), - JIDs2 = format_and_check_privacy(From, StateData, Packet, JIDs, out), - send_multiple(StateData, From, JIDs2, Packet). - -%% Send presence when updating presence -presence_broadcast_to_trusted(StateData, From, Trusted, JIDSet, Packet) -> - JIDs = ?SETS:to_list(JIDSet), - JIDs_trusted = [JID || JID <- JIDs, ?SETS:is_element(JID, Trusted)], - JIDs2 = format_and_check_privacy(From, StateData, Packet, JIDs_trusted, out), - send_multiple(StateData, From, JIDs2, Packet). - -%% Send presence when connecting -presence_broadcast_first(From, StateData, Packet) -> - JIDsProbe = - ?SETS:fold( - fun(JID, L) -> [JID | L] end, - [], - StateData#state.pres_t), - PacketProbe = #xmlel{name = <<"presence">>, attrs = [{<<"type">>,<<"probe">>}], children = []}, - JIDs2Probe = format_and_check_privacy(From, StateData, PacketProbe, JIDsProbe, out), - Server = StateData#state.server, - send_multiple(StateData, From, JIDs2Probe, PacketProbe), - {As, JIDs} = - ?SETS:fold( - fun(JID, {A, JID_list}) -> - {?SETS:add_element(JID, A), JID_list++[JID]} - end, - {StateData#state.pres_a, []}, - StateData#state.pres_f), - JIDs2 = format_and_check_privacy(From, StateData, Packet, JIDs, out), - Server = StateData#state.server, - send_multiple(StateData, From, JIDs2, Packet), - StateData#state{pres_a = As}. - -format_and_check_privacy(From, StateData, Packet, JIDs, Dir) -> - FJIDs = [jlib:make_jid(JID) || JID <- JIDs], - lists:filter( - fun(FJID) -> - case ejabberd_hooks:run_fold( - privacy_check_packet, StateData#state.server, - allow, - [StateData#state.user, - StateData#state.server, - StateData#state.privacy_list, - {From, FJID, Packet}, - Dir]) of - deny -> false; - allow -> true - end - end, - FJIDs). - -send_multiple(StateData, From, JIDs, Packet) -> - lists:foreach( - fun(JID) -> - case privacy_check_packet(StateData, From, JID, Packet, out) of - deny -> - ok; - allow -> - ejabberd_router:route(From, JID, Packet) - end - end, JIDs). - -remove_element(E, Set) -> - case (?SETS):is_element(E, Set) of - true -> (?SETS):del_element(E, Set); - _ -> Set - end. - -roster_change(IJID, ISubscription, StateData) -> - LIJID = jlib:jid_tolower(IJID), - IsFrom = (ISubscription == both) or (ISubscription == from), - IsTo = (ISubscription == both) or (ISubscription == to), - OldIsFrom = (?SETS):is_element(LIJID, StateData#state.pres_f), - FSet = if - IsFrom -> (?SETS):add_element(LIJID, StateData#state.pres_f); - not IsFrom -> remove_element(LIJID, StateData#state.pres_f) - end, - TSet = if - IsTo -> (?SETS):add_element(LIJID, StateData#state.pres_t); - not IsTo -> remove_element(LIJID, StateData#state.pres_t) - end, - case StateData#state.pres_last of - undefined -> - StateData#state{pres_f = FSet, pres_t = TSet}; - P -> - ?DEBUG("roster changed for ~p~n", - [StateData#state.user]), - From = StateData#state.jid, - To = jlib:make_jid(IJID), - Cond1 = IsFrom andalso not OldIsFrom, - Cond2 = not IsFrom andalso OldIsFrom andalso - ((?SETS):is_element(LIJID, StateData#state.pres_a)), - if Cond1 -> - ?DEBUG("C1: ~p~n", [LIJID]), - case privacy_check_packet(StateData, From, To, P, out) - of - deny -> ok; - allow -> ejabberd_router:route(From, To, P) - end, - A = (?SETS):add_element(LIJID, StateData#state.pres_a), - StateData#state{pres_a = A, pres_f = FSet, - pres_t = TSet}; - Cond2 -> - ?DEBUG("C2: ~p~n", [LIJID]), - PU = #xmlel{name = <<"presence">>, - attrs = [{<<"type">>, <<"unavailable">>}], - children = []}, - case privacy_check_packet(StateData, From, To, PU, out) - of - deny -> ok; - allow -> ejabberd_router:route(From, To, PU) - end, - A = remove_element(LIJID, StateData#state.pres_a), - StateData#state{pres_a = A, pres_f = FSet, - pres_t = TSet}; - true -> StateData#state{pres_f = FSet, pres_t = TSet} - end - end. - -update_priority(Priority, Packet, StateData) -> - Info = [{ip, StateData#state.ip}, {conn, StateData#state.conn}, - {auth_module, StateData#state.auth_module}], - ejabberd_sm:set_presence(StateData#state.sid, - StateData#state.user, StateData#state.server, - StateData#state.resource, Priority, Packet, Info). - -get_priority_from_presence(PresencePacket) -> - case xml:get_subtag(PresencePacket, <<"priority">>) of - false -> 0; - SubEl -> - case catch - jlib:binary_to_integer(xml:get_tag_cdata(SubEl)) - of - P when is_integer(P) -> P; - _ -> 0 - end - end. - -process_privacy_iq(From, To, - #iq{type = Type, sub_el = SubEl} = IQ, StateData) -> - {Res, NewStateData} = case Type of - get -> - R = ejabberd_hooks:run_fold(privacy_iq_get, - StateData#state.server, - {error, - ?ERR_FEATURE_NOT_IMPLEMENTED}, - [From, To, IQ, - StateData#state.privacy_list]), - {R, StateData}; - set -> - case ejabberd_hooks:run_fold(privacy_iq_set, - StateData#state.server, - {error, - ?ERR_FEATURE_NOT_IMPLEMENTED}, - [From, To, IQ]) - of - {result, R, NewPrivList} -> - {{result, R}, - StateData#state{privacy_list = - NewPrivList}}; - R -> {R, StateData} - end - end, - IQRes = case Res of - {result, Result} -> - IQ#iq{type = result, sub_el = Result}; - {error, Error} -> - IQ#iq{type = error, sub_el = [SubEl, Error]} - end, - ejabberd_router:route(To, From, jlib:iq_to_xml(IQRes)), - NewStateData. - -resend_offline_messages(StateData) -> - case ejabberd_hooks:run_fold(resend_offline_messages_hook, - StateData#state.server, [], - [StateData#state.user, StateData#state.server]) - of - Rs -> %%when is_list(Rs) -> - lists:foreach(fun ({route, From, To, - #xmlel{} = Packet}) -> - Pass = case privacy_check_packet(StateData, - From, To, - Packet, in) - of - allow -> true; - deny -> false - end, - if Pass -> - ejabberd_router:route(From, To, Packet); - true -> ok - end - end, - Rs) - end. - -resend_subscription_requests(#state{user = User, - server = Server} = StateData) -> - PendingSubscriptions = - ejabberd_hooks:run_fold(resend_subscription_requests_hook, - Server, [], [User, Server]), - lists:foldl(fun (XMLPacket, AccStateData) -> - send_packet(AccStateData, XMLPacket) - end, - StateData, - PendingSubscriptions). - -get_showtag(undefined) -> <<"unavailable">>; -get_showtag(Presence) -> - case xml:get_path_s(Presence, [{elem, <<"show">>}, cdata]) of - <<"">> -> <<"available">>; - ShowTag -> ShowTag - end. - -get_statustag(undefined) -> <<"">>; -get_statustag(Presence) -> - xml:get_path_s(Presence, [{elem, <<"status">>}, cdata]). - -process_unauthenticated_stanza(StateData, El) -> - NewEl = case xml:get_tag_attr_s(<<"xml:lang">>, El) of - <<"">> -> - case StateData#state.lang of - <<"">> -> El; - Lang -> xml:replace_tag_attr(<<"xml:lang">>, Lang, El) - end; - _ -> El - end, - case jlib:iq_query_info(NewEl) of - #iq{} = IQ -> - Res = ejabberd_hooks:run_fold(c2s_unauthenticated_iq, - StateData#state.server, empty, - [StateData#state.server, IQ, - StateData#state.ip]), - case Res of - empty -> - ResIQ = IQ#iq{type = error, - sub_el = [?ERR_SERVICE_UNAVAILABLE]}, - Res1 = jlib:replace_from_to(jlib:make_jid(<<"">>, - StateData#state.server, - <<"">>), - jlib:make_jid(<<"">>, <<"">>, - <<"">>), - jlib:iq_to_xml(ResIQ)), - send_element(StateData, - jlib:remove_attr(<<"to">>, Res1)); - _ -> send_element(StateData, Res) - end; - _ -> - % Drop any stanza, which isn't IQ stanza - ok - end. - -peerip(SockMod, Socket) -> - IP = case SockMod of - gen_tcp -> inet:peername(Socket); - _ -> SockMod:peername(Socket) - end, - case IP of - {ok, IPOK} -> IPOK; - _ -> undefined - end. - -%% fsm_next_state_pack: Pack the StateData structure to improve -%% sharing. -fsm_next_state_pack(StateName, StateData) -> - fsm_next_state_gc(StateName, pack(StateData)). - -%% fsm_next_state_gc: Garbage collect the process heap to make use of -%% the newly packed StateData structure. -fsm_next_state_gc(StateName, PackedStateData) -> - erlang:garbage_collect(), - fsm_next_state(StateName, PackedStateData). - -%% fsm_next_state: Generate the next_state FSM tuple with different -%% timeout, depending on the future state -fsm_next_state(session_established, #state{mgmt_max_queue = exceeded} = - StateData) -> - ?WARNING_MSG("ACK queue too long, terminating session for ~s", - [jlib:jid_to_string(StateData#state.jid)]), - Err = ?SERRT_POLICY_VIOLATION(StateData#state.lang, - <<"Too many unacked stanzas">>), - send_element(StateData, Err), - send_trailer(StateData), - {stop, normal, StateData#state{mgmt_resend = false}}; -fsm_next_state(session_established, #state{mgmt_state = pending} = StateData) -> - fsm_next_state(wait_for_resume, StateData); -fsm_next_state(session_established, StateData) -> - {next_state, session_established, StateData, - ?C2S_HIBERNATE_TIMEOUT}; -fsm_next_state(wait_for_resume, #state{mgmt_timeout = 0} = StateData) -> - {stop, normal, StateData}; -fsm_next_state(wait_for_resume, #state{mgmt_pending_since = undefined} = - StateData) -> - ?INFO_MSG("Waiting for resumption of stream for ~s", - [jlib:jid_to_string(StateData#state.jid)]), - {next_state, wait_for_resume, - StateData#state{mgmt_state = pending, mgmt_pending_since = os:timestamp()}, - StateData#state.mgmt_timeout}; -fsm_next_state(wait_for_resume, StateData) -> - Diff = timer:now_diff(os:timestamp(), StateData#state.mgmt_pending_since), - Timeout = max(StateData#state.mgmt_timeout - Diff div 1000, 1), - {next_state, wait_for_resume, StateData, Timeout}; -fsm_next_state(StateName, StateData) -> - {next_state, StateName, StateData, ?C2S_OPEN_TIMEOUT}. - -%% fsm_reply: Generate the reply FSM tuple with different timeout, -%% depending on the future state -fsm_reply(Reply, session_established, StateData) -> - {reply, Reply, session_established, StateData, - ?C2S_HIBERNATE_TIMEOUT}; -fsm_reply(Reply, wait_for_resume, StateData) -> - Diff = timer:now_diff(os:timestamp(), StateData#state.mgmt_pending_since), - Timeout = max(StateData#state.mgmt_timeout - Diff div 1000, 1), - {reply, Reply, wait_for_resume, StateData, Timeout}; -fsm_reply(Reply, StateName, StateData) -> - {reply, Reply, StateName, StateData, ?C2S_OPEN_TIMEOUT}. - -%% Used by c2s blacklist plugins -is_ip_blacklisted(undefined, _Lang) -> false; -is_ip_blacklisted({IP, _Port}, Lang) -> - ejabberd_hooks:run_fold(check_bl_c2s, false, [IP, Lang]). - -%% Check from attributes -%% returns invalid-from|NewElement -check_from(El, FromJID) -> - case xml:get_tag_attr(<<"from">>, El) of - false -> - El; - {value, SJID} -> - JID = jlib:string_to_jid(SJID), - case JID of - error -> - 'invalid-from'; - #jid{} -> - if - (JID#jid.luser == FromJID#jid.luser) and - (JID#jid.lserver == FromJID#jid.lserver) and - (JID#jid.lresource == FromJID#jid.lresource) -> - El; - (JID#jid.luser == FromJID#jid.luser) and - (JID#jid.lserver == FromJID#jid.lserver) and - (JID#jid.lresource == <<"">>) -> - El; - true -> - 'invalid-from' - end - end - end. - -fsm_limit_opts(Opts) -> - case lists:keysearch(max_fsm_queue, 1, Opts) of - {value, {_, N}} when is_integer(N) -> [{max_queue, N}]; - _ -> - case ejabberd_config:get_option( - max_fsm_queue, - fun(I) when is_integer(I), I > 0 -> I end) of - undefined -> []; - N -> [{max_queue, N}] - end - end. - -bounce_messages() -> - receive - {route, From, To, El} -> - ejabberd_router:route(From, To, El), bounce_messages() - after 0 -> ok - end. - -%%%---------------------------------------------------------------------- -%%% XEP-0191 -%%%---------------------------------------------------------------------- - -route_blocking(What, StateData) -> - SubEl = case What of - {block, JIDs} -> - #xmlel{name = <<"block">>, - attrs = [{<<"xmlns">>, ?NS_BLOCKING}], - children = - lists:map(fun (JID) -> - #xmlel{name = <<"item">>, - attrs = - [{<<"jid">>, - jlib:jid_to_string(JID)}], - children = []} - end, - JIDs)}; - {unblock, JIDs} -> - #xmlel{name = <<"unblock">>, - attrs = [{<<"xmlns">>, ?NS_BLOCKING}], - children = - lists:map(fun (JID) -> - #xmlel{name = <<"item">>, - attrs = - [{<<"jid">>, - jlib:jid_to_string(JID)}], - children = []} - end, - JIDs)}; - unblock_all -> - #xmlel{name = <<"unblock">>, - attrs = [{<<"xmlns">>, ?NS_BLOCKING}], children = []} - end, - PrivPushIQ = #iq{type = set, id = <<"push">>, sub_el = [SubEl]}, - PrivPushEl = - jlib:replace_from_to(jlib:jid_remove_resource(StateData#state.jid), - StateData#state.jid, jlib:iq_to_xml(PrivPushIQ)), - %% No need to replace active privacy list here, - %% blocking pushes are always accompanied by - %% Privacy List pushes - send_stanza(StateData, PrivPushEl). - -%%%---------------------------------------------------------------------- -%%% XEP-0198 -%%%---------------------------------------------------------------------- - -stream_mgmt_enabled(#state{mgmt_state = disabled}) -> - false; -stream_mgmt_enabled(_StateData) -> - true. - -dispatch_stream_mgmt(El, StateData) - when StateData#state.mgmt_state == active; - StateData#state.mgmt_state == pending -> - perform_stream_mgmt(El, StateData); -dispatch_stream_mgmt(El, StateData) -> - negotiate_stream_mgmt(El, StateData). - -negotiate_stream_mgmt(_El, #state{resource = <<"">>} = StateData) -> - %% XEP-0198 says: "For client-to-server connections, the client MUST NOT - %% attempt to enable stream management until after it has completed Resource - %% Binding unless it is resuming a previous session". However, it also - %% says: "Stream management errors SHOULD be considered recoverable", so we - %% won't bail out. - send_element(StateData, ?MGMT_UNEXPECTED_REQUEST(?NS_STREAM_MGMT_3)), - StateData; -negotiate_stream_mgmt(#xmlel{name = Name, attrs = Attrs}, StateData) -> - case xml:get_attr_s(<<"xmlns">>, Attrs) of - Xmlns when ?IS_SUPPORTED_MGMT_XMLNS(Xmlns) -> - case stream_mgmt_enabled(StateData) of - true -> - case Name of - <<"enable">> -> - handle_enable(StateData#state{mgmt_xmlns = Xmlns}, Attrs); - _ -> - Res = if Name == <<"a">>; - Name == <<"r">>; - Name == <<"resume">> -> - ?MGMT_UNEXPECTED_REQUEST(Xmlns); - true -> - ?MGMT_BAD_REQUEST(Xmlns) - end, - send_element(StateData, Res), - StateData - end; - false -> - send_element(StateData, ?MGMT_SERVICE_UNAVAILABLE(Xmlns)), - StateData - end; - _ -> - send_element(StateData, ?MGMT_UNSUPPORTED_VERSION(?NS_STREAM_MGMT_3)), - StateData - end. - -perform_stream_mgmt(#xmlel{name = Name, attrs = Attrs}, StateData) -> - case xml:get_attr_s(<<"xmlns">>, Attrs) of - Xmlns when Xmlns == StateData#state.mgmt_xmlns -> - case Name of - <<"r">> -> - handle_r(StateData); - <<"a">> -> - handle_a(StateData, Attrs); - _ -> - Res = if Name == <<"enable">>; - Name == <<"resume">> -> - ?MGMT_UNEXPECTED_REQUEST(Xmlns); - true -> - ?MGMT_BAD_REQUEST(Xmlns) - end, - send_element(StateData, Res), - StateData - end; - _ -> - send_element(StateData, - ?MGMT_UNSUPPORTED_VERSION(StateData#state.mgmt_xmlns)), - StateData - end. - -handle_enable(#state{mgmt_timeout = ConfigTimeout} = StateData, Attrs) -> - Timeout = case xml:get_attr_s(<<"resume">>, Attrs) of - ResumeAttr when ResumeAttr == <<"true">>; - ResumeAttr == <<"1">> -> - MaxAttr = xml:get_attr_s(<<"max">>, Attrs), - case catch jlib:binary_to_integer(MaxAttr) of - Max when is_integer(Max), Max > 0, Max =< ConfigTimeout -> - Max; - _ -> - ConfigTimeout - end; - _ -> - 0 - end, - ResAttrs = [{<<"xmlns">>, StateData#state.mgmt_xmlns}] ++ - if Timeout > 0 -> - ?INFO_MSG("Stream management with resumption enabled for ~s", - [jlib:jid_to_string(StateData#state.jid)]), - [{<<"id">>, make_resume_id(StateData)}, - {<<"resume">>, <<"true">>}, - {<<"max">>, jlib:integer_to_binary(Timeout)}]; - true -> - ?INFO_MSG("Stream management without resumption enabled for ~s", - [jlib:jid_to_string(StateData#state.jid)]), - [] - end, - Res = #xmlel{name = <<"enabled">>, - attrs = ResAttrs, - children = []}, - send_element(StateData, Res), - StateData#state{mgmt_state = active, - mgmt_queue = queue:new(), - mgmt_timeout = Timeout * 1000}. - -handle_r(StateData) -> - H = jlib:integer_to_binary(StateData#state.mgmt_stanzas_in), - Res = #xmlel{name = <<"a">>, - attrs = [{<<"xmlns">>, StateData#state.mgmt_xmlns}, - {<<"h">>, H}], - children = []}, - send_element(StateData, Res), - StateData. - -handle_a(StateData, Attrs) -> - case catch jlib:binary_to_integer(xml:get_attr_s(<<"h">>, Attrs)) of - H when is_integer(H), H >= 0 -> - check_h_attribute(StateData, H); - _ -> - ?DEBUG("Ignoring invalid ACK element from ~s", - [jlib:jid_to_string(StateData#state.jid)]), - StateData - end. - -handle_resume(StateData, Attrs) -> - R = case xml:get_attr_s(<<"xmlns">>, Attrs) of - Xmlns when ?IS_SUPPORTED_MGMT_XMLNS(Xmlns) -> - case stream_mgmt_enabled(StateData) of - true -> - case {xml:get_attr(<<"previd">>, Attrs), - catch jlib:binary_to_integer(xml:get_attr_s(<<"h">>, Attrs))} - of - {{value, PrevID}, H} when is_integer(H), H >= 0 -> - case inherit_session_state(StateData, PrevID) of - {ok, InheritedState} -> - {ok, InheritedState, H}; - {error, Err} -> - {error, ?MGMT_ITEM_NOT_FOUND(Xmlns), Err} - end; - _ -> - {error, ?MGMT_BAD_REQUEST(Xmlns), - <<"Invalid request">>} - end; - false -> - {error, ?MGMT_SERVICE_UNAVAILABLE(Xmlns), - <<"XEP-0198 disabled">>} - end; - _ -> - {error, ?MGMT_UNSUPPORTED_VERSION(?NS_STREAM_MGMT_3), - <<"Invalid XMLNS">>} - end, - case R of - {ok, ResumedState, NumHandled} -> - NewState = check_h_attribute(ResumedState, NumHandled), - AttrXmlns = NewState#state.mgmt_xmlns, - AttrId = make_resume_id(NewState), - AttrH = jlib:integer_to_binary(NewState#state.mgmt_stanzas_in), - send_element(NewState, - #xmlel{name = <<"resumed">>, - attrs = [{<<"xmlns">>, AttrXmlns}, - {<<"h">>, AttrH}, - {<<"previd">>, AttrId}], - children = []}), - SendFun = fun(_F, _T, El, Time) -> - NewEl = add_resent_delay_info(NewState, El, Time), - send_element(NewState, NewEl) +-spec fix_from_to(xmpp_element(), state()) -> stanza() | xmpp_element(). +fix_from_to(Pkt, #{jid := JID}) when ?is_stanza(Pkt) -> + #jid{luser = U, lserver = S, lresource = R} = JID, + case xmpp:get_from(Pkt) of + undefined -> + Pkt; + From -> + From1 = case jid:tolower(From) of + {U, S, R} -> JID; + {U, S, _} -> jid:replace_resource(JID, From#jid.resource); + _ -> From end, - handle_unacked_stanzas(NewState, SendFun), - send_element(NewState, - #xmlel{name = <<"r">>, - attrs = [{<<"xmlns">>, AttrXmlns}], - children = []}), - FlushedState = csi_queue_flush(NewState), - NewStateData = FlushedState#state{csi_state = active}, - ?INFO_MSG("Resumed session for ~s", - [jlib:jid_to_string(NewStateData#state.jid)]), - {ok, NewStateData}; - {error, El, Msg} -> - send_element(StateData, El), - ?INFO_MSG("Cannot resume session for ~s@~s: ~s", - [StateData#state.user, StateData#state.server, Msg]), - error - end. - -check_h_attribute(#state{mgmt_stanzas_out = NumStanzasOut} = StateData, H) - when H > NumStanzasOut -> - ?DEBUG("~s acknowledged ~B stanzas, but only ~B were sent", - [jlib:jid_to_string(StateData#state.jid), H, NumStanzasOut]), - mgmt_queue_drop(StateData#state{mgmt_stanzas_out = H}, NumStanzasOut); -check_h_attribute(#state{mgmt_stanzas_out = NumStanzasOut} = StateData, H) -> - ?DEBUG("~s acknowledged ~B of ~B stanzas", - [jlib:jid_to_string(StateData#state.jid), H, NumStanzasOut]), - mgmt_queue_drop(StateData, H). - -update_num_stanzas_in(#state{mgmt_state = active} = StateData, El) -> - NewNum = case {is_stanza(El), StateData#state.mgmt_stanzas_in} of - {true, 4294967295} -> - 0; - {true, Num} -> - Num + 1; - {false, Num} -> - Num - end, - StateData#state{mgmt_stanzas_in = NewNum}; -update_num_stanzas_in(StateData, _El) -> - StateData. - -send_stanza_and_ack_req(StateData, Stanza) -> - AckReq = #xmlel{name = <<"r">>, - attrs = [{<<"xmlns">>, StateData#state.mgmt_xmlns}], - children = []}, - StanzaS = xml:element_to_binary(Stanza), - AckReqS = xml:element_to_binary(AckReq), - send_text(StateData, [StanzaS, AckReqS]). - -mgmt_queue_add(StateData, El) -> - NewNum = case StateData#state.mgmt_stanzas_out of - 4294967295 -> - 0; - Num -> - Num + 1 - end, - NewQueue = queue:in({NewNum, now(), El}, StateData#state.mgmt_queue), - NewState = StateData#state{mgmt_queue = NewQueue, - mgmt_stanzas_out = NewNum}, - check_queue_length(NewState). - -mgmt_queue_drop(StateData, NumHandled) -> - NewQueue = jlib:queue_drop_while(fun({N, _T, _E}) -> N =< NumHandled end, - StateData#state.mgmt_queue), - StateData#state{mgmt_queue = NewQueue}. - -check_queue_length(#state{mgmt_max_queue = Limit} = StateData) - when Limit == infinity; - Limit == exceeded -> - StateData; -check_queue_length(#state{mgmt_queue = Queue, - mgmt_max_queue = Limit} = StateData) -> - case queue:len(Queue) > Limit of - true -> - StateData#state{mgmt_max_queue = exceeded}; - false -> - StateData - end. - -handle_unacked_stanzas(StateData, F) - when StateData#state.mgmt_state == active; - StateData#state.mgmt_state == pending -> - Queue = StateData#state.mgmt_queue, - case queue:len(Queue) of - 0 -> - ok; - N -> - ?INFO_MSG("~B stanzas were not acknowledged by ~s", - [N, jlib:jid_to_string(StateData#state.jid)]), - lists:foreach( - fun({_, Time, #xmlel{attrs = Attrs} = El}) -> - From_s = xml:get_attr_s(<<"from">>, Attrs), - From = jlib:string_to_jid(From_s), - To_s = xml:get_attr_s(<<"to">>, Attrs), - To = jlib:string_to_jid(To_s), - F(From, To, El, Time) - end, queue:to_list(Queue)) + To1 = case xmpp:get_to(Pkt) of + #jid{lresource = <<>>} = To2 -> To2; + _ -> JID + end, + xmpp:set_from_to(Pkt, From1, To1) end; -handle_unacked_stanzas(_StateData, _F) -> - ok. +fix_from_to(Pkt, _State) -> + Pkt. -handle_unacked_stanzas(StateData) - when StateData#state.mgmt_state == active; - StateData#state.mgmt_state == pending -> - ResendOnTimeout = - case StateData#state.mgmt_resend of - Resend when is_boolean(Resend) -> - Resend; - if_offline -> - ejabberd_sm:get_user_resources(StateData#state.user, - StateData#state.server) == [] - end, - ReRoute = case ResendOnTimeout of - true -> - fun(From, To, El, Time) -> - NewEl = add_resent_delay_info(StateData, El, Time), - ejabberd_router:route(From, To, NewEl) - end; - false -> - fun(From, To, El, _Time) -> - Err = - jlib:make_error_reply(El, - ?ERR_SERVICE_UNAVAILABLE), - ejabberd_router:route(To, From, Err) - end - end, - F = fun(From, To, El, Time) -> - %% We'll drop the stanza if it was by some - %% encapsulating protocol as per XEP-0297. One such protocol is - %% XEP-0280, which says: "When a receiving server attempts to - %% deliver a forked message, and that message bounces with an - %% error for any reason, the receiving server MUST NOT forward - %% that error back to the original sender." Resending such a - %% stanza could easily lead to unexpected results as well. - case is_encapsulated_forward(El) of - true -> - ?DEBUG("Dropping forwarded stanza from ~s", - [xml:get_attr_s(<<"from">>, El#xmlel.attrs)]); - false -> - ReRoute(From, To, El, Time) - end - end, - handle_unacked_stanzas(StateData, F); -handle_unacked_stanzas(_StateData) -> - ok. +-spec change_shaper(state()) -> state(). +change_shaper(#{shaper := ShaperName, ip := {IP, _}, lserver := LServer, + user := U, server := S, resource := R} = State) -> + JID = jid:make(U, S, R), + Shaper = ejabberd_shaper:match(LServer, ShaperName, + #{usr => jid:split(JID), ip => IP}), + xmpp_stream_in:change_shaper(State, ejabberd_shaper:new(Shaper)). -is_encapsulated_forward(#xmlel{name = <<"message">>} = El) -> - SubTag = case {xml:get_subtag(El, <<"sent">>), - xml:get_subtag(El, <<"received">>), - xml:get_subtag(El, <<"result">>)} of - {false, false, false} -> - false; - {Tag, false, false} -> - Tag; - {false, Tag, false} -> - Tag; - {_, _, Tag} -> - Tag - end, - if SubTag == false -> - false; - true -> - case xml:get_subtag(SubTag, <<"forwarded">>) of - false -> - false; - _ -> - true - end - end; -is_encapsulated_forward(_El) -> - false. +-spec format_reason(state(), term()) -> binary(). +format_reason(#{stop_reason := Reason}, _) -> + xmpp_stream_in:format_error(Reason); +format_reason(_, normal) -> + <<"unknown reason">>; +format_reason(_, shutdown) -> + <<"stopped by supervisor">>; +format_reason(_, {shutdown, _}) -> + <<"stopped by supervisor">>; +format_reason(_, _) -> + <<"internal server error">>. -inherit_session_state(#state{user = U, server = S} = StateData, ResumeID) -> - case jlib:base64_to_term(ResumeID) of - {term, {R, Time}} -> - case ejabberd_sm:get_session_pid(U, S, R) of - none -> - {error, <<"Previous session PID not found">>}; - OldPID -> - OldSID = {Time, OldPID}, - case catch resume_session(OldSID) of - {ok, OldStateData} -> - NewSID = {Time, self()}, % Old time, new PID - Priority = case OldStateData#state.pres_last of - undefined -> - 0; - Presence -> - get_priority_from_presence(Presence) - end, - Conn = get_conn_type(StateData), - Info = [{ip, StateData#state.ip}, {conn, Conn}, - {auth_module, StateData#state.auth_module}], - ejabberd_sm:open_session(NewSID, U, S, R, - Priority, Info), - {ok, StateData#state{conn = Conn, - sid = NewSID, - jid = OldStateData#state.jid, - resource = OldStateData#state.resource, - pres_t = OldStateData#state.pres_t, - pres_f = OldStateData#state.pres_f, - pres_a = OldStateData#state.pres_a, - pres_last = OldStateData#state.pres_last, - pres_timestamp = OldStateData#state.pres_timestamp, - privacy_list = OldStateData#state.privacy_list, - aux_fields = OldStateData#state.aux_fields, - csi_state = OldStateData#state.csi_state, - csi_queue = OldStateData#state.csi_queue, - mgmt_xmlns = OldStateData#state.mgmt_xmlns, - mgmt_queue = OldStateData#state.mgmt_queue, - mgmt_timeout = OldStateData#state.mgmt_timeout, - mgmt_stanzas_in = OldStateData#state.mgmt_stanzas_in, - mgmt_stanzas_out = OldStateData#state.mgmt_stanzas_out, - mgmt_state = active}}; - {error, Msg} -> - {error, Msg}; - _ -> - {error, <<"Cannot grab session state">>} - end - end; - _ -> - {error, <<"Invalid 'previd' value">>} - end. +listen_opt_type(starttls) -> + econf:bool(); +listen_opt_type(starttls_required) -> + econf:bool(); +listen_opt_type(allow_unencrypted_sasl2) -> + econf:bool(); +listen_opt_type(tls_verify) -> + econf:bool(); +listen_opt_type(zlib) -> + econf:and_then( + econf:bool(), + fun(false) -> false; + (true) -> + ejabberd:start_app(ezlib), + true + end). -resume_session({Time, PID}) -> - (?GEN_FSM):sync_send_all_state_event(PID, {resume_session, Time}, 3000). - -make_resume_id(StateData) -> - {Time, _} = StateData#state.sid, - jlib:term_to_base64({StateData#state.resource, Time}). - -add_resent_delay_info(#state{server = From}, El, Time) -> - jlib:add_delay_info(El, From, Time, <<"Resent">>). - -%%%---------------------------------------------------------------------- -%%% XEP-0352 -%%%---------------------------------------------------------------------- - -csi_filter_stanza(#state{csi_state = CsiState, jid = JID} = StateData, - Stanza) -> - Action = ejabberd_hooks:run_fold(csi_filter_stanza, - StateData#state.server, - send, [Stanza]), - ?DEBUG("Going to ~p stanza for inactive client ~p", - [Action, jlib:jid_to_string(JID)]), - case Action of - queue -> csi_queue_add(StateData, Stanza); - drop -> StateData; - send -> - From = xml:get_tag_attr_s(<<"from">>, Stanza), - StateData1 = csi_queue_send(StateData, From), - StateData2 = send_stanza(StateData1#state{csi_state = active}, - Stanza), - StateData2#state{csi_state = CsiState} - end. - -csi_queue_add(#state{csi_queue = Queue} = StateData, Stanza) -> - case length(StateData#state.csi_queue) >= csi_max_queue(StateData) of - true -> csi_queue_add(csi_queue_flush(StateData), Stanza); - false -> - From = xml:get_tag_attr_s(<<"from">>, Stanza), - NewQueue = lists:keystore(From, 1, Queue, {From, now(), Stanza}), - StateData#state{csi_queue = NewQueue} - end. - -csi_queue_send(#state{csi_queue = Queue, csi_state = CsiState, server = Host} = - StateData, From) -> - case lists:keytake(From, 1, Queue) of - {value, {From, Time, Stanza}, NewQueue} -> - NewStanza = jlib:add_delay_info(Stanza, Host, Time, - <<"Client Inactive">>), - NewStateData = send_stanza(StateData#state{csi_state = active}, - NewStanza), - NewStateData#state{csi_queue = NewQueue, csi_state = CsiState}; - false -> StateData - end. - -csi_queue_flush(#state{csi_queue = Queue, csi_state = CsiState, jid = JID, - server = Host} = StateData) -> - ?DEBUG("Flushing CSI queue for ~s", [jlib:jid_to_string(JID)]), - NewStateData = - lists:foldl(fun({_From, Time, Stanza}, AccState) -> - NewStanza = - jlib:add_delay_info(Stanza, Host, Time, - <<"Client Inactive">>), - send_stanza(AccState, NewStanza) - end, StateData#state{csi_state = active}, Queue), - NewStateData#state{csi_queue = [], csi_state = CsiState}. - -%% Make sure we won't push too many messages to the XEP-0198 queue when the -%% client becomes 'active' again. Otherwise, the client might not manage to -%% acknowledge the message flood in time. Also, don't let the queue grow to -%% more than 100 stanzas. -csi_max_queue(#state{mgmt_max_queue = infinity}) -> 100; -csi_max_queue(#state{mgmt_max_queue = Max}) when Max > 200 -> 100; -csi_max_queue(#state{mgmt_max_queue = Max}) when Max < 2 -> 1; -csi_max_queue(#state{mgmt_max_queue = Max}) -> Max div 2. - -%%%---------------------------------------------------------------------- -%%% JID Set memory footprint reduction code -%%%---------------------------------------------------------------------- - -%% Try to reduce the heap footprint of the four presence sets -%% by ensuring that we re-use strings and Jids wherever possible. -pack(S = #state{pres_a = A, pres_f = F, - pres_t = T}) -> - {NewA, Pack2} = pack_jid_set(A, gb_trees:empty()), - {NewF, Pack3} = pack_jid_set(F, Pack2), - {NewT, _Pack4} = pack_jid_set(T, Pack3), - S#state{pres_a = NewA, pres_f = NewF, - pres_t = NewT}. - -pack_jid_set(Set, Pack) -> - Jids = (?SETS):to_list(Set), - {PackedJids, NewPack} = pack_jids(Jids, Pack, []), - {(?SETS):from_list(PackedJids), NewPack}. - -pack_jids([], Pack, Acc) -> {Acc, Pack}; -pack_jids([{U, S, R} = Jid | Jids], Pack, Acc) -> - case gb_trees:lookup(Jid, Pack) of - {value, PackedJid} -> - pack_jids(Jids, Pack, [PackedJid | Acc]); - none -> - {NewU, Pack1} = pack_string(U, Pack), - {NewS, Pack2} = pack_string(S, Pack1), - {NewR, Pack3} = pack_string(R, Pack2), - NewJid = {NewU, NewS, NewR}, - NewPack = gb_trees:insert(NewJid, NewJid, Pack3), - pack_jids(Jids, NewPack, [NewJid | Acc]) - end. - -pack_string(String, Pack) -> - case gb_trees:lookup(String, Pack) of - {value, PackedString} -> {PackedString, Pack}; - none -> {String, gb_trees:insert(String, String, Pack)} - end. - -transform_listen_option(Opt, Opts) -> - [Opt|Opts]. +listen_options() -> + [{access, all}, + {shaper, none}, + {ciphers, undefined}, + {dhfile, undefined}, + {cafile, undefined}, + {protocol_options, undefined}, + {tls, false}, + {tls_compression, false}, + {starttls, false}, + {starttls_required, false}, + {allow_unencrypted_sasl2, false}, + {tls_verify, false}, + {zlib, false}, + {max_stanza_size, infinity}, + {max_fsm_queue, 10000}]. diff --git a/src/ejabberd_c2s_config.erl b/src/ejabberd_c2s_config.erl index a971f0af4..0c80ebec9 100644 --- a/src/ejabberd_c2s_config.erl +++ b/src/ejabberd_c2s_config.erl @@ -6,7 +6,7 @@ %%% Created : 2 Nov 2007 by Mickael Remond %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -33,33 +33,20 @@ %% Get first c2s configuration limitations to apply it to other c2s %% connectors. get_c2s_limits() -> - case ejabberd_config:get_option(listen, fun(V) -> V end) of - undefined -> []; - C2SFirstListen -> - case lists:keysearch(ejabberd_c2s, 2, C2SFirstListen) of - false -> []; - {value, {_Port, ejabberd_c2s, Opts}} -> - select_opts_values(Opts) - end + C2SFirstListen = ejabberd_option:listen(), + case lists:keysearch(ejabberd_c2s, 2, C2SFirstListen) of + false -> []; + {value, {_Port, ejabberd_c2s, Opts}} -> + select_opts_values(Opts) end. + %% Only get access, shaper and max_stanza_size values - select_opts_values(Opts) -> - select_opts_values(Opts, []). - -select_opts_values([], SelectedValues) -> - SelectedValues; -select_opts_values([{access, Value} | Opts], - SelectedValues) -> - select_opts_values(Opts, - [{access, Value} | SelectedValues]); -select_opts_values([{shaper, Value} | Opts], - SelectedValues) -> - select_opts_values(Opts, - [{shaper, Value} | SelectedValues]); -select_opts_values([{max_stanza_size, Value} | Opts], - SelectedValues) -> - select_opts_values(Opts, - [{max_stanza_size, Value} | SelectedValues]); -select_opts_values([_Opt | Opts], SelectedValues) -> - select_opts_values(Opts, SelectedValues). + maps:fold( + fun(Opt, Val, Acc) when Opt == access; + Opt == shaper; + Opt == max_stanza_size -> + [{Opt, Val}|Acc]; + (_, _, Acc) -> + Acc + end, [], Opts). diff --git a/src/ejabberd_captcha.erl b/src/ejabberd_captcha.erl index 110da1f69..d1d62e59b 100644 --- a/src/ejabberd_captcha.erl +++ b/src/ejabberd_captcha.erl @@ -5,7 +5,7 @@ %%% Created : 26 Apr 2008 by Evgeniy Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -25,6 +25,9 @@ -module(ejabberd_captcha). +-protocol({xep, 158, '1.0', '2.1.0', "complete", ""}). +-protocol({xep, 231, '1.0', '2.1.0', "complete", ""}). + -behaviour(gen_server). %% API @@ -37,250 +40,106 @@ -export([create_captcha/6, build_captcha_html/2, check_captcha/2, process_reply/1, process/2, is_feature_available/0, create_captcha_x/5, - create_captcha_x/6]). + host_up/1, host_down/1, + config_reloaded/0, process_iq/1]). --include("jlib.hrl"). - --include("ejabberd.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). -include("logger.hrl"). - -include("ejabberd_http.hrl"). - --define(VFIELD(Type, Var, Value), - #xmlel{name = <<"field">>, - attrs = [{<<"type">>, Type}, {<<"var">>, Var}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = [Value]}]}). - --define(CAPTCHA_TEXT(Lang), - translate:translate(Lang, - <<"Enter the text you see">>)). +-include("translate.hrl"). -define(CAPTCHA_LIFETIME, 120000). - -define(LIMIT_PERIOD, 60*1000*1000). --type error() :: efbig | enodata | limit | malformed_image | timeout. +-type image_error() :: efbig | enodata | limit | malformed_image | timeout. +-type priority() :: neg_integer(). +-type callback() :: fun((captcha_succeed | captcha_failed) -> any()). --record(state, {limits = treap:empty() :: treap:treap()}). +-record(state, {limits = treap:empty() :: treap:treap(), + enabled = false :: boolean()}). -record(captcha, {id :: binary(), - pid :: pid(), + pid :: pid() | undefined, key :: binary(), tref :: reference(), args :: any()}). -%%==================================================================== -%% API -%%==================================================================== -%%-------------------------------------------------------------------- -%% Function: start_link() -> {ok,Pid} | ignore | {error,Error} -%% Description: Starts the server -%%-------------------------------------------------------------------- start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). --spec create_captcha(binary(), jid(), jid(), - binary(), any(), any()) -> {error, error()} | - {ok, binary(), [xmlel()]}. +-spec captcha_text(binary()) -> binary(). +captcha_text(Lang) -> + translate:translate(Lang, ?T("Enter the text you see")). +-spec mk_ocr_field(binary(), binary(), binary()) -> xdata_field(). +mk_ocr_field(Lang, CID, Type) -> + URI = #media_uri{type = Type, uri = <<"cid:", CID/binary>>}, + [_, F] = captcha_form:encode([{ocr, <<>>}], Lang, [ocr]), + xmpp:set_els(F, [#media{uri = [URI]}]). + +update_captcha_key(_Id, Key, Key) -> + ok; +update_captcha_key(Id, _Key, Key2) -> + true = ets:update_element(captcha, Id, [{4, Key2}]). + +-spec create_captcha(binary(), jid(), jid(), + binary(), any(), + callback() | term()) -> {error, image_error()} | + {ok, binary(), [text()], [xmpp_element()]}. create_captcha(SID, From, To, Lang, Limiter, Args) -> case create_image(Limiter) of {ok, Type, Key, Image} -> - Id = <<(randoms:get_string())/binary>>, - B64Image = jlib:encode_base64((Image)), - JID = jlib:jid_to_string(From), - CID = <<"sha1+", (p1_sha:sha(Image))/binary, - "@bob.xmpp.org">>, - Data = #xmlel{name = <<"data">>, - attrs = - [{<<"xmlns">>, ?NS_BOB}, {<<"cid">>, CID}, - {<<"max-age">>, <<"0">>}, {<<"type">>, Type}], - children = [{xmlcdata, B64Image}]}, - Captcha = #xmlel{name = <<"captcha">>, - attrs = [{<<"xmlns">>, ?NS_CAPTCHA}], - children = - [#xmlel{name = <<"x">>, - attrs = - [{<<"xmlns">>, ?NS_XDATA}, - {<<"type">>, <<"form">>}], - children = - [?VFIELD(<<"hidden">>, - <<"FORM_TYPE">>, - {xmlcdata, ?NS_CAPTCHA}), - ?VFIELD(<<"hidden">>, <<"from">>, - {xmlcdata, - jlib:jid_to_string(To)}), - ?VFIELD(<<"hidden">>, - <<"challenge">>, - {xmlcdata, Id}), - ?VFIELD(<<"hidden">>, <<"sid">>, - {xmlcdata, SID}), - #xmlel{name = <<"field">>, - attrs = - [{<<"var">>, <<"ocr">>}, - {<<"label">>, - ?CAPTCHA_TEXT(Lang)}], - children = - [#xmlel{name = - <<"required">>, - attrs = [], - children = []}, - #xmlel{name = - <<"media">>, - attrs = - [{<<"xmlns">>, - ?NS_MEDIA}], - children = - [#xmlel{name - = - <<"uri">>, - attrs - = - [{<<"type">>, - Type}], - children - = - [{xmlcdata, - <<"cid:", - CID/binary>>}]}]}]}]}]}, - BodyString1 = translate:translate(Lang, - <<"Your messages to ~s are being blocked. " - "To unblock them, visit ~s">>), - BodyString = iolist_to_binary(io_lib:format(BodyString1, - [JID, get_url(Id)])), - Body = #xmlel{name = <<"body">>, attrs = [], - children = [{xmlcdata, BodyString}]}, - OOB = #xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_OOB}], - children = - [#xmlel{name = <<"url">>, attrs = [], - children = [{xmlcdata, get_url(Id)}]}]}, - Tref = erlang:send_after(?CAPTCHA_LIFETIME, ?MODULE, - {remove_id, Id}), - ets:insert(captcha, - #captcha{id = Id, pid = self(), key = Key, tref = Tref, - args = Args}), - {ok, Id, [Body, OOB, Captcha, Data]}; - Err -> Err + Id = <<(p1_rand:get_string())/binary>>, + JID = jid:encode(From), + CID = <<"sha1+", (str:sha(Image))/binary, "@bob.xmpp.org">>, + Data = #bob_data{cid = CID, 'max-age' = 0, type = Type, data = Image}, + Fs = captcha_form:encode( + [{from, To}, {challenge, Id}, {sid, SID}, + mk_ocr_field(Lang, CID, Type)], + Lang, [challenge]), + X = #xdata{type = form, fields = Fs}, + Captcha = #xcaptcha{xdata = X}, + BodyString = {?T("Your subscription request and/or messages to ~s have been blocked. " + "To unblock your subscription request, visit ~s"), [JID, get_url(Id)]}, + Body = xmpp:mk_text(BodyString, Lang), + OOB = #oob_x{url = get_url(Id)}, + Hint = #hint{type = 'no-store'}, + Tref = erlang:send_after(?CAPTCHA_LIFETIME, ?MODULE, {remove_id, Id}), + ets:insert(captcha, + #captcha{id = Id, pid = self(), key = Key, tref = Tref, + args = Args}), + {ok, Id, Body, [Hint, OOB, Captcha, Data]}; + Err -> Err end. --spec create_captcha_x(binary(), jid(), binary(), - any(), [xmlel()]) -> {ok, [xmlel()]} | - {error, error()}. - -create_captcha_x(SID, To, Lang, Limiter, HeadEls) -> - create_captcha_x(SID, To, Lang, Limiter, HeadEls, []). - --spec create_captcha_x(binary(), jid(), binary(), - any(), [xmlel()], [xmlel()]) -> {ok, [xmlel()]} | - {error, error()}. - -create_captcha_x(SID, To, Lang, Limiter, HeadEls, - TailEls) -> +-spec create_captcha_x(binary(), jid(), binary(), any(), xdata()) -> + {ok, [xmpp_element()]} | {error, image_error()}. +create_captcha_x(SID, To, Lang, Limiter, #xdata{fields = Fs} = X) -> case create_image(Limiter) of {ok, Type, Key, Image} -> - Id = <<(randoms:get_string())/binary>>, - B64Image = jlib:encode_base64((Image)), - CID = <<"sha1+", (p1_sha:sha(Image))/binary, - "@bob.xmpp.org">>, - Data = #xmlel{name = <<"data">>, - attrs = - [{<<"xmlns">>, ?NS_BOB}, {<<"cid">>, CID}, - {<<"max-age">>, <<"0">>}, {<<"type">>, Type}], - children = [{xmlcdata, B64Image}]}, - HelpTxt = translate:translate(Lang, - <<"If you don't see the CAPTCHA image here, " - "visit the web page.">>), - Imageurl = get_url(<>), - Captcha = #xmlel{name = <<"x">>, - attrs = - [{<<"xmlns">>, ?NS_XDATA}, - {<<"type">>, <<"form">>}], - children = - [?VFIELD(<<"hidden">>, <<"FORM_TYPE">>, - {xmlcdata, ?NS_CAPTCHA}) - | HeadEls] - ++ - [#xmlel{name = <<"field">>, - attrs = [{<<"type">>, <<"fixed">>}], - children = - [#xmlel{name = <<"value">>, - attrs = [], - children = - [{xmlcdata, - HelpTxt}]}]}, - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, <<"hidden">>}, - {<<"var">>, <<"captchahidden">>}], - children = - [#xmlel{name = <<"value">>, - attrs = [], - children = - [{xmlcdata, - <<"workaround-for-psi">>}]}]}, - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, <<"text-single">>}, - {<<"label">>, - translate:translate(Lang, - <<"CAPTCHA web page">>)}, - {<<"var">>, <<"url">>}], - children = - [#xmlel{name = <<"value">>, - attrs = [], - children = - [{xmlcdata, - Imageurl}]}]}, - ?VFIELD(<<"hidden">>, <<"from">>, - {xmlcdata, jlib:jid_to_string(To)}), - ?VFIELD(<<"hidden">>, <<"challenge">>, - {xmlcdata, Id}), - ?VFIELD(<<"hidden">>, <<"sid">>, - {xmlcdata, SID}), - #xmlel{name = <<"field">>, - attrs = - [{<<"var">>, <<"ocr">>}, - {<<"label">>, - ?CAPTCHA_TEXT(Lang)}], - children = - [#xmlel{name = <<"required">>, - attrs = [], children = []}, - #xmlel{name = <<"media">>, - attrs = - [{<<"xmlns">>, - ?NS_MEDIA}], - children = - [#xmlel{name = - <<"uri">>, - attrs = - [{<<"type">>, - Type}], - children = - [{xmlcdata, - <<"cid:", - CID/binary>>}]}]}]}] - ++ TailEls}, - Tref = erlang:send_after(?CAPTCHA_LIFETIME, ?MODULE, - {remove_id, Id}), - ets:insert(captcha, - #captcha{id = Id, key = Key, tref = Tref}), - {ok, [Captcha, Data]}; - Err -> Err + Id = <<(p1_rand:get_string())/binary>>, + CID = <<"sha1+", (str:sha(Image))/binary, "@bob.xmpp.org">>, + Data = #bob_data{cid = CID, 'max-age' = 0, type = Type, data = Image}, + HelpTxt = translate:translate( + Lang, ?T("If you don't see the CAPTCHA image here, visit the web page.")), + Imageurl = get_url(<>), + [H|T] = captcha_form:encode( + [{'captcha-fallback-text', HelpTxt}, + {'captcha-fallback-url', Imageurl}, + {from, To}, {challenge, Id}, {sid, SID}, + mk_ocr_field(Lang, CID, Type)], + Lang, [challenge]), + Captcha = X#xdata{type = form, fields = [H|Fs ++ T]}, + Tref = erlang:send_after(?CAPTCHA_LIFETIME, ?MODULE, {remove_id, Id}), + ets:insert(captcha, #captcha{id = Id, key = Key, tref = Tref}), + {ok, [Captcha, Data]}; + Err -> Err end. -%% @spec (Id::string(), Lang::string()) -> {FormEl, {ImgEl, TextEl, IdEl, KeyEl}} | captcha_not_found -%% where FormEl = xmlelement() -%% ImgEl = xmlelement() -%% TextEl = xmlelement() -%% IdEl = xmlelement() -%% KeyEl = xmlelement() -spec build_captcha_html(binary(), binary()) -> captcha_not_found | {xmlel(), - {xmlel(), xmlel(), + {xmlel(), cdata(), xmlel(), xmlel()}}. build_captcha_html(Id, Lang) -> @@ -290,7 +149,7 @@ build_captcha_html(Id, Lang) -> attrs = [{<<"src">>, get_url(<>)}], children = []}, - TextEl = {xmlcdata, ?CAPTCHA_TEXT(Lang)}, + Text = {xmlcdata, captcha_text(Lang)}, IdEl = #xmlel{name = <<"input">>, attrs = [{<<"type">>, <<"hidden">>}, {<<"name">>, <<"id">>}, @@ -310,7 +169,7 @@ build_captcha_html(Id, Lang) -> [ImgEl, #xmlel{name = <<"br">>, attrs = [], children = []}, - TextEl, + Text, #xmlel{name = <<"br">>, attrs = [], children = []}, IdEl, KeyEl, @@ -320,45 +179,63 @@ build_captcha_html(Id, Lang) -> attrs = [{<<"type">>, <<"submit">>}, {<<"name">>, <<"enter">>}, - {<<"value">>, <<"OK">>}], + {<<"value">>, ?T("OK")}], children = []}]}, - {FormEl, {ImgEl, TextEl, IdEl, KeyEl}}; + {FormEl, {ImgEl, Text, IdEl, KeyEl}}; _ -> captcha_not_found end. -%% @spec (Id::string(), ProvidedKey::string()) -> captcha_valid | captcha_non_valid | captcha_not_found --spec check_captcha(binary(), binary()) -> captcha_not_found | - captcha_valid | - captcha_non_valid. +-spec process_reply(xmpp_element()) -> ok | {error, bad_match | not_found | malformed}. - --spec process_reply(xmlel()) -> ok | {error, bad_match | not_found | malformed}. - -process_reply(#xmlel{} = El) -> - case xml:get_subtag(El, <<"x">>) of - false -> {error, malformed}; - Xdata -> - Fields = jlib:parse_xdata_submit(Xdata), - case catch {proplists:get_value(<<"challenge">>, - Fields), - proplists:get_value(<<"ocr">>, Fields)} - of - {[Id | _], [OCR | _]} -> - case check_captcha(Id, OCR) of - captcha_valid -> ok; - captcha_non_valid -> {error, bad_match}; - captcha_not_found -> {error, not_found} - end; - _ -> {error, malformed} - end +process_reply(#xdata{} = X) -> + Required = [<<"challenge">>, <<"ocr">>], + Fs = lists:filter( + fun(#xdata_field{var = Var}) -> + lists:member(Var, [<<"FORM_TYPE">>|Required]) + end, X#xdata.fields), + try captcha_form:decode(Fs, [?NS_CAPTCHA], Required) of + Props -> + Id = proplists:get_value(challenge, Props), + OCR = proplists:get_value(ocr, Props), + case check_captcha(Id, OCR) of + captcha_valid -> ok; + captcha_non_valid -> {error, bad_match}; + captcha_not_found -> {error, not_found} + end + catch _:{captcha_form, Why} -> + ?WARNING_MSG("Malformed CAPTCHA form: ~ts", + [captcha_form:format_error(Why)]), + {error, malformed} end; -process_reply(_) -> {error, malformed}. +process_reply(#xcaptcha{xdata = #xdata{} = X}) -> + process_reply(X); +process_reply(_) -> + {error, malformed}. + +-spec process_iq(iq()) -> iq(). +process_iq(#iq{type = set, lang = Lang, sub_els = [#xcaptcha{} = El]} = IQ) -> + case process_reply(El) of + ok -> + xmpp:make_iq_result(IQ); + {error, malformed} -> + Txt = ?T("Incorrect CAPTCHA submit"), + xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)); + {error, _} -> + Txt = ?T("The CAPTCHA verification has failed"), + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)) + end; +process_iq(#iq{type = get, lang = Lang} = IQ) -> + Txt = ?T("Value 'get' of 'type' attribute is not allowed"), + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); +process_iq(#iq{lang = Lang} = IQ) -> + Txt = ?T("No module is handling this query"), + xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)). process(_Handlers, #request{method = 'GET', lang = Lang, path = [_, Id]}) -> case build_captcha_html(Id, Lang) of - {FormEl, _} when is_tuple(FormEl) -> + {FormEl, _} -> Form = #xmlel{name = <<"div">>, attrs = [{<<"align">>, <<"center">>}], children = [FormEl]}, @@ -372,7 +249,8 @@ process(_Handlers, case lookup_captcha(Id) of {ok, #captcha{key = Key}} -> case create_image(Addr, Key) of - {ok, Type, _, Img} -> + {ok, Type, Key2, Img} -> + update_captcha_key(Id, Key, Key2), {200, [{<<"Content-Type">>, Type}, {<<"Cache-Control">>, <<"no-cache">>}, @@ -393,7 +271,7 @@ process(_Handlers, children = [{xmlcdata, translate:translate(Lang, - <<"The CAPTCHA is valid.">>)}]}, + ?T("The CAPTCHA is valid."))}]}, ejabberd_web:make_xhtml([Form]); captcha_non_valid -> ejabberd_web:error(not_allowed); captcha_not_found -> ejabberd_web:error(not_found) @@ -401,15 +279,29 @@ process(_Handlers, process(_Handlers, _Request) -> ejabberd_web:error(not_found). -%%==================================================================== -%% gen_server callbacks -%%==================================================================== +host_up(Host) -> + gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_CAPTCHA, + ?MODULE, process_iq). + +host_down(Host) -> + gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_CAPTCHA). + +config_reloaded() -> + gen_server:call(?MODULE, config_reloaded, timer:minutes(1)). + init([]) -> - mnesia:delete_table(captcha), - ets:new(captcha, - [named_table, public, {keypos, #captcha.id}]), - check_captcha_setup(), - {ok, #state{}}. + _ = mnesia:delete_table(captcha), + _ = ets:new(captcha, [named_table, public, {keypos, #captcha.id}]), + case check_captcha_setup() of + true -> + register_handlers(), + ejabberd_hooks:add(config_reloaded, ?MODULE, config_reloaded, 70), + {ok, #state{enabled = true}}; + false -> + {ok, #state{enabled = false}}; + {error, Reason} -> + {stop, Reason} + end. handle_call({is_limited, Limiter, RateLimit}, _From, State) -> @@ -428,53 +320,98 @@ handle_call({is_limited, Limiter, RateLimit}, _From, Limits), {reply, false, State#state{limits = NewLimits}} end; -handle_call(_Request, _From, State) -> - {reply, bad_request, State}. +handle_call(config_reloaded, _From, #state{enabled = Enabled} = State) -> + State1 = case is_feature_available() of + true when not Enabled -> + case check_captcha_setup() of + true -> + register_handlers(), + State#state{enabled = true}; + _ -> + State + end; + false when Enabled -> + unregister_handlers(), + State#state{enabled = false}; + _ -> + State + end, + {reply, ok, State1}; +handle_call(Request, From, State) -> + ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), + {noreply, State}. -handle_cast(_Msg, State) -> {noreply, State}. +handle_cast(Msg, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Msg]), + {noreply, State}. handle_info({remove_id, Id}, State) -> - ?DEBUG("captcha ~p timed out", [Id]), + ?DEBUG("CAPTCHA ~p timed out", [Id]), case ets:lookup(captcha, Id) of - [#captcha{args = Args, pid = Pid}] -> - if is_pid(Pid) -> Pid ! {captcha_failed, Args}; - true -> ok - end, - ets:delete(captcha, Id); - _ -> ok + [#captcha{args = Args, pid = Pid}] -> + callback(captcha_failed, Pid, Args), + ets:delete(captcha, Id); + _ -> ok end, {noreply, State}; -handle_info(_Info, State) -> {noreply, State}. +handle_info(Info, State) -> + ?WARNING_MSG("Unexpected info: ~p", [Info]), + {noreply, State}. -terminate(_Reason, _State) -> ok. +terminate(_Reason, #state{enabled = Enabled}) -> + if Enabled -> unregister_handlers(); + true -> ok + end, + ejabberd_hooks:delete(config_reloaded, ?MODULE, config_reloaded, 70). + +register_handlers() -> + ejabberd_hooks:add(host_up, ?MODULE, host_up, 50), + ejabberd_hooks:add(host_down, ?MODULE, host_down, 50), + lists:foreach(fun host_up/1, ejabberd_option:hosts()). + +unregister_handlers() -> + ejabberd_hooks:delete(host_up, ?MODULE, host_up, 50), + ejabberd_hooks:delete(host_down, ?MODULE, host_down, 50), + lists:foreach(fun host_down/1, ejabberd_option:hosts()). code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%-------------------------------------------------------------------- -%%% Internal functions -%%-------------------------------------------------------------------- -%%-------------------------------------------------------------------- -%% Function: create_image() -> {ok, Type, Key, Image} | {error, Reason} -%% Type = "image/png" | "image/jpeg" | "image/gif" -%% Key = string() -%% Image = binary() -%% Reason = atom() -%%-------------------------------------------------------------------- -create_image() -> create_image(undefined). +-spec create_image() -> {ok, binary(), binary(), binary()} | + {error, image_error()}. +create_image() -> + create_image(undefined). +-spec create_image(term()) -> {ok, binary(), binary(), binary()} | + {error, image_error()}. create_image(Limiter) -> - Key = str:substr(randoms:get_string(), 1, 6), + Key = str:substr(p1_rand:get_string(), 1, 6), create_image(Limiter, Key). +-spec create_image(term(), binary()) -> {ok, binary(), binary(), binary()} | + {error, image_error()}. create_image(Limiter, Key) -> case is_limited(Limiter) of - true -> {error, limit}; - false -> do_create_image(Key) + true -> {error, limit}; + false -> do_create_image(Key) end. +-spec do_create_image(binary()) -> {ok, binary(), binary(), binary()} | + {error, image_error()}. do_create_image(Key) -> FileName = get_prog_name(), - Cmd = lists:flatten(io_lib:format("~s ~s", [FileName, Key])), + case length(binary:split(FileName, <<"/">>)) == 1 of + true -> + do_create_image(Key, misc:binary_to_atom(FileName)); + false -> + do_create_image(Key, FileName) + end. + +do_create_image(Key, Module) when is_atom(Module) -> + Function = create_image, + erlang:apply(Module, Function, [Key]); + +do_create_image(Key, FileName) when is_binary(FileName) -> + Cmd = lists:flatten(io_lib:format("~ts ~ts", [FileName, Key])), case cmd(Cmd) of {ok, <<137, $P, $N, $G, $\r, $\n, 26, $\n, _/binary>> = @@ -486,102 +423,155 @@ do_create_image(Key) -> when X == $7; X == $9 -> {ok, <<"image/gif">>, Key, Img}; {error, enodata = Reason} -> - ?ERROR_MSG("Failed to process output from \"~s\". " + ?ERROR_MSG("Failed to process output from \"~ts\". " "Maybe ImageMagick's Convert program " "is not installed.", [Cmd]), {error, Reason}; {error, Reason} -> - ?ERROR_MSG("Failed to process an output from \"~s\": ~p", + ?ERROR_MSG("Failed to process an output from \"~ts\": ~p", [Cmd, Reason]), {error, Reason}; _ -> Reason = malformed_image, - ?ERROR_MSG("Failed to process an output from \"~s\": ~p", + ?ERROR_MSG("Failed to process an output from \"~ts\": ~p", [Cmd, Reason]), {error, Reason} end. get_prog_name() -> - case ejabberd_config:get_option( - captcha_cmd, - fun(FileName) -> - F = iolist_to_binary(FileName), - if F /= <<"">> -> F end - end) of + case ejabberd_option:captcha_cmd() of undefined -> - ?DEBUG("The option captcha_cmd is not configured, " + ?WARNING_MSG("The option captcha_cmd is not configured, " "but some module wants to use the CAPTCHA " "feature.", []), false; FileName -> + maybe_warning_norequesthandler(), FileName end. -get_url(Str) -> - CaptchaHost = ejabberd_config:get_option( - captcha_host, - fun iolist_to_binary/1, - <<"">>), - case str:tokens(CaptchaHost, <<":">>) of - [Host] -> - <<"http://", Host/binary, "/captcha/", Str/binary>>; - [<<"http", _/binary>> = TransferProt, Host] -> - <>; - [Host, PortString] -> - TransferProt = - iolist_to_binary(atom_to_list(get_transfer_protocol(PortString))), - <>; - [TransferProt, Host, PortString] -> - <>; - _ -> - <<"http://", (?MYNAME)/binary, "/captcha/", Str/binary>> +maybe_warning_norequesthandler() -> + Host = hd(ejabberd_option:hosts()), + AutoURL = get_auto_url(any, ?MODULE, Host), + ManualURL = ejabberd_option:captcha_url(), + case (AutoURL == undefined) and not is_binary(ManualURL) of + true -> + ?CRITICAL_MSG("The option captcha_cmd is configured " + "and captcha_url is set to auto, " + "but I couldn't find a request_handler in listen option " + "configured with ejabberd_captcha and integer port. " + "Please setup the URL with option captcha_url, see " + "https://docs.ejabberd.im/admin/configuration/basic/#captcha", + []); + _ -> + ok end. +-spec get_url(binary()) -> binary(). +get_url(Str) -> + case ejabberd_option:captcha_url() of + auto -> + Host = ejabberd_config:get_myname(), + URL = get_auto_url(any, ?MODULE, Host), + <>; + undefined -> + URL = parse_captcha_host(), + <>; + URL -> + <> + end. + +-spec parse_captcha_host() -> binary(). +parse_captcha_host() -> + CaptchaHost = ejabberd_option:captcha_host(), + case str:tokens(CaptchaHost, <<":">>) of + [Host] -> + <<"http://", Host/binary>>; + [<<"http", _/binary>> = TransferProt, Host] -> + <>; + [Host, PortString] -> + TransferProt = atom_to_binary(get_transfer_protocol(PortString), latin1), + <>; + [TransferProt, Host, PortString] -> + <>; + _ -> + <<"http://", (ejabberd_config:get_myname())/binary>> + end. + +get_auto_url(Tls, Module, Host) -> + case find_handler_port_path(Tls, Module) of + [] -> undefined; + TPPs -> + {ThisTls, Port, Path} = case lists:keyfind(true, 1, TPPs) of + false -> + lists:keyfind(false, 1, TPPs); + TPP -> + TPP + end, + Protocol = case ThisTls of + false -> <<"http">>; + true -> <<"https">> + end, + <>))/binary>> + end. + +find_handler_port_path(Tls, Module) -> + lists:filtermap( + fun({{Port, _, _}, + ejabberd_http, + #{tls := ThisTls, request_handlers := Handlers}}) + when is_integer(Port) and ((Tls == any) or (Tls == ThisTls)) -> + case lists:keyfind(Module, 2, Handlers) of + false -> false; + {Path, Module} -> {true, {ThisTls, Port, Path}} + end; + (_) -> false + end, ets:tab2list(ejabberd_listener)). + get_transfer_protocol(PortString) -> - PortNumber = jlib:binary_to_integer(PortString), + PortNumber = binary_to_integer(PortString), PortListeners = get_port_listeners(PortNumber), get_captcha_transfer_protocol(PortListeners). get_port_listeners(PortNumber) -> - AllListeners = ejabberd_config:get_option(listen, fun(V) -> V end), - lists:filter(fun (Listener) when is_list(Listener) -> - case proplists:get_value(port, Listener) of - PortNumber -> true; - _ -> false - end; - (_) -> false - end, - AllListeners). + AllListeners = ejabberd_option:listen(), + lists:filter( + fun({{Port, _IP, _Transport}, _Module, _Opts}) -> + Port == PortNumber + end, AllListeners). get_captcha_transfer_protocol([]) -> throw(<<"The port number mentioned in captcha_host " "is not a ejabberd_http listener with " "'captcha' option. Change the port number " "or specify http:// in that option.">>); -get_captcha_transfer_protocol([Listener | Listeners]) when is_list(Listener) -> - case proplists:get_value(module, Listener) == ejabberd_http andalso - proplists:get_bool(captcha, Listener) of - true -> - case proplists:get_bool(tls, Listener) of - true -> https; - false -> http - end; - false -> get_captcha_transfer_protocol(Listeners) +get_captcha_transfer_protocol([{_, ejabberd_http, Opts} | Listeners]) -> + Handlers = maps:get(request_handlers, Opts, []), + case lists:any( + fun({_, ?MODULE}) -> true; + ({_, _}) -> false + end, Handlers) of + true -> + case maps:get(tls, Opts) of + true -> https; + false -> http + end; + false -> + get_captcha_transfer_protocol(Listeners) end; get_captcha_transfer_protocol([_ | Listeners]) -> get_captcha_transfer_protocol(Listeners). is_limited(undefined) -> false; is_limited(Limiter) -> - case ejabberd_config:get_option( - captcha_limit, - fun(I) when is_integer(I), I > 0 -> I end) of - undefined -> false; + case ejabberd_option:captcha_limit() of + infinity -> false; Int -> case catch gen_server:call(?MODULE, {is_limited, Limiter, Int}, 5000) @@ -592,22 +582,18 @@ is_limited(Limiter) -> end end. -%%-------------------------------------------------------------------- -%% Function: cmd(Cmd) -> Data | {error, Reason} -%% Cmd = string() -%% Data = binary() -%% Description: os:cmd/1 replacement -%%-------------------------------------------------------------------- -define(CMD_TIMEOUT, 5000). -define(MAX_FILE_SIZE, 64 * 1024). +-spec cmd(string()) -> {ok, binary()} | {error, image_error()}. cmd(Cmd) -> Port = open_port({spawn, Cmd}, [stream, eof, binary]), TRef = erlang:start_timer(?CMD_TIMEOUT, self(), timeout), recv_data(Port, TRef, <<>>). +-spec recv_data(port(), reference(), binary()) -> {ok, binary()} | {error, image_error()}. recv_data(Port, TRef, Buf) -> receive {Port, {data, Bytes}} -> @@ -624,61 +610,63 @@ recv_data(Port, TRef, Buf) -> return(Port, TRef, {error, timeout}) end. +-spec return(port(), reference(), {ok, binary()} | {error, image_error()}) -> + {ok, binary()} | {error, image_error()}. return(Port, TRef, Result) -> - case erlang:cancel_timer(TRef) of - false -> - receive {timeout, TRef, _} -> ok after 0 -> ok end; - _ -> ok - end, + misc:cancel_timer(TRef), catch port_close(Port), Result. is_feature_available() -> case get_prog_name() of - Prog when is_binary(Prog) -> true; + PathOrModule when is_binary(PathOrModule) -> true; false -> false end. check_captcha_setup() -> case is_feature_available() of - true -> - case create_image() of - {ok, _, _, _} -> ok; - _Err -> - ?CRITICAL_MSG("Captcha is enabled in the option captcha_cmd, " - "but it can't generate images.", - []), - throw({error, captcha_cmd_enabled_but_fails}) - end; - false -> ok + true -> + case create_image() of + {ok, _, _, _} -> + true; + Err -> + ?CRITICAL_MSG("Captcha is enabled in the option captcha_cmd, " + "but it can't generate images.", + []), + Err + end; + false -> + false end. +-spec lookup_captcha(binary()) -> {ok, #captcha{}} | {error, enoent}. lookup_captcha(Id) -> case ets:lookup(captcha, Id) of [C] -> {ok, C}; - _ -> {error, enoent} + [] -> {error, enoent} end. +-spec check_captcha(binary(), binary()) -> captcha_not_found | + captcha_valid | + captcha_non_valid. + check_captcha(Id, ProvidedKey) -> - case ets:lookup(captcha, Id) of - [#captcha{pid = Pid, args = Args, key = ValidKey, - tref = Tref}] -> - ets:delete(captcha, Id), - erlang:cancel_timer(Tref), - if ValidKey == ProvidedKey -> - if is_pid(Pid) -> Pid ! {captcha_succeed, Args}; - true -> ok - end, - captcha_valid; - true -> - if is_pid(Pid) -> Pid ! {captcha_failed, Args}; - true -> ok - end, - captcha_non_valid - end; - _ -> captcha_not_found + case lookup_captcha(Id) of + {ok, #captcha{pid = Pid, args = Args, key = ValidKey, tref = Tref}} -> + ets:delete(captcha, Id), + misc:cancel_timer(Tref), + if ValidKey == ProvidedKey -> + callback(captcha_succeed, Pid, Args), + captcha_valid; + true -> + callback(captcha_failed, Pid, Args), + captcha_non_valid + end; + {error, _} -> + captcha_not_found end. +-spec clean_treap(treap:treap(), priority()) -> treap:treap(). clean_treap(Treap, CleanPriority) -> case treap:is_empty(Treap) of true -> Treap; @@ -690,6 +678,16 @@ clean_treap(Treap, CleanPriority) -> end end. +-spec callback(captcha_succeed | captcha_failed, + pid() | undefined, + callback() | term()) -> any(). +callback(Result, _Pid, F) when is_function(F) -> + F(Result); +callback(Result, Pid, Args) when is_pid(Pid) -> + Pid ! {Result, Args}; +callback(_, _, _) -> + ok. + +-spec now_priority() -> priority(). now_priority() -> - {MSec, Sec, USec} = now(), - -((MSec * 1000000 + Sec) * 1000000 + USec). + -erlang:system_time(microsecond). diff --git a/src/ejabberd_cluster.erl b/src/ejabberd_cluster.erl new file mode 100644 index 000000000..38a378d30 --- /dev/null +++ b/src/ejabberd_cluster.erl @@ -0,0 +1,227 @@ +%%%------------------------------------------------------------------- +%%% Author : Evgeny Khramtsov +%%% Created : 5 Jul 2017 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(ejabberd_cluster). +-behaviour(gen_server). + +%% API +-export([start_link/0, call/4, call/5, multicall/3, multicall/4, multicall/5, + eval_everywhere/3, eval_everywhere/4]). +%% Backend dependent API +-export([get_nodes/0, get_known_nodes/0, join/1, leave/1, subscribe/0, + subscribe/1, node_id/0, get_node_by_id/1, send/2, wait_for_sync/1]). +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). +%% hooks +-export([set_ticktime/0]). + +-include("logger.hrl"). + +-type dst() :: pid() | atom() | {atom(), node()}. + +-callback init() -> ok | {error, any()}. +-callback get_nodes() -> [node()]. +-callback get_known_nodes() -> [node()]. +-callback join(node()) -> ok | {error, any()}. +-callback leave(node()) -> ok | {error, any()}. +-callback node_id() -> binary(). +-callback get_node_by_id(binary()) -> node(). +-callback send({atom(), node()}, term()) -> boolean(). +-callback wait_for_sync(timeout()) -> ok | {error, any()}. +-callback subscribe(dst()) -> ok. + +-record(state, {}). + +%%%=================================================================== +%%% API +%%%=================================================================== +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +-spec call(node(), module(), atom(), [any()]) -> any(). +call(Node, Module, Function, Args) -> + call(Node, Module, Function, Args, rpc_timeout()). + +-spec call(node(), module(), atom(), [any()], timeout()) -> any(). +call(Node, Module, Function, Args, Timeout) -> + rpc:call(Node, Module, Function, Args, Timeout). + +-spec multicall(module(), atom(), [any()]) -> {list(), [node()]}. +multicall(Module, Function, Args) -> + multicall(get_nodes(), Module, Function, Args). + +-spec multicall([node()], module(), atom(), list()) -> {list(), [node()]}. +multicall(Nodes, Module, Function, Args) -> + multicall(Nodes, Module, Function, Args, rpc_timeout()). + +-spec multicall([node()], module(), atom(), list(), timeout()) -> {list(), [node()]}. +multicall(Nodes, Module, Function, Args, Timeout) -> + rpc:multicall(Nodes, Module, Function, Args, Timeout). + +-spec eval_everywhere(module(), atom(), [any()]) -> ok. +eval_everywhere(Module, Function, Args) -> + eval_everywhere(get_nodes(), Module, Function, Args), + ok. + +-spec eval_everywhere([node()], module(), atom(), [any()]) -> ok. +eval_everywhere(Nodes, Module, Function, Args) -> + rpc:eval_everywhere(Nodes, Module, Function, Args), + ok. + +%%%=================================================================== +%%% Backend dependent API +%%%=================================================================== +-spec get_nodes() -> [node()]. +get_nodes() -> + Mod = get_mod(), + Mod:get_nodes(). + +-spec get_known_nodes() -> [node()]. +get_known_nodes() -> + Mod = get_mod(), + Mod:get_known_nodes(). + +-spec join(node()) -> ok | {error, any()}. +join(Node) -> + Mod = get_mod(), + Mod:join(Node). + +-spec leave(node()) -> ok | {error, any()}. +leave(Node) -> + Mod = get_mod(), + Mod:leave(Node). + +-spec node_id() -> binary(). +node_id() -> + Mod = get_mod(), + Mod:node_id(). + +-spec get_node_by_id(binary()) -> node(). +get_node_by_id(ID) -> + Mod = get_mod(), + Mod:get_node_by_id(ID). + +%% Note that false positive returns are possible, while false negatives are not. +%% In other words: positive return value (i.e. 'true') doesn't guarantee +%% successful delivery, while negative return value ('false') means +%% the delivery has definitely failed. +-spec send(dst(), term()) -> boolean(). +send({Name, Node}, Msg) when Node == node() -> + send(Name, Msg); +send(undefined, _Msg) -> + false; +send(Name, Msg) when is_atom(Name) -> + send(whereis(Name), Msg); +send(Pid, Msg) when is_pid(Pid) andalso node(Pid) == node() -> + case erlang:is_process_alive(Pid) of + true -> + erlang:send(Pid, Msg), + true; + false -> + false + end; +send(Dst, Msg) -> + Mod = get_mod(), + Mod:send(Dst, Msg). + +-spec wait_for_sync(timeout()) -> ok | {error, any()}. +wait_for_sync(Timeout) -> + Mod = get_mod(), + Mod:wait_for_sync(Timeout). + +-spec subscribe() -> ok. +subscribe() -> + subscribe(self()). + +-spec subscribe(dst()) -> ok. +subscribe(Proc) -> + Mod = get_mod(), + Mod:subscribe(Proc). + +%%%=================================================================== +%%% Hooks +%%%=================================================================== +set_ticktime() -> + Ticktime = ejabberd_option:net_ticktime() div 1000, + case net_kernel:set_net_ticktime(Ticktime) of + {ongoing_change_to, Time} when Time /= Ticktime -> + ?ERROR_MSG("Failed to set new net_ticktime because " + "the net kernel is busy changing it to the " + "previously configured value. Please wait for " + "~B seconds and retry", [Time]); + _ -> + ok + end. + +%%%=================================================================== +%%% gen_server API +%%%=================================================================== +init([]) -> + set_ticktime(), + Nodes = ejabberd_option:cluster_nodes(), + lists:foreach(fun(Node) -> + net_kernel:connect_node(Node) + end, Nodes), + Mod = get_mod(), + case Mod:init() of + ok -> + ejabberd_hooks:add(config_reloaded, ?MODULE, set_ticktime, 50), + Mod:subscribe(?MODULE), + {ok, #state{}}; + {error, Reason} -> + {stop, Reason} + end. + +handle_call(Request, From, State) -> + ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), + {noreply, State}. + +handle_cast(Msg, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Msg]), + {noreply, State}. + +handle_info({node_up, Node}, State) -> + ?INFO_MSG("Node ~ts has joined", [Node]), + {noreply, State}; +handle_info({node_down, Node}, State) -> + ?INFO_MSG("Node ~ts has left", [Node]), + {noreply, State}; +handle_info(Info, State) -> + ?WARNING_MSG("Unexpected info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, _State) -> + ejabberd_hooks:delete(config_reloaded, ?MODULE, set_ticktime, 50). + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +get_mod() -> + Backend = ejabberd_option:cluster_backend(), + list_to_existing_atom("ejabberd_cluster_" ++ atom_to_list(Backend)). + +rpc_timeout() -> + ejabberd_option:rpc_timeout(). diff --git a/src/ejabberd_cluster_mnesia.erl b/src/ejabberd_cluster_mnesia.erl new file mode 100644 index 000000000..ada0703be --- /dev/null +++ b/src/ejabberd_cluster_mnesia.erl @@ -0,0 +1,154 @@ +%%%---------------------------------------------------------------------- +%%% File : ejabberd_cluster_mnesia.erl +%%% Author : Christophe Romain +%%% Purpose : ejabberd clustering management via Mnesia +%%% Created : 7 Oct 2015 by Christophe Romain +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(ejabberd_cluster_mnesia). +-behaviour(ejabberd_cluster). + +%% API +-export([init/0, get_nodes/0, join/1, leave/1, + get_known_nodes/0, node_id/0, get_node_by_id/1, + send/2, wait_for_sync/1, subscribe/1]). + +-include("logger.hrl"). + +-spec init() -> ok. +init() -> + ok. + +-spec get_nodes() -> [node()]. + +get_nodes() -> + mnesia:system_info(running_db_nodes). + +-spec get_known_nodes() -> [node()]. + +get_known_nodes() -> + lists:usort(mnesia:system_info(db_nodes) + ++ mnesia:system_info(extra_db_nodes)). + +-spec join(node()) -> ok | {error, any()}. + +join(Node) -> + case {node(), net_adm:ping(Node)} of + {Node, _} -> + {error, {not_master, Node}}; + {_, pong} -> + application:stop(ejabberd), + application:stop(mnesia), + mnesia:delete_schema([node()]), + application:start(mnesia), + case mnesia:change_config(extra_db_nodes, [Node]) of + {ok, _} -> + replicate_database(Node), + wait_for_sync(infinity), + application:stop(mnesia), + application:start(ejabberd); + {error, Reason} -> + {error, Reason} + end; + _ -> + {error, {no_ping, Node}} + end. + +-spec leave(node()) -> ok | {error, any()}. + +leave(Node) -> + case {node(), net_adm:ping(Node)} of + {Node, _} -> + Cluster = get_nodes()--[Node], + leave(Cluster, Node); + {_, pong} -> + rpc:call(Node, ?MODULE, leave, [Node], 10000); + {_, pang} -> + case mnesia:del_table_copy(schema, Node) of + {atomic, ok} -> ok; + {aborted, Reason} -> {error, Reason} + end + end. +leave([], Node) -> + {error, {no_cluster, Node}}; +leave([Master|_], Node) -> + application:stop(ejabberd), + application:stop(mnesia), + spawn(fun() -> + rpc:call(Master, mnesia, del_table_copy, [schema, Node]), + mnesia:delete_schema([node()]), + erlang:halt(0) + end), + ok. + +-spec node_id() -> binary(). +node_id() -> + integer_to_binary(erlang:phash2(node())). + +-spec get_node_by_id(binary()) -> node(). +get_node_by_id(Hash) -> + try binary_to_integer(Hash) of + I -> match_node_id(I) + catch _:_ -> + node() + end. + +-spec send({atom(), node()}, term()) -> boolean(). +send(Dst, Msg) -> + case erlang:send(Dst, Msg, [nosuspend, noconnect]) of + ok -> true; + _ -> false + end. + +-spec wait_for_sync(timeout()) -> ok. +wait_for_sync(Timeout) -> + ?INFO_MSG("Waiting for Mnesia synchronization to complete", []), + mnesia:wait_for_tables(mnesia:system_info(local_tables), Timeout), + ok. + +-spec subscribe(_) -> ok. +subscribe(_) -> + ok. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== + +replicate_database(Node) -> + mnesia:change_table_copy_type(schema, node(), disc_copies), + lists:foreach( + fun(Table) -> + Type = rpc:call(Node, mnesia, table_info, [Table, storage_type]), + mnesia:add_table_copy(Table, node(), Type) + end, mnesia:system_info(tables)--[schema]). + +-spec match_node_id(integer()) -> node(). +match_node_id(I) -> + match_node_id(I, get_nodes()). + +-spec match_node_id(integer(), [node()]) -> node(). +match_node_id(I, [Node|Nodes]) -> + case erlang:phash2(Node) of + I -> Node; + _ -> match_node_id(I, Nodes) + end; +match_node_id(_I, []) -> + node(). diff --git a/src/ejabberd_commands.erl b/src/ejabberd_commands.erl index c279f2d0f..f1e724da3 100644 --- a/src/ejabberd_commands.erl +++ b/src/ejabberd_commands.erl @@ -5,7 +5,7 @@ %%% Created : 20 May 2008 by Badlop %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -23,314 +23,320 @@ %%% %%%---------------------------------------------------------------------- -%%% @headerfile "ejabberd_commands.hrl" - -%%% @doc Management of ejabberd commands. -%%% -%%% An ejabberd command is an abstract function identified by a name, -%%% with a defined number and type of calling arguments and type of -%%% result, that can be defined in any Erlang module and executed -%%% using any valid frontend. -%%% -%%% -%%% == Define a new ejabberd command == -%%% -%%% ejabberd commands can be defined and registered in -%%% any Erlang module. -%%% -%%% Some commands are procedures; and their purpose is to perform an -%%% action in the server, so the command result is only some result -%%% code or result tuple. Other commands are inspectors, and their -%%% purpose is to gather some information about the server and return -%%% a detailed response: it can be integer, string, atom, tuple, list -%%% or a mix of those ones. -%%% -%%% The arguments and result of an ejabberd command are strictly -%%% defined. The number and format of the arguments provided when -%%% calling an ejabberd command must match the definition of that -%%% command. The format of the result provided by an ejabberd command -%%% must be exactly its definition. For example, if a command is said -%%% to return an integer, it must always return an integer (except in -%%% case of a crash). -%%% -%%% If you are developing an Erlang module that will run inside -%%% ejabberd and you want to provide a new ejabberd command to -%%% administer some task related to your module, you only need to: -%%% implement a function, define the command, and register it. -%%% -%%% -%%% === Define a new ejabberd command === -%%% -%%% An ejabberd command is defined using the Erlang record -%%% 'ejabberd_commands'. This record has several elements that you -%%% must define. Note that 'tags', 'desc' and 'longdesc' are optional. -%%% -%%% For example let's define an ejabberd command 'pow' that gets the -%%% integers 'base' and 'exponent'. Its result will be an integer -%%% 'power': -%%% -%%%
#ejabberd_commands{name = pow, tags = [test],
-%%%                 desc = "Return the power of base for exponent",
-%%%                 longdesc = "This is an example command. The formula is:\n"
-%%%                 "  power = base ^ exponent",
-%%%                 module = ?MODULE, function = pow,
-%%%                 args = [{base, integer}, {exponent, integer}],
-%%%                 result = {power, integer}}
-%%% -%%% -%%% === Implement the function associated to the command === -%%% -%%% Now implement a function in your module that matches the arguments -%%% and result of the ejabberd command. -%%% -%%% For example the function calc_power gets two integers Base and -%%% Exponent. It calculates the power and rounds to an integer: -%%% -%%%
calc_power(Base, Exponent) ->
-%%%    PowFloat = math:pow(Base, Exponent),
-%%%    round(PowFloat).
-%%% -%%% Since this function will be called by ejabberd_commands, it must be exported. -%%% Add to your module: -%%%
-export([calc_power/2]).
-%%% -%%% Only some types of result formats are allowed. -%%% If the format is defined as 'rescode', then your function must return: -%%% ok | true | atom() -%%% where the atoms ok and true as considered positive answers, -%%% and any other response atom is considered negative. -%%% -%%% If the format is defined as 'restuple', then the command must return: -%%% {rescode(), string()} -%%% -%%% If the format is defined as '{list, something()}', then the command -%%% must return a list of something(). -%%% -%%% -%%% === Register the command === -%%% -%%% Define this function and put inside the #ejabberd_command you -%%% defined in the beginning: -%%% -%%%
commands() ->
-%%%    [
-%%%
-%%%    ].
-%%% -%%% You need to include this header file in order to use the record: -%%% -%%%
-include("ejabberd_commands.hrl").
-%%% -%%% When your module is initialized or started, register your commands: -%%% -%%%
ejabberd_commands:register_commands(commands()),
-%%% -%%% And when your module is stopped, unregister your commands: -%%% -%%%
ejabberd_commands:unregister_commands(commands()),
-%%% -%%% That's all! Now when your module is started, the command will be -%%% registered and any frontend can access it. For example: -%%% -%%%
$ ejabberdctl help pow
-%%%
-%%%   Command Name: pow
-%%%
-%%%   Arguments: base::integer
-%%%              exponent::integer
-%%%
-%%%   Returns: power::integer
-%%%
-%%%   Tags: test
-%%%
-%%%   Description: Return the power of base for exponent
-%%%
-%%% This is an example command. The formula is:
-%%%  power = base ^ exponent
-%%%
-%%% $ ejabberdctl pow 3 4
-%%% 81
-%%% 
-%%% -%%% -%%% == Execute an ejabberd command == -%%% -%%% ejabberd commands are mean to be executed using any valid -%%% frontend. An ejabberd commands is implemented in a regular Erlang -%%% function, so it is also possible to execute this function in any -%%% Erlang module, without dealing with the associated ejabberd -%%% commands. -%%% -%%% -%%% == Frontend to ejabberd commands == -%%% -%%% Currently there are two frontends to ejabberd commands: the shell -%%% script {@link ejabberd_ctl. ejabberdctl}, and the XML-RPC server -%%% ejabberd_xmlrpc. -%%% -%%% -%%% === ejabberdctl as a frontend to ejabberd commands === -%%% -%%% It is possible to use ejabberdctl to get documentation of any -%%% command. But ejabberdctl does not support all the argument types -%%% allowed in ejabberd commands, so there are some ejabberd commands -%%% that cannot be executed using ejabberdctl. -%%% -%%% Also note that the ejabberdctl shell administration script also -%%% manages ejabberdctl commands, which are unrelated to ejabberd -%%% commands and can only be executed using ejabberdctl. -%%% -%%% -%%% === ejabberd_xmlrpc as a frontend to ejabberd commands === -%%% -%%% ejabberd_xmlrpc provides an XML-RPC server to execute ejabberd commands. -%%% ejabberd_xmlrpc is a contributed module published in ejabberd-modules SVN. -%%% -%%% Since ejabberd_xmlrpc does not provide any method to get documentation -%%% of the ejabberd commands, please use ejabberdctl to know which -%%% commands are available, and their usage. -%%% -%%% The number and format of the arguments provided when calling an -%%% ejabberd command must match the definition of that command. Please -%%% make sure the XML-RPC call provides the required arguments, with -%%% the specified format. The order of the arguments in an XML-RPC -%%% call is not important, because all the data is tagged and will be -%%% correctly prepared by ejabberd_xmlrpc before executing the ejabberd -%%% command. - -%%% TODO: consider this feature: -%%% All commands are catched. If an error happens, return the restuple: -%%% {error, flattened error string} -%%% This means that ecomm call APIs (ejabberd_ctl, ejabberd_xmlrpc) need to allows this. -%%% And ejabberd_xmlrpc must be prepared to handle such an unexpected response. - - -module(ejabberd_commands). -author('badlop@process-one.net'). --export([init/0, +-behaviour(gen_server). + +-define(DEFAULT_VERSION, 1000000). + +-export([start_link/0, list_commands/0, + list_commands/1, + list_commands/2, get_command_format/1, + get_command_format/2, + get_command_format/3, get_command_definition/1, + get_command_definition/2, get_tags_commands/0, + get_tags_commands/1, register_commands/1, + register_commands/2, + register_commands/3, unregister_commands/1, - execute_command/2, - execute_command/4 - ]). + unregister_commands/3, + get_commands_spec/0, + get_commands_definition/0, + get_commands_definition/1, + execute_command2/3, + execute_command2/4]). +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). -include("ejabberd_commands.hrl"). --include("ejabberd.hrl"). -include("logger.hrl"). +-include_lib("stdlib/include/ms_transform.hrl"). +-type auth() :: {binary(), binary(), binary() | {oauth, binary()}, boolean()} | map(). -init() -> - ets:new(ejabberd_commands, [named_table, set, public, - {keypos, #ejabberd_commands.name}]). +-record(state, {}). + +get_commands_spec() -> + [ + #ejabberd_commands{name = gen_html_doc_for_commands, tags = [documentation], + desc = "Generates html documentation for ejabberd_commands", + module = ejabberd_commands_doc, function = generate_html_output, + args = [{file, binary}, {regexp, binary}, {examples, binary}], + result = {res, rescode}, + args_desc = ["Path to file where generated " + "documentation should be stored", + "Regexp matching names of commands or modules " + "that will be included inside generated document", + "Comma separated list of languages (chosen from `java`, `perl`, `xmlrpc`, `json`) " + "that will have example invocation include in markdown document"], + result_desc = "0 if command failed, 1 when succeeded", + args_example = ["/home/me/docs/api.html", "mod_admin", "java,json"], + result_example = ok}, + #ejabberd_commands{name = gen_markdown_doc_for_commands, tags = [documentation], + desc = "Generates markdown documentation for ejabberd_commands", + module = ejabberd_commands_doc, function = generate_md_output, + args = [{file, binary}, {regexp, binary}, {examples, binary}], + result = {res, rescode}, + args_desc = ["Path to file where generated " + "documentation should be stored", + "Regexp matching names of commands or modules " + "that will be included inside generated document, " + "or `runtime` to get commands registered at runtime", + "Comma separated list of languages (chosen from `java`, `perl`, `xmlrpc`, `json`) " + "that will have example invocation include in markdown document"], + result_desc = "0 if command failed, 1 when succeeded", + args_example = ["/home/me/docs/api.html", "mod_admin", "java,json"], + 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}, + args_desc = ["Path to file where generated " + "documentation should be stored"], + result_desc = "0 if command failed, 1 when succeeded", + args_example = ["/home/me/docs/tags.md"], + result_example = ok}]. + +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +init([]) -> + try mnesia:transform_table(ejabberd_commands, ignore, + record_info(fields, ejabberd_commands)) + catch exit:{aborted, {no_exists, _}} -> ok + end, + ejabberd_mnesia:create(?MODULE, ejabberd_commands, + [{ram_copies, [node()]}, + {local_content, true}, + {attributes, record_info(fields, ejabberd_commands)}, + {type, bag}]), + register_commands(get_commands_spec()), + {ok, #state{}}. + +handle_call(Request, From, State) -> + ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), + {noreply, State}. + +handle_cast(Msg, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Msg]), + {noreply, State}. + +handle_info(Info, State) -> + ?WARNING_MSG("Unexpected info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. -spec register_commands([ejabberd_commands()]) -> ok. -%% @doc Register ejabberd commands. -%% If a command is already registered, a warning is printed and the old command is preserved. register_commands(Commands) -> + register_commands(unknown, Commands). + +-spec register_commands(atom(), [ejabberd_commands()]) -> ok. + +register_commands(Definer, Commands) -> + ExistingCommands = list_commands(), lists:foreach( fun(Command) -> - case ets:insert_new(ejabberd_commands, Command) of - true -> - ok; - false -> - ?DEBUG("This command is already defined:~n~p", [Command]) - end + Name = Command#ejabberd_commands.name, + case lists:keyfind(Name, 1, ExistingCommands) of + false -> + mnesia:dirty_write(register_command_prepare(Command, Definer)); + _ -> + OtherCommandDef = get_command_definition(Name), + ?CRITICAL_MSG("Error trying to define a command: another one already exists with the same name:~n Existing: ~p~n New: ~p", [OtherCommandDef, Command]) + end end, - Commands). + Commands), + ejabberd_access_permissions:invalidate(), + ok. + +-spec register_commands(binary(), atom(), [ejabberd_commands()]) -> ok. + +register_commands(Host, Definer, Commands) -> + case gen_mod:is_loaded_elsewhere(Host, Definer) of + false -> + register_commands(Definer, Commands); + true -> + ok + end. + +register_command_prepare(Command, Definer) -> + Tags1 = Command#ejabberd_commands.tags, + Tags2 = case Command#ejabberd_commands.version of + 0 -> Tags1; + Version -> Tags1 ++ [list_to_atom("v"++integer_to_list(Version))] + end, + Command#ejabberd_commands{definer = Definer, tags = Tags2}. + -spec unregister_commands([ejabberd_commands()]) -> ok. -%% @doc Unregister ejabberd commands. unregister_commands(Commands) -> lists:foreach( fun(Command) -> - ets:delete_object(ejabberd_commands, Command) + mnesia:dirty_delete(ejabberd_commands, Command#ejabberd_commands.name) end, - Commands). + Commands), + ejabberd_access_permissions:invalidate(). + +-spec unregister_commands(binary(), atom(), [ejabberd_commands()]) -> ok. + +unregister_commands(Host, Definer, Commands) -> + case gen_mod:is_loaded_elsewhere(Host, Definer) of + false -> + unregister_commands(Commands); + true -> + ok + end. -spec list_commands() -> [{atom(), [aterm()], string()}]. -%% @doc Get a list of all the available commands, arguments and description. list_commands() -> - Commands = ets:match(ejabberd_commands, - #ejabberd_commands{name = '$1', - args = '$2', - desc = '$3', - _ = '_'}), - [{A, B, C} || [A, B, C] <- Commands]. + list_commands(?DEFAULT_VERSION). --spec get_command_format(atom()) -> {[aterm()], rterm()} | {error, command_unknown}. +-spec list_commands(integer()) -> [{atom(), [aterm()], string()}]. + +list_commands(Version) -> + Commands = get_commands_definition(Version), + [{Name, Args, Desc} || #ejabberd_commands{name = Name, + args = Args, + tags = Tags, + desc = Desc} <- Commands, + not lists:member(internal, Tags)]. + +-spec list_commands(integer(), map()) -> [{atom(), [aterm()], string()}]. + +list_commands(Version, CallerInfo) -> + lists:filter( + fun({Name, _Args, _Desc}) -> + allow == ejabberd_access_permissions:can_access(Name, CallerInfo) + end, + list_commands(Version) + ). + +-spec get_command_format(atom()) -> {[aterm()], [{atom(),atom()}], rterm()}. -%% @doc Get the format of arguments and result of a command. get_command_format(Name) -> - Matched = ets:match(ejabberd_commands, - #ejabberd_commands{name = Name, - args = '$1', - result = '$2', - _ = '_'}), - case Matched of - [] -> - {error, command_unknown}; - [[Args, Result]] -> - {Args, Result} + get_command_format(Name, noauth, ?DEFAULT_VERSION). +get_command_format(Name, Version) when is_integer(Version) -> + get_command_format(Name, noauth, Version); +get_command_format(Name, Auth) -> + get_command_format(Name, Auth, ?DEFAULT_VERSION). + +-spec get_command_format(atom(), noauth | admin | auth(), integer()) -> {[aterm()], [{atom(),atom()}], rterm()}. +get_command_format(Name, Auth, Version) -> + Admin = is_admin(Name, Auth, #{}), + #ejabberd_commands{args = Args, + result = Result, + args_rename = Rename, + policy = Policy} = + get_command_definition(Name, Version), + case Policy of + user when Admin; + Auth == noauth -> + {[{user, binary}, {host, binary} | Args], Rename, Result}; + _ -> + {Args, Rename, Result} end. --spec get_command_definition(atom()) -> ejabberd_commands() | command_not_found. +-spec get_command_definition(atom()) -> ejabberd_commands(). -%% @doc Get the definition record of a command. get_command_definition(Name) -> - case ets:lookup(ejabberd_commands, Name) of - [E] -> E; - [] -> command_not_found + get_command_definition(Name, ?DEFAULT_VERSION). + +-spec get_command_definition(atom(), integer()) -> ejabberd_commands(). + +get_command_definition(Name, Version) -> + case lists:reverse( + lists:sort( + mnesia:dirty_select( + ejabberd_commands, + ets:fun2ms( + fun(#ejabberd_commands{name = N, version = V} = C) + when N == Name, V =< Version -> + {V, C} + end)))) of + [{_, Command} | _ ] -> Command; + _E -> throw({error, unknown_command}) end. -%% @spec (Name::atom(), Arguments) -> ResultTerm | {error, command_unknown} -%% @doc Execute a command. -execute_command(Name, Arguments) -> - execute_command([], noauth, Name, Arguments). +get_commands_definition() -> + get_commands_definition(?DEFAULT_VERSION). -%% @spec (AccessCommands, Auth, Name::atom(), Arguments) -> ResultTerm | {error, Error} -%% where -%% AccessCommands = [{Access, CommandNames, Arguments}] -%% Auth = {User::string(), Server::string(), Password::string()} | noauth -%% Method = atom() -%% Arguments = [any()] -%% Error = command_unknown | account_unprivileged | invalid_account_data | no_auth_provided -execute_command(AccessCommands, Auth, Name, Arguments) -> - case ets:lookup(ejabberd_commands, Name) of - [Command] -> - try check_access_commands(AccessCommands, Auth, Name, Command, Arguments) of - ok -> execute_command2(Command, Arguments) - catch - {error, Error} -> {error, Error} - end; - [] -> {error, command_unknown} +-spec get_commands_definition(integer()) -> [ejabberd_commands()]. + +get_commands_definition(Version) -> + L = lists:reverse( + lists:sort( + mnesia:dirty_select( + ejabberd_commands, + ets:fun2ms( + fun(#ejabberd_commands{name = Name, version = V} = C) + when V =< Version -> + {Name, V, C} + end)))), + F = fun({_Name, _V, Command}, []) -> + [Command]; + ({Name, _V, _Command}, [#ejabberd_commands{name=Name}|_T] = Acc) -> + Acc; + ({_Name, _V, Command}, Acc) -> [Command | Acc] + end, + lists:foldl(F, [], L). + +execute_command2(Name, Arguments, CallerInfo) -> + execute_command2(Name, Arguments, CallerInfo, ?DEFAULT_VERSION). + +execute_command2(Name, Arguments, CallerInfo, Version) -> + Command = get_command_definition(Name, Version), + FrontedCalledInternal = + maps:get(caller_module, CallerInfo, none) /= ejabberd_web_admin + andalso lists:member(internal, Command#ejabberd_commands.tags), + case {ejabberd_access_permissions:can_access(Name, CallerInfo), + FrontedCalledInternal} of + {allow, false} -> + do_execute_command(Command, Arguments); + {_, true} -> + throw({error, frontend_cannot_call_an_internal_command}); + {deny, false} -> + throw({error, access_rules_unauthorized}) end. -execute_command2(Command, Arguments) -> + +do_execute_command(Command, Arguments) -> Module = Command#ejabberd_commands.module, Function = Command#ejabberd_commands.function, ?DEBUG("Executing command ~p:~p with Args=~p", [Module, Function, Arguments]), + ejabberd_hooks:run(api_call, [Module, Function, Arguments]), apply(Module, Function, Arguments). -spec get_tags_commands() -> [{string(), [string()]}]. -%% @spec () -> [{Tag::string(), [CommandName::string()]}] -%% @doc Get all the tags and associated commands. get_tags_commands() -> - CommandTags = ets:match(ejabberd_commands, - #ejabberd_commands{ - name = '$1', - tags = '$2', - _ = '_'}), + get_tags_commands(?DEFAULT_VERSION). + +-spec get_tags_commands(integer()) -> [{string(), [string()]}]. + +get_tags_commands(Version) -> + CommandTags = [{Name, Tags} || + #ejabberd_commands{name = Name, tags = Tags} + <- get_commands_definition(Version), + not lists:member(internal, Tags)], Dict = lists:foldl( - fun([CommandNameAtom, CTags], D) -> + fun({CommandNameAtom, CTags}, D) -> CommandName = atom_to_list(CommandNameAtom), case CTags of [] -> @@ -349,91 +355,15 @@ get_tags_commands() -> CommandTags), orddict:to_list(Dict). - %% ----------------------------- %% Access verification %% ----------------------------- - -%% @spec (AccessCommands, Auth, Method, Command, Arguments) -> ok -%% where -%% AccessCommands = [ {Access, CommandNames, Arguments} ] -%% Auth = {User::string(), Server::string(), Password::string()} | noauth -%% Method = atom() -%% Arguments = [any()] -%% @doc Check access is allowed to that command. -%% At least one AccessCommand must be satisfied. -%% It may throw {error, Error} where: -%% Error = account_unprivileged | invalid_account_data -check_access_commands([], _Auth, _Method, _Command, _Arguments) -> - ok; -check_access_commands(AccessCommands, Auth, Method, Command, Arguments) -> - AccessCommandsAllowed = - lists:filter( - fun({Access, Commands, ArgumentRestrictions}) -> - case check_access(Access, Auth) of - true -> - check_access_command(Commands, Command, ArgumentRestrictions, - Method, Arguments); - false -> - false - end - end, - AccessCommands), - case AccessCommandsAllowed of - [] -> throw({error, account_unprivileged}); - L when is_list(L) -> ok - end. - --spec check_auth(noauth) -> noauth_provided; - ({binary(), binary(), binary()}) -> {ok, binary(), binary()}. - -check_auth(noauth) -> - no_auth_provided; -check_auth({User, Server, Password}) -> - %% Check the account exists and password is valid - case ejabberd_auth:check_password(User, Server, Password) of - true -> {ok, User, Server}; - _ -> throw({error, invalid_account_data}) - end. - -check_access(all, _) -> +-spec is_admin(atom(), admin | noauth | auth(), map()) -> boolean(). +is_admin(_Name, admin, _Extra) -> true; -check_access(Access, Auth) -> - case check_auth(Auth) of - {ok, User, Server} -> - check_access(Access, User, Server); - _ -> - false - end. - -check_access(Access, User, Server) -> - %% Check this user has access permission - case acl:match_rule(Server, Access, jlib:make_jid(User, Server, <<"">>)) of - allow -> true; - deny -> false - end. - -check_access_command(Commands, Command, ArgumentRestrictions, Method, Arguments) -> - case Commands==all orelse lists:member(Method, Commands) of - true -> check_access_arguments(Command, ArgumentRestrictions, Arguments); - false -> false - end. - -check_access_arguments(Command, ArgumentRestrictions, Arguments) -> - ArgumentsTagged = tag_arguments(Command#ejabberd_commands.args, Arguments), - lists:all( - fun({ArgName, ArgAllowedValue}) -> - %% If the call uses the argument, check the value is acceptable - case lists:keysearch(ArgName, 1, ArgumentsTagged) of - {value, {ArgName, ArgValue}} -> ArgValue == ArgAllowedValue; - false -> true - end - end, ArgumentRestrictions). - -tag_arguments(ArgsDefs, Args) -> - lists:zipwith( - fun({ArgName, _ArgType}, ArgValue) -> - {ArgName, ArgValue} - end, - ArgsDefs, - Args). +is_admin(_Name, {_User, _Server, _, false}, _Extra) -> + false; +is_admin(_Name, Map, _extra) when is_map(Map) -> + true; +is_admin(_Name, _Auth, _Extra) -> + false. diff --git a/src/ejabberd_commands_doc.erl b/src/ejabberd_commands_doc.erl new file mode 100644 index 000000000..79bfe6147 --- /dev/null +++ b/src/ejabberd_commands_doc.erl @@ -0,0 +1,662 @@ +%%%---------------------------------------------------------------------- +%%% File : ejabberd_commands_doc.erl +%%% Author : Badlop +%%% Purpose : Management of ejabberd commands +%%% Created : 20 May 2008 by Badlop +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(ejabberd_commands_doc). +-author('pawel@process-one.net'). + +-export([generate_html_output/3]). +-export([generate_md_output/3]). +-export([generate_tags_md/1]). + +-include("ejabberd_commands.hrl"). + +-define(RAW(V), if HTMLOutput -> fxml:crypt(iolist_to_binary(V)); true -> iolist_to_binary(V) end). +-define(TAG_BIN(N), (atom_to_binary(N, latin1))/binary). +-define(TAG_STR(N), atom_to_list(N)). +-define(TAG(N), if HTMLOutput -> [<<"<", ?TAG_BIN(N), "/>">>]; true -> md_tag(N, <<"">>) end). +-define(TAG(N, V), if HTMLOutput -> [<<"<", ?TAG_BIN(N), ">">>, V, <<"">>]; true -> md_tag(N, V) end). +-define(TAG(N, C, V), if HTMLOutput -> [<<"<", ?TAG_BIN(N), " class='", C, "'>">>, V, <<"">>]; true -> md_tag(N, V) end). +-define(TAG_R(N, V), ?TAG(N, ?RAW(V))). +-define(TAG_R(N, C, V), ?TAG(N, C, ?RAW(V))). +-define(SPAN(N, V), ?TAG_R(span, ??N, V)). + +-define(STR(A), ?SPAN(str,[<<"\"">>, A, <<"\"">>])). +-define(NUM(A), ?SPAN(num,integer_to_binary(A))). +-define(FIELD(A), ?SPAN(field,A)). +-define(ID(A), ?SPAN(id,A)). +-define(OP(A), ?SPAN(op,A)). +-define(ARG(A), ?FIELD(atom_to_list(A))). +-define(KW(A), ?SPAN(kw,A)). +-define(BR, <<"\n">>). + +-define(ARG_S(A), ?STR(atom_to_list(A))). + +-define(RAW_L(A), ?RAW(<>)). +-define(STR_L(A), ?STR(<>)). +-define(FIELD_L(A), ?FIELD(<>)). +-define(ID_L(A), ?ID(<>)). +-define(OP_L(A), ?OP(<>)). +-define(KW_L(A), ?KW(<>)). + +-define(STR_A(A), ?STR(atom_to_list(A))). +-define(ID_A(A), ?ID(atom_to_list(A))). + +list_join_with([], _M) -> + []; +list_join_with([El|Tail], M) -> + lists:reverse(lists:foldl(fun(E, Acc) -> + [E, M | Acc] + end, [El], Tail)). + +md_tag(dt, V) -> + [<<"- ">>, V]; +md_tag(dd, V) -> + [<<" : ">>, V, <<"\n">>]; +md_tag(li, V) -> + [<<"- ">>, V, <<"\n">>]; +md_tag(pre, V) -> + [V, <<"\n">>]; +md_tag(p, V) -> + [<<"\n\n">>, V, <<"\n">>]; +md_tag(h1, V) -> + [<<"\n\n## ">>, V, <<"\n">>]; +md_tag(h2, V) -> + [<<"\n__">>, V, <<"__\n\n">>]; +md_tag(strong, V) -> + [<<"*">>, V, <<"*">>]; +md_tag('div', V) -> + [<<"*Note* about this command: ">>, V, <<".">>]; +md_tag(_, V) -> + V. + +perl_gen({Name, integer}, Int, _Indent, HTMLOutput) -> + [?ARG(Name), ?OP_L(" => "), ?NUM(Int)]; +perl_gen({Name, string}, Str, _Indent, HTMLOutput) -> + [?ARG(Name), ?OP_L(" => "), ?STR(Str)]; +perl_gen({Name, binary}, Str, _Indent, HTMLOutput) -> + [?ARG(Name), ?OP_L(" => "), ?STR(Str)]; +perl_gen({Name, atom}, Atom, _Indent, HTMLOutput) -> + [?ARG(Name), ?OP_L(" => "), ?STR_A(Atom)]; +perl_gen({Name, {tuple, Fields}}, Tuple, Indent, HTMLOutput) -> + Res = lists:map(fun({A,B})->perl_gen(A, B, Indent, HTMLOutput) end, lists:zip(Fields, tuple_to_list(Tuple))), + [?ARG(Name), ?OP_L(" => {"), list_join_with(Res, [?OP_L(", ")]), ?OP_L("}")]; +perl_gen({Name, {list, ElDesc}}, List, Indent, HTMLOutput) -> + Res = lists:map(fun(E) -> [?OP_L("{"), perl_gen(ElDesc, E, Indent, HTMLOutput), ?OP_L("}")] end, List), + [?ARG(Name), ?OP_L(" => ["), list_join_with(Res, [?OP_L(", ")]), ?OP_L("]")]. + +perl_call(Name, ArgsDesc, Values, HTMLOutput) -> + {Indent, Preamble} = if HTMLOutput -> {<<"">>, []}; true -> {<<" ">>, <<"~~~ perl\n">>} end, + [Preamble, + Indent, ?ID_L("XMLRPC::Lite"), ?OP_L("->"), ?ID_L("proxy"), ?OP_L("("), ?ID_L("$url"), ?OP_L(")->"), + ?ID_L("call"), ?OP_L("("), ?STR_A(Name), ?OP_L(", {"), ?BR, Indent, <<" ">>, + list_join_with(lists:map(fun({A,B})->perl_gen(A, B, <>, HTMLOutput) end, lists:zip(ArgsDesc, Values)), [?OP_L(","), ?BR, Indent, <<" ">>]), + ?BR, Indent, ?OP_L("})->"), ?ID_L("results"), ?OP_L("()")]. + +java_gen_map(Vals, Indent, HTMLOutput) -> + {Split, NL} = case Indent of + none -> {<<" ">>, <<" ">>}; + _ -> {[?BR, <<" ", Indent/binary>>], [?BR, Indent]} + end, + [?KW_L("new "), ?ID_L("HashMap"), ?OP_L("<"), ?ID_L("String"), ?OP_L(", "), ?ID_L("Object"), + ?OP_L(">() {{"), Split, list_join_with(Vals, Split), NL, ?OP_L("}}")]. + +java_gen({Name, integer}, Int, _Indent, HTMLOutput) -> + [?ID_L("put"), ?OP_L("("), ?STR_A(Name), ?OP_L(", "), ?KW_L("new "), ?ID_L("Integer"), ?OP_L("("), ?NUM(Int), ?OP_L("));")]; +java_gen({Name, string}, Str, _Indent, HTMLOutput) -> + [?ID_L("put"), ?OP_L("("), ?STR_A(Name), ?OP_L(", "), ?STR(Str), ?OP_L(");")]; +java_gen({Name, binary}, Str, _Indent, HTMLOutput) -> + [?ID_L("put"), ?OP_L("("), ?STR_A(Name), ?OP_L(", "), ?STR(Str), ?OP_L(");")]; +java_gen({Name, atom}, Atom, _Indent, HTMLOutput) -> + [?ID_L("put"), ?OP_L("("), ?STR_A(Name), ?OP_L(", "), ?STR_A(Atom), ?OP_L(");")]; +java_gen({Name, {tuple, Fields}}, Tuple, Indent, HTMLOutput) -> + NewIndent = <<" ", Indent/binary>>, + Res = lists:map(fun({A, B}) -> [java_gen(A, B, NewIndent, HTMLOutput)] end, lists:zip(Fields, tuple_to_list(Tuple))), + [?ID_L("put"), ?OP_L("("), ?STR_A(Name), ?OP_L(", "), java_gen_map(Res, Indent, HTMLOutput), ?OP_L(")")]; +java_gen({Name, {list, ElDesc}}, List, Indent, HTMLOutput) -> + {NI, NI2, I} = case List of + [_] -> {" ", " ", Indent}; + _ -> {[?BR, <<" ", Indent/binary>>], + [?BR, <<" ", Indent/binary>>], + <<" ", Indent/binary>>} + end, + Res = lists:map(fun(E) -> java_gen_map([java_gen(ElDesc, E, I, HTMLOutput)], none, HTMLOutput) end, List), + [?ID_L("put"), ?OP_L("("), ?STR_A(Name), ?OP_L(", "), ?KW_L("new "), ?ID_L("Object"), ?OP_L("[] {"), NI, + list_join_with(Res, [?OP_L(","), NI]), NI2, ?OP_L("});")]. + +java_call(Name, ArgsDesc, Values, HTMLOutput) -> + {Indent, Preamble} = if HTMLOutput -> {<<"">>, []}; true -> {<<" ">>, <<"~~~ java\n">>} end, + [Preamble, + Indent, ?ID_L("XmlRpcClientConfigImpl config"), ?OP_L(" = "), ?KW_L("new "), ?ID_L("XmlRpcClientConfigImpl"), ?OP_L("();"), ?BR, + Indent, ?ID_L("config"), ?OP_L("."), ?ID_L("setServerURL"), ?OP_L("("), ?ID_L("url"), ?OP_L(");"), ?BR, Indent, ?BR, + Indent, ?ID_L("XmlRpcClient client"), ?OP_L(" = "), ?KW_L("new "), ?ID_L("XmlRpcClient"), ?OP_L("();"), ?BR, + Indent, ?ID_L("client"), ?OP_L("."), ?ID_L("setConfig"), ?OP_L("("), ?ID_L("config"), ?OP_L(");"), ?BR, Indent, ?BR, + Indent, ?ID_L("client"), ?OP_L("."), ?ID_L("execute"), ?OP_L("("), ?STR_A(Name), ?OP_L(", "), + java_gen_map(lists:map(fun({A,B})->java_gen(A, B, Indent, HTMLOutput) end, lists:zip(ArgsDesc, Values)), Indent, HTMLOutput), + ?OP_L(");")]. + +-define(XML_S(N, V), ?OP_L("<"), ?FIELD_L(??N), ?OP_L(">"), V). +-define(XML_E(N), ?OP_L("")). +-define(XML(N, Indent, V), ?BR, Indent, ?XML_S(N, V), ?BR, Indent, ?XML_E(N)). +-define(XML(N, Indent, D, V), ?XML(N, [Indent, lists:duplicate(D, <<" ">>)], V)). +-define(XML_L(N, Indent, V), ?BR, Indent, ?XML_S(N, V), ?XML_E(N)). +-define(XML_L(N, Indent, D, V), ?XML_L(N, [Indent, lists:duplicate(D, <<" ">>)], V)). + +xml_gen({Name, integer}, Int, Indent, HTMLOutput) -> + [?XML(member, Indent, + [?XML_L(name, Indent, 1, ?ID_A(Name)), + ?XML(value, Indent, 1, + [?XML_L(integer, Indent, 2, ?ID(integer_to_binary(Int)))])])]; +xml_gen({Name, string}, Str, Indent, HTMLOutput) -> + [?XML(member, Indent, + [?XML_L(name, Indent, 1, ?ID_A(Name)), + ?XML(value, Indent, 1, + [?XML_L(string, Indent, 2, ?ID(Str))])])]; +xml_gen({Name, binary}, Str, Indent, HTMLOutput) -> + [?XML(member, Indent, + [?XML_L(name, Indent, 1, ?ID_A(Name)), + ?XML(value, Indent, 1, + [?XML_L(string, Indent, 2, ?ID(Str))])])]; +xml_gen({Name, atom}, Atom, Indent, HTMLOutput) -> + [?XML(member, Indent, + [?XML_L(name, Indent, 1, ?ID_A(Name)), + ?XML(value, Indent, 1, + [?XML_L(string, Indent, 2, ?ID(atom_to_list(Atom)))])])]; +xml_gen({Name, {tuple, Fields}}, Tuple, Indent, HTMLOutput) -> + NewIndent = <<" ", Indent/binary>>, + Res = lists:map(fun({A, B}) -> xml_gen(A, B, NewIndent, HTMLOutput) end, lists:zip(Fields, tuple_to_list(Tuple))), + [?XML(member, Indent, + [?XML_L(name, Indent, 1, ?ID_A(Name)), + ?XML(value, Indent, 1, [?XML(struct, NewIndent, Res)])])]; +xml_gen({Name, {list, ElDesc}}, List, Indent, HTMLOutput) -> + Ind1 = <<" ", Indent/binary>>, + Ind2 = <<" ", Ind1/binary>>, + Res = lists:map(fun(E) -> [?XML(value, Ind1, [?XML(struct, Ind1, 1, xml_gen(ElDesc, E, Ind2, HTMLOutput))])] end, List), + [?XML(member, Indent, + [?XML_L(name, Indent, 1, ?ID_A(Name)), + ?XML(value, Indent, 1, [?XML(array, Indent, 2, [?XML(data, Indent, 3, Res)])])])]. + +xml_call(Name, ArgsDesc, Values, HTMLOutput) -> + {Indent, Preamble} = if HTMLOutput -> {<<"">>, []}; true -> {<<" ">>, <<"~~~ xml">>} end, + Res = lists:map(fun({A, B}) -> xml_gen(A, B, <>, HTMLOutput) end, lists:zip(ArgsDesc, Values)), + [Preamble, + ?XML(methodCall, Indent, + [?XML_L(methodName, Indent, 1, ?ID_A(Name)), + ?XML(params, Indent, 1, + [?XML(param, Indent, 2, + [?XML(value, Indent, 3, + [?XML(struct, Indent, 4, Res)])])])])]. + +% [?ARG_S(Name), ?OP_L(": "), ?STR(Str)]; +json_gen({_Name, integer}, Int, _Indent, HTMLOutput) -> + [?NUM(Int)]; +json_gen({_Name, string}, Str, _Indent, HTMLOutput) -> + [?STR(Str)]; +json_gen({_Name, binary}, Str, _Indent, HTMLOutput) -> + [?STR(Str)]; +json_gen({_Name, atom}, Atom, _Indent, HTMLOutput) -> + [?STR_A(Atom)]; +json_gen({_Name, rescode}, Val, _Indent, HTMLOutput) -> + [?ID_A(Val == ok orelse Val == true)]; +json_gen({_Name, restuple}, {Val, Str}, _Indent, HTMLOutput) -> + [?OP_L("{"), ?STR_L("res"), ?OP_L(": "), ?ID_A(Val == ok orelse Val == true), ?OP_L(", "), + ?STR_L("text"), ?OP_L(": "), ?STR(Str), ?OP_L("}")]; +json_gen({_Name, {list, {_, {tuple, [{_, atom}, ValFmt]}}}}, List, Indent, HTMLOutput) -> + Indent2 = <<" ", Indent/binary>>, + Res = lists:map(fun({N, V})->[?STR_A(N), ?OP_L(": "), json_gen(ValFmt, V, Indent2, HTMLOutput)] end, List), + [?OP_L("{"), ?BR, Indent2, list_join_with(Res, [?OP_L(","), ?BR, Indent2]), ?BR, Indent, ?OP_L("}")]; +json_gen({_Name, {tuple, Fields}}, Tuple, Indent, HTMLOutput) -> + Indent2 = <<" ", Indent/binary>>, + Res = lists:map(fun({{N, _} = A, B})->[?STR_A(N), ?OP_L(": "), json_gen(A, B, Indent2, HTMLOutput)] end, + lists:zip(Fields, tuple_to_list(Tuple))), + [?OP_L("{"), ?BR, Indent2, list_join_with(Res, [?OP_L(","), ?BR, Indent2]), ?BR, Indent, ?OP_L("}")]; +json_gen({_Name, {list, ElDesc}}, List, Indent, HTMLOutput) -> + Indent2 = <<" ", Indent/binary>>, + Res = lists:map(fun(E) -> json_gen(ElDesc, E, Indent2, HTMLOutput) end, List), + [?OP_L("["), ?BR, Indent2, list_join_with(Res, [?OP_L(","), ?BR, Indent2]), ?BR, Indent, ?OP_L("]")]. + +json_call(Name, ArgsDesc, Values, ResultDesc, Result, HTMLOutput) -> + {Indent, Preamble} = if HTMLOutput -> {<<"">>, []}; true -> {<<"">>, <<"~~~ json\n">>} end, + {Code, ResultStr} = case {ResultDesc, Result} of + {{_, rescode}, V} when V == true; V == ok -> + {200, [?STR_L("")]}; + {{_, rescode}, _} -> + {500, [?STR_L("")]}; + {{_, restuple}, {V1, Text1}} when V1 == true; V1 == ok -> + {200, [?STR(Text1)]}; + {{_, restuple}, {_, Text2}} -> + {500, [?STR(Text2)]}; + {{_, _}, _} -> + {200, json_gen(ResultDesc, Result, Indent, HTMLOutput)} + end, + CodeStr = case Code of + 200 -> <<" 200 OK">>; + 500 -> <<" 500 Internal Server Error">> + end, + [Preamble, + Indent, ?ID_L("POST /api/"), ?ID_A(Name), ?BR, + Indent, ?OP_L("{"), ?BR, Indent, <<" ">>, + list_join_with(lists:map(fun({{N,_}=A,B})->[?STR_A(N), ?OP_L(": "), json_gen(A, B, <>, HTMLOutput)] end, + lists:zip(ArgsDesc, Values)), [?OP_L(","), ?BR, Indent, <<" ">>]), + ?BR, Indent, ?OP_L("}"), ?BR, Indent, ?BR, Indent, + ?ID_L("HTTP/1.1"), ?ID(CodeStr), ?BR, Indent, + ResultStr + ]. + +generate_example_input({_Name, integer}, {LastStr, LastNum}) -> + {LastNum+1, {LastStr, LastNum+1}}; +generate_example_input({_Name, string}, {LastStr, LastNum}) -> + {string:chars(LastStr+1, 5), {LastStr+1, LastNum}}; +generate_example_input({_Name, binary}, {LastStr, LastNum}) -> + {iolist_to_binary(string:chars(LastStr+1, 5)), {LastStr+1, LastNum}}; +generate_example_input({_Name, atom}, {LastStr, LastNum}) -> + {list_to_atom(string:chars(LastStr+1, 5)), {LastStr+1, LastNum}}; +generate_example_input({_Name, rescode}, {LastStr, LastNum}) -> + {ok, {LastStr, LastNum}}; +generate_example_input({_Name, restuple}, {LastStr, LastNum}) -> + {{ok, <<"Success">>}, {LastStr, LastNum}}; +generate_example_input({_Name, {tuple, Fields}}, Data) -> + {R, D} = lists:foldl(fun(Field, {Res2, Data2}) -> + {Res3, Data3} = generate_example_input(Field, Data2), + {[Res3 | Res2], Data3} + end, {[], Data}, Fields), + {list_to_tuple(lists:reverse(R)), D}; +generate_example_input({_Name, {list, Desc}}, Data) -> + {R1, D1} = generate_example_input(Desc, Data), + {R2, D2} = generate_example_input(Desc, D1), + {[R1, R2], D2}. + +gen_calls(#ejabberd_commands{args_example=none, args=ArgsDesc} = C, HTMLOutput, Langs) -> + {R, _} = lists:foldl(fun(Arg, {Res, Data}) -> + {Res3, Data3} = generate_example_input(Arg, Data), + {[Res3 | Res], Data3} + end, {[], {$a-1, 0}}, ArgsDesc), + gen_calls(C#ejabberd_commands{args_example=lists:reverse(R)}, HTMLOutput, Langs); +gen_calls(#ejabberd_commands{result_example=none, result=ResultDesc} = C, HTMLOutput, Langs) -> + {R, _} = generate_example_input(ResultDesc, {$a-1, 0}), + gen_calls(C#ejabberd_commands{result_example=R}, HTMLOutput, Langs); +gen_calls(#ejabberd_commands{args_example=Values, args=ArgsDesc, + result_example=Result, result=ResultDesc, + name=Name}, HTMLOutput, Langs) -> + Perl = perl_call(Name, ArgsDesc, Values, HTMLOutput), + Java = java_call(Name, ArgsDesc, Values, HTMLOutput), + XML = xml_call(Name, ArgsDesc, Values, HTMLOutput), + JSON = json_call(Name, ArgsDesc, Values, ResultDesc, Result, HTMLOutput), + if HTMLOutput -> + [?TAG(ul, "code-samples-names", + [case lists:member(<<"java">>, Langs) of true -> ?TAG(li, <<"Java">>); _ -> [] end, + case lists:member(<<"perl">>, Langs) of true -> ?TAG(li, <<"Perl">>); _ -> [] end, + case lists:member(<<"xmlrpc">>, Langs) of true -> ?TAG(li, <<"XML">>); _ -> [] end, + case lists:member(<<"json">>, Langs) of true -> ?TAG(li, <<"JSON">>); _ -> [] end]), + ?TAG(ul, "code-samples", + [case lists:member(<<"java">>, Langs) of true -> ?TAG(li, ?TAG(pre, Java)); _ -> [] end, + case lists:member(<<"perl">>, Langs) of true -> ?TAG(li, ?TAG(pre, Perl)); _ -> [] end, + case lists:member(<<"xmlrpc">>, Langs) of true -> ?TAG(li, ?TAG(pre, XML)); _ -> [] end, + case lists:member(<<"json">>, Langs) of true -> ?TAG(li, ?TAG(pre, JSON)); _ -> [] end])]; + true -> + case Langs of + Val when length(Val) == 0 orelse length(Val) == 1 -> + [case lists:member(<<"java">>, Langs) of true -> [<<"\n">>, ?TAG(pre, Java), <<"~~~\n">>]; _ -> [] end, + case lists:member(<<"perl">>, Langs) of true -> [<<"\n">>, ?TAG(pre, Perl), <<"~~~\n">>]; _ -> [] end, + case lists:member(<<"xmlrpc">>, Langs) of true -> [<<"\n">>, ?TAG(pre, XML), <<"~~~\n">>]; _ -> [] end, + case lists:member(<<"json">>, Langs) of true -> [<<"\n">>, ?TAG(pre, JSON), <<"~~~\n">>]; _ -> [] end, + <<"\n\n">>]; + _ -> + [<<"\n">>, case lists:member(<<"java">>, Langs) of true -> <<"* Java\n">>; _ -> [] end, + case lists:member(<<"perl">>, Langs) of true -> <<"* Perl\n">>; _ -> [] end, + case lists:member(<<"xmlrpc">>, Langs) of true -> <<"* XmlRPC\n">>; _ -> [] end, + case lists:member(<<"json">>, Langs) of true -> <<"* JSON\n">>; _ -> [] end, + <<"{: .code-samples-labels}\n">>, + case lists:member(<<"java">>, Langs) of true -> [<<"\n* ">>, ?TAG(pre, Java), <<"~~~\n">>]; _ -> [] end, + case lists:member(<<"perl">>, Langs) of true -> [<<"\n* ">>, ?TAG(pre, Perl), <<"~~~\n">>]; _ -> [] end, + case lists:member(<<"xmlrpc">>, Langs) of true -> [<<"\n* ">>, ?TAG(pre, XML), <<"~~~\n">>]; _ -> [] end, + case lists:member(<<"json">>, Langs) of true -> [<<"\n* ">>, ?TAG(pre, JSON), <<"~~~\n">>]; _ -> [] end, + <<"{: .code-samples-tabs}\n\n">>] + end + end. + +format_type({list, {_, {tuple, Els}}}) -> + io_lib:format("[~ts]", [format_type({tuple, Els})]); +format_type({list, El}) -> + io_lib:format("[~ts]", [format_type(El)]); +format_type({tuple, Els}) -> + Args = [format_type(El) || El <- Els], + io_lib:format("{~ts}", [string:join(Args, ", ")]); +format_type({Name, Type}) -> + io_lib:format("~ts::~ts", [Name, format_type(Type)]); +format_type(binary) -> + "string"; +format_type(atom) -> + "string"; +format_type(Type) -> + io_lib:format("~p", [Type]). + +gen_param(Name, Type, undefined, HTMLOutput) -> + [?TAG(li, [?TAG_R(strong, atom_to_list(Name)), <<" :: ">>, ?RAW(format_type(Type))])]; +gen_param(Name, Type, Desc, HTMLOutput) -> + [?TAG(dt, [?TAG_R(strong, atom_to_list(Name)), <<" :: ">>, ?RAW(format_type(Type))]), + ?TAG(dd, ?RAW(Desc))]. + +make_tags(HTMLOutput) -> + TagsList = ejabberd_commands:get_tags_commands(1000000), + lists:map(fun(T) -> gen_tags(T, HTMLOutput) end, TagsList). + +-dialyzer({no_match, gen_tags/2}). +gen_tags({TagName, Commands}, HTMLOutput) -> + [?TAG(h1, TagName) | [?TAG(p, ?RAW("* _`"++C++"`_")) || C <- Commands]]. + +gen_doc(#ejabberd_commands{name=Name, tags=Tags, desc=Desc, longdesc=LongDesc, + args=Args, args_desc=ArgsDesc, note=Note, definer=Definer, + result=Result, result_desc=ResultDesc}=Cmd, HTMLOutput, Langs) -> + try + ArgsText = case ArgsDesc of + none -> + [?TAG(ul, "args-list", [gen_param(AName, Type, undefined, HTMLOutput) + || {AName, Type} <- Args])]; + _ -> + [?TAG(dl, "args-list", [gen_param(AName, Type, ADesc, HTMLOutput) + || {{AName, Type}, ADesc} <- lists:zip(Args, ArgsDesc)])] + end, + ResultText = case Result of + {res,rescode} -> + [?TAG(dl, [gen_param(res, integer, + "Status code (`0` on success, `1` otherwise)", + HTMLOutput)])]; + {res,restuple} -> + [?TAG(dl, [gen_param(res, string, + "Raw result string", + HTMLOutput)])]; + {RName, Type} -> + case ResultDesc of + none -> + [?TAG(ul, [gen_param(RName, Type, undefined, HTMLOutput)])]; + _ -> + [?TAG(dl, [gen_param(RName, Type, ResultDesc, HTMLOutput)])] + end + end, + TagsText = ?RAW(string:join(["_`"++atom_to_list(Tag)++"`_" || Tag <- Tags], ", ")), + IsDefinerMod = case Definer of + unknown -> false; + _ -> lists:member(gen_mod, lists:flatten(proplists:get_all_values(behaviour, Definer:module_info(attributes)))) + end, + ModuleText = case IsDefinerMod of + true -> + [?TAG(h2, <<"Module:">>), ?TAG(p, ?RAW("_`"++atom_to_list(Definer)++"`_"))]; + false -> + [] + end, + NoteEl = case Note of + "" -> []; + _ -> ?TAG('div', "note-down", ?RAW(Note)) + end, + {NotePre, NotePost} = + if HTMLOutput -> {[], NoteEl}; + true -> {NoteEl, []} + end, + + [?TAG(h1, make_command_name(Name, Note)), + NotePre, + ?TAG(p, ?RAW(Desc)), + case LongDesc of + "" -> []; + _ -> ?TAG(p, ?RAW(LongDesc)) + end, + NotePost, + ?TAG(h2, <<"Arguments:">>), ArgsText, + ?TAG(h2, <<"Result:">>), ResultText, + ?TAG(h2, <<"Tags:">>), ?TAG(p, TagsText)] + ++ ModuleText ++ [ + ?TAG(h2, <<"Examples:">>), gen_calls(Cmd, HTMLOutput, Langs)] + catch + _:Ex -> + throw(iolist_to_binary(io_lib:format( + <<"Error when generating documentation for command '~p': ~p">>, + [Name, Ex]))) + end. + +get_version_mark("") -> + ""; +get_version_mark(Note) -> + [XX, YY | _] = string:tokens(binary_to_list(ejabberd_option:version()), "."), + XXYY = string:join([XX, YY], "."), + case string:find(Note, XXYY) of + nomatch -> ""; + _ -> " 🟤" + end. + +make_command_name(Name, Note) -> + atom_to_list(Name) ++ get_version_mark(Note). + +find_commands_definitions() -> + lists:flatmap( + fun(Mod) -> + code:ensure_loaded(Mod), + Cs = case erlang:function_exported(Mod, get_commands_spec, 0) of + true -> + apply(Mod, get_commands_spec, []); + _ -> + [] + end, + [C#ejabberd_commands{definer = Mod} || C <- Cs] + end, ejabberd_config:beams(all)). + +generate_html_output(File, RegExp, Languages) -> + Cmds = find_commands_definitions(), + {ok, RE} = re:compile(RegExp), + Cmds2 = lists:filter(fun(#ejabberd_commands{name=Name, module=Module}) -> + re:run(atom_to_list(Name), RE, [{capture, none}]) == match orelse + re:run(atom_to_list(Module), RE, [{capture, none}]) == match + end, Cmds), + Cmds3 = lists:sort(fun(#ejabberd_commands{name=N1}, #ejabberd_commands{name=N2}) -> + N1 =< N2 + end, Cmds2), + Cmds4 = [maybe_add_policy_arguments(Cmd) || Cmd <- Cmds3], + Langs = binary:split(Languages, <<",">>, [global]), + Out = lists:map(fun(C) -> gen_doc(C, true, Langs) end, Cmds4), + {ok, Fh} = file:open(File, [write]), + io:format(Fh, "~ts", [[html_pre(), Out, html_post()]]), + file:close(Fh), + ok. + +maybe_add_policy_arguments(#ejabberd_commands{args=Args1, policy=user}=Cmd) -> + Args2 = [{user, binary}, {host, binary} | Args1], + Cmd#ejabberd_commands{args = Args2}; +maybe_add_policy_arguments(Cmd) -> + Cmd. + +generate_md_output(File, <<"runtime">>, Languages) -> + Cmds = lists:map(fun({N, _, _}) -> + ejabberd_commands:get_command_definition(N) + end, ejabberd_commands:list_commands()), + generate_md_output(File, <<".">>, Languages, Cmds); +generate_md_output(File, RegExp, Languages) -> + Cmds = find_commands_definitions(), + generate_md_output(File, RegExp, Languages, Cmds). + +generate_md_output(File, RegExp, Languages, Cmds) -> + {ok, RE} = re:compile(RegExp), + Cmds2 = lists:filter(fun(#ejabberd_commands{name=Name, module=Module}) -> + re:run(atom_to_list(Name), RE, [{capture, none}]) == match orelse + re:run(atom_to_list(Module), RE, [{capture, none}]) == match + end, Cmds), + Cmds3 = lists:sort(fun(#ejabberd_commands{name=N1}, #ejabberd_commands{name=N2}) -> + N1 =< N2 + end, Cmds2), + Cmds4 = [maybe_add_policy_arguments(Cmd) || Cmd <- Cmds3], + Langs = binary:split(Languages, <<",">>, [global]), + Version = binary_to_list(ejabberd_config:version()), + Header = ["# API Reference\n\n" + "This section describes API commands of ejabberd ", Version, ". " + "The commands that changed in this version are marked with 🟤.\n\n"], + Out = lists:map(fun(C) -> gen_doc(C, false, Langs) end, Cmds4), + {ok, Fh} = file:open(File, [write, {encoding, utf8}]), + io:format(Fh, "~ts~ts", [Header, Out]), + file:close(Fh), + ok. + +generate_tags_md(File) -> + Version = binary_to_list(ejabberd_config:version()), + Header = ["# API Tags\n\n" + "This section enumerates the API tags of ejabberd ", Version, ". \n\n"], + Tags = make_tags(false), + {ok, Fh} = file:open(File, [write, {encoding, utf8}]), + io:format(Fh, "~ts~ts", [Header, Tags]), + file:close(Fh), + ok. + +html_pre() -> + " + + + + + + + ". + +html_post() -> +" + +". diff --git a/src/ejabberd_config.erl b/src/ejabberd_config.erl index 54a635905..11173f9e8 100644 --- a/src/ejabberd_config.erl +++ b/src/ejabberd_config.erl @@ -5,7 +5,7 @@ %%% Created : 14 Dec 2002 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -22,1011 +22,963 @@ %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- - -module(ejabberd_config). --author('alexey@process-one.net'). --export([start/0, load_file/1, reload_file/0, read_file/1, - add_global_option/2, add_local_option/2, - get_global_option/2, get_local_option/2, - get_global_option/3, get_local_option/3, - get_option/2, get_option/3, add_option/2, - get_vh_by_auth_method/1, is_file_readable/1, - get_version/0, get_myhosts/0, get_mylang/0, - prepare_opt_val/4, convert_table_to_binary/5, - transform_options/1, collect_options/1, - convert_to_yaml/1, convert_to_yaml/2]). +%% API +-export([get_option/1]). +-export([load/0, reload/0, format_error/1, path/0]). +-export([env_binary_to_list/2]). +-export([get_myname/0, get_uri/0, get_copyright/0]). +-export([get_shared_key/0, get_node_start/0]). +-export([fsm_limit_opts/1]). +-export([codec_options/0]). +-export([version/0]). +-export([default_db/2, default_db/3, default_ram_db/2, default_ram_db/3]). +-export([beams/1, validators/1, globals/0, may_hide_data/1]). +-export([dump/0, dump/1, convert_to_yaml/1, convert_to_yaml/2]). +-export([callback_modules/1]). +-export([set_option/2]). +-export([get_defined_keywords/1, get_predefined_keywords/1, replace_keywords/2, replace_keywords/3]). +-export([resolve_host_alias/1]). + +%% Deprecated functions +-export([get_option/2]). +-export([get_version/0, get_myhosts/0]). +-export([get_mylang/0, get_lang/1]). +-deprecated([{get_option, 2}, + {get_version, 0}, + {get_myhosts, 0}, + {get_mylang, 0}, + {get_lang, 1}]). --include("ejabberd.hrl"). -include("logger.hrl"). --include("ejabberd_config.hrl"). --include_lib("kernel/include/file.hrl"). -%% @type macro() = {macro_key(), macro_value()} +-type option() :: atom() | {atom(), global | binary()}. +-type error_reason() :: {merge_conflict, atom(), binary()} | + {old_config, file:filename_all(), term()} | + {write_file, file:filename_all(), term()} | + {exception, term(), term(), term()}. +-type error_return() :: {error, econf:error_reason(), term()} | + {error, error_reason()}. +-type host_config() :: #{{atom(), binary() | global} => term()}. -%% @type macro_key() = atom(). -%% The atom must have all characters in uppercase. +-callback opt_type(atom()) -> econf:validator(). +-callback options() -> [atom() | {atom(), term()}]. +-callback globals() -> [atom()]. +-callback doc() -> any(). -%% @type macro_value() = term(). +-optional_callbacks([globals/0]). +-ifndef(OTP_BELOW_28). +-dialyzer([no_opaque_union]). +-endif. -start() -> - case catch mnesia:table_info(local_config, storage_type) of - disc_copies -> - mnesia:delete_table(local_config); - _ -> - ok - end, - mnesia:create_table(local_config, - [{ram_copies, [node()]}, - {local_content, true}, - {attributes, record_info(fields, local_config)}]), - mnesia:add_table_copy(local_config, node(), ram_copies), - Config = get_ejabberd_config_path(), - State = read_file(Config), - %% This start time is used by mod_last: - {MegaSecs, Secs, _} = now(), - UnixTime = MegaSecs*1000000 + Secs, - SharedKey = case erlang:get_cookie() of - nocookie -> - p1_sha:sha(randoms:get_string()); - Cookie -> - p1_sha:sha(jlib:atom_to_binary(Cookie)) - end, - State1 = set_option({node_start, global}, UnixTime, State), - State2 = set_option({shared_key, global}, SharedKey, State1), - set_opts(State2). +%%%=================================================================== +%%% API +%%%=================================================================== +-spec load() -> ok | error_return(). +load() -> + load(path()). -%% @doc Get the filename of the ejabberd configuration file. -%% The filename can be specified with: erl -config "/path/to/ejabberd.yml". -%% It can also be specified with the environtment variable EJABBERD_CONFIG_PATH. -%% If not specified, the default value 'ejabberd.yml' is assumed. -%% @spec () -> string() -get_ejabberd_config_path() -> - case application:get_env(config) of - {ok, Path} -> Path; - undefined -> - case os:getenv("EJABBERD_CONFIG_PATH") of - false -> - ?CONFIG_PATH; - Path -> - Path +-spec load(file:filename_all()) -> ok | error_return(). +load(Path) -> + ConfigFile = unicode:characters_to_binary(Path), + UnixTime = erlang:monotonic_time(second), + ?INFO_MSG("Loading configuration from ~ts", [ConfigFile]), + _ = ets:new(ejabberd_options, + [named_table, public, {read_concurrency, true}]), + case load_file(ConfigFile) of + ok -> + set_shared_key(), + set_node_start(UnixTime), + ?INFO_MSG("Configuration loaded successfully", []); + Err -> + Err + end. + +-spec reload() -> ok | error_return(). +reload() -> + ejabberd_systemd:reloading(), + ConfigFile = path(), + ?INFO_MSG("Reloading configuration from ~ts", [ConfigFile]), + OldHosts = get_myhosts(), + Res = case load_file(ConfigFile) of + ok -> + NewHosts = get_myhosts(), + AddHosts = NewHosts -- OldHosts, + DelHosts = OldHosts -- NewHosts, + lists:foreach( + fun(Host) -> + ejabberd_hooks:run(host_up, [Host]) + end, AddHosts), + lists:foreach( + fun(Host) -> + ejabberd_hooks:run(host_down, [Host]) + end, DelHosts), + ejabberd_hooks:run(config_reloaded, []), + % logger is started too early to be able to use hooks, so + % we need to call it separately + ejabberd_logger:config_reloaded(), + delete_host_options(DelHosts), + ?INFO_MSG("Configuration reloaded successfully", []); + Err -> + ?ERROR_MSG("Configuration reload aborted: ~ts", + [format_error(Err)]), + Err + end, + ejabberd_systemd:ready(), + Res. + +-spec dump() -> ok | error_return(). +dump() -> + dump(stdout). + +-spec dump(stdout | file:filename_all()) -> ok | error_return(). +dump(Output) -> + Y = get_option(yaml_config), + dump(Y, Output). + +-spec dump(term(), stdout | file:filename_all()) -> ok | error_return(). +dump(Y, Output) -> + Data = fast_yaml:encode(Y), + case Output of + stdout -> + io:format("~ts~n", [Data]); + FileName -> + try + ok = filelib:ensure_dir(FileName), + ok = file:write_file(FileName, Data) + catch _:{badmatch, {error, Reason}} -> + {error, {write_file, FileName, Reason}} end end. -%% @doc Read the ejabberd configuration file. -%% It also includes additional configuration files and replaces macros. -%% This function will crash if finds some error in the configuration file. -%% @spec (File::string()) -> #state{}. -read_file(File) -> - read_file(File, [{replace_macros, true}, - {include_files, true}]). +-spec get_option(option(), term()) -> term(). +get_option(Opt, Default) -> + try get_option(Opt) + catch _:badarg -> Default + end. -read_file(File, Opts) -> - Terms1 = get_plain_terms_file(File, Opts), - Terms_macros = case proplists:get_bool(replace_macros, Opts) of - true -> replace_macros(Terms1); - false -> Terms1 - end, - Terms = transform_terms(Terms_macros), - State = lists:foldl(fun search_hosts/2, #state{}, Terms), - {Head, Tail} = lists:partition( - fun({host_config, _}) -> false; - ({append_host_config, _}) -> false; - (_) -> true - end, Terms), - State1 = lists:foldl(fun process_term/2, State, Head ++ Tail), - State1#state{opts = compact(State1#state.opts)}. +-spec get_option(option()) -> term(). +get_option(Opt) when is_atom(Opt) -> + get_option({Opt, global}); +get_option({O, Host} = Opt) -> + Tab = case get_tmp_config() of + undefined -> ejabberd_options; + T -> T + end, + try ets:lookup_element(Tab, Opt, 2) + catch + error:badarg:StackTrace when Host /= global -> + Val = get_option({O, global}), + ?DEBUG("Option '~ts' is not defined for virtual host '~ts'. " + "This is a bug, please report it with the following " + "stacktrace included:~n** ~ts", + [O, Host, misc:format_exception(2, error, badarg, StackTrace)]), + Val + end. --spec load_file(string()) -> ok. +-spec set_option(option(), term()) -> ok. +set_option(Opt, Val) when is_atom(Opt) -> + set_option({Opt, global}, Val); +set_option(Opt, Val) -> + Tab = case get_tmp_config() of + undefined -> ejabberd_options; + T -> T + end, + ets:insert(Tab, {Opt, Val}), + ok. -load_file(File) -> - State = read_file(File), - set_opts(State). +-spec get_version() -> binary(). +get_version() -> + get_option(version). --spec reload_file() -> ok. +-spec get_myhosts() -> [binary(), ...]. +get_myhosts() -> + get_option(hosts). -reload_file() -> - Config = get_ejabberd_config_path(), - load_file(Config). +-spec get_myname() -> binary(). +get_myname() -> + get_option(host). --spec convert_to_yaml(file:filename()) -> ok | {error, any()}. +-spec get_mylang() -> binary(). +get_mylang() -> + get_lang(global). +-spec get_lang(global | binary()) -> binary(). +get_lang(Host) -> + get_option({language, Host}). + +-spec get_uri() -> binary(). +get_uri() -> + <<"https://www.process-one.net/ejabberd/">>. + +-spec get_copyright() -> binary(). +get_copyright() -> + <<"Copyright (c) ProcessOne">>. + +-spec get_shared_key() -> binary(). +get_shared_key() -> + get_option(shared_key). + +-spec get_node_start() -> integer(). +get_node_start() -> + get_option(node_start). + +-spec fsm_limit_opts([proplists:property()]) -> [{max_queue, pos_integer()}]. +fsm_limit_opts(Opts) -> + case lists:keyfind(max_fsm_queue, 1, Opts) of + {_, I} when is_integer(I), I>0 -> + [{max_queue, I}]; + false -> + case get_option(max_fsm_queue) of + undefined -> []; + N -> [{max_queue, N}] + end + end. + +-spec codec_options() -> [xmpp:decode_option()]. +codec_options() -> + case get_option(validate_stream) of + true -> []; + false -> [ignore_els] + end. + +%% Do not use this function in runtime: +%% It's slow and doesn't read 'version' option from the config. +%% Use ejabberd_option:version() instead. +-spec version() -> binary(). +version() -> + case application:get_env(ejabberd, custom_vsn) of + {ok, Vsn0} when is_list(Vsn0) -> + list_to_binary(Vsn0); + {ok, Vsn1} when is_binary(Vsn1) -> + Vsn1; + _ -> + case application:get_key(ejabberd, vsn) of + undefined -> <<"">>; + {ok, Vsn} -> list_to_binary(Vsn) + end + end. + +-spec default_db(binary() | global, module()) -> atom(). +default_db(Host, Module) -> + default_db(default_db, db_type, Host, Module, mnesia). + +-spec default_db(binary() | global, module(), atom()) -> atom(). +default_db(Host, Module, Default) -> + default_db(default_db, db_type, Host, Module, Default). + +-spec default_ram_db(binary() | global, module()) -> atom(). +default_ram_db(Host, Module) -> + default_db(default_ram_db, ram_db_type, Host, Module, mnesia). + +-spec default_ram_db(binary() | global, module(), atom()) -> atom(). +default_ram_db(Host, Module, Default) -> + default_db(default_ram_db, ram_db_type, Host, Module, Default). + +-spec default_db(default_db | default_ram_db, db_type | ram_db_type, binary() | global, module(), atom()) -> atom(). +default_db(Opt, ModOpt, Host, Mod, Default) -> + Type = get_option({Opt, Host}), + DBMod = list_to_atom(atom_to_list(Mod) ++ "_" ++ atom_to_list(Type)), + case code:ensure_loaded(DBMod) of + {module, _} -> Type; + {error, _} -> + ?WARNING_MSG("Module ~ts doesn't support database '~ts' " + "defined in toplevel option '~ts': will use the value " + "set in ~ts option '~ts', or '~ts' as fallback", + [Mod, Type, Opt, Mod, ModOpt, Default]), + Default + end. + +-spec beams(local | external | all) -> [module()]. +beams(local) -> + {ok, Mods} = application:get_key(ejabberd, modules), + Mods; +beams(external) -> + ExtMods = [Name || {Name, _Details} <- ext_mod:installed()], + lists:foreach( + fun(ExtMod) -> + ExtModPath = ext_mod:module_ebin_dir(ExtMod), + case lists:member(ExtModPath, code:get_path()) of + true -> ok; + false -> code:add_patha(ExtModPath) + end + end, ExtMods), + case application:get_env(ejabberd, external_beams) of + {ok, Path0} -> + Paths = case Path0 of + [L|_] = V when is_list(L) -> V; + L -> [L] + end, + CustMods = lists:foldl( + fun(Path, CM) -> + case lists:member(Path, code:get_path()) of + true -> ok; + false -> code:add_patha(Path) + end, + Beams = filelib:wildcard(filename:join(Path, "*\.beam")), + CM ++ [list_to_atom(filename:rootname(filename:basename(Beam))) + || Beam <- Beams] + end, [], Paths), + CustMods ++ ExtMods; + _ -> + ExtMods + end; +beams(all) -> + beams(local) ++ beams(external). + +-spec may_hide_data(term()) -> term(). +may_hide_data(Data) -> + case get_option(hide_sensitive_log_data) of + false -> Data; + true -> "hidden_by_ejabberd" + end. + +%% Some Erlang apps expects env parameters to be list and not binary. +%% For example, Mnesia is not able to start if mnesia dir is passed as a binary. +%% However, binary is most common on Elixir, so it is easy to make a setup mistake. +-spec env_binary_to_list(atom(), atom()) -> {ok, any()} | undefined. +env_binary_to_list(Application, Parameter) -> + %% Application need to be loaded to allow setting parameters + case proplists:is_defined(Application, application:loaded_applications()) of + true -> + ok; + false -> + application:load(Application) + end, + case application:get_env(Application, Parameter) of + {ok, Val} when is_binary(Val) -> + BVal = binary_to_list(Val), + application:set_env(Application, Parameter, BVal), + {ok, BVal}; + Other -> + Other + end. + +%% ejabberd_options calls this function when parsing options inside host_config +-spec validators([atom()]) -> {econf:validators(), [atom()]}. +validators(Disallowed) -> + Host = global, + DefinedKeywords = get_defined_keywords(Host), + validators(Disallowed, DefinedKeywords). + +%% validate/1 calls this function when parsing toplevel options +-spec validators([atom()], [any()]) -> {econf:validators(), [atom()]}. +validators(Disallowed, DK) -> + Modules = callback_modules(all), + Validators = lists:foldl( + fun(M, Vs) -> + maps:merge(Vs, validators(M, Disallowed, DK)) + end, #{}, Modules), + Required = lists:flatmap( + fun(M) -> + [O || O <- M:options(), is_atom(O)] + end, Modules), + {Validators, Required}. + +-spec convert_to_yaml(file:filename()) -> ok | error_return(). convert_to_yaml(File) -> convert_to_yaml(File, stdout). -spec convert_to_yaml(file:filename(), - stdout | file:filename()) -> ok | {error, any()}. - + stdout | file:filename()) -> ok | error_return(). convert_to_yaml(File, Output) -> - State = read_file(File, [{include_files, false}]), - Opts = [{K, V} || #local_config{key = K, value = V} <- State#state.opts], - {GOpts, HOpts} = split_by_hosts(Opts), - NewOpts = GOpts ++ lists:map( - fun({Host, Opts1}) -> - {host_config, [{Host, Opts1}]} - end, HOpts), - Data = p1_yaml:encode(lists:reverse(NewOpts)), - case Output of - stdout -> - io:format("~s~n", [Data]); - FileName -> - file:write_file(FileName, Data) + case read_erlang_file(File, []) of + {ok, Y} -> + dump(Y, Output); + Err -> + Err end. -%% @doc Read an ejabberd configuration file and return the terms. -%% Input is an absolute or relative path to an ejabberd config file. -%% Returns a list of plain terms, -%% in which the options 'include_config_file' were parsed -%% and the terms in those files were included. -%% @spec(string()) -> [term()] -%% @spec(iolist()) -> [term()] -get_plain_terms_file(File) -> - get_plain_terms_file(File, [{include_files, true}]). +-spec format_error(error_return()) -> string(). +format_error({error, Reason, Ctx}) -> + econf:format_error(Reason, Ctx); +format_error({error, {merge_conflict, Opt, Host}}) -> + lists:flatten( + io_lib:format( + "Cannot merge value of option '~ts' defined in append_host_config " + "for virtual host ~ts: only options of type list or map are allowed " + "in append_host_config. Hint: specify the option in host_config", + [Opt, Host])); +format_error({error, {old_config, Path, Reason}}) -> + lists:flatten( + io_lib:format( + "Failed to read configuration from '~ts': ~ts~ts", + [Path, + case Reason of + {_, _, _} -> "at line "; + _ -> "" + end, file:format_error(Reason)])); +format_error({error, {write_file, Path, Reason}}) -> + lists:flatten( + io_lib:format( + "Failed to write to '~ts': ~ts", + [Path, + file:format_error(Reason)])); +format_error({error, {exception, Class, Reason, St}}) -> + lists:flatten( + io_lib:format( + "Exception occurred during configuration processing. " + "This is most likely due to faulty/incompatible validator in " + "third-party code. If you are not running any third-party " + "code, please report the bug with ejabberd configuration " + "file attached and the following stacktrace included:~n** ~ts", + [misc:format_exception(2, Class, Reason, St)])). -get_plain_terms_file(File, Opts) when is_binary(File) -> - get_plain_terms_file(binary_to_list(File), Opts); -get_plain_terms_file(File1, Opts) -> - File = get_absolute_path(File1), - case consult(File) of - {ok, Terms} -> - BinTerms = strings_to_binary(Terms), - case proplists:get_bool(include_files, Opts) of - true -> - include_config_files(BinTerms); - false -> - BinTerms - end; - {error, Reason} -> - ?ERROR_MSG(Reason, []), - exit_or_halt(Reason) - end. +%% @format-begin -consult(File) -> - case filename:extension(File) of - ".yml" -> - case p1_yaml:decode_from_file(File, [plain_as_atom]) of - {ok, []} -> - {ok, []}; - {ok, [Document|_]} -> - {ok, parserl(Document)}; - {error, Err} -> - Msg1 = "Cannot load " ++ File ++ ": ", - Msg2 = p1_yaml:format_error(Err), - {error, Msg1 ++ Msg2} - end; - _ -> - case file:consult(File) of - {ok, Terms} -> - {ok, Terms}; - {error, {LineNumber, erl_parse, _ParseMessage} = Reason} -> - {error, describe_config_problem(File, Reason, LineNumber)}; - {error, Reason} -> - {error, describe_config_problem(File, Reason)} +replace_keywords(Host, Value) -> + Keywords = get_defined_keywords(Host) ++ get_predefined_keywords(Host), + replace_keywords(Host, Value, Keywords). + +replace_keywords(Host, List, Keywords) when is_list(List) -> + [replace_keywords(Host, Element, Keywords) || Element <- List]; +replace_keywords(Host, Atom, Keywords) when is_atom(Atom) -> + Str = atom_to_list(Atom), + Bin = iolist_to_binary(Str), + case Str == string:uppercase(Str) of + false -> + BinaryReplaced = replace_keywords(Host, Bin, Keywords), + binary_to_atom(BinaryReplaced, utf8); + true -> + case proplists:get_value(Bin, Keywords) of + undefined -> + Atom; + Replacement -> + Replacement end - end. - -parserl(<<"> ", Term/binary>>) -> - {ok, A2, _} = erl_scan:string(binary_to_list(Term)), - {ok, A3} = erl_parse:parse_term(A2), - A3; -parserl({A, B}) -> - {parserl(A), parserl(B)}; -parserl([El|Tail]) -> - [parserl(El) | parserl(Tail)]; -parserl(Other) -> - Other. - -%% @doc Convert configuration filename to absolute path. -%% Input is an absolute or relative path to an ejabberd configuration file. -%% And returns an absolute path to the configuration file. -%% @spec (string()) -> string() -get_absolute_path(File) -> - case filename:pathtype(File) of - absolute -> - File; - relative -> - {ok, Dir} = file:get_cwd(), - filename:absname_join(Dir, File) - end. - - -search_hosts(Term, State) -> - case Term of - {host, Host} -> - if - State#state.hosts == [] -> - add_hosts_to_option([Host], State); - true -> - ?ERROR_MSG("Can't load config file: " - "too many hosts definitions", []), - exit("too many hosts definitions") - end; - {hosts, Hosts} -> - if - State#state.hosts == [] -> - add_hosts_to_option(Hosts, State); - true -> - ?ERROR_MSG("Can't load config file: " - "too many hosts definitions", []), - exit("too many hosts definitions") - end; - _ -> - State - end. - -add_hosts_to_option(Hosts, State) -> - PrepHosts = normalize_hosts(Hosts), - set_option({hosts, global}, PrepHosts, State#state{hosts = PrepHosts}). - -normalize_hosts(Hosts) -> - normalize_hosts(Hosts,[]). -normalize_hosts([], PrepHosts) -> - lists:reverse(PrepHosts); -normalize_hosts([Host|Hosts], PrepHosts) -> - case jlib:nodeprep(iolist_to_binary(Host)) of - error -> - ?ERROR_MSG("Can't load config file: " - "invalid host name [~p]", [Host]), - exit("invalid hostname"); - PrepHost -> - normalize_hosts(Hosts, [PrepHost|PrepHosts]) - end. - - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%%% Errors reading the config file - -describe_config_problem(Filename, Reason) -> - Text1 = lists:flatten("Problem loading ejabberd config file " ++ Filename), - Text2 = lists:flatten(" : " ++ file:format_error(Reason)), - ExitText = Text1 ++ Text2, - ExitText. - -describe_config_problem(Filename, Reason, LineNumber) -> - Text1 = lists:flatten("Problem loading ejabberd config file " ++ Filename), - Text2 = lists:flatten(" approximately in the line " - ++ file:format_error(Reason)), - ExitText = Text1 ++ Text2, - Lines = get_config_lines(Filename, LineNumber, 10, 3), - ?ERROR_MSG("The following lines from your configuration file might be" - " relevant to the error: ~n~s", [Lines]), - ExitText. - -get_config_lines(Filename, TargetNumber, PreContext, PostContext) -> - {ok, Fd} = file:open(Filename, [read]), - LNumbers = lists:seq(TargetNumber-PreContext, TargetNumber+PostContext), - NextL = io:get_line(Fd, no_prompt), - R = get_config_lines2(Fd, NextL, 1, LNumbers, []), - file:close(Fd), - R. - -get_config_lines2(_Fd, eof, _CurrLine, _LNumbers, R) -> - lists:reverse(R); -get_config_lines2(_Fd, _NewLine, _CurrLine, [], R) -> - lists:reverse(R); -get_config_lines2(Fd, Data, CurrLine, [NextWanted | LNumbers], R) when is_list(Data) -> - NextL = io:get_line(Fd, no_prompt), - if - CurrLine >= NextWanted -> - Line2 = [integer_to_list(CurrLine), ": " | Data], - get_config_lines2(Fd, NextL, CurrLine+1, LNumbers, [Line2 | R]); - true -> - get_config_lines2(Fd, NextL, CurrLine+1, [NextWanted | LNumbers], R) - end. - -%% If ejabberd isn't yet running in this node, then halt the node -exit_or_halt(ExitText) -> - case [Vsn || {ejabberd, _Desc, Vsn} <- application:which_applications()] of - [] -> - timer:sleep(1000), - halt(string:substr(ExitText, 1, 199)); - [_] -> - exit(ExitText) - end. - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%%% Support for 'include_config_file' - -%% @doc Include additional configuration files in the list of terms. -%% @spec ([term()]) -> [term()] -include_config_files(Terms) -> - {FileOpts, Terms1} = - lists:mapfoldl( - fun({include_config_file, _} = T, Ts) -> - {[transform_include_option(T)], Ts}; - ({include_config_file, _, _} = T, Ts) -> - {[transform_include_option(T)], Ts}; - (T, Ts) -> - {[], [T|Ts]} - end, [], Terms), - Terms2 = lists:flatmap( - fun({File, Opts}) -> - include_config_file(File, Opts) - end, lists:flatten(FileOpts)), - Terms1 ++ Terms2. - -transform_include_option({include_config_file, File}) when is_list(File) -> - case is_string(File) of - true -> {File, []}; - false -> File end; -transform_include_option({include_config_file, Filename}) -> - {Filename, []}; -transform_include_option({include_config_file, Filename, Options}) -> - {Filename, Options}. +replace_keywords(_Host, Binary, Keywords) when is_binary(Binary) -> + lists:foldl(fun ({Key, Replacement}, V) when is_binary(Replacement) -> + misc:expand_keyword(<<"@", Key/binary, "@">>, V, Replacement); + ({_, _}, V) -> + V + end, + Binary, + Keywords); +replace_keywords(Host, {Element1, Element2}, Keywords) -> + {Element1, replace_keywords(Host, Element2, Keywords)}; +replace_keywords(_Host, Value, _DK) -> + Value. -include_config_file(Filename, Options) -> - Included_terms = get_plain_terms_file(Filename), - Disallow = proplists:get_value(disallow, Options, []), - Included_terms2 = delete_disallowed(Disallow, Included_terms), - Allow_only = proplists:get_value(allow_only, Options, all), - keep_only_allowed(Allow_only, Included_terms2). - -%% @doc Filter from the list of terms the disallowed. -%% Returns a sublist of Terms without the ones which first element is -%% included in Disallowed. -%% @spec (Disallowed::[atom()], Terms::[term()]) -> [term()] -delete_disallowed(Disallowed, Terms) -> - lists:foldl( - fun(Dis, Ldis) -> - delete_disallowed2(Dis, Ldis) - end, - Terms, - Disallowed). - -delete_disallowed2(Disallowed, [H|T]) -> - case element(1, H) of - Disallowed -> - ?WARNING_MSG("The option '~p' is disallowed, " - "and will not be accepted", [Disallowed]), - delete_disallowed2(Disallowed, T); - _ -> - [H|delete_disallowed2(Disallowed, T)] - end; -delete_disallowed2(_, []) -> - []. - -%% @doc Keep from the list only the allowed terms. -%% Returns a sublist of Terms with only the ones which first element is -%% included in Allowed. -%% @spec (Allowed::[atom()], Terms::[term()]) -> [term()] -keep_only_allowed(all, Terms) -> - Terms; -keep_only_allowed(Allowed, Terms) -> - {As, NAs} = lists:partition( - fun(Term) -> - lists:member(element(1, Term), Allowed) - end, - Terms), - [?WARNING_MSG("This option is not allowed, " - "and will not be accepted:~n~p", [NA]) - || NA <- NAs], - As. - - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%%% Support for Macro - -%% @doc Replace the macros with their defined values. -%% @spec (Terms::[term()]) -> [term()] -replace_macros(Terms) -> - {TermsOthers, Macros} = split_terms_macros(Terms), - replace(TermsOthers, Macros). - -%% @doc Split Terms into normal terms and macro definitions. -%% @spec (Terms) -> {Terms, Macros} -%% Terms = [term()] -%% Macros = [macro()] -split_terms_macros(Terms) -> - lists:foldl( - fun(Term, {TOs, Ms}) -> - case Term of - {define_macro, Key, Value} -> - case is_correct_macro({Key, Value}) of - true -> - {TOs, Ms++[{Key, Value}]}; - false -> - exit({macro_not_properly_defined, Term}) - end; - {define_macro, KeyVals} -> - case lists:all(fun is_correct_macro/1, KeyVals) of - true -> - {TOs, Ms ++ KeyVals}; - false -> - exit({macros_not_properly_defined, Term}) - end; - Term -> - {TOs ++ [Term], Ms} - end - end, - {[], []}, - Terms). - -is_correct_macro({Key, _Val}) -> - is_atom(Key) and is_all_uppercase(Key); -is_correct_macro(_) -> - false. - -%% @doc Recursively replace in Terms macro usages with the defined value. -%% @spec (Terms, Macros) -> Terms -%% Terms = [term()] -%% Macros = [macro()] -replace([], _) -> - []; -replace([Term|Terms], Macros) -> - [replace_term(Term, Macros) | replace(Terms, Macros)]; -replace(Term, Macros) -> - replace_term(Term, Macros). - -replace_term(Key, Macros) when is_atom(Key) -> - case is_all_uppercase(Key) of - true -> - case proplists:get_value(Key, Macros) of - undefined -> exit({undefined_macro, Key}); - Value -> Value - end; - false -> - Key - end; -replace_term({use_macro, Key, Value}, Macros) -> - proplists:get_value(Key, Macros, Value); -replace_term(Term, Macros) when is_list(Term) -> - replace(Term, Macros); -replace_term(Term, Macros) when is_tuple(Term) -> - List = tuple_to_list(Term), - List2 = replace(List, Macros), - list_to_tuple(List2); -replace_term(Term, _) -> - Term. - -is_all_uppercase(Atom) -> - String = erlang:atom_to_list(Atom), - lists:all(fun(C) when C >= $a, C =< $z -> false; - (_) -> true - end, String). - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%%% Process terms - -process_term(Term, State) -> - case Term of - {host_config, HostTerms} -> - lists:foldl( - fun({Host, Terms}, AccState) -> - lists:foldl(fun(T, S) -> - process_host_term(T, Host, S, set) - end, AccState, Terms) - end, State, HostTerms); - {append_host_config, HostTerms} -> - lists:foldl( - fun({Host, Terms}, AccState) -> - lists:foldl(fun(T, S) -> - process_host_term(T, Host, S, append) - end, AccState, Terms) - end, State, HostTerms); - _ -> - process_host_term(Term, global, State, set) - end. - -process_host_term(Term, Host, State, Action) -> - case Term of - {modules, Modules} when Action == set -> - set_option({modules, Host}, replace_modules(Modules), State); - {modules, Modules} when Action == append -> - append_option({modules, Host}, replace_modules(Modules), State); - {host, _} -> - State; - {hosts, _} -> - State; - {Opt, Val} when Action == set -> - set_option({Opt, Host}, Val, State); - {Opt, Val} when Action == append -> - append_option({Opt, Host}, Val, State); - Opt -> - ?WARNING_MSG("Ignore invalid (outdated?) option ~p", [Opt]), - State - end. - -set_option(Opt, Val, State) -> - State#state{opts = [#local_config{key = Opt, value = Val} | - State#state.opts]}. - -append_option({Opt, Host}, Val, State) -> - GlobalVals = lists:flatmap( - fun(#local_config{key = {O, global}, value = V}) - when O == Opt -> - if is_list(V) -> V; - true -> [V] - end; - (_) -> - [] - end, State#state.opts), - NewVal = if is_list(Val) -> Val ++ GlobalVals; - true -> [Val|GlobalVals] - end, - set_option({Opt, Host}, NewVal, State). - -set_opts(State) -> - Opts = State#state.opts, - F = fun() -> - lists:foreach(fun(R) -> - mnesia:write(R) - end, Opts) - end, - case mnesia:transaction(F) of - {atomic, _} -> ok; - {aborted,{no_exists,Table}} -> - MnesiaDirectory = mnesia:system_info(directory), - ?ERROR_MSG("Error reading Mnesia database spool files:~n" - "The Mnesia database couldn't read the spool file for the table '~p'.~n" - "ejabberd needs read and write access in the directory:~n ~s~n" - "Maybe the problem is a change in the computer hostname,~n" - "or a change in the Erlang node name, which is currently:~n ~p~n" - "Check the ejabberd guide for details about changing the~n" - "computer hostname or Erlang node name.~n", - [Table, MnesiaDirectory, node()]), - exit("Error reading Mnesia database") - end. - -add_global_option(Opt, Val) -> - add_option(Opt, Val). - -add_local_option(Opt, Val) -> - add_option(Opt, Val). - -add_option(Opt, Val) when is_atom(Opt) -> - add_option({Opt, global}, Val); -add_option(Opt, Val) -> - mnesia:transaction(fun() -> - mnesia:write(#local_config{key = Opt, - value = Val}) - end). - --spec prepare_opt_val(any(), any(), check_fun(), any()) -> any(). - -prepare_opt_val(Opt, Val, F, Default) -> - Res = case F of - {Mod, Fun} -> - catch Mod:Fun(Val); - _ -> - catch F(Val) +get_defined_keywords(Host) -> + Tab = case get_tmp_config() of + undefined -> + ejabberd_options; + T -> + T end, - case Res of - {'EXIT', _} -> - ?INFO_MSG("Configuration problem:~n" - "** Option: ~s~n" - "** Invalid value: ~s~n" - "** Using as fallback: ~s", - [format_term(Opt), - format_term(Val), - format_term(Default)]), - Default; - _ -> - Res - end. + get_defined_keywords(Tab, Host). --type check_fun() :: fun((any()) -> any()) | {module(), atom()}. +get_defined_keywords(Tab, Host) -> + KeysHost = + case ets:lookup(Tab, {define_keyword, Host}) of + [{_, List}] -> + List; + _ -> + [] + end, + KeysGlobal = + case Host /= global andalso ets:lookup(Tab, {define_keyword, global}) of + [{_, ListG}] -> + ListG; + _ -> + [] + end, + %% Trying to get defined keywords in host_config when starting ejabberd, + %% the options are not yet stored in ets + KeysTemp = + case not is_atom(Tab) andalso KeysHost == [] andalso KeysGlobal == [] of + true -> + get_defined_keywords_yaml_config(ets:lookup_element(Tab, {yaml_config, global}, 2)); + false -> + [] + end, + lists:reverse(KeysTemp ++ KeysGlobal ++ KeysHost). --spec get_global_option(any(), check_fun()) -> any(). +get_defined_keywords_yaml_config(Y) -> + [{erlang:atom_to_binary(KwAtom, latin1), KwValue} + || {KwAtom, KwValue} <- proplists:get_value(define_keyword, Y, [])]. -get_global_option(Opt, F) -> - get_option(Opt, F, undefined). +get_predefined_keywords(Host) -> + HostList = + case Host of + global -> + []; + _ -> + [{<<"HOST">>, Host}, {<<"HOST_URL_ENCODE">>, misc:url_encode(Host)}] + end, + Home = misc:get_home(), + ConfigDirPath = + iolist_to_binary(filename:dirname( + ejabberd_config:path())), + LogDirPath = + iolist_to_binary(filename:dirname( + ejabberd_logger:get_log_path())), + HostList + ++ [{<<"HOME">>, list_to_binary(Home)}, + {<<"CONFIG_PATH">>, ConfigDirPath}, + {<<"LOG_PATH">>, LogDirPath}, + {<<"SEMVER">>, ejabberd_option:version()}, + {<<"VERSION">>, + misc:semver_to_xxyy( + ejabberd_option:version())}]. --spec get_global_option(any(), check_fun(), any()) -> any(). - -get_global_option(Opt, F, Default) -> - get_option(Opt, F, Default). - --spec get_local_option(any(), check_fun()) -> any(). - -get_local_option(Opt, F) -> - get_option(Opt, F, undefined). - --spec get_local_option(any(), check_fun(), any()) -> any(). - -get_local_option(Opt, F, Default) -> - get_option(Opt, F, Default). - --spec get_option(any(), check_fun()) -> any(). - -get_option(Opt, F) -> - get_option(Opt, F, undefined). - --spec get_option(any(), check_fun(), any()) -> any(). - -get_option(Opt, F, Default) when is_atom(Opt) -> - get_option({Opt, global}, F, Default); -get_option(Opt, F, Default) -> - case Opt of - {O, global} when is_atom(O) -> ok; - {O, H} when is_atom(O), is_binary(H) -> ok; - _ -> ?WARNING_MSG("Option ~p has invalid (outdated?) format. " - "This is likely a bug", [Opt]) - end, - case ets:lookup(local_config, Opt) of - [#local_config{value = Val}] -> - prepare_opt_val(Opt, Val, F, Default); - _ -> - case Opt of - {Key, Host} when Host /= global -> - get_option({Key, global}, F, Default); - _ -> - Default - end - end. - --spec get_vh_by_auth_method(atom()) -> [binary()]. - -%% Return the list of hosts handled by a given module -get_vh_by_auth_method(AuthMethod) -> - mnesia:dirty_select(local_config, - [{#local_config{key = {auth_method, '$1'}, - value=AuthMethod},[],['$1']}]). - -%% @spec (Path::string()) -> true | false -is_file_readable(Path) -> - case file:read_file_info(Path) of - {ok, FileInfo} -> - case {FileInfo#file_info.type, FileInfo#file_info.access} of - {regular, read} -> true; - {regular, read_write} -> true; - _ -> false - end; - {error, _Reason} -> - false - end. - -get_version() -> - list_to_binary(element(2, application:get_key(ejabberd, vsn))). - --spec get_myhosts() -> [binary()]. - -get_myhosts() -> - get_option(hosts, fun(V) -> V end). - --spec get_mylang() -> binary(). - -get_mylang() -> - get_option( - language, - fun iolist_to_binary/1, - <<"en">>). - -replace_module(mod_announce_odbc) -> {mod_announce, odbc}; -replace_module(mod_blocking_odbc) -> {mod_blocking, odbc}; -replace_module(mod_caps_odbc) -> {mod_caps, odbc}; -replace_module(mod_irc_odbc) -> {mod_irc, odbc}; -replace_module(mod_last_odbc) -> {mod_last, odbc}; -replace_module(mod_muc_odbc) -> {mod_muc, odbc}; -replace_module(mod_offline_odbc) -> {mod_offline, odbc}; -replace_module(mod_privacy_odbc) -> {mod_privacy, odbc}; -replace_module(mod_private_odbc) -> {mod_private, odbc}; -replace_module(mod_roster_odbc) -> {mod_roster, odbc}; -replace_module(mod_shared_roster_odbc) -> {mod_shared_roster, odbc}; -replace_module(mod_vcard_odbc) -> {mod_vcard, odbc}; -replace_module(mod_vcard_xupdate_odbc) -> {mod_vcard_xupdate, odbc}; -replace_module(Module) -> - case is_elixir_module(Module) of - true -> expand_elixir_module(Module); - false -> Module - end. - -replace_modules(Modules) -> lists:map( fun({Module, Opts}) -> case - replace_module(Module) of {NewModule, DBType} -> - emit_deprecation_warning(Module, NewModule, DBType), NewOpts = - [{db_type, DBType} | lists:keydelete(db_type, 1, Opts)], - {NewModule, transform_module_options(Module, NewOpts)}; NewModule - -> if Module /= NewModule -> emit_deprecation_warning(Module, - NewModule); true -> ok end, {NewModule, - transform_module_options(Module, Opts)} end end, Modules). - -%% Elixir module naming -%% ==================== - -%% If module name start with uppercase letter, this is an Elixir module: -is_elixir_module(Module) -> - case atom_to_list(Module) of - [H|_] when H >= 65, H =< 90 -> true; - _ ->false - end. - -%% We assume we know this is an elixir module -expand_elixir_module(Module) -> - case atom_to_list(Module) of - %% Module name already specified as an Elixir from Erlang module name - "Elixir." ++ _ -> Module; - %% if start with uppercase letter, this is an Elixir module: Append 'Elixir.' to module name. - ModuleString -> - list_to_atom("Elixir." ++ ModuleString) - end. - -strings_to_binary([]) -> - []; -strings_to_binary(L) when is_list(L) -> - case is_string(L) of +resolve_host_alias(Host) -> + case lists:member(Host, ejabberd_option:hosts()) of true -> - list_to_binary(L); + Host; false -> - strings_to_binary1(L) - end; -strings_to_binary({A, B, C, D}) when - is_integer(A), is_integer(B), is_integer(C), is_integer(D) -> - {A, B, C ,D}; -strings_to_binary(T) when is_tuple(T) -> - list_to_tuple(strings_to_binary1(tuple_to_list(T))); -strings_to_binary(X) -> - X. - -strings_to_binary1([El|L]) -> - [strings_to_binary(El)|strings_to_binary1(L)]; -strings_to_binary1([]) -> - []; -strings_to_binary1(T) -> - T. - -is_string([C|T]) when (C >= 0) and (C =< 255) -> - is_string(T); -is_string([]) -> - true; -is_string(_) -> - false. - -binary_to_strings(B) when is_binary(B) -> - binary_to_list(B); -binary_to_strings([H|T]) -> - [binary_to_strings(H)|binary_to_strings(T)]; -binary_to_strings(T) when is_tuple(T) -> - list_to_tuple(binary_to_strings(tuple_to_list(T))); -binary_to_strings(T) -> - T. - -format_term(Bin) when is_binary(Bin) -> - io_lib:format("\"~s\"", [Bin]); -format_term(S) when is_list(S), S /= [] -> - case lists:all(fun(C) -> (C>=0) and (C=<255) end, S) of - true -> - io_lib:format("\"~s\"", [S]); - false -> - io_lib:format("~p", [binary_to_strings(S)]) - end; -format_term(T) -> - io_lib:format("~p", [binary_to_strings(T)]). - -transform_terms(Terms) -> - %% We could check all ejabberd beams, but this - %% slows down start-up procedure :( - Mods = [mod_register, - mod_last, - ejabberd_s2s, - ejabberd_listener, - ejabberd_odbc_sup, - shaper, - ejabberd_s2s_out, - acl, - ejabberd_config], - collect_options(transform_terms(Mods, Terms)). - -transform_terms([Mod|Mods], Terms) -> - case catch Mod:transform_options(Terms) of - {'EXIT', _} = Err -> - ?ERROR_MSG("Failed to transform terms by ~p: ~p", [Mod, Err]), - transform_terms(Mods, Terms); - NewTerms -> - transform_terms(Mods, NewTerms) - end; -transform_terms([], NewTerms) -> - NewTerms. - -transform_module_options(Module, Opts) -> - Opts1 = gen_iq_handler:transform_module_options(Opts), - try - Module:transform_module_options(Opts1) - catch error:undef -> - Opts1 + resolve_host_alias2(Host) end. -compact(Cfg) -> - Opts = [{K, V} || #local_config{key = K, value = V} <- Cfg], - {GOpts, HOpts} = split_by_hosts(Opts), - [#local_config{key = {O, global}, value = V} || {O, V} <- GOpts] ++ - lists:flatmap( - fun({Host, OptVal}) -> - case lists:member(OptVal, GOpts) of - true -> - []; - false -> - [#local_config{key = {Opt, Host}, value = Val} - || {Opt, Val} <- OptVal] - end - end, lists:flatten(HOpts)). - -split_by_hosts(Opts) -> - Opts1 = orddict:to_list( - lists:foldl( - fun({{Opt, Host}, Val}, D) -> - orddict:append(Host, {Opt, Val}, D) - end, orddict:new(), Opts)), - case lists:keytake(global, 1, Opts1) of - {value, {global, GlobalOpts}, HostOpts} -> - {GlobalOpts, HostOpts}; - _ -> - {[], Opts1} +resolve_host_alias2(Host) -> + Result = + lists:filter(fun({Alias1, _Vhost}) -> is_glob_match(Host, Alias1) end, + ejabberd_option:hosts_alias()), + case Result of + [{_, Vhost} | _] when is_binary(Vhost) -> + ?DEBUG("(~p) Alias host '~s' resolved into vhost '~s'", [self(), Host, Vhost]), + Vhost; + [] -> + ?DEBUG("(~p) Request sent to host '~s', which isn't a vhost or an alias", + [self(), Host]), + Host end. -collect_options(Opts) -> - {D, InvalidOpts} = - lists:foldl( - fun({K, V}, {D, Os}) when is_list(V) -> - {orddict:append_list(K, V, D), Os}; - ({K, V}, {D, Os}) -> - {orddict:store(K, V, D), Os}; - (Opt, {D, Os}) -> - {D, [Opt|Os]} - end, {orddict:new(), []}, Opts), - InvalidOpts ++ orddict:to_list(D). - -transform_options(Opts) -> - Opts1 = lists:foldl(fun transform_options/2, [], Opts), - {HOpts, Opts2} = lists:mapfoldl( - fun({host_config, O}, Os) -> - {[O], Os}; - (O, Os) -> - {[], [O|Os]} - end, [], Opts1), - {AHOpts, Opts3} = lists:mapfoldl( - fun({append_host_config, O}, Os) -> - {[O], Os}; - (O, Os) -> - {[], [O|Os]} - end, [], Opts2), - HOpts1 = case collect_options(lists:flatten(HOpts)) of - [] -> - []; - HOs -> - [{host_config, - [{H, transform_terms(O)} || {H, O} <- HOs]}] - end, - AHOpts1 = case collect_options(lists:flatten(AHOpts)) of - [] -> - []; - AHOs -> - [{append_host_config, - [{H, transform_terms(O)} || {H, O} <- AHOs]}] - end, - HOpts1 ++ AHOpts1 ++ Opts3. - -transform_options({domain_certfile, Domain, CertFile}, Opts) -> - ?WARNING_MSG("Option 'domain_certfile' now should be defined " - "per virtual host or globally. The old format is " - "still supported but it is better to fix your config", []), - [{host_config, [{Domain, [{domain_certfile, CertFile}]}]}|Opts]; -transform_options(Opt, Opts) when Opt == override_global; - Opt == override_local; - Opt == override_acls -> - ?WARNING_MSG("Ignoring '~s' option which has no effect anymore", [Opt]), - Opts; -transform_options({host_config, Host, HOpts}, Opts) -> - {AddOpts, HOpts1} = - lists:mapfoldl( - fun({{add, Opt}, Val}, Os) -> - ?WARNING_MSG("Option 'add' is deprecated. " - "The option is still supported " - "but it is better to fix your config: " - "use 'append_host_config' instead.", []), - {[{Opt, Val}], Os}; - (O, Os) -> - {[], [O|Os]} - end, [], HOpts), - [{append_host_config, [{Host, lists:flatten(AddOpts)}]}, - {host_config, [{Host, HOpts1}]}|Opts]; -transform_options({define_macro, Macro, Val}, Opts) -> - [{define_macro, [{Macro, Val}]}|Opts]; -transform_options({include_config_file, _} = Opt, Opts) -> - [{include_config_file, [transform_include_option(Opt)]} | Opts]; -transform_options({include_config_file, _, _} = Opt, Opts) -> - [{include_config_file, [transform_include_option(Opt)]} | Opts]; -transform_options(Opt, Opts) -> - [Opt|Opts]. - --spec convert_table_to_binary(atom(), [atom()], atom(), - fun(), fun()) -> ok. - -convert_table_to_binary(Tab, Fields, Type, DetectFun, ConvertFun) -> - case is_table_still_list(Tab, DetectFun) of - true -> - ?INFO_MSG("Converting '~s' table from strings to binaries.", [Tab]), - TmpTab = list_to_atom(atom_to_list(Tab) ++ "_tmp_table"), - catch mnesia:delete_table(TmpTab), - case mnesia:create_table(TmpTab, - [{disc_only_copies, [node()]}, - {type, Type}, - {local_content, true}, - {record_name, Tab}, - {attributes, Fields}]) of - {atomic, ok} -> - mnesia:transform_table(Tab, ignore, Fields), - case mnesia:transaction( - fun() -> - mnesia:write_lock_table(TmpTab), - mnesia:foldl( - fun(R, _) -> - NewR = ConvertFun(R), - mnesia:dirty_write(TmpTab, NewR) - end, ok, Tab) - end) of - {atomic, ok} -> - mnesia:clear_table(Tab), - case mnesia:transaction( - fun() -> - mnesia:write_lock_table(Tab), - mnesia:foldl( - fun(R, _) -> - mnesia:dirty_write(R) - end, ok, TmpTab) - end) of - {atomic, ok} -> - mnesia:delete_table(TmpTab); - Err -> - report_and_stop(Tab, Err) - end; - Err -> - report_and_stop(Tab, Err) - end; - Err -> - report_and_stop(Tab, Err) - end; - false -> - ok - end. - -is_table_still_list(Tab, DetectFun) -> - is_table_still_list(Tab, DetectFun, mnesia:dirty_first(Tab)). - -is_table_still_list(_Tab, _DetectFun, '$end_of_table') -> - false; -is_table_still_list(Tab, DetectFun, Key) -> - Rs = mnesia:dirty_read(Tab, Key), - Res = lists:foldl(fun(_, true) -> - true; - (_, false) -> - false; - (R, _) -> - case DetectFun(R) of - '$next' -> - '$next'; - El -> - is_list(El) - end - end, '$next', Rs), - case Res of - true -> - true; - false -> +%% Copied from ejabberd-2.0.0/src/acl.erl +is_regexp_match(String, RegExp) -> + case ejabberd_regexp:run(String, RegExp) of + nomatch -> false; - '$next' -> - is_table_still_list(Tab, DetectFun, mnesia:dirty_next(Tab, Key)) + match -> + true; + {error, ErrDesc} -> + io:format("Wrong regexp ~p in ACL: ~p", [RegExp, ErrDesc]), + false end. -report_and_stop(Tab, Err) -> - ErrTxt = lists:flatten( - io_lib:format( - "Failed to convert '~s' table to binary: ~p", - [Tab, Err])), - ?CRITICAL_MSG(ErrTxt, []), - timer:sleep(1000), - halt(string:substr(ErrTxt, 1, 199)). +is_glob_match(String, <<"!", Glob/binary>>) -> + not is_regexp_match(String, ejabberd_regexp:sh_to_awk(Glob)); +is_glob_match(String, Glob) -> + is_regexp_match(String, ejabberd_regexp:sh_to_awk(Glob)). +%% @format-end -emit_deprecation_warning(Module, NewModule, DBType) -> - ?WARNING_MSG("Module ~s is deprecated, use ~s with 'db_type: ~s'" - " instead", [Module, NewModule, DBType]). +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +-spec path() -> binary(). +path() -> + unicode:characters_to_binary( + case get_env_config() of + {ok, Path} -> + Path; + undefined -> + case os:getenv("EJABBERD_CONFIG_PATH") of + false -> + "ejabberd.yml"; + Path -> + Path + end + end). -emit_deprecation_warning(Module, NewModule) -> - case is_elixir_module(NewModule) of - %% Do not emit deprecation warning for Elixir - true -> ok; - false -> - ?WARNING_MSG("Module ~s is deprecated, use ~s instead", - [Module, NewModule]) +-spec get_env_config() -> {ok, string()} | undefined. +get_env_config() -> + %% First case: the filename can be specified with: erl -config "/path/to/ejabberd.yml". + case application:get_env(ejabberd, config) of + R = {ok, _Path} -> R; + undefined -> + %% Second case for embbeding ejabberd in another app, for example for Elixir: + %% config :ejabberd, + %% file: "config/ejabberd.yml" + application:get_env(ejabberd, file) end. + +-spec create_tmp_config() -> ok. +create_tmp_config() -> + T = ets:new(options, [private]), + put(ejabberd_options, T), + ok. + +-spec get_tmp_config() -> ets:tid() | undefined. +get_tmp_config() -> + get(ejabberd_options). + +-spec delete_tmp_config() -> ok. +delete_tmp_config() -> + case get_tmp_config() of + undefined -> + ok; + T -> + erase(ejabberd_options), + ets:delete(T), + ok + end. + +-spec callback_modules(local | external | all) -> [module()]. +callback_modules(local) -> + [ejabberd_options]; +callback_modules(external) -> + lists:filter( + fun(M) -> + case code:ensure_loaded(M) of + {module, _} -> + erlang:function_exported(M, options, 0) + andalso erlang:function_exported(M, opt_type, 1); + {error, _} -> + false + end + end, beams(external)); +callback_modules(all) -> + misc:lists_uniq(callback_modules(local) ++ callback_modules(external)). + +-spec validators(module(), [atom()], [any()]) -> econf:validators(). +validators(Mod, Disallowed, DK) -> + Keywords = DK ++ get_predefined_keywords(global), + maps:from_list( + lists:filtermap( + fun(O) -> + case lists:member(O, Disallowed) of + true -> false; + false -> + Type = + try Mod:opt_type(O) + catch _:_ -> + ejabberd_options:opt_type(O) + end, + TypeProcessed = + econf:and_then( + fun(B) -> + replace_keywords(global, B, Keywords) + end, + Type), + {true, {O, TypeProcessed}} + end + end, proplists:get_keys(Mod:options()))). + +read_file(File) -> + read_file(File, [replace_macros, include_files, include_modules_configs]). + +read_file(File, Opts) -> + {Opts1, Opts2} = proplists:split(Opts, [replace_macros, include_files]), + Ret = case filename:extension(File) of + Ex when Ex == <<".yml">> orelse Ex == <<".yaml">> -> + Files = case proplists:get_bool(include_modules_configs, Opts2) of + true -> ext_mod:modules_configs(); + false -> [] + end, + lists:foreach( + fun(F) -> + ?INFO_MSG("Loading third-party configuration from ~ts", [F]) + end, Files), + read_yaml_files([File|Files], lists:flatten(Opts1)); + _ -> + read_erlang_file(File, lists:flatten(Opts1)) + end, + case Ret of + {ok, Y} -> + InstalledModules = maybe_install_contrib_modules(Y), + ValResult = validate(Y), + case InstalledModules of + [] -> ok; + _ -> spawn(fun() -> timer:sleep(5000), ?MODULE:reload() end) + end, + ValResult; + Err -> + Err + end. + +get_additional_macros() -> + MacroStrings = lists:foldl(fun([$E, $J, $A, $B, $B, $E, $R, $D, $_, $M, $A, $C, $R, $O, $_ | MacroString], Acc) -> + [parse_macro_string(MacroString) | Acc]; + (_, Acc) -> + Acc + end, + [], + os:getenv()), + {additional_macros, MacroStrings}. + +parse_macro_string(MacroString) -> + [NameString, ValueString] = string:split(MacroString, "="), + {ok, [ValueDecoded]} = fast_yaml:decode(ValueString), + {list_to_atom(NameString), ValueDecoded}. + +read_yaml_files(Files, Opts) -> + ParseOpts = [plain_as_atom, get_additional_macros() | lists:flatten(Opts)], + lists:foldl( + fun(File, {ok, Y1}) -> + case econf:parse(File, #{'_' => econf:any()}, ParseOpts) of + {ok, Y2} -> {ok, Y1 ++ Y2}; + Err -> Err + end; + (_, Err) -> + Err + end, {ok, []}, Files). + +read_erlang_file(File, _) -> + case ejabberd_old_config:read_file(File) of + {ok, Y} -> + econf:replace_macros(Y); + Err -> + Err + end. + +-spec maybe_install_contrib_modules(term()) -> [atom()]. +maybe_install_contrib_modules(Options) -> + case {lists:keysearch(allow_contrib_modules, 1, Options), + lists:keysearch(install_contrib_modules, 1, Options)} of + {Allow, {value, {_, InstallContribModules}}} + when (Allow == false) or + (Allow == {value, {allow_contrib_modules, true}}) -> + ext_mod:install_contrib_modules(InstallContribModules, Options); + _ -> + [] + end. + +-spec validate(term()) -> {ok, [{atom(), term()}]} | error_return(). +validate(Y1) -> + case pre_validate(Y1) of + {ok, Y2} -> + set_loglevel(proplists:get_value(loglevel, Y2, info)), + ejabberd_logger:set_modules_fully_logged(proplists:get_value(log_modules_fully, Y2, [])), + case ejabberd_config_transformer:map_reduce(Y2) of + {ok, Y3} -> + Hosts = proplists:get_value(hosts, Y3), + Version = proplists:get_value(version, Y3, version()), + DK = get_defined_keywords_yaml_config(Y3), + create_tmp_config(), + set_option(hosts, Hosts), + set_option(host, hd(Hosts)), + set_option(version, Version), + set_option(yaml_config, Y3), + {Validators, Required} = validators([], DK), + Validator = econf:options(Validators, + [{required, Required}, + unique]), + econf:validate(Validator, Y3); + Err -> + Err + end; + Err -> + Err + end. + +-spec pre_validate(term()) -> {ok, [{atom(), term()}]} | error_return(). +pre_validate(Y1) -> + econf:validate( + econf:and_then( + econf:options( + #{hosts => ejabberd_options:opt_type(hosts), + loglevel => ejabberd_options:opt_type(loglevel), + version => ejabberd_options:opt_type(version), + '_' => econf:any()}, + [{required, [hosts]}]), + fun econf:group_dups/1), Y1). + +-spec load_file(binary()) -> ok | error_return(). +load_file(File) -> + try + case read_file(File) of + {ok, Terms} -> + case set_host_config(Terms) of + {ok, Map} -> + T = get_tmp_config(), + Hosts = get_myhosts(), + apply_defaults(T, Hosts, Map), + case validate_modules(Hosts) of + {ok, ModOpts} -> + ets:insert(T, ModOpts), + set_option(host, hd(Hosts)), + commit(), + set_fqdn(); + Err -> + abort(Err) + end; + Err -> + abort(Err) + end; + Err -> + abort(Err) + end + catch + Class:Reason:Stack -> + {error, {exception, Class, Reason, Stack}} + end. + +-spec commit() -> ok. +commit() -> + T = get_tmp_config(), + NewOpts = ets:tab2list(T), + ets:insert(ejabberd_options, NewOpts), + delete_tmp_config(). + +-spec abort(error_return()) -> error_return(). +abort(Err) -> + delete_tmp_config(), + try ets:lookup_element(ejabberd_options, {loglevel, global}, 2) of + Level -> set_loglevel(Level) + catch _:badarg -> + ok + end, + Err. + +-spec set_host_config([{atom(), term()}]) -> {ok, host_config()} | error_return(). +set_host_config(Opts) -> + Map1 = lists:foldl( + fun({Opt, Val}, M) when Opt /= host_config, + Opt /= append_host_config -> + maps:put({Opt, global}, Val, M); + (_, M) -> + M + end, #{}, Opts), + HostOpts = proplists:get_value(host_config, Opts, []), + AppendHostOpts = proplists:get_value(append_host_config, Opts, []), + Map2 = lists:foldl( + fun({Host, Opts1}, M1) -> + lists:foldl( + fun({Opt, Val}, M2) -> + maps:put({Opt, Host}, Val, M2) + end, M1, Opts1) + end, Map1, HostOpts), + Map3 = lists:foldl( + fun(_, {error, _} = Err) -> + Err; + ({Host, Opts1}, M1) -> + lists:foldl( + fun(_, {error, _} = Err) -> + Err; + ({Opt, L1}, M2) when is_list(L1) -> + L2 = try maps:get({Opt, Host}, M2) + catch _:{badkey, _} -> + maps:get({Opt, global}, M2, []) + end, + L3 = L2 ++ L1, + maps:put({Opt, Host}, L3, M2); + ({Opt, _}, _) -> + {error, {merge_conflict, Opt, Host}} + end, M1, Opts1) + end, Map2, AppendHostOpts), + case Map3 of + {error, _} -> Map3; + _ -> {ok, Map3} + end. + +-spec apply_defaults(ets:tid(), [binary()], host_config()) -> ok. +apply_defaults(Tab, Hosts, Map) -> + Defaults1 = defaults(), + apply_defaults(Tab, global, Map, Defaults1), + {_, Defaults2} = proplists:split(Defaults1, globals()), + lists:foreach( + fun(Host) -> + set_option(host, Host), + apply_defaults(Tab, Host, Map, Defaults2) + end, Hosts). + +-spec apply_defaults(ets:tid(), global | binary(), + host_config(), + [atom() | {atom(), term()}]) -> ok. +apply_defaults(Tab, Host, Map, Defaults) -> + lists:foreach( + fun({Opt, Default}) -> + try maps:get({Opt, Host}, Map) of + Val -> + ets:insert(Tab, {{Opt, Host}, Val}) + catch _:{badkey, _} when Host == global -> + Default1 = compute_default(Default, Host), + ets:insert(Tab, {{Opt, Host}, Default1}); + _:{badkey, _} -> + try maps:get({Opt, global}, Map) of + V -> ets:insert(Tab, {{Opt, Host}, V}) + catch _:{badkey, _} -> + Default1 = compute_default(Default, Host), + ets:insert(Tab, {{Opt, Host}, Default1}) + end + end; + (Opt) when Host == global -> + Val = maps:get({Opt, Host}, Map), + ets:insert(Tab, {{Opt, Host}, Val}); + (_) -> + ok + end, Defaults). + +-spec defaults() -> [atom() | {atom(), term()}]. +defaults() -> + lists:foldl( + fun(Mod, Acc) -> + lists:foldl( + fun({Opt, Val}, Acc1) -> + lists:keystore(Opt, 1, Acc1, {Opt, Val}); + (Opt, Acc1) -> + case lists:member(Opt, Acc1) of + true -> Acc1; + false -> [Opt|Acc1] + end + end, Acc, Mod:options()) + end, ejabberd_options:options(), callback_modules(external)). + +-spec globals() -> [atom()]. +globals() -> + lists:usort( + lists:flatmap( + fun(Mod) -> + case erlang:function_exported(Mod, globals, 0) of + true -> Mod:globals(); + false -> [] + end + end, callback_modules(all))). + +%% The module validator depends on virtual host, so we have to +%% validate modules in this separate function. +-spec validate_modules([binary()]) -> {ok, list()} | error_return(). +validate_modules(Hosts) -> + lists:foldl( + fun(Host, {ok, Acc}) -> + set_option(host, Host), + ModOpts = get_option({modules, Host}), + case gen_mod:validate(Host, ModOpts) of + {ok, ModOpts1} -> + {ok, [{{modules, Host}, ModOpts1}|Acc]}; + Err -> + Err + end; + (_, Err) -> + Err + end, {ok, []}, Hosts). + +-spec delete_host_options([binary()]) -> ok. +delete_host_options(Hosts) -> + lists:foreach( + fun(Host) -> + ets:match_delete(ejabberd_options, {{'_', Host}, '_'}) + end, Hosts). + +-spec compute_default(fun((global | binary()) -> T) | T, global | binary()) -> T. +compute_default(F, Host) when is_function(F, 1) -> + F(Host); +compute_default(Val, _) -> + Val. + +-spec set_fqdn() -> ok. +set_fqdn() -> + FQDNs = get_option(fqdn), + xmpp:set_config([{fqdn, FQDNs}]). + +-spec set_shared_key() -> ok. +set_shared_key() -> + Key = case erlang:get_cookie() of + nocookie -> + str:sha(p1_rand:get_string()); + Cookie -> + str:sha(erlang:atom_to_binary(Cookie, latin1)) + end, + set_option(shared_key, Key). + +-spec set_node_start(integer()) -> ok. +set_node_start(UnixTime) -> + set_option(node_start, UnixTime). + +-spec set_loglevel(logger:level()) -> ok. +set_loglevel(Level) -> + ejabberd_logger:set(Level). diff --git a/src/ejabberd_config_transformer.erl b/src/ejabberd_config_transformer.erl new file mode 100644 index 000000000..1aed7c6a8 --- /dev/null +++ b/src/ejabberd_config_transformer.erl @@ -0,0 +1,644 @@ +%%%---------------------------------------------------------------------- +%%% ejabberd, Copyright (C) 2002-2025 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(ejabberd_config_transformer). + +%% API +-export([map_reduce/1]). + +-include("logger.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +map_reduce(Y) -> + F = + fun(Y1) -> + Y2 = (validator())(Y1), + Y3 = transform(Y2), + case application:get_env(ejabberd, custom_config_transformer) of + {ok, TransMod} when is_atom(TransMod) -> + TransMod:transform(Y3); + _ -> + Y3 + end + end, + econf:validate(F, Y). + +%%%=================================================================== +%%% Transformer +%%%=================================================================== +transform(Y) -> + {Y1, Acc1} = transform(global, Y, #{}), + {Y2, Acc2} = update(Y1, Acc1), + filter(global, Y2, Acc2). + +transform(Host, Y, Acc) -> + filtermapfoldr( + fun({Opt, HostOpts}, Acc1) when (Opt == host_config orelse + Opt == append_host_config) + andalso Host == global -> + case filtermapfoldr( + fun({Host1, Opts}, Acc2) -> + case transform(Host1, Opts, Acc2) of + {[], Acc3} -> + {false, Acc3}; + {Opts1, Acc3} -> + {{true, {Host1, Opts1}}, Acc3} + end + end, Acc1, HostOpts) of + {[], Acc4} -> + {false, Acc4}; + {HostOpts1, Acc4} -> + {{true, {Opt, HostOpts1}}, Acc4} + end; + ({Opt, Val}, Acc1) -> + transform(Host, Opt, Val, Acc1) + end, Acc, Y). + +transform(Host, modules, ModOpts, Acc) -> + {ModOpts1, Acc2} = + lists:mapfoldr( + fun({Mod, Opts}, Acc1) -> + Opts1 = transform_module_options(Opts), + transform_module(Host, Mod, Opts1, Acc1) + end, Acc, ModOpts), + {{true, {modules, ModOpts1}}, Acc2}; +transform(global, listen, Listeners, Acc) -> + {Listeners1, Acc2} = + lists:mapfoldr( + fun(Opts, Acc1) -> + transform_listener(Opts, Acc1) + end, Acc, Listeners), + {{true, {listen, Listeners1}}, Acc2}; +transform(_Host, Opt, CertFile, Acc) when (Opt == domain_certfile) orelse + (Opt == c2s_certfile) orelse + (Opt == s2s_certfile) -> + ?WARNING_MSG("Option '~ts' is deprecated and was automatically " + "appended to 'certfiles' option. ~ts", + [Opt, adjust_hint()]), + CertFiles = maps:get(certfiles, Acc, []), + Acc1 = maps:put(certfiles, CertFiles ++ [CertFile], Acc), + {false, Acc1}; +transform(_Host, certfiles, CertFiles1, Acc) -> + CertFiles2 = maps:get(certfiles, Acc, []), + Acc1 = maps:put(certfiles, CertFiles1 ++ CertFiles2, Acc), + {true, Acc1}; +transform(_Host, acme, ACME, Acc) -> + ACME1 = lists:map( + fun({ca_url, URL} = Opt) -> + case misc:uri_parse(URL) of + {ok, _, _, "acme-v01.api.letsencrypt.org", _, _, _} -> + NewURL = ejabberd_acme:default_directory_url(), + ?WARNING_MSG("ACME directory URL ~ts defined in " + "option acme->ca_url is deprecated " + "and was automatically replaced " + "with ~ts. ~ts", + [URL, NewURL, adjust_hint()]), + {ca_url, NewURL}; + _ -> + Opt + end; + (Opt) -> + Opt + end, ACME), + {{true, {acme, ACME1}}, Acc}; +transform(Host, s2s_use_starttls, required_trusted, Acc) -> + ?WARNING_MSG("The value 'required_trusted' of option " + "'s2s_use_starttls' is deprecated and was " + "automatically replaced with value 'required'. " + "The module 'mod_s2s_dialback' has also " + "been automatically removed from the configuration. ~ts", + [adjust_hint()]), + Hosts = maps:get(remove_s2s_dialback, Acc, []), + Acc1 = maps:put(remove_s2s_dialback, [Host|Hosts], Acc), + {{true, {s2s_use_starttls, required}}, Acc1}; +transform(Host, define_macro, Macro, Acc) when is_binary(Host) -> + ?WARNING_MSG("The option 'define_macro' is not supported inside 'host_config'. " + "Consequently those macro definitions for host '~ts' are unused: ~ts", + [Host, io_lib:format("~p", [Macro])]), + {true, Acc}; +transform(_Host, _Opt, _Val, Acc) -> + {true, Acc}. + +update(Y, Acc) -> + set_certfiles(Y, Acc). + +filter(Host, Y, Acc) -> + lists:filtermap( + fun({Opt, HostOpts}) when (Opt == host_config orelse + Opt == append_host_config) + andalso Host == global -> + HostOpts1 = lists:map( + fun({Host1, Opts1}) -> + {Host1, filter(Host1, Opts1, Acc)} + end, HostOpts), + {true, {Opt, HostOpts1}}; + ({Opt, Val}) -> + filter(Host, Opt, Val, Acc) + end, Y). + +filter(_Host, log_rotate_date, _, _) -> + warn_removed_option(log_rotate_date), + false; +filter(_Host, log_rate_limit, _, _) -> + warn_removed_option(log_rate_limit), + false; +filter(_Host, ca_path, _, _) -> + warn_removed_option(ca_path, ca_file), + false; +filter(_Host, iqdisc, _, _) -> + warn_removed_option(iqdisc), + false; +filter(_Host, access, _, _) -> + warn_removed_option(access, access_rules), + false; +filter(_Host, commands, _, _) -> + warn_removed_option(commands, api_permissions), + false; +filter(_Host, ejabberdctl_access_commands, _, _) -> + warn_removed_option(ejabberdctl_access_commands, api_permissions), + false; +filter(_Host, commands_admin_access, _, _) -> + warn_removed_option(commands_admin_access, api_permissions), + false; +filter(_Host, ldap_group_cache_size, _, _) -> + warn_removed_option(ldap_group_cache_size, cache_size), + false; +filter(_Host, ldap_user_cache_size, _, _) -> + warn_removed_option(ldap_user_cache_size, cache_size), + false; +filter(_Host, ldap_group_cache_validity, _, _) -> + warn_removed_option(ldap_group_cache_validity, cache_life_time), + false; +filter(_Host, ldap_user_cache_validity, _, _) -> + warn_removed_option(ldap_user_cache_validity, cache_life_time), + false; +filter(_Host, ldap_local_filter, _, _) -> + warn_removed_option(ldap_local_filter), + false; +filter(_Host, deref_aliases, Val, _) -> + warn_replaced_option(deref_aliases, ldap_deref_aliases), + {true, {ldap_deref_aliases, Val}}; +filter(_Host, default_db, internal, _) -> + {true, {default_db, mnesia}}; +filter(_Host, default_db, odbc, _) -> + {true, {default_db, sql}}; +filter(_Host, auth_method, Ms, _) -> + Ms1 = lists:map( + fun(internal) -> mnesia; + (odbc) -> sql; + (M) -> M + end, Ms), + {true, {auth_method, Ms1}}; +filter(_Host, default_ram_db, internal, _) -> + {true, {default_ram_db, mnesia}}; +filter(_Host, default_ram_db, odbc, _) -> + {true, {default_ram_db, sql}}; +filter(_Host, extauth_cache, _, _) -> + ?WARNING_MSG("Option 'extauth_cache' is deprecated " + "and has no effect, use authentication " + "or global cache configuration options: " + "auth_use_cache, auth_cache_life_time, " + "use_cache, cache_life_time, and so on", []), + false; +filter(_Host, extauth_instances, Val, _) -> + warn_replaced_option(extauth_instances, extauth_pool_size), + {true, {extauth_pool_size, Val}}; +filter(_Host, Opt, Val, _) when Opt == outgoing_s2s_timeout; + Opt == s2s_dns_timeout -> + warn_huge_timeout(Opt, Val), + true; +filter(_Host, captcha_host, _, _) -> + warn_deprecated_option(captcha_host, captcha_url), + true; +filter(_Host, route_subdomains, _, _) -> + warn_removed_option(route_subdomains, s2s_access), + false; +filter(_Host, auth_password_types_hidden_in_scram1, Val, _) -> + {true, {auth_password_types_hidden_in_sasl1, Val}}; +filter(Host, modules, ModOpts, State) -> + NoDialbackHosts = maps:get(remove_s2s_dialback, State, []), + ModOpts1 = lists:filter( + fun({mod_s2s_dialback, _}) -> + not lists:member(Host, NoDialbackHosts); + ({mod_echo, _}) -> + warn_removed_module(mod_echo), + false; + (_) -> + true + end, ModOpts), + {true, {modules, ModOpts1}}; +filter(_, _, _, _) -> + true. + +%%%=================================================================== +%%% Listener transformers +%%%=================================================================== +transform_listener(Opts, Acc) -> + Opts1 = transform_request_handlers(Opts), + Opts2 = transform_turn_ip(Opts1), + Opts3 = remove_inet_options(Opts2), + collect_listener_certfiles(Opts3, Acc). + +transform_request_handlers(Opts) -> + case lists:keyfind(module, 1, Opts) of + {_, ejabberd_http} -> + replace_request_handlers(Opts); + {_, ejabberd_xmlrpc} -> + remove_xmlrpc_access_commands(Opts); + _ -> + Opts + end. + +transform_turn_ip(Opts) -> + case lists:keyfind(module, 1, Opts) of + {_, ejabberd_stun} -> + replace_turn_ip(Opts); + _ -> + Opts + end. + +replace_request_handlers(Opts) -> + Handlers = proplists:get_value(request_handlers, Opts, []), + Handlers1 = + lists:foldl( + fun({captcha, IsEnabled}, Acc) -> + Handler = {<<"/captcha">>, ejabberd_captcha}, + warn_replaced_handler(captcha, Handler, IsEnabled), + [Handler|Acc]; + ({register, IsEnabled}, Acc) -> + Handler = {<<"/register">>, mod_register_web}, + warn_replaced_handler(register, Handler, IsEnabled), + [Handler|Acc]; + ({web_admin, IsEnabled}, Acc) -> + Handler = {<<"/admin">>, ejabberd_web_admin}, + warn_replaced_handler(web_admin, Handler, IsEnabled), + [Handler|Acc]; + ({http_bind, IsEnabled}, Acc) -> + Handler = {<<"/bosh">>, mod_bosh}, + warn_replaced_handler(http_bind, Handler, IsEnabled), + [Handler|Acc]; + ({xmlrpc, IsEnabled}, Acc) -> + Handler = {<<"/">>, ejabberd_xmlrpc}, + warn_replaced_handler(xmlrpc, Handler, IsEnabled), + Acc ++ [Handler]; + (_, Acc) -> + Acc + end, Handlers, Opts), + Handlers2 = lists:map( + fun({Path, mod_http_bind}) -> + warn_replaced_module(mod_http_bind, mod_bosh), + {Path, mod_bosh}; + (PathMod) -> + PathMod + end, Handlers1), + Opts1 = lists:filtermap( + fun({captcha, _}) -> false; + ({register, _}) -> false; + ({web_admin, _}) -> false; + ({http_bind, _}) -> false; + ({xmlrpc, _}) -> false; + ({http_poll, _}) -> + ?WARNING_MSG("Listening option 'http_poll' is " + "ignored: HTTP Polling support was " + "removed in ejabberd 15.04. ~ts", + [adjust_hint()]), + false; + ({request_handlers, _}) -> + false; + (_) -> true + end, Opts), + case Handlers2 of + [] -> Opts1; + _ -> [{request_handlers, Handlers2}|Opts1] + end. + +remove_xmlrpc_access_commands(Opts) -> + lists:filter( + fun({access_commands, _}) -> + warn_removed_option(access_commands, api_permissions), + false; + (_) -> + true + end, Opts). + +replace_turn_ip(Opts) -> + lists:filtermap( + fun({turn_ip, Val}) -> + warn_replaced_option(turn_ip, turn_ipv4_address), + {true, {turn_ipv4_address, Val}}; + (_) -> + true + end, Opts). + +remove_inet_options(Opts) -> + lists:filter( + fun({Opt, _}) when Opt == inet; Opt == inet6 -> + warn_removed_option(Opt, ip), + false; + (_) -> + true + end, Opts). + +collect_listener_certfiles(Opts, Acc) -> + Mod = proplists:get_value(module, Opts), + if Mod == ejabberd_http; + Mod == ejabberd_c2s; + Mod == ejabberd_s2s_in -> + case lists:keyfind(certfile, 1, Opts) of + {_, CertFile} -> + ?WARNING_MSG("Listening option 'certfile' of module ~ts " + "is deprecated and was automatically " + "appended to global 'certfiles' option. ~ts", + [Mod, adjust_hint()]), + CertFiles = maps:get(certfiles, Acc, []), + {proplists:delete(certfile, Opts), + maps:put(certfiles, [CertFile|CertFiles], Acc)}; + false -> + {Opts, Acc} + end; + true -> + {Opts, Acc} + end. + +%%%=================================================================== +%%% Module transformers +%%% NOTE: transform_module_options/1 is called before transform_module/4 +%%%=================================================================== +transform_module_options(Opts) -> + lists:filtermap( + fun({Opt, internal}) when Opt == db_type; + Opt == ram_db_type -> + {true, {Opt, mnesia}}; + ({Opt, odbc}) when Opt == db_type; + Opt == ram_db_type -> + {true, {Opt, sql}}; + ({deref_aliases, Val}) -> + warn_replaced_option(deref_aliases, ldap_deref_aliases), + {true, {ldap_deref_aliases, Val}}; + ({ldap_group_cache_size, _}) -> + warn_removed_option(ldap_group_cache_size, cache_size), + false; + ({ldap_user_cache_size, _}) -> + warn_removed_option(ldap_user_cache_size, cache_size), + false; + ({ldap_group_cache_validity, _}) -> + warn_removed_option(ldap_group_cache_validity, cache_life_time), + false; + ({ldap_user_cache_validity, _}) -> + warn_removed_option(ldap_user_cache_validity, cache_life_time), + false; + ({iqdisc, _}) -> + warn_removed_option(iqdisc), + false; + (_) -> + true + end, Opts). + +transform_module(Host, mod_http_bind, Opts, Acc) -> + warn_replaced_module(mod_http_bind, mod_bosh), + transform_module(Host, mod_bosh, Opts, Acc); +transform_module(Host, mod_vcard_xupdate_odbc, Opts, Acc) -> + warn_replaced_module(mod_vcard_xupdate_odbc, mod_vcard_xupdate), + transform_module(Host, mod_vcard_xupdate, Opts, Acc); +transform_module(Host, mod_vcard_ldap, Opts, Acc) -> + warn_replaced_module(mod_vcard_ldap, mod_vcard, ldap), + transform_module(Host, mod_vcard, [{db_type, ldap}|Opts], Acc); +transform_module(Host, M, Opts, Acc) when (M == mod_announce_odbc orelse + M == mod_blocking_odbc orelse + M == mod_caps_odbc orelse + M == mod_last_odbc orelse + M == mod_muc_odbc orelse + M == mod_offline_odbc orelse + M == mod_privacy_odbc orelse + M == mod_private_odbc orelse + M == mod_pubsub_odbc orelse + M == mod_roster_odbc orelse + M == mod_shared_roster_odbc orelse + M == mod_vcard_odbc) -> + M1 = strip_odbc_suffix(M), + warn_replaced_module(M, M1, sql), + transform_module(Host, M1, [{db_type, sql}|Opts], Acc); +transform_module(_Host, mod_blocking, Opts, Acc) -> + Opts1 = lists:filter( + fun({db_type, _}) -> + warn_removed_module_option(db_type, mod_blocking), + false; + (_) -> + true + end, Opts), + {{mod_blocking, Opts1}, Acc}; +transform_module(_Host, mod_carboncopy, Opts, Acc) -> + Opts1 = lists:filter( + fun({Opt, _}) when Opt == ram_db_type; + Opt == use_cache; + Opt == cache_size; + Opt == cache_missed; + Opt == cache_life_time -> + warn_removed_module_option(Opt, mod_carboncopy), + false; + (_) -> + true + end, Opts), + {{mod_carboncopy, Opts1}, Acc}; +transform_module(_Host, mod_http_api, Opts, Acc) -> + Opts1 = lists:filter( + fun({admin_ip_access, _}) -> + warn_removed_option(admin_ip_access, api_permissions), + false; + (_) -> + true + end, Opts), + {{mod_http_api, Opts1}, Acc}; +transform_module(_Host, mod_http_upload, Opts, Acc) -> + Opts1 = lists:filter( + fun({service_url, _}) -> + warn_deprecated_option(service_url, external_secret), + true; + (_) -> + true + end, Opts), + {{mod_http_upload, Opts1}, Acc}; +transform_module(_Host, mod_pubsub, Opts, Acc) -> + Opts1 = lists:map( + fun({plugins, Plugins}) -> + {plugins, + lists:filter( + fun(Plugin) -> + case lists:member( + Plugin, + [<<"buddy">>, <<"club">>, <<"dag">>, + <<"dispatch">>, <<"hometree">>, <<"mb">>, + <<"mix">>, <<"online">>, <<"private">>, + <<"public">>]) of + true -> + ?WARNING_MSG( + "Plugin '~ts' of mod_pubsub is not " + "supported anymore and has been " + "automatically removed from 'plugins' " + "option. ~ts", + [Plugin, adjust_hint()]), + false; + false -> + true + end + end, Plugins)}; + (Opt) -> + Opt + end, Opts), + {{mod_pubsub, Opts1}, Acc}; +transform_module(_Host, Mod, Opts, Acc) -> + {{Mod, Opts}, Acc}. + +strip_odbc_suffix(M) -> + [_|T] = lists:reverse(string:tokens(atom_to_list(M), "_")), + list_to_atom(string:join(lists:reverse(T), "_")). + +%%%=================================================================== +%%% Aux +%%%=================================================================== +filtermapfoldr(Fun, Init, List) -> + lists:foldr( + fun(X, {Ret, Acc}) -> + case Fun(X, Acc) of + {true, Acc1} -> {[X|Ret], Acc1}; + {{true, X1}, Acc1} -> {[X1|Ret], Acc1}; + {false, Acc1} -> {Ret, Acc1} + end + end, {[], Init}, List). + +set_certfiles(Y, #{certfiles := CertFiles} = Acc) -> + {lists:keystore(certfiles, 1, Y, {certfiles, CertFiles}), Acc}; +set_certfiles(Y, Acc) -> + {Y, Acc}. + +%%%=================================================================== +%%% Warnings +%%%=================================================================== +warn_replaced_module(From, To) -> + ?WARNING_MSG("Module ~ts is deprecated and was automatically " + "replaced by ~ts. ~ts", + [From, To, adjust_hint()]). + +warn_replaced_module(From, To, Type) -> + ?WARNING_MSG("Module ~ts is deprecated and was automatically " + "replaced by ~ts with db_type: ~ts. ~ts", + [From, To, Type, adjust_hint()]). + +warn_removed_module(Mod) -> + ?WARNING_MSG("Module ~ts is deprecated and was automatically " + "removed from the configuration. ~ts", [Mod, adjust_hint()]). + +warn_replaced_handler(Opt, {Path, Module}, false) -> + ?WARNING_MSG("Listening option '~ts' is deprecated, " + "please use instead the " + "HTTP request handler: \"~ts\" -> ~ts. ~ts", + [Opt, Path, Module, adjust_hint()]); +warn_replaced_handler(Opt, {Path, Module}, true) -> + ?WARNING_MSG("Listening option '~ts' is deprecated " + "and was automatically replaced by " + "HTTP request handler: \"~ts\" -> ~ts. ~ts", + [Opt, Path, Module, adjust_hint()]). + +warn_deprecated_option(OldOpt, NewOpt) -> + ?WARNING_MSG("Option '~ts' is deprecated. Use option '~ts' instead.", + [OldOpt, NewOpt]). + +warn_replaced_option(OldOpt, NewOpt) -> + ?WARNING_MSG("Option '~ts' is deprecated and was automatically " + "replaced by '~ts'. ~ts", + [OldOpt, NewOpt, adjust_hint()]). + +warn_removed_option(Opt) -> + ?WARNING_MSG("Option '~ts' is deprecated and has no effect anymore. " + "Please remove it from the configuration.", [Opt]). + +warn_removed_option(OldOpt, NewOpt) -> + ?WARNING_MSG("Option '~ts' is deprecated and has no effect anymore. " + "Use option '~ts' instead.", [OldOpt, NewOpt]). + +warn_removed_module_option(Opt, Mod) -> + ?WARNING_MSG("Option '~ts' of module ~ts is deprecated " + "and has no effect anymore. ~ts", + [Opt, Mod, adjust_hint()]). + +warn_huge_timeout(Opt, T) when is_integer(T), T >= 1000 -> + ?WARNING_MSG("Value '~B' of option '~ts' is too big, " + "are you sure you have set seconds?", + [T, Opt]); +warn_huge_timeout(_, _) -> + ok. + +adjust_hint() -> + "Please adjust your configuration file accordingly. " + "Hint: run `ejabberdctl dump-config` command to view current " + "configuration as it is seen by ejabberd.". + +%%%=================================================================== +%%% Very raw validator: just to make sure we get properly typed terms +%%% Expand it if you need to transform more options, but don't +%%% abuse complex types: simple and composite types are preferred +%%%=================================================================== +validator() -> + Validators = + #{s2s_use_starttls => econf:atom(), + certfiles => econf:list(econf:any()), + c2s_certfile => econf:binary(), + s2s_certfile => econf:binary(), + domain_certfile => econf:binary(), + default_db => econf:atom(), + default_ram_db => econf:atom(), + auth_method => econf:list_or_single(econf:atom()), + acme => econf:options( + #{ca_url => econf:binary(), + '_' => econf:any()}, + [unique]), + listen => + econf:list( + econf:options( + #{captcha => econf:bool(), + register => econf:bool(), + web_admin => econf:bool(), + http_bind => econf:bool(), + http_poll => econf:bool(), + xmlrpc => econf:bool(), + module => econf:atom(), + certfile => econf:binary(), + request_handlers => + econf:map(econf:binary(), econf:atom()), + '_' => econf:any()}, + [])), + modules => + econf:options( + #{'_' => + econf:options( + #{db_type => econf:atom(), + plugins => econf:list(econf:binary()), + '_' => econf:any()}, + [])}, + []), + '_' => econf:any()}, + econf:options( + Validators#{host_config => + econf:map(econf:binary(), + econf:options(Validators, [])), + append_host_config => + econf:map(econf:binary(), + econf:options(Validators, []))}, + []). diff --git a/src/ejabberd_ctl.erl b/src/ejabberd_ctl.erl index 6ab383b12..075854e40 100644 --- a/src/ejabberd_ctl.erl +++ b/src/ejabberd_ctl.erl @@ -5,7 +5,7 @@ %%% Created : 11 Jan 2004 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -23,205 +23,246 @@ %%% %%%---------------------------------------------------------------------- -%%% @headerfile "ejabberd_ctl.hrl" - -%%% @doc Management of ejabberdctl commands and frontend to ejabberd commands. -%%% -%%% An ejabberdctl command is an abstract function identified by a -%%% name, with a defined number of calling arguments, that can be -%%% defined in any Erlang module and executed using ejabberdctl -%%% administration script. -%%% -%%% Note: strings cannot have blankspaces -%%% -%%% Does not support commands that have arguments with ctypes: list, tuple -%%% -%%% TODO: Update the guide -%%% TODO: Mention this in the release notes -%%% Note: the commands with several words use now the underline: _ -%%% It is still possible to call the commands with dash: - -%%% but this is deprecated, and may be removed in a future version. - - -module(ejabberd_ctl). + +-behaviour(gen_server). -author('alexey@process-one.net'). --export([start/0, - init/0, - process/1, - process2/2, - register_commands/3, - unregister_commands/3]). +-export([start/0, start_link/0, process/1, process/2, process2/2]). +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). +-export([get_commands_spec/0, format_arg/2, + get_usage_command/4]). -include("ejabberd_ctl.hrl"). -include("ejabberd_commands.hrl"). --include("ejabberd.hrl"). +-include("ejabberd_http.hrl"). -include("logger.hrl"). +-define(DEFAULT_VERSION, 1000000). + +-record(state, {}). + %%----------------------------- %% Module %%----------------------------- start() -> - case init:get_plain_arguments() of - [SNode | Args] -> - SNode1 = case string:tokens(SNode, "@") of - [_Node, _Server] -> - SNode; - _ -> - case net_kernel:longnames() of - true -> - lists:flatten([SNode, "@", inet_db:gethostname(), - ".", inet_db:res_option(domain)]); - false -> - lists:flatten([SNode, "@", inet_db:gethostname()]); - _ -> - SNode - end - end, - Node = list_to_atom(SNode1), - Status = case rpc:call(Node, ?MODULE, process, [Args]) of - {badrpc, Reason} -> - print("Failed RPC connection to the node ~p: ~p~n", - [Node, Reason]), - %% TODO: show minimal start help - ?STATUS_BADRPC; - S -> - S - end, - halt(Status); - _ -> - print_usage(), - halt(?STATUS_USAGE) - end. + disable_logging(), + [SNode, Timeout, Args] = case init:get_plain_arguments() of + [SNode2, "--no-timeout" | Args2] -> + [SNode2, infinity, Args2]; + [SNode3 | Args3] -> + [SNode3, 60000, Args3]; + _ -> + print_usage(?DEFAULT_VERSION), + halt(?STATUS_USAGE) + end, + SNode1 = case string:tokens(SNode, "@") of + [_Node, _Server] -> + SNode; + _ -> + case net_kernel:longnames() of + true -> + lists:flatten([SNode, "@", inet_db:gethostname(), + ".", inet_db:res_option(domain)]); + false -> + lists:flatten([SNode, "@", inet_db:gethostname()]); + _ -> + SNode + end + end, + Node = list_to_atom(SNode1), + Status = case ejabberd_cluster:call(Node, ?MODULE, process, [Args], Timeout) of + {badrpc, Reason} -> + print("Failed RPC connection to the node ~p: ~p~n", + [Node, Reason]), + %% TODO: show minimal start help + ?STATUS_BADRPC; + {invalid_version, V} -> + print("Invalid API version number: ~p~n", [V]), + ?STATUS_ERROR; + S -> + S + end, + halt(Status). -init() -> - ets:new(ejabberd_ctl_cmds, [named_table, set, public]), - ets:new(ejabberd_ctl_host_cmds, [named_table, set, public]). +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). +init([]) -> + ejabberd_commands:register_commands(?MODULE, get_commands_spec()), + {ok, #state{}}. -%%----------------------------- -%% ejabberdctl Command managment -%%----------------------------- +handle_call(Request, From, State) -> + ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), + {noreply, State}. -register_commands(CmdDescs, Module, Function) -> - ets:insert(ejabberd_ctl_cmds, CmdDescs), - ejabberd_hooks:add(ejabberd_ctl_process, - Module, Function, 50), - ok. - -unregister_commands(CmdDescs, Module, Function) -> - lists:foreach(fun(CmdDesc) -> - ets:delete_object(ejabberd_ctl_cmds, CmdDesc) - end, CmdDescs), - ejabberd_hooks:delete(ejabberd_ctl_process, - Module, Function, 50), +handle_cast(Msg, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Msg]), + {noreply, State}. + +handle_info(Info, State) -> + ?WARNING_MSG("Unexpected info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, _State) -> + ejabberd_commands:unregister_commands(get_commands_spec()), ok. +code_change(_OldVsn, State, _Extra) -> + {ok, State}. %%----------------------------- -%% Process +%% Process http +%%----------------------------- + +-spec process_http([binary()], tuple()) -> {non_neg_integer(), [{binary(), binary()}], string()}. + +process_http([_Call], #request{data = Data, path = [<<"ctl">> | _]}) -> + Args = [binary_to_list(E) || E <- misc:json_decode(Data)], + process_http2(Args, ?DEFAULT_VERSION). + +process_http2(["--version", Arg | Args], _) -> + Version = + try + list_to_integer(Arg) + catch _:_ -> + throw({invalid_version, Arg}) + end, + process_http2(Args, Version); + +process_http2(Args, Version) -> + {String, Code} = process2(Args, [], Version), + String2 = case String of + [] -> String; + _ -> [String, "\n"] + end, + {200, [{<<"status-code">>, integer_to_binary(Code)}], String2}. + +%%----------------------------- +%% Process command line %%----------------------------- -spec process([string()]) -> non_neg_integer(). +process(Args) -> + process(Args, ?DEFAULT_VERSION). + +-spec process([string() | binary()], non_neg_integer() | tuple()) -> non_neg_integer(). + +process([Call], Request) when is_binary(Call) and is_record(Request, request) -> + process_http([Call], Request); %% The commands status, stop and restart are defined here to ensure %% they are usable even if ejabberd is completely stopped. -process(["status"]) -> +process(["status"], _Version) -> {InternalStatus, ProvidedStatus} = init:get_status(), print("The node ~p is ~p with status: ~p~n", [node(), InternalStatus, ProvidedStatus]), - case lists:keysearch(ejabberd, 1, application:which_applications()) of + case lists:keymember(ejabberd, 1, application:which_applications()) of false -> EjabberdLogPath = ejabberd_logger:get_log_path(), print("ejabberd is not running in that node~n" - "Check for error messages: ~s~n" + "Check for error messages: ~ts~n" "or other files in that directory.~n", [EjabberdLogPath]), ?STATUS_ERROR; - {value, {_, _, Version}} -> - print("ejabberd ~s is running in that node~n", [Version]), + true -> + print("ejabberd ~ts is running in that node~n", [ejabberd_option:version()]), ?STATUS_SUCCESS end; -process(["stop"]) -> - %%ejabberd_cover:stop(), - init:stop(), - ?STATUS_SUCCESS; - -process(["restart"]) -> - init:restart(), - ?STATUS_SUCCESS; - -process(["mnesia"]) -> - print("~p~n", [mnesia:system_info(all)]), - ?STATUS_SUCCESS; - -process(["mnesia", "info"]) -> +%% TODO: Mnesia operations should not be hardcoded in ejabberd_ctl module. +%% For now, I leave them there to avoid breaking those commands for people that +%% may be using it (as format of response is going to change). +process(["mnesia_info_ctl"], _Version) -> mnesia:info(), ?STATUS_SUCCESS; -process(["mnesia", Arg]) -> - case catch mnesia:system_info(list_to_atom(Arg)) of - {'EXIT', Error} -> print("Error: ~p~n", [Error]); - Return -> print("~p~n", [Return]) - end, - ?STATUS_SUCCESS; +process(["print_sql_schema", DBType, DBVersion, NewSchema], _Version) -> + ejabberd_sql_schema:print_schema(DBType, DBVersion, NewSchema); %% The arguments --long and --dual are not documented because they are %% automatically selected depending in the number of columns of the shell -process(["help" | Mode]) -> +process(["help" | Mode], Version) -> {MaxC, ShCode} = get_shell_info(), case Mode of [] -> - print_usage(dual, MaxC, ShCode), - ?STATUS_USAGE; - ["--dual"] -> - print_usage(dual, MaxC, ShCode), - ?STATUS_USAGE; - ["--long"] -> - print_usage(long, MaxC, ShCode), - ?STATUS_USAGE; - ["--tags"] -> - print_usage_tags(MaxC, ShCode), - ?STATUS_SUCCESS; - ["--tags", Tag] -> - print_usage_tags(Tag, MaxC, ShCode), - ?STATUS_SUCCESS; - ["help"] -> print_usage_help(MaxC, ShCode), ?STATUS_SUCCESS; - [CmdString | _] -> - CmdStringU = ejabberd_regexp:greplace( - list_to_binary(CmdString), <<"-">>, <<"_">>), - print_usage_commands(binary_to_list(CmdStringU), MaxC, ShCode), + ["--dual"] -> + print_usage(dual, MaxC, ShCode, Version), + ?STATUS_USAGE; + ["--long"] -> + print_usage(long, MaxC, ShCode, Version), + ?STATUS_USAGE; + ["tags"] -> + print_usage_tags(MaxC, ShCode, Version), + ?STATUS_SUCCESS; + ["--tags"] -> % deprecated in favor of "tags" + print_usage_tags(MaxC, ShCode, Version), + ?STATUS_SUCCESS; + ["commands"] -> + print_usage_tags_long(MaxC, ShCode, Version), + ?STATUS_SUCCESS; + ["--tags", Tag] -> % deprecated in favor of simply "Tag" + print_usage_tags(Tag, MaxC, ShCode, Version), + ?STATUS_SUCCESS; + [String | _] -> + case determine_string_type(String, Version) of + no_idea -> + io:format("No tag or command matches '~ts'~n", [String]); + both -> + print_usage_tags(String, MaxC, ShCode, Version), + print_usage_commands2(String, MaxC, ShCode, Version); + tag -> + print_usage_tags(String, MaxC, ShCode, Version); + command -> + print_usage_commands2(String, MaxC, ShCode, Version) + end, ?STATUS_SUCCESS end; -process(Args) -> - AccessCommands = get_accesscommands(), - {String, Code} = process2(Args, AccessCommands), +process(["--version", Arg | Args], _) -> + Version = + try + list_to_integer(Arg) + catch _:_ -> + throw({invalid_version, Arg}) + end, + process(Args, Version); + +process(Args, Version) -> + {String, Code} = process2(Args, [], Version), case String of [] -> ok; _ -> - io:format("~s~n", [String]) + io:format("~ts~n", [String]) end, Code. -%% @spec (Args::[string()], AccessCommands) -> {String::string(), Code::integer()} -process2(["--auth", User, Server, Pass | Args], AccessCommands) -> - process2(Args, {list_to_binary(User), list_to_binary(Server), list_to_binary(Pass)}, AccessCommands); +-spec process2(Args::[string()], AccessCommands::any()) -> + {String::string(), Code::integer()}. process2(Args, AccessCommands) -> - process2(Args, noauth, AccessCommands). + process2(Args, AccessCommands, ?DEFAULT_VERSION). -process2(Args, Auth, AccessCommands) -> - case try_run_ctp(Args, Auth, AccessCommands) of +process2(["--auth", User, Server, Pass | Args], AccessCommands, Version) -> + process2(Args, AccessCommands, {list_to_binary(User), list_to_binary(Server), + list_to_binary(Pass), true}, Version); +process2(Args, AccessCommands, Version) -> + process2(Args, AccessCommands, noauth, Version). + + + +process2(Args, AccessCommands, Auth, Version) -> + case try_run_ctp(Args, Auth, AccessCommands, Version) of {String, wrong_command_arguments} when is_list(String) -> io:format(lists:flatten(["\n" | String]++["\n"])), [CommandString | _] = Args, - process(["help" | [CommandString]]), - {lists:flatten(String), ?STATUS_ERROR}; + process(["help" | [CommandString]], Version), + {lists:flatten(String), ?STATUS_USAGE}; {String, Code} when is_list(String) and is_integer(Code) -> {lists:flatten(String), Code}; @@ -235,74 +276,99 @@ process2(Args, Auth, AccessCommands) -> {"Erroneous result: " ++ io_lib:format("~p", [Other]), ?STATUS_ERROR} end. -get_accesscommands() -> - ejabberd_config:get_option(ejabberdctl_access_commands, - fun(V) when is_list(V) -> V end, []). +determine_string_type(String, Version) -> + TagsCommands = ejabberd_commands:get_tags_commands(Version), + CommandsNames = case lists:keysearch(String, 1, TagsCommands) of + {value, {String, CNs}} -> CNs; + false -> [] + end, + AllCommandsNames = [atom_to_list(Name) || {Name, _, _} <- ejabberd_commands:list_commands(Version)], + Cmds = filter_commands(AllCommandsNames, String), + case {CommandsNames, Cmds} of + {[], []} -> no_idea; + {[], _} -> command; + {_, []} -> tag; + {_, _} -> both + end. %%----------------------------- %% Command calling %%----------------------------- -%% @spec (Args::[string()], Auth, AccessCommands) -> string() | integer() | {string(), integer()} -try_run_ctp(Args, Auth, AccessCommands) -> +try_run_ctp(Args, Auth, AccessCommands, Version) -> try ejabberd_hooks:run_fold(ejabberd_ctl_process, false, [Args]) of false when Args /= [] -> - try_call_command(Args, Auth, AccessCommands); + try_call_command(Args, Auth, AccessCommands, Version); false -> - print_usage(), - {"", ?STATUS_USAGE}; + print_usage(Version), + {"", ?STATUS_BADRPC}; Status -> {"", Status} catch exit:Why -> - print_usage(), + print_usage(Version), {io_lib:format("Error in ejabberd ctl process: ~p", [Why]), ?STATUS_USAGE}; Error:Why -> %% In this case probably ejabberd is not started, so let's show Status - process(["status"]), + process(["status"], Version), print("~n", []), {io_lib:format("Error in ejabberd ctl process: '~p' ~p", [Error, Why]), ?STATUS_USAGE} end. -%% @spec (Args::[string()], Auth, AccessCommands) -> string() | integer() | {string(), integer()} -try_call_command(Args, Auth, AccessCommands) -> - try call_command(Args, Auth, AccessCommands) of - {error, command_unknown} -> - {io_lib:format("Error: command ~p not known.", [hd(Args)]), ?STATUS_ERROR}; - {error, wrong_command_arguments} -> - {"Error: wrong arguments", ?STATUS_ERROR}; +try_call_command(Args, Auth, AccessCommands, Version) -> + try call_command(Args, Auth, AccessCommands, Version) of + {Reason, wrong_command_arguments} -> + {Reason, ?STATUS_USAGE}; Res -> Res catch - A:Why -> - Stack = erlang:get_stacktrace(), - {io_lib:format("Problem '~p ~p' occurred executing the command.~nStacktrace: ~p", [A, Why, Stack]), ?STATUS_ERROR} + throw:{error, unknown_command} -> + KnownCommands = [Cmd || {Cmd, _, _} <- ejabberd_commands:list_commands(Version)], + UnknownCommand = list_to_atom(hd(Args)), + {io_lib:format( + "Error: unknown command '~ts'. Did you mean '~ts'?", + [hd(Args), misc:best_match(UnknownCommand, KnownCommands)]), + ?STATUS_ERROR}; + throw:Error -> + {io_lib:format("~p", [Error]), ?STATUS_ERROR}; + A:Why:StackTrace -> + {io_lib:format("Unhandled exception occurred executing the command:~n** ~ts", + [misc:format_exception(2, A, Why, StackTrace)]), + ?STATUS_ERROR} end. -%% @spec (Args::[string()], Auth, AccessCommands) -> string() | integer() | {string(), integer()} | {error, ErrorType} -call_command([CmdString | Args], Auth, AccessCommands) -> +-spec call_command(Args::[string()], + Auth::noauth | {binary(), binary(), binary(), true}, + AccessCommands::[any()], + Version::integer()) -> + string() | integer() | {string(), integer()} | {error, ErrorType::any()}. +call_command([CmdString | Args], Auth, _AccessCommands, Version) -> CmdStringU = ejabberd_regexp:greplace( list_to_binary(CmdString), <<"-">>, <<"_">>), Command = list_to_atom(binary_to_list(CmdStringU)), - case ejabberd_commands:get_command_format(Command) of - {error, command_unknown} -> - {error, command_unknown}; - {ArgsFormat, ResultFormat} -> - case (catch format_args(Args, ArgsFormat)) of - ArgsFormatted when is_list(ArgsFormatted) -> - Result = ejabberd_commands:execute_command(AccessCommands, Auth, Command, - ArgsFormatted), - format_result(Result, ResultFormat); - {'EXIT', {function_clause,[{lists,zip,[A1, A2], _} | _]}} -> - {NumCompa, TextCompa} = - case {length(A1), length(A2)} of - {L1, L2} when L1 < L2 -> {L2-L1, "less argument"}; - {L1, L2} when L1 > L2 -> {L1-L2, "more argument"} - end, - {io_lib:format("Error: the command ~p requires ~p ~s.", - [CmdString, NumCompa, TextCompa]), - wrong_command_arguments} - end + {ArgsFormat, _, ResultFormat} = ejabberd_commands:get_command_format(Command, Auth, Version), + case (catch format_args(Args, ArgsFormat)) of + ArgsFormatted when is_list(ArgsFormatted) -> + CI = case Auth of + {U, S, _, _} -> #{usr => {U, S, <<"">>}, caller_host => S}; + _ -> #{} + end, + CI2 = CI#{caller_module => ?MODULE}, + Result = ejabberd_commands:execute_command2(Command, + ArgsFormatted, + CI2, + Version), + format_result_preliminary(Result, ResultFormat, Version); + {'EXIT', {function_clause,[{lists,zip,[A1,A2|_], _} | _]}} -> + {NumCompa, TextCompa} = + case {length(A1), length(A2)} of + {L1, L2} when L1 < L2 -> {L2-L1, "less argument"}; + {L1, L2} when L1 > L2 -> {L1-L2, "more argument"} + end, + process(["help" | [CmdString]], Version), + {io_lib:format("Error: the command '~ts' requires ~p ~ts.", + [CmdString, NumCompa, TextCompa]), + wrong_command_arguments} end. @@ -322,13 +388,24 @@ format_args(Args, ArgsFormat) -> format_arg(Arg, integer) -> format_arg2(Arg, "~d"); format_arg(Arg, binary) -> - list_to_binary(format_arg(Arg, string)); + unicode:characters_to_binary(Arg, utf8); 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, {list, {_ArgName, ArgFormat}}) -> + [format_arg(string:trim(Element), ArgFormat) || Element <- string:tokens(Arg, ",")]; +format_arg(Arg, {list, ArgFormat}) -> + [format_arg(string:trim(Element), ArgFormat) || Element <- string:tokens(Arg, ",")]; +format_arg(Arg, {tuple, Elements}) -> + Args = string:tokens(Arg, ":"), + list_to_tuple(format_args(Args, Elements)); +format_arg(Arg, Format) -> + S = unicode:characters_to_binary(Arg, utf8), + JSON = misc:json_decode(S), + mod_http_api:format_arg(JSON, Format). format_arg2(Arg, Parse)-> {ok, [Arg2], _RemainingArguments} = io_lib:fread(Parse, Arg), @@ -338,37 +415,77 @@ format_arg2(Arg, Parse)-> %% Format result %%----------------------------- -format_result({error, ErrorAtom}, _) -> +format_result_preliminary(Result, {A, {list, B}}, Version) -> + format_result(Result, {A, {top_result_list, B}}, Version); +format_result_preliminary(Result, ResultFormat, Version) -> + format_result(Result, ResultFormat, Version). + +format_result({error, ErrorAtom}, _, _Version) -> {io_lib:format("Error: ~p", [ErrorAtom]), make_status(error)}; -format_result(Atom, {_Name, atom}) -> +%% An error should always be allowed to return extended error to help with API. +%% Extended error is of the form: +%% {error, type :: atom(), code :: int(), Desc :: string()} +format_result({error, ErrorAtom, Code, Msg}, _, _Version) -> + {io_lib:format("Error: ~p: ~s", [ErrorAtom, Msg]), make_status(Code)}; + +format_result(Atom, {_Name, atom}, _Version) -> io_lib:format("~p", [Atom]); -format_result(Int, {_Name, integer}) -> +format_result(Int, {_Name, integer}, _Version) -> io_lib:format("~p", [Int]); -format_result(String, {_Name, string}) when is_list(String) -> - io_lib:format("~s", [String]); +format_result([A|_]=String, {_Name, string}, _Version) when is_list(String) and is_integer(A) -> + io_lib:format("~ts", [String]); -format_result(Binary, {_Name, string}) when is_binary(Binary) -> - io_lib:format("~s", [binary_to_list(Binary)]); +format_result(Binary, {_Name, binary}, _Version) when is_binary(Binary) -> + io_lib:format("~ts", [Binary]); -format_result(Code, {_Name, rescode}) -> +format_result(String, {_Name, binary}, _Version) when is_list(String) -> + io_lib:format("~ts", [String]); + +format_result(Binary, {_Name, string}, _Version) when is_binary(Binary) -> + io_lib:format("~ts", [Binary]); + +format_result(Atom, {_Name, string}, _Version) when is_atom(Atom) -> + io_lib:format("~ts", [atom_to_list(Atom)]); + +format_result(Integer, {_Name, string}, _Version) when is_integer(Integer) -> + io_lib:format("~ts", [integer_to_list(Integer)]); + +format_result(Other, {_Name, string}, _Version) -> + io_lib:format("~p", [Other]); + +format_result(Code, {_Name, rescode}, _Version) -> make_status(Code); -format_result({Code, Text}, {_Name, restuple}) -> - {io_lib:format("~s", [Text]), make_status(Code)}; +format_result({Code, Text}, {_Name, restuple}, _Version) -> + {io_lib:format("~ts", [Text]), make_status(Code)}; + +format_result([], {_Name, {top_result_list, _ElementsDef}}, _Version) -> + ""; +format_result([FirstElement | Elements], {_Name, {top_result_list, ElementsDef}}, Version) -> + [format_result(FirstElement, ElementsDef, Version) | + lists:map( + fun(Element) -> + ["\n" | format_result(Element, ElementsDef, Version)] + end, + Elements)]; %% The result is a list of something: [something()] -format_result([], {_Name, {list, _ElementsDef}}) -> +format_result([], {_Name, {list, _ElementsDef}}, _Version) -> ""; -format_result([FirstElement | Elements], {_Name, {list, ElementsDef}}) -> +format_result([FirstElement | Elements], {_Name, {list, ElementsDef}}, Version) -> + Separator = case Version of + 0 -> ";"; + _ -> "," + end, %% Start formatting the first element - [format_result(FirstElement, ElementsDef) | + [format_result(FirstElement, ElementsDef, Version) | %% If there are more elements, put always first a newline character lists:map( fun(Element) -> - ["\n" | format_result(Element, ElementsDef)] + [Separator | format_result(Element, ElementsDef, Version)] end, Elements)]; @@ -376,100 +493,98 @@ format_result([FirstElement | Elements], {_Name, {list, ElementsDef}}) -> %% NOTE: the elements in the tuple are separated with tabular characters, %% if a string is empty, it will be difficult to notice in the shell, %% maybe a different separation character should be used, like ;;? -format_result(ElementsTuple, {_Name, {tuple, ElementsDef}}) -> +format_result(ElementsTuple, {_Name, {tuple, ElementsDef}}, Version) -> ElementsList = tuple_to_list(ElementsTuple), [{FirstE, FirstD} | ElementsAndDef] = lists:zip(ElementsList, ElementsDef), - [format_result(FirstE, FirstD) | + [format_result(FirstE, FirstD, Version) | lists:map( fun({Element, ElementDef}) -> - ["\t" | format_result(Element, ElementDef)] + ["\t" | format_result(Element, ElementDef, Version)] end, - ElementsAndDef)]. + ElementsAndDef)]; + +format_result(404, {_Name, _}, _Version) -> + make_status(not_found). make_status(ok) -> ?STATUS_SUCCESS; make_status(true) -> ?STATUS_SUCCESS; -make_status(_Error) -> ?STATUS_ERROR. +make_status(Code) when is_integer(Code), Code > 255 -> ?STATUS_ERROR; +make_status(Code) when is_integer(Code), Code > 0 -> Code; +make_status(Error) -> + io:format("Error: ~p~n", [Error]), + ?STATUS_ERROR. -get_list_commands() -> - try ejabberd_commands:list_commands() of +get_list_commands(Version) -> + try ejabberd_commands:list_commands(Version) of Commands -> - [tuple_command_help(Command) - || {N,_,_}=Command <- Commands, - %% Don't show again those commands, because they are already - %% announced by ejabberd_ctl itself - N /= status, N /= stop, N /= restart] + [tuple_command_help(Command) || Command <- Commands] catch exit:_ -> [] end. %% Return: {string(), [string()], string()} -tuple_command_help({Name, Args, Desc}) -> +tuple_command_help({Name, _Args, Desc}) -> + {Args, _, _} = ejabberd_commands:get_command_format(Name, admin), Arguments = [atom_to_list(ArgN) || {ArgN, _ArgF} <- Args], - Prepend = case is_supported_args(Args) of - true -> ""; - false -> "*" - end, CallString = atom_to_list(Name), - {CallString, Arguments, Prepend ++ Desc}. + {CallString, Arguments, Desc}. -is_supported_args(Args) -> - lists:all( - fun({_Name, Format}) -> - (Format == integer) - or (Format == string) - or (Format == binary) +has_tuple_args(Args) -> + lists:any( + fun({_Name, tuple}) -> true; + ({_Name, {tuple, _}}) -> true; + ({_Name, {list, SubArg}}) -> + has_tuple_args([SubArg]); + (_) -> false end, Args). -get_list_ctls() -> - case catch ets:tab2list(ejabberd_ctl_cmds) of - {'EXIT', _} -> []; - Cs -> [{NameArgs, [], Desc} || {NameArgs, Desc} <- Cs] - end. - +has_list_args(Args) -> + lists:any( + fun({_Name, list}) -> true; + ({_Name, {list, _}}) -> true; + (_) -> false + end, + Args). %%----------------------------- %% Print help %%----------------------------- -%% Bold +%% Commands are Bold -define(B1, "\e[1m"). -define(B2, "\e[22m"). --define(B(S), case ShCode of true -> [?B1, S, ?B2]; false -> S end). +-define(C(S), case ShCode of true -> [?B1, S, ?B2]; false -> S end). -%% Underline +%% Arguments are Dim +-define(D1, "\e[2m"). +-define(D2, "\e[22m"). +-define(A(S), case ShCode of true -> [?D1, S, ?D2]; false -> S end). + +%% Tags are Underline -define(U1, "\e[4m"). -define(U2, "\e[24m"). --define(U(S), case ShCode of true -> [?U1, S, ?U2]; false -> S end). +-define(G(S), case ShCode of true -> [?U1, S, ?U2]; false -> S end). -print_usage() -> +%% B are Nothing +-define(N1, "\e[0m"). +-define(N2, "\e[0m"). +-define(B(S), case ShCode of true -> [?N1, S, ?N2]; false -> S end). + +print_usage(Version) -> {MaxC, ShCode} = get_shell_info(), - print_usage(dual, MaxC, ShCode). -print_usage(HelpMode, MaxC, ShCode) -> - AllCommands = - [ - {"status", [], "Get ejabberd status"}, - {"stop", [], "Stop ejabberd"}, - {"restart", [], "Restart ejabberd"}, - {"help", ["[--tags [tag] | com?*]"], "Show help (try: ejabberdctl help help)"}, - {"mnesia", ["[info]"], "show information of Mnesia system"}] ++ - get_list_commands() ++ - get_list_ctls(), + print_usage(dual, MaxC, ShCode, Version). +print_usage(HelpMode, MaxC, ShCode, Version) -> + AllCommands = get_list_commands(Version), print( - ["Usage: ", ?B("ejabberdctl"), " [--node ", ?U("nodename"), "] [--auth ", - ?U("user"), " ", ?U("host"), " ", ?U("password"), "] ", - ?U("command"), " [", ?U("options"), "]\n" + ["Usage: ", "ejabberdctl", " [--no-timeout] [--node ", ?A("name"), "] [--version ", ?A("apiv"), "] ", + "[--auth ", ?A("user host pass"), "] ", + ?C("command"), " [", ?A("arguments"), "]\n" "\n" "Available commands in this ejabberd node:\n"], []), - print_usage_commands(HelpMode, MaxC, ShCode, AllCommands), - print( - ["\n" - "Examples:\n" - " ejabberdctl restart\n" - " ejabberdctl --node ejabberd@host restart\n"], - []). + print_usage_commands(HelpMode, MaxC, ShCode, AllCommands). print_usage_commands(HelpMode, MaxC, ShCode, Commands) -> CmdDescsSorted = lists:keysort(1, Commands), @@ -515,6 +630,14 @@ get_shell_info() -> %% Split this command description in several lines of proper length prepare_description(DescInit, MaxC, Desc) -> + case string:find(Desc, "\n") of + nomatch -> + prepare_description2(DescInit, MaxC, Desc); + _ -> + Desc + end. + +prepare_description2(DescInit, MaxC, Desc) -> Words = string:tokens(Desc, " "), prepare_long_line(DescInit, MaxC, Words). @@ -561,21 +684,27 @@ format_command_lines(CALD, MaxCmdLen, MaxC, ShCode, dual) %% If the space available for descriptions is too narrow, enforce long help mode format_command_lines(CALD, MaxCmdLen, MaxC, ShCode, long); +format_command_lines(CALD, _MaxCmdLen, _MaxC, ShCode, short) -> + lists:map( + fun({Cmd, Args, _CmdArgsL, _Desc}) -> + [" ", ?C(Cmd), [[" ", ?A(Arg)] || Arg <- Args], "\n"] + end, CALD); + format_command_lines(CALD, MaxCmdLen, MaxC, ShCode, dual) -> lists:map( fun({Cmd, Args, CmdArgsL, Desc}) -> DescFmt = prepare_description(MaxCmdLen+4, MaxC, Desc), - [" ", ?B(Cmd), " ", [[?U(Arg), " "] || Arg <- Args], - string:chars($\s, MaxCmdLen - CmdArgsL + 1), + [" ", ?C(Cmd), [[" ", ?A(Arg)] || Arg <- Args], + lists:duplicate(MaxCmdLen - CmdArgsL + 1, $\s), DescFmt, "\n"] end, CALD); format_command_lines(CALD, _MaxCmdLen, MaxC, ShCode, long) -> lists:map( fun({Cmd, Args, _CmdArgsL, Desc}) -> - DescFmt = prepare_description(8, MaxC, Desc), - ["\n ", ?B(Cmd), " ", [[?U(Arg), " "] || Arg <- Args], "\n", " ", - DescFmt, "\n"] + DescFmt = prepare_description(13, MaxC, Desc), + [" ", ?C(Cmd), [[" ", ?A(Arg)] || Arg <- Args], "\n", + " ", DescFmt, "\n"] end, CALD). @@ -583,23 +712,45 @@ format_command_lines(CALD, _MaxCmdLen, MaxC, ShCode, long) -> %% Print Tags %%----------------------------- -print_usage_tags(MaxC, ShCode) -> - print("Available tags and commands:", []), - TagsCommands = ejabberd_commands:get_tags_commands(), +print_usage_tags(MaxC, ShCode, Version) -> + print("Available tags and list of commands:", []), + TagsCommands = ejabberd_commands:get_tags_commands(Version), lists:foreach( fun({Tag, Commands} = _TagCommands) -> - print(["\n\n ", ?B(Tag), "\n "], []), + print(["\n\n ", ?G(Tag), "\n "], []), Words = lists:sort(Commands), Desc = prepare_long_line(5, MaxC, Words), - print(Desc, []) + print(?C(Desc), []) end, TagsCommands), print("\n\n", []). -print_usage_tags(Tag, MaxC, ShCode) -> - print(["Available commands with tag ", ?B(Tag), ":", "\n"], []), +print_usage_tags_long(MaxC, ShCode, Version) -> + print("Available tags and commands details:", []), + TagsCommands = ejabberd_commands:get_tags_commands(Version), + print("\n", []), + lists:foreach( + fun({Tag, CommandsNames} = _TagCommands) -> + print(["\n ", ?G(Tag), "\n"], []), + CommandsList = lists:map( + fun(NameString) -> + C = ejabberd_commands:get_command_definition( + list_to_atom(NameString), Version), + #ejabberd_commands{name = Name, + args = Args, + desc = Desc} = C, + tuple_command_help({Name, Args, Desc}) + end, + CommandsNames), + print_usage_commands(short, MaxC, ShCode, CommandsList) + end, + TagsCommands), + print("\n", []). + +print_usage_tags(Tag, MaxC, ShCode, Version) -> + print(["Available commands with tag ", ?G(Tag), ":", "\n", "\n"], []), HelpMode = long, - TagsCommands = ejabberd_commands:get_tags_commands(), + TagsCommands = ejabberd_commands:get_tags_commands(Version), CommandsNames = case lists:keysearch(Tag, 1, TagsCommands) of {value, {Tag, CNs}} -> CNs; false -> [] @@ -607,7 +758,7 @@ print_usage_tags(Tag, MaxC, ShCode) -> CommandsList = lists:map( fun(NameString) -> C = ejabberd_commands:get_command_definition( - list_to_atom(NameString)), + list_to_atom(NameString), Version), #ejabberd_commands{name = Name, args = Args, desc = Desc} = C, @@ -624,62 +775,74 @@ print_usage_tags(Tag, MaxC, ShCode) -> print_usage_help(MaxC, ShCode) -> LongDesc = - ["The special 'help' ejabberdctl command provides help of ejabberd commands.\n\n" - "The format is:\n ", ?B("ejabberdctl"), " ", ?B("help"), " [", ?B("--tags"), " ", ?U("[tag]"), " | ", ?U("com?*"), "]\n\n" + ["This special ", ?C("help"), " command provides help of ejabberd commands.\n\n" + "The format is:\n ", ?B("ejabberdctl"), " ", ?C("help"), + " [", ?A("tags"), " | ", ?A("commands"), " | ", ?G("tag"), " | ", ?C("command"), " | ", ?C("com?*"), "]\n\n" "The optional arguments:\n" - " ",?B("--tags")," Show all tags and the names of commands in each tag\n" - " ",?B("--tags"), " ", ?U("tag")," Show description of commands in this tag\n" - " ",?U("command")," Show detailed description of the command\n" - " ",?U("com?*")," Show detailed description of commands that match this glob.\n" - " You can use ? to match a simple character,\n" - " and * to match several characters.\n" + " ",?A("tags")," Show all tags and commands names in each tag\n" + " ",?A("commands")," Show all tags and commands details in each tag\n" + " ",?G("tag")," Show commands related to this tag\n" + " ",?C("command")," Show detailed description of this command\n" + " ",?C("com?*")," Show commands that match this glob.\n" + " (? will match a simple character, and\n" + " * will match several characters)\n" "\n", "Some example usages:\n", - " ejabberdctl help\n", - " ejabberdctl help --tags\n", - " ejabberdctl help --tags accounts\n", - " ejabberdctl help register\n", - " ejabberdctl help regist*\n", + " ejabberdctl ", ?C("help"), "\n", + " ejabberdctl ", ?C("help"), " ", ?A("tags"), "\n", + " ejabberdctl ", ?C("help"), " ", ?A("commands"), "\n", + " ejabberdctl ", ?C("help"), " ", ?G("accounts"), "\n", + " ejabberdctl ", ?C("help"), " ", ?C("register"), "\n", + " ejabberdctl ", ?C("help"), " ", ?C("regist*"), "\n", "\n", - "Please note that 'ejabberdctl help' shows all ejabberd commands,\n", - "even those that cannot be used in the shell with ejabberdctl.\n", - "Those commands can be identified because the description starts with: *"], + "Some command arguments are lists or tuples, like add_rosteritem and create_room_with_opts.\n", + "Separate the elements in a list with the , character.\n", + "Separate the elements in a tuple with the : character.\n", + "\n", + "Some commands results are lists or tuples, like get_roster and get_user_subscriptions.\n", + "The elements in a list are separated with a , character.\n", + "The elements in a tuple are separated with a tabular character.\n"], ArgsDef = [], C = #ejabberd_commands{ - desc = "Show help of ejabberd commands", - longdesc = lists:flatten(LongDesc), - args = ArgsDef, - result = {help, string}}, - print_usage_command("help", C, MaxC, ShCode). + name = help, + desc = "Show help of ejabberd commands", + longdesc = lists:flatten(LongDesc), + args = ArgsDef, + result = {help, string}}, + print(get_usage_command2("help", C, MaxC, ShCode), []). %%----------------------------- %% Print usage command %%----------------------------- -%% @spec (CmdSubString::string(), MaxC::integer(), ShCode::boolean()) -> ok -print_usage_commands(CmdSubString, MaxC, ShCode) -> +-spec print_usage_commands2(CmdSubString::string(), MaxC::integer(), + ShCode::boolean(), Version::integer()) -> ok. +print_usage_commands2(CmdSubString, MaxC, ShCode, Version) -> %% Get which command names match this substring - AllCommandsNames = [atom_to_list(Name) || {Name, _, _} <- ejabberd_commands:list_commands()], + AllCommandsNames = [atom_to_list(Name) || {Name, _, _} <- ejabberd_commands:list_commands(Version)], Cmds = filter_commands(AllCommandsNames, CmdSubString), case Cmds of - [] -> io:format("Error: not command found that match: ~p~n", [CmdSubString]); - _ -> print_usage_commands2(lists:sort(Cmds), MaxC, ShCode) + [] -> io:format("Error: no command found that match '~ts'~n", [CmdSubString]); + _ -> print_usage_commands3(lists:sort(Cmds), MaxC, ShCode, Version) end. -print_usage_commands2(Cmds, MaxC, ShCode) -> - %% Then for each one print it - lists:mapfoldl( - fun(Cmd, Remaining) -> - print_usage_command(Cmd, MaxC, ShCode), - case Remaining > 1 of - true -> print([" ", lists:duplicate(MaxC, 126), " \n"], []); - false -> ok - end, - {ok, Remaining-1} - end, - length(Cmds), - Cmds). +print_usage_commands3([Cmd], MaxC, ShCode, Version) -> + print_usage_command(Cmd, MaxC, ShCode, Version); +print_usage_commands3(Cmds, MaxC, ShCode, Version) -> + CommandsList = lists:map( + fun(NameString) -> + C = ejabberd_commands:get_command_definition( + list_to_atom(NameString), Version), + #ejabberd_commands{name = Name, + args = Args, + desc = Desc} = C, + tuple_command_help({Name, Args, Desc}) + end, + Cmds), + + print_usage_commands(long, MaxC, ShCode, CommandsList), %% que aqui solo muestre un par de lineas + ok. filter_commands(All, SubString) -> case lists:member(SubString, All) of @@ -700,28 +863,42 @@ filter_commands_regexp(All, Glob) -> end, All). -%% @spec (Cmd::string(), MaxC::integer(), ShCode::boolean()) -> ok -print_usage_command(Cmd, MaxC, ShCode) -> - Name = list_to_atom(Cmd), - case ejabberd_commands:get_command_definition(Name) of - command_not_found -> - io:format("Error: command ~p not known.~n", [Cmd]); - C -> - print_usage_command(Cmd, C, MaxC, ShCode) - end. +maybe_add_policy_arguments(Args, user) -> + [{user, binary}, {host, binary} | Args]; +maybe_add_policy_arguments(Args, _) -> + Args. -print_usage_command(Cmd, C, MaxC, ShCode) -> +-spec print_usage_command(Cmd::string(), MaxC::integer(), + ShCode::boolean(), Version::integer()) -> ok. +print_usage_command(Cmd, MaxC, ShCode, Version) -> + print(get_usage_command(Cmd, MaxC, ShCode, Version), []). + +get_usage_command(Cmd, MaxC, ShCode, Version) -> + Name = list_to_atom(Cmd), + C = ejabberd_commands:get_command_definition(Name, Version), + get_usage_command2(Cmd, C, MaxC, ShCode). + +get_usage_command2(Cmd, C, MaxC, ShCode) -> #ejabberd_commands{ tags = TagsAtoms, + definer = Definer, desc = Desc, + args = ArgsDefPreliminary, + args_desc = ArgsDesc, + args_example = ArgsExample, + result_example = ResultExample, + policy = Policy, longdesc = LongDesc, - args = ArgsDef, + note = Note, result = ResultDef} = C, - NameFmt = [" ", ?B("Command Name"), ": ", Cmd, "\n"], + NameFmt = [" ", ?B("Command Name"), ": ", ?C(Cmd), "\n"], %% Initial indentation of result is 13 = length(" Arguments: ") - Args = [format_usage_ctype(ArgDef, 13) || ArgDef <- ArgsDef], + ArgsDef = maybe_add_policy_arguments(ArgsDefPreliminary, Policy), + ArgsDetailed = add_args_desc(ArgsDef, ArgsDesc), + Args = [format_usage_ctype1(ArgDetailed, 13, ShCode) || ArgDetailed <- ArgsDetailed], + ArgsMargin = lists:duplicate(13, $\s), ArgsListFmt = case Args of [] -> "\n"; @@ -731,32 +908,104 @@ print_usage_command(Cmd, C, MaxC, ShCode) -> %% Initial indentation of result is 11 = length(" Returns: ") ResultFmt = format_usage_ctype(ResultDef, 11), - ReturnsFmt = [" ",?B("Returns"),": ", ResultFmt], + ReturnsFmt = [" ",?B("Result"),": ", ResultFmt], - XmlrpcFmt = "", %%+++ [" ",?B("XML-RPC"),": ", format_usage_xmlrpc(ArgsDef, ResultDef), "\n\n"], + ExampleMargin = lists:duplicate(11, $\s), + Example = format_usage_example(Cmd, ArgsExample, ResultExample, ExampleMargin), + ExampleFmt = case Example of + [] -> + ""; + _ -> + ExampleListFmt = [ [Ex, "\n", ExampleMargin] || Ex <- Example], + [" ",?B("Example"),": ", ExampleListFmt, "\n"] + end, - TagsFmt = [" ",?B("Tags"),": ", prepare_long_line(8, MaxC, [atom_to_list(TagA) || TagA <- TagsAtoms])], + TagsFmt = [" ",?B("Tags"),":", prepare_long_line(8, MaxC, [?G(atom_to_list(TagA)) || TagA <- TagsAtoms])], - DescFmt = [" ",?B("Description"),": ", prepare_description(15, MaxC, Desc)], + IsDefinerMod = case Definer of + unknown -> true; + _ -> lists:member([gen_mod], proplists:get_all_values(behaviour, Definer:module_info(attributes))) + end, + ModuleFmt = case IsDefinerMod of + true -> [" ",?B("Module"),": ", atom_to_list(Definer), "\n\n"]; + false -> [] + end, + + NoteFmt = case Note of + "" -> []; + _ -> [" ",?B("Note"),": ", Note, "\n\n"] + end, + + DescFmt = [" ",?B("Description"),":", prepare_description(15, MaxC, Desc)], LongDescFmt = case LongDesc of "" -> ""; _ -> ["", prepare_description(0, MaxC, LongDesc), "\n\n"] end, - NoteEjabberdctl = case is_supported_args(ArgsDef) of - true -> ""; - false -> [" ", ?B("Note:"), " This command cannot be executed using ejabberdctl. Try ejabberd_xmlrpc.\n\n"] + NoteEjabberdctlList = case has_list_args(ArgsDefPreliminary) of + true -> [" ", ?B("Note:"), " In a list argument, separate the elements using the , character for example: one,two,three\n\n"]; + false -> "" + end, + NoteEjabberdctlTuple = case has_tuple_args(ArgsDefPreliminary) of + true -> [" ", ?B("Note:"), " In a tuple argument, separate the elements using the : character for example: members_only:true\n\n"]; + false -> "" end, - print(["\n", NameFmt, "\n", ArgsFmt, "\n", ReturnsFmt, "\n\n", XmlrpcFmt, TagsFmt, "\n\n", DescFmt, "\n\n", LongDescFmt, NoteEjabberdctl], []). + First = case Cmd of + "help" -> ""; + _ -> [NameFmt, "\n", ArgsFmt, "\n", ReturnsFmt, + "\n\n", ExampleFmt, TagsFmt, "\n\n", ModuleFmt, NoteFmt, DescFmt, "\n\n"] + end, + [First, LongDescFmt, NoteEjabberdctlList, NoteEjabberdctlTuple]. + +%%----------------------------- +%% Format Arguments Help +%%----------------------------- + +add_args_desc(Definitions, none) -> + Descriptions = lists:duplicate(length(Definitions), ""), + add_args_desc(Definitions, Descriptions); +add_args_desc(Definitions, Descriptions) -> + lists:zipwith(fun({Name, Type}, Description) -> + {Name, Type, Description} end, + Definitions, + Descriptions). + +format_usage_ctype1({_Name, _Type} = Definition, Indentation, ShCode) -> + [Arg] = add_args_desc([Definition], none), + format_usage_ctype1(Arg, Indentation, ShCode); +format_usage_ctype1({Name, Type, Description}, Indentation, ShCode) -> + TypeString = case Type of + {list, ElementDef} -> + NameFmt = atom_to_list(Name), + Indentation2 = Indentation + length(NameFmt) + 4, + ElementFmt = format_usage_ctype1(ElementDef, Indentation2, ShCode), + io_lib:format("[ ~s ]", [lists:flatten(ElementFmt)]); + {tuple, ElementsDef} -> + NameFmt = atom_to_list(Name), + Indentation2 = Indentation + length(NameFmt) + 4, + ElementsFmt = format_usage_tuple(ElementsDef, Indentation2), + io_lib:format("{ ~s }", [lists:flatten(ElementsFmt)]); + _ -> + Type + end, + DescriptionText = case Description of + "" -> ""; + Description -> " : "++Description + end, + io_lib:format("~p::~s~s", [Name, TypeString, DescriptionText]). + format_usage_ctype(Type, _Indentation) - when (Type==atom) or (Type==integer) or (Type==string) or (Type==binary) or (Type==rescode) or (Type==restuple)-> + when (Type==atom) or (Type==integer) or (Type==string) or (Type==binary) + or (Type==rescode) or (Type==restuple) -> io_lib:format("~p", [Type]); format_usage_ctype({Name, Type}, _Indentation) - when (Type==atom) or (Type==integer) or (Type==string) or (Type==binary) or (Type==rescode) or (Type==restuple)-> + when (Type==atom) or (Type==integer) or (Type==string) or (Type==binary) + or (Type==rescode) or (Type==restuple) + or (Type==any) -> io_lib:format("~p::~p", [Name, Type]); format_usage_ctype({Name, {list, ElementDef}}, Indentation) -> @@ -769,12 +1018,13 @@ format_usage_ctype({Name, {tuple, ElementsDef}}, Indentation) -> NameFmt = atom_to_list(Name), Indentation2 = Indentation + length(NameFmt) + 4, ElementsFmt = format_usage_tuple(ElementsDef, Indentation2), - [NameFmt, "::{ " | ElementsFmt]. + [NameFmt, "::{ "] ++ ElementsFmt ++ [" }"]. + format_usage_tuple([], _Indentation) -> []; format_usage_tuple([ElementDef], Indentation) -> - [format_usage_ctype(ElementDef, Indentation) , " }"]; + format_usage_ctype(ElementDef, Indentation); format_usage_tuple([ElementDef | ElementsDef], Indentation) -> ElementFmt = format_usage_ctype(ElementDef, Indentation), MarginString = lists:duplicate(Indentation, $\s), % Put spaces @@ -783,12 +1033,144 @@ format_usage_tuple([ElementDef | ElementsDef], Indentation) -> print(Format, Args) -> io:format(lists:flatten(Format), Args). +-ifdef(LAGER). +disable_logging() -> + ok. +-else. +disable_logging() -> + logger:set_primary_config(level, none). +-endif. + %%----------------------------- -%% Command managment +%% Format Example Help %%----------------------------- -%%+++ -%% Struct(Integer res) create_account(Struct(String user, String server, String password)) -%%format_usage_xmlrpc(ArgsDef, ResultDef) -> -%% ["aaaa bbb ccc"]. +format_usage_example(_Cmd, none, _ResultExample, _Indentation) -> + ""; +format_usage_example(Cmd, ArgsExample, ResultExample, Indentation) -> + Arguments = format_usage_arguments(ArgsExample, []), + Result = format_usage_result([ResultExample], [], Indentation), + [lists:join(" ", ["ejabberdctl", Cmd] ++ Arguments) | Result]. +format_usage_arguments([], R) -> + lists:reverse(R); + +format_usage_arguments([Argument | Arguments], R) + when is_integer(Argument) -> + format_usage_arguments(Arguments, [integer_to_list(Argument) | R]); + +format_usage_arguments([[Integer|_] = Argument | Arguments], R) + when is_list(Argument) and is_integer(Integer) -> + Result = case contains_more_than_letters(Argument) of + true -> ["\"", Argument, "\""]; + false -> [Argument] + end, + format_usage_arguments(Arguments, [Result | R]); + +format_usage_arguments([[Element | _] = Argument | Arguments], R) + when is_list(Argument) and is_tuple(Element) -> + ArgumentFmt = format_usage_arguments(Argument, []), + format_usage_arguments(Arguments, [lists:join(",", ArgumentFmt) | R]); + +format_usage_arguments([Argument | Arguments], R) + when is_list(Argument) -> + Result = format_usage_arguments(Argument, []), + format_usage_arguments(Arguments, [lists:join(",", Result) | R]); + +format_usage_arguments([Argument | Arguments], R) + when is_tuple(Argument) -> + Result = format_usage_arguments(tuple_to_list(Argument), []), + format_usage_arguments(Arguments, [lists:join(":", Result) | R]); + +format_usage_arguments([Argument | Arguments], R) + when is_binary(Argument) -> + Result = case contains_more_than_letters(binary_to_list(Argument)) of + true -> ["\"", Argument, "\""]; + false -> [Argument] + end, + format_usage_arguments(Arguments, [Result | R]); + +format_usage_arguments([Argument | Arguments], R) -> + format_usage_arguments(Arguments, [Argument | R]). + +format_usage_result([none], _R, _Indentation) -> + ""; +format_usage_result([], R, _Indentation) -> + lists:reverse(R); + +format_usage_result([{Code, Text} | Arguments], R, Indentation) + when is_atom(Code) and is_binary(Text) -> + format_usage_result(Arguments, [Text | R], Indentation); + +format_usage_result([Argument | Arguments], R, Indentation) + when is_atom(Argument) -> + format_usage_result(Arguments, [["\'", atom_to_list(Argument), "\'"] | R], Indentation); + +format_usage_result([Argument | Arguments], R, Indentation) + when is_integer(Argument) -> + format_usage_result(Arguments, [integer_to_list(Argument) | R], Indentation); + +format_usage_result([[Integer|_] = Argument | Arguments], R, Indentation) + when is_list(Argument) and is_integer(Integer) -> + format_usage_result(Arguments, [Argument | R], Indentation); + +format_usage_result([[Element | _] = Argument | Arguments], R, Indentation) + when is_list(Argument) and is_tuple(Element) -> + ArgumentFmt = format_usage_result(Argument, [], Indentation), + format_usage_result(Arguments, [lists:join("\n"++Indentation, ArgumentFmt) | R], Indentation); + +format_usage_result([Argument | Arguments], R, Indentation) + when is_list(Argument) -> + format_usage_result(Arguments, [lists:join("\n"++Indentation, Argument) | R], Indentation); + +format_usage_result([Argument | Arguments], R, Indentation) + when is_tuple(Argument) -> + Result = format_usage_result(tuple_to_list(Argument), [], Indentation), + format_usage_result(Arguments, [lists:join("\t", Result) | R], Indentation); + +format_usage_result([Argument | Arguments], R, Indentation) -> + format_usage_result(Arguments, [Argument | R], Indentation). + +contains_more_than_letters(Argument) -> + lists:any(fun(I) when (I < $A) -> true; + (I) when (I > $z) -> true; + (_) -> false end, + Argument). + +%%----------------------------- +%% Register commands +%%----------------------------- + +get_commands_spec() -> + [ + #ejabberd_commands{name = help, tags = [ejabberdctl], + desc = "Get list of commands, or help of a command (only ejabberdctl)", + longdesc = "This command is exclusive for the ejabberdctl command-line script, " + "don't attempt to execute it using any other API frontend."}, + #ejabberd_commands{name = mnesia_change, tags = [ejabberdctl, mnesia], + desc = "Change the erlang node name in the mnesia database (only ejabberdctl)", + longdesc = "This command internally calls the _`mnesia_change_nodename`_ API. " + "This is a special command that starts and stops ejabberd several times: " + "do not attempt to run this command when ejabberd is running. " + "This command is exclusive for the ejabberdctl command-line script, " + "don't attempt to execute it using any other API frontend.", + note = "added in 25.08", + args = [{old_node_name, string}], + args_desc = ["Old erlang node name"], + args_example = ["ejabberd@oldmachine"]}, + #ejabberd_commands{name = mnesia_info_ctl, tags = [ejabberdctl, mnesia], + desc = "Show information of Mnesia system (only ejabberdctl)", + note = "renamed in 24.02", + longdesc = "This command is exclusive for the ejabberdctl command-line script, " + "don't attempt to execute it using any other API frontend."}, + #ejabberd_commands{name = print_sql_schema, tags = [ejabberdctl, sql], + desc = "Print SQL schema for the given RDBMS (only ejabberdctl)", + longdesc = "This command is exclusive for the ejabberdctl command-line script, " + "don't attempt to execute it using any other API frontend.", + note = "added in 24.02", + args = [{db_type, string}, {db_version, string}, {new_schema, string}], + args_desc = ["Database type: pgsql | mysql | sqlite", + "Your database version: 16.1, 8.2.0...", + "Use new schema: 0, false, 1 or true"], + args_example = ["pgsql", "16.1", "true"]} + ]. diff --git a/src/ejabberd_db_sup.erl b/src/ejabberd_db_sup.erl new file mode 100644 index 000000000..192c355c3 --- /dev/null +++ b/src/ejabberd_db_sup.erl @@ -0,0 +1,46 @@ +%%%------------------------------------------------------------------- +%%% Created : 13 June 2019 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(ejabberd_db_sup). + +-behaviour(supervisor). + +%% API +-export([start_link/0]). + +%% Supervisor callbacks +-export([init/1]). + +%%%=================================================================== +%%% API functions +%%%=================================================================== +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +%%%=================================================================== +%%% Supervisor callbacks +%%%=================================================================== +init([]) -> + {ok, {{one_for_one, 10, 1}, []}}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== diff --git a/src/ejabberd_doc.erl b/src/ejabberd_doc.erl new file mode 100644 index 000000000..c2bf33922 --- /dev/null +++ b/src/ejabberd_doc.erl @@ -0,0 +1,515 @@ +%%%---------------------------------------------------------------------- +%%% File : ejabberd_doc.erl +%%% Purpose : Options documentation generator +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(ejabberd_doc). + +%% API +-export([man/0, man/1, have_a2x/0]). + +-include("ejabberd_commands.hrl"). +-include("translate.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +man() -> + man(<<"en">>). + +man(Lang) when is_list(Lang) -> + man(list_to_binary(Lang)); +man(Lang) -> + {ModDoc, SubModDoc} = + lists:foldl( + fun(M, {Mods, SubMods} = Acc) -> + case lists:prefix("mod_", atom_to_list(M)) orelse + lists:prefix("Elixir.Mod", atom_to_list(M)) of + true -> + try M:mod_doc() of + #{desc := Descr} = Map -> + DocOpts = maps:get(opts, Map, []), + Example = maps:get(example, Map, []), + Note = maps:get(note, Map, []), + Apitags = get_module_apitags(M), + {[{M, Descr, DocOpts, #{example => Example, note => Note, apitags => Apitags}}|Mods], SubMods}; + #{opts := DocOpts} -> + {ParentMod, Backend} = strip_backend_suffix(M), + {Mods, dict:append(ParentMod, {M, Backend, DocOpts}, SubMods)}; + #{} -> + warn("module ~s is not properly documented", [M]), + Acc + catch _:undef -> + case erlang:function_exported( + M, mod_options, 1) of + true -> + warn("module ~s is not documented", [M]); + false -> + ok + end, + Acc + end; + false -> + Acc + end + end, {[], dict:new()}, ejabberd_config:beams(all)), + Doc = lists:flatmap( + fun(M) -> + try M:doc() + catch _:undef -> [] + end + end, ejabberd_config:callback_modules(all)), + Version = binary_to_list(ejabberd_config:version()), + Options = + ["TOP LEVEL OPTIONS", + "-----------------", + "This section describes top level options of ejabberd " ++ Version ++ ".", + "The options that changed in this version are marked with 🟤.", + io_lib:nl()] ++ + lists:flatmap( + fun(Opt) -> + opt_to_man(Lang, Opt, 1) + end, lists:keysort(1, Doc)), + ModDoc1 = lists:map( + fun({M, Descr, DocOpts, Ex}) -> + case dict:find(M, SubModDoc) of + {ok, Backends} -> + {M, Descr, DocOpts, Backends, Ex}; + error -> + {M, Descr, DocOpts, [], Ex} + end + end, ModDoc), + ModOptions = + [io_lib:nl(), + "MODULES", + "-------", + "[[modules]]", + "This section describes modules options of ejabberd " ++ Version ++ ".", + "The modules that changed in this version are marked with 🟤.", + io_lib:nl()] ++ + lists:flatmap( + fun({M, Descr, DocOpts, Backends, Example}) -> + ModName = atom_to_list(M), + VersionMark = get_version_mark(Example), + [io_lib:nl(), + lists:flatten([ModName, VersionMark]), + lists:duplicate(length(atom_to_list(M)), $~), + "[[" ++ ModName ++ "]]", + io_lib:nl()] ++ + format_versions(Lang, Example) ++ [io_lib:nl()] ++ + tr_multi(Lang, Descr) ++ [io_lib:nl()] ++ + opts_to_man(Lang, [{M, '', DocOpts}|Backends]) ++ + format_example(0, Lang, Example) ++ [io_lib:nl()] ++ + format_apitags(Lang, Example) + end, lists:keysort(1, ModDoc1)), + ListenOptions = + [io_lib:nl(), + "LISTENERS", + "-------", + "[[listeners]]", + "This section describes listeners options of ejabberd " ++ Version ++ ".", + io_lib:nl(), + "TODO"], + AsciiData = + [[unicode:characters_to_binary(Line), io_lib:nl()] + || Line <- man_header(Lang) ++ Options ++ [io_lib:nl()] + ++ ModOptions ++ ListenOptions ++ man_footer(Lang)], + warn_undocumented_modules(ModDoc1), + warn_undocumented_options(Doc), + write_man(AsciiData). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +opts_to_man(Lang, [{_, _, []}]) -> + Text = tr(Lang, ?T("The module has no options.")), + [Text, io_lib:nl()]; +opts_to_man(Lang, Backends) -> + lists:flatmap( + fun({_, Backend, DocOpts}) when DocOpts /= [] -> + Text = if Backend == '' -> + tr(Lang, ?T("Available options")); + true -> + lists:flatten( + io_lib:format( + tr(Lang, ?T("Available options for '~s' backend")), + [Backend])) + end, + [Text ++ ":", lists:duplicate(length(Text)+1, $^)| + lists:flatmap( + fun(Opt) -> opt_to_man(Lang, Opt, 1) end, + lists:keysort(1, DocOpts))] ++ [io_lib:nl()]; + (_) -> + [] + end, Backends). + +opt_to_man(Lang, {Option, Options}, Level) -> + [format_option(Lang, Option, Options)|format_versions(Lang, Options)++format_desc(Lang, Options)] ++ + format_example(Level, Lang, Options); +opt_to_man(Lang, {Option, Options, Children}, Level) -> + [format_option(Lang, Option, Options)|format_desc(Lang, Options)] ++ + lists:append( + [[H ++ ":"|T] + || [H|T] <- lists:map( + fun(Opt) -> opt_to_man(Lang, Opt, Level+1) end, + lists:keysort(1, Children))]) ++ + [io_lib:nl()|format_example(Level, Lang, Options)]. + +get_version_mark(#{note := Note}) -> + [XX, YY | _] = string:tokens(binary_to_list(ejabberd_option:version()), "."), + XXYY = string:join([XX, YY], "."), + case string:find(Note, XXYY) of + nomatch -> ""; + _ -> " 🟤" + end; +get_version_mark(_) -> + "". + +format_option(Lang, Option, #{value := Val} = Options) -> + VersionMark = get_version_mark(Options), + "*" ++ atom_to_list(Option) ++ VersionMark ++ "*: 'pass:[" ++ + tr(Lang, Val) ++ "]'::"; +format_option(_Lang, Option, #{}) -> + "*" ++ atom_to_list(Option) ++ "*::". + +format_versions(_Lang, #{note := Note}) when Note /= [] -> + ["_Note_ about this option: " ++ Note ++ ". "]; +format_versions(_, _) -> + []. + +%% @format-begin +get_module_apitags(M) -> + AllCommands = ejabberd_commands:get_commands_definition(), + Tags = [C#ejabberd_commands.tags || C <- AllCommands, C#ejabberd_commands.module == M], + TagsClean = + lists:sort( + misc:lists_uniq( + lists:flatten(Tags))), + TagsStrings = [atom_to_list(C) || C <- TagsClean], + TagFiltering = + fun ("internal") -> + false; + ([$v | Rest]) -> + {error, no_integer} == string:to_integer(Rest); + (_) -> + true + end, + TagsFiltered = lists:filter(TagFiltering, TagsStrings), + TagsUrls = + [["_`../../developer/ejabberd-api/admin-tags.md#", C, "|", C, "`_"] || C <- TagsFiltered], + lists:join(", ", TagsUrls). + +format_apitags(_Lang, #{apitags := TagsString}) when TagsString /= "" -> + ["**API Tags:** ", TagsString]; +format_apitags(_, _) -> + []. +%% @format-end + +format_desc(Lang, #{desc := Desc}) -> + tr_multi(Lang, Desc). + +format_example(Level, Lang, #{example := [_|_] = Example}) -> + case lists:all(fun is_list/1, Example) of + true -> + if Level == 0 -> + ["*Example*:", + "^^^^^^^^^^"]; + true -> + ["+", "*Example*:", "+"] + end ++ format_yaml(Example); + false when Level == 0 -> + ["Examples:", + "^^^^^^^^^"] ++ + lists:flatmap( + fun({Text, Lines}) -> + [tr(Lang, Text)] ++ format_yaml(Lines) + end, Example); + false -> + lists:flatmap( + fun(Block) -> + ["+", "*Examples*:", "+"|Block] + end, + lists:map( + fun({Text, Lines}) -> + [tr(Lang, Text), "+"] ++ format_yaml(Lines) + end, Example)) + end; +format_example(_, _, _) -> + []. + +format_yaml(Lines) -> + ["==========================", + "[source,yaml]", + "----"|Lines] ++ + ["----", + "=========================="]. + +man_header(Lang) -> + ["ejabberd.yml(5)", + "===============", + ":doctype: manpage", + ":version: " ++ binary_to_list(ejabberd_config:version()), + io_lib:nl(), + "NAME", + "----", + "ejabberd.yml - " ++ tr(Lang, ?T("main configuration file for ejabberd.")), + io_lib:nl(), + "SYNOPSIS", + "--------", + "ejabberd.yml", + io_lib:nl(), + "DESCRIPTION", + "-----------", + tr(Lang, ?T("The configuration file is written in " + "https://en.wikipedia.org/wiki/YAML[YAML] language.")), + io_lib:nl(), + tr(Lang, ?T("WARNING: YAML is indentation sensitive, so make sure you respect " + "indentation, or otherwise you will get pretty cryptic " + "configuration errors.")), + io_lib:nl(), + tr(Lang, ?T("Logically, configuration options are split into 3 main categories: " + "'Modules', 'Listeners' and everything else called 'Top Level' options. " + "Thus this document is split into 3 main chapters describing each " + "category separately. So, the contents of ejabberd.yml will typically " + "look like this:")), + io_lib:nl(), + "==========================", + "[source,yaml]", + "----", + "hosts:", + " - example.com", + " - domain.tld", + "loglevel: info", + "...", + "listen:", + " -", + " port: 5222", + " module: ejabberd_c2s", + " ...", + "modules:", + " mod_roster: {}", + " ...", + "----", + "==========================", + io_lib:nl(), + tr(Lang, ?T("Any configuration error (such as syntax error, unknown option " + "or invalid option value) is fatal in the sense that ejabberd will " + "refuse to load the whole configuration file and will not start or will " + "abort configuration reload.")), + io_lib:nl(), + tr(Lang, ?T("All options can be changed in runtime by running 'ejabberdctl " + "reload-config' command. Configuration reload is atomic: either all options " + "are accepted and applied simultaneously or the new configuration is " + "refused without any impact on currently running configuration.")), + io_lib:nl(), + tr(Lang, ?T("Some options can be specified for particular virtual host(s) only " + "using 'host_config' or 'append_host_config' options. Such options " + "are called 'local'. Examples are 'modules', 'auth_method' and 'default_db'. " + "The options that cannot be defined per virtual host are called 'global'. " + "Examples are 'loglevel', 'certfiles' and 'listen'. It is a configuration " + "mistake to put 'global' options under 'host_config' or 'append_host_config' " + "section - ejabberd will refuse to load such configuration.")), + io_lib:nl(), + str:format( + tr(Lang, ?T("It is not recommended to write ejabberd.yml from scratch. Instead it is " + "better to start from \"default\" configuration file available at ~s. " + "Once you get ejabberd running you can start changing configuration " + "options to meet your requirements.")), + [default_config_url()]), + io_lib:nl(), + str:format( + tr(Lang, ?T("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 ~s.")), + [configuration_guide_url()]), + io_lib:nl()]. + +man_footer(Lang) -> + {Year, _, _} = date(), + [io_lib:nl(), + "AUTHOR", + "------", + "https://www.process-one.net[ProcessOne].", + io_lib:nl(), + "VERSION", + "-------", + str:format( + tr(Lang, ?T("This document describes the configuration file of ejabberd ~ts. " + "Configuration options of other ejabberd versions " + "may differ significantly.")), + [ejabberd_config:version()]), + io_lib:nl(), + "REPORTING BUGS", + "--------------", + tr(Lang, ?T("Report bugs to ")), + io_lib:nl(), + "SEE ALSO", + "---------", + tr(Lang, ?T("Default configuration file")) ++ ": " ++ default_config_url(), + io_lib:nl(), + tr(Lang, ?T("Main site")) ++ ": ", + io_lib:nl(), + tr(Lang, ?T("Documentation")) ++ ": ", + io_lib:nl(), + tr(Lang, ?T("Configuration Guide")) ++ ": " ++ configuration_guide_url(), + io_lib:nl(), + tr(Lang, ?T("Source code")) ++ ": ", + io_lib:nl(), + "COPYING", + "-------", + "Copyright (c) 2002-" ++ integer_to_list(Year) ++ + " https://www.process-one.net[ProcessOne]."]. + +tr(Lang, {Format, Args}) -> + unicode:characters_to_list( + str:format( + translate:translate(Lang, iolist_to_binary(Format)), + Args)); +tr(Lang, Txt) -> + unicode:characters_to_list(translate:translate(Lang, iolist_to_binary(Txt))). + +tr_multi(Lang, Txt) when is_binary(Txt) -> + tr_multi(Lang, [Txt]); +tr_multi(Lang, {Format, Args}) -> + tr_multi(Lang, [{Format, Args}]); +tr_multi(Lang, Lines) when is_list(Lines) -> + [tr(Lang, Txt) || Txt <- Lines]. + +write_man(AsciiData) -> + case file:get_cwd() of + {ok, Cwd} -> + AsciiDocFile = filename:join(Cwd, "ejabberd.yml.5.txt"), + ManPage = filename:join(Cwd, "ejabberd.yml.5"), + case file:write_file(AsciiDocFile, AsciiData) of + ok -> + Ret = run_a2x(Cwd, AsciiDocFile), + %%file:delete(AsciiDocFile), + case Ret of + ok -> + {ok, lists:flatten( + io_lib:format( + "The manpage saved as ~ts", [ManPage]))}; + {error, Error} -> + {error, lists:flatten( + io_lib:format( + "Failed to generate manpage: ~ts", [Error]))} + end; + {error, Reason} -> + {error, lists:flatten( + io_lib:format( + "Failed to write to ~ts: ~s", + [AsciiDocFile, file:format_error(Reason)]))} + end; + {error, Reason} -> + {error, lists:flatten( + io_lib:format("Failed to get current directory: ~s", + [file:format_error(Reason)]))} + end. + +have_a2x() -> + case os:find_executable("a2x") of + false -> false; + Path -> {true, Path} + end. + +run_a2x(Cwd, AsciiDocFile) -> + case have_a2x() of + false -> + {error, "a2x was not found: do you have 'asciidoc' installed?"}; + {true, Path} -> + Cmd = lists:flatten( + io_lib:format("~ts --no-xmllint -f manpage ~ts -D ~ts", + [Path, AsciiDocFile, Cwd])), + case os:cmd(Cmd) of + "" -> ok; + Ret -> {error, Ret} + end + end. + +warn_undocumented_modules(Docs) -> + lists:foreach( + fun({M, _, DocOpts, Backends, _}) -> + warn_undocumented_module(M, DocOpts), + lists:foreach( + fun({SubM, _, SubOpts}) -> + warn_undocumented_module(SubM, SubOpts) + end, Backends) + end, Docs). + +warn_undocumented_module(M, DocOpts) -> + try M:mod_options(ejabberd_config:get_myname()) of + Defaults -> + lists:foreach( + fun(OptDefault) -> + Opt = case OptDefault of + O when is_atom(O) -> O; + {O, _} -> O + end, + case lists:keymember(Opt, 1, DocOpts) of + false -> + warn("~s: option ~s is not documented", + [M, Opt]); + true -> + ok + end + end, Defaults) + catch _:undef -> + ok + end. + +warn_undocumented_options(Docs) -> + Opts = lists:flatmap( + fun(M) -> + try M:options() of + Defaults -> + lists:map( + fun({O, _}) -> O; + (O) when is_atom(O) -> O + end, Defaults) + catch _:undef -> + [] + end + end, ejabberd_config:callback_modules(all)), + lists:foreach( + fun(Opt) -> + case lists:keymember(Opt, 1, Docs) of + false -> + warn("option ~s is not documented", [Opt]); + true -> + ok + end + end, Opts). + +warn(Format, Args) -> + io:format(standard_error, "Warning: " ++ Format ++ "~n", Args). + +strip_backend_suffix(M) -> + [H|T] = lists:reverse(string:tokens(atom_to_list(M), "_")), + {list_to_atom(string:join(lists:reverse(T), "_")), list_to_atom(H)}. + +default_config_url() -> + "". + +configuration_guide_url() -> + "". diff --git a/src/ejabberd_frontend_socket.erl b/src/ejabberd_frontend_socket.erl deleted file mode 100644 index b169572b3..000000000 --- a/src/ejabberd_frontend_socket.erl +++ /dev/null @@ -1,293 +0,0 @@ -%%%------------------------------------------------------------------- -%%% File : ejabberd_frontend_socket.erl -%%% Author : Alexey Shchepin -%%% Purpose : Frontend socket with zlib and TLS support library -%%% Created : 23 Aug 2006 by Alexey Shchepin -%%% -%%% -%%% ejabberd, Copyright (C) 2002-2015 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(ejabberd_frontend_socket). - --author('alexey@process-one.net'). - --behaviour(gen_server). - -%% API --export([start/4, - start_link/5, - %connect/3, - starttls/2, - starttls/3, - compress/1, - compress/2, - reset_stream/1, - send/2, - change_shaper/2, - monitor/1, - get_sockmod/1, - get_peer_certificate/1, - get_verify_result/1, - close/1, - sockname/1, peername/1]). - -%% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, - handle_info/2, terminate/2, code_change/3]). - --record(state, {sockmod, socket, receiver}). - --define(HIBERNATE_TIMEOUT, 90000). - -%%==================================================================== -%% API -%%==================================================================== -%%-------------------------------------------------------------------- -%% Function: start_link() -> {ok,Pid} | ignore | {error,Error} -%% Description: Starts the server -%%-------------------------------------------------------------------- -start_link(Module, SockMod, Socket, Opts, Receiver) -> - gen_server:start_link(?MODULE, - [Module, SockMod, Socket, Opts, Receiver], []). - -start(Module, SockMod, Socket, Opts) -> - case Module:socket_type() of - xml_stream -> - MaxStanzaSize = case lists:keysearch(max_stanza_size, 1, - Opts) - of - {value, {_, Size}} -> Size; - _ -> infinity - end, - Receiver = ejabberd_receiver:start(Socket, SockMod, - none, MaxStanzaSize), - case SockMod:controlling_process(Socket, Receiver) of - ok -> ok; - {error, _Reason} -> SockMod:close(Socket) - end, - supervisor:start_child(ejabberd_frontend_socket_sup, - [Module, SockMod, Socket, Opts, Receiver]); - raw -> - %{ok, Pid} = Module:start({SockMod, Socket}, Opts), - %case SockMod:controlling_process(Socket, Pid) of - % ok -> - % ok; - % {error, _Reason} -> - % SockMod:close(Socket) - %end - todo - end. - -starttls(FsmRef, _TLSOpts) -> - %% TODO: Frontend improvements planned by Aleksey - %%gen_server:call(FsmRef, {starttls, TLSOpts}), - FsmRef. - -starttls(FsmRef, TLSOpts, Data) -> - gen_server:call(FsmRef, {starttls, TLSOpts, Data}), - FsmRef. - -compress(FsmRef) -> compress(FsmRef, undefined). - -compress(FsmRef, Data) -> - gen_server:call(FsmRef, {compress, Data}), FsmRef. - -reset_stream(FsmRef) -> - gen_server:call(FsmRef, reset_stream). - -send(FsmRef, Data) -> - gen_server:call(FsmRef, {send, Data}). - -change_shaper(FsmRef, Shaper) -> - gen_server:call(FsmRef, {change_shaper, Shaper}). - -monitor(FsmRef) -> erlang:monitor(process, FsmRef). - -get_sockmod(FsmRef) -> - gen_server:call(FsmRef, get_sockmod). - -get_peer_certificate(FsmRef) -> - gen_server:call(FsmRef, get_peer_certificate). - -get_verify_result(FsmRef) -> - gen_server:call(FsmRef, get_verify_result). - -close(FsmRef) -> gen_server:call(FsmRef, close). - -sockname(FsmRef) -> gen_server:call(FsmRef, sockname). - -peername(_FsmRef) -> - %% TODO: Frontend improvements planned by Aleksey - %%gen_server:call(FsmRef, peername). - {ok, {{0, 0, 0, 0}, 0}}. - - -%%==================================================================== -%% gen_server callbacks -%%==================================================================== - -%%-------------------------------------------------------------------- -%% Function: init(Args) -> {ok, State} | -%% {ok, State, Timeout} | -%% ignore | -%% {stop, Reason} -%% Description: Initiates the server -%%-------------------------------------------------------------------- -init([Module, SockMod, Socket, Opts, Receiver]) -> - Node = ejabberd_node_groups:get_closest_node(backend), - {SockMod2, Socket2} = check_starttls(SockMod, Socket, Receiver, Opts), - {ok, Pid} = - rpc:call(Node, Module, start, [{?MODULE, self()}, Opts]), - ejabberd_receiver:become_controller(Receiver, Pid), - {ok, #state{sockmod = SockMod2, - socket = Socket2, - receiver = Receiver}}. - -%%-------------------------------------------------------------------- -%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | -%% {reply, Reply, State, Timeout} | -%% {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, Reply, State} | -%% {stop, Reason, State} -%% Description: Handling call messages -%%-------------------------------------------------------------------- -handle_call({starttls, TLSOpts}, _From, State) -> - {ok, TLSSocket} = p1_tls:tcp_to_tls(State#state.socket, TLSOpts), - ejabberd_receiver:starttls(State#state.receiver, TLSSocket), - Reply = ok, - {reply, Reply, State#state{socket = TLSSocket, sockmod = p1_tls}, - ?HIBERNATE_TIMEOUT}; - -handle_call({starttls, TLSOpts, Data}, _From, State) -> - {ok, TLSSocket} = p1_tls:tcp_to_tls(State#state.socket, TLSOpts), - ejabberd_receiver:starttls(State#state.receiver, TLSSocket), - catch (State#state.sockmod):send( - State#state.socket, Data), - Reply = ok, - {reply, Reply, - State#state{socket = TLSSocket, sockmod = p1_tls}, - ?HIBERNATE_TIMEOUT}; - -handle_call({compress, Data}, _From, State) -> - {ok, ZlibSocket} = - ejabberd_receiver:compress(State#state.receiver, Data), - Reply = ok, - {reply, Reply, - State#state{socket = ZlibSocket, sockmod = ezlib}, - ?HIBERNATE_TIMEOUT}; -handle_call(reset_stream, _From, State) -> - ejabberd_receiver:reset_stream(State#state.receiver), - Reply = ok, - {reply, Reply, State, ?HIBERNATE_TIMEOUT}; -handle_call({send, Data}, _From, State) -> - catch (State#state.sockmod):send(State#state.socket, Data), - Reply = ok, - {reply, Reply, State, ?HIBERNATE_TIMEOUT}; -handle_call({change_shaper, Shaper}, _From, State) -> - ejabberd_receiver:change_shaper(State#state.receiver, - Shaper), - Reply = ok, - {reply, Reply, State, ?HIBERNATE_TIMEOUT}; -handle_call(get_sockmod, _From, State) -> - Reply = State#state.sockmod, - {reply, Reply, State, ?HIBERNATE_TIMEOUT}; -handle_call(get_peer_certificate, _From, State) -> - Reply = p1_tls:get_peer_certificate(State#state.socket), - {reply, Reply, State, ?HIBERNATE_TIMEOUT}; -handle_call(get_verify_result, _From, State) -> - Reply = p1_tls:get_verify_result(State#state.socket), - {reply, Reply, State, ?HIBERNATE_TIMEOUT}; -handle_call(close, _From, State) -> - ejabberd_receiver:close(State#state.receiver), - Reply = ok, - {stop, normal, Reply, State}; -handle_call(sockname, _From, State) -> - #state{sockmod = SockMod, socket = Socket} = State, - Reply = - case SockMod of - gen_tcp -> - inet:sockname(Socket); - _ -> - SockMod:sockname(Socket) - end, - {reply, Reply, State, ?HIBERNATE_TIMEOUT}; -handle_call(peername, _From, State) -> - #state{sockmod = SockMod, socket = Socket} = State, - Reply = case SockMod of - gen_tcp -> inet:peername(Socket); - _ -> SockMod:peername(Socket) - end, - {reply, Reply, State, ?HIBERNATE_TIMEOUT}; -handle_call(_Request, _From, State) -> - Reply = ok, {reply, Reply, State, ?HIBERNATE_TIMEOUT}. - -%%-------------------------------------------------------------------- -%% Function: handle_cast(Msg, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} -%% Description: Handling cast messages -%%-------------------------------------------------------------------- -handle_cast(_Msg, State) -> - {noreply, State, ?HIBERNATE_TIMEOUT}. - -%%-------------------------------------------------------------------- -%% Function: handle_info(Info, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} -%% Description: Handling all non call/cast messages -%%-------------------------------------------------------------------- -handle_info(timeout, State) -> - proc_lib:hibernate(gen_server, enter_loop, - [?MODULE, [], State]), - {noreply, State, ?HIBERNATE_TIMEOUT}; -handle_info(_Info, State) -> - {noreply, State, ?HIBERNATE_TIMEOUT}. - -%%-------------------------------------------------------------------- -%% Function: terminate(Reason, State) -> void() -%% Description: This function is called by a gen_server when it is about to -%% terminate. It should be the opposite of Module:init/1 and do any necessary -%% cleaning up. When it returns, the gen_server terminates with Reason. -%% The return value is ignored. -%%-------------------------------------------------------------------- -terminate(_Reason, _State) -> ok. - -%%-------------------------------------------------------------------- -%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} -%% Description: Convert process state when code is changed -%%-------------------------------------------------------------------- -code_change(_OldVsn, State, _Extra) -> {ok, State}. - -%%-------------------------------------------------------------------- -%%% Internal functions -%%-------------------------------------------------------------------- -check_starttls(SockMod, Socket, Receiver, Opts) -> - TLSEnabled = proplists:get_bool(tls, Opts), - TLSOpts = lists:filter(fun({certfile, _}) -> true; - (_) -> false - end, Opts), - if - TLSEnabled -> - {ok, TLSSocket} = p1_tls:tcp_to_tls(Socket, TLSOpts), - ejabberd_receiver:starttls(Receiver, TLSSocket), - {p1_tls, TLSSocket}; - true -> - {SockMod, Socket} - end. diff --git a/src/ejabberd_hooks.erl b/src/ejabberd_hooks.erl index c1cdefcb2..8f378aa48 100644 --- a/src/ejabberd_hooks.erl +++ b/src/ejabberd_hooks.erl @@ -5,7 +5,7 @@ %%% Created : 8 Aug 2004 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -22,29 +22,26 @@ %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- - -module(ejabberd_hooks). -author('alexey@process-one.net'). - -behaviour(gen_server). %% External exports -export([start_link/0, add/3, add/4, - add_dist/5, + add/5, delete/3, delete/4, - delete_dist/5, - run/2, - run_fold/3, - add/5, - add_dist/6, delete/5, - delete_dist/6, + subscribe/4, + subscribe/5, + unsubscribe/4, + unsubscribe/5, + run/2, run/3, + run_fold/3, run_fold/4]). - %% gen_server callbacks -export([init/1, handle_call/3, @@ -53,27 +50,37 @@ handle_info/2, terminate/2]). --include("ejabberd.hrl"). + +-export( + [ + get_tracing_options/3, + trace_off/3, + trace_on/5,human_readable_time_string/1 + ] +). + -include("logger.hrl"). -%% Timeout of 5 seconds in calls to distributed hooks --define(TIMEOUT_DISTRIBUTED_HOOK, 5000). -record(state, {}). +-type subscriber() :: {Module :: atom(), Function :: atom(), InitArg :: any()}. +-type subscriber_event() :: before | 'after' | before_callback | after_callback. +-type hook() :: {Seq :: integer(), Module :: atom(), Function :: atom() | fun()}. +-define(TRACE_HOOK_KEY, '$trace_hook'). +-define(TIMING_KEY, '$trace_hook_timer'). %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- start_link() -> - gen_server:start_link({local, ejabberd_hooks}, ejabberd_hooks, [], []). - --spec add(atom(), fun(), number()) -> any(). + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). +-spec add(atom(), fun(), integer()) -> ok. %% @doc See add/4. add(Hook, Function, Seq) when is_function(Function) -> add(Hook, global, undefined, Function, Seq). --spec add(atom(), binary() | atom(), fun() | atom() , number()) -> any(). +-spec add(atom(), HostOrModule :: binary() | atom(), fun() | atom(), integer()) -> ok. add(Hook, Host, Function, Seq) when is_function(Function) -> add(Hook, Host, undefined, Function, Seq); @@ -82,29 +89,44 @@ add(Hook, Host, Function, Seq) when is_function(Function) -> add(Hook, Module, Function, Seq) -> add(Hook, global, Module, Function, Seq). --spec add(atom(), binary() | global, atom(), atom() | fun(), number()) -> any(). - +-spec add(atom(), binary() | global, atom(), atom() | fun(), integer()) -> ok. add(Hook, Host, Module, Function, Seq) -> - gen_server:call(ejabberd_hooks, {add, Hook, Host, Module, Function, Seq}). + gen_server:call(?MODULE, {add, Hook, Host, Module, Function, Seq}). --spec add_dist(atom(), atom(), atom(), atom() | fun(), number()) -> any(). +-spec subscribe(atom(), atom(), atom(), any()) -> ok. +%% @doc Add a subscriber to this hook. +%% +%% Before running any hook callback, the subscriber will be called in form of +%% Module:Function(InitArg, 'before', Host :: binary() | global, Hook, HookArgs) +%% Above function should return new state. +%% +%% Before running each callback, the subscriber will be called in form of +%% Module:Function(State, 'before_callback', Host :: binary() | global, Hook, {CallbackMod, CallbackArg, Seq, HookArgs}) +%% Above function should return new state. +%% +%% After running each callback, the subscriber will be called in form of +%% Module:Function(State, 'after_callback', Host :: binary() | global, Hook, {CallbackMod, CallbackArg, Seq, HookArgs}) +%% Above function should return new state. +%% +%% After running any hook callback, the subscriber will be called in form of +%% Module:Function(State, 'after', Host :: binary() | global, Hook, HookArgs) +%% Return value of this function call will be dropped. +%% +%% For every ejabberd_hooks:[run|run_fold] for every subscriber above functions will be called and the hook runner +%% maintains State in above four calls. +subscribe(Hook, Module, Function, InitArg) -> + subscribe(Hook, global, Module, Function, InitArg). -add_dist(Hook, Node, Module, Function, Seq) -> - gen_server:call(ejabberd_hooks, {add, Hook, global, Node, Module, Function, Seq}). - --spec add_dist(atom(), binary() | global, atom(), atom(), atom() | fun(), number()) -> any(). - -add_dist(Hook, Host, Node, Module, Function, Seq) -> - gen_server:call(ejabberd_hooks, {add, Hook, Host, Node, Module, Function, Seq}). - --spec delete(atom(), fun(), number()) -> ok. +-spec subscribe(atom(), binary() | global, atom(), atom(), any()) -> ok. +subscribe(Hook, Host, Module, Function, InitArg) -> + gen_server:call(?MODULE, {subscribe, Hook, Host, Module, Function, InitArg}). +-spec delete(atom(), fun(), integer()) -> ok. %% @doc See del/4. delete(Hook, Function, Seq) when is_function(Function) -> delete(Hook, global, undefined, Function, Seq). --spec delete(atom(), binary() | atom(), atom() | fun(), number()) -> ok. - +-spec delete(atom(), binary() | atom(), atom() | fun(), integer()) -> ok. delete(Hook, Host, Function, Seq) when is_function(Function) -> delete(Hook, Host, undefined, Function, Seq); @@ -113,207 +135,260 @@ delete(Hook, Host, Function, Seq) when is_function(Function) -> delete(Hook, Module, Function, Seq) -> delete(Hook, global, Module, Function, Seq). --spec delete(atom(), binary() | global, atom(), atom() | fun(), number()) -> ok. - +-spec delete(atom(), binary() | global, atom(), atom() | fun(), integer()) -> ok. delete(Hook, Host, Module, Function, Seq) -> - gen_server:call(ejabberd_hooks, {delete, Hook, Host, Module, Function, Seq}). + gen_server:call(?MODULE, {delete, Hook, Host, Module, Function, Seq}). --spec delete_dist(atom(), atom(), atom(), atom() | fun(), number()) -> ok. -delete_dist(Hook, Node, Module, Function, Seq) -> - delete_dist(Hook, global, Node, Module, Function, Seq). --spec delete_dist(atom(), binary() | global, atom(), atom(), atom() | fun(), number()) -> ok. +-spec unsubscribe(atom(), atom(), atom(), any()) -> ok. +%% @doc Removes a subscriber from this hook. +unsubscribe(Hook, Module, Function, InitArg) -> + unsubscribe(Hook, global, Module, Function, InitArg). + +-spec unsubscribe(atom(), binary() | global, atom(), atom(), any()) -> ok. +unsubscribe(Hook, Host, Module, Function, InitArg) -> + gen_server:call(?MODULE, {unsubscribe, Hook, Host, Module, Function, InitArg}). -delete_dist(Hook, Host, Node, Module, Function, Seq) -> - gen_server:call(ejabberd_hooks, {delete, Hook, Host, Node, Module, Function, Seq}). -spec run(atom(), list()) -> ok. - -%% @doc Run the calls of this hook in order, don't care about function results. +%% @doc Run the calls (and subscribers) of this hook in order, don't care about function results. %% If a call returns stop, no more calls are performed. run(Hook, Args) -> run(Hook, global, Args). -spec run(atom(), binary() | global, list()) -> ok. - run(Hook, Host, Args) -> - case ets:lookup(hooks, {Hook, Host}) of - [{_, Ls}] -> - run1(Ls, Hook, Args); + try ets:lookup(hooks, {Hook, Host}) of + [{_, Ls, Subs}] -> + case erlang:get(?TRACE_HOOK_KEY) of + undefined when Subs == [] -> + run1(Ls, Hook, Args); + undefined -> + Subs2 = call_subscriber_list(Subs, Host, Hook, Args, before, []), + Subs3 = run1(Ls, Hook, Args, Host, Subs2), + _Subs4 = call_subscriber_list(Subs3, Host, Hook, Args, 'after', []), + ok; + TracingHooksOpts -> + case do_get_tracing_options(Hook, Host, TracingHooksOpts) of + undefined -> + Subs2 = call_subscriber_list(Subs, Host, Hook, Args, before, []), + Subs3 = run1(Ls, Hook, Args, Host, Subs2), + _Subs4 = call_subscriber_list(Subs3, Host, Hook, Args, 'after', []), + ok; + TracingOpts -> + foreach_start_hook_tracing(TracingOpts, Hook, Host, Args), + Subs2 = call_subscriber_list(Subs, Host, Hook, Args, before, []), + Subs3 = run2(Ls, Hook, Args, Host, TracingOpts, Subs2), + _Subs4 = call_subscriber_list(Subs3, Host, Hook, Args, 'after', []), + ok + end + end; [] -> ok + catch _:badarg -> + ok end. --spec run_fold(atom(), any(), list()) -> any(). - -%% @doc Run the calls of this hook in order. +-spec run_fold(atom(), T, list()) -> T. +%% @doc Run the calls (and subscribers) of this hook in order. %% The arguments passed to the function are: [Val | Args]. %% The result of a call is used as Val for the next call. -%% If a call returns 'stop', no more calls are performed and 'stopped' is returned. +%% If a call returns 'stop', no more calls are performed. %% If a call returns {stop, NewVal}, no more calls are performed and NewVal is returned. run_fold(Hook, Val, Args) -> run_fold(Hook, global, Val, Args). --spec run_fold(atom(), binary() | global, any(), list()) -> any(). - +-spec run_fold(atom(), binary() | global, T, list()) -> T. run_fold(Hook, Host, Val, Args) -> - case ets:lookup(hooks, {Hook, Host}) of - [{_, Ls}] -> - run_fold1(Ls, Hook, Val, Args); + try ets:lookup(hooks, {Hook, Host}) of + [{_, Ls, Subs}] -> + case erlang:get(?TRACE_HOOK_KEY) of + undefined when Subs == [] -> + run_fold1(Ls, Hook, Val, Args); + undefined -> + Subs2 = call_subscriber_list(Subs, Host, Hook, [Val | Args], before, []), + {Val2, Subs3} = run_fold1(Ls, Hook, Val, Args, Host, Subs2), + _Subs4 = call_subscriber_list(Subs3, Host, Hook, [Val2 | Args], 'after', []), + Val2; + TracingHooksOpts -> + case do_get_tracing_options(Hook, Host, TracingHooksOpts) of + undefined -> + Subs2 = call_subscriber_list(Subs, Host, Hook, [Val | Args], before, []), + {Val2, Subs3} = run_fold1(Ls, Hook, Val, Args, Host, Subs2), + _Subs4 = call_subscriber_list(Subs3, Host, Hook, [Val2 | Args], 'after', []), + Val2; + TracingOpts -> + fold_start_hook_tracing(TracingOpts, Hook, Host, [Val | Args]), + Subs2 = call_subscriber_list(Subs, Host, Hook, [Val | Args], before, []), + {Val2, Subs3} = run_fold2(Ls, Hook, Val, Args, Host, TracingOpts, Subs2), + _Subs4 = call_subscriber_list(Subs3, Host, Hook, [Val2 | Args], 'after', []), + Val2 + end + end; [] -> Val + catch _:badarg -> + Val + end. + +get_tracing_options(Hook, Host, Pid) when Pid == erlang:self() -> + do_get_tracing_options(Hook, Host, erlang:get(?TRACE_HOOK_KEY)); +get_tracing_options(Hook, Host, Pid) when erlang:is_pid(Pid) -> + case erlang:process_info(Pid, dictionary) of + {_, DictPropList} -> + case lists:keyfind(?TRACE_HOOK_KEY, 1, DictPropList) of + {_, TracingHooksOpts} -> + do_get_tracing_options(Hook, Host, TracingHooksOpts); + _ -> + undefined + end; + _ -> + undefined + end. + +trace_on(Hook, Host, Pid, #{}=Opts, Timeout) when Pid == erlang:self() -> + do_trace_on(Hook, Host, Opts, Timeout); +trace_on(Hook, Host, Proc, #{}=Opts, Timeout) -> + try sys:replace_state( + Proc, + fun(State) -> + do_trace_on(Hook, Host, Opts, Timeout), + State + end, + 15000 + ) of + _ -> % process state + ok + catch + _:Reason -> + {error, Reason} + end. + +trace_off(Hook, Host, Pid) when Pid == erlang:self() -> + do_trace_off(Hook, Host); +trace_off(Hook, Host, Proc) -> + try sys:replace_state( + Proc, + fun(State) -> + do_trace_off(Hook, Host), + State + end, + 15000 + ) of + _ -> % process state + ok + catch + _:Reason -> + {error, Reason} end. %%%---------------------------------------------------------------------- %%% Callback functions from gen_server %%%---------------------------------------------------------------------- - -%%---------------------------------------------------------------------- -%% Func: init/1 -%% Returns: {ok, State} | -%% {ok, State, Timeout} | -%% ignore | -%% {stop, Reason} -%%---------------------------------------------------------------------- init([]) -> - ets:new(hooks, [named_table]), + _ = ets:new(hooks, [named_table, {read_concurrency, true}]), {ok, #state{}}. -%%---------------------------------------------------------------------- -%% Func: handle_call/3 -%% Returns: {reply, Reply, State} | -%% {reply, Reply, State, Timeout} | -%% {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, Reply, State} | (terminate/2 is called) -%% {stop, Reason, State} (terminate/2 is called) -%%---------------------------------------------------------------------- handle_call({add, Hook, Host, Module, Function, Seq}, _From, State) -> - Reply = case ets:lookup(hooks, {Hook, Host}) of - [{_, Ls}] -> - El = {Seq, Module, Function}, - case lists:member(El, Ls) of - true -> - ok; - false -> - NewLs = lists:merge(Ls, [El]), - ets:insert(hooks, {{Hook, Host}, NewLs}), - ok - end; - [] -> - NewLs = [{Seq, Module, Function}], - ets:insert(hooks, {{Hook, Host}, NewLs}), - ok - end, - {reply, Reply, State}; -handle_call({add, Hook, Host, Node, Module, Function, Seq}, _From, State) -> - Reply = case ets:lookup(hooks, {Hook, Host}) of - [{_, Ls}] -> - El = {Seq, Node, Module, Function}, - case lists:member(El, Ls) of - true -> - ok; - false -> - NewLs = lists:merge(Ls, [El]), - ets:insert(hooks, {{Hook, Host}, NewLs}), - ok - end; - [] -> - NewLs = [{Seq, Node, Module, Function}], - ets:insert(hooks, {{Hook, Host}, NewLs}), - ok - end, + HookFormat = {Seq, Module, Function}, + Reply = handle_add(Hook, Host, HookFormat), {reply, Reply, State}; handle_call({delete, Hook, Host, Module, Function, Seq}, _From, State) -> - Reply = case ets:lookup(hooks, {Hook, Host}) of - [{_, Ls}] -> - NewLs = lists:delete({Seq, Module, Function}, Ls), - ets:insert(hooks, {{Hook, Host}, NewLs}), - ok; - [] -> - ok - end, + HookFormat = {Seq, Module, Function}, + Reply = handle_delete(Hook, Host, HookFormat), {reply, Reply, State}; -handle_call({delete, Hook, Host, Node, Module, Function, Seq}, _From, State) -> - Reply = case ets:lookup(hooks, {Hook, Host}) of - [{_, Ls}] -> - NewLs = lists:delete({Seq, Node, Module, Function}, Ls), - ets:insert(hooks, {{Hook, Host}, NewLs}), - ok; - [] -> - ok - end, +handle_call({subscribe, Hook, Host, Module, Function, InitArg}, _From, State) -> + SubscriberFormat = {Module, Function, InitArg}, + Reply = handle_subscribe(Hook, Host, SubscriberFormat), {reply, Reply, State}; -handle_call(_Request, _From, State) -> - Reply = ok, - {reply, Reply, State}. - -%%---------------------------------------------------------------------- -%% Func: handle_cast/2 -%% Returns: {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} (terminate/2 is called) -%%---------------------------------------------------------------------- -handle_cast(_Msg, State) -> +handle_call({unsubscribe, Hook, Host, Module, Function, InitArg}, _From, State) -> + SubscriberFormat = {Module, Function, InitArg}, + Reply = handle_unsubscribe(Hook, Host, SubscriberFormat), + {reply, Reply, State}; +handle_call(Request, From, State) -> + ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. -%%---------------------------------------------------------------------- -%% Func: handle_info/2 -%% Returns: {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} (terminate/2 is called) -%%---------------------------------------------------------------------- -handle_info(_Info, State) -> +-spec handle_add(atom(), atom(), hook()) -> ok. +handle_add(Hook, Host, El) -> + case ets:lookup(hooks, {Hook, Host}) of + [{_, Ls, Subs}] -> + case lists:member(El, Ls) of + true -> + ok; + false -> + NewLs = lists:merge(Ls, [El]), + ets:insert(hooks, {{Hook, Host}, NewLs, Subs}), + ok + end; + [] -> + NewLs = [El], + ets:insert(hooks, {{Hook, Host}, NewLs, []}), + ok + end. + +-spec handle_delete(atom(), atom(), hook()) -> ok. +handle_delete(Hook, Host, El) -> + case ets:lookup(hooks, {Hook, Host}) of + [{_, Ls, Subs}] -> + NewLs = lists:delete(El, Ls), + ets:insert(hooks, {{Hook, Host}, NewLs, Subs}), + ok; + [] -> + ok + end. + +-spec handle_subscribe(atom(), atom(), subscriber()) -> ok. +handle_subscribe(Hook, Host, El) -> + case ets:lookup(hooks, {Hook, Host}) of + [{_, Ls, Subs}] -> + case lists:member(El, Subs) of + true -> + ok; + false -> + ets:insert(hooks, {{Hook, Host}, Ls, Subs ++ [El]}), + ok + end; + [] -> + ets:insert(hooks, {{Hook, Host}, [], [El]}), + ok + end. + +-spec handle_unsubscribe(atom(), atom(), subscriber()) -> ok. +handle_unsubscribe(Hook, Host, El) -> + case ets:lookup(hooks, {Hook, Host}) of + [{_, Ls, Subs}] -> + ets:insert(hooks, {{Hook, Host}, Ls, lists:delete(El, Subs)}), + ok; + [] -> + ok + end. + +handle_cast(Msg, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Msg]), + {noreply, State}. + +handle_info(Info, State) -> + ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. -%%---------------------------------------------------------------------- -%% Func: terminate/2 -%% Purpose: Shutdown the server -%% Returns: any (ignored by gen_server) -%%---------------------------------------------------------------------- terminate(_Reason, _State) -> ok. - code_change(_OldVsn, State, _Extra) -> {ok, State}. %%%---------------------------------------------------------------------- %%% Internal functions %%%---------------------------------------------------------------------- - +-spec run1([hook()], atom(), list()) -> ok. run1([], _Hook, _Args) -> ok; -run1([{_Seq, Node, Module, Function} | Ls], Hook, Args) -> - case rpc:call(Node, Module, Function, Args, ?TIMEOUT_DISTRIBUTED_HOOK) of - timeout -> - ?ERROR_MSG("Timeout on RPC to ~p~nrunning hook: ~p", - [Node, {Hook, Args}]), - run1(Ls, Hook, Args); - {badrpc, Reason} -> - ?ERROR_MSG("Bad RPC error to ~p: ~p~nrunning hook: ~p", - [Node, Reason, {Hook, Args}]), - run1(Ls, Hook, Args); - stop -> - ?INFO_MSG("~nThe process ~p in node ~p ran a hook in node ~p.~n" - "Stop.", [self(), node(), Node]), % debug code - ok; - Res -> - ?INFO_MSG("~nThe process ~p in node ~p ran a hook in node ~p.~n" - "The response is:~n~s", [self(), node(), Node, Res]), % debug code - run1(Ls, Hook, Args) - end; run1([{_Seq, Module, Function} | Ls], Hook, Args) -> - Res = if is_function(Function) -> - catch apply(Function, Args); - true -> - catch apply(Module, Function, Args) - end, + Res = safe_apply(Hook, Module, Function, Args), case Res of - {'EXIT', Reason} -> - ?ERROR_MSG("~p~nrunning hook: ~p", - [Reason, {Hook, Args}]), + 'EXIT' -> run1(Ls, Hook, Args); stop -> ok; @@ -321,45 +396,527 @@ run1([{_Seq, Module, Function} | Ls], Hook, Args) -> run1(Ls, Hook, Args) end. - +-spec run_fold1([hook()], atom(), T, list()) -> T. run_fold1([], _Hook, Val, _Args) -> Val; -run_fold1([{_Seq, Node, Module, Function} | Ls], Hook, Val, Args) -> - case rpc:call(Node, Module, Function, [Val | Args], ?TIMEOUT_DISTRIBUTED_HOOK) of - {badrpc, Reason} -> - ?ERROR_MSG("Bad RPC error to ~p: ~p~nrunning hook: ~p", - [Node, Reason, {Hook, Args}]), - run_fold1(Ls, Hook, Val, Args); - timeout -> - ?ERROR_MSG("Timeout on RPC to ~p~nrunning hook: ~p", - [Node, {Hook, Args}]), - run_fold1(Ls, Hook, Val, Args); - stop -> - stopped; - {stop, NewVal} -> - ?INFO_MSG("~nThe process ~p in node ~p ran a hook in node ~p.~n" - "Stop, and the NewVal is:~n~p", [self(), node(), Node, NewVal]), % debug code - NewVal; - NewVal -> - ?INFO_MSG("~nThe process ~p in node ~p ran a hook in node ~p.~n" - "The NewVal is:~n~p", [self(), node(), Node, NewVal]), % debug code - run_fold1(Ls, Hook, NewVal, Args) - end; run_fold1([{_Seq, Module, Function} | Ls], Hook, Val, Args) -> - Res = if is_function(Function) -> - catch apply(Function, [Val | Args]); - true -> - catch apply(Module, Function, [Val | Args]) - end, + Res = safe_apply(Hook, Module, Function, [Val | Args]), case Res of - {'EXIT', Reason} -> - ?ERROR_MSG("~p~nrunning hook: ~p", - [Reason, {Hook, Args}]), + 'EXIT' -> run_fold1(Ls, Hook, Val, Args); stop -> - stopped; + Val; {stop, NewVal} -> NewVal; NewVal -> run_fold1(Ls, Hook, NewVal, Args) end. + +-spec run1([hook()], atom(), list(), binary() | global, [subscriber()]) -> [subscriber()]. +run1([], _Hook, _Args, _Host, SubscriberList) -> + SubscriberList; +run1([{Seq, Module, Function} | Ls], Hook, Args, Host, SubscriberList) -> + SubscriberList2 = call_subscriber_list(SubscriberList, Host, Hook, {Module, Function, Seq, Args}, before_callback, []), + Res = safe_apply(Hook, Module, Function, Args), + SubscriberList3 = call_subscriber_list(SubscriberList2, Host, Hook, {Module, Function, Seq, Args}, after_callback, []), + case Res of + 'EXIT' -> + run1(Ls, Hook, Args, Host, SubscriberList3); + stop -> + SubscriberList3; + _ -> + run1(Ls, Hook, Args, Host, SubscriberList3) + end. + +-spec run_fold1([hook()], atom(), T, list(), binary() | global, [subscriber()]) -> {T, [subscriber()]}. +run_fold1([], _Hook, Val, _Args, _Host, SubscriberList) -> + {Val, SubscriberList}; +run_fold1([{Seq, Module, Function} | Ls], Hook, Val, Args, Host, SubscriberList) -> + SubscriberList2 = call_subscriber_list(SubscriberList, Host, Hook, {Module, Function, Seq, [Val | Args]}, before_callback, []), + Res = safe_apply(Hook, Module, Function, [Val | Args]), + SubscriberList3 = call_subscriber_list(SubscriberList2, Host, Hook, {Module, Function, Seq, [Val | Args]}, after_callback, []), + case Res of + 'EXIT' -> + run_fold1(Ls, Hook, Val, Args, Host, SubscriberList3); + stop -> + {Val, SubscriberList3}; + {stop, NewVal} -> + {NewVal, SubscriberList3}; + NewVal -> + run_fold1(Ls, Hook, NewVal, Args, Host, SubscriberList3) + end. + +-spec safe_apply(atom(), atom(), atom() | fun(), list()) -> any(). +safe_apply(Hook, Module, Function, Args) -> + ?DEBUG("Running hook ~p: ~p:~p/~B", + [Hook, Module, Function, length(Args)]), + try if is_function(Function) -> + apply(Function, Args); + true -> + apply(Module, Function, Args) + end + catch + E:R:Stack when E /= exit; R /= normal -> + ?ERROR_MSG("Hook ~p crashed when running ~p:~p/~p:~n" ++ + string:join( + ["** ~ts" | [ "** Arg " ++ integer_to_list(I) ++ " = ~p" + || I <- lists:seq(1, length(Args)) ]], + "~n"), + [Hook, + Module, + Function, + length(Args), + misc:format_exception(2, E, R, Stack) | Args]), + 'EXIT' + end. + +-spec call_subscriber_list([subscriber()], binary() | global, atom(), {atom(), atom(), integer(), list()} | list(), subscriber_event(), [subscriber()]) -> any(). +call_subscriber_list([], _Host, _Hook, _CallbackOrArgs, _Event, []) -> + []; +call_subscriber_list([], _Host, _Hook, _CallbackOrArgs, _Event, Result) -> + lists:reverse(Result); +call_subscriber_list([{Mod, Func, InitArg} | SubscriberList], Host, Hook, CallbackOrArgs, Event, Result) -> + SubscriberArgs = [InitArg, Event, Host, Hook, CallbackOrArgs], + ?DEBUG("Running hook subscriber ~p: ~p:~p/~B with event ~p", + [Hook, Mod, Func, length(SubscriberArgs), Event]), + try apply(Mod, Func, SubscriberArgs) of + State -> + call_subscriber_list(SubscriberList, Host, Hook, CallbackOrArgs, Event, [{Mod, Func, State} | Result]) + catch + E:R:Stack when E /= exit; R /= normal -> + ?ERROR_MSG("Hook subscriber ~p crashed when running ~p:~p/~p:~n" ++ + string:join( + ["** ~ts" | [ "** Arg " ++ integer_to_list(I) ++ " = ~p" + || I <- lists:seq(1, length(SubscriberArgs)) ]], + "~n"), + [Hook, + Mod, + Func, + length(SubscriberArgs), + misc:format_exception(2, E, R, Stack) | SubscriberArgs]), + %% Do not append subscriber for next calls: + call_subscriber_list(SubscriberList, Host, Hook, CallbackOrArgs, Event, Result) + end. + +%%%---------------------------------------------------------------------- +%%% Internal tracing functions +%%%---------------------------------------------------------------------- + +do_trace_on(Hook, Host, Opts, Timeout) when erlang:is_list(Host) -> + do_trace_on(Hook, erlang:list_to_binary(Host), Opts, Timeout); +do_trace_on(Hook, Host, Opts, undefined) -> + case erlang:get(?TRACE_HOOK_KEY) of + _ when Hook == all andalso Host == <<"*">> -> + % Trace everything: + erlang:put(?TRACE_HOOK_KEY, #{all => #{<<"*">> => Opts}}); + #{all := #{<<"*">> := _}} -> % Already tracing everything + % Update Opts: + erlang:put(?TRACE_HOOK_KEY, #{all => #{<<"*">> => Opts}}); + #{all := HostOpts} when Hook == all -> % Already Tracing everything for some hosts + % Add/Update Host and Opts: + erlang:put(?TRACE_HOOK_KEY, #{all => HostOpts#{Host => Opts}}); + #{all := _} -> % Already tracing everything and Hook is not all + ok; + #{} when Hook == all -> + % Remove other hooks by just adding all: + erlang:put(?TRACE_HOOK_KEY, #{all => #{Host => Opts}}); + #{}=TraceHooksOpts when Host == <<"*">> -> % Want to trace a hook for all hosts + erlang:put(?TRACE_HOOK_KEY, TraceHooksOpts#{Hook => #{Host => Opts}}); + #{}=TraceHooksOpts -> + case maps:get(Hook, TraceHooksOpts, #{}) of + #{<<"*">> := _} -> % Already tracing this hook for all hosts + ok; + HostOpts -> + erlang:put(?TRACE_HOOK_KEY, TraceHooksOpts#{Hook => HostOpts#{Host => Opts}}) + end; + undefined -> + erlang:put(?TRACE_HOOK_KEY, #{Hook => #{Host => Opts}}) + end, + ok; +do_trace_on(Hook, Host, Opts, TimeoutSeconds) -> % Trace myself `Timeout` time + Timeout = timer:seconds(TimeoutSeconds), + ParentPid = erlang:self(), + try erlang:spawn( + fun() -> + MonitorRef = erlang:monitor(process, ParentPid), + receive + {_, MonitorRef, _, _, _} -> + ok + after Timeout -> + trace_off(Hook, Host, ParentPid) + end, + erlang:exit(normal) + end + ) of + _ -> + do_trace_on(Hook, Host, Opts, undefined) % ok + catch + _:Reason -> % system_limit + {error, Reason} + end. + +do_trace_off(Hook, Host) when erlang:is_list(Host) -> + do_trace_off(Hook, erlang:list_to_binary(Host)); +do_trace_off(Hook, Host) -> + case erlang:get(?TRACE_HOOK_KEY) of + _ when Hook == all andalso Host == <<"*">> -> + % Remove all tracing: + erlang:erase(?TRACE_HOOK_KEY); + #{all := HostOpts} when Hook == all -> % Already tracing all hooks + % Remove Host: + HostOpts2 = maps:remove(Host, HostOpts), + if + HostOpts2 == #{} -> + % Remove all tracing: + erlang:erase(?TRACE_HOOK_KEY); + true -> + erlang:put(?TRACE_HOOK_KEY, #{all => HostOpts2}) + end; + #{}=TraceHooksOpts when Host == <<"*">> -> + % Remove tracing of this hook for all hosts: + TraceHooksOpts2 = maps:remove(Hook, TraceHooksOpts), + if + TraceHooksOpts2 == #{} -> + % Remove all tracing: + erlang:erase(?TRACE_HOOK_KEY); + true -> + erlang:put(?TRACE_HOOK_KEY, TraceHooksOpts2) + end; + #{}=TraceHooksOpts -> + case maps:get(Hook, TraceHooksOpts, undefined) of + #{}=HostOpts -> + NewHostOpts = maps:remove(Host, HostOpts), + if + NewHostOpts == #{} -> + % Remove hook: + erlang:put(?TRACE_HOOK_KEY, maps:remove(Hook, TraceHooksOpts)); + true -> + erlang:put(?TRACE_HOOK_KEY, TraceHooksOpts#{Hook => NewHostOpts}) + end; + _ -> + ok + end; + undefined -> + ok + end, + ok. + +do_get_tracing_options(Hook, Host, MaybeMap) -> + case MaybeMap of + undefined -> + undefined; + #{all := #{<<"*">> := Opts}} -> % Tracing everything + Opts; + #{all := HostOpts} -> % Tracing all hooks for some hosts + maps:get(Host, HostOpts, undefined); + #{}=TraceHooksOpts -> + HostOpts = maps:get(Hook, TraceHooksOpts, #{}), + case maps:get(Host, HostOpts, undefined) of + undefined -> + maps:get(<<"*">>, HostOpts, undefined); + Opts -> + Opts + end + end. + +run2([], Hook, Args, Host, Opts, SubscriberList) -> + foreach_stop_hook_tracing(Opts, Hook, Host, Args, undefined), + SubscriberList; +run2([{Seq, Module, Function} | Ls], Hook, Args, Host, TracingOpts, SubscriberList) -> + foreach_start_callback_tracing(TracingOpts, Hook, Host, Module, Function, Args, Seq), + SubscriberList2 = call_subscriber_list(SubscriberList, Host, Hook, {Module, Function, Seq, Args}, before_callback, []), + Res = safe_apply(Hook, Module, Function, Args), + SubscriberList3 = call_subscriber_list(SubscriberList2, Host, Hook, {Module, Function, Seq, Args}, after_callback, []), + foreach_stop_callback_tracing(TracingOpts, Hook, Host, Module, Function, Args, Seq, Res), + case Res of + 'EXIT' -> + run2(Ls, Hook, Args, Host, TracingOpts, SubscriberList3); + stop -> + foreach_stop_hook_tracing(TracingOpts, Hook, Host, Args, {Module, Function, Seq, Ls}), + SubscriberList3; + _ -> + run2(Ls, Hook, Args, Host, TracingOpts, SubscriberList3) + end. + +run_fold2([], Hook, Val, Args, Host, Opts, SubscriberList) -> + fold_stop_hook_tracing(Opts, Hook, Host, [Val | Args], undefined), + {Val, SubscriberList}; +run_fold2([{Seq, Module, Function} | Ls], Hook, Val, Args, Host, TracingOpts, SubscriberList) -> + fold_start_callback_tracing(TracingOpts, Hook, Host, Module, Function, [Val | Args], Seq), + SubscriberList2 = call_subscriber_list(SubscriberList, Host, Hook, {Module, Function, Seq, [Val | Args]}, before_callback, []), + Res = safe_apply(Hook, Module, Function, [Val | Args]), + SubscriberList3 = call_subscriber_list(SubscriberList2, Host, Hook, {Module, Function, Seq, [Val | Args]}, after_callback, []), + fold_stop_callback_tracing(TracingOpts, Hook, Host, Module, Function, [Val | Args], Seq, Res), + case Res of + 'EXIT' -> + run_fold2(Ls, Hook, Val, Args, Host, TracingOpts, SubscriberList3); + stop -> + fold_stop_hook_tracing(TracingOpts, Hook, Host, [Val | Args], {Module, Function, Seq, {old, Val}, Ls}), + {Val, SubscriberList3}; + {stop, NewVal} -> + fold_stop_hook_tracing(TracingOpts, Hook, Host, [Val | Args], {Module, Function, Seq, {new, NewVal}, Ls}), + {NewVal, SubscriberList3}; + NewVal -> + run_fold2(Ls, Hook, NewVal, Args, Host, TracingOpts, SubscriberList3) + end. + +foreach_start_hook_tracing(TracingOpts, Hook, Host, Args) -> + run_event_handlers(TracingOpts, Hook, Host, start_hook, [Args], foreach). + +foreach_stop_hook_tracing(TracingOpts, Hook, Host, Args, BreakCallback) -> + run_event_handlers(TracingOpts, Hook, Host, stop_hook, [Args, BreakCallback], foreach). + +foreach_start_callback_tracing(TracingOpts, Hook, Host, Mod, Func, Args, Seq) -> + run_event_handlers(TracingOpts, Hook, Host, start_callback, [Mod, Func, Args, Seq], foreach). + +foreach_stop_callback_tracing(TracingOpts, Hook, Host, Mod, Func, Args, Seq, Res) -> + run_event_handlers(TracingOpts, Hook, Host, stop_callback, [Mod, Func, Args, Seq, Res], foreach). + +fold_start_hook_tracing(TracingOpts, Hook, Host, Args) -> + run_event_handlers(TracingOpts, Hook, Host, start_hook, [Args], fold). + +fold_stop_hook_tracing(TracingOpts, Hook, Host, Args, BreakCallback) -> + run_event_handlers(TracingOpts, Hook, Host, stop_hook, [Args, BreakCallback], fold). + +fold_start_callback_tracing(TracingOpts, Hook, Host, Mod, Func, Args, Seq) -> + run_event_handlers(TracingOpts, Hook, Host, start_callback, [Mod, Func, Args, Seq], fold). + +fold_stop_callback_tracing(TracingOpts, Hook, Host, Mod, Func, Args, Seq, Res) -> + run_event_handlers(TracingOpts, Hook, Host, stop_callback, [Mod, Func, Args, Seq, Res], fold). + +run_event_handlers(TracingOpts, Hook, Host, Event, EventArgs, RunType) -> + EventHandlerList = maps:get(event_handler_list, TracingOpts, default_tracing_event_handler_list()), + EventHandlerOpts = maps:get(event_handler_options, TracingOpts, #{}), + if + erlang:is_list(EventHandlerList) -> + lists:foreach( + fun(EventHandler) -> + try + if + erlang:is_function(EventHandler) -> + erlang:apply( + EventHandler, + [Event, EventArgs, RunType, Hook, Host, EventHandlerOpts, TracingOpts] + ); + true -> + EventHandler:handle_hook_tracing_event( + Event, + EventArgs, + RunType, + Hook, + Host, + EventHandlerOpts, + TracingOpts + ) + end + of + _ -> + ok + catch + E:R:Stack -> + ?ERROR_MSG( + "(~0p|~ts|~0p) Tracing event '~0p' handler exception(~0p): ~0p: ~0p", + [Hook, Host, erlang:self(), EventHandler, E, R, Stack]), + ok + end + end, + EventHandlerList + ); % ok + true -> + ?ERROR_MSG("(~0p|~ts|~0p) Bad event handler list: ~0p", [Hook, Host, erlang:self(), EventHandlerList]), + ok + end. + +default_tracing_event_handler_list() -> + [fun tracing_timing_event_handler/7]. + +tracing_timing_event_handler(start_hook, EventArgs, RunType, Hook, Host, _, TracingOpts) -> + HookStart = erlang:system_time(nanosecond), + % Generate new event: + run_event_handlers(TracingOpts, Hook, Host, start_hook_timing, EventArgs ++ [HookStart], RunType); +tracing_timing_event_handler(stop_hook, EventArgs, RunType, Hook, Host, _, TracingOpts) -> + HookStop = erlang:system_time(nanosecond), + TimingMap = #{} = erlang:get(?TIMING_KEY), + {HookStart, CallbackList} = maps:get({Hook, Host}, TimingMap), + {CallbackListTiming, CallbackListTotal} = lists:foldl( + fun({_, _, _, CallbackStart, CallbackStop}=CallbackTimingInfo, {CallbackListTimingX, Total}) -> + {CallbackListTimingX ++ [CallbackTimingInfo], Total + (CallbackStop - CallbackStart)} + end, + {[], 0}, + CallbackList + ), + % Generate new event: + run_event_handlers( + TracingOpts, + Hook, + Host, + stop_hook_timing, + EventArgs ++ [HookStart, HookStop, CallbackListTiming, CallbackListTotal], + RunType + ); +tracing_timing_event_handler(start_callback, EventArgs, RunType, Hook, Host, _, TracingOpts) -> + CallbackStart = erlang:system_time(nanosecond), + % Generate new event: + run_event_handlers(TracingOpts, Hook, Host, start_callback_timing, EventArgs ++ [CallbackStart], RunType); +tracing_timing_event_handler(stop_callback, EventArgs, RunType, Hook, Host, _, TracingOpts) -> + CallbackStop = erlang:system_time(nanosecond), + TimingMap = #{} = erlang:get(?TIMING_KEY), + {_, [{_, _, _, CallbackStart} | _]} = maps:get({Hook, Host}, TimingMap), + run_event_handlers( + TracingOpts, + Hook, + Host, + stop_callback_timing, + EventArgs ++ [CallbackStart, CallbackStop], + RunType + ), + ok; +tracing_timing_event_handler(start_hook_timing, [_, HookStart], RunType, Hook, Host, EventHandlerOpts, _) -> + tracing_output(EventHandlerOpts, "(~0p|~ts|~0p|~0p) Timing started\n", [Hook, Host, erlang:self(), RunType]), + case erlang:get(?TIMING_KEY) of + #{}=TimingMap -> + erlang:put(?TIMING_KEY, TimingMap#{{Hook, Host} => {HookStart, []}}); + _ -> + erlang:put(?TIMING_KEY, #{{Hook, Host} => {HookStart, []}}) + end, + ok; +tracing_timing_event_handler( + stop_hook_timing, + [_, _, HookStart, HookStop, CallbackListTiming, CallbackListTotal], + RunType, + Hook, + Host, + EventHandlerOpts, + _ +) -> + if + erlang:length(CallbackListTiming) < 2 -> % We don't need sorted timing result + ok; + true -> + CallbackListTimingText = + lists:foldl( + fun({Mod, Func, Arity, Diff}, CallbackListTimingText) -> + CallbackListTimingText + ++ "\n\t" + ++ mfa_string({Mod, Func, Arity}) + ++ " -> " + ++ human_readable_time_string(Diff) + end, + "", + lists:keysort( + 4, + [ + {Mod, Func, Arity, CallbackStop - CallbackStart} || + {Mod, Func, Arity, CallbackStart, CallbackStop} <- CallbackListTiming + ] + ) + ), + tracing_output( + EventHandlerOpts, + "(~0p|~ts|~0p|~0p) All callbacks took ~ts to run. Sorted running time:" + ++ CallbackListTimingText + ++ "\n", + [Hook, Host, erlang:self(), RunType, human_readable_time_string(CallbackListTotal)] + ), + tracing_output( + EventHandlerOpts, + "(~0p|~ts|~0p|~0p) Time calculations for all callbacks took ~ts\n", + [ + Hook, + Host, + erlang:self(), + RunType, + human_readable_time_string((HookStop - HookStart) - CallbackListTotal) + ] + ) + end, + tracing_output(EventHandlerOpts, "(~0p|~ts|~0p|~0p) Timing stopped\n", [Hook, Host, erlang:self(), RunType]), + TimingMap = #{} = erlang:get(?TIMING_KEY), + NewTimingMap = maps:remove({Hook, Host}, TimingMap), + if + NewTimingMap == #{} -> + erlang:erase(?TIMING_KEY); + true -> + erlang:put(?TIMING_KEY, NewTimingMap) + end, + ok; +tracing_timing_event_handler(start_callback_timing, [Mod, Func, Args, _, CallbackStart], _, Hook, Host, _, _) -> + TimingMap = #{} = erlang:get(?TIMING_KEY), + {HookStart, Callbacks} = maps:get({Hook, Host}, TimingMap), + erlang:put( + ?TIMING_KEY, + TimingMap#{ + {Hook, Host} => {HookStart, [{Mod, Func, erlang:length(Args), CallbackStart} | Callbacks]} + } + ), + ok; +tracing_timing_event_handler( + stop_callback_timing, + [Mod, Func, _, _, _, CallbackStart, CallbackStop], + RunType, + Hook, + Host, + EventHandlerOpts, + _ +) -> + TimingMap = #{} = erlang:get(?TIMING_KEY), + {HookStart, [{Mod, Func, Arity, CallbackStart} | Callbacks]} = maps:get({Hook, Host}, TimingMap), + maps:get(output_for_each_callback, maps:get(timing, EventHandlerOpts, #{}), false) andalso tracing_output( + EventHandlerOpts, + "(~0p|~ts|~0p|~0p) " + ++ mfa_string({Mod, Func, Arity}) + ++ " took " + ++ human_readable_time_string(CallbackStop - CallbackStart) + ++ "\n", + [Hook, Host, erlang:self(), RunType] + ), + erlang:put( + ?TIMING_KEY, + TimingMap#{ + {Hook, Host} => {HookStart, [{Mod, Func, Arity, CallbackStart, CallbackStop} | Callbacks]} + } + ), + ok; +tracing_timing_event_handler(_, _, _, _, _, _, _) -> + ok. + +tracing_output(#{output_function := OutputF}, Text, Args) -> + try + OutputF(Text, Args) + of + _ -> + ok + catch + E:R:Stack -> + ?ERROR_MSG("Tracing output function exception(~0p): ~0p: ~0p", [E, R, Stack]), + ok + end; +tracing_output(#{output_log_level := Output}, Text, Args) -> + if + Output == debug -> + ?DEBUG(Text, Args); + true -> % info + ?INFO_MSG(Text, Args) + end, + ok; +tracing_output(Opts, Text, Args) -> + tracing_output(Opts#{output_log_level => info}, Text, Args). + +mfa_string({_, Fun, _}) when erlang:is_function(Fun) -> + io_lib:format("~0p", [Fun]); +mfa_string({Mod, Func, Arity}) -> + erlang:atom_to_list(Mod) ++ ":" ++ erlang:atom_to_list(Func) ++ "/" ++ erlang:integer_to_list(Arity). + +human_readable_time_string(TimeNS) -> + {Time, Unit, Decimals} = + if + TimeNS >= 1000000000 -> + {TimeNS / 1000000000, "", 10}; + TimeNS >= 1000000 -> + {TimeNS / 1000000, "m", 7}; + TimeNS >= 1000 -> + {TimeNS / 1000, "μ", 4}; + true -> + {TimeNS / 1, "n", 0} + end, + erlang:float_to_list(Time, [{decimals, Decimals}, compact]) ++ Unit ++ "s". diff --git a/src/ejabberd_http.erl b/src/ejabberd_http.erl index 3c91c3c58..709585145 100644 --- a/src/ejabberd_http.erl +++ b/src/ejabberd_http.erl @@ -5,7 +5,7 @@ %%% Created : 27 Feb 2004 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -24,24 +24,24 @@ %%%---------------------------------------------------------------------- -module(ejabberd_http). +-behaviour(ejabberd_listener). -author('alexey@process-one.net'). %% External exports --export([start/2, start_link/2, become_controller/1, - socket_type/0, receive_headers/1, url_encode/1, - transform_listen_option/2]). +-export([start/3, start_link/3, + accept/1, receive_headers/1, recv_file/2, + listen_opt_type/1, listen_options/0, + apply_custom_headers/2]). -%% Callbacks --export([init/2]). +-export([init/3]). --include("ejabberd.hrl"). -include("logger.hrl"). - --include("jlib.hrl"). - +-include_lib("xmpp/include/xmpp.hrl"). -include("ejabberd_http.hrl"). +-include_lib("kernel/include/file.hrl"). + -record(state, {sockmod, socket, request_method, @@ -49,14 +49,14 @@ request_path, request_auth, request_keepalive, - request_content_length, + request_content_length = 0, request_lang = <<"en">>, %% XXX bard: request handlers are configured in %% ejabberd.cfg under the HTTP service. For example, %% to have the module test_web handle requests with %% paths starting with "/test/module": %% - %% {5280, ejabberd_http, [http_poll, web_admin, + %% {5280, ejabberd_http, [http_bind, web_admin, %% {request_handlers, [{["test", "module"], mod_test_web}]}]} %% request_handlers = [], @@ -66,8 +66,11 @@ request_headers = [], end_of_request = false, options = [], - default_host, - trail = <<>> + custom_headers, + trail = <<>>, + allow_unencrypted_sasl2, + addr_re, + sock_peer_name = none }). -define(XHTML_DOCTYPE, @@ -82,18 +85,26 @@ "org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n" "">>). -start(SockData, Opts) -> - supervisor:start_child(ejabberd_http_sup, - [SockData, Opts]). +-define(RECV_BUF, 65536). +-define(SEND_BUF, 65536). +-define(MAX_POST_SIZE, 20971520). %% 20Mb -start_link(SockData, Opts) -> +start(SockMod, Socket, Opts) -> + {ok, + proc_lib:spawn(ejabberd_http, init, + [SockMod, Socket, Opts])}. + +start_link(SockMod, Socket, Opts) -> {ok, proc_lib:spawn_link(ejabberd_http, init, - [SockData, Opts])}. + [SockMod, Socket, Opts])}. -init({SockMod, Socket}, Opts) -> +init(SockMod, Socket, Opts) -> TLSEnabled = proplists:get_bool(tls, Opts), - TLSOpts1 = lists:filter(fun ({certfile, _}) -> true; + TLSOpts1 = lists:filter(fun ({ciphers, _}) -> true; + ({dhfile, _}) -> true; + ({cafile, _}) -> true; + ({protocol_options, _}) -> true; (_) -> false end, Opts), @@ -101,103 +112,98 @@ init({SockMod, Socket}, Opts) -> false -> [compression_none | TLSOpts1]; true -> TLSOpts1 end, - TLSOpts = [verify_none | TLSOpts2], + TLSOpts3 = case ejabberd_pkix:get_certfile( + ejabberd_config:get_myname()) of + error -> TLSOpts2; + {ok, CertFile} -> [{certfile, CertFile}|TLSOpts2] + end, + TLSOpts = [verify_none | TLSOpts3], {SockMod1, Socket1} = if TLSEnabled -> - inet:setopts(Socket, [{recbuf, 8192}]), - {ok, TLSSocket} = p1_tls:tcp_to_tls(Socket, + inet:setopts(Socket, [{recbuf, ?RECV_BUF}]), + {ok, TLSSocket} = fast_tls:tcp_to_tls(Socket, TLSOpts), - {p1_tls, TLSSocket}; + {fast_tls, TLSSocket}; true -> {SockMod, Socket} end, - case SockMod1 of - gen_tcp -> - inet:setopts(Socket1, [{packet, http_bin}, {recbuf, 8192}]); - _ -> ok - end, - Captcha = case proplists:get_bool(captcha, Opts) of - true -> [{[<<"captcha">>], ejabberd_captcha}]; - false -> [] - end, - Register = case proplists:get_bool(register, Opts) of - true -> [{[<<"register">>], mod_register_web}]; - false -> [] - end, - Admin = case proplists:get_bool(web_admin, Opts) of - true -> [{[<<"admin">>], ejabberd_web_admin}]; - false -> [] - end, - Bind = case proplists:get_bool(http_bind, Opts) of - true -> [{[<<"http-bind">>], mod_http_bind}]; - false -> [] - end, - Poll = case proplists:get_bool(http_poll, Opts) of - true -> [{[<<"http-poll">>], ejabberd_http_poll}]; - false -> [] - end, - XMLRPC = case proplists:get_bool(xmlrpc, Opts) of - true -> [{[], ejabberd_xmlrpc}]; - false -> [] - end, - DefinedHandlers = gen_mod:get_opt( - request_handlers, Opts, - fun(Hs) -> - [{str:tokens( - iolist_to_binary(Path), <<"/">>), - Mod} || {Path, Mod} <- Hs] - end, []), - RequestHandlers = DefinedHandlers ++ Captcha ++ Register ++ - Admin ++ Bind ++ Poll ++ XMLRPC, + SockPeer = proplists:get_value(sock_peer_name, Opts, none), + RequestHandlers = proplists:get_value(request_handlers, Opts, []), ?DEBUG("S: ~p~n", [RequestHandlers]), - DefaultHost = gen_mod:get_opt(default_host, Opts, fun(A) -> A end, undefined), + {ok, RE} = re:compile(<<"^(?:\\[(.*?)\\]|(.*?))(?::(\\d+))?$">>), - ?INFO_MSG("started: ~p", [{SockMod1, Socket1}]), + CustomHeaders = proplists:get_value(custom_headers, Opts, []), + + AllowUnencryptedSasl2 = proplists:get_bool(allow_unencrypted_sasl2, Opts), State = #state{sockmod = SockMod1, socket = Socket1, - default_host = DefaultHost, + custom_headers = CustomHeaders, options = Opts, - request_handlers = RequestHandlers}, - receive_headers(State). + allow_unencrypted_sasl2 = AllowUnencryptedSasl2, + request_handlers = RequestHandlers, + sock_peer_name = SockPeer, + addr_re = RE}, + try receive_headers(State) of + V -> V + catch + {error, _} -> State + end. -become_controller(_Pid) -> ok. - -socket_type() -> - raw. +accept(_Pid) -> + ok. +send_text(_State, none) -> + ok; send_text(State, Text) -> - case catch - (State#state.sockmod):send(State#state.socket, Text) - of - ok -> ok; - {error, timeout} -> - ?INFO_MSG("Timeout on ~p:send", [State#state.sockmod]), - exit(normal); - Error -> - ?DEBUG("Error in ~p:send: ~p", - [State#state.sockmod, Error]), - exit(normal) + case (State#state.sockmod):send(State#state.socket, Text) of + ok -> ok; + {error, timeout} -> + ?INFO_MSG("Timeout on ~p:send", [State#state.sockmod]), + exit(normal); + Error -> + ?DEBUG("Error in ~p:send: ~p", + [State#state.sockmod, Error]), + exit(normal) + end. + +send_file(State, Fd, Size, FileName) -> + try + case State#state.sockmod of + gen_tcp -> + {ok, _} = file:sendfile(Fd, State#state.socket, 0, Size, []), + ok; + _ -> + case file:read(Fd, ?SEND_BUF) of + {ok, Data} -> + send_text(State, Data), + send_file(State, Fd, Size, FileName); + eof -> + ok + end + end + catch _:{case_clause, {error, Why}} -> + if Why /= closed -> + ?WARNING_MSG("Failed to read ~ts: ~ts", + [FileName, file_format_error(Why)]), + exit(normal); + true -> + ok + end end. receive_headers(#state{trail = Trail} = State) -> SockMod = State#state.sockmod, Socket = State#state.socket, Data = SockMod:recv(Socket, 0, 300000), - case State#state.sockmod of - gen_tcp -> - NewState = process_header(State, Data), - case NewState#state.end_of_request of - true -> - ok; - _ -> - receive_headers(NewState) - end; - _ -> - case Data of - {ok, D} -> - parse_headers(State#state{trail = <>}); - {error, _} -> - ok - end + case Data of + {error, closed} when State#state.request_method == undefined -> + % socket closed without receiving anything in it + ok; + {error, Error} -> + ?DEBUG("Error when retrieving http headers ~p: ~p", + [State#state.sockmod, Error]), + ok; + {ok, D} -> + parse_headers(State#state{trail = <>}) end. parse_headers(#state{trail = <<>>} = State) -> @@ -206,22 +212,20 @@ parse_headers(#state{request_method = Method, trail = Data} = State) -> PktType = case Method of - undefined -> http_bin; - _ -> httph_bin - end, + undefined -> http_bin; + _ -> httph_bin + end, case erlang:decode_packet(PktType, Data, []) of - {ok, Pkt, Rest} -> - NewState = process_header(State#state{trail = Rest}, {ok, Pkt}), + {ok, Pkt, Rest} -> + NewState = process_header(State#state{trail = Rest}, {ok, Pkt}), case NewState#state.end_of_request of - true -> - ok; - _ -> - parse_headers(NewState) + true -> ok; + _ -> parse_headers(NewState) end; - {more, _} -> - receive_headers(State#state{trail = Data}); - _ -> - ok + {more, _} -> + receive_headers(State#state{trail = Data}); + _ -> + ok end. process_header(State, Data) -> @@ -244,7 +248,7 @@ process_header(State, Data) -> request_version = Version, request_path = Path, request_keepalive = KeepAlive}; {ok, {http_header, _, 'Connection' = Name, _, Conn}} -> - KeepAlive1 = case jlib:tolower(Conn) of + KeepAlive1 = case misc:tolower(Conn) of <<"keep-alive">> -> true; <<"close">> -> false; _ -> State#state.request_keepalive @@ -257,7 +261,7 @@ process_header(State, Data) -> request_headers = add_header(Name, Auth, State)}; {ok, {http_header, _, 'Content-Length' = Name, _, SLen}} -> - case catch jlib:binary_to_integer(SLen) of + case catch binary_to_integer(SLen) of Len when is_integer(Len) -> State#state{request_content_length = Len, request_headers = add_header(Name, SLen, State)}; @@ -267,373 +271,437 @@ process_header(State, Data) -> {http_header, _, 'Accept-Language' = Name, _, Langs}} -> State#state{request_lang = parse_lang(Langs), request_headers = add_header(Name, Langs, State)}; - {ok, {http_header, _, 'Host' = Name, _, Host}} -> - State#state{request_host = Host, - request_headers = add_header(Name, Host, State)}; + {ok, {http_header, _, 'Host' = Name, _, Value}} -> + {Host, Port, TP} = get_transfer_protocol(State#state.addr_re, SockMod, Value), + State#state{request_host = ejabberd_config:resolve_host_alias(Host), + request_port = Port, + request_tp = TP, + request_headers = add_header(Name, Value, State)}; + {ok, {http_header, _, Name, _, Value}} when is_binary(Name) -> + State#state{request_headers = + add_header(normalize_header_name(Name), Value, State)}; {ok, {http_header, _, Name, _, Value}} -> State#state{request_headers = add_header(Name, Value, State)}; - {ok, http_eoh} - when State#state.request_host == undefined -> - ?WARNING_MSG("An HTTP request without 'Host' HTTP " - "header was received.", - []), - throw(http_request_no_host_header); + {ok, http_eoh} when State#state.request_host == undefined; + State#state.request_host == error -> + {State1, Out} = process_request(State), + send_text(State1, Out), + process_header(State, {ok, {http_error, <<>>}}); {ok, http_eoh} -> - ?DEBUG("(~w) http query: ~w ~p~n", - [State#state.socket, State#state.request_method, - element(2, State#state.request_path)]), - {HostProvided, Port, TP} = - get_transfer_protocol(SockMod, - State#state.request_host), - Host = get_host_really_served(State#state.default_host, - HostProvided), - State2 = State#state{request_host = Host, - request_port = Port, request_tp = TP}, - Out = process_request(State2), - send_text(State2, Out), - case State2#state.request_keepalive of - true -> - case SockMod of - gen_tcp -> inet:setopts(Socket, [{packet, http_bin}]); - _ -> ok - end, - #state{sockmod = SockMod, socket = Socket, - options = State#state.options, - request_handlers = State#state.request_handlers}; - _ -> - #state{end_of_request = true, - options = State#state.options, - request_handlers = State#state.request_handlers} - end; + ?DEBUG("(~w) http query: ~w ~p~n", + [State#state.socket, State#state.request_method, + element(2, State#state.request_path)]), + {State3, Out} = process_request(State), + send_text(State3, Out), + case State3#state.request_keepalive of + true -> + #state{sockmod = SockMod, socket = Socket, + trail = State3#state.trail, + options = State#state.options, + custom_headers = State#state.custom_headers, + request_handlers = State#state.request_handlers, + addr_re = State#state.addr_re}; + _ -> + #state{end_of_request = true, + trail = State3#state.trail, + options = State#state.options, + custom_headers = State#state.custom_headers, + request_handlers = State#state.request_handlers, + addr_re = State#state.addr_re} + end; _ -> #state{end_of_request = true, options = State#state.options, - request_handlers = State#state.request_handlers} + custom_headers = State#state.custom_headers, + request_handlers = State#state.request_handlers, + addr_re = State#state.addr_re} end. add_header(Name, Value, State)-> [{Name, Value} | State#state.request_headers]. -get_host_really_served(undefined, Provided) -> - Provided; -get_host_really_served(Default, Provided) -> - case lists:member(Provided, ?MYHOSTS) of - true -> Provided; - false -> Default - end. +get_transfer_protocol(RE, SockMod, HostPort) -> + {Proto, DefPort} = case SockMod of + gen_tcp -> {http, 80}; + fast_tls -> {https, 443} + end, + {Host, Port} = case re:run(HostPort, RE, [{capture,[1,2,3],binary}]) of + nomatch -> + {error, DefPort}; + {match, [<<>>, H, <<>>]} -> + {jid:nameprep(H), DefPort}; + {match, [H, <<>>, <<>>]} -> + {jid:nameprep(H), DefPort}; + {match, [<<>>, H, PortStr]} -> + {jid:nameprep(H), binary_to_integer(PortStr)}; + {match, [H, <<>>, PortStr]} -> + {jid:nameprep(H), binary_to_integer(PortStr)} + end, -%% @spec (SockMod, HostPort) -> {Host::string(), Port::integer(), TP} -%% where -%% SockMod = gen_tcp | tls -%% HostPort = string() -%% TP = http | https -%% @doc Given a socket and hostport header, return data of transfer protocol. -%% Note that HostPort can be a string of a host like "example.org", -%% or a string of a host and port like "example.org:5280". -get_transfer_protocol(SockMod, HostPort) -> - [Host | PortList] = str:tokens(HostPort, <<":">>), - case {SockMod, PortList} of - {gen_tcp, []} -> {Host, 80, http}; - {gen_tcp, [Port]} -> - {Host, jlib:binary_to_integer(Port), http}; - {p1_tls, []} -> {Host, 443, https}; - {p1_tls, [Port]} -> - {Host, jlib:binary_to_integer(Port), https} - end. + {Host, Port, Proto}. %% XXX bard: search through request handlers looking for one that %% matches the requested URL path, and pass control to it. If none is %% found, answer with HTTP 404. -process([], _) -> - ejabberd_web:error(not_found); + +process([], _) -> ejabberd_web:error(not_found); process(Handlers, Request) -> - %% Only the first element in the path prefix is checked - [{HandlerPathPrefix, HandlerModule} | HandlersLeft] = - Handlers, - case lists:prefix(HandlerPathPrefix, - Request#request.path) - or (HandlerPathPrefix == Request#request.path) - of - true -> - ?DEBUG("~p matches ~p", - [Request#request.path, HandlerPathPrefix]), - LocalPath = lists:nthtail(length(HandlerPathPrefix), - Request#request.path), - ?DEBUG("~p", [Request#request.headers]), - R = HandlerModule:process(LocalPath, Request), - ejabberd_hooks:run(http_request_debug, - [{LocalPath, Request}]), - R; - false -> process(HandlersLeft, Request) + {HandlerPathPrefix, HandlerModule, HandlerOpts, HandlersLeft} = + case Handlers of + [{Pfx, Mod} | Tail] -> + {Pfx, Mod, [], Tail}; + [{Pfx, Mod, Opts} | Tail] -> + {Pfx, Mod, Opts, Tail} + end, + + case (lists:prefix(HandlerPathPrefix, Request#request.path) or + (HandlerPathPrefix==Request#request.path)) of + true -> + ?DEBUG("~p matches ~p", [Request#request.path, HandlerPathPrefix]), + %% LocalPath is the path "local to the handler", i.e. if + %% the handler was registered to handle "/test/" and the + %% requested path is "/test/foo/bar", the local path is + %% ["foo", "bar"] + LocalPath = lists:nthtail(length(HandlerPathPrefix), Request#request.path), + R = case erlang:function_exported(HandlerModule, socket_handoff, 3) of + true -> + HandlerModule:socket_handoff( + LocalPath, Request, HandlerOpts); + false -> + try + HandlerModule:process(LocalPath, Request) + catch + Class:Reason:Stack -> + ?ERROR_MSG( + "HTTP handler crashed: ~s", + [misc:format_exception(2, Class, Reason, Stack)]), + erlang:raise(Class, Reason, Stack) + end + end, + ejabberd_hooks:run(http_request_debug, [{LocalPath, Request}]), + R; + false -> + process(HandlersLeft, Request) end. -process_request(#state{request_method = Method, options = Options, - request_path = {abs_path, Path}, request_auth = Auth, - request_lang = Lang, request_handlers = RequestHandlers, - request_host = Host, request_port = Port, - request_tp = TP, request_headers = RequestHeaders, +extract_path_query(#state{request_method = Method, + request_path = {abs_path, Path}} = State) + when Method =:= 'GET' orelse + Method =:= 'HEAD' orelse + Method =:= 'DELETE' orelse Method =:= 'OPTIONS' -> + case catch url_decode_q_split_normalize(Path) of + {'EXIT', Error} -> + ?DEBUG("Error decoding URL '~p': ~p", [Path, Error]), + {State, false}; + {LPath, Query} -> + LQuery = case catch parse_urlencoded(Query) of + {'EXIT', _Reason} -> []; + LQ -> LQ + end, + {State, {LPath, LQuery, <<"">>, Path}} + end; +extract_path_query(#state{request_method = Method, + request_path = {abs_path, Path}, + request_content_length = Len, + trail = Trail, + sockmod = _SockMod, + socket = _Socket} = State) + when (Method =:= 'POST' orelse Method =:= 'PUT') andalso Len>0 -> + case catch url_decode_q_split_normalize(Path) of + {'EXIT', Error} -> + ?DEBUG("Error decoding URL '~p': ~p", [Path, Error]), + {State, false}; + {LPath, _Query} -> + case Method of + 'PUT' -> + {State, {LPath, [], Trail, Path}}; + 'POST' -> + case recv_data(State) of + {ok, Data} -> + LQuery = case catch parse_urlencoded(Data) of + {'EXIT', _Reason} -> []; + LQ -> LQ + end, + {State, {LPath, LQuery, Data, Path}}; + error -> + {State, false} + end + end + end; +extract_path_query(State) -> + {State, false}. + +process_request(#state{request_host = undefined, + custom_headers = CustomHeaders} = State) -> + {State, make_text_output(State, 400, CustomHeaders, + <<"Missing Host header">>)}; +process_request(#state{request_host = error, + custom_headers = CustomHeaders} = State) -> + {State, make_text_output(State, 400, CustomHeaders, + <<"Malformed Host header">>)}; +process_request(#state{request_method = Method, + request_auth = Auth, + request_lang = Lang, + request_version = Version, sockmod = SockMod, - socket = Socket} = State) - when Method=:='GET' orelse Method=:='HEAD' orelse Method=:='DELETE' orelse Method=:='OPTIONS' -> - case (catch url_decode_q_split(Path)) of - {'EXIT', _} -> - make_bad_request(State); - {NPath, Query} -> - LPath = normalize_path([NPE || NPE <- str:tokens(path_decode(NPath), <<"/">>)]), - LQuery = case (catch parse_urlencoded(Query)) of - {'EXIT', _Reason} -> - []; - LQ -> - LQ - end, - {ok, IPHere} = - case SockMod of - gen_tcp -> - inet:peername(Socket); - _ -> - SockMod:peername(Socket) - end, - XFF = proplists:get_value('X-Forwarded-For', RequestHeaders, []), - IP = analyze_ip_xff(IPHere, XFF, Host), - Request = #request{method = Method, - path = LPath, - opts = Options, - q = LQuery, - auth = Auth, - lang = Lang, - host = Host, - port = Port, - tp = TP, - headers = RequestHeaders, - ip = IP}, - %% XXX bard: This previously passed control to - %% ejabberd_web:process_get, now passes it to a local - %% procedure (process) that handles dispatching based on - %% URL path prefix. - case process(RequestHandlers, Request) of - El when element(1, El) == xmlel -> - make_xhtml_output(State, 200, [], El); - {Status, Headers, El} when - element(1, El) == xmlel -> - make_xhtml_output(State, Status, Headers, El); - Output when is_list(Output) or is_binary(Output) -> - make_text_output(State, 200, [], Output); - {Status, Headers, Output} when is_list(Output) or is_binary(Output) -> - make_text_output(State, Status, Headers, Output) - end - end; -process_request(#state{request_method = Method, options = Options, - request_path = {abs_path, Path}, request_auth = Auth, - request_content_length = Len, request_lang = Lang, - sockmod = SockMod, socket = Socket, request_host = Host, - request_port = Port, request_tp = TP, + socket = Socket, + sock_peer_name = SockPeer, + options = Options, + request_host = Host, + request_port = Port, + request_tp = TP, + request_content_length = Length, request_headers = RequestHeaders, - request_handlers = RequestHandlers} = - State) - when (Method =:= 'POST' orelse Method =:= 'PUT') andalso - is_integer(Len) -> - {ok, IPHere} = case SockMod of - gen_tcp -> inet:peername(Socket); - _ -> SockMod:peername(Socket) - end, - XFF = proplists:get_value('X-Forwarded-For', - RequestHeaders, []), - IP = analyze_ip_xff(IPHere, XFF, Host), - case SockMod of - gen_tcp -> inet:setopts(Socket, [{packet, 0}]); - _ -> ok + request_handlers = RequestHandlers, + custom_headers = CustomHeaders} = State) -> + case proplists:get_value(<<"Expect">>, RequestHeaders, <<>>) of + <<"100-", _/binary>> when Version == {1, 1} -> + send_text(State, <<"HTTP/1.1 100 Continue\r\n\r\n">>); + _ -> + ok end, - Data = recv_data(State, Len), - ?DEBUG("client data: ~p~n", [Data]), - case (catch url_decode_q_split(Path)) of - {'EXIT', _} -> - make_bad_request(State); - {NPath, _Query} -> - LPath = normalize_path([NPE || NPE <- str:tokens(path_decode(NPath), <<"/">>)]), - LQuery = case (catch parse_urlencoded(Data)) of - {'EXIT', _Reason} -> - []; - LQ -> - LQ - end, - Request = #request{method = Method, - path = LPath, - q = LQuery, - opts = Options, - auth = Auth, + case extract_path_query(State) of + {State2, false} -> + {State2, make_bad_request(State)}; + {State2, {LPath, LQuery, Data, RawPath}} -> + PeerName = case SockPeer of + none -> + case SockMod of + gen_tcp -> + inet:peername(Socket); + _ -> + SockMod:peername(Socket) + end; + {_, Peer} -> + {ok, Peer} + end, + IPHere = case PeerName of + {ok, V} -> V; + {error, _} = E -> throw(E) + end, + XFF = proplists:get_value('X-Forwarded-For', RequestHeaders, []), + IP = analyze_ip_xff(IPHere, XFF), + Request = #request{method = Method, + path = LPath, + raw_path = RawPath, + q = LQuery, + auth = Auth, + length = Length, + sockmod = SockMod, + socket = Socket, data = Data, - lang = Lang, - host = Host, - port = Port, - tp = TP, - headers = RequestHeaders, - ip = IP}, - case process(RequestHandlers, Request) of - El when element(1, El) == xmlel -> - make_xhtml_output(State, 200, [], El); - {Status, Headers, El} when - element(1, El) == xmlel -> - make_xhtml_output(State, Status, Headers, El); - Output when is_list(Output) or is_binary(Output) -> - make_text_output(State, 200, [], Output); - {Status, Headers, Output} when is_list(Output) or is_binary(Output) -> - make_text_output(State, Status, Headers, Output) - end - end; -process_request(State) -> make_bad_request(State). + lang = Lang, + host = Host, + port = Port, + tp = TP, + opts = Options, + headers = RequestHeaders, + ip = IP}, + RequestHandlers1 = ejabberd_hooks:run_fold( + http_request_handlers, RequestHandlers, [Host, Request]), + Res = case process(RequestHandlers1, Request) of + El when is_record(El, xmlel) -> + make_xhtml_output(State, 200, CustomHeaders, El); + {Status, Headers, El} + when is_record(El, xmlel) -> + make_xhtml_output(State, Status, + apply_custom_headers(Headers, CustomHeaders), El); + Output when is_binary(Output) or is_list(Output) -> + make_text_output(State, 200, CustomHeaders, Output); + {Status, Headers, Output} + when is_binary(Output) or is_list(Output) -> + make_text_output(State, Status, + apply_custom_headers(Headers, CustomHeaders), Output); + {Status, Headers, {file, FileName}} -> + make_file_output(State, Status, Headers, FileName); + {Status, Reason, Headers, Output} + when is_binary(Output) or is_list(Output) -> + make_text_output(State, Status, Reason, + apply_custom_headers(Headers, CustomHeaders), Output); + _ -> + none + end, + {State2#state{trail = <<>>}, Res} + end. make_bad_request(State) -> -%% Support for X-Forwarded-From - make_xhtml_output(State, 400, [], + make_xhtml_output(State, 400, State#state.custom_headers, ejabberd_web:make_xhtml([#xmlel{name = <<"h1">>, attrs = [], children = [{xmlcdata, <<"400 Bad Request">>}]}])). -analyze_ip_xff(IP, [], _Host) -> IP; -analyze_ip_xff({IPLast, Port}, XFF, Host) -> +analyze_ip_xff(IP, []) -> IP; +analyze_ip_xff({IPLast, Port}, XFF) -> [ClientIP | ProxiesIPs] = str:tokens(XFF, <<", ">>) ++ - [jlib:ip_to_list(IPLast)], - TrustedProxies = ejabberd_config:get_option( - {trusted_proxies, Host}, - fun(TPs) -> - [iolist_to_binary(TP) || TP <- TPs] - end, []), + [misc:ip_to_list(IPLast)], + TrustedProxies = ejabberd_option:trusted_proxies(), IPClient = case is_ipchain_trusted(ProxiesIPs, TrustedProxies) of true -> - {ok, IPFirst} = inet_parse:address( - binary_to_list(ClientIP)), - ?DEBUG("The IP ~w was replaced with ~w due to " - "header X-Forwarded-For: ~s", - [IPLast, IPFirst, XFF]), - IPFirst; + case inet_parse:address(binary_to_list(ClientIP)) of + {ok, IPFirst} -> + ?DEBUG("The IP ~w was replaced with ~w due to " + "header X-Forwarded-For: ~ts", + [IPLast, IPFirst, XFF]), + IPFirst; + E -> throw(E) + end; false -> IPLast end, {IPClient, Port}. +is_ipchain_trusted([], _) -> false; is_ipchain_trusted(_UserIPs, all) -> true; -is_ipchain_trusted(UserIPs, TrustedIPs) -> - [] == UserIPs -- [<<"127.0.0.1">> | TrustedIPs]. +is_ipchain_trusted(UserIPs, Masks) -> + lists:all( + fun(IP) -> + case inet:parse_address(binary_to_list(IP)) of + {ok, IP2} -> + lists:any( + fun({Mask, MaskLen}) -> + misc:match_ip_mask(IP2, Mask, MaskLen) + end, Masks); + _ -> + false + end + end, UserIPs). -recv_data(State, Len) -> recv_data(State, Len, <<>>). - -recv_data(_State, 0, Acc) -> (iolist_to_binary(Acc)); -recv_data(State, Len, Acc) -> - case State#state.trail of - <<>> -> - case (State#state.sockmod):recv(State#state.socket, Len, - 300000) - of - {ok, Data} -> - recv_data(State, Len - byte_size(Data), <>); - _ -> <<"">> - end; - _ -> - Trail = (State#state.trail), - recv_data(State#state{trail = <<>>}, - Len - byte_size(Trail), <>) +recv_data(#state{request_content_length = Len}) when Len >= ?MAX_POST_SIZE -> + error; +recv_data(#state{request_content_length = Len, trail = Trail, + sockmod = SockMod, socket = Socket}) -> + NewLen = Len - byte_size(Trail), + if NewLen > 0 -> + case SockMod:recv(Socket, NewLen, 60000) of + {ok, Data} -> {ok, <>}; + {error, _} -> error + end; + true -> + {ok, Trail} end. -make_xhtml_output(State, Status, Headers, XHTML) -> - Data = case lists:member(html, Headers) of - true -> - iolist_to_binary([?HTML_DOCTYPE, - xml:element_to_binary(XHTML)]); - _ -> - iolist_to_binary([?XHTML_DOCTYPE, - xml:element_to_binary(XHTML)]) - end, - Headers1 = case lists:keysearch(<<"Content-Type">>, 1, - Headers) - of - {value, _} -> - [{<<"Content-Length">>, - iolist_to_binary(integer_to_list(byte_size(Data)))} - | Headers]; - _ -> - [{<<"Content-Type">>, <<"text/html; charset=utf-8">>}, - {<<"Content-Length">>, - iolist_to_binary(integer_to_list(byte_size(Data)))} - | Headers] +recv_file(#request{length = Len, data = Trail, + sockmod = SockMod, socket = Socket}, Path) -> + case file:open(Path, [write, exclusive, raw]) of + {ok, Fd} -> + Res = case file:write(Fd, Trail) of + ok -> + NewLen = max(0, Len - byte_size(Trail)), + do_recv_file(NewLen, SockMod, Socket, Fd); + {error, _} = Err -> + Err + end, + file:close(Fd), + case Res of + ok -> ok; + {error, _} -> file:delete(Path) + end, + Res; + {error, _} = Err -> + Err + end. + +do_recv_file(0, _SockMod, _Socket, _Fd) -> + ok; +do_recv_file(Len, SockMod, Socket, Fd) -> + ChunkLen = min(Len, ?RECV_BUF), + case SockMod:recv(Socket, ChunkLen, timer:seconds(30)) of + {ok, Data} -> + case file:write(Fd, Data) of + ok -> + do_recv_file(Len-size(Data), SockMod, Socket, Fd); + {error, _} = Err -> + Err + end; + {error, _} -> + {error, closed} + end. + +make_headers(State, Status, Reason, Headers, Data) -> + Len = if is_integer(Data) -> Data; + true -> iolist_size(Data) + end, + Headers1 = [{<<"Content-Length">>, integer_to_binary(Len)} | Headers], + Headers2 = case lists:keyfind(<<"Content-Type">>, 1, Headers) of + {_, _} -> + Headers1; + false -> + [{<<"Content-Type">>, <<"text/html; charset=utf-8">>} + | Headers1] end, HeadersOut = case {State#state.request_version, - State#state.request_keepalive} - of - {{1, 1}, true} -> Headers1; - {_, true} -> - [{<<"Connection">>, <<"keep-alive">>} | Headers1]; - {_, false} -> - [{<<"Connection">>, <<"close">>} | Headers1] + State#state.request_keepalive} of + {{1, 1}, true} -> Headers2; + {_, true} -> + [{<<"Connection">>, <<"keep-alive">>} | Headers2]; + {_, false} -> + [{<<"Connection">>, <<"close">>} | Headers2] end, Version = case State#state.request_version of - {1, 1} -> <<"HTTP/1.1 ">>; - _ -> <<"HTTP/1.0 ">> + {1, 1} -> <<"HTTP/1.1 ">>; + _ -> <<"HTTP/1.0 ">> end, - H = lists:map(fun ({Attr, Val}) -> - [Attr, <<": ">>, Val, <<"\r\n">>]; - (_) -> [] - end, - HeadersOut), + H = [[Attr, <<": ">>, Val, <<"\r\n">>] || {Attr, Val} <- HeadersOut], + NewReason = case Reason of + <<"">> -> code_to_phrase(Status); + _ -> Reason + end, SL = [Version, - iolist_to_binary(integer_to_list(Status)), <<" ">>, - code_to_phrase(Status), <<"\r\n">>], - Data2 = case State#state.request_method of - 'HEAD' -> <<"">>; - _ -> Data - end, - [SL, H, <<"\r\n">>, Data2]. + integer_to_binary(Status), <<" ">>, + NewReason, <<"\r\n">>], + [SL, H, <<"\r\n">>]. + +make_xhtml_output(State, Status, Headers, XHTML) -> + Data = case State#state.request_method of + 'HEAD' -> <<"">>; + _ -> + DocType = case lists:member(html, Headers) of + true -> ?HTML_DOCTYPE; + false -> ?XHTML_DOCTYPE + end, + iolist_to_binary([DocType, fxml:element_to_binary(XHTML)]) + end, + EncodedHdrs = make_headers(State, Status, <<"">>, Headers, Data), + [EncodedHdrs, Data]. make_text_output(State, Status, Headers, Text) -> make_text_output(State, Status, <<"">>, Headers, Text). make_text_output(State, Status, Reason, Headers, Text) -> Data = iolist_to_binary(Text), - Headers1 = case lists:keysearch(<<"Content-Type">>, 1, - Headers) - of - {value, _} -> - [{<<"Content-Length">>, - jlib:integer_to_binary(byte_size(Data))} - | Headers]; - _ -> - [{<<"Content-Type">>, <<"text/html; charset=utf-8">>}, - {<<"Content-Length">>, - jlib:integer_to_binary(byte_size(Data))} - | Headers] - end, - HeadersOut = case {State#state.request_version, - State#state.request_keepalive} - of - {{1, 1}, true} -> Headers1; - {_, true} -> - [{<<"Connection">>, <<"keep-alive">>} | Headers1]; - {_, false} -> - [{<<"Connection">>, <<"close">>} | Headers1] - end, - Version = case State#state.request_version of - {1, 1} -> <<"HTTP/1.1 ">>; - _ -> <<"HTTP/1.0 ">> - end, - H = lists:map(fun ({Attr, Val}) -> - [Attr, <<": ">>, Val, <<"\r\n">>] - end, - HeadersOut), - NewReason = case Reason of - <<"">> -> code_to_phrase(Status); - _ -> Reason - end, - SL = [Version, - jlib:integer_to_binary(Status), <<" ">>, - NewReason, <<"\r\n">>], Data2 = case State#state.request_method of - 'HEAD' -> <<"">>; - _ -> Data + 'HEAD' -> <<"">>; + _ -> Data end, - [SL, H, <<"\r\n">>, Data2]. + EncodedHdrs = make_headers(State, Status, Reason, Headers, Data2), + [EncodedHdrs, Data2]. + +make_file_output(State, Status, Headers, FileName) -> + case file:read_file_info(FileName) of + {ok, #file_info{size = Size}} when State#state.request_method == 'HEAD' -> + make_headers(State, Status, <<"">>, Headers, Size); + {ok, #file_info{size = Size}} -> + case file:open(FileName, [raw, read]) of + {ok, Fd} -> + EncodedHdrs = make_headers(State, Status, <<"">>, Headers, Size), + send_text(State, EncodedHdrs), + send_file(State, Fd, Size, FileName), + file:close(Fd), + none; + {error, Why} -> + Reason = file_format_error(Why), + ?ERROR_MSG("Failed to open ~ts: ~ts", [FileName, Reason]), + make_text_output(State, 404, Reason, [], <<>>) + end; + {error, Why} -> + Reason = file_format_error(Why), + ?ERROR_MSG("Failed to read info of ~ts: ~ts", [FileName, Reason]), + make_text_output(State, 404, Reason, [], <<>>) + end. parse_lang(Langs) -> case str:tokens(Langs, <<",; ">>) of @@ -641,8 +709,20 @@ parse_lang(Langs) -> [] -> <<"en">> end. +file_format_error(Reason) -> + case file:format_error(Reason) of + "unknown POSIX error" -> atom_to_list(Reason); + Text -> Text + end. + +url_decode_q_split_normalize(Path) -> + {NPath, Query} = url_decode_q_split(Path), + LPath = normalize_path([NPE + || NPE <- str:tokens(misc:uri_decode(NPath), <<"/">>)]), + {LPath, Query}. + % Code below is taken (with some modifications) from the yaws webserver, which -% is distributed under the folowing license: +% is distributed under the following license: % % This software (the yaws webserver) is free software. % Parts of this software is Copyright (c) Claes Wikstrom @@ -666,19 +746,6 @@ url_decode_q_split(<>, Acc) when H /= 0 -> url_decode_q_split(<<>>, Ack) -> {path_norm_reverse(Ack), <<>>}. -%% @doc Decode a part of the URL and return string() -path_decode(Path) -> path_decode(Path, <<>>). - -path_decode(<<$%, Hi, Lo, Tail/binary>>, Acc) -> - Hex = hex_to_integer([Hi, Lo]), - if Hex == 0 -> exit(badurl); - true -> ok - end, - path_decode(Tail, <>); -path_decode(<>, Acc) when H /= 0 -> - path_decode(T, <>); -path_decode(<<>>, Acc) -> Acc. - path_norm_reverse(<<"/", T/binary>>) -> start_dir(0, <<"/">>, T); path_norm_reverse(T) -> start_dir(0, <<"">>, T). @@ -702,22 +769,6 @@ rest_dir(0, Path, <>) -> rest_dir(0, <>, T); rest_dir(N, Path, <<_H, T/binary>>) -> rest_dir(N, Path, T). -%% hex_to_integer - -hex_to_integer(Hex) -> - case catch list_to_integer(Hex, 16) of - {'EXIT', _} -> old_hex_to_integer(Hex); - X -> X - end. - -old_hex_to_integer(Hex) -> - DEHEX = fun (H) when H >= $a, H =< $f -> H - $a + 10; - (H) when H >= $A, H =< $F -> H - $A + 10; - (H) when H >= $0, H =< $9 -> H - $0 - end, - lists:foldl(fun (E, Acc) -> Acc * 16 + DEHEX(E) end, 0, - Hex). - code_to_phrase(100) -> <<"Continue">>; code_to_phrase(101) -> <<"Switching Protocols ">>; code_to_phrase(200) -> <<"OK">>; @@ -763,25 +814,32 @@ code_to_phrase(503) -> <<"Service Unavailable">>; code_to_phrase(504) -> <<"Gateway Timeout">>; code_to_phrase(505) -> <<"HTTP Version Not Supported">>. +-spec parse_auth(binary()) -> {binary(), binary()} | {oauth, binary(), []} | invalid. parse_auth(<<"Basic ", Auth64/binary>>) -> - Auth = jlib:decode_base64(Auth64), - %% Auth should be a string with the format: user@server:password - %% Note that password can contain additional characters '@' and ':' - case str:chr(Auth, $:) of - 0 -> - undefined; - Pos -> - {User, <<$:, Pass/binary>>} = erlang:split_binary(Auth, Pos-1), - {User, Pass} + try base64:decode(Auth64) of + Auth -> + case binary:split(Auth, <<":">>) of + [User, Pass] -> + PassUtf8 = unicode:characters_to_binary(Pass, utf8), + {User, PassUtf8}; + _ -> + invalid + end + catch _:_ -> + invalid end; -parse_auth(<<_/binary>>) -> undefined. +parse_auth(<<"Bearer ", SToken/binary>>) -> + Token = str:strip(SToken), + {oauth, Token, []}; +parse_auth(<<_/binary>>) -> + invalid. parse_urlencoded(S) -> parse_urlencoded(S, nokey, <<>>, key). parse_urlencoded(<<$%, Hi, Lo, Tail/binary>>, Last, Cur, State) -> - Hex = hex_to_integer([Hi, Lo]), + Hex = list_to_integer([Hi, Lo], 16), parse_urlencoded(Tail, Last, <>, State); parse_urlencoded(<<$&, Tail/binary>>, _Last, Cur, key) -> [{Cur, <<"">>} | parse_urlencoded(Tail, @@ -801,40 +859,34 @@ parse_urlencoded(<<>>, Last, Cur, _State) -> [{Last, Cur}]; parse_urlencoded(undefined, _, _, _) -> []. +apply_custom_headers(Headers, CustomHeaders) -> + {Doctype, Headers2} = case Headers -- [html] of + Headers -> {[], Headers}; + Other -> {[html], Other} + end, + M = maps:merge(maps:from_list(Headers2), + maps:from_list(CustomHeaders)), + Doctype ++ maps:to_list(M). -url_encode(A) -> - url_encode(A, <<>>). +% The following code is mostly taken from yaws_ssl.erl -url_encode(<>, Acc) when - (H >= $a andalso H =< $z) orelse - (H >= $A andalso H =< $Z) orelse - (H >= $0 andalso H =< $9) orelse - H == $_ orelse - H == $. orelse - H == $- orelse - H == $/ orelse - H == $: -> - url_encode(T, <>); -url_encode(<>, Acc) -> - case integer_to_hex(H) of - [X, Y] -> url_encode(T, <>); - [X] -> url_encode(T, <>) - end; -url_encode(<<>>, Acc) -> - Acc. +toupper(C) when C >= $a andalso C =< $z -> C - 32; +toupper(C) -> C. +tolower(C) when C >= $A andalso C =< $Z -> C + 32; +tolower(C) -> C. -integer_to_hex(I) -> - case catch erlang:integer_to_list(I, 16) of - {'EXIT', _} -> old_integer_to_hex(I); - Int -> Int - end. +normalize_header_name(Name) -> + normalize_header_name(Name, [], true). -old_integer_to_hex(I) when I < 10 -> integer_to_list(I); -old_integer_to_hex(I) when I < 16 -> [I - 10 + $A]; -old_integer_to_hex(I) when I >= 16 -> - N = trunc(I / 16), - old_integer_to_hex(N) ++ old_integer_to_hex(I rem 16). +normalize_header_name(<<"">>, Acc, _) -> + iolist_to_binary(Acc); +normalize_header_name(<<"-", Rest/binary>>, Acc, _) -> + normalize_header_name(Rest, [Acc, "-"], true); +normalize_header_name(<>, Acc, true) -> + normalize_header_name(Rest, [Acc, toupper(C)], false); +normalize_header_name(<>, Acc, false) -> + normalize_header_name(Rest, [Acc, tolower(C)], false). normalize_path(Path) -> normalize_path(Path, []). @@ -847,24 +899,29 @@ normalize_path([_Parent, <<"..">>|Path], Norm) -> normalize_path([Part | Path], Norm) -> normalize_path(Path, [Part|Norm]). -transform_listen_option(captcha, Opts) -> - [{captcha, true}|Opts]; -transform_listen_option(register, Opts) -> - [{register, true}|Opts]; -transform_listen_option(web_admin, Opts) -> - [{web_admin, true}|Opts]; -transform_listen_option(http_bind, Opts) -> - [{http_bind, true}|Opts]; -transform_listen_option(http_poll, Opts) -> - [{http_poll, true}|Opts]; -transform_listen_option({request_handlers, Hs}, Opts) -> - Hs1 = lists:map( - fun({PList, Mod}) when is_list(PList) -> - Path = iolist_to_binary([[$/, P] || P <- PList]), - {Path, Mod}; - (Opt) -> - Opt - end, Hs), - [{request_handlers, Hs1} | Opts]; -transform_listen_option(Opt, Opts) -> - [Opt|Opts]. +listen_opt_type(tag) -> + econf:binary(); +listen_opt_type(allow_unencrypted_sasl2) -> + econf:bool(); +listen_opt_type(request_handlers) -> + econf:map( + econf:and_then( + econf:binary(), + fun(Path) -> str:tokens(Path, <<"/">>) end), + econf:beam([[{socket_handoff, 3}, {process, 2}]])); +listen_opt_type(custom_headers) -> + econf:map( + econf:binary(), + econf:binary()). + +listen_options() -> + [{ciphers, undefined}, + {dhfile, undefined}, + {cafile, undefined}, + {protocol_options, undefined}, + {tls, false}, + {tls_compression, false}, + {allow_unencrypted_sasl2, false}, + {request_handlers, []}, + {tag, <<>>}, + {custom_headers, []}]. diff --git a/src/ejabberd_http_bind.erl b/src/ejabberd_http_bind.erl deleted file mode 100644 index 234ccf35a..000000000 --- a/src/ejabberd_http_bind.erl +++ /dev/null @@ -1,1236 +0,0 @@ -%%%---------------------------------------------------------------------- -%%% File : ejabberd_http_bind.erl -%%% Author : Stefan Strigler -%%% Purpose : Implements XMPP over BOSH (XEP-0206) (formerly known as -%%% HTTP Binding) -%%% Created : 21 Sep 2005 by Stefan Strigler -%%% Modified: may 2009 by Mickael Remond, Alexey Schepin -%%% Id : $Id: ejabberd_http_bind.erl 953 2009-05-07 10:40:40Z alexey $ -%%%---------------------------------------------------------------------- - --module(ejabberd_http_bind). - --behaviour(gen_fsm). - -%% External exports --export([start_link/3, - init/1, - handle_event/3, - handle_sync_event/4, - code_change/4, - handle_info/3, - terminate/3, - send/2, - send_xml/2, - sockname/1, - peername/1, - setopts/2, - controlling_process/2, - become_controller/2, - custom_receiver/1, - reset_stream/1, - change_shaper/2, - monitor/1, - close/1, - start/4, - handle_session_start/8, - handle_http_put/7, - http_put/7, - http_get/2, - prepare_response/4, - process_request/2]). - --include("ejabberd.hrl"). --include("logger.hrl"). - --include("jlib.hrl"). - --include("ejabberd_http.hrl"). - --include("http_bind.hrl"). - --record(http_bind, - {id, pid, to, hold, wait, process_delay, version}). - --define(NULL_PEER, {{0, 0, 0, 0}, 0}). - -%% http binding request --record(hbr, {rid, - key, - out}). - --record(state, {id, - rid = none, - key, - socket, - output = "", - input = queue:new(), - waiting_input = false, - shaper_state, - shaper_timer, - last_receiver, - last_poll, - http_receiver, - out_of_order_receiver = false, - wait_timer, - ctime = 0, - timer, - pause=0, - unprocessed_req_list = [], % list of request that have been delayed for proper reordering: {Request, PID} - req_list = [], % list of requests (cache) - max_inactivity, - max_pause, - ip = ?NULL_PEER - }). - -%% Internal request format: --record(http_put, {rid, - attrs, - payload, - payload_size, - hold, - stream, - ip}). - -%%-define(DBGFSM, true). --ifdef(DBGFSM). - --define(FSMOPTS, [{debug, [trace]}]). - --else. - --define(FSMOPTS, []). - --endif. - -%% Wait 100ms before continue processing, to allow the client provide more related stanzas. --define(BOSH_VERSION, <<"1.8">>). - --define(NS_CLIENT, <<"jabber:client">>). - --define(NS_BOSH, <<"urn:xmpp:xbosh">>). - --define(NS_HTTP_BIND, - <<"http://jabber.org/protocol/httpbind">>). - --define(MAX_REQUESTS, 2). - --define(MIN_POLLING, 2000000). - --define(MAX_WAIT, 3600). - --define(MAX_INACTIVITY, 30000). - --define(MAX_PAUSE, 120). - --define(PROCESS_DELAY_DEFAULT, 100). - --define(PROCESS_DELAY_MIN, 0). - --define(PROCESS_DELAY_MAX, 1000). - -%% Line copied from mod_http_bind.erl --define(PROCNAME_MHB, ejabberd_mod_http_bind). - -%%%---------------------------------------------------------------------- -%%% API -%%%---------------------------------------------------------------------- -%% TODO: If compile with no supervisor option, start the session without -%% supervisor -start(XMPPDomain, Sid, Key, IP) -> - ?DEBUG("Starting session", []), - SupervisorProc = gen_mod:get_module_proc(XMPPDomain, ?PROCNAME_MHB), - case catch supervisor:start_child(SupervisorProc, [Sid, Key, IP]) of - {ok, Pid} -> {ok, Pid}; - _ -> check_bind_module(XMPPDomain), - {error, "Cannot start HTTP bind session"} - end. - -start_link(Sid, Key, IP) -> - gen_fsm:start_link(?MODULE, [Sid, Key, IP], ?FSMOPTS). - -send({http_bind, FsmRef, _IP}, Packet) -> - gen_fsm:sync_send_all_state_event(FsmRef, - {send, Packet}). - -send_xml({http_bind, FsmRef, _IP}, Packet) -> - gen_fsm:sync_send_all_state_event(FsmRef, - {send_xml, Packet}). - -setopts({http_bind, FsmRef, _IP}, Opts) -> - case lists:member({active, once}, Opts) of - true -> - gen_fsm:send_all_state_event(FsmRef, {activate, self()}); - _ -> - ok - end. - -controlling_process(_Socket, _Pid) -> ok. - -custom_receiver({http_bind, FsmRef, _IP}) -> - {receiver, ?MODULE, FsmRef}. - -become_controller(FsmRef, C2SPid) -> - gen_fsm:send_all_state_event(FsmRef, - {become_controller, C2SPid}). - -reset_stream({http_bind, _FsmRef, _IP}) -> - ok. - -change_shaper({http_bind, FsmRef, _IP}, Shaper) -> - gen_fsm:send_all_state_event(FsmRef, - {change_shaper, Shaper}). - -monitor({http_bind, FsmRef, _IP}) -> - erlang:monitor(process, FsmRef). - -close({http_bind, FsmRef, _IP}) -> - catch gen_fsm:sync_send_all_state_event(FsmRef, - {stop, close}). - -sockname(_Socket) -> {ok, ?NULL_PEER}. - -peername({http_bind, _FsmRef, IP}) -> {ok, IP}. - - -%% Entry point for data coming from client through ejabberd HTTP server: -process_request(Data, IP) -> - Opts1 = ejabberd_c2s_config:get_c2s_limits(), - Opts = [{xml_socket, true} | Opts1], - MaxStanzaSize = case lists:keysearch(max_stanza_size, 1, - Opts) - of - {value, {_, Size}} -> Size; - _ -> infinity - end, - PayloadSize = iolist_size(Data), - case catch parse_request(Data, PayloadSize, - MaxStanzaSize) - of - %% No existing session: - {ok, {<<"">>, Rid, Attrs, Payload}} -> - case xml:get_attr_s(<<"to">>, Attrs) of - <<"">> -> - ?DEBUG("Session not created (Improper addressing)", []), - {200, ?HEADER, - <<"">>}; - XmppDomain -> - Sid = p1_sha:sha(term_to_binary({now(), make_ref()})), - case start(XmppDomain, Sid, <<"">>, IP) of - {error, _} -> - {500, ?HEADER, - <<"Internal Server Error">>}; - {ok, Pid} -> - handle_session_start(Pid, XmppDomain, Sid, Rid, Attrs, - Payload, PayloadSize, IP) - end - end; - %% Existing session - {ok, {Sid, Rid, Attrs, Payload1}} -> - StreamStart = case xml:get_attr_s(<<"xmpp:restart">>, - Attrs) - of - <<"true">> -> true; - _ -> false - end, - Payload2 = case xml:get_attr_s(<<"type">>, Attrs) of - <<"terminate">> -> - Payload1 ++ [{xmlstreamend, <<"stream:stream">>}]; - _ -> Payload1 - end, - handle_http_put(Sid, Rid, Attrs, Payload2, PayloadSize, - StreamStart, IP); - {size_limit, Sid} -> - case mnesia:dirty_read({http_bind, Sid}) of - {error, _} -> {404, ?HEADER, <<"">>}; - {ok, #http_bind{pid = FsmRef}} -> - gen_fsm:sync_send_all_state_event(FsmRef, - {stop, close}), - {200, ?HEADER, - <<"Request Too Large">>} - end; - _ -> - ?DEBUG("Received bad request: ~p", [Data]), - {400, ?HEADER, <<"">>} - end. - -handle_session_start(Pid, XmppDomain, Sid, Rid, Attrs, - Payload, PayloadSize, IP) -> - ?DEBUG("got pid: ~p", [Pid]), - Wait = case str:to_integer(xml:get_attr_s(<<"wait">>, - Attrs)) - of - {error, _} -> ?MAX_WAIT; - {CWait, _} -> - if CWait > (?MAX_WAIT) -> ?MAX_WAIT; - true -> CWait - end - end, - Hold = case str:to_integer(xml:get_attr_s(<<"hold">>, - Attrs)) - of - {error, _} -> (?MAX_REQUESTS) - 1; - {CHold, _} -> - if CHold > (?MAX_REQUESTS) - 1 -> (?MAX_REQUESTS) - 1; - true -> CHold - end - end, - Pdelay = case - str:to_integer(xml:get_attr_s(<<"process-delay">>, - Attrs)) - of - {error, _} -> ?PROCESS_DELAY_DEFAULT; - {CPdelay, _} - when ((?PROCESS_DELAY_MIN) =< CPdelay) and - (CPdelay =< (?PROCESS_DELAY_MAX)) -> - CPdelay; - {CPdelay, _} -> - lists:max([lists:min([CPdelay, ?PROCESS_DELAY_MAX]), - ?PROCESS_DELAY_MIN]) - end, - Version = case catch - list_to_float(binary_to_list(xml:get_attr_s(<<"ver">>, Attrs))) - of - {'EXIT', _} -> 0.0; - V -> V - end, - XmppVersion = xml:get_attr_s(<<"xmpp:version">>, Attrs), - ?DEBUG("Create session: ~p", [Sid]), - mnesia:dirty_write( - #http_bind{id = Sid, - pid = Pid, - to = {XmppDomain, - XmppVersion}, - hold = Hold, - wait = Wait, - process_delay = Pdelay, - version = Version - }), - handle_http_put(Sid, Rid, Attrs, Payload, PayloadSize, true, IP). - -%%%---------------------------------------------------------------------- -%%% Callback functions from gen_fsm -%%%---------------------------------------------------------------------- - -%%---------------------------------------------------------------------- -%% Func: init/1 -%% Returns: {ok, StateName, StateData} | -%% {ok, StateName, StateData, Timeout} | -%% ignore | -%% {stop, StopReason} -%%---------------------------------------------------------------------- -init([Sid, Key, IP]) -> - ?DEBUG("started: ~p", [{Sid, Key, IP}]), - Opts1 = ejabberd_c2s_config:get_c2s_limits(), - Opts = [{xml_socket, true} | Opts1], - Shaper = none, - ShaperState = shaper:new(Shaper), - Socket = {http_bind, self(), IP}, - ejabberd_socket:start(ejabberd_c2s, ?MODULE, Socket, Opts), - Timer = erlang:start_timer(?MAX_INACTIVITY, self(), []), - {ok, loop, #state{id = Sid, - key = Key, - socket = Socket, - shaper_state = ShaperState, - max_inactivity = ?MAX_INACTIVITY, - max_pause = ?MAX_PAUSE, - timer = Timer}}. - -%%---------------------------------------------------------------------- -%% Func: handle_event/3 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} -%%---------------------------------------------------------------------- -handle_event({become_controller, C2SPid}, StateName, StateData) -> - case StateData#state.input of - cancel -> - {next_state, StateName, - StateData#state{waiting_input = C2SPid}}; - Input -> - lists:foreach(fun (Event) -> C2SPid ! Event end, - queue:to_list(Input)), - {next_state, StateName, - StateData#state{input = queue:new(), - waiting_input = C2SPid}} - end; -handle_event({change_shaper, Shaper}, StateName, - StateData) -> - NewShaperState = shaper:new(Shaper), - {next_state, StateName, - StateData#state{shaper_state = NewShaperState}}; -handle_event(_Event, StateName, StateData) -> - {next_state, StateName, StateData}. - -%%---------------------------------------------------------------------- -%% Func: handle_sync_event/4 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {reply, Reply, NextStateName, NextStateData} | -%% {reply, Reply, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} | -%% {stop, Reason, Reply, NewStateData} -%%---------------------------------------------------------------------- -handle_sync_event({send_xml, Packet}, _From, StateName, - #state{http_receiver = undefined} = StateData) -> - Output = [Packet | StateData#state.output], - Reply = ok, - {reply, Reply, StateName, - StateData#state{output = Output}}; -handle_sync_event({send_xml, Packet}, _From, StateName, - #state{out_of_order_receiver = true} = StateData) -> - Output = [Packet | StateData#state.output], - Reply = ok, - {reply, Reply, StateName, - StateData#state{output = Output}}; -handle_sync_event({send_xml, Packet}, _From, StateName, - StateData) -> - Output = [Packet | StateData#state.output], - cancel_timer(StateData#state.timer), - Timer = set_inactivity_timer(StateData#state.pause, - StateData#state.max_inactivity), - HTTPReply = {ok, Output}, - gen_fsm:reply(StateData#state.http_receiver, HTTPReply), - cancel_timer(StateData#state.wait_timer), - Rid = StateData#state.rid, - ReqList = [#hbr{rid = Rid, key = StateData#state.key, - out = Output} - | [El - || El <- StateData#state.req_list, El#hbr.rid /= Rid]], - Reply = ok, - {reply, Reply, StateName, - StateData#state{output = [], http_receiver = undefined, - req_list = ReqList, wait_timer = undefined, - timer = Timer}}; - -handle_sync_event({stop,close}, _From, _StateName, StateData) -> - Reply = ok, - {stop, normal, Reply, StateData}; -handle_sync_event({stop,stream_closed}, _From, _StateName, StateData) -> - Reply = ok, - {stop, normal, Reply, StateData}; -handle_sync_event({stop,Reason}, _From, _StateName, StateData) -> - ?DEBUG("Closing bind session ~p - Reason: ~p", [StateData#state.id, Reason]), - Reply = ok, - {stop, normal, Reply, StateData}; -%% HTTP PUT: Receive packets from the client -handle_sync_event(#http_put{rid = Rid}, _From, - StateName, StateData) - when StateData#state.shaper_timer /= undefined -> - Pause = case - erlang:read_timer(StateData#state.shaper_timer) - of - false -> 0; - P -> P - end, - Reply = {wait, Pause}, - ?DEBUG("Shaper timer for RID ~p: ~p", [Rid, Reply]), - {reply, Reply, StateName, StateData}; -handle_sync_event(#http_put{payload_size = - PayloadSize} = - Request, - _From, StateName, StateData) -> - ?DEBUG("New request: ~p", [Request]), - {NewShaperState, NewShaperTimer} = - update_shaper(StateData#state.shaper_state, - PayloadSize), - handle_http_put_event(Request, StateName, - StateData#state{shaper_state = NewShaperState, - shaper_timer = NewShaperTimer}); -%% HTTP GET: send packets to the client -handle_sync_event({http_get, Rid, Wait, Hold}, From, StateName, StateData) -> - %% setup timer - TNow = tnow(), - if - (Hold > 0) and - ((StateData#state.output == []) or (StateData#state.rid < Rid)) and - ((TNow - StateData#state.ctime) < (Wait*1000*1000)) and - (StateData#state.rid =< Rid) and - (StateData#state.pause == 0) -> - send_receiver_reply(StateData#state.http_receiver, {ok, empty}), - cancel_timer(StateData#state.wait_timer), - WaitTimer = erlang:start_timer(Wait * 1000, self(), []), - %% MR: Not sure we should cancel the state timer here. - cancel_timer(StateData#state.timer), - {next_state, StateName, StateData#state{ - http_receiver = From, - out_of_order_receiver = StateData#state.rid < Rid, - wait_timer = WaitTimer, - timer = undefined}}; - true -> - cancel_timer(StateData#state.timer), - Reply = {ok, StateData#state.output}, - %% save request - ReqList = [#hbr{rid = Rid, - key = StateData#state.key, - out = StateData#state.output - } | - [El || El <- StateData#state.req_list, - El#hbr.rid /= Rid ] - ], - if - (StateData#state.http_receiver /= undefined) and - StateData#state.out_of_order_receiver -> - {reply, Reply, StateName, StateData#state{ - output = [], - timer = undefined, - req_list = ReqList, - out_of_order_receiver = false}}; - true -> - send_receiver_reply(StateData#state.http_receiver, {ok, empty}), - cancel_timer(StateData#state.wait_timer), - Timer = set_inactivity_timer(StateData#state.pause, - StateData#state.max_inactivity), - {reply, Reply, StateName, - StateData#state{output = [], - http_receiver = undefined, - wait_timer = undefined, - timer = Timer, - req_list = ReqList}} - end - end; -handle_sync_event(peername, _From, StateName, - StateData) -> - Reply = {ok, StateData#state.ip}, - {reply, Reply, StateName, StateData}; -handle_sync_event(_Event, _From, StateName, - StateData) -> - Reply = ok, {reply, Reply, StateName, StateData}. - -code_change(_OldVsn, StateName, StateData, _Extra) -> - {ok, StateName, StateData}. - -%%---------------------------------------------------------------------- -%% Func: handle_info/3 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} -%%---------------------------------------------------------------------- -%% We reached the max_inactivity timeout: -handle_info({timeout, Timer, _}, _StateName, - #state{id = SID, timer = Timer} = StateData) -> - ?INFO_MSG("Session timeout. Closing the HTTP bind " - "session: ~p", - [SID]), - {stop, normal, StateData}; -handle_info({timeout, WaitTimer, _}, StateName, - #state{wait_timer = WaitTimer} = StateData) -> - if StateData#state.http_receiver /= undefined -> - cancel_timer(StateData#state.timer), - Timer = set_inactivity_timer(StateData#state.pause, - StateData#state.max_inactivity), - gen_fsm:reply(StateData#state.http_receiver, - {ok, empty}), - Rid = StateData#state.rid, - ReqList = [#hbr{rid = Rid, key = StateData#state.key, - out = []} - | [El - || El <- StateData#state.req_list, El#hbr.rid /= Rid]], - {next_state, StateName, - StateData#state{http_receiver = undefined, - req_list = ReqList, wait_timer = undefined, - timer = Timer}}; - true -> {next_state, StateName, StateData} - end; -handle_info({timeout, ShaperTimer, _}, StateName, - #state{shaper_timer = ShaperTimer} = StateData) -> - {next_state, StateName, StateData#state{shaper_timer = undefined}}; - -handle_info(_, StateName, StateData) -> - {next_state, StateName, StateData}. - -%%---------------------------------------------------------------------- -%% Func: terminate/3 -%% Purpose: Shutdown the fsm -%% Returns: any -%%---------------------------------------------------------------------- -terminate(_Reason, _StateName, StateData) -> - ?DEBUG("terminate: Deleting session ~s", - [StateData#state.id]), - mnesia:dirty_delete({http_bind, StateData#state.id}), - send_receiver_reply(StateData#state.http_receiver, - {ok, terminate}), - case StateData#state.waiting_input of - false -> ok; - C2SPid -> gen_fsm:send_event(C2SPid, closed) - end, - ok. - -%%%---------------------------------------------------------------------- -%%% Internal functions -%%%---------------------------------------------------------------------- - -%% PUT / Get processing: -handle_http_put_event(#http_put{rid = Rid, - attrs = Attrs, hold = Hold} = - Request, - StateName, StateData) -> - ?DEBUG("New request: ~p", [Request]), - RidAllow = rid_allow(StateData#state.rid, Rid, Attrs, - Hold, StateData#state.max_pause), - case RidAllow of - buffer -> - ?DEBUG("Buffered request: ~p", [Request]), - PendingRequests = StateData#state.unprocessed_req_list, - Requests = lists:keydelete(Rid, 2, PendingRequests), - ReqList = [#hbr{rid = Rid, key = StateData#state.key, - out = []} - | [El - || El <- StateData#state.req_list, - El#hbr.rid > Rid - 1 - Hold]], - ?DEBUG("reqlist: ~p", [ReqList]), - UnprocessedReqList = [Request | Requests], - cancel_timer(StateData#state.timer), - Timer = set_inactivity_timer(0, - StateData#state.max_inactivity), - {reply, ok, StateName, - StateData#state{unprocessed_req_list = - UnprocessedReqList, - req_list = ReqList, timer = Timer}, - hibernate}; - _ -> - process_http_put(Request, StateName, StateData, - RidAllow) - end. - -process_http_put(#http_put{rid = Rid, attrs = Attrs, - payload = Payload, hold = Hold, stream = StreamTo, - ip = IP} = - Request, - StateName, StateData, RidAllow) -> - ?DEBUG("Actually processing request: ~p", [Request]), - Key = xml:get_attr_s(<<"key">>, Attrs), - NewKey = xml:get_attr_s(<<"newkey">>, Attrs), - KeyAllow = case RidAllow of - repeat -> true; - false -> false; - {true, _} -> - case StateData#state.key of - <<"">> -> true; - OldKey -> - NextKey = p1_sha:sha(Key), - ?DEBUG("Key/OldKey/NextKey: ~s/~s/~s", - [Key, OldKey, NextKey]), - if OldKey == NextKey -> true; - true -> ?DEBUG("wrong key: ~s", [Key]), false - end - end - end, - TNow = tnow(), - LastPoll = if Payload == [] -> TNow; - true -> 0 - end, - if (Payload == []) and (Hold == 0) and - (TNow - StateData#state.last_poll < (?MIN_POLLING)) -> - Reply = {error, polling_too_frequently}, - {reply, Reply, StateName, StateData}; - KeyAllow -> - case RidAllow of - false -> - Reply = {error, not_exists}, - {reply, Reply, StateName, StateData}; - repeat -> - ?DEBUG("REPEATING ~p", [Rid]), - case [El#hbr.out - || El <- StateData#state.req_list, El#hbr.rid == Rid] - of - [] -> {error, not_exists}; - [Out | _XS] -> - if (Rid == StateData#state.rid) and - (StateData#state.http_receiver /= undefined) -> - {reply, ok, StateName, StateData}; - true -> - Reply = {repeat, lists:reverse(Out)}, - {reply, Reply, StateName, - StateData#state{last_poll = LastPoll}} - end - end; - {true, Pause} -> - SaveKey = if NewKey == <<"">> -> Key; - true -> NewKey - end, - ?DEBUG(" -- SaveKey: ~s~n", [SaveKey]), - ReqList1 = [El - || El <- StateData#state.req_list, - El#hbr.rid > Rid - 1 - Hold], - ReqList = case lists:keymember(Rid, #hbr.rid, ReqList1) - of - true -> ReqList1; - false -> - [#hbr{rid = Rid, key = StateData#state.key, - out = []} - | ReqList1] - end, - ?DEBUG("reqlist: ~p", [ReqList]), - cancel_timer(StateData#state.timer), - Timer = set_inactivity_timer(Pause, - StateData#state.max_inactivity), - case StateData#state.waiting_input of - false -> - Input = lists:foldl(fun queue:in/2, - StateData#state.input, Payload), - Reply = ok, - process_buffered_request(Reply, StateName, - StateData#state{input = Input, - rid = Rid, - key = SaveKey, - ctime = TNow, - timer = Timer, - pause = Pause, - last_poll = - LastPoll, - req_list = - ReqList, - ip = IP}); - C2SPid -> - case StreamTo of - {To, <<"">>} -> - gen_fsm:send_event(C2SPid, - {xmlstreamstart, - <<"stream:stream">>, - [{<<"to">>, To}, - {<<"xmlns">>, ?NS_CLIENT}, - {<<"xmlns:stream">>, - ?NS_STREAM}]}); - {To, Version} -> - gen_fsm:send_event(C2SPid, - {xmlstreamstart, - <<"stream:stream">>, - [{<<"to">>, To}, - {<<"xmlns">>, ?NS_CLIENT}, - {<<"version">>, Version}, - {<<"xmlns:stream">>, - ?NS_STREAM}]}); - _ -> ok - end, - MaxInactivity = get_max_inactivity(StreamTo, - StateData#state.max_inactivity), - MaxPause = get_max_inactivity(StreamTo, - StateData#state.max_pause), - ?DEBUG("really sending now: ~p", [Payload]), - lists:foreach(fun ({xmlstreamend, End}) -> - gen_fsm:send_event(C2SPid, - {xmlstreamend, - End}); - (El) -> - gen_fsm:send_event(C2SPid, - {xmlstreamelement, - El}) - end, - Payload), - Reply = ok, - process_buffered_request(Reply, StateName, - StateData#state{input = - queue:new(), - rid = Rid, - key = SaveKey, - ctime = TNow, - timer = Timer, - pause = Pause, - last_poll = - LastPoll, - req_list = - ReqList, - max_inactivity = - MaxInactivity, - max_pause = - MaxPause, - ip = IP}) - end - end; - true -> - Reply = {error, bad_key}, - {reply, Reply, StateName, StateData} - end. - -process_buffered_request(Reply, StateName, StateData) -> - Rid = StateData#state.rid, - Requests = StateData#state.unprocessed_req_list, - case lists:keysearch(Rid + 1, 2, Requests) of - {value, Request} -> - ?DEBUG("Processing buffered request: ~p", [Request]), - NewRequests = lists:keydelete(Rid + 1, 2, Requests), - handle_http_put_event(Request, StateName, - StateData#state{unprocessed_req_list = - NewRequests}); - _ -> {reply, Reply, StateName, StateData, hibernate} - end. - -handle_http_put(Sid, Rid, Attrs, Payload, PayloadSize, - StreamStart, IP) -> - case http_put(Sid, Rid, Attrs, Payload, PayloadSize, - StreamStart, IP) - of - {error, not_exists} -> - ?DEBUG("no session associated with sid: ~p", [Sid]), - {404, ?HEADER, <<"">>}; - {{error, Reason}, Sess} -> - ?DEBUG("Error on HTTP put. Reason: ~p", [Reason]), - handle_http_put_error(Reason, Sess); - {{repeat, OutPacket}, Sess} -> - ?DEBUG("http_put said 'repeat!' ...~nOutPacket: ~p", - [OutPacket]), - send_outpacket(Sess, OutPacket); - {{wait, Pause}, _Sess} -> - ?DEBUG("Trafic Shaper: Delaying request ~p", [Rid]), - timer:sleep(Pause), - handle_http_put(Sid, Rid, Attrs, Payload, PayloadSize, - StreamStart, IP); - {ok, Sess} -> - prepare_response(Sess, Rid, [], StreamStart) - end. - -http_put(Sid, Rid, Attrs, Payload, PayloadSize, - StreamStart, IP) -> - ?DEBUG("Looking for session: ~p", [Sid]), - case mnesia:dirty_read({http_bind, Sid}) of - [] -> - {error, not_exists}; - [#http_bind{pid = FsmRef, hold=Hold, to={To, StreamVersion}}=Sess] -> - NewStream = - case StreamStart of - true -> - {To, StreamVersion}; - _ -> - <<"">> - end, - {gen_fsm:sync_send_all_state_event( - FsmRef, #http_put{rid = Rid, attrs = Attrs, payload = Payload, - payload_size = PayloadSize, hold = Hold, - stream = NewStream, ip = IP}, 30000), Sess} - end. - -handle_http_put_error(Reason, - #http_bind{pid = FsmRef, version = Version}) - when Version >= 0 -> - gen_fsm:sync_send_all_state_event(FsmRef, - {stop, {put_error, Reason}}), - case Reason of - not_exists -> - {200, ?HEADER, - xml:element_to_binary(#xmlel{name = <<"body">>, - attrs = - [{<<"xmlns">>, ?NS_HTTP_BIND}, - {<<"type">>, <<"terminate">>}, - {<<"condition">>, - <<"item-not-found">>}], - children = []})}; - bad_key -> - {200, ?HEADER, - xml:element_to_binary(#xmlel{name = <<"body">>, - attrs = - [{<<"xmlns">>, ?NS_HTTP_BIND}, - {<<"type">>, <<"terminate">>}, - {<<"condition">>, - <<"item-not-found">>}], - children = []})}; - polling_too_frequently -> - {200, ?HEADER, - xml:element_to_binary(#xmlel{name = <<"body">>, - attrs = - [{<<"xmlns">>, ?NS_HTTP_BIND}, - {<<"type">>, <<"terminate">>}, - {<<"condition">>, - <<"policy-violation">>}], - children = []})} - end; -handle_http_put_error(Reason, - #http_bind{pid = FsmRef}) -> - gen_fsm:sync_send_all_state_event(FsmRef, - {stop, {put_error_no_version, Reason}}), - case Reason of - not_exists -> %% bad rid - ?DEBUG("Closing HTTP bind session (Bad rid).", []), - {404, ?HEADER, <<"">>}; - bad_key -> - ?DEBUG("Closing HTTP bind session (Bad key).", []), - {404, ?HEADER, <<"">>}; - polling_too_frequently -> - ?DEBUG("Closing HTTP bind session (User polling " - "too frequently).", - []), - {403, ?HEADER, <<"">>} - end. - -%% Control RID ordering -rid_allow(none, _NewRid, _Attrs, _Hold, _MaxPause) -> - {true, 0}; -rid_allow(OldRid, NewRid, Attrs, Hold, MaxPause) -> - ?DEBUG("Previous rid / New rid: ~p/~p", - [OldRid, NewRid]), - if - %% We did not miss any packet, we can process it immediately: - NewRid == OldRid + 1 -> - case catch - jlib:binary_to_integer(xml:get_attr_s(<<"pause">>, - Attrs)) - of - {'EXIT', _} -> {true, 0}; - Pause1 when Pause1 =< MaxPause -> - ?DEBUG("got pause: ~p", [Pause1]), {true, Pause1}; - _ -> {true, 0} - end; - %% We have missed packets, we need to cached it to process it later on: - (OldRid < NewRid) and (NewRid =< OldRid + Hold + 1) -> - buffer; - (NewRid =< OldRid) and (NewRid > OldRid - Hold - 1) -> - repeat; - true -> false - end. - -update_shaper(ShaperState, PayloadSize) -> - {NewShaperState, Pause} = shaper:update(ShaperState, - PayloadSize), - if Pause > 0 -> - ShaperTimer = erlang:start_timer(Pause, self(), - activate), - {NewShaperState, ShaperTimer}; - true -> {NewShaperState, undefined} - end. - -prepare_response(Sess, Rid, OutputEls, StreamStart) -> - receive after Sess#http_bind.process_delay -> ok end, - case catch http_get(Sess, Rid) of - {ok, cancel} -> - {200, ?HEADER, - <<"">>}; - {ok, empty} -> - {200, ?HEADER, - <<"">>}; - {ok, terminate} -> - {200, ?HEADER, - <<"">>}; - {ok, ROutPacket} -> - OutPacket = lists:reverse(ROutPacket), - ?DEBUG("OutPacket: ~p", [OutputEls ++ OutPacket]), - prepare_outpacket_response(Sess, Rid, - OutputEls ++ OutPacket, StreamStart); - {'EXIT', {shutdown, _}} -> - {200, ?HEADER, - <<"">>}; - {'EXIT', _Reason} -> - {200, ?HEADER, - <<"">>} - end. - -%% Send output payloads on establised sessions -prepare_outpacket_response(Sess, _Rid, OutPacket, - false) -> - case catch send_outpacket(Sess, OutPacket) of - {'EXIT', _Reason} -> - ?DEBUG("Error in sending packet ~p ", [_Reason]), - {200, ?HEADER, - <<"">>}; - SendRes -> SendRes - end; -%% Handle a new session along with its output payload -prepare_outpacket_response(#http_bind{id = Sid, - wait = Wait, hold = Hold, to = To} = - _Sess, - _Rid, OutPacket, true) -> - case OutPacket of - [{xmlstreamstart, _, OutAttrs} | Els] -> - AuthID = xml:get_attr_s(<<"id">>, OutAttrs), - From = xml:get_attr_s(<<"from">>, OutAttrs), - Version = xml:get_attr_s(<<"version">>, OutAttrs), - OutEls = case Els of - [] -> []; - [{xmlstreamelement, - #xmlel{name = <<"stream:features">>, - attrs = StreamAttribs, children = StreamEls}} - | StreamTail] -> - TypedTail = [check_default_xmlns(OEl) - || {xmlstreamelement, OEl} <- StreamTail], - [#xmlel{name = <<"stream:features">>, - attrs = - [{<<"xmlns:stream">>, ?NS_STREAM}] ++ - StreamAttribs, - children = StreamEls}] - ++ TypedTail; - StreamTail -> - [check_default_xmlns(OEl) - || {xmlstreamelement, OEl} <- StreamTail] - end, - case OutEls of - [#xmlel{name = <<"stream:error">>}] -> - {200, ?HEADER, - <<"">>}; - _ -> - BOSH_attribs = [{<<"authid">>, AuthID}, - {<<"xmlns:xmpp">>, ?NS_BOSH}, - {<<"xmlns:stream">>, ?NS_STREAM}] - ++ - case OutEls of - [] -> []; - _ -> [{<<"xmpp:version">>, Version}] - end, - MaxInactivity = get_max_inactivity(To, ?MAX_INACTIVITY), - MaxPause = get_max_pause(To), - {200, ?HEADER, - xml:element_to_binary(#xmlel{name = <<"body">>, - attrs = - [{<<"xmlns">>, ?NS_HTTP_BIND}, - {<<"sid">>, Sid}, - {<<"wait">>, - iolist_to_binary(integer_to_list(Wait))}, - {<<"requests">>, - iolist_to_binary(integer_to_list(Hold - + - 1))}, - {<<"inactivity">>, - iolist_to_binary(integer_to_list(trunc(MaxInactivity - / - 1000)))}, - {<<"maxpause">>, - iolist_to_binary(integer_to_list(MaxPause))}, - {<<"polling">>, - iolist_to_binary(integer_to_list(trunc((?MIN_POLLING) - / - 1000000)))}, - {<<"ver">>, ?BOSH_VERSION}, - {<<"from">>, From}, - {<<"secure">>, <<"true">>}] - ++ BOSH_attribs, - children = OutEls})} - end; - _ -> - {200, ?HEADER, - <<"">>} - end. - -http_get(#http_bind{pid = FsmRef, wait = Wait, - hold = Hold}, - Rid) -> - gen_fsm:sync_send_all_state_event(FsmRef, - {http_get, Rid, Wait, Hold}, - 2 * (?MAX_WAIT) * 1000). - -send_outpacket(#http_bind{pid = FsmRef}, OutPacket) -> - case OutPacket of - [] -> - {200, ?HEADER, - <<"">>}; - [{xmlstreamend, _}] -> - gen_fsm:sync_send_all_state_event(FsmRef, - {stop, stream_closed}), - {200, ?HEADER, - <<"">>}; - _ -> - AllElements = lists:all(fun ({xmlstreamelement, - #xmlel{name = <<"stream:error">>}}) -> - false; - ({xmlstreamelement, _}) -> true; - ({xmlstreamraw, _}) -> true; - (_) -> false - end, - OutPacket), - case AllElements of - true -> - TypedEls = lists:foldl(fun ({xmlstreamelement, El}, - Acc) -> - Acc ++ - [xml:element_to_binary(check_default_xmlns(El))]; - ({xmlstreamraw, R}, Acc) -> - Acc ++ [R] - end, - [], OutPacket), - Body = <<"", - (iolist_to_binary(TypedEls))/binary, "">>, - ?DEBUG(" --- outgoing data --- ~n~s~n --- END " - "--- ~n", - [Body]), - {200, ?HEADER, Body}; - false -> - case OutPacket of - [{xmlstreamstart, _, _} | SEls] -> - OutEls = case SEls of - [{xmlstreamelement, - #xmlel{name = <<"stream:features">>, - attrs = StreamAttribs, - children = StreamEls}} - | StreamTail] -> - TypedTail = [check_default_xmlns(OEl) - || {xmlstreamelement, OEl} - <- StreamTail], - [#xmlel{name = <<"stream:features">>, - attrs = - [{<<"xmlns:stream">>, - ?NS_STREAM}] - ++ StreamAttribs, - children = StreamEls}] - ++ TypedTail; - StreamTail -> - [check_default_xmlns(OEl) - || {xmlstreamelement, OEl} <- StreamTail] - end, - {200, ?HEADER, - xml:element_to_binary(#xmlel{name = <<"body">>, - attrs = - [{<<"xmlns">>, - ?NS_HTTP_BIND}], - children = OutEls})}; - _ -> - SErrCond = lists:filter(fun ({xmlstreamelement, - #xmlel{name = - <<"stream:error">>}}) -> - true; - (_) -> false - end, - OutPacket), - StreamErrCond = case SErrCond of - [] -> null; - [{xmlstreamelement, - #xmlel{} = StreamErrorTag} - | _] -> - [StreamErrorTag] - end, - gen_fsm:sync_send_all_state_event(FsmRef, - {stop, - {stream_error, - OutPacket}}), - case StreamErrCond of - null -> - {200, ?HEADER, - <<"">>}; - _ -> - {200, ?HEADER, - <<"", - (elements_to_string(StreamErrCond))/binary, - "">>} - end - end - end - end. - -parse_request(Data, PayloadSize, MaxStanzaSize) -> - ?DEBUG("--- incoming data --- ~n~s~n --- END " - "--- ", - [Data]), - case xml_stream:parse_element(Data) of - #xmlel{name = <<"body">>, attrs = Attrs, - children = Els} -> - Xmlns = xml:get_attr_s(<<"xmlns">>, Attrs), - if Xmlns /= (?NS_HTTP_BIND) -> {error, bad_request}; - true -> - case catch - jlib:binary_to_integer(xml:get_attr_s(<<"rid">>, - Attrs)) - of - {'EXIT', _} -> {error, bad_request}; - Rid -> - FixedEls = lists:filter(fun (I) -> - case I of - #xmlel{} -> true; - _ -> false - end - end, - Els), - Sid = xml:get_attr_s(<<"sid">>, Attrs), - if PayloadSize =< MaxStanzaSize -> - {ok, {Sid, Rid, Attrs, FixedEls}}; - true -> {size_limit, Sid} - end - end - end; - #xmlel{} -> {error, bad_request}; - {error, _Reason} -> {error, bad_request} - end. - -send_receiver_reply(undefined, _Reply) -> ok; -send_receiver_reply(Receiver, Reply) -> - gen_fsm:reply(Receiver, Reply). - -%% Cancel timer and empty message queue. -cancel_timer(undefined) -> ok; -cancel_timer(Timer) -> - erlang:cancel_timer(Timer), - receive {timeout, Timer, _} -> ok after 0 -> ok end. - -%% If client asked for a pause (pause > 0), we apply the pause value -%% as inactivity timer: -set_inactivity_timer(Pause, _MaxInactivity) - when Pause > 0 -> - erlang:start_timer(Pause * 1000, self(), []); -%% Otherwise, we apply the max_inactivity value as inactivity timer: -set_inactivity_timer(_Pause, MaxInactivity) -> - erlang:start_timer(MaxInactivity, self(), []). - -%% TODO: Use tail recursion and list reverse ? -elements_to_string([]) -> []; -elements_to_string([El | Els]) -> - [xml:element_to_binary(El) | elements_to_string(Els)]. - -%% @spec (To, Default::integer()) -> integer() -%% where To = [] | {Host::string(), Version::string()} -get_max_inactivity({Host, _}, Default) -> - case gen_mod:get_module_opt(Host, mod_http_bind, max_inactivity, - fun(I) when is_integer(I), I>0 -> I end, - undefined) - of - Seconds when is_integer(Seconds) -> Seconds * 1000; - undefined -> Default - end; -get_max_inactivity(_, Default) -> Default. - -get_max_pause({Host, _}) -> - gen_mod:get_module_opt(Host, mod_http_bind, max_pause, - fun(I) when is_integer(I), I>0 -> I end, - ?MAX_PAUSE); -get_max_pause(_) -> ?MAX_PAUSE. - -%% Current time as integer -tnow() -> - {TMegSec, TSec, TMSec} = now(), - (TMegSec * 1000000 + TSec) * 1000000 + TMSec. - -check_default_xmlns(#xmlel{name = Name, attrs = Attrs, - children = Els} = - El) -> - case xml:get_tag_attr_s(<<"xmlns">>, El) of - <<"">> -> - #xmlel{name = Name, - attrs = [{<<"xmlns">>, ?NS_CLIENT} | Attrs], - children = Els}; - _ -> El - end; -check_default_xmlns(El) -> El. - -%% Check that mod_http_bind has been defined in config file. -%% Print a warning in log file if this is not the case. -check_bind_module(XmppDomain) -> - case gen_mod:is_loaded(XmppDomain, mod_http_bind) of - true -> true; - false -> - ?ERROR_MSG("You are trying to use BOSH (HTTP Bind) " - "in host ~p, but the module mod_http_bind " - "is not started in that host. Configure " - "your BOSH client to connect to the correct " - "host, or add your desired host to the " - "configuration, or check your 'modules' " - "section in your ejabberd configuration " - "file.", - [XmppDomain]), - false - end. diff --git a/src/ejabberd_http_poll.erl b/src/ejabberd_http_poll.erl deleted file mode 100644 index 174c78211..000000000 --- a/src/ejabberd_http_poll.erl +++ /dev/null @@ -1,425 +0,0 @@ -%%%---------------------------------------------------------------------- -%%% File : ejabberd_http_poll.erl -%%% Author : Alexey Shchepin -%%% Purpose : HTTP Polling support (XEP-0025) -%%% Created : 4 Mar 2004 by Alexey Shchepin -%%% -%%% -%%% ejabberd, Copyright (C) 2002-2015 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(ejabberd_http_poll). - --author('alexey@process-one.net'). - --behaviour(gen_fsm). - -%% External exports --export([start_link/3, init/1, handle_event/3, - handle_sync_event/4, code_change/4, handle_info/3, - terminate/3, send/2, setopts/2, sockname/1, peername/1, - controlling_process/2, close/1, process/2]). - --include("ejabberd.hrl"). --include("logger.hrl"). - --include("jlib.hrl"). - --include("ejabberd_http.hrl"). - --record(http_poll, {id :: pid() | binary(), pid :: pid()}). - --type poll_socket() :: #http_poll{}. --export_type([poll_socket/0]). - --record(state, - {id, key, socket, output = [], input = <<"">>, - waiting_input = false, last_receiver, http_poll_timeout, - timer}). - -%-define(DBGFSM, true). - --ifdef(DBGFSM). - --define(FSMOPTS, [{debug, [trace]}]). - --else. - --define(FSMOPTS, []). - --endif. - --define(HTTP_POLL_TIMEOUT, 300). - --define(CT, - {<<"Content-Type">>, <<"text/xml; charset=utf-8">>}). - --define(BAD_REQUEST, - [?CT, {<<"Set-Cookie">>, <<"ID=-3:0; expires=-1">>}]). - -%%%---------------------------------------------------------------------- -%%% API -%%%---------------------------------------------------------------------- -start(ID, Key, IP) -> - mnesia:create_table(http_poll, - [{ram_copies, [node()]}, - {attributes, record_info(fields, http_poll)}]), - supervisor:start_child(ejabberd_http_poll_sup, [ID, Key, IP]). - -start_link(ID, Key, IP) -> - gen_fsm:start_link(?MODULE, [ID, Key, IP], ?FSMOPTS). - -send({http_poll, FsmRef, _IP}, Packet) -> - gen_fsm:sync_send_all_state_event(FsmRef, - {send, Packet}). - -setopts({http_poll, FsmRef, _IP}, Opts) -> - case lists:member({active, once}, Opts) of - true -> - gen_fsm:send_all_state_event(FsmRef, - {activate, self()}); - _ -> ok - end. - -sockname(_Socket) -> {ok, {{0, 0, 0, 0}, 0}}. - -peername({http_poll, _FsmRef, IP}) -> {ok, IP}. - -controlling_process(_Socket, _Pid) -> ok. - -close({http_poll, FsmRef, _IP}) -> - catch gen_fsm:sync_send_all_state_event(FsmRef, close). - -process([], - #request{data = Data, ip = IP} = _Request) -> - case catch parse_request(Data) of - {ok, ID1, Key, NewKey, Packet} -> - ID = if - (ID1 == <<"0">>) or (ID1 == <<"mobile">>) -> - NewID = p1_sha:sha(term_to_binary({now(), make_ref()})), - {ok, Pid} = start(NewID, <<"">>, IP), - mnesia:transaction( - fun() -> - mnesia:write(#http_poll{id = NewID, pid = Pid}) - end), - NewID; - true -> - ID1 - end, - case http_put(ID, Key, NewKey, Packet) of - {error, not_exists} -> - {200, ?BAD_REQUEST, <<"">>}; - {error, bad_key} -> - {200, ?BAD_REQUEST, <<"">>}; - ok -> - receive - after 100 -> ok - end, - case http_get(ID) of - {error, not_exists} -> - {200, ?BAD_REQUEST, <<"">>}; - {ok, OutPacket} -> - if - ID == ID1 -> - Cookie = <<"ID=", ID/binary, "; expires=-1">>, - {200, [?CT, {<<"Set-Cookie">>, Cookie}], - OutPacket}; - ID1 == <<"mobile">> -> - {200, [?CT], [ID, $\n, OutPacket]}; - true -> - Cookie = <<"ID=", ID/binary, "; expires=-1">>, - {200, [?CT, {<<"Set-Cookie">>, Cookie}], - OutPacket} - end - end - end; - _ -> - HumanHTMLxmlel = get_human_html_xmlel(), - {200, [?CT, {<<"Set-Cookie">>, <<"ID=-2:0; expires=-1">>}], HumanHTMLxmlel} - end; -process(_, _Request) -> - {400, [], - #xmlel{name = <<"h1">>, attrs = [], - children = [{xmlcdata, <<"400 Bad Request">>}]}}. - -%% Code copied from mod_http_bind.erl and customized -get_human_html_xmlel() -> - Heading = <<"ejabberd ", - (iolist_to_binary(atom_to_list(?MODULE)))/binary>>, - #xmlel{name = <<"html">>, - attrs = - [{<<"xmlns">>, <<"http://www.w3.org/1999/xhtml">>}], - children = - [#xmlel{name = <<"head">>, attrs = [], - children = - [#xmlel{name = <<"title">>, attrs = [], - children = [{xmlcdata, Heading}]}]}, - #xmlel{name = <<"body">>, attrs = [], - children = - [#xmlel{name = <<"h1">>, attrs = [], - children = [{xmlcdata, Heading}]}, - #xmlel{name = <<"p">>, attrs = [], - children = - [{xmlcdata, <<"An implementation of ">>}, - #xmlel{name = <<"a">>, - attrs = - [{<<"href">>, - <<"http://xmpp.org/extensions/xep-0025.html">>}], - children = - [{xmlcdata, - <<"Jabber HTTP Polling (XEP-0025)">>}]}]}, - #xmlel{name = <<"p">>, attrs = [], - children = - [{xmlcdata, - <<"This web page is only informative. To " - "use HTTP-Poll you need a Jabber/XMPP " - "client that supports it.">>}]}]}]}. - -%%%---------------------------------------------------------------------- -%%% Callback functions from gen_fsm -%%%---------------------------------------------------------------------- - -%%---------------------------------------------------------------------- -%% Func: init/1 -%% Returns: {ok, StateName, StateData} | -%% {ok, StateName, StateData, Timeout} | -%% ignore | -%% {stop, StopReason} -%%---------------------------------------------------------------------- -init([ID, Key, IP]) -> - ?INFO_MSG("started: ~p", [{ID, Key, IP}]), - Opts = ejabberd_c2s_config:get_c2s_limits(), - HTTPPollTimeout = ejabberd_config:get_option( - {http_poll_timeout, ?MYNAME}, - fun(I) when is_integer(I), I>0 -> I end, - ?HTTP_POLL_TIMEOUT) * 1000, - Socket = {http_poll, self(), IP}, - ejabberd_socket:start(ejabberd_c2s, ?MODULE, Socket, - Opts), - Timer = erlang:start_timer(HTTPPollTimeout, self(), []), - {ok, loop, - #state{id = ID, key = Key, socket = Socket, - http_poll_timeout = HTTPPollTimeout, timer = Timer}}. - -%%---------------------------------------------------------------------- -%% Func: StateName/2 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} -%% {stop, Reason, NewStateData} -%%---------------------------------------------------------------------- - -%%---------------------------------------------------------------------- -%% Func: StateName/3 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {reply, Reply, NextStateName, NextStateData} | -%% {reply, Reply, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} | -%% {stop, Reason, Reply, NewStateData} -%% {stop, Reason, Reply, NewStateData} -%%---------------------------------------------------------------------- -%state_name(Event, From, StateData) -> -% Reply = ok, -% {reply, Reply, state_name, StateData}. - -%%---------------------------------------------------------------------- -%% Func: handle_event/3 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} -%%---------------------------------------------------------------------- -handle_event({activate, From}, StateName, StateData) -> - case StateData#state.input of - <<"">> -> - {next_state, StateName, - StateData#state{waiting_input = {From, ok}}}; - Input -> - Receiver = From, - Receiver ! - {tcp, StateData#state.socket, Input}, - {next_state, StateName, - StateData#state{input = <<"">>, waiting_input = false, - last_receiver = Receiver}} - end; -handle_event(_Event, StateName, StateData) -> - {next_state, StateName, StateData}. - -%%---------------------------------------------------------------------- -%% Func: handle_sync_event/4 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {reply, Reply, NextStateName, NextStateData} | -%% {reply, Reply, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} | -%% {stop, Reason, Reply, NewStateData} -%%---------------------------------------------------------------------- -handle_sync_event({send, Packet}, _From, StateName, - StateData) -> - Packet2 = iolist_to_binary(Packet), - Output = StateData#state.output ++ [Packet2], - Reply = ok, - {reply, Reply, StateName, - StateData#state{output = Output}}; -handle_sync_event(stop, _From, _StateName, StateData) -> - Reply = ok, {stop, normal, Reply, StateData}; -handle_sync_event({http_put, Key, NewKey, Packet}, - _From, StateName, StateData) -> - Allow = case StateData#state.key of - <<"">> -> true; - OldKey -> - NextKey = jlib:encode_base64((p1_sha:sha1(Key))), - if OldKey == NextKey -> true; - true -> false - end - end, - if Allow -> - case StateData#state.waiting_input of - false -> - Input = <<(StateData#state.input)/binary, Packet/binary>>, - Reply = ok, - {reply, Reply, StateName, - StateData#state{input = Input, key = NewKey}}; - {Receiver, _Tag} -> - Receiver ! - {tcp, StateData#state.socket, iolist_to_binary(Packet)}, - cancel_timer(StateData#state.timer), - Timer = - erlang:start_timer(StateData#state.http_poll_timeout, - self(), []), - Reply = ok, - {reply, Reply, StateName, - StateData#state{waiting_input = false, - last_receiver = Receiver, key = NewKey, - timer = Timer}} - end; - true -> - Reply = {error, bad_key}, - {reply, Reply, StateName, StateData} - end; -handle_sync_event(http_get, _From, StateName, - StateData) -> - Reply = {ok, StateData#state.output}, - {reply, Reply, StateName, - StateData#state{output = []}}; -handle_sync_event(_Event, _From, StateName, - StateData) -> - Reply = ok, {reply, Reply, StateName, StateData}. - -code_change(_OldVsn, StateName, StateData, _Extra) -> - {ok, StateName, StateData}. - -%%---------------------------------------------------------------------- -%% Func: handle_info/3 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} -%%---------------------------------------------------------------------- -handle_info({timeout, Timer, _}, _StateName, - #state{timer = Timer} = StateData) -> - {stop, normal, StateData}; -handle_info(_, StateName, StateData) -> - {next_state, StateName, StateData}. - -%%---------------------------------------------------------------------- -%% Func: terminate/3 -%% Purpose: Shutdown the fsm -%% Returns: any -%%---------------------------------------------------------------------- -terminate(_Reason, _StateName, StateData) -> - mnesia:transaction( - fun() -> - mnesia:delete({http_poll, StateData#state.id}) - end), - case StateData#state.waiting_input of - false -> - case StateData#state.last_receiver of - undefined -> ok; - Receiver -> - Receiver ! {tcp_closed, StateData#state.socket} - end; - {Receiver, _Tag} -> - Receiver ! {tcp_closed, StateData#state.socket} - end, - catch resend_messages(StateData#state.output), - ok. - -%%%---------------------------------------------------------------------- -%%% Internal functions -%%%---------------------------------------------------------------------- - -http_put(ID, Key, NewKey, Packet) -> - case mnesia:dirty_read({http_poll, ID}) of - [] -> - {error, not_exists}; - [#http_poll{pid = FsmRef}] -> - gen_fsm:sync_send_all_state_event( - FsmRef, {http_put, Key, NewKey, Packet}) - end. - -http_get(ID) -> - case mnesia:dirty_read({http_poll, ID}) of - [] -> - {error, not_exists}; - [#http_poll{pid = FsmRef}] -> - gen_fsm:sync_send_all_state_event(FsmRef, http_get) - end. - -parse_request(Data) -> - Comma = str:chr(Data, $,), - Header = str:substr(Data, 1, Comma - 1), - Packet = str:substr(Data, Comma + 1, byte_size(Data)), - {ID, Key, NewKey} = case str:tokens(Header, <<";">>) of - [ID1] -> {ID1, <<"">>, <<"">>}; - [ID1, Key1] -> {ID1, Key1, Key1}; - [ID1, Key1, NewKey1] -> {ID1, Key1, NewKey1} - end, - {ok, ID, Key, NewKey, Packet}. - -cancel_timer(Timer) -> - erlang:cancel_timer(Timer), - receive {timeout, Timer, _} -> ok after 0 -> ok end. - -%% Resend the polled messages -resend_messages(Messages) -> -%% This function is used to resend messages that have been polled but not -%% delivered. - lists:foreach(fun (Packet) -> resend_message(Packet) - end, - Messages). - -resend_message(Packet) -> - #xmlel{name = Name} = ParsedPacket = - xml_stream:parse_element(Packet), - if Name == <<"iq">>; - Name == <<"message">>; - Name == <<"presence">> -> - From = get_jid(<<"from">>, ParsedPacket), - To = get_jid(<<"to">>, ParsedPacket), - ?DEBUG("Resend ~p ~p ~p~n", [From, To, ParsedPacket]), - ejabberd_router:route(From, To, ParsedPacket); - true -> ok - end. - -%% Type can be "from" or "to" -%% Parsed packet is a parsed Jabber packet. -get_jid(Type, ParsedPacket) -> - case xml:get_tag_attr(Type, ParsedPacket) of - {value, StringJid} -> jlib:string_to_jid(StringJid); - false -> jlib:make_jid(<<"">>, <<"">>, <<"">>) - end. diff --git a/src/ejabberd_http_ws.erl b/src/ejabberd_http_ws.erl new file mode 100644 index 000000000..c14ed2d58 --- /dev/null +++ b/src/ejabberd_http_ws.erl @@ -0,0 +1,322 @@ +%%%---------------------------------------------------------------------- +%%% File : ejabberd_websocket.erl +%%% Author : Eric Cestari +%%% Purpose : XMPP Websocket support +%%% Created : 09-10-2010 by Eric Cestari +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(ejabberd_http_ws). +-author('ecestari@process-one.net'). +-behaviour(xmpp_socket). +-behaviour(p1_fsm). + +-export([start/1, start_link/1, init/1, handle_event/3, + handle_sync_event/4, code_change/4, handle_info/3, + terminate/3, send_xml/2, setopts/2, sockname/1, + peername/1, controlling_process/2, get_owner/1, + reset_stream/1, close/1, change_shaper/2, + socket_handoff/3, get_transport/1]). + +-include("logger.hrl"). + +-include_lib("xmpp/include/xmpp.hrl"). + +-include("ejabberd_http.hrl"). + +-record(state, + {socket :: ws_socket(), + ping_interval :: non_neg_integer(), + ping_timer = make_ref() :: reference(), + pong_expected = false :: boolean(), + timeout :: non_neg_integer(), + timer = make_ref() :: reference(), + input = [] :: list(), + active = false :: boolean(), + c2s_pid :: pid(), + ws :: {#ws{}, pid()}}). + +%-define(DBGFSM, true). + +-ifdef(DBGFSM). + +-define(FSMOPTS, [{debug, [trace]}]). + +-else. + +-define(FSMOPTS, []). + +-endif. + +-type ws_socket() :: {http_ws, pid(), {inet:ip_address(), inet:port_number()}}. +-export_type([ws_socket/0]). + +start(WS) -> + p1_fsm:start(?MODULE, [WS], ?FSMOPTS). + +start_link(WS) -> + p1_fsm:start_link(?MODULE, [WS], ?FSMOPTS). + +send_xml({http_ws, FsmRef, _IP}, Packet) -> + case catch p1_fsm:sync_send_all_state_event(FsmRef, + {send_xml, Packet}, + 15000) + of + {'EXIT', {timeout, _}} -> {error, timeout}; + {'EXIT', _} -> {error, einval}; + Res -> Res + end. + +setopts({http_ws, FsmRef, _IP}, Opts) -> + case lists:member({active, once}, Opts) of + true -> + p1_fsm:send_all_state_event(FsmRef, + {activate, self()}); + _ -> ok + end. + +sockname(_Socket) -> {ok, {{0, 0, 0, 0}, 0}}. + +peername({http_ws, _FsmRef, IP}) -> {ok, IP}. + +controlling_process(_Socket, _Pid) -> ok. + +close({http_ws, FsmRef, _IP}) -> + catch p1_fsm:sync_send_all_state_event(FsmRef, close). + +reset_stream({http_ws, _FsmRef, _IP} = Socket) -> + Socket. + +change_shaper({http_ws, FsmRef, _IP}, Shaper) -> + p1_fsm:send_all_state_event(FsmRef, {new_shaper, Shaper}). + +get_transport(_Socket) -> + websocket. + +get_owner({http_ws, FsmRef, _IP}) -> + FsmRef. + +socket_handoff(LocalPath, Request, Opts) -> + ejabberd_websocket:socket_handoff(LocalPath, Request, Opts, ?MODULE, fun get_human_html_xmlel/0). + +%%% Internal + +init([{#ws{ip = IP, http_opts = HOpts}, _} = WS]) -> + SOpts = lists:filtermap(fun({stream_management, _}) -> true; + ({max_ack_queue, _}) -> true; + ({ack_timeout, _}) -> true; + ({resume_timeout, _}) -> true; + ({allow_unencrypted_sasl2, _}) -> true; + ({max_resume_timeout, _}) -> true; + ({resend_on_timeout, _}) -> true; + ({access, _}) -> true; + (_) -> false + end, HOpts), + Opts = ejabberd_c2s_config:get_c2s_limits() ++ SOpts, + PingInterval = ejabberd_option:websocket_ping_interval(), + WSTimeout = ejabberd_option:websocket_timeout(), + Socket = {http_ws, self(), IP}, + ?DEBUG("Client connected through websocket ~p", + [Socket]), + case ejabberd_c2s:start(?MODULE, Socket, [{receiver, self()}|Opts]) of + {ok, C2SPid} -> + ejabberd_c2s:accept(C2SPid), + Timer = erlang:start_timer(WSTimeout, self(), []), + {ok, loop, + #state{socket = Socket, timeout = WSTimeout, + timer = Timer, ws = WS, c2s_pid = C2SPid, + ping_interval = PingInterval}}; + {error, Reason} -> + {stop, Reason}; + ignore -> + ignore + end. + +handle_event({activate, From}, StateName, State) -> + State1 = case State#state.input of + [] -> State#state{active = true}; + Input -> + lists:foreach( + fun(I) when is_binary(I)-> + From ! {tcp, State#state.socket, I}; + (I2) -> + From ! {tcp, State#state.socket, [I2]} + end, Input), + State#state{active = false, input = []} + end, + {next_state, StateName, State1#state{c2s_pid = From}}; +handle_event({new_shaper, Shaper}, StateName, #state{ws = {_, WsPid}} = StateData) -> + WsPid ! {new_shaper, Shaper}, + {next_state, StateName, StateData}. + +handle_sync_event({send_xml, Packet}, _From, StateName, + #state{ws = {_, WsPid}} = StateData) -> + SN2 = case Packet of + {xmlstreamstart, _, Attrs} -> + Attrs2 = [{<<"xmlns">>, <<"urn:ietf:params:xml:ns:xmpp-framing">>} | + lists:keydelete(<<"xmlns">>, 1, lists:keydelete(<<"xmlns:stream">>, 1, Attrs))], + route_el(WsPid, #xmlel{name = <<"open">>, attrs = Attrs2}), + StateName; + {xmlstreamend, _} -> + route_el(WsPid, #xmlel{name = <<"close">>, + attrs = [{<<"xmlns">>, <<"urn:ietf:params:xml:ns:xmpp-framing">>}]}), + stream_end_sent; + {xmlstreamraw, <<"\r\n\r\n">>} -> + % cdata ping + StateName; + {xmlstreamelement, #xmlel{name = Name2} = El2} -> + El3 = case Name2 of + <<"stream:", _/binary>> -> + fxml:replace_tag_attr(<<"xmlns:stream">>, ?NS_STREAM, El2); + _ -> + case fxml:get_tag_attr_s(<<"xmlns">>, El2) of + <<"">> -> + fxml:replace_tag_attr(<<"xmlns">>, <<"jabber:client">>, El2); + _ -> + El2 + end + end, + route_el(WsPid, El3), + StateName + end, + {reply, ok, SN2, StateData}; +handle_sync_event(close, _From, StateName, #state{ws = {_, WsPid}} = StateData) + when StateName /= stream_end_sent -> + Close = #xmlel{name = <<"close">>, + attrs = [{<<"xmlns">>, <<"urn:ietf:params:xml:ns:xmpp-framing">>}]}, + route_text(WsPid, fxml:element_to_binary(Close)), + {stop, normal, StateData}; +handle_sync_event(close, _From, _StateName, StateData) -> + {stop, normal, StateData}. + +handle_info(closed, _StateName, StateData) -> + {stop, normal, StateData}; +handle_info({received, Packet}, StateName, StateDataI) -> + {StateData, Parsed} = parse(StateDataI, Packet), + SD = case StateData#state.active of + false -> + Input = StateData#state.input ++ Parsed, + StateData#state{input = Input}; + true -> + StateData#state.c2s_pid ! {tcp, StateData#state.socket, Parsed}, + setup_timers(StateData#state{active = false}) + end, + {next_state, StateName, SD}; +handle_info(PingPong, StateName, StateData) when PingPong == ping orelse + PingPong == pong -> + StateData2 = setup_timers(StateData), + {next_state, StateName, + StateData2#state{pong_expected = false}}; +handle_info({timeout, Timer, _}, _StateName, + #state{timer = Timer} = StateData) -> + ?DEBUG("Closing websocket connection from hitting inactivity timeout", []), + {stop, normal, StateData}; +handle_info({timeout, Timer, _}, StateName, + #state{ping_timer = Timer, ws = {_, WsPid}} = StateData) -> + case StateData#state.pong_expected of + false -> + misc:cancel_timer(StateData#state.ping_timer), + PingTimer = erlang:start_timer(StateData#state.ping_interval, + self(), []), + WsPid ! {ping, <<>>}, + {next_state, StateName, + StateData#state{ping_timer = PingTimer, pong_expected = true}}; + true -> + ?DEBUG("Closing websocket connection from missing pongs", []), + {stop, normal, StateData} + end; +handle_info(_, StateName, StateData) -> + {next_state, StateName, StateData}. + +code_change(_OldVsn, StateName, StateData, _Extra) -> + {ok, StateName, StateData}. + +terminate(_Reason, _StateName, StateData) -> + StateData#state.c2s_pid ! {tcp_closed, StateData#state.socket}. + +setup_timers(StateData) -> + misc:cancel_timer(StateData#state.timer), + Timer = erlang:start_timer(StateData#state.timeout, + self(), []), + misc:cancel_timer(StateData#state.ping_timer), + PingTimer = case StateData#state.ping_interval of + 0 -> StateData#state.ping_timer; + V -> erlang:start_timer(V, self(), []) + end, + StateData#state{timer = Timer, ping_timer = PingTimer, + pong_expected = false}. + +get_human_html_xmlel() -> + Heading = <<"ejabberd ", (misc:atom_to_binary(?MODULE))/binary>>, + #xmlel{name = <<"html">>, + attrs = + [{<<"xmlns">>, <<"http://www.w3.org/1999/xhtml">>}], + children = + [#xmlel{name = <<"head">>, attrs = [], + children = + [#xmlel{name = <<"title">>, attrs = [], + children = [{xmlcdata, Heading}]}]}, + #xmlel{name = <<"body">>, attrs = [], + children = + [#xmlel{name = <<"h1">>, attrs = [], + children = [{xmlcdata, Heading}]}, + #xmlel{name = <<"p">>, attrs = [], + children = + [{xmlcdata, <<"An implementation of ">>}, + #xmlel{name = <<"a">>, + attrs = + [{<<"href">>, + <<"http://tools.ietf.org/html/rfc6455">>}], + children = + [{xmlcdata, + <<"WebSocket protocol">>}]}]}, + #xmlel{name = <<"p">>, attrs = [], + children = + [{xmlcdata, + <<"This web page is only informative. To " + "use WebSocket connection you need a Jabber/XMPP " + "client that supports it.">>}]}]}]}. + + +parse(State, Data) -> + El = fxml_stream:parse_element(Data), + case El of + #xmlel{name = <<"open">>, attrs = Attrs} -> + Attrs2 = [{<<"xmlns:stream">>, ?NS_STREAM}, {<<"xmlns">>, <<"jabber:client">>} | + lists:keydelete(<<"xmlns">>, 1, lists:keydelete(<<"xmlns:stream">>, 1, Attrs))], + {State, [{xmlstreamstart, <<"stream:stream">>, Attrs2}]}; + #xmlel{name = <<"close">>} -> + {State, [{xmlstreamend, <<"stream:stream">>}]}; + {error, _} -> + {State, [{xmlstreamerror, {4, <<"not well-formed">>}}]}; + _ -> + {State, [El]} + end. + +-spec route_text(pid(), binary()) -> ok. +route_text(Pid, Data) -> + Pid ! {text_with_reply, Data, self()}, + receive + {text_reply, Pid} -> + ok + end. + +-spec route_el(pid(), xmlel() | cdata()) -> ok. +route_el(Pid, Data) -> + route_text(Pid, fxml:element_to_binary(Data)). diff --git a/src/ejabberd_iq.erl b/src/ejabberd_iq.erl new file mode 100644 index 000000000..5db539cab --- /dev/null +++ b/src/ejabberd_iq.erl @@ -0,0 +1,188 @@ +%%%------------------------------------------------------------------- +%%% File : ejabberd_iq.erl +%%% Author : Evgeny Khramtsov +%%% Purpose : +%%% Created : 10 Nov 2017 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(ejabberd_iq). + +-behaviour(gen_server). + +%% API +-export([start_link/0, route/4, dispatch/1]). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-include_lib("xmpp/include/xmpp.hrl"). +-include("logger.hrl"). + + +-record(state, {expire = infinity :: timeout()}). +-type state() :: #state{}. + +%%%=================================================================== +%%% API +%%%=================================================================== +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +-spec route(iq(), atom() | pid(), term(), non_neg_integer()) -> ok. +route(#iq{type = T} = IQ, Proc, Ctx, Timeout) when T == set; T == get -> + Expire = current_time() + Timeout, + Rnd = p1_rand:get_string(), + ID = encode_id(Expire, Rnd), + ets:insert(?MODULE, {{Expire, Rnd}, Proc, Ctx}), + gen_server:cast(?MODULE, {restart_timer, Expire}), + ejabberd_router:route(IQ#iq{id = ID}). + +-spec dispatch(iq()) -> boolean(). +dispatch(#iq{type = T, id = ID} = IQ) when T == error; T == result -> + case decode_id(ID) of + {ok, Expire, Rnd, Node} -> + ejabberd_cluster:send({?MODULE, Node}, {route, IQ, {Expire, Rnd}}); + error -> + false + end; +dispatch(_) -> + false. + +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== +init([]) -> + _ = ets:new(?MODULE, [named_table, ordered_set, public]), + {ok, #state{}}. + +handle_call(Request, From, State) -> + ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), + noreply(State). + +handle_cast({restart_timer, Expire}, State) -> + State1 = State#state{expire = min(Expire, State#state.expire)}, + noreply(State1); +handle_cast(Msg, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Msg]), + noreply(State). + +handle_info({route, IQ, Key}, State) -> + case ets:lookup(?MODULE, Key) of + [{_, Proc, Ctx}] -> + callback(Proc, IQ, Ctx), + ets:delete(?MODULE, Key); + [] -> + ok + end, + noreply(State); +handle_info(timeout, State) -> + Expire = clean(ets:first(?MODULE)), + noreply(State#state{expire = Expire}); +handle_info(Info, State) -> + ?WARNING_MSG("Unexpected info: ~p", [Info]), + noreply(State). + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +-spec current_time() -> non_neg_integer(). +current_time() -> + erlang:system_time(millisecond). + +-spec clean({non_neg_integer(), binary()} | '$end_of_table') + -> non_neg_integer() | infinity. +clean({Expire, _} = Key) -> + case current_time() of + Time when Time >= Expire -> + case ets:lookup(?MODULE, Key) of + [{_, Proc, Ctx}] -> + callback(Proc, timeout, Ctx), + ets:delete(?MODULE, Key); + [] -> + ok + end, + clean(ets:next(?MODULE, Key)); + _ -> + Expire + end; +clean('$end_of_table') -> + infinity. + +-spec noreply(state()) -> {noreply, state()} | {noreply, state(), non_neg_integer()}. +noreply(#state{expire = Expire} = State) -> + case Expire of + infinity -> + {noreply, State}; + _ -> + Timeout = max(0, Expire - current_time()), + {noreply, State, Timeout} + end. + +-spec encode_id(non_neg_integer(), binary()) -> binary(). +encode_id(Expire, Rnd) -> + ExpireBin = integer_to_binary(Expire), + Node = ejabberd_cluster:node_id(), + CheckSum = calc_checksum(<>), + <<"rr-", ExpireBin/binary, $-, Rnd/binary, $-, CheckSum/binary, $-, Node/binary>>. + +-spec decode_id(binary()) -> {ok, non_neg_integer(), binary(), atom()} | error. +decode_id(<<"rr-", ID/binary>>) -> + try + [ExpireBin, Tail] = binary:split(ID, <<"-">>), + [Rnd, Rest] = binary:split(Tail, <<"-">>), + [CheckSum, NodeBin] = binary:split(Rest, <<"-">>), + CheckSum = calc_checksum(<>), + Node = ejabberd_cluster:get_node_by_id(NodeBin), + Expire = binary_to_integer(ExpireBin), + {ok, Expire, Rnd, Node} + catch _:{badmatch, _} -> + error + end; +decode_id(_) -> + error. + +-spec calc_checksum(binary()) -> binary(). +calc_checksum(Data) -> + Key = ejabberd_config:get_shared_key(), + base64:encode(crypto:hash(sha, <>)). + +-spec callback(atom() | pid(), #iq{} | timeout, term()) -> any(). +callback(undefined, IQRes, Fun) -> + try Fun(IQRes) + catch + Class:Reason:StackTrace -> + ?ERROR_MSG("Failed to process iq response:~n~ts~n** ~ts", + [xmpp:pp(IQRes), + misc:format_exception(2, Class, Reason, StackTrace)]) + end; +callback(Proc, IQRes, Ctx) -> + try + Proc ! {iq_reply, IQRes, Ctx} + catch _:badarg -> + ok + end. diff --git a/src/ejabberd_listener.erl b/src/ejabberd_listener.erl index 7db5ab826..d23f4f189 100644 --- a/src/ejabberd_listener.erl +++ b/src/ejabberd_listener.erl @@ -5,7 +5,7 @@ %%% Created : 16 Nov 2002 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -24,327 +24,457 @@ %%%---------------------------------------------------------------------- -module(ejabberd_listener). +-behaviour(supervisor). -author('alexey@process-one.net'). +-author('ekhramtsov@process-one.net'). --export([start_link/0, init/1, start/3, - init/3, - start_listeners/0, - start_listener/3, - stop_listeners/0, - stop_listener/2, - parse_listener_portip/2, - add_listener/3, - delete_listener/2, - transform_options/1, - validate_cfg/1 - ]). +-export([start_link/0, init/1, stop/0, start/3, init/3, + start_listeners/0, start_listener/3, stop_listeners/0, + add_listener/3, delete_listener/2, + config_reloaded/0]). +-export([listen_options/0, listen_opt_type/1, validator/0]). +-export([tls_listeners/0]). --include("ejabberd.hrl"). -include("logger.hrl"). -%% We do not block on send anymore. --define(TCP_SEND_TIMEOUT, 15000). +-type transport() :: tcp | udp. +-type endpoint() :: {inet:port_number(), inet:ip_address(), transport()}. +-type list_opts() :: [{atom(), term()}]. +-type opts() :: #{atom() => term()}. +-type listener() :: {endpoint(), module(), opts()}. +-type sockmod() :: gen_tcp. +-type socket() :: inet:socket(). +-type state() :: term(). + +-export_type([listener/0]). + +-callback start(sockmod(), socket(), state()) -> + {ok, pid()} | {error, any()} | ignore. +-callback start_link(sockmod(), socket(), state()) -> + {ok, pid()} | {error, any()} | ignore. +-callback accept(pid()) -> any(). +-callback listen_opt_type(atom()) -> econf:validator(). +-callback listen_options() -> [{atom(), term()} | atom()]. +-callback tcp_init(socket(), list_opts()) -> state(). +-callback udp_init(socket(), list_opts()) -> state(). + +-optional_callbacks([listen_opt_type/1, tcp_init/2, udp_init/2]). start_link() -> - supervisor:start_link({local, ejabberd_listeners}, ?MODULE, []). - + supervisor:start_link({local, ?MODULE}, ?MODULE, []). init(_) -> - ets:new(listen_sockets, [named_table, public]), - bind_tcp_ports(), - {ok, {{one_for_one, 10, 1}, []}}. + _ = ets:new(?MODULE, [named_table, public]), + ejabberd_hooks:add(config_reloaded, ?MODULE, config_reloaded, 50), + Listeners = ejabberd_option:listen(), + {ok, {{one_for_one, 10, 1}, listeners_childspec(Listeners)}}. -bind_tcp_ports() -> - case ejabberd_config:get_option(listen, fun validate_cfg/1) of - undefined -> - ignore; - Ls -> - lists:foreach( - fun({Port, Module, Opts}) -> - ModuleRaw = strip_frontend(Module), - case ModuleRaw:socket_type() of - independent -> ok; - _ -> - bind_tcp_port(Port, Module, Opts) - end - end, Ls) - end. +stop() -> + ejabberd_hooks:delete(config_reloaded, ?MODULE, config_reloaded, 50), + stop_listeners(), + ejabberd_sup:stop_child(?MODULE). -bind_tcp_port(PortIP, Module, RawOpts) -> - try check_listener_options(RawOpts) of - ok -> - {Port, IPT, IPS, IPV, Proto, OptsClean} = parse_listener_portip(PortIP, RawOpts), - {_Opts, SockOpts} = prepare_opts(IPT, IPV, OptsClean), - case Proto of - udp -> ok; - _ -> - ListenSocket = listen_tcp(PortIP, Module, SockOpts, Port, IPS), - ets:insert(listen_sockets, {PortIP, ListenSocket}), - ok - end - catch - throw:{error, Error} -> - ?ERROR_MSG(Error, []) - end. +-spec listeners_childspec([listener()]) -> [supervisor:child_spec()]. +listeners_childspec(Listeners) -> + lists:map( + fun({EndPoint, Module, Opts}) -> + ets:insert(?MODULE, {EndPoint, Module, Opts}), + {EndPoint, + {?MODULE, start, [EndPoint, Module, Opts]}, + transient, brutal_kill, worker, [?MODULE]} + end, Listeners). +-spec start_listeners() -> ok. start_listeners() -> - case ejabberd_config:get_option(listen, fun validate_cfg/1) of - undefined -> - ignore; - Ls -> - Ls2 = lists:map( - fun({Port, Module, Opts}) -> - case start_listener(Port, Module, Opts) of - {ok, _Pid} = R -> R; - {error, Error} -> - throw(Error) - end - end, Ls), - report_duplicated_portips(Ls), - {ok, {{one_for_one, 10, 1}, Ls2}} - end. + Listeners = ejabberd_option:listen(), + lists:foreach( + fun(Spec) -> + supervisor:start_child(?MODULE, Spec) + end, listeners_childspec(Listeners)). -report_duplicated_portips(L) -> - LKeys = [Port || {Port, _, _} <- L], - LNoDupsKeys = proplists:get_keys(L), - case LKeys -- LNoDupsKeys of - [] -> ok; - Dups -> - ?CRITICAL_MSG("In the ejabberd configuration there are duplicated " - "Port number + IP address:~n ~p", - [Dups]) - end. +-spec start(endpoint(), module(), opts()) -> term(). +start(EndPoint, Module, Opts) -> + proc_lib:start_link(?MODULE, init, [EndPoint, Module, Opts]). -start(Port, Module, Opts) -> - %% Check if the module is an ejabberd listener or an independent listener - ModuleRaw = strip_frontend(Module), - case ModuleRaw:socket_type() of - independent -> ModuleRaw:start_listener(Port, Opts); - _ -> start_dependent(Port, Module, Opts) - end. +-spec init(endpoint(), module(), opts()) -> ok. +init({_, _, Transport} = EndPoint, Module, AllOpts) -> + {ModuleOpts, SockOpts} = split_opts(Transport, AllOpts), + init(EndPoint, Module, ModuleOpts, SockOpts). -%% @spec(Port, Module, Opts) -> {ok, Pid} | {error, ErrorMessage} -start_dependent(Port, Module, Opts) -> - try check_listener_options(Opts) of - ok -> - proc_lib:start_link(?MODULE, init, [Port, Module, Opts]) - catch - throw:{error, Error} -> - ?ERROR_MSG(Error, []), - {error, Error} - end. - -init(PortIP, Module, RawOpts) -> - {Port, IPT, IPS, IPV, Proto, OptsClean} = parse_listener_portip(PortIP, RawOpts), - {Opts, SockOpts} = prepare_opts(IPT, IPV, OptsClean), - if Proto == udp -> - init_udp(PortIP, Module, Opts, SockOpts, Port, IPS); - true -> - init_tcp(PortIP, Module, Opts, SockOpts, Port, IPS) - end. - -init_udp(PortIP, Module, Opts, SockOpts, Port, IPS) -> - case gen_udp:open(Port, [binary, +-spec init(endpoint(), module(), opts(), [gen_tcp:option()]) -> ok. +init({Port, _, udp} = EndPoint, Module, Opts, SockOpts) -> + {Port2, ExtraOpts} = case Port of + <<"unix:", Path/binary>> -> + SO = lists:keydelete(ip, 1, SockOpts), + setup_provisional_udsocket_dir(Path), + file:delete(Path), + {0, [{ip, {local, Path}} | SO]}; + _ -> + {Port, SockOpts} + end, + ExtraOpts2 = lists:keydelete(send_timeout, 1, ExtraOpts), + case {gen_udp:open(Port2, [binary, {active, false}, {reuseaddr, true} | - SockOpts]) of - {ok, Socket} -> - %% Inform my parent that this port was opened succesfully - proc_lib:init_ack({ok, self()}), - case erlang:function_exported(Module, udp_init, 2) of - false -> - udp_recv(Socket, Module, Opts); - true -> - case catch Module:udp_init(Socket, Opts) of - {'EXIT', _} = Err -> - ?ERROR_MSG("failed to process callback function " - "~p:~s(~p, ~p): ~p", - [Module, udp_init, Socket, Opts, Err]), - udp_recv(Socket, Module, Opts); - NewOpts -> - udp_recv(Socket, Module, NewOpts) - end - end; - {error, Reason} -> - socket_error(Reason, PortIP, Module, SockOpts, Port, IPS) - end. - -init_tcp(PortIP, Module, Opts, SockOpts, Port, IPS) -> - ListenSocket = listen_tcp(PortIP, Module, SockOpts, Port, IPS), - %% Inform my parent that this port was opened succesfully - proc_lib:init_ack({ok, self()}), - case erlang:function_exported(Module, tcp_init, 2) of - false -> - accept(ListenSocket, Module, Opts); - true -> - case catch Module:tcp_init(ListenSocket, Opts) of - {'EXIT', _} = Err -> - ?ERROR_MSG("failed to process callback function " - "~p:~s(~p, ~p): ~p", - [Module, tcp_init, ListenSocket, Opts, Err]), - accept(ListenSocket, Module, Opts); - NewOpts -> - accept(ListenSocket, Module, NewOpts) - end - end. - -listen_tcp(PortIP, Module, SockOpts, Port, IPS) -> - case ets:lookup(listen_sockets, PortIP) of - [{PortIP, ListenSocket}] -> - ?INFO_MSG("Reusing listening port for ~p", [Port]), - ets:delete(listen_sockets, Port), - ListenSocket; - _ -> - SockOpts2 = try erlang:system_info(otp_release) >= "R13B" of - true -> [{send_timeout_close, true} | SockOpts]; - false -> SockOpts - catch - _:_ -> [] - end, - Res = gen_tcp:listen(Port, [binary, - {packet, 0}, - {active, false}, - {reuseaddr, true}, - {nodelay, true}, - {send_timeout, ?TCP_SEND_TIMEOUT}, - {keepalive, true} | - SockOpts2]), - case Res of - {ok, ListenSocket} -> - ListenSocket; + ExtraOpts2]), + set_definitive_udsocket(Port, Opts)} of + {{ok, Socket}, ok} -> + misc:set_proc_label({?MODULE, udp, Port}), + case inet:sockname(Socket) of + {ok, {Addr, Port1}} -> + proc_lib:init_ack({ok, self()}), + case application:ensure_started(ejabberd) of + ok -> + ?INFO_MSG("Start accepting ~ts connections at ~ts for ~p", + [format_transport(udp, Opts), + format_endpoint({Port1, Addr, udp}), Module]), + Opts1 = opts_to_list(Module, Opts), + case erlang:function_exported(Module, udp_init, 2) of + false -> + udp_recv(Socket, Module, Opts1); + true -> + State = Module:udp_init(Socket, Opts1), + udp_recv(Socket, Module, State) + end; + {error, _} -> + ok + end; {error, Reason} -> - socket_error(Reason, PortIP, Module, SockOpts, Port, IPS) - end + return_socket_error(Reason, EndPoint, Module) + end; + {{error, Reason}, _} -> + return_socket_error(Reason, EndPoint, Module); + {_, {error, Reason} } -> + return_socket_error(Reason, EndPoint, Module) + end; +init({Port, _, tcp} = EndPoint, Module, Opts, SockOpts) -> + case {listen_tcp(Port, SockOpts), + set_definitive_udsocket(Port, Opts)} of + {{ok, ListenSocket}, ok} -> + case inet:sockname(ListenSocket) of + {ok, {Addr, Port1}} -> + proc_lib:init_ack({ok, self()}), + misc:set_proc_label({?MODULE, tcp, Port}), + case application:ensure_started(ejabberd) of + ok -> + Sup = start_module_sup(Module, Opts), + Interval = maps:get(accept_interval, Opts), + Proxy = maps:get(use_proxy_protocol, Opts), + ?INFO_MSG("Start accepting ~ts connections at ~ts for ~p", + [format_transport(tcp, Opts), + format_endpoint({Port1, Addr, tcp}), Module]), + Opts1 = opts_to_list(Module, Opts), + case erlang:function_exported(Module, tcp_init, 2) of + false -> + accept(ListenSocket, Module, Opts1, Sup, Interval, Proxy); + true -> + State = Module:tcp_init(ListenSocket, Opts1), + accept(ListenSocket, Module, State, Sup, Interval, Proxy) + end; + {error, _} -> + ok + end; + {error, Reason} -> + return_socket_error(Reason, EndPoint, Module) + end; + {{error, Reason}, _} -> + return_socket_error(Reason, EndPoint, Module); + {_, {error, Reason}} -> + return_socket_error(Reason, EndPoint, Module) end. -%% @spec (PortIP, Opts) -> {Port, IPT, IPS, IPV, OptsClean} -%% where -%% PortIP = Port | {Port, IPT | IPS} -%% Port = integer() -%% IPT = tuple() -%% IPS = string() -%% IPV = inet | inet6 -%% Opts = [IPV | {ip, IPT} | atom() | tuple()] -%% OptsClean = [atom() | tuple()] -%% @doc Parse any kind of ejabberd listener specification. -%% The parsed options are returned in several formats. -%% OptsClean does not include inet/inet6 or ip options. -%% Opts can include the options inet6 and {ip, Tuple}, -%% but they are only used when no IP address was specified in the PortIP. -%% The IP version (either IPv4 or IPv6) is inferred from the IP address type, -%% so the option inet/inet6 is only used when no IP is specified at all. -parse_listener_portip(PortIP, Opts) -> - {IPOpt, Opts2} = strip_ip_option(Opts), - {IPVOpt, OptsClean} = case lists:member(inet6, Opts2) of - true -> {inet6, Opts2 -- [inet6]}; - false -> {inet, Opts2} - end, - {Port, IPT, IPS, Proto} = - case add_proto(PortIP, Opts) of - {P, Prot} -> - T = get_ip_tuple(IPOpt, IPVOpt), - S = jlib:ip_to_list(T), - {P, T, S, Prot}; - {P, T, Prot} when is_integer(P) and is_tuple(T) -> - S = jlib:ip_to_list(T), - {P, T, S, Prot}; - {P, S, Prot} when is_integer(P) and is_binary(S) -> - [S | _] = str:tokens(S, <<"/">>), - {ok, T} = inet_parse:address(binary_to_list(S)), - {P, T, S, Prot} - end, - IPV = case tuple_size(IPT) of - 4 -> inet; - 8 -> inet6 - end, - {Port, IPT, IPS, IPV, Proto, OptsClean}. - -prepare_opts(IPT, IPV, OptsClean) -> - %% The first inet|inet6 and the last {ip, _} work, - %% so overriding those in Opts - Opts = [IPV | OptsClean] ++ [{ip, IPT}], - SockOpts = lists:filter(fun({ip, _}) -> true; - (inet6) -> true; - (inet) -> true; - ({backlog, _}) -> true; - (_) -> false - end, Opts), - {Opts, SockOpts}. - -add_proto(Port, Opts) when is_integer(Port) -> - {Port, get_proto(Opts)}; -add_proto({Port, Proto}, _Opts) when is_atom(Proto) -> - {Port, normalize_proto(Proto)}; -add_proto({Port, Addr}, Opts) -> - {Port, Addr, get_proto(Opts)}; -add_proto({Port, Addr, Proto}, _Opts) -> - {Port, Addr, normalize_proto(Proto)}. - -strip_ip_option(Opts) -> - {IPL, OptsNoIP} = lists:partition( - fun({ip, _}) -> true; - (_) -> false - end, - Opts), - case IPL of - %% Only the first ip option is considered - [{ip, T1} | _] -> - {T1, OptsNoIP}; - [] -> - {no_ip_option, OptsNoIP} +-spec listen_tcp(inet:port_number(), [gen_tcp:option()]) -> + {ok, inet:socket()} | {error, system_limit | inet:posix()}. +listen_tcp(Port, SockOpts) -> + {Port2, ExtraOpts} = case Port of + <<"unix:", Path/binary>> -> + SO = lists:keydelete(ip, 1, SockOpts), + Prov = setup_provisional_udsocket_dir(Path), + file:delete(Path), + file:delete(Prov), + {0, [{ip, {local, Prov}} | SO]}; + _ -> + {Port, SockOpts} + end, + Res = gen_tcp:listen(Port2, [binary, + {packet, 0}, + {active, false}, + {reuseaddr, true}, + {nodelay, true}, + {send_timeout_close, true}, + {keepalive, true} | ExtraOpts]), + case Res of + {ok, ListenSocket} -> + {ok, ListenSocket}; + {error, _} = Err -> + Err end. -get_ip_tuple(no_ip_option, inet) -> - {0, 0, 0, 0}; -get_ip_tuple(no_ip_option, inet6) -> - {0, 0, 0, 0, 0, 0, 0, 0}; -get_ip_tuple(IPOpt, _IPVOpt) -> - IPOpt. +%%% +%%% Unix Domain Socket utility functions +%%% -accept(ListenSocket, Module, Opts) -> +setup_provisional_udsocket_dir(DefinitivePath) -> + ProvisionalPath = get_provisional_udsocket_path(DefinitivePath), + ?INFO_MSG("Creating a Unix Domain Socket provisional file at ~ts for the definitive path ~s", + [ProvisionalPath, DefinitivePath]), + ProvisionalPathAbsolute = relative_socket_to_mnesia(ProvisionalPath), + create_base_dir(ProvisionalPathAbsolute), + ProvisionalPathAbsolute. + +get_provisional_udsocket_path(Path) -> + ReproducibleSecret = binary:part(crypto:hash(sha, misc:atom_to_binary(erlang:get_cookie())), 1, 8), + PathBase64 = misc:term_to_base64({ReproducibleSecret, Path}), + PathBuild = filename:join(misc:get_home(), PathBase64), + DestPath = filename:join(filename:dirname(Path), PathBase64), + case {byte_size(DestPath) > 107, byte_size(PathBuild) > 107} of + {false, _} -> + DestPath; + {true, false} -> + ?INFO_MSG("The provisional Unix Domain Socket path ~ts is longer than 107, let's use home directory instead which is ~p", [DestPath, byte_size(PathBuild)]), + PathBuild; + {true, true} -> + ?ERROR_MSG("The Unix Domain Socket path ~ts is too long, " + "and I cannot create the provisional file safely. " + "Please configure a shorter path and try again.", [Path]), + throw({error_socket_path_too_long, Path}) + end. + +get_definitive_udsocket_path(<<"unix", _>> = Unix) -> + Unix; +get_definitive_udsocket_path(ProvisionalPath) -> + PathBase64 = filename:basename(ProvisionalPath), + {term, {_, Path}} = misc:base64_to_term(PathBase64), + relative_socket_to_mnesia(Path). + +-spec set_definitive_udsocket(integer() | binary(), opts()) -> ok | {error, file:posix() | badarg}. + +set_definitive_udsocket(<<"unix:", Path/binary>>, Opts) -> + Prov = get_provisional_udsocket_path(Path), + Usd = maps:get(unix_socket, Opts), + case maps:get(mode, Usd, undefined) of + undefined -> ok; + Mode -> ok = file:change_mode(Prov, Mode) + end, + case maps:get(owner, Usd, undefined) of + undefined -> ok; + Owner -> + try + ok = file:change_owner(Prov, Owner) + catch + error:{badmatch, {error, eperm}} -> + ?ERROR_MSG("Error trying to set owner ~p for socket ~p", [Owner, Prov]), + throw({error_setting_socket_owner, Owner, Prov}) + end + end, + case maps:get(group, Usd, undefined) of + undefined -> ok; + Group -> + try + ok = file:change_group(Prov, Group) + catch + error:{badmatch, {error, eperm}} -> + ?ERROR_MSG("Error trying to set group ~p for socket ~p", [Group, Prov]), + throw({error_setting_socket_group, Group, Prov}) + end + end, + FinalPath = relative_socket_to_mnesia(Path), + create_base_dir(FinalPath), + file:rename(Prov, FinalPath); +set_definitive_udsocket(Port, _Opts) when is_integer(Port) -> + ok. + +create_base_dir(Path) -> + Dirname = filename:dirname(Path), + case file:make_dir(Dirname) of + ok -> + file:change_mode(Dirname, 8#00700); + _ -> + ok + end. + +relative_socket_to_mnesia(Path1) -> + case filename:pathtype(Path1) of + absolute -> + Path1; + relative -> + MnesiaDir = mnesia:system_info(directory), + filename:join(MnesiaDir, Path1) + end. + +maybe_delete_udsocket_file(<<"unix:", Path/binary>>) -> + PathAbsolute = relative_socket_to_mnesia(Path), + file:delete(PathAbsolute); +maybe_delete_udsocket_file(_Port) -> + ok. + +%%% +%%% +%%% + +-spec split_opts(transport(), opts()) -> {opts(), [gen_tcp:option()]}. +split_opts(Transport, Opts) -> + maps:fold( + fun(Opt, Val, {ModOpts, SockOpts}) -> + case {Opt, Val} of + {ip, _} -> + {ModOpts, [{Opt, Val} | SockOpts]}; + {backlog, _} when Transport == tcp -> + {ModOpts, [{Opt, Val} | SockOpts]}; + {backlog, _} -> + {ModOpts, SockOpts}; + {send_timeout, _} -> + {ModOpts, [{Opt, Val} | SockOpts]}; + _ -> + {ModOpts#{Opt => Val}, SockOpts} + end + end, {#{}, []}, Opts). + +-spec accept(inet:socket(), module(), state(), atom(), + non_neg_integer(), boolean()) -> no_return(). +accept(ListenSocket, Module, State, Sup, Interval, Proxy) -> + Arity = case erlang:function_exported(Module, start, 3) of + true -> 3; + false -> 2 + end, + accept(ListenSocket, Module, State, Sup, Interval, Proxy, Arity). + +-spec accept(inet:socket(), module(), state(), atom(), + non_neg_integer(), boolean(), 2|3) -> no_return(). +accept(ListenSocket, Module, State, Sup, Interval, Proxy, Arity) -> + NewInterval = apply_rate_limit(Interval), case gen_tcp:accept(ListenSocket) of + {ok, Socket} when Proxy -> + case proxy_protocol:decode(gen_tcp, Socket, 10000) of + {error, Err} -> + ?ERROR_MSG("(~w) Proxy protocol parsing failed: ~ts", + [ListenSocket, format_error(Err)]), + gen_tcp:close(Socket); + {undefined, undefined} -> + gen_tcp:close(Socket); + {{Addr, Port}, {PAddr, PPort}} = SP -> + %% THIS IS WRONG + State2 = [{sock_peer_name, SP} | State], + Receiver = case start_connection(Module, Arity, Socket, State2, Sup) of + {ok, RecvPid} -> + RecvPid; + _ -> + gen_tcp:close(Socket), + none + end, + ?INFO_MSG("(~p) Accepted proxied connection ~ts -> ~ts", + [Receiver, + ejabberd_config:may_hide_data( + format_endpoint({PPort, PAddr, tcp})), + format_endpoint({Port, Addr, tcp})]) + end, + accept(ListenSocket, Module, State, Sup, NewInterval, Proxy, Arity); {ok, Socket} -> case {inet:sockname(Socket), inet:peername(Socket)} of {{ok, {Addr, Port}}, {ok, {PAddr, PPort}}} -> - ?INFO_MSG("(~w) Accepted connection ~s:~p -> ~s:~p", - [Socket, inet_parse:ntoa(PAddr), PPort, - inet_parse:ntoa(Addr), Port]); + Receiver = case start_connection(Module, Arity, Socket, State, Sup) of + {ok, RecvPid} -> + RecvPid; + _ -> + gen_tcp:close(Socket), + none + end, + case is_ctl_over_http(State) of + false -> + ?INFO_MSG("(~p) Accepted connection ~ts -> ~ts", + [Receiver, + ejabberd_config:may_hide_data( + format_endpoint({PPort, PAddr, tcp})), + format_endpoint({Port, Addr, tcp})]); + true -> + ?DEBUG("(~p) Accepted connection ~ts -> ~ts", + [Receiver, + ejabberd_config:may_hide_data( + format_endpoint({PPort, PAddr, tcp})), + format_endpoint({Port, Addr, tcp})]) + end; _ -> - ok + gen_tcp:close(Socket) end, - CallMod = case is_frontend(Module) of - true -> ejabberd_frontend_socket; - false -> ejabberd_socket - end, - CallMod:start(strip_frontend(Module), gen_tcp, Socket, Opts), - accept(ListenSocket, Module, Opts); + accept(ListenSocket, Module, State, Sup, NewInterval, Proxy, Arity); {error, Reason} -> - ?ERROR_MSG("(~w) Failed TCP accept: ~w", - [ListenSocket, Reason]), - accept(ListenSocket, Module, Opts) + ?ERROR_MSG("(~w) Failed TCP accept: ~ts", + [ListenSocket, format_error(Reason)]), + accept(ListenSocket, Module, State, Sup, NewInterval, Proxy, Arity) end. -udp_recv(Socket, Module, Opts) -> +is_ctl_over_http(State) -> + case lists:keyfind(request_handlers, 1, State) of + {request_handlers, Handlers} -> + case lists:keyfind(ejabberd_ctl, 2, Handlers) of + {_, ejabberd_ctl} -> true; + _ -> false + end; + _ -> false + end. + +-spec udp_recv(inet:socket(), module(), state()) -> no_return(). +udp_recv(Socket, Module, State) -> case gen_udp:recv(Socket, 0) of {ok, {Addr, Port, Packet}} -> - case catch Module:udp_recv(Socket, Addr, Port, Packet, Opts) of + case catch Module:udp_recv(Socket, Addr, Port, Packet, State) of {'EXIT', Reason} -> - ?ERROR_MSG("failed to process UDP packet:~n" + ?ERROR_MSG("Failed to process UDP packet:~n" "** Source: {~p, ~p}~n" "** Reason: ~p~n** Packet: ~p", [Addr, Port, Reason, Packet]), - udp_recv(Socket, Module, Opts); - NewOpts -> - udp_recv(Socket, Module, NewOpts) + udp_recv(Socket, Module, State); + NewState -> + udp_recv(Socket, Module, NewState) end; {error, Reason} -> - ?ERROR_MSG("unexpected UDP error: ~s", [format_error(Reason)]), + ?ERROR_MSG("Unexpected UDP error: ~ts", [format_error(Reason)]), throw({error, Reason}) end. -%% @spec (Port, Module, Opts) -> {ok, Pid} | {error, Error} -start_listener(Port, Module, Opts) -> - case start_listener2(Port, Module, Opts) of +-spec start_connection(module(), 2|3, inet:socket(), state(), atom()) -> + {ok, pid()} | {error, any()} | ignore. +start_connection(Module, Arity, Socket, State, Sup) -> + Res = case Sup of + undefined when Arity == 3 -> + Module:start(gen_tcp, Socket, State); + undefined -> + Module:start({gen_tcp, Socket}, State); + _ when Arity == 3 -> + supervisor:start_child(Sup, [gen_tcp, Socket, State]); + _ -> + supervisor:start_child(Sup, [{gen_tcp, Socket}, State]) + end, + case Res of + {ok, Pid, preowned_socket} -> + Module:accept(Pid), + {ok, Pid}; + {ok, Pid} -> + case gen_tcp:controlling_process(Socket, Pid) of + ok -> + Module:accept(Pid), + {ok, Pid}; + Err -> + case Sup of + undefined -> + exit(Pid, kill); + _ -> + supervisor:terminate_child(Sup, Pid) + end, + Err + end; + Err -> + Err + end. + +-spec start_listener(endpoint(), module(), opts()) -> + {ok, pid()} | {error, any()}. +start_listener(EndPoint, Module, Opts) -> + %% It is only required to start the supervisor in some cases. + %% But it doesn't hurt to attempt to start it for any listener. + %% So, it's normal (and harmless) that in most cases this + %% call returns: {error, {already_started, pid()}} + case start_listener_sup(EndPoint, Module, Opts) of {ok, _Pid} = R -> R; {error, {{'EXIT', {undef, [{M, _F, _A}|_]}}, _} = Error} -> ?ERROR_MSG("Error starting the ejabberd listener: ~p.~n" @@ -357,206 +487,140 @@ start_listener(Port, Module, Opts) -> {error, Error} end. -%% @spec (Port, Module, Opts) -> {ok, Pid} | {error, Error} -start_listener2(Port, Module, Opts) -> - %% It is only required to start the supervisor in some cases. - %% But it doesn't hurt to attempt to start it for any listener. - %% So, it's normal (and harmless) that in most cases this call returns: {error, {already_started, pid()}} - maybe_start_sip(Module), - start_module_sup(Port, Module), - start_listener_sup(Port, Module, Opts). +-spec start_module_sup(module(), opts()) -> atom(). +start_module_sup(Module, Opts) -> + case maps:get(supervisor, Opts) of + true -> + Proc = list_to_atom(atom_to_list(Module) ++ "_sup"), + ChildSpec = {Proc, {ejabberd_tmp_sup, start_link, [Proc, Module]}, + permanent, + infinity, + supervisor, + [ejabberd_tmp_sup]}, + case supervisor:start_child(ejabberd_sup, ChildSpec) of + {ok, _} -> Proc; + {error, {already_started, _}} -> Proc; + _ -> undefined + end; + false -> + undefined + end. -start_module_sup(_Port, Module) -> - Proc1 = gen_mod:get_module_proc(<<"sup">>, Module), - ChildSpec1 = - {Proc1, - {ejabberd_tmp_sup, start_link, [Proc1, strip_frontend(Module)]}, - permanent, - infinity, - supervisor, - [ejabberd_tmp_sup]}, - supervisor:start_child(ejabberd_sup, ChildSpec1). - -start_listener_sup(Port, Module, Opts) -> - ChildSpec = {Port, - {?MODULE, start, [Port, Module, Opts]}, +-spec start_listener_sup(endpoint(), module(), opts()) -> + {ok, pid()} | {error, any()}. +start_listener_sup(EndPoint, Module, Opts) -> + ChildSpec = {EndPoint, + {?MODULE, start, [EndPoint, Module, Opts]}, transient, brutal_kill, worker, [?MODULE]}, - supervisor:start_child(ejabberd_listeners, ChildSpec). + supervisor:start_child(?MODULE, ChildSpec). +-spec stop_listeners() -> ok. stop_listeners() -> - Ports = ejabberd_config:get_option(listen, fun validate_cfg/1), + Ports = ejabberd_option:listen(), lists:foreach( fun({PortIpNetp, Module, _Opts}) -> delete_listener(PortIpNetp, Module) end, Ports). -%% @spec (PortIP, Module) -> ok -%% where -%% PortIP = {Port, IPT | IPS} -%% Port = integer() -%% IPT = tuple() -%% IPS = string() -%% Module = atom() -stop_listener(PortIP, _Module) -> - supervisor:terminate_child(ejabberd_listeners, PortIP), - supervisor:delete_child(ejabberd_listeners, PortIP). +-spec stop_listener(endpoint(), module(), opts()) -> ok | {error, any()}. +stop_listener({Port, _, Transport} = EndPoint, Module, Opts) -> + case supervisor:terminate_child(?MODULE, EndPoint) of + ok -> + ?INFO_MSG("Stop accepting ~ts connections at ~ts for ~p", + [format_transport(Transport, Opts), + format_endpoint(EndPoint), Module]), + maybe_delete_udsocket_file(Port), + ets:delete(?MODULE, EndPoint), + supervisor:delete_child(?MODULE, EndPoint); + Err -> + Err + end. -%% @spec (PortIP, Module, Opts) -> {ok, Pid} | {error, Error} -%% where -%% PortIP = {Port, IPT | IPS} -%% Port = integer() -%% IPT = tuple() -%% IPS = string() -%% IPV = inet | inet6 -%% Module = atom() -%% Opts = [IPV | {ip, IPT} | atom() | tuple()] -%% @doc Add a listener and store in config if success -add_listener(PortIP, Module, Opts) -> - {Port, IPT, _, _, Proto, _} = parse_listener_portip(PortIP, Opts), - PortIP1 = {Port, IPT, Proto}, - case start_listener(PortIP1, Module, Opts) of +-spec add_listener(endpoint(), module(), opts()) -> ok | {error, any()}. +add_listener(EndPoint, Module, Opts) -> + Opts1 = apply_defaults(Module, Opts), + case start_listener(EndPoint, Module, Opts1) of {ok, _Pid} -> - Ports = case ejabberd_config:get_option( - listen, fun validate_cfg/1) of - undefined -> - []; - Ls -> - Ls - end, - Ports1 = lists:keydelete(PortIP1, 1, Ports), - Ports2 = [{PortIP1, Module, Opts} | Ports1], - Ports3 = lists:map(fun transform_option/1, Ports2), - ejabberd_config:add_option(listen, Ports3), ok; {error, {already_started, _Pid}} -> - {error, {already_started, PortIP}}; + {error, {already_started, EndPoint}}; {error, Error} -> {error, Error} end. -delete_listener(PortIP, Module) -> - delete_listener(PortIP, Module, []). - -%% @spec (PortIP, Module, Opts) -> ok -%% where -%% PortIP = {Port, IPT | IPS} -%% Port = integer() -%% IPT = tuple() -%% IPS = string() -%% Module = atom() -%% Opts = [term()] -delete_listener(PortIP, Module, Opts) -> - {Port, IPT, _, _, Proto, _} = parse_listener_portip(PortIP, Opts), - PortIP1 = {Port, IPT, Proto}, - Ports = case ejabberd_config:get_option( - listen, fun validate_cfg/1) of - undefined -> - []; - Ls -> - Ls - end, - Ports1 = lists:keydelete(PortIP1, 1, Ports), - Ports2 = lists:map(fun transform_option/1, Ports1), - ejabberd_config:add_option(listen, Ports2), - stop_listener(PortIP1, Module). - - --spec is_frontend({frontend, module} | module()) -> boolean(). - -is_frontend({frontend, _Module}) -> true; -is_frontend(_) -> false. - -%% @doc(FrontMod) -> atom() -%% where FrontMod = atom() | {frontend, atom()} --spec strip_frontend({frontend, module()} | module()) -> module(). - -strip_frontend({frontend, Module}) -> Module; -strip_frontend(Module) when is_atom(Module) -> Module. - -maybe_start_sip(esip_socket) -> - ejabberd:start_app(esip); -maybe_start_sip(_) -> - ok. - -%%% -%%% Check options -%%% - -check_listener_options(Opts) -> - case includes_deprecated_ssl_option(Opts) of - false -> ok; - true -> - Error = "There is a problem with your ejabberd configuration file: " - "the option 'ssl' for listening sockets is no longer available." - " To get SSL encryption use the option 'tls'.", - throw({error, Error}) - end, - case certfile_readable(Opts) of - true -> ok; - {false, Path} -> - ErrorText = "There is a problem in the configuration: " - "the specified file is not readable: ", - throw({error, ErrorText ++ Path}) - end, - ok. - -%% Parse the options of the socket, -%% and return if the deprecated option 'ssl' is included -%% @spec (Opts) -> true | false -includes_deprecated_ssl_option(Opts) -> - case lists:keysearch(ssl, 1, Opts) of - {value, {ssl, _SSLOpts}} -> - true; - _ -> - lists:member(ssl, Opts) +-spec delete_listener(endpoint(), module()) -> ok | {error, any()}. +delete_listener(EndPoint, Module) -> + try ets:lookup_element(?MODULE, EndPoint, 3) of + Opts -> stop_listener(EndPoint, Module, Opts) + catch _:badarg -> + ok end. -%% @spec (Opts) -> true | {false, Path::string()} -certfile_readable(Opts) -> - case proplists:lookup(certfile, Opts) of - none -> true; - {certfile, Path} -> - PathS = binary_to_list(Path), - case ejabberd_config:is_file_readable(PathS) of - true -> true; - false -> {false, PathS} - end - end. +-spec tls_listeners() -> [module()]. +tls_listeners() -> + lists:usort( + lists:filtermap( + fun({_, Module, #{tls := true}}) -> {true, Module}; + ({_, Module, #{starttls := true}}) -> {true, Module}; + (_) -> false + end, ets:tab2list(?MODULE))). -get_proto(Opts) -> - case proplists:get_value(proto, Opts) of - undefined -> - tcp; - Proto -> - normalize_proto(Proto) - end. - -normalize_proto(tcp) -> tcp; -normalize_proto(udp) -> udp; -normalize_proto(UnknownProto) -> - ?WARNING_MSG("There is a problem in the configuration: " - "~p is an unknown IP protocol. Using tcp as fallback", - [UnknownProto]), - tcp. - -socket_error(Reason, PortIP, Module, SockOpts, Port, IPS) -> - ReasonT = case Reason of - eaddrnotavail -> - "IP address not available: " ++ IPS; - eaddrinuse -> - "IP address and port number already used: " - ++binary_to_list(IPS)++" "++integer_to_list(Port); +-spec config_reloaded() -> ok. +config_reloaded() -> + New = ejabberd_option:listen(), + Old = ets:tab2list(?MODULE), + lists:foreach( + fun({EndPoint, Module, Opts}) -> + case lists:keyfind(EndPoint, 1, New) of + false -> + stop_listener(EndPoint, Module, Opts); _ -> - format_error(Reason) - end, - ?ERROR_MSG("Failed to open socket:~n ~p~nReason: ~s", - [{Port, Module, SockOpts}, ReasonT]), - throw({Reason, PortIP}). + ok + end + end, Old), + lists:foreach( + fun({EndPoint, Module, Opts}) -> + case lists:keyfind(EndPoint, 1, Old) of + {_, Module, Opts} -> + ok; + {_, OldModule, OldOpts} -> + _ = stop_listener(EndPoint, OldModule, OldOpts), + case start_listener(EndPoint, Module, Opts) of + {ok, _} -> + ets:insert(?MODULE, {EndPoint, Module, Opts}); + _ -> + ok + end; + false -> + case start_listener(EndPoint, Module, Opts) of + {ok, _} -> + ets:insert(?MODULE, {EndPoint, Module, Opts}); + _ -> + ok + end + end + end, New). +-spec return_socket_error(inet:posix(), endpoint(), module()) -> no_return(). +return_socket_error(Reason, EndPoint, Module) -> + ?ERROR_MSG("Failed to open socket at ~ts for ~ts: ~ts", + [format_endpoint(EndPoint), Module, format_error(Reason)]), + return_init_error(Reason). + +-ifdef(OTP_BELOW_26). +return_init_error(Reason) -> + proc_lib:init_ack({error, Reason}). +-else. +-spec return_init_error(inet:posix()) -> no_return(). +return_init_error(Reason) -> + proc_lib:init_fail({error, Reason}, {exit, normal}). +-endif. + +-spec format_error(inet:posix() | atom()) -> string(). format_error(Reason) -> case inet:format_error(Reason) of "unknown POSIX error" -> @@ -565,103 +629,244 @@ format_error(Reason) -> ReasonStr end. --define(IS_CHAR(C), (is_integer(C) and (C >= 0) and (C =< 255))). --define(IS_UINT(U), (is_integer(U) and (U >= 0) and (U =< 65535))). --define(IS_PORT(P), (is_integer(P) and (P > 0) and (P =< 65535))). --define(IS_TRANSPORT(T), ((T == tcp) or (T == udp))). +-spec format_endpoint(endpoint()) -> string(). +format_endpoint({Port, IP, Transport}) -> + case Port of + <<"unix:", _/binary>> -> + Port; + <<>> when (IP == local) and (Transport == tcp) -> + "local-unix-socket-domain"; + Unix when is_binary(Unix) -> + Def = get_definitive_udsocket_path(Unix), + <<"unix:", Def/binary>>; + _ -> + IPStr = case tuple_size(IP) of + 4 -> inet:ntoa(IP); + 8 -> "[" ++ inet:ntoa(IP) ++ "]" + end, + IPStr ++ ":" ++ integer_to_list(Port) + end. -transform_option({{Port, IP, Transport}, Mod, Opts}) -> - IPStr = if is_tuple(IP) -> - list_to_binary(inet_parse:ntoa(IP)); - true -> - IP - end, - Opts1 = lists:map( - fun({ip, IPT}) when is_tuple(IPT) -> - {ip, list_to_binary(inet_parse:ntoa(IP))}; - (tls) -> {tls, true}; - (ssl) -> {tls, true}; - (zlib) -> {zlib, true}; - (starttls) -> {starttls, true}; - (starttls_required) -> {starttls_required, true}; - (Opt) -> Opt - end, Opts), - Opts2 = lists:foldl( - fun(Opt, Acc) -> - try - Mod:transform_listen_option(Opt, Acc) - catch error:undef -> - [Opt|Acc] - end - end, [], Opts1), - TransportOpt = if Transport == tcp -> []; - true -> [{transport, Transport}] - end, - IPOpt = if IPStr == <<"0.0.0.0">> -> []; - true -> [{ip, IPStr}] - end, - IPOpt ++ TransportOpt ++ [{port, Port}, {module, Mod} | Opts2]; -transform_option({{Port, Transport}, Mod, Opts}) - when ?IS_TRANSPORT(Transport) -> - transform_option({{Port, {0,0,0,0}, Transport}, Mod, Opts}); -transform_option({{Port, IP}, Mod, Opts}) -> - transform_option({{Port, IP, tcp}, Mod, Opts}); -transform_option({Port, Mod, Opts}) -> - transform_option({{Port, {0,0,0,0}, tcp}, Mod, Opts}); -transform_option(Opt) -> - Opt. +-spec format_transport(transport(), opts()) -> string(). +format_transport(Transport, Opts) -> + case maps:get(tls, Opts, false) of + true when Transport == tcp -> "TLS"; + true when Transport == udp -> "DTLS"; + false when Transport == tcp -> "TCP"; + false when Transport == udp -> "UDP" + end. -transform_options(Opts) -> - lists:foldl(fun transform_options/2, [], Opts). +-spec apply_rate_limit(non_neg_integer()) -> non_neg_integer(). +apply_rate_limit(Interval) -> + NewInterval = receive + {rate_limit, AcceptInterval} -> + AcceptInterval + after 0 -> + Interval + end, + case NewInterval of + 0 -> ok; + Ms when is_integer(Ms) -> + timer:sleep(Ms); + {linear, I1, T1, T2, I2} -> + {MSec, Sec, _USec} = os:timestamp(), + TS = MSec * 1000000 + Sec, + I = + if + TS =< T1 -> I1; + TS >= T1 + T2 -> I2; + true -> + round((I2 - I1) * (TS - T1) / T2 + I1) + end, + timer:sleep(I) + end, + NewInterval. -transform_options({listen, LOpts}, Opts) -> - [{listen, lists:map(fun transform_option/1, LOpts)} | Opts]; -transform_options(Opt, Opts) -> - [Opt|Opts]. +-spec validator() -> econf:validator(). +validator() -> + econf:and_then( + econf:list( + econf:and_then( + econf:options( + #{module => listen_opt_type(module), + transport => listen_opt_type(transport), + '_' => econf:any()}, + [{required, [module]}]), + fun(Opts) -> + M = proplists:get_value(module, Opts), + T = proplists:get_value(transport, Opts, tcp), + (validator(M, T))(Opts) + end)), + fun prepare_opts/1). --type transport() :: udp | tcp. --type port_ip_transport() :: inet:port_number() | - {inet:port_number(), transport()} | - {inet:port_number(), inet:ip_address()} | - {inet:port_number(), inet:ip_address(), - transport()}. --spec validate_cfg(list()) -> [{port_ip_transport(), module(), list()}]. +-spec validator(module(), transport()) -> econf:validator(). +validator(M, T) -> + Options = listen_options() ++ M:listen_options(), + Required = lists:usort([Opt || Opt <- Options, is_atom(Opt)]), + Disallowed = if T == udp -> + [backlog, use_proxy_protocol, accept_interval]; + true -> + [] + end, + Keywords = ejabberd_config:get_defined_keywords(global) ++ ejabberd_config:get_predefined_keywords(global), + Validator = maps:from_list( + lists:map( + fun(Opt) -> + Type = try M:listen_opt_type(Opt) + catch _:_ when M /= ?MODULE -> + listen_opt_type(Opt) + end, + TypeProcessed = + econf:and_then( + fun(B) -> + ejabberd_config:replace_keywords(global, B, Keywords) + end, + Type), + {Opt, TypeProcessed} + end, proplists:get_keys(Options))), + econf:options( + Validator, + [{required, Required}, {disallowed, Disallowed}, + {return, map}, unique]). -validate_cfg(L) -> - lists:map( - fun(LOpts) -> - lists:foldl( - fun({port, Port}, {{_, IP, T}, Mod, Opts}) -> - true = ?IS_PORT(Port), - {{Port, IP, T}, Mod, Opts}; - ({ip, IP}, {{Port, _, T}, Mod, Opts}) -> - {{Port, prepare_ip(IP), T}, Mod, Opts}; - ({transport, T}, {{Port, IP, _}, Mod, Opts}) -> - true = ?IS_TRANSPORT(T), - {{Port, IP, T}, Mod, Opts}; - ({module, Mod}, {Port, _, Opts}) -> - {Port, prepare_mod(Mod), Opts}; - (Opt, {Port, Mod, Opts}) -> - {Port, Mod, [Opt|Opts]} - end, {{5222, {0,0,0,0}, tcp}, ejabberd_c2s, []}, LOpts) - end, L). +-spec prepare_opts([opts()]) -> [listener()]. +prepare_opts(Listeners) -> + check_overlapping_listeners( + lists:map( + fun(Opts1) -> + {Opts2, Opts3} = partition( + fun({port, _}) -> true; + ({transport, _}) -> true; + ({module, _}) -> true; + (_) -> false + end, Opts1), + Mod = maps:get(module, Opts2), + Port = maps:get(port, Opts2), + Transport = maps:get(transport, Opts2, tcp), + IP = maps:get(ip, Opts3, {0,0,0,0}), + Opts4 = apply_defaults(Mod, Opts3), + {{Port, IP, Transport}, Mod, Opts4} + end, Listeners)). -prepare_ip({A, B, C, D} = IP) - when ?IS_CHAR(A) and ?IS_CHAR(B) and ?IS_CHAR(C) and ?IS_CHAR(D) -> - IP; -prepare_ip({A, B, C, D, E, F, G, H} = IP) - when ?IS_UINT(A) and ?IS_UINT(B) and ?IS_UINT(C) and ?IS_UINT(D) - and ?IS_UINT(E) and ?IS_UINT(F) and ?IS_UINT(G) and ?IS_UINT(H) -> - IP; -prepare_ip(IP) when is_list(IP) -> - {ok, Addr} = inet_parse:address(IP), - Addr; -prepare_ip(IP) when is_binary(IP) -> - prepare_ip(binary_to_list(IP)). +-spec check_overlapping_listeners([listener()]) -> [listener()]. +check_overlapping_listeners(Listeners) -> + _ = lists:foldl( + fun({{Port, IP, Transport} = Key, _, _}, Acc) -> + case lists:member(Key, Acc) of + true -> + econf:fail({listener_dup, {IP, Port}}); + false -> + ZeroIP = case size(IP) of + 8 -> {0,0,0,0,0,0,0,0}; + 4 -> {0,0,0,0} + end, + Key1 = {Port, ZeroIP, Transport}, + case lists:member(Key1, Acc) of + true -> + econf:fail({listener_conflict, + {IP, Port}, {ZeroIP, Port}}); + false -> + [Key|Acc] + end + end + end, [], Listeners), + Listeners. -prepare_mod(ejabberd_sip) -> - prepare_mod(sip); -prepare_mod(sip) -> - esip_socket; -prepare_mod(Mod) when is_atom(Mod) -> - Mod. +-spec apply_defaults(module(), opts()) -> opts(). +apply_defaults(Mod, Opts) -> + lists:foldl( + fun({Opt, Default}, M) -> + case maps:is_key(Opt, M) of + true -> M; + false -> M#{Opt => Default} + end; + (_, M) -> + M + end, Opts, Mod:listen_options() ++ listen_options()). + +%% Convert options to list with removing defaults +-spec opts_to_list(module(), opts()) -> list_opts(). +opts_to_list(Mod, Opts) -> + Defaults = Mod:listen_options() ++ listen_options(), + maps:fold( + fun(Opt, Val, Acc) -> + case proplists:get_value(Opt, Defaults) of + Val -> Acc; + _ -> [{Opt, Val}|Acc] + end + end, [], Opts). + +-spec partition(fun(({atom(), term()}) -> boolean()), opts()) -> {opts(), opts()}. +partition(Fun, Opts) -> + maps:fold( + fun(Opt, Val, {True, False}) -> + case Fun({Opt, Val}) of + true -> {True#{Opt => Val}, False}; + false -> {True, False#{Opt => Val}} + end + end, {#{}, #{}}, Opts). + +-spec listen_opt_type(atom()) -> econf:validator(). +listen_opt_type(port) -> + econf:either( + econf:int(0, 65535), + econf:binary("^unix:.*")); +listen_opt_type(module) -> + econf:beam([[{start, 3}, {start, 2}], + [{start_link, 3}, {start_link, 2}], + {accept, 1}, {listen_options, 0}]); +listen_opt_type(ip) -> + econf:ip(); +listen_opt_type(transport) -> + econf:enum([tcp, udp]); +listen_opt_type(accept_interval) -> + econf:non_neg_int(); +listen_opt_type(backlog) -> + econf:non_neg_int(); +listen_opt_type(supervisor) -> + econf:bool(); +listen_opt_type(ciphers) -> + econf:binary(); +listen_opt_type(dhfile) -> + econf:file(); +listen_opt_type(cafile) -> + econf:pem(); +listen_opt_type(certfile) -> + econf:pem(); +listen_opt_type(protocol_options) -> + econf:and_then( + econf:list(econf:binary()), + fun(Options) -> str:join(Options, <<"|">>) end); +listen_opt_type(tls_compression) -> + econf:bool(); +listen_opt_type(tls) -> + econf:bool(); +listen_opt_type(max_stanza_size) -> + econf:pos_int(infinity); +listen_opt_type(max_fsm_queue) -> + econf:pos_int(); +listen_opt_type(send_timeout) -> + econf:timeout(second, infinity); +listen_opt_type(shaper) -> + econf:shaper(); +listen_opt_type(access) -> + econf:acl(); +listen_opt_type(unix_socket) -> + econf:options( + #{group => econf:non_neg_int(), + owner => econf:non_neg_int(), + mode => econf:octal()}, + [unique, {return, map}]); +listen_opt_type(use_proxy_protocol) -> + econf:bool(). + +listen_options() -> + [module, port, + {transport, tcp}, + {ip, {0,0,0,0}}, + {accept_interval, 0}, + {send_timeout, 15000}, + {backlog, 128}, + {unix_socket, #{}}, + {use_proxy_protocol, false}, + {supervisor, true}]. diff --git a/src/ejabberd_local.erl b/src/ejabberd_local.erl index cec39ced2..5d9557415 100644 --- a/src/ejabberd_local.erl +++ b/src/ejabberd_local.erl @@ -5,7 +5,7 @@ %%% Created : 30 Nov 2002 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -30,36 +30,29 @@ -behaviour(gen_server). %% API --export([start_link/0]). +-export([start/0, start_link/0]). --export([route/3, route_iq/4, route_iq/5, - process_iq_reply/3, register_iq_handler/4, - register_iq_handler/5, register_iq_response_handler/4, - register_iq_response_handler/5, unregister_iq_handler/2, - unregister_iq_response_handler/2, refresh_iq_handlers/0, - bounce_resource_packet/3]). +-export([route/1, + get_features/1, + bounce_resource_packet/1, + host_up/1, host_down/1]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --include("ejabberd.hrl"). --include("logger.hrl"). +%% deprecated functions: use ejabberd_router:route_iq/3,4 +-export([route_iq/2, route_iq/3]). +-deprecated([{route_iq, 2}, {route_iq, 3}]). --include("jlib.hrl"). +-include("logger.hrl"). +-include_lib("stdlib/include/ms_transform.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). + +-include("translate.hrl"). -record(state, {}). --record(iq_response, {id = <<"">> :: binary(), - module :: atom(), - function :: atom() | fun(), - timer = make_ref() :: reference()}). - --define(IQTABLE, local_iqtable). - -%% This value is used in SIP and Megaco for a transaction lifetime. --define(IQ_TIMEOUT, 32000). - %%==================================================================== %% API %%==================================================================== @@ -67,296 +60,114 @@ %% Function: start_link() -> {ok,Pid} | ignore | {error,Error} %% Description: Starts the server %%-------------------------------------------------------------------- +start() -> + ChildSpec = {?MODULE, {?MODULE, start_link, []}, + transient, 1000, worker, [?MODULE]}, + supervisor:start_child(ejabberd_sup, ChildSpec). + start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). -process_iq(From, To, Packet) -> - IQ = jlib:iq_query_info(Packet), - case IQ of - #iq{xmlns = XMLNS} -> - Host = To#jid.lserver, - case ets:lookup(?IQTABLE, {XMLNS, Host}) of - [{_, Module, Function}] -> - ResIQ = Module:Function(From, To, IQ), - if ResIQ /= ignore -> - ejabberd_router:route(To, From, jlib:iq_to_xml(ResIQ)); - true -> ok - end; - [{_, Module, Function, Opts}] -> - gen_iq_handler:handle(Host, Module, Function, Opts, - From, To, IQ); - [] -> - Err = jlib:make_error_reply(Packet, - ?ERR_FEATURE_NOT_IMPLEMENTED), - ejabberd_router:route(To, From, Err) - end; - reply -> - IQReply = jlib:iq_query_or_response_info(Packet), - process_iq_reply(From, To, IQReply); - _ -> - Err = jlib:make_error_reply(Packet, ?ERR_BAD_REQUEST), - ejabberd_router:route(To, From, Err), - ok +-spec route(stanza()) -> ok. +route(Packet) -> + ?DEBUG("Local route:~n~ts", [xmpp:pp(Packet)]), + Type = xmpp:get_type(Packet), + To = xmpp:get_to(Packet), + if To#jid.luser /= <<"">> -> + ejabberd_sm:route(Packet); + is_record(Packet, iq), To#jid.lresource == <<"">> -> + gen_iq_handler:handle(?MODULE, Packet); + Type == result; Type == error -> + ok; + true -> + ejabberd_hooks:run(local_send_to_resource_hook, + To#jid.lserver, [Packet]) end. -process_iq_reply(From, To, #iq{id = ID} = IQ) -> - case get_iq_callback(ID) of - {ok, undefined, Function} -> Function(IQ), ok; - {ok, Module, Function} -> - Module:Function(From, To, IQ), ok; - _ -> nothing - end. +-spec route_iq(iq(), function()) -> ok. +route_iq(IQ, Fun) -> + route_iq(IQ, Fun, undefined). -route(From, To, Packet) -> - case catch do_route(From, To, Packet) of - {'EXIT', Reason} -> - ?ERROR_MSG("~p~nwhen processing: ~p", - [Reason, {From, To, Packet}]); - _ -> ok - end. +-spec route_iq(iq(), function(), undefined | non_neg_integer()) -> ok. +route_iq(IQ, Fun, Timeout) -> + ejabberd_router:route_iq(IQ, Fun, undefined, Timeout). -route_iq(From, To, IQ, F) -> - route_iq(From, To, IQ, F, undefined). - -route_iq(From, To, #iq{type = Type} = IQ, F, Timeout) - when is_function(F) -> - Packet = if Type == set; Type == get -> - ID = randoms:get_string(), - Host = From#jid.lserver, - register_iq_response_handler(Host, ID, undefined, F, Timeout), - jlib:iq_to_xml(IQ#iq{id = ID}); - true -> - jlib:iq_to_xml(IQ) - end, - ejabberd_router:route(From, To, Packet). - -register_iq_response_handler(Host, ID, Module, - Function) -> - register_iq_response_handler(Host, ID, Module, Function, - undefined). - -register_iq_response_handler(_Host, ID, Module, - Function, Timeout0) -> - Timeout = case Timeout0 of - undefined -> ?IQ_TIMEOUT; - N when is_integer(N), N > 0 -> N - end, - TRef = erlang:start_timer(Timeout, ejabberd_local, ID), - mnesia:dirty_write(#iq_response{id = ID, - module = Module, - function = Function, - timer = TRef}). - -register_iq_handler(Host, XMLNS, Module, Fun) -> - ejabberd_local ! - {register_iq_handler, Host, XMLNS, Module, Fun}. - -register_iq_handler(Host, XMLNS, Module, Fun, Opts) -> - ejabberd_local ! - {register_iq_handler, Host, XMLNS, Module, Fun, Opts}. - -unregister_iq_response_handler(_Host, ID) -> - catch get_iq_callback(ID), ok. - -unregister_iq_handler(Host, XMLNS) -> - ejabberd_local ! {unregister_iq_handler, Host, XMLNS}. - -refresh_iq_handlers() -> - ejabberd_local ! refresh_iq_handlers. - -bounce_resource_packet(From, To, Packet) -> - Err = jlib:make_error_reply(Packet, - ?ERR_ITEM_NOT_FOUND), - ejabberd_router:route(To, From, Err), +-spec bounce_resource_packet(stanza()) -> ok | stop. +bounce_resource_packet(#presence{to = #jid{lresource = <<"">>}}) -> + ok; +bounce_resource_packet(#message{to = #jid{lresource = <<"">>}, type = headline}) -> + ok; +bounce_resource_packet(Packet) -> + Lang = xmpp:get_lang(Packet), + Txt = ?T("No available resource found"), + Err = xmpp:err_item_not_found(Txt, Lang), + ejabberd_router:route_error(Packet, Err), stop. +-spec get_features(binary()) -> [binary()]. +get_features(Host) -> + gen_iq_handler:get_features(?MODULE, Host). + %%==================================================================== %% gen_server callbacks %%==================================================================== -%%-------------------------------------------------------------------- -%% Function: init(Args) -> {ok, State} | -%% {ok, State, Timeout} | -%% ignore | -%% {stop, Reason} -%% Description: Initiates the server -%%-------------------------------------------------------------------- init([]) -> - lists:foreach(fun (Host) -> - ejabberd_router:register_route(Host, - {apply, ?MODULE, - route}), - ejabberd_hooks:add(local_send_to_resource_hook, Host, - ?MODULE, bounce_resource_packet, - 100) - end, - ?MYHOSTS), - catch ets:new(?IQTABLE, [named_table, public]), + process_flag(trap_exit, true), + lists:foreach(fun host_up/1, ejabberd_option:hosts()), + ejabberd_hooks:add(host_up, ?MODULE, host_up, 10), + ejabberd_hooks:add(host_down, ?MODULE, host_down, 100), + gen_iq_handler:start(?MODULE), update_table(), - mnesia:create_table(iq_response, - [{ram_copies, [node()]}, - {attributes, record_info(fields, iq_response)}]), - mnesia:add_table_copy(iq_response, node(), ram_copies), {ok, #state{}}. -%%-------------------------------------------------------------------- -%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | -%% {reply, Reply, State, Timeout} | -%% {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, Reply, State} | -%% {stop, Reason, State} -%% Description: Handling call messages -%%-------------------------------------------------------------------- -handle_call(_Request, _From, State) -> -%%-------------------------------------------------------------------- -%% Function: handle_cast(Msg, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} -%% Description: Handling cast messages -%%-------------------------------------------------------------------- -%%-------------------------------------------------------------------- -%% Function: handle_info(Info, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} -%% Description: Handling all non call/cast messages -%%-------------------------------------------------------------------- - Reply = ok, {reply, Reply, State}. +handle_call(Request, From, State) -> + ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), + {noreply, State}. -handle_cast(_Msg, State) -> {noreply, State}. +handle_cast(Msg, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Msg]), + {noreply, State}. -handle_info({route, From, To, Packet}, State) -> - case catch do_route(From, To, Packet) of - {'EXIT', Reason} -> - ?ERROR_MSG("~p~nwhen processing: ~p", - [Reason, {From, To, Packet}]); - _ -> ok - end, +handle_info({route, Packet}, State) -> + route(Packet), {noreply, State}; -handle_info({register_iq_handler, Host, XMLNS, Module, - Function}, - State) -> - ets:insert(?IQTABLE, {{XMLNS, Host}, Module, Function}), - catch mod_disco:register_feature(Host, XMLNS), - {noreply, State}; -handle_info({register_iq_handler, Host, XMLNS, Module, - Function, Opts}, - State) -> - ets:insert(?IQTABLE, - {{XMLNS, Host}, Module, Function, Opts}), - catch mod_disco:register_feature(Host, XMLNS), - {noreply, State}; -handle_info({unregister_iq_handler, Host, XMLNS}, - State) -> - case ets:lookup(?IQTABLE, {XMLNS, Host}) of - [{_, Module, Function, Opts}] -> - gen_iq_handler:stop_iq_handler(Module, Function, Opts); - _ -> ok - end, - ets:delete(?IQTABLE, {XMLNS, Host}), - catch mod_disco:unregister_feature(Host, XMLNS), - {noreply, State}; -handle_info(refresh_iq_handlers, State) -> - lists:foreach(fun (T) -> - case T of - {{XMLNS, Host}, _Module, _Function, _Opts} -> - catch mod_disco:register_feature(Host, XMLNS); - {{XMLNS, Host}, _Module, _Function} -> - catch mod_disco:register_feature(Host, XMLNS); - _ -> ok - end - end, - ets:tab2list(?IQTABLE)), - {noreply, State}; -handle_info({timeout, _TRef, ID}, State) -> - process_iq_timeout(ID), - {noreply, State}; -%%-------------------------------------------------------------------- -%% Function: terminate(Reason, State) -> void() -%% Description: This function is called by a gen_server when it is about to -%% terminate. It should be the opposite of Module:init/1 and do any necessary -%% cleaning up. When it returns, the gen_server terminates with Reason. -%% The return value is ignored. -%%-------------------------------------------------------------------- -%%-------------------------------------------------------------------- -%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} -%% Description: Convert process state when code is changed -%%-------------------------------------------------------------------- +handle_info(Info, State) -> + ?WARNING_MSG("Unexpected info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, _State) -> + lists:foreach(fun host_down/1, ejabberd_option:hosts()), + ejabberd_hooks:delete(host_up, ?MODULE, host_up, 10), + ejabberd_hooks:delete(host_down, ?MODULE, host_down, 100), + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- -handle_info(_Info, State) -> {noreply, State}. - -terminate(_Reason, _State) -> ok. - -code_change(_OldVsn, State, _Extra) -> {ok, State}. - -do_route(From, To, Packet) -> - ?DEBUG("local route~n\tfrom ~p~n\tto ~p~n\tpacket " - "~P~n", - [From, To, Packet, 8]), - if To#jid.luser /= <<"">> -> - ejabberd_sm:route(From, To, Packet); - To#jid.lresource == <<"">> -> - #xmlel{name = Name} = Packet, - case Name of - <<"iq">> -> process_iq(From, To, Packet); - <<"message">> -> ok; - <<"presence">> -> ok; - _ -> ok - end; - true -> - #xmlel{attrs = Attrs} = Packet, - case xml:get_attr_s(<<"type">>, Attrs) of - <<"error">> -> ok; - <<"result">> -> ok; - _ -> - ejabberd_hooks:run(local_send_to_resource_hook, - To#jid.lserver, [From, To, Packet]) - end - end. - +-spec update_table() -> ok. update_table() -> - case catch mnesia:table_info(iq_response, attributes) of - [id, module, function] -> - mnesia:delete_table(iq_response); - [id, module, function, timer] -> - ok; - {'EXIT', _} -> - ok - end. + catch mnesia:delete_table(iq_response), + ok. -get_iq_callback(ID) -> - case mnesia:dirty_read(iq_response, ID) of - [#iq_response{module = Module, timer = TRef, - function = Function}] -> - cancel_timer(TRef), - mnesia:dirty_delete(iq_response, ID), - {ok, Module, Function}; - _ -> - error - end. +host_up(Host) -> + Owner = case whereis(?MODULE) of + undefined -> self(); + Pid -> Pid + end, + ejabberd_router:register_route(Host, Host, {apply, ?MODULE, route}, Owner), + ejabberd_hooks:add(local_send_to_resource_hook, Host, + ?MODULE, bounce_resource_packet, 100). -process_iq_timeout(ID) -> - spawn(fun process_iq_timeout/0) ! ID. - -process_iq_timeout() -> - receive - ID -> - case get_iq_callback(ID) of - {ok, undefined, Function} -> - Function(timeout); - _ -> - ok - end - after 5000 -> - ok - end. - -cancel_timer(TRef) -> - case erlang:cancel_timer(TRef) of - false -> - receive {timeout, TRef, _} -> ok after 0 -> ok end; - _ -> ok - end. +host_down(Host) -> + Owner = case whereis(?MODULE) of + undefined -> self(); + Pid -> Pid + end, + ejabberd_router:unregister_route(Host, Owner), + ejabberd_hooks:delete(local_send_to_resource_hook, Host, + ?MODULE, bounce_resource_packet, 100). diff --git a/src/ejabberd_logger.erl b/src/ejabberd_logger.erl index 59beca16d..c002914bf 100644 --- a/src/ejabberd_logger.erl +++ b/src/ejabberd_logger.erl @@ -1,12 +1,11 @@ %%%------------------------------------------------------------------- -%%% @author Evgeniy Khramtsov -%%% @copyright (C) 2013, Evgeniy Khramtsov -%%% @doc -%%% -%%% @end +%%% File : ejabberd_logger.erl +%%% Author : Evgeniy Khramtsov +%%% Purpose : ejabberd logger wrapper %%% Created : 12 May 2013 by Evgeniy Khramtsov %%% -%%% ejabberd, Copyright (C) 2013-2015 ProcessOne +%%% +%%% ejabberd, Copyright (C) 2013-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -24,55 +23,85 @@ %%%------------------------------------------------------------------- -module(ejabberd_logger). +-compile({no_auto_import, [get/0]}). %% API --export([start/0, reopen_log/0, get/0, set/1, get_log_path/0]). +-export([start/0, get/0, set/1, get_log_path/0, flush/0]). +-export([convert_loglevel/1, loglevels/0, set_modules_fully_logged/1, config_reloaded/0]). +-ifndef(LAGER). +-export([progress_filter/2]). +-endif. +%% Deprecated functions +-export([restart/0, reopen_log/0, rotate_log/0]). +-deprecated([{restart, 0}, + {reopen_log, 0}, + {rotate_log, 0}]). --include("ejabberd.hrl"). +-type loglevel() :: none | emergency | alert | critical | + error | warning | notice | info | debug. --type loglevel() :: 0 | 1 | 2 | 3 | 4 | 5. +-define(is_loglevel(L), + ((L == none) or (L == emergency) or (L == alert) + or (L == critical) or (L == error) or (L == warning) + or (L == notice) or (L == info) or (L == debug))). --spec start() -> ok. --spec get_log_path() -> string(). --spec reopen_log() -> ok. --spec get() -> {loglevel(), atom(), string()}. --spec set(loglevel() | {loglevel(), list()}) -> {module, module()}. +-export_type([loglevel/0]). + +-include("logger.hrl"). %%%=================================================================== %%% API %%%=================================================================== -%% @doc Returns the full path to the ejabberd log file. -%% It first checks for application configuration parameter 'log_path'. -%% If not defined it checks the environment variable EJABBERD_LOG_PATH. -%% And if that one is neither defined, returns the default value: -%% "ejabberd.log" in current directory. +-spec get_log_path() -> string(). get_log_path() -> - case application:get_env(ejabberd, log_path) of + case ejabberd_config:env_binary_to_list(ejabberd, log_path) of {ok, Path} -> Path; undefined -> case os:getenv("EJABBERD_LOG_PATH") of false -> - ?LOG_PATH; + "ejabberd.log"; Path -> Path end end. --ifdef(LAGER). +-spec loglevels() -> [loglevel(), ...]. +loglevels() -> + [none, emergency, alert, critical, error, warning, notice, info, debug]. +-spec convert_loglevel(0..5) -> loglevel(). +convert_loglevel(0) -> none; +convert_loglevel(1) -> critical; +convert_loglevel(2) -> error; +convert_loglevel(3) -> warning; +convert_loglevel(4) -> info; +convert_loglevel(5) -> debug. + +quiet_mode() -> + case application:get_env(ejabberd, quiet) of + {ok, true} -> true; + _ -> false + end. + +-spec get_integer_env(atom(), T) -> T. get_integer_env(Name, Default) -> case application:get_env(ejabberd, Name) of {ok, I} when is_integer(I), I>=0 -> I; + {ok, infinity} -> + infinity; undefined -> Default; {ok, Junk} -> - error_logger:error_msg("wrong value for ~s: ~p; " + error_logger:error_msg("wrong value for ~ts: ~p; " "using ~p as a fallback~n", [Name, Junk, Default]), Default end. + +-ifdef(LAGER). +-spec get_string_env(atom(), T) -> T. get_string_env(Name, Default) -> case application:get_env(ejabberd, Name) of {ok, L} when is_list(L) -> @@ -80,13 +109,40 @@ get_string_env(Name, Default) -> undefined -> Default; {ok, Junk} -> - error_logger:error_msg("wrong value for ~s: ~p; " + error_logger:error_msg("wrong value for ~ts: ~p; " "using ~p as a fallback~n", [Name, Junk, Default]), Default end. start() -> + start(info). + +start(Level) -> + StartedApps = application:which_applications(5000), + case lists:keyfind(logger, 1, StartedApps) of + %% Elixir logger is started. We assume everything is in place + %% to use lager to Elixir logger bridge. + {logger, _, _} -> + error_logger:info_msg("Ignoring ejabberd logger options, using Elixir Logger.", []), + %% Do not start lager, we rely on Elixir Logger + do_start_for_logger(Level); + _ -> + do_start(Level) + end. + +do_start_for_logger(Level) -> + application:load(sasl), + application:set_env(sasl, sasl_error_logger, false), + application:load(lager), + application:set_env(lager, error_logger_redirect, false), + application:set_env(lager, error_logger_whitelist, ['Elixir.Logger.ErrorHandler']), + application:set_env(lager, crash_log, false), + application:set_env(lager, handlers, [{elixir_logger_backend, [{level, Level}]}]), + ejabberd:start_app(lager), + ok. + +do_start(Level) -> application:load(sasl), application:set_env(sasl, sasl_error_logger, false), application:load(lager), @@ -95,14 +151,25 @@ start() -> ErrorLog = filename:join([Dir, "error.log"]), CrashLog = filename:join([Dir, "crash.log"]), LogRotateDate = get_string_env(log_rotate_date, ""), - LogRotateSize = get_integer_env(log_rotate_size, 10*1024*1024), + LogRotateSize = case get_integer_env(log_rotate_size, 10*1024*1024) of + infinity -> 0; + V -> V + end, LogRotateCount = get_integer_env(log_rotate_count, 1), LogRateLimit = get_integer_env(log_rate_limit, 100), + ConsoleLevel0 = case quiet_mode() of + true -> critical; + _ -> Level + end, + ConsoleLevel = case get_lager_version() >= "3.6.0" of + true -> [{level, ConsoleLevel0}]; + false -> ConsoleLevel0 + end, application:set_env(lager, error_logger_hwm, LogRateLimit), application:set_env( lager, handlers, - [{lager_console_backend, info}, - {lager_file_backend, [{file, ConsoleLog}, {level, info}, {date, LogRotateDate}, + [{lager_console_backend, ConsoleLevel}, + {lager_file_backend, [{file, ConsoleLog}, {level, Level}, {date, LogRotateDate}, {count, LogRotateCount}, {size, LogRotateSize}]}, {lager_file_backend, [{file, ErrorLog}, {level, error}, {date, LogRotateDate}, {count, LogRotateCount}, {size, LogRotateSize}]}]), @@ -111,10 +178,23 @@ start() -> application:set_env(lager, crash_log_size, LogRotateSize), application:set_env(lager, crash_log_count, LogRotateCount), ejabberd:start_app(lager), + lists:foreach(fun(Handler) -> + lager:set_loghwm(Handler, LogRateLimit) + end, gen_event:which_handlers(lager_event)). + +restart() -> + Level = ejabberd_option:loglevel(), + application:stop(lager), + start(Level). + +config_reloaded() -> ok. reopen_log() -> - lager_crash_log ! rotate, + ok. + +rotate_log() -> + catch lager_crash_log ! rotate, lists:foreach( fun({lager_file_backend, File}) -> whereis(lager_event) ! {rotate, File}; @@ -123,96 +203,245 @@ reopen_log() -> end, gen_event:which_handlers(lager_event)). get() -> - case lager:get_loglevel(lager_console_backend) of - none -> {0, no_log, "No log"}; - emergency -> {1, critical, "Critical"}; - alert -> {1, critical, "Critical"}; - critical -> {1, critical, "Critical"}; - error -> {2, error, "Error"}; - warning -> {3, warning, "Warning"}; - notice -> {3, warning, "Warning"}; - info -> {4, info, "Info"}; - debug -> {5, debug, "Debug"} - end. + Handlers = get_lager_handlers(), + lists:foldl(fun(lager_console_backend, _Acc) -> + lager:get_loglevel(lager_console_backend); + (elixir_logger_backend, _Acc) -> + lager:get_loglevel(elixir_logger_backend); + (_, Acc) -> + Acc + end, + none, Handlers). -set(LogLevel) when is_integer(LogLevel) -> - LagerLogLevel = case LogLevel of - 0 -> none; - 1 -> critical; - 2 -> error; - 3 -> warning; - 4 -> info; - 5 -> debug - end, - case lager:get_loglevel(lager_console_backend) of - LagerLogLevel -> +set(N) when is_integer(N), N>=0, N=<5 -> + set(convert_loglevel(N)); +set(Level) when ?is_loglevel(Level) -> + case get() of + Level -> ok; _ -> ConsoleLog = get_log_path(), + QuietMode = quiet_mode(), lists:foreach( fun({lager_file_backend, File} = H) when File == ConsoleLog -> - lager:set_loglevel(H, LagerLogLevel); - (lager_console_backend = H) -> - lager:set_loglevel(H, LagerLogLevel); + lager:set_loglevel(H, Level); + (lager_console_backend = H) when not QuietMode -> + lager:set_loglevel(H, Level); + (elixir_logger_backend = H) -> + lager:set_loglevel(H, Level); (_) -> ok - end, gen_event:which_handlers(lager_event)) + end, get_lager_handlers()) end, - {module, lager}; -set({_LogLevel, _}) -> - error_logger:error_msg("custom loglevels are not supported for 'lager'"), - {module, lager}. + case Level of + debug -> xmpp:set_config([{debug, true}]); + _ -> xmpp:set_config([{debug, false}]) + end. + +get_lager_handlers() -> + case catch gen_event:which_handlers(lager_event) of + {'EXIT',noproc} -> + []; + Result -> + Result + end. + +-spec get_lager_version() -> string(). +get_lager_version() -> + Apps = application:loaded_applications(), + case lists:keyfind(lager, 1, Apps) of + {_, _, Vsn} -> Vsn; + false -> "0.0.0" + end. + +set_modules_fully_logged(_) -> ok. + +flush() -> + application:stop(lager), + application:stop(sasl). -else. +-include_lib("kernel/include/logger.hrl"). +-spec start() -> ok | {error, term()}. start() -> - set(4), - LogPath = get_log_path(), - error_logger:add_report_handler(p1_logger_h, LogPath), - ok. + start(info). -reopen_log() -> - %% TODO: Use the Reopen log API for logger_h ? - p1_logger_h:reopen_log(), - reopen_sasl_log(). - -get() -> - p1_loglevel:get(). - -set(LogLevel) -> - p1_loglevel:set(LogLevel). - -%%%=================================================================== -%%% Internal functions -%%%=================================================================== -reopen_sasl_log() -> - case application:get_env(sasl,sasl_error_logger) of - {ok, {file, SASLfile}} -> - error_logger:delete_report_handler(sasl_report_file_h), - rotate_sasl_log(SASLfile), - error_logger:add_report_handler(sasl_report_file_h, - {SASLfile, get_sasl_error_logger_type()}); - _ -> false +start(Level) -> + EjabberdLog = get_log_path(), + Dir = filename:dirname(EjabberdLog), + ErrorLog = filename:join([Dir, "error.log"]), + LogRotateSize = get_integer_env(log_rotate_size, 10*1024*1024), + LogRotateCount = get_integer_env(log_rotate_count, 1), + LogBurstLimitWindowTime = get_integer_env(log_burst_limit_window_time, 1000), + LogBurstLimitCount = get_integer_env(log_burst_limit_count, 500), + Config = #{max_no_bytes => LogRotateSize, + max_no_files => LogRotateCount, + filesync_repeat_interval => no_repeat, + file_check => 1000, + sync_mode_qlen => 1000, + drop_mode_qlen => 1000, + flush_qlen => 5000, + burst_limit_window_time => LogBurstLimitWindowTime, + burst_limit_max_count => LogBurstLimitCount}, + FmtConfig = #{legacy_header => false, + time_designator => $\s, + max_size => 100*1024, + single_line => false}, + FileFmtConfig = FmtConfig#{template => file_template()}, + ConsoleFmtConfig = FmtConfig#{template => console_template()}, + try + ok = logger:set_primary_config(level, Level), + DefaultHandlerId = get_default_handlerid(), + ok = logger:update_formatter_config(DefaultHandlerId, ConsoleFmtConfig), + case quiet_mode() of + true -> + ok = logger:set_handler_config(DefaultHandlerId, level, critical); + _ -> + ok end, + case logger:add_primary_filter(progress_report, + {fun ?MODULE:progress_filter/2, stop}) of + ok -> ok; + {error, {already_exist, _}} -> ok + end, + case logger:add_handler(ejabberd_log, logger_std_h, + #{level => all, + config => Config#{file => EjabberdLog}, + formatter => {logger_formatter, FileFmtConfig}}) of + ok -> ok; + {error, {already_exist, _}} -> ok + end, + case logger:add_handler(error_log, logger_std_h, + #{level => error, + config => Config#{file => ErrorLog}, + formatter => {logger_formatter, FileFmtConfig}}) of + ok -> ok; + {error, {already_exist, _}} -> ok + end + catch _:{Tag, Err} when Tag == badmatch; Tag == case_clause -> + ?LOG_CRITICAL("Failed to set logging: ~p", [Err]), + Err + end. + +get_default_handlerid() -> + Ids = logger:get_handler_ids(), + case lists:member(default, Ids) of + true -> default; + false -> hd(Ids) + end. + +-spec restart() -> ok. +restart() -> ok. -rotate_sasl_log(Filename) -> - case file:read_file_info(Filename) of - {ok, _FileInfo} -> - file:rename(Filename, [Filename, ".0"]), - ok; - {error, _Reason} -> - ok +-spec config_reloaded() -> ok. +config_reloaded() -> + LogRotateSize = ejabberd_option:log_rotate_size(), + LogRotateCount = ejabberd_option:log_rotate_count(), + LogBurstLimitWindowTime = ejabberd_option:log_burst_limit_window_time(), + LogBurstLimitCount = ejabberd_option:log_burst_limit_count(), + lists:foreach( + fun(Handler) -> + case logger:get_handler_config(Handler) of + {ok, #{config := Config}} -> + Config2 = Config#{ + max_no_bytes => LogRotateSize, + max_no_files => LogRotateCount, + burst_limit_window_time => LogBurstLimitWindowTime, + burst_limit_max_count => LogBurstLimitCount}, + logger:update_handler_config(Handler, config, Config2); + _ -> + ok + end + end, [ejabberd_log, error_log]). + +progress_filter(#{level:=info,msg:={report,#{label:={_,progress}}}} = Event, _) -> + case get() of + debug -> + logger_filters:progress(Event#{level => debug}, log); + _ -> + stop + end; +progress_filter(Event, _) -> + Event. + +-ifdef(ELIXIR_ENABLED). +console_template() -> + case (false /= code:is_loaded('Elixir.Logger')) + andalso + 'Elixir.System':version() >= <<"1.15">> of + true -> + {ok, DC} = logger:get_handler_config(default), + MessageFormat = case maps:get(formatter, DC) of + %% https://hexdocs.pm/logger/1.17.2/Logger.Formatter.html#module-formatting + {'Elixir.Logger.Formatter', _} -> + message; + %% https://www.erlang.org/doc/apps/kernel/logger_formatter#t:template/0 + {logger_formatter, _} -> + msg + end, + [date, " ", time, " [", level, "] ", MessageFormat, "\n"]; + false -> + [time, " [", level, "] " | msg()] + end. +msg() -> + [{logger_formatter, [[logger_formatter, title], ":", io_lib:nl()], []}, + msg, io_lib:nl()]. +-else. +console_template() -> + [time, " ", ?CLEAD, ?CDEFAULT, clevel, "[", level, "] ", ?CMID, ?CDEFAULT, ctext | msg()]. +msg() -> + [{logger_formatter, [[logger_formatter, title], ":", io_lib:nl()], []}, + msg, ?CCLEAN, io_lib:nl()]. +-endif. + +file_template() -> + [time, " [", level, "] ", pid, + {mfa, ["@", mfa, {line, [":", line], []}], []}, " " | msg()]. + +-spec reopen_log() -> ok. +reopen_log() -> + ok. + +-spec rotate_log() -> ok. +rotate_log() -> + ok. + +-spec get() -> loglevel(). +get() -> + #{level := Level} = logger:get_primary_config(), + Level. + +-spec set(0..5 | loglevel()) -> ok. +set(N) when is_integer(N), N>=0, N=<5 -> + set(convert_loglevel(N)); +set(Level) when ?is_loglevel(Level) -> + case get() of + Level -> ok; + PrevLevel -> + ?LOG_NOTICE("Changing loglevel from '~s' to '~s'", + [PrevLevel, Level]), + logger:set_primary_config(level, Level), + case Level of + debug -> xmpp:set_config([{debug, true}]); + _ -> xmpp:set_config([{debug, false}]) + end end. -%% Function copied from Erlang/OTP lib/sasl/src/sasl.erl which doesn't export it -get_sasl_error_logger_type () -> - case application:get_env (sasl, errlog_type) of - {ok, error} -> error; - {ok, progress} -> progress; - {ok, all} -> all; - {ok, Bad} -> exit ({bad_config, {sasl, {errlog_type, Bad}}}); - _ -> all - end. +set_modules_fully_logged(Modules) -> + logger:unset_module_level(), + logger:set_module_level(Modules, all). + +-spec flush() -> ok. +flush() -> + lists:foreach( + fun(#{id := HandlerId, module := logger_std_h}) -> + logger_std_h:filesync(HandlerId); + (#{id := HandlerId, module := logger_disk_log_h}) -> + logger_disk_log_h:filesync(HandlerId); + (_) -> + ok + end, logger:get_handler_config()). -endif. diff --git a/src/ejabberd_mnesia.erl b/src/ejabberd_mnesia.erl new file mode 100644 index 000000000..d9db27219 --- /dev/null +++ b/src/ejabberd_mnesia.erl @@ -0,0 +1,465 @@ +%%%---------------------------------------------------------------------- +%%% File : mnesia_mnesia.erl +%%% Author : Christophe Romain +%%% Purpose : Handle configurable mnesia schema +%%% Created : 17 Nov 2016 by Christophe Romain +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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. +%%% +%%%---------------------------------------------------------------------- + +%%% This module should be used everywhere ejabberd creates a mnesia table +%%% to make the schema customizable without code change +%%% Just apply this change in ejabberd modules +%%% s/ejabberd_mnesia:create(?MODULE, /ejabberd_mnesia:create(?MODULE, / + +-module(ejabberd_mnesia). +-author('christophe.romain@process-one.net'). + +-behaviour(gen_server). + +-export([start/0, create/3, update/2, transform/2, transform/3, + dump_schema/0]). +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-define(NEED_RESET, [local_content, type]). + +-include("logger.hrl"). + + +-record(state, {tables = #{} :: tables(), + schema = [] :: [{atom(), custom_schema()}]}). + +-type tables() :: #{atom() => {[{atom(), term()}], term()}}. +-type custom_schema() :: [{ram_copies | disc_copies | disc_only_copies, [node()]} | + {local_content, boolean()} | + {type, set | ordered_set | bag} | + {attributes, [atom()]} | + {index, [atom()]}]. + +start() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +-spec create(module(), atom(), list()) -> any(). +create(Module, Name, TabDef) -> + gen_server:call(?MODULE, {create, Module, Name, TabDef}, + %% Huge timeout is need to have enough + %% time to transform huge tables + timer:minutes(30)). + +init([]) -> + ejabberd_config:env_binary_to_list(mnesia, dir), + MyNode = node(), + DbNodes = mnesia:system_info(db_nodes), + case lists:member(MyNode, DbNodes) of + true -> + case mnesia:system_info(extra_db_nodes) of + [] -> mnesia:create_schema([node()]); + _ -> ok + end, + ejabberd:start_app(mnesia, permanent), + ?DEBUG("Waiting for Mnesia tables synchronization...", []), + mnesia:wait_for_tables(mnesia:system_info(local_tables), infinity), + Schema = read_schema_file(), + {ok, #state{schema = Schema}}; + false -> + ?CRITICAL_MSG("Erlang node name mismatch: I'm running in node [~ts], " + "but the mnesia database is owned by ~p", [MyNode, DbNodes]), + ?CRITICAL_MSG("Either set ERLANG_NODE in ejabberdctl.cfg " + "or change node name in Mnesia by running: " + "ejabberdctl mnesia_change ~ts", [hd(DbNodes)]), + {stop, node_name_mismatch} + end. + +handle_call({create, Module, Name, TabDef}, _From, State) -> + case maps:get(Name, State#state.tables, undefined) of + {TabDef, Result} -> + {reply, Result, State}; + _ -> + Result = do_create(Module, Name, TabDef, State#state.schema), + Tables = maps:put(Name, {TabDef, Result}, State#state.tables), + {reply, Result, State#state{tables = Tables}} + end; +handle_call(Request, From, State) -> + ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), + {noreply, State}. + +handle_cast(Msg, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Msg]), + {noreply, State}. + +handle_info(Info, State) -> + ?WARNING_MSG("Unexpected info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +do_create(Module, Name, TabDef, TabDefs) -> + code:ensure_loaded(Module), + Schema = schema(Name, TabDef, TabDefs), + {attributes, Attrs} = lists:keyfind(attributes, 1, Schema), + case catch mnesia:table_info(Name, attributes) of + {'EXIT', _} -> + create(Name, TabDef); + Attrs -> + case need_reset(Name, Schema) of + true -> + reset(Name, Schema); + false -> + case update(Name, Attrs, Schema) of + {atomic, ok} -> + transform(Module, Name, Attrs, Attrs); + Err -> + Err + end + end; + OldAttrs -> + transform(Module, Name, OldAttrs, Attrs) + end. + +reset(Name, TabDef) -> + ?INFO_MSG("Deleting Mnesia table '~ts'", [Name]), + mnesia_op(delete_table, [Name]), + create(Name, TabDef). + +update(Name, TabDef) -> + {attributes, Attrs} = lists:keyfind(attributes, 1, TabDef), + update(Name, Attrs, TabDef). + +update(Name, Attrs, TabDef) -> + case change_table_copy_type(Name, TabDef) of + {atomic, ok} -> + CurrIndexes = [lists:nth(N-1, Attrs) || + N <- mnesia:table_info(Name, index)], + NewIndexes = proplists:get_value(index, TabDef, []), + case delete_indexes(Name, CurrIndexes -- NewIndexes) of + {atomic, ok} -> + add_indexes(Name, NewIndexes -- CurrIndexes); + Err -> + Err + end; + Err -> + Err + end. + +change_table_copy_type(Name, TabDef) -> + CurrType = mnesia:table_info(Name, storage_type), + NewType = case lists:filter(fun is_storage_type_option/1, TabDef) of + [{Type, _}|_] -> Type; + [] -> CurrType + end, + if NewType /= CurrType -> + ?INFO_MSG("Changing Mnesia table '~ts' from ~ts to ~ts", + [Name, CurrType, NewType]), + if CurrType == unknown -> mnesia_op(add_table_copy, [Name, node(), NewType]); + true -> + mnesia_op(change_table_copy_type, [Name, node(), NewType]) + end; + true -> + {atomic, ok} + end. + +delete_indexes(Name, [Index|Indexes]) -> + ?INFO_MSG("Deleting index '~ts' from Mnesia table '~ts'", [Index, Name]), + case mnesia_op(del_table_index, [Name, Index]) of + {atomic, ok} -> + delete_indexes(Name, Indexes); + Err -> + Err + end; +delete_indexes(_Name, []) -> + {atomic, ok}. + +add_indexes(Name, [Index|Indexes]) -> + ?INFO_MSG("Adding index '~ts' to Mnesia table '~ts'", [Index, Name]), + case mnesia_op(add_table_index, [Name, Index]) of + {atomic, ok} -> + add_indexes(Name, Indexes); + Err -> + Err + end; +add_indexes(_Name, []) -> + {atomic, ok}. + +% +% utilities +% + +schema(Name, Default, Schema) -> + case lists:keyfind(Name, 1, Schema) of + {_, Custom} -> + TabDefs = merge(Custom, Default), + ?DEBUG("Using custom schema for table '~ts': ~p", + [Name, TabDefs]), + TabDefs; + false -> + Default + end. + +-spec read_schema_file() -> [{atom(), custom_schema()}]. +read_schema_file() -> + File = schema_path(), + case fast_yaml:decode_from_file(File, [plain_as_atom]) of + {ok, Y} -> + case econf:validate(validator(), lists:flatten(Y)) of + {ok, []} -> + ?WARNING_MSG("Mnesia schema file ~ts is empty", [File]), + []; + {ok, Config} -> + lists:map( + fun({Tab, Opts}) -> + {Tab, lists:map( + fun({storage_type, T}) -> {T, [node()]}; + (Other) -> Other + end, Opts)} + end, Config); + {error, Reason, Ctx} -> + ?ERROR_MSG("Failed to read Mnesia schema from ~ts: ~ts", + [File, econf:format_error(Reason, Ctx)]), + [] + end; + {error, enoent} -> + ?DEBUG("No custom Mnesia schema file found at ~ts", [File]), + []; + {error, Reason} -> + ?ERROR_MSG("Failed to read Mnesia schema file ~ts: ~ts", + [File, fast_yaml:format_error(Reason)]) + end. + +-spec validator() -> econf:validator(). +validator() -> + econf:map( + econf:atom(), + econf:options( + #{storage_type => econf:enum([ram_copies, disc_copies, disc_only_copies]), + local_content => econf:bool(), + type => econf:enum([set, ordered_set, bag]), + attributes => econf:list(econf:atom()), + index => econf:list(econf:atom())}, + [{return, orddict}, unique]), + [unique]). + +create(Name, TabDef) -> + Type = lists:foldl( + fun({ram_copies, _}, _) -> " ram "; + ({disc_copies, _}, _) -> " disc "; + ({disc_only_copies, _}, _) -> " disc_only "; + (_, Acc) -> Acc + end, " ", TabDef), + ?INFO_MSG("Creating Mnesia~tstable '~ts'", [Type, Name]), + case mnesia_op(create_table, [Name, TabDef]) of + {atomic, ok} -> + add_table_copy(Name); + Err -> + Err + end. + +%% The table MUST exist, otherwise the function would fail +add_table_copy(Name) -> + Type = mnesia:table_info(Name, storage_type), + Nodes = mnesia:table_info(Name, Type), + case lists:member(node(), Nodes) of + true -> + {atomic, ok}; + false -> + mnesia_op(add_table_copy, [Name, node(), Type]) + end. + +merge(Custom, Default) -> + NewDefault = case lists:any(fun is_storage_type_option/1, Custom) of + true -> + lists:filter( + fun(O) -> + not is_storage_type_option(O) + end, Default); + false -> + Default + end, + lists:ukeymerge(1, Custom, lists:ukeysort(1, NewDefault)). + +need_reset(Table, TabDef) -> + ValuesF = [mnesia:table_info(Table, Key) || Key <- ?NEED_RESET], + ValuesT = [proplists:get_value(Key, TabDef) || Key <- ?NEED_RESET], + lists:foldl( + fun({Val, Val}, Acc) -> Acc; + ({_, undefined}, Acc) -> Acc; + ({_, _}, _) -> true + end, false, lists:zip(ValuesF, ValuesT)). + +transform(Module, Name) -> + try mnesia:table_info(Name, attributes) of + Attrs -> + transform(Module, Name, Attrs, Attrs) + catch _:{aborted, _} = Err -> + Err + end. + +transform(Module, Name, NewAttrs) -> + try mnesia:table_info(Name, attributes) of + OldAttrs -> + transform(Module, Name, OldAttrs, NewAttrs) + catch _:{aborted, _} = Err -> + Err + end. + +transform(Module, Name, Attrs, Attrs) -> + case need_transform(Module, Name) of + true -> + ?INFO_MSG("Transforming table '~ts', this may take a while", [Name]), + transform_table(Module, Name); + false -> + {atomic, ok} + end; +transform(Module, Name, OldAttrs, NewAttrs) -> + Fun = case erlang:function_exported(Module, transform, 1) of + true -> transform_fun(Module, Name); + false -> fun(Old) -> do_transform(OldAttrs, NewAttrs, Old) end + end, + mnesia_op(transform_table, [Name, Fun, NewAttrs]). + +-spec need_transform(module(), atom()) -> boolean(). +need_transform(Module, Name) -> + case erlang:function_exported(Module, need_transform, 1) of + true -> + do_need_transform(Module, Name, mnesia:dirty_first(Name)); + false -> + false + end. + +do_need_transform(_Module, _Name, '$end_of_table') -> + false; +do_need_transform(Module, Name, Key) -> + Objs = mnesia:dirty_read(Name, Key), + case lists:foldl( + fun(_, true) -> true; + (Obj, _) -> Module:need_transform(Obj) + end, undefined, Objs) of + true -> true; + false -> false; + _ -> + do_need_transform(Module, Name, mnesia:dirty_next(Name, Key)) + end. + +do_transform(OldAttrs, Attrs, Old) -> + [Name|OldValues] = tuple_to_list(Old), + Before = lists:zip(OldAttrs, OldValues), + After = lists:foldl( + fun(Attr, Acc) -> + case lists:keyfind(Attr, 1, Before) of + false -> [{Attr, undefined}|Acc]; + Value -> [Value|Acc] + end + end, [], lists:reverse(Attrs)), + {Attrs, NewRecord} = lists:unzip(After), + list_to_tuple([Name|NewRecord]). + +transform_fun(Module, Name) -> + fun(Obj) -> + try Module:transform(Obj) + catch + Class:Reason:StackTrace -> + ?ERROR_MSG("Failed to transform Mnesia table ~ts:~n" + "** Record: ~p~n" + "** ~ts", + [Name, + Obj, + misc:format_exception(2, Class, Reason, StackTrace)]), + erlang:raise(Class, Reason, StackTrace) + end + end. + +transform_table(Module, Name) -> + Type = mnesia:table_info(Name, type), + Attrs = mnesia:table_info(Name, attributes), + TmpTab = list_to_atom(atom_to_list(Name) ++ "_backup"), + StorageType = if Type == ordered_set -> disc_copies; + true -> disc_only_copies + end, + mnesia:create_table(TmpTab, + [{StorageType, [node()]}, + {type, Type}, + {local_content, true}, + {record_name, Name}, + {attributes, Attrs}]), + mnesia:clear_table(TmpTab), + Fun = transform_fun(Module, Name), + Res = mnesia_op( + transaction, + [fun() -> do_transform_table(Name, Fun, TmpTab, mnesia:first(Name)) end]), + mnesia:delete_table(TmpTab), + Res. + +do_transform_table(Name, _Fun, TmpTab, '$end_of_table') -> + mnesia:foldl( + fun(Obj, _) -> + mnesia:write(Name, Obj, write) + end, ok, TmpTab); +do_transform_table(Name, Fun, TmpTab, Key) -> + Next = mnesia:next(Name, Key), + Objs = mnesia:read(Name, Key), + lists:foreach( + fun(Obj) -> + mnesia:write(TmpTab, Fun(Obj), write), + mnesia:delete_object(Obj) + end, Objs), + do_transform_table(Name, Fun, TmpTab, Next). + +mnesia_op(Fun, Args) -> + case apply(mnesia, Fun, Args) of + {atomic, ok} -> + {atomic, ok}; + Other -> + ?ERROR_MSG("Failure on mnesia ~ts ~p: ~p", + [Fun, Args, Other]), + Other + end. + +schema_path() -> + Dir = case os:getenv("EJABBERD_MNESIA_SCHEMA") of + false -> mnesia:system_info(directory); + Path -> Path + end, + filename:join(Dir, "ejabberd.schema"). + +is_storage_type_option({O, _}) -> + O == ram_copies orelse O == disc_copies orelse O == disc_only_copies. + +dump_schema() -> + File = schema_path(), + Schema = lists:flatmap( + fun(schema) -> + []; + (Tab) -> + [{Tab, [{storage_type, + mnesia:table_info(Tab, storage_type)}, + {local_content, + mnesia:table_info(Tab, local_content)}]}] + end, mnesia:system_info(tables)), + case file:write_file(File, [fast_yaml:encode(Schema), io_lib:nl()]) of + ok -> + io:format("Mnesia schema is written to ~ts~n", [File]); + {error, Reason} -> + io:format("Failed to write Mnesia schema to ~ts: ~ts", + [File, file:format_error(Reason)]) + end. diff --git a/src/ejabberd_node_groups.erl b/src/ejabberd_node_groups.erl deleted file mode 100644 index da0bffe99..000000000 --- a/src/ejabberd_node_groups.erl +++ /dev/null @@ -1,165 +0,0 @@ -%%%---------------------------------------------------------------------- -%%% File : ejabberd_node_groups.erl -%%% Author : Alexey Shchepin -%%% Purpose : Distributed named node groups based on pg2 module -%%% Created : 1 Nov 2006 by Alexey Shchepin -%%% -%%% -%%% ejabberd, Copyright (C) 2002-2015 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(ejabberd_node_groups). --author('alexey@process-one.net'). - --behaviour(gen_server). - -%% API --export([start_link/0, - join/1, - leave/1, - get_members/1, - get_closest_node/1]). - -%% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). - --define(PG2, pg2). - --record(state, {}). - -%%==================================================================== -%% API -%%==================================================================== -%%-------------------------------------------------------------------- -%% Function: start_link() -> {ok,Pid} | ignore | {error,Error} -%% Description: Starts the server -%%-------------------------------------------------------------------- -start_link() -> - gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). - -join(Name) -> - PG = {?MODULE, Name}, - pg2:create(PG), - pg2:join(PG, whereis(?MODULE)). - -leave(Name) -> - PG = {?MODULE, Name}, - pg2:leave(PG, whereis(?MODULE)). - -get_members(Name) -> - PG = {?MODULE, Name}, - [node(P) || P <- pg2:get_members(PG)]. - -get_closest_node(Name) -> - PG = {?MODULE, Name}, - node(pg2:get_closest_pid(PG)). - -%%==================================================================== -%% gen_server callbacks -%%==================================================================== - -%%-------------------------------------------------------------------- -%% Function: init(Args) -> {ok, State} | -%% {ok, State, Timeout} | -%% ignore | -%% {stop, Reason} -%% Description: Initiates the server -%%-------------------------------------------------------------------- -init([]) -> - {FE, BE} = - case ejabberd_config:get_option( - node_type, - fun(frontend) -> frontend; - (backend) -> backend; - (generic) -> generic - end, generic) of - frontend -> - {true, false}; - backend -> - {false, true}; - generic -> - {true, true}; - undefined -> - {true, true} - end, - if - FE -> - join(frontend); - true -> - ok - end, - if - BE -> - join(backend); - true -> - ok - end, - {ok, #state{}}. - -%%-------------------------------------------------------------------- -%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | -%% {reply, Reply, State, Timeout} | -%% {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, Reply, State} | -%% {stop, Reason, State} -%% Description: Handling call messages -%%-------------------------------------------------------------------- -handle_call(_Request, _From, State) -> - Reply = ok, - {reply, Reply, State}. - -%%-------------------------------------------------------------------- -%% Function: handle_cast(Msg, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} -%% Description: Handling cast messages -%%-------------------------------------------------------------------- -handle_cast(_Msg, State) -> - {noreply, State}. - -%%-------------------------------------------------------------------- -%% Function: handle_info(Info, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} -%% Description: Handling all non call/cast messages -%%-------------------------------------------------------------------- -handle_info(_Info, State) -> - {noreply, State}. - -%%-------------------------------------------------------------------- -%% Function: terminate(Reason, State) -> void() -%% Description: This function is called by a gen_server when it is about to -%% terminate. It should be the opposite of Module:init/1 and do any necessary -%% cleaning up. When it returns, the gen_server terminates with Reason. -%% The return value is ignored. -%%-------------------------------------------------------------------- -terminate(_Reason, _State) -> - ok. - -%%-------------------------------------------------------------------- -%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} -%% Description: Convert process state when code is changed -%%-------------------------------------------------------------------- -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -%%-------------------------------------------------------------------- -%%% Internal functions -%%-------------------------------------------------------------------- diff --git a/src/ejabberd_oauth.erl b/src/ejabberd_oauth.erl new file mode 100644 index 000000000..a18596d46 --- /dev/null +++ b/src/ejabberd_oauth.erl @@ -0,0 +1,844 @@ +%%%------------------------------------------------------------------- +%%% File : ejabberd_oauth.erl +%%% Author : Alexey Shchepin +%%% Purpose : OAUTH2 support +%%% Created : 20 Mar 2015 by Alexey Shchepin +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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., 59 Temple Place, Suite 330, Boston, MA +%%% 02111-1307 USA +%%% +%%%------------------------------------------------------------------- + +-module(ejabberd_oauth). + +-behaviour(gen_server). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, + handle_info/2, terminate/2, code_change/3]). + +-export([start_link/0, + get_client_identity/2, + verify_redirection_uri/3, + authenticate_user/2, + authenticate_client/2, + associate_access_code/3, + associate_access_token/3, + associate_refresh_token/3, + check_token/1, + check_token/4, + check_token/2, + scope_in_scope_list/2, + process/2, + config_reloaded/0, + verify_resowner_scope/3]). + +-export([get_commands_spec/0, + oauth_issue_token/3, oauth_list_tokens/0, oauth_revoke_token/1, + oauth_add_client_password/3, + oauth_add_client_implicit/3, + oauth_remove_client/1]). + +-export([web_menu_main/2, web_page_main/2]). + +-include_lib("xmpp/include/xmpp.hrl"). +-include("logger.hrl"). +-include("ejabberd_http.hrl"). +-include("ejabberd_web_admin.hrl"). +-include("ejabberd_oauth.hrl"). +-include("ejabberd_commands.hrl"). +-include("translate.hrl"). + +-callback init() -> any(). +-callback store(#oauth_token{}) -> ok | {error, any()}. +-callback lookup(binary()) -> {ok, #oauth_token{}} | error. +-callback revoke(binary()) -> ok | {error, binary()}. +-callback clean(non_neg_integer()) -> any(). + +-record(oauth_ctx, { + password :: binary() | admin_generated, + client :: #oauth_client{} | undefined + }). + +%% There are two ways to obtain an oauth token: +%% * Using the web form/api results in the token being generated in behalf of the user providing the user/pass +%% * Using the command line and oauth_issue_token command, the token is generated in behalf of ejabberd' sysadmin +%% (as it has access to ejabberd command line). + +get_commands_spec() -> + [ + #ejabberd_commands{name = oauth_issue_token, tags = [oauth], + desc = "Issue an OAuth token for the given jid", + module = ?MODULE, function = oauth_issue_token, + args = [{jid, string},{ttl, integer}, {scopes, string}], + policy = restricted, + args_example = ["user@server.com", 3600, "connected_users_number;muc_online_rooms"], + args_desc = ["Jid for which issue token", + "Time to live of generated token in seconds", + "List of scopes to allow, separated by ';'"], + result = {result, {tuple, [{token, string}, {scopes, string}, {expires_in, string}]}} + }, + #ejabberd_commands{name = oauth_issue_token, tags = [oauth], + desc = "Issue an OAuth token for the given jid", + module = ?MODULE, function = oauth_issue_token, + version = 1, + note = "updated in 24.02", + args = [{jid, string}, {ttl, integer}, {scopes, {list, {scope, binary}}}], + policy = restricted, + args_example = ["user@server.com", 3600, ["connected_users_number", "muc_online_rooms"]], + args_desc = ["Jid for which issue token", + "Time to live of generated token in seconds", + "List of scopes to allow"], + result = {result, {tuple, [{token, string}, {scopes, {list, {scope, string}}}, {expires_in, string}]}} + }, + #ejabberd_commands{name = oauth_list_tokens, tags = [oauth], + desc = "List OAuth tokens, user, scope, and seconds to expire (only Mnesia)", + longdesc = "List _`oauth.md|OAuth`_ tokens, their user and scope, and how many seconds remain until expiry", + module = ?MODULE, function = oauth_list_tokens, + args = [], + policy = restricted, + result = {tokens, {list, {token, {tuple, [{token, string}, {user, string}, {scope, string}, {expires_in, string}]}}}} + }, + #ejabberd_commands{name = oauth_revoke_token, tags = [oauth], + desc = "Revoke authorization for an OAuth token", + note = "changed in 22.05", + module = ?MODULE, function = oauth_revoke_token, + args = [{token, binary}], + policy = restricted, + result = {res, restuple}, + result_desc = "Result code" + }, + #ejabberd_commands{name = oauth_add_client_password, tags = [oauth], + desc = "Add OAuth client_id with password grant type", + module = ?MODULE, function = oauth_add_client_password, + args = [{client_id, binary}, + {client_name, binary}, + {secret, binary}], + policy = restricted, + result = {res, restuple} + }, + #ejabberd_commands{name = oauth_add_client_implicit, tags = [oauth], + desc = "Add OAuth client_id with implicit grant type", + module = ?MODULE, function = oauth_add_client_implicit, + args = [{client_id, binary}, + {client_name, binary}, + {redirect_uri, binary}], + policy = restricted, + result = {res, restuple} + }, + #ejabberd_commands{name = oauth_remove_client, tags = [oauth], + desc = "Remove OAuth client_id", + module = ?MODULE, function = oauth_remove_client, + args = [{client_id, binary}], + policy = restricted, + result = {res, restuple} + } + ]. + +oauth_issue_token(Jid, TTLSeconds, [Head|_] = ScopesString) when is_integer(Head) -> + Scopes = [list_to_binary(Scope) || Scope <- string:tokens(ScopesString, ";")], + oauth_issue_token(Jid, TTLSeconds, Scopes); +oauth_issue_token(Jid, TTLSeconds, Scopes) -> + try jid:decode(list_to_binary(Jid)) of + #jid{luser =Username, lserver = Server} -> + Ctx1 = #oauth_ctx{password = admin_generated}, + case oauth2:authorize_password({Username, Server}, Scopes, Ctx1) of + {ok, {_Ctx,Authorization}} -> + {ok, {_AppCtx2, Response}} = oauth2:issue_token(Authorization, [{expiry_time, TTLSeconds}]), + {ok, AccessToken} = oauth2_response:access_token(Response), + {ok, VerifiedScope} = oauth2_response:scope(Response), + {AccessToken, VerifiedScope, integer_to_list(TTLSeconds) ++ " seconds"}; + {error, Error} -> + {error, Error} + end + catch _:{bad_jid, _} -> + {error, "Invalid JID: " ++ Jid} + end. + +oauth_list_tokens() -> + Tokens = mnesia:dirty_match_object(#oauth_token{_ = '_'}), + {MegaSecs, Secs, _MiniSecs} = os:timestamp(), + TS = 1000000 * MegaSecs + Secs, + [{Token, jid:encode(jid:make(U,S)), Scope, integer_to_list(Expires - TS) ++ " seconds"} || + #oauth_token{token=Token, scope=Scope, us= {U,S},expire=Expires} <- Tokens]. + + +oauth_revoke_token(Token) -> + DBMod = get_db_backend(), + case DBMod:revoke(Token) of + ok -> + ets_cache:delete(oauth_cache, Token, + ejabberd_cluster:get_nodes()), + {ok, ""}; + Other -> + Other + end. + +oauth_add_client_password(ClientID, ClientName, Secret) -> + DBMod = get_db_backend(), + DBMod:store_client(#oauth_client{client_id = ClientID, + client_name = ClientName, + grant_type = password, + options = [{secret, Secret}]}), + {ok, []}. + +oauth_add_client_implicit(ClientID, ClientName, RedirectURI) -> + DBMod = get_db_backend(), + DBMod:store_client(#oauth_client{client_id = ClientID, + client_name = ClientName, + grant_type = implicit, + options = [{redirect_uri, RedirectURI}]}), + {ok, []}. + +oauth_remove_client(Client) -> + DBMod = get_db_backend(), + DBMod:remove_client(Client), + {ok, []}. + +config_reloaded() -> + DBMod = get_db_backend(), + case init_cache(DBMod) of + true -> + ets_cache:setopts(oauth_cache, cache_opts()); + false -> + ok + end. + +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + + +init([]) -> + DBMod = get_db_backend(), + DBMod:init(), + init_cache(DBMod), + Expire = expire(), + application:set_env(oauth2, backend, ejabberd_oauth), + application:set_env(oauth2, expiry_time, Expire div 1000), + application:start(oauth2), + ejabberd_commands:register_commands(get_commands_spec()), + ejabberd_hooks:add(webadmin_menu_main, ?MODULE, web_menu_main, 50), + ejabberd_hooks:add(webadmin_page_main, ?MODULE, web_page_main, 50), + ejabberd_hooks:add(config_reloaded, ?MODULE, config_reloaded, 50), + erlang:send_after(expire(), self(), clean), + {ok, ok}. + +handle_call(Request, From, State) -> + ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), + {noreply, State}. + +handle_cast(Msg, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Msg]), + {noreply, State}. + +handle_info(clean, State) -> + {MegaSecs, Secs, MiniSecs} = os:timestamp(), + TS = 1000000 * MegaSecs + Secs, + DBMod = get_db_backend(), + DBMod:clean(TS), + erlang:send_after(trunc(expire() * (1 + MiniSecs / 1000000)), + self(), clean), + {noreply, State}; +handle_info(Info, State) -> + ?WARNING_MSG("Unexpected info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, _State) -> + ejabberd_hooks:delete(webadmin_menu_main, ?MODULE, web_menu_main, 50), + ejabberd_hooks:delete(webadmin_page_main, ?MODULE, web_page_main, 50), + ejabberd_hooks:delete(config_reloaded, ?MODULE, config_reloaded, 50). + +code_change(_OldVsn, State, _Extra) -> {ok, State}. + +get_client_identity(<<"">>, Ctx) -> + {ok, {Ctx, {client, unknown_client}}}; +get_client_identity(ClientID, Ctx) when is_binary(ClientID) -> + {ok, {Ctx, {client, ClientID}}}. + +verify_redirection_uri(_ClientID, RedirectURI, Ctx) -> + case Ctx of + #oauth_ctx{client = #oauth_client{grant_type = implicit} = Client} -> + case get_redirect_uri(Client) of + RedirectURI -> + {ok, Ctx}; + _ -> + {error, invalid_uri} + end; + #oauth_ctx{client = #oauth_client{}} -> + {error, invalid_client}; + _ -> + {ok, Ctx} + end. + +authenticate_user({User, Server}, Ctx) -> + case jid:make(User, Server) of + #jid{} = JID -> + Access = + ejabberd_option:oauth_access(JID#jid.lserver), + case acl:match_rule(JID#jid.lserver, Access, JID) of + allow -> + case Ctx of + #oauth_ctx{password = admin_generated} -> + {ok, {Ctx, {user, User, Server}}}; + #oauth_ctx{password = Password} + when is_binary(Password) -> + case ejabberd_auth:check_password(User, <<"">>, Server, Password) of + true -> + {ok, {Ctx, {user, User, Server}}}; + false -> + {error, badpass} + end + end; + deny -> + {error, badpass} + end; + error -> + {error, badpass} + end. + +authenticate_client(ClientID, Ctx) -> + case ejabberd_option:oauth_client_id_check() of + allow -> + {ok, {Ctx, {client, ClientID}}}; + deny -> {error, not_allowed}; + db -> + DBMod = get_db_backend(), + case DBMod:lookup_client(ClientID) of + {ok, #oauth_client{} = Client} -> + {ok, {Ctx#oauth_ctx{client = Client}, {client, ClientID}}}; + _ -> + {error, not_allowed} + end + end. + +-spec verify_resowner_scope({user, binary(), binary()}, [binary()], any()) -> + {ok, any(), [binary()]} | {error, any()}. +verify_resowner_scope({user, _User, _Server}, Scope, Ctx) -> + Cmds = [atom_to_binary(Name, utf8) || {Name, _, _} <- ejabberd_commands:list_commands()], + AllowedScopes = [<<"ejabberd:user">>, <<"ejabberd:admin">>, <<"sasl_auth">>] ++ Cmds, + case oauth2_priv_set:is_subset(oauth2_priv_set:new(Scope), + oauth2_priv_set:new(AllowedScopes)) of + true -> + {ok, {Ctx, Scope}}; + false -> + {error, badscope} + end; +verify_resowner_scope(_, _, _) -> + {error, badscope}. + +%% This is callback for oauth tokens generated through the command line. Only open and admin commands are +%% made available. +%verify_client_scope({client, ejabberd_ctl}, Scope, Ctx) -> +% RegisteredScope = dict:fetch_keys(get_cmd_scopes()), +% case oauth2_priv_set:is_subset(oauth2_priv_set:new(Scope), +% oauth2_priv_set:new(RegisteredScope)) of +% true -> +% {ok, {Ctx, Scope}}; +% false -> +% {error, badscope} +% end. + + + + +-spec seconds_since_epoch(integer()) -> non_neg_integer(). +seconds_since_epoch(Diff) -> + {Mega, Secs, _} = os:timestamp(), + Mega * 1000000 + Secs + Diff. + + +associate_access_code(_AccessCode, _Context, AppContext) -> + %put(?ACCESS_CODE_TABLE, AccessCode, Context), + {ok, AppContext}. + +associate_access_token(AccessToken, Context, AppContext) -> + {user, User, Server} = proplists:get_value(<<"resource_owner">>, Context, <<"">>), + Expire = case proplists:get_value(expiry_time, AppContext, undefined) of + undefined -> + proplists:get_value(<<"expiry_time">>, Context, 0); + ExpiresIn -> + %% There is no clean way in oauth2 lib to actually override the TTL of the generated token. + %% It always pass the global configured value. Here we use the app context to pass the per-case + %% ttl if we want to override it. + seconds_since_epoch(ExpiresIn) + end, + {user, User, Server} = proplists:get_value(<<"resource_owner">>, Context, <<"">>), + Scope = proplists:get_value(<<"scope">>, Context, []), + R = #oauth_token{ + token = AccessToken, + us = {jid:nodeprep(User), jid:nodeprep(Server)}, + scope = Scope, + expire = Expire + }, + store(R), + {ok, AppContext}. + +associate_refresh_token(_RefreshToken, _Context, AppContext) -> + %put(?REFRESH_TOKEN_TABLE, RefreshToken, Context), + {ok, AppContext}. + +scope_in_scope_list(Scope, ScopeList) -> + TokenScopeSet = oauth2_priv_set:new(Scope), + lists:any(fun(Scope2) -> + oauth2_priv_set:is_member(Scope2, TokenScopeSet) end, + ScopeList). + +-spec check_token(binary()) -> {ok, {binary(), binary()}, [binary()]} | + {false, expired | not_found}. +check_token(Token) -> + case lookup(Token) of + {ok, #oauth_token{us = US, + scope = TokenScope, + expire = Expire}} -> + {MegaSecs, Secs, _} = os:timestamp(), + TS = 1000000 * MegaSecs + Secs, + if + Expire > TS -> + {ok, US, TokenScope}; + true -> + {false, expired} + end; + _ -> + {false, not_found} + end. + +check_token(User, Server, ScopeList, Token) -> + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + case lookup(Token) of + {ok, #oauth_token{us = {LUser, LServer}, + scope = TokenScope, + expire = Expire}} -> + {MegaSecs, Secs, _} = os:timestamp(), + TS = 1000000 * MegaSecs + Secs, + if + Expire > TS -> + TokenScopeSet = oauth2_priv_set:new(TokenScope), + lists:any(fun(Scope) -> + oauth2_priv_set:is_member(Scope, TokenScopeSet) end, + ScopeList); + true -> + {false, expired} + end; + _ -> + {false, not_found} + end. + +check_token(ScopeList, Token) -> + case lookup(Token) of + {ok, #oauth_token{us = US, + scope = TokenScope, + expire = Expire}} -> + {MegaSecs, Secs, _} = os:timestamp(), + TS = 1000000 * MegaSecs + Secs, + if + Expire > TS -> + TokenScopeSet = oauth2_priv_set:new(TokenScope), + case lists:any(fun(Scope) -> + oauth2_priv_set:is_member(Scope, TokenScopeSet) end, + ScopeList) of + true -> {ok, user, US}; + false -> {false, no_matching_scope} + end; + true -> + {false, expired} + end; + _ -> + {false, not_found} + end. + + +store(R) -> + DBMod = get_db_backend(), + case DBMod:store(R) of + ok -> + ets_cache:delete(oauth_cache, R#oauth_token.token, + ejabberd_cluster:get_nodes()); + {error, _} = Err -> + Err + end. + +lookup(Token) -> + ets_cache:lookup(oauth_cache, Token, + fun() -> + DBMod = get_db_backend(), + DBMod:lookup(Token) + end). + +-spec init_cache(module()) -> boolean(). +init_cache(DBMod) -> + UseCache = use_cache(DBMod), + case UseCache of + true -> + ets_cache:new(oauth_cache, cache_opts()); + false -> + ets_cache:delete(oauth_cache) + end, + UseCache. + +use_cache(DBMod) -> + case erlang:function_exported(DBMod, use_cache, 0) of + true -> DBMod:use_cache(); + false -> ejabberd_option:oauth_use_cache() + end. + +cache_opts() -> + MaxSize = ejabberd_option:oauth_cache_size(), + CacheMissed = ejabberd_option:oauth_cache_missed(), + LifeTime = ejabberd_option:oauth_cache_life_time(), + [{max_size, MaxSize}, {life_time, LifeTime}, {cache_missed, CacheMissed}]. + +expire() -> + ejabberd_option:oauth_expire(). + +-define(DIV(Class, Els), + ?XAE(<<"div">>, [{<<"class">>, Class}], Els)). +-define(INPUTID(Type, Name, Value), + ?XA(<<"input">>, + [{<<"type">>, Type}, {<<"name">>, Name}, + {<<"value">>, Value}, {<<"id">>, Name}])). +-define(LABEL(ID, Els), + ?XAE(<<"label">>, [{<<"for">>, ID}], Els)). + +process(_Handlers, + #request{method = 'GET', q = Q, lang = Lang, + path = [_, <<"authorization_token">>]}) -> + ResponseType = proplists:get_value(<<"response_type">>, Q, <<"">>), + ClientId = proplists:get_value(<<"client_id">>, Q, <<"">>), + JidEls = case proplists:get_value(<<"jid">>, Q, <<"">>) of + <<"">> -> [?INPUTID(<<"email">>, <<"username">>, <<"">>)]; + Jid -> [?C(Jid), ?INPUT(<<"hidden">>, <<"username">>, Jid)] + end, + RedirectURI = proplists:get_value(<<"redirect_uri">>, Q, <<"">>), + Scope = proplists:get_value(<<"scope">>, Q, <<"">>), + State = proplists:get_value(<<"state">>, Q, <<"">>), + Form = + ?XAE(<<"form">>, + [{<<"action">>, <<"authorization_token">>}, + {<<"method">>, <<"post">>}], + [?LABEL(<<"username">>, [?CT(?T("User (jid)")), ?C(<<": ">>)]) + ] ++ JidEls ++ [ + ?BR, + ?LABEL(<<"password">>, [?CT(?T("Password")), ?C(<<": ">>)]), + ?INPUTID(<<"password">>, <<"password">>, <<"">>), + ?INPUT(<<"hidden">>, <<"response_type">>, ResponseType), + ?INPUT(<<"hidden">>, <<"client_id">>, ClientId), + ?INPUT(<<"hidden">>, <<"redirect_uri">>, RedirectURI), + ?INPUT(<<"hidden">>, <<"scope">>, Scope), + ?INPUT(<<"hidden">>, <<"state">>, State), + ?BR, + ?LABEL(<<"ttl">>, [?CT(?T("Token TTL")), ?C(<<": ">>)]), + ?XAE(<<"select">>, [{<<"name">>, <<"ttl">>}], + [ + ?XAC(<<"option">>, [{<<"value">>, <<"3600">>}],<<"1 Hour">>), + ?XAC(<<"option">>, [{<<"value">>, <<"86400">>}],<<"1 Day">>), + ?XAC(<<"option">>, [{<<"value">>, <<"2592000">>}],<<"1 Month">>), + ?XAC(<<"option">>, [{<<"selected">>, <<"selected">>},{<<"value">>, <<"31536000">>}],<<"1 Year">>), + ?XAC(<<"option">>, [{<<"value">>, <<"315360000">>}],<<"10 Years">>)]), + ?BR, + ?INPUTT(<<"submit">>, <<"">>, ?T("Accept")) + ]), + Top = + ?DIV(<<"section">>, + [?DIV(<<"block">>, + [?A(<<"https://www.ejabberd.im">>, + [?XA(<<"img">>, + [{<<"height">>, <<"32">>}, + {<<"src">>, logo()}])] + )])]), + Middle = + ?DIV(<<"white section">>, + [?DIV(<<"block">>, + [?XC(<<"h1">>, <<"Authorization request">>), + ?XE(<<"p">>, + [?C(<<"Application ">>), + ?XC(<<"em">>, ClientId), + ?C(<<" wants to access scope ">>), + ?XC(<<"em">>, Scope)]), + Form + ])]), + Bottom = + ?DIV(<<"section">>, + [?DIV(<<"block">>, + [?XAC(<<"a">>, + [{<<"href">>, <<"https://www.ejabberd.im">>}, + {<<"title">>, <<"ejabberd XMPP server">>}], + <<"ejabberd">>), + ?C(<<" is maintained by ">>), + ?XAC(<<"a">>, + [{<<"href">>, <<"https://www.process-one.net">>}, + {<<"title">>, <<"ProcessOne - Leader in Instant Messaging and Push Solutions">>}], + <<"ProcessOne">>) + ])]), + Body = ?DIV(<<"container">>, [Top, Middle, Bottom]), + ejabberd_web:make_xhtml(web_head(), [Body]); +process(_Handlers, + #request{method = 'POST', q = Q, lang = _Lang, + path = [_, <<"authorization_token">>]}) -> + _ResponseType = proplists:get_value(<<"response_type">>, Q, <<"">>), + ClientId = proplists:get_value(<<"client_id">>, Q, <<"">>), + RedirectURI = proplists:get_value(<<"redirect_uri">>, Q, <<"">>), + SScope = proplists:get_value(<<"scope">>, Q, <<"">>), + StringJID = proplists:get_value(<<"username">>, Q, <<"">>), + try jid:decode(StringJID) of + #jid{user = Username, server = Server} -> + Password = proplists:get_value(<<"password">>, Q, <<"">>), + State = proplists:get_value(<<"state">>, Q, <<"">>), + Scope = str:tokens(SScope, <<" ">>), + TTL = proplists:get_value(<<"ttl">>, Q, <<"">>), + ExpiresIn = case TTL of + <<>> -> undefined; + _ -> binary_to_integer(TTL) + end, + case oauth2:authorize_password({Username, Server}, + ClientId, + RedirectURI, + Scope, + #oauth_ctx{password = Password}) of + {ok, {_AppContext, Authorization}} -> + {ok, {_AppContext2, Response}} = + oauth2:issue_token(Authorization, [{expiry_time, ExpiresIn} || ExpiresIn /= undefined]), + {ok, AccessToken} = oauth2_response:access_token(Response), + {ok, Type} = oauth2_response:token_type(Response), + %%Ugly: workardound to return the correct expirity time, given than oauth2 lib doesn't really have + %%per-case expirity time. + Expires = case ExpiresIn of + undefined -> + {ok, Ex} = oauth2_response:expires_in(Response), + Ex; + _ -> + ExpiresIn + end, + {ok, VerifiedScope} = oauth2_response:scope(Response), + %oauth2_wrq:redirected_access_token_response(ReqData, + % RedirectURI, + % AccessToken, + % Type, + % Expires, + % VerifiedScope, + % State, + % Context); + {302, [{<<"Location">>, + <>))/binary, + "&state=", State/binary>> + }], + ejabberd_web:make_xhtml([?XC(<<"h1">>, <<"302 Found">>)])}; + {error, Error} when is_atom(Error) -> + %oauth2_wrq:redirected_error_response( + % ReqData, RedirectURI, Error, State, Context) + {302, [{<<"Location">>, + <>, <<"302 Found">>)])} + end + catch _:{bad_jid, _} -> + State = proplists:get_value(<<"state">>, Q, <<"">>), + {400, [{<<"Location">>, + <>, <<"400 Invalid request">>)])} + end; +process(_Handlers, + #request{method = 'POST', q = Q, lang = _Lang, + auth = HTTPAuth, + path = [_, <<"token">>]}) -> + Access = + case ejabberd_option:oauth_client_id_check() of + allow -> + case proplists:get_value(<<"grant_type">>, Q, <<"">>) of + <<"password">> -> + password; + _ -> + unsupported_grant_type + end; + deny -> + deny; + db -> + {ClientID, Secret} = + case HTTPAuth of + {ClientID1, Secret1} -> + {ClientID1, Secret1}; + _ -> + ClientID1 = proplists:get_value( + <<"client_id">>, Q, <<"">>), + Secret1 = proplists:get_value( + <<"client_secret">>, Q, <<"">>), + {ClientID1, Secret1} + end, + DBMod = get_db_backend(), + case DBMod:lookup_client(ClientID) of + {ok, #oauth_client{grant_type = password} = Client} -> + case get_client_secret(Client) of + Secret -> + case proplists:get_value(<<"grant_type">>, Q, <<"">>) of + <<"password">> when + Client#oauth_client.grant_type == password -> + password; + _ -> + unsupported_grant_type + end; + _ -> + deny + end; + _ -> + deny + end + end, + case Access of + password -> + SScope = proplists:get_value(<<"scope">>, Q, <<"">>), + StringJID = proplists:get_value(<<"username">>, Q, <<"">>), + try jid:decode(StringJID) of + #jid{user = Username, server = Server} -> + Password = proplists:get_value(<<"password">>, Q, <<"">>), + Scope = str:tokens(SScope, <<" ">>), + TTL = proplists:get_value(<<"ttl">>, Q, <<"">>), + ExpiresIn = case TTL of + <<>> -> undefined; + _ -> binary_to_integer(TTL) + end, + case oauth2:authorize_password({Username, Server}, + Scope, + #oauth_ctx{password = Password}) of + {ok, {_AppContext, Authorization}} -> + {ok, {_AppContext2, Response}} = + oauth2:issue_token(Authorization, [{expiry_time, ExpiresIn} || ExpiresIn /= undefined]), + {ok, AccessToken} = oauth2_response:access_token(Response), + {ok, Type} = oauth2_response:token_type(Response), + %%Ugly: workardound to return the correct expirity time, given than oauth2 lib doesn't really have + %%per-case expirity time. + Expires = case ExpiresIn of + undefined -> + {ok, Ex} = oauth2_response:expires_in(Response), + Ex; + _ -> + ExpiresIn + end, + {ok, VerifiedScope} = oauth2_response:scope(Response), + json_response(200, #{<<"access_token">> => AccessToken, + <<"token_type">> => Type, + <<"scope">> => str:join(VerifiedScope, <<" ">>), + <<"expires_in">> => Expires}); + {error, Error} when is_atom(Error) -> + json_error(400, <<"invalid_grant">>, Error) + end + catch _:{bad_jid, _} -> + json_error(400, <<"invalid_request">>, invalid_jid) + end; + unsupported_grant_type -> + json_error(400, <<"unsupported_grant_type">>, + unsupported_grant_type); + deny -> + ejabberd_web:error(not_allowed) + end; + +process(_Handlers, _Request) -> + ejabberd_web:error(not_found). + +-spec get_db_backend() -> module(). + +get_db_backend() -> + DBType = ejabberd_option:oauth_db_type(), + list_to_existing_atom("ejabberd_oauth_" ++ atom_to_list(DBType)). + +get_client_secret(#oauth_client{grant_type = password, options = Options}) -> + proplists:get_value(secret, Options, false). + +get_redirect_uri(#oauth_client{grant_type = implicit, options = Options}) -> + proplists:get_value(redirect_uri, Options, false). + +%% Headers as per RFC 6749 +json_response(Code, Body) -> + {Code, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}, + {<<"Cache-Control">>, <<"no-store">>}, + {<<"Pragma">>, <<"no-cache">>}], + misc:json_encode(Body)}. + +%% OAauth error are defined in: +%% https://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-5.2 +json_error(Code, Error, Reason) -> + Desc = json_error_desc(Reason), + Body = #{<<"error">> => Error, + <<"error_description">> => Desc}, + json_response(Code, Body). + +json_error_desc(access_denied) -> <<"Access denied">>; +json_error_desc(badpass) -> <<"Bad password">>; +json_error_desc(unsupported_grant_type) -> <<"Unsupported grant type">>; +json_error_desc(invalid_scope) -> <<"Invalid scope">>; +json_error_desc(invalid_jid) -> <<"Invalid JID">>. + +web_head() -> + [?XA(<<"meta">>, [{<<"http-equiv">>, <<"X-UA-Compatible">>}, + {<<"content">>, <<"IE=edge">>}]), + ?XA(<<"meta">>, [{<<"name">>, <<"viewport">>}, + {<<"content">>, + <<"width=device-width, initial-scale=1">>}]), + ?XC(<<"title">>, <<"Authorization request">>), + ?XC(<<"style">>, css()) + ]. + +css() -> + case misc:read_css("oauth.css") of + {ok, Data} -> Data; + {error, _} -> <<>> + end. + +logo() -> + case misc:read_img("oauth-logo.png") of + {ok, Img} -> + B64Img = base64:encode(Img), + <<"data:image/png;base64,", B64Img/binary>>; + {error, _} -> + <<>> + end. + +%%% +%%% WebAdmin +%%% + +%% @format-begin + +web_menu_main(Acc, _Lang) -> + Acc ++ [{<<"oauth">>, <<"OAuth">>}]. + +web_page_main(_, #request{path = [<<"oauth">>]} = R) -> + Head = ?H1GLraw(<<"OAuth">>, <<"developer/ejabberd-api/oauth/">>, <<"OAuth">>), + Set = [?X(<<"hr">>), + ?XAC(<<"h2">>, [{<<"id">>, <<"token">>}], <<"Token">>), + ?XE(<<"blockquote">>, + [ejabberd_web_admin:make_command(oauth_list_tokens, R), + ejabberd_web_admin:make_command(oauth_issue_token, R), + ejabberd_web_admin:make_command(oauth_revoke_token, R)]), + ?X(<<"hr">>), + ?XAC(<<"h2">>, [{<<"id">>, <<"client">>}], <<"Client">>), + ?XE(<<"blockquote">>, + [ejabberd_web_admin:make_command(oauth_add_client_implicit, R), + ejabberd_web_admin:make_command(oauth_add_client_password, R), + ejabberd_web_admin:make_command(oauth_remove_client, R)])], + {stop, Head ++ Set}; +web_page_main(Acc, _) -> + Acc. diff --git a/src/ejabberd_oauth_mnesia.erl b/src/ejabberd_oauth_mnesia.erl new file mode 100644 index 000000000..37fa3285c --- /dev/null +++ b/src/ejabberd_oauth_mnesia.erl @@ -0,0 +1,99 @@ +%%%------------------------------------------------------------------- +%%% File : ejabberd_oauth_mnesia.erl +%%% Author : Alexey Shchepin +%%% Purpose : OAUTH2 mnesia backend +%%% Created : 20 Jul 2016 by Alexey Shchepin +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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., 59 Temple Place, Suite 330, Boston, MA +%%% 02111-1307 USA +%%% +%%%------------------------------------------------------------------- + +-module(ejabberd_oauth_mnesia). +-behaviour(ejabberd_oauth). + +-export([init/0, + store/1, + lookup/1, + clean/1, + lookup_client/1, + store_client/1, + remove_client/1, + use_cache/0, revoke/1]). + +-include("ejabberd_oauth.hrl"). + +init() -> + ejabberd_mnesia:create(?MODULE, oauth_token, + [{disc_only_copies, [node()]}, + {attributes, + record_info(fields, oauth_token)}]), + ejabberd_mnesia:create(?MODULE, oauth_client, + [{disc_copies, [node()]}, + {attributes, + record_info(fields, oauth_client)}]), + ok. + +use_cache() -> + case mnesia:table_info(oauth_token, storage_type) of + disc_only_copies -> + ejabberd_option:oauth_use_cache(); + _ -> + false + end. + +store(R) -> + mnesia:dirty_write(R). + +lookup(Token) -> + case catch mnesia:dirty_read(oauth_token, Token) of + [R] -> + {ok, R}; + _ -> + error + end. + + +-spec revoke(binary()) -> ok | {error, binary()}. +revoke(Token) -> + mnesia:dirty_delete(oauth_token, Token). + +clean(TS) -> + F = fun() -> + Ts = mnesia:select( + oauth_token, + [{#oauth_token{expire = '$1', _ = '_'}, + [{'<', '$1', TS}], + ['$_']}]), + lists:foreach(fun mnesia:delete_object/1, Ts) + end, + mnesia:async_dirty(F). + +lookup_client(ClientID) -> + case catch mnesia:dirty_read(oauth_client, ClientID) of + [R] -> + {ok, R}; + _ -> + error + end. + +remove_client(ClientID) -> + mnesia:dirty_delete(oauth_client, ClientID). + +store_client(R) -> + mnesia:dirty_write(R). diff --git a/src/ejabberd_oauth_rest.erl b/src/ejabberd_oauth_rest.erl new file mode 100644 index 000000000..b7200872a --- /dev/null +++ b/src/ejabberd_oauth_rest.erl @@ -0,0 +1,174 @@ +%%%------------------------------------------------------------------- +%%% File : ejabberd_oauth_rest.erl +%%% Author : Alexey Shchepin +%%% Purpose : OAUTH2 REST backend +%%% Created : 26 Jul 2016 by Alexey Shchepin +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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., 59 Temple Place, Suite 330, Boston, MA +%%% 02111-1307 USA +%%% +%%%------------------------------------------------------------------- + +-module(ejabberd_oauth_rest). +-behaviour(ejabberd_oauth). + +-export([init/0, + store/1, + lookup/1, + clean/1, + lookup_client/1, + store_client/1, revoke/1]). + +-include("ejabberd_oauth.hrl"). +-include("logger.hrl"). +-include_lib("xmpp/include/jid.hrl"). + +init() -> + rest:start(ejabberd_config:get_myname()), + ok. + +store(R) -> + Path = path(<<"store">>), + %% Retry 2 times, with a backoff of 500millisec + {User, Server} = R#oauth_token.us, + SJID = jid:encode({User, Server, <<"">>}), + case rest:with_retry( + post, + [ejabberd_config:get_myname(), Path, [], + #{<<"token">> => R#oauth_token.token, + <<"user">> => SJID, + <<"scope">> => R#oauth_token.scope, + <<"expire">> => R#oauth_token.expire + }], 2, 500) of + {ok, Code, _} when Code == 200 orelse Code == 201 -> + ok; + Err -> + ?ERROR_MSG("Failed to store oauth record ~p: ~p", [R, Err]), + {error, db_failure} + end. + +lookup(Token) -> + Path = path(<<"lookup">>), + case rest:with_retry(post, [ejabberd_config:get_myname(), Path, [], + #{<<"token">> => Token}], + 2, 500) of + {ok, 200, Data} -> + SJID = case maps:find(<<"user">>, Data) of + {ok, U} -> U; + error -> <<>> + end, + JID = jid:decode(SJID), + US = {JID#jid.luser, JID#jid.lserver}, + Scope = case maps:find(<<"scope">>, Data) of + {ok, S} -> S; + error -> [] + end, + Expire = case maps:find(<<"expire">>, Data) of + {ok, E} -> E; + error -> 0 + end, + {ok, #oauth_token{token = Token, + us = US, + scope = Scope, + expire = Expire}}; + {ok, 404, _Resp} -> + error; + Other -> + ?ERROR_MSG("Unexpected response for oauth lookup: ~p", [Other]), + case ejabberd_option:oauth_cache_rest_failure_life_time() of + infinity -> error; + Time -> {cache_with_timeout, error, Time} + end + end. + +-spec revoke(binary()) -> ok | {error, binary()}. +revoke(_Token) -> + {error, <<"not available">>}. + +clean(_TS) -> + ok. + +path(Path) -> + Base = ejabberd_option:ext_api_path_oauth(), + <>. + +store_client(#oauth_client{client_id = ClientID, + client_name = ClientName, + grant_type = GrantType, + options = Options} = R) -> + Path = path(<<"store_client">>), + SGrantType = + case GrantType of + password -> <<"password">>; + implicit -> <<"implicit">> + end, + SOptions = misc:term_to_base64(Options), + %% Retry 2 times, with a backoff of 500millisec + case rest:with_retry( + post, + [ejabberd_config:get_myname(), Path, [], + #{<<"client_id">> => ClientID, + <<"client_name">> => ClientName, + <<"grant_type">> => SGrantType, + <<"options">> => SOptions + }], 2, 500) of + {ok, Code, _} when Code == 200 orelse Code == 201 -> + ok; + Err -> + ?ERROR_MSG("Failed to store oauth record ~p: ~p", [R, Err]), + {error, db_failure} + end. + +lookup_client(ClientID) -> + Path = path(<<"lookup_client">>), + case rest:with_retry(post, [ejabberd_config:get_myname(), Path, [], + #{<<"client_id">> => ClientID}], + 2, 500) of + {ok, 200, Data} -> + ClientName = case maps:find(<<"client_name">>, Data) of + {ok, CN} -> CN; + error -> <<>> + end, + SGrantType = case maps:find(<<"grant_type">>, Data) of + {ok, GT} -> GT; + error -> <<>> + end, + GrantType = + case SGrantType of + <<"password">> -> password; + <<"implicit">> -> implicit + end, + SOptions = case maps:find(<<"options">>, Data) of + {ok, O} -> O; + error -> <<>> + end, + case misc:base64_to_term(SOptions) of + {term, Options} -> + {ok, #oauth_client{client_id = ClientID, + client_name = ClientName, + grant_type = GrantType, + options = Options}}; + _ -> + error + end; + {ok, 404, _Resp} -> + error; + Other -> + ?ERROR_MSG("Unexpected response for oauth lookup: ~p", [Other]), + error + end. diff --git a/src/ejabberd_oauth_sql.erl b/src/ejabberd_oauth_sql.erl new file mode 100644 index 000000000..fe0a159ad --- /dev/null +++ b/src/ejabberd_oauth_sql.erl @@ -0,0 +1,174 @@ +%%%------------------------------------------------------------------- +%%% File : ejabberd_oauth_sql.erl +%%% Author : Alexey Shchepin +%%% Purpose : OAUTH2 SQL backend +%%% Created : 27 Jul 2016 by Alexey Shchepin +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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., 59 Temple Place, Suite 330, Boston, MA +%%% 02111-1307 USA +%%% +%%%------------------------------------------------------------------- + +-module(ejabberd_oauth_sql). +-behaviour(ejabberd_oauth). + +-export([init/0, + store/1, + lookup/1, + clean/1, + lookup_client/1, + store_client/1, + remove_client/1, revoke/1]). +-export([sql_schemas/0]). + +-include("ejabberd_oauth.hrl"). +-include("ejabberd_sql_pt.hrl"). +-include_lib("xmpp/include/jid.hrl"). +-include("logger.hrl"). + +init() -> + ejabberd_sql_schema:update_schema( + ejabberd_config:get_myname(), ?MODULE, sql_schemas()), + ok. + +sql_schemas() -> + [#sql_schema{ + version = 1, + tables = + [#sql_table{ + name = <<"oauth_token">>, + columns = + [#sql_column{name = <<"token">>, type = text}, + #sql_column{name = <<"jid">>, type = text}, + #sql_column{name = <<"scope">>, type = text}, + #sql_column{name = <<"expire">>, type = bigint}], + indices = [#sql_index{ + columns = [<<"token">>], + unique = true}]}, + #sql_table{ + name = <<"oauth_client">>, + columns = + [#sql_column{name = <<"client_id">>, type = text}, + #sql_column{name = <<"client_name">>, type = text}, + #sql_column{name = <<"grant_type">>, type = text}, + #sql_column{name = <<"options">>, type = text}], + indices = [#sql_index{ + columns = [<<"client_id">>], + unique = true}]}]}]. + +store(R) -> + Token = R#oauth_token.token, + {User, Server} = R#oauth_token.us, + SJID = jid:encode({User, Server, <<"">>}), + Scope = str:join(R#oauth_token.scope, <<" ">>), + Expire = R#oauth_token.expire, + case ?SQL_UPSERT( + ejabberd_config:get_myname(), + "oauth_token", + ["!token=%(Token)s", + "jid=%(SJID)s", + "scope=%(Scope)s", + "expire=%(Expire)d"]) of + ok -> + ok; + _ -> + {error, db_failure} + end. + +lookup(Token) -> + case ejabberd_sql:sql_query( + ejabberd_config:get_myname(), + ?SQL("select @(jid)s, @(scope)s, @(expire)d" + " from oauth_token where token=%(Token)s")) of + {selected, [{SJID, Scope, Expire}]} -> + JID = jid:decode(SJID), + US = {JID#jid.luser, JID#jid.lserver}, + {ok, #oauth_token{token = Token, + us = US, + scope = str:tokens(Scope, <<" ">>), + expire = Expire}}; + _ -> + error + end. + +revoke(Token) -> + case ejabberd_sql:sql_query( + ejabberd_config:get_myname(), + ?SQL("delete from oauth_token where token=%(Token)s")) of + {error, _} -> + {error, <<"db error">>}; + _ -> + ok + end. + +clean(TS) -> + ejabberd_sql:sql_query( + ejabberd_config:get_myname(), + ?SQL("delete from oauth_token where expire < %(TS)d")). + +lookup_client(ClientID) -> + case ejabberd_sql:sql_query( + ejabberd_config:get_myname(), + ?SQL("select @(client_name)s, @(grant_type)s, @(options)s" + " from oauth_client where client_id=%(ClientID)s")) of + {selected, [{ClientName, SGrantType, SOptions}]} -> + GrantType = + case SGrantType of + <<"password">> -> password; + <<"implicit">> -> implicit + end, + case misc:base64_to_term(SOptions) of + {term, Options} -> + {ok, #oauth_client{client_id = ClientID, + client_name = ClientName, + grant_type = GrantType, + options = Options}}; + _ -> + error + end; + _ -> + error + end. + +store_client(#oauth_client{client_id = ClientID, + client_name = ClientName, + grant_type = GrantType, + options = Options}) -> + SGrantType = + case GrantType of + password -> <<"password">>; + implicit -> <<"implicit">> + end, + SOptions = misc:term_to_base64(Options), + case ?SQL_UPSERT( + ejabberd_config:get_myname(), + "oauth_client", + ["!client_id=%(ClientID)s", + "client_name=%(ClientName)s", + "grant_type=%(SGrantType)s", + "options=%(SOptions)s"]) of + ok -> + ok; + _ -> + {error, db_failure} + end. + +remove_client(Client) -> + ejabberd_sql:sql_query( + ejabberd_config:get_myname(), + ?SQL("delete from oauth_client where client=%(Client)s")). diff --git a/src/ejabberd_odbc.erl b/src/ejabberd_odbc.erl deleted file mode 100644 index 9cf30f53e..000000000 --- a/src/ejabberd_odbc.erl +++ /dev/null @@ -1,624 +0,0 @@ -%%%---------------------------------------------------------------------- -%%% File : ejabberd_odbc.erl -%%% Author : Alexey Shchepin -%%% Purpose : Serve ODBC connection -%%% Created : 8 Dec 2004 by Alexey Shchepin -%%% -%%% -%%% ejabberd, Copyright (C) 2002-2015 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(ejabberd_odbc). - --author('alexey@process-one.net'). - --define(GEN_FSM, p1_fsm). - --behaviour(?GEN_FSM). - -%% External exports --export([start/1, start_link/2, - sql_query/2, - sql_query_t/1, - sql_transaction/2, - sql_bloc/2, - escape/1, - escape_like/1, - to_bool/1, - encode_term/1, - decode_term/1, - keep_alive/1]). - -%% gen_fsm callbacks --export([init/1, handle_event/3, handle_sync_event/4, - handle_info/3, terminate/3, print_state/1, - code_change/4]). - -%% gen_fsm states --export([connecting/2, connecting/3, - session_established/2, session_established/3]). - --include("ejabberd.hrl"). --include("logger.hrl"). - --record(state, - {db_ref = self() :: pid(), - db_type = odbc :: pgsql | mysql | odbc, - start_interval = 0 :: non_neg_integer(), - host = <<"">> :: binary(), - max_pending_requests_len :: non_neg_integer(), - pending_requests = {0, queue:new()} :: {non_neg_integer(), queue()}}). - --define(STATE_KEY, ejabberd_odbc_state). - --define(NESTING_KEY, ejabberd_odbc_nesting_level). - --define(TOP_LEVEL_TXN, 0). - --define(PGSQL_PORT, 5432). - --define(MYSQL_PORT, 3306). - --define(MAX_TRANSACTION_RESTARTS, 10). - --define(TRANSACTION_TIMEOUT, 60000). - --define(KEEPALIVE_TIMEOUT, 60000). - --define(KEEPALIVE_QUERY, <<"SELECT 1;">>). - -%%-define(DBGFSM, true). - --ifdef(DBGFSM). - --define(FSMOPTS, [{debug, [trace]}]). - --else. - --define(FSMOPTS, []). - --endif. - -%%%---------------------------------------------------------------------- -%%% API -%%%---------------------------------------------------------------------- -start(Host) -> - (?GEN_FSM):start(ejabberd_odbc, [Host], - fsm_limit_opts() ++ (?FSMOPTS)). - -start_link(Host, StartInterval) -> - (?GEN_FSM):start_link(ejabberd_odbc, - [Host, StartInterval], - fsm_limit_opts() ++ (?FSMOPTS)). - --type sql_query() :: [sql_query() | binary()]. --type sql_query_result() :: {updated, non_neg_integer()} | - {error, binary()} | - {selected, [binary()], - [[binary()]]}. - --spec sql_query(binary(), sql_query()) -> sql_query_result(). - -sql_query(Host, Query) -> - sql_call(Host, {sql_query, Query}). - -%% SQL transaction based on a list of queries -%% This function automatically --spec sql_transaction(binary(), [sql_query()] | fun(() -> any())) -> - {atomic, any()} | - {aborted, any()}. - -sql_transaction(Host, Queries) - when is_list(Queries) -> - F = fun () -> - lists:foreach(fun (Query) -> sql_query_t(Query) end, - Queries) - end, - sql_transaction(Host, F); -%% SQL transaction, based on a erlang anonymous function (F = fun) -sql_transaction(Host, F) when is_function(F) -> - sql_call(Host, {sql_transaction, F}). - -%% SQL bloc, based on a erlang anonymous function (F = fun) -sql_bloc(Host, F) -> sql_call(Host, {sql_bloc, F}). - -sql_call(Host, Msg) -> - case get(?STATE_KEY) of - undefined -> - case ejabberd_odbc_sup:get_random_pid(Host) of - none -> {error, <<"Unknown Host">>}; - Pid -> - (?GEN_FSM):sync_send_event(Pid,{sql_cmd, Msg, now()}, - ?TRANSACTION_TIMEOUT) - end; - _State -> nested_op(Msg) - end. - -keep_alive(PID) -> - (?GEN_FSM):sync_send_event(PID, - {sql_cmd, {sql_query, ?KEEPALIVE_QUERY}, now()}, - ?KEEPALIVE_TIMEOUT). - --spec sql_query_t(sql_query()) -> sql_query_result(). - -%% This function is intended to be used from inside an sql_transaction: -sql_query_t(Query) -> - QRes = sql_query_internal(Query), - case QRes of - {error, Reason} -> throw({aborted, Reason}); - Rs when is_list(Rs) -> - case lists:keysearch(error, 1, Rs) of - {value, {error, Reason}} -> throw({aborted, Reason}); - _ -> QRes - end; - _ -> QRes - end. - -%% Escape character that will confuse an SQL engine -escape(S) -> - << <<(odbc_queries:escape(Char))/binary>> || <> <= S >>. - -%% Escape character that will confuse an SQL engine -%% Percent and underscore only need to be escaped for pattern matching like -%% statement -escape_like(S) when is_binary(S) -> - << <<(escape_like(C))/binary>> || <> <= S >>; -escape_like($%) -> <<"\\%">>; -escape_like($_) -> <<"\\_">>; -escape_like(C) when is_integer(C), C >= 0, C =< 255 -> odbc_queries:escape(C). - -to_bool(<<"t">>) -> true; -to_bool(<<"true">>) -> true; -to_bool(<<"1">>) -> true; -to_bool(true) -> true; -to_bool(1) -> true; -to_bool(_) -> false. - -encode_term(Term) -> - escape(list_to_binary( - erl_prettypr:format(erl_syntax:abstract(Term)))). - -decode_term(Bin) -> - Str = binary_to_list(<>), - {ok, Tokens, _} = erl_scan:string(Str), - {ok, Term} = erl_parse:parse_term(Tokens), - Term. - -%%%---------------------------------------------------------------------- -%%% Callback functions from gen_fsm -%%%---------------------------------------------------------------------- -init([Host, StartInterval]) -> - case ejabberd_config:get_option( - {odbc_keepalive_interval, Host}, - fun(I) when is_integer(I), I>0 -> I end) of - undefined -> - ok; - KeepaliveInterval -> - timer:apply_interval(KeepaliveInterval * 1000, ?MODULE, - keep_alive, [self()]) - end, - [DBType | _] = db_opts(Host), - (?GEN_FSM):send_event(self(), connect), - ejabberd_odbc_sup:add_pid(Host, self()), - {ok, connecting, - #state{db_type = DBType, host = Host, - max_pending_requests_len = max_fsm_queue(), - pending_requests = {0, queue:new()}, - start_interval = StartInterval}}. - -connecting(connect, #state{host = Host} = State) -> - ConnectRes = case db_opts(Host) of - [mysql | Args] -> apply(fun mysql_connect/5, Args); - [pgsql | Args] -> apply(fun pgsql_connect/5, Args); - [odbc | Args] -> apply(fun odbc_connect/1, Args) - end, - {_, PendingRequests} = State#state.pending_requests, - case ConnectRes of - {ok, Ref} -> - erlang:monitor(process, Ref), - lists:foreach(fun (Req) -> - (?GEN_FSM):send_event(self(), Req) - end, - queue:to_list(PendingRequests)), - {next_state, session_established, - State#state{db_ref = Ref, - pending_requests = {0, queue:new()}}}; - {error, Reason} -> - ?INFO_MSG("~p connection failed:~n** Reason: ~p~n** " - "Retry after: ~p seconds", - [State#state.db_type, Reason, - State#state.start_interval div 1000]), - (?GEN_FSM):send_event_after(State#state.start_interval, - connect), - {next_state, connecting, State} - end; -connecting(Event, State) -> - ?WARNING_MSG("unexpected event in 'connecting': ~p", - [Event]), - {next_state, connecting, State}. - -connecting({sql_cmd, {sql_query, ?KEEPALIVE_QUERY}, - _Timestamp}, - From, State) -> - (?GEN_FSM):reply(From, - {error, <<"SQL connection failed">>}), - {next_state, connecting, State}; -connecting({sql_cmd, Command, Timestamp} = Req, From, - State) -> - ?DEBUG("queuing pending request while connecting:~n\t~p", - [Req]), - {Len, PendingRequests} = State#state.pending_requests, - NewPendingRequests = if Len < - State#state.max_pending_requests_len -> - {Len + 1, - queue:in({sql_cmd, Command, From, Timestamp}, - PendingRequests)}; - true -> - lists:foreach(fun ({sql_cmd, _, To, - _Timestamp}) -> - (?GEN_FSM):reply(To, - {error, - <<"SQL connection failed">>}) - end, - queue:to_list(PendingRequests)), - {1, - queue:from_list([{sql_cmd, Command, From, - Timestamp}])} - end, - {next_state, connecting, - State#state{pending_requests = NewPendingRequests}}; -connecting(Request, {Who, _Ref}, State) -> - ?WARNING_MSG("unexpected call ~p from ~p in 'connecting'", - [Request, Who]), - {reply, {error, badarg}, connecting, State}. - -session_established({sql_cmd, Command, Timestamp}, From, - State) -> - run_sql_cmd(Command, From, State, Timestamp); -session_established(Request, {Who, _Ref}, State) -> - ?WARNING_MSG("unexpected call ~p from ~p in 'session_establ" - "ished'", - [Request, Who]), - {reply, {error, badarg}, session_established, State}. - -session_established({sql_cmd, Command, From, Timestamp}, - State) -> - run_sql_cmd(Command, From, State, Timestamp); -session_established(Event, State) -> - ?WARNING_MSG("unexpected event in 'session_established': ~p", - [Event]), - {next_state, session_established, State}. - -handle_event(_Event, StateName, State) -> - {next_state, StateName, State}. - -handle_sync_event(_Event, _From, StateName, State) -> - {reply, {error, badarg}, StateName, State}. - -code_change(_OldVsn, StateName, State, _Extra) -> - {ok, StateName, State}. - -%% We receive the down signal when we loose the MySQL connection (we are -%% monitoring the connection) -handle_info({'DOWN', _MonitorRef, process, _Pid, _Info}, - _StateName, State) -> - (?GEN_FSM):send_event(self(), connect), - {next_state, connecting, State}; -handle_info(Info, StateName, State) -> - ?WARNING_MSG("unexpected info in ~p: ~p", - [StateName, Info]), - {next_state, StateName, State}. - -terminate(_Reason, _StateName, State) -> - ejabberd_odbc_sup:remove_pid(State#state.host, self()), - case State#state.db_type of - mysql -> catch p1_mysql_conn:stop(State#state.db_ref); - _ -> ok - end, - ok. - -%%---------------------------------------------------------------------- -%% Func: print_state/1 -%% Purpose: Prepare the state to be printed on error log -%% Returns: State to print -%%---------------------------------------------------------------------- -print_state(State) -> State. - -%%%---------------------------------------------------------------------- -%%% Internal functions -%%%---------------------------------------------------------------------- - -run_sql_cmd(Command, From, State, Timestamp) -> - case timer:now_diff(now(), Timestamp) div 1000 of - Age when Age < (?TRANSACTION_TIMEOUT) -> - put(?NESTING_KEY, ?TOP_LEVEL_TXN), - put(?STATE_KEY, State), - abort_on_driver_error(outer_op(Command), From); - Age -> - ?ERROR_MSG("Database was not available or too slow, " - "discarding ~p milliseconds old request~n~p~n", - [Age, Command]), - {next_state, session_established, State} - end. - -%% Only called by handle_call, only handles top level operations. -%% @spec outer_op(Op) -> {error, Reason} | {aborted, Reason} | {atomic, Result} -outer_op({sql_query, Query}) -> - sql_query_internal(Query); -outer_op({sql_transaction, F}) -> - outer_transaction(F, ?MAX_TRANSACTION_RESTARTS, <<"">>); -outer_op({sql_bloc, F}) -> execute_bloc(F). - -%% Called via sql_query/transaction/bloc from client code when inside a -%% nested operation -nested_op({sql_query, Query}) -> - sql_query_internal(Query); -nested_op({sql_transaction, F}) -> - NestingLevel = get(?NESTING_KEY), - if NestingLevel =:= (?TOP_LEVEL_TXN) -> - outer_transaction(F, ?MAX_TRANSACTION_RESTARTS, <<"">>); - true -> inner_transaction(F) - end; -nested_op({sql_bloc, F}) -> execute_bloc(F). - -%% Never retry nested transactions - only outer transactions -inner_transaction(F) -> - PreviousNestingLevel = get(?NESTING_KEY), - case get(?NESTING_KEY) of - ?TOP_LEVEL_TXN -> - {backtrace, T} = process_info(self(), backtrace), - ?ERROR_MSG("inner transaction called at outer txn " - "level. Trace: ~s", - [T]), - erlang:exit(implementation_faulty); - _N -> ok - end, - put(?NESTING_KEY, PreviousNestingLevel + 1), - Result = (catch F()), - put(?NESTING_KEY, PreviousNestingLevel), - case Result of - {aborted, Reason} -> {aborted, Reason}; - {'EXIT', Reason} -> {'EXIT', Reason}; - {atomic, Res} -> {atomic, Res}; - Res -> {atomic, Res} - end. - -outer_transaction(F, NRestarts, _Reason) -> - PreviousNestingLevel = get(?NESTING_KEY), - case get(?NESTING_KEY) of - ?TOP_LEVEL_TXN -> ok; - _N -> - {backtrace, T} = process_info(self(), backtrace), - ?ERROR_MSG("outer transaction called at inner txn " - "level. Trace: ~s", - [T]), - erlang:exit(implementation_faulty) - end, - sql_query_internal(<<"begin;">>), - put(?NESTING_KEY, PreviousNestingLevel + 1), - Result = (catch F()), - put(?NESTING_KEY, PreviousNestingLevel), - case Result of - {aborted, Reason} when NRestarts > 0 -> - sql_query_internal(<<"rollback;">>), - outer_transaction(F, NRestarts - 1, Reason); - {aborted, Reason} when NRestarts =:= 0 -> - ?ERROR_MSG("SQL transaction restarts exceeded~n** " - "Restarts: ~p~n** Last abort reason: " - "~p~n** Stacktrace: ~p~n** When State " - "== ~p", - [?MAX_TRANSACTION_RESTARTS, Reason, - erlang:get_stacktrace(), get(?STATE_KEY)]), - sql_query_internal(<<"rollback;">>), - {aborted, Reason}; - {'EXIT', Reason} -> - sql_query_internal(<<"rollback;">>), {aborted, Reason}; - Res -> sql_query_internal(<<"commit;">>), {atomic, Res} - end. - -execute_bloc(F) -> - case catch F() of - {aborted, Reason} -> {aborted, Reason}; - {'EXIT', Reason} -> {aborted, Reason}; - Res -> {atomic, Res} - end. - -sql_query_internal(Query) -> - State = get(?STATE_KEY), - Res = case State#state.db_type of - odbc -> - to_odbc(odbc:sql_query(State#state.db_ref, Query, - (?TRANSACTION_TIMEOUT) - 1000)); - pgsql -> - pgsql_to_odbc(pgsql:squery(State#state.db_ref, Query)); - mysql -> - ?DEBUG("MySQL, Send query~n~p~n", [Query]), - %%squery to be able to specify result_type = binary - %%[Query] because p1_mysql_conn expect query to be a list (elements can be binaries, or iolist) - %% but doesn't accept just a binary - R = mysql_to_odbc(p1_mysql_conn:squery(State#state.db_ref, - [Query], self(), - [{timeout, (?TRANSACTION_TIMEOUT) - 1000}, - {result_type, binary}])), - %% ?INFO_MSG("MySQL, Received result~n~p~n", [R]), - R - end, - case Res of - {error, <<"No SQL-driver information available.">>} -> - {updated, 0}; - _Else -> Res - end. - -%% Generate the OTP callback return tuple depending on the driver result. -abort_on_driver_error({error, <<"query timed out">>} = - Reply, - From) -> - (?GEN_FSM):reply(From, Reply), - {stop, timeout, get(?STATE_KEY)}; -abort_on_driver_error({error, - <<"Failed sending data on socket", _/binary>>} = - Reply, - From) -> - (?GEN_FSM):reply(From, Reply), - {stop, closed, get(?STATE_KEY)}; -abort_on_driver_error(Reply, From) -> - (?GEN_FSM):reply(From, Reply), - {next_state, session_established, get(?STATE_KEY)}. - -%% == pure ODBC code - -%% part of init/1 -%% Open an ODBC database connection -odbc_connect(SQLServer) -> - ejabberd:start_app(odbc), - odbc:connect(binary_to_list(SQLServer), [{scrollable_cursors, off}]). - -%% == Native PostgreSQL code - -%% part of init/1 -%% Open a database connection to PostgreSQL -pgsql_connect(Server, Port, DB, Username, Password) -> - case pgsql:connect([{host, Server}, - {database, DB}, - {user, Username}, - {password, Password}, - {port, Port}, - {as_binary, true}]) of - {ok, Ref} -> - pgsql:squery(Ref, [<<"alter database ">>, DB, <<" set ">>, - <<"standard_conforming_strings='off';">>]), - {ok, Ref}; - Err -> - Err - end. - -%% Convert PostgreSQL query result to Erlang ODBC result formalism -pgsql_to_odbc({ok, PGSQLResult}) -> - case PGSQLResult of - [Item] -> pgsql_item_to_odbc(Item); - Items -> [pgsql_item_to_odbc(Item) || Item <- Items] - end. - -pgsql_item_to_odbc({<<"SELECT", _/binary>>, Rows, - Recs}) -> - {selected, [element(1, Row) || Row <- Rows], Recs}; -pgsql_item_to_odbc({<<"FETCH", _/binary>>, Rows, - Recs}) -> - {selected, [element(1, Row) || Row <- Rows], Recs}; -pgsql_item_to_odbc(<<"INSERT ", OIDN/binary>>) -> - [_OID, N] = str:tokens(OIDN, <<" ">>), - {updated, jlib:binary_to_integer(N)}; -pgsql_item_to_odbc(<<"DELETE ", N/binary>>) -> - {updated, jlib:binary_to_integer(N)}; -pgsql_item_to_odbc(<<"UPDATE ", N/binary>>) -> - {updated, jlib:binary_to_integer(N)}; -pgsql_item_to_odbc({error, Error}) -> {error, Error}; -pgsql_item_to_odbc(_) -> {updated, undefined}. - -%% == Native MySQL code - -%% part of init/1 -%% Open a database connection to MySQL -mysql_connect(Server, Port, DB, Username, Password) -> - case p1_mysql_conn:start(binary_to_list(Server), Port, - binary_to_list(Username), binary_to_list(Password), - binary_to_list(DB), fun log/3) - of - {ok, Ref} -> - p1_mysql_conn:fetch(Ref, [<<"set names 'utf8';">>], - self()), - {ok, Ref}; - Err -> Err - end. - -%% Convert MySQL query result to Erlang ODBC result formalism -mysql_to_odbc({updated, MySQLRes}) -> - {updated, p1_mysql:get_result_affected_rows(MySQLRes)}; -mysql_to_odbc({data, MySQLRes}) -> - mysql_item_to_odbc(p1_mysql:get_result_field_info(MySQLRes), - p1_mysql:get_result_rows(MySQLRes)); -mysql_to_odbc({error, MySQLRes}) - when is_binary(MySQLRes) -> - {error, MySQLRes}; -mysql_to_odbc({error, MySQLRes}) - when is_list(MySQLRes) -> - {error, list_to_binary(MySQLRes)}; -mysql_to_odbc({error, MySQLRes}) -> - {error, p1_mysql:get_result_reason(MySQLRes)}; -mysql_to_odbc(ok) -> - ok. - - -%% When tabular data is returned, convert it to the ODBC formalism -mysql_item_to_odbc(Columns, Recs) -> - {selected, [element(2, Column) || Column <- Columns], Recs}. - -to_odbc({selected, Columns, Recs}) -> - {selected, Columns, [tuple_to_list(Rec) || Rec <- Recs]}; -to_odbc(Res) -> - Res. - -log(Level, Format, Args) -> - case Level of - debug -> ?DEBUG(Format, Args); - normal -> ?INFO_MSG(Format, Args); - error -> ?ERROR_MSG(Format, Args) - end. - -db_opts(Host) -> - Type = ejabberd_config:get_option({odbc_type, Host}, - fun(mysql) -> mysql; - (pgsql) -> pgsql; - (odbc) -> odbc - end, odbc), - Server = ejabberd_config:get_option({odbc_server, Host}, - fun iolist_to_binary/1, - <<"localhost">>), - case Type of - odbc -> - [odbc, Server]; - _ -> - Port = ejabberd_config:get_option( - {odbc_port, Host}, - fun(P) when is_integer(P), P > 0, P < 65536 -> P end, - case Type of - mysql -> ?MYSQL_PORT; - pgsql -> ?PGSQL_PORT - end), - DB = ejabberd_config:get_option({odbc_database, Host}, - fun iolist_to_binary/1, - <<"ejabberd">>), - User = ejabberd_config:get_option({odbc_username, Host}, - fun iolist_to_binary/1, - <<"ejabberd">>), - Pass = ejabberd_config:get_option({odbc_password, Host}, - fun iolist_to_binary/1, - <<"">>), - [Type, Server, Port, DB, User, Pass] - end. - -max_fsm_queue() -> - ejabberd_config:get_option( - max_fsm_queue, - fun(N) when is_integer(N), N > 0 -> N end). - -fsm_limit_opts() -> - case max_fsm_queue() of - N when is_integer(N) -> [{max_queue, N}]; - _ -> [] - end. diff --git a/src/ejabberd_odbc_sup.erl b/src/ejabberd_odbc_sup.erl deleted file mode 100644 index 602e7e03b..000000000 --- a/src/ejabberd_odbc_sup.erl +++ /dev/null @@ -1,117 +0,0 @@ -%%%---------------------------------------------------------------------- -%%% File : ejabberd_odbc_sup.erl -%%% Author : Alexey Shchepin -%%% Purpose : ODBC connections supervisor -%%% Created : 22 Dec 2004 by Alexey Shchepin -%%% -%%% -%%% ejabberd, Copyright (C) 2002-2015 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(ejabberd_odbc_sup). - --author('alexey@process-one.net'). - -%% API --export([start_link/1, init/1, add_pid/2, remove_pid/2, - get_pids/1, get_random_pid/1, transform_options/1]). - --include("ejabberd.hrl"). --include("logger.hrl"). - --define(PGSQL_PORT, 5432). - --define(MYSQL_PORT, 3306). - --define(DEFAULT_POOL_SIZE, 10). - --define(DEFAULT_ODBC_START_INTERVAL, 30). - --define(CONNECT_TIMEOUT, 500). - --record(sql_pool, {host, pid}). - -start_link(Host) -> - mnesia:create_table(sql_pool, - [{ram_copies, [node()]}, {type, bag}, - {local_content, true}, - {attributes, record_info(fields, sql_pool)}]), - mnesia:add_table_copy(sql_pool, node(), ram_copies), - F = fun () -> mnesia:delete({sql_pool, Host}) end, - mnesia:ets(F), - supervisor:start_link({local, - gen_mod:get_module_proc(Host, ?MODULE)}, - ?MODULE, [Host]). - -init([Host]) -> - PoolSize = ejabberd_config:get_option( - {odbc_pool_size, Host}, - fun(I) when is_integer(I), I>0 -> I end, - ?DEFAULT_POOL_SIZE), - StartInterval = ejabberd_config:get_option( - {odbc_start_interval, Host}, - fun(I) when is_integer(I), I>0 -> I end, - ?DEFAULT_ODBC_START_INTERVAL), - {ok, - {{one_for_one, PoolSize * 10, 1}, - lists:map(fun (I) -> - {I, - {ejabberd_odbc, start_link, - [Host, StartInterval * 1000]}, - transient, 2000, worker, [?MODULE]} - end, - lists:seq(1, PoolSize))}}. - -get_pids(Host) -> - Rs = mnesia:dirty_read(sql_pool, Host), - [R#sql_pool.pid || R <- Rs]. - -get_random_pid(Host) -> - case get_pids(Host) of - [] -> none; - Pids -> lists:nth(erlang:phash(now(), length(Pids)), Pids) - end. - -add_pid(Host, Pid) -> - F = fun () -> - mnesia:write(#sql_pool{host = Host, pid = Pid}) - end, - mnesia:ets(F). - -remove_pid(Host, Pid) -> - F = fun () -> - mnesia:delete_object(#sql_pool{host = Host, pid = Pid}) - end, - mnesia:ets(F). - -transform_options(Opts) -> - lists:foldl(fun transform_options/2, [], Opts). - -transform_options({odbc_server, {Type, Server, Port, DB, User, Pass}}, Opts) -> - [{odbc_type, Type}, - {odbc_server, Server}, - {odbc_port, Port}, - {odbc_database, DB}, - {odbc_username, User}, - {odbc_password, Pass}|Opts]; -transform_options({odbc_server, {mysql, Server, DB, User, Pass}}, Opts) -> - transform_options({odbc_server, {mysql, Server, ?MYSQL_PORT, DB, User, Pass}}, Opts); -transform_options({odbc_server, {pgsql, Server, DB, User, Pass}}, Opts) -> - transform_options({odbc_server, {pgsql, Server, ?PGSQL_PORT, DB, User, Pass}}, Opts); -transform_options(Opt, Opts) -> - [Opt|Opts]. diff --git a/src/ejabberd_old_config.erl b/src/ejabberd_old_config.erl new file mode 100644 index 000000000..670dd7158 --- /dev/null +++ b/src/ejabberd_old_config.erl @@ -0,0 +1,643 @@ +%%%---------------------------------------------------------------------- +%%% Purpose: Transform old-style Erlang config to YAML config +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(ejabberd_old_config). + +%% API +-export([read_file/1]). + +-include("logger.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +read_file(File) -> + case consult(File) of + {ok, Terms1} -> + ?INFO_MSG("Converting from old configuration format", []), + Terms2 = strings_to_binary(Terms1), + Terms3 = transform(Terms2), + Terms4 = transform_certfiles(Terms3), + Terms5 = transform_host_config(Terms4), + {ok, collect_options(Terms5)}; + {error, Reason} -> + {error, {old_config, File, Reason}} + end. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +collect_options(Opts) -> + {D, InvalidOpts} = + lists:foldl( + fun({K, V}, {D, Os}) when is_list(V) -> + {orddict:append_list(K, V, D), Os}; + ({K, V}, {D, Os}) -> + {orddict:store(K, V, D), Os}; + (Opt, {D, Os}) -> + {D, [Opt|Os]} + end, {orddict:new(), []}, Opts), + InvalidOpts ++ orddict:to_list(D). + +transform(Opts) -> + Opts1 = transform_register(Opts), + Opts2 = transform_s2s(Opts1), + Opts3 = transform_listeners(Opts2), + Opts5 = transform_sql(Opts3), + Opts6 = transform_shaper(Opts5), + Opts7 = transform_s2s_out(Opts6), + Opts8 = transform_acl(Opts7), + Opts9 = transform_modules(Opts8), + Opts10 = transform_globals(Opts9), + collect_options(Opts10). + +%%%=================================================================== +%%% mod_register +%%%=================================================================== +transform_register(Opts) -> + try + {value, {modules, ModOpts}, Opts1} = lists:keytake(modules, 1, Opts), + {value, {?MODULE, RegOpts}, ModOpts1} = lists:keytake(?MODULE, 1, ModOpts), + {value, {ip_access, L}, RegOpts1} = lists:keytake(ip_access, 1, RegOpts), + true = is_list(L), + ?WARNING_MSG("Old 'ip_access' format detected. " + "The old format is still supported " + "but it is better to fix your config: " + "use access rules instead.", []), + ACLs = lists:flatmap( + fun({Action, S}) -> + ACLName = misc:binary_to_atom( + iolist_to_binary( + ["ip_", S])), + [{Action, ACLName}, + {acl, ACLName, {ip, S}}] + end, L), + Access = {access, mod_register_networks, + [{Action, ACLName} || {Action, ACLName} <- ACLs]}, + [ACL || {acl, _, _} = ACL <- ACLs] ++ + [Access, + {modules, + [{mod_register, + [{ip_access, mod_register_networks}|RegOpts1]} + | ModOpts1]}|Opts1] + catch error:{badmatch, false} -> + Opts + end. + +%%%=================================================================== +%%% ejabberd_s2s +%%%=================================================================== +transform_s2s(Opts) -> + lists:foldl(fun transform_s2s/2, [], Opts). + +transform_s2s({{s2s_host, Host}, Action}, Opts) -> + ?WARNING_MSG("Option 's2s_host' is deprecated.", []), + ACLName = misc:binary_to_atom( + iolist_to_binary(["s2s_access_", Host])), + [{acl, ACLName, {server, Host}}, + {access, s2s, [{Action, ACLName}]}, + {s2s_access, s2s} | + Opts]; +transform_s2s({s2s_default_policy, Action}, Opts) -> + ?WARNING_MSG("Option 's2s_default_policy' is deprecated. " + "The option is still supported but it is better to " + "fix your config: " + "use 's2s_access' with an access rule.", []), + [{access, s2s, [{Action, all}]}, + {s2s_access, s2s} | + Opts]; +transform_s2s(Opt, Opts) -> + [Opt|Opts]. + +%%%=================================================================== +%%% ejabberd_s2s_out +%%%=================================================================== +transform_s2s_out(Opts) -> + lists:foldl(fun transform_s2s_out/2, [], Opts). + +transform_s2s_out({outgoing_s2s_options, Families, Timeout}, Opts) -> + ?WARNING_MSG("Option 'outgoing_s2s_options' is deprecated. " + "The option is still supported " + "but it is better to fix your config: " + "use 'outgoing_s2s_timeout' and " + "'outgoing_s2s_families' instead.", []), + [{outgoing_s2s_families, Families}, + {outgoing_s2s_timeout, Timeout} + | Opts]; +transform_s2s_out({s2s_dns_options, S2SDNSOpts}, AllOpts) -> + ?WARNING_MSG("Option 's2s_dns_options' is deprecated. " + "The option is still supported " + "but it is better to fix your config: " + "use 's2s_dns_timeout' and " + "'s2s_dns_retries' instead", []), + lists:foldr( + fun({timeout, T}, AccOpts) -> + [{s2s_dns_timeout, T}|AccOpts]; + ({retries, R}, AccOpts) -> + [{s2s_dns_retries, R}|AccOpts]; + (_, AccOpts) -> + AccOpts + end, AllOpts, S2SDNSOpts); +transform_s2s_out(Opt, Opts) -> + [Opt|Opts]. + +%%%=================================================================== +%%% ejabberd_listener +%%%=================================================================== +transform_listeners(Opts) -> + lists:foldl(fun transform_listeners/2, [], Opts). + +transform_listeners({listen, LOpts}, Opts) -> + [{listen, lists:map(fun transform_listener/1, LOpts)} | Opts]; +transform_listeners(Opt, Opts) -> + [Opt|Opts]. + +transform_listener({{Port, IP, Transport}, Mod, Opts}) -> + IPStr = if is_tuple(IP) -> + list_to_binary(inet_parse:ntoa(IP)); + true -> + IP + end, + Opts1 = lists:map( + fun({ip, IPT}) when is_tuple(IPT) -> + {ip, list_to_binary(inet_parse:ntoa(IP))}; + (ssl) -> {tls, true}; + (A) when is_atom(A) -> {A, true}; + (Opt) -> Opt + end, Opts), + Opts2 = lists:foldl( + fun(Opt, Acc) -> + transform_listen_option(Mod, Opt, Acc) + end, [], Opts1), + TransportOpt = if Transport == tcp -> []; + true -> [{transport, Transport}] + end, + IPOpt = if IPStr == <<"0.0.0.0">> -> []; + true -> [{ip, IPStr}] + end, + IPOpt ++ TransportOpt ++ [{port, Port}, {module, Mod} | Opts2]; +transform_listener({{Port, Transport}, Mod, Opts}) + when Transport == tcp orelse Transport == udp -> + transform_listener({{Port, all_zero_ip(Opts), Transport}, Mod, Opts}); +transform_listener({{Port, IP}, Mod, Opts}) -> + transform_listener({{Port, IP, tcp}, Mod, Opts}); +transform_listener({Port, Mod, Opts}) -> + transform_listener({{Port, all_zero_ip(Opts), tcp}, Mod, Opts}); +transform_listener(Opt) -> + Opt. + +transform_listen_option(ejabberd_http, captcha, Opts) -> + [{captcha, true}|Opts]; +transform_listen_option(ejabberd_http, register, Opts) -> + [{register, true}|Opts]; +transform_listen_option(ejabberd_http, web_admin, Opts) -> + [{web_admin, true}|Opts]; +transform_listen_option(ejabberd_http, http_bind, Opts) -> + [{http_bind, true}|Opts]; +transform_listen_option(ejabberd_http, http_poll, Opts) -> + [{http_poll, true}|Opts]; +transform_listen_option(ejabberd_http, {request_handlers, Hs}, Opts) -> + Hs1 = lists:map( + fun({PList, Mod}) when is_list(PList) -> + Path = iolist_to_binary([[$/, P] || P <- PList]), + {Path, Mod}; + (Opt) -> + Opt + end, Hs), + [{request_handlers, Hs1} | Opts]; +transform_listen_option(ejabberd_service, {hosts, Hosts, O}, Opts) -> + case lists:keyfind(hosts, 1, Opts) of + {_, PrevHostOpts} -> + NewHostOpts = + lists:foldl( + fun(H, Acc) -> + dict:append_list(H, O, Acc) + end, dict:from_list(PrevHostOpts), Hosts), + [{hosts, dict:to_list(NewHostOpts)}| + lists:keydelete(hosts, 1, Opts)]; + _ -> + [{hosts, [{H, O} || H <- Hosts]}|Opts] + end; +transform_listen_option(ejabberd_service, {host, Host, Os}, Opts) -> + transform_listen_option(ejabberd_service, {hosts, [Host], Os}, Opts); +transform_listen_option(ejabberd_xmlrpc, {access_commands, ACOpts}, Opts) -> + NewACOpts = lists:map( + fun({AName, ACmds, AOpts}) -> + {AName, [{commands, ACmds}, {options, AOpts}]}; + (Opt) -> + Opt + end, ACOpts), + [{access_commands, NewACOpts}|Opts]; +transform_listen_option(_, Opt, Opts) -> + [Opt|Opts]. + +-spec all_zero_ip([proplists:property()]) -> inet:ip_address(). +all_zero_ip(Opts) -> + case proplists:get_bool(inet6, Opts) of + true -> {0,0,0,0,0,0,0,0}; + false -> {0,0,0,0} + end. + +%%%=================================================================== +%%% ejabberd_shaper +%%%=================================================================== +transform_shaper(Opts) -> + lists:foldl(fun transform_shaper/2, [], Opts). + +transform_shaper({shaper, Name, {maxrate, N}}, Opts) -> + [{shaper, [{Name, N}]} | Opts]; +transform_shaper({shaper, Name, none}, Opts) -> + [{shaper, [{Name, none}]} | Opts]; +transform_shaper({shaper, List}, Opts) when is_list(List) -> + R = lists:map( + fun({Name, Args}) when is_list(Args) -> + MaxRate = proplists:get_value(rate, Args, 1000), + BurstSize = proplists:get_value(burst_size, Args, MaxRate), + {Name, MaxRate, BurstSize}; + ({Name, Val}) -> + {Name, Val, Val} + end, List), + [{shaper, R} | Opts]; +transform_shaper(Opt, Opts) -> + [Opt | Opts]. + +%%%=================================================================== +%%% acl +%%%=================================================================== +transform_acl(Opts) -> + Opts1 = lists:foldl(fun transform_acl/2, [], Opts), + {ACLOpts, Opts2} = lists:mapfoldl( + fun({acl, Os}, Acc) -> + {Os, Acc}; + (O, Acc) -> + {[], [O|Acc]} + end, [], Opts1), + {AccessOpts, Opts3} = lists:mapfoldl( + fun({access, Os}, Acc) -> + {Os, Acc}; + (O, Acc) -> + {[], [O|Acc]} + end, [], Opts2), + {NewAccessOpts, Opts4} = lists:mapfoldl( + fun({access_rules, Os}, Acc) -> + {Os, Acc}; + (O, Acc) -> + {[], [O|Acc]} + end, [], Opts3), + {ShaperOpts, Opts5} = lists:mapfoldl( + fun({shaper_rules, Os}, Acc) -> + {Os, Acc}; + (O, Acc) -> + {[], [O|Acc]} + end, [], Opts4), + ACLOpts1 = collect_options(lists:flatten(ACLOpts)), + AccessOpts1 = case collect_options(lists:flatten(AccessOpts)) of + [] -> []; + L1 -> [{access, L1}] + end, + ACLOpts2 = case lists:map( + fun({ACLName, Os}) -> + {ACLName, collect_options(Os)} + end, ACLOpts1) of + [] -> []; + L2 -> [{acl, L2}] + end, + NewAccessOpts1 = case lists:map( + fun({NAName, Os}) -> + {NAName, transform_access_rules_config(Os)} + end, lists:flatten(NewAccessOpts)) of + [] -> []; + L3 -> [{access_rules, L3}] + end, + ShaperOpts1 = case lists:map( + fun({SName, Ss}) -> + {SName, transform_access_rules_config(Ss)} + end, lists:flatten(ShaperOpts)) of + [] -> []; + L4 -> [{shaper_rules, L4}] + end, + ACLOpts2 ++ AccessOpts1 ++ NewAccessOpts1 ++ ShaperOpts1 ++ Opts5. + +transform_acl({acl, Name, Type}, Opts) -> + T = case Type of + all -> all; + none -> none; + {user, U} -> {user, [b(U)]}; + {user, U, S} -> {user, [[{b(U), b(S)}]]}; + {shared_group, G} -> {shared_group, [b(G)]}; + {shared_group, G, H} -> {shared_group, [[{b(G), b(H)}]]}; + {user_regexp, UR} -> {user_regexp, [b(UR)]}; + {user_regexp, UR, S} -> {user_regexp, [[{b(UR), b(S)}]]}; + {node_regexp, UR, SR} -> {node_regexp, [[{b(UR), b(SR)}]]}; + {user_glob, UR} -> {user_glob, [b(UR)]}; + {user_glob, UR, S} -> {user_glob, [[{b(UR), b(S)}]]}; + {node_glob, UR, SR} -> {node_glob, [[{b(UR), b(SR)}]]}; + {server, S} -> {server, [b(S)]}; + {resource, R} -> {resource, [b(R)]}; + {server_regexp, SR} -> {server_regexp, [b(SR)]}; + {server_glob, S} -> {server_glob, [b(S)]}; + {ip, S} -> {ip, [b(S)]}; + {resource_glob, R} -> {resource_glob, [b(R)]}; + {resource_regexp, R} -> {resource_regexp, [b(R)]} + end, + [{acl, [{Name, [T]}]}|Opts]; +transform_acl({access, Name, Rules}, Opts) -> + NewRules = [{ACL, Action} || {Action, ACL} <- Rules], + [{access, [{Name, NewRules}]}|Opts]; +transform_acl(Opt, Opts) -> + [Opt|Opts]. + +transform_access_rules_config(Config) when is_list(Config) -> + lists:map(fun transform_access_rules_config2/1, lists:flatten(Config)); +transform_access_rules_config(Config) -> + transform_access_rules_config([Config]). + +transform_access_rules_config2(Type) when is_integer(Type); is_atom(Type) -> + {Type, [all]}; +transform_access_rules_config2({Type, ACL}) when is_atom(ACL) -> + {Type, [{acl, ACL}]}; +transform_access_rules_config2({Res, Rules}) when is_list(Rules) -> + T = lists:map(fun({Type, Args}) when is_list(Args) -> + {Type, hd(lists:flatten(Args))}; + (V) -> + V + end, lists:flatten(Rules)), + {Res, T}; +transform_access_rules_config2({Res, Rule}) -> + {Res, [Rule]}. + +%%%=================================================================== +%%% SQL +%%%=================================================================== +-define(PGSQL_PORT, 5432). +-define(MYSQL_PORT, 3306). + +transform_sql(Opts) -> + lists:foldl(fun transform_sql/2, [], Opts). + +transform_sql({odbc_server, {Type, Server, Port, DB, User, Pass}}, Opts) -> + [{sql_type, Type}, + {sql_server, Server}, + {sql_port, Port}, + {sql_database, DB}, + {sql_username, User}, + {sql_password, Pass}|Opts]; +transform_sql({odbc_server, {mysql, Server, DB, User, Pass}}, Opts) -> + transform_sql({odbc_server, {mysql, Server, ?MYSQL_PORT, DB, User, Pass}}, Opts); +transform_sql({odbc_server, {pgsql, Server, DB, User, Pass}}, Opts) -> + transform_sql({odbc_server, {pgsql, Server, ?PGSQL_PORT, DB, User, Pass}}, Opts); +transform_sql({odbc_server, {sqlite, DB}}, Opts) -> + [{sql_type, sqlite}, + {sql_database, DB}|Opts]; +transform_sql({odbc_pool_size, N}, Opts) -> + [{sql_pool_size, N}|Opts]; +transform_sql(Opt, Opts) -> + [Opt|Opts]. + +%%%=================================================================== +%%% modules +%%%=================================================================== +transform_modules(Opts) -> + lists:foldl(fun transform_modules/2, [], Opts). + +transform_modules({modules, ModOpts}, Opts) -> + [{modules, lists:map( + fun({Mod, Opts1}) -> + {Mod, transform_module(Mod, Opts1)}; + (Other) -> + Other + end, ModOpts)}|Opts]; +transform_modules(Opt, Opts) -> + [Opt|Opts]. + +transform_module(mod_disco, Opts) -> + lists:map( + fun({server_info, Infos}) -> + NewInfos = lists:map( + fun({Modules, Name, URLs}) -> + [[{modules, Modules}, + {name, Name}, + {urls, URLs}]]; + (Opt) -> + Opt + end, Infos), + {server_info, NewInfos}; + (Opt) -> + Opt + end, Opts); +transform_module(mod_muc_log, Opts) -> + lists:map( + fun({top_link, {S1, S2}}) -> + {top_link, [{S1, S2}]}; + (Opt) -> + Opt + end, Opts); +transform_module(mod_proxy65, Opts) -> + lists:map( + fun({ip, IP}) when is_tuple(IP) -> + {ip, misc:ip_to_list(IP)}; + ({hostname, IP}) when is_tuple(IP) -> + {hostname, misc:ip_to_list(IP)}; + (Opt) -> + Opt + end, Opts); +transform_module(mod_register, Opts) -> + lists:flatmap( + fun({welcome_message, {Subj, Body}}) -> + [{welcome_message, [{subject, Subj}, {body, Body}]}]; + (Opt) -> + [Opt] + end, Opts); +transform_module(_Mod, Opts) -> + Opts. + +%%%=================================================================== +%%% Host config +%%%=================================================================== +transform_host_config(Opts) -> + Opts1 = lists:foldl(fun transform_host_config/2, [], Opts), + {HOpts, Opts2} = lists:mapfoldl( + fun({host_config, O}, Os) -> + {[O], Os}; + (O, Os) -> + {[], [O|Os]} + end, [], Opts1), + {AHOpts, Opts3} = lists:mapfoldl( + fun({append_host_config, O}, Os) -> + {[O], Os}; + (O, Os) -> + {[], [O|Os]} + end, [], Opts2), + HOpts1 = case collect_options(lists:flatten(HOpts)) of + [] -> + []; + HOs -> + [{host_config, + [{H, transform(O)} || {H, O} <- HOs]}] + end, + AHOpts1 = case collect_options(lists:flatten(AHOpts)) of + [] -> + []; + AHOs -> + [{append_host_config, + [{H, transform(O)} || {H, O} <- AHOs]}] + end, + HOpts1 ++ AHOpts1 ++ Opts3. + +transform_host_config({host_config, Host, HOpts}, Opts) -> + {AddOpts, HOpts1} = + lists:mapfoldl( + fun({{add, Opt}, Val}, Os) -> + {[{Opt, Val}], Os}; + (O, Os) -> + {[], [O|Os]} + end, [], HOpts), + [{append_host_config, [{Host, lists:flatten(AddOpts)}]}, + {host_config, [{Host, HOpts1}]}|Opts]; +transform_host_config(Opt, Opts) -> + [Opt|Opts]. + +%%%=================================================================== +%%% Top-level options +%%%=================================================================== +transform_globals(Opts) -> + lists:foldl(fun transform_globals/2, [], Opts). + +transform_globals(Opt, Opts) when Opt == override_global; + Opt == override_local; + Opt == override_acls -> + ?WARNING_MSG("Option '~ts' has no effect anymore", [Opt]), + Opts; +transform_globals({node_start, _}, Opts) -> + ?WARNING_MSG("Option 'node_start' has no effect anymore", []), + Opts; +transform_globals({iqdisc, {queues, N}}, Opts) -> + [{iqdisc, N}|Opts]; +transform_globals({define_macro, Macro, Val}, Opts) -> + [{define_macro, [{Macro, Val}]}|Opts]; +transform_globals(Opt, Opts) -> + [Opt|Opts]. + +%%%=================================================================== +%%% Certfiles +%%%=================================================================== +transform_certfiles(Opts) -> + lists:foldl(fun transform_certfiles/2, [], Opts). + +transform_certfiles({domain_certfile, Domain, CertFile}, Opts) -> + [{host_config, [{Domain, [{domain_certfile, CertFile}]}]}|Opts]; +transform_certfiles(Opt, Opts) -> + [Opt|Opts]. + +%%%=================================================================== +%%% Consult file +%%%=================================================================== +consult(File) -> + case file:consult(File) of + {ok, Terms} -> + include_config_files(Terms); + Err -> + Err + end. + +include_config_files(Terms) -> + include_config_files(Terms, []). + +include_config_files([], Res) -> + {ok, Res}; +include_config_files([{include_config_file, Filename} | Terms], Res) -> + include_config_files([{include_config_file, Filename, []} | Terms], Res); +include_config_files([{include_config_file, Filename, Options} | Terms], Res) -> + case consult(Filename) of + {ok, Included_terms} -> + Disallow = proplists:get_value(disallow, Options, []), + Included_terms2 = delete_disallowed(Disallow, Included_terms), + Allow_only = proplists:get_value(allow_only, Options, all), + Included_terms3 = keep_only_allowed(Allow_only, Included_terms2), + include_config_files(Terms, Res ++ Included_terms3); + Err -> + Err + end; +include_config_files([Term | Terms], Res) -> + include_config_files(Terms, Res ++ [Term]). + +delete_disallowed(Disallowed, Terms) -> + lists:foldl( + fun(Dis, Ldis) -> + delete_disallowed2(Dis, Ldis) + end, + Terms, + Disallowed). + +delete_disallowed2(Disallowed, [H|T]) -> + case element(1, H) of + Disallowed -> + delete_disallowed2(Disallowed, T); + _ -> + [H|delete_disallowed2(Disallowed, T)] + end; +delete_disallowed2(_, []) -> + []. + +keep_only_allowed(all, Terms) -> + Terms; +keep_only_allowed(Allowed, Terms) -> + {As, _NAs} = lists:partition( + fun(Term) -> + lists:member(element(1, Term), Allowed) + end, Terms), + As. + +%%%=================================================================== +%%% Aux functions +%%%=================================================================== +strings_to_binary([]) -> + []; +strings_to_binary(L) when is_list(L) -> + case is_string(L) of + true -> + list_to_binary(L); + false -> + strings_to_binary1(L) + end; +strings_to_binary({A, B, C, D}) when + is_integer(A), is_integer(B), is_integer(C), is_integer(D) -> + {A, B, C, D}; +strings_to_binary(T) when is_tuple(T) -> + list_to_tuple(strings_to_binary1(tuple_to_list(T))); +strings_to_binary(X) -> + X. + +strings_to_binary1([El|L]) -> + [strings_to_binary(El)|strings_to_binary1(L)]; +strings_to_binary1([]) -> + []; +strings_to_binary1(T) -> + T. + +is_string([C|T]) when (C >= 0) and (C =< 255) -> + is_string(T); +is_string([]) -> + true; +is_string(_) -> + false. + +b(S) -> + iolist_to_binary(S). diff --git a/src/ejabberd_option.erl b/src/ejabberd_option.erl new file mode 100644 index 000000000..775ea14c9 --- /dev/null +++ b/src/ejabberd_option.erl @@ -0,0 +1,1204 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(ejabberd_option). + +-export([access_rules/0, access_rules/1]). +-export([acl/0, acl/1]). +-export([acme/0]). +-export([allow_contrib_modules/0]). +-export([allow_multiple_connections/0, allow_multiple_connections/1]). +-export([anonymous_protocol/0, anonymous_protocol/1]). +-export([api_permissions/0]). +-export([append_host_config/0]). +-export([auth_cache_life_time/0]). +-export([auth_cache_missed/0]). +-export([auth_cache_size/0]). +-export([auth_external_user_exists_check/0, auth_external_user_exists_check/1]). +-export([auth_method/0, auth_method/1]). +-export([auth_opts/0, auth_opts/1]). +-export([auth_password_format/0, auth_password_format/1]). +-export([auth_password_types_hidden_in_sasl1/0, auth_password_types_hidden_in_sasl1/1]). +-export([auth_scram_hash/0, auth_scram_hash/1]). +-export([auth_stored_password_types/0, auth_stored_password_types/1]). +-export([auth_use_cache/0, auth_use_cache/1]). +-export([c2s_cafile/0, c2s_cafile/1]). +-export([c2s_ciphers/0, c2s_ciphers/1]). +-export([c2s_dhfile/0, c2s_dhfile/1]). +-export([c2s_protocol_options/0, c2s_protocol_options/1]). +-export([c2s_tls_compression/0, c2s_tls_compression/1]). +-export([ca_file/0]). +-export([cache_life_time/0, cache_life_time/1]). +-export([cache_missed/0, cache_missed/1]). +-export([cache_size/0, cache_size/1]). +-export([captcha_cmd/0]). +-export([captcha_host/0]). +-export([captcha_limit/0]). +-export([captcha_url/0]). +-export([certfiles/0]). +-export([cluster_backend/0]). +-export([cluster_nodes/0]). +-export([default_db/0, default_db/1]). +-export([default_ram_db/0, default_ram_db/1]). +-export([define_keyword/0, define_keyword/1]). +-export([define_macro/0]). +-export([disable_sasl_mechanisms/0, disable_sasl_mechanisms/1]). +-export([disable_sasl_scram_downgrade_protection/0, disable_sasl_scram_downgrade_protection/1]). +-export([domain_balancing/0]). +-export([ext_api_headers/0, ext_api_headers/1]). +-export([ext_api_http_pool_size/0, ext_api_http_pool_size/1]). +-export([ext_api_path_oauth/0]). +-export([ext_api_url/0, ext_api_url/1]). +-export([extauth_pool_name/0, extauth_pool_name/1]). +-export([extauth_pool_size/0, extauth_pool_size/1]). +-export([extauth_program/0, extauth_program/1]). +-export([fqdn/0]). +-export([hide_sensitive_log_data/0, hide_sensitive_log_data/1]). +-export([host_config/0]). +-export([hosts/0]). +-export([hosts_alias/0]). +-export([include_config_file/0, include_config_file/1]). +-export([install_contrib_modules/0]). +-export([jwt_auth_only_rule/0, jwt_auth_only_rule/1]). +-export([jwt_jid_field/0, jwt_jid_field/1]). +-export([jwt_key/0, jwt_key/1]). +-export([language/0, language/1]). +-export([ldap_backups/0, ldap_backups/1]). +-export([ldap_base/0, ldap_base/1]). +-export([ldap_deref_aliases/0, ldap_deref_aliases/1]). +-export([ldap_dn_filter/0, ldap_dn_filter/1]). +-export([ldap_encrypt/0, ldap_encrypt/1]). +-export([ldap_filter/0, ldap_filter/1]). +-export([ldap_password/0, ldap_password/1]). +-export([ldap_port/0, ldap_port/1]). +-export([ldap_rootdn/0, ldap_rootdn/1]). +-export([ldap_servers/0, ldap_servers/1]). +-export([ldap_tls_cacertfile/0, ldap_tls_cacertfile/1]). +-export([ldap_tls_certfile/0, ldap_tls_certfile/1]). +-export([ldap_tls_depth/0, ldap_tls_depth/1]). +-export([ldap_tls_verify/0, ldap_tls_verify/1]). +-export([ldap_uids/0, ldap_uids/1]). +-export([listen/0]). +-export([log_burst_limit_count/0]). +-export([log_burst_limit_window_time/0]). +-export([log_modules_fully/0]). +-export([log_rotate_count/0]). +-export([log_rotate_size/0]). +-export([loglevel/0]). +-export([max_fsm_queue/0, max_fsm_queue/1]). +-export([modules/0, modules/1]). +-export([negotiation_timeout/0]). +-export([net_ticktime/0]). +-export([new_sql_schema/0]). +-export([oauth_access/0, oauth_access/1]). +-export([oauth_cache_life_time/0]). +-export([oauth_cache_missed/0]). +-export([oauth_cache_rest_failure_life_time/0]). +-export([oauth_cache_size/0]). +-export([oauth_client_id_check/0, oauth_client_id_check/1]). +-export([oauth_db_type/0]). +-export([oauth_expire/0]). +-export([oauth_use_cache/0]). +-export([oom_killer/0]). +-export([oom_queue/0]). +-export([oom_watermark/0]). +-export([outgoing_s2s_families/0, outgoing_s2s_families/1]). +-export([outgoing_s2s_ipv4_address/0, outgoing_s2s_ipv4_address/1]). +-export([outgoing_s2s_ipv6_address/0, outgoing_s2s_ipv6_address/1]). +-export([outgoing_s2s_port/0, outgoing_s2s_port/1]). +-export([outgoing_s2s_timeout/0, outgoing_s2s_timeout/1]). +-export([pam_service/0, pam_service/1]). +-export([pam_userinfotype/0, pam_userinfotype/1]). +-export([pgsql_users_number_estimate/0, pgsql_users_number_estimate/1]). +-export([queue_dir/0]). +-export([queue_type/0, queue_type/1]). +-export([redis_connect_timeout/0]). +-export([redis_db/0]). +-export([redis_password/0]). +-export([redis_pool_size/0]). +-export([redis_port/0]). +-export([redis_queue_type/0]). +-export([redis_server/0]). +-export([registration_timeout/0]). +-export([resource_conflict/0, resource_conflict/1]). +-export([rest_proxy/0, rest_proxy/1]). +-export([rest_proxy_password/0, rest_proxy_password/1]). +-export([rest_proxy_port/0, rest_proxy_port/1]). +-export([rest_proxy_username/0, rest_proxy_username/1]). +-export([router_cache_life_time/0]). +-export([router_cache_missed/0]). +-export([router_cache_size/0]). +-export([router_db_type/0]). +-export([router_use_cache/0]). +-export([rpc_timeout/0]). +-export([s2s_access/0, s2s_access/1]). +-export([s2s_cafile/0, s2s_cafile/1]). +-export([s2s_ciphers/0, s2s_ciphers/1]). +-export([s2s_dhfile/0, s2s_dhfile/1]). +-export([s2s_dns_retries/0, s2s_dns_retries/1]). +-export([s2s_dns_timeout/0, s2s_dns_timeout/1]). +-export([s2s_max_retry_delay/0]). +-export([s2s_protocol_options/0, s2s_protocol_options/1]). +-export([s2s_queue_type/0, s2s_queue_type/1]). +-export([s2s_timeout/0, s2s_timeout/1]). +-export([s2s_tls_compression/0, s2s_tls_compression/1]). +-export([s2s_use_starttls/0, s2s_use_starttls/1]). +-export([s2s_zlib/0, s2s_zlib/1]). +-export([shaper/0]). +-export([shaper_rules/0, shaper_rules/1]). +-export([sm_cache_life_time/0]). +-export([sm_cache_missed/0]). +-export([sm_cache_size/0]). +-export([sm_db_type/0, sm_db_type/1]). +-export([sm_use_cache/0, sm_use_cache/1]). +-export([sql_connect_timeout/0, sql_connect_timeout/1]). +-export([sql_database/0, sql_database/1]). +-export([sql_flags/0, sql_flags/1]). +-export([sql_keepalive_interval/0, sql_keepalive_interval/1]). +-export([sql_odbc_driver/0, sql_odbc_driver/1]). +-export([sql_password/0, sql_password/1]). +-export([sql_pool_size/0, sql_pool_size/1]). +-export([sql_port/0, sql_port/1]). +-export([sql_prepared_statements/0, sql_prepared_statements/1]). +-export([sql_query_timeout/0, sql_query_timeout/1]). +-export([sql_queue_type/0, sql_queue_type/1]). +-export([sql_server/0, sql_server/1]). +-export([sql_ssl/0, sql_ssl/1]). +-export([sql_ssl_cafile/0, sql_ssl_cafile/1]). +-export([sql_ssl_certfile/0, sql_ssl_certfile/1]). +-export([sql_ssl_verify/0, sql_ssl_verify/1]). +-export([sql_start_interval/0, sql_start_interval/1]). +-export([sql_type/0, sql_type/1]). +-export([sql_username/0, sql_username/1]). +-export([trusted_proxies/0]). +-export([update_sql_schema/0]). +-export([update_sql_schema_timeout/0, update_sql_schema_timeout/1]). +-export([use_cache/0, use_cache/1]). +-export([validate_stream/0]). +-export([version/0]). +-export([websocket_origin/0]). +-export([websocket_ping_interval/0]). +-export([websocket_timeout/0]). + +-spec access_rules() -> [{atom(),acl:access()}]. +access_rules() -> + access_rules(global). +-spec access_rules(global | binary()) -> [{atom(),acl:access()}]. +access_rules(Host) -> + ejabberd_config:get_option({access_rules, Host}). + +-spec acl() -> [{atom(),[acl:acl_rule()]}]. +acl() -> + acl(global). +-spec acl(global | binary()) -> [{atom(),[acl:acl_rule()]}]. +acl(Host) -> + ejabberd_config:get_option({acl, Host}). + +-spec acme() -> #{'auto'=>boolean(), 'ca_url'=>binary(), 'cert_type'=>'ec' | 'rsa', 'contact'=>[binary()]}. +acme() -> + ejabberd_config:get_option({acme, global}). + +-spec allow_contrib_modules() -> boolean(). +allow_contrib_modules() -> + ejabberd_config:get_option({allow_contrib_modules, global}). + +-spec allow_multiple_connections() -> boolean(). +allow_multiple_connections() -> + allow_multiple_connections(global). +-spec allow_multiple_connections(global | binary()) -> boolean(). +allow_multiple_connections(Host) -> + ejabberd_config:get_option({allow_multiple_connections, Host}). + +-spec anonymous_protocol() -> 'both' | 'login_anon' | 'sasl_anon'. +anonymous_protocol() -> + anonymous_protocol(global). +-spec anonymous_protocol(global | binary()) -> 'both' | 'login_anon' | 'sasl_anon'. +anonymous_protocol(Host) -> + ejabberd_config:get_option({anonymous_protocol, Host}). + +-spec api_permissions() -> [ejabberd_access_permissions:permission()]. +api_permissions() -> + ejabberd_config:get_option({api_permissions, global}). + +-spec append_host_config() -> [{binary(),any()}]. +append_host_config() -> + ejabberd_config:get_option({append_host_config, global}). + +-spec auth_cache_life_time() -> 'infinity' | pos_integer(). +auth_cache_life_time() -> + ejabberd_config:get_option({auth_cache_life_time, global}). + +-spec auth_cache_missed() -> boolean(). +auth_cache_missed() -> + ejabberd_config:get_option({auth_cache_missed, global}). + +-spec auth_cache_size() -> 'infinity' | pos_integer(). +auth_cache_size() -> + ejabberd_config:get_option({auth_cache_size, global}). + +-spec auth_external_user_exists_check() -> boolean(). +auth_external_user_exists_check() -> + auth_external_user_exists_check(global). +-spec auth_external_user_exists_check(global | binary()) -> boolean(). +auth_external_user_exists_check(Host) -> + ejabberd_config:get_option({auth_external_user_exists_check, Host}). + +-spec auth_method() -> [atom()]. +auth_method() -> + auth_method(global). +-spec auth_method(global | binary()) -> [atom()]. +auth_method(Host) -> + ejabberd_config:get_option({auth_method, Host}). + +-spec auth_opts() -> [{any(),any()}]. +auth_opts() -> + auth_opts(global). +-spec auth_opts(global | binary()) -> [{any(),any()}]. +auth_opts(Host) -> + ejabberd_config:get_option({auth_opts, Host}). + +-spec auth_password_format() -> 'plain' | 'scram'. +auth_password_format() -> + auth_password_format(global). +-spec auth_password_format(global | binary()) -> 'plain' | 'scram'. +auth_password_format(Host) -> + ejabberd_config:get_option({auth_password_format, Host}). + +-spec auth_password_types_hidden_in_sasl1() -> ['plain' | 'scram_sha1' | 'scram_sha256' | 'scram_sha512']. +auth_password_types_hidden_in_sasl1() -> + auth_password_types_hidden_in_sasl1(global). +-spec auth_password_types_hidden_in_sasl1(global | binary()) -> ['plain' | 'scram_sha1' | 'scram_sha256' | 'scram_sha512']. +auth_password_types_hidden_in_sasl1(Host) -> + ejabberd_config:get_option({auth_password_types_hidden_in_sasl1, Host}). + +-spec auth_scram_hash() -> 'sha' | 'sha256' | 'sha512'. +auth_scram_hash() -> + auth_scram_hash(global). +-spec auth_scram_hash(global | binary()) -> 'sha' | 'sha256' | 'sha512'. +auth_scram_hash(Host) -> + ejabberd_config:get_option({auth_scram_hash, Host}). + +-spec auth_stored_password_types() -> ['plain' | 'scram_sha1' | 'scram_sha256' | 'scram_sha512']. +auth_stored_password_types() -> + auth_stored_password_types(global). +-spec auth_stored_password_types(global | binary()) -> ['plain' | 'scram_sha1' | 'scram_sha256' | 'scram_sha512']. +auth_stored_password_types(Host) -> + ejabberd_config:get_option({auth_stored_password_types, Host}). + +-spec auth_use_cache() -> boolean(). +auth_use_cache() -> + auth_use_cache(global). +-spec auth_use_cache(global | binary()) -> boolean(). +auth_use_cache(Host) -> + ejabberd_config:get_option({auth_use_cache, Host}). + +-spec c2s_cafile() -> 'undefined' | binary(). +c2s_cafile() -> + c2s_cafile(global). +-spec c2s_cafile(global | binary()) -> 'undefined' | binary(). +c2s_cafile(Host) -> + ejabberd_config:get_option({c2s_cafile, Host}). + +-spec c2s_ciphers() -> 'undefined' | binary(). +c2s_ciphers() -> + c2s_ciphers(global). +-spec c2s_ciphers(global | binary()) -> 'undefined' | binary(). +c2s_ciphers(Host) -> + ejabberd_config:get_option({c2s_ciphers, Host}). + +-spec c2s_dhfile() -> 'undefined' | binary(). +c2s_dhfile() -> + c2s_dhfile(global). +-spec c2s_dhfile(global | binary()) -> 'undefined' | binary(). +c2s_dhfile(Host) -> + ejabberd_config:get_option({c2s_dhfile, Host}). + +-spec c2s_protocol_options() -> 'undefined' | binary(). +c2s_protocol_options() -> + c2s_protocol_options(global). +-spec c2s_protocol_options(global | binary()) -> 'undefined' | binary(). +c2s_protocol_options(Host) -> + ejabberd_config:get_option({c2s_protocol_options, Host}). + +-spec c2s_tls_compression() -> 'false' | 'true' | 'undefined'. +c2s_tls_compression() -> + c2s_tls_compression(global). +-spec c2s_tls_compression(global | binary()) -> 'false' | 'true' | 'undefined'. +c2s_tls_compression(Host) -> + ejabberd_config:get_option({c2s_tls_compression, Host}). + +-spec ca_file() -> binary(). +ca_file() -> + ejabberd_config:get_option({ca_file, global}). + +-spec cache_life_time() -> 'infinity' | pos_integer(). +cache_life_time() -> + cache_life_time(global). +-spec cache_life_time(global | binary()) -> 'infinity' | pos_integer(). +cache_life_time(Host) -> + ejabberd_config:get_option({cache_life_time, Host}). + +-spec cache_missed() -> boolean(). +cache_missed() -> + cache_missed(global). +-spec cache_missed(global | binary()) -> boolean(). +cache_missed(Host) -> + ejabberd_config:get_option({cache_missed, Host}). + +-spec cache_size() -> 'infinity' | pos_integer(). +cache_size() -> + cache_size(global). +-spec cache_size(global | binary()) -> 'infinity' | pos_integer(). +cache_size(Host) -> + ejabberd_config:get_option({cache_size, Host}). + +-spec captcha_cmd() -> 'undefined' | binary(). +captcha_cmd() -> + ejabberd_config:get_option({captcha_cmd, global}). + +-spec captcha_host() -> binary(). +captcha_host() -> + ejabberd_config:get_option({captcha_host, global}). + +-spec captcha_limit() -> 'infinity' | pos_integer(). +captcha_limit() -> + ejabberd_config:get_option({captcha_limit, global}). + +-spec captcha_url() -> 'auto' | 'undefined' | binary(). +captcha_url() -> + ejabberd_config:get_option({captcha_url, global}). + +-spec certfiles() -> 'undefined' | [binary()]. +certfiles() -> + ejabberd_config:get_option({certfiles, global}). + +-spec cluster_backend() -> atom(). +cluster_backend() -> + ejabberd_config:get_option({cluster_backend, global}). + +-spec cluster_nodes() -> [atom()]. +cluster_nodes() -> + ejabberd_config:get_option({cluster_nodes, global}). + +-spec default_db() -> 'mnesia' | 'sql'. +default_db() -> + default_db(global). +-spec default_db(global | binary()) -> 'mnesia' | 'sql'. +default_db(Host) -> + ejabberd_config:get_option({default_db, Host}). + +-spec default_ram_db() -> 'mnesia' | 'redis' | 'sql'. +default_ram_db() -> + default_ram_db(global). +-spec default_ram_db(global | binary()) -> 'mnesia' | 'redis' | 'sql'. +default_ram_db(Host) -> + ejabberd_config:get_option({default_ram_db, Host}). + +-spec define_keyword() -> any(). +define_keyword() -> + define_keyword(global). +-spec define_keyword(global | binary()) -> any(). +define_keyword(Host) -> + ejabberd_config:get_option({define_keyword, Host}). + +-spec define_macro() -> any(). +define_macro() -> + ejabberd_config:get_option({define_macro, global}). + +-spec disable_sasl_mechanisms() -> [binary()]. +disable_sasl_mechanisms() -> + disable_sasl_mechanisms(global). +-spec disable_sasl_mechanisms(global | binary()) -> [binary()]. +disable_sasl_mechanisms(Host) -> + ejabberd_config:get_option({disable_sasl_mechanisms, Host}). + +-spec disable_sasl_scram_downgrade_protection() -> boolean(). +disable_sasl_scram_downgrade_protection() -> + disable_sasl_scram_downgrade_protection(global). +-spec disable_sasl_scram_downgrade_protection(global | binary()) -> boolean(). +disable_sasl_scram_downgrade_protection(Host) -> + ejabberd_config:get_option({disable_sasl_scram_downgrade_protection, Host}). + +-spec domain_balancing() -> #{binary()=>#{'component_number'=>1..1114111, 'type'=>'bare_destination' | 'bare_source' | 'destination' | 'random' | 'source'}}. +domain_balancing() -> + ejabberd_config:get_option({domain_balancing, global}). + +-spec ext_api_headers() -> binary(). +ext_api_headers() -> + ext_api_headers(global). +-spec ext_api_headers(global | binary()) -> binary(). +ext_api_headers(Host) -> + ejabberd_config:get_option({ext_api_headers, Host}). + +-spec ext_api_http_pool_size() -> pos_integer(). +ext_api_http_pool_size() -> + ext_api_http_pool_size(global). +-spec ext_api_http_pool_size(global | binary()) -> pos_integer(). +ext_api_http_pool_size(Host) -> + ejabberd_config:get_option({ext_api_http_pool_size, Host}). + +-spec ext_api_path_oauth() -> binary(). +ext_api_path_oauth() -> + ejabberd_config:get_option({ext_api_path_oauth, global}). + +-spec ext_api_url() -> binary(). +ext_api_url() -> + ext_api_url(global). +-spec ext_api_url(global | binary()) -> binary(). +ext_api_url(Host) -> + ejabberd_config:get_option({ext_api_url, Host}). + +-spec extauth_pool_name() -> 'undefined' | binary(). +extauth_pool_name() -> + extauth_pool_name(global). +-spec extauth_pool_name(global | binary()) -> 'undefined' | binary(). +extauth_pool_name(Host) -> + ejabberd_config:get_option({extauth_pool_name, Host}). + +-spec extauth_pool_size() -> 'undefined' | pos_integer(). +extauth_pool_size() -> + extauth_pool_size(global). +-spec extauth_pool_size(global | binary()) -> 'undefined' | pos_integer(). +extauth_pool_size(Host) -> + ejabberd_config:get_option({extauth_pool_size, Host}). + +-spec extauth_program() -> 'undefined' | string(). +extauth_program() -> + extauth_program(global). +-spec extauth_program(global | binary()) -> 'undefined' | string(). +extauth_program(Host) -> + ejabberd_config:get_option({extauth_program, Host}). + +-spec fqdn() -> [binary()]. +fqdn() -> + ejabberd_config:get_option({fqdn, global}). + +-spec hide_sensitive_log_data() -> boolean(). +hide_sensitive_log_data() -> + hide_sensitive_log_data(global). +-spec hide_sensitive_log_data(global | binary()) -> boolean(). +hide_sensitive_log_data(Host) -> + ejabberd_config:get_option({hide_sensitive_log_data, Host}). + +-spec host_config() -> [{binary(),any()}]. +host_config() -> + ejabberd_config:get_option({host_config, global}). + +-spec hosts() -> [binary(),...]. +hosts() -> + ejabberd_config:get_option({hosts, global}). + +-spec hosts_alias() -> [{binary(),binary()}]. +hosts_alias() -> + ejabberd_config:get_option({hosts_alias, global}). + +-spec include_config_file() -> any(). +include_config_file() -> + include_config_file(global). +-spec include_config_file(global | binary()) -> any(). +include_config_file(Host) -> + ejabberd_config:get_option({include_config_file, Host}). + +-spec install_contrib_modules() -> [atom()]. +install_contrib_modules() -> + ejabberd_config:get_option({install_contrib_modules, global}). + +-spec jwt_auth_only_rule() -> atom(). +jwt_auth_only_rule() -> + jwt_auth_only_rule(global). +-spec jwt_auth_only_rule(global | binary()) -> atom(). +jwt_auth_only_rule(Host) -> + ejabberd_config:get_option({jwt_auth_only_rule, Host}). + +-spec jwt_jid_field() -> binary(). +jwt_jid_field() -> + jwt_jid_field(global). +-spec jwt_jid_field(global | binary()) -> binary(). +jwt_jid_field(Host) -> + ejabberd_config:get_option({jwt_jid_field, Host}). + +-spec jwt_key() -> jose_jwk:key() | 'undefined'. +jwt_key() -> + jwt_key(global). +-spec jwt_key(global | binary()) -> jose_jwk:key() | 'undefined'. +jwt_key(Host) -> + ejabberd_config:get_option({jwt_key, Host}). + +-spec language() -> binary(). +language() -> + language(global). +-spec language(global | binary()) -> binary(). +language(Host) -> + ejabberd_config:get_option({language, Host}). + +-spec ldap_backups() -> [binary()]. +ldap_backups() -> + ldap_backups(global). +-spec ldap_backups(global | binary()) -> [binary()]. +ldap_backups(Host) -> + ejabberd_config:get_option({ldap_backups, Host}). + +-spec ldap_base() -> binary(). +ldap_base() -> + ldap_base(global). +-spec ldap_base(global | binary()) -> binary(). +ldap_base(Host) -> + ejabberd_config:get_option({ldap_base, Host}). + +-spec ldap_deref_aliases() -> 'always' | 'finding' | 'never' | 'searching'. +ldap_deref_aliases() -> + ldap_deref_aliases(global). +-spec ldap_deref_aliases(global | binary()) -> 'always' | 'finding' | 'never' | 'searching'. +ldap_deref_aliases(Host) -> + ejabberd_config:get_option({ldap_deref_aliases, Host}). + +-spec ldap_dn_filter() -> {binary(),[binary()]}. +ldap_dn_filter() -> + ldap_dn_filter(global). +-spec ldap_dn_filter(global | binary()) -> {binary(),[binary()]}. +ldap_dn_filter(Host) -> + ejabberd_config:get_option({ldap_dn_filter, Host}). + +-spec ldap_encrypt() -> 'none' | 'starttls' | 'tls'. +ldap_encrypt() -> + ldap_encrypt(global). +-spec ldap_encrypt(global | binary()) -> 'none' | 'starttls' | 'tls'. +ldap_encrypt(Host) -> + ejabberd_config:get_option({ldap_encrypt, Host}). + +-spec ldap_filter() -> binary(). +ldap_filter() -> + ldap_filter(global). +-spec ldap_filter(global | binary()) -> binary(). +ldap_filter(Host) -> + ejabberd_config:get_option({ldap_filter, Host}). + +-spec ldap_password() -> binary(). +ldap_password() -> + ldap_password(global). +-spec ldap_password(global | binary()) -> binary(). +ldap_password(Host) -> + ejabberd_config:get_option({ldap_password, Host}). + +-spec ldap_port() -> 1..1114111. +ldap_port() -> + ldap_port(global). +-spec ldap_port(global | binary()) -> 1..1114111. +ldap_port(Host) -> + ejabberd_config:get_option({ldap_port, Host}). + +-spec ldap_rootdn() -> binary(). +ldap_rootdn() -> + ldap_rootdn(global). +-spec ldap_rootdn(global | binary()) -> binary(). +ldap_rootdn(Host) -> + ejabberd_config:get_option({ldap_rootdn, Host}). + +-spec ldap_servers() -> [binary()]. +ldap_servers() -> + ldap_servers(global). +-spec ldap_servers(global | binary()) -> [binary()]. +ldap_servers(Host) -> + ejabberd_config:get_option({ldap_servers, Host}). + +-spec ldap_tls_cacertfile() -> 'undefined' | binary(). +ldap_tls_cacertfile() -> + ldap_tls_cacertfile(global). +-spec ldap_tls_cacertfile(global | binary()) -> 'undefined' | binary(). +ldap_tls_cacertfile(Host) -> + ejabberd_config:get_option({ldap_tls_cacertfile, Host}). + +-spec ldap_tls_certfile() -> 'undefined' | binary(). +ldap_tls_certfile() -> + ldap_tls_certfile(global). +-spec ldap_tls_certfile(global | binary()) -> 'undefined' | binary(). +ldap_tls_certfile(Host) -> + ejabberd_config:get_option({ldap_tls_certfile, Host}). + +-spec ldap_tls_depth() -> 'undefined' | non_neg_integer(). +ldap_tls_depth() -> + ldap_tls_depth(global). +-spec ldap_tls_depth(global | binary()) -> 'undefined' | non_neg_integer(). +ldap_tls_depth(Host) -> + ejabberd_config:get_option({ldap_tls_depth, Host}). + +-spec ldap_tls_verify() -> 'false' | 'hard' | 'soft'. +ldap_tls_verify() -> + ldap_tls_verify(global). +-spec ldap_tls_verify(global | binary()) -> 'false' | 'hard' | 'soft'. +ldap_tls_verify(Host) -> + ejabberd_config:get_option({ldap_tls_verify, Host}). + +-spec ldap_uids() -> [{binary(),binary()}]. +ldap_uids() -> + ldap_uids(global). +-spec ldap_uids(global | binary()) -> [{binary(),binary()}]. +ldap_uids(Host) -> + ejabberd_config:get_option({ldap_uids, Host}). + +-spec listen() -> [ejabberd_listener:listener()]. +listen() -> + ejabberd_config:get_option({listen, global}). + +-spec log_burst_limit_count() -> pos_integer(). +log_burst_limit_count() -> + ejabberd_config:get_option({log_burst_limit_count, global}). + +-spec log_burst_limit_window_time() -> pos_integer(). +log_burst_limit_window_time() -> + ejabberd_config:get_option({log_burst_limit_window_time, global}). + +-spec log_modules_fully() -> [atom()]. +log_modules_fully() -> + ejabberd_config:get_option({log_modules_fully, global}). + +-spec log_rotate_count() -> non_neg_integer(). +log_rotate_count() -> + ejabberd_config:get_option({log_rotate_count, global}). + +-spec log_rotate_size() -> 'infinity' | pos_integer(). +log_rotate_size() -> + ejabberd_config:get_option({log_rotate_size, global}). + +-spec loglevel() -> ejabberd_logger:loglevel(). +loglevel() -> + ejabberd_config:get_option({loglevel, global}). + +-spec max_fsm_queue() -> 'undefined' | pos_integer(). +max_fsm_queue() -> + max_fsm_queue(global). +-spec max_fsm_queue(global | binary()) -> 'undefined' | pos_integer(). +max_fsm_queue(Host) -> + ejabberd_config:get_option({max_fsm_queue, Host}). + +-spec modules() -> [{module(),gen_mod:opts(),integer()}]. +modules() -> + modules(global). +-spec modules(global | binary()) -> [{module(),gen_mod:opts(),integer()}]. +modules(Host) -> + ejabberd_config:get_option({modules, Host}). + +-spec negotiation_timeout() -> pos_integer(). +negotiation_timeout() -> + ejabberd_config:get_option({negotiation_timeout, global}). + +-spec net_ticktime() -> pos_integer(). +net_ticktime() -> + ejabberd_config:get_option({net_ticktime, global}). + +-spec new_sql_schema() -> boolean(). +new_sql_schema() -> + ejabberd_config:get_option({new_sql_schema, global}). + +-spec oauth_access() -> 'none' | acl:acl(). +oauth_access() -> + oauth_access(global). +-spec oauth_access(global | binary()) -> 'none' | acl:acl(). +oauth_access(Host) -> + ejabberd_config:get_option({oauth_access, Host}). + +-spec oauth_cache_life_time() -> 'infinity' | pos_integer(). +oauth_cache_life_time() -> + ejabberd_config:get_option({oauth_cache_life_time, global}). + +-spec oauth_cache_missed() -> boolean(). +oauth_cache_missed() -> + ejabberd_config:get_option({oauth_cache_missed, global}). + +-spec oauth_cache_rest_failure_life_time() -> 'infinity' | pos_integer(). +oauth_cache_rest_failure_life_time() -> + ejabberd_config:get_option({oauth_cache_rest_failure_life_time, global}). + +-spec oauth_cache_size() -> 'infinity' | pos_integer(). +oauth_cache_size() -> + ejabberd_config:get_option({oauth_cache_size, global}). + +-spec oauth_client_id_check() -> 'allow' | 'db' | 'deny'. +oauth_client_id_check() -> + oauth_client_id_check(global). +-spec oauth_client_id_check(global | binary()) -> 'allow' | 'db' | 'deny'. +oauth_client_id_check(Host) -> + ejabberd_config:get_option({oauth_client_id_check, Host}). + +-spec oauth_db_type() -> atom(). +oauth_db_type() -> + ejabberd_config:get_option({oauth_db_type, global}). + +-spec oauth_expire() -> pos_integer(). +oauth_expire() -> + ejabberd_config:get_option({oauth_expire, global}). + +-spec oauth_use_cache() -> boolean(). +oauth_use_cache() -> + ejabberd_config:get_option({oauth_use_cache, global}). + +-spec oom_killer() -> boolean(). +oom_killer() -> + ejabberd_config:get_option({oom_killer, global}). + +-spec oom_queue() -> pos_integer(). +oom_queue() -> + ejabberd_config:get_option({oom_queue, global}). + +-spec oom_watermark() -> 1..255. +oom_watermark() -> + ejabberd_config:get_option({oom_watermark, global}). + +-spec outgoing_s2s_families() -> ['inet' | 'inet6',...]. +outgoing_s2s_families() -> + outgoing_s2s_families(global). +-spec outgoing_s2s_families(global | binary()) -> ['inet' | 'inet6',...]. +outgoing_s2s_families(Host) -> + ejabberd_config:get_option({outgoing_s2s_families, Host}). + +-spec outgoing_s2s_ipv4_address() -> 'undefined' | inet:ip4_address(). +outgoing_s2s_ipv4_address() -> + outgoing_s2s_ipv4_address(global). +-spec outgoing_s2s_ipv4_address(global | binary()) -> 'undefined' | inet:ip4_address(). +outgoing_s2s_ipv4_address(Host) -> + ejabberd_config:get_option({outgoing_s2s_ipv4_address, Host}). + +-spec outgoing_s2s_ipv6_address() -> 'undefined' | inet:ip6_address(). +outgoing_s2s_ipv6_address() -> + outgoing_s2s_ipv6_address(global). +-spec outgoing_s2s_ipv6_address(global | binary()) -> 'undefined' | inet:ip6_address(). +outgoing_s2s_ipv6_address(Host) -> + ejabberd_config:get_option({outgoing_s2s_ipv6_address, Host}). + +-spec outgoing_s2s_port() -> 1..1114111. +outgoing_s2s_port() -> + outgoing_s2s_port(global). +-spec outgoing_s2s_port(global | binary()) -> 1..1114111. +outgoing_s2s_port(Host) -> + ejabberd_config:get_option({outgoing_s2s_port, Host}). + +-spec outgoing_s2s_timeout() -> 'infinity' | pos_integer(). +outgoing_s2s_timeout() -> + outgoing_s2s_timeout(global). +-spec outgoing_s2s_timeout(global | binary()) -> 'infinity' | pos_integer(). +outgoing_s2s_timeout(Host) -> + ejabberd_config:get_option({outgoing_s2s_timeout, Host}). + +-spec pam_service() -> binary(). +pam_service() -> + pam_service(global). +-spec pam_service(global | binary()) -> binary(). +pam_service(Host) -> + ejabberd_config:get_option({pam_service, Host}). + +-spec pam_userinfotype() -> 'jid' | 'username'. +pam_userinfotype() -> + pam_userinfotype(global). +-spec pam_userinfotype(global | binary()) -> 'jid' | 'username'. +pam_userinfotype(Host) -> + ejabberd_config:get_option({pam_userinfotype, Host}). + +-spec pgsql_users_number_estimate() -> boolean(). +pgsql_users_number_estimate() -> + pgsql_users_number_estimate(global). +-spec pgsql_users_number_estimate(global | binary()) -> boolean(). +pgsql_users_number_estimate(Host) -> + ejabberd_config:get_option({pgsql_users_number_estimate, Host}). + +-spec queue_dir() -> 'undefined' | binary(). +queue_dir() -> + ejabberd_config:get_option({queue_dir, global}). + +-spec queue_type() -> 'file' | 'ram'. +queue_type() -> + queue_type(global). +-spec queue_type(global | binary()) -> 'file' | 'ram'. +queue_type(Host) -> + ejabberd_config:get_option({queue_type, Host}). + +-spec redis_connect_timeout() -> pos_integer(). +redis_connect_timeout() -> + ejabberd_config:get_option({redis_connect_timeout, global}). + +-spec redis_db() -> non_neg_integer(). +redis_db() -> + ejabberd_config:get_option({redis_db, global}). + +-spec redis_password() -> string(). +redis_password() -> + ejabberd_config:get_option({redis_password, global}). + +-spec redis_pool_size() -> pos_integer(). +redis_pool_size() -> + ejabberd_config:get_option({redis_pool_size, global}). + +-spec redis_port() -> 1..1114111. +redis_port() -> + ejabberd_config:get_option({redis_port, global}). + +-spec redis_queue_type() -> 'file' | 'ram'. +redis_queue_type() -> + ejabberd_config:get_option({redis_queue_type, global}). + +-spec redis_server() -> string(). +redis_server() -> + ejabberd_config:get_option({redis_server, global}). + +-spec registration_timeout() -> 'infinity' | pos_integer(). +registration_timeout() -> + ejabberd_config:get_option({registration_timeout, global}). + +-spec resource_conflict() -> 'acceptnew' | 'closenew' | 'closeold' | 'setresource'. +resource_conflict() -> + resource_conflict(global). +-spec resource_conflict(global | binary()) -> 'acceptnew' | 'closenew' | 'closeold' | 'setresource'. +resource_conflict(Host) -> + ejabberd_config:get_option({resource_conflict, Host}). + +-spec rest_proxy() -> binary(). +rest_proxy() -> + rest_proxy(global). +-spec rest_proxy(global | binary()) -> binary(). +rest_proxy(Host) -> + ejabberd_config:get_option({rest_proxy, Host}). + +-spec rest_proxy_password() -> string(). +rest_proxy_password() -> + rest_proxy_password(global). +-spec rest_proxy_password(global | binary()) -> string(). +rest_proxy_password(Host) -> + ejabberd_config:get_option({rest_proxy_password, Host}). + +-spec rest_proxy_port() -> char(). +rest_proxy_port() -> + rest_proxy_port(global). +-spec rest_proxy_port(global | binary()) -> char(). +rest_proxy_port(Host) -> + ejabberd_config:get_option({rest_proxy_port, Host}). + +-spec rest_proxy_username() -> string(). +rest_proxy_username() -> + rest_proxy_username(global). +-spec rest_proxy_username(global | binary()) -> string(). +rest_proxy_username(Host) -> + ejabberd_config:get_option({rest_proxy_username, Host}). + +-spec router_cache_life_time() -> 'infinity' | pos_integer(). +router_cache_life_time() -> + ejabberd_config:get_option({router_cache_life_time, global}). + +-spec router_cache_missed() -> boolean(). +router_cache_missed() -> + ejabberd_config:get_option({router_cache_missed, global}). + +-spec router_cache_size() -> 'infinity' | pos_integer(). +router_cache_size() -> + ejabberd_config:get_option({router_cache_size, global}). + +-spec router_db_type() -> atom(). +router_db_type() -> + ejabberd_config:get_option({router_db_type, global}). + +-spec router_use_cache() -> boolean(). +router_use_cache() -> + ejabberd_config:get_option({router_use_cache, global}). + +-spec rpc_timeout() -> pos_integer(). +rpc_timeout() -> + ejabberd_config:get_option({rpc_timeout, global}). + +-spec s2s_access() -> 'all' | acl:acl(). +s2s_access() -> + s2s_access(global). +-spec s2s_access(global | binary()) -> 'all' | acl:acl(). +s2s_access(Host) -> + ejabberd_config:get_option({s2s_access, Host}). + +-spec s2s_cafile() -> 'undefined' | binary(). +s2s_cafile() -> + s2s_cafile(global). +-spec s2s_cafile(global | binary()) -> 'undefined' | binary(). +s2s_cafile(Host) -> + ejabberd_config:get_option({s2s_cafile, Host}). + +-spec s2s_ciphers() -> 'undefined' | binary(). +s2s_ciphers() -> + s2s_ciphers(global). +-spec s2s_ciphers(global | binary()) -> 'undefined' | binary(). +s2s_ciphers(Host) -> + ejabberd_config:get_option({s2s_ciphers, Host}). + +-spec s2s_dhfile() -> 'undefined' | binary(). +s2s_dhfile() -> + s2s_dhfile(global). +-spec s2s_dhfile(global | binary()) -> 'undefined' | binary(). +s2s_dhfile(Host) -> + ejabberd_config:get_option({s2s_dhfile, Host}). + +-spec s2s_dns_retries() -> non_neg_integer(). +s2s_dns_retries() -> + s2s_dns_retries(global). +-spec s2s_dns_retries(global | binary()) -> non_neg_integer(). +s2s_dns_retries(Host) -> + ejabberd_config:get_option({s2s_dns_retries, Host}). + +-spec s2s_dns_timeout() -> 'infinity' | pos_integer(). +s2s_dns_timeout() -> + s2s_dns_timeout(global). +-spec s2s_dns_timeout(global | binary()) -> 'infinity' | pos_integer(). +s2s_dns_timeout(Host) -> + ejabberd_config:get_option({s2s_dns_timeout, Host}). + +-spec s2s_max_retry_delay() -> pos_integer(). +s2s_max_retry_delay() -> + ejabberd_config:get_option({s2s_max_retry_delay, global}). + +-spec s2s_protocol_options() -> 'undefined' | binary(). +s2s_protocol_options() -> + s2s_protocol_options(global). +-spec s2s_protocol_options(global | binary()) -> 'undefined' | binary(). +s2s_protocol_options(Host) -> + ejabberd_config:get_option({s2s_protocol_options, Host}). + +-spec s2s_queue_type() -> 'file' | 'ram'. +s2s_queue_type() -> + s2s_queue_type(global). +-spec s2s_queue_type(global | binary()) -> 'file' | 'ram'. +s2s_queue_type(Host) -> + ejabberd_config:get_option({s2s_queue_type, Host}). + +-spec s2s_timeout() -> 'infinity' | pos_integer(). +s2s_timeout() -> + s2s_timeout(global). +-spec s2s_timeout(global | binary()) -> 'infinity' | pos_integer(). +s2s_timeout(Host) -> + ejabberd_config:get_option({s2s_timeout, Host}). + +-spec s2s_tls_compression() -> 'false' | 'true' | 'undefined'. +s2s_tls_compression() -> + s2s_tls_compression(global). +-spec s2s_tls_compression(global | binary()) -> 'false' | 'true' | 'undefined'. +s2s_tls_compression(Host) -> + ejabberd_config:get_option({s2s_tls_compression, Host}). + +-spec s2s_use_starttls() -> 'false' | 'optional' | 'required' | 'true'. +s2s_use_starttls() -> + s2s_use_starttls(global). +-spec s2s_use_starttls(global | binary()) -> 'false' | 'optional' | 'required' | 'true'. +s2s_use_starttls(Host) -> + ejabberd_config:get_option({s2s_use_starttls, Host}). + +-spec s2s_zlib() -> boolean(). +s2s_zlib() -> + s2s_zlib(global). +-spec s2s_zlib(global | binary()) -> boolean(). +s2s_zlib(Host) -> + ejabberd_config:get_option({s2s_zlib, Host}). + +-spec shaper() -> #{atom()=>ejabberd_shaper:shaper_rate()}. +shaper() -> + ejabberd_config:get_option({shaper, global}). + +-spec shaper_rules() -> [{atom(),[ejabberd_shaper:shaper_rule()]}]. +shaper_rules() -> + shaper_rules(global). +-spec shaper_rules(global | binary()) -> [{atom(),[ejabberd_shaper:shaper_rule()]}]. +shaper_rules(Host) -> + ejabberd_config:get_option({shaper_rules, Host}). + +-spec sm_cache_life_time() -> 'infinity' | pos_integer(). +sm_cache_life_time() -> + ejabberd_config:get_option({sm_cache_life_time, global}). + +-spec sm_cache_missed() -> boolean(). +sm_cache_missed() -> + ejabberd_config:get_option({sm_cache_missed, global}). + +-spec sm_cache_size() -> 'infinity' | pos_integer(). +sm_cache_size() -> + ejabberd_config:get_option({sm_cache_size, global}). + +-spec sm_db_type() -> atom(). +sm_db_type() -> + sm_db_type(global). +-spec sm_db_type(global | binary()) -> atom(). +sm_db_type(Host) -> + ejabberd_config:get_option({sm_db_type, Host}). + +-spec sm_use_cache() -> boolean(). +sm_use_cache() -> + sm_use_cache(global). +-spec sm_use_cache(global | binary()) -> boolean(). +sm_use_cache(Host) -> + ejabberd_config:get_option({sm_use_cache, Host}). + +-spec sql_connect_timeout() -> pos_integer(). +sql_connect_timeout() -> + sql_connect_timeout(global). +-spec sql_connect_timeout(global | binary()) -> pos_integer(). +sql_connect_timeout(Host) -> + ejabberd_config:get_option({sql_connect_timeout, Host}). + +-spec sql_database() -> 'undefined' | binary(). +sql_database() -> + sql_database(global). +-spec sql_database(global | binary()) -> 'undefined' | binary(). +sql_database(Host) -> + ejabberd_config:get_option({sql_database, Host}). + +-spec sql_flags() -> ['mysql_alternative_upsert']. +sql_flags() -> + sql_flags(global). +-spec sql_flags(global | binary()) -> ['mysql_alternative_upsert']. +sql_flags(Host) -> + ejabberd_config:get_option({sql_flags, Host}). + +-spec sql_keepalive_interval() -> 'undefined' | pos_integer(). +sql_keepalive_interval() -> + sql_keepalive_interval(global). +-spec sql_keepalive_interval(global | binary()) -> 'undefined' | pos_integer(). +sql_keepalive_interval(Host) -> + ejabberd_config:get_option({sql_keepalive_interval, Host}). + +-spec sql_odbc_driver() -> binary(). +sql_odbc_driver() -> + sql_odbc_driver(global). +-spec sql_odbc_driver(global | binary()) -> binary(). +sql_odbc_driver(Host) -> + ejabberd_config:get_option({sql_odbc_driver, Host}). + +-spec sql_password() -> binary(). +sql_password() -> + sql_password(global). +-spec sql_password(global | binary()) -> binary(). +sql_password(Host) -> + ejabberd_config:get_option({sql_password, Host}). + +-spec sql_pool_size() -> pos_integer(). +sql_pool_size() -> + sql_pool_size(global). +-spec sql_pool_size(global | binary()) -> pos_integer(). +sql_pool_size(Host) -> + ejabberd_config:get_option({sql_pool_size, Host}). + +-spec sql_port() -> 1..1114111. +sql_port() -> + sql_port(global). +-spec sql_port(global | binary()) -> 1..1114111. +sql_port(Host) -> + ejabberd_config:get_option({sql_port, Host}). + +-spec sql_prepared_statements() -> boolean(). +sql_prepared_statements() -> + sql_prepared_statements(global). +-spec sql_prepared_statements(global | binary()) -> boolean(). +sql_prepared_statements(Host) -> + ejabberd_config:get_option({sql_prepared_statements, Host}). + +-spec sql_query_timeout() -> pos_integer(). +sql_query_timeout() -> + sql_query_timeout(global). +-spec sql_query_timeout(global | binary()) -> pos_integer(). +sql_query_timeout(Host) -> + ejabberd_config:get_option({sql_query_timeout, Host}). + +-spec sql_queue_type() -> 'file' | 'ram'. +sql_queue_type() -> + sql_queue_type(global). +-spec sql_queue_type(global | binary()) -> 'file' | 'ram'. +sql_queue_type(Host) -> + ejabberd_config:get_option({sql_queue_type, Host}). + +-spec sql_server() -> binary(). +sql_server() -> + sql_server(global). +-spec sql_server(global | binary()) -> binary(). +sql_server(Host) -> + ejabberd_config:get_option({sql_server, Host}). + +-spec sql_ssl() -> boolean(). +sql_ssl() -> + sql_ssl(global). +-spec sql_ssl(global | binary()) -> boolean(). +sql_ssl(Host) -> + ejabberd_config:get_option({sql_ssl, Host}). + +-spec sql_ssl_cafile() -> 'undefined' | binary(). +sql_ssl_cafile() -> + sql_ssl_cafile(global). +-spec sql_ssl_cafile(global | binary()) -> 'undefined' | binary(). +sql_ssl_cafile(Host) -> + ejabberd_config:get_option({sql_ssl_cafile, Host}). + +-spec sql_ssl_certfile() -> 'undefined' | binary(). +sql_ssl_certfile() -> + sql_ssl_certfile(global). +-spec sql_ssl_certfile(global | binary()) -> 'undefined' | binary(). +sql_ssl_certfile(Host) -> + ejabberd_config:get_option({sql_ssl_certfile, Host}). + +-spec sql_ssl_verify() -> boolean(). +sql_ssl_verify() -> + sql_ssl_verify(global). +-spec sql_ssl_verify(global | binary()) -> boolean(). +sql_ssl_verify(Host) -> + ejabberd_config:get_option({sql_ssl_verify, Host}). + +-spec sql_start_interval() -> pos_integer(). +sql_start_interval() -> + sql_start_interval(global). +-spec sql_start_interval(global | binary()) -> pos_integer(). +sql_start_interval(Host) -> + ejabberd_config:get_option({sql_start_interval, Host}). + +-spec sql_type() -> 'mssql' | 'mysql' | 'odbc' | 'pgsql' | 'sqlite'. +sql_type() -> + sql_type(global). +-spec sql_type(global | binary()) -> 'mssql' | 'mysql' | 'odbc' | 'pgsql' | 'sqlite'. +sql_type(Host) -> + ejabberd_config:get_option({sql_type, Host}). + +-spec sql_username() -> binary(). +sql_username() -> + sql_username(global). +-spec sql_username(global | binary()) -> binary(). +sql_username(Host) -> + ejabberd_config:get_option({sql_username, Host}). + +-spec trusted_proxies() -> 'all' | [{inet:ip4_address() | inet:ip6_address(),byte()}]. +trusted_proxies() -> + ejabberd_config:get_option({trusted_proxies, global}). + +-spec update_sql_schema() -> boolean(). +update_sql_schema() -> + ejabberd_config:get_option({update_sql_schema, global}). + +-spec update_sql_schema_timeout() -> 'infinity' | pos_integer(). +update_sql_schema_timeout() -> + update_sql_schema_timeout(global). +-spec update_sql_schema_timeout(global | binary()) -> 'infinity' | pos_integer(). +update_sql_schema_timeout(Host) -> + ejabberd_config:get_option({update_sql_schema_timeout, Host}). + +-spec use_cache() -> boolean(). +use_cache() -> + use_cache(global). +-spec use_cache(global | binary()) -> boolean(). +use_cache(Host) -> + ejabberd_config:get_option({use_cache, Host}). + +-spec validate_stream() -> boolean(). +validate_stream() -> + ejabberd_config:get_option({validate_stream, global}). + +-spec version() -> binary(). +version() -> + ejabberd_config:get_option({version, global}). + +-spec websocket_origin() -> [binary()]. +websocket_origin() -> + ejabberd_config:get_option({websocket_origin, global}). + +-spec websocket_ping_interval() -> pos_integer(). +websocket_ping_interval() -> + ejabberd_config:get_option({websocket_ping_interval, global}). + +-spec websocket_timeout() -> pos_integer(). +websocket_timeout() -> + ejabberd_config:get_option({websocket_timeout, global}). + diff --git a/src/ejabberd_options.erl b/src/ejabberd_options.erl new file mode 100644 index 000000000..609d75b93 --- /dev/null +++ b/src/ejabberd_options.erl @@ -0,0 +1,865 @@ +%%%---------------------------------------------------------------------- +%%% ejabberd, Copyright (C) 2002-2025 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(ejabberd_options). +-behaviour(ejabberd_config). + +-export([opt_type/1, options/0, globals/0, doc/0]). + +-ifdef(NEW_SQL_SCHEMA). +-define(USE_NEW_SQL_SCHEMA_DEFAULT, true). +-else. +-define(USE_NEW_SQL_SCHEMA_DEFAULT, false). +-endif. + +-include_lib("kernel/include/inet.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +-spec opt_type(atom()) -> econf:validator(). +opt_type(access_rules) -> + acl:validator(access_rules); +opt_type(acl) -> + acl:validator(acl); +opt_type(acme) -> + econf:options( + #{ca_url => econf:url(), + contact => econf:list_or_single(econf:binary("^[a-zA-Z]+:[^:]+$")), + auto => econf:bool(), + cert_type => econf:enum([ec, rsa])}, + [unique, {return, map}]); +opt_type(allow_contrib_modules) -> + econf:bool(); +opt_type(allow_multiple_connections) -> + econf:bool(); +opt_type(anonymous_protocol) -> + econf:enum([sasl_anon, login_anon, both]); +opt_type(api_permissions) -> + ejabberd_access_permissions:validator(); +opt_type(append_host_config) -> + opt_type(host_config); +opt_type(auth_cache_life_time) -> + econf:timeout(second, infinity); +opt_type(auth_cache_missed) -> + econf:bool(); +opt_type(auth_cache_size) -> + econf:pos_int(infinity); +opt_type(auth_method) -> + econf:list_or_single(econf:db_type(ejabberd_auth)); +opt_type(auth_opts) -> + fun(L) when is_list(L) -> + lists:map( + fun({host, V}) when is_binary(V) -> + {host, V}; + ({connection_pool_size, V}) when is_integer(V) -> + {connection_pool_size, V}; + ({connection_opts, V}) when is_list(V) -> + {connection_opts, V}; + ({basic_auth, V}) when is_binary(V) -> + {basic_auth, V}; + ({path_prefix, V}) when is_binary(V) -> + {path_prefix, V} + end, L) + end; +opt_type(auth_stored_password_types) -> + econf:list(econf:enum([plain, scram_sha1, scram_sha256, scram_sha512])); +opt_type(auth_password_types_hidden_in_sasl1) -> + econf:list(econf:enum([plain, scram_sha1, scram_sha256, scram_sha512])); +opt_type(auth_password_format) -> + econf:enum([plain, scram]); +opt_type(auth_scram_hash) -> + econf:enum([sha, sha256, sha512]); +opt_type(auth_external_user_exists_check) -> + econf:bool(); +opt_type(auth_use_cache) -> + econf:bool(); +opt_type(c2s_cafile) -> + econf:file(); +opt_type(c2s_ciphers) -> + fun(L) when is_list(L) -> + (econf:and_then( + econf:list(econf:binary(), [unique]), + concat_binary($:)))(L); + (B) -> + (econf:binary())(B) + end; +opt_type(c2s_dhfile) -> + econf:file(); +opt_type(c2s_protocol_options) -> + econf:and_then( + econf:list(econf:binary(), [unique]), + concat_binary($|)); +opt_type(c2s_tls_compression) -> + econf:bool(); +opt_type(ca_file) -> + econf:pem(); +opt_type(cache_life_time) -> + econf:timeout(second, infinity); +opt_type(cache_missed) -> + econf:bool(); +opt_type(cache_size) -> + econf:pos_int(infinity); +opt_type(captcha_cmd) -> + econf:binary(); +opt_type(captcha_host) -> + econf:binary(); +opt_type(captcha_limit) -> + econf:pos_int(infinity); +opt_type(captcha_url) -> + econf:either( + econf:url(), + econf:enum([auto, undefined])); +opt_type(certfiles) -> + econf:list(econf:binary()); +opt_type(cluster_backend) -> + econf:db_type(ejabberd_cluster); +opt_type(cluster_nodes) -> + econf:list(econf:atom(), [unique]); +opt_type(default_db) -> + econf:enum([mnesia, sql]); +opt_type(default_ram_db) -> + econf:enum([mnesia, sql, redis]); +opt_type(define_keyword) -> + econf:map(econf:binary(), econf:any(), [unique]); +opt_type(define_macro) -> + econf:map(econf:binary(), econf:any(), [unique]); +opt_type(disable_sasl_scram_downgrade_protection) -> + econf:bool(); +opt_type(disable_sasl_mechanisms) -> + econf:list_or_single( + econf:and_then( + econf:binary(), + fun str:to_upper/1)); +opt_type(domain_balancing) -> + econf:map( + econf:domain(), + econf:options( + #{component_number => econf:int(2, 1000), + type => econf:enum([random, source, destination, + bare_source, bare_destination])}, + [{return, map}, unique]), + [{return, map}]); +opt_type(ext_api_path_oauth) -> + econf:binary(); +opt_type(ext_api_http_pool_size) -> + econf:pos_int(); +opt_type(ext_api_url) -> + econf:url(); +opt_type(ext_api_headers) -> + econf:binary(); +opt_type(extauth_pool_name) -> + econf:binary(); +opt_type(extauth_pool_size) -> + econf:pos_int(); +opt_type(extauth_program) -> + econf:string(); +opt_type(fqdn) -> + econf:list_or_single(econf:domain()); +opt_type(hide_sensitive_log_data) -> + econf:bool(); +opt_type(host_config) -> + econf:and_then( + econf:and_then( + econf:map(econf:domain(), econf:list(econf:any())), + fun econf:group_dups/1), + econf:map( + econf:enum(ejabberd_config:get_option(hosts)), + validator(), + [unique])); +opt_type(hosts) -> + econf:non_empty(econf:list(econf:domain(), [unique])); +opt_type(hosts_alias) -> + econf:and_then( + econf:map(econf:domain(), econf:domain(), [unique]), + econf:map( + econf:domain(), + econf:enum(ejabberd_config:get_option(hosts)), + [unique])); +opt_type(include_config_file) -> + econf:any(); +opt_type(install_contrib_modules) -> + econf:list(econf:atom()); +opt_type(language) -> + econf:lang(); +opt_type(ldap_backups) -> + econf:list(econf:domain(), [unique]); +opt_type(ldap_base) -> + econf:binary(); +opt_type(ldap_deref_aliases) -> + econf:enum([never, searching, finding, always]); +opt_type(ldap_dn_filter) -> + econf:and_then( + econf:non_empty( + econf:map( + econf:ldap_filter(), + econf:list(econf:binary()))), + fun hd/1); +opt_type(ldap_encrypt) -> + econf:enum([tls, starttls, none]); +opt_type(ldap_filter) -> + econf:ldap_filter(); +opt_type(ldap_password) -> + econf:binary(); +opt_type(ldap_port) -> + econf:port(); +opt_type(ldap_rootdn) -> + econf:binary(); +opt_type(ldap_servers) -> + econf:list(econf:domain(), [unique]); +opt_type(ldap_tls_cacertfile) -> + econf:pem(); +opt_type(ldap_tls_certfile) -> + econf:pem(); +opt_type(ldap_tls_depth) -> + econf:non_neg_int(); +opt_type(ldap_tls_verify) -> + econf:enum([hard, soft, false]); +opt_type(ldap_uids) -> + econf:either( + econf:list( + econf:and_then( + econf:binary(), + fun(U) -> {U, <<"%u">>} end)), + econf:map(econf:binary(), econf:binary(), [unique])); +opt_type(listen) -> + ejabberd_listener:validator(); +opt_type(log_rotate_count) -> + econf:non_neg_int(); +opt_type(log_rotate_size) -> + econf:pos_int(infinity); +opt_type(log_burst_limit_window_time) -> + econf:timeout(second); +opt_type(log_burst_limit_count) -> + econf:pos_int(); +opt_type(log_modules_fully) -> + econf:list(econf:atom()); +opt_type(loglevel) -> + fun(N) when is_integer(N) -> + (econf:and_then( + econf:int(0, 5), + fun ejabberd_logger:convert_loglevel/1))(N); + (Level) -> + (econf:enum([none, emergency, alert, critical, + error, warning, notice, info, debug]))(Level) + end; +opt_type(max_fsm_queue) -> + econf:pos_int(); +opt_type(modules) -> + econf:map(econf:atom(), econf:any()); +opt_type(negotiation_timeout) -> + econf:timeout(second); +opt_type(net_ticktime) -> + econf:timeout(second); +opt_type(new_sql_schema) -> + econf:bool(); +opt_type(update_sql_schema) -> + econf:bool(); +opt_type(update_sql_schema_timeout) -> + econf:timeout(second, infinity); +opt_type(oauth_access) -> + econf:acl(); +opt_type(oauth_cache_life_time) -> + econf:timeout(second, infinity); +opt_type(oauth_cache_missed) -> + econf:bool(); +opt_type(oauth_cache_rest_failure_life_time) -> + econf:timeout(second, infinity); +opt_type(oauth_cache_size) -> + econf:pos_int(infinity); +opt_type(oauth_db_type) -> + econf:db_type(ejabberd_oauth); +opt_type(oauth_expire) -> + econf:timeout(second); +opt_type(oauth_use_cache) -> + econf:bool(); +opt_type(oauth_client_id_check) -> + econf:enum([allow, deny, db]); +opt_type(oom_killer) -> + econf:bool(); +opt_type(oom_queue) -> + econf:pos_int(); +opt_type(oom_watermark) -> + econf:int(1, 99); +opt_type(outgoing_s2s_families) -> + econf:and_then( + econf:non_empty( + econf:list(econf:enum([ipv4, ipv6]), [unique])), + fun(L) -> + lists:map( + fun(ipv4) -> inet; + (ipv6) -> inet6 + end, L) + end); +opt_type(outgoing_s2s_ipv4_address) -> + econf:ipv4(); +opt_type(outgoing_s2s_ipv6_address) -> + econf:ipv6(); +opt_type(outgoing_s2s_port) -> + econf:port(); +opt_type(outgoing_s2s_timeout) -> + econf:timeout(second, infinity); +opt_type(pam_service) -> + econf:binary(); +opt_type(pam_userinfotype) -> + econf:enum([username, jid]); +opt_type(pgsql_users_number_estimate) -> + econf:bool(); +opt_type(queue_dir) -> + econf:directory(write); +opt_type(queue_type) -> + econf:enum([ram, file]); +opt_type(redis_connect_timeout) -> + econf:timeout(second); +opt_type(redis_db) -> + econf:non_neg_int(); +opt_type(redis_password) -> + econf:string(); +opt_type(redis_pool_size) -> + econf:pos_int(); +opt_type(redis_port) -> + econf:port(); +opt_type(redis_queue_type) -> + econf:enum([ram, file]); +opt_type(redis_server) -> + econf:string(); +opt_type(registration_timeout) -> + econf:timeout(second, infinity); +opt_type(resource_conflict) -> + econf:enum([setresource, closeold, closenew, acceptnew]); +opt_type(rest_proxy) -> + econf:domain(); +opt_type(rest_proxy_port) -> + econf:port(); +opt_type(rest_proxy_username) -> + econf:string(); +opt_type(rest_proxy_password) -> + econf:string(); +opt_type(router_cache_life_time) -> + econf:timeout(second, infinity); +opt_type(router_cache_missed) -> + econf:bool(); +opt_type(router_cache_size) -> + econf:pos_int(infinity); +opt_type(router_db_type) -> + econf:db_type(ejabberd_router); +opt_type(router_use_cache) -> + econf:bool(); +opt_type(rpc_timeout) -> + econf:timeout(second); +opt_type(s2s_access) -> + econf:acl(); +opt_type(s2s_cafile) -> + econf:pem(); +opt_type(s2s_ciphers) -> + opt_type(c2s_ciphers); +opt_type(s2s_dhfile) -> + econf:file(); +opt_type(s2s_dns_retries) -> + econf:non_neg_int(); +opt_type(s2s_dns_timeout) -> + econf:timeout(second, infinity); +opt_type(s2s_max_retry_delay) -> + econf:timeout(second); +opt_type(s2s_protocol_options) -> + opt_type(c2s_protocol_options); +opt_type(s2s_queue_type) -> + econf:enum([ram, file]); +opt_type(s2s_timeout) -> + econf:timeout(second, infinity); +opt_type(s2s_tls_compression) -> + econf:bool(); +opt_type(s2s_use_starttls) -> + econf:either( + econf:bool(), + econf:enum([optional, required])); +opt_type(s2s_zlib) -> + econf:and_then( + econf:bool(), + fun(false) -> false; + (true) -> + ejabberd:start_app(ezlib), + true + end); +opt_type(shaper) -> + ejabberd_shaper:validator(shaper); +opt_type(shaper_rules) -> + ejabberd_shaper:validator(shaper_rules); +opt_type(sm_cache_life_time) -> + econf:timeout(second, infinity); +opt_type(sm_cache_missed) -> + econf:bool(); +opt_type(sm_cache_size) -> + econf:pos_int(infinity); +opt_type(sm_db_type) -> + econf:db_type(ejabberd_sm); +opt_type(sm_use_cache) -> + econf:bool(); +opt_type(sql_connect_timeout) -> + econf:timeout(second); +opt_type(sql_database) -> + econf:binary(); +opt_type(sql_keepalive_interval) -> + econf:timeout(second); +opt_type(sql_password) -> + econf:binary(); +opt_type(sql_odbc_driver) -> + econf:binary(); +opt_type(sql_pool_size) -> + econf:pos_int(); +opt_type(sql_port) -> + econf:port(); +opt_type(sql_query_timeout) -> + econf:timeout(second); +opt_type(sql_queue_type) -> + econf:enum([ram, file]); +opt_type(sql_server) -> + econf:binary(); +opt_type(sql_ssl) -> + econf:bool(); +opt_type(sql_ssl_cafile) -> + econf:pem(); +opt_type(sql_ssl_certfile) -> + econf:pem(); +opt_type(sql_ssl_verify) -> + econf:bool(); +opt_type(sql_start_interval) -> + econf:timeout(second); +opt_type(sql_type) -> + econf:enum([mysql, pgsql, sqlite, mssql, odbc]); +opt_type(sql_username) -> + econf:binary(); +opt_type(sql_prepared_statements) -> + econf:bool(); +opt_type(sql_flags) -> + econf:list_or_single(econf:enum([mysql_alternative_upsert]), [sorted, unique]); +opt_type(trusted_proxies) -> + econf:either(all, econf:list(econf:ip_mask())); +opt_type(use_cache) -> + econf:bool(); +opt_type(validate_stream) -> + econf:bool(); +opt_type(version) -> + econf:binary(); +opt_type(websocket_origin) -> + econf:list( + econf:and_then( + econf:and_then( + econf:binary_sep("\\s+"), + econf:list(econf:url(), [unique])), + fun(L) -> str:join(L, <<" ">>) end), + [unique]); +opt_type(websocket_ping_interval) -> + econf:timeout(second); +opt_type(websocket_timeout) -> + econf:timeout(second); +opt_type(jwt_key) -> + econf:and_then( + econf:path(), + fun(Path) -> + case file:read_file(Path) of + {ok, Data} -> + try jose_jwk:from_binary(Data) of + {error, _} -> econf:fail({bad_jwt_key, Path}); + JWK -> + case jose_jwk:to_map(JWK) of + {_, #{<<"keys">> := [Key]}} -> + jose_jwk:from_map(Key); + {_, #{<<"keys">> := [_|_]}} -> + econf:fail({bad_jwt_key_set, Path}); + {_, #{<<"keys">> := _}} -> + econf:fail({bad_jwt_key, Path}); + _ -> + JWK + end + catch _:_ -> + econf:fail({bad_jwt_key, Path}) + end; + {error, Reason} -> + econf:fail({read_file, Reason, Path}) + end + end); +opt_type(jwt_jid_field) -> + econf:binary(); +opt_type(jwt_auth_only_rule) -> + econf:atom(). + +%% We only define the types of options that cannot be derived +%% automatically by tools/opt_type.sh script +-spec options() -> [{s2s_protocol_options, undefined | binary()} | + {c2s_protocol_options, undefined | binary()} | + {s2s_ciphers, undefined | binary()} | + {c2s_ciphers, undefined | binary()} | + {websocket_origin, [binary()]} | + {disable_sasl_mechanisms, [binary()]} | + {s2s_zlib, boolean()} | + {loglevel, ejabberd_logger:loglevel()} | + {auth_opts, [{any(), any()}]} | + {listen, [ejabberd_listener:listener()]} | + {modules, [{module(), gen_mod:opts(), integer()}]} | + {ldap_uids, [{binary(), binary()}]} | + {ldap_dn_filter, {binary(), [binary()]}} | + {outgoing_s2s_families, [inet | inet6, ...]} | + {acl, [{atom(), [acl:acl_rule()]}]} | + {access_rules, [{atom(), acl:access()}]} | + {shaper, #{atom() => ejabberd_shaper:shaper_rate()}} | + {shaper_rules, [{atom(), [ejabberd_shaper:shaper_rule()]}]} | + {api_permissions, [ejabberd_access_permissions:permission()]} | + {jwt_key, jose_jwk:key() | undefined} | + {append_host_config, [{binary(), any()}]} | + {host_config, [{binary(), any()}]} | + {define_keyword, any()} | + {define_macro, any()} | + {include_config_file, any()} | + {atom(), any()}]. +options() -> + [%% Top-priority options + hosts, + {loglevel, info}, + {cache_life_time, timer:seconds(3600)}, + {cache_missed, true}, + {cache_size, 1000}, + {use_cache, true}, + {default_db, mnesia}, + {default_ram_db, mnesia}, + {queue_type, ram}, + {version, ejabberd_config:version()}, + %% Other options + {acl, []}, + {access_rules, []}, + {acme, #{}}, + {allow_contrib_modules, true}, + {install_contrib_modules, []}, + {allow_multiple_connections, false}, + {anonymous_protocol, sasl_anon}, + {api_permissions, + [{<<"admin access">>, + {[], + [{acl, admin}, + {oauth, {[<<"ejabberd:admin">>], [{acl, admin}]}}], + {all, [start, stop]}}}]}, + {append_host_config, []}, + {auth_cache_life_time, + fun(Host) -> ejabberd_config:get_option({cache_life_time, Host}) end}, + {auth_cache_missed, + fun(Host) -> ejabberd_config:get_option({cache_missed, Host}) end}, + {auth_cache_size, + fun(Host) -> ejabberd_config:get_option({cache_size, Host}) end}, + {auth_method, + fun(Host) -> [ejabberd_config:default_db(Host, ejabberd_auth)] end}, + {auth_opts, []}, + {auth_password_format, plain}, + {auth_scram_hash, sha}, + {auth_stored_password_types, []}, + {auth_password_types_hidden_in_sasl1, []}, + {auth_external_user_exists_check, true}, + {auth_use_cache, + fun(Host) -> ejabberd_config:get_option({use_cache, Host}) end}, + {c2s_cafile, undefined}, + {c2s_ciphers, undefined}, + {c2s_dhfile, undefined}, + {c2s_protocol_options, undefined}, + {c2s_tls_compression, undefined}, + {ca_file, iolist_to_binary(pkix:get_cafile())}, + {captcha_cmd, undefined}, + {captcha_host, <<"">>}, + {captcha_limit, infinity}, + {captcha_url, auto}, + {certfiles, undefined}, + {cluster_backend, mnesia}, + {cluster_nodes, []}, + {define_keyword, []}, + {define_macro, []}, + {disable_sasl_scram_downgrade_protection, false}, + {disable_sasl_mechanisms, []}, + {domain_balancing, #{}}, + {ext_api_headers, <<>>}, + {ext_api_http_pool_size, 100}, + {ext_api_path_oauth, <<"/oauth">>}, + {ext_api_url, <<"http://localhost/api">>}, + {extauth_pool_name, undefined}, + {extauth_pool_size, undefined}, + {extauth_program, undefined}, + {fqdn, fun fqdn/1}, + {hide_sensitive_log_data, false}, + {hosts_alias, []}, + {host_config, []}, + {include_config_file, []}, + {language, <<"en">>}, + {ldap_backups, []}, + {ldap_base, <<"">>}, + {ldap_deref_aliases, never}, + {ldap_dn_filter, {undefined, []}}, + {ldap_encrypt, none}, + {ldap_filter, <<"">>}, + {ldap_password, <<"">>}, + {ldap_port, + fun(Host) -> + case ejabberd_config:get_option({ldap_encrypt, Host}) of + tls -> 636; + _ -> 389 + end + end}, + {ldap_rootdn, <<"">>}, + {ldap_servers, [<<"localhost">>]}, + {ldap_tls_cacertfile, undefined}, + {ldap_tls_certfile, undefined}, + {ldap_tls_depth, undefined}, + {ldap_tls_verify, false}, + {ldap_uids, [{<<"uid">>, <<"%u">>}]}, + {listen, []}, + {log_rotate_count, 1}, + {log_rotate_size, 10*1024*1024}, + {log_burst_limit_window_time, timer:seconds(1)}, + {log_burst_limit_count, 500}, + {log_modules_fully, []}, + {max_fsm_queue, undefined}, + {modules, []}, + {negotiation_timeout, timer:seconds(120)}, + {net_ticktime, timer:seconds(60)}, + {new_sql_schema, ?USE_NEW_SQL_SCHEMA_DEFAULT}, + {update_sql_schema, true}, + {update_sql_schema_timeout, timer:minutes(5)}, + {oauth_access, none}, + {oauth_cache_life_time, + fun(Host) -> ejabberd_config:get_option({cache_life_time, Host}) end}, + {oauth_cache_missed, + fun(Host) -> ejabberd_config:get_option({cache_missed, Host}) end}, + {oauth_cache_size, + fun(Host) -> ejabberd_config:get_option({cache_size, Host}) end}, + {oauth_cache_rest_failure_life_time, infinity}, + {oauth_db_type, + fun(Host) -> ejabberd_config:default_db(Host, ejabberd_oauth) end}, + {oauth_expire, 4294967}, + {oauth_use_cache, + fun(Host) -> ejabberd_config:get_option({use_cache, Host}) end}, + {oauth_client_id_check, allow}, + {oom_killer, true}, + {oom_queue, 10000}, + {oom_watermark, 80}, + {outgoing_s2s_families, [inet6, inet]}, + {outgoing_s2s_ipv4_address, undefined}, + {outgoing_s2s_ipv6_address, undefined}, + {outgoing_s2s_port, 5269}, + {outgoing_s2s_timeout, timer:seconds(10)}, + {pam_service, <<"ejabberd">>}, + {pam_userinfotype, username}, + {pgsql_users_number_estimate, false}, + {queue_dir, undefined}, + {redis_connect_timeout, timer:seconds(1)}, + {redis_db, 0}, + {redis_password, ""}, + {redis_pool_size, 10}, + {redis_port, 6379}, + {redis_queue_type, + fun(Host) -> ejabberd_config:get_option({queue_type, Host}) end}, + {redis_server, "localhost"}, + {registration_timeout, timer:seconds(600)}, + {resource_conflict, acceptnew}, + {rest_proxy, <<>>}, + {rest_proxy_port, 0}, + {rest_proxy_username, ""}, + {rest_proxy_password, ""}, + {router_cache_life_time, + fun(Host) -> ejabberd_config:get_option({cache_life_time, Host}) end}, + {router_cache_missed, + fun(Host) -> ejabberd_config:get_option({cache_missed, Host}) end}, + {router_cache_size, + fun(Host) -> ejabberd_config:get_option({cache_size, Host}) end}, + {router_db_type, + fun(Host) -> ejabberd_config:default_ram_db(Host, ejabberd_router) end}, + {router_use_cache, + fun(Host) -> ejabberd_config:get_option({use_cache, Host}) end}, + {rpc_timeout, timer:seconds(5)}, + {s2s_access, all}, + {s2s_cafile, undefined}, + {s2s_ciphers, undefined}, + {s2s_dhfile, undefined}, + {s2s_dns_retries, 2}, + {s2s_dns_timeout, timer:seconds(10)}, + {s2s_max_retry_delay, timer:seconds(300)}, + {s2s_protocol_options, undefined}, + {s2s_queue_type, + fun(Host) -> ejabberd_config:get_option({queue_type, Host}) end}, + {s2s_timeout, timer:hours(1)}, + {s2s_tls_compression, undefined}, + {s2s_use_starttls, false}, + {s2s_zlib, false}, + {shaper, #{}}, + {shaper_rules, []}, + {sm_cache_life_time, + fun(Host) -> ejabberd_config:get_option({cache_life_time, Host}) end}, + {sm_cache_missed, + fun(Host) -> ejabberd_config:get_option({cache_missed, Host}) end}, + {sm_cache_size, + fun(Host) -> ejabberd_config:get_option({cache_size, Host}) end}, + {sm_db_type, + fun(Host) -> ejabberd_config:default_ram_db(Host, ejabberd_sm) end}, + {sm_use_cache, + fun(Host) -> ejabberd_config:get_option({use_cache, Host}) end}, + {sql_type, odbc}, + {sql_connect_timeout, timer:seconds(5)}, + {sql_database, undefined}, + {sql_keepalive_interval, undefined}, + {sql_password, <<"">>}, + {sql_odbc_driver, <<"libtdsodbc.so">>}, % default is FreeTDS driver + {sql_pool_size, + fun(Host) -> + case ejabberd_config:get_option({sql_type, Host}) of + sqlite -> 1; + _ -> 10 + end + end}, + {sql_port, + fun(Host) -> + case ejabberd_config:get_option({sql_type, Host}) of + mssql -> 1433; + mysql -> 3306; + pgsql -> 5432; + _ -> undefined + end + end}, + {sql_query_timeout, timer:seconds(60)}, + {sql_queue_type, + fun(Host) -> ejabberd_config:get_option({queue_type, Host}) end}, + {sql_server, <<"localhost">>}, + {sql_ssl, false}, + {sql_ssl_cafile, undefined}, + {sql_ssl_certfile, undefined}, + {sql_ssl_verify, false}, + {sql_start_interval, timer:seconds(30)}, + {sql_username, <<"ejabberd">>}, + {sql_prepared_statements, true}, + {sql_flags, []}, + {trusted_proxies, []}, + {validate_stream, false}, + {websocket_origin, []}, + {websocket_ping_interval, timer:seconds(60)}, + {websocket_timeout, timer:minutes(5)}, + {jwt_key, undefined}, + {jwt_jid_field, <<"jid">>}, + {jwt_auth_only_rule, none}]. + +-spec globals() -> [atom()]. +globals() -> + [acme, + allow_contrib_modules, + api_permissions, + append_host_config, + auth_cache_life_time, + auth_cache_missed, + auth_cache_size, + ca_file, + captcha_cmd, + captcha_host, + captcha_limit, + captcha_url, + certfiles, + cluster_backend, + cluster_nodes, + define_macro, + domain_balancing, + ext_api_path_oauth, + fqdn, + hosts, + hosts_alias, + host_config, + install_contrib_modules, + listen, + loglevel, + log_rotate_count, + log_rotate_size, + log_burst_limit_count, + log_burst_limit_window_time, + log_modules_fully, + negotiation_timeout, + net_ticktime, + new_sql_schema, + update_sql_schema, + node_start, + oauth_cache_life_time, + oauth_cache_missed, + oauth_cache_size, + oauth_cache_rest_failure_life_time, + oauth_db_type, + oauth_expire, + oauth_use_cache, + oom_killer, + oom_queue, + oom_watermark, + queue_dir, + redis_connect_timeout, + redis_db, + redis_password, + redis_pool_size, + redis_port, + redis_queue_type, + redis_server, + registration_timeout, + router_cache_life_time, + router_cache_missed, + router_cache_size, + router_db_type, + router_use_cache, + rpc_timeout, + s2s_max_retry_delay, + shaper, + sm_cache_life_time, + sm_cache_missed, + sm_cache_size, + trusted_proxies, + validate_stream, + version, + websocket_origin, + websocket_ping_interval, + websocket_timeout]. + +doc() -> + ejabberd_options_doc:doc(). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +-spec validator() -> econf:validator(). +validator() -> + Disallowed = ejabberd_config:globals(), + {Validators, Required} = ejabberd_config:validators(Disallowed), + econf:and_then( + fun econf:group_dups/1, + econf:options( + Validators, + [{disallowed, Required ++ Disallowed}, unique])). + +-spec fqdn(global | binary()) -> [binary()]. +fqdn(global) -> + {ok, Hostname} = inet:gethostname(), + case inet:gethostbyname(Hostname) of + {ok, #hostent{h_name = FQDN}} -> + case jid:nameprep(iolist_to_binary(FQDN)) of + error -> []; + Domain -> [Domain] + end; + {error, _} -> + [] + end; +fqdn(_) -> + ejabberd_config:get_option(fqdn). + +-spec concat_binary(char()) -> fun(([binary()]) -> binary()). +concat_binary(C) -> + fun(Opts) -> str:join(Opts, <>) end. diff --git a/src/ejabberd_options_doc.erl b/src/ejabberd_options_doc.erl new file mode 100644 index 000000000..56e2633c3 --- /dev/null +++ b/src/ejabberd_options_doc.erl @@ -0,0 +1,1618 @@ +%%%---------------------------------------------------------------------- +%%% ejabberd, Copyright (C) 2002-2025 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(ejabberd_options_doc). + +%% API +-export([doc/0]). + +-include("translate.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +doc() -> + [{hosts, + #{value => ?T("[Domain1, Domain2, ...]"), + desc => + ?T("List of one or more " + "_`../configuration/basic.md#host-names|host names`_ " + "(or domains) that ejabberd will serve. This is a " + "**mandatory** option.")}}, + {listen, + #{value => "[Options, ...]", + desc => + ?T("The option for listeners configuration. See the " + "_`listen.md|Listen Modules`_ section " + "for details.")}}, + {modules, + #{value => "{Module: Options}", + desc => + ?T("Set all the " + "_`modules.md|modules`_ configuration options.")}}, + {loglevel, + #{value => + "none | emergency | alert | critical | " + "error | warning | notice | info | debug", + desc => + ?T("Verbosity of ejabberd " + "_`../configuration/basic.md#logging|logging`_. " + "The default value is 'info'. " + "NOTE: previous versions of ejabberd had log levels " + "defined in numeric format ('0..5'). The numeric values " + "are still accepted for backward compatibility, but " + "are not recommended.")}}, + {cache_life_time, + #{value => "timeout()", + desc => + ?T("The time of a cached item to keep in cache. " + "Once it's expired, the corresponding item is " + "erased from cache. The default value is '1 hour'. " + "Several modules have a similar option; and some core " + "ejabberd parts support similar options too, see " + "_`auth_cache_life_time`_, _`oauth_cache_life_time`_, " + "_`router_cache_life_time`_, and _`sm_cache_life_time`_.")}}, + {cache_missed, + #{value => "true | false", + desc => + ?T("Whether or not to cache missed lookups. When there is " + "an attempt to lookup for a value in a database and " + "this value is not found and the option is set to 'true', " + "this attempt will be cached and no attempts will be " + "performed until the cache expires (see _`cache_life_time`_). " + "Usually you don't want to change it. Default is 'true'. " + "Several modules have a similar option; and some core " + "ejabberd parts support similar options too, see " + "_`auth_cache_missed`_, _`oauth_cache_missed`_, " + "_`router_cache_missed`_, and _`sm_cache_missed`_.")}}, + {cache_size, + #{value => "pos_integer() | infinity", + desc => + ?T("A maximum number of items (not memory!) in cache. " + "The rule of thumb, for all tables except rosters, " + "you should set it to the number of maximum online " + "users you expect. For roster multiply this number " + "by 20 or so. If the cache size reaches this threshold, " + "it's fully cleared, i.e. all items are deleted, and " + "the corresponding warning is logged. You should avoid " + "frequent cache clearance, because this degrades " + "performance. The default value is '1000'. " + "Several modules have a similar option; and some core " + "ejabberd parts support similar options too, see " + "_`auth_cache_size`_, _`oauth_cache_size`_, " + "_`router_cache_size`_, and _`sm_cache_size`_.")}}, + {use_cache, + #{value => "true | false", + desc => + ?T("Enable or disable cache. The default is 'true'. " + "Several modules have a similar option; and some core " + "ejabberd parts support similar options too, see " + "_`auth_use_cache`_, _`oauth_use_cache`_, _`router_use_cache`_, " + "and _`sm_use_cache`_.")}}, + {default_db, + #{value => "mnesia | sql", + desc => + ?T("_`database.md#default-database|Default database`_ " + "to store persistent data in ejabberd. " + "Some components can be configured with specific toplevel options " + "like _`oauth_db_type`_. " + "Many modules can be configured with specific module options, " + "usually named `db_type`. " + "The default value is 'mnesia'.")}}, + {default_ram_db, + #{value => "mnesia | redis | sql", + desc => + ?T("Default volatile (in-memory) storage for ejabberd. " + "Some components can be configured with specific toplevel options " + "like _`router_db_type`_ and _`sm_db_type`_. " + "Some modules can be configured with specific module options, " + "usually named `ram_db_type`. " + "The default value is 'mnesia'.")}}, + {queue_type, + #{value => "ram | file", + desc => + ?T("Default type of queues in ejabberd. " + "Modules may have its own value of the option. " + "The value of 'ram' means that queues will be kept in memory. " + "If value 'file' is set, you may also specify directory " + "in _`queue_dir`_ option where file queues will be placed. " + "The default value is 'ram'.")}}, + {version, + #{value => "string()", + desc => + ?T("The option can be used to set custom ejabberd version, " + "that will be used by different parts of ejabberd, for " + "example by _`mod_version`_ module. The default value is " + "obtained at compile time from the underlying version " + "control system.")}}, + {acl, + #{value => "{ACLName: {ACLType: ACLValue}}", + desc => + ?T("This option defines " + "_`../configuration/basic.md#acl|access control lists`_: " + "named sets " + "of rules which are used to match against different targets " + "(such as a JID or an IP address). Every set of rules " + "has name 'ACLName': it can be any string except 'all' or 'none' " + "(those are predefined names for the rules that match all or nothing " + "respectively). The name 'ACLName' can be referenced from other " + "parts of the configuration file, for example in _`access_rules`_ " + "option. The rules of 'ACLName' are represented by mapping " + "'pass:[{ACLType: ACLValue}]'. These can be one of the following:")}, + [{user, + #{value => ?T("Username"), + desc => + ?T("If 'Username' is in the form of \"user@server\", " + "the rule matches a JID against this value. " + "Otherwise, if 'Username' is in the form of \"user\", " + "the rule matches any JID that has 'Username' in the node part " + "as long as the server part of this JID is any virtual " + "host served by ejabberd.")}}, + {server, + #{value => ?T("Server"), + desc => + ?T("The rule matches any JID from server 'Server'. " + "The value of 'Server' must be a valid " + "hostname or an IP address.")}}, + {resource, + #{value => ?T("Resource"), + desc => + ?T("The rule matches any JID with a resource 'Resource'.")}}, + {ip, + #{value => ?T("Network"), + desc => + ?T("The rule matches any IP address from the 'Network'.")}}, + {user_regexp, + #{value => ?T("Regexp"), + desc => + ?T("If 'Regexp' is in the form of \"regexp@server\", the rule " + "matches any JID with node part matching regular expression " + "\"regexp\" as long as the server part of this JID is equal " + "to \"server\". If 'Regexp' is in the form of \"regexp\", the rule " + "matches any JID with node part matching regular expression " + "\"regexp\" as long as the server part of this JID is any virtual " + "host served by ejabberd.")}}, + {server_regexp, + #{value => ?T("Regexp"), + desc => + ?T("The rule matches any JID from the server that " + "matches regular expression 'Regexp'.")}}, + {resource_regexp, + #{value => ?T("Regexp"), + desc => + ?T("The rule matches any JID with a resource that " + "matches regular expression 'Regexp'.")}}, + {node_regexp, + #{value => ?T("user_regexp@server_regexp"), + desc => + ?T("The rule matches any JID with node part matching regular " + "expression 'user_regexp' and server part matching regular " + "expression 'server_regexp'.")}}, + {user_glob, + #{value => ?T("Pattern"), + desc => + ?T("Same as 'user_regexp', but matching is performed on a " + "specified 'Pattern' according to the rules used by the " + "Unix shell.")}}, + {server_glob, + #{value => ?T("Pattern"), + desc => + ?T("Same as 'server_regexp', but matching is performed on a " + "specified 'Pattern' according to the rules used by the " + "Unix shell.")}}, + {resource_glob, + #{value => ?T("Pattern"), + desc => + ?T("Same as 'resource_regexp', but matching is performed on a " + "specified 'Pattern' according to the rules used by the " + "Unix shell.")}}, + {node_glob, + #{value => ?T("Pattern"), + desc => + ?T("Same as 'node_regexp', but matching is performed on a " + "specified 'Pattern' according to the rules used by the " + "Unix shell.")}}]}, + {access_rules, + #{value => "{AccessName: {allow|deny: ACLName|ACLDefinition}}", + desc => + ?T("This option defines " + "_`basic.md#access-rules|Access Rules`_. " + "Each access rule is " + "assigned a name that can be referenced from other parts " + "of the configuration file (mostly from 'access' options of " + "ejabberd modules). Each rule definition may contain " + "arbitrary number of 'allow' or 'deny' sections, and each " + "section may contain any number of ACL rules (see _`acl`_ option). " + "There are no access rules defined by default."), + example => + ["access_rules:", + " configure:", + " allow: admin", + " something:", + " deny: someone", + " allow: all", + " s2s_banned:", + " deny: problematic_hosts", + " deny: banned_forever", + " deny:", + " ip: 222.111.222.111/32", + " deny:", + " ip: 111.222.111.222/32", + " allow: all", + " xmlrpc_access:", + " allow:", + " user: peter@example.com", + " allow:", + " user: ivone@example.com", + " allow:", + " user: bot@example.com", + " ip: 10.0.0.0/24"]}}, + {acme, + #{value => ?T("Options"), + desc => + ?T("_`basic.md#acme|ACME`_ configuration, to automatically " + "obtain SSL certificates for the domains served by ejabberd, " + "which means that certificate requests and renewals are " + "performed to some CA server (aka \"ACME server\") in a fully " + "automated mode. The 'Options' are:"), + example => + ["acme:", + " ca_url: https://acme-v02.api.letsencrypt.org/directory", + " contact:", + " - mailto:admin@domain.tld", + " - mailto:bot@domain.tld", + " auto: true", + " cert_type: rsa"]}, + [{ca_url, + #{value => ?T("URL"), + desc => + ?T("The ACME directory URL used as an entry point " + "for the ACME server. The default value is " + " - " + "the directory URL of Let's Encrypt authority.")}}, + {contact, + #{value => ?T("[Contact, ...]"), + desc => + ?T("A list of contact addresses (typically emails) " + "where an ACME server will send notifications " + "when problems occur. The value of 'Contact' must " + "be in the form of \"scheme:address\" (e.g. " + "\"mailto:user@domain.tld\"). The default " + "is an empty list which means an ACME server " + "will send no notices.")}}, + {auto, + #{value => "true | false", + desc => + ?T("Whether to automatically request certificates for " + "all configured domains (that yet have no a certificate) " + "on server start or configuration reload. The default is 'true'.")}}, + {cert_type, + #{value => "rsa | ec", + desc => + ?T("A type of a certificate key. Available values are " + "'ec' and 'rsa' for EC and RSA certificates respectively. " + "It's better to have RSA certificates for the purpose " + "of backward compatibility with legacy clients and servers, " + "thus the default is 'rsa'.")}}]}, + {allow_contrib_modules, + #{value => "true | false", + desc => + ?T("Whether to allow installation of third-party modules or not. " + "See _`../../admin/guide/modules.md#ejabberd-contrib|ejabberd-contrib`_ " + "documentation section. " + "The default value is 'true'.")}}, + {allow_multiple_connections, + #{value => "true | false", + desc => + ?T("This option is only used when the anonymous mode is enabled. " + "Setting it to 'true' means that the same username can be " + "taken multiple times in anonymous login mode if different " + "resource are used to connect. This option is only useful " + "in very special occasions. The default value is 'false'.")}}, + {anonymous_protocol, + #{value => "login_anon | sasl_anon | both", + desc => + [?T("Define what " + "_`authentication.md#anonymous-login-and-sasl-anonymous|anonymous`_ " + "protocol will be used: "), "", + ?T("* 'login_anon' means that the anonymous login method will be used. "), "", + ?T("* 'sasl_anon' means that the SASL Anonymous method will be used. "), "", + ?T("* 'both' means that SASL Anonymous and login anonymous are both " + "enabled."), "", + ?T("The default value is 'sasl_anon'."), ""]}}, + {api_permissions, + #{value => "[Permission, ...]", + desc => + ?T("Define the permissions for API access. Please consult the " + "ejabberd Docs web -> For Developers -> ejabberd ReST API -> " + "_`../../developer/ejabberd-api/permissions.md|API Permissions`_.")}}, + {append_host_config, + #{value => "{Host: Options}", + desc => + ?T("Add a few specific options to a certain " + "_`../configuration/basic.md#virtual-hosting|virtual host`_.")}}, + {auth_cache_life_time, + #{value => "timeout()", + desc => + ?T("Same as _`cache_life_time`_, but applied to authentication cache " + "only. If not set, the value from _`cache_life_time`_ will be used.")}}, + {auth_cache_missed, + #{value => "true | false", + desc => + ?T("Same as _`cache_missed`_, but applied to authentication cache " + "only. If not set, the value from _`cache_missed`_ will be used.")}}, + {auth_cache_size, + #{value => "pos_integer() | infinity", + desc => + ?T("Same as _`cache_size`_, but applied to authentication cache " + "only. If not set, the value from _`cache_size`_ will be used.")}}, + {auth_method, + #{value => "[mnesia | sql | anonymous | external | jwt | ldap | pam, ...]", + desc => + ?T("A list of _`authentication.md|authentication`_ methods to use. " + "If several methods are defined, authentication is " + "considered successful as long as authentication of " + "at least one of the methods succeeds. " + "The default value is '[mnesia]'.")}}, + {auth_opts, + #{value => "[Option, ...]", + desc => + ?T("This is used by the contributed module " + "'ejabberd_auth_http' that can be installed from the " + "_`../../admin/guide/modules.md#ejabberd-contrib|ejabberd-contrib`_ " + "Git repository. Please refer to that " + "module's README file for details.")}}, + {auth_password_format, + #{value => "plain | scram", + note => "improved in 20.01", + desc => + [?T("The option defines in what format the users passwords " + "are stored, plain text or in _`authentication.md#scram|SCRAM`_ format:"), "", + ?T("* 'plain': 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/256/512(-PLUS). "), "", + ?T("* 'scram': The password is not stored, only some information " + "required 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/256/512(-PLUS). The SCRAM variant " + "depends on the _`auth_scram_hash`_ option."), "", + ?T("The default value is 'plain'."), ""]}}, + + {auth_password_types_hidden_in_sasl1, + #{value => "[plain | scram_sha1 | scram_sha256 | scram_sha512]", + note => "added in 25.07", + desc => + ?T("List of password types that should not be offered in SASL1 authenticatication. " + "Because SASL1, unlike SASL2, can't have list of available mechanisms tailored to " + "individual user, it's possible that offered mechanisms will not be compatible " + "with stored password, especially if new password type was added recently. " + "This option allows disabling offering some mechanisms in SASL1, to a time until new " + "password type will be available for all users.")}}, + {auth_scram_hash, + #{value => "sha | sha256 | sha512", + desc => + ?T("Hash algorithm that should be used to store password in _`authentication.md#scram|SCRAM`_ format. " + "You shouldn't 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 'sha'.")}}, + {auth_stored_password_types, + #{value => "[plain | scram_sha1 | scram_sha256 | scram_sha512]", + note => "added in 25.03", + desc => + ?T("List of password types that should be stored simultaneously for each user in database. " + "When the user sets the account password, database will be updated to store the password in formats " + "compatible with each type listed here. This can be used to migrate user passwords " + "to a more secure format. If this option if set, it will override values set in _`auth_scram_hash`_ " + "and _`auth_password_format`_ options. The default value is `[]`.")}}, + {auth_external_user_exists_check, + #{value => "true | false", + note => "added in 23.10", + desc => + ?T("Supplement check for user existence based on _`mod_last`_ data, for authentication " + "methods that don't have a way to reliably tell if a user exists (like is the case for " + "'jwt' and certificate based authentication). This helps with processing offline message " + "for those users. The default value is 'true'.")}}, + {auth_use_cache, + #{value => "true | false", + desc => + ?T("Same as _`use_cache`_, but applied to authentication cache " + "only. If not set, the value from _`use_cache`_ will be used.")}}, + {c2s_cafile, + #{value => ?T("Path"), + desc => + [?T("Full path to a file containing one or more CA certificates " + "in PEM format. All client certificates should be signed by " + "one of these root CA certificates and should contain the " + "corresponding JID(s) in 'subjectAltName' field. " + "There is no default value."), "", + ?T("You can use _`host_config`_ to specify this option per-vhost."), "", + ?T("To set a specific file per listener, use the listener's _`listen-options.md#cafile|cafile`_ option. Please notice that 'c2s_cafile' overrides the listener's 'cafile' option."), "" + ]}}, + {c2s_ciphers, + #{value => "[Cipher, ...]", + desc => + ?T("A list of OpenSSL ciphers to use for c2s connections. " + "The default value is shown in the example below:"), + example => + ["c2s_ciphers:", + " - HIGH", + " - \"!aNULL\"", + " - \"!eNULL\"", + " - \"!3DES\"", + " - \"@STRENGTH\""]}}, + {c2s_dhfile, + #{value => ?T("Path"), + desc => + ?T("Full path to a file containing custom DH parameters " + "to use for c2s connections. " + "Such a file could be created with the command '\"openssl " + "dhparam -out dh.pem 2048\"'. If this option is not specified, " + "2048-bit MODP Group with 256-bit Prime Order Subgroup will be " + "used as defined in RFC5114 Section 2.3.")}}, + {c2s_protocol_options, + #{value => "[Option, ...]", + desc => + ?T("List of general SSL options to use for c2s connections. " + "These map to OpenSSL's 'set_options()'. The default value is " + "shown in the example below:"), + example => + ["c2s_protocol_options:", + " - no_sslv3", + " - cipher_server_preference", + " - no_compression"]}}, + {c2s_tls_compression, + #{value => "true | false", + desc => + ?T("Whether to enable or disable TLS compression for c2s connections. " + "The default value is 'false'.")}}, + {ca_file, + #{value => ?T("Path"), + desc => + [?T("Path to a file of CA root certificates. " + "The default is to use system defined file if possible."), "", + ?T("For server connections, this 'ca_file' option is overridden by the _`s2s_cafile`_ option."), "" + ]}}, + {captcha_cmd, + #{value => ?T("Path | ModuleName"), + note => "improved in 23.01", + desc => + ?T("Full path to a script that generates _`basic.md#captcha|CAPTCHA`_ images. " + "The keyword '@VERSION@' is replaced with ejabberd version number in 'XX.YY' format. " + "The keyword '@SEMVER@' is replaced with ejabberd version number in semver format " + "when compiled with Elixir's mix, or XX.YY format otherwise. " + "Alternatively, it can be the name of a module that implements ejabberd CAPTCHA support. " + "There is no default value: when this option is not " + "set, CAPTCHA functionality is completely disabled."), + example => + [{?T("When using the ejabberd installers or container image, the example captcha scripts can be used like this:"), + ["captcha_cmd: /opt/ejabberd-@VERSION@/lib/ejabberd-@SEMVER@/priv/bin/captcha.sh"]}]}}, + {captcha_limit, + #{value => "pos_integer() | infinity", + desc => + ?T("Maximum number of _`basic.md#captcha|CAPTCHA`_ generated images per minute for " + "any given JID. The option is intended to protect the server " + "from CAPTCHA DoS. The default value is 'infinity'.")}}, + {captcha_host, + #{value => "String", + desc => ?T("Deprecated. Use _`captcha_url`_ instead.")}}, + {captcha_url, + #{value => ?T("URL | auto | undefined"), + note => "improved in 23.04", + desc => + ?T("An URL where _`basic.md#captcha|CAPTCHA`_ requests should be sent. NOTE: you need " + "to configure 'request_handlers' for 'ejabberd_http' listener " + "as well. " + "If set to 'auto', it builds the URL using a 'request_handler' " + "already enabled, with encryption if available. " + "If set to 'undefined', it builds the URL using " + "the deprecated _`captcha_host`_ '+ /captcha'. " + "The default value is 'auto'.")}}, + {certfiles, + #{value => "[Path, ...]", + desc => + ?T("The option accepts a list of file paths (optionally with " + "wildcards) containing either PEM certificates or PEM private " + "keys. At startup or configuration reload, ejabberd reads all " + "certificates from these files, sorts them, removes duplicates, " + "finds matching private keys and then rebuilds full certificate " + "chains for the use in TLS connections. " + "Use this option when TLS is enabled in either of " + "ejabberd listeners: 'ejabberd_c2s', 'ejabberd_http' and so on. " + "NOTE: if you modify the certificate files or change the value " + "of the option, run 'ejabberdctl reload-config' in order to " + "rebuild and reload the certificate chains."), + example => + [{?T("If you use https://letsencrypt.org[Let's Encrypt] certificates " + "for your domain \"domain.tld\", the configuration will look " + "like this:"), + ["certfiles:", + " - /etc/letsencrypt/live/domain.tld/fullchain.pem", + " - /etc/letsencrypt/live/domain.tld/privkey.pem"]}]}}, + {cluster_backend, + #{value => ?T("Backend"), + desc => + ?T("A database backend to use for storing information about " + "cluster. The only available value so far is 'mnesia'.")}}, + {cluster_nodes, + #{value => "[Node, ...]", + desc => + ?T("A list of Erlang nodes to connect on ejabberd startup. " + "This option is mostly intended for ejabberd customization " + "and sophisticated setups. The default value is an empty list.")}}, + {define_keyword, + #{value => "{NAME: Value}", + note => "added in 25.03", + desc => + ?T("Allows to define configuration " + "_`../configuration/file-format.md#macros-and-keywords|keywords`_. "), + example => + ["define_keyword:", + " SQL_USERNAME: \"eja.global\"", + "", + "host_config:", + " localhost:", + " define_keyword:", + " SQL_USERNAME: \"eja.localhost\"", + "", + "sql_username: \"prefix.@SQL_USERNAME@\""]}}, + {define_macro, + #{value => "{NAME: Value}", + note => "improved in 25.03", + desc => + ?T("Allows to define configuration " + "_`../configuration/file-format.md#macros-and-keywords|macros`_. "), + example => + ["define_macro:", + " DEBUG: debug", + " LOG_LEVEL: DEBUG", + " USERBOB:", + " user: bob@localhost", + "", + "loglevel: LOG_LEVEL", + "", + "acl:", + " admin: USERBOB"]}}, + {disable_sasl_scram_downgrade_protection, + #{value => "true | false", + desc => + ?T("Allows to disable sending data required by " + "'XEP-0474: SASL SCRAM Downgrade Protection'. " + "There are known buggy clients (like those that use strophejs 1.6.2) " + "which will not be able to authenticatate when servers sends data from " + "that specification. This options allows server to disable it to allow " + "even buggy clients connects, but in exchange decrease MITM protection. " + "The default value of this option is 'false' which enables this extension.")}}, + {disable_sasl_mechanisms, + #{value => "[Mechanism, ...]", + desc => + ?T("Specify a list of SASL mechanisms (such as 'DIGEST-MD5' or " + "'SCRAM-SHA1') that should not be offered to the client. " + "For convenience, the value of 'Mechanism' is case-insensitive. " + "The default value is an empty list, i.e. no mechanisms " + "are disabled by default.")}}, + {domain_balancing, + #{value => "{Domain: Options}", + desc => + ?T("An algorithm to " + "_`../guide/clustering.md#service-load-balancing|load-balance`_ " + "the components that are plugged " + "on an ejabberd cluster. It means that you can plug one or several " + "instances of the same component on each ejabberd node and that " + "the traffic will be automatically distributed. The algorithm " + "to deliver messages to the component(s) can be specified by " + "this option. For any component connected as 'Domain', available " + "'Options' are:"), + example => + ["domain_balancing:", + " component.domain.tld:", + " type: destination", + " component_number: 5", + " transport.example.org:", + " type: bare_source"]}, + [{type, + #{value => ?T("Value"), + desc => + ?T("How to deliver stanzas to connected components. " + "The default value is 'random'. Possible values: ")}, + [{'- random', + #{desc => + ?T("an instance is chosen at random")}}, + {'- source', + #{desc => + ?T("by the full JID of the packet's 'from' attribute")}}, + {'- bare_destination', + #{desc => + ?T("by the bare JID (without resource) of the packet's 'to' attribute")}}, + {'- bare_source', + #{desc => + ?T("by the bare JID (without resource) of the packet's 'from' attribute is used")}}, + {'- destination', + #{desc => + ?T("an instance is chosen by the full JID of the packet's 'to' attribute")}}]}, + {component_number, + #{value => "2..1000", + desc => + ?T("The number of components to balance.")}}]}, + {extauth_pool_name, + #{value => ?T("Name"), + desc => + ?T("Define the pool name appendix in " + "_`authentication.md#external-script|external auth`_, " + "so the full pool name will be " + "'extauth_pool_Name'. The default value is the hostname.")}}, + {extauth_pool_size, + #{value => ?T("Size"), + desc => + ?T("The option defines the number of instances of the same " + "_`authentication.md#external-script|external auth`_ " + "program to start for better load balancing. " + "The default is the number of available CPU cores.")}}, + {extauth_program, + #{value => ?T("Path"), + desc => + ?T("Indicate in this option the full path to the " + "_`authentication.md#external-script|external authentication script`_. " + "The script must be executable by ejabberd.")}}, + {ext_api_headers, + #{value => "Headers", + desc => + ?T("String of headers (separated with commas ',') that will be " + "provided by ejabberd when sending ReST requests. " + "The default value is an empty string of headers: '\"\"'.")}}, + {ext_api_http_pool_size, + #{value => "pos_integer()", + desc => + ?T("Define the size of the HTTP pool, that is, the maximum number " + "of sessions that the ejabberd ReST service will handle " + "simultaneously. The default value is: '100'.")}}, + {ext_api_path_oauth, + #{value => "Path", + desc => + ?T("Define the base URI path when performing OAUTH ReST requests. " + "The default value is: '\"/oauth\"'.")}}, + {ext_api_url, + #{value => "URL", + desc => + ?T("Define the base URI when performing ReST requests. " + "The default value is: '\"http://localhost/api\"'.")}}, + {fqdn, + #{value => ?T("Domain"), + desc => + ?T("A fully qualified domain name that will be used in " + "SASL DIGEST-MD5 authentication. The default is detected " + "automatically.")}}, + {hide_sensitive_log_data, + #{value => "true | false", + desc => + ?T("A privacy option to not log sensitive data " + "(mostly IP addresses). The default value " + "is 'false' for backward compatibility.")}}, + {host_config, + #{value => "{Host: Options}", + desc => + ?T("The option is used to redefine 'Options' for " + "_`../configuration/basic.md#virtual-hosting|virtual host`_ " + "'Host'. " + "In the example below LDAP authentication method " + "will be used on virtual host 'domain.tld' and SQL method " + "will be used on virtual host 'example.org'."), + example => + ["hosts:", + " - domain.tld", + " - example.org", + "", + "auth_method:", + " - sql", + "", + "host_config:", + " domain.tld:", + " auth_method:", + " - ldap"]}}, + {hosts_alias, + #{value => "{Alias: Host}", + desc => + ?T("Define aliases for existing vhosts managed by ejabberd. " + "An alias may be a regexp expression. " + "This option is only consulted by the 'ejabberd_http' listener."), + note => "added in 25.07", + example => + ["hosts:", + " - domain.tld", + " - example.org", + "", + "hosts_alias:", + " xmpp.domain.tld: domain.tld", + " jabber.domain.tld: domain.tld", + " mytest.net: example.org", + " \"exa*\": example.org"]}}, + {include_config_file, + #{value => "[Filename, ...\\] | {Filename: Options}", + desc => + ?T("Read and " + "_`../configuration/file-format.md#include-additional-files|include additional file`_ " + "from 'Filename'. If the " + "value is provided in 'pass:[{Filename: Options}]' format, the " + "'Options' must be one of the following:")}, + [{disallow, + #{value => "[OptionName, ...]", + desc => + ?T("Disallows the usage of those options in the included " + "file 'Filename'. The options that match this criteria " + "are not accepted. The default value is an empty list.")}}, + {allow_only, + #{value => "[OptionName, ...]", + desc => + ?T("Allows only the usage of those options in the included " + "file 'Filename'. The options that do not match this " + "criteria are not accepted. The default value is to include " + "all options.")}}]}, + {install_contrib_modules, + #{value => "[Module, ...]", + note => "added in 23.10", + desc => + ?T("Modules to install from " + "_`../../admin/guide/modules.md#ejabberd-contrib|ejabberd-contrib`_ " + "at start time. " + "The default value is an empty list of modules: '[]'.")}}, + {jwt_auth_only_rule, + #{value => ?T("AccessName"), + desc => + ?T("This ACL rule defines accounts that can use only the " + "_`authentication.md#jwt-authentication|JWT`_ auth " + "method, even if others are also defined in the ejabberd " + "configuration file. In other words: if there are several auth " + "methods enabled for this host (JWT, SQL, ...), users that " + "match this rule can only use JWT. " + "The default value is 'none'.")}}, + {jwt_jid_field, + #{value => ?T("FieldName"), + desc => + ?T("By default, the JID is defined in the '\"jid\"' JWT field. " + "In this option you can specify other " + "_`authentication.md#jwt-authentication|JWT`_ " + "field name " + "where the JID is defined.")}}, + {jwt_key, + #{value => ?T("FilePath"), + desc => + ?T("Path to the file that contains the " + "_`authentication.md#jwt-authentication|JWT`_ key. " + "The default value is 'undefined'.")}}, + {language, + #{value => ?T("Language"), + desc => + ?T("Define the " + "_`../configuration/basic.md#default-language|default language`_ " + "of server strings " + "that can be seen by XMPP clients. If an XMPP client does not " + "possess 'xml:lang' attribute, the specified language is used. " + "The default value is '\"en\"'. ")}}, + {ldap_servers, + #{value => "[Host, ...]", + desc => + ?T("A list of IP addresses or DNS names of your LDAP servers (see " + "_`../configuration/ldap.md#ldap-connection|LDAP connection`_). " + "ejabberd connects immediately to all of them, " + "and reconnects infinitely if connection is lost. " + "The default value is '[localhost]'.")}}, + {ldap_backups, + #{value => "[Host, ...]", + desc => + ?T("A list of IP addresses or DNS names of LDAP backup servers (see " + "_`../configuration/ldap.md#ldap-connection|LDAP connection`_). " + "When no servers listed in _`ldap_servers`_ option are reachable, " + "ejabberd connects to these backup servers. " + "The default is an empty list, i.e. no backup servers specified. " + "Please notice that ejabberd only connects to the next server " + "when the existing connection is lost; it doesn't detect when a " + "previously-attempted server becomes available again.")}}, + {ldap_encrypt, + #{value => "tls | none", + desc => + ?T("Whether to encrypt LDAP connection using TLS or not. " + "The default value is 'none'. NOTE: STARTTLS encryption " + "is not supported.")}}, + {ldap_tls_certfile, + #{value => ?T("Path"), + desc => + ?T("A path to a file containing PEM encoded certificate " + "along with PEM encoded private key. This certificate " + "will be provided by ejabberd when TLS enabled for " + "LDAP connections. There is no default value, which means " + "no client certificate will be sent.")}}, + {ldap_tls_verify, + #{value => "false | soft | hard", + desc => + ?T("This option specifies whether to verify LDAP server " + "certificate or not when TLS is enabled. When 'hard' is set, " + "ejabberd doesn't proceed if the certificate is invalid. " + "When 'soft' is set, ejabberd proceeds even if the check has failed. " + "The default is 'false', which means no checks are performed.")}}, + {ldap_tls_cacertfile, + #{value => ?T("Path"), + desc => + ?T("A path to a file containing PEM encoded CA certificates. " + "This option is required when TLS verification is enabled.")}}, + {ldap_tls_depth, + #{value => ?T("Number"), + desc => + ?T("Specifies the maximum verification depth when TLS verification " + "is enabled, i.e. how far in a chain of certificates the " + "verification process can proceed before the verification " + "is considered to be failed. Peer certificate = 0, " + "CA certificate = 1, higher level CA certificate = 2, etc. " + "The value '2' thus means that a chain can at most contain " + "peer cert, CA cert, next CA cert, and an additional CA cert. " + "The default value is '1'.")}}, + {ldap_port, + #{value => "1..65535", + desc => + ?T("Port to connect to your LDAP server. The default port is " + "'389' if encryption is disabled and '636' if encryption is " + "enabled.")}}, + {ldap_rootdn, + #{value => "RootDN", + desc => + ?T("Bind Distinguished Name. The default value is an empty " + "string, which means \"anonymous connection\".")}}, + {ldap_password, + #{value => ?T("Password"), + desc => + ?T("Bind password. The default value is an empty string.")}}, + {ldap_deref_aliases, + #{value => "never | always | finding | searching", + desc => + ?T("Whether to dereference aliases or not. " + "The default value is 'never'.")}}, + {ldap_base, + #{value => "Base", + desc => + ?T("LDAP base directory which stores users accounts. " + "There is no default value: you must set the option " + "in order for LDAP connections to work properly.")}}, + {ldap_uids, + #{value => "[Attr\\] | {Attr: AttrFormat}", + desc => + ?T("LDAP attributes which hold a list of attributes to use " + "as alternatives for getting the JID, where 'Attr' is " + "an LDAP attribute which holds the user's part of the JID and " + "'AttrFormat' must contain one and only one pattern variable " + "'\"%u\"' which will be replaced by the user's part of the JID. " + "For example, '\"%u@example.org\"'. If the value is in the form " + "of '[Attr]' then 'AttrFormat' is assumed to be '\"%u\"'.")}}, + {ldap_filter, + #{value => ?T("Filter"), + desc => + ?T("An LDAP filter as defined in " + "https://tools.ietf.org/html/rfc4515[RFC4515]. " + "There is no default value. Example: " + "'\"(&(objectClass=shadowAccount)(memberOf=XMPP Users))\"'. " + "NOTE: don't forget to close brackets and don't use superfluous " + "whitespaces. Also you must not use '\"uid\"' attribute in the " + "filter because this attribute will be appended to the filter " + "automatically.")}}, + {ldap_dn_filter, + #{value => "{Filter: FilterAttrs}", + desc => + ?T("This filter is applied on the results returned by the main " + "filter. The filter performs an additional LDAP lookup to make " + "the complete result. This is useful when you are unable to " + "define all filter rules in 'ldap_filter'. You can define " + "'\"%u\"', '\"%d\"', '\"%s\"' and '\"%D\"' pattern variables in 'Filter: " + "\"%u\"' is replaced by a user's part of the JID, '\"%d\"' is " + "replaced by the corresponding domain (virtual host), all '\"%s\"' " + "variables are consecutively replaced by values from the attributes " + "in 'FilterAttrs' and '\"%D\"' is replaced by Distinguished Name from " + "the result set. There is no default value, which means the " + "result is not filtered. WARNING: Since this filter makes " + "additional LDAP lookups, use it only as the last resort: " + "try to define all filter rules in _`ldap_filter`_ option if possible."), + example => + ["ldap_dn_filter:", + " \"(&(name=%s)(owner=%D)(user=%u@%d))\": [sn]"]}}, + {log_rotate_count, + #{value => ?T("Number"), + desc => + ?T("The number of rotated log files to keep. " + "The default value is '1', which means that only keeps " + "`ejabberd.log.0`, `error.log.0` and `crash.log.0`.")}}, + {log_rotate_size, + #{value => "pos_integer() | infinity", + desc => + ?T("The size (in bytes) of a log file to trigger rotation. " + "If set to 'infinity', log rotation is disabled. " + "The default value is 10 Mb expressed in bytes: '10485760'.")}}, + {log_burst_limit_count, + #{value => ?T("Number"), + note => "added in 22.10", + desc => + ?T("The number of messages to accept in " + "`log_burst_limit_window_time` period before starting to " + "drop them. Default `500`")}}, + {log_burst_limit_window_time, + #{value => ?T("Number"), + note => "added in 22.10", + desc => + ?T("The time period to rate-limit log messages " + "by. Defaults to `1` second.")}}, + {log_modules_fully, + #{value => "[Module, ...]", + note => "added in 23.01", + desc => + ?T("List of modules that will log everything " + "independently from the general loglevel option.")}}, + {max_fsm_queue, + #{value => ?T("Size"), + desc => + ?T("This option specifies the maximum number of elements " + "in the queue of the FSM (Finite State Machine). Roughly " + "speaking, each message in such queues represents one " + "XML stanza queued to be sent into its relevant outgoing " + "stream. If queue size reaches the limit (because, for " + "example, the receiver of stanzas is too slow), the FSM " + "and the corresponding connection (if any) will be terminated " + "and error message will be logged. The reasonable value for " + "this option depends on your hardware configuration. " + "The allowed values are positive integers. " + "The default value is '10000'.")}}, + {negotiation_timeout, + #{value => "timeout()", + desc => + ?T("Time to wait for an XMPP stream negotiation to complete. " + "When timeout occurs, the corresponding XMPP stream is closed. " + "The default value is '120' seconds.")}}, + {net_ticktime, + #{value => "timeout()", + desc => + ?T("This option can be used to tune tick time parameter of " + "'net_kernel'. 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 '1 minute'.")}}, + {new_sql_schema, + #{value => "true | false", + desc => + {?T("Whether to use the " + "_`database.md#default-and-new-schemas|new SQL schema`_. " + "All schemas are located " + "at . " + "There are two schemas available. The default legacy schema " + "stores one XMPP domain into one ejabberd database. " + "The 'new' schema can handle several XMPP domains in a " + "single ejabberd database. Using this 'new' schema is best when " + "serving several XMPP domains and/or changing domains from " + "time to time. This avoid need to manage several databases and " + "handle complex configuration changes. The default depends on " + "configuration flag '--enable-new-sql-schema' which is set " + "at compile time."), + [binary:part(ejabberd_config:version(), {0,5})]}}}, + {update_sql_schema, + #{value => "true | false", + note => "updated in 24.06", + desc => + ?T("Allow ejabberd to update SQL schema in " + "MySQL, PostgreSQL and SQLite databases. " + "This option was added in ejabberd 23.10, " + "and enabled by default since 24.06. " + "The default value is 'true'.")}}, + {update_sql_schema_timeout, + #{value => "timeout()", + note => "added in 24.07", + desc => + ?T("Time allocated to SQL schema update queries. " + "The default value is set to 5 minutes.")}}, + {oauth_access, + #{value => ?T("AccessName"), + desc => ?T("By default creating OAuth tokens is not allowed. " + "To define which users can create OAuth tokens, " + "you can refer to an ejabberd access rule in the " + "'oauth_access' option. Use 'all' to allow everyone " + "to create tokens.")}}, + {oauth_cache_life_time, + #{value => "timeout()", + desc => + ?T("Same as _`cache_life_time`_, but applied to OAuth cache " + "only. If not set, the value from _`cache_life_time`_ will be used.")}}, + {oauth_cache_missed, + #{value => "true | false", + desc => + ?T("Same as _`cache_missed`_, but applied to OAuth cache " + "only. If not set, the value from _`cache_missed`_ will be used.")}}, + {oauth_cache_rest_failure_life_time, + #{value => "timeout()", + note => "added in 21.01", + desc => + ?T("The time that a failure in OAuth ReST is cached. " + "The default value is 'infinity'.")}}, + {oauth_cache_size, + #{value => "pos_integer() | infinity", + desc => + ?T("Same as _`cache_size`_, but applied to OAuth cache " + "only. If not set, the value from _`cache_size`_ will be used.")}}, + {oauth_client_id_check, + #{value => "allow | db | deny", + desc => + ?T("Define whether the client authentication is always allowed, " + "denied, or it will depend if the client ID is present in the " + "database. The default value is 'allow'.")}}, + {oauth_use_cache, + #{value => "true | false", + desc => + ?T("Same as _`use_cache`_, but applied to OAuth cache " + "only. If not set, the value from _`use_cache`_ will be used.")}}, + {oauth_db_type, + #{value => "mnesia | sql", + desc => + ?T("Database backend to use for OAuth authentication. " + "The default value is picked from _`default_db`_ option, or " + "if it's not set, 'mnesia' will be used.")}}, + {oauth_expire, + #{value => "timeout()", + desc => + ?T("Time during which the OAuth token is valid, in seconds. " + "After that amount of time, the token expires and the delegated " + "credential cannot be used and is removed from the database. " + "The default is '4294967' seconds.")}}, + {oom_killer, + #{value => "true | false", + desc => + ?T("Enable or disable OOM (out-of-memory) killer. " + "When system memory raises above the limit defined in " + "_`oom_watermark`_ option, ejabberd triggers OOM killer " + "to terminate most memory consuming Erlang processes. " + "Note that in order to maintain functionality, ejabberd only " + "attempts to kill transient processes, such as those managing " + "client sessions, s2s or database connections. " + "The default value is 'true'.")}}, + {oom_queue, + #{value => ?T("Size"), + desc => + ?T("Trigger OOM killer when some of the running Erlang processes " + "have messages queue above this 'Size'. Note that " + "such processes won't be killed if _`oom_killer`_ option is set " + "to 'false' or if _`oom_watermark`_ is not reached yet.")}}, + {oom_watermark, + #{value => ?T("Percent"), + desc => + ?T("A percent of total system memory consumed at which " + "OOM killer should be activated with some of the processes " + "possibly be killed (see _`oom_killer`_ option). Later, when " + "memory drops below this 'Percent', OOM killer is deactivated. " + "The default value is '80' percents.")}}, + {outgoing_s2s_families, + #{value => "[ipv6 | ipv4, ...]", + note => "changed in 23.01", + desc => + ?T("Specify which address families to try, in what order. " + "The default is '[ipv6, ipv4]' which means it first tries " + "connecting with IPv6, if that fails it tries using IPv4. " + "This option is obsolete and irrelevant when using ejabberd 23.01 " + "and Erlang/OTP 22, or newer versions of them.")}}, + {outgoing_s2s_ipv4_address, + #{value => "Address", + note => "added in 20.12", + desc => + ?T("Specify the IPv4 address that will be used when establishing " + "an outgoing S2S IPv4 connection, for example '\"127.0.0.1\"'. " + "The default value is 'undefined'.")}}, + {outgoing_s2s_ipv6_address, + #{value => "Address", + note => "added in 20.12", + desc => + ?T("Specify the IPv6 address that will be used when establishing " + "an outgoing S2S IPv6 connection, for example " + "'\"::FFFF:127.0.0.1\"'. The default value is 'undefined'.")}}, + {outgoing_s2s_port, + #{value => "1..65535", + desc => + ?T("A port number to use for outgoing s2s connections when the target " + "server doesn't have an SRV record. The default value is '5269'.")}}, + {outgoing_s2s_timeout, + #{value => "timeout()", + desc => + ?T("The timeout in seconds for outgoing S2S connection attempts. " + "The default value is '10' seconds.")}}, + {pam_service, + #{value => ?T("Name"), + desc => + ?T("This option defines the " + "_`authentication.md#pam-authentication|PAM`_ " + "service name. Refer to the PAM " + "documentation of your operation system for more information. " + "The default value is 'ejabberd'.")}}, + {pam_userinfotype, + #{value => "username | jid", + desc => + ?T("This option defines what type of information about the " + "user ejabberd provides to the " + "_`authentication.md#pam-authentication|PAM`_ " + "service: only the username, " + "or the user's JID. Default is 'username'.")}}, + {pgsql_users_number_estimate, + #{value => "true | false", + desc => + ?T("Whether to use PostgreSQL estimation when counting registered " + "users. The default value is 'false'.")}}, + {queue_dir, + #{value => ?T("Directory"), + desc => + ?T("If _`queue_type`_ option is set to 'file', use this 'Directory' " + "to store file queues. The default is to keep queues inside " + "Mnesia directory.")}}, + {redis_connect_timeout, + #{value => "timeout()", + desc => + ?T("A timeout to wait for the connection to be re-established " + "to the _`database.md#redis|Redis`_ " + "server. The default is '1 second'.")}}, + {redis_db, + #{value => ?T("Number"), + desc => ?T("_`database.md#redis|Redis`_ " + "database number. The default is '0'.")}}, + {redis_password, + #{value => ?T("Password"), + desc => + ?T("The password to the _`database.md#redis|Redis`_ server. " + "The default is an empty string, i.e. no password.")}}, + {redis_pool_size, + #{value => ?T("Number"), + desc => + ?T("The number of simultaneous connections to the " + "_`database.md#redis|Redis`_ server. " + "The default value is '10'.")}}, + {redis_port, + #{value => "1..65535", + desc => + ?T("The port where the _`database.md#redis|Redis`_ " + " server is accepting connections. " + "The default is '6379'.")}}, + {redis_queue_type, + #{value => "ram | file", + desc => + ?T("The type of request queue for the " + "_`database.md#redis|Redis`_ server. " + "See description of _`queue_type`_ option for the explanation. " + "The default value is the value defined in _`queue_type`_ " + "or 'ram' if the latter is not set.")}}, + {redis_server, + #{value => "Host | IP Address | Unix Socket Path", + note => "improved in 24.12", + desc => + ?T("A hostname, IP address or unix domain socket file of the " + "_`database.md#redis|Redis`_ server. " + "Setup the path to unix domain socket like: '\"unix:/path/to/socket\"'. " + "The default value is 'localhost'.")}}, + {registration_timeout, + #{value => "timeout()", + desc => + ?T("This is a global option for module _`mod_register`_. " + "It limits the frequency of registrations from a given " + "IP or username. So, a user that tries to register a " + "new account from the same IP address or JID during " + "this time after their previous registration " + "will receive an error with the corresponding explanation. " + "To disable this limitation, set the value to 'infinity'. " + "The default value is '600 seconds'.")}}, + {resource_conflict, + #{value => "setresource | closeold | closenew", + desc => + ?T("NOTE: this option is deprecated and may be removed " + "anytime in the future versions. The possible values " + "match exactly the three possibilities described in " + "https://tools.ietf.org/html/rfc6120#section-7.7.2.2" + "[XMPP Core: section 7.7.2.2]. " + "The default value is 'closeold'. If the client " + "uses old Jabber Non-SASL authentication (XEP-0078), " + "then this option is not respected, and the action performed " + "is 'closeold'.")}}, + {rest_proxy, + #{value => "Host", + note => "added in 25.07", + desc => ?T("Address of a HTTP Connect proxy used by modules issuing rest calls " + "(like ejabberd_oauth_rest)")}}, + {rest_proxy_port, + #{value => "1..65535", + note => "added in 25.07", + desc => ?T("Port of a HTTP Connect proxy used by modules issuing rest calls " + "(like ejabberd_oauth_rest)")}}, + {rest_proxy_username, + #{value => "string()", + note => "added in 25.07", + desc => ?T("Username used to authenticate to HTTP Connect proxy used by modules issuing rest calls " + "(like ejabberd_oauth_rest)")}}, + {rest_proxy_password, + #{value => "string()", + note => "added in 25.07", + desc => ?T("Password used to authenticate to HTTP Connect proxy used by modules issuing rest calls " + "(like ejabberd_oauth_rest)")}}, + {router_cache_life_time, + #{value => "timeout()", + desc => + ?T("Same as _`cache_life_time`_, but applied to routing table cache " + "only. If not set, the value from _`cache_life_time`_ will be used.")}}, + {router_cache_missed, + #{value => "true | false", + desc => + ?T("Same as _`cache_missed`_, but applied to routing table cache " + "only. If not set, the value from _`cache_missed`_ will be used.")}}, + {router_cache_size, + #{value => "pos_integer() | infinity", + desc => + ?T("Same as _`cache_size`_, but applied to routing table cache " + "only. If not set, the value from _`cache_size`_ will be used.")}}, + {router_db_type, + #{value => "mnesia | redis | sql", + desc => + ?T("Database backend to use for routing information. " + "The default value is picked from _`default_ram_db`_ option, or " + "if it's not set, 'mnesia' will be used.")}}, + {router_use_cache, + #{value => "true | false", + desc => + ?T("Same as _`use_cache`_, but applied to routing table cache " + "only. If not set, the value from _`use_cache`_ will be used.")}}, + {rpc_timeout, + #{value => "timeout()", + desc => + ?T("A timeout for remote function calls between nodes " + "in an ejabberd cluster. You should probably never change " + "this value since those calls are used for internal needs " + "only. The default value is '5' seconds.")}}, + {s2s_access, + #{value => ?T("Access"), + desc => + ?T("This _`basic.md#access-rules|Access Rule`_ defines to " + "what remote servers can s2s connections be established. " + "The default value is 'all'; no restrictions are applied, it is" + " allowed to connect s2s to/from all remote servers.")}}, + {s2s_cafile, + #{value => ?T("Path"), + desc => + [?T("A path to a file with CA root certificates that will " + "be used to authenticate s2s connections. If not set, " + "the value of _`ca_file`_ will be used."), "", + ?T("You can use _`host_config`_ to specify this option per-vhost."), "" + ]}}, + {s2s_ciphers, + #{value => "[Cipher, ...]", + desc => + ?T("A list of OpenSSL ciphers to use for s2s connections. " + "The default value is shown in the example below:"), + example => + ["s2s_ciphers:", + " - HIGH", + " - \"!aNULL\"", + " - \"!eNULL\"", + " - \"!3DES\"", + " - \"@STRENGTH\""]}}, + {s2s_dhfile, + #{value => ?T("Path"), + desc => + ?T("Full path to a file containing custom DH parameters " + "to use for s2s connections. " + "Such a file could be created with the command '\"openssl " + "dhparam -out dh.pem 2048\"'. If this option is not specified, " + "2048-bit MODP Group with 256-bit Prime Order Subgroup will be " + "used as defined in RFC5114 Section 2.3.")}}, + {s2s_protocol_options, + #{value => "[Option, ...]", + desc => + ?T("List of general SSL options to use for s2s connections. " + "These map to OpenSSL's 'set_options()'. The default value is " + "shown in the example below:"), + example => + ["s2s_protocol_options:", + " - no_sslv3", + " - cipher_server_preference", + " - no_compression"]}}, + {s2s_tls_compression, + #{value => "true | false", + desc => + ?T("Whether to enable or disable TLS compression for s2s connections. " + "The default value is 'false'.")}}, + {s2s_dns_retries, + #{value => ?T("Number"), + desc => + ?T("DNS resolving retries. The default value is '2'.")}}, + {s2s_dns_timeout, + #{value => "timeout()", + desc => + ?T("The timeout for DNS resolving. The default value is '10' seconds.")}}, + {s2s_max_retry_delay, + #{value => "timeout()", + desc => + ?T("The maximum allowed delay for s2s connection retry to connect after a " + "failed connection attempt. The default value is '300' seconds " + "(5 minutes).")}}, + {s2s_queue_type, + #{value => "ram | file", + desc => + ?T("The type of a queue for s2s packets. " + "See description of _`queue_type`_ option for the explanation. " + "The default value is the value defined in _`queue_type`_ " + "or 'ram' if the latter is not set.")}}, + {s2s_timeout, + #{value => "timeout()", + desc => + ?T("A time to wait before closing an idle s2s connection. " + "The default value is '1' hour.")}}, + {s2s_use_starttls, + #{value => "true | false | optional | required", + desc => + ?T("Whether to use STARTTLS for s2s connections. " + "The value of 'false' means STARTTLS is prohibited. " + "The value of 'true' or 'optional' means STARTTLS is enabled " + "but plain connections are still allowed. And the value of " + "'required' means that only STARTTLS connections are allowed. " + "The default value is 'false' (for historical reasons).")}}, + {s2s_zlib, + #{value => "true | false", + desc => + ?T("Whether to use 'zlib' compression (as defined in " + "https://xmpp.org/extensions/xep-0138.html[XEP-0138]) or not. " + "The default value is 'false'. WARNING: this type " + "of compression is nowadays considered insecure.")}}, + {shaper, + #{value => "{ShaperName: Rate}", + desc => + ?T("The option defines a set of " + "_`../configuration/basic.md#shapers|shapers`_. " + "Every shaper is assigned " + "a name 'ShaperName' that can be used in other parts of the " + "configuration file, such as _`shaper_rules`_ option. The shaper " + "itself is defined by its 'Rate', where 'Rate' stands for the " + "maximum allowed incoming rate in **bytes** per second. " + "When a connection exceeds this limit, ejabberd stops reading " + "from the socket until the average rate is again below the " + "allowed maximum. In the example below shaper 'normal' limits " + "the traffic speed to 1,000 bytes/sec and shaper 'fast' limits " + "the traffic speed to 50,000 bytes/sec:"), + example => + ["shaper:", + " normal: 1000", + " fast: 50000"]}}, + {shaper_rules, + #{value => "{ShaperRuleName: {Number|ShaperName: ACLName|ACLDefinition}}", + desc => + ?T("This option defines " + "_`../configuration/basic.md#shaper-rules|shaper rules`_ " + "to use for matching user/hosts. " + "Semantics is similar to _`access_rules`_ option, the only difference is " + "that instead using 'allow' or 'deny', a name of a shaper (defined in " + "_`shaper`_ option) or a positive number should be used."), + example => + ["shaper_rules:", + " connections_limit:", + " 10:", + " user: peter@example.com", + " 100: admin", + " 5: all", + " download_speed:", + " fast: admin", + " slow: anonymous_users", + " normal: all", + " log_days: 30"]}}, + {sm_cache_life_time, + #{value => "timeout()", + desc => + ?T("Same as _`cache_life_time`_, but applied to client sessions table cache " + "only. If not set, the value from _`cache_life_time`_ will be used.")}}, + {sm_cache_missed, + #{value => "true | false", + desc => + ?T("Same as _`cache_missed`_, but applied to client sessions table cache " + "only. If not set, the value from _`cache_missed`_ will be used.")}}, + {sm_cache_size, + #{value => "pos_integer() | infinity", + desc => + ?T("Same as _`cache_size`_, but applied to client sessions table cache " + "only. If not set, the value from _`cache_size`_ will be used.")}}, + {sm_db_type, + #{value => "mnesia | redis | sql", + desc => + ?T("Database backend to use for client sessions information. " + "The default value is picked from _`default_ram_db`_ option, or " + "if it's not set, 'mnesia' will be used.")}}, + {sm_use_cache, + #{value => "true | false", + desc => + ?T("Same as _`use_cache`_, but applied to client sessions table cache " + "only. If not set, the value from _`use_cache`_ will be used.")}}, + {sql_type, + #{value => "mssql | mysql | odbc | pgsql | sqlite", + desc => + ?T("The type of an SQL connection. The default is 'odbc'.")}}, + {sql_connect_timeout, + #{value => "timeout()", + desc => + ?T("A time to wait for connection to an SQL server to be " + "established. The default value is '5' seconds.")}}, + {sql_database, + #{value => ?T("Database"), + desc => + ?T("An SQL database name. For SQLite this must be a full " + "path to a database file. The default value is 'ejabberd'.")}}, + {sql_keepalive_interval, + #{value => "timeout()", + desc => + ?T("An interval to make a dummy SQL request to keep alive the " + "connections to the database. There is no default value, so no " + "keepalive requests are made.")}}, + {sql_odbc_driver, + #{value => "Path", + note => "added in 20.12", + desc => + ?T("Path to the ODBC driver to use to connect to a Microsoft SQL " + "Server database. This option only applies if the _`sql_type`_ " + "option is set to 'mssql' and _`sql_server`_ is not an ODBC " + "connection string. The default value is: 'libtdsodbc.so'")}}, + {sql_password, + #{value => ?T("Password"), + desc => + ?T("The password for SQL authentication. The default is empty string.")}}, + {sql_pool_size, + #{value => ?T("Size"), + desc => + ?T("Number of connections to the SQL server that ejabberd will " + "open for each virtual host. The default value is '10'. WARNING: " + "for SQLite this value is '1' by default and it's not recommended " + "to change it due to potential race conditions.")}}, + {sql_port, + #{value => "1..65535", + desc => + ?T("The port where the SQL server is accepting connections. " + "The default is '3306' for MySQL, '5432' for PostgreSQL and " + "'1433' for MS SQL. The option has no effect for SQLite.")}}, + {sql_prepared_statements, + #{value => "true | false", + note => "added in 20.01", + desc => + ?T("This option is 'true' by default, and is useful to disable " + "prepared statements. The option is valid for PostgreSQL and MySQL.")}}, + {sql_flags, + #{value => "[mysql_alternative_upsert]", + note => "added in 24.02", + desc => + ?T("This option accepts a list of SQL flags, and is empty by default. " + "'mysql_alternative_upsert' forces the alternative upsert implementation in MySQL.")}}, + {sql_query_timeout, + #{value => "timeout()", + desc => + ?T("A time to wait for an SQL query response. " + "The default value is '60' seconds.")}}, + {sql_queue_type, + #{value => "ram | file", + desc => + ?T("The type of a request queue for the SQL server. " + "See description of _`queue_type`_ option for the explanation. " + "The default value is the value defined in _`queue_type`_ " + "or 'ram' if the latter is not set.")}}, + {sql_server, + #{value => "Host | IP Address | ODBC Connection String | Unix Socket Path", + note => "improved in 24.06", + desc => + ?T("The hostname or IP address of the SQL server. For _`sql_type`_ " + "'mssql' or 'odbc' this can also be an ODBC connection string. " + "When _`sql_type`_ is 'mysql' or 'pgsql', this can be the path to " + "a unix domain socket expressed like: '\"unix:/path/to/socket\"'." + "The default value is 'localhost'.")}}, + {sql_ssl, + #{value => "true | false", + note => "improved in 20.03", + desc => + ?T("Whether to use SSL encrypted connections to the " + "SQL server. The option is only available for MySQL, MS SQL and " + "PostgreSQL. The default value is 'false'.")}}, + {sql_ssl_cafile, + #{value => ?T("Path"), + desc => + ?T("A path to a file with CA root certificates that will " + "be used to verify SQL connections. Implies _`sql_ssl`_ " + "and _`sql_ssl_verify`_ options are set to 'true'. " + "There is no default which means " + "certificate verification is disabled. " + "This option has no effect for MS SQL.")}}, + {sql_ssl_certfile, + #{value => ?T("Path"), + desc => + ?T("A path to a certificate file that will be used " + "for SSL connections to the SQL server. Implies _`sql_ssl`_ " + "option is set to 'true'. There is no default which means " + "ejabberd won't provide a client certificate to the SQL " + "server. " + "This option has no effect for MS SQL.")}}, + {sql_ssl_verify, + #{value => "true | false", + desc => + ?T("Whether to verify SSL connection to the SQL server against " + "CA root certificates defined in _`sql_ssl_cafile`_ option. " + "Implies _`sql_ssl`_ option is set to 'true'. " + "This option has no effect for MS SQL. " + "The default value is 'false'.")}}, + {sql_start_interval, + #{value => "timeout()", + desc => + ?T("A time to wait before retrying to restore failed SQL connection. " + "The default value is '30' seconds.")}}, + {sql_username, + #{value => ?T("Username"), + desc => + ?T("A user name for SQL authentication. " + "The default value is 'ejabberd'.")}}, + {trusted_proxies, + #{value => "all | [Network1, Network2, ...]", + desc => + ?T("Specify what proxies are trusted when an HTTP request " + "contains the header 'X-Forwarded-For'. You can specify " + "'all' to allow all proxies, or specify a list of IPs, " + "possibly with masks. The default value is an empty list. " + "Using this option you can know the real IP " + "of the request, for admin purpose, or security configuration " + "(for example using _`mod_fail2ban`_). IMPORTANT: The proxy MUST " + "be configured to set the 'X-Forwarded-For' header if you " + "enable this option as, otherwise, the client can set it " + "itself and as a result the IP value cannot be trusted for " + "security rules in ejabberd.")}}, + {validate_stream, + #{value => "true | false", + desc => + ?T("Whether to validate any incoming XML packet according " + "to the schemas of " + "https://github.com/processone/xmpp#supported-xmpp-elements" + "[supported XMPP extensions]. WARNING: the validation is only " + "intended for the use by client developers - don't enable " + "it in production environment. The default value is 'false'.")}}, + {websocket_origin, + #{value => "ignore | URL", + desc => + ?T("This option enables validation for 'Origin' header to " + "protect against connections from other domains than given " + "in the configuration file. In this way, the lower layer load " + "balancer can be chosen for a specific ejabberd implementation " + "while still providing a secure WebSocket connection. " + "The default value is 'ignore'. An example value of the 'URL' is " + "'\"https://test.example.org:8081\"'.")}}, + {websocket_ping_interval, + #{value => "timeout()", + desc => + ?T("Defines time between pings sent by the server to a client " + "(WebSocket level protocol pings are used for this) to keep " + "a connection active. If the client doesn't respond to two " + "consecutive pings, the connection will be assumed as closed. " + "The value of '0' can be used to disable the feature. This option " + "makes the server sending pings only for connections using the RFC " + "compliant protocol. For older style connections the server " + "expects that whitespace pings would be used for this purpose. " + "The default value is '60' seconds.")}}, + {websocket_timeout, + #{value => "timeout()", + desc => + ?T("Amount of time without any communication after which the " + "connection would be closed. The default value is '300' seconds.")}}]. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== diff --git a/src/ejabberd_piefxis.erl b/src/ejabberd_piefxis.erl index 9a9659270..789be7359 100644 --- a/src/ejabberd_piefxis.erl +++ b/src/ejabberd_piefxis.erl @@ -1,15 +1,11 @@ %%%---------------------------------------------------------------------- %%% File : ejabberd_piefxis.erl -%%% Author : Pablo Polvorin, Vidal Santiago Martinez +%%% Author : Pablo Polvorin, Vidal Santiago Martinez, Evgeniy Khramtsov %%% Purpose : XEP-0227: Portable Import/Export Format for XMPP-IM Servers %%% Created : 17 Jul 2008 by Pablo Polvorin -%%%------------------------------------------------------------------- -%%% @author Evgeniy Khramtsov -%%% @copyright (C) 2012, Evgeniy Khramtsov -%%% @doc %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -28,24 +24,23 @@ %%%---------------------------------------------------------------------- %%% Not implemented: +%%% - PEP nodes export/import +%%% - message archives export/import %%% - write mod_piefxis with ejabberdctl commands -%%% - Export from mod_offline_odbc.erl -%%% - Export from mod_private_odbc.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). -%% API +-protocol({xep, 227, '1.1', '2.1.0', "partial", ""}). + -export([import_file/1, export_server/1, export_host/2]). -define(CHUNK_SIZE, 1024*20). %20k --include("ejabberd.hrl"). +-include_lib("xmpp/include/scram.hrl"). -include("logger.hrl"). --include("jlib.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). -include("mod_privacy.hrl"). -include("mod_roster.hrl"). @@ -61,10 +56,10 @@ -define(NS_PIEFXIS, <<"http://www.xmpp.org/extensions/xep-0227.html#ns">>). -define(NS_XI, <<"http://www.w3.org/2001/XInclude">>). --record(state, {xml_stream_state :: xml_stream:xml_stream_state(), +-record(state, {xml_stream_state :: fxml_stream:xml_stream_state() | undefined, user = <<"">> :: binary(), server = <<"">> :: binary(), - fd :: file:io_device(), + fd = self() :: file:io_device(), dir = <<"">> :: binary()}). -type state() :: #state{}. @@ -77,12 +72,11 @@ import_file(FileName) -> import_file(FileName, #state{}). -spec import_file(binary(), state()) -> ok | {error, atom()}. - import_file(FileName, State) -> case file:open(FileName, [read, binary]) of {ok, Fd} -> Dir = filename:dirname(FileName), - XMLStreamState = xml_stream:new(self(), infinity), + XMLStreamState = fxml_stream:new(self(), infinity), Res = process(State#state{xml_stream_state = XMLStreamState, fd = Fd, dir = Dir}), @@ -90,76 +84,18 @@ import_file(FileName, State) -> Res; {error, Reason} -> ErrTxt = file:format_error(Reason), - ?ERROR_MSG("Failed to open file '~s': ~s", [FileName, ErrTxt]), + ?ERROR_MSG("Failed to open file '~ts': ~ts", [FileName, ErrTxt]), {error, Reason} end. -%%%================================== -%%%% Process Elements -%%%================================== -%%%% Process Element -%%%================================== -%%%% Add user -%% @spec (El::xmlel(), Domain::string(), User::binary(), Password::binary() | none) -%% -> ok | {error, ErrorText::string()} -%% @doc Add a new user to the database. -%% If user already exists, it will be only updated. -spec export_server(binary()) -> any(). - -%% @spec (User::string(), Password::string(), Domain::string()) -%% -> ok | {atomic, exists} | {error, not_allowed} -%% @doc Create a new user export_server(Dir) -> - export_hosts(?MYHOSTS, Dir). + export_hosts(ejabberd_option:hosts(), Dir). -%%%================================== -%%%% Populate user -%% @spec (User::string(), Domain::string(), El::xml()) -%% -> ok | {error, not_found} -%% -%% @doc Add a new user from a XML file with a roster list. -%% -%% Example of a file: -%% ``` -%% -%% -%% -%% -%% -%% -%% Friends -%% -%% -%% -%% -%% -%% ''' -spec export_host(binary(), binary()) -> any(). - export_host(Dir, Host) -> export_hosts([Host], Dir). -%% @spec User = String with the user name -%% Domain = String with a domain name -%% El = Sub XML element with vCard tags values -%% @ret ok | {error, not_found} -%% @doc Read vcards from the XML and send it to the server -%% -%% Example: -%% ``` -%% -%% -%% -%% -%% -%% Admin -%% -%% -%% -%% -%% ''' %%%=================================================================== %%% Internal functions %%%=================================================================== @@ -187,22 +123,17 @@ export_hosts(Hosts, Dir) -> end, ok, FilesAndHosts); {error, Reason} -> ErrTxt = file:format_error(Reason), - ?ERROR_MSG("Failed to open file '~s': ~s", [DFn, ErrTxt]), + ?ERROR_MSG("Failed to open file '~ts': ~ts", [DFn, ErrTxt]), {error, Reason} end. -%% @spec User = String with the user name -%% Domain = String with a domain name -%% El = Sub XML element with offline messages values -%% @ret ok | {error, not_found} -%% @doc Read off-line message from the XML and send it to the server export_host(Dir, FnH, Host) -> DFn = make_host_basefilename(Dir, FnH), case file:open(DFn, [raw, write]) of {ok, Fd} -> print(Fd, make_piefxis_xml_head()), print(Fd, make_piefxis_host_head(Host)), - Users = ejabberd_auth:get_vh_registered_users(Host), + Users = ejabberd_auth:get_users(Host), case export_users(Users, Host, Fd) of ok -> print(Fd, make_piefxis_host_tail()), @@ -216,15 +147,10 @@ export_host(Dir, FnH, Host) -> end; {error, Reason} -> ErrTxt = file:format_error(Reason), - ?ERROR_MSG("Failed to open file '~s': ~s", [DFn, ErrTxt]), + ?ERROR_MSG("Failed to open file '~ts': ~ts", [DFn, ErrTxt]), {error, Reason} end. -%% @spec User = String with the user name -%% Domain = String with a domain name -%% El = Sub XML element with private storage values -%% @ret ok | {error, not_found} -%% @doc Private storage parsing export_users([{User, _S}|Users], Server, Fd) -> case export_user(User, Server, Fd) of ok -> @@ -235,78 +161,135 @@ export_users([{User, _S}|Users], Server, Fd) -> export_users([], _Server, _Fd) -> ok. -%%%================================== -%%%% Utilities export_user(User, Server, Fd) -> - Pass = ejabberd_auth:get_password_s(User, Server), - Els = get_offline(User, Server) ++ + Password = ejabberd_auth:get_password_s(User, Server), + LServer = jid:nameprep(Server), + {PassPlain, PassScram} = case ejabberd_auth:password_format(LServer) of + scram -> {[], [format_scram_password(Password)]}; + _ when Password == <<"">> -> {[], []}; + _ -> {[{<<"password">>, Password}], []} + end, + Els = + PassScram ++ + get_offline(User, Server) ++ get_vcard(User, Server) ++ get_privacy(User, Server) ++ get_roster(User, Server) ++ get_private(User, Server), - print(Fd, xml:element_to_binary( + 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)), + 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">>, <>}, + {<<"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]), + [Hash, StoredKeyB64, ServerKeyB64, SaltB64, IterationCountBin] = + case Split of + [K1, K2, K3, K4] -> [sha, K1, K2, K3, K4]; + [<<"sha256">>, K1, K2, K3, K4] -> [sha256, K1, K2, K3, K4]; + [<<"sha512">>, K1, K2, K3, K4] -> [sha512, K1, K2, K3, K4] + end, + #scram{ + storedkey = base64:decode(StoredKeyB64), + serverkey = base64:decode(ServerKeyB64), + salt = base64:decode(SaltB64), + hash = Hash, + iterationcount = (binary_to_integer(IterationCountBin)) + }. + +-spec get_vcard(binary(), binary()) -> [xmlel()]. get_vcard(User, Server) -> - JID = jlib:make_jid(User, Server, <<>>), - case mod_vcard:process_sm_iq(JID, JID, #iq{type = get}) of - #iq{type = result, sub_el = [_|_] = VCardEls} -> - VCardEls; - _ -> - [] + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + 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) -> - case mod_offline:get_offline_els(User, Server) of + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + try mod_offline:get_offline_els(LUser, LServer) of [] -> []; Els -> - NewEls = lists:map( - fun(#xmlel{attrs = Attrs} = El) -> - NewAttrs = lists:keystore(<<"xmlns">>, 1, - Attrs, - {<<"xmlns">>, - <<"jabber:client">>}), - El#xmlel{attrs = NewAttrs} - end, Els), + NewEls = lists:map(fun xmpp:encode/1, Els), [#xmlel{name = <<"offline-messages">>, children = NewEls}] + catch + error:{module_not_loaded, _, _} -> [] end. -%%%% Export hosts +-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( fun({Name, Items}) -> XItems = lists:map( - fun mod_privacy:item_to_xml/1, Items), - #xmlel{name = <<"list">>, - attrs = [{<<"name">>, Name}], - children = XItems} + fun mod_privacy:encode_list_item/1, + Items), + #privacy_list{name = Name, items = XItems} end, Lists), - DefaultEl = case Default of - none -> - []; - _ -> - [#xmlel{name = <<"default">>, - attrs = [{<<"name">>, Default}]}] - end, - [#xmlel{name = <<"query">>, - attrs = [{<<"xmlns">>, ?NS_PRIVACY}], - children = DefaultEl ++ XLists}]; + [xmpp:encode(#privacy_query{default = Default, lists = XLists})]; _ -> [] + catch + error:{module_not_loaded, _, _} -> [] end. -%% @spec (Dir::string(), Hosts::[string()]) -> ok +-spec get_roster(binary(), binary()) -> [xmlel()]. get_roster(User, Server) -> - JID = jlib:make_jid(User, Server, <<>>), - case mod_roster:get_roster(User, Server) of + JID = jid:make(User, Server), + try mod_roster:get_roster(User, Server) of [_|_] = Items -> Subs = lists:flatmap( @@ -316,18 +299,11 @@ get_roster(User, Server) -> Status = if is_binary(Msg) -> (Msg); true -> <<"">> end, - [#xmlel{name = <<"presence">>, - attrs = - [{<<"from">>, - jlib:jid_to_string(R#roster.jid)}, - {<<"to">>, jlib:jid_to_string(JID)}, - {<<"xmlns">>, <<"jabber:client">>}, - {<<"type">>, <<"subscribe">>}], - children = - [#xmlel{name = <<"status">>, - attrs = [], - children = - [{xmlcdata, Status}]}]}]; + [xmpp:encode( + #presence{from = jid:make(R#roster.jid), + to = JID, + type = subscribe, + status = xmpp:mk_text(Status)})]; (_) -> [] end, Items), @@ -335,63 +311,73 @@ get_roster(User, Server) -> fun(#roster{ask = in, subscription = none}) -> []; (R) -> - [mod_roster:item_to_xml(R)] + [mod_roster:encode_item(R)] end, Items), - [#xmlel{name = <<"query">>, - attrs = [{<<"xmlns">>, ?NS_ROSTER}], - children = Rs} | Subs]; + [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 -> - [#xmlel{name = <<"query">>, - attrs = [{<<"xmlns">>, ?NS_PRIVATE}], - children = Els}]; + [xmpp:encode(#private{sub_els = Els})]; _ -> [] + catch + error:{module_not_loaded, _, _} -> [] end. process(#state{xml_stream_state = XMLStreamState, fd = Fd} = State) -> case file:read(Fd, ?CHUNK_SIZE) of {ok, Data} -> - NewXMLStreamState = xml_stream:parse(XMLStreamState, Data), + NewXMLStreamState = fxml_stream:parse(XMLStreamState, Data), case process_els(State#state{xml_stream_state = NewXMLStreamState}) of {ok, NewState} -> process(NewState); Err -> - xml_stream:close(NewXMLStreamState), + fxml_stream:close(NewXMLStreamState), Err end; eof -> - xml_stream:close(XMLStreamState), + fxml_stream:close(XMLStreamState), ok end. process_els(State) -> + Els = gather_els(State, []), + process_els(State, lists:reverse(Els)). + +gather_els(State, List) -> receive {'$gen_event', El} -> - case process_el(El, State) of - {ok, NewState} -> - process_els(NewState); - Err -> - Err - end + gather_els(State, [El | List]) after 0 -> - {ok, State} - end. + List +end. + +process_els(State, [El | Tail]) -> + case process_el(El, State) of + {ok, NewState} -> + process_els(NewState, Tail); + Err -> + Err + end; +process_els(State, []) -> + {ok, State}. process_el({xmlstreamstart, <<"server-data">>, Attrs}, State) -> - case xml:get_attr_s(<<"xmlns">>, Attrs) of + case fxml:get_attr_s(<<"xmlns">>, Attrs) of ?NS_PIEFXIS -> {ok, State}; ?NS_PIE -> {ok, State}; NS -> - stop("Unknown 'server-data' namespace = ~s", [NS]) + stop("Unknown 'server-data' namespace = ~ts", [NS]) end; process_el({xmlstreamend, _}, State) -> {ok, State}; @@ -400,7 +386,7 @@ process_el({xmlstreamcdata, _}, State) -> process_el({xmlstreamelement, #xmlel{name = <<"xi:include">>, attrs = Attrs}}, #state{dir = Dir, user = <<"">>} = State) -> - FileName = xml:get_attr_s(<<"href">>, Attrs), + FileName = fxml:get_attr_s(<<"href">>, Attrs), case import_file(filename:join([Dir, FileName]), State) of ok -> {ok, State}; @@ -413,17 +399,17 @@ process_el({xmlstreamstart, <<"host">>, Attrs}, State) -> process_el({xmlstreamelement, #xmlel{name = <<"host">>, attrs = Attrs, children = Els}}, State) -> - JIDS = xml:get_attr_s(<<"jid">>, Attrs), - case jlib:string_to_jid(JIDS) of + JIDS = fxml:get_attr_s(<<"jid">>, Attrs), + try jid:decode(JIDS) of #jid{lserver = S} -> - case lists:member(S, ?MYHOSTS) of + case ejabberd_router:is_my_host(S) of true -> process_users(Els, State#state{server = S}); false -> - stop("Unknown host: ~s", [S]) - end; - error -> - stop("Invalid 'jid': ~s", [JIDS]) + stop("Unknown host: ~ts", [S]) + end + catch _:{bad_jid, _} -> + stop("Invalid 'jid': ~ts", [JIDS]) end; process_el({xmlstreamstart, <<"user">>, Attrs}, State = #state{server = S}) when S /= <<"">> -> @@ -454,22 +440,40 @@ 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 = xml:get_attr_s(<<"name">>, Attrs), - Pass = xml:get_attr_s(<<"password">>, Attrs), - case jlib:nodeprep(Name) of + Name = fxml:get_attr_s(<<"name">>, Attrs), + Pass = process_password(El, LServer), + case jid:nodeprep(Name) of error -> - stop("Invalid 'user': ~s", [Name]); + stop("Invalid 'user': ~ts", [Name]); LUser -> case ejabberd_auth:try_register(LUser, LServer, Pass) of - {atomic, _} -> + ok -> process_user_els(Els, State#state{user = LUser}); - Err -> - stop("Failed to create user '~s': ~p", [Name, Err]) + {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} -> @@ -484,121 +488,134 @@ process_user_els([], State) -> process_user_el(#xmlel{name = Name, attrs = Attrs, children = Els} = El, State) -> - case {Name, xml:get_attr_s(<<"xmlns">>, Attrs)} of - {<<"query">>, ?NS_ROSTER} -> - process_roster(El, State); - {<<"query">>, ?NS_PRIVACY} -> - %% Make sure elements go before and - NewEls = lists:reverse(lists:keysort(#xmlel.name, Els)), - process_privacy_el(El#xmlel{children = NewEls}, State); - {<<"query">>, ?NS_PRIVATE} -> - process_private(El, State); - {<<"vCard">>, ?NS_VCARD} -> - process_vcard(El, State); - {<<"offline-messages">>, _} -> - process_offline_msgs(Els, State); - {<<"presence">>, <<"jabber:client">>} -> - process_presence(El, State); - _ -> - {ok, State} + try + case {Name, fxml:get_attr_s(<<"xmlns">>, Attrs)} of + {<<"query">>, ?NS_ROSTER} -> + process_roster(xmpp:decode(El), State); + {<<"query">>, ?NS_PRIVACY} -> + %% Make sure elements go before and + process_privacy(xmpp:decode(El), State); + {<<"query">>, ?NS_PRIVATE} -> + process_private(xmpp:decode(El), State); + {<<"vCard">>, ?NS_VCARD} -> + process_vcard(xmpp:decode(El), State); + {<<"offline-messages">>, NS} -> + Msgs = [xmpp:decode(E, NS, [ignore_els]) || E <- Els], + process_offline_msgs(Msgs, State); + {<<"presence">>, ?NS_CLIENT} -> + process_presence(xmpp:decode(El, ?NS_CLIENT, [ignore_els]), State); + _ -> + {ok, State} + end + catch _:{xmpp_codec, Why} -> + ErrTxt = xmpp:format_error(Why), + stop("failed to decode XML '~ts': ~ts", + [fxml:element_to_binary(El), ErrTxt]) end. -process_privacy_el(#xmlel{children = [#xmlel{} = SubEl|SubEls]} = El, State) -> - case process_privacy(#xmlel{children = [SubEl]}, State) of +-spec process_offline_msgs([stanza()], state()) -> {ok, state()} | {error, _}. +process_offline_msgs([#message{} = Msg|Msgs], State) -> + case process_offline_msg(Msg, State) of {ok, NewState} -> - process_privacy_el(El#xmlel{children = SubEls}, NewState); + process_offline_msgs(Msgs, NewState); Err -> Err end; -process_privacy_el(#xmlel{children = [_|SubEls]} = El, State) -> - process_privacy_el(El#xmlel{children = SubEls}, State); -process_privacy_el(#xmlel{children = []}, State) -> - {ok, State}. - -process_offline_msgs([#xmlel{} = El|Els], State) -> - case process_offline_msg(El, State) of - {ok, NewState} -> - process_offline_msgs(Els, NewState); - Err -> - Err - end; -process_offline_msgs([_|Els], State) -> - process_offline_msgs(Els, State); +process_offline_msgs([_|Msgs], State) -> + process_offline_msgs(Msgs, State); process_offline_msgs([], State) -> {ok, State}. -process_roster(El, State = #state{user = U, server = S}) -> - case mod_roster:set_items(U, S, El) of +-spec process_roster(roster_query(), state()) -> {ok, state()} | {error, _}. +process_roster(RosterQuery, State = #state{user = U, server = S}) -> + case mod_roster:set_items(U, S, RosterQuery) of {atomic, _} -> {ok, State}; Err -> stop("Failed to write roster: ~p", [Err]) end. -%%%================================== -%%%% Export server -process_privacy(El, State = #state{user = U, server = S}) -> - JID = jlib:make_jid(U, S, <<"">>), - case mod_privacy:process_iq_set( - [], JID, JID, #iq{type = set, sub_el = El}) of - {error, _} = Err -> - stop("Failed to write privacy: ~p", [Err]); - _ -> - {ok, State} - end. +-spec process_privacy(privacy_query(), state()) -> {ok, state()} | {error, _}. +process_privacy(#privacy_query{lists = Lists, + default = Default, + active = Active}, + State = #state{user = U, server = S}) -> + JID = jid:make(U, S), + if Lists /= undefined -> + process_privacy2(JID, #privacy_query{lists = Lists}); + true -> + ok + end, + if Active /= undefined -> + process_privacy2(JID, #privacy_query{active = Active}); + true -> + ok + end, + if Default /= undefined -> + process_privacy2(JID, #privacy_query{default = Default}); + true -> + ok + end, + {ok, State}. -%% @spec (Dir::string()) -> ok -process_private(El, State = #state{user = U, server = S}) -> - JID = jlib:make_jid(U, S, <<"">>), - case mod_private:process_sm_iq( - JID, JID, #iq{type = set, sub_el = El}) of +process_privacy2(JID, PQ) -> + case mod_privacy:process_iq(#iq{type = set, id = p1_rand:get_string(), + from = JID, to = JID, + sub_els = [PQ]}) of + #iq{type = error} = ResIQ -> + #stanza_error{reason = Reason} = xmpp:get_error(ResIQ), + if Reason /= 'item-not-found' -> + %% Failed to set default list because there is no + %% list with such name. We shouldn't stop here. + stop("Failed to write default privacy: ~p", [Reason]); + true -> + ok + end; + _ -> + ok + end. + +-spec process_private(private(), state()) -> {ok, state()} | {error, _}. +process_private(Private, State = #state{user = U, server = S}) -> + JID = jid:make(U, S), + IQ = #iq{type = set, id = p1_rand:get_string(), + from = JID, to = JID, sub_els = [Private]}, + case mod_private:process_sm_iq(IQ) of #iq{type = result} -> {ok, State}; Err -> stop("Failed to write private: ~p", [Err]) end. -%%%================================== -%%%% Export host +-spec process_vcard(xmpp_element(), state()) -> {ok, state()} | {error, _}. process_vcard(El, State = #state{user = U, server = S}) -> - JID = jlib:make_jid(U, S, <<"">>), - case mod_vcard:process_sm_iq( - JID, JID, #iq{type = set, sub_el = El}) of + JID = jid:make(U, S), + IQ = #iq{type = set, id = p1_rand:get_string(), + from = JID, to = JID, sub_els = [El]}, + case mod_vcard:process_sm_iq(IQ) of #iq{type = result} -> {ok, State}; Err -> stop("Failed to write vcard: ~p", [Err]) end. -%% @spec (Dir::string(), Host::string()) -> ok -process_offline_msg(El, State = #state{user = U, server = S}) -> - FromS = xml:get_attr_s(<<"from">>, El#xmlel.attrs), - case jlib:string_to_jid(FromS) of - #jid{} = From -> - To = jlib:make_jid(U, S, <<>>), - NewEl = jlib:replace_from_to(From, To, El), - case catch mod_offline:store_packet(From, To, NewEl) of - {'EXIT', _} = Err -> - stop("Failed to store offline message: ~p", [Err]); - _ -> - {ok, State} - end; - _ -> - stop("Invalid 'from' = ~s", [FromS]) - end. +-spec process_offline_msg(message(), state()) -> {ok, state()} | {error, _}. +process_offline_msg(#message{from = undefined}, _State) -> + stop("No 'from' attribute found", []); +process_offline_msg(Msg, State = #state{user = U, server = S}) -> + To = jid:make(U, S), + ejabberd_hooks:run_fold( + offline_message_hook, To#jid.lserver, {pass, xmpp:set_to(Msg, To)}, []), + {ok, State}. -%% @spec (Dir::string(), Fn::string(), Host::string()) -> ok -process_presence(El, #state{user = U, server = S} = State) -> - FromS = xml:get_attr_s(<<"from">>, El#xmlel.attrs), - case jlib:string_to_jid(FromS) of - #jid{} = From -> - To = jlib:make_jid(U, S, <<>>), - NewEl = jlib:replace_from_to(From, To, El), - ejabberd_router:route(From, To, NewEl), - {ok, State}; - _ -> - stop("Invalid 'from' = ~s", [FromS]) - end. +-spec process_presence(presence(), state()) -> {ok, state()} | {error, _}. +process_presence(#presence{from = undefined}, _State) -> + stop("No 'from' attribute found", []); +process_presence(Pres, #state{user = U, server = S} = State) -> + To = jid:make(U, S), + NewPres = xmpp:set_to(Pres, To), + ejabberd_router:route(NewPres), + {ok, State}. stop(Fmt, Args) -> ?ERROR_MSG(Fmt, Args), @@ -606,17 +623,16 @@ stop(Fmt, Args) -> make_filename_template() -> {{Year, Month, Day}, {Hour, Minute, Second}} = calendar:local_time(), - list_to_binary( - io_lib:format("~4..0w~2..0w~2..0w-~2..0w~2..0w~2..0w", - [Year, Month, Day, Hour, Minute, Second])). + str:format("~4..0w~2..0w~2..0w-~2..0w~2..0w~2..0w", + [Year, Month, Day, Hour, Minute, Second]). make_main_basefilename(Dir, FnT) -> Filename2 = <>, filename:join([Dir, Filename2]). %% @doc Make the filename for the host. -%% Example: ``(<<"20080804-231550">>, <<"jabber.example.org">>) -> -%% <<"20080804-231550_jabber_example_org.xml">>'' +%% Example: ``(<<"20080804-231550">>, <<"xmpp.domain.tld">>) -> +%% <<"20080804-231550_xmpp_domain_tld.xml">>'' make_host_filename(FnT, Host) -> Host2 = str:join(str:tokens(Host, <<".">>), <<"_">>), <>. @@ -626,59 +642,29 @@ make_host_filename(FnT, Host) -> make_host_basefilename(Dir, FnT) -> filename:join([Dir, FnT]). -%% @spec () -> string() make_piefxis_xml_head() -> "". -%% @spec () -> string() make_piefxis_xml_tail() -> "". -%% @spec () -> string() make_piefxis_server_head() -> - io_lib:format("", + io_lib:format("", [?NS_PIE, ?NS_XI]). -%% @spec () -> string() make_piefxis_server_tail() -> "". -%% @spec (Host::string()) -> string() make_piefxis_host_head(Host) -> - io_lib:format("", + io_lib:format("", [?NS_PIE, ?NS_XI, Host]). -%% @spec () -> string() make_piefxis_host_tail() -> "". -%% @spec (Fn::string()) -> string() make_xinclude(Fn) -> Base = filename:basename(Fn), - io_lib:format("", [Base]). + io_lib:format("", [Base]). -%%%================================== -%%%% Export user -%% @spec (Fd, Username::string(), Host::string()) -> ok -%% @doc Extract user information and print it. -%% @spec (Username::string(), Host::string()) -> string() -%% @spec (InfoName::atom(), Username::string(), Host::string()) -> string() -%%%================================== -%%%% Interface with ejabberd offline storage -%% Copied from mod_offline.erl and customized -%%%================================== -%%%% Interface with ejabberd private storage -%%%================================== -%%%% Disk file access -%% @spec () -> string() -%% @spec (Dir::string(), FnT::string()) -> string() -%% @spec (FnT::string(), Host::string()) -> FnH::string() -%% @doc Make the filename for the host. -%% Example: ``("20080804-231550", "jabber.example.org") -> "20080804-231550_jabber_example_org.xml"'' -%% @spec (Fn::string()) -> {ok, Fd} -%% @spec (Fd) -> ok -%% @spec (Fd, String::string()) -> ok print(Fd, String) -> -%%%================================== -%%% vim: set filetype=erlang tabstop=8 foldmarker=%%%%,%%%= foldmethod=marker: file:write(Fd, String). diff --git a/src/ejabberd_pkix.erl b/src/ejabberd_pkix.erl new file mode 100644 index 000000000..b699454dd --- /dev/null +++ b/src/ejabberd_pkix.erl @@ -0,0 +1,433 @@ +%%%------------------------------------------------------------------- +%%% Author : Evgeny Khramtsov +%%% Created : 4 Mar 2017 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(ejabberd_pkix). +-behaviour(gen_server). + +%% API +-export([start_link/0]). +-export([certs_dir/0]). +-export([add_certfile/1, del_certfile/1, commit/0]). +-export([notify_expired/1]). +-export([try_certfile/1, get_certfile/0, get_certfile/1]). +-export([get_certfile_no_default/1]). +%% Hooks +-export([ejabberd_started/0, config_reloaded/0, cert_expired/2]). +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-include("logger.hrl"). +-define(CALL_TIMEOUT, timer:minutes(1)). + +-record(state, {files = sets:new() :: sets:set(filename())}). + +-type state() :: #state{}. +-type filename() :: binary(). + +%%%=================================================================== +%%% API +%%%=================================================================== +-spec start_link() -> {ok, pid()} | {error, {already_started, pid()} | term()}. +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +-spec add_certfile(file:filename_all()) -> {ok, filename()} | {error, pkix:error_reason()}. +add_certfile(Path0) -> + Path = prep_path(Path0), + try gen_server:call(?MODULE, {add_certfile, Path}, ?CALL_TIMEOUT) + catch exit:{noproc, _} -> + case add_file(Path) of + ok -> {ok, Path}; + Err -> Err + end + end. + +-spec del_certfile(file:filename_all()) -> ok. +del_certfile(Path0) -> + Path = prep_path(Path0), + try gen_server:call(?MODULE, {del_certfile, Path}, ?CALL_TIMEOUT) + catch exit:{noproc, _} -> + pkix:del_file(Path) + end. + +-spec try_certfile(file:filename_all()) -> filename(). +try_certfile(Path0) -> + Path = prep_path(Path0), + case pkix:is_pem_file(Path) of + true -> Path; + {false, Reason} -> + ?ERROR_MSG("Failed to read PEM file ~ts: ~ts", + [Path, pkix:format_error(Reason)]), + erlang:error(badarg) + end. + +-spec get_certfile(binary()) -> {ok, filename()} | error. +get_certfile(Domain) -> + case get_certfile_no_default(Domain) of + {ok, Path} -> + {ok, Path}; + error -> + get_certfile() + end. + +-spec get_certfile_no_default(binary()) -> {ok, filename()} | error. +get_certfile_no_default(Domain) -> + try list_to_binary(idna:utf8_to_ascii(Domain)) of + ASCIIDomain -> + case pkix:get_certfile(ASCIIDomain) of + error -> error; + Ret -> {ok, select_certfile(Ret)} + end + catch _:_ -> + error + end. + +-spec get_certfile() -> {ok, filename()} | error. +get_certfile() -> + case pkix:get_certfile() of + error -> error; + Ret -> {ok, select_certfile(Ret)} + end. + +-spec certs_dir() -> file:filename_all(). +certs_dir() -> + MnesiaDir = mnesia:system_info(directory), + filename:join(MnesiaDir, "certs"). + +-spec commit() -> ok. +commit() -> + gen_server:call(?MODULE, commit, ?CALL_TIMEOUT). + +-spec ejabberd_started() -> ok. +ejabberd_started() -> + gen_server:call(?MODULE, ejabberd_started, ?CALL_TIMEOUT). + +-spec config_reloaded() -> ok. +config_reloaded() -> + gen_server:call(?MODULE, config_reloaded, ?CALL_TIMEOUT). + +-spec notify_expired(pkix:notify_event()) -> ok. +notify_expired(Event) -> + gen_server:cast(?MODULE, Event). + +-spec cert_expired(_, pkix:cert_info()) -> ok. +cert_expired(_Cert, #{domains := Domains, + expiry := Expiry, + files := [{Path, Line}|_]}) -> + ?WARNING_MSG("Certificate in ~ts (at line: ~B)~ts ~ts", + [Path, Line, + case Domains of + [] -> ""; + _ -> " for " ++ misc:format_hosts_list(Domains) + end, + format_expiration_date(Expiry)]). + +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== +-spec init([]) -> {ok, state()}. +init([]) -> + process_flag(trap_exit, true), + ejabberd_hooks:add(cert_expired, ?MODULE, cert_expired, 50), + ejabberd_hooks:add(config_reloaded, ?MODULE, config_reloaded, 100), + ejabberd_hooks:add(ejabberd_started, ?MODULE, ejabberd_started, 30), + case add_files() of + {_Files, []} -> + {ok, #state{}}; + {Files, [_|_]} -> + case ejabberd:is_loaded() of + true -> + {ok, #state{}}; + false -> + del_files(Files), + stop_ejabberd() + end + end. + +-spec handle_call(term(), {pid(), term()}, state()) -> + {reply, ok, state()} | {noreply, state()}. +handle_call({add_certfile, Path}, _From, State) -> + case add_file(Path) of + ok -> + {reply, {ok, Path}, State}; + {error, _} = Err -> + {reply, Err, State} + end; +handle_call({del_certfile, Path}, _From, State) -> + pkix:del_file(Path), + {reply, ok, State}; +handle_call(ejabberd_started, _From, State) -> + case do_commit() of + {ok, []} -> + check_domain_certfiles(), + {reply, ok, State}; + _ -> + stop_ejabberd() + end; +handle_call(config_reloaded, _From, State) -> + Files = get_certfiles_from_config_options(), + _ = add_files(Files), + case do_commit() of + {ok, _} -> + check_domain_certfiles(), + {reply, ok, State}; + error -> + {reply, ok, State} + end; +handle_call(commit, From, State) -> + handle_call(config_reloaded, From, State); +handle_call(Request, _From, State) -> + ?WARNING_MSG("Unexpected call: ~p", [Request]), + {noreply, State}. + +-spec handle_cast(term(), state()) -> {noreply, state()}. +handle_cast({cert_expired, Cert, CertInfo}, State) -> + ejabberd_hooks:run(cert_expired, [Cert, CertInfo]), + {noreply, State}; +handle_cast(Request, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Request]), + {noreply, State}. + +-spec handle_info(term(), state()) -> {noreply, state()}. +handle_info(Info, State) -> + ?WARNING_MSG("Unexpected info: ~p", [Info]), + {noreply, State}. + +-spec terminate(normal | shutdown | {shutdown, term()} | term(), + state()) -> any(). +terminate(_Reason, State) -> + ejabberd_hooks:delete(cert_expired, ?MODULE, cert_expired, 50), + ejabberd_hooks:delete(ejabberd_started, ?MODULE, ejabberd_started, 30), + ejabberd_hooks:delete(config_reloaded, ?MODULE, config_reloaded, 100), + del_files(State#state.files). + +-spec code_change(term() | {down, term()}, state(), term()) -> {ok, state()}. +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +-spec add_files() -> {sets:set(filename()), [{filename(), pkix:error_reason()}]}. +add_files() -> + Files = get_certfiles_from_config_options(), + add_files(sets:to_list(Files), sets:new(), []). + +-spec add_files(sets:set(filename())) -> + {sets:set(filename()), [{filename(), pkix:error_reason()}]}. +add_files(Files) -> + add_files(sets:to_list(Files), sets:new(), []). + +-spec add_files([filename()], sets:set(filename()), + [{filename(), pkix:error_reason()}]) -> + {sets:set(filename()), [{filename(), pkix:error_reason()}]}. +add_files([File|Files], Set, Errs) -> + case add_file(File) of + ok -> + Set1 = sets:add_element(File, Set), + add_files(Files, Set1, Errs); + {error, Reason} -> + Errs1 = [{File, Reason}|Errs], + add_files(Files, Set, Errs1) + end; +add_files([], Set, Errs) -> + {Set, Errs}. + +-spec add_file(filename()) -> ok | {error, pkix:error_reason()}. +add_file(File) -> + case pkix:add_file(File) of + ok -> ok; + {error, Reason} = Err -> + ?ERROR_MSG("Failed to read PEM file ~ts: ~ts", + [File, pkix:format_error(Reason)]), + Err + end. + +-spec del_files(sets:set(filename())) -> ok. +del_files(Files) -> + lists:foreach(fun pkix:del_file/1, sets:to_list(Files)). + +-spec do_commit() -> {ok, [{filename(), pkix:error_reason()}]} | error. +do_commit() -> + CAFile = ejabberd_option:ca_file(), + ?DEBUG("Using CA root certificates from: ~ts", [CAFile]), + Opts = [{cafile, CAFile}, + {notify_before, [7*24*60*60, % 1 week + 24*60*60, % 1 day + 60*60, % 1 hour + 0]}, + {notify_fun, fun ?MODULE:notify_expired/1}], + case pkix:commit(certs_dir(), Opts) of + {ok, Errors, Warnings, CAError} -> + log_errors(Errors), + log_cafile_error(CAError), + log_warnings(Warnings), + fast_tls_add_certfiles(), + {ok, Errors}; + {error, File, Reason} -> + ?CRITICAL_MSG("Failed to write to ~ts: ~ts", + [File, file:format_error(Reason)]), + error + end. + +-spec check_domain_certfiles() -> ok. +check_domain_certfiles() -> + Hosts = ejabberd_option:hosts(), + Routes = ejabberd_router:get_all_routes(), + check_domain_certfiles(Hosts ++ Routes). + +-spec check_domain_certfiles([binary()]) -> ok. +check_domain_certfiles(Hosts) -> + case ejabberd_listener:tls_listeners() of + [] -> ok; + _ -> + lists:foreach( + fun(Host) -> + case get_certfile_no_default(Host) of + error -> + ?WARNING_MSG( + "No certificate found matching ~ts", + [Host]); + _ -> + ok + end + end, Hosts) + end. + +-spec get_certfiles_from_config_options() -> sets:set(filename()). +get_certfiles_from_config_options() -> + case ejabberd_option:certfiles() of + undefined -> + sets:new(); + Paths -> + lists:foldl( + fun(Path, Acc) -> + Files = wildcard(Path), + lists:foldl(fun sets:add_element/2, Acc, Files) + end, sets:new(), Paths) + end. + +-spec prep_path(file:filename_all()) -> filename(). +prep_path(Path0) -> + case filename:pathtype(Path0) of + relative -> + case file:get_cwd() of + {ok, CWD} -> + unicode:characters_to_binary(filename:join(CWD, Path0)); + {error, Reason} -> + ?WARNING_MSG("Failed to get current directory name: ~ts", + [file:format_error(Reason)]), + unicode:characters_to_binary(Path0) + end; + _ -> + unicode:characters_to_binary(Path0) + end. + +-spec stop_ejabberd() -> no_return(). +stop_ejabberd() -> + ?CRITICAL_MSG("ejabberd initialization was aborted due to " + "invalid certificates configuration", []), + ejabberd:halt(). + +-spec wildcard(file:filename_all()) -> [filename()]. +wildcard(Path) when is_binary(Path) -> + wildcard(binary_to_list(Path)); +wildcard(Path) -> + case filelib:wildcard(Path) of + [] -> + ?WARNING_MSG("Path ~ts is empty, please make sure ejabberd has " + "sufficient rights to read it", [Path]), + []; + Files -> + [prep_path(File) || File <- Files] + end. + +-spec select_certfile({filename() | undefined, + filename() | undefined, + filename() | undefined}) -> filename(). +select_certfile({EC, _, _}) when EC /= undefined -> EC; +select_certfile({_, RSA, _}) when RSA /= undefined -> RSA; +select_certfile({_, _, DSA}) when DSA /= undefined -> DSA. + +-spec fast_tls_add_certfiles() -> ok. +fast_tls_add_certfiles() -> + lists:foreach( + fun({Domain, Files}) -> + fast_tls:add_certfile(Domain, select_certfile(Files)) + end, pkix:get_certfiles()), + fast_tls:clear_cache(). + +reason_to_fmt({invalid_cert, _, _}) -> + "Invalid certificate in ~ts: ~ts"; +reason_to_fmt(_) -> + "Failed to read PEM file ~ts: ~ts". + +-spec log_warnings([{filename(), pkix:error_reason()}]) -> ok. +log_warnings(Warnings) -> + lists:foreach( + fun({File, Reason}) -> + ?WARNING_MSG(reason_to_fmt(Reason), + [File, pkix:format_error(Reason)]) + end, Warnings). + +-spec log_errors([{filename(), pkix:error_reason()}]) -> ok. +log_errors(Errors) -> + lists:foreach( + fun({File, Reason}) -> + ?ERROR_MSG(reason_to_fmt(Reason), + [File, pkix:format_error(Reason)]) + end, Errors). + +-spec log_cafile_error({filename(), pkix:error_reason()} | undefined) -> ok. +log_cafile_error({File, Reason}) -> + ?CRITICAL_MSG("Failed to read CA certitificates from ~ts: ~ts. " + "Try to change/set option 'ca_file'", + [File, pkix:format_error(Reason)]); +log_cafile_error(_) -> + ok. + +-spec time_before_expiration(calendar:datetime()) -> {non_neg_integer(), string()}. +time_before_expiration(Expiry) -> + T1 = calendar:datetime_to_gregorian_seconds(Expiry), + T2 = calendar:datetime_to_gregorian_seconds( + calendar:now_to_datetime(erlang:timestamp())), + Secs = max(0, T1 - T2), + if Secs == {0, ""}; + Secs >= 220752000 -> {round(Secs/220752000), "year"}; + Secs >= 2592000 -> {round(Secs/2592000), "month"}; + Secs >= 604800 -> {round(Secs/604800), "week"}; + Secs >= 86400 -> {round(Secs/86400), "day"}; + Secs >= 3600 -> {round(Secs/3600), "hour"}; + Secs >= 60 -> {round(Secs/60), "minute"}; + true -> {Secs, "second"} + end. + +-spec format_expiration_date(calendar:datetime()) -> string(). +format_expiration_date(DateTime) -> + case time_before_expiration(DateTime) of + {0, _} -> "is expired"; + {1, Unit} -> "will expire in a " ++ Unit; + {Int, Unit} -> + "will expire in " ++ integer_to_list(Int) + ++ " " ++ Unit ++ "s" + end. diff --git a/src/ejabberd_rdbms.erl b/src/ejabberd_rdbms.erl deleted file mode 100644 index e71728da5..000000000 --- a/src/ejabberd_rdbms.erl +++ /dev/null @@ -1,83 +0,0 @@ -%%%---------------------------------------------------------------------- -%%% File : ejabberd_rdbms.erl -%%% Author : Mickael Remond -%%% Purpose : Manage the start of the database modules when needed -%%% Created : 31 Jan 2003 by Alexey Shchepin -%%% -%%% -%%% ejabberd, Copyright (C) 2002-2015 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(ejabberd_rdbms). - --author('alexey@process-one.net'). - --export([start/0]). - --include("ejabberd.hrl"). --include("logger.hrl"). - -start() -> - case lists:any(fun(H) -> needs_odbc(H) /= false end, - ?MYHOSTS) of - true -> - start_hosts(); - false -> - ok - end. - -%% Start relationnal DB module on the nodes where it is needed -start_hosts() -> - lists:foreach(fun (Host) -> - case needs_odbc(Host) of - {true, App} -> start_odbc(Host, App); - false -> ok - end - end, - ?MYHOSTS). - -%% Start the ODBC module on the given host -start_odbc(Host, App) -> - ejabberd:start_app(App), - Supervisor_name = gen_mod:get_module_proc(Host, - ejabberd_odbc_sup), - ChildSpec = {Supervisor_name, - {ejabberd_odbc_sup, start_link, [Host]}, transient, - infinity, supervisor, [ejabberd_odbc_sup]}, - case supervisor:start_child(ejabberd_sup, ChildSpec) of - {ok, _PID} -> ok; - _Error -> - ?ERROR_MSG("Start of supervisor ~p failed:~n~p~nRetrying." - "..~n", - [Supervisor_name, _Error]), - start_odbc(Host, App) - end. - -%% Returns {true, App} if we have configured odbc for the given host -needs_odbc(Host) -> - LHost = jlib:nameprep(Host), - case ejabberd_config:get_option({odbc_type, LHost}, - fun(mysql) -> mysql; - (pgsql) -> pgsql; - (odbc) -> odbc - end, undefined) of - mysql -> {true, p1_mysql}; - pgsql -> {true, p1_pgsql}; - odbc -> {true, odbc}; - undefined -> false - end. diff --git a/src/ejabberd_receiver.erl b/src/ejabberd_receiver.erl deleted file mode 100644 index 819e6d898..000000000 --- a/src/ejabberd_receiver.erl +++ /dev/null @@ -1,377 +0,0 @@ -%%%---------------------------------------------------------------------- -%%% File : ejabberd_receiver.erl -%%% Author : Alexey Shchepin -%%% Purpose : Socket receiver for C2S and S2S connections -%%% Created : 10 Nov 2003 by Alexey Shchepin -%%% -%%% -%%% ejabberd, Copyright (C) 2002-2015 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(ejabberd_receiver). - --author('alexey@process-one.net'). - --behaviour(gen_server). - -%% API --export([start_link/4, - start/3, - start/4, - change_shaper/2, - reset_stream/1, - starttls/2, - compress/2, - become_controller/2, - close/1]). - -%% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, - handle_info/2, terminate/2, code_change/3]). - --include("ejabberd.hrl"). --include("logger.hrl"). - --record(state, - {socket :: inet:socket() | p1_tls:tls_socket() | ezlib:zlib_socket(), - sock_mod = gen_tcp :: gen_tcp | p1_tls | ezlib, - shaper_state = none :: shaper:shaper(), - c2s_pid :: pid(), - max_stanza_size = infinity :: non_neg_integer() | infinity, - xml_stream_state :: xml_stream:xml_stream_state(), - timeout = infinity:: timeout()}). - --define(HIBERNATE_TIMEOUT, 90000). - -%%==================================================================== -%% API -%%==================================================================== -%%-------------------------------------------------------------------- -%% Function: start_link() -> {ok,Pid} | ignore | {error,Error} -%% Description: Starts the server -%%-------------------------------------------------------------------- --spec start_link(inet:socket(), atom(), shaper:shaper(), - non_neg_integer() | infinity) -> ignore | - {error, any()} | - {ok, pid()}. - -start_link(Socket, SockMod, Shaper, MaxStanzaSize) -> - gen_server:start_link(?MODULE, - [Socket, SockMod, Shaper, MaxStanzaSize], []). - --spec start(inet:socket(), atom(), shaper:shaper()) -> undefined | pid(). - -%%-------------------------------------------------------------------- -%% Function: start() -> {ok,Pid} | ignore | {error,Error} -%% Description: Starts the server -%%-------------------------------------------------------------------- -start(Socket, SockMod, Shaper) -> - start(Socket, SockMod, Shaper, infinity). - --spec start(inet:socket(), atom(), shaper:shaper(), - non_neg_integer() | infinity) -> undefined | pid(). - -start(Socket, SockMod, Shaper, MaxStanzaSize) -> - {ok, Pid} = - supervisor:start_child(ejabberd_receiver_sup, - [Socket, SockMod, Shaper, MaxStanzaSize]), - Pid. - --spec change_shaper(pid(), shaper:shaper()) -> ok. - -change_shaper(Pid, Shaper) -> - gen_server:cast(Pid, {change_shaper, Shaper}). - --spec reset_stream(pid()) -> ok | {error, any()}. - -reset_stream(Pid) -> do_call(Pid, reset_stream). - --spec starttls(pid(), p1_tls:tls_socket()) -> ok. - -starttls(Pid, TLSSocket) -> - do_call(Pid, {starttls, TLSSocket}). - --spec compress(pid(), iodata() | undefined) -> {error, any()} | - {ok, ezlib:zlib_socket()}. - -compress(Pid, Data) -> - do_call(Pid, {compress, Data}). - --spec become_controller(pid(), pid()) -> ok | {error, any()}. - -become_controller(Pid, C2SPid) -> - do_call(Pid, {become_controller, C2SPid}). - --spec close(pid()) -> ok. - -close(Pid) -> - gen_server:cast(Pid, close). - - -%%==================================================================== -%% gen_server callbacks -%%==================================================================== - -%%-------------------------------------------------------------------- -%% Function: init(Args) -> {ok, State} | -%% {ok, State, Timeout} | -%% ignore | -%% {stop, Reason} -%% Description: Initiates the server -%%-------------------------------------------------------------------- -init([Socket, SockMod, Shaper, MaxStanzaSize]) -> - ShaperState = shaper:new(Shaper), - Timeout = case SockMod of - ssl -> 20; - _ -> infinity - end, - {ok, - #state{socket = Socket, sock_mod = SockMod, - shaper_state = ShaperState, - max_stanza_size = MaxStanzaSize, timeout = Timeout}}. - -%%-------------------------------------------------------------------- -%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | -%% {reply, Reply, State, Timeout} | -%% {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, Reply, State} | -%% {stop, Reason, State} -%% Description: Handling call messages -%%-------------------------------------------------------------------- -handle_call({starttls, TLSSocket}, _From, - #state{xml_stream_state = XMLStreamState, - c2s_pid = C2SPid, - max_stanza_size = MaxStanzaSize} = State) -> - close_stream(XMLStreamState), - NewXMLStreamState = xml_stream:new(C2SPid, - MaxStanzaSize), - NewState = State#state{socket = TLSSocket, - sock_mod = p1_tls, - xml_stream_state = NewXMLStreamState}, - case p1_tls:recv_data(TLSSocket, <<"">>) of - {ok, TLSData} -> - {reply, ok, process_data(TLSData, NewState), ?HIBERNATE_TIMEOUT}; - {error, _Reason} -> - {stop, normal, ok, NewState} - end; -handle_call({compress, Data}, _From, - #state{xml_stream_state = XMLStreamState, - c2s_pid = C2SPid, socket = Socket, sock_mod = SockMod, - max_stanza_size = MaxStanzaSize} = - State) -> - {ok, ZlibSocket} = ezlib:enable_zlib(SockMod, - Socket), - if Data /= undefined -> do_send(State, Data); - true -> ok - end, - close_stream(XMLStreamState), - NewXMLStreamState = xml_stream:new(C2SPid, - MaxStanzaSize), - NewState = State#state{socket = ZlibSocket, - sock_mod = ezlib, - xml_stream_state = NewXMLStreamState}, - case ezlib:recv_data(ZlibSocket, <<"">>) of - {ok, ZlibData} -> - {reply, {ok, ZlibSocket}, - process_data(ZlibData, NewState), ?HIBERNATE_TIMEOUT}; - {error, _Reason} -> {stop, normal, ok, NewState} - end; -handle_call(reset_stream, _From, - #state{xml_stream_state = XMLStreamState, - c2s_pid = C2SPid, max_stanza_size = MaxStanzaSize} = - State) -> - close_stream(XMLStreamState), - NewXMLStreamState = xml_stream:new(C2SPid, - MaxStanzaSize), - Reply = ok, - {reply, Reply, - State#state{xml_stream_state = NewXMLStreamState}, - ?HIBERNATE_TIMEOUT}; -handle_call({become_controller, C2SPid}, _From, State) -> - XMLStreamState = xml_stream:new(C2SPid, State#state.max_stanza_size), - NewState = State#state{c2s_pid = C2SPid, - xml_stream_state = XMLStreamState}, - activate_socket(NewState), - Reply = ok, - {reply, Reply, NewState, ?HIBERNATE_TIMEOUT}; -handle_call(_Request, _From, State) -> - Reply = ok, {reply, Reply, State, ?HIBERNATE_TIMEOUT}. - -%%-------------------------------------------------------------------- -%% Function: handle_cast(Msg, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} -%% Description: Handling cast messages -%%-------------------------------------------------------------------- -handle_cast({change_shaper, Shaper}, State) -> - NewShaperState = shaper:new(Shaper), - {noreply, State#state{shaper_state = NewShaperState}, - ?HIBERNATE_TIMEOUT}; -handle_cast(close, State) -> {stop, normal, State}; -handle_cast(_Msg, State) -> - {noreply, State, ?HIBERNATE_TIMEOUT}. - -%%-------------------------------------------------------------------- -%% Function: handle_info(Info, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} -%% Description: Handling all non call/cast messages -%%-------------------------------------------------------------------- -handle_info({Tag, _TCPSocket, Data}, - #state{socket = Socket, sock_mod = SockMod} = State) - when (Tag == tcp) or (Tag == ssl) or - (Tag == ejabberd_xml) -> - case SockMod of - p1_tls -> - case p1_tls:recv_data(Socket, Data) of - {ok, TLSData} -> - {noreply, process_data(TLSData, State), - ?HIBERNATE_TIMEOUT}; - {error, _Reason} -> {stop, normal, State} - end; - ezlib -> - case ezlib:recv_data(Socket, Data) of - {ok, ZlibData} -> - {noreply, process_data(ZlibData, State), - ?HIBERNATE_TIMEOUT}; - {error, _Reason} -> {stop, normal, State} - end; - _ -> - {noreply, process_data(Data, State), ?HIBERNATE_TIMEOUT} - end; -handle_info({Tag, _TCPSocket}, State) - when (Tag == tcp_closed) or (Tag == ssl_closed) -> - {stop, normal, State}; -handle_info({Tag, _TCPSocket, Reason}, State) - when (Tag == tcp_error) or (Tag == ssl_error) -> - case Reason of - timeout -> {noreply, State, ?HIBERNATE_TIMEOUT}; - _ -> {stop, normal, State} - end; -handle_info({timeout, _Ref, activate}, State) -> - activate_socket(State), - {noreply, State, ?HIBERNATE_TIMEOUT}; -handle_info(timeout, State) -> - proc_lib:hibernate(gen_server, enter_loop, - [?MODULE, [], State]), - {noreply, State, ?HIBERNATE_TIMEOUT}; -handle_info(_Info, State) -> - {noreply, State, ?HIBERNATE_TIMEOUT}. - -%%-------------------------------------------------------------------- -%% Function: terminate(Reason, State) -> void() -%% Description: This function is called by a gen_server when it is about to -%% terminate. It should be the opposite of Module:init/1 and do any necessary -%% cleaning up. When it returns, the gen_server terminates with Reason. -%% The return value is ignored. -%%-------------------------------------------------------------------- -terminate(_Reason, - #state{xml_stream_state = XMLStreamState, - c2s_pid = C2SPid} = - State) -> - close_stream(XMLStreamState), - if C2SPid /= undefined -> - gen_fsm:send_event(C2SPid, closed); - true -> ok - end, - catch (State#state.sock_mod):close(State#state.socket), - ok. - -%%-------------------------------------------------------------------- -%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} -%% Description: Convert process state when code is changed -%%-------------------------------------------------------------------- -code_change(_OldVsn, State, _Extra) -> {ok, State}. - -%%-------------------------------------------------------------------- -%%% Internal functions -%%-------------------------------------------------------------------- - -activate_socket(#state{socket = Socket, - sock_mod = SockMod}) -> - PeerName = case SockMod of - gen_tcp -> - inet:setopts(Socket, [{active, once}]), - inet:peername(Socket); - _ -> - SockMod:setopts(Socket, [{active, once}]), - SockMod:peername(Socket) - end, - case PeerName of - {error, _Reason} -> self() ! {tcp_closed, Socket}; - {ok, _} -> ok - end. - -%% Data processing for connectors directly generating xmlelement in -%% Erlang data structure. -%% WARNING: Shaper does not work with Erlang data structure. -process_data([], State) -> - activate_socket(State), State; -process_data([Element | Els], - #state{c2s_pid = C2SPid} = State) - when element(1, Element) == xmlel; - element(1, Element) == xmlstreamstart; - element(1, Element) == xmlstreamelement; - element(1, Element) == xmlstreamend -> - if C2SPid == undefined -> State; - true -> - catch gen_fsm:send_event(C2SPid, - element_wrapper(Element)), - process_data(Els, State) - end; -%% Data processing for connectors receivind data as string. -process_data(Data, - #state{xml_stream_state = XMLStreamState, - shaper_state = ShaperState, c2s_pid = C2SPid} = - State) -> - ?DEBUG("Received XML on stream = ~p", [(Data)]), - XMLStreamState1 = xml_stream:parse(XMLStreamState, Data), - {NewShaperState, Pause} = shaper:update(ShaperState, byte_size(Data)), - if - C2SPid == undefined -> - ok; - Pause > 0 -> - erlang:start_timer(Pause, self(), activate); - true -> - activate_socket(State) - end, - State#state{xml_stream_state = XMLStreamState1, - shaper_state = NewShaperState}. - -%% Element coming from XML parser are wrapped inside xmlstreamelement -%% When we receive directly xmlelement tuple (from a socket module -%% speaking directly Erlang XML), we wrap it inside the same -%% xmlstreamelement coming from the XML parser. -element_wrapper(XMLElement) - when element(1, XMLElement) == xmlel -> - {xmlstreamelement, XMLElement}; -element_wrapper(Element) -> Element. - -close_stream(undefined) -> ok; -close_stream(XMLStreamState) -> - xml_stream:close(XMLStreamState). - -do_send(State, Data) -> - (State#state.sock_mod):send(State#state.socket, Data). - -do_call(Pid, Msg) -> - case catch gen_server:call(Pid, Msg) of - {'EXIT', Why} -> {error, Why}; - Res -> Res - end. diff --git a/src/ejabberd_redis.erl b/src/ejabberd_redis.erl new file mode 100644 index 000000000..c0e61c0e6 --- /dev/null +++ b/src/ejabberd_redis.erl @@ -0,0 +1,661 @@ +%%%------------------------------------------------------------------- +%%% File : ejabberd_redis.erl +%%% Author : Evgeny Khramtsov +%%% Created : 8 May 2016 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(ejabberd_redis). +-ifndef(GEN_SERVER). +-define(GEN_SERVER, gen_server). +-endif. +-behaviour(?GEN_SERVER). + +-compile({no_auto_import, [get/1, put/2]}). + +%% API +-export([start_link/1, get_proc/1, get_connection/1, q/1, qp/1, format_error/1]). +%% Commands +-export([multi/1, get/1, set/2, del/1, info/1, + sadd/2, srem/2, smembers/1, sismember/2, scard/1, + hget/2, hset/3, hdel/2, hlen/1, hgetall/1, hkeys/1, + subscribe/1, publish/2, script_load/1, evalsha/3]). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-define(TR_STACK, redis_transaction_stack). +-define(DEFAULT_MAX_QUEUE, 10000). +-define(MAX_RETRIES, 1). +-define(CALL_TIMEOUT, 60*1000). %% 60 seconds + +-include("logger.hrl"). + + +-record(state, {connection :: pid() | undefined, + num :: pos_integer(), + subscriptions = #{} :: subscriptions(), + pending_q :: queue()}). + +-type queue() :: p1_queue:queue({{pid(), term()}, integer()}). +-type subscriptions() :: #{binary() => [pid()]}. +-type error_reason() :: binary() | timeout | disconnected | overloaded. +-type redis_error() :: {error, error_reason()}. +-type redis_reply() :: undefined | binary() | [binary()]. +-type redis_command() :: [iodata() | integer()]. +-type redis_pipeline() :: [redis_command()]. +-type redis_info() :: server | clients | memory | persistence | + stats | replication | cpu | commandstats | + cluster | keyspace | default | all. +-type state() :: #state{}. + +-export_type([error_reason/0]). + +-ifdef(USE_OLD_HTTP_URI). % Erlang/OTP lower than 21 +-dialyzer([{no_return, do_connect/6}, + {no_unused, flush_queue/1}, + {no_match, flush_queue/1}, + {no_unused, re_subscribe/2}, + {no_match, handle_info/2}]). +-endif. + +%%%=================================================================== +%%% API +%%%=================================================================== +start_link(I) -> + ?GEN_SERVER:start_link({local, get_proc(I)}, ?MODULE, [I], []). + +get_proc(I) -> + misc:binary_to_atom( + iolist_to_binary( + [atom_to_list(?MODULE), $_, integer_to_list(I)])). + +get_connection(I) -> + misc:binary_to_atom( + iolist_to_binary( + [atom_to_list(?MODULE), "_connection_", integer_to_list(I)])). + +-spec q(redis_command()) -> {ok, redis_reply()} | redis_error(). +q(Command) -> + call(get_rnd_id(), {q, Command}, ?MAX_RETRIES). + +-spec qp(redis_pipeline()) -> [{ok, redis_reply()} | redis_error()] | redis_error(). +qp(Pipeline) -> + call(get_rnd_id(), {qp, Pipeline}, ?MAX_RETRIES). + +-spec multi(fun(() -> any())) -> {ok, redis_reply()} | redis_error(). +multi(F) -> + case erlang:get(?TR_STACK) of + undefined -> + erlang:put(?TR_STACK, []), + try F() of + _ -> + Stack = erlang:erase(?TR_STACK), + Command = [["MULTI"]|lists:reverse([["EXEC"]|Stack])], + case qp(Command) of + {error, _} = Err -> Err; + Result -> get_result(Result) + end + catch + E:R:St -> + erlang:erase(?TR_STACK), + erlang:raise(E, R, St) + end; + _ -> + erlang:error(nested_transaction) + end. + +-spec format_error(atom() | binary()) -> binary(). +format_error(Reason) when is_atom(Reason) -> + format_error(misc:atom_to_binary(Reason)); +format_error(Reason) -> + Reason. + +%%%=================================================================== +%%% Redis commands API +%%%=================================================================== +-spec get(iodata()) -> {ok, undefined | binary()} | redis_error(). +get(Key) -> + case erlang:get(?TR_STACK) of + undefined -> + q([<<"GET">>, Key]); + _ -> + erlang:error(transaction_unsupported) + end. + +-spec set(iodata(), iodata()) -> ok | redis_error() | queued. +set(Key, Val) -> + Cmd = [<<"SET">>, Key, Val], + case erlang:get(?TR_STACK) of + undefined -> + case q(Cmd) of + {ok, <<"OK">>} -> ok; + {error, _} = Err -> Err + end; + Stack -> + tr_enq(Cmd, Stack) + end. + +-spec del(list()) -> {ok, non_neg_integer()} | redis_error() | queued. +del([]) -> + reply(0); +del(Keys) -> + Cmd = [<<"DEL">>|Keys], + case erlang:get(?TR_STACK) of + undefined -> + case q(Cmd) of + {ok, N} -> {ok, binary_to_integer(N)}; + {error, _} = Err -> Err + end; + Stack -> + tr_enq(Cmd, Stack) + end. + +-spec sadd(iodata(), list()) -> {ok, non_neg_integer()} | redis_error() | queued. +sadd(_Set, []) -> + reply(0); +sadd(Set, Members) -> + Cmd = [<<"SADD">>, Set|Members], + case erlang:get(?TR_STACK) of + undefined -> + case q(Cmd) of + {ok, N} -> {ok, binary_to_integer(N)}; + {error, _} = Err -> Err + end; + Stack -> + tr_enq(Cmd, Stack) + end. + +-spec srem(iodata(), list()) -> {ok, non_neg_integer()} | redis_error() | queued. +srem(_Set, []) -> + reply(0); +srem(Set, Members) -> + Cmd = [<<"SREM">>, Set|Members], + case erlang:get(?TR_STACK) of + undefined -> + case q(Cmd) of + {ok, N} -> {ok, binary_to_integer(N)}; + {error, _} = Err -> Err + end; + Stack -> + tr_enq(Cmd, Stack) + end. + +-spec smembers(iodata()) -> {ok, [binary()]} | redis_error(). +smembers(Set) -> + case erlang:get(?TR_STACK) of + undefined -> + q([<<"SMEMBERS">>, Set]); + _ -> + erlang:error(transaction_unsupported) + end. + +-spec sismember(iodata(), iodata()) -> boolean() | redis_error(). +sismember(Set, Member) -> + case erlang:get(?TR_STACK) of + undefined -> + case q([<<"SISMEMBER">>, Set, Member]) of + {ok, Flag} -> {ok, dec_bool(Flag)}; + {error, _} = Err -> Err + end; + _ -> + erlang:error(transaction_unsupported) + end. + +-spec scard(iodata()) -> {ok, non_neg_integer()} | redis_error(). +scard(Set) -> + case erlang:get(?TR_STACK) of + undefined -> + case q([<<"SCARD">>, Set]) of + {ok, N} -> + {ok, binary_to_integer(N)}; + {error, _} = Err -> + Err + end; + _ -> + erlang:error(transaction_unsupported) + end. + +-spec hget(iodata(), iodata()) -> {ok, undefined | binary()} | redis_error(). +hget(Key, Field) -> + case erlang:get(?TR_STACK) of + undefined -> + q([<<"HGET">>, Key, Field]); + _ -> + erlang:error(transaction_unsupported) + end. + +-spec hset(iodata(), iodata(), iodata()) -> {ok, boolean()} | redis_error() | queued. +hset(Key, Field, Val) -> + Cmd = [<<"HSET">>, Key, Field, Val], + case erlang:get(?TR_STACK) of + undefined -> + case q(Cmd) of + {ok, Flag} -> {ok, dec_bool(Flag)}; + {error, _} = Err -> Err + end; + Stack -> + tr_enq(Cmd, Stack) + end. + +-spec hdel(iodata(), list()) -> {ok, non_neg_integer()} | redis_error() | queued. +hdel(_Key, []) -> + reply(0); +hdel(Key, Fields) -> + Cmd = [<<"HDEL">>, Key|Fields], + case erlang:get(?TR_STACK) of + undefined -> + case q(Cmd) of + {ok, N} -> {ok, binary_to_integer(N)}; + {error, _} = Err -> Err + end; + Stack -> + tr_enq(Cmd, Stack) + end. + +-spec hgetall(iodata()) -> {ok, [{binary(), binary()}]} | redis_error(). +hgetall(Key) -> + case erlang:get(?TR_STACK) of + undefined -> + case q([<<"HGETALL">>, Key]) of + {ok, Pairs} -> {ok, decode_pairs(Pairs)}; + {error, _} = Err -> Err + end; + _ -> + erlang:error(transaction_unsupported) + end. + +-spec hlen(iodata()) -> {ok, non_neg_integer()} | redis_error(). +hlen(Key) -> + case erlang:get(?TR_STACK) of + undefined -> + case q([<<"HLEN">>, Key]) of + {ok, N} -> {ok, binary_to_integer(N)}; + {error, _} = Err -> Err + end; + _ -> + erlang:error(transaction_unsupported) + end. + +-spec hkeys(iodata()) -> {ok, [binary()]} | redis_error(). +hkeys(Key) -> + case erlang:get(?TR_STACK) of + undefined -> + q([<<"HKEYS">>, Key]); + _ -> + erlang:error(transaction_unsupported) + end. + +-spec subscribe([binary()]) -> ok | redis_error(). +subscribe(Channels) -> + try gen_server_call(get_proc(1), {subscribe, self(), Channels}) + catch exit:{Why, {?GEN_SERVER, call, _}} -> + Reason = case Why of + timeout -> timeout; + _ -> disconnected + end, + {error, Reason} + end. + +-spec publish(iodata(), iodata()) -> {ok, non_neg_integer()} | redis_error() | queued. +publish(Channel, Data) -> + Cmd = [<<"PUBLISH">>, Channel, Data], + case erlang:get(?TR_STACK) of + undefined -> + case q(Cmd) of + {ok, N} -> {ok, binary_to_integer(N)}; + {error, _} = Err -> Err + end; + Stack -> + tr_enq(Cmd, Stack) + end. + +-spec script_load(iodata()) -> {ok, binary()} | redis_error(). +script_load(Data) -> + case erlang:get(?TR_STACK) of + undefined -> + q([<<"SCRIPT">>, <<"LOAD">>, Data]); + _ -> + erlang:error(transaction_unsupported) + end. + +-spec evalsha(binary(), [iodata()], [iodata() | integer()]) -> {ok, binary()} | redis_error(). +evalsha(SHA, Keys, Args) -> + case erlang:get(?TR_STACK) of + undefined -> + q([<<"EVALSHA">>, SHA, length(Keys)|Keys ++ Args]); + _ -> + erlang:error(transaction_unsupported) + end. + +-spec info(redis_info()) -> {ok, [{atom(), binary()}]} | redis_error(). +info(Type) -> + case erlang:get(?TR_STACK) of + undefined -> + case q([<<"INFO">>, misc:atom_to_binary(Type)]) of + {ok, Info} -> + Lines = binary:split(Info, <<"\r\n">>, [global]), + KVs = [binary:split(Line, <<":">>) || Line <- Lines], + {ok, [{misc:binary_to_atom(K), V} || [K, V] <- KVs]}; + {error, _} = Err -> + Err + end; + _ -> + erlang:error(transaction_unsupported) + end. + +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== +init([I]) -> + process_flag(trap_exit, true), + QueueType = get_queue_type(), + Limit = max_fsm_queue(), + self() ! connect, + {ok, #state{num = I, pending_q = p1_queue:new(QueueType, Limit)}}. + +handle_call(connect, From, #state{connection = undefined, + pending_q = Q} = State) -> + CurrTime = erlang:monotonic_time(millisecond), + Q2 = try p1_queue:in({From, CurrTime}, Q) + catch error:full -> + Q1 = clean_queue(Q, CurrTime), + p1_queue:in({From, CurrTime}, Q1) + end, + {noreply, State#state{pending_q = Q2}}; +handle_call(connect, From, #state{connection = Pid} = State) -> + case is_process_alive(Pid) of + true -> + {reply, ok, State}; + false -> + self() ! connect, + handle_call(connect, From, State#state{connection = undefined}) + end; +handle_call({subscribe, Caller, Channels}, _From, + #state{connection = Pid, subscriptions = Subs} = State) -> + Subs1 = lists:foldl( + fun(Channel, Acc) -> + Callers = maps:get(Channel, Acc, []) -- [Caller], + maps:put(Channel, [Caller|Callers], Acc) + end, Subs, Channels), + eredis_subscribe(Pid, Channels), + {reply, ok, State#state{subscriptions = Subs1}}; +handle_call(Request, _From, State) -> + ?WARNING_MSG("Unexpected call: ~p", [Request]), + {noreply, State}. + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info(connect, #state{connection = undefined} = State) -> + NewState = case connect(State) of + {ok, Connection} -> + Q1 = flush_queue(State#state.pending_q), + re_subscribe(Connection, State#state.subscriptions), + State#state{connection = Connection, pending_q = Q1}; + {error, _} -> + State + end, + {noreply, NewState}; +handle_info(connect, State) -> + %% Already connected + {noreply, State}; +handle_info({'EXIT', Pid, _}, State) -> + case State#state.connection of + Pid -> + self() ! connect, + {noreply, State#state{connection = undefined}}; + _ -> + {noreply, State} + end; +handle_info({subscribed, Channel, Pid}, State) -> + case State#state.connection of + Pid -> + case maps:is_key(Channel, State#state.subscriptions) of + true -> eredis_sub:ack_message(Pid); + false -> + ?WARNING_MSG("Got subscription ack for unknown channel ~ts", + [Channel]) + end; + _ -> + ok + end, + {noreply, State}; +handle_info({message, Channel, Data, Pid}, State) -> + case State#state.connection of + Pid -> + lists:foreach( + fun(Subscriber) -> + erlang:send(Subscriber, {redis_message, Channel, Data}) + end, maps:get(Channel, State#state.subscriptions, [])), + eredis_sub:ack_message(Pid); + _ -> + ok + end, + {noreply, State}; +handle_info(Info, State) -> + ?WARNING_MSG("Unexpected info = ~p", [Info]), + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +-spec connect(state()) -> {ok, pid()} | {error, any()}. +connect(#state{num = Num}) -> + Server1 = ejabberd_option:redis_server(), + Port = ejabberd_option:redis_port(), + DB = ejabberd_option:redis_db(), + Pass = ejabberd_option:redis_password(), + ConnTimeout = ejabberd_option:redis_connect_timeout(), + Server = parse_server(Server1), + try case do_connect(Num, Server, Port, Pass, DB, ConnTimeout) of + {ok, Client} -> + ?DEBUG("Connection #~p established to Redis at ~ts:~p", + [Num, Server, Port]), + register(get_connection(Num), Client), + {ok, Client}; + {error, Why} -> + erlang:error(Why) + end + catch _:Reason -> + Timeout = p1_rand:uniform( + min(10, ejabberd_redis_sup:get_pool_size())), + ?ERROR_MSG("Redis connection #~p at ~ts:~p has failed: ~p; " + "reconnecting in ~p seconds", + [Num, Server, Port, Reason, Timeout]), + erlang:send_after(timer:seconds(Timeout), self(), connect), + {error, Reason} + end. + +parse_server([$u,$n,$i,$x,$: | Path]) -> + {local, Path}; +parse_server(Server) -> + Server. + +do_connect(1, Server, Port, Pass, _DB, _ConnTimeout) -> + %% First connection in the pool is always a subscriber + Options = [{host, Server}, + {port, Port}, + {password, Pass}, + {reconnect_sleep, no_reconnect}, + {max_queue_size, infinity}, + {queue_behaviour, drop}], + Res = eredis_sub:start_link(Options), + case Res of + {ok, Pid} -> eredis_sub:controlling_process(Pid); + _ -> ok + end, + Res; +do_connect(_, Server, Port, Pass, DB, ConnTimeout) -> + Options = [{host, Server}, + {port, Port}, + {database, DB}, + {password, Pass}, + {reconnect_sleep, no_reconnect}, + {connect_timeout, ConnTimeout}], + eredis:start_link(Options). + +-spec call(pos_integer(), {q, redis_command()}, integer()) -> + {ok, redis_reply()} | redis_error(); + (pos_integer(), {qp, redis_pipeline()}, integer()) -> + [{ok, redis_reply()} | redis_error()] | redis_error(). +call(I, {F, Cmd}, Retries) -> + ?DEBUG("Redis query: ~p", [Cmd]), + Conn = get_connection(I), + Res = try eredis:F(Conn, Cmd, ?CALL_TIMEOUT) of + {error, Reason} when is_atom(Reason) -> + try exit(whereis(Conn), kill) catch _:_ -> ok end, + {error, disconnected}; + Other -> + Other + catch exit:{timeout, _} -> {error, timeout}; + exit:{_, {gen_server, call, _}} -> {error, disconnected} + end, + case Res of + {error, disconnected} when Retries > 0 -> + try gen_server_call(get_proc(I), connect) of + ok -> call(I, {F, Cmd}, Retries-1); + {error, _} = Err -> Err + catch exit:{Why, {?GEN_SERVER, call, _}} -> + Reason1 = case Why of + timeout -> timeout; + _ -> disconnected + end, + log_error(Cmd, Reason1), + {error, Reason1} + end; + {error, Reason1} -> + log_error(Cmd, Reason1), + Res; + _ -> + Res + end. + +gen_server_call(Proc, Msg) -> + case ejabberd_redis_sup:start() of + ok -> + ?GEN_SERVER:call(Proc, Msg, ?CALL_TIMEOUT); + {error, _} -> + {error, disconnected} + end. + +-spec log_error(redis_command() | redis_pipeline(), atom() | binary()) -> ok. +log_error(Cmd, Reason) -> + ?ERROR_MSG("Redis request has failed:~n" + "** request = ~p~n" + "** response = ~ts", + [Cmd, format_error(Reason)]). + +-spec get_rnd_id() -> pos_integer(). +get_rnd_id() -> + p1_rand:round_robin(ejabberd_redis_sup:get_pool_size() - 1) + 2. + +-spec get_result([{ok, redis_reply()} | redis_error()]) -> + {ok, redis_reply()} | redis_error(). +get_result([{error, _} = Err|_]) -> + Err; +get_result([{ok, _} = OK]) -> + OK; +get_result([_|T]) -> + get_result(T). + +-spec tr_enq([iodata()], list()) -> queued. +tr_enq(Cmd, Stack) -> + erlang:put(?TR_STACK, [Cmd|Stack]), + queued. + +-spec decode_pairs([binary()]) -> [{binary(), binary()}]. +decode_pairs(Pairs) -> + decode_pairs(Pairs, []). + +-spec decode_pairs([binary()], [{binary(), binary()}]) -> [{binary(), binary()}]. +decode_pairs([Field, Val|Pairs], Acc) -> + decode_pairs(Pairs, [{Field, Val}|Acc]); +decode_pairs([], Acc) -> + lists:reverse(Acc). + +dec_bool(<<$1>>) -> true; +dec_bool(<<$0>>) -> false. + +-spec reply(T) -> {ok, T} | queued. +reply(Val) -> + case erlang:get(?TR_STACK) of + undefined -> {ok, Val}; + _ -> queued + end. + +-spec max_fsm_queue() -> pos_integer(). +max_fsm_queue() -> + proplists:get_value(max_queue, fsm_limit_opts(), ?DEFAULT_MAX_QUEUE). + +fsm_limit_opts() -> + ejabberd_config:fsm_limit_opts([]). + +get_queue_type() -> + ejabberd_option:redis_queue_type(). + +-spec flush_queue(queue()) -> queue(). +flush_queue(Q) -> + CurrTime = erlang:monotonic_time(millisecond), + p1_queue:dropwhile( + fun({From, Time}) -> + if (CurrTime - Time) >= ?CALL_TIMEOUT -> + ok; + true -> + ?GEN_SERVER:reply(From, ok) + end, + true + end, Q). + +-spec clean_queue(queue(), integer()) -> queue(). +clean_queue(Q, CurrTime) -> + Q1 = p1_queue:dropwhile( + fun({_From, Time}) -> + (CurrTime - Time) >= ?CALL_TIMEOUT + end, Q), + Len = p1_queue:len(Q1), + Limit = p1_queue:get_limit(Q1), + if Len >= Limit -> + ?ERROR_MSG("Redis request queue is overloaded", []), + p1_queue:dropwhile( + fun({From, _Time}) -> + ?GEN_SERVER:reply(From, {error, overloaded}), + true + end, Q1); + true -> + Q1 + end. + +re_subscribe(Pid, Subs) -> + case maps:keys(Subs) of + [] -> ok; + Channels -> eredis_subscribe(Pid, Channels) + end. + +eredis_subscribe(Pid, Channels) -> + ?DEBUG("Redis query: ~p", [[<<"SUBSCRIBE">>|Channels]]), + eredis_sub:subscribe(Pid, Channels). diff --git a/src/ejabberd_redis_sup.erl b/src/ejabberd_redis_sup.erl new file mode 100644 index 000000000..8d49f4632 --- /dev/null +++ b/src/ejabberd_redis_sup.erl @@ -0,0 +1,109 @@ +%%%------------------------------------------------------------------- +%%% Author : Evgeny Khramtsov +%%% Created : 6 Apr 2017 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(ejabberd_redis_sup). + +-behaviour(supervisor). + +%% API +-export([start/0, stop/0, start_link/0]). +-export([get_pool_size/0, config_reloaded/0]). + +%% Supervisor callbacks +-export([init/1]). + +-include("logger.hrl"). + +%%%=================================================================== +%%% API functions +%%%=================================================================== +start() -> + case is_started() of + true -> ok; + false -> + ejabberd:start_app(eredis), + Spec = {?MODULE, {?MODULE, start_link, []}, + permanent, infinity, supervisor, [?MODULE]}, + case supervisor:start_child(ejabberd_db_sup, Spec) of + {ok, _} -> ok; + {error, {already_started, Pid}} -> + %% Wait for the supervisor to fully start + _ = supervisor:count_children(Pid), + ok; + {error, Why} = Err -> + ?ERROR_MSG("Failed to start ~ts: ~p", [?MODULE, Why]), + Err + end + end. + +stop() -> + ejabberd_hooks:delete(config_reloaded, ?MODULE, config_reloaded, 20), + _ = supervisor:terminate_child(ejabberd_db_sup, ?MODULE), + _ = supervisor:delete_child(ejabberd_db_sup, ?MODULE), + ok. + +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +config_reloaded() -> + case is_started() of + true -> + lists:foreach( + fun(Spec) -> + supervisor:start_child(?MODULE, Spec) + end, get_specs()), + PoolSize = get_pool_size(), + lists:foreach( + fun({Id, _, _, _}) when Id > PoolSize -> + case supervisor:terminate_child(?MODULE, Id) of + ok -> supervisor:delete_child(?MODULE, Id); + _ -> ok + end; + (_) -> + ok + end, supervisor:which_children(?MODULE)); + false -> + ok + end. + +%%%=================================================================== +%%% Supervisor callbacks +%%%=================================================================== +init([]) -> + ejabberd_hooks:add(config_reloaded, ?MODULE, config_reloaded, 20), + {ok, {{one_for_one, 500, 1}, get_specs()}}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +get_specs() -> + lists:map( + fun(I) -> + {I, {ejabberd_redis, start_link, [I]}, + transient, 2000, worker, [?MODULE]} + end, lists:seq(1, get_pool_size())). + +get_pool_size() -> + ejabberd_option:redis_pool_size() + 1. + +is_started() -> + whereis(?MODULE) /= undefined. diff --git a/src/ejabberd_regexp.erl b/src/ejabberd_regexp.erl index 86ebd1846..0d18deac6 100644 --- a/src/ejabberd_regexp.erl +++ b/src/ejabberd_regexp.erl @@ -1,11 +1,11 @@ %%%---------------------------------------------------------------------- %%% File : ejabberd_regexp.erl -%%% Author : Badlop -%%% Purpose : Frontend to Re and Regexp OTP modules -%%% Created : 8 Dec 2011 by Badlop +%%% Author : Badlop +%%% Purpose : Frontend to Re OTP module +%%% Created : 8 Dec 2011 by Badlop %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -25,71 +25,84 @@ -module(ejabberd_regexp). --compile([export_all]). - -exec({ReM, ReF, ReA}, {RgM, RgF, RgA}) -> - try apply(ReM, ReF, ReA) catch - error:undef -> apply(RgM, RgF, RgA); - A:B -> {error, {A, B}} - end. +-export([run/2, split/2, replace/3, greplace/3, sh_to_awk/1]). -spec run(binary(), binary()) -> match | nomatch | {error, any()}. run(String, Regexp) -> - case exec({re, run, [String, Regexp, [{capture, none}]]}, - {regexp, first_match, [binary_to_list(String), - binary_to_list(Regexp)]}) - of - {match, _, _} -> match; - {match, _} -> match; - match -> match; - nomatch -> nomatch; - {error, Error} -> {error, Error} - end. + re:run(String, Regexp, [{capture, none}, unicode]). -spec split(binary(), binary()) -> [binary()]. split(String, Regexp) -> - case exec({re, split, [String, Regexp, [{return, binary}]]}, - {regexp, split, [binary_to_list(String), - binary_to_list(Regexp)]}) - of - {ok, FieldList} -> [iolist_to_binary(F) || F <- FieldList]; - {error, Error} -> throw(Error); - A -> A - end. + re:split(String, Regexp, [{return, binary}]). -spec replace(binary(), binary(), binary()) -> binary(). replace(String, Regexp, New) -> - case exec({re, replace, [String, Regexp, New, [{return, binary}]]}, - {regexp, sub, [binary_to_list(String), - binary_to_list(Regexp), - binary_to_list(New)]}) - of - {ok, NewString, _RepCount} -> iolist_to_binary(NewString); - {error, Error} -> throw(Error); - A -> A - end. + re:replace(String, Regexp, New, [{return, binary}]). -spec greplace(binary(), binary(), binary()) -> binary(). greplace(String, Regexp, New) -> - case exec({re, replace, [String, Regexp, New, [global, {return, binary}]]}, - {regexp, sub, [binary_to_list(String), - binary_to_list(Regexp), - binary_to_list(New)]}) - of - {ok, NewString, _RepCount} -> iolist_to_binary(NewString); - {error, Error} -> throw(Error); - A -> A - end. + re:replace(String, Regexp, New, [global, {return, binary}]). + +%% This code was copied and adapted from xmerl_regexp.erl -spec sh_to_awk(binary()) -> binary(). +sh_to_awk(Sh) -> + iolist_to_binary([<<"^(">>, sh_to_awk_1(Sh)]). %Fix the beginning -sh_to_awk(ShRegExp) -> - case exec({xmerl_regexp, sh_to_awk, [binary_to_list(ShRegExp)]}, - {regexp, sh_to_awk, [binary_to_list(ShRegExp)]}) - of - A -> iolist_to_binary(A) - end. +sh_to_awk_1(<<"*", Sh/binary>>) -> %This matches any string + [<<".*">>, sh_to_awk_1(Sh)]; +sh_to_awk_1(<<"?", Sh/binary>>) -> %This matches any character + [$., sh_to_awk_1(Sh)]; +sh_to_awk_1(<<"[^]", Sh/binary>>) -> %This takes careful handling + [<<"\\^">>, sh_to_awk_1(Sh)]; +%% Must move '^' to end. +sh_to_awk_1(<<"[^", Sh/binary>>) -> + [$[, sh_to_awk_2(Sh, true)]; +sh_to_awk_1(<<"[!", Sh/binary>>) -> + [<<"[^">>, sh_to_awk_2(Sh, false)]; +sh_to_awk_1(<<"[", Sh/binary>>) -> + [$[, sh_to_awk_2(Sh, false)]; +sh_to_awk_1(<>) -> %% Unspecialise everything else which is not an escape character. + case sh_special_char(C) of + true -> [$\\,C|sh_to_awk_1(Sh)]; + false -> [C|sh_to_awk_1(Sh)] + end; +sh_to_awk_1(<<>>) -> + <<")$">>. %Fix the end + +sh_to_awk_2(<<"]", Sh/binary>>, UpArrow) -> + [$]|sh_to_awk_3(Sh, UpArrow)]; +sh_to_awk_2(Sh, UpArrow) -> + sh_to_awk_3(Sh, UpArrow). + +sh_to_awk_3(<<"]", Sh/binary>>, true) -> + [<<"^]">>, sh_to_awk_1(Sh)]; +sh_to_awk_3(<<"]", Sh/binary>>, false) -> + [$]|sh_to_awk_1(Sh)]; +sh_to_awk_3(<>, UpArrow) -> + [C|sh_to_awk_3(Sh, UpArrow)]; +sh_to_awk_3(<<>>, true) -> + [$^|sh_to_awk_1(<<>>)]; +sh_to_awk_3(<<>>, false) -> + sh_to_awk_1(<<>>). + +%% Test if a character is a special character. +-spec sh_special_char(char()) -> boolean(). +sh_special_char($|) -> true; +sh_special_char($*) -> true; +sh_special_char($+) -> true; +sh_special_char($?) -> true; +sh_special_char($() -> true; +sh_special_char($)) -> true; +sh_special_char($\\) -> true; +sh_special_char($^) -> true; +sh_special_char($$) -> true; +sh_special_char($.) -> true; +sh_special_char($[) -> true; +sh_special_char($]) -> true; +sh_special_char($") -> true; +sh_special_char(_C) -> false. diff --git a/src/ejabberd_riak.erl b/src/ejabberd_riak.erl deleted file mode 100644 index f677ca91a..000000000 --- a/src/ejabberd_riak.erl +++ /dev/null @@ -1,554 +0,0 @@ -%%%------------------------------------------------------------------- -%%% @author Alexey Shchepin -%%% @doc -%%% Interface for Riak database -%%% @end -%%% Created : 29 Dec 2011 by Alexey Shchepin -%%% @copyright (C) 2002-2015 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., 59 Temple Place, Suite 330, Boston, MA -%%% 02111-1307 USA -%%% -%%%------------------------------------------------------------------- --module(ejabberd_riak). - --behaviour(gen_server). - -%% API --export([start_link/4, get_proc/1, make_bucket/1, put/2, put/3, - get/2, get/3, get_by_index/4, delete/1, delete/2, - count_by_index/3, get_by_index_range/5, - get_keys/1, get_keys_by_index/3, is_connected/0, - count/1, delete_by_index/3]). -%% For debugging --export([get_tables/0]). -%% map/reduce exports --export([map_key/3]). - -%% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). - --include("ejabberd.hrl"). --include("logger.hrl"). - --record(state, {pid = self() :: pid()}). - --type index() :: {binary(), any()}. - --type index_info() :: [{i, any()} | {'2i', [index()]}]. - -%% The `record_schema()' is just a tuple: -%% {record_info(fields, some_record), #some_record{}} - --type record_schema() :: {[atom()], tuple()}. - -%% The `index_info()' is used in put/delete functions: -%% `i' defines a primary index, `` '2i' '' defines secondary indexes. -%% There must be only one primary index. If `i' is not specified, -%% the first element of the record is assumed as a primary index, -%% i.e. `i' = element(2, Record). - --export_types([index_info/0]). - -%%%=================================================================== -%%% API -%%%=================================================================== -%% @private -start_link(Num, Server, Port, _StartInterval) -> - gen_server:start_link({local, get_proc(Num)}, ?MODULE, [Server, Port], []). - -%% @private -is_connected() -> - catch riakc_pb_socket:is_connected(get_random_pid()). - -%% @private -get_proc(I) -> - jlib:binary_to_atom( - iolist_to_binary( - [atom_to_list(?MODULE), $_, integer_to_list(I)])). - --spec make_bucket(atom()) -> binary(). -%% @doc Makes a bucket from a table name -%% @private -make_bucket(Table) -> - erlang:atom_to_binary(Table, utf8). - --spec put(tuple(), record_schema()) -> ok | {error, any()}. -%% @equiv put(Record, []) -put(Record, RecFields) -> - ?MODULE:put(Record, RecFields, []). - --spec put(tuple(), record_schema(), index_info()) -> ok | {error, any()}. -%% @doc Stores a record `Rec' with indexes described in ``IndexInfo'' -put(Rec, RecSchema, IndexInfo) -> - Key = encode_key(proplists:get_value(i, IndexInfo, element(2, Rec))), - SecIdxs = [encode_index_key(K, V) || - {K, V} <- proplists:get_value('2i', IndexInfo, [])], - Table = element(1, Rec), - Value = encode_record(Rec, RecSchema), - case put_raw(Table, Key, Value, SecIdxs) of - ok -> - ok; - {error, _} = Error -> - log_error(Error, put, [{record, Rec}, - {index_info, IndexInfo}]), - Error - end. - -put_raw(Table, Key, Value, Indexes) -> - Bucket = make_bucket(Table), - Obj = riakc_obj:new(Bucket, Key, Value, "application/x-erlang-term"), - Obj1 = if Indexes /= [] -> - MetaData = dict:store(<<"index">>, Indexes, dict:new()), - riakc_obj:update_metadata(Obj, MetaData); - true -> - Obj - end, - catch riakc_pb_socket:put(get_random_pid(), Obj1). - -get_object_raw(Table, Key) -> - Bucket = make_bucket(Table), - catch riakc_pb_socket:get(get_random_pid(), Bucket, Key). - --spec get(atom(), record_schema()) -> {ok, [any()]} | {error, any()}. -%% @doc Returns all objects from table `Table' -get(Table, RecSchema) -> - Bucket = make_bucket(Table), - case catch riakc_pb_socket:mapred( - get_random_pid(), - Bucket, - [{map, {modfun, riak_kv_mapreduce, map_object_value}, - none, true}]) of - {ok, [{_, Objs}]} -> - {ok, lists:flatmap( - fun(Obj) -> - case catch decode_record(Obj, RecSchema) of - {'EXIT', _} -> - Error = {error, make_invalid_object(Obj)}, - log_error(Error, get, - [{table, Table}]), - []; - Term -> - [Term] - end - end, Objs)}; - {ok, []} -> - {ok, []}; - {error, notfound} -> - {ok, []}; - {error, _} = Error -> - Error - end. - --spec get(atom(), record_schema(), any()) -> {ok, any()} | {error, any()}. -%% @doc Reads record by `Key' from table `Table' -get(Table, RecSchema, Key) -> - case get_raw(Table, encode_key(Key)) of - {ok, Val} -> - case catch decode_record(Val, RecSchema) of - {'EXIT', _} -> - Error = {error, make_invalid_object(Val)}, - log_error(Error, get, [{table, Table}, {key, Key}]), - {error, notfound}; - Term -> - {ok, Term} - end; - {error, _} = Error -> - log_error(Error, get, [{table, Table}, - {key, Key}]), - Error - end. - --spec get_by_index(atom(), record_schema(), binary(), any()) -> - {ok, [any()]} | {error, any()}. -%% @doc Reads records by `Index' and value `Key' from `Table' -get_by_index(Table, RecSchema, Index, Key) -> - {NewIndex, NewKey} = encode_index_key(Index, Key), - case get_by_index_raw(Table, NewIndex, NewKey) of - {ok, Vals} -> - {ok, lists:flatmap( - fun(Val) -> - case catch decode_record(Val, RecSchema) of - {'EXIT', _} -> - Error = {error, make_invalid_object(Val)}, - log_error(Error, get_by_index, - [{table, Table}, - {index, Index}, - {key, Key}]), - []; - Term -> - [Term] - end - end, Vals)}; - {error, notfound} -> - {ok, []}; - {error, _} = Error -> - log_error(Error, get_by_index, - [{table, Table}, - {index, Index}, - {key, Key}]), - Error - end. - --spec get_by_index_range(atom(), record_schema(), binary(), any(), any()) -> - {ok, [any()]} | {error, any()}. -%% @doc Reads records by `Index' in the range `FromKey'..`ToKey' from `Table' -get_by_index_range(Table, RecSchema, Index, FromKey, ToKey) -> - {NewIndex, NewFromKey} = encode_index_key(Index, FromKey), - {NewIndex, NewToKey} = encode_index_key(Index, ToKey), - case get_by_index_range_raw(Table, NewIndex, NewFromKey, NewToKey) of - {ok, Vals} -> - {ok, lists:flatmap( - fun(Val) -> - case catch decode_record(Val, RecSchema) of - {'EXIT', _} -> - Error = {error, make_invalid_object(Val)}, - log_error(Error, get_by_index_range, - [{table, Table}, - {index, Index}, - {start_key, FromKey}, - {end_key, ToKey}]), - []; - Term -> - [Term] - end - end, Vals)}; - {error, notfound} -> - {ok, []}; - {error, _} = Error -> - log_error(Error, get_by_index_range, - [{table, Table}, {index, Index}, - {start_key, FromKey}, {end_key, ToKey}]), - Error - end. - -get_raw(Table, Key) -> - case get_object_raw(Table, Key) of - {ok, Obj} -> - {ok, riakc_obj:get_value(Obj)}; - {error, _} = Error -> - Error - end. - --spec get_keys(atom()) -> {ok, [any()]} | {error, any()}. -%% @doc Returns a list of index values -get_keys(Table) -> - Bucket = make_bucket(Table), - case catch riakc_pb_socket:mapred( - get_random_pid(), - Bucket, - [{map, {modfun, ?MODULE, map_key}, none, true}]) of - {ok, [{_, Keys}]} -> - {ok, Keys}; - {ok, []} -> - {ok, []}; - {error, _} = Error -> - log_error(Error, get_keys, [{table, Table}]), - Error - end. - --spec get_keys_by_index(atom(), binary(), - any()) -> {ok, [any()]} | {error, any()}. -%% @doc Returns a list of primary keys of objects indexed by `Key'. -get_keys_by_index(Table, Index, Key) -> - {NewIndex, NewKey} = encode_index_key(Index, Key), - Bucket = make_bucket(Table), - case catch riakc_pb_socket:mapred( - get_random_pid(), - {index, Bucket, NewIndex, NewKey}, - [{map, {modfun, ?MODULE, map_key}, none, true}]) of - {ok, [{_, Keys}]} -> - {ok, Keys}; - {ok, []} -> - {ok, []}; - {error, _} = Error -> - log_error(Error, get_keys_by_index, [{table, Table}, - {index, Index}, - {key, Key}]), - Error - end. - -%% @hidden -get_tables() -> - catch riakc_pb_socket:list_buckets(get_random_pid()). - -get_by_index_raw(Table, Index, Key) -> - Bucket = make_bucket(Table), - case riakc_pb_socket:mapred( - get_random_pid(), - {index, Bucket, Index, Key}, - [{map, {modfun, riak_kv_mapreduce, map_object_value}, - none, true}]) of - {ok, [{_, Objs}]} -> - {ok, Objs}; - {ok, []} -> - {ok, []}; - {error, _} = Error -> - Error - end. - -get_by_index_range_raw(Table, Index, FromKey, ToKey) -> - Bucket = make_bucket(Table), - case catch riakc_pb_socket:mapred( - get_random_pid(), - {index, Bucket, Index, FromKey, ToKey}, - [{map, {modfun, riak_kv_mapreduce, map_object_value}, - none, true}]) of - {ok, [{_, Objs}]} -> - {ok, Objs}; - {ok, []} -> - {ok, []}; - {error, _} = Error -> - Error - end. - --spec count(atom()) -> {ok, non_neg_integer()} | {error, any()}. -%% @doc Returns the number of objects in the `Table' -count(Table) -> - Bucket = make_bucket(Table), - case catch riakc_pb_socket:mapred( - get_random_pid(), - Bucket, - [{reduce, {modfun, riak_kv_mapreduce, reduce_count_inputs}, - none, true}]) of - {ok, [{_, [Cnt]}]} -> - {ok, Cnt}; - {error, _} = Error -> - log_error(Error, count, [{table, Table}]), - Error - end. - --spec count_by_index(atom(), binary(), any()) -> - {ok, non_neg_integer()} | {error, any()}. -%% @doc Returns the number of objects in the `Table' by index -count_by_index(Tab, Index, Key) -> - {NewIndex, NewKey} = encode_index_key(Index, Key), - case count_by_index_raw(Tab, NewIndex, NewKey) of - {ok, Cnt} -> - {ok, Cnt}; - {error, notfound} -> - {ok, 0}; - {error, _} = Error -> - log_error(Error, count_by_index, - [{table, Tab}, - {index, Index}, - {key, Key}]), - Error - end. - -count_by_index_raw(Table, Index, Key) -> - Bucket = make_bucket(Table), - case catch riakc_pb_socket:mapred( - get_random_pid(), - {index, Bucket, Index, Key}, - [{reduce, {modfun, riak_kv_mapreduce, reduce_count_inputs}, - none, true}]) of - {ok, [{_, [Cnt]}]} -> - {ok, Cnt}; - {error, _} = Error -> - Error - end. - --spec delete(tuple() | atom()) -> ok | {error, any()}. -%% @doc Same as delete(T, []) when T is record. -%% Or deletes all elements from table if T is atom. -delete(Rec) when is_tuple(Rec) -> - delete(Rec, []); -delete(Table) when is_atom(Table) -> - try - {ok, Keys} = ?MODULE:get_keys(Table), - lists:foreach( - fun(K) -> - ok = delete(Table, K) - end, Keys) - catch _:{badmatch, Err} -> - Err - end. - --spec delete(tuple() | atom(), index_info() | any()) -> ok | {error, any()}. -%% @doc Delete an object -delete(Rec, Opts) when is_tuple(Rec) -> - Table = element(1, Rec), - Key = proplists:get_value(i, Opts, element(2, Rec)), - delete(Table, Key); -delete(Table, Key) when is_atom(Table) -> - case delete_raw(Table, encode_key(Key)) of - ok -> - ok; - Err -> - log_error(Err, delete, [{table, Table}, {key, Key}]), - Err - end. - -delete_raw(Table, Key) -> - Bucket = make_bucket(Table), - catch riakc_pb_socket:delete(get_random_pid(), Bucket, Key). - --spec delete_by_index(atom(), binary(), any()) -> ok | {error, any()}. -%% @doc Deletes objects by index -delete_by_index(Table, Index, Key) -> - try - {ok, Keys} = get_keys_by_index(Table, Index, Key), - lists:foreach( - fun(K) -> - ok = delete(Table, K) - end, Keys) - catch _:{badmatch, Err} -> - Err - end. - -%%%=================================================================== -%%% map/reduce functions -%%%=================================================================== -%% @private -map_key(Obj, _, _) -> - [case riak_object:key(Obj) of - <<"b_", B/binary>> -> - B; - <<"i_", B/binary>> -> - list_to_integer(binary_to_list(B)); - B -> - erlang:binary_to_term(B) - end]. - -%%%=================================================================== -%%% gen_server API -%%%=================================================================== -%% @private -init([Server, Port]) -> - case riakc_pb_socket:start( - Server, Port, - [auto_reconnect]) of - {ok, Pid} -> - erlang:monitor(process, Pid), - {ok, #state{pid = Pid}}; - Err -> - {stop, Err} - end. - -%% @private -handle_call(get_pid, _From, #state{pid = Pid} = State) -> - {reply, {ok, Pid}, State}; -handle_call(_Request, _From, State) -> - Reply = ok, - {reply, Reply, State}. - -%% @private -handle_cast(_Msg, State) -> - {noreply, State}. - -%% @private -handle_info({'DOWN', _MonitorRef, _Type, _Object, _Info}, State) -> - {stop, normal, State}; -handle_info(_Info, State) -> - ?ERROR_MSG("unexpected info: ~p", [_Info]), - {noreply, State}. - -%% @private -terminate(_Reason, _State) -> - ok. - -%% @private -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -%%%=================================================================== -%%% Internal functions -%%%=================================================================== -encode_index_key(Idx, Key) when is_integer(Key) -> - {<>, Key}; -encode_index_key(Idx, Key) -> - {<>, encode_key(Key)}. - -encode_key(Bin) when is_binary(Bin) -> - <<"b_", Bin/binary>>; -encode_key(Int) when is_integer(Int) -> - <<"i_", (list_to_binary(integer_to_list(Int)))/binary>>; -encode_key(Term) -> - erlang:term_to_binary(Term). - -log_error({error, notfound}, _, _) -> - ok; -log_error({error, Why} = Err, Function, Opts) -> - Txt = lists:map( - fun({table, Table}) -> - io_lib:fwrite("** Table: ~p~n", [Table]); - ({key, Key}) -> - io_lib:fwrite("** Key: ~p~n", [Key]); - ({index, Index}) -> - io_lib:fwrite("** Index = ~p~n", [Index]); - ({start_key, Key}) -> - io_lib:fwrite("** Start Key: ~p~n", [Key]); - ({end_key, Key}) -> - io_lib:fwrite("** End Key: ~p~n", [Key]); - ({record, Rec}) -> - io_lib:fwrite("** Record = ~p~n", [Rec]); - ({index_info, IdxInfo}) -> - io_lib:fwrite("** Index info = ~p~n", [IdxInfo]); - (_) -> - "" - end, Opts), - ErrTxt = if is_binary(Why) -> - io_lib:fwrite("** Error: ~s", [Why]); - true -> - io_lib:fwrite("** Error: ~p", [Err]) - end, - ?ERROR_MSG("database error:~n** Function: ~p~n~s~s", - [Function, Txt, ErrTxt]); -log_error(_, _, _) -> - ok. - -make_invalid_object(Val) -> - list_to_binary(io_lib:fwrite("Invalid object: ~p", [Val])). - -get_random_pid() -> - PoolPid = ejabberd_riak_sup:get_random_pid(), - case catch gen_server:call(PoolPid, get_pid) of - {ok, Pid} -> - Pid; - {'EXIT', {timeout, _}} -> - throw({error, timeout}); - {'EXIT', Err} -> - throw({error, Err}) - end. - -encode_record(Rec, {Fields, DefRec}) -> - term_to_binary(encode_record(Rec, Fields, DefRec, 2)). - -encode_record(Rec, [FieldName|Fields], DefRec, Pos) -> - Value = element(Pos, Rec), - DefValue = element(Pos, DefRec), - if Value == DefValue -> - encode_record(Rec, Fields, DefRec, Pos+1); - true -> - [{FieldName, Value}|encode_record(Rec, Fields, DefRec, Pos+1)] - end; -encode_record(_, [], _, _) -> - []. - -decode_record(Bin, {Fields, DefRec}) -> - decode_record(binary_to_term(Bin), Fields, DefRec, 2). - -decode_record(KeyVals, [FieldName|Fields], Rec, Pos) -> - case lists:keyfind(FieldName, 1, KeyVals) of - {_, Value} -> - NewRec = setelement(Pos, Rec, Value), - decode_record(KeyVals, Fields, NewRec, Pos+1); - false -> - decode_record(KeyVals, Fields, Rec, Pos+1) - end; -decode_record(_, [], Rec, _) -> - Rec. diff --git a/src/ejabberd_riak_sup.erl b/src/ejabberd_riak_sup.erl deleted file mode 100644 index 9711e6652..000000000 --- a/src/ejabberd_riak_sup.erl +++ /dev/null @@ -1,161 +0,0 @@ -%%%---------------------------------------------------------------------- -%%% File : ejabberd_riak_sup.erl -%%% Author : Alexey Shchepin -%%% Purpose : Riak connections supervisor -%%% Created : 29 Dec 2011 by Alexey Shchepin -%%% -%%% -%%% ejabberd, Copyright (C) 2002-2015 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., 59 Temple Place, Suite 330, Boston, MA -%%% 02111-1307 USA -%%% -%%%---------------------------------------------------------------------- - --module(ejabberd_riak_sup). --author('alexey@process-one.net'). - -%% API --export([start/0, - start_link/0, - init/1, - get_pids/0, - transform_options/1, - get_random_pid/0, - get_random_pid/1 - ]). - --include("ejabberd.hrl"). --include("logger.hrl"). - --define(DEFAULT_POOL_SIZE, 10). --define(DEFAULT_RIAK_START_INTERVAL, 30). % 30 seconds --define(DEFAULT_RIAK_HOST, "127.0.0.1"). --define(DEFAULT_RIAK_PORT, 8087). - -% time to wait for the supervisor to start its child before returning -% a timeout error to the request --define(CONNECT_TIMEOUT, 500). % milliseconds - -start() -> - case lists:any( - fun(Host) -> - is_riak_configured(Host) - end, ?MYHOSTS) of - true -> - ejabberd:start_app(riakc), - do_start(); - false -> - ok - end. - -is_riak_configured(Host) -> - ServerConfigured = ejabberd_config:get_option( - {riak_server, Host}, - fun(_) -> true end, false), - PortConfigured = ejabberd_config:get_option( - {riak_port, Host}, - fun(_) -> true end, false), - AuthConfigured = lists:member( - ejabberd_auth_riak, - ejabberd_auth:auth_modules(Host)), - Modules = ejabberd_config:get_option( - {modules, Host}, - fun(L) when is_list(L) -> L end, []), - ModuleWithRiakDBConfigured = lists:any( - fun({_Module, Opts}) -> - gen_mod:db_type(Opts) == riak - end, Modules), - ServerConfigured or PortConfigured - or AuthConfigured or ModuleWithRiakDBConfigured. - -do_start() -> - SupervisorName = ?MODULE, - ChildSpec = - {SupervisorName, - {?MODULE, start_link, []}, - transient, - infinity, - supervisor, - [?MODULE]}, - case supervisor:start_child(ejabberd_sup, ChildSpec) of - {ok, _PID} -> - ok; - _Error -> - ?ERROR_MSG("Start of supervisor ~p failed:~n~p~nRetrying...~n", - [SupervisorName, _Error]), - timer:sleep(5000), - start() - end. - -start_link() -> - supervisor:start_link({local, ?MODULE}, ?MODULE, []). - -init([]) -> - PoolSize = get_pool_size(), - StartInterval = get_start_interval(), - Server = get_riak_server(), - Port = get_riak_port(), - {ok, {{one_for_one, PoolSize*10, 1}, - lists:map( - fun(I) -> - {ejabberd_riak:get_proc(I), - {ejabberd_riak, start_link, - [I, Server, Port, StartInterval*1000]}, - transient, 2000, worker, [?MODULE]} - end, lists:seq(1, PoolSize))}}. - -get_start_interval() -> - ejabberd_config:get_option( - riak_start_interval, - fun(N) when is_integer(N), N >= 1 -> N end, - ?DEFAULT_RIAK_START_INTERVAL). - -get_pool_size() -> - ejabberd_config:get_option( - riak_pool_size, - fun(N) when is_integer(N), N >= 1 -> N end, - ?DEFAULT_POOL_SIZE). - -get_riak_server() -> - ejabberd_config:get_option( - riak_server, - fun(S) -> - binary_to_list(iolist_to_binary(S)) - end, ?DEFAULT_RIAK_HOST). - -get_riak_port() -> - ejabberd_config:get_option( - riak_port, - fun(P) when is_integer(P), P > 0, P < 65536 -> P end, - ?DEFAULT_RIAK_PORT). - -get_pids() -> - [ejabberd_riak:get_proc(I) || I <- lists:seq(1, get_pool_size())]. - -get_random_pid() -> - get_random_pid(now()). - -get_random_pid(Term) -> - I = erlang:phash2(Term, get_pool_size()) + 1, - ejabberd_riak:get_proc(I). - -transform_options(Opts) -> - lists:foldl(fun transform_options/2, [], Opts). - -transform_options({riak_server, {S, P}}, Opts) -> - [{riak_server, S}, {riak_port, P}|Opts]; -transform_options(Opt, Opts) -> - [Opt|Opts]. diff --git a/src/ejabberd_router.erl b/src/ejabberd_router.erl index 76ef71dc4..236b5081a 100644 --- a/src/ejabberd_router.erl +++ b/src/ejabberd_router.erl @@ -5,7 +5,7 @@ %%% Created : 27 Nov 2002 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -27,382 +27,529 @@ -author('alexey@process-one.net'). --behaviour(gen_server). +-ifndef(GEN_SERVER). +-define(GEN_SERVER, gen_server). +-endif. +-behaviour(?GEN_SERVER). %% API --export([route/3, - route_error/4, - register_route/1, +-export([route/1, + route_error/2, + route_iq/2, + route_iq/3, + route_iq/4, register_route/2, + register_route/3, + register_route/4, register_routes/1, + host_of_route/1, + process_iq/1, unregister_route/1, + unregister_route/2, unregister_routes/1, - dirty_get_all_routes/0, - dirty_get_all_domains/0 - ]). + get_all_routes/0, + is_my_route/1, + is_my_host/1, + clean_cache/1, + config_reloaded/0, + get_backend/0]). -export([start_link/0]). -%% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --include("ejabberd.hrl"). +%% Deprecated functions +-export([route/3, route_error/4]). +-deprecated([{route, 3}, {route_error, 4}]). + +%% This value is used in SIP and Megaco for a transaction lifetime. +-define(IQ_TIMEOUT, 32000). +-define(CALL_TIMEOUT, timer:minutes(10)). + -include("logger.hrl"). +-include("ejabberd_router.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). --include("jlib.hrl"). --type local_hint() :: undefined | integer() | {apply, atom(), atom()}. --record(route, {domain, pid, local_hint}). +-callback init() -> any(). +-callback register_route(binary(), binary(), local_hint(), + undefined | pos_integer(), pid()) -> ok | {error, term()}. +-callback unregister_route(binary(), undefined | pos_integer(), pid()) -> ok | {error, term()}. +-callback find_routes(binary()) -> {ok, [#route{}]} | {error, any()}. +-callback get_all_routes() -> {ok, [binary()]} | {error, any()}. --record(state, {}). +-record(state, {route_monitors = #{} :: #{{binary(), pid()} => reference()}}). %%==================================================================== %% API %%==================================================================== -%%-------------------------------------------------------------------- -%% Function: start_link() -> {ok,Pid} | ignore | {error,Error} -%% Description: Starts the server -%%-------------------------------------------------------------------- start_link() -> - gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + ?GEN_SERVER:start_link({local, ?MODULE}, ?MODULE, [], []). --spec route(jid(), jid(), xmlel()) -> ok. +-spec route(stanza()) -> ok. +route(Packet) -> + try do_route(Packet) + catch + Class:Reason:StackTrace -> + ?ERROR_MSG("Failed to route packet:~n~ts~n** ~ts", + [xmpp:pp(Packet), + misc:format_exception(2, Class, Reason, StackTrace)]) + end. -route(From, To, Packet) -> - case catch do_route(From, To, Packet) of - {'EXIT', Reason} -> - ?ERROR_MSG("~p~nwhen processing: ~p", - [Reason, {From, To, Packet}]); - _ -> - ok +-spec route(jid(), jid(), xmlel() | stanza()) -> ok. +route(#jid{} = From, #jid{} = To, #xmlel{} = El) -> + try xmpp:decode(El, ?NS_CLIENT, [ignore_els]) of + Pkt -> route(From, To, Pkt) + catch _:{xmpp_codec, Why} -> + ?ERROR_MSG("Failed to decode xml element ~p when " + "routing from ~ts to ~ts: ~ts", + [El, jid:encode(From), jid:encode(To), + xmpp:format_error(Why)]) + end; +route(#jid{} = From, #jid{} = To, Packet) -> + route(xmpp:set_from_to(Packet, From, To)). + +-spec route_error(stanza(), stanza_error()) -> ok. +route_error(Packet, Err) -> + Type = xmpp:get_type(Packet), + if Type == error; Type == result -> + ok; + true -> + route(xmpp:make_error(Packet, Err)) end. %% Route the error packet only if the originating packet is not an error itself. %% RFC3920 9.3.1 --spec route_error(jid(), jid(), xmlel(), xmlel()) -> ok. - -route_error(From, To, ErrPacket, OrigPacket) -> +-spec route_error(jid(), jid(), xmlel(), xmlel()) -> ok; + (jid(), jid(), stanza(), stanza_error()) -> ok. +route_error(From, To, #xmlel{} = ErrPacket, #xmlel{} = OrigPacket) -> #xmlel{attrs = Attrs} = OrigPacket, - case <<"error">> == xml:get_attr_s(<<"type">>, Attrs) of + case <<"error">> == fxml:get_attr_s(<<"type">>, Attrs) of false -> route(From, To, ErrPacket); true -> ok + end; +route_error(From, To, Packet, #stanza_error{} = Err) -> + Type = xmpp:get_type(Packet), + if Type == error; Type == result -> + ok; + true -> + route(From, To, xmpp:make_error(Packet, Err)) end. --spec register_route(binary()) -> term(). +-spec route_iq(iq(), fun((iq() | timeout) -> any())) -> ok. +route_iq(IQ, Fun) -> + route_iq(IQ, Fun, undefined, ?IQ_TIMEOUT). -register_route(Domain) -> - register_route(Domain, undefined). +-spec route_iq(iq(), term(), pid() | atom()) -> ok. +route_iq(IQ, State, Proc) -> + route_iq(IQ, State, Proc, ?IQ_TIMEOUT). --spec register_route(binary(), local_hint()) -> term(). +-spec route_iq(iq(), term(), pid() | atom(), undefined | non_neg_integer()) -> ok. +route_iq(IQ, State, Proc, undefined) -> + route_iq(IQ, State, Proc, ?IQ_TIMEOUT); +route_iq(IQ, State, Proc, Timeout) -> + ejabberd_iq:route(IQ, Proc, State, Timeout). -register_route(Domain, LocalHint) -> - case jlib:nameprep(Domain) of - error -> erlang:error({invalid_domain, Domain}); - LDomain -> - Pid = self(), - case get_component_number(LDomain) of - undefined -> - F = fun () -> - mnesia:write(#route{domain = LDomain, pid = Pid, - local_hint = LocalHint}) - end, - mnesia:transaction(F); - N -> - F = fun () -> - case mnesia:wread({route, LDomain}) of - [] -> - mnesia:write(#route{domain = LDomain, - pid = Pid, - local_hint = 1}), - lists:foreach(fun (I) -> - mnesia:write(#route{domain - = - LDomain, - pid - = - undefined, - local_hint - = - I}) - end, - lists:seq(2, N)); - Rs -> - lists:any(fun (#route{pid = undefined, - local_hint = I} = - R) -> - mnesia:write(#route{domain = - LDomain, - pid = - Pid, - local_hint - = - I}), - mnesia:delete_object(R), - true; - (_) -> false - end, - Rs) - end - end, - mnesia:transaction(F) - end +-spec register_route(binary(), binary()) -> ok. +register_route(Domain, ServerHost) -> + register_route(Domain, ServerHost, undefined). + +-spec register_route(binary(), binary(), local_hint() | undefined) -> ok. +register_route(Domain, ServerHost, LocalHint) -> + register_route(Domain, ServerHost, LocalHint, self()). + +-spec register_route(binary(), binary(), local_hint() | undefined, pid()) -> ok. +register_route(Domain, ServerHost, LocalHint, Pid) -> + case {jid:nameprep(Domain), jid:nameprep(ServerHost)} of + {error, _} -> + erlang:error({invalid_domain, Domain}); + {_, error} -> + erlang:error({invalid_domain, ServerHost}); + {LDomain, LServerHost} -> + Mod = get_backend(), + case Mod:register_route(LDomain, LServerHost, LocalHint, + get_component_number(LDomain), Pid) of + ok -> + ?DEBUG("Route registered: ~ts", [LDomain]), + monitor_route(LDomain, Pid), + ejabberd_hooks:run(route_registered, [LDomain]), + delete_cache(Mod, LDomain); + {error, Err} -> + ?ERROR_MSG("Failed to register route ~ts: ~p", + [LDomain, Err]) + end end. --spec register_routes([binary()]) -> ok. - +-spec register_routes([{binary(), binary()}]) -> ok. register_routes(Domains) -> - lists:foreach(fun (Domain) -> register_route(Domain) + lists:foreach(fun ({Domain, ServerHost}) -> register_route(Domain, ServerHost) end, Domains). --spec unregister_route(binary()) -> term(). - +-spec unregister_route(binary()) -> ok. unregister_route(Domain) -> - case jlib:nameprep(Domain) of - error -> erlang:error({invalid_domain, Domain}); - LDomain -> - Pid = self(), - case get_component_number(LDomain) of - undefined -> - F = fun () -> - case mnesia:match_object(#route{domain = LDomain, - pid = Pid, _ = '_'}) - of - [R] -> mnesia:delete_object(R); - _ -> ok - end - end, - mnesia:transaction(F); - _ -> - F = fun () -> - case mnesia:match_object(#route{domain = LDomain, - pid = Pid, _ = '_'}) - of - [R] -> - I = R#route.local_hint, - mnesia:write(#route{domain = LDomain, - pid = undefined, - local_hint = I}), - mnesia:delete_object(R); - _ -> ok - end - end, - mnesia:transaction(F) - end + unregister_route(Domain, self()). + +-spec unregister_route(binary(), pid()) -> ok. +unregister_route(Domain, Pid) -> + case jid:nameprep(Domain) of + error -> + erlang:error({invalid_domain, Domain}); + LDomain -> + Mod = get_backend(), + case Mod:unregister_route( + LDomain, get_component_number(LDomain), Pid) of + ok -> + ?DEBUG("Route unregistered: ~ts", [LDomain]), + demonitor_route(LDomain, Pid), + ejabberd_hooks:run(route_unregistered, [LDomain]), + delete_cache(Mod, LDomain); + {error, Err} -> + ?ERROR_MSG("Failed to unregister route ~ts: ~p", + [LDomain, Err]) + end end. -spec unregister_routes([binary()]) -> ok. - unregister_routes(Domains) -> lists:foreach(fun (Domain) -> unregister_route(Domain) end, Domains). --spec dirty_get_all_routes() -> [binary()]. +-spec find_routes(binary()) -> [#route{}]. +find_routes(Domain) -> + Mod = get_backend(), + case use_cache(Mod) of + true -> + case ets_cache:lookup( + ?ROUTES_CACHE, {route, Domain}, + fun() -> + case Mod:find_routes(Domain) of + {ok, Rs} when Rs /= [] -> + {ok, Rs}; + _ -> + error + end + end) of + {ok, Rs} -> Rs; + error -> [] + end; + false -> + case Mod:find_routes(Domain) of + {ok, Rs} -> Rs; + _ -> [] + end + end. -dirty_get_all_routes() -> - lists:usort(mnesia:dirty_all_keys(route)) -- (?MYHOSTS). +-spec get_all_routes() -> [binary()]. +get_all_routes() -> + Mod = get_backend(), + case use_cache(Mod) of + true -> + case ets_cache:lookup( + ?ROUTES_CACHE, routes, + fun() -> + case Mod:get_all_routes() of + {ok, Rs} when Rs /= [] -> + {ok, Rs}; + _ -> + error + end + end) of + {ok, Rs} -> Rs; + error -> [] + end; + false -> + case Mod:get_all_routes() of + {ok, Rs} -> Rs; + _ -> [] + end + end. --spec dirty_get_all_domains() -> [binary()]. +-spec host_of_route(binary()) -> binary(). +host_of_route(Domain) -> + case jid:nameprep(Domain) of + error -> + erlang:error({invalid_domain, Domain}); + LDomain -> + case find_routes(LDomain) of + [#route{server_host = ServerHost}|_] -> + ServerHost; + _ -> + erlang:error({unregistered_route, Domain}) + end + end. -dirty_get_all_domains() -> - lists:usort(mnesia:dirty_all_keys(route)). +-spec is_my_route(binary()) -> boolean(). +is_my_route(Domain) -> + case jid:nameprep(Domain) of + error -> + erlang:error({invalid_domain, Domain}); + LDomain -> + find_routes(LDomain) /= [] + end. +-spec is_my_host(binary()) -> boolean(). +is_my_host(Domain) -> + case jid:nameprep(Domain) of + error -> + erlang:error({invalid_domain, Domain}); + LDomain -> + case find_routes(LDomain) of + [#route{server_host = LDomain}|_] -> true; + _ -> false + end + end. + +-spec process_iq(iq()) -> any(). +process_iq(IQ) -> + gen_iq_handler:handle(IQ). + +-spec config_reloaded() -> ok. +config_reloaded() -> + Mod = get_backend(), + init_cache(Mod). %%==================================================================== %% gen_server callbacks %%==================================================================== - -%%-------------------------------------------------------------------- -%% Function: init(Args) -> {ok, State} | -%% {ok, State, Timeout} | -%% ignore | -%% {stop, Reason} -%% Description: Initiates the server -%%-------------------------------------------------------------------- init([]) -> - update_tables(), - mnesia:create_table(route, - [{ram_copies, [node()]}, {type, bag}, - {attributes, record_info(fields, route)}]), - mnesia:add_table_copy(route, node(), ram_copies), - mnesia:subscribe({table, route, simple}), - lists:foreach(fun (Pid) -> erlang:monitor(process, Pid) - end, - mnesia:dirty_select(route, - [{{route, '_', '$1', '_'}, [], ['$1']}])), + ejabberd_hooks:add(config_reloaded, ?MODULE, config_reloaded, 50), + Mod = get_backend(), + init_cache(Mod), + Mod:init(), + clean_cache(), {ok, #state{}}. -%%-------------------------------------------------------------------- -%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | -%% {reply, Reply, State, Timeout} | -%% {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, Reply, State} | -%% {stop, Reason, State} -%% Description: Handling call messages -%%-------------------------------------------------------------------- -handle_call(_Request, _From, State) -> - Reply = ok, {reply, Reply, State}. - -%%-------------------------------------------------------------------- -%% Function: handle_cast(Msg, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} -%% Description: Handling cast messages -%%-------------------------------------------------------------------- -handle_cast(_Msg, State) -> {noreply, State}. - -%%-------------------------------------------------------------------- -%% Function: handle_info(Info, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} -%% Description: Handling all non call/cast messages -%%-------------------------------------------------------------------- -handle_info({route, From, To, Packet}, State) -> - case catch do_route(From, To, Packet) of - {'EXIT', Reason} -> - ?ERROR_MSG("~p~nwhen processing: ~p", - [Reason, {From, To, Packet}]); - _ -> ok - end, - {noreply, State}; -handle_info({mnesia_table_event, - {write, #route{pid = Pid}, _ActivityId}}, - State) -> - erlang:monitor(process, Pid), {noreply, State}; -handle_info({'DOWN', _Ref, _Type, Pid, _Info}, State) -> - F = fun () -> - Es = mnesia:select(route, - [{#route{pid = Pid, _ = '_'}, [], ['$_']}]), - lists:foreach(fun (E) -> - if is_integer(E#route.local_hint) -> - LDomain = E#route.domain, - I = E#route.local_hint, - mnesia:write(#route{domain = - LDomain, - pid = - undefined, - local_hint = - I}), - mnesia:delete_object(E); - true -> mnesia:delete_object(E) - end - end, - Es) - end, - mnesia:transaction(F), - {noreply, State}; -handle_info(_Info, State) -> +handle_call({monitor, Domain, Pid}, _From, State) -> + MRefs = State#state.route_monitors, + MRefs1 = case maps:is_key({Domain, Pid}, MRefs) of + true -> MRefs; + false -> + MRef = erlang:monitor(process, Pid), + MRefs#{{Domain, Pid} => MRef} + end, + {reply, ok, State#state{route_monitors = MRefs1}}; +handle_call({demonitor, Domain, Pid}, _From, State) -> + MRefs = State#state.route_monitors, + MRefs1 = case maps:find({Domain, Pid}, MRefs) of + {ok, MRef} -> + erlang:demonitor(MRef, [flush]), + maps:remove({Domain, Pid}, MRefs); + error -> + MRefs + end, + {reply, ok, State#state{route_monitors = MRefs1}}; +handle_call(Request, From, State) -> + ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. -%%-------------------------------------------------------------------- -%% Function: terminate(Reason, State) -> void() -%% Description: This function is called by a gen_server when it is about to -%% terminate. It should be the opposite of Module:init/1 and do any necessary -%% cleaning up. When it returns, the gen_server terminates with Reason. -%% The return value is ignored. -%%-------------------------------------------------------------------- -terminate(_Reason, _State) -> - ok. +handle_cast(Msg, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Msg]), + {noreply, State}. + +handle_info({route, Packet}, State) -> + route(Packet), + {noreply, State}; +handle_info({'DOWN', MRef, _, Pid, Info}, State) -> + MRefs = maps:filter( + fun({Domain, P}, M) when P == Pid, M == MRef -> + ?DEBUG("Process ~p with route registered to ~ts " + "has terminated unexpectedly with reason: ~p", + [P, Domain, Info]), + try unregister_route(Domain, Pid) + catch _:_ -> ok + end, + false; + (_, _) -> + true + end, State#state.route_monitors), + {noreply, State#state{route_monitors = MRefs}}; +handle_info(Info, State) -> + ?ERROR_MSG("Unexpected info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, _State) -> + ejabberd_hooks:delete(config_reloaded, ?MODULE, config_reloaded, 50). -%%-------------------------------------------------------------------- -%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} -%% Description: Convert process state when code is changed -%%-------------------------------------------------------------------- code_change(_OldVsn, State, _Extra) -> {ok, State}. %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- -do_route(OrigFrom, OrigTo, OrigPacket) -> - ?DEBUG("route~n\tfrom ~p~n\tto ~p~n\tpacket " - "~p~n", - [OrigFrom, OrigTo, OrigPacket]), - case ejabberd_hooks:run_fold(filter_packet, - {OrigFrom, OrigTo, OrigPacket}, []) - of - {From, To, Packet} -> - LDstDomain = To#jid.lserver, - case mnesia:dirty_read(route, LDstDomain) of - [] -> ejabberd_s2s:route(From, To, Packet); - [R] -> - Pid = R#route.pid, - if node(Pid) == node() -> - case R#route.local_hint of - {apply, Module, Function} -> - Module:Function(From, To, Packet); - _ -> Pid ! {route, From, To, Packet} - end; - is_pid(Pid) -> Pid ! {route, From, To, Packet}; - true -> drop - end; - Rs -> - Value = case - ejabberd_config:get_local_option({domain_balancing, - LDstDomain}, fun(D) when is_atom(D) -> D end) - of - undefined -> now(); - random -> now(); - source -> jlib:jid_tolower(From); - destination -> jlib:jid_tolower(To); - bare_source -> - jlib:jid_remove_resource(jlib:jid_tolower(From)); - bare_destination -> - jlib:jid_remove_resource(jlib:jid_tolower(To)) - end, - case get_component_number(LDstDomain) of - undefined -> - case [R || R <- Rs, node(R#route.pid) == node()] of +-spec do_route(stanza()) -> ok. +do_route(OrigPacket1) -> + ?DEBUG("Route:~n~ts", [xmpp:pp(OrigPacket1)]), + OrigPacket = process_privilege_iq(OrigPacket1), + case ejabberd_hooks:run_fold(filter_packet, OrigPacket, []) of + drop -> + ok; + Packet -> + case ejabberd_iq:dispatch(Packet) of + true -> + ok; + false -> + To = xmpp:get_to(Packet), + LDstDomain = To#jid.lserver, + case find_routes(LDstDomain) of [] -> - R = lists:nth(erlang:phash(Value, length(Rs)), Rs), - Pid = R#route.pid, - if is_pid(Pid) -> Pid ! {route, From, To, Packet}; - true -> drop - end; - LRs -> - R = lists:nth(erlang:phash(Value, length(LRs)), - LRs), - Pid = R#route.pid, - case R#route.local_hint of - {apply, Module, Function} -> - Module:Function(From, To, Packet); - _ -> Pid ! {route, From, To, Packet} - end - end; - _ -> - SRs = lists:ukeysort(#route.local_hint, Rs), - R = lists:nth(erlang:phash(Value, length(SRs)), SRs), - Pid = R#route.pid, - if is_pid(Pid) -> Pid ! {route, From, To, Packet}; - true -> drop - end - end - end; - drop -> ok + ejabberd_s2s:route(Packet); + [Route] -> + do_route(Packet, Route); + Routes -> + From = xmpp:get_from(Packet), + balancing_route(From, To, Packet, Routes) + end, + ok + end end. +%% @format-begin +process_privilege_iq(Packet) -> + Type = xmpp:get_type(Packet), + case xmpp:get_meta(Packet, privilege_iq, none) of + {OriginalId, OriginalHost, ReplacedJid} when (Type == result) or (Type == error) -> + Privilege = #privilege{forwarded = #forwarded{sub_els = [Packet]}}, + #iq{type = xmpp:get_type(Packet), + id = OriginalId, + to = jid:make(OriginalHost), + from = ReplacedJid, + sub_els = [Privilege]}; + _ -> + Packet + end. +%% @format-end + +-spec do_route(stanza(), #route{}) -> any(). +do_route(Pkt, #route{local_hint = LocalHint, + pid = Pid}) when is_pid(Pid) -> + case LocalHint of + {apply, Module, Function} when node(Pid) == node() -> + Module:Function(Pkt); + _ -> + ejabberd_cluster:send(Pid, {route, Pkt}) + end; +do_route(_Pkt, _Route) -> + ok. + +-spec balancing_route(jid(), jid(), stanza(), [#route{}]) -> any(). +balancing_route(From, To, Packet, Rs) -> + case get_domain_balancing(From, To, To#jid.lserver) of + undefined -> + Value = erlang:system_time(), + case [R || R <- Rs, node(R#route.pid) == node()] of + [] -> + R = lists:nth(erlang:phash2(Value, length(Rs))+1, Rs), + do_route(Packet, R); + LRs -> + R = lists:nth(erlang:phash2(Value, length(LRs))+1, LRs), + do_route(Packet, R) + end; + Value -> + SRs = lists:ukeysort(#route.local_hint, Rs), + R = lists:nth(erlang:phash2(Value, length(SRs))+1, SRs), + do_route(Packet, R) + end. + +-spec get_component_number(binary()) -> pos_integer() | undefined. get_component_number(LDomain) -> - ejabberd_config:get_option( - {domain_balancing_component_number, LDomain}, - fun(N) when is_integer(N), N > 1 -> N end, - undefined). - -update_tables() -> - case catch mnesia:table_info(route, attributes) of - [domain, node, pid] -> mnesia:delete_table(route); - [domain, pid] -> mnesia:delete_table(route); - [domain, pid, local_hint] -> ok; - [domain, pid, local_hint|_] -> mnesia:delete_table(route); - {'EXIT', _} -> ok - end, - case lists:member(local_route, - mnesia:system_info(tables)) - of - true -> mnesia:delete_table(local_route); - false -> ok + M = ejabberd_option:domain_balancing(), + case maps:get(LDomain, M, undefined) of + undefined -> undefined; + Opts -> maps:get(component_number, Opts) end. +-spec get_domain_balancing(jid(), jid(), binary()) -> integer() | ljid() | undefined. +get_domain_balancing(From, To, LDomain) -> + M = ejabberd_option:domain_balancing(), + case maps:get(LDomain, M, undefined) of + undefined -> undefined; + Opts -> + case maps:get(type, Opts, random) of + random -> erlang:system_time(); + source -> jid:tolower(From); + destination -> jid:tolower(To); + bare_source -> jid:remove_resource(jid:tolower(From)); + bare_destination -> jid:remove_resource(jid:tolower(To)) + end + end. + +-spec monitor_route(binary(), pid()) -> ok. +monitor_route(Domain, Pid) -> + ?GEN_SERVER:call(?MODULE, {monitor, Domain, Pid}, ?CALL_TIMEOUT). + +-spec demonitor_route(binary(), pid()) -> ok. +demonitor_route(Domain, Pid) -> + case whereis(?MODULE) == self() of + true -> + ok; + false -> + ?GEN_SERVER:call(?MODULE, {demonitor, Domain, Pid}, ?CALL_TIMEOUT) + end. + +-spec get_backend() -> module(). +get_backend() -> + DBType = ejabberd_option:router_db_type(), + list_to_existing_atom("ejabberd_router_" ++ atom_to_list(DBType)). + +-spec cache_nodes(module()) -> [node()]. +cache_nodes(Mod) -> + case erlang:function_exported(Mod, cache_nodes, 0) of + true -> Mod:cache_nodes(); + false -> ejabberd_cluster:get_nodes() + end. + +-spec use_cache(module()) -> boolean(). +use_cache(Mod) -> + case erlang:function_exported(Mod, use_cache, 0) of + true -> Mod:use_cache(); + false -> ejabberd_option:router_use_cache() + end. + +-spec delete_cache(module(), binary()) -> ok. +delete_cache(Mod, Domain) -> + case use_cache(Mod) of + true -> + ets_cache:delete(?ROUTES_CACHE, {route, Domain}, cache_nodes(Mod)), + ets_cache:delete(?ROUTES_CACHE, routes, cache_nodes(Mod)); + false -> + ok + end. + +-spec init_cache(module()) -> ok. +init_cache(Mod) -> + case use_cache(Mod) of + true -> + ets_cache:new(?ROUTES_CACHE, cache_opts()); + false -> + ets_cache:delete(?ROUTES_CACHE) + end. + +-spec cache_opts() -> [proplists:property()]. +cache_opts() -> + MaxSize = ejabberd_option:router_cache_size(), + CacheMissed = ejabberd_option:router_cache_missed(), + LifeTime = ejabberd_option:router_cache_life_time(), + [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}]. + +-spec clean_cache(node()) -> non_neg_integer(). +clean_cache(Node) -> + ets_cache:filter( + ?ROUTES_CACHE, + fun(_, error) -> + false; + (routes, _) -> + false; + ({route, _}, {ok, Rs}) -> + not lists:any( + fun(#route{pid = Pid}) -> + node(Pid) == Node + end, Rs) + end). + +-spec clean_cache() -> ok. +clean_cache() -> + ejabberd_cluster:eval_everywhere(?MODULE, clean_cache, [node()]). diff --git a/src/ejabberd_router_mnesia.erl b/src/ejabberd_router_mnesia.erl new file mode 100644 index 000000000..66ae02208 --- /dev/null +++ b/src/ejabberd_router_mnesia.erl @@ -0,0 +1,236 @@ +%%%------------------------------------------------------------------- +%%% Created : 11 Jan 2017 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(ejabberd_router_mnesia). +-behaviour(ejabberd_router). +-behaviour(gen_server). + +%% API +-export([init/0, register_route/5, unregister_route/3, find_routes/1, + get_all_routes/0, use_cache/0]). +%% gen_server callbacks +-export([init/1, handle_cast/2, handle_call/3, handle_info/2, + terminate/2, code_change/3, start_link/0]). + +-include("ejabberd_router.hrl"). +-include("logger.hrl"). +-include_lib("stdlib/include/ms_transform.hrl"). + +-record(state, {}). + +%%%=================================================================== +%%% API +%%%=================================================================== +-spec init() -> ok | {error, any()}. +init() -> + Spec = {?MODULE, {?MODULE, start_link, []}, + transient, 5000, worker, [?MODULE]}, + case supervisor:start_child(ejabberd_backend_sup, Spec) of + {ok, _Pid} -> ok; + Err -> Err + end. + +-spec start_link() -> {ok, pid()} | {error, any()}. +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +use_cache() -> + false. + +register_route(Domain, ServerHost, LocalHint, undefined, Pid) -> + F = fun () -> + mnesia:write(#route{domain = Domain, + pid = Pid, + server_host = ServerHost, + local_hint = LocalHint}) + end, + transaction(F); +register_route(Domain, ServerHost, _LocalHint, N, Pid) -> + F = fun () -> + case mnesia:wread({route, Domain}) of + [] -> + mnesia:write(#route{domain = Domain, + server_host = ServerHost, + pid = Pid, + local_hint = 1}), + lists:foreach( + fun (I) -> + mnesia:write( + #route{domain = Domain, + pid = undefined, + server_host = ServerHost, + local_hint = I}) + end, + lists:seq(2, N)); + Rs -> + lists:any( + fun (#route{pid = undefined, + local_hint = I} = R) -> + mnesia:write( + #route{domain = Domain, + pid = Pid, + server_host = ServerHost, + local_hint = I}), + mnesia:delete_object(R), + true; + (_) -> false + end, + Rs) + end + end, + transaction(F). + +unregister_route(Domain, undefined, Pid) -> + F = fun () -> + case mnesia:select( + route, + ets:fun2ms( + fun(#route{domain = D, pid = P} = R) + when D == Domain, P == Pid -> R + end)) of + [R] -> mnesia:delete_object(R); + _ -> ok + end + end, + transaction(F); +unregister_route(Domain, _, Pid) -> + F = fun () -> + case mnesia:select( + route, + ets:fun2ms( + fun(#route{domain = D, pid = P} = R) + when D == Domain, P == Pid -> R + end)) of + [R] -> + I = R#route.local_hint, + ServerHost = R#route.server_host, + mnesia:write(#route{domain = Domain, + server_host = ServerHost, + pid = undefined, + local_hint = I}), + mnesia:delete_object(R); + _ -> ok + end + end, + transaction(F). + +find_routes(Domain) -> + {ok, mnesia:dirty_read(route, Domain)}. + +get_all_routes() -> + {ok, mnesia:dirty_select( + route, + ets:fun2ms( + fun(#route{domain = Domain, server_host = ServerHost}) + when Domain /= ServerHost -> Domain + end))}. + +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== +init([]) -> + update_tables(), + ejabberd_mnesia:create(?MODULE, route, + [{ram_copies, [node()]}, + {type, bag}, + {attributes, record_info(fields, route)}]), + mnesia:subscribe({table, route, simple}), + lists:foreach( + fun (Pid) -> erlang:monitor(process, Pid) end, + mnesia:dirty_select( + route, + ets:fun2ms( + fun(#route{pid = Pid}) -> Pid end))), + {ok, #state{}}. + +handle_call(Request, From, State) -> + ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), + {noreply, State}. + +handle_cast(Msg, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Msg]), + {noreply, State}. + +handle_info({mnesia_table_event, + {write, #route{pid = Pid}, _ActivityId}}, State) -> + erlang:monitor(process, Pid), + {noreply, State}; +handle_info({mnesia_table_event, _}, State) -> + {noreply, State}; +handle_info({'DOWN', _Ref, _Type, Pid, _Info}, State) -> + F = fun () -> + Es = mnesia:select( + route, + ets:fun2ms( + fun(#route{pid = P} = E) + when P == Pid -> E + end)), + lists:foreach( + fun(E) -> + if is_integer(E#route.local_hint) -> + LDomain = E#route.domain, + I = E#route.local_hint, + ServerHost = E#route.server_host, + mnesia:write(#route{domain = LDomain, + server_host = ServerHost, + pid = undefined, + local_hint = I}), + mnesia:delete_object(E); + true -> + mnesia:delete_object(E) + end + end, Es) + end, + transaction(F), + {noreply, State}; +handle_info(Info, State) -> + ?ERROR_MSG("Unexpected info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +transaction(F) -> + case mnesia:transaction(F) of + {atomic, _} -> + ok; + {aborted, Reason} -> + ?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]), + {error, db_failure} + end. + +-spec update_tables() -> ok. +update_tables() -> + try + mnesia:transform_table(route, ignore, record_info(fields, route)) + catch exit:{aborted, {no_exists, _}} -> + ok + end, + case lists:member(local_route, mnesia:system_info(tables)) of + true -> mnesia:delete_table(local_route); + false -> ok + end. diff --git a/src/ejabberd_router_multicast.erl b/src/ejabberd_router_multicast.erl new file mode 100644 index 000000000..df8473c2b --- /dev/null +++ b/src/ejabberd_router_multicast.erl @@ -0,0 +1,272 @@ +%%%---------------------------------------------------------------------- +%%% File : ejabberd_router_multicast.erl +%%% Author : Badlop +%%% Purpose : Multicast router +%%% Created : 11 Aug 2007 by Badlop +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(ejabberd_router_multicast). +-author('alexey@process-one.net'). +-author('badlop@process-one.net'). + +-behaviour(gen_server). + +%% API +-export([route_multicast/5, + register_route/1, + unregister_route/1 + ]). + +-export([start_link/0]). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3, update_to_in_wrapped/2]). + +-include("logger.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). + +-record(route_multicast, {domain = <<"">> :: binary() | '_', + pid = self() :: pid()}). +-record(state, {}). + +%%==================================================================== +%% API +%%==================================================================== +%%-------------------------------------------------------------------- +%% Function: start_link() -> {ok,Pid} | ignore | {error,Error} +%% Description: Starts the server +%%-------------------------------------------------------------------- +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +-spec route_multicast(jid(), binary(), [jid()], stanza(), boolean()) -> ok. +route_multicast(From0, Domain0, Destinations0, Packet0, Wrapped0) -> + {From, Domain, Destinations, Packet, Wrapped} = + ejabberd_hooks:run_fold(multicast_route, Domain0, {From0, Domain0, Destinations0, Packet0, Wrapped0}, []), + case catch do_route(Domain, Destinations, xmpp:set_from(Packet, From), Wrapped) of + {'EXIT', Reason} -> + ?ERROR_MSG("~p~nwhen processing: ~p", + [Reason, {From, Domain, Destinations, Packet}]); + _ -> + ok + end. + +-spec register_route(binary()) -> any(). +register_route(Domain) -> + case jid:nameprep(Domain) of + error -> + erlang:error({invalid_domain, Domain}); + LDomain -> + Pid = self(), + F = fun() -> + mnesia:write(#route_multicast{domain = LDomain, + pid = Pid}) + end, + mnesia:transaction(F) + end. + +-spec unregister_route(binary()) -> any(). +unregister_route(Domain) -> + case jid:nameprep(Domain) of + error -> + erlang:error({invalid_domain, Domain}); + LDomain -> + Pid = self(), + F = fun() -> + case mnesia:select(route_multicast, + [{#route_multicast{pid = Pid, domain = LDomain, _ = '_'}, + [], + ['$_']}]) of + [R] -> mnesia:delete_object(R); + _ -> ok + end + end, + mnesia:transaction(F) + end. + + +%%==================================================================== +%% gen_server callbacks +%%==================================================================== + +%%-------------------------------------------------------------------- +%% Function: init(Args) -> {ok, State} | +%% {ok, State, Timeout} | +%% ignore | +%% {stop, Reason} +%% Description: Initiates the server +%%-------------------------------------------------------------------- +init([]) -> + ejabberd_mnesia:create(?MODULE, route_multicast, + [{ram_copies, [node()]}, + {type, bag}, + {attributes, + record_info(fields, route_multicast)}]), + mnesia:subscribe({table, route_multicast, simple}), + lists:foreach( + fun(Pid) -> + erlang:monitor(process, Pid) + end, + mnesia:dirty_select(route_multicast, [{{route_multicast, '_', '$1'}, [], ['$1']}])), + {ok, #state{}}. + +%%-------------------------------------------------------------------- +%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | +%% {reply, Reply, State, Timeout} | +%% {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, Reply, State} | +%% {stop, Reason, State} +%% Description: Handling call messages +%%-------------------------------------------------------------------- +handle_call(Request, From, State) -> + ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), + {noreply, State}. + +%%-------------------------------------------------------------------- +%% Function: handle_cast(Msg, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} +%% Description: Handling cast messages +%%-------------------------------------------------------------------- +handle_cast(Msg, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Msg]), + {noreply, State}. + +%%-------------------------------------------------------------------- +%% Function: handle_info(Info, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} +%% Description: Handling all non call/cast messages +%%-------------------------------------------------------------------- +handle_info({route_multicast, Domain, Destinations, Packet}, State) -> + case catch do_route(Domain, Destinations, Packet, false) of + {'EXIT', Reason} -> + ?ERROR_MSG("~p~nwhen processing: ~p", + [Reason, {Domain, Destinations, Packet}]); + _ -> + ok + end, + {noreply, State}; +handle_info({mnesia_table_event, {write, #route_multicast{pid = Pid}, _ActivityId}}, + State) -> + erlang:monitor(process, Pid), + {noreply, State}; +handle_info({'DOWN', _Ref, _Type, Pid, _Info}, State) -> + F = fun() -> + Es = mnesia:select( + route_multicast, + [{#route_multicast{pid = Pid, _ = '_'}, + [], + ['$_']}]), + lists:foreach( + fun(E) -> + mnesia:delete_object(E) + end, Es) + end, + mnesia:transaction(F), + {noreply, State}; +handle_info(Info, State) -> + ?WARNING_MSG("Unexpected info: ~p", [Info]), + {noreply, State}. + +%%-------------------------------------------------------------------- +%% Function: terminate(Reason, State) -> void() +%% Description: This function is called by a gen_server when it is about to +%% terminate. It should be the opposite of Module:init/1 and do any necessary +%% cleaning up. When it returns, the gen_server terminates with Reason. +%% The return value is ignored. +%%-------------------------------------------------------------------- +terminate(_Reason, _State) -> + ok. + +%%-------------------------------------------------------------------- +%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} +%% Description: Convert process state when code is changed +%%-------------------------------------------------------------------- +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +-spec update_to_in_wrapped(stanza(), jid()) -> stanza(). +update_to_in_wrapped(Packet, To) -> + case Packet of + #message{sub_els = [#ps_event{ + items = #ps_items{ + items = [#ps_item{ + sub_els = [Internal] + } = PSItem] + } = PSItems + } = PSEvent]} -> + Internal2 = xmpp:set_to(Internal, To), + PSItem2 = PSItem#ps_item{sub_els = [Internal2]}, + PSItems2 = PSItems#ps_items{items = [PSItem2]}, + PSEvent2 = PSEvent#ps_event{items = PSItems2}, + xmpp:set_to(Packet#message{sub_els = [PSEvent2]}, To); + _ -> + xmpp:set_to(Packet, To) + end. + +%%-------------------------------------------------------------------- +%%% Internal functions +%%-------------------------------------------------------------------- +%% From = #jid +%% Destinations = [#jid] +-spec do_route(binary(), [jid()], stanza(), boolean()) -> any(). +do_route(Domain, Destinations, Packet, true) -> + ?DEBUG("Route multicast:~n~ts~nDomain: ~ts~nDestinations: ~ts~n", + [xmpp:pp(Packet), Domain, + str:join([jid:encode(To) || To <- Destinations], <<", ">>)]), + lists:foreach( + fun(To) -> + Packet2 = update_to_in_wrapped(Packet, To), + ejabberd_router:route(Packet2) + end, Destinations); +do_route(Domain, Destinations, Packet, false) -> + ?DEBUG("Route multicast:~n~ts~nDomain: ~ts~nDestinations: ~ts~n", + [xmpp:pp(Packet), Domain, + str:join([jid:encode(To) || To <- Destinations], <<", ">>)]), + %% Try to find an appropriate multicast service + case mnesia:dirty_read(route_multicast, Domain) of + + %% If no multicast service is available in this server, send manually + [] -> do_route_normal(Destinations, Packet); + + %% If some is available, send the packet using multicast service + Rs when is_list(Rs) -> + Pid = pick_multicast_pid(Rs), + Pid ! {route_trusted, Destinations, Packet} + end. + +-spec pick_multicast_pid([#route_multicast{}]) -> pid(). +pick_multicast_pid(Rs) -> + List = case [R || R <- Rs, node(R#route_multicast.pid) == node()] of + [] -> Rs; + RLocals -> RLocals + end, + (hd(List))#route_multicast.pid. + +-spec do_route_normal([jid()], stanza()) -> any(). +do_route_normal(Destinations, Packet) -> + lists:foreach( + fun(To) -> + ejabberd_router:route(xmpp:set_to(Packet, To)) + end, Destinations). diff --git a/src/ejabberd_router_redis.erl b/src/ejabberd_router_redis.erl new file mode 100644 index 000000000..0ddd63aa7 --- /dev/null +++ b/src/ejabberd_router_redis.erl @@ -0,0 +1,189 @@ +%%%------------------------------------------------------------------- +%%% Author : Evgeny Khramtsov +%%% Created : 28 Mar 2017 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(ejabberd_router_redis). +-behaviour(ejabberd_router). +-behaviour(gen_server). + +%% API +-export([init/0, register_route/5, unregister_route/3, find_routes/1, + get_all_routes/0]). +%% gen_server callbacks +-export([init/1, handle_cast/2, handle_call/3, handle_info/2, + terminate/2, code_change/3, start_link/0]). + +-include("logger.hrl"). +-include("ejabberd_router.hrl"). + +-record(state, {}). + +-define(ROUTES_KEY, <<"ejabberd:routes">>). +-define(DOMAINS_KEY, <<"ejabberd:domains">>). + +%%%=================================================================== +%%% API +%%%=================================================================== +init() -> + Spec = {?MODULE, {?MODULE, start_link, []}, + transient, 5000, worker, [?MODULE]}, + case supervisor:start_child(ejabberd_backend_sup, Spec) of + {ok, _Pid} -> ok; + Err -> Err + end. + +-spec start_link() -> {ok, pid()} | {error, any()}. +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +register_route(Domain, ServerHost, LocalHint, _, Pid) -> + DomKey = domain_key(Domain), + PidKey = term_to_binary(Pid), + T = term_to_binary({ServerHost, LocalHint}), + case ejabberd_redis:multi( + fun() -> + ejabberd_redis:hset(DomKey, PidKey, T), + ejabberd_redis:sadd(?DOMAINS_KEY, [Domain]), + if Domain /= ServerHost -> + ejabberd_redis:sadd(?ROUTES_KEY, [Domain]); + true -> + ok + end + end) of + {ok, _} -> + ok; + {error, _} -> + {error, db_failure} + end. + +unregister_route(Domain, _, Pid) -> + DomKey = domain_key(Domain), + PidKey = term_to_binary(Pid), + try + {ok, Num} = ejabberd_redis:hdel(DomKey, [PidKey]), + if Num > 0 -> + {ok, Len} = ejabberd_redis:hlen(DomKey), + if Len == 0 -> + {ok, _} = ejabberd_redis:multi( + fun() -> + ejabberd_redis:del([DomKey]), + ejabberd_redis:srem(?ROUTES_KEY, [Domain]), + ejabberd_redis:srem(?DOMAINS_KEY, [Domain]) + end), + ok; + true -> + ok + end; + true -> + ok + end + catch _:{badmatch, {error, _}} -> + {error, db_failure} + end. + +find_routes(Domain) -> + DomKey = domain_key(Domain), + case ejabberd_redis:hgetall(DomKey) of + {ok, Vals} -> + {ok, decode_routes(Domain, Vals)}; + _ -> + {error, db_failure} + end. + +get_all_routes() -> + case ejabberd_redis:smembers(?ROUTES_KEY) of + {ok, Routes} -> + {ok, Routes}; + _ -> + {error, db_failure} + end. + +get_all_domains() -> + case ejabberd_redis:smembers(?DOMAINS_KEY) of + {ok, Domains} -> + {ok, Domains}; + _ -> + {error, db_failure} + end. + +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== +init([]) -> + clean_table(), + {ok, #state{}}. + +handle_call(Request, From, State) -> + ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), + {noreply, State}. + +handle_cast(Msg, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Msg]), + {noreply, State}. + +handle_info(Info, State) -> + ?ERROR_MSG("Unexpected info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +clean_table() -> + ?DEBUG("Cleaning Redis route entries...", []), + lists:foreach( + fun(#route{domain = Domain, pid = Pid}) when node(Pid) == node() -> + unregister_route(Domain, undefined, Pid); + (_) -> + ok + end, find_routes()). + +find_routes() -> + case get_all_domains() of + {ok, Domains} -> + lists:flatmap( + fun(Domain) -> + case find_routes(Domain) of + {ok, Routes} -> Routes; + {error, _} -> [] + end + end, Domains); + {error, _} -> + [] + end. + +domain_key(Domain) -> + <<"ejabberd:route:", Domain/binary>>. + +decode_routes(Domain, Vals) -> + lists:map( + fun({Pid, Data}) -> + {ServerHost, LocalHint} = binary_to_term(Data), + #route{domain = Domain, + pid = binary_to_term(Pid), + server_host = ServerHost, + local_hint = LocalHint} + end, Vals). diff --git a/src/ejabberd_router_sql.erl b/src/ejabberd_router_sql.erl new file mode 100644 index 000000000..2d7631476 --- /dev/null +++ b/src/ejabberd_router_sql.erl @@ -0,0 +1,154 @@ +%%%------------------------------------------------------------------- +%%% Author : Evgeny Khramtsov +%%% Created : 28 Mar 2017 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(ejabberd_router_sql). +-behaviour(ejabberd_router). + + +%% API +-export([init/0, register_route/5, unregister_route/3, find_routes/1, + get_all_routes/0]). +-export([sql_schemas/0]). + +-include("logger.hrl"). +-include("ejabberd_sql_pt.hrl"). +-include("ejabberd_router.hrl"). + + + +%%%=================================================================== +%%% API +%%%=================================================================== +init() -> + ejabberd_sql_schema:update_schema( + ejabberd_config:get_myname(), ?MODULE, sql_schemas()), + Node = erlang:atom_to_binary(node(), latin1), + ?DEBUG("Cleaning SQL 'route' table...", []), + case ejabberd_sql:sql_query( + ejabberd_config:get_myname(), ?SQL("delete from route where node=%(Node)s")) of + {updated, _} -> + ok; + Err -> + ?ERROR_MSG("Failed to clean 'route' table: ~p", [Err]), + Err + end. + +sql_schemas() -> + [#sql_schema{ + version = 1, + tables = + [#sql_table{ + name = <<"route">>, + columns = + [#sql_column{name = <<"domain">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"node">>, type = text}, + #sql_column{name = <<"pid">>, type = text}, + #sql_column{name = <<"local_hint">>, type = text}], + indices = [#sql_index{ + columns = [<<"domain">>, <<"server_host">>, + <<"node">>, <<"pid">>], + unique = true}]}]}]. + +register_route(Domain, ServerHost, LocalHint, _, Pid) -> + PidS = misc:encode_pid(Pid), + LocalHintS = enc_local_hint(LocalHint), + Node = erlang:atom_to_binary(node(Pid), latin1), + case ?SQL_UPSERT(ejabberd_config:get_myname(), "route", + ["!domain=%(Domain)s", + "!server_host=%(ServerHost)s", + "!node=%(Node)s", + "!pid=%(PidS)s", + "local_hint=%(LocalHintS)s"]) of + ok -> + ok; + _ -> + {error, db_failure} + end. + +unregister_route(Domain, _, Pid) -> + PidS = misc:encode_pid(Pid), + Node = erlang:atom_to_binary(node(Pid), latin1), + case ejabberd_sql:sql_query( + ejabberd_config:get_myname(), + ?SQL("delete from route where domain=%(Domain)s " + "and pid=%(PidS)s and node=%(Node)s")) of + {updated, _} -> + ok; + _ -> + {error, db_failure} + end. + +find_routes(Domain) -> + case ejabberd_sql:sql_query( + ejabberd_config:get_myname(), + ?SQL("select @(server_host)s, @(node)s, @(pid)s, @(local_hint)s " + "from route where domain=%(Domain)s")) of + {selected, Rows} -> + {ok, lists:flatmap( + fun(Row) -> + row_to_route(Domain, Row) + end, Rows)}; + _ -> + {error, db_failure} + end. + +get_all_routes() -> + case ejabberd_sql:sql_query( + ejabberd_config:get_myname(), + ?SQL("select @(domain)s from route where domain <> server_host")) of + {selected, Domains} -> + {ok, [Domain || {Domain} <- Domains]}; + _ -> + {error, db_failure} + end. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +enc_local_hint(undefined) -> + <<"">>; +enc_local_hint(LocalHint) -> + misc:term_to_expr(LocalHint). + +dec_local_hint(<<"">>) -> + undefined; +dec_local_hint(S) -> + ejabberd_sql:decode_term(S). + +row_to_route(Domain, {ServerHost, NodeS, PidS, LocalHintS} = Row) -> + try [#route{domain = Domain, + server_host = ServerHost, + pid = misc:decode_pid(PidS, NodeS), + local_hint = dec_local_hint(LocalHintS)}] + catch _:{bad_node, _} -> + []; + Class:Reason:StackTrace -> + ?ERROR_MSG("Failed to decode row from 'route' table:~n" + "** Row = ~p~n" + "** Domain = ~ts~n" + "** ~ts", + [Row, + Domain, + misc:format_exception(2, Class, Reason, StackTrace)]), + [] + end. diff --git a/src/ejabberd_s2s.erl b/src/ejabberd_s2s.erl index a7b3234fd..0876ca584 100644 --- a/src/ejabberd_s2s.erl +++ b/src/ejabberd_s2s.erl @@ -5,7 +5,7 @@ %%% Created : 7 Dec 2002 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -30,155 +30,96 @@ -behaviour(gen_server). %% API --export([start_link/0, route/3, have_connection/1, - has_key/2, get_connections_pids/1, try_register/1, - remove_connection/3, find_connection/2, +-export([start_link/0, stop/0, route/1, have_connection/1, + 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, clean_temporarily_blocked_table/0, list_temporarily_blocked_hosts/0, external_host_overloaded/1, is_temporarly_blocked/1, - check_peer_certificate/3]). + get_commands_spec/0, zlib_enabled/1, get_idle_timeout/1, + tls_required/1, tls_enabled/1, tls_options/3, + host_up/1, host_down/1, queue_type/1, register_connection/1]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -%% ejabberd API --export([get_info_s2s_connections/1, transform_options/1]). +-export([get_info_s2s_connections/1]). --include("ejabberd.hrl"). -include("logger.hrl"). - --include("jlib.hrl"). - +-include_lib("xmpp/include/xmpp.hrl"). -include("ejabberd_commands.hrl"). +-include_lib("stdlib/include/ms_transform.hrl"). --include_lib("public_key/include/public_key.hrl"). - --define(PKIXEXPLICIT, 'OTP-PUB-KEY'). - --define(PKIXIMPLICIT, 'OTP-PUB-KEY'). - --include("XmppAddr.hrl"). +-include("translate.hrl"). -define(DEFAULT_MAX_S2S_CONNECTIONS_NUMBER, 1). - -define(DEFAULT_MAX_S2S_CONNECTIONS_NUMBER_PER_NODE, 1). - -define(S2S_OVERLOAD_BLOCK_PERIOD, 60). -%% once a server is temporarly blocked, it stay blocked for 60 seconds +%% once a server is temporary blocked, it stay blocked for 60 seconds --record(s2s, {fromto = {<<"">>, <<"">>} :: {binary(), binary()} | '_', - pid = self() :: pid() | '_' | '$1', - key = <<"">> :: binary() | '_'}). +-record(s2s, {fromto :: {binary(), binary()} | '_', + pid :: pid()}). -record(state, {}). --record(temporarily_blocked, {host = <<"">> :: binary(), - timestamp = now() :: erlang:timestamp()}). +-record(temporarily_blocked, {host :: binary(), + timestamp :: integer()}). -type temporarily_blocked() :: #temporarily_blocked{}. - -%%==================================================================== -%% API -%%==================================================================== -%%-------------------------------------------------------------------- -%% Function: start_link() -> {ok,Pid} | ignore | {error,Error} -%% Description: Starts the server -%%-------------------------------------------------------------------- start_link() -> - gen_server:start_link({local, ?MODULE}, ?MODULE, [], - []). + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). --spec route(jid(), jid(), xmlel()) -> ok. - -route(From, To, Packet) -> - case catch do_route(From, To, Packet) of - {'EXIT', Reason} -> - ?ERROR_MSG("~p~nwhen processing: ~p", - [Reason, {From, To, Packet}]); - _ -> ok - end. +-spec stop() -> ok. +stop() -> + _ = supervisor:terminate_child(ejabberd_sup, ?MODULE), + _ = supervisor:delete_child(ejabberd_sup, ?MODULE), + ok. clean_temporarily_blocked_table() -> mnesia:clear_table(temporarily_blocked). -spec list_temporarily_blocked_hosts() -> [temporarily_blocked()]. - list_temporarily_blocked_hosts() -> ets:tab2list(temporarily_blocked). -spec external_host_overloaded(binary()) -> {aborted, any()} | {atomic, ok}. - external_host_overloaded(Host) -> - ?INFO_MSG("Disabling connections from ~s for ~p " - "seconds", + ?INFO_MSG("Disabling s2s connections to ~ts for ~p seconds", [Host, ?S2S_OVERLOAD_BLOCK_PERIOD]), mnesia:transaction(fun () -> + Time = erlang:monotonic_time(), mnesia:write(#temporarily_blocked{host = Host, - timestamp = - now()}) + timestamp = Time}) end). -spec is_temporarly_blocked(binary()) -> boolean(). - is_temporarly_blocked(Host) -> case mnesia:dirty_read(temporarily_blocked, Host) of - [] -> false; - [#temporarily_blocked{timestamp = T} = Entry] -> - case timer:now_diff(now(), T) of - N when N > (?S2S_OVERLOAD_BLOCK_PERIOD) * 1000 * 1000 -> - mnesia:dirty_delete_object(Entry), false; - _ -> true - end - end. - --spec remove_connection({binary(), binary()}, - pid(), binary()) -> {atomic, ok} | - ok | - {aborted, any()}. - -remove_connection(FromTo, Pid, Key) -> - case catch mnesia:dirty_match_object(s2s, - #s2s{fromto = FromTo, pid = Pid, - _ = '_'}) - of - [#s2s{pid = Pid, key = Key}] -> - F = fun () -> - mnesia:delete_object(#s2s{fromto = FromTo, pid = Pid, - key = Key}) - end, - mnesia:transaction(F); - _ -> ok + [] -> false; + [#temporarily_blocked{timestamp = T} = Entry] -> + Diff = erlang:monotonic_time() - T, + case erlang:convert_time_unit(Diff, native, microsecond) of + N when N > (?S2S_OVERLOAD_BLOCK_PERIOD) * 1000 * 1000 -> + mnesia:dirty_delete_object(Entry), false; + _ -> true + end end. -spec have_connection({binary(), binary()}) -> boolean(). - have_connection(FromTo) -> case catch mnesia:dirty_read(s2s, FromTo) of - [_] -> + [_] -> true; _ -> false end. --spec has_key({binary(), binary()}, binary()) -> boolean(). - -has_key(FromTo, Key) -> - case mnesia:dirty_select(s2s, - [{#s2s{fromto = FromTo, key = Key, _ = '_'}, - [], - ['$_']}]) of - [] -> - false; - _ -> - true - end. - -spec get_connections_pids({binary(), binary()}) -> [pid()]. - get_connections_pids(FromTo) -> case catch mnesia:dirty_read(s2s, FromTo) of L when is_list(L) -> @@ -187,189 +128,235 @@ get_connections_pids(FromTo) -> [] end. --spec try_register({binary(), binary()}) -> {key, binary()} | false. - -try_register(FromTo) -> - Key = randoms:get_string(), - 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(), - key = Key}), - {key, Key}; - true -> false - end - end, - case mnesia:transaction(F) of - {atomic, Res} -> Res; - _ -> false - end. +-spec register_connection(FromTo :: {binary(), binary()}) -> ok. +register_connection(FromTo) -> + gen_server:call(ejabberd_s2s, {register_connection, FromTo, self()}). -spec dirty_get_connections() -> [{binary(), binary()}]. - dirty_get_connections() -> mnesia:dirty_all_keys(s2s). -check_peer_certificate(SockMod, Sock, Peer) -> - case SockMod:get_peer_certificate(Sock) of - {ok, Cert} -> - case SockMod:get_verify_result(Sock) of - 0 -> - case idna:domain_utf8_to_ascii(Peer) of - false -> - {error, <<"Cannot decode remote server name">>}; - AsciiPeer -> - case - lists:any(fun(D) -> match_domain(AsciiPeer, D) end, - get_cert_domains(Cert)) of - true -> - {ok, <<"Verification successful">>}; - false -> - {error, <<"Certificate host name mismatch">>} - end - end; - VerifyRes -> - {error, p1_tls:get_cert_verify_string(VerifyRes, Cert)} - end; - error -> - {error, <<"Cannot get peer certificate">>} +-spec tls_options(binary(), binary(), [proplists:property()]) -> [proplists:property()]. +tls_options(LServer, ServerHost, DefaultOpts) -> + TLSOpts1 = case ejabberd_pkix:get_certfile(LServer) of + error -> DefaultOpts; + {ok, CertFile} -> + lists:keystore(certfile, 1, DefaultOpts, + {certfile, CertFile}) + end, + TLSOpts2 = case ejabberd_option:s2s_ciphers(ServerHost) of + undefined -> TLSOpts1; + Ciphers -> lists:keystore(ciphers, 1, TLSOpts1, + {ciphers, Ciphers}) + end, + TLSOpts3 = case ejabberd_option:s2s_protocol_options(ServerHost) of + undefined -> TLSOpts2; + ProtoOpts -> lists:keystore(protocol_options, 1, TLSOpts2, + {protocol_options, ProtoOpts}) + end, + TLSOpts4 = case ejabberd_option:s2s_dhfile(ServerHost) of + undefined -> TLSOpts3; + DHFile -> lists:keystore(dhfile, 1, TLSOpts3, + {dhfile, DHFile}) + end, + TLSOpts5 = case lists:keymember(cafile, 1, TLSOpts4) of + true -> TLSOpts4; + false -> [{cafile, get_cafile(ServerHost)}|TLSOpts4] + end, + case ejabberd_option:s2s_tls_compression(ServerHost) of + undefined -> TLSOpts5; + false -> [compression_none | TLSOpts5]; + true -> lists:delete(compression_none, TLSOpts5) + end. + +-spec tls_required(binary()) -> boolean(). +tls_required(LServer) -> + TLS = use_starttls(LServer), + TLS == required. + +-spec tls_enabled(binary()) -> boolean(). +tls_enabled(LServer) -> + TLS = use_starttls(LServer), + TLS /= false. + +-spec zlib_enabled(binary()) -> boolean(). +zlib_enabled(LServer) -> + ejabberd_option:s2s_zlib(LServer). + +-spec use_starttls(binary()) -> boolean() | optional | required. +use_starttls(LServer) -> + ejabberd_option:s2s_use_starttls(LServer). + +-spec get_idle_timeout(binary()) -> non_neg_integer() | infinity. +get_idle_timeout(LServer) -> + ejabberd_option:s2s_timeout(LServer). + +-spec queue_type(binary()) -> ram | file. +queue_type(LServer) -> + ejabberd_option:s2s_queue_type(LServer). + +-spec get_cafile(binary()) -> file:filename_all() | undefined. +get_cafile(LServer) -> + case ejabberd_option:s2s_cafile(LServer) of + undefined -> + ejabberd_option:ca_file(); + File -> + File end. %%==================================================================== %% gen_server callbacks %%==================================================================== - -%%-------------------------------------------------------------------- -%% Function: init(Args) -> {ok, State} | -%% {ok, State, Timeout} | -%% ignore | -%% {stop, Reason} -%% Description: Initiates the server -%%-------------------------------------------------------------------- init([]) -> update_tables(), - mnesia:create_table(s2s, [{ram_copies, [node()]}, {type, bag}, - {attributes, record_info(fields, s2s)}]), - mnesia:add_table_copy(s2s, node(), ram_copies), - mnesia:subscribe(system), - ejabberd_commands:register_commands(commands()), - mnesia:create_table(temporarily_blocked, [{ram_copies, [node()]}, {attributes, record_info(fields, temporarily_blocked)}]), - {ok, #state{}}. + ejabberd_mnesia:create(?MODULE, s2s, + [{ram_copies, [node()]}, + {type, bag}, + {attributes, record_info(fields, s2s)}]), + case mnesia:subscribe(system) of + {ok, _} -> + ejabberd_commands:register_commands(get_commands_spec()), + ejabberd_mnesia:create( + ?MODULE, temporarily_blocked, + [{ram_copies, [node()]}, + {attributes, record_info(fields, temporarily_blocked)}]), + ejabberd_hooks:add(host_up, ?MODULE, host_up, 50), + ejabberd_hooks:add(host_down, ?MODULE, host_down, 60), + lists:foreach(fun host_up/1, ejabberd_option:hosts()), + {ok, #state{}}; + {error, Reason} -> + {stop, Reason} + end. -%%-------------------------------------------------------------------- -%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | -%% {reply, Reply, State, Timeout} | -%% {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, Reply, State} | -%% {stop, Reason, State} -%% Description: Handling call messages -%%-------------------------------------------------------------------- -handle_call(_Request, _From, State) -> - Reply = ok, - {reply, Reply, State}. - -%%-------------------------------------------------------------------- -%% Function: handle_cast(Msg, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} -%% Description: Handling cast messages -%%-------------------------------------------------------------------- -handle_cast(_Msg, State) -> +handle_call({new_connection, Args}, _From, State) -> + {reply, erlang:apply(fun new_connection_int/7, Args), State}; +handle_call({register_connection, FromTo, Pid}, _From, State) -> + {reply, register_connection_int(FromTo, Pid), State}; +handle_call(Request, From, State) -> + ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), + {noreply, State}. + +handle_cast(Msg, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. -%%-------------------------------------------------------------------- -%% Function: handle_info(Info, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} -%% Description: Handling all non call/cast messages -%%-------------------------------------------------------------------- handle_info({mnesia_system_event, {mnesia_down, Node}}, State) -> + ?INFO_MSG("Node ~p has left our Mnesia S2S tables", [Node]), clean_table_from_bad_node(Node), {noreply, State}; -handle_info({route, From, To, Packet}, State) -> - case catch do_route(From, To, Packet) of - {'EXIT', Reason} -> - ?ERROR_MSG("~p~nwhen processing: ~p", - [Reason, {From, To, Packet}]); - _ -> ok +handle_info({mnesia_system_event, {mnesia_up, Node}}, State) -> + ?INFO_MSG("Node ~p joined our Mnesia S2S tables", [Node]), + {noreply, State}; +handle_info({route, Packet}, State) -> + try route(Packet) + catch + Class:Reason:StackTrace -> + ?ERROR_MSG("Failed to route packet:~n~ts~n** ~ts", + [xmpp:pp(Packet), + misc:format_exception(2, Class, Reason, StackTrace)]) end, {noreply, State}; -handle_info(_Info, State) -> {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}. -%%-------------------------------------------------------------------- -%% Function: terminate(Reason, State) -> void() -%% Description: This function is called by a gen_server when it is about to -%% terminate. It should be the opposite of Module:init/1 and do any necessary -%% cleaning up. When it returns, the gen_server terminates with Reason. -%% The return value is ignored. -%%-------------------------------------------------------------------- terminate(_Reason, _State) -> - ejabberd_commands:unregister_commands(commands()), - ok. + ejabberd_commands:unregister_commands(get_commands_spec()), + stop_s2s_connections(stream_error()), + lists:foreach(fun host_down/1, ejabberd_option:hosts()), + ejabberd_hooks:delete(host_up, ?MODULE, host_up, 50), + ejabberd_hooks:delete(host_down, ?MODULE, host_down, 60). -%%-------------------------------------------------------------------- -%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} -%% Description: Convert process state when code is changed -%%-------------------------------------------------------------------- code_change(_OldVsn, State, _Extra) -> {ok, State}. %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- +-spec host_up(binary()) -> ok. +host_up(Host) -> + ejabberd_s2s_in:host_up(Host), + ejabberd_s2s_out:host_up(Host). + +-spec host_down(binary()) -> ok. +host_down(Host) -> + Err = stream_error(), + lists:foreach( + fun(#s2s{fromto = {From, _}, pid = Pid}) when node(Pid) == node() -> + case ejabberd_router:host_of_route(From) of + Host -> + ejabberd_s2s_out:send(Pid, Err), + ejabberd_s2s_out:stop_async(Pid); + _ -> + ok + end; + (_) -> + ok + end, ets:tab2list(s2s)), + ejabberd_s2s_in:host_down(Host), + ejabberd_s2s_out:host_down(Host). + +-spec clean_table_from_bad_node(node()) -> any(). clean_table_from_bad_node(Node) -> F = fun() -> Es = mnesia:select( s2s, - [{#s2s{pid = '$1', _ = '_'}, - [{'==', {node, '$1'}, Node}], - ['$_']}]), - lists:foreach(fun(E) -> - mnesia:delete_object(E) - end, Es) + ets:fun2ms( + fun(#s2s{pid = Pid} = E) when node(Pid) == Node -> + E + end)), + lists:foreach(fun mnesia:delete_object/1, Es) end, mnesia:async_dirty(F). -do_route(From, To, Packet) -> - ?DEBUG("s2s manager~n\tfrom ~p~n\tto ~p~n\tpacket " - "~P~n", - [From, To, Packet, 8]), - case find_connection(From, To) of - {atomic, Pid} when is_pid(Pid) -> - ?DEBUG("sending to process ~p~n", [Pid]), - #xmlel{name = Name, attrs = Attrs, children = Els} = - Packet, - NewAttrs = - jlib:replace_from_to_attrs(jlib:jid_to_string(From), - jlib:jid_to_string(To), Attrs), - #jid{lserver = MyServer} = From, - ejabberd_hooks:run(s2s_send_packet, MyServer, - [From, To, Packet]), - send_element(Pid, - #xmlel{name = Name, attrs = NewAttrs, children = Els}), - ok; - {aborted, _Reason} -> - case xml:get_tag_attr_s(<<"type">>, Packet) of - <<"error">> -> ok; - <<"result">> -> ok; - _ -> - Err = jlib:make_error_reply(Packet, - ?ERR_SERVICE_UNAVAILABLE), - ejabberd_router:route(To, From, Err) - end, - false +-spec route(stanza()) -> ok. +route(Packet) -> + ?DEBUG("Local route:~n~ts", [xmpp:pp(Packet)]), + From = xmpp:get_from(Packet), + To = xmpp:get_to(Packet), + case start_connection(From, To) of + {ok, Pid} when is_pid(Pid) -> + ?DEBUG("Sending to process ~p~n", [Pid]), + #jid{lserver = MyServer} = From, + case ejabberd_hooks:run_fold(s2s_send_packet, MyServer, Packet, + []) of + drop -> ok; + Packet1 -> ejabberd_s2s_out:route(Pid, Packet1) + end; + {error, Reason} -> + Lang = xmpp:get_lang(Packet), + Err = case Reason of + forbidden -> + xmpp:err_forbidden(?T("Access denied by service policy"), Lang); + internal_server_error -> + xmpp:err_internal_server_error() + end, + ejabberd_router:route_error(Packet, Err) end. --spec find_connection(jid(), jid()) -> {aborted, any()} | {atomic, pid()}. +-spec start_connection(jid(), jid()) + -> {ok, pid()} | {error, forbidden | internal_server_error}. +start_connection(From, To) -> + start_connection(From, To, []). -find_connection(From, To) -> +-spec start_connection(jid(), jid(), [proplists:property()]) + -> {ok, pid()} | {error, forbidden | internal_server_error}. +start_connection(From, To, Opts) -> #jid{lserver = MyServer} = From, #jid{lserver = Server} = To, FromTo = {MyServer, Server}, @@ -378,257 +365,258 @@ find_connection(From, To) -> MaxS2SConnectionsNumberPerNode = max_s2s_connections_number_per_node(FromTo), ?DEBUG("Finding connection for ~p~n", [FromTo]), - case catch mnesia:dirty_read(s2s, FromTo) of - {'EXIT', Reason} -> {aborted, Reason}; - [] -> - %% We try to establish all the connections if the host is not a - %% service and if the s2s host is not blacklisted or - %% is in whitelist: - case not is_service(From, To) andalso - allow_host(MyServer, Server) - of - true -> - NeededConnections = needed_connections_number([], - MaxS2SConnectionsNumber, - MaxS2SConnectionsNumberPerNode), - open_several_connections(NeededConnections, MyServer, - Server, From, FromTo, - MaxS2SConnectionsNumber, - MaxS2SConnectionsNumberPerNode); - false -> {aborted, error} - end; - L when is_list(L) -> - NeededConnections = needed_connections_number(L, - MaxS2SConnectionsNumber, - MaxS2SConnectionsNumberPerNode), - if NeededConnections > 0 -> - %% We establish the missing connections for this pair. - open_several_connections(NeededConnections, MyServer, - Server, From, FromTo, + case mnesia:dirty_read(s2s, FromTo) of + [] -> + %% We try to establish all the connections if the host is not a + %% service and if the s2s host is not blacklisted or + %% is in whitelist: + LServer = ejabberd_router:host_of_route(MyServer), + case allow_host(LServer, Server) of + true -> + NeededConnections = needed_connections_number( + [], MaxS2SConnectionsNumber, - MaxS2SConnectionsNumberPerNode); - true -> - %% We choose a connexion from the pool of opened ones. - {atomic, choose_connection(From, L)} - end + MaxS2SConnectionsNumberPerNode), + open_several_connections(NeededConnections, MyServer, + Server, From, FromTo, + MaxS2SConnectionsNumber, + MaxS2SConnectionsNumberPerNode, Opts); + false -> + {error, forbidden} + end; + L when is_list(L) -> + NeededConnections = needed_connections_number(L, + MaxS2SConnectionsNumber, + MaxS2SConnectionsNumberPerNode), + if NeededConnections > 0 -> + %% We establish the missing connections for this pair. + open_several_connections(NeededConnections, MyServer, + Server, From, FromTo, + MaxS2SConnectionsNumber, + MaxS2SConnectionsNumberPerNode, Opts); + true -> + %% We choose a connection from the pool of opened ones. + {ok, choose_connection(From, L)} + end end. +-spec choose_connection(jid(), [#s2s{}]) -> pid(). choose_connection(From, Connections) -> choose_pid(From, [C#s2s.pid || C <- Connections]). +-spec choose_pid(jid(), [pid()]) -> pid(). choose_pid(From, Pids) -> Pids1 = case [P || P <- Pids, node(P) == node()] of - [] -> Pids; - Ps -> Ps + [] -> Pids; + Ps -> Ps end, Pid = - lists:nth(erlang:phash(jlib:jid_remove_resource(From), - length(Pids1)), + lists:nth(erlang:phash2(jid:remove_resource(From), + length(Pids1))+1, Pids1), ?DEBUG("Using ejabberd_s2s_out ~p~n", [Pid]), Pid. +-spec open_several_connections(pos_integer(), binary(), binary(), + jid(), {binary(), binary()}, + integer(), integer(), [proplists:property()]) -> + {ok, pid()} | {error, internal_server_error}. open_several_connections(N, MyServer, Server, From, FromTo, MaxS2SConnectionsNumber, - MaxS2SConnectionsNumberPerNode) -> - ConnectionsResult = [new_connection(MyServer, Server, - From, FromTo, MaxS2SConnectionsNumber, - MaxS2SConnectionsNumberPerNode) - || _N <- lists:seq(1, N)], - case [PID || {atomic, PID} <- ConnectionsResult] of - [] -> hd(ConnectionsResult); - PIDs -> {atomic, choose_pid(From, PIDs)} + MaxS2SConnectionsNumberPerNode, Opts) -> + case lists:flatmap( + fun(_) -> + new_connection(MyServer, Server, + From, FromTo, MaxS2SConnectionsNumber, + MaxS2SConnectionsNumberPerNode, Opts) + end, lists:seq(1, N)) of + [] -> + {error, internal_server_error}; + PIDs -> + {ok, choose_pid(From, PIDs)} end. +-spec new_connection(binary(), binary(), jid(), {binary(), binary()}, + integer(), integer(), [proplists:property()]) -> [pid()]. new_connection(MyServer, Server, From, FromTo, - MaxS2SConnectionsNumber, MaxS2SConnectionsNumberPerNode) -> - Key = randoms:get_string(), - {ok, Pid} = ejabberd_s2s_out:start( - MyServer, Server, {new, Key}), + 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}), NeededConnections = needed_connections_number(L, MaxS2SConnectionsNumber, MaxS2SConnectionsNumberPerNode), if NeededConnections > 0 -> - mnesia:write(#s2s{fromto = FromTo, pid = Pid, - key = Key}), - ?INFO_MSG("New s2s connection started ~p", [Pid]), - Pid; + mnesia:write(#s2s{fromto = FromTo, pid = Pid}), + Pid; true -> choose_connection(From, L) end end, TRes = mnesia:transaction(F), case TRes of - {atomic, Pid} -> ejabberd_s2s_out:start_connection(Pid); - _ -> ejabberd_s2s_out:stop_connection(Pid) - end, - TRes. + {atomic, Pid1} -> + if Pid1 == Pid -> + erlang:monitor(process, Pid), + ejabberd_s2s_out:connect(Pid); + true -> + ejabberd_s2s_out:stop_async(Pid) + end, + [Pid1]; + {aborted, Reason} -> + ?ERROR_MSG("Failed to register s2s connection ~ts -> ~ts: " + "Mnesia failure: ~p", + [MyServer, Server, Reason]), + ejabberd_s2s_out:stop_async(Pid), + [] + end. +-spec register_connection_int(FromTo :: {binary(), binary()}, Pid :: pid()) -> ok. +register_connection_int(FromTo, Pid) -> + F = fun() -> + mnesia:write(#s2s{fromto = FromTo, pid = Pid}) + end, + TRes = mnesia:transaction(F), + case TRes of + {atomic, _} -> + erlang:monitor(process, Pid), + ok; + _ -> + ok + end. + +-spec max_s2s_connections_number({binary(), binary()}) -> pos_integer(). max_s2s_connections_number({From, To}) -> - case acl:match_rule(From, max_s2s_connections, - jlib:make_jid(<<"">>, To, <<"">>)) - of - Max when is_integer(Max) -> Max; - _ -> ?DEFAULT_MAX_S2S_CONNECTIONS_NUMBER + case ejabberd_shaper:match(From, max_s2s_connections, jid:make(To)) of + Max when is_integer(Max) -> Max; + _ -> ?DEFAULT_MAX_S2S_CONNECTIONS_NUMBER end. +-spec max_s2s_connections_number_per_node({binary(), binary()}) -> pos_integer(). max_s2s_connections_number_per_node({From, To}) -> - case acl:match_rule(From, max_s2s_connections_per_node, - jlib:make_jid(<<"">>, To, <<"">>)) - of - Max when is_integer(Max) -> Max; - _ -> ?DEFAULT_MAX_S2S_CONNECTIONS_NUMBER_PER_NODE + case ejabberd_shaper:match(From, max_s2s_connections_per_node, jid:make(To)) of + Max when is_integer(Max) -> Max; + _ -> ?DEFAULT_MAX_S2S_CONNECTIONS_NUMBER_PER_NODE end. +-spec needed_connections_number([#s2s{}], integer(), integer()) -> integer(). needed_connections_number(Ls, MaxS2SConnectionsNumber, MaxS2SConnectionsNumberPerNode) -> LocalLs = [L || L <- Ls, node(L#s2s.pid) == node()], lists:min([MaxS2SConnectionsNumber - length(Ls), MaxS2SConnectionsNumberPerNode - length(LocalLs)]). -%%-------------------------------------------------------------------- -%% Function: is_service(From, To) -> true | false -%% Description: Return true if the destination must be considered as a -%% service. -%% -------------------------------------------------------------------- -is_service(From, To) -> - LFromDomain = From#jid.lserver, - case ejabberd_config:get_option( - {route_subdomains, LFromDomain}, - fun(s2s) -> s2s; (local) -> local end, local) of - s2s -> % bypass RFC 3920 10.3 - false; - local -> - Hosts = (?MYHOSTS), - P = fun (ParentDomain) -> - lists:member(ParentDomain, Hosts) - end, - lists:any(P, parent_domains(To#jid.lserver)) - end. - -parent_domains(Domain) -> - lists:foldl(fun (Label, []) -> [Label]; - (Label, [Head | Tail]) -> - [<>, + {<<"title">>, LinkTitle}. + +get_commit_link(_CommitHtmlUrl, _TitleErl, unknown_sha) -> + ?C(<<"Please Update Specs">>); +get_commit_link(CommitHtmlUrl, TitleEl, CommitSha) -> + ?AXC(CommitHtmlUrl, [TitleEl], binary:part(CommitSha, {0, 8})). + +get_content(Node, Query, Lang) -> + {{_CommandCtl}, _Res} = + case catch parse_and_execute(Query, Node) of + {'EXIT', _} -> {{""}, <<"">>}; + Result_tuple -> Result_tuple + end, + + AvailableModulesEls = get_available_modules_table(Lang), + InstalledModulesEls = get_installed_modules_table(Lang), + + Sources = get_sources_list(), + SourceEls = (?XAE(<<"table">>, + [], + [?XE(<<"tbody">>, + (lists:map( + fun(Dirname) -> + #{sha := CommitSha, + date := CommitDate, + message := CommitMessage, + html := Html, + author_name := AuthorName, + commit_html_url := CommitHtmlUrl + } = get_commit_details(Dirname), + TitleEl = make_title_el(CommitDate, CommitMessage, AuthorName), + ?XE(<<"tr">>, + [?XE(<<"td">>, [?AC(Html, Dirname)]), + ?XE(<<"td">>, + [get_commit_link(CommitHtmlUrl, TitleEl, CommitSha)] + ), + ?XE(<<"td">>, [?C(CommitMessage)]) + ]) + end, + lists:sort(Sources) + )) + ) + ] + )), + + [?XC(<<"p">>, + translate:translate( + Lang, ?T("Update specs to get modules source, then install desired ones.") + ) + ), + ?XAE(<<"form">>, + [{<<"method">>, <<"post">>}], + [?XCT(<<"h3">>, ?T("Sources Specs:")), + SourceEls, + ?BR, + ?INPUTT(<<"submit">>, + <<"updatespecs">>, + translate:translate(Lang, ?T("Update Specs"))), + + ?XCT(<<"h3">>, ?T("Installed Modules:")), + InstalledModulesEls, + ?BR, + + ?XCT(<<"h3">>, ?T("Other Modules Available:")), + AvailableModulesEls + ] + ) + ]. + +get_sources_list() -> + case file:list_dir(sources_dir()) of + {ok, Filenames} -> Filenames; + {error, enoent} -> [] + end. + +get_available_notinstalled() -> + Installed = installed(), + lists:filter( + fun({Mod, _}) -> + not lists:keymember(Mod, 1, Installed) + end, + available() + ). + +parse_and_execute(Query, Node) -> + {[Exec], _} = lists:partition( + fun(ExType) -> + lists:keymember(ExType, 1, Query) + end, + [<<"updatespecs">>] + ), + Commands = {get_val(<<"updatespecs">>, Query)}, + {_, R} = parse1_command(Exec, Commands, Node), + {Commands, R}. + +get_val(Val, Query) -> + {value, {_, R}} = lists:keysearch(Val, 1, Query), + binary_to_list(R). + +parse1_command(<<"updatespecs">>, {_}, _Node) -> + Res = update(), + {oook, io_lib:format("~p", [Res])}. + +list_modules_parse_query(Query) -> + case {lists:keysearch(<<"install">>, 1, Query), + lists:keysearch(<<"upgrade">>, 1, Query), + lists:keysearch(<<"uninstall">>, 1, Query)} + of + {{value, _}, _, _} -> list_modules_parse_install(Query); + {_, {value, _}, _} -> list_modules_parse_upgrade(Query); + {_, _, {value, _}} -> list_modules_parse_uninstall(Query); + _ -> nothing + end. + +list_modules_parse_install(Query) -> + lists:foreach( + fun({Mod, _}) -> + ModBin = misc:atom_to_binary(Mod), + case lists:member({<<"selected_install">>, ModBin}, Query) of + true -> install(Mod); + _ -> ok + end + end, + get_available_notinstalled()), + ok. + +list_modules_parse_upgrade(Query) -> + lists:foreach( + fun({Mod, _}) -> + ModBin = misc:atom_to_binary(Mod), + case lists:member({<<"selected_upgrade">>, ModBin}, Query) of + true -> upgrade(Mod); + _ -> ok + end + end, + installed()), + ok. + +list_modules_parse_uninstall(Query) -> + lists:foreach( + fun({Mod, _}) -> + ModBin = misc:atom_to_binary(Mod), + case lists:member({<<"selected_uninstall">>, ModBin}, Query) of + true -> uninstall(Mod); + _ -> ok + end + end, + installed()), + ok. + +install_contrib_modules(Modules, Config) -> + lists:filter(fun(Module) -> + case install(misc:atom_to_binary(Module), Config) of + {error, conflict} -> + false; + ok -> + true + end + end, + Modules). diff --git a/src/extauth.erl b/src/extauth.erl index eb936ddf9..b1acd4a7e 100644 --- a/src/extauth.erl +++ b/src/extauth.erl @@ -1,11 +1,8 @@ -%%%---------------------------------------------------------------------- -%%% File : extauth.erl -%%% Author : Leif Johansson -%%% Purpose : External authentication using a simple port-driver -%%% Created : 30 Jul 2004 by Leif Johansson +%%%------------------------------------------------------------------- +%%% Created : 7 May 2018 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -21,139 +18,199 @@ %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% -%%%---------------------------------------------------------------------- - +%%%------------------------------------------------------------------- -module(extauth). --author('leifj@it.su.se'). +-ifndef(GEN_SERVER). +-define(GEN_SERVER, gen_server). +-endif. +-behaviour(?GEN_SERVER). --export([start/2, stop/1, init/2, check_password/3, - set_password/3, try_register/3, remove_user/2, - remove_user/3, is_user_exists/2]). +-define(CALL_TIMEOUT, timer:seconds(30)). + +%% API +-export([start/1, stop/1, reload/1, start_link/2]). +-export([check_password/3, set_password/3, try_register/3, remove_user/2, + remove_user/3, user_exists/2, check_certificate/3]). +-export([prog_name/1, pool_name/1, worker_name/2, pool_size/1]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). --include("ejabberd.hrl"). -include("logger.hrl"). --define(INIT_TIMEOUT, 60000). +-record(state, {port :: port(), + prog :: string(), + start_time :: integer(), + os_pid :: integer() | undefined}). --define(CALL_TIMEOUT, 10000). - -start(Host, ExtPrg) -> - lists:foreach(fun (This) -> - start_instance(get_process_name(Host, This), ExtPrg) - end, - lists:seq(0, get_instances(Host) - 1)). - -start_instance(ProcessName, ExtPrg) -> - spawn(?MODULE, init, [ProcessName, ExtPrg]). - -restart_instance(ProcessName, ExtPrg) -> - unregister(ProcessName), - start_instance(ProcessName, ExtPrg). - -init(ProcessName, ExtPrg) -> - register(ProcessName, self()), - process_flag(trap_exit, true), - Port = open_port({spawn, ExtPrg}, [{packet, 2}]), - loop(Port, ?INIT_TIMEOUT, ProcessName, ExtPrg). +%%%=================================================================== +%%% API +%%%=================================================================== +start(Host) -> + extauth_sup:start(Host). stop(Host) -> - lists:foreach(fun (This) -> - get_process_name(Host, This) ! stop - end, - lists:seq(0, get_instances(Host) - 1)). + extauth_sup:stop(Host). -get_process_name(Host, Integer) -> - gen_mod:get_module_proc(iolist_to_binary([Host, - integer_to_list(Integer)]), - eauth). +reload(Host) -> + extauth_sup:reload(Host). + +start_link(Name, Prog) -> + ?GEN_SERVER:start_link({local, Name}, ?MODULE, [Prog], []). check_password(User, Server, Password) -> call_port(Server, [<<"auth">>, User, Server, Password]). -is_user_exists(User, Server) -> +check_certificate(User, Server, Certificate) -> + call_port(Server, [<<"certauth">>, User, Server, Certificate]). + +user_exists(User, Server) -> call_port(Server, [<<"isuser">>, User, Server]). set_password(User, Server, Password) -> call_port(Server, [<<"setpass">>, User, Server, Password]). try_register(User, Server, Password) -> - case call_port(Server, - [<<"tryregister">>, User, Server, Password]) - of - true -> {atomic, ok}; - false -> {error, not_allowed} - end. + call_port(Server, [<<"tryregister">>, User, Server, Password]). remove_user(User, Server) -> call_port(Server, [<<"removeuser">>, User, Server]). remove_user(User, Server, Password) -> - call_port(Server, - [<<"removeuser3">>, User, Server, Password]). + call_port(Server, [<<"removeuser3">>, User, Server, Password]). -call_port(Server, Msg) -> - LServer = jlib:nameprep(Server), - ProcessName = get_process_name(LServer, - random_instance(get_instances(LServer))), - ProcessName ! {call, self(), Msg}, - receive {eauth, Result} -> Result end. +-spec prog_name(binary()) -> string() | undefined. +prog_name(Host) -> + ejabberd_option:extauth_program(Host). -random_instance(MaxNum) -> - {A1, A2, A3} = now(), - random:seed(A1, A2, A3), - random:uniform(MaxNum) - 1. +-spec pool_name(binary()) -> atom(). +pool_name(Host) -> + case ejabberd_option:extauth_pool_name(Host) of + undefined -> + list_to_atom("extauth_pool_" ++ binary_to_list(Host)); + Name -> + list_to_atom("extauth_pool_" ++ binary_to_list(Name)) + end. -get_instances(Server) -> - ejabberd_config:get_option( - {extauth_instances, Server}, - fun(V) when is_integer(V), V > 0 -> - V - end, 1). +-spec worker_name(atom(), integer()) -> atom(). +worker_name(Pool, N) -> + list_to_atom(atom_to_list(Pool) ++ "_" ++ integer_to_list(N)). -loop(Port, Timeout, ProcessName, ExtPrg) -> - receive - {call, Caller, Msg} -> - port_command(Port, encode(Msg)), - receive - {Port, {data, Data}} -> - ?DEBUG("extauth call '~p' received data response:~n~p", - [Msg, Data]), - Caller ! {eauth, decode(Data)}, - loop(Port, ?CALL_TIMEOUT, ProcessName, ExtPrg); - {Port, Other} -> - ?ERROR_MSG("extauth call '~p' received strange response:~n~p", - [Msg, Other]), - Caller ! {eauth, false}, - loop(Port, ?CALL_TIMEOUT, ProcessName, ExtPrg) +-spec pool_size(binary()) -> pos_integer(). +pool_size(Host) -> + case ejabberd_option:extauth_pool_size(Host) of + undefined -> misc:logical_processors(); + Size -> + Size + end. + +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== +init([Prog]) -> + process_flag(trap_exit, true), + {Port, OSPid} = start_port(Prog), + Time = curr_time(), + {ok, #state{port = Port, start_time = Time, + prog = Prog, os_pid = OSPid}}. + +handle_call({cmd, Cmd, EndTime}, _From, State) -> + Timeout = EndTime - curr_time(), + if Timeout > 0 -> + Port = State#state.port, + port_command(Port, Cmd), + receive + {Port, {data, [0, N] = Data}} when N == 0; N == 1 -> + ?DEBUG("Received response from external authentication " + "program: ~p", [Data]), + {reply, decode_bool(N), State}; + {Port, Data} -> + ?ERROR_MSG("Received unexpected response from external " + "authentication program '~ts': ~p " + "(port = ~p, pid = ~w)", + [State#state.prog, Data, Port, State#state.os_pid]), + {reply, {error, unexpected_response}, State}; + {'EXIT', Port, Reason} -> + handle_info({'EXIT', Port, Reason}, State) after Timeout -> - ?ERROR_MSG("extauth call '~p' didn't receive response", - [Msg]), - Caller ! {eauth, false}, - Pid = restart_instance(ProcessName, ExtPrg), - flush_buffer_and_forward_messages(Pid), - exit(port_terminated) - end; - stop -> - Port ! {self(), close}, - receive {Port, closed} -> exit(normal) end; - {'EXIT', Port, Reason} -> - ?CRITICAL_MSG("extauth script has exitted abruptly " - "with reason '~p'", - [Reason]), - Pid = restart_instance(ProcessName, ExtPrg), - flush_buffer_and_forward_messages(Pid), - exit(port_terminated) + {stop, normal, State} + end; + true -> + {noreply, State} end. -flush_buffer_and_forward_messages(Pid) -> - receive - Message -> - Pid ! Message, flush_buffer_and_forward_messages(Pid) - after 0 -> true +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info({'EXIT', Port, _Reason}, #state{port = Port, + start_time = Time} = State) -> + case curr_time() - Time of + Diff when Diff < 1000 -> + ?ERROR_MSG("Failed to start external authentication program '~ts'", + [State#state.prog]), + {stop, normal, State}; + _ -> + ?ERROR_MSG("External authentication program '~ts' has terminated " + "unexpectedly (pid=~w), restarting via supervisor...", + [State#state.prog, State#state.os_pid]), + {stop, normal, State} + end; +handle_info(Info, State) -> + ?WARNING_MSG("Unexpected info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, State) -> + catch port_close(State#state.port), + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +-spec curr_time() -> non_neg_integer(). +curr_time() -> + erlang:monotonic_time(millisecond). + +-spec start_port(string()) -> {port(), integer() | undefined}. +start_port(Path) -> + Port = open_port({spawn, Path}, [{packet, 2}]), + link(Port), + case erlang:port_info(Port, os_pid) of + {os_pid, OSPid} -> + {Port, OSPid}; + undefined -> + {Port, undefined} end. -encode(L) -> str:join(L, <<":">>). +call_port(Server, Args) -> + call_port(Server, Args, ?CALL_TIMEOUT). -decode([0, 0]) -> false; -decode([0, 1]) -> true. +call_port(Server, Args, Timeout) -> + StartTime = erlang:monotonic_time(millisecond), + Pool = pool_name(Server), + PoolSize = pool_size(Server), + I = p1_rand:round_robin(PoolSize), + Cmd = str:join(Args, <<":">>), + do_call(Cmd, I, I + PoolSize, Pool, PoolSize, + StartTime + Timeout, StartTime). + +do_call(_, Max, Max, _, _, _, _) -> + {error, disconnected}; +do_call(Cmd, I, Max, Pool, PoolSize, EndTime, CurrTime) -> + Timeout = EndTime - CurrTime, + if Timeout > 0 -> + Proc = worker_name(Pool, (I rem PoolSize) + 1), + try ?GEN_SERVER:call(Proc, {cmd, Cmd, EndTime}, Timeout) + catch exit:{timeout, {?GEN_SERVER, call, _}} -> + {error, timeout}; + exit:{_, {?GEN_SERVER, call, _}} -> + do_call(Cmd, I+1, Max, Pool, PoolSize, EndTime, curr_time()) + end; + true -> + {error, timeout} + end. + +decode_bool(0) -> false; +decode_bool(1) -> true. diff --git a/src/extauth_sup.erl b/src/extauth_sup.erl new file mode 100644 index 000000000..40769cbc9 --- /dev/null +++ b/src/extauth_sup.erl @@ -0,0 +1,110 @@ +%%%------------------------------------------------------------------- +%%% Created : 7 May 2018 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(extauth_sup). +-behaviour(supervisor). + +%% API +-export([start/1, stop/1, reload/1, start_link/3]). +%% Supervisor callbacks +-export([init/1]). + +-include("logger.hrl"). + +%%%=================================================================== +%%% API functions +%%%=================================================================== +start(Host) -> + case extauth:prog_name(Host) of + undefined -> + ?ERROR_MSG("Option 'extauth_program' is not set for '~ts'", + [Host]), + ignore; + Prog -> + Pool = extauth:pool_name(Host), + ChildSpec = {Pool, {?MODULE, start_link, [Host, Prog, Pool]}, + permanent, infinity, supervisor, [?MODULE]}, + supervisor:start_child(ejabberd_backend_sup, ChildSpec) + end. + +stop(Host) -> + Pool = extauth:pool_name(Host), + supervisor:terminate_child(ejabberd_backend_sup, Pool), + supervisor:delete_child(ejabberd_backend_sup, Pool). + +reload(Host) -> + Pool = extauth:pool_name(Host), + Prog = extauth:prog_name(Host), + PoolSize = extauth:pool_size(Host), + try process_info(whereis(Pool), dictionary) of + {dictionary, Dict} -> + case proplists:get_value(extauth_program, Dict) of + Prog -> + OldPoolSize = try supervisor:which_children(Pool) of + Children -> length(Children) + catch _:_ -> PoolSize + end, + if OldPoolSize > PoolSize -> + lists:foreach( + fun(I) -> + Worker = extauth:worker_name(Pool, I), + supervisor:terminate_child(Pool, Worker), + supervisor:delete_child(Pool, Worker) + end, lists:seq(PoolSize+1, OldPoolSize)); + OldPoolSize < PoolSize -> + lists:foreach( + fun(I) -> + Spec = worker_spec(Pool, Prog, I), + supervisor:start_child(Pool, Spec) + end, lists:seq(OldPoolSize+1, PoolSize)); + OldPoolSize == PoolSize -> + ok + end; + _ -> + stop(Host), + start(Host) + end + catch _:badarg -> + ok + end. + +start_link(Host, Prog, Pool) -> + supervisor:start_link({local, Pool}, ?MODULE, [Host, Prog, Pool]). + +%%%=================================================================== +%%% Supervisor callbacks +%%%=================================================================== +init([Host, Prog, Pool]) -> + PoolSize = extauth:pool_size(Host), + Children = lists:map( + fun(I) -> + worker_spec(Pool, Prog, I) + end, lists:seq(1, PoolSize)), + put(extauth_program, Prog), + {ok, {{one_for_one, PoolSize, 1}, Children}}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +worker_spec(Pool, Prog, I) -> + Worker = extauth:worker_name(Pool, I), + {Worker, {extauth, start_link, [Worker, Prog]}, + permanent, 5000, worker, [extauth]}. diff --git a/src/gen_iq_handler.erl b/src/gen_iq_handler.erl index bbad1eca7..c123f6383 100644 --- a/src/gen_iq_handler.erl +++ b/src/gen_iq_handler.erl @@ -5,7 +5,7 @@ %%% Created : 22 Jan 2003 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -27,187 +27,124 @@ -author('alexey@process-one.net'). --behaviour(gen_server). - %% API --export([start_link/3, add_iq_handler/6, - remove_iq_handler/3, stop_iq_handler/3, handle/7, - process_iq/6, check_type/1, transform_module_options/1]). +-export([add_iq_handler/5, remove_iq_handler/3, handle/1, handle/2, + start/1, get_features/2]). +%% Deprecated functions +-export([add_iq_handler/6, handle/5, iqdisc/1]). +-deprecated([{add_iq_handler, 6}, {handle, 5}, {iqdisc, 1}]). -%% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, - handle_info/2, terminate/2, code_change/3]). - --include("ejabberd.hrl"). -include("logger.hrl"). --include("jlib.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). +-include("translate.hrl"). --record(state, {host, module, function}). -type component() :: ejabberd_sm | ejabberd_local. --type type() :: no_queue | one_queue | pos_integer() | parallel. --type opts() :: no_queue | {one_queue, pid()} | {queues, [pid()]} | parallel. %%==================================================================== %% API %%==================================================================== -%%-------------------------------------------------------------------- -%% Function: start_link() -> {ok,Pid} | ignore | {error,Error} -%% Description: Starts the server -%%-------------------------------------------------------------------- -start_link(Host, Module, Function) -> - gen_server:start_link(?MODULE, [Host, Module, Function], - []). +-spec start(component()) -> ok. +start(Component) -> + catch ets:new(Component, [named_table, public, ordered_set, + {read_concurrency, true}, + {heir, erlang:group_leader(), none}]), + ok. -add_iq_handler(Component, Host, NS, Module, Function, - Type) -> - case Type of - no_queue -> - Component:register_iq_handler(Host, NS, Module, - Function, no_queue); - one_queue -> - {ok, Pid} = supervisor:start_child(ejabberd_iq_sup, - [Host, Module, Function]), - Component:register_iq_handler(Host, NS, Module, - Function, {one_queue, Pid}); - N when is_integer(N) -> - Pids = lists:map(fun (_) -> - {ok, Pid} = - supervisor:start_child(ejabberd_iq_sup, - [Host, Module, - Function]), - Pid - end, - lists:seq(1, N)), - Component:register_iq_handler(Host, NS, Module, - Function, {queues, Pids}); - parallel -> - Component:register_iq_handler(Host, NS, Module, - Function, parallel) - end. +-spec add_iq_handler(component(), binary(), binary(), module(), atom()) -> ok. +add_iq_handler(Component, Host, NS, Module, Function) -> + ets:insert(Component, {{Host, NS}, Module, Function}), + ok. +-spec remove_iq_handler(component(), binary(), binary()) -> ok. remove_iq_handler(Component, Host, NS) -> - Component:unregister_iq_handler(Host, NS). + ets:delete(Component, {Host, NS}), + ok. -stop_iq_handler(_Module, _Function, Opts) -> - case Opts of - {one_queue, Pid} -> gen_server:call(Pid, stop); - {queues, Pids} -> - lists:foreach(fun (Pid) -> - catch gen_server:call(Pid, stop) - end, - Pids); - _ -> ok +-spec handle(iq()) -> ok. +handle(#iq{to = To} = IQ) -> + Component = case To#jid.luser of + <<"">> -> ejabberd_local; + _ -> ejabberd_sm + end, + handle(Component, IQ). + +-spec handle(component(), iq()) -> ok. +handle(Component, + #iq{to = To, type = T, lang = Lang, sub_els = [El]} = Packet) + when T == get; T == set -> + XMLNS = xmpp:get_ns(El), + Host = To#jid.lserver, + case ets:lookup(Component, {Host, XMLNS}) of + [{_, Module, Function}] -> + process_iq(Host, Module, Function, Packet); + [] -> + Txt = ?T("No module is handling this query"), + Err = xmpp:err_service_unavailable(Txt, Lang), + ejabberd_router:route_error(Packet, Err) + end; +handle(_, #iq{type = T, lang = Lang, sub_els = SubEls} = Packet) + when T == get; T == set -> + Txt = case SubEls of + [] -> ?T("No child elements found"); + _ -> ?T("Too many child elements") + end, + Err = xmpp:err_bad_request(Txt, Lang), + ejabberd_router:route_error(Packet, Err); +handle(_, #iq{type = T}) when T == result; T == error -> + ok. + +-spec get_features(component(), binary()) -> [binary()]. +get_features(Component, Host) -> + get_features(Component, ets:next(Component, {Host, <<"">>}), Host, []). + +get_features(Component, {Host, XMLNS}, Host, XMLNSs) -> + get_features(Component, + ets:next(Component, {Host, XMLNS}), Host, [XMLNS|XMLNSs]); +get_features(_, _, _, XMLNSs) -> + XMLNSs. + +-spec process_iq(binary(), atom(), atom(), iq()) -> ok. +process_iq(_Host, Module, Function, IQ) -> + try process_iq(Module, Function, IQ) of + #iq{} = ResIQ -> + ejabberd_router:route(ResIQ); + ignore -> + ok + catch + Class:Reason:StackTrace -> + ?ERROR_MSG("Failed to process iq:~n~ts~n** ~ts", + [xmpp:pp(IQ), + misc:format_exception(2, Class, Reason, StackTrace)]), + Txt = ?T("Module failed to handle the query"), + Err = xmpp:err_internal_server_error(Txt, IQ#iq.lang), + ejabberd_router:route_error(IQ, Err) end. -handle(Host, Module, Function, Opts, From, To, IQ) -> - case Opts of - no_queue -> - process_iq(Host, Module, Function, From, To, IQ); - {one_queue, Pid} -> Pid ! {process_iq, From, To, IQ}; - {queues, Pids} -> - Pid = lists:nth(erlang:phash(now(), length(Pids)), - Pids), - Pid ! {process_iq, From, To, IQ}; - parallel -> - spawn(?MODULE, process_iq, - [Host, Module, Function, From, To, IQ]); - _ -> todo +-spec process_iq(module(), atom(), iq()) -> ignore | iq(). +process_iq(Module, Function, #iq{lang = Lang, sub_els = [El]} = IQ) -> + try + Pkt = case erlang:function_exported(Module, decode_iq_subel, 1) of + true -> Module:decode_iq_subel(El); + false -> xmpp:decode(El) + end, + Module:Function(IQ#iq{sub_els = [Pkt]}) + catch error:{xmpp_codec, Why} -> + Txt = xmpp:io_format_error(Why), + xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)) end. -process_iq(_Host, Module, Function, From, To, IQ) -> - case catch Module:Function(From, To, IQ) of - {'EXIT', Reason} -> ?ERROR_MSG("~p", [Reason]); - ResIQ -> - if ResIQ /= ignore -> - ejabberd_router:route(To, From, jlib:iq_to_xml(ResIQ)); - true -> ok - end - end. - --spec check_type(type()) -> type(). - -check_type(no_queue) -> no_queue; -check_type(one_queue) -> one_queue; -check_type(N) when is_integer(N), N>0 -> N; -check_type(parallel) -> parallel. - --spec transform_module_options([{atom(), any()}]) -> [{atom(), any()}]. - -transform_module_options(Opts) -> - lists:map( - fun({iqdisc, {queues, N}}) -> - {iqdisc, N}; - (Opt) -> - Opt - end, Opts). +-spec iqdisc(binary() | global) -> no_queue. +iqdisc(_Host) -> + no_queue. %%==================================================================== -%% gen_server callbacks +%% Deprecated API %%==================================================================== +-spec add_iq_handler(module(), binary(), binary(), module(), atom(), any()) -> ok. +add_iq_handler(Component, Host, NS, Module, Function, _Type) -> + add_iq_handler(Component, Host, NS, Module, Function). -%%-------------------------------------------------------------------- -%% Function: init(Args) -> {ok, State} | -%% {ok, State, Timeout} | -%% ignore | -%% {stop, Reason} -%% Description: Initiates the server -%%-------------------------------------------------------------------- -init([Host, Module, Function]) -> - {ok, - #state{host = Host, module = Module, - function = Function}}. - -%%-------------------------------------------------------------------- -%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | -%% {reply, Reply, State, Timeout} | -%% {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, Reply, State} | -%% {stop, Reason, State} -%% Description: Handling call messages -%%-------------------------------------------------------------------- -handle_call(stop, _From, State) -> - Reply = ok, {stop, normal, Reply, State}. - -%%-------------------------------------------------------------------- -%% Function: handle_cast(Msg, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} -%% Description: Handling cast messages -%%-------------------------------------------------------------------- -handle_cast(_Msg, State) -> {noreply, State}. - -%%-------------------------------------------------------------------- -%% Function: handle_info(Info, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} -%% Description: Handling all non call/cast messages -%%-------------------------------------------------------------------- -handle_info({process_iq, From, To, IQ}, - #state{host = Host, module = Module, - function = Function} = - State) -> - process_iq(Host, Module, Function, From, To, IQ), - {noreply, State}; -handle_info(_Info, State) -> {noreply, State}. - -%%-------------------------------------------------------------------- -%% Function: terminate(Reason, State) -> void() -%% Description: This function is called by a gen_server when it is about to -%% terminate. It should be the opposite of Module:init/1 and do any necessary -%% cleaning up. When it returns, the gen_server terminates with Reason. -%% The return value is ignored. -%%-------------------------------------------------------------------- -terminate(_Reason, _State) -> ok. - -%%-------------------------------------------------------------------- -%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} -%% Description: Convert process state when code is changed -%%-------------------------------------------------------------------- -code_change(_OldVsn, State, _Extra) -> {ok, State}. - -%%-------------------------------------------------------------------- -%%% Internal functions -%%-------------------------------------------------------------------- - +-spec handle(binary(), atom(), atom(), any(), iq()) -> any(). +handle(Host, Module, Function, _Opts, IQ) -> + process_iq(Host, Module, Function, IQ). diff --git a/src/gen_mod.erl b/src/gen_mod.erl index 645e3142d..667ff5055 100644 --- a/src/gen_mod.erl +++ b/src/gen_mod.erl @@ -1,12 +1,11 @@ %%%---------------------------------------------------------------------- %%% File : gen_mod.erl %%% Author : Alexey Shchepin -%%% Purpose : %%% Purpose : %%% Created : 24 Jan 2003 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -23,81 +22,291 @@ %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- - -module(gen_mod). - +-behaviour(supervisor). -author('alexey@process-one.net'). --export([start/0, start_module/2, start_module/3, stop_module/2, - stop_module_keep_config/2, get_opt/3, get_opt/4, - get_opt_host/3, db_type/1, db_type/2, get_module_opt/5, - get_module_opt_host/3, loaded_modules/1, - loaded_modules_with_opts/1, get_hosts/2, - get_module_proc/2, is_loaded/2]). +-export([init/1, start_link/0, start_child/3, start_child/4, + stop_child/1, stop_child/2, prep_stop/0, stop/0, config_reloaded/0]). +-export([start_module/2, stop_module/2, stop_module_keep_config/2, + get_opt/2, set_opt/3, get_opt_hosts/1, is_equal_opt/3, + get_module_opt/3, get_module_opts/2, get_module_opt_hosts/2, + loaded_modules/1, loaded_modules_with_opts/1, + get_hosts/2, get_module_proc/2, is_loaded/2, is_loaded_elsewhere/2, + start_modules/0, start_modules/1, stop_modules/0, stop_modules/1, + db_mod/2, ram_db_mod/2]). +-export([validate/2]). -%%-export([behaviour_info/1]). +%% Deprecated functions +%% update_module/3 is used by test suite ONLY +-export([update_module/3]). +-deprecated([{update_module, 3}]). --include("ejabberd.hrl"). -include("logger.hrl"). +-include_lib("stdlib/include/ms_transform.hrl"). + +-include("ejabberd_commands.hrl"). -record(ejabberd_module, {module_host = {undefined, <<"">>} :: {atom(), binary()}, - opts = [] :: opts() | '_' | '$2'}). + opts = [] :: opts() | '_' | '$2', + registrations = [] :: [registration()], + order = 0 :: integer()}). --type opts() :: [{atom(), any()}]. +-type opts() :: #{atom() => term()}. +-type db_type() :: atom(). +-type opt_desc() :: #{desc => binary() | [binary()], + value => string() | binary()}. +-type opt_doc() :: {atom(), opt_desc()} | {atom(), opt_desc(), [opt_doc()]}. --callback start(binary(), opts()) -> any(). +-type component() :: ejabberd_sm | ejabberd_local. +-type registration() :: + {hook, atom(), atom(), integer()} | + {hook, atom(), atom(), integer(), binary() | global} | + {hook, atom(), module(), atom(), integer()} | + {hook_subscribe, atom(), atom(), [any()]} | + {hook_subscribe, atom(), atom(), [any()], binary() | global} | + {hook_subscribe, atom(), module(), atom(), [any()]} | + {hook_subscribe, atom(), module(), atom(), [any()], binary() | global} | + {commands, [ejabberd_commands()]} | + {iq_handler, component(), binary(), atom()} | + {iq_handler, component(), binary(), module(), atom()}. +-export_type([registration/0]). + +-callback start(binary(), opts()) -> + ok | {ok, pid()} | + {ok, [registration()]} | {error, term()}. +-callback prep_stop(binary()) -> any(). -callback stop(binary()) -> any(). +-callback reload(binary(), opts(), opts()) -> ok | {ok, pid()} | {error, term()}. +-callback mod_opt_type(atom()) -> econf:validator(). +-callback mod_options(binary()) -> [{atom(), term()} | atom()]. +-callback mod_doc() -> #{desc => binary() | [binary()], + note => string(), + opts => [opt_doc()], + example => [string()] | [{binary(), [string()]}]}. +-callback depends(binary(), opts()) -> [{module(), hard | soft}]. + +-optional_callbacks([mod_opt_type/1, reload/3, prep_stop/1]). -export_type([opts/0]). +-export_type([db_type/0]). -%%behaviour_info(callbacks) -> [{start, 2}, {stop, 1}]; -%%behaviour_info(_Other) -> undefined. +-ifndef(GEN_SERVER). +-define(GEN_SERVER, gen_server). +-endif. -start() -> +start_link() -> + case supervisor:start_link({local, ejabberd_gen_mod_sup}, ?MODULE, []) of + {ok, Pid} -> + start_modules(), + {ok, Pid}; + Err -> + Err + end. + +init([]) -> + ejabberd_hooks:add(config_reloaded, ?MODULE, config_reloaded, 60), + ejabberd_hooks:add(host_up, ?MODULE, start_modules, 40), + ejabberd_hooks:add(host_down, ?MODULE, stop_modules, 70), ets:new(ejabberd_modules, [named_table, public, - {keypos, #ejabberd_module.module_host}]), - ok. + {keypos, #ejabberd_module.module_host}, + {read_concurrency, true}]), + {ok, {{one_for_one, 10, 1}, []}}. --spec start_module(binary(), atom()) -> any(). +-spec prep_stop() -> ok. +prep_stop() -> + prep_stop_modules(). +-spec stop() -> ok. +stop() -> + ejabberd_hooks:delete(config_reloaded, ?MODULE, config_reloaded, 60), + ejabberd_hooks:delete(host_up, ?MODULE, start_modules, 40), + ejabberd_hooks:delete(host_down, ?MODULE, stop_modules, 70), + stop_modules(), + ejabberd_sup:stop_child(ejabberd_gen_mod_sup). + +-spec start_child(module(), binary(), opts()) -> {ok, pid()} | {error, any()}. +start_child(Mod, Host, Opts) -> + start_child(Mod, Host, Opts, get_module_proc(Host, Mod)). + +-spec start_child(module(), binary(), opts(), atom()) -> {ok, pid()} | {error, any()}. +start_child(Mod, Host, Opts, Proc) -> + Spec = {Proc, {?GEN_SERVER, start_link, + [{local, Proc}, Mod, [Host, Opts], + ejabberd_config:fsm_limit_opts([])]}, + transient, timer:minutes(1), worker, [Mod]}, + supervisor:start_child(ejabberd_gen_mod_sup, Spec). + +-spec stop_child(module(), binary()) -> ok | {error, any()}. +stop_child(Mod, Host) -> + stop_child(get_module_proc(Host, Mod)). + +-spec stop_child(atom()) -> ok | {error, any()}. +stop_child(Proc) -> + supervisor:terminate_child(ejabberd_gen_mod_sup, Proc), + supervisor:delete_child(ejabberd_gen_mod_sup, Proc). + +-spec start_modules() -> any(). +start_modules() -> + Hosts = ejabberd_option:hosts(), + ?INFO_MSG("Loading modules for ~ts", [misc:format_hosts_list(Hosts)]), + lists:foreach(fun start_modules/1, Hosts). + +-spec start_modules(binary()) -> ok. +start_modules(Host) -> + Modules = ejabberd_option:modules(Host), + lists:foreach( + fun({Module, Opts, Order}) -> + start_module(Host, Module, Opts, Order) + end, Modules). + +-spec start_module(binary(), atom()) -> ok | {ok, pid()} | {error, not_found_in_config}. start_module(Host, Module) -> - Modules = ejabberd_config:get_option( - {modules, Host}, - fun(L) when is_list(L) -> L end, []), + Modules = ejabberd_option:modules(Host), case lists:keyfind(Module, 1, Modules) of - {_, Opts} -> - start_module(Host, Module, Opts); + {_, Opts, Order} -> + start_module(Host, Module, Opts, Order); false -> {error, not_found_in_config} end. --spec start_module(binary(), atom(), opts()) -> any(). +-spec start_module(binary(), atom(), opts(), integer()) -> ok | {ok, pid()}. +start_module(Host, Module, Opts, Order) -> + ?DEBUG("Loading ~ts at ~ts", [Module, Host]), + store_options(Host, Module, Opts, Order), + try case Module:start(Host, Opts) of + ok -> ok; + {ok, Pid} when is_pid(Pid) -> {ok, Pid}; + {ok, Registrations} when is_list(Registrations) -> + store_options(Host, Module, Opts, Registrations, Order), + add_registrations(Host, Module, Registrations), + ok; + Err -> + ets:delete(ejabberd_modules, {Module, Host}), + erlang:error({bad_return, Module, Err}) + end + catch + Class:Reason:StackTrace -> + ets:delete(ejabberd_modules, {Module, Host}), + ErrorText = format_module_error( + Module, + start, + 2, + Opts, + Class, + Reason, + StackTrace), + ?CRITICAL_MSG(ErrorText, []), + maybe_halt_ejabberd(), + erlang:raise(Class, Reason, StackTrace) + end. -start_module(Host, Module, Opts) -> +-spec reload_modules(binary()) -> ok. +reload_modules(Host) -> + NewMods = ejabberd_option:modules(Host), + OldMods = lists:reverse(loaded_modules_with_opts(Host)), + lists:foreach( + fun({Mod, _Opts}) -> + case lists:keymember(Mod, 1, NewMods) of + false -> + stop_module(Host, Mod); + true -> + ok + end + end, OldMods), + lists:foreach( + fun({Mod, Opts, Order}) -> + case lists:keymember(Mod, 1, OldMods) of + false -> + start_module(Host, Mod, Opts, Order); + true -> + ok + end + end, NewMods), + lists:foreach( + fun({Mod, OldOpts}) -> + case lists:keyfind(Mod, 1, NewMods) of + {_, NewOpts, Order} -> + if OldOpts /= NewOpts -> + reload_module(Host, Mod, NewOpts, OldOpts, Order); + true -> + ok + end; + _ -> + ok + end + end, OldMods). + +-spec reload_module(binary(), module(), opts(), opts(), integer()) -> ok | {ok, pid()}. +reload_module(Host, Module, NewOpts, OldOpts, Order) -> + case erlang:function_exported(Module, reload, 3) of + true -> + ?DEBUG("Reloading ~ts at ~ts", [Module, Host]), + store_options(Host, Module, NewOpts, Order), + try case Module:reload(Host, NewOpts, OldOpts) of + ok -> ok; + {ok, Pid} when is_pid(Pid) -> {ok, Pid}; + Err -> erlang:error({bad_return, Module, Err}) + end + catch + Class:Reason:StackTrace -> + ErrorText = format_module_error( + Module, + reload, + 3, + NewOpts, + Class, + Reason, + StackTrace), + ?CRITICAL_MSG(ErrorText, []), + erlang:raise(Class, Reason, StackTrace) + end; + false -> + ?WARNING_MSG("Module ~ts doesn't support reloading " + "and will be restarted", [Module]), + stop_module(Host, Module), + start_module(Host, Module, NewOpts, Order) + end. + +-spec update_module(binary(), module(), opts()) -> ok | {ok, pid()}. +update_module(Host, Module, Opts) -> + case ets:lookup(ejabberd_modules, {Module, Host}) of + [#ejabberd_module{opts = OldOpts, order = Order}] -> + NewOpts = maps:merge(OldOpts, Opts), + reload_module(Host, Module, NewOpts, OldOpts, Order); + [] -> + erlang:error({module_not_loaded, Module, Host}) + end. + +-spec store_options(binary(), module(), opts(), integer()) -> true. +store_options(Host, Module, Opts, Order) -> + case ets:lookup(ejabberd_modules, {Module, Host}) of + [M] -> + store_options( + Host, Module, Opts, M#ejabberd_module.registrations, Order); + [] -> + store_options(Host, Module, Opts, [], Order) + end. + +-spec store_options(binary(), module(), opts(), [registration()], integer()) -> true. +store_options(Host, Module, Opts, Registrations, Order) -> ets:insert(ejabberd_modules, #ejabberd_module{module_host = {Module, Host}, - opts = Opts}), - try Module:start(Host, Opts) catch - Class:Reason -> - ets:delete(ejabberd_modules, {Module, Host}), - ErrorText = - io_lib:format("Problem starting the module ~p for host " - "~p ~n options: ~p~n ~p: ~p~n~p", - [Module, Host, Opts, Class, Reason, - erlang:get_stacktrace()]), - ?CRITICAL_MSG(ErrorText, []), - case is_app_running(ejabberd) of - true -> - erlang:raise(Class, Reason, erlang:get_stacktrace()); - false -> - ?CRITICAL_MSG("ejabberd initialization was aborted " - "because a module start failed.", - []), - timer:sleep(3000), - erlang:halt(string:substr(lists:flatten(ErrorText), 1, 199)) - end + opts = Opts, + registrations = Registrations, + order = Order}). + +maybe_halt_ejabberd() -> + case is_app_running(ejabberd) of + false -> + ?CRITICAL_MSG("ejabberd initialization was aborted " + "because a module start failed.", + []), + ejabberd:halt(); + true -> + ok end. is_app_running(AppName) -> @@ -105,157 +314,214 @@ is_app_running(AppName) -> lists:keymember(AppName, 1, application:which_applications(Timeout)). --spec stop_module(binary(), atom()) -> error | {aborted, any()} | {atomic, any()}. +-spec prep_stop_modules() -> ok. +prep_stop_modules() -> + lists:foreach( + fun(Host) -> + prep_stop_modules(Host) + end, ejabberd_option:hosts()). -%% @doc Stop the module in a host, and forget its configuration. +-spec prep_stop_modules(binary()) -> ok. +prep_stop_modules(Host) -> + Modules = lists:reverse(loaded_modules_with_opts(Host)), + lists:foreach( + fun({Module, _Args}) -> + prep_stop_module_keep_config(Host, Module) + end, Modules). + +-spec stop_modules() -> ok. +stop_modules() -> + lists:foreach( + fun(Host) -> + stop_modules(Host) + end, ejabberd_option:hosts()). + +-spec stop_modules(binary()) -> ok. +stop_modules(Host) -> + Modules = lists:reverse(loaded_modules_with_opts(Host)), + lists:foreach( + fun({Module, _Args}) -> + stop_module_keep_config(Host, Module) + end, Modules). + +-spec stop_module(binary(), atom()) -> error | ok. stop_module(Host, Module) -> - case stop_module_keep_config(Host, Module) of - error -> error; - ok -> ok + stop_module_keep_config(Host, Module). + +-spec prep_stop_module_keep_config(binary(), atom()) -> error | ok. +prep_stop_module_keep_config(Host, Module) -> + ?DEBUG("Preparing to stop ~ts at ~ts", [Module, Host]), + try Module:prep_stop(Host) of + _ -> + ok + catch + error:undef:_St -> + ok; + Class:Reason:StackTrace -> + ?ERROR_MSG("Failed to prepare stop module ~ts at ~ts:~n** ~ts", + [Module, + Host, + misc:format_exception(2, Class, Reason, StackTrace)]), + error end. -%% @doc Stop the module in a host, but keep its configuration. -%% As the module configuration is kept in the Mnesia local_config table, -%% when ejabberd is restarted the module will be started again. -%% This function is useful when ejabberd is being stopped -%% and it stops all modules. -spec stop_module_keep_config(binary(), atom()) -> error | ok. - stop_module_keep_config(Host, Module) -> - case catch Module:stop(Host) of - {'EXIT', Reason} -> ?ERROR_MSG("~p", [Reason]), error; - {wait, ProcList} when is_list(ProcList) -> - lists:foreach(fun wait_for_process/1, ProcList), - ets:delete(ejabberd_modules, {Module, Host}), - ok; - {wait, Process} -> - wait_for_process(Process), - ets:delete(ejabberd_modules, {Module, Host}), - ok; - _ -> ets:delete(ejabberd_modules, {Module, Host}), ok + ?DEBUG("Stopping ~ts at ~ts", [Module, Host]), + Registrations = + case ets:lookup(ejabberd_modules, {Module, Host}) of + [M] -> + M#ejabberd_module.registrations; + [] -> + [] + end, + del_registrations(Host, Module, Registrations), + try Module:stop(Host) of + _ -> + ets:delete(ejabberd_modules, {Module, Host}), + ok + catch + Class:Reason:StackTrace -> + ?ERROR_MSG("Failed to stop module ~ts at ~ts:~n** ~ts", + [Module, + Host, + misc:format_exception(2, Class, Reason, StackTrace)]), + error end. -wait_for_process(Process) -> - MonitorReference = erlang:monitor(process, Process), - wait_for_stop(Process, MonitorReference). +-spec add_registrations(binary(), module(), [registration()]) -> ok. +add_registrations(Host, Module, Registrations) -> + lists:foreach( + fun({hook, Hook, Function, Seq}) -> + ejabberd_hooks:add(Hook, Host, Module, Function, Seq); + ({hook, Hook, Function, Seq, Host1}) when is_integer(Seq) -> + ejabberd_hooks:add(Hook, Host1, Module, Function, Seq); + ({hook, Hook, Module1, Function, Seq}) when is_integer(Seq) -> + ejabberd_hooks:add(Hook, Host, Module1, Function, Seq); + ({hook_subscribe, Hook, Function, InitArg}) -> + ejabberd_hooks:subscribe(Hook, Host, Module, Function, InitArg); + ({hook_subscribe, Hook, Function, InitArg, Host1}) when is_binary(Host1) or (Host1 == global) -> + ejabberd_hooks:subscribe(Hook, Host1, Module, Function, InitArg); + ({hook_subscribe, Hook, Module1, Function, InitArg}) -> + ejabberd_hooks:subscribe(Hook, Host, Module1, Function, InitArg); + ({hook_subscribe, Hook, Module1, Function, InitArg, Host1}) -> + ejabberd_hooks:subscribe(Hook, Host1, Module1, Function, InitArg); + ({commands, Commands}) -> + ejabberd_commands:register_commands(Host, Module, Commands); + ({iq_handler, Component, NS, Function}) -> + gen_iq_handler:add_iq_handler( + Component, Host, NS, Module, Function); + ({iq_handler, Component, NS, Module1, Function}) -> + gen_iq_handler:add_iq_handler( + Component, Host, NS, Module1, Function) + end, Registrations). -wait_for_stop(Process, MonitorReference) -> - receive - {'DOWN', MonitorReference, _Type, _Object, _Info} -> ok - after 5000 -> - catch exit(whereis(Process), kill), - wait_for_stop1(MonitorReference) +-spec del_registrations(binary(), module(), [registration()]) -> ok. +del_registrations(Host, Module, Registrations) -> + lists:foreach( + fun({hook, Hook, Function, Seq}) -> + ejabberd_hooks:delete(Hook, Host, Module, Function, Seq); + ({hook, Hook, Function, Seq, Host1}) when is_integer(Seq) -> + ejabberd_hooks:delete(Hook, Host1, Module, Function, Seq); + ({hook, Hook, Module1, Function, Seq}) when is_integer(Seq) -> + ejabberd_hooks:delete(Hook, Host, Module1, Function, Seq); + ({hook_subscribe, Hook, Function, InitArg}) -> + ejabberd_hooks:unsubscribe(Hook, Host, Module, Function, InitArg); + ({hook_subscribe, Hook, Function, InitArg, Host1}) when is_binary(Host1) or (Host1 == global) -> + ejabberd_hooks:unsubscribe(Hook, Host1, Module, Function, InitArg); + ({hook_subscribe, Hook, Module1, Function, InitArg}) -> + ejabberd_hooks:unsubscribe(Hook, Host, Module1, Function, InitArg); + ({hook_subscribe, Hook, Module1, Function, InitArg, Host1}) -> + ejabberd_hooks:unsubscribe(Hook, Host1, Module1, Function, InitArg); + ({commands, Commands}) -> + ejabberd_commands:unregister_commands(Host, Module, Commands); + ({iq_handler, Component, NS, _Function}) -> + gen_iq_handler:remove_iq_handler(Component, Host, NS); + ({iq_handler, Component, NS, _Module, _Function}) -> + gen_iq_handler:remove_iq_handler(Component, Host, NS) + end, Registrations). + +-spec get_opt(atom(), opts()) -> any(). +get_opt(Opt, Opts) -> + maps:get(Opt, Opts). + +-spec set_opt(atom(), term(), opts()) -> opts(). +set_opt(Opt, Val, Opts) -> + maps:put(Opt, Val, Opts). + +-spec get_module_opt(global | binary(), atom(), atom()) -> any(). +get_module_opt(global, Module, Opt) -> + get_module_opt(ejabberd_config:get_myname(), Module, Opt); +get_module_opt(Host, Module, Opt) -> + Opts = get_module_opts(Host, Module), + get_opt(Opt, Opts). + +-spec get_module_opt_hosts(binary(), module()) -> [binary()]. +get_module_opt_hosts(Host, Module) -> + Opts = get_module_opts(Host, Module), + get_opt_hosts(Opts). + +-spec get_opt_hosts(opts()) -> [binary()]. +get_opt_hosts(Opts) -> + case get_opt(hosts, Opts) of + L when L == [] orelse L == undefined -> + [get_opt(host, Opts)]; + L -> + L end. -wait_for_stop1(MonitorReference) -> - receive - {'DOWN', MonitorReference, _Type, _Object, _Info} -> ok - after 5000 -> ok +-spec get_module_opts(binary(), module()) -> opts(). +get_module_opts(Host, Module) -> + try ets:lookup_element(ejabberd_modules, {Module, Host}, 3) + catch _:badarg -> erlang:error({module_not_loaded, Module, Host}) end. --type check_fun() :: fun((any()) -> any()) | {module(), atom()}. +-spec db_mod(binary() | global | db_type() | opts(), module()) -> module(). +db_mod(T, M) -> + db_mod(db_type, T, M). --spec get_opt(atom(), opts(), check_fun()) -> any(). +-spec ram_db_mod(binary() | global | db_type() | opts(), module()) -> module(). +ram_db_mod(T, M) -> + db_mod(ram_db_type, T, M). -get_opt(Opt, Opts, F) -> - get_opt(Opt, Opts, F, undefined). - --spec get_opt(atom(), opts(), check_fun(), any()) -> any(). - -get_opt(Opt, Opts, F, Default) -> - case lists:keysearch(Opt, 1, Opts) of - false -> - Default; - {value, {_, Val}} -> - ejabberd_config:prepare_opt_val(Opt, Val, F, Default) - end. - --spec get_module_opt(global | binary(), atom(), atom(), check_fun(), any()) -> any(). - -get_module_opt(global, Module, Opt, F, Default) -> - Hosts = (?MYHOSTS), - [Value | Values] = lists:map(fun (Host) -> - get_module_opt(Host, Module, Opt, - F, Default) - end, - Hosts), - Same_all = lists:all(fun (Other_value) -> - Other_value == Value - end, - Values), - case Same_all of - true -> Value; - false -> Default - end; -get_module_opt(Host, Module, Opt, F, Default) -> - OptsList = ets:lookup(ejabberd_modules, {Module, Host}), - case OptsList of - [] -> Default; - [#ejabberd_module{opts = Opts} | _] -> - get_opt(Opt, Opts, F, Default) - end. - --spec get_module_opt_host(global | binary(), atom(), binary()) -> binary(). - -get_module_opt_host(Host, Module, Default) -> - Val = get_module_opt(Host, Module, host, - fun iolist_to_binary/1, - Default), - ejabberd_regexp:greplace(Val, <<"@HOST@">>, Host). - --spec get_opt_host(binary(), opts(), binary()) -> binary(). - -get_opt_host(Host, Opts, Default) -> - Val = get_opt(host, Opts, fun iolist_to_binary/1, Default), - ejabberd_regexp:greplace(Val, <<"@HOST@">>, Host). - --spec db_type(opts()) -> odbc | mnesia | riak. - -db_type(Opts) -> - get_opt(db_type, Opts, - fun(odbc) -> odbc; - (internal) -> mnesia; - (mnesia) -> mnesia; - (riak) -> riak - end, - mnesia). - --spec db_type(binary(), atom()) -> odbc | mnesia | riak. - -db_type(Host, Module) -> - get_module_opt(Host, Module, db_type, - fun(odbc) -> odbc; - (internal) -> mnesia; - (mnesia) -> mnesia; - (riak) -> riak - end, - mnesia). +-spec db_mod(db_type | ram_db_type, + binary() | global | db_type() | opts(), module()) -> module(). +db_mod(Opt, Host, Module) when is_binary(Host) orelse Host == global -> + db_mod(Opt, get_module_opt(Host, Module, Opt), Module); +db_mod(Opt, Opts, Module) when is_map(Opts) -> + db_mod(Opt, get_opt(Opt, Opts), Module); +db_mod(_Opt, Type, Module) when is_atom(Type) -> + list_to_existing_atom(atom_to_list(Module) ++ "_" ++ atom_to_list(Type)). -spec loaded_modules(binary()) -> [atom()]. - loaded_modules(Host) -> - ets:select(ejabberd_modules, - [{#ejabberd_module{_ = '_', module_host = {'$1', Host}}, - [], ['$1']}]). + Mods = ets:select( + ejabberd_modules, + ets:fun2ms( + fun(#ejabberd_module{module_host = {Mod, H}, + order = Order}) when H == Host -> + {Mod, Order} + end)), + [Mod || {Mod, _} <- lists:keysort(2, Mods)]. -spec loaded_modules_with_opts(binary()) -> [{atom(), opts()}]. - loaded_modules_with_opts(Host) -> - ets:select(ejabberd_modules, - [{#ejabberd_module{_ = '_', module_host = {'$1', Host}, - opts = '$2'}, - [], [{{'$1', '$2'}}]}]). + Mods = ets:select( + ejabberd_modules, + ets:fun2ms( + fun(#ejabberd_module{module_host = {Mod, H}, opts = Opts, + order = Order}) when H == Host -> + {Mod, Opts, Order} + end)), + [{Mod, Opts} || {Mod, Opts, _} <- lists:keysort(3, Mods)]. -spec get_hosts(opts(), binary()) -> [binary()]. - get_hosts(Opts, Prefix) -> - case get_opt(hosts, Opts, - fun(Hs) -> [iolist_to_binary(H) || H <- Hs] end) of + case get_opt(hosts, Opts) of undefined -> - case get_opt(host, Opts, - fun iolist_to_binary/1) of + case get_opt(host, Opts) of undefined -> - [<> || Host <- ?MYHOSTS]; + [<> || Host <- ejabberd_option:hosts()]; Host -> [Host] end; @@ -263,16 +529,207 @@ get_hosts(Opts, Prefix) -> Hosts end. --spec get_module_proc(binary(), {frontend, atom()} | atom()) -> atom(). - -get_module_proc(Host, {frontend, Base}) -> - get_module_proc(<<"frontend_", Host/binary>>, Base); +-spec get_module_proc(binary() | global, atom()) -> atom(). +get_module_proc(global, Base) -> + get_module_proc(<<"global">>, Base); get_module_proc(Host, Base) -> binary_to_atom( <<(erlang:atom_to_binary(Base, latin1))/binary, "_", Host/binary>>, latin1). -spec is_loaded(binary(), atom()) -> boolean(). - is_loaded(Host, Module) -> ets:member(ejabberd_modules, {Module, Host}). + +-spec is_loaded_elsewhere(binary(), atom()) -> boolean(). +is_loaded_elsewhere(Host, Module) -> + ets:select_count( + ejabberd_modules, + ets:fun2ms( + fun(#ejabberd_module{module_host = {Mod, H}}) -> + (Mod == Module) and (H /= Host) + end)) /= 0. + +-spec config_reloaded() -> ok. +config_reloaded() -> + lists:foreach(fun reload_modules/1, ejabberd_option:hosts()). + +-spec is_equal_opt(atom(), opts(), opts()) -> true | {false, any(), any()}. +is_equal_opt(Opt, NewOpts, OldOpts) -> + NewVal = get_opt(Opt, NewOpts), + OldVal = get_opt(Opt, OldOpts), + if NewVal /= OldVal -> + {false, NewVal, OldVal}; + true -> + true + end. + +%%%=================================================================== +%%% Formatters +%%%=================================================================== +-spec format_module_error(atom(), start | reload, non_neg_integer(), opts(), + error | exit | throw, any(), + [tuple()]) -> iolist(). +format_module_error(Module, Fun, Arity, Opts, Class, Reason, St) -> + case {Class, Reason} of + {error, {bad_return, Module, {error, _} = Err}} -> + io_lib:format("Failed to ~ts module ~ts: ~ts", + [Fun, Module, misc:format_val(Err)]); + {error, {bad_return, Module, Ret}} -> + io_lib:format("Module ~ts returned unexpected value from ~ts/~B:~n" + "** Error: ~p~n" + "** Hint: this is either not an ejabberd module " + "or it implements ejabberd API incorrectly", + [Module, Fun, Arity, Ret]); + _ -> + io_lib:format("Internal error of module ~ts has " + "occurred during ~ts:~n" + "** Options: ~p~n" + "** ~ts", + [Module, Fun, Opts, + misc:format_exception(2, Class, Reason, St)]) + end. + +%%%=================================================================== +%%% Validation +%%%=================================================================== +-spec validator(binary()) -> econf:validator(). +validator(Host) -> + econf:options( + #{modules => + econf:and_then( + econf:map( + econf:beam([{start, 2}, {stop, 1}, + {mod_options, 1}, {depends, 2}]), + econf:options( + #{db_type => econf:atom(), + ram_db_type => econf:atom(), + '_' => econf:any()})), + fun(L) -> + Validators = maps:from_list( + lists:map( + fun({Mod, Opts}) -> + {Mod, validator(Host, Mod, Opts)} + end, L)), + Validator = econf:options(Validators, [unique]), + Validator(L) + end)}). + +-spec validator(binary(), module(), [{atom(), term()}]) -> econf:validator(). +validator(Host, Module, Opts) -> + {Required, {DefaultOpts1, Validators}} = + lists:mapfoldl( + fun({M, DefOpts}, {DAcc, VAcc}) -> + lists:mapfoldl( + fun({Opt, Def}, {DAcc1, VAcc1}) -> + {[], {DAcc1#{Opt => Def}, + VAcc1#{Opt => get_opt_type(Host, Module, M, Opt)}}}; + (Opt, {DAcc1, VAcc1}) -> + {[Opt], {DAcc1, + VAcc1#{Opt => get_opt_type(Host, Module, M, Opt)}}} + end, {DAcc, VAcc}, DefOpts) + end, {#{}, #{}}, get_defaults(Host, Module, Opts)), + econf:and_then( + econf:options( + Validators, + [{required, lists:usort(lists:flatten(Required))}, + {return, map}, unique]), + fun(Opts1) -> + maps:merge(DefaultOpts1, Opts1) + end). + +-spec validate(binary(), [{module(), opts()}]) -> + {ok, [{module(), opts(), integer()}]} | + econf:error_return(). +validate(Host, ModOpts) -> + case econf:validate(validator(Host), [{modules, ModOpts}]) of + {ok, [{modules, ModOpts1}]} -> + try sort_modules(Host, ModOpts1) + catch throw:{?MODULE, Reason} -> + {error, Reason, [modules]} + end; + {error, _, _} = Err -> + Err + end. + +-spec get_defaults(binary(), module(), [{atom(), term()}]) -> + [{module(), [{atom(), term()} | atom()]}]. +get_defaults(Host, Module, Opts) -> + DefaultOpts = Module:mod_options(Host), + [{Module, DefaultOpts}| + lists:filtermap( + fun({Opt, T1}) when Opt == db_type; Opt == ram_db_type -> + T2 = proplists:get_value(Opt, Opts, T1), + DBMod = list_to_atom(atom_to_list(Module) ++ "_" ++ atom_to_list(T2)), + case code:ensure_loaded(DBMod) of + {module, _} -> + case erlang:function_exported(DBMod, mod_options, 1) of + true -> + {true, {DBMod, DBMod:mod_options(Host)}}; + false -> + false + end; + _ -> + false + end; + (_) -> + false + end, DefaultOpts)]. + +-spec get_opt_type(binary(), module(), module(), atom()) -> econf:validator(). +get_opt_type(Host, Mod, SubMod, Opt) -> + Type = try SubMod:mod_opt_type(Opt) + catch _:_ -> Mod:mod_opt_type(Opt) + end, + econf:and_then( + fun(B) -> + ejabberd_config:replace_keywords(Host, B) + end, + Type). + +-spec sort_modules(binary(), [{module(), opts()}]) -> {ok, [{module(), opts(), integer()}]}. +sort_modules(Host, ModOpts) -> + G = digraph:new([acyclic]), + lists:foreach( + fun({Mod, Opts}) -> + digraph:add_vertex(G, Mod, Opts), + Deps = Mod:depends(Host, Opts), + lists:foreach( + fun({DepMod, Type}) -> + case lists:keyfind(DepMod, 1, ModOpts) of + false when Type == hard -> + throw({?MODULE, {missing_module_dep, Mod, DepMod}}); + false when Type == soft -> + warn_soft_dep_fail(DepMod, Mod); + {DepMod, DepOpts} -> + digraph:add_vertex(G, DepMod, DepOpts), + case digraph:add_edge(G, DepMod, Mod) of + {error, {bad_edge, Path}} -> + warn_cyclic_dep(Path); + _ -> + ok + end + end + end, Deps) + end, ModOpts), + {Result, _} = lists:mapfoldl( + fun(V, Order) -> + {M, O} = digraph:vertex(G, V), + {{M, O, Order}, Order+1} + end, 1, digraph_utils:topsort(G)), + digraph:delete(G), + {ok, Result}. + +-spec warn_soft_dep_fail(module(), module()) -> ok. +warn_soft_dep_fail(DepMod, Mod) -> + ?WARNING_MSG("Module ~ts is recommended for module " + "~ts but is not found in the config", + [DepMod, Mod]). + +-spec warn_cyclic_dep([module()]) -> ok. +warn_cyclic_dep(Path) -> + ?WARNING_MSG("Cyclic dependency detected between modules ~ts. " + "This is either a bug, or the modules are not " + "supposed to work together in this configuration. " + "The modules will still be loaded though", + [misc:format_cycle(Path)]). diff --git a/src/gen_pubsub_node.erl b/src/gen_pubsub_node.erl index da8cd6a0e..48b43a05b 100644 --- a/src/gen_pubsub_node.erl +++ b/src/gen_pubsub_node.erl @@ -1,267 +1,229 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. -%%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. -%%% -%%% The Initial Developer of the Original Code is ProcessOne. -%%% Portions created by ProcessOne are Copyright 2006-2015, ProcessOne -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2015, ProcessOne. +%%%---------------------------------------------------------------------- +%%% File : gen_pubsub_node.erl +%%% Author : Christophe Romain +%%% Purpose : Define pubsub plugin behaviour +%%% Created : 1 Dec 2007 by Christophe Romain %%% %%% -%%% @copyright 2006-2015 ProcessOne -%%% @author Christophe Romain -%%% [http://www.process-one.net/] -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== - -%%% @private -%%% @doc

The module {@module} defines the PubSub node -%%% plugin behaviour. This behaviour is used to check that a PubSub plugin -%%% respects the current ejabberd PubSub plugin API.

+%%% ejabberd, Copyright (C) 2002-2025 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(gen_pubsub_node). --include("jlib.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). --type(host() :: mod_pubsub:host() - | mod_pubsub_odbc:host() -). - --type(nodeId() :: mod_pubsub:nodeId() - | mod_pubsub_odbc:nodeId() -). - --type(nodeIdx() :: mod_pubsub:nodeIdx() - | mod_pubsub_odbc:nodeIdx() -). - --type(itemId() :: mod_pubsub:itemId() - | mod_pubsub_odbc:itemId() -). - --type(pubsubNode() :: mod_pubsub:pubsubNode() - | mod_pubsub_odbc:pubsubNode() -). - --type(pubsubState() :: mod_pubsub:pubsubState() - | mod_pubsub_odbc:pubsubState() -). - --type(pubsubItem() :: mod_pubsub:pubsubItem() - | mod_pubsub_odbc:pubsubItem() -). - --type(nodeOptions() :: mod_pubsub:nodeOptions() - | mod_pubsub_odbc:nodeOptions() -). - --type(subOptions() :: mod_pubsub:subOptions() - | mod_pubsub_odbc:subOptions() -). - --type(affiliation() :: mod_pubsub:affiliation() - | mod_pubsub_odbc:affiliation() -). - --type(subscription() :: mod_pubsub:subscription() - | mod_pubsub_odbc:subscription() -). - --type(subId() :: mod_pubsub:subId() - | mod_pubsub_odbc:subId() -). - --type(accessModel() :: mod_pubsub:accessModel() - | mod_pubsub_odbc:accessModel() -). - --type(publishModel() :: mod_pubsub:publishModel() - | mod_pubsub_odbc:publishModel() -). - --type(payload() :: mod_pubsub:payload() - | mod_pubsub_odbc:payload() -). +-type(host() :: mod_pubsub:host()). +-type(nodeId() :: mod_pubsub:nodeId()). +-type(nodeIdx() :: mod_pubsub:nodeIdx()). +-type(itemId() :: mod_pubsub:itemId()). +-type(pubsubNode() :: mod_pubsub:pubsubNode()). +-type(pubsubState() :: mod_pubsub:pubsubState()). +-type(pubsubItem() :: mod_pubsub:pubsubItem()). +-type(subOptions() :: mod_pubsub:subOptions()). +-type(pubOptions() :: mod_pubsub:pubOptions()). +-type(affiliation() :: mod_pubsub:affiliation()). +-type(subscription() :: mod_pubsub:subscription()). +-type(subId() :: mod_pubsub:subId()). +-type(accessModel() :: mod_pubsub:accessModel()). +-type(publishModel() :: mod_pubsub:publishModel()). +-type(payload() :: mod_pubsub:payload()). -callback init(Host :: binary(), - ServerHost :: binary(), - Opts :: [any()]) -> atom(). + ServerHost :: binary(), + Opts :: [any()]) -> atom(). -callback terminate(Host :: host(), - ServerHost :: binary()) -> atom(). + ServerHost :: binary()) -> atom(). -callback options() -> [{atom(), any()}]. -callback features() -> [binary()]. -callback create_node_permission(Host :: host(), - ServerHost :: binary(), - Node :: nodeId(), - ParentNode :: nodeId(), - Owner :: jid(), Access :: atom()) -> + ServerHost :: binary(), + Node :: nodeId(), + ParentNode :: nodeId(), + Owner :: jid(), Access :: atom()) -> {result, boolean()}. -callback create_node(NodeIdx :: nodeIdx(), - Owner :: jid()) -> + Owner :: jid()) -> {result, {default, broadcast}}. -callback delete_node(Nodes :: [pubsubNode(),...]) -> {result, - {default, broadcast, - [{pubsubNode(), - [{ljid(), [{subscription(), subId()}]},...]},...] - } - } + {default, broadcast, + [{pubsubNode(), + [{ljid(), [{subscription(), subId()}]},...]},...] + } + } | {result, - {[], - [{pubsubNode(), - [{ljid(), [{subscription(), subId()}]},...]},...] - } - }. + {[], + [{pubsubNode(), + [{ljid(), [{subscription(), subId()}]},...]},...] + } + }. -callback purge_node(NodeIdx :: nodeIdx(), - Owner :: jid()) -> + Owner :: jid()) -> {result, {default, broadcast}} | - {error, xmlel()}. + {error, stanza_error()}. -callback subscribe_node(NodeIdx :: nodeIdx(), - Sender :: jid(), - Subscriber :: ljid(), - AccessModel :: accessModel(), - SendLast :: 'never' | 'on_sub' | 'on_sub_and_presence', - PresenceSubscription :: boolean(), - RosterGroup :: boolean(), - Options :: subOptions()) -> + Sender :: jid(), + Subscriber :: jid(), + AccessModel :: accessModel(), + SendLast :: 'never' | 'on_sub' | 'on_sub_and_presence', + PresenceSubscription :: boolean(), + RosterGroup :: boolean(), + Options :: subOptions()) -> {result, {default, subscribed, subId()}} | {result, {default, subscribed, subId(), send_last}} | {result, {default, pending, subId()}} | - {error, xmlel()}. + {error, stanza_error()}. -callback unsubscribe_node(NodeIdx :: nodeIdx(), - Sender :: jid(), - Subscriber :: ljid(), - SubId :: subId()) -> - {result, default} | - {error, xmlel()}. + Sender :: jid(), + Subscriber :: jid(), + SubId :: subId()) -> + {result, []} | + {error, stanza_error()}. -callback publish_item(NodeId :: nodeIdx(), - Publisher :: jid(), - PublishModel :: publishModel(), - Max_Items :: non_neg_integer(), - ItemId :: <<>> | itemId(), - Payload :: payload()) -> + Publisher :: jid(), + PublishModel :: publishModel(), + Max_Items :: non_neg_integer(), + ItemId :: <<>> | itemId(), + Payload :: payload(), + Options :: pubOptions()) -> {result, {default, broadcast, [itemId()]}} | - {error, xmlel()}. + {error, stanza_error()}. -callback delete_item(NodeIdx :: nodeIdx(), - Publisher :: jid(), - PublishModel :: publishModel(), - ItemId :: <<>> | itemId()) -> + Publisher :: jid(), + PublishModel :: publishModel(), + ItemId :: <<>> | itemId()) -> {result, {default, broadcast}} | - {error, xmlel()}. + {error, stanza_error()}. -callback remove_extra_items(NodeIdx :: nodeIdx(), - Max_Items :: unlimited | non_neg_integer(), - ItemIds :: [itemId()]) -> + Max_Items :: unlimited | non_neg_integer()) -> {result, {[itemId()], [itemId()]} - }. + }. + +-callback remove_extra_items(NodeIdx :: nodeIdx(), + Max_Items :: unlimited | non_neg_integer(), + ItemIds :: [itemId()]) -> + {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()}]}. -callback get_entity_affiliations(Host :: host(), - Owner :: jid()) -> + Owner :: jid()) -> {result, [{pubsubNode(), affiliation()}]}. -callback get_affiliation(NodeIdx :: nodeIdx(), - Owner :: jid()) -> + Owner :: jid()) -> {result, affiliation()}. -callback set_affiliation(NodeIdx :: nodeIdx(), - Owner :: ljid(), - Affiliation :: affiliation()) -> - ok | - {error, xmlel()}. + Owner :: jid(), + Affiliation :: affiliation()) -> + {result, ok} | + {error, stanza_error()}. -callback get_node_subscriptions(NodeIdx :: nodeIdx()) -> {result, - [{ljid(), subscription(), subId()}] | - [{ljid(), none},...] - }. + [{ljid(), subscription(), subId()}] | + [{ljid(), none},...] + }. -callback get_entity_subscriptions(Host :: host(), - Owner :: jid()) -> + Key :: jid()) -> {result, [{pubsubNode(), subscription(), subId(), ljid()}] - }. + }. -callback get_subscriptions(NodeIdx :: nodeIdx(), - Owner :: ljid()) -> + Owner :: jid()) -> {result, [{subscription(), subId()}]}. -callback get_pending_nodes(Host :: host(), - Owner :: jid()) -> + Owner :: jid()) -> {result, [nodeId()]}. -callback get_states(NodeIdx::nodeIdx()) -> {result, [pubsubState()]}. -callback get_state(NodeIdx :: nodeIdx(), - JID :: ljid()) -> + Key :: ljid()) -> pubsubState(). -callback set_state(State::pubsubState()) -> ok | - {error, xmlel()}. + {error, stanza_error()}. --callback get_items(NodeIdx :: nodeIdx(), - JID :: jid(), - AccessModel :: accessModel(), - Presence_Subscription :: boolean(), - RosterGroup :: boolean(), - SubId :: subId()) -> - {result, [pubsubItem()]} | - {error, xmlel()}. +-callback get_items(nodeIdx(), jid(), accessModel(), + boolean(), boolean(), binary(), + undefined | rsm_set()) -> + {result, {[pubsubItem()], undefined | rsm_set()}} | {error, stanza_error()}. --callback get_items(NodeIdx :: nodeIdx(), - From :: jid()) -> +-callback get_items(nodeIdx(), jid(), undefined | rsm_set()) -> + {result, {[pubsubItem()], undefined | rsm_set()}}. + +-callback get_last_items(nodeIdx(), jid(), undefined | rsm_set()) -> + {result, [pubsubItem()]}. + +-callback get_only_item(nodeIdx(), jid()) -> {result, [pubsubItem()]}. -callback get_item(NodeIdx :: nodeIdx(), - ItemId :: itemId(), - JID :: jid(), - AccessModel :: accessModel(), - PresenceSubscription :: boolean(), - RosterGroup :: boolean(), - SubId :: subId()) -> + ItemId :: itemId(), + JID :: jid(), + AccessModel :: accessModel(), + PresenceSubscription :: boolean(), + RosterGroup :: boolean(), + SubId :: subId()) -> {result, pubsubItem()} | - {error, xmlel()}. + {error, stanza_error()}. -callback get_item(NodeIdx :: nodeIdx(), - ItemId :: itemId()) -> + ItemId :: itemId()) -> {result, pubsubItem()} | - {error, xmlel()}. + {error, stanza_error()}. -callback set_item(Item :: pubsubItem()) -> ok. % | {error, _}. -callback get_item_name(Host :: host(), - ServerHost :: binary(), - Node :: nodeId()) -> - itemId(). + ServerHost :: binary(), + Node :: nodeId()) -> + {result, itemId()}. -callback node_to_path(Node :: nodeId()) -> - [nodeId()]. + {result, [nodeId()]}. -callback path_to_node(Node :: [nodeId()]) -> - nodeId(). + {result, nodeId()}. diff --git a/src/gen_pubsub_nodetree.erl b/src/gen_pubsub_nodetree.erl index 8cbe1d3df..6d958ae62 100644 --- a/src/gen_pubsub_nodetree.erl +++ b/src/gen_pubsub_nodetree.erl @@ -1,124 +1,106 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. -%%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. -%%% -%%% The Initial Developer of the Original Code is ProcessOne. -%%% Portions created by ProcessOne are Copyright 2006-2015, ProcessOne -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2015, ProcessOne. +%%%---------------------------------------------------------------------- +%%% File : gen_pubsub_nodetree.erl +%%% Author : Christophe Romain +%%% Purpose : Define the pubsub node tree plugin behaviour +%%% Created : 1 Dec 2007 by Christophe Romain %%% %%% -%%% @copyright 2006-2015 ProcessOne -%%% @author Christophe Romain -%%% [http://www.process-one.net/] -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== - -%%% @private -%%% @doc

The module {@module} defines the PubSub node -%%% tree plugin behaviour. This behaviour is used to check that a PubSub -%%% node tree plugin respects the current ejabberd PubSub plugin API.

+%%% ejabberd, Copyright (C) 2002-2025 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(gen_pubsub_nodetree). --include("jlib.hrl"). --type(host() :: mod_pubsub:host() - | mod_pubsub_odbc:host() -). - --type(nodeId() :: mod_pubsub:nodeId() - | mod_pubsub_odbc:nodeId() -). - --type(nodeIdx() :: mod_pubsub:nodeIdx() - | mod_pubsub_odbc:nodeIdx() -). - --type(itemId() :: mod_pubsub:itemId() - | mod_pubsub_odbc:itemId() -). - --type(pubsubNode() :: mod_pubsub:pubsubNode() - | mod_pubsub_odbc:pubsubNode() -). - --type(nodeOptions() :: mod_pubsub:nodeOptions() - | mod_pubsub_odbc:nodeOptions() -). +-type(host() :: mod_pubsub:host()). +-type(nodeId() :: mod_pubsub:nodeId()). +-type(nodeIdx() :: mod_pubsub:nodeIdx()). +-type(pubsubNode() :: mod_pubsub:pubsubNode()). +-type(nodeOptions() :: mod_pubsub:nodeOptions()). -callback init(Host :: host(), - ServerHost :: binary(), - Opts :: [any()]) -> atom(). + ServerHost :: binary(), + Opts :: [any()]) -> atom(). + +-include_lib("xmpp/include/xmpp.hrl"). -callback terminate(Host :: host(), ServerHost :: binary()) -> atom(). -callback options() -> nodeOptions(). -callback set_node(PubsubNode :: pubsubNode()) -> - ok | {result, NodeIdx::mod_pubsub_odbc:nodeIdx()} | {error, xmlel()}. + ok | {result, NodeIdx::nodeIdx()} | {error, stanza_error()}. -callback get_node(Host :: host(), - NodeId :: nodeId(), - From :: jid()) -> + NodeId :: nodeId(), + From :: jid:jid()) -> pubsubNode() | - {error, xmlel()}. + {error, stanza_error()}. -callback get_node(Host :: host(), - NodeId :: nodeId()) -> + NodeId :: nodeId()) -> pubsubNode() | - {error, xmlel()}. + {error, stanza_error()}. -callback get_node(NodeIdx :: nodeIdx()) -> pubsubNode() | - {error, xmlel()}. + {error, stanza_error()}. -callback get_nodes(Host :: host(), - From :: jid())-> + Limit :: non_neg_integer() | infinity)-> [pubsubNode()]. -callback get_nodes(Host :: host())-> [pubsubNode()]. +-callback get_all_nodes(Host :: host()) -> + [pubsubNode()]. + -callback get_parentnodes(Host :: host(), - NodeId :: nodeId(), - From :: jid()) -> + NodeId :: nodeId(), + From :: jid:jid()) -> [pubsubNode()] | - {error, xmlel()}. + {error, stanza_error()}. -callback get_parentnodes_tree(Host :: host(), - NodeId :: nodeId(), - From :: jid()) -> + NodeId :: nodeId(), + From :: jid:jid()) -> [{0, [pubsubNode(),...]}]. -callback get_subnodes(Host :: host(), - NodeId :: nodeId(), - From :: ljid()) -> + NodeId :: nodeId(), + Limit :: non_neg_integer() | infinity) -> [pubsubNode()]. -callback get_subnodes_tree(Host :: host(), - NodeId :: nodeId(), - From :: ljid()) -> + NodeId :: nodeId(), + From :: jid:jid()) -> [pubsubNode()]. -callback create_node(Host :: host(), - NodeId :: nodeId(), - Type :: binary(), - Owner :: jid(), - Options :: nodeOptions(), - Parents :: [nodeId()]) -> + NodeId :: nodeId(), + Type :: binary(), + Owner :: jid:jid(), + Options :: nodeOptions(), + Parents :: [nodeId()]) -> {ok, NodeIdx::nodeIdx()} | - {error, xmlel()}. + {error, stanza_error()} | + {error, {virtual, {host(), nodeId()} | nodeId()}}. -callback delete_node(Host :: host(), - NodeId :: nodeId()) -> + NodeId :: nodeId()) -> [pubsubNode()]. diff --git a/src/idna.erl b/src/idna.erl deleted file mode 100644 index dd19ad988..000000000 --- a/src/idna.erl +++ /dev/null @@ -1,224 +0,0 @@ -%%%---------------------------------------------------------------------- -%%% File : idna.erl -%%% Author : Alexey Shchepin -%%% Purpose : Support for IDNA (RFC3490) -%%% Created : 10 Apr 2004 by Alexey Shchepin -%%% -%%% -%%% ejabberd, Copyright (C) 2002-2015 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(idna). - --author('alexey@process-one.net'). - --export([domain_utf8_to_ascii/1, - domain_ucs2_to_ascii/1, - utf8_to_ucs2/1]). - --ifdef(TEST). --include_lib("eunit/include/eunit.hrl"). --endif. - --spec domain_utf8_to_ascii(binary()) -> false | binary(). - -domain_utf8_to_ascii(Domain) -> - domain_ucs2_to_ascii(utf8_to_ucs2(Domain)). - -utf8_to_ucs2(S) -> - utf8_to_ucs2(binary_to_list(S), ""). - -utf8_to_ucs2([], R) -> lists:reverse(R); -utf8_to_ucs2([C | S], R) when C < 128 -> - utf8_to_ucs2(S, [C | R]); -utf8_to_ucs2([C1, C2 | S], R) when C1 < 224 -> - utf8_to_ucs2(S, [C1 band 31 bsl 6 bor C2 band 63 | R]); -utf8_to_ucs2([C1, C2, C3 | S], R) when C1 < 240 -> - utf8_to_ucs2(S, - [C1 band 15 bsl 12 bor (C2 band 63 bsl 6) bor C3 band 63 - | R]). - --spec domain_ucs2_to_ascii(list()) -> false | binary(). - -domain_ucs2_to_ascii(Domain) -> - case catch domain_ucs2_to_ascii1(Domain) of - {'EXIT', _Reason} -> false; - Res -> iolist_to_binary(Res) - end. - -domain_ucs2_to_ascii1(Domain) -> - Parts = string:tokens(Domain, - [46, 12290, 65294, 65377]), - ASCIIParts = lists:map(fun (P) -> to_ascii(P) end, - Parts), - string:strip(lists:flatmap(fun (P) -> [$. | P] end, - ASCIIParts), - left, $.). - -%% Domain names are already nameprep'ed in ejabberd, so we skiping this step -to_ascii(Name) -> - false = lists:any(fun (C) - when (0 =< C) and (C =< 44) or - (46 =< C) and (C =< 47) - or (58 =< C) and (C =< 64) - or (91 =< C) and (C =< 96) - or (123 =< C) and (C =< 127) -> - true; - (_) -> false - end, - Name), - case Name of - [H | _] when H /= $- -> true = lists:last(Name) /= $- - end, - ASCIIName = case lists:any(fun (C) -> C > 127 end, Name) - of - true -> - true = case Name of - "xn--" ++ _ -> false; - _ -> true - end, - "xn--" ++ punycode_encode(Name); - false -> Name - end, - L = length(ASCIIName), - true = (1 =< L) and (L =< 63), - ASCIIName. - -%%% PUNYCODE (RFC3492) - --define(BASE, 36). - --define(TMIN, 1). - --define(TMAX, 26). - --define(SKEW, 38). - --define(DAMP, 700). - --define(INITIAL_BIAS, 72). - --define(INITIAL_N, 128). - -punycode_encode(Input) -> - N = (?INITIAL_N), - Delta = 0, - Bias = (?INITIAL_BIAS), - Basic = lists:filter(fun (C) -> C =< 127 end, Input), - NonBasic = lists:filter(fun (C) -> C > 127 end, Input), - L = length(Input), - B = length(Basic), - SNonBasic = lists:usort(NonBasic), - Output1 = if B > 0 -> Basic ++ "-"; - true -> "" - end, - Output2 = punycode_encode1(Input, SNonBasic, B, B, L, N, - Delta, Bias, ""), - Output1 ++ Output2. - -punycode_encode1(Input, [M | SNonBasic], B, H, L, N, - Delta, Bias, Out) - when H < L -> - Delta1 = Delta + (M - N) * (H + 1), - % let n = m - {NewDelta, NewBias, NewH, NewOut} = lists:foldl(fun (C, - {ADelta, ABias, AH, - AOut}) -> - if C < M -> - {ADelta + 1, - ABias, AH, - AOut}; - C == M -> - NewOut = - punycode_encode_delta(ADelta, - ABias, - AOut), - NewBias = - adapt(ADelta, - H + - 1, - H - == - B), - {0, NewBias, - AH + 1, - NewOut}; - true -> - {ADelta, - ABias, AH, - AOut} - end - end, - {Delta1, Bias, H, Out}, - Input), - punycode_encode1(Input, SNonBasic, B, NewH, L, M + 1, - NewDelta + 1, NewBias, NewOut); -punycode_encode1(_Input, _SNonBasic, _B, _H, _L, _N, - _Delta, _Bias, Out) -> - lists:reverse(Out). - -punycode_encode_delta(Delta, Bias, Out) -> - punycode_encode_delta(Delta, Bias, Out, ?BASE). - -punycode_encode_delta(Delta, Bias, Out, K) -> - T = if K =< Bias -> ?TMIN; - K >= Bias + (?TMAX) -> ?TMAX; - true -> K - Bias - end, - if Delta < T -> [codepoint(Delta) | Out]; - true -> - C = T + (Delta - T) rem ((?BASE) - T), - punycode_encode_delta((Delta - T) div ((?BASE) - T), - Bias, [codepoint(C) | Out], K + (?BASE)) - end. - -adapt(Delta, NumPoints, FirstTime) -> - Delta1 = if FirstTime -> Delta div (?DAMP); - true -> Delta div 2 - end, - Delta2 = Delta1 + Delta1 div NumPoints, - adapt1(Delta2, 0). - -adapt1(Delta, K) -> - if Delta > ((?BASE) - (?TMIN)) * (?TMAX) div 2 -> - adapt1(Delta div ((?BASE) - (?TMIN)), K + (?BASE)); - true -> - K + - ((?BASE) - (?TMIN) + 1) * Delta div (Delta + (?SKEW)) - end. - -codepoint(C) -> - if (0 =< C) and (C =< 25) -> C + 97; - (26 =< C) and (C =< 35) -> C + 22 - end. - -%%%=================================================================== -%%% Unit tests -%%%=================================================================== --ifdef(TEST). - -acsii_test() -> - ?assertEqual(<<"test.org">>, domain_utf8_to_ascii(<<"test.org">>)). - -utf8_test() -> - ?assertEqual( - <<"xn--d1acufc.xn--p1ai">>, - domain_utf8_to_ascii( - <<208,180,208,190,208,188,208,181,208,189,46,209,128,209,132>>)). - --endif. diff --git a/src/jd2ejd.erl b/src/jd2ejd.erl index bfa52bb2a..cf17acdd5 100644 --- a/src/jd2ejd.erl +++ b/src/jd2ejd.erl @@ -5,7 +5,7 @@ %%% Created : 2 Feb 2003 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -30,10 +30,8 @@ %% External exports -export([import_file/1, import_dir/1]). --include("ejabberd.hrl"). -include("logger.hrl"). - --include("jlib.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). %%%---------------------------------------------------------------------- %%% API @@ -42,34 +40,34 @@ import_file(File) -> User = filename:rootname(filename:basename(File)), Server = filename:basename(filename:dirname(File)), - case jlib:nodeprep(User) /= error andalso - jlib:nameprep(Server) /= error + case jid:nodeprep(User) /= error andalso + jid:nameprep(Server) /= error of true -> case file:read_file(File) of {ok, Text} -> - case xml_stream:parse_element(Text) of + case fxml_stream:parse_element(Text) of El when is_record(El, xmlel) -> case catch process_xdb(User, Server, El) of {'EXIT', Reason} -> - ?ERROR_MSG("Error while processing file \"~s\": " + ?ERROR_MSG("Error while processing file \"~ts\": " "~p~n", [File, Reason]), {error, Reason}; _ -> ok end; {error, Reason} -> - ?ERROR_MSG("Can't parse file \"~s\": ~p~n", + ?ERROR_MSG("Can't parse file \"~ts\": ~p~n", [File, Reason]), {error, Reason} end; {error, Reason} -> - ?ERROR_MSG("Can't read file \"~s\": ~p~n", + ?ERROR_MSG("Can't read file \"~ts\": ~p~n", [File, Reason]), {error, Reason} end; false -> - ?ERROR_MSG("Illegal user/server name in file \"~s\"~n", + ?ERROR_MSG("Illegal user/server name in file \"~ts\"~n", [File]), {error, <<"illegal user/server">>} end. @@ -112,65 +110,63 @@ process_xdb(User, Server, xdb_data(_User, _Server, {xmlcdata, _CData}) -> ok; xdb_data(User, Server, #xmlel{attrs = Attrs} = El) -> - From = jlib:make_jid(User, Server, <<"">>), - case xml:get_attr_s(<<"xmlns">>, Attrs) of + From = jid:make(User, Server), + LServer = From#jid.lserver, + case fxml:get_attr_s(<<"xmlns">>, Attrs) of ?NS_AUTH -> - Password = xml:get_tag_cdata(El), + Password = fxml:get_tag_cdata(El), ejabberd_auth:set_password(User, Server, Password), ok; ?NS_ROSTER -> - catch mod_roster:set_items(User, Server, El), ok; + catch mod_roster:set_items(User, Server, xmpp:decode(El)), + ok; ?NS_LAST -> - TimeStamp = xml:get_attr_s(<<"last">>, Attrs), - Status = xml:get_tag_cdata(El), + TimeStamp = fxml:get_attr_s(<<"last">>, Attrs), + Status = fxml:get_tag_cdata(El), catch mod_last:store_last_info(User, Server, - jlib:binary_to_integer(TimeStamp), + binary_to_integer(TimeStamp), Status), ok; ?NS_VCARD -> - catch mod_vcard:process_sm_iq(From, - jlib:make_jid(<<"">>, Server, <<"">>), - #iq{type = set, xmlns = ?NS_VCARD, - sub_el = El}), + catch mod_vcard:set_vcard(User, LServer, El), ok; <<"jabber:x:offline">> -> process_offline(Server, From, El), ok; XMLNS -> - case xml:get_attr_s(<<"j_private_flag">>, Attrs) of + case fxml:get_attr_s(<<"j_private_flag">>, Attrs) of <<"1">> -> - catch mod_private:process_sm_iq(From, - jlib:make_jid(<<"">>, Server, - <<"">>), - #iq{type = set, - xmlns = ?NS_PRIVATE, - sub_el = - #xmlel{name = - <<"query">>, - attrs = [], - children = - [jlib:remove_attr(<<"j_private_flag">>, - jlib:remove_attr(<<"xdbns">>, - El))]}}); + NewAttrs = lists:filter( + fun({<<"j_private_flag">>, _}) -> false; + ({<<"xdbns">>, _}) -> false; + (_) -> true + end, Attrs), + catch mod_private:set_data( + From, + [{XMLNS, El#xmlel{attrs = NewAttrs}}]); _ -> - ?DEBUG("jd2ejd: Unknown namespace \"~s\"~n", [XMLNS]) + ?DEBUG("Unknown namespace \"~ts\"~n", [XMLNS]) end, ok end. process_offline(Server, To, #xmlel{children = Els}) -> - LServer = jlib:nameprep(Server), - lists:foreach(fun (#xmlel{attrs = Attrs} = El) -> - FromS = xml:get_attr_s(<<"from">>, Attrs), - From = case FromS of - <<"">> -> - jlib:make_jid(<<"">>, Server, <<"">>); - _ -> jlib:string_to_jid(FromS) - end, - case From of - error -> ok; - _ -> - ejabberd_hooks:run(offline_message_hook, - LServer, [From, To, El]) - end - end, - Els). + LServer = jid:nameprep(Server), + lists:foreach( + fun(#xmlel{} = El) -> + try xmpp:decode(El, ?NS_CLIENT, [ignore_els]) of + #message{from = JID} = Msg -> + From = case JID of + undefined -> jid:make(Server); + _ -> JID + end, + ejabberd_hooks:run_fold( + offline_message_hook, + LServer, {pass, xmpp:set_from_to(Msg, From, To)}, []); + _ -> + ok + catch _:{xmpp_codec, Why} -> + Txt = xmpp:format_error(Why), + ?ERROR_MSG("Failed to decode XML '~ts': ~ts", + [fxml:element_to_binary(El), Txt]) + end + end, Els). diff --git a/src/jlib.erl b/src/jlib.erl deleted file mode 100644 index 2c0f30b3f..000000000 --- a/src/jlib.erl +++ /dev/null @@ -1,988 +0,0 @@ -%%%---------------------------------------------------------------------- -%%% File : jlib.erl -%%% Author : Alexey Shchepin -%%% Purpose : General XMPP library. -%%% Created : 23 Nov 2002 by Alexey Shchepin -%%% -%%% -%%% ejabberd, Copyright (C) 2002-2015 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(jlib). - --author('alexey@process-one.net'). - --compile({no_auto_import, [atom_to_binary/2, - binary_to_integer/1, - integer_to_binary/1]}). - --export([make_result_iq_reply/1, make_error_reply/3, - make_error_reply/2, make_error_element/2, - make_correct_from_to_attrs/3, replace_from_to_attrs/3, - replace_from_to/3, replace_from_attrs/2, replace_from/2, - remove_attr/2, make_jid/3, make_jid/1, string_to_jid/1, - jid_to_string/1, is_nodename/1, tolower/1, nodeprep/1, - nameprep/1, resourceprep/1, jid_tolower/1, - jid_remove_resource/1, jid_replace_resource/2, - get_iq_namespace/1, iq_query_info/1, - iq_query_or_response_info/1, is_iq_request_type/1, - iq_to_xml/1, parse_xdata_submit/1, - add_delay_info/3, add_delay_info/4, - timestamp_to_iso/1, timestamp_to_iso/2, - now_to_utc_string/1, now_to_local_string/1, - datetime_string_to_timestamp/1, - term_to_base64/1, base64_to_term/1, - decode_base64/1, encode_base64/1, ip_to_list/1, - rsm_encode/1, rsm_encode/2, rsm_decode/1, - binary_to_integer/1, binary_to_integer/2, - integer_to_binary/1, integer_to_binary/2, - atom_to_binary/1, binary_to_atom/1, tuple_to_binary/1, - l2i/1, i2l/1, i2l/2, queue_drop_while/2]). - -%% TODO: Remove once XEP-0091 is Obsolete -%% TODO: Remove once XEP-0091 is Obsolete - --include("jlib.hrl"). - --export_type([jid/0]). - -%send_iq(From, To, ID, SubTags) -> -% ok. - --spec make_result_iq_reply(xmlel()) -> xmlel(). - -make_result_iq_reply(#xmlel{name = Name, attrs = Attrs, - children = SubTags}) -> - NewAttrs = make_result_iq_reply_attrs(Attrs), - #xmlel{name = Name, attrs = NewAttrs, - children = SubTags}. - --spec make_result_iq_reply_attrs([attr()]) -> [attr()]. - -make_result_iq_reply_attrs(Attrs) -> - To = xml:get_attr(<<"to">>, Attrs), - From = xml:get_attr(<<"from">>, Attrs), - Attrs1 = lists:keydelete(<<"to">>, 1, Attrs), - Attrs2 = lists:keydelete(<<"from">>, 1, Attrs1), - Attrs3 = case To of - {value, ToVal} -> [{<<"from">>, ToVal} | Attrs2]; - _ -> Attrs2 - end, - Attrs4 = case From of - {value, FromVal} -> [{<<"to">>, FromVal} | Attrs3]; - _ -> Attrs3 - end, - Attrs5 = lists:keydelete(<<"type">>, 1, Attrs4), - Attrs6 = [{<<"type">>, <<"result">>} | Attrs5], - Attrs6. - --spec make_error_reply(xmlel(), binary(), binary()) -> xmlel(). - -make_error_reply(#xmlel{name = Name, attrs = Attrs, - children = SubTags}, - Code, Desc) -> - NewAttrs = make_error_reply_attrs(Attrs), - #xmlel{name = Name, attrs = NewAttrs, - children = - SubTags ++ - [#xmlel{name = <<"error">>, - attrs = [{<<"code">>, Code}], - children = [{xmlcdata, Desc}]}]}. - --spec make_error_reply(xmlel(), xmlel()) -> xmlel(). - -make_error_reply(#xmlel{name = Name, attrs = Attrs, - children = SubTags}, - Error) -> - NewAttrs = make_error_reply_attrs(Attrs), - #xmlel{name = Name, attrs = NewAttrs, - children = SubTags ++ [Error]}. - --spec make_error_reply_attrs([attr()]) -> [attr()]. - -make_error_reply_attrs(Attrs) -> - To = xml:get_attr(<<"to">>, Attrs), - From = xml:get_attr(<<"from">>, Attrs), - Attrs1 = lists:keydelete(<<"to">>, 1, Attrs), - Attrs2 = lists:keydelete(<<"from">>, 1, Attrs1), - Attrs3 = case To of - {value, ToVal} -> [{<<"from">>, ToVal} | Attrs2]; - _ -> Attrs2 - end, - Attrs4 = case From of - {value, FromVal} -> [{<<"to">>, FromVal} | Attrs3]; - _ -> Attrs3 - end, - Attrs5 = lists:keydelete(<<"type">>, 1, Attrs4), - Attrs6 = [{<<"type">>, <<"error">>} | Attrs5], - Attrs6. - --spec make_error_element(binary(), binary()) -> xmlel(). - -make_error_element(Code, Desc) -> - #xmlel{name = <<"error">>, attrs = [{<<"code">>, Code}], - children = [{xmlcdata, Desc}]}. - --spec make_correct_from_to_attrs(binary(), binary(), [attr()]) -> [attr()]. - -make_correct_from_to_attrs(From, To, Attrs) -> - Attrs1 = lists:keydelete(<<"from">>, 1, Attrs), - Attrs2 = case xml:get_attr(<<"to">>, Attrs) of - {value, _} -> Attrs1; - _ -> [{<<"to">>, To} | Attrs1] - end, - Attrs3 = [{<<"from">>, From} | Attrs2], - Attrs3. - --spec replace_from_to_attrs(binary(), binary(), [attr()]) -> [attr()]. - -replace_from_to_attrs(From, To, Attrs) -> - Attrs1 = lists:keydelete(<<"to">>, 1, Attrs), - Attrs2 = lists:keydelete(<<"from">>, 1, Attrs1), - Attrs3 = [{<<"to">>, To} | Attrs2], - Attrs4 = [{<<"from">>, From} | Attrs3], - Attrs4. - --spec replace_from_to(jid(), jid(), xmlel()) -> xmlel(). - -replace_from_to(From, To, - #xmlel{name = Name, attrs = Attrs, children = Els}) -> - NewAttrs = - replace_from_to_attrs(jlib:jid_to_string(From), - jlib:jid_to_string(To), Attrs), - #xmlel{name = Name, attrs = NewAttrs, children = Els}. - --spec replace_from_attrs(binary(), [attr()]) -> [attr()]. - -replace_from_attrs(From, Attrs) -> - Attrs1 = lists:keydelete(<<"from">>, 1, Attrs), - [{<<"from">>, From} | Attrs1]. - --spec replace_from(jid(), xmlel()) -> xmlel(). - -replace_from(From, - #xmlel{name = Name, attrs = Attrs, children = Els}) -> - NewAttrs = replace_from_attrs(jlib:jid_to_string(From), - Attrs), - #xmlel{name = Name, attrs = NewAttrs, children = Els}. - --spec remove_attr(binary(), xmlel()) -> xmlel(). - -remove_attr(Attr, - #xmlel{name = Name, attrs = Attrs, children = Els}) -> - NewAttrs = lists:keydelete(Attr, 1, Attrs), - #xmlel{name = Name, attrs = NewAttrs, children = Els}. - --spec make_jid(binary(), binary(), binary()) -> jid() | error. - -make_jid(User, Server, Resource) -> - case nodeprep(User) of - error -> error; - LUser -> - case nameprep(Server) of - error -> error; - LServer -> - case resourceprep(Resource) of - error -> error; - LResource -> - #jid{user = User, server = Server, resource = Resource, - luser = LUser, lserver = LServer, - lresource = LResource} - end - end - end. - --spec make_jid({binary(), binary(), binary()}) -> jid() | error. - -make_jid({User, Server, Resource}) -> - make_jid(User, Server, Resource). - --spec string_to_jid(binary()) -> jid() | error. - -string_to_jid(S) -> - string_to_jid1(binary_to_list(S), ""). - -string_to_jid1([$@ | _J], "") -> error; -string_to_jid1([$@ | J], N) -> - string_to_jid2(J, lists:reverse(N), ""); -string_to_jid1([$/ | _J], "") -> error; -string_to_jid1([$/ | J], N) -> - string_to_jid3(J, "", lists:reverse(N), ""); -string_to_jid1([C | J], N) -> - string_to_jid1(J, [C | N]); -string_to_jid1([], "") -> error; -string_to_jid1([], N) -> - make_jid(<<"">>, list_to_binary(lists:reverse(N)), <<"">>). - -%% Only one "@" is admitted per JID -string_to_jid2([$@ | _J], _N, _S) -> error; -string_to_jid2([$/ | _J], _N, "") -> error; -string_to_jid2([$/ | J], N, S) -> - string_to_jid3(J, N, lists:reverse(S), ""); -string_to_jid2([C | J], N, S) -> - string_to_jid2(J, N, [C | S]); -string_to_jid2([], _N, "") -> error; -string_to_jid2([], N, S) -> - make_jid(list_to_binary(N), list_to_binary(lists:reverse(S)), <<"">>). - -string_to_jid3([C | J], N, S, R) -> - string_to_jid3(J, N, S, [C | R]); -string_to_jid3([], N, S, R) -> - make_jid(list_to_binary(N), list_to_binary(S), - list_to_binary(lists:reverse(R))). - --spec jid_to_string(jid() | ljid()) -> binary(). - -jid_to_string(#jid{user = User, server = Server, - resource = Resource}) -> - jid_to_string({User, Server, Resource}); -jid_to_string({N, S, R}) -> - Node = iolist_to_binary(N), - Server = iolist_to_binary(S), - Resource = iolist_to_binary(R), - S1 = case Node of - <<"">> -> <<"">>; - _ -> <> - end, - S2 = <>, - S3 = case Resource of - <<"">> -> S2; - _ -> <> - end, - S3. - --spec is_nodename(binary()) -> boolean(). - -is_nodename(Node) -> - N = nodeprep(Node), - (N /= error) and (N /= <<>>). - -%tolower_c(C) when C >= $A, C =< $Z -> -% C + 32; -%tolower_c(C) -> -% C. - --define(LOWER(Char), - if Char >= $A, Char =< $Z -> Char + 32; - true -> Char - end). - -%tolower(S) -> -% lists:map(fun tolower_c/1, S). - -%tolower(S) -> -% [?LOWER(Char) || Char <- S]. - --spec tolower(binary()) -> binary(). - -tolower(B) -> - iolist_to_binary(tolower_s(binary_to_list(B))). - -tolower_s([C | Cs]) -> - if C >= $A, C =< $Z -> [C + 32 | tolower_s(Cs)]; - true -> [C | tolower_s(Cs)] - end; -tolower_s([]) -> []. - -%tolower([C | Cs]) when C >= $A, C =< $Z -> -% [C + 32 | tolower(Cs)]; -%tolower([C | Cs]) -> -% [C | tolower(Cs)]; -%tolower([]) -> -% []. - --spec nodeprep(binary()) -> binary() | error. - -nodeprep("") -> <<>>; -nodeprep(S) when byte_size(S) < 1024 -> - R = stringprep:nodeprep(S), - if byte_size(R) < 1024 -> R; - true -> error - end; -nodeprep(_) -> error. - --spec nameprep(binary()) -> binary() | error. - -nameprep(S) when byte_size(S) < 1024 -> - R = stringprep:nameprep(S), - if byte_size(R) < 1024 -> R; - true -> error - end; -nameprep(_) -> error. - --spec resourceprep(binary()) -> binary() | error. - -resourceprep(S) when byte_size(S) < 1024 -> - R = stringprep:resourceprep(S), - if byte_size(R) < 1024 -> R; - true -> error - end; -resourceprep(_) -> error. - --spec jid_tolower(jid() | ljid()) -> error | ljid(). - -jid_tolower(#jid{luser = U, lserver = S, - lresource = R}) -> - {U, S, R}; -jid_tolower({U, S, R}) -> - case nodeprep(U) of - error -> error; - LUser -> - case nameprep(S) of - error -> error; - LServer -> - case resourceprep(R) of - error -> error; - LResource -> {LUser, LServer, LResource} - end - end - end. - --spec jid_remove_resource(jid()) -> jid(); - (ljid()) -> ljid(). - -jid_remove_resource(#jid{} = JID) -> - JID#jid{resource = <<"">>, lresource = <<"">>}; -jid_remove_resource({U, S, _R}) -> {U, S, <<"">>}. - --spec jid_replace_resource(jid(), binary()) -> error | jid(). - -jid_replace_resource(JID, Resource) -> - case resourceprep(Resource) of - error -> error; - LResource -> - JID#jid{resource = Resource, lresource = LResource} - end. - --spec get_iq_namespace(xmlel()) -> binary(). - -get_iq_namespace(#xmlel{name = <<"iq">>, children = Els}) -> - case xml:remove_cdata(Els) of - [#xmlel{attrs = Attrs}] -> xml:get_attr_s(<<"xmlns">>, Attrs); - _ -> <<"">> - end; -get_iq_namespace(_) -> <<"">>. - -%% --spec(iq_query_info/1 :: -( - Xmlel :: xmlel()) - -> iq_request() | 'reply' | 'invalid' | 'not_iq' -). - -%% @spec (xmlelement()) -> iq() | reply | invalid | not_iq -iq_query_info(El) -> iq_info_internal(El, request). - -%% --spec(iq_query_or_response_info/1 :: -( - Xmlel :: xmlel()) - -> iq_request() | iq_reply() | 'reply' | 'invalid' | 'not_iq' -). - -iq_query_or_response_info(El) -> - iq_info_internal(El, any). - -iq_info_internal(#xmlel{name = <<"iq">>, attrs = Attrs, children = Els}, Filter) -> - ID = xml:get_attr_s(<<"id">>, Attrs), - Lang = xml:get_attr_s(<<"xml:lang">>, Attrs), - {Type, Class} = case xml:get_attr_s(<<"type">>, Attrs) of - <<"set">> -> {set, request}; - <<"get">> -> {get, request}; - <<"result">> -> {result, reply}; - <<"error">> -> {error, reply}; - _ -> {invalid, invalid} - end, - if Type == invalid -> invalid; Class == request; Filter == any -> - FilteredEls = xml:remove_cdata(Els), - {XMLNS, SubEl} = case {Class, FilteredEls} of - {request, [#xmlel{attrs = Attrs2}]} -> - {xml:get_attr_s(<<"xmlns">>, Attrs2), hd(FilteredEls)}; - {reply, _} -> - NonErrorEls = [El || #xmlel{name = SubName} = El <- FilteredEls, - SubName /= <<"error">>], - {case NonErrorEls of - [NonErrorEl] -> xml:get_tag_attr_s(<<"xmlns">>, NonErrorEl); - _ -> <<"">> - end, - FilteredEls}; - _ -> - {<<"">>, []} - end, - if XMLNS == <<"">>, Class == request -> - invalid; - true -> - #iq{id = ID, type = Type, xmlns = XMLNS, lang = Lang, sub_el = SubEl} - end; - Class == reply, Filter /= any -> - reply - end; -iq_info_internal(_, _) -> not_iq. - --spec is_iq_request_type(set | get | result | error) -> boolean(). - -is_iq_request_type(set) -> true; -is_iq_request_type(get) -> true; -is_iq_request_type(_) -> false. - -iq_type_to_string(set) -> <<"set">>; -iq_type_to_string(get) -> <<"get">>; -iq_type_to_string(result) -> <<"result">>; -iq_type_to_string(error) -> <<"error">>. - --spec(iq_to_xml/1 :: -( - IQ :: iq()) - -> xmlel() -). - -iq_to_xml(#iq{id = ID, type = Type, sub_el = SubEl}) -> - if ID /= <<"">> -> - #xmlel{name = <<"iq">>, - attrs = - [{<<"id">>, ID}, {<<"type">>, iq_type_to_string(Type)}], - children = SubEl}; - true -> - #xmlel{name = <<"iq">>, - attrs = [{<<"type">>, iq_type_to_string(Type)}], - children = SubEl} - end. - --spec(parse_xdata_submit/1 :: -( - El :: xmlel()) - -> [{Var::binary(), Values::[binary()]}] - %% - | 'invalid' -). - -parse_xdata_submit(#xmlel{attrs = Attrs, children = Els}) -> - case xml:get_attr_s(<<"type">>, Attrs) of - <<"submit">> -> - lists:reverse(parse_xdata_fields(Els, [])); - <<"form">> -> %% This is a workaround to accept Psi's wrong forms - lists:reverse(parse_xdata_fields(Els, [])); - _ -> - invalid - end. - --spec(parse_xdata_fields/2 :: -( - Xmlels :: [xmlel() | cdata()], - Res :: [{Var::binary(), Values :: [binary()]}]) - -> [{Var::binary(), Values::[binary()]}] -). - -parse_xdata_fields([], Res) -> Res; -parse_xdata_fields([#xmlel{name = <<"field">>, attrs = Attrs, children = SubEls} - | Els], Res) -> - case xml:get_attr_s(<<"var">>, Attrs) of - <<>> -> - parse_xdata_fields(Els, Res); - Var -> - Field = {Var, lists:reverse(parse_xdata_values(SubEls, []))}, - parse_xdata_fields(Els, [Field | Res]) - end; -parse_xdata_fields([_ | Els], Res) -> - parse_xdata_fields(Els, Res). - --spec(parse_xdata_values/2 :: -( - Xmlels :: [xmlel() | cdata()], - Res :: [binary()]) - -> [binary()] -). - -parse_xdata_values([], Res) -> Res; -parse_xdata_values([#xmlel{name = <<"value">>, children = SubEls} | Els], Res) -> - Val = xml:get_cdata(SubEls), - parse_xdata_values(Els, [Val | Res]); -parse_xdata_values([_ | Els], Res) -> - parse_xdata_values(Els, Res). - --spec rsm_decode(iq() | xmlel()) -> none | rsm_in(). - -rsm_decode(#iq{sub_el = SubEl}) -> rsm_decode(SubEl); -rsm_decode(#xmlel{} = SubEl) -> - case xml:get_subtag(SubEl, <<"set">>) of - false -> none; - #xmlel{name = <<"set">>, children = SubEls} -> - lists:foldl(fun rsm_parse_element/2, #rsm_in{}, SubEls) - end. - -rsm_parse_element(#xmlel{name = <<"max">>, attrs = []} = - Elem, - RsmIn) -> - CountStr = xml:get_tag_cdata(Elem), - {Count, _} = str:to_integer(CountStr), - RsmIn#rsm_in{max = Count}; -rsm_parse_element(#xmlel{name = <<"before">>, - attrs = []} = - Elem, - RsmIn) -> - UID = xml:get_tag_cdata(Elem), - RsmIn#rsm_in{direction = before, id = UID}; -rsm_parse_element(#xmlel{name = <<"after">>, - attrs = []} = - Elem, - RsmIn) -> - UID = xml:get_tag_cdata(Elem), - RsmIn#rsm_in{direction = aft, id = UID}; -rsm_parse_element(#xmlel{name = <<"index">>, - attrs = []} = - Elem, - RsmIn) -> - IndexStr = xml:get_tag_cdata(Elem), - {Index, _} = str:to_integer(IndexStr), - RsmIn#rsm_in{index = Index}; -rsm_parse_element(_, RsmIn) -> RsmIn. - --spec rsm_encode(iq(), rsm_out()) -> iq(). - -rsm_encode(#iq{sub_el = SubEl} = IQ, RsmOut) -> - Set = #xmlel{name = <<"set">>, - attrs = [{<<"xmlns">>, ?NS_RSM}], - children = lists:reverse(rsm_encode_out(RsmOut))}, - #xmlel{name = Name, attrs = Attrs, children = SubEls} = - SubEl, - New = #xmlel{name = Name, attrs = Attrs, - children = [Set | SubEls]}, - IQ#iq{sub_el = New}. - --spec rsm_encode(none | rsm_out()) -> [xmlel()]. - -rsm_encode(none) -> []; -rsm_encode(RsmOut) -> - [#xmlel{name = <<"set">>, - attrs = [{<<"xmlns">>, ?NS_RSM}], - children = lists:reverse(rsm_encode_out(RsmOut))}]. - -rsm_encode_out(#rsm_out{count = Count, index = Index, - first = First, last = Last}) -> - El = rsm_encode_first(First, Index, []), - El2 = rsm_encode_last(Last, El), - rsm_encode_count(Count, El2). - -rsm_encode_first(undefined, undefined, Arr) -> Arr; -rsm_encode_first(First, undefined, Arr) -> - [#xmlel{name = <<"first">>, attrs = [], - children = [{xmlcdata, First}]} - | Arr]; -rsm_encode_first(First, Index, Arr) -> - [#xmlel{name = <<"first">>, - attrs = [{<<"index">>, i2l(Index)}], - children = [{xmlcdata, First}]} - | Arr]. - -rsm_encode_last(undefined, Arr) -> Arr; -rsm_encode_last(Last, Arr) -> - [#xmlel{name = <<"last">>, attrs = [], - children = [{xmlcdata, Last}]} - | Arr]. - -rsm_encode_count(undefined, Arr) -> Arr; -rsm_encode_count(Count, Arr) -> - [#xmlel{name = <<"count">>, attrs = [], - children = [{xmlcdata, i2l(Count)}]} - | Arr]. - --spec add_delay_info(xmlel(), jid() | ljid() | binary(), erlang:timestamp()) - -> xmlel(). - -add_delay_info(El, From, Time) -> - add_delay_info(El, From, Time, <<"">>). - --spec add_delay_info(xmlel(), jid() | ljid() | binary(), erlang:timestamp(), - binary()) -> xmlel(). - -add_delay_info(El, From, Time, Desc) -> - %% TODO: Remove support for , XEP-0091 is obsolete. - El1 = add_delay_info(El, From, Time, Desc, <<"delay">>, ?NS_DELAY), - El2 = add_delay_info(El1, From, Time, Desc, <<"x">>, ?NS_DELAY91), - El2. - --spec add_delay_info(xmlel(), jid() | ljid() | binary(), erlang:timestamp(), - binary(), binary(), binary()) -> xmlel(). - -add_delay_info(El, From, Time, Desc, Name, XMLNS) -> - case xml:get_subtag_with_xmlns(El, Name, XMLNS) of - false -> - %% Add new tag - DelayTag = create_delay_tag(Time, From, Desc, XMLNS), - xml:append_subtags(El, [DelayTag]); - DelayTag -> - %% Update existing tag - NewDelayTag = - case {xml:get_tag_cdata(DelayTag), Desc} of - {<<"">>, <<"">>} -> - DelayTag; - {OldDesc, <<"">>} -> - DelayTag#xmlel{children = [{xmlcdata, OldDesc}]}; - {<<"">>, NewDesc} -> - DelayTag#xmlel{children = [{xmlcdata, NewDesc}]}; - {OldDesc, NewDesc} -> - case binary:match(OldDesc, NewDesc) of - nomatch -> - FinalDesc = <>, - DelayTag#xmlel{children = [{xmlcdata, FinalDesc}]}; - _ -> - DelayTag#xmlel{children = [{xmlcdata, OldDesc}]} - end - end, - NewEl = xml:remove_subtags(El, Name, {<<"xmlns">>, XMLNS}), - xml:append_subtags(NewEl, [NewDelayTag]) - end. - --spec create_delay_tag(erlang:timestamp(), jid() | ljid() | binary(), binary(), - binary()) -> xmlel() | error. - -create_delay_tag(TimeStamp, FromJID, Desc, XMLNS) when is_tuple(FromJID) -> - From = jlib:jid_to_string(FromJID), - {Name, Stamp} = case XMLNS of - ?NS_DELAY -> - {<<"delay">>, now_to_utc_string(TimeStamp, 3)}; - ?NS_DELAY91 -> - DateTime = calendar:now_to_universal_time(TimeStamp), - {<<"x">>, timestamp_to_iso(DateTime)} - end, - Children = case Desc of - <<"">> -> []; - _ -> [{xmlcdata, Desc}] - end, - #xmlel{name = Name, - attrs = - [{<<"xmlns">>, XMLNS}, {<<"from">>, From}, - {<<"stamp">>, Stamp}], - children = Children}; -create_delay_tag(DateTime, Host, Desc, XMLNS) when is_binary(Host) -> - FromJID = jlib:make_jid(<<"">>, Host, <<"">>), - create_delay_tag(DateTime, FromJID, Desc, XMLNS). - --type tz() :: {binary(), {integer(), integer()}} | {integer(), integer()} | utc. - -%% Timezone = utc | {Sign::string(), {Hours, Minutes}} | {Hours, Minutes} -%% Hours = integer() -%% Minutes = integer() --spec timestamp_to_iso(calendar:datetime(), tz()) -> {binary(), binary()}. - -timestamp_to_iso({{Year, Month, Day}, - {Hour, Minute, Second}}, - Timezone) -> - Timestamp_string = - lists:flatten(io_lib:format("~4..0B-~2..0B-~2..0BT~2..0B:~2..0B:~2..0B", - [Year, Month, Day, Hour, Minute, Second])), - Timezone_string = case Timezone of - utc -> "Z"; - {Sign, {TZh, TZm}} -> - io_lib:format("~s~2..0B:~2..0B", [Sign, TZh, TZm]); - {TZh, TZm} -> - Sign = case TZh >= 0 of - true -> "+"; - false -> "-" - end, - io_lib:format("~s~2..0B:~2..0B", - [Sign, abs(TZh), TZm]) - end, - {iolist_to_binary(Timestamp_string), iolist_to_binary(Timezone_string)}. - --spec timestamp_to_iso(calendar:datetime()) -> binary(). - -timestamp_to_iso({{Year, Month, Day}, - {Hour, Minute, Second}}) -> - iolist_to_binary(io_lib:format("~4..0B~2..0B~2..0BT~2..0B:~2..0B:~2..0B", - [Year, Month, Day, Hour, Minute, Second])). - --spec now_to_utc_string(erlang:timestamp()) -> binary(). - -now_to_utc_string({MegaSecs, Secs, MicroSecs}) -> - now_to_utc_string({MegaSecs, Secs, MicroSecs}, 6). - --spec now_to_utc_string(erlang:timestamp(), 1..6) -> binary(). - -now_to_utc_string({MegaSecs, Secs, MicroSecs}, Precision) -> - {{Year, Month, Day}, {Hour, Minute, Second}} = - calendar:now_to_universal_time({MegaSecs, Secs, - MicroSecs}), - Max = round(math:pow(10, Precision)), - case round(MicroSecs / math:pow(10, 6 - Precision)) of - Max -> - now_to_utc_string({MegaSecs, Secs + 1, 0}, Precision); - FracOfSec -> - list_to_binary(io_lib:format("~4..0B-~2..0B-~2..0BT" - "~2..0B:~2..0B:~2..0B.~*..0BZ", - [Year, Month, Day, Hour, Minute, Second, - Precision, FracOfSec])) - end. - --spec now_to_local_string(erlang:timestamp()) -> binary(). - -now_to_local_string({MegaSecs, Secs, MicroSecs}) -> - LocalTime = calendar:now_to_local_time({MegaSecs, Secs, - MicroSecs}), - UTCTime = calendar:now_to_universal_time({MegaSecs, - Secs, MicroSecs}), - Seconds = - calendar:datetime_to_gregorian_seconds(LocalTime) - - calendar:datetime_to_gregorian_seconds(UTCTime), - {{H, M, _}, Sign} = if Seconds < 0 -> - {calendar:seconds_to_time(-Seconds), "-"}; - true -> {calendar:seconds_to_time(Seconds), "+"} - end, - {{Year, Month, Day}, {Hour, Minute, Second}} = - LocalTime, - list_to_binary(io_lib:format("~4..0B-~2..0B-~2..0BT~2..0B:~2..0B:~2..0B.~6." - ".0B~s~2..0B:~2..0B", - [Year, Month, Day, Hour, Minute, Second, - MicroSecs, Sign, H, M])). - --spec datetime_string_to_timestamp(binary()) -> undefined | erlang:timestamp(). - -datetime_string_to_timestamp(TimeStr) -> - case catch parse_datetime(TimeStr) of - {'EXIT', _Err} -> undefined; - TimeStamp -> TimeStamp - end. - -parse_datetime(TimeStr) -> - [Date, Time] = str:tokens(TimeStr, <<"T">>), - D = parse_date(Date), - {T, MS, TZH, TZM} = parse_time(Time), - S = calendar:datetime_to_gregorian_seconds({D, T}), - S1 = calendar:datetime_to_gregorian_seconds({{1970, 1, - 1}, - {0, 0, 0}}), - Seconds = S - S1 - TZH * 60 * 60 - TZM * 60, - {Seconds div 1000000, Seconds rem 1000000, MS}. - -% yyyy-mm-dd -parse_date(Date) -> - [Y, M, D] = str:tokens(Date, <<"-">>), - Date1 = {binary_to_integer(Y), binary_to_integer(M), - binary_to_integer(D)}, - case calendar:valid_date(Date1) of - true -> Date1; - _ -> false - end. - -% hh:mm:ss[.sss]TZD -parse_time(Time) -> - case str:str(Time, <<"Z">>) of - 0 -> parse_time_with_timezone(Time); - _ -> - [T | _] = str:tokens(Time, <<"Z">>), - {TT, MS} = parse_time1(T), - {TT, MS, 0, 0} - end. - -parse_time_with_timezone(Time) -> - case str:str(Time, <<"+">>) of - 0 -> - case str:str(Time, <<"-">>) of - 0 -> false; - _ -> parse_time_with_timezone(Time, <<"-">>) - end; - _ -> parse_time_with_timezone(Time, <<"+">>) - end. - -parse_time_with_timezone(Time, Delim) -> - [T, TZ] = str:tokens(Time, Delim), - {TZH, TZM} = parse_timezone(TZ), - {TT, MS} = parse_time1(T), - case Delim of - <<"-">> -> {TT, MS, -TZH, -TZM}; - <<"+">> -> {TT, MS, TZH, TZM} - end. - -parse_timezone(TZ) -> - [H, M] = str:tokens(TZ, <<":">>), - {[H1, M1], true} = check_list([{H, 12}, {M, 60}]), - {H1, M1}. - -parse_time1(Time) -> - [HMS | T] = str:tokens(Time, <<".">>), - MS = case T of - [] -> 0; - [Val] -> binary_to_integer(str:left(Val, 6, $0)) - end, - [H, M, S] = str:tokens(HMS, <<":">>), - {[H1, M1, S1], true} = check_list([{H, 24}, {M, 60}, - {S, 60}]), - {{H1, M1, S1}, MS}. - -check_list(List) -> - lists:mapfoldl(fun ({L, N}, B) -> - V = binary_to_integer(L), - if (V >= 0) and (V =< N) -> {V, B}; - true -> {false, false} - end - end, - true, List). - -% -% Base64 stuff (based on httpd_util.erl) -% - --spec term_to_base64(term()) -> binary(). - -term_to_base64(Term) -> - encode_base64(term_to_binary(Term)). - --spec base64_to_term(binary()) -> {term, term()} | error. - -base64_to_term(Base64) -> - case catch binary_to_term(decode_base64(Base64), [safe]) of - {'EXIT', _} -> - error; - Term -> - {term, Term} - end. - --spec decode_base64(binary()) -> binary(). - -decode_base64(S) -> - case catch binary:last(S) of - C when C == $\n; C == $\s -> - decode_base64(binary:part(S, 0, byte_size(S) - 1)); - _ -> - decode_base64_bin(S, <<>>) - end. - -take_without_spaces(Bin, Count) -> - take_without_spaces(Bin, Count, <<>>). - -take_without_spaces(Bin, 0, Acc) -> - {Acc, Bin}; -take_without_spaces(<<>>, _, Acc) -> - {Acc, <<>>}; -take_without_spaces(<<$\s, Tail/binary>>, Count, Acc) -> - take_without_spaces(Tail, Count, Acc); -take_without_spaces(<<$\t, Tail/binary>>, Count, Acc) -> - take_without_spaces(Tail, Count, Acc); -take_without_spaces(<<$\n, Tail/binary>>, Count, Acc) -> - take_without_spaces(Tail, Count, Acc); -take_without_spaces(<<$\r, Tail/binary>>, Count, Acc) -> - take_without_spaces(Tail, Count, Acc); -take_without_spaces(<>, Count, Acc) -> - take_without_spaces(Tail, Count-1, <>). - -decode_base64_bin(<<>>, Acc) -> - Acc; -decode_base64_bin(Bin, Acc) -> - case take_without_spaces(Bin, 4) of - {<>, _} -> - <>; - {<>, _} -> - <>; - {<>, Tail} -> - Acc2 = <>, - decode_base64_bin(Tail, Acc2); - _ -> - <<"">> - end. - -d(X) when X >= $A, X =< $Z -> X - 65; -d(X) when X >= $a, X =< $z -> X - 71; -d(X) when X >= $0, X =< $9 -> X + 4; -d($+) -> 62; -d($/) -> 63; -d(_) -> 63. - - -%% Convert Erlang inet IP to list --spec encode_base64(binary()) -> binary(). - -encode_base64(Data) -> - encode_base64_bin(Data, <<>>). - -encode_base64_bin(<>, Acc) -> - encode_base64_bin(Tail, <>); -encode_base64_bin(<>, Acc) -> - <>; -encode_base64_bin(<>, Acc) -> - <>; -encode_base64_bin(<<>>, Acc) -> - Acc. - -e(X) when X >= 0, X < 26 -> X + 65; -e(X) when X > 25, X < 52 -> X + 71; -e(X) when X > 51, X < 62 -> X - 4; -e(62) -> $+; -e(63) -> $/; -e(X) -> exit({bad_encode_base64_token, X}). - --spec ip_to_list(inet:ip_address() | undefined | - {inet:ip_address(), inet:port_number()}) -> binary(). - -ip_to_list({IP, _Port}) -> - ip_to_list(IP); -%% This function clause could use inet_parse too: -ip_to_list(undefined) -> - <<"unknown">>; -ip_to_list(IP) -> - list_to_binary(inet_parse:ntoa(IP)). - -binary_to_atom(Bin) -> - erlang:binary_to_atom(Bin, utf8). - -binary_to_integer(Bin) -> - list_to_integer(binary_to_list(Bin)). - -binary_to_integer(Bin, Base) -> - list_to_integer(binary_to_list(Bin), Base). - -integer_to_binary(I) -> - list_to_binary(integer_to_list(I)). - -integer_to_binary(I, Base) -> - list_to_binary(erlang:integer_to_list(I, Base)). - -tuple_to_binary(T) -> - iolist_to_binary(tuple_to_list(T)). - -atom_to_binary(A) -> - erlang:atom_to_binary(A, utf8). - - -l2i(I) when is_integer(I) -> I; -l2i(L) when is_binary(L) -> binary_to_integer(L). - -i2l(I) when is_integer(I) -> integer_to_binary(I); -i2l(L) when is_binary(L) -> L. - -i2l(I, N) when is_integer(I) -> i2l(i2l(I), N); -i2l(L, N) when is_binary(L) -> - case str:len(L) of - N -> L; - C when C > N -> L; - _ -> i2l(<<$0, L/binary>>, N) - end. - --spec queue_drop_while(fun((term()) -> boolean()), queue()) -> queue(). - -queue_drop_while(F, Q) -> - case queue:peek(Q) of - {value, Item} -> - case F(Item) of - true -> - queue_drop_while(F, queue:drop(Q)); - _ -> - Q - end; - empty -> - Q - end. diff --git a/src/misc.erl b/src/misc.erl new file mode 100644 index 000000000..87f8b24e6 --- /dev/null +++ b/src/misc.erl @@ -0,0 +1,830 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov +%%% @doc +%%% This is the place for some unsorted auxiliary functions +%%% Some functions from jlib.erl are moved here +%%% Mild rubbish heap is accepted ;) +%%% @end +%%% Created : 30 Mar 2017 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(misc). + +%% API +-export([add_delay_info/3, add_delay_info/4, + unwrap_carbon/1, unwrap_mucsub_message/1, is_standalone_chat_state/1, + tolower/1, term_to_base64/1, base64_to_term/1, ip_to_list/1, + hex_to_bin/1, hex_to_base64/1, url_encode/1, expand_keyword/3, + atom_to_binary/1, binary_to_atom/1, tuple_to_binary/1, + l2i/1, i2l/1, i2l/2, expr_to_term/1, term_to_expr/1, + now_to_usec/1, usec_to_now/1, encode_pid/1, decode_pid/2, + compile_exprs/2, join_atoms/2, try_read_file/1, get_descr/2, + get_home/0, warn_unset_home/0, + css_dir/0, img_dir/0, js_dir/0, msgs_dir/0, sql_dir/0, lua_dir/0, + read_css/1, read_img/1, read_js/1, read_lua/1, + intersection/2, format_val/1, cancel_timer/1, unique_timestamp/0, + is_mucsub_message/1, best_match/2, pmap/2, peach/2, format_exception/4, + get_my_ipv4_address/0, get_my_ipv6_address/0, parse_ip_mask/1, + crypto_hmac/3, crypto_hmac/4, uri_parse/1, uri_parse/2, uri_quote/1, + uri_decode/1, + json_encode/1, json_decode/1, + set_proc_label/1, + match_ip_mask/3, format_hosts_list/1, format_cycle/1, delete_dir/1, + semver_to_xxyy/1, logical_processors/0, get_mucsub_event_type/1, + lists_uniq/1]). + +%% Deprecated functions +-export([decode_base64/1, encode_base64/1]). +-deprecated([{decode_base64, 1}, + {encode_base64, 1}]). + +-include("logger.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). +-include_lib("kernel/include/file.hrl"). + +-ifdef(OTP_BELOW_27). +%% Copied from erlang/otp/lib/stdlib/src/re.erl +-type re_mp() :: {re_pattern, _, _, _, _}. +-type json_value() :: jiffy:json_value(). +-else. +-type re_mp() :: re:mp(). +-type json_value() :: json:encode_value(). +-endif. +-export_type([re_mp/0]). +-export_type([json_value/0]). + +-type distance_cache() :: #{{string(), string()} => non_neg_integer()}. + +-spec uri_parse(binary()|string()) -> {ok, string(), string(), string(), number(), string(), string()} | {error, term()}. +uri_parse(URL) -> + yconf:parse_uri(URL). + +uri_parse(URL, Protocols) -> + yconf:parse_uri(URL, Protocols). + +-ifdef(OTP_BELOW_25). +-ifdef(OTP_BELOW_24). +uri_quote(Data) -> + Data. +-else. +uri_quote(Data) -> + http_uri:encode(Data). +-endif. +-else. +uri_quote(Data) -> + uri_string:quote(Data). +-endif. + +%% @doc Decode a part of the URL and return string() +%% -spec url_decode(binary()) -> bitstring(). + +-ifdef(OTP_BELOW_24). +uri_decode(Path) -> uri_decode(Path, <<>>). + +uri_decode(<<$%, Hi, Lo, Tail/binary>>, Acc) -> + Hex = list_to_integer([Hi, Lo], 16), + if Hex == 0 -> exit(badurl); + true -> ok + end, + uri_decode(Tail, <>); +uri_decode(<>, Acc) when H /= 0 -> + uri_decode(T, <>); +uri_decode(<<>>, Acc) -> Acc. +-else. +uri_decode(Path) -> + uri_string:percent_decode(Path). +-endif. + +-ifdef(USE_OLD_CRYPTO_HMAC). +crypto_hmac(Type, Key, Data) -> crypto:hmac(Type, Key, Data). +crypto_hmac(Type, Key, Data, MacL) -> crypto:hmac(Type, Key, Data, MacL). +-else. +crypto_hmac(Type, Key, Data) -> crypto:mac(hmac, Type, Key, Data). +crypto_hmac(Type, Key, Data, MacL) -> crypto:macN(hmac, Type, Key, Data, MacL). +-endif. + +-ifdef(OTP_BELOW_27). +json_encode(Term) -> + jiffy:encode(Term). +json_decode(Bin) -> + jiffy:decode(Bin, [return_maps]). +-else. +json_encode({[{_Key, _Value} | _]} = Term) -> + iolist_to_binary(json:encode(Term, + fun({Val}, Encoder) when is_list(Val) -> + json:encode_key_value_list(Val, Encoder); + (Val, Encoder) -> + json:encode_value(Val, Encoder) + end)); +json_encode({[]}) -> + %% Jiffy was able to handle this case, but Json library does not + <<"{}">>; +json_encode(Term) -> + iolist_to_binary(json:encode(Term)). +json_decode(Bin) -> + json:decode(Bin). +-endif. + +%%%=================================================================== +%%% API +%%%=================================================================== +-spec add_delay_info(stanza(), jid(), erlang:timestamp()) -> stanza(). +add_delay_info(Stz, From, Time) -> + add_delay_info(Stz, From, Time, <<"">>). + +-spec add_delay_info(stanza(), jid(), erlang:timestamp(), binary()) -> stanza(). +add_delay_info(Stz, From, Time, Desc) -> + Delays = xmpp:get_subtags(Stz, #delay{stamp = {0,0,0}}), + Matching = lists:any( + fun(#delay{from = OldFrom}) when is_record(OldFrom, jid) -> + jid:tolower(From) == jid:tolower(OldFrom); + (_) -> + false + end, Delays), + case Matching of + true -> + Stz; + _ -> + NewDelay = #delay{stamp = Time, from = From, desc = Desc}, + xmpp:append_subtags(Stz, [NewDelay]) + end. + +-spec unwrap_carbon(stanza()) -> xmpp_element(). +unwrap_carbon(#message{} = Msg) -> + try + case xmpp:get_subtag(Msg, #carbons_sent{forwarded = #forwarded{}}) of + #carbons_sent{forwarded = #forwarded{sub_els = [El]}} -> + xmpp:decode(El, ?NS_CLIENT, [ignore_els]); + _ -> + case xmpp:get_subtag(Msg, #carbons_received{ + forwarded = #forwarded{}}) of + #carbons_received{forwarded = #forwarded{sub_els = [El]}} -> + xmpp:decode(El, ?NS_CLIENT, [ignore_els]); + _ -> + Msg + end + end + catch _:{xmpp_codec, _} -> + Msg + end; +unwrap_carbon(Stanza) -> Stanza. + +-spec unwrap_mucsub_message(xmpp_element()) -> message() | false. +unwrap_mucsub_message(#message{} = OuterMsg) -> + case xmpp:get_subtag(OuterMsg, #ps_event{}) of + #ps_event{ + items = #ps_items{ + node = Node, + items = [ + #ps_item{ + sub_els = [#message{} = InnerMsg]} | _]}} + when Node == ?NS_MUCSUB_NODES_MESSAGES; + Node == ?NS_MUCSUB_NODES_SUBJECT -> + InnerMsg; + _ -> + false + end; +unwrap_mucsub_message(_Packet) -> + false. + +-spec is_mucsub_message(xmpp_element()) -> boolean(). +is_mucsub_message(Packet) -> + get_mucsub_event_type(Packet) /= false. + +-spec get_mucsub_event_type(xmpp_element()) -> binary() | false. +get_mucsub_event_type(#message{} = OuterMsg) -> + case xmpp:get_subtag(OuterMsg, #ps_event{}) of + #ps_event{ + items = #ps_items{ + node = Node}} + when Node == ?NS_MUCSUB_NODES_MESSAGES; + Node == ?NS_MUCSUB_NODES_SUBJECT; + Node == ?NS_MUCSUB_NODES_AFFILIATIONS; + Node == ?NS_MUCSUB_NODES_CONFIG; + Node == ?NS_MUCSUB_NODES_PARTICIPANTS; + Node == ?NS_MUCSUB_NODES_PRESENCE; + Node == ?NS_MUCSUB_NODES_SUBSCRIBERS -> + Node; + _ -> + false + end; +get_mucsub_event_type(_Packet) -> + false. + +-spec is_standalone_chat_state(stanza()) -> boolean(). +is_standalone_chat_state(Stanza) -> + case unwrap_carbon(Stanza) of + #message{body = [], subject = [], sub_els = Els} -> + IgnoreNS = [?NS_CHATSTATES, ?NS_DELAY, ?NS_EVENT, ?NS_HINTS], + Stripped = [El || El <- Els, + not lists:member(xmpp:get_ns(El), IgnoreNS)], + Stripped == []; + _ -> + false + end. + +-spec tolower(binary()) -> binary(). +tolower(B) -> + iolist_to_binary(tolower_s(binary_to_list(B))). + +tolower_s([C | Cs]) -> + if C >= $A, C =< $Z -> [C + 32 | tolower_s(Cs)]; + true -> [C | tolower_s(Cs)] + end; +tolower_s([]) -> []. + +-spec term_to_base64(term()) -> binary(). +term_to_base64(Term) -> + encode_base64(term_to_binary(Term)). + +-spec base64_to_term(binary()) -> {term, term()} | error. +base64_to_term(Base64) -> + try binary_to_term(base64:decode(Base64), [safe]) of + Term -> {term, Term} + catch _:_ -> + error + end. + +-spec decode_base64(binary()) -> binary(). +decode_base64(S) -> + try base64:mime_decode(S) + catch _:badarg -> <<>> + end. + +-spec encode_base64(binary()) -> binary(). +encode_base64(Data) -> + base64:encode(Data). + +-spec ip_to_list(inet:ip_address() | undefined | + {inet:ip_address(), inet:port_number()}) -> binary(). + +ip_to_list({IP, _Port}) -> + ip_to_list(IP); +%% This function clause could use inet_parse too: +ip_to_list(undefined) -> + <<"unknown">>; +ip_to_list(local) -> + <<"unix">>; +ip_to_list(IP) -> + list_to_binary(inet_parse:ntoa(IP)). + +-spec hex_to_bin(binary()) -> binary(). +hex_to_bin(Hex) -> + hex_to_bin(binary_to_list(Hex), []). + +-spec hex_to_bin(list(), list()) -> binary(). +hex_to_bin([], Acc) -> + list_to_binary(lists:reverse(Acc)); +hex_to_bin([H1, H2 | T], Acc) -> + {ok, [V], []} = io_lib:fread("~16u", [H1, H2]), + hex_to_bin(T, [V | Acc]). + +-spec hex_to_base64(binary()) -> binary(). +hex_to_base64(Hex) -> + base64:encode(hex_to_bin(Hex)). + +-spec url_encode(binary()) -> binary(). +url_encode(A) -> + url_encode(A, <<>>). + +-spec expand_keyword(iodata(), iodata(), iodata()) -> binary(). +expand_keyword(Keyword, Input, Replacement) -> + re:replace(Input, Keyword, Replacement, + [{return, binary}, global]). + +binary_to_atom(Bin) -> + erlang:binary_to_atom(Bin, utf8). + +tuple_to_binary(T) -> + iolist_to_binary(tuple_to_list(T)). + +%% erlang:atom_to_binary/1 is available since OTP 23 +%% https://www.erlang.org/doc/apps/erts/erlang#atom_to_binary/1 +%% Let's use /2 for backwards compatibility. +atom_to_binary(A) -> + erlang:atom_to_binary(A, utf8). + +expr_to_term(Expr) -> + Str = binary_to_list(<>), + {ok, Tokens, _} = erl_scan:string(Str), + {ok, Term} = erl_parse:parse_term(Tokens), + Term. + +term_to_expr(Term) -> + list_to_binary(io_lib:print(Term, 1, 999999, -1)). + +-spec now_to_usec(erlang:timestamp()) -> non_neg_integer(). +now_to_usec({MSec, Sec, USec}) -> + (MSec*1000000 + Sec)*1000000 + USec. + +-spec usec_to_now(non_neg_integer()) -> erlang:timestamp(). +usec_to_now(Int) -> + Secs = Int div 1000000, + USec = Int rem 1000000, + MSec = Secs div 1000000, + Sec = Secs rem 1000000, + {MSec, Sec, USec}. + +l2i(I) when is_integer(I) -> I; +l2i(L) when is_binary(L) -> binary_to_integer(L). + +i2l(I) when is_integer(I) -> integer_to_binary(I); +i2l(L) when is_binary(L) -> L. + +i2l(I, N) when is_integer(I) -> i2l(i2l(I), N); +i2l(L, N) when is_binary(L) -> + case str:len(L) of + N -> L; + C when C > N -> L; + _ -> i2l(<<$0, L/binary>>, N) + end. + +-spec encode_pid(pid()) -> binary(). +encode_pid(Pid) -> + list_to_binary(erlang:pid_to_list(Pid)). + +-spec decode_pid(binary(), binary()) -> pid(). +decode_pid(PidBin, NodeBin) -> + PidStr = binary_to_list(PidBin), + Pid = erlang:list_to_pid(PidStr), + case erlang:binary_to_atom(NodeBin, latin1) of + Node when Node == node() -> + Pid; + Node -> + try set_node_id(PidStr, NodeBin) + catch _:badarg -> + erlang:error({bad_node, Node}) + end + end. + +-spec compile_exprs(module(), [string()]) -> ok | {error, any()}. +compile_exprs(Mod, Exprs) -> + try + Forms = lists:map( + fun(Expr) -> + {ok, Tokens, _} = erl_scan:string(lists:flatten(Expr)), + {ok, Form} = erl_parse:parse_form(Tokens), + Form + end, Exprs), + {ok, Code} = case compile:forms(Forms, []) of + {ok, Mod, Bin} -> {ok, Bin}; + {ok, Mod, Bin, _Warnings} -> {ok, Bin}; + Error -> Error + end, + {module, Mod} = code:load_binary(Mod, "nofile", Code), + ok + catch _:{badmatch, {error, ErrInfo, _ErrLocation}} -> + {error, ErrInfo}; + _:{badmatch, {error, _} = Err} -> + Err; + _:{badmatch, error} -> + {error, compile_failed} + end. + +-spec join_atoms([atom()], binary()) -> binary(). +join_atoms(Atoms, Sep) -> + str:join([io_lib:format("~p", [A]) || A <- lists:sort(Atoms)], Sep). + +%% @doc Checks if the file is readable and converts its name to binary. +%% Fails with `badarg' otherwise. The function is intended for usage +%% in configuration validators only. +-spec try_read_file(file:filename_all()) -> binary(). +try_read_file(Path) -> + case file:open(Path, [read]) of + {ok, Fd} -> + file:close(Fd), + iolist_to_binary(Path); + {error, Why} -> + ?ERROR_MSG("Failed to read ~ts: ~ts", [Path, file:format_error(Why)]), + erlang:error(badarg) + end. + +-spec css_dir() -> file:filename(). +css_dir() -> + get_dir("css"). + +-spec img_dir() -> file:filename(). +img_dir() -> + get_dir("img"). + +-spec js_dir() -> file:filename(). +js_dir() -> + get_dir("js"). + +-spec msgs_dir() -> file:filename(). +msgs_dir() -> + get_dir("msgs"). + +-spec sql_dir() -> file:filename(). +sql_dir() -> + get_dir("sql"). + +-spec lua_dir() -> file:filename(). +lua_dir() -> + get_dir("lua"). + +-spec read_css(file:filename()) -> {ok, binary()} | {error, file:posix()}. +read_css(File) -> + read_file(filename:join(css_dir(), File)). + +-spec read_img(file:filename()) -> {ok, binary()} | {error, file:posix()}. +read_img(File) -> + read_file(filename:join(img_dir(), File)). + +-spec read_js(file:filename()) -> {ok, binary()} | {error, file:posix()}. +read_js(File) -> + read_file(filename:join(js_dir(), File)). + +-spec read_lua(file:filename()) -> {ok, binary()} | {error, file:posix()}. +read_lua(File) -> + read_file(filename:join(lua_dir(), File)). + +-spec get_descr(binary(), binary()) -> binary(). +get_descr(Lang, Text) -> + Desc = translate:translate(Lang, Text), + Copyright = ejabberd_config:get_copyright(), + <>. + +-spec get_home() -> string(). +get_home() -> + case init:get_argument(home) of + {ok, [[Home]]} -> + Home; + error -> + mnesia:system_info(directory) + end. + +warn_unset_home() -> + case init:get_argument(home) of + {ok, [[_Home]]} -> + ok; + error -> + ?INFO_MSG("The 'HOME' environment variable is not set, " + "ejabberd will use as HOME the Mnesia directory: ~s.", + [mnesia:system_info(directory)]) + end. + +-spec intersection(list(), list()) -> list(). +intersection(L1, L2) -> + lists:filter( + fun(E) -> + lists:member(E, L2) + end, L1). + +-spec format_val(any()) -> iodata(). +format_val({yaml, S}) when is_integer(S); is_binary(S); is_atom(S) -> + format_val(S); +format_val({yaml, YAML}) -> + S = try fast_yaml:encode(YAML) + catch _:_ -> YAML + end, + format_val(S); +format_val(I) when is_integer(I) -> + integer_to_list(I); +format_val(B) when is_atom(B) -> + erlang:atom_to_binary(B, utf8); +format_val(Term) -> + S = try iolist_to_binary(Term) + catch _:_ -> list_to_binary(io_lib:format("~p", [Term])) + end, + case binary:match(S, <<"\n">>) of + nomatch -> S; + _ -> [io_lib:nl(), S] + end. + +-spec cancel_timer(reference() | undefined) -> ok. +cancel_timer(TRef) when is_reference(TRef) -> + case erlang:cancel_timer(TRef) of + false -> + receive {timeout, TRef, _} -> ok + after 0 -> ok + end; + _ -> + ok + end; +cancel_timer(_) -> + ok. + +-spec best_match(atom() | binary() | string(), + [atom() | binary() | string()]) -> string(). +best_match(Pattern, []) -> + Pattern; +best_match(Pattern, Opts) -> + String = to_string(Pattern), + {Ds, _} = lists:mapfoldl( + fun(Opt, Cache) -> + SOpt = to_string(Opt), + {Distance, Cache1} = ld(String, SOpt, Cache), + {{Distance, SOpt}, Cache1} + end, #{}, Opts), + element(2, lists:min(Ds)). + +-spec logical_processors() -> non_neg_integer(). +logical_processors() -> + case erlang:system_info(logical_processors) of + V when is_integer(V), V >= 2 -> V; + _ -> 1 + end. + +-spec pmap(fun((T1) -> T2), [T1]) -> [T2]. +pmap(Fun, [_,_|_] = List) -> + case logical_processors() of + 1 -> lists:map(Fun, List); + _ -> + Self = self(), + lists:map( + fun({Pid, Ref}) -> + receive + {Pid, Ret} -> + receive + {'DOWN', Ref, _, _, _} -> + Ret + end; + {'DOWN', Ref, _, _, Reason} -> + exit(Reason) + end + end, [spawn_monitor( + fun() -> Self ! {self(), Fun(X)} end) + || X <- List]) + end; +pmap(Fun, List) -> + lists:map(Fun, List). + +-spec peach(fun((T) -> any()), [T]) -> ok. +peach(Fun, [_,_|_] = List) -> + case logical_processors() of + 1 -> lists:foreach(Fun, List); + _ -> + Self = self(), + lists:foreach( + fun({Pid, Ref}) -> + receive + Pid -> + receive + {'DOWN', Ref, _, _, _} -> + ok + end; + {'DOWN', Ref, _, _, Reason} -> + exit(Reason) + end + end, [spawn_monitor( + fun() -> Fun(X), Self ! self() end) + || X <- List]) + end; +peach(Fun, List) -> + lists:foreach(Fun, List). + +-ifdef(HAVE_ERL_ERROR). +format_exception(Level, Class, Reason, Stacktrace) -> + erl_error:format_exception( + Level, Class, Reason, Stacktrace, + fun(_M, _F, _A) -> false end, + fun(Term, I) -> + io_lib:print(Term, I, 80, -1) + end). +-else. +format_exception(Level, Class, Reason, Stacktrace) -> + lib:format_exception( + Level, Class, Reason, Stacktrace, + fun(_M, _F, _A) -> false end, + fun(Term, I) -> + io_lib:print(Term, I, 80, -1) + end). +-endif. + +-spec get_my_ipv4_address() -> inet:ip4_address(). +get_my_ipv4_address() -> + {ok, MyHostName} = inet:gethostname(), + case inet:getaddr(MyHostName, inet) of + {ok, Addr} -> Addr; + {error, _} -> {127, 0, 0, 1} + end. + +-spec get_my_ipv6_address() -> inet:ip6_address(). +get_my_ipv6_address() -> + {ok, MyHostName} = inet:gethostname(), + case inet:getaddr(MyHostName, inet6) of + {ok, Addr} -> Addr; + {error, _} -> {0, 0, 0, 0, 0, 0, 0, 1} + end. + +-spec parse_ip_mask(binary()) -> {ok, {inet:ip4_address(), 0..32}} | + {ok, {inet:ip6_address(), 0..128}} | + error. +parse_ip_mask(S) -> + case econf:validate(econf:ip_mask(), S) of + {ok, _} = Ret -> Ret; + _ -> error + end. + +-spec match_ip_mask(inet:ip_address(), inet:ip_address(), 0..128) -> boolean(). +match_ip_mask({_, _, _, _} = IP, {_, _, _, _} = Net, Mask) -> + IPInt = ip_to_integer(IP), + NetInt = ip_to_integer(Net), + M = bnot (1 bsl (32 - Mask) - 1), + IPInt band M =:= NetInt band M; +match_ip_mask({_, _, _, _, _, _, _, _} = IP, + {_, _, _, _, _, _, _, _} = Net, Mask) -> + IPInt = ip_to_integer(IP), + NetInt = ip_to_integer(Net), + M = bnot (1 bsl (128 - Mask) - 1), + IPInt band M =:= NetInt band M; +match_ip_mask({_, _, _, _} = IP, + {0, 0, 0, 0, 0, 16#FFFF, _, _} = Net, Mask) -> + IPInt = ip_to_integer({0, 0, 0, 0, 0, 16#FFFF, 0, 0}) + ip_to_integer(IP), + NetInt = ip_to_integer(Net), + M = bnot (1 bsl (128 - Mask) - 1), + IPInt band M =:= NetInt band M; +match_ip_mask({0, 0, 0, 0, 0, 16#FFFF, _, _} = IP, + {_, _, _, _} = Net, Mask) -> + IPInt = ip_to_integer(IP) - ip_to_integer({0, 0, 0, 0, 0, 16#FFFF, 0, 0}), + NetInt = ip_to_integer(Net), + M = bnot (1 bsl (32 - Mask) - 1), + IPInt band M =:= NetInt band M; +match_ip_mask(_, _, _) -> + false. + +-spec format_hosts_list([binary(), ...]) -> iolist(). +format_hosts_list([Host]) -> + Host; +format_hosts_list([H1, H2]) -> + [H1, " and ", H2]; +format_hosts_list([H1, H2, H3]) -> + [H1, ", ", H2, " and ", H3]; +format_hosts_list([H1, H2|Hs]) -> + io_lib:format("~ts, ~ts and ~B more hosts", + [H1, H2, length(Hs)]). + +-spec format_cycle([atom(), ...]) -> iolist(). +format_cycle([M1]) -> + atom_to_list(M1); +format_cycle([M1, M2]) -> + [atom_to_list(M1), " and ", atom_to_list(M2)]; +format_cycle([M|Ms]) -> + atom_to_list(M) ++ ", " ++ format_cycle(Ms). + +-spec delete_dir(file:filename_all()) -> ok | {error, file:posix()}. +delete_dir(Dir) -> + try + {ok, Entries} = file:list_dir(Dir), + lists:foreach(fun(Path) -> + case filelib:is_dir(Path) of + true -> + ok = delete_dir(Path); + false -> + ok = file:delete(Path) + end + end, [filename:join(Dir, Entry) || Entry <- Entries]), + ok = file:del_dir(Dir) + catch + _:{badmatch, {error, Error}} -> + {error, Error} + end. + +-spec semver_to_xxyy(binary()) -> binary(). +semver_to_xxyy(<>) -> + <>; +semver_to_xxyy(<>) -> + <>; +semver_to_xxyy(<>) -> + <>; +semver_to_xxyy(Version) when is_binary(Version) -> + Version. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +-spec url_encode(binary(), binary()) -> binary(). +url_encode(<>, Acc) when + (H >= $a andalso H =< $z) orelse + (H >= $A andalso H =< $Z) orelse + (H >= $0 andalso H =< $9) orelse + H == $_ orelse + H == $. orelse + H == $- orelse + H == $/ orelse + H == $: -> + url_encode(T, <>); +url_encode(<>, Acc) -> + case integer_to_list(H, 16) of + [X, Y] -> url_encode(T, <>); + [X] -> url_encode(T, <>) + end; +url_encode(<<>>, Acc) -> + Acc. + +-spec set_node_id(string(), binary()) -> pid(). +set_node_id(PidStr, NodeBin) -> + ExtPidStr = erlang:pid_to_list( + binary_to_term( + <<131,103,100,(size(NodeBin)):16,NodeBin/binary,0:72>>)), + [H|_] = string:tokens(ExtPidStr, "."), + [_|T] = string:tokens(PidStr, "."), + erlang:list_to_pid(string:join([H|T], ".")). + +-spec read_file(file:filename()) -> {ok, binary()} | {error, file:posix()}. +read_file(Path) -> + case file:read_file(Path) of + {ok, Data} -> + {ok, Data}; + {error, Why} = Err -> + ?ERROR_MSG("Failed to read file ~ts: ~ts", + [Path, file:format_error(Why)]), + Err + end. + +-spec get_dir(string()) -> file:filename(). +get_dir(Type) -> + Env = "EJABBERD_" ++ string:to_upper(Type) ++ "_PATH", + case os:getenv(Env) of + false -> + case code:priv_dir(ejabberd) of + {error, _} -> filename:join(["priv", Type]); + Path -> filename:join([Path, Type]) + end; + Path -> + Path + end. + +%% Generates erlang:timestamp() that is guaranteed to unique +-spec unique_timestamp() -> erlang:timestamp(). +unique_timestamp() -> + {MS, S, _} = erlang:timestamp(), + {MS, S, erlang:unique_integer([positive, monotonic]) rem 1000000}. + +%% Levenshtein distance +-spec ld(string(), string(), distance_cache()) -> {non_neg_integer(), distance_cache()}. +ld([] = S, T, Cache) -> + {length(T), maps:put({S, T}, length(T), Cache)}; +ld(S, [] = T, Cache) -> + {length(S), maps:put({S, T}, length(S), Cache)}; +ld([X|S], [X|T], Cache) -> + ld(S, T, Cache); +ld([_|ST] = S, [_|TT] = T, Cache) -> + try {maps:get({S, T}, Cache), Cache} + catch _:{badkey, _} -> + {L1, C1} = ld(S, TT, Cache), + {L2, C2} = ld(ST, T, C1), + {L3, C3} = ld(ST, TT, C2), + L = 1 + lists:min([L1, L2, L3]), + {L, maps:put({S, T}, L, C3)} + end. + +-spec ip_to_integer(inet:ip_address()) -> non_neg_integer(). +ip_to_integer({IP1, IP2, IP3, IP4}) -> + IP1 bsl 8 bor IP2 bsl 8 bor IP3 bsl 8 bor IP4; +ip_to_integer({IP1, IP2, IP3, IP4, IP5, IP6, IP7, + IP8}) -> + IP1 bsl 16 bor IP2 bsl 16 bor IP3 bsl 16 bor IP4 bsl 16 + bor IP5 bsl 16 bor IP6 bsl 16 bor IP7 bsl 16 bor IP8. + +-spec to_string(atom() | binary() | string()) -> string(). +to_string(A) when is_atom(A) -> + atom_to_list(A); +to_string(B) when is_binary(B) -> + binary_to_list(B); +to_string(S) -> + S. + +-ifdef(OTP_BELOW_27). +set_proc_label(_Label) -> + ok. +-else. +set_proc_label(Label) -> + proc_lib:set_label(Label). +-endif. + +-ifdef(OTP_BELOW_25). +-spec lists_uniq([term()]) -> [term()]. +lists_uniq(List) -> + lists_uniq_int(List, #{}). + +lists_uniq_int([El | Rest], Existing) -> + case maps:is_key(El, Existing) of + true -> lists_uniq_int(Rest, Existing); + _ -> [El | lists_uniq_int(Rest, Existing#{El => true})] + end; +lists_uniq_int([], _) -> + []. +-else. +-spec lists_uniq([term()]) -> [term()]. +lists_uniq(List) -> + lists:uniq(List). +-endif. diff --git a/src/mod_adhoc.erl b/src/mod_adhoc.erl index ec41e73f5..231480b3d 100644 --- a/src/mod_adhoc.erl +++ b/src/mod_adhoc.erl @@ -5,7 +5,7 @@ %%% Created : 15 Nov 2005 by Magnus Henoch %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -27,77 +27,45 @@ -author('henoch@dtek.chalmers.se'). +-protocol({xep, 50, '1.2', '1.1.0', "complete", ""}). + -behaviour(gen_mod). --export([start/2, stop/1, process_local_iq/3, - process_sm_iq/3, get_local_commands/5, +-export([start/2, stop/1, reload/3, process_local_iq/1, + process_sm_iq/1, get_local_commands/5, get_local_identity/5, get_local_features/5, get_sm_commands/5, get_sm_identity/5, get_sm_features/5, - ping_item/4, ping_command/4]). + ping_item/4, ping_command/4, mod_opt_type/1, depends/2, + mod_options/1, mod_doc/0]). --include("ejabberd.hrl"). -include("logger.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). +-include("translate.hrl"). --include("jlib.hrl"). +start(_Host, _Opts) -> + {ok, [{iq_handler, ejabberd_local, ?NS_COMMANDS, process_local_iq}, + {iq_handler, ejabberd_sm, ?NS_COMMANDS, process_sm_iq}, + {hook, disco_local_identity, get_local_identity, 99}, + {hook, disco_local_features, get_local_features, 99}, + {hook, disco_local_items, get_local_commands, 99}, + {hook, disco_sm_identity, get_sm_identity, 99}, + {hook, disco_sm_features, get_sm_features, 99}, + {hook, disco_sm_items, get_sm_commands, 99}, + {hook, adhoc_local_items, ping_item, 100}, + {hook, adhoc_local_commands, ping_command, 100}]}. --include("adhoc.hrl"). +stop(_Host) -> + ok. -start(Host, Opts) -> - IQDisc = gen_mod:get_opt(iqdisc, Opts, fun gen_iq_handler:check_type/1, - one_queue), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, - ?NS_COMMANDS, ?MODULE, process_local_iq, - IQDisc), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, - ?NS_COMMANDS, ?MODULE, process_sm_iq, IQDisc), - ejabberd_hooks:add(disco_local_identity, Host, ?MODULE, - get_local_identity, 99), - ejabberd_hooks:add(disco_local_features, Host, ?MODULE, - get_local_features, 99), - ejabberd_hooks:add(disco_local_items, Host, ?MODULE, - get_local_commands, 99), - ejabberd_hooks:add(disco_sm_identity, Host, ?MODULE, - get_sm_identity, 99), - ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, - get_sm_features, 99), - ejabberd_hooks:add(disco_sm_items, Host, ?MODULE, - get_sm_commands, 99), - ejabberd_hooks:add(adhoc_local_items, Host, ?MODULE, - ping_item, 100), - ejabberd_hooks:add(adhoc_local_commands, Host, ?MODULE, - ping_command, 100). - -stop(Host) -> - ejabberd_hooks:delete(adhoc_local_commands, Host, - ?MODULE, ping_command, 100), - ejabberd_hooks:delete(adhoc_local_items, Host, ?MODULE, - ping_item, 100), - ejabberd_hooks:delete(disco_sm_items, Host, ?MODULE, - get_sm_commands, 99), - ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, - get_sm_features, 99), - ejabberd_hooks:delete(disco_sm_identity, Host, ?MODULE, - get_sm_identity, 99), - ejabberd_hooks:delete(disco_local_items, Host, ?MODULE, - get_local_commands, 99), - ejabberd_hooks:delete(disco_local_features, Host, - ?MODULE, get_local_features, 99), - ejabberd_hooks:delete(disco_local_identity, Host, - ?MODULE, get_local_identity, 99), - gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, - ?NS_COMMANDS), - gen_iq_handler:remove_iq_handler(ejabberd_local, Host, - ?NS_COMMANDS). +reload(_Host, _NewOpts, _OldOpts) -> + ok. %------------------------------------------------------------------------- - +-spec get_local_commands(mod_disco:items_acc(), jid(), jid(), binary(), binary()) -> mod_disco:items_acc(). get_local_commands(Acc, _From, #jid{server = Server, lserver = LServer} = _To, <<"">>, Lang) -> - Display = gen_mod:get_module_opt(LServer, ?MODULE, - report_commands_node, - fun(B) when is_boolean(B) -> B end, - false), + Display = mod_adhoc_opt:report_commands_node(LServer), case Display of false -> Acc; _ -> @@ -105,12 +73,9 @@ get_local_commands(Acc, _From, {result, I} -> I; _ -> [] end, - Nodes = [#xmlel{name = <<"item">>, - attrs = - [{<<"jid">>, Server}, {<<"node">>, ?NS_COMMANDS}, - {<<"name">>, - translate:translate(Lang, <<"Commands">>)}], - children = []}], + Nodes = [#disco_item{jid = jid:make(Server), + node = ?NS_COMMANDS, + name = translate:translate(Lang, ?T("Commands"))}], {result, Items ++ Nodes} end; get_local_commands(_Acc, From, @@ -124,13 +89,10 @@ get_local_commands(Acc, _From, _To, _Node, _Lang) -> Acc. %------------------------------------------------------------------------- - +-spec get_sm_commands(mod_disco:items_acc(), jid(), jid(), binary(), binary()) -> mod_disco:items_acc(). get_sm_commands(Acc, _From, #jid{lserver = LServer} = To, <<"">>, Lang) -> - Display = gen_mod:get_module_opt(LServer, ?MODULE, - report_commands_node, - fun(B) when is_boolean(B) -> B end, - false), + Display = mod_adhoc_opt:report_commands_node(LServer), case Display of false -> Acc; _ -> @@ -138,13 +100,9 @@ get_sm_commands(Acc, _From, {result, I} -> I; _ -> [] end, - Nodes = [#xmlel{name = <<"item">>, - attrs = - [{<<"jid">>, jlib:jid_to_string(To)}, - {<<"node">>, ?NS_COMMANDS}, - {<<"name">>, - translate:translate(Lang, <<"Commands">>)}], - children = []}], + Nodes = [#disco_item{jid = To, + node = ?NS_COMMANDS, + name = translate:translate(Lang, ?T("Commands"))}], {result, Items ++ Nodes} end; get_sm_commands(_Acc, From, @@ -154,45 +112,34 @@ get_sm_commands(_Acc, From, get_sm_commands(Acc, _From, _To, _Node, _Lang) -> Acc. %------------------------------------------------------------------------- - +-spec get_local_identity([identity()], jid(), jid(), binary(), binary()) -> [identity()]. %% On disco info request to the ad-hoc node, return automation/command-list. get_local_identity(Acc, _From, _To, ?NS_COMMANDS, Lang) -> - [#xmlel{name = <<"identity">>, - attrs = - [{<<"category">>, <<"automation">>}, - {<<"type">>, <<"command-list">>}, - {<<"name">>, - translate:translate(Lang, <<"Commands">>)}], - children = []} + [#identity{category = <<"automation">>, + type = <<"command-list">>, + name = translate:translate(Lang, ?T("Commands"))} | Acc]; get_local_identity(Acc, _From, _To, <<"ping">>, Lang) -> - [#xmlel{name = <<"identity">>, - attrs = - [{<<"category">>, <<"automation">>}, - {<<"type">>, <<"command-node">>}, - {<<"name">>, translate:translate(Lang, <<"Ping">>)}], - children = []} + [#identity{category = <<"automation">>, + type = <<"command-node">>, + name = translate:translate(Lang, ?T("Ping"))} | Acc]; get_local_identity(Acc, _From, _To, _Node, _Lang) -> Acc. %------------------------------------------------------------------------- - +-spec get_sm_identity([identity()], jid(), jid(), binary(), binary()) -> [identity()]. %% On disco info request to the ad-hoc node, return automation/command-list. get_sm_identity(Acc, _From, _To, ?NS_COMMANDS, Lang) -> - [#xmlel{name = <<"identity">>, - attrs = - [{<<"category">>, <<"automation">>}, - {<<"type">>, <<"command-list">>}, - {<<"name">>, - translate:translate(Lang, <<"Commands">>)}], - children = []} + [#identity{category = <<"automation">>, + type = <<"command-list">>, + name = translate:translate(Lang, ?T("Commands"))} | Acc]; get_sm_identity(Acc, _From, _To, _Node, _Lang) -> Acc. %------------------------------------------------------------------------- - +-spec get_local_features(mod_disco:features_acc(), jid(), jid(), binary(), binary()) -> mod_disco:features_acc(). get_local_features(Acc, _From, _To, <<"">>, _Lang) -> Feats = case Acc of {result, I} -> I; @@ -209,7 +156,7 @@ get_local_features(Acc, _From, _To, _Node, _Lang) -> Acc. %------------------------------------------------------------------------- - +-spec get_sm_features(mod_disco:features_acc(), jid(), jid(), binary(), binary()) -> mod_disco:features_acc(). get_sm_features(Acc, _From, _To, <<"">>, _Lang) -> Feats = case Acc of {result, I} -> I; @@ -222,59 +169,97 @@ get_sm_features(_Acc, _From, _To, ?NS_COMMANDS, get_sm_features(Acc, _From, _To, _Node, _Lang) -> Acc. %------------------------------------------------------------------------- +-spec process_local_iq(iq()) -> iq() | ignore. +process_local_iq(IQ) -> + process_adhoc_request(IQ, local). -process_local_iq(From, To, IQ) -> - process_adhoc_request(From, To, IQ, - adhoc_local_commands). +-spec process_sm_iq(iq()) -> iq() | ignore. +process_sm_iq(IQ) -> + process_adhoc_request(IQ, sm). -process_sm_iq(From, To, IQ) -> - process_adhoc_request(From, To, IQ, adhoc_sm_commands). - -process_adhoc_request(From, To, - #iq{sub_el = SubEl} = IQ, Hook) -> - ?DEBUG("About to parse ~p...", [IQ]), - case adhoc:parse_request(IQ) of - {error, Error} -> - IQ#iq{type = error, sub_el = [SubEl, Error]}; - #adhoc_request{} = AdhocRequest -> - Host = To#jid.lserver, - case ejabberd_hooks:run_fold(Hook, Host, empty, - [From, To, AdhocRequest]) - of - ignore -> ignore; - empty -> - IQ#iq{type = error, - sub_el = [SubEl, ?ERR_ITEM_NOT_FOUND]}; - {error, Error} -> - IQ#iq{type = error, sub_el = [SubEl, Error]}; - Command -> IQ#iq{type = result, sub_el = [Command]} - end - end. +-spec process_adhoc_request(iq(), sm | local) -> iq() | ignore. +process_adhoc_request(#iq{from = From, to = To, + type = set, lang = Lang, + sub_els = [#adhoc_command{} = SubEl]} = IQ, Type) -> + Host = To#jid.lserver, + Res = case Type of + local -> + ejabberd_hooks:run_fold(adhoc_local_commands, Host, empty, + [From, To, fix_lang(Lang, SubEl)]); + sm -> + ejabberd_hooks:run_fold(adhoc_sm_commands, Host, empty, + [From, To, fix_lang(Lang, SubEl)]) + end, + case Res of + ignore -> + ignore; + empty -> + Txt = ?T("No hook has processed this command"), + xmpp:make_error(IQ, xmpp:err_item_not_found(Txt, Lang)); + {error, Error} -> + xmpp:make_error(IQ, Error); + Command -> + xmpp:make_iq_result(IQ, Command) + end; +process_adhoc_request(#iq{} = IQ, _Hooks) -> + xmpp:make_error(IQ, xmpp:err_bad_request()). +-spec ping_item(mod_disco:items_acc(), jid(), jid(), binary()) -> {result, [disco_item()]}. ping_item(Acc, _From, #jid{server = Server} = _To, Lang) -> Items = case Acc of {result, I} -> I; _ -> [] end, - Nodes = [#xmlel{name = <<"item">>, - attrs = - [{<<"jid">>, Server}, {<<"node">>, <<"ping">>}, - {<<"name">>, translate:translate(Lang, <<"Ping">>)}], - children = []}], + Nodes = [#disco_item{jid = jid:make(Server), + node = <<"ping">>, + name = translate:translate(Lang, ?T("Ping"))}], {result, Items ++ Nodes}. +-spec ping_command(adhoc_command(), jid(), jid(), adhoc_command()) -> + adhoc_command() | {error, stanza_error()}. ping_command(_Acc, _From, _To, - #adhoc_request{lang = Lang, node = <<"ping">>, - sessionid = _Sessionid, action = Action} = - Request) -> - if Action == <<"">>; Action == <<"execute">> -> - adhoc:produce_response(Request, - #adhoc_response{status = completed, - notes = - [{<<"info">>, - translate:translate(Lang, - <<"Pong">>)}]}); - true -> {error, ?ERR_BAD_REQUEST} + #adhoc_command{lang = Lang, node = <<"ping">>, + action = Action} = Request) -> + if Action == execute -> + xmpp_util:make_adhoc_response( + Request, + #adhoc_command{ + status = completed, + notes = [#adhoc_note{ + type = info, + data = translate:translate(Lang, ?T("Pong"))}]}); + true -> + Txt = ?T("Incorrect value of 'action' attribute"), + {error, xmpp:err_bad_request(Txt, Lang)} end; ping_command(Acc, _From, _To, _Request) -> Acc. + +-spec fix_lang(binary(), adhoc_command()) -> adhoc_command(). +fix_lang(Lang, #adhoc_command{lang = <<>>} = Cmd) -> + Cmd#adhoc_command{lang = Lang}; +fix_lang(_, Cmd) -> + Cmd. + +depends(_Host, _Opts) -> + []. + +mod_opt_type(report_commands_node) -> + econf:bool(). + +mod_options(_Host) -> + [{report_commands_node, false}]. + +mod_doc() -> + #{desc => + [?T("def:ad-hoc command"), "", + ?T(": Command that can be executed by an XMPP client using XEP-0050."), "", + ?T("This module implements https://xmpp.org/extensions/xep-0050.html" + "[XEP-0050: Ad-Hoc Commands]. It's an auxiliary module and is " + "only needed by some of the other modules.")], + opts => + [{report_commands_node, + #{value => "true | false", + desc => + ?T("Provide the Commands item in the Service Discovery. " + "Default value: 'false'.")}}]}. diff --git a/src/mod_adhoc_api.erl b/src/mod_adhoc_api.erl new file mode 100644 index 000000000..73c95bc10 --- /dev/null +++ b/src/mod_adhoc_api.erl @@ -0,0 +1,731 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_adhoc_api.erl +%%% Author : Badlop +%%% Purpose : Frontend for ejabberd API Commands via XEP-0050 Ad-Hoc Commands +%%% Created : 21 Feb 2025 by Badlop +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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. +%%% +%%%---------------------------------------------------------------------- + +%%%% definitions +%% @format-begin + +-module(mod_adhoc_api). + +-behaviour(gen_mod). + +-author('badlop@process-one.net'). + +%% gen_mod callbacks +-export([start/2, stop/1, reload/3, mod_opt_type/1, mod_options/1, depends/2, mod_doc/0]). +%% hooks +-export([adhoc_local_commands/4, adhoc_local_items/4, disco_local_features/5, + disco_local_identity/5, disco_local_items/5]). + +-include("ejabberd_commands.hrl"). +-include("ejabberd_sm.hrl"). +-include("logger.hrl"). +-include("translate.hrl"). + +-include_lib("stdlib/include/ms_transform.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). + +-define(DEFAULT_API_VERSION, 1000000). + +%%%================================== +%%%% gen_mod + +start(_Host, _Opts) -> + {ok, + [{hook, adhoc_local_commands, adhoc_local_commands, 40}, + {hook, adhoc_local_items, adhoc_local_items, 40}, + {hook, disco_local_features, disco_local_features, 40}, + {hook, disco_local_identity, disco_local_identity, 40}, + {hook, disco_local_items, disco_local_items, 40}]}. + +stop(_Host) -> + ok. + +reload(_Host, _NewOpts, _OldOpts) -> + ok. + +mod_opt_type(default_version) -> + econf:either( + econf:int(0, 3), + econf:and_then( + econf:binary(), + fun(Binary) -> + case binary_to_list(Binary) of + F when F >= "24.06" -> + 2; + F when (F > "23.10") and (F < "24.06") -> + 1; + F when F =< "23.10" -> + 0 + end + end)). + +-spec mod_options(binary()) -> [{default_version, integer()}]. +mod_options(_) -> + [{default_version, ?DEFAULT_API_VERSION}]. + +depends(_Host, _Opts) -> + [{mod_adhoc, hard}, {mod_last, soft}]. + +mod_doc() -> + #{desc => + ?T("Execute (def:API commands) " + "in a XMPP client using " + "https://xmpp.org/extensions/xep-0050.html[XEP-0050: Ad-Hoc Commands]. " + "This module requires _`mod_adhoc`_ (to execute the commands), " + "and recommends _`mod_disco`_ (to discover the commands)."), + note => "added in 25.03", + opts => + [{default_version, + #{value => "integer() | string()", + desc => + ?T("What API version to use. " + "If setting an ejabberd version, it will use the latest API " + "version that was available in that (def:c2s) ejabberd version. " + "For example, setting '\"24.06\"' in this option implies '2'. " + "The default value is the latest version.")}}], + example => + ["acl:", + " admin:", + " user: jan@localhost", + "", + "api_permissions:", + " \"adhoc commands\":", + " from: mod_adhoc_api", + " who: admin", + " what:", + " - \"[tag:roster]\"", + " - \"[tag:session]\"", + " - stats", + " - status", + "", + "modules:", + " mod_adhoc_api:", + " default_version: 2"]}. + +%%%================================== +%%%% Ad-Hoc Commands (copied from mod_configure) + +-define(INFO_IDENTITY(Category, Type, Name, Lang), + [#identity{category = Category, + type = Type, + name = tr(Lang, Name)}]). +-define(INFO_COMMAND(Name, Lang), + ?INFO_IDENTITY(<<"automation">>, <<"command-node">>, Name, Lang)). +-define(NODE(Name, Node), + #disco_item{jid = jid:make(Server), + node = Node, + name = tr(Lang, Name)}). + +-spec tokenize(binary()) -> [binary()]. +tokenize(Node) -> + str:tokens(Node, <<"/#">>). + +-spec tr(binary(), binary()) -> binary(). +tr(Lang, Text) -> + translate:translate(Lang, Text). + +%%%================================== +%%%% - disco identity + +-spec disco_local_identity([identity()], jid(), jid(), binary(), binary()) -> + [identity()]. +disco_local_identity(Acc, _From, #jid{lserver = LServer} = _To, Node, Lang) -> + case tokenize(Node) of + [<<"api-commands">>] -> + ?INFO_COMMAND(?T("API Commands"), Lang); + [<<"api-commands">>, CommandName] -> + ?INFO_COMMAND(get_api_command_desc(CommandName, LServer), Lang); + _ -> + Acc + end. + +get_api_command_desc(NameAtom, Host) -> + iolist_to_binary((get_api_command(NameAtom, Host))#ejabberd_commands.desc). + +%%%================================== +%%%% - disco features + +-spec disco_local_features(mod_disco:features_acc(), jid(), jid(), binary(), binary()) -> + mod_disco:features_acc(). +disco_local_features(Acc, _From, #jid{lserver = LServer} = _To, Node, _Lang) -> + case gen_mod:is_loaded(LServer, mod_adhoc) of + false -> + Acc; + _ -> + case tokenize(Node) of + [<<"api-commands">>] -> + {result, []}; + [<<"api-commands">>, _] -> + {result, [?NS_COMMANDS]}; + _ -> + Acc + end + end. + +%%%================================== +%%%% - adhoc items + +-spec adhoc_local_items(mod_disco:items_acc(), jid(), jid(), binary()) -> + mod_disco:items_acc(). +adhoc_local_items(Acc, From, #jid{lserver = LServer, server = Server} = To, Lang) -> + Items = + case Acc of + {result, Its} -> + Its; + empty -> + [] + end, + Nodes = recursively_get_local_items(From, global, LServer, <<"">>, Server, Lang), + Nodes1 = + lists:filter(fun(#disco_item{node = Nd}) -> + F = disco_local_features(empty, From, To, Nd, Lang), + case F of + {result, [?NS_COMMANDS]} -> + true; + _ -> + false + end + end, + Nodes), + {result, Items ++ Nodes1}. + +-spec recursively_get_local_items(jid(), + global | vhost, + binary(), + binary(), + binary(), + binary()) -> + [disco_item()]. +recursively_get_local_items(From, PermLev, LServer, Node, Server, Lang) -> + Items = + case get_local_items2(From, {PermLev, LServer}, tokenize(Node), Server, Lang) of + {result, Res} -> + Res; + {error, _Error} -> + [] + end, + lists:flatten( + lists:map(fun(#disco_item{jid = #jid{server = S}, node = Nd} = Item) -> + if (S /= Server) or (Nd == <<"">>) -> + []; + true -> + [Item, + recursively_get_local_items(From, PermLev, LServer, Nd, Server, Lang)] + end + end, + Items)). + +%%%================================== +%%%% - disco items + +-spec disco_local_items(mod_disco:items_acc(), jid(), jid(), binary(), binary()) -> + mod_disco:items_acc(). +disco_local_items(Acc, From, #jid{lserver = LServer} = To, Node, Lang) -> + case gen_mod:is_loaded(LServer, mod_adhoc) of + false -> + Acc; + _ -> + Items = + case Acc of + {result, Its} -> + Its; + empty -> + []; + Other -> + Other + end, + case tokenize(Node) of + LNode when (LNode == [<<"api-commands">>]) or (LNode == []) -> + case get_local_items2(From, {global, LServer}, LNode, jid:encode(To), Lang) of + {result, Res} -> + {result, Res}; + {error, Error} -> + {error, Error} + end; + _ -> + {result, Items} + end + end. + +%%%================================== +%%%% - get_local_items2 + +-spec get_local_items2(jid(), + {global | vhost, binary()}, + [binary()], + binary(), + binary()) -> + {result, [disco_item()]} | {error, stanza_error()}. +get_local_items2(_From, _Host, [], Server, Lang) -> + {result, [?NODE(?T("API Commands"), <<"api-commands">>)]}; +get_local_items2(From, {_, Host}, [<<"api-commands">>], _Server, Lang) -> + {result, get_api_commands(From, Host, Lang)}; +get_local_items2(_From, {_, _Host}, [<<"api-commands">>, _], _Server, _Lang) -> + {result, []}; +get_local_items2(_From, _Host, _, _Server, _Lang) -> + {error, xmpp:err_item_not_found()}. + +-spec get_api_commands(jid(), binary(), binary()) -> [disco_item()]. +get_api_commands(From, Server, Lang) -> + ApiVersion = mod_adhoc_api_opt:default_version(Server), + lists:map(fun({Name, _Args, _Desc}) -> + NameBin = list_to_binary(atom_to_list(Name)), + ?NODE(NameBin, <<"api-commands/", NameBin/binary>>) + end, + ejabberd_commands:list_commands(ApiVersion, get_caller_info(From))). + +%%%================================== +%%%% - adhoc commands + +-define(COMMANDS_RESULT(LServerOrGlobal, From, To, Request, Lang), + adhoc_local_commands(From, To, Request)). + +-spec adhoc_local_commands(adhoc_command(), jid(), jid(), adhoc_command()) -> + adhoc_command() | {error, stanza_error()}. +adhoc_local_commands(Acc, From, To, #adhoc_command{node = Node} = Request) -> + case tokenize(Node) of + [<<"api-commands">>, _CommandName] -> + ?COMMANDS_RESULT(LServer, From, To, Request, Lang); + _ -> + Acc + end. + +-spec adhoc_local_commands(jid(), jid(), adhoc_command()) -> + adhoc_command() | {error, stanza_error()}. +adhoc_local_commands(From, + #jid{lserver = LServer} = _To, + #adhoc_command{lang = Lang, + node = Node, + sid = SessionID, + action = Action, + xdata = XData} = + Request) -> + LNode = tokenize(Node), + ActionIsExecute = Action == execute orelse Action == complete, + if Action == cancel -> + #adhoc_command{status = canceled, + lang = Lang, + node = Node, + sid = SessionID}; + XData == undefined, ActionIsExecute -> + case get_form(LServer, LNode, Lang) of + {result, Form} -> + xmpp_util:make_adhoc_response(Request, + #adhoc_command{status = executing, xdata = Form}); + {error, Error} -> + {error, Error} + end; + XData /= undefined, ActionIsExecute -> + case set_form(From, LServer, LNode, Lang, XData) of + {result, Res} -> + xmpp_util:make_adhoc_response(Request, + #adhoc_command{xdata = Res, status = completed}); + %%{'EXIT', _} -> {error, xmpp:err_bad_request()}; + {error, Error} -> + {error, Error} + end; + true -> + {error, xmpp:err_bad_request(?T("Unexpected action"), Lang)} + end. + +-spec get_form(binary(), [binary()], binary()) -> + {result, xdata()} | {error, stanza_error()}. +get_form(Host, [<<"api-commands">>, CommandName], Lang) -> + get_form_api_command(CommandName, Host, Lang); +get_form(_Host, _, _Lang) -> + {error, xmpp:err_service_unavailable()}. + +-spec set_form(jid(), binary(), [binary()], binary(), xdata()) -> + {result, xdata() | undefined} | {error, stanza_error()}. +set_form(From, Host, [<<"api-commands">>, Command], Lang, XData) -> + set_form_api_command(From, Host, Command, XData, Lang); +set_form(_From, _Host, _, _Lang, _XData) -> + {error, xmpp:err_service_unavailable()}. + +%%%================================== +%%%% API Commands + +get_api_command(Name, Host) when is_binary(Name) -> + get_api_command(binary_to_existing_atom(Name, latin1), Host); +get_api_command(Name, Host) when is_atom(Name) -> + ApiVersion = mod_adhoc_api_opt:default_version(Host), + ejabberd_commands:get_command_definition(Name, ApiVersion). + +get_caller_info(#jid{user = User, server = Server} = From) -> + #{tag => <<>>, + usr => {User, Server, <<"">>}, + caller_server => Server, + ip => get_ip_address(From), + caller_module => ?MODULE}. + +get_ip_address(#jid{user = User, + server = Server, + resource = Resource}) -> + case ejabberd_sm:get_user_ip(User, Server, Resource) of + {IP, _Port} when is_tuple(IP) -> + IP; + _ -> + error_ip_address + end. + +%%%================================== +%%%% - get form + +get_form_api_command(NameBin, Host, _Lang) -> + Def = get_api_command(NameBin, Host), + Title = list_to_binary(atom_to_list(Def#ejabberd_commands.name)), + Instructions = get_instructions(Def), + FieldsArgs = + build_fields(Def#ejabberd_commands.args, + Def#ejabberd_commands.args_desc, + Def#ejabberd_commands.args_example, + Def#ejabberd_commands.policy, + get_replacements(Host), + true), + FieldsArgsWithHeads = + case FieldsArgs of + [] -> + []; + _ -> + [#xdata_field{type = fixed, label = ?T("Arguments")} | FieldsArgs] + end, + NodeFields = build_node_fields(), + {result, + #xdata{title = Title, + type = form, + instructions = Instructions, + fields = FieldsArgsWithHeads ++ NodeFields}}. + +get_replacements(Host) -> + [{user, <<"">>}, + {localuser, <<"">>}, + {host, Host}, + {localhost, Host}, + {password, <<"">>}, + {newpass, <<"">>}, + {service, mod_muc_admin:find_hosts(Host)}]. + +build_node_fields() -> + build_node_fields([node() | nodes()]). + +build_node_fields([_ThisNode]) -> + []; +build_node_fields(AtomNodes) -> + [ThisNode | _] = Nodes = [atom_to_binary(Atom, latin1) || Atom <- AtomNodes], + Options = [#xdata_option{label = N, value = N} || N <- Nodes], + [#xdata_field{type = fixed, label = ?T("Clustering")}, + #xdata_field{type = 'list-single', + label = <<"ejabberd node">>, + var = <<"mod_adhoc_api_target_node">>, + values = [ThisNode], + options = Options}]. + +%%%================================== +%%%% - set form + +set_form_api_command(From, Host, CommandNameBin, XData, _Lang) -> + %% Description + Def = get_api_command(CommandNameBin, Host), + Title = list_to_binary(atom_to_list(Def#ejabberd_commands.name)), + Instructions = get_instructions(Def), + + %% Arguments + FieldsArgs1 = [Field || Field <- XData#xdata.fields, Field#xdata_field.type /= fixed], + + {Node, FieldsArgs} = + case lists:keytake(<<"mod_adhoc_api_target_node">>, #xdata_field.var, FieldsArgs1) of + {value, #xdata_field{values = [TargetNode]}, FAs} -> + {binary_to_existing_atom(TargetNode, latin1), FAs}; + false -> + {node(), FieldsArgs1} + end, + + FieldsArgsWithHeads = + case FieldsArgs of + [] -> + []; + _ -> + [#xdata_field{type = fixed, label = ?T("Arguments")} | FieldsArgs] + end, + + %% Execute + Arguments = api_extract_fields(FieldsArgs, Def#ejabberd_commands.args), + ApiVersion = mod_adhoc_api_opt:default_version(Host), + CallResult = + ejabberd_cluster:call(Node, + mod_http_api, + handle, + [binary_to_existing_atom(CommandNameBin, latin1), + get_caller_info(From), + Arguments, + ApiVersion]), + + %% Command result + FieldsResult2 = + case CallResult of + {200, RR} -> + build_fields([Def#ejabberd_commands.result], + [Def#ejabberd_commands.result_desc], + [RR], + restricted, + [{host, Host}], + false); + {Code, _ApiErrorCode, MessageBin} -> + [#xdata_field{type = 'text-single', + label = <<"Error ", (integer_to_binary(Code))/binary>>, + values = encode(MessageBin, irrelevat_type), + var = <<"error">>}]; + {Code, MessageBin} -> + [#xdata_field{type = 'text-single', + label = <<"Error ", (integer_to_binary(Code))/binary>>, + values = encode(MessageBin, irrelevat_type), + var = <<"error">>}] + end, + FieldsResultWithHeads = + [#xdata_field{type = fixed, label = <<"">>}, + #xdata_field{type = fixed, label = ?T("Result")} + | FieldsResult2], + + %% Result stanza + {result, + #xdata{title = Title, + type = result, + instructions = Instructions, + fields = FieldsArgsWithHeads ++ FieldsResultWithHeads}}. + +api_extract_fields(Fields, ArgsDef) -> + lists:map(fun(#xdata_field{values = Values, var = ANameBin}) -> + ArgDef = proplists:get_value(binary_to_existing_atom(ANameBin, latin1), ArgsDef), + V = case {Values, ArgDef} of + {Values, {list, {_ElementName, {tuple, ElementsDef}}}} -> + [parse_tuple(ElementsDef, Value) || Value <- Values]; + {[Value], {tuple, ElementsDef}} -> + parse_tuple(ElementsDef, Value); + {[Value], _} -> + Value; + _ -> + Values + end, + {ANameBin, V} + end, + Fields). + +parse_tuple(ElementsDef, Value) -> + Values = str:tokens(Value, <<":">>), + List1 = + [{atom_to_binary(Name, latin1), Val} + || {{Name, _Type}, Val} <- lists:zip(ElementsDef, Values)], + maps:from_list(List1). + +%%%================================== +%%%% - get instructions + +get_instructions(Def) -> + Note2 = + case Def#ejabberd_commands.note of + [] -> + []; + Note -> + N = iolist_to_binary(Note), + [<<"Note: ", N/binary>>] + end, + Tags2 = + case Def#ejabberd_commands.tags of + [] -> + []; + Tags -> + T = str:join([atom_to_binary(Tag, latin1) || Tag <- Tags], <<", ">>), + [<<"Tags: ", T/binary>>] + end, + Module2 = + case Def#ejabberd_commands.definer of + unknown -> + []; + DefinerAtom -> + D = atom_to_binary(DefinerAtom, latin1), + [<<"Module: ", D/binary>>] + end, + Version2 = + case Def#ejabberd_commands.version of + 0 -> + []; + Version -> + V = integer_to_binary(Version), + [<<"API version: ", V/binary>>] + end, + get_instructions2([Def#ejabberd_commands.desc, Def#ejabberd_commands.longdesc] + ++ Note2 + ++ Tags2 + ++ Module2 + ++ Version2). + +get_instructions2(ListStrings) -> + [re:replace(String, "[\t]*[ ]+", " ", [{return, binary}, global]) + || String <- ListStrings, String /= ""]. + +%%%================================== +%%%% - build fields + +build_fields(NameTypes, none, Examples, Policy, Replacements, Required) -> + build_fields(NameTypes, [], Examples, Policy, Replacements, Required); +build_fields(NameTypes, Descs, none, Policy, Replacements, Required) -> + build_fields(NameTypes, Descs, [], Policy, Replacements, Required); +build_fields(NameTypes, [none], Examples, Policy, Replacements, Required) -> + build_fields(NameTypes, [], Examples, Policy, Replacements, Required); +build_fields(NameTypes, Descs, [none], Policy, Replacements, Required) -> + build_fields(NameTypes, Descs, [], Policy, Replacements, Required); +build_fields(NameTypes, Descs, Examples, Policy, Replacements, Required) -> + {NameTypes2, Descs2, Examples2} = + case Policy of + user -> + {[{user, binary}, {host, binary} | NameTypes], + ["Username", "Server host" | Descs], + ["tom", "example.com" | Examples]}; + _ -> + {NameTypes, Descs, Examples} + end, + build_fields2(NameTypes2, Descs2, Examples2, Replacements, Required). + +build_fields2([{_ArgName, {list, _ArgNameType}}] = NameTypes, + Descs, + Examples, + _Replacements, + Required) -> + Args = lists_zip3_pad(NameTypes, Descs, Examples), + lists:map(fun({{AName, AType}, ADesc, AExample}) -> + ANameBin = list_to_binary(atom_to_list(AName)), + #xdata_field{type = 'text-multi', + label = ANameBin, + desc = list_to_binary(ADesc), + values = encode(AExample, AType), + required = Required, + var = ANameBin} + end, + Args); +build_fields2(NameTypes, Descs, Examples, Replacements, Required) -> + Args = lists_zip3_pad(NameTypes, Descs, Examples), + lists:map(fun({{AName, AType}, ADesc, AExample}) -> + ANameBin = list_to_binary(atom_to_list(AName)), + AValue = proplists:get_value(AName, Replacements, AExample), + Values = encode(AValue, AType), + Type = + case {AType, Values} of + {{list, _}, _} -> + 'text-multi'; + {string, [_, _ | _]} -> + 'text-multi'; + _ -> + 'text-single' + end, + #xdata_field{type = Type, + label = ANameBin, + desc = make_desc(ADesc, AValue), + values = Values, + required = Required, + var = ANameBin} + end, + Args). + +-ifdef(OTP_BELOW_26). + +lists_zip3_pad(As, Bs, Cs) -> + lists_zip3_pad(As, Bs, Cs, []). + +lists_zip3_pad([A | As], [B | Bs], [C | Cs], Xs) -> + lists_zip3_pad(As, Bs, Cs, [{A, B, C} | Xs]); +lists_zip3_pad([A | As], [B | Bs], Nil, Xs) when (Nil == none) or (Nil == []) -> + lists_zip3_pad(As, Bs, [], [{A, B, ""} | Xs]); +lists_zip3_pad([A | As], Nil, [C | Cs], Xs) when (Nil == none) or (Nil == []) -> + lists_zip3_pad(As, [], Cs, [{A, "", C} | Xs]); +lists_zip3_pad([A | As], Nil, Nil, Xs) when (Nil == none) or (Nil == []) -> + lists_zip3_pad(As, [], [], [{A, "", ""} | Xs]); +lists_zip3_pad([], Nil, Nil, Xs) when (Nil == none) or (Nil == []) -> + lists:reverse(Xs). + +-endif. + +-ifndef(OTP_BELOW_26). + +lists_zip3_pad(As, Bs, Cs) -> + lists:zip3(As, Bs, Cs, {pad, {error_missing_args_def, "", ""}}). + +-endif. + +make_desc(ADesc, T) when is_tuple(T) -> + T3 = string:join(tuple_to_list(T), " : "), + iolist_to_binary([ADesc, " {", T3, "}"]); +make_desc(ADesc, M) when is_map(M) -> + M2 = [binary_to_list(V) || V <- maps:keys(M)], + M3 = string:join(M2, " : "), + iolist_to_binary([ADesc, " {", M3, "}"]); +make_desc(ADesc, _M) -> + iolist_to_binary(ADesc). + +%%%================================== +%%%% - encode + +encode({[T | _] = List}, Type) when is_tuple(T) -> + encode(List, Type); +encode([T | _] = List, Type) when is_tuple(T) -> + [encode(Element, Type) || Element <- List]; +encode(T, _Type) when is_tuple(T) -> + T2 = [x_to_binary(E) || E <- tuple_to_list(T)], + T3 = str:join(T2, <<":">>), + [T3]; +encode(M, {tuple, Types}) when is_map(M) -> + M2 = [x_to_list(maps:get(atom_to_binary(Key, latin1), M)) + || {Key, _ElementType} <- Types], + M3 = string:join(M2, " : "), + [iolist_to_binary(M3)]; +encode([S | _] = SList, _Type) when is_list(S) -> + [iolist_to_binary(A) || A <- SList]; +encode([B | _] = BList, _Type) when is_binary(B) -> + BList; +encode(I, _Type) when is_integer(I) -> + [integer_to_binary(I)]; +encode([M | _] = List, {list, {_Name, TupleType}}) when is_map(M) -> + [encode(M1, TupleType) || M1 <- List]; +encode(S, _Type) when is_list(S) -> + [iolist_to_binary(S)]; +encode(B, _Type) when is_binary(B) -> + str:tokens(B, <<"\n">>). + +x_to_list(B) when is_binary(B) -> + binary_to_list(B); +x_to_list(I) when is_integer(I) -> + integer_to_list(I); +x_to_list(L) when is_list(L) -> + L. + +x_to_binary(B) when is_binary(B) -> + B; +x_to_binary(I) when is_integer(I) -> + integer_to_binary(I); +x_to_binary(L) when is_list(L) -> + iolist_to_binary(L). + +%%%================================== + +%%% vim: set foldmethod=marker foldmarker=%%%%,%%%=: diff --git a/src/mod_adhoc_api_opt.erl b/src/mod_adhoc_api_opt.erl new file mode 100644 index 000000000..bd7cdce42 --- /dev/null +++ b/src/mod_adhoc_api_opt.erl @@ -0,0 +1,13 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_adhoc_api_opt). + +-export([default_version/1]). + +-spec default_version(gen_mod:opts() | global | binary()) -> integer(). +default_version(Opts) when is_map(Opts) -> + gen_mod:get_opt(default_version, Opts); +default_version(Host) -> + gen_mod:get_module_opt(Host, mod_adhoc_api, default_version). + diff --git a/src/mod_adhoc_opt.erl b/src/mod_adhoc_opt.erl new file mode 100644 index 000000000..bb805a447 --- /dev/null +++ b/src/mod_adhoc_opt.erl @@ -0,0 +1,13 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_adhoc_opt). + +-export([report_commands_node/1]). + +-spec report_commands_node(gen_mod:opts() | global | binary()) -> boolean(). +report_commands_node(Opts) when is_map(Opts) -> + gen_mod:get_opt(report_commands_node, Opts); +report_commands_node(Host) -> + gen_mod:get_module_opt(Host, mod_adhoc, report_commands_node). + diff --git a/src/mod_admin_extra.erl b/src/mod_admin_extra.erl new file mode 100644 index 000000000..4a7877d19 --- /dev/null +++ b/src/mod_admin_extra.erl @@ -0,0 +1,2377 @@ +%%%------------------------------------------------------------------- +%%% File : mod_admin_extra.erl +%%% Author : Badlop +%%% Purpose : Contributed administrative functions and commands +%%% Created : 10 Aug 2008 by Badlop +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_admin_extra). +-author('badlop@process-one.net'). + +-behaviour(gen_mod). + +-include("logger.hrl"). +-include("translate.hrl"). + +-export([start/2, stop/1, reload/3, mod_options/1, + get_commands_spec/0, depends/2, mod_doc/0]). + +% Commands API +-export([ + % Adminsys + compile/1, get_cookie/0, + restart_module/2, + + % Sessions + num_resources/2, resource_num/3, + kick_session/4, status_num/2, status_num/1, + status_list/2, status_list_v3/2, + status_list/1, status_list_v3/1, connected_users_info/0, + connected_users_vhost/1, set_presence/7, + get_presence/2, user_sessions_info/2, get_last/2, set_last/4, + + % Accounts + set_password/3, check_password_hash/4, delete_old_users/1, + delete_old_users_vhost/2, check_password/3, + ban_account/3, ban_account_v2/3, get_ban_details/2, unban_account/2, + + % vCard + set_nickname/3, get_vcard/3, + get_vcard/4, get_vcard_multi/4, set_vcard/4, + set_vcard/5, + + % Roster + add_rosteritem/7, delete_rosteritem/4, + get_roster/2, get_roster_count/2, push_roster/3, + push_roster_all/1, push_alltoall/2, + push_roster_item/5, build_roster_item/3, + + % Private storage + private_get/4, private_set/3, + + % Shared roster + srg_create/5, srg_add/2, + srg_delete/2, srg_list/1, srg_get_info/2, + srg_set_info/4, + srg_get_displayed/2, srg_add_displayed/3, srg_del_displayed/3, + srg_get_members/2, srg_user_add/4, srg_user_del/4, + + % Send message + send_message/5, send_stanza/3, send_stanza_c2s/4, + + % Privacy list + privacy_set/3, + + % Stats + stats/1, stats/2 + ]). +-export([web_menu_main/2, web_page_main/2, + web_menu_host/3, web_page_host/3, + web_menu_hostuser/4, web_page_hostuser/4, + web_menu_hostnode/4, web_page_hostnode/4, + web_menu_node/3, web_page_node/3]). + +-import(ejabberd_web_admin, [make_command/4, make_table/2]). + +-include("ejabberd_commands.hrl"). +-include("ejabberd_http.hrl"). +-include("ejabberd_web_admin.hrl"). +-include("mod_roster.hrl"). +-include("mod_privacy.hrl"). +-include("ejabberd_sm.hrl"). +-include_lib("xmpp/include/scram.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). + +%%% +%%% gen_mod +%%% + +start(_Host, _Opts) -> + {ok, [{commands, get_commands_spec()}, + {hook, webadmin_menu_main, web_menu_main, 50, global}, + {hook, webadmin_page_main, web_page_main, 50, global}, + {hook, webadmin_menu_host, web_menu_host, 50}, + {hook, webadmin_page_host, web_page_host, 50}, + {hook, webadmin_menu_hostuser, web_menu_hostuser, 50}, + {hook, webadmin_page_hostuser, web_page_hostuser, 50}, + {hook, webadmin_menu_hostnode, web_menu_hostnode, 50}, + {hook, webadmin_page_hostnode, web_page_hostnode, 50}, + {hook, webadmin_menu_node, web_menu_node, 50, global}, + {hook, webadmin_page_node, web_page_node, 50, global}]}. + +stop(_Host) -> + ok. + +reload(_Host, _NewOpts, _OldOpts) -> + ok. + +depends(_Host, _Opts) -> + []. + +%%% +%%% Register commands +%%% + +get_commands_spec() -> + Vcard1FieldsString = "Some vcard field names in `get`/`set_vcard` are:\n\n" + "* FN - Full Name\n" + "* NICKNAME - Nickname\n" + "* BDAY - Birthday\n" + "* TITLE - Work: Position\n" + "* ROLE - Work: Role\n", + + Vcard2FieldsString = "Some vcard field names and subnames in `get`/`set_vcard2` are:\n\n" + "* N FAMILY - Family name\n" + "* N GIVEN - Given name\n" + "* N MIDDLE - Middle name\n" + "* ADR CTRY - Address: Country\n" + "* ADR LOCALITY - Address: City\n" + "* TEL HOME - Telephone: Home\n" + "* TEL CELL - Telephone: Cellphone\n" + "* TEL WORK - Telephone: Work\n" + "* TEL VOICE - Telephone: Voice\n" + "* EMAIL USERID - E-Mail Address\n" + "* ORG ORGNAME - Work: Company\n" + "* ORG ORGUNIT - Work: Department\n", + + VcardXEP = "For a full list of vCard fields check [XEP-0054: vcard-temp]" + "(https://xmpp.org/extensions/xep-0054.html)", + + [ + #ejabberd_commands{name = compile, tags = [erlang], + desc = "Recompile and reload Erlang source code file", + module = ?MODULE, function = compile, + args = [{file, string}], + args_example = ["/home/me/srcs/ejabberd/mod_example.erl"], + args_desc = ["Filename of erlang source file to compile"], + result = {res, rescode}, + result_example = ok}, + #ejabberd_commands{name = get_cookie, tags = [erlang], + desc = "Get the Erlang cookie of this node", + module = ?MODULE, function = get_cookie, + args = [], + result = {cookie, string}, + result_example = "MWTAVMODFELNLSMYXPPD", + result_desc = "Erlang cookie used for authentication by ejabberd"}, + #ejabberd_commands{name = restart_module, tags = [erlang], + desc = "Stop an ejabberd module, reload code and start", + module = ?MODULE, function = restart_module, + args = [{host, binary}, {module, binary}], + args_example = ["myserver.com","mod_admin_extra"], + args_desc = ["Server name", "Module to restart"], + result = {res, integer}, + result_example = 0, + result_desc = "Returns integer code:\n" + " - `0`: code reloaded, module restarted\n" + " - `1`: error: module not loaded\n" + " - `2`: code not reloaded, but module restarted"}, + #ejabberd_commands{name = delete_old_users, tags = [accounts, purge], + desc = "Delete users that didn't log in last days, or that never logged", + longdesc = "To protect admin accounts, configure this for example:\n" + "``` yaml\n" + "access_rules:\n" + " protect_old_users:\n" + " - allow: admin\n" + " - deny: all\n" + "```\n", + module = ?MODULE, function = delete_old_users, + args = [{days, integer}], + args_example = [30], + args_desc = ["Last login age in days of accounts that should be removed"], + result = {res, restuple}, + result_example = {ok, <<"Deleted 2 users: [\"oldman@myserver.com\", \"test@myserver.com\"]">>}, + result_desc = "Result tuple"}, + #ejabberd_commands{name = delete_old_users_vhost, tags = [accounts, purge], + desc = "Delete users that didn't log in last days in vhost, or that never logged", + longdesc = "To protect admin accounts, configure this for example:\n" + "``` yaml\n" + "access_rules:\n" + " delete_old_users:\n" + " - deny: admin\n" + " - allow: all\n" + "```\n", + module = ?MODULE, function = delete_old_users_vhost, + args = [{host, binary}, {days, integer}], + args_example = [<<"myserver.com">>, 30], + args_desc = ["Server name", + "Last login age in days of accounts that should be removed"], + result = {res, restuple}, + result_example = {ok, <<"Deleted 2 users: [\"oldman@myserver.com\", \"test@myserver.com\"]">>}, + result_desc = "Result tuple"}, + #ejabberd_commands{name = check_account, tags = [accounts], + desc = "Check if an account exists or not", + module = ejabberd_auth, function = user_exists, + args = [{user, binary}, {host, binary}], + args_example = [<<"peter">>, <<"myserver.com">>], + args_desc = ["User name to check", "Server to check"], + result = {res, rescode}, + result_example = ok}, + #ejabberd_commands{name = check_password, tags = [accounts], + desc = "Check if a password is correct", + module = ?MODULE, function = check_password, + args = [{user, binary}, {host, binary}, {password, binary}], + args_example = [<<"peter">>, <<"myserver.com">>, <<"secret">>], + args_desc = ["User name to check", "Server to check", "Password to check"], + result = {res, rescode}, + result_example = ok}, + #ejabberd_commands{name = check_password_hash, tags = [accounts], + desc = "Check if the password hash is correct", + longdesc = "Allows hash methods from the Erlang/OTP " + "[crypto](https://www.erlang.org/doc/apps/crypto/crypto.html) application.", + module = ?MODULE, function = check_password_hash, + args = [{user, binary}, {host, binary}, {passwordhash, binary}, + {hashmethod, binary}], + args_example = [<<"peter">>, <<"myserver.com">>, + <<"5ebe2294ecd0e0f08eab7690d2a6ee69">>, <<"md5">>], + args_desc = ["User name to check", "Server to check", + "Password's hash value", "Name of hash method"], + result = {res, rescode}, + result_example = ok}, + #ejabberd_commands{name = change_password, tags = [accounts], + desc = "Change the password of an account", + module = ?MODULE, function = set_password, + args = [{user, binary}, {host, binary}, {newpass, binary}], + args_example = [<<"peter">>, <<"myserver.com">>, <<"blank">>], + args_desc = ["User name", "Server name", + "New password for user"], + result = {res, rescode}, + result_example = ok}, + + #ejabberd_commands{name = ban_account, tags = [accounts], + desc = "Ban an account: kick sessions and set random password", + longdesc = "This simply sets a random password.", + module = ?MODULE, function = ban_account, + args = [{user, binary}, {host, binary}, {reason, binary}], + args_example = [<<"attacker">>, <<"myserver.com">>, <<"Spaming other users">>], + args_desc = ["User name to ban", "Server name", + "Reason for banning user"], + result = {res, rescode}, + result_example = ok}, + #ejabberd_commands{name = ban_account, tags = [accounts], + desc = "Ban an account", + longdesc = "This command kicks the account sessions, " + "stores ban details in the account private storage, " + "which blocks login to the account. " + "This command requires _`mod_private`_ to be enabled. " + "Check also _`get_ban_details`_ API " + "and _`unban_account`_ API.", + module = ?MODULE, function = ban_account_v2, + version = 2, + note = "improved in 25.08", + args = [{user, binary}, {host, binary}, {reason, binary}], + args_example = [<<"attacker">>, <<"myserver.com">>, <<"Spaming other users">>], + args_desc = ["User name to ban", "Server name", + "Reason for banning user"], + result = {res, rescode}, + result_example = ok}, + #ejabberd_commands{name = get_ban_details, tags = [accounts], + desc = "Get ban details about an account", + longdesc = "Check _`ban_account`_ API.", + module = ?MODULE, function = get_ban_details, + version = 2, + note = "added in 24.06", + args = [{user, binary}, {host, binary}], + args_example = [<<"attacker">>, <<"myserver.com">>], + args_desc = ["User name to unban", "Server name"], + result = {ban_details, {list, + {detail, {tuple, [{name, string}, + {value, string} + ]}} + }}, + result_example = [{"reason", "Spamming other users"}, + {"bandate", "2024-04-22T09:16:47.975312Z"}, + {"lastdate", "2024-04-22T08:39:12Z"}, + {"lastreason", "Connection reset by peer"}]}, + #ejabberd_commands{name = unban_account, tags = [accounts], + desc = "Remove the ban from an account", + longdesc = "Check _`ban_account`_ API.", + module = ?MODULE, function = unban_account, + version = 2, + note = "added in 24.06", + args = [{user, binary}, {host, binary}], + args_example = [<<"gooduser">>, <<"myserver.com">>], + args_desc = ["User name to unban", "Server name"], + result = {res, rescode}, + result_example = ok}, + + #ejabberd_commands{name = num_resources, tags = [session], + desc = "Get the number of resources of a user", + module = ?MODULE, function = num_resources, + args = [{user, binary}, {host, binary}], + args_example = [<<"peter">>, <<"myserver.com">>], + args_desc = ["User name", "Server name"], + result = {resources, integer}, + result_example = 5, + result_desc = "Number of active resources for a user"}, + #ejabberd_commands{name = resource_num, tags = [session], + desc = "Resource string of a session number", + module = ?MODULE, function = resource_num, + args = [{user, binary}, {host, binary}, {num, integer}], + args_example = [<<"peter">>, <<"myserver.com">>, 2], + args_desc = ["User name", "Server name", "ID of resource to return"], + result = {resource, string}, + result_example = <<"Psi">>, + result_desc = "Name of user resource"}, + #ejabberd_commands{name = kick_session, tags = [session], + desc = "Kick a user session", + module = ?MODULE, function = kick_session, + args = [{user, binary}, {host, binary}, {resource, binary}, {reason, binary}], + args_example = [<<"peter">>, <<"myserver.com">>, <<"Psi">>, + <<"Stuck connection">>], + args_desc = ["User name", "Server name", "User's resource", + "Reason for closing session"], + result = {res, rescode}, + result_example = ok}, + #ejabberd_commands{name = status_num_host, tags = [session, statistics], + desc = "Number of logged users with this status in host", + policy = admin, + module = ?MODULE, function = status_num, + args = [{host, binary}, {status, binary}], + args_example = [<<"myserver.com">>, <<"dnd">>], + args_desc = ["Server name", "Status type to check"], + result = {users, integer}, + result_example = 23, + result_desc = "Number of connected sessions with given status type"}, + #ejabberd_commands{name = status_num, tags = [session, statistics], + desc = "Number of logged users with this status", + policy = admin, + module = ?MODULE, function = status_num, + args = [{status, binary}], + args_example = [<<"dnd">>], + args_desc = ["Status type to check"], + result = {users, integer}, + result_example = 23, + result_desc = "Number of connected sessions with given status type"}, + #ejabberd_commands{name = status_list_host, tags = [session], + desc = "List of users logged in host with their statuses", + module = ?MODULE, function = status_list, + args = [{host, binary}, {status, binary}], + args_example = [<<"myserver.com">>, <<"dnd">>], + args_desc = ["Server name", "Status type to check"], + result_example = [{<<"peter">>,<<"myserver.com">>,<<"tka">>,6,<<"Busy">>}], + result = {users, {list, + {userstatus, {tuple, [ + {user, string}, + {host, string}, + {resource, string}, + {priority, integer}, + {status, string} + ]}} + }}}, + #ejabberd_commands{name = status_list_host, tags = [session], + desc = "List of users logged in host with their statuses", + module = ?MODULE, function = status_list_v3, + version = 3, + note = "updated in 24.12", + args = [{host, binary}, {status, binary}], + args_example = [<<"myserver.com">>, <<"dnd">>], + args_desc = ["Server name", "Status type to check"], + result_example = [{<<"peter@myserver.com/tka">>,6,<<"Busy">>}], + result = {users, {list, + {userstatus, {tuple, [{jid, string}, + {priority, integer}, + {status, string} + ]}} + }}}, + #ejabberd_commands{name = status_list, tags = [session], + desc = "List of logged users with this status", + module = ?MODULE, function = status_list, + args = [{status, binary}], + args_example = [<<"dnd">>], + args_desc = ["Status type to check"], + result_example = [{<<"peter">>,<<"myserver.com">>,<<"tka">>,6,<<"Busy">>}], + result = {users, {list, + {userstatus, {tuple, [ + {user, string}, + {host, string}, + {resource, string}, + {priority, integer}, + {status, string} + ]}} + }}}, + #ejabberd_commands{name = status_list, tags = [session], + desc = "List of logged users with this status", + module = ?MODULE, function = status_list_v3, + version = 3, + note = "updated in 24.12", + args = [{status, binary}], + args_example = [<<"dnd">>], + args_desc = ["Status type to check"], + result_example = [{<<"peter@myserver.com/tka">>,6,<<"Busy">>}], + result = {users, {list, + {userstatus, {tuple, [{jid, string}, + {priority, integer}, + {status, string} + ]}} + }}}, + #ejabberd_commands{name = connected_users_info, + tags = [session], + desc = "List all established sessions and their information", + module = ?MODULE, function = connected_users_info, + args = [], + result_example = [{"user1@myserver.com/tka", + "c2s", "127.0.0.1", 42656,8, "ejabberd@localhost", + 231, <<"dnd">>, <<"tka">>, <<>>}], + result = {connected_users_info, + {list, + {session, {tuple, + [{jid, string}, + {connection, string}, + {ip, string}, + {port, integer}, + {priority, integer}, + {node, string}, + {uptime, integer}, + {status, string}, + {resource, string}, + {statustext, string} + ]}} + }}}, + + #ejabberd_commands{name = connected_users_vhost, + tags = [session], + desc = "Get the list of established sessions in a vhost", + module = ?MODULE, function = connected_users_vhost, + args_example = [<<"myexample.com">>], + args_desc = ["Server name"], + args = [{host, binary}], + result_example = [<<"user1@myserver.com/tka">>, <<"user2@localhost/tka">>], + result_desc = "List of sessions full JIDs", + result = {connected_users_vhost, {list, {sessions, string}}}}, + #ejabberd_commands{name = user_sessions_info, + tags = [session], + desc = "Get information about all sessions of a user", + module = ?MODULE, function = user_sessions_info, + args = [{user, binary}, {host, binary}], + args_example = [<<"peter">>, <<"myserver.com">>], + args_desc = ["User name", "Server name"], + result_example = [{"c2s", "127.0.0.1", 42656,8, "ejabberd@localhost", + 231, <<"dnd">>, <<"tka">>, <<>>}], + result = {sessions_info, + {list, + {session, {tuple, + [{connection, string}, + {ip, string}, + {port, integer}, + {priority, integer}, + {node, string}, + {uptime, integer}, + {status, string}, + {resource, string}, + {statustext, string} + ]}} + }}}, + + #ejabberd_commands{name = get_presence, tags = [session], + desc = + "Retrieve the resource with highest priority, " + "and its presence (show and status message) " + "for a given user.", + longdesc = + "The `jid` value contains the user JID " + "with resource.\n\nThe `show` value contains " + "the user presence flag. It can take " + "limited values:\n\n - `available`\n - `chat` " + "(Free for chat)\n - `away`\n - `dnd` (Do " + "not disturb)\n - `xa` (Not available, " + "extended away)\n - `unavailable` (Not " + "connected)\n\n`status` is a free text " + "defined by the user client.", + module = ?MODULE, function = get_presence, + args = [{user, binary}, {host, binary}], + args_rename = [{server, host}], + args_example = [<<"peter">>, <<"myexample.com">>], + args_desc = ["User name", "Server name"], + result_example = {<<"user1@myserver.com/tka">>, <<"dnd">>, <<"Busy">>}, + result = + {presence, + {tuple, + [{jid, string}, {show, string}, + {status, string}]}}}, + #ejabberd_commands{name = set_presence, + tags = [session], + desc = "Set presence of a session", + module = ?MODULE, function = set_presence, + args = [{user, binary}, {host, binary}, + {resource, binary}, {type, binary}, + {show, binary}, {status, binary}, + {priority, binary}], + args_example = [<<"user1">>,<<"myserver.com">>,<<"tka1">>, + <<"available">>,<<"away">>,<<"BB">>, <<"7">>], + args_desc = ["User name", "Server name", "Resource", + "Type: `available`, `error`, `probe`...", + "Show: `away`, `chat`, `dnd`, `xa`.", "Status text", + "Priority, provide this value as an integer"], + result = {res, rescode}}, + #ejabberd_commands{name = set_presence, + tags = [session], + desc = "Set presence of a session", + module = ?MODULE, function = set_presence, + version = 1, + note = "updated in 24.02", + args = [{user, binary}, {host, binary}, + {resource, binary}, {type, binary}, + {show, binary}, {status, binary}, + {priority, integer}], + args_example = [<<"user1">>,<<"myserver.com">>,<<"tka1">>, + <<"available">>,<<"away">>,<<"BB">>, 7], + args_desc = ["User name", "Server name", "Resource", + "Type: `available`, `error`, `probe`...", + "Show: `away`, `chat`, `dnd`, `xa`.", "Status text", + "Priority, provide this value as an integer"], + result = {res, rescode}}, + + #ejabberd_commands{name = set_nickname, tags = [vcard], + desc = "Set nickname in a user's vCard", + module = ?MODULE, function = set_nickname, + args = [{user, binary}, {host, binary}, {nickname, binary}], + args_example = [<<"user1">>,<<"myserver.com">>,<<"User 1">>], + args_desc = ["User name", "Server name", "Nickname"], + result = {res, rescode}}, + #ejabberd_commands{name = get_vcard, tags = [vcard], + desc = "Get content from a vCard field", + longdesc = Vcard1FieldsString ++ "\n" ++ VcardXEP, + module = ?MODULE, function = get_vcard, + args = [{user, binary}, {host, binary}, {name, binary}], + args_example = [<<"user1">>,<<"myserver.com">>,<<"NICKNAME">>], + args_desc = ["User name", "Server name", "Field name"], + result_example = "User 1", + result_desc = "Field content", + result = {content, string}}, + #ejabberd_commands{name = get_vcard2, tags = [vcard], + desc = "Get content from a vCard subfield", + longdesc = Vcard2FieldsString ++ "\n" ++ VcardXEP, + module = ?MODULE, function = get_vcard, + args = [{user, binary}, {host, binary}, {name, binary}, {subname, binary}], + args_example = [<<"user1">>,<<"myserver.com">>,<<"N">>, <<"FAMILY">>], + args_desc = ["User name", "Server name", "Field name", "Subfield name"], + result_example = "Schubert", + result_desc = "Field content", + result = {content, string}}, + #ejabberd_commands{name = get_vcard2_multi, tags = [vcard], + desc = "Get multiple contents from a vCard field", + longdesc = Vcard2FieldsString ++ "\n" ++ VcardXEP, + module = ?MODULE, function = get_vcard_multi, + args = [{user, binary}, {host, binary}, {name, binary}, {subname, binary}], + result = {contents, {list, {value, string}}}}, + + #ejabberd_commands{name = set_vcard, tags = [vcard], + desc = "Set content in a vCard field", + longdesc = Vcard1FieldsString ++ "\n" ++ VcardXEP, + module = ?MODULE, function = set_vcard, + args = [{user, binary}, {host, binary}, {name, binary}, {content, binary}], + args_example = [<<"user1">>,<<"myserver.com">>, <<"URL">>, <<"www.example.com">>], + args_desc = ["User name", "Server name", "Field name", "Value"], + result = {res, rescode}}, + #ejabberd_commands{name = set_vcard2, tags = [vcard], + desc = "Set content in a vCard subfield", + longdesc = Vcard2FieldsString ++ "\n" ++ VcardXEP, + module = ?MODULE, function = set_vcard, + args = [{user, binary}, {host, binary}, {name, binary}, {subname, binary}, {content, binary}], + args_example = [<<"user1">>,<<"myserver.com">>,<<"TEL">>, <<"NUMBER">>, <<"123456">>], + args_desc = ["User name", "Server name", "Field name", "Subfield name", "Value"], + result = {res, rescode}}, + #ejabberd_commands{name = set_vcard2_multi, tags = [vcard], + desc = "Set multiple contents in a vCard subfield", + longdesc = Vcard2FieldsString ++ "\n" ++ VcardXEP, + module = ?MODULE, function = set_vcard, + args = [{user, binary}, {host, binary}, {name, binary}, {subname, binary}, {contents, {list, {value, binary}}}], + result = {res, rescode}}, + + #ejabberd_commands{name = add_rosteritem, tags = [roster], + desc = "Add an item to a user's roster (supports ODBC)", + longdesc = "Group can be several groups separated by `;` for example: `g1;g2;g3`", + module = ?MODULE, function = add_rosteritem, + args = [{localuser, binary}, {localhost, binary}, + {user, binary}, {host, binary}, + {nick, binary}, {group, binary}, + {subs, binary}], + args_rename = [{localserver, localhost}, {server, host}], + args_example = [<<"user1">>,<<"myserver.com">>,<<"user2">>, <<"myserver.com">>, + <<"User 2">>, <<"Friends">>, <<"both">>], + args_desc = ["User name", "Server name", "Contact user name", "Contact server name", + "Nickname", "Group", "Subscription"], + result = {res, rescode}}, + #ejabberd_commands{name = add_rosteritem, tags = [roster], + desc = "Add an item to a user's roster (supports ODBC)", + longdesc = "The client will receive a `jabber:iq:roster` IQ notifying them of the added entry.", + module = ?MODULE, function = add_rosteritem, + version = 1, + note = "updated in 24.02", + args = [{localuser, binary}, {localhost, binary}, + {user, binary}, {host, binary}, + {nick, binary}, {groups, {list, {group, binary}}}, + {subs, binary}], + args_rename = [{localserver, localhost}, {server, host}], + args_example = [<<"user1">>,<<"myserver.com">>,<<"user2">>, <<"myserver.com">>, + <<"User 2">>, [<<"Friends">>, <<"Team 1">>], <<"both">>], + args_desc = ["User name", "Server name", "Contact user name", "Contact server name", + "Nickname", "Groups", "Subscription"], + result = {res, rescode}}, + %%{"", "subs= none, from, to or both"}, + %%{"", "example: add-roster peter localhost mike server.com MiKe Employees both"}, + %%{"", "will add mike@server.com to peter@localhost roster"}, + #ejabberd_commands{name = delete_rosteritem, tags = [roster], + desc = "Delete an item from a user's roster (supports ODBC)", + longdesc = "The client will receive a `jabber:iq:roster` IQ notifying them of the removed entry.", + module = ?MODULE, function = delete_rosteritem, + args = [{localuser, binary}, {localhost, binary}, + {user, binary}, {host, binary}], + args_rename = [{localserver, localhost}, {server, host}], + args_example = [<<"user1">>,<<"myserver.com">>,<<"user2">>, <<"myserver.com">>], + args_desc = ["User name", "Server name", "Contact user name", "Contact server name"], + result = {res, rescode}}, + #ejabberd_commands{name = process_rosteritems, tags = [roster], + desc = "List/delete rosteritems that match filter", + longdesc = "Explanation of each argument:\n\n" + "* `action`: what to do with each rosteritem that " + "matches all the filtering options\n" + "* `subs`: subscription type\n" + "* `asks`: pending subscription\n" + "* `users`: the JIDs of the local user\n" + "* `contacts`: the JIDs of the contact in the roster\n" + "\n" + "**Mnesia backend:**\n" + "\n" + "Allowed values in the arguments:\n\n" + "* `action` = `list` | `delete`\n" + "* `subs` = `any` | SUB[:SUB]*\n" + "* `asks` = `any` | ASK[:ASK]*\n" + "* `users` = `any` | JID[:JID]*\n" + "* `contacts` = `any` | JID[:JID]*\n" + "\nwhere\n\n" + "* SUB = `none` | `from `| `to` | `both`\n" + "* ASK = `none` | `out` | `in`\n" + "* JID = characters valid in a JID, and can use the " + "globs: `*`, `?`, `!` and `[...]`\n" + "\n" + "This example will list roster items with subscription " + "`none`, `from` or `to` that have any ask property, of " + "local users which JID is in the virtual host " + "`example.org` and that the contact JID is either a " + "bare server name (without user part) or that has a " + "user part and the server part contains the word `icq`" + ":\n `list none:from:to any *@example.org *:*@*icq*`" + "\n\n" + "**SQL backend:**\n" + "\n" + "Allowed values in the arguments:\n\n" + "* `action` = `list` | `delete`\n" + "* `subs` = `any` | SUB\n" + "* `asks` = `any` | ASK\n" + "* `users` = JID\n" + "* `contacts` = JID\n" + "\nwhere\n\n" + "* SUB = `none` | `from` | `to` | `both`\n" + "* ASK = `none` | `out` | `in`\n" + "* JID = characters valid in a JID, and can use the " + "globs: `_` and `%`\n" + "\n" + "This example will list roster items with subscription " + "`to` that have any ask property, of " + "local users which JID is in the virtual host " + "`example.org` and that the contact JID's " + "server part contains the word `icq`" + ":\n `list to any %@example.org %@%icq%`", + module = mod_roster, function = process_rosteritems, + args = [{action, string}, {subs, string}, + {asks, string}, {users, string}, + {contacts, string}], + result = {response, + {list, + {pairs, {tuple, + [{user, string}, + {contact, string} + ]}} + }}}, + #ejabberd_commands{name = get_roster, tags = [roster], + desc = "Get list of contacts in a local user roster", + longdesc = + "`subscription` can be: `none`, `from`, `to`, `both`.\n\n" + "`pending` can be: `in`, `out`, `none`.", + note = "improved in 23.10", + policy = user, + module = ?MODULE, function = get_roster, + args = [], + args_rename = [{server, host}], + result_example = [{<<"user2@localhost">>, <<"User 2">>, <<"none">>, <<"subscribe">>, [<<"Group1">>]}], + result = {contacts, {list, {contact, {tuple, [ + {jid, string}, + {nick, string}, + {subscription, string}, + {pending, string}, + {groups, {list, {group, string}}} + ]}}}}}, + #ejabberd_commands{name = get_roster_count, tags = [roster], + desc = "Get number of contacts in a local user roster", + note = "added in 24.06", + policy = user, + module = ?MODULE, function = get_roster_count, + args = [], + args_rename = [{server, host}], + result_example = 5, + result_desc = "Number", + result = {value, integer}}, + #ejabberd_commands{name = push_roster, tags = [roster], + desc = "Push template roster from file to a user", + longdesc = "The text file must contain an erlang term: a list " + "of tuples with username, servername, group and nick. For example:\n" + "`[{\"user1\", \"localhost\", \"Workers\", \"User 1\"},\n" + " {\"user2\", \"localhost\", \"Workers\", \"User 2\"}].`\n\n" + "If there are problems parsing UTF8 character encoding, " + "provide the corresponding string with the `<<\"STRING\"/utf8>>` syntax, for example:\n" + "`[{\"user2\", \"localhost\", \"Workers\", <<\"User 2\"/utf8>>}]`.", + module = ?MODULE, function = push_roster, + args = [{file, binary}, {user, binary}, {host, binary}], + args_example = [<<"/home/ejabberd/roster.txt">>, <<"user1">>, <<"localhost">>], + args_desc = ["File path", "User name", "Server name"], + result = {res, rescode}}, + #ejabberd_commands{name = push_roster_all, tags = [roster], + desc = "Push template roster from file to all those users", + longdesc = "The text file must contain an erlang term: a list " + "of tuples with username, servername, group and nick. Example:\n" + "`[{\"user1\", \"localhost\", \"Workers\", \"User 1\"},\n" + " {\"user2\", \"localhost\", \"Workers\", \"User 2\"}].`", + module = ?MODULE, function = push_roster_all, + args = [{file, binary}], + args_example = [<<"/home/ejabberd/roster.txt">>], + args_desc = ["File path"], + result = {res, rescode}}, + #ejabberd_commands{name = push_alltoall, tags = [roster], + desc = "Add all the users to all the users of Host in Group", + module = ?MODULE, function = push_alltoall, + args = [{host, binary}, {group, binary}], + args_example = [<<"myserver.com">>,<<"Everybody">>], + args_desc = ["Server name", "Group name"], + result = {res, rescode}}, + + #ejabberd_commands{name = get_last, tags = [last], + desc = "Get last activity information", + longdesc = "Timestamp is UTC and " + "[XEP-0082](https://xmpp.org/extensions/xep-0082.html)" + " format, for example: " + "`2017-02-23T22:25:28.063062Z ONLINE`", + module = ?MODULE, function = get_last, + args = [{user, binary}, {host, binary}], + args_example = [<<"user1">>,<<"myserver.com">>], + args_desc = ["User name", "Server name"], + result_example = {<<"2017-06-30T14:32:16.060684Z">>, "ONLINE"}, + result_desc = "Last activity timestamp and status", + result = {last_activity, + {tuple, [{timestamp, string}, + {status, string} + ]}}}, + #ejabberd_commands{name = set_last, tags = [last], + desc = "Set last activity information", + longdesc = "Timestamp is the seconds since " + "`1970-01-01 00:00:00 UTC`. For example value see `date +%s`", + module = ?MODULE, function = set_last, + args = [{user, binary}, {host, binary}, {timestamp, integer}, {status, binary}], + args_example = [<<"user1">>,<<"myserver.com">>, 1500045311, <<"GoSleeping">>], + args_desc = ["User name", "Server name", "Number of seconds since epoch", "Status message"], + result = {res, rescode}}, + + #ejabberd_commands{name = private_get, tags = [private], + desc = "Get some information from a user private storage", + module = ?MODULE, function = private_get, + args = [{user, binary}, {host, binary}, {element, binary}, {ns, binary}], + args_example = [<<"user1">>,<<"myserver.com">>,<<"storage">>, <<"storage:rosternotes">>], + args_desc = ["User name", "Server name", "Element name", "Namespace"], + result = {res, string}}, + #ejabberd_commands{name = private_set, tags = [private], + desc = "Set to the user private storage", + module = ?MODULE, function = private_set, + args = [{user, binary}, {host, binary}, {element, binary}], + args_example = [<<"user1">>,<<"myserver.com">>, + <<"">>], + args_desc = ["User name", "Server name", "XML storage element"], + result = {res, rescode}}, + + #ejabberd_commands{name = srg_create, tags = [shared_roster_group], + desc = "Create a Shared Roster Group", + longdesc = "If you want to specify several group " + "identifiers in the Display argument,\n" + "put `\\ \"` around the argument and\nseparate the " + "identifiers with `\\ \\ n`\n" + "For example:\n" + " `ejabberdctl srg_create group3 myserver.com " + "name desc \\\"group1\\\\ngroup2\\\"`", + note = "changed in 21.07", + module = ?MODULE, function = srg_create, + args = [{group, binary}, {host, binary}, + {label, binary}, {description, binary}, {display, binary}], + args_rename = [{name, label}], + args_example = [<<"group3">>, <<"myserver.com">>, <<"Group3">>, + <<"Third group">>, <<"group1\\\\ngroup2">>], + args_desc = ["Group identifier", "Group server name", "Group name", + "Group description", "Groups to display"], + result = {res, rescode}}, + #ejabberd_commands{name = srg_create, tags = [shared_roster_group], + desc = "Create a Shared Roster Group", + module = ?MODULE, function = srg_create, + version = 1, + note = "updated in 24.02", + args = [{group, binary}, {host, binary}, + {label, binary}, {description, binary}, {display, {list, {group, binary}}}], + args_rename = [{name, label}], + args_example = [<<"group3">>, <<"myserver.com">>, <<"Group3">>, + <<"Third group">>, [<<"group1">>, <<"group2">>]], + args_desc = ["Group identifier", "Group server name", "Group name", + "Group description", "List of groups to display"], + result = {res, rescode}}, + #ejabberd_commands{name = srg_add, tags = [shared_roster_group], + desc = "Add/Create a Shared Roster Group (without details)", + module = ?MODULE, function = srg_add, + note = "added in 24.06", + args = [{group, binary}, {host, binary}], + args_example = [<<"group3">>, <<"myserver.com">>], + args_desc = ["Group identifier", "Group server name"], + result = {res, rescode}}, + #ejabberd_commands{name = srg_delete, tags = [shared_roster_group], + desc = "Delete a Shared Roster Group", + module = ?MODULE, function = srg_delete, + args = [{group, binary}, {host, binary}], + args_example = [<<"group3">>, <<"myserver.com">>], + args_desc = ["Group identifier", "Group server name"], + result = {res, rescode}}, + #ejabberd_commands{name = srg_list, tags = [shared_roster_group], + desc = "List the Shared Roster Groups in Host", + module = ?MODULE, function = srg_list, + args = [{host, binary}], + args_example = [<<"myserver.com">>], + args_desc = ["Server name"], + result_example = [<<"group1">>, <<"group2">>], + result_desc = "List of group identifiers", + result = {groups, {list, {id, string}}}}, + #ejabberd_commands{name = srg_get_info, tags = [shared_roster_group], + desc = "Get info of a Shared Roster Group", + module = ?MODULE, function = srg_get_info, + args = [{group, binary}, {host, binary}], + args_example = [<<"group3">>, <<"myserver.com">>], + args_desc = ["Group identifier", "Group server name"], + result_example = [{<<"name">>, "Group 3"}, {<<"displayed_groups">>, "group1"}], + result_desc = "List of group information, as key and value", + result = {informations, {list, {information, {tuple, [{key, string}, {value, string}]}}}}}, + #ejabberd_commands{name = srg_set_info, tags = [shared_roster_group], + desc = "Set info of a Shared Roster Group", + module = ?MODULE, function = srg_set_info, + note = "added in 24.06", + args = [{group, binary}, {host, binary}, {key, binary}, {value, binary}], + args_example = [<<"group3">>, <<"myserver.com">>, <<"label">>, <<"Family">>], + args_desc = ["Group identifier", "Group server name", + "Information key: label, description", + "Information value"], + result = {res, rescode}}, + + #ejabberd_commands{name = srg_get_displayed, tags = [shared_roster_group], + desc = "Get displayed groups of a Shared Roster Group", + module = ?MODULE, function = srg_get_displayed, + note = "added in 24.06", + args = [{group, binary}, {host, binary}], + args_example = [<<"group3">>, <<"myserver.com">>], + args_desc = ["Group identifier", "Group server name"], + result_example = [<<"group1">>, <<"group2">>], + result_desc = "List of groups to display", + result = {display, {list, {group, binary}}}}, + #ejabberd_commands{name = srg_add_displayed, tags = [shared_roster_group], + desc = "Add a group to displayed_groups of a Shared Roster Group", + module = ?MODULE, function = srg_add_displayed, + note = "added in 24.06", + args = [{group, binary}, {host, binary}, + {add, binary}], + args_example = [<<"group3">>, <<"myserver.com">>, <<"group1">>], + args_desc = ["Group identifier", "Group server name", + "Group to add to displayed_groups"], + result = {res, rescode}}, + #ejabberd_commands{name = srg_del_displayed, tags = [shared_roster_group], + desc = "Delete a group from displayed_groups of a Shared Roster Group", + module = ?MODULE, function = srg_del_displayed, + note = "added in 24.06", + args = [{group, binary}, {host, binary}, + {del, binary}], + args_example = [<<"group3">>, <<"myserver.com">>, <<"group1">>], + args_desc = ["Group identifier", "Group server name", + "Group to delete from displayed_groups"], + result = {res, rescode}}, + + #ejabberd_commands{name = srg_get_members, tags = [shared_roster_group], + desc = "Get members of a Shared Roster Group", + module = ?MODULE, function = srg_get_members, + args = [{group, binary}, {host, binary}], + args_example = [<<"group3">>, <<"myserver.com">>], + args_desc = ["Group identifier", "Group server name"], + result_example = [<<"user1@localhost">>, <<"user2@localhost">>], + result_desc = "List of group identifiers", + result = {members, {list, {member, string}}}}, + #ejabberd_commands{name = srg_user_add, tags = [shared_roster_group], + desc = "Add the JID user@host to the Shared Roster Group", + module = ?MODULE, function = srg_user_add, + args = [{user, binary}, {host, binary}, {group, binary}, {grouphost, binary}], + args_example = [<<"user1">>, <<"myserver.com">>, <<"group3">>, <<"myserver.com">>], + args_desc = ["Username", "User server name", "Group identifier", "Group server name"], + result = {res, rescode}}, + #ejabberd_commands{name = srg_user_del, tags = [shared_roster_group], + desc = "Delete this JID user@host from the Shared Roster Group", + module = ?MODULE, function = srg_user_del, + args = [{user, binary}, {host, binary}, {group, binary}, {grouphost, binary}], + args_example = [<<"user1">>, <<"myserver.com">>, <<"group3">>, <<"myserver.com">>], + args_desc = ["Username", "User server name", "Group identifier", "Group server name"], + result = {res, rescode}}, + + #ejabberd_commands{name = get_offline_count, + tags = [offline], + desc = "Get the number of unread offline messages", + policy = user, + module = mod_offline, function = count_offline_messages, + args = [], + args_rename = [{server, host}], + result_example = 5, + result_desc = "Number", + result = {value, integer}}, + #ejabberd_commands{name = get_offline_messages, + tags = [internal, offline], + desc = "Get the offline messages", + policy = user, + module = mod_offline, function = get_offline_messages, + args = [], + result = {queue, {list, {messages, {tuple, [{time, string}, + {from, string}, + {to, string}, + {packet, string} + ]}}}}}, + + #ejabberd_commands{name = send_message, tags = [stanza], + desc = "Send a message to a local or remote bare of full JID", + longdesc = "When sending a groupchat message to a MUC room, " + "`from` must be the full JID of a room occupant, " + "or the bare JID of a MUC service admin, " + "or the bare JID of a MUC/Sub subscribed user.", + module = ?MODULE, function = send_message, + args = [{type, binary}, {from, binary}, {to, binary}, + {subject, binary}, {body, binary}], + args_example = [<<"headline">>, <<"admin@localhost">>, <<"user1@localhost">>, + <<"Restart">>, <<"In 5 minutes">>], + args_desc = ["Message type: `normal`, `chat`, `headline`, `groupchat`", "Sender JID", + "Receiver JID", "Subject, or empty string", "Body"], + result = {res, rescode}}, + #ejabberd_commands{name = send_stanza_c2s, tags = [stanza], + desc = "Send a stanza from an existing C2S session", + longdesc = "`user`@`host`/`resource` must be an existing C2S session." + " As an alternative, use _`send_stanza`_ API instead.", + module = ?MODULE, function = send_stanza_c2s, + args = [{user, binary}, {host, binary}, {resource, binary}, {stanza, binary}], + args_example = [<<"admin">>, <<"myserver.com">>, <<"bot">>, + <<"">>], + args_desc = ["Username", "Server name", "Resource", "Stanza"], + result = {res, rescode}}, + #ejabberd_commands{name = send_stanza, tags = [stanza], + desc = "Send a stanza; provide From JID and valid To JID", + module = ?MODULE, function = send_stanza, + args = [{from, binary}, {to, binary}, {stanza, binary}], + args_example = [<<"admin@localhost">>, <<"user1@localhost">>, + <<"">>], + args_desc = ["Sender JID", "Destination JID", "Stanza"], + result = {res, rescode}}, + #ejabberd_commands{name = privacy_set, tags = [stanza], + desc = "Send a IQ set privacy stanza for a local account", + module = ?MODULE, function = privacy_set, + args = [{user, binary}, {host, binary}, {xmlquery, binary}], + args_example = [<<"user1">>, <<"myserver.com">>, + <<"...">>], + args_desc = ["Username", "Server name", "Query XML element"], + result = {res, rescode}}, + + #ejabberd_commands{name = stats, tags = [statistics], + desc = "Get some statistical value for the whole ejabberd server", + longdesc = "Allowed statistics `name` are: `registeredusers`, " + "`onlineusers`, `onlineusersnode`, `uptimeseconds`, `processes`.", + policy = admin, + module = ?MODULE, function = stats, + args = [{name, binary}], + args_example = [<<"registeredusers">>], + args_desc = ["Statistic name"], + result_example = 6, + result_desc = "Integer statistic value", + result = {stat, integer}}, + #ejabberd_commands{name = stats_host, tags = [statistics], + desc = "Get some statistical value for this host", + longdesc = "Allowed statistics `name` are: `registeredusers`, `onlineusers`.", + policy = admin, + module = ?MODULE, function = stats, + args = [{name, binary}, {host, binary}], + args_example = [<<"registeredusers">>, <<"example.com">>], + args_desc = ["Statistic name", "Server JID"], + result_example = 6, + result_desc = "Integer statistic value", + result = {stat, integer}} + ]. + + +%%% +%%% Adminsys +%%% + +compile(File) -> + Ebin = filename:join(code:lib_dir(ejabberd), "ebin"), + case ext_mod:compile_erlang_file(Ebin, File) of + {ok, Module} -> + code:purge(Module), + code:load_file(Module), + ok; + _ -> + error + end. + +get_cookie() -> + atom_to_list(erlang:get_cookie()). + +restart_module(Host, Module) when is_binary(Module) -> + restart_module(Host, misc:binary_to_atom(Module)); +restart_module(Host, Module) when is_atom(Module) -> + case gen_mod:is_loaded(Host, Module) of + false -> + % not a running module, force code reload anyway + code:purge(Module), + code:delete(Module), + code:load_file(Module), + 1; + true -> + gen_mod:stop_module(Host, Module), + case code:soft_purge(Module) of + true -> + code:delete(Module), + code:load_file(Module), + gen_mod:start_module(Host, Module), + 0; + false -> + gen_mod:start_module(Host, Module), + 2 + end + end. + +%%% +%%% Accounts +%%% + +set_password(User, Host, Password) -> + Fun = fun () -> ejabberd_auth:set_password(User, Host, Password) end, + user_action(User, Host, Fun, ok). + +check_password(User, Host, Password) -> + ejabberd_auth:check_password(User, <<>>, Host, Password). + +%% Copied some code from ejabberd_commands.erln +check_password_hash(User, Host, PasswordHash, HashMethod) -> + AccountPass = ejabberd_auth:get_password_s(User, Host), + Methods = lists:map(fun(A) -> atom_to_binary(A, latin1) end, + proplists:get_value(hashs, crypto:supports())), + MethodAllowed = lists:member(HashMethod, Methods), + AccountPassHash = case {AccountPass, MethodAllowed} of + {A, _} when is_tuple(A) -> scrammed; + {_, true} -> get_hash(AccountPass, HashMethod); + {_, false} -> + ?ERROR_MSG("Check_password_hash called " + "with hash method: ~p", [HashMethod]), + undefined + end, + case AccountPassHash of + scrammed -> + ?ERROR_MSG("Passwords are scrammed, and check_password_hash cannot work.", []), + throw(passwords_scrammed_command_cannot_work); + undefined -> throw(unkown_hash_method); + PasswordHash -> ok; + _ -> false + end. + +get_hash(AccountPass, Method) -> + iolist_to_binary([io_lib:format("~2.16.0B", [X]) + || X <- binary_to_list( + crypto:hash(binary_to_atom(Method, latin1), AccountPass))]). + +delete_old_users(Days) -> + %% Get the list of registered users + Users = ejabberd_auth:get_users(), + + {removed, N, UR} = delete_old_users(Days, Users), + {ok, io_lib:format("Deleted ~p users: ~p", [N, UR])}. + +delete_old_users_vhost(Host, Days) -> + %% Get the list of registered users + Users = ejabberd_auth:get_users(Host), + + {removed, N, UR} = delete_old_users(Days, Users), + {ok, io_lib:format("Deleted ~p users: ~p", [N, UR])}. + +delete_old_users(Days, Users) -> + SecOlder = Days*24*60*60, + TimeStamp_now = erlang:system_time(second), + TimeStamp_oldest = TimeStamp_now - SecOlder, + F = fun({LUser, LServer}) -> + case catch delete_or_not(LUser, LServer, TimeStamp_oldest) of + true -> + ejabberd_auth:remove_user(LUser, LServer), + true; + _ -> + false + end + end, + Users_removed = lists:filter(F, Users), + {removed, length(Users_removed), Users_removed}. + +delete_or_not(LUser, LServer, TimeStamp_oldest) -> + deny = acl:match_rule(LServer, protect_old_users, jid:make(LUser, LServer)), + [] = ejabberd_sm:get_user_resources(LUser, LServer), + case mod_last:get_last_info(LUser, LServer) of + {ok, TimeStamp, _Status} -> + if TimeStamp_oldest < TimeStamp -> + false; + true -> + true + end; + not_found -> + true + end. + +%% +%% Ban account v0 + +ban_account(User, Host, ReasonText) -> + Reason = prepare_reason(ReasonText), + kick_sessions(User, Host, Reason), + set_random_password(User, Host, Reason), + ok. + +kick_sessions(User, Server, Reason) -> + ejabberd_hooks:run(sm_kick_user, Server, [User, Server]), + lists:map( + fun(Resource) -> + kick_this_session(User, Server, Resource, Reason) + end, + ejabberd_sm:get_user_resources(User, Server)). + +set_random_password(User, Server, Reason) -> + NewPass = build_random_password(Reason), + set_password_auth(User, Server, NewPass). + +build_random_password(Reason) -> + {{Year, Month, Day}, {Hour, Minute, Second}} = calendar:universal_time(), + Date = str:format("~4..0B~2..0B~2..0BT~2..0B:~2..0B:~2..0B", + [Year, Month, Day, Hour, Minute, Second]), + RandomString = p1_rand:get_string(), + <<"BANNED_ACCOUNT--", Date/binary, "--", RandomString/binary, "--", Reason/binary>>. + +set_password_auth(User, Server, Password) -> + ok = ejabberd_auth:set_password(User, Server, Password). + +prepare_reason([]) -> + <<"Kicked by administrator">>; +prepare_reason([Reason]) -> + Reason; +prepare_reason(Reason) when is_binary(Reason) -> + Reason. + +%% +%% Ban account v2 + +ban_account_v2(User, Host, ReasonText) -> + IsPrivateEnabled = gen_mod:is_loaded(Host, mod_private), + Exists = ejabberd_auth:user_exists(User, Host), + IsBanned = is_banned(User, Host), + case {IsPrivateEnabled, Exists, IsBanned} of + {true, true, false} -> + ban_account_v2_b(User, Host, ReasonText); + {false, _, _} -> + mod_private_is_required_but_disabled; + {_, false, _} -> + account_does_not_exist; + {_, _, true} -> + account_was_already_banned; + {_, _, _} -> + other_error + end. + +ban_account_v2_b(User, Host, ReasonText) -> + Reason = prepare_reason(ReasonText), + Last = get_last(User, Host), + BanDate = xmpp_util:encode_timestamp(erlang:timestamp()), + Hash = get_hash_value(User, Host), + BanPrivateXml = build_ban_xmlel(Reason, Last, BanDate, Hash), + ok = private_set2(User, Host, BanPrivateXml), + kick_sessions(User, Host, Reason), + ok. + +get_hash_value(User, Host) -> + Cookie = misc:atom_to_binary(erlang:get_cookie()), + misc:term_to_base64(crypto:hash(sha256, <>)). + +build_ban_xmlel(Reason, {LastDate, LastReason}, BanDate, Hash) -> + #xmlel{name = <<"banned">>, + attrs = [{<<"xmlns">>, <<"jabber:ejabberd:banned">>}], + children = [#xmlel{name = <<"reason">>, attrs = [], children = [{xmlcdata, Reason}]}, + #xmlel{name = <<"lastdate">>, attrs = [], children = [{xmlcdata, LastDate}]}, + #xmlel{name = <<"lastreason">>, attrs = [], children = [{xmlcdata, LastReason}]}, + #xmlel{name = <<"bandate">>, attrs = [], children = [{xmlcdata, BanDate}]}, + #xmlel{name = <<"hash">>, attrs = [], children = [{xmlcdata, Hash}]} + ]}. + +%% +%% Get ban details + +get_ban_details(User, Host) -> + case private_get2(User, Host, <<"banned">>, <<"jabber:ejabberd:banned">>) of + [El] -> + get_ban_details(User, Host, El); + [] -> + [] + end. + +get_ban_details(User, Host, El) -> + Reason = fxml:get_subtag_cdata(El, <<"reason">>), + LastDate = fxml:get_subtag_cdata(El, <<"lastdate">>), + LastReason = fxml:get_subtag_cdata(El, <<"lastreason">>), + BanDate = fxml:get_subtag_cdata(El, <<"bandate">>), + Hash = fxml:get_subtag_cdata(El, <<"hash">>), + case Hash == get_hash_value(User, Host) of + true -> + [{"reason", Reason}, + {"bandate", BanDate}, + {"lastdate", LastDate}, + {"lastreason", LastReason}]; + false -> + [] + end. + +is_banned(User, Host) -> + case lists:keyfind("bandate", 1, get_ban_details(User, Host)) of + {_, BanDate} when BanDate /= <<>> -> + true; + _ -> + false + end. + +%% +%% Unban account + +unban_account(User, Host) -> + IsPrivateEnabled = gen_mod:is_loaded(Host, mod_private), + Exists = ejabberd_auth:user_exists(User, Host), + IsBanned = is_banned(User, Host), + case {IsPrivateEnabled, Exists, IsBanned} of + {true, true, true} -> + unban_account2(User, Host); + {false, _, _} -> + mod_private_is_required_but_disabled; + {_, false, _} -> + account_does_not_exist; + {_, _, false} -> + account_was_not_banned; + {_, _, _} -> + other_error + end. + +unban_account2(User, Host) -> + UnBanPrivateXml = build_unban_xmlel(), + private_set2(User, Host, UnBanPrivateXml). + +build_unban_xmlel() -> + #xmlel{name = <<"banned">>, attrs = [{<<"xmlns">>, <<"jabber:ejabberd:banned">>}]}. + +%%% +%%% Sessions +%%% + +num_resources(User, Host) -> + length(ejabberd_sm:get_user_resources(User, Host)). + +resource_num(User, Host, Num) -> + Resources = ejabberd_sm:get_user_resources(User, Host), + case (0 + lists:nth(Num, Resources); + false -> + throw({bad_argument, + lists:flatten(io_lib:format("Wrong resource number: ~p", [Num]))}) + end. + +kick_session(User, Server, Resource, ReasonText) -> + kick_this_session(User, Server, Resource, prepare_reason(ReasonText)), + ok. + +kick_this_session(User, Server, Resource, Reason) -> + ejabberd_sm:route(jid:make(User, Server, Resource), + {exit, Reason}). + +status_num(Host, Status) -> + length(get_status_list(Host, Status)). +status_num(Status) -> + status_num(<<"all">>, Status). +status_list(Host, Status) -> + Res = get_status_list(Host, Status), + [{U, S, R, num_prio(P), St} || {U, S, R, P, St} <- Res]. +status_list(Status) -> + status_list(<<"all">>, Status). + +status_list_v3(ArgHost, Status) -> + List = status_list(ArgHost, Status), + [{jid:encode(jid:make(User, Host, Resource)), Priority, StatusText} + || {User, Host, Resource, Priority, StatusText} <- List]. + +status_list_v3(Status) -> + status_list_v3(<<"all">>, Status). + + +get_status_list(Host, Status_required) -> + %% Get list of all logged users + Sessions = ejabberd_sm:dirty_get_my_sessions_list(), + %% Reformat the list + Sessions2 = [ {Session#session.usr, Session#session.sid, Session#session.priority} || Session <- Sessions], + Fhost = case Host of + <<"all">> -> + %% All hosts are requested, so don't filter at all + fun(_, _) -> true end; + _ -> + %% Filter the list, only Host is interesting + fun(A, B) -> A == B end + end, + Sessions3 = [ {Pid, Server, Priority} || {{_User, Server, _Resource}, {_, Pid}, Priority} <- Sessions2, apply(Fhost, [Server, Host])], + %% For each Pid, get its presence + Sessions4 = [ {catch get_presence(Pid), Server, Priority} || {Pid, Server, Priority} <- Sessions3], + %% Filter by status + Fstatus = case Status_required of + <<"all">> -> + fun(_, _) -> true end; + _ -> + fun(A, B) -> A == B end + end, + [{User, Server, Resource, num_prio(Priority), stringize(Status_text)} + || {{User, Resource, Status, Status_text}, Server, Priority} <- Sessions4, + apply(Fstatus, [Status, Status_required])]. + +connected_users_info() -> + lists:filtermap( + fun({U, S, R}) -> + case user_session_info(U, S, R) of + offline -> + false; + Info -> + Jid = jid:encode(jid:make(U, S, R)), + {true, erlang:insert_element(1, Info, Jid)} + end + end, + ejabberd_sm:dirty_get_sessions_list()). + +connected_users_vhost(Host) -> + USRs = ejabberd_sm:get_vh_session_list(Host), + [ jid:encode(jid:make(USR)) || USR <- USRs]. + +%% Make string more print-friendly +stringize(String) -> + %% Replace newline characters with other code + ejabberd_regexp:greplace(String, <<"\n">>, <<"\\n">>). + +get_presence(Pid) -> + try get_presence2(Pid) of + {_, _, _, _} = Res -> + Res + catch + _:_ -> {<<"">>, <<"">>, <<"offline">>, <<"">>} + end. +get_presence2(Pid) -> + Pres = #presence{from = From} = ejabberd_c2s:get_presence(Pid), + Show = case Pres of + #presence{type = unavailable} -> <<"unavailable">>; + #presence{show = undefined} -> <<"available">>; + #presence{show = S} -> atom_to_binary(S, utf8) + end, + Status = xmpp:get_text(Pres#presence.status), + {From#jid.user, From#jid.resource, Show, Status}. + +get_presence(U, S) -> + Pids = [ejabberd_sm:get_session_pid(U, S, R) + || R <- ejabberd_sm:get_user_resources(U, S)], + OnlinePids = [Pid || Pid <- Pids, Pid=/=none], + case OnlinePids of + [] -> + {jid:encode({U, S, <<>>}), <<"unavailable">>, <<"">>}; + [SessionPid|_] -> + {_User, Resource, Show, Status} = get_presence(SessionPid), + FullJID = jid:encode({U, S, Resource}), + {FullJID, Show, Status} + end. + +set_presence(User, Host, Resource, Type, Show, Status, Priority) when is_binary(Priority) -> + set_presence(User, Host, Resource, Type, Show, Status, binary_to_integer(Priority)); + +set_presence(User, Host, Resource, Type, Show, Status, Priority) -> + Pres = #presence{ + from = jid:make(User, Host, Resource), + to = jid:make(User, Host), + type = misc:binary_to_atom(Type), + status = xmpp:mk_text(Status), + show = misc:binary_to_atom(Show), + priority = Priority, + sub_els = []}, + case ejabberd_sm:get_session_pid(User, Host, Resource) of + none -> throw({error, "User session not found"}); + Ref -> ejabberd_c2s:set_presence(Ref, Pres) + end. + +user_sessions_info(User, Host) -> + lists:filtermap(fun(Resource) -> + case user_session_info(User, Host, Resource) of + offline -> false; + Info -> {true, Info} + end + end, ejabberd_sm:get_user_resources(User, Host)). + +user_session_info(User, Host, Resource) -> + CurrentSec = calendar:datetime_to_gregorian_seconds({date(), time()}), + case ejabberd_sm:get_user_info(User, Host, Resource) of + offline -> + offline; + Info -> + Now = proplists:get_value(ts, Info), + Pid = proplists:get_value(pid, Info), + {_U, _Resource, Status, StatusText} = get_presence(Pid), + Priority = proplists:get_value(priority, Info), + Conn = proplists:get_value(conn, Info), + {Ip, Port} = proplists:get_value(ip, Info), + IPS = inet_parse:ntoa(Ip), + NodeS = atom_to_list(node(Pid)), + Uptime = CurrentSec - calendar:datetime_to_gregorian_seconds( + calendar:now_to_local_time(Now)), + {atom_to_list(Conn), IPS, Port, num_prio(Priority), NodeS, Uptime, Status, Resource, StatusText} + end. + + +%%% +%%% Vcard +%%% + +set_nickname(User, Host, Nickname) -> + VCard = xmpp:encode(#vcard_temp{nickname = Nickname}), + case mod_vcard:set_vcard(User, jid:nameprep(Host), VCard) of + {error, badarg} -> + error; + ok -> + ok + end. + +get_vcard(User, Host, Name) -> + [Res | _] = get_vcard_content(User, Host, [Name]), + Res. + +get_vcard(User, Host, Name, Subname) -> + [Res | _] = get_vcard_content(User, Host, [Name, Subname]), + Res. + +get_vcard_multi(User, Host, Name, Subname) -> + get_vcard_content(User, Host, [Name, Subname]). + +set_vcard(User, Host, Name, SomeContent) -> + set_vcard_content(User, Host, [Name], SomeContent). + +set_vcard(User, Host, Name, Subname, SomeContent) -> + set_vcard_content(User, Host, [Name, Subname], SomeContent). + +%% +%% Room vcard + +is_muc_service(Domain) -> + try mod_muc_admin:get_room_serverhost(Domain) of + Domain -> false; + Service when is_binary(Service) -> true + catch _:{unregistered_route, _} -> + throw(error_wrong_hostname) + end. + +get_room_vcard(Name, Service) -> + case mod_muc_admin:get_room_options(Name, Service) of + [] -> + throw(error_no_vcard_found); + Opts -> + case lists:keyfind(<<"vcard">>, 1, Opts) of + false -> + throw(error_no_vcard_found); + {_, VCardRaw} -> + [fxml_stream:parse_element(VCardRaw)] + end + end. + +%% +%% Internal vcard + +get_vcard_content(User, Server, Data) -> + case get_vcard_element(User, Server) of + [El|_] -> + case get_vcard(Data, El) of + [false] -> throw(error_no_value_found_in_vcard); + ElemList -> ?DEBUG("ELS ~p", [ElemList]), [fxml:get_tag_cdata(Elem) || Elem <- ElemList] + end; + [] -> + throw(error_no_vcard_found); + error -> + throw(database_failure) + end. + +get_vcard_element(User, Server) -> + case is_muc_service(Server) of + true -> + get_room_vcard(User, Server); + false -> + mod_vcard:get_vcard(jid:nodeprep(User), jid:nameprep(Server)) + end. + +get_vcard([<<"TEL">>, TelType], {_, _, _, OldEls}) -> + {TakenEl, _NewEls} = take_vcard_tel(TelType, OldEls, [], not_found), + [TakenEl]; + +get_vcard([Data1, Data2], A1) -> + case get_subtag(A1, Data1) of + [false] -> [false]; + A2List -> + lists:flatten([get_vcard([Data2], A2) || A2 <- A2List]) + end; + +get_vcard([Data], A1) -> + get_subtag(A1, Data). + +get_subtag(Xmlelement, Name) -> + [fxml:get_subtag(Xmlelement, Name)]. + +set_vcard_content(User, Server, Data, SomeContent) -> + ContentList = case SomeContent of + [Bin | _] when is_binary(Bin) -> SomeContent; + Bin when is_binary(Bin) -> [SomeContent] + end, + %% Get old vcard + A4 = case mod_vcard:get_vcard(jid:nodeprep(User), jid:nameprep(Server)) of + [A1] -> + {_, _, _, A2} = A1, + update_vcard_els(Data, ContentList, A2); + [] -> + update_vcard_els(Data, ContentList, []); + error -> + throw(database_failure) + end, + %% Build new vcard + SubEl = {xmlel, <<"vCard">>, [{<<"xmlns">>,<<"vcard-temp">>}], A4}, + mod_vcard:set_vcard(User, jid:nameprep(Server), SubEl). + +take_vcard_tel(TelType, [{xmlel, <<"TEL">>, _, SubEls}=OldEl | OldEls], NewEls, Taken) -> + {Taken2, NewEls2} = case lists:keymember(TelType, 2, SubEls) of + true -> {fxml:get_subtag(OldEl, <<"NUMBER">>), NewEls}; + false -> {Taken, [OldEl | NewEls]} + end, + take_vcard_tel(TelType, OldEls, NewEls2, Taken2); +take_vcard_tel(TelType, [OldEl | OldEls], NewEls, Taken) -> + take_vcard_tel(TelType, OldEls, [OldEl | NewEls], Taken); +take_vcard_tel(_TelType, [], NewEls, Taken) -> + {Taken, NewEls}. + +update_vcard_els([<<"TEL">>, TelType], [TelValue], OldEls) -> + {_, NewEls} = take_vcard_tel(TelType, OldEls, [], not_found), + NewEl = {xmlel,<<"TEL">>,[], + [{xmlel,TelType,[],[]}, + {xmlel,<<"NUMBER">>,[],[{xmlcdata,TelValue}]}]}, + [NewEl | NewEls]; + +update_vcard_els(Data, ContentList, Els1) -> + Els2 = lists:keysort(2, Els1), + [Data1 | Data2] = Data, + NewEls = case Data2 of + [] -> + [{xmlel, Data1, [], [{xmlcdata,Content}]} || Content <- ContentList]; + [D2] -> + OldEl = case lists:keysearch(Data1, 2, Els2) of + {value, A} -> A; + false -> {xmlel, Data1, [], []} + end, + {xmlel, _, _, ContentOld1} = OldEl, + Content2 = [{xmlel, D2, [], [{xmlcdata,Content}]} || Content <- ContentList], + ContentOld2 = [A || {_, X, _, _} = A <- ContentOld1, X/=D2], + ContentOld3 = lists:keysort(2, ContentOld2), + ContentNew = lists:keymerge(2, Content2, ContentOld3), + [{xmlel, Data1, [], ContentNew}] + end, + Els3 = lists:keydelete(Data1, 2, Els2), + lists:keymerge(2, NewEls, Els3). + + +%%% +%%% Roster +%%% + +add_rosteritem(LocalUser, LocalServer, User, Server, Nick, Group, Subs) when is_binary(Group) -> + add_rosteritem(LocalUser, LocalServer, User, Server, Nick, [Group], Subs); +add_rosteritem(LocalUser, LocalServer, User, Server, Nick, Groups, Subs) -> + case {jid:make(LocalUser, LocalServer), jid:make(User, Server)} of + {error, _} -> + throw({error, "Invalid 'localuser'/'localserver'"}); + {_, error} -> + throw({error, "Invalid 'user'/'server'"}); + {Jid, _Jid2} -> + RosterItem = build_roster_item(User, Server, {add, Nick, Subs, Groups}), + case mod_roster:set_item_and_notify_clients(Jid, RosterItem, true) of + ok -> ok; + _ -> error + end + end. + +subscribe(LU, LS, User, Server, Nick, Group, Subscription, _Xattrs) -> + case {jid:make(LU, LS), jid:make(User, Server)} of + {error, _} -> + throw({error, "Invalid 'localuser'/'localserver'"}); + {_, error} -> + throw({error, "Invalid 'user'/'server'"}); + {_Jid, _Jid2} -> + ItemEl = build_roster_item(User, Server, {add, Nick, Subscription, Group}), + mod_roster:set_items(LU, LS, #roster_query{items = [ItemEl]}) + end. + +delete_rosteritem(LocalUser, LocalServer, User, Server) -> + case {jid:make(LocalUser, LocalServer), jid:make(User, Server)} of + {error, _} -> + throw({error, "Invalid 'localuser'/'localserver'"}); + {_, error} -> + throw({error, "Invalid 'user'/'server'"}); + {Jid, _Jid2} -> + RosterItem = build_roster_item(User, Server, remove), + case mod_roster:set_item_and_notify_clients(Jid, RosterItem, true) of + ok -> ok; + _ -> error + end + end. + +%% ----------------------------- +%% Get Roster +%% ----------------------------- + +get_roster(User, Server) -> + case jid:make(User, Server) of + error -> + throw({error, "Invalid 'user'/'server'"}); + #jid{luser = U, lserver = S} -> + Items = ejabberd_hooks:run_fold(roster_get, S, [], [{U, S}]), + make_roster_xmlrpc(Items) + end. + +make_roster_xmlrpc(Roster) -> + lists:map( + fun(#roster_item{jid = JID, name = Nick, subscription = Sub, ask = Ask, groups = Groups}) -> + JIDS = jid:encode(JID), + Subs = atom_to_list(Sub), + Asks = atom_to_list(Ask), + {JIDS, Nick, Subs, Asks, Groups} + end, + Roster). + +get_roster_count(User, Server) -> + case jid:make(User, Server) of + error -> + throw({error, "Invalid 'user'/'server'"}); + #jid{luser = U, lserver = S} -> + Items = ejabberd_hooks:run_fold(roster_get, S, [], [{U, S}]), + length(Items) + end. + +%%----------------------------- +%% Push Roster from file +%%----------------------------- + +push_roster(File, User, Server) -> + {ok, [Roster]} = file:consult(File), + subscribe_roster({User, Server, <<>>, User}, Roster). + +push_roster_all(File) -> + {ok, [Roster]} = file:consult(File), + subscribe_all(Roster). + +subscribe_all(Roster) -> + subscribe_all(Roster, Roster). +subscribe_all([], _) -> + ok; +subscribe_all([User1 | Users], Roster) -> + subscribe_roster(User1, Roster), + subscribe_all(Users, Roster). + +subscribe_roster(_, []) -> + ok; +%% Do not subscribe a user to itself +subscribe_roster({Name, Server, Group, Nick}, [{Name, Server, _, _} | Roster]) -> + subscribe_roster({Name, Server, Group, Nick}, Roster); +%% Subscribe Name2 to Name1 +subscribe_roster({Name1, Server1, Group1, Nick1}, [{Name2, Server2, Group2, Nick2} | Roster]) -> + subscribe(iolist_to_binary(Name1), iolist_to_binary(Server1), iolist_to_binary(Name2), iolist_to_binary(Server2), + iolist_to_binary(Nick2), iolist_to_binary(Group2), <<"both">>, []), + subscribe_roster({Name1, Server1, Group1, Nick1}, Roster). + +push_alltoall(S, G) -> + Users = ejabberd_auth:get_users(S), + Users2 = build_list_users(G, Users, []), + subscribe_all(Users2), + ok. + +build_list_users(_Group, [], Res) -> + Res; +build_list_users(Group, [{User, Server}|Users], Res) -> + build_list_users(Group, Users, [{User, Server, Group, User}|Res]). + +%% @spec(LU, LS, U, S, Action) -> ok +%% Action = {add, Nick, Subs, Group} | remove +%% @doc Push to the roster of account LU@LS the contact U@S. +%% The specific action to perform is defined in Action. +push_roster_item(LU, LS, U, S, Action) -> + lists:foreach(fun(R) -> + push_roster_item(LU, LS, R, U, S, Action) + end, ejabberd_sm:get_user_resources(LU, LS)). + +push_roster_item(LU, LS, R, U, S, Action) -> + LJID = jid:make(LU, LS, R), + BroadcastEl = build_broadcast(U, S, Action), + ejabberd_sm:route(LJID, BroadcastEl), + Item = build_roster_item(U, S, Action), + ResIQ = build_iq_roster_push(Item), + ejabberd_router:route( + xmpp:set_from_to(ResIQ, jid:remove_resource(LJID), LJID)). + +build_roster_item(U, S, {add, Nick, Subs, Groups}) when is_list(Groups) -> + #roster_item{jid = jid:make(U, S), + name = Nick, + subscription = misc:binary_to_atom(Subs), + groups = Groups}; +build_roster_item(U, S, {add, Nick, Subs, Group}) -> + Groups = binary:split(Group,<<";">>, [global, trim]), + #roster_item{jid = jid:make(U, S), + name = Nick, + subscription = misc:binary_to_atom(Subs), + groups = Groups}; +build_roster_item(U, S, remove) -> + #roster_item{jid = jid:make(U, S), subscription = remove}. + +build_iq_roster_push(Item) -> + #iq{type = set, id = <<"push">>, + sub_els = [#roster_query{items = [Item]}]}. + +build_broadcast(U, S, {add, _Nick, Subs, _Group}) -> + build_broadcast(U, S, list_to_atom(binary_to_list(Subs))); +build_broadcast(U, S, remove) -> + build_broadcast(U, S, none); +%% @spec (U::binary(), S::binary(), Subs::atom()) -> any() +%% Subs = both | from | to | none +build_broadcast(U, S, SubsAtom) when is_atom(SubsAtom) -> + {item, {U, S, <<>>}, SubsAtom}. + +%%% +%%% Last Activity +%%% + +get_last(User, Server) -> + {Now, Status} = case ejabberd_sm:get_user_resources(User, Server) of + [] -> + case mod_last:get_last_info(User, Server) of + not_found -> + {erlang:timestamp(), "NOT FOUND"}; + {ok, Shift, Status1} -> + {{Shift div 1000000, Shift rem 1000000, 0}, Status1} + end; + _ -> + {erlang:timestamp(), "ONLINE"} + end, + {xmpp_util:encode_timestamp(Now), Status}. + +set_last(User, Server, Timestamp, Status) -> + case mod_last:store_last_info(User, Server, Timestamp, Status) of + {ok, _} -> ok; + Error -> Error + end. + +%%% +%%% Private Storage +%%% + +%% Example usage: +%% $ ejabberdctl private_set badlop localhost "\Cluth\" +%% $ ejabberdctl private_get badlop localhost aa bb +%% Cluth + +private_get(Username, Host, Element, Ns) -> + Els = private_get2(Username, Host, Element, Ns), + binary_to_list(fxml:element_to_binary(xmpp:encode(#private{sub_els = Els}))). + +private_get2(Username, Host, Element, Ns) -> + case gen_mod:is_loaded(Host, mod_private) of + true -> private_get3(Username, Host, Element, Ns); + false -> [] + end. + +private_get3(Username, Host, Element, Ns) -> + ElementXml = #xmlel{name = Element, attrs = [{<<"xmlns">>, Ns}]}, + mod_private:get_data(jid:nodeprep(Username), jid:nameprep(Host), + [{Ns, ElementXml}]). + +private_set(Username, Host, ElementString) -> + case fxml_stream:parse_element(ElementString) of + {error, Error} -> + io:format("Error found parsing the element:~n ~p~nError: ~p~n", + [ElementString, Error]), + error; + Xml -> + private_set2(Username, Host, Xml) + end. + +private_set2(Username, Host, Xml) -> + NS = fxml:get_tag_attr_s(<<"xmlns">>, Xml), + JID = jid:make(Username, Host), + mod_private:set_data(JID, [{NS, Xml}]). + +%%% +%%% Shared Roster Groups +%%% + +srg_create(Group, Host, Label, Description, Display) when is_binary(Display) -> + DisplayList = case Display of + <<>> -> []; + _ -> ejabberd_regexp:split(Display, <<"\\\\n">>) + end, + srg_create(Group, Host, Label, Description, DisplayList); + +srg_create(Group, Host, Label, Description, DisplayList) -> + {_DispGroups, WrongDispGroups} = filter_groups_existence(Host, DisplayList), + case (WrongDispGroups -- [Group]) /= [] of + true -> + {wrong_displayed_groups, WrongDispGroups}; + false -> + srg_create2(Group, Host, Label, Description, DisplayList) + end. + +srg_create2(Group, Host, Label, Description, DisplayList) -> + Opts = [{label, Label}, + {displayed_groups, DisplayList}, + {description, Description}], + case mod_shared_roster:create_group(Host, Group, Opts) of + {atomic, _} -> ok; + {error, Err} -> Err + end. + +srg_add(Group, Host) -> + Opts = [{label, <<"">>}, + {description, <<"">>}, + {displayed_groups, []} + ], + case mod_shared_roster:create_group(Host, Group, Opts) of + {atomic, _} -> ok; + {error, Err} -> Err + end. + +srg_delete(Group, Host) -> + {atomic, _} = mod_shared_roster:delete_group(Host, Group), + ok. + +srg_list(Host) -> + lists:sort(mod_shared_roster:list_groups(Host)). + +srg_get_info(Group, Host) -> + Opts = case mod_shared_roster:get_group_opts(Host,Group) of + Os when is_list(Os) -> Os; + error -> [] + end, + [{misc:atom_to_binary(Title), to_list(Value)} || {Title, Value} <- Opts]. + +to_list([]) -> []; +to_list([H|_]=List) when is_binary(H) -> lists:join(", ", [to_list(E) || E <- List]); +to_list(E) when is_atom(E) -> atom_to_list(E); +to_list(E) when is_binary(E) -> binary_to_list(E). + +%% @format-begin + +srg_set_info(Group, Host, Key, Value) -> + Opts = + case mod_shared_roster:get_group_opts(Host, Group) of + Os when is_list(Os) -> + Os; + error -> + [] + end, + Opts2 = srg_set_info(Key, Value, Opts), + case mod_shared_roster:set_group_opts(Host, Group, Opts2) of + {atomic, ok} -> + ok; + Problem -> + ?INFO_MSG("Problem: ~n ~p", [Problem]), %+++ + error + end. + +srg_set_info(<<"description">>, Value, Opts) -> + [{description, Value} | proplists:delete(description, Opts)]; +srg_set_info(<<"label">>, Value, Opts) -> + [{label, Value} | proplists:delete(label, Opts)]; +srg_set_info(<<"all_users">>, <<"true">>, Opts) -> + [{all_users, true} | proplists:delete(all_users, Opts)]; +srg_set_info(<<"online_users">>, <<"true">>, Opts) -> + [{online_users, true} | proplists:delete(online_users, Opts)]; +srg_set_info(<<"all_users">>, _, Opts) -> + proplists:delete(all_users, Opts); +srg_set_info(<<"online_users">>, _, Opts) -> + proplists:delete(online_users, Opts); +srg_set_info(Key, _Value, Opts) -> + ?ERROR_MSG("Unknown Key in srg_set_info: ~p", [Key]), + Opts. + +srg_get_displayed(Group, Host) -> + Opts = + case mod_shared_roster:get_group_opts(Host, Group) of + Os when is_list(Os) -> + Os; + error -> + [] + end, + proplists:get_value(displayed_groups, Opts, []). + +srg_add_displayed(Group, Host, NewGroup) -> + Opts = + case mod_shared_roster:get_group_opts(Host, Group) of + Os when is_list(Os) -> + Os; + error -> + [] + end, + {DispGroups, WrongDispGroups} = filter_groups_existence(Host, [NewGroup]), + case WrongDispGroups /= [] of + true -> + {wrong_displayed_groups, WrongDispGroups}; + false -> + DisplayedOld = proplists:get_value(displayed_groups, Opts, []), + Opts2 = + [{displayed_groups, lists:flatten(DisplayedOld, DispGroups)} + | proplists:delete(displayed_groups, Opts)], + case mod_shared_roster:set_group_opts(Host, Group, Opts2) of + {atomic, ok} -> + ok; + Problem -> + ?INFO_MSG("Problem: ~n ~p", [Problem]), %+++ + error + end + end. + +srg_del_displayed(Group, Host, OldGroup) -> + Opts = + case mod_shared_roster:get_group_opts(Host, Group) of + Os when is_list(Os) -> + Os; + error -> + [] + end, + DisplayedOld = proplists:get_value(displayed_groups, Opts, []), + {DispGroups, OldDispGroups} = lists:partition(fun(G) -> G /= OldGroup end, DisplayedOld), + case OldDispGroups == [] of + true -> + {inexistent_displayed_groups, OldGroup}; + false -> + Opts2 = [{displayed_groups, DispGroups} | proplists:delete(displayed_groups, Opts)], + case mod_shared_roster:set_group_opts(Host, Group, Opts2) of + {atomic, ok} -> + ok; + Problem -> + ?INFO_MSG("Problem: ~n ~p", [Problem]), %+++ + error + end + end. + +filter_groups_existence(Host, Groups) -> + lists:partition(fun(Group) -> error /= mod_shared_roster:get_group_opts(Host, Group) end, + Groups). +%% @format-end + +srg_get_members(Group, Host) -> + Members = mod_shared_roster:get_group_explicit_users(Host,Group), + [jid:encode(jid:make(MUser, MServer)) + || {MUser, MServer} <- Members]. + +srg_user_add(User, Host, Group, GroupHost) -> + mod_shared_roster:add_user_to_group(GroupHost, {User, Host}, Group), + ok. + +srg_user_del(User, Host, Group, GroupHost) -> + mod_shared_roster:remove_user_from_group(GroupHost, {User, Host}, Group), + ok. + + +%%% +%%% Stanza +%%% + +%% @doc Send a message to an XMPP account. +-spec send_message(Type::binary(), From::binary(), To::binary(), + Subject::binary(), Body::binary()) -> ok. +send_message(Type, From, To, Subject, Body) -> + CodecOpts = ejabberd_config:codec_options(), + try xmpp:decode( + #xmlel{name = <<"message">>, + attrs = [{<<"to">>, To}, + {<<"from">>, From}, + {<<"type">>, Type}, + {<<"id">>, p1_rand:get_string()}], + children = + [#xmlel{name = <<"subject">>, + children = [{xmlcdata, Subject}]}, + #xmlel{name = <<"body">>, + children = [{xmlcdata, Body}]}]}, + ?NS_CLIENT, CodecOpts) of + #message{from = JID, subject = SubjectEl, body = BodyEl} = Msg -> + Msg2 = case {xmpp:get_text(SubjectEl), xmpp:get_text(BodyEl)} of + {Subject, <<>>} -> Msg; + {<<>>, Body} -> Msg#message{subject = []}; + _ -> Msg + end, + State = #{jid => JID}, + ejabberd_hooks:run_fold(user_send_packet, JID#jid.lserver, {Msg2, State}, []), + ejabberd_router:route(Msg2) + catch _:{xmpp_codec, Why} -> + {error, xmpp:format_error(Why)} + end. + +send_stanza(FromString, ToString, Stanza) -> + try + #xmlel{} = El = fxml_stream:parse_element(Stanza), + From = jid:decode(FromString), + To = jid:decode(ToString), + CodecOpts = ejabberd_config:codec_options(), + Pkt = xmpp:decode(El, ?NS_CLIENT, CodecOpts), + Pkt2 = xmpp:set_from_to(Pkt, From, To), + State = #{jid => From}, + ejabberd_hooks:run_fold(user_send_packet, From#jid.lserver, + {Pkt2, State}, []), + ejabberd_router:route(Pkt2) + catch _:{xmpp_codec, Why} -> + io:format("incorrect stanza: ~ts~n", [xmpp:format_error(Why)]), + {error, Why}; + _:{badmatch, {error, {Code, Why}}} when is_integer(Code) -> + io:format("invalid xml: ~p~n", [Why]), + {error, Why}; + _:{badmatch, {error, Why}} -> + io:format("invalid xml: ~p~n", [Why]), + {error, Why}; + _:{bad_jid, S} -> + io:format("malformed JID: ~ts~n", [S]), + {error, "JID malformed"} + end. + +-spec send_stanza_c2s(binary(), binary(), binary(), binary()) -> ok | {error, any()}. +send_stanza_c2s(Username, Host, Resource, Stanza) -> + try + #xmlel{} = El = fxml_stream:parse_element(Stanza), + CodecOpts = ejabberd_config:codec_options(), + Pkt = xmpp:decode(El, ?NS_CLIENT, CodecOpts), + case ejabberd_sm:get_session_pid(Username, Host, Resource) of + Pid when is_pid(Pid) -> + ejabberd_c2s:send(Pid, Pkt); + _ -> + {error, no_session} + end + catch _:{badmatch, {error, Why} = Err} -> + io:format("invalid xml: ~p~n", [Why]), + Err; + _:{xmpp_codec, Why} -> + io:format("incorrect stanza: ~ts~n", [xmpp:format_error(Why)]), + {error, Why} + end. + +privacy_set(Username, Host, QueryS) -> + Jid = jid:make(Username, Host), + QueryEl = fxml_stream:parse_element(QueryS), + SubEl = xmpp:decode(QueryEl), + IQ = #iq{type = set, id = <<"push">>, sub_els = [SubEl], + from = Jid, to = Jid}, + Result = mod_privacy:process_iq(IQ), + Result#iq.type == result. + +%%% +%%% Stats +%%% + +stats(Name) -> + case Name of + <<"uptimeseconds">> -> trunc(element(1, erlang:statistics(wall_clock))/1000); + <<"processes">> -> length(erlang:processes()); + <<"registeredusers">> -> lists:foldl(fun(Host, Sum) -> ejabberd_auth:count_users(Host) + Sum end, 0, ejabberd_option:hosts()); + <<"onlineusersnode">> -> length(ejabberd_sm:dirty_get_my_sessions_list()); + <<"onlineusers">> -> length(ejabberd_sm:dirty_get_sessions_list()) + end. + +stats(Name, Host) -> + case Name of + <<"registeredusers">> -> ejabberd_auth:count_users(Host); + <<"onlineusers">> -> length(ejabberd_sm:get_vh_session_list(Host)) + end. + + +user_action(User, Server, Fun, OK) -> + case ejabberd_auth:user_exists(User, Server) of + true -> + case catch Fun() of + OK -> ok; + {error, Error} -> throw(Error); + Error -> + ?ERROR_MSG("Command returned: ~p", [Error]), + 1 + end; + false -> + throw({not_found, "unknown_user"}) + end. + +num_prio(Priority) when is_integer(Priority) -> + Priority; +num_prio(_) -> + -1. + +%%% +%%% Web Admin +%%% + +%% @format-begin + +%%% Main + +web_menu_main(Acc, _Lang) -> + Acc ++ [{<<"stats">>, <<"Statistics">>}]. + +web_page_main(_, #request{path = [<<"stats">>]} = R) -> + Res = ?H1GL(<<"Statistics">>, <<"modules/#mod_stats">>, <<"mod_stats">>) + ++ [make_command(stats_host, R, [], [{only, presentation}]), + make_command(incoming_s2s_number, R, [], [{only, presentation}]), + make_command(outgoing_s2s_number, R, [], [{only, presentation}]), + make_table([<<"stat name">>, {<<"stat value">>, right}], + [{?C(<<"Registered Users:">>), + make_command(stats, + R, + [{<<"name">>, <<"registeredusers">>}], + [{only, value}])}, + {?C(<<"Online Users:">>), + make_command(stats, + R, + [{<<"name">>, <<"onlineusers">>}], + [{only, value}])}, + {?C(<<"S2S Connections Incoming:">>), + make_command(incoming_s2s_number, R, [], [{only, value}])}, + {?C(<<"S2S Connections Outgoing:">>), + make_command(outgoing_s2s_number, R, [], [{only, value}])}])], + {stop, Res}; +web_page_main(Acc, _) -> + Acc. + +%%% Host + +web_menu_host(Acc, _Host, _Lang) -> + Acc ++ [{<<"purge">>, <<"Purge">>}, {<<"stats">>, <<"Statistics">>}]. + +web_page_host(_, Host, #request{path = [<<"purge">>]} = R) -> + Head = [?XC(<<"h1">>, <<"Purge">>)], + Set = [ejabberd_web_admin:make_command(delete_old_users_vhost, + R, + [{<<"host">>, Host}], + [])], + {stop, Head ++ Set}; +web_page_host(_, Host, #request{path = [<<"stats">>]} = R) -> + Res = ?H1GL(<<"Statistics">>, <<"modules/#mod_stats">>, <<"mod_stats">>) + ++ [make_command(stats_host, R, [], [{only, presentation}]), + make_table([<<"stat name">>, {<<"stat value">>, right}], + [{?C(<<"Registered Users:">>), + make_command(stats_host, + R, + [{<<"host">>, Host}, {<<"name">>, <<"registeredusers">>}], + [{only, value}, + {result_links, [{stat, arg_host, 3, <<"users">>}]}])}, + {?C(<<"Online Users:">>), + make_command(stats_host, + R, + [{<<"host">>, Host}, {<<"name">>, <<"onlineusers">>}], + [{only, value}, + {result_links, + [{stat, arg_host, 3, <<"online-users">>}]}])}])], + {stop, Res}; +web_page_host(Acc, _, _) -> + Acc. + +%%% HostUser + +web_menu_hostuser(Acc, _Host, _Username, _Lang) -> + Acc ++ [{<<"auth">>, <<"Authentication">>}, {<<"session">>, <<"Sessions">>}]. + +web_page_hostuser(_, Host, User, #request{path = [<<"auth">>]} = R) -> + Ban = make_command(ban_account, + R, + [{<<"user">>, User}, {<<"host">>, Host}], + [{style, danger}]), + Unban = make_command(unban_account, R, [{<<"user">>, User}, {<<"host">>, Host}], []), + Res = ?H1GLraw(<<"Authentication">>, + <<"admin/configuration/authentication/">>, + <<"Authentication">>) + ++ [make_command(register, R, [{<<"user">>, User}, {<<"host">>, Host}], []), + make_command(check_account, R, [{<<"user">>, User}, {<<"host">>, Host}], []), + ?X(<<"hr">>), + make_command(check_password, R, [{<<"user">>, User}, {<<"host">>, Host}], []), + make_command(check_password_hash, R, [{<<"user">>, User}, {<<"host">>, Host}], []), + make_command(change_password, + R, + [{<<"user">>, User}, {<<"host">>, Host}], + [{style, danger}]), + ?X(<<"hr">>), + make_command(get_ban_details, R, [{<<"user">>, User}, {<<"host">>, Host}], []), + Ban, + Unban, + ?X(<<"hr">>), + make_command(unregister, + R, + [{<<"user">>, User}, {<<"host">>, Host}], + [{style, danger}])], + {stop, Res}; +web_page_hostuser(_, Host, User, #request{path = [<<"session">>]} = R) -> + Head = [?XC(<<"h1">>, <<"Sessions">>), ?BR], + Set = [make_command(resource_num, R, [{<<"user">>, User}, {<<"host">>, Host}], []), + make_command(set_presence, R, [{<<"user">>, User}, {<<"host">>, Host}], []), + make_command(kick_user, R, [{<<"user">>, User}, {<<"host">>, Host}], [{style, danger}]), + make_command(kick_session, + R, + [{<<"user">>, User}, {<<"host">>, Host}], + [{style, danger}])], + timer:sleep(100), % kicking sessions takes a while, let's delay the get commands + Get = [make_command(user_sessions_info, + R, + [{<<"user">>, User}, {<<"host">>, Host}], + [{result_links, [{node, node, 5, <<>>}]}]), + make_command(user_resources, R, [{<<"user">>, User}, {<<"host">>, Host}], []), + make_command(get_presence, R, [{<<"user">>, User}, {<<"host">>, Host}], []), + make_command(num_resources, R, [{<<"user">>, User}, {<<"host">>, Host}], [])], + {stop, Head ++ Get ++ Set}; +web_page_hostuser(Acc, _, _, _) -> + Acc. + +%%% HostNode + +web_menu_hostnode(Acc, _Host, _Username, _Lang) -> + Acc ++ [{<<"modules">>, <<"Modules">>}]. + +web_page_hostnode(_, Host, Node, #request{path = [<<"modules">>]} = R) -> + Res = ?H1GLraw(<<"Modules">>, <<"admin/configuration/modules/">>, <<"Modules Options">>) + ++ [ejabberd_cluster:call(Node, + ejabberd_web_admin, + make_command, + [restart_module, R, [{<<"host">>, Host}], []])], + {stop, Res}; +web_page_hostnode(Acc, _Host, _Node, _Request) -> + Acc. + +%%% Node + +web_menu_node(Acc, _Node, _Lang) -> + Acc ++ [{<<"stats">>, <<"Statistics">>}]. + +web_page_node(_, Node, #request{path = [<<"stats">>]} = R) -> + UpSecs = + ejabberd_cluster:call(Node, + ejabberd_web_admin, + make_command, + [stats, R, [{<<"name">>, <<"uptimeseconds">>}], [{only, value}]]), + UpDaysBin = + integer_to_binary(binary_to_integer(fxml:get_tag_cdata(UpSecs)) + div 86400), % 24*60*60 + UpDays = + #xmlel{name = <<"code">>, + attrs = [], + children = [{xmlcdata, UpDaysBin}]}, + Res = ?H1GL(<<"Statistics">>, <<"modules/#mod_stats">>, <<"mod_stats">>) + ++ [make_command(stats, R, [], [{only, presentation}]), + make_table([<<"stat name">>, {<<"stat value">>, right}], + [{?C(<<"Online Users in this node:">>), + ejabberd_cluster:call(Node, + ejabberd_web_admin, + make_command, + [stats, + R, + [{<<"name">>, <<"onlineusersnode">>}], + [{only, value}]])}, + {?C(<<"Uptime Seconds:">>), UpSecs}, + {?C(<<"Uptime Seconds (rounded to days):">>), UpDays}, + {?C(<<"Processes:">>), + ejabberd_cluster:call(Node, + ejabberd_web_admin, + make_command, + [stats, + R, + [{<<"name">>, <<"processes">>}], + [{only, value}]])}])], + {stop, Res}; +web_page_node(Acc, _, _) -> + Acc. +%% @format-end + +%%% +%%% Document +%%% + +mod_options(_) -> []. + +mod_doc() -> + #{desc => + [?T("This module provides additional administrative commands."), "", + ?T("Details for some commands:"), "", + ?T("_`ban_account`_ API:"), + ?T("This command kicks all the connected sessions of the account " + "from the server. It also changes their password to a randomly " + "generated one, so they can't login anymore unless a server " + "administrator changes their password again. It is possible to " + "define the reason of the ban. The new password also includes " + "the reason and the date and time of the ban. See an example below."), "", + ?T("_`push_roster`_ API (and _`push_roster_all`_ API):"), + ?T("The roster file must be placed, if using Windows, on the " + "directory where you installed ejabberd: " + "`C:/Program Files/ejabberd` or similar. If you use other " + "Operating System, place the file on the same directory where " + "the .beam files are installed. See below an example roster file."), "", + ?T("_`srg_create`_ API:"), + ?T("If you want to put a group Name with blank spaces, use the " + "characters '\"\'' and '\'\"' to define when the Name starts and " + "ends. See an example below.")], + example => + [{?T("With this configuration, vCards can only be modified with " + "mod_admin_extra commands:"), + ["acl:", + " adminextraresource:", + " - resource: \"modadminextraf8x,31ad\"", + "access_rules:", + " vcard_set:", + " - allow: adminextraresource", + "modules:", + " mod_admin_extra: {}", + " mod_vcard:", + " access_set: vcard_set"]}, + {?T("Content of roster file for _`push_roster`_ API:"), + ["[{<<\"bob\">>, <<\"example.org\">>, <<\"workers\">>, <<\"Bob\">>},", + "{<<\"mart\">>, <<\"example.org\">>, <<\"workers\">>, <<\"Mart\">>},", + "{<<\"Rich\">>, <<\"example.org\">>, <<\"bosses\">>, <<\"Rich\">>}]."]}, + {?T("With this call, the sessions of the local account which JID is " + "'boby@example.org' will be kicked, and its password will be set " + "to something like " + "'BANNED_ACCOUNT--20080425T21:45:07--2176635--Spammed_rooms'"), + ["ejabberdctl vhost example.org ban_account boby \"Spammed rooms\""]}, + {?T("Call to _`srg_create`_ API using double-quotes and single-quotes:"), + ["ejabberdctl srg_create g1 example.org \"\'Group number 1\'\" this_is_g1 g1"]}]}. diff --git a/src/mod_admin_update_sql.erl b/src/mod_admin_update_sql.erl new file mode 100644 index 000000000..105828f7d --- /dev/null +++ b/src/mod_admin_update_sql.erl @@ -0,0 +1,654 @@ +%%%------------------------------------------------------------------- +%%% File : mod_admin_update_sql.erl +%%% Author : Alexey Shchepin +%%% Purpose : Convert SQL DB to the new format +%%% Created : 9 Aug 2017 by Alexey Shchepin +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_admin_update_sql). +-author('alexey@process-one.net'). + +-behaviour(gen_mod). + +-export([start/2, stop/1, reload/3, mod_options/1, + get_commands_spec/0, depends/2, mod_doc/0]). + +% Commands API +-export([update_sql/0]). + +% For testing +-export([update_sql/1]). + +-include("logger.hrl"). +-include("ejabberd_commands.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). +-include("ejabberd_sql_pt.hrl"). +-include("translate.hrl"). + +%%% +%%% gen_mod +%%% + +start(_Host, _Opts) -> + {ok, [{commands, get_commands_spec()}]}. + +stop(_Host) -> + ok. + +reload(_Host, _NewOpts, _OldOpts) -> + ok. + +depends(_Host, _Opts) -> + []. + +%%% +%%% Register commands +%%% + +get_commands_spec() -> + [#ejabberd_commands{name = update_sql, tags = [sql], + desc = "Convert MS SQL, MySQL or PostgreSQL DB to the new format", + note = "improved in 23.04", + module = ?MODULE, function = update_sql, + args = [], + args_example = [], + args_desc = [], + result = {res, rescode}, + result_example = ok} + ]. + +update_sql() -> + lists:foreach( + fun(Host) -> + case ejabberd_sql_sup:is_started(Host) of + false -> + ok; + true -> + update_sql(Host) + end + end, ejabberd_option:hosts()). + +-record(state, {host :: binary(), + dbtype :: mysql | pgsql | sqlite | mssql | odbc, + escape}). + +update_sql(Host) -> + LHost = jid:nameprep(Host), + DBType = ejabberd_option:sql_type(LHost), + IsSupported = + case DBType of + mssql -> true; + mysql -> true; + pgsql -> true; + _ -> false + end, + if + not IsSupported -> + io:format("Converting ~p DB is not supported~n", [DBType]), + error; + true -> + Escape = + case DBType of + mssql -> fun ejabberd_sql:standard_escape/1; + sqlite -> fun ejabberd_sql:standard_escape/1; + _ -> fun ejabberd_sql:escape/1 + end, + State = #state{host = LHost, + dbtype = DBType, + escape = Escape}, + update_tables(State), + check_config() + end. + +check_config() -> + case ejabberd_sql:use_new_schema() of + true -> ok; + false -> + ejabberd_config:set_option(new_sql_schema, true), + io:format('~nNOTE: you must add "new_sql_schema: true" to ejabberd.yml before next restart~n~n', []) + end. + +update_tables(State) -> + case add_sh_column(State, "users") of + true -> + drop_pkey(State, "users"), + add_pkey(State, "users", ["server_host", "username", "type"]), + drop_sh_default(State, "users"); + false -> + ok + end, + + case add_sh_column(State, "last") of + true -> + drop_pkey(State, "last"), + add_pkey(State, "last", ["server_host", "username"]), + drop_sh_default(State, "last"); + false -> + ok + end, + + case add_sh_column(State, "rosterusers") of + true -> + drop_index(State, "rosterusers", "i_rosteru_user_jid"), + drop_index(State, "rosterusers", "i_rosteru_username"), + drop_index(State, "rosterusers", "i_rosteru_jid"), + create_unique_index(State, "rosterusers", "i_rosteru_sh_user_jid", ["server_host", "username", "jid"]), + create_index(State, "rosterusers", "i_rosteru_sh_jid", ["server_host", "jid"]), + drop_sh_default(State, "rosterusers"); + false -> + ok + end, + + case add_sh_column(State, "rostergroups") of + true -> + drop_index(State, "rostergroups", "pk_rosterg_user_jid"), + create_index(State, "rostergroups", "i_rosterg_sh_user_jid", ["server_host", "username", "jid"]), + drop_sh_default(State, "rostergroups"); + false -> + ok + end, + + case add_sh_column(State, "sr_group") of + true -> + drop_index(State, "sr_group", "i_sr_group_name"), + create_unique_index(State, "sr_group", "i_sr_group_sh_name", ["server_host", "name"]), + drop_sh_default(State, "sr_group"); + false -> + ok + end, + + case add_sh_column(State, "sr_user") of + true -> + drop_index(State, "sr_user", "i_sr_user_jid_grp"), + drop_index(State, "sr_user", "i_sr_user_jid"), + drop_index(State, "sr_user", "i_sr_user_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_grp", ["server_host", "grp"]), + drop_sh_default(State, "sr_user"); + false -> + ok + end, + + case add_sh_column(State, "spool") of + true -> + drop_index(State, "spool", "i_despool"), + create_index(State, "spool", "i_spool_sh_username", ["server_host", "username"]), + drop_sh_default(State, "spool"); + false -> + ok + end, + + case add_sh_column(State, "archive") of + true -> + drop_index(State, "archive", "i_username"), + drop_index(State, "archive", "i_username_timestamp"), + drop_index(State, "archive", "i_timestamp"), + drop_index(State, "archive", "i_peer"), + drop_index(State, "archive", "i_bare_peer"), + drop_index(State, "archive", "i_username_peer"), + drop_index(State, "archive", "i_username_bare_peer"), + create_index(State, "archive", "i_archive_sh_username_timestamp", ["server_host", "username", "timestamp"]), + create_index(State, "archive", "i_archive_sh_timestamp", ["server_host", "timestamp"]), + create_index(State, "archive", "i_archive_sh_username_peer", ["server_host", "username", "peer"]), + create_index(State, "archive", "i_archive_sh_username_bare_peer", ["server_host", "username", "bare_peer"]), + drop_sh_default(State, "archive"); + false -> + ok + end, + + case add_sh_column(State, "archive_prefs") of + true -> + drop_pkey(State, "archive_prefs"), + add_pkey(State, "archive_prefs", ["server_host", "username"]), + drop_sh_default(State, "archive_prefs"); + false -> + ok + end, + + case add_sh_column(State, "vcard") of + true -> + drop_pkey(State, "vcard"), + add_pkey(State, "vcard", ["server_host", "username"]), + drop_sh_default(State, "vcard"); + false -> + ok + end, + + case add_sh_column(State, "vcard_search") of + true -> + drop_pkey(State, "vcard_search"), + drop_index(State, "vcard_search", "i_vcard_search_lfn"), + drop_index(State, "vcard_search", "i_vcard_search_lfamily"), + drop_index(State, "vcard_search", "i_vcard_search_lgiven"), + drop_index(State, "vcard_search", "i_vcard_search_lmiddle"), + drop_index(State, "vcard_search", "i_vcard_search_lnickname"), + drop_index(State, "vcard_search", "i_vcard_search_lbday"), + drop_index(State, "vcard_search", "i_vcard_search_lctry"), + drop_index(State, "vcard_search", "i_vcard_search_llocality"), + drop_index(State, "vcard_search", "i_vcard_search_lemail"), + drop_index(State, "vcard_search", "i_vcard_search_lorgname"), + drop_index(State, "vcard_search", "i_vcard_search_lorgunit"), + add_pkey(State, "vcard_search", ["server_host", "lusername"]), + create_index(State, "vcard_search", "i_vcard_search_sh_lfn", ["server_host", "lfn"]), + create_index(State, "vcard_search", "i_vcard_search_sh_lfamily", ["server_host", "lfamily"]), + create_index(State, "vcard_search", "i_vcard_search_sh_lgiven", ["server_host", "lgiven"]), + create_index(State, "vcard_search", "i_vcard_search_sh_lmiddle", ["server_host", "lmiddle"]), + create_index(State, "vcard_search", "i_vcard_search_sh_lnickname", ["server_host", "lnickname"]), + create_index(State, "vcard_search", "i_vcard_search_sh_lbday", ["server_host", "lbday"]), + create_index(State, "vcard_search", "i_vcard_search_sh_lctry", ["server_host", "lctry"]), + create_index(State, "vcard_search", "i_vcard_search_sh_llocality", ["server_host", "llocality"]), + create_index(State, "vcard_search", "i_vcard_search_sh_lemail", ["server_host", "lemail"]), + create_index(State, "vcard_search", "i_vcard_search_sh_lorgname", ["server_host", "lorgname"]), + create_index(State, "vcard_search", "i_vcard_search_sh_lorgunit", ["server_host", "lorgunit"]), + drop_sh_default(State, "vcard_search"); + false -> + ok + end, + + case add_sh_column(State, "privacy_default_list") of + true -> + drop_pkey(State, "privacy_default_list"), + add_pkey(State, "privacy_default_list", ["server_host", "username"]), + drop_sh_default(State, "privacy_default_list"); + false -> + ok + end, + + case add_sh_column(State, "privacy_list") of + true -> + drop_index(State, "privacy_list", "i_privacy_list_username"), + drop_index(State, "privacy_list", "i_privacy_list_username_name"), + create_unique_index(State, "privacy_list", "i_privacy_list_sh_username_name", ["server_host", "username", "name"]), + drop_sh_default(State, "privacy_list"); + false -> + ok + end, + + case add_sh_column(State, "private_storage") of + true -> + drop_index(State, "private_storage", "i_private_storage_username"), + drop_index(State, "private_storage", "i_private_storage_username_namespace"), + drop_pkey(State, "private_storage"), + add_pkey(State, "private_storage", ["server_host", "username", "namespace"]), + drop_sh_default(State, "private_storage"); + false -> + ok + end, + + case add_sh_column(State, "roster_version") of + true -> + drop_pkey(State, "roster_version"), + add_pkey(State, "roster_version", ["server_host", "username"]), + drop_sh_default(State, "roster_version"); + false -> + ok + end, + + case add_sh_column(State, "muc_room") of + true -> + drop_sh_default(State, "muc_room"); + false -> + ok + end, + + case add_sh_column(State, "muc_registered") of + true -> + drop_sh_default(State, "muc_registered"); + false -> + ok + end, + + case add_sh_column(State, "muc_online_room") of + true -> + drop_sh_default(State, "muc_online_room"); + false -> + ok + end, + + case add_sh_column(State, "muc_online_users") of + true -> + drop_sh_default(State, "muc_online_users"); + false -> + ok + end, + + case add_sh_column(State, "motd") of + true -> + drop_pkey(State, "motd"), + add_pkey(State, "motd", ["server_host", "username"]), + drop_sh_default(State, "motd"); + false -> + ok + end, + + case add_sh_column(State, "sm") of + true -> + drop_index(State, "sm", "i_sm_sid"), + drop_index(State, "sm", "i_sm_username"), + drop_pkey(State, "sm"), + add_pkey(State, "sm", ["usec", "pid"]), + create_index(State, "sm", "i_sm_sh_username", ["server_host", "username"]), + drop_sh_default(State, "sm"); + false -> + ok + end, + + case add_sh_column(State, "push_session") of + true -> + drop_index(State, "push_session", "i_push_usn"), + drop_index(State, "push_session", "i_push_ut"), + create_unique_index(State, "push_session", "i_push_session_susn", ["server_host", "username", "service", "node"]), + create_index(State, "push_session", "i_push_session_sh_username_timestamp", ["server_host", "username", "timestamp"]), + drop_sh_default(State, "push_session"); + false -> + ok + end, + + case add_sh_column(State, "mix_pam") of + true -> + drop_index(State, "mix_pam", "i_mix_pam"), + drop_index(State, "mix_pam", "i_mix_pam_u"), + drop_index(State, "mix_pam", "i_mix_pam_us"), + create_unique_index(State, "mix_pam", "i_mix_pam", ["username", "server_host", "channel", "service"]), + drop_sh_default(State, "mix_pam"); + false -> + ok + end, + + case add_sh_column(State, "mqtt_pub") of + true -> + drop_index(State, "mqtt_pub", "i_mqtt_topic"), + create_unique_index(State, "mqtt_pub", "i_mqtt_topic_server", ["topic", "server_host"]), + drop_sh_default(State, "mqtt_pub"); + false -> + ok + end, + + ok. + +check_sh_column(#state{dbtype = mysql} = State, Table) -> + DB = ejabberd_option:sql_database(State#state.host), + sql_query( + State#state.host, + ["SELECT 1 FROM information_schema.columns ", + "WHERE table_name = '", Table, "' AND column_name = 'server_host' ", + "AND table_schema = '", (State#state.escape)(DB), "' ", + "GROUP BY table_name, column_name;"], false); +check_sh_column(State, Table) -> + DB = ejabberd_option:sql_database(State#state.host), + sql_query( + State#state.host, + ["SELECT 1 FROM information_schema.columns ", + "WHERE table_name = '", Table, "' AND column_name = 'server_host' ", + "AND table_catalog = '", (State#state.escape)(DB), "' ", + "GROUP BY table_name, column_name;"], false). + +add_sh_column(State, Table) -> + case check_sh_column(State, Table) of + true -> false; + false -> + do_add_sh_column(State, Table), + true + end. + +do_add_sh_column(#state{dbtype = pgsql} = State, Table) -> + sql_query( + State#state.host, + ["ALTER TABLE ", Table, " ADD COLUMN server_host text NOT NULL DEFAULT '", + (State#state.escape)(State#state.host), + "';"]); +do_add_sh_column(#state{dbtype = mssql} = State, Table) -> + sql_query( + State#state.host, + ["ALTER TABLE [", Table, "] ADD [server_host] varchar (250) NOT NULL ", + "CONSTRAINT [server_host_default] DEFAULT '", + (State#state.escape)(State#state.host), + "';"]); +do_add_sh_column(#state{dbtype = mysql} = State, Table) -> + sql_query( + State#state.host, + ["ALTER TABLE ", Table, " ADD COLUMN server_host varchar(191) NOT NULL DEFAULT '", + (State#state.escape)(State#state.host), + "';"]). + +drop_pkey(#state{dbtype = pgsql} = State, Table) -> + sql_query( + State#state.host, + ["ALTER TABLE ", Table, " DROP CONSTRAINT ", Table, "_pkey;"]); +drop_pkey(#state{dbtype = mssql} = State, Table) -> + sql_query( + State#state.host, + ["ALTER TABLE [", Table, "] DROP CONSTRAINT [", Table, "_PRIMARY];"]); +drop_pkey(#state{dbtype = mysql} = State, Table) -> + sql_query( + State#state.host, + ["ALTER TABLE ", Table, " DROP PRIMARY KEY;"]). + +add_pkey(#state{dbtype = pgsql} = State, Table, Cols) -> + Cols2 = lists:map(fun("type") -> "\"type\""; (V) -> V end, Cols), + SCols = string:join(Cols2, ", "), + sql_query( + State#state.host, + ["ALTER TABLE ", Table, " ADD PRIMARY KEY (", SCols, ");"]); +add_pkey(#state{dbtype = mssql} = State, Table, Cols) -> + SCols = string:join(Cols, "], ["), + sql_query( + State#state.host, + ["ALTER TABLE [", Table, "] ADD CONSTRAINT [", Table, "_PRIMARY] PRIMARY KEY CLUSTERED ([", SCols, "]) ", + "WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ", + "ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY];"]); +add_pkey(#state{dbtype = mysql} = State, Table, Cols) -> + Cols2 = [C ++ mysql_keylen(Table, C) || C <- Cols], + SCols = string:join(Cols2, ", "), + sql_query( + State#state.host, + ["ALTER TABLE ", Table, " ADD PRIMARY KEY (", SCols, ");"]). + +drop_sh_default(#state{dbtype = pgsql} = State, Table) -> + sql_query( + State#state.host, + ["ALTER TABLE ", Table, " ALTER COLUMN server_host DROP DEFAULT;"]); +drop_sh_default(#state{dbtype = mssql} = State, Table) -> + sql_query( + State#state.host, + ["ALTER TABLE [", Table, "] DROP CONSTRAINT [server_host_default];"]); +drop_sh_default(#state{dbtype = mysql} = State, Table) -> + sql_query( + State#state.host, + ["ALTER TABLE ", Table, " ALTER COLUMN server_host DROP DEFAULT;"]). + +check_index(#state{dbtype = pgsql} = State, Table, Index) -> + sql_query( + State#state.host, + ["SELECT 1 FROM pg_indexes WHERE tablename = '", Table, + "' AND indexname = '", Index, "';"], false); +check_index(#state{dbtype = mssql} = State, Table, Index) -> + sql_query( + State#state.host, + ["SELECT 1 FROM sys.tables t ", + "INNER JOIN sys.indexes i ON i.object_id = t.object_id ", + "WHERE i.index_id > 0 ", + "AND i.name = '", Index, "' ", + "AND t.name = '", Table, "';"], false); +check_index(#state{dbtype = mysql} = State, Table, Index) -> + DB = ejabberd_option:sql_database(State#state.host), + sql_query( + State#state.host, + ["SELECT 1 FROM information_schema.statistics ", + "WHERE table_name = '", Table, "' AND index_name = '", Index, "' ", + "AND table_schema = '", (State#state.escape)(DB), "' ", + "GROUP BY table_name, index_name;"], false). + +drop_index(State, Table, Index) -> + OldIndex = old_index_name(State#state.dbtype, Index), + case check_index(State, Table, OldIndex) of + true -> do_drop_index(State, Table, OldIndex); + false -> ok + end. + +do_drop_index(#state{dbtype = pgsql} = State, _Table, Index) -> + sql_query( + State#state.host, + ["DROP INDEX ", Index, ";"]); +do_drop_index(#state{dbtype = mssql} = State, Table, Index) -> + sql_query( + State#state.host, + ["DROP INDEX [", Index, "] ON [", Table, "];"]); +do_drop_index(#state{dbtype = mysql} = State, Table, Index) -> + sql_query( + State#state.host, + ["ALTER TABLE ", Table, " DROP INDEX ", Index, ";"]). + +create_unique_index(#state{dbtype = pgsql} = State, Table, Index, Cols) -> + SCols = string:join(Cols, ", "), + sql_query( + State#state.host, + ["CREATE UNIQUE INDEX ", Index, " ON ", Table, " USING btree (", + SCols, ");"]); +create_unique_index(#state{dbtype = mssql} = State, Table, "i_privacy_list_sh_username_name" = Index, Cols) -> + create_index(State, Table, Index, Cols); +create_unique_index(#state{dbtype = mssql} = State, Table, Index, Cols) -> + SCols = string:join(Cols, ", "), + sql_query( + State#state.host, + ["CREATE UNIQUE ", mssql_clustered(Index), "INDEX [", new_index_name(State#state.dbtype, Index), "] ", + "ON [", Table, "] (", SCols, ") ", + "WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON);"]); +create_unique_index(#state{dbtype = mysql} = State, Table, Index, Cols) -> + Cols2 = [C ++ mysql_keylen(Index, C) || C <- Cols], + SCols = string:join(Cols2, ", "), + sql_query( + State#state.host, + ["CREATE UNIQUE INDEX ", Index, " ON ", Table, "(", + SCols, ");"]). + +create_index(#state{dbtype = pgsql} = State, Table, Index, Cols) -> + NewIndex = new_index_name(State#state.dbtype, Index), + SCols = string:join(Cols, ", "), + sql_query( + State#state.host, + ["CREATE INDEX ", NewIndex, " ON ", Table, " USING btree (", + SCols, ");"]); +create_index(#state{dbtype = mssql} = State, Table, Index, Cols) -> + NewIndex = new_index_name(State#state.dbtype, Index), + SCols = string:join(Cols, ", "), + sql_query( + State#state.host, + ["CREATE INDEX [", NewIndex, "] ON [", Table, "] (", SCols, ") ", + "WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON);"]); +create_index(#state{dbtype = mysql} = State, Table, Index, Cols) -> + NewIndex = new_index_name(State#state.dbtype, Index), + Cols2 = [C ++ mysql_keylen(NewIndex, C) || C <- Cols], + SCols = string:join(Cols2, ", "), + sql_query( + State#state.host, + ["CREATE INDEX ", NewIndex, " ON ", Table, "(", + SCols, ");"]). + +old_index_name(mssql, "i_bare_peer") -> "archive_bare_peer"; +old_index_name(mssql, "i_peer") -> "archive_peer"; +old_index_name(mssql, "i_timestamp") -> "archive_timestamp"; +old_index_name(mssql, "i_username") -> "archive_username"; +old_index_name(mssql, "i_username_bare_peer") -> "archive_username_bare_peer"; +old_index_name(mssql, "i_username_peer") -> "archive_username_peer"; +old_index_name(mssql, "i_username_timestamp") -> "archive_username_timestamp"; +old_index_name(mssql, "i_push_usn") -> "i_push_usn"; +old_index_name(mssql, "i_push_ut") -> "i_push_ut"; +old_index_name(mssql, "pk_rosterg_user_jid") -> "rostergroups_username_jid"; +old_index_name(mssql, "i_rosteru_jid") -> "rosterusers_jid"; +old_index_name(mssql, "i_rosteru_username") -> "rosterusers_username"; +old_index_name(mssql, "i_rosteru_user_jid") -> "rosterusers_username_jid"; +old_index_name(mssql, "i_despool") -> "spool_username"; +old_index_name(mssql, "i_sr_user_jid_grp") -> "sr_user_jid_group"; +old_index_name(mssql, Index) -> string:substr(Index, 3); +old_index_name(_Type, Index) -> Index. + +new_index_name(mssql, "i_rosterg_sh_user_jid") -> "rostergroups_sh_username_jid"; +new_index_name(mssql, "i_rosteru_sh_jid") -> "rosterusers_sh_jid"; +new_index_name(mssql, "i_rosteru_sh_user_jid") -> "rosterusers_sh_username_jid"; +new_index_name(mssql, "i_sr_user_sh_jid_grp") -> "sr_user_sh_jid_group"; +new_index_name(mssql, Index) -> string:substr(Index, 3); +new_index_name(_Type, Index) -> Index. + +mssql_clustered("i_mix_pam") -> ""; +mssql_clustered("i_push_session_susn") -> ""; +mssql_clustered(_) -> "CLUSTERED ". + +mysql_keylen(_, "bare_peer") -> "(191)"; +mysql_keylen(_, "channel") -> "(191)"; +mysql_keylen(_, "domain") -> "(75)"; +mysql_keylen(_, "grp") -> "(191)"; %% in mysql*.sql this is text, not varchar(191) +mysql_keylen(_, "jid") -> "(75)"; +mysql_keylen(_, "lbday") -> "(191)"; +mysql_keylen(_, "lctry") -> "(191)"; +mysql_keylen(_, "lemail") -> "(191)"; +mysql_keylen(_, "lfamily") -> "(191)"; +mysql_keylen(_, "lfn") -> "(191)"; +mysql_keylen(_, "lgiven") -> "(191)"; +mysql_keylen(_, "llocality") -> "(191)"; +mysql_keylen(_, "lmiddle") -> "(191)"; +mysql_keylen(_, "lnickname") -> "(191)"; +mysql_keylen(_, "lorgname") -> "(191)"; +mysql_keylen(_, "lorgunit") -> "(191)"; +mysql_keylen(_, "lusername") -> "(191)"; +mysql_keylen(_, "name") -> "(75)"; +mysql_keylen(_, "namespace") -> "(191)"; +mysql_keylen(_, "node") -> "(75)"; +mysql_keylen(_, "peer") -> "(191)"; +mysql_keylen(_, "pid") -> "(75)"; +mysql_keylen(_, "server_host") -> "(191)"; +mysql_keylen(_, "service") -> "(191)"; +mysql_keylen(_, "topic") -> "(191)"; +mysql_keylen("i_privacy_list_sh_username_name", "username") -> "(75)"; +mysql_keylen("i_rosterg_sh_user_jid", "username") -> "(75)"; +mysql_keylen("i_rosteru_sh_user_jid", "username") -> "(75)"; +mysql_keylen(_, "username") -> "(191)"; +mysql_keylen(_, _) -> "". + +sql_query(Host, Query) -> + sql_query(Host, Query, true). + +sql_query(Host, Query, Log) -> + case Log of + true -> io:format("executing \"~ts\" on ~ts~n", [Query, Host]); + false -> ok + end, + case ejabberd_sql:sql_query(Host, Query) of + {selected, _Cols, []} -> + false; + {selected, _Cols, [_Rows]} -> + true; + {error, Error} -> + io:format("error: ~p~n", [Error]), + false; + _ -> + ok + end. + +mod_options(_) -> []. + +mod_doc() -> + #{desc => + ?T("This module can be used to update existing SQL database " + "from the default to the new schema. Check the section " + "_`database.md#default-and-new-schemas|Default and New Schemas`_ for details. " + "Please note that only MS SQL, MySQL, and PostgreSQL are supported. " + "When the module is loaded use _`update_sql`_ API.")}. diff --git a/src/mod_announce.erl b/src/mod_announce.erl index 0e7c9fa32..b382dc3a6 100644 --- a/src/mod_announce.erl +++ b/src/mod_announce.erl @@ -5,7 +5,7 @@ %%% Created : 11 Aug 2003 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -29,55 +29,84 @@ -module(mod_announce). -author('alexey@process-one.net'). +-behaviour(gen_server). -behaviour(gen_mod). --export([start/2, - init/0, - stop/1, - export/1, - import/1, - import/3, - announce/3, - send_motd/1, - disco_identity/5, - disco_features/5, - disco_items/5, - send_announcement_to_all/3, - announce_commands/4, - announce_items/4]). +-export([start/2, stop/1, reload/3, export/1, import_info/0, + import_start/2, import/5, announce/1, send_motd/1, disco_identity/5, + disco_features/5, disco_items/5, depends/2, + send_announcement_to_all/3, announce_commands/4, mod_doc/0, + announce_items/4, mod_opt_type/1, mod_options/1, clean_cache/1]). +-export([init/1, handle_call/3, handle_cast/2, + handle_info/2, terminate/2, code_change/3]). +-export([announce_all/1, + announce_all_hosts_all/1, + announce_online/1, + announce_all_hosts_online/1, + announce_motd/1, + announce_all_hosts_motd/1, + announce_motd_update/1, + announce_all_hosts_motd_update/1, + announce_motd_delete/1, + announce_all_hosts_motd_delete/1]). --include("ejabberd.hrl"). -include("logger.hrl"). --include("jlib.hrl"). --include("adhoc.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). +-include("mod_announce.hrl"). +-include("translate.hrl"). --record(motd, {server = <<"">> :: binary(), - packet = #xmlel{} :: xmlel()}). --record(motd_users, {us = {<<"">>, <<"">>} :: {binary(), binary()} | '$1', - dummy = [] :: [] | '_'}). +-callback init(binary(), gen_mod:opts()) -> any(). +-callback import(binary(), binary(), [binary()]) -> ok. +-callback set_motd_users(binary(), [{binary(), binary(), binary()}]) -> ok | {error, any()}. +-callback set_motd(binary(), xmlel()) -> ok | {error, any()}. +-callback delete_motd(binary()) -> ok | {error, any()}. +-callback get_motd(binary()) -> {ok, xmlel()} | error | {error, any()}. +-callback is_motd_user(binary(), binary()) -> {ok, boolean()} | {error, any()}. +-callback set_motd_user(binary(), binary()) -> ok | {error, any()}. +-callback use_cache(binary()) -> boolean(). +-callback cache_nodes(binary()) -> [node()]. --define(PROCNAME, ejabberd_announce). +-optional_callbacks([use_cache/1, cache_nodes/1]). + +-record(state, {host :: binary()}). -define(NS_ADMINL(Sub), [<<"http:">>, <<"jabber.org">>, <<"protocol">>, <<"admin">>, <>]). +-define(MOTD_CACHE, motd_cache). tokenize(Node) -> str:tokens(Node, <<"/#">>). +%%==================================================================== +%% gen_mod callbacks +%%==================================================================== start(Host, Opts) -> - case gen_mod:db_type(Opts) of - mnesia -> - mnesia:create_table(motd, - [{disc_copies, [node()]}, - {attributes, - record_info(fields, motd)}]), - mnesia:create_table(motd_users, - [{disc_copies, [node()]}, - {attributes, - record_info(fields, motd_users)}]), - update_tables(); - _ -> - ok + gen_mod:start_child(?MODULE, Host, Opts). + +stop(Host) -> + gen_mod:stop_child(?MODULE, Host). + +reload(Host, NewOpts, OldOpts) -> + NewMod = gen_mod:db_mod(NewOpts, ?MODULE), + OldMod = gen_mod:db_mod(OldOpts, ?MODULE), + if NewMod /= OldMod -> + NewMod:init(Host, NewOpts); + true -> + ok end, + init_cache(NewMod, Host, NewOpts). + +depends(_Host, _Opts) -> + [{mod_adhoc, hard}]. + +%%==================================================================== +%% gen_server callbacks +%%==================================================================== +init([Host|_]) -> + process_flag(trap_exit, true), + Opts = gen_mod:get_module_opts(Host, ?MODULE), + Mod = gen_mod:db_mod(Opts, ?MODULE), + Mod:init(Host, Opts), + init_cache(Mod, Host, Opts), ejabberd_hooks:add(local_send_to_resource_hook, Host, ?MODULE, announce, 50), ejabberd_hooks:add(disco_local_identity, Host, ?MODULE, disco_identity, 50), @@ -85,111 +114,98 @@ start(Host, Opts) -> ejabberd_hooks:add(disco_local_items, Host, ?MODULE, disco_items, 50), ejabberd_hooks:add(adhoc_local_items, Host, ?MODULE, announce_items, 50), ejabberd_hooks:add(adhoc_local_commands, Host, ?MODULE, announce_commands, 50), - ejabberd_hooks:add(user_available_hook, Host, + ejabberd_hooks:add(c2s_self_presence, Host, ?MODULE, send_motd, 50), - register(gen_mod:get_module_proc(Host, ?PROCNAME), - proc_lib:spawn(?MODULE, init, [])). + {ok, #state{host = Host}}. -init() -> - loop(). +handle_call(Request, From, State) -> + ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), + {noreply, State}. -loop() -> - receive - {announce_all, From, To, Packet} -> - announce_all(From, To, Packet), - loop(); - {announce_all_hosts_all, From, To, Packet} -> - announce_all_hosts_all(From, To, Packet), - loop(); - {announce_online, From, To, Packet} -> - announce_online(From, To, Packet), - loop(); - {announce_all_hosts_online, From, To, Packet} -> - announce_all_hosts_online(From, To, Packet), - loop(); - {announce_motd, From, To, Packet} -> - announce_motd(From, To, Packet), - loop(); - {announce_all_hosts_motd, From, To, Packet} -> - announce_all_hosts_motd(From, To, Packet), - loop(); - {announce_motd_update, From, To, Packet} -> - announce_motd_update(From, To, Packet), - loop(); - {announce_all_hosts_motd_update, From, To, Packet} -> - announce_all_hosts_motd_update(From, To, Packet), - loop(); - {announce_motd_delete, From, To, Packet} -> - announce_motd_delete(From, To, Packet), - loop(); - {announce_all_hosts_motd_delete, From, To, Packet} -> - announce_all_hosts_motd_delete(From, To, Packet), - loop(); - _ -> - loop() - end. +handle_cast({F, #message{from = From, to = To} = Pkt}, State) when is_atom(F) -> + LServer = To#jid.lserver, + Host = case F of + announce_all -> LServer; + announce_all_hosts_all -> global; + announce_online -> LServer; + announce_all_hosts_online -> global; + announce_motd -> LServer; + announce_all_hosts_motd -> global; + announce_motd_update -> LServer; + announce_all_hosts_motd_update -> global; + announce_motd_delete -> LServer; + announce_all_hosts_motd_delete -> global + end, + Access = get_access(Host), + case acl:match_rule(Host, Access, From) of + deny -> + route_forbidden_error(Pkt); + allow -> + ?MODULE:F(Pkt) + end, + {noreply, State}; +handle_cast(Msg, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Msg]), + {noreply, State}. -stop(Host) -> + +handle_info(Info, State) -> + ?WARNING_MSG("Unexpected info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, #state{host = Host}) -> ejabberd_hooks:delete(adhoc_local_commands, Host, ?MODULE, announce_commands, 50), ejabberd_hooks:delete(adhoc_local_items, Host, ?MODULE, announce_items, 50), ejabberd_hooks:delete(disco_local_identity, Host, ?MODULE, disco_identity, 50), ejabberd_hooks:delete(disco_local_features, Host, ?MODULE, disco_features, 50), ejabberd_hooks:delete(disco_local_items, Host, ?MODULE, disco_items, 50), - ejabberd_hooks:delete(local_send_to_resource_hook, Host, - ?MODULE, announce, 50), - ejabberd_hooks:delete(user_available_hook, Host, - ?MODULE, send_motd, 50), - Proc = gen_mod:get_module_proc(Host, ?PROCNAME), - exit(whereis(Proc), stop), - {wait, Proc}. + ejabberd_hooks:delete(local_send_to_resource_hook, Host, ?MODULE, announce, 50), + ejabberd_hooks:delete(c2s_self_presence, Host, ?MODULE, send_motd, 50). + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. %% Announcing via messages to a custom resource -announce(From, #jid{luser = <<>>} = To, #xmlel{name = <<"message">>} = Packet) -> - Proc = gen_mod:get_module_proc(To#jid.lserver, ?PROCNAME), - case To#jid.lresource of - <<"announce/all">> -> - Proc ! {announce_all, From, To, Packet}, - stop; - <<"announce/all-hosts/all">> -> - Proc ! {announce_all_hosts_all, From, To, Packet}, - stop; - <<"announce/online">> -> - Proc ! {announce_online, From, To, Packet}, - stop; - <<"announce/all-hosts/online">> -> - Proc ! {announce_all_hosts_online, From, To, Packet}, - stop; - <<"announce/motd">> -> - Proc ! {announce_motd, From, To, Packet}, - stop; - <<"announce/all-hosts/motd">> -> - Proc ! {announce_all_hosts_motd, From, To, Packet}, - stop; - <<"announce/motd/update">> -> - Proc ! {announce_motd_update, From, To, Packet}, - stop; - <<"announce/all-hosts/motd/update">> -> - Proc ! {announce_all_hosts_motd_update, From, To, Packet}, - stop; - <<"announce/motd/delete">> -> - Proc ! {announce_motd_delete, From, To, Packet}, - stop; - <<"announce/all-hosts/motd/delete">> -> - Proc ! {announce_all_hosts_motd_delete, From, To, Packet}, - stop; - _ -> - ok +-spec announce(stanza()) -> ok | stop. +announce(#message{to = #jid{luser = <<>>} = To} = Packet) -> + Proc = gen_mod:get_module_proc(To#jid.lserver, ?MODULE), + Res = case To#jid.lresource of + <<"announce/all">> -> + gen_server:cast(Proc, {announce_all, Packet}); + <<"announce/all-hosts/all">> -> + gen_server:cast(Proc, {announce_all_hosts_all, Packet}); + <<"announce/online">> -> + gen_server:cast(Proc, {announce_online, Packet}); + <<"announce/all-hosts/online">> -> + gen_server:cast(Proc, {announce_all_hosts_online, Packet}); + <<"announce/motd">> -> + gen_server:cast(Proc, {announce_motd, Packet}); + <<"announce/all-hosts/motd">> -> + gen_server:cast(Proc, {announce_all_hosts_motd, Packet}); + <<"announce/motd/update">> -> + gen_server:cast(Proc, {announce_motd_update, Packet}); + <<"announce/all-hosts/motd/update">> -> + gen_server:cast(Proc, {announce_all_hosts_motd_update, Packet}); + <<"announce/motd/delete">> -> + gen_server:cast(Proc, {announce_motd_delete, Packet}); + <<"announce/all-hosts/motd/delete">> -> + gen_server:cast(Proc, {announce_all_hosts_motd_delete, Packet}); + _ -> + undefined + end, + case Res of + ok -> stop; + _ -> ok end; -announce(_From, _To, _Packet) -> +announce(_Packet) -> ok. %%------------------------------------------------------------------------- %% Announcing via ad-hoc commands -define(INFO_COMMAND(Lang, Node), - [#xmlel{name = <<"identity">>, - attrs = [{<<"category">>, <<"automation">>}, - {<<"type">>, <<"command-node">>}, - {<<"name">>, get_title(Lang, Node)}]}]). + [#identity{category = <<"automation">>, + type = <<"command-node">>, + name = get_title(Lang, Node)}]). disco_identity(Acc, _From, _To, Node, Lang) -> LNode = tokenize(Node), @@ -220,15 +236,15 @@ disco_identity(Acc, _From, _To, Node, Lang) -> %%------------------------------------------------------------------------- --define(INFO_RESULT(Allow, Feats), +-define(INFO_RESULT(Allow, Feats, Lang), case Allow of deny -> - {error, ?ERR_FORBIDDEN}; + {error, xmpp:err_forbidden(?T("Access denied by service policy"), Lang)}; allow -> {result, Feats} end). -disco_features(Acc, From, #jid{lserver = LServer} = _To, <<"announce">>, _Lang) -> +disco_features(Acc, From, #jid{lserver = LServer} = _To, <<"announce">>, Lang) -> case gen_mod:is_loaded(LServer, mod_adhoc) of false -> Acc; @@ -238,13 +254,14 @@ disco_features(Acc, From, #jid{lserver = LServer} = _To, <<"announce">>, _Lang) case {acl:match_rule(LServer, Access1, From), acl:match_rule(global, Access2, From)} of {deny, deny} -> - {error, ?ERR_FORBIDDEN}; + Txt = ?T("Access denied by service policy"), + {error, xmpp:err_forbidden(Txt, Lang)}; _ -> {result, []} end end; -disco_features(Acc, From, #jid{lserver = LServer} = _To, Node, _Lang) -> +disco_features(Acc, From, #jid{lserver = LServer} = _To, Node, Lang) -> case gen_mod:is_loaded(LServer, mod_adhoc) of false -> Acc; @@ -255,25 +272,25 @@ disco_features(Acc, From, #jid{lserver = LServer} = _To, Node, _Lang) -> AllowGlobal = acl:match_rule(global, AccessGlobal, From), case Node of ?NS_ADMIN_ANNOUNCE -> - ?INFO_RESULT(Allow, [?NS_COMMANDS]); + ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMIN_ANNOUNCE_ALL -> - ?INFO_RESULT(Allow, [?NS_COMMANDS]); + ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMIN_SET_MOTD -> - ?INFO_RESULT(Allow, [?NS_COMMANDS]); + ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMIN_EDIT_MOTD -> - ?INFO_RESULT(Allow, [?NS_COMMANDS]); + ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMIN_DELETE_MOTD -> - ?INFO_RESULT(Allow, [?NS_COMMANDS]); + ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMIN_ANNOUNCE_ALLHOSTS -> - ?INFO_RESULT(AllowGlobal, [?NS_COMMANDS]); + ?INFO_RESULT(AllowGlobal, [?NS_COMMANDS], Lang); ?NS_ADMIN_ANNOUNCE_ALL_ALLHOSTS -> - ?INFO_RESULT(AllowGlobal, [?NS_COMMANDS]); + ?INFO_RESULT(AllowGlobal, [?NS_COMMANDS], Lang); ?NS_ADMIN_SET_MOTD_ALLHOSTS -> - ?INFO_RESULT(AllowGlobal, [?NS_COMMANDS]); + ?INFO_RESULT(AllowGlobal, [?NS_COMMANDS], Lang); ?NS_ADMIN_EDIT_MOTD_ALLHOSTS -> - ?INFO_RESULT(AllowGlobal, [?NS_COMMANDS]); + ?INFO_RESULT(AllowGlobal, [?NS_COMMANDS], Lang); ?NS_ADMIN_DELETE_MOTD_ALLHOSTS -> - ?INFO_RESULT(AllowGlobal, [?NS_COMMANDS]); + ?INFO_RESULT(AllowGlobal, [?NS_COMMANDS], Lang); _ -> Acc end @@ -281,26 +298,19 @@ disco_features(Acc, From, #jid{lserver = LServer} = _To, Node, _Lang) -> %%------------------------------------------------------------------------- -define(NODE_TO_ITEM(Lang, Server, Node), -( - #xmlel{ - name = <<"item">>, - attrs = [ - {<<"jid">>, Server}, - {<<"node">>, Node}, - {<<"name">>, get_title(Lang, Node)} - ] - } -)). + #disco_item{jid = jid:make(Server), + node = Node, + name = get_title(Lang, Node)}). --define(ITEMS_RESULT(Allow, Items), +-define(ITEMS_RESULT(Allow, Items, Lang), case Allow of deny -> - {error, ?ERR_FORBIDDEN}; + {error, xmpp:err_forbidden(?T("Access denied by service policy"), Lang)}; allow -> {result, Items} end). -disco_items(Acc, From, #jid{lserver = LServer, server = Server} = _To, <<>>, Lang) -> +disco_items(Acc, From, #jid{lserver = LServer, server = Server} = _To, <<"">>, Lang) -> case gen_mod:is_loaded(LServer, mod_adhoc) of false -> Acc; @@ -329,7 +339,7 @@ disco_items(Acc, From, #jid{lserver = LServer} = To, <<"announce">>, Lang) -> announce_items(Acc, From, To, Lang) end; -disco_items(Acc, From, #jid{lserver = LServer} = _To, Node, _Lang) -> +disco_items(Acc, From, #jid{lserver = LServer} = _To, Node, Lang) -> case gen_mod:is_loaded(LServer, mod_adhoc) of false -> Acc; @@ -340,32 +350,35 @@ disco_items(Acc, From, #jid{lserver = LServer} = _To, Node, _Lang) -> AllowGlobal = acl:match_rule(global, AccessGlobal, From), case Node of ?NS_ADMIN_ANNOUNCE -> - ?ITEMS_RESULT(Allow, []); + ?ITEMS_RESULT(Allow, [], Lang); ?NS_ADMIN_ANNOUNCE_ALL -> - ?ITEMS_RESULT(Allow, []); + ?ITEMS_RESULT(Allow, [], Lang); ?NS_ADMIN_SET_MOTD -> - ?ITEMS_RESULT(Allow, []); + ?ITEMS_RESULT(Allow, [], Lang); ?NS_ADMIN_EDIT_MOTD -> - ?ITEMS_RESULT(Allow, []); + ?ITEMS_RESULT(Allow, [], Lang); ?NS_ADMIN_DELETE_MOTD -> - ?ITEMS_RESULT(Allow, []); + ?ITEMS_RESULT(Allow, [], Lang); ?NS_ADMIN_ANNOUNCE_ALLHOSTS -> - ?ITEMS_RESULT(AllowGlobal, []); + ?ITEMS_RESULT(AllowGlobal, [], Lang); ?NS_ADMIN_ANNOUNCE_ALL_ALLHOSTS -> - ?ITEMS_RESULT(AllowGlobal, []); + ?ITEMS_RESULT(AllowGlobal, [], Lang); ?NS_ADMIN_SET_MOTD_ALLHOSTS -> - ?ITEMS_RESULT(AllowGlobal, []); + ?ITEMS_RESULT(AllowGlobal, [], Lang); ?NS_ADMIN_EDIT_MOTD_ALLHOSTS -> - ?ITEMS_RESULT(AllowGlobal, []); + ?ITEMS_RESULT(AllowGlobal, [], Lang); ?NS_ADMIN_DELETE_MOTD_ALLHOSTS -> - ?ITEMS_RESULT(AllowGlobal, []); + ?ITEMS_RESULT(AllowGlobal, [], Lang); _ -> Acc end end. %%------------------------------------------------------------------------- - +-spec announce_items(empty | {error, stanza_error()} | {result, [disco_item()]}, + jid(), jid(), binary()) -> {error, stanza_error()} | + {result, [disco_item()]} | + empty. announce_items(Acc, From, #jid{lserver = LServer, server = Server} = _To, Lang) -> Access1 = get_access(LServer), Nodes1 = case acl:match_rule(LServer, Access1, From) of @@ -405,14 +418,16 @@ announce_items(Acc, From, #jid{lserver = LServer, server = Server} = _To, Lang) commands_result(Allow, From, To, Request) -> case Allow of deny -> - {error, ?ERR_FORBIDDEN}; + Lang = Request#adhoc_command.lang, + {error, xmpp:err_forbidden(?T("Access denied by service policy"), Lang)}; allow -> announce_commands(From, To, Request) end. - +-spec announce_commands(empty | adhoc_command(), jid(), jid(), adhoc_command()) -> + adhoc_command() | {error, stanza_error()}. announce_commands(Acc, From, #jid{lserver = LServer} = To, - #adhoc_request{ node = Node} = Request) -> + #adhoc_command{node = Node} = Request) -> LNode = tokenize(Node), F = fun() -> Access = get_access(global), @@ -451,111 +466,69 @@ announce_commands(Acc, From, #jid{lserver = LServer} = To, %%------------------------------------------------------------------------- announce_commands(From, To, - #adhoc_request{lang = Lang, + #adhoc_command{lang = Lang, node = Node, - action = Action, - xdata = XData} = Request) -> - %% If the "action" attribute is not present, it is - %% understood as "execute". If there was no - %% element in the first response (which there isn't in our - %% case), "execute" and "complete" are equivalent. - ActionIsExecute = lists:member(Action, [<<>>, <<"execute">>, <<"complete">>]), - if Action == <<"cancel">> -> + sid = SID, + xdata = XData, + action = Action} = Request) -> + if Action == cancel -> %% User cancels request - adhoc:produce_response(Request, #adhoc_response{status = canceled}); - XData == false, ActionIsExecute -> + #adhoc_command{status = canceled, lang = Lang, node = Node, + sid = SID}; + XData == undefined andalso Action == execute -> %% User requests form - Elements = generate_adhoc_form(Lang, Node, To#jid.lserver), - adhoc:produce_response(Request, - #adhoc_response{status = executing,elements = [Elements]}); - XData /= false, ActionIsExecute -> - %% User returns form. - case jlib:parse_xdata_submit(XData) of - invalid -> - {error, ?ERR_BAD_REQUEST}; - Fields -> - handle_adhoc_form(From, To, Request, Fields) + Form = generate_adhoc_form(Lang, Node, To#jid.lserver), + xmpp_util:make_adhoc_response( + #adhoc_command{status = executing, lang = Lang, node = Node, + sid = SID, xdata = Form}); + XData /= undefined andalso (Action == execute orelse Action == complete) -> + case handle_adhoc_form(From, To, Request) of + ok -> + #adhoc_command{lang = Lang, node = Node, sid = SID, + status = completed}; + {error, _} = Err -> + Err end; true -> - {error, ?ERR_BAD_REQUEST} + Txt = ?T("Unexpected action"), + {error, xmpp:err_bad_request(Txt, Lang)} end. --define(VVALUE(Val), -( - #xmlel{ - name = <<"value">>, - children = [{xmlcdata, Val}] - } -)). - --define(TVFIELD(Type, Var, Val), -( - #xmlel{ - name = <<"field">>, - attrs = [{<<"type">>, Type}, {<<"var">>, Var}], - children = vvaluel(Val) - } -)). - --define(HFIELD(), ?TVFIELD(<<"hidden">>, <<"FORM_TYPE">>, ?NS_ADMIN)). - vvaluel(Val) -> case Val of <<>> -> []; - _ -> [?VVALUE(Val)] + _ -> [Val] end. generate_adhoc_form(Lang, Node, ServerHost) -> LNode = tokenize(Node), - {OldSubject, OldBody} = if (LNode == ?NS_ADMINL("edit-motd")) + {OldSubject, OldBody} = if (LNode == ?NS_ADMINL("edit-motd")) or (LNode == ?NS_ADMINL("edit-motd-allhosts")) -> get_stored_motd(ServerHost); - true -> + true -> {<<>>, <<>>} end, - #xmlel{ - name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_XDATA}, {<<"type">>, <<"form">>}], - children = [ - ?HFIELD(), - #xmlel{name = <<"title">>, children = [{xmlcdata, get_title(Lang, Node)}]} - ] - ++ - if (LNode == ?NS_ADMINL("delete-motd")) - or (LNode == ?NS_ADMINL("delete-motd-allhosts")) -> - [#xmlel{ - name = <<"field">>, - attrs = [ - {<<"var">>, <<"confirm">>}, - {<<"type">>, <<"boolean">>}, - {<<"label">>, - translate:translate(Lang, <<"Really delete message of the day?">>)} - ], - children = [ - #xmlel{name = <<"value">>, children = [{xmlcdata, <<"true">>}]} - ] - } - ]; - true -> - [#xmlel{ - name = <<"field">>, - attrs = [ - {<<"var">>, <<"subject">>}, - {<<"type">>, <<"text-single">>}, - {<<"label">>, translate:translate(Lang, <<"Subject">>)}], - children = vvaluel(OldSubject) - }, - #xmlel{ - name = <<"field">>, - attrs = [ - {<<"var">>, <<"body">>}, - {<<"type">>, <<"text-multi">>}, - {<<"label">>, translate:translate(Lang, <<"Message body">>)}], - children = vvaluel(OldBody) - } - ] - - end}. + Fs = if (LNode == ?NS_ADMINL("delete-motd")) + or (LNode == ?NS_ADMINL("delete-motd-allhosts")) -> + [#xdata_field{type = boolean, + var = <<"confirm">>, + label = translate:translate( + Lang, ?T("Really delete message of the day?")), + values = [<<"true">>]}]; + true -> + [#xdata_field{type = 'text-single', + var = <<"subject">>, + label = translate:translate(Lang, ?T("Subject")), + values = vvaluel(OldSubject)}, + #xdata_field{type = 'text-multi', + var = <<"body">>, + label = translate:translate(Lang, ?T("Message body")), + values = vvaluel(OldBody)}] + end, + #xdata{type = form, + title = get_title(Lang, Node), + fields = [#xdata_field{type = hidden, var = <<"FORM_TYPE">>, + values = [?NS_ADMIN]}|Fs]}. join_lines([]) -> <<>>; @@ -568,587 +541,446 @@ join_lines([], Acc) -> iolist_to_binary(lists:reverse(tl(Acc))). handle_adhoc_form(From, #jid{lserver = LServer} = To, - #adhoc_request{lang = Lang, - node = Node, - sessionid = SessionID}, - Fields) -> - Confirm = case lists:keysearch(<<"confirm">>, 1, Fields) of - {value, {<<"confirm">>, [<<"true">>]}} -> - true; - {value, {<<"confirm">>, [<<"1">>]}} -> - true; - _ -> - false + #adhoc_command{lang = Lang, node = Node, + xdata = XData}) -> + Confirm = case xmpp_util:get_xdata_values(<<"confirm">>, XData) of + [<<"true">>] -> true; + [<<"1">>] -> true; + _ -> false end, - Subject = case lists:keysearch(<<"subject">>, 1, Fields) of - {value, {<<"subject">>, SubjectLines}} -> - %% There really shouldn't be more than one - %% subject line, but can we stop them? - join_lines(SubjectLines); - _ -> - <<>> - end, - Body = case lists:keysearch(<<"body">>, 1, Fields) of - {value, {<<"body">>, BodyLines}} -> - join_lines(BodyLines); - _ -> - <<>> - end, - Response = #adhoc_response{lang = Lang, - node = Node, - sessionid = SessionID, - status = completed}, - Packet = #xmlel{ - name = <<"message">>, - attrs = [{<<"type">>, <<"headline">>}], - children = if Subject /= <<>> -> - [#xmlel{name = <<"subject">>, children = [{xmlcdata, Subject}]}]; - true -> - [] - end - ++ - if Body /= <<>> -> - [#xmlel{name = <<"body">>, children = [{xmlcdata, Body}]}]; - true -> - [] - end - }, - Proc = gen_mod:get_module_proc(LServer, ?PROCNAME), + Subject = join_lines(xmpp_util:get_xdata_values(<<"subject">>, XData)), + Body = join_lines(xmpp_util:get_xdata_values(<<"body">>, XData)), + Packet = #message{from = From, + to = To, + type = headline, + body = xmpp:mk_text(Body), + subject = xmpp:mk_text(Subject)}, + Proc = gen_mod:get_module_proc(LServer, ?MODULE), case {Node, Body} of {?NS_ADMIN_DELETE_MOTD, _} -> if Confirm -> - Proc ! {announce_motd_delete, From, To, Packet}, - adhoc:produce_response(Response); + gen_server:cast(Proc, {announce_motd_delete, Packet}); true -> - adhoc:produce_response(Response) + ok end; {?NS_ADMIN_DELETE_MOTD_ALLHOSTS, _} -> if Confirm -> - Proc ! {announce_all_hosts_motd_delete, From, To, Packet}, - adhoc:produce_response(Response); + gen_server:cast(Proc, {announce_all_hosts_motd_delete, Packet}); true -> - adhoc:produce_response(Response) + ok end; {_, <<>>} -> %% An announce message with no body is definitely an operator error. %% Throw an error and give him/her a chance to send message again. - {error, ?ERRT_NOT_ACCEPTABLE(Lang, - <<"No body provided for announce message">>)}; - %% Now send the packet to ?PROCNAME. + {error, xmpp:err_not_acceptable( + ?T("No body provided for announce message"), Lang)}; + %% Now send the packet to ?MODULE. %% We don't use direct announce_* functions because it %% leads to large delay in response and queries processing {?NS_ADMIN_ANNOUNCE, _} -> - Proc ! {announce_online, From, To, Packet}, - adhoc:produce_response(Response); - {?NS_ADMIN_ANNOUNCE_ALLHOSTS, _} -> - Proc ! {announce_all_hosts_online, From, To, Packet}, - adhoc:produce_response(Response); + gen_server:cast(Proc, {announce_online, Packet}); + {?NS_ADMIN_ANNOUNCE_ALLHOSTS, _} -> + gen_server:cast(Proc, {announce_all_hosts_online, Packet}); {?NS_ADMIN_ANNOUNCE_ALL, _} -> - Proc ! {announce_all, From, To, Packet}, - adhoc:produce_response(Response); - {?NS_ADMIN_ANNOUNCE_ALL_ALLHOSTS, _} -> - Proc ! {announce_all_hosts_all, From, To, Packet}, - adhoc:produce_response(Response); + gen_server:cast(Proc, {announce_all, Packet}); + {?NS_ADMIN_ANNOUNCE_ALL_ALLHOSTS, _} -> + gen_server:cast(Proc, {announce_all_hosts_all, Packet}); {?NS_ADMIN_SET_MOTD, _} -> - Proc ! {announce_motd, From, To, Packet}, - adhoc:produce_response(Response); - {?NS_ADMIN_SET_MOTD_ALLHOSTS, _} -> - Proc ! {announce_all_hosts_motd, From, To, Packet}, - adhoc:produce_response(Response); + gen_server:cast(Proc, {announce_motd, Packet}); + {?NS_ADMIN_SET_MOTD_ALLHOSTS, _} -> + gen_server:cast(Proc, {announce_all_hosts_motd, Packet}); {?NS_ADMIN_EDIT_MOTD, _} -> - Proc ! {announce_motd_update, From, To, Packet}, - adhoc:produce_response(Response); - {?NS_ADMIN_EDIT_MOTD_ALLHOSTS, _} -> - Proc ! {announce_all_hosts_motd_update, From, To, Packet}, - adhoc:produce_response(Response); - _ -> + gen_server:cast(Proc, {announce_motd_update, Packet}); + {?NS_ADMIN_EDIT_MOTD_ALLHOSTS, _} -> + gen_server:cast(Proc, {announce_all_hosts_motd_update, Packet}); + Junk -> %% This can't happen, as we haven't registered any other %% command nodes. - {error, ?ERR_INTERNAL_SERVER_ERROR} + ?ERROR_MSG("Unexpected node/body = ~p", [Junk]), + {error, xmpp:err_internal_server_error()} end. get_title(Lang, <<"announce">>) -> - translate:translate(Lang, <<"Announcements">>); + translate:translate(Lang, ?T("Announcements")); get_title(Lang, ?NS_ADMIN_ANNOUNCE_ALL) -> - translate:translate(Lang, <<"Send announcement to all users">>); + translate:translate(Lang, ?T("Send announcement to all users")); get_title(Lang, ?NS_ADMIN_ANNOUNCE_ALL_ALLHOSTS) -> - translate:translate(Lang, <<"Send announcement to all users on all hosts">>); + translate:translate(Lang, ?T("Send announcement to all users on all hosts")); get_title(Lang, ?NS_ADMIN_ANNOUNCE) -> - translate:translate(Lang, <<"Send announcement to all online users">>); + translate:translate(Lang, ?T("Send announcement to all online users")); get_title(Lang, ?NS_ADMIN_ANNOUNCE_ALLHOSTS) -> - translate:translate(Lang, <<"Send announcement to all online users on all hosts">>); + translate:translate(Lang, ?T("Send announcement to all online users on all hosts")); get_title(Lang, ?NS_ADMIN_SET_MOTD) -> - translate:translate(Lang, <<"Set message of the day and send to online users">>); + translate:translate(Lang, ?T("Set message of the day and send to online users")); get_title(Lang, ?NS_ADMIN_SET_MOTD_ALLHOSTS) -> - translate:translate(Lang, <<"Set message of the day on all hosts and send to online users">>); + translate:translate(Lang, ?T("Set message of the day on all hosts and send to online users")); get_title(Lang, ?NS_ADMIN_EDIT_MOTD) -> - translate:translate(Lang, <<"Update message of the day (don't send)">>); + translate:translate(Lang, ?T("Update message of the day (don't send)")); get_title(Lang, ?NS_ADMIN_EDIT_MOTD_ALLHOSTS) -> - translate:translate(Lang, <<"Update message of the day on all hosts (don't send)">>); + translate:translate(Lang, ?T("Update message of the day on all hosts (don't send)")); get_title(Lang, ?NS_ADMIN_DELETE_MOTD) -> - translate:translate(Lang, <<"Delete message of the day">>); + translate:translate(Lang, ?T("Delete message of the day")); get_title(Lang, ?NS_ADMIN_DELETE_MOTD_ALLHOSTS) -> - translate:translate(Lang, <<"Delete message of the day on all hosts">>). + translate:translate(Lang, ?T("Delete message of the day on all hosts")). %%------------------------------------------------------------------------- -announce_all(From, To, Packet) -> - Host = To#jid.lserver, - Access = get_access(Host), - case acl:match_rule(Host, Access, From) of - deny -> - Err = jlib:make_error_reply(Packet, ?ERR_FORBIDDEN), - ejabberd_router:route(To, From, Err); - allow -> - Local = jlib:make_jid(<<>>, To#jid.server, <<>>), - lists:foreach( - fun({User, Server}) -> - Dest = jlib:make_jid(User, Server, <<>>), - ejabberd_router:route(Local, Dest, Packet) - end, ejabberd_auth:get_vh_registered_users(Host)) - end. +announce_all(#message{to = To} = Packet) -> + Local = jid:make(To#jid.server), + lists:foreach( + fun({User, Server}) -> + Dest = jid:make(User, Server), + ejabberd_router:route( + xmpp:set_from_to(add_store_hint(Packet), Local, Dest)) + end, ejabberd_auth:get_users(To#jid.lserver)). -announce_all_hosts_all(From, To, Packet) -> - Access = get_access(global), - case acl:match_rule(global, Access, From) of - deny -> - Err = jlib:make_error_reply(Packet, ?ERR_FORBIDDEN), - ejabberd_router:route(To, From, Err); - allow -> - Local = jlib:make_jid(<<>>, To#jid.server, <<>>), - lists:foreach( - fun({User, Server}) -> - Dest = jlib:make_jid(User, Server, <<>>), - ejabberd_router:route(Local, Dest, Packet) - end, ejabberd_auth:dirty_get_registered_users()) - end. +announce_all_hosts_all(#message{to = To} = Packet) -> + Local = jid:make(To#jid.server), + lists:foreach( + fun({User, Server}) -> + Dest = jid:make(User, Server), + ejabberd_router:route( + xmpp:set_from_to(add_store_hint(Packet), Local, Dest)) + end, ejabberd_auth:get_users()). -announce_online(From, To, Packet) -> - Host = To#jid.lserver, - Access = get_access(Host), - case acl:match_rule(Host, Access, From) of - deny -> - Err = jlib:make_error_reply(Packet, ?ERR_FORBIDDEN), - ejabberd_router:route(To, From, Err); - allow -> - announce_online1(ejabberd_sm:get_vh_session_list(Host), - To#jid.server, - Packet) - end. +announce_online(#message{to = To} = Packet) -> + announce_online1(ejabberd_sm:get_vh_session_list(To#jid.lserver), + To#jid.server, Packet). -announce_all_hosts_online(From, To, Packet) -> - Access = get_access(global), - case acl:match_rule(global, Access, From) of - deny -> - Err = jlib:make_error_reply(Packet, ?ERR_FORBIDDEN), - ejabberd_router:route(To, From, Err); - allow -> - announce_online1(ejabberd_sm:dirty_get_sessions_list(), - To#jid.server, - Packet) - end. +announce_all_hosts_online(#message{to = To} = Packet) -> + announce_online1(ejabberd_sm:dirty_get_sessions_list(), + To#jid.server, Packet). announce_online1(Sessions, Server, Packet) -> - Local = jlib:make_jid(<<>>, Server, <<>>), + Local = jid:make(Server), lists:foreach( fun({U, S, R}) -> - Dest = jlib:make_jid(U, S, R), - ejabberd_router:route(Local, Dest, Packet) + Dest = jid:make(U, S, R), + ejabberd_router:route(xmpp:set_from_to(Packet, Local, Dest)) end, Sessions). -announce_motd(From, To, Packet) -> - Host = To#jid.lserver, - Access = get_access(Host), - case acl:match_rule(Host, Access, From) of - deny -> - Err = jlib:make_error_reply(Packet, ?ERR_FORBIDDEN), - ejabberd_router:route(To, From, Err); - allow -> - announce_motd(Host, Packet) - end. +announce_motd(#message{to = To} = Packet) -> + announce_motd(To#jid.lserver, Packet). -announce_all_hosts_motd(From, To, Packet) -> - Access = get_access(global), - case acl:match_rule(global, Access, From) of - deny -> - Err = jlib:make_error_reply(Packet, ?ERR_FORBIDDEN), - ejabberd_router:route(To, From, Err); - allow -> - Hosts = ?MYHOSTS, - [announce_motd(Host, Packet) || Host <- Hosts] - end. +announce_all_hosts_motd(Packet) -> + Hosts = ejabberd_option:hosts(), + [announce_motd(Host, Packet) || Host <- Hosts]. announce_motd(Host, Packet) -> - LServer = jlib:nameprep(Host), + LServer = jid:nameprep(Host), announce_motd_update(LServer, Packet), Sessions = ejabberd_sm:get_vh_session_list(LServer), announce_online1(Sessions, LServer, Packet), - case gen_mod:db_type(LServer, ?MODULE) of - mnesia -> - F = fun() -> - lists:foreach( - fun({U, S, _R}) -> - mnesia:write(#motd_users{us = {U, S}}) - end, Sessions) - end, - mnesia:transaction(F); - riak -> - try - lists:foreach( - fun({U, S, _R}) -> - ok = ejabberd_riak:put(#motd_users{us = {U, S}}, - motd_users_schema(), - [{'2i', [{<<"server">>, S}]}]) - end, Sessions), - {atomic, ok} - catch _:{badmatch, Err} -> - {atomic, Err} - end; - odbc -> - F = fun() -> - lists:foreach( - fun({U, _S, _R}) -> - Username = ejabberd_odbc:escape(U), - odbc_queries:update_t( - <<"motd">>, - [<<"username">>, <<"xml">>], - [Username, <<"">>], - [<<"username='">>, Username, <<"'">>]) - end, Sessions) - end, - ejabberd_odbc:sql_transaction(LServer, F) - end. + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:set_motd_users(LServer, Sessions). -announce_motd_update(From, To, Packet) -> - Host = To#jid.lserver, - Access = get_access(Host), - case acl:match_rule(Host, Access, From) of - deny -> - Err = jlib:make_error_reply(Packet, ?ERR_FORBIDDEN), - ejabberd_router:route(To, From, Err); - allow -> - announce_motd_update(Host, Packet) - end. +announce_motd_update(#message{to = To} = Packet) -> + announce_motd_update(To#jid.lserver, Packet). -announce_all_hosts_motd_update(From, To, Packet) -> - Access = get_access(global), - case acl:match_rule(global, Access, From) of - deny -> - Err = jlib:make_error_reply(Packet, ?ERR_FORBIDDEN), - ejabberd_router:route(To, From, Err); - allow -> - Hosts = ?MYHOSTS, - [announce_motd_update(Host, Packet) || Host <- Hosts] - end. +announce_all_hosts_motd_update(Packet) -> + Hosts = ejabberd_option:hosts(), + [announce_motd_update(Host, Packet) || Host <- Hosts]. announce_motd_update(LServer, Packet) -> - announce_motd_delete(LServer), - case gen_mod:db_type(LServer, ?MODULE) of - mnesia -> - F = fun() -> - mnesia:write(#motd{server = LServer, packet = Packet}) - end, - mnesia:transaction(F); - riak -> - {atomic, ejabberd_riak:put(#motd{server = LServer, - packet = Packet}, - motd_schema())}; - odbc -> - XML = ejabberd_odbc:escape(xml:element_to_binary(Packet)), - F = fun() -> - odbc_queries:update_t( - <<"motd">>, - [<<"username">>, <<"xml">>], - [<<"">>, XML], - [<<"username=''">>]) - end, - ejabberd_odbc:sql_transaction(LServer, F) - end. + Mod = gen_mod:db_mod(LServer, ?MODULE), + delete_motd(Mod, LServer), + set_motd(Mod, LServer, xmpp:encode(Packet)). -announce_motd_delete(From, To, Packet) -> - Host = To#jid.lserver, - Access = get_access(Host), - case acl:match_rule(Host, Access, From) of - deny -> - Err = jlib:make_error_reply(Packet, ?ERR_FORBIDDEN), - ejabberd_router:route(To, From, Err); - allow -> - announce_motd_delete(Host) - end. +announce_motd_delete(#message{to = To}) -> + LServer = To#jid.lserver, + Mod = gen_mod:db_mod(LServer, ?MODULE), + delete_motd(Mod, LServer). -announce_all_hosts_motd_delete(From, To, Packet) -> - Access = get_access(global), - case acl:match_rule(global, Access, From) of - deny -> - Err = jlib:make_error_reply(Packet, ?ERR_FORBIDDEN), - ejabberd_router:route(To, From, Err); - allow -> - Hosts = ?MYHOSTS, - [announce_motd_delete(Host) || Host <- Hosts] - end. +announce_all_hosts_motd_delete(_Packet) -> + lists:foreach( + fun(Host) -> + Mod = gen_mod:db_mod(Host, ?MODULE), + delete_motd(Mod, Host) + end, ejabberd_option:hosts()). -announce_motd_delete(LServer) -> - case gen_mod:db_type(LServer, ?MODULE) of - mnesia -> - F = fun() -> - mnesia:delete({motd, LServer}), - mnesia:write_lock_table(motd_users), - Users = mnesia:select( - motd_users, - [{#motd_users{us = '$1', _ = '_'}, - [{'==', {element, 2, '$1'}, LServer}], - ['$1']}]), - lists:foreach(fun(US) -> - mnesia:delete({motd_users, US}) - end, Users) - end, - mnesia:transaction(F); - riak -> - try - ok = ejabberd_riak:delete(motd, LServer), - ok = ejabberd_riak:delete_by_index(motd_users, - <<"server">>, - LServer), - {atomic, ok} - catch _:{badmatch, Err} -> - {atomic, Err} - end; - odbc -> - F = fun() -> - ejabberd_odbc:sql_query_t([<<"delete from motd;">>]) - end, - ejabberd_odbc:sql_transaction(LServer, F) - end. - -send_motd(JID) -> - send_motd(JID, gen_mod:db_type(JID#jid.lserver, ?MODULE)). - -send_motd(#jid{luser = LUser, lserver = LServer} = JID, mnesia) -> - case catch mnesia:dirty_read({motd, LServer}) of - [#motd{packet = Packet}] -> - US = {LUser, LServer}, - case catch mnesia:dirty_read({motd_users, US}) of - [#motd_users{}] -> - ok; - _ -> - Local = jlib:make_jid(<<>>, LServer, <<>>), - ejabberd_router:route(Local, JID, Packet), - F = fun() -> - mnesia:write(#motd_users{us = US}) - end, - mnesia:transaction(F) +-spec send_motd({presence(), ejabberd_c2s:state()}) -> {presence(), ejabberd_c2s:state()}. +send_motd({_, #{pres_last := _}} = Acc) -> + %% This is just a presence update, nothing to do + Acc; +send_motd({#presence{type = available}, + #{jid := #jid{luser = LUser, lserver = LServer} = JID}} = Acc) + when LUser /= <<>> -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + case get_motd(Mod, LServer) of + {ok, Packet} -> + CodecOpts = ejabberd_config:codec_options(), + try xmpp:decode(Packet, ?NS_CLIENT, CodecOpts) of + Msg -> + case is_motd_user(Mod, LUser, LServer) of + false -> + Local = jid:make(LServer), + ejabberd_router:route( + xmpp:set_from_to(Msg, Local, JID)), + set_motd_user(Mod, LUser, LServer); + true -> + ok + end + catch _:{xmpp_codec, Why} -> + ?ERROR_MSG("Failed to decode motd packet ~p: ~ts", + [Packet, xmpp:format_error(Why)]) end; _ -> ok - end; -send_motd(#jid{luser = LUser, lserver = LServer} = JID, riak) -> - case catch ejabberd_riak:get(motd, motd_schema(), LServer) of - {ok, #motd{packet = Packet}} -> - US = {LUser, LServer}, - case ejabberd_riak:get(motd_users, motd_users_schema(), US) of - {ok, #motd_users{}} -> - ok; - _ -> - Local = jlib:make_jid(<<>>, LServer, <<>>), - ejabberd_router:route(Local, JID, Packet), - {atomic, ejabberd_riak:put( - #motd_users{us = US}, motd_users_schema(), - [{'2i', [{<<"server">>, LServer}]}])} - end; - _ -> - ok - end; -send_motd(#jid{luser = LUser, lserver = LServer} = JID, odbc) when LUser /= <<>> -> - case catch ejabberd_odbc:sql_query( - LServer, [<<"select xml from motd where username='';">>]) of - {selected, [<<"xml">>], [[XML]]} -> - case xml_stream:parse_element(XML) of - {error, _} -> - ok; - Packet -> - Username = ejabberd_odbc:escape(LUser), - case catch ejabberd_odbc:sql_query( - LServer, - [<<"select username from motd " - "where username='">>, Username, <<"';">>]) of - {selected, [<<"username">>], []} -> - Local = jlib:make_jid(<<"">>, LServer, <<"">>), - ejabberd_router:route(Local, JID, Packet), - F = fun() -> - odbc_queries:update_t( - <<"motd">>, - [<<"username">>, <<"xml">>], - [Username, <<"">>], - [<<"username='">>, Username, <<"'">>]) - end, - ejabberd_odbc:sql_transaction(LServer, F); - _ -> - ok - end - end; - _ -> - ok - end; -send_motd(_, odbc) -> - ok. + end, + Acc; +send_motd(Acc) -> + Acc. -get_stored_motd(LServer) -> - case get_stored_motd_packet(LServer, gen_mod:db_type(LServer, ?MODULE)) of - {ok, Packet} -> - {xml:get_subtag_cdata(Packet, <<"subject">>), - xml:get_subtag_cdata(Packet, <<"body">>)}; - error -> - {<<>>, <<>>} +-spec get_motd(module(), binary()) -> {ok, xmlel()} | error | {error, any()}. +get_motd(Mod, LServer) -> + case use_cache(Mod, LServer) of + true -> + ets_cache:lookup( + ?MOTD_CACHE, {<<"">>, LServer}, + fun() -> Mod:get_motd(LServer) end); + false -> + Mod:get_motd(LServer) end. -get_stored_motd_packet(LServer, mnesia) -> - case catch mnesia:dirty_read({motd, LServer}) of - [#motd{packet = Packet}] -> - {ok, Packet}; - _ -> - error - end; -get_stored_motd_packet(LServer, riak) -> - case ejabberd_riak:get(motd, motd_schema(), LServer) of - {ok, #motd{packet = Packet}} -> - {ok, Packet}; - _ -> - error - end; -get_stored_motd_packet(LServer, odbc) -> - case catch ejabberd_odbc:sql_query( - LServer, [<<"select xml from motd where username='';">>]) of - {selected, [<<"xml">>], [[XML]]} -> - case xml_stream:parse_element(XML) of - {error, _} -> - error; - Packet -> - {ok, Packet} - end; +-spec set_motd(module(), binary(), xmlel()) -> any(). +set_motd(Mod, LServer, XML) -> + case use_cache(Mod, LServer) of + true -> + ets_cache:update( + ?MOTD_CACHE, {<<"">>, LServer}, {ok, XML}, + fun() -> Mod:set_motd(LServer, XML) end, + cache_nodes(Mod, LServer)); + false -> + Mod:set_motd(LServer, XML) + end. + +-spec is_motd_user(module(), binary(), binary()) -> boolean(). +is_motd_user(Mod, LUser, LServer) -> + Res = case use_cache(Mod, LServer) of + true -> + ets_cache:lookup( + ?MOTD_CACHE, {LUser, LServer}, + fun() -> Mod:is_motd_user(LUser, LServer) end); + false -> + Mod:is_motd_user(LUser, LServer) + end, + case Res of + {ok, Bool} -> Bool; + _ -> false + end. + +-spec set_motd_user(module(), binary(), binary()) -> any(). +set_motd_user(Mod, LUser, LServer) -> + case use_cache(Mod, LServer) of + true -> + ets_cache:update( + ?MOTD_CACHE, {LUser, LServer}, {ok, true}, + fun() -> Mod:set_motd_user(LUser, LServer) end, + cache_nodes(Mod, LServer)); + false -> + Mod:set_motd_user(LUser, LServer) + end. + +-spec delete_motd(module(), binary()) -> ok | {error, any()}. +delete_motd(Mod, LServer) -> + case Mod:delete_motd(LServer) of + ok -> + case use_cache(Mod, LServer) of + true -> + ejabberd_cluster:eval_everywhere( + ?MODULE, clean_cache, [LServer]); + false -> + ok + end; + Err -> + Err + end. + +get_stored_motd(LServer) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + case get_motd(Mod, LServer) of + {ok, Packet} -> + CodecOpts = ejabberd_config:codec_options(), + try xmpp:decode(Packet, ?NS_CLIENT, CodecOpts) of + #message{body = Body, subject = Subject} -> + {xmpp:get_text(Subject), xmpp:get_text(Body)} + catch _:{xmpp_codec, Why} -> + ?ERROR_MSG("Failed to decode motd packet ~p: ~ts", + [Packet, xmpp:format_error(Why)]) + end; _ -> - error + {<<>>, <<>>} end. %% This function is similar to others, but doesn't perform any ACL verification send_announcement_to_all(Host, SubjectS, BodyS) -> - SubjectEls = if SubjectS /= <<>> -> - [#xmlel{name = <<"subject">>, children = [{xmlcdata, SubjectS}]}]; - true -> - [] - end, - BodyEls = if BodyS /= <<>> -> - [#xmlel{name = <<"body">>, children = [{xmlcdata, BodyS}]}]; - true -> - [] - end, - Packet = #xmlel{ - name = <<"message">>, - attrs = [{<<"type">>, <<"headline">>}], - children = SubjectEls ++ BodyEls - }, + Packet = #message{type = headline, + body = xmpp:mk_text(BodyS), + subject = xmpp:mk_text(SubjectS)}, Sessions = ejabberd_sm:dirty_get_sessions_list(), - Local = jlib:make_jid(<<>>, Host, <<>>), + Local = jid:make(Host), lists:foreach( fun({U, S, R}) -> - Dest = jlib:make_jid(U, S, R), - ejabberd_router:route(Local, Dest, Packet) + Dest = jid:make(U, S, R), + ejabberd_router:route( + xmpp:set_from_to(add_store_hint(Packet), Local, Dest)) end, Sessions). -spec get_access(global | binary()) -> atom(). get_access(Host) -> - gen_mod:get_module_opt(Host, ?MODULE, access, - fun(A) when is_atom(A) -> A end, - none). + mod_announce_opt:access(Host). + +-spec add_store_hint(stanza()) -> stanza(). +add_store_hint(El) -> + xmpp:set_subtag(El, #hint{type = store}). + +-spec route_forbidden_error(stanza()) -> ok. +route_forbidden_error(Packet) -> + Lang = xmpp:get_lang(Packet), + Err = xmpp:err_forbidden(?T("Access denied by service policy"), Lang), + ejabberd_router:route_error(Packet, Err). + +-spec init_cache(module(), binary(), gen_mod:opts()) -> ok. +init_cache(Mod, Host, Opts) -> + case use_cache(Mod, Host) of + true -> + CacheOpts = cache_opts(Opts), + ets_cache:new(?MOTD_CACHE, CacheOpts); + false -> + ets_cache:delete(?MOTD_CACHE) + end. + +-spec cache_opts(gen_mod:opts()) -> [proplists:property()]. +cache_opts(Opts) -> + MaxSize = mod_announce_opt:cache_size(Opts), + CacheMissed = mod_announce_opt:cache_missed(Opts), + LifeTime = mod_announce_opt:cache_life_time(Opts), + [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}]. + +-spec use_cache(module(), binary()) -> boolean(). +use_cache(Mod, Host) -> + case erlang:function_exported(Mod, use_cache, 1) of + true -> Mod:use_cache(Host); + false -> mod_announce_opt:use_cache(Host) + end. + +-spec cache_nodes(module(), binary()) -> [node()]. +cache_nodes(Mod, Host) -> + case erlang:function_exported(Mod, cache_nodes, 1) of + true -> Mod:cache_nodes(Host); + false -> ejabberd_cluster:get_nodes() + end. + +-spec clean_cache(binary()) -> non_neg_integer(). +clean_cache(LServer) -> + ets_cache:filter( + ?MOTD_CACHE, + fun({_, S}, _) -> S /= LServer end). %%------------------------------------------------------------------------- +export(LServer) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:export(LServer). -update_tables() -> - update_motd_table(), - update_motd_users_table(). +import_info() -> + [{<<"motd">>, 3}]. -update_motd_table() -> - Fields = record_info(fields, motd), - case mnesia:table_info(motd, attributes) of - Fields -> - ejabberd_config:convert_table_to_binary( - motd, Fields, set, - fun(#motd{server = S}) -> S end, - fun(#motd{server = S, packet = P} = R) -> - NewS = iolist_to_binary(S), - NewP = xml:to_xmlel(P), - R#motd{server = NewS, packet = NewP} - end); - _ -> - ?INFO_MSG("Recreating motd table", []), - mnesia:transform_table(motd, ignore, Fields) - end. +import_start(LServer, DBType) -> + Mod = gen_mod:db_mod(DBType, ?MODULE), + Mod:init(LServer, []). +import(LServer, {sql, _}, DBType, Tab, List) -> + Mod = gen_mod:db_mod(DBType, ?MODULE), + Mod:import(LServer, Tab, List). -update_motd_users_table() -> - Fields = record_info(fields, motd_users), - case mnesia:table_info(motd_users, attributes) of - Fields -> - ejabberd_config:convert_table_to_binary( - motd_users, Fields, set, - fun(#motd_users{us = {U, _}}) -> U end, - fun(#motd_users{us = {U, S}} = R) -> - NewUS = {iolist_to_binary(U), - iolist_to_binary(S)}, - R#motd_users{us = NewUS} - end); - _ -> - ?INFO_MSG("Recreating motd_users table", []), - mnesia:transform_table(motd_users, ignore, Fields) - end. +mod_opt_type(access) -> + econf:acl(); +mod_opt_type(db_type) -> + econf:db_type(?MODULE); +mod_opt_type(use_cache) -> + econf:bool(); +mod_opt_type(cache_size) -> + econf:pos_int(infinity); +mod_opt_type(cache_missed) -> + econf:bool(); +mod_opt_type(cache_life_time) -> + econf:timeout(second, infinity). -motd_schema() -> - {record_info(fields, motd), #motd{}}. +mod_options(Host) -> + [{access, none}, + {db_type, ejabberd_config:default_db(Host, ?MODULE)}, + {use_cache, ejabberd_option:use_cache(Host)}, + {cache_size, ejabberd_option:cache_size(Host)}, + {cache_missed, ejabberd_option:cache_missed(Host)}, + {cache_life_time, ejabberd_option:cache_life_time(Host)}]. -motd_users_schema() -> - {record_info(fields, motd_users), #motd_users{}}. - -export(_Server) -> - [{motd, - fun(Host, #motd{server = LServer, packet = El}) - when LServer == Host -> - [[<<"delete from motd where username='';">>], - [<<"insert into motd(username, xml) values ('', '">>, - ejabberd_odbc:escape(xml:element_to_binary(El)), - <<"');">>]]; - (_Host, _R) -> - [] - end}, - {motd_users, - fun(Host, #motd_users{us = {LUser, LServer}}) - when LServer == Host, LUser /= <<"">> -> - Username = ejabberd_odbc:escape(LUser), - [[<<"delete from motd where username='">>, Username, <<"';">>], - [<<"insert into motd(username, xml) values ('">>, - Username, <<"', '');">>]]; - (_Host, _R) -> - [] - end}]. - -import(LServer) -> - [{<<"select xml from motd where username='';">>, - fun([XML]) -> - El = xml_stream:parse_element(XML), - #motd{server = LServer, packet = El} - end}, - {<<"select username from motd where xml='';">>, - fun([LUser]) -> - #motd_users{us = {LUser, LServer}} - end}]. - -import(_LServer, mnesia, #motd{} = Motd) -> - mnesia:dirty_write(Motd); -import(_LServer, mnesia, #motd_users{} = Users) -> - mnesia:dirty_write(Users); -import(_LServer, riak, #motd{} = Motd) -> - ejabberd_riak:put(Motd, motd_schema()); -import(_LServer, riak, #motd_users{us = {_, S}} = Users) -> - ejabberd_riak:put(Users, motd_users_schema(), - [{'2i', [{<<"server">>, S}]}]); -import(_, _, _) -> - pass. +mod_doc() -> + #{desc => + [?T("This module enables configured users to broadcast " + "announcements and to set the message of the day (MOTD). " + "Configured users can perform these actions with an XMPP " + "client either using Ad-Hoc Commands or sending messages " + "to specific JIDs."), "", + ?T("NOTE: This module can be resource intensive on large " + "deployments as it may broadcast a lot of messages. This module " + "should be disabled for instances of ejabberd with hundreds of " + "thousands users."), "", + ?T("To send announcements using " + "https://xmpp.org/extensions/xep-0050.html[XEP-0050: Ad-Hoc Commands], " + "this module requires _`mod_adhoc`_ (to execute the commands), " + "and recommends _`mod_disco`_ (to discover the commands)."), "", + ?T("To send announcements by sending messages to specific JIDs, these are the destination JIDs:"), "", + "- 'example.org/announce/all':", + ?T("Send the message to all registered users in that vhost. If the user is " + "online and connected to several resources, only the resource " + "with the highest priority will receive the message. " + "If the registered user is not connected, the message is " + "stored offline in assumption that offline storage (see _`mod_offline`_) " + "is enabled."), + "- 'example.org/announce/online':", + ?T("Send the message to all connected users. If the user is " + "online and connected to several resources, all resources will " + "receive the message."), + "- 'example.org/announce/motd':", + ?T("Set the message of the day (MOTD) that is sent " + "to users when they login. Also sends the message to all " + "connected users (similar to 'announce/online')."), + "- 'example.org/announce/motd/update':", + ?T("Set the message of the day (MOTD) that is sent to users " + "when they login. This does not send the message to any currently connected user."), + "- 'example.org/announce/motd/delete':", + ?T("Remove the existing message of the day (MOTD) by sending a message to this JID."), "", + ?T("There are similar destination JIDs to apply to all virtual hosts in ejabberd:"), "", + "- 'example.org/announce/all-hosts/all': send to all registered accounts", + "- 'example.org/announce/all-hosts/online': send to online sessions", + "- 'example.org/announce/all-hosts/motd': set MOTD and send to online", + "- 'example.org/announce/all-hosts/motd/update': update MOTD", + "- 'example.org/announce/all-hosts/motd/delete': delete MOTD"], + opts => + [{access, + #{value => ?T("AccessName"), + desc => + ?T("This option specifies who is allowed to send announcements " + "and to set the message of the day. The default value is 'none' " + "(i.e. nobody is able to send such messages).")}}, + {db_type, + #{value => "mnesia | sql", + desc => + ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}, + {use_cache, + #{value => "true | false", + desc => + ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, + {cache_size, + #{value => "pos_integer() | infinity", + desc => + ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, + {cache_missed, + #{value => "true | false", + desc => + ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, + {cache_life_time, + #{value => "timeout()", + desc => + ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}]}. diff --git a/src/mod_announce_mnesia.erl b/src/mod_announce_mnesia.erl new file mode 100644 index 000000000..6e578e001 --- /dev/null +++ b/src/mod_announce_mnesia.erl @@ -0,0 +1,134 @@ +%%%------------------------------------------------------------------- +%%% File : mod_announce_mnesia.erl +%%% Author : Evgeny Khramtsov +%%% Created : 13 Apr 2016 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_announce_mnesia). + +-behaviour(mod_announce). + +%% API +-export([init/2, set_motd_users/2, set_motd/2, delete_motd/1, + get_motd/1, is_motd_user/2, set_motd_user/2, import/3]). +-export([need_transform/1, transform/1]). + +-include_lib("xmpp/include/xmpp.hrl"). +-include("mod_announce.hrl"). +-include("logger.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + ejabberd_mnesia:create(?MODULE, motd, + [{disc_only_copies, [node()]}, + {attributes, + record_info(fields, motd)}]), + ejabberd_mnesia:create(?MODULE, motd_users, + [{disc_only_copies, [node()]}, + {attributes, + record_info(fields, motd_users)}]). + +set_motd_users(_LServer, USRs) -> + F = fun() -> + lists:foreach( + fun({U, S, _R}) -> + mnesia:write(#motd_users{us = {U, S}}) + end, USRs) + end, + transaction(F). + +set_motd(LServer, Packet) -> + F = fun() -> + mnesia:write(#motd{server = LServer, packet = Packet}) + end, + transaction(F). + +delete_motd(LServer) -> + F = fun() -> + mnesia:delete({motd, LServer}), + mnesia:write_lock_table(motd_users), + Users = mnesia:select( + motd_users, + [{#motd_users{us = '$1', _ = '_'}, + [{'==', {element, 2, '$1'}, LServer}], + ['$1']}]), + lists:foreach(fun(US) -> + mnesia:delete({motd_users, US}) + end, Users) + end, + transaction(F). + +get_motd(LServer) -> + case mnesia:dirty_read({motd, LServer}) of + [#motd{packet = Packet}] -> + {ok, Packet}; + [] -> + error + end. + +is_motd_user(LUser, LServer) -> + case mnesia:dirty_read({motd_users, {LUser, LServer}}) of + [#motd_users{}] -> {ok, true}; + _ -> {ok, false} + end. + +set_motd_user(LUser, LServer) -> + F = fun() -> + mnesia:write(#motd_users{us = {LUser, LServer}}) + end, + transaction(F). + +need_transform({motd, S, _}) when is_list(S) -> + ?INFO_MSG("Mnesia table 'motd' will be converted to binary", []), + true; +need_transform({motd_users, {U, S}, _}) when is_list(U) orelse is_list(S) -> + ?INFO_MSG("Mnesia table 'motd_users' will be converted to binary", []), + true; +need_transform(_) -> + false. + +transform(#motd{server = S, packet = P} = R) -> + NewS = iolist_to_binary(S), + NewP = fxml:to_xmlel(P), + R#motd{server = NewS, packet = NewP}; +transform(#motd_users{us = {U, S}} = R) -> + NewUS = {iolist_to_binary(U), iolist_to_binary(S)}, + R#motd_users{us = NewUS}. + +import(LServer, <<"motd">>, [<<>>, XML, _TimeStamp]) -> + El = fxml_stream:parse_element(XML), + mnesia:dirty_write(#motd{server = LServer, packet = El}); +import(LServer, <<"motd">>, [LUser, <<>>, _TimeStamp]) -> + mnesia:dirty_write(#motd_users{us = {LUser, LServer}}). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +transaction(F) -> + case mnesia:transaction(F) of + {atomic, Res} -> + Res; + {aborted, Reason} -> + ?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]), + {error, db_failure} + end. diff --git a/src/mod_announce_opt.erl b/src/mod_announce_opt.erl new file mode 100644 index 000000000..7292378a5 --- /dev/null +++ b/src/mod_announce_opt.erl @@ -0,0 +1,48 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_announce_opt). + +-export([access/1]). +-export([cache_life_time/1]). +-export([cache_missed/1]). +-export([cache_size/1]). +-export([db_type/1]). +-export([use_cache/1]). + +-spec access(gen_mod:opts() | global | binary()) -> 'none' | acl:acl(). +access(Opts) when is_map(Opts) -> + gen_mod:get_opt(access, Opts); +access(Host) -> + gen_mod:get_module_opt(Host, mod_announce, access). + +-spec cache_life_time(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +cache_life_time(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_life_time, Opts); +cache_life_time(Host) -> + gen_mod:get_module_opt(Host, mod_announce, cache_life_time). + +-spec cache_missed(gen_mod:opts() | global | binary()) -> boolean(). +cache_missed(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_missed, Opts); +cache_missed(Host) -> + gen_mod:get_module_opt(Host, mod_announce, cache_missed). + +-spec cache_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +cache_size(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_size, Opts); +cache_size(Host) -> + gen_mod:get_module_opt(Host, mod_announce, cache_size). + +-spec db_type(gen_mod:opts() | global | binary()) -> atom(). +db_type(Opts) when is_map(Opts) -> + gen_mod:get_opt(db_type, Opts); +db_type(Host) -> + gen_mod:get_module_opt(Host, mod_announce, db_type). + +-spec use_cache(gen_mod:opts() | global | binary()) -> boolean(). +use_cache(Opts) when is_map(Opts) -> + gen_mod:get_opt(use_cache, Opts); +use_cache(Host) -> + gen_mod:get_module_opt(Host, mod_announce, use_cache). + diff --git a/src/mod_announce_sql.erl b/src/mod_announce_sql.erl new file mode 100644 index 000000000..2a2692d37 --- /dev/null +++ b/src/mod_announce_sql.erl @@ -0,0 +1,178 @@ +%%%------------------------------------------------------------------- +%%% File : mod_announce_sql.erl +%%% Author : Evgeny Khramtsov +%%% Created : 13 Apr 2016 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_announce_sql). + +-behaviour(mod_announce). + + +%% API +-export([init/2, set_motd_users/2, set_motd/2, delete_motd/1, + get_motd/1, is_motd_user/2, set_motd_user/2, import/3, + export/1]). +-export([sql_schemas/0]). + +-include_lib("xmpp/include/xmpp.hrl"). +-include("mod_announce.hrl"). +-include("ejabberd_sql_pt.hrl"). +-include("logger.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(Host, _Opts) -> + ejabberd_sql_schema:update_schema(Host, ?MODULE, sql_schemas()), + ok. + +sql_schemas() -> + [#sql_schema{ + version = 1, + tables = + [#sql_table{ + name = <<"motd">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"xml">>, type = text}, + #sql_column{name = <<"created_at">>, type = timestamp, + default = true}], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"username">>], + unique = true}]}]}]. + +set_motd_users(LServer, USRs) -> + F = fun() -> + lists:foreach( + fun({U, _S, _R}) -> + ?SQL_UPSERT_T( + "motd", + ["!username=%(U)s", + "!server_host=%(LServer)s", + "xml=''"]) + end, USRs) + end, + transaction(LServer, F). + +set_motd(LServer, Packet) -> + XML = fxml:element_to_binary(Packet), + F = fun() -> + ?SQL_UPSERT_T( + "motd", + ["!username=''", + "!server_host=%(LServer)s", + "xml=%(XML)s"]) + end, + transaction(LServer, F). + +delete_motd(LServer) -> + F = fun() -> + ejabberd_sql:sql_query_t( + ?SQL("delete from motd where %(LServer)H")) + end, + transaction(LServer, F). + +get_motd(LServer) -> + case catch ejabberd_sql:sql_query( + LServer, + ?SQL("select @(xml)s from motd" + " where username='' and %(LServer)H")) of + {selected, [{XML}]} -> + parse_element(XML); + {selected, []} -> + error; + _ -> + {error, db_failure} + end. + +is_motd_user(LUser, LServer) -> + case catch ejabberd_sql:sql_query( + LServer, + ?SQL("select @(username)s from motd" + " where username=%(LUser)s and %(LServer)H")) of + {selected, [_|_]} -> + {ok, true}; + {selected, []} -> + {ok, false}; + _ -> + {error, db_failure} + end. + +set_motd_user(LUser, LServer) -> + F = fun() -> + ?SQL_UPSERT_T( + "motd", + ["!username=%(LUser)s", + "!server_host=%(LServer)s", + "xml=''"]) + end, + transaction(LServer, F). + +export(_Server) -> + [{motd, + fun(Host, #motd{server = LServer, packet = El}) + when LServer == Host -> + XML = fxml:element_to_binary(El), + [?SQL("delete from motd where username='' and %(LServer)H;"), + ?SQL_INSERT( + "motd", + ["username=''", + "server_host=%(LServer)s", + "xml=%(XML)s"])]; + (_Host, _R) -> + [] + end}, + {motd_users, + fun(Host, #motd_users{us = {LUser, LServer}}) + when LServer == Host, LUser /= <<"">> -> + [?SQL("delete from motd where username=%(LUser)s and %(LServer)H;"), + ?SQL_INSERT( + "motd", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "xml=''"])]; + (_Host, _R) -> + [] + end}]. + +import(_, _, _) -> + ok. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +transaction(LServer, F) -> + case ejabberd_sql:sql_transaction(LServer, F) of + {atomic, _} -> ok; + _ -> {error, db_failure} + end. + +parse_element(XML) -> + case fxml_stream:parse_element(XML) of + El when is_record(El, xmlel) -> + {ok, El}; + _ -> + ?ERROR_MSG("Malformed XML element in SQL table " + "'motd' for username='': ~ts", [XML]), + {error, db_failure} + end. diff --git a/src/mod_antispam.erl b/src/mod_antispam.erl new file mode 100644 index 000000000..b2e7c5937 --- /dev/null +++ b/src/mod_antispam.erl @@ -0,0 +1,913 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_antispam.erl +%%% Author : Holger Weiss +%%% Author : Stefan Strigler +%%% Purpose : Filter spam messages based on sender JID and content +%%% Created : 31 Mar 2019 by Holger Weiss +%%% +%%% +%%% ejabberd, Copyright (C) 2019-2025 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. +%%% +%%%---------------------------------------------------------------------- + +%%| definitions + +-module(mod_antispam). +-author('holger@zedat.fu-berlin.de'). +-author('stefan@strigler.de'). + +-behaviour(gen_server). +-behaviour(gen_mod). + +%% gen_mod callbacks. +-export([start/2, + prep_stop/1, + stop/1, + reload/3, + depends/2, + mod_doc/0, + mod_opt_type/1, + mod_options/1]). + +%% gen_server callbacks. +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). + +-export([get_rtbl_services_option/1]). + +%% ejabberd_commands callbacks. +-export([add_blocked_domain/2, + add_to_spam_filter_cache/2, + drop_from_spam_filter_cache/2, + expire_spam_filter_cache/2, + get_blocked_domains/1, + get_commands_spec/0, + get_spam_filter_cache/1, + reload_spam_filter_files/1, + remove_blocked_domain/2]). + +-include("ejabberd_commands.hrl"). +-include("logger.hrl"). +-include("mod_antispam.hrl"). +-include("translate.hrl"). + +-include_lib("xmpp/include/xmpp.hrl"). + +-record(state, + {host = <<>> :: binary(), + dump_fd = undefined :: file:io_device() | undefined, + url_set = sets:new() :: url_set(), + jid_set = sets:new() :: jid_set(), + jid_cache = #{} :: map(), + max_cache_size = 0 :: non_neg_integer() | unlimited, + rtbl_host = none :: binary() | none, + rtbl_subscribed = false :: boolean(), + rtbl_retry_timer = undefined :: reference() | undefined, + rtbl_domains_node :: binary(), + blocked_domains = #{} :: #{binary() => any()}, + whitelist_domains = #{} :: #{binary() => false} + }). + +-type state() :: #state{}. + +-define(COMMAND_TIMEOUT, timer:seconds(30)). +-define(DEFAULT_CACHE_SIZE, 10000). + +%% @format-begin + +%%-------------------------------------------------------------------- +%%| gen_mod callbacks + +-spec start(binary(), gen_mod:opts()) -> ok | {error, any()}. +start(Host, Opts) -> + case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of + false -> + ejabberd_commands:register_commands(?MODULE, get_commands_spec()); + true -> + ok + end, + gen_mod:start_child(?MODULE, Host, Opts). + +-spec prep_stop(binary()) -> ok | {error, any()}. +prep_stop(Host) -> + case try_call_by_host(Host, prepare_stop) of + ready_to_stop -> + ok + end. + +-spec stop(binary()) -> ok | {error, any()}. +stop(Host) -> + case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of + false -> + ejabberd_commands:unregister_commands(get_commands_spec()); + true -> + ok + end, + gen_mod:stop_child(?MODULE, Host). + +-spec reload(binary(), gen_mod:opts(), gen_mod:opts()) -> ok. +reload(Host, NewOpts, OldOpts) -> + ?DEBUG("reloading", []), + Proc = get_proc_name(Host), + gen_server:cast(Proc, {reload, NewOpts, OldOpts}). + +-spec depends(binary(), gen_mod:opts()) -> [{module(), hard | soft}]. +depends(_Host, _Opts) -> + [{mod_pubsub, soft}]. + +-spec mod_opt_type(atom()) -> econf:validator(). +mod_opt_type(access_spam) -> + econf:acl(); +mod_opt_type(cache_size) -> + econf:pos_int(unlimited); +mod_opt_type(rtbl_services) -> + econf:list( + econf:either( + econf:binary(), + econf:map( + econf:binary(), + econf:map( + econf:enum([spam_source_domains_node]), econf:binary())))); +mod_opt_type(spam_domains_file) -> + econf:either( + econf:enum([none]), econf:file()); +mod_opt_type(spam_dump_file) -> + econf:either( + econf:bool(), econf:file(write)); +mod_opt_type(spam_jids_file) -> + econf:either( + econf:enum([none]), econf:file()); +mod_opt_type(spam_urls_file) -> + econf:either( + econf:enum([none]), econf:file()); +mod_opt_type(whitelist_domains_file) -> + econf:either( + econf:enum([none]), econf:file()). + +-spec mod_options(binary()) -> [{rtbl_services, [tuple()]} | {atom(), any()}]. +mod_options(_Host) -> + [{access_spam, none}, + {cache_size, ?DEFAULT_CACHE_SIZE}, + {rtbl_services, []}, + {spam_domains_file, none}, + {spam_dump_file, false}, + {spam_jids_file, none}, + {spam_urls_file, none}, + {whitelist_domains_file, none}]. + +mod_doc() -> + #{desc => + ?T("Filter spam messages and subscription requests received from " + "remote servers based on " + "https://xmppbl.org/[Real-Time Block Lists (RTBL)], " + "lists of known spammer JIDs and/or URLs mentioned in spam messages. " + "Traffic classified as spam is rejected with an error " + "(and an '[info]' message is logged) unless the sender " + "is subscribed to the recipient's presence."), + note => "added in 25.07", + opts => + [{access_spam, + #{value => ?T("Access"), + desc => + ?T("Access rule that controls what accounts may receive spam messages. " + "If the rule returns 'allow' for a given recipient, " + "spam messages aren't rejected for that recipient. " + "The default value is 'none', which means that all recipients " + "are subject to spam filtering verification.")}}, + {cache_size, + #{value => "pos_integer()", + desc => + ?T("Maximum number of JIDs that will be cached due to sending spam URLs. " + "If that limit is exceeded, the least recently used " + "entries are removed from the cache. " + "Setting this option to '0' disables the caching feature. " + "Note that separate caches are used for each virtual host, " + " and that the caches aren't distributed across cluster nodes. " + "The default value is '10000'.")}}, + {rtbl_services, + #{value => ?T("[Service]"), + example => + ["rtbl_services:", + " - pubsub.server1.localhost:", + " spam_source_domains_node: actual_custom_pubsub_node"], + desc => + ?T("Query a RTBL service to get domains to block, as provided by " + "https://xmppbl.org/[xmppbl.org]. " + "Please note right now this option only supports one service in that list. " + "For blocking spam and abuse on MUC channels, please use _`mod_muc_rtbl`_ for now. " + "If only the host is provided, the default node names will be assumed. " + "If the node name is different than 'spam_source_domains', " + "you can setup the custom node name with the option 'spam_source_domains_node'. " + "The default value is an empty list of services.")}}, + {spam_domains_file, + #{value => ?T("none | Path"), + desc => + ?T("Path to a plain text file containing a list of " + "known spam domains, one domain per line. " + "Messages and subscription requests sent from one of the listed domains " + "are classified as spam if sender is not in recipient's roster. " + "This list of domains gets merged with the one retrieved " + "by an RTBL host if any given. " + "Use an absolute path, or the '@CONFIG_PATH@' " + "https://docs.ejabberd.im/admin/configuration/file-format/#predefined-keywords[predefined keyword] " + "if the file is available in the configuration directory. " + "The default value is 'none'.")}}, + {spam_dump_file, + #{value => ?T("false | true | Path"), + desc => + ?T("Path to the file to store blocked messages. " + "Use an absolute path, or the '@LOG_PATH@' " + "https://docs.ejabberd.im/admin/configuration/file-format/#predefined-keywords[predefined keyword] " + "to store logs " + "in the same place that the other ejabberd log files. " + "If set to 'false', it doesn't dump stanzas, which is the default. " + "If set to 'true', it stores in '\"@LOG_PATH@/spam_dump_@HOST@.log\"'.")}}, + {spam_jids_file, + #{value => ?T("none | Path"), + desc => + ?T("Path to a plain text file containing a list of " + "known spammer JIDs, one JID per line. " + "Messages and subscription requests sent from one of " + "the listed JIDs are classified as spam. " + "Messages containing at least one of the listed JIDs" + "are classified as spam as well. " + "Furthermore, the sender's JID will be cached, " + "so that future traffic originating from that JID will also be classified as spam. " + "Use an absolute path, or the '@CONFIG_PATH@' " + "https://docs.ejabberd.im/admin/configuration/file-format/#predefined-keywords[predefined keyword] " + "if the file is available in the configuration directory. " + "The default value is 'none'.")}}, + {spam_urls_file, + #{value => ?T("none | Path"), + desc => + ?T("Path to a plain text file containing a list of " + "URLs known to be mentioned in spam message bodies. " + "Messages containing at least one of the listed URLs are classified as spam. " + "Furthermore, the sender's JID will be cached, " + "so that future traffic originating from that JID will be classified as spam as well. " + "Use an absolute path, or the '@CONFIG_PATH@' " + "https://docs.ejabberd.im/admin/configuration/file-format/#predefined-keywords[predefined keyword] " + "if the file is available in the configuration directory. " + "The default value is 'none'.")}}, + {whitelist_domains_file, + #{value => ?T("none | Path"), + desc => + ?T("Path to a file containing a list of " + "domains to whitelist from being blocked, one per line. " + "If either it is in 'spam_domains_file' or more realistically " + "in a domain sent by a RTBL host (see option 'rtbl_services') " + "then this domain will be ignored and stanzas from there won't be blocked. " + "Use an absolute path, or the '@CONFIG_PATH@' " + "https://docs.ejabberd.im/admin/configuration/file-format/#predefined-keywords[predefined keyword] " + "if the file is available in the configuration directory. " + "The default value is 'none'.")}}], + example => + ["modules:", + " mod_antispam:", + " rtbl_services:", + " - xmppbl.org", + " spam_jids_file: \"@CONFIG_PATH@/spam_jids.txt\"", + " spam_dump_file: \"@LOG_PATH@/spam/host-@HOST@.log\""]}. + +%%-------------------------------------------------------------------- +%%| gen_server callbacks + +-spec init(list()) -> {ok, state()} | {stop, term()}. +init([Host, Opts]) -> + process_flag(trap_exit, true), + mod_antispam_files:init_files(Host), + FilesResults = read_files(Host), + #{jid := JIDsSet, + url := URLsSet, + domains := SpamDomainsSet, + whitelist_domains := WhitelistDomains} = + FilesResults, + ejabberd_hooks:add(local_send_to_resource_hook, + Host, + mod_antispam_rtbl, + pubsub_event_handler, + 50), + [#rtbl_service{host = RTBLHost, node = RTBLDomainsNode}] = get_rtbl_services_option(Opts), + mod_antispam_filter:init_filtering(Host), + InitState = + #state{host = Host, + jid_set = JIDsSet, + url_set = URLsSet, + dump_fd = mod_antispam_dump:init_dumping(Host), + max_cache_size = gen_mod:get_opt(cache_size, Opts), + blocked_domains = set_to_map(SpamDomainsSet), + whitelist_domains = set_to_map(WhitelistDomains, false), + rtbl_host = RTBLHost, + rtbl_domains_node = RTBLDomainsNode}, + mod_antispam_rtbl:request_blocked_domains(RTBLHost, RTBLDomainsNode, Host), + {ok, InitState}. + +-spec handle_call(term(), {pid(), term()}, state()) -> + {reply, {spam_filter, term()}, state()} | {noreply, state()}. +handle_call({check_jid, From}, _From, #state{jid_set = JIDsSet} = State) -> + {Result, State1} = filter_jid(From, JIDsSet, State), + {reply, {spam_filter, Result}, State1}; +handle_call({check_body, URLs, JIDs, From}, + _From, + #state{url_set = URLsSet, jid_set = JIDsSet} = State) -> + {Result1, State1} = filter_body(URLs, URLsSet, From, State), + {Result2, State2} = filter_body(JIDs, JIDsSet, From, State1), + Result = + if Result1 == spam -> + Result1; + true -> + Result2 + end, + {reply, {spam_filter, Result}, State2}; +handle_call(reload_spam_files, _From, State) -> + {Result, State1} = reload_files(State), + {reply, {spam_filter, Result}, State1}; +handle_call({expire_cache, Age}, _From, State) -> + {Result, State1} = expire_cache(Age, State), + {reply, {spam_filter, Result}, State1}; +handle_call({add_to_cache, JID}, _From, State) -> + {Result, State1} = add_to_cache(JID, State), + {reply, {spam_filter, Result}, State1}; +handle_call({drop_from_cache, JID}, _From, State) -> + {Result, State1} = drop_from_cache(JID, State), + {reply, {spam_filter, Result}, State1}; +handle_call(get_cache, _From, #state{jid_cache = Cache} = State) -> + {reply, {spam_filter, maps:to_list(Cache)}, State}; +handle_call({add_blocked_domain, Domain}, + _From, + #state{blocked_domains = BlockedDomains} = State) -> + BlockedDomains1 = maps:merge(BlockedDomains, #{Domain => true}), + Txt = format("~s added to blocked domains", [Domain]), + {reply, {spam_filter, {ok, Txt}}, State#state{blocked_domains = BlockedDomains1}}; +handle_call({remove_blocked_domain, Domain}, + _From, + #state{blocked_domains = BlockedDomains} = State) -> + BlockedDomains1 = maps:remove(Domain, BlockedDomains), + Txt = format("~s removed from blocked domains", [Domain]), + {reply, {spam_filter, {ok, Txt}}, State#state{blocked_domains = BlockedDomains1}}; +handle_call(get_blocked_domains, + _From, + #state{blocked_domains = BlockedDomains, whitelist_domains = WhitelistDomains} = + State) -> + {reply, {blocked_domains, maps:merge(BlockedDomains, WhitelistDomains)}, State}; +handle_call({is_blocked_domain, Domain}, + _From, + #state{blocked_domains = BlockedDomains, whitelist_domains = WhitelistDomains} = + State) -> + {reply, + maps:get(Domain, maps:merge(BlockedDomains, WhitelistDomains), false) =/= false, + State}; +handle_call(prepare_stop, + _From, + #state{host = Host, + rtbl_host = RTBLHost, + rtbl_domains_node = RTBLDomainsNode} = + State) -> + mod_antispam_rtbl:unsubscribe(RTBLHost, RTBLDomainsNode, Host), + {reply, ready_to_stop, State}; +handle_call(Request, From, State) -> + ?ERROR_MSG("Got unexpected request from ~p: ~p", [From, Request]), + {noreply, State}. + +-spec handle_cast(term(), state()) -> {noreply, state()}. +handle_cast({dump_stanza, XML}, #state{dump_fd = Fd} = State) -> + mod_antispam_dump:write_stanza_dump(Fd, XML), + {noreply, State}; +handle_cast(reopen_log, #state{host = Host, dump_fd = Fd} = State) -> + {noreply, State#state{dump_fd = mod_antispam_dump:reopen_dump_file(Host, Fd)}}; +handle_cast({reload, NewOpts, OldOpts}, + #state{host = Host, + dump_fd = Fd, + rtbl_host = OldRTBLHost, + rtbl_domains_node = OldRTBLDomainsNode, + rtbl_retry_timer = RTBLRetryTimer} = + State) -> + misc:cancel_timer(RTBLRetryTimer), + State1 = + State#state{dump_fd = mod_antispam_dump:reload_dumping(Host, Fd, OldOpts, NewOpts)}, + State2 = + case {gen_mod:get_opt(cache_size, OldOpts), gen_mod:get_opt(cache_size, NewOpts)} of + {OldMax, NewMax} when NewMax < OldMax -> + shrink_cache(State1#state{max_cache_size = NewMax}); + {OldMax, NewMax} when NewMax > OldMax -> + State1#state{max_cache_size = NewMax}; + {_OldMax, _NewMax} -> + State1 + end, + ok = mod_antispam_rtbl:unsubscribe(OldRTBLHost, OldRTBLDomainsNode, Host), + {_Result, State3} = reload_files(State2#state{blocked_domains = #{}}), + [#rtbl_service{host = RTBLHost, node = RTBLDomainsNode}] = + get_rtbl_services_option(NewOpts), + ok = mod_antispam_rtbl:request_blocked_domains(RTBLHost, RTBLDomainsNode, Host), + {noreply, State3#state{rtbl_host = RTBLHost, rtbl_domains_node = RTBLDomainsNode}}; +handle_cast({update_blocked_domains, NewItems}, + #state{blocked_domains = BlockedDomains} = State) -> + {noreply, State#state{blocked_domains = maps:merge(BlockedDomains, NewItems)}}; +handle_cast(Request, State) -> + ?ERROR_MSG("Got unexpected request from: ~p", [Request]), + {noreply, State}. + +-spec handle_info(term(), state()) -> {noreply, state()}. +handle_info({iq_reply, timeout, blocked_domains}, State) -> + ?WARNING_MSG("Fetching blocked domains failed: fetch timeout. Retrying in 60 seconds", + []), + {noreply, + State#state{rtbl_retry_timer = + erlang:send_after(60000, self(), request_blocked_domains)}}; +handle_info({iq_reply, #iq{type = error} = IQ, blocked_domains}, State) -> + ?WARNING_MSG("Fetching blocked domains failed: ~p. Retrying in 60 seconds", + [xmpp:format_stanza_error( + xmpp:get_error(IQ))]), + {noreply, + State#state{rtbl_retry_timer = + erlang:send_after(60000, self(), request_blocked_domains)}}; +handle_info({iq_reply, IQReply, blocked_domains}, + #state{blocked_domains = OldBlockedDomains, + rtbl_host = RTBLHost, + rtbl_domains_node = RTBLDomainsNode, + host = Host} = + State) -> + case mod_antispam_rtbl:parse_blocked_domains(IQReply) of + undefined -> + ?WARNING_MSG("Fetching initial list failed: invalid result payload", []), + {noreply, State#state{rtbl_retry_timer = undefined}}; + NewBlockedDomains -> + ok = mod_antispam_rtbl:subscribe(RTBLHost, RTBLDomainsNode, Host), + {noreply, + State#state{rtbl_retry_timer = undefined, + rtbl_subscribed = true, + blocked_domains = maps:merge(OldBlockedDomains, NewBlockedDomains)}} + end; +handle_info({iq_reply, timeout, subscribe_result}, State) -> + ?WARNING_MSG("Subscription error: request timeout", []), + {noreply, State#state{rtbl_subscribed = false}}; +handle_info({iq_reply, #iq{type = error} = IQ, subscribe_result}, State) -> + ?WARNING_MSG("Subscription error: ~p", + [xmpp:format_stanza_error( + xmpp:get_error(IQ))]), + {noreply, State#state{rtbl_subscribed = false}}; +handle_info({iq_reply, IQReply, subscribe_result}, State) -> + ?DEBUG("Got subscribe result: ~p", [IQReply]), + {noreply, State#state{rtbl_subscribed = true}}; +handle_info({iq_reply, _IQReply, unsubscribe_result}, State) -> + %% FIXME: we should check it's true (of type `result`, not `error`), but at that point, what + %% would we do? + {noreply, State#state{rtbl_subscribed = false}}; +handle_info(request_blocked_domains, + #state{host = Host, + rtbl_host = RTBLHost, + rtbl_domains_node = RTBLDomainsNode} = + State) -> + mod_antispam_rtbl:request_blocked_domains(RTBLHost, RTBLDomainsNode, Host), + {noreply, State}; +handle_info(Info, State) -> + ?ERROR_MSG("Got unexpected info: ~p", [Info]), + {noreply, State}. + +-spec terminate(normal | shutdown | {shutdown, term()} | term(), state()) -> ok. +terminate(Reason, + #state{host = Host, + dump_fd = Fd, + rtbl_host = RTBLHost, + rtbl_domains_node = RTBLDomainsNode, + rtbl_retry_timer = RTBLRetryTimer} = + _State) -> + ?DEBUG("Stopping spam filter process for ~s: ~p", [Host, Reason]), + misc:cancel_timer(RTBLRetryTimer), + mod_antispam_dump:terminate_dumping(Host, Fd), + mod_antispam_files:terminate_files(Host), + mod_antispam_filter:terminate_filtering(Host), + ejabberd_hooks:delete(local_send_to_resource_hook, + Host, + mod_antispam_rtbl, + pubsub_event_handler, + 50), + mod_antispam_rtbl:unsubscribe(RTBLHost, RTBLDomainsNode, Host), + ok. + +-spec code_change({down, term()} | term(), state(), term()) -> {ok, state()}. +code_change(_OldVsn, #state{host = Host} = State, _Extra) -> + ?DEBUG("Updating spam filter process for ~s", [Host]), + {ok, State}. + +%%-------------------------------------------------------------------- +%%| Internal functions + +-spec filter_jid(ljid(), jid_set(), state()) -> {ham | spam, state()}. +filter_jid(From, Set, #state{host = Host} = State) -> + case sets:is_element(From, Set) of + true -> + ?DEBUG("Spam JID found: ~s", [jid:encode(From)]), + ejabberd_hooks:run(spam_found, Host, [{jid, From}]), + {spam, State}; + false -> + case cache_lookup(From, State) of + {true, State1} -> + ?DEBUG("Spam JID found: ~s", [jid:encode(From)]), + ejabberd_hooks:run(spam_found, Host, [{jid, From}]), + {spam, State1}; + {false, State1} -> + ?DEBUG("JID not listed: ~s", [jid:encode(From)]), + {ham, State1} + end + end. + +-spec filter_body({urls, [url()]} | {jids, [ljid()]} | none, + url_set() | jid_set(), + jid(), + state()) -> + {ham | spam, state()}. +filter_body({_, Addrs}, Set, From, #state{host = Host} = State) -> + case lists:any(fun(Addr) -> sets:is_element(Addr, Set) end, Addrs) of + true -> + ?DEBUG("Spam addresses found: ~p", [Addrs]), + ejabberd_hooks:run(spam_found, Host, [{body, Addrs}]), + {spam, cache_insert(From, State)}; + false -> + ?DEBUG("Addresses not listed: ~p", [Addrs]), + {ham, State} + end; +filter_body(none, _Set, _From, State) -> + {ham, State}. + +-spec reload_files(state()) -> {ok | {error, binary()}, state()}. +reload_files(#state{host = Host, blocked_domains = BlockedDomains} = State) -> + case read_files(Host) of + #{jid := JIDsSet, + url := URLsSet, + domains := SpamDomainsSet, + whitelist_domains := WhitelistDomains} -> + case sets_equal(JIDsSet, State#state.jid_set) of + true -> + ?INFO_MSG("Reloaded spam JIDs for ~s (unchanged)", [Host]); + false -> + ?INFO_MSG("Reloaded spam JIDs for ~s (changed)", [Host]) + end, + case sets_equal(URLsSet, State#state.url_set) of + true -> + ?INFO_MSG("Reloaded spam URLs for ~s (unchanged)", [Host]); + false -> + ?INFO_MSG("Reloaded spam URLs for ~s (changed)", [Host]) + end, + {ok, + State#state{jid_set = JIDsSet, + url_set = URLsSet, + blocked_domains = maps:merge(BlockedDomains, set_to_map(SpamDomainsSet)), + whitelist_domains = set_to_map(WhitelistDomains, false)}}; + {config_error, ErrorText} -> + {{error, ErrorText}, State} + end. + +set_to_map(Set) -> + set_to_map(Set, true). + +set_to_map(Set, V) -> + sets:fold(fun(K, M) -> M#{K => V} end, #{}, Set). + +read_files(Host) -> + AccInitial = + #{jid => sets:new(), + url => sets:new(), + domains => sets:new(), + whitelist_domains => sets:new()}, + Files = + #{jid => gen_mod:get_module_opt(Host, ?MODULE, spam_jids_file), + url => gen_mod:get_module_opt(Host, ?MODULE, spam_urls_file), + domains => gen_mod:get_module_opt(Host, ?MODULE, spam_domains_file), + whitelist_domains => gen_mod:get_module_opt(Host, ?MODULE, whitelist_domains_file)}, + ejabberd_hooks:run_fold(antispam_get_lists, Host, AccInitial, [Files]). + +get_rtbl_services_option(Host) when is_binary(Host) -> + get_rtbl_services_option(gen_mod:get_module_opts(Host, ?MODULE)); +get_rtbl_services_option(Opts) when is_map(Opts) -> + Services = gen_mod:get_opt(rtbl_services, Opts), + case length(Services) =< 1 of + true -> + ok; + false -> + ?WARNING_MSG("Option rtbl_services only supports one service, but several " + "were configured. Will use only first one", + []) + end, + case Services of + [] -> + [#rtbl_service{}]; + [Host | _] when is_binary(Host) -> + [#rtbl_service{host = Host, node = ?DEFAULT_RTBL_DOMAINS_NODE}]; + [[{Host, [{spam_source_domains_node, Node}]}] | _] -> + [#rtbl_service{host = Host, node = Node}] + end. + +-spec get_proc_name(binary()) -> atom(). +get_proc_name(Host) -> + gen_mod:get_module_proc(Host, ?MODULE). + +-spec get_spam_filter_hosts() -> [binary()]. +get_spam_filter_hosts() -> + [H || H <- ejabberd_option:hosts(), gen_mod:is_loaded(H, ?MODULE)]. + +-spec sets_equal(sets:set(), sets:set()) -> boolean(). +sets_equal(A, B) -> + sets:is_subset(A, B) andalso sets:is_subset(B, A). + +-spec format(io:format(), [term()]) -> binary(). +format(Format, Data) -> + iolist_to_binary(io_lib:format(Format, Data)). + +%%-------------------------------------------------------------------- +%%| Caching + +-spec cache_insert(ljid(), state()) -> state(). +cache_insert(_LJID, #state{max_cache_size = 0} = State) -> + State; +cache_insert(LJID, #state{jid_cache = Cache, max_cache_size = MaxSize} = State) + when MaxSize /= unlimited, map_size(Cache) >= MaxSize -> + cache_insert(LJID, shrink_cache(State)); +cache_insert(LJID, #state{jid_cache = Cache} = State) -> + ?INFO_MSG("Caching spam JID: ~s", [jid:encode(LJID)]), + Cache1 = Cache#{LJID => erlang:monotonic_time(second)}, + State#state{jid_cache = Cache1}. + +-spec cache_lookup(ljid(), state()) -> {boolean(), state()}. +cache_lookup(LJID, #state{jid_cache = Cache} = State) -> + case Cache of + #{LJID := _Timestamp} -> + Cache1 = Cache#{LJID => erlang:monotonic_time(second)}, + State1 = State#state{jid_cache = Cache1}, + {true, State1}; + #{} -> + {false, State} + end. + +-spec shrink_cache(state()) -> state(). +shrink_cache(#state{jid_cache = Cache, max_cache_size = MaxSize} = State) -> + ShrinkedSize = round(MaxSize / 2), + N = map_size(Cache) - ShrinkedSize, + L = lists:keysort(2, maps:to_list(Cache)), + Cache1 = + maps:from_list( + lists:nthtail(N, L)), + State#state{jid_cache = Cache1}. + +-spec expire_cache(integer(), state()) -> {{ok, binary()}, state()}. +expire_cache(Age, #state{jid_cache = Cache} = State) -> + Threshold = erlang:monotonic_time(second) - Age, + Cache1 = maps:filter(fun(_, TS) -> TS >= Threshold end, Cache), + NumExp = map_size(Cache) - map_size(Cache1), + Txt = format("Expired ~B cache entries", [NumExp]), + {{ok, Txt}, State#state{jid_cache = Cache1}}. + +-spec add_to_cache(ljid(), state()) -> {{ok, binary()}, state()}. +add_to_cache(LJID, State) -> + State1 = cache_insert(LJID, State), + Txt = format("~s added to cache", [jid:encode(LJID)]), + {{ok, Txt}, State1}. + +-spec drop_from_cache(ljid(), state()) -> {{ok, binary()}, state()}. +drop_from_cache(LJID, #state{jid_cache = Cache} = State) -> + Cache1 = maps:remove(LJID, Cache), + if map_size(Cache1) < map_size(Cache) -> + Txt = format("~s removed from cache", [jid:encode(LJID)]), + {{ok, Txt}, State#state{jid_cache = Cache1}}; + true -> + Txt = format("~s wasn't cached", [jid:encode(LJID)]), + {{ok, Txt}, State} + end. + +%%-------------------------------------------------------------------- +%%| ejabberd command callbacks + +-spec get_commands_spec() -> [ejabberd_commands()]. +get_commands_spec() -> + [#ejabberd_commands{name = reload_spam_filter_files, + tags = [spam], + desc = "Reload spam JID/URL files", + module = ?MODULE, + function = reload_spam_filter_files, + note = "added in 25.07", + args = [{host, binary}], + result = {res, rescode}}, + #ejabberd_commands{name = get_spam_filter_cache, + tags = [spam], + desc = "Show spam filter cache contents", + module = ?MODULE, + function = get_spam_filter_cache, + note = "added in 25.07", + args = [{host, binary}], + result = + {spammers, + {list, {spammer, {tuple, [{jid, string}, {timestamp, integer}]}}}}}, + #ejabberd_commands{name = expire_spam_filter_cache, + tags = [spam], + desc = "Remove old/unused spam JIDs from cache", + module = ?MODULE, + function = expire_spam_filter_cache, + note = "added in 25.07", + args = [{host, binary}, {seconds, integer}], + result = {res, restuple}}, + #ejabberd_commands{name = add_to_spam_filter_cache, + tags = [spam], + desc = "Add JID to spam filter cache", + module = ?MODULE, + function = add_to_spam_filter_cache, + note = "added in 25.07", + args = [{host, binary}, {jid, binary}], + result = {res, restuple}}, + #ejabberd_commands{name = drop_from_spam_filter_cache, + tags = [spam], + desc = "Drop JID from spam filter cache", + module = ?MODULE, + function = drop_from_spam_filter_cache, + note = "added in 25.07", + args = [{host, binary}, {jid, binary}], + result = {res, restuple}}, + #ejabberd_commands{name = get_blocked_domains, + tags = [spam], + desc = "Get list of domains being blocked", + module = ?MODULE, + function = get_blocked_domains, + note = "added in 25.07", + args = [{host, binary}], + result = {blocked_domains, {list, {jid, string}}}}, + #ejabberd_commands{name = add_blocked_domain, + tags = [spam], + desc = "Add domain to list of blocked domains", + module = ?MODULE, + function = add_blocked_domain, + note = "added in 25.07", + args = [{host, binary}, {domain, binary}], + result = {res, restuple}}, + #ejabberd_commands{name = remove_blocked_domain, + tags = [spam], + desc = "Remove domain from list of blocked domains", + module = ?MODULE, + function = remove_blocked_domain, + note = "added in 25.07", + args = [{host, binary}, {domain, binary}], + result = {res, restuple}}]. + +for_all_hosts(F, A) -> + try lists:map(fun(Host) -> apply(F, [Host | A]) end, get_spam_filter_hosts()) of + List -> + case lists:filter(fun ({error, _}) -> + true; + (_) -> + false + end, + List) + of + [] -> + hd(List); + Errors -> + hd(Errors) + end + catch + error:{badmatch, {error, _Reason} = Error} -> + Error + end. + +try_call_by_host(Host, Call) -> + LServer = jid:nameprep(Host), + Proc = get_proc_name(LServer), + try gen_server:call(Proc, Call, ?COMMAND_TIMEOUT) of + Result -> + Result + catch + exit:{noproc, _} -> + {error, "Not configured for " ++ binary_to_list(Host)}; + exit:{timeout, _} -> + {error, "Timeout while querying ejabberd"} + end. + +-spec reload_spam_filter_files(binary()) -> ok | {error, string()}. +reload_spam_filter_files(<<"global">>) -> + for_all_hosts(fun reload_spam_filter_files/1, []); +reload_spam_filter_files(Host) -> + case try_call_by_host(Host, reload_spam_files) of + {spam_filter, ok} -> + ok; + {spam_filter, {error, Txt}} -> + {error, Txt}; + {error, _R} = Error -> + Error + end. + +-spec get_blocked_domains(binary()) -> [binary()]. +get_blocked_domains(Host) -> + case try_call_by_host(Host, get_blocked_domains) of + {blocked_domains, BlockedDomains} -> + maps:keys( + maps:filter(fun (_, false) -> + false; + (_, _) -> + true + end, + BlockedDomains)); + {error, _R} = Error -> + Error + end. + +-spec add_blocked_domain(binary(), binary()) -> {ok, string()}. +add_blocked_domain(<<"global">>, Domain) -> + for_all_hosts(fun add_blocked_domain/2, [Domain]); +add_blocked_domain(Host, Domain) -> + case try_call_by_host(Host, {add_blocked_domain, Domain}) of + {spam_filter, {Status, Txt}} -> + {Status, binary_to_list(Txt)}; + {error, _R} = Error -> + Error + end. + +-spec remove_blocked_domain(binary(), binary()) -> {ok, string()}. +remove_blocked_domain(<<"global">>, Domain) -> + for_all_hosts(fun remove_blocked_domain/2, [Domain]); +remove_blocked_domain(Host, Domain) -> + case try_call_by_host(Host, {remove_blocked_domain, Domain}) of + {spam_filter, {Status, Txt}} -> + {Status, binary_to_list(Txt)}; + {error, _R} = Error -> + Error + end. + +-spec get_spam_filter_cache(binary()) -> [{binary(), integer()}] | {error, string()}. +get_spam_filter_cache(Host) -> + case try_call_by_host(Host, get_cache) of + {spam_filter, Cache} -> + [{jid:encode(JID), TS + erlang:time_offset(second)} || {JID, TS} <- Cache]; + {error, _R} = Error -> + Error + end. + +-spec expire_spam_filter_cache(binary(), integer()) -> {ok | error, string()}. +expire_spam_filter_cache(<<"global">>, Age) -> + for_all_hosts(fun expire_spam_filter_cache/2, [Age]); +expire_spam_filter_cache(Host, Age) -> + case try_call_by_host(Host, {expire_cache, Age}) of + {spam_filter, {Status, Txt}} -> + {Status, binary_to_list(Txt)}; + {error, _R} = Error -> + Error + end. + +-spec add_to_spam_filter_cache(binary(), binary()) -> + [{binary(), integer()}] | {error, string()}. +add_to_spam_filter_cache(<<"global">>, JID) -> + for_all_hosts(fun add_to_spam_filter_cache/2, [JID]); +add_to_spam_filter_cache(Host, EncJID) -> + try jid:decode(EncJID) of + #jid{} = JID -> + LJID = + jid:remove_resource( + jid:tolower(JID)), + case try_call_by_host(Host, {add_to_cache, LJID}) of + {spam_filter, {Status, Txt}} -> + {Status, binary_to_list(Txt)}; + {error, _R} = Error -> + Error + end + catch + _:{bad_jid, _} -> + {error, "Not a valid JID: " ++ binary_to_list(EncJID)} + end. + +-spec drop_from_spam_filter_cache(binary(), binary()) -> {ok | error, string()}. +drop_from_spam_filter_cache(<<"global">>, JID) -> + for_all_hosts(fun drop_from_spam_filter_cache/2, [JID]); +drop_from_spam_filter_cache(Host, EncJID) -> + try jid:decode(EncJID) of + #jid{} = JID -> + LJID = + jid:remove_resource( + jid:tolower(JID)), + case try_call_by_host(Host, {drop_from_cache, LJID}) of + {spam_filter, {Status, Txt}} -> + {Status, binary_to_list(Txt)}; + {error, _R} = Error -> + Error + end + catch + _:{bad_jid, _} -> + {error, "Not a valid JID: " ++ binary_to_list(EncJID)} + end. + +%%-------------------------------------------------------------------- + +%%| vim: set foldmethod=marker foldmarker=%%|,%%-: diff --git a/src/mod_antispam_dump.erl b/src/mod_antispam_dump.erl new file mode 100644 index 000000000..f92ac0bf1 --- /dev/null +++ b/src/mod_antispam_dump.erl @@ -0,0 +1,186 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_antispam_dump.erl +%%% Author : Holger Weiss +%%% Author : Stefan Strigler +%%% Purpose : Manage dump file for filtered spam messages +%%% Created : 31 Mar 2019 by Holger Weiss +%%% +%%% +%%% ejabberd, Copyright (C) 2019-2025 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. +%%% +%%%---------------------------------------------------------------------- + +%%| Definitions +%% @format-begin + +-module(mod_antispam_dump). + +-author('holger@zedat.fu-berlin.de'). +-author('stefan@strigler.de'). + +-export([init_dumping/1, terminate_dumping/2, reload_dumping/4, reopen_dump_file/2, + write_stanza_dump/2]). +%% ejabberd_hooks callbacks +-export([dump_spam_stanza/1, reopen_log/0]). + +-include("logger.hrl"). +-include("mod_antispam.hrl"). +-include("translate.hrl"). + +-include_lib("xmpp/include/xmpp.hrl"). + +%%-------------------------------------------------------------------- +%%| Exported + +init_dumping(Host) -> + case get_path_option(Host) of + false -> + undefined; + DumpFile when is_binary(DumpFile) -> + case filelib:ensure_dir(DumpFile) of + ok -> + ejabberd_hooks:add(spam_stanza_rejected, Host, ?MODULE, dump_spam_stanza, 50), + ejabberd_hooks:add(reopen_log_hook, ?MODULE, reopen_log, 50), + open_dump_file(DumpFile); + {error, Reason} -> + Dirname = filename:dirname(DumpFile), + throw({open, Dirname, Reason}) + end + end. + +terminate_dumping(_Host, false) -> + ok; +terminate_dumping(Host, Fd) -> + DumpFile1 = get_path_option(Host), + close_dump_file(Fd, DumpFile1), + ejabberd_hooks:delete(spam_stanza_rejected, Host, ?MODULE, dump_spam_stanza, 50), + case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of + false -> + ejabberd_hooks:delete(reopen_log_hook, ?MODULE, reopen_log, 50); + true -> + ok + end. + +reload_dumping(Host, Fd, OldOpts, NewOpts) -> + case {get_path_option(Host, OldOpts), get_path_option(Host, NewOpts)} of + {Old, Old} -> + Fd; + {Old, New} -> + reopen_dump_file(Fd, Old, New) + end. + +-spec reopen_dump_file(binary(), file:io_device()) -> file:io_device(). +reopen_dump_file(Host, Fd) -> + DumpFile1 = get_path_option(Host), + reopen_dump_file(Fd, DumpFile1, DumpFile1). + +%%-------------------------------------------------------------------- +%%| Hook callbacks + +-spec dump_spam_stanza(message()) -> ok. +dump_spam_stanza(#message{to = #jid{lserver = LServer}} = Msg) -> + By = jid:make(<<>>, LServer), + Proc = get_proc_name(LServer), + Time = erlang:timestamp(), + Msg1 = misc:add_delay_info(Msg, By, Time), + XML = fxml:element_to_binary( + xmpp:encode(Msg1)), + gen_server:cast(Proc, {dump_stanza, XML}). + +-spec reopen_log() -> ok. +reopen_log() -> + lists:foreach(fun(Host) -> + Proc = get_proc_name(Host), + gen_server:cast(Proc, reopen_log) + end, + get_spam_filter_hosts()). + +%%-------------------------------------------------------------------- +%%| File management + +-spec open_dump_file(filename()) -> undefined | file:io_device(). +open_dump_file(false) -> + undefined; +open_dump_file(Name) -> + Modes = [append, raw, binary, delayed_write], + case file:open(Name, Modes) of + {ok, Fd} -> + ?DEBUG("Opened ~s", [Name]), + Fd; + {error, Reason} -> + ?ERROR_MSG("Cannot open dump file ~s: ~s", [Name, file:format_error(Reason)]), + undefined + end. + +-spec close_dump_file(undefined | file:io_device(), filename()) -> ok. +close_dump_file(undefined, false) -> + ok; +close_dump_file(Fd, Name) -> + case file:close(Fd) of + ok -> + ?DEBUG("Closed ~s", [Name]); + {error, Reason} -> + ?ERROR_MSG("Cannot close ~s: ~s", [Name, file:format_error(Reason)]) + end. + +-spec reopen_dump_file(file:io_device(), binary(), binary()) -> file:io_device(). +reopen_dump_file(Fd, OldDumpFile, NewDumpFile) -> + close_dump_file(Fd, OldDumpFile), + open_dump_file(NewDumpFile). + +write_stanza_dump(Fd, XML) -> + case file:write(Fd, [XML, <<$\n>>]) of + ok -> + ok; + {error, Reason} -> + ?ERROR_MSG("Cannot write spam to dump file: ~s", [file:format_error(Reason)]) + end. + +%%-------------------------------------------------------------------- +%%| Auxiliary + +get_path_option(Host) -> + Opts = gen_mod:get_module_opts(Host, ?MODULE_ANTISPAM), + get_path_option(Host, Opts). + +get_path_option(Host, Opts) -> + case gen_mod:get_opt(spam_dump_file, Opts) of + false -> + false; + true -> + LogDirPath = + iolist_to_binary(filename:dirname( + ejabberd_logger:get_log_path())), + filename:join([LogDirPath, <<"spam_dump_", Host/binary, ".log">>]); + B when is_binary(B) -> + B + end. + +%%-------------------------------------------------------------------- +%%| Copied from mod_antispam.erl + +-spec get_proc_name(binary()) -> atom(). +get_proc_name(Host) -> + gen_mod:get_module_proc(Host, ?MODULE_ANTISPAM). + +-spec get_spam_filter_hosts() -> [binary()]. +get_spam_filter_hosts() -> + [H || H <- ejabberd_option:hosts(), gen_mod:is_loaded(H, ?MODULE_ANTISPAM)]. + +%%-------------------------------------------------------------------- + +%%| vim: set foldmethod=marker foldmarker=%%|,%%-: diff --git a/src/mod_antispam_files.erl b/src/mod_antispam_files.erl new file mode 100644 index 000000000..0362a1b72 --- /dev/null +++ b/src/mod_antispam_files.erl @@ -0,0 +1,181 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_antispam_files.erl +%%% Author : Holger Weiss +%%% Author : Stefan Strigler +%%% Purpose : Filter spam messages based on sender JID and content +%%% Created : 31 Mar 2019 by Holger Weiss +%%% +%%% +%%% ejabberd, Copyright (C) 2019-2025 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. +%%% +%%%---------------------------------------------------------------------- + +%%| definitions +%% @format-begin + +-module(mod_antispam_files). + +-author('holger@zedat.fu-berlin.de'). +-author('stefan@strigler.de'). + +%% Exported +-export([init_files/1, terminate_files/1]). +% Hooks +-export([get_files_lists/2]). + +-include("ejabberd_commands.hrl"). +-include("logger.hrl"). +-include("mod_antispam.hrl"). +-include("translate.hrl"). + +-include_lib("xmpp/include/xmpp.hrl"). + +-type files_map() :: #{atom() => filename()}. +-type lists_map() :: + #{jid => jid_set(), + url => url_set(), + atom() => sets:set(binary())}. + +-define(COMMAND_TIMEOUT, timer:seconds(30)). +-define(DEFAULT_CACHE_SIZE, 10000). +-define(HTTPC_TIMEOUT, timer:seconds(3)). + +%%-------------------------------------------------------------------- +%%| Exported + +init_files(Host) -> + ejabberd_hooks:add(antispam_get_lists, Host, ?MODULE, get_files_lists, 50). + +terminate_files(Host) -> + ejabberd_hooks:delete(antispam_get_lists, Host, ?MODULE, get_files_lists, 50). + +%%-------------------------------------------------------------------- +%%| Hooks + +-spec get_files_lists(lists_map(), files_map()) -> lists_map(). +get_files_lists(#{jid := AccJids, + url := AccUrls, + domains := AccDomains, + whitelist_domains := AccWhitelist} = + Acc, + Files) -> + try read_files(Files) of + #{jid := JIDsSet, + url := URLsSet, + domains := SpamDomainsSet, + whitelist_domains := WhitelistDomains} -> + Acc#{jid => sets:union(AccJids, JIDsSet), + url => sets:union(AccUrls, URLsSet), + domains => sets:union(AccDomains, SpamDomainsSet), + whitelist_domains => sets:union(AccWhitelist, WhitelistDomains)} + catch + {Op, File, Reason} when Op == open; Op == read -> + ErrorText = format("Error trying to ~s file ~s: ~s", [Op, File, format_error(Reason)]), + ?CRITICAL_MSG(ErrorText, []), + {stop, {config_error, ErrorText}} + end. + +%%-------------------------------------------------------------------- +%%| read_files + +-spec read_files(files_map()) -> lists_map(). +read_files(Files) -> + maps:map(fun(Type, Filename) -> read_file(Filename, line_parser(Type)) end, Files). + +-spec line_parser(Type :: atom()) -> fun((binary()) -> binary()). +line_parser(jid) -> + fun parse_jid/1; +line_parser(url) -> + fun parse_url/1; +line_parser(_) -> + fun trim/1. + +-spec read_file(filename(), fun((binary()) -> ljid() | url())) -> jid_set() | url_set(). +read_file(none, _ParseLine) -> + sets:new(); +read_file(File, ParseLine) -> + case file:open(File, [read, binary, raw, {read_ahead, 65536}]) of + {ok, Fd} -> + try + read_line(Fd, ParseLine, sets:new()) + catch + E -> + throw({read, File, E}) + after + ok = file:close(Fd) + end; + {error, Reason} -> + throw({open, File, Reason}) + end. + +-spec read_line(file:io_device(), + fun((binary()) -> ljid() | url()), + jid_set() | url_set()) -> + jid_set() | url_set(). +read_line(Fd, ParseLine, Set) -> + case file:read_line(Fd) of + {ok, Line} -> + read_line(Fd, ParseLine, sets:add_element(ParseLine(Line), Set)); + {error, Reason} -> + throw(Reason); + eof -> + Set + end. + +-spec parse_jid(binary()) -> ljid(). +parse_jid(S) -> + try jid:decode(trim(S)) of + #jid{} = JID -> + jid:remove_resource( + jid:tolower(JID)) + catch + _:{bad_jid, _} -> + throw({bad_jid, S}) + end. + +-spec parse_url(binary()) -> url(). +parse_url(S) -> + URL = trim(S), + RE = <<"https?://\\S+$">>, + Options = [anchored, caseless, {capture, none}], + case re:run(URL, RE, Options) of + match -> + URL; + nomatch -> + throw({bad_url, S}) + end. + +-spec trim(binary()) -> binary(). +trim(S) -> + re:replace(S, <<"\\s+$">>, <<>>, [{return, binary}]). + +%% Function copied from mod_antispam.erl +-spec format(io:format(), [term()]) -> binary(). +format(Format, Data) -> + iolist_to_binary(io_lib:format(Format, Data)). + +-spec format_error(atom() | tuple()) -> binary(). +format_error({bad_jid, JID}) -> + <<"Not a valid JID: ", JID/binary>>; +format_error({bad_url, URL}) -> + <<"Not an HTTP(S) URL: ", URL/binary>>; +format_error(Reason) -> + list_to_binary(file:format_error(Reason)). + +%%-------------------------------------------------------------------- + +%%| vim: set foldmethod=marker foldmarker=%%|,%%-: diff --git a/src/mod_antispam_filter.erl b/src/mod_antispam_filter.erl new file mode 100644 index 000000000..7367c2ba8 --- /dev/null +++ b/src/mod_antispam_filter.erl @@ -0,0 +1,298 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_antispam_filter.erl +%%% Author : Holger Weiss +%%% Author : Stefan Strigler +%%% Purpose : Filter C2S and S2S stanzas +%%% Created : 31 Mar 2019 by Holger Weiss +%%% +%%% +%%% ejabberd, Copyright (C) 2019-2025 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. +%%% +%%%---------------------------------------------------------------------- + +%%| Definitions +%% @format-begin + +-module(mod_antispam_filter). + +-author('holger@zedat.fu-berlin.de'). +-author('stefan@strigler.de'). + +-export([init_filtering/1, terminate_filtering/1]). +%% ejabberd_hooks callbacks +-export([s2s_in_handle_info/2, s2s_receive_packet/1, sm_receive_packet/1]). + +-include("logger.hrl"). +-include("translate.hrl"). +-include("mod_antispam.hrl"). + +-include_lib("xmpp/include/xmpp.hrl"). + +-type s2s_in_state() :: ejabberd_s2s_in:state(). + +-define(HTTPC_TIMEOUT, timer:seconds(3)). + +%%-------------------------------------------------------------------- +%%| Exported + +init_filtering(Host) -> + ejabberd_hooks:add(s2s_in_handle_info, Host, ?MODULE, s2s_in_handle_info, 90), + ejabberd_hooks:add(s2s_receive_packet, Host, ?MODULE, s2s_receive_packet, 50), + ejabberd_hooks:add(sm_receive_packet, Host, ?MODULE, sm_receive_packet, 50). + +terminate_filtering(Host) -> + ejabberd_hooks:delete(s2s_receive_packet, Host, ?MODULE, s2s_receive_packet, 50), + ejabberd_hooks:delete(sm_receive_packet, Host, ?MODULE, sm_receive_packet, 50), + ejabberd_hooks:delete(s2s_in_handle_info, Host, ?MODULE, s2s_in_handle_info, 90). + +%%-------------------------------------------------------------------- +%%| Hook callbacks + +-spec s2s_receive_packet({stanza() | drop, s2s_in_state()}) -> + {stanza() | drop, s2s_in_state()} | {stop, {drop, s2s_in_state()}}. +s2s_receive_packet({A, State}) -> + case sm_receive_packet(A) of + {stop, drop} -> + {stop, {drop, State}}; + Result -> + {Result, State} + end. + +-spec sm_receive_packet(stanza() | drop) -> stanza() | drop | {stop, drop}. +sm_receive_packet(drop = Acc) -> + Acc; +sm_receive_packet(#message{from = From, + to = #jid{lserver = LServer} = To, + type = Type} = + Msg) + when Type /= groupchat, Type /= error -> + do_check(From, To, LServer, Msg); +sm_receive_packet(#presence{from = From, + to = #jid{lserver = LServer} = To, + type = subscribe} = + Presence) -> + do_check(From, To, LServer, Presence); +sm_receive_packet(Acc) -> + Acc. + +%%-------------------------------------------------------------------- +%%| Filtering deciding + +do_check(From, To, LServer, Stanza) -> + case needs_checking(From, To) of + true -> + case check_from(LServer, From) of + ham -> + case check_stanza(LServer, From, Stanza) of + ham -> + Stanza; + spam -> + reject(Stanza), + {stop, drop} + end; + spam -> + reject(Stanza), + {stop, drop} + end; + false -> + Stanza + end. + +check_stanza(LServer, From, #message{body = Body}) -> + check_body(LServer, From, xmpp:get_text(Body)); +check_stanza(_, _, _) -> + ham. + +-spec s2s_in_handle_info(s2s_in_state(), any()) -> + s2s_in_state() | {stop, s2s_in_state()}. +s2s_in_handle_info(State, {_Ref, {spam_filter, _}}) -> + ?DEBUG("Dropping expired spam filter result", []), + {stop, State}; +s2s_in_handle_info(State, _) -> + State. + +-spec needs_checking(jid(), jid()) -> boolean(). +needs_checking(#jid{lserver = FromHost} = From, #jid{lserver = LServer} = To) -> + case gen_mod:is_loaded(LServer, ?MODULE_ANTISPAM) of + true -> + Access = gen_mod:get_module_opt(LServer, ?MODULE_ANTISPAM, access_spam), + case acl:match_rule(LServer, Access, To) of + allow -> + ?DEBUG("Spam not filtered for ~s", [jid:encode(To)]), + false; + deny -> + ?DEBUG("Spam is filtered for ~s", [jid:encode(To)]), + not mod_roster:is_subscribed(From, To) + andalso not + mod_roster:is_subscribed( + jid:make(<<>>, FromHost), + To) % likely a gateway + end; + false -> + ?DEBUG("~s not loaded for ~s", [?MODULE_ANTISPAM, LServer]), + false + end. + +-spec check_from(binary(), jid()) -> ham | spam. +check_from(Host, From) -> + Proc = get_proc_name(Host), + LFrom = + {_, FromDomain, _} = + jid:remove_resource( + jid:tolower(From)), + try + case gen_server:call(Proc, {is_blocked_domain, FromDomain}) of + true -> + ?DEBUG("Spam JID found in blocked domains: ~p", [From]), + ejabberd_hooks:run(spam_found, Host, [{jid, From}]), + spam; + false -> + case gen_server:call(Proc, {check_jid, LFrom}) of + {spam_filter, Result} -> + Result + end + end + catch + exit:{timeout, _} -> + ?WARNING_MSG("Timeout while checking ~s against list of blocked domains or spammers", + [jid:encode(From)]), + ham + end. + +-spec check_body(binary(), jid(), binary()) -> ham | spam. +check_body(Host, From, Body) -> + case {extract_urls(Host, Body), extract_jids(Body)} of + {none, none} -> + ?DEBUG("No JIDs/URLs found in message", []), + ham; + {URLs, JIDs} -> + Proc = get_proc_name(Host), + LFrom = + jid:remove_resource( + jid:tolower(From)), + try gen_server:call(Proc, {check_body, URLs, JIDs, LFrom}) of + {spam_filter, Result} -> + Result + catch + exit:{timeout, _} -> + ?WARNING_MSG("Timeout while checking body", []), + ham + end + end. + +%%-------------------------------------------------------------------- +%%| Auxiliary + +-spec extract_urls(binary(), binary()) -> {urls, [url()]} | none. +extract_urls(Host, Body) -> + RE = <<"https?://\\S+">>, + Options = [global, {capture, all, binary}], + case re:run(Body, RE, Options) of + {match, Captured} when is_list(Captured) -> + Urls = resolve_redirects(Host, lists:flatten(Captured)), + {urls, Urls}; + nomatch -> + none + end. + +-spec resolve_redirects(binary(), [url()]) -> [url()]. +resolve_redirects(_Host, URLs) -> + try do_resolve_redirects(URLs, []) of + ResolvedURLs -> + ResolvedURLs + catch + exit:{timeout, _} -> + ?WARNING_MSG("Timeout while resolving redirects: ~p", [URLs]), + URLs + end. + +-spec do_resolve_redirects([url()], [url()]) -> [url()]. +do_resolve_redirects([], Result) -> + Result; +do_resolve_redirects([URL | Rest], Acc) -> + case httpc:request(get, + {URL, [{"user-agent", "curl/8.7.1"}]}, + [{autoredirect, false}, {timeout, ?HTTPC_TIMEOUT}], + []) + of + {ok, {{_, StatusCode, _}, Headers, _Body}} when StatusCode >= 300, StatusCode < 400 -> + Location = proplists:get_value("location", Headers), + case Location == undefined orelse lists:member(Location, Acc) of + true -> + do_resolve_redirects(Rest, [URL | Acc]); + false -> + do_resolve_redirects([Location | Rest], [URL | Acc]) + end; + _Res -> + do_resolve_redirects(Rest, [URL | Acc]) + end. + +-spec extract_jids(binary()) -> {jids, [ljid()]} | none. +extract_jids(Body) -> + RE = <<"\\S+@\\S+">>, + Options = [global, {capture, all, binary}], + case re:run(Body, RE, Options) of + {match, Captured} when is_list(Captured) -> + {jids, lists:filtermap(fun try_decode_jid/1, lists:flatten(Captured))}; + nomatch -> + none + end. + +-spec try_decode_jid(binary()) -> {true, ljid()} | false. +try_decode_jid(S) -> + try jid:decode(S) of + #jid{} = JID -> + {true, + jid:remove_resource( + jid:tolower(JID))} + catch + _:{bad_jid, _} -> + false + end. + +-spec reject(stanza()) -> ok. +reject(#message{from = From, + to = To, + type = Type, + lang = Lang} = + Msg) + when Type /= groupchat, Type /= error -> + ?INFO_MSG("Rejecting unsolicited message from ~s to ~s", + [jid:encode(From), jid:encode(To)]), + Txt = <<"Your message is unsolicited">>, + Err = xmpp:err_policy_violation(Txt, Lang), + ejabberd_hooks:run(spam_stanza_rejected, To#jid.lserver, [Msg]), + ejabberd_router:route_error(Msg, Err); +reject(#presence{from = From, + to = To, + lang = Lang} = + Presence) -> + ?INFO_MSG("Rejecting unsolicited presence from ~s to ~s", + [jid:encode(From), jid:encode(To)]), + Txt = <<"Your traffic is unsolicited">>, + Err = xmpp:err_policy_violation(Txt, Lang), + ejabberd_router:route_error(Presence, Err); +reject(_) -> + ok. + +-spec get_proc_name(binary()) -> atom(). +get_proc_name(Host) -> + gen_mod:get_module_proc(Host, ?MODULE_ANTISPAM). + +%%-------------------------------------------------------------------- + +%%| vim: set foldmethod=marker foldmarker=%%|,%%-: diff --git a/src/mod_antispam_opt.erl b/src/mod_antispam_opt.erl new file mode 100644 index 000000000..008a2aac1 --- /dev/null +++ b/src/mod_antispam_opt.erl @@ -0,0 +1,62 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_antispam_opt). + +-export([access_spam/1]). +-export([cache_size/1]). +-export([rtbl_services/1]). +-export([spam_domains_file/1]). +-export([spam_dump_file/1]). +-export([spam_jids_file/1]). +-export([spam_urls_file/1]). +-export([whitelist_domains_file/1]). + +-spec access_spam(gen_mod:opts() | global | binary()) -> 'none' | acl:acl(). +access_spam(Opts) when is_map(Opts) -> + gen_mod:get_opt(access_spam, Opts); +access_spam(Host) -> + gen_mod:get_module_opt(Host, mod_antispam, access_spam). + +-spec cache_size(gen_mod:opts() | global | binary()) -> 'unlimited' | pos_integer(). +cache_size(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_size, Opts); +cache_size(Host) -> + gen_mod:get_module_opt(Host, mod_antispam, cache_size). + +-spec rtbl_services(gen_mod:opts() | global | binary()) -> [binary() | [{binary(),[{'spam_source_domains_node',binary()}]}]]. +rtbl_services(Opts) when is_map(Opts) -> + gen_mod:get_opt(rtbl_services, Opts); +rtbl_services(Host) -> + gen_mod:get_module_opt(Host, mod_antispam, rtbl_services). + +-spec spam_domains_file(gen_mod:opts() | global | binary()) -> 'none' | binary(). +spam_domains_file(Opts) when is_map(Opts) -> + gen_mod:get_opt(spam_domains_file, Opts); +spam_domains_file(Host) -> + gen_mod:get_module_opt(Host, mod_antispam, spam_domains_file). + +-spec spam_dump_file(gen_mod:opts() | global | binary()) -> boolean() | binary(). +spam_dump_file(Opts) when is_map(Opts) -> + gen_mod:get_opt(spam_dump_file, Opts); +spam_dump_file(Host) -> + gen_mod:get_module_opt(Host, mod_antispam, spam_dump_file). + +-spec spam_jids_file(gen_mod:opts() | global | binary()) -> 'none' | binary(). +spam_jids_file(Opts) when is_map(Opts) -> + gen_mod:get_opt(spam_jids_file, Opts); +spam_jids_file(Host) -> + gen_mod:get_module_opt(Host, mod_antispam, spam_jids_file). + +-spec spam_urls_file(gen_mod:opts() | global | binary()) -> 'none' | binary(). +spam_urls_file(Opts) when is_map(Opts) -> + gen_mod:get_opt(spam_urls_file, Opts); +spam_urls_file(Host) -> + gen_mod:get_module_opt(Host, mod_antispam, spam_urls_file). + +-spec whitelist_domains_file(gen_mod:opts() | global | binary()) -> 'none' | binary(). +whitelist_domains_file(Opts) when is_map(Opts) -> + gen_mod:get_opt(whitelist_domains_file, Opts); +whitelist_domains_file(Host) -> + gen_mod:get_module_opt(Host, mod_antispam, whitelist_domains_file). + diff --git a/src/mod_antispam_rtbl.erl b/src/mod_antispam_rtbl.erl new file mode 100644 index 000000000..df5bac591 --- /dev/null +++ b/src/mod_antispam_rtbl.erl @@ -0,0 +1,147 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_antispam_rtbl.erl +%%% Author : Stefan Strigler +%%% Purpose : Collection of RTBL specific functionality +%%% Created : 20 Mar 2025 by Stefan Strigler +%%% +%%% +%%% ejabberd, Copyright (C) 2025 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_antispam_rtbl). +-author('stefan@strigler.de'). + +-include_lib("xmpp/include/xmpp.hrl"). +-include("logger.hrl"). +-include("mod_antispam.hrl"). + +-define(SERVICE_MODULE, mod_antispam). +-define(SERVICE_JID_PREFIX, "rtbl-"). + +-export([parse_blocked_domains/1, + parse_pubsub_event/1, + pubsub_event_handler/1, + request_blocked_domains/3, + subscribe/3, + unsubscribe/3]). + +%% @format-begin + +subscribe(RTBLHost, RTBLDomainsNode, From) -> + FromJID = service_jid(From), + SubIQ = + #iq{type = set, + to = jid:make(RTBLHost), + from = FromJID, + sub_els = [#pubsub{subscribe = #ps_subscribe{jid = FromJID, node = RTBLDomainsNode}}]}, + ?DEBUG("Sending subscription request:~n~p", [xmpp:encode(SubIQ)]), + ejabberd_router:route_iq(SubIQ, subscribe_result, self()). + +-spec unsubscribe(binary() | none, binary(), binary()) -> ok. +unsubscribe(none, _PSNode, _From) -> + ok; +unsubscribe(RTBLHost, RTBLDomainsNode, From) -> + FromJID = jid:make(From), + SubIQ = + #iq{type = set, + to = jid:make(RTBLHost), + from = FromJID, + sub_els = + [#pubsub{unsubscribe = #ps_unsubscribe{jid = FromJID, node = RTBLDomainsNode}}]}, + ejabberd_router:route_iq(SubIQ, unsubscribe_result, self()). + +-spec request_blocked_domains(binary() | none, binary(), binary()) -> ok. +request_blocked_domains(none, _PSNode, _From) -> + ok; +request_blocked_domains(RTBLHost, RTBLDomainsNode, From) -> + IQ = #iq{type = get, + from = jid:make(From), + to = jid:make(RTBLHost), + sub_els = [#pubsub{items = #ps_items{node = RTBLDomainsNode}}]}, + ?DEBUG("Requesting RTBL blocked domains from ~s:~n~p", [RTBLHost, xmpp:encode(IQ)]), + ejabberd_router:route_iq(IQ, blocked_domains, self()). + +-spec parse_blocked_domains(stanza()) -> #{binary() => any()} | undefined. +parse_blocked_domains(#iq{to = #jid{lserver = LServer}, type = result} = IQ) -> + ?DEBUG("parsing iq-result items: ~p", [IQ]), + [#rtbl_service{node = RTBLDomainsNode}] = mod_antispam:get_rtbl_services_option(LServer), + case xmpp:get_subtag(IQ, #pubsub{}) of + #pubsub{items = #ps_items{node = RTBLDomainsNode, items = Items}} -> + ?DEBUG("Got items:~n~p", [Items]), + parse_items(Items); + _ -> + undefined + end. + +-spec parse_pubsub_event(stanza()) -> #{binary() => any()}. +parse_pubsub_event(#message{to = #jid{lserver = LServer}} = Msg) -> + [#rtbl_service{node = RTBLDomainsNode}] = mod_antispam:get_rtbl_services_option(LServer), + case xmpp:get_subtag(Msg, #ps_event{}) of + #ps_event{items = + #ps_items{node = RTBLDomainsNode, + items = Items, + retract = RetractIds}} -> + maps:merge(retract_items(RetractIds), parse_items(Items)); + Other -> + ?WARNING_MSG("Couldn't extract items: ~p", [Other]), + #{} + end. + +-spec parse_items([ps_item()]) -> #{binary() => any()}. +parse_items(Items) -> + lists:foldl(fun(#ps_item{id = ID}, Acc) -> + %% TODO extract meta/extra instructions + maps:put(ID, true, Acc) + end, + #{}, + Items). + +-spec retract_items([binary()]) -> #{binary() => false}. +retract_items(Ids) -> + lists:foldl(fun(ID, Acc) -> Acc#{ID => false} end, #{}, Ids). + +-spec service_jid(binary()) -> jid(). +service_jid(Host) -> + jid:make(<<>>, Host, <>). + +%%-------------------------------------------------------------------- +%% Hook callbacks. +%%-------------------------------------------------------------------- + +-spec pubsub_event_handler(stanza()) -> drop | stanza(). +pubsub_event_handler(#message{from = FromJid, + to = + #jid{lserver = LServer, + lresource = <>}} = + Msg) -> + ?DEBUG("Got RTBL message:~n~p", [Msg]), + From = jid:encode(FromJid), + [#rtbl_service{host = RTBLHost}] = mod_antispam:get_rtbl_services_option(LServer), + case RTBLHost of + From -> + ParsedItems = parse_pubsub_event(Msg), + Proc = gen_mod:get_module_proc(LServer, ?SERVICE_MODULE), + gen_server:cast(Proc, {update_blocked_domains, ParsedItems}), + %% FIXME what's the difference between `{drop, ...}` and `{stop, {drop, ...}}`? + drop; + _Other -> + ?INFO_MSG("Got unexpected message from ~s to rtbl resource:~n~p", [From, Msg]), + Msg + end; +pubsub_event_handler(Acc) -> + ?DEBUG("unexpected something on pubsub_event_handler: ~p", [Acc]), + Acc. diff --git a/src/mod_auth_fast.erl b/src/mod_auth_fast.erl new file mode 100644 index 000000000..c72153267 --- /dev/null +++ b/src/mod_auth_fast.erl @@ -0,0 +1,177 @@ +%%%------------------------------------------------------------------- +%%% File : mod_auth_fast.erl +%%% Author : Pawel Chmielowski +%%% Created : 1 Dec 2024 by Pawel Chmielowski +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_auth_fast). +-behaviour(gen_mod). +-protocol({xep, 484, '0.2.0', '24.12', "complete", ""}). + +%% gen_mod API +-export([start/2, stop/1, reload/3, depends/2, mod_options/1, mod_opt_type/1]). +-export([mod_doc/0]). +%% Hooks +-export([c2s_inline_features/3, c2s_handle_sasl2_inline/1, + get_tokens/3, get_mechanisms/1, remove_user_tokens/2]). + +-include_lib("xmpp/include/xmpp.hrl"). +-include_lib("xmpp/include/scram.hrl"). +-include("logger.hrl"). +-include("translate.hrl"). + +-callback get_tokens(binary(), binary(), binary()) -> + [{current | next, binary(), non_neg_integer()}]. +-callback rotate_token(binary(), binary(), binary()) -> + ok | {error, atom()}. +-callback del_token(binary(), binary(), binary(), current | next) -> + ok | {error, atom()}. +-callback set_token(binary(), binary(), binary(), current | next, binary(), non_neg_integer()) -> + ok | {error, atom()}. + +%%%=================================================================== +%%% API +%%%=================================================================== +-spec start(binary(), gen_mod:opts()) -> {ok, [gen_mod:registration()]}. +start(Host, Opts) -> + Mod = gen_mod:db_mod(Opts, ?MODULE), + Mod:init(Host, Opts), + {ok, [{hook, c2s_inline_features, c2s_inline_features, 50}, + {hook, c2s_handle_sasl2_inline, c2s_handle_sasl2_inline, 10}, + {hook, set_password, remove_user_tokens, 50}, + {hook, sm_kick_user, remove_user_tokens, 50}, + {hook, remove_user, remove_user_tokens, 50}]}. + +-spec stop(binary()) -> ok. +stop(_Host) -> + ok. + +-spec reload(binary(), gen_mod:opts(), gen_mod:opts()) -> ok. +reload(Host, NewOpts, OldOpts) -> + NewMod = gen_mod:db_mod(NewOpts, ?MODULE), + OldMod = gen_mod:db_mod(OldOpts, ?MODULE), + if NewMod /= OldMod -> + NewMod:init(Host, NewOpts); + true -> + ok + end, + ok. + +-spec depends(binary(), gen_mod:opts()) -> [{module(), hard | soft}]. +depends(_Host, _Opts) -> + []. + +-spec mod_opt_type(atom()) -> econf:validator(). +mod_opt_type(db_type) -> + econf:db_type(?MODULE); +mod_opt_type(token_lifetime) -> + econf:timeout(second); +mod_opt_type(token_refresh_age) -> + econf:timeout(second). + +-spec mod_options(binary()) -> [{atom(), any()}]. +mod_options(Host) -> + [{db_type, ejabberd_config:default_db(Host, ?MODULE)}, + {token_lifetime, 30*24*60*60}, + {token_refresh_age, 24*60*60}]. + +mod_doc() -> + #{desc => + [?T("The module adds support for " + "https://xmpp.org/extensions/xep-0484.html" + "[XEP-0484: Fast Authentication Streamlining Tokens] that allows users to authenticate " + "using self-managed tokens.")], + note => "added in 24.12", + opts => + [{db_type, + #{value => "mnesia", + desc => + ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}, + {token_lifetime, + #{value => "timeout()", + desc => ?T("Time that tokens will be kept, measured from it's creation time. " + "Default value set to 30 days")}}, + {token_refresh_age, + #{value => "timeout()", + desc => ?T("This time determines age of token, that qualifies for automatic refresh. " + "Default value set to 1 day")}}], + example => + ["modules:", + " mod_auth_fast:", + " token_lifetime: 14days"]}. + +get_mechanisms(_LServer) -> + [<<"HT-SHA-256-NONE">>, <<"HT-SHA-256-UNIQ">>, <<"HT-SHA-256-EXPR">>, <<"HT-SHA-256-ENDP">>]. + +ua_hash(UA) -> + crypto:hash(sha256, UA). + +get_tokens(LServer, LUser, UA) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + ToRefresh = erlang:system_time(second) - mod_auth_fast_opt:token_refresh_age(LServer), + lists:map( + fun({Type, Token, CreatedAt}) -> + {{Type, CreatedAt < ToRefresh}, Token} + end, Mod:get_tokens(LServer, LUser, ua_hash(UA))). + +c2s_inline_features({Sasl, Bind, Extra}, Host, _State) -> + {Sasl ++ [#fast{mechs = get_mechanisms(Host)}], Bind, Extra}. + +gen_token(#{sasl2_ua_id := UA, server := Server, user := User}) -> + Mod = gen_mod:db_mod(Server, ?MODULE), + Token = base64:encode(ua_hash(<>)), + ExpiresAt = erlang:system_time(second) + mod_auth_fast_opt:token_lifetime(Server), + Mod:set_token(Server, User, ua_hash(UA), next, Token, ExpiresAt), + #fast_token{token = Token, expiry = misc:usec_to_now(ExpiresAt*1000000)}. + +c2s_handle_sasl2_inline({#{server := Server, user := User, sasl2_ua_id := UA, + sasl2_axtra_auth_info := Extra} = State, Els, Results} = Acc) -> + Mod = gen_mod:db_mod(Server, ?MODULE), + NeedRegen = + case Extra of + {token, {next, Rotate}} -> + Mod:rotate_token(Server, User, ua_hash(UA)), + Rotate; + {token, {_, true}} -> + true; + _ -> + false + end, + case {lists:keyfind(fast_request_token, 1, Els), lists:keyfind(fast, 1, Els)} of + {#fast_request_token{mech = _Mech}, #fast{invalidate = true}} -> + Mod:del_token(Server, User, ua_hash(UA), current), + {State, Els, [gen_token(State) | Results]}; + {_, #fast{invalidate = true}} -> + Mod:del_token(Server, User, ua_hash(UA), current), + Acc; + {#fast_request_token{mech = _Mech}, _} -> + {State, Els, [gen_token(State) | Results]}; + _ when NeedRegen -> + {State, Els, [gen_token(State) | Results]}; + _ -> + Acc + end. + +-spec remove_user_tokens(binary(), binary()) -> ok. +remove_user_tokens(User, Server) -> + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:del_tokens(LServer, LUser). diff --git a/src/mod_auth_fast_mnesia.erl b/src/mod_auth_fast_mnesia.erl new file mode 100644 index 000000000..081449b6e --- /dev/null +++ b/src/mod_auth_fast_mnesia.erl @@ -0,0 +1,131 @@ +%%%------------------------------------------------------------------- +%%% File : mod_auth_fast_mnesia.erl +%%% Author : Pawel Chmielowski +%%% Created : 1 Dec 2024 by Pawel Chmielowski +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_auth_fast_mnesia). + +-behaviour(mod_auth_fast). + +%% API +-export([init/2]). +-export([get_tokens/3, del_token/4, del_tokens/2, set_token/6, rotate_token/3]). + +-include_lib("xmpp/include/xmpp.hrl"). +-include("logger.hrl"). + +-record(mod_auth_fast, {key = {<<"">>, <<"">>, <<"">>} :: {binary(), binary(), binary() | '_'} | '$1', + token = <<>> :: binary() | '_', + created_at = 0 :: non_neg_integer() | '_', + expires_at = 0 :: non_neg_integer() | '_'}). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + ejabberd_mnesia:create(?MODULE, mod_auth_fast, + [{disc_only_copies, [node()]}, + {attributes, + record_info(fields, mod_auth_fast)}]). + +-spec get_tokens(binary(), binary(), binary()) -> + [{current | next, binary(), non_neg_integer()}]. +get_tokens(LServer, LUser, UA) -> + Now = erlang:system_time(second), + case mnesia:dirty_read(mod_auth_fast, {LServer, LUser, token_id(UA, next)}) of + [#mod_auth_fast{token = Token, created_at = Created, expires_at = Expires}] when Expires > Now -> + [{next, Token, Created}]; + [#mod_auth_fast{}] -> + del_token(LServer, LUser, UA, next), + []; + _ -> + [] + end ++ + case mnesia:dirty_read(mod_auth_fast, {LServer, LUser, token_id(UA, current)}) of + [#mod_auth_fast{token = Token, created_at = Created, expires_at = Expires}] when Expires > Now -> + [{current, Token, Created}]; + [#mod_auth_fast{}] -> + del_token(LServer, LUser, UA, current), + []; + _ -> + [] + end. + +-spec rotate_token(binary(), binary(), binary()) -> + ok | {error, atom()}. +rotate_token(LServer, LUser, UA) -> + F = fun() -> + case mnesia:dirty_read(mod_auth_fast, {LServer, LUser, token_id(UA, next)}) of + [#mod_auth_fast{token = Token, created_at = Created, expires_at = Expires}] -> + mnesia:write(#mod_auth_fast{key = {LServer, LUser, token_id(UA, current)}, + token = Token, created_at = Created, + expires_at = Expires}), + mnesia:delete({mod_auth_fast, {LServer, LUser, token_id(UA, next)}}); + _ -> + ok + end + end, + transaction(F). + +-spec del_token(binary(), binary(), binary(), current | next) -> + ok | {error, atom()}. +del_token(LServer, LUser, UA, Type) -> + F = fun() -> + mnesia:delete({mod_auth_fast, {LServer, LUser, token_id(UA, Type)}}) + end, + transaction(F). + +-spec del_tokens(binary(), binary()) -> ok | {error, atom()}. +del_tokens(LServer, LUser) -> + F = fun() -> + Elements = mnesia:match_object(#mod_auth_fast{key = {LServer, LUser, '_'}, _ = '_'}), + [mnesia:delete_object(E) || E <- Elements] + end, + transaction(F). + +-spec set_token(binary(), binary(), binary(), current | next, binary(), non_neg_integer()) -> + ok | {error, atom()}. +set_token(LServer, LUser, UA, Type, Token, Expires) -> + F = fun() -> + mnesia:write(#mod_auth_fast{key = {LServer, LUser, token_id(UA, Type)}, + token = Token, created_at = erlang:system_time(second), + expires_at = Expires}) + end, + transaction(F). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== + +token_id(UA, current) -> + <<"c:", UA/binary>>; +token_id(UA, _) -> + <<"n:", UA/binary>>. + +transaction(F) -> + case mnesia:transaction(F) of + {atomic, Res} -> + Res; + {aborted, Reason} -> + ?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]), + {error, db_failure} + end. diff --git a/src/mod_auth_fast_opt.erl b/src/mod_auth_fast_opt.erl new file mode 100644 index 000000000..19578aa2c --- /dev/null +++ b/src/mod_auth_fast_opt.erl @@ -0,0 +1,27 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_auth_fast_opt). + +-export([db_type/1]). +-export([token_lifetime/1]). +-export([token_refresh_age/1]). + +-spec db_type(gen_mod:opts() | global | binary()) -> atom(). +db_type(Opts) when is_map(Opts) -> + gen_mod:get_opt(db_type, Opts); +db_type(Host) -> + gen_mod:get_module_opt(Host, mod_auth_fast, db_type). + +-spec token_lifetime(gen_mod:opts() | global | binary()) -> pos_integer(). +token_lifetime(Opts) when is_map(Opts) -> + gen_mod:get_opt(token_lifetime, Opts); +token_lifetime(Host) -> + gen_mod:get_module_opt(Host, mod_auth_fast, token_lifetime). + +-spec token_refresh_age(gen_mod:opts() | global | binary()) -> pos_integer(). +token_refresh_age(Opts) when is_map(Opts) -> + gen_mod:get_opt(token_refresh_age, Opts); +token_refresh_age(Host) -> + gen_mod:get_module_opt(Host, mod_auth_fast, token_refresh_age). + diff --git a/src/mod_avatar.erl b/src/mod_avatar.erl new file mode 100644 index 000000000..e5fc3503b --- /dev/null +++ b/src/mod_avatar.erl @@ -0,0 +1,484 @@ +%%%------------------------------------------------------------------- +%%% Author : Evgeny Khramtsov +%%% Created : 13 Sep 2017 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_avatar). +-behaviour(gen_mod). + +-protocol({xep, 398, '0.2.0', '18.03', "complete", ""}). + +%% gen_mod API +-export([start/2, stop/1, reload/3, depends/2, mod_opt_type/1, mod_options/1]). +-export([mod_doc/0]). +%% Hooks +-export([pubsub_publish_item/6, vcard_iq_convert/1, vcard_iq_publish/1, + get_sm_features/5]). + +-include_lib("xmpp/include/xmpp.hrl"). +-include("logger.hrl"). +-include("pubsub.hrl"). +-include("translate.hrl"). + +-type avatar_id_meta() :: #{avatar_meta => {binary(), avatar_meta()}}. +-opaque convert_rule() :: {default | eimp:img_type(), eimp:img_type()}. +-export_type([convert_rule/0]). + +%%%=================================================================== +%%% API +%%%=================================================================== +start(_Host, _Opts) -> + {ok, [{hook, pubsub_publish_item, pubsub_publish_item, 50}, + {hook, vcard_iq_set, vcard_iq_convert, 30}, + {hook, vcard_iq_set, vcard_iq_publish, 100}, + {hook, disco_sm_features, get_sm_features, 50}]}. + +stop(_Host) -> + ok. + +reload(_Host, _NewOpts, _OldOpts) -> + ok. + +depends(_Host, _Opts) -> + [{mod_vcard, hard}, {mod_vcard_xupdate, hard}, {mod_pubsub, hard}]. + +%%%=================================================================== +%%% Hooks +%%%=================================================================== +-spec pubsub_publish_item(binary(), binary(), jid(), jid(), binary(), [xmlel()]) -> ok. +pubsub_publish_item(LServer, ?NS_AVATAR_METADATA, + #jid{luser = LUser, lserver = LServer} = From, + #jid{luser = LUser, lserver = LServer} = Host, + ItemId, [Payload|_]) -> + try xmpp:decode(Payload) of + #avatar_meta{info = []} -> + delete_vcard_avatar(From); + #avatar_meta{info = Info} -> + Rules = mod_avatar_opt:convert(LServer), + case get_meta_info(Info, Rules) of + #avatar_info{type = MimeType, id = ID, url = <<"">>} = I -> + case get_avatar_data(Host, ID) of + {ok, Data} -> + Meta = #avatar_meta{info = [I]}, + Photo = #vcard_photo{type = MimeType, + binval = Data}, + set_vcard_avatar(From, Photo, + #{avatar_meta => {ID, Meta}}); + {error, _} -> + ok + end; + #avatar_info{type = MimeType, url = URL} -> + Photo = #vcard_photo{type = MimeType, + extval = URL}, + set_vcard_avatar(From, Photo, #{}) + end; + _ -> + ?WARNING_MSG("Invalid avatar metadata of ~ts@~ts published " + "with item id ~ts", + [LUser, LServer, ItemId]) + catch _:{xmpp_codec, Why} -> + ?WARNING_MSG("Failed to decode avatar metadata of ~ts@~ts: ~ts", + [LUser, LServer, xmpp:format_error(Why)]) + end; +pubsub_publish_item(_, _, _, _, _, _) -> + ok. + +-spec vcard_iq_convert(iq()) -> iq() | {stop, stanza_error()}. +vcard_iq_convert(#iq{from = From, lang = Lang, sub_els = [VCard]} = IQ) -> + #jid{luser = LUser, lserver = LServer} = From, + case convert_avatar(LUser, LServer, VCard) of + {ok, MimeType, Data} -> + VCard1 = VCard#vcard_temp{ + photo = #vcard_photo{type = MimeType, + binval = Data}}, + IQ#iq{sub_els = [VCard1]}; + pass -> + IQ; + {error, Reason} -> + stop_with_error(Lang, Reason) + end; +vcard_iq_convert(Acc) -> + Acc. + +-spec vcard_iq_publish(iq()) -> iq() | {stop, stanza_error()}. +vcard_iq_publish(#iq{sub_els = [#vcard_temp{photo = undefined}]} = IQ) -> + publish_avatar(IQ, #avatar_meta{}, <<>>, <<>>, <<>>); +vcard_iq_publish(#iq{sub_els = [#vcard_temp{ + photo = #vcard_photo{ + type = MimeType, + binval = Data}}]} = IQ) + when is_binary(Data), Data /= <<>> -> + SHA1 = str:sha(Data), + M = get_avatar_meta(IQ), + case M of + {ok, SHA1, _} -> + IQ; + {ok, _ItemID, #avatar_meta{info = Info} = Meta} -> + case lists:keyfind(SHA1, #avatar_info.id, Info) of + #avatar_info{} -> + IQ; + false -> + Info1 = lists:filter( + fun(#avatar_info{url = URL}) -> URL /= <<"">> end, + Info), + Meta1 = Meta#avatar_meta{info = Info1}, + publish_avatar(IQ, Meta1, MimeType, Data, SHA1) + end; + {error, _} -> + publish_avatar(IQ, #avatar_meta{}, MimeType, Data, SHA1) + end; +vcard_iq_publish(Acc) -> + Acc. + +-spec get_sm_features({error, stanza_error()} | empty | {result, [binary()]}, + jid(), jid(), binary(), binary()) -> + {error, stanza_error()} | empty | {result, [binary()]}. +get_sm_features({error, _Error} = Acc, _From, _To, _Node, _Lang) -> + Acc; +get_sm_features(Acc, _From, _To, <<"">>, _Lang) -> + {result, [?NS_PEP_VCARD_CONVERSION_0 | + case Acc of + {result, Features} -> Features; + empty -> [] + end]}; +get_sm_features(Acc, _From, _To, _Node, _Lang) -> + Acc. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +-spec get_meta_info([avatar_info()], [convert_rule()]) -> avatar_info(). +get_meta_info(Info, Rules) -> + case lists:foldl( + fun(_, #avatar_info{} = Acc) -> + Acc; + (#avatar_info{url = URL}, Acc) when URL /= <<"">> -> + Acc; + (#avatar_info{} = I, _) when Rules == [] -> + I; + (#avatar_info{type = MimeType} = I, Acc) -> + T = decode_mime_type(MimeType), + case lists:keymember(T, 2, Rules) of + true -> + I; + false -> + case convert_to_type(T, Rules) of + undefined -> + Acc; + _ -> + [I|Acc] + end + end + end, [], Info) of + #avatar_info{} = I -> I; + [] -> hd(Info); + Is -> hd(lists:reverse(Is)) + end. + +-spec get_avatar_data(jid(), binary()) -> {ok, binary()} | + {error, + notfound | invalid_data | internal_error}. +get_avatar_data(JID, ItemID) -> + {LUser, LServer, _} = LBJID = jid:remove_resource(jid:tolower(JID)), + case mod_pubsub:get_item(LBJID, ?NS_AVATAR_DATA, ItemID) of + #pubsub_item{payload = [Payload|_]} -> + try xmpp:decode(Payload) of + #avatar_data{data = Data} -> + {ok, Data}; + _ -> + ?WARNING_MSG("Invalid avatar data detected " + "for ~ts@~ts with item id ~ts", + [LUser, LServer, ItemID]), + {error, invalid_data} + catch _:{xmpp_codec, Why} -> + ?WARNING_MSG("Failed to decode avatar data for " + "~ts@~ts with item id ~ts: ~ts", + [LUser, LServer, ItemID, + xmpp:format_error(Why)]), + {error, invalid_data} + end; + #pubsub_item{payload = []} -> + ?WARNING_MSG("Empty avatar data detected " + "for ~ts@~ts with item id ~ts", + [LUser, LServer, ItemID]), + {error, invalid_data}; + {error, #stanza_error{reason = 'item-not-found'}} -> + {error, notfound}; + {error, Reason} -> + ?WARNING_MSG("Failed to get item for ~ts@~ts at node ~ts " + "with item id ~ts: ~p", + [LUser, LServer, ?NS_AVATAR_METADATA, ItemID, Reason]), + {error, internal_error} + end. + +-spec get_avatar_meta(iq()) -> {ok, binary(), avatar_meta()} | + {error, + notfound | invalid_metadata | internal_error}. +get_avatar_meta(#iq{meta = #{avatar_meta := {ItemID, Meta}}}) -> + {ok, ItemID, Meta}; +get_avatar_meta(#iq{from = JID}) -> + {LUser, LServer, _} = LBJID = jid:remove_resource(jid:tolower(JID)), + case mod_pubsub:get_items(LBJID, ?NS_AVATAR_METADATA) of + [#pubsub_item{itemid = {ItemID, _}, payload = [Payload|_]}|_] -> + try xmpp:decode(Payload) of + #avatar_meta{} = Meta -> + {ok, ItemID, Meta}; + _ -> + ?WARNING_MSG("Invalid metadata payload detected " + "for ~ts@~ts with item id ~ts", + [LUser, LServer, ItemID]), + {error, invalid_metadata} + catch _:{xmpp_codec, Why} -> + ?WARNING_MSG("Failed to decode metadata for " + "~ts@~ts with item id ~ts: ~ts", + [LUser, LServer, ItemID, + xmpp:format_error(Why)]), + {error, invalid_metadata} + end; + {error, #stanza_error{reason = 'item-not-found'}} -> + {error, notfound}; + {error, Reason} -> + ?WARNING_MSG("Failed to get items for ~ts@~ts at node ~ts: ~p", + [LUser, LServer, ?NS_AVATAR_METADATA, Reason]), + {error, internal_error} + end. + +-spec publish_avatar(iq(), avatar_meta(), binary(), binary(), binary()) -> + iq() | {stop, stanza_error()}. +publish_avatar(#iq{from = JID} = IQ, Meta, <<>>, <<>>, <<>>) -> + {_, LServer, _} = LBJID = jid:remove_resource(jid:tolower(JID)), + case mod_pubsub:publish_item( + LBJID, LServer, ?NS_AVATAR_METADATA, + JID, <<>>, [xmpp:encode(Meta)]) of + {result, _} -> + IQ; + {error, StanzaErr} -> + {stop, StanzaErr} + end; +publish_avatar(#iq{from = JID} = IQ, Meta, MimeType, Data, ItemID) -> + #avatar_meta{info = Info} = Meta, + {_, LServer, _} = LBJID = jid:remove_resource(jid:tolower(JID)), + Payload = xmpp:encode(#avatar_data{data = Data}), + case mod_pubsub:publish_item( + LBJID, LServer, ?NS_AVATAR_DATA, + JID, ItemID, [Payload]) of + {result, _} -> + {W, H} = case eimp:identify(Data) of + {ok, ImgInfo} -> + {proplists:get_value(width, ImgInfo), + proplists:get_value(height, ImgInfo)}; + _ -> + {undefined, undefined} + end, + I = #avatar_info{id = ItemID, + width = W, + height = H, + type = MimeType, + bytes = size(Data)}, + Meta1 = Meta#avatar_meta{info = [I|Info]}, + case mod_pubsub:publish_item( + LBJID, LServer, ?NS_AVATAR_METADATA, + JID, ItemID, [xmpp:encode(Meta1)]) of + {result, _} -> + IQ; + {error, StanzaErr} -> + ?ERROR_MSG("Failed to publish avatar metadata for ~ts: ~p", + [jid:encode(JID), StanzaErr]), + {stop, StanzaErr} + end; + {error, #stanza_error{reason = 'not-acceptable'} = StanzaErr} -> + ?WARNING_MSG("Failed to publish avatar data for ~ts: ~p", + [jid:encode(JID), StanzaErr]), + {stop, StanzaErr}; + {error, StanzaErr} -> + ?ERROR_MSG("Failed to publish avatar data for ~ts: ~p", + [jid:encode(JID), StanzaErr]), + {stop, StanzaErr} + end. + +-spec convert_avatar(binary(), binary(), vcard_temp()) -> + {ok, binary(), binary()} | + {error, eimp:error_reason() | base64_error} | + pass. +convert_avatar(LUser, LServer, VCard) -> + case mod_avatar_opt:convert(LServer) of + [] -> + pass; + Rules -> + case VCard#vcard_temp.photo of + #vcard_photo{binval = Data} when is_binary(Data) -> + convert_avatar(LUser, LServer, Data, Rules); + _ -> + pass + end + end. + +-spec convert_avatar(binary(), binary(), binary(), [convert_rule()]) -> + {ok, binary(), binary()} | + {error, eimp:error_reason()} | + pass. +convert_avatar(LUser, LServer, Data, Rules) -> + Type = get_type(Data), + NewType = convert_to_type(Type, Rules), + if NewType == undefined -> + pass; + true -> + ?DEBUG("Converting avatar of ~ts@~ts: ~ts -> ~ts", + [LUser, LServer, Type, NewType]), + RateLimit = mod_avatar_opt:rate_limit(LServer), + Opts = [{limit_by, {LUser, LServer}}, + {rate_limit, RateLimit}], + case eimp:convert(Data, NewType, Opts) of + {ok, NewData} -> + {ok, encode_mime_type(NewType), NewData}; + {error, Reason} = Err -> + ?ERROR_MSG("Failed to convert avatar of " + "~ts@~ts (~ts -> ~ts): ~ts", + [LUser, LServer, Type, NewType, + eimp:format_error(Reason)]), + Err + end + end. + +-spec set_vcard_avatar(jid(), vcard_photo() | undefined, avatar_id_meta()) -> ok. +set_vcard_avatar(JID, VCardPhoto, Meta) -> + case get_vcard(JID) of + {ok, #vcard_temp{photo = VCardPhoto}} -> + ok; + {ok, VCard} -> + VCard1 = VCard#vcard_temp{photo = VCardPhoto}, + IQ = #iq{from = JID, to = JID, id = p1_rand:get_string(), + type = set, sub_els = [VCard1], meta = Meta}, + LServer = JID#jid.lserver, + ejabberd_hooks:run_fold(vcard_iq_set, LServer, IQ, []), + ok; + {error, _} -> + ok + end. + +-spec delete_vcard_avatar(jid()) -> ok. +delete_vcard_avatar(JID) -> + set_vcard_avatar(JID, undefined, #{}). + +-spec get_vcard(jid()) -> {ok, vcard_temp()} | {error, invalid_vcard}. +get_vcard(#jid{luser = LUser, lserver = LServer}) -> + VCardEl = case mod_vcard:get_vcard(LUser, LServer) of + [El] -> El; + _ -> #vcard_temp{} + end, + try xmpp:decode(VCardEl, ?NS_VCARD, []) of + #vcard_temp{} = VCard -> + {ok, VCard}; + _ -> + ?ERROR_MSG("Invalid vCard of ~ts@~ts in the database", + [LUser, LServer]), + {error, invalid_vcard} + catch _:{xmpp_codec, Why} -> + ?ERROR_MSG("Failed to decode vCard of ~ts@~ts: ~ts", + [LUser, LServer, xmpp:format_error(Why)]), + {error, invalid_vcard} + end. + +-spec stop_with_error(binary(), eimp:error_reason()) -> + {stop, stanza_error()}. +stop_with_error(Lang, Reason) -> + Txt = eimp:format_error(Reason), + {stop, xmpp:err_internal_server_error(Txt, Lang)}. + +-spec get_type(binary()) -> eimp:img_type() | unknown. +get_type(Data) -> + eimp:get_type(Data). + +-spec convert_to_type(eimp:img_type() | unknown, [convert_rule()]) -> + eimp:img_type() | undefined. +convert_to_type(unknown, _Rules) -> + undefined; +convert_to_type(Type, Rules) -> + case proplists:get_value(Type, Rules) of + undefined -> + proplists:get_value(default, Rules); + Type -> + undefined; + T -> + T + end. + +-spec decode_mime_type(binary()) -> eimp:img_type() | unknown. +decode_mime_type(MimeType) -> + case str:to_lower(MimeType) of + <<"image/jpeg">> -> jpeg; + <<"image/png">> -> png; + <<"image/webp">> -> webp; + <<"image/gif">> -> gif; + _ -> unknown + end. + +-spec encode_mime_type(eimp:img_type()) -> binary(). +encode_mime_type(Type) -> + <<"image/", (atom_to_binary(Type, latin1))/binary>>. + +mod_opt_type(convert) -> + case eimp:supported_formats() of + [] -> + fun(_) -> econf:fail(eimp_error) end; + Formats -> + econf:options( + maps:from_list( + [{Type, econf:enum(Formats)} + || Type <- [default|Formats]])) + end; +mod_opt_type(rate_limit) -> + econf:pos_int(). + +-spec mod_options(binary()) -> [{convert, [?MODULE:convert_rule()]} | + {atom(), any()}]. +mod_options(_) -> + [{rate_limit, 10}, + {convert, []}]. + +mod_doc() -> + #{desc => + [?T("The purpose of the module is to cope with legacy and modern " + "XMPP clients posting avatars. The process is described in " + "https://xmpp.org/extensions/xep-0398.html" + "[XEP-0398: User Avatar to vCard-Based Avatars Conversion]."), "", + ?T("Also, the module supports conversion between avatar " + "image formats on the fly."), "", + ?T("The module depends on _`mod_vcard`_, _`mod_vcard_xupdate`_ and " + "_`mod_pubsub`_.")], + opts => + [{convert, + #{value => "{From: To}", + desc => + ?T("Defines image conversion rules: the format in 'From' " + "will be converted to format in 'To'. The value of 'From' " + "can also be 'default', which is match-all rule. NOTE: " + "the list of supported formats is detected at compile time " + "depending on the image libraries installed in the system."), + example => + ["convert:", + " webp: jpg", + " default: png"]}}, + {rate_limit, + #{value => ?T("Number"), + desc => + ?T("Limit any given JID by the number of avatars it is able " + "to convert per minute. This is to protect the server from " + "image conversion DoS. The default value is '10'.")}}]}. diff --git a/src/mod_avatar_opt.erl b/src/mod_avatar_opt.erl new file mode 100644 index 000000000..187ed16db --- /dev/null +++ b/src/mod_avatar_opt.erl @@ -0,0 +1,20 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_avatar_opt). + +-export([convert/1]). +-export([rate_limit/1]). + +-spec convert(gen_mod:opts() | global | binary()) -> [mod_avatar:convert_rule()]. +convert(Opts) when is_map(Opts) -> + gen_mod:get_opt(convert, Opts); +convert(Host) -> + gen_mod:get_module_opt(Host, mod_avatar, convert). + +-spec rate_limit(gen_mod:opts() | global | binary()) -> pos_integer(). +rate_limit(Opts) when is_map(Opts) -> + gen_mod:get_opt(rate_limit, Opts); +rate_limit(Host) -> + gen_mod:get_module_opt(Host, mod_avatar, rate_limit). + diff --git a/src/mod_block_strangers.erl b/src/mod_block_strangers.erl new file mode 100644 index 000000000..fd4e9974d --- /dev/null +++ b/src/mod_block_strangers.erl @@ -0,0 +1,321 @@ +%%%------------------------------------------------------------------- +%%% File : mod_block_strangers.erl +%%% Author : Alexey Shchepin +%%% Purpose : Block packets from non-subscribers +%%% Created : 25 Dec 2016 by Alexey Shchepin +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_block_strangers). + +-author('alexey@process-one.net'). + +-behaviour(gen_mod). + +%% API +-export([start/2, stop/1, reload/3, mod_doc/0, + depends/2, mod_opt_type/1, mod_options/1]). + +-export([filter_packet/1, filter_offline_msg/1, filter_subscription/2, + get_sm_features/5]). + +-include_lib("xmpp/include/xmpp.hrl"). +-include("logger.hrl"). +-include("translate.hrl"). + +-define(SETS, gb_sets). + +-define(NS_BLOCK_STRANGERS, <<"urn:ejabberd:block-strangers">>). + +-type c2s_state() :: ejabberd_c2s:state(). + +%%%=================================================================== +%%% Callbacks and hooks +%%%=================================================================== +start(_Host, _Opts) -> + {ok, [{hook, disco_local_features, get_sm_features, 50}, + {hook, disco_sm_features, get_sm_features, 50}, + {hook, user_receive_packet, filter_packet, 25}, + {hook, roster_in_subscription, filter_subscription, 25}, + {hook, offline_message_hook, filter_offline_msg, 25}]}. + +stop(_Host) -> + ok. + +reload(_Host, _NewOpts, _OldOpts) -> + ok. + +get_sm_features(Acc, _From, _To, <<"">>, _Lang) -> + Features = case Acc of + {result, I} -> I; + _ -> [] + end, + {result, [?NS_BLOCK_STRANGERS | Features]}; + +get_sm_features(Acc, _From, _To, _Node, _Lang) -> + Acc. + +-spec filter_packet({stanza(), c2s_state()}) -> {stanza(), c2s_state()} | + {stop, {drop, c2s_state()}}. +filter_packet({#message{from = From} = Msg, State} = Acc) -> + LFrom = jid:tolower(From), + LBFrom = jid:remove_resource(LFrom), + #{pres_a := PresA} = State, + case (?SETS):is_element(LFrom, PresA) + orelse (?SETS):is_element(LBFrom, PresA) + orelse sets_bare_member(LBFrom, PresA) of + false -> + case check_message(Msg) of + allow -> Acc; + deny -> {stop, {drop, State}} + end; + true -> + Acc + end; +filter_packet(Acc) -> + Acc. + +-spec filter_offline_msg({_, message()}) -> {_, message()} | {stop, {drop, message()}}. +filter_offline_msg({_Action, #message{} = Msg} = Acc) -> + case check_message(Msg) of + allow -> Acc; + deny -> {stop, {drop, Msg}} + end. + +-spec filter_subscription(boolean(), presence()) -> boolean() | {stop, false}. +filter_subscription(Acc, #presence{meta = #{captcha := passed}}) -> + Acc; +filter_subscription(Acc, #presence{from = From, to = To, lang = Lang, + id = SID, type = subscribe} = Pres) -> + LServer = To#jid.lserver, + case mod_block_strangers_opt:drop(LServer) andalso + mod_block_strangers_opt:captcha(LServer) andalso + need_check(Pres) of + true -> + case check_subscription(From, To) of + false -> + BFrom = jid:remove_resource(From), + BTo = jid:remove_resource(To), + Limiter = jid:tolower(BFrom), + case ejabberd_captcha:create_captcha( + SID, BTo, BFrom, Lang, Limiter, + fun(Res) -> handle_captcha_result(Res, Pres) end) of + {ok, ID, Body, CaptchaEls} -> + Msg = #message{from = BTo, to = From, + id = ID, body = Body, + sub_els = CaptchaEls}, + case mod_block_strangers_opt:log(LServer) of + true -> + ?INFO_MSG("Challenge subscription request " + "from stranger ~ts to ~ts with " + "CAPTCHA", + [jid:encode(From), jid:encode(To)]); + false -> + ok + end, + ejabberd_router:route(Msg); + {error, limit} -> + ErrText = ?T("Too many CAPTCHA requests"), + Err = xmpp:err_resource_constraint(ErrText, Lang), + ejabberd_router:route_error(Pres, Err); + _ -> + ErrText = ?T("Unable to generate a CAPTCHA"), + Err = xmpp:err_internal_server_error(ErrText, Lang), + ejabberd_router:route_error(Pres, Err) + end, + {stop, false}; + true -> + Acc + end; + false -> + Acc + end; +filter_subscription(Acc, _) -> + Acc. + +-spec handle_captcha_result(captcha_succeed | captcha_failed, presence()) -> ok. +handle_captcha_result(captcha_succeed, Pres) -> + Pres1 = xmpp:put_meta(Pres, captcha, passed), + ejabberd_router:route(Pres1); +handle_captcha_result(captcha_failed, #presence{lang = Lang} = Pres) -> + Txt = ?T("The CAPTCHA verification has failed"), + ejabberd_router:route_error(Pres, xmpp:err_not_allowed(Txt, Lang)). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +-spec check_message(message()) -> allow | deny. +check_message(#message{from = From, to = To, lang = Lang} = Msg) -> + LServer = To#jid.lserver, + case need_check(Msg) of + true -> + case check_subscription(From, To) of + false -> + Drop = mod_block_strangers_opt:drop(LServer), + Log = mod_block_strangers_opt:log(LServer), + if + Log -> + ?INFO_MSG("~ts message from stranger ~ts to ~ts", + [if Drop -> "Rejecting"; + true -> "Allow" + end, + jid:encode(From), jid:encode(To)]); + true -> + ok + end, + if + Drop -> + Txt = ?T("Messages from strangers are rejected"), + Err = xmpp:err_policy_violation(Txt, Lang), + Msg1 = maybe_adjust_from(Msg), + ejabberd_router:route_error(Msg1, Err), + deny; + true -> + allow + end; + true -> + allow + end; + false -> + allow + end. + +-spec maybe_adjust_from(message()) -> message(). +maybe_adjust_from(#message{type = groupchat, from = From} = Msg) -> + Msg#message{from = jid:remove_resource(From)}; +maybe_adjust_from(#message{} = Msg) -> + Msg. + +-spec need_check(presence() | message()) -> boolean(). +need_check(Pkt) -> + To = xmpp:get_to(Pkt), + From = xmpp:get_from(Pkt), + IsSelf = To#jid.luser == From#jid.luser andalso + To#jid.lserver == From#jid.lserver, + LServer = To#jid.lserver, + IsEmpty = case Pkt of + #message{body = [], subject = []} -> + true; + _ -> + false + end, + IsError = (error == xmpp:get_type(Pkt)), + AllowLocalUsers = mod_block_strangers_opt:allow_local_users(LServer), + Access = mod_block_strangers_opt:access(LServer), + not (IsSelf orelse IsEmpty orelse IsError + orelse acl:match_rule(LServer, Access, From) == allow + orelse ((AllowLocalUsers orelse From#jid.luser == <<"">>) + andalso ejabberd_router:is_my_host(From#jid.lserver))). + +-spec check_subscription(jid(), jid()) -> boolean(). +check_subscription(From, To) -> + LocalServer = To#jid.lserver, + {RemoteUser, RemoteServer, _} = jid:tolower(From), + case mod_roster:is_subscribed(From, To) of + false when RemoteUser == <<"">> -> + false; + false -> + %% Check if the contact's server is in the roster + mod_block_strangers_opt:allow_transports(LocalServer) + andalso mod_roster:is_subscribed(jid:make(RemoteServer), To); + true -> + true + end. + +-spec sets_bare_member(ljid(), ?SETS:set()) -> boolean(). +sets_bare_member({U, S, <<"">>} = LBJID, Set) -> + case ?SETS:next(?SETS:iterator_from(LBJID, Set)) of + {{U, S, _}, _} -> true; + _ -> false + end. + +depends(_Host, _Opts) -> + []. + +mod_opt_type(access) -> + econf:acl(); +mod_opt_type(drop) -> + econf:bool(); +mod_opt_type(log) -> + econf:bool(); +mod_opt_type(captcha) -> + econf:bool(); +mod_opt_type(allow_local_users) -> + econf:bool(); +mod_opt_type(allow_transports) -> + econf:bool(). + +mod_options(_) -> + [{access, none}, + {drop, true}, + {log, false}, + {captcha, false}, + {allow_local_users, true}, + {allow_transports, true}]. + +mod_doc() -> + #{desc => + ?T("This module blocks and logs any messages coming from an " + "unknown entity. If a writing entity is not in your roster, " + "you can let this module drop and/or log the message. " + "By default you'll just not receive message from that entity. " + "Enable this module if you want to drop SPAM messages."), + opts => + [{access, + #{value => ?T("AccessName"), + desc => + ?T("The option is supposed to be used when 'allow_local_users' " + "and 'allow_transports' are not enough. It's an Access Rule where " + "'deny' means the stanza will be rejected; there's an exception " + "if option 'captcha' is configured. And " + "'allow' means the sender is whitelisted and the stanza " + "will pass through. The default value is 'none', which " + "means nothing is whitelisted.")}}, + {drop, + #{value => "true | false", + desc => + ?T("This option specifies if strangers messages should " + "be dropped or not. The default value is 'true'.")}}, + {log, + #{value => "true | false", + desc => + ?T("This option specifies if strangers' messages should " + "be logged (as info message) in ejabberd.log. " + "The default value is 'false'.")}}, + {allow_local_users, + #{value => "true | false", + desc => + ?T("This option specifies if strangers from the same " + "local host should be accepted or not. " + "The default value is 'true'.")}}, + {allow_transports, + #{value => "true | false", + desc => + ?T("If set to 'true' and some server's JID is in user's " + "roster, then messages from any user of this server " + "are accepted even if no subscription present. " + "The default value is 'true'.")}}, + {captcha, + #{value => "true | false", + desc => + ?T("Whether to generate CAPTCHA challenges in response to " + "incoming presence subscription requests from strangers. " + "See also section _`basic.md#captcha|CAPTCHA`_" + " of the Configuration Guide. " + "The default value is 'false'.")}}]}. diff --git a/src/mod_block_strangers_opt.erl b/src/mod_block_strangers_opt.erl new file mode 100644 index 000000000..33dc6cc09 --- /dev/null +++ b/src/mod_block_strangers_opt.erl @@ -0,0 +1,48 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_block_strangers_opt). + +-export([access/1]). +-export([allow_local_users/1]). +-export([allow_transports/1]). +-export([captcha/1]). +-export([drop/1]). +-export([log/1]). + +-spec access(gen_mod:opts() | global | binary()) -> 'none' | acl:acl(). +access(Opts) when is_map(Opts) -> + gen_mod:get_opt(access, Opts); +access(Host) -> + gen_mod:get_module_opt(Host, mod_block_strangers, access). + +-spec allow_local_users(gen_mod:opts() | global | binary()) -> boolean(). +allow_local_users(Opts) when is_map(Opts) -> + gen_mod:get_opt(allow_local_users, Opts); +allow_local_users(Host) -> + gen_mod:get_module_opt(Host, mod_block_strangers, allow_local_users). + +-spec allow_transports(gen_mod:opts() | global | binary()) -> boolean(). +allow_transports(Opts) when is_map(Opts) -> + gen_mod:get_opt(allow_transports, Opts); +allow_transports(Host) -> + gen_mod:get_module_opt(Host, mod_block_strangers, allow_transports). + +-spec captcha(gen_mod:opts() | global | binary()) -> boolean(). +captcha(Opts) when is_map(Opts) -> + gen_mod:get_opt(captcha, Opts); +captcha(Host) -> + gen_mod:get_module_opt(Host, mod_block_strangers, captcha). + +-spec drop(gen_mod:opts() | global | binary()) -> boolean(). +drop(Opts) when is_map(Opts) -> + gen_mod:get_opt(drop, Opts); +drop(Host) -> + gen_mod:get_module_opt(Host, mod_block_strangers, drop). + +-spec log(gen_mod:opts() | global | binary()) -> boolean(). +log(Opts) when is_map(Opts) -> + gen_mod:get_opt(log, Opts); +log(Host) -> + gen_mod:get_module_opt(Host, mod_block_strangers, log). + diff --git a/src/mod_blocking.erl b/src/mod_blocking.erl index 07e9027b6..562899c3b 100644 --- a/src/mod_blocking.erl +++ b/src/mod_blocking.erl @@ -5,7 +5,7 @@ %%% Created : 24 Aug 2008 by Stephan Maka %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -27,501 +27,247 @@ -behaviour(gen_mod). --export([start/2, stop/1, process_iq/3, - process_iq_set/4, process_iq_get/5]). +-protocol({xep, 191, '1.2', '2.1.7', "complete", ""}). + +-export([start/2, stop/1, reload/3, process_iq/1, depends/2, + disco_features/5, mod_options/1, mod_doc/0]). --include("ejabberd.hrl"). -include("logger.hrl"). - --include("jlib.hrl"). - +-include_lib("xmpp/include/xmpp.hrl"). -include("mod_privacy.hrl"). +-include("translate.hrl"). -start(Host, Opts) -> - IQDisc = gen_mod:get_opt(iqdisc, Opts, fun gen_iq_handler:check_type/1, - one_queue), - ejabberd_hooks:add(privacy_iq_get, Host, ?MODULE, - process_iq_get, 40), - ejabberd_hooks:add(privacy_iq_set, Host, ?MODULE, - process_iq_set, 40), - mod_disco:register_feature(Host, ?NS_BLOCKING), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, - ?NS_BLOCKING, ?MODULE, process_iq, IQDisc). +start(_Host, _Opts) -> + {ok, [{hook, disco_local_features, disco_features, 50}, + {iq_handler, ejabberd_sm, ?NS_BLOCKING, process_iq}]}. -stop(Host) -> - ejabberd_hooks:delete(privacy_iq_get, Host, ?MODULE, - process_iq_get, 40), - ejabberd_hooks:delete(privacy_iq_set, Host, ?MODULE, - process_iq_set, 40), - mod_disco:unregister_feature(Host, ?NS_BLOCKING), - gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, - ?NS_BLOCKING). +stop(_Host) -> + ok. -process_iq(_From, _To, IQ) -> - SubEl = IQ#iq.sub_el, - IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}. +reload(_Host, _NewOpts, _OldOpts) -> + ok. -process_iq_get(_, From, _To, - #iq{xmlns = ?NS_BLOCKING, - sub_el = #xmlel{name = <<"blocklist">>}}, - _) -> - #jid{luser = LUser, lserver = LServer} = From, - {stop, process_blocklist_get(LUser, LServer)}; -process_iq_get(Acc, _, _, _, _) -> Acc. +depends(_Host, _Opts) -> + [{mod_privacy, hard}]. -process_iq_set(_, From, _To, - #iq{xmlns = ?NS_BLOCKING, - sub_el = - #xmlel{name = SubElName, children = SubEls}}) -> - #jid{luser = LUser, lserver = LServer} = From, - Res = case {SubElName, xml:remove_cdata(SubEls)} of - {<<"block">>, []} -> {error, ?ERR_BAD_REQUEST}; - {<<"block">>, Els} -> - JIDs = parse_blocklist_items(Els, []), - process_blocklist_block(LUser, LServer, JIDs); - {<<"unblock">>, []} -> - process_blocklist_unblock_all(LUser, LServer); - {<<"unblock">>, Els} -> - JIDs = parse_blocklist_items(Els, []), - process_blocklist_unblock(LUser, LServer, JIDs); - _ -> {error, ?ERR_BAD_REQUEST} - end, - {stop, Res}; -process_iq_set(Acc, _, _, _) -> Acc. +-spec disco_features({error, stanza_error()} | {result, [binary()]} | empty, + jid(), jid(), binary(), binary()) -> + {error, stanza_error()} | {result, [binary()]}. +disco_features({error, Err}, _From, _To, _Node, _Lang) -> + {error, Err}; +disco_features(empty, _From, _To, <<"">>, _Lang) -> + {result, [?NS_BLOCKING]}; +disco_features({result, Feats}, _From, _To, <<"">>, _Lang) -> + {result, [?NS_BLOCKING|Feats]}; +disco_features(Acc, _From, _To, _Node, _Lang) -> + Acc. -list_to_blocklist_jids([], JIDs) -> JIDs; -list_to_blocklist_jids([#listitem{type = jid, - action = deny, value = JID} = - Item - | Items], +-spec process_iq(iq()) -> iq(). +process_iq(#iq{type = Type, + from = #jid{luser = U, lserver = S}, + to = #jid{luser = U, lserver = S}} = IQ) -> + case Type of + get -> process_iq_get(IQ); + set -> process_iq_set(IQ) + end; +process_iq(#iq{lang = Lang} = IQ) -> + Txt = ?T("Query to another users is forbidden"), + xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)). + +-spec process_iq_get(iq()) -> iq(). +process_iq_get(#iq{sub_els = [#block_list{}]} = IQ) -> + process_get(IQ); +process_iq_get(#iq{lang = Lang} = IQ) -> + Txt = ?T("No module is handling this query"), + xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)). + +-spec process_iq_set(iq()) -> iq(). +process_iq_set(#iq{lang = Lang, sub_els = [SubEl]} = IQ) -> + case SubEl of + #block{items = []} -> + Txt = ?T("No items found in this query"), + xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)); + #block{items = Items} -> + JIDs = [jid:tolower(JID) || #block_item{jid = JID} <- Items], + process_block(IQ, JIDs); + #unblock{items = []} -> + process_unblock_all(IQ); + #unblock{items = Items} -> + JIDs = [jid:tolower(JID) || #block_item{jid = JID} <- Items], + process_unblock(IQ, JIDs); + _ -> + Txt = ?T("No module is handling this query"), + xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)) + end. + +-spec listitems_to_jids([listitem()], [ljid()]) -> [ljid()]. +listitems_to_jids([], JIDs) -> + JIDs; +listitems_to_jids([#listitem{type = jid, + action = deny, value = JID} = Item | Items], JIDs) -> - case Item of - #listitem{match_all = true} -> Match = true; - #listitem{match_iq = true, match_message = true, - match_presence_in = true, match_presence_out = true} -> - Match = true; - _ -> Match = false - end, - if Match -> list_to_blocklist_jids(Items, [JID | JIDs]); - true -> list_to_blocklist_jids(Items, JIDs) + Match = case Item of + #listitem{match_all = true} -> + true; + #listitem{match_iq = true, + match_message = true, + match_presence_in = true, + match_presence_out = true} -> + true; + _ -> + false + end, + if Match -> listitems_to_jids(Items, [JID | JIDs]); + true -> listitems_to_jids(Items, JIDs) end; % Skip Privacy List items than cannot be mapped to Blocking items -list_to_blocklist_jids([_ | Items], JIDs) -> - list_to_blocklist_jids(Items, JIDs). +listitems_to_jids([_ | Items], JIDs) -> + listitems_to_jids(Items, JIDs). -parse_blocklist_items([], JIDs) -> JIDs; -parse_blocklist_items([#xmlel{name = <<"item">>, - attrs = Attrs} - | Els], - JIDs) -> - case xml:get_attr(<<"jid">>, Attrs) of - {value, JID1} -> - JID = jlib:jid_tolower(jlib:string_to_jid(JID1)), - parse_blocklist_items(Els, [JID | JIDs]); - false -> parse_blocklist_items(Els, JIDs) - end; -parse_blocklist_items([_ | Els], JIDs) -> - parse_blocklist_items(Els, JIDs). - -process_blocklist_block(LUser, LServer, JIDs) -> - Filter = fun (List) -> - AlreadyBlocked = list_to_blocklist_jids(List, []), - lists:foldr(fun (JID, List1) -> - case lists:member(JID, AlreadyBlocked) - of - true -> List1; - false -> - [#listitem{type = jid, - value = JID, - action = deny, - order = 0, - match_all = true} - | List1] - end - end, - List, JIDs) - end, - case process_blocklist_block(LUser, LServer, Filter, - gen_mod:db_type(LServer, mod_privacy)) - of - {atomic, {ok, Default, List}} -> - UserList = make_userlist(Default, List), - broadcast_list_update(LUser, LServer, Default, - UserList), - broadcast_blocklist_event(LUser, LServer, - {block, JIDs}), - {result, [], UserList}; - _ -> {error, ?ERR_INTERNAL_SERVER_ERROR} +-spec process_block(iq(), [ljid()]) -> iq(). +process_block(#iq{from = From} = IQ, LJIDs) -> + #jid{luser = LUser, lserver = LServer} = From, + case mod_privacy:get_user_list(LUser, LServer, default) of + {error, _} -> + err_db_failure(IQ); + Res -> + {Name, List} = case Res of + error -> {<<"Blocked contacts">>, []}; + {ok, NameList} -> NameList + end, + AlreadyBlocked = listitems_to_jids(List, []), + NewList = lists:foldr( + fun(LJID, List1) -> + case lists:member(LJID, AlreadyBlocked) of + true -> + List1; + false -> + [#listitem{type = jid, + value = LJID, + action = deny, + order = 0, + match_all = true}|List1] + end + end, List, LJIDs), + case mod_privacy:set_list(LUser, LServer, Name, NewList) of + ok -> + case (if Res == error -> + mod_privacy:set_default_list( + LUser, LServer, Name); + true -> + ok + end) of + ok -> + mod_privacy:push_list_update(From, Name), + Items = [#block_item{jid = jid:make(LJID)} + || LJID <- LJIDs], + broadcast_event(From, #block{items = Items}), + xmpp:make_iq_result(IQ); + {error, notfound} -> + ?ERROR_MSG("Failed to set default list '~ts': " + "the list should exist, but not found", + [Name]), + err_db_failure(IQ); + {error, _} -> + err_db_failure(IQ) + end; + {error, _} -> + err_db_failure(IQ) + end end. -process_blocklist_block(LUser, LServer, Filter, - mnesia) -> - F = fun () -> - case mnesia:wread({privacy, {LUser, LServer}}) of - [] -> - P = #privacy{us = {LUser, LServer}}, - NewDefault = <<"Blocked contacts">>, - NewLists1 = [], - List = []; - [#privacy{default = Default, lists = Lists} = P] -> - case lists:keysearch(Default, 1, Lists) of - {value, {_, List}} -> - NewDefault = Default, - NewLists1 = lists:keydelete(Default, 1, Lists); - false -> - NewDefault = <<"Blocked contacts">>, - NewLists1 = Lists, - List = [] - end - end, - NewList = Filter(List), - NewLists = [{NewDefault, NewList} | NewLists1], - mnesia:write(P#privacy{default = NewDefault, - lists = NewLists}), - {ok, NewDefault, NewList} - end, - mnesia:transaction(F); -process_blocklist_block(LUser, LServer, Filter, - riak) -> - {atomic, - begin - case ejabberd_riak:get(privacy, mod_privacy:privacy_schema(), - {LUser, LServer}) of - {ok, #privacy{default = Default, lists = Lists} = P} -> - case lists:keysearch(Default, 1, Lists) of - {value, {_, List}} -> - NewDefault = Default, - NewLists1 = lists:keydelete(Default, 1, Lists); - false -> - NewDefault = <<"Blocked contacts">>, - NewLists1 = Lists, - List = [] - end; - {error, _} -> - P = #privacy{us = {LUser, LServer}}, - NewDefault = <<"Blocked contacts">>, - NewLists1 = [], - List = [] - end, - NewList = Filter(List), - NewLists = [{NewDefault, NewList} | NewLists1], - case ejabberd_riak:put(P#privacy{default = NewDefault, - lists = NewLists}, - mod_privacy:privacy_schema()) of - ok -> - {ok, NewDefault, NewList}; - Err -> - Err - end - end}; -process_blocklist_block(LUser, LServer, Filter, odbc) -> - F = fun () -> - Default = case - mod_privacy:sql_get_default_privacy_list_t(LUser) - of - {selected, [<<"name">>], []} -> - Name = <<"Blocked contacts">>, - mod_privacy:sql_add_privacy_list(LUser, Name), - mod_privacy:sql_set_default_privacy_list(LUser, - Name), - Name; - {selected, [<<"name">>], [[Name]]} -> Name - end, - {selected, [<<"id">>], [[ID]]} = - mod_privacy:sql_get_privacy_list_id_t(LUser, Default), - case mod_privacy:sql_get_privacy_list_data_by_id_t(ID) - of - {selected, - [<<"t">>, <<"value">>, <<"action">>, <<"ord">>, - <<"match_all">>, <<"match_iq">>, <<"match_message">>, - <<"match_presence_in">>, <<"match_presence_out">>], - RItems = [_ | _]} -> - List = lists:map(fun mod_privacy:raw_to_item/1, RItems); - _ -> List = [] - end, - NewList = Filter(List), - NewRItems = lists:map(fun mod_privacy:item_to_raw/1, - NewList), - mod_privacy:sql_set_privacy_list(ID, NewRItems), - {ok, Default, NewList} - end, - ejabberd_odbc:sql_transaction(LServer, F). - -process_blocklist_unblock_all(LUser, LServer) -> - Filter = fun (List) -> - lists:filter(fun (#listitem{action = A}) -> A =/= deny - end, - List) - end, - case process_blocklist_unblock_all(LUser, LServer, - Filter, - gen_mod:db_type(LServer, mod_privacy)) - of - {atomic, ok} -> {result, []}; - {atomic, {ok, Default, List}} -> - UserList = make_userlist(Default, List), - broadcast_list_update(LUser, LServer, Default, - UserList), - broadcast_blocklist_event(LUser, LServer, unblock_all), - {result, [], UserList}; - _ -> {error, ?ERR_INTERNAL_SERVER_ERROR} +-spec process_unblock_all(iq()) -> iq(). +process_unblock_all(#iq{from = From} = IQ) -> + #jid{luser = LUser, lserver = LServer} = From, + case mod_privacy:get_user_list(LUser, LServer, default) of + {ok, {Name, List}} -> + NewList = lists:filter( + fun(#listitem{action = A}) -> + A /= deny + end, List), + case mod_privacy:set_list(LUser, LServer, Name, NewList) of + ok -> + mod_privacy:push_list_update(From, Name), + broadcast_event(From, #unblock{}), + xmpp:make_iq_result(IQ); + {error, _} -> + err_db_failure(IQ) + end; + error -> + broadcast_event(From, #unblock{}), + xmpp:make_iq_result(IQ); + {error, _} -> + err_db_failure(IQ) end. -process_blocklist_unblock_all(LUser, LServer, Filter, - mnesia) -> - F = fun () -> - case mnesia:read({privacy, {LUser, LServer}}) of - [] -> - % No lists, nothing to unblock - ok; - [#privacy{default = Default, lists = Lists} = P] -> - case lists:keysearch(Default, 1, Lists) of - {value, {_, List}} -> - NewList = Filter(List), - NewLists1 = lists:keydelete(Default, 1, Lists), - NewLists = [{Default, NewList} | NewLists1], - mnesia:write(P#privacy{lists = NewLists}), - {ok, Default, NewList}; - false -> - % No default list, nothing to unblock - ok - end - end - end, - mnesia:transaction(F); -process_blocklist_unblock_all(LUser, LServer, Filter, - riak) -> - {atomic, - case ejabberd_riak:get(privacy, {LUser, LServer}) of - {ok, #privacy{default = Default, lists = Lists} = P} -> - case lists:keysearch(Default, 1, Lists) of - {value, {_, List}} -> - NewList = Filter(List), - NewLists1 = lists:keydelete(Default, 1, Lists), - NewLists = [{Default, NewList} | NewLists1], - case ejabberd_riak:put(P#privacy{lists = NewLists}, - mod_privacy:privacy_schema()) of - ok -> - {ok, Default, NewList}; - Err -> - Err - end; - false -> - %% No default list, nothing to unblock - ok - end; - {error, _} -> - %% No lists, nothing to unblock - ok - end}; -process_blocklist_unblock_all(LUser, LServer, Filter, - odbc) -> - F = fun () -> - case mod_privacy:sql_get_default_privacy_list_t(LUser) - of - {selected, [<<"name">>], []} -> ok; - {selected, [<<"name">>], [[Default]]} -> - {selected, [<<"id">>], [[ID]]} = - mod_privacy:sql_get_privacy_list_id_t(LUser, Default), - case mod_privacy:sql_get_privacy_list_data_by_id_t(ID) - of - {selected, - [<<"t">>, <<"value">>, <<"action">>, <<"ord">>, - <<"match_all">>, <<"match_iq">>, <<"match_message">>, - <<"match_presence_in">>, <<"match_presence_out">>], - RItems = [_ | _]} -> - List = lists:map(fun mod_privacy:raw_to_item/1, - RItems), - NewList = Filter(List), - NewRItems = lists:map(fun mod_privacy:item_to_raw/1, - NewList), - mod_privacy:sql_set_privacy_list(ID, NewRItems), - {ok, Default, NewList}; - _ -> ok - end; - _ -> ok - end - end, - ejabberd_odbc:sql_transaction(LServer, F). - -process_blocklist_unblock(LUser, LServer, JIDs) -> - Filter = fun (List) -> - lists:filter(fun (#listitem{action = deny, type = jid, - value = JID}) -> - not lists:member(JID, JIDs); - (_) -> true - end, - List) - end, - case process_blocklist_unblock(LUser, LServer, Filter, - gen_mod:db_type(LServer, mod_privacy)) - of - {atomic, ok} -> {result, []}; - {atomic, {ok, Default, List}} -> - UserList = make_userlist(Default, List), - broadcast_list_update(LUser, LServer, Default, - UserList), - broadcast_blocklist_event(LUser, LServer, - {unblock, JIDs}), - {result, [], UserList}; - _ -> {error, ?ERR_INTERNAL_SERVER_ERROR} +-spec process_unblock(iq(), [ljid()]) -> iq(). +process_unblock(#iq{from = From} = IQ, LJIDs) -> + #jid{luser = LUser, lserver = LServer} = From, + case mod_privacy:get_user_list(LUser, LServer, default) of + {ok, {Name, List}} -> + NewList = lists:filter( + fun(#listitem{action = deny, type = jid, + value = LJID}) -> + not lists:member(LJID, LJIDs); + (_) -> + true + end, List), + case mod_privacy:set_list(LUser, LServer, Name, NewList) of + ok -> + mod_privacy:push_list_update(From, Name), + Items = [#block_item{jid = jid:make(LJID)} + || LJID <- LJIDs], + broadcast_event(From, #unblock{items = Items}), + xmpp:make_iq_result(IQ); + {error, _} -> + err_db_failure(IQ) + end; + error -> + Items = [#block_item{jid = jid:make(LJID)} + || LJID <- LJIDs], + broadcast_event(From, #unblock{items = Items}), + xmpp:make_iq_result(IQ); + {error, _} -> + err_db_failure(IQ) end. -process_blocklist_unblock(LUser, LServer, Filter, - mnesia) -> - F = fun () -> - case mnesia:read({privacy, {LUser, LServer}}) of - [] -> - % No lists, nothing to unblock - ok; - [#privacy{default = Default, lists = Lists} = P] -> - case lists:keysearch(Default, 1, Lists) of - {value, {_, List}} -> - NewList = Filter(List), - NewLists1 = lists:keydelete(Default, 1, Lists), - NewLists = [{Default, NewList} | NewLists1], - mnesia:write(P#privacy{lists = NewLists}), - {ok, Default, NewList}; - false -> - % No default list, nothing to unblock - ok - end - end - end, - mnesia:transaction(F); -process_blocklist_unblock(LUser, LServer, Filter, - riak) -> - {atomic, - case ejabberd_riak:get(privacy, mod_privacy:privacy_schema(), - {LUser, LServer}) of - {error, _} -> - %% No lists, nothing to unblock - ok; - {ok, #privacy{default = Default, lists = Lists} = P} -> - case lists:keysearch(Default, 1, Lists) of - {value, {_, List}} -> - NewList = Filter(List), - NewLists1 = lists:keydelete(Default, 1, Lists), - NewLists = [{Default, NewList} | NewLists1], - case ejabberd_riak:put(P#privacy{lists = NewLists}, - mod_privacy:privacy_schema()) of - ok -> - {ok, Default, NewList}; - Err -> - Err - end; - false -> - %% No default list, nothing to unblock - ok - end - end}; -process_blocklist_unblock(LUser, LServer, Filter, - odbc) -> - F = fun () -> - case mod_privacy:sql_get_default_privacy_list_t(LUser) - of - {selected, [<<"name">>], []} -> ok; - {selected, [<<"name">>], [[Default]]} -> - {selected, [<<"id">>], [[ID]]} = - mod_privacy:sql_get_privacy_list_id_t(LUser, Default), - case mod_privacy:sql_get_privacy_list_data_by_id_t(ID) - of - {selected, - [<<"t">>, <<"value">>, <<"action">>, <<"ord">>, - <<"match_all">>, <<"match_iq">>, <<"match_message">>, - <<"match_presence_in">>, <<"match_presence_out">>], - RItems = [_ | _]} -> - List = lists:map(fun mod_privacy:raw_to_item/1, - RItems), - NewList = Filter(List), - NewRItems = lists:map(fun mod_privacy:item_to_raw/1, - NewList), - mod_privacy:sql_set_privacy_list(ID, NewRItems), - {ok, Default, NewList}; - _ -> ok - end; - _ -> ok - end - end, - ejabberd_odbc:sql_transaction(LServer, F). +-spec broadcast_event(jid(), block() | unblock()) -> ok. +broadcast_event(#jid{luser = LUser, lserver = LServer} = From, Event) -> + BFrom = jid:remove_resource(From), + lists:foreach( + fun(R) -> + To = jid:replace_resource(From, R), + IQ = #iq{type = set, from = BFrom, to = To, + id = <<"push", (p1_rand:get_string())/binary>>, + sub_els = [Event]}, + ejabberd_router:route(IQ) + end, ejabberd_sm:get_user_resources(LUser, LServer)). -make_userlist(Name, List) -> - NeedDb = mod_privacy:is_list_needdb(List), - #userlist{name = Name, list = List, needdb = NeedDb}. - -broadcast_list_update(LUser, LServer, Name, UserList) -> - ejabberd_sm:route(jlib:make_jid(LUser, LServer, - <<"">>), - jlib:make_jid(LUser, LServer, <<"">>), - {broadcast, {privacy_list, UserList, Name}}). - -broadcast_blocklist_event(LUser, LServer, Event) -> - JID = jlib:make_jid(LUser, LServer, <<"">>), - ejabberd_sm:route(JID, JID, - {broadcast, {blocking, Event}}). - -process_blocklist_get(LUser, LServer) -> - case process_blocklist_get(LUser, LServer, - gen_mod:db_type(LServer, mod_privacy)) - of - error -> {error, ?ERR_INTERNAL_SERVER_ERROR}; - List -> - JIDs = list_to_blocklist_jids(List, []), - Items = lists:map(fun (JID) -> - ?DEBUG("JID: ~p", [JID]), - #xmlel{name = <<"item">>, - attrs = - [{<<"jid">>, - jlib:jid_to_string(JID)}], - children = []} - end, - JIDs), - {result, - [#xmlel{name = <<"blocklist">>, - attrs = [{<<"xmlns">>, ?NS_BLOCKING}], - children = Items}]} +-spec process_get(iq()) -> iq(). +process_get(#iq{from = #jid{luser = LUser, lserver = LServer}} = IQ) -> + case mod_privacy:get_user_list(LUser, LServer, default) of + {ok, {_, List}} -> + LJIDs = listitems_to_jids(List, []), + Items = [#block_item{jid = jid:make(J)} || J <- LJIDs], + xmpp:make_iq_result(IQ, #block_list{items = Items}); + error -> + xmpp:make_iq_result(IQ, #block_list{}); + {error, _} -> + err_db_failure(IQ) end. -process_blocklist_get(LUser, LServer, mnesia) -> - case catch mnesia:dirty_read(privacy, {LUser, LServer}) - of - {'EXIT', _Reason} -> error; - [] -> []; - [#privacy{default = Default, lists = Lists}] -> - case lists:keysearch(Default, 1, Lists) of - {value, {_, List}} -> List; - _ -> [] - end - end; -process_blocklist_get(LUser, LServer, riak) -> - case ejabberd_riak:get(privacy, mod_privacy:privacy_schema(), - {LUser, LServer}) of - {ok, #privacy{default = Default, lists = Lists}} -> - case lists:keysearch(Default, 1, Lists) of - {value, {_, List}} -> List; - _ -> [] - end; - {error, notfound} -> - []; - {error, _} -> - error - end; -process_blocklist_get(LUser, LServer, odbc) -> - case catch - mod_privacy:sql_get_default_privacy_list(LUser, LServer) - of - {selected, [<<"name">>], []} -> []; - {selected, [<<"name">>], [[Default]]} -> - case catch mod_privacy:sql_get_privacy_list_data(LUser, - LServer, Default) - of - {selected, - [<<"t">>, <<"value">>, <<"action">>, <<"ord">>, - <<"match_all">>, <<"match_iq">>, <<"match_message">>, - <<"match_presence_in">>, <<"match_presence_out">>], - RItems} -> - lists:map(fun mod_privacy:raw_to_item/1, RItems); - {'EXIT', _} -> error - end; - {'EXIT', _} -> error - end. +-spec err_db_failure(iq()) -> iq(). +err_db_failure(#iq{lang = Lang} = IQ) -> + Txt = ?T("Database failure"), + xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)). + +mod_options(_Host) -> + []. + +mod_doc() -> + #{desc => + [?T("The module implements " + "https://xmpp.org/extensions/xep-0191.html" + "[XEP-0191: Blocking Command]."), "", + ?T("This module depends on _`mod_privacy`_ where " + "all the configuration is performed.")]}. diff --git a/src/mod_bosh.erl b/src/mod_bosh.erl new file mode 100644 index 000000000..5f70a4ea9 --- /dev/null +++ b/src/mod_bosh.erl @@ -0,0 +1,429 @@ +%%%------------------------------------------------------------------- +%%% File : mod_bosh.erl +%%% Author : Evgeniy Khramtsov +%%% Purpose : This module acts as a bridge to ejabberd_bosh which implements +%%% the real stuff, this is to handle the new pluggable architecture +%%% for extending ejabberd's http service. +%%% Created : 20 Jul 2011 by Evgeniy Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_bosh). + +-author('steve@zeank.in-berlin.de'). + +%%-define(ejabberd_debug, true). + +-behaviour(gen_mod). + +-export([start_link/0]). +-export([start/2, stop/1, reload/3, process/2, open_session/2, + close_session/1, find_session/1, clean_cache/1]). + +-export([depends/2, mod_opt_type/1, mod_options/1, mod_doc/0]). + +-include("logger.hrl"). +-include_lib("stdlib/include/ms_transform.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). +-include("ejabberd_http.hrl"). +-include("bosh.hrl"). +-include("translate.hrl"). + +-callback init() -> any(). +-callback open_session(binary(), pid()) -> ok | {error, any()}. +-callback close_session(binary()) -> ok | {error, any()}. +-callback find_session(binary()) -> {ok, pid()} | {error, any()}. +-callback use_cache() -> boolean(). +-callback cache_nodes() -> [node()]. + +-optional_callbacks([use_cache/0, cache_nodes/0]). + +%%%---------------------------------------------------------------------- +%%% API +%%%---------------------------------------------------------------------- +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +process([], #request{method = 'POST', data = <<>>}) -> + ?DEBUG("Bad Request: no data", []), + {400, ?HEADER(?CT_XML), + #xmlel{name = <<"h1">>, attrs = [], + children = [{xmlcdata, <<"400 Bad Request">>}]}}; +process([], + #request{method = 'POST', data = Data, ip = IP, headers = Hdrs}) -> + ?DEBUG("Incoming data: ~p", [Data]), + Type = get_type(Hdrs), + ejabberd_bosh:process_request(Data, IP, Type); +process([], #request{method = 'GET', data = <<>>}) -> + {200, ?HEADER(?CT_XML), get_human_html_xmlel()}; +process([], #request{method = 'OPTIONS', data = <<>>}) -> + {200, ?OPTIONS_HEADER, []}; +process(_Path, Request) -> + ?DEBUG("Bad Request: ~p", [Request]), + {400, ?HEADER(?CT_XML), + #xmlel{name = <<"h1">>, attrs = [], + children = [{xmlcdata, <<"400 Bad Request">>}]}}. + +-spec open_session(binary(), pid()) -> ok | {error, any()}. +open_session(SID, Pid) -> + Mod = gen_mod:ram_db_mod(global, ?MODULE), + case Mod:open_session(SID, Pid) of + ok -> + delete_cache(Mod, SID); + {error, _} = Err -> + Err + end. + +-spec close_session(binary()) -> ok. +close_session(SID) -> + Mod = gen_mod:ram_db_mod(global, ?MODULE), + Mod:close_session(SID), + delete_cache(Mod, SID). + +-spec find_session(binary()) -> {ok, pid()} | error. +find_session(SID) -> + Mod = gen_mod:ram_db_mod(global, ?MODULE), + case use_cache(Mod) of + true -> + ets_cache:lookup( + ?BOSH_CACHE, SID, + fun() -> + case Mod:find_session(SID) of + {ok, Pid} -> {ok, Pid}; + {error, _} -> error + end + end); + false -> + case Mod:find_session(SID) of + {ok, Pid} -> {ok, Pid}; + {error, _} -> error + end + end. + +start(Host, _Opts) -> + Mod = gen_mod:ram_db_mod(Host, ?MODULE), + init_cache(Host, Mod), + Mod:init(), + clean_cache(), + TmpSup = gen_mod:get_module_proc(Host, ?MODULE), + TmpSupSpec = {TmpSup, + {ejabberd_tmp_sup, start_link, [TmpSup, ejabberd_bosh]}, + permanent, infinity, supervisor, [ejabberd_tmp_sup]}, + supervisor:start_child(ejabberd_gen_mod_sup, TmpSupSpec). + +stop(Host) -> + TmpSup = gen_mod:get_module_proc(Host, ?MODULE), + supervisor:terminate_child(ejabberd_gen_mod_sup, TmpSup), + supervisor:delete_child(ejabberd_gen_mod_sup, TmpSup). + +reload(Host, _NewOpts, _OldOpts) -> + Mod = gen_mod:ram_db_mod(global, ?MODULE), + init_cache(Host, Mod), + Mod:init(), + ok. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +get_type(Hdrs) -> + try + {_, S} = lists:keyfind('Content-Type', 1, Hdrs), + [T|_] = str:tokens(S, <<";">>), + [_, <<"json">>] = str:tokens(T, <<"/">>), + json + catch _:_ -> + xml + end. + +depends(_Host, _Opts) -> + []. + +-ifdef(OTP_BELOW_27). +mod_opt_type_json() -> + econf:and_then( + econf:bool(), + fun(false) -> false; + (true) -> + ejabberd:start_app(jiffy), + true + end). +-else. +mod_opt_type_json() -> + econf:bool(). +-endif. + +mod_opt_type(json) -> + mod_opt_type_json(); +mod_opt_type(max_concat) -> + econf:pos_int(unlimited); +mod_opt_type(max_inactivity) -> + econf:timeout(second); +mod_opt_type(max_pause) -> + econf:timeout(second); +mod_opt_type(prebind) -> + econf:bool(); +mod_opt_type(queue_type) -> + econf:queue_type(); +mod_opt_type(ram_db_type) -> + econf:db_type(?MODULE); +mod_opt_type(use_cache) -> + econf:bool(); +mod_opt_type(cache_size) -> + econf:pos_int(infinity); +mod_opt_type(cache_missed) -> + econf:bool(); +mod_opt_type(cache_life_time) -> + econf:timeout(second, infinity). + +-spec mod_options(binary()) -> [{json, boolean()} | + {atom(), term()}]. +mod_options(Host) -> + [{json, false}, + {max_concat, unlimited}, + {max_inactivity, timer:seconds(30)}, + {max_pause, timer:seconds(120)}, + {prebind, false}, + {ram_db_type, ejabberd_config:default_ram_db(Host, ?MODULE)}, + {queue_type, ejabberd_option:queue_type(Host)}, + {use_cache, ejabberd_option:use_cache(Host)}, + {cache_size, ejabberd_option:cache_size(Host)}, + {cache_missed, ejabberd_option:cache_missed(Host)}, + {cache_life_time, ejabberd_option:cache_life_time(Host)}]. + +mod_doc() -> + #{desc => + ?T("This module implements XMPP over BOSH as defined in " + "https://xmpp.org/extensions/xep-0124.html[XEP-0124] and " + "https://xmpp.org/extensions/xep-0206.html[XEP-0206]. BOSH " + "stands for Bidirectional-streams Over Synchronous HTTP. " + "It makes it possible to simulate long lived connections " + "required by XMPP over the HTTP protocol. In practice, " + "this module makes it possible to use XMPP in a browser without " + "WebSocket support and more generally to have a way to use " + "XMPP while having to get through an HTTP proxy."), + opts => + [{json, + #{value => "true | false", + desc => ?T("This option has no effect.")}}, + {max_concat, + #{value => "pos_integer() | infinity", + desc => + ?T("This option limits the number of stanzas that the server " + "will send in a single bosh request. " + "The default value is 'unlimited'.")}}, + {max_inactivity, + #{value => "timeout()", + desc => + ?T("The option defines the maximum inactivity period. " + "The default value is '30' seconds.")}}, + {max_pause, + #{value => "pos_integer()", + desc => + ?T("Indicate the maximum length of a temporary session pause " + "(in seconds) that a client can request. " + "The default value is '120'.")}}, + {prebind, + #{value => "true | false", + desc => + ?T("If enabled, the client can create the session without " + "going through authentication. Basically, it creates a " + "new session with anonymous authentication. " + "The default value is 'false'.")}}, + {queue_type, + #{value => "ram | file", + desc => + ?T("Same as top-level _`queue_type`_ option, but applied to this module only.")}}, + {ram_db_type, + #{value => "mnesia | sql | redis", + desc => + ?T("Same as top-level _`default_ram_db`_ option, " + "but applied to this module only.")}}, + {use_cache, + #{value => "true | false", + desc => + ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, + {cache_size, + #{value => "pos_integer() | infinity", + desc => + ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, + {cache_missed, + #{value => "true | false", + desc => + ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, + {cache_life_time, + #{value => "timeout()", + desc => + ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}], + example => + ["listen:", + " -", + " port: 5222", + " module: ejabberd_c2s", + " -", + " port: 5443", + " module: ejabberd_http", + " request_handlers:", + " /bosh: mod_bosh", + "", + "modules:", + " mod_bosh: {}"]}. + +%%%---------------------------------------------------------------------- +%%% Cache stuff +%%%---------------------------------------------------------------------- +-spec init_cache(binary(), module()) -> ok. +init_cache(Host, Mod) -> + case use_cache(Mod, Host) of + true -> + ets_cache:new(?BOSH_CACHE, cache_opts(Host)); + false -> + ets_cache:delete(?BOSH_CACHE) + end. + +-spec use_cache(module()) -> boolean(). +use_cache(Mod) -> + use_cache(Mod, global). + +-spec use_cache(module(), global | binary()) -> boolean(). +use_cache(Mod, Host) -> + case erlang:function_exported(Mod, use_cache, 0) of + true -> Mod:use_cache(); + false -> mod_bosh_opt:use_cache(Host) + end. + +-spec cache_nodes(module()) -> [node()]. +cache_nodes(Mod) -> + case erlang:function_exported(Mod, cache_nodes, 0) of + true -> Mod:cache_nodes(); + false -> ejabberd_cluster:get_nodes() + end. + +-spec delete_cache(module(), binary()) -> ok. +delete_cache(Mod, SID) -> + case use_cache(Mod) of + true -> + ets_cache:delete(?BOSH_CACHE, SID, cache_nodes(Mod)); + false -> + ok + end. + +-spec cache_opts(binary()) -> [proplists:property()]. +cache_opts(Host) -> + MaxSize = mod_bosh_opt:cache_size(Host), + CacheMissed = mod_bosh_opt:cache_missed(Host), + LifeTime = mod_bosh_opt:cache_life_time(Host), + [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}]. + +-spec clean_cache(node()) -> non_neg_integer(). +clean_cache(Node) -> + ets_cache:filter( + ?BOSH_CACHE, + fun(_, error) -> + false; + (_, {ok, Pid}) -> + node(Pid) /= Node + end). + +-spec clean_cache() -> ok. +clean_cache() -> + ejabberd_cluster:eval_everywhere(?MODULE, clean_cache, [node()]). + +%%%---------------------------------------------------------------------- +%%% Help Web Page +%%%---------------------------------------------------------------------- + +get_human_html_xmlel() -> + Heading = <<"ejabberd ", + (iolist_to_binary(atom_to_list(?MODULE)))/binary>>, + #xmlel{name = <<"html">>, + attrs = + [{<<"xmlns">>, <<"http://www.w3.org/1999/xhtml">>}], + children = + [#xmlel{name = <<"head">>, + children = + [#xmlel{name = <<"title">>, + children = [{xmlcdata, Heading}]}, + #xmlel{name = <<"style">>, + children = [{xmlcdata, get_style_cdata()}]}]}, + #xmlel{name = <<"body">>, + children = + [#xmlel{name = <<"div">>, + attrs = [{<<"class">>, <<"container">>}], + children = get_container_children(Heading)}]}]}. + +get_container_children(Heading) -> + [#xmlel{name = <<"div">>, + attrs = [{<<"class">>, <<"section">>}], + children = + [#xmlel{name = <<"div">>, + attrs = [{<<"class">>, <<"block">>}], + children = + [#xmlel{name = <<"a">>, + attrs = [{<<"href">>, <<"https://www.ejabberd.im">>}], + children = + [#xmlel{name = <<"img">>, + attrs = [{<<"height">>, <<"32">>}, + {<<"src">>, get_image_src()}]}]}]}]}, + #xmlel{name = <<"div">>, + attrs = [{<<"class">>, <<"white section">>}], + children = + [#xmlel{name = <<"div">>, + attrs = [{<<"class">>, <<"block">>}], + children = + [#xmlel{name = <<"h1">>, children = [{xmlcdata, Heading}]}, + #xmlel{name = <<"p">>, children = + [{xmlcdata, <<"An implementation of ">>}, + #xmlel{name = <<"a">>, + attrs = [{<<"href">>, <<"http://xmpp.org/extensions/xep-0206.html">>}], + children = [{xmlcdata, <<"XMPP over BOSH (XEP-0206)">>}]}]}, + #xmlel{name = <<"p">>, children = + [{xmlcdata, <<"This web page is only informative. To " + "use HTTP-Bind you need a Jabber/XMPP " + "client that supports it.">>}]}]}]}, + #xmlel{name = <<"div">>, + attrs = [{<<"class">>, <<"section">>}], + children = + [#xmlel{name = <<"div">>, + attrs = [{<<"class">>, <<"block">>}], + children = + [#xmlel{name = <<"a">>, + attrs = [{<<"href">>, <<"https://www.ejabberd.im">>}, + {<<"title">>, <<"ejabberd XMPP server">>}], + children = [{xmlcdata, <<"ejabberd">>}]}, + {xmlcdata, <<" is maintained by ">>}, + #xmlel{name = <<"a">>, + attrs = [{<<"href">>, <<"https://www.process-one.net">>}, + {<<"title">>, <<"ProcessOne - Leader in Instant Messaging and Push Solutions">>}], + children = [{xmlcdata, <<"ProcessOne">>}]} ]}]} + ]. + +get_style_cdata() -> + case misc:read_css("bosh.css") of + {ok, Data} -> Data; + {error, _} -> <<>> + end. + +get_image_src() -> + case misc:read_img("bosh-logo.png") of + {ok, Img} -> + B64Img = base64:encode(Img), + <<"data:image/png;base64,", B64Img/binary>>; + {error, _} -> + <<>> + end. diff --git a/src/mod_bosh_mnesia.erl b/src/mod_bosh_mnesia.erl new file mode 100644 index 000000000..7ac19df01 --- /dev/null +++ b/src/mod_bosh_mnesia.erl @@ -0,0 +1,265 @@ +%%%------------------------------------------------------------------- +%%% Created : 12 Jan 2017 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_bosh_mnesia). + +-behaviour(gen_server). +-behaviour(mod_bosh). + +%% mod_bosh API +-export([init/0, open_session/2, close_session/1, find_session/1, + use_cache/0]). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3, start_link/0]). + +-include("logger.hrl"). +-include_lib("stdlib/include/ms_transform.hrl"). + +-define(CALL_TIMEOUT, timer:minutes(10)). + +-record(bosh, {sid = <<"">> :: binary(), + timestamp = erlang:timestamp() :: erlang:timestamp(), + pid = self() :: pid()}). + +-record(state, {nodes = #{} :: #{node() => {pid(), reference()}}}). +-type state() :: #state{}. + +%%%=================================================================== +%%% API +%%%=================================================================== +-spec init() -> ok | {error, any()}. +init() -> + Spec = {?MODULE, {?MODULE, start_link, []}, + transient, 5000, worker, [?MODULE]}, + case supervisor:start_child(ejabberd_backend_sup, Spec) of + {ok, _Pid} -> ok; + {error, {already_started, _}} -> ok; + Err -> Err + end. + +-spec start_link() -> {ok, pid()} | {error, any()}. +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +use_cache() -> + false. + +-spec open_session(binary(), pid()) -> ok. +open_session(SID, Pid) -> + Session = #bosh{sid = SID, timestamp = erlang:timestamp(), pid = Pid}, + gen_server:call(?MODULE, {write, Session}, ?CALL_TIMEOUT). + +-spec close_session(binary()) -> ok. +close_session(SID) -> + case mnesia:dirty_read(bosh, SID) of + [Session] -> + gen_server:call(?MODULE, {delete, Session}, ?CALL_TIMEOUT); + [] -> + ok + end. + +-spec find_session(binary()) -> {ok, pid()} | {error, notfound}. +find_session(SID) -> + case mnesia:dirty_read(bosh, SID) of + [#bosh{pid = Pid}] -> + {ok, Pid}; + [] -> + {error, notfound} + end. + +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== +-spec init([]) -> {ok, state()}. +init([]) -> + setup_database(), + multicast({join, node(), self()}), + mnesia:subscribe(system), + {ok, #state{}}. + +-spec handle_call(_, _, state()) -> {reply, ok, state()} | {noreply, state()}. +handle_call({write, Session} = Msg, _From, State) -> + write_session(Session), + multicast(Msg), + {reply, ok, State}; +handle_call({delete, Session} = Msg, _From, State) -> + delete_session(Session), + multicast(Msg), + {reply, ok, State}; +handle_call(Request, From, State) -> + ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), + {noreply, State}. + +-spec handle_cast(_, state()) -> {noreply, state()}. +handle_cast(Msg, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Msg]), + {noreply, State}. + +-spec handle_info(_, state()) -> {noreply, state()}. +handle_info({write, Session}, State) -> + write_session(Session), + {noreply, State}; +handle_info({delete, Session}, State) -> + delete_session(Session), + {noreply, State}; +handle_info({join, Node, Pid}, State) -> + ejabberd_cluster:send(Pid, {joined, node(), self()}), + case maps:find(Node, State#state.nodes) of + {ok, {Pid, _}} -> + ok; + _ -> + ejabberd_cluster:send(Pid, {join, node(), self()}) + end, + {noreply, State}; +handle_info({joined, Node, Pid}, State) -> + case maps:find(Node, State#state.nodes) of + {ok, {Pid, _}} -> + {noreply, State}; + Ret -> + MRef = erlang:monitor(process, {?MODULE, Node}), + Nodes = maps:put(Node, {Pid, MRef}, State#state.nodes), + case Ret of + error -> ejabberd_cluster:send(Pid, {first, self()}); + _ -> ok + end, + {noreply, State#state{nodes = Nodes}} + end; +handle_info({first, From}, State) -> + ejabberd_cluster:send(From, {replica, node(), first_session()}), + {noreply, State}; +handle_info({next, From, Key}, State) -> + ejabberd_cluster:send(From, {replica, node(), next_session(Key)}), + {noreply, State}; +handle_info({replica, _From, '$end_of_table'}, State) -> + {noreply, State}; +handle_info({replica, From, Session}, State) -> + write_session(Session), + ejabberd_cluster:send(From, {next, self(), Session#bosh.sid}), + {noreply, State}; +handle_info({'DOWN', _, process, {?MODULE, _}, _Info}, State) -> + {noreply, State}; +handle_info({mnesia_system_event, {mnesia_down, Node}}, State) -> + Sessions = + ets:select( + bosh, + ets:fun2ms( + fun(#bosh{pid = Pid} = S) when node(Pid) == Node -> + S + end)), + lists:foreach( + fun(S) -> + mnesia:dirty_delete_object(S) + end, Sessions), + Nodes = maps:remove(Node, State#state.nodes), + {noreply, State#state{nodes = Nodes}}; +handle_info({mnesia_system_event, _}, State) -> + {noreply, State}; +handle_info(Info, State) -> + ?WARNING_MSG("Unexpected info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +-spec write_session(#bosh{}) -> ok. +write_session(#bosh{pid = Pid1, sid = SID, timestamp = T1} = S1) -> + case mnesia:dirty_read(bosh, SID) of + [#bosh{pid = Pid2, timestamp = T2} = S2] -> + if Pid1 == Pid2 -> + mnesia:dirty_write(S1); + T1 < T2 -> + ejabberd_cluster:send(Pid2, replaced), + mnesia:dirty_write(S1); + true -> + ejabberd_cluster:send(Pid1, replaced), + mnesia:dirty_write(S2) + end; + [] -> + mnesia:dirty_write(S1) + end. + +-spec delete_session(#bosh{}) -> ok. +delete_session(#bosh{sid = SID, pid = Pid1}) -> + case mnesia:dirty_read(bosh, SID) of + [#bosh{pid = Pid2}] -> + if Pid1 == Pid2 -> + mnesia:dirty_delete(bosh, SID); + true -> + ok + end; + [] -> + ok + end. + +-spec multicast(_) -> ok. +multicast(Msg) -> + lists:foreach( + fun(Node) when Node /= node() -> + ejabberd_cluster:send({?MODULE, Node}, Msg); + (_) -> + ok + end, ejabberd_cluster:get_nodes()). + +setup_database() -> + case catch mnesia:table_info(bosh, attributes) of + [sid, pid] -> + mnesia:delete_table(bosh); + _ -> + ok + end, + ejabberd_mnesia:create(?MODULE, bosh, + [{ram_copies, [node()]}, {local_content, true}, + {attributes, record_info(fields, bosh)}]). + +-spec first_session() -> #bosh{} | '$end_of_table'. +first_session() -> + case mnesia:dirty_first(bosh) of + '$end_of_table' -> + '$end_of_table'; + First -> + read_session(First) + end. + +-spec next_session(binary()) -> #bosh{} | '$end_of_table'. +next_session(Prev) -> + case mnesia:dirty_next(bosh, Prev) of + '$end_of_table' -> + '$end_of_table'; + Next -> + read_session(Next) + end. + +-spec read_session(binary()) -> #bosh{} | '$end_of_table'. +read_session(Key) -> + case mnesia:dirty_read(bosh, Key) of + [#bosh{pid = Pid} = Session] when node(Pid) == node() -> + Session; + _ -> + next_session(Key) + end. diff --git a/src/mod_bosh_opt.erl b/src/mod_bosh_opt.erl new file mode 100644 index 000000000..44b908515 --- /dev/null +++ b/src/mod_bosh_opt.erl @@ -0,0 +1,83 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_bosh_opt). + +-export([cache_life_time/1]). +-export([cache_missed/1]). +-export([cache_size/1]). +-export([json/1]). +-export([max_concat/1]). +-export([max_inactivity/1]). +-export([max_pause/1]). +-export([prebind/1]). +-export([queue_type/1]). +-export([ram_db_type/1]). +-export([use_cache/1]). + +-spec cache_life_time(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +cache_life_time(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_life_time, Opts); +cache_life_time(Host) -> + gen_mod:get_module_opt(Host, mod_bosh, cache_life_time). + +-spec cache_missed(gen_mod:opts() | global | binary()) -> boolean(). +cache_missed(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_missed, Opts); +cache_missed(Host) -> + gen_mod:get_module_opt(Host, mod_bosh, cache_missed). + +-spec cache_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +cache_size(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_size, Opts); +cache_size(Host) -> + gen_mod:get_module_opt(Host, mod_bosh, cache_size). + +-spec json(gen_mod:opts() | global | binary()) -> boolean(). +json(Opts) when is_map(Opts) -> + gen_mod:get_opt(json, Opts); +json(Host) -> + gen_mod:get_module_opt(Host, mod_bosh, json). + +-spec max_concat(gen_mod:opts() | global | binary()) -> 'unlimited' | pos_integer(). +max_concat(Opts) when is_map(Opts) -> + gen_mod:get_opt(max_concat, Opts); +max_concat(Host) -> + gen_mod:get_module_opt(Host, mod_bosh, max_concat). + +-spec max_inactivity(gen_mod:opts() | global | binary()) -> pos_integer(). +max_inactivity(Opts) when is_map(Opts) -> + gen_mod:get_opt(max_inactivity, Opts); +max_inactivity(Host) -> + gen_mod:get_module_opt(Host, mod_bosh, max_inactivity). + +-spec max_pause(gen_mod:opts() | global | binary()) -> pos_integer(). +max_pause(Opts) when is_map(Opts) -> + gen_mod:get_opt(max_pause, Opts); +max_pause(Host) -> + gen_mod:get_module_opt(Host, mod_bosh, max_pause). + +-spec prebind(gen_mod:opts() | global | binary()) -> boolean(). +prebind(Opts) when is_map(Opts) -> + gen_mod:get_opt(prebind, Opts); +prebind(Host) -> + gen_mod:get_module_opt(Host, mod_bosh, prebind). + +-spec queue_type(gen_mod:opts() | global | binary()) -> 'file' | 'ram'. +queue_type(Opts) when is_map(Opts) -> + gen_mod:get_opt(queue_type, Opts); +queue_type(Host) -> + gen_mod:get_module_opt(Host, mod_bosh, queue_type). + +-spec ram_db_type(gen_mod:opts() | global | binary()) -> atom(). +ram_db_type(Opts) when is_map(Opts) -> + gen_mod:get_opt(ram_db_type, Opts); +ram_db_type(Host) -> + gen_mod:get_module_opt(Host, mod_bosh, ram_db_type). + +-spec use_cache(gen_mod:opts() | global | binary()) -> boolean(). +use_cache(Opts) when is_map(Opts) -> + gen_mod:get_opt(use_cache, Opts); +use_cache(Host) -> + gen_mod:get_module_opt(Host, mod_bosh, use_cache). + diff --git a/src/mod_bosh_redis.erl b/src/mod_bosh_redis.erl new file mode 100644 index 000000000..efd05a7ba --- /dev/null +++ b/src/mod_bosh_redis.erl @@ -0,0 +1,150 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_bosh_redis.erl +%%% Author : Evgeny Khramtsov +%%% Purpose : +%%% Created : 28 Mar 2017 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2017-2025 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_bosh_redis). +-behaviour(mod_bosh). +-behaviour(gen_server). + +%% API +-export([init/0, open_session/2, close_session/1, find_session/1, + cache_nodes/0]). +%% gen_server callbacks +-export([init/1, handle_cast/2, handle_call/3, handle_info/2, + terminate/2, code_change/3, start_link/0]). + +-include("logger.hrl"). +-include("bosh.hrl"). + +-record(state, {}). + +-define(BOSH_KEY, <<"ejabberd:bosh">>). + +%%%=================================================================== +%%% API +%%%=================================================================== +init() -> + Spec = {?MODULE, {?MODULE, start_link, []}, + transient, 5000, worker, [?MODULE]}, + case supervisor:start_child(ejabberd_backend_sup, Spec) of + {ok, _Pid} -> ok; + Err -> Err + end. + +-spec start_link() -> {ok, pid()} | {error, any()}. +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +open_session(SID, Pid) -> + PidBin = term_to_binary(Pid), + case ejabberd_redis:multi( + fun() -> + ejabberd_redis:hset(?BOSH_KEY, SID, PidBin), + ejabberd_redis:publish(?BOSH_KEY, SID) + end) of + {ok, _} -> + ok; + {error, _} -> + {error, db_failure} + end. + +close_session(SID) -> + case ejabberd_redis:multi( + fun() -> + ejabberd_redis:hdel(?BOSH_KEY, [SID]), + ejabberd_redis:publish(?BOSH_KEY, SID) + end) of + {ok, _} -> + ok; + {error, _} -> + {error, db_failure} + end. + +find_session(SID) -> + case ejabberd_redis:hget(?BOSH_KEY, SID) of + {ok, undefined} -> + {error, notfound}; + {ok, Pid} -> + try + {ok, binary_to_term(Pid)} + catch _:badarg -> + ?ERROR_MSG("Malformed data in redis (key = '~ts'): ~p", + [SID, Pid]), + {error, db_failure} + end; + {error, _} -> + {error, db_failure} + end. + +cache_nodes() -> + [node()]. + +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== +init([]) -> + clean_table(), + {ok, #state{}}. + +handle_call(Request, From, State) -> + ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), + {noreply, State}. + +handle_cast(Msg, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Msg]), + {noreply, State}. + +handle_info({redis_message, ?BOSH_KEY, SID}, State) -> + ets_cache:delete(?BOSH_CACHE, SID), + {noreply, State}; +handle_info(Info, State) -> + ?WARNING_MSG("Unexpected info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +clean_table() -> + ?DEBUG("Cleaning Redis BOSH sessions...", []), + case ejabberd_redis:hgetall(?BOSH_KEY) of + {ok, Vals} -> + ejabberd_redis:multi( + fun() -> + lists:foreach( + fun({SID, Pid}) when node(Pid) == node() -> + ejabberd_redis:hdel(?BOSH_KEY, [SID]); + (_) -> + ok + end, Vals) + end), + ok; + {error, _} -> + ?ERROR_MSG("Failed to clean bosh sessions in redis", []) + end. diff --git a/src/mod_bosh_sql.erl b/src/mod_bosh_sql.erl new file mode 100644 index 000000000..29549ed49 --- /dev/null +++ b/src/mod_bosh_sql.erl @@ -0,0 +1,106 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_bosh_sql.erl +%%% Author : Evgeny Khramtsov +%%% Purpose : +%%% Created : 28 Mar 2017 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2017-2025 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_bosh_sql). +-behaviour(mod_bosh). + + +%% API +-export([init/0, open_session/2, close_session/1, find_session/1]). +-export([sql_schemas/0]). + +-include("logger.hrl"). +-include("ejabberd_sql_pt.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init() -> + ejabberd_sql_schema:update_schema( + ejabberd_config:get_myname(), ?MODULE, sql_schemas()), + Node = erlang:atom_to_binary(node(), latin1), + ?DEBUG("Cleaning SQL 'bosh' table...", []), + case ejabberd_sql:sql_query( + ejabberd_config:get_myname(), ?SQL("delete from bosh where node=%(Node)s")) of + {updated, _} -> + ok; + Err -> + ?ERROR_MSG("Failed to clean 'route' table: ~p", [Err]), + Err + end. + +sql_schemas() -> + [#sql_schema{ + version = 1, + tables = + [#sql_table{ + name = <<"bosh">>, + columns = + [#sql_column{name = <<"sid">>, type = text}, + #sql_column{name = <<"node">>, type = text}, + #sql_column{name = <<"pid">>, type = text}], + indices = [#sql_index{ + columns = [<<"sid">>], + unique = true}]}]}]. + +open_session(SID, Pid) -> + PidS = misc:encode_pid(Pid), + Node = erlang:atom_to_binary(node(Pid), latin1), + case ?SQL_UPSERT(ejabberd_config:get_myname(), "bosh", + ["!sid=%(SID)s", + "node=%(Node)s", + "pid=%(PidS)s"]) of + ok -> + ok; + _Err -> + {error, db_failure} + end. + +close_session(SID) -> + case ejabberd_sql:sql_query( + ejabberd_config:get_myname(), ?SQL("delete from bosh where sid=%(SID)s")) of + {updated, _} -> + ok; + _Err -> + {error, db_failure} + end. + +find_session(SID) -> + case ejabberd_sql:sql_query( + ejabberd_config:get_myname(), + ?SQL("select @(pid)s, @(node)s from bosh where sid=%(SID)s")) of + {selected, [{Pid, Node}]} -> + try {ok, misc:decode_pid(Pid, Node)} + catch _:{bad_node, _} -> {error, notfound} + end; + {selected, []} -> + {error, notfound}; + _Err -> + {error, db_failure} + end. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== diff --git a/src/mod_caps.erl b/src/mod_caps.erl index 5c6d041f8..79ae36edc 100644 --- a/src/mod_caps.erl +++ b/src/mod_caps.erl @@ -5,7 +5,7 @@ %%% Created : 7 Oct 2006 by Magnus Henoch %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -17,10 +17,9 @@ %%% 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., 59 Temple Place, Suite 330, Boston, MA -%%% 02111-1307 USA +%%% 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. %%% %%% 2009, improvements from ProcessOne to support correct PEP handling %%% through s2s, use less memory, and speedup global caps handling @@ -30,170 +29,165 @@ -author('henoch@dtek.chalmers.se'). +-protocol({xep, 115, '1.5', '2.1.4', "complete", ""}). + -behaviour(gen_server). -behaviour(gen_mod). --export([read_caps/1, caps_stream_features/2, +-export([read_caps/1, list_features/1, caps_stream_features/2, disco_features/5, disco_identity/5, disco_info/5, get_features/2, export/1, import_info/0, import/5, - import_start/2, import_stop/2]). + get_user_caps/2, import_start/2, import_stop/2, + compute_disco_hash/2, is_valid_node/1]). %% gen_mod callbacks --export([start/2, start_link/2, stop/1]). +-export([start/2, stop/1, reload/3, depends/2]). %% gen_server callbacks -export([init/1, handle_info/2, handle_call/3, handle_cast/2, terminate/2, code_change/3]). -%% hook handlers --export([user_send_packet/3, user_receive_packet/4, - c2s_presence_in/2, c2s_filter_packet/6, - c2s_broadcast_recipients/6]). +-export([user_send_packet/1, user_receive_packet/1, + c2s_presence_in/2, c2s_copy_session/2, + mod_opt_type/1, mod_options/1, mod_doc/0]). --include("ejabberd.hrl"). -include("logger.hrl"). --include("jlib.hrl"). - --define(PROCNAME, ejabberd_mod_caps). +-include_lib("xmpp/include/xmpp.hrl"). +-include("mod_caps.hrl"). +-include("translate.hrl"). -define(BAD_HASH_LIFETIME, 600). --record(caps, -{ - node = <<"">> :: binary(), - version = <<"">> :: binary(), - hash = <<"">> :: binary(), - exts = [] :: [binary()] -}). - --type caps() :: #caps{}. - --export_type([caps/0]). - --record(caps_features, -{ - node_pair = {<<"">>, <<"">>} :: {binary(), binary()}, - features = [] :: [binary()] | pos_integer() -}). - -record(state, {host = <<"">> :: binary()}). -start_link(Host, Opts) -> - Proc = gen_mod:get_module_proc(Host, ?PROCNAME), - gen_server:start_link({local, Proc}, ?MODULE, - [Host, Opts], []). +-type digest_type() :: md5 | sha | sha224 | sha256 | sha384 | sha512. + +-callback init(binary(), gen_mod:opts()) -> any(). +-callback import(binary(), {binary(), binary()}, [binary() | pos_integer()]) -> ok. +-callback caps_read(binary(), {binary(), binary()}) -> + {ok, non_neg_integer() | [binary()]} | error. +-callback caps_write(binary(), {binary(), binary()}, + non_neg_integer() | [binary()]) -> any(). +-callback use_cache(binary()) -> boolean(). + +-optional_callbacks([use_cache/1]). start(Host, Opts) -> - Proc = gen_mod:get_module_proc(Host, ?PROCNAME), - ChildSpec = {Proc, {?MODULE, start_link, [Host, Opts]}, - transient, 1000, worker, [?MODULE]}, - supervisor:start_child(ejabberd_sup, ChildSpec). + gen_mod:start_child(?MODULE, Host, Opts). stop(Host) -> - Proc = gen_mod:get_module_proc(Host, ?PROCNAME), - gen_server:call(Proc, stop), - supervisor:terminate_child(ejabberd_sup, Proc), - supervisor:delete_child(ejabberd_sup, Proc). + gen_mod:stop_child(?MODULE, Host). +-spec get_features(binary(), nothing | caps()) -> [binary()]. get_features(_Host, nothing) -> []; get_features(Host, #caps{node = Node, version = Version, exts = Exts}) -> SubNodes = [Version | Exts], - lists:foldl(fun (SubNode, Acc) -> - NodePair = {Node, SubNode}, - case cache_tab:lookup(caps_features, NodePair, - caps_read_fun(Host, NodePair)) - of - {ok, Features} when is_list(Features) -> - Features ++ Acc; - _ -> Acc - end - end, - [], SubNodes). + Mod = gen_mod:db_mod(Host, ?MODULE), + lists:foldl( + fun(SubNode, Acc) -> + NodePair = {Node, SubNode}, + Res = case use_cache(Mod, Host) of + true -> + ets_cache:lookup(caps_features_cache, NodePair, + caps_read_fun(Host, NodePair)); + false -> + Mod:caps_read(Host, NodePair) + end, + case Res of + {ok, Features} when is_list(Features) -> + Features ++ Acc; + _ -> Acc + end + end, [], SubNodes). --spec read_caps([xmlel()]) -> nothing | caps(). +-spec list_features(ejabberd_c2s:state()) -> [{ljid(), caps()}]. +list_features(C2SState) -> + Rs = maps:get(caps_resources, C2SState, gb_trees:empty()), + gb_trees:to_list(Rs). -read_caps(Els) -> read_caps(Els, nothing). - -read_caps([#xmlel{name = <<"c">>, attrs = Attrs} - | Tail], - Result) -> - case xml:get_attr_s(<<"xmlns">>, Attrs) of - ?NS_CAPS -> - Node = xml:get_attr_s(<<"node">>, Attrs), - Version = xml:get_attr_s(<<"ver">>, Attrs), - Hash = xml:get_attr_s(<<"hash">>, Attrs), - Exts = str:tokens(xml:get_attr_s(<<"ext">>, Attrs), - <<" ">>), - read_caps(Tail, - #caps{node = Node, hash = Hash, version = Version, - exts = Exts}); - _ -> read_caps(Tail, Result) - end; -read_caps([#xmlel{name = <<"x">>, attrs = Attrs} - | Tail], - Result) -> - case xml:get_attr_s(<<"xmlns">>, Attrs) of - ?NS_MUC_USER -> nothing; - _ -> read_caps(Tail, Result) - end; -read_caps([_ | Tail], Result) -> - read_caps(Tail, Result); -read_caps([], Result) -> Result. - -user_send_packet(#jid{luser = User, lserver = Server} = From, - #jid{luser = User, lserver = Server, - lresource = <<"">>}, - #xmlel{name = <<"presence">>, attrs = Attrs, - children = Els} = Pkt) -> - Type = xml:get_attr_s(<<"type">>, Attrs), - if Type == <<"">>; Type == <<"available">> -> - case read_caps(Els) of - nothing -> ok; - #caps{version = Version, exts = Exts} = Caps -> - feature_request(Server, From, Caps, [Version | Exts]) - end; - true -> ok - end, - Pkt; -user_send_packet( _From, _To, Pkt) -> - Pkt. - -user_receive_packet(#jid{lserver = Server}, - From, _To, - #xmlel{name = <<"presence">>, attrs = Attrs, - children = Els} = Pkt) -> - Type = xml:get_attr_s(<<"type">>, Attrs), - IsRemote = not lists:member(From#jid.lserver, ?MYHOSTS), - if IsRemote and - ((Type == <<"">>) or (Type == <<"available">>)) -> - case read_caps(Els) of - nothing -> ok; - #caps{version = Version, exts = Exts} = Caps -> - feature_request(Server, From, Caps, [Version | Exts]) - end; - true -> ok - end, - Pkt; -user_receive_packet( _JID, _From, _To, Pkt) -> - Pkt. - --spec caps_stream_features([xmlel()], binary()) -> [xmlel()]. - -caps_stream_features(Acc, MyHost) -> - case make_my_disco_hash(MyHost) of - <<"">> -> Acc; - Hash -> - [#xmlel{name = <<"c">>, - attrs = - [{<<"xmlns">>, ?NS_CAPS}, {<<"hash">>, <<"sha-1">>}, - {<<"node">>, ?EJABBERD_URI}, {<<"ver">>, Hash}], - children = []} - | Acc] +-spec get_user_caps(jid() | ljid(), ejabberd_c2s:state()) -> {ok, caps()} | error. +get_user_caps(JID, C2SState) -> + Rs = maps:get(caps_resources, C2SState, gb_trees:empty()), + LJID = jid:tolower(JID), + case gb_trees:lookup(LJID, Rs) of + {value, Caps} -> + {ok, Caps}; + none -> + error end. +-spec read_caps(#presence{}) -> nothing | caps(). +read_caps(Presence) -> + case xmpp:get_subtag(Presence, #caps{}) of + false -> nothing; + Caps -> Caps + end. + +-spec user_send_packet({stanza(), ejabberd_c2s:state()}) -> {stanza(), ejabberd_c2s:state()}. +user_send_packet({#presence{type = available, + from = #jid{luser = U, lserver = LServer} = From, + to = #jid{luser = U, lserver = LServer, + lresource = <<"">>}} = Pkt, + #{jid := To} = State}) -> + case read_caps(Pkt) of + nothing -> ok; + #caps{version = Version, exts = Exts} = Caps -> + feature_request(LServer, From, To, Caps, [Version | Exts]) + end, + {Pkt, State}; +user_send_packet(Acc) -> + Acc. + +-spec user_receive_packet({stanza(), ejabberd_c2s:state()}) -> {stanza(), ejabberd_c2s:state()}. +user_receive_packet({#presence{from = From, type = available} = Pkt, + #{lserver := LServer, jid := To} = State}) -> + IsRemote = case From#jid.lresource of + % Don't store caps for presences sent by our muc rooms + <<>> -> + try ejabberd_router:host_of_route(From#jid.lserver) of + MaybeMuc -> + not lists:member(From#jid.lserver, + gen_mod:get_module_opt_hosts(MaybeMuc, mod_muc)) + catch error:{unregistered_route, _} -> + true + end; + _ -> + not ejabberd_router:is_my_host(From#jid.lserver) + end, + if IsRemote -> + case read_caps(Pkt) of + nothing -> ok; + #caps{version = Version, exts = Exts} = Caps -> + feature_request(LServer, To, From, Caps, [Version | Exts]) + end; + true -> ok + end, + {Pkt, State}; +user_receive_packet(Acc) -> + Acc. + +-spec caps_stream_features([xmpp_element()], binary()) -> [xmpp_element()]. +caps_stream_features(Acc, MyHost) -> + case gen_mod:is_loaded(MyHost, ?MODULE) of + true -> + case make_my_disco_hash(MyHost) of + <<"">> -> + Acc; + Hash -> + [#caps{hash = <<"sha-1">>, node = ejabberd_config:get_uri(), + version = Hash} | Acc] + end; + false -> + Acc + end. + +-spec disco_features({error, stanza_error()} | {result, [binary()]} | empty, + jid(), jid(), + binary(), binary()) -> + {error, stanza_error()} | {result, [binary()]} | empty. disco_features(Acc, From, To, Node, Lang) -> case is_valid_node(Node) of true -> @@ -204,6 +198,9 @@ disco_features(Acc, From, To, Node, Lang) -> Acc end. +-spec disco_identity([identity()], jid(), jid(), + binary(), binary()) -> + [identity()]. disco_identity(Acc, From, To, Node, Lang) -> case is_valid_node(Node) of true -> @@ -214,137 +211,109 @@ disco_identity(Acc, From, To, Node, Lang) -> Acc end. -disco_info(Acc, Host, Module, Node, Lang) -> +-spec disco_info([xdata()], binary(), module(), binary(), binary()) -> [xdata()]; + ([xdata()], jid(), jid(), binary(), binary()) -> [xdata()]. +disco_info(Acc, Host, Module, Node, Lang) when is_atom(Module) -> case is_valid_node(Node) of true -> ejabberd_hooks:run_fold(disco_info, Host, [], [Host, Module, <<"">>, Lang]); false -> Acc - end. + end; +disco_info(Acc, _, _, _Node, _Lang) -> + Acc. +-spec c2s_presence_in(ejabberd_c2s:state(), presence()) -> ejabberd_c2s:state(). c2s_presence_in(C2SState, - {From, To, {_, _, Attrs, Els}}) -> - Type = xml:get_attr_s(<<"type">>, Attrs), - Subscription = ejabberd_c2s:get_subscription(From, - C2SState), - Insert = ((Type == <<"">>) or (Type == <<"available">>)) - and ((Subscription == both) or (Subscription == to)), - Delete = (Type == <<"unavailable">>) or - (Type == <<"error">>), - if Insert or Delete -> - LFrom = jlib:jid_tolower(From), - Rs = case ejabberd_c2s:get_aux_field(caps_resources, - C2SState) - of - {ok, Rs1} -> Rs1; - error -> gb_trees:empty() - end, - Caps = read_caps(Els), - {CapsUpdated, NewRs} = case Caps of - nothing when Insert == true -> {false, Rs}; - _ when Insert == true -> - case gb_trees:lookup(LFrom, Rs) of - {value, Caps} -> {false, Rs}; - none -> - {true, - gb_trees:insert(LFrom, Caps, - Rs)}; - _ -> - {true, - gb_trees:update(LFrom, Caps, Rs)} - end; - _ -> {false, gb_trees:delete_any(LFrom, Rs)} - end, - if CapsUpdated -> - ejabberd_hooks:run(caps_update, To#jid.lserver, - [From, To, - get_features(To#jid.lserver, Caps)]); - true -> ok - end, - ejabberd_c2s:set_aux_field(caps_resources, NewRs, - C2SState); - true -> C2SState + #presence{from = From, to = To, type = Type} = Presence) -> + ToSelf = (From#jid.luser == To#jid.luser) + andalso (From#jid.lserver == To#jid.lserver), + Caps = read_caps(Presence), + Operation = + case {Type, ToSelf, Caps} of + {unavailable, _, _} -> delete; + {error, _, _} -> delete; + {available, _, nothing} -> skip; + {available, true, _} -> insert; + {available, _, _} -> + {Subscription, _, _} = ejabberd_hooks:run_fold( + roster_get_jid_info, To#jid.lserver, + {none, none, []}, + [To#jid.luser, To#jid.lserver, From]), + case Subscription of + from -> insert; + both -> insert; + _ -> skip + end; + _ -> + skip + end, + case Operation of + skip -> + C2SState; + delete -> + LFrom = jid:tolower(From), + Rs = maps:get(caps_resources, C2SState, gb_trees:empty()), + C2SState#{caps_resources => gb_trees:delete_any(LFrom, Rs)}; + insert -> + LFrom = jid:tolower(From), + Rs = maps:get(caps_resources, C2SState, gb_trees:empty()), + NewRs = case gb_trees:lookup(LFrom, Rs) of + {value, Caps} -> Rs; + none -> + ejabberd_hooks:run(caps_add, To#jid.lserver, + [From, To, + get_features(To#jid.lserver, Caps)]), + gb_trees:insert(LFrom, Caps, Rs); + _ -> + ejabberd_hooks:run(caps_update, To#jid.lserver, + [From, To, + get_features(To#jid.lserver, Caps)]), + gb_trees:update(LFrom, Caps, Rs) + end, + C2SState#{caps_resources => NewRs} end. -c2s_filter_packet(InAcc, Host, C2SState, {pep_message, Feature}, To, _Packet) -> - case ejabberd_c2s:get_aux_field(caps_resources, C2SState) of - {ok, Rs} -> - LTo = jlib:jid_tolower(To), - case gb_trees:lookup(LTo, Rs) of - {value, Caps} -> - Drop = not lists:member(Feature, get_features(Host, Caps)), - {stop, Drop}; - none -> - {stop, true} - end; - _ -> InAcc - end; -c2s_filter_packet(Acc, _, _, _, _, _) -> Acc. +-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. -c2s_broadcast_recipients(InAcc, Host, C2SState, - {pep_message, Feature}, _From, _Packet) -> - case ejabberd_c2s:get_aux_field(caps_resources, - C2SState) - of - {ok, Rs} -> - gb_trees_fold(fun (USR, Caps, Acc) -> - case lists:member(Feature, - get_features(Host, Caps)) - of - true -> [USR | Acc]; - false -> Acc - end - end, - InAcc, Rs); - _ -> InAcc - end; -c2s_broadcast_recipients(Acc, _, _, _, _, _) -> Acc. +-spec depends(binary(), gen_mod:opts()) -> [{module(), hard | soft}]. +depends(_Host, _Opts) -> + []. -init_db(mnesia, _Host) -> - case catch mnesia:table_info(caps_features, storage_type) of - {'EXIT', _} -> - ok; - disc_only_copies -> - ok; - _ -> - mnesia:delete_table(caps_features) +reload(Host, NewOpts, OldOpts) -> + NewMod = gen_mod:db_mod(NewOpts, ?MODULE), + OldMod = gen_mod:db_mod(OldOpts, ?MODULE), + if OldMod /= NewMod -> + NewMod:init(Host, NewOpts); + true -> + ok end, - mnesia:create_table(caps_features, - [{disc_only_copies, [node()]}, - {local_content, true}, - {attributes, - record_info(fields, caps_features)}]), - update_table(), - mnesia:add_table_copy(caps_features, node(), - disc_only_copies); -init_db(_, _) -> - ok. + init_cache(NewMod, Host, NewOpts). -init([Host, Opts]) -> - init_db(gen_mod:db_type(Opts), Host), - MaxSize = gen_mod:get_opt(cache_size, Opts, - fun(I) when is_integer(I), I>0 -> I end, - 1000), - LifeTime = gen_mod:get_opt(cache_life_time, Opts, - fun(I) when is_integer(I), I>0 -> I end, - timer:hours(24) div 1000), - cache_tab:new(caps_features, - [{max_size, MaxSize}, {life_time, LifeTime}]), +init([Host|_]) -> + process_flag(trap_exit, true), + Opts = gen_mod:get_module_opts(Host, ?MODULE), + Mod = gen_mod:db_mod(Opts, ?MODULE), + init_cache(Mod, Host, Opts), + Mod:init(Host, Opts), ejabberd_hooks:add(c2s_presence_in, Host, ?MODULE, c2s_presence_in, 75), - ejabberd_hooks:add(c2s_filter_packet, Host, ?MODULE, - c2s_filter_packet, 75), - ejabberd_hooks:add(c2s_broadcast_recipients, Host, - ?MODULE, c2s_broadcast_recipients, 75), ejabberd_hooks:add(user_send_packet, Host, ?MODULE, user_send_packet, 75), ejabberd_hooks:add(user_receive_packet, Host, ?MODULE, user_receive_packet, 75), - ejabberd_hooks:add(c2s_stream_features, Host, ?MODULE, + ejabberd_hooks:add(c2s_post_auth_features, Host, ?MODULE, caps_stream_features, 75), - ejabberd_hooks:add(s2s_stream_features, Host, ?MODULE, + 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, @@ -355,29 +324,35 @@ init([Host, Opts]) -> handle_call(stop, _From, State) -> {stop, normal, ok, State}; -handle_call(_Req, _From, State) -> - {reply, {error, badarg}, State}. +handle_call(Request, From, State) -> + ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), + {noreply, State}. -handle_cast(_Msg, State) -> {noreply, State}. +handle_cast(Msg, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Msg]), + {noreply, State}. -handle_info(_Info, State) -> {noreply, State}. +handle_info({iq_reply, IQReply, {Host, From, To, Caps, SubNodes}}, State) -> + feature_response(IQReply, Host, From, To, Caps, SubNodes), + {noreply, State}; +handle_info(Info, State) -> + ?WARNING_MSG("Unexpected info: ~p", [Info]), + {noreply, State}. terminate(_Reason, State) -> Host = State#state.host, ejabberd_hooks:delete(c2s_presence_in, Host, ?MODULE, c2s_presence_in, 75), - ejabberd_hooks:delete(c2s_filter_packet, Host, ?MODULE, - c2s_filter_packet, 75), - ejabberd_hooks:delete(c2s_broadcast_recipients, Host, - ?MODULE, c2s_broadcast_recipients, 75), ejabberd_hooks:delete(user_send_packet, Host, ?MODULE, user_send_packet, 75), ejabberd_hooks:delete(user_receive_packet, Host, ?MODULE, user_receive_packet, 75), - ejabberd_hooks:delete(c2s_stream_features, Host, + ejabberd_hooks:delete(c2s_post_auth_features, Host, ?MODULE, caps_stream_features, 75), - ejabberd_hooks:delete(s2s_stream_features, Host, + 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, @@ -388,131 +363,83 @@ terminate(_Reason, State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -feature_request(Host, From, Caps, +-spec feature_request(binary(), jid(), jid(), caps(), [binary()]) -> any(). +feature_request(Host, From, To, Caps, [SubNode | Tail] = SubNodes) -> Node = Caps#caps.node, NodePair = {Node, SubNode}, - case cache_tab:lookup(caps_features, NodePair, - caps_read_fun(Host, NodePair)) - of - {ok, Fs} when is_list(Fs) -> - feature_request(Host, From, Caps, Tail); - Other -> - NeedRequest = case Other of - {ok, TS} -> now_ts() >= TS + (?BAD_HASH_LIFETIME); - _ -> true - end, - if NeedRequest -> - IQ = #iq{type = get, xmlns = ?NS_DISCO_INFO, - sub_el = - [#xmlel{name = <<"query">>, - attrs = - [{<<"xmlns">>, ?NS_DISCO_INFO}, - {<<"node">>, - <>}], - children = []}]}, - cache_tab:insert(caps_features, NodePair, now_ts(), - caps_write_fun(Host, NodePair, now_ts())), - F = fun (IQReply) -> - feature_response(IQReply, Host, From, Caps, - SubNodes) - end, - ejabberd_local:route_iq(jlib:make_jid(<<"">>, Host, - <<"">>), - From, IQ, F); - true -> feature_request(Host, From, Caps, Tail) - end + Mod = gen_mod:db_mod(Host, ?MODULE), + Res = case use_cache(Mod, Host) of + true -> + ets_cache:lookup(caps_features_cache, NodePair, + caps_read_fun(Host, NodePair)); + false -> + Mod:caps_read(Host, NodePair) + end, + case Res of + {ok, Fs} when is_list(Fs) -> + feature_request(Host, From, To, Caps, Tail); + _ -> + LTo = jid:tolower(To), + case ets_cache:insert_new(caps_requests_cache, {LTo, NodePair}, ok) of + true -> + IQ = #iq{type = get, + from = From, + to = To, + sub_els = [#disco_info{node = <>}]}, + ejabberd_router:route_iq( + IQ, {Host, From, To, Caps, SubNodes}, + gen_mod:get_module_proc(Host, ?MODULE)); + false -> + ok + end, + feature_request(Host, From, To, Caps, Tail) end; -feature_request(_Host, _From, _Caps, []) -> ok. +feature_request(_Host, _From, _To, _Caps, []) -> ok. -feature_response(#iq{type = result, - sub_el = [#xmlel{children = Els}]}, - Host, From, Caps, [SubNode | SubNodes]) -> +-spec feature_response(iq(), binary(), jid(), jid(), caps(), [binary()]) -> any(). +feature_response(#iq{type = result, sub_els = [El]}, + Host, From, To, Caps, [SubNode | SubNodes]) -> NodePair = {Caps#caps.node, SubNode}, - case check_hash(Caps, Els) of - true -> - Features = lists:flatmap(fun (#xmlel{name = - <<"feature">>, - attrs = FAttrs}) -> - [xml:get_attr_s(<<"var">>, FAttrs)]; - (_) -> [] - end, - Els), - cache_tab:insert(caps_features, NodePair, - Features, - caps_write_fun(Host, NodePair, Features)); - false -> ok + try + DiscoInfo = xmpp:decode(El), + case check_hash(Caps, DiscoInfo) of + true -> + Features = DiscoInfo#disco_info.features, + LServer = jid:nameprep(Host), + Mod = gen_mod:db_mod(LServer, ?MODULE), + case Mod:caps_write(LServer, NodePair, Features) of + ok -> + case use_cache(Mod, LServer) of + true -> + ets_cache:delete(caps_features_cache, NodePair); + false -> + ok + end; + {error, _} -> + ok + end; + false -> ok + end + catch _:{xmpp_codec, _Why} -> + ok end, - feature_request(Host, From, Caps, SubNodes); -feature_response(_IQResult, Host, From, Caps, + feature_request(Host, From, To, Caps, SubNodes); +feature_response(_IQResult, Host, From, To, Caps, [_SubNode | SubNodes]) -> - feature_request(Host, From, Caps, SubNodes). + feature_request(Host, From, To, Caps, SubNodes). +-spec caps_read_fun(binary(), {binary(), binary()}) + -> fun(() -> {ok, [binary()] | non_neg_integer()} | error). caps_read_fun(Host, Node) -> - LServer = jlib:nameprep(Host), - DBType = gen_mod:db_type(LServer, ?MODULE), - caps_read_fun(LServer, Node, DBType). - -caps_read_fun(_LServer, Node, mnesia) -> - fun () -> - case mnesia:dirty_read({caps_features, Node}) of - [#caps_features{features = Features}] -> {ok, Features}; - _ -> error - end - end; -caps_read_fun(_LServer, Node, riak) -> - fun() -> - case ejabberd_riak:get(caps_features, caps_features_schema(), Node) of - {ok, #caps_features{features = Features}} -> {ok, Features}; - _ -> error - end - end; -caps_read_fun(LServer, {Node, SubNode}, odbc) -> - fun() -> - SNode = ejabberd_odbc:escape(Node), - SSubNode = ejabberd_odbc:escape(SubNode), - case ejabberd_odbc:sql_query( - LServer, [<<"select feature from caps_features where ">>, - <<"node='">>, SNode, <<"' and subnode='">>, - SSubNode, <<"';">>]) of - {selected, [<<"feature">>], [[H]|_] = Fs} -> - case catch jlib:binary_to_integer(H) of - Int when is_integer(Int), Int>=0 -> - {ok, Int}; - _ -> - {ok, lists:flatten(Fs)} - end; - _ -> - error - end - end. - -caps_write_fun(Host, Node, Features) -> - LServer = jlib:nameprep(Host), - DBType = gen_mod:db_type(LServer, ?MODULE), - caps_write_fun(LServer, Node, Features, DBType). - -caps_write_fun(_LServer, Node, Features, mnesia) -> - fun () -> - mnesia:dirty_write(#caps_features{node_pair = Node, - features = Features}) - end; -caps_write_fun(_LServer, Node, Features, riak) -> - fun () -> - ejabberd_riak:put(#caps_features{node_pair = Node, - features = Features}, - caps_features_schema()) - end; -caps_write_fun(LServer, NodePair, Features, odbc) -> - fun () -> - ejabberd_odbc:sql_transaction( - LServer, - sql_write_features_t(NodePair, Features)) - end. + LServer = jid:nameprep(Host), + Mod = gen_mod:db_mod(LServer, ?MODULE), + fun() -> Mod:caps_read(LServer, Node) end. +-spec make_my_disco_hash(binary()) -> binary(). make_my_disco_hash(Host) -> - JID = jlib:make_jid(<<"">>, Host, <<"">>), + JID = jid:make(Host), case {ejabberd_hooks:run_fold(disco_local_features, Host, empty, [JID, JID, <<"">>, <<"">>]), ejabberd_hooks:run_fold(disco_local_identity, Host, [], @@ -521,204 +448,122 @@ make_my_disco_hash(Host) -> [Host, undefined, <<"">>, <<"">>])} of {{result, Features}, Identities, Info} -> - Feats = lists:map(fun ({{Feat, _Host}}) -> - #xmlel{name = <<"feature">>, - attrs = [{<<"var">>, Feat}], - children = []}; - (Feat) -> - #xmlel{name = <<"feature">>, - attrs = [{<<"var">>, Feat}], - children = []} + Feats = lists:map(fun ({{Feat, _Host}}) -> Feat; + (Feat) -> Feat end, Features), - make_disco_hash(Identities ++ Info ++ Feats, sha1); + DiscoInfo = #disco_info{identities = Identities, + features = Feats, + xdata = Info}, + compute_disco_hash(DiscoInfo, sha); _Err -> <<"">> end. -make_disco_hash(DiscoEls, Algo) -> - Concat = list_to_binary([concat_identities(DiscoEls), - concat_features(DiscoEls), concat_info(DiscoEls)]), - jlib:encode_base64(case Algo of - md5 -> erlang:md5(Concat); - sha1 -> p1_sha:sha1(Concat); - sha224 -> p1_sha:sha224(Concat); - sha256 -> p1_sha:sha256(Concat); - sha384 -> p1_sha:sha384(Concat); - sha512 -> p1_sha:sha512(Concat) - end). +-spec compute_disco_hash(disco_info(), digest_type()) -> binary(). +compute_disco_hash(DiscoInfo, Algo) -> + Concat = list_to_binary([concat_identities(DiscoInfo), + concat_features(DiscoInfo), concat_info(DiscoInfo)]), + base64:encode(case Algo of + md5 -> erlang:md5(Concat); + sha -> crypto:hash(sha, Concat); + sha224 -> crypto:hash(sha224, Concat); + sha256 -> crypto:hash(sha256, Concat); + sha384 -> crypto:hash(sha384, Concat); + sha512 -> crypto:hash(sha512, Concat) + end). -check_hash(Caps, Els) -> +-spec check_hash(caps(), disco_info()) -> boolean(). +check_hash(Caps, DiscoInfo) -> case Caps#caps.hash of <<"md5">> -> - Caps#caps.version == make_disco_hash(Els, md5); + Caps#caps.version == compute_disco_hash(DiscoInfo, md5); <<"sha-1">> -> - Caps#caps.version == make_disco_hash(Els, sha1); + Caps#caps.version == compute_disco_hash(DiscoInfo, sha); <<"sha-224">> -> - Caps#caps.version == make_disco_hash(Els, sha224); + Caps#caps.version == compute_disco_hash(DiscoInfo, sha224); <<"sha-256">> -> - Caps#caps.version == make_disco_hash(Els, sha256); + Caps#caps.version == compute_disco_hash(DiscoInfo, sha256); <<"sha-384">> -> - Caps#caps.version == make_disco_hash(Els, sha384); + Caps#caps.version == compute_disco_hash(DiscoInfo, sha384); <<"sha-512">> -> - Caps#caps.version == make_disco_hash(Els, sha512); + Caps#caps.version == compute_disco_hash(DiscoInfo, sha512); _ -> true end. -concat_features(Els) -> - lists:usort(lists:flatmap(fun (#xmlel{name = - <<"feature">>, - attrs = Attrs}) -> - [[xml:get_attr_s(<<"var">>, Attrs), $<]]; - (_) -> [] - end, - Els)). +-spec concat_features(disco_info()) -> iolist(). +concat_features(#disco_info{features = Features}) -> + lists:usort([[Feat, $<] || Feat <- Features]). -concat_identities(Els) -> - lists:sort(lists:flatmap(fun (#xmlel{name = - <<"identity">>, - attrs = Attrs}) -> - [[xml:get_attr_s(<<"category">>, Attrs), - $/, xml:get_attr_s(<<"type">>, Attrs), - $/, - xml:get_attr_s(<<"xml:lang">>, Attrs), - $/, xml:get_attr_s(<<"name">>, Attrs), - $<]]; - (_) -> [] - end, - Els)). +-spec concat_identities(disco_info()) -> iolist(). +concat_identities(#disco_info{identities = Identities}) -> + lists:sort( + [[Cat, $/, T, $/, Lang, $/, Name, $<] || + #identity{category = Cat, type = T, + lang = Lang, name = Name} <- Identities]). -concat_info(Els) -> - lists:sort(lists:flatmap(fun (#xmlel{name = <<"x">>, - attrs = Attrs, children = Fields}) -> - case {xml:get_attr_s(<<"xmlns">>, Attrs), - xml:get_attr_s(<<"type">>, Attrs)} - of - {?NS_XDATA, <<"result">>} -> - [concat_xdata_fields(Fields)]; - _ -> [] - end; - (_) -> [] - end, - Els)). +-spec concat_info(disco_info()) -> iolist(). +concat_info(#disco_info{xdata = Xs}) -> + lists:sort( + [concat_xdata_fields(X) || #xdata{type = result} = X <- Xs]). -concat_xdata_fields(Fields) -> - [Form, Res] = lists:foldl(fun (#xmlel{name = - <<"field">>, - attrs = Attrs, children = Els} = - El, - [FormType, VarFields] = Acc) -> - case xml:get_attr_s(<<"var">>, Attrs) of - <<"">> -> Acc; - <<"FORM_TYPE">> -> - [xml:get_subtag_cdata(El, - <<"value">>), - VarFields]; - Var -> - [FormType, - [[[Var, $<], - lists:sort(lists:flatmap(fun - (#xmlel{name - = - <<"value">>, - children - = - VEls}) -> - [[xml:get_cdata(VEls), - $<]]; - (_) -> - [] - end, - Els))] - | VarFields]] - end; - (_, Acc) -> Acc - end, - [<<"">>, []], Fields), +-spec concat_xdata_fields(xdata()) -> iolist(). +concat_xdata_fields(#xdata{fields = Fields} = X) -> + Form = xmpp_util:get_xdata_values(<<"FORM_TYPE">>, X), + Res = [[Var, $<, lists:sort([[Val, $<] || Val <- Values])] + || #xdata_field{var = Var, values = Values} <- Fields, + is_binary(Var), Var /= <<"FORM_TYPE">>], [Form, $<, lists:sort(Res)]. -gb_trees_fold(F, Acc, Tree) -> - Iter = gb_trees:iterator(Tree), - gb_trees_fold_iter(F, Acc, Iter). - -gb_trees_fold_iter(F, Acc, Iter) -> - case gb_trees:next(Iter) of - {Key, Val, NewIter} -> - NewAcc = F(Key, Val, Acc), - gb_trees_fold_iter(F, NewAcc, NewIter); - _ -> Acc - end. - -now_ts() -> - {MegaSecs, Secs, _} = now(), MegaSecs * 1000000 + Secs. - +-spec is_valid_node(binary()) -> boolean(). is_valid_node(Node) -> case str:tokens(Node, <<"#">>) of - [?EJABBERD_URI|_] -> - true; - _ -> + [H|_] -> + H == ejabberd_config:get_uri(); + [] -> false end. -update_table() -> - Fields = record_info(fields, caps_features), - case mnesia:table_info(caps_features, attributes) of - Fields -> - ejabberd_config:convert_table_to_binary( - caps_features, Fields, set, - fun(#caps_features{node_pair = {N, _}}) -> N end, - fun(#caps_features{node_pair = {N, P}, - features = Fs} = R) -> - NewFs = if is_integer(Fs) -> - Fs; - true -> - [iolist_to_binary(F) || F <- Fs] - end, - R#caps_features{node_pair = {iolist_to_binary(N), - iolist_to_binary(P)}, - features = NewFs} - end); - _ -> - ?INFO_MSG("Recreating caps_features table", []), - mnesia:transform_table(caps_features, ignore, Fields) +init_cache(Mod, Host, Opts) -> + CacheOpts = cache_opts(Opts), + case use_cache(Mod, Host) of + true -> + ets_cache:new(caps_features_cache, CacheOpts); + false -> + ets_cache:delete(caps_features_cache) + end, + CacheSize = proplists:get_value(max_size, CacheOpts), + ets_cache:new(caps_requests_cache, + [{max_size, CacheSize}, + {life_time, timer:seconds(?BAD_HASH_LIFETIME)}]). + +use_cache(Mod, Host) -> + case erlang:function_exported(Mod, use_cache, 1) of + true -> Mod:use_cache(Host); + false -> mod_caps_opt:use_cache(Host) end. -sql_write_features_t({Node, SubNode}, Features) -> - SNode = ejabberd_odbc:escape(Node), - SSubNode = ejabberd_odbc:escape(SubNode), - NewFeatures = if is_integer(Features) -> - [jlib:integer_to_binary(Features)]; - true -> - Features - end, - [[<<"delete from caps_features where node='">>, - SNode, <<"' and subnode='">>, SSubNode, <<"';">>]| - [[<<"insert into caps_features(node, subnode, feature) ">>, - <<"values ('">>, SNode, <<"', '">>, SSubNode, <<"', '">>, - ejabberd_odbc:escape(F), <<"');">>] || F <- NewFeatures]]. +cache_opts(Opts) -> + MaxSize = mod_caps_opt:cache_size(Opts), + CacheMissed = mod_caps_opt:cache_missed(Opts), + LifeTime = mod_caps_opt:cache_life_time(Opts), + [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}]. -caps_features_schema() -> - {record_info(fields, caps_features), #caps_features{}}. - -export(_Server) -> - [{caps_features, - fun(_Host, #caps_features{node_pair = NodePair, - features = Features}) -> - sql_write_features_t(NodePair, Features); - (_Host, _R) -> - [] - end}]. +export(LServer) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:export(LServer). import_info() -> [{<<"caps_features">>, 4}]. import_start(LServer, DBType) -> ets:new(caps_features_tmp, [private, named_table, bag]), - init_db(DBType, LServer), + Mod = gen_mod:db_mod(DBType, ?MODULE), + Mod:init(LServer, []), ok. -import(_LServer, {odbc, _}, _DBType, <<"caps_features">>, +import(_LServer, {sql, _}, _DBType, <<"caps_features">>, [Node, SubNode, Feature, _TimeStamp]) -> - Feature1 = case catch jlib:binary_to_integer(Feature) of + Feature1 = case catch binary_to_integer(Feature) of I when is_integer(I), I>0 -> I; _ -> Feature end, @@ -734,22 +579,53 @@ import_next(_LServer, _DBType, '$end_of_table') -> ok; import_next(LServer, DBType, NodePair) -> Features = [F || {_, F} <- ets:lookup(caps_features_tmp, NodePair)], - case Features of - [I] when is_integer(I), DBType == mnesia -> - mnesia:dirty_write( - #caps_features{node_pair = NodePair, features = I}); - [I] when is_integer(I), DBType == riak -> - ejabberd_riak:put( - #caps_features{node_pair = NodePair, features = I}, - caps_features_schema()); - _ when DBType == mnesia -> - mnesia:dirty_write( - #caps_features{node_pair = NodePair, features = Features}); - _ when DBType == riak -> - ejabberd_riak:put( - #caps_features{node_pair = NodePair, features = Features}, - caps_features_schema()); - _ when DBType == odbc -> - ok - end, + Mod = gen_mod:db_mod(DBType, ?MODULE), + Mod:import(LServer, NodePair, Features), import_next(LServer, DBType, ets:next(caps_features_tmp, NodePair)). + +mod_opt_type(db_type) -> + econf:db_type(?MODULE); +mod_opt_type(use_cache) -> + econf:bool(); +mod_opt_type(cache_size) -> + econf:pos_int(infinity); +mod_opt_type(cache_missed) -> + econf:bool(); +mod_opt_type(cache_life_time) -> + econf:timeout(second, infinity). + +mod_options(Host) -> + [{db_type, ejabberd_config:default_db(Host, ?MODULE)}, + {use_cache, ejabberd_option:use_cache(Host)}, + {cache_size, ejabberd_option:cache_size(Host)}, + {cache_missed, ejabberd_option:cache_missed(Host)}, + {cache_life_time, ejabberd_option:cache_life_time(Host)}]. + +mod_doc() -> + #{desc => + [?T("This module implements " + "https://xmpp.org/extensions/xep-0115.html" + "[XEP-0115: Entity Capabilities]."), + ?T("The main purpose of the module is to provide " + "PEP functionality (see _`mod_pubsub`_).")], + opts => + [{db_type, + #{value => "mnesia | sql", + desc => + ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}, + {use_cache, + #{value => "true | false", + desc => + ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, + {cache_size, + #{value => "pos_integer() | infinity", + desc => + ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, + {cache_missed, + #{value => "true | false", + desc => + ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, + {cache_life_time, + #{value => "timeout()", + desc => + ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}]}. diff --git a/src/mod_caps_mnesia.erl b/src/mod_caps_mnesia.erl new file mode 100644 index 000000000..a0dc48c4f --- /dev/null +++ b/src/mod_caps_mnesia.erl @@ -0,0 +1,84 @@ +%%%------------------------------------------------------------------- +%%% File : mod_caps_mnesia.erl +%%% Author : Evgeny Khramtsov +%%% Created : 13 Apr 2016 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_caps_mnesia). + +-behaviour(mod_caps). + +%% API +-export([init/2, caps_read/2, caps_write/3, import/3]). +-export([need_transform/1, transform/1]). + +-include("mod_caps.hrl"). +-include("logger.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + ejabberd_mnesia:create(?MODULE, caps_features, + [{disc_only_copies, [node()]}, + {local_content, true}, + {attributes, record_info(fields, caps_features)}]). + +caps_read(_LServer, Node) -> + case mnesia:dirty_read({caps_features, Node}) of + [#caps_features{features = Features}] -> {ok, Features}; + _ -> error + end. + +caps_write(_LServer, Node, Features) -> + mnesia:dirty_write(#caps_features{node_pair = Node, + features = Features}). + +import(_LServer, NodePair, [I]) when is_integer(I) -> + mnesia:dirty_write( + #caps_features{node_pair = NodePair, features = I}); +import(_LServer, NodePair, Features) -> + mnesia:dirty_write( + #caps_features{node_pair = NodePair, features = Features}). + +need_transform(#caps_features{node_pair = {N, P}, features = Fs}) -> + case is_list(N) orelse is_list(P) orelse + (is_list(Fs) andalso lists:any(fun is_list/1, Fs)) of + true -> + ?INFO_MSG("Mnesia table 'caps_features' will be " + "converted to binary", []), + true; + false -> + false + end. + +transform(#caps_features{node_pair = {N, P}, features = Fs} = R) -> + NewFs = if is_integer(Fs) -> + Fs; + true -> + [iolist_to_binary(F) || F <- Fs] + end, + R#caps_features{node_pair = {iolist_to_binary(N), iolist_to_binary(P)}, + features = NewFs}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== diff --git a/src/mod_caps_opt.erl b/src/mod_caps_opt.erl new file mode 100644 index 000000000..9af68f115 --- /dev/null +++ b/src/mod_caps_opt.erl @@ -0,0 +1,41 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_caps_opt). + +-export([cache_life_time/1]). +-export([cache_missed/1]). +-export([cache_size/1]). +-export([db_type/1]). +-export([use_cache/1]). + +-spec cache_life_time(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +cache_life_time(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_life_time, Opts); +cache_life_time(Host) -> + gen_mod:get_module_opt(Host, mod_caps, cache_life_time). + +-spec cache_missed(gen_mod:opts() | global | binary()) -> boolean(). +cache_missed(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_missed, Opts); +cache_missed(Host) -> + gen_mod:get_module_opt(Host, mod_caps, cache_missed). + +-spec cache_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +cache_size(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_size, Opts); +cache_size(Host) -> + gen_mod:get_module_opt(Host, mod_caps, cache_size). + +-spec db_type(gen_mod:opts() | global | binary()) -> atom(). +db_type(Opts) when is_map(Opts) -> + gen_mod:get_opt(db_type, Opts); +db_type(Host) -> + gen_mod:get_module_opt(Host, mod_caps, db_type). + +-spec use_cache(gen_mod:opts() | global | binary()) -> boolean(). +use_cache(Opts) when is_map(Opts) -> + gen_mod:get_opt(use_cache, Opts); +use_cache(Host) -> + gen_mod:get_module_opt(Host, mod_caps, use_cache). + diff --git a/src/mod_caps_sql.erl b/src/mod_caps_sql.erl new file mode 100644 index 000000000..9d96697e7 --- /dev/null +++ b/src/mod_caps_sql.erl @@ -0,0 +1,111 @@ +%%%------------------------------------------------------------------- +%%% File : mod_caps_sql.erl +%%% Author : Evgeny Khramtsov +%%% Created : 13 Apr 2016 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_caps_sql). + +-behaviour(mod_caps). + + +%% API +-export([init/2, caps_read/2, caps_write/3, export/1, import/3]). +-export([sql_schemas/0]). + +-include("mod_caps.hrl"). +-include("ejabberd_sql_pt.hrl"). +-include("logger.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(Host, _Opts) -> + ejabberd_sql_schema:update_schema(Host, ?MODULE, sql_schemas()), + ok. + +sql_schemas() -> + [#sql_schema{ + version = 1, + tables = + [#sql_table{ + name = <<"caps_features">>, + columns = + [#sql_column{name = <<"node">>, type = text}, + #sql_column{name = <<"subnode">>, type = text}, + #sql_column{name = <<"feature">>, type = text}, + #sql_column{name = <<"created_at">>, type = timestamp, + default = true}], + indices = [#sql_index{ + columns = [<<"node">>, <<"subnode">>]}]}]}]. + +caps_read(LServer, {Node, SubNode}) -> + case ejabberd_sql:sql_query( + LServer, + ?SQL("select @(feature)s from caps_features where" + " node=%(Node)s and subnode=%(SubNode)s")) of + {selected, [{H}|_] = Fs} -> + case catch binary_to_integer(H) of + Int when is_integer(Int), Int>=0 -> + {ok, Int}; + _ -> + {ok, [F || {F} <- Fs]} + end; + _ -> + error + end. + +caps_write(LServer, NodePair, Features) -> + case ejabberd_sql:sql_transaction( + LServer, + sql_write_features_t(NodePair, Features)) of + {atomic, _} -> + ok; + {aborted, _Reason} -> + {error, db_failure} + end. + +export(_Server) -> + [{caps_features, + fun(_Host, #caps_features{node_pair = NodePair, + features = Features}) -> + sql_write_features_t(NodePair, Features); + (_Host, _R) -> + [] + end}]. + +import(_, _, _) -> + ok. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +sql_write_features_t({Node, SubNode}, Features) -> + NewFeatures = if is_integer(Features) -> + [integer_to_binary(Features)]; + true -> + Features + end, + [?SQL("delete from caps_features where node=%(Node)s" + " and subnode=%(SubNode)s;") | + [?SQL("insert into caps_features(node, subnode, feature)" + " values (%(Node)s, %(SubNode)s, %(F)s);") || F <- NewFeatures]]. + diff --git a/src/mod_carboncopy.erl b/src/mod_carboncopy.erl index 24c09bffd..d040d948f 100644 --- a/src/mod_carboncopy.erl +++ b/src/mod_carboncopy.erl @@ -7,7 +7,7 @@ %%% {mod_carboncopy, []} %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -25,148 +25,169 @@ %%% %%%---------------------------------------------------------------------- -module (mod_carboncopy). --author ('ecestari@process-one.net'). --behavior(gen_mod). +-author ('ecestari@process-one.net'). +-protocol({xep, 280, '1.0.1', '13.06', "complete", ""}). + +-behaviour(gen_mod). %% API: --export([start/2, - stop/1]). +-export([start/2, stop/1, reload/3]). -%% Hooks: --export([user_send_packet/3, - user_receive_packet/4, - iq_handler2/3, - iq_handler1/3, - remove_connection/4, - is_carbon_copy/1]). +-export([user_send_packet/1, user_receive_packet/1, + iq_handler/1, disco_features/5, + depends/2, mod_options/1, mod_doc/0]). +-export([c2s_copy_session/2, c2s_session_opened/1, c2s_session_resumed/1, + c2s_inline_features/3, c2s_handle_bind2_inline/1]). +%% For debugging purposes +-export([list/2]). --include("ejabberd.hrl"). -include("logger.hrl"). --include("jlib.hrl"). --define(PROCNAME, ?MODULE). --define(TABLE, carboncopy). +-include_lib("xmpp/include/xmpp.hrl"). +-include("translate.hrl"). --type matchspec_atom() :: '_' | '$1' | '$2' | '$3'. --record(carboncopy,{us :: {binary(), binary()} | matchspec_atom(), - resource :: binary() | matchspec_atom(), - version :: binary() | matchspec_atom()}). +-type direction() :: sent | received. +-type c2s_state() :: ejabberd_c2s:state(). -is_carbon_copy(Packet) -> - is_carbon_copy(Packet, <<"sent">>) orelse - is_carbon_copy(Packet, <<"received">>). +start(_Host, _Opts) -> + {ok, [{hook, disco_local_features, disco_features, 50}, + %% why priority 89: to define clearly that we must run BEFORE mod_logdb hook (90) + {hook, user_send_packet, user_send_packet, 89}, + {hook, user_receive_packet, user_receive_packet, 89}, + {hook, c2s_copy_session, c2s_copy_session, 50}, + {hook, c2s_session_resumed, c2s_session_resumed, 50}, + {hook, c2s_session_opened, c2s_session_opened, 50}, + {hook, c2s_inline_features, c2s_inline_features, 50}, + {hook, c2s_handle_bind2_inline, c2s_handle_bind2_inline, 50}, + {iq_handler, ejabberd_sm, ?NS_CARBONS_2, iq_handler}]}. -is_carbon_copy(Packet, Direction) -> - case xml:get_subtag(Packet, Direction) of - #xmlel{name = Direction, attrs = Attrs} -> - case xml:get_attr_s(<<"xmlns">>, Attrs) of - ?NS_CARBONS_2 -> true; - ?NS_CARBONS_1 -> true; - _ -> false - end; - _ -> false +stop(_Host) -> + ok. + +reload(_Host, _NewOpts, _OldOpts) -> + ok. + +-spec disco_features({error, stanza_error()} | {result, [binary()]} | empty, + jid(), jid(), binary(), binary()) -> + {error, stanza_error()} | {result, [binary()]}. +disco_features(empty, From, To, <<"">>, Lang) -> + disco_features({result, []}, From, To, <<"">>, Lang); +disco_features({result, Feats}, _From, _To, <<"">>, _Lang) -> + {result, [?NS_CARBONS_2,?NS_CARBONS_RULES_0|Feats]}; +disco_features(Acc, _From, _To, _Node, _Lang) -> + Acc. + +-spec iq_handler(iq()) -> iq(). +iq_handler(#iq{type = set, lang = Lang, from = From, + sub_els = [El]} = IQ) when is_record(El, carbons_enable); + is_record(El, carbons_disable) -> + {U, S, R} = jid:tolower(From), + Result = case El of + #carbons_enable{} -> enable(S, U, R, ?NS_CARBONS_2); + #carbons_disable{} -> disable(S, U, R) + end, + case Result of + ok -> + xmpp:make_iq_result(IQ); + {error, _} -> + Txt = ?T("Database failure"), + xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) + end; +iq_handler(#iq{type = set, lang = Lang} = IQ) -> + Txt = ?T("Only or tags are allowed"), + xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)); +iq_handler(#iq{type = get, lang = Lang} = IQ)-> + Txt = ?T("Value 'get' of 'type' attribute is not allowed"), + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)). + +-spec user_send_packet({stanza(), ejabberd_c2s:state()}) + -> {stanza(), ejabberd_c2s:state()} | {stop, {stanza(), ejabberd_c2s:state()}}. +user_send_packet({#message{meta = #{carbon_copy := true}}, _C2SState} = Acc) -> + %% Stop the hook chain, we don't want logging modules to duplicate this + %% message. + {stop, Acc}; +user_send_packet({#message{from = From, to = To} = Msg, C2SState}) -> + {check_and_forward(From, To, Msg, sent), C2SState}; +user_send_packet(Acc) -> + Acc. + +-spec user_receive_packet({stanza(), ejabberd_c2s:state()}) + -> {stanza(), ejabberd_c2s:state()} | {stop, {stanza(), ejabberd_c2s:state()}}. +user_receive_packet({#message{meta = #{carbon_copy := true}}, _C2SState} = Acc) -> + %% Stop the hook chain, we don't want logging modules to duplicate this + %% message. + {stop, Acc}; +user_receive_packet({#message{to = To} = Msg, #{jid := JID} = C2SState}) -> + {check_and_forward(JID, To, Msg, received), C2SState}; +user_receive_packet(Acc) -> + Acc. + +-spec c2s_copy_session(c2s_state(), c2s_state()) -> c2s_state(). +c2s_copy_session(State, #{user := U, server := S, resource := R}) -> + case ejabberd_sm:get_user_info(U, S, R) of + offline -> State; + Info -> + case lists:keyfind(carboncopy, 1, Info) of + {_, CC} -> State#{carboncopy => CC}; + false -> State + end end. -start(Host, Opts) -> - IQDisc = gen_mod:get_opt(iqdisc, Opts,fun gen_iq_handler:check_type/1, one_queue), - mod_disco:register_feature(Host, ?NS_CARBONS_1), - mod_disco:register_feature(Host, ?NS_CARBONS_2), - Fields = record_info(fields, ?TABLE), - try mnesia:table_info(?TABLE, attributes) of - Fields -> ok; - _ -> mnesia:delete_table(?TABLE) %% recreate.. - catch _:_Error -> ok %%probably table don't exist - end, - mnesia:create_table(?TABLE, - [{ram_copies, [node()]}, - {attributes, record_info(fields, ?TABLE)}, - {type, bag}]), - mnesia:add_table_copy(?TABLE, node(), ram_copies), - ejabberd_hooks:add(unset_presence_hook,Host, ?MODULE, remove_connection, 10), - %% why priority 89: to define clearly that we must run BEFORE mod_logdb hook (90) - ejabberd_hooks:add(user_send_packet,Host, ?MODULE, user_send_packet, 89), - ejabberd_hooks:add(user_receive_packet,Host, ?MODULE, user_receive_packet, 89), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_CARBONS_2, ?MODULE, iq_handler2, IQDisc), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_CARBONS_1, ?MODULE, iq_handler1, IQDisc). +-spec c2s_session_resumed(c2s_state()) -> c2s_state(). +c2s_session_resumed(#{user := U, server := S, resource := R, + carboncopy := CC} = State) -> + ejabberd_sm:set_user_info(U, S, R, carboncopy, CC), + maps:remove(carboncopy, State); +c2s_session_resumed(State) -> + State. -stop(Host) -> - gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_CARBONS_1), - gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_CARBONS_2), - mod_disco:unregister_feature(Host, ?NS_CARBONS_2), - mod_disco:unregister_feature(Host, ?NS_CARBONS_1), - %% why priority 89: to define clearly that we must run BEFORE mod_logdb hook (90) - ejabberd_hooks:delete(user_send_packet,Host, ?MODULE, user_send_packet, 89), - ejabberd_hooks:delete(user_receive_packet,Host, ?MODULE, user_receive_packet, 89), - ejabberd_hooks:delete(unset_presence_hook,Host, ?MODULE, remove_connection, 10). +-spec c2s_session_opened(c2s_state()) -> c2s_state(). +c2s_session_opened(State) -> + maps:remove(carboncopy, State). -iq_handler2(From, To, IQ) -> - iq_handler(From, To, IQ, ?NS_CARBONS_2). -iq_handler1(From, To, IQ) -> - iq_handler(From, To, IQ, ?NS_CARBONS_1). +c2s_inline_features({Sasl, Bind, Extra} = Acc, Host, _State) -> + case gen_mod:is_loaded(Host, ?MODULE) of + true -> + {Sasl, [#bind2_feature{var = ?NS_CARBONS_2} | Bind], Extra}; + false -> + Acc + end. -iq_handler(From, _To, #iq{type=set, sub_el = #xmlel{name = Operation, children = []}} = IQ, CC)-> - ?DEBUG("carbons IQ received: ~p", [IQ]), - {U, S, R} = jlib:jid_tolower(From), - Result = case Operation of - <<"enable">>-> - ?INFO_MSG("carbons enabled for user ~s@~s/~s", [U,S,R]), - enable(S,U,R,CC); - <<"disable">>-> - ?INFO_MSG("carbons disabled for user ~s@~s/~s", [U,S,R]), - disable(S, U, R) - end, - case Result of - ok -> - ?DEBUG("carbons IQ result: ok", []), - IQ#iq{type=result, sub_el=[]}; - {error,_Error} -> - ?WARNING_MSG("Error enabling / disabling carbons: ~p", [Result]), - IQ#iq{type=error,sub_el = [?ERR_BAD_REQUEST]} - end; +c2s_handle_bind2_inline({#{user := U, server := S, resource := R} = State, Els, Results}) -> + case lists:keyfind(carbons_enable, 1, Els) of + #carbons_enable{} -> + enable(S, U, R, ?NS_CARBONS_2), + {State, Els, Results}; + _ -> + {State, Els, Results} + end. -iq_handler(_From, _To, IQ, _CC)-> - IQ#iq{type=error, sub_el = [?ERR_NOT_ALLOWED]}. - -user_send_packet(From, To, Packet) -> - check_and_forward(From, To, Packet, sent). - -user_receive_packet(JID, _From, To, Packet) -> - check_and_forward(JID, To, Packet, received). - -% verifier si le trafic est local -% Modified from original version: +% Modified from original version: % - registered to the user_send_packet hook, to be called only once even for multicast % - do not support "private" message mode, and do not modify the original packet in any way % - we also replicate "read" notifications -check_and_forward(JID, To, Packet, Direction)-> - case is_chat_or_normal_message(Packet) andalso - xml:get_subtag(Packet, <<"private">>) == false andalso - xml:get_subtag(Packet, <<"no-copy">>) == false of +-spec check_and_forward(jid(), jid(), message(), direction()) -> message(). +check_and_forward(JID, To, Msg, Direction)-> + case (is_chat_message(Msg) orelse + is_received_muc_invite(Msg, Direction)) andalso + not is_received_muc_pm(To, Msg, Direction) andalso + not xmpp:has_subtag(Msg, #carbons_private{}) andalso + not xmpp:has_subtag(Msg, #hint{type = 'no-copy'}) of true -> - case is_carbon_copy(Packet) of - false -> - send_copies(JID, To, Packet, Direction); - true -> - %% stop the hook chain, we don't want mod_logdb to register - %% this message (duplicate) - stop - end; - _ -> + send_copies(JID, To, Msg, Direction); + false -> ok - end. - -remove_connection(User, Server, Resource, _Status)-> - disable(Server, User, Resource), - ok. - + end, + Msg. %%% Internal %% Direction = received | sent -send_copies(JID, To, Packet, Direction)-> - {U, S, R} = jlib:jid_tolower(JID), +-spec send_copies(jid(), jid(), message(), direction()) -> ok. +send_copies(JID, To, Msg, Direction)-> + {U, S, R} = jid:tolower(JID), PrioRes = ejabberd_sm:get_user_present_resources(U, S), {_, AvailRs} = lists:unzip(PrioRes), - {MaxPrio, MaxRes} = case catch lists:max(PrioRes) of + {MaxPrio, _MaxRes} = case catch lists:max(PrioRes) of {Prio, Res} -> {Prio, Res}; _ -> {0, undefined} end, @@ -179,111 +200,121 @@ send_copies(JID, To, Packet, Direction)-> end, %% list of JIDs that should receive a carbon copy of this message (excluding the %% receiver(s) of the original message - TargetJIDs = case {IsBareTo, R} of - {true, MaxRes} -> - OrigTo = fun(Res) -> lists:member({MaxPrio, Res}, PrioRes) end, - [ {jlib:make_jid({U, S, CCRes}), CC_Version} - || {CCRes, CC_Version} <- list(U, S), - lists:member(CCRes, AvailRs), not OrigTo(CCRes) ]; - {true, _} -> + TargetJIDs = case {IsBareTo, Msg} of + {true, #message{meta = #{sm_copy := true}}} -> %% The message was sent to our bare JID, and we currently have %% multiple resources with the same highest priority, so the session %% manager routes the message to each of them. We create carbon - %% copies only from one of those resources (the one where R equals - %% MaxRes) in order to avoid duplicates. + %% copies only from one of those resources in order to avoid + %% duplicates. []; + {true, _} -> + OrigTo = fun(Res) -> lists:member({MaxPrio, Res}, PrioRes) end, + [ {jid:make({U, S, CCRes}), CC_Version} + || {CCRes, CC_Version} <- list(U, S), + lists:member(CCRes, AvailRs), not OrigTo(CCRes) ]; {false, _} -> - [ {jlib:make_jid({U, S, CCRes}), CC_Version} + [ {jid:make({U, S, CCRes}), CC_Version} || {CCRes, CC_Version} <- list(U, S), lists:member(CCRes, AvailRs), CCRes /= R ] - %TargetJIDs = lists:delete(JID, [ jlib:make_jid({U, S, CCRes}) || CCRes <- list(U, S) ]), + %TargetJIDs = lists:delete(JID, [ jid:make({U, S, CCRes}) || CCRes <- list(U, S) ]), end, - lists:map(fun({Dest,Version}) -> - {_, _, Resource} = jlib:jid_tolower(Dest), - ?DEBUG("Sending: ~p =/= ~p", [R, Resource]), - Sender = jlib:make_jid({U, S, <<>>}), - %{xmlelement, N, A, C} = Packet, - New = build_forward_packet(JID, Packet, Sender, Dest, Direction, Version), - ejabberd_router:route(Sender, Dest, New) - end, TargetJIDs), - ok. - -build_forward_packet(JID, Packet, Sender, Dest, Direction, ?NS_CARBONS_2) -> - #xmlel{name = <<"message">>, - attrs = [{<<"xmlns">>, <<"jabber:client">>}, - {<<"type">>, message_type(Packet)}, - {<<"from">>, jlib:jid_to_string(Sender)}, - {<<"to">>, jlib:jid_to_string(Dest)}], - children = [ - #xmlel{name = list_to_binary(atom_to_list(Direction)), - attrs = [{<<"xmlns">>, ?NS_CARBONS_2}], - children = [ - #xmlel{name = <<"forwarded">>, - attrs = [{<<"xmlns">>, ?NS_FORWARD}], - children = [ - complete_packet(JID, Packet, Direction)]} - ]} - ]}; -build_forward_packet(JID, Packet, Sender, Dest, Direction, ?NS_CARBONS_1) -> - #xmlel{name = <<"message">>, - attrs = [{<<"xmlns">>, <<"jabber:client">>}, - {<<"type">>, message_type(Packet)}, - {<<"from">>, jlib:jid_to_string(Sender)}, - {<<"to">>, jlib:jid_to_string(Dest)}], - children = [ - #xmlel{name = list_to_binary(atom_to_list(Direction)), - attrs = [{<<"xmlns">>, ?NS_CARBONS_1}]}, - #xmlel{name = <<"forwarded">>, - attrs = [{<<"xmlns">>, ?NS_FORWARD}], - children = [complete_packet(JID, Packet, Direction)]} - ]}. + lists:foreach( + fun({Dest, _Version}) -> + {_, _, Resource} = jid:tolower(Dest), + ?DEBUG("Sending: ~p =/= ~p", [R, Resource]), + Sender = jid:make({U, S, <<>>}), + New = build_forward_packet(Msg, Sender, Dest, Direction), + ejabberd_router:route(xmpp:set_from_to(New, Sender, Dest)) + end, TargetJIDs). +-spec build_forward_packet(message(), jid(), jid(), direction()) -> message(). +build_forward_packet(#message{type = T} = Msg, Sender, Dest, Direction) -> + Forwarded = #forwarded{sub_els = [Msg]}, + Carbon = case Direction of + sent -> #carbons_sent{forwarded = Forwarded}; + received -> #carbons_received{forwarded = Forwarded} + end, + #message{from = Sender, to = Dest, type = T, sub_els = [Carbon], + meta = #{carbon_copy => true}}. +-spec enable(binary(), binary(), binary(), binary()) -> ok | {error, any()}. enable(Host, U, R, CC)-> - ?DEBUG("enabling for ~p", [U]), - try mnesia:dirty_write(#carboncopy{us = {U, Host}, resource=R, version = CC}) of - ok -> ok - catch _:Error -> {error, Error} - end. + ?DEBUG("Enabling carbons for ~ts@~ts/~ts", [U, Host, R]), + case ejabberd_sm:set_user_info(U, Host, R, carboncopy, CC) of + ok -> ok; + {error, Reason} = Err -> + ?ERROR_MSG("Failed to enable carbons for ~ts@~ts/~ts: ~p", + [U, Host, R, Reason]), + Err + end. +-spec disable(binary(), binary(), binary()) -> ok | {error, any()}. disable(Host, U, R)-> - ?DEBUG("disabling for ~p", [U]), - ToDelete = mnesia:dirty_match_object(?TABLE, #carboncopy{us = {U, Host}, resource = R, version = '_'}), - try lists:foreach(fun mnesia:dirty_delete_object/1, ToDelete) of - ok -> ok - catch _:Error -> {error, Error} + ?DEBUG("Disabling carbons for ~ts@~ts/~ts", [U, Host, R]), + case ejabberd_sm:del_user_info(U, Host, R, carboncopy) of + ok -> ok; + {error, notfound} -> ok; + {error, Reason} = Err -> + ?ERROR_MSG("Failed to disable carbons for ~ts@~ts/~ts: ~p", + [U, Host, R, Reason]), + Err end. -complete_packet(From, #xmlel{name = <<"message">>, attrs = OrigAttrs} = Packet, sent) -> - %% if this is a packet sent by user on this host, then Packet doesn't - %% include the 'from' attribute. We must add it. - Attrs = lists:keystore(<<"xmlns">>, 1, OrigAttrs, {<<"xmlns">>, <<"jabber:client">>}), - case proplists:get_value(<<"from">>, Attrs) of - undefined -> - Packet#xmlel{attrs = [{<<"from">>, jlib:jid_to_string(From)}|Attrs]}; +-spec is_chat_message(message()) -> boolean(). +is_chat_message(#message{type = chat}) -> + true; +is_chat_message(#message{type = normal, body = [_|_]}) -> + true; +is_chat_message(#message{type = Type} = Msg) when Type == chat; + Type == normal -> + has_chatstate(Msg) orelse xmpp:has_subtag(Msg, #receipt_response{}); +is_chat_message(_) -> + false. + +-spec is_received_muc_invite(message(), direction()) -> boolean(). +is_received_muc_invite(_Msg, sent) -> + false; +is_received_muc_invite(Msg, received) -> + case xmpp:get_subtag(Msg, #muc_user{}) of + #muc_user{invites = [_|_]} -> + true; _ -> - Packet#xmlel{attrs = Attrs} - end; -complete_packet(_From, #xmlel{name = <<"message">>, attrs=OrigAttrs} = Packet, received) -> - Attrs = lists:keystore(<<"xmlns">>, 1, OrigAttrs, {<<"xmlns">>, <<"jabber:client">>}), - Packet#xmlel{attrs = Attrs}. - -message_type(#xmlel{attrs = Attrs}) -> - case xml:get_attr(<<"type">>, Attrs) of - {value, Type} -> Type; - false -> <<"normal">> + xmpp:has_subtag(Msg, #x_conference{jid = jid:make(<<"">>)}) end. -is_chat_or_normal_message(#xmlel{name = <<"message">>} = Packet) -> - case message_type(Packet) of - <<"chat">> -> true; - <<"normal">> -> true; - _ -> false - end; -is_chat_or_normal_message(_Packet) -> false. +-spec is_received_muc_pm(jid(), message(), direction()) -> boolean(). +is_received_muc_pm(#jid{lresource = <<>>}, _Msg, _Direction) -> + false; +is_received_muc_pm(_To, _Msg, sent) -> + false; +is_received_muc_pm(_To, Msg, received) -> + xmpp:has_subtag(Msg, #muc_user{}). -%% list {resource, cc_version} with carbons enabled for given user and host -list(User, Server)-> - mnesia:dirty_select(?TABLE, [{#carboncopy{us = {User, Server}, resource = '$2', version = '$3'}, [], [{{'$2','$3'}}]}]). +-spec has_chatstate(message()) -> boolean(). +has_chatstate(#message{sub_els = Els}) -> + lists:any(fun(El) -> xmpp:get_ns(El) == ?NS_CHATSTATES end, Els). +-spec list(binary(), binary()) -> [{Resource :: binary(), Namespace :: binary()}]. +list(User, Server) -> + lists:filtermap( + fun({Resource, Info}) -> + case lists:keyfind(carboncopy, 1, Info) of + {_, NS} -> {true, {Resource, NS}}; + false -> false + end + end, ejabberd_sm:get_user_info(User, Server)). + +depends(_Host, _Opts) -> + []. + +mod_options(_) -> + []. + +mod_doc() -> + #{desc => + ?T("The module implements https://xmpp.org/extensions/xep-0280.html" + "[XEP-0280: Message Carbons]. " + "The module broadcasts messages on all connected " + "user resources (devices).")}. diff --git a/src/mod_client_state.erl b/src/mod_client_state.erl index fd72c02f6..67f973784 100644 --- a/src/mod_client_state.erl +++ b/src/mod_client_state.erl @@ -1,11 +1,11 @@ %%%---------------------------------------------------------------------- %%% File : mod_client_state.erl -%%% Author : Holger Weiss +%%% Author : Holger Weiss %%% Purpose : Filter stanzas sent to inactive clients (XEP-0352) -%%% Created : 11 Sep 2014 by Holger Weiss +%%% Created : 11 Sep 2014 by Holger Weiss %%% %%% -%%% ejabberd, Copyright (C) 2014-2015 ProcessOne +%%% ejabberd, Copyright (C) 2014-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -25,85 +25,412 @@ -module(mod_client_state). -author('holger@zedat.fu-berlin.de'). +-protocol({xep, 85, '2.1', '2.1.0', "complete", ""}). +-protocol({xep, 352, '0.1', '14.12', "complete", ""}). --behavior(gen_mod). +-behaviour(gen_mod). --export([start/2, stop/1, add_stream_feature/2, filter_presence/2, - filter_chat_states/2]). +%% gen_mod callbacks. +-export([start/2, stop/1, reload/3, mod_opt_type/1, depends/2, mod_options/1]). +-export([mod_doc/0]). + +%% ejabberd_hooks callbacks. +-export([filter_presence/1, filter_chat_states/1, + filter_pep/1, filter_other/1, + c2s_stream_started/2, add_stream_feature/2, + c2s_authenticated_packet/2, csi_activity/2, + c2s_copy_session/2, c2s_session_resumed/1]). --include("ejabberd.hrl"). -include("logger.hrl"). --include("jlib.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). +-include("translate.hrl"). +-define(CSI_QUEUE_MAX, 100). + +-type csi_type() :: presence | chatstate | {pep, binary()}. +-type csi_queue() :: {non_neg_integer(), #{csi_key() => csi_element()}}. +-type csi_timestamp() :: {non_neg_integer(), erlang:timestamp()}. +-type csi_key() :: {ljid(), csi_type()}. +-type csi_element() :: {csi_timestamp(), stanza()}. +-type c2s_state() :: ejabberd_c2s:state(). +-type filter_acc() :: {stanza() | drop, c2s_state()}. + +%%-------------------------------------------------------------------- +%% gen_mod callbacks. +%%-------------------------------------------------------------------- +-spec start(binary(), gen_mod:opts()) -> ok. start(Host, Opts) -> - QueuePresence = gen_mod:get_opt(queue_presence, Opts, - fun(true) -> true; - (false) -> false - end, false), - DropChatStates = gen_mod:get_opt(drop_chat_states, Opts, - fun(true) -> true; - (false) -> false - end, false), - if QueuePresence; DropChatStates -> - ejabberd_hooks:add(c2s_post_auth_features, Host, ?MODULE, - add_stream_feature, 50), + QueuePresence = mod_client_state_opt:queue_presence(Opts), + QueueChatStates = mod_client_state_opt:queue_chat_states(Opts), + QueuePEP = mod_client_state_opt:queue_pep(Opts), + if QueuePresence; QueueChatStates; QueuePEP -> + register_hooks(Host), if QueuePresence -> - ejabberd_hooks:add(csi_filter_stanza, Host, ?MODULE, + ejabberd_hooks:add(c2s_filter_send, Host, ?MODULE, filter_presence, 50); true -> ok end, - if DropChatStates -> - ejabberd_hooks:add(csi_filter_stanza, Host, ?MODULE, + if QueueChatStates -> + ejabberd_hooks:add(c2s_filter_send, Host, ?MODULE, filter_chat_states, 50); true -> ok + end, + if QueuePEP -> + ejabberd_hooks:add(c2s_filter_send, Host, ?MODULE, + filter_pep, 50); + true -> ok end; true -> ok - end, - ok. + end. +-spec stop(binary()) -> ok. stop(Host) -> - ejabberd_hooks:delete(csi_filter_stanza, Host, ?MODULE, - filter_presence, 50), - ejabberd_hooks:delete(csi_filter_stanza, Host, ?MODULE, - filter_chat_states, 50), + QueuePresence = mod_client_state_opt:queue_presence(Host), + QueueChatStates = mod_client_state_opt:queue_chat_states(Host), + QueuePEP = mod_client_state_opt:queue_pep(Host), + if QueuePresence; QueueChatStates; QueuePEP -> + unregister_hooks(Host), + if QueuePresence -> + ejabberd_hooks:delete(c2s_filter_send, Host, ?MODULE, + filter_presence, 50); + true -> ok + end, + if QueueChatStates -> + ejabberd_hooks:delete(c2s_filter_send, Host, ?MODULE, + filter_chat_states, 50); + true -> ok + end, + if QueuePEP -> + ejabberd_hooks:delete(c2s_filter_send, Host, ?MODULE, + filter_pep, 50); + true -> ok + end; + true -> ok + end. + +-spec reload(binary(), gen_mod:opts(), gen_mod:opts()) -> ok. +reload(Host, NewOpts, _OldOpts) -> + QueuePresence = mod_client_state_opt:queue_presence(NewOpts), + QueueChatStates = mod_client_state_opt:queue_chat_states(NewOpts), + QueuePEP = mod_client_state_opt:queue_pep(NewOpts), + if QueuePresence; QueueChatStates; QueuePEP -> + register_hooks(Host); + true -> + unregister_hooks(Host) + end, + if QueuePresence -> + ejabberd_hooks:add(c2s_filter_send, Host, ?MODULE, + filter_presence, 50); + true -> + ejabberd_hooks:delete(c2s_filter_send, Host, ?MODULE, + filter_presence, 50) + end, + if QueueChatStates -> + ejabberd_hooks:add(c2s_filter_send, Host, ?MODULE, + filter_chat_states, 50); + true -> + ejabberd_hooks:delete(c2s_filter_send, Host, ?MODULE, + filter_chat_states, 50) + end, + if QueuePEP -> + ejabberd_hooks:add(c2s_filter_send, Host, ?MODULE, + filter_pep, 50); + true -> + ejabberd_hooks:delete(c2s_filter_send, Host, ?MODULE, + filter_pep, 50) + end. + +-spec mod_opt_type(atom()) -> econf:validator(). +mod_opt_type(queue_presence) -> + econf:bool(); +mod_opt_type(queue_chat_states) -> + econf:bool(); +mod_opt_type(queue_pep) -> + econf:bool(). + +mod_options(_) -> + [{queue_presence, true}, + {queue_chat_states, true}, + {queue_pep, true}]. + +mod_doc() -> + #{desc => + [?T("This module allows for queueing certain types of stanzas " + "when a client indicates that the user is not actively using " + "the client right now (see https://xmpp.org/extensions/xep-0352.html" + "[XEP-0352: Client State Indication]). This can save bandwidth and " + "resources."), "", + ?T("A stanza is dropped from the queue if it's effectively obsoleted " + "by a new one (e.g., a new presence stanza would replace an old " + "one from the same client). The queue is flushed if a stanza arrives " + "that won't be queued, or if the queue size reaches a certain limit " + "(currently 100 stanzas), or if the client becomes active again.")], + opts => + [{queue_presence, + #{value => "true | false", + desc => + ?T("While a client is inactive, queue presence stanzas " + "that indicate (un)availability. The default value is 'true'.")}}, + {queue_chat_states, + #{value => "true | false", + desc => + ?T("Queue \"standalone\" chat state notifications (as defined in " + "https://xmpp.org/extensions/xep-0085.html" + "[XEP-0085: Chat State Notifications]) while a client " + "indicates inactivity. The default value is 'true'.")}}, + {queue_pep, + #{value => "true | false", + desc => + ?T("Queue PEP notifications while a client is inactive. " + "When the queue is flushed, only the most recent notification " + "of a given PEP node is delivered. The default value is 'true'.")}}]}. + +-spec depends(binary(), gen_mod:opts()) -> [{module(), hard | soft}]. +depends(_Host, _Opts) -> + []. + +-spec register_hooks(binary()) -> ok. +register_hooks(Host) -> + ejabberd_hooks:add(c2s_stream_started, Host, ?MODULE, + c2s_stream_started, 50), + ejabberd_hooks:add(c2s_post_auth_features, Host, ?MODULE, + add_stream_feature, 50), + ejabberd_hooks:add(c2s_authenticated_packet, Host, ?MODULE, + c2s_authenticated_packet, 50), + ejabberd_hooks:add(csi_activity, Host, ?MODULE, + csi_activity, 50), + ejabberd_hooks:add(c2s_copy_session, Host, ?MODULE, + c2s_copy_session, 50), + ejabberd_hooks:add(c2s_session_resumed, Host, ?MODULE, + c2s_session_resumed, 50), + ejabberd_hooks:add(c2s_filter_send, Host, ?MODULE, + filter_other, 75). + +-spec unregister_hooks(binary()) -> ok. +unregister_hooks(Host) -> + ejabberd_hooks:delete(c2s_stream_started, Host, ?MODULE, + c2s_stream_started, 50), ejabberd_hooks:delete(c2s_post_auth_features, Host, ?MODULE, add_stream_feature, 50), - ok. + ejabberd_hooks:delete(c2s_authenticated_packet, Host, ?MODULE, + c2s_authenticated_packet, 50), + ejabberd_hooks:delete(csi_activity, Host, ?MODULE, + csi_activity, 50), + ejabberd_hooks:delete(c2s_copy_session, Host, ?MODULE, + c2s_copy_session, 50), + ejabberd_hooks:delete(c2s_session_resumed, Host, ?MODULE, + c2s_session_resumed, 50), + ejabberd_hooks:delete(c2s_filter_send, Host, ?MODULE, + filter_other, 75). -add_stream_feature(Features, _Host) -> - Feature = #xmlel{name = <<"csi">>, - attrs = [{<<"xmlns">>, ?NS_CLIENT_STATE}], - children = []}, - [Feature | Features]. +%%-------------------------------------------------------------------- +%% ejabberd_hooks callbacks. +%%-------------------------------------------------------------------- +-spec c2s_stream_started(c2s_state(), stream_start()) -> c2s_state(). +c2s_stream_started(State, _) -> + init_csi_state(State). -filter_presence(_Action, #xmlel{name = <<"presence">>, attrs = Attrs}) -> - case xml:get_attr(<<"type">>, Attrs) of - {value, Type} when Type /= <<"unavailable">> -> - ?DEBUG("Got important presence stanza", []), - {stop, send}; - _ -> - ?DEBUG("Got availability presence stanza", []), - {stop, queue} +-spec c2s_authenticated_packet(c2s_state(), xmpp_element()) -> c2s_state(). +c2s_authenticated_packet(#{lserver := LServer} = C2SState, #csi{type = active}) -> + ejabberd_hooks:run_fold(csi_activity, LServer, C2SState, [active]); +c2s_authenticated_packet(#{lserver := LServer} = C2SState, #csi{type = inactive}) -> + ejabberd_hooks:run_fold(csi_activity, LServer, C2SState, [inactive]); +c2s_authenticated_packet(C2SState, _) -> + C2SState. + +-spec csi_activity(c2s_state(), active | inactive) -> c2s_state(). +csi_activity(C2SState, active) -> + C2SState1 = C2SState#{csi_state => active}, + flush_queue(C2SState1); +csi_activity(C2SState, inactive) -> + C2SState#{csi_state => inactive}. + +-spec c2s_copy_session(c2s_state(), c2s_state()) -> c2s_state(). +c2s_copy_session(C2SState, #{csi_queue := Q}) -> + C2SState#{csi_queue => Q}; +c2s_copy_session(C2SState, _) -> + C2SState. + +-spec c2s_session_resumed(c2s_state()) -> c2s_state(). +c2s_session_resumed(C2SState) -> + flush_queue(C2SState). + +-spec filter_presence(filter_acc()) -> filter_acc(). +filter_presence({#presence{meta = #{csi_resend := true}}, _} = Acc) -> + Acc; +filter_presence({#presence{to = To, type = Type} = Pres, + #{csi_state := inactive} = C2SState}) + when Type == available; Type == unavailable -> + ?DEBUG("Got availability presence stanza for ~ts", [jid:encode(To)]), + enqueue_stanza(presence, Pres, C2SState); +filter_presence(Acc) -> + Acc. + +-spec filter_chat_states(filter_acc()) -> filter_acc(). +filter_chat_states({#message{meta = #{csi_resend := true}}, _} = Acc) -> + Acc; +filter_chat_states({#message{from = From, to = To} = Msg, + #{csi_state := inactive} = C2SState} = Acc) -> + case misc:is_standalone_chat_state(Msg) of + true -> + case {From, To} of + {#jid{luser = U, lserver = S}, #jid{luser = U, lserver = S}} -> + %% Don't queue (carbon copies of) chat states from other + %% resources, as they might be used to sync the state of + %% conversations across clients. + Acc; + _ -> + ?DEBUG("Got standalone chat state notification for ~ts", + [jid:encode(To)]), + enqueue_stanza(chatstate, Msg, C2SState) + end; + false -> + Acc end; -filter_presence(Action, _Stanza) -> Action. +filter_chat_states(Acc) -> + Acc. -filter_chat_states(_Action, #xmlel{name = <<"message">>} = Stanza) -> - %% All XEP-0085 chat states except for : - ChatStates = [<<"active">>, <<"inactive">>, <<"composing">>, <<"paused">>], - Stripped = - lists:foldl(fun(ChatState, AccStanza) -> - xml:remove_subtags(AccStanza, ChatState, - {<<"xmlns">>, ?NS_CHATSTATES}) - end, Stanza, ChatStates), - case Stripped of - #xmlel{children = [#xmlel{name = <<"thread">>}]} -> - ?DEBUG("Got standalone chat state notification", []), - {stop, drop}; - #xmlel{children = []} -> - ?DEBUG("Got standalone chat state notification", []), - {stop, drop}; - _ -> - ?DEBUG("Got message with chat state notification", []), - {stop, send} +-spec filter_pep(filter_acc()) -> filter_acc(). +filter_pep({#message{meta = #{csi_resend := true}}, _} = Acc) -> + Acc; +filter_pep({#message{to = To} = Msg, + #{csi_state := inactive} = C2SState} = Acc) -> + case get_pep_node(Msg) of + undefined -> + Acc; + Node -> + ?DEBUG("Got PEP notification for ~ts", [jid:encode(To)]), + enqueue_stanza({pep, Node}, Msg, C2SState) end; -filter_chat_states(Action, _Stanza) -> Action. +filter_pep(Acc) -> + Acc. + +-spec filter_other(filter_acc()) -> filter_acc(). +filter_other({Stanza, #{jid := JID} = C2SState} = Acc) when ?is_stanza(Stanza) -> + case xmpp:get_meta(Stanza) of + #{csi_resend := true} -> + Acc; + _ -> + ?DEBUG("Won't add stanza for ~ts to CSI queue", [jid:encode(JID)]), + From = case xmpp:get_from(Stanza) of + undefined -> JID; + F -> F + end, + C2SState1 = dequeue_sender(From, C2SState), + {Stanza, C2SState1} + end; +filter_other(Acc) -> + Acc. + +-spec add_stream_feature([xmpp_element()], binary()) -> [xmpp_element()]. +add_stream_feature(Features, Host) -> + case gen_mod:is_loaded(Host, ?MODULE) of + true -> + [#feature_csi{} | Features]; + false -> + Features + end. + +%%-------------------------------------------------------------------- +%% Internal functions. +%%-------------------------------------------------------------------- +-spec init_csi_state(c2s_state()) -> c2s_state(). +init_csi_state(C2SState) -> + C2SState#{csi_state => active, csi_queue => queue_new()}. + +-spec enqueue_stanza(csi_type(), stanza(), c2s_state()) -> filter_acc(). +enqueue_stanza(Type, Stanza, #{csi_state := inactive, + csi_queue := Q} = C2SState) -> + case queue_len(Q) >= ?CSI_QUEUE_MAX of + true -> + ?DEBUG("CSI queue too large, going to flush it", []), + C2SState1 = flush_queue(C2SState), + enqueue_stanza(Type, Stanza, C2SState1); + false -> + From = jid:tolower(xmpp:get_from(Stanza)), + Q1 = queue_in({From, Type}, Stanza, Q), + {stop, {drop, C2SState#{csi_queue => Q1}}} + end; +enqueue_stanza(_Type, Stanza, State) -> + {Stanza, State}. + +-spec dequeue_sender(jid(), c2s_state()) -> c2s_state(). +dequeue_sender(#jid{luser = U, lserver = S} = Sender, + #{jid := JID} = C2SState) -> + case maps:get(csi_queue, C2SState, undefined) of + undefined -> + %% This may happen when the module is (re)loaded in runtime + init_csi_state(C2SState); + Q -> + ?DEBUG("Flushing packets of ~ts@~ts from CSI queue of ~ts", + [U, S, jid:encode(JID)]), + {Elems, Q1} = queue_take(Sender, Q), + C2SState1 = flush_stanzas(C2SState, Elems), + C2SState1#{csi_queue => Q1} + end. + +-spec flush_queue(c2s_state()) -> c2s_state(). +flush_queue(#{csi_queue := Q, jid := JID} = C2SState) -> + ?DEBUG("Flushing CSI queue of ~ts", [jid:encode(JID)]), + C2SState1 = flush_stanzas(C2SState, queue_to_list(Q)), + C2SState1#{csi_queue => queue_new()}. + +-spec flush_stanzas(c2s_state(), + [{csi_type(), csi_timestamp(), stanza()}]) -> c2s_state(). +flush_stanzas(#{lserver := LServer} = C2SState, Elems) -> + lists:foldl( + fun({Time, Stanza}, AccState) -> + Stanza1 = add_delay_info(Stanza, LServer, Time), + ejabberd_c2s:send(AccState, Stanza1) + end, C2SState, Elems). + +-spec add_delay_info(stanza(), binary(), csi_timestamp()) -> stanza(). +add_delay_info(Stanza, LServer, {_Seq, TimeStamp}) -> + Stanza1 = misc:add_delay_info( + Stanza, jid:make(LServer), TimeStamp, + <<"Client Inactive">>), + xmpp:put_meta(Stanza1, csi_resend, true). + +-spec get_pep_node(message()) -> binary() | undefined. +get_pep_node(#message{from = #jid{luser = <<>>}}) -> + %% It's not PEP. + undefined; +get_pep_node(#message{} = Msg) -> + case xmpp:get_subtag(Msg, #ps_event{}) of + #ps_event{items = #ps_items{node = Node}} -> + Node; + _ -> + undefined + end. + +%%-------------------------------------------------------------------- +%% Queue interface +%%-------------------------------------------------------------------- +-spec queue_new() -> csi_queue(). +queue_new() -> + {0, #{}}. + +-spec queue_in(csi_key(), stanza(), csi_queue()) -> csi_queue(). +queue_in(Key, Stanza, {Seq, Q}) -> + Seq1 = Seq + 1, + Time = {Seq1, erlang:timestamp()}, + Q1 = maps:put(Key, {Time, Stanza}, Q), + {Seq1, Q1}. + +-spec queue_take(jid(), csi_queue()) -> {[csi_element()], csi_queue()}. +queue_take(#jid{luser = LUser, lserver = LServer}, {Seq, Q}) -> + {Vals, Q1} = maps:fold(fun({{U, S, _}, _} = Key, Val, {AccVals, AccQ}) + when U == LUser, S == LServer -> + {[Val | AccVals], maps:remove(Key, AccQ)}; + (_, _, Acc) -> + Acc + end, {[], Q}, Q), + {lists:keysort(1, Vals), {Seq, Q1}}. + +-spec queue_len(csi_queue()) -> non_neg_integer(). +queue_len({_, Q}) -> + maps:size(Q). + +-spec queue_to_list(csi_queue()) -> [csi_element()]. +queue_to_list({_, Q}) -> + lists:keysort(1, maps:values(Q)). diff --git a/src/mod_client_state_opt.erl b/src/mod_client_state_opt.erl new file mode 100644 index 000000000..ff286dc15 --- /dev/null +++ b/src/mod_client_state_opt.erl @@ -0,0 +1,27 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_client_state_opt). + +-export([queue_chat_states/1]). +-export([queue_pep/1]). +-export([queue_presence/1]). + +-spec queue_chat_states(gen_mod:opts() | global | binary()) -> boolean(). +queue_chat_states(Opts) when is_map(Opts) -> + gen_mod:get_opt(queue_chat_states, Opts); +queue_chat_states(Host) -> + gen_mod:get_module_opt(Host, mod_client_state, queue_chat_states). + +-spec queue_pep(gen_mod:opts() | global | binary()) -> boolean(). +queue_pep(Opts) when is_map(Opts) -> + gen_mod:get_opt(queue_pep, Opts); +queue_pep(Host) -> + gen_mod:get_module_opt(Host, mod_client_state, queue_pep). + +-spec queue_presence(gen_mod:opts() | global | binary()) -> boolean(). +queue_presence(Opts) when is_map(Opts) -> + gen_mod:get_opt(queue_presence, Opts); +queue_presence(Host) -> + gen_mod:get_module_opt(Host, mod_client_state, queue_presence). + diff --git a/src/mod_configure.erl b/src/mod_configure.erl index 9e6e83e1c..81e7d5e7b 100644 --- a/src/mod_configure.erl +++ b/src/mod_configure.erl @@ -1,11 +1,11 @@ %%%---------------------------------------------------------------------- %%% File : mod_configure.erl %%% Author : Alexey Shchepin -%%% Purpose : Support for online configuration of ejabberd +%%% Purpose : Support for online configuration of ejabberd using XEP-0050 %%% Created : 19 Jan 2003 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -23,108 +23,65 @@ %%% %%%---------------------------------------------------------------------- -%%% Implements most of XEP-0133: Service Administration Version 1.1 -%%% (2005-08-19) - -module(mod_configure). -author('alexey@process-one.net'). +-protocol({xep, 133, '1.3.0', '13.10', "partial", ""}). + -behaviour(gen_mod). --export([start/2, stop/1, get_local_identity/5, +-export([start/2, stop/1, reload/3, get_local_identity/5, get_local_features/5, get_local_items/5, adhoc_local_items/4, adhoc_local_commands/4, get_sm_identity/5, get_sm_features/5, get_sm_items/5, - adhoc_sm_items/4, adhoc_sm_commands/4]). + adhoc_sm_items/4, adhoc_sm_commands/4, mod_options/1, + mod_opt_type/1, + depends/2, mod_doc/0]). --include("ejabberd.hrl"). -include("logger.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). +-include("ejabberd_sm.hrl"). +-include("translate.hrl"). +-include_lib("stdlib/include/ms_transform.hrl"). --include("jlib.hrl"). +start(_Host, _Opts) -> + {ok, [{hook, disco_local_items, get_local_items, 50}, + {hook, disco_local_features, get_local_features, 50}, + {hook, disco_local_identity, get_local_identity, 50}, + {hook, disco_sm_items, get_sm_items, 50}, + {hook, disco_sm_features, get_sm_features, 50}, + {hook, disco_sm_identity, get_sm_identity, 50}, + {hook, adhoc_local_items, adhoc_local_items, 50}, + {hook, adhoc_local_commands, adhoc_local_commands, 50}, + {hook, adhoc_sm_items, adhoc_sm_items, 50}, + {hook, adhoc_sm_commands, adhoc_sm_commands, 50}]}. --include("adhoc.hrl"). - --define(T(Lang, Text), translate:translate(Lang, Text)). - -%% Copied from ejabberd_sm.erl --record(session, {sid, usr, us, priority, info}). - -start(Host, _Opts) -> - ejabberd_hooks:add(disco_local_items, Host, ?MODULE, - get_local_items, 50), - ejabberd_hooks:add(disco_local_features, Host, ?MODULE, - get_local_features, 50), - ejabberd_hooks:add(disco_local_identity, Host, ?MODULE, - get_local_identity, 50), - ejabberd_hooks:add(disco_sm_items, Host, ?MODULE, - get_sm_items, 50), - ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, - get_sm_features, 50), - ejabberd_hooks:add(disco_sm_identity, Host, ?MODULE, - get_sm_identity, 50), - ejabberd_hooks:add(adhoc_local_items, Host, ?MODULE, - adhoc_local_items, 50), - ejabberd_hooks:add(adhoc_local_commands, Host, ?MODULE, - adhoc_local_commands, 50), - ejabberd_hooks:add(adhoc_sm_items, Host, ?MODULE, - adhoc_sm_items, 50), - ejabberd_hooks:add(adhoc_sm_commands, Host, ?MODULE, - adhoc_sm_commands, 50), +stop(_Host) -> ok. -stop(Host) -> - ejabberd_hooks:delete(adhoc_sm_commands, Host, ?MODULE, - adhoc_sm_commands, 50), - ejabberd_hooks:delete(adhoc_sm_items, Host, ?MODULE, - adhoc_sm_items, 50), - ejabberd_hooks:delete(adhoc_local_commands, Host, - ?MODULE, adhoc_local_commands, 50), - ejabberd_hooks:delete(adhoc_local_items, Host, ?MODULE, - adhoc_local_items, 50), - ejabberd_hooks:delete(disco_sm_identity, Host, ?MODULE, - get_sm_identity, 50), - ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, - get_sm_features, 50), - ejabberd_hooks:delete(disco_sm_items, Host, ?MODULE, - get_sm_items, 50), - ejabberd_hooks:delete(disco_local_identity, Host, - ?MODULE, get_local_identity, 50), - ejabberd_hooks:delete(disco_local_features, Host, - ?MODULE, get_local_features, 50), - ejabberd_hooks:delete(disco_local_items, Host, ?MODULE, - get_local_items, 50), - gen_iq_handler:remove_iq_handler(ejabberd_local, Host, - ?NS_COMMANDS), - gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, - ?NS_COMMANDS). +reload(_Host, _NewOpts, _OldOpts) -> + ok. + +depends(_Host, _Opts) -> + [{mod_adhoc, hard}, {mod_last, soft}]. %%%----------------------------------------------------------------------- -define(INFO_IDENTITY(Category, Type, Name, Lang), - [#xmlel{name = <<"identity">>, - attrs = - [{<<"category">>, Category}, {<<"type">>, Type}, - {<<"name">>, ?T(Lang, Name)}], - children = []}]). + [#identity{category = Category, type = Type, name = tr(Lang, Name)}]). -define(INFO_COMMAND(Name, Lang), ?INFO_IDENTITY(<<"automation">>, <<"command-node">>, Name, Lang)). -define(NODEJID(To, Name, Node), - #xmlel{name = <<"item">>, - attrs = - [{<<"jid">>, jlib:jid_to_string(To)}, - {<<"name">>, ?T(Lang, Name)}, {<<"node">>, Node}], - children = []}). + #disco_item{jid = To, name = tr(Lang, Name), node = Node}). -define(NODE(Name, Node), - #xmlel{name = <<"item">>, - attrs = - [{<<"jid">>, Server}, {<<"name">>, ?T(Lang, Name)}, - {<<"node">>, Node}], - children = []}). + #disco_item{jid = jid:make(Server), + node = Node, + name = tr(Lang, Name)}). -define(NS_ADMINX(Sub), <<(?NS_ADMIN)/binary, "#", Sub/binary>>). @@ -133,176 +90,176 @@ stop(Host) -> [<<"http:">>, <<"jabber.org">>, <<"protocol">>, <<"admin">>, Sub]). +-spec tokenize(binary()) -> [binary()]. tokenize(Node) -> str:tokens(Node, <<"/#">>). +acl_match_rule(Host, From) -> + Access = mod_configure_opt:access(Host), + acl:match_rule(Host, Access, From). + +-spec get_sm_identity([identity()], jid(), jid(), binary(), binary()) -> [identity()]. get_sm_identity(Acc, _From, _To, Node, Lang) -> case Node of <<"config">> -> - ?INFO_COMMAND(<<"Configuration">>, Lang); + ?INFO_COMMAND(?T("Configuration"), Lang); _ -> Acc end. +-spec get_local_identity([identity()], jid(), jid(), binary(), binary()) -> [identity()]. get_local_identity(Acc, _From, _To, Node, Lang) -> LNode = tokenize(Node), case LNode of [<<"running nodes">>, ENode] -> ?INFO_IDENTITY(<<"ejabberd">>, <<"node">>, ENode, Lang); [<<"running nodes">>, _ENode, <<"DB">>] -> - ?INFO_COMMAND(<<"Database">>, Lang); - [<<"running nodes">>, _ENode, <<"modules">>, - <<"start">>] -> - ?INFO_COMMAND(<<"Start Modules">>, Lang); - [<<"running nodes">>, _ENode, <<"modules">>, - <<"stop">>] -> - ?INFO_COMMAND(<<"Stop Modules">>, Lang); + ?INFO_COMMAND(?T("Database"), Lang); [<<"running nodes">>, _ENode, <<"backup">>, <<"backup">>] -> - ?INFO_COMMAND(<<"Backup">>, Lang); + ?INFO_COMMAND(?T("Backup"), Lang); [<<"running nodes">>, _ENode, <<"backup">>, <<"restore">>] -> - ?INFO_COMMAND(<<"Restore">>, Lang); + ?INFO_COMMAND(?T("Restore"), Lang); [<<"running nodes">>, _ENode, <<"backup">>, <<"textfile">>] -> - ?INFO_COMMAND(<<"Dump to Text File">>, Lang); + ?INFO_COMMAND(?T("Dump to Text File"), Lang); [<<"running nodes">>, _ENode, <<"import">>, <<"file">>] -> - ?INFO_COMMAND(<<"Import File">>, Lang); + ?INFO_COMMAND(?T("Import File"), Lang); [<<"running nodes">>, _ENode, <<"import">>, <<"dir">>] -> - ?INFO_COMMAND(<<"Import Directory">>, Lang); + ?INFO_COMMAND(?T("Import Directory"), Lang); [<<"running nodes">>, _ENode, <<"restart">>] -> - ?INFO_COMMAND(<<"Restart Service">>, Lang); + ?INFO_COMMAND(?T("Restart Service"), Lang); [<<"running nodes">>, _ENode, <<"shutdown">>] -> - ?INFO_COMMAND(<<"Shut Down Service">>, Lang); + ?INFO_COMMAND(?T("Shut Down Service"), Lang); ?NS_ADMINL(<<"add-user">>) -> - ?INFO_COMMAND(<<"Add User">>, Lang); + ?INFO_COMMAND(?T("Add User"), Lang); ?NS_ADMINL(<<"delete-user">>) -> - ?INFO_COMMAND(<<"Delete User">>, Lang); + ?INFO_COMMAND(?T("Delete User"), Lang); ?NS_ADMINL(<<"end-user-session">>) -> - ?INFO_COMMAND(<<"End User Session">>, Lang); - ?NS_ADMINL(<<"get-user-password">>) -> - ?INFO_COMMAND(<<"Get User Password">>, Lang); + ?INFO_COMMAND(?T("End User Session"), Lang); ?NS_ADMINL(<<"change-user-password">>) -> - ?INFO_COMMAND(<<"Change User Password">>, Lang); + ?INFO_COMMAND(?T("Change User Password"), Lang); ?NS_ADMINL(<<"get-user-lastlogin">>) -> - ?INFO_COMMAND(<<"Get User Last Login Time">>, Lang); + ?INFO_COMMAND(?T("Get User Last Login Time"), Lang); ?NS_ADMINL(<<"user-stats">>) -> - ?INFO_COMMAND(<<"Get User Statistics">>, Lang); - ?NS_ADMINL(<<"get-registered-users-num">>) -> - ?INFO_COMMAND(<<"Get Number of Registered Users">>, + ?INFO_COMMAND(?T("Get User Statistics"), Lang); + ?NS_ADMINL(<<"get-registered-users-list">>) -> + ?INFO_COMMAND(?T("Get List of Registered Users"), Lang); + ?NS_ADMINL(<<"get-registered-users-num">>) -> + ?INFO_COMMAND(?T("Get Number of Registered Users"), + Lang); + ?NS_ADMINL(<<"get-online-users-list">>) -> + ?INFO_COMMAND(?T("Get List of Online Users"), Lang); ?NS_ADMINL(<<"get-online-users-num">>) -> - ?INFO_COMMAND(<<"Get Number of Online Users">>, Lang); - [<<"config">>, <<"acls">>] -> - ?INFO_COMMAND(<<"Access Control Lists">>, Lang); - [<<"config">>, <<"access">>] -> - ?INFO_COMMAND(<<"Access Rules">>, Lang); + ?INFO_COMMAND(?T("Get Number of Online Users"), Lang); _ -> Acc end. %%%----------------------------------------------------------------------- --define(INFO_RESULT(Allow, Feats), +-define(INFO_RESULT(Allow, Feats, Lang), case Allow of - deny -> {error, ?ERR_FORBIDDEN}; + deny -> {error, xmpp:err_forbidden(?T("Access denied by service policy"), Lang)}; allow -> {result, Feats} end). +-spec get_sm_features(mod_disco:features_acc(), jid(), jid(), + binary(), binary()) -> mod_disco:features_acc(). get_sm_features(Acc, From, - #jid{lserver = LServer} = _To, Node, _Lang) -> + #jid{lserver = LServer} = _To, Node, Lang) -> case gen_mod:is_loaded(LServer, mod_adhoc) of false -> Acc; _ -> - Allow = acl:match_rule(LServer, configure, From), + Allow = acl_match_rule(LServer, From), case Node of - <<"config">> -> ?INFO_RESULT(Allow, [?NS_COMMANDS]); + <<"config">> -> ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); _ -> Acc end end. +-spec get_local_features(mod_disco:features_acc(), jid(), jid(), + binary(), binary()) -> mod_disco:features_acc(). get_local_features(Acc, From, - #jid{lserver = LServer} = _To, Node, _Lang) -> + #jid{lserver = LServer} = _To, Node, Lang) -> case gen_mod:is_loaded(LServer, mod_adhoc) of false -> Acc; _ -> LNode = tokenize(Node), - Allow = acl:match_rule(LServer, configure, From), + Allow = acl_match_rule(LServer, From), case LNode of - [<<"config">>] -> ?INFO_RESULT(Allow, []); - [<<"user">>] -> ?INFO_RESULT(Allow, []); - [<<"online users">>] -> ?INFO_RESULT(Allow, []); - [<<"all users">>] -> ?INFO_RESULT(Allow, []); + [<<"config">>] -> ?INFO_RESULT(Allow, [], Lang); + [<<"user">>] -> ?INFO_RESULT(Allow, [], Lang); + [<<"online users">>] -> ?INFO_RESULT(Allow, [], Lang); + [<<"all users">>] -> ?INFO_RESULT(Allow, [], Lang); [<<"all users">>, <<$@, _/binary>>] -> - ?INFO_RESULT(Allow, []); - [<<"outgoing s2s">> | _] -> ?INFO_RESULT(Allow, []); - [<<"running nodes">>] -> ?INFO_RESULT(Allow, []); - [<<"stopped nodes">>] -> ?INFO_RESULT(Allow, []); + ?INFO_RESULT(Allow, [], Lang); + [<<"outgoing s2s">> | _] -> ?INFO_RESULT(Allow, [], Lang); + [<<"running nodes">>] -> ?INFO_RESULT(Allow, [], Lang); + [<<"stopped nodes">>] -> ?INFO_RESULT(Allow, [], Lang); [<<"running nodes">>, _ENode] -> - ?INFO_RESULT(Allow, [?NS_STATS]); + ?INFO_RESULT(Allow, [?NS_STATS], Lang); [<<"running nodes">>, _ENode, <<"DB">>] -> - ?INFO_RESULT(Allow, [?NS_COMMANDS]); - [<<"running nodes">>, _ENode, <<"modules">>] -> - ?INFO_RESULT(Allow, []); - [<<"running nodes">>, _ENode, <<"modules">>, _] -> - ?INFO_RESULT(Allow, [?NS_COMMANDS]); + ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); [<<"running nodes">>, _ENode, <<"backup">>] -> - ?INFO_RESULT(Allow, []); + ?INFO_RESULT(Allow, [], Lang); [<<"running nodes">>, _ENode, <<"backup">>, _] -> - ?INFO_RESULT(Allow, [?NS_COMMANDS]); + ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); [<<"running nodes">>, _ENode, <<"import">>] -> - ?INFO_RESULT(Allow, []); + ?INFO_RESULT(Allow, [], Lang); [<<"running nodes">>, _ENode, <<"import">>, _] -> - ?INFO_RESULT(Allow, [?NS_COMMANDS]); + ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); [<<"running nodes">>, _ENode, <<"restart">>] -> - ?INFO_RESULT(Allow, [?NS_COMMANDS]); + ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); [<<"running nodes">>, _ENode, <<"shutdown">>] -> - ?INFO_RESULT(Allow, [?NS_COMMANDS]); + ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); [<<"config">>, _] -> - ?INFO_RESULT(Allow, [?NS_COMMANDS]); + ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMINL(<<"add-user">>) -> - ?INFO_RESULT(Allow, [?NS_COMMANDS]); + ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMINL(<<"delete-user">>) -> - ?INFO_RESULT(Allow, [?NS_COMMANDS]); + ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMINL(<<"end-user-session">>) -> - ?INFO_RESULT(Allow, [?NS_COMMANDS]); - ?NS_ADMINL(<<"get-user-password">>) -> - ?INFO_RESULT(Allow, [?NS_COMMANDS]); + ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMINL(<<"change-user-password">>) -> - ?INFO_RESULT(Allow, [?NS_COMMANDS]); + ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMINL(<<"get-user-lastlogin">>) -> - ?INFO_RESULT(Allow, [?NS_COMMANDS]); + ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMINL(<<"user-stats">>) -> - ?INFO_RESULT(Allow, [?NS_COMMANDS]); + ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); + ?NS_ADMINL(<<"get-registered-users-list">>) -> + ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMINL(<<"get-registered-users-num">>) -> - ?INFO_RESULT(Allow, [?NS_COMMANDS]); + ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); + ?NS_ADMINL(<<"get-online-users-list">>) -> + ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMINL(<<"get-online-users-num">>) -> - ?INFO_RESULT(Allow, [?NS_COMMANDS]); + ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); _ -> Acc end end. %%%----------------------------------------------------------------------- - +-spec adhoc_sm_items(mod_disco:items_acc(), + jid(), jid(), binary()) -> mod_disco:items_acc(). adhoc_sm_items(Acc, From, #jid{lserver = LServer} = To, Lang) -> - case acl:match_rule(LServer, configure, From) of + case acl_match_rule(LServer, From) of allow -> Items = case Acc of {result, Its} -> Its; empty -> [] end, - Nodes = [#xmlel{name = <<"item">>, - attrs = - [{<<"jid">>, jlib:jid_to_string(To)}, - {<<"name">>, ?T(Lang, <<"Configuration">>)}, - {<<"node">>, <<"config">>}], - children = []}], + Nodes = [#disco_item{jid = To, node = <<"config">>, + name = tr(Lang, ?T("Configuration"))}], {result, Items ++ Nodes}; _ -> Acc end. %%%----------------------------------------------------------------------- - +-spec get_sm_items(mod_disco:items_acc(), jid(), jid(), + binary(), binary()) -> mod_disco:items_acc(). get_sm_items(Acc, From, #jid{user = User, server = Server, lserver = LServer} = To, @@ -314,59 +271,59 @@ get_sm_items(Acc, From, {result, Its} -> Its; empty -> [] end, - case {acl:match_rule(LServer, configure, From), Node} of + case {acl_match_rule(LServer, From), Node} of {allow, <<"">>} -> - Nodes = [?NODEJID(To, <<"Configuration">>, + Nodes = [?NODEJID(To, ?T("Configuration"), <<"config">>), - ?NODEJID(To, <<"User Management">>, <<"user">>)], + ?NODEJID(To, ?T("User Management"), <<"user">>)], {result, Items ++ Nodes ++ get_user_resources(User, Server)}; {allow, <<"config">>} -> {result, []}; - {_, <<"config">>} -> {error, ?ERR_FORBIDDEN}; + {_, <<"config">>} -> + {error, xmpp:err_forbidden(?T("Access denied by service policy"), Lang)}; _ -> Acc end end. +-spec get_user_resources(binary(), binary()) -> [disco_item()]. get_user_resources(User, Server) -> Rs = ejabberd_sm:get_user_resources(User, Server), lists:map(fun (R) -> - #xmlel{name = <<"item">>, - attrs = - [{<<"jid">>, - <>}, - {<<"name">>, User}], - children = []} + #disco_item{jid = jid:make(User, Server, R), + name = User} end, lists:sort(Rs)). %%%----------------------------------------------------------------------- +-spec adhoc_local_items(mod_disco:items_acc(), + jid(), jid(), binary()) -> mod_disco:items_acc(). adhoc_local_items(Acc, From, #jid{lserver = LServer, server = Server} = To, Lang) -> - case acl:match_rule(LServer, configure, From) of + case acl_match_rule(LServer, From) of allow -> Items = case Acc of {result, Its} -> Its; empty -> [] end, - PermLev = get_permission_level(From), + PermLev = get_permission_level(From, LServer), Nodes = recursively_get_local_items(PermLev, LServer, <<"">>, Server, Lang), - Nodes1 = lists:filter(fun (N) -> - Nd = xml:get_tag_attr_s(<<"node">>, N), - F = get_local_features([], From, To, Nd, - Lang), - case F of - {result, [?NS_COMMANDS]} -> true; - _ -> false - end - end, - Nodes), + Nodes1 = lists:filter( + fun (#disco_item{node = Nd}) -> + F = get_local_features(empty, From, To, Nd, Lang), + case F of + {result, [?NS_COMMANDS]} -> true; + _ -> false + end + end, + Nodes), {result, Items ++ Nodes1}; _ -> Acc end. +-spec recursively_get_local_items(global | vhost, binary(), binary(), + binary(), binary()) -> [disco_item()]. recursively_get_local_items(_PermLev, _LServer, <<"online users">>, _Server, _Lang) -> []; @@ -382,28 +339,24 @@ recursively_get_local_items(PermLev, LServer, Node, {result, Res} -> Res; {error, _Error} -> [] end, - Nodes = lists:flatten(lists:map(fun (N) -> - S = xml:get_tag_attr_s(<<"jid">>, - N), - Nd = xml:get_tag_attr_s(<<"node">>, - N), - if (S /= Server) or - (Nd == <<"">>) -> - []; - true -> - [N, - recursively_get_local_items(PermLev, - LServer, - Nd, - Server, - Lang)] - end - end, - Items)), - Nodes. + lists:flatten( + lists:map( + fun(#disco_item{jid = #jid{server = S}, node = Nd} = Item) -> + if (S /= Server) or + (Nd == <<"">>) -> + []; + true -> + [Item, + recursively_get_local_items( + PermLev, LServer, Nd, Server, Lang)] + end + end, + Items)). -get_permission_level(JID) -> - case acl:match_rule(global, configure, JID) of +-spec get_permission_level(jid(), binary()) -> global | vhost. +get_permission_level(JID, Host) -> + Access = mod_configure_opt:access(Host), + case acl:match_rule(global, Access, JID) of allow -> global; deny -> vhost end. @@ -414,15 +367,17 @@ get_permission_level(JID) -> case Allow of deny -> Fallback; allow -> - PermLev = get_permission_level(From), + PermLev = get_permission_level(From, LServer), case get_local_items({PermLev, LServer}, LNode, - jlib:jid_to_string(To), Lang) + jid:encode(To), Lang) of {result, Res} -> {result, Res}; {error, Error} -> {error, Error} end end). +-spec get_local_items(mod_disco:items_acc(), jid(), jid(), + binary(), binary()) -> mod_disco:items_acc(). get_local_items(Acc, From, #jid{lserver = LServer} = To, <<"">>, Lang) -> case gen_mod:is_loaded(LServer, mod_adhoc) of @@ -432,13 +387,13 @@ get_local_items(Acc, From, #jid{lserver = LServer} = To, {result, Its} -> Its; empty -> [] end, - Allow = acl:match_rule(LServer, configure, From), + Allow = acl_match_rule(LServer, From), case Allow of deny -> {result, Items}; allow -> - PermLev = get_permission_level(From), + PermLev = get_permission_level(From, LServer), case get_local_items({PermLev, LServer}, [], - jlib:jid_to_string(To), Lang) + jid:encode(To), Lang) of {result, Res} -> {result, Items ++ Res}; {error, _Error} -> {result, Items} @@ -451,108 +406,103 @@ get_local_items(Acc, From, #jid{lserver = LServer} = To, false -> Acc; _ -> LNode = tokenize(Node), - Allow = acl:match_rule(LServer, configure, From), + Allow = acl_match_rule(LServer, From), + Err = xmpp:err_forbidden(?T("Access denied by service policy"), Lang), case LNode of [<<"config">>] -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"user">>] -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"online users">>] -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"all users">>] -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"all users">>, <<$@, _/binary>>] -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"outgoing s2s">> | _] -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"running nodes">>] -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"stopped nodes">>] -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"running nodes">>, _ENode] -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"running nodes">>, _ENode, <<"DB">>] -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); - [<<"running nodes">>, _ENode, <<"modules">>] -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); - [<<"running nodes">>, _ENode, <<"modules">>, _] -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"running nodes">>, _ENode, <<"backup">>] -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"running nodes">>, _ENode, <<"backup">>, _] -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"running nodes">>, _ENode, <<"import">>] -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"running nodes">>, _ENode, <<"import">>, _] -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"running nodes">>, _ENode, <<"restart">>] -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"running nodes">>, _ENode, <<"shutdown">>] -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"config">>, _] -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); ?NS_ADMINL(<<"add-user">>) -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); ?NS_ADMINL(<<"delete-user">>) -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); ?NS_ADMINL(<<"end-user-session">>) -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); - ?NS_ADMINL(<<"get-user-password">>) -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); ?NS_ADMINL(<<"change-user-password">>) -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); ?NS_ADMINL(<<"get-user-lastlogin">>) -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); ?NS_ADMINL(<<"user-stats">>) -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); + ?NS_ADMINL(<<"get-registered-users-list">>) -> + ?ITEMS_RESULT(Allow, LNode, {error, Err}); ?NS_ADMINL(<<"get-registered-users-num">>) -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); + ?NS_ADMINL(<<"get-online-users-list">>) -> + ?ITEMS_RESULT(Allow, LNode, {error, Err}); ?NS_ADMINL(<<"get-online-users-num">>) -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); _ -> Acc end end. %%%----------------------------------------------------------------------- - -%% @spec ({PermissionLevel, Host}, [string()], Server::string(), Lang) -%% -> {result, [xmlelement()]} -%% PermissionLevel = global | vhost +-spec get_local_items({global | vhost, binary()}, [binary()], + binary(), binary()) -> {result, [disco_item()]} | {error, stanza_error()}. get_local_items(_Host, [], Server, Lang) -> {result, - [?NODE(<<"Configuration">>, <<"config">>), - ?NODE(<<"User Management">>, <<"user">>), - ?NODE(<<"Online Users">>, <<"online users">>), - ?NODE(<<"All Users">>, <<"all users">>), - ?NODE(<<"Outgoing s2s Connections">>, + [?NODE(?T("Configuration"), <<"config">>), + ?NODE(?T("User Management"), <<"user">>), + ?NODE(?T("Online Users"), <<"online users">>), + ?NODE(?T("All Users"), <<"all users">>), + ?NODE(?T("Outgoing s2s Connections"), <<"outgoing s2s">>), - ?NODE(<<"Running Nodes">>, <<"running nodes">>), - ?NODE(<<"Stopped Nodes">>, <<"stopped nodes">>)]}; -get_local_items(_Host, [<<"config">>], Server, Lang) -> - {result, - [?NODE(<<"Access Control Lists">>, <<"config/acls">>), - ?NODE(<<"Access Rules">>, <<"config/access">>)]}; + ?NODE(?T("Running Nodes"), <<"running nodes">>), + ?NODE(?T("Stopped Nodes"), <<"stopped nodes">>)]}; get_local_items(_Host, [<<"config">>, _], _Server, _Lang) -> {result, []}; get_local_items(_Host, [<<"user">>], Server, Lang) -> {result, - [?NODE(<<"Add User">>, (?NS_ADMINX(<<"add-user">>))), - ?NODE(<<"Delete User">>, + [?NODE(?T("Add User"), (?NS_ADMINX(<<"add-user">>))), + ?NODE(?T("Delete User"), (?NS_ADMINX(<<"delete-user">>))), - ?NODE(<<"End User Session">>, + ?NODE(?T("End User Session"), (?NS_ADMINX(<<"end-user-session">>))), - ?NODE(<<"Get User Password">>, - (?NS_ADMINX(<<"get-user-password">>))), - ?NODE(<<"Change User Password">>, + ?NODE(?T("Change User Password"), (?NS_ADMINX(<<"change-user-password">>))), - ?NODE(<<"Get User Last Login Time">>, + ?NODE(?T("Get User Last Login Time"), (?NS_ADMINX(<<"get-user-lastlogin">>))), - ?NODE(<<"Get User Statistics">>, + ?NODE(?T("Get User Statistics"), (?NS_ADMINX(<<"user-stats">>))), - ?NODE(<<"Get Number of Registered Users">>, + ?NODE(?T("Get List of Registered Users"), + (?NS_ADMINX(<<"get-registered-users-list">>))), + ?NODE(?T("Get Number of Registered Users"), (?NS_ADMINX(<<"get-registered-users-num">>))), - ?NODE(<<"Get Number of Online Users">>, + ?NODE(?T("Get List of Online Users"), + (?NS_ADMINX(<<"get-online-users-list">>))), + ?NODE(?T("Get Number of Online Users"), (?NS_ADMINX(<<"get-online-users-num">>)))]}; get_local_items(_Host, [<<"http:">> | _], _Server, _Lang) -> @@ -566,33 +516,20 @@ get_local_items({_, Host}, [<<"all users">>], _Server, get_local_items({_, Host}, [<<"all users">>, <<$@, Diap/binary>>], _Server, _Lang) -> - case catch ejabberd_auth:get_vh_registered_users(Host) - of - {'EXIT', _Reason} -> ?ERR_INTERNAL_SERVER_ERROR; - Users -> - SUsers = lists:sort([{S, U} || {U, S} <- Users]), - case catch begin - [S1, S2] = ejabberd_regexp:split(Diap, <<"-">>), - N1 = jlib:binary_to_integer(S1), - N2 = jlib:binary_to_integer(S2), - Sub = lists:sublist(SUsers, N1, N2 - N1 + 1), - lists:map(fun ({S, U}) -> - #xmlel{name = <<"item">>, - attrs = - [{<<"jid">>, - <>}, - {<<"name">>, - <>}], - children = []} - end, - Sub) - end - of - {'EXIT', _Reason} -> ?ERR_NOT_ACCEPTABLE; - Res -> {result, Res} - end + Users = ejabberd_auth:get_users(Host), + SUsers = lists:sort([{S, U} || {U, S} <- Users]), + try + [S1, S2] = ejabberd_regexp:split(Diap, <<"-">>), + N1 = binary_to_integer(S1), + N2 = binary_to_integer(S2), + Sub = lists:sublist(SUsers, N1, N2 - N1 + 1), + {result, lists:map( + fun({S, U}) -> + #disco_item{jid = jid:make(U, S), + name = <>} + end, Sub)} + catch _:_ -> + {error, xmpp:err_not_acceptable()} end; get_local_items({_, Host}, [<<"outgoing s2s">>], _Server, Lang) -> @@ -609,48 +546,29 @@ get_local_items(_Host, [<<"stopped nodes">>], _Server, get_local_items({global, _Host}, [<<"running nodes">>, ENode], Server, Lang) -> {result, - [?NODE(<<"Database">>, + [?NODE(?T("Database"), <<"running nodes/", ENode/binary, "/DB">>), - ?NODE(<<"Modules">>, - <<"running nodes/", ENode/binary, "/modules">>), - ?NODE(<<"Backup Management">>, + ?NODE(?T("Backup Management"), <<"running nodes/", ENode/binary, "/backup">>), - ?NODE(<<"Import Users From jabberd14 Spool Files">>, + ?NODE(?T("Import Users From jabberd14 Spool Files"), <<"running nodes/", ENode/binary, "/import">>), - ?NODE(<<"Restart Service">>, + ?NODE(?T("Restart Service"), <<"running nodes/", ENode/binary, "/restart">>), - ?NODE(<<"Shut Down Service">>, + ?NODE(?T("Shut Down Service"), <<"running nodes/", ENode/binary, "/shutdown">>)]}; -get_local_items({vhost, _Host}, - [<<"running nodes">>, ENode], Server, Lang) -> - {result, - [?NODE(<<"Modules">>, - <<"running nodes/", ENode/binary, "/modules">>)]}; get_local_items(_Host, [<<"running nodes">>, _ENode, <<"DB">>], _Server, _Lang) -> {result, []}; -get_local_items(_Host, - [<<"running nodes">>, ENode, <<"modules">>], Server, - Lang) -> - {result, - [?NODE(<<"Start Modules">>, - <<"running nodes/", ENode/binary, "/modules/start">>), - ?NODE(<<"Stop Modules">>, - <<"running nodes/", ENode/binary, "/modules/stop">>)]}; -get_local_items(_Host, - [<<"running nodes">>, _ENode, <<"modules">>, _], - _Server, _Lang) -> - {result, []}; get_local_items(_Host, [<<"running nodes">>, ENode, <<"backup">>], Server, Lang) -> {result, - [?NODE(<<"Backup">>, + [?NODE(?T("Backup"), <<"running nodes/", ENode/binary, "/backup/backup">>), - ?NODE(<<"Restore">>, + ?NODE(?T("Restore"), <<"running nodes/", ENode/binary, "/backup/restore">>), - ?NODE(<<"Dump to Text File">>, + ?NODE(?T("Dump to Text File"), <<"running nodes/", ENode/binary, "/backup/textfile">>)]}; get_local_items(_Host, @@ -661,9 +579,9 @@ get_local_items(_Host, [<<"running nodes">>, ENode, <<"import">>], Server, Lang) -> {result, - [?NODE(<<"Import File">>, + [?NODE(?T("Import File"), <<"running nodes/", ENode/binary, "/import/file">>), - ?NODE(<<"Import Directory">>, + ?NODE(?T("Import Directory"), <<"running nodes/", ENode/binary, "/import/dir">>)]}; get_local_items(_Host, [<<"running nodes">>, _ENode, <<"import">>, _], _Server, @@ -678,1062 +596,665 @@ get_local_items(_Host, _Lang) -> {result, []}; get_local_items(_Host, _, _Server, _Lang) -> - {error, ?ERR_ITEM_NOT_FOUND}. + {error, xmpp:err_item_not_found()}. +-spec get_online_vh_users(binary()) -> [disco_item()]. get_online_vh_users(Host) -> - case catch ejabberd_sm:get_vh_session_list(Host) of - {'EXIT', _Reason} -> []; - USRs -> - SURs = lists:sort([{S, U, R} || {U, S, R} <- USRs]), - lists:map(fun ({S, U, R}) -> - #xmlel{name = <<"item">>, - attrs = - [{<<"jid">>, - <>}, - {<<"name">>, - <>}], - children = []} - end, - SURs) - end. + USRs = ejabberd_sm:get_vh_session_list(Host), + SURs = lists:sort([{S, U, R} || {U, S, R} <- USRs]), + lists:map( + fun({S, U, R}) -> + #disco_item{jid = jid:make(U, S, R), + name = <>} + end, SURs). +-spec get_all_vh_users(binary()) -> [disco_item()]. get_all_vh_users(Host) -> - case catch ejabberd_auth:get_vh_registered_users(Host) - of - {'EXIT', _Reason} -> []; - Users -> - SUsers = lists:sort([{S, U} || {U, S} <- Users]), - case length(SUsers) of - N when N =< 100 -> - lists:map(fun ({S, U}) -> - #xmlel{name = <<"item">>, - attrs = - [{<<"jid">>, - <>}, - {<<"name">>, - <>}], - children = []} - end, - SUsers); - N -> - NParts = trunc(math:sqrt(N * 6.17999999999999993783e-1)) - + 1, - M = trunc(N / NParts) + 1, - lists:map(fun (K) -> - L = K + M - 1, - Node = <<"@", - (iolist_to_binary(integer_to_list(K)))/binary, - "-", - (iolist_to_binary(integer_to_list(L)))/binary>>, - {FS, FU} = lists:nth(K, SUsers), - {LS, LU} = if L < N -> lists:nth(L, SUsers); - true -> lists:last(SUsers) - end, - Name = <>, - #xmlel{name = <<"item">>, - attrs = - [{<<"jid">>, Host}, - {<<"node">>, - <<"all users/", Node/binary>>}, - {<<"name">>, Name}], - children = []} - end, - lists:seq(1, N, M)) - end + Users = ejabberd_auth:get_users(Host), + SUsers = lists:sort([{S, U} || {U, S} <- Users]), + case length(SUsers) of + N when N =< 100 -> + lists:map(fun({S, U}) -> + #disco_item{jid = jid:make(U, S), + name = <>} + end, SUsers); + N -> + NParts = trunc(math:sqrt(N * 6.17999999999999993783e-1)) + 1, + M = trunc(N / NParts) + 1, + lists:map( + fun (K) -> + L = K + M - 1, + Node = <<"@", + (integer_to_binary(K))/binary, + "-", + (integer_to_binary(L))/binary>>, + {FS, FU} = lists:nth(K, SUsers), + {LS, LU} = if L < N -> lists:nth(L, SUsers); + true -> lists:last(SUsers) + end, + Name = <>, + #disco_item{jid = jid:make(Host), + node = <<"all users/", Node/binary>>, + name = Name} + end, lists:seq(1, N, M)) end. +-spec get_outgoing_s2s(binary(), binary()) -> [disco_item()]. get_outgoing_s2s(Host, Lang) -> - case catch ejabberd_s2s:dirty_get_connections() of - {'EXIT', _Reason} -> []; - Connections -> - DotHost = <<".", Host/binary>>, - TConns = [TH - || {FH, TH} <- Connections, - Host == FH orelse str:suffix(DotHost, FH)], - lists:map(fun (T) -> - #xmlel{name = <<"item">>, - attrs = - [{<<"jid">>, Host}, - {<<"node">>, - <<"outgoing s2s/", T/binary>>}, - {<<"name">>, - iolist_to_binary(io_lib:format(?T(Lang, - <<"To ~s">>), - [T]))}], - children = []} - end, - lists:usort(TConns)) - end. + Connections = ejabberd_s2s:dirty_get_connections(), + DotHost = <<".", Host/binary>>, + TConns = [TH || {FH, TH} <- Connections, + Host == FH orelse str:suffix(DotHost, FH)], + lists:map( + fun (T) -> + Name = str:translate_and_format(Lang, ?T("To ~ts"),[T]), + #disco_item{jid = jid:make(Host), + node = <<"outgoing s2s/", T/binary>>, + name = Name} + end, lists:usort(TConns)). +-spec get_outgoing_s2s(binary(), binary(), binary()) -> [disco_item()]. get_outgoing_s2s(Host, Lang, To) -> - case catch ejabberd_s2s:dirty_get_connections() of - {'EXIT', _Reason} -> []; - Connections -> - lists:map(fun ({F, _T}) -> - #xmlel{name = <<"item">>, - attrs = - [{<<"jid">>, Host}, - {<<"node">>, - <<"outgoing s2s/", To/binary, "/", - F/binary>>}, - {<<"name">>, - iolist_to_binary(io_lib:format(?T(Lang, - <<"From ~s">>), - [F]))}], - children = []} - end, - lists:keysort(1, - lists:filter(fun (E) -> element(2, E) == To - end, - Connections))) - end. + Connections = ejabberd_s2s:dirty_get_connections(), + lists:map( + fun ({F, _T}) -> + Node = <<"outgoing s2s/", To/binary, "/", F/binary>>, + Name = str:translate_and_format(Lang, ?T("From ~ts"), [F]), + #disco_item{jid = jid:make(Host), node = Node, name = Name} + end, + lists:keysort( + 1, + lists:filter(fun (E) -> element(2, E) == To end, + Connections))). +-spec get_running_nodes(binary(), binary()) -> [disco_item()]. get_running_nodes(Server, _Lang) -> - case catch mnesia:system_info(running_db_nodes) of - {'EXIT', _Reason} -> []; - DBNodes -> - lists:map(fun (N) -> - S = iolist_to_binary(atom_to_list(N)), - #xmlel{name = <<"item">>, - attrs = - [{<<"jid">>, Server}, - {<<"node">>, - <<"running nodes/", S/binary>>}, - {<<"name">>, S}], - children = []} - end, - lists:sort(DBNodes)) - end. + DBNodes = mnesia:system_info(running_db_nodes), + lists:map( + fun (N) -> + S = iolist_to_binary(atom_to_list(N)), + #disco_item{jid = jid:make(Server), + node = <<"running nodes/", S/binary>>, + name = S} + end, lists:sort(DBNodes)). +-spec get_stopped_nodes(binary()) -> [disco_item()]. get_stopped_nodes(_Lang) -> - case catch lists:usort(mnesia:system_info(db_nodes) ++ - mnesia:system_info(extra_db_nodes)) - -- mnesia:system_info(running_db_nodes) - of - {'EXIT', _Reason} -> []; - DBNodes -> - lists:map(fun (N) -> - S = iolist_to_binary(atom_to_list(N)), - #xmlel{name = <<"item">>, - attrs = - [{<<"jid">>, ?MYNAME}, - {<<"node">>, - <<"stopped nodes/", S/binary>>}, - {<<"name">>, S}], - children = []} - end, - lists:sort(DBNodes)) - end. + DBNodes = lists:usort(mnesia:system_info(db_nodes) ++ + mnesia:system_info(extra_db_nodes)) + -- mnesia:system_info(running_db_nodes), + lists:map( + fun (N) -> + S = iolist_to_binary(atom_to_list(N)), + #disco_item{jid = jid:make(ejabberd_config:get_myname()), + node = <<"stopped nodes/", S/binary>>, + name = S} + end, lists:sort(DBNodes)). %%------------------------------------------------------------------------- -define(COMMANDS_RESULT(LServerOrGlobal, From, To, - Request), - case acl:match_rule(LServerOrGlobal, configure, From) of - deny -> {error, ?ERR_FORBIDDEN}; + Request, Lang), + case acl_match_rule(LServerOrGlobal, From) of + deny -> {error, xmpp:err_forbidden(?T("Access denied by service policy"), Lang)}; allow -> adhoc_local_commands(From, To, Request) end). +-spec adhoc_local_commands(adhoc_command(), jid(), jid(), adhoc_command()) -> + adhoc_command() | {error, stanza_error()}. adhoc_local_commands(Acc, From, #jid{lserver = LServer} = To, - #adhoc_request{node = Node} = Request) -> + #adhoc_command{node = Node, lang = Lang} = Request) -> LNode = tokenize(Node), case LNode of [<<"running nodes">>, _ENode, <<"DB">>] -> - ?COMMANDS_RESULT(global, From, To, Request); - [<<"running nodes">>, _ENode, <<"modules">>, _] -> - ?COMMANDS_RESULT(LServer, From, To, Request); + ?COMMANDS_RESULT(global, From, To, Request, Lang); [<<"running nodes">>, _ENode, <<"backup">>, _] -> - ?COMMANDS_RESULT(global, From, To, Request); + ?COMMANDS_RESULT(global, From, To, Request, Lang); [<<"running nodes">>, _ENode, <<"import">>, _] -> - ?COMMANDS_RESULT(global, From, To, Request); + ?COMMANDS_RESULT(global, From, To, Request, Lang); [<<"running nodes">>, _ENode, <<"restart">>] -> - ?COMMANDS_RESULT(global, From, To, Request); + ?COMMANDS_RESULT(global, From, To, Request, Lang); [<<"running nodes">>, _ENode, <<"shutdown">>] -> - ?COMMANDS_RESULT(global, From, To, Request); + ?COMMANDS_RESULT(global, From, To, Request, Lang); [<<"config">>, _] -> - ?COMMANDS_RESULT(LServer, From, To, Request); + ?COMMANDS_RESULT(LServer, From, To, Request, Lang); ?NS_ADMINL(_) -> - ?COMMANDS_RESULT(LServer, From, To, Request); + ?COMMANDS_RESULT(LServer, From, To, Request, Lang); _ -> Acc end. +-spec adhoc_local_commands(jid(), jid(), adhoc_command()) -> adhoc_command() | {error, stanza_error()}. adhoc_local_commands(From, #jid{lserver = LServer} = _To, - #adhoc_request{lang = Lang, node = Node, - sessionid = SessionID, action = Action, - xdata = XData} = - Request) -> + #adhoc_command{lang = Lang, node = Node, + sid = SessionID, action = Action, + xdata = XData} = Request) -> LNode = tokenize(Node), - ActionIsExecute = lists:member(Action, - [<<"">>, <<"execute">>, <<"complete">>]), - if Action == <<"cancel">> -> - adhoc:produce_response(Request, - #adhoc_response{status = canceled}); - XData == false, ActionIsExecute -> + ActionIsExecute = Action == execute orelse Action == complete, + if Action == cancel -> + #adhoc_command{status = canceled, lang = Lang, + node = Node, sid = SessionID}; + XData == undefined, ActionIsExecute -> case get_form(LServer, LNode, Lang) of {result, Form} -> - adhoc:produce_response(Request, - #adhoc_response{status = executing, - elements = Form}); + xmpp_util:make_adhoc_response( + Request, + #adhoc_command{status = executing, xdata = Form}); {result, Status, Form} -> - adhoc:produce_response(Request, - #adhoc_response{status = Status, - elements = Form}); + xmpp_util:make_adhoc_response( + Request, + #adhoc_command{status = Status, xdata = Form}); {error, Error} -> {error, Error} end; - XData /= false, ActionIsExecute -> - case jlib:parse_xdata_submit(XData) of - invalid -> {error, ?ERR_BAD_REQUEST}; - Fields -> - case catch set_form(From, LServer, LNode, Lang, Fields) - of - {result, Res} -> - adhoc:produce_response(#adhoc_response{lang = Lang, - node = Node, - sessionid = - SessionID, - elements = Res, - status = - completed}); - {'EXIT', _} -> {error, ?ERR_BAD_REQUEST}; - {error, Error} -> {error, Error} - end - end; - true -> {error, ?ERR_BAD_REQUEST} + XData /= undefined, ActionIsExecute -> + case set_form(From, LServer, LNode, Lang, XData) of + {result, Res} -> + xmpp_util:make_adhoc_response( + Request, + #adhoc_command{xdata = Res, status = completed}); + %%{'EXIT', _} -> {error, xmpp:err_bad_request()}; + {error, Error} -> {error, Error} + end; + true -> + {error, xmpp:err_bad_request(?T("Unexpected action"), Lang)} end. -define(TVFIELD(Type, Var, Val), - #xmlel{name = <<"field">>, - attrs = [{<<"type">>, Type}, {<<"var">>, Var}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, Val}]}]}). + #xdata_field{type = Type, var = Var, values = [Val]}). -define(HFIELD(), - ?TVFIELD(<<"hidden">>, <<"FORM_TYPE">>, (?NS_ADMIN))). + ?TVFIELD(hidden, <<"FORM_TYPE">>, (?NS_ADMIN))). -define(TLFIELD(Type, Label, Var), - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, Type}, {<<"label">>, ?T(Lang, Label)}, - {<<"var">>, Var}], - children = []}). + #xdata_field{type = Type, label = tr(Lang, Label), var = Var}). -define(XFIELD(Type, Label, Var, Val), - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, Type}, {<<"label">>, ?T(Lang, Label)}, - {<<"var">>, Var}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, Val}]}]}). + #xdata_field{type = Type, label = tr(Lang, Label), + var = Var, values = [Val]}). -define(XMFIELD(Type, Label, Var, Vals), - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, Type}, {<<"label">>, ?T(Lang, Label)}, - {<<"var">>, Var}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, Val}]} - || Val <- Vals]}). + #xdata_field{type = Type, label = tr(Lang, Label), + var = Var, values = Vals}). -define(TABLEFIELD(Table, Val), - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, <<"list-single">>}, - {<<"label">>, iolist_to_binary(atom_to_list(Table))}, - {<<"var">>, iolist_to_binary(atom_to_list(Table))}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = - [{xmlcdata, - iolist_to_binary(atom_to_list(Val))}]}, - #xmlel{name = <<"option">>, - attrs = [{<<"label">>, ?T(Lang, <<"RAM copy">>)}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = - [{xmlcdata, <<"ram_copies">>}]}]}, - #xmlel{name = <<"option">>, - attrs = - [{<<"label">>, - ?T(Lang, <<"RAM and disc copy">>)}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = - [{xmlcdata, <<"disc_copies">>}]}]}, - #xmlel{name = <<"option">>, - attrs = - [{<<"label">>, ?T(Lang, <<"Disc only copy">>)}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = - [{xmlcdata, - <<"disc_only_copies">>}]}]}, - #xmlel{name = <<"option">>, - attrs = [{<<"label">>, ?T(Lang, <<"Remote copy">>)}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = - [{xmlcdata, <<"unknown">>}]}]}]}). + #xdata_field{ + type = 'list-single', + label = iolist_to_binary(atom_to_list(Table)), + var = iolist_to_binary(atom_to_list(Table)), + values = [iolist_to_binary(atom_to_list(Val))], + options = [#xdata_option{label = tr(Lang, ?T("RAM copy")), + value = <<"ram_copies">>}, + #xdata_option{label = tr(Lang, ?T("RAM and disc copy")), + value = <<"disc_copies">>}, + #xdata_option{label = tr(Lang, ?T("Disc only copy")), + value = <<"disc_only_copies">>}, + #xdata_option{label = tr(Lang, ?T("Remote copy")), + value = <<"unknown">>}]}). +-spec get_form(binary(), [binary()], binary()) -> {result, xdata()} | + {result, completed, xdata()} | + {error, stanza_error()}. get_form(_Host, [<<"running nodes">>, ENode, <<"DB">>], Lang) -> case search_running_node(ENode) of - false -> {error, ?ERR_ITEM_NOT_FOUND}; + false -> + Txt = ?T("No running node found"), + {error, xmpp:err_item_not_found(Txt, Lang)}; Node -> - case rpc:call(Node, mnesia, system_info, [tables]) of - {badrpc, _Reason} -> - {error, ?ERR_INTERNAL_SERVER_ERROR}; + case ejabberd_cluster:call(Node, mnesia, system_info, [tables]) of + {badrpc, Reason} -> + ?ERROR_MSG("RPC call mnesia:system_info(tables) on node " + "~ts failed: ~p", [Node, Reason]), + {error, xmpp:err_internal_server_error()}; Tables -> STables = lists:sort(Tables), - {result, - [#xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_XDATA}], - children = - [?HFIELD(), - #xmlel{name = <<"title">>, attrs = [], - children = - [{xmlcdata, - <<(?T(Lang, - <<"Database Tables Configuration at ">>))/binary, - ENode/binary>>}]}, - #xmlel{name = <<"instructions">>, attrs = [], - children = - [{xmlcdata, - ?T(Lang, - <<"Choose storage type of tables">>)}]} - | lists:map(fun (Table) -> - case rpc:call(Node, mnesia, - table_info, - [Table, - storage_type]) - of - {badrpc, _} -> - ?TABLEFIELD(Table, - unknown); - Type -> - ?TABLEFIELD(Table, Type) - end - end, - STables)]}]} - end - end; -get_form(Host, - [<<"running nodes">>, ENode, <<"modules">>, <<"stop">>], - Lang) -> - case search_running_node(ENode) of - false -> {error, ?ERR_ITEM_NOT_FOUND}; - Node -> - case rpc:call(Node, gen_mod, loaded_modules, [Host]) of - {badrpc, _Reason} -> - {error, ?ERR_INTERNAL_SERVER_ERROR}; - Modules -> - SModules = lists:sort(Modules), - {result, - [#xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_XDATA}], - children = - [?HFIELD(), - #xmlel{name = <<"title">>, attrs = [], - children = - [{xmlcdata, - <<(?T(Lang, - <<"Stop Modules at ">>))/binary, - ENode/binary>>}]}, - #xmlel{name = <<"instructions">>, attrs = [], - children = - [{xmlcdata, - ?T(Lang, - <<"Choose modules to stop">>)}]} - | lists:map(fun (M) -> - S = jlib:atom_to_binary(M), - ?XFIELD(<<"boolean">>, S, S, - <<"0">>) - end, - SModules)]}]} - end - end; -get_form(_Host, - [<<"running nodes">>, ENode, <<"modules">>, - <<"start">>], - Lang) -> - {result, - [#xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_XDATA}], - children = - [?HFIELD(), - #xmlel{name = <<"title">>, attrs = [], - children = - [{xmlcdata, - <<(?T(Lang, <<"Start Modules at ">>))/binary, - ENode/binary>>}]}, - #xmlel{name = <<"instructions">>, attrs = [], - children = - [{xmlcdata, - ?T(Lang, - <<"Enter list of {Module, [Options]}">>)}]}, - ?XFIELD(<<"text-multi">>, - <<"List of modules to start">>, <<"modules">>, - <<"[].">>)]}]}; -get_form(_Host, - [<<"running nodes">>, ENode, <<"backup">>, - <<"backup">>], - Lang) -> - {result, - [#xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_XDATA}], - children = - [?HFIELD(), - #xmlel{name = <<"title">>, attrs = [], - children = - [{xmlcdata, - <<(?T(Lang, <<"Backup to File at ">>))/binary, - ENode/binary>>}]}, - #xmlel{name = <<"instructions">>, attrs = [], - children = - [{xmlcdata, - ?T(Lang, <<"Enter path to backup file">>)}]}, - ?XFIELD(<<"text-single">>, <<"Path to File">>, - <<"path">>, <<"">>)]}]}; -get_form(_Host, - [<<"running nodes">>, ENode, <<"backup">>, - <<"restore">>], - Lang) -> - {result, - [#xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_XDATA}], - children = - [?HFIELD(), - #xmlel{name = <<"title">>, attrs = [], - children = - [{xmlcdata, - <<(?T(Lang, - <<"Restore Backup from File at ">>))/binary, - ENode/binary>>}]}, - #xmlel{name = <<"instructions">>, attrs = [], - children = - [{xmlcdata, - ?T(Lang, <<"Enter path to backup file">>)}]}, - ?XFIELD(<<"text-single">>, <<"Path to File">>, - <<"path">>, <<"">>)]}]}; -get_form(_Host, - [<<"running nodes">>, ENode, <<"backup">>, - <<"textfile">>], - Lang) -> - {result, - [#xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_XDATA}], - children = - [?HFIELD(), - #xmlel{name = <<"title">>, attrs = [], - children = - [{xmlcdata, - <<(?T(Lang, - <<"Dump Backup to Text File at ">>))/binary, - ENode/binary>>}]}, - #xmlel{name = <<"instructions">>, attrs = [], - children = - [{xmlcdata, - ?T(Lang, <<"Enter path to text file">>)}]}, - ?XFIELD(<<"text-single">>, <<"Path to File">>, - <<"path">>, <<"">>)]}]}; -get_form(_Host, - [<<"running nodes">>, ENode, <<"import">>, <<"file">>], - Lang) -> - {result, - [#xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_XDATA}], - children = - [?HFIELD(), - #xmlel{name = <<"title">>, attrs = [], - children = - [{xmlcdata, - <<(?T(Lang, - <<"Import User from File at ">>))/binary, - ENode/binary>>}]}, - #xmlel{name = <<"instructions">>, attrs = [], - children = - [{xmlcdata, - ?T(Lang, - <<"Enter path to jabberd14 spool file">>)}]}, - ?XFIELD(<<"text-single">>, <<"Path to File">>, - <<"path">>, <<"">>)]}]}; -get_form(_Host, - [<<"running nodes">>, ENode, <<"import">>, <<"dir">>], - Lang) -> - {result, - [#xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_XDATA}], - children = - [?HFIELD(), - #xmlel{name = <<"title">>, attrs = [], - children = - [{xmlcdata, - <<(?T(Lang, - <<"Import Users from Dir at ">>))/binary, - ENode/binary>>}]}, - #xmlel{name = <<"instructions">>, attrs = [], - children = - [{xmlcdata, - ?T(Lang, - <<"Enter path to jabberd14 spool dir">>)}]}, - ?XFIELD(<<"text-single">>, <<"Path to Dir">>, - <<"path">>, <<"">>)]}]}; -get_form(_Host, - [<<"running nodes">>, _ENode, <<"restart">>], Lang) -> - Make_option = fun (LabelNum, LabelUnit, Value) -> - #xmlel{name = <<"option">>, - attrs = - [{<<"label">>, - <>}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, Value}]}]} - end, - {result, - [#xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_XDATA}], - children = - [?HFIELD(), - #xmlel{name = <<"title">>, attrs = [], - children = - [{xmlcdata, ?T(Lang, <<"Restart Service">>)}]}, - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, <<"list-single">>}, - {<<"label">>, ?T(Lang, <<"Time delay">>)}, - {<<"var">>, <<"delay">>}], - children = - [Make_option(<<"">>, <<"immediately">>, <<"1">>), - Make_option(<<"15 ">>, <<"seconds">>, <<"15">>), - Make_option(<<"30 ">>, <<"seconds">>, <<"30">>), - Make_option(<<"60 ">>, <<"seconds">>, <<"60">>), - Make_option(<<"90 ">>, <<"seconds">>, <<"90">>), - Make_option(<<"2 ">>, <<"minutes">>, <<"120">>), - Make_option(<<"3 ">>, <<"minutes">>, <<"180">>), - Make_option(<<"4 ">>, <<"minutes">>, <<"240">>), - Make_option(<<"5 ">>, <<"minutes">>, <<"300">>), - Make_option(<<"10 ">>, <<"minutes">>, <<"600">>), - Make_option(<<"15 ">>, <<"minutes">>, <<"900">>), - Make_option(<<"30 ">>, <<"minutes">>, <<"1800">>), - #xmlel{name = <<"required">>, attrs = [], - children = []}]}, - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, <<"fixed">>}, - {<<"label">>, - ?T(Lang, - <<"Send announcement to all online users " - "on all hosts">>)}], - children = []}, - #xmlel{name = <<"field">>, - attrs = - [{<<"var">>, <<"subject">>}, - {<<"type">>, <<"text-single">>}, - {<<"label">>, ?T(Lang, <<"Subject">>)}], - children = []}, - #xmlel{name = <<"field">>, - attrs = - [{<<"var">>, <<"announcement">>}, - {<<"type">>, <<"text-multi">>}, - {<<"label">>, ?T(Lang, <<"Message body">>)}], - children = []}]}]}; -get_form(_Host, - [<<"running nodes">>, _ENode, <<"shutdown">>], Lang) -> - Make_option = fun (LabelNum, LabelUnit, Value) -> - #xmlel{name = <<"option">>, - attrs = - [{<<"label">>, - <>}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, Value}]}]} - end, - {result, - [#xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_XDATA}], - children = - [?HFIELD(), - #xmlel{name = <<"title">>, attrs = [], - children = - [{xmlcdata, ?T(Lang, <<"Shut Down Service">>)}]}, - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, <<"list-single">>}, - {<<"label">>, ?T(Lang, <<"Time delay">>)}, - {<<"var">>, <<"delay">>}], - children = - [Make_option(<<"">>, <<"immediately">>, <<"1">>), - Make_option(<<"15 ">>, <<"seconds">>, <<"15">>), - Make_option(<<"30 ">>, <<"seconds">>, <<"30">>), - Make_option(<<"60 ">>, <<"seconds">>, <<"60">>), - Make_option(<<"90 ">>, <<"seconds">>, <<"90">>), - Make_option(<<"2 ">>, <<"minutes">>, <<"120">>), - Make_option(<<"3 ">>, <<"minutes">>, <<"180">>), - Make_option(<<"4 ">>, <<"minutes">>, <<"240">>), - Make_option(<<"5 ">>, <<"minutes">>, <<"300">>), - Make_option(<<"10 ">>, <<"minutes">>, <<"600">>), - Make_option(<<"15 ">>, <<"minutes">>, <<"900">>), - Make_option(<<"30 ">>, <<"minutes">>, <<"1800">>), - #xmlel{name = <<"required">>, attrs = [], - children = []}]}, - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, <<"fixed">>}, - {<<"label">>, - ?T(Lang, - <<"Send announcement to all online users " - "on all hosts">>)}], - children = []}, - #xmlel{name = <<"field">>, - attrs = - [{<<"var">>, <<"subject">>}, - {<<"type">>, <<"text-single">>}, - {<<"label">>, ?T(Lang, <<"Subject">>)}], - children = []}, - #xmlel{name = <<"field">>, - attrs = - [{<<"var">>, <<"announcement">>}, - {<<"type">>, <<"text-multi">>}, - {<<"label">>, ?T(Lang, <<"Message body">>)}], - children = []}]}]}; -get_form(Host, [<<"config">>, <<"acls">>], Lang) -> - {result, - [#xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_XDATA}], - children = - [?HFIELD(), - #xmlel{name = <<"title">>, attrs = [], - children = - [{xmlcdata, - ?T(Lang, - <<"Access Control List Configuration">>)}]}, - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, <<"text-multi">>}, - {<<"label">>, - ?T(Lang, <<"Access control lists">>)}, - {<<"var">>, <<"acls">>}], - children = - lists:map(fun (S) -> - #xmlel{name = <<"value">>, - attrs = [], - children = - [{xmlcdata, S}]} - end, - str:tokens(iolist_to_binary(io_lib:format("~p.", - [ets:select(acl, - [{{acl, - {'$1', - '$2'}, - '$3'}, - [{'==', - '$2', - Host}], - [{{acl, - '$1', - '$3'}}]}])])), - <<"\n">>))}]}]}; -get_form(Host, [<<"config">>, <<"access">>], Lang) -> - {result, - [#xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_XDATA}], - children = - [?HFIELD(), - #xmlel{name = <<"title">>, attrs = [], - children = - [{xmlcdata, - ?T(Lang, <<"Access Configuration">>)}]}, - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, <<"text-multi">>}, - {<<"label">>, ?T(Lang, <<"Access rules">>)}, - {<<"var">>, <<"access">>}], - children = - lists:map(fun (S) -> - #xmlel{name = <<"value">>, - attrs = [], - children = - [{xmlcdata, S}]} - end, - str:tokens(iolist_to_binary(io_lib:format("~p.", - [ets:select(local_config, - [{{local_config, - {access, - '$1', - '$2'}, - '$3'}, - [{'==', - '$2', - Host}], - [{{access, - '$1', - '$3'}}]}])])), - <<"\n">>))}]}]}; -get_form(_Host, ?NS_ADMINL(<<"add-user">>), Lang) -> - {result, - [#xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_XDATA}], - children = - [?HFIELD(), - #xmlel{name = <<"title">>, attrs = [], - children = [{xmlcdata, ?T(Lang, <<"Add User">>)}]}, - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, <<"jid-single">>}, - {<<"label">>, ?T(Lang, <<"Jabber ID">>)}, - {<<"var">>, <<"accountjid">>}], - children = - [#xmlel{name = <<"required">>, attrs = [], - children = []}]}, - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, <<"text-private">>}, - {<<"label">>, ?T(Lang, <<"Password">>)}, - {<<"var">>, <<"password">>}], - children = - [#xmlel{name = <<"required">>, attrs = [], - children = []}]}, - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, <<"text-private">>}, - {<<"label">>, - ?T(Lang, <<"Password Verification">>)}, - {<<"var">>, <<"password-verify">>}], - children = - [#xmlel{name = <<"required">>, attrs = [], - children = []}]}]}]}; -get_form(_Host, ?NS_ADMINL(<<"delete-user">>), Lang) -> - {result, - [#xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_XDATA}], - children = - [?HFIELD(), - #xmlel{name = <<"title">>, attrs = [], - children = [{xmlcdata, ?T(Lang, <<"Delete User">>)}]}, - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, <<"jid-multi">>}, - {<<"label">>, ?T(Lang, <<"Jabber ID">>)}, - {<<"var">>, <<"accountjids">>}], - children = - [#xmlel{name = <<"required">>, attrs = [], - children = []}]}]}]}; -get_form(_Host, ?NS_ADMINL(<<"end-user-session">>), - Lang) -> - {result, - [#xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_XDATA}], - children = - [?HFIELD(), - #xmlel{name = <<"title">>, attrs = [], - children = - [{xmlcdata, ?T(Lang, <<"End User Session">>)}]}, - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, <<"jid-single">>}, - {<<"label">>, ?T(Lang, <<"Jabber ID">>)}, - {<<"var">>, <<"accountjid">>}], - children = - [#xmlel{name = <<"required">>, attrs = [], - children = []}]}]}]}; -get_form(_Host, ?NS_ADMINL(<<"get-user-password">>), - Lang) -> - {result, - [#xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_XDATA}], - children = - [?HFIELD(), - #xmlel{name = <<"title">>, attrs = [], - children = - [{xmlcdata, ?T(Lang, <<"Get User Password">>)}]}, - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, <<"jid-single">>}, - {<<"label">>, ?T(Lang, <<"Jabber ID">>)}, - {<<"var">>, <<"accountjid">>}], - children = - [#xmlel{name = <<"required">>, attrs = [], - children = []}]}]}]}; -get_form(_Host, ?NS_ADMINL(<<"change-user-password">>), - Lang) -> - {result, - [#xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_XDATA}], - children = - [?HFIELD(), - #xmlel{name = <<"title">>, attrs = [], - children = - [{xmlcdata, ?T(Lang, <<"Get User Password">>)}]}, - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, <<"jid-single">>}, - {<<"label">>, ?T(Lang, <<"Jabber ID">>)}, - {<<"var">>, <<"accountjid">>}], - children = - [#xmlel{name = <<"required">>, attrs = [], - children = []}]}, - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, <<"text-private">>}, - {<<"label">>, ?T(Lang, <<"Password">>)}, - {<<"var">>, <<"password">>}], - children = - [#xmlel{name = <<"required">>, attrs = [], - children = []}]}]}]}; -get_form(_Host, ?NS_ADMINL(<<"get-user-lastlogin">>), - Lang) -> - {result, - [#xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_XDATA}], - children = - [?HFIELD(), - #xmlel{name = <<"title">>, attrs = [], - children = - [{xmlcdata, - ?T(Lang, <<"Get User Last Login Time">>)}]}, - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, <<"jid-single">>}, - {<<"label">>, ?T(Lang, <<"Jabber ID">>)}, - {<<"var">>, <<"accountjid">>}], - children = - [#xmlel{name = <<"required">>, attrs = [], - children = []}]}]}]}; -get_form(_Host, ?NS_ADMINL(<<"user-stats">>), Lang) -> - {result, - [#xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_XDATA}], - children = - [?HFIELD(), - #xmlel{name = <<"title">>, attrs = [], - children = - [{xmlcdata, ?T(Lang, <<"Get User Statistics">>)}]}, - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, <<"jid-single">>}, - {<<"label">>, ?T(Lang, <<"Jabber ID">>)}, - {<<"var">>, <<"accountjid">>}], - children = - [#xmlel{name = <<"required">>, attrs = [], - children = []}]}]}]}; -get_form(Host, - ?NS_ADMINL(<<"get-registered-users-num">>), Lang) -> - Num = list_to_binary( - io_lib:format("~p", - [ejabberd_auth:get_vh_registered_users_number(Host)])), - {result, completed, - [#xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_XDATA}], - children = - [?HFIELD(), - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, <<"text-single">>}, - {<<"label">>, - ?T(Lang, <<"Number of registered users">>)}, - {<<"var">>, <<"registeredusersnum">>}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, Num}]}]}]}]}; -get_form(Host, ?NS_ADMINL(<<"get-online-users-num">>), - Lang) -> - Num = list_to_binary( - io_lib:format("~p", - [length(ejabberd_sm:get_vh_session_list(Host))])), - {result, completed, - [#xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_XDATA}], - children = - [?HFIELD(), - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, <<"text-single">>}, - {<<"label">>, - ?T(Lang, <<"Number of online users">>)}, - {<<"var">>, <<"onlineusersnum">>}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, Num}]}]}]}]}; -get_form(_Host, _, _Lang) -> - {error, ?ERR_SERVICE_UNAVAILABLE}. - -set_form(_From, _Host, - [<<"running nodes">>, ENode, <<"DB">>], _Lang, XData) -> - case search_running_node(ENode) of - false -> {error, ?ERR_ITEM_NOT_FOUND}; - Node -> - lists:foreach(fun ({SVar, SVals}) -> - Table = jlib:binary_to_atom(SVar), - Type = case SVals of - [<<"unknown">>] -> unknown; - [<<"ram_copies">>] -> ram_copies; - [<<"disc_copies">>] -> disc_copies; - [<<"disc_only_copies">>] -> - disc_only_copies; - _ -> false - end, - if Type == false -> ok; - Type == unknown -> - mnesia:del_table_copy(Table, Node); - true -> - case mnesia:add_table_copy(Table, Node, - Type) - of - {aborted, _} -> - mnesia:change_table_copy_type(Table, - Node, - Type); - _ -> ok - end - end - end, - XData), - {result, []} - end; -set_form(_From, Host, - [<<"running nodes">>, ENode, <<"modules">>, <<"stop">>], - _Lang, XData) -> - case search_running_node(ENode) of - false -> {error, ?ERR_ITEM_NOT_FOUND}; - Node -> - lists:foreach(fun ({Var, Vals}) -> - case Vals of - [<<"1">>] -> - Module = jlib:binary_to_atom(Var), - rpc:call(Node, gen_mod, stop_module, - [Host, Module]); - _ -> ok - end - end, - XData), - {result, []} - end; -set_form(_From, Host, - [<<"running nodes">>, ENode, <<"modules">>, - <<"start">>], - _Lang, XData) -> - case search_running_node(ENode) of - false -> {error, ?ERR_ITEM_NOT_FOUND}; - Node -> - case lists:keysearch(<<"modules">>, 1, XData) of - false -> {error, ?ERR_BAD_REQUEST}; - {value, {_, Strings}} -> - String = lists:foldl(fun (S, Res) -> - <> - end, - <<"">>, Strings), - case erl_scan:string(binary_to_list(String)) of - {ok, Tokens, _} -> - case erl_parse:parse_term(Tokens) of - {ok, Modules} -> - lists:foreach(fun ({Module, Args}) -> - rpc:call(Node, gen_mod, - start_module, - [Host, Module, Args]) - end, - Modules), - {result, []}; - _ -> {error, ?ERR_BAD_REQUEST} - end; - _ -> {error, ?ERR_BAD_REQUEST} + Title = <<(tr(Lang, ?T("Database Tables Configuration at ")))/binary, + ENode/binary>>, + Instr = tr(Lang, ?T("Choose storage type of tables")), + try + Fs = lists:map( + fun(Table) -> + case ejabberd_cluster:call( + Node, mnesia, table_info, + [Table, storage_type]) of + Type when is_atom(Type) -> + ?TABLEFIELD(Table, Type) + end + end, STables), + {result, #xdata{title = Title, + type = form, + instructions = [Instr], + fields = [?HFIELD()|Fs]}} + catch _:{case_clause, {badrpc, Reason}} -> + ?ERROR_MSG("RPC call mnesia:table_info/2 " + "on node ~ts failed: ~p", [Node, Reason]), + {error, xmpp:err_internal_server_error()} end end end; +get_form(_Host, + [<<"running nodes">>, ENode, <<"backup">>, + <<"backup">>], + Lang) -> + {result, + #xdata{title = <<(tr(Lang, ?T("Backup to File at ")))/binary, ENode/binary>>, + type = form, + instructions = [tr(Lang, ?T("Enter path to backup file"))], + fields = [?HFIELD(), + ?XFIELD('text-single', ?T("Path to File"), + <<"path">>, <<"">>)]}}; +get_form(_Host, + [<<"running nodes">>, ENode, <<"backup">>, + <<"restore">>], + Lang) -> + {result, + #xdata{title = <<(tr(Lang, ?T("Restore Backup from File at ")))/binary, + ENode/binary>>, + type = form, + instructions = [tr(Lang, ?T("Enter path to backup file"))], + fields = [?HFIELD(), + ?XFIELD('text-single', ?T("Path to File"), + <<"path">>, <<"">>)]}}; +get_form(_Host, + [<<"running nodes">>, ENode, <<"backup">>, + <<"textfile">>], + Lang) -> + {result, + #xdata{title = <<(tr(Lang, ?T("Dump Backup to Text File at ")))/binary, + ENode/binary>>, + type = form, + instructions = [tr(Lang, ?T("Enter path to text file"))], + fields = [?HFIELD(), + ?XFIELD('text-single', ?T("Path to File"), + <<"path">>, <<"">>)]}}; +get_form(_Host, + [<<"running nodes">>, ENode, <<"import">>, <<"file">>], + Lang) -> + {result, + #xdata{title = <<(tr(Lang, ?T("Import User from File at ")))/binary, + ENode/binary>>, + type = form, + instructions = [tr(Lang, ?T("Enter path to jabberd14 spool file"))], + fields = [?HFIELD(), + ?XFIELD('text-single', ?T("Path to File"), + <<"path">>, <<"">>)]}}; +get_form(_Host, + [<<"running nodes">>, ENode, <<"import">>, <<"dir">>], + Lang) -> + {result, + #xdata{title = <<(tr(Lang, ?T("Import Users from Dir at ")))/binary, + ENode/binary>>, + type = form, + instructions = [tr(Lang, ?T("Enter path to jabberd14 spool dir"))], + fields = [?HFIELD(), + ?XFIELD('text-single', ?T("Path to Dir"), + <<"path">>, <<"">>)]}}; +get_form(_Host, + [<<"running nodes">>, _ENode, <<"restart">>], Lang) -> + Make_option = + fun (LabelNum, LabelUnit, Value) -> + #xdata_option{ + label = <>, + value = Value} + end, + {result, + #xdata{title = tr(Lang, ?T("Restart Service")), + type = form, + fields = [?HFIELD(), + #xdata_field{ + type = 'list-single', + label = tr(Lang, ?T("Time delay")), + var = <<"delay">>, + required = true, + options = + [Make_option(<<"">>, <<"immediately">>, <<"1">>), + Make_option(<<"15 ">>, <<"seconds">>, <<"15">>), + Make_option(<<"30 ">>, <<"seconds">>, <<"30">>), + Make_option(<<"60 ">>, <<"seconds">>, <<"60">>), + Make_option(<<"90 ">>, <<"seconds">>, <<"90">>), + Make_option(<<"2 ">>, <<"minutes">>, <<"120">>), + Make_option(<<"3 ">>, <<"minutes">>, <<"180">>), + Make_option(<<"4 ">>, <<"minutes">>, <<"240">>), + Make_option(<<"5 ">>, <<"minutes">>, <<"300">>), + Make_option(<<"10 ">>, <<"minutes">>, <<"600">>), + Make_option(<<"15 ">>, <<"minutes">>, <<"900">>), + Make_option(<<"30 ">>, <<"minutes">>, <<"1800">>)]}, + #xdata_field{type = fixed, + label = tr(Lang, + ?T("Send announcement to all online users " + "on all hosts"))}, + #xdata_field{var = <<"subject">>, + type = 'text-single', + label = tr(Lang, ?T("Subject"))}, + #xdata_field{var = <<"announcement">>, + type = 'text-multi', + label = tr(Lang, ?T("Message body"))}]}}; +get_form(_Host, + [<<"running nodes">>, _ENode, <<"shutdown">>], Lang) -> + Make_option = + fun (LabelNum, LabelUnit, Value) -> + #xdata_option{ + label = <>, + value = Value} + end, + {result, + #xdata{title = tr(Lang, ?T("Shut Down Service")), + type = form, + fields = [?HFIELD(), + #xdata_field{ + type = 'list-single', + label = tr(Lang, ?T("Time delay")), + var = <<"delay">>, + required = true, + options = + [Make_option(<<"">>, <<"immediately">>, <<"1">>), + Make_option(<<"15 ">>, <<"seconds">>, <<"15">>), + Make_option(<<"30 ">>, <<"seconds">>, <<"30">>), + Make_option(<<"60 ">>, <<"seconds">>, <<"60">>), + Make_option(<<"90 ">>, <<"seconds">>, <<"90">>), + Make_option(<<"2 ">>, <<"minutes">>, <<"120">>), + Make_option(<<"3 ">>, <<"minutes">>, <<"180">>), + Make_option(<<"4 ">>, <<"minutes">>, <<"240">>), + Make_option(<<"5 ">>, <<"minutes">>, <<"300">>), + Make_option(<<"10 ">>, <<"minutes">>, <<"600">>), + Make_option(<<"15 ">>, <<"minutes">>, <<"900">>), + Make_option(<<"30 ">>, <<"minutes">>, <<"1800">>)]}, + #xdata_field{type = fixed, + label = tr(Lang, + ?T("Send announcement to all online users " + "on all hosts"))}, + #xdata_field{var = <<"subject">>, + type = 'text-single', + label = tr(Lang, ?T("Subject"))}, + #xdata_field{var = <<"announcement">>, + type = 'text-multi', + label = tr(Lang, ?T("Message body"))}]}}; +get_form(_Host, ?NS_ADMINL(<<"add-user">>), Lang) -> + {result, + #xdata{title = tr(Lang, ?T("Add User")), + type = form, + fields = [?HFIELD(), + #xdata_field{type = 'jid-single', + label = tr(Lang, ?T("Jabber ID")), + required = true, + var = <<"accountjid">>}, + #xdata_field{type = 'text-private', + label = tr(Lang, ?T("Password")), + required = true, + var = <<"password">>}, + #xdata_field{type = 'text-private', + label = tr(Lang, ?T("Password Verification")), + required = true, + var = <<"password-verify">>}]}}; +get_form(_Host, ?NS_ADMINL(<<"delete-user">>), Lang) -> + {result, + #xdata{title = tr(Lang, ?T("Delete User")), + type = form, + fields = [?HFIELD(), + #xdata_field{type = 'jid-multi', + label = tr(Lang, ?T("Jabber ID")), + required = true, + var = <<"accountjids">>}]}}; +get_form(_Host, ?NS_ADMINL(<<"end-user-session">>), + Lang) -> + {result, + #xdata{title = tr(Lang, ?T("End User Session")), + type = form, + fields = [?HFIELD(), + #xdata_field{type = 'jid-single', + label = tr(Lang, ?T("Jabber ID")), + required = true, + var = <<"accountjid">>}]}}; +get_form(_Host, ?NS_ADMINL(<<"change-user-password">>), + Lang) -> + {result, + #xdata{title = tr(Lang, ?T("Change User Password")), + type = form, + fields = [?HFIELD(), + #xdata_field{type = 'jid-single', + label = tr(Lang, ?T("Jabber ID")), + required = true, + var = <<"accountjid">>}, + #xdata_field{type = 'text-private', + label = tr(Lang, ?T("Password")), + required = true, + var = <<"password">>}]}}; +get_form(_Host, ?NS_ADMINL(<<"get-user-lastlogin">>), + Lang) -> + {result, + #xdata{title = tr(Lang, ?T("Get User Last Login Time")), + type = form, + fields = [?HFIELD(), + #xdata_field{type = 'jid-single', + label = tr(Lang, ?T("Jabber ID")), + var = <<"accountjid">>, + required = true}]}}; +get_form(_Host, ?NS_ADMINL(<<"user-stats">>), Lang) -> + {result, + #xdata{title = tr(Lang, ?T("Get User Statistics")), + type = form, + fields = [?HFIELD(), + #xdata_field{type = 'jid-single', + label = tr(Lang, ?T("Jabber ID")), + var = <<"accountjid">>, + required = true}]}}; +get_form(Host, ?NS_ADMINL(<<"get-registered-users-list">>), Lang) -> + Values = [jid:encode(jid:make(U, Host)) + || {U, _} <- ejabberd_auth:get_users(Host)], + {result, completed, + #xdata{type = form, + fields = [?HFIELD(), + #xdata_field{type = 'jid-multi', + label = tr(Lang, ?T("The list of all users")), + var = <<"registereduserjids">>, + values = Values}]}}; +get_form(Host, + ?NS_ADMINL(<<"get-registered-users-num">>), Lang) -> + Num = integer_to_binary(ejabberd_auth:count_users(Host)), + {result, completed, + #xdata{type = form, + fields = [?HFIELD(), + #xdata_field{type = 'text-single', + label = tr(Lang, ?T("Number of registered users")), + var = <<"registeredusersnum">>, + values = [Num]}]}}; +get_form(Host, ?NS_ADMINL(<<"get-online-users-list">>), Lang) -> + Accounts = [jid:encode(jid:make(U, Host)) + || {U, _, _} <- ejabberd_sm:get_vh_session_list(Host)], + Values = lists:usort(Accounts), + {result, completed, + #xdata{type = form, + fields = [?HFIELD(), + #xdata_field{type = 'jid-multi', + label = tr(Lang, ?T("The list of all online users")), + var = <<"onlineuserjids">>, + values = Values}]}}; +get_form(Host, ?NS_ADMINL(<<"get-online-users-num">>), + Lang) -> + Num = integer_to_binary(ejabberd_sm:get_vh_session_number(Host)), + {result, completed, + #xdata{type = form, + fields = [?HFIELD(), + #xdata_field{type = 'text-single', + label = tr(Lang, ?T("Number of online users")), + var = <<"onlineusersnum">>, + values = [Num]}]}}; +get_form(_Host, _, _Lang) -> + {error, xmpp:err_service_unavailable()}. + +-spec set_form(jid(), binary(), [binary()], binary(), xdata()) -> {result, xdata() | undefined} | + {error, stanza_error()}. +set_form(_From, _Host, + [<<"running nodes">>, ENode, <<"DB">>], Lang, XData) -> + case search_running_node(ENode) of + false -> + Txt = ?T("No running node found"), + {error, xmpp:err_item_not_found(Txt, Lang)}; + Node -> + lists:foreach( + fun(#xdata_field{var = SVar, values = SVals}) -> + Table = misc:binary_to_atom(SVar), + Type = case SVals of + [<<"unknown">>] -> unknown; + [<<"ram_copies">>] -> ram_copies; + [<<"disc_copies">>] -> disc_copies; + [<<"disc_only_copies">>] -> disc_only_copies; + _ -> false + end, + if Type == false -> ok; + Type == unknown -> + mnesia:del_table_copy(Table, Node); + true -> + case mnesia:add_table_copy(Table, Node, Type) of + {aborted, _} -> + mnesia:change_table_copy_type( + Table, Node, Type); + _ -> ok + end + end + end, XData#xdata.fields), + {result, undefined} + end; set_form(_From, _Host, [<<"running nodes">>, ENode, <<"backup">>, <<"backup">>], - _Lang, XData) -> + Lang, XData) -> case search_running_node(ENode) of - false -> {error, ?ERR_ITEM_NOT_FOUND}; - Node -> - case lists:keysearch(<<"path">>, 1, XData) of - false -> {error, ?ERR_BAD_REQUEST}; - {value, {_, [String]}} -> - case rpc:call(Node, mnesia, backup, [String]) of - {badrpc, _Reason} -> - {error, ?ERR_INTERNAL_SERVER_ERROR}; - {error, _Reason} -> {error, ?ERR_INTERNAL_SERVER_ERROR}; - _ -> {result, []} - end; - _ -> {error, ?ERR_BAD_REQUEST} - end + false -> + Txt = ?T("No running node found"), + {error, xmpp:err_item_not_found(Txt, Lang)}; + Node -> + case xmpp_util:get_xdata_values(<<"path">>, XData) of + [] -> + Txt = ?T("No 'path' found in data form"), + {error, xmpp:err_bad_request(Txt, Lang)}; + [String] -> + case ejabberd_cluster:call( + Node, mnesia, backup, [binary_to_list(String)], + timer:minutes(10)) of + {badrpc, Reason} -> + ?ERROR_MSG("RPC call mnesia:backup(~ts) to node ~ts " + "failed: ~p", [String, Node, Reason]), + {error, xmpp:err_internal_server_error()}; + {error, Reason} -> + ?ERROR_MSG("RPC call mnesia:backup(~ts) to node ~ts " + "failed: ~p", [String, Node, Reason]), + {error, xmpp:err_internal_server_error()}; + _ -> + {result, undefined} + end; + _ -> + Txt = ?T("Incorrect value of 'path' in data form"), + {error, xmpp:err_bad_request(Txt, Lang)} + end end; set_form(_From, _Host, [<<"running nodes">>, ENode, <<"backup">>, <<"restore">>], - _Lang, XData) -> + Lang, XData) -> case search_running_node(ENode) of - false -> {error, ?ERR_ITEM_NOT_FOUND}; - Node -> - case lists:keysearch(<<"path">>, 1, XData) of - false -> {error, ?ERR_BAD_REQUEST}; - {value, {_, [String]}} -> - case rpc:call(Node, ejabberd_admin, restore, [String]) - of - {badrpc, _Reason} -> - {error, ?ERR_INTERNAL_SERVER_ERROR}; - {error, _Reason} -> {error, ?ERR_INTERNAL_SERVER_ERROR}; - _ -> {result, []} - end; - _ -> {error, ?ERR_BAD_REQUEST} - end + false -> + Txt = ?T("No running node found"), + {error, xmpp:err_item_not_found(Txt, Lang)}; + Node -> + case xmpp_util:get_xdata_values(<<"path">>, XData) of + [] -> + Txt = ?T("No 'path' found in data form"), + {error, xmpp:err_bad_request(Txt, Lang)}; + [String] -> + case ejabberd_cluster:call( + Node, ejabberd_admin, restore, + [String], timer:minutes(10)) of + {badrpc, Reason} -> + ?ERROR_MSG("RPC call ejabberd_admin:restore(~ts) to node " + "~ts failed: ~p", [String, Node, Reason]), + {error, xmpp:err_internal_server_error()}; + {error, Reason} -> + ?ERROR_MSG("RPC call ejabberd_admin:restore(~ts) to node " + "~ts failed: ~p", [String, Node, Reason]), + {error, xmpp:err_internal_server_error()}; + _ -> + {result, undefined} + end; + _ -> + Txt = ?T("Incorrect value of 'path' in data form"), + {error, xmpp:err_bad_request(Txt, Lang)} + end end; set_form(_From, _Host, [<<"running nodes">>, ENode, <<"backup">>, <<"textfile">>], - _Lang, XData) -> + Lang, XData) -> case search_running_node(ENode) of - false -> {error, ?ERR_ITEM_NOT_FOUND}; - Node -> - case lists:keysearch(<<"path">>, 1, XData) of - false -> {error, ?ERR_BAD_REQUEST}; - {value, {_, [String]}} -> - case rpc:call(Node, ejabberd_admin, dump_to_textfile, - [String]) - of - {badrpc, _Reason} -> - {error, ?ERR_INTERNAL_SERVER_ERROR}; - {error, _Reason} -> {error, ?ERR_INTERNAL_SERVER_ERROR}; - _ -> {result, []} - end; - _ -> {error, ?ERR_BAD_REQUEST} - end + false -> + Txt = ?T("No running node found"), + {error, xmpp:err_item_not_found(Txt, Lang)}; + Node -> + case xmpp_util:get_xdata_values(<<"path">>, XData) of + [] -> + Txt = ?T("No 'path' found in data form"), + {error, xmpp:err_bad_request(Txt, Lang)}; + [String] -> + case ejabberd_cluster:call( + Node, ejabberd_admin, dump_to_textfile, + [String], timer:minutes(10)) of + {badrpc, Reason} -> + ?ERROR_MSG("RPC call ejabberd_admin:dump_to_textfile(~ts) " + "to node ~ts failed: ~p", [String, Node, Reason]), + {error, xmpp:err_internal_server_error()}; + {error, Reason} -> + ?ERROR_MSG("RPC call ejabberd_admin:dump_to_textfile(~ts) " + "to node ~ts failed: ~p", [String, Node, Reason]), + {error, xmpp:err_internal_server_error()}; + _ -> + {result, undefined} + end; + _ -> + Txt = ?T("Incorrect value of 'path' in data form"), + {error, xmpp:err_bad_request(Txt, Lang)} + end end; set_form(_From, _Host, [<<"running nodes">>, ENode, <<"import">>, <<"file">>], - _Lang, XData) -> + Lang, XData) -> case search_running_node(ENode) of - false -> {error, ?ERR_ITEM_NOT_FOUND}; - Node -> - case lists:keysearch(<<"path">>, 1, XData) of - false -> {error, ?ERR_BAD_REQUEST}; - {value, {_, [String]}} -> - rpc:call(Node, jd2ejd, import_file, [String]), - {result, []}; - _ -> {error, ?ERR_BAD_REQUEST} - end + false -> + Txt = ?T("No running node found"), + {error, xmpp:err_item_not_found(Txt, Lang)}; + Node -> + case xmpp_util:get_xdata_values(<<"path">>, XData) of + [] -> + Txt = ?T("No 'path' found in data form"), + {error, xmpp:err_bad_request(Txt, Lang)}; + [String] -> + ejabberd_cluster:call(Node, jd2ejd, import_file, [String]), + {result, undefined}; + _ -> + Txt = ?T("Incorrect value of 'path' in data form"), + {error, xmpp:err_bad_request(Txt, Lang)} + end end; set_form(_From, _Host, [<<"running nodes">>, ENode, <<"import">>, <<"dir">>], - _Lang, XData) -> + Lang, XData) -> case search_running_node(ENode) of - false -> {error, ?ERR_ITEM_NOT_FOUND}; - Node -> - case lists:keysearch(<<"path">>, 1, XData) of - false -> {error, ?ERR_BAD_REQUEST}; - {value, {_, [String]}} -> - rpc:call(Node, jd2ejd, import_dir, [String]), - {result, []}; - _ -> {error, ?ERR_BAD_REQUEST} - end + false -> + Txt = ?T("No running node found"), + {error, xmpp:err_item_not_found(Txt, Lang)}; + Node -> + case xmpp_util:get_xdata_values(<<"path">>, XData) of + [] -> + Txt = ?T("No 'path' found in data form"), + {error, xmpp:err_bad_request(Txt, Lang)}; + [String] -> + ejabberd_cluster:call(Node, jd2ejd, import_dir, [String]), + {result, undefined}; + _ -> + Txt = ?T("Incorrect value of 'path' in data form"), + {error, xmpp:err_bad_request(Txt, Lang)} + end end; set_form(From, Host, [<<"running nodes">>, ENode, <<"restart">>], _Lang, @@ -1743,305 +1264,183 @@ set_form(From, Host, [<<"running nodes">>, ENode, <<"shutdown">>], _Lang, XData) -> stop_node(From, Host, ENode, stop, XData); -set_form(_From, Host, [<<"config">>, <<"acls">>], _Lang, - XData) -> - case lists:keysearch(<<"acls">>, 1, XData) of - {value, {_, Strings}} -> - String = lists:foldl(fun (S, Res) -> - <> - end, - <<"">>, Strings), - case erl_scan:string(binary_to_list(String)) of - {ok, Tokens, _} -> - case erl_parse:parse_term(Tokens) of - {ok, ACLs} -> - acl:add_list(Host, ACLs, true), - {result, []}; - _ -> {error, ?ERR_BAD_REQUEST} - end; - _ -> {error, ?ERR_BAD_REQUEST} - end; - _ -> {error, ?ERR_BAD_REQUEST} - end; -set_form(_From, Host, [<<"config">>, <<"access">>], - _Lang, XData) -> - SetAccess = fun (Rs) -> - mnesia:transaction(fun () -> - Os = mnesia:select(local_config, - [{{local_config, - {access, - '$1', - '$2'}, - '$3'}, - [{'==', - '$2', - Host}], - ['$_']}]), - lists:foreach(fun (O) -> - mnesia:delete_object(O) - end, - Os), - lists:foreach(fun ({access, - Name, - Rules}) -> - mnesia:write({local_config, - {access, - Name, - Host}, - Rules}) - end, - Rs) - end) - end, - case lists:keysearch(<<"access">>, 1, XData) of - {value, {_, Strings}} -> - String = lists:foldl(fun (S, Res) -> - <> - end, - <<"">>, Strings), - case erl_scan:string(binary_to_list(String)) of - {ok, Tokens, _} -> - case erl_parse:parse_term(Tokens) of - {ok, Rs} -> - case SetAccess(Rs) of - {atomic, _} -> {result, []}; - _ -> {error, ?ERR_BAD_REQUEST} - end; - _ -> {error, ?ERR_BAD_REQUEST} - end; - _ -> {error, ?ERR_BAD_REQUEST} - end; - _ -> {error, ?ERR_BAD_REQUEST} - end; set_form(From, Host, ?NS_ADMINL(<<"add-user">>), _Lang, XData) -> AccountString = get_value(<<"accountjid">>, XData), Password = get_value(<<"password">>, XData), Password = get_value(<<"password-verify">>, XData), - AccountJID = jlib:string_to_jid(AccountString), + AccountJID = jid:decode(AccountString), User = AccountJID#jid.luser, Server = AccountJID#jid.lserver, - true = lists:member(Server, ?MYHOSTS), + true = lists:member(Server, ejabberd_option:hosts()), true = Server == Host orelse - get_permission_level(From) == global, - ejabberd_auth:try_register(User, Server, Password), - {result, []}; + get_permission_level(From, Host) == global, + case ejabberd_auth:try_register(User, Server, Password) of + ok -> {result, undefined}; + {error, exists} -> {error, xmpp:err_conflict()}; + {error, not_allowed} -> {error, xmpp:err_not_allowed()} + end; set_form(From, Host, ?NS_ADMINL(<<"delete-user">>), _Lang, XData) -> AccountStringList = get_values(<<"accountjids">>, XData), [_ | _] = AccountStringList, ASL2 = lists:map(fun (AccountString) -> - JID = jlib:string_to_jid(AccountString), + JID = jid:decode(AccountString), User = JID#jid.luser, Server = JID#jid.lserver, true = Server == Host orelse - get_permission_level(From) == global, - true = ejabberd_auth:is_user_exists(User, Server), + get_permission_level(From, Host) == global, + true = ejabberd_auth:user_exists(User, Server), {User, Server} end, AccountStringList), [ejabberd_auth:remove_user(User, Server) || {User, Server} <- ASL2], - {result, []}; + {result, undefined}; set_form(From, Host, ?NS_ADMINL(<<"end-user-session">>), - Lang, XData) -> + _Lang, XData) -> AccountString = get_value(<<"accountjid">>, XData), - JID = jlib:string_to_jid(AccountString), - LUser = JID#jid.luser, + JID = jid:decode(AccountString), LServer = JID#jid.lserver, true = LServer == Host orelse - get_permission_level(From) == global, - Xmlelement = ?SERRT_POLICY_VIOLATION(Lang, <<"has been kicked">>), + get_permission_level(From, Host) == global, case JID#jid.lresource of - <<>> -> - SIDs = mnesia:dirty_select(session, - [{#session{sid = '$1', - usr = {LUser, LServer, '_'}, - _ = '_'}, - [], ['$1']}]), - [Pid ! {kick, kicked_by_admin, Xmlelement} || {_, Pid} <- SIDs]; - R -> - [{_, Pid}] = mnesia:dirty_select(session, - [{#session{sid = '$1', - usr = {LUser, LServer, R}, - _ = '_'}, - [], ['$1']}]), - Pid ! {kick, kicked_by_admin, Xmlelement} + <<>> -> + ejabberd_sm:kick_user(JID#jid.luser, JID#jid.lserver); + R -> + ejabberd_sm:kick_user(JID#jid.luser, JID#jid.lserver, R) end, - {result, []}; -set_form(From, Host, - ?NS_ADMINL(<<"get-user-password">>), Lang, XData) -> - AccountString = get_value(<<"accountjid">>, XData), - JID = jlib:string_to_jid(AccountString), - User = JID#jid.luser, - Server = JID#jid.lserver, - true = Server == Host orelse - get_permission_level(From) == global, - Password = ejabberd_auth:get_password(User, Server), - true = is_binary(Password), - {result, - [#xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_XDATA}], - children = - [?HFIELD(), - ?XFIELD(<<"jid-single">>, <<"Jabber ID">>, - <<"accountjid">>, AccountString), - ?XFIELD(<<"text-single">>, <<"Password">>, - <<"password">>, Password)]}]}; + {result, undefined}; set_form(From, Host, ?NS_ADMINL(<<"change-user-password">>), _Lang, XData) -> AccountString = get_value(<<"accountjid">>, XData), Password = get_value(<<"password">>, XData), - JID = jlib:string_to_jid(AccountString), + JID = jid:decode(AccountString), User = JID#jid.luser, Server = JID#jid.lserver, true = Server == Host orelse - get_permission_level(From) == global, - true = ejabberd_auth:is_user_exists(User, Server), + get_permission_level(From, Host) == global, + true = ejabberd_auth:user_exists(User, Server), ejabberd_auth:set_password(User, Server, Password), - {result, []}; + {result, undefined}; set_form(From, Host, ?NS_ADMINL(<<"get-user-lastlogin">>), Lang, XData) -> AccountString = get_value(<<"accountjid">>, XData), - JID = jlib:string_to_jid(AccountString), + JID = jid:decode(AccountString), User = JID#jid.luser, Server = JID#jid.lserver, true = Server == Host orelse - get_permission_level(From) == global, + get_permission_level(From, Host) == global, FLast = case ejabberd_sm:get_user_resources(User, Server) of [] -> - _US = {User, Server}, case get_last_info(User, Server) of - not_found -> ?T(Lang, <<"Never">>); + not_found -> tr(Lang, ?T("Never")); {ok, Timestamp, _Status} -> Shift = Timestamp, TimeStamp = {Shift div 1000000, Shift rem 1000000, 0}, {{Year, Month, Day}, {Hour, Minute, Second}} = calendar:now_to_local_time(TimeStamp), - iolist_to_binary(io_lib:format("~w-~.2.0w-~.2.0w ~.2.0w:~.2.0w:~.2.0w", + (str:format("~w-~.2.0w-~.2.0w ~.2.0w:~.2.0w:~.2.0w", [Year, Month, Day, Hour, Minute, Second])) end; - _ -> ?T(Lang, <<"Online">>) + _ -> tr(Lang, ?T("Online")) end, {result, - [#xmlel{name = <<"x">>, - attrs = - [{<<"xmlns">>, ?NS_XDATA}, {<<"type">>, <<"result">>}], - children = - [?HFIELD(), - ?XFIELD(<<"jid-single">>, <<"Jabber ID">>, - <<"accountjid">>, AccountString), - ?XFIELD(<<"text-single">>, <<"Last login">>, - <<"lastlogin">>, FLast)]}]}; + #xdata{type = form, + fields = [?HFIELD(), + ?XFIELD('jid-single', ?T("Jabber ID"), + <<"accountjid">>, AccountString), + ?XFIELD('text-single', ?T("Last login"), + <<"lastlogin">>, FLast)]}}; set_form(From, Host, ?NS_ADMINL(<<"user-stats">>), Lang, XData) -> AccountString = get_value(<<"accountjid">>, XData), - JID = jlib:string_to_jid(AccountString), + JID = jid:decode(AccountString), User = JID#jid.luser, Server = JID#jid.lserver, true = Server == Host orelse - get_permission_level(From) == global, + get_permission_level(From, Host) == global, Resources = ejabberd_sm:get_user_resources(User, Server), IPs1 = [ejabberd_sm:get_user_ip(User, Server, Resource) || Resource <- Resources], - IPs = [<<(jlib:ip_to_list(IP))/binary, ":", - (jlib:integer_to_binary(Port))/binary>> + IPs = [<<(misc:ip_to_list(IP))/binary, ":", + (integer_to_binary(Port))/binary>> || {IP, Port} <- IPs1], Items = ejabberd_hooks:run_fold(roster_get, Server, [], [{User, Server}]), - Rostersize = jlib:integer_to_binary(erlang:length(Items)), + Rostersize = integer_to_binary(erlang:length(Items)), {result, - [#xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_XDATA}], - children = - [?HFIELD(), - ?XFIELD(<<"jid-single">>, <<"Jabber ID">>, - <<"accountjid">>, AccountString), - ?XFIELD(<<"text-single">>, <<"Roster size">>, - <<"rostersize">>, Rostersize), - ?XMFIELD(<<"text-multi">>, <<"IP addresses">>, - <<"ipaddresses">>, IPs), - ?XMFIELD(<<"text-multi">>, <<"Resources">>, - <<"onlineresources">>, Resources)]}]}; + #xdata{type = form, + fields = [?HFIELD(), + ?XFIELD('jid-single', ?T("Jabber ID"), + <<"accountjid">>, AccountString), + ?XFIELD('text-single', ?T("Roster size"), + <<"rostersize">>, Rostersize), + ?XMFIELD('text-multi', ?T("IP addresses"), + <<"ipaddresses">>, IPs), + ?XMFIELD('text-multi', ?T("Resources"), + <<"onlineresources">>, Resources)]}}; set_form(_From, _Host, _, _Lang, _XData) -> - {error, ?ERR_SERVICE_UNAVAILABLE}. + {error, xmpp:err_service_unavailable()}. -get_value(Field, XData) -> hd(get_values(Field, XData)). +-spec get_value(binary(), xdata()) -> binary(). +get_value(Field, XData) -> + hd(get_values(Field, XData)). +-spec get_values(binary(), xdata()) -> [binary()]. get_values(Field, XData) -> - {value, {_, ValueList}} = lists:keysearch(Field, 1, - XData), - ValueList. + xmpp_util:get_xdata_values(Field, XData). +-spec search_running_node(binary()) -> false | node(). search_running_node(SNode) -> search_running_node(SNode, mnesia:system_info(running_db_nodes)). +-spec search_running_node(binary(), [node()]) -> false | node(). search_running_node(_, []) -> false; search_running_node(SNode, [Node | Nodes]) -> - case iolist_to_binary(atom_to_list(Node)) of - SNode -> Node; - _ -> search_running_node(SNode, Nodes) + case atom_to_binary(Node, utf8) of + SNode -> Node; + _ -> search_running_node(SNode, Nodes) end. +-spec stop_node(jid(), binary(), binary(), restart | stop, xdata()) -> {result, undefined}. stop_node(From, Host, ENode, Action, XData) -> - Delay = jlib:binary_to_integer(get_value(<<"delay">>, - XData)), - Subject = case get_value(<<"subject">>, XData) of - <<"">> -> []; - S -> - [#xmlel{name = <<"field">>, - attrs = [{<<"var">>, <<"subject">>}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, S}]}]}] + Delay = binary_to_integer(get_value(<<"delay">>, XData)), + Subject = case get_values(<<"subject">>, XData) of + [] -> + []; + [S|_] -> + [#xdata_field{var = <<"subject">>, values = [S]}] end, - Announcement = case get_values(<<"announcement">>, - XData) - of - [] -> []; - As -> - [#xmlel{name = <<"field">>, - attrs = [{<<"var">>, <<"body">>}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, Line}]} - || Line <- As]}] + Announcement = case get_values(<<"announcement">>, XData) of + [] -> + []; + As -> + [#xdata_field{var = <<"body">>, values = As}] end, case Subject ++ Announcement of - [] -> ok; - SubEls -> - Request = #adhoc_request{node = - ?NS_ADMINX(<<"announce-allhosts">>), - action = <<"complete">>, - xdata = - #xmlel{name = <<"x">>, - attrs = - [{<<"xmlns">>, - <<"jabber:x:data">>}, - {<<"type">>, <<"submit">>}], - children = SubEls}, - others = - [#xmlel{name = <<"x">>, - attrs = - [{<<"xmlns">>, - <<"jabber:x:data">>}, - {<<"type">>, <<"submit">>}], - children = SubEls}]}, - To = jlib:make_jid(<<"">>, Host, <<"">>), - mod_announce:announce_commands(empty, From, To, Request) + [] -> + ok; + Fields -> + Request = #adhoc_command{node = ?NS_ADMINX(<<"announce-allhosts">>), + action = complete, + xdata = #xdata{type = submit, + fields = Fields}}, + To = jid:make(Host), + mod_announce:announce_commands(empty, From, To, Request) end, Time = timer:seconds(Delay), - Node = jlib:binary_to_atom(ENode), - {ok, _} = timer:apply_after(Time, rpc, call, - [Node, init, Action, []]), - {result, []}. + Node = misc:binary_to_atom(ENode), + {ok, _} = timer:apply_after(Time, ejabberd_cluster, call, [Node, init, Action, []]), + {result, undefined}. +-spec get_last_info(binary(), binary()) -> {ok, non_neg_integer(), binary()} | not_found. get_last_info(User, Server) -> case gen_mod:is_loaded(Server, mod_last) of true -> mod_last:get_last_info(User, Server); @@ -2049,103 +1448,164 @@ get_last_info(User, Server) -> end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - +-spec adhoc_sm_commands(adhoc_command(), jid(), jid(), adhoc_command()) -> adhoc_command() | + {error, stanza_error()}. adhoc_sm_commands(_Acc, From, - #jid{user = User, server = Server, lserver = LServer} = - _To, - #adhoc_request{lang = Lang, node = <<"config">>, - action = Action, xdata = XData} = - Request) -> - case acl:match_rule(LServer, configure, From) of - deny -> {error, ?ERR_FORBIDDEN}; - allow -> - ActionIsExecute = lists:member(Action, - [<<"">>, <<"execute">>, - <<"complete">>]), - if Action == <<"cancel">> -> - adhoc:produce_response(Request, - #adhoc_response{status = canceled}); - XData == false, ActionIsExecute -> - case get_sm_form(User, Server, <<"config">>, Lang) of - {result, Form} -> - adhoc:produce_response(Request, - #adhoc_response{status = - executing, - elements = Form}); - {error, Error} -> {error, Error} - end; - XData /= false, ActionIsExecute -> - case jlib:parse_xdata_submit(XData) of - invalid -> {error, ?ERR_BAD_REQUEST}; - Fields -> - set_sm_form(User, Server, <<"config">>, Request, Fields) - end; - true -> {error, ?ERR_BAD_REQUEST} - end + #jid{user = User, server = Server, lserver = LServer}, + #adhoc_command{lang = Lang, node = <<"config">>, + action = Action, xdata = XData} = Request) -> + case acl_match_rule(LServer, From) of + deny -> + {error, xmpp:err_forbidden(?T("Access denied by service policy"), Lang)}; + allow -> + ActionIsExecute = Action == execute orelse Action == complete, + if Action == cancel -> + xmpp_util:make_adhoc_response( + Request, #adhoc_command{status = canceled}); + XData == undefined, ActionIsExecute -> + case get_sm_form(User, Server, <<"config">>, Lang) of + {result, Form} -> + xmpp_util:make_adhoc_response( + Request, #adhoc_command{status = executing, + xdata = Form}); + {error, Error} -> + {error, Error} + end; + XData /= undefined, ActionIsExecute -> + set_sm_form(User, Server, <<"config">>, Request); + true -> + Txt = ?T("Unexpected action"), + {error, xmpp:err_bad_request(Txt, Lang)} + end end; adhoc_sm_commands(Acc, _From, _To, _Request) -> Acc. +-spec get_sm_form(binary(), binary(), binary(), binary()) -> {result, xdata()} | + {error, stanza_error()}. get_sm_form(User, Server, <<"config">>, Lang) -> {result, - [#xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_XDATA}], - children = - [?HFIELD(), - #xmlel{name = <<"title">>, attrs = [], - children = - [{xmlcdata, - <<(?T(Lang, <<"Administration of ">>))/binary, - User/binary>>}]}, - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, <<"list-single">>}, - {<<"label">>, ?T(Lang, <<"Action on user">>)}, - {<<"var">>, <<"action">>}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, <<"edit">>}]}, - #xmlel{name = <<"option">>, - attrs = - [{<<"label">>, - ?T(Lang, <<"Edit Properties">>)}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = - [{xmlcdata, - <<"edit">>}]}]}, - #xmlel{name = <<"option">>, - attrs = - [{<<"label">>, - ?T(Lang, <<"Remove User">>)}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = - [{xmlcdata, - <<"remove">>}]}]}]}, - ?XFIELD(<<"text-private">>, <<"Password">>, - <<"password">>, - (ejabberd_auth:get_password_s(User, Server)))]}]}; + #xdata{type = form, + title = <<(tr(Lang, ?T("Administration of ")))/binary, User/binary>>, + fields = + [?HFIELD(), + #xdata_field{ + type = 'list-single', + label = tr(Lang, ?T("Action on user")), + var = <<"action">>, + values = [<<"edit">>], + options = [#xdata_option{ + label = tr(Lang, ?T("Edit Properties")), + value = <<"edit">>}, + #xdata_option{ + label = tr(Lang, ?T("Remove User")), + value = <<"remove">>}]}, + ?XFIELD('text-private', ?T("Password"), + <<"password">>, + ejabberd_auth:get_password_s(User, Server))]}}; get_sm_form(_User, _Server, _Node, _Lang) -> - {error, ?ERR_SERVICE_UNAVAILABLE}. + {error, xmpp:err_service_unavailable()}. +-spec set_sm_form(binary(), binary(), binary(), adhoc_command()) -> adhoc_command() | + {error, stanza_error()}. set_sm_form(User, Server, <<"config">>, - #adhoc_request{lang = Lang, node = Node, - sessionid = SessionID}, - XData) -> - Response = #adhoc_response{lang = Lang, node = Node, - sessionid = SessionID, status = completed}, - case lists:keysearch(<<"action">>, 1, XData) of - {value, {_, [<<"edit">>]}} -> - case lists:keysearch(<<"password">>, 1, XData) of - {value, {_, [Password]}} -> - ejabberd_auth:set_password(User, Server, Password), - adhoc:produce_response(Response); - _ -> {error, ?ERR_NOT_ACCEPTABLE} - end; - {value, {_, [<<"remove">>]}} -> - catch ejabberd_auth:remove_user(User, Server), - adhoc:produce_response(Response); - _ -> {error, ?ERR_NOT_ACCEPTABLE} + #adhoc_command{lang = Lang, node = Node, + sid = SessionID, xdata = XData}) -> + Response = #adhoc_command{lang = Lang, node = Node, + sid = SessionID, status = completed}, + case xmpp_util:get_xdata_values(<<"action">>, XData) of + [<<"edit">>] -> + case xmpp_util:get_xdata_values(<<"password">>, XData) of + [Password] -> + ejabberd_auth:set_password(User, Server, Password), + xmpp_util:make_adhoc_response(Response); + _ -> + Txt = ?T("No 'password' found in data form"), + {error, xmpp:err_not_acceptable(Txt, Lang)} + end; + [<<"remove">>] -> + ejabberd_auth:remove_user(User, Server), + xmpp_util:make_adhoc_response(Response); + _ -> + Txt = ?T("Incorrect value of 'action' in data form"), + {error, xmpp:err_not_acceptable(Txt, Lang)} end; -set_sm_form(_User, _Server, _Node, _Request, _Fields) -> - {error, ?ERR_SERVICE_UNAVAILABLE}. +set_sm_form(_User, _Server, _Node, _Request) -> + {error, xmpp:err_service_unavailable()}. + +-spec tr(binary(), binary()) -> binary(). +tr(Lang, Text) -> + translate:translate(Lang, Text). + +-spec mod_opt_type(atom()) -> econf:validator(). +mod_opt_type(access) -> + econf:acl(). + +-spec mod_options(binary()) -> [{services, [tuple()]} | {atom(), any()}]. +mod_options(_Host) -> + [{access, configure}]. + +%% @format-begin + +%% All ad-hoc commands implemented by mod_configure are available as API Commands: +%% - add-user -> register +%% - delete-user -> unregister +%% - end-user-session -> kick_session / kick_user +%% - change-user-password -> change_password +%% - get-user-lastlogin -> get_last +%% - user-stats -> user_sessions_info +%% - get-registered-users-list -> registered_users +%% - get-registered-users-num -> stats +%% - get-online-users-list -> connected_users +%% - get-online-users-num -> stats +%% - stopped nodes -> list_cluster_detailed +%% - DB -> mnesia_list_tables and mnesia_table_change_storage +%% - restart -> stop_kindly / restart +%% - shutdown -> stop_kindly +%% - backup -> backup +%% - restore -> restore +%% - textfile -> dump +%% - import/file -> import_file +%% - import/dir -> import_dir +%% +%% An exclusive feature available only in this module is to list items and discover them: +%% - outgoing s2s +%% - online users +%% - all users + +mod_doc() -> + #{desc => + [?T("The module provides server configuration functionalities using " + "https://xmpp.org/extensions/xep-0030.html[XEP-0030: Service Discovery] and " + "https://xmpp.org/extensions/xep-0050.html[XEP-0050: Ad-Hoc Commands]:"), + "", + "- List and discover outgoing s2s, online client sessions and all registered accounts", + "- Most of the ad-hoc commands defined in https://xmpp.org/extensions/xep-0133.html[XEP-0133: Service Administration]", + "- Additional custom ad-hoc commands specific to ejabberd", + "", + ?T("This module requires _`mod_adhoc`_ (to execute the commands), " + "and recommends _`mod_disco`_ (to discover the commands). "), + "", + ?T("Please notice that all the ad-hoc commands implemented by this module " + "have an equivalent " + "https://docs.ejabberd.im/developer/ejabberd-api/[API Command] " + "that you can execute using _`mod_adhoc_api`_ or any other API frontend.")], + opts => + [{access, + #{value => ?T("AccessName"), + note => "added in 25.03", + desc => + ?T("This option defines which access rule will be used to " + "control who is allowed to access the features provided by this module. " + "The default value is 'configure'.")}}], + example => + ["acl:", + " admin:", + " user: sun@localhost", + "", + "access_rules:", + " configure:", + " allow: admin", + "", + "modules:", + " mod_configure:", + " access: configure"]}. diff --git a/src/mod_configure2.erl b/src/mod_configure2.erl deleted file mode 100644 index b70f8fe51..000000000 --- a/src/mod_configure2.erl +++ /dev/null @@ -1,197 +0,0 @@ -%%%---------------------------------------------------------------------- -%%% File : mod_configure2.erl -%%% Author : Alexey Shchepin -%%% Purpose : Support for online configuration of ejabberd -%%% Created : 26 Oct 2003 by Alexey Shchepin -%%% -%%% -%%% ejabberd, Copyright (C) 2002-2015 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_configure2). - --author('alexey@process-one.net'). - --behaviour(gen_mod). - --export([start/2, stop/1, process_local_iq/3]). - --include("ejabberd.hrl"). --include("logger.hrl"). - --include("jlib.hrl"). - --define(NS_ECONFIGURE, - <<"http://ejabberd.jabberstudio.org/protocol/con" - "figure">>). - -start(Host, Opts) -> - IQDisc = gen_mod:get_opt(iqdisc, Opts, fun gen_iq_handler:check_type/1, - one_queue), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, - ?NS_ECONFIGURE, ?MODULE, process_local_iq, - IQDisc), - ok. - -stop(Host) -> - gen_iq_handler:remove_iq_handler(ejabberd_local, Host, - ?NS_ECONFIGURE). - -process_local_iq(From, To, - #iq{type = Type, lang = _Lang, sub_el = SubEl} = IQ) -> - case acl:match_rule(To#jid.lserver, configure, From) of - deny -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; - allow -> - case Type of - set -> - IQ#iq{type = error, - sub_el = [SubEl, ?ERR_FEATURE_NOT_IMPLEMENTED]}; - %%case xml:get_tag_attr_s("type", SubEl) of - %% "cancel" -> - %% IQ#iq{type = result, - %% sub_el = [{xmlelement, "query", - %% [{"xmlns", XMLNS}], []}]}; - %% "submit" -> - %% XData = jlib:parse_xdata_submit(SubEl), - %% case XData of - %% invalid -> - %% IQ#iq{type = error, - %% sub_el = [SubEl, ?ERR_BAD_REQUEST]}; - %% _ -> - %% Node = - %% string:tokens( - %% xml:get_tag_attr_s("node", SubEl), - %% "/"), - %% case set_form(Node, Lang, XData) of - %% {result, Res} -> - %% IQ#iq{type = result, - %% sub_el = [{xmlelement, "query", - %% [{"xmlns", XMLNS}], - %% Res - %% }]}; - %% {error, Error} -> - %% IQ#iq{type = error, - %% sub_el = [SubEl, Error]} - %% end - %% end; - %% _ -> - %% IQ#iq{type = error, - %% sub_el = [SubEl, ?ERR_NOT_ALLOWED]} - %%end; - get -> - case process_get(SubEl) of - {result, Res} -> IQ#iq{type = result, sub_el = [Res]}; - {error, Error} -> - IQ#iq{type = error, sub_el = [SubEl, Error]} - end - end - end. - -process_get(#xmlel{name = <<"info">>}) -> - S2SConns = ejabberd_s2s:dirty_get_connections(), - TConns = lists:usort([element(2, C) || C <- S2SConns]), - Attrs = [{<<"registered-users">>, - iolist_to_binary(integer_to_list(mnesia:table_info(passwd, - size)))}, - {<<"online-users">>, - iolist_to_binary(integer_to_list(mnesia:table_info(presence, - size)))}, - {<<"running-nodes">>, - iolist_to_binary(integer_to_list(length(mnesia:system_info(running_db_nodes))))}, - {<<"stopped-nodes">>, - iolist_to_binary(integer_to_list(length(lists:usort(mnesia:system_info(db_nodes) - ++ - mnesia:system_info(extra_db_nodes)) - -- - mnesia:system_info(running_db_nodes))))}, - {<<"outgoing-s2s-servers">>, - iolist_to_binary(integer_to_list(length(TConns)))}], - {result, - #xmlel{name = <<"info">>, - attrs = [{<<"xmlns">>, ?NS_ECONFIGURE} | Attrs], - children = []}}; -process_get(#xmlel{name = <<"welcome-message">>, - attrs = Attrs}) -> - {Subj, Body} = ejabberd_config:get_option( - welcome_message, - fun({Subj, Body}) -> - {iolist_to_binary(Subj), - iolist_to_binary(Body)} - end, - {<<"">>, <<"">>}), - {result, - #xmlel{name = <<"welcome-message">>, attrs = Attrs, - children = - [#xmlel{name = <<"subject">>, attrs = [], - children = [{xmlcdata, Subj}]}, - #xmlel{name = <<"body">>, attrs = [], - children = [{xmlcdata, Body}]}]}}; -process_get(#xmlel{name = <<"registration-watchers">>, - attrs = Attrs}) -> - SubEls = ejabberd_config:get_option( - registration_watchers, - fun(JIDs) when is_list(JIDs) -> - lists:map( - fun(J) -> - #xmlel{name = <<"jid">>, attrs = [], - children = [{xmlcdata, - iolist_to_binary(J)}]} - end, JIDs) - end, []), - {result, - #xmlel{name = <<"registration_watchers">>, - attrs = Attrs, children = SubEls}}; -process_get(#xmlel{name = <<"acls">>, attrs = Attrs}) -> - Str = iolist_to_binary(io_lib:format("~p.", - [ets:tab2list(acl)])), - {result, - #xmlel{name = <<"acls">>, attrs = Attrs, - children = [{xmlcdata, Str}]}}; -process_get(#xmlel{name = <<"access">>, - attrs = Attrs}) -> - Str = iolist_to_binary(io_lib:format("~p.", - [ets:select(local_config, - [{{local_config, {access, '$1'}, - '$2'}, - [], - [{{access, '$1', - '$2'}}]}])])), - {result, - #xmlel{name = <<"access">>, attrs = Attrs, - children = [{xmlcdata, Str}]}}; -process_get(#xmlel{name = <<"last">>, attrs = Attrs}) -> - case catch mnesia:dirty_select(last_activity, - [{{last_activity, '_', '$1', '_'}, [], - ['$1']}]) - of - {'EXIT', _Reason} -> - {error, ?ERR_INTERNAL_SERVER_ERROR}; - Vals -> - {MegaSecs, Secs, _MicroSecs} = now(), - TimeStamp = MegaSecs * 1000000 + Secs, - Str = list_to_binary( - [[jlib:integer_to_binary(TimeStamp - V), - <<" ">>] || V <- Vals]), - {result, - #xmlel{name = <<"last">>, attrs = Attrs, - children = [{xmlcdata, Str}]}} - end; -%%process_get({xmlelement, Name, Attrs, SubEls}) -> -%% {result, }; -process_get(_) -> {error, ?ERR_BAD_REQUEST}. diff --git a/src/mod_configure_opt.erl b/src/mod_configure_opt.erl new file mode 100644 index 000000000..0a8c190fd --- /dev/null +++ b/src/mod_configure_opt.erl @@ -0,0 +1,13 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_configure_opt). + +-export([access/1]). + +-spec access(gen_mod:opts() | global | binary()) -> 'configure' | acl:acl(). +access(Opts) when is_map(Opts) -> + gen_mod:get_opt(access, Opts); +access(Host) -> + gen_mod:get_module_opt(Host, mod_configure, access). + diff --git a/src/mod_conversejs.erl b/src/mod_conversejs.erl new file mode 100644 index 000000000..c28151076 --- /dev/null +++ b/src/mod_conversejs.erl @@ -0,0 +1,462 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_conversejs.erl +%%% Author : Alexey Shchepin +%%% Purpose : Serve simple page for Converse.js XMPP web browser client +%%% Created : 8 Nov 2021 by Alexey Shchepin +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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]). +-export([web_menu_system/2]). + +-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, [{hook, webadmin_menu_system_post, web_menu_system, 50, global}]}. + +stop(_Host) -> + ok. + +reload(_Host, _NewOpts, _OldOpts) -> + ok. + +depends(_Host, _Opts) -> + []. + +process([], #request{method = 'GET', host = Host, q = Query, raw_path = RawPath1}) -> + [RawPath | _] = string:split(RawPath1, "?"), + ExtraOptions = get_auth_options(Host) + ++ get_autologin_options(Query) + ++ get_register_options(Host) + ++ get_extra_options(Host), + Domain = mod_conversejs_opt:default_domain(Host), + Script = get_file_url(Host, conversejs_script, + <>, + <<"https://cdn.conversejs.org/dist/converse.min.js">>), + CSS = get_file_url(Host, conversejs_css, + <>, + <<"https://cdn.conversejs.org/dist/converse.min.css">>), + PluginsHtml = get_plugins_html(Host, RawPath), + Init = [{<<"discover_connection_methods">>, false}, + {<<"default_domain">>, Domain}, + {<<"domain_placeholder">>, Domain}, + {<<"registration_domain">>, Domain}, + {<<"assets_path">>, <>}, + {<<"view_mode">>, <<"fullscreen">>} + | ExtraOptions], + Init2 = + case mod_host_meta:get_url(?MODULE, websocket, any, Host) of + undefined -> Init; + WSURL -> [{<<"websocket_url">>, WSURL} | Init] + end, + Init3 = + case mod_host_meta:get_url(?MODULE, bosh, any, Host) of + undefined -> Init2; + BoshURL -> [{<<"bosh_service_url">>, BoshURL} | Init2] + end, + Init4 = maps:from_list(Init3), + {200, [html], + [<<"">>, + <<"">>, + <<"">>, + <<"">>, + <<"">>, + <<"">> + ] ++ PluginsHtml ++ [ + <<"">>, + <<"">>, + <<"">>, + <<"">>, + <<"">>]}; +process(LocalPath, #request{host = Host}) -> + case is_served_file(LocalPath) of + true -> serve(Host, LocalPath); + false -> ejabberd_web:error(not_found) + end. + +%%---------------------------------------------------------------------- +%% File server +%%---------------------------------------------------------------------- + +is_served_file([<<"converse.min.js">>]) -> true; +is_served_file([<<"converse.min.css">>]) -> true; +is_served_file([<<"converse.min.js.map">>]) -> true; +is_served_file([<<"converse.min.css.map">>]) -> true; +is_served_file([<<"emojis.js">>]) -> true; +is_served_file([<<"locales">>, _]) -> true; +is_served_file([<<"locales">>, <<"dayjs">>, _]) -> true; +is_served_file([<<"webfonts">>, _]) -> true; +is_served_file([<<"plugins">>, _]) -> true; +is_served_file(_) -> false. + +serve(Host, LocalPath) -> + case get_conversejs_resources(Host) of + undefined -> + Path = str:join(LocalPath, <<"/">>), + {303, [{<<"Location">>, <<"https://cdn.conversejs.org/dist/", Path/binary>>}], <<>>}; + MainPath -> serve2(LocalPath, MainPath) + end. + +get_conversejs_resources(Host) -> + Opts = gen_mod:get_module_opts(Host, ?MODULE), + mod_conversejs_opt:conversejs_resources(Opts). + +%% Copied from mod_muc_log_http.erl + +serve2(LocalPathBin, MainPathBin) -> + LocalPath = [binary_to_list(LPB) || LPB <- LocalPathBin], + MainPath = binary_to_list(MainPathBin), + FileName = filename:join(filename:split(MainPath) ++ LocalPath), + case file:read_file(FileName) of + {ok, FileContents} -> + ?DEBUG("Delivering content.", []), + {200, + [{<<"Content-Type">>, content_type(FileName)}], + FileContents}; + {error, eisdir} -> + {403, [], "Forbidden"}; + {error, Error} -> + ?DEBUG("Delivering error: ~p", [Error]), + case Error of + eacces -> {403, [], "Forbidden"}; + enoent -> {404, [], "Not found"}; + _Else -> {404, [], atom_to_list(Error)} + end + end. + +content_type(Filename) -> + case string:to_lower(filename:extension(Filename)) of + ".css" -> "text/css"; + ".js" -> "text/javascript"; + ".map" -> "application/json"; + ".ttf" -> "font/ttf"; + ".woff" -> "font/woff"; + ".woff2" -> "font/woff2" + end. + +%%---------------------------------------------------------------------- +%% Options parsing +%%---------------------------------------------------------------------- + +get_auth_options(Domain) -> + case {ejabberd_auth_anonymous:is_login_anonymous_enabled(Domain), + ejabberd_auth_anonymous:is_sasl_anonymous_enabled(Domain)} of + {false, false} -> + [{<<"authentication">>, <<"login">>}]; + {true, false} -> + [{<<"authentication">>, <<"external">>}]; + {_, true} -> + [{<<"authentication">>, <<"anonymous">>}, + {<<"jid">>, Domain}] + end. + +get_register_options(Server) -> + AuthSupportsRegister = + lists:any( + fun(ejabberd_auth_mnesia) -> true; + (ejabberd_auth_external) -> true; + (ejabberd_auth_sql) -> true; + (_) -> false + end, + ejabberd_auth:auth_modules(Server)), + Modules = get_register_modules(Server), + ModRegisterAllowsMe = (Modules == all) orelse lists:member(?MODULE, Modules), + [{<<"allow_registration">>, AuthSupportsRegister and ModRegisterAllowsMe}]. + +get_register_modules(Server) -> + try mod_register_opt:allow_modules(Server) + catch + error:{module_not_loaded, mod_register, _} -> + ?DEBUG("mod_conversejs couldn't get mod_register configuration for " + "vhost ~p: module not loaded in that vhost.", [Server]), + [] + end. + +get_extra_options(Host) -> + RawOpts = gen_mod:get_module_opt(Host, ?MODULE, conversejs_options), + lists:map(fun({Name, <<"true">>}) -> {Name, true}; + ({Name, <<"false">>}) -> {Name, false}; + ({<<"locked_domain">> = Name, Value}) -> + {Name, misc:expand_keyword(<<"@HOST@">>, Value, Host)}; + ({Name, Value}) -> + {Name, Value} + end, + RawOpts). + +get_file_url(Host, Option, Filename, Default) -> + FileRaw = case gen_mod:get_module_opt(Host, ?MODULE, Option) of + auto -> get_auto_file_url(Host, Filename, Default); + F -> F + end, + misc:expand_keyword(<<"@HOST@">>, FileRaw, Host). + +get_auto_file_url(Host, Filename, Default) -> + case get_conversejs_resources(Host) of + undefined -> Default; + _ -> Filename + end. + +get_plugins_html(Host, RawPath) -> + Resources = get_conversejs_resources(Host), + lists:map(fun(F) -> + Plugin = + case {F, Resources} of + {<<"libsignal">>, undefined} -> + <<"https://cdn.conversejs.org/3rdparty/libsignal-protocol.min.js">>; + {<<"libsignal">>, Path} -> + ?WARNING_MSG("~p is configured to use local Converse files " + "from path ~ts but the public plugin ~ts!", + [?MODULE, Path, F]), + <<"https://cdn.conversejs.org/3rdparty/libsignal-protocol.min.js">>; + _ -> + fxml:crypt(<>) + end, + <<"">> + end, + gen_mod:get_module_opt(Host, ?MODULE, conversejs_plugins)). + +%%---------------------------------------------------------------------- +%% WebAdmin link and autologin +%%---------------------------------------------------------------------- + +%% @format-begin + +web_menu_system(Result, + #request{host = Host, + auth = Auth, + tp = Protocol}) -> + AutoUrl = mod_host_meta:get_auto_url(any, ?MODULE), + ConverseUrl = misc:expand_keyword(<<"@HOST@">>, AutoUrl, Host), + AutologinQuery = + case {Protocol, Auth} of + {http, {Jid, _Password}} -> + <<"/?autologinjid=", Jid/binary>>; + {https, {Jid, Password}} -> + AuthToken = build_token(Jid, Password), + <<"/?autologinjid=", Jid/binary, "&autologintoken=", AuthToken/binary>>; + _ -> + <<"">> + end, + ConverseEl = + ?LI([?C(unicode:characters_to_binary("☯️")), + ?XAE(<<"a">>, + [{<<"href">>, <>}, + {<<"target">>, <<"_blank">>}], + [?C(unicode:characters_to_binary("Converse"))])]), + [ConverseEl | Result]. + +get_autologin_options(Query) -> + case {proplists:get_value(<<"autologinjid">>, Query), + proplists:get_value(<<"autologintoken">>, Query)} + of + {undefined, _} -> + []; + {Jid, Token} -> + [{<<"auto_login">>, <<"true">>}, + {<<"jid">>, <<"admin@localhost">>}, + {<<"password">>, check_token_get_password(Jid, Token)}] + end. + +build_token(Jid, Password) -> + Minutes = + integer_to_binary(calendar:datetime_to_gregorian_seconds( + calendar:universal_time()) + div 60), + Cookie = + misc:atom_to_binary( + erlang:get_cookie()), + str:sha(<>). + +check_token_get_password(_, undefined) -> + <<"">>; +check_token_get_password(JidString, TokenProvided) -> + Jid = jid:decode(JidString), + Password = ejabberd_auth:get_password_s(Jid#jid.luser, Jid#jid.lserver), + case build_token(JidString, Password) of + TokenProvided -> + Password; + _ -> + <<"">> + end. +%% @format-end + +%%---------------------------------------------------------------------- +%% +%%---------------------------------------------------------------------- + +mod_opt_type(bosh_service_url) -> + econf:either(auto, econf:binary()); +mod_opt_type(websocket_url) -> + econf:either(auto, econf:binary()); +mod_opt_type(conversejs_resources) -> + econf:either(undefined, econf:directory()); +mod_opt_type(conversejs_options) -> + econf:map(econf:binary(), econf:either(econf:binary(), econf:int())); +mod_opt_type(conversejs_script) -> + econf:binary(); +mod_opt_type(conversejs_css) -> + econf:binary(); +mod_opt_type(conversejs_plugins) -> + econf:list(econf:binary()); +mod_opt_type(default_domain) -> + econf:host(). + +mod_options(Host) -> + [{bosh_service_url, auto}, + {websocket_url, auto}, + {default_domain, Host}, + {conversejs_resources, undefined}, + {conversejs_options, []}, + {conversejs_script, auto}, + {conversejs_plugins, []}, + {conversejs_css, auto}]. + +mod_doc() -> + #{desc => + [?T("This module serves a simple page for the " + "https://conversejs.org/[Converse] XMPP web browser client."), "", + ?T("To use this module, in addition to adding it to the 'modules' " + "section, you must also enable it in 'listen' -> 'ejabberd_http' -> " + "_`listen-options.md#request_handlers|request_handlers`_."), "", + ?T("Make sure either _`mod_bosh`_ or _`listen.md#ejabberd_http_ws|ejabberd_http_ws`_ " + "are enabled in at least one 'request_handlers'."), "", + ?T("When 'conversejs_css' and 'conversejs_script' are 'auto', " + "by default they point to the public Converse client."), "", + ?T("This module is available since ejabberd 21.12.") + ], + note => "improved in 25.07", + example => + [{?T("Manually setup WebSocket url, and use the public Converse client:"), + ["listen:", + " -", + " port: 5280", + " module: ejabberd_http", + " request_handlers:", + " /bosh: mod_bosh", + " /websocket: ejabberd_http_ws", + " /conversejs: mod_conversejs", + "", + "modules:", + " mod_bosh: {}", + " mod_conversejs:", + " conversejs_plugins: [\"libsignal\"]", + " websocket_url: \"ws://@HOST@:5280/websocket\""]}, + {?T("Host Converse locally and let auto detection of WebSocket and Converse URLs:"), + ["listen:", + " -", + " port: 443", + " module: ejabberd_http", + " tls: true", + " request_handlers:", + " /websocket: ejabberd_http_ws", + " /conversejs: mod_conversejs", + "", + "modules:", + " mod_conversejs:", + " conversejs_resources: \"/home/ejabberd/conversejs-x.y.z/package/dist\"", + " conversejs_plugins: [\"libsignal-protocol.min.js\"]", + " # File path is: /home/ejabberd/conversejs-x.y.z/package/dist/plugins/libsignal-protocol.min.js"]}, + {?T("Configure some additional options for Converse"), + ["modules:", + " mod_conversejs:", + " websocket_url: auto", + " conversejs_options:", + " auto_away: 30", + " clear_cache_on_logout: true", + " i18n: \"pt\"", + " locked_domain: \"@HOST@\"", + " message_archiving: always", + " theme: dracula"]} + ], + opts => + [{websocket_url, + #{value => ?T("auto | WebSocketURL"), + desc => + ?T("A WebSocket URL to which Converse can connect to. " + "The '@HOST@' keyword is replaced with the real virtual " + "host name. " + "If set to 'auto', it will build the URL of the first " + "configured WebSocket request handler. " + "The default value is 'auto'.")}}, + {bosh_service_url, + #{value => ?T("auto | BoshURL"), + desc => + ?T("BOSH service URL to which Converse can connect to. " + "The keyword '@HOST@' is replaced with the real " + "virtual host name. " + "If set to 'auto', it will build the URL of the first " + "configured BOSH request handler. " + "The default value is 'auto'.")}}, + {default_domain, + #{value => ?T("Domain"), + desc => + ?T("Specify a domain to act as the default for user JIDs. " + "The keyword '@HOST@' is replaced with the hostname. " + "The default value is '@HOST@'.")}}, + {conversejs_resources, + #{value => ?T("Path"), + note => "added in 22.05", + desc => + ?T("Local path to the Converse files. " + "If not set, the public Converse client will be used instead.")}}, + {conversejs_options, + #{value => "{Name: Value}", + note => "added in 22.05", + desc => + ?T("Specify additional options to be passed to Converse. " + "See https://conversejs.org/docs/html/configuration.html[Converse configuration]. " + "Only boolean, integer and string values are supported; " + "lists are not supported.")}}, + {conversejs_plugins, + #{value => ?T("[Filename]"), + desc => + ?T("List of additional local files to include as scripts in the homepage. " + "Please make sure those files are available in the path specified in " + "'conversejs_resources' option, in subdirectory 'plugins/'. " + "If using the public Converse client, then '\"libsignal\"' " + "gets replaced with the URL of the public library. " + "The default value is '[]'.")}}, + {conversejs_script, + #{value => ?T("auto | URL"), + desc => + ?T("Converse main script URL. " + "The keyword '@HOST@' is replaced with the hostname. " + "The default value is 'auto'.")}}, + {conversejs_css, + #{value => ?T("auto | URL"), + desc => + ?T("Converse CSS URL. " + "The keyword '@HOST@' is replaced with the hostname. " + "The default value is 'auto'.")}}] + }. diff --git a/src/mod_conversejs_opt.erl b/src/mod_conversejs_opt.erl new file mode 100644 index 000000000..37deac7ef --- /dev/null +++ b/src/mod_conversejs_opt.erl @@ -0,0 +1,62 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_conversejs_opt). + +-export([bosh_service_url/1]). +-export([conversejs_css/1]). +-export([conversejs_options/1]). +-export([conversejs_plugins/1]). +-export([conversejs_resources/1]). +-export([conversejs_script/1]). +-export([default_domain/1]). +-export([websocket_url/1]). + +-spec bosh_service_url(gen_mod:opts() | global | binary()) -> 'auto' | 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()) -> 'auto' | 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_options(gen_mod:opts() | global | binary()) -> [{binary(),binary() | integer()}]. +conversejs_options(Opts) when is_map(Opts) -> + gen_mod:get_opt(conversejs_options, Opts); +conversejs_options(Host) -> + gen_mod:get_module_opt(Host, mod_conversejs, conversejs_options). + +-spec conversejs_plugins(gen_mod:opts() | global | binary()) -> [binary()]. +conversejs_plugins(Opts) when is_map(Opts) -> + gen_mod:get_opt(conversejs_plugins, Opts); +conversejs_plugins(Host) -> + gen_mod:get_module_opt(Host, mod_conversejs, conversejs_plugins). + +-spec conversejs_resources(gen_mod:opts() | global | binary()) -> 'undefined' | binary(). +conversejs_resources(Opts) when is_map(Opts) -> + gen_mod:get_opt(conversejs_resources, Opts); +conversejs_resources(Host) -> + gen_mod:get_module_opt(Host, mod_conversejs, conversejs_resources). + +-spec conversejs_script(gen_mod:opts() | global | binary()) -> 'auto' | 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()) -> 'auto' | 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_delegation.erl b/src/mod_delegation.erl new file mode 100644 index 000000000..f6879ea7f --- /dev/null +++ b/src/mod_delegation.erl @@ -0,0 +1,434 @@ +%%%------------------------------------------------------------------- +%%% File : mod_delegation.erl +%%% Author : Anna Mukharram +%%% Purpose : XEP-0355: Namespace Delegation +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_delegation). + +-author('amuhar3@gmail.com'). + +-protocol({xep, 355, '0.4.1', '16.09', "complete", ""}). + +-behaviour(gen_server). +-behaviour(gen_mod). + +%% API +-export([start/2, stop/1, reload/3, mod_opt_type/1, depends/2, mod_options/1]). +-export([mod_doc/0]). +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). +-export([component_connected/1, component_disconnected/2, + ejabberd_local/1, ejabberd_sm/1, decode_iq_subel/1, + disco_local_features/5, disco_sm_features/5, + disco_local_identity/5, disco_sm_identity/5]). + +-include("logger.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). +-include("translate.hrl"). + +-type route_type() :: ejabberd_sm | ejabberd_local. +-type delegations() :: #{{binary(), route_type()} => {binary(), disco_info()}}. +-record(state, {server_host = <<"">> :: binary()}). + +%%%=================================================================== +%%% API +%%%=================================================================== +start(Host, Opts) -> + gen_mod:start_child(?MODULE, Host, Opts). + +stop(Host) -> + gen_mod:stop_child(?MODULE, Host). + +reload(_Host, _NewOpts, _OldOpts) -> + ok. + +mod_opt_type(namespaces) -> + econf:and_then( + econf:map( + econf:binary(), + econf:options( + #{filtering => econf:list(econf:binary()), + access => econf:acl()})), + fun(L) -> + lists:map( + fun({NS, Opts}) -> + Attrs = proplists:get_value(filtering, Opts, []), + Access = proplists:get_value(access, Opts, none), + {NS, Attrs, Access} + end, L) + end). + +-spec mod_options(binary()) -> [{namespaces, + [{binary(), [binary()], acl:acl()}]} | + {atom(), term()}]. +mod_options(_Host) -> + [{namespaces, []}]. + +mod_doc() -> + #{desc => + [?T("This module is an implementation of " + "https://xmpp.org/extensions/xep-0355.html" + "[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."), "", + ?T("WARNING: Security issue: Namespace delegation gives components " + "access to sensitive data, so permission should be granted " + "carefully, only if you trust the component."), "", + ?T("NOTE: This module is complementary to _`mod_privilege`_ but can " + "also be used separately.")], + opts => + [{namespaces, + #{value => "{Namespace: Options}", + desc => + ?T("If you want to delegate namespaces to a component, " + "specify them in this option, and associate them " + "to an access rule. The 'Options' are:")}, + [{filtering, + #{value => ?T("Attributes"), + desc => + ?T("The list of attributes. Currently not used.")}}, + {access, + #{value => ?T("AccessName"), + desc => + ?T("The option defines which components are allowed " + "for namespace delegation. The default value is 'none'.")}}]}], + example => + [{?T("Make sure you do not delegate the same namespace to several " + "services at the same time. As in the example provided later, " + "to have the 'sat-pubsub.example.org' component perform " + "correctly disable the _`mod_pubsub`_ module."), + ["access_rules:", + " external_pubsub:", + " allow: external_component", + " external_mam:", + " allow: external_component", + "", + "acl:", + " external_component:", + " server: sat-pubsub.example.org", + "", + "modules:", + " mod_delegation:", + " namespaces:", + " urn:xmpp:mam:1:", + " access: external_mam", + " http://jabber.org/protocol/pubsub:", + " access: external_pubsub"]}]}. + +depends(_, _) -> + []. + +-spec decode_iq_subel(xmpp_element() | xmlel()) -> xmpp_element() | xmlel(). +%% Tell gen_iq_handler not to auto-decode IQ payload +decode_iq_subel(El) -> + El. + +-spec component_connected(binary()) -> ok. +component_connected(Host) -> + lists:foreach( + fun(ServerHost) -> + Proc = gen_mod:get_module_proc(ServerHost, ?MODULE), + gen_server:cast(Proc, {component_connected, Host}) + end, ejabberd_option:hosts()). + +-spec component_disconnected(binary(), binary()) -> ok. +component_disconnected(Host, _Reason) -> + lists:foreach( + fun(ServerHost) -> + Proc = gen_mod:get_module_proc(ServerHost, ?MODULE), + gen_server:cast(Proc, {component_disconnected, Host}) + end, ejabberd_option:hosts()). + +-spec ejabberd_local(iq()) -> iq(). +ejabberd_local(IQ) -> + process_iq(IQ, ejabberd_local). + +-spec ejabberd_sm(iq()) -> iq(). +ejabberd_sm(IQ) -> + process_iq(IQ, ejabberd_sm). + +-spec disco_local_features(mod_disco:features_acc(), jid(), jid(), + binary(), binary()) -> mod_disco:features_acc(). +disco_local_features(Acc, From, To, Node, Lang) -> + disco_features(Acc, From, To, Node, Lang, ejabberd_local). + +-spec disco_sm_features(mod_disco:features_acc(), jid(), jid(), + binary(), binary()) -> mod_disco:features_acc(). +disco_sm_features(Acc, From, To, Node, Lang) -> + disco_features(Acc, From, To, Node, Lang, ejabberd_sm). + +-spec disco_local_identity([identity()], jid(), jid(), binary(), binary()) -> [identity()]. +disco_local_identity(Acc, From, To, Node, Lang) -> + disco_identity(Acc, From, To, Node, Lang, ejabberd_local). + +-spec disco_sm_identity([identity()], jid(), jid(), binary(), binary()) -> [identity()]. +disco_sm_identity(Acc, From, To, Node, Lang) -> + disco_identity(Acc, From, To, Node, Lang, ejabberd_sm). + +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== +init([Host|_]) -> + process_flag(trap_exit, true), + catch ets:new(?MODULE, + [named_table, public, + {heir, erlang:group_leader(), none}]), + ejabberd_hooks:add(component_connected, ?MODULE, + component_connected, 50), + ejabberd_hooks:add(component_disconnected, ?MODULE, + component_disconnected, 50), + ejabberd_hooks:add(disco_local_features, Host, ?MODULE, + disco_local_features, 50), + ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, + disco_sm_features, 50), + ejabberd_hooks:add(disco_local_identity, Host, ?MODULE, + disco_local_identity, 50), + ejabberd_hooks:add(disco_sm_identity, Host, ?MODULE, + disco_sm_identity, 50), + {ok, #state{server_host = Host}}. + +handle_call(Request, From, State) -> + ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), + {noreply, State}. + +handle_cast({component_connected, Host}, State) -> + ServerHost = State#state.server_host, + To = jid:make(Host), + NSAttrsAccessList = mod_delegation_opt:namespaces(ServerHost), + lists:foreach( + fun({NS, _Attrs, Access}) -> + case acl:match_rule(ServerHost, Access, To) of + allow -> + send_disco_queries(ServerHost, Host, NS); + deny -> + ?DEBUG("Denied delegation for ~ts on ~ts", [Host, NS]) + end + end, NSAttrsAccessList), + {noreply, State}; +handle_cast({component_disconnected, Host}, State) -> + ServerHost = State#state.server_host, + Delegations = + maps:filter( + fun({NS, Type}, {H, _}) when H == Host -> + ?INFO_MSG("Remove delegation of namespace '~ts' " + "from external component '~ts'", + [NS, Host]), + gen_iq_handler:remove_iq_handler(Type, ServerHost, NS), + false; + (_, _) -> + true + end, get_delegations(ServerHost)), + set_delegations(ServerHost, Delegations), + {noreply, State}; +handle_cast(Msg, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Msg]), + {noreply, State}. + +handle_info({iq_reply, ResIQ, {disco_info, Type, Host, NS}}, State) -> + case ResIQ of + #iq{type = result, sub_els = [SubEl]} -> + try xmpp:decode(SubEl) of + #disco_info{} = Info -> + ServerHost = State#state.server_host, + process_disco_info(ServerHost, Type, Host, NS, Info) + catch _:{xmpp_codec, _} -> + ok + end; + _ -> + ok + end, + {noreply, State}; +handle_info({iq_reply, ResIQ, #iq{} = IQ}, State) -> + process_iq_result(IQ, ResIQ), + {noreply, State}; +handle_info(Info, State) -> + ?WARNING_MSG("Unexpected info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, State) -> + ServerHost = State#state.server_host, + case gen_mod:is_loaded_elsewhere(ServerHost, ?MODULE) of + false -> + ejabberd_hooks:delete(component_connected, ?MODULE, + component_connected, 50), + ejabberd_hooks:delete(component_disconnected, ?MODULE, + component_disconnected, 50); + true -> + ok + end, + ejabberd_hooks:delete(disco_local_features, ServerHost, ?MODULE, + disco_local_features, 50), + ejabberd_hooks:delete(disco_sm_features, ServerHost, ?MODULE, + disco_sm_features, 50), + ejabberd_hooks:delete(disco_local_identity, ServerHost, ?MODULE, + disco_local_identity, 50), + ejabberd_hooks:delete(disco_sm_identity, ServerHost, ?MODULE, + disco_sm_identity, 50), + lists:foreach( + fun({NS, Type}) -> + gen_iq_handler:remove_iq_handler(Type, ServerHost, NS) + end, maps:keys(get_delegations(ServerHost))), + ets:delete(?MODULE, ServerHost). + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +-spec get_delegations(binary()) -> delegations(). +get_delegations(Host) -> + try ets:lookup_element(?MODULE, Host, 2) + catch _:badarg -> #{} + end. + +-spec set_delegations(binary(), delegations()) -> true. +set_delegations(ServerHost, Delegations) -> + case maps:size(Delegations) of + 0 -> ets:delete(?MODULE, ServerHost); + _ -> ets:insert(?MODULE, {ServerHost, Delegations}) + end. + +-spec process_iq(iq(), route_type()) -> ignore | iq(). +process_iq(#iq{to = To, lang = Lang, sub_els = [SubEl]} = IQ, Type) -> + LServer = To#jid.lserver, + NS = xmpp:get_ns(SubEl), + Delegations = get_delegations(LServer), + case maps:find({NS, Type}, Delegations) of + {ok, {Host, _}} -> + Delegation = #delegation{ + forwarded = #forwarded{sub_els = [IQ]}}, + NewFrom = jid:make(LServer), + NewTo = jid:make(Host), + ejabberd_router:route_iq( + #iq{type = set, + from = NewFrom, + to = NewTo, + sub_els = [Delegation]}, + IQ, gen_mod:get_module_proc(LServer, ?MODULE)), + ignore; + error -> + Txt = ?T("Failed to map delegated namespace to external component"), + xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) + end. + +-spec process_iq_result(iq(), iq()) -> ok. +process_iq_result(#iq{from = From, to = To, id = ID, lang = Lang} = IQ, + #iq{type = result} = ResIQ) -> + try + CodecOpts = ejabberd_config:codec_options(), + #delegation{forwarded = #forwarded{sub_els = [SubEl]}} = + xmpp:get_subtag(ResIQ, #delegation{}), + case xmpp:decode(SubEl, ?NS_CLIENT, CodecOpts) of + #iq{from = To, to = From, type = Type, id = ID} = Reply + when Type == error; Type == result -> + ejabberd_router:route(Reply) + end + catch _:_ -> + ?ERROR_MSG("Got iq-result with invalid delegated " + "payload:~n~ts", [xmpp:pp(ResIQ)]), + Txt = ?T("External component failure"), + Err = xmpp:err_internal_server_error(Txt, Lang), + ejabberd_router:route_error(IQ, Err) + end; +process_iq_result(#iq{from = From, to = To}, #iq{type = error} = ResIQ) -> + Err = xmpp:set_from_to(ResIQ, To, From), + ejabberd_router:route(Err); +process_iq_result(#iq{lang = Lang} = IQ, timeout) -> + Txt = ?T("External component timeout"), + Err = xmpp:err_internal_server_error(Txt, Lang), + ejabberd_router:route_error(IQ, Err). + +-spec process_disco_info(binary(), route_type(), + binary(), binary(), disco_info()) -> ok. +process_disco_info(ServerHost, Type, Host, NS, Info) -> + From = jid:make(ServerHost), + To = jid:make(Host), + Delegations = get_delegations(ServerHost), + case maps:find({NS, Type}, Delegations) of + error -> + Msg = #message{from = From, to = To, + sub_els = [#delegation{delegated = [#delegated{ns = NS}]}]}, + Delegations1 = maps:put({NS, Type}, {Host, Info}, Delegations), + gen_iq_handler:add_iq_handler(Type, ServerHost, NS, ?MODULE, Type), + ejabberd_router:route(Msg), + set_delegations(ServerHost, Delegations1), + ?INFO_MSG("Namespace '~ts' is delegated to external component '~ts'", + [NS, Host]); + {ok, {AnotherHost, _}} -> + ?WARNING_MSG("Failed to delegate namespace '~ts' to " + "external component '~ts' because it's already " + "delegated to '~ts'", + [NS, Host, AnotherHost]) + end. + +-spec send_disco_queries(binary(), binary(), binary()) -> ok. +send_disco_queries(LServer, Host, NS) -> + From = jid:make(LServer), + To = jid:make(Host), + lists:foreach( + fun({Type, Node}) -> + ejabberd_router:route_iq( + #iq{type = get, from = From, to = To, + sub_els = [#disco_info{node = Node}]}, + {disco_info, Type, Host, NS}, + gen_mod:get_module_proc(LServer, ?MODULE)) + end, [{ejabberd_local, <<(?NS_DELEGATION)/binary, "::", NS/binary>>}, + {ejabberd_sm, <<(?NS_DELEGATION)/binary, ":bare:", NS/binary>>}]). + +-spec disco_features(mod_disco:features_acc(), jid(), jid(), binary(), binary(), + route_type()) -> mod_disco:features_acc(). +disco_features(Acc, _From, To, <<"">>, _Lang, Type) -> + Delegations = get_delegations(To#jid.lserver), + Features = my_features(Type) ++ + lists:flatmap( + fun({{_, T}, {_, Info}}) when T == Type -> + Info#disco_info.features; + (_) -> + [] + end, maps:to_list(Delegations)), + case Acc of + empty when Features /= [] -> {result, Features}; + {result, Fs} -> {result, Fs ++ Features}; + _ -> Acc + end; +disco_features(Acc, _, _, _, _, _) -> + Acc. + +-spec disco_identity([identity()], jid(), jid(), binary(), binary(), + route_type()) -> [identity()]. +disco_identity(Acc, _From, To, <<"">>, _Lang, Type) -> + Delegations = get_delegations(To#jid.lserver), + Identities = lists:flatmap( + fun({{_, T}, {_, Info}}) when T == Type -> + Info#disco_info.identities; + (_) -> + [] + end, maps:to_list(Delegations)), + Acc ++ Identities; +disco_identity(Acc, _From, _To, _Node, _Lang, _Type) -> + Acc. + +my_features(ejabberd_local) -> [?NS_DELEGATION]; +my_features(ejabberd_sm) -> []. diff --git a/src/mod_delegation_opt.erl b/src/mod_delegation_opt.erl new file mode 100644 index 000000000..90965007e --- /dev/null +++ b/src/mod_delegation_opt.erl @@ -0,0 +1,13 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_delegation_opt). + +-export([namespaces/1]). + +-spec namespaces(gen_mod:opts() | global | binary()) -> [{binary(),[binary()],acl:acl()}]. +namespaces(Opts) when is_map(Opts) -> + gen_mod:get_opt(namespaces, Opts); +namespaces(Host) -> + gen_mod:get_module_opt(Host, mod_delegation, namespaces). + diff --git a/src/mod_disco.erl b/src/mod_disco.erl index 40f0f8e06..3dbecf6a9 100644 --- a/src/mod_disco.erl +++ b/src/mod_disco.erl @@ -5,7 +5,7 @@ %%% Created : 1 Jan 2003 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -27,255 +27,180 @@ -author('alexey@process-one.net'). +-protocol({xep, 30, '2.4', '0.1.0', "complete", ""}). +-protocol({xep, 157, '1.0', '2.1.0', "complete", ""}). + -behaviour(gen_mod). --export([start/2, stop/1, process_local_iq_items/3, - process_local_iq_info/3, get_local_identity/5, +-export([start/2, stop/1, reload/3, process_local_iq_items/1, + process_local_iq_info/1, get_local_identity/5, get_local_features/5, get_local_services/5, - process_sm_iq_items/3, process_sm_iq_info/3, + process_sm_iq_items/1, process_sm_iq_info/1, get_sm_identity/5, get_sm_features/5, get_sm_items/5, - get_info/5, register_feature/2, unregister_feature/2, - register_extra_domain/2, unregister_extra_domain/2, - transform_module_options/1]). + get_info/5, mod_opt_type/1, mod_options/1, depends/2, + mod_doc/0]). --include("ejabberd.hrl"). -include("logger.hrl"). - --include("jlib.hrl"). - +-include("translate.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). +-include_lib("stdlib/include/ms_transform.hrl"). -include("mod_roster.hrl"). +-type features_acc() :: {error, stanza_error()} | {result, [binary()]} | empty. +-type items_acc() :: {error, stanza_error()} | {result, [disco_item()]} | empty. +-export_type([features_acc/0, items_acc/0]). + start(Host, Opts) -> - ejabberd_local:refresh_iq_handlers(), - IQDisc = gen_mod:get_opt(iqdisc, Opts, fun gen_iq_handler:check_type/1, - one_queue), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, - ?NS_DISCO_ITEMS, ?MODULE, - process_local_iq_items, IQDisc), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, - ?NS_DISCO_INFO, ?MODULE, - process_local_iq_info, IQDisc), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, - ?NS_DISCO_ITEMS, ?MODULE, process_sm_iq_items, - IQDisc), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, - ?NS_DISCO_INFO, ?MODULE, process_sm_iq_info, - IQDisc), - catch ets:new(disco_features, - [named_table, ordered_set, public]), - register_feature(Host, <<"iq">>), - register_feature(Host, <<"presence">>), catch ets:new(disco_extra_domains, - [named_table, ordered_set, public]), - ExtraDomains = gen_mod:get_opt(extra_domains, Opts, - fun(Hs) -> - [iolist_to_binary(H) || H <- Hs] - end, []), + [named_table, ordered_set, public, + {heir, erlang:group_leader(), none}]), + ExtraDomains = mod_disco_opt:extra_domains(Opts), lists:foreach(fun (Domain) -> register_extra_domain(Host, Domain) end, ExtraDomains), - catch ets:new(disco_sm_features, - [named_table, ordered_set, public]), - catch ets:new(disco_sm_nodes, - [named_table, ordered_set, public]), - ejabberd_hooks:add(disco_local_items, Host, ?MODULE, - get_local_services, 100), - ejabberd_hooks:add(disco_local_features, Host, ?MODULE, - get_local_features, 100), - ejabberd_hooks:add(disco_local_identity, Host, ?MODULE, - get_local_identity, 100), - ejabberd_hooks:add(disco_sm_items, Host, ?MODULE, - get_sm_items, 100), - ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, - get_sm_features, 100), - ejabberd_hooks:add(disco_sm_identity, Host, ?MODULE, - get_sm_identity, 100), - ejabberd_hooks:add(disco_info, Host, ?MODULE, get_info, - 100), - ok. + {ok, [{iq_handler, ejabberd_local, ?NS_DISCO_ITEMS, process_local_iq_items}, + {iq_handler, ejabberd_local, ?NS_DISCO_INFO, process_local_iq_info}, + {iq_handler, ejabberd_sm, ?NS_DISCO_ITEMS, process_sm_iq_items}, + {iq_handler, ejabberd_sm, ?NS_DISCO_INFO, process_sm_iq_info}, + {hook, disco_local_items, get_local_services, 100}, + {hook, disco_local_features, get_local_features, 100}, + {hook, disco_local_identity, get_local_identity, 100}, + {hook, disco_sm_items, get_sm_items, 100}, + {hook, disco_sm_features, get_sm_features, 100}, + {hook, disco_sm_identity, get_sm_identity, 100}, + {hook, disco_info, get_info, 100}]}. stop(Host) -> - ejabberd_hooks:delete(disco_sm_identity, Host, ?MODULE, - get_sm_identity, 100), - ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, - get_sm_features, 100), - ejabberd_hooks:delete(disco_sm_items, Host, ?MODULE, - get_sm_items, 100), - ejabberd_hooks:delete(disco_local_identity, Host, - ?MODULE, get_local_identity, 100), - ejabberd_hooks:delete(disco_local_features, Host, - ?MODULE, get_local_features, 100), - ejabberd_hooks:delete(disco_local_items, Host, ?MODULE, - get_local_services, 100), - ejabberd_hooks:delete(disco_info, Host, ?MODULE, - get_info, 100), - gen_iq_handler:remove_iq_handler(ejabberd_local, Host, - ?NS_DISCO_ITEMS), - gen_iq_handler:remove_iq_handler(ejabberd_local, Host, - ?NS_DISCO_INFO), - gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, - ?NS_DISCO_ITEMS), - gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, - ?NS_DISCO_INFO), - catch ets:match_delete(disco_features, {{'_', Host}}), catch ets:match_delete(disco_extra_domains, {{'_', Host}}), ok. -register_feature(Host, Feature) -> - catch ets:new(disco_features, - [named_table, ordered_set, public]), - ets:insert(disco_features, {{Feature, Host}}). - -unregister_feature(Host, Feature) -> - catch ets:new(disco_features, - [named_table, ordered_set, public]), - ets:delete(disco_features, {Feature, Host}). +reload(Host, NewOpts, OldOpts) -> + NewDomains = mod_disco_opt:extra_domains(NewOpts), + OldDomains = mod_disco_opt:extra_domains(OldOpts), + lists:foreach( + fun(Domain) -> + register_extra_domain(Host, Domain) + end, NewDomains -- OldDomains), + lists:foreach( + fun(Domain) -> + unregister_extra_domain(Host, Domain) + end, OldDomains -- NewDomains). +-spec register_extra_domain(binary(), binary()) -> true. register_extra_domain(Host, Domain) -> - catch ets:new(disco_extra_domains, - [named_table, ordered_set, public]), ets:insert(disco_extra_domains, {{Domain, Host}}). +-spec unregister_extra_domain(binary(), binary()) -> true. unregister_extra_domain(Host, Domain) -> - catch ets:new(disco_extra_domains, - [named_table, ordered_set, public]), - ets:delete(disco_extra_domains, {Domain, Host}). + ets:delete_object(disco_extra_domains, {{Domain, Host}}). -process_local_iq_items(From, To, - #iq{type = Type, lang = Lang, sub_el = SubEl} = IQ) -> - case Type of - set -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; - get -> - Node = xml:get_tag_attr_s(<<"node">>, SubEl), - Host = To#jid.lserver, - case ejabberd_hooks:run_fold(disco_local_items, Host, - empty, [From, To, Node, Lang]) - of - {result, Items} -> - ANode = case Node of - <<"">> -> []; - _ -> [{<<"node">>, Node}] - end, - IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"query">>, - attrs = - [{<<"xmlns">>, ?NS_DISCO_ITEMS} | ANode], - children = Items}]}; - {error, Error} -> - IQ#iq{type = error, sub_el = [SubEl, Error]} - end +-spec process_local_iq_items(iq()) -> iq(). +process_local_iq_items(#iq{type = set, lang = Lang} = IQ) -> + Txt = ?T("Value 'set' of 'type' attribute is not allowed"), + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); +process_local_iq_items(#iq{type = get, lang = Lang, + from = From, to = To, + sub_els = [#disco_items{node = Node}]} = IQ) -> + Host = To#jid.lserver, + case ejabberd_hooks:run_fold(disco_local_items, Host, + empty, [From, To, Node, Lang]) of + {result, Items} -> + xmpp:make_iq_result(IQ, #disco_items{node = Node, items = Items}); + {error, Error} -> + xmpp:make_error(IQ, Error) end. -process_local_iq_info(From, To, - #iq{type = Type, lang = Lang, sub_el = SubEl} = IQ) -> - case Type of - set -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; - get -> - Host = To#jid.lserver, - Node = xml:get_tag_attr_s(<<"node">>, SubEl), - Identity = ejabberd_hooks:run_fold(disco_local_identity, - Host, [], [From, To, Node, Lang]), - Info = ejabberd_hooks:run_fold(disco_info, Host, [], - [Host, ?MODULE, Node, Lang]), - case ejabberd_hooks:run_fold(disco_local_features, Host, - empty, [From, To, Node, Lang]) - of - {result, Features} -> - ANode = case Node of - <<"">> -> []; - _ -> [{<<"node">>, Node}] - end, - IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"query">>, - attrs = - [{<<"xmlns">>, ?NS_DISCO_INFO} | ANode], - children = - Identity ++ - Info ++ features_to_xml(Features)}]}; - {error, Error} -> - IQ#iq{type = error, sub_el = [SubEl, Error]} - end +-spec process_local_iq_info(iq()) -> iq(). +process_local_iq_info(#iq{type = set, lang = Lang} = IQ) -> + Txt = ?T("Value 'set' of 'type' attribute is not allowed"), + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); +process_local_iq_info(#iq{type = get, lang = Lang, + from = From, to = To, + sub_els = [#disco_info{node = Node}]} = IQ) -> + Host = To#jid.lserver, + Identity = ejabberd_hooks:run_fold(disco_local_identity, + Host, [], [From, To, Node, Lang]), + Info = ejabberd_hooks:run_fold(disco_info, Host, [], + [Host, ?MODULE, Node, Lang]), + case ejabberd_hooks:run_fold(disco_local_features, Host, + empty, [From, To, Node, Lang]) of + {result, Features} -> + xmpp:make_iq_result(IQ, #disco_info{node = Node, + identities = Identity, + xdata = Info, + features = Features}); + {error, Error} -> + xmpp:make_error(IQ, Error) end. -get_local_identity(Acc, _From, _To, <<>>, _Lang) -> - Acc ++ - [#xmlel{name = <<"identity">>, - attrs = - [{<<"category">>, <<"server">>}, {<<"type">>, <<"im">>}, - {<<"name">>, <<"ejabberd">>}], - children = []}]; +-spec get_local_identity([identity()], jid(), jid(), + binary(), binary()) -> [identity()]. +get_local_identity(Acc, _From, To, <<"">>, _Lang) -> + Host = To#jid.lserver, + Name = mod_disco_opt:name(Host), + Acc ++ [#identity{category = <<"server">>, + type = <<"im">>, + name = Name}]; get_local_identity(Acc, _From, _To, _Node, _Lang) -> Acc. +-spec get_local_features(features_acc(), jid(), jid(), binary(), binary()) -> + {error, stanza_error()} | {result, [binary()]}. get_local_features({error, _Error} = Acc, _From, _To, _Node, _Lang) -> Acc; -get_local_features(Acc, _From, To, <<>>, _Lang) -> +get_local_features(Acc, _From, To, <<"">>, _Lang) -> Feats = case Acc of - {result, Features} -> Features; - empty -> [] + {result, Features} -> Features; + empty -> [] end, - Host = To#jid.lserver, - {result, - ets:select(disco_features, - [{{{'_', Host}}, [], ['$_']}]) - ++ Feats}; -get_local_features(Acc, _From, _To, _Node, _Lang) -> + {result, lists:usort( + lists:flatten( + [?NS_FEATURE_IQ, ?NS_FEATURE_PRESENCE, + ?NS_DISCO_INFO, ?NS_DISCO_ITEMS, Feats, + ejabberd_local:get_features(To#jid.lserver)]))}; +get_local_features(Acc, _From, _To, _Node, Lang) -> case Acc of {result, _Features} -> Acc; - empty -> {error, ?ERR_ITEM_NOT_FOUND} + empty -> + Txt = ?T("No features available"), + {error, xmpp:err_item_not_found(Txt, Lang)} end. -features_to_xml(FeatureList) -> - [#xmlel{name = <<"feature">>, - attrs = [{<<"var">>, Feat}], children = []} - || Feat - <- lists:usort(lists:map(fun ({{Feature, _Host}}) -> - Feature; - (Feature) when is_binary(Feature) -> - Feature - end, - FeatureList))]. - -domain_to_xml({Domain}) -> - #xmlel{name = <<"item">>, attrs = [{<<"jid">>, Domain}], - children = []}; -domain_to_xml(Domain) -> - #xmlel{name = <<"item">>, attrs = [{<<"jid">>, Domain}], - children = []}. - +-spec get_local_services(items_acc(), jid(), jid(), binary(), binary()) -> + {error, stanza_error()} | {result, [disco_item()]}. get_local_services({error, _Error} = Acc, _From, _To, _Node, _Lang) -> Acc; -get_local_services(Acc, _From, To, <<>>, _Lang) -> +get_local_services(Acc, _From, To, <<"">>, _Lang) -> Items = case Acc of {result, Its} -> Its; empty -> [] end, Host = To#jid.lserver, {result, - lists:usort(lists:map(fun domain_to_xml/1, - get_vh_services(Host) ++ - ets:select(disco_extra_domains, - [{{{'$1', Host}}, [], ['$1']}]))) - ++ Items}; + lists:usort( + lists:map( + fun(Domain) -> #disco_item{jid = jid:make(Domain)} end, + get_vh_services(Host) ++ + ets:select(disco_extra_domains, + ets:fun2ms( + fun({{D, H}}) when H == Host -> D end)))) + ++ Items}; get_local_services({result, _} = Acc, _From, _To, _Node, _Lang) -> Acc; -get_local_services(empty, _From, _To, _Node, _Lang) -> - {error, ?ERR_ITEM_NOT_FOUND}. +get_local_services(empty, _From, _To, _Node, Lang) -> + {error, xmpp:err_item_not_found(?T("No services available"), Lang)}. +-spec get_vh_services(binary()) -> [binary()]. get_vh_services(Host) -> Hosts = lists:sort(fun (H1, H2) -> byte_size(H1) >= byte_size(H2) end, - ?MYHOSTS), + ejabberd_option:hosts()), lists:filter(fun (H) -> case lists:dropwhile(fun (VH) -> not @@ -289,54 +214,45 @@ get_vh_services(Host) -> [VH | _] -> VH == Host end end, - ejabberd_router:dirty_get_all_routes()). + ejabberd_router:get_all_routes()). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -process_sm_iq_items(From, To, - #iq{type = Type, lang = Lang, sub_el = SubEl} = IQ) -> - case Type of - set -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; - get -> - case is_presence_subscribed(From, To) of - true -> - Host = To#jid.lserver, - Node = xml:get_tag_attr_s(<<"node">>, SubEl), - case ejabberd_hooks:run_fold(disco_sm_items, Host, - empty, [From, To, Node, Lang]) - of - {result, Items} -> - ANode = case Node of - <<"">> -> []; - _ -> [{<<"node">>, Node}] - end, - IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"query">>, - attrs = - [{<<"xmlns">>, ?NS_DISCO_ITEMS} - | ANode], - children = Items}]}; - {error, Error} -> - IQ#iq{type = error, sub_el = [SubEl, Error]} - end; - false -> - IQ#iq{type = error, - sub_el = [SubEl, ?ERR_SERVICE_UNAVAILABLE]} - end +-spec process_sm_iq_items(iq()) -> iq(). +process_sm_iq_items(#iq{type = set, lang = Lang} = IQ) -> + Txt = ?T("Value 'set' of 'type' attribute is not allowed"), + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); +process_sm_iq_items(#iq{type = get, lang = Lang, + from = From, to = To, + sub_els = [#disco_items{node = Node}]} = IQ) -> + case mod_roster:is_subscribed(From, To) of + true -> + Host = To#jid.lserver, + case ejabberd_hooks:run_fold(disco_sm_items, Host, + empty, [From, To, Node, Lang]) of + {result, Items} -> + xmpp:make_iq_result( + IQ, #disco_items{node = Node, items = Items}); + {error, Error} -> + xmpp:make_error(IQ, Error) + end; + false -> + Txt = ?T("Not subscribed"), + xmpp:make_error(IQ, xmpp:err_subscription_required(Txt, Lang)) end. +-spec get_sm_items(items_acc(), jid(), jid(), binary(), binary()) -> + {error, stanza_error()} | {result, [disco_item()]}. get_sm_items({error, _Error} = Acc, _From, _To, _Node, _Lang) -> Acc; get_sm_items(Acc, From, - #jid{user = User, server = Server} = To, <<>>, _Lang) -> + #jid{user = User, server = Server} = To, <<"">>, _Lang) -> Items = case Acc of {result, Its} -> Its; empty -> [] end, - Items1 = case is_presence_subscribed(From, To) of + Items1 = case mod_roster:is_subscribed(From, To) of true -> get_user_resources(User, Server); _ -> [] end, @@ -344,175 +260,212 @@ get_sm_items(Acc, From, get_sm_items({result, _} = Acc, _From, _To, _Node, _Lang) -> Acc; -get_sm_items(empty, From, To, _Node, _Lang) -> +get_sm_items(empty, From, To, _Node, Lang) -> #jid{luser = LFrom, lserver = LSFrom} = From, #jid{luser = LTo, lserver = LSTo} = To, case {LFrom, LSFrom} of - {LTo, LSTo} -> {error, ?ERR_ITEM_NOT_FOUND}; - _ -> {error, ?ERR_NOT_ALLOWED} + {LTo, LSTo} -> {error, xmpp:err_item_not_found()}; + _ -> + Txt = ?T("Query to another users is forbidden"), + {error, xmpp:err_not_allowed(Txt, Lang)} end. -is_presence_subscribed(#jid{luser = User, - lserver = Server}, - #jid{luser = LUser, lserver = LServer}) -> - lists:any(fun (#roster{jid = {TUser, TServer, _}, - subscription = S}) -> - if LUser == TUser, LServer == TServer, S /= none -> - true; - true -> false - end - end, - ejabberd_hooks:run_fold(roster_get, Server, [], - [{User, Server}])) - orelse User == LUser andalso Server == LServer. - -process_sm_iq_info(From, To, - #iq{type = Type, lang = Lang, sub_el = SubEl} = IQ) -> - case Type of - set -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; - get -> - case is_presence_subscribed(From, To) of - true -> - Host = To#jid.lserver, - Node = xml:get_tag_attr_s(<<"node">>, SubEl), - Identity = ejabberd_hooks:run_fold(disco_sm_identity, - Host, [], - [From, To, Node, Lang]), - case ejabberd_hooks:run_fold(disco_sm_features, Host, - empty, [From, To, Node, Lang]) - of - {result, Features} -> - ANode = case Node of - <<"">> -> []; - _ -> [{<<"node">>, Node}] - end, - IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"query">>, - attrs = - [{<<"xmlns">>, ?NS_DISCO_INFO} - | ANode], - children = - Identity ++ - features_to_xml(Features)}]}; - {error, Error} -> - IQ#iq{type = error, sub_el = [SubEl, Error]} - end; - false -> - IQ#iq{type = error, - sub_el = [SubEl, ?ERR_SERVICE_UNAVAILABLE]} - end +-spec process_sm_iq_info(iq()) -> iq(). +process_sm_iq_info(#iq{type = set, lang = Lang} = IQ) -> + Txt = ?T("Value 'set' of 'type' attribute is not allowed"), + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); +process_sm_iq_info(#iq{type = get, lang = Lang, + from = From, to = To, + sub_els = [#disco_info{node = Node}]} = IQ) -> + case mod_roster:is_subscribed(From, To) of + true -> + Host = To#jid.lserver, + Identity = ejabberd_hooks:run_fold(disco_sm_identity, + Host, [], + [From, To, Node, Lang]), + Info = ejabberd_hooks:run_fold(disco_info, Host, [], + [From, To, Node, Lang]), + case ejabberd_hooks:run_fold(disco_sm_features, Host, + empty, [From, To, Node, Lang]) of + {result, Features} -> + xmpp:make_iq_result(IQ, #disco_info{node = Node, + identities = Identity, + xdata = Info, + features = Features}); + {error, Error} -> + xmpp:make_error(IQ, Error) + end; + false -> + Txt = ?T("Not subscribed"), + xmpp:make_error(IQ, xmpp:err_subscription_required(Txt, Lang)) end. +-spec get_sm_identity([identity()], jid(), jid(), + binary(), binary()) -> [identity()]. get_sm_identity(Acc, _From, #jid{luser = LUser, lserver = LServer}, _Node, _Lang) -> Acc ++ - case ejabberd_auth:is_user_exists(LUser, LServer) of + case ejabberd_auth:user_exists(LUser, LServer) of true -> - [#xmlel{name = <<"identity">>, - attrs = - [{<<"category">>, <<"account">>}, - {<<"type">>, <<"registered">>}], - children = []}]; + [#identity{category = <<"account">>, type = <<"registered">>}]; _ -> [] end. -get_sm_features(empty, From, To, _Node, _Lang) -> +-spec get_sm_features(features_acc(), jid(), jid(), binary(), binary()) -> + {error, stanza_error()} | {result, [binary()]}. +get_sm_features(empty, From, To, Node, Lang) -> #jid{luser = LFrom, lserver = LSFrom} = From, #jid{luser = LTo, lserver = LSTo} = To, case {LFrom, LSFrom} of - {LTo, LSTo} -> {error, ?ERR_ITEM_NOT_FOUND}; - _ -> {error, ?ERR_NOT_ALLOWED} + {LTo, LSTo} -> + case Node of + <<"">> -> {result, [?NS_DISCO_INFO, ?NS_DISCO_ITEMS]}; + _ -> {error, xmpp:err_item_not_found()} + end; + _ -> + Txt = ?T("Query to another users is forbidden"), + {error, xmpp:err_not_allowed(Txt, Lang)} end; +get_sm_features({result, Features}, _From, _To, <<"">>, _Lang) -> + {result, [?NS_DISCO_INFO, ?NS_DISCO_ITEMS|Features]}; get_sm_features(Acc, _From, _To, _Node, _Lang) -> Acc. +-spec get_user_resources(binary(), binary()) -> [disco_item()]. get_user_resources(User, Server) -> Rs = ejabberd_sm:get_user_resources(User, Server), - lists:map(fun (R) -> - #xmlel{name = <<"item">>, - attrs = - [{<<"jid">>, - <>}, - {<<"name">>, User}], - children = []} - end, - lists:sort(Rs)). - -transform_module_options(Opts) -> - lists:map( - fun({server_info, Infos}) -> - NewInfos = lists:map( - fun({Modules, Name, URLs}) -> - [[{modules, Modules}, - {name, Name}, - {urls, URLs}]]; - (Opt) -> - Opt - end, Infos), - {server_info, NewInfos}; - (Opt) -> - Opt - end, Opts). + [#disco_item{jid = jid:make(User, Server, Resource), name = User} + || Resource <- lists:sort(Rs)]. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% Support for: XEP-0157 Contact Addresses for XMPP Services -get_info(_A, Host, Mod, Node, _Lang) when Node == <<>> -> +-spec get_info([xdata()], binary(), module(), binary(), binary()) -> [xdata()]; + ([xdata()], jid(), jid(), binary(), binary()) -> [xdata()]. +get_info(_A, Host, Mod, Node, _Lang) when is_atom(Mod), Node == <<"">> -> Module = case Mod of undefined -> ?MODULE; _ -> Mod end, - Serverinfo_fields = get_fields_xml(Host, Module), - [#xmlel{name = <<"x">>, - attrs = - [{<<"xmlns">>, ?NS_XDATA}, {<<"type">>, <<"result">>}], - children = - [#xmlel{name = <<"field">>, - attrs = - [{<<"var">>, <<"FORM_TYPE">>}, - {<<"type">>, <<"hidden">>}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, ?NS_SERVERINFO}]}]}] - ++ Serverinfo_fields}]; + [#xdata{type = result, + fields = [#xdata_field{type = hidden, + var = <<"FORM_TYPE">>, + values = [?NS_SERVERINFO]} + | get_fields(Host, Module)]}]; get_info(Acc, _, _, _Node, _) -> Acc. -get_fields_xml(Host, Module) -> - Fields = gen_mod:get_module_opt( - Host, ?MODULE, server_info, - fun(L) -> - lists:map( - fun(Opts) -> - Mods = proplists:get_value(modules, Opts, all), - Name = proplists:get_value(name, Opts, <<>>), - URLs = proplists:get_value(urls, Opts, []), - {Mods, Name, URLs} - end, L) - end, []), - Fields_good = lists:filter(fun ({Modules, _, _}) -> - case Modules of - all -> true; - Modules -> - lists:member(Module, Modules) - end - end, - Fields), - fields_to_xml(Fields_good). +-spec get_fields(binary(), module()) -> [xdata_field()]. +get_fields(Host, Module) -> + Fields = mod_disco_opt:server_info(Host), + Fields1 = lists:filter(fun ({Modules, _, _}) -> + case Modules of + all -> true; + Modules -> + lists:member(Module, Modules) + end + end, + Fields), + [#xdata_field{var = Var, + type = 'list-multi', + values = Values} || {_, Var, Values} <- Fields1]. -fields_to_xml(Fields) -> - [field_to_xml(Field) || Field <- Fields]. +-spec depends(binary(), gen_mod:opts()) -> []. +depends(_Host, _Opts) -> + []. -field_to_xml({_, Var, Values}) -> - Values_xml = values_to_xml(Values), - #xmlel{name = <<"field">>, attrs = [{<<"var">>, Var}], - children = Values_xml}. +mod_opt_type(extra_domains) -> + econf:list(econf:binary()); +mod_opt_type(name) -> + econf:binary(); +mod_opt_type(server_info) -> + econf:list( + econf:and_then( + econf:options( + #{name => econf:binary(), + urls => econf:list(econf:binary()), + modules => + econf:either( + all, + econf:list(econf:beam()))}), + fun(Opts) -> + Mods = proplists:get_value(modules, Opts, all), + Name = proplists:get_value(name, Opts, <<>>), + URLs = proplists:get_value(urls, Opts, []), + {Mods, Name, URLs} + end)). -values_to_xml(Values) -> - lists:map(fun (Value) -> - #xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, Value}]} - end, - Values). +-spec mod_options(binary()) -> [{server_info, + [{all | [module()], binary(), [binary()]}]} | + {atom(), any()}]. +mod_options(_Host) -> + [{extra_domains, []}, + {server_info, []}, + {name, ?T("ejabberd")}]. + +mod_doc() -> + #{desc => + ?T("This module adds support for " + "https://xmpp.org/extensions/xep-0030.html" + "[XEP-0030: Service Discovery]. With this module enabled, " + "services on your server can be discovered by XMPP clients."), + opts => + [{extra_domains, + #{value => "[Domain, ...]", + desc => + ?T("With this option, you can specify a list of extra " + "domains that are added to the Service Discovery item list. " + "The default value is an empty list.")}}, + {name, + #{value => ?T("Name"), + desc => + ?T("A name of the server in the Service Discovery. " + "This will only be displayed by special XMPP clients. " + "The default value is 'ejabberd'.")}}, + {server_info, + #{value => "[Info, ...]", + example => + ["server_info:", + " -", + " modules: all", + " name: abuse-addresses", + " urls: [\"mailto:abuse@shakespeare.lit\"]", + " -", + " modules: [mod_muc]", + " name: \"Web chatroom logs\"", + " urls: [\"http://www.example.org/muc-logs\"]", + " -", + " modules: [mod_disco]", + " name: feedback-addresses", + " urls:", + " - http://shakespeare.lit/feedback.php", + " - mailto:feedback@shakespeare.lit", + " - xmpp:feedback@shakespeare.lit", + " -", + " modules:", + " - mod_disco", + " - mod_vcard", + " name: admin-addresses", + " urls:", + " - mailto:xmpp@shakespeare.lit", + " - xmpp:admins@shakespeare.lit"], + desc => + ?T("Specify additional information about the server, " + "as described in https://xmpp.org/extensions/xep-0157.html" + "[XEP-0157: Contact Addresses for XMPP Services]. Every 'Info' " + "element in the list is constructed from the following options:")}, + [{modules, + #{value => "all | [Module, ...]", + desc => + ?T("The value can be the keyword 'all', in which case the " + "information is reported in all the services, " + "or a list of ejabberd modules, in which case the " + "information is only specified for the services provided " + "by those modules.")}}, + {name, + #{value => ?T("Name"), + desc => ?T("The field 'var' name that will be defined. " + "See XEP-0157 for some standardized names.")}}, + {urls, + #{value => "[URI, ...]", + desc => ?T("A list of contact URIs, such as " + "HTTP URLs, XMPP URIs and so on.")}}]}]}. diff --git a/src/mod_disco_opt.erl b/src/mod_disco_opt.erl new file mode 100644 index 000000000..a66c3293a --- /dev/null +++ b/src/mod_disco_opt.erl @@ -0,0 +1,27 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_disco_opt). + +-export([extra_domains/1]). +-export([name/1]). +-export([server_info/1]). + +-spec extra_domains(gen_mod:opts() | global | binary()) -> [binary()]. +extra_domains(Opts) when is_map(Opts) -> + gen_mod:get_opt(extra_domains, Opts); +extra_domains(Host) -> + gen_mod:get_module_opt(Host, mod_disco, extra_domains). + +-spec name(gen_mod:opts() | global | binary()) -> binary(). +name(Opts) when is_map(Opts) -> + gen_mod:get_opt(name, Opts); +name(Host) -> + gen_mod:get_module_opt(Host, mod_disco, name). + +-spec server_info(gen_mod:opts() | global | binary()) -> [{'all' | [module()],binary(),[binary()]}]. +server_info(Opts) when is_map(Opts) -> + gen_mod:get_opt(server_info, Opts); +server_info(Host) -> + gen_mod:get_module_opt(Host, mod_disco, server_info). + diff --git a/src/mod_echo.erl b/src/mod_echo.erl deleted file mode 100644 index 63dc8c81b..000000000 --- a/src/mod_echo.erl +++ /dev/null @@ -1,198 +0,0 @@ -%%%---------------------------------------------------------------------- -%%% File : mod_echo.erl -%%% Author : Alexey Shchepin -%%% Purpose : Simple ejabberd module. -%%% Created : 15 Jan 2003 by Alexey Shchepin -%%% -%%% -%%% ejabberd, Copyright (C) 2002-2015 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_echo). - --author('alexey@process-one.net'). - --behaviour(gen_server). - --behaviour(gen_mod). - -%% API --export([start_link/2, start/2, stop/1, - do_client_version/3]). - -%% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, - handle_info/2, terminate/2, code_change/3]). - --include("ejabberd.hrl"). --include("logger.hrl"). - --include("jlib.hrl"). - --record(state, {host = <<"">> :: binary()}). - --define(PROCNAME, ejabberd_mod_echo). - -%%==================================================================== -%% API -%%==================================================================== -%%-------------------------------------------------------------------- -%% Function: start_link() -> {ok,Pid} | ignore | {error,Error} -%% Description: Starts the server -%%-------------------------------------------------------------------- -start_link(Host, Opts) -> - Proc = gen_mod:get_module_proc(Host, ?PROCNAME), - gen_server:start_link({local, Proc}, ?MODULE, - [Host, Opts], []). - -start(Host, Opts) -> - Proc = gen_mod:get_module_proc(Host, ?PROCNAME), - ChildSpec = {Proc, {?MODULE, start_link, [Host, Opts]}, - temporary, 1000, worker, [?MODULE]}, - supervisor:start_child(ejabberd_sup, ChildSpec). - -stop(Host) -> - Proc = gen_mod:get_module_proc(Host, ?PROCNAME), - gen_server:call(Proc, stop), - supervisor:terminate_child(ejabberd_sup, Proc), - supervisor:delete_child(ejabberd_sup, Proc). - -%%==================================================================== -%% gen_server callbacks -%%==================================================================== - -%%-------------------------------------------------------------------- -%% Function: init(Args) -> {ok, State} | -%% {ok, State, Timeout} | -%% ignore | -%% {stop, Reason} -%% Description: Initiates the server -%%-------------------------------------------------------------------- -init([Host, Opts]) -> - MyHost = gen_mod:get_opt_host(Host, Opts, - <<"echo.@HOST@">>), - ejabberd_router:register_route(MyHost), - {ok, #state{host = MyHost}}. - -%%-------------------------------------------------------------------- -%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | -%% {reply, Reply, State, Timeout} | -%% {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, Reply, State} | -%% {stop, Reason, State} -%% Description: Handling call messages -%%-------------------------------------------------------------------- -handle_call(stop, _From, State) -> - {stop, normal, ok, State}. - -%%-------------------------------------------------------------------- -%% Function: handle_cast(Msg, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} -%% Description: Handling cast messages -%%-------------------------------------------------------------------- -handle_cast(_Msg, State) -> {noreply, State}. - -%%-------------------------------------------------------------------- -%% Function: handle_info(Info, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} -%% Description: Handling all non call/cast messages -%%-------------------------------------------------------------------- -handle_info({route, From, To, Packet}, State) -> - Packet2 = case From#jid.user of - <<"">> -> - jlib:make_error_reply(Packet, ?ERR_BAD_REQUEST); - _ -> Packet - end, - do_client_version(disabled, To, From), - ejabberd_router:route(To, From, Packet2), - {noreply, State}; -handle_info(_Info, State) -> {noreply, State}. - -%%-------------------------------------------------------------------- -%% Function: terminate(Reason, State) -> void() -%% Description: This function is called by a gen_server when it is about to -%% terminate. It should be the opposite of Module:init/1 and do any necessary -%% cleaning up. When it returns, the gen_server terminates with Reason. -%% The return value is ignored. -%%-------------------------------------------------------------------- -terminate(_Reason, State) -> - ejabberd_router:unregister_route(State#state.host), ok. - -%%-------------------------------------------------------------------- -%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} -%% Description: Convert process state when code is changed -%%-------------------------------------------------------------------- -code_change(_OldVsn, State, _Extra) -> {ok, State}. - -%%-------------------------------------------------------------------- -%% Example of routing XMPP packets using Erlang's message passing -%%-------------------------------------------------------------------- - -%% To enable this educational example, edit the function handle_info: -%% replace the argument 'disabled' with 'enabled' in the call to the -%% function do_client_version. - -%% ejabberd provides a method to receive XMPP packets using Erlang's -%% message passing mechanism. -%% -%% The packets received by ejabberd are sent -%% to the local destination process by sending an Erlang message. -%% This means that you can receive XMPP stanzas in an Erlang process -%% using Erlang's Receive, as long as this process is registered in -%% ejabberd as the process which handles the destination JID. -%% -%% This example function is called when a client queries the echo service. -%% This function then sends a query to the client, and waits 5 seconds to -%% receive an answer. The answer will only be accepted if it was sent -%% using exactly the same JID. We add a (mostly) random resource to -%% try to guarantee that the received response matches the request sent. -%% Finally, the received response is printed in the ejabberd log file. -do_client_version(disabled, _From, _To) -> ok; -do_client_version(enabled, From, To) -> - ToS = jlib:jid_to_string(To), - Random_resource = - iolist_to_binary(integer_to_list(random:uniform(100000))), - From2 = From#jid{resource = Random_resource, - lresource = Random_resource}, - Packet = #xmlel{name = <<"iq">>, - attrs = [{<<"to">>, ToS}, {<<"type">>, <<"get">>}], - children = - [#xmlel{name = <<"query">>, - attrs = [{<<"xmlns">>, ?NS_VERSION}], - children = []}]}, - ejabberd_router:route(From2, To, Packet), - Els = receive - {route, To, From2, IQ} -> - #xmlel{name = <<"query">>, children = List} = - xml:get_subtag(IQ, <<"query">>), - List - after 5000 -> % Timeout in miliseconds: 5 seconds - [] - end, - Values = [{Name, Value} - || #xmlel{name = Name, attrs = [], - children = [{xmlcdata, Value}]} - <- Els], - Values_string1 = [io_lib:format("~n~s: ~p", [N, V]) - || {N, V} <- Values], - Values_string2 = iolist_to_binary(Values_string1), - ?INFO_MSG("Information of the client: ~s~s", - [ToS, Values_string2]). diff --git a/src/mod_fail2ban.erl b/src/mod_fail2ban.erl index 7c9eba88a..2dbd8575c 100644 --- a/src/mod_fail2ban.erl +++ b/src/mod_fail2ban.erl @@ -1,12 +1,11 @@ %%%------------------------------------------------------------------- -%%% @author Evgeny Khramtsov -%%% @copyright (C) 2014, Evgeny Khramtsov -%%% @doc -%%% -%%% @end +%%% File : mod_fail2ban.erl +%%% Author : Evgeny Khramtsov +%%% Purpose : %%% Created : 15 Aug 2014 by Evgeny Khramtsov %%% -%%% ejabberd, Copyright (C) 2014-2015 ProcessOne +%%% +%%% ejabberd, Copyright (C) 2014-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -21,26 +20,31 @@ %%% 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_fail2ban). -behaviour(gen_mod). -behaviour(gen_server). %% API --export([start_link/2, start/2, stop/1, c2s_auth_result/4, check_bl_c2s/3]). +-export([start/2, stop/1, reload/3, c2s_auth_result/3, + c2s_stream_started/2]). -%% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). +-export([init/1, handle_call/3, handle_cast/2, + handle_info/2, terminate/2, code_change/3, + mod_opt_type/1, mod_options/1, depends/2, mod_doc/0]). + +%% ejabberd command. +-export([get_commands_spec/0, unban/1]). -include_lib("stdlib/include/ms_transform.hrl"). --include("ejabberd.hrl"). +-include("ejabberd_commands.hrl"). -include("logger.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). +-include("translate.hrl"). --define(C2S_AUTH_BAN_LIFETIME, 3600). %% 1 hour --define(C2S_MAX_AUTH_FAILURES, 20). -define(CLEAN_INTERVAL, timer:minutes(10)). -record(state, {host = <<"">> :: binary()}). @@ -48,131 +52,236 @@ %%%=================================================================== %%% API %%%=================================================================== -start_link(Host, Opts) -> - Proc = gen_mod:get_module_proc(Host, ?MODULE), - gen_server:start_link({local, Proc}, ?MODULE, [Host, Opts], []). - -c2s_auth_result(false, _User, LServer, {Addr, _Port}) -> - BanLifetime = gen_mod:get_module_opt( - LServer, ?MODULE, c2s_auth_ban_lifetime, - fun(T) when is_integer(T), T > 0 -> T end, - ?C2S_AUTH_BAN_LIFETIME), - MaxFailures = gen_mod:get_module_opt( - LServer, ?MODULE, c2s_max_auth_failures, - fun(I) when is_integer(I), I > 0 -> I end, - ?C2S_MAX_AUTH_FAILURES), - UnbanTS = unban_timestamp(BanLifetime), - case ets:lookup(failed_auth, Addr) of - [{Addr, N, _, _}] -> - ets:insert(failed_auth, {Addr, N+1, UnbanTS, MaxFailures}); - [] -> - ets:insert(failed_auth, {Addr, 1, UnbanTS, MaxFailures}) +-spec c2s_auth_result(ejabberd_c2s:state(), true | {false, binary()}, binary()) + -> ejabberd_c2s:state() | {stop, ejabberd_c2s:state()}. +c2s_auth_result(#{sasl_mech := Mech} = State, {false, _}, _User) + when Mech == <<"EXTERNAL">> -> + State; +c2s_auth_result(#{ip := {Addr, _}, lserver := LServer} = State, {false, _}, _User) -> + case is_whitelisted(LServer, Addr) of + true -> + State; + false -> + BanLifetime = mod_fail2ban_opt:c2s_auth_ban_lifetime(LServer), + MaxFailures = mod_fail2ban_opt:c2s_max_auth_failures(LServer), + UnbanTS = current_time() + BanLifetime, + Attempts = case ets:lookup(failed_auth, Addr) of + [{Addr, N, _, _}] -> + ets:insert(failed_auth, + {Addr, N+1, UnbanTS, MaxFailures}), + N+1; + [] -> + ets:insert(failed_auth, + {Addr, 1, UnbanTS, MaxFailures}), + 1 + end, + if Attempts >= MaxFailures -> + log_and_disconnect(State, Attempts, UnbanTS); + true -> + State + end end; -c2s_auth_result(true, _User, _Server, _AddrPort) -> - ok. +c2s_auth_result(#{ip := {Addr, _}} = State, true, _User) -> + ets:delete(failed_auth, Addr), + State. -check_bl_c2s(_Acc, Addr, Lang) -> +-spec c2s_stream_started(ejabberd_c2s:state(), stream_start()) + -> ejabberd_c2s:state() | {stop, ejabberd_c2s:state()}. +c2s_stream_started(#{ip := {Addr, _}} = State, _) -> case ets:lookup(failed_auth, Addr) of [{Addr, N, TS, MaxFailures}] when N >= MaxFailures -> - case TS > now() of + case TS > current_time() of true -> - IP = jlib:ip_to_list(Addr), - UnbanDate = format_date( - calendar:now_to_universal_time(TS)), - LogReason = io_lib:fwrite( - "Too many (~p) failed authentications " - "from this IP address (~s). The address " - "will be unblocked at ~s UTC", - [N, IP, UnbanDate]), - ReasonT = io_lib:fwrite( - translate:translate( - Lang, - <<"Too many (~p) failed authentications " - "from this IP address (~s). The address " - "will be unblocked at ~s UTC">>), - [N, IP, UnbanDate]), - {stop, {true, LogReason, ReasonT}}; + log_and_disconnect(State, N, TS); false -> ets:delete(failed_auth, Addr), - false + State end; _ -> - false + State end. %%==================================================================== %% gen_mod callbacks %%==================================================================== start(Host, Opts) -> - catch ets:new(failed_auth, [named_table, public]), - Proc = gen_mod:get_module_proc(Host, ?MODULE), - ChildSpec = {Proc, {?MODULE, start_link, [Host, Opts]}, - transient, 1000, worker, [?MODULE]}, - supervisor:start_child(ejabberd_sup, ChildSpec). + catch ets:new(failed_auth, [named_table, public, + {heir, erlang:group_leader(), none}]), + ejabberd_commands:register_commands(Host, ?MODULE, get_commands_spec()), + gen_mod:start_child(?MODULE, Host, Opts). stop(Host) -> - Proc = gen_mod:get_module_proc(Host, ?MODULE), - supervisor:terminate_child(ejabberd_sup, Proc), - supervisor:delete_child(ejabberd_sup, Proc). + ejabberd_commands:unregister_commands(Host, ?MODULE, get_commands_spec()), + gen_mod:stop_child(?MODULE, Host). + +reload(_Host, _NewOpts, _OldOpts) -> + ok. + +depends(_Host, _Opts) -> + []. %%%=================================================================== %%% gen_server callbacks %%%=================================================================== -init([Host, _Opts]) -> +init([Host|_]) -> + process_flag(trap_exit, true), ejabberd_hooks:add(c2s_auth_result, Host, ?MODULE, c2s_auth_result, 100), - ejabberd_hooks:add(check_bl_c2s, ?MODULE, check_bl_c2s, 100), + ejabberd_hooks:add(c2s_stream_started, Host, ?MODULE, c2s_stream_started, 100), erlang:send_after(?CLEAN_INTERVAL, self(), clean), {ok, #state{host = Host}}. -handle_call(_Request, _From, State) -> - Reply = ok, - {reply, Reply, State}. +handle_call(Request, From, State) -> + ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), + {noreply, State}. -handle_cast(_Msg, State) -> - ?ERROR_MSG("got unexpected cast = ~p", [_Msg]), +handle_cast(Msg, State) -> + ?WARNING_MSG("Unexpected cast = ~p", [Msg]), {noreply, State}. handle_info(clean, State) -> - ?DEBUG("cleaning ~p ETS table", [failed_auth]), - Now = now(), + ?DEBUG("Cleaning ~p ETS table", [failed_auth]), + Now = current_time(), ets:select_delete( failed_auth, ets:fun2ms(fun({_, _, UnbanTS, _}) -> UnbanTS =< Now end)), erlang:send_after(?CLEAN_INTERVAL, self(), clean), {noreply, State}; -handle_info(_Info, State) -> - ?ERROR_MSG("got unexpected info = ~p", [_Info]), +handle_info(Info, State) -> + ?WARNING_MSG("Unexpected info = ~p", [Info]), {noreply, State}. terminate(_Reason, #state{host = Host}) -> ejabberd_hooks:delete(c2s_auth_result, Host, ?MODULE, c2s_auth_result, 100), - case is_loaded_at_other_hosts(Host) of + ejabberd_hooks:delete(c2s_stream_started, Host, ?MODULE, c2s_stream_started, 100), + case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of true -> ok; false -> - ejabberd_hooks:delete(check_bl_c2s, ?MODULE, check_bl_c2s, 100), ets:delete(failed_auth) end. code_change(_OldVsn, State, _Extra) -> {ok, State}. +%%-------------------------------------------------------------------- +%% ejabberd command callback. +%%-------------------------------------------------------------------- +-spec get_commands_spec() -> [ejabberd_commands()]. +get_commands_spec() -> + [#ejabberd_commands{name = unban_ip, tags = [accounts], + desc = "Remove banned IP addresses from the fail2ban table", + longdesc = "Accepts an IP address with a network mask. " + "Returns the number of unbanned addresses, or a negative integer if there were any error.", + module = ?MODULE, function = unban, + args = [{address, binary}], + args_example = [<<"::FFFF:127.0.0.1/128">>], + args_desc = ["IP address, optionally with network mask."], + result_example = 3, + result_desc = "Amount of unbanned entries, or negative in case of error.", + result = {unbanned, integer}}]. + +-spec unban(binary()) -> integer(). +unban(S) -> + case misc:parse_ip_mask(S) of + {ok, {Net, Mask}} -> + unban(Net, Mask); + error -> + ?WARNING_MSG("Invalid network address when trying to unban: ~p", [S]), + -1 + end. + +-spec unban(inet:ip_address(), 0..128) -> non_neg_integer(). +unban(Net, Mask) -> + ets:foldl( + fun({Addr, _, _, _}, Acc) -> + case misc:match_ip_mask(Addr, Net, Mask) of + true -> + ets:delete(failed_auth, Addr), + Acc+1; + false -> Acc + end + end, 0, failed_auth). + %%%=================================================================== %%% Internal functions %%%=================================================================== -unban_timestamp(BanLifetime) -> - {MegaSecs, MSecs, USecs} = now(), - UnbanSecs = MegaSecs * 1000000 + MSecs + BanLifetime, - {UnbanSecs div 1000000, UnbanSecs rem 1000000, USecs}. +-spec log_and_disconnect(ejabberd_c2s:state(), pos_integer(), non_neg_integer()) + -> {stop, ejabberd_c2s:state()}. +log_and_disconnect(#{ip := {Addr, _}, lang := Lang} = State, Attempts, UnbanTS) -> + IP = misc:ip_to_list(Addr), + UnbanDate = format_date( + calendar:now_to_universal_time(msec_to_now(UnbanTS))), + Format = ?T("Too many (~p) failed authentications " + "from this IP address (~s). The address " + "will be unblocked at ~s UTC"), + Args = [Attempts, IP, UnbanDate], + ?WARNING_MSG("Connection attempt from blacklisted IP ~ts: ~ts", + [IP, io_lib:fwrite(Format, Args)]), + Err = xmpp:serr_policy_violation({Format, Args}, Lang), + {stop, ejabberd_c2s:send(State, Err)}. -is_loaded_at_other_hosts(Host) -> - lists:any( - fun(VHost) when VHost == Host -> - false; - (VHost) -> - gen_mod:is_loaded(VHost, ?MODULE) - end, ?MYHOSTS). +-spec is_whitelisted(binary(), inet:ip_address()) -> boolean(). +is_whitelisted(Host, Addr) -> + Access = mod_fail2ban_opt:access(Host), + acl:match_rule(Host, Access, Addr) == allow. +-spec msec_to_now(pos_integer()) -> erlang:timestamp(). +msec_to_now(MSecs) -> + Secs = MSecs div 1000, + {Secs div 1000000, Secs rem 1000000, 0}. + +-spec format_date(calendar:datetime()) -> iolist(). format_date({{Year, Month, Day}, {Hour, Minute, Second}}) -> io_lib:format("~2..0w:~2..0w:~2..0w ~2..0w.~2..0w.~4..0w", [Hour, Minute, Second, Day, Month, Year]). + +current_time() -> + erlang:system_time(millisecond). + +mod_opt_type(access) -> + econf:acl(); +mod_opt_type(c2s_auth_ban_lifetime) -> + econf:timeout(second); +mod_opt_type(c2s_max_auth_failures) -> + econf:pos_int(). + +mod_options(_Host) -> + [{access, none}, + {c2s_auth_ban_lifetime, timer:hours(1)}, + {c2s_max_auth_failures, 20}]. + +mod_doc() -> + #{desc => + [?T("The module bans IPs that show the malicious signs. " + "Currently only C2S authentication failures are detected."), "", + ?T("Unlike the standalone program, 'mod_fail2ban' clears the " + "record of authentication failures after some time since the " + "first failure or on a successful authentication. " + "It also does not simply block network traffic, but " + "provides the client with a descriptive error message."), "", + ?T("WARNING: You should not use this module behind a proxy or load " + "balancer. ejabberd will see the failures as coming from the " + "load balancer and, when the threshold of auth failures is " + "reached, will reject all connections coming from the load " + "balancer. You can lock all your user base out of ejabberd " + "when using this module behind a proxy.")], + opts => + [{access, + #{value => ?T("AccessName"), + desc => + ?T("Specify an access rule for whitelisting IP " + "addresses or networks. If the rule returns 'allow' " + "for a given IP address, that address will never be " + "banned. The 'AccessName' should be of type 'ip'. " + "The default value is 'none'.")}}, + {c2s_auth_ban_lifetime, + #{value => "timeout()", + desc => + ?T("The lifetime of the IP ban caused by too many " + "C2S authentication failures. The default value is " + "'1' hour.")}}, + {c2s_max_auth_failures, + #{value => ?T("Number"), + desc => + ?T("The number of C2S authentication failures to " + "trigger the IP ban. The default value is '20'.")}}]}. diff --git a/src/mod_fail2ban_opt.erl b/src/mod_fail2ban_opt.erl new file mode 100644 index 000000000..15abbedd0 --- /dev/null +++ b/src/mod_fail2ban_opt.erl @@ -0,0 +1,27 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_fail2ban_opt). + +-export([access/1]). +-export([c2s_auth_ban_lifetime/1]). +-export([c2s_max_auth_failures/1]). + +-spec access(gen_mod:opts() | global | binary()) -> 'none' | acl:acl(). +access(Opts) when is_map(Opts) -> + gen_mod:get_opt(access, Opts); +access(Host) -> + gen_mod:get_module_opt(Host, mod_fail2ban, access). + +-spec c2s_auth_ban_lifetime(gen_mod:opts() | global | binary()) -> pos_integer(). +c2s_auth_ban_lifetime(Opts) when is_map(Opts) -> + gen_mod:get_opt(c2s_auth_ban_lifetime, Opts); +c2s_auth_ban_lifetime(Host) -> + gen_mod:get_module_opt(Host, mod_fail2ban, c2s_auth_ban_lifetime). + +-spec c2s_max_auth_failures(gen_mod:opts() | global | binary()) -> pos_integer(). +c2s_max_auth_failures(Opts) when is_map(Opts) -> + gen_mod:get_opt(c2s_max_auth_failures, Opts); +c2s_max_auth_failures(Host) -> + gen_mod:get_module_opt(Host, mod_fail2ban, c2s_max_auth_failures). + diff --git a/src/mod_host_meta.erl b/src/mod_host_meta.erl new file mode 100644 index 000000000..ae7d7d697 --- /dev/null +++ b/src/mod_host_meta.erl @@ -0,0 +1,257 @@ +%%%------------------------------------------------------------------- +%%% File : mod_host_meta.erl +%%% Author : Badlop +%%% Purpose : Serve host-meta files as described in XEP-0156 +%%% Created : 25 March 2022 by Badlop +%%% +%%% +%%% ejabberd, Copyright (C) 2022 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_host_meta). + +-author('badlop@process-one.net'). + +-protocol({xep, 156, '1.4.0', '22.05', "complete", ""}). + +-behaviour(gen_mod). + +-export([start/2, stop/1, reload/3, process/2, + mod_opt_type/1, mod_options/1, depends/2]). +-export([mod_doc/0]). +-export([get_url/4, get_auto_url/2]). + +-include("logger.hrl"). + +-include_lib("xmpp/include/xmpp.hrl"). + +-include("ejabberd_http.hrl"). + +-include("ejabberd_web_admin.hrl"). + +-include("translate.hrl"). + +%%%---------------------------------------------------------------------- +%%% gen_mod callbacks +%%%---------------------------------------------------------------------- + +start(_Host, _Opts) -> + report_hostmeta_listener(), + ok. + +stop(_Host) -> + ok. + +reload(_Host, _NewOpts, _OldOpts) -> + report_hostmeta_listener(), + ok. + +depends(_Host, _Opts) -> + [{mod_bosh, soft}]. + +%%%---------------------------------------------------------------------- +%%% HTTP handlers +%%%---------------------------------------------------------------------- + +process([], #request{method = 'GET', host = Host, path = Path}) -> + case lists:last(Path) of + <<"host-meta">> -> + file_xml(Host); + <<"host-meta.json">> -> + file_json(Host) + end; +process(_Path, _Request) -> + {404, [], "Not Found"}. + +%%%---------------------------------------------------------------------- +%%% Internal +%%%---------------------------------------------------------------------- + +%% When set to 'auto', it only takes the first valid listener options it finds + +file_xml(Host) -> + BoshList = case get_url(?MODULE, bosh, true, Host) of + undefined -> []; + BoshUrl -> + [?XA(<<"Link">>, + [{<<"rel">>, <<"urn:xmpp:alt-connections:xbosh">>}, + {<<"href">>, BoshUrl}] + )] + end, + WsList = case get_url(?MODULE, websocket, true, Host) of + undefined -> []; + WsUrl -> + [?XA(<<"Link">>, + [{<<"rel">>, <<"urn:xmpp:alt-connections:websocket">>}, + {<<"href">>, WsUrl}] + )] + end, + {200, [html, + {<<"Content-Type">>, <<"application/xrd+xml">>}, + {<<"Access-Control-Allow-Origin">>, <<"*">>}], + [<<"\n">>, + fxml:element_to_binary( + ?XAE(<<"XRD">>, + [{<<"xmlns">>,<<"http://docs.oasis-open.org/ns/xri/xrd-1.0">>}], + BoshList ++ WsList) + )]}. + +file_json(Host) -> + BoshList = case get_url(?MODULE, bosh, true, Host) of + undefined -> []; + BoshUrl -> [#{rel => <<"urn:xmpp:alt-connections:xbosh">>, + href => BoshUrl}] + end, + WsList = case get_url(?MODULE, websocket, true, Host) of + undefined -> []; + WsUrl -> [#{rel => <<"urn:xmpp:alt-connections:websocket">>, + href => WsUrl}] + end, + {200, [html, + {<<"Content-Type">>, <<"application/json">>}, + {<<"Access-Control-Allow-Origin">>, <<"*">>}], + [misc:json_encode(#{links => BoshList ++ WsList})]}. + +get_url(M, bosh, Tls, Host) -> + get_url(M, Tls, Host, bosh_service_url, mod_bosh); +get_url(M, websocket, Tls, Host) -> + get_url(M, Tls, Host, websocket_url, ejabberd_http_ws). + +get_url(M, Tls, Host, Option, Module) -> + case get_url_preliminar(M, Tls, Host, Option, Module) of + undefined -> undefined; + Url -> misc:expand_keyword(<<"@HOST@">>, Url, Host) + end. + +get_url_preliminar(M, Tls, Host, Option, Module) -> + case gen_mod:get_module_opt(Host, M, Option) of + undefined -> undefined; + auto -> get_auto_url(Tls, Module); + <<"auto">> -> get_auto_url(Tls, Module); + U when is_binary(U) -> U + end. + +get_auto_url(Tls, Module) -> + case find_handler_port_path(Tls, Module) of + [] -> undefined; + [{ThisTls, Port, Path} | _] -> + Protocol = case {ThisTls, Module} of + {false, ejabberd_http_ws} -> <<"ws">>; + {true, ejabberd_http_ws} -> <<"wss">>; + {false, _} -> <<"http">>; + {true, _} -> <<"https">> + end, + <>))/binary>> + end. + +find_handler_port_path(Tls, Module) -> + lists:filtermap( + fun({{Port, _, _}, + ejabberd_http, + #{tls := ThisTls, request_handlers := Handlers}}) + when is_integer(Port) and ((Tls == any) or (Tls == ThisTls)) -> + case lists:keyfind(Module, 2, Handlers) of + false -> false; + {Path, Module} -> {true, {ThisTls, Port, Path}} + end; + (_) -> false + end, ets:tab2list(ejabberd_listener)). + +report_hostmeta_listener() -> + case {find_handler_port_path(false, ?MODULE), + find_handler_port_path(true, ?MODULE)} of + {[], []} -> + ?CRITICAL_MSG("It seems you enabled ~p in 'modules' but forgot to " + "add it as a request_handler in an ejabberd_http " + "listener.", [?MODULE]); + {[_|_], _} -> + ?WARNING_MSG("Apparently ~p is enabled in a request_handler in a " + "non-encrypted ejabberd_http listener. This is " + "disallowed by XEP-0156. Please enable 'tls' in that " + "listener, or setup a proxy encryption mechanism.", + [?MODULE]); + {[], [_|_]} -> + ok + end. + +%%%---------------------------------------------------------------------- +%%% Options and Doc +%%%---------------------------------------------------------------------- + +mod_opt_type(bosh_service_url) -> + econf:either(undefined, econf:binary()); +mod_opt_type(websocket_url) -> + econf:either(undefined, econf:binary()). + +mod_options(_) -> + [{bosh_service_url, <<"auto">>}, + {websocket_url, <<"auto">>}]. + +mod_doc() -> + #{desc => + [?T("This module serves small 'host-meta' files as described in " + "https://xmpp.org/extensions/xep-0156.html[XEP-0156: Discovering " + "Alternative XMPP Connection Methods]."), "", + ?T("To use this module, in addition to adding it to the 'modules' " + "section, you must also enable it in 'listen' -> 'ejabberd_http' -> " + "_`listen-options.md#request_handlers|request_handlers`_."), "", + ?T("Notice it only works if _`listen.md#ejabberd_http|ejabberd_http`_ " + "has _`listen-options.md#tls|tls`_ enabled.")], + note => "added in 22.05", + example => + ["listen:", + " -", + " port: 443", + " module: ejabberd_http", + " tls: true", + " request_handlers:", + " /bosh: mod_bosh", + " /ws: ejabberd_http_ws", + " /.well-known/host-meta: mod_host_meta", + " /.well-known/host-meta.json: mod_host_meta", + "", + "modules:", + " mod_bosh: {}", + " mod_host_meta:", + " bosh_service_url: \"https://@HOST@:5443/bosh\"", + " websocket_url: \"wss://@HOST@:5443/ws\""], + + opts => + [{websocket_url, + #{value => "undefined | auto | WebSocketURL", + desc => + ?T("WebSocket URL to announce. " + "The keyword '@HOST@' is replaced with the real virtual " + "host name. " + "If set to 'auto', it will build the URL of the first " + "configured WebSocket request handler. " + "The default value is 'auto'.")}}, + {bosh_service_url, + #{value => "undefined | auto | BoshURL", + desc => + ?T("BOSH service URL to announce. " + "The keyword '@HOST@' is replaced with the real " + "virtual host name. " + "If set to 'auto', it will build the URL of the first " + "configured BOSH request handler. " + "The default value is 'auto'.")}}] + }. diff --git a/src/mod_host_meta_opt.erl b/src/mod_host_meta_opt.erl new file mode 100644 index 000000000..965e95cf8 --- /dev/null +++ b/src/mod_host_meta_opt.erl @@ -0,0 +1,20 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_host_meta_opt). + +-export([bosh_service_url/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_host_meta, bosh_service_url). + +-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_host_meta, websocket_url). + diff --git a/src/mod_http_api.erl b/src/mod_http_api.erl new file mode 100644 index 000000000..087be0e72 --- /dev/null +++ b/src/mod_http_api.erl @@ -0,0 +1,622 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_http_api.erl +%%% Author : Christophe romain +%%% Purpose : Implements REST API for ejabberd using JSON data +%%% Created : 15 Sep 2014 by Christophe Romain +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_http_api). + +-author('cromain@process-one.net'). + +-behaviour(gen_mod). + +-export([start/2, stop/1, reload/3, process/2, depends/2, + format_arg/2, handle/4, + 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"). + +-define(DEFAULT_API_VERSION, 1000000). + +-define(CT_PLAIN, + {<<"Content-Type">>, <<"text/plain">>}). + +-define(CT_XML, + {<<"Content-Type">>, <<"text/xml; charset=utf-8">>}). + +-define(CT_JSON, + {<<"Content-Type">>, <<"application/json">>}). + +-define(AC_ALLOW_ORIGIN, + {<<"Access-Control-Allow-Origin">>, <<"*">>}). + +-define(AC_ALLOW_METHODS, + {<<"Access-Control-Allow-Methods">>, + <<"GET, POST, OPTIONS">>}). + +-define(AC_ALLOW_HEADERS, + {<<"Access-Control-Allow-Headers">>, + <<"Content-Type, Authorization, X-Admin">>}). + +-define(AC_MAX_AGE, + {<<"Access-Control-Max-Age">>, <<"86400">>}). + +-define(OPTIONS_HEADER, + [?CT_PLAIN, ?AC_ALLOW_ORIGIN, ?AC_ALLOW_METHODS, + ?AC_ALLOW_HEADERS, ?AC_MAX_AGE]). + +-define(HEADER(CType), + [CType, ?AC_ALLOW_ORIGIN, ?AC_ALLOW_HEADERS]). + +%% ------------------- +%% Module control +%% ------------------- + +start(_Host, _Opts) -> + ok. + +stop(_Host) -> + ok. + +reload(_Host, _NewOpts, _OldOpts) -> + ok. + +depends(_Host, _Opts) -> + []. + +%% ---------- +%% basic auth +%% ---------- + +extract_auth(#request{auth = HTTPAuth, ip = {IP, _}, opts = Opts}) -> + Info = case HTTPAuth of + {SJID, Pass} -> + try jid:decode(SJID) of + #jid{luser = User, lserver = Server} -> + case ejabberd_auth:check_password(User, <<"">>, Server, Pass) of + true -> + #{usr => {User, Server, <<"">>}, caller_server => Server}; + false -> + {error, invalid_auth} + end + catch _:{bad_jid, _} -> + {error, invalid_auth} + end; + {oauth, Token, _} -> + case ejabberd_oauth:check_token(Token) of + {ok, {U, S}, Scope} -> + #{usr => {U, S, <<"">>}, oauth_scope => Scope, caller_server => S}; + {false, Reason} -> + {error, Reason} + end; + invalid -> + {error, invalid_auth}; + _ -> + #{} + end, + case Info of + Map when is_map(Map) -> + Tag = proplists:get_value(tag, Opts, <<>>), + Map#{caller_module => ?MODULE, ip => IP, tag => Tag}; + _ -> + ?DEBUG("Invalid auth data: ~p", [Info]), + Info + end. + +%% ------------------ +%% command processing +%% ------------------ + +%process(Call, Request) -> +% ?DEBUG("~p~n~p", [Call, Request]), ok; +process(_, #request{method = 'POST', data = <<>>}) -> + ?DEBUG("Bad Request: no data", []), + badrequest_response(<<"Missing POST data">>); +process([Call | _], #request{method = 'POST', data = Data, ip = IPPort} = Req) -> + Version = get_api_version(Req), + try + Args = extract_args(Data), + log(Call, Args, IPPort), + perform_call(Call, Args, Req, Version) + catch + %% TODO We need to refactor to remove redundant error return formatting + throw:{error, unknown_command} -> + json_format({404, 44, <<"Command not found.">>}); + _:{error,{_,invalid_json}} = Err -> + ?DEBUG("Bad Request: ~p", [Err]), + badrequest_response(<<"Invalid JSON input">>); + _Class:Error:StackTrace -> + ?DEBUG("Bad Request: ~p ~p", [Error, StackTrace]), + badrequest_response() + end; +process([Call | _], #request{method = 'GET', q = Data, ip = {IP, _}} = Req) -> + Version = get_api_version(Req), + try + Args = case Data of + [{nokey, <<>>}] -> []; + _ -> Data + end, + log(Call, Args, IP), + perform_call(Call, Args, Req, Version) + catch + %% TODO We need to refactor to remove redundant error return formatting + throw:{error, unknown_command} -> + json_format({404, 44, <<"Command not found.">>}); + _:Error:StackTrace -> + ?DEBUG("Bad Request: ~p ~p", [Error, StackTrace]), + badrequest_response() + end; +process([_Call], #request{method = 'OPTIONS', data = <<>>}) -> + {200, ?OPTIONS_HEADER, []}; +process(_, #request{method = 'OPTIONS'}) -> + {400, ?OPTIONS_HEADER, []}; +process(_Path, Request) -> + ?DEBUG("Bad Request: no handler ~p", [Request]), + json_error(400, 40, <<"Missing command name.">>). + +perform_call(Command, Args, Req, Version) -> + case catch binary_to_existing_atom(Command, utf8) of + Call when is_atom(Call) -> + case extract_auth(Req) of + {error, expired} -> invalid_token_response(); + {error, not_found} -> invalid_token_response(); + {error, invalid_auth} -> unauthorized_response(); + Auth when is_map(Auth) -> + Result = handle(Call, Auth, Args, Version), + json_format(Result) + end; + _ -> + json_error(404, 40, <<"Endpoint not found.">>) + end. + +%% Be tolerant to make API more easily usable from command-line pipe. +extract_args(<<"\n">>) -> []; +extract_args(Data) -> + Maps = misc:json_decode(Data), + maps:to_list(Maps). + +% get API version N from last "vN" element in URL path +get_api_version(#request{path = Path, host = Host}) -> + get_api_version(lists:reverse(Path), Host). + +get_api_version([<<"v", String/binary>> | Tail], Host) -> + case catch binary_to_integer(String) of + N when is_integer(N) -> + N; + _ -> + get_api_version(Tail, Host) + end; +get_api_version([_Head | Tail], Host) -> + get_api_version(Tail, Host); +get_api_version([], Host) -> + try mod_http_api_opt:default_version(Host) + catch error:{module_not_loaded, ?MODULE, Host} -> + ?WARNING_MSG("Using module ~p for host ~s, but it isn't configured " + "in the configuration file", [?MODULE, Host]), + ?DEFAULT_API_VERSION + end. + +%% ---------------- +%% command handlers +%% ---------------- + +%% TODO Check accept types of request before decided format of reply. + +% generic ejabberd command handler +handle(Call, Auth, Args, Version) when is_atom(Call), is_list(Args) -> + Args2 = [{misc:binary_to_atom(Key), Value} || {Key, Value} <- Args], + try handle2(Call, Auth, Args2, Version) + catch throw:not_found -> + {404, <<"not_found">>}; + throw:{not_found, Why} when is_atom(Why) -> + {404, misc:atom_to_binary(Why)}; + throw:{not_found, Msg} -> + {404, iolist_to_binary(Msg)}; + throw:not_allowed -> + {401, <<"not_allowed">>}; + throw:{not_allowed, Why} when is_atom(Why) -> + {401, misc:atom_to_binary(Why)}; + throw:{not_allowed, Msg} -> + {401, iolist_to_binary(Msg)}; + throw:{error, account_unprivileged} -> + {403, 31, <<"Command need to be run with admin privilege.">>}; + throw:{error, access_rules_unauthorized} -> + {403, 32, <<"AccessRules: Account does not have the right to perform the operation.">>}; + throw:{invalid_parameter, Msg} -> + {400, iolist_to_binary(Msg)}; + throw:{error, Why} when is_atom(Why) -> + {400, misc:atom_to_binary(Why)}; + throw:{error, Msg} -> + {400, iolist_to_binary(Msg)}; + throw:Error when is_atom(Error) -> + {400, misc:atom_to_binary(Error)}; + throw:Msg when is_list(Msg); is_binary(Msg) -> + {400, iolist_to_binary(Msg)}; + Class:Error:StackTrace -> + ?ERROR_MSG("REST API Error: " + "~ts(~p) -> ~p:~p ~p", + [Call, + hide_sensitive_args(Args), + Class, + Error, + StackTrace]), + {500, <<"internal_error">>} + end. + +handle2(Call, Auth, Args, Version) when is_atom(Call), is_list(Args) -> + {ArgsF, ArgsR, _ResultF} = ejabberd_commands:get_command_format(Call, Auth, Version), + ArgsFormatted = format_args(Call, rename_old_args(Args, ArgsR), ArgsF), + case ejabberd_commands:execute_command2(Call, ArgsFormatted, Auth, Version) of + {error, Error} -> + throw(Error); + Res -> + format_command_result(Call, Auth, Res, Version) + end. + +rename_old_args(Args, []) -> + Args; +rename_old_args(Args, [{OldName, NewName} | ArgsR]) -> + Args2 = case lists:keytake(OldName, 1, Args) of + {value, {OldName, Value}, ArgsTail} -> + [{NewName, Value} | ArgsTail]; + false -> + Args + end, + rename_old_args(Args2, ArgsR). + +get_elem_delete(Call, A, L, F) -> + case proplists:get_all_values(A, L) of + [Value] -> {Value, proplists:delete(A, L)}; + [_, _ | _] -> + ?INFO_MSG("Command ~ts call rejected, it has duplicate attribute ~w", + [Call, A]), + throw({invalid_parameter, + io_lib:format("Request have duplicate argument: ~w", [A])}); + [] -> + case F of + {list, _} -> + {[], L}; + _ -> + ?INFO_MSG("Command ~ts call rejected, missing attribute ~w", + [Call, A]), + throw({invalid_parameter, + io_lib:format("Request have missing argument: ~w", [A])}) + end + end. + +format_args(Call, Args, ArgsFormat) -> + {ArgsRemaining, R} = lists:foldl(fun ({ArgName, + ArgFormat}, + {Args1, Res}) -> + {ArgValue, Args2} = + get_elem_delete(Call, ArgName, + Args1, ArgFormat), + Formatted = format_arg(ArgValue, + ArgFormat), + {Args2, Res ++ [Formatted]} + end, + {Args, []}, ArgsFormat), + case ArgsRemaining of + [] -> R; + L when is_list(L) -> + ExtraArgs = [N || {N, _} <- L], + ?INFO_MSG("Command ~ts call rejected, it has unknown arguments ~w", + [Call, ExtraArgs]), + throw({invalid_parameter, + io_lib:format("Request have unknown arguments: ~w", [ExtraArgs])}) + end. + +format_arg({Elements}, + {list, {_ElementDefName, {tuple, [{_Tuple1N, Tuple1S}, {_Tuple2N, Tuple2S}]} = Tuple}}) + when is_list(Elements) andalso + (Tuple1S == binary orelse Tuple1S == string) -> + lists:map(fun({F1, F2}) -> + {format_arg(F1, Tuple1S), format_arg(F2, Tuple2S)}; + ({Val}) when is_list(Val) -> + format_arg({Val}, Tuple) + end, Elements); +format_arg(Map, + {list, {_ElementDefName, {tuple, [{_Tuple1N, Tuple1S}, {_Tuple2N, Tuple2S}]}}}) + when is_map(Map) andalso + (Tuple1S == binary orelse Tuple1S == string) -> + maps:fold( + fun(K, V, Acc) -> + [{format_arg(K, Tuple1S), format_arg(V, Tuple2S)} | Acc] + end, [], Map); +format_arg(Elements, + {list, {_ElementDefName, {list, _} = ElementDefFormat}}) + when is_list(Elements) -> + [{format_arg(Element, ElementDefFormat)} + || Element <- Elements]; + +%% Covered by command_test_list and command_test_list_tuple +format_arg(Element, {list, Def}) + when not is_list(Element) -> + format_arg([Element], {list, Def}); +format_arg(Elements, + {list, {_ElementDefName, ElementDefFormat}}) + when is_list(Elements) -> + [format_arg(Element, ElementDefFormat) + || Element <- Elements]; + +format_arg({[{Name, Value}]}, + {tuple, [{_Tuple1N, Tuple1S}, {_Tuple2N, Tuple2S}]}) + when Tuple1S == binary; + Tuple1S == string -> + {format_arg(Name, Tuple1S), format_arg(Value, Tuple2S)}; + +%% Covered by command_test_tuple and command_test_list_tuple +format_arg(Elements, + {tuple, ElementsDef}) + when is_map(Elements) -> + list_to_tuple([element(2, maps:find(atom_to_binary(Name, latin1), Elements)) + || {Name, _Format} <- ElementsDef]); + +format_arg({Elements}, + {tuple, ElementsDef}) + when is_list(Elements) -> + F = lists:map(fun({TElName, TElDef}) -> + case lists:keyfind(atom_to_binary(TElName, latin1), 1, Elements) of + {_, Value} -> + format_arg(Value, TElDef); + _ when TElDef == binary; TElDef == string -> + <<"">>; + _ -> + ?ERROR_MSG("Missing field ~p in tuple ~p", [TElName, Elements]), + throw({invalid_parameter, + io_lib:format("Missing field ~w in tuple ~w", [TElName, Elements])}) + end + end, ElementsDef), + list_to_tuple(F); + +format_arg(Elements, {list, ElementsDef}) + when is_list(Elements) and is_atom(ElementsDef) -> + [format_arg(Element, ElementsDef) + || Element <- Elements]; + +format_arg(Arg, integer) when is_integer(Arg) -> Arg; +format_arg(Arg, integer) when is_binary(Arg) -> binary_to_integer(Arg); +format_arg(Arg, binary) when is_list(Arg) -> process_unicode_codepoints(Arg); +format_arg(Arg, binary) when is_binary(Arg) -> Arg; +format_arg(Arg, string) when is_list(Arg) -> Arg; +format_arg(Arg, string) when is_binary(Arg) -> binary_to_list(Arg); +format_arg(undefined, binary) -> <<>>; +format_arg(undefined, string) -> ""; +format_arg(Arg, Format) -> + ?ERROR_MSG("Don't know how to format Arg ~p for format ~p", [Arg, Format]), + throw({invalid_parameter, + io_lib:format("Arg ~w is not in format ~w", + [Arg, Format])}). + +process_unicode_codepoints(Str) -> + iolist_to_binary(lists:map(fun(X) when X > 255 -> unicode:characters_to_binary([X]); + (Y) -> Y + end, Str)). + +%% ---------------- +%% internal helpers +%% ---------------- + +format_command_result(Cmd, Auth, Result, Version) -> + {_, _, ResultFormat} = ejabberd_commands:get_command_format(Cmd, Auth, Version), + case {ResultFormat, Result} of + {{_, rescode}, V} when V == true; V == ok -> + {200, 0}; + {{_, rescode}, _} -> + {200, 1}; + {_, {error, ErrorAtom, Code, Msg}} -> + format_error_result(ErrorAtom, Code, Msg); + {{_, restuple}, {V, Text}} when V == true; V == ok -> + {200, iolist_to_binary(Text)}; + {{_, restuple}, {ErrorAtom, Msg}} -> + format_error_result(ErrorAtom, 0, Msg); + {{_, {list, _}}, _V} -> + {_, L} = format_result(Result, ResultFormat), + {200, L}; + {{_, {tuple, _}}, _V} -> + {_, T} = format_result(Result, ResultFormat), + {200, T}; + _ -> + OtherResult1 = format_result(Result, ResultFormat), + OtherResult2 = case Version of + 0 -> + {[OtherResult1]}; + _ -> + {_, Other3} = OtherResult1, + Other3 + end, + {200, OtherResult2} + end. + +format_result(Atom, {Name, atom}) -> + {misc:atom_to_binary(Name), misc:atom_to_binary(Atom)}; + +format_result(Int, {Name, integer}) -> + {misc:atom_to_binary(Name), Int}; + +format_result([String | _] = StringList, {Name, string}) when is_list(String) -> + Binarized = iolist_to_binary(string:join(StringList, "\n")), + {misc:atom_to_binary(Name), Binarized}; + +format_result(String, {Name, string}) -> + {misc:atom_to_binary(Name), iolist_to_binary(String)}; + +format_result(Binary, {Name, binary}) -> + {misc:atom_to_binary(Name), Binary}; + +format_result(Code, {Name, rescode}) -> + {misc:atom_to_binary(Name), Code == true orelse Code == ok}; + +format_result({Code, Text}, {Name, restuple}) -> + {misc:atom_to_binary(Name), + {[{<<"res">>, Code == true orelse Code == ok}, + {<<"text">>, iolist_to_binary(Text)}]}}; + +format_result(Code, {Name, restuple}) -> + {misc:atom_to_binary(Name), + {[{<<"res">>, Code == true orelse Code == ok}, + {<<"text">>, <<"">>}]}}; + +format_result(Els1, {Name, {list, {_, {tuple, [{_, atom}, _]}} = Fmt}}) -> + Els = lists:keysort(1, Els1), + {misc:atom_to_binary(Name), {[format_result(El, Fmt) || El <- Els]}}; + +format_result(Els1, {Name, {list, {_, {tuple, [{name, string}, {value, _}]}} = Fmt}}) -> + Els = lists:keysort(1, Els1), + {misc:atom_to_binary(Name), {[format_result(El, Fmt) || El <- Els]}}; + +%% Covered by command_test_list and command_test_list_tuple +format_result(Els1, {Name, {list, Def}}) -> + Els = lists:sort(Els1), + {misc:atom_to_binary(Name), [element(2, format_result(El, Def)) || El <- Els]}; + +format_result(Tuple, {_Name, {tuple, [{_, atom}, ValFmt]}}) -> + {Name2, Val} = Tuple, + {_, Val2} = format_result(Val, ValFmt), + {misc:atom_to_binary(Name2), Val2}; + +format_result(Tuple, {_Name, {tuple, [{name, string}, {value, _} = ValFmt]}}) -> + {Name2, Val} = Tuple, + {_, Val2} = format_result(Val, ValFmt), + {iolist_to_binary(Name2), Val2}; + +%% Covered by command_test_tuple and command_test_list_tuple +format_result(Tuple, {Name, {tuple, Def}}) -> + Els = lists:zip(tuple_to_list(Tuple), Def), + Els2 = [format_result(El, ElDef) || {El, ElDef} <- Els], + {misc:atom_to_binary(Name), maps:from_list(Els2)}; + +format_result(404, {_Name, _}) -> + "not_found". + + +format_error_result(conflict, Code, Msg) -> + {409, Code, iolist_to_binary(Msg)}; +format_error_result(not_exists, Code, Msg) -> + {404, Code, iolist_to_binary(Msg)}; +format_error_result(_ErrorAtom, Code, Msg) -> + {500, Code, iolist_to_binary(Msg)}. + +unauthorized_response() -> + json_error(401, 10, <<"You are not authorized to call this command.">>). + +invalid_token_response() -> + json_error(401, 10, <<"Oauth Token is invalid or expired.">>). + +%% outofscope_response() -> +%% json_error(401, 11, <<"Token does not grant usage to command required scope.">>). + +badrequest_response() -> + badrequest_response(<<"400 Bad Request">>). +badrequest_response(Body) -> + json_response(400, misc:json_encode(Body)). + +json_format({Code, Result}) -> + json_response(Code, misc:json_encode(Result)); +json_format({HTMLCode, JSONErrorCode, Message}) -> + json_error(HTMLCode, JSONErrorCode, Message). + +json_response(Code, Body) when is_integer(Code) -> + {Code, ?HEADER(?CT_JSON), Body}. + +%% HTTPCode, JSONCode = integers +%% message is binary +json_error(HTTPCode, JSONCode, Message) -> + {HTTPCode, ?HEADER(?CT_JSON), + misc:json_encode(#{<<"status">> => <<"error">>, + <<"code">> => JSONCode, + <<"message">> => Message}) + }. + +log(Call, Args, {Addr, Port}) -> + AddrS = misc:ip_to_list({Addr, Port}), + ?INFO_MSG("API call ~ts ~p from ~ts:~p", [Call, hide_sensitive_args(Args), AddrS, Port]); +log(Call, Args, IP) -> + ?INFO_MSG("API call ~ts ~p (~p)", [Call, hide_sensitive_args(Args), IP]). + +hide_sensitive_args(Args=[_H|_T]) -> + lists:map(fun({<<"password">>, Password}) -> {<<"password">>, ejabberd_config:may_hide_data(Password)}; + ({<<"newpass">>,NewPassword}) -> {<<"newpass">>, ejabberd_config:may_hide_data(NewPassword)}; + (E) -> E end, + Args); +hide_sensitive_args(NonListArgs) -> + NonListArgs. + +mod_opt_type(default_version) -> + econf:either( + econf:int(0, 3), + econf:and_then( + econf:binary(), + fun(Binary) -> + case binary_to_list(Binary) of + F when F >= "24.06" -> + 2; + F when (F > "23.10") and (F < "24.06") -> + 1; + F when F =< "23.10" -> + 0 + end + end)). + +-spec mod_options(binary()) -> [{default_version, integer()}]. + +mod_options(_) -> + [{default_version, ?DEFAULT_API_VERSION}]. + +mod_doc() -> + #{desc => + [?T("This module provides a ReST interface to call " + "_`../../developer/ejabberd-api/index.md|ejabberd API`_ " + "commands using JSON data."), "", + ?T("To use this module, in addition to adding it to the 'modules' " + "section, you must also enable it in 'listen' -> 'ejabberd_http' -> " + "_`listen-options.md#request_handlers|request_handlers`_."), "", + ?T("To use a specific API version N, when defining the URL path " + "in the request_handlers, add a vN. " + "For example: '/api/v2: mod_http_api'."), "", + ?T("To run a command, send a POST request to the corresponding " + "URL: 'http://localhost:5280/api/COMMAND-NAME'")], + opts => + [{default_version, + #{value => "integer() | string()", + note => "added in 24.12", + desc => + ?T("What API version to use when none is specified in the URL path. " + "If setting an ejabberd version, it will use the latest API " + "version that was available in that ejabberd version. " + "For example, setting '\"24.06\"' in this option implies '2'. " + "The default value is the latest version.")}}], + example => + ["listen:", + " -", + " port: 5280", + " module: ejabberd_http", + " request_handlers:", + " /api: mod_http_api", + "", + "modules:", + " mod_http_api:", + " default_version: 2"]}. diff --git a/src/mod_http_api_opt.erl b/src/mod_http_api_opt.erl new file mode 100644 index 000000000..326c53e02 --- /dev/null +++ b/src/mod_http_api_opt.erl @@ -0,0 +1,13 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_http_api_opt). + +-export([default_version/1]). + +-spec default_version(gen_mod:opts() | global | binary()) -> integer(). +default_version(Opts) when is_map(Opts) -> + gen_mod:get_opt(default_version, Opts); +default_version(Host) -> + gen_mod:get_module_opt(Host, mod_http_api, default_version). + diff --git a/src/mod_http_bind.erl b/src/mod_http_bind.erl deleted file mode 100644 index 773ef241f..000000000 --- a/src/mod_http_bind.erl +++ /dev/null @@ -1,144 +0,0 @@ -%%%---------------------------------------------------------------------- -%%% File : mod_http_bind.erl -%%% Author : Stefan Strigler -%%% Purpose : Implementation of XMPP over BOSH (XEP-0206) -%%% Created : Tue Feb 20 13:15:52 CET 2007 -%%% -%%% -%%% ejabberd, Copyright (C) 2002-2015 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. -%%% -%%%---------------------------------------------------------------------- - -%%%---------------------------------------------------------------------- -%%% This module acts as a bridge to ejabberd_http_bind which implements -%%% the real stuff, this is to handle the new pluggable architecture for -%%% extending ejabberd's http service. -%%%---------------------------------------------------------------------- - --module(mod_http_bind). - --author('steve@zeank.in-berlin.de'). - -%%-define(ejabberd_debug, true). - --behaviour(gen_mod). - --export([start/2, stop/1, process/2]). - --include("ejabberd.hrl"). --include("logger.hrl"). - --include("jlib.hrl"). - --include("ejabberd_http.hrl"). - --include("http_bind.hrl"). - --define(PROCNAME_MHB, ejabberd_mod_http_bind). - -%% Duplicated from ejabberd_http_bind. -%% TODO: move to hrl file. --record(http_bind, - {id, pid, to, hold, wait, process_delay, version}). - -%%%---------------------------------------------------------------------- -%%% API -%%%---------------------------------------------------------------------- - -process([], #request{method = 'POST', data = <<>>}) -> - ?DEBUG("Bad Request: no data", []), - {400, ?HEADER, - #xmlel{name = <<"h1">>, children = [{xmlcdata, <<"400 Bad Request">>}]}}; -process([], - #request{method = 'POST', data = Data, ip = IP}) -> - ?DEBUG("Incoming data: ~s", [Data]), - ejabberd_http_bind:process_request(Data, IP); -process([], #request{method = 'GET', data = <<>>}) -> - {200, ?HEADER, get_human_html_xmlel()}; -process([], #request{method = 'OPTIONS', data = <<>>}) -> - {200, ?OPTIONS_HEADER, <<>>}; -process([], #request{method = 'HEAD'}) -> - {200, ?HEADER, <<>>}; -process(_Path, _Request) -> - ?DEBUG("Bad Request: ~p", [_Request]), - {400, ?HEADER, - #xmlel{name = <<"h1">>, children = [{xmlcdata, <<"400 Bad Request">>}]}}. - -get_human_html_xmlel() -> - Heading = <<"ejabberd ", - (iolist_to_binary(atom_to_list(?MODULE)))/binary>>, - #xmlel{name = <<"html">>, - attrs = - [{<<"xmlns">>, <<"http://www.w3.org/1999/xhtml">>}], - children = - [#xmlel{name = <<"head">>, - children = - [#xmlel{name = <<"title">>, - children = [{xmlcdata, Heading}]}]}, - #xmlel{name = <<"body">>, - children = - [#xmlel{name = <<"h1">>, - children = [{xmlcdata, Heading}]}, - #xmlel{name = <<"p">>, - children = - [{xmlcdata, <<"An implementation of ">>}, - #xmlel{name = <<"a">>, - attrs = - [{<<"href">>, - <<"http://xmpp.org/extensions/xep-0206.html">>}], - children = - [{xmlcdata, - <<"XMPP over BOSH (XEP-0206)">>}]}]}, - #xmlel{name = <<"p">>, - children = - [{xmlcdata, - <<"This web page is only informative. To " - "use HTTP-Bind you need a Jabber/XMPP " - "client that supports it.">>}]}]}]}. - -%%%---------------------------------------------------------------------- -%%% BEHAVIOUR CALLBACKS -%%%---------------------------------------------------------------------- -start(Host, _Opts) -> - setup_database(), - Proc = gen_mod:get_module_proc(Host, ?PROCNAME_MHB), - ChildSpec = {Proc, - {ejabberd_tmp_sup, start_link, - [Proc, ejabberd_http_bind]}, - permanent, infinity, supervisor, [ejabberd_tmp_sup]}, - supervisor:start_child(ejabberd_sup, ChildSpec). - -stop(Host) -> - Proc = gen_mod:get_module_proc(Host, ?PROCNAME_MHB), - supervisor:terminate_child(ejabberd_sup, Proc), - supervisor:delete_child(ejabberd_sup, Proc). - -setup_database() -> - migrate_database(), - mnesia:create_table(http_bind, - [{ram_copies, [node()]}, - {attributes, record_info(fields, http_bind)}]). - -migrate_database() -> - case catch mnesia:table_info(http_bind, attributes) of - [id, pid, to, hold, wait, process_delay, version] -> - ok; - _ -> - %% Since the stored information is not important, instead - %% of actually migrating data, let's just destroy the table - mnesia:delete_table(http_bind) - end. diff --git a/src/mod_http_fileserver.erl b/src/mod_http_fileserver.erl index 97738355e..3f8db94f5 100644 --- a/src/mod_http_fileserver.erl +++ b/src/mod_http_fileserver.erl @@ -5,7 +5,7 @@ %%% Created : %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -31,10 +31,7 @@ -behaviour(gen_server). %% gen_mod callbacks --export([start/2, stop/1]). - -%% API --export([start_link/2]). +-export([start/2, stop/1, reload/3]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, @@ -43,33 +40,34 @@ %% request_handlers callbacks -export([process/2]). -%% ejabberd_hooks callbacks --export([reopen_log/1]). +%% utility for other http modules +-export([content_type/3]). + +-export([reopen_log/0, mod_opt_type/1, mod_options/1, depends/2, mod_doc/0]). --include("ejabberd.hrl"). -include("logger.hrl"). -include("ejabberd_http.hrl"). - --include("jlib.hrl"). - -include_lib("kernel/include/file.hrl"). +-include("translate.hrl"). -record(state, {host, docroot, accesslog, accesslogfd, directory_indices, custom_headers, default_content_type, - content_types = []}). - --define(PROCNAME, ejabberd_mod_http_fileserver). + content_types = [], user_access = none}). %% Response is {DataSize, Code, [{HeaderKey, HeaderValue}], Data} -define(HTTP_ERR_FILE_NOT_FOUND, {-1, 404, [], <<"Not found">>}). +-define(REQUEST_AUTH_HEADERS, + [{<<"WWW-Authenticate">>, <<"Basic realm=\"ejabberd\"">>}]). + -define(HTTP_ERR_FORBIDDEN, {-1, 403, [], <<"Forbidden">>}). - --define(DEFAULT_CONTENT_TYPE, - <<"application/octet-stream">>). +-define(HTTP_ERR_REQUEST_AUTH, + {-1, 401, ?REQUEST_AUTH_HEADERS, <<"Unauthorized">>}). +-define(HTTP_ERR_HOST_UNKNOWN, + {-1, 410, [], <<"Host unknown">>}). -define(DEFAULT_CONTENT_TYPES, [{<<".css">>, <<"text/css">>}, @@ -86,39 +84,22 @@ {<<".xpi">>, <<"application/x-xpinstall">>}, {<<".xul">>, <<"application/vnd.mozilla.xul+xml">>}]). --compile(export_all). - %%==================================================================== %% gen_mod callbacks %%==================================================================== start(Host, Opts) -> - Proc = get_proc_name(Host), - ChildSpec = - {Proc, - {?MODULE, start_link, [Host, Opts]}, - transient, % if process crashes abruptly, it gets restarted - 1000, - worker, - [?MODULE]}, - supervisor:start_child(ejabberd_sup, ChildSpec). + gen_mod:start_child(?MODULE, Host, Opts). stop(Host) -> - Proc = get_proc_name(Host), - gen_server:call(Proc, stop), - supervisor:terminate_child(ejabberd_sup, Proc), - supervisor:delete_child(ejabberd_sup, Proc). + gen_mod:stop_child(?MODULE, Host). -%%==================================================================== -%% API -%%==================================================================== -%%-------------------------------------------------------------------- -%% Function: start_link() -> {ok,Pid} | ignore | {error,Error} -%% Description: Starts the server -%%-------------------------------------------------------------------- -start_link(Host, Opts) -> +reload(Host, NewOpts, OldOpts) -> Proc = get_proc_name(Host), - gen_server:start_link({local, Proc}, ?MODULE, [Host, Opts], []). + gen_server:cast(Proc, {reload, Host, NewOpts, OldOpts}). + +depends(_Host, _Opts) -> + []. %%==================================================================== %% gen_server callbacks @@ -130,53 +111,49 @@ start_link(Host, Opts) -> %% {stop, Reason} %% Description: Initiates the server %%-------------------------------------------------------------------- -init([Host, Opts]) -> +init([Host|_]) -> + Opts = gen_mod:get_module_opts(Host, ?MODULE), try initialize(Host, Opts) of - {DocRoot, AccessLog, AccessLogFD, DirectoryIndices, - CustomHeaders, DefaultContentType, ContentTypes} -> - {ok, #state{host = Host, - accesslog = AccessLog, - accesslogfd = AccessLogFD, - docroot = DocRoot, - directory_indices = DirectoryIndices, - custom_headers = CustomHeaders, - default_content_type = DefaultContentType, - content_types = ContentTypes}} + State -> + process_flag(trap_exit, true), + {ok, State} catch throw:Reason -> {stop, Reason} end. initialize(Host, Opts) -> - DocRoot = gen_mod:get_opt(docroot, Opts, fun(A) -> A end, undefined), - check_docroot_defined(DocRoot, Host), - DRInfo = check_docroot_exists(DocRoot), - check_docroot_is_dir(DRInfo, DocRoot), - check_docroot_is_readable(DRInfo, DocRoot), - AccessLog = gen_mod:get_opt(accesslog, Opts, - fun iolist_to_binary/1, - undefined), + DocRoot = mod_http_fileserver_opt:docroot(Opts), + AccessLog = mod_http_fileserver_opt:accesslog(Opts), AccessLogFD = try_open_log(AccessLog, Host), - DirectoryIndices = gen_mod:get_opt(directory_indices, Opts, - fun(L) when is_list(L) -> L end, - []), - CustomHeaders = gen_mod:get_opt(custom_headers, Opts, - fun(L) when is_list(L) -> L end, - []), - DefaultContentType = gen_mod:get_opt(default_content_type, Opts, - fun iolist_to_binary/1, - ?DEFAULT_CONTENT_TYPE), + DirectoryIndices = mod_http_fileserver_opt:directory_indices(Opts), + CustomHeaders = mod_http_fileserver_opt:custom_headers(Opts), + DefaultContentType = mod_http_fileserver_opt:default_content_type(Opts), + UserAccess0 = mod_http_fileserver_opt:must_authenticate_with(Opts), + UserAccess = case UserAccess0 of + [] -> none; + _ -> + maps:from_list(UserAccess0) + end, ContentTypes = build_list_content_types( - gen_mod:get_opt(content_types, Opts, - fun(L) when is_list(L) -> L end, - []), + mod_http_fileserver_opt:content_types(Opts), ?DEFAULT_CONTENT_TYPES), - ?INFO_MSG("initialize: ~n ~p", [ContentTypes]),%+++ - {DocRoot, AccessLog, AccessLogFD, DirectoryIndices, - CustomHeaders, DefaultContentType, ContentTypes}. + ?DEBUG("Known content types: ~ts", + [str:join([[$*, K, " -> ", V] || {K, V} <- ContentTypes], + <<", ">>)]), + #state{host = Host, + accesslog = AccessLog, + accesslogfd = AccessLogFD, + docroot = DocRoot, + directory_indices = DirectoryIndices, + custom_headers = CustomHeaders, + default_content_type = DefaultContentType, + content_types = ContentTypes, + user_access = UserAccess}. - -%% @spec (AdminCTs::[CT], Default::[CT]) -> [CT] +-spec build_list_content_types(AdminCTs::[{binary(), binary()|undefined}], + Default::[{binary(), binary()|undefined}]) -> + [{string(), string()|undefined}]. %% where CT = {Extension::string(), Value} %% Value = string() | undefined %% @doc Return a unified list without duplicates. @@ -191,35 +168,9 @@ build_list_content_types(AdminCTsUnsorted, DefaultCTsUnsorted) -> || {Extension, Value} <- CTsUnfiltered, Value /= undefined]. -check_docroot_defined(DocRoot, Host) -> - case DocRoot of - undefined -> throw({undefined_docroot_option, Host}); - _ -> ok - end. - -check_docroot_exists(DocRoot) -> - case file:read_file_info(DocRoot) of - {error, Reason} -> - throw({error_access_docroot, DocRoot, Reason}); - {ok, FI} -> FI - end. - -check_docroot_is_dir(DRInfo, DocRoot) -> - case DRInfo#file_info.type of - directory -> ok; - _ -> throw({docroot_not_directory, DocRoot}) - end. - -check_docroot_is_readable(DRInfo, DocRoot) -> - case DRInfo#file_info.access of - read -> ok; - read_write -> ok; - _ -> throw({docroot_not_readable, DocRoot}) - end. - try_open_log(undefined, _Host) -> undefined; -try_open_log(FN, Host) -> +try_open_log(FN, _Host) -> FD = try open_log(FN) of FD1 -> FD1 catch @@ -227,7 +178,7 @@ try_open_log(FN, Host) -> ?ERROR_MSG("Cannot open access log file: ~p~nReason: ~p", [FN, Reason]), undefined end, - ejabberd_hooks:add(reopen_log_hook, Host, ?MODULE, reopen_log, 50), + ejabberd_hooks:add(reopen_log_hook, ?MODULE, reopen_log, 50), FD. %%-------------------------------------------------------------------- @@ -239,13 +190,21 @@ try_open_log(FN, Host) -> %% {stop, Reason, State} %% Description: Handling call messages %%-------------------------------------------------------------------- -handle_call({serve, LocalPath}, _From, State) -> - Reply = serve(LocalPath, State#state.docroot, State#state.directory_indices, +handle_call({serve, LocalPath, Auth, RHeaders}, _From, State) -> + IfModifiedSince = case find_header('If-Modified-Since', RHeaders, bad_date) of + bad_date -> + bad_date; + Val -> + httpd_util:convert_request_date(binary_to_list(Val)) + end, + Reply = serve(LocalPath, Auth, State#state.docroot, State#state.directory_indices, State#state.custom_headers, - State#state.default_content_type, State#state.content_types), + State#state.default_content_type, State#state.content_types, + State#state.user_access, IfModifiedSince), {reply, Reply, State}; -handle_call(_Request, _From, State) -> - {reply, ok, State}. +handle_call(Request, From, State) -> + ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), + {noreply, State}. %%-------------------------------------------------------------------- %% Function: handle_cast(Msg, State) -> {noreply, State} | @@ -259,7 +218,16 @@ handle_cast({add_to_log, FileSize, Code, Request}, State) -> handle_cast(reopen_log, State) -> FD2 = reopen_log(State#state.accesslog, State#state.accesslogfd), {noreply, State#state{accesslogfd = FD2}}; -handle_cast(_Msg, State) -> +handle_cast({reload, Host, NewOpts, _OldOpts}, OldState) -> + try initialize(Host, NewOpts) of + NewState -> + FD = reopen_log(NewState#state.accesslog, OldState#state.accesslogfd), + {noreply, NewState#state{accesslogfd = FD}} + catch throw:_ -> + {noreply, OldState} + end; +handle_cast(Msg, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. %%-------------------------------------------------------------------- @@ -268,7 +236,8 @@ handle_cast(_Msg, State) -> %% {stop, Reason, State} %% Description: Handling all non call/cast messages %%-------------------------------------------------------------------- -handle_info(_Info, State) -> +handle_info(Info, State) -> + ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. %%-------------------------------------------------------------------- @@ -278,10 +247,14 @@ handle_info(_Info, State) -> %% cleaning up. When it returns, the gen_server terminates with Reason. %% The return value is ignored. %%-------------------------------------------------------------------- -terminate(_Reason, State) -> +terminate(_Reason, #state{host = Host} = State) -> close_log(State#state.accesslogfd), - ejabberd_hooks:delete(reopen_log_hook, State#state.host, ?MODULE, reopen_log, 50), - ok. + case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of + false -> + ejabberd_hooks:delete(reopen_log_hook, ?MODULE, reopen_log, 50); + true -> + ok + end. %%-------------------------------------------------------------------- %% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} @@ -294,38 +267,70 @@ code_change(_OldVsn, State, _Extra) -> %% request_handlers callbacks %%==================================================================== -%% @spec (LocalPath, Request) -> {HTTPCode::integer(), [Header], Page::string()} +-spec process(LocalPath::[binary()], #request{}) -> + {HTTPCode::integer(), [{binary(), binary()}], Page::string()}. %% @doc Handle an HTTP request. %% LocalPath is the part of the requested URL path that is "local to the module". %% Returns the page to be sent back to the client and/or HTTP status code. -process(LocalPath, Request) -> +process(LocalPath, #request{host = Host, auth = Auth, headers = RHeaders} = Request) -> ?DEBUG("Requested ~p", [LocalPath]), - try gen_server:call(get_proc_name(Request#request.host), {serve, LocalPath}) of - {FileSize, Code, Headers, Contents} -> - add_to_log(FileSize, Code, Request), - {Code, Headers, Contents} - catch - exit:{noproc, _} -> - ?ERROR_MSG("Received an HTTP request with Host ~p, but couldn't find the related " - "ejabberd virtual host", [Request#request.host]), - ejabberd_web:error(not_found) + try + VHost = ejabberd_router:host_of_route(Host), + {FileSize, Code, Headers, Contents} = + gen_server:call(get_proc_name(VHost), + {serve, LocalPath, Auth, RHeaders}), + add_to_log(FileSize, Code, Request#request{host = VHost}), + {Code, Headers, Contents} + catch _:{Why, _} when Why == noproc; Why == invalid_domain; Why == unregistered_route -> + ?DEBUG("Received an HTTP request with Host: ~ts, " + "but couldn't find the related " + "ejabberd virtual host", [Host]), + {FileSize1, Code1, Headers1, Contents1} = ?HTTP_ERR_HOST_UNKNOWN, + add_to_log(FileSize1, Code1, Request#request{host = ejabberd_config:get_myname()}), + {Code1, Headers1, Contents1} end. -serve(LocalPath, DocRoot, DirectoryIndices, CustomHeaders, DefaultContentType, ContentTypes) -> - FileName = filename:join(filename:split(DocRoot) ++ LocalPath), - case file:read_file_info(FileName) of - {error, enoent} -> ?HTTP_ERR_FILE_NOT_FOUND; - {error, enotdir} -> ?HTTP_ERR_FILE_NOT_FOUND; - {error, eacces} -> ?HTTP_ERR_FORBIDDEN; - {ok, #file_info{type = directory}} -> serve_index(FileName, - DirectoryIndices, - CustomHeaders, - DefaultContentType, - ContentTypes); - {ok, FileInfo} -> serve_file(FileInfo, FileName, - CustomHeaders, - DefaultContentType, - ContentTypes) +serve(LocalPath, Auth, DocRoot, DirectoryIndices, CustomHeaders, DefaultContentType, + ContentTypes, UserAccess, IfModifiedSince) -> + CanProceed = case {UserAccess, Auth} of + {none, _} -> true; + {_, {User, Pass}} -> + case maps:find(User, UserAccess) of + {ok, Pass} -> true; + _ -> false + end; + _ -> + false + end, + case CanProceed of + false -> + ?HTTP_ERR_REQUEST_AUTH; + true -> + FileName = filename:join(filename:split(DocRoot) ++ LocalPath), + case file:read_file_info(FileName) of + {error, enoent} -> + ?HTTP_ERR_FILE_NOT_FOUND; + {error, enotdir} -> + ?HTTP_ERR_FILE_NOT_FOUND; + {error, eacces} -> + ?HTTP_ERR_FORBIDDEN; + {ok, #file_info{type = directory}} -> serve_index(FileName, + DirectoryIndices, + CustomHeaders, + DefaultContentType, + ContentTypes); + {ok, #file_info{mtime = MTime} = FileInfo} -> + case calendar:local_time_to_universal_time_dst(MTime) of + [IfModifiedSince | _] -> + serve_not_modified(FileInfo, FileName, + CustomHeaders); + _ -> + serve_file(FileInfo, FileName, + CustomHeaders, + DefaultContentType, + ContentTypes) + end + end end. %% Troll through the directory indices attempting to find one which @@ -340,19 +345,27 @@ serve_index(FileName, [Index | T], CH, DefaultContentType, ContentTypes) -> {ok, FileInfo} -> serve_file(FileInfo, IndexFileName, CH, DefaultContentType, ContentTypes) end. +serve_not_modified(FileInfo, FileName, CustomHeaders) -> + ?DEBUG("Delivering not modified: ~ts", [FileName]), + {0, 304, + ejabberd_http:apply_custom_headers( + [{<<"Server">>, <<"ejabberd">>}, + {<<"Last-Modified">>, last_modified(FileInfo)}], + CustomHeaders), <<>>}. + %% Assume the file exists if we got this far and attempt to read it in %% and serve it up. serve_file(FileInfo, FileName, CustomHeaders, DefaultContentType, ContentTypes) -> - ?DEBUG("Delivering: ~s", [FileName]), + ?DEBUG("Delivering: ~ts", [FileName]), ContentType = content_type(FileName, DefaultContentType, ContentTypes), - {ok, FileContents} = file:read_file(FileName), {FileInfo#file_info.size, 200, - [{<<"Server">>, <<"ejabberd">>}, - {<<"Last-Modified">>, last_modified(FileInfo)}, - {<<"Content-Type">>, ContentType} - | CustomHeaders], - FileContents}. + ejabberd_http:apply_custom_headers( + [{<<"Server">>, <<"ejabberd">>}, + {<<"Last-Modified">>, last_modified(FileInfo)}, + {<<"Content-Type">>, ContentType}], + CustomHeaders), + {file, FileName}}. %%---------------------------------------------------------------------- %% Log file @@ -375,8 +388,11 @@ reopen_log(FN, FD) -> close_log(FD), open_log(FN). -reopen_log(Host) -> - gen_server:cast(get_proc_name(Host), reopen_log). +reopen_log() -> + lists:foreach( + fun(Host) -> + gen_server:cast(get_proc_name(Host), reopen_log) + end, ejabberd_option:hosts()). add_to_log(FileSize, Code, Request) -> gen_server:cast(get_proc_name(Request#request.host), @@ -388,9 +404,8 @@ add_to_log(File, FileSize, Code, Request) -> {{Year, Month, Day}, {Hour, Minute, Second}} = calendar:local_time(), IP = ip_to_string(element(1, Request#request.ip)), Path = join(Request#request.path, "/"), - Query = case join(lists:map(fun(E) -> lists:concat([element(1, E), "=", binary_to_list(element(2, E))]) end, - Request#request.q), "&") of - [] -> + Query = case stringify_query(Request#request.q) of + <<"">> -> ""; String -> [$? | String] @@ -399,16 +414,25 @@ add_to_log(File, FileSize, Code, Request) -> Referer = find_header('Referer', Request#request.headers, "-"), %% Pseudo Combined Apache log format: %% 127.0.0.1 - - [28/Mar/2007:18:41:55 +0200] "GET / HTTP/1.1" 302 303 "-" "tsung" - %% TODO some fields are harcoded/missing: + %% TODO some fields are hardcoded/missing: %% The date/time integers should have always 2 digits. For example day "7" should be "07" %% Month should be 3*letter, not integer 1..12 %% Missing time zone = (`+' | `-') 4*digit %% Missing protocol version: HTTP/1.1 %% For reference: http://httpd.apache.org/docs/2.2/logs.html - io:format(File, "~s - - [~p/~p/~p:~p:~p:~p] \"~s /~s~s\" ~p ~p ~p ~p~n", + io:format(File, "~ts - - [~p/~p/~p:~p:~p:~p] \"~ts /~ts~ts\" ~p ~p ~p ~p~n", [IP, Day, Month, Year, Hour, Minute, Second, Request#request.method, Path, Query, Code, FileSize, Referer, UserAgent]). +stringify_query(Q) -> + stringify_query(Q, []). +stringify_query([], Res) -> + join(lists:reverse(Res), "&"); +stringify_query([{nokey, _B} | Q], Res) -> + stringify_query(Q, Res); +stringify_query([{A, B} | Q], Res) -> + stringify_query(Q, [join([A,B], "=") | Res]). + find_header(Header, Headers, Default) -> case lists:keysearch(Header, 1, Headers) of {value, {_, Value}} -> Value; @@ -419,7 +443,7 @@ find_header(Header, Headers, Default) -> %% Utilities %%---------------------------------------------------------------------- -get_proc_name(Host) -> gen_mod:get_module_proc(Host, ?PROCNAME). +get_proc_name(Host) -> gen_mod:get_module_proc(Host, ?MODULE). join([], _) -> <<"">>; @@ -448,3 +472,112 @@ ip_to_string(Address) when size(Address) == 4 -> ip_to_string(Address) when size(Address) == 8 -> Parts = lists:map(fun (Int) -> io_lib:format("~.16B", [Int]) end, tuple_to_list(Address)), string:to_lower(lists:flatten(join(Parts, ":"))). + +mod_opt_type(accesslog) -> + econf:file(write); +mod_opt_type(content_types) -> + econf:map(econf:binary(), econf:binary()); +mod_opt_type(custom_headers) -> + econf:map(econf:binary(), econf:binary()); +mod_opt_type(default_content_type) -> + econf:binary(); +mod_opt_type(directory_indices) -> + econf:list(econf:binary()); +mod_opt_type(docroot) -> + econf:directory(write); +mod_opt_type(must_authenticate_with) -> + econf:list( + econf:and_then( + econf:and_then( + econf:binary("^[^:]+:[^:]+$"), + econf:binary_sep(":")), + fun([K, V]) -> {K, V} end)). + +-spec mod_options(binary()) -> [{must_authenticate_with, [{binary(), binary()}]} | + {atom(), any()}]. +mod_options(_) -> + [{accesslog, undefined}, + {content_types, []}, + {default_content_type, <<"application/octet-stream">>}, + {custom_headers, []}, + {directory_indices, []}, + {must_authenticate_with, []}, + %% Required option + docroot]. + +mod_doc() -> + #{desc => + ?T("This simple module serves files from the local disk over HTTP."), + opts => + [{accesslog, + #{value => ?T("Path"), + desc => + ?T("File to log accesses using an Apache-like format. " + "No log will be recorded if this option is not specified.")}}, + {docroot, + #{value => ?T("Path"), + desc => + ?T("Directory to serve the files from. " + "This is a mandatory option.")}}, + {content_types, + #{value => "{Extension: Type}", + desc => + ?T("Specify mappings of extension to content type. " + "There are several content types already defined. " + "With this option you can add new definitions " + "or modify existing ones. The default values are:"), + example => + ["content_types:"| + [" " ++ binary_to_list(E) ++ ": " ++ binary_to_list(T) + || {E, T} <- ?DEFAULT_CONTENT_TYPES]]}}, + {default_content_type, + #{value => ?T("Type"), + desc => + ?T("Specify the content type to use for unknown extensions. " + "The default value is 'application/octet-stream'.")}}, + {custom_headers, + #{value => "{Name: Value}", + desc => + ?T("Indicate custom HTTP headers to be included in all responses. " + "There are no custom headers by default.")}}, + {directory_indices, + #{value => "[Index, ...]", + desc => + ?T("Indicate one or more directory index files, " + "similarly to Apache's 'DirectoryIndex' variable. " + "When an HTTP request hits a directory instead of a " + "regular file, those directory indices are looked in order, " + "and the first one found is returned. " + "The default value is an empty list.")}}, + {must_authenticate_with, + #{value => ?T("[{Username, Hostname}, ...]"), + desc => + ?T("List of accounts that are allowed to use this service. " + "Default value: '[]'.")}}], + example => + [{?T("This example configuration will serve the files from the " + "local directory '/var/www' in the address " + "'http://example.org:5280/pub/content/'. In this example a new " + "content type 'ogg' is defined, 'png' is redefined, and 'jpg' " + "definition is deleted:"), + ["listen:", + " -", + " port: 5280", + " module: ejabberd_http", + " request_handlers:", + " /pub/content: mod_http_fileserver", + "", + "modules:", + " mod_http_fileserver:", + " docroot: /var/www", + " accesslog: /var/log/ejabberd/access.log", + " directory_indices:", + " - index.html", + " - main.htm", + " custom_headers:", + " X-Powered-By: Erlang/OTP", + " X-Fry: \"It's a widely-believed fact!\"", + " content_types:", + " .ogg: audio/ogg", + " .png: image/png", + " default_content_type: text/html"]}]}. diff --git a/src/mod_http_fileserver_opt.erl b/src/mod_http_fileserver_opt.erl new file mode 100644 index 000000000..442ce1d90 --- /dev/null +++ b/src/mod_http_fileserver_opt.erl @@ -0,0 +1,55 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_http_fileserver_opt). + +-export([accesslog/1]). +-export([content_types/1]). +-export([custom_headers/1]). +-export([default_content_type/1]). +-export([directory_indices/1]). +-export([docroot/1]). +-export([must_authenticate_with/1]). + +-spec accesslog(gen_mod:opts() | global | binary()) -> 'undefined' | binary(). +accesslog(Opts) when is_map(Opts) -> + gen_mod:get_opt(accesslog, Opts); +accesslog(Host) -> + gen_mod:get_module_opt(Host, mod_http_fileserver, accesslog). + +-spec content_types(gen_mod:opts() | global | binary()) -> [{binary(),binary()}]. +content_types(Opts) when is_map(Opts) -> + gen_mod:get_opt(content_types, Opts); +content_types(Host) -> + gen_mod:get_module_opt(Host, mod_http_fileserver, content_types). + +-spec custom_headers(gen_mod:opts() | global | binary()) -> [{binary(),binary()}]. +custom_headers(Opts) when is_map(Opts) -> + gen_mod:get_opt(custom_headers, Opts); +custom_headers(Host) -> + gen_mod:get_module_opt(Host, mod_http_fileserver, custom_headers). + +-spec default_content_type(gen_mod:opts() | global | binary()) -> binary(). +default_content_type(Opts) when is_map(Opts) -> + gen_mod:get_opt(default_content_type, Opts); +default_content_type(Host) -> + gen_mod:get_module_opt(Host, mod_http_fileserver, default_content_type). + +-spec directory_indices(gen_mod:opts() | global | binary()) -> [binary()]. +directory_indices(Opts) when is_map(Opts) -> + gen_mod:get_opt(directory_indices, Opts); +directory_indices(Host) -> + gen_mod:get_module_opt(Host, mod_http_fileserver, directory_indices). + +-spec docroot(gen_mod:opts() | global | binary()) -> binary(). +docroot(Opts) when is_map(Opts) -> + gen_mod:get_opt(docroot, Opts); +docroot(Host) -> + gen_mod:get_module_opt(Host, mod_http_fileserver, docroot). + +-spec must_authenticate_with(gen_mod:opts() | global | binary()) -> [{binary(),binary()}]. +must_authenticate_with(Opts) when is_map(Opts) -> + gen_mod:get_opt(must_authenticate_with, Opts); +must_authenticate_with(Host) -> + gen_mod:get_module_opt(Host, mod_http_fileserver, must_authenticate_with). + diff --git a/src/mod_http_upload.erl b/src/mod_http_upload.erl new file mode 100644 index 000000000..faa085811 --- /dev/null +++ b/src/mod_http_upload.erl @@ -0,0 +1,1206 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_http_upload.erl +%%% Author : Holger Weiss +%%% Purpose : HTTP File Upload (XEP-0363) +%%% Created : 20 Aug 2015 by Holger Weiss +%%% +%%% +%%% ejabberd, Copyright (C) 2015-2025 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_http_upload). +-author('holger@zedat.fu-berlin.de'). +-behaviour(gen_server). +-behaviour(gen_mod). +-protocol({xep, 363, '0.3.0', '15.10', "complete", ""}). + +-define(SERVICE_REQUEST_TIMEOUT, 5000). % 5 seconds. +-define(CALL_TIMEOUT, 60000). % 1 minute. +-define(SLOT_TIMEOUT, timer:hours(5)). +-define(DEFAULT_CONTENT_TYPE, <<"application/octet-stream">>). +-define(CONTENT_TYPES, + [{<<".avi">>, <<"video/avi">>}, + {<<".bmp">>, <<"image/bmp">>}, + {<<".bz2">>, <<"application/x-bzip2">>}, + {<<".gif">>, <<"image/gif">>}, + {<<".gz">>, <<"application/x-gzip">>}, + {<<".jpeg">>, <<"image/jpeg">>}, + {<<".jpg">>, <<"image/jpeg">>}, + {<<".m4a">>, <<"audio/mp4">>}, + {<<".mp3">>, <<"audio/mpeg">>}, + {<<".mp4">>, <<"video/mp4">>}, + {<<".mpeg">>, <<"video/mpeg">>}, + {<<".mpg">>, <<"video/mpeg">>}, + {<<".ogg">>, <<"application/ogg">>}, + {<<".pdf">>, <<"application/pdf">>}, + {<<".png">>, <<"image/png">>}, + {<<".rtf">>, <<"application/rtf">>}, + {<<".svg">>, <<"image/svg+xml">>}, + {<<".tiff">>, <<"image/tiff">>}, + {<<".txt">>, <<"text/plain">>}, + {<<".wav">>, <<"audio/wav">>}, + {<<".webp">>, <<"image/webp">>}, + {<<".xz">>, <<"application/x-xz">>}, + {<<".zip">>, <<"application/zip">>}]). + +%% gen_mod/supervisor callbacks. +-export([start/2, + stop/1, + reload/3, + depends/2, + mod_doc/0, + mod_opt_type/1, + mod_options/1]). + +%% gen_server callbacks. +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). + +%% ejabberd_http callback. +-export([process/2]). + +%% ejabberd_hooks callback. +-export([remove_user/2]). + +%% Utility functions. +-export([get_proc_name/2, + expand_home/1, + expand_host/2]). + +-include("ejabberd_http.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). +-include("logger.hrl"). +-include("translate.hrl"). + +-record(state, + {server_host = <<>> :: binary(), + hosts = [] :: [binary()], + name = <<>> :: binary(), + access = none :: atom(), + max_size = infinity :: pos_integer() | infinity, + secret_length = 40 :: pos_integer(), + jid_in_url = sha1 :: sha1 | node, + file_mode :: integer() | undefined, + dir_mode :: integer() | undefined, + docroot = <<>> :: binary(), + put_url = <<>> :: binary(), + get_url = <<>> :: binary(), + service_url :: binary() | undefined, + thumbnail = false :: boolean(), + custom_headers = [] :: [{binary(), binary()}], + slots = #{} :: slots(), + external_secret = <<>> :: binary()}). + +-record(media_info, + {path :: binary(), + type :: atom(), + height :: integer(), + width :: integer()}). + +-type state() :: #state{}. +-type slot() :: [binary(), ...]. +-type slots() :: #{slot() => {pos_integer(), reference()}}. +-type media_info() :: #media_info{}. + +%%-------------------------------------------------------------------- +%% gen_mod/supervisor callbacks. +%%-------------------------------------------------------------------- +-spec start(binary(), gen_mod:opts()) -> {ok, pid()} | {error, term()}. +start(ServerHost, Opts) -> + Proc = get_proc_name(ServerHost, ?MODULE), + case gen_mod:start_child(?MODULE, ServerHost, Opts, Proc) of + {ok, _} = Ret -> Ret; + {error, {already_started, _}} = Err -> + ?ERROR_MSG("Multiple virtual hosts can't use a single 'put_url' " + "without the @HOST@ keyword", []), + Err; + Err -> + Err + end. + +-spec stop(binary()) -> ok | {error, any()}. +stop(ServerHost) -> + Proc = get_proc_name(ServerHost, ?MODULE), + gen_mod:stop_child(Proc). + +-spec reload(binary(), gen_mod:opts(), gen_mod:opts()) -> ok | {ok, pid()} | {error, term()}. +reload(ServerHost, NewOpts, OldOpts) -> + NewURL = mod_http_upload_opt:put_url(NewOpts), + OldURL = mod_http_upload_opt:put_url(OldOpts), + OldProc = get_proc_name(ServerHost, ?MODULE, OldURL), + NewProc = get_proc_name(ServerHost, ?MODULE, NewURL), + if OldProc /= NewProc -> + gen_mod:stop_child(OldProc), + start(ServerHost, NewOpts); + true -> + gen_server:cast(NewProc, {reload, NewOpts, OldOpts}) + end. + +-spec mod_opt_type(atom()) -> econf:validator(). +mod_opt_type(name) -> + econf:binary(); +mod_opt_type(access) -> + econf:acl(); +mod_opt_type(max_size) -> + econf:pos_int(infinity); +mod_opt_type(secret_length) -> + econf:int(8, 1000); +mod_opt_type(jid_in_url) -> + econf:enum([sha1, node]); +mod_opt_type(file_mode) -> + econf:octal(); +mod_opt_type(dir_mode) -> + econf:octal(); +mod_opt_type(docroot) -> + econf:binary(); +mod_opt_type(put_url) -> + econf:url(); +mod_opt_type(get_url) -> + econf:url(); +mod_opt_type(service_url) -> + econf:url(); +mod_opt_type(custom_headers) -> + econf:map(econf:binary(), econf:binary()); +mod_opt_type(rm_on_unregister) -> + econf:bool(); +mod_opt_type(thumbnail) -> + econf:and_then( + econf:bool(), + fun(true) -> + case eimp:supported_formats() of + [] -> econf:fail(eimp_error); + [_|_] -> true + end; + (false) -> + false + end); +mod_opt_type(external_secret) -> + econf:binary(); +mod_opt_type(host) -> + econf:host(); +mod_opt_type(hosts) -> + econf:hosts(); +mod_opt_type(vcard) -> + econf:vcard_temp(). + +-spec mod_options(binary()) -> [{thumbnail, boolean()} | + {atom(), any()}]. +mod_options(Host) -> + [{host, <<"upload.", Host/binary>>}, + {hosts, []}, + {name, ?T("HTTP File Upload")}, + {vcard, undefined}, + {access, local}, + {max_size, 104857600}, + {secret_length, 40}, + {jid_in_url, sha1}, + {file_mode, undefined}, + {dir_mode, undefined}, + {docroot, <<"@HOME@/upload">>}, + {put_url, <<"https://", Host/binary, ":5443/upload">>}, + {get_url, undefined}, + {service_url, undefined}, + {external_secret, <<"">>}, + {custom_headers, []}, + {rm_on_unregister, true}, + {thumbnail, false}]. + +mod_doc() -> + #{desc => + [?T("This module allows for requesting permissions to " + "upload a file via HTTP as described in " + "https://xmpp.org/extensions/xep-0363.html" + "[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."), "", + ?T("In order to use this module, it must be enabled " + "in 'listen' -> 'ejabberd_http' -> " + "_`listen-options.md#request_handlers|request_handlers`_.")], + opts => + [{host, + #{desc => ?T("Deprecated. Use 'hosts' instead.")}}, + {hosts, + #{value => ?T("[Host, ...]"), + desc => + ?T("This option defines the Jabber IDs of the service. " + "If the 'hosts' option is not specified, the only Jabber ID will " + "be the hostname of the virtual host with the prefix '\"upload.\"'. " + "The keyword '@HOST@' is replaced with the real virtual host name.")}}, + {name, + #{value => ?T("Name"), + desc => + ?T("A name of the service in the Service Discovery. " + "The default value is '\"HTTP File Upload\"'. " + "Please note this will only be displayed by some XMPP clients.")}}, + {access, + #{value => ?T("AccessName"), + desc => + ?T("This option defines the access rule to limit who is " + "permitted to use the HTTP upload service. " + "The default value is 'local'. If no access rule of " + "that name exists, no user will be allowed to use the service.")}}, + {max_size, + #{value => ?T("Size"), + desc => + ?T("This option limits the acceptable file size. " + "Either a number of bytes (larger than zero) or " + "'infinity' must be specified. " + "The default value is '104857600'.")}}, + {secret_length, + #{value => ?T("Length"), + desc => + ?T("This option defines the length of the random " + "string included in the GET and PUT URLs generated " + "by 'mod_http_upload'. The minimum length is '8' characters, " + "but it is recommended to choose a larger value. " + "The default value is '40'.")}}, + {jid_in_url, + #{value => "node | sha1", + desc => + ?T("When this option is set to 'node', the node identifier " + "of the user's JID (i.e., the user name) is included in " + "the GET and PUT URLs generated by 'mod_http_upload'. " + "Otherwise, a SHA-1 hash of the user's bare JID is " + "included instead. The default value is 'sha1'.")}}, + {thumbnail, + #{value => "true | false", + desc => + ?T("This option specifies whether ejabberd should create " + "thumbnails of uploaded images. If a thumbnail is created, " + "a element that contains the download " + "and some metadata is returned with the PUT response. " + "The default value is 'false'.")}}, + {file_mode, + #{value => ?T("Permission"), + desc => + ?T("This option defines the permission bits of uploaded files. " + "The bits are specified as an octal number (see the 'chmod(1)' " + "manual page) within double quotes. For example: '\"0644\"'. " + "The default is undefined, which means no explicit permissions " + "will be set.")}}, + {dir_mode, + #{value => ?T("Permission"), + desc => + ?T("This option defines the permission bits of the 'docroot' " + "directory and any directories created during file uploads. " + "The bits are specified as an octal number (see the 'chmod(1)' " + "manual page) within double quotes. For example: '\"0755\"'. " + "The default is undefined, which means no explicit permissions " + "will be set.")}}, + {docroot, + #{value => ?T("Path"), + desc => + ?T("Uploaded files are stored below the directory specified " + "(as an absolute path) with this option. The keyword " + "'@HOME@' is replaced with the home directory of the user " + "running ejabberd, and the keyword '@HOST@' with the virtual " + "host name. The default value is '\"@HOME@/upload\"'.")}}, + {put_url, + #{value => ?T("URL"), + desc => + ?T("This option specifies the initial part of the PUT URLs " + "used for file uploads. The keyword '@HOST@' is replaced " + "with the virtual host name. " + "And '@HOST_URL_ENCODE@' is replaced with the host name encoded for URL, " + "useful when your virtual hosts contain non-latin characters. " + "NOTE: different virtual hosts cannot use the same PUT URL. " + "The default value is '\"https://@HOST@:5443/upload\"'.")}}, + {get_url, + #{value => ?T("URL"), + desc => + ?T("This option specifies the initial part of the GET URLs " + "used for downloading the files. The default value is 'undefined'. " + "When this option is 'undefined', this option is set " + "to the same value as 'put_url'. The keyword '@HOST@' is " + "replaced with the virtual host name. NOTE: if GET requests " + "are handled by this module, the 'get_url' must match the " + "'put_url'. Setting it to a different value only makes " + "sense if an external web server or _`mod_http_fileserver`_ " + "is used to serve the uploaded files.")}}, + {service_url, + #{desc => ?T("Deprecated.")}}, + {custom_headers, + #{value => "{Name: Value}", + desc => + ?T("This option specifies additional header fields to be " + "included in all HTTP responses. By default no custom " + "headers are included.")}}, + {external_secret, + #{value => ?T("Text"), + desc => + ?T("This option makes it possible to offload all HTTP " + "Upload processing to a separate HTTP server. " + "Both ejabberd and the HTTP server should share this " + "secret and behave exactly as described at " + "https://modules.prosody.im/mod_http_upload_external.html#implementation" + "[Prosody's mod_http_upload_external: Implementation]. " + "There is no default value.")}}, + {rm_on_unregister, + #{value => "true | false", + desc => + ?T("This option specifies whether files uploaded by a user " + "should be removed when that user is unregistered. " + "The default value is 'true'.")}}, + {vcard, + #{value => ?T("vCard"), + desc => + ?T("A custom vCard of the service that will be displayed " + "by some XMPP clients in Service Discovery. The value of " + "'vCard' is a YAML map constructed from an XML representation " + "of vCard. Since the representation has no attributes, " + "the mapping is straightforward."), + example => + ["# This XML representation of vCard:", + "# ", + "# Conferences", + "# ", + "# ", + "# Elm Street", + "# ", + "# ", + "# ", + "# is translated to:", + "vcard:", + " fn: Conferences", + " adr:", + " -", + " work: true", + " street: Elm Street"]}}], + example => + ["listen:", + " -", + " port: 5443", + " module: ejabberd_http", + " tls: true", + " request_handlers:", + " /upload: mod_http_upload", + "", + "modules:", + " mod_http_upload:", + " docroot: /ejabberd/upload", + " put_url: \"https://@HOST@:5443/upload\""]}. + +-spec depends(binary(), gen_mod:opts()) -> [{module(), hard | soft}]. +depends(_Host, _Opts) -> + []. + +%%-------------------------------------------------------------------- +%% gen_server callbacks. +%%-------------------------------------------------------------------- +-spec init(list()) -> {ok, state()}. +init([ServerHost|_]) -> + process_flag(trap_exit, true), + Opts = gen_mod:get_module_opts(ServerHost, ?MODULE), + Hosts = gen_mod:get_opt_hosts(Opts), + case mod_http_upload_opt:rm_on_unregister(Opts) of + true -> + ejabberd_hooks:add(remove_user, ServerHost, ?MODULE, + remove_user, 50); + false -> + ok + end, + State = init_state(ServerHost, Hosts, Opts), + {ok, State}. + +-spec handle_call(_, {pid(), _}, state()) + -> {reply, {ok, pos_integer(), binary(), + pos_integer() | undefined, + pos_integer() | undefined}, state()} | + {reply, {error, atom()}, state()} | {noreply, state()}. +handle_call({use_slot, Slot, Size}, _From, + #state{file_mode = FileMode, + dir_mode = DirMode, + get_url = GetPrefix, + thumbnail = Thumbnail, + custom_headers = CustomHeaders, + docroot = DocRoot} = State) -> + case get_slot(Slot, State) of + {ok, {Size, TRef}} -> + misc:cancel_timer(TRef), + NewState = del_slot(Slot, State), + Path = str:join([DocRoot | Slot], <<$/>>), + {reply, + {ok, Path, FileMode, DirMode, GetPrefix, Thumbnail, CustomHeaders}, + NewState}; + {ok, {_WrongSize, _TRef}} -> + {reply, {error, size_mismatch}, State}; + error -> + {reply, {error, invalid_slot}, State} + end; +handle_call(get_conf, _From, + #state{docroot = DocRoot, + custom_headers = CustomHeaders} = State) -> + {reply, {ok, DocRoot, CustomHeaders}, State}; +handle_call(Request, From, State) -> + ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), + {noreply, State}. + +-spec handle_cast(_, state()) -> {noreply, state()}. +handle_cast({reload, NewOpts, OldOpts}, + #state{server_host = ServerHost} = State) -> + case {mod_http_upload_opt:rm_on_unregister(NewOpts), + mod_http_upload_opt:rm_on_unregister(OldOpts)} of + {true, false} -> + ejabberd_hooks:add(remove_user, ServerHost, ?MODULE, + remove_user, 50); + {false, true} -> + ejabberd_hooks:delete(remove_user, ServerHost, ?MODULE, + remove_user, 50); + _ -> + ok + end, + NewHosts = gen_mod:get_opt_hosts(NewOpts), + OldHosts = gen_mod:get_opt_hosts(OldOpts), + lists:foreach(fun ejabberd_router:unregister_route/1, OldHosts -- NewHosts), + NewState = init_state(State#state{hosts = NewHosts -- OldHosts}, NewOpts), + {noreply, NewState}; +handle_cast(Request, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Request]), + {noreply, State}. + +-spec handle_info(timeout | _, state()) -> {noreply, state()}. +handle_info({route, #iq{lang = Lang} = Packet}, State) -> + try xmpp:decode_els(Packet) of + IQ -> + {Reply, NewState} = case process_iq(IQ, State) of + R when is_record(R, iq) -> + {R, State}; + {R, S} -> + {R, S}; + not_request -> + {none, State} + end, + if Reply /= none -> + ejabberd_router:route(Reply); + true -> + ok + end, + {noreply, NewState} + catch _:{xmpp_codec, Why} -> + Txt = xmpp:io_format_error(Why), + Err = xmpp:err_bad_request(Txt, Lang), + ejabberd_router:route_error(Packet, Err), + {noreply, State} + end; +handle_info({timeout, _TRef, Slot}, State) -> + NewState = del_slot(Slot, State), + {noreply, NewState}; +handle_info(Info, State) -> + ?WARNING_MSG("Unexpected info: ~p", [Info]), + {noreply, State}. + +-spec terminate(normal | shutdown | {shutdown, _} | _, state()) -> ok. +terminate(Reason, #state{server_host = ServerHost, hosts = Hosts}) -> + ?DEBUG("Stopping HTTP upload process for ~ts: ~p", [ServerHost, Reason]), + ejabberd_hooks:delete(remove_user, ServerHost, ?MODULE, remove_user, 50), + lists:foreach(fun ejabberd_router:unregister_route/1, Hosts). + +-spec code_change({down, _} | _, state(), _) -> {ok, state()}. +code_change(_OldVsn, #state{server_host = ServerHost} = State, _Extra) -> + ?DEBUG("Updating HTTP upload process for ~ts", [ServerHost]), + {ok, State}. + +%%-------------------------------------------------------------------- +%% ejabberd_http callback. +%%-------------------------------------------------------------------- +-spec process([binary()], #request{}) + -> {pos_integer(), [{binary(), binary()}], binary()}. +process(LocalPath, #request{method = Method, host = Host, ip = IP}) + when length(LocalPath) < 3, + Method == 'PUT' orelse + Method == 'GET' orelse + Method == 'HEAD' -> + ?DEBUG("Rejecting ~ts request from ~ts for ~ts: Too few path components", + [Method, encode_addr(IP), Host]), + http_response(404); +process(_LocalPath, #request{method = 'PUT', host = Host, ip = IP, + length = Length} = Request0) -> + Request = Request0#request{host = redecode_url(Host)}, + {Proc, Slot} = parse_http_request(Request), + try gen_server:call(Proc, {use_slot, Slot, Length}, ?CALL_TIMEOUT) of + {ok, Path, FileMode, DirMode, GetPrefix, Thumbnail, CustomHeaders} -> + ?DEBUG("Storing file from ~ts for ~ts: ~ts", + [encode_addr(IP), Host, Path]), + case store_file(Path, Request, FileMode, DirMode, + GetPrefix, Slot, Thumbnail) of + ok -> + http_response(201, CustomHeaders); + {ok, Headers, OutData} -> + http_response(201, ejabberd_http:apply_custom_headers(Headers, CustomHeaders), OutData); + {error, closed} -> + ?DEBUG("Cannot store file ~ts from ~ts for ~ts: connection closed", + [Path, encode_addr(IP), Host]), + http_response(404); + {error, Error} -> + ?ERROR_MSG("Cannot store file ~ts from ~ts for ~ts: ~ts", + [Path, encode_addr(IP), Host, format_error(Error)]), + http_response(500) + end; + {error, size_mismatch} -> + ?WARNING_MSG("Rejecting file ~ts from ~ts for ~ts: Unexpected size (~B)", + [lists:last(Slot), encode_addr(IP), Host, Length]), + http_response(413); + {error, invalid_slot} -> + ?WARNING_MSG("Rejecting file ~ts from ~ts for ~ts: Invalid slot", + [lists:last(Slot), encode_addr(IP), Host]), + http_response(403) + catch + exit:{noproc, _} -> + ?WARNING_MSG("Cannot handle PUT request from ~ts for ~ts: " + "Upload not configured for this host", + [encode_addr(IP), Host]), + http_response(404); + _:Error -> + ?ERROR_MSG("Cannot handle PUT request from ~ts for ~ts: ~p", + [encode_addr(IP), Host, Error]), + http_response(500) + end; +process(_LocalPath, #request{method = Method, host = Host, ip = IP} = Request0) + when Method == 'GET'; + Method == 'HEAD' -> + Request = Request0#request{host = redecode_url(Host)}, + {Proc, [_UserDir, _RandDir, FileName] = Slot} = parse_http_request(Request), + try gen_server:call(Proc, get_conf, ?CALL_TIMEOUT) of + {ok, DocRoot, CustomHeaders} -> + Path = str:join([DocRoot | Slot], <<$/>>), + case file:open(Path, [read]) of + {ok, Fd} -> + file:close(Fd), + ?INFO_MSG("Serving ~ts to ~ts", [Path, encode_addr(IP)]), + ContentType = guess_content_type(FileName), + Headers1 = case ContentType of + <<"image/", _SubType/binary>> -> []; + <<"text/", _SubType/binary>> -> []; + _ -> + [{<<"Content-Disposition">>, + <<"attachment; filename=", + $", FileName/binary, $">>}] + end, + Headers2 = [{<<"Content-Type">>, ContentType} | Headers1], + Headers3 = ejabberd_http:apply_custom_headers(Headers2, CustomHeaders), + http_response(200, Headers3, {file, Path}); + {error, eacces} -> + ?WARNING_MSG("Cannot serve ~ts to ~ts: Permission denied", + [Path, encode_addr(IP)]), + http_response(403); + {error, enoent} -> + ?WARNING_MSG("Cannot serve ~ts to ~ts: No such file", + [Path, encode_addr(IP)]), + http_response(404); + {error, eisdir} -> + ?WARNING_MSG("Cannot serve ~ts to ~ts: Is a directory", + [Path, encode_addr(IP)]), + http_response(404); + {error, Error} -> + ?WARNING_MSG("Cannot serve ~ts to ~ts: ~ts", + [Path, encode_addr(IP), format_error(Error)]), + http_response(500) + end + catch + exit:{noproc, _} -> + ?WARNING_MSG("Cannot handle ~ts request from ~ts for ~ts: " + "Upload not configured for this host", + [Method, encode_addr(IP), Host]), + http_response(404); + _:Error -> + ?ERROR_MSG("Cannot handle ~ts request from ~ts for ~ts: ~p", + [Method, encode_addr(IP), Host, Error]), + http_response(500) + end; +process(_LocalPath, #request{method = 'OPTIONS', host = Host, + ip = IP} = Request) -> + ?DEBUG("Responding to OPTIONS request from ~ts for ~ts", + [encode_addr(IP), Host]), + {Proc, _Slot} = parse_http_request(Request), + try gen_server:call(Proc, get_conf, ?CALL_TIMEOUT) of + {ok, _DocRoot, CustomHeaders} -> + AllowHeader = {<<"Allow">>, <<"OPTIONS, HEAD, GET, PUT">>}, + http_response(200, ejabberd_http:apply_custom_headers([AllowHeader], CustomHeaders)) + catch + exit:{noproc, _} -> + ?WARNING_MSG("Cannot handle OPTIONS request from ~ts for ~ts: " + "Upload not configured for this host", + [encode_addr(IP), Host]), + http_response(404); + _:Error -> + ?ERROR_MSG("Cannot handle OPTIONS request from ~ts for ~ts: ~p", + [encode_addr(IP), Host, Error]), + http_response(500) + end; +process(_LocalPath, #request{method = Method, host = Host, ip = IP}) -> + ?DEBUG("Rejecting ~ts request from ~ts for ~ts", + [Method, encode_addr(IP), Host]), + http_response(405, [{<<"Allow">>, <<"OPTIONS, HEAD, GET, PUT">>}]). + +%%-------------------------------------------------------------------- +%% State initialization +%%-------------------------------------------------------------------- +-spec init_state(binary(), [binary()], gen_mod:opts()) -> state(). +init_state(ServerHost, Hosts, Opts) -> + init_state(#state{server_host = ServerHost, hosts = Hosts}, Opts). + +-spec init_state(state(), gen_mod:opts()) -> state(). +init_state(#state{server_host = ServerHost, hosts = Hosts} = State, Opts) -> + Name = mod_http_upload_opt:name(Opts), + Access = mod_http_upload_opt:access(Opts), + MaxSize = mod_http_upload_opt:max_size(Opts), + SecretLength = mod_http_upload_opt:secret_length(Opts), + JIDinURL = mod_http_upload_opt:jid_in_url(Opts), + DocRoot = mod_http_upload_opt:docroot(Opts), + FileMode = mod_http_upload_opt:file_mode(Opts), + DirMode = mod_http_upload_opt:dir_mode(Opts), + PutURL = mod_http_upload_opt:put_url(Opts), + GetURL = case mod_http_upload_opt:get_url(Opts) of + undefined -> PutURL; + URL -> URL + end, + ServiceURL = mod_http_upload_opt:service_url(Opts), + Thumbnail = mod_http_upload_opt:thumbnail(Opts), + ExternalSecret = mod_http_upload_opt:external_secret(Opts), + CustomHeaders = mod_http_upload_opt:custom_headers(Opts), + DocRoot1 = expand_home(str:strip(DocRoot, right, $/)), + DocRoot2 = expand_host(DocRoot1, ServerHost), + case DirMode of + undefined -> + ok; + Mode -> + file:change_mode(DocRoot2, Mode) + end, + lists:foreach( + fun(Host) -> + ejabberd_router:register_route(Host, ServerHost) + end, Hosts), + State#state{server_host = ServerHost, hosts = Hosts, name = Name, + access = Access, max_size = MaxSize, + secret_length = SecretLength, jid_in_url = JIDinURL, + file_mode = FileMode, dir_mode = DirMode, + thumbnail = Thumbnail, + docroot = DocRoot2, + put_url = expand_host(str:strip(PutURL, right, $/), ServerHost), + get_url = expand_host(str:strip(GetURL, right, $/), ServerHost), + service_url = ServiceURL, + external_secret = ExternalSecret, + custom_headers = CustomHeaders}. + +%%-------------------------------------------------------------------- +%% Exported utility functions. +%%-------------------------------------------------------------------- +-spec get_proc_name(binary(), atom()) -> atom(). +get_proc_name(ServerHost, ModuleName) -> + PutURL = mod_http_upload_opt:put_url(ServerHost), + get_proc_name(ServerHost, ModuleName, PutURL). + +-spec get_proc_name(binary(), atom(), binary()) -> atom(). +get_proc_name(ServerHost, ModuleName, PutURL) -> + %% Once we depend on OTP >= 20.0, we can use binaries with http_uri. + {ok, _Scheme, _UserInfo, Host0, _Port, Path0, _Query} = + misc:uri_parse(expand_host(PutURL, ServerHost)), + Host = jid:nameprep(iolist_to_binary(Host0)), + Path = str:strip(iolist_to_binary(Path0), right, $/), + ProcPrefix = <>, + gen_mod:get_module_proc(ProcPrefix, ModuleName). + +-spec expand_home(binary()) -> binary(). +expand_home(Input) -> + Home = misc:get_home(), + misc:expand_keyword(<<"@HOME@">>, Input, Home). + +-spec expand_host(binary(), binary()) -> binary(). +expand_host(Input, Host) -> + misc:expand_keyword(<<"@HOST@">>, Input, Host). + +%%-------------------------------------------------------------------- +%% Internal functions. +%%-------------------------------------------------------------------- + +%% XMPP request handling. + +-spec process_iq(iq(), state()) -> {iq(), state()} | iq() | not_request. +process_iq(#iq{type = get, lang = Lang, sub_els = [#disco_info{}]} = IQ, + #state{server_host = ServerHost, name = Name}) -> + AddInfo = ejabberd_hooks:run_fold(disco_info, ServerHost, [], + [ServerHost, ?MODULE, <<"">>, <<"">>]), + xmpp:make_iq_result(IQ, iq_disco_info(ServerHost, Lang, Name, AddInfo)); +process_iq(#iq{type = get, sub_els = [#disco_items{}]} = IQ, _State) -> + xmpp:make_iq_result(IQ, #disco_items{}); +process_iq(#iq{type = get, sub_els = [#vcard_temp{}], lang = Lang} = IQ, + #state{server_host = ServerHost}) -> + VCard = case mod_http_upload_opt:vcard(ServerHost) of + undefined -> + #vcard_temp{fn = <<"ejabberd/mod_http_upload">>, + url = ejabberd_config:get_uri(), + desc = misc:get_descr( + Lang, ?T("ejabberd HTTP Upload service"))}; + V -> + V + end, + xmpp:make_iq_result(IQ, VCard); +process_iq(#iq{type = get, sub_els = [#upload_request{filename = File, + size = Size, + 'content-type' = CType, + xmlns = XMLNS}]} = IQ, + State) -> + process_slot_request(IQ, File, Size, CType, XMLNS, State); +process_iq(#iq{type = get, sub_els = [#upload_request_0{filename = File, + size = Size, + 'content-type' = CType, + xmlns = XMLNS}]} = IQ, + State) -> + process_slot_request(IQ, File, Size, CType, XMLNS, State); +process_iq(#iq{type = T, lang = Lang} = IQ, _State) when T == get; T == set -> + Txt = ?T("No module is handling this query"), + xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)); +process_iq(#iq{}, _State) -> + not_request. + +-spec process_slot_request(iq(), binary(), pos_integer(), binary(), binary(), + state()) -> {iq(), state()} | iq(). +process_slot_request(#iq{lang = Lang, from = From} = IQ, + File, Size, CType, XMLNS, + #state{server_host = ServerHost, + access = Access} = State) -> + case acl:match_rule(ServerHost, Access, From) of + allow -> + ContentType = yield_content_type(CType), + case create_slot(State, From, File, Size, ContentType, XMLNS, + Lang) of + {ok, Slot} -> + Query = make_query_string(Slot, Size, State), + NewState = add_slot(Slot, Size, State), + NewSlot = mk_slot(Slot, State, XMLNS, Query), + {xmpp:make_iq_result(IQ, NewSlot), NewState}; + {ok, PutURL, GetURL} -> + Slot = mk_slot(PutURL, GetURL, XMLNS, <<"">>), + xmpp:make_iq_result(IQ, Slot); + {error, Error} -> + xmpp:make_error(IQ, Error) + end; + deny -> + ?DEBUG("Denying HTTP upload slot request from ~ts", + [jid:encode(From)]), + Txt = ?T("Access denied by service policy"), + xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)) + end. + +-spec create_slot(state(), jid(), binary(), pos_integer(), binary(), binary(), + binary()) + -> {ok, slot()} | {ok, binary(), binary()} | {error, xmpp_element()}. +create_slot(#state{service_url = undefined, max_size = MaxSize}, + JID, File, Size, _ContentType, XMLNS, Lang) + when MaxSize /= infinity, + Size > MaxSize -> + Text = {?T("File larger than ~w bytes"), [MaxSize]}, + ?WARNING_MSG("Rejecting file ~ts from ~ts (too large: ~B bytes)", + [File, jid:encode(JID), Size]), + Error = xmpp:err_not_acceptable(Text, Lang), + Els = xmpp:get_els(Error), + Els1 = [#upload_file_too_large{'max-file-size' = MaxSize, + xmlns = XMLNS} | Els], + Error1 = xmpp:set_els(Error, Els1), + {error, Error1}; +create_slot(#state{service_url = undefined, + jid_in_url = JIDinURL, + secret_length = SecretLength, + server_host = ServerHost, + docroot = DocRoot}, + JID, File, Size, _ContentType, _XMLNS, Lang) -> + UserStr = make_user_string(JID, JIDinURL), + UserDir = <>, + case ejabberd_hooks:run_fold(http_upload_slot_request, ServerHost, allow, + [ServerHost, JID, UserDir, Size, Lang]) of + allow -> + RandStr = p1_rand:get_alphanum_string(SecretLength), + FileStr = make_file_string(File), + ?INFO_MSG("Got HTTP upload slot for ~ts (file: ~ts, size: ~B)", + [jid:encode(JID), File, Size]), + {ok, [UserStr, RandStr, FileStr]}; + deny -> + {error, xmpp:err_service_unavailable()}; + #stanza_error{} = Error -> + {error, Error} + end; +create_slot(#state{service_url = ServiceURL}, + #jid{luser = U, lserver = S} = JID, + File, Size, ContentType, _XMLNS, Lang) -> + Options = [{body_format, binary}, {full_result, false}], + HttpOptions = [{timeout, ?SERVICE_REQUEST_TIMEOUT}], + SizeStr = integer_to_binary(Size), + JidStr = jid:encode({U, S, <<"">>}), + GetRequest = <> = PutURL, + <<"http", _/binary>> = GetURL] -> + ?INFO_MSG("Got HTTP upload slot for ~ts (file: ~ts, size: ~B)", + [jid:encode(JID), File, Size]), + {ok, PutURL, GetURL}; + Lines -> + ?ERROR_MSG("Can't parse data received for ~ts from <~ts>: ~p", + [jid:encode(JID), ServiceURL, Lines]), + Txt = ?T("Failed to parse HTTP response"), + {error, xmpp:err_service_unavailable(Txt, Lang)} + end; + {ok, {402, _Body}} -> + ?WARNING_MSG("Got status code 402 for ~ts from <~ts>", + [jid:encode(JID), ServiceURL]), + {error, xmpp:err_resource_constraint()}; + {ok, {403, _Body}} -> + ?WARNING_MSG("Got status code 403 for ~ts from <~ts>", + [jid:encode(JID), ServiceURL]), + {error, xmpp:err_not_allowed()}; + {ok, {413, _Body}} -> + ?WARNING_MSG("Got status code 413 for ~ts from <~ts>", + [jid:encode(JID), ServiceURL]), + {error, xmpp:err_not_acceptable()}; + {ok, {Code, _Body}} -> + ?ERROR_MSG("Unexpected status code for ~ts from <~ts>: ~B", + [jid:encode(JID), ServiceURL, Code]), + {error, xmpp:err_service_unavailable()}; + {error, Reason} -> + ?ERROR_MSG("Error requesting upload slot for ~ts from <~ts>: ~p", + [jid:encode(JID), ServiceURL, Reason]), + {error, xmpp:err_service_unavailable()} + end. + +-spec add_slot(slot(), pos_integer(), state()) -> state(). +add_slot(Slot, Size, #state{external_secret = <<>>, slots = Slots} = State) -> + TRef = erlang:start_timer(?SLOT_TIMEOUT, self(), Slot), + NewSlots = maps:put(Slot, {Size, TRef}, Slots), + State#state{slots = NewSlots}; +add_slot(_Slot, _Size, State) -> + State. + +-spec get_slot(slot(), state()) -> {ok, {pos_integer(), reference()}} | error. +get_slot(Slot, #state{slots = Slots}) -> + maps:find(Slot, Slots). + +-spec del_slot(slot(), state()) -> state(). +del_slot(Slot, #state{slots = Slots} = State) -> + NewSlots = maps:remove(Slot, Slots), + State#state{slots = NewSlots}. + +-spec mk_slot(slot(), state(), binary(), binary()) -> upload_slot(); + (binary(), binary(), binary(), binary()) -> upload_slot(). +mk_slot(Slot, #state{put_url = PutPrefix, get_url = GetPrefix}, XMLNS, Query) -> + PutURL = str:join([PutPrefix | Slot], <<$/>>), + GetURL = str:join([GetPrefix | Slot], <<$/>>), + mk_slot(PutURL, GetURL, XMLNS, Query); +mk_slot(PutURL, GetURL, XMLNS, Query) -> + PutURL1 = <<(reencode_url(PutURL))/binary, Query/binary>>, + GetURL1 = reencode_url(GetURL), + case XMLNS of + ?NS_HTTP_UPLOAD_0 -> + #upload_slot_0{get = GetURL1, put = PutURL1, xmlns = XMLNS}; + _ -> + #upload_slot{get = GetURL1, put = PutURL1, xmlns = XMLNS} + end. + +reencode_url(UrlString) -> + {ok, _, _, Host, _, _, _} = yconf:parse_uri(misc:url_encode(UrlString)), + HostDecoded = misc:uri_decode(Host), + HostIdna = idna:encode(HostDecoded), + re:replace(UrlString, Host, HostIdna, [{return, binary}]). + +redecode_url(UrlString) -> + {ok, _, _, HostIdna, _, _, _} = yconf:parse_uri(<<"http://", UrlString/binary>>), + HostDecoded = idna:decode(HostIdna), + Host = misc:uri_quote(HostDecoded), + re:replace(UrlString, HostIdna, Host, [{return, binary}]). + +-spec make_user_string(jid(), sha1 | node) -> binary(). +make_user_string(#jid{luser = U, lserver = S}, sha1) -> + str:sha(<>); +make_user_string(#jid{luser = U}, node) -> + replace_special_chars(U). + +-spec make_file_string(binary()) -> binary(). +make_file_string(File) -> + replace_special_chars(File). + +-spec make_query_string(slot(), non_neg_integer(), state()) -> binary(). +make_query_string(Slot, Size, #state{external_secret = Key}) when Key /= <<>> -> + UrlPath = str:join(Slot, <<$/>>), + SizeStr = integer_to_binary(Size), + Data = <>, + HMAC = str:to_hexlist(misc:crypto_hmac(sha256, Key, Data)), + <<"?v=", HMAC/binary>>; +make_query_string(_Slot, _Size, _State) -> + <<>>. + +-spec replace_special_chars(binary()) -> binary(). +replace_special_chars(S) -> + re:replace(S, <<"[^\\p{Xan}_.-]">>, <<$_>>, + [unicode, global, {return, binary}]). + +-spec yield_content_type(binary()) -> binary(). +yield_content_type(<<"">>) -> ?DEFAULT_CONTENT_TYPE; +yield_content_type(Type) -> Type. + +-spec encode_addr(inet:ip_address() | {inet:ip_address(), inet:port_number()} | + undefined) -> binary(). +encode_addr(IP) -> + ejabberd_config:may_hide_data(misc:ip_to_list(IP)). + +-spec iq_disco_info(binary(), binary(), binary(), [xdata()]) -> disco_info(). +iq_disco_info(Host, Lang, Name, AddInfo) -> + Form = case mod_http_upload_opt:max_size(Host) of + infinity -> + AddInfo; + MaxSize -> + lists:foldl( + fun(NS, Acc) -> + Fs = http_upload:encode( + [{'max-file-size', MaxSize}], NS, Lang), + [#xdata{type = result, fields = Fs}|Acc] + end, AddInfo, [?NS_HTTP_UPLOAD_0, ?NS_HTTP_UPLOAD]) + end, + #disco_info{identities = [#identity{category = <<"store">>, + type = <<"file">>, + name = translate:translate(Lang, Name)}], + features = [?NS_HTTP_UPLOAD, + ?NS_HTTP_UPLOAD_0, + ?NS_HTTP_UPLOAD_OLD, + ?NS_VCARD, + ?NS_DISCO_INFO, + ?NS_DISCO_ITEMS], + xdata = Form}. + +%% HTTP request handling. + +-spec parse_http_request(#request{}) -> {atom(), slot()}. +parse_http_request(#request{host = Host0, path = Path}) -> + Host = jid:nameprep(Host0), + PrefixLength = length(Path) - 3, + {ProcURL, Slot} = if PrefixLength > 0 -> + Prefix = lists:sublist(Path, PrefixLength), + {str:join([Host | Prefix], $/), + lists:nthtail(PrefixLength, Path)}; + true -> + {Host, Path} + end, + {gen_mod:get_module_proc(ProcURL, ?MODULE), Slot}. + +-spec store_file(binary(), http_request(), + integer() | undefined, + integer() | undefined, + binary(), slot(), boolean()) + -> ok | {ok, [{binary(), binary()}], binary()} | {error, term()}. +store_file(Path, Request, FileMode, DirMode, GetPrefix, Slot, Thumbnail) -> + case do_store_file(Path, Request, FileMode, DirMode) of + ok when Thumbnail -> + case read_image(Path) of + {ok, Data, MediaInfo} -> + case convert(Data, MediaInfo) of + {ok, #media_info{path = OutPath} = OutMediaInfo} -> + [UserDir, RandDir | _] = Slot, + FileName = filename:basename(OutPath), + URL = str:join([GetPrefix, UserDir, + RandDir, FileName], <<$/>>), + ThumbEl = thumb_el(OutMediaInfo, URL), + {ok, + [{<<"Content-Type">>, + <<"text/xml; charset=utf-8">>}], + fxml:element_to_binary(ThumbEl)}; + pass -> + ok + end; + pass -> + ok + end; + ok -> + ok; + Err -> + Err + end. + +-spec do_store_file(file:filename_all(), http_request(), + integer() | undefined, + integer() | undefined) + -> ok | {error, term()}. +do_store_file(Path, Request, FileMode, DirMode) -> + try + ok = filelib:ensure_dir(Path), + ok = ejabberd_http:recv_file(Request, Path), + if is_integer(FileMode) -> + ok = file:change_mode(Path, FileMode); + FileMode == undefined -> + ok + end, + if is_integer(DirMode) -> + RandDir = filename:dirname(Path), + UserDir = filename:dirname(RandDir), + ok = file:change_mode(RandDir, DirMode), + ok = file:change_mode(UserDir, DirMode); + DirMode == undefined -> + ok + end + catch + _:{badmatch, {error, Error}} -> + {error, Error} + end. + +-spec guess_content_type(binary()) -> binary(). +guess_content_type(FileName) -> + mod_http_fileserver:content_type(FileName, + ?DEFAULT_CONTENT_TYPE, + ?CONTENT_TYPES). + +-spec http_response(100..599) + -> {pos_integer(), [{binary(), binary()}], binary()}. +http_response(Code) -> + http_response(Code, []). + +-spec http_response(100..599, [{binary(), binary()}]) + -> {pos_integer(), [{binary(), binary()}], binary()}. +http_response(Code, ExtraHeaders) -> + Message = <<(code_to_message(Code))/binary, $\n>>, + http_response(Code, ExtraHeaders, Message). + +-type http_body() :: binary() | {file, file:filename_all()}. +-spec http_response(100..599, [{binary(), binary()}], http_body()) + -> {pos_integer(), [{binary(), binary()}], http_body()}. +http_response(Code, ExtraHeaders, Body) -> + Headers = case proplists:is_defined(<<"Content-Type">>, ExtraHeaders) of + true -> + ExtraHeaders; + false -> + [{<<"Content-Type">>, <<"text/plain">>} | ExtraHeaders] + end, + {Code, Headers, Body}. + +-spec code_to_message(100..599) -> binary(). +code_to_message(201) -> <<"Upload successful.">>; +code_to_message(403) -> <<"Forbidden.">>; +code_to_message(404) -> <<"Not found.">>; +code_to_message(405) -> <<"Method not allowed.">>; +code_to_message(413) -> <<"File size doesn't match requested size.">>; +code_to_message(500) -> <<"Internal server error.">>; +code_to_message(_Code) -> <<"">>. + +-spec format_error(atom()) -> string(). +format_error(Reason) -> + case file:format_error(Reason) of + "unknown POSIX error" -> + case inet:format_error(Reason) of + "unknown POSIX error" -> + atom_to_list(Reason); + Txt -> + Txt + end; + Txt -> + Txt + end. + +%%-------------------------------------------------------------------- +%% Image manipulation stuff. +%%-------------------------------------------------------------------- +-spec read_image(binary()) -> {ok, binary(), media_info()} | pass. +read_image(Path) -> + case file:read_file(Path) of + {ok, Data} -> + case eimp:identify(Data) of + {ok, Info} -> + {ok, Data, + #media_info{ + path = Path, + type = proplists:get_value(type, Info), + width = proplists:get_value(width, Info), + height = proplists:get_value(height, Info)}}; + {error, Why} -> + ?DEBUG("Cannot identify type of ~ts: ~ts", + [Path, eimp:format_error(Why)]), + pass + end; + {error, Reason} -> + ?DEBUG("Failed to read file ~ts: ~ts", + [Path, format_error(Reason)]), + pass + end. + +-spec convert(binary(), media_info()) -> {ok, media_info()} | pass. +convert(InData, #media_info{path = Path, type = T, width = W, height = H} = Info) -> + if W * H >= 25000000 -> + ?DEBUG("The image ~ts is more than 25 Mpix", [Path]), + pass; + W =< 300, H =< 300 -> + {ok, Info}; + true -> + Dir = filename:dirname(Path), + Ext = atom_to_binary(T, latin1), + FileName = <<(p1_rand:get_string())/binary, $., Ext/binary>>, + OutPath = filename:join(Dir, FileName), + {W1, H1} = if W > H -> {300, round(H*300/W)}; + H > W -> {round(W*300/H), 300}; + true -> {300, 300} + end, + OutInfo = #media_info{path = OutPath, type = T, width = W1, height = H1}, + case eimp:convert(InData, T, [{scale, {W1, H1}}]) of + {ok, OutData} -> + case file:write_file(OutPath, OutData) of + ok -> + {ok, OutInfo}; + {error, Why} -> + ?ERROR_MSG("Failed to write to ~ts: ~ts", + [OutPath, format_error(Why)]), + pass + end; + {error, Why} -> + ?ERROR_MSG("Failed to convert ~ts to ~ts: ~ts", + [Path, OutPath, eimp:format_error(Why)]), + pass + end + end. + +-spec thumb_el(media_info(), binary()) -> xmlel(). +thumb_el(#media_info{type = T, height = H, width = W}, URI) -> + MimeType = <<"image/", (atom_to_binary(T, latin1))/binary>>, + Thumb = #thumbnail{'media-type' = MimeType, uri = URI, + height = H, width = W}, + xmpp:encode(Thumb). + +%%-------------------------------------------------------------------- +%% Remove user. +%%-------------------------------------------------------------------- +-spec remove_user(binary(), binary()) -> ok. +remove_user(User, Server) -> + ServerHost = jid:nameprep(Server), + DocRoot = mod_http_upload_opt:docroot(ServerHost), + JIDinURL = mod_http_upload_opt:jid_in_url(ServerHost), + DocRoot1 = expand_host(expand_home(DocRoot), ServerHost), + UserStr = make_user_string(jid:make(User, Server), JIDinURL), + UserDir = str:join([DocRoot1, UserStr], <<$/>>), + case misc:delete_dir(UserDir) of + ok -> + ?INFO_MSG("Removed HTTP upload directory of ~ts@~ts", [User, Server]); + {error, enoent} -> + ?DEBUG("Found no HTTP upload directory of ~ts@~ts", [User, Server]); + {error, Error} -> + ?ERROR_MSG("Cannot remove HTTP upload directory of ~ts@~ts: ~ts", + [User, Server, format_error(Error)]) + end, + ok. diff --git a/src/mod_http_upload_opt.erl b/src/mod_http_upload_opt.erl new file mode 100644 index 000000000..8590a38a1 --- /dev/null +++ b/src/mod_http_upload_opt.erl @@ -0,0 +1,132 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_http_upload_opt). + +-export([access/1]). +-export([custom_headers/1]). +-export([dir_mode/1]). +-export([docroot/1]). +-export([external_secret/1]). +-export([file_mode/1]). +-export([get_url/1]). +-export([host/1]). +-export([hosts/1]). +-export([jid_in_url/1]). +-export([max_size/1]). +-export([name/1]). +-export([put_url/1]). +-export([rm_on_unregister/1]). +-export([secret_length/1]). +-export([service_url/1]). +-export([thumbnail/1]). +-export([vcard/1]). + +-spec access(gen_mod:opts() | global | binary()) -> 'local' | acl:acl(). +access(Opts) when is_map(Opts) -> + gen_mod:get_opt(access, Opts); +access(Host) -> + gen_mod:get_module_opt(Host, mod_http_upload, access). + +-spec custom_headers(gen_mod:opts() | global | binary()) -> [{binary(),binary()}]. +custom_headers(Opts) when is_map(Opts) -> + gen_mod:get_opt(custom_headers, Opts); +custom_headers(Host) -> + gen_mod:get_module_opt(Host, mod_http_upload, custom_headers). + +-spec dir_mode(gen_mod:opts() | global | binary()) -> 'undefined' | non_neg_integer(). +dir_mode(Opts) when is_map(Opts) -> + gen_mod:get_opt(dir_mode, Opts); +dir_mode(Host) -> + gen_mod:get_module_opt(Host, mod_http_upload, dir_mode). + +-spec docroot(gen_mod:opts() | global | binary()) -> binary(). +docroot(Opts) when is_map(Opts) -> + gen_mod:get_opt(docroot, Opts); +docroot(Host) -> + gen_mod:get_module_opt(Host, mod_http_upload, docroot). + +-spec external_secret(gen_mod:opts() | global | binary()) -> binary(). +external_secret(Opts) when is_map(Opts) -> + gen_mod:get_opt(external_secret, Opts); +external_secret(Host) -> + gen_mod:get_module_opt(Host, mod_http_upload, external_secret). + +-spec file_mode(gen_mod:opts() | global | binary()) -> 'undefined' | non_neg_integer(). +file_mode(Opts) when is_map(Opts) -> + gen_mod:get_opt(file_mode, Opts); +file_mode(Host) -> + gen_mod:get_module_opt(Host, mod_http_upload, file_mode). + +-spec get_url(gen_mod:opts() | global | binary()) -> 'undefined' | binary(). +get_url(Opts) when is_map(Opts) -> + gen_mod:get_opt(get_url, Opts); +get_url(Host) -> + gen_mod:get_module_opt(Host, mod_http_upload, get_url). + +-spec host(gen_mod:opts() | global | binary()) -> binary(). +host(Opts) when is_map(Opts) -> + gen_mod:get_opt(host, Opts); +host(Host) -> + gen_mod:get_module_opt(Host, mod_http_upload, host). + +-spec hosts(gen_mod:opts() | global | binary()) -> [binary()]. +hosts(Opts) when is_map(Opts) -> + gen_mod:get_opt(hosts, Opts); +hosts(Host) -> + gen_mod:get_module_opt(Host, mod_http_upload, hosts). + +-spec jid_in_url(gen_mod:opts() | global | binary()) -> 'node' | 'sha1'. +jid_in_url(Opts) when is_map(Opts) -> + gen_mod:get_opt(jid_in_url, Opts); +jid_in_url(Host) -> + gen_mod:get_module_opt(Host, mod_http_upload, jid_in_url). + +-spec max_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +max_size(Opts) when is_map(Opts) -> + gen_mod:get_opt(max_size, Opts); +max_size(Host) -> + gen_mod:get_module_opt(Host, mod_http_upload, max_size). + +-spec name(gen_mod:opts() | global | binary()) -> binary(). +name(Opts) when is_map(Opts) -> + gen_mod:get_opt(name, Opts); +name(Host) -> + gen_mod:get_module_opt(Host, mod_http_upload, name). + +-spec put_url(gen_mod:opts() | global | binary()) -> binary(). +put_url(Opts) when is_map(Opts) -> + gen_mod:get_opt(put_url, Opts); +put_url(Host) -> + gen_mod:get_module_opt(Host, mod_http_upload, put_url). + +-spec rm_on_unregister(gen_mod:opts() | global | binary()) -> boolean(). +rm_on_unregister(Opts) when is_map(Opts) -> + gen_mod:get_opt(rm_on_unregister, Opts); +rm_on_unregister(Host) -> + gen_mod:get_module_opt(Host, mod_http_upload, rm_on_unregister). + +-spec secret_length(gen_mod:opts() | global | binary()) -> 1..1114111. +secret_length(Opts) when is_map(Opts) -> + gen_mod:get_opt(secret_length, Opts); +secret_length(Host) -> + gen_mod:get_module_opt(Host, mod_http_upload, secret_length). + +-spec service_url(gen_mod:opts() | global | binary()) -> 'undefined' | binary(). +service_url(Opts) when is_map(Opts) -> + gen_mod:get_opt(service_url, Opts); +service_url(Host) -> + gen_mod:get_module_opt(Host, mod_http_upload, service_url). + +-spec thumbnail(gen_mod:opts() | global | binary()) -> boolean(). +thumbnail(Opts) when is_map(Opts) -> + gen_mod:get_opt(thumbnail, Opts); +thumbnail(Host) -> + gen_mod:get_module_opt(Host, mod_http_upload, thumbnail). + +-spec vcard(gen_mod:opts() | global | binary()) -> 'undefined' | tuple(). +vcard(Opts) when is_map(Opts) -> + gen_mod:get_opt(vcard, Opts); +vcard(Host) -> + gen_mod:get_module_opt(Host, mod_http_upload, vcard). + diff --git a/src/mod_http_upload_quota.erl b/src/mod_http_upload_quota.erl new file mode 100644 index 000000000..76b99ca05 --- /dev/null +++ b/src/mod_http_upload_quota.erl @@ -0,0 +1,386 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_http_upload_quota.erl +%%% Author : Holger Weiss +%%% Purpose : Quota management for HTTP File Upload (XEP-0363) +%%% Created : 15 Oct 2015 by Holger Weiss +%%% +%%% +%%% ejabberd, Copyright (C) 2015-2025 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_http_upload_quota). +-author('holger@zedat.fu-berlin.de'). + +-define(TIMEOUT, timer:hours(24)). +-define(FORMAT(Error), file:format_error(Error)). + +-behaviour(gen_server). +-behaviour(gen_mod). + +%% gen_mod/supervisor callbacks. +-export([start/2, + stop/1, + depends/2, + mod_doc/0, + mod_opt_type/1, + mod_options/1]). + +%% gen_server callbacks. +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). + +%% ejabberd_hooks callback. +-export([handle_slot_request/6]). + +-include_lib("xmpp/include/jid.hrl"). +-include("logger.hrl"). +-include("translate.hrl"). +-include_lib("kernel/include/file.hrl"). + +-record(state, + {server_host :: binary(), + access_soft_quota :: atom(), + access_hard_quota :: atom(), + max_days :: pos_integer() | infinity, + docroot :: binary(), + disk_usage = #{} :: disk_usage(), + timer :: reference() | undefined}). + +-type disk_usage() :: #{{binary(), binary()} => non_neg_integer()}. +-type state() :: #state{}. + +%%-------------------------------------------------------------------- +%% gen_mod/supervisor callbacks. +%%-------------------------------------------------------------------- +start(ServerHost, Opts) -> + Proc = mod_http_upload:get_proc_name(ServerHost, ?MODULE), + gen_mod:start_child(?MODULE, ServerHost, Opts, Proc). + +stop(ServerHost) -> + Proc = mod_http_upload:get_proc_name(ServerHost, ?MODULE), + gen_mod:stop_child(Proc). + +-spec mod_opt_type(atom()) -> econf:validator(). +mod_opt_type(access_soft_quota) -> + econf:shaper(); +mod_opt_type(access_hard_quota) -> + econf:shaper(); +mod_opt_type(max_days) -> + econf:pos_int(infinity). + +-spec mod_options(binary()) -> [{atom(), any()}]. +mod_options(_) -> + [{access_soft_quota, soft_upload_quota}, + {access_hard_quota, hard_upload_quota}, + {max_days, infinity}]. + +mod_doc() -> + #{desc => + [?T("This module adds quota support for mod_http_upload."), "", + ?T("This module depends on _`mod_http_upload`_.")], + opts => + [{max_days, + #{value => ?T("Days"), + desc => + ?T("If a number larger than zero is specified, " + "any files (and directories) older than this " + "number of days are removed from the subdirectories " + "of the 'docroot' directory, once per day. " + "The default value is 'infinity'.")}}, + {access_soft_quota, + #{value => ?T("AccessName"), + desc => + ?T("This option defines which access rule is used " + "to specify the \"soft quota\" for the matching JIDs. " + "That rule must yield a positive number of megabytes " + "for any JID that is supposed to have a quota limit. " + "See the description of the 'access_hard_quota' option " + "for details. The default value is 'soft_upload_quota'.")}}, + {access_hard_quota, + #{value => ?T("AccessName"), + desc => + ?T("This option defines which access rule is used to " + "specify the \"hard quota\" for the matching JIDs. " + "That rule must yield a positive number for any " + "JID that is supposed to have a quota limit. " + "This is the number of megabytes a corresponding " + "user may upload. When this threshold is exceeded, " + "ejabberd deletes the oldest files uploaded by that " + "user until their disk usage equals or falls below " + "the specified soft quota (see also option 'access_soft_quota'). " + "The default value is 'hard_upload_quota'.")}}], + example => + [{?T("Notice it's not necessary to specify the " + "'access_hard_quota' and 'access_soft_quota' options in order " + "to use the quota feature. You can stick to the default names " + "and just specify access rules such as those in this example:"), + ["shaper_rules:", + " soft_upload_quota:", + " 1000: all # MiB", + " hard_upload_quota:", + " 1100: all # MiB", + "", + "modules:", + " mod_http_upload: {}", + " mod_http_upload_quota:", + " max_days: 100"]}]}. + +-spec depends(binary(), gen_mod:opts()) -> [{module(), hard | soft}]. +depends(_Host, _Opts) -> + [{mod_http_upload, hard}]. + +%%-------------------------------------------------------------------- +%% gen_server callbacks. +%%-------------------------------------------------------------------- +-spec init(list()) -> {ok, state()}. +init([ServerHost|_]) -> + process_flag(trap_exit, true), + Opts = gen_mod:get_module_opts(ServerHost, ?MODULE), + AccessSoftQuota = mod_http_upload_quota_opt:access_soft_quota(Opts), + AccessHardQuota = mod_http_upload_quota_opt:access_hard_quota(Opts), + MaxDays = mod_http_upload_quota_opt:max_days(Opts), + DocRoot1 = mod_http_upload_opt:docroot(ServerHost), + DocRoot2 = mod_http_upload:expand_home(str:strip(DocRoot1, right, $/)), + DocRoot3 = mod_http_upload:expand_host(DocRoot2, ServerHost), + Timer = if MaxDays == infinity -> undefined; + true -> + Timeout = p1_rand:uniform(?TIMEOUT div 2), + erlang:send_after(Timeout, self(), sweep) + end, + ejabberd_hooks:add(http_upload_slot_request, ServerHost, ?MODULE, + handle_slot_request, 50), + {ok, #state{server_host = ServerHost, + access_soft_quota = AccessSoftQuota, + access_hard_quota = AccessHardQuota, + max_days = MaxDays, + docroot = DocRoot3, + timer = Timer}}. + +-spec handle_call(_, {pid(), _}, state()) -> {noreply, state()}. +handle_call(Request, From, State) -> + ?ERROR_MSG("Unexpected request from ~p: ~p", [From, Request]), + {noreply, State}. + +-spec handle_cast(_, state()) -> {noreply, state()}. +handle_cast({handle_slot_request, #jid{user = U, server = S} = JID, Path, Size}, + #state{server_host = ServerHost, + access_soft_quota = AccessSoftQuota, + access_hard_quota = AccessHardQuota, + disk_usage = DiskUsage} = State) -> + HardQuota = case ejabberd_shaper:match(ServerHost, AccessHardQuota, JID) of + Hard when is_integer(Hard), Hard > 0 -> + Hard * 1024 * 1024; + _ -> + 0 + end, + SoftQuota = case ejabberd_shaper:match(ServerHost, AccessSoftQuota, JID) of + Soft when is_integer(Soft), Soft > 0 -> + Soft * 1024 * 1024; + _ -> + 0 + end, + OldSize = case maps:find({U, S}, DiskUsage) of + {ok, Value} -> + Value; + error -> + undefined + end, + NewSize = case {HardQuota, SoftQuota} of + {0, 0} -> + ?DEBUG("No quota specified for ~ts", + [jid:encode(JID)]), + undefined; + {0, _} -> + ?WARNING_MSG("No hard quota specified for ~ts", + [jid:encode(JID)]), + enforce_quota(Path, Size, OldSize, SoftQuota, SoftQuota); + {_, 0} -> + ?WARNING_MSG("No soft quota specified for ~ts", + [jid:encode(JID)]), + enforce_quota(Path, Size, OldSize, HardQuota, HardQuota); + _ when SoftQuota > HardQuota -> + ?WARNING_MSG("Bad quota for ~ts (soft: ~p, hard: ~p)", + [jid:encode(JID), + SoftQuota, HardQuota]), + enforce_quota(Path, Size, OldSize, SoftQuota, SoftQuota); + _ -> + ?DEBUG("Enforcing quota for ~ts", + [jid:encode(JID)]), + enforce_quota(Path, Size, OldSize, SoftQuota, HardQuota) + end, + NewDiskUsage = if is_integer(NewSize) -> + maps:put({U, S}, NewSize, DiskUsage); + true -> + DiskUsage + end, + {noreply, State#state{disk_usage = NewDiskUsage}}; +handle_cast(Request, State) -> + ?ERROR_MSG("Unexpected request: ~p", [Request]), + {noreply, State}. + +-spec handle_info(_, state()) -> {noreply, state()}. +handle_info(sweep, #state{server_host = ServerHost, + docroot = DocRoot, + max_days = MaxDays} = State) + when is_integer(MaxDays), MaxDays > 0 -> + ?DEBUG("Got 'sweep' message for ~ts", [ServerHost]), + Timer = erlang:send_after(?TIMEOUT, self(), sweep), + case file:list_dir(DocRoot) of + {ok, Entries} -> + BackThen = secs_since_epoch() - (MaxDays * 86400), + DocRootS = binary_to_list(DocRoot), + PathNames = lists:map(fun(Entry) -> + DocRootS ++ "/" ++ Entry + end, Entries), + UserDirs = lists:filter(fun filelib:is_dir/1, PathNames), + lists:foreach(fun(UserDir) -> + delete_old_files(UserDir, BackThen) + end, UserDirs); + {error, Error} -> + ?ERROR_MSG("Cannot open document root ~ts: ~ts", + [DocRoot, ?FORMAT(Error)]) + end, + {noreply, State#state{timer = Timer}}; +handle_info(Info, State) -> + ?ERROR_MSG("Unexpected info: ~p", [Info]), + {noreply, State}. + +-spec terminate(normal | shutdown | {shutdown, _} | _, state()) -> ok. +terminate(Reason, #state{server_host = ServerHost, timer = Timer}) -> + ?DEBUG("Stopping upload quota process for ~ts: ~p", [ServerHost, Reason]), + ejabberd_hooks:delete(http_upload_slot_request, ServerHost, ?MODULE, + handle_slot_request, 50), + misc:cancel_timer(Timer). + +-spec code_change({down, _} | _, state(), _) -> {ok, state()}. +code_change(_OldVsn, #state{server_host = ServerHost} = State, _Extra) -> + ?DEBUG("Updating upload quota process for ~ts", [ServerHost]), + {ok, State}. + +%%-------------------------------------------------------------------- +%% ejabberd_hooks callback. +%%-------------------------------------------------------------------- +-spec handle_slot_request(allow | deny, binary(), jid(), binary(), + non_neg_integer(), binary()) -> allow | deny. +handle_slot_request(allow, ServerHost, JID, Path, Size, _Lang) -> + Proc = mod_http_upload:get_proc_name(ServerHost, ?MODULE), + gen_server:cast(Proc, {handle_slot_request, JID, Path, Size}), + allow; +handle_slot_request(Acc, _ServerHost, _JID, _Path, _Size, _Lang) -> Acc. + +%%-------------------------------------------------------------------- +%% Internal functions. +%%-------------------------------------------------------------------- +-spec enforce_quota(file:filename_all(), non_neg_integer(), + non_neg_integer() | undefined, non_neg_integer(), + non_neg_integer()) + -> non_neg_integer(). +enforce_quota(_UserDir, SlotSize, OldSize, _MinSize, MaxSize) + when is_integer(OldSize), OldSize + SlotSize =< MaxSize -> + OldSize + SlotSize; +enforce_quota(UserDir, SlotSize, _OldSize, MinSize, MaxSize) -> + Files = lists:sort(fun({_PathA, _SizeA, TimeA}, {_PathB, _SizeB, TimeB}) -> + TimeA > TimeB + end, gather_file_info(UserDir)), + {DelFiles, OldSize, NewSize} = + lists:foldl(fun({_Path, Size, _Time}, {[], AccSize, AccSize}) + when AccSize + Size + SlotSize =< MinSize -> + {[], AccSize + Size, AccSize + Size}; + ({Path, Size, _Time}, {[], AccSize, AccSize}) -> + {[Path], AccSize + Size, AccSize}; + ({Path, Size, _Time}, {AccFiles, AccSize, NewSize}) -> + {[Path | AccFiles], AccSize + Size, NewSize} + end, {[], 0, 0}, Files), + if OldSize + SlotSize > MaxSize -> + lists:foreach(fun del_file_and_dir/1, DelFiles), + file:del_dir(UserDir), % In case it's empty, now. + NewSize + SlotSize; + true -> + OldSize + SlotSize + end. + +-spec delete_old_files(file:filename_all(), integer()) -> ok. +delete_old_files(UserDir, CutOff) -> + FileInfo = gather_file_info(UserDir), + case [Path || {Path, _Size, Time} <- FileInfo, Time < CutOff] of + [] -> + ok; + OldFiles -> + lists:foreach(fun del_file_and_dir/1, OldFiles), + file:del_dir(UserDir) % In case it's empty, now. + end. + +-spec gather_file_info(file:filename_all()) + -> [{binary(), non_neg_integer(), non_neg_integer()}]. +gather_file_info(Dir) when is_binary(Dir) -> + gather_file_info(binary_to_list(Dir)); +gather_file_info(Dir) -> + case file:list_dir(Dir) of + {ok, Entries} -> + lists:foldl(fun(Entry, Acc) -> + Path = Dir ++ "/" ++ Entry, + case file:read_file_info(Path, + [{time, posix}]) of + {ok, #file_info{type = directory}} -> + gather_file_info(Path) ++ Acc; + {ok, #file_info{type = regular, + mtime = Time, + size = Size}} -> + [{Path, Size, Time} | Acc]; + {ok, _Info} -> + ?DEBUG("Won't stat(2) non-regular file ~ts", + [Path]), + Acc; + {error, Error} -> + ?ERROR_MSG("Cannot stat(2) ~ts: ~ts", + [Path, ?FORMAT(Error)]), + Acc + end + end, [], Entries); + {error, enoent} -> + ?DEBUG("Directory ~ts doesn't exist", [Dir]), + []; + {error, Error} -> + ?ERROR_MSG("Cannot open directory ~ts: ~ts", [Dir, ?FORMAT(Error)]), + [] + end. + +-spec del_file_and_dir(file:name_all()) -> ok. +del_file_and_dir(File) -> + case file:delete(File) of + ok -> + ?INFO_MSG("Removed ~ts", [File]), + Dir = filename:dirname(File), + case file:del_dir(Dir) of + ok -> + ?DEBUG("Removed ~ts", [Dir]); + {error, Error} -> + ?DEBUG("Cannot remove ~ts: ~ts", [Dir, ?FORMAT(Error)]) + end; + {error, Error} -> + ?WARNING_MSG("Cannot remove ~ts: ~ts", [File, ?FORMAT(Error)]) + end. + +-spec secs_since_epoch() -> non_neg_integer(). +secs_since_epoch() -> + {MegaSecs, Secs, _MicroSecs} = os:timestamp(), + MegaSecs * 1000000 + Secs. diff --git a/src/mod_http_upload_quota_opt.erl b/src/mod_http_upload_quota_opt.erl new file mode 100644 index 000000000..acf739fab --- /dev/null +++ b/src/mod_http_upload_quota_opt.erl @@ -0,0 +1,27 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_http_upload_quota_opt). + +-export([access_hard_quota/1]). +-export([access_soft_quota/1]). +-export([max_days/1]). + +-spec access_hard_quota(gen_mod:opts() | global | binary()) -> atom() | [ejabberd_shaper:shaper_rule()]. +access_hard_quota(Opts) when is_map(Opts) -> + gen_mod:get_opt(access_hard_quota, Opts); +access_hard_quota(Host) -> + gen_mod:get_module_opt(Host, mod_http_upload_quota, access_hard_quota). + +-spec access_soft_quota(gen_mod:opts() | global | binary()) -> atom() | [ejabberd_shaper:shaper_rule()]. +access_soft_quota(Opts) when is_map(Opts) -> + gen_mod:get_opt(access_soft_quota, Opts); +access_soft_quota(Host) -> + gen_mod:get_module_opt(Host, mod_http_upload_quota, access_soft_quota). + +-spec max_days(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +max_days(Opts) when is_map(Opts) -> + gen_mod:get_opt(max_days, Opts); +max_days(Host) -> + gen_mod:get_module_opt(Host, mod_http_upload_quota, max_days). + diff --git a/src/mod_ip_blacklist.erl b/src/mod_ip_blacklist.erl deleted file mode 100644 index 6225343c0..000000000 --- a/src/mod_ip_blacklist.erl +++ /dev/null @@ -1,131 +0,0 @@ -%%%---------------------------------------------------------------------- -%%% File : mod_ip_blacklist.erl -%%% Author : Mickael Remond -%%% Purpose : Download blacklists from ProcessOne -%%% Created : 5 May 2008 by Mickael Remond -%%% Usage : Add the following line in modules section of ejabberd.cfg: -%%% {mod_ip_blacklist, []} -%%% -%%% -%%% ejabberd, Copyright (C) 2002-2015 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_ip_blacklist). - --author('mremond@process-one.net'). - --behaviour(gen_mod). - -%% API: --export([start/2, preinit/2, init/1, stop/1]). - --export([update_bl_c2s/0]). - -%% Hooks: --export([is_ip_in_c2s_blacklist/3]). - --include("ejabberd.hrl"). --include("logger.hrl"). - --define(PROCNAME, ?MODULE). - --define(BLC2S, - <<"http://xaai.process-one.net/bl_c2s.txt">>). - --define(UPDATE_INTERVAL, 6). - --record(state, {timer}). - -%% Start once for all vhost --record(bl_c2s, {ip = <<"">> :: binary()}). - -start(_Host, _Opts) -> - Pid = spawn(?MODULE, preinit, [self(), #state{}]), - receive {ok, Pid, PreinitResult} -> PreinitResult end. - -preinit(Parent, State) -> - Pid = self(), - try register(?PROCNAME, Pid) of - true -> Parent ! {ok, Pid, true}, init(State) - catch - error:_ -> Parent ! {ok, Pid, true} - end. - -%% TODO: -stop(_Host) -> ok. - -init(State) -> - ets:new(bl_c2s, - [named_table, public, {keypos, #bl_c2s.ip}]), - update_bl_c2s(), - ejabberd_hooks:add(check_bl_c2s, ?MODULE, - is_ip_in_c2s_blacklist, 50), - timer:apply_interval(timer:hours(?UPDATE_INTERVAL), - ?MODULE, update_bl_c2s, []), - loop(State). - -%% Remove timer when stop is received. -loop(_State) -> receive stop -> ok end. - -%% Download blacklist file from ProcessOne XAAI -%% and update the table internal table -%% TODO: Support comment lines starting by % -update_bl_c2s() -> - ?INFO_MSG("Updating C2S Blacklist", []), - case httpc:request(?BLC2S) of - {ok, 200, _Headers, Body} -> - IPs = str:tokens(Body, <<"\n">>), - ets:delete_all_objects(bl_c2s), - lists:foreach(fun (IP) -> - ets:insert(bl_c2s, - #bl_c2s{ip = IP}) - end, - IPs); - {error, Reason} -> - ?ERROR_MSG("Cannot download C2S blacklist file. " - "Reason: ~p", - [Reason]) - end. - -%% Hook is run with: -%% ejabberd_hooks:run_fold(check_bl_c2s, false, [IP]), -%% Return: false: IP not blacklisted -%% true: IP is blacklisted -%% IPV4 IP tuple: -is_ip_in_c2s_blacklist(_Val, IP, Lang) when is_tuple(IP) -> - BinaryIP = jlib:ip_to_list(IP), - case ets:lookup(bl_c2s, BinaryIP) of - [] -> %% Not in blacklist - false; - [_] -> - LogReason = io_lib:fwrite( - "This IP address is blacklisted in ~s", - [?BLC2S]), - ReasonT = io_lib:fwrite( - translate:translate( - Lang, - <<"This IP address is blacklisted in ~s">>), - [?BLC2S]), - {stop, {true, LogReason, ReasonT}} - end; -is_ip_in_c2s_blacklist(_Val, _IP, _Lang) -> false. - -%% TODO: -%% - For now, we do not kick user already logged on a given IP after -%% we update the blacklist. - diff --git a/src/mod_irc.erl b/src/mod_irc.erl deleted file mode 100644 index 2cc57786c..000000000 --- a/src/mod_irc.erl +++ /dev/null @@ -1,1347 +0,0 @@ -%%%---------------------------------------------------------------------- -%%% File : mod_irc.erl -%%% Author : Alexey Shchepin -%%% Purpose : IRC transport -%%% Created : 15 Feb 2003 by Alexey Shchepin -%%% -%%% -%%% ejabberd, Copyright (C) 2002-2015 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_irc). - --author('alexey@process-one.net'). - --behaviour(gen_server). - --behaviour(gen_mod). - -%% API --export([start_link/2, start/2, stop/1, export/1, import/1, - import/3, closed_connection/3, get_connection_params/3]). - -%% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, - handle_info/2, terminate/2, code_change/3]). - --include("ejabberd.hrl"). --include("logger.hrl"). - --include("jlib.hrl"). - --include("adhoc.hrl"). - --define(DEFAULT_IRC_ENCODING, <<"iso8859-1">>). - --define(DEFAULT_IRC_PORT, 6667). - --define(POSSIBLE_ENCODINGS, - [<<"koi8-r">>, <<"iso8859-1">>, <<"iso8859-2">>, - <<"utf-8">>, <<"utf-8+latin-1">>]). - --type conn_param() :: {binary(), binary(), inet:port_number(), binary()} | - {binary(), binary(), inet:port_number()} | - {binary(), binary()} | - {binary()}. - --record(irc_connection, - {jid_server_host = {#jid{}, <<"">>, <<"">>} :: {jid(), binary(), binary()}, - pid = self() :: pid()}). - --record(irc_custom, - {us_host = {{<<"">>, <<"">>}, <<"">>} :: {{binary(), binary()}, - binary()}, - data = [] :: [{username, binary()} | - {connections_params, [conn_param()]}]}). - --record(state, {host = <<"">> :: binary(), - server_host = <<"">> :: binary(), - access = all :: atom()}). - --define(PROCNAME, ejabberd_mod_irc). - -%%==================================================================== -%% API -%%==================================================================== -%%-------------------------------------------------------------------- -%% Function: start_link() -> {ok,Pid} | ignore | {error,Error} -%% Description: Starts the server -%%-------------------------------------------------------------------- -start_link(Host, Opts) -> - Proc = gen_mod:get_module_proc(Host, ?PROCNAME), - gen_server:start_link({local, Proc}, ?MODULE, - [Host, Opts], []). - -start(Host, Opts) -> - start_supervisor(Host), - Proc = gen_mod:get_module_proc(Host, ?PROCNAME), - ChildSpec = {Proc, {?MODULE, start_link, [Host, Opts]}, - temporary, 1000, worker, [?MODULE]}, - supervisor:start_child(ejabberd_sup, ChildSpec). - -stop(Host) -> - stop_supervisor(Host), - Proc = gen_mod:get_module_proc(Host, ?PROCNAME), - gen_server:call(Proc, stop), - supervisor:delete_child(ejabberd_sup, Proc). - -%%==================================================================== -%% gen_server callbacks -%%==================================================================== - -%%-------------------------------------------------------------------- -%% Function: init(Args) -> {ok, State} | -%% {ok, State, Timeout} | -%% ignore | -%% {stop, Reason} -%% Description: Initiates the server -%%-------------------------------------------------------------------- -init([Host, Opts]) -> - ejabberd:start_app(p1_iconv), - MyHost = gen_mod:get_opt_host(Host, Opts, - <<"irc.@HOST@">>), - case gen_mod:db_type(Opts) of - mnesia -> - mnesia:create_table(irc_custom, - [{disc_copies, [node()]}, - {attributes, record_info(fields, irc_custom)}]), - update_table(); - _ -> ok - end, - Access = gen_mod:get_opt(access, Opts, - fun(A) when is_atom(A) -> A end, - all), - catch ets:new(irc_connection, - [named_table, public, - {keypos, #irc_connection.jid_server_host}]), - ejabberd_router:register_route(MyHost), - {ok, - #state{host = MyHost, server_host = Host, - access = Access}}. - -%%-------------------------------------------------------------------- -%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | -%% {reply, Reply, State, Timeout} | -%% {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, Reply, State} | -%% {stop, Reason, State} -%% Description: Handling call messages -%%-------------------------------------------------------------------- -handle_call(stop, _From, State) -> - {stop, normal, ok, State}. - -%%-------------------------------------------------------------------- -%% Function: handle_cast(Msg, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} -%% Description: Handling cast messages -%%-------------------------------------------------------------------- -handle_cast(_Msg, State) -> {noreply, State}. - -%%-------------------------------------------------------------------- -%% Function: handle_info(Info, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} -%% Description: Handling all non call/cast messages -%%-------------------------------------------------------------------- -handle_info({route, From, To, Packet}, - #state{host = Host, server_host = ServerHost, - access = Access} = - State) -> - case catch do_route(Host, ServerHost, Access, From, To, - Packet) - of - {'EXIT', Reason} -> ?ERROR_MSG("~p", [Reason]); - _ -> ok - end, - {noreply, State}; -handle_info(_Info, State) -> {noreply, State}. - -%%-------------------------------------------------------------------- -%% Function: terminate(Reason, State) -> void() -%% Description: This function is called by a gen_server when it is about to -%% terminate. It should be the opposite of Module:init/1 and do any necessary -%% cleaning up. When it returns, the gen_server terminates with Reason. -%% The return value is ignored. -%%-------------------------------------------------------------------- -terminate(_Reason, State) -> - ejabberd_router:unregister_route(State#state.host), ok. - -%%-------------------------------------------------------------------- -%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} -%% Description: Convert process state when code is changed -%%-------------------------------------------------------------------- -code_change(_OldVsn, State, _Extra) -> {ok, State}. - -%%-------------------------------------------------------------------- -%%% Internal functions -%%-------------------------------------------------------------------- -start_supervisor(Host) -> - Proc = gen_mod:get_module_proc(Host, - ejabberd_mod_irc_sup), - ChildSpec = {Proc, - {ejabberd_tmp_sup, start_link, - [Proc, mod_irc_connection]}, - permanent, infinity, supervisor, [ejabberd_tmp_sup]}, - supervisor:start_child(ejabberd_sup, ChildSpec). - -stop_supervisor(Host) -> - Proc = gen_mod:get_module_proc(Host, - ejabberd_mod_irc_sup), - supervisor:terminate_child(ejabberd_sup, Proc), - supervisor:delete_child(ejabberd_sup, Proc). - -do_route(Host, ServerHost, Access, From, To, Packet) -> - case acl:match_rule(ServerHost, Access, From) of - allow -> do_route1(Host, ServerHost, From, To, Packet); - _ -> - #xmlel{attrs = Attrs} = Packet, - Lang = xml:get_attr_s(<<"xml:lang">>, Attrs), - ErrText = <<"Access denied by service policy">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_FORBIDDEN(Lang, ErrText)), - ejabberd_router:route(To, From, Err) - end. - -do_route1(Host, ServerHost, From, To, Packet) -> - #jid{user = ChanServ, resource = Resource} = To, - #xmlel{} = Packet, - case ChanServ of - <<"">> -> - case Resource of - <<"">> -> - case jlib:iq_query_info(Packet) of - #iq{type = get, xmlns = (?NS_DISCO_INFO) = XMLNS, - sub_el = SubEl, lang = Lang} = - IQ -> - Node = xml:get_tag_attr_s(<<"node">>, SubEl), - Info = ejabberd_hooks:run_fold(disco_info, ServerHost, - [], - [ServerHost, ?MODULE, - <<"">>, <<"">>]), - case iq_disco(ServerHost, Node, Lang) of - [] -> - Res = IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"query">>, - attrs = - [{<<"xmlns">>, XMLNS}], - children = []}]}, - ejabberd_router:route(To, From, - jlib:iq_to_xml(Res)); - DiscoInfo -> - Res = IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"query">>, - attrs = - [{<<"xmlns">>, XMLNS}], - children = - DiscoInfo ++ Info}]}, - ejabberd_router:route(To, From, jlib:iq_to_xml(Res)) - end; - #iq{type = get, xmlns = (?NS_DISCO_ITEMS) = XMLNS, - sub_el = SubEl, lang = Lang} = - IQ -> - Node = xml:get_tag_attr_s(<<"node">>, SubEl), - case Node of - <<>> -> - ResIQ = IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"query">>, - attrs = - [{<<"xmlns">>, - XMLNS}], - children = []}]}, - Res = jlib:iq_to_xml(ResIQ); - <<"join">> -> - ResIQ = IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"query">>, - attrs = - [{<<"xmlns">>, - XMLNS}], - children = []}]}, - Res = jlib:iq_to_xml(ResIQ); - <<"register">> -> - ResIQ = IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"query">>, - attrs = - [{<<"xmlns">>, - XMLNS}], - children = []}]}, - Res = jlib:iq_to_xml(ResIQ); - ?NS_COMMANDS -> - ResIQ = IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"query">>, - attrs = - [{<<"xmlns">>, XMLNS}, - {<<"node">>, Node}], - children = - command_items(ServerHost, - Host, - Lang)}]}, - Res = jlib:iq_to_xml(ResIQ); - _ -> - Res = jlib:make_error_reply(Packet, - ?ERR_ITEM_NOT_FOUND) - end, - ejabberd_router:route(To, From, Res); - #iq{xmlns = ?NS_REGISTER} = IQ -> - process_register(ServerHost, Host, From, To, IQ); - #iq{type = get, xmlns = (?NS_VCARD) = XMLNS, - lang = Lang} = - IQ -> - Res = IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"vCard">>, - attrs = [{<<"xmlns">>, XMLNS}], - children = iq_get_vcard(Lang)}]}, - ejabberd_router:route(To, From, jlib:iq_to_xml(Res)); - #iq{type = set, xmlns = ?NS_COMMANDS, lang = _Lang, - sub_el = SubEl} = - IQ -> - Request = adhoc:parse_request(IQ), - case lists:keysearch(Request#adhoc_request.node, 1, - commands(ServerHost)) - of - {value, {_, _, Function}} -> - case catch Function(From, To, Request) of - {'EXIT', Reason} -> - ?ERROR_MSG("~p~nfor ad-hoc handler of ~p", - [Reason, {From, To, IQ}]), - Res = IQ#iq{type = error, - sub_el = - [SubEl, - ?ERR_INTERNAL_SERVER_ERROR]}; - ignore -> Res = ignore; - {error, Error} -> - Res = IQ#iq{type = error, - sub_el = [SubEl, Error]}; - Command -> - Res = IQ#iq{type = result, sub_el = [Command]} - end, - if Res /= ignore -> - ejabberd_router:route(To, From, - jlib:iq_to_xml(Res)); - true -> ok - end; - _ -> - Err = jlib:make_error_reply(Packet, - ?ERR_ITEM_NOT_FOUND), - ejabberd_router:route(To, From, Err) - end; - #iq{} = _IQ -> - Err = jlib:make_error_reply(Packet, - ?ERR_FEATURE_NOT_IMPLEMENTED), - ejabberd_router:route(To, From, Err); - _ -> ok - end; - _ -> - Err = jlib:make_error_reply(Packet, ?ERR_BAD_REQUEST), - ejabberd_router:route(To, From, Err) - end; - _ -> - case str:tokens(ChanServ, <<"%">>) of - [<<_, _/binary>> = Channel, <<_, _/binary>> = Server] -> - case ets:lookup(irc_connection, {From, Server, Host}) of - [] -> - ?DEBUG("open new connection~n", []), - {Username, Encoding, Port, Password} = - get_connection_params(Host, ServerHost, From, Server), - ConnectionUsername = case Packet of - %% If the user tries to join a - %% chatroom, the packet for sure - %% contains the desired username. - #xmlel{name = <<"presence">>} -> - Resource; - %% Otherwise, there is no firm - %% conclusion from the packet. - %% Better to use the configured - %% username (which defaults to the - %% username part of the JID). - _ -> Username - end, - {ok, Pid} = mod_irc_connection:start(From, Host, - ServerHost, Server, - ConnectionUsername, - Encoding, Port, - Password, ?MODULE), - ets:insert(irc_connection, - #irc_connection{jid_server_host = - {From, Server, Host}, - pid = Pid}), - mod_irc_connection:route_chan(Pid, Channel, Resource, - Packet), - ok; - [R] -> - Pid = R#irc_connection.pid, - ?DEBUG("send to process ~p~n", [Pid]), - mod_irc_connection:route_chan(Pid, Channel, Resource, - Packet), - ok - end; - _ -> - case str:tokens(ChanServ, <<"!">>) of - [<<_, _/binary>> = Nick, <<_, _/binary>> = Server] -> - case ets:lookup(irc_connection, {From, Server, Host}) of - [] -> - Err = jlib:make_error_reply(Packet, - ?ERR_SERVICE_UNAVAILABLE), - ejabberd_router:route(To, From, Err); - [R] -> - Pid = R#irc_connection.pid, - ?DEBUG("send to process ~p~n", [Pid]), - mod_irc_connection:route_nick(Pid, Nick, Packet), - ok - end; - _ -> - Err = jlib:make_error_reply(Packet, ?ERR_BAD_REQUEST), - ejabberd_router:route(To, From, Err) - end - end - end. - -closed_connection(Host, From, Server) -> - ets:delete(irc_connection, {From, Server, Host}). - -iq_disco(_ServerHost, <<>>, Lang) -> - [#xmlel{name = <<"identity">>, - attrs = - [{<<"category">>, <<"conference">>}, - {<<"type">>, <<"irc">>}, - {<<"name">>, - translate:translate(Lang, <<"IRC Transport">>)}], - children = []}, - #xmlel{name = <<"feature">>, - attrs = [{<<"var">>, ?NS_DISCO_INFO}], children = []}, - #xmlel{name = <<"feature">>, - attrs = [{<<"var">>, ?NS_MUC}], children = []}, - #xmlel{name = <<"feature">>, - attrs = [{<<"var">>, ?NS_REGISTER}], children = []}, - #xmlel{name = <<"feature">>, - attrs = [{<<"var">>, ?NS_VCARD}], children = []}, - #xmlel{name = <<"feature">>, - attrs = [{<<"var">>, ?NS_COMMANDS}], children = []}]; -iq_disco(ServerHost, Node, Lang) -> - case lists:keysearch(Node, 1, commands(ServerHost)) of - {value, {_, Name, _}} -> - [#xmlel{name = <<"identity">>, - attrs = - [{<<"category">>, <<"automation">>}, - {<<"type">>, <<"command-node">>}, - {<<"name">>, translate:translate(Lang, Name)}], - children = []}, - #xmlel{name = <<"feature">>, - attrs = [{<<"var">>, ?NS_COMMANDS}], children = []}, - #xmlel{name = <<"feature">>, - attrs = [{<<"var">>, ?NS_XDATA}], children = []}]; - _ -> [] - end. - -iq_get_vcard(Lang) -> - [#xmlel{name = <<"FN">>, attrs = [], - children = [{xmlcdata, <<"ejabberd/mod_irc">>}]}, - #xmlel{name = <<"URL">>, attrs = [], - children = [{xmlcdata, ?EJABBERD_URI}]}, - #xmlel{name = <<"DESC">>, attrs = [], - children = - [{xmlcdata, - <<(translate:translate(Lang, - <<"ejabberd IRC module">>))/binary, - "\nCopyright (c) 2003-2015 ProcessOne">>}]}]. - -command_items(ServerHost, Host, Lang) -> - lists:map(fun ({Node, Name, _Function}) -> - #xmlel{name = <<"item">>, - attrs = - [{<<"jid">>, Host}, {<<"node">>, Node}, - {<<"name">>, - translate:translate(Lang, Name)}], - children = []} - end, - commands(ServerHost)). - -commands(ServerHost) -> - [{<<"join">>, <<"Join channel">>, fun adhoc_join/3}, - {<<"register">>, - <<"Configure username, encoding, port and " - "password">>, - fun (From, To, Request) -> - adhoc_register(ServerHost, From, To, Request) - end}]. - -process_register(ServerHost, Host, From, To, - #iq{} = IQ) -> - case catch process_irc_register(ServerHost, Host, From, - To, IQ) - of - {'EXIT', Reason} -> ?ERROR_MSG("~p", [Reason]); - ResIQ -> - if ResIQ /= ignore -> - ejabberd_router:route(To, From, jlib:iq_to_xml(ResIQ)); - true -> ok - end - end. - -find_xdata_el(#xmlel{children = SubEls}) -> - find_xdata_el1(SubEls). - -find_xdata_el1([]) -> false; -find_xdata_el1([#xmlel{name = Name, attrs = Attrs, - children = SubEls} - | Els]) -> - case xml:get_attr_s(<<"xmlns">>, Attrs) of - ?NS_XDATA -> - #xmlel{name = Name, attrs = Attrs, children = SubEls}; - _ -> find_xdata_el1(Els) - end; -find_xdata_el1([_ | Els]) -> find_xdata_el1(Els). - -process_irc_register(ServerHost, Host, From, _To, - #iq{type = Type, xmlns = XMLNS, lang = Lang, - sub_el = SubEl} = - IQ) -> - case Type of - set -> - XDataEl = find_xdata_el(SubEl), - case XDataEl of - false -> - IQ#iq{type = error, - sub_el = [SubEl, ?ERR_NOT_ACCEPTABLE]}; - #xmlel{attrs = Attrs} -> - case xml:get_attr_s(<<"type">>, Attrs) of - <<"cancel">> -> - IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"query">>, - attrs = [{<<"xmlns">>, XMLNS}], - children = []}]}; - <<"submit">> -> - XData = jlib:parse_xdata_submit(XDataEl), - case XData of - invalid -> - IQ#iq{type = error, - sub_el = [SubEl, ?ERR_BAD_REQUEST]}; - _ -> - Node = str:tokens(xml:get_tag_attr_s(<<"node">>, - SubEl), - <<"/">>), - case set_form(ServerHost, Host, From, Node, Lang, - XData) - of - {result, Res} -> - IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"query">>, - attrs = - [{<<"xmlns">>, XMLNS}], - children = Res}]}; - {error, Error} -> - IQ#iq{type = error, sub_el = [SubEl, Error]} - end - end; - _ -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_BAD_REQUEST]} - end - end; - get -> - Node = str:tokens(xml:get_tag_attr_s(<<"node">>, SubEl), - <<"/">>), - case get_form(ServerHost, Host, From, Node, Lang) of - {result, Res} -> - IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"query">>, - attrs = [{<<"xmlns">>, XMLNS}], - children = Res}]}; - {error, Error} -> - IQ#iq{type = error, sub_el = [SubEl, Error]} - end - end. - -get_data(ServerHost, Host, From) -> - LServer = jlib:nameprep(ServerHost), - get_data(LServer, Host, From, - gen_mod:db_type(LServer, ?MODULE)). - -get_data(_LServer, Host, From, mnesia) -> - #jid{luser = LUser, lserver = LServer} = From, - US = {LUser, LServer}, - case catch mnesia:dirty_read({irc_custom, {US, Host}}) - of - {'EXIT', _Reason} -> error; - [] -> empty; - [#irc_custom{data = Data}] -> Data - end; -get_data(LServer, Host, From, riak) -> - #jid{luser = LUser, lserver = LServer} = From, - US = {LUser, LServer}, - case ejabberd_riak:get(irc_custom, irc_custom_schema(), {US, Host}) of - {ok, #irc_custom{data = Data}} -> - Data; - {error, notfound} -> - empty; - _Err -> - error - end; -get_data(LServer, Host, From, odbc) -> - SJID = - ejabberd_odbc:escape(jlib:jid_to_string(jlib:jid_tolower(jlib:jid_remove_resource(From)))), - SHost = ejabberd_odbc:escape(Host), - case catch ejabberd_odbc:sql_query(LServer, - [<<"select data from irc_custom where jid='">>, - SJID, <<"' and host='">>, SHost, - <<"';">>]) - of - {selected, [<<"data">>], [[SData]]} -> - data_to_binary(From, ejabberd_odbc:decode_term(SData)); - {'EXIT', _} -> error; - {selected, _, _} -> empty - end. - -get_form(ServerHost, Host, From, [], Lang) -> - #jid{user = User, server = Server} = From, - DefaultEncoding = get_default_encoding(Host), - Customs = case get_data(ServerHost, Host, From) of - error -> {error, ?ERR_INTERNAL_SERVER_ERROR}; - empty -> {User, []}; - Data -> get_username_and_connection_params(Data) - end, - case Customs of - {error, _Error} -> Customs; - {Username, ConnectionsParams} -> - {result, - [#xmlel{name = <<"instructions">>, attrs = [], - children = - [{xmlcdata, - translate:translate(Lang, - <<"You need an x:data capable client to " - "configure mod_irc settings">>)}]}, - #xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_XDATA}], - children = - [#xmlel{name = <<"title">>, attrs = [], - children = - [{xmlcdata, - <<(translate:translate(Lang, - <<"Registration in mod_irc for ">>))/binary, - User/binary, "@", Server/binary>>}]}, - #xmlel{name = <<"instructions">>, attrs = [], - children = - [{xmlcdata, - translate:translate(Lang, - <<"Enter username, encodings, ports and " - "passwords you wish to use for connecting " - "to IRC servers">>)}]}, - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, <<"text-single">>}, - {<<"label">>, - translate:translate(Lang, - <<"IRC Username">>)}, - {<<"var">>, <<"username">>}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, Username}]}]}, - #xmlel{name = <<"field">>, - attrs = [{<<"type">>, <<"fixed">>}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = - [{xmlcdata, - iolist_to_binary( - io_lib:format( - translate:translate( - Lang, - <<"If you want to specify" - " different ports, " - "passwords, encodings " - "for IRC servers, " - "fill this list with " - "values in format " - "'{\"irc server\", " - "\"encoding\", port, " - "\"password\"}'. " - "By default this " - "service use \"~s\" " - "encoding, port ~p, " - "empty password.">>), - [DefaultEncoding, - ?DEFAULT_IRC_PORT]))}]}]}, - #xmlel{name = <<"field">>, - attrs = [{<<"type">>, <<"fixed">>}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = - [{xmlcdata, - translate:translate(Lang, - <<"Example: [{\"irc.lucky.net\", \"koi8-r\", " - "6667, \"secret\"}, {\"vendetta.fef.net\", " - "\"iso8859-1\", 7000}, {\"irc.sometestserver.n" - "et\", \"utf-8\"}].">>)}]}]}, - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, <<"text-multi">>}, - {<<"label">>, - translate:translate(Lang, - <<"Connections parameters">>)}, - {<<"var">>, <<"connections_params">>}], - children = - lists:map(fun (S) -> - #xmlel{name = <<"value">>, - attrs = [], - children = - [{xmlcdata, S}]} - end, - str:tokens(list_to_binary( - io_lib:format( - "~p.", - [conn_params_to_list( - ConnectionsParams)])), - <<"\n">>))}]}]} - end; -get_form(_ServerHost, _Host, _, _, _Lang) -> - {error, ?ERR_SERVICE_UNAVAILABLE}. - -set_data(ServerHost, Host, From, Data) -> - LServer = jlib:nameprep(ServerHost), - set_data(LServer, Host, From, data_to_binary(From, Data), - gen_mod:db_type(LServer, ?MODULE)). - -set_data(_LServer, Host, From, Data, mnesia) -> - {LUser, LServer, _} = jlib:jid_tolower(From), - US = {LUser, LServer}, - F = fun () -> - mnesia:write(#irc_custom{us_host = {US, Host}, - data = Data}) - end, - mnesia:transaction(F); -set_data(LServer, Host, From, Data, riak) -> - {LUser, LServer, _} = jlib:jid_tolower(From), - US = {LUser, LServer}, - {atomic, ejabberd_riak:put(#irc_custom{us_host = {US, Host}, - data = Data}, - irc_custom_schema())}; -set_data(LServer, Host, From, Data, odbc) -> - SJID = - ejabberd_odbc:escape(jlib:jid_to_string(jlib:jid_tolower(jlib:jid_remove_resource(From)))), - SHost = ejabberd_odbc:escape(Host), - SData = ejabberd_odbc:encode_term(Data), - F = fun () -> - odbc_queries:update_t(<<"irc_custom">>, - [<<"jid">>, <<"host">>, <<"data">>], - [SJID, SHost, SData], - [<<"jid='">>, SJID, <<"' and host='">>, - SHost, <<"'">>]), - ok - end, - ejabberd_odbc:sql_transaction(LServer, F). - -set_form(ServerHost, Host, From, [], _Lang, XData) -> - case {lists:keysearch(<<"username">>, 1, XData), - lists:keysearch(<<"connections_params">>, 1, XData)} - of - {{value, {_, [Username]}}, {value, {_, Strings}}} -> - EncString = lists:foldl(fun (S, Res) -> - <> - end, - <<"">>, Strings), - case erl_scan:string(binary_to_list(EncString)) of - {ok, Tokens, _} -> - case erl_parse:parse_term(Tokens) of - {ok, ConnectionsParams} -> - case set_data(ServerHost, Host, From, - [{username, Username}, - {connections_params, ConnectionsParams}]) - of - {atomic, _} -> {result, []}; - _ -> {error, ?ERR_NOT_ACCEPTABLE} - end; - _ -> {error, ?ERR_NOT_ACCEPTABLE} - end; - _ -> {error, ?ERR_NOT_ACCEPTABLE} - end; - _ -> {error, ?ERR_NOT_ACCEPTABLE} - end; -set_form(_ServerHost, _Host, _, _, _Lang, _XData) -> - {error, ?ERR_SERVICE_UNAVAILABLE}. - -%% Host = "irc.example.com" -%% ServerHost = "example.com" -get_connection_params(Host, From, IRCServer) -> - [_ | HostTail] = str:tokens(Host, <<".">>), - ServerHost = str:join(HostTail, <<".">>), - get_connection_params(Host, ServerHost, From, - IRCServer). - -get_default_encoding(ServerHost) -> - Result = gen_mod:get_module_opt(ServerHost, ?MODULE, default_encoding, - fun iolist_to_binary/1, - ?DEFAULT_IRC_ENCODING), - ?INFO_MSG("The default_encoding configured for " - "host ~p is: ~p~n", - [ServerHost, Result]), - Result. - -get_connection_params(Host, ServerHost, From, - IRCServer) -> - #jid{user = User, server = _Server} = From, - DefaultEncoding = get_default_encoding(ServerHost), - case get_data(ServerHost, Host, From) of - error -> - {User, DefaultEncoding, ?DEFAULT_IRC_PORT, <<"">>}; - empty -> - {User, DefaultEncoding, ?DEFAULT_IRC_PORT, <<"">>}; - Data -> - {Username, ConnParams} = get_username_and_connection_params(Data), - {NewUsername, NewEncoding, NewPort, NewPassword} = case - lists:keysearch(IRCServer, - 1, - ConnParams) - of - {value, - {_, Encoding, - Port, - Password}} -> - {Username, - Encoding, - Port, - Password}; - {value, - {_, Encoding, - Port}} -> - {Username, - Encoding, - Port, - <<"">>}; - {value, - {_, - Encoding}} -> - {Username, - Encoding, - ?DEFAULT_IRC_PORT, - <<"">>}; - _ -> - {Username, - DefaultEncoding, - ?DEFAULT_IRC_PORT, - <<"">>} - end, - {iolist_to_binary(NewUsername), - iolist_to_binary(NewEncoding), - if NewPort >= 0 andalso NewPort =< 65535 -> NewPort; - true -> ?DEFAULT_IRC_PORT - end, - iolist_to_binary(NewPassword)} - end. - -adhoc_join(_From, _To, - #adhoc_request{action = <<"cancel">>} = Request) -> - adhoc:produce_response(Request, - #adhoc_response{status = canceled}); -adhoc_join(From, To, - #adhoc_request{lang = Lang, node = _Node, - action = _Action, xdata = XData} = - Request) -> - if XData == false -> - Form = #xmlel{name = <<"x">>, - attrs = - [{<<"xmlns">>, ?NS_XDATA}, - {<<"type">>, <<"form">>}], - children = - [#xmlel{name = <<"title">>, attrs = [], - children = - [{xmlcdata, - translate:translate(Lang, - <<"Join IRC channel">>)}]}, - #xmlel{name = <<"field">>, - attrs = - [{<<"var">>, <<"channel">>}, - {<<"type">>, <<"text-single">>}, - {<<"label">>, - translate:translate(Lang, - <<"IRC channel (don't put the first #)">>)}], - children = - [#xmlel{name = <<"required">>, - attrs = [], children = []}]}, - #xmlel{name = <<"field">>, - attrs = - [{<<"var">>, <<"server">>}, - {<<"type">>, <<"text-single">>}, - {<<"label">>, - translate:translate(Lang, - <<"IRC server">>)}], - children = - [#xmlel{name = <<"required">>, - attrs = [], children = []}]}]}, - adhoc:produce_response(Request, - #adhoc_response{status = executing, - elements = [Form]}); - true -> - case jlib:parse_xdata_submit(XData) of - invalid -> {error, ?ERR_BAD_REQUEST}; - Fields -> - Channel = case lists:keysearch(<<"channel">>, 1, Fields) - of - {value, {<<"channel">>, [C]}} -> C; - _ -> false - end, - Server = case lists:keysearch(<<"server">>, 1, Fields) - of - {value, {<<"server">>, [S]}} -> S; - _ -> false - end, - if Channel /= false, Server /= false -> - RoomJID = <>, - Invite = #xmlel{name = <<"message">>, attrs = [], - children = - [#xmlel{name = <<"x">>, - attrs = - [{<<"xmlns">>, - ?NS_MUC_USER}], - children = - [#xmlel{name = - <<"invite">>, - attrs = - [{<<"from">>, - jlib:jid_to_string(From)}], - children = - [#xmlel{name - = - <<"reason">>, - attrs - = - [], - children - = - [{xmlcdata, - translate:translate(Lang, - <<"Join the IRC channel here.">>)}]}]}]}, - #xmlel{name = <<"x">>, - attrs = - [{<<"xmlns">>, - ?NS_XCONFERENCE}], - children = - [{xmlcdata, - translate:translate(Lang, - <<"Join the IRC channel here.">>)}]}, - #xmlel{name = <<"body">>, - attrs = [], - children = - [{xmlcdata, - iolist_to_binary( - io_lib:format( - translate:translate( - Lang, - <<"Join the IRC channel in this Jabber ID: ~s">>), - [RoomJID]))}]}]}, - ejabberd_router:route(jlib:string_to_jid(RoomJID), From, - Invite), - adhoc:produce_response(Request, - #adhoc_response{status = - completed}); - true -> {error, ?ERR_BAD_REQUEST} - end - end - end. - -adhoc_register(_ServerHost, _From, _To, - #adhoc_request{action = <<"cancel">>} = Request) -> - adhoc:produce_response(Request, - #adhoc_response{status = canceled}); -adhoc_register(ServerHost, From, To, - #adhoc_request{lang = Lang, node = _Node, xdata = XData, - action = Action} = - Request) -> - #jid{user = User} = From, - #jid{lserver = Host} = To, - if XData == false -> - case get_data(ServerHost, Host, From) of - error -> Username = User, ConnectionsParams = []; - empty -> Username = User, ConnectionsParams = []; - Data -> - {Username, ConnectionsParams} = - get_username_and_connection_params(Data) - end, - Error = false; - true -> - case jlib:parse_xdata_submit(XData) of - invalid -> - Error = {error, ?ERR_BAD_REQUEST}, - Username = false, - ConnectionsParams = false; - Fields -> - Username = case lists:keysearch(<<"username">>, 1, - Fields) - of - {value, {<<"username">>, U}} -> U; - _ -> User - end, - ConnectionsParams = parse_connections_params(Fields), - Error = false - end - end, - if Error /= false -> Error; - Action == <<"complete">> -> - case set_data(ServerHost, Host, From, - [{username, Username}, - {connections_params, ConnectionsParams}]) - of - {atomic, _} -> - adhoc:produce_response(Request, - #adhoc_response{status = completed}); - _ -> {error, ?ERR_INTERNAL_SERVER_ERROR} - end; - true -> - Form = generate_adhoc_register_form(Lang, Username, - ConnectionsParams), - adhoc:produce_response(Request, - #adhoc_response{status = executing, - elements = [Form], - actions = - [<<"next">>, - <<"complete">>]}) - end. - -generate_adhoc_register_form(Lang, Username, - ConnectionsParams) -> - #xmlel{name = <<"x">>, - attrs = - [{<<"xmlns">>, ?NS_XDATA}, {<<"type">>, <<"form">>}], - children = - [#xmlel{name = <<"title">>, attrs = [], - children = - [{xmlcdata, - translate:translate(Lang, <<"IRC settings">>)}]}, - #xmlel{name = <<"instructions">>, attrs = [], - children = - [{xmlcdata, - translate:translate(Lang, - <<"Enter username and encodings you wish " - "to use for connecting to IRC servers. " - " Press 'Next' to get more fields to " - "fill in. Press 'Complete' to save settings.">>)}]}, - #xmlel{name = <<"field">>, - attrs = - [{<<"var">>, <<"username">>}, - {<<"type">>, <<"text-single">>}, - {<<"label">>, - translate:translate(Lang, <<"IRC username">>)}], - children = - [#xmlel{name = <<"required">>, attrs = [], - children = []}, - #xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, Username}]}]}] - ++ - generate_connection_params_fields(Lang, - ConnectionsParams, 1, [])}. - -generate_connection_params_fields(Lang, [], Number, - Acc) -> - Field = generate_connection_params_field(Lang, <<"">>, - <<"">>, -1, <<"">>, Number), - lists:reverse(Field ++ Acc); -generate_connection_params_fields(Lang, - [ConnectionParams | ConnectionsParams], - Number, Acc) -> - case ConnectionParams of - {Server, Encoding, Port, Password} -> - Field = generate_connection_params_field(Lang, Server, - Encoding, Port, Password, - Number), - generate_connection_params_fields(Lang, - ConnectionsParams, Number + 1, - Field ++ Acc); - {Server, Encoding, Port} -> - Field = generate_connection_params_field(Lang, Server, - Encoding, Port, <<"">>, Number), - generate_connection_params_fields(Lang, - ConnectionsParams, Number + 1, - Field ++ Acc); - {Server, Encoding} -> - Field = generate_connection_params_field(Lang, Server, - Encoding, -1, <<"">>, Number), - generate_connection_params_fields(Lang, - ConnectionsParams, Number + 1, - Field ++ Acc); - _ -> [] - end. - -generate_connection_params_field(Lang, Server, Encoding, - Port, Password, Number) -> - EncodingUsed = case Encoding of - <<>> -> get_default_encoding(Server); - _ -> Encoding - end, - PortUsedInt = if Port >= 0 andalso Port =< 65535 -> - Port; - true -> ?DEFAULT_IRC_PORT - end, - PortUsed = - iolist_to_binary(integer_to_list(PortUsedInt)), - PasswordUsed = case Password of - <<>> -> <<>>; - _ -> Password - end, - NumberString = - iolist_to_binary(integer_to_list(Number)), - [#xmlel{name = <<"field">>, - attrs = - [{<<"var">>, <<"password", NumberString/binary>>}, - {<<"type">>, <<"text-single">>}, - {<<"label">>, - iolist_to_binary( - io_lib:format( - translate:translate(Lang, <<"Password ~b">>), - [Number]))}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, PasswordUsed}]}]}, - #xmlel{name = <<"field">>, - attrs = - [{<<"var">>, <<"port", NumberString/binary>>}, - {<<"type">>, <<"text-single">>}, - {<<"label">>, - iolist_to_binary( - io_lib:format(translate:translate(Lang, <<"Port ~b">>), - [Number]))}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, PortUsed}]}]}, - #xmlel{name = <<"field">>, - attrs = - [{<<"var">>, <<"encoding", NumberString/binary>>}, - {<<"type">>, <<"list-single">>}, - {<<"label">>, - list_to_binary( - io_lib:format(translate:translate( - Lang, - <<"Encoding for server ~b">>), - [Number]))}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, EncodingUsed}]} - | lists:map(fun (E) -> - #xmlel{name = <<"option">>, - attrs = [{<<"label">>, E}], - children = - [#xmlel{name = <<"value">>, - attrs = [], - children = - [{xmlcdata, E}]}]} - end, - ?POSSIBLE_ENCODINGS)]}, - #xmlel{name = <<"field">>, - attrs = - [{<<"var">>, <<"server", NumberString/binary>>}, - {<<"type">>, <<"text-single">>}, - {<<"label">>, - list_to_binary( - io_lib:format(translate:translate(Lang, <<"Server ~b">>), - [Number]))}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, Server}]}]}]. - -parse_connections_params(Fields) -> - Servers = lists:flatmap( - fun({<<"server", Var/binary>>, Value}) -> - [{Var, Value}]; - (_) -> - [] - end, Fields), - Encodings = lists:flatmap( - fun({<<"encoding", Var/binary>>, Value}) -> - [{Var, Value}]; - (_) -> - [] - end, Fields), - Ports = lists:flatmap( - fun({<<"port", Var/binary>>, Value}) -> - [{Var, Value}]; - (_) -> - [] - end, Fields), - Passwords = lists:flatmap( - fun({<<"password", Var/binary>>, Value}) -> - [{Var, Value}]; - (_) -> - [] - end, Fields), - parse_connections_params(Servers, Encodings, Ports, - Passwords). - -retrieve_connections_params(ConnectionParams, - ServerN) -> - case ConnectionParams of - [{ConnectionParamN, ConnectionParam} - | ConnectionParamsTail] -> - if ServerN == ConnectionParamN -> - {ConnectionParam, ConnectionParamsTail}; - ServerN < ConnectionParamN -> - {[], - [{ConnectionParamN, ConnectionParam} - | ConnectionParamsTail]}; - ServerN > ConnectionParamN -> {[], ConnectionParamsTail} - end; - _ -> {[], []} - end. - -parse_connections_params([], _, _, _) -> []; -parse_connections_params(_, [], [], []) -> []; -parse_connections_params([{ServerN, Server} | Servers], - Encodings, Ports, Passwords) -> - {NewEncoding, NewEncodings} = - retrieve_connections_params(Encodings, ServerN), - {NewPort, NewPorts} = retrieve_connections_params(Ports, - ServerN), - {NewPassword, NewPasswords} = - retrieve_connections_params(Passwords, ServerN), - [{Server, NewEncoding, NewPort, NewPassword} - | parse_connections_params(Servers, NewEncodings, - NewPorts, NewPasswords)]. - -get_username_and_connection_params(Data) -> - Username = case lists:keysearch(username, 1, Data) of - {value, {_, U}} when is_binary(U) -> - U; - _ -> - <<"">> - end, - ConnParams = case lists:keysearch(connections_params, 1, Data) of - {value, {_, L}} when is_list(L) -> - L; - _ -> - [] - end, - {Username, ConnParams}. - -data_to_binary(JID, Data) -> - lists:map( - fun({username, U}) -> - {username, iolist_to_binary(U)}; - ({connections_params, Params}) -> - {connections_params, - lists:flatmap( - fun(Param) -> - try - [conn_param_to_binary(Param)] - catch _:_ -> - if JID /= error -> - ?ERROR_MSG("failed to convert " - "parameter ~p for user ~s", - [Param, - jlib:jid_to_string(JID)]); - true -> - ?ERROR_MSG("failed to convert " - "parameter ~p", - [Param]) - end, - [] - end - end, Params)}; - (Opt) -> - Opt - end, Data). - -conn_param_to_binary({S}) -> - {iolist_to_binary(S)}; -conn_param_to_binary({S, E}) -> - {iolist_to_binary(S), iolist_to_binary(E)}; -conn_param_to_binary({S, E, Port}) when is_integer(Port) -> - {iolist_to_binary(S), iolist_to_binary(E), Port}; -conn_param_to_binary({S, E, Port, P}) when is_integer(Port) -> - {iolist_to_binary(S), iolist_to_binary(E), Port, iolist_to_binary(P)}. - -conn_params_to_list(Params) -> - lists:map( - fun({S}) -> - {binary_to_list(S)}; - ({S, E}) -> - {binary_to_list(S), binary_to_list(E)}; - ({S, E, Port}) -> - {binary_to_list(S), binary_to_list(E), Port}; - ({S, E, Port, P}) -> - {binary_to_list(S), binary_to_list(E), - Port, binary_to_list(P)} - end, Params). - -irc_custom_schema() -> - {record_info(fields, irc_custom), #irc_custom{}}. - -update_table() -> - Fields = record_info(fields, irc_custom), - case mnesia:table_info(irc_custom, attributes) of - Fields -> - ejabberd_config:convert_table_to_binary( - irc_custom, Fields, set, - fun(#irc_custom{us_host = {_, H}}) -> H end, - fun(#irc_custom{us_host = {{U, S}, H}, - data = Data} = R) -> - JID = jlib:make_jid(U, S, <<"">>), - R#irc_custom{us_host = {{iolist_to_binary(U), - iolist_to_binary(S)}, - iolist_to_binary(H)}, - data = data_to_binary(JID, Data)} - end); - _ -> - ?INFO_MSG("Recreating irc_custom table", []), - mnesia:transform_table(irc_custom, ignore, Fields) - end. - -export(_Server) -> - [{irc_custom, - fun(Host, #irc_custom{us_host = {{U, S}, IRCHost}, - data = Data}) -> - case str:suffix(Host, IRCHost) of - true -> - SJID = ejabberd_odbc:escape( - jlib:jid_to_string( - jlib:make_jid(U, S, <<"">>))), - SIRCHost = ejabberd_odbc:escape(IRCHost), - SData = ejabberd_odbc:encode_term(Data), - [[<<"delete from irc_custom where jid='">>, SJID, - <<"' and host='">>, SIRCHost, <<"';">>], - [<<"insert into irc_custom(jid, host, " - "data) values ('">>, - SJID, <<"', '">>, SIRCHost, <<"', '">>, SData, - <<"');">>]]; - false -> - [] - end - end}]. - -import(_LServer) -> - [{<<"select jid, host, data from irc_custom;">>, - fun([SJID, IRCHost, SData]) -> - #jid{luser = U, lserver = S} = jlib:string_to_jid(SJID), - Data = ejabberd_odbc:decode_term(SData), - #irc_custom{us_host = {{U, S}, IRCHost}, - data = Data} - end}]. - -import(_LServer, mnesia, #irc_custom{} = R) -> - mnesia:dirty_write(R); -import(_LServer, riak, #irc_custom{} = R) -> - ejabberd_riak:put(R, irc_custom_schema()); -import(_, _, _) -> - pass. diff --git a/src/mod_irc_connection.erl b/src/mod_irc_connection.erl deleted file mode 100644 index c31adf754..000000000 --- a/src/mod_irc_connection.erl +++ /dev/null @@ -1,1581 +0,0 @@ -%%%---------------------------------------------------------------------- -%%% File : mod_irc_connection.erl -%%% Author : Alexey Shchepin -%%% Purpose : -%%% Created : 15 Feb 2003 by Alexey Shchepin -%%% -%%% -%%% ejabberd, Copyright (C) 2002-2015 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_irc_connection). - --author('alexey@process-one.net'). - --behaviour(gen_fsm). - -%% External exports --export([start_link/8, start/9, route_chan/4, - route_nick/3]). - -%% gen_fsm callbacks --export([init/1, open_socket/2, wait_for_registration/2, - stream_established/2, handle_event/3, - handle_sync_event/4, handle_info/3, terminate/3, - code_change/4]). - --include("ejabberd.hrl"). --include("logger.hrl"). - --include("jlib.hrl"). - --define(SETS, gb_sets). - --record(state, - {socket :: inet:socket(), - encoding = <<"">> :: binary(), - port = 0 :: inet:port_number(), - password = <<"">> :: binary(), - queue = queue:new() :: queue(), - user = #jid{} :: jid(), - host = <<"">> :: binary(), - server = <<"">> :: binary(), - nick = <<"">> :: binary(), - channels = dict:new() :: dict(), - nickchannel :: binary(), - mod = mod_irc :: atom(), - inbuf = <<"">> :: binary(), - outbuf = <<"">> :: binary()}). - -%-define(DBGFSM, true). - --ifdef(DBGFSM). - --define(FSMOPTS, [{debug, [trace]}]). - --else. - --define(FSMOPTS, []). - -%%%---------------------------------------------------------------------- -%%% API -%%%---------------------------------------------------------------------- --endif. - -start(From, Host, ServerHost, Server, Username, - Encoding, Port, Password, Mod) -> - Supervisor = gen_mod:get_module_proc(ServerHost, - ejabberd_mod_irc_sup), - supervisor:start_child(Supervisor, - [From, Host, Server, Username, Encoding, Port, - Password, Mod]). - -start_link(From, Host, Server, Username, Encoding, Port, - Password, Mod) -> - gen_fsm:start_link(?MODULE, - [From, Host, Server, Username, Encoding, Port, Password, - Mod], - ?FSMOPTS). - -%%%---------------------------------------------------------------------- -%%% Callback functions from gen_fsm -%%%---------------------------------------------------------------------- - -%%---------------------------------------------------------------------- -%% Func: init/1 -%% Returns: {ok, StateName, StateData} | -%% {ok, StateName, StateData, Timeout} | -%% ignore | -%% {stop, StopReason} -%%---------------------------------------------------------------------- -init([From, Host, Server, Username, Encoding, Port, - Password, Mod]) -> - gen_fsm:send_event(self(), init), - {ok, open_socket, - #state{queue = queue:new(), mod = Mod, - encoding = Encoding, port = Port, password = Password, - user = From, nick = Username, host = Host, - server = Server}}. - -%%---------------------------------------------------------------------- -%% Func: StateName/2 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} -%%---------------------------------------------------------------------- -open_socket(init, StateData) -> - Addr = StateData#state.server, - Port = StateData#state.port, - ?DEBUG("Connecting with IPv6 to ~s:~p", [Addr, Port]), - Connect6 = gen_tcp:connect(binary_to_list(Addr), Port, - [inet6, binary, {packet, 0}]), - Connect = case Connect6 of - {error, _} -> - ?DEBUG("Connection with IPv6 to ~s:~p failed. " - "Now using IPv4.", - [Addr, Port]), - gen_tcp:connect(binary_to_list(Addr), Port, - [inet, binary, {packet, 0}]); - _ -> Connect6 - end, - case Connect of - {ok, Socket} -> - NewStateData = StateData#state{socket = Socket}, - if StateData#state.password /= <<"">> -> - send_text(NewStateData, - io_lib:format("PASS ~s\r\n", - [StateData#state.password])); - true -> true - end, - send_text(NewStateData, - io_lib:format("NICK ~s\r\n", [StateData#state.nick])), - send_text(NewStateData, - io_lib:format("USER ~s ~s ~s :~s\r\n", - [StateData#state.nick, StateData#state.nick, - StateData#state.host, - StateData#state.nick])), - {next_state, wait_for_registration, NewStateData}; - {error, Reason} -> - ?DEBUG("connect return ~p~n", [Reason]), - Text = case Reason of - timeout -> <<"Server Connect Timeout">>; - _ -> <<"Server Connect Failed">> - end, - bounce_messages(Text), - {stop, normal, StateData} - end. - -wait_for_registration(closed, StateData) -> - {stop, normal, StateData}. - -stream_established({xmlstreamend, _Name}, StateData) -> - {stop, normal, StateData}; -stream_established(timeout, StateData) -> - {stop, normal, StateData}; -stream_established(closed, StateData) -> - {stop, normal, StateData}. - -%%---------------------------------------------------------------------- -%% Func: StateName/3 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {reply, Reply, NextStateName, NextStateData} | -%% {reply, Reply, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} | -%% {stop, Reason, Reply, NewStateData} -%%---------------------------------------------------------------------- -%state_name(Event, From, StateData) -> -% Reply = ok, -% {reply, Reply, state_name, StateData}. - -%%---------------------------------------------------------------------- -%% Func: handle_event/3 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} -%%---------------------------------------------------------------------- -handle_event(_Event, StateName, StateData) -> - {next_state, StateName, StateData}. - -%%---------------------------------------------------------------------- -%% Func: handle_sync_event/4 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {reply, Reply, NextStateName, NextStateData} | -%% {reply, Reply, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} | -%% {stop, Reason, Reply, NewStateData} -%%---------------------------------------------------------------------- -handle_sync_event(_Event, _From, StateName, - StateData) -> - Reply = ok, {reply, Reply, StateName, StateData}. - -code_change(_OldVsn, StateName, StateData, _Extra) -> - {ok, StateName, StateData}. - --define(SEND(S), - if StateName == stream_established -> - send_text(StateData, S), StateData; - true -> - StateData#state{outbuf = <<(StateData#state.outbuf)/binary, - (iolist_to_binary(S))/binary>>} - end). - -get_password_from_presence(#xmlel{name = <<"presence">>, - children = Els}) -> - case lists:filter(fun (El) -> - case El of - #xmlel{name = <<"x">>, attrs = Attrs} -> - case xml:get_attr_s(<<"xmlns">>, Attrs) of - ?NS_MUC -> true; - _ -> false - end; - _ -> false - end - end, - Els) - of - [ElXMUC | _] -> - case xml:get_subtag(ElXMUC, <<"password">>) of - #xmlel{name = <<"password">>} = PasswordTag -> - {true, xml:get_tag_cdata(PasswordTag)}; - _ -> false - end; - _ -> false - end. - -%%---------------------------------------------------------------------- -%% Func: handle_info/3 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} -%%---------------------------------------------------------------------- -handle_info({route_chan, Channel, Resource, - #xmlel{name = <<"presence">>, attrs = Attrs} = - Presence}, - StateName, StateData) -> - NewStateData = case xml:get_attr_s(<<"type">>, Attrs) of - <<"unavailable">> -> - send_stanza_unavailable(Channel, StateData), - S1 = (?SEND((io_lib:format("PART #~s\r\n", - [Channel])))), - S1#state{channels = - dict:erase(Channel, S1#state.channels)}; - <<"subscribe">> -> StateData; - <<"subscribed">> -> StateData; - <<"unsubscribe">> -> StateData; - <<"unsubscribed">> -> StateData; - <<"error">> -> stop; - _ -> - Nick = case Resource of - <<"">> -> StateData#state.nick; - _ -> Resource - end, - S1 = if Nick /= StateData#state.nick -> - S11 = (?SEND((io_lib:format("NICK ~s\r\n", - [Nick])))), - S11#state{nickchannel = Channel}; - true -> StateData - end, - case dict:is_key(Channel, S1#state.channels) of - true -> S1; - _ -> - case get_password_from_presence(Presence) of - {true, Password} -> - S2 = - (?SEND((io_lib:format("JOIN #~s ~s\r\n", - [Channel, - Password])))); - _ -> - S2 = (?SEND((io_lib:format("JOIN #~s\r\n", - [Channel])))) - end, - S2#state{channels = - dict:store(Channel, (?SETS):new(), - S1#state.channels)} - end - end, - if NewStateData == stop -> {stop, normal, StateData}; - true -> - case dict:fetch_keys(NewStateData#state.channels) of - [] -> {stop, normal, NewStateData}; - _ -> {next_state, StateName, NewStateData} - end - end; -handle_info({route_chan, Channel, Resource, - #xmlel{name = <<"message">>, attrs = Attrs} = El}, - StateName, StateData) -> - NewStateData = case xml:get_attr_s(<<"type">>, Attrs) of - <<"groupchat">> -> - case xml:get_path_s(El, [{elem, <<"subject">>}, cdata]) - of - <<"">> -> - ejabberd_router:route( - jlib:make_jid( - iolist_to_binary([Channel, - <<"%">>, - StateData#state.server]), - StateData#state.host, - StateData#state.nick), - StateData#state.user, El), - Body = xml:get_path_s(El, - [{elem, <<"body">>}, - cdata]), - case Body of - <<"/quote ", Rest/binary>> -> - ?SEND(<>); - <<"/msg ", Rest/binary>> -> - ?SEND(<<"PRIVMSG ", Rest/binary, "\r\n">>); - <<"/me ", Rest/binary>> -> - Strings = str:tokens(Rest, <<"\n">>), - Res = iolist_to_binary( - lists:map( - fun (S) -> - io_lib:format( - "PRIVMSG #~s :\001ACTION ~s\001\r\n", - [Channel, S]) - end, - Strings)), - ?SEND(Res); - <<"/ctcp ", Rest/binary>> -> - Words = str:tokens(Rest, <<" ">>), - case Words of - [CtcpDest | _] -> - CtcpCmd = str:to_upper( - str:substr(Rest, - str:str(Rest, - <<" ">>) - + 1)), - Res = - io_lib:format("PRIVMSG ~s :\001~s\001\r\n", - [CtcpDest, - CtcpCmd]), - ?SEND(Res); - _ -> ok - end; - _ -> - Strings = str:tokens(Body, <<"\n">>), - Res = iolist_to_binary( - lists:map( - fun (S) -> - io_lib:format("PRIVMSG #~s :~s\r\n", - [Channel, S]) - end, - Strings)), - ?SEND(Res) - end; - Subject -> - Strings = str:tokens(Subject, <<"\n">>), - Res = iolist_to_binary( - lists:map( - fun (S) -> - io_lib:format("TOPIC #~s :~s\r\n", - [Channel, S]) - end, - Strings)), - ?SEND(Res) - end; - Type - when Type == <<"chat">>; - Type == <<"">>; - Type == <<"normal">> -> - Body = xml:get_path_s(El, [{elem, <<"body">>}, cdata]), - case Body of - <<"/quote ", Rest/binary>> -> - ?SEND(<>); - <<"/msg ", Rest/binary>> -> - ?SEND(<<"PRIVMSG ", Rest/binary, "\r\n">>); - <<"/me ", Rest/binary>> -> - Strings = str:tokens(Rest, <<"\n">>), - Res = iolist_to_binary( - lists:map( - fun (S) -> - io_lib:format( - "PRIVMSG ~s :\001ACTION ~s\001\r\n", - [Resource, S]) - end, - Strings)), - ?SEND(Res); - <<"/ctcp ", Rest/binary>> -> - Words = str:tokens(Rest, <<" ">>), - case Words of - [CtcpDest | _] -> - CtcpCmd = str:to_upper( - str:substr(Rest, - str:str(Rest, - <<" ">>) - + 1)), - Res = io_lib:format("PRIVMSG ~s :~s\r\n", - [CtcpDest, - <<"\001", - CtcpCmd/binary, - "\001">>]), - ?SEND(Res); - _ -> ok - end; - _ -> - Strings = str:tokens(Body, <<"\n">>), - Res = iolist_to_binary( - lists:map( - fun (S) -> - io_lib:format( - "PRIVMSG ~s :~s\r\n", - [Resource, S]) - end, - Strings)), - ?SEND(Res) - end; - <<"error">> -> stop; - _ -> StateData - end, - if NewStateData == stop -> {stop, normal, StateData}; - true -> {next_state, StateName, NewStateData} - end; -handle_info({route_chan, Channel, Resource, - #xmlel{name = <<"iq">>} = El}, - StateName, StateData) -> - From = StateData#state.user, - To = jlib:make_jid(iolist_to_binary([Channel, <<"%">>, - StateData#state.server]), - StateData#state.host, StateData#state.nick), - _ = case jlib:iq_query_info(El) of - #iq{xmlns = ?NS_MUC_ADMIN} = IQ -> - iq_admin(StateData, Channel, From, To, IQ); - #iq{xmlns = ?NS_VERSION} -> - Res = io_lib:format("PRIVMSG ~s :\001VERSION\001\r\n", - [Resource]), - _ = (?SEND(Res)), - Err = jlib:make_error_reply(El, - ?ERR_FEATURE_NOT_IMPLEMENTED), - ejabberd_router:route(To, From, Err); - #iq{xmlns = ?NS_TIME} -> - Res = io_lib:format("PRIVMSG ~s :\001TIME\001\r\n", - [Resource]), - _ = (?SEND(Res)), - Err = jlib:make_error_reply(El, - ?ERR_FEATURE_NOT_IMPLEMENTED), - ejabberd_router:route(To, From, Err); - #iq{xmlns = ?NS_VCARD} -> - Res = io_lib:format("WHOIS ~s \r\n", [Resource]), - _ = (?SEND(Res)), - Err = jlib:make_error_reply(El, - ?ERR_FEATURE_NOT_IMPLEMENTED), - ejabberd_router:route(To, From, Err); - #iq{} -> - Err = jlib:make_error_reply(El, - ?ERR_FEATURE_NOT_IMPLEMENTED), - ejabberd_router:route(To, From, Err); - _ -> ok - end, - {next_state, StateName, StateData}; -handle_info({route_chan, _Channel, _Resource, _Packet}, - StateName, StateData) -> - {next_state, StateName, StateData}; -handle_info({route_nick, Nick, - #xmlel{name = <<"message">>, attrs = Attrs} = El}, - StateName, StateData) -> - NewStateData = case xml:get_attr_s(<<"type">>, Attrs) of - <<"chat">> -> - Body = xml:get_path_s(El, [{elem, <<"body">>}, cdata]), - case Body of - <<"/quote ", Rest/binary>> -> - ?SEND(<>); - <<"/msg ", Rest/binary>> -> - ?SEND(<<"PRIVMSG ", Rest/binary, "\r\n">>); - <<"/me ", Rest/binary>> -> - Strings = str:tokens(Rest, <<"\n">>), - Res = iolist_to_binary( - lists:map( - fun (S) -> - io_lib:format( - "PRIVMSG ~s :\001ACTION ~s\001\r\n", - [Nick, S]) - end, - Strings)), - ?SEND(Res); - <<"/ctcp ", Rest/binary>> -> - Words = str:tokens(Rest, <<" ">>), - case Words of - [CtcpDest | _] -> - CtcpCmd = str:to_upper( - str:substr(Rest, - str:str(Rest, - <<" ">>) - + 1)), - Res = io_lib:format("PRIVMSG ~s :~s\r\n", - [CtcpDest, - <<"\001", - CtcpCmd/binary, - "\001">>]), - ?SEND(Res); - _ -> ok - end; - _ -> - Strings = str:tokens(Body, <<"\n">>), - Res = iolist_to_binary( - lists:map( - fun (S) -> - io_lib:format( - "PRIVMSG ~s :~s\r\n", - [Nick, S]) - end, - Strings)), - ?SEND(Res) - end; - <<"error">> -> stop; - _ -> StateData - end, - if NewStateData == stop -> {stop, normal, StateData}; - true -> {next_state, StateName, NewStateData} - end; -handle_info({route_nick, _Nick, _Packet}, StateName, - StateData) -> - {next_state, StateName, StateData}; -handle_info({ircstring, - <<$P, $I, $N, $G, $\s, ID/binary>>}, - StateName, StateData) -> - send_text(StateData, <<"PONG ", ID/binary, "\r\n">>), - {next_state, StateName, StateData}; -handle_info({ircstring, <<$:, String/binary>>}, - wait_for_registration, StateData) -> - Words = str:tokens(String, <<" ">>), - {NewState, NewStateData} = case Words of - [_, <<"001">> | _] -> - send_text(StateData, - io_lib:format("CODEPAGE ~s\r\n", - [StateData#state.encoding])), - {stream_established, StateData}; - [_, <<"433">> | _] -> - {error, - {error, - error_nick_in_use(StateData, String), - StateData}}; - [_, <<$4, _, _>> | _] -> - {error, - {error, - error_unknown_num(StateData, String, - <<"cancel">>), - StateData}}; - [_, <<$5, _, _>> | _] -> - {error, - {error, - error_unknown_num(StateData, String, - <<"cancel">>), - StateData}}; - _ -> - ?DEBUG("unknown irc command '~s'~n", - [String]), - {wait_for_registration, StateData} - end, - if NewState == error -> {stop, normal, NewStateData}; - true -> {next_state, NewState, NewStateData} - end; -handle_info({ircstring, <<$:, String/binary>>}, - _StateName, StateData) -> - Words = str:tokens(String, <<" ">>), - NewStateData = case Words of - [_, <<"353">> | Items] -> - process_channel_list(StateData, Items); - [_, <<"332">>, _Nick, <<$#, Chan/binary>> | _] -> - process_channel_topic(StateData, Chan, String), - StateData; - [_, <<"333">>, _Nick, <<$#, Chan/binary>> | _] -> - process_channel_topic_who(StateData, Chan, String), - StateData; - [_, <<"318">>, _, Nick | _] -> - process_endofwhois(StateData, String, Nick), StateData; - [_, <<"311">>, _, Nick, Ident, Irchost | _] -> - process_whois311(StateData, String, Nick, Ident, - Irchost), - StateData; - [_, <<"312">>, _, Nick, Ircserver | _] -> - process_whois312(StateData, String, Nick, Ircserver), - StateData; - [_, <<"319">>, _, Nick | _] -> - process_whois319(StateData, String, Nick), StateData; - [_, <<"433">> | _] -> - process_nick_in_use(StateData, String); - % CODEPAGE isn't standard, so don't complain if it's not there. - [_, <<"421">>, _, <<"CODEPAGE">> | _] -> StateData; - [_, <<$4, _, _>> | _] -> - process_num_error(StateData, String); - [_, <<$5, _, _>> | _] -> - process_num_error(StateData, String); - [From, <<"PRIVMSG">>, <<$#, Chan/binary>> | _] -> - process_chanprivmsg(StateData, Chan, From, String), - StateData; - [From, <<"NOTICE">>, <<$#, Chan/binary>> | _] -> - process_channotice(StateData, Chan, From, String), - StateData; - [From, <<"PRIVMSG">>, Nick, <<":\001VERSION\001">> - | _] -> - process_version(StateData, Nick, From), StateData; - [From, <<"PRIVMSG">>, Nick, <<":\001USERINFO\001">> - | _] -> - process_userinfo(StateData, Nick, From), StateData; - [From, <<"PRIVMSG">>, Nick | _] -> - process_privmsg(StateData, Nick, From, String), - StateData; - [From, <<"NOTICE">>, Nick | _] -> - process_notice(StateData, Nick, From, String), - StateData; - [From, <<"TOPIC">>, <<$#, Chan/binary>> | _] -> - process_topic(StateData, Chan, From, String), - StateData; - [From, <<"PART">>, <<$#, Chan/binary>> | _] -> - process_part(StateData, Chan, From, String); - [From, <<"QUIT">> | _] -> - process_quit(StateData, From, String); - [From, <<"JOIN">>, Chan | _] -> - process_join(StateData, Chan, From, String); - [From, <<"MODE">>, <<$#, Chan/binary>>, <<"+o">>, Nick - | _] -> - process_mode_o(StateData, Chan, From, Nick, - <<"admin">>, <<"moderator">>), - StateData; - [From, <<"MODE">>, <<$#, Chan/binary>>, <<"-o">>, Nick - | _] -> - process_mode_o(StateData, Chan, From, Nick, - <<"member">>, <<"participant">>), - StateData; - [From, <<"KICK">>, <<$#, Chan/binary>>, Nick | _] -> - process_kick(StateData, Chan, From, Nick, String), - StateData; - [From, <<"NICK">>, Nick | _] -> - process_nick(StateData, From, Nick); - _ -> - ?DEBUG("unknown irc command '~s'~n", [String]), - StateData - end, - NewStateData1 = case StateData#state.outbuf of - <<"">> -> NewStateData; - Data -> - send_text(NewStateData, Data), - NewStateData#state{outbuf = <<"">>} - end, - {next_state, stream_established, NewStateData1}; -handle_info({ircstring, - <<$E, $R, $R, $O, $R, _/binary>> = String}, - StateName, StateData) -> - process_error(StateData, String), - {next_state, StateName, StateData}; -handle_info({ircstring, String}, StateName, - StateData) -> - ?DEBUG("unknown irc command '~s'~n", [String]), - {next_state, StateName, StateData}; -handle_info({send_text, Text}, StateName, StateData) -> - send_text(StateData, Text), - {next_state, StateName, StateData}; -handle_info({tcp, _Socket, Data}, StateName, - StateData) -> - Buf = <<(StateData#state.inbuf)/binary, Data/binary>>, - Strings = ejabberd_regexp:split(<< <> - || <> <= Buf, C /= $\r >>, - <<"\n">>), - ?DEBUG("strings=~p~n", [Strings]), - NewBuf = process_lines(StateData#state.encoding, - Strings), - {next_state, StateName, - StateData#state{inbuf = NewBuf}}; -handle_info({tcp_closed, _Socket}, StateName, - StateData) -> - gen_fsm:send_event(self(), closed), - {next_state, StateName, StateData}; -handle_info({tcp_error, _Socket, _Reason}, StateName, - StateData) -> - gen_fsm:send_event(self(), closed), - {next_state, StateName, StateData}. - -%%---------------------------------------------------------------------- -%% Func: terminate/3 -%% Purpose: Shutdown the fsm -%% Returns: any -%%---------------------------------------------------------------------- -terminate(_Reason, _StateName, FullStateData) -> - {Error, StateData} = case FullStateData of - {error, SError, SStateData} -> {SError, SStateData}; - _ -> - {#xmlel{name = <<"error">>, - attrs = [{<<"code">>, <<"502">>}], - children = - [{xmlcdata, - <<"Server Connect Failed">>}]}, - FullStateData} - end, - (FullStateData#state.mod):closed_connection(StateData#state.host, - StateData#state.user, - StateData#state.server), - bounce_messages(<<"Server Connect Failed">>), - lists:foreach(fun (Chan) -> - Stanza = #xmlel{name = <<"presence">>, - attrs = [{<<"type">>, <<"error">>}], - children = [Error]}, - send_stanza(Chan, StateData, Stanza) - end, - dict:fetch_keys(StateData#state.channels)), - case StateData#state.socket of - undefined -> ok; - Socket -> gen_tcp:close(Socket) - end, - ok. - -send_stanza(Chan, StateData, Stanza) -> - ejabberd_router:route( - jlib:make_jid( - iolist_to_binary([Chan, - <<"%">>, - StateData#state.server]), - StateData#state.host, - StateData#state.nick), - StateData#state.user, Stanza). - -send_stanza_unavailable(Chan, StateData) -> - Affiliation = <<"member">>, - Role = <<"none">>, - Stanza = #xmlel{name = <<"presence">>, - attrs = [{<<"type">>, <<"unavailable">>}], - children = - [#xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_MUC_USER}], - children = - [#xmlel{name = <<"item">>, - attrs = - [{<<"affiliation">>, - Affiliation}, - {<<"role">>, Role}], - children = []}, - #xmlel{name = <<"status">>, - attrs = [{<<"code">>, <<"110">>}], - children = []}]}]}, - send_stanza(Chan, StateData, Stanza). - -%%%---------------------------------------------------------------------- -%%% Internal functions -%%%---------------------------------------------------------------------- - -send_text(#state{socket = Socket, encoding = Encoding}, - Text) -> - CText = iconv:convert(<<"utf-8">>, Encoding, iolist_to_binary(Text)), - gen_tcp:send(Socket, CText). - -%send_queue(Socket, Q) -> -% case queue:out(Q) of -% {{value, El}, Q1} -> -% send_element(Socket, El), -% send_queue(Socket, Q1); -% {empty, Q1} -> -% ok -% end. - -bounce_messages(Reason) -> - receive - {send_element, El} -> - #xmlel{attrs = Attrs} = El, - case xml:get_attr_s(<<"type">>, Attrs) of - <<"error">> -> ok; - _ -> - Err = jlib:make_error_reply(El, <<"502">>, Reason), - From = jlib:string_to_jid(xml:get_attr_s(<<"from">>, - Attrs)), - To = jlib:string_to_jid(xml:get_attr_s(<<"to">>, - Attrs)), - ejabberd_router:route(To, From, Err) - end, - bounce_messages(Reason) - after 0 -> ok - end. - -route_chan(Pid, Channel, Resource, Packet) -> - Pid ! {route_chan, Channel, Resource, Packet}. - -route_nick(Pid, Nick, Packet) -> - Pid ! {route_nick, Nick, Packet}. - -process_lines(_Encoding, [S]) -> S; -process_lines(Encoding, [S | Ss]) -> - self() ! - {ircstring, iconv:convert(Encoding, <<"utf-8">>, S)}, - process_lines(Encoding, Ss). - -process_channel_list(StateData, Items) -> - process_channel_list_find_chan(StateData, Items). - -process_channel_list_find_chan(StateData, []) -> - StateData; -process_channel_list_find_chan(StateData, - [<<$#, Chan/binary>> | Items]) -> - process_channel_list_users(StateData, Chan, Items); -process_channel_list_find_chan(StateData, - [_ | Items]) -> - process_channel_list_find_chan(StateData, Items). - -process_channel_list_users(StateData, _Chan, []) -> - StateData; -process_channel_list_users(StateData, Chan, - [User | Items]) -> - NewStateData = process_channel_list_user(StateData, - Chan, User), - process_channel_list_users(NewStateData, Chan, Items). - -process_channel_list_user(StateData, Chan, User) -> - User1 = case User of - <<$:, U1/binary>> -> U1; - _ -> User - end, - {User2, Affiliation, Role} = case User1 of - <<$@, U2/binary>> -> - {U2, <<"admin">>, <<"moderator">>}; - <<$+, U2/binary>> -> - {U2, <<"member">>, <<"participant">>}; - <<$%, U2/binary>> -> - {U2, <<"admin">>, <<"moderator">>}; - <<$&, U2/binary>> -> - {U2, <<"admin">>, <<"moderator">>}; - <<$~, U2/binary>> -> - {U2, <<"admin">>, <<"moderator">>}; - _ -> {User1, <<"member">>, <<"participant">>} - end, - ejabberd_router:route(jlib:make_jid(iolist_to_binary([Chan, - <<"%">>, - StateData#state.server]), - StateData#state.host, User2), - StateData#state.user, - #xmlel{name = <<"presence">>, attrs = [], - children = - [#xmlel{name = <<"x">>, - attrs = - [{<<"xmlns">>, ?NS_MUC_USER}], - children = - [#xmlel{name = <<"item">>, - attrs = - [{<<"affiliation">>, - Affiliation}, - {<<"role">>, - Role}], - children = []}]}]}), - case catch dict:update(Chan, - fun (Ps) -> (?SETS):add_element(User2, Ps) end, - StateData#state.channels) - of - {'EXIT', _} -> StateData; - NS -> StateData#state{channels = NS} - end. - -process_channel_topic(StateData, Chan, String) -> - Msg = ejabberd_regexp:replace(String, <<".*332[^:]*:">>, - <<"">>), - Msg1 = filter_message(Msg), - ejabberd_router:route(jlib:make_jid(iolist_to_binary([Chan, - <<"%">>, - StateData#state.server]), - StateData#state.host, <<"">>), - StateData#state.user, - #xmlel{name = <<"message">>, - attrs = [{<<"type">>, <<"groupchat">>}], - children = - [#xmlel{name = <<"subject">>, attrs = [], - children = [{xmlcdata, Msg1}]}, - #xmlel{name = <<"body">>, attrs = [], - children = - [{xmlcdata, - <<"Topic for #", Chan/binary, - ": ", Msg1/binary>>}]}]}). - -process_channel_topic_who(StateData, Chan, String) -> - Words = str:tokens(String, <<" ">>), - Msg1 = case Words of - [_, <<"333">>, _, _Chan, Whoset, Timeset] -> - {Unixtimeset, _Rest} = str:to_integer(Timeset), - <<"Topic for #", Chan/binary, " set by ", Whoset/binary, - " at ", (unixtime2string(Unixtimeset))/binary>>; - [_, <<"333">>, _, _Chan, Whoset | _] -> - <<"Topic for #", Chan/binary, " set by ", - Whoset/binary>>; - _ -> String - end, - Msg2 = filter_message(Msg1), - ejabberd_router:route(jlib:make_jid(iolist_to_binary([Chan, - <<"%">>, - StateData#state.server]), - StateData#state.host, <<"">>), - StateData#state.user, - #xmlel{name = <<"message">>, - attrs = [{<<"type">>, <<"groupchat">>}], - children = - [#xmlel{name = <<"body">>, attrs = [], - children = [{xmlcdata, Msg2}]}]}). - -error_nick_in_use(_StateData, String) -> - Msg = ejabberd_regexp:replace(String, - <<".*433 +[^ ]* +">>, <<"">>), - Msg1 = filter_message(Msg), - #xmlel{name = <<"error">>, - attrs = - [{<<"code">>, <<"409">>}, {<<"type">>, <<"cancel">>}], - children = - [#xmlel{name = <<"conflict">>, - attrs = [{<<"xmlns">>, ?NS_STANZAS}], children = []}, - #xmlel{name = <<"text">>, - attrs = [{<<"xmlns">>, ?NS_STANZAS}], - children = [{xmlcdata, Msg1}]}]}. - -process_nick_in_use(StateData, String) -> - Error = error_nick_in_use(StateData, String), - case StateData#state.nickchannel of - undefined -> - % Shouldn't happen with a well behaved server - StateData; - Chan -> - ejabberd_router:route(jlib:make_jid(iolist_to_binary([Chan, - <<"%">>, - StateData#state.server]), - StateData#state.host, - StateData#state.nick), - StateData#state.user, - #xmlel{name = <<"presence">>, - attrs = [{<<"type">>, <<"error">>}], - children = [Error]}), - StateData#state{nickchannel = undefined} - end. - -process_num_error(StateData, String) -> - Error = error_unknown_num(StateData, String, - <<"continue">>), - lists:foreach(fun (Chan) -> - ejabberd_router:route( - jlib:make_jid( - iolist_to_binary( - [Chan, - <<"%">>, - StateData#state.server]), - StateData#state.host, - StateData#state.nick), - StateData#state.user, - #xmlel{name = <<"message">>, - attrs = - [{<<"type">>, - <<"error">>}], - children = [Error]}) - end, - dict:fetch_keys(StateData#state.channels)), - StateData. - -process_endofwhois(StateData, _String, Nick) -> - ejabberd_router:route(jlib:make_jid(iolist_to_binary([Nick, - <<"!">>, - StateData#state.server]), - StateData#state.host, <<"">>), - StateData#state.user, - #xmlel{name = <<"message">>, - attrs = [{<<"type">>, <<"chat">>}], - children = - [#xmlel{name = <<"body">>, attrs = [], - children = - [{xmlcdata, - <<"End of WHOIS">>}]}]}). - -process_whois311(StateData, String, Nick, Ident, - Irchost) -> - Fullname = ejabberd_regexp:replace(String, - <<".*311[^:]*:">>, <<"">>), - ejabberd_router:route(jlib:make_jid(iolist_to_binary([Nick, - <<"!">>, - StateData#state.server]), - StateData#state.host, <<"">>), - StateData#state.user, - #xmlel{name = <<"message">>, - attrs = [{<<"type">>, <<"chat">>}], - children = - [#xmlel{name = <<"body">>, attrs = [], - children = - [{xmlcdata, - iolist_to_binary( - [<<"WHOIS: ">>, - Nick, - <<" is ">>, - Ident, - <<"@">>, - Irchost, - <<" : ">>, - Fullname])}]}]}). - -process_whois312(StateData, String, Nick, Ircserver) -> - Ircserverdesc = ejabberd_regexp:replace(String, - <<".*312[^:]*:">>, <<"">>), - ejabberd_router:route(jlib:make_jid(iolist_to_binary([Nick, - <<"!">>, - StateData#state.server]), - StateData#state.host, <<"">>), - StateData#state.user, - #xmlel{name = <<"message">>, - attrs = [{<<"type">>, <<"chat">>}], - children = - [#xmlel{name = <<"body">>, attrs = [], - children = - [{xmlcdata, - iolist_to_binary( - [<<"WHOIS: ">>, - Nick, - <<" use ">>, - Ircserver, - <<" : ">>, - Ircserverdesc])}]}]}). - -process_whois319(StateData, String, Nick) -> - Chanlist = ejabberd_regexp:replace(String, - <<".*319[^:]*:">>, <<"">>), - ejabberd_router:route(jlib:make_jid(iolist_to_binary( - [Nick, - <<"!">>, - StateData#state.server]), - StateData#state.host, <<"">>), - StateData#state.user, - #xmlel{name = <<"message">>, - attrs = [{<<"type">>, <<"chat">>}], - children = - [#xmlel{name = <<"body">>, attrs = [], - children = - [{xmlcdata, - iolist_to_binary( - [<<"WHOIS: ">>, - Nick, - <<" is on ">>, - Chanlist])}]}]}). - -process_chanprivmsg(StateData, Chan, From, String) -> - [FromUser | _] = str:tokens(From, <<"!">>), - Msg = ejabberd_regexp:replace(String, - <<".*PRIVMSG[^:]*:">>, <<"">>), - Msg1 = case Msg of - <<1, $A, $C, $T, $I, $O, $N, $\s, Rest/binary>> -> - <<"/me ", Rest/binary>>; - _ -> Msg - end, - Msg2 = filter_message(Msg1), - ejabberd_router:route(jlib:make_jid(iolist_to_binary( - [Chan, - <<"%">>, - StateData#state.server]), - StateData#state.host, FromUser), - StateData#state.user, - #xmlel{name = <<"message">>, - attrs = [{<<"type">>, <<"groupchat">>}], - children = - [#xmlel{name = <<"body">>, attrs = [], - children = [{xmlcdata, Msg2}]}]}). - -process_channotice(StateData, Chan, From, String) -> - [FromUser | _] = str:tokens(From, <<"!">>), - Msg = ejabberd_regexp:replace(String, - <<".*NOTICE[^:]*:">>, <<"">>), - Msg1 = case Msg of - <<1, $A, $C, $T, $I, $O, $N, $\s, Rest/binary>> -> - <<"/me ", Rest/binary>>; - _ -> <<"/me NOTICE: ", Msg/binary>> - end, - Msg2 = filter_message(Msg1), - ejabberd_router:route(jlib:make_jid(iolist_to_binary( - [Chan, - <<"%">>, - StateData#state.server]), - StateData#state.host, FromUser), - StateData#state.user, - #xmlel{name = <<"message">>, - attrs = [{<<"type">>, <<"groupchat">>}], - children = - [#xmlel{name = <<"body">>, attrs = [], - children = [{xmlcdata, Msg2}]}]}). - -process_privmsg(StateData, _Nick, From, String) -> - [FromUser | _] = str:tokens(From, <<"!">>), - Msg = ejabberd_regexp:replace(String, - <<".*PRIVMSG[^:]*:">>, <<"">>), - Msg1 = case Msg of - <<1, $A, $C, $T, $I, $O, $N, $\s, Rest/binary>> -> - <<"/me ", Rest/binary>>; - _ -> Msg - end, - Msg2 = filter_message(Msg1), - ejabberd_router:route(jlib:make_jid(iolist_to_binary( - [FromUser, - <<"!">>, - StateData#state.server]), - StateData#state.host, <<"">>), - StateData#state.user, - #xmlel{name = <<"message">>, - attrs = [{<<"type">>, <<"chat">>}], - children = - [#xmlel{name = <<"body">>, attrs = [], - children = [{xmlcdata, Msg2}]}]}). - -process_notice(StateData, _Nick, From, String) -> - [FromUser | _] = str:tokens(From, <<"!">>), - Msg = ejabberd_regexp:replace(String, - <<".*NOTICE[^:]*:">>, <<"">>), - Msg1 = case Msg of - <<1, $A, $C, $T, $I, $O, $N, $\s, Rest/binary>> -> - <<"/me ", Rest/binary>>; - _ -> <<"/me NOTICE: ", Msg/binary>> - end, - Msg2 = filter_message(Msg1), - ejabberd_router:route(jlib:make_jid(iolist_to_binary( - [FromUser, - <<"!">>, - StateData#state.server]), - StateData#state.host, <<"">>), - StateData#state.user, - #xmlel{name = <<"message">>, - attrs = [{<<"type">>, <<"chat">>}], - children = - [#xmlel{name = <<"body">>, attrs = [], - children = [{xmlcdata, Msg2}]}]}). - -process_version(StateData, _Nick, From) -> - [FromUser | _] = str:tokens(From, <<"!">>), - send_text(StateData, - io_lib:format("NOTICE ~s :\001VERSION ejabberd IRC " - "transport ~s (c) Alexey Shchepin\001\r\n", - [FromUser, ?VERSION]) - ++ - io_lib:format("NOTICE ~s :\001VERSION http://ejabberd.jabber" - "studio.org/\001\r\n", - [FromUser])). - -process_userinfo(StateData, _Nick, From) -> - [FromUser | _] = str:tokens(From, <<"!">>), - send_text(StateData, - io_lib:format("NOTICE ~s :\001USERINFO xmpp:~s\001\r\n", - [FromUser, - jlib:jid_to_string(StateData#state.user)])). - -process_topic(StateData, Chan, From, String) -> - [FromUser | _] = str:tokens(From, <<"!">>), - Msg = ejabberd_regexp:replace(String, - <<".*TOPIC[^:]*:">>, <<"">>), - Msg1 = filter_message(Msg), - ejabberd_router:route(jlib:make_jid(iolist_to_binary( - [Chan, - <<"%">>, - StateData#state.server]), - StateData#state.host, FromUser), - StateData#state.user, - #xmlel{name = <<"message">>, - attrs = [{<<"type">>, <<"groupchat">>}], - children = - [#xmlel{name = <<"subject">>, attrs = [], - children = [{xmlcdata, Msg1}]}, - #xmlel{name = <<"body">>, attrs = [], - children = - [{xmlcdata, - <<"/me has changed the subject to: ", - Msg1/binary>>}]}]}). - -process_part(StateData, Chan, From, String) -> - [FromUser | FromIdent] = str:tokens(From, <<"!">>), - Msg = ejabberd_regexp:replace(String, - <<".*PART[^:]*:">>, <<"">>), - Msg1 = filter_message(Msg), - ejabberd_router:route(jlib:make_jid(iolist_to_binary( - [Chan, - <<"%">>, - StateData#state.server]), - StateData#state.host, FromUser), - StateData#state.user, - #xmlel{name = <<"presence">>, - attrs = [{<<"type">>, <<"unavailable">>}], - children = - [#xmlel{name = <<"x">>, - attrs = - [{<<"xmlns">>, ?NS_MUC_USER}], - children = - [#xmlel{name = <<"item">>, - attrs = - [{<<"affiliation">>, - <<"member">>}, - {<<"role">>, - <<"none">>}], - children = []}]}, - #xmlel{name = <<"status">>, attrs = [], - children = - [{xmlcdata, - list_to_binary( - [Msg1, " (", - FromIdent, ")"])}]}]}), - case catch dict:update(Chan, - fun (Ps) -> remove_element(FromUser, Ps) end, - StateData#state.channels) - of - {'EXIT', _} -> StateData; - NS -> StateData#state{channels = NS} - end. - -process_quit(StateData, From, String) -> - [FromUser | FromIdent] = str:tokens(From, <<"!">>), - Msg = ejabberd_regexp:replace(String, - <<".*QUIT[^:]*:">>, <<"">>), - Msg1 = filter_message(Msg), - dict:map(fun (Chan, Ps) -> - case (?SETS):is_member(FromUser, Ps) of - true -> - ejabberd_router:route(jlib:make_jid(iolist_to_binary( - [Chan, - <<"%">>, - StateData#state.server]), - StateData#state.host, - FromUser), - StateData#state.user, - #xmlel{name = <<"presence">>, - attrs = - [{<<"type">>, - <<"unavailable">>}], - children = - [#xmlel{name = - <<"x">>, - attrs = - [{<<"xmlns">>, - ?NS_MUC_USER}], - children = - [#xmlel{name - = - <<"item">>, - attrs - = - [{<<"affiliation">>, - <<"member">>}, - {<<"role">>, - <<"none">>}], - children - = - []}]}, - #xmlel{name = - <<"status">>, - attrs = [], - children = - [{xmlcdata, - list_to_binary( - [Msg1, " (", - FromIdent, - ")"])}]}]}), - remove_element(FromUser, Ps); - _ -> Ps - end - end, - StateData#state.channels), - StateData. - -process_join(StateData, Channel, From, _String) -> - [FromUser | FromIdent] = str:tokens(From, <<"!">>), - [Chan | _] = binary:split(Channel, <<":#">>), - ejabberd_router:route(jlib:make_jid(iolist_to_binary( - [Chan, - <<"%">>, - StateData#state.server]), - StateData#state.host, FromUser), - StateData#state.user, - #xmlel{name = <<"presence">>, attrs = [], - children = - [#xmlel{name = <<"x">>, - attrs = - [{<<"xmlns">>, ?NS_MUC_USER}], - children = - [#xmlel{name = <<"item">>, - attrs = - [{<<"affiliation">>, - <<"member">>}, - {<<"role">>, - <<"participant">>}], - children = []}]}, - #xmlel{name = <<"status">>, attrs = [], - children = - [{xmlcdata, - list_to_binary(FromIdent)}]}]}), - case catch dict:update(Chan, - fun (Ps) -> (?SETS):add_element(FromUser, Ps) end, - StateData#state.channels) - of - {'EXIT', _} -> StateData; - NS -> StateData#state{channels = NS} - end. - -process_mode_o(StateData, Chan, _From, Nick, - Affiliation, Role) -> - ejabberd_router:route(jlib:make_jid(iolist_to_binary( - [Chan, - <<"%">>, - StateData#state.server]), - StateData#state.host, Nick), - StateData#state.user, - #xmlel{name = <<"presence">>, attrs = [], - children = - [#xmlel{name = <<"x">>, - attrs = - [{<<"xmlns">>, ?NS_MUC_USER}], - children = - [#xmlel{name = <<"item">>, - attrs = - [{<<"affiliation">>, - Affiliation}, - {<<"role">>, - Role}], - children = []}]}]}). - -process_kick(StateData, Chan, From, Nick, String) -> - Msg = lists:last(str:tokens(String, <<":">>)), - Msg2 = <>, - ejabberd_router:route(jlib:make_jid(iolist_to_binary( - [Chan, - <<"%">>, - StateData#state.server]), - StateData#state.host, <<"">>), - StateData#state.user, - #xmlel{name = <<"message">>, - attrs = [{<<"type">>, <<"groupchat">>}], - children = - [#xmlel{name = <<"body">>, attrs = [], - children = [{xmlcdata, Msg2}]}]}), - ejabberd_router:route(jlib:make_jid(iolist_to_binary( - [Chan, - <<"%">>, - StateData#state.server]), - StateData#state.host, Nick), - StateData#state.user, - #xmlel{name = <<"presence">>, - attrs = [{<<"type">>, <<"unavailable">>}], - children = - [#xmlel{name = <<"x">>, - attrs = - [{<<"xmlns">>, ?NS_MUC_USER}], - children = - [#xmlel{name = <<"item">>, - attrs = - [{<<"affiliation">>, - <<"none">>}, - {<<"role">>, - <<"none">>}], - children = []}, - #xmlel{name = <<"status">>, - attrs = - [{<<"code">>, - <<"307">>}], - children = []}]}]}). - -process_nick(StateData, From, NewNick) -> - [FromUser | _] = str:tokens(From, <<"!">>), - [Nick | _] = binary:split(NewNick, <<":">>), - NewChans = dict:map(fun (Chan, Ps) -> - case (?SETS):is_member(FromUser, Ps) of - true -> - ejabberd_router:route(jlib:make_jid( - iolist_to_binary( - [Chan, - <<"%">>, - StateData#state.server]), - StateData#state.host, - FromUser), - StateData#state.user, - #xmlel{name = - <<"presence">>, - attrs = - [{<<"type">>, - <<"unavailable">>}], - children = - [#xmlel{name - = - <<"x">>, - attrs - = - [{<<"xmlns">>, - ?NS_MUC_USER}], - children - = - [#xmlel{name - = - <<"item">>, - attrs - = - [{<<"affiliation">>, - <<"member">>}, - {<<"role">>, - <<"participant">>}, - {<<"nick">>, - Nick}], - children - = - []}, - #xmlel{name - = - <<"status">>, - attrs - = - [{<<"code">>, - <<"303">>}], - children - = - []}]}]}), - ejabberd_router:route(jlib:make_jid( - iolist_to_binary( - [Chan, - <<"%">>, - StateData#state.server]), - StateData#state.host, - Nick), - StateData#state.user, - #xmlel{name = - <<"presence">>, - attrs = [], - children = - [#xmlel{name - = - <<"x">>, - attrs - = - [{<<"xmlns">>, - ?NS_MUC_USER}], - children - = - [#xmlel{name - = - <<"item">>, - attrs - = - [{<<"affiliation">>, - <<"member">>}, - {<<"role">>, - <<"participant">>}], - children - = - []}]}]}), - (?SETS):add_element(Nick, - remove_element(FromUser, - Ps)); - _ -> Ps - end - end, - StateData#state.channels), - if FromUser == StateData#state.nick -> - StateData#state{nick = Nick, nickchannel = undefined, - channels = NewChans}; - true -> StateData#state{channels = NewChans} - end. - -process_error(StateData, String) -> - lists:foreach(fun (Chan) -> - ejabberd_router:route(jlib:make_jid( - iolist_to_binary( - [Chan, - <<"%">>, - StateData#state.server]), - StateData#state.host, - StateData#state.nick), - StateData#state.user, - #xmlel{name = <<"presence">>, - attrs = - [{<<"type">>, - <<"error">>}], - children = - [#xmlel{name = - <<"error">>, - attrs = - [{<<"code">>, - <<"502">>}], - children = - [{xmlcdata, - String}]}]}) - end, - dict:fetch_keys(StateData#state.channels)). - -error_unknown_num(_StateData, String, Type) -> - Msg = ejabberd_regexp:replace(String, - <<".*[45][0-9][0-9] +[^ ]* +">>, <<"">>), - Msg1 = filter_message(Msg), - #xmlel{name = <<"error">>, - attrs = [{<<"code">>, <<"500">>}, {<<"type">>, Type}], - children = - [#xmlel{name = <<"undefined-condition">>, - attrs = [{<<"xmlns">>, ?NS_STANZAS}], children = []}, - #xmlel{name = <<"text">>, - attrs = [{<<"xmlns">>, ?NS_STANZAS}], - children = [{xmlcdata, Msg1}]}]}. - -remove_element(E, Set) -> - case (?SETS):is_element(E, Set) of - true -> (?SETS):del_element(E, Set); - _ -> Set - end. - -iq_admin(StateData, Channel, From, To, - #iq{type = Type, xmlns = XMLNS, sub_el = SubEl} = IQ) -> - case catch process_iq_admin(StateData, Channel, Type, - SubEl) - of - {'EXIT', Reason} -> ?ERROR_MSG("~p", [Reason]); - Res -> - if Res /= ignore -> - ResIQ = case Res of - {result, ResEls} -> - IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"query">>, - attrs = [{<<"xmlns">>, XMLNS}], - children = ResEls}]}; - {error, Error} -> - IQ#iq{type = error, sub_el = [SubEl, Error]} - end, - ejabberd_router:route(To, From, jlib:iq_to_xml(ResIQ)); - true -> ok - end - end. - -process_iq_admin(StateData, Channel, set, SubEl) -> - case xml:get_subtag(SubEl, <<"item">>) of - false -> {error, ?ERR_BAD_REQUEST}; - ItemEl -> - Nick = xml:get_tag_attr_s(<<"nick">>, ItemEl), - Affiliation = xml:get_tag_attr_s(<<"affiliation">>, - ItemEl), - Role = xml:get_tag_attr_s(<<"role">>, ItemEl), - Reason = xml:get_path_s(ItemEl, - [{elem, <<"reason">>}, cdata]), - process_admin(StateData, Channel, Nick, Affiliation, - Role, Reason) - end; -process_iq_admin(_StateData, _Channel, get, _SubEl) -> - {error, ?ERR_FEATURE_NOT_IMPLEMENTED}. - -process_admin(_StateData, _Channel, <<"">>, - _Affiliation, _Role, _Reason) -> - {error, ?ERR_FEATURE_NOT_IMPLEMENTED}; -process_admin(StateData, Channel, Nick, _Affiliation, - <<"none">>, Reason) -> - case Reason of - <<"">> -> - send_text(StateData, - io_lib:format("KICK #~s ~s\r\n", [Channel, Nick])); - _ -> - send_text(StateData, - io_lib:format("KICK #~s ~s :~s\r\n", - [Channel, Nick, Reason])) - end, - {result, []}; -process_admin(_StateData, _Channel, _Nick, _Affiliation, - _Role, _Reason) -> - {error, ?ERR_FEATURE_NOT_IMPLEMENTED}. - -filter_message(Msg) -> - list_to_binary( - lists:filter(fun (C) -> - if (C < 32) and (C /= 9) and (C /= 10) and (C /= 13) -> - false; - true -> true - end - end, - binary_to_list(filter_mirc_colors(Msg)))). - -filter_mirc_colors(Msg) -> - ejabberd_regexp:greplace(Msg, - <<"(\\003[0-9]+)(,[0-9]+)?">>, <<"">>). - -unixtime2string(Unixtime) -> - Secs = Unixtime + - calendar:datetime_to_gregorian_seconds({{1970, 1, 1}, - {0, 0, 0}}), - {{Year, Month, Day}, {Hour, Minute, Second}} = - calendar:universal_time_to_local_time(calendar:gregorian_seconds_to_datetime(Secs)), - iolist_to_binary(io_lib:format("~4..0w-~2..0w-~2..0w ~2..0w:~2..0w:~2..0w", - [Year, Month, Day, Hour, Minute, Second])). diff --git a/src/mod_jidprep.erl b/src/mod_jidprep.erl new file mode 100644 index 000000000..3de051156 --- /dev/null +++ b/src/mod_jidprep.erl @@ -0,0 +1,142 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_jidprep.erl +%%% Author : Holger Weiss +%%% Purpose : JID Prep (XEP-0328) +%%% Created : 11 Sep 2019 by Holger Weiss +%%% +%%% +%%% ejabberd, Copyright (C) 2019-2025 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_jidprep). +-author('holger@zedat.fu-berlin.de'). +-protocol({xep, 328, '0.1', '19.09', "complete", ""}). + +-behaviour(gen_mod). + +%% gen_mod callbacks. +-export([start/2, stop/1, reload/3, mod_opt_type/1, mod_options/1, depends/2]). +-export([mod_doc/0]). + +%% ejabberd_hooks callbacks. +-export([disco_local_features/5]). + +%% gen_iq_handler callback. +-export([process_iq/1]). + +-include("logger.hrl"). +-include("translate.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). + +%%-------------------------------------------------------------------- +%% gen_mod callbacks. +%%-------------------------------------------------------------------- +-spec start(binary(), gen_mod:opts()) -> {ok, [gen_mod:registration()]}. +start(_Host, _Opts) -> + {ok, [{iq_handler, ejabberd_local, ?NS_JIDPREP_0, process_iq}, + {hook, disco_local_features, disco_local_features, 50}]}. + +-spec stop(binary()) -> ok. +stop(_Host) -> + ok. + +-spec reload(binary(), gen_mod:opts(), gen_mod:opts()) -> ok. +reload(_Host, _NewOpts, _OldOpts) -> + ok. + +-spec depends(binary(), gen_mod:opts()) -> [{module(), hard | soft}]. +depends(_Host, _Opts) -> + []. + +-spec mod_opt_type(atom()) -> econf:validator(). +mod_opt_type(access) -> + econf:acl(). + +-spec mod_options(binary()) -> [{atom(), any()}]. +mod_options(_Host) -> + [{access, local}]. + +mod_doc() -> + #{desc => + ?T("This module allows XMPP clients to ask the " + "server to normalize a JID as per the rules specified " + "in https://tools.ietf.org/html/rfc6122" + "[RFC 6122: XMPP Address Format]. This might be useful " + "for clients in certain constrained environments, " + "or for testing purposes."), + opts => + [{access, + #{value => ?T("AccessName"), + desc => + ?T("This option defines which access rule will " + "be used to control who is allowed to use this " + "service. The default value is 'local'.")}}]}. + +%%-------------------------------------------------------------------- +%% Service discovery. +%%-------------------------------------------------------------------- +-spec disco_local_features(mod_disco:features_acc(), jid(), jid(), binary(), + binary()) -> mod_disco:features_acc(). +disco_local_features(empty, From, To, Node, Lang) -> + disco_local_features({result, []}, From, To, Node, Lang); +disco_local_features({result, OtherFeatures} = Acc, From, + #jid{lserver = LServer}, <<"">>, _Lang) -> + Access = mod_jidprep_opt:access(LServer), + case acl:match_rule(LServer, Access, From) of + allow -> + {result, [?NS_JIDPREP_0 | OtherFeatures]}; + deny -> + Acc + end; +disco_local_features(Acc, _From, _To, _Node, _Lang) -> + Acc. + +%%-------------------------------------------------------------------- +%% IQ handlers. +%%-------------------------------------------------------------------- +-spec process_iq(iq()) -> iq(). +process_iq(#iq{type = set, lang = Lang} = IQ) -> + Txt = ?T("Value 'set' of 'type' attribute is not allowed"), + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); +process_iq(#iq{from = From, to = #jid{lserver = LServer}, lang = Lang, + sub_els = [#jidprep{jid = #jid{luser = U, + lserver = S, + lresource = R} = JID}]} = IQ) -> + Access = mod_jidprep_opt:access(LServer), + case acl:match_rule(LServer, Access, From) of + allow -> + case jid:make(U, S, R) of + #jid{} = Normalized -> + ?DEBUG("Normalized JID for ~ts: ~ts", + [jid:encode(From), jid:encode(JID)]), + xmpp:make_iq_result(IQ, #jidprep{jid = Normalized}); + error -> % Cannot happen. + ?DEBUG("Normalizing JID failed for ~ts: ~ts", + [jid:encode(From), jid:encode(JID)]), + Txt = ?T("JID normalization failed"), + xmpp:make_error(IQ, xmpp:err_jid_malformed(Txt, Lang)) + end; + deny -> + ?DEBUG("Won't return normalized JID to ~ts: ~ts", + [jid:encode(From), jid:encode(JID)]), + Txt = ?T("JID normalization denied by service policy"), + xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)) + end; +process_iq(#iq{lang = Lang} = IQ) -> + Txt = ?T("No module is handling this query"), + xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)). diff --git a/src/mod_jidprep_opt.erl b/src/mod_jidprep_opt.erl new file mode 100644 index 000000000..f30f70632 --- /dev/null +++ b/src/mod_jidprep_opt.erl @@ -0,0 +1,13 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_jidprep_opt). + +-export([access/1]). + +-spec access(gen_mod:opts() | global | binary()) -> 'local' | acl:acl(). +access(Opts) when is_map(Opts) -> + gen_mod:get_opt(access, Opts); +access(Host) -> + gen_mod:get_module_opt(Host, mod_jidprep, access). + diff --git a/src/mod_last.erl b/src/mod_last.erl index 038378c7b..ed701ea50 100644 --- a/src/mod_last.erl +++ b/src/mod_last.erl @@ -5,7 +5,7 @@ %%% Created : 24 Oct 2003 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -27,325 +27,320 @@ -author('alexey@process-one.net'). +-protocol({xep, 12, '2.0', '0.5.0', "complete", ""}). + -behaviour(gen_mod). --export([start/2, stop/1, process_local_iq/3, export/1, - process_sm_iq/3, on_presence_update/4, import/1, import/3, - store_last_info/4, get_last_info/2, remove_user/2, - transform_options/1]). +-export([start/2, stop/1, reload/3, process_local_iq/1, export/1, + process_sm_iq/1, on_presence_update/4, import_info/0, + import/5, import_start/2, store_last_info/4, get_last_info/2, + remove_user/2, mod_opt_type/1, mod_options/1, mod_doc/0, + register_user/2, depends/2, privacy_check_packet/4]). --include("ejabberd.hrl"). -include("logger.hrl"). - --include("jlib.hrl"). - +-include_lib("xmpp/include/xmpp.hrl"). -include("mod_privacy.hrl"). +-include("mod_last.hrl"). +-include("translate.hrl"). --record(last_activity, {us = {<<"">>, <<"">>} :: {binary(), binary()}, - timestamp = 0 :: non_neg_integer(), - status = <<"">> :: binary()}). +-define(LAST_CACHE, last_activity_cache). + +-type c2s_state() :: ejabberd_c2s:state(). + +-callback init(binary(), gen_mod:opts()) -> any(). +-callback import(binary(), #last_activity{}) -> ok | pass. +-callback get_last(binary(), binary()) -> + {ok, {non_neg_integer(), binary()}} | error | {error, any()}. +-callback store_last_info(binary(), binary(), non_neg_integer(), binary()) -> ok | {error, any()}. +-callback remove_user(binary(), binary()) -> any(). +-callback use_cache(binary()) -> boolean(). +-callback cache_nodes(binary()) -> [node()]. + +-optional_callbacks([use_cache/1, cache_nodes/1]). start(Host, Opts) -> - IQDisc = gen_mod:get_opt(iqdisc, Opts, fun gen_iq_handler:check_type/1, - one_queue), - case gen_mod:db_type(Opts) of - mnesia -> - mnesia:create_table(last_activity, - [{disc_copies, [node()]}, - {attributes, - record_info(fields, last_activity)}]), - update_table(); - _ -> ok - end, - gen_iq_handler:add_iq_handler(ejabberd_local, Host, - ?NS_LAST, ?MODULE, process_local_iq, IQDisc), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, - ?NS_LAST, ?MODULE, process_sm_iq, IQDisc), - ejabberd_hooks:add(remove_user, Host, ?MODULE, - remove_user, 50), - ejabberd_hooks:add(unset_presence_hook, Host, ?MODULE, - on_presence_update, 50). + Mod = gen_mod:db_mod(Opts, ?MODULE), + Mod:init(Host, Opts), + init_cache(Mod, Host, Opts), + {ok, [{iq_handler, ejabberd_local, ?NS_LAST, process_local_iq}, + {iq_handler, ejabberd_sm, ?NS_LAST, process_sm_iq}, + {hook, privacy_check_packet, privacy_check_packet, 30}, + {hook, register_user, register_user, 50}, + {hook, remove_user, remove_user, 50}, + {hook, unset_presence_hook, on_presence_update, 50}]}. -stop(Host) -> - ejabberd_hooks:delete(remove_user, Host, ?MODULE, - remove_user, 50), - ejabberd_hooks:delete(unset_presence_hook, Host, - ?MODULE, on_presence_update, 50), - gen_iq_handler:remove_iq_handler(ejabberd_local, Host, - ?NS_LAST), - gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, - ?NS_LAST). +stop(_Host) -> + ok. + +reload(Host, NewOpts, OldOpts) -> + NewMod = gen_mod:db_mod(NewOpts, ?MODULE), + OldMod = gen_mod:db_mod(OldOpts, ?MODULE), + if NewMod /= OldMod -> + NewMod:init(Host, NewOpts); + true -> + ok + end, + init_cache(NewMod, Host, NewOpts). %%% %%% Uptime of ejabberd node %%% -process_local_iq(_From, _To, - #iq{type = Type, sub_el = SubEl} = IQ) -> - case Type of - set -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; - get -> - Sec = get_node_uptime(), - IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"query">>, - attrs = - [{<<"xmlns">>, ?NS_LAST}, - {<<"seconds">>, - iolist_to_binary(integer_to_list(Sec))}], - children = []}]} - end. +-spec process_local_iq(iq()) -> iq(). +process_local_iq(#iq{type = set, lang = Lang} = IQ) -> + Txt = ?T("Value 'set' of 'type' attribute is not allowed"), + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); +process_local_iq(#iq{type = get} = IQ) -> + xmpp:make_iq_result(IQ, #last{seconds = get_node_uptime()}). -%% @spec () -> integer() +-spec get_node_uptime() -> non_neg_integer(). %% @doc Get the uptime of the ejabberd node, expressed in seconds. %% When ejabberd is starting, ejabberd_config:start/0 stores the datetime. get_node_uptime() -> - case ejabberd_config:get_option( - node_start, - fun(S) when is_integer(S), S >= 0 -> S end) of - undefined -> - trunc(element(1, erlang:statistics(wall_clock)) / 1000); - Now -> - now_to_seconds(now()) - Now - end. - -now_to_seconds({MegaSecs, Secs, _MicroSecs}) -> - MegaSecs * 1000000 + Secs. + NodeStart = ejabberd_config:get_node_start(), + erlang:monotonic_time(second) - NodeStart. %%% %%% Serve queries about user last online %%% -process_sm_iq(From, To, - #iq{type = Type, sub_el = SubEl} = IQ) -> - case Type of - set -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; - get -> - User = To#jid.luser, - Server = To#jid.lserver, - {Subscription, _Groups} = - ejabberd_hooks:run_fold(roster_get_jid_info, Server, - {none, []}, [User, Server, From]), - if (Subscription == both) or (Subscription == from) or - (From#jid.luser == To#jid.luser) and - (From#jid.lserver == To#jid.lserver) -> - UserListRecord = - ejabberd_hooks:run_fold(privacy_get_user_list, Server, - #userlist{}, [User, Server]), - case ejabberd_hooks:run_fold(privacy_check_packet, - Server, allow, - [User, Server, UserListRecord, - {To, From, - #xmlel{name = <<"presence">>, - attrs = [], - children = []}}, - out]) - of - allow -> get_last_iq(IQ, SubEl, User, Server); - deny -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_FORBIDDEN]} - end; - true -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_FORBIDDEN]} - end +-spec process_sm_iq(iq()) -> iq(). +process_sm_iq(#iq{type = set, lang = Lang} = IQ) -> + Txt = ?T("Value 'set' of 'type' attribute is not allowed"), + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); +process_sm_iq(#iq{from = From, to = To, lang = Lang} = IQ) -> + User = To#jid.luser, + Server = To#jid.lserver, + {Subscription, _Ask, _Groups} = + ejabberd_hooks:run_fold(roster_get_jid_info, Server, + {none, none, []}, [User, Server, From]), + if (Subscription == both) or (Subscription == from) or + (From#jid.luser == To#jid.luser) and + (From#jid.lserver == To#jid.lserver) -> + Pres = xmpp:set_from_to(#presence{}, To, From), + case ejabberd_hooks:run_fold(privacy_check_packet, + Server, allow, + [To, Pres, out]) of + allow -> get_last_iq(IQ, User, Server); + deny -> xmpp:make_error(IQ, xmpp:err_forbidden()) + end; + true -> + Txt = ?T("Not subscribed"), + xmpp:make_error(IQ, xmpp:err_subscription_required(Txt, Lang)) end. -%% @spec (LUser::string(), LServer::string()) -> -%% {ok, TimeStamp::integer(), Status::string()} | not_found | {error, Reason} +-spec privacy_check_packet(allow | deny, c2s_state(), stanza(), in | out) -> allow | deny | {stop, deny}. +privacy_check_packet(allow, C2SState, + #iq{from = From, to = To, type = T} = IQ, in) + when T == get; T == set -> + case xmpp:has_subtag(IQ, #last{}) of + true -> + #jid{luser = LUser, lserver = LServer} = To, + {Sub, _, _} = ejabberd_hooks:run_fold( + roster_get_jid_info, LServer, + {none, none, []}, [LUser, LServer, From]), + if Sub == from; Sub == both -> + Pres = #presence{from = To, to = From}, + case ejabberd_hooks:run_fold( + privacy_check_packet, allow, + [C2SState, Pres, out]) of + allow -> + allow; + deny -> + {stop, deny} + end; + true -> + {stop, deny} + end; + false -> + allow + end; +privacy_check_packet(Acc, _, _, _) -> + Acc. + +-spec get_last(binary(), binary()) -> {ok, non_neg_integer(), binary()} | + not_found | {error, any()}. get_last(LUser, LServer) -> - get_last(LUser, LServer, - gen_mod:db_type(LServer, ?MODULE)). - -get_last(LUser, LServer, mnesia) -> - case catch mnesia:dirty_read(last_activity, - {LUser, LServer}) - of - {'EXIT', Reason} -> {error, Reason}; - [] -> not_found; - [#last_activity{timestamp = TimeStamp, - status = Status}] -> - {ok, TimeStamp, Status} - end; -get_last(LUser, LServer, riak) -> - case ejabberd_riak:get(last_activity, last_activity_schema(), - {LUser, LServer}) of - {ok, #last_activity{timestamp = TimeStamp, - status = Status}} -> - {ok, TimeStamp, Status}; - {error, notfound} -> - not_found; - Err -> - Err - end; -get_last(LUser, LServer, odbc) -> - Username = ejabberd_odbc:escape(LUser), - case catch odbc_queries:get_last(LServer, Username) of - {selected, [<<"seconds">>, <<"state">>], []} -> - not_found; - {selected, [<<"seconds">>, <<"state">>], - [[STimeStamp, Status]]} -> - case catch jlib:binary_to_integer(STimeStamp) of - TimeStamp when is_integer(TimeStamp) -> - {ok, TimeStamp, Status}; - Reason -> {error, {invalid_timestamp, Reason}} - end; - Reason -> {error, {invalid_result, Reason}} + Mod = gen_mod:db_mod(LServer, ?MODULE), + Res = case use_cache(Mod, LServer) of + true -> + ets_cache:lookup( + ?LAST_CACHE, {LUser, LServer}, + fun() -> Mod:get_last(LUser, LServer) end); + false -> + Mod:get_last(LUser, LServer) + end, + case Res of + {ok, {TimeStamp, Status}} -> {ok, TimeStamp, Status}; + error -> not_found; + Err -> Err end. -get_last_iq(IQ, SubEl, LUser, LServer) -> +-spec get_last_iq(iq(), binary(), binary()) -> iq(). +get_last_iq(#iq{lang = Lang} = IQ, LUser, LServer) -> case ejabberd_sm:get_user_resources(LUser, LServer) of [] -> case get_last(LUser, LServer) of {error, _Reason} -> - IQ#iq{type = error, - sub_el = [SubEl, ?ERR_INTERNAL_SERVER_ERROR]}; + Txt = ?T("Database failure"), + xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)); not_found -> - IQ#iq{type = error, - sub_el = [SubEl, ?ERR_SERVICE_UNAVAILABLE]}; + Txt = ?T("No info about last activity found"), + xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)); {ok, TimeStamp, Status} -> - TimeStamp2 = now_to_seconds(now()), + TimeStamp2 = erlang:system_time(second), Sec = TimeStamp2 - TimeStamp, - IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"query">>, - attrs = - [{<<"xmlns">>, ?NS_LAST}, - {<<"seconds">>, - iolist_to_binary(integer_to_list(Sec))}], - children = [{xmlcdata, Status}]}]} + xmpp:make_iq_result(IQ, #last{seconds = Sec, status = Status}) end; _ -> - IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"query">>, - attrs = - [{<<"xmlns">>, ?NS_LAST}, - {<<"seconds">>, <<"0">>}], - children = []}]} + xmpp:make_iq_result(IQ, #last{seconds = 0}) end. +-spec register_user(binary(), binary()) -> any(). +register_user(User, Server) -> + on_presence_update( + User, + Server, + <<"RegisterResource">>, + <<"Registered but didn't login">>). + +-spec on_presence_update(binary(), binary(), binary(), binary()) -> any(). on_presence_update(User, Server, _Resource, Status) -> - TimeStamp = now_to_seconds(now()), + TimeStamp = erlang:system_time(second), store_last_info(User, Server, TimeStamp, Status). +-spec store_last_info(binary(), binary(), non_neg_integer(), binary()) -> any(). store_last_info(User, Server, TimeStamp, Status) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - DBType = gen_mod:db_type(LServer, ?MODULE), - store_last_info(LUser, LServer, TimeStamp, Status, - DBType). + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + Mod = gen_mod:db_mod(LServer, ?MODULE), + case use_cache(Mod, LServer) of + true -> + ets_cache:update( + ?LAST_CACHE, {LUser, LServer}, {ok, {TimeStamp, Status}}, + fun() -> + Mod:store_last_info(LUser, LServer, TimeStamp, Status) + end, cache_nodes(Mod, LServer)); + false -> + Mod:store_last_info(LUser, LServer, TimeStamp, Status) + end. -store_last_info(LUser, LServer, TimeStamp, Status, - mnesia) -> - US = {LUser, LServer}, - F = fun () -> - mnesia:write(#last_activity{us = US, - timestamp = TimeStamp, - status = Status}) - end, - mnesia:transaction(F); -store_last_info(LUser, LServer, TimeStamp, Status, - riak) -> - US = {LUser, LServer}, - {atomic, ejabberd_riak:put(#last_activity{us = US, - timestamp = TimeStamp, - status = Status}, - last_activity_schema())}; -store_last_info(LUser, LServer, TimeStamp, Status, - odbc) -> - Username = ejabberd_odbc:escape(LUser), - Seconds = - ejabberd_odbc:escape(iolist_to_binary(integer_to_list(TimeStamp))), - State = ejabberd_odbc:escape(Status), - odbc_queries:set_last_t(LServer, Username, Seconds, - State). - -%% @spec (LUser::string(), LServer::string()) -> -%% {ok, TimeStamp::integer(), Status::string()} | not_found +-spec get_last_info(binary(), binary()) -> {ok, non_neg_integer(), binary()} | + not_found. get_last_info(LUser, LServer) -> case get_last(LUser, LServer) of {error, _Reason} -> not_found; Res -> Res end. +-spec remove_user(binary(), binary()) -> any(). remove_user(User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - DBType = gen_mod:db_type(LServer, ?MODULE), - remove_user(LUser, LServer, DBType). + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:remove_user(LUser, LServer), + ets_cache:delete(?LAST_CACHE, {LUser, LServer}, cache_nodes(Mod, LServer)). -remove_user(LUser, LServer, mnesia) -> - US = {LUser, LServer}, - F = fun () -> mnesia:delete({last_activity, US}) end, - mnesia:transaction(F); -remove_user(LUser, LServer, odbc) -> - Username = ejabberd_odbc:escape(LUser), - odbc_queries:del_last(LServer, Username); -remove_user(LUser, LServer, riak) -> - {atomic, ejabberd_riak:delete(last_activity, {LUser, LServer})}. - -update_table() -> - Fields = record_info(fields, last_activity), - case mnesia:table_info(last_activity, attributes) of - Fields -> - ejabberd_config:convert_table_to_binary( - last_activity, Fields, set, - fun(#last_activity{us = {U, _}}) -> U end, - fun(#last_activity{us = {U, S}, status = Status} = R) -> - R#last_activity{us = {iolist_to_binary(U), - iolist_to_binary(S)}, - status = iolist_to_binary(Status)} - end); - _ -> - ?INFO_MSG("Recreating last_activity table", []), - mnesia:transform_table(last_activity, ignore, Fields) +-spec init_cache(module(), binary(), gen_mod:opts()) -> ok. +init_cache(Mod, Host, Opts) -> + case use_cache(Mod, Host) of + true -> + CacheOpts = cache_opts(Opts), + ets_cache:new(?LAST_CACHE, CacheOpts); + false -> + ets_cache:delete(?LAST_CACHE) end. -last_activity_schema() -> - {record_info(fields, last_activity), #last_activity{}}. +-spec cache_opts(gen_mod:opts()) -> [proplists:property()]. +cache_opts(Opts) -> + MaxSize = mod_last_opt:cache_size(Opts), + CacheMissed = mod_last_opt:cache_missed(Opts), + LifeTime = mod_last_opt:cache_life_time(Opts), + [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}]. -export(_Server) -> - [{last_activity, - fun(Host, #last_activity{us = {LUser, LServer}, - timestamp = TimeStamp, status = Status}) - when LServer == Host -> - Username = ejabberd_odbc:escape(LUser), - Seconds = - ejabberd_odbc:escape(jlib:integer_to_binary(TimeStamp)), - State = ejabberd_odbc:escape(Status), - [[<<"delete from last where username='">>, Username, <<"';">>], - [<<"insert into last(username, seconds, " - "state) values ('">>, - Username, <<"', '">>, Seconds, <<"', '">>, State, - <<"');">>]]; - (_Host, _R) -> - [] - end}]. +-spec use_cache(module(), binary()) -> boolean(). +use_cache(Mod, Host) -> + case erlang:function_exported(Mod, use_cache, 1) of + true -> Mod:use_cache(Host); + false -> mod_last_opt:use_cache(Host) + end. -import(LServer) -> - [{<<"select username, seconds, state from last">>, - fun([LUser, TimeStamp, State]) -> - #last_activity{us = {LUser, LServer}, - timestamp = jlib:binary_to_integer( - TimeStamp), - status = State} - end}]. +-spec cache_nodes(module(), binary()) -> [node()]. +cache_nodes(Mod, Host) -> + case erlang:function_exported(Mod, cache_nodes, 1) of + true -> Mod:cache_nodes(Host); + false -> ejabberd_cluster:get_nodes() + end. -import(_LServer, mnesia, #last_activity{} = LA) -> - mnesia:dirty_write(LA); -import(_LServer, riak, #last_activity{} = LA) -> - ejabberd_riak:put(LA, last_activity_schema()); -import(_, _, _) -> - pass. +import_info() -> + [{<<"last">>, 3}]. -transform_options(Opts) -> - lists:foldl(fun transform_options/2, [], Opts). +import_start(LServer, DBType) -> + Mod = gen_mod:db_mod(DBType, ?MODULE), + Mod:init(LServer, []). -transform_options({node_start, {_, _, _} = Now}, Opts) -> - ?WARNING_MSG("Old 'node_start' format detected. This is still supported " - "but it is better to fix your config.", []), - [{node_start, now_to_seconds(Now)}|Opts]; -transform_options(Opt, Opts) -> - [Opt|Opts]. +import(LServer, {sql, _}, DBType, <<"last">>, [LUser, TimeStamp, State]) -> + TS = case TimeStamp of + <<"">> -> 0; + _ -> binary_to_integer(TimeStamp) + end, + LA = #last_activity{us = {LUser, LServer}, + timestamp = TS, + status = State}, + Mod = gen_mod:db_mod(DBType, ?MODULE), + Mod:import(LServer, LA). + +export(LServer) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:export(LServer). + +depends(_Host, _Opts) -> + []. + +mod_opt_type(db_type) -> + econf:db_type(?MODULE); +mod_opt_type(use_cache) -> + econf:bool(); +mod_opt_type(cache_size) -> + econf:pos_int(infinity); +mod_opt_type(cache_missed) -> + econf:bool(); +mod_opt_type(cache_life_time) -> + econf:timeout(second, infinity). + +mod_options(Host) -> + [{db_type, ejabberd_config:default_db(Host, ?MODULE)}, + {use_cache, ejabberd_option:use_cache(Host)}, + {cache_size, ejabberd_option:cache_size(Host)}, + {cache_missed, ejabberd_option:cache_missed(Host)}, + {cache_life_time, ejabberd_option:cache_life_time(Host)}]. + +mod_doc() -> + #{desc => + ?T("This module adds support for " + "https://xmpp.org/extensions/xep-0012.html" + "[XEP-0012: Last Activity]. It can be used " + "to discover when a disconnected user last accessed " + "the server, to know when a connected user was last " + "active on the server, or to query the uptime of the ejabberd server."), + opts => + [{db_type, + #{value => "mnesia | sql", + desc => + ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}, + {use_cache, + #{value => "true | false", + desc => + ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, + {cache_size, + #{value => "pos_integer() | infinity", + desc => + ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, + {cache_missed, + #{value => "true | false", + desc => + ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, + {cache_life_time, + #{value => "timeout()", + desc => + ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}]}. diff --git a/src/mod_last_mnesia.erl b/src/mod_last_mnesia.erl new file mode 100644 index 000000000..f108101c9 --- /dev/null +++ b/src/mod_last_mnesia.erl @@ -0,0 +1,87 @@ +%%%------------------------------------------------------------------- +%%% File : mod_last_mnesia.erl +%%% Author : Evgeny Khramtsov +%%% Created : 13 Apr 2016 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_last_mnesia). + +-behaviour(mod_last). + +%% API +-export([init/2, import/2, get_last/2, store_last_info/4, + remove_user/2, use_cache/1]). +-export([need_transform/1, transform/1]). + +-include("mod_last.hrl"). +-include("logger.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + ejabberd_mnesia:create(?MODULE, last_activity, + [{disc_only_copies, [node()]}, + {attributes, record_info(fields, last_activity)}]). + +use_cache(Host) -> + case mnesia:table_info(last_activity, storage_type) of + disc_only_copies -> + mod_last_opt:use_cache(Host); + _ -> + false + end. + +get_last(LUser, LServer) -> + case mnesia:dirty_read(last_activity, {LUser, LServer}) of + [] -> + error; + [#last_activity{timestamp = TimeStamp, + status = Status}] -> + {ok, {TimeStamp, Status}} + end. + +store_last_info(LUser, LServer, TimeStamp, Status) -> + mnesia:dirty_write(#last_activity{us = {LUser, LServer}, + timestamp = TimeStamp, + status = Status}). + +remove_user(LUser, LServer) -> + US = {LUser, LServer}, + mnesia:dirty_delete({last_activity, US}). + +import(_LServer, #last_activity{} = LA) -> + mnesia:dirty_write(LA). + +need_transform({last_activity, {U, S}, _, Status}) + when is_list(U) orelse is_list(S) orelse is_list(Status) -> + ?INFO_MSG("Mnesia table 'last_activity' will be converted to binary", []), + true; +need_transform(_) -> + false. + +transform(#last_activity{us = {U, S}, status = Status} = R) -> + R#last_activity{us = {iolist_to_binary(U), iolist_to_binary(S)}, + status = iolist_to_binary(Status)}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== diff --git a/src/mod_last_opt.erl b/src/mod_last_opt.erl new file mode 100644 index 000000000..470ffce5e --- /dev/null +++ b/src/mod_last_opt.erl @@ -0,0 +1,41 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_last_opt). + +-export([cache_life_time/1]). +-export([cache_missed/1]). +-export([cache_size/1]). +-export([db_type/1]). +-export([use_cache/1]). + +-spec cache_life_time(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +cache_life_time(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_life_time, Opts); +cache_life_time(Host) -> + gen_mod:get_module_opt(Host, mod_last, cache_life_time). + +-spec cache_missed(gen_mod:opts() | global | binary()) -> boolean(). +cache_missed(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_missed, Opts); +cache_missed(Host) -> + gen_mod:get_module_opt(Host, mod_last, cache_missed). + +-spec cache_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +cache_size(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_size, Opts); +cache_size(Host) -> + gen_mod:get_module_opt(Host, mod_last, cache_size). + +-spec db_type(gen_mod:opts() | global | binary()) -> atom(). +db_type(Opts) when is_map(Opts) -> + gen_mod:get_opt(db_type, Opts); +db_type(Host) -> + gen_mod:get_module_opt(Host, mod_last, db_type). + +-spec use_cache(gen_mod:opts() | global | binary()) -> boolean(). +use_cache(Opts) when is_map(Opts) -> + gen_mod:get_opt(use_cache, Opts); +use_cache(Host) -> + gen_mod:get_module_opt(Host, mod_last, use_cache). + diff --git a/src/mod_last_sql.erl b/src/mod_last_sql.erl new file mode 100644 index 000000000..b61300fd2 --- /dev/null +++ b/src/mod_last_sql.erl @@ -0,0 +1,109 @@ +%%%------------------------------------------------------------------- +%%% File : mod_last_sql.erl +%%% Author : Evgeny Khramtsov +%%% Created : 13 Apr 2016 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_last_sql). + +-behaviour(mod_last). + + +%% API +-export([init/2, get_last/2, store_last_info/4, remove_user/2, + import/2, export/1]). +-export([sql_schemas/0]). + +-include("mod_last.hrl"). +-include("logger.hrl"). +-include("ejabberd_sql_pt.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(Host, _Opts) -> + ejabberd_sql_schema:update_schema(Host, ?MODULE, sql_schemas()), + ok. + +sql_schemas() -> + [#sql_schema{ + version = 1, + tables = + [#sql_table{ + name = <<"last">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"seconds">>, type = text}, + #sql_column{name = <<"state">>, type = text}], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"username">>], + unique = true}]}]}]. + +get_last(LUser, LServer) -> + case ejabberd_sql:sql_query( + LServer, + ?SQL("select @(seconds)d, @(state)s from last" + " where username=%(LUser)s and %(LServer)H")) of + {selected, []} -> + error; + {selected, [{TimeStamp, Status}]} -> + {ok, {TimeStamp, Status}}; + _Reason -> + {error, db_failure} + end. + +store_last_info(LUser, LServer, TimeStamp, Status) -> + TS = integer_to_binary(TimeStamp), + case ?SQL_UPSERT(LServer, "last", + ["!username=%(LUser)s", + "!server_host=%(LServer)s", + "seconds=%(TS)s", + "state=%(Status)s"]) of + ok -> + ok; + _Err -> + {error, db_failure} + end. + +remove_user(LUser, LServer) -> + ejabberd_sql:sql_query( + LServer, + ?SQL("delete from last where username=%(LUser)s and %(LServer)H")). + +export(_Server) -> + [{last_activity, + fun(Host, #last_activity{us = {LUser, LServer}, + timestamp = TimeStamp, status = Status}) + when LServer == Host -> + TS = integer_to_binary(TimeStamp), + [?SQL("delete from last where username=%(LUser)s and %(LServer)H;"), + ?SQL_INSERT("last", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "seconds=%(TS)s", + "state=%(Status)s"])]; + (_Host, _R) -> + [] + end}]. + +import(_LServer, _LA) -> + pass. diff --git a/src/mod_legacy_auth.erl b/src/mod_legacy_auth.erl new file mode 100644 index 000000000..1fb772d2c --- /dev/null +++ b/src/mod_legacy_auth.erl @@ -0,0 +1,175 @@ +%%%------------------------------------------------------------------- +%%% Created : 11 Dec 2016 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_legacy_auth). +-behaviour(gen_mod). + +-protocol({xep, 78, '2.5', '17.03', "complete", ""}). + +%% gen_mod API +-export([start/2, stop/1, reload/3, depends/2, mod_options/1, mod_doc/0]). +%% hooks +-export([c2s_unauthenticated_packet/2, c2s_stream_features/2]). + +-include_lib("xmpp/include/xmpp.hrl"). +-include("translate.hrl"). + +-type c2s_state() :: ejabberd_c2s:state(). + +%%%=================================================================== +%%% API +%%%=================================================================== +start(_Host, _Opts) -> + {ok, [{hook, c2s_unauthenticated_packet, c2s_unauthenticated_packet, 50}, + {hook, c2s_pre_auth_features, c2s_stream_features, 50}]}. + +stop(_Host) -> + ok. + +reload(_Host, _NewOpts, _OldOpts) -> + ok. + +depends(_Host, _Opts) -> + []. + +mod_options(_) -> + []. + +mod_doc() -> + #{desc => + [?T("The module implements " + "https://xmpp.org/extensions/xep-0078.html" + "[XEP-0078: Non-SASL Authentication]."), "", + ?T("NOTE: This type of authentication was obsoleted in " + "2008 and you unlikely need this module unless " + "you have something like outdated Jabber bots.")]}. + +-spec c2s_unauthenticated_packet(c2s_state(), iq()) -> + c2s_state() | {stop, c2s_state()}. +c2s_unauthenticated_packet(State, #iq{type = T, sub_els = [_]} = IQ) + when T == get; T == set -> + try xmpp:try_subtag(IQ, #legacy_auth{}) of + #legacy_auth{} = Auth -> + {stop, authenticate(State, xmpp:set_els(IQ, [Auth]))}; + false -> + State + catch _:{xmpp_codec, Why} -> + Txt = xmpp:io_format_error(Why), + Lang = maps:get(lang, State), + Err = xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)), + {stop, ejabberd_c2s:send(State, Err)} + end; +c2s_unauthenticated_packet(State, _) -> + State. + +-spec c2s_stream_features([xmpp_element()], binary()) -> [xmpp_element()]. +c2s_stream_features(Acc, LServer) -> + case gen_mod:is_loaded(LServer, ?MODULE) of + true -> + [#legacy_auth_feature{}|Acc]; + false -> + Acc + end. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +-spec authenticate(c2s_state(), iq()) -> c2s_state(). +authenticate(#{server := Server} = State, + #iq{type = get, sub_els = [#legacy_auth{}]} = IQ) -> + LServer = jid:nameprep(Server), + Auth = #legacy_auth{username = <<>>, password = <<>>, resource = <<>>}, + Res = case ejabberd_auth:plain_password_required(LServer) of + false -> + xmpp:make_iq_result(IQ, Auth#legacy_auth{digest = <<>>}); + true -> + xmpp:make_iq_result(IQ, Auth) + end, + ejabberd_c2s:send(State, Res); +authenticate(State, + #iq{type = set, lang = Lang, + sub_els = [#legacy_auth{username = U, + resource = R}]} = IQ) + when U == undefined; R == undefined; U == <<"">>; R == <<"">> -> + Txt = ?T("Both the username and the resource are required"), + Err = xmpp:make_error(IQ, xmpp:err_not_acceptable(Txt, Lang)), + ejabberd_c2s:send(State, Err); +authenticate(#{stream_id := StreamID, server := Server, + access := Access, ip := IP} = State, + #iq{type = set, lang = Lang, + sub_els = [#legacy_auth{username = U, + password = P0, + digest = D0, + resource = R}]} = IQ) -> + P = if is_binary(P0) -> P0; true -> <<>> end, + D = if is_binary(D0) -> D0; true -> <<>> end, + DGen = fun (PW) -> str:sha(<>) end, + JID = jid:make(U, Server, R), + case JID /= error andalso + acl:match_rule(JID#jid.lserver, Access, + #{usr => jid:split(JID), ip => IP}) == allow of + true -> + case ejabberd_auth:check_password_with_authmodule( + U, U, JID#jid.lserver, P, D, DGen) of + {true, AuthModule} -> + State1 = State#{sasl_mech => <<"legacy">>}, + State2 = ejabberd_c2s:handle_auth_success( + U, <<"legacy">>, AuthModule, State1), + State3 = State2#{user := U}, + open_session(State3, IQ, R); + _ -> + Err = xmpp:make_error(IQ, xmpp:err_not_authorized()), + process_auth_failure(State, U, Err, 'not-authorized') + end; + false when JID == error -> + Err = xmpp:make_error(IQ, xmpp:err_jid_malformed()), + process_auth_failure(State, U, Err, 'jid-malformed'); + false -> + Txt = ?T("Access denied by service policy"), + Err = xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)), + process_auth_failure(State, U, Err, 'forbidden') + end. + +-spec open_session(c2s_state(), iq(), binary()) -> c2s_state(). +open_session(State, IQ, R) -> + case ejabberd_c2s:bind(R, State) of + {ok, State1} -> + Res = xmpp:make_iq_result(IQ), + ejabberd_c2s:send(State1, Res); + {error, Err, State1} -> + Res = xmpp:make_error(IQ, Err), + ejabberd_c2s:send(State1, Res) + end. + +-spec process_auth_failure(c2s_state(), binary(), iq(), atom()) -> c2s_state(). +process_auth_failure(State, User, StanzaErr, Reason) -> + State1 = ejabberd_c2s:send(State, StanzaErr), + State2 = State1#{sasl_mech => <<"legacy">>}, + Text = format_reason(Reason), + ejabberd_c2s:handle_auth_failure(User, <<"legacy">>, Text, State2). + +-spec format_reason(atom()) -> binary(). +format_reason('not-authorized') -> + <<"Invalid username or password">>; +format_reason('forbidden') -> + <<"Access denied by service policy">>; +format_reason('jid-malformed') -> + <<"Malformed XMPP address">>. diff --git a/src/mod_mam.erl b/src/mod_mam.erl new file mode 100644 index 000000000..c3d5ae435 --- /dev/null +++ b/src/mod_mam.erl @@ -0,0 +1,1906 @@ +%%%------------------------------------------------------------------- +%%% File : mod_mam.erl +%%% Author : Evgeniy Khramtsov +%%% Purpose : Message Archive Management (XEP-0313) +%%% Created : 4 Jul 2013 by Evgeniy Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2013-2025 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_mam). + +-protocol({xep, 313, '0.6.1', '15.06', "complete", ""}). +-protocol({xep, 334, '0.2', '16.01', "complete", ""}). +-protocol({xep, 359, '0.5.0', '15.09', "complete", ""}). +-protocol({xep, 424, '0.4.2', '24.02', "partial", "Tombstones not implemented"}). +-protocol({xep, 425, '0.3.0', '24.06', "complete", ""}). +-protocol({xep, 441, '0.2.0', '15.06', "complete", ""}). +-protocol({xep, 431, '0.2.0', '24.12', "complete", ""}). + +-behaviour(gen_mod). + +%% API +-export([start/2, stop/1, reload/3, depends/2, mod_doc/0]). + +-export([sm_receive_packet/1, user_receive_packet/1, user_send_packet/1, + user_send_packet_strip_tag/1, process_iq_v0_2/1, process_iq_v0_3/1, + disco_local_features/5, + disco_sm_features/5, remove_user/2, remove_room/3, mod_opt_type/1, + muc_process_iq/2, muc_filter_message/3, message_is_archived/3, + delete_old_messages/2, get_commands_spec/0, msg_to_el/4, + get_room_config/4, set_room_option/3, offline_message/1, export/1, + mod_options/1, remove_mam_for_user_with_peer/3, remove_mam_for_user/2, + is_empty_for_user/2, is_empty_for_room/3, check_create_room/4, + process_iq/3, store_mam_message/7, make_id/0, wrap_as_mucsub/2, select/7, + is_archiving_enabled/2, + get_mam_count/2, + webadmin_menu_hostuser/4, + webadmin_page_hostuser/4, + get_mam_messages/2, webadmin_user/4, + delete_old_messages_batch/5, delete_old_messages_status/1, delete_old_messages_abort/1, + remove_message_from_archive/3]). + +-import(ejabberd_web_admin, [make_command/4, make_command/2]). + +-include_lib("xmpp/include/xmpp.hrl"). +-include("logger.hrl"). +-include("mod_muc_room.hrl"). +-include("ejabberd_commands.hrl"). +-include("ejabberd_http.hrl"). +-include("ejabberd_web_admin.hrl"). +-include("mod_mam.hrl"). +-include("translate.hrl"). + +-define(DEF_PAGE_SIZE, 50). +-define(MAX_PAGE_SIZE, 250). + +-type c2s_state() :: ejabberd_c2s:state(). +-type count() :: non_neg_integer() | undefined. + +-callback init(binary(), gen_mod:opts()) -> any(). +-callback remove_user(binary(), binary()) -> any(). +-callback remove_room(binary(), binary(), binary()) -> any(). +-callback delete_old_messages(binary() | global, + erlang:timestamp(), + all | chat | groupchat) -> any(). +-callback extended_fields(binary()) -> [mam_query:property() | #xdata_field{}]. +-callback store(xmlel(), binary(), {binary(), binary()}, chat | groupchat, + jid(), binary(), recv | send, integer(), binary(), + {true, binary()} | false) -> ok | any(). +-callback write_prefs(binary(), binary(), #archive_prefs{}, binary()) -> ok | any(). +-callback get_prefs(binary(), binary()) -> {ok, #archive_prefs{}} | error | {error, db_failure}. +-callback select(binary(), jid(), jid(), mam_query:result(), + #rsm_set{} | undefined, chat | groupchat) -> + {[{binary(), non_neg_integer(), xmlel()}], boolean(), count()} | + {error, db_failure}. +-callback select(binary(), jid(), jid(), mam_query:result(), + #rsm_set{} | undefined, chat | groupchat, + all | only_count | only_messages) -> + {[{binary(), non_neg_integer(), xmlel()}], boolean(), count()} | + {error, db_failure}. +-callback use_cache(binary()) -> boolean(). +-callback cache_nodes(binary()) -> [node()]. +-callback remove_from_archive(binary(), binary(), jid() | none) -> ok | {error, any()}. +-callback is_empty_for_user(binary(), binary()) -> boolean(). +-callback is_empty_for_room(binary(), binary(), binary()) -> boolean(). +-callback select_with_mucsub(binary(), jid(), jid(), mam_query:result(), + #rsm_set{} | undefined, all | only_count | only_messages) -> + {[{binary(), non_neg_integer(), xmlel()}], boolean(), count()} | + {error, db_failure}. + +-callback delete_old_messages_batch(binary(), erlang:timestamp(), + all | chat | groupchat, + pos_integer()) -> + {ok, non_neg_integer()} | {error, term()}. + +-callback delete_old_messages_batch(binary(), erlang:timestamp(), + all | chat | groupchat, + pos_integer(), any()) -> + {ok, any(), non_neg_integer()} | {error, term()}. + +-optional_callbacks([use_cache/1, cache_nodes/1, select_with_mucsub/6, select/6, select/7, + delete_old_messages_batch/5, delete_old_messages_batch/4]). + +%%%=================================================================== +%%% API +%%%=================================================================== +start(Host, Opts) -> + case mod_mam_opt:db_type(Opts) of + mnesia -> + ?WARNING_MSG("Mnesia backend for ~ts is not recommended: " + "it's limited to 2GB and often gets corrupted " + "when reaching this limit. SQL backend is " + "recommended. Namely, for small servers SQLite " + "is a preferred choice because it's very easy " + "to configure.", [?MODULE]); + _ -> + ok + end, + Mod = gen_mod:db_mod(Opts, ?MODULE), + case Mod:init(Host, Opts) of + ok -> + init_cache(Mod, Host, Opts), + register_iq_handlers(Host), + ejabberd_hooks:add(sm_receive_packet, Host, ?MODULE, + sm_receive_packet, 50), + ejabberd_hooks:add(user_receive_packet, Host, ?MODULE, + user_receive_packet, 88), + ejabberd_hooks:add(user_send_packet, Host, ?MODULE, + user_send_packet, 88), + ejabberd_hooks:add(user_send_packet, Host, ?MODULE, + user_send_packet_strip_tag, 500), + ejabberd_hooks:add(offline_message_hook, Host, ?MODULE, + offline_message, 49), + ejabberd_hooks:add(muc_filter_message, Host, ?MODULE, + muc_filter_message, 50), + ejabberd_hooks:add(muc_process_iq, Host, ?MODULE, + muc_process_iq, 50), + ejabberd_hooks:add(disco_local_features, Host, ?MODULE, + disco_local_features, 50), + ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, + disco_sm_features, 50), + ejabberd_hooks:add(remove_user, Host, ?MODULE, + remove_user, 50), + ejabberd_hooks:add(get_room_config, Host, ?MODULE, + get_room_config, 50), + ejabberd_hooks:add(set_room_option, Host, ?MODULE, + set_room_option, 50), + ejabberd_hooks:add(store_mam_message, Host, ?MODULE, + store_mam_message, 100), + ejabberd_hooks:add(webadmin_menu_hostuser, Host, ?MODULE, + webadmin_menu_hostuser, 50), + ejabberd_hooks:add(webadmin_page_hostuser, Host, ?MODULE, + webadmin_page_hostuser, 50), + ejabberd_hooks:add(webadmin_user, Host, ?MODULE, + webadmin_user, 50), + case mod_mam_opt:assume_mam_usage(Opts) of + true -> + ejabberd_hooks:add(message_is_archived, Host, ?MODULE, + message_is_archived, 50); + false -> + ok + end, + case mod_mam_opt:clear_archive_on_room_destroy(Opts) of + true -> + ejabberd_hooks:add(remove_room, Host, ?MODULE, + remove_room, 50); + false -> + ejabberd_hooks:add(check_create_room, Host, ?MODULE, + check_create_room, 50) + end, + ejabberd_commands:register_commands(Host, ?MODULE, get_commands_spec()), + ok; + Err -> + Err + end. + +use_cache(Mod, Host) -> + case erlang:function_exported(Mod, use_cache, 2) of + true -> Mod:use_cache(Host); + false -> mod_mam_opt:use_cache(Host) + end. + +cache_nodes(Mod, Host) -> + case erlang:function_exported(Mod, cache_nodes, 1) of + true -> Mod:cache_nodes(Host); + false -> ejabberd_cluster:get_nodes() + end. + +init_cache(Mod, Host, Opts) -> + case use_cache(Mod, Host) of + true -> + ets_cache:new(archive_prefs_cache, cache_opts(Opts)); + false -> + ets_cache:delete(archive_prefs_cache) + end. + +cache_opts(Opts) -> + MaxSize = mod_mam_opt:cache_size(Opts), + CacheMissed = mod_mam_opt:cache_missed(Opts), + LifeTime = mod_mam_opt:cache_life_time(Opts), + [{max_size, MaxSize}, {life_time, LifeTime}, {cache_missed, CacheMissed}]. + +stop(Host) -> + unregister_iq_handlers(Host), + ejabberd_hooks:delete(sm_receive_packet, Host, ?MODULE, + sm_receive_packet, 50), + ejabberd_hooks:delete(user_receive_packet, Host, ?MODULE, + user_receive_packet, 88), + ejabberd_hooks:delete(user_send_packet, Host, ?MODULE, + user_send_packet, 88), + ejabberd_hooks:delete(user_send_packet, Host, ?MODULE, + user_send_packet_strip_tag, 500), + ejabberd_hooks:delete(offline_message_hook, Host, ?MODULE, + offline_message, 49), + ejabberd_hooks:delete(muc_filter_message, Host, ?MODULE, + muc_filter_message, 50), + ejabberd_hooks:delete(muc_process_iq, Host, ?MODULE, + muc_process_iq, 50), + ejabberd_hooks:delete(disco_local_features, Host, ?MODULE, + disco_local_features, 50), + ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, + disco_sm_features, 50), + ejabberd_hooks:delete(remove_user, Host, ?MODULE, + remove_user, 50), + ejabberd_hooks:delete(get_room_config, Host, ?MODULE, + get_room_config, 50), + ejabberd_hooks:delete(set_room_option, Host, ?MODULE, + set_room_option, 50), + ejabberd_hooks:delete(store_mam_message, Host, ?MODULE, + store_mam_message, 100), + ejabberd_hooks:delete(webadmin_menu_hostuser, Host, ?MODULE, + webadmin_menu_hostuser, 50), + ejabberd_hooks:delete(webadmin_page_hostuser, Host, ?MODULE, + webadmin_page_hostuser, 50), + ejabberd_hooks:delete(webadmin_user, Host, ?MODULE, + webadmin_user, 50), + case mod_mam_opt:assume_mam_usage(Host) of + true -> + ejabberd_hooks:delete(message_is_archived, Host, ?MODULE, + message_is_archived, 50); + false -> + ok + end, + case mod_mam_opt:clear_archive_on_room_destroy(Host) of + true -> + ejabberd_hooks:delete(remove_room, Host, ?MODULE, + remove_room, 50); + false -> + ejabberd_hooks:delete(check_create_room, Host, ?MODULE, + check_create_room, 50) + end, + ejabberd_commands:unregister_commands(Host, ?MODULE, get_commands_spec()). + +reload(Host, NewOpts, OldOpts) -> + NewMod = gen_mod:db_mod(NewOpts, ?MODULE), + OldMod = gen_mod:db_mod(OldOpts, ?MODULE), + if NewMod /= OldMod -> + NewMod:init(Host, NewOpts); + true -> + ok + end, + init_cache(NewMod, Host, NewOpts), + case {mod_mam_opt:assume_mam_usage(NewOpts), + mod_mam_opt:assume_mam_usage(OldOpts)} of + {true, false} -> + ejabberd_hooks:add(message_is_archived, Host, ?MODULE, + message_is_archived, 50); + {false, true} -> + ejabberd_hooks:delete(message_is_archived, Host, ?MODULE, + message_is_archived, 50); + _ -> + ok + end. + +depends(_Host, _Opts) -> + []. + +-spec register_iq_handlers(binary()) -> ok. +register_iq_handlers(Host) -> + gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_MAM_TMP, + ?MODULE, process_iq_v0_2), + gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_MAM_TMP, + ?MODULE, process_iq_v0_2), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_MAM_0, + ?MODULE, process_iq_v0_3), + gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_MAM_0, ?MODULE, + process_iq_v0_3), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_MAM_1, + ?MODULE, process_iq_v0_3), + gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_MAM_1, + ?MODULE, process_iq_v0_3), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_MAM_2, + ?MODULE, process_iq_v0_3), + gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_MAM_2, + ?MODULE, process_iq_v0_3). + +-spec unregister_iq_handlers(binary()) -> ok. +unregister_iq_handlers(Host) -> + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_MAM_TMP), + gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_MAM_TMP), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_MAM_0), + gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_MAM_0), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_MAM_1), + gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_MAM_1), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_MAM_2), + gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_MAM_2). + +-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), + Mod:remove_user(LUser, LServer), + case use_cache(Mod, LServer) of + true -> + ets_cache:delete(archive_prefs_cache, {LUser, LServer}, + cache_nodes(Mod, LServer)); + false -> + ok + end. + +-spec remove_room(binary(), binary(), binary()) -> ok. +remove_room(LServer, Name, Host) -> + LName = jid:nodeprep(Name), + LHost = jid:nameprep(Host), + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:remove_room(LServer, LName, LHost), + ok. + +-spec remove_mam_for_user(binary(), binary()) -> + {ok, binary()} | {error, binary()}. +remove_mam_for_user(User, Server) -> + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + Mod = gen_mod:db_mod(LServer, ?MODULE), + case Mod:remove_from_archive(LUser, LServer, none) of + ok -> + {ok, <<"MAM archive removed">>}; + {error, Bin} when is_binary(Bin) -> + {error, Bin}; + {error, _} -> + {error, <<"Db returned error">>} + end. + +-spec remove_mam_for_user_with_peer(binary(), binary(), binary()) -> + {ok, binary()} | {error, binary()}. +remove_mam_for_user_with_peer(User, Server, Peer) -> + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + try jid:decode(Peer) of + Jid -> + Mod = get_module_host(LServer), + case Mod:remove_from_archive(LUser, LServer, Jid) of + ok -> + {ok, <<"MAM archive removed">>}; + {error, Bin} when is_binary(Bin) -> + {error, Bin}; + {error, _} -> + {error, <<"Db returned error">>} + end + catch _:_ -> + {error, <<"Invalid peer JID">>} + end. + +-spec remove_message_from_archive( + User :: binary() | {User :: binary(), Host :: binary()}, + Server :: binary(), StanzaId :: integer()) -> + ok | {error, binary()}. +remove_message_from_archive(User, Server, StanzaId) when is_binary(User) -> + remove_message_from_archive({User, Server}, Server, StanzaId); +remove_message_from_archive({_User, _Host} = UserHost, Server, StanzaId) -> + Mod = gen_mod:db_mod(Server, ?MODULE), + case Mod:remove_from_archive(UserHost, Server, StanzaId) of + ok -> + ok; + {error, Bin} when is_binary(Bin) -> + {error, Bin}; + {error, _} -> + {error, <<"Db returned error">>} + end. + +get_module_host(LServer) -> + try gen_mod:db_mod(LServer, ?MODULE) + catch error:{module_not_loaded, ?MODULE, LServer} -> + gen_mod:db_mod(ejabberd_router:host_of_route(LServer), ?MODULE) + end. + +-spec get_room_config([muc_roomconfig:property()], mod_muc_room:state(), + jid(), binary()) -> [muc_roomconfig:property()]. +get_room_config(Fields, RoomState, _From, _Lang) -> + Config = RoomState#state.config, + Fields ++ [{mam, Config#config.mam}]. + +-spec set_room_option({pos_integer(), _}, muc_roomconfig:property(), binary()) + -> {pos_integer(), _}. +set_room_option(_Acc, {mam, Val}, _Lang) -> + {#config.mam, Val}; +set_room_option(Acc, _Property, _Lang) -> + Acc. + +-spec sm_receive_packet(stanza()) -> stanza(). +sm_receive_packet(#message{to = #jid{lserver = LServer}} = Pkt) -> + init_stanza_id(Pkt, LServer); +sm_receive_packet(Acc) -> + Acc. + +-spec user_receive_packet({stanza(), c2s_state()}) -> {stanza(), c2s_state()}. +user_receive_packet({#message{from = Peer} = Pkt, #{jid := JID} = C2SState}) -> + LUser = JID#jid.luser, + LServer = JID#jid.lserver, + Pkt1 = case should_archive(Pkt, LServer) of + true -> + case store_msg(Pkt, LUser, LServer, Peer, recv) of + ok -> + mark_stored_msg(Pkt, JID); + _ -> + Pkt + end; + _ -> + Pkt + end, + {Pkt1, C2SState}; +user_receive_packet(Acc) -> + Acc. + +-spec user_send_packet({stanza(), c2s_state()}) + -> {stanza(), c2s_state()}. +user_send_packet({#message{to = Peer} = Pkt, #{jid := JID} = C2SState}) -> + LUser = JID#jid.luser, + LServer = JID#jid.lserver, + Pkt1 = init_stanza_id(Pkt, LServer), + Pkt2 = case should_archive(Pkt1, LServer) of + true -> + case store_msg(xmpp:set_from_to(Pkt1, JID, Peer), + LUser, LServer, Peer, send) of + ok -> + mark_stored_msg(Pkt1, JID); + _ -> + Pkt1 + end; + false -> + Pkt1 + end, + {Pkt2, C2SState}; +user_send_packet(Acc) -> + Acc. + +-spec user_send_packet_strip_tag({stanza(), c2s_state()}) + -> {stanza(), c2s_state()}. +user_send_packet_strip_tag({#message{} = Pkt, #{jid := JID} = C2SState}) -> + LServer = JID#jid.lserver, + Pkt1 = xmpp:del_meta(Pkt, stanza_id), + Pkt2 = strip_my_stanza_id(Pkt1, LServer), + {Pkt2, C2SState}; +user_send_packet_strip_tag(Acc) -> + Acc. + +-spec offline_message({any(), message()}) -> {any(), message()}. +offline_message({_Action, #message{from = Peer, to = To} = Pkt} = Acc) -> + LUser = To#jid.luser, + LServer = To#jid.lserver, + case should_archive(Pkt, LServer) of + true -> + case store_msg(Pkt, LUser, LServer, Peer, recv) of + ok -> + {archived, mark_stored_msg(Pkt, To)}; + _ -> + Acc + end; + false -> + Acc + end. + +-spec muc_filter_message(message(), mod_muc_room:state(), + binary()) -> message(). +muc_filter_message(#message{meta = #{mam_ignore := true}} = Pkt, _MUCState, _FromNick) -> + Pkt; +muc_filter_message(#message{from = From} = Pkt, + #state{config = Config, jid = RoomJID} = MUCState, + FromNick) -> + LServer = RoomJID#jid.lserver, + Pkt1 = init_stanza_id(Pkt, LServer), + if Config#config.mam -> + StorePkt = strip_x_jid_tags(Pkt1), + case store_muc(MUCState, StorePkt, RoomJID, From, FromNick) of + ok -> + mark_stored_msg(Pkt1, RoomJID); + _ -> + Pkt1 + end; + true -> + Pkt1 + end; +muc_filter_message(Acc, _MUCState, _FromNick) -> + Acc. + +-spec make_id() -> integer(). +make_id() -> + erlang:system_time(microsecond). + +-spec get_stanza_id(stanza()) -> integer(). +get_stanza_id(#message{meta = #{stanza_id := ID}}) -> + ID. + +-spec init_stanza_id(stanza(), binary()) -> stanza(). +init_stanza_id(#message{meta = #{stanza_id := _ID}} = Pkt, _LServer) -> + Pkt; +init_stanza_id(#message{meta = #{from_offline := true}} = Pkt, _LServer) -> + Pkt; +init_stanza_id(Pkt, LServer) -> + ID = make_id(), + Pkt1 = strip_my_stanza_id(Pkt, LServer), + xmpp:put_meta(Pkt1, stanza_id, ID). + +-spec set_stanza_id(stanza(), jid(), binary()) -> stanza(). +set_stanza_id(Pkt, JID, ID) -> + BareJID = jid:remove_resource(JID), + Archived = #mam_archived{by = BareJID, id = ID}, + StanzaID = #stanza_id{by = BareJID, id = ID}, + NewEls = [Archived, StanzaID|xmpp:get_els(Pkt)], + xmpp:set_els(Pkt, NewEls). + +-spec get_origin_id(stanza()) -> binary(). +get_origin_id(#message{type = groupchat} = Pkt) -> + integer_to_binary(get_stanza_id(Pkt)); +get_origin_id(#message{} = Pkt) -> + case xmpp:get_subtag(Pkt, #origin_id{}) of + #origin_id{id = ID} -> + ID; + _ -> + xmpp:get_id(Pkt) + end. + +-spec mark_stored_msg(message(), jid()) -> message(). +mark_stored_msg(#message{meta = #{stanza_id := ID}} = Pkt, JID) -> + Pkt1 = set_stanza_id(Pkt, JID, integer_to_binary(ID)), + xmpp:put_meta(Pkt1, mam_archived, true). + +% Query archive v0.2 +process_iq_v0_2(#iq{from = #jid{lserver = LServer}, + to = #jid{lserver = LServer}, + type = get, sub_els = [#mam_query{}]} = IQ) -> + process_iq(LServer, IQ, chat); +process_iq_v0_2(IQ) -> + process_iq(IQ). + +% Query archive v0.3 +process_iq_v0_3(#iq{from = #jid{lserver = LServer}, + to = #jid{lserver = LServer}, + type = set, sub_els = [#mam_query{}]} = IQ) -> + process_iq(LServer, IQ, chat); +process_iq_v0_3(#iq{from = #jid{lserver = LServer}, + to = #jid{lserver = LServer}, + type = get, sub_els = [#mam_query{}]} = IQ) -> + process_iq(LServer, IQ); +process_iq_v0_3(IQ) -> + process_iq(IQ). + +-spec muc_process_iq(ignore | iq(), mod_muc_room:state()) -> ignore | iq(). +muc_process_iq(#iq{type = T, lang = Lang, + from = From, + sub_els = [#mam_query{xmlns = NS}]} = IQ, + MUCState) + when (T == set andalso (NS /= ?NS_MAM_TMP)) orelse + (T == get andalso NS == ?NS_MAM_TMP) -> + case may_enter_room(From, MUCState) of + true -> + LServer = MUCState#state.server_host, + Role = mod_muc_room:get_role(From, MUCState), + process_iq(LServer, IQ, {groupchat, Role, MUCState}); + false -> + Text = ?T("Only members may query archives of this room"), + xmpp:make_error(IQ, xmpp:err_forbidden(Text, Lang)) + end; +muc_process_iq(#iq{type = get, + sub_els = [#mam_query{xmlns = NS}]} = IQ, + MUCState) when NS /= ?NS_MAM_TMP -> + LServer = MUCState#state.server_host, + process_iq(LServer, IQ); +muc_process_iq(IQ, _MUCState) -> + IQ. + +parse_query(#mam_query{xmlns = ?NS_MAM_TMP, + start = Start, 'end' = End, + with = With, withtext = Text}, _Lang) -> + {ok, [{start, Start}, {'end', End}, + {with, With}, {withtext, Text}]}; +parse_query(#mam_query{xdata = #xdata{}} = Query, Lang) -> + X = xmpp_util:set_xdata_field( + #xdata_field{var = <<"FORM_TYPE">>, + type = hidden, values = [?NS_MAM_1]}, + Query#mam_query.xdata), + {Fields, WithText} = case lists:keytake(<<"{urn:xmpp:fulltext:0}fulltext">>, #xdata_field.var, X#xdata.fields) of + false -> {X#xdata.fields, <<>>}; + {value, #xdata_field{values = [V]}, F} -> {F, V}; + {value, _, F} -> {F, <<>>} + end, + try mam_query:decode(Fields) of + Form -> + if WithText /= <<>> -> + {ok, lists:keystore(withtext, 1, Form, {withtext, WithText})}; + true -> + {ok, Form} + end + catch _:{mam_query, Why} -> + Txt = mam_query:format_error(Why), + {error, xmpp:err_bad_request(Txt, Lang)} + end; +parse_query(#mam_query{}, _Lang) -> + {ok, []}. + +disco_local_features({error, _Error} = Acc, _From, _To, _Node, _Lang) -> + Acc; +disco_local_features(Acc, _From, _To, <<"">>, _Lang) -> + Features = case Acc of + {result, Fs} -> Fs; + empty -> [] + end, + {result, [?NS_MESSAGE_RETRACT | Features]}; +disco_local_features(empty, _From, _To, _Node, Lang) -> + Txt = ?T("No features available"), + {error, xmpp:err_item_not_found(Txt, Lang)}; +disco_local_features(Acc, _From, _To, _Node, _Lang) -> + Acc. + +disco_sm_features(empty, From, To, Node, Lang) -> + disco_sm_features({result, []}, From, To, Node, Lang); +disco_sm_features({result, OtherFeatures}, + #jid{luser = U, lserver = S}, + #jid{luser = U, lserver = S}, <<"">>, _Lang) -> + {result, [?NS_MAM_TMP, ?NS_MAM_0, ?NS_MAM_1, ?NS_MAM_2, ?NS_SID_0, + ?NS_MESSAGE_RETRACT | + OtherFeatures]}; +disco_sm_features(Acc, _From, _To, _Node, _Lang) -> + Acc. + +-spec message_is_archived(boolean(), c2s_state(), message()) -> boolean(). +message_is_archived(true, _C2SState, _Pkt) -> + true; +message_is_archived(false, #{lserver := LServer}, Pkt) -> + case mod_mam_opt:assume_mam_usage(LServer) of + true -> + is_archived(Pkt, LServer); + false -> + false + end. + +%%% +%%% Commands +%%% + +%% @format-begin + +get_mam_count(User, Host) -> + Jid = jid:make(User, Host), + {_, _, Count} = select(Host, Jid, Jid, [], #rsm_set{}, chat, only_count), + Count. + +get_mam_messages(User, Host) -> + Jid = jid:make(User, Host), + {Messages, _, _} = select(Host, Jid, Jid, [], #rsm_set{}, chat, only_messages), + format_user_messages(Messages). + +format_user_messages(Messages) -> + lists:map(fun({_ID, _IDInt, Fwd}) -> + El = hd(Fwd#forwarded.sub_els), + FPacket = + ejabberd_web_admin:pretty_print_xml( + xmpp:encode(El)), + SFrom = jid:encode(El#message.from), + STo = jid:encode(El#message.to), + Time = format_time(Fwd#forwarded.delay#delay.stamp), + {Time, SFrom, STo, FPacket} + end, + Messages). + +format_time(Now) -> + {{Year, Month, Day}, {Hour, Minute, Second}} = calendar:now_to_local_time(Now), + str:format("~w-~.2.0w-~.2.0w ~.2.0w:~.2.0w:~.2.0w", + [Year, Month, Day, Hour, Minute, Second]). + +webadmin_user(Acc, User, Server, R) -> + Acc + ++ [make_command(get_mam_count, + R, + [{<<"user">>, User}, {<<"host">>, Server}], + [{result_links, [{value, arg_host, 4, <<"user/", User/binary, "/mam/">>}]}])]. +%% @format-end + +%%% +%%% Commands: Purge +%%% + +delete_old_messages_batch(Server, Type, Days, BatchSize, Rate) when Type == <<"chat">>; + Type == <<"groupchat">>; + Type == <<"all">> -> + CurrentTime = make_id(), + Diff = Days * 24 * 60 * 60 * 1000000, + TimeStamp = misc:usec_to_now(CurrentTime - Diff), + TypeA = misc:binary_to_atom(Type), + LServer = jid:nameprep(Server), + Mod = gen_mod:db_mod(LServer, ?MODULE), + + case ejabberd_batch:register_task({mam, LServer}, 0, Rate, {LServer, TypeA, TimeStamp, BatchSize, none}, + fun({L, T, St, B, IS} = S) -> + case {erlang:function_exported(Mod, delete_old_messages_batch, 4), + erlang:function_exported(Mod, delete_old_messages_batch, 5)} of + {true, _} -> + case Mod:delete_old_messages_batch(L, St, T, B) of + {ok, Count} -> + {ok, S, Count}; + {error, _} = E -> + E + end; + {_, true} -> + case Mod:delete_old_messages_batch(L, St, T, B, IS) of + {ok, IS2, Count} -> + {ok, {L, St, T, B, IS2}, Count}; + {error, _} = E -> + E + end; + _ -> + {error, not_implemented_for_backend} + end + end) of + ok -> + {ok, ""}; + {error, in_progress} -> + {error, "Operation in progress"} + end. +delete_old_messages_status(Server) -> + LServer = jid:nameprep(Server), + Msg = case ejabberd_batch:task_status({mam, LServer}) of + not_started -> + "Operation not started"; + {failed, Steps, Error} -> + io_lib:format("Operation failed after deleting ~p messages with error ~p", + [Steps, misc:format_val(Error)]); + {aborted, Steps} -> + io_lib:format("Operation was aborted after deleting ~p messages", + [Steps]); + {working, Steps} -> + io_lib:format("Operation in progress, deleted ~p messages", + [Steps]); + {completed, Steps} -> + io_lib:format("Operation was completed after deleting ~p messages", + [Steps]) + end, + lists:flatten(Msg). + +delete_old_messages_abort(Server) -> + LServer = jid:nameprep(Server), + case ejabberd_batch:abort_task({mam, LServer}) of + aborted -> "Operation aborted"; + not_started -> "No task running" + end. + +delete_old_messages(TypeBin, Days) when TypeBin == <<"chat">>; + TypeBin == <<"groupchat">>; + TypeBin == <<"all">> -> + CurrentTime = make_id(), + Diff = Days * 24 * 60 * 60 * 1000000, + TimeStamp = misc:usec_to_now(CurrentTime - Diff), + Type = misc:binary_to_atom(TypeBin), + DBTypes = lists:usort( + lists:map( + fun(Host) -> + case mod_mam_opt:db_type(Host) of + sql -> {sql, Host}; + Other -> {Other, global} + end + end, ejabberd_option:hosts())), + Results = lists:map( + fun({DBType, ServerHost}) -> + Mod = gen_mod:db_mod(DBType, ?MODULE), + Mod:delete_old_messages(ServerHost, TimeStamp, Type) + end, DBTypes), + case lists:filter(fun(Res) -> Res /= ok end, Results) of + [] -> ok; + [NotOk|_] -> NotOk + end; +delete_old_messages(_TypeBin, _Days) -> + unsupported_type. + +export(LServer) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:export(LServer). + +-spec is_empty_for_user(binary(), binary()) -> boolean(). +is_empty_for_user(User, Server) -> + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:is_empty_for_user(LUser, LServer). + +-spec is_empty_for_room(binary(), binary(), binary()) -> boolean(). +is_empty_for_room(LServer, Name, Host) -> + LName = jid:nodeprep(Name), + LHost = jid:nameprep(Host), + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:is_empty_for_room(LServer, LName, LHost). + +-spec check_create_room(boolean(), binary(), binary(), binary()) -> boolean(). +check_create_room(Acc, ServerHost, RoomID, Host) -> + Acc and is_empty_for_room(ServerHost, RoomID, Host). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== + +process_iq(LServer, #iq{sub_els = [#mam_query{xmlns = NS}]} = IQ) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + CommonFields = [{with, undefined}, + {start, undefined}, + {'end', undefined}], + ExtendedFields = Mod:extended_fields(LServer), + Fields = mam_query:encode(CommonFields ++ ExtendedFields), + X = xmpp_util:set_xdata_field( + #xdata_field{var = <<"FORM_TYPE">>, type = hidden, values = [NS]}, + #xdata{type = form, fields = Fields}), + xmpp:make_iq_result(IQ, #mam_query{xmlns = NS, xdata = X}). + +% Preference setting (both v0.2 & v0.3) +process_iq(#iq{type = set, lang = Lang, + sub_els = [#mam_prefs{default = undefined, xmlns = NS}]} = IQ) -> + Why = {missing_attr, <<"default">>, <<"prefs">>, NS}, + ErrTxt = xmpp:io_format_error(Why), + xmpp:make_error(IQ, xmpp:err_bad_request(ErrTxt, Lang)); +process_iq(#iq{from = #jid{luser = LUser, lserver = LServer}, + to = #jid{lserver = LServer}, + type = set, lang = Lang, + sub_els = [#mam_prefs{xmlns = NS, + default = Default, + always = Always0, + never = Never0}]} = IQ) -> + Access = mod_mam_opt:access_preferences(LServer), + case acl:match_rule(LServer, Access, jid:make(LUser, LServer)) of + allow -> + Always = lists:usort(get_jids(Always0)), + Never = lists:usort(get_jids(Never0)), + case write_prefs(LUser, LServer, LServer, Default, Always, Never) of + ok -> + NewPrefs = prefs_el(Default, Always, Never, NS), + xmpp:make_iq_result(IQ, NewPrefs); + _Err -> + Txt = ?T("Database failure"), + xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) + end; + deny -> + Txt = ?T("MAM preference modification denied by service policy"), + xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)) + end; +process_iq(#iq{from = #jid{luser = LUser, lserver = LServer}, + to = #jid{lserver = LServer}, lang = Lang, + type = get, sub_els = [#mam_prefs{xmlns = NS}]} = IQ) -> + case get_prefs(LUser, LServer) of + {ok, Prefs} -> + PrefsEl = prefs_el(Prefs#archive_prefs.default, + Prefs#archive_prefs.always, + Prefs#archive_prefs.never, + NS), + xmpp:make_iq_result(IQ, PrefsEl); + {error, _} -> + Txt = ?T("Database failure"), + xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) + end; +process_iq(IQ) -> + xmpp:make_error(IQ, xmpp:err_not_allowed()). + +process_iq(LServer, #iq{from = #jid{luser = LUser}, lang = Lang, + sub_els = [SubEl]} = IQ, MsgType) -> + Ret = case MsgType of + chat -> + maybe_activate_mam(LUser, LServer); + _ -> + ok + end, + case Ret of + ok -> + case SubEl of + #mam_query{rsm = #rsm_set{index = I}} when is_integer(I) -> + Txt = ?T("Unsupported element"), + xmpp:make_error(IQ, xmpp:err_feature_not_implemented(Txt, Lang)); + #mam_query{rsm = RSM, flippage = FlipPage, xmlns = NS} -> + case parse_query(SubEl, Lang) of + {ok, Query} -> + NewRSM = limit_max(RSM, NS), + select_and_send(LServer, Query, NewRSM, FlipPage, IQ, MsgType); + {error, Err} -> + xmpp:make_error(IQ, Err) + end + end; + {error, _} -> + Txt = ?T("Database failure"), + xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) + end. + +-spec should_archive(message(), binary()) -> boolean(). +should_archive(#message{type = error}, _LServer) -> + false; +should_archive(#message{type = groupchat, meta = #{is_muc_subscriber := true}} = Msg, LServer) -> + case mod_mam_opt:archive_muc_as_mucsub(LServer) of + true -> + should_archive(Msg#message{type = chat}, LServer); + false -> + false + end; +should_archive(#message{type = groupchat}, _LServer) -> + false; +should_archive(#message{meta = #{from_offline := true}}, _LServer) -> + false; +should_archive(#message{body = Body, subject = Subject, + type = Type} = Pkt, LServer) -> + case is_archived(Pkt, LServer) of + true -> + false; + false -> + case check_store_hint(Pkt) of + store -> + true; + no_store -> + false; + none when Type == headline -> + false; + none -> + case xmpp:get_text(Body) /= <<>> orelse + xmpp:get_text(Subject) /= <<>> of + true -> + true; + _ -> + case misc:unwrap_mucsub_message(Pkt) of + #message{type = groupchat} = Msg -> + should_archive(Msg#message{type = chat}, LServer); + #message{} = Msg -> + should_archive(Msg, LServer); + _ -> + misc:is_mucsub_message(Pkt) + end + end + end + end; +should_archive(_, _LServer) -> + false. + +-spec strip_my_stanza_id(stanza(), binary()) -> stanza(). +strip_my_stanza_id(Pkt, LServer) -> + Els = xmpp:get_els(Pkt), + NewEls = lists:filter( + fun(El) -> + Name = xmpp:get_name(El), + NS = xmpp:get_ns(El), + if (Name == <<"archived">> andalso NS == ?NS_MAM_TMP); + (Name == <<"stanza-id">> andalso NS == ?NS_SID_0) -> + try xmpp:decode(El) of + #mam_archived{by = By} -> + By#jid.lserver /= LServer; + #stanza_id{by = By} -> + By#jid.lserver /= LServer + catch _:{xmpp_codec, _} -> + false + end; + true -> + true + end + end, Els), + xmpp:set_els(Pkt, NewEls). + +-spec strip_x_jid_tags(stanza()) -> stanza(). +strip_x_jid_tags(Pkt) -> + Els = xmpp:get_els(Pkt), + NewEls = lists:filter( + fun(El) -> + case xmpp:get_name(El) of + <<"x">> -> + NS = xmpp:get_ns(El), + Items = if NS == ?NS_MUC_USER; + NS == ?NS_MUC_ADMIN; + NS == ?NS_MUC_OWNER -> + try xmpp:decode(El) of + #muc_user{items = Is} -> Is; + #muc_admin{items = Is} -> Is; + #muc_owner{items = Is} -> Is + catch _:{xmpp_codec, _} -> + [] + end; + true -> + [] + end, + not lists:any( + fun(#muc_item{jid = JID}) -> + JID /= undefined + end, Items); + _ -> + true + end + end, Els), + xmpp:set_els(Pkt, NewEls). + +-spec should_archive_peer(binary(), binary(), + #archive_prefs{}, jid()) -> boolean(). +should_archive_peer(LUser, LServer, + #archive_prefs{default = Default, + always = Always, + never = Never}, + Peer) -> + LPeer = jid:remove_resource(jid:tolower(Peer)), + case lists:member(LPeer, Always) of + true -> + true; + false -> + case lists:member(LPeer, Never) of + true -> + false; + false -> + case Default of + always -> true; + never -> false; + roster -> + {Sub, _, _} = ejabberd_hooks:run_fold( + roster_get_jid_info, + LServer, {none, none, []}, + [LUser, LServer, Peer]), + Sub == both orelse Sub == from orelse Sub == to + end + end + end. + +-spec should_archive_muc(message()) -> boolean(). +should_archive_muc(#message{type = groupchat, + body = Body, subject = Subj} = Pkt) -> + case check_store_hint(Pkt) of + store -> + true; + no_store -> + false; + none -> + case xmpp:get_text(Body) of + <<"">> -> + case xmpp:get_text(Subj) of + <<"">> -> + false; + _ -> + true + end; + _ -> + true + end + end; +should_archive_muc(_) -> + false. + +-spec check_store_hint(message()) -> store | no_store | none. +check_store_hint(Pkt) -> + case has_store_hint(Pkt) of + true -> + store; + false -> + case has_no_store_hint(Pkt) of + true -> + no_store; + false -> + none + end + end. + +-spec has_store_hint(message()) -> boolean(). +has_store_hint(Message) -> + xmpp:has_subtag(Message, #hint{type = 'store'}). + +-spec has_no_store_hint(message()) -> boolean(). +has_no_store_hint(Message) -> + xmpp:has_subtag(Message, #hint{type = 'no-store'}) orelse + xmpp:has_subtag(Message, #hint{type = 'no-storage'}) orelse + xmpp:has_subtag(Message, #hint{type = 'no-permanent-store'}) orelse + xmpp:has_subtag(Message, #hint{type = 'no-permanent-storage'}). + +-spec is_archived(message(), binary()) -> boolean(). +is_archived(Pkt, LServer) -> + case xmpp:get_subtag(Pkt, #stanza_id{by = #jid{}}) of + #stanza_id{by = #jid{lserver = LServer}} -> + true; + _ -> + false + end. + +-spec may_enter_room(jid(), mod_muc_room:state()) -> boolean(). +may_enter_room(From, + #state{config = #config{members_only = false}} = MUCState) -> + mod_muc_room:get_affiliation(From, MUCState) /= outcast; +may_enter_room(From, MUCState) -> + mod_muc_room:is_occupant_or_admin(From, MUCState). + +-spec store_msg(message(), binary(), binary(), jid(), send | recv) + -> ok | pass | any(). +store_msg(#message{type = groupchat, from = From, to = To, meta = #{is_muc_subscriber := true}} = Pkt, LUser, LServer, _Peer, Dir) -> + BarePeer = jid:remove_resource(From), + StanzaId = xmpp:get_subtag(Pkt, #stanza_id{by = #jid{}}), + Id = case StanzaId of + #stanza_id{id = Id2} -> + Id2; + _ -> + p1_rand:get_string() + end, + Pkt2 = #message{ + to = To, + from = BarePeer, + id = Id, + sub_els = [#ps_event{ + items = #ps_items{ + node = ?NS_MUCSUB_NODES_MESSAGES, + items = [#ps_item{ + id = Id, + sub_els = [Pkt] + }] + } + }] + }, + Pkt3 = xmpp:put_meta(Pkt2, stanza_id, binary_to_integer(Id)), + store_msg(Pkt3, LUser, LServer, BarePeer, Dir); +store_msg(Pkt, LUser, LServer, Peer, Dir) -> + case get_prefs(LUser, LServer) of + {ok, Prefs} -> + UseMucArchive = mod_mam_opt:user_mucsub_from_muc_archive(LServer), + StoredInMucMam = UseMucArchive andalso xmpp:get_meta(Pkt, in_muc_mam, false), + case {should_archive_peer(LUser, LServer, Prefs, Peer), Pkt, StoredInMucMam} of + {true, #message{meta = #{sm_copy := true}}, _} -> + ok; % Already stored. + {true, _, true} -> + ok; % Stored in muc archive. + {true, _, _} -> + case ejabberd_hooks:run_fold(store_mam_message, LServer, Pkt, + [LUser, LServer, Peer, <<"">>, chat, Dir]) of + #message{} -> ok; + _ -> pass + end; + {false, _, _} -> + pass + end; + {error, _} -> + pass + end. + +-spec store_muc(mod_muc_room:state(), message(), jid(), jid(), binary()) + -> ok | pass | any(). +store_muc(MUCState, Pkt, RoomJID, Peer, Nick) -> + case should_archive_muc(Pkt) of + true -> + {U, S, _} = jid:tolower(RoomJID), + LServer = MUCState#state.server_host, + case ejabberd_hooks:run_fold(store_mam_message, LServer, Pkt, + [U, S, Peer, Nick, groupchat, recv]) of + #message{} -> ok; + _ -> pass + end; + false -> + pass + end. + +store_mam_message(Pkt, U, S, Peer, Nick, Type, Dir) -> + LServer = ejabberd_router:host_of_route(S), + US = {U, S}, + ID = get_stanza_id(Pkt), + OriginID = get_origin_id(Pkt), + Retract = case xmpp:get_subtag(Pkt, #message_retract{}) of + #message_retract{id = RID} when RID /= <<"">> -> + {true, RID}; + _ -> + false + end, + El = xmpp:encode(Pkt), + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:store(El, LServer, US, Type, Peer, Nick, Dir, ID, OriginID, Retract), + Pkt. + +write_prefs(LUser, LServer, Host, Default, Always, Never) -> + Prefs = #archive_prefs{us = {LUser, LServer}, + default = Default, + always = Always, + never = Never}, + Mod = gen_mod:db_mod(Host, ?MODULE), + case Mod:write_prefs(LUser, LServer, Prefs, Host) of + ok -> + case use_cache(Mod, LServer) of + true -> + ets_cache:delete(archive_prefs_cache, {LUser, LServer}, + cache_nodes(Mod, LServer)); + false -> + ok + end; + _Err -> + {error, db_failure} + end. + +get_prefs(LUser, LServer) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + Res = case use_cache(Mod, LServer) of + true -> + ets_cache:lookup(archive_prefs_cache, {LUser, LServer}, + fun() -> Mod:get_prefs(LUser, LServer) end); + false -> + Mod:get_prefs(LUser, LServer) + end, + case Res of + {ok, Prefs} -> + {ok, Prefs}; + {error, _} -> + {error, db_failure}; + error -> + ActivateOpt = mod_mam_opt:request_activates_archiving(LServer), + case ActivateOpt of + true -> + {ok, #archive_prefs{us = {LUser, LServer}, default = never}}; + false -> + Default = mod_mam_opt:default(LServer), + {ok, #archive_prefs{us = {LUser, LServer}, default = Default}} + end + end. + +prefs_el(Default, Always, Never, NS) -> + #mam_prefs{default = Default, + always = [jid:make(LJ) || LJ <- Always], + never = [jid:make(LJ) || LJ <- Never], + xmlns = NS}. + +maybe_activate_mam(LUser, LServer) -> + ActivateOpt = mod_mam_opt:request_activates_archiving(LServer), + case ActivateOpt of + true -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + Res = case use_cache(Mod, LServer) of + true -> + ets_cache:lookup(archive_prefs_cache, + {LUser, LServer}, + fun() -> + Mod:get_prefs(LUser, LServer) + end); + false -> + Mod:get_prefs(LUser, LServer) + end, + case Res of + {ok, _Prefs} -> + ok; + {error, _} -> + {error, db_failure}; + error -> + Default = mod_mam_opt:default(LServer), + write_prefs(LUser, LServer, LServer, Default, [], []) + end; + false -> + ok + end. + +select_and_send(LServer, Query, RSM, FlipPage, #iq{from = From, to = To} = IQ, MsgType) -> + Ret = case MsgType of + chat -> + select(LServer, From, From, Query, RSM, MsgType); + _ -> + select(LServer, From, To, Query, RSM, MsgType) + end, + case Ret of + {Msgs, IsComplete, Count} -> + SortedMsgs = lists:keysort(2, Msgs), + SortedMsgs2 = case FlipPage of + true -> lists:reverse(SortedMsgs); + false -> SortedMsgs + end, + send(SortedMsgs2, Count, IsComplete, IQ); + {error, _} -> + Txt = ?T("Database failure"), + Err = xmpp:err_internal_server_error(Txt, IQ#iq.lang), + xmpp:make_error(IQ, Err) + end. + +select(LServer, JidRequestor, JidArchive, Query, RSM, MsgType) -> + select(LServer, JidRequestor, JidArchive, Query, RSM, MsgType, all). + +select(_LServer, JidRequestor, JidArchive, Query, RSM, + {groupchat, _Role, #state{config = #config{mam = false}, + history = History}} = MsgType, _Flags) -> + Start = proplists:get_value(start, Query), + End = proplists:get_value('end', Query), + #lqueue{queue = Q} = History, + L = p1_queue:len(Q), + Msgs = + lists:flatmap( + fun({Nick, Pkt, _HaveSubject, Now, _Size}) -> + TS = misc:now_to_usec(Now), + case match_interval(Now, Start, End) and + match_rsm(Now, RSM) of + true -> + case msg_to_el(#archive_msg{ + id = integer_to_binary(TS), + type = groupchat, + timestamp = Now, + peer = undefined, + nick = Nick, + packet = Pkt}, + MsgType, JidRequestor, JidArchive) of + {ok, Msg} -> + [{integer_to_binary(TS), TS, Msg}]; + {error, _} -> + [] + end; + false -> + [] + end + end, p1_queue:to_list(Q)), + case RSM of + #rsm_set{max = Max, before = Before} when is_binary(Before) -> + {NewMsgs, IsComplete} = filter_by_max(lists:reverse(Msgs), Max), + {NewMsgs, IsComplete, L}; + #rsm_set{max = Max} -> + {NewMsgs, IsComplete} = filter_by_max(Msgs, Max), + {NewMsgs, IsComplete, L}; + _ -> + {Msgs, true, L} + end; +select(LServer, JidRequestor, JidArchive, Query, RSM, MsgType, Flags) -> + case might_expose_jid(Query, MsgType) of + true -> + {[], true, 0}; + false -> + case {MsgType, mod_mam_opt:user_mucsub_from_muc_archive(LServer)} of + {chat, true} -> + select_with_mucsub(LServer, JidRequestor, JidArchive, Query, RSM, Flags); + _ -> + db_select(LServer, JidRequestor, JidArchive, Query, RSM, MsgType, Flags) + end + end. + +select_with_mucsub(LServer, JidRequestor, JidArchive, Query, RSM, Flags) -> + MucHosts = mod_muc_admin:find_hosts(LServer), + Mod = gen_mod:db_mod(LServer, ?MODULE), + case proplists:get_value(with, Query) of + #jid{lserver = WithLServer} = MucJid -> + case lists:member(WithLServer, MucHosts) of + true -> + select(LServer, JidRequestor, MucJid, Query, RSM, + {groupchat, member, #state{config = #config{mam = true}}}); + _ -> + db_select(LServer, JidRequestor, JidArchive, Query, RSM, chat, Flags) + end; + _ -> + case erlang:function_exported(Mod, select_with_mucsub, 6) of + true -> + Mod:select_with_mucsub(LServer, JidRequestor, JidArchive, Query, RSM, Flags); + false -> + select_with_mucsub_fallback(LServer, JidRequestor, JidArchive, Query, RSM, Flags) + end + end. + +select_with_mucsub_fallback(LServer, JidRequestor, JidArchive, Query, RSM, Flags) -> + case db_select(LServer, JidRequestor, JidArchive, Query, RSM, chat, Flags) of + {error, _} = Err -> + Err; + {Entries, All, Count} -> + {Dir, Max} = case RSM of + #rsm_set{max = M, before = V} when is_binary(V) -> + {desc, M}; + #rsm_set{max = M} -> + {asc, M}; + _ -> + {asc, undefined} + end, + SubRooms = case mod_muc_admin:find_hosts(LServer) of + [First|_] -> + case mod_muc:get_subscribed_rooms(First, JidRequestor) of + {ok, L} -> L; + {error, _} -> [] + end; + _ -> + [] + end, + SubRoomJids = [Jid || {Jid, _, _} <- SubRooms], + {E2, A2, C2} = + lists:foldl( + fun(MucJid, {E0, A0, C0}) -> + case select(LServer, JidRequestor, MucJid, Query, RSM, + {groupchat, member, #state{config = #config{mam = true}}}) of + {error, _} -> + {E0, A0, C0}; + {E, A, C} -> + {lists:keymerge(2, E0, wrap_as_mucsub(E, JidRequestor)), + A0 andalso A, C0 + C} + end + end, {Entries, All, Count}, SubRoomJids), + case {Dir, Max} of + {_, undefined} -> + {E2, A2, C2}; + {desc, _} -> + Start = case length(E2) of + Len when Len < Max -> 1; + Len -> Len - Max + 1 + end, + Sub = lists:sublist(E2, Start, Max), + {Sub, if Sub == E2 -> A2; true -> false end, C2}; + _ -> + Sub = lists:sublist(E2, 1, Max), + {Sub, if Sub == E2 -> A2; true -> false end, C2} + end + end. + +db_select(LServer, JidRequestor, JidArchive, Query, RSM, MsgType, Flags) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + case erlang:function_exported(Mod, select, 7) of + true -> + Mod:select(LServer, JidRequestor, JidArchive, Query, RSM, MsgType, Flags); + _ -> + Mod:select(LServer, JidRequestor, JidArchive, Query, RSM, MsgType) + end. + +wrap_as_mucsub(Messages, #jid{lserver = LServer} = Requester) -> + ReqBare = jid:remove_resource(Requester), + ReqServer = jid:make(<<>>, LServer, <<>>), + [{T1, T2, wrap_as_mucsub(M, ReqBare, ReqServer)} || {T1, T2, M} <- Messages]. + +wrap_as_mucsub(Message, Requester, ReqServer) -> + case Message of + #forwarded{delay = #delay{stamp = Stamp, desc = Desc}, + sub_els = [#message{from = From, sub_els = SubEls, subject = Subject} = Msg]} -> + {L1, SubEls2} = case lists:keytake(mam_archived, 1, SubEls) of + {value, Arch, Rest} -> + {[Arch#mam_archived{by = Requester}], Rest}; + _ -> + {[], SubEls} + end, + {Sid, L2, SubEls3} = case lists:keytake(stanza_id, 1, SubEls2) of + {value, #stanza_id{id = Sid0} = SID, Rest2} -> + {Sid0, [SID#stanza_id{by = Requester} | L1], Rest2}; + _ -> + {p1_rand:get_string(), L1, SubEls2} + end, + Msg2 = Msg#message{to = Requester, sub_els = SubEls3}, + Node = case Subject of + [] -> + ?NS_MUCSUB_NODES_MESSAGES; + _ -> + ?NS_MUCSUB_NODES_SUBJECT + end, + #forwarded{delay = #delay{stamp = Stamp, desc = Desc, from = ReqServer}, + sub_els = [ + #message{from = jid:remove_resource(From), to = Requester, + id = Sid, + sub_els = [#ps_event{ + items = #ps_items{ + node = Node, + items = [#ps_item{ + id = Sid, + sub_els = [Msg2] + }]}} | L2]}]}; + _ -> + Message + end. + + +msg_to_el(#archive_msg{timestamp = TS, packet = El, nick = Nick, + peer = Peer, id = ID}, + MsgType, JidRequestor, #jid{lserver = LServer} = JidArchive) -> + CodecOpts = ejabberd_config:codec_options(), + try xmpp:decode(El, ?NS_CLIENT, CodecOpts) of + Pkt1 -> + Pkt2 = case MsgType of + chat -> set_stanza_id(Pkt1, JidArchive, ID); + {groupchat, _, _} -> set_stanza_id(Pkt1, JidArchive, ID); + _ -> Pkt1 + end, + Pkt3 = maybe_update_from_to( + Pkt2, JidRequestor, JidArchive, Peer, MsgType, Nick), + Pkt4 = xmpp:put_meta(Pkt3, archive_nick, Nick), + Delay = #delay{stamp = TS, from = jid:make(LServer)}, + {ok, #forwarded{sub_els = [Pkt4], delay = Delay}} + catch _:{xmpp_codec, Why} -> + ?ERROR_MSG("Failed to decode raw element ~p from message " + "archive of user ~ts: ~ts", + [El, jid:encode(JidArchive), xmpp:format_error(Why)]), + {error, invalid_xml} + end. + +maybe_update_from_to(#message{sub_els = Els} = Pkt, JidRequestor, JidArchive, + Peer, {groupchat, Role, + #state{config = #config{anonymous = Anon}}}, + Nick) -> + ExposeJID = case {Peer, JidRequestor} of + {undefined, _JidRequestor} -> + false; + {{U, S, _R}, #jid{luser = U, lserver = S}} -> + true; + {_Peer, _JidRequestor} when not Anon; Role == moderator -> + true; + {_Peer, _JidRequestor} -> + false + end, + Items = case ExposeJID of + true -> + [#muc_user{items = [#muc_item{jid = Peer}]}]; + false -> + [] + end, + Pkt#message{from = jid:replace_resource(JidArchive, Nick), + to = undefined, + sub_els = Items ++ Els}; +maybe_update_from_to(Pkt, _JidRequestor, _JidArchive, _Peer, _MsgType, _Nick) -> + Pkt. + +-spec send([{binary(), integer(), xmlel()}], + count(), boolean(), iq()) -> iq() | ignore. +send(Msgs, Count, IsComplete, + #iq{from = From, to = To, + sub_els = [#mam_query{id = QID, xmlns = NS}]} = IQ) -> + Hint = #hint{type = 'no-store'}, + Els = lists:map( + fun({ID, _IDInt, El}) -> + #message{from = To, + to = From, + sub_els = [#mam_result{xmlns = NS, + id = ID, + queryid = QID, + sub_els = [El]}]} + end, Msgs), + RSMOut = make_rsm_out(Msgs, Count), + Result = if NS == ?NS_MAM_TMP -> + #mam_query{xmlns = NS, id = QID, rsm = RSMOut}; + NS == ?NS_MAM_0 -> + #mam_fin{xmlns = NS, id = QID, rsm = RSMOut, + complete = IsComplete}; + true -> + #mam_fin{xmlns = NS, rsm = RSMOut, complete = IsComplete} + end, + if NS /= ?NS_MAM_0 -> + lists:foreach( + fun(El) -> + ejabberd_router:route(El) + end, Els), + xmpp:make_iq_result(IQ, Result); + true -> + ejabberd_router:route(xmpp:make_iq_result(IQ)), + lists:foreach( + fun(El) -> + ejabberd_router:route(El) + end, Els), + ejabberd_router:route( + #message{from = To, to = From, sub_els = [Result, Hint]}), + ignore + end. + +-spec make_rsm_out([{binary(), integer(), xmlel()}], count()) -> rsm_set(). +make_rsm_out([], Count) -> + #rsm_set{count = Count}; +make_rsm_out([{FirstID, _, _}|_] = Msgs, Count) -> + {LastID, _, _} = lists:last(Msgs), + #rsm_set{first = #rsm_first{data = FirstID}, last = LastID, count = Count}. + +filter_by_max(Msgs, undefined) -> + {Msgs, true}; +filter_by_max(Msgs, Len) when is_integer(Len), Len >= 0 -> + {lists:sublist(Msgs, Len), length(Msgs) =< Len}; +filter_by_max(_Msgs, _Junk) -> + {[], true}. + +-spec limit_max(rsm_set(), binary()) -> rsm_set() | undefined. +limit_max(RSM, ?NS_MAM_TMP) -> + RSM; % XEP-0313 v0.2 doesn't require clients to support RSM. +limit_max(undefined, _NS) -> + #rsm_set{max = ?DEF_PAGE_SIZE}; +limit_max(#rsm_set{max = Max} = RSM, _NS) when not is_integer(Max) -> + RSM#rsm_set{max = ?DEF_PAGE_SIZE}; +limit_max(#rsm_set{max = Max} = RSM, _NS) when Max > ?MAX_PAGE_SIZE -> + RSM#rsm_set{max = ?MAX_PAGE_SIZE}; +limit_max(RSM, _NS) -> + RSM. + +match_interval(Now, Start, undefined) -> + Now >= Start; +match_interval(Now, Start, End) -> + (Now >= Start) and (Now =< End). + +match_rsm(Now, #rsm_set{'after' = ID}) when is_binary(ID), ID /= <<"">> -> + Now1 = (catch misc:usec_to_now(binary_to_integer(ID))), + Now > Now1; +match_rsm(Now, #rsm_set{before = ID}) when is_binary(ID), ID /= <<"">> -> + Now1 = (catch misc:usec_to_now(binary_to_integer(ID))), + Now < Now1; +match_rsm(_Now, _) -> + true. + +might_expose_jid(Query, + {groupchat, Role, #state{config = #config{anonymous = true}}}) + when Role /= moderator -> + proplists:is_defined(with, Query); +might_expose_jid(_Query, _MsgType) -> + false. + +get_jids(undefined) -> + []; +get_jids(Js) -> + [jid:tolower(jid:remove_resource(J)) || J <- Js]. + +is_archiving_enabled(LUser, LServer) -> + case gen_mod:is_loaded(LServer, mod_mam) of + true -> + case get_prefs(LUser, LServer) of + {ok, #archive_prefs{default = Default}} when Default /= never -> + true; + _ -> + false + end; + false -> + false + end. + +get_commands_spec() -> + [ + #ejabberd_commands{name = get_mam_count, tags = [mam], + desc = "Get number of MAM messages in a local user archive", + module = ?MODULE, function = get_mam_count, + note = "added in 24.10", + policy = user, + args = [], + result_example = 5, + result_desc = "Number", + result = {value, integer}}, + #ejabberd_commands{name = get_mam_messages, + tags = [internal, mam], + desc = "Get the mam messages", + policy = user, + module = mod_mam, function = get_mam_messages, + args = [], + result = {archive, {list, {messages, {tuple, [{time, string}, + {from, string}, + {to, string}, + {packet, string} + ]}}}}}, + + #ejabberd_commands{name = delete_old_mam_messages, tags = [mam, purge], + desc = "Delete MAM messages older than DAYS", + longdesc = "Valid message TYPEs: " + "`chat`, `groupchat`, `all`.", + module = ?MODULE, function = delete_old_messages, + args_desc = ["Type of messages to delete (`chat`, `groupchat`, `all`)", + "Days to keep messages"], + args_example = [<<"all">>, 31], + args = [{type, binary}, {days, integer}], + result = {res, rescode}}, + #ejabberd_commands{name = delete_old_mam_messages_batch, tags = [mam, purge], + desc = "Delete MAM messages older than DAYS", + note = "added in 22.05", + longdesc = "Valid message TYPEs: " + "`chat`, `groupchat`, `all`.", + module = ?MODULE, function = delete_old_messages_batch, + args_desc = ["Name of host where messages should be deleted", + "Type of messages to delete (`chat`, `groupchat`, `all`)", + "Days to keep messages", + "Number of messages to delete per batch", + "Desired rate of messages to delete per minute"], + args_example = [<<"localhost">>, <<"all">>, 31, 1000, 10000], + args = [{host, binary}, {type, binary}, {days, integer}, {batch_size, integer}, {rate, integer}], + result = {res, restuple}, + result_desc = "Result tuple", + result_example = {ok, <<"Removal of 5000 messages in progress">>}}, + #ejabberd_commands{name = delete_old_mam_messages_status, tags = [mam, purge], + desc = "Status of delete old MAM messages operation", + note = "added in 22.05", + module = ?MODULE, function = delete_old_messages_status, + args_desc = ["Name of host where messages should be deleted"], + args_example = [<<"localhost">>], + args = [{host, binary}], + result = {status, string}, + result_desc = "Status test", + result_example = "Operation in progress, delete 5000 messages"}, + #ejabberd_commands{name = abort_delete_old_mam_messages, tags = [mam, purge], + desc = "Abort currently running delete old MAM messages operation", + note = "added in 22.05", + module = ?MODULE, function = delete_old_messages_abort, + args_desc = ["Name of host where operation should be aborted"], + args_example = [<<"localhost">>], + args = [{host, binary}], + result = {status, string}, + result_desc = "Status text", + result_example = "Operation aborted"}, + #ejabberd_commands{name = remove_mam_for_user, tags = [mam], + desc = "Remove mam archive for user", + module = ?MODULE, function = remove_mam_for_user, + args = [{user, binary}, {host, binary}], + args_rename = [{server, host}], + args_desc = ["Username", "Server"], + args_example = [<<"bob">>, <<"example.com">>], + result = {res, restuple}, + result_desc = "Result tuple", + result_example = {ok, <<"MAM archive removed">>}}, + #ejabberd_commands{name = remove_mam_for_user_with_peer, tags = [mam], + desc = "Remove mam archive for user with peer", + module = ?MODULE, function = remove_mam_for_user_with_peer, + args = [{user, binary}, {host, binary}, {with, binary}], + args_rename = [{server, host}], + args_desc = ["Username", "Server", "Peer"], + args_example = [<<"bob">>, <<"example.com">>, <<"anne@example.com">>], + result = {res, restuple}, + result_desc = "Result tuple", + result_example = {ok, <<"MAM archive removed">>}} + ]. + + +%%% +%%% WebAdmin +%%% + +webadmin_menu_hostuser(Acc, _Host, _Username, _Lang) -> + Acc ++ [{<<"mam">>, <<"MAM">>}, + {<<"mam-archive">>, <<"MAM Archive">>}]. + +webadmin_page_hostuser(_, Host, U, + #request{us = _US, path = [<<"mam">>]} = R) -> + Res = ?H1GL(<<"MAM">>, <<"modules/#mod_mam">>, <<"mod_mam">>) + ++ [make_command(get_mam_count, + R, + [{<<"user">>, U}, {<<"host">>, Host}], + [{result_links, + [{value, arg_host, 5, <<"user/", U/binary, "/mam-archive/">>}]}]), + make_command(remove_mam_for_user, + R, + [{<<"user">>, U}, {<<"host">>, Host}], + [{style, danger}]), + make_command(remove_mam_for_user_with_peer, + R, + [{<<"user">>, U}, {<<"host">>, Host}], + [{style, danger}])], + {stop, Res}; +webadmin_page_hostuser(_, Host, U, + #request{us = _US, path = [<<"mam-archive">> | RPath], + lang = Lang} = R) -> + PageTitle = + str:translate_and_format(Lang, ?T("~ts's MAM Archive"), [jid:encode({U, Host, <<"">>})]), + Head = ?H1GL(PageTitle, <<"modules/#mod_mam">>, <<"mod_mam">>), + Res = make_command(get_mam_messages, R, [{<<"user">>, U}, + {<<"host">>, Host}], + [{table_options, {10, RPath}}, + {result_links, [{packet, paragraph, 1, <<"">>}]}]), + {stop, Head ++ [Res]}; +webadmin_page_hostuser(Acc, _, _, _) -> Acc. + +%%% +%%% Documentation +%%% + +mod_opt_type(compress_xml) -> + econf:bool(); +mod_opt_type(assume_mam_usage) -> + econf:bool(); +mod_opt_type(default) -> + econf:enum([always, never, roster]); +mod_opt_type(request_activates_archiving) -> + econf:bool(); +mod_opt_type(clear_archive_on_room_destroy) -> + econf:bool(); +mod_opt_type(user_mucsub_from_muc_archive) -> + econf:bool(); +mod_opt_type(archive_muc_as_mucsub) -> + econf:bool(); +mod_opt_type(access_preferences) -> + econf:acl(); +mod_opt_type(db_type) -> + econf:db_type(?MODULE); +mod_opt_type(use_cache) -> + econf:bool(); +mod_opt_type(cache_size) -> + econf:pos_int(infinity); +mod_opt_type(cache_missed) -> + econf:bool(); +mod_opt_type(cache_life_time) -> + econf:timeout(second, infinity). + +mod_options(Host) -> + [{assume_mam_usage, false}, + {default, never}, + {request_activates_archiving, false}, + {compress_xml, false}, + {clear_archive_on_room_destroy, true}, + {access_preferences, all}, + {user_mucsub_from_muc_archive, false}, + {archive_muc_as_mucsub, false}, + {db_type, ejabberd_config:default_db(Host, ?MODULE)}, + {use_cache, ejabberd_option:use_cache(Host)}, + {cache_size, ejabberd_option:cache_size(Host)}, + {cache_missed, ejabberd_option:cache_missed(Host)}, + {cache_life_time, ejabberd_option:cache_life_time(Host)}]. + +mod_doc() -> + #{desc => + [?T("This module implements " + "https://xmpp.org/extensions/xep-0313.html" + "[XEP-0313: Message Archive Management] and " + "https://xmpp.org/extensions/xep-0441.html" + "[XEP-0441: Message Archive Management Preferences]. " + "Compatible XMPP clients can use it to store their " + "chat history on the server."), "", + ?T("NOTE: Mnesia backend for mod_mam is not recommended: it's limited " + "to 2GB and often gets corrupted when reaching this limit. " + "SQL backend is recommended. Namely, for small servers SQLite " + "is a preferred choice because it's very easy to configure.")], + opts => + [{access_preferences, + #{value => ?T("AccessName"), + desc => + ?T("This access rule defines who is allowed to modify the " + "MAM preferences. The default value is 'all'.")}}, + {assume_mam_usage, + #{value => "true | false", + desc => + ?T("This option determines how ejabberd's " + "stream management code (see _`mod_stream_mgmt`_) " + "handles unacknowledged messages when the " + "connection is lost. Usually, such messages are " + "either bounced or resent. However, neither is " + "done for messages that were stored in the user's " + "MAM archive if this option is set to 'true'. In " + "this case, ejabberd assumes those messages will " + "be retrieved from the archive. " + "The default value is 'false'.")}}, + {default, + #{value => "always | never | roster", + desc => + ?T("The option defines default policy for chat history. " + "When 'always' is set every chat message is stored. " + "With 'roster' only chat history with contacts from " + "user's roster is stored. And 'never' fully disables " + "chat history. Note that a client can change its " + "policy via protocol commands. " + "The default value is 'never'.")}}, + {request_activates_archiving, + #{value => "true | false", + desc => + ?T("If the value is 'true', no messages are stored " + "for a user until their client issue a MAM request, " + "regardless of the value of the 'default' option. " + "Once the server received a request, that user's " + "messages are archived as usual. " + "The default value is 'false'.")}}, + {compress_xml, + #{value => "true | false", + desc => + ?T("When enabled, new messages added to archives are " + "compressed using a custom compression algorithm. " + "This feature works only with SQL backends. " + "The default value is 'false'.")}}, + {clear_archive_on_room_destroy, + #{value => "true | false", + desc => + ?T("Whether to destroy message archive of a room " + "(see _`mod_muc`_) when it gets destroyed. " + "The default value is 'true'.")}}, + {db_type, + #{value => "mnesia | sql", + desc => + ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}, + {use_cache, + #{value => "true | false", + desc => + ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, + {cache_size, + #{value => "pos_integer() | infinity", + desc => + ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, + {cache_missed, + #{value => "true | false", + desc => + ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, + {cache_life_time, + #{value => "timeout()", + desc => + ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}, + {user_mucsub_from_muc_archive, + #{value => "true | false", + desc => + ?T("When this option is disabled, for each individual " + "subscriber a separate mucsub message is stored. With this " + "option enabled, when a user fetches archive virtual " + "mucsub, messages are generated from muc archives. " + "The default value is 'false'.") + }}, + {archive_muc_as_mucsub, + #{ + value => "true | false", + desc => + ?T("When this option is enabled incoming groupchat messages " + "for users that have mucsub subscription to a room from which " + "message originated will have those messages archived after being " + "converted to mucsub event messages." + "The default value is 'false'.") + }}] + }. diff --git a/src/mod_mam_mnesia.erl b/src/mod_mam_mnesia.erl new file mode 100644 index 000000000..4f59fa1fc --- /dev/null +++ b/src/mod_mam_mnesia.erl @@ -0,0 +1,370 @@ +%%%------------------------------------------------------------------- +%%% File : mod_mam_mnesia.erl +%%% Author : Evgeny Khramtsov +%%% Created : 15 Apr 2016 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_mam_mnesia). + +-behaviour(mod_mam). + +%% API +-export([init/2, remove_user/2, remove_room/3, delete_old_messages/3, + extended_fields/1, store/10, write_prefs/4, get_prefs/2, select/6, + remove_from_archive/3, + is_empty_for_user/2, is_empty_for_room/3, delete_old_messages_batch/5, + transform/1]). + +-include_lib("stdlib/include/ms_transform.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). +-include("logger.hrl"). +-include("mod_mam.hrl"). + +-define(BIN_GREATER_THAN(A, B), + ((A > B andalso byte_size(A) == byte_size(B)) + orelse byte_size(A) > byte_size(B))). +-define(BIN_LESS_THAN(A, B), + ((A < B andalso byte_size(A) == byte_size(B)) + orelse byte_size(A) < byte_size(B))). + +-define(TABLE_SIZE_LIMIT, 2000000000). % A bit less than 2 GiB. + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + try + {atomic, _} = ejabberd_mnesia:create( + ?MODULE, archive_msg, + [{disc_only_copies, [node()]}, + {type, bag}, + {attributes, record_info(fields, archive_msg)}]), + {atomic, _} = ejabberd_mnesia:create( + ?MODULE, archive_prefs, + [{disc_only_copies, [node()]}, + {attributes, record_info(fields, archive_prefs)}]), + ok + catch _:{badmatch, _} -> + {error, db_failure} + end. + +remove_user(LUser, LServer) -> + US = {LUser, LServer}, + F = fun () -> + mnesia:delete({archive_msg, US}), + mnesia:delete({archive_prefs, US}) + end, + mnesia:transaction(F). + +remove_room(_LServer, LName, LHost) -> + remove_user(LName, LHost). + +remove_from_archive(LUser, LHost, Key) when is_binary(LUser) -> + remove_from_archive({LUser, LHost}, LHost, Key); +remove_from_archive(US, _LServer, none) -> + case mnesia:transaction(fun () -> mnesia:delete({archive_msg, US}) end) of + {atomic, _} -> ok; + {aborted, Reason} -> {error, Reason} + end; +remove_from_archive(US, _LServer, #jid{} = WithJid) -> + Peer = jid:remove_resource(jid:split(WithJid)), + F = fun () -> + Msgs = mnesia:select( + archive_msg, + ets:fun2ms( + fun(#archive_msg{us = US1, bare_peer = Peer1} = Msg) + when US1 == US, Peer1 == Peer -> Msg + end)), + lists:foreach(fun mnesia:delete_object/1, Msgs) + end, + case mnesia:transaction(F) of + {atomic, _} -> ok; + {aborted, Reason} -> {error, Reason} + end; +remove_from_archive(US, _LServer, StanzaId) -> + Timestamp = misc:usec_to_now(StanzaId), + F = fun () -> + Msgs = mnesia:select( + archive_msg, + ets:fun2ms( + fun(#archive_msg{us = US1, timestamp = Timestamp1} = Msg) + when US1 == US, Timestamp1 == Timestamp -> Msg + end)), + lists:foreach(fun mnesia:delete_object/1, Msgs) + end, + case mnesia:transaction(F) of + {atomic, _} -> ok; + {aborted, Reason} -> {error, Reason} + end. + +delete_old_messages(global, TimeStamp, Type) -> + mnesia:change_table_copy_type(archive_msg, node(), disc_copies), + Result = delete_old_user_messages(mnesia:dirty_first(archive_msg), TimeStamp, Type), + mnesia:change_table_copy_type(archive_msg, node(), disc_only_copies), + Result. + +delete_old_user_messages('$end_of_table', _TimeStamp, _Type) -> + ok; +delete_old_user_messages(User, TimeStamp, Type) -> + F = fun() -> + Msgs = mnesia:read(archive_msg, User), + Keep = lists:filter( + fun(#archive_msg{timestamp = MsgTS, + type = MsgType}) -> + MsgTS >= TimeStamp orelse (Type /= all andalso + Type /= MsgType) + end, Msgs), + if length(Keep) < length(Msgs) -> + mnesia:delete({archive_msg, User}), + lists:foreach(fun(Msg) -> mnesia:write(Msg) end, Keep); + true -> + ok + end + end, + NextRecord = mnesia:dirty_next(archive_msg, User), + case mnesia:transaction(F) of + {atomic, ok} -> + delete_old_user_messages(NextRecord, TimeStamp, Type); + {aborted, Err} -> + ?ERROR_MSG("Cannot delete old MAM messages: ~ts", [Err]), + Err + end. + +delete_batch('$end_of_table', _LServer, _TS, _Type, Num) -> + {Num, '$end_of_table'}; +delete_batch(LastUS, _LServer, _TS, _Type, 0) -> + {0, LastUS}; +delete_batch(none, LServer, TS, Type, Num) -> + delete_batch(mnesia:first(archive_msg), LServer, TS, Type, Num); +delete_batch({_, LServer2} = LastUS, LServer, TS, Type, Num) when LServer /= LServer2 -> + delete_batch(mnesia:next(archive_msg, LastUS), LServer, TS, Type, Num); +delete_batch(LastUS, LServer, TS, Type, Num) -> + Left = + lists:foldl( + fun(_, 0) -> + 0; + (#archive_msg{timestamp = TS2, type = Type2} = O, Num2) when TS2 < TS, (Type == all orelse Type == Type2) -> + mnesia:delete_object(O), + Num2 - 1; + (_, Num2) -> + Num2 + end, Num, mnesia:wread({archive_msg, LastUS})), + case Left of + 0 -> {0, LastUS}; + _ -> delete_batch(mnesia:next(archive_msg, LastUS), LServer, TS, Type, Left) + end. + +delete_old_messages_batch(LServer, TimeStamp, Type, Batch, LastUS) -> + R = mnesia:transaction( + fun() -> + {Num, NextUS} = delete_batch(LastUS, LServer, TimeStamp, Type, Batch), + {Batch - Num, NextUS} + end), + case R of + {atomic, {Num, State}} -> + {ok, State, Num}; + {aborted, Err} -> + {error, Err} + end. + +extended_fields(_) -> + []. + +store(Pkt, _, {LUser, LServer}, Type, Peer, Nick, _Dir, TS, + OriginID, Retract) -> + case Retract of + {true, RID} -> + mnesia:transaction( + fun () -> + {PUser, PServer, _} = jid:tolower(Peer), + Msgs = mnesia:select( + archive_msg, + ets:fun2ms( + fun(#archive_msg{ + us = US1, + bare_peer = Peer1, + origin_id = OriginID1} = Msg) + when US1 == {LUser, LServer}, + Peer1 == {PUser, PServer, <<>>}, + OriginID1 == RID -> Msg + end)), + lists:foreach(fun mnesia:delete_object/1, Msgs) + end); + false -> ok + end, + case {mnesia:table_info(archive_msg, disc_only_copies), + mnesia:table_info(archive_msg, memory)} of + {[_|_], TableSize} when TableSize > ?TABLE_SIZE_LIMIT -> + ?ERROR_MSG("MAM archives too large, won't store message for ~ts@~ts", + [LUser, LServer]), + {error, overflow}; + _ -> + LPeer = {PUser, PServer, _} = jid:tolower(Peer), + F = fun() -> + mnesia:write( + #archive_msg{us = {LUser, LServer}, + id = integer_to_binary(TS), + timestamp = misc:usec_to_now(TS), + peer = LPeer, + bare_peer = {PUser, PServer, <<>>}, + type = Type, + nick = Nick, + packet = Pkt, + origin_id = OriginID}) + end, + case mnesia:transaction(F) of + {atomic, ok} -> + ok; + {aborted, Err} -> + ?ERROR_MSG("Cannot add message to MAM archive of ~ts@~ts: ~ts", + [LUser, LServer, Err]), + Err + end + end. + +write_prefs(_LUser, _LServer, Prefs, _ServerHost) -> + mnesia:dirty_write(Prefs). + +get_prefs(LUser, LServer) -> + case mnesia:dirty_read(archive_prefs, {LUser, LServer}) of + [Prefs] -> + {ok, Prefs}; + _ -> + error + end. + +select(_LServer, JidRequestor, + #jid{luser = LUser, lserver = LServer} = JidArchive, + Query, RSM, MsgType) -> + Start = proplists:get_value(start, Query), + End = proplists:get_value('end', Query), + With = proplists:get_value(with, Query), + LWith = if With /= undefined -> jid:tolower(With); + true -> undefined + end, + MS = make_matchspec(LUser, LServer, Start, End, LWith), + Msgs = mnesia:dirty_select(archive_msg, MS), + SortedMsgs = lists:keysort(#archive_msg.timestamp, Msgs), + {FilteredMsgs, IsComplete} = filter_by_rsm(SortedMsgs, RSM), + Count = length(Msgs), + Result = {lists:flatmap( + fun(Msg) -> + case mod_mam:msg_to_el( + Msg, MsgType, JidRequestor, JidArchive) of + {ok, El} -> + [{Msg#archive_msg.id, + binary_to_integer(Msg#archive_msg.id), + El}]; + {error, _} -> + [] + end + end, FilteredMsgs), IsComplete, Count}, + erlang:garbage_collect(), + Result. + +is_empty_for_user(LUser, LServer) -> + mnesia:dirty_read(archive_msg, {LUser, LServer}) == []. + +is_empty_for_room(_LServer, LName, LHost) -> + is_empty_for_user(LName, LHost). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +make_matchspec(LUser, LServer, Start, undefined, With) -> + %% List is always greater than a tuple + make_matchspec(LUser, LServer, Start, [], With); +make_matchspec(LUser, LServer, Start, End, {_, _, <<>>} = With) -> + ets:fun2ms( + fun(#archive_msg{timestamp = TS, + us = US, + bare_peer = BPeer} = Msg) + when Start =< TS, End >= TS, + US == {LUser, LServer}, + BPeer == With -> + Msg + end); +make_matchspec(LUser, LServer, Start, End, {_, _, _} = With) -> + ets:fun2ms( + fun(#archive_msg{timestamp = TS, + us = US, + peer = Peer} = Msg) + when Start =< TS, End >= TS, + US == {LUser, LServer}, + Peer == With -> + Msg + end); +make_matchspec(LUser, LServer, Start, End, undefined) -> + ets:fun2ms( + fun(#archive_msg{timestamp = TS, + us = US, + peer = Peer} = Msg) + when Start =< TS, End >= TS, + US == {LUser, LServer} -> + Msg + end). + +filter_by_rsm(Msgs, undefined) -> + {Msgs, true}; +filter_by_rsm(_Msgs, #rsm_set{max = Max}) when Max < 0 -> + {[], true}; +filter_by_rsm(Msgs, #rsm_set{max = Max, before = Before, 'after' = After}) -> + NewMsgs = if is_binary(After), After /= <<"">> -> + lists:filter( + fun(#archive_msg{id = I}) -> + ?BIN_GREATER_THAN(I, After) + end, Msgs); + is_binary(Before), Before /= <<"">> -> + lists:foldl( + fun(#archive_msg{id = I} = Msg, Acc) + when ?BIN_LESS_THAN(I, Before) -> + [Msg|Acc]; + (_, Acc) -> + Acc + end, [], Msgs); + is_binary(Before), Before == <<"">> -> + lists:reverse(Msgs); + true -> + Msgs + end, + filter_by_max(NewMsgs, Max). + +filter_by_max(Msgs, undefined) -> + {Msgs, true}; +filter_by_max(Msgs, Len) when is_integer(Len), Len >= 0 -> + {lists:sublist(Msgs, Len), length(Msgs) =< Len}; +filter_by_max(_Msgs, _Junk) -> + {[], true}. + +transform({archive_msg, US, ID, Timestamp, Peer, BarePeer, + Packet, Nick, Type}) -> + #archive_msg{ + us = US, + id = ID, + timestamp = Timestamp, + peer = Peer, + bare_peer = BarePeer, + packet = Packet, + nick = Nick, + type = Type, + origin_id = <<"">>}; +transform(Other) -> + Other. diff --git a/src/mod_mam_opt.erl b/src/mod_mam_opt.erl new file mode 100644 index 000000000..940ddd183 --- /dev/null +++ b/src/mod_mam_opt.erl @@ -0,0 +1,97 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_mam_opt). + +-export([access_preferences/1]). +-export([archive_muc_as_mucsub/1]). +-export([assume_mam_usage/1]). +-export([cache_life_time/1]). +-export([cache_missed/1]). +-export([cache_size/1]). +-export([clear_archive_on_room_destroy/1]). +-export([compress_xml/1]). +-export([db_type/1]). +-export([default/1]). +-export([request_activates_archiving/1]). +-export([use_cache/1]). +-export([user_mucsub_from_muc_archive/1]). + +-spec access_preferences(gen_mod:opts() | global | binary()) -> 'all' | acl:acl(). +access_preferences(Opts) when is_map(Opts) -> + gen_mod:get_opt(access_preferences, Opts); +access_preferences(Host) -> + gen_mod:get_module_opt(Host, mod_mam, access_preferences). + +-spec archive_muc_as_mucsub(gen_mod:opts() | global | binary()) -> boolean(). +archive_muc_as_mucsub(Opts) when is_map(Opts) -> + gen_mod:get_opt(archive_muc_as_mucsub, Opts); +archive_muc_as_mucsub(Host) -> + gen_mod:get_module_opt(Host, mod_mam, archive_muc_as_mucsub). + +-spec assume_mam_usage(gen_mod:opts() | global | binary()) -> boolean(). +assume_mam_usage(Opts) when is_map(Opts) -> + gen_mod:get_opt(assume_mam_usage, Opts); +assume_mam_usage(Host) -> + gen_mod:get_module_opt(Host, mod_mam, assume_mam_usage). + +-spec cache_life_time(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +cache_life_time(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_life_time, Opts); +cache_life_time(Host) -> + gen_mod:get_module_opt(Host, mod_mam, cache_life_time). + +-spec cache_missed(gen_mod:opts() | global | binary()) -> boolean(). +cache_missed(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_missed, Opts); +cache_missed(Host) -> + gen_mod:get_module_opt(Host, mod_mam, cache_missed). + +-spec cache_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +cache_size(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_size, Opts); +cache_size(Host) -> + gen_mod:get_module_opt(Host, mod_mam, cache_size). + +-spec clear_archive_on_room_destroy(gen_mod:opts() | global | binary()) -> boolean(). +clear_archive_on_room_destroy(Opts) when is_map(Opts) -> + gen_mod:get_opt(clear_archive_on_room_destroy, Opts); +clear_archive_on_room_destroy(Host) -> + gen_mod:get_module_opt(Host, mod_mam, clear_archive_on_room_destroy). + +-spec compress_xml(gen_mod:opts() | global | binary()) -> boolean(). +compress_xml(Opts) when is_map(Opts) -> + gen_mod:get_opt(compress_xml, Opts); +compress_xml(Host) -> + gen_mod:get_module_opt(Host, mod_mam, compress_xml). + +-spec db_type(gen_mod:opts() | global | binary()) -> atom(). +db_type(Opts) when is_map(Opts) -> + gen_mod:get_opt(db_type, Opts); +db_type(Host) -> + gen_mod:get_module_opt(Host, mod_mam, db_type). + +-spec default(gen_mod:opts() | global | binary()) -> 'always' | 'never' | 'roster'. +default(Opts) when is_map(Opts) -> + gen_mod:get_opt(default, Opts); +default(Host) -> + gen_mod:get_module_opt(Host, mod_mam, default). + +-spec request_activates_archiving(gen_mod:opts() | global | binary()) -> boolean(). +request_activates_archiving(Opts) when is_map(Opts) -> + gen_mod:get_opt(request_activates_archiving, Opts); +request_activates_archiving(Host) -> + gen_mod:get_module_opt(Host, mod_mam, request_activates_archiving). + +-spec use_cache(gen_mod:opts() | global | binary()) -> boolean(). +use_cache(Opts) when is_map(Opts) -> + gen_mod:get_opt(use_cache, Opts); +use_cache(Host) -> + gen_mod:get_module_opt(Host, mod_mam, use_cache). + +-spec user_mucsub_from_muc_archive(gen_mod:opts() | global | binary()) -> boolean(). +user_mucsub_from_muc_archive(Opts) when is_map(Opts) -> + gen_mod:get_opt(user_mucsub_from_muc_archive, Opts); +user_mucsub_from_muc_archive(Host) -> + gen_mod:get_module_opt(Host, mod_mam, user_mucsub_from_muc_archive). + diff --git a/src/mod_mam_sql.erl b/src/mod_mam_sql.erl new file mode 100644 index 000000000..8a1d8e02f --- /dev/null +++ b/src/mod_mam_sql.erl @@ -0,0 +1,773 @@ +%%%------------------------------------------------------------------- +%%% File : mod_mam_sql.erl +%%% Author : Evgeny Khramtsov +%%% Created : 15 Apr 2016 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_mam_sql). + + +-behaviour(mod_mam). + +%% API +-export([init/2, remove_user/2, remove_room/3, delete_old_messages/3, + extended_fields/1, store/10, write_prefs/4, get_prefs/2, select/7, export/1, remove_from_archive/3, + is_empty_for_user/2, is_empty_for_room/3, select_with_mucsub/6, + delete_old_messages_batch/4, count_messages_to_delete/3]). +-export([sql_schemas/0]). + +-include_lib("stdlib/include/ms_transform.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). +-include("mod_mam.hrl"). +-include("logger.hrl"). +-include("ejabberd_sql_pt.hrl"). +-include("mod_muc_room.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(Host, _Opts) -> + ejabberd_sql_schema:update_schema(Host, ?MODULE, sql_schemas()), + ok. + +sql_schemas() -> + [#sql_schema{ + version = 2, + tables = + [#sql_table{ + name = <<"archive">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"timestamp">>, type = bigint}, + #sql_column{name = <<"peer">>, type = text}, + #sql_column{name = <<"bare_peer">>, type = text}, + #sql_column{name = <<"xml">>, type = {text, big}}, + #sql_column{name = <<"txt">>, type = {text, big}}, + #sql_column{name = <<"id">>, type = bigserial}, + #sql_column{name = <<"kind">>, type = {text, 10}}, + #sql_column{name = <<"nick">>, type = text}, + #sql_column{name = <<"origin_id">>, type = text}, + #sql_column{name = <<"created_at">>, type = timestamp, + default = true}], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"username">>, <<"timestamp">>]}, + #sql_index{ + columns = [<<"server_host">>, <<"username">>, <<"peer">>]}, + #sql_index{ + columns = [<<"server_host">>, <<"username">>, <<"bare_peer">>]}, + #sql_index{ + columns = [<<"server_host">>, <<"timestamp">>]}, + #sql_index{ + columns = [<<"server_host">>, <<"username">>, <<"origin_id">>]} + ], + post_create = + fun(#sql_schema_info{db_type = mysql}) -> + [<<"CREATE FULLTEXT INDEX i_archive_txt ON archive(txt);">>]; + (_) -> + [] + end}, + #sql_table{ + name = <<"archive_prefs">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"def">>, type = text}, + #sql_column{name = <<"always">>, type = text}, + #sql_column{name = <<"never">>, type = text}, + #sql_column{name = <<"created_at">>, type = timestamp, + default = true}], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"username">>], + unique = true}]}], + update = + [{add_column, <<"archive">>, <<"origin_id">>}, + {create_index, <<"archive">>, + [<<"server_host">>, <<"username">>, <<"origin_id">>]} + ]}, + #sql_schema{ + version = 1, + tables = + [#sql_table{ + name = <<"archive">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"timestamp">>, type = bigint}, + #sql_column{name = <<"peer">>, type = text}, + #sql_column{name = <<"bare_peer">>, type = text}, + #sql_column{name = <<"xml">>, type = {text, big}}, + #sql_column{name = <<"txt">>, type = {text, big}}, + #sql_column{name = <<"id">>, type = bigserial}, + #sql_column{name = <<"kind">>, type = {text, 10}}, + #sql_column{name = <<"nick">>, type = text}, + #sql_column{name = <<"created_at">>, type = timestamp, + default = true}], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"username">>, <<"timestamp">>]}, + #sql_index{ + columns = [<<"server_host">>, <<"username">>, <<"peer">>]}, + #sql_index{ + columns = [<<"server_host">>, <<"username">>, <<"bare_peer">>]}, + #sql_index{ + columns = [<<"server_host">>, <<"timestamp">>]} + ], + post_create = + fun(#sql_schema_info{db_type = mysql}) -> + ejabberd_sql:sql_query_t( + <<"CREATE FULLTEXT INDEX i_archive_txt ON archive(txt);">>); + (_) -> + ok + end}, + #sql_table{ + name = <<"archive_prefs">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"def">>, type = text}, + #sql_column{name = <<"always">>, type = text}, + #sql_column{name = <<"never">>, type = text}, + #sql_column{name = <<"created_at">>, type = timestamp, + default = true}], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"username">>], + unique = true}]}]}]. + +remove_user(LUser, LServer) -> + ejabberd_sql:sql_query( + LServer, + ?SQL("delete from archive where username=%(LUser)s and %(LServer)H")), + ejabberd_sql:sql_query( + LServer, + ?SQL("delete from archive_prefs where username=%(LUser)s and %(LServer)H")). + +remove_room(LServer, LName, LHost) -> + LUser = jid:encode({LName, LHost, <<>>}), + remove_user(LUser, LServer). + +remove_from_archive({LUser, LHost}, LServer, Key) -> + remove_from_archive(jid:encode({LUser, LHost, <<>>}), LServer, Key); +remove_from_archive(LUser, LServer, none) -> + case ejabberd_sql:sql_query(LServer, + ?SQL("delete from archive where username=%(LUser)s and %(LServer)H")) of + {error, Reason} -> {error, Reason}; + _ -> ok + end; +remove_from_archive(LUser, LServer, #jid{} = WithJid) -> + Peer = jid:encode(jid:remove_resource(WithJid)), + case ejabberd_sql:sql_query(LServer, + ?SQL("delete from archive where username=%(LUser)s and %(LServer)H and bare_peer=%(Peer)s")) of + {error, Reason} -> {error, Reason}; + _ -> ok + end; +remove_from_archive(LUser, LServer, StanzaId) -> + case ejabberd_sql:sql_query(LServer, + ?SQL("delete from archive where username=%(LUser)s and %(LServer)H and timestamp=%(StanzaId)d")) of + {error, Reason} -> {error, Reason}; + _ -> ok + end. + +count_messages_to_delete(ServerHost, TimeStamp, Type) -> + TS = misc:now_to_usec(TimeStamp), + Res = + case Type of + all -> + ejabberd_sql:sql_query( + ServerHost, + ?SQL("select count(*) from archive" + " where timestamp < %(TS)d and %(ServerHost)H")); + _ -> + SType = misc:atom_to_binary(Type), + ejabberd_sql:sql_query( + ServerHost, + ?SQL("select @(count(*))d from archive" + " where timestamp < %(TS)d" + " and kind=%(SType)s" + " and %(ServerHost)H")) + end, + case Res of + {selected, [Count]} -> + {ok, Count}; + _ -> + error + end. + +delete_old_messages_batch(ServerHost, TimeStamp, Type, Batch) -> + TS = misc:now_to_usec(TimeStamp), + Res = + case Type of + all -> + ejabberd_sql:sql_query( + ServerHost, + fun(sqlite, _) -> + ejabberd_sql:sql_query_t( + ?SQL("delete from archive where rowid in " + "(select rowid from archive where timestamp < %(TS)d and %(ServerHost)H limit %(Batch)d)")); + (mssql, _) -> + ejabberd_sql:sql_query_t( + ?SQL("delete top(%(Batch)d)§ from archive" + " where timestamp < %(TS)d and %(ServerHost)H")); + (_, _) -> + ejabberd_sql:sql_query_t( + ?SQL("delete from archive" + " where timestamp < %(TS)d and %(ServerHost)H limit %(Batch)d")) + end); + _ -> + SType = misc:atom_to_binary(Type), + ejabberd_sql:sql_query( + ServerHost, + fun(sqlite,_)-> + ejabberd_sql:sql_query_t( + ?SQL("delete from archive where rowid in (" + " select rowid from archive where timestamp < %(TS)d" + " and kind=%(SType)s" + " and %(ServerHost)H limit %(Batch)d)")); + (mssql, _)-> + ejabberd_sql:sql_query_t( + ?SQL("delete top(%(Batch)d) from archive" + " where timestamp < %(TS)d" + " and kind=%(SType)s" + " and %(ServerHost)H")); + (_,_)-> + ejabberd_sql:sql_query_t( + ?SQL("delete from archive" + " where timestamp < %(TS)d" + " and kind=%(SType)s" + " and %(ServerHost)H limit %(Batch)d")) + end) + end, + case Res of + {updated, Count} -> + {ok, Count}; + {error, _} = Error -> + Error + end. + +delete_old_messages(ServerHost, TimeStamp, Type) -> + TS = misc:now_to_usec(TimeStamp), + case Type of + all -> + ejabberd_sql:sql_query( + ServerHost, + ?SQL("delete from archive" + " where timestamp < %(TS)d and %(ServerHost)H")); + _ -> + SType = misc:atom_to_binary(Type), + ejabberd_sql:sql_query( + ServerHost, + ?SQL("delete from archive" + " where timestamp < %(TS)d" + " and kind=%(SType)s" + " and %(ServerHost)H")) + end, + ok. + +extended_fields(LServer) -> + case ejabberd_option:sql_type(LServer) of + mysql -> + [{withtext, <<"">>}, + #xdata_field{var = <<"{urn:xmpp:fulltext:0}fulltext">>, + type = 'text-single', + label = <<"Search the text">>, + values = []}]; + _ -> + [] + end. + +store(Pkt, LServer, {LUser, LHost}, Type, Peer, Nick, _Dir, TS, + OriginID, Retract) -> + SUser = case Type of + chat -> LUser; + groupchat -> jid:encode({LUser, LHost, <<>>}) + end, + BarePeer = jid:encode( + jid:tolower( + jid:remove_resource(Peer))), + LPeer = jid:encode( + jid:tolower(Peer)), + Body = fxml:get_subtag_cdata(Pkt, <<"body">>), + SType = misc:atom_to_binary(Type), + SqlType = ejabberd_option:sql_type(LServer), + XML = case mod_mam_opt:compress_xml(LServer) of + true -> + J1 = case Type of + chat -> jid:encode({LUser, LHost, <<>>}); + groupchat -> SUser + end, + xml_compress:encode(Pkt, J1, LPeer); + _ -> + fxml:element_to_binary(Pkt) + end, + case Retract of + {true, RID} -> + ejabberd_sql:sql_query( + LServer, + ?SQL("delete from archive" + " where username=%(SUser)s" + " and %(LServer)H" + " and bare_peer=%(BarePeer)s" + " and origin_id=%(RID)s")); + false -> ok + end, + case SqlType of + mssql -> case ejabberd_sql:sql_query( + LServer, + ?SQL_INSERT( + "archive", + ["username=%(SUser)s", + "server_host=%(LServer)s", + "timestamp=%(TS)d", + "peer=%(LPeer)s", + "bare_peer=%(BarePeer)s", + "xml=N%(XML)s", + "txt=N%(Body)s", + "kind=%(SType)s", + "nick=%(Nick)s", + "origin_id=%(OriginID)s"])) of + {updated, _} -> + ok; + Err -> + Err + end; + _ -> case ejabberd_sql:sql_query( + LServer, + ?SQL_INSERT( + "archive", + ["username=%(SUser)s", + "server_host=%(LServer)s", + "timestamp=%(TS)d", + "peer=%(LPeer)s", + "bare_peer=%(BarePeer)s", + "xml=%(XML)s", + "txt=%(Body)s", + "kind=%(SType)s", + "nick=%(Nick)s", + "origin_id=%(OriginID)s"])) of + {updated, _} -> + ok; + Err -> + Err + end + end. + +write_prefs(LUser, _LServer, #archive_prefs{default = Default, + never = Never, + always = Always}, + ServerHost) -> + SDefault = erlang:atom_to_binary(Default, utf8), + SAlways = misc:term_to_expr(Always), + SNever = misc:term_to_expr(Never), + case ?SQL_UPSERT( + ServerHost, + "archive_prefs", + ["!username=%(LUser)s", + "!server_host=%(ServerHost)s", + "def=%(SDefault)s", + "always=%(SAlways)s", + "never=%(SNever)s"]) of + ok -> + ok; + Err -> + Err + end. + +get_prefs(LUser, LServer) -> + case ejabberd_sql:sql_query( + LServer, + ?SQL("select @(def)s, @(always)s, @(never)s from archive_prefs" + " where username=%(LUser)s and %(LServer)H")) of + {selected, [{SDefault, SAlways, SNever}]} -> + Default = erlang:binary_to_existing_atom(SDefault, utf8), + Always = ejabberd_sql:decode_term(SAlways), + Never = ejabberd_sql:decode_term(SNever), + {ok, #archive_prefs{us = {LUser, LServer}, + default = Default, + always = Always, + never = Never}}; + _ -> + error + end. + +select(LServer, JidRequestor, #jid{luser = LUser} = JidArchive, + MAMQuery, RSM, MsgType, Flags) -> + User = case MsgType of + chat -> LUser; + _ -> jid:encode(JidArchive) + end, + {Query, CountQuery} = make_sql_query(User, LServer, MAMQuery, RSM, none), + do_select_query(LServer, JidRequestor, JidArchive, RSM, MsgType, Query, CountQuery, Flags). + +-spec select_with_mucsub(binary(), jid(), jid(), mam_query:result(), + #rsm_set{} | undefined, all | only_count | only_messages) -> + {[{binary(), non_neg_integer(), xmlel()}], boolean(), non_neg_integer()} | + {error, db_failure}. +select_with_mucsub(LServer, JidRequestor, #jid{luser = LUser} = JidArchive, + MAMQuery, RSM, Flags) -> + Extra = case gen_mod:db_mod(LServer, mod_muc) of + mod_muc_sql -> + subscribers_table; + _ -> + SubRooms = case mod_muc_admin:find_hosts(LServer) of + [First|_] -> + case mod_muc:get_subscribed_rooms(First, JidRequestor) of + {ok, L} -> L; + {error, _} -> [] + end; + _ -> + [] + end, + [jid:encode(Jid) || {Jid, _, _} <- SubRooms] + end, + {Query, CountQuery} = make_sql_query(LUser, LServer, MAMQuery, RSM, Extra), + do_select_query(LServer, JidRequestor, JidArchive, RSM, chat, Query, CountQuery, Flags). + +do_select_query(LServer, JidRequestor, #jid{luser = LUser} = JidArchive, RSM, + MsgType, Query, CountQuery, Flags) -> + % TODO from XEP-0313 v0.2: "To conserve resources, a server MAY place a + % reasonable limit on how many stanzas may be pushed to a client in one + % request. If a query returns a number of stanzas greater than this limit + % and the client did not specify a limit using RSM then the server should + % return a policy-violation error to the client." We currently don't do this + % for v0.2 requests, but we do limit #rsm_in.max for v0.3 and newer. + QRes = case Flags of + all -> + {ejabberd_sql:sql_query(LServer, Query), ejabberd_sql:sql_query(LServer, CountQuery)}; + only_messages -> + {ejabberd_sql:sql_query(LServer, Query), {selected, ok, [[<<"0">>]]}}; + only_count -> + {{selected, ok, []}, ejabberd_sql:sql_query(LServer, CountQuery)} + end, + case QRes of + {{selected, _, Res}, {selected, _, [[Count]]}} -> + {Max, Direction, _} = get_max_direction_id(RSM), + {Res1, IsComplete} = + if Max >= 0 andalso Max /= undefined andalso length(Res) > Max -> + if Direction == before -> + {lists:nthtail(1, Res), false}; + true -> + {lists:sublist(Res, Max), false} + end; + true -> + {Res, true} + end, + MucState = #state{config = #config{anonymous = true}}, + JidArchiveS = jid:encode(jid:remove_resource(JidArchive)), + {lists:flatmap( + fun([TS, XML, PeerBin, Kind, Nick]) -> + case make_archive_el(JidArchiveS, TS, XML, PeerBin, Kind, Nick, + MsgType, JidRequestor, JidArchive) of + {ok, El} -> + [{TS, binary_to_integer(TS), El}]; + {error, _} -> + [] + end; + ([User, TS, XML, PeerBin, Kind, Nick]) when User == LUser -> + case make_archive_el(JidArchiveS, TS, XML, PeerBin, Kind, Nick, + MsgType, JidRequestor, JidArchive) of + {ok, El} -> + [{TS, binary_to_integer(TS), El}]; + {error, _} -> + [] + end; + ([User, TS, XML, PeerBin, Kind, Nick]) -> + case make_archive_el(User, TS, XML, PeerBin, Kind, Nick, + {groupchat, member, MucState}, JidRequestor, + jid:decode(User)) of + {ok, El} -> + mod_mam:wrap_as_mucsub([{TS, binary_to_integer(TS), El}], + JidRequestor); + {error, _} -> + [] + end + end, Res1), IsComplete, binary_to_integer(Count)}; + _ -> + {[], false, 0} + end. + +export(_Server) -> + [{archive_prefs, + fun(Host, #archive_prefs{us = + {LUser, LServer}, + default = Default, + always = Always, + never = Never}) + when LServer == Host -> + SDefault = erlang:atom_to_binary(Default, utf8), + SAlways = misc:term_to_expr(Always), + SNever = misc:term_to_expr(Never), + [?SQL_INSERT( + "archive_prefs", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "def=%(SDefault)s", + "always=%(SAlways)s", + "never=%(SNever)s"])]; + (_Host, _R) -> + [] + end}, + {archive_msg, + fun([Host | HostTail], #archive_msg{us ={LUser, LServer}, + id = _ID, timestamp = TS, peer = Peer, + type = Type, nick = Nick, packet = Pkt, origin_id = OriginID}) + when (LServer == Host) or ([LServer] == HostTail) -> + TStmp = misc:now_to_usec(TS), + SUser = case Type of + chat -> LUser; + groupchat -> jid:encode({LUser, LServer, <<>>}) + end, + BarePeer = jid:encode(jid:tolower(jid:remove_resource(Peer))), + LPeer = jid:encode(jid:tolower(Peer)), + XML = fxml:element_to_binary(Pkt), + Body = fxml:get_subtag_cdata(Pkt, <<"body">>), + SType = misc:atom_to_binary(Type), + SqlType = ejabberd_option:sql_type(Host), + case SqlType of + mssql -> [?SQL_INSERT( + "archive", + ["username=%(SUser)s", + "server_host=%(LServer)s", + "timestamp=%(TStmp)d", + "peer=%(LPeer)s", + "bare_peer=%(BarePeer)s", + "xml=N%(XML)s", + "txt=N%(Body)s", + "kind=%(SType)s", + "nick=%(Nick)s", + "origin_id=%(OriginID)s"])]; + _ -> [?SQL_INSERT( + "archive", + ["username=%(SUser)s", + "server_host=%(LServer)s", + "timestamp=%(TStmp)d", + "peer=%(LPeer)s", + "bare_peer=%(BarePeer)s", + "xml=%(XML)s", + "txt=%(Body)s", + "kind=%(SType)s", + "nick=%(Nick)s", + "origin_id=%(OriginID)s"])] + end; + (_Host, _R) -> + [] + end}]. + +is_empty_for_user(LUser, LServer) -> + case ejabberd_sql:sql_query( + LServer, + ?SQL("select @(1)d from archive" + " where username=%(LUser)s and %(LServer)H limit 1")) of + {selected, [{1}]} -> + false; + _ -> + true + end. + +is_empty_for_room(LServer, LName, LHost) -> + LUser = jid:encode({LName, LHost, <<>>}), + is_empty_for_user(LUser, LServer). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +make_sql_query(User, LServer, MAMQuery, RSM, ExtraUsernames) -> + Start = proplists:get_value(start, MAMQuery), + End = proplists:get_value('end', MAMQuery), + With = proplists:get_value(with, MAMQuery), + WithText = proplists:get_value(withtext, MAMQuery), + {Max, Direction, ID} = get_max_direction_id(RSM), + ODBCType = ejabberd_option:sql_type(LServer), + ToString = fun(S) -> ejabberd_sql:to_string_literal(ODBCType, S) end, + LimitClause = if is_integer(Max), Max >= 0, ODBCType /= mssql -> + [<<" limit ">>, integer_to_binary(Max+1)]; + true -> + [] + end, + TopClause = if is_integer(Max), Max >= 0, ODBCType == mssql -> + [<<" TOP ">>, integer_to_binary(Max+1)]; + true -> + [] + end, + SubOrderClause = if LimitClause /= []; TopClause /= [] -> + <<" ORDER BY timestamp DESC ">>; + true -> + [] + end, + WithTextClause = if is_binary(WithText), WithText /= <<>> -> + [<<" and match (txt) against (">>, + ToString(WithText), <<")">>]; + true -> + [] + end, + WithClause = case catch jid:tolower(With) of + {_, _, <<>>} -> + [<<" and bare_peer=">>, + ToString(jid:encode(With))]; + {_, _, _} -> + [<<" and peer=">>, + ToString(jid:encode(With))]; + _ -> + [] + end, + PageClause = case catch binary_to_integer(ID) of + I when is_integer(I), I >= 0 -> + case Direction of + before -> + [<<" AND timestamp < ">>, ID]; + 'after' -> + [<<" AND timestamp > ">>, ID]; + _ -> + [] + end; + _ -> + [] + end, + StartClause = case Start of + {_, _, _} -> + [<<" and timestamp >= ">>, + integer_to_binary(misc:now_to_usec(Start))]; + _ -> + [] + end, + EndClause = case End of + {_, _, _} -> + [<<" and timestamp <= ">>, + integer_to_binary(misc:now_to_usec(End))]; + _ -> + [] + end, + SUser = ToString(User), + SServer = ToString(LServer), + + HostMatch = case ejabberd_sql:use_new_schema() of + true -> + [<<" and server_host=", SServer/binary>>]; + _ -> + <<"">> + end, + + {UserSel, UserWhere} = case ExtraUsernames of + Users when is_list(Users) -> + EscUsers = [ToString(U) || U <- [User | Users]], + {<<" username,">>, + [<<" username in (">>, str:join(EscUsers, <<",">>), <<")">>]}; + subscribers_table -> + SJid = ToString(jid:encode({User, LServer, <<>>})), + RoomName = case ODBCType of + sqlite -> + <<"room || '@' || host">>; + _ -> + <<"concat(room, '@', host)">> + end, + {<<" username,">>, + [<<" (username = ">>, SUser, + <<" or username in (select ">>, RoomName, + <<" from muc_room_subscribers where jid=">>, SJid, HostMatch, <<"))">>]}; + _ -> + {<<>>, [<<" username=">>, SUser]} + end, + + Query = [<<"SELECT ">>, TopClause, UserSel, + <<" timestamp, xml, peer, kind, nick" + " FROM archive WHERE">>, UserWhere, HostMatch, + WithClause, WithTextClause, + StartClause, EndClause, PageClause], + + QueryPage = + case Direction of + before -> + % ID can be empty because of + % XEP-0059: Result Set Management + % 2.5 Requesting the Last Page in a Result Set + [<<"SELECT">>, UserSel, <<" timestamp, xml, peer, kind, nick FROM (">>, + Query, SubOrderClause, + LimitClause, <<") AS t ORDER BY timestamp ASC;">>]; + _ -> + [Query, <<" ORDER BY timestamp ASC ">>, + LimitClause, <<";">>] + end, + {QueryPage, + [<<"SELECT COUNT(*) FROM archive WHERE ">>, UserWhere, + HostMatch, WithClause, WithTextClause, + StartClause, EndClause, <<";">>]}. + +-spec get_max_direction_id(rsm_set() | undefined) -> + {integer() | undefined, + before | 'after' | undefined, + binary()}. +get_max_direction_id(RSM) -> + case RSM of + #rsm_set{max = Max, before = Before} when is_binary(Before) -> + {Max, before, Before}; + #rsm_set{max = Max, 'after' = After} when is_binary(After) -> + {Max, 'after', After}; + #rsm_set{max = Max} -> + {Max, undefined, <<>>}; + _ -> + {undefined, undefined, <<>>} + end. + +-spec make_archive_el(binary(), binary(), binary(), binary(), binary(), + binary(), _, jid(), jid()) -> + {ok, xmpp_element()} | {error, invalid_jid | + invalid_timestamp | + invalid_xml}. +make_archive_el(User, TS, XML, Peer, Kind, Nick, MsgType, JidRequestor, JidArchive) -> + case xml_compress:decode(XML, User, Peer) of + #xmlel{} = El -> + try binary_to_integer(TS) of + TSInt -> + try jid:decode(Peer) of + PeerJID -> + Now = misc:usec_to_now(TSInt), + PeerLJID = jid:tolower(PeerJID), + T = case Kind of + <<"">> -> chat; + null -> chat; + _ -> misc:binary_to_atom(Kind) + end, + mod_mam:msg_to_el( + #archive_msg{timestamp = Now, + id = TS, + packet = El, + type = T, + nick = Nick, + peer = PeerLJID}, + MsgType, JidRequestor, JidArchive) + catch _:{bad_jid, _} -> + ?ERROR_MSG("Malformed 'peer' field with value " + "'~ts' detected for user ~ts in table " + "'archive': invalid JID", + [Peer, jid:encode(JidArchive)]), + {error, invalid_jid} + end + catch _:_ -> + ?ERROR_MSG("Malformed 'timestamp' field with value '~ts' " + "detected for user ~ts in table 'archive': " + "not an integer", + [TS, jid:encode(JidArchive)]), + {error, invalid_timestamp} + end; + {error, {_, Reason}} -> + ?ERROR_MSG("Malformed 'xml' field with value '~ts' detected " + "for user ~ts in table 'archive': ~ts", + [XML, jid:encode(JidArchive), Reason]), + {error, invalid_xml} + end. diff --git a/src/mod_matrix_gw.erl b/src/mod_matrix_gw.erl new file mode 100644 index 000000000..54edafcf2 --- /dev/null +++ b/src/mod_matrix_gw.erl @@ -0,0 +1,1073 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_matrix_gw.erl +%%% Author : Alexey Shchepin +%%% Purpose : Matrix gateway +%%% Created : 23 Apr 2022 by Alexey Shchepin +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_matrix_gw). +-ifndef(OTP_BELOW_25). + +-author('alexey@process-one.net'). + +-ifndef(GEN_SERVER). +-define(GEN_SERVER, gen_server). +-endif. +-behaviour(?GEN_SERVER). +-behaviour(gen_mod). + +-export([start/2, stop/1, reload/3, process/2, + start_link/1, + procname/1, + init/1, handle_call/3, handle_cast/2, + handle_info/2, terminate/2, code_change/3, + depends/2, mod_opt_type/1, mod_options/1, mod_doc/0]). +-export([parse_auth/1, encode_canonical_json/1, + is_canonical_json/1, + get_id_domain_exn/1, + base64_decode/1, base64_encode/1, + prune_event/2, get_event_id/2, content_hash/1, + sign_event/3, sign_pruned_event/2, sign_json/2, + send_request/8, s2s_out_bounce_packet/2, user_receive_packet/1, + process_disco_info/1, + process_disco_items/1, + route/1]). + +-include_lib("xmpp/include/xmpp.hrl"). +-include("logger.hrl"). +-include("ejabberd_http.hrl"). +-include("translate.hrl"). +-include("ejabberd_web_admin.hrl"). +-include("mod_matrix_gw.hrl"). + +-define(MAX_REQUEST_SIZE, 1000000). + +-define(CORS_HEADERS, + [{<<"Access-Control-Allow-Origin">>, <<"*">>}, + {<<"Access-Control-Allow-Methods">>, <<"GET, POST, PUT, DELETE, OPTIONS">>}, + {<<"Access-Control-Allow-Headers">>, <<"X-Requested-With, Content-Type, Authorization">>}]). + +process([<<"key">>, <<"v2">>, <<"server">> | _], + #request{method = 'GET', host = _Host} = _Request) -> + Host = ejabberd_config:get_myname(), + KeyName = mod_matrix_gw_opt:key_name(Host), + KeyID = <<"ed25519:", KeyName/binary>>, + ServerName = mod_matrix_gw_opt:matrix_domain(Host), + TS = erlang:system_time(millisecond) + timer:hours(24 * 7), + {PubKey, _PrivKey} = mod_matrix_gw_opt:key(Host), + JSON = #{<<"old_verify_keys">> => #{}, + <<"server_name">> => ServerName, + <<"valid_until_ts">> => TS, + <<"verify_keys">> => #{ + KeyID => #{ + <<"key">> => base64_encode(PubKey) + } + }}, + SJSON = sign_json(Host, JSON), + {200, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], + misc:json_encode(SJSON)}; +process([<<"federation">>, <<"v1">>, <<"version">>], + #request{method = 'GET', host = _Host} = _Request) -> + JSON = #{<<"server">> => #{<<"name">> => <<"ejabberd/mod_matrix_gw">>, + <<"version">> => <<"0.1">>}}, + {200, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], + misc:json_encode(JSON)}; +process([<<"federation">>, <<"v1">>, <<"query">>, <<"profile">>], + #request{method = 'GET', host = _Host} = Request) -> + case proplists:get_value(<<"user_id">>, Request#request.q) of + UserID when is_binary(UserID) -> + Field = + case proplists:get_value(<<"field">>, Request#request.q) of + <<"displayname">> -> displayname; + <<"avatar_url">> -> avatar_url; + undefined -> all; + _ -> error + end, + case Field of + error -> + {400, [], <<"400 Bad Request: bad 'field' parameter">>}; + _ -> + case preprocess_federation_request(Request) of + {ok, _JSON, _Origin} -> + {200, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], <<"{}">>}; + {result, HTTPResult} -> + HTTPResult + end + end; + undefined -> + {400, [], <<"400 Bad Request: missing 'user_id' parameter">>} + end; +process([<<"federation">>, <<"v1">>, <<"user">>, <<"devices">>, UserID], + #request{method = 'GET', host = _Host} = Request) -> + case preprocess_federation_request(Request) of + {ok, _JSON, _Origin} -> + Res = #{<<"devices">> => + [#{<<"device_id">> => <<"ejabberd/mod_matrix_gw">>, + <<"keys">> => []}], + <<"stream_id">> => 1, + <<"user_id">> => UserID}, + {200, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], misc:json_encode(Res)}; + {result, HTTPResult} -> + HTTPResult + end; +process([<<"federation">>, <<"v1">>, <<"user">>, <<"keys">>, <<"query">>], + #request{method = 'POST', host = _Host} = Request) -> + case preprocess_federation_request(Request, false) of + {ok, #{<<"device_keys">> := DeviceKeys}, _Origin} -> + DeviceKeys2 = maps:map(fun(_Key, _) -> #{} end, DeviceKeys), + Res = #{<<"device_keys">> => DeviceKeys2}, + {200, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], + misc:json_encode(Res)}; + {ok, _JSON, _Origin} -> + {400, [], <<"400 Bad Request: invalid format">>}; + {result, HTTPResult} -> + HTTPResult + end; +process([<<"federation">>, <<"v2">>, <<"invite">>, RoomID, EventID], + #request{method = 'PUT', host = _Host} = Request) -> + case preprocess_federation_request(Request) of + {ok, #{<<"event">> := #{%<<"origin">> := Origin, + <<"content">> := Content, + <<"room_id">> := RoomID, + <<"sender">> := Sender, + <<"state_key">> := UserID} = Event, + <<"room_version">> := RoomVer} = JSON, + Origin} -> + case mod_matrix_gw_room:binary_to_room_version(RoomVer) of + #room_version{} = RoomVersion -> + %% TODO: check type and userid + Host = ejabberd_config:get_myname(), + PrunedEvent = prune_event(Event, RoomVersion), + %?DEBUG("invite ~p~n", [{RoomID, EventID, Event, RoomVer, catch mod_matrix_gw_s2s:check_signature(Host, PrunedEvent, RoomVersion), get_pruned_event_id(PrunedEvent)}]), + case mod_matrix_gw_s2s:check_signature(Host, PrunedEvent, RoomVersion) of + true -> + case get_pruned_event_id(PrunedEvent) of + EventID -> + SEvent = sign_pruned_event(Host, PrunedEvent), + ?DEBUG("sign event ~p~n", [SEvent]), + ResJSON = #{<<"event">> => SEvent}, + case Content of + #{<<"is_direct">> := true} -> + mod_matrix_gw_room:join_direct(Host, Origin, RoomID, Sender, UserID); + _ -> + IRS = case JSON of + #{<<"invite_room_state">> := IRS1} -> IRS1; + _ -> [] + end, + mod_matrix_gw_room:send_muc_invite(Host, Origin, RoomID, Sender, UserID, Event, IRS) + end, + ?DEBUG("res ~s~n", [misc:json_encode(ResJSON)]), + {200, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], misc:json_encode(ResJSON)}; + _ -> + {400, [], <<"400 Bad Request: bad event id">>} + end; + false -> + {400, [], <<"400 Bad Request: signature check failed">>} + end; + false -> + {400, [], <<"400 Bad Request: unsupported room version">>} + end; + {ok, _JSON, _Origin} -> + {400, [], <<"400 Bad Request: invalid format">>}; + {result, HTTPResult} -> + HTTPResult + end; +process([<<"federation">>, <<"v1">>, <<"send">>, _TxnID], + #request{method = 'PUT', host = _Host} = Request) -> + case preprocess_federation_request(Request, false) of + {ok, #{<<"origin">> := Origin, + <<"pdus">> := PDUs} = JSON, + Origin} -> + ?DEBUG("send request ~p~n", [JSON]), + Host = ejabberd_config:get_myname(), + Res = lists:map( + fun(PDU) -> + case mod_matrix_gw_room:process_pdu(Host, Origin, PDU) of + {ok, EventID} -> {EventID, #{}}; + {error, Error} -> + {get_event_id(PDU, mod_matrix_gw_room:binary_to_room_version(<<"9">>)), + #{<<"error">> => Error}} + end + end, PDUs), + ?DEBUG("send res ~p~n", [Res]), + {200, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], + misc:json_encode(maps:from_list(Res))}; + {ok, _JSON, _Origin} -> + {400, [], <<"400 Bad Request: invalid format">>}; + {result, HTTPResult} -> + HTTPResult + end; +process([<<"federation">>, <<"v1">>, <<"get_missing_events">>, RoomID], + #request{method = 'POST', host = _Host} = Request) -> + case preprocess_federation_request(Request, false) of + {ok, #{<<"earliest_events">> := EarliestEvents, + <<"latest_events">> := LatestEvents} = JSON, + Origin} -> + ?DEBUG("get_missing_events request ~p~n", [JSON]), + Limit = maps:get(<<"limit">>, JSON, 10), + MinDepth = maps:get(<<"min_depth">>, JSON, 0), + Host = ejabberd_config:get_myname(), + PDUs = mod_matrix_gw_room:get_missing_events( + Host, Origin, RoomID, EarliestEvents, LatestEvents, Limit, MinDepth), + ?DEBUG("get_missing_events res ~p~n", [PDUs]), + Res = #{<<"events">> => PDUs}, + {200, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], + misc:json_encode(Res)}; + {ok, _JSON, _Origin} -> + {400, [], <<"400 Bad Request: invalid format">>}; + {result, HTTPResult} -> + HTTPResult + end; +process([<<"federation">>, <<"v1">>, <<"backfill">>, RoomID], + #request{method = 'GET', host = _Host} = Request) -> + case catch binary_to_integer(proplists:get_value(<<"limit">>, Request#request.q)) of + Limit when is_integer(Limit) -> + case preprocess_federation_request(Request, false) of + {ok, _JSON, Origin} -> + LatestEvents = proplists:get_all_values(<<"v">>, Request#request.q), + ?DEBUG("backfill request ~p~n", [{Limit, LatestEvents}]), + Host = ejabberd_config:get_myname(), + PDUs1 = mod_matrix_gw_room:get_missing_events( + Host, Origin, RoomID, [], LatestEvents, Limit, 0), + PDUs2 = lists:flatmap( + fun(EventID) -> + case mod_matrix_gw_room:get_event(Host, RoomID, EventID) of + {ok, PDU} -> + [PDU]; + _ -> + [] + end + end, LatestEvents), + PDUs = PDUs2 ++ PDUs1, + ?DEBUG("backfill res ~p~n", [PDUs]), + MatrixServer = mod_matrix_gw_opt:matrix_domain(Host), + Res = #{<<"origin">> => MatrixServer, + <<"origin_server_ts">> => erlang:system_time(millisecond), + <<"pdus">> => PDUs}, + {200, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], + misc:json_encode(Res)}; + {result, HTTPResult} -> + HTTPResult + end; + _ -> + {400, [], <<"400 Bad Request: bad 'limit' parameter">>} + end; +process([<<"federation">>, <<"v1">>, <<"state_ids">>, RoomID], + #request{method = 'GET', host = _Host} = Request) -> + case proplists:get_value(<<"event_id">>, Request#request.q) of + EventID when is_binary(EventID) -> + case preprocess_federation_request(Request) of + {ok, _JSON, Origin} -> + Host = ejabberd_config:get_myname(), + case mod_matrix_gw_room:get_state_ids(Host, Origin, RoomID, EventID) of + {ok, AuthChain, PDUs} -> + Res = #{<<"auth_chain_ids">> => AuthChain, + <<"pdu_ids">> => PDUs}, + ?DEBUG("get_state_ids res ~p~n", [Res]), + {200, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], + misc:json_encode(Res)}; + {error, room_not_found} -> + {400, [], <<"400 Bad Request: room not found">>}; + {error, not_allowed} -> + {403, [], <<"403 Forbidden: origin not in room">>}; + {error, event_not_found} -> + {400, [], <<"400 Bad Request: 'event_id' not found">>} + end; + {result, HTTPResult} -> + HTTPResult + end; + undefined -> + {400, [], <<"400 Bad Request: missing 'event_id' parameter">>} + end; +process([<<"federation">>, <<"v1">>, <<"event">>, EventID], + #request{method = 'GET', host = _Host} = Request) -> + case preprocess_federation_request(Request) of + {ok, _JSON, _Origin} -> + Host = ejabberd_config:get_myname(), + %% TODO: very inefficient, replace with an SQL call + PDU = + lists:foldl( + fun(RoomID, undefined) -> + case mod_matrix_gw_room:get_event(Host, RoomID, EventID) of + {ok, PDU} -> + PDU; + _ -> + undefined + end; + (_, Acc) -> + Acc + end, undefined, mod_matrix_gw_room:get_rooms_list()), + ?DEBUG("get_event res ~p~n", [PDU]), + case PDU of + undefined -> + {400, [], <<"400 Bad Request: event not found">>}; + _ -> + MatrixServer = mod_matrix_gw_opt:matrix_domain(Host), + Res = #{<<"origin">> => MatrixServer, + <<"origin_server_ts">> => erlang:system_time(millisecond), + <<"pdus">> => [PDU]}, + {200, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], + misc:json_encode(Res)} + end; + {result, HTTPResult} -> + HTTPResult + end; +process([<<"federation">>, <<"v1">>, <<"make_join">>, RoomID, UserID], + #request{method = 'GET', host = _Host, q = Params} = Request) -> + case preprocess_federation_request(Request) of + {ok, _JSON, Origin} -> + Host = ejabberd_config:get_myname(), + case get_id_domain_exn(UserID) of + Origin -> + case mod_matrix_gw_room:make_join(Host, RoomID, UserID, Params) of + {error, room_not_found} -> + Res = #{<<"errcode">> => <<"M_NOT_FOUND">>, + <<"error">> => <<"Unknown room">>}, + {404, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], + misc:json_encode(Res)}; + {error, not_invited} -> + Res = #{<<"errcode">> => <<"M_FORBIDDEN">>, + <<"error">> => <<"You are not invited to this room">>}, + {403, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], + misc:json_encode(Res)}; + {error, {incompatible_version, Ver}} -> + Res = #{<<"errcode">> => <<"M_INCOMPATIBLE_ROOM_VERSION">>, + <<"error">> => <<"Your homeserver does not support the features required to join this room">>, + <<"room_version">> => Ver}, + {400, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], + misc:json_encode(Res)}; + {ok, Res} -> + {200, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], + misc:json_encode(Res)} + end; + _ -> + Res = #{<<"errcode">> => <<"M_FORBIDDEN">>, + <<"error">> => <<"User not from origin">>}, + {403, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], + misc:json_encode(Res)} + end; + {result, HTTPResult} -> + HTTPResult + end; +process([<<"federation">>, <<"v2">>, <<"send_join">>, RoomID, EventID], + #request{method = 'PUT', host = _Host} = Request) -> + case preprocess_federation_request(Request) of + {ok, #{<<"content">> := #{<<"membership">> := <<"join">>}, + %<<"origin">> := Origin, + <<"room_id">> := RoomID, + <<"sender">> := Sender, + <<"state_key">> := Sender, + <<"type">> := <<"m.room.member">>} = JSON, Origin} -> + Host = ejabberd_config:get_myname(), + case get_id_domain_exn(Sender) of + Origin -> + case mod_matrix_gw_room:send_join(Host, Origin, RoomID, EventID, JSON) of + {error, Error} when is_binary(Error) -> + Res = #{<<"errcode">> => <<"M_BAD_REQUEST">>, + <<"error">> => Error}, + {403, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], + misc:json_encode(Res)}; + {ok, Res} -> + ?DEBUG("send_join res: ~p~n", [Res]), + {200, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], + misc:json_encode(Res)} + end; + _ -> + Res = #{<<"errcode">> => <<"M_FORBIDDEN">>, + <<"error">> => <<"User not from origin">>}, + {403, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], + misc:json_encode(Res)} + end; + {ok, _JSON, _Origin} -> + Res = #{<<"errcode">> => <<"M_BAD_REQUEST">>, + <<"error">> => <<"Invalid event format">>}, + {400, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], + misc:json_encode(Res)}; + {result, HTTPResult} -> + HTTPResult + end; +%process([<<"client">> | ClientPath], Request) -> +% {HTTPCode, Headers, JSON} = mod_matrix_gw_c2s:process(ClientPath, Request), +% ?DEBUG("resp ~p~n", [JSON]), +% {HTTPCode, +% [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>} | +% ?CORS_HEADERS] ++ Headers, +% jiffy:encode(JSON)}; +process(Path, Request) -> + ?DEBUG("matrix 404: ~p~n~p~n", [Path, Request]), + ejabberd_web:error(not_found). + +preprocess_federation_request(Request) -> + preprocess_federation_request(Request, true). + +preprocess_federation_request(Request, DoSignCheck) -> + ?DEBUG("matrix federation: ~p~n", [Request]), + case proplists:get_value('Authorization', Request#request.headers) of + Auth when is_binary(Auth) -> + case parse_auth(Auth) of + #{<<"origin">> := MatrixServer, + <<"key">> := _, + <<"sig">> := _} = AuthParams -> + ?DEBUG("auth ~p~n", [AuthParams]), + if + Request#request.length =< ?MAX_REQUEST_SIZE -> + Request2 = recv_data(Request), + JSON = + if + Request#request.length > 0 -> + try + misc:json_decode(Request2#request.data) + catch + _:_ -> + error + end; + true -> + none + end, + ?DEBUG("json ~p~n", [JSON]), + case JSON of + error -> + {result, {400, [], <<"400 Bad Request: invalid JSON">>}}; + JSON when not DoSignCheck -> + {ok, JSON, MatrixServer}; + JSON -> + Host = ejabberd_config:get_myname(), + case mod_matrix_gw_s2s:check_auth( + Host, MatrixServer, + AuthParams, JSON, + Request2) of + true -> + ?DEBUG("auth ok~n", []), + {ok, JSON, MatrixServer}; + false -> + ?DEBUG("auth failed~n", []), + {result, {401, [], <<"401 Unauthorized">>}} + end + end; + true -> + {result, {400, [], <<"400 Bad Request: size limit">>}} + end; + _ -> + {result, {400, [], <<"400 Bad Request: bad 'Authorization' header">>}} + end; + undefined -> + {result, {400, [], <<"400 Bad Request: no 'Authorization' header">>}} + end. + +recv_data(#request{length = Len, data = Trail, + sockmod = SockMod, socket = Socket} = Request) -> + NewLen = Len - byte_size(Trail), + if NewLen > 0 -> + case SockMod:recv(Socket, NewLen, 60000) of + {ok, Data} -> + Request#request{data = <>}; + {error, _} -> Request + end; + true -> + Request + end. + + +-record(state, + {host :: binary(), + server_host :: binary()}). + +-type state() :: #state{}. + +start(Host, _Opts) -> + case mod_matrix_gw_sup:start(Host) of + {ok, _} -> + {ok, [{hook, s2s_out_bounce_packet, s2s_out_bounce_packet, 50}, + {hook, user_receive_packet, user_receive_packet, 50}]}; + Err -> + Err + end. + +stop(Host) -> + Proc = mod_matrix_gw_sup:procname(Host), + supervisor:terminate_child(ejabberd_gen_mod_sup, Proc), + supervisor:delete_child(ejabberd_gen_mod_sup, Proc). + +reload(_Host, _NewOpts, _OldOpts) -> + ok. + +start_link(Host) -> + Proc = procname(Host), + ?GEN_SERVER:start_link({local, Proc}, ?MODULE, [Host], + ejabberd_config:fsm_limit_opts([])). + +-spec init(list()) -> {ok, state()}. +init([Host]) -> + process_flag(trap_exit, true), + mod_matrix_gw_s2s:create_db(), + mod_matrix_gw_room:create_db(), + %mod_matrix_gw_c2s:create_db(), + Opts = gen_mod:get_module_opts(Host, ?MODULE), + MyHost = gen_mod:get_opt(host, Opts), + register_routes(Host, [MyHost]), + gen_iq_handler:add_iq_handler(ejabberd_local, MyHost, ?NS_DISCO_INFO, + ?MODULE, process_disco_info), + gen_iq_handler:add_iq_handler(ejabberd_local, MyHost, ?NS_DISCO_ITEMS, + ?MODULE, process_disco_items), + {ok, #state{server_host = Host, host = MyHost}}. + +-spec handle_call(term(), {pid(), term()}, state()) -> + {reply, ok | {ok, pid()} | {error, any()}, state()} | + {stop, normal, ok, state()}. +handle_call(stop, _From, State) -> + {stop, normal, ok, State}. + +-spec handle_cast(term(), state()) -> {noreply, state()}. +handle_cast(Msg, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Msg]), + {noreply, State}. + +-spec handle_info(term(), state()) -> {noreply, state()}. +handle_info(Info, State) -> + ?WARNING_MSG("Unexpected info: ~p", [Info]), + {noreply, State}. + +-spec terminate(term(), state()) -> any(). +terminate(_Reason, #state{host = Host}) -> + unregister_routes([Host]), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_ITEMS). + +-spec code_change(term(), state(), term()) -> {ok, state()}. +code_change(_OldVsn, State, _Extra) -> {ok, State}. + + +-spec register_routes(binary(), [binary()]) -> ok. +register_routes(ServerHost, Hosts) -> + lists:foreach( + fun(Host) -> + ejabberd_router:register_route( + Host, ServerHost, {apply, ?MODULE, route}) + end, Hosts). + +unregister_routes(Hosts) -> + lists:foreach( + fun(Host) -> + ejabberd_router:unregister_route(Host) + end, Hosts). + +procname(Host) -> + binary_to_atom( + <<(atom_to_binary(?MODULE, latin1))/binary, "_", Host/binary>>, utf8). + +parse_auth(<<"X-Matrix ", S/binary>>) -> + parse_auth1(S, <<>>, []); +parse_auth(_) -> + error. + +parse_auth1(<<$=, Cs/binary>>, S, Ts) -> + parse_auth2(Cs, S, <<>>, Ts); +parse_auth1(<<$,, Cs/binary>>, <<>>, Ts) -> parse_auth1(Cs, [], Ts); +parse_auth1(<<$\s, Cs/binary>>, <<>>, Ts) -> parse_auth1(Cs, [], Ts); +parse_auth1(<>, S, Ts) -> parse_auth1(Cs, <>, Ts); +parse_auth1(<<>>, <<>>, T) -> maps:from_list(T); +parse_auth1(<<>>, _S, _T) -> error. + +parse_auth2(<<$", Cs/binary>>, Key, Val, Ts) -> + parse_auth3(Cs, Key, Val, Ts); +parse_auth2(<>, Key, Val, Ts) -> + parse_auth4(Cs, Key, <>, Ts); +parse_auth2(<<>>, _, _, _) -> error. + +parse_auth3(<<$", Cs/binary>>, Key, Val, Ts) -> + parse_auth4(Cs, Key, Val, Ts); +parse_auth3(<<$\\, C, Cs/binary>>, Key, Val, Ts) -> + parse_auth3(Cs, Key, <>, Ts); +parse_auth3(<>, Key, Val, Ts) -> + parse_auth3(Cs, Key, <>, Ts); +parse_auth3(<<>>, _, _, _) -> error. + +parse_auth4(<<$,, Cs/binary>>, Key, Val, Ts) -> + parse_auth1(Cs, <<>>, [{Key, Val} | Ts]); +parse_auth4(<<$\s, Cs/binary>>, Key, Val, Ts) -> + parse_auth4(Cs, Key, Val, Ts); +parse_auth4(<>, Key, Val, Ts) -> + parse_auth4(Cs, Key, <>, Ts); +parse_auth4(<<>>, Key, Val, Ts) -> + parse_auth1(<<>>, <<>>, [{Key, Val} | Ts]). + +prune_event(#{<<"type">> := Type, <<"content">> := Content} = Event, + RoomVersion) -> + Keys = + case RoomVersion#room_version.updated_redaction_rules of + false -> + [<<"event_id">>, <<"type">>, <<"room_id">>, <<"sender">>, + <<"state_key">>, <<"content">>, <<"hashes">>, + <<"signatures">>, <<"depth">>, <<"prev_events">>, + <<"prev_state">>, <<"auth_events">>, <<"origin">>, + <<"origin_server_ts">>, <<"membership">>]; + true -> + [<<"event_id">>, <<"type">>, <<"room_id">>, <<"sender">>, + <<"state_key">>, <<"content">>, <<"hashes">>, + <<"signatures">>, <<"depth">>, <<"prev_events">>, + <<"auth_events">>, <<"origin_server_ts">>] + end, + Keys2 = + case {RoomVersion#room_version.hydra, Type} of + {true, <<"m.room.create">>} -> + lists:delete(<<"room_id">>, Keys); + _ -> + Keys + end, + Event2 = maps:with(Keys2, Event), + Content2 = + case Type of + <<"m.room.member">> -> + C3 = + case RoomVersion#room_version.restricted_join_rule_fix of + true -> + maps:with([<<"membership">>, + <<"join_authorised_via_users_server">>], + Content); + false -> + maps:with([<<"membership">>], Content) + end, + case RoomVersion#room_version.updated_redaction_rules of + false -> + C3; + true -> + case Content of + #{<<"third_party_invite">> := + #{<<"signed">> := InvSign}} -> + C3#{<<"third_party_invite">> => + #{<<"signed">> => InvSign}}; + _ -> + C3 + end + end; + <<"m.room.create">> -> + case RoomVersion#room_version.updated_redaction_rules of + false -> + maps:with([<<"creator">>], Content); + true -> + Content + end; + <<"m.room.join_rules">> -> + case RoomVersion#room_version.restricted_join_rule of + false -> + maps:with([<<"join_rule">>], Content); + true -> + maps:with([<<"join_rule">>, <<"allow">>], Content) + end; + <<"m.room.power_levels">> -> + case RoomVersion#room_version.updated_redaction_rules of + false -> + maps:with( + [<<"ban">>, <<"events">>, <<"events_default">>, + <<"kick">>, <<"redact">>, <<"state_default">>, + <<"users">>, <<"users_default">>], Content); + true -> + maps:with( + [<<"ban">>, <<"events">>, <<"events_default">>, + <<"invite">>, + <<"kick">>, <<"redact">>, <<"state_default">>, + <<"users">>, <<"users_default">>], Content) + end; + <<"m.room.history_visibility">> -> + maps:with([<<"history_visibility">>], Content); + <<"m.room.redaction">> -> + case RoomVersion#room_version.updated_redaction_rules of + false -> + #{}; + true -> + maps:with([<<"redacts">>], Content) + end; + <<"m.room.aliases">> when RoomVersion#room_version.special_case_aliases_auth -> + maps:with([<<"aliases">>], Content); + _ -> #{} + end, + Event2#{<<"content">> := Content2}. + +reference_hash(PrunedEvent) -> + Event2 = maps:without([<<"signatures">>, <<"age_ts">>, <<"unsigned">>], + PrunedEvent), + S = encode_canonical_json(Event2), + crypto:hash(sha256, S). + +content_hash(Event) -> + Event2 = maps:without([<<"signatures">>, <<"age_ts">>, <<"unsigned">>, + <<"hashes">>, <<"outlier">>, <<"destinations">>], + Event), + S = encode_canonical_json(Event2), + crypto:hash(sha256, S). + +get_event_id(Event, RoomVersion) -> + PrunedEvent = prune_event(Event, RoomVersion), + get_pruned_event_id(PrunedEvent). + +get_pruned_event_id(PrunedEvent) -> + B = base64url_encode(reference_hash(PrunedEvent)), + <<$$, B/binary>>. + +encode_canonical_json(JSON) -> + JSON2 = sort_json(JSON), + misc:json_encode(JSON2). + +sort_json(#{} = Map) -> + Map2 = maps:map(fun(_K, V) -> + sort_json(V) + end, Map), + {lists:sort(maps:to_list(Map2))}; +sort_json(List) when is_list(List) -> + lists:map(fun sort_json/1, List); +sort_json(JSON) -> + JSON. + +is_canonical_json(N) when is_integer(N), + -16#1FFFFFFFFFFFFF =< N, + N =< 16#1FFFFFFFFFFFFF -> + true; +is_canonical_json(B) when is_binary(B) -> + true; +is_canonical_json(B) when is_boolean(B) -> + true; +is_canonical_json(null) -> + true; +is_canonical_json(Map) when is_map(Map) -> + maps:fold( + fun(_K, V, true) -> + is_canonical_json(V); + (_K, _V, false) -> + false + end, true, Map); +is_canonical_json(List) when is_list(List) -> + lists:all(fun is_canonical_json/1, List); +is_canonical_json(_) -> + false. + + +base64_decode(B) -> + Fixed = + case size(B) rem 4 of + 0 -> B; + %1 -> <>; + 2 -> <>; + 3 -> <>, <<"-">>, [global]), + binary:replace(D1, <<"/">>, <<"_">>, [global]). + +sign_event(Host, Event, RoomVersion) -> + PrunedEvent = prune_event(Event, RoomVersion), + case sign_pruned_event(Host, PrunedEvent) of + #{<<"signatures">> := Signatures} -> + Event#{<<"signatures">> => Signatures} + end. + +sign_pruned_event(Host, PrunedEvent) -> + Event2 = maps:without([<<"age_ts">>, <<"unsigned">>], PrunedEvent), + sign_json(Host, Event2). + +sign_json(Host, JSON) -> + Signatures = maps:get(<<"signatures">>, JSON, #{}), + JSON2 = maps:without([<<"signatures">>, <<"unsigned">>], JSON), + Msg = encode_canonical_json(JSON2), + SignatureName = mod_matrix_gw_opt:matrix_domain(Host), + KeyName = mod_matrix_gw_opt:key_name(Host), + {_PubKey, PrivKey} = mod_matrix_gw_opt:key(Host), + KeyID = <<"ed25519:", KeyName/binary>>, + Sig = crypto:sign(eddsa, none, Msg, [PrivKey, ed25519]), + Sig64 = base64_encode(Sig), + Signatures2 = Signatures#{SignatureName => #{KeyID => Sig64}}, + JSON#{<<"signatures">> => Signatures2}. + +-spec send_request( + binary(), + get | post | put, + binary(), + [binary()], + [{binary(), binary()}], + none | #{atom() | binary() => misc:json_value()}, + [any()], + [any()]) -> {ok, any()} | {error, any()}. + +send_request(Host, Method, MatrixServer, Path, Query, JSON, + HTTPOptions, Options) -> + URI1 = iolist_to_binary( + lists:map(fun(P) -> [$/, misc:uri_quote(P)] end, Path)), + URI = + case Query of + [] -> URI1; + _ -> + URI2 = str:join( + lists:map( + fun({K, V}) -> + iolist_to_binary( + [misc:uri_quote(K), $=, + misc:uri_quote(V)]) + end, Query), $&), + <> + end, + {MHost, MPort} = mod_matrix_gw_s2s:get_matrix_host_port(Host, MatrixServer), + URL = <<"https://", MHost/binary, + ":", (integer_to_binary(MPort))/binary, + URI/binary>>, + SMethod = + case Method of + get -> <<"GET">>; + put -> <<"PUT">>; + post -> <<"POST">> + end, + Auth = make_auth_header(Host, MatrixServer, SMethod, URI, JSON), + Headers = [{"Authorization", binary_to_list(Auth)}], + Content = + case JSON of + none -> <<>>; + _ -> misc:json_encode(JSON) + end, + Request = + case Method of + get -> + {URL, Headers}; + _ -> + {URL, Headers, "application/json;charset=UTF-8", Content} + end, + ?DEBUG("httpc request ~p", [{Method, Request, HTTPOptions, Options}]), + HTTPRes = + httpc:request(Method, + Request, + HTTPOptions, + Options), + ?DEBUG("httpc request res ~p", [HTTPRes]), + HTTPRes. + +make_auth_header(Host, MatrixServer, Method, URI, Content) -> + Origin = mod_matrix_gw_opt:matrix_domain(Host), + JSON = #{<<"method">> => Method, + <<"uri">> => URI, + <<"origin">> => Origin, + <<"destination">> => MatrixServer + }, + JSON2 = + case Content of + none -> JSON; + _ -> + JSON#{<<"content">> => Content} + end, + JSON3 = sign_json(Host, JSON2), + #{<<"signatures">> := #{Origin := #{} = KeySig}} = JSON3, + {KeyID, Sig, _} = maps:next(maps:iterator(KeySig)), + <<"X-Matrix origin=", Origin/binary, ",key=\"", KeyID/binary, + "\",sig=\"", Sig/binary, "\",", + "destination=\"", MatrixServer/binary, "\"">>. + +get_id_domain_exn(B) -> + case binary:split(B, <<":">>) of + [_, Tail] -> Tail; + _ -> error({invalid_id, B}) + end. + +s2s_out_bounce_packet(S2SState, Pkt) -> + #{server_host := Host} = S2SState, + case mod_matrix_gw_opt:matrix_id_as_jid(Host) of + false -> + S2SState; + true -> + To = xmpp:get_to(Pkt), + ServiceHost = mod_matrix_gw_opt:host(Host), + EscU = mod_matrix_gw_room:escape(To#jid.user), + EscS = mod_matrix_gw_room:escape(To#jid.lserver), + NewTo = jid:make(<>, ServiceHost), + ejabberd_router:route(xmpp:set_to(Pkt, NewTo)), + {stop, ignore} + end. + +user_receive_packet({Pkt, C2SState} = Acc) -> + #{lserver := Host} = C2SState, + case mod_matrix_gw_opt:matrix_id_as_jid(Host) of + false -> + Acc; + true -> + ServiceHost = mod_matrix_gw_opt:host(Host), + From = xmpp:get_from(Pkt), + case From#jid.lserver of + ServiceHost -> + case binary:split(From#jid.user, <<"%">>) of + [EscU, EscS] -> + U = mod_matrix_gw_room:unescape(EscU), + S = mod_matrix_gw_room:unescape(EscS), + NewFrom = jid:make(U, S), + {xmpp:set_from(Pkt, NewFrom), C2SState}; + _ -> + Acc + end; + _ -> + Acc + end + end. + +-spec route(stanza()) -> ok. +route(#iq{to = #jid{luser = <<"">>, lresource = <<"">>}} = IQ) -> + ejabberd_router:process_iq(IQ); +route(Pkt) -> + mod_matrix_gw_room:route(Pkt). + +-spec process_disco_info(iq()) -> iq(). +process_disco_info(#iq{type = set, lang = Lang} = IQ) -> + Txt = ?T("Value 'set' of 'type' attribute is not allowed"), + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); +process_disco_info(#iq{type = get, + sub_els = [#disco_info{node = <<"">>}]} = IQ) -> + Features = [?NS_DISCO_INFO, ?NS_DISCO_ITEMS, ?NS_MUC], + Identity = #identity{category = <<"gateway">>, + type = <<"matrix">>}, + xmpp:make_iq_result( + IQ, #disco_info{features = Features, + identities = [Identity]}); +process_disco_info(#iq{type = get, lang = Lang, + sub_els = [#disco_info{}]} = IQ) -> + xmpp:make_error(IQ, xmpp:err_item_not_found(?T("Node not found"), Lang)); +process_disco_info(#iq{lang = Lang} = IQ) -> + Txt = ?T("No module is handling this query"), + xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)). + +-spec process_disco_items(iq()) -> iq(). +process_disco_items(#iq{type = set, lang = Lang} = IQ) -> + Txt = ?T("Value 'set' of 'type' attribute is not allowed"), + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); +process_disco_items(#iq{type = get, + sub_els = [#disco_items{node = <<>>}]} = IQ) -> + xmpp:make_iq_result(IQ, #disco_items{}); +process_disco_items(#iq{type = get, lang = Lang, + sub_els = [#disco_items{}]} = IQ) -> + xmpp:make_error(IQ, xmpp:err_item_not_found(?T("Node not found"), Lang)); +process_disco_items(#iq{lang = Lang} = IQ) -> + Txt = ?T("No module is handling this query"), + xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)). + + +depends(_Host, _Opts) -> + []. + +mod_opt_type(host) -> + econf:host(); +mod_opt_type(matrix_domain) -> + econf:host(); +mod_opt_type(key_name) -> + econf:binary(); +mod_opt_type(key) -> + fun(Key) -> + Key1 = (yconf:binary())(Key), + Key2 = base64_decode(Key1), + crypto:generate_key(eddsa, ed25519, Key2) + end; +mod_opt_type(matrix_id_as_jid) -> + econf:bool(); +mod_opt_type(notary_servers) -> + econf:list(econf:host()); +mod_opt_type(leave_timeout) -> + econf:non_neg_int(). + +-spec mod_options(binary()) -> [{key, {binary(), binary()}} | + {atom(), any()}]. + +mod_options(Host) -> + [{matrix_domain, Host}, + {host, <<"matrix.", Host/binary>>}, + {key_name, <<"">>}, + {key, {<<"">>, <<"">>}}, + {matrix_id_as_jid, false}, + {notary_servers, []}, + {leave_timeout, 0}]. + +mod_doc() -> + #{desc => + [?T("https://matrix.org/[Matrix] gateway. "), + ?T("Supports room versions 9, 10 and 11 since ejabberd 25.03; " + "room versions 4 and higher since ejabberd 25.07; " + "room version 12 (hydra rooms) since ejabberd 25.08. "), + ?T("Erlang/OTP 25 or higher is required to use this module."), + ?T("This module is available since ejabberd 24.02.")], + note => "improved in 25.08", + example => + ["listen:", + " -", + " port: 8448", + " module: ejabberd_http", + " tls: true", + " request_handlers:", + " \"/_matrix\": mod_matrix_gw", + "", + "modules:", + " mod_matrix_gw:", + " key_name: \"key1\"", + " key: \"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\"", + " matrix_id_as_jid: true"], + opts => + [{matrix_domain, + #{value => ?T("Domain"), + desc => + ?T("Specify a domain in the Matrix federation. " + "The keyword '@HOST@' is replaced with the hostname. " + "The default value is '@HOST@'.")}}, + {host, + #{value => ?T("Host"), + desc => + ?T("This option defines the Jabber IDs of the service. " + "If the 'host' option is not specified, the Jabber ID will be " + "the hostname of the virtual host with the prefix '\"matrix.\"'. " + "The keyword '@HOST@' is replaced with the real virtual host name.")}}, + {key_name, + #{value => "string()", + desc => + ?T("Name of the matrix signing key.")}}, + {key, + #{value => "string()", + desc => + ?T("Value of the matrix signing key, in base64.")}}, + {matrix_id_as_jid, + #{value => "true | false", + desc => + ?T("If set to 'true', all packets failing to be delivered via an XMPP " + "server-to-server connection will then be routed to the Matrix gateway " + "by translating a Jabber ID 'user@matrixdomain.tld' to a Matrix user " + "identifier '@user:matrixdomain.tld'. When set to 'false', messages " + "must be explicitly sent to the matrix gateway service Jabber ID to be " + "routed to a remote Matrix server. In this case, to send a message to " + "Matrix user '@user:matrixdomain.tld', the client must send a message " + "to the JID 'user%matrixdomain.tld@matrix.myxmppdomain.tld', where " + "'matrix.myxmppdomain.tld' is the JID of the gateway service as set by the " + "'host' option. The default is 'false'.")}}, + {notary_servers, + #{value => "[Server, ...]", + desc => + ?T("A list of notary servers.")}}, + {leave_timeout, + #{value => "integer()", + desc => + ?T("Delay in seconds between a user leaving a MUC room and sending 'leave' Matrix event.")}} + ] + }. +-endif. diff --git a/src/mod_matrix_gw_opt.erl b/src/mod_matrix_gw_opt.erl new file mode 100644 index 000000000..974aafd48 --- /dev/null +++ b/src/mod_matrix_gw_opt.erl @@ -0,0 +1,55 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_matrix_gw_opt). + +-export([host/1]). +-export([key/1]). +-export([key_name/1]). +-export([leave_timeout/1]). +-export([matrix_domain/1]). +-export([matrix_id_as_jid/1]). +-export([notary_servers/1]). + +-spec host(gen_mod:opts() | global | binary()) -> binary(). +host(Opts) when is_map(Opts) -> + gen_mod:get_opt(host, Opts); +host(Host) -> + gen_mod:get_module_opt(Host, mod_matrix_gw, host). + +-spec key(gen_mod:opts() | global | binary()) -> {binary(),binary()}. +key(Opts) when is_map(Opts) -> + gen_mod:get_opt(key, Opts); +key(Host) -> + gen_mod:get_module_opt(Host, mod_matrix_gw, key). + +-spec key_name(gen_mod:opts() | global | binary()) -> binary(). +key_name(Opts) when is_map(Opts) -> + gen_mod:get_opt(key_name, Opts); +key_name(Host) -> + gen_mod:get_module_opt(Host, mod_matrix_gw, key_name). + +-spec leave_timeout(gen_mod:opts() | global | binary()) -> non_neg_integer(). +leave_timeout(Opts) when is_map(Opts) -> + gen_mod:get_opt(leave_timeout, Opts); +leave_timeout(Host) -> + gen_mod:get_module_opt(Host, mod_matrix_gw, leave_timeout). + +-spec matrix_domain(gen_mod:opts() | global | binary()) -> binary(). +matrix_domain(Opts) when is_map(Opts) -> + gen_mod:get_opt(matrix_domain, Opts); +matrix_domain(Host) -> + gen_mod:get_module_opt(Host, mod_matrix_gw, matrix_domain). + +-spec matrix_id_as_jid(gen_mod:opts() | global | binary()) -> boolean(). +matrix_id_as_jid(Opts) when is_map(Opts) -> + gen_mod:get_opt(matrix_id_as_jid, Opts); +matrix_id_as_jid(Host) -> + gen_mod:get_module_opt(Host, mod_matrix_gw, matrix_id_as_jid). + +-spec notary_servers(gen_mod:opts() | global | binary()) -> [binary()]. +notary_servers(Opts) when is_map(Opts) -> + gen_mod:get_opt(notary_servers, Opts); +notary_servers(Host) -> + gen_mod:get_module_opt(Host, mod_matrix_gw, notary_servers). + diff --git a/src/mod_matrix_gw_room.erl b/src/mod_matrix_gw_room.erl new file mode 100644 index 000000000..65babf7e5 --- /dev/null +++ b/src/mod_matrix_gw_room.erl @@ -0,0 +1,3882 @@ +%%%------------------------------------------------------------------- +%%% File : mod_matrix_gw_room.erl +%%% Author : Alexey Shchepin +%%% Purpose : Matrix rooms +%%% Created : 1 May 2022 by Alexey Shchepin +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_matrix_gw_room). + +-ifndef(OTP_BELOW_25). +-behaviour(gen_statem). + +%% API +-export([start_link/2, supervisor/1, create_db/0, + get_room_pid/2, join_direct/5, process_pdu/3, + get_missing_events/7, get_state_ids/4, + get_rooms_list/0, get_event/3, + make_join/4, send_join/5, + create_new_room/3, room_add_event/3, + binary_to_room_version/1, + parse_user_id/1, + send_muc_invite/7, + escape/1, unescape/1, + route/1]). + +%% gen_statem callbacks +-export([init/1, terminate/3, code_change/4, callback_mode/0]). +-export([handle_event/4]). + +-include_lib("xmpp/include/xmpp.hrl"). +-include("logger.hrl"). +-include("ejabberd_http.hrl"). +-include("mod_matrix_gw.hrl"). + +-record(matrix_room, + {room_id :: binary(), + pid :: pid()}). + +-record(matrix_direct, + {local_remote, + room_id :: binary()}). + +-record(event, + {id :: binary(), + room_version :: #room_version{}, + room_id :: binary(), + type :: binary(), + state_key :: binary() | undefined, + depth :: integer(), + auth_events :: [binary()], + sender :: binary(), + prev_events :: [binary()], + origin_server_ts :: integer(), + json :: #{atom() | binary() => misc:json_value()}, + state_map}). + +-record(direct, + {local_user :: jid() | undefined, + remote_user :: binary() | undefined, + client_state}). + +-record(multi_user, + {join_ts :: integer(), + room_jid :: jid()}). + +-record(multi, + {users :: #{{binary(), binary()} => + ({online, #{binary() => #multi_user{}}} | + {offline, reference()})}}). + +-record(data, + {host :: binary(), + kind :: #direct{} | #multi{} | undefined, + room_id :: binary(), + room_jid :: jid(), + room_version :: #room_version{}, + via :: binary | undefined, + events = #{}, + latest_events = sets:new([{version, 2}]), + nonlatest_events = sets:new([{version, 2}]), + event_queue = treap:empty(), + outgoing_txns = #{}}). + +-define(ROOM_CREATE, <<"m.room.create">>). +-define(ROOM_MEMBER, <<"m.room.member">>). +-define(ROOM_JOIN_RULES, <<"m.room.join_rules">>). +-define(ROOM_POWER_LEVELS, <<"m.room.power_levels">>). +-define(ROOM_3PI, <<"m.room.third_party_invite">>). +-define(ROOM_MESSAGE, <<"m.room.message">>). +-define(ROOM_HISTORY_VISIBILITY, <<"m.room.history_visibility">>). +-define(ROOM_TOPIC, <<"m.room.topic">>). +-define(ROOM_ALIASES, <<"m.room.aliases">>). + +-define(CREATOR_PL, (1 bsl 53)). + +-define(MAX_DEPTH, 16#7FFFFFFFFFFFFFFF). +-define(MAX_TXN_RETRIES, 5). + +-define(MATRIX_ROOM_ALIAS_CACHE, matrix_room_alias_cache). +-define(MATRIX_ROOM_ALIAS_CACHE_ERROR_TIMEOUT, 60000). + +%%%=================================================================== +%%% API +%%%=================================================================== + +%%-------------------------------------------------------------------- +%% @doc +%% Creates a gen_statem process which calls Module:init/1 to +%% initialize. To ensure a synchronized start-up procedure, this +%% function does not return until Module:init/1 has returned. +%% +%% @end +%%-------------------------------------------------------------------- +-spec start_link(binary(), binary()) -> + {ok, Pid :: pid()} | + ignore | + {error, Error :: term()}. +start_link(Host, RoomID) -> + gen_statem:start_link(?MODULE, [Host, RoomID], + ejabberd_config:fsm_limit_opts([])). + +-spec supervisor(binary()) -> atom(). +supervisor(Host) -> + gen_mod:get_module_proc(Host, mod_matrix_gw_room_sup). + +create_db() -> + ejabberd_mnesia:create( + ?MODULE, matrix_room, + [{ram_copies, [node()]}, + {type, set}, + {attributes, record_info(fields, matrix_room)}]), + ejabberd_mnesia:create( + ?MODULE, matrix_direct, + [{ram_copies, [node()]}, + {type, set}, + {attributes, record_info(fields, matrix_direct)}]), + ets_cache:new(?MATRIX_ROOM_ALIAS_CACHE), + ok. + +get_room_pid(Host, RoomID) -> + case get_existing_room_pid(Host, RoomID) of + {error, not_found} -> + case supervisor:start_child(supervisor(Host), + [Host, RoomID]) of + {ok, undefined} -> {error, ignored}; + Res -> Res + end; + {ok, Pid} -> + {ok, Pid} + end. + +get_existing_room_pid(_Host, RoomID) -> + case mnesia:dirty_read(matrix_room, RoomID) of + [] -> + {error, not_found}; + [#matrix_room{pid = Pid}] -> + {ok, Pid} + end. + +join_direct(Host, MatrixServer, RoomID, Sender, UserID) -> + case get_room_pid(Host, RoomID) of + {ok, Pid} -> + gen_statem:cast(Pid, {join_direct, MatrixServer, RoomID, Sender, UserID}); + {error, _} = Error -> + Error + end. + +route(#presence{from = From, to = #jid{luser = <>} = To, + type = Type} = Packet) + when C == $!; + C == $# -> + Host = ejabberd_config:get_myname(), + case room_id_from_xmpp(Host, To#jid.luser) of + {ok, RoomID, Via} -> + case From#jid.lserver of + Host -> + case Type of + available -> + case get_room_pid(Host, RoomID) of + {ok, Pid} -> + gen_statem:cast(Pid, {join, From, Packet, Via}); + {error, _} = Error -> + ?DEBUG("join failed ~p", [{From, To, Error}]), + ok + end; + unavailable -> + case get_existing_room_pid(Host, RoomID) of + {ok, Pid} -> + gen_statem:cast(Pid, {leave, From}); + _ -> + ok + end; + _ -> + ok + end; + _ -> + ok + end; + error -> + Lang = xmpp:get_lang(Packet), + Txt = <<"bad or non-existing room id">>, + Err = xmpp:err_not_acceptable(Txt, Lang), + ejabberd_router:route_error(Packet, Err), + ok + end; +route(#message{from = From, to = #jid{luser = <>} = To, + type = groupchat, + body = Body, + id = MsgID}) + when C == $!; + C == $# -> + Host = ejabberd_config:get_myname(), + case xmpp:get_text(Body) of + <<"">> -> + ok; + Text -> + case room_id_from_xmpp(Host, To#jid.luser) of + {ok, RoomID, _Via} -> + case From#jid.lserver of + Host -> + case user_id_from_jid(From, Host) of + {ok, UserID} -> + case get_existing_room_pid(Host, RoomID) of + {ok, Pid} -> + JSON = + #{<<"content">> => + #{<<"body">> => Text, + <<"msgtype">> => <<"m.text">>, + <<"net.process-one.xmpp-id">> => MsgID}, + <<"sender">> => UserID, + <<"type">> => ?ROOM_MESSAGE}, + gen_statem:cast(Pid, {add_event, JSON}), + ok; + _ -> + ok + end; + error -> + ok + end; + _ -> + ok + end; + error -> + ok + end + end; +route(#message{from = From, to = To, body = Body} = _Pkt) -> + Host = ejabberd_config:get_myname(), + case user_id_from_jid(To, Host) of + {ok, ToMatrixID} -> + case xmpp:get_text(Body) of + <<"">> -> + ok; + Text -> + Key = {{From#jid.luser, From#jid.lserver}, ToMatrixID}, + case mnesia:dirty_read(matrix_direct, Key) of + [#matrix_direct{room_id = RoomID}] -> + ?DEBUG("msg ~p~n", [{RoomID, From, ToMatrixID, Text}]), + case get_existing_room_pid(Host, RoomID) of + {ok, Pid} -> + MatrixServer = mod_matrix_gw_opt:matrix_domain(Host), + FromMatrixID = + <<$@, (From#jid.luser)/binary, $:, MatrixServer/binary>>, + JSON = + #{<<"content">> => + #{<<"body">> => Text, + <<"msgtype">> => <<"m.text">>}, + <<"sender">> => FromMatrixID, + <<"type">> => ?ROOM_MESSAGE}, + gen_statem:cast(Pid, {add_event, JSON}), + ok; + {error, _} -> + %%TODO + ok + end; + _ -> + RoomID = new_room_id(), + ?DEBUG("new room id ~p~n", [RoomID]), + case get_room_pid(Host, RoomID) of + {ok, Pid} -> + MatrixServer = mod_matrix_gw_opt:matrix_domain(Host), + FromMatrixID = + <<$@, (From#jid.luser)/binary, $:, MatrixServer/binary>>, + gen_statem:cast(Pid, {create, MatrixServer, RoomID, + FromMatrixID, ToMatrixID}), + JSONs = + [#{<<"content">> => + #{<<"creator">> => FromMatrixID, + <<"room_version">> => <<"9">>}, + <<"sender">> => FromMatrixID, + <<"state_key">> => <<"">>, + <<"type">> => ?ROOM_CREATE}, + #{<<"content">> => + #{<<"membership">> => <<"join">>}, + <<"sender">> => FromMatrixID, + <<"state_key">> => FromMatrixID, + <<"type">> => ?ROOM_MEMBER}, + #{<<"content">> => + #{<<"ban">> => 50, + <<"events">> => + #{<<"m.room.avatar">> => 50, + <<"m.room.canonical_alias">> => 50, + <<"m.room.encryption">> => 100, + <<"m.room.history_visibility">> => 100, + <<"m.room.name">> => 50, + <<"m.room.power_levels">> => 100, + <<"m.room.server_acl">> => 100, + <<"m.room.tombstone">> => 100}, + <<"events_default">> => 0, + <<"historical">> => 100, + <<"invite">> => 0, + <<"kick">> => 50, + <<"redact">> => 50, + <<"state_default">> => 50, + <<"users">> => + #{FromMatrixID => 100, + ToMatrixID => 100}, + <<"users_default">> => 0}, + <<"sender">> => FromMatrixID, + <<"state_key">> => <<"">>, + <<"type">> => ?ROOM_POWER_LEVELS}, + #{<<"content">> => #{<<"join_rule">> => <<"invite">>}, + <<"sender">> => FromMatrixID, + <<"state_key">> => <<"">>, + <<"type">> => ?ROOM_JOIN_RULES}, + #{<<"content">> => #{<<"history_visibility">> => <<"shared">>}, + <<"sender">> => FromMatrixID, + <<"state_key">> => <<"">>, + <<"type">> => ?ROOM_HISTORY_VISIBILITY}, + #{<<"content">> => #{<<"guest_access">> => <<"can_join">>}, + <<"sender">> => FromMatrixID, + <<"state_key">> => <<"">>, + <<"type">> => <<"m.room.guest_access">>}, + #{<<"content">> => + #{<<"is_direct">> => true, + <<"membership">> => <<"invite">>}, + <<"sender">> => FromMatrixID, + <<"state_key">> => ToMatrixID, + <<"type">> => ?ROOM_MEMBER}, + #{<<"content">> => + #{<<"body">> => Text, + <<"msgtype">> => <<"m.text">>}, + <<"sender">> => FromMatrixID, + <<"type">> => ?ROOM_MESSAGE} + ], + lists:foreach(fun(JSON) -> + gen_statem:cast(Pid, {add_event, JSON}) + end, JSONs), + ok; + {error, _} -> + %%TODO + ok + end + end + end; + error -> + ok + end; +route(#iq{type = Type}) when Type == error; Type == result -> + ok; +route(#iq{type = Type, lang = Lang, sub_els = [_]} = IQ0) -> + try xmpp:decode_els(IQ0) of + #iq{sub_els = [SubEl]} = IQ -> + Result = + case {Type, SubEl} of + {set, _} -> + {error, xmpp:err_not_allowed()}; + {get, #disco_info{node = <<>>}} -> + {result, + #disco_info{identities = + [#identity{category = <<"conference">>, + type = <<"text">>}], + features = [?NS_MUC, ?NS_DISCO_INFO, ?NS_DISCO_ITEMS]}}; + {get, #disco_info{node = _}} -> + {error, xmpp:err_item_not_found()}; + {get, #disco_items{node = <<>>}} -> + {result, #disco_items{}}; + {get, #disco_items{node = _}} -> + {error, xmpp:err_item_not_found()}; + _ -> + {error, xmpp:err_service_unavailable()} + end, + case Result of + {result, Res} -> + ejabberd_router:route(xmpp:make_iq_result(IQ, Res)); + {error, Error} -> + ejabberd_router:route(xmpp:make_error(IQ, Error)) + end + catch _:{xmpp_codec, Why} -> + ErrTxt = xmpp:io_format_error(Why), + Err = xmpp:err_bad_request(ErrTxt, Lang), + ejabberd_router:route_error(IQ0, Err), + ok + end; +route(_) -> + ok. + +get_missing_events(Host, Origin, RoomID, EarliestEvents, LatestEvents, Limit, MinDepth) -> + case get_existing_room_pid(Host, RoomID) of + {ok, Pid} -> + Events = gen_statem:call( + Pid, {get_missing_events, Origin, EarliestEvents, LatestEvents, + Limit, MinDepth}), + [E#event.json || E <- Events]; + {error, _} -> + %%TODO + [] + end. + +get_state_ids(Host, Origin, RoomID, EventID) -> + case get_existing_room_pid(Host, RoomID) of + {ok, Pid} -> + gen_statem:call( + Pid, {get_state_ids, Origin, EventID}); + {error, _} -> + %%TODO + {error, room_not_found} + end. + +get_rooms_list() -> + mnesia:dirty_all_keys(matrix_room). + +get_event(Host, RoomID, EventID) -> + case get_existing_room_pid(Host, RoomID) of + {ok, Pid} -> + gen_statem:call(Pid, {get_event, EventID}); + {error, _} -> + %%TODO + {error, room_not_found} + end. + +make_join(Host, RoomID, UserID, Params) -> + case get_existing_room_pid(Host, RoomID) of + {ok, Pid} -> + gen_statem:call(Pid, {make_join, UserID, Params}); + {error, _} -> + {error, room_not_found} + end. + +send_join(Host, Origin, RoomID, EventID, JSON) -> + case process_pdu(Host, Origin, JSON) of + {ok, EventID} -> + {ok, EventJSON} = get_event(Host, RoomID, EventID), + {ok, AuthChain, StateMap} = get_state_ids(Host, Origin, RoomID, EventID), + AuthChainJSON = + lists:map(fun(EID) -> {ok, E} = get_event(Host, RoomID, EID), E end, AuthChain), + StateMapJSON = + lists:map(fun(EID) -> {ok, E} = get_event(Host, RoomID, EID), E end, StateMap), + MyOrigin = mod_matrix_gw_opt:matrix_domain(Host), + Res = #{<<"event">> => EventJSON, + <<"state">> => StateMapJSON, + <<"auth_chain">> => AuthChainJSON, + <<"origin">> => MyOrigin}, + {ok, Res}; + {ok, _} -> + {error, <<"Bad event id">>}; + {error, _} = Error -> + Error + end. + +create_new_room(Host, XMPPID, MatrixID) -> + RoomID = new_room_id(), + case get_room_pid(Host, RoomID) of + {ok, Pid} -> + MatrixServer = mod_matrix_gw_opt:matrix_domain(Host), + gen_statem:cast(Pid, {create, MatrixServer, RoomID, + XMPPID, MatrixID}), + {ok, RoomID}; + {error, _} = Error -> + Error + end. + +room_add_event(Host, RoomID, Event) -> + case get_existing_room_pid(Host, RoomID) of + {ok, Pid} -> + gen_statem:call(Pid, {add_event, Event}); + {error, _} -> + {error, room_not_found} + end. + +%%%=================================================================== +%%% gen_statem callbacks +%%%=================================================================== + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Whenever a gen_statem is started using gen_statem:start/[3,4] or +%% gen_statem:start_link/[3,4], this function is called by the new +%% process to initialize. +%% @end +%%-------------------------------------------------------------------- +-spec init(Args :: term()) -> gen_statem:init_result(term()). +init([Host, RoomID]) -> + ServiceHost = mod_matrix_gw_opt:host(Host), + {ok, RID} = room_id_to_xmpp(RoomID), + RoomJID = jid:make(RID, ServiceHost), + mnesia:dirty_write( + #matrix_room{room_id = RoomID, + pid = self()}), + {ok, state_name, + #data{host = Host, + room_id = RoomID, + room_jid = RoomJID, + room_version = binary_to_room_version(<<"9">>)}}. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% If the gen_statem runs with CallbackMode =:= handle_event_function +%% this function is called for every event a gen_statem receives. +%% @end +%%-------------------------------------------------------------------- +-spec handle_event( + gen_statem:event_type(), Msg :: term(), + State :: term(), Data :: term()) -> + gen_statem:handle_event_result(). +handle_event({call, From}, get_room_version, _State, Data) -> + {keep_state, Data, [{reply, From, Data#data.room_version}]}; +handle_event({call, From}, get_latest_events, _State, Data) -> + {keep_state, Data, [{reply, From, Data#data.latest_events}]}; +%% set_latest_events is for debugging only +handle_event({call, From}, {set_latest_events, LE}, _State, Data) -> + {keep_state, Data#data{latest_events = LE}, [{reply, From, ok}]}; +handle_event({call, From}, {find_event, EventID}, _State, Data) -> + Res = maps:find(EventID, Data#data.events), + {keep_state, Data, [{reply, From, Res}]}; +handle_event({call, From}, {partition_missed_events, EventIDs}, _State, Data) -> + Res = lists:partition( + fun(EventID) -> + maps:is_key(EventID, Data#data.events) + end, EventIDs), + {keep_state, Data, [{reply, From, Res}]}; +handle_event({call, From}, {partition_events_with_statemap, EventIDs}, _State, Data) -> + Res = lists:partition( + fun(EventID) -> + case maps:find(EventID, Data#data.events) of + {ok, #event{state_map = undefined}} -> false; + {ok, _} -> true; + error -> false + end + end, EventIDs), + {keep_state, Data, [{reply, From, Res}]}; +handle_event({call, From}, {auth_and_store_external_events, EventList}, _State, Data) -> + try + Data2 = do_auth_and_store_external_events(EventList, Data), + {keep_state, Data2, [{reply, From, ok}]} + catch + Class:Reason:ST -> + ?INFO_MSG("failed auth_and_store_external_events: ~p", [{Class, Reason, ST}]), + {keep_state, Data, [{reply, From, {error, Reason}}, + {next_event, internal, update_client}]} + end; +handle_event({call, From}, {resolve_auth_store_event, Event}, _State, Data) -> + try + Data2 = do_resolve_auth_store_event(Event, Data), + {keep_state, Data2, [{reply, From, ok}, + {next_event, internal, update_client}]} + catch + Class:Reason:ST -> + ?INFO_MSG("failed resolve_auth_store_event: ~p", [{Class, Reason, ST}]), + {keep_state, Data, [{reply, From, {error, Reason}}, + {next_event, internal, update_client}]} + end; +handle_event({call, From}, + {get_missing_events, Origin, EarliestEvents, LatestEvents, Limit, MinDepth}, + _State, Data) -> + try + PDUs = do_get_missing_events(Origin, EarliestEvents, LatestEvents, Limit, MinDepth, Data), + {keep_state_and_data, [{reply, From, PDUs}]} + catch + Class:Reason:ST -> + ?INFO_MSG("failed get_missing_events: ~p", [{Class, Reason, ST}]), + {keep_state, Data, [{reply, From, {error, Reason}}]} + end; +handle_event({call, From}, + {get_state_ids, Origin, EventID}, + _State, Data) -> + try + Reply = do_get_state_ids(Origin, EventID, Data), + {keep_state_and_data, [{reply, From, Reply}]} + catch + Class:Reason:ST -> + ?INFO_MSG("failed get_state_ids: ~p", [{Class, Reason, ST}]), + {keep_state, Data, [{reply, From, {error, Reason}}]} + end; +handle_event({call, From}, + {get_event, EventID}, + _State, Data) -> + try + Reply = + case maps:find(EventID, Data#data.events) of + {ok, Event} -> + {ok, Event#event.json}; + _ -> + {error, event_not_found} + end, + {keep_state_and_data, [{reply, From, Reply}]} + catch + Class:Reason:ST -> + ?INFO_MSG("failed get_event: ~p", [{Class, Reason, ST}]), + {keep_state, Data, [{reply, From, {error, Reason}}]} + end; +handle_event({call, From}, + {make_join, UserID, Params}, + _State, Data) -> + try + Ver = (Data#data.room_version)#room_version.id, + Reply = + case lists:member({<<"ver">>, Ver}, Params) of + true -> + JSON = #{<<"content">> => + #{<<"membership">> => <<"join">>}, + <<"sender">> => UserID, + <<"state_key">> => UserID, + <<"type">> => ?ROOM_MEMBER}, + {JSON2, _} = fill_event(JSON, Data), + Event = json_to_event(JSON2, Data#data.room_version), + case check_event_auth(Event, Data) of + true -> + Res = #{<<"event">> => JSON2, + <<"room_version">> => Ver}, + {ok, Res}; + false -> + {error, not_invited} + end; + false -> + {error, {incompatible_version, Ver}} + end, + {keep_state_and_data, [{reply, From, Reply}]} + catch + Class:Reason:ST -> + ?INFO_MSG("failed make_join: ~p", [{Class, Reason, ST}]), + {keep_state, Data, [{reply, From, {error, Reason}}]} + end; +handle_event(cast, {join_direct, MatrixServer, RoomID, Sender, UserID}, State, Data) -> + Host = Data#data.host, + %% TODO: check if there is another solution to "You are not invited to this room" and not receiving the first messages in the room + timer:sleep(1000), + case user_id_to_jid(UserID, Data) of + #jid{lserver = Host} = UserJID -> + mnesia:dirty_write( + #matrix_direct{local_remote = {{UserJID#jid.luser, UserJID#jid.lserver}, Sender}, + room_id = RoomID}), + MakeJoinRes = + mod_matrix_gw:send_request( + Host, get, MatrixServer, + [<<"_matrix">>, <<"federation">>, <<"v1">>, <<"make_join">>, + RoomID, UserID], + [{<<"ver">>, V} || V <- supported_versions()], + none, + [{timeout, 5000}], + [{sync, true}, + {body_format, binary}]), + ?DEBUG("make_join ~p~n", [MakeJoinRes]), + case MakeJoinRes of + {ok, {{_, 200, _}, _Headers, Body}} -> + try misc:json_decode(Body) of + #{<<"event">> := Event, + <<"room_version">> := SRoomVersion} -> + case binary_to_room_version(SRoomVersion) of + false -> + ?DEBUG("unsupported room version on make_join: ~p", [MakeJoinRes]), + {keep_state, Data, []}; + #room_version{} = RoomVersion -> + Origin = mod_matrix_gw_opt:matrix_domain(Host), + Event2 = + Event#{<<"origin">> => Origin, + <<"origin_server_ts">> => + erlang:system_time(millisecond)}, + CHash = mod_matrix_gw:content_hash(Event2), + Event3 = + Event2#{<<"hashes">> => + #{<<"sha256">> => + mod_matrix_gw:base64_encode(CHash)}}, + Event4 = mod_matrix_gw:sign_event(Host, Event3, RoomVersion), + EventID = mod_matrix_gw:get_event_id(Event4, RoomVersion), + SendJoinRes = + mod_matrix_gw:send_request( + Data#data.host, put, MatrixServer, + [<<"_matrix">>, <<"federation">>, + <<"v2">>, <<"send_join">>, + RoomID, EventID], + [], + Event4, + [{connect_timeout, 5000}, + {timeout, 60000}], + [{sync, true}, + {body_format, binary}]), + ?DEBUG("send_join ~p~n", [SendJoinRes]), + process_send_join_res( + MatrixServer, SendJoinRes, RoomVersion, + Data#data{ + kind = #direct{local_user = UserJID, + remote_user = Sender}, + room_version = RoomVersion}) + end; + _JSON -> + ?DEBUG("received bad JSON on make_join: ~p", [MakeJoinRes]), + {next_state, State, Data, []} + catch + _:_ -> + ?DEBUG("received bad JSON on make_join: ~p", [MakeJoinRes]), + {next_state, State, Data, []} + end; + _ -> + ?DEBUG("failed make_join: ~p", [MakeJoinRes]), + {next_state, State, Data, []} + end; + UserJID -> + ?INFO_MSG("bad join user id: ~p", [{UserID, UserJID}]), + {stop, normal} + end; +handle_event(cast, {join, UserJID, Packet, Via}, _State, Data) -> + Host = Data#data.host, + {LUser, LServer, LResource} = jid:tolower(UserJID), + case Data#data.kind of + #multi{users = #{{LUser, LServer} := {online, #{LResource := _}}}} -> + {keep_state_and_data, []}; + #multi{} = Kind -> + case user_id_from_jid(UserJID, Host) of + {ok, UserID} -> + JoinTS = erlang:system_time(millisecond), + JSON = #{<<"content">> => + #{<<"membership">> => <<"join">>}, + <<"sender">> => UserID, + <<"state_key">> => UserID, + <<"type">> => ?ROOM_MEMBER}, + Users = Kind#multi.users, + Resources = + case Users of + #{{LUser, LServer} := {online, Rs}} -> Rs; + #{{LUser, LServer} := {offline, TimerRef}} -> + erlang:cancel_timer(TimerRef), + #{}; + _ -> #{} + end, + RoomJID = jid:remove_resource(xmpp:get_to(Packet)), + Data2 = + Data#data{ + kind = + Kind#multi{ + users = + Users#{{LUser, LServer} => + {online, + Resources#{LResource => + #multi_user{join_ts = JoinTS, + room_jid = RoomJID}}}}}}, + {keep_state, Data2, [{next_event, cast, {add_event, JSON}}]}; + error -> + ?INFO_MSG("bad join user id: ~p", [UserJID]), + {keep_state_and_data, []} + end; + #direct{} -> + {keep_state_and_data, []}; + _ -> + Lang = xmpp:get_lang(Packet), + case user_id_from_jid(UserJID, Host) of + {ok, UserID} -> + %% TODO: async + RoomID = Data#data.room_id, + case Via of + MatrixServer when is_binary(MatrixServer) -> + MakeJoinRes = + mod_matrix_gw:send_request( + Host, get, MatrixServer, + [<<"_matrix">>, <<"federation">>, <<"v1">>, <<"make_join">>, + RoomID, UserID], + [{<<"ver">>, V} || V <- supported_versions()], + none, + [{timeout, 5000}], + [{sync, true}, + {body_format, binary}]), + ?DEBUG("make_join ~p~n", [MakeJoinRes]), + case MakeJoinRes of + {ok, {{_, 200, _}, _Headers, Body}} -> + try misc:json_decode(Body) of + #{<<"event">> := Event, + <<"room_version">> := SRoomVersion} -> + case binary_to_room_version(SRoomVersion) of + false -> + ?DEBUG("unsupported room version on make_join: ~p", [MakeJoinRes]), + {stop, normal}; + #room_version{} = RoomVersion -> + JoinTS = erlang:system_time(millisecond), + Origin = mod_matrix_gw_opt:matrix_domain(Host), + Event2 = + Event#{<<"origin">> => Origin, + <<"origin_server_ts">> => JoinTS}, + CHash = mod_matrix_gw:content_hash(Event2), + Event3 = + Event2#{<<"hashes">> => + #{<<"sha256">> => + mod_matrix_gw:base64_encode(CHash)}}, + Event4 = mod_matrix_gw:sign_event(Host, Event3, RoomVersion), + EventID = mod_matrix_gw:get_event_id(Event4, RoomVersion), + SendJoinRes = + mod_matrix_gw:send_request( + Data#data.host, put, MatrixServer, + [<<"_matrix">>, <<"federation">>, + <<"v2">>, <<"send_join">>, + RoomID, EventID], + [], + Event4, + [{connect_timeout, 5000}, + {timeout, 60000}], + [{sync, true}, + {body_format, binary}]), + RoomJID = jid:remove_resource(xmpp:get_to(Packet)), + ?DEBUG("send_join ~p~n", [SendJoinRes]), + process_send_join_res( + MatrixServer, SendJoinRes, RoomVersion, + Data#data{ + kind = + #multi{users = + #{{LUser, LServer} => + {online, + #{LResource => #multi_user{join_ts = JoinTS, + room_jid = RoomJID}}}}}, + room_version = RoomVersion}) + end; + _JSON -> + ?DEBUG("received bad JSON on make_join: ~p", [MakeJoinRes]), + Txt = <<"received bad JSON on make_join">>, + Err = xmpp:err_bad_request(Txt, Lang), + ejabberd_router:route_error(Packet, Err), + {stop, normal} + catch + _:_ -> + ?DEBUG("received bad JSON on make_join: ~p", [MakeJoinRes]), + Txt = <<"received bad JSON on make_join">>, + Err = xmpp:err_bad_request(Txt, Lang), + ejabberd_router:route_error(Packet, Err), + {stop, normal} + end; + {ok, {{_, 400, _}, _Headers, Body}} -> + ?DEBUG("failed make_join: ~p", [MakeJoinRes]), + Txt = <<"make_join failed: ", Body/binary>>, + Err = xmpp:err_bad_request(Txt, Lang), + ejabberd_router:route_error(Packet, Err), + {stop, normal}; + _ -> + ?DEBUG("failed make_join: ~p", [MakeJoinRes]), + Txt = <<"make_join failed">>, + Err = xmpp:err_bad_request(Txt, Lang), + ejabberd_router:route_error(Packet, Err), + {stop, normal} + end; + undefined -> + ?DEBUG("don't know which server to connect to", []), + Txt = <<"unknown remote server">>, + Err = xmpp:err_bad_request(Txt, Lang), + ejabberd_router:route_error(Packet, Err), + {stop, normal} + end; + error -> + ?INFO_MSG("bad join user id: ~p", [UserJID]), + Txt = <<"bad user id">>, + Err = xmpp:err_bad_request(Txt, Lang), + ejabberd_router:route_error(Packet, Err), + {stop, normal} + end + end; +handle_event(cast, {leave, UserJID}, _State, Data) -> + Host = Data#data.host, + {LUser, LServer, LResource} = jid:tolower(UserJID), + case Data#data.kind of + #multi{users = #{{LUser, LServer} := {online, #{LResource := _} = Resources}} = Users} -> + Resources2 = maps:remove(LResource, Resources), + if + Resources2 == #{} -> + LeaveTimeout = mod_matrix_gw_opt:leave_timeout(Host) * 1000, + TimerRef = erlang:start_timer(LeaveTimeout, self(), + {leave, LUser, LServer}), + Users2 = Users#{{LUser, LServer} => {offline, TimerRef}}, + Kind = (Data#data.kind)#multi{users = Users2}, + Data2 = Data#data{kind = Kind}, + {keep_state, Data2, []}; + true -> + Users2 = Users#{{LUser, LServer} => {online, Resources2}}, + Kind = (Data#data.kind)#multi{users = Users2}, + Data2 = Data#data{kind = Kind}, + {keep_state, Data2, []} + end; + _ -> + {keep_state_and_data, []} + end; +handle_event(cast, {create, _MatrixServer, RoomID, LocalUserID, RemoteUserID}, _State, Data) -> + Host = Data#data.host, + case user_id_to_jid(LocalUserID, Data) of + #jid{lserver = Host} = UserJID -> + mnesia:dirty_write( + #matrix_direct{local_remote = {{UserJID#jid.luser, UserJID#jid.lserver}, RemoteUserID}, + room_id = RoomID}), + {keep_state, + Data#data{kind = #direct{local_user = UserJID, + remote_user = RemoteUserID}}, []}; + UserJID -> + ?INFO_MSG("bad create user id: ~p", [{LocalUserID, UserJID}]), + {stop, normal} + end; +handle_event(cast, {add_event, JSON}, _State, Data) -> + try + {Data2, _Event} = add_event(JSON, Data), + {keep_state, Data2, [{next_event, internal, update_client}]} + catch + Class:Reason:ST -> + ?INFO_MSG("failed add_event: ~p", [{Class, Reason, ST}]), + {keep_state, Data, []} + end; +handle_event({call, From}, {add_event, JSON}, _State, Data) -> + try + {Data2, Event} = add_event(JSON, Data), + {keep_state, Data2, [{reply, From, {ok, Event#event.id}}, + {next_event, internal, update_client}]} + catch + Class:Reason:ST -> + ?INFO_MSG("failed add_event: ~p", [{Class, Reason, ST}]), + {keep_state, Data, []} + end; +handle_event(cast, Msg, State, Data) -> + ?WARNING_MSG("Unexpected cast: ~p", [Msg]), + {next_state, State, Data, []}; +handle_event(internal, update_client, _State, + #data{kind = #direct{local_user = JID}} = Data) -> + try + case update_client(Data) of + {ok, Data2} -> + {keep_state, Data2, []}; + {leave, LeaveReason, Data2} -> + ?INFO_MSG("leaving ~p: ~p", [Data#data.room_id, LeaveReason]), + Host = Data#data.host, + MatrixServer = mod_matrix_gw_opt:matrix_domain(Host), + LocalUserID = <<$@, (JID#jid.luser)/binary, $:, MatrixServer/binary>>, + JSON = #{<<"content">> => + #{<<"membership">> => <<"leave">>}, + <<"sender">> => LocalUserID, + <<"state_key">> => LocalUserID, + <<"type">> => ?ROOM_MEMBER}, + {keep_state, Data2, [{next_event, cast, {add_event, JSON}}]}; + stop -> + {stop, normal} + end + catch + Class:Reason:ST -> + ?INFO_MSG("failed update_client: ~p", [{Class, Reason, ST}]), + {keep_state_and_data, []} + end; +handle_event(internal, update_client, _State, + #data{kind = #multi{}} = Data) -> + try + case update_client(Data) of + {ok, Data2} -> + {keep_state, Data2, []}; + stop -> + {stop, normal} + end + catch + Class:Reason:ST -> + ?INFO_MSG("failed update_client: ~p", [{Class, Reason, ST}]), + {keep_state_and_data, []} + end; +handle_event(internal, update_client, _State, #data{kind = undefined}) -> + {keep_state_and_data, []}; +handle_event(info, {send_txn_res, RequestID, TxnID, Server, Res}, _State, Data) -> + ?DEBUG("send_txn_res ~p", [{RequestID, TxnID, Server, Res}]), + case Data#data.outgoing_txns of + #{Server := {{RequestID, TxnID, _Events, Count}, Queue}} -> + Done = + case Res of + {{_, 200, _}, _Headers, _Body} -> true; + _ when Count < ?MAX_TXN_RETRIES -> false; + _ -> true + end, + case Done of + true -> + Data2 = + case Queue of + [] -> + Data#data{outgoing_txns = + maps:remove(Server, Data#data.outgoing_txns)}; + _ -> + send_new_txn(lists:reverse(Queue), Server, Data) + end, + {keep_state, Data2, []}; + false -> + erlang:send_after(30000, self(), {resend_txn, Server}), + {keep_state, Data, []} + end; + _ -> + {keep_state, Data, []} + end; +handle_event(info, {resend_txn, Server}, _State, Data) -> + case Data#data.outgoing_txns of + #{Server := {{_RequestID, TxnID, Events, Count}, Queue}} -> + Data2 = send_txn(TxnID, Events, Server, Count + 1, Queue, Data), + {keep_state, Data2, []}; + _ -> + {keep_state, Data, []} + end; +handle_event(info, {timeout, TimerRef, {leave, LUser, LServer}}, State, Data) -> + Host = Data#data.host, + case Data#data.kind of + #multi{users = #{{LUser, LServer} := {offline, TimerRef}} = Users} -> + Users2 = maps:remove({LUser, LServer}, Users), + Kind = (Data#data.kind)#multi{users = Users2}, + Data2 = Data#data{kind = Kind}, + {ok, UserID} = user_id_from_jid(jid:make(LUser, LServer), Host), + JSON = #{<<"content">> => + #{<<"membership">> => <<"leave">>}, + <<"sender">> => UserID, + <<"state_key">> => UserID, + <<"type">> => ?ROOM_MEMBER}, + {keep_state, Data2, [{next_event, cast, {add_event, JSON}}]}; + _ -> + {next_state, State, Data, []} + end; +handle_event(info, Info, State, Data) -> + ?WARNING_MSG("Unexpected info: ~p", [Info]), + {next_state, State, Data, []}. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% This function is called by a gen_statem when it is about to +%% terminate. It should be the opposite of Module:init/1 and do any +%% necessary cleaning up. When it returns, the gen_statem terminates with +%% Reason. The return value is ignored. +%% @end +%%-------------------------------------------------------------------- +-spec terminate(Reason :: term(), State :: term(), Data :: term()) -> + any(). +terminate(Reason, _State, Data) -> + mnesia:dirty_delete_object( + #matrix_room{room_id = Data#data.room_id, + pid = self()}), + %% TODO: wait for messages + case Data#data.kind of + #direct{local_user = #jid{} = LocalUserJID, + remote_user = RemoteUser} -> + mnesia:dirty_delete_object( + #matrix_direct{local_remote = {{LocalUserJID#jid.luser, LocalUserJID#jid.lserver}, + RemoteUser}, + room_id = Data#data.room_id}); + _ -> + ok + end, + ?INFO_MSG("terminated ~p: ~p", [Data#data.room_id, Reason]), + ok. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Convert process state when code is changed +%% @end +%%-------------------------------------------------------------------- +-spec code_change( + OldVsn :: term() | {down,term()}, + State :: term(), Data :: term(), Extra :: term()) -> + {ok, NewState :: term(), NewData :: term()}. +code_change(_OldVsn, State, Data, _Extra) -> + {ok, State, Data}. + +callback_mode() -> + handle_event_function. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== + +get_event_exn(EventID, Data) -> + maps:get(EventID, Data#data.events). + +process_send_join_res(MatrixServer, SendJoinRes, RoomVersion, Data) -> + case SendJoinRes of + {ok, {{_, 200, _}, _Headers, Body}} -> + try + case misc:json_decode(Body) of + #{<<"auth_chain">> := JSONAuthChain, + <<"event">> := JSONEvent, + <<"state">> := JSONState} = JSON when is_list(JSONAuthChain), + is_list(JSONState) -> + AuthChain = + lists:map(fun(J) -> json_to_event(J, RoomVersion) end, + JSONAuthChain), + State = + lists:map(fun(J) -> json_to_event(J, RoomVersion) end, + JSONState), + Event = json_to_event(JSONEvent, RoomVersion), + ?DEBUG("send_join res: ~p~n", [JSON]), + case Data#data.kind of + #multi{} -> + %% TODO: do check_event_sig_and_hash, but faster + ok; + _ -> + lists:foreach( + fun(E) -> + ?DEBUG("send_join res check ~p~n", [E]), + case check_event_sig_and_hash(Data#data.host, E) of + {ok, _} -> ok; + {error, Error} -> error(Error) + end + end, [Event] ++ AuthChain ++ State) + end, + CreateEvents = + lists:filter( + fun(#event{type = ?ROOM_CREATE, + state_key = <<"">>}) -> true; + (_) -> false + end, State), + RoomVersionID = RoomVersion#room_version.id, + case CreateEvents of + [#event{ + id = CreateEventID, + json = + #{<<"content">> := + #{<<"room_version">> := + RoomVersionID}}} = + CreateEvent] -> + ?DEBUG("create event: ~p~n", [CreateEvent]), + AuthCreateEvents = + lists:filtermap( + fun(#event{id = ID, + type = ?ROOM_CREATE, + state_key = <<"">>}) -> + {true, ID}; + (_) -> false + end, AuthChain), + case AuthCreateEvents of + [CreateEventID] -> + Data2 = process_send_join_res2( + MatrixServer, AuthChain, Event, State, + Data), + {keep_state, Data2, []}; + _ -> + ?DEBUG("bad auth create events: ~p, expected: ~p", [AuthCreateEvents, [CreateEventID]]), + {keep_state, Data, []} + end; + _ -> + ?DEBUG("bad create event: ~p", [CreateEvents]), + {stop, normal, Data} + end + end + catch + error:{invalid_signature, EventID} -> + ?INFO_MSG("failed signature check on event ~p", [EventID]), + {stop, normal, Data}; + Class:Reason:ST -> + ?INFO_MSG("failed send_join: ~p", [{Class, Reason, ST}]), + {stop, normal, Data} + end; + _ -> + ?DEBUG("failed send_join: ~p", [SendJoinRes]), + {stop, normal, Data} + end. + +process_send_join_res2(MatrixServer, AuthChain, Event, State, Data) -> + Data2 = do_auth_and_store_external_events(AuthChain ++ State, Data), + StateMap = + lists:foldl( + fun(E, Acc) -> + Acc#{{E#event.type, E#event.state_key} => E#event.id} + end, #{}, State), + StateMap2 = + case Event#event.state_key of + undefined -> + StateMap; + _ -> + StateMap#{{Event#event.type, Event#event.state_key} => Event#event.id} + end, + Event2 = Event#event{state_map = StateMap2}, + Data3 = + case check_event_auth(Event2, Data2) of + true -> + store_event(Event2, Data2); + false -> + error({event_auth_error, Event2#event.id}) + end, + MissingEventsQuery = + #{<<"earliest_events">> => [], + <<"latest_events">> => [Event#event.id], + <<"limit">> => 10}, + Host = Data3#data.host, + Pid = self(), + RoomID = Data3#data.room_id, + RoomVersion = Data3#data.room_version, + mod_matrix_gw:send_request( + Host, post, MatrixServer, + [<<"_matrix">>, <<"federation">>, <<"v1">>, + <<"get_missing_events">>, RoomID], + [], + MissingEventsQuery, + [{timeout, 60000}], + [{sync, false}, + {body_format, binary}, + {receiver, + fun({_, Res}) -> + spawn(fun() -> + process_missing_events_res( + Host, MatrixServer, Pid, RoomID, RoomVersion, + {ok, Res}) + end) + end}]), + Data3. + +do_auth_and_store_external_events(EventList, Data) -> + Events = maps:from_list(lists:map(fun(E) -> {E#event.id, E} end, + EventList)), + SortedEvents = simple_toposort(Events), + ?DEBUG("topo ~p~n", [SortedEvents]), + %% TODO: add more checks + Data2 = + lists:foldl( + fun(E, Acc) -> + Ev = maps:get(E, Events), + case check_event_auth(Ev, Acc) of + true -> + store_event(Ev, Acc); + false -> + error({event_auth_error, E}) + end + end, Data, SortedEvents), + Data2. + +auth_and_store_external_events(Pid, EventList) -> + gen_statem:call(Pid, {auth_and_store_external_events, EventList}). + +statemap_find(Key, StateMap, Data) -> + case maps:find(Key, StateMap) of + {ok, #event{}} = Res -> + Res; + {ok, EventID} when is_binary(EventID) -> + maps:find(EventID, Data#data.events); + error -> + error + end. + +check_event_auth(Event, Data) -> + StateMap = + maps:from_list( + lists:map( + fun(EID) -> + E = get_event_exn(EID, Data), + {{E#event.type, E#event.state_key}, E} + end, Event#event.auth_events)), + check_event_auth(Event, StateMap, Data). + +check_event_auth(Event, StateMap, Data) -> + RoomVersion = Data#data.room_version, + case Event#event.type of + ?ROOM_CREATE -> + case {maps:size(StateMap), Event#event.prev_events} of + {0, []} -> + Check12 = + case RoomVersion#room_version.hydra of + true -> + case Event#event.json of + #{<<"room_id">> := _} -> + false; + _ -> + true + end; + false -> + RDomain = mod_matrix_gw:get_id_domain_exn(Data#data.room_id), + SDomain = mod_matrix_gw:get_id_domain_exn(Event#event.sender), + RDomain == SDomain + end, + if + Check12 -> + %% TODO: check content.room_version + case RoomVersion#room_version.implicit_room_creator of + false -> + case Event#event.json of + #{<<"content">> := + #{<<"creator">> := _}} -> + true; + _ -> + false + end; + true -> + case RoomVersion#room_version.hydra of + true -> + case Event#event.json of + #{<<"content">> := + #{<<"additional_creators">> := Creators}} when is_list(Creators) -> + lists:foreach( + fun(C) -> + case check_user_id(C) of + true -> ok; + false -> error(not_allowed) + end + end, Creators), + true; + #{<<"content">> := + #{<<"additional_creators">> := _}} -> + false; + _ -> + true + end; + false -> + true + end + end; + true -> + false + end; + _ -> + false + end; + _ -> + case StateMap of + #{{?ROOM_CREATE, <<"">>} := _} -> + case Event#event.type of + ?ROOM_MEMBER -> + case Event#event.json of + #{<<"content">> := + #{<<"membership">> := Membership}} -> + %% TODO: join_authorised_via_users_server + case Membership of + <<"join">> -> + check_event_auth_join( + Event, StateMap, Data); + <<"invite">> -> + check_event_auth_invite( + Event, StateMap, Data); + <<"leave">> -> + check_event_auth_leave( + Event, StateMap, Data); + <<"ban">> -> + check_event_auth_ban( + Event, StateMap, Data); + <<"knock">> when (Data#data.room_version)#room_version.knock_join_rule -> + check_event_auth_knock( + Event, StateMap, Data); + _ -> + false + end; + _ -> + false + end; + ?ROOM_ALIASES when (Data#data.room_version)#room_version.special_case_aliases_auth -> + case Event#event.state_key of + undefined -> + false; + StateKey -> + case mod_matrix_gw:get_id_domain_exn(Event#event.sender) of + StateKey -> + true; + _ -> + false + end + end; + _ -> + Sender = Event#event.sender, + case maps:find({?ROOM_MEMBER, Sender}, StateMap) of + {ok, #event{ + json = #{<<"content">> := + #{<<"membership">> := + <<"join">>}}}} -> + case Event#event.type of + ?ROOM_3PI -> + SenderLevel = get_user_power_level(Event#event.sender, StateMap, Data), + InviteLevel = + case maps:find({?ROOM_POWER_LEVELS, <<"">>}, StateMap) of + {ok, #event{json = #{<<"content">> := #{<<"invite">> := S}}}} -> + get_int(S); + _ -> 0 + end, + SenderLevel >= InviteLevel; + _ -> + case check_event_power_level( + Event, StateMap, Data) of + true -> + case Event#event.type of + ?ROOM_POWER_LEVELS -> + check_event_auth_power_levels( + Event, StateMap, Data); + _ -> + true + end; + false -> + false + end + end; + _ -> + false + end + end; + _ -> + false + end + end. + +check_event_auth_join(Event, StateMap, Data) -> + RoomVersion = Data#data.room_version, + StateKey = Event#event.state_key, + case {length(Event#event.auth_events), + RoomVersion#room_version.implicit_room_creator, + maps:get({?ROOM_CREATE, <<"">>}, StateMap, undefined)} of + {1, false, #event{json = #{<<"content">> := #{<<"creator">> := StateKey}}}} -> + ?DEBUG("creator join ~p~n", [Event]), + true; + {1, true, #event{sender = StateKey}} -> + ?DEBUG("creator join ~p~n", [Event]), + true; + _ -> + case Event#event.sender of + StateKey -> + JoinRule = + case maps:find({?ROOM_JOIN_RULES, <<"">>}, StateMap) of + {ok, #event{ + json = #{<<"content">> := + #{<<"join_rule">> := JR}}}} -> + JR; + _ -> + <<"invite">> + end, + case maps:find({?ROOM_MEMBER, StateKey}, StateMap) of + {ok, #event{ + json = #{<<"content">> := + #{<<"membership">> := + <<"ban">>}}}} -> + false; + {ok, #event{ + json = #{<<"content">> := + #{<<"membership">> := + <<"join">>}}}} -> + true; + {ok, #event{ + json = #{<<"content">> := + #{<<"membership">> := + SenderMembership}}}} -> + case {JoinRule, SenderMembership} of + {<<"public">>, _} -> true; + {<<"invite">>, <<"invite">>} -> true; + {<<"knock">>, <<"invite">>} + when (Data#data.room_version)#room_version.knock_join_rule -> + true; + {<<"restricted">>, <<"invite">>} + when (Data#data.room_version)#room_version.restricted_join_rule -> + %% TODO + true; + {<<"knock_restricted">>, <<"invite">>} + when (Data#data.room_version)#room_version.knock_restricted_join_rule -> + %% TODO + true; + _ -> false + end; + error -> + case JoinRule of + <<"public">> -> true; + _ -> false + end + end; + _ -> + false + end + end. + +check_event_auth_invite(Event, StateMap, Data) -> + StateKey = Event#event.state_key, + case Event#event.json of + #{<<"content">> := #{<<"third_party_invite">> := _}} -> + %% TODO + {todo, Event}; + _ -> + case maps:find({?ROOM_MEMBER, Event#event.sender}, StateMap) of + {ok, #event{ + json = #{<<"content">> := + #{<<"membership">> := + <<"join">>}}}} -> + case maps:find({?ROOM_MEMBER, StateKey}, StateMap) of + {ok, #event{ + json = #{<<"content">> := + #{<<"membership">> := + <<"ban">>}}}} -> + false; + {ok, #event{ + json = #{<<"content">> := + #{<<"membership">> := + <<"join">>}}}} -> + false; + _ -> + UserLevel = get_user_power_level(Event#event.sender, StateMap, Data), + InviteLevel = + case maps:find({?ROOM_POWER_LEVELS, <<"">>}, StateMap) of + {ok, #event{json = #{<<"content">> := #{<<"invite">> := S}}}} -> + get_int(S); + _ -> 0 + end, + UserLevel >= InviteLevel + end; + _ -> + false + end + end. + +check_event_auth_leave(Event, StateMap, Data) -> + StateKey = Event#event.state_key, + case maps:find({?ROOM_MEMBER, Event#event.sender}, StateMap) of + {ok, #event{ + json = #{<<"content">> := + #{<<"membership">> := SenderMembership}}}} -> + case Event#event.sender of + StateKey -> + case SenderMembership of + <<"invite">> -> true; + <<"join">> -> true; + <<"knock">> when (Data#data.room_version)#room_version.knock_join_rule -> true; + _ -> false + end; + _ -> + case SenderMembership of + <<"join">> -> + SenderLevel = get_user_power_level(Event#event.sender, StateMap, Data), + CheckBan = + case maps:find({?ROOM_MEMBER, StateKey}, StateMap) of + {ok, #event{ + json = #{<<"content">> := + #{<<"membership">> := + <<"ban">>}}}} -> + BanLevel = + case maps:find({?ROOM_POWER_LEVELS, <<"">>}, StateMap) of + {ok, #event{json = #{<<"content">> := #{<<"ban">> := S}}}} -> + get_int(S); + _ -> 50 + end, + SenderLevel >= BanLevel; + _ -> + true + end, + if + CheckBan -> + KickLevel = + case maps:find({?ROOM_POWER_LEVELS, <<"">>}, StateMap) of + {ok, #event{json = #{<<"content">> := #{<<"kick">> := S1}}}} -> + get_int(S1); + _ -> 50 + end, + TargetLevel = get_user_power_level(StateKey, StateMap, Data), + SenderLevel >= KickLevel andalso SenderLevel > TargetLevel; + true -> + false + end; + _ -> + false + end + end; + _ -> + false + end. + +check_event_auth_ban(Event, StateMap, Data) -> + StateKey = Event#event.state_key, + case maps:find({?ROOM_MEMBER, Event#event.sender}, StateMap) of + {ok, #event{ + json = #{<<"content">> := + #{<<"membership">> := SenderMembership}}}} -> + case SenderMembership of + <<"join">> -> + SenderLevel = get_user_power_level(Event#event.sender, StateMap, Data), + BanLevel = + case maps:find({?ROOM_POWER_LEVELS, <<"">>}, StateMap) of + {ok, #event{json = #{<<"content">> := #{<<"ban">> := S}}}} -> + get_int(S); + _ -> 50 + end, + TargetLevel = get_user_power_level(StateKey, StateMap, Data), + SenderLevel >= BanLevel andalso SenderLevel > TargetLevel; + _ -> + false + end; + _ -> + false + end. + +check_event_auth_knock(Event, StateMap, Data) -> + StateKey = Event#event.state_key, + case Event#event.sender of + StateKey -> + JoinRule = + case maps:find({?ROOM_JOIN_RULES, <<"">>}, StateMap) of + {ok, #event{ + json = #{<<"content">> := + #{<<"join_rule">> := JR}}}} -> + JR; + _ -> + <<"invite">> + end, + IsKnock = + case JoinRule of + <<"knock">> -> + true; + <<"knock_restricted">> when (Data#data.room_version)#room_version.knock_restricted_join_rule -> + true; + _ -> + false + end, + case IsKnock of + true -> + case maps:find({?ROOM_MEMBER, StateKey}, StateMap) of + {ok, #event{ + json = #{<<"content">> := + #{<<"membership">> := + <<"ban">>}}}} -> + false; + {ok, #event{ + json = #{<<"content">> := + #{<<"membership">> := + <<"join">>}}}} -> + false; + _ -> + true + end; + false -> + false + end; + _ -> + false + end. + +check_event_power_level(Event, StateMap, Data) -> + PLContent = + case maps:find({?ROOM_POWER_LEVELS, <<"">>}, StateMap) of + {ok, #event{json = #{<<"content">> := C}}} -> C; + _ -> #{} + end, + RequiredLevel = get_event_power_level( + Event#event.type, Event#event.state_key, PLContent), + UserLevel = get_user_power_level(Event#event.sender, StateMap, Data), + if + UserLevel >= RequiredLevel -> + Sender = Event#event.sender, + case Event#event.state_key of + Sender -> true; + <<$@, _/binary>> -> false; + _ -> true + end; + true -> + false + end. + +get_event_power_level(Type, StateKey, PL) -> + case {StateKey, PL} of + {_, #{<<"events">> := #{Type := Level}}} -> + get_int(Level); + {undefined, #{<<"events_default">> := Level}} -> + get_int(Level); + {undefined, _} -> + 0; + {StateKey, #{<<"state_default">> := Level}} when is_binary(StateKey) -> + get_int(Level); + {StateKey, _} when is_binary(StateKey) -> + 50 + end. + +get_user_power_level(User, StateMap, Data) -> + RoomVersion = Data#data.room_version, + PL = + case statemap_find({?ROOM_POWER_LEVELS, <<"">>}, StateMap, Data) of + {ok, #event{json = #{<<"content">> := C}}} -> C; + _ -> #{} + end, + IsCreator = + case {RoomVersion#room_version.hydra, + RoomVersion#room_version.implicit_room_creator, + statemap_find({?ROOM_CREATE, <<"">>}, StateMap, Data)} of + {false, false, + {ok, #event{json = #{<<"content">> := #{<<"creator">> := User}}}}} -> + true; + {false, true, {ok, #event{sender = User}}} -> + true; + {true, _, {ok, #event{sender = User}}} -> + true; + {true, _, + {ok, #event{ + json = #{<<"content">> := + #{<<"additional_creators">> := Creators}}}}} + when is_list(Creators) -> + lists:member(User, Creators); + _ -> + false + end, + case {RoomVersion#room_version.hydra, + IsCreator, + PL} of + {true, true, _} -> + ?CREATOR_PL; + {_, _, #{<<"users">> := #{User := Level}}} -> get_int(Level); + {_, _, #{<<"users_default">> := Level}} -> get_int(Level); + {_, true, _} -> + 100; + _ -> + 0 + end. + +check_event_auth_power_levels(Event, StateMap, Data) -> + try + case Event#event.json of + #{<<"content">> := NewPL = #{<<"users">> := Users}} when is_map(Users) -> + case (Data#data.room_version)#room_version.hydra of + false -> + ok; + true -> + case statemap_find({?ROOM_CREATE, <<"">>}, StateMap, Data) of + {ok, #event{sender = C} = E} -> + Creators = + case E#event.json of + #{<<"content">> := + #{<<"additional_creators">> := ACs}} -> + [C | ACs]; + _ -> + [C] + end, + case maps:size(maps:with(Creators, Users)) > 0 of + true -> + error(creators_in_pl); + false -> + ok + end; + _ -> + error(missed_create_event) + end + end, + CheckKeys = + case (Data#data.room_version)#room_version.limit_notifications_power_levels of + false -> + [<<"events">>, <<"users">>]; + true -> + [<<"events">>, <<"users">>, <<"notifications">>] + end, + case (Data#data.room_version)#room_version.enforce_int_power_levels of + true -> + lists:foreach( + fun(Field) -> + case NewPL of + #{Field := V} when is_integer(V) -> ok; + #{Field := _V} -> error(not_allowed); + _ -> ok + end + end, + [<<"users_default">>, <<"events_default">>, <<"state_default">>, + <<"ban">>, <<"redact">>, <<"kick">>, <<"invite">>]), + lists:foreach( + fun(Key) -> + NewMap = maps:get(Key, NewPL, #{}), + maps:fold( + fun(_Field, V, _) -> + if + is_integer(V) -> ok; + true -> error(not_allowed) + end + end, [], NewMap) + end, + CheckKeys); + false -> + ok + end, + maps:fold( + fun(K, _V, _) -> + case check_user_id(K) of + true -> ok; + false -> error(not_allowed) + end + end, ok, Users), + StateKey = Event#event.state_key, + case StateMap of + #{{?ROOM_POWER_LEVELS, StateKey} := + #event{json = #{<<"content">> := OldPL}}} -> + UserLevel = get_user_power_level(Event#event.sender, StateMap, Data), + lists:foreach( + fun(Field) -> + case check_event_auth_power_levels_aux( + Field, OldPL, NewPL, UserLevel, none) of + true -> ok; + false -> error(not_allowed) + end + end, + [<<"users_default">>, <<"events_default">>, <<"state_default">>, + <<"ban">>, <<"redact">>, <<"kick">>, <<"invite">>]), + lists:foreach( + fun(Key) -> + OldMap = maps:get(Key, OldPL, #{}), + NewMap = maps:get(Key, NewPL, #{}), + UserID = + case Key of + <<"users">> -> + {some, Event#event.sender}; + _ -> none + end, + maps:fold( + fun(Field, _, _) -> + case check_event_auth_power_levels_aux( + Field, OldMap, NewMap, UserLevel, UserID) of + true -> ok; + false -> error(not_allowed) + end + end, [], maps:merge(OldMap, NewMap)) + end, + CheckKeys), + true; + _ -> + true + end; + _ -> + false + end + catch + error:not_allowed -> + false + end. + +check_event_auth_power_levels_aux(Field, OldDict, NewDict, UserLevel, UserID) -> + UserLevel2 = + case UserID of + none -> UserLevel; + {some, Field} -> UserLevel; + {some, _} -> UserLevel - 1 + end, + case {maps:find(Field, OldDict), maps:find(Field, NewDict)} of + {error, error} -> true; + {error, {ok, S}} -> + get_int(S) =< UserLevel; + {{ok, S}, error} -> + get_int(S) =< UserLevel2; + {{ok, S1}, {ok, S2}} -> + OldLevel = get_int(S1), + NewLevel = get_int(S2), + if + OldLevel == NewLevel -> true; + true -> + OldLevel =< UserLevel2 andalso NewLevel =< UserLevel + end + end. + +check_user_id(S) -> + case S of + <<$@, Parts/binary>> -> + case binary:split(Parts, <<":">>) of + [_, _] -> true; + _ -> false + end; + _ -> + false + end. + +parse_user_id(Str) -> + case Str of + <<$@, Parts/binary>> -> + case binary:split(Parts, <<":">>) of + [U, S] -> {ok, U, S}; + _ -> error + end; + _ -> + error + end. + +get_int(I) when is_integer(I) -> I; +get_int(S) when is_binary(S) -> binary_to_integer(S). + +fill_event(JSON, Data) -> + Host = Data#data.host, + MatrixServer = mod_matrix_gw_opt:matrix_domain(Host), + PrevEvents = sets:to_list(Data#data.latest_events), + Depth = + lists:max( + [0 | lists:map( + fun(EID) -> + (maps:get(EID, Data#data.events))#event.depth + end, PrevEvents)]), + Depth2 = min(Depth + 1, ?MAX_DEPTH), + ?DEBUG("fill ~p", [{PrevEvents, Data#data.events}]), + StateMaps = + lists:map( + fun(EID) -> + case Data#data.events of + #{EID := #event{state_map = undefined}} -> + error({missed_state_map, EID}); + #{EID := #event{state_map = SM}} -> + SM; + _ -> + error({missed_prev_event, EID}) + end + end, PrevEvents), + StateMap = resolve_state_maps(StateMaps, Data), + AuthEvents = + lists:usort( + lists:flatmap( + fun(Key) -> + case StateMap of + #{Key := E} -> [E]; + _ -> [] + end + end, + compute_event_auth_keys(JSON, Data#data.room_version))), + ?DEBUG("auth_events ~p", [{AuthEvents, compute_event_auth_keys(JSON, Data#data.room_version)}]), + {JSON#{<<"auth_events">> => AuthEvents, + <<"depth">> => Depth2, + <<"origin">> => MatrixServer, + <<"origin_server_ts">> => erlang:system_time(millisecond), + <<"prev_events">> => PrevEvents, + <<"room_id">> => Data#data.room_id}, + StateMap}. + +add_event(JSON, Data) -> + Host = Data#data.host, + {Msg, StateMap} = fill_event(JSON, Data), + CHash = mod_matrix_gw:content_hash(Msg), + Msg2 = + Msg#{<<"hashes">> => + #{<<"sha256">> => + mod_matrix_gw:base64_encode(CHash)}}, + Msg3 = mod_matrix_gw:sign_event(Host, Msg2, Data#data.room_version), + Event = json_to_event(Msg3, Data#data.room_version), + StateMap2 = + case Event#event.state_key of + undefined -> + StateMap; + _ -> + StateMap#{{Event#event.type, Event#event.state_key} => Event#event.id} + end, + Event2 = Event#event{state_map = StateMap2}, + ?DEBUG("add_event ~p~n", [Event2]), + case check_event_auth(Event2, Data) of + true -> + %%TODO: soft fail + {store_event(Event2, Data), Event2}; + false -> + error({event_auth_error, Event2#event.id}) + end. + + +store_event(Event, Data) -> + %% TODO + Events = Data#data.events, + case maps:find(Event#event.id, Events) of + {ok, #event{state_map = undefined}} when Event#event.state_map /= undefined -> + Data#data{events = Events#{Event#event.id => Event}}; + {ok, _} -> + Data; + error -> + ?DEBUG("store ~p~n", [Event#event.id]), + Data2 = notify_event(Event, Data), + {LatestEvents, NonLatestEvents} = + case Event of + #event{state_map = undefined} -> + {Data2#data.latest_events, Data2#data.nonlatest_events}; + _ -> + SeenEvents = Event#event.prev_events ++ Event#event.auth_events, + LatestEs = + lists:foldl(fun(E, Acc) -> sets:del_element(E, Acc) end, Data2#data.latest_events, + SeenEvents), + NonLatestEs = + lists:foldl(fun(E, Acc) -> sets:add_element(E, Acc) end, Data2#data.nonlatest_events, + SeenEvents), + LatestEs2 = + case maps:is_key(Event#event.id, NonLatestEs) of + true -> + LatestEs; + false -> + LatestEs#{Event#event.id => []} + end, + %%?DEBUG("latest ~p~n", [{LatestEvents2, NonLatestEvents}]), + {LatestEs2, NonLatestEs} + end, + EventQueue = + treap:insert( + Event#event.id, + {erlang:monotonic_time(micro_seconds), + erlang:unique_integer([monotonic])}, + [], Data2#data.event_queue), + Data2#data{events = Events#{Event#event.id => Event}, + latest_events = LatestEvents, + nonlatest_events = NonLatestEvents, + event_queue = EventQueue} + end. + +simple_toposort(Events) -> + {Res, _Used} = + lists:foldl( + fun(E, {_Res, Used} = Acc) -> + EventID = E#event.id, + case maps:is_key(EventID, Used) of + false -> + simple_toposort_dfs(EventID, Acc, Events); + true -> + Acc + end + end, {[], #{}}, maps:values(Events)), + lists:reverse(Res). + +simple_toposort_dfs(EventID, {Res, Used}, Events) -> + case maps:find(EventID, Events) of + error -> + %error({unknown_event, EventID}); + {Res, Used}; + {ok, Event} -> + Used2 = Used#{EventID => gray}, + {Res8, Used8} = + lists:foldl( + fun(ID, {_Res3, Used3} = Acc) -> + case maps:get(ID, Used3, white) of + white -> + simple_toposort_dfs(ID, Acc, Events); + gray -> + error(loop_in_auth_chain); + black -> + Acc + end + end, {Res, Used2}, Event#event.auth_events), + Used9 = Used8#{EventID => black}, + Res9 = [EventID | Res8], + {Res9, Used9} + end. + +check_event_sig_and_hash(Host, Event) -> + case check_event_signature(Host, Event) of + true -> + case check_event_content_hash(Event) of + true -> + {ok, Event}; + false -> + ?DEBUG("mismatched content hash: ~p", [Event#event.id]), + PrunedJSON = mod_matrix_gw:prune_event( + Event#event.json, Event#event.room_version), + {ok, Event#event{json = PrunedJSON}} + end; + false -> + {error, {invalid_signature, Event#event.id}} + end. + +get_room_version(Pid) -> + gen_statem:call(Pid, get_room_version). + +partition_missed_events(Pid, EventIDs) -> + gen_statem:call(Pid, {partition_missed_events, EventIDs}). + +partition_events_with_statemap(Pid, EventIDs) -> + gen_statem:call(Pid, {partition_events_with_statemap, EventIDs}). + +get_latest_events(Pid) -> + gen_statem:call(Pid, get_latest_events). + +check_event_signature(Host, Event) -> + PrunedEvent = mod_matrix_gw:prune_event(Event#event.json, + Event#event.room_version), + mod_matrix_gw_s2s:check_signature(Host, PrunedEvent, + Event#event.room_version). + +find_event(Pid, EventID) -> + gen_statem:call(Pid, {find_event, EventID}). + +resolve_auth_store_event(Pid, Event) -> + gen_statem:call(Pid, {resolve_auth_store_event, Event}). + +process_pdu(Host, Origin, PDU) -> + %% TODO: error handling + #{<<"room_id">> := RoomID} = PDU, + case get_existing_room_pid(Host, RoomID) of + {ok, Pid} -> + RoomVersion = get_room_version(Pid), + Event = json_to_event(PDU, RoomVersion), + case check_event_signature(Host, Event) of + true -> + ?DEBUG("process pdu: ~p~n", [PDU]), + {SeenEvents, MissedEvents} = + partition_missed_events(Pid, Event#event.prev_events), + ?DEBUG("seen/missed: ~p~n", [{SeenEvents, MissedEvents}]), + case MissedEvents of + [] -> + ok; + _ -> + LatestEvents = get_latest_events(Pid), + EarliestEvents = + lists:foldl( + fun(E, Acc) -> + Acc#{E => []} + end, LatestEvents, SeenEvents), + ?DEBUG("earliest ~p~n", [EarliestEvents]), + MissingEventsQuery = + #{<<"earliest_events">> => maps:keys(EarliestEvents), + <<"latest_events">> => [Event#event.id], + <<"limit">> => 10}, + MissingEventsRes = + mod_matrix_gw:send_request( + Host, post, Origin, + [<<"_matrix">>, <<"federation">>, <<"v1">>, + <<"get_missing_events">>, RoomID], + [], + MissingEventsQuery, + [{timeout, 60000}], + [{sync, true}, + {body_format, binary}]), + ?DEBUG("missing res ~p~n", [MissingEventsRes]), + process_missing_events_res(Host, Origin, Pid, RoomID, RoomVersion, + MissingEventsRes), + ok + end, + resolve_auth_store_event(Pid, Event), + {ok, Event#event.id}; + false -> + {error, <<"Signature check failed">>} + end; + {error, not_found} -> + {error, <<"Room doesn't exist">>} + end. + +process_missing_events_res(Host, Origin, Pid, RoomID, RoomVersion, + {ok, {{_, 200, _}, _Headers, Body}}) -> + try + case misc:json_decode(Body) of + #{<<"events">> := JSONEvents} when is_list(JSONEvents) -> + process_missing_events(Host, Origin, Pid, RoomID, RoomVersion, JSONEvents) + end + catch + Class:Reason:ST -> + ?DEBUG("failed process_missing_events_res: ~p", [{Class, Reason, ST}]), + ok + end; +process_missing_events_res(_Host, _Origin, _Pid, _RoomID, _RoomVersion, _) -> + ok. + +process_missing_events(Host, Origin, Pid, RoomID, RoomVersion, JSONEvents) -> + Events = lists:map(fun(J) -> json_to_event(J, RoomVersion) end, JSONEvents), + SortedEvents = lists:keysort(#event.depth, Events), + ?DEBUG("sevents ~p~n", [SortedEvents]), + lists:foreach( + fun(Event) -> + case check_event_sig_and_hash(Host, Event) of + {ok, _} -> + ShouldProcess = + case find_event(Pid, Event#event.id) of + {ok, #event{state_map = undefined}} -> + true; + {ok, _} -> + false; + error -> + true + end, + case ShouldProcess of + true -> + fetch_prev_statemaps(Host, Origin, Pid, + RoomID, RoomVersion, Event), + resolve_auth_store_event(Pid, Event), + ok; + false -> + ok + end; + {error, Reason} -> + error(Reason) + end + end, SortedEvents), + ok. + +fetch_prev_statemaps(Host, Origin, Pid, RoomID, RoomVersion, Event) -> + ?DEBUG("fetch_prev_statemaps ~p~n", [Event#event.id]), + {SeenEvents, MissedEvents} = + partition_events_with_statemap(Pid, Event#event.prev_events), + ?DEBUG("s/m ~p~n", [{SeenEvents, MissedEvents}]), + lists:foreach( + fun(MissedEventID) -> + case request_event(Host, Origin, Pid, RoomID, RoomVersion, MissedEventID) of + {ok, MissedEvent} -> + case request_room_state(Host, Origin, Pid, RoomID, RoomVersion, MissedEvent) of + {ok, AuthChain, State} -> + auth_and_store_external_events(Pid, AuthChain ++ State), + StateMap = + lists:foldl( + fun(E, Acc) -> + Acc#{{E#event.type, E#event.state_key} => E#event.id} + end, #{}, State), + auth_and_store_external_events( + Pid, [MissedEvent#event{state_map = StateMap}]), + ok; + {error, Reason} -> + ?INFO_MSG("failed request_room_state: ~p", [{RoomID, Event#event.id, Reason}]), + ok + end; + {error, Error} -> + error(Error) + end + end, MissedEvents). + +request_room_state(Host, Origin, _Pid, RoomID, RoomVersion, Event) -> + Res = + mod_matrix_gw:send_request( + Host, get, Origin, + [<<"_matrix">>, <<"federation">>, + <<"v1">>, <<"state">>, + RoomID], + [{<<"event_id">>, Event#event.id}], + none, + [{connect_timeout, 5000}, + {timeout, 60000}], + [{sync, true}, + {body_format, binary}]), + case Res of + {ok, {{_, 200, _}, _Headers, Body}} -> + try + case misc:json_decode(Body) of + #{<<"auth_chain">> := JSONAuthChain, + <<"pdus">> := JSONState} = _JSON when is_list(JSONAuthChain), + is_list(JSONState) -> + AuthChain = + lists:map(fun(J) -> json_to_event(J, RoomVersion) end, + JSONAuthChain), + State = + lists:map(fun(J) -> json_to_event(J, RoomVersion) end, + JSONState), + lists:foreach( + fun(E) -> + case check_event_sig_and_hash(Host, E) of + {ok, _} -> + case E#event.room_id of + RoomID -> + case E#event.state_key of + undefined -> + error({missed_state_key, E#event.id}); + _ -> + ok + end; + RoomID2 -> + error({mismatched_room_id, E#event.id, RoomID, RoomID2}) + end; + {error, Error} -> error(Error) + end + end, AuthChain ++ State), + ?DEBUG("req state ~p~n", + [{[E#event.id || E <- AuthChain], + [E#event.id || E <- State]}]), + {ok, AuthChain, State} + end + catch + Class:Reason:ST -> + ?INFO_MSG("failed request_room_state: ~p", [{Class, Reason, ST}]), + {error, Reason} + end; + {ok, {{_, _Status, Reason}, _Headers, _Body}} -> + {error, Reason}; + {error, Reason} -> + {error, Reason} + end. + +request_event(Host, Origin, _Pid, RoomID, RoomVersion, EventID) -> + Res = + mod_matrix_gw:send_request( + Host, get, Origin, + [<<"_matrix">>, <<"federation">>, + <<"v1">>, <<"event">>, + EventID], + [], + none, + [{timeout, 5000}], + [{sync, true}, + {body_format, binary}]), + case Res of + {ok, {{_, 200, _}, _Headers, Body}} -> + try + case misc:json_decode(Body) of + #{<<"pdus">> := [PDU]} -> + Event = json_to_event(PDU, RoomVersion), + case check_event_sig_and_hash(Host, Event) of + {ok, _} -> + case Event#event.room_id of + RoomID -> + ok; + RoomID2 -> + error({mismatched_room_id, Event#event.id, RoomID, RoomID2}) + end; + {error, Error} -> error(Error) + end, + {ok, Event} + end + catch + Class:Reason:ST -> + ?INFO_MSG("failed request_event: ~p", [{Class, Reason, ST}]), + {error, Reason} + end; + {ok, {{_, _Status, Reason}, _Headers, _Body}} -> + {error, Reason}; + {error, Reason} -> + {error, Reason} + end. + +get_event_prev_state_map(Event, Data) -> + StateMaps = + lists:map( + fun(EID) -> + case Data#data.events of + #{EID := #event{state_map = undefined}} -> + error({missed_state_map, EID}); + #{EID := #event{state_map = SM}} -> + SM; + _ -> + error({missed_prev_event, EID}) + end + end, Event#event.prev_events), + resolve_state_maps(StateMaps, Data). + +do_resolve_auth_store_event(Event, Data) -> + StateMap = get_event_prev_state_map(Event, Data), + StateMap2 = + case Event#event.state_key of + undefined -> + StateMap; + _ -> + StateMap#{{Event#event.type, Event#event.state_key} => Event#event.id} + end, + Event2 = Event#event{state_map = StateMap2}, + case check_event_auth(Event2, Data) of + true -> + %TODO: soft fail + store_event(Event2, Data); + false -> + error({event_auth_error, Event2#event.id}) + end. + +resolve_state_maps([], _Data) -> + #{}; +resolve_state_maps([StateMap], _Data) -> + StateMap; +resolve_state_maps(StateMaps, Data) -> + {Unconflicted, Conflicted0} = calculate_conflict(StateMaps), + Conflicted1 = lists:append(maps:values(Conflicted0)), + Conflicted = + case (Data#data.room_version)#room_version.hydra of + false -> + Conflicted1; + true -> + calculate_conflicted_subgraph(Conflicted1, Data) + end, + ?DEBUG("confl ~p~n", [{Unconflicted, Conflicted}]), + case Conflicted of + [] -> + Unconflicted; + _ -> + AuthDiff = calculate_auth_diff(StateMaps, Data), + ?DEBUG("auth diff ~p~n", [AuthDiff]), + FullConflictedSet = + maps:from_list([{E, []} || E <- AuthDiff ++ Conflicted]), + ?DEBUG("fcs ~p~n", [FullConflictedSet]), + %% TODO: test + PowerEvents = + lists:filter( + fun(EventID) -> + Event = maps:get(EventID, Data#data.events), + is_power_event(Event) + end, maps:keys(FullConflictedSet)), + SortedPowerEvents = lexicographic_toposort(PowerEvents, FullConflictedSet, Data), + ?DEBUG("spe ~p~n", [SortedPowerEvents]), + StateMap = + case (Data#data.room_version)#room_version.hydra of + false -> + iterative_auth_checks(SortedPowerEvents, Unconflicted, Data); + true -> + maps:merge( + Unconflicted, + iterative_auth_checks(SortedPowerEvents, #{}, Data)) + end, + PowerEventsSet = maps:from_list([{E, []} || E <- SortedPowerEvents]), + OtherEvents = lists:filter(fun(E) -> not maps:is_key(E, PowerEventsSet) end, + maps:keys(FullConflictedSet)), + PLID = maps:get({?ROOM_POWER_LEVELS, <<"">>}, StateMap, undefined), + SortedOtherEvents = mainline_sort(OtherEvents, PLID, Data), + ?DEBUG("mainline ~p~n", [SortedOtherEvents]), + StateMap2 = iterative_auth_checks(SortedOtherEvents, StateMap, Data), + Resolved = maps:merge(StateMap2, Unconflicted), + ?DEBUG("resolved ~p~n", [Resolved]), + Resolved + end. + +calculate_conflict(StateMaps) -> + Keys = lists:usort(lists:flatmap(fun maps:keys/1, StateMaps)), + lists:foldl( + fun(Key, {Unconflicted, Conflicted}) -> + EventIDs = + lists:usort( + lists:map(fun(StateMap) -> + maps:find(Key, StateMap) + end, StateMaps)), + case EventIDs of + [{ok, EventID}] -> + {Unconflicted#{Key => EventID}, Conflicted}; + _ -> + EventIDs2 = + lists:flatmap( + fun(error) -> []; + ({ok, EventID}) -> [EventID] + end, EventIDs), + {Unconflicted, Conflicted#{Key => EventIDs2}} + end + end, {#{}, #{}}, Keys). + +calculate_conflicted_subgraph([], _Data) -> + []; +calculate_conflicted_subgraph(Events, Data) -> + MinDepth = + lists:min( + [(maps:get(EID, Data#data.events))#event.depth || EID <- Events]), + AuthEvents = + lists:append( + [(maps:get(EID, Data#data.events))#event.auth_events || EID <- Events]), + Used0 = + maps:from_list([{E, true} || E <- Events]), + {Res, _Used} = + lists:foldl( + fun(EID, {_Res, Used} = Acc) -> + case maps:is_key(EID, Used) of + false -> + calculate_conflicted_subgraph_dfs(EID, Acc, MinDepth, Data); + true -> + Acc + end + end, {Events, Used0}, AuthEvents), + Res. + +calculate_conflicted_subgraph_dfs(EventID, {Res, Used}, MinDepth, Data) -> + case maps:find(EventID, Data#data.events) of + error -> + {Res, Used}; + {ok, Event} when Event#event.depth < MinDepth -> + {Res, Used}; + {ok, Event} -> + Used2 = Used#{EventID => gray}, + {Res8, Used8, Reachable} = + lists:foldl( + fun(_ID, {_Res3, _Used3, true} = Acc) -> + Acc; + (ID, {Res3, Used3, false}) -> + {Res4, Used4} = + case maps:get(ID, Used3, white) of + white -> + calculate_conflicted_subgraph_dfs(ID, {Res3, Used3}, MinDepth, Data); + _ -> + {Res3, Used3} + end, + case maps:get(ID, Used4, white) of + gray -> + error(loop_in_auth_chain); + true -> + {Res4, Used4, true}; + _ -> + {Res4, Used4, false} + end + end, {Res, Used2, false}, Event#event.auth_events), + Used9 = Used8#{EventID => Reachable}, + Res9 = + case Reachable of + true -> + [EventID | Res8]; + false -> + Res8 + end, + {Res9, Used9} + end. + + +%% TODO: not optimal +calculate_auth_diff(StateMaps, Data) -> + N = length(StateMaps), + Queue = + lists:foldl( + fun({K, StateMap}, Q) -> + maps:fold( + fun(_, EID, Q2) -> + Depth = (maps:get(EID, Data#data.events))#event.depth, + Set = + case gb_trees:lookup({Depth, EID}, Q2) of + none -> + 1 bsl N - 1; + {value, S} -> + S + end, + Set2 = Set band bnot (1 bsl K), + gb_trees:enter({Depth, EID}, Set2, Q2) + end, Q, StateMap) + end, gb_trees:empty(), + lists:zip(lists:seq(0, N - 1), StateMaps)), + Count = lists:sum(gb_trees:values(Queue)), + calculate_auth_diff_bfs(Queue, Count, [], Data). + +calculate_auth_diff_bfs(_Queue, 0, Res, _Data) -> + Res; +calculate_auth_diff_bfs(Queue, Count, Res, Data) -> + %?DEBUG("authdiff bfs ~p~n", [{gb_trees:to_list(Queue), Count, Res}]), + case gb_trees:is_empty(Queue) of + true -> + error(internal_error); + false -> + {{_, EventID}, Set, Queue2} = gb_trees:take_largest(Queue), + Res2 = case Set of + 0 -> Res; + _ -> [EventID | Res] + end, + Event = maps:get(EventID, Data#data.events), + calculate_auth_diff_bfs2(Event#event.auth_events, Set, Queue2, Count - Set, Res2, Data) + end. + +calculate_auth_diff_bfs2([], _Set, Queue, Count, Res, Data) -> + calculate_auth_diff_bfs(Queue, Count, Res, Data); +calculate_auth_diff_bfs2([EID | Events], Set, Queue, Count, Res, Data) -> + Event = maps:get(EID, Data#data.events), + case gb_trees:lookup({Event#event.depth, EID}, Queue) of + none -> + Queue2 = gb_trees:insert({Event#event.depth, EID}, Set, Queue), + calculate_auth_diff_bfs2(Events, Set, Queue2, Count + Set, Res, Data); + {value, Set2} -> + Set3 = Set band Set2, + Queue2 = gb_trees:enter({Event#event.depth, EID}, Set3, Queue), + calculate_auth_diff_bfs2(Events, Set, Queue2, Count - Set2 + Set3, Res, Data) + end. + +is_power_event(#event{type = ?ROOM_POWER_LEVELS, state_key = <<"">>}) -> + true; +is_power_event(#event{type = ?ROOM_JOIN_RULES, state_key = <<"">>}) -> + true; +is_power_event(#event{type = ?ROOM_MEMBER, state_key = StateKey, sender = Sender, + json = #{<<"content">> := #{<<"membership">> := <<"leave">>}}}) -> + StateKey /= Sender; +is_power_event(#event{type = ?ROOM_MEMBER, state_key = StateKey, sender = Sender, + json = #{<<"content">> := #{<<"membership">> := <<"ban">>}}}) -> + StateKey /= Sender; +is_power_event(_) -> + false. + +lexicographic_toposort(EventIDs, EventSet, Data) -> + {Used, Rev} = + lists:foldl( + fun(EventID, {Used, Rev} = Acc) -> + case maps:is_key(EventID, EventSet) of + true -> + case maps:is_key(EventID, Used) of + false -> + lexicographic_toposort_prepare(EventID, Used, Rev, EventSet, Data); + true -> + Acc + end; + false -> + Acc + end + end, {#{}, #{}}, EventIDs), + ?DEBUG("rev ~p~n", [Rev]), + OutgoingCnt = + maps:fold( + fun(EventID, _, Acc) -> + lists:foldl( + fun(EID, Acc2) -> + case maps:is_key(EID, Acc2) of + true -> + C = maps:get(EID, Acc2), + maps:put(EID, C + 1, Acc2); + false -> + Acc2 + end + end, Acc, maps:get(EventID, Rev, [])) + end, maps:map(fun(_, _) -> 0 end, Used), Used), + Current = + maps:fold( + fun(EventID, 0, Acc) -> + Event = maps:get(EventID, Data#data.events), + PowerLevel = get_sender_power_level(EventID, Data), + gb_trees:enter({-PowerLevel, Event#event.origin_server_ts, EventID}, [], Acc); + (_, _, Acc) -> + Acc + end, gb_trees:empty(), OutgoingCnt), + OutgoingCnt2 = maps:filter(fun(_, 0) -> false; (_, _) -> true end, OutgoingCnt), + lexicographic_toposort_loop(Current, OutgoingCnt2, Rev, [], Data). + +lexicographic_toposort_prepare(EventID, Used, Rev, EventSet, Data) -> + Event = maps:get(EventID, Data#data.events), + Used2 = Used#{EventID => []}, + lists:foldl( + fun(EID, {Used3, Rev3} = Acc) -> + case maps:is_key(EID, EventSet) of + true -> + Rev4 = maps:update_with( + EID, + fun(Es) -> [EventID | Es] end, [EventID], Rev3), + case maps:is_key(EID, Used3) of + false -> + lexicographic_toposort_prepare(EID, Used3, Rev4, EventSet, Data); + true -> + {Used3, Rev4} + end; + false -> + Acc + end + end, {Used2, Rev}, Event#event.auth_events). + +lexicographic_toposort_loop(Current, OutgoingCnt, Rev, Res, Data) -> + %?DEBUG("toposort ~p", [{gb_trees:to_list(Current), OutgoingCnt, Res}]), + case gb_trees:is_empty(Current) of + true -> + case maps:size(OutgoingCnt) of + 0 -> + lists:reverse(Res); + _ -> + error(loop_in_auth_chain) + end; + false -> + {{_, _, EventID}, _, Current2} = gb_trees:take_smallest(Current), + {OutgoingCnt2, Current3} = + lists:foldl( + fun(EID, {OutCnt, Cur} = Acc) -> + case maps:is_key(EID, OutCnt) of + true -> + C = maps:get(EID, OutCnt) - 1, + case C of + 0 -> + E = maps:get(EID, Data#data.events), + PowerLevel = get_sender_power_level(EID, Data), + Cur2 = gb_trees:enter({-PowerLevel, E#event.origin_server_ts, EID}, [], Cur), + {maps:remove(EID, OutCnt), Cur2}; + _ -> + {maps:put(EID, C, OutCnt), Cur} + end; + false -> + Acc + end + end, {OutgoingCnt, Current2}, maps:get(EventID, Rev, [])), + lexicographic_toposort_loop(Current3, OutgoingCnt2, Rev, [EventID | Res], Data) + end. + +get_sender_power_level(EventID, Data) -> + RoomVersion = Data#data.room_version, + Event = maps:get(EventID, Data#data.events), + PowerEventID = find_power_level_event(EventID, Data), + PowerEvent = + case PowerEventID of + undefined -> undefined; + _ -> maps:get(PowerEventID, Data#data.events) + end, + Sender = Event#event.sender, + IsCreator = is_creator(EventID, Sender, Data), + case {RoomVersion#room_version.hydra, + IsCreator, + PowerEvent} of + {true, true, _} -> + ?CREATOR_PL; + {_, true, undefined} -> + 100; + {_, false, undefined} -> + 0; + {_, _, + #event{json = #{<<"content">> := #{<<"users">> := #{Sender := Level}}}}} -> + get_int(Level); + {_, _, + #event{json = #{<<"content">> := #{<<"users_default">> := Level}}}} -> + get_int(Level); + _ -> + 0 + end. + +iterative_auth_checks(Events, StateMap, Data) -> + lists:foldl( + fun(EventID, StateMap2) -> + Event = maps:get(EventID, Data#data.events), + StateMap3 = + lists:foldl( + fun(EID, SM) -> + E = maps:get(EID, Data#data.events), + case maps:is_key({E#event.type, E#event.state_key}, SM) of + true -> + SM; + false -> + SM#{{E#event.type, E#event.state_key} => E#event.id} + end + end, StateMap2, Event#event.auth_events), + %% TODO: not optimal + StateMap4 = + maps:map(fun(_, EID) -> maps:get(EID, Data#data.events) end, StateMap3), + case check_event_auth(Event, StateMap4, Data) of + true -> + StateMap2#{{Event#event.type, Event#event.state_key} => EventID}; + false -> + StateMap2 + end + end, StateMap, Events). + +mainline_sort(OtherEvents, PLID, Data) -> + IdxMap = mainline_sort_init(PLID, -1, #{}, Data), + {OtherEvents2, _} = + lists:foldl( + fun(EventID, {Events, IMap}) -> + Event = maps:get(EventID, Data#data.events), + {Idx, IMap2} = mainline_sort_find(EventID, IMap, Data), + {[{Idx, Event#event.origin_server_ts, EventID} | Events], IMap2} + end, {[], IdxMap}, OtherEvents), + lists:map(fun({_, _, EID}) -> EID end, lists:sort(OtherEvents2)). + +mainline_sort_init(undefined, _Idx, IdxMap, _Data) -> + IdxMap; +mainline_sort_init(PLID, Idx, IdxMap, Data) when is_binary(PLID) -> + IdxMap2 = maps:put(PLID, Idx, IdxMap), + PLID2 = find_power_level_event(PLID, Data), + mainline_sort_init(PLID2, Idx - 1, IdxMap2, Data). + +mainline_sort_find(undefined, IdxMap, _Data) -> + {0, IdxMap}; +mainline_sort_find(EventID, IdxMap, Data) -> + case maps:find(EventID, IdxMap) of + {ok, Idx} -> {Idx, IdxMap}; + error -> + PLID = find_power_level_event(EventID, Data), + {Idx, IdxMap2} = mainline_sort_find(PLID, IdxMap, Data), + IdxMap3 = maps:put(EventID, Idx, IdxMap2), + {Idx, IdxMap3} + end. + +find_power_level_event(EventID, Data) -> + Event = maps:get(EventID, Data#data.events), + lists:foldl( + fun(EID, undefined) -> + E = maps:get(EID, Data#data.events), + case E of + #event{type = ?ROOM_POWER_LEVELS, state_key = <<"">>} -> EID; + _ -> undefined + end; + (_, PLID) -> + PLID + end, undefined, Event#event.auth_events). + +find_create_event(EventID, Data) -> + Event = maps:get(EventID, Data#data.events), + lists:foldl( + fun(EID, undefined) -> + E = maps:get(EID, Data#data.events), + case E of + #event{type = ?ROOM_CREATE, state_key = <<"">>} -> E; + _ -> undefined + end; + (_, Create) -> + Create + end, undefined, Event#event.auth_events). + +is_creator(EventID, User, Data) -> + case find_create_event(EventID, Data) of + undefined -> + false; + CreateEvent -> + RoomVersion = Data#data.room_version, + case {RoomVersion#room_version.hydra, + RoomVersion#room_version.implicit_room_creator, + CreateEvent} of + {false, false, + #event{type = ?ROOM_CREATE, state_key = <<"">>, + json = #{<<"content">> := + #{<<"creator">> := User}}}} -> + true; + {false, true, + #event{type = ?ROOM_CREATE, state_key = <<"">>, + sender = User}} -> + true; + {true, _, + #event{type = ?ROOM_CREATE, state_key = <<"">>, + sender = User}} -> + true; + {true, _, + #event{ + json = #{<<"content">> := + #{<<"additional_creators">> := Creators}}}} + when is_list(Creators) -> + lists:member(User, Creators); + _ -> + false + end + end. + + +binary_to_room_version(<<"4">>) -> + #room_version{id = <<"4">>, + enforce_key_validity = false, + special_case_aliases_auth = true, + strict_canonicaljson = false, + limit_notifications_power_levels = false, + knock_join_rule = false, + restricted_join_rule = false, + restricted_join_rule_fix = false, + knock_restricted_join_rule = false, + enforce_int_power_levels = false, + implicit_room_creator = false, + updated_redaction_rules = false, + hydra = false + }; +binary_to_room_version(<<"5">>) -> + #room_version{id = <<"5">>, + enforce_key_validity = true, + special_case_aliases_auth = true, + strict_canonicaljson = false, + limit_notifications_power_levels = false, + knock_join_rule = false, + restricted_join_rule = false, + restricted_join_rule_fix = false, + knock_restricted_join_rule = false, + enforce_int_power_levels = false, + implicit_room_creator = false, + updated_redaction_rules = false, + hydra = false + }; +binary_to_room_version(<<"6">>) -> + #room_version{id = <<"6">>, + enforce_key_validity = true, + special_case_aliases_auth = false, + strict_canonicaljson = true, + limit_notifications_power_levels = true, + knock_join_rule = false, + restricted_join_rule = false, + restricted_join_rule_fix = false, + knock_restricted_join_rule = false, + enforce_int_power_levels = false, + implicit_room_creator = false, + updated_redaction_rules = false, + hydra = false + }; +binary_to_room_version(<<"7">>) -> + #room_version{id = <<"7">>, + enforce_key_validity = true, + special_case_aliases_auth = false, + strict_canonicaljson = true, + limit_notifications_power_levels = true, + knock_join_rule = true, + restricted_join_rule = false, + restricted_join_rule_fix = false, + knock_restricted_join_rule = false, + enforce_int_power_levels = false, + implicit_room_creator = false, + updated_redaction_rules = false, + hydra = false + }; +binary_to_room_version(<<"8">>) -> + #room_version{id = <<"8">>, + enforce_key_validity = true, + special_case_aliases_auth = false, + strict_canonicaljson = true, + limit_notifications_power_levels = true, + knock_join_rule = true, + restricted_join_rule = true, + restricted_join_rule_fix = false, + knock_restricted_join_rule = false, + enforce_int_power_levels = false, + implicit_room_creator = false, + updated_redaction_rules = false, + hydra = false + }; +binary_to_room_version(<<"9">>) -> + #room_version{id = <<"9">>, + enforce_key_validity = true, + special_case_aliases_auth = false, + strict_canonicaljson = true, + limit_notifications_power_levels = true, + knock_join_rule = true, + restricted_join_rule = true, + restricted_join_rule_fix = true, + knock_restricted_join_rule = false, + enforce_int_power_levels = false, + implicit_room_creator = false, + updated_redaction_rules = false, + hydra = false + }; +binary_to_room_version(<<"10">>) -> + #room_version{id = <<"10">>, + enforce_key_validity = true, + special_case_aliases_auth = false, + strict_canonicaljson = true, + limit_notifications_power_levels = true, + knock_join_rule = true, + restricted_join_rule = true, + restricted_join_rule_fix = true, + knock_restricted_join_rule = true, + enforce_int_power_levels = true, + implicit_room_creator = false, + updated_redaction_rules = false, + hydra = false + }; +binary_to_room_version(<<"11">>) -> + #room_version{id = <<"11">>, + enforce_key_validity = true, + special_case_aliases_auth = false, + strict_canonicaljson = true, + limit_notifications_power_levels = true, + knock_join_rule = true, + restricted_join_rule = true, + restricted_join_rule_fix = true, + knock_restricted_join_rule = true, + enforce_int_power_levels = true, + implicit_room_creator = true, + updated_redaction_rules = true, + hydra = false + }; +binary_to_room_version(<<"org.matrix.hydra.11">>) -> + #room_version{id = <<"org.matrix.hydra.11">>, + enforce_key_validity = true, + special_case_aliases_auth = false, + strict_canonicaljson = true, + limit_notifications_power_levels = true, + knock_join_rule = true, + restricted_join_rule = true, + restricted_join_rule_fix = true, + knock_restricted_join_rule = true, + enforce_int_power_levels = true, + implicit_room_creator = true, + updated_redaction_rules = true, + hydra = true + }; +binary_to_room_version(<<"12">>) -> + #room_version{id = <<"12">>, + enforce_key_validity = true, + special_case_aliases_auth = false, + strict_canonicaljson = true, + limit_notifications_power_levels = true, + knock_join_rule = true, + restricted_join_rule = true, + restricted_join_rule_fix = true, + knock_restricted_join_rule = true, + enforce_int_power_levels = true, + implicit_room_creator = true, + updated_redaction_rules = true, + hydra = true + }; +binary_to_room_version(_) -> + false. + +supported_versions() -> + [<<"4">>, <<"5">>, <<"6">>, <<"7">>, <<"8">>, <<"9">>, + <<"10">>, <<"11">>, <<"org.matrix.hydra.11">>, <<"12">>]. + +json_to_event(#{<<"type">> := Type, + <<"depth">> := Depth, + <<"auth_events">> := AuthEvents0, + <<"sender">> := Sender, + <<"prev_events">> := PrevEvents, + <<"origin_server_ts">> := OriginServerTS} = JSON, RoomVersion) + when is_binary(Type), + is_integer(Depth), + is_list(AuthEvents0) -> + EventID = mod_matrix_gw:get_event_id(JSON, RoomVersion), + {RoomID, AuthEvents} = + case RoomVersion#room_version.hydra of + true -> + case {maps:get(<<"room_id">>, JSON, undefined), Type} of + {undefined, ?ROOM_CREATE} -> + <<$$, S/binary>> = EventID, + {<<$!, S/binary>>, AuthEvents0}; + {undefined, _} -> + throw(missed_room_id); + {RID, ?ROOM_CREATE} when is_binary(RID) -> + throw(room_id_in_create); + {<<$!, S/binary>> = RID, _} -> + CreateEvent = <<$$, S/binary>>, + case lists:member(CreateEvent, AuthEvents0) of + true -> + throw(create_in_auth_events); + false -> + ok + end, + {RID, [CreateEvent | AuthEvents0]} + end; + false -> + {maps:get(<<"room_id">>, JSON), AuthEvents0} + end, + StateKey = maps:get(<<"state_key">>, JSON, undefined), + case RoomVersion#room_version.strict_canonicaljson of + true -> + case mod_matrix_gw:is_canonical_json(JSON) of + true -> + ok; + false -> + throw(non_canonical_json) + end; + false -> + ok + end, + #event{id = EventID, + room_version = RoomVersion, + room_id = RoomID, + type = Type, + state_key = StateKey, + depth = Depth, + auth_events = AuthEvents, + sender = Sender, + prev_events = PrevEvents, + origin_server_ts = OriginServerTS, + json = JSON}. + +check_event_content_hash(Event) -> + JSON = Event#event.json, + case JSON of + #{<<"hashes">> := #{<<"sha256">> := S}} -> + Hash = mod_matrix_gw:content_hash(JSON), + mod_matrix_gw:base64_decode(S) == Hash; + _ -> + false + end. + +notify_event(Event, Data) -> + Data2 = notify_event_matrix(Event, Data), + notify_event_xmpp(Event, Data2). + +notify_event_matrix( + #event{type = ?ROOM_MEMBER, + state_key = StateKey, + sender = Sender, + json = #{<<"content">> := #{<<"membership">> := <<"invite">>}}} = Event, + #data{kind = #direct{}} = Data) -> + Host = Data#data.host, + MatrixServer = mod_matrix_gw_opt:matrix_domain(Host), + case mod_matrix_gw:get_id_domain_exn(StateKey) of + MatrixServer -> + Data; + RemoteServer -> + StrippedState = + maps:with([{?ROOM_CREATE, <<"">>}, {?ROOM_JOIN_RULES, <<"">>}, + {?ROOM_MEMBER, Sender}], + Event#event.state_map), + StrippedState2 = + maps:map( + fun(_, EID) -> + E = maps:get(EID, Data#data.events), + maps:with([<<"sender">>, <<"type">>, <<"state_key">>, <<"content">>], + E#event.json) + end, StrippedState), + JSON = #{<<"event">> => Event#event.json, + <<"room_version">> => (Event#event.room_version)#room_version.id, + <<"invite_room_state">> => maps:values(StrippedState2)}, + InviteRes = + mod_matrix_gw:send_request( + Data#data.host, put, RemoteServer, + [<<"_matrix">>, <<"federation">>, + <<"v2">>, <<"invite">>, + Data#data.room_id, Event#event.id], + [], + JSON, + [{timeout, 5000}], + [{sync, true}, + {body_format, binary}]), + ?DEBUG("send invite ~p~n", [InviteRes]), + Data + end; +notify_event_matrix(#event{sender = Sender} = Event, + Data) -> + case user_id_to_jid(Sender, Data) of + #jid{} = SenderJID -> + %RemoteServers = maps:keys(Data#data.remote_servers), + RemoteServers = get_remote_servers(Data), + Host = Data#data.host, + MatrixServer = mod_matrix_gw_opt:matrix_domain(Host), + lists:foldl( + fun(Server, DataAcc) -> + case Server of + MatrixServer -> + %% TODO + %case parse_user_id(Data#data.remote_user) of + % {ok, U, MatrixServer} -> + % mod_matrix_gw_c2s:notify( + % Host, U, Event); + % _ -> + % ok + %end, + DataAcc; + _ -> + case SenderJID#jid.lserver of + Host -> + case DataAcc#data.outgoing_txns of + #{Server := {T, Queue}} -> + Queue2 = [Event | Queue], + DataAcc#data{ + outgoing_txns = + maps:put(Server, {T, Queue2}, + DataAcc#data.outgoing_txns)}; + _ -> + send_new_txn([Event], Server, DataAcc) + end; + _ -> + Data + end + end + end, Data, RemoteServers); + error -> + Data + end. + +notify_event_xmpp( + #event{type = ?ROOM_MESSAGE, sender = Sender, + json = #{<<"content">> := #{<<"msgtype">> := <<"m.text">>, + <<"body">> := Body}}}, + #data{kind = #direct{local_user = UserJID}} = Data) -> + case user_id_to_jid(Sender, Data) of + #jid{} = SenderJID -> + LSenderJID = jid:tolower(SenderJID), + LUserJID = jid:tolower(UserJID), + case LSenderJID of + LUserJID -> + Data; + _ -> + RoomID = Data#data.room_id, + Msg = #message{from = SenderJID, + to = UserJID, + type = chat, + body = [#text{data = Body}], + sub_els = [#xmlel{name = <<"x">>, + attrs = [{<<"xmlns">>, <<"p1:matrix">>}, + {<<"room_id">>, RoomID}]}] + }, + ejabberd_router:route(Msg), + Data + end; + error -> + Data + end; +notify_event_xmpp( + #event{type = ?ROOM_MESSAGE, sender = Sender, + json = #{<<"content">> := #{<<"msgtype">> := <<"m.text">>, + <<"body">> := Body} = Content, + <<"origin_server_ts">> := OriginTS}}, + #data{kind = #multi{users = Users}} = Data) -> + case Sender of + <<$@, SenderUser/binary>> -> + ?DEBUG("notify xmpp ~p", [Users]), + maps:fold( + fun({LUser, LServer}, {online, Resources}, ok) -> + maps:fold( + fun(LResource, #multi_user{join_ts = JoinTS, + room_jid = RoomJID}, ok) + when JoinTS =< OriginTS -> + From = jid:replace_resource(RoomJID, SenderUser), + UserJID = jid:make(LUser, LServer, LResource), + MsgID = + case Content of + #{<<"net.process-one.xmpp-id">> := MID} + when is_binary(MID) -> + MID; + _ -> + <<"">> + end, + Msg = #message{id = MsgID, + from = From, + to = UserJID, + type = groupchat, + body = [#text{data = Body}] + }, + TimeStamp = misc:usec_to_now(OriginTS * 1000), + TSMsg = misc:add_delay_info( + Msg, Data#data.room_jid, TimeStamp), + ejabberd_router:route(TSMsg); + (_, _, _) -> ok + end, ok, Resources); + (_, _, ok) -> + ok + end, ok, Users), + Data; + _ -> + Data + end; +notify_event_xmpp( + #event{type = ?ROOM_MEMBER, sender = Sender, + json = #{<<"content">> := #{<<"membership">> := <<"join">>}, + <<"origin_server_ts">> := OriginTS}} = Event, + #data{kind = #multi{users = Users}} = Data) -> + case user_id_to_jid(Sender, Data) of + #jid{} = SenderJID -> + <<$@, SenderUser/binary>> = Sender, + maps:fold( + fun({LUser, LServer}, {online, Resources}, ok) -> + maps:fold( + fun(LResource, #multi_user{join_ts = JoinTS, + room_jid = RoomJID}, ok) + when JoinTS =< OriginTS -> + From = jid:replace_resource(RoomJID, SenderUser), + IsSelfPresence = + case jid:tolower(SenderJID) of + {LUser, LServer, _} -> + send_initial_presences( + SenderJID, RoomJID, Event, Data), + true; + _ -> + false + end, + UserJID = jid:make(LUser, LServer, LResource), + Item = + get_user_muc_item( + Sender, Event#event.state_map, Data), + Status = case IsSelfPresence of + true -> [110]; + false -> [] + end, + Pres = #presence{ + from = From, + to = UserJID, + type = available, + sub_els = [#muc_user{items = [Item], + status_codes = Status}] + }, + ejabberd_router:route(Pres), + case IsSelfPresence of + true -> + Topic = + case Event#event.state_map of + #{{?ROOM_TOPIC, <<"">>} := TEID} -> + case maps:find(TEID, Data#data.events) of + {ok, #event{json = #{<<"content">> := #{<<"topic">> := T}}}} when is_binary(T) -> + T; + _ -> + <<"">> + end; + _ -> + <<"">> + end, + Subject = + #message{ + from = RoomJID, + to = UserJID, + type = groupchat, + subject = [#text{data = Topic}] + }, + ejabberd_router:route(Subject); + false -> ok + end; + (_, _, _) -> ok + end, ok, Resources); + (_, _, ok) -> + ok + end, ok, Users), + Data; + error -> + Data + end; +notify_event_xmpp( + #event{type = ?ROOM_MEMBER, + state_key = StateKey, + json = #{<<"content">> := #{<<"membership">> := Membership}, + <<"origin_server_ts">> := OriginTS}}, + #data{kind = #multi{users = Users}} = Data) + when Membership == <<"leave">>; + Membership == <<"ban">> -> + case StateKey of + <<$@, RUser/binary>> -> + maps:fold( + fun({LUser, LServer}, {online, Resources}, ok) -> + maps:fold( + fun(LResource, #multi_user{join_ts = JoinTS, + room_jid = RoomJID}, ok) + when JoinTS =< OriginTS -> + From = jid:replace_resource(RoomJID, RUser), + UserJID = jid:make(LUser, LServer, LResource), + Item = #muc_item{affiliation = none, + role = none}, + Pres = #presence{from = From, + to = UserJID, + type = unavailable, + sub_els = [#muc_user{items = [Item]}] + }, + ejabberd_router:route(Pres); + (_, _, _) -> ok + end, ok, Resources); + (_, _, ok) -> + ok + end, ok, Users), + case user_id_to_jid(StateKey, Data) of + #jid{} = RJID -> + US = {RJID#jid.luser, RJID#jid.lserver}, + case Users of + #{US := {online, Resources}} -> + JoinTS = + maps:fold( + fun(_, #multi_user{join_ts = TS}, Acc) -> + max(Acc, TS) + end, 0, Resources), + if + JoinTS =< OriginTS -> + Users2 = maps:remove(US, Users), + Data#data{ + kind = (Data#data.kind)#multi{ + users = Users2}}; + true -> + Data + end; + _ -> + Data + end; + error -> + Data + end; + _ -> + Data + end; +notify_event_xmpp(_Event, Data) -> + Data. + +send_initial_presences(JID, RoomJID, Event, Data) -> + ?DEBUG("send_initial_presences ~p", [{JID, Event}]), + maps:fold( + fun({?ROOM_MEMBER, _}, EID, ok) -> + case maps:find(EID, Data#data.events) of + {ok, #event{ + sender = <<$@, SenderUser/binary>> = Sender, + json = #{<<"content">> := + #{<<"membership">> := <<"join">>}}}} -> + From = jid:replace_resource(RoomJID, SenderUser), + Item = + get_user_muc_item( + Sender, Event#event.state_map, Data), + Pres = #presence{from = From, + to = JID, + type = available, + sub_els = [#muc_user{items = [Item]}] + }, + ejabberd_router:route(Pres), + ok; + _ -> + ok + end; + (_, _, ok) -> + ok + end, ok, Event#event.state_map). + +get_user_muc_item(User, StateMap, Data) -> + SenderLevel = get_user_power_level(User, StateMap, Data), + BanLevel = + case statemap_find({?ROOM_POWER_LEVELS, <<"">>}, StateMap, Data) of + {ok, #event{json = #{<<"content">> := #{<<"ban">> := S}}}} -> + get_int(S); + _ -> 50 + end, + if + SenderLevel >= BanLevel -> + #muc_item{affiliation = admin, + role = moderator}; + true -> + #muc_item{affiliation = member, + role = participant} + end. + + +send_new_txn(Events, Server, Data) -> + TxnID = p1_rand:get_string(), + send_txn(TxnID, Events, Server, 1, [], Data). + +send_txn(TxnID, Events, Server, Count, Queue, Data) -> + ?DEBUG("send txn ~p~n", [{TxnID, Server}]), + Host = Data#data.host, + Origin = mod_matrix_gw_opt:matrix_domain(Host), + PDUs = + lists:map(fun(E) -> E#event.json end, Events), + Body = + #{<<"origin">> => Origin, + <<"origin_server_ts">> => + erlang:system_time(millisecond), + <<"pdus">> => PDUs}, + Self = self(), + Receiver = + fun({RequestID, Res}) -> + ?DEBUG("send_txn_res ~p", [{RequestID, Res}]), + Self ! {send_txn_res, RequestID, TxnID, Server, Res} + end, + {ok, RequestID} = + mod_matrix_gw:send_request( + Host, put, Server, + [<<"_matrix">>, <<"federation">>, + <<"v1">>, <<"send">>, + TxnID], + [], + Body, + [{timeout, 5000}], + [{sync, false}, + {receiver, Receiver}]), + Data#data{outgoing_txns = + maps:put(Server, {{RequestID, TxnID, Events, Count}, Queue}, + Data#data.outgoing_txns)}. + +do_get_missing_events(Origin, EarliestEvents, LatestEvents, Limit, MinDepth, Data) -> + case is_server_joined(Origin, Data) of + true -> + Visited = maps:from_list([{E, []} || E <- EarliestEvents]), + Queue = queue:from_list(LatestEvents), + Limit2 = min(max(Limit, 0), 20), + do_get_missing_events_bfs(Queue, Visited, Limit2, MinDepth, [], Data); + false -> + [] + end. + +do_get_missing_events_bfs(_Queue, _Visited, 0, _MinDepth, Res, _Data) -> + Res; +do_get_missing_events_bfs(Queue, Visited, Limit, MinDepth, Res, Data) -> + case queue:out(Queue) of + {{value, EventID}, Queue2} -> + case maps:find(EventID, Data#data.events) of + {ok, #event{prev_events = PrevEvents}} -> + do_get_missing_events_bfs2( + PrevEvents, Queue2, Visited, Limit, MinDepth, Res, Data); + _ -> + do_get_missing_events_bfs(Queue2, Visited, Limit, MinDepth, Res, Data) + end; + {empty, _} -> + Res + end. + +do_get_missing_events_bfs2(_PrevEvents, _Queue, _Visited, 0, _MinDepth, Res, _Data) -> + Res; +do_get_missing_events_bfs2([], Queue, Visited, Limit, MinDepth, Res, Data) -> + do_get_missing_events_bfs(Queue, Visited, Limit, MinDepth, Res, Data); +do_get_missing_events_bfs2([EventID | PrevEvents], Queue, Visited, Limit, MinDepth, Res, Data) -> + case maps:is_key(EventID, Visited) of + true -> + do_get_missing_events_bfs2(PrevEvents, Queue, Visited, Limit, MinDepth, Res, Data); + false -> + case maps:find(EventID, Data#data.events) of + {ok, #event{depth = Depth} = Event} when Depth >= MinDepth -> + Queue2 = queue:in(EventID, Queue), + Visited2 = Visited#{EventID => []}, + Res2 = [Event | Res], + do_get_missing_events_bfs2( + PrevEvents, Queue2, Visited2, Limit - 1, MinDepth, Res2, Data); + _ -> + do_get_missing_events_bfs2(PrevEvents, Queue, Visited, Limit, MinDepth, Res, Data) + end + end. + +do_get_state_ids(Origin, EventID, Data) -> + case is_server_joined(Origin, Data) of + true -> + case maps:find(EventID, Data#data.events) of + {ok, #event{state_map = StateMap} = Event} when is_map(StateMap) -> + PrevStateMap = get_event_prev_state_map(Event, Data), + PDUs = maps:values(PrevStateMap), + AuthChain = do_get_state_ids_dfs(PDUs, #{}, [], Data), + {ok, AuthChain, PDUs}; + error -> + {error, event_not_found} + end; + false -> + {error, not_allowed} + end. + +do_get_state_ids_dfs([], _Visited, Res, _Data) -> + Res; +do_get_state_ids_dfs([EventID | Queue], Visited, Res, Data) -> + case maps:is_key(EventID, Visited) of + true -> + do_get_state_ids_dfs(Queue, Visited, Res, Data); + false -> + case maps:find(EventID, Data#data.events) of + {ok, Event} -> + Visited2 = Visited#{EventID => []}, + do_get_state_ids_dfs( + Event#event.auth_events ++ Queue, Visited2, [EventID | Res], Data); + error -> + do_get_state_ids_dfs(Queue, Visited, Res, Data) + end + end. + + +is_server_joined(Server, Data) -> + try + sets:fold( + fun(EventID, ok) -> + case maps:find(EventID, Data#data.events) of + {ok, Event} -> + maps:fold( + fun({?ROOM_MEMBER, UserID}, EID, ok) -> + case mod_matrix_gw:get_id_domain_exn(UserID) of + Server -> + case maps:find(EID, Data#data.events) of + {ok, #event{ + json = #{<<"content">> := + #{<<"membership">> := + <<"join">>}}}} -> + throw(found); + _ -> + ok + end; + _ -> + ok + end; + (_, _, ok) -> + ok + end, ok, Event#event.state_map), + ok; + _ -> + ok + end + end, ok, Data#data.latest_events), + false + catch + throw:found -> + true + end. + +get_remote_servers(Data) -> + Servers = + maps:fold( + fun(EventID, _, Acc) -> + case maps:find(EventID, Data#data.events) of + {ok, Event} when is_map(Event#event.state_map) -> + maps:fold( + fun({?ROOM_MEMBER, UserID}, EID, Acc2) -> + Server = mod_matrix_gw:get_id_domain_exn(UserID), + case maps:find(EID, Data#data.events) of + {ok, #event{ + json = #{<<"content">> := + #{<<"membership">> := + <<"join">>}}}} -> + maps:put(Server, [], Acc2); + _ -> + Acc2 + end; + (_, _, Acc2) -> + Acc2 + end, Acc, Event#event.state_map); + _ -> + Acc + end + end, #{}, Data#data.latest_events), + maps:keys(Servers). + +get_joined_users(Data) -> + Users = + maps:fold( + fun(EventID, _, Acc) -> + case maps:find(EventID, Data#data.events) of + {ok, Event} when is_map(Event#event.state_map) -> + maps:fold( + fun({?ROOM_MEMBER, UserID}, EID, Acc2) -> + case maps:find(EID, Data#data.events) of + {ok, #event{ + json = #{<<"content">> := + #{<<"membership">> := + <<"join">>}}}} -> + maps:put(UserID, [], Acc2); + _ -> + Acc2 + end; + (_, _, Acc2) -> + Acc2 + end, Acc, Event#event.state_map); + _ -> + Acc + end + end, #{}, Data#data.latest_events), + maps:keys(Users). + +user_id_to_jid(Str, #data{} = Data) -> + user_id_to_jid(Str, Data#data.host); +user_id_to_jid(Str, Host) when is_binary(Host) -> + ServerName = mod_matrix_gw_opt:matrix_domain(Host), + case parse_user_id(Str) of + {ok, U, ServerName} -> + jid:make(U, Host); + {ok, U, S} -> + ServiceHost = mod_matrix_gw_opt:host(Host), + EscU = escape(U), + EscS = escape(S), + jid:make(<>, ServiceHost); + error -> + error + end. + +user_id_from_jid(#jid{luser = U, lserver = Host}, Host) -> + ServerName = mod_matrix_gw_opt:matrix_domain(Host), + {ok, <<$@, U/binary, $:, ServerName/binary>>}; +user_id_from_jid(JID, _Host) -> + case binary:split(JID#jid.luser, <<"%">>) of + [EscU, EscS] -> + U = unescape(EscU), + S = unescape(EscS), + {ok, <<$@, U/binary, $:, S/binary>>}; + _ -> + error + end. + +new_room_id() -> + Host = ejabberd_config:get_myname(), + Letters = <<"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ">>, + N = size(Letters), + S = << <<(binary:at(Letters, X rem N))>> || + <> <= crypto:strong_rand_bytes(18)>>, + MatrixServer = mod_matrix_gw_opt:matrix_domain(Host), + <<$!, S/binary, $:, MatrixServer/binary>>. + +compute_event_auth_keys(#{<<"type">> := ?ROOM_CREATE}, _RoomVersion) -> + []; +compute_event_auth_keys(#{<<"type">> := ?ROOM_MEMBER, + <<"sender">> := Sender, + <<"content">> := #{<<"membership">> := Membership} = Content, + <<"state_key">> := StateKey}, + RoomVersion) -> + Common1 = [{?ROOM_POWER_LEVELS, <<"">>}, + {?ROOM_MEMBER, Sender}, + {?ROOM_MEMBER, StateKey}], + Common = + case RoomVersion#room_version.hydra of + false -> + [{?ROOM_CREATE, <<"">>} | Common1]; + true -> + Common1 + end, + case Membership of + <<"join">> -> + case Content of + #{<<"join_authorised_via_users_server">> := AuthUser} + when RoomVersion#room_version.restricted_join_rule -> + [{?ROOM_MEMBER, AuthUser}, {?ROOM_JOIN_RULES, <<"">>} | Common]; + _ -> + [{?ROOM_JOIN_RULES, <<"">>} | Common] + end; + <<"invite">> -> + case Content of + #{<<"third_party_invite">> := #{<<"signed">> := #{<<"token">> := Token}}} -> + [{?ROOM_3PI, Token}, {?ROOM_JOIN_RULES, <<"">>} | Common]; + _ -> + [{?ROOM_JOIN_RULES, <<"">>} | Common] + end; + <<"knock">> -> + [{?ROOM_JOIN_RULES, <<"">>} | Common]; + _ -> + Common + end; +compute_event_auth_keys(#{<<"type">> := _, <<"sender">> := Sender}, RoomVersion) -> + Common1 = + [{?ROOM_POWER_LEVELS, <<"">>}, + {?ROOM_MEMBER, Sender}], + case RoomVersion#room_version.hydra of + false -> + [{?ROOM_CREATE, <<"">>} | Common1]; + true -> + Common1 + end. + + +update_client(#data{kind = #direct{client_state = undefined, + local_user = JID, + remote_user = RemoteUserID}} = Data) -> + Host = Data#data.host, + MatrixServer = mod_matrix_gw_opt:matrix_domain(Host), + LocalUserID = <<$@, (JID#jid.luser)/binary, $:, MatrixServer/binary>>, + Users = get_joined_users(Data), + case lists:member(LocalUserID, Users) of + true -> + case lists:delete(LocalUserID, Users) of + [RemoteUserID] -> + {ok, Data#data{kind = (Data#data.kind)#direct{client_state = established}}}; + [_] -> + {leave, unknown_remote_user, + Data#data{kind = (Data#data.kind)#direct{client_state = leave}}}; + [] -> + {ok, Data}; + _ -> + {leave, too_many_users, + Data#data{kind = (Data#data.kind)#direct{client_state = leave}}} + end; + false -> + {ok, Data} + end; +update_client(#data{kind = #direct{client_state = established, + local_user = JID, + remote_user = RemoteUserID}} = Data) -> + Host = Data#data.host, + MatrixServer = mod_matrix_gw_opt:matrix_domain(Host), + LocalUserID = <<$@, (JID#jid.luser)/binary, $:, MatrixServer/binary>>, + Users = get_joined_users(Data), + case lists:member(LocalUserID, Users) of + true -> + case lists:member(RemoteUserID, Users) of + true -> + {ok, Data}; + false -> + {leave, remote_user_left, Data#data{kind = (Data#data.kind)#direct{client_state = leave}}} + end; + false -> + stop + end; +update_client(#data{kind = #direct{client_state = leave}}) -> + stop; +update_client(#data{kind = #multi{users = Users}} = Data) -> + ?DEBUG("update_client ~p", [Data#data.kind]), + if + Users == #{} -> + stop; + true -> + {ok, Data} + end. + + +send_muc_invite(Host, Origin, RoomID, Sender, UserID, Event, IRS) -> + case {user_id_to_jid(Sender, Host), user_id_to_jid(UserID, Host)} of + {#jid{} = SenderJID, #jid{lserver = Host} = UserJID} -> + process_pdu(Host, Origin, Event), + ServiceHost = mod_matrix_gw_opt:host(Host), + Alias = + lists:foldl( + fun(#{<<"type">> := <<"m.room.canonical_alias">>, + <<"content">> := #{<<"alias">> := A}}, _) + when is_binary(A) -> A; + (_, Acc) -> Acc + end, none, IRS), + {ok, EscRoomID} = + case Alias of + <<$#, Parts/binary>> -> + case binary:split(Parts, <<":">>) of + [R, S] -> + User = <<$#, R/binary, $%, S/binary>>, + case jid:nodeprep(User) of + error -> + room_id_to_xmpp(RoomID, Origin); + _ -> + {ok, User} + end; + _ -> + room_id_to_xmpp(RoomID, Origin) + end; + _ -> + room_id_to_xmpp(RoomID, Origin) + end, + RoomJID = jid:make(EscRoomID, ServiceHost), + Invite = #muc_invite{to = undefined, from = SenderJID}, + XUser = #muc_user{invites = [Invite]}, + Msg = #message{ + from = RoomJID, + to = UserJID, + sub_els = [XUser] + }, + ejabberd_router:route(Msg); + _ -> + ok + end. + +room_id_to_xmpp(RoomID) -> + room_id_to_xmpp(RoomID, undefined). + +room_id_to_xmpp(RoomID, Origin) -> + case RoomID of + <<$!, Parts/binary>> -> + case binary:split(Parts, <<":">>) of + [R, S] -> + Len = 8 * size(R), + <> = R, + HR = integer_to_binary(IR, 16), + {ok, <<$!, HR/binary, $%, S/binary>>}; + [R] -> + Len = 8 * size(R), + <> = R, + HR = integer_to_binary(IR, 16), + case Origin of + undefined -> + {ok, <<$!, HR/binary>>}; + S when is_binary(S) -> + {ok, <<$!, HR/binary, $%, $%, S/binary>>} + end; + _ -> error + end; + _ -> + error + end. + +room_id_from_xmpp(Host, RID) -> + case RID of + <<$!, Parts/binary>> -> + case binary:split(Parts, <<"%">>) of + [R, <<$%, S/binary>>] -> + IR = binary_to_integer(R, 16), + Len = size(R) * 4, + RoomID = <>, + {ok, <<$!, RoomID/binary>>, S}; + [R, S] -> + IR = binary_to_integer(R, 16), + Len = size(R) * 4, + RoomID = <>, + {ok, <<$!, RoomID/binary, $:, S/binary>>, S}; + [R] -> + IR = binary_to_integer(R, 16), + Len = size(R) * 4, + RoomID = <>, + {ok, <<$!, RoomID/binary>>, undefined}; + _ -> error + end; + <<$#, Parts/binary>> -> + case binary:split(Parts, <<"%">>) of + [R, S] -> + Alias = <<$#, R/binary, $:, S/binary>>, + case resolve_alias(Host, S, Alias) of + {ok, <<$!, _/binary>> = RoomID} -> + {ok, RoomID, S}; + error -> + error + end; + _ -> error + end; + _ -> + error + end. + +resolve_alias(Host, Origin, Alias) -> + ets_cache:lookup( + ?MATRIX_ROOM_ALIAS_CACHE, Alias, + fun() -> + Res = + mod_matrix_gw:send_request( + Host, get, Origin, + [<<"_matrix">>, <<"federation">>, + <<"v1">>, <<"query">>, <<"directory">>], + [{<<"room_alias">>, Alias}], + none, + [{timeout, 5000}], + [{sync, true}, + {body_format, binary}]), + case Res of + {ok, {{_, 200, _}, _Headers, Body}} -> + try + case misc:json_decode(Body) of + #{<<"room_id">> := RoomID} -> + {ok, RoomID} + end + catch + Class:Reason:ST -> + ?DEBUG("failed resolve_alias: ~p", [{Class, Reason, ST}]), + {cache_with_timeout, error, ?MATRIX_ROOM_ALIAS_CACHE_ERROR_TIMEOUT} + end; + {ok, {{_, _Status, _Reason}, _Headers, _Body}} -> + {cache_with_timeout, error, ?MATRIX_ROOM_ALIAS_CACHE_ERROR_TIMEOUT}; + {error, _Reason} -> + {cache_with_timeout, error, ?MATRIX_ROOM_ALIAS_CACHE_ERROR_TIMEOUT} + end + end). + + +escape(S) -> + escape(S, <<>>). + +escape(<<>>, Res) -> + Res; +escape(<>, Res) -> + Res2 = + case C of + $\s -> <>; + $" -> <>; + $% -> <>; + $& -> <>; + $' -> <>; + $/ -> <>; + $: -> <>; + $< -> <>; + $> -> <>; + $@ -> <>; + $\\ -> <>; + _ -> <> + end, + escape(S, Res2). + +unescape(S) -> + unescape(S, <<>>). + +unescape(<<>>, Res) -> Res; +unescape(<<"\\20", S/binary>>, Res) -> unescape(S, <>); +unescape(<<"\\22", S/binary>>, Res) -> unescape(S, <>); +unescape(<<"\\25", S/binary>>, Res) -> unescape(S, <>); +unescape(<<"\\26", S/binary>>, Res) -> unescape(S, <>); +unescape(<<"\\27", S/binary>>, Res) -> unescape(S, <>); +unescape(<<"\\2f", S/binary>>, Res) -> unescape(S, <>); +unescape(<<"\\3a", S/binary>>, Res) -> unescape(S, <>); +unescape(<<"\\3c", S/binary>>, Res) -> unescape(S, <>); +unescape(<<"\\3e", S/binary>>, Res) -> unescape(S, <>>); +unescape(<<"\\40", S/binary>>, Res) -> unescape(S, <>); +unescape(<<"\\5c", S/binary>>, Res) -> unescape(S, <>); +unescape(<>, Res) -> unescape(S, <>). + +-endif. diff --git a/src/mod_matrix_gw_s2s.erl b/src/mod_matrix_gw_s2s.erl new file mode 100644 index 000000000..241b78ef6 --- /dev/null +++ b/src/mod_matrix_gw_s2s.erl @@ -0,0 +1,595 @@ +%%%------------------------------------------------------------------- +%%% File : mod_matrix_gw_s2s.erl +%%% Author : Alexey Shchepin +%%% Purpose : Matrix S2S +%%% Created : 1 May 2022 by Alexey Shchepin +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_matrix_gw_s2s). +-ifndef(OTP_BELOW_25). +-behaviour(gen_statem). + +%% API +-export([start_link/2, supervisor/1, create_db/0, + get_connection/2, check_auth/5, check_signature/3, + get_matrix_host_port/2]). + +%% gen_statem callbacks +-export([init/1, terminate/3, code_change/4, callback_mode/0]). +-export([handle_event/4]). + +-include("logger.hrl"). +-include("ejabberd_http.hrl"). +-include_lib("kernel/include/inet.hrl"). +-include("mod_matrix_gw.hrl"). + +-record(matrix_s2s, + {to :: binary(), + pid :: pid()}). + +-record(pending, + {request_id :: any(), + servers :: [binary()], + key_queue = []}). + +-record(wait, + {timer_ref :: reference(), + last :: integer()}). + +-record(data, + {host :: binary(), + matrix_server :: binary(), + matrix_host_port :: {binary(), integer()} | undefined, + keys = #{}, + state :: #pending{} | #wait{}}). + +-define(KEYS_REQUEST_TIMEOUT, 600000). + +%%%=================================================================== +%%% API +%%%=================================================================== + +%%-------------------------------------------------------------------- +%% @doc +%% Creates a gen_statem process which calls Module:init/1 to +%% initialize. To ensure a synchronized start-up procedure, this +%% function does not return until Module:init/1 has returned. +%% +%% @end +%%-------------------------------------------------------------------- +-spec start_link(binary(), binary()) -> + {ok, Pid :: pid()} | + ignore | + {error, Error :: term()}. +start_link(Host, MatrixServer) -> + gen_statem:start_link(?MODULE, [Host, MatrixServer], + ejabberd_config:fsm_limit_opts([])). + +-spec supervisor(binary()) -> atom(). +supervisor(Host) -> + gen_mod:get_module_proc(Host, mod_matrix_gw_s2s_sup). + +create_db() -> + ejabberd_mnesia:create( + ?MODULE, matrix_s2s, + [{ram_copies, [node()]}, + {type, set}, + {attributes, record_info(fields, matrix_s2s)}]), + ok. + +get_connection(Host, MatrixServer) -> + case mnesia:dirty_read(matrix_s2s, MatrixServer) of + [] -> + case supervisor:start_child(supervisor(Host), + [Host, MatrixServer]) of + {ok, undefined} -> {error, ignored}; + Res -> Res + end; + [#matrix_s2s{pid = Pid}] -> + {ok, Pid} + end. + +get_key(Host, MatrixServer, KeyID) -> + case mod_matrix_gw_opt:matrix_domain(Host) of + MatrixServer -> + {PubKey, _PrivKey} = mod_matrix_gw_opt:key(Host), + TS = erlang:system_time(millisecond) + timer:hours(24 * 7), + {ok, PubKey, TS}; + _ -> + case get_connection(Host, MatrixServer) of + {ok, S2SPid} -> + gen_statem:call(S2SPid, {get_key, KeyID}); + Error -> Error + end + end. + +get_matrix_host_port(Host, MatrixServer) -> + case mod_matrix_gw_opt:matrix_domain(Host) of + MatrixServer -> + error; + _ -> + case get_connection(Host, MatrixServer) of + {ok, S2SPid} -> + gen_statem:call(S2SPid, get_matrix_host_port); + Error -> Error + end + end. + + +%process_query(Host, MatrixServer, AuthParams, Query, JSON, Request) -> +% case get_connection(Host, MatrixServer) of +% {ok, S2SPid} -> +% #request{sockmod = SockMod, socket = Socket} = Request, +% SockMod:controlling_process(Socket, S2SPid), +% gen_statem:cast(S2SPid, {query, AuthParams, Query, JSON, Request}), +% ok; +% {error, _} = Error -> +% Error +% end. + +check_auth(Host, MatrixServer, AuthParams, Content, Request) -> + case get_connection(Host, MatrixServer) of + {ok, S2SPid} -> + #{<<"key">> := KeyID} = AuthParams, + case catch gen_statem:call(S2SPid, {get_key, KeyID}) of + {ok, VerifyKey, _ValidUntil} -> + %% TODO: check ValidUntil + Destination = mod_matrix_gw_opt:matrix_domain(Host), + #{<<"sig">> := Sig} = AuthParams, + JSON = #{<<"method">> => atom_to_binary(Request#request.method, latin1), + <<"uri">> => Request#request.raw_path, + <<"origin">> => MatrixServer, + <<"destination">> => Destination, + <<"signatures">> => #{ + MatrixServer => #{KeyID => Sig} + } + }, + JSON2 = + case Content of + none -> JSON; + _ -> + JSON#{<<"content">> => Content} + end, + case check_signature(JSON2, MatrixServer, KeyID, VerifyKey) of + true -> + true; + false -> + ?WARNING_MSG("Failed authentication: ~p", [JSON2]), + false + end; + _ -> + false + end; + {error, _} = _Error -> + false + end. + +check_signature(Host, JSON, RoomVersion) -> + case JSON of + #{<<"sender">> := Sender, + <<"signatures">> := Sigs, + <<"origin_server_ts">> := OriginServerTS} -> + MatrixServer = mod_matrix_gw:get_id_domain_exn(Sender), + case Sigs of + #{MatrixServer := #{} = KeySig} -> + case maps:next(maps:iterator(KeySig)) of + {KeyID, _Sig, _} -> + case catch get_key(Host, MatrixServer, KeyID) of + {ok, VerifyKey, ValidUntil} -> + if + not RoomVersion#room_version.enforce_key_validity or + (OriginServerTS =< ValidUntil) -> + case check_signature(JSON, MatrixServer, KeyID, VerifyKey) of + true -> + true; + false -> + ?WARNING_MSG("Failed authentication: ~p", [JSON]), + false + end; + true -> + false + end; + _ -> + false + end; + _ -> + false + end; + _ -> + false + end; + _ -> + false + end. + + +%%%=================================================================== +%%% gen_statem callbacks +%%%=================================================================== + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Whenever a gen_statem is started using gen_statem:start/[3,4] or +%% gen_statem:start_link/[3,4], this function is called by the new +%% process to initialize. +%% @end +%%-------------------------------------------------------------------- +-spec init(Args :: term()) -> gen_statem:init_result(term()). +init([Host, MatrixServer]) -> + mnesia:dirty_write( + #matrix_s2s{to = MatrixServer, + pid = self()}), + {ok, state_name, + request_keys( + MatrixServer, + #data{host = Host, + matrix_server = MatrixServer, + state = #wait{timer_ref = make_ref(), last = 0}})}. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% If the gen_statem runs with CallbackMode =:= handle_event_function +%% this function is called for every event a gen_statem receives. +%% @end +%%-------------------------------------------------------------------- +-spec handle_event( + gen_statem:event_type(), Msg :: term(), + State :: term(), Data :: term()) -> + gen_statem:handle_event_result(). +%handle_event({call, From}, _Msg, State, Data) -> +% {next_state, State, Data, [{reply, From, ok}]}. +handle_event({call, From}, get_matrix_host_port, _State, Data) -> + case Data#data.matrix_host_port of + undefined -> + Result = do_get_matrix_host_port(Data#data.matrix_server), + Data2 = Data#data{matrix_host_port = Result}, + {keep_state, Data2, [{reply, From, Result}]}; + Result -> + {keep_state_and_data, [{reply, From, Result}]} + end; +handle_event({call, From}, {get_key, KeyID}, State, Data) -> + case maps:find(KeyID, Data#data.keys) of + {ok, {Key, ValidUntil}} -> + {keep_state, Data, [{reply, From, {ok, Key, ValidUntil}}]}; + error -> + case Data#data.state of + #pending{key_queue = KeyQueue} = St -> + KeyQueue2 = [{From, KeyID} | KeyQueue], + {next_state, State, + Data#data{state = St#pending{key_queue = KeyQueue2}}, []}; + #wait{timer_ref = TimerRef, last = Last} -> + TS = erlang:system_time(millisecond), + if + Last + ?KEYS_REQUEST_TIMEOUT =< TS -> + Data2 = request_keys(Data#data.matrix_server, Data), + #pending{key_queue = KeyQueue} = St = Data2#data.state, + KeyQueue2 = [{From, KeyID} | KeyQueue], + {next_state, State, + Data2#data{state = St#pending{key_queue = KeyQueue2}}, []}; + true -> + Timeout = + case erlang:read_timer(TimerRef) of + false -> + Last + ?KEYS_REQUEST_TIMEOUT - TS; + Left -> + erlang:cancel_timer(TimerRef), + min(Left, + Last + ?KEYS_REQUEST_TIMEOUT - TS) + end, + TRef = erlang:start_timer(Timeout, self(), []), + {next_state, State, + Data#data{state = #wait{timer_ref = TRef, + last = Last}}, + [{reply, From, error}]} + end + end + end; +handle_event(cast, {key_reply, RequestID, HTTPResult}, State, + #data{state = #pending{request_id = RequestID, + servers = Servers, + key_queue = KeyQueue} = St} = Data) -> + TS = erlang:system_time(millisecond), + Res = + case HTTPResult of + {{_, 200, _}, _, SJSON} -> + try + JSON1 = misc:json_decode(SJSON), + JSON = + case JSON1 of + #{<<"server_keys">> := [J]} -> J; + _ -> JSON1 + end, + ?DEBUG("keys ~p~n", [JSON]), + #{<<"verify_keys">> := VerifyKeys} = JSON, + {KeyID, KeyData, _} = maps:next(maps:iterator(VerifyKeys)), + #{<<"key">> := SKey} = KeyData, + VerifyKey = mod_matrix_gw:base64_decode(SKey), + ?DEBUG("key ~p~n", [VerifyKey]), + ?DEBUG("check ~p~n", + [catch check_signature( + JSON, Data#data.matrix_server, + KeyID, VerifyKey)]), + true = check_signature( + JSON, Data#data.matrix_server, + KeyID, VerifyKey), + #{<<"valid_until_ts">> := ValidUntil} = JSON, + ValidUntil2 = + min(ValidUntil, + erlang:system_time(millisecond) + timer:hours(24 * 7)), + OldKeysJSON = + case JSON of + #{<<"old_verify_keys">> := OldKeysJ} + when is_map(OldKeysJ) -> + OldKeysJ; + _ -> + #{} + end, + OldKeys = + maps:filtermap( + fun(_KID, + #{<<"key">> := SK, + <<"expired_ts">> := Exp}) + when is_integer(Exp), + is_binary(SK) -> + {true, {mod_matrix_gw:base64_decode(SK), + Exp}}; + (_, _) -> false + end, OldKeysJSON), + NewKeys = + maps:filtermap( + fun(_KID, + #{<<"key">> := SK}) + when is_binary(SK) -> + {true, {mod_matrix_gw:base64_decode(SK), + ValidUntil2}}; + (_, _) -> false + end, VerifyKeys), + {ok, maps:merge(OldKeys, NewKeys), ValidUntil2} + catch + _:_ -> + {ok, Data#data.keys, TS + ?KEYS_REQUEST_TIMEOUT} + end; + _ -> + case Servers of + [] -> + {ok, Data#data.keys, TS + ?KEYS_REQUEST_TIMEOUT}; + [S | Servers1] -> + {error, + request_keys( + S, + Data#data{state = St#pending{servers = Servers1}})} + end + end, + case Res of + {ok, Keys, ValidTS} -> + Replies = + lists:map( + fun({From, KeyID}) -> + case maps:find(KeyID, Keys) of + {ok, {Key, KeyValidUntil}} -> + {reply, From, {ok, Key, KeyValidUntil}}; + error -> + {reply, From, error} + end + end, + KeyQueue), + TimerRef = erlang:start_timer(max(ValidTS - TS, ?KEYS_REQUEST_TIMEOUT), + self(), []), + Data2 = Data#data{keys = Keys, + state = #wait{timer_ref = TimerRef, + last = TS}}, + ?DEBUG("KEYS ~p~n", [{Keys, Data2}]), + {next_state, State, Data2, Replies}; + {error, Data2} -> + {next_state, State, Data2, []} + end; +handle_event(info, {timeout, TimerRef, []}, State, + #data{state = #wait{timer_ref = TimerRef}} = Data) -> + Data2 = request_keys(Data#data.matrix_server, Data), + {next_state, State, Data2, []}; +handle_event(cast, Msg, State, Data) -> + ?WARNING_MSG("Unexpected cast: ~p", [Msg]), + {next_state, State, Data, []}; +handle_event(info, Info, State, Data) -> + ?WARNING_MSG("Unexpected info: ~p", [Info]), + {next_state, State, Data, []}. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% This function is called by a gen_statem when it is about to +%% terminate. It should be the opposite of Module:init/1 and do any +%% necessary cleaning up. When it returns, the gen_statem terminates with +%% Reason. The return value is ignored. +%% @end +%%-------------------------------------------------------------------- +-spec terminate(Reason :: term(), State :: term(), Data :: term()) -> + any(). +terminate(_Reason, _State, Data) -> + mnesia:dirty_delete_object( + #matrix_s2s{to = Data#data.matrix_server, + pid = self()}), + %% TODO: wait for messages + ok. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Convert process state when code is changed +%% @end +%%-------------------------------------------------------------------- +-spec code_change( + OldVsn :: term() | {down,term()}, + State :: term(), Data :: term(), Extra :: term()) -> + {ok, NewState :: term(), NewData :: term()}. +code_change(_OldVsn, State, Data, _Extra) -> + {ok, State, Data}. + +callback_mode() -> + handle_event_function. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== + +do_get_matrix_host_port(MatrixServer) -> + case binary:split(MatrixServer, <<":">>) of + [Addr] -> + case inet:parse_address(binary_to_list(Addr)) of + {ok, _} -> + {Addr, 8448}; + _ -> + URL = <<"https://", Addr/binary, "/.well-known/matrix/server">>, + HTTPRes = + httpc:request(get, {URL, []}, + [{timeout, 5000}], + [{sync, true}, + {body_format, binary}]), + ?DEBUG("HTTPRes ~p~n", [HTTPRes]), + Res = + case HTTPRes of + {ok, {{_, 200, _}, _Headers, Body}} -> + try + case misc:json_decode(Body) of + #{<<"m.server">> := Server} -> + case binary:split(Server, <<":">>) of + [ServerAddr] -> + {ServerAddr, 8448}; + [ServerAddr, ServerPort] -> + {ServerAddr, binary_to_integer(ServerPort)} + end + end + catch + _:_ -> + error + end; + _ -> + error + end, + case Res of + error -> + SRVName = + "_matrix._tcp." ++ binary_to_list(MatrixServer), + case inet_res:getbyname(SRVName, srv, 5000) of + {ok, HostEntry} -> + {hostent, _Name, _Aliases, _AddrType, _Len, + HAddrList} = HostEntry, + case h_addr_list_to_host_ports(HAddrList) of + {ok, [{Host, Port} | _]} -> + {list_to_binary(Host), Port}; + _ -> + {MatrixServer, 8448} + end; + {error, _} -> + {MatrixServer, 8448} + end; + _ -> + Res + end + end; + [Addr, SPort] -> + case catch binary_to_integer(SPort) of + Port when is_integer(Port) -> + {Addr, Port}; + _ -> + error + end + end. + +%% Copied from xmpp_stream_out.erl +-type host_port() :: {inet:hostname(), inet:port_number()}. +-type h_addr_list() :: [{integer(), integer(), inet:port_number(), string()}]. +-spec h_addr_list_to_host_ports(h_addr_list()) -> {ok, [host_port(),...]} | + {error, nxdomain}. +h_addr_list_to_host_ports(AddrList) -> + PrioHostPorts = lists:flatmap( + fun({Priority, Weight, Port, Host}) -> + N = case Weight of + 0 -> 0; + _ -> (Weight + 1) * p1_rand:uniform() + end, + [{Priority * 65536 - N, Host, Port}]; + (_) -> + [] + end, AddrList), + HostPorts = [{Host, Port} + || {_Priority, Host, Port} <- lists:usort(PrioHostPorts)], + case HostPorts of + [] -> {error, nxdomain}; + _ -> {ok, HostPorts} + end. + + +check_signature(JSON, SignatureName, KeyID, VerifyKey) -> + try + #{<<"signatures">> := Signatures} = JSON, + #{SignatureName := SignatureData} = Signatures, + #{KeyID := SSignature} = SignatureData, + Signature = mod_matrix_gw:base64_decode(SSignature), + JSON2 = maps:without([<<"signatures">>, <<"unsigned">>], JSON), + Msg = mod_matrix_gw:encode_canonical_json(JSON2), + crypto:verify(eddsa, none, Msg, Signature, [VerifyKey, ed25519]) + catch + _:_ -> + false + end. + +request_keys(Via, Data) -> + {MHost, MPort} = do_get_matrix_host_port(Via), + URL = + case Data#data.matrix_server of + Via -> + <<"https://", MHost/binary, + ":", (integer_to_binary(MPort))/binary, + "/_matrix/key/v2/server">>; + MatrixServer -> + <<"https://", MHost/binary, + ":", (integer_to_binary(MPort))/binary, + "/_matrix/key/v2/query/", MatrixServer/binary>> + end, + Self = self(), + {ok, RequestID} = + httpc:request(get, {URL, []}, + [{timeout, 5000}], + [{sync, false}, + {receiver, + fun({RequestId, Result}) -> + gen_statem:cast( + Self, {key_reply, RequestId, Result}) + end}]), + case Data#data.state of + #pending{request_id = OldReqID} = St -> + case OldReqID of + undefined -> + ok; + _ -> + httpc:cancel_request(OldReqID) + end, + Data#data{state = St#pending{request_id = RequestID}}; + #wait{timer_ref = TimerRef} -> + erlang:cancel_timer(TimerRef), + NotaryServers = mod_matrix_gw_opt:notary_servers(Data#data.host), + Data#data{state = #pending{request_id = RequestID, + servers = NotaryServers}} + end. + +-endif. diff --git a/src/mod_matrix_gw_sup.erl b/src/mod_matrix_gw_sup.erl new file mode 100644 index 000000000..f08f36e68 --- /dev/null +++ b/src/mod_matrix_gw_sup.erl @@ -0,0 +1,77 @@ +%%%---------------------------------------------------------------------- +%%% Created : 1 May 2022 by Alexey Shchepin +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_matrix_gw_sup). +-ifndef(OTP_BELOW_25). +-behaviour(supervisor). + +%% API +-export([start/1, start_link/1, procname/1]). +%% Supervisor callbacks +-export([init/1]). + +%%%=================================================================== +%%% API functions +%%%=================================================================== +start(Host) -> + Spec = #{id => procname(Host), + start => {?MODULE, start_link, [Host]}, + restart => permanent, + shutdown => infinity, + type => supervisor, + modules => [?MODULE]}, + supervisor:start_child(ejabberd_gen_mod_sup, Spec). + +start_link(Host) -> + Proc = procname(Host), + supervisor:start_link({local, Proc}, ?MODULE, [Host]). + +-spec procname(binary()) -> atom(). +procname(Host) -> + gen_mod:get_module_proc(Host, ?MODULE). + +%%%=================================================================== +%%% Supervisor callbacks +%%%=================================================================== +init([Host]) -> + S2SName = mod_matrix_gw_s2s:supervisor(Host), + RoomName = mod_matrix_gw_room:supervisor(Host), + Specs = + [#{id => S2SName, + start => {ejabberd_tmp_sup, start_link, [S2SName, mod_matrix_gw_s2s]}, + restart => permanent, + shutdown => infinity, + type => supervisor, + modules => [ejabberd_tmp_sup]}, + #{id => RoomName, + start => {ejabberd_tmp_sup, start_link, [RoomName, mod_matrix_gw_room]}, + restart => permanent, + shutdown => infinity, + type => supervisor, + modules => [ejabberd_tmp_sup]}, + #{id => mod_matrix_gw:procname(Host), + start => {mod_matrix_gw, start_link, [Host]}, + restart => permanent, + shutdown => timer:minutes(1), + type => worker, + modules => [mod_matrix_gw]}], + {ok, {{one_for_one, 10, 1}, Specs}}. +-endif. diff --git a/src/mod_metrics.erl b/src/mod_metrics.erl new file mode 100644 index 000000000..77b690867 --- /dev/null +++ b/src/mod_metrics.erl @@ -0,0 +1,213 @@ +%%%------------------------------------------------------------------- +%%% File : mod_metrics.erl +%%% Author : Christophe Romain +%%% Purpose : Simple metrics handler for runtime statistics +%%% Created : 22 Oct 2015 by Christophe Romain +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_metrics). + +-author('christophe.romain@process-one.net'). +-behaviour(gen_mod). + +-include("logger.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). +-include("translate.hrl"). + +-export([start/2, stop/1, mod_opt_type/1, mod_options/1, depends/2, reload/3]). +-export([push/2, mod_doc/0]). +-export([offline_message_hook/1, + sm_register_connection_hook/3, sm_remove_connection_hook/3, + user_send_packet/1, user_receive_packet/1, + s2s_send_packet/1, s2s_receive_packet/1, + remove_user/2, register_user/2]). + +-define(SOCKET_NAME, mod_metrics_udp_socket). +-define(SOCKET_REGISTER_RETRIES, 10). + +-type probe() :: atom() | {atom(), integer()}. + +%%==================================================================== +%% API +%%==================================================================== + +start(_Host, _Opts) -> + {ok, [{hook, offline_message_hook, offline_message_hook, 20}, + {hook, sm_register_connection_hook, sm_register_connection_hook, 20}, + {hook, sm_remove_connection_hook, sm_remove_connection_hook, 20}, + {hook, user_send_packet, user_send_packet, 20}, + {hook, user_receive_packet, user_receive_packet, 20}, + {hook, s2s_send_packet, s2s_send_packet, 20}, + {hook, s2s_receive_packet, s2s_receive_packet, 20}, + {hook, remove_user, remove_user, 20}, + {hook, register_user, register_user, 20}]}. + +stop(_Host) -> + ok. + +reload(_Host, _NewOpts, _OldOpts) -> + ok. + +depends(_Host, _Opts) -> + []. + +%%==================================================================== +%% Hooks handlers +%%==================================================================== +-spec offline_message_hook({any(), message()}) -> {any(), message()}. +offline_message_hook({_Action, #message{to = #jid{lserver = LServer}}} = Acc) -> + push(LServer, offline_message), + Acc. + +-spec sm_register_connection_hook(ejabberd_sm:sid(), jid(), ejabberd_sm:info()) -> any(). +sm_register_connection_hook(_SID, #jid{lserver=LServer}, _Info) -> + push(LServer, sm_register_connection). + +-spec sm_remove_connection_hook(ejabberd_sm:sid(), jid(), ejabberd_sm:info()) -> any(). +sm_remove_connection_hook(_SID, #jid{lserver=LServer}, _Info) -> + push(LServer, sm_remove_connection). + +-spec user_send_packet({stanza(), ejabberd_c2s:state()}) -> {stanza(), ejabberd_c2s:state()}. +user_send_packet({Packet, #{jid := #jid{lserver = LServer}} = C2SState}) -> + push(LServer, user_send_packet), + {Packet, C2SState}. + +-spec user_receive_packet({stanza(), ejabberd_c2s:state()}) -> {stanza(), ejabberd_c2s:state()}. +user_receive_packet({Packet, #{jid := #jid{lserver = LServer}} = C2SState}) -> + push(LServer, user_receive_packet), + {Packet, C2SState}. + +-spec s2s_send_packet(stanza()) -> stanza(). +s2s_send_packet(Packet) -> + #jid{lserver = LServer} = xmpp:get_from(Packet), + push(LServer, s2s_send_packet), + Packet. + +-spec s2s_receive_packet({stanza(), ejabberd_s2s_in:state()}) -> + {stanza(), ejabberd_s2s_in:state()}. +s2s_receive_packet({Packet, S2SState}) -> + To = xmpp:get_to(Packet), + LServer = ejabberd_router:host_of_route(To#jid.lserver), + push(LServer, s2s_receive_packet), + {Packet, S2SState}. + +-spec remove_user(binary(), binary()) -> any(). +remove_user(_User, Server) -> + push(jid:nameprep(Server), remove_user). + +-spec register_user(binary(), binary()) -> any(). +register_user(_User, Server) -> + push(jid:nameprep(Server), register_user). + +%%==================================================================== +%% metrics push handler +%%==================================================================== +-spec push(binary(), probe()) -> ok | {error, not_owner | inet:posix()}. +push(Host, Probe) -> + IP = mod_metrics_opt:ip(Host), + Port = mod_metrics_opt:port(Host), + send_metrics(Host, Probe, IP, Port). + +-spec send_metrics(binary(), probe(), inet:ip4_address(), inet:port_number()) -> + ok | {error, not_owner | inet:posix()}. +send_metrics(Host, Probe, Peer, Port) -> + % our default metrics handler is https://github.com/processone/grapherl + % grapherl metrics are named first with service domain, then nodename + % and name of the data itself, followed by type timestamp and value + % example => process-one.net/xmpp-1.user_receive_packet:c/1441784958:1 + [_, FQDN] = binary:split(misc:atom_to_binary(node()), <<"@">>), + [Node|_] = binary:split(FQDN, <<".">>), + BaseId = <>, + TS = integer_to_binary(erlang:system_time(second)), + case get_socket(?SOCKET_REGISTER_RETRIES) of + {ok, Socket} -> + case Probe of + {Key, Val} -> + BVal = integer_to_binary(Val), + Data = <>, + gen_udp:send(Socket, Peer, Port, Data); + Key -> + Data = <>, + gen_udp:send(Socket, Peer, Port, Data) + end; + Err -> + Err + end. + +-spec get_socket(integer()) -> {ok, gen_udp:socket()} | {error, inet:posix()}. +get_socket(N) -> + case whereis(?SOCKET_NAME) of + undefined -> + case gen_udp:open(0) of + {ok, Socket} -> + try register(?SOCKET_NAME, Socket) of + true -> {ok, Socket} + catch _:badarg when N > 1 -> + gen_udp:close(Socket), + get_socket(N-1) + end; + {error, Reason} = Err -> + ?ERROR_MSG("Can not open udp socket to grapherl: ~ts", + [inet:format_error(Reason)]), + Err + end; + Socket -> + {ok, Socket} + end. + +mod_opt_type(ip) -> + econf:ipv4(); +mod_opt_type(port) -> + econf:port(). + +mod_options(_) -> + [{ip, {127,0,0,1}}, {port, 11111}]. + +mod_doc() -> + #{desc => + [?T("This module sends events to external backend " + "(by now only https://github.com/processone/grapherl" + "[grapherl] is supported). Supported events are:"), "", + "- sm_register_connection", "", + "- sm_remove_connection", "", + "- user_send_packet", "", + "- user_receive_packet", "", + "- s2s_send_packet", "", + "- s2s_receive_packet", "", + "- register_user", "", + "- remove_user", "", + "- offline_message", "", + ?T("When enabled, every call to these hooks triggers " + "a counter event to be sent to the external backend.")], + opts => + [{ip, + #{value => ?T("IPv4Address"), + desc => + ?T("IPv4 address where the backend is located. " + "The default value is '127.0.0.1'.")}}, + {port, + #{value => ?T("Port"), + desc => + ?T("An internet port number at which the backend " + "is listening for incoming connections/packets. " + "The default value is '11111'.")}}]}. diff --git a/src/mod_metrics_opt.erl b/src/mod_metrics_opt.erl new file mode 100644 index 000000000..22b656775 --- /dev/null +++ b/src/mod_metrics_opt.erl @@ -0,0 +1,20 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_metrics_opt). + +-export([ip/1]). +-export([port/1]). + +-spec ip(gen_mod:opts() | global | binary()) -> {127,0,0,1} | inet:ip4_address(). +ip(Opts) when is_map(Opts) -> + gen_mod:get_opt(ip, Opts); +ip(Host) -> + gen_mod:get_module_opt(Host, mod_metrics, ip). + +-spec port(gen_mod:opts() | global | binary()) -> 1..1114111. +port(Opts) when is_map(Opts) -> + gen_mod:get_opt(port, Opts); +port(Host) -> + gen_mod:get_module_opt(Host, mod_metrics, port). + diff --git a/src/mod_mix.erl b/src/mod_mix.erl new file mode 100644 index 000000000..25216b6fc --- /dev/null +++ b/src/mod_mix.erl @@ -0,0 +1,718 @@ +%%%------------------------------------------------------------------- +%%% File : mod_mix.erl +%%% Author : Evgeny Khramtsov +%%% Created : 2 Mar 2016 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2018 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_mix). +-behaviour(gen_mod). +-behaviour(gen_server). +-protocol({xep, 369, '0.14.1', '16.03', "complete", ""}). + +%% API +-export([route/1]). +%% gen_mod callbacks +-export([start/2, stop/1, reload/3, depends/2, mod_opt_type/1, mod_options/1]). +-export([mod_doc/0]). +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). +%% Hooks +-export([process_disco_info/1, + process_disco_items/1, + process_mix_core/1, + process_mam_query/1, + process_pubsub_query/1]). + +-include_lib("xmpp/include/xmpp.hrl"). +-include("logger.hrl"). +-include("translate.hrl"). + + + +-callback init(binary(), gen_mod:opts()) -> ok | {error, db_failure}. +-callback set_channel(binary(), binary(), binary(), + jid:jid(), boolean(), binary()) -> + ok | {error, db_failure}. +-callback get_channels(binary(), binary()) -> + {ok, [binary()]} | {error, db_failure}. +-callback get_channel(binary(), binary(), binary()) -> + {ok, {jid(), boolean(), binary()}} | + {error, notfound | db_failure}. +-callback set_participant(binary(), binary(), binary(), jid(), binary(), binary()) -> + ok | {error, db_failure}. +-callback get_participant(binary(), binary(), binary(), jid()) -> + {ok, {binary(), binary()}} | {error, notfound | db_failure}. + +-record(state, {hosts :: [binary()], + server_host :: binary()}). + +%%%=================================================================== +%%% API +%%%=================================================================== +start(Host, Opts) -> + gen_mod:start_child(?MODULE, Host, Opts). + +stop(Host) -> + gen_mod:stop_child(?MODULE, Host). + +reload(Host, NewOpts, OldOpts) -> + Proc = gen_mod:get_module_proc(Host, ?MODULE), + gen_server:cast(Proc, {reload, Host, NewOpts, OldOpts}). + +depends(_Host, _Opts) -> + [{mod_mam, hard}]. + +mod_opt_type(access_create) -> + econf:acl(); +mod_opt_type(name) -> + econf:binary(); +mod_opt_type(host) -> + econf:host(); +mod_opt_type(hosts) -> + econf:hosts(); +mod_opt_type(db_type) -> + econf:db_type(?MODULE). + +mod_options(Host) -> + [{access_create, all}, + {host, <<"mix.", Host/binary>>}, + {hosts, []}, + {name, ?T("Channels")}, + {db_type, ejabberd_config:default_db(Host, ?MODULE)}]. + +mod_doc() -> + #{desc => + [?T("This module is an experimental implementation of " + "https://xmpp.org/extensions/xep-0369.html" + "[XEP-0369: Mediated Information eXchange (MIX)]. " + "It's asserted that " + "the MIX protocol is going to replace the MUC protocol " + "in the future (see _`mod_muc`_)."), "", + ?T("To learn more about how to use that feature, you can refer to " + "our tutorial: _`../../tutorials/mix-010.md|Getting started with MIX`_"), "", + ?T("The module depends on _`mod_mam`_.")], + note => "added in 16.03 and improved in 19.02", + opts => + [{access_create, + #{value => ?T("AccessName"), + desc => + ?T("An access rule to control MIX channels creations. " + "The default value is 'all'.")}}, + {host, + #{desc => ?T("Deprecated. Use 'hosts' instead.")}}, + {hosts, + #{value => ?T("[Host, ...]"), + desc => + ?T("This option defines the Jabber IDs of the service. " + "If the 'hosts' option is not specified, the only Jabber ID will " + "be the hostname of the virtual host with the prefix '\"mix.\"'. " + "The keyword '@HOST@' is replaced with the real virtual host name.")}}, + {name, + #{value => ?T("Name"), + desc => + ?T("A name of the service in the Service Discovery. " + "This will only be displayed by special XMPP clients. " + "The default value is 'Channels'.")}}, + {db_type, + #{value => "mnesia | sql", + desc => + ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}]}. + +-spec route(stanza()) -> ok. +route(#iq{} = IQ) -> + ejabberd_router:process_iq(IQ); +route(#message{type = groupchat, id = ID, lang = Lang, + to = #jid{luser = <<_, _/binary>>}} = Msg) -> + case ID of + <<>> -> + Txt = ?T("Attribute 'id' is mandatory for MIX messages"), + Err = xmpp:err_bad_request(Txt, Lang), + ejabberd_router:route_error(Msg, Err); + _ -> + process_mix_message(Msg) + end; +route(Pkt) -> + ?DEBUG("Dropping packet:~n~ts", [xmpp:pp(Pkt)]). + +-spec process_disco_info(iq()) -> iq(). +process_disco_info(#iq{type = set, lang = Lang} = IQ) -> + Txt = ?T("Value 'set' of 'type' attribute is not allowed"), + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); +process_disco_info(#iq{type = get, to = #jid{luser = <<>>} = To, + from = _From, lang = Lang, + sub_els = [#disco_info{node = <<>>}]} = IQ) -> + ServerHost = ejabberd_router:host_of_route(To#jid.lserver), + X = ejabberd_hooks:run_fold(disco_info, ServerHost, [], + [ServerHost, ?MODULE, <<"">>, Lang]), + Name = mod_mix_opt:name(ServerHost), + Identity = #identity{category = <<"conference">>, + type = <<"mix">>, + name = translate:translate(Lang, Name)}, + Features = [?NS_DISCO_INFO, ?NS_DISCO_ITEMS, ?NS_MIX_CORE_0, + ?NS_MIX_CORE_SEARCHABLE_0, ?NS_MIX_CORE_CREATE_CHANNEL_0, + ?NS_MIX_CORE_1, ?NS_MIX_CORE_SEARCHABLE_1, + ?NS_MIX_CORE_CREATE_CHANNEL_1], + xmpp:make_iq_result( + IQ, #disco_info{features = Features, + identities = [Identity], + xdata = X}); +process_disco_info(#iq{type = get, to = #jid{luser = <<_, _/binary>>} = To, + sub_els = [#disco_info{node = Node}]} = IQ) + when Node == <<"mix">>; Node == <<>> -> + {Chan, Host, _} = jid:tolower(To), + ServerHost = ejabberd_router:host_of_route(Host), + Mod = gen_mod:db_mod(ServerHost, ?MODULE), + case Mod:get_channel(ServerHost, Chan, Host) of + {ok, _} -> + Identity = #identity{category = <<"conference">>, + type = <<"mix">>}, + Features = [?NS_DISCO_INFO, ?NS_DISCO_ITEMS, + ?NS_MIX_CORE_0, ?NS_MIX_CORE_1, ?NS_MAM_2], + xmpp:make_iq_result( + IQ, #disco_info{node = Node, + features = Features, + identities = [Identity]}); + {error, notfound} -> + xmpp:make_error(IQ, no_channel_error(IQ)); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) + end; +process_disco_info(#iq{type = get, sub_els = [#disco_info{node = Node}]} = IQ) -> + xmpp:make_iq_result(IQ, #disco_info{node = Node, features = [?NS_DISCO_INFO]}); +process_disco_info(IQ) -> + xmpp:make_error(IQ, unsupported_error(IQ)). + +-spec process_disco_items(iq()) -> iq(). +process_disco_items(#iq{type = set, lang = Lang} = IQ) -> + Txt = ?T("Value 'set' of 'type' attribute is not allowed"), + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); +process_disco_items(#iq{type = get, to = #jid{luser = <<>>} = To, + sub_els = [#disco_items{node = <<>>}]} = IQ) -> + Host = To#jid.lserver, + ServerHost = ejabberd_router:host_of_route(Host), + Mod = gen_mod:db_mod(ServerHost, ?MODULE), + case Mod:get_channels(ServerHost, Host) of + {ok, Channels} -> + Items = [#disco_item{jid = jid:make(Channel, Host)} + || Channel <- Channels], + xmpp:make_iq_result(IQ, #disco_items{items = Items}); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) + end; +process_disco_items(#iq{type = get, to = #jid{luser = <<_, _/binary>>} = To, + sub_els = [#disco_items{node = Node}]} = IQ) + when Node == <<"mix">>; Node == <<>> -> + {Chan, Host, _} = jid:tolower(To), + ServerHost = ejabberd_router:host_of_route(Host), + Mod = gen_mod:db_mod(ServerHost, ?MODULE), + case Mod:get_channel(ServerHost, Chan, Host) of + {ok, _} -> + BTo = jid:remove_resource(To), + Items = [#disco_item{jid = BTo, node = N} || N <- known_nodes()], + xmpp:make_iq_result(IQ, #disco_items{node = Node, items = Items}); + {error, notfound} -> + xmpp:make_error(IQ, no_channel_error(IQ)); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) + end; +process_disco_items(#iq{type = get, sub_els = [#disco_items{node = Node}]} = IQ) -> + xmpp:make_iq_result(IQ, #disco_items{node = Node}); +process_disco_items(IQ) -> + xmpp:make_error(IQ, unsupported_error(IQ)). + +-spec process_mix_core(iq()) -> iq(). +process_mix_core(#iq{type = set, to = #jid{luser = <<>>}, + sub_els = [#mix_create{}]} = IQ) -> + process_mix_create(IQ); +process_mix_core(#iq{type = set, to = #jid{luser = <<>>}, + sub_els = [#mix_destroy{}]} = IQ) -> + process_mix_destroy(IQ); +process_mix_core(#iq{type = set, to = #jid{luser = <<_, _/binary>>}, + sub_els = [#mix_join{}]} = IQ) -> + process_mix_join(IQ); +process_mix_core(#iq{type = set, to = #jid{luser = <<_, _/binary>>}, + sub_els = [#mix_leave{}]} = IQ) -> + process_mix_leave(IQ); +process_mix_core(#iq{type = set, to = #jid{luser = <<_, _/binary>>}, + sub_els = [#mix_setnick{}]} = IQ) -> + process_mix_setnick(IQ); +process_mix_core(IQ) -> + xmpp:make_error(IQ, unsupported_error(IQ)). + +process_pubsub_query(#iq{type = get, + sub_els = [#pubsub{items = #ps_items{node = Node}}]} = IQ) + when Node == ?NS_MIX_NODES_PARTICIPANTS -> + process_participants_list(IQ); +process_pubsub_query(IQ) -> + xmpp:make_error(IQ, unsupported_error(IQ)). + +process_mam_query(#iq{from = From, to = To, type = T, + sub_els = [#mam_query{}]} = IQ) + when T == get; T == set -> + {Chan, Host, _} = jid:tolower(To), + ServerHost = ejabberd_router:host_of_route(Host), + Mod = gen_mod:db_mod(ServerHost, ?MODULE), + case Mod:get_channel(ServerHost, Chan, Host) of + {ok, _} -> + BFrom = jid:remove_resource(From), + case Mod:get_participant(ServerHost, Chan, Host, BFrom) of + {ok, _} -> + mod_mam:process_iq(ServerHost, IQ, mix); + {error, notfound} -> + xmpp:make_error(IQ, not_joined_error(IQ)); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) + end; + {error, notfound} -> + xmpp:make_error(IQ, no_channel_error(IQ)); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) + end; +process_mam_query(IQ) -> + xmpp:make_error(IQ, unsupported_error(IQ)). + +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== +init([Host|_]) -> + process_flag(trap_exit, true), + Opts = gen_mod:get_module_opts(Host, ?MODULE), + Mod = gen_mod:db_mod(Opts, ?MODULE), + MyHosts = gen_mod:get_opt_hosts(Opts), + case Mod:init(Host, gen_mod:set_opt(hosts, MyHosts, Opts)) of + ok -> + lists:foreach( + fun(MyHost) -> + ejabberd_router:register_route( + MyHost, Host, {apply, ?MODULE, route}), + register_iq_handlers(MyHost) + end, MyHosts), + {ok, #state{hosts = MyHosts, server_host = Host}}; + {error, db_failure} -> + {stop, db_failure} + end. + +handle_call(Request, From, State) -> + ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), + {noreply, State}. + +handle_cast(Request, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Request]), + {noreply, State}. + +handle_info({route, Packet}, State) -> + try route(Packet) + catch + Class:Reason:StackTrace -> + ?ERROR_MSG("Failed to route packet:~n~ts~n** ~ts", + [xmpp:pp(Packet), + misc:format_exception(2, Class, Reason, StackTrace)]) + end, + {noreply, State}; +handle_info(Info, State) -> + ?WARNING_MSG("Unexpected info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, State) -> + lists:foreach( + fun(MyHost) -> + unregister_iq_handlers(MyHost), + ejabberd_router:unregister_route(MyHost) + end, State#state.hosts). + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +-spec process_mix_create(iq()) -> iq(). +process_mix_create(#iq{to = To, from = From, + sub_els = [#mix_create{channel = Chan, xmlns = XmlNs}]} = IQ) -> + Host = To#jid.lserver, + ServerHost = ejabberd_router:host_of_route(Host), + Mod = gen_mod:db_mod(ServerHost, ?MODULE), + Creator = jid:remove_resource(From), + Chan1 = case Chan of + <<>> -> p1_rand:get_string(); + _ -> Chan + end, + Ret = case Mod:get_channel(ServerHost, Chan1, Host) of + {ok, {#jid{luser = U, lserver = S}, _, _}} -> + case {From#jid.luser, From#jid.lserver} of + {U, S} -> ok; + _ -> {error, conflict} + end; + {error, notfound} -> + Key = xmpp_util:hex(p1_rand:bytes(20)), + Mod:set_channel(ServerHost, Chan1, Host, + Creator, Chan == <<>>, Key); + {error, db_failure} = Err -> + Err + end, + case Ret of + ok -> + xmpp:make_iq_result(IQ, #mix_create{channel = Chan1, xmlns = XmlNs}); + {error, conflict} -> + xmpp:make_error(IQ, channel_exists_error(IQ)); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) + end. + +-spec process_mix_destroy(iq()) -> iq(). +process_mix_destroy(#iq{to = To, + from = #jid{luser = U, lserver = S}, + sub_els = [#mix_destroy{channel = Chan, xmlns = XmlNs}]} = IQ) -> + Host = To#jid.lserver, + ServerHost = ejabberd_router:host_of_route(Host), + Mod = gen_mod:db_mod(ServerHost, ?MODULE), + case Mod:get_channel(ServerHost, Chan, Host) of + {ok, {#jid{luser = U, lserver = S}, _, _}} -> + case Mod:del_channel(ServerHost, Chan, Host) of + ok -> + xmpp:make_iq_result(IQ, #mix_destroy{channel = Chan, xmlns = XmlNs}); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) + end; + {ok, _} -> + xmpp:make_error(IQ, ownership_error(IQ)); + {error, notfound} -> + xmpp:make_error(IQ, no_channel_error(IQ)); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) + end. + +-spec process_mix_join(iq()) -> iq(). +process_mix_join(#iq{to = To, from = From, + sub_els = [#mix_join{xmlns = XmlNs} = JoinReq]} = IQ) -> + Chan = To#jid.luser, + Host = To#jid.lserver, + ServerHost = ejabberd_router:host_of_route(Host), + Mod = gen_mod:db_mod(ServerHost, ?MODULE), + case Mod:get_channel(ServerHost, Chan, Host) of + {ok, {_, _, Key}} -> + ID = make_id(From, Key), + Nick = JoinReq#mix_join.nick, + BFrom = jid:remove_resource(From), + Nodes = filter_nodes(JoinReq#mix_join.subscribe), + try + ok = Mod:set_participant(ServerHost, Chan, Host, BFrom, ID, Nick), + ok = Mod:subscribe(ServerHost, Chan, Host, BFrom, Nodes), + notify_participant_joined(Mod, ServerHost, To, From, ID, Nick), + xmpp:make_iq_result(IQ, #mix_join{id = ID, + subscribe = Nodes, + jid = make_channel_id(To, ID), + nick = Nick, + xmlns = XmlNs}) + catch _:{badmatch, {error, db_failure}} -> + xmpp:make_error(IQ, db_error(IQ)) + end; + {error, notfound} -> + xmpp:make_error(IQ, no_channel_error(IQ)); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) + end. + +-spec process_mix_leave(iq()) -> iq(). +process_mix_leave(#iq{to = To, from = From, + sub_els = [#mix_leave{xmlns = XmlNs}]} = IQ) -> + {Chan, Host, _} = jid:tolower(To), + ServerHost = ejabberd_router:host_of_route(Host), + Mod = gen_mod:db_mod(ServerHost, ?MODULE), + BFrom = jid:remove_resource(From), + case Mod:get_channel(ServerHost, Chan, Host) of + {ok, _} -> + case Mod:get_participant(ServerHost, Chan, Host, BFrom) of + {ok, {ID, _}} -> + try + ok = Mod:unsubscribe(ServerHost, Chan, Host, BFrom), + ok = Mod:del_participant(ServerHost, Chan, Host, BFrom), + notify_participant_left(Mod, ServerHost, To, ID), + xmpp:make_iq_result(IQ, #mix_leave{}) + catch _:{badmatch, {error, db_failure}} -> + xmpp:make_error(IQ, db_error(IQ)) + end; + {error, notfound} -> + xmpp:make_iq_result(IQ, #mix_leave{xmlns = XmlNs}); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) + end; + {error, notfound} -> + xmpp:make_iq_result(IQ, #mix_leave{}); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) + end. + +-spec process_mix_setnick(iq()) -> iq(). +process_mix_setnick(#iq{to = To, from = From, + sub_els = [#mix_setnick{nick = Nick, xmlns = XmlNs}]} = IQ) -> + {Chan, Host, _} = jid:tolower(To), + ServerHost = ejabberd_router:host_of_route(Host), + Mod = gen_mod:db_mod(ServerHost, ?MODULE), + BFrom = jid:remove_resource(From), + case Mod:get_channel(ServerHost, Chan, Host) of + {ok, _} -> + case Mod:get_participant(ServerHost, Chan, Host, BFrom) of + {ok, {_, Nick}} -> + xmpp:make_iq_result(IQ, #mix_setnick{nick = Nick, xmlns = XmlNs}); + {ok, {ID, _}} -> + case Mod:set_participant(ServerHost, Chan, Host, BFrom, ID, Nick) of + ok -> + notify_participant_joined(Mod, ServerHost, To, From, ID, Nick), + xmpp:make_iq_result(IQ, #mix_setnick{nick = Nick, xmlns = XmlNs}); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) + end; + {error, notfound} -> + xmpp:make_error(IQ, not_joined_error(IQ)); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) + end; + {error, notfound} -> + xmpp:make_error(IQ, no_channel_error(IQ)); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) + end. + +-spec process_mix_message(message()) -> ok. +process_mix_message(#message{from = From, to = To, + id = SubmissionID} = Msg) -> + {Chan, Host, _} = jid:tolower(To), + {FUser, FServer, _} = jid:tolower(From), + ServerHost = ejabberd_router:host_of_route(Host), + Mod = gen_mod:db_mod(ServerHost, ?MODULE), + case Mod:get_channel(ServerHost, Chan, Host) of + {ok, _} -> + BFrom = jid:remove_resource(From), + case Mod:get_participant(ServerHost, Chan, Host, BFrom) of + {ok, {StableID, Nick}} -> + MamID = mod_mam:make_id(), + Msg1 = xmpp:set_subtag( + Msg#message{from = jid:replace_resource(To, StableID), + to = undefined, + id = integer_to_binary(MamID)}, + #mix{jid = BFrom, nick = Nick}), + Msg2 = xmpp:put_meta(Msg1, stanza_id, MamID), + case ejabberd_hooks:run_fold( + store_mam_message, ServerHost, Msg2, + [Chan, Host, BFrom, Nick, groupchat, recv]) of + #message{} -> + multicast(Mod, ServerHost, Chan, Host, + ?NS_MIX_NODES_MESSAGES, + fun(#jid{luser = U, lserver = S}) + when U == FUser, S == FServer -> + xmpp:set_subtag( + Msg1, #mix{jid = BFrom, + nick = Nick, + submission_id = SubmissionID}); + (_) -> + Msg1 + end); + _ -> + ok + end; + {error, notfound} -> + ejabberd_router:route_error(Msg, not_joined_error(Msg)); + {error, db_failure} -> + ejabberd_router:route_error(Msg, db_error(Msg)) + end; + {error, notfound} -> + ejabberd_router:route_error(Msg, no_channel_error(Msg)); + {error, db_failure} -> + ejabberd_router:route_error(Msg, db_error(Msg)) + end. + +-spec process_participants_list(iq()) -> iq(). +process_participants_list(#iq{from = From, to = To} = IQ) -> + {Chan, Host, _} = jid:tolower(To), + ServerHost = ejabberd_router:host_of_route(Host), + Mod = gen_mod:db_mod(ServerHost, ?MODULE), + case Mod:get_channel(ServerHost, Chan, Host) of + {ok, _} -> + BFrom = jid:remove_resource(From), + case Mod:get_participant(ServerHost, Chan, Host, BFrom) of + {ok, _} -> + case Mod:get_participants(ServerHost, Chan, Host) of + {ok, Participants} -> + Items = items_of_participants(Participants), + Pubsub = #pubsub{ + items = #ps_items{ + node = ?NS_MIX_NODES_PARTICIPANTS, + items = Items}}, + xmpp:make_iq_result(IQ, Pubsub); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) + end; + {error, notfound} -> + xmpp:make_error(IQ, not_joined_error(IQ)); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) + end; + {error, notfound} -> + xmpp:make_error(IQ, no_channel_error(IQ)); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) + end. + +-spec items_of_participants([{jid(), binary(), binary()}]) -> [ps_item()]. +items_of_participants(Participants) -> + lists:map( + fun({JID, ID, Nick}) -> + Participant = #mix_participant{jid = JID, nick = Nick}, + #ps_item{id = ID, + sub_els = [xmpp:encode(Participant)]} + end, Participants). + +-spec known_nodes() -> [binary()]. +known_nodes() -> + [?NS_MIX_NODES_MESSAGES, + ?NS_MIX_NODES_PARTICIPANTS]. + +-spec filter_nodes([binary()]) -> [binary()]. +filter_nodes(Nodes) -> + KnownNodes = known_nodes(), + [Node || KnownNode <- KnownNodes, Node <- Nodes, KnownNode == Node]. + +-spec multicast(module(), binary(), binary(), + binary(), binary(), fun((jid()) -> message())) -> ok. +multicast(Mod, LServer, Chan, Service, Node, F) -> + case Mod:get_subscribed(LServer, Chan, Service, Node) of + {ok, Subscribers} -> + lists:foreach( + fun(To) -> + Msg = xmpp:set_to(F(To), To), + ejabberd_router:route(Msg) + end, Subscribers); + {error, db_failure} -> + ok + end. + +-spec notify_participant_joined(module(), binary(), + jid(), jid(), binary(), binary()) -> ok. +notify_participant_joined(Mod, LServer, To, From, ID, Nick) -> + {Chan, Host, _} = jid:tolower(To), + Participant = #mix_participant{jid = jid:remove_resource(From), + nick = Nick}, + Item = #ps_item{id = ID, + sub_els = [xmpp:encode(Participant)]}, + Items = #ps_items{node = ?NS_MIX_NODES_PARTICIPANTS, + items = [Item]}, + Event = #ps_event{items = Items}, + Msg = #message{from = jid:remove_resource(To), + id = p1_rand:get_string(), + sub_els = [Event]}, + multicast(Mod, LServer, Chan, Host, + ?NS_MIX_NODES_PARTICIPANTS, + fun(_) -> Msg end). + +-spec notify_participant_left(module(), binary(), jid(), binary()) -> ok. +notify_participant_left(Mod, LServer, To, ID) -> + {Chan, Host, _} = jid:tolower(To), + Items = #ps_items{node = ?NS_MIX_NODES_PARTICIPANTS, + retract = [ID]}, + Event = #ps_event{items = Items}, + Msg = #message{from = jid:remove_resource(To), + id = p1_rand:get_string(), + sub_els = [Event]}, + multicast(Mod, LServer, Chan, Host, ?NS_MIX_NODES_PARTICIPANTS, + fun(_) -> Msg end). + +-spec make_id(jid(), binary()) -> binary(). +make_id(JID, Key) -> + Data = jid:encode(jid:tolower(jid:remove_resource(JID))), + xmpp_util:hex(misc:crypto_hmac(sha256, Data, Key, 10)). + +-spec make_channel_id(jid(), binary()) -> jid(). +make_channel_id(JID, ID) -> + {U, S, R} = jid:split(JID), + jid:make(<>, S, R). + +%%%=================================================================== +%%% Error generators +%%%=================================================================== +-spec db_error(stanza()) -> stanza_error(). +db_error(Pkt) -> + Txt = ?T("Database failure"), + xmpp:err_internal_server_error(Txt, xmpp:get_lang(Pkt)). + +-spec channel_exists_error(stanza()) -> stanza_error(). +channel_exists_error(Pkt) -> + Txt = ?T("Channel already exists"), + xmpp:err_conflict(Txt, xmpp:get_lang(Pkt)). + +-spec no_channel_error(stanza()) -> stanza_error(). +no_channel_error(Pkt) -> + Txt = ?T("Channel does not exist"), + xmpp:err_item_not_found(Txt, xmpp:get_lang(Pkt)). + +-spec not_joined_error(stanza()) -> stanza_error(). +not_joined_error(Pkt) -> + Txt = ?T("You are not joined to the channel"), + xmpp:err_forbidden(Txt, xmpp:get_lang(Pkt)). + +-spec unsupported_error(stanza()) -> stanza_error(). +unsupported_error(Pkt) -> + Txt = ?T("No module is handling this query"), + xmpp:err_service_unavailable(Txt, xmpp:get_lang(Pkt)). + +-spec ownership_error(stanza()) -> stanza_error(). +ownership_error(Pkt) -> + Txt = ?T("Owner privileges required"), + xmpp:err_forbidden(Txt, xmpp:get_lang(Pkt)). + +%%%=================================================================== +%%% IQ handlers +%%%=================================================================== +-spec register_iq_handlers(binary()) -> ok. +register_iq_handlers(Host) -> + gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO, + ?MODULE, process_disco_info), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_DISCO_ITEMS, + ?MODULE, process_disco_items), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_MIX_CORE_0, + ?MODULE, process_mix_core), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_MIX_CORE_1, + ?MODULE, process_mix_core), + gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_DISCO_INFO, + ?MODULE, process_disco_info), + gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_DISCO_ITEMS, + ?MODULE, process_disco_items), + gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_MIX_CORE_0, + ?MODULE, process_mix_core), + gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_MIX_CORE_1, + ?MODULE, process_mix_core), + gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_PUBSUB, + ?MODULE, process_pubsub_query), + gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_MAM_2, + ?MODULE, process_mam_query). + +-spec unregister_iq_handlers(binary()) -> ok. +unregister_iq_handlers(Host) -> + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_ITEMS), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_MIX_CORE_0), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_MIX_CORE_1), + gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_DISCO_INFO), + gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_DISCO_ITEMS), + gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_MIX_CORE_0), + gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_MIX_CORE_1), + gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_PUBSUB), + gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_MAM_2). diff --git a/src/mod_mix_mnesia.erl b/src/mod_mix_mnesia.erl new file mode 100644 index 000000000..0d3a4d20c --- /dev/null +++ b/src/mod_mix_mnesia.erl @@ -0,0 +1,189 @@ +%%%------------------------------------------------------------------- +%%% Created : 1 Dec 2018 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2018 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_mix_mnesia). +-behaviour(mod_mix). + +%% API +-export([init/2]). +-export([set_channel/6, get_channels/2, get_channel/3, del_channel/3]). +-export([set_participant/6, get_participant/4, get_participants/3, del_participant/4]). +-export([subscribe/5, unsubscribe/4, unsubscribe/5, get_subscribed/4]). + +-include("logger.hrl"). +-include("ejabberd_sql_pt.hrl"). + +-record(mix_channel, + {chan_serv :: {binary(), binary()}, + service :: binary(), + creator :: jid:jid(), + hidden :: boolean(), + hmac_key :: binary(), + created_at :: erlang:timestamp()}). + +-record(mix_participant, + {user_chan :: {binary(), binary(), binary(), binary()}, + chan_serv :: {binary(), binary()}, + jid :: jid:jid(), + id :: binary(), + nick :: binary(), + created_at :: erlang:timestamp()}). + +-record(mix_subscription, + {user_chan_node :: {binary(), binary(), binary(), binary(), binary()}, + user_chan :: {binary(), binary(), binary(), binary()}, + chan_serv_node :: {binary(), binary(), binary()}, + chan_serv :: {binary(), binary()}, + jid :: jid:jid()}). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + try + {atomic, _} = ejabberd_mnesia:create( + ?MODULE, mix_channel, + [{disc_only_copies, [node()]}, + {attributes, record_info(fields, mix_channel)}, + {index, [service]}]), + {atomic, _} = ejabberd_mnesia:create( + ?MODULE, mix_participant, + [{disc_only_copies, [node()]}, + {attributes, record_info(fields, mix_participant)}, + {index, [chan_serv]}]), + {atomic, _} = ejabberd_mnesia:create( + ?MODULE, mix_subscription, + [{disc_only_copies, [node()]}, + {attributes, record_info(fields, mix_subscription)}, + {index, [user_chan, chan_serv_node, chan_serv]}]), + ok + catch _:{badmatch, _} -> + {error, db_failure} + end. + +set_channel(_LServer, Channel, Service, CreatorJID, Hidden, Key) -> + mnesia:dirty_write( + #mix_channel{chan_serv = {Channel, Service}, + service = Service, + creator = jid:remove_resource(CreatorJID), + hidden = Hidden, + hmac_key = Key, + created_at = erlang:timestamp()}). + +get_channels(_LServer, Service) -> + Ret = mnesia:dirty_index_read(mix_channel, Service, #mix_channel.service), + {ok, lists:filtermap( + fun(#mix_channel{chan_serv = {Channel, _}, + hidden = false}) -> + {true, Channel}; + (_) -> + false + end, Ret)}. + +get_channel(_LServer, Channel, Service) -> + case mnesia:dirty_read(mix_channel, {Channel, Service}) of + [#mix_channel{creator = JID, + hidden = Hidden, + hmac_key = Key}] -> + {ok, {JID, Hidden, Key}}; + [] -> + {error, notfound} + end. + +del_channel(_LServer, Channel, Service) -> + Key = {Channel, Service}, + L1 = mnesia:dirty_read(mix_channel, Key), + L2 = mnesia:dirty_index_read(mix_participant, Key, + #mix_participant.chan_serv), + L3 = mnesia:dirty_index_read(mix_subscription, Key, + #mix_subscription.chan_serv), + lists:foreach(fun mnesia:dirty_delete_object/1, L1++L2++L3). + +set_participant(_LServer, Channel, Service, JID, ID, Nick) -> + {User, Domain, _} = jid:tolower(JID), + mnesia:dirty_write( + #mix_participant{ + user_chan = {User, Domain, Channel, Service}, + chan_serv = {Channel, Service}, + jid = jid:remove_resource(JID), + id = ID, + nick = Nick, + created_at = erlang:timestamp()}). + +-spec get_participant(binary(), binary(), binary(), jid:jid()) -> {ok, {binary(), binary()}} | {error, notfound}. +get_participant(_LServer, Channel, Service, JID) -> + {User, Domain, _} = jid:tolower(JID), + case mnesia:dirty_read(mix_participant, {User, Domain, Channel, Service}) of + [#mix_participant{id = ID, nick = Nick}] -> {ok, {ID, Nick}}; + [] -> {error, notfound} + end. + +get_participants(_LServer, Channel, Service) -> + Ret = mnesia:dirty_index_read(mix_participant, + {Channel, Service}, + #mix_participant.chan_serv), + {ok, lists:map( + fun(#mix_participant{jid = JID, id = ID, nick = Nick}) -> + {JID, ID, Nick} + end, Ret)}. + +del_participant(_LServer, Channel, Service, JID) -> + {User, Domain, _} = jid:tolower(JID), + mnesia:dirty_delete(mix_participant, {User, Domain, Channel, Service}). + +subscribe(_LServer, Channel, Service, JID, Nodes) -> + {User, Domain, _} = jid:tolower(JID), + BJID = jid:remove_resource(JID), + lists:foreach( + fun(Node) -> + mnesia:dirty_write( + #mix_subscription{ + user_chan_node = {User, Domain, Channel, Service, Node}, + user_chan = {User, Domain, Channel, Service}, + chan_serv_node = {Channel, Service, Node}, + chan_serv = {Channel, Service}, + jid = BJID}) + end, Nodes). + +get_subscribed(_LServer, Channel, Service, Node) -> + Ret = mnesia:dirty_index_read(mix_subscription, + {Channel, Service, Node}, + #mix_subscription.chan_serv_node), + {ok, [JID || #mix_subscription{jid = JID} <- Ret]}. + +unsubscribe(_LServer, Channel, Service, JID) -> + {User, Domain, _} = jid:tolower(JID), + Ret = mnesia:dirty_index_read(mix_subscription, + {User, Domain, Channel, Service}, + #mix_subscription.user_chan), + lists:foreach(fun mnesia:dirty_delete_object/1, Ret). + +unsubscribe(_LServer, Channel, Service, JID, Nodes) -> + {User, Domain, _} = jid:tolower(JID), + lists:foreach( + fun(Node) -> + mnesia:dirty_delete(mix_subscription, + {User, Domain, Channel, Service, Node}) + end, Nodes). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== diff --git a/src/mod_mix_opt.erl b/src/mod_mix_opt.erl new file mode 100644 index 000000000..b8225b19e --- /dev/null +++ b/src/mod_mix_opt.erl @@ -0,0 +1,41 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_mix_opt). + +-export([access_create/1]). +-export([db_type/1]). +-export([host/1]). +-export([hosts/1]). +-export([name/1]). + +-spec access_create(gen_mod:opts() | global | binary()) -> 'all' | acl:acl(). +access_create(Opts) when is_map(Opts) -> + gen_mod:get_opt(access_create, Opts); +access_create(Host) -> + gen_mod:get_module_opt(Host, mod_mix, access_create). + +-spec db_type(gen_mod:opts() | global | binary()) -> atom(). +db_type(Opts) when is_map(Opts) -> + gen_mod:get_opt(db_type, Opts); +db_type(Host) -> + gen_mod:get_module_opt(Host, mod_mix, db_type). + +-spec host(gen_mod:opts() | global | binary()) -> binary(). +host(Opts) when is_map(Opts) -> + gen_mod:get_opt(host, Opts); +host(Host) -> + gen_mod:get_module_opt(Host, mod_mix, host). + +-spec hosts(gen_mod:opts() | global | binary()) -> [binary()]. +hosts(Opts) when is_map(Opts) -> + gen_mod:get_opt(hosts, Opts); +hosts(Host) -> + gen_mod:get_module_opt(Host, mod_mix, hosts). + +-spec name(gen_mod:opts() | global | binary()) -> binary(). +name(Opts) when is_map(Opts) -> + gen_mod:get_opt(name, Opts); +name(Host) -> + gen_mod:get_module_opt(Host, mod_mix, name). + diff --git a/src/mod_mix_pam.erl b/src/mod_mix_pam.erl new file mode 100644 index 000000000..bae6133fb --- /dev/null +++ b/src/mod_mix_pam.erl @@ -0,0 +1,515 @@ +%%%------------------------------------------------------------------- +%%% Author : Evgeny Khramtsov +%%% Created : 4 Dec 2018 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2018 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_mix_pam). +-behaviour(gen_mod). +-protocol({xep, 405, '0.3.0', '19.02', "complete", ""}). + +%% gen_mod callbacks +-export([start/2, stop/1, reload/3, depends/2, mod_opt_type/1, mod_options/1]). +-export([mod_doc/0]). +%% Hooks and handlers +-export([bounce_sm_packet/1, + disco_sm_features/5, + remove_user/2, + process_iq/1, + get_mix_roster_items/2, + webadmin_user/4, + webadmin_menu_hostuser/4, webadmin_page_hostuser/4]). + +-include_lib("xmpp/include/xmpp.hrl"). +-include("logger.hrl"). +-include("mod_roster.hrl"). +-include("translate.hrl"). +-include("ejabberd_http.hrl"). +-include("ejabberd_web_admin.hrl"). + +-define(MIX_PAM_CACHE, mix_pam_cache). + +-callback init(binary(), gen_mod:opts()) -> ok | {error, db_failure}. +-callback add_channel(jid(), jid(), binary()) -> ok | {error, db_failure}. +-callback del_channel(jid(), jid()) -> ok | {error, db_failure}. +-callback get_channel(jid(), jid()) -> {ok, binary()} | {error, notfound | db_failure}. +-callback get_channels(jid()) -> {ok, [{jid(), binary()}]} | {error, db_failure}. +-callback del_channels(jid()) -> ok | {error, db_failure}. +-callback use_cache(binary()) -> boolean(). +-callback cache_nodes(binary()) -> [node()]. + +-optional_callbacks([use_cache/1, cache_nodes/1]). + +%%%=================================================================== +%%% API +%%%=================================================================== +start(Host, Opts) -> + Mod = gen_mod:db_mod(Opts, ?MODULE), + case Mod:init(Host, Opts) of + ok -> + init_cache(Mod, Host, Opts), + {ok, + [{hook, bounce_sm_packet, bounce_sm_packet, 50}, + {hook, disco_sm_features, disco_sm_features, 50}, + {hook, remove_user, remove_user, 50}, + {hook, roster_get, get_mix_roster_items, 50}, + {hook, webadmin_user, webadmin_user, 50}, + {hook, webadmin_menu_hostuser, webadmin_menu_hostuser, 50}, + {hook, webadmin_page_hostuser, webadmin_page_hostuser, 50}, + {iq_handler, ejabberd_sm, ?NS_MIX_PAM_0, process_iq}, + {iq_handler, ejabberd_sm, ?NS_MIX_PAM_2, process_iq}]}; + Err -> + Err + end. + +stop(_Host) -> + ok. + +reload(Host, NewOpts, OldOpts) -> + NewMod = gen_mod:db_mod(NewOpts, ?MODULE), + OldMod = gen_mod:db_mod(OldOpts, ?MODULE), + if NewMod /= OldMod -> + NewMod:init(Host, NewOpts); + true -> + ok + end, + init_cache(NewMod, Host, NewOpts). + +depends(_Host, _Opts) -> + []. + +mod_opt_type(db_type) -> + econf:db_type(?MODULE); +mod_opt_type(use_cache) -> + econf:bool(); +mod_opt_type(cache_size) -> + econf:pos_int(infinity); +mod_opt_type(cache_missed) -> + econf:bool(); +mod_opt_type(cache_life_time) -> + econf:timeout(second, infinity). + +mod_options(Host) -> + [{db_type, ejabberd_config:default_db(Host, ?MODULE)}, + {use_cache, ejabberd_option:use_cache(Host)}, + {cache_size, ejabberd_option:cache_size(Host)}, + {cache_missed, ejabberd_option:cache_missed(Host)}, + {cache_life_time, ejabberd_option:cache_life_time(Host)}]. + +mod_doc() -> + #{desc => + [?T("This module implements " + "https://xmpp.org/extensions/xep-0405.html" + "[XEP-0405: Mediated Information eXchange (MIX): " + "Participant Server Requirements]. " + "The module is needed if MIX compatible clients " + "on your server are going to join MIX channels " + "(either on your server or on any remote servers)."), "", + ?T("NOTE: _`mod_mix`_ is not required for this module " + "to work, however, without 'mod_mix_pam' the MIX " + "functionality of your local XMPP clients will be impaired.")], + opts => + [{db_type, + #{value => "mnesia | sql", + desc => + ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}, + {use_cache, + #{value => "true | false", + desc => + ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, + {cache_size, + #{value => "pos_integer() | infinity", + desc => + ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, + {cache_missed, + #{value => "true | false", + desc => + ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, + {cache_life_time, + #{value => "timeout()", + desc => + ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}]}. + +-spec bounce_sm_packet({term(), stanza()}) -> {term(), stanza()}. +bounce_sm_packet({_, #message{to = #jid{lresource = <<>>} = To, + from = From, + type = groupchat} = Msg} = Acc) -> + case xmpp:has_subtag(Msg, #mix{}) of + true -> + {LUser, LServer, _} = jid:tolower(To), + case get_channel(To, From) of + {ok, _} -> + lists:foreach( + fun(R) -> + To1 = jid:replace_resource(To, R), + ejabberd_router:route(xmpp:set_to(Msg, To1)) + end, ejabberd_sm:get_user_resources(LUser, LServer)), + {pass, Msg}; + _ -> + Acc + end; + false -> + Acc + end; +bounce_sm_packet(Acc) -> + Acc. + +-spec disco_sm_features({error, stanza_error()} | empty | {result, [binary()]}, + jid(), jid(), binary(), binary()) -> + {error, stanza_error()} | empty | {result, [binary()]}. +disco_sm_features({error, _Error} = Acc, _From, _To, _Node, _Lang) -> + Acc; +disco_sm_features(Acc, _From, _To, <<"">>, _Lang) -> + {result, [?NS_MIX_PAM_0, ?NS_MIX_PAM_2 | + case Acc of + {result, Features} -> Features; + empty -> [] + end]}; +disco_sm_features(Acc, _From, _To, _Node, _Lang) -> + Acc. + +-spec process_iq(iq()) -> iq() | ignore. +process_iq(#iq{from = #jid{luser = U1, lserver = S1}, + to = #jid{luser = U2, lserver = S2}} = IQ) + when {U1, S1} /= {U2, S2} -> + xmpp:make_error(IQ, forbidden_query_error(IQ)); +process_iq(#iq{type = set, + sub_els = [#mix_client_join{} = Join]} = IQ) -> + case Join#mix_client_join.channel of + undefined -> + xmpp:make_error(IQ, missing_channel_error(IQ)); + _ -> + process_join(IQ) + end; +process_iq(#iq{type = set, + sub_els = [#mix_client_leave{} = Leave]} = IQ) -> + case Leave#mix_client_leave.channel of + undefined -> + xmpp:make_error(IQ, missing_channel_error(IQ)); + _ -> + process_leave(IQ) + end; +process_iq(IQ) -> + xmpp:make_error(IQ, unsupported_query_error(IQ)). + +-spec get_mix_roster_items([#roster_item{}], {binary(), binary()}) -> [#roster_item{}]. +get_mix_roster_items(Acc, {LUser, LServer}) -> + JID = jid:make(LUser, LServer), + case get_channels(JID) of + {ok, Channels} -> + lists:map( + fun({ItemJID, Id}) -> + #roster_item{ + jid = ItemJID, + name = <<>>, + subscription = both, + ask = undefined, + groups = [], + mix_channel = #mix_roster_channel{participant_id = Id} + } + end, Channels); + _ -> + [] + end ++ Acc. + +-spec remove_user(binary(), binary()) -> ok | {error, db_failure}. +remove_user(LUser, LServer) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + JID = jid:make(LUser, LServer), + Chans = case Mod:get_channels(JID) of + {ok, Channels} -> + lists:map( + fun({Channel, _}) -> + ejabberd_router:route( + #iq{from = JID, + to = Channel, + id = p1_rand:get_string(), + type = set, + sub_els = [#mix_leave{}]}), + Channel + end, Channels); + _ -> + [] + end, + Mod:del_channels(jid:make(LUser, LServer)), + lists:foreach( + fun(Chan) -> + delete_cache(Mod, JID, Chan) + end, Chans). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +-spec process_join(iq()) -> ignore. +process_join(#iq{from = From, lang = Lang, + sub_els = [#mix_client_join{channel = Channel, + join = Join}]} = IQ) -> + ejabberd_router:route_iq( + #iq{from = jid:remove_resource(From), + to = Channel, type = set, sub_els = [Join]}, + fun(#iq{sub_els = [El]} = ResIQ) -> + try xmpp:decode(El) of + MixJoin -> + process_join_result(ResIQ#iq { + sub_els = [MixJoin] + }, IQ) + catch + _:{xmpp_codec, Reason} -> + Txt = xmpp:io_format_error(Reason), + Err = xmpp:err_bad_request(Txt, Lang), + ejabberd_router:route_error(IQ, Err) + end + end), + ignore. + +-spec process_leave(iq()) -> iq() | error. +process_leave(#iq{from = From, + sub_els = [#mix_client_leave{channel = Channel, + leave = Leave}]} = IQ) -> + case del_channel(From, Channel) of + ok -> + ejabberd_router:route_iq( + #iq{from = jid:remove_resource(From), + to = Channel, type = set, sub_els = [Leave]}, + fun(ResIQ) -> process_leave_result(ResIQ, IQ) end), + ignore; + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) + end. + +-spec process_join_result(iq(), iq()) -> ok. +process_join_result(#iq{from = #jid{} = Channel, + type = result, sub_els = [#mix_join{id = ID, xmlns = XmlNs} = Join]}, + #iq{to = To} = IQ) -> + case add_channel(To, Channel, ID) of + ok -> + % Do roster push + mod_roster:push_item(To, #roster_item{jid = #jid{}}, #roster_item{ + jid = Channel, + name = <<>>, + subscription = none, + ask = undefined, + groups = [], + mix_channel = #mix_roster_channel{participant_id = ID} + }), + % send IQ result + ChanID = make_channel_id(Channel, ID), + Join1 = Join#mix_join{id = <<"">>, jid = ChanID}, + ResIQ = xmpp:make_iq_result(IQ, #mix_client_join{join = Join1, xmlns = XmlNs}), + ejabberd_router:route(ResIQ); + {error, db_failure} -> + ejabberd_router:route_error(IQ, db_error(IQ)) + end; +process_join_result(#iq{type = error} = Err, IQ) -> + process_iq_error(Err, IQ). + +-spec process_leave_result(iq(), iq()) -> ok. +process_leave_result(#iq{from = Channel, type = result, sub_els = [#mix_leave{xmlns = XmlNs} = Leave]}, + #iq{to = User} = IQ) -> + % Do roster push + mod_roster:push_item(User, + #roster_item{jid = Channel, subscription = none}, + #roster_item{jid = Channel, subscription = remove}), + % send iq result + ResIQ = xmpp:make_iq_result(IQ, #mix_client_leave{leave = Leave, xmlns = XmlNs}), + ejabberd_router:route(ResIQ); +process_leave_result(Err, IQ) -> + process_iq_error(Err, IQ). + +-spec process_iq_error(iq(), iq()) -> ok. +process_iq_error(#iq{type = error} = ErrIQ, #iq{sub_els = [El]} = IQ) -> + case xmpp:get_error(ErrIQ) of + undefined -> + %% Not sure if this stuff is correct because + %% RFC6120 section 8.3.1 bullet 4 states that + %% an error stanza MUST contain an child element + IQ1 = xmpp:make_iq_result(IQ, El), + ejabberd_router:route(IQ1#iq{type = error}); + Err -> + ejabberd_router:route_error(IQ, Err) + end; +process_iq_error(timeout, IQ) -> + Txt = ?T("Request has timed out"), + Err = xmpp:err_recipient_unavailable(Txt, IQ#iq.lang), + ejabberd_router:route_error(IQ, Err). + +-spec make_channel_id(jid(), binary()) -> jid(). +make_channel_id(JID, ID) -> + {U, S, R} = jid:split(JID), + jid:make(<>, S, R). + +%%%=================================================================== +%%% Error generators +%%%=================================================================== +-spec missing_channel_error(stanza()) -> stanza_error(). +missing_channel_error(Pkt) -> + Txt = ?T("Attribute 'channel' is required for this request"), + xmpp:err_bad_request(Txt, xmpp:get_lang(Pkt)). + +-spec forbidden_query_error(stanza()) -> stanza_error(). +forbidden_query_error(Pkt) -> + Txt = ?T("Query to another users is forbidden"), + xmpp:err_forbidden(Txt, xmpp:get_lang(Pkt)). + +-spec unsupported_query_error(stanza()) -> stanza_error(). +unsupported_query_error(Pkt) -> + Txt = ?T("No module is handling this query"), + xmpp:err_service_unavailable(Txt, xmpp:get_lang(Pkt)). + +-spec db_error(stanza()) -> stanza_error(). +db_error(Pkt) -> + Txt = ?T("Database failure"), + xmpp:err_internal_server_error(Txt, xmpp:get_lang(Pkt)). + +%%%=================================================================== +%%% Database queries +%%%=================================================================== +get_channel(JID, Channel) -> + {LUser, LServer, _} = jid:tolower(JID), + {Chan, Service, _} = jid:tolower(Channel), + Mod = gen_mod:db_mod(LServer, ?MODULE), + case use_cache(Mod, LServer) of + false -> Mod:get_channel(JID, Channel); + true -> + case ets_cache:lookup( + ?MIX_PAM_CACHE, {LUser, LServer, Chan, Service}, + fun() -> Mod:get_channel(JID, Channel) end) of + error -> {error, notfound}; + Ret -> Ret + end + end. + +get_channels(JID) -> + {_, LServer, _} = jid:tolower(JID), + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:get_channels(JID). + +add_channel(JID, Channel, ID) -> + Mod = gen_mod:db_mod(JID#jid.lserver, ?MODULE), + case Mod:add_channel(JID, Channel, ID) of + ok -> delete_cache(Mod, JID, Channel); + Err -> Err + end. + +del_channel(JID, Channel) -> + Mod = gen_mod:db_mod(JID#jid.lserver, ?MODULE), + case Mod:del_channel(JID, Channel) of + ok -> delete_cache(Mod, JID, Channel); + Err -> Err + end. + +%%%=================================================================== +%%% Cache management +%%%=================================================================== +-spec init_cache(module(), binary(), gen_mod:opts()) -> ok. +init_cache(Mod, Host, Opts) -> + case use_cache(Mod, Host) of + true -> + CacheOpts = cache_opts(Opts), + ets_cache:new(?MIX_PAM_CACHE, CacheOpts); + false -> + ets_cache:delete(?MIX_PAM_CACHE) + end. + +-spec cache_opts(gen_mod:opts()) -> [proplists:property()]. +cache_opts(Opts) -> + MaxSize = mod_mix_pam_opt:cache_size(Opts), + CacheMissed = mod_mix_pam_opt:cache_missed(Opts), + LifeTime = mod_mix_pam_opt:cache_life_time(Opts), + [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}]. + +-spec use_cache(module(), binary()) -> boolean(). +use_cache(Mod, Host) -> + case erlang:function_exported(Mod, use_cache, 1) of + true -> Mod:use_cache(Host); + false -> mod_mix_pam_opt:use_cache(Host) + end. + +-spec cache_nodes(module(), binary()) -> [node()]. +cache_nodes(Mod, Host) -> + case erlang:function_exported(Mod, cache_nodes, 1) of + true -> Mod:cache_nodes(Host); + false -> ejabberd_cluster:get_nodes() + end. + +-spec delete_cache(module(), jid(), jid()) -> ok. +delete_cache(Mod, JID, Channel) -> + {LUser, LServer, _} = jid:tolower(JID), + {Chan, Service, _} = jid:tolower(Channel), + case use_cache(Mod, LServer) of + true -> + ets_cache:delete(?MIX_PAM_CACHE, + {LUser, LServer, Chan, Service}, + cache_nodes(Mod, LServer)); + false -> + ok + end. + +%%%=================================================================== +%%% Webadmin interface +%%%=================================================================== +webadmin_user(Acc, User, Server, #request{lang = Lang}) -> + QueueLen = case get_channels({jid:nodeprep(User), jid:nameprep(Server), <<>>}) of + {ok, Channels} -> length(Channels); + error -> -1 + end, + FQueueLen = ?C(integer_to_binary(QueueLen)), + FQueueView = ?AC(<<"mix_channels/">>, ?T("View joined MIX channels")), + Acc ++ + [?XCT(<<"h3">>, ?T("Joined MIX channels:")), + FQueueLen, + ?C(<<" | ">>), + FQueueView]. + +webadmin_menu_hostuser(Acc, _Host, _Username, _Lang) -> + Acc ++ [{<<"mix_channels">>, <<"MIX Channels">>}]. + +webadmin_page_hostuser(_, Host, U, #request{path = [<<"mix_channels">>], lang = Lang}) -> + Res = web_mix_channels(U, Host, Lang), + {stop, Res}; +webadmin_page_hostuser(Acc, _, _, _) -> Acc. + +web_mix_channels(User, Server, Lang) -> + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + US = {LUser, LServer}, + Items = case get_channels({jid:nodeprep(User), jid:nameprep(Server), <<>>}) of + {ok, Channels} -> Channels; + error -> [] + end, + SItems = lists:sort(Items), + FItems = case SItems of + [] -> [?CT(?T("None"))]; + _ -> + THead = ?XE(<<"thead">>, [?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Channel JID")), + ?XCT(<<"td">>, ?T("Participant ID"))])]), + Entries = lists:map(fun ({JID, ID}) -> + ?XE(<<"tr">>, [ + ?XAC(<<"td">>, [{<<"class">>, <<"valign">>}], jid:encode(JID)), + ?XAC(<<"td">>, [{<<"class">>, <<"valign">>}], ID) + ]) + end, SItems), + [?XE(<<"table">>, [THead, ?XE(<<"tbody">>, Entries)])] + end, + PageTitle = str:translate_and_format(Lang, ?T("Joined MIX channels of ~ts"), [us_to_list(US)]), + (?H1GL(PageTitle, <<"modules/#mod_mix_pam">>, <<"mod_mix_pam">>)) + ++ FItems. + +us_to_list({User, Server}) -> + jid:encode({User, Server, <<"">>}). diff --git a/src/mod_mix_pam_mnesia.erl b/src/mod_mix_pam_mnesia.erl new file mode 100644 index 000000000..7d14579eb --- /dev/null +++ b/src/mod_mix_pam_mnesia.erl @@ -0,0 +1,91 @@ +%%%------------------------------------------------------------------- +%%% Author : Evgeny Khramtsov +%%% Created : 4 Dec 2018 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2018 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_mix_pam_mnesia). +-behaviour(mod_mix_pam). + +%% API +-export([init/2, add_channel/3, get_channel/2, + get_channels/1, del_channel/2, del_channels/1, + use_cache/1]). + +-record(mix_pam, {user_channel :: {binary(), binary(), binary(), binary()}, + user :: {binary(), binary()}, + id :: binary()}). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + case ejabberd_mnesia:create(?MODULE, mix_pam, + [{disc_only_copies, [node()]}, + {attributes, record_info(fields, mix_pam)}, + {index, [user]}]) of + {atomic, _} -> ok; + _ -> {error, db_failure} + end. + +use_cache(Host) -> + case mnesia:table_info(mix_pam, storage_type) of + disc_only_copies -> + mod_mix_pam_opt:use_cache(Host); + _ -> + false + end. + +add_channel(User, Channel, ID) -> + {LUser, LServer, _} = jid:tolower(User), + {Chan, Service, _} = jid:tolower(Channel), + mnesia:dirty_write(#mix_pam{user_channel = {LUser, LServer, Chan, Service}, + user = {LUser, LServer}, + id = ID}). + +get_channel(User, Channel) -> + {LUser, LServer, _} = jid:tolower(User), + {Chan, Service, _} = jid:tolower(Channel), + case mnesia:dirty_read(mix_pam, {LUser, LServer, Chan, Service}) of + [#mix_pam{id = ID}] -> {ok, ID}; + [] -> {error, notfound} + end. + +get_channels(User) -> + {LUser, LServer, _} = jid:tolower(User), + Ret = mnesia:dirty_index_read(mix_pam, {LUser, LServer}, #mix_pam.user), + {ok, lists:map( + fun(#mix_pam{user_channel = {_, _, Chan, Service}, + id = ID}) -> + {jid:make(Chan, Service), ID} + end, Ret)}. + +del_channel(User, Channel) -> + {LUser, LServer, _} = jid:tolower(User), + {Chan, Service, _} = jid:tolower(Channel), + mnesia:dirty_delete(mix_pam, {LUser, LServer, Chan, Service}). + +del_channels(User) -> + {LUser, LServer, _} = jid:tolower(User), + Ret = mnesia:dirty_index_read(mix_pam, {LUser, LServer}, #mix_pam.user), + lists:foreach(fun mnesia:dirty_delete_object/1, Ret). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== diff --git a/src/mod_mix_pam_opt.erl b/src/mod_mix_pam_opt.erl new file mode 100644 index 000000000..103e6039c --- /dev/null +++ b/src/mod_mix_pam_opt.erl @@ -0,0 +1,41 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_mix_pam_opt). + +-export([cache_life_time/1]). +-export([cache_missed/1]). +-export([cache_size/1]). +-export([db_type/1]). +-export([use_cache/1]). + +-spec cache_life_time(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +cache_life_time(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_life_time, Opts); +cache_life_time(Host) -> + gen_mod:get_module_opt(Host, mod_mix_pam, cache_life_time). + +-spec cache_missed(gen_mod:opts() | global | binary()) -> boolean(). +cache_missed(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_missed, Opts); +cache_missed(Host) -> + gen_mod:get_module_opt(Host, mod_mix_pam, cache_missed). + +-spec cache_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +cache_size(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_size, Opts); +cache_size(Host) -> + gen_mod:get_module_opt(Host, mod_mix_pam, cache_size). + +-spec db_type(gen_mod:opts() | global | binary()) -> atom(). +db_type(Opts) when is_map(Opts) -> + gen_mod:get_opt(db_type, Opts); +db_type(Host) -> + gen_mod:get_module_opt(Host, mod_mix_pam, db_type). + +-spec use_cache(gen_mod:opts() | global | binary()) -> boolean(). +use_cache(Opts) when is_map(Opts) -> + gen_mod:get_opt(use_cache, Opts); +use_cache(Host) -> + gen_mod:get_module_opt(Host, mod_mix_pam, use_cache). + diff --git a/src/mod_mix_pam_sql.erl b/src/mod_mix_pam_sql.erl new file mode 100644 index 000000000..af22c74f4 --- /dev/null +++ b/src/mod_mix_pam_sql.erl @@ -0,0 +1,134 @@ +%%%------------------------------------------------------------------- +%%% Author : Evgeny Khramtsov +%%% Created : 4 Dec 2018 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2018 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_mix_pam_sql). +-behaviour(mod_mix_pam). + +%% API +-export([init/2, add_channel/3, get_channel/2, + get_channels/1, del_channel/2, del_channels/1]). +-export([sql_schemas/0]). + +-include("logger.hrl"). +-include("ejabberd_sql_pt.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(Host, _Opts) -> + ejabberd_sql_schema:update_schema(Host, ?MODULE, sql_schemas()), + ok. + +sql_schemas() -> + [#sql_schema{ + version = 1, + tables = + [#sql_table{ + name = <<"mix_pam">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"channel">>, type = text}, + #sql_column{name = <<"service">>, type = text}, + #sql_column{name = <<"id">>, type = text}, + #sql_column{name = <<"created_at">>, type = timestamp, + default = true}], + indices = [#sql_index{ + columns = [<<"username">>, <<"server_host">>, + <<"channel">>, <<"service">>], + unique = true}]}]}]. + +add_channel(User, Channel, ID) -> + {LUser, LServer, _} = jid:tolower(User), + {Chan, Service, _} = jid:tolower(Channel), + case ?SQL_UPSERT(LServer, "mix_pam", + ["!channel=%(Chan)s", + "!service=%(Service)s", + "!username=%(LUser)s", + "!server_host=%(LServer)s", + "id=%(ID)s"]) of + ok -> ok; + _Err -> {error, db_failure} + end. + +get_channel(User, Channel) -> + {LUser, LServer, _} = jid:tolower(User), + {Chan, Service, _} = jid:tolower(Channel), + case ejabberd_sql:sql_query( + LServer, + ?SQL("select @(id)s from mix_pam where " + "channel=%(Chan)s and service=%(Service)s " + "and username=%(LUser)s and %(LServer)H")) of + {selected, [{ID}]} -> {ok, ID}; + {selected, []} -> {error, notfound}; + _Err -> {error, db_failure} + end. + +get_channels(User) -> + {LUser, LServer, _} = jid:tolower(User), + SQL = ?SQL("select @(channel)s, @(service)s, @(id)s from mix_pam " + "where username=%(LUser)s and %(LServer)H"), + case ejabberd_sql:sql_query(LServer, SQL) of + {selected, Ret} -> + {ok, lists:filtermap( + fun({Chan, Service, ID}) -> + case jid:make(Chan, Service) of + error -> + report_corrupted(SQL), + false; + JID -> + {true, {JID, ID}} + end + end, Ret)}; + _Err -> + {error, db_failure} + end. + +del_channel(User, Channel) -> + {LUser, LServer, _} = jid:tolower(User), + {Chan, Service, _} = jid:tolower(Channel), + case ejabberd_sql:sql_query( + LServer, + ?SQL("delete from mix_pam where " + "channel=%(Chan)s and service=%(Service)s " + "and username=%(LUser)s and %(LServer)H")) of + {updated, _} -> ok; + _Err -> {error, db_failure} + end. + +del_channels(User) -> + {LUser, LServer, _} = jid:tolower(User), + case ejabberd_sql:sql_query( + LServer, + ?SQL("delete from mix_pam where " + "username=%(LUser)s and %(LServer)H")) of + {updated, _} -> ok; + _Err -> {error, db_failure} + end. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +-spec report_corrupted(#sql_query{}) -> ok. +report_corrupted(SQL) -> + ?ERROR_MSG("Corrupted values returned by SQL request: ~ts", + [SQL#sql_query.hash]). diff --git a/src/mod_mix_sql.erl b/src/mod_mix_sql.erl new file mode 100644 index 000000000..be3b28124 --- /dev/null +++ b/src/mod_mix_sql.erl @@ -0,0 +1,292 @@ +%%%------------------------------------------------------------------- +%%% Created : 1 Dec 2018 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2018 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_mix_sql). +-behaviour(mod_mix). + +%% API +-export([init/2]). +-export([set_channel/6, get_channels/2, get_channel/3, del_channel/3]). +-export([set_participant/6, get_participant/4, get_participants/3, del_participant/4]). +-export([subscribe/5, unsubscribe/4, unsubscribe/5, get_subscribed/4]). +-export([sql_schemas/0]). + +-include("logger.hrl"). +-include("ejabberd_sql_pt.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(Host, _Opts) -> + ejabberd_sql_schema:update_schema(Host, ?MODULE, sql_schemas()), + ok. + +sql_schemas() -> + [#sql_schema{ + version = 1, + tables = + [#sql_table{ + name = <<"mix_channel">>, + columns = + [#sql_column{name = <<"channel">>, type = text}, + #sql_column{name = <<"service">>, type = text}, + #sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"domain">>, type = text}, + #sql_column{name = <<"jid">>, type = text}, + #sql_column{name = <<"hidden">>, type = boolean}, + #sql_column{name = <<"hmac_key">>, type = text}, + #sql_column{name = <<"created_at">>, type = timestamp, + default = true}], + indices = [#sql_index{ + columns = [<<"channel">>, <<"service">>], + unique = true}, + #sql_index{ + columns = [<<"service">>]}]}, + #sql_table{ + name = <<"mix_participant">>, + columns = + [#sql_column{name = <<"channel">>, type = text}, + #sql_column{name = <<"service">>, type = text}, + #sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"domain">>, type = text}, + #sql_column{name = <<"jid">>, type = text}, + #sql_column{name = <<"id">>, type = text}, + #sql_column{name = <<"nick">>, type = text}, + #sql_column{name = <<"created_at">>, type = timestamp, + default = true}], + indices = [#sql_index{ + columns = [<<"channel">>, <<"service">>, + <<"username">>, <<"domain">>], + unique = true}]}, + #sql_table{ + name = <<"mix_subscription">>, + columns = + [#sql_column{name = <<"channel">>, type = text}, + #sql_column{name = <<"service">>, type = {text, 75}}, + #sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"domain">>, type = {text, 75}}, + #sql_column{name = <<"node">>, type = text}, + #sql_column{name = <<"jid">>, type = text}], + indices = [#sql_index{ + columns = [<<"channel">>, <<"service">>, + <<"username">>, <<"domain">>, + <<"node">>], + unique = true}, + #sql_index{ + columns = [<<"channel">>, <<"service">>, + <<"node">>]}]}]}]. + +set_channel(LServer, Channel, Service, CreatorJID, Hidden, Key) -> + {User, Domain, _} = jid:tolower(CreatorJID), + RawJID = jid:encode(jid:remove_resource(CreatorJID)), + case ?SQL_UPSERT(LServer, "mix_channel", + ["!channel=%(Channel)s", + "!service=%(Service)s", + "username=%(User)s", + "domain=%(Domain)s", + "jid=%(RawJID)s", + "hidden=%(Hidden)b", + "hmac_key=%(Key)s"]) of + ok -> ok; + _Err -> {error, db_failure} + end. + +get_channels(LServer, Service) -> + case ejabberd_sql:sql_query( + LServer, + ?SQL("select @(channel)s, @(hidden)b from mix_channel " + "where service=%(Service)s")) of + {selected, Ret} -> + {ok, [Channel || {Channel, Hidden} <- Ret, Hidden == false]}; + _Err -> + {error, db_failure} + end. + +get_channel(LServer, Channel, Service) -> + SQL = ?SQL("select @(jid)s, @(hidden)b, @(hmac_key)s from mix_channel " + "where channel=%(Channel)s and service=%(Service)s"), + case ejabberd_sql:sql_query(LServer, SQL) of + {selected, [{RawJID, Hidden, Key}]} -> + try jid:decode(RawJID) of + JID -> {ok, {JID, Hidden, Key}} + catch _:{bad_jid, _} -> + report_corrupted(jid, SQL), + {error, db_failure} + end; + {selected, []} -> {error, notfound}; + _Err -> {error, db_failure} + end. + +del_channel(LServer, Channel, Service) -> + F = fun() -> + ejabberd_sql:sql_query_t( + ?SQL("delete from mix_channel where " + "channel=%(Channel)s and service=%(Service)s")), + ejabberd_sql:sql_query_t( + ?SQL("delete from mix_participant where " + "channel=%(Channel)s and service=%(Service)s")), + ejabberd_sql:sql_query_t( + ?SQL("delete from mix_subscription where " + "channel=%(Channel)s and service=%(Service)s")) + end, + case ejabberd_sql:sql_transaction(LServer, F) of + {atomic, _} -> ok; + _Err -> {error, db_failure} + end. + +set_participant(LServer, Channel, Service, JID, ID, Nick) -> + {User, Domain, _} = jid:tolower(JID), + RawJID = jid:encode(jid:remove_resource(JID)), + case ?SQL_UPSERT(LServer, "mix_participant", + ["!channel=%(Channel)s", + "!service=%(Service)s", + "!username=%(User)s", + "!domain=%(Domain)s", + "jid=%(RawJID)s", + "id=%(ID)s", + "nick=%(Nick)s"]) of + ok -> ok; + _Err -> {error, db_failure} + end. + +-spec get_participant(binary(), binary(), binary(), jid:jid()) -> {ok, {binary(), binary()}} | {error, notfound | db_failure}. +get_participant(LServer, Channel, Service, JID) -> + {User, Domain, _} = jid:tolower(JID), + case ejabberd_sql:sql_query( + LServer, + ?SQL("select @(id)s, @(nick)s from mix_participant " + "where channel=%(Channel)s and service=%(Service)s " + "and username=%(User)s and domain=%(Domain)s")) of + {selected, [Ret]} -> {ok, Ret}; + {selected, []} -> {error, notfound}; + _Err -> {error, db_failure} + end. + +get_participants(LServer, Channel, Service) -> + SQL = ?SQL("select @(jid)s, @(id)s, @(nick)s from mix_participant " + "where channel=%(Channel)s and service=%(Service)s"), + case ejabberd_sql:sql_query(LServer, SQL) of + {selected, Ret} -> + {ok, lists:filtermap( + fun({RawJID, ID, Nick}) -> + try jid:decode(RawJID) of + JID -> {true, {JID, ID, Nick}} + catch _:{bad_jid, _} -> + report_corrupted(jid, SQL), + false + end + end, Ret)}; + _Err -> + {error, db_failure} + end. + +del_participant(LServer, Channel, Service, JID) -> + {User, Domain, _} = jid:tolower(JID), + case ejabberd_sql:sql_query( + LServer, + ?SQL("delete from mix_participant where " + "channel=%(Channel)s and service=%(Service)s " + "and username=%(User)s and domain=%(Domain)s")) of + {updated, _} -> ok; + _Err -> {error, db_failure} + end. + +subscribe(_LServer, _Channel, _Service, _JID, []) -> + ok; +subscribe(LServer, Channel, Service, JID, Nodes) -> + {User, Domain, _} = jid:tolower(JID), + RawJID = jid:encode(jid:remove_resource(JID)), + F = fun() -> + lists:foreach( + fun(Node) -> + ?SQL_UPSERT_T( + "mix_subscription", + ["!channel=%(Channel)s", + "!service=%(Service)s", + "!username=%(User)s", + "!domain=%(Domain)s", + "!node=%(Node)s", + "jid=%(RawJID)s"]) + end, Nodes) + end, + case ejabberd_sql:sql_transaction(LServer, F) of + {atomic, _} -> ok; + _Err -> {error, db_failure} + end. + +get_subscribed(LServer, Channel, Service, Node) -> + SQL = ?SQL("select @(jid)s from mix_subscription " + "where channel=%(Channel)s and service=%(Service)s " + "and node=%(Node)s"), + case ejabberd_sql:sql_query(LServer, SQL) of + {selected, Ret} -> + {ok, lists:filtermap( + fun({RawJID}) -> + try jid:decode(RawJID) of + JID -> {true, JID} + catch _:{bad_jid, _} -> + report_corrupted(jid, SQL), + false + end + end, Ret)}; + _Err -> + {error, db_failure} + end. + +unsubscribe(LServer, Channel, Service, JID) -> + {User, Domain, _} = jid:tolower(JID), + case ejabberd_sql:sql_query( + LServer, + ?SQL("delete from mix_subscription " + "where channel=%(Channel)s and service=%(Service)s " + "and username=%(User)s and domain=%(Domain)s")) of + {updated, _} -> ok; + _Err -> {error, db_failure} + end. + +unsubscribe(_LServer, _Channel, _Service, _JID, []) -> + ok; +unsubscribe(LServer, Channel, Service, JID, Nodes) -> + {User, Domain, _} = jid:tolower(JID), + F = fun() -> + lists:foreach( + fun(Node) -> + ejabberd_sql:sql_query_t( + ?SQL("delete from mix_subscription " + "where channel=%(Channel)s " + "and service=%(Service)s " + "and username=%(User)s " + "and domain=%(Domain)s " + "and node=%(Node)s")) + end, Nodes) + end, + case ejabberd_sql:sql_transaction(LServer, F) of + {atomic, ok} -> ok; + _Err -> {error, db_failure} + end. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +-spec report_corrupted(atom(), #sql_query{}) -> ok. +report_corrupted(Column, SQL) -> + ?ERROR_MSG("Corrupted value of '~ts' column returned by " + "SQL request: ~ts", [Column, SQL#sql_query.hash]). diff --git a/src/mod_mqtt.erl b/src/mod_mqtt.erl new file mode 100644 index 000000000..e38c7aae6 --- /dev/null +++ b/src/mod_mqtt.erl @@ -0,0 +1,685 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov +%%% @copyright (C) 2002-2025 ProcessOne, SARL. All Rights Reserved. +%%% +%%% Licensed under the Apache License, Version 2.0 (the "License"); +%%% you may not use this file except in compliance with the License. +%%% You may obtain a copy of the License at +%%% +%%% http://www.apache.org/licenses/LICENSE-2.0 +%%% +%%% Unless required by applicable law or agreed to in writing, software +%%% distributed under the License is distributed on an "AS IS" BASIS, +%%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%%% See the License for the specific language governing permissions and +%%% limitations under the License. +%%% +%%%------------------------------------------------------------------- +-module(mod_mqtt). +-behaviour(p1_server). +-behaviour(gen_mod). +-behaviour(ejabberd_listener). +-dialyzer({no_improper_lists, join_filter/1}). + +%% gen_mod API +-export([start/2, stop/1, reload/3, depends/2, mod_options/1, mod_opt_type/1]). +-export([mod_doc/0]). +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). +%% ejabberd_listener API +-export([start/3, start_link/3, listen_opt_type/1, listen_options/0, accept/1]). +%% ejabberd_http API +-export([socket_handoff/3]). +%% Legacy ejabberd_listener API +-export([become_controller/2, socket_type/0]). +%% API +-export([open_session/1, close_session/1, lookup_session/1, + publish/3, subscribe/4, unsubscribe/2, select_retained/4, + check_publish_access/2, check_subscribe_access/2]). +%% ejabberd_hooks +-export([remove_user/2]). + +-include("logger.hrl"). +-include("mqtt.hrl"). +-include("translate.hrl"). + +-define(MQTT_TOPIC_CACHE, mqtt_topic_cache). +-define(MQTT_PAYLOAD_CACHE, mqtt_payload_cache). + +-type continuation() :: term(). +-type seconds() :: non_neg_integer(). + +%% RAM backend callbacks +-callback init() -> ok | {error, any()}. +-callback open_session(jid:ljid()) -> ok | {error, db_failure}. +-callback close_session(jid:ljid()) -> ok | {error, db_failure}. +-callback lookup_session(jid:ljid()) -> {ok, pid()} | {error, notfound | db_failure}. +-callback get_sessions(binary(), binary()) -> [jid:ljid()]. +-callback subscribe(jid:ljid(), binary(), sub_opts(), non_neg_integer()) -> ok | {error, db_failure}. +-callback unsubscribe(jid:ljid(), binary()) -> ok | {error, notfound | db_failure}. +-callback find_subscriber(binary(), binary() | continuation()) -> + {ok, {pid(), qos()}, continuation()} | {error, notfound | db_failure}. +%% Disc backend callbacks +-callback init(binary(), gen_mod:opts()) -> ok | {error, any()}. +-callback publish(jid:ljid(), binary(), binary(), qos(), properties(), seconds()) -> + ok | {error, db_failure}. +-callback delete_published(jid:ljid(), binary()) -> ok | {error, db_failure}. +-callback lookup_published(jid:ljid(), binary()) -> + {ok, {binary(), qos(), properties(), seconds()}} | + {error, notfound | db_failure}. +-callback list_topics(binary()) -> {ok, [binary()]} | {error, db_failure}. +-callback use_cache(binary()) -> boolean(). +-callback cache_nodes(binary()) -> [node()]. + +-optional_callbacks([use_cache/1, cache_nodes/1]). + +-record(state, {host :: binary()}). + +%%%=================================================================== +%%% API +%%%=================================================================== +start(SockMod, Sock, ListenOpts) -> + mod_mqtt_session:start(SockMod, Sock, ListenOpts). + +start(Host, Opts) -> + gen_mod:start_child(?MODULE, Host, Opts). + +start_link(SockMod, Sock, ListenOpts) -> + mod_mqtt_session:start_link(SockMod, Sock, ListenOpts). + +stop(Host) -> + gen_mod:stop_child(?MODULE, Host). + +reload(_Host, _NewOpts, _OldOpts) -> + ok. + +depends(_Host, _Opts) -> + []. + +socket_type() -> + raw. + +become_controller(Pid, _) -> + accept(Pid). + +accept(Pid) -> + mod_mqtt_session:accept(Pid). + +socket_handoff(LocalPath, Request, Opts) -> + mod_mqtt_ws:socket_handoff(LocalPath, Request, Opts). + +open_session({U, S, R}) -> + Mod = gen_mod:ram_db_mod(S, ?MODULE), + Mod:open_session({U, S, R}). + +close_session({U, S, R}) -> + Mod = gen_mod:ram_db_mod(S, ?MODULE), + Mod:close_session({U, S, R}). + +lookup_session({U, S, R}) -> + Mod = gen_mod:ram_db_mod(S, ?MODULE), + Mod:lookup_session({U, S, R}). + +-spec publish(jid:ljid(), publish(), seconds()) -> + {ok, non_neg_integer()} | {error, db_failure | publish_forbidden}. +publish({_, S, _} = USR, Pkt, ExpiryTime) -> + case check_publish_access(Pkt#publish.topic, USR) of + allow -> + case retain(USR, Pkt, ExpiryTime) of + ok -> + ejabberd_hooks:run(mqtt_publish, S, [USR, Pkt, ExpiryTime]), + Mod = gen_mod:ram_db_mod(S, ?MODULE), + route(Mod, S, Pkt, ExpiryTime); + {error, _} = Err -> + Err + end; + deny -> + {error, publish_forbidden} + end. + +-spec subscribe(jid:ljid(), binary(), sub_opts(), non_neg_integer()) -> + ok | {error, db_failure | subscribe_forbidden}. +subscribe({_, S, _} = USR, TopicFilter, SubOpts, ID) -> + Mod = gen_mod:ram_db_mod(S, ?MODULE), + Limit = mod_mqtt_opt:max_topic_depth(S), + case check_topic_depth(TopicFilter, Limit) of + allow -> + case check_subscribe_access(TopicFilter, USR) of + allow -> + ejabberd_hooks:run(mqtt_subscribe, S, [USR, TopicFilter, SubOpts, ID]), + Mod:subscribe(USR, TopicFilter, SubOpts, ID); + deny -> + {error, subscribe_forbidden} + end; + deny -> + {error, subscribe_forbidden} + end. + +-spec unsubscribe(jid:ljid(), binary()) -> ok | {error, notfound | db_failure}. +unsubscribe({U, S, R}, Topic) -> + Mod = gen_mod:ram_db_mod(S, ?MODULE), + ejabberd_hooks:run(mqtt_unsubscribe, S, [{U, S, R}, Topic]), + Mod:unsubscribe({U, S, R}, Topic). + +-spec select_retained(jid:ljid(), binary(), qos(), non_neg_integer()) -> + [{publish(), seconds()}]. +select_retained({_, S, _} = USR, TopicFilter, QoS, SubID) -> + Mod = gen_mod:db_mod(S, ?MODULE), + Limit = mod_mqtt_opt:match_retained_limit(S), + select_retained(Mod, USR, TopicFilter, QoS, SubID, Limit). + +remove_user(User, Server) -> + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + Mod = gen_mod:ram_db_mod(LServer, ?MODULE), + Sessions = Mod:get_sessions(LUser, LServer), + [close_session(Session) || Session <- Sessions]. + +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== +init([Host|_]) -> + Opts = gen_mod:get_module_opts(Host, ?MODULE), + Mod = gen_mod:db_mod(Opts, ?MODULE), + RMod = gen_mod:ram_db_mod(Opts, ?MODULE), + ejabberd_hooks:add(remove_user, Host, ?MODULE, remove_user, 50), + try + ok = Mod:init(Host, Opts), + ok = RMod:init(), + ok = init_cache(Mod, Host, Opts), + {ok, #state{host = Host}} + catch _:{badmatch, {error, Why}} -> + {stop, Why} + end. + +handle_call(Request, From, State) -> + ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), + {noreply, State}. + +handle_cast(Msg, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Msg]), + {noreply, State}. + +handle_info(Info, State) -> + ?WARNING_MSG("Unexpected info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, #state{host = Host}) -> + ejabberd_hooks:delete(remove_user, Host, ?MODULE, remove_user, 50), + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%%=================================================================== +%%% Options +%%%=================================================================== +-spec mod_options(binary()) -> [{access_publish, [{[binary()], acl:acl()}]} | + {access_subscribe, [{[binary()], acl:acl()}]} | + {atom(), any()}]. +mod_options(Host) -> + [{match_retained_limit, 1000}, + {max_topic_depth, 8}, + {max_topic_aliases, 100}, + {session_expiry, timer:minutes(5)}, + {max_queue, 5000}, + {access_subscribe, []}, + {access_publish, []}, + {db_type, ejabberd_config:default_db(Host, ?MODULE)}, + {ram_db_type, ejabberd_config:default_ram_db(Host, ?MODULE)}, + {queue_type, ejabberd_option:queue_type(Host)}, + {use_cache, ejabberd_option:use_cache(Host)}, + {cache_size, ejabberd_option:cache_size(Host)}, + {cache_missed, ejabberd_option:cache_missed(Host)}, + {cache_life_time, ejabberd_option:cache_life_time(Host)}]. + +mod_opt_type(max_queue) -> + econf:pos_int(unlimited); +mod_opt_type(session_expiry) -> + econf:either( + econf:int(0, 0), + econf:timeout(second)); +mod_opt_type(match_retained_limit) -> + econf:pos_int(infinity); +mod_opt_type(max_topic_depth) -> + econf:pos_int(infinity); +mod_opt_type(max_topic_aliases) -> + econf:int(0, 65535); +mod_opt_type(access_subscribe) -> + topic_access_validator(); +mod_opt_type(access_publish) -> + topic_access_validator(); +mod_opt_type(queue_type) -> + econf:queue_type(); +mod_opt_type(db_type) -> + econf:db_type(?MODULE); +mod_opt_type(ram_db_type) -> + econf:db_type(?MODULE); +mod_opt_type(use_cache) -> + econf:bool(); +mod_opt_type(cache_size) -> + econf:pos_int(infinity); +mod_opt_type(cache_missed) -> + econf:bool(); +mod_opt_type(cache_life_time) -> + econf:timeout(second, infinity). + +listen_opt_type(tls_verify) -> + econf:bool(); +listen_opt_type(max_payload_size) -> + econf:pos_int(infinity). + +listen_options() -> + [{max_fsm_queue, 10000}, + {max_payload_size, infinity}, + {tls, false}, + {tls_verify, false}]. + +%%%=================================================================== +%%% Doc +%%%=================================================================== +mod_doc() -> + #{desc => + ?T("This module adds " + "_`../guide/mqtt/index.md|support for the MQTT`_ " + "protocol version '3.1.1' and '5.0'. Remember to configure " + "'mod_mqtt' in 'modules' and 'listen' sections."), + opts => + [{access_subscribe, + #{value => "{TopicFilter: AccessName}", + desc => + ?T("Access rules to restrict access to topics " + "for subscribers. By default there are no restrictions.")}}, + {access_publish, + #{value => "{TopicFilter: AccessName}", + desc => + ?T("Access rules to restrict access to topics " + "for publishers. By default there are no restrictions.")}}, + {max_queue, + #{value => ?T("Size"), + desc => + ?T("Maximum queue size for outgoing packets. " + "The default value is '5000'.")}}, + {session_expiry, + #{value => "timeout()", + desc => + ?T("The option specifies how long to wait for " + "an MQTT session resumption. When '0' is set, " + "the session gets destroyed when the underlying " + "client connection is closed. The default value is " + "'5' minutes.")}}, + {max_topic_depth, + #{value => ?T("Depth"), + desc => + ?T("The maximum topic depth, i.e. the number of " + "slashes ('/') in the topic. The default " + "value is '8'.")}}, + {max_topic_aliases, + #{value => "0..65535", + desc => + ?T("The maximum number of aliases a client " + "is able to associate with the topics. " + "The default value is '100'.")}}, + {match_retained_limit, + #{value => "pos_integer() | infinity", + desc => + ?T("The option limits the number of retained messages " + "returned to a client when it subscribes to some " + "topic filter. The default value is '1000'.")}}, + {queue_type, + #{value => "ram | file", + desc => + ?T("Same as top-level _`queue_type`_ option, " + "but applied to this module only.")}}, + {ram_db_type, + #{value => "mnesia", + desc => + ?T("Same as top-level _`default_ram_db`_ option, " + "but applied to this module only.")}}, + {db_type, + #{value => "mnesia | sql", + desc => + ?T("Same as top-level _`default_db`_ option, " + "but applied to this module only.")}}, + {use_cache, + #{value => "true | false", + desc => + ?T("Same as top-level _`use_cache`_ option, " + "but applied to this module only.")}}, + {cache_size, + #{value => "pos_integer() | infinity", + desc => + ?T("Same as top-level _`cache_size`_ option, " + "but applied to this module only.")}}, + {cache_missed, + #{value => "true | false", + desc => + ?T("Same as top-level _`cache_missed`_ option, " + "but applied to this module only.")}}, + {cache_life_time, + #{value => "timeout()", + desc => + ?T("Same as top-level _`cache_life_time`_ option, " + "but applied to this module only.")}}]}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +route(Mod, LServer, Pkt, ExpiryTime) -> + route(Mod, LServer, Pkt, ExpiryTime, Pkt#publish.topic, 0). + +route(Mod, LServer, Pkt, ExpiryTime, Continuation, Num) -> + case Mod:find_subscriber(LServer, Continuation) of + {ok, {Pid, #sub_opts{no_local = true}, _}, Continuation1} + when Pid == self() -> + route(Mod, LServer, Pkt, ExpiryTime, Continuation1, Num); + {ok, {Pid, SubOpts, ID}, Continuation1} -> + ?DEBUG("Route to ~p: ~ts", [Pid, Pkt#publish.topic]), + MinQoS = min(SubOpts#sub_opts.qos, Pkt#publish.qos), + Retain = case SubOpts#sub_opts.retain_as_published of + false -> false; + true -> Pkt#publish.retain + end, + Props = set_sub_id(ID, Pkt#publish.properties), + mod_mqtt_session:route( + Pid, {Pkt#publish{qos = MinQoS, + dup = false, + retain = Retain, + properties = Props}, + ExpiryTime}), + route(Mod, LServer, Pkt, ExpiryTime, Continuation1, Num+1); + {error, _} -> + {ok, Num} + end. + +select_retained(Mod, {_, LServer, _} = USR, TopicFilter, QoS, SubID, Limit) -> + Topics = match_topics(TopicFilter, LServer, Limit), + lists:filtermap( + fun({{Filter, _}, Topic}) -> + case lookup_published(Mod, USR, Topic) of + {ok, {Payload, QoS1, Props, ExpiryTime}} -> + Props1 = set_sub_id(SubID, Props), + {true, {#publish{topic = Topic, + payload = Payload, + retain = true, + properties = Props1, + qos = min(QoS, QoS1)}, + ExpiryTime}}; + error -> + ets:delete(?MQTT_TOPIC_CACHE, {Filter, LServer}), + false; + _ -> + false + end + end, Topics). + +match_topics(Topic, LServer, Limit) -> + Filter = topic_filter(Topic), + case Limit of + infinity -> + ets:match_object(?MQTT_TOPIC_CACHE, {{Filter, LServer}, '_'}); + _ -> + case ets:select(?MQTT_TOPIC_CACHE, + [{{{Filter, LServer}, '_'}, [], ['$_']}], Limit) of + {Topics, _} -> Topics; + '$end_of_table' -> [] + end + end. + +retain({_, S, _} = USR, #publish{retain = true, + topic = Topic, payload = Data, + qos = QoS, properties = Props}, + ExpiryTime) -> + Mod = gen_mod:db_mod(S, ?MODULE), + TopicKey = topic_key(Topic), + case Data of + <<>> -> + ets:delete(?MQTT_TOPIC_CACHE, {TopicKey, S}), + case use_cache(Mod, S) of + true -> + ets_cache:delete(?MQTT_PAYLOAD_CACHE, {S, Topic}, + cache_nodes(Mod, S)); + false -> + ok + end, + Mod:delete_published(USR, Topic); + _ -> + ets:insert(?MQTT_TOPIC_CACHE, {{TopicKey, S}, Topic}), + case use_cache(Mod, S) of + true -> + case ets_cache:update( + ?MQTT_PAYLOAD_CACHE, {S, Topic}, + {ok, {Data, QoS, Props, ExpiryTime}}, + fun() -> + Mod:publish(USR, Topic, Data, + QoS, Props, ExpiryTime) + end, cache_nodes(Mod, S)) of + {ok, _} -> ok; + {error, _} = Err -> Err + end; + false -> + Mod:publish(USR, Topic, Data, QoS, Props, ExpiryTime) + end + end; +retain(_, _, _) -> + ok. + +lookup_published(Mod, {_, LServer, _} = USR, Topic) -> + case use_cache(Mod, LServer) of + true -> + ets_cache:lookup( + ?MQTT_PAYLOAD_CACHE, {LServer, Topic}, + fun() -> + Mod:lookup_published(USR, Topic) + end); + false -> + Mod:lookup_published(USR, Topic) + end. + +set_sub_id(0, Props) -> + Props; +set_sub_id(ID, Props) -> + Props#{subscription_identifier => [ID]}. + +%%%=================================================================== +%%% Matching functions +%%%=================================================================== +topic_key(S) -> + Parts = split_path(S), + case join_key(Parts) of + [<<>>|T] -> T; + T -> T + end. + +topic_filter(S) -> + Parts = split_path(S), + case join_filter(Parts) of + [<<>>|T] -> T; + T -> T + end. + +join_key([X,Y|T]) -> + [X, $/|join_key([Y|T])]; +join_key([X]) -> + [X]; +join_key([]) -> + []. + +join_filter([X, <<$#>>]) -> + [wildcard(X)|'_']; +join_filter([X,Y|T]) -> + [wildcard(X), $/|join_filter([Y|T])]; +join_filter([<<>>]) -> + []; +join_filter([<<$#>>]) -> + '_'; +join_filter([X]) -> + [wildcard(X)]; +join_filter([]) -> + []. + +wildcard(<<$+>>) -> '_'; +wildcard(Bin) -> Bin. + +check_topic_depth(_Topic, infinity) -> + allow; +check_topic_depth(_, N) when N=<0 -> + deny; +check_topic_depth(<<$/, T/binary>>, N) -> + check_topic_depth(T, N-1); +check_topic_depth(<<_, T/binary>>, N) -> + check_topic_depth(T, N); +check_topic_depth(<<>>, _) -> + allow. + +split_path(Path) -> + binary:split(Path, <<$/>>, [global]). + +%%%=================================================================== +%%% Validators +%%%=================================================================== +-spec topic_access_validator() -> econf:validator(). +topic_access_validator() -> + econf:and_then( + econf:map( + fun(TF) -> + try split_path(mqtt_codec:topic_filter(TF)) + catch _:{mqtt_codec, _} = Reason -> + econf:fail(Reason) + end + end, + econf:acl(), + [{return, orddict}]), + fun lists:reverse/1). + +%%%=================================================================== +%%% ACL checks +%%%=================================================================== +check_subscribe_access(Topic, {_, S, _} = USR) -> + Rules = mod_mqtt_opt:access_subscribe(S), + check_access(Topic, USR, Rules). + +check_publish_access(<<$$, _/binary>>, _) -> + deny; +check_publish_access(Topic, {_, S, _} = USR) -> + Rules = mod_mqtt_opt:access_publish(S), + check_access(Topic, USR, Rules). + +check_access(_, _, []) -> + allow; +check_access(Topic, {U, S, R} = USR, FilterRules) -> + TopicParts = binary:split(Topic, <<$/>>, [global]), + case lists:any( + fun({FilterParts, Rule}) -> + case match(TopicParts, FilterParts, U, S, R) of + true -> + allow == acl:match_rule(S, Rule, USR); + false -> + false + end + end, FilterRules) of + true -> allow; + false -> deny + end. + +match(_, [<<"#">>|_], _, _, _) -> + true; +match([], [<<>>, <<"#">>|_], _, _, _) -> + true; +match([_|T1], [<<"+">>|T2], U, S, R) -> + match(T1, T2, U, S, R); +match([H|T1], [<<"%u">>|T2], U, S, R) -> + case jid:nodeprep(H) of + U -> match(T1, T2, U, S, R); + _ -> false + end; +match([H|T1], [<<"%d">>|T2], U, S, R) -> + case jid:nameprep(H) of + S -> match(T1, T2, U, S, R); + _ -> false + end; +match([H|T1], [<<"%c">>|T2], U, S, R) -> + case jid:resourceprep(H) of + R -> match(T1, T2, U, S, R); + _ -> false + end; +match([H|T1], [<<"%g">>|T2], U, S, R) -> + case jid:resourceprep(H) of + H -> + case acl:loaded_shared_roster_module(S) of + undefined -> false; + Mod -> + case Mod:get_group_opts(S, H) of + error -> false; + _ -> + case Mod:is_user_in_group({U, S}, H, S) of + true -> match(T1, T2, U, S, R); + _ -> false + end + end + end; + _ -> false + end; +match([H|T1], [H|T2], U, S, R) -> + match(T1, T2, U, S, R); +match([], [], _, _, _) -> + true; +match(_, _, _, _, _) -> + false. + +%%%=================================================================== +%%% Cache stuff +%%%=================================================================== +-spec init_cache(module(), binary(), gen_mod:opts()) -> ok | {error, db_failure}. +init_cache(Mod, Host, Opts) -> + init_payload_cache(Mod, Host, Opts), + init_topic_cache(Mod, Host). + +-spec init_topic_cache(module(), binary()) -> ok | {error, db_failure}. +init_topic_cache(Mod, Host) -> + catch ets:new(?MQTT_TOPIC_CACHE, + [named_table, ordered_set, public, + {heir, erlang:group_leader(), none}]), + ?INFO_MSG("Building MQTT cache for ~ts, this may take a while", [Host]), + case Mod:list_topics(Host) of + {ok, Topics} -> + lists:foreach( + fun(Topic) -> + ets:insert(?MQTT_TOPIC_CACHE, + {{topic_key(Topic), Host}, Topic}) + end, Topics); + {error, _} = Err -> + Err + end. + +-spec init_payload_cache(module(), binary(), gen_mod:opts()) -> ok. +init_payload_cache(Mod, Host, Opts) -> + case use_cache(Mod, Host) of + true -> + CacheOpts = cache_opts(Opts), + ets_cache:new(?MQTT_PAYLOAD_CACHE, CacheOpts); + false -> + ets_cache:delete(?MQTT_PAYLOAD_CACHE) + end. + +-spec cache_opts(gen_mod:opts()) -> [proplists:property()]. +cache_opts(Opts) -> + MaxSize = mod_mqtt_opt:cache_size(Opts), + CacheMissed = mod_mqtt_opt:cache_missed(Opts), + LifeTime = mod_mqtt_opt:cache_life_time(Opts), + [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}]. + +-spec use_cache(module(), binary()) -> boolean(). +use_cache(Mod, Host) -> + case erlang:function_exported(Mod, use_cache, 1) of + true -> Mod:use_cache(Host); + false -> mod_mqtt_opt:use_cache(Host) + end. + +-spec cache_nodes(module(), binary()) -> [node()]. +cache_nodes(Mod, Host) -> + case erlang:function_exported(Mod, cache_nodes, 1) of + true -> Mod:cache_nodes(Host); + false -> ejabberd_cluster:get_nodes() + end. diff --git a/src/mod_mqtt_bridge.erl b/src/mod_mqtt_bridge.erl new file mode 100644 index 000000000..cb60594e9 --- /dev/null +++ b/src/mod_mqtt_bridge.erl @@ -0,0 +1,218 @@ +%%%------------------------------------------------------------------- +%%% @author Pawel Chmielowski +%%% @copyright (C) 2002-2025 ProcessOne, SARL. All Rights Reserved. +%%% +%%% Licensed under the Apache License, Version 2.0 (the "License"); +%%% you may not use this file except in compliance with the License. +%%% You may obtain a copy of the License at +%%% +%%% http://www.apache.org/licenses/LICENSE-2.0 +%%% +%%% Unless required by applicable law or agreed to in writing, software +%%% distributed under the License is distributed on an "AS IS" BASIS, +%%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%%% See the License for the specific language governing permissions and +%%% limitations under the License. +%%% +%%%------------------------------------------------------------------- +-module(mod_mqtt_bridge). +-behaviour(gen_mod). + +%% gen_mod API +-export([start/2, stop/1, reload/3, depends/2, mod_options/1, mod_opt_type/1]). +-export([mod_doc/0]). + +%% API +-export([mqtt_publish_hook/3]). + +-include("logger.hrl"). +-include("mqtt.hrl"). +-include("translate.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +start(_Host, Opts) -> + User = mod_mqtt_bridge_opt:replication_user(Opts), + start_servers(User, element(1, mod_mqtt_bridge_opt:servers(Opts))), + {ok, [{hook, mqtt_publish, mqtt_publish_hook, 50}]}. + +stop(Host) -> + stop_servers(element(1, mod_mqtt_bridge_opt:servers(Host))), + ok. + +start_servers(User, Servers) -> + lists:foldl( + fun({Proc, Transport, HostAddr, Port, Path, Publish, Subscribe, Authentication}, Started) -> + case Started of + #{Proc := _} -> + ?DEBUG("Already started ~p", [Proc]), + Started; + _ -> + ChildSpec = {Proc, + {mod_mqtt_bridge_session, start_link, + [Proc, Transport, HostAddr, Port, Path, Publish, Subscribe, Authentication, User]}, + transient, + 1000, + worker, + [mod_mqtt_bridge_session]}, + Res = supervisor:start_child(ejabberd_gen_mod_sup, ChildSpec), + ?DEBUG("Starting ~p ~p", [Proc, Res]), + Started#{Proc => true} + end + end, #{}, Servers). + +stop_servers(Servers) -> + lists:foreach( + fun({Proc, _Transport, _Host, _Port, _Path, _Publish, _Subscribe, _Authentication}) -> + try p1_server:call(Proc, stop) + catch _:_ -> ok + end, + supervisor:terminate_child(ejabberd_gen_mod_sup, Proc), + supervisor:delete_child(ejabberd_gen_mod_sup, Proc) + end, Servers). + +reload(_Host, NewOpts, OldOpts) -> + OldServers = element(1, mod_mqtt_bridge_opt:servers(OldOpts)), + NewServers = element(1, mod_mqtt_bridge_opt:servers(NewOpts)), + Deleted = lists:filter( + fun(E) -> not lists:keymember(element(1, E), 1, NewServers) end, + OldServers), + Added = lists:filter( + fun(E) -> not lists:keymember(element(1, E), 1, OldServers) end, + NewServers), + stop_servers(Deleted), + start_servers(mod_mqtt_bridge_opt:replication_user(NewOpts), Added), + ok. + +depends(_Host, _Opts) -> + [{mod_mqtt, hard}]. + +proc_name(Proto, Host, Port, Path) -> + HostB = list_to_binary(Host), + TransportB = list_to_binary(Proto), + PathB = case Path of + V when is_list(V) -> + list_to_binary(V); + _ -> <<>> + end, + binary_to_atom(<<"mod_mqtt_bridge_", TransportB/binary, "_", HostB/binary, + "_", (integer_to_binary(Port))/binary, PathB/binary>>, utf8). + +-spec mqtt_publish_hook(jid:ljid(), publish(), non_neg_integer()) -> ok. +mqtt_publish_hook({_, S, _}, #publish{topic = Topic} = Pkt, _ExpiryTime) -> + {_, Publish} = mod_mqtt_bridge_opt:servers(S), + case maps:find(Topic, Publish) of + error -> ok; + {ok, Procs} -> + lists:foreach( + fun(Proc) -> + Proc ! {publish, Pkt} + end, Procs) + end. + +%%%=================================================================== +%%% Options +%%%=================================================================== +-spec mod_options(binary()) -> + [{servers, + {[{atom(), mqtt | mqtts | mqtt5 | mqtt5s, binary(), non_neg_integer(), + #{binary() => binary()}, #{binary() => binary()}, map()}], + #{binary() => [atom()]}}} | + {atom(), any()}]. +mod_options(Host) -> + [{servers, []}, + {replication_user, jid:make(<<"admin">>, Host)}]. + +mod_opt_type(replication_user) -> + econf:jid(); +mod_opt_type(servers) -> + econf:and_then( + econf:map(econf:url([mqtt, mqtts, mqtt5, mqtt5s, ws, wss, ws5, wss5]), + econf:options( + #{ + publish => econf:map(econf:binary(), econf:binary(), [{return, map}]), + subscribe => econf:map(econf:binary(), econf:binary(), [{return, map}]), + authentication => econf:either( + econf:options( + #{ + username => econf:binary(), + password => econf:binary() + }, [{return, map}]), + econf:options( + #{ + certfile => econf:pem() + }, [{return, map}]))}, + [{return, map}]), + [{return, map}]), + fun(Servers) -> + maps:fold( + fun(Url, Opts, {HAcc, PAcc}) -> + {ok, Scheme, _UserInfo, Host, Port, Path, _Query} = + misc:uri_parse(Url, [{mqtt, 1883}, {mqtts, 8883}, + {mqtt5, 1883}, {mqtt5s, 8883}, + {ws, 80}, {wss, 443}, + {ws5, 80}, {wss5, 443}]), + Publish = maps:get(publish, Opts, #{}), + Subscribe = maps:get(subscribe, Opts, #{}), + Authentication = maps:get(authentication, Opts, []), + Proto = list_to_atom(Scheme), + Proc = proc_name(Scheme, Host, Port, Path), + PAcc2 = maps:fold( + fun(Topic, _RemoteTopic, Acc) -> + maps:update_with(Topic, fun(V) -> [Proc | V] end, [Proc], Acc) + end, PAcc, Publish), + {[{Proc, Proto, Host, Port, Path, Publish, Subscribe, Authentication} | HAcc], PAcc2} + end, {[], #{}}, Servers) + end + ). + +%%%=================================================================== +%%% Doc +%%%=================================================================== +mod_doc() -> + #{desc => + [?T("This module adds ability to synchronize local MQTT topics with data on remote servers"), + ?T("It can update topics on remote servers when local user updates local topic, or can subscribe " + "for changes on remote server, and update local copy when remote data is updated."), + ?T("It is available since ejabberd 23.01.")], + example => + ["modules:", + " mod_mqtt_bridge:", + " replication_user: \"mqtt@xmpp.server.com\"", + " servers:", + " \"mqtt://server.com\":", + " authentication:", + " certfile: \"/etc/ejabberd/mqtt_server.pem\"", + " publish:", + " \"localA\": \"remoteA\" # local changes to 'localA' will be replicated on remote server as 'remoteA'", + " \"topicB\": \"topicB\"", + " subscribe:", + " \"remoteB\": \"localB\" # changes to 'remoteB' on remote server will be stored as 'localB' on local server"], + opts => + [{servers, + #{value => "{ServerUrl: {Key: Value}}", + desc => + ?T("Declaration of data to share for each ServerUrl. " + "Server URLs can use schemas: 'mqtt', 'mqtts' (mqtt with tls), 'mqtt5', " + "'mqtt5s' (both to trigger v5 protocol), 'ws', 'wss', 'ws5', 'wss5'. " + "Keys must be:")}, + [{authentication, + #{value => "{AuthKey: AuthValue}", + desc => ?T("List of authentication information, where AuthKey can be: " + "'username' and 'password' fields, or 'certfile' pointing to client certificate. " + "Certificate authentication can be used only with mqtts, mqtt5s, wss, wss5.")}}, + {publish, + #{value => "{LocalTopic: RemoteTopic}", + desc => ?T("Either publish or subscribe must be set, or both.")}}, + {subscribe, + #{value => "{RemoteTopic: LocalTopic}", + desc => ?T("Either publish or subscribe must be set, or both.")}}]}, + {replication_user, + #{value => "JID", + desc => + ?T("Identifier of a user that will be assigned as owner of local changes.")}}]}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== diff --git a/src/mod_mqtt_bridge_opt.erl b/src/mod_mqtt_bridge_opt.erl new file mode 100644 index 000000000..e10f72e1d --- /dev/null +++ b/src/mod_mqtt_bridge_opt.erl @@ -0,0 +1,20 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_mqtt_bridge_opt). + +-export([replication_user/1]). +-export([servers/1]). + +-spec replication_user(gen_mod:opts() | global | binary()) -> jid:jid(). +replication_user(Opts) when is_map(Opts) -> + gen_mod:get_opt(replication_user, Opts); +replication_user(Host) -> + gen_mod:get_module_opt(Host, mod_mqtt_bridge, replication_user). + +-spec servers(gen_mod:opts() | global | binary()) -> {[{atom(),'mqtt' | 'mqtts' | 'mqtt5' | 'mqtt5s',binary(),non_neg_integer(),#{binary()=>binary()},#{binary()=>binary()},map()}],#{binary()=>[atom()]}}. +servers(Opts) when is_map(Opts) -> + gen_mod:get_opt(servers, Opts); +servers(Host) -> + gen_mod:get_module_opt(Host, mod_mqtt_bridge, servers). + diff --git a/src/mod_mqtt_bridge_session.erl b/src/mod_mqtt_bridge_session.erl new file mode 100644 index 000000000..1c5f53f9d --- /dev/null +++ b/src/mod_mqtt_bridge_session.erl @@ -0,0 +1,530 @@ +%%%------------------------------------------------------------------- +%%% @author Pawel Chmielowski +%%% @copyright (C) 2002-2025 ProcessOne, SARL. All Rights Reserved. +%%% +%%% Licensed under the Apache License, Version 2.0 (the "License"); +%%% you may not use this file except in compliance with the License. +%%% You may obtain a copy of the License at +%%% +%%% http://www.apache.org/licenses/LICENSE-2.0 +%%% +%%% Unless required by applicable law or agreed to in writing, software +%%% distributed under the License is distributed on an "AS IS" BASIS, +%%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%%% See the License for the specific language governing permissions and +%%% limitations under the License. +%%% +%%%------------------------------------------------------------------- +-module(mod_mqtt_bridge_session). +-behaviour(p1_server). +-define(VSN, 1). +-vsn(?VSN). + +%% API +-export([start/9, start_link/9]). +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-include("logger.hrl"). +-include("mqtt.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). +-include_lib("public_key/include/public_key.hrl"). + +-type error_reason() :: + {auth, reason_code()} | + {code, reason_code()} | + {peer_disconnected, reason_code(), binary()} | + {socket, socket_error_reason()} | + {codec, mqtt_codec:error_reason()} | + {unexpected_packet, atom()} | + {tls, inet:posix() | atom() | binary()} | + {replaced, pid()} | + {resumed, pid()} | + subscribe_forbidden | publish_forbidden | + will_topic_forbidden | internal_server_error | + session_expired | idle_connection | + queue_full | shutdown | db_failure | + {payload_format_invalid, will | publish} | + session_expiry_non_zero | unknown_topic_alias. + +-type socket() :: + {gen_tcp, inet:socket()} | + {fast_tls, fast_tls:tls_socket()} | + {mod_mqtt_ws, mod_mqtt_ws:socket()}. +-type seconds() :: non_neg_integer(). +-type socket_error_reason() :: closed | timeout | inet:posix(). + +-define(PING_TIMEOUT, timer:seconds(50)). +-define(MAX_UINT32, 4294967295). + +-record(state, {vsn = ?VSN :: integer(), + version :: undefined | mqtt_version(), + socket :: undefined | socket(), + usr :: undefined | {binary(), binary(), binary()}, + ping_timer = undefined :: undefined | reference(), + stop_reason :: undefined | error_reason(), + subscriptions = #{}, + publish = #{}, + ws_codec = none, + id = 0 :: non_neg_integer(), + codec :: mqtt_codec:state(), + authentication :: #{username => binary(), password => binary(), certfile => binary()}}). + +-type state() :: #state{}. + +%%%=================================================================== +%%% API +%%%=================================================================== +start(Proc, Transport, Host, Port, Path, Publish, Subscribe, Authentication, ReplicationUser) -> + p1_server:start({local, Proc}, ?MODULE, [Proc, Transport, Host, Port, Path, Publish, Subscribe, Authentication, + ReplicationUser], []). + +start_link(Proc, Transport, Host, Port, Path, Publish, Subscribe, Authentication, ReplicationUser) -> + p1_server:start_link({local, Proc}, ?MODULE, [Proc, Transport, Host, Port, Path, Publish, Subscribe, + Authentication, ReplicationUser], []). + +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== +init([_Proc, Proto, Host, Port, Path, Publish, Subscribe, Authentication, ReplicationUser]) -> + {Version, Transport, IsWs} = + case Proto of + mqtt -> {4, gen_tcp, false}; + mqtts -> {4, ssl, false}; + mqtt5 -> {5, gen_tcp, false}; + mqtt5s -> {5, ssl, false}; + ws -> {4, gen_tcp, true}; + wss -> {4, ssl, true}; + ws5 -> {5, gen_tcp, true}; + wss5 -> {5, ssl, true} + end, + State = #state{version = Version, + id = p1_rand:uniform(65535), + codec = mqtt_codec:new(4096), + subscriptions = Subscribe, + authentication = Authentication, + usr = jid:tolower(ReplicationUser), + publish = Publish}, + case Authentication of + #{certfile := Cert} when Transport == ssl -> + Sock = ssl:connect(Host, Port, [binary, {active, true}, {certfile, Cert}]), + if IsWs -> + connect_ws(Host, Port, Path, Sock, State, ssl, none); + true -> connect(Sock, State, ssl, none) + end; + #{username := User, password := Pass} -> + Sock = Transport:connect(Host, Port, [binary, {active, true}]), + if IsWs -> + connect_ws(Host, Port, Path, Sock, State, Transport, {User, Pass}); + true -> connect(Sock, State, Transport, {User, Pass}) + end; + _ -> + {stop, {error, <<"Certificate can be only used for encrypted connections">> }} + end. + +handle_call(stop, _From, State) -> + {stop, normal, ok, State}; +handle_call(Request, From, State) -> + ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), + {noreply, State}. + +handle_cast(Msg, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Msg]), + {noreply, State}. + +-spec handle_info(term(), state()) -> + {noreply, state()} | {noreply, state(), timeout()} | {stop, term(), state()}. +handle_info({Tag, TCPSock, TCPData}, + #state{ws_codec = {init, Hash, Auth, Last}} = State) + when (Tag == tcp orelse Tag == ssl) -> + Data = <>, + case erlang:decode_packet(http_bin, Data, []) of + {ok, {http_response, _, 101, _}, Rest} -> + handle_info({tcp, TCPSock, Rest}, State#state{ws_codec = {inith, Hash, none, Auth, <<>>}}); + {ok, {http_response, _, _, _}, _Rest} -> + stop(State, {socket, closed}); + {ok, {http_error, _}, _} -> + stop(State, {socket, closed}); + {error, _} -> + stop(State, {socket, closed}); + {more, _} -> + {noreply, State#state{ws_codec = {init, Hash, Auth, Data}}} + end; +handle_info({Tag, TCPSock, TCPData}, + #state{ws_codec = {inith, Hash, Upgrade, Auth, Last}, + socket = {Transport, _}} = State) + when (Tag == tcp orelse Tag == ssl) -> + Data = <>, + case erlang:decode_packet(httph_bin, Data, []) of + {ok, {http_header, _, <<"Sec-Websocket-Accept">>, _, Val}, Rest} -> + case str:to_lower(Val) of + Hash -> + handle_info({tcp, TCPSock, Rest}, + State#state{ws_codec = {inith, ok, Upgrade, Auth, <<>>}}); + _ -> + stop(State, {socket, closed}) + end; + {ok, {http_header, _, 'Connection', _, Val}, Rest} -> + case str:to_lower(Val) of + <<"upgrade">> -> + handle_info({tcp, TCPSock, Rest}, + State#state{ws_codec = {inith, Hash, ok, Auth, <<>>}}); + _ -> + stop(State, {socket, closed}) + end; + {ok, {http_header, _, _, _, _}, Rest} -> + handle_info({tcp, TCPSock, Rest}, State); + {ok, {http_error, _}, _} -> + stop(State, {socket, closed}); + {ok, http_eoh, Rest} -> + case {Hash, Upgrade} of + {ok, ok} -> + {ok, State2} = connect({ok, TCPSock}, + State#state{ws_codec = ejabberd_websocket_codec:new_client()}, + Transport, Auth), + handle_info({tcp, TCPSock, Rest}, State2); + _ -> + stop(State, {socket, closed}) + end; + {error, _} -> + stop(State, {socket, closed}); + {more, _} -> + {noreply, State#state{ws_codec = {inith, Hash, Upgrade, Data}}} + end; +handle_info({Tag, TCPSock, TCPData}, + #state{ws_codec = WSCodec} = State) + when (Tag == tcp orelse Tag == ssl) andalso WSCodec /= none -> + {Packets, Acc0} = + case ejabberd_websocket_codec:decode(WSCodec, TCPData) of + {ok, NewWSCodec, Packets0} -> + {Packets0, {noreply, ok, State#state{ws_codec = NewWSCodec}}}; + {error, _Error, Packets0} -> + {Packets0, {stop_after, {socket, closed}, State}} + end, + Res2 = + lists:foldl( + fun(_, {stop, _, _} = Res) -> Res; + ({_Op, Data}, {Tag2, Res, S}) -> + case handle_info({tcp_decoded, TCPSock, Data}, S) of + {stop, _, _} = Stop -> + Stop; + {_, NewState} -> + {Tag2, Res, NewState} + end + end, Acc0, Packets), + case Res2 of + {noreply, _, State2} -> + {noreply, State2}; + {Tag3, Res3, State2} when Tag3 == stop; Tag3 == stop_after -> + {stop, Res3, State2} + end; +handle_info({Tag, TCPSock, TCPData}, + #state{codec = Codec} = State) + when Tag == tcp; Tag == ssl; Tag == tcp_decoded -> + case mqtt_codec:decode(Codec, TCPData) of + {ok, Pkt, Codec1} -> + ?DEBUG("Got MQTT packet:~n~ts", [pp(Pkt)]), + State1 = State#state{codec = Codec1}, + case handle_packet(Pkt, State1) of + {ok, State2} -> + handle_info({tcp_decoded, TCPSock, <<>>}, State2); + {error, State2, Reason} -> + stop(State2, Reason) + end; + {more, Codec1} -> + State1 = State#state{codec = Codec1}, + {noreply, State1}; + {error, Why} -> + stop(State, {codec, Why}) + end; +handle_info({tcp_closed, _Sock}, State) -> + ?DEBUG("MQTT connection reset by peer", []), + stop(State, {socket, closed}); +handle_info({ssl_closed, _Sock}, State) -> + ?DEBUG("MQTT connection reset by peer", []), + stop(State, {socket, closed}); +handle_info({tcp_error, _Sock, Reason}, State) -> + ?DEBUG("MQTT connection error: ~ts", [format_inet_error(Reason)]), + stop(State, {socket, Reason}); +handle_info({ssl_error, _Sock, Reason}, State) -> + ?DEBUG("MQTT connection error: ~ts", [format_inet_error(Reason)]), + stop(State, {socket, Reason}); +handle_info({publish, #publish{topic = Topic} = Pkt}, #state{publish = Publish} = State) -> + case maps:find(Topic, Publish) of + {ok, RemoteTopic} -> + case send(State, Pkt#publish{qos = 0, topic = RemoteTopic}) of + {ok, State2} -> + {noreply, State2} + end; + _ -> + {noreply, State} + end; +handle_info({timeout, _TRef, ping_timeout}, State) -> + case send(State, #pingreq{}) of + {ok, State2} -> + {noreply, State2} + end; +handle_info(Info, State) -> + ?WARNING_MSG("Unexpected info: ~p", [Info]), + {noreply, State}. + +-spec handle_packet(mqtt_packet(), state()) -> + {ok, state()} | + {error, state(), error_reason()}. +handle_packet(#connack{} = Pkt, State) -> + handle_connack(Pkt, State); +handle_packet(#suback{}, State) -> + {ok, State}; +handle_packet(#publish{} = Pkt, State) -> + handle_publish(Pkt, State); +handle_packet(#pingresp{}, State) -> + {ok, State}; +handle_packet(#disconnect{properties = #{session_expiry_interval := SE}}, + State) when SE > 0 -> + %% Protocol violation + {error, State, session_expiry_non_zero}; +handle_packet(#disconnect{code = Code, properties = Props}, + State) -> + Reason = maps:get(reason_string, Props, <<>>), + {error, State, {peer_disconnected, Code, Reason}}; +handle_packet(Pkt, State) -> + ?WARNING_MSG("Unexpected packet:~n~ts~n** when state:~n~ts", + [pp(Pkt), pp(State)]), + {error, State, {unexpected_packet, element(1, Pkt)}}. + +terminate(Reason, State) -> + Reason1 = case Reason of + shutdown -> shutdown; + {shutdown, _} -> shutdown; + normal -> State#state.stop_reason; + {process_limit, _} -> queue_full; + _ -> internal_server_error + end, + disconnect(State, Reason1). + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%%=================================================================== +%%% State transitions +%%%=================================================================== +connect({error, Reason}, _State, _Transport, _Auth) -> + {stop, {error, Reason}}; +connect({ok, Sock}, State0, Transport, Auth) -> + State = State0#state{socket = {Transport, Sock}}, + Connect = case Auth of + {User, Pass} -> + #connect{client_id = integer_to_binary(State#state.id), + clean_start = true, + username = User, + password = Pass, + keep_alive = 60, + proto_level = State#state.version}; + _ -> + #connect{client_id = integer_to_binary(State#state.id), + clean_start = true, + keep_alive = 60, + proto_level = State#state.version} + end, + Pkt = mqtt_codec:encode(State#state.version, Connect), + send(State, Connect), + {ok, _, Codec2} = mqtt_codec:decode(State#state.codec, Pkt), + {ok, State#state{codec = Codec2}}. + +connect_ws(_Host, _Port, _Path, {error, Reason}, _State, _Transport, _Auth) -> + {stop, {error, Reason}}; +connect_ws(Host, Port, Path, {ok, Sock}, State0, Transport, Auth) -> + Key = base64:encode(p1_rand:get_string()), + Hash = str:to_lower(base64:encode(crypto:hash(sha, <>))), + Data = <<"GET ", (list_to_binary(Path))/binary, " HTTP/1.1\r\n", + "Host: ", (list_to_binary(Host))/binary, ":", (integer_to_binary(Port))/binary,"\r\n", + "Upgrade: websocket\r\n", + "Connection: Upgrade\r\n", + "Sec-WebSocket-Protocol: mqtt\r\n", + "Sec-WebSocket-Key: ", Key/binary, "\r\n", + "Sec-WebSocket-Version: 13\r\n\r\n">>, + Res = Transport:send(Sock, Data), + check_sock_result({Transport, Sock}, Res), + {ok, State0#state{ws_codec = {init, Hash, Auth, <<>>}, socket = {Transport, Sock}}}. + +-spec stop(state(), error_reason()) -> + {noreply, state(), infinity} | + {stop, normal, state()}. +stop(State, Reason) -> + {stop, normal, State#state{stop_reason = Reason}}. + + +%%%=================================================================== +%%% CONNECT/PUBLISH/SUBSCRIBE/UNSUBSCRIBE handlers +%%%=================================================================== +-spec handle_connack(connack(), state()) -> + {ok, state()} | + {error, state(), error_reason()}. +handle_connack(#connack{code = success}, #state{subscriptions = Subs} = State) -> + Filters = maps:fold( + fun(RemoteTopic, _LocalTopic, Acc) -> + [{RemoteTopic, #sub_opts{}} | Acc] + end, [], Subs), + Pkt = #subscribe{id = 1, filters = Filters}, + send(State, Pkt); +handle_connack(#connack{}, State) -> + {error, State, {auth, 'not-authorized'}}. + +-spec handle_publish(publish(), state()) -> + {ok, state()} | + {error, state(), error_reason()}. +handle_publish(#publish{topic = Topic, payload = Payload, properties = Props}, + #state{usr = USR, subscriptions = Subs} = State) -> + case maps:get(Topic, Subs, none) of + none -> + {ok, State}; + LocalTopic -> + MessageExpiry = maps:get(message_expiry_interval, Props, ?MAX_UINT32), + ExpiryTime = min(unix_time() + MessageExpiry, ?MAX_UINT32), + mod_mqtt:publish(USR, #publish{retain = true, topic = LocalTopic, payload = Payload, properties = Props}, + ExpiryTime), + {ok, State} + end. + +%%%=================================================================== +%%% Socket management +%%%=================================================================== +-spec send(state(), mqtt_packet()) -> + {ok, state()} | + {error, state(), error_reason()}. +send(State, #publish{} = Pkt) -> + case is_expired(Pkt) of + {false, Pkt1} -> + {ok, do_send(State, Pkt1)}; + true -> + {ok, State} + end; +send(State, Pkt) -> + {ok, do_send(State, Pkt)}. + +-spec do_send(state(), mqtt_packet()) -> state(). +do_send(#state{ws_codec = WSCodec, socket = {SockMod, Sock} = Socket} = State, Pkt) + when WSCodec /= none -> + ?DEBUG("Send MQTT packet:~n~ts", [pp(Pkt)]), + Data = mqtt_codec:encode(State#state.version, Pkt), + WSData = ejabberd_websocket_codec:encode(WSCodec, 2, Data), + Res = SockMod:send(Sock, WSData), + check_sock_result(Socket, Res), + reset_ping_timer(State); +do_send(#state{socket = {SockMod, Sock} = Socket} = State, Pkt) -> + ?DEBUG("Send MQTT packet:~n~ts", [pp(Pkt)]), + Data = mqtt_codec:encode(State#state.version, Pkt), + Res = SockMod:send(Sock, Data), + check_sock_result(Socket, Res), + reset_ping_timer(State); +do_send(State, _Pkt) -> + State. + +-spec disconnect(state(), error_reason()) -> state(). +disconnect(#state{socket = {SockMod, Sock}} = State, Err) -> + State1 = case Err of + {auth, Code} -> + do_send(State, #connack{code = Code}); + {codec, {Tag, _, _} = CErr} when Tag == unsupported_protocol_version; + Tag == unsupported_protocol_name -> + do_send(State#state{version = ?MQTT_VERSION_4}, + #connack{code = mqtt_codec:error_reason_code(CErr)}); + _ when State#state.version == undefined -> + State; + {Tag, _} when Tag == socket; Tag == tls -> + State; + {peer_disconnected, _, _} -> + State; + _ -> + case State of + _ when State#state.version == ?MQTT_VERSION_5 -> + Code = disconnect_reason_code(Err), + Pkt = #disconnect{code = Code}, + do_send(State, Pkt); + _ -> + State + end + end, + SockMod:close(Sock), + State1#state{socket = undefined, + version = undefined, + codec = mqtt_codec:renew(State#state.codec)}; +disconnect(State, _) -> + State. + +-spec reset_ping_timer(state()) -> state(). +reset_ping_timer(State) -> + misc:cancel_timer(State#state.ping_timer), + State#state{ping_timer = erlang:start_timer(?PING_TIMEOUT, self(), ping_timeout)}. + +-spec check_sock_result(socket(), ok | {error, inet:posix()}) -> ok. +check_sock_result(_, ok) -> + ok; +check_sock_result({_, Sock}, {error, Why}) -> + self() ! {tcp_closed, Sock}, + ?DEBUG("MQTT socket error: ~p", [format_inet_error(Why)]). + +%%%=================================================================== +%%% Formatters +%%%=================================================================== +-spec pp(any()) -> iolist(). +pp(Term) -> + io_lib_pretty:print(Term, fun pp/2). + +-spec format_inet_error(socket_error_reason()) -> string(). +format_inet_error(closed) -> + "connection closed"; +format_inet_error(timeout) -> + format_inet_error(etimedout); +format_inet_error(Reason) -> + case inet:format_error(Reason) of + "unknown POSIX error" -> atom_to_list(Reason); + Txt -> Txt + end. + +-spec pp(atom(), non_neg_integer()) -> [atom()] | no. +pp(state, 17) -> record_info(fields, state); +pp(Rec, Size) -> mqtt_codec:pp(Rec, Size). + +-spec disconnect_reason_code(error_reason()) -> reason_code(). +disconnect_reason_code({code, Code}) -> Code; +disconnect_reason_code({codec, Err}) -> mqtt_codec:error_reason_code(Err); +disconnect_reason_code({unexpected_packet, _}) -> 'protocol-error'; +disconnect_reason_code({replaced, _}) -> 'session-taken-over'; +disconnect_reason_code({resumed, _}) -> 'session-taken-over'; +disconnect_reason_code(internal_server_error) -> 'implementation-specific-error'; +disconnect_reason_code(db_failure) -> 'implementation-specific-error'; +disconnect_reason_code(idle_connection) -> 'keep-alive-timeout'; +disconnect_reason_code(queue_full) -> 'quota-exceeded'; +disconnect_reason_code(shutdown) -> 'server-shutting-down'; +disconnect_reason_code(subscribe_forbidden) -> 'topic-filter-invalid'; +disconnect_reason_code(publish_forbidden) -> 'topic-name-invalid'; +disconnect_reason_code(will_topic_forbidden) -> 'topic-name-invalid'; +disconnect_reason_code({payload_format_invalid, _}) -> 'payload-format-invalid'; +disconnect_reason_code(session_expiry_non_zero) -> 'protocol-error'; +disconnect_reason_code(unknown_topic_alias) -> 'protocol-error'; +disconnect_reason_code(_) -> 'unspecified-error'. + +%%%=================================================================== +%%% Timings +%%%=================================================================== +-spec unix_time() -> seconds(). +unix_time() -> + erlang:system_time(second). + +-spec is_expired(publish()) -> true | {false, publish()}. +is_expired(#publish{meta = Meta, properties = Props} = Pkt) -> + case maps:get(expiry_time, Meta, ?MAX_UINT32) of + ?MAX_UINT32 -> + {false, Pkt}; + ExpiryTime -> + Left = ExpiryTime - unix_time(), + if Left > 0 -> + Props1 = Props#{message_expiry_interval => Left}, + {false, Pkt#publish{properties = Props1}}; + true -> + ?DEBUG("Dropping expired packet:~n~ts", [pp(Pkt)]), + true + end + end. diff --git a/src/mod_mqtt_mnesia.erl b/src/mod_mqtt_mnesia.erl new file mode 100644 index 000000000..5c2902d2b --- /dev/null +++ b/src/mod_mqtt_mnesia.erl @@ -0,0 +1,314 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov +%%% @copyright (C) 2002-2025 ProcessOne, SARL. All Rights Reserved. +%%% +%%% Licensed under the Apache License, Version 2.0 (the "License"); +%%% you may not use this file except in compliance with the License. +%%% You may obtain a copy of the License at +%%% +%%% http://www.apache.org/licenses/LICENSE-2.0 +%%% +%%% Unless required by applicable law or agreed to in writing, software +%%% distributed under the License is distributed on an "AS IS" BASIS, +%%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%%% See the License for the specific language governing permissions and +%%% limitations under the License. +%%% +%%%------------------------------------------------------------------- +-module(mod_mqtt_mnesia). +-behaviour(mod_mqtt). + +%% API +-export([init/2, publish/6, delete_published/2, lookup_published/2]). +-export([list_topics/1, use_cache/1]). +-export([init/0]). +-export([subscribe/4, unsubscribe/2, find_subscriber/2, mqtree_match/1]). +-export([open_session/1, close_session/1, lookup_session/1, get_sessions/2]). + +-include("logger.hrl"). +-include("mqtt.hrl"). + +-record(mqtt_pub, {topic_server :: {binary(), binary()}, + user :: binary(), + resource :: binary(), + qos :: 0..2, + payload :: binary(), + expiry :: non_neg_integer(), + payload_format = binary :: binary | utf8, + response_topic = <<>> :: binary(), + correlation_data = <<>> :: binary(), + content_type = <<>> :: binary(), + user_properties = [] :: [{binary(), binary()}]}). + +-record(mqtt_sub, {topic :: {binary(), binary(), binary(), binary()}, + options :: sub_opts(), + id :: non_neg_integer(), + pid :: pid(), + timestamp :: erlang:timestamp()}). + +-record(mqtt_session, {usr :: jid:ljid() | {'_', '_', '$1'}, + pid :: pid() | '_', + timestamp :: erlang:timestamp() | '_'}). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + case ejabberd_mnesia:create( + ?MODULE, mqtt_pub, + [{disc_only_copies, [node()]}, + {attributes, record_info(fields, mqtt_pub)}]) of + {atomic, _} -> + ok; + Err -> + {error, Err} + end. + +use_cache(Host) -> + case mnesia:table_info(mqtt_pub, storage_type) of + disc_only_copies -> + mod_mqtt_opt:use_cache(Host); + _ -> + false + end. + +publish({U, LServer, R}, Topic, Payload, QoS, Props, ExpiryTime) -> + PayloadFormat = maps:get(payload_format_indicator, Props, binary), + ResponseTopic = maps:get(response_topic, Props, <<"">>), + CorrelationData = maps:get(correlation_data, Props, <<"">>), + ContentType = maps:get(content_type, Props, <<"">>), + UserProps = maps:get(user_property, Props, []), + mnesia:dirty_write(#mqtt_pub{topic_server = {Topic, LServer}, + user = U, + resource = R, + qos = QoS, + payload = Payload, + expiry = ExpiryTime, + payload_format = PayloadFormat, + response_topic = ResponseTopic, + correlation_data = CorrelationData, + content_type = ContentType, + user_properties = UserProps}). + +delete_published({_, S, _}, Topic) -> + mnesia:dirty_delete(mqtt_pub, {Topic, S}). + +lookup_published({_, S, _}, Topic) -> + case mnesia:dirty_read(mqtt_pub, {Topic, S}) of + [#mqtt_pub{qos = QoS, + payload = Payload, + expiry = ExpiryTime, + payload_format = PayloadFormat, + response_topic = ResponseTopic, + correlation_data = CorrelationData, + content_type = ContentType, + user_properties = UserProps}] -> + Props = #{payload_format_indicator => PayloadFormat, + response_topic => ResponseTopic, + correlation_data => CorrelationData, + content_type => ContentType, + user_property => UserProps}, + {ok, {Payload, QoS, Props, ExpiryTime}}; + [] -> + {error, notfound} + end. + +list_topics(S) -> + {ok, [Topic || {Topic, S1} <- mnesia:dirty_all_keys(mqtt_pub), S1 == S]}. + +init() -> + case mqtree:whereis(mqtt_sub_index) of + undefined -> + T = mqtree:new(), + mqtree:register(mqtt_sub_index, T); + _ -> + ok + end, + try + {atomic, ok} = ejabberd_mnesia:create( + ?MODULE, + mqtt_session, + [{ram_copies, [node()]}, + {attributes, record_info(fields, mqtt_session)}]), + {atomic, ok} = ejabberd_mnesia:create( + ?MODULE, + mqtt_sub, + [{ram_copies, [node()]}, + {type, ordered_set}, + {attributes, record_info(fields, mqtt_sub)}]), + ok + catch _:{badmatch, Err} -> + {error, Err} + end. + +open_session(USR) -> + TS1 = misc:unique_timestamp(), + P1 = self(), + F = fun() -> + case mnesia:read(mqtt_session, USR) of + [#mqtt_session{pid = P2, timestamp = TS2}] -> + if TS1 >= TS2 -> + mod_mqtt_session:route(P2, {replaced, P1}), + mnesia:write( + #mqtt_session{usr = USR, + pid = P1, + timestamp = TS1}); + true -> + case is_process_dead(P2) of + true -> + mnesia:write( + #mqtt_session{usr = USR, + pid = P1, + timestamp = TS1}); + false -> + mod_mqtt_session:route(P1, {replaced, P2}) + end + end; + [] -> + mnesia:write( + #mqtt_session{usr = USR, + pid = P1, + timestamp = TS1}) + end + end, + case mnesia:transaction(F) of + {atomic, _} -> ok; + {aborted, Reason} -> + db_fail("Failed to register MQTT session for ~ts", + Reason, [jid:encode(USR)]) + end. + +close_session(USR) -> + close_session(USR, self()). + +lookup_session(USR) -> + case mnesia:dirty_read(mqtt_session, USR) of + [#mqtt_session{pid = Pid}] -> + case is_process_dead(Pid) of + true -> + %% Read-Repair + close_session(USR, Pid), + {error, notfound}; + false -> + {ok, Pid} + end; + [] -> + {error, notfound} + end. + +get_sessions(U, S) -> + Resources = mnesia:dirty_select(mqtt_session, + [{#mqtt_session{usr = {U, S, '$1'}, + _ = '_'}, + [], + ['$1']}]), + [{U, S, Resource} || Resource <- Resources]. + +subscribe({U, S, R} = USR, TopicFilter, SubOpts, ID) -> + T1 = misc:unique_timestamp(), + P1 = self(), + Key = {TopicFilter, S, U, R}, + F = fun() -> + case mnesia:read(mqtt_sub, Key) of + [#mqtt_sub{timestamp = T2}] when T1 < T2 -> + ok; + _ -> + Tree = mqtree:whereis(mqtt_sub_index), + mqtree:insert(Tree, TopicFilter), + mnesia:write( + #mqtt_sub{topic = {TopicFilter, S, U, R}, + options = SubOpts, + id = ID, + pid = P1, + timestamp = T1}) + end + end, + case mnesia:transaction(F) of + {atomic, _} -> ok; + {aborted, Reason} -> + db_fail("Failed to subscribe ~ts to ~ts", + Reason, [jid:encode(USR), TopicFilter]) + end. + +unsubscribe({U, S, R} = USR, Topic) -> + Pid = self(), + F = fun() -> + Tree = mqtree:whereis(mqtt_sub_index), + mqtree:delete(Tree, Topic), + case mnesia:read(mqtt_sub, {Topic, S, U, R}) of + [#mqtt_sub{pid = Pid} = Obj] -> + mnesia:delete_object(Obj); + _ -> + ok + end + end, + case mnesia:transaction(F) of + {atomic, _} -> ok; + {aborted, Reason} -> + db_fail("Failed to unsubscribe ~ts from ~ts", + Reason, [jid:encode(USR), Topic]) + end. + +mqtree_match(Topic) -> + Tree = mqtree:whereis(mqtt_sub_index), + mqtree:match(Tree, Topic). + +mqtree_multi_match(Topic) -> + {Res, []} = ejabberd_cluster:multicall(?MODULE, mqtree_match, [Topic]), + lists:umerge(Res). + +find_subscriber(S, Topic) when is_binary(Topic) -> + case mqtree_multi_match(Topic) of + [Filter|Filters] -> + find_subscriber(S, {Filters, {Filter, S, '_', '_'}}); + [] -> + {error, notfound} + end; +find_subscriber(S, {Filters, {Filter, S, _, _} = Prev}) -> + case mnesia:dirty_next(mqtt_sub, Prev) of + {Filter, S, _, _} = Next -> + case mnesia:dirty_read(mqtt_sub, Next) of + [#mqtt_sub{options = SubOpts, id = ID, pid = Pid}] -> + case is_process_dead(Pid) of + true -> + find_subscriber(S, {Filters, Next}); + false -> + {ok, {Pid, SubOpts, ID}, {Filters, Next}} + end; + [] -> + find_subscriber(S, {Filters, Next}) + end; + _ -> + case Filters of + [] -> + {error, notfound}; + [Filter1|Filters1] -> + find_subscriber(S, {Filters1, {Filter1, S, '_', '_'}}) + end + end. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +close_session(USR, Pid) -> + F = fun() -> + case mnesia:read(mqtt_session, USR) of + [#mqtt_session{pid = Pid} = Obj] -> + mnesia:delete_object(Obj); + _ -> + ok + end + end, + case mnesia:transaction(F) of + {atomic, _} -> ok; + {aborted, Reason} -> + db_fail("Failed to unregister MQTT session for ~ts", + Reason, [jid:encode(USR)]) + end. + +is_process_dead(Pid) -> + node(Pid) == node() andalso not is_process_alive(Pid). + +db_fail(Format, Reason, Args) -> + ?ERROR_MSG(Format ++ ": ~p", Args ++ [Reason]), + {error, db_failure}. diff --git a/src/mod_mqtt_opt.erl b/src/mod_mqtt_opt.erl new file mode 100644 index 000000000..5459f39e8 --- /dev/null +++ b/src/mod_mqtt_opt.erl @@ -0,0 +1,104 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_mqtt_opt). + +-export([access_publish/1]). +-export([access_subscribe/1]). +-export([cache_life_time/1]). +-export([cache_missed/1]). +-export([cache_size/1]). +-export([db_type/1]). +-export([match_retained_limit/1]). +-export([max_queue/1]). +-export([max_topic_aliases/1]). +-export([max_topic_depth/1]). +-export([queue_type/1]). +-export([ram_db_type/1]). +-export([session_expiry/1]). +-export([use_cache/1]). + +-spec access_publish(gen_mod:opts() | global | binary()) -> [{[binary()],acl:acl()}]. +access_publish(Opts) when is_map(Opts) -> + gen_mod:get_opt(access_publish, Opts); +access_publish(Host) -> + gen_mod:get_module_opt(Host, mod_mqtt, access_publish). + +-spec access_subscribe(gen_mod:opts() | global | binary()) -> [{[binary()],acl:acl()}]. +access_subscribe(Opts) when is_map(Opts) -> + gen_mod:get_opt(access_subscribe, Opts); +access_subscribe(Host) -> + gen_mod:get_module_opt(Host, mod_mqtt, access_subscribe). + +-spec cache_life_time(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +cache_life_time(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_life_time, Opts); +cache_life_time(Host) -> + gen_mod:get_module_opt(Host, mod_mqtt, cache_life_time). + +-spec cache_missed(gen_mod:opts() | global | binary()) -> boolean(). +cache_missed(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_missed, Opts); +cache_missed(Host) -> + gen_mod:get_module_opt(Host, mod_mqtt, cache_missed). + +-spec cache_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +cache_size(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_size, Opts); +cache_size(Host) -> + gen_mod:get_module_opt(Host, mod_mqtt, cache_size). + +-spec db_type(gen_mod:opts() | global | binary()) -> atom(). +db_type(Opts) when is_map(Opts) -> + gen_mod:get_opt(db_type, Opts); +db_type(Host) -> + gen_mod:get_module_opt(Host, mod_mqtt, db_type). + +-spec match_retained_limit(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +match_retained_limit(Opts) when is_map(Opts) -> + gen_mod:get_opt(match_retained_limit, Opts); +match_retained_limit(Host) -> + gen_mod:get_module_opt(Host, mod_mqtt, match_retained_limit). + +-spec max_queue(gen_mod:opts() | global | binary()) -> 'unlimited' | pos_integer(). +max_queue(Opts) when is_map(Opts) -> + gen_mod:get_opt(max_queue, Opts); +max_queue(Host) -> + gen_mod:get_module_opt(Host, mod_mqtt, max_queue). + +-spec max_topic_aliases(gen_mod:opts() | global | binary()) -> char(). +max_topic_aliases(Opts) when is_map(Opts) -> + gen_mod:get_opt(max_topic_aliases, Opts); +max_topic_aliases(Host) -> + gen_mod:get_module_opt(Host, mod_mqtt, max_topic_aliases). + +-spec max_topic_depth(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +max_topic_depth(Opts) when is_map(Opts) -> + gen_mod:get_opt(max_topic_depth, Opts); +max_topic_depth(Host) -> + gen_mod:get_module_opt(Host, mod_mqtt, max_topic_depth). + +-spec queue_type(gen_mod:opts() | global | binary()) -> 'file' | 'ram'. +queue_type(Opts) when is_map(Opts) -> + gen_mod:get_opt(queue_type, Opts); +queue_type(Host) -> + gen_mod:get_module_opt(Host, mod_mqtt, queue_type). + +-spec ram_db_type(gen_mod:opts() | global | binary()) -> atom(). +ram_db_type(Opts) when is_map(Opts) -> + gen_mod:get_opt(ram_db_type, Opts); +ram_db_type(Host) -> + gen_mod:get_module_opt(Host, mod_mqtt, ram_db_type). + +-spec session_expiry(gen_mod:opts() | global | binary()) -> non_neg_integer(). +session_expiry(Opts) when is_map(Opts) -> + gen_mod:get_opt(session_expiry, Opts); +session_expiry(Host) -> + gen_mod:get_module_opt(Host, mod_mqtt, session_expiry). + +-spec use_cache(gen_mod:opts() | global | binary()) -> boolean(). +use_cache(Opts) when is_map(Opts) -> + gen_mod:get_opt(use_cache, Opts); +use_cache(Host) -> + gen_mod:get_module_opt(Host, mod_mqtt, use_cache). + diff --git a/src/mod_mqtt_session.erl b/src/mod_mqtt_session.erl new file mode 100644 index 000000000..c6d0338d9 --- /dev/null +++ b/src/mod_mqtt_session.erl @@ -0,0 +1,1442 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov +%%% @copyright (C) 2002-2025 ProcessOne, SARL. All Rights Reserved. +%%% +%%% Licensed under the Apache License, Version 2.0 (the "License"); +%%% you may not use this file except in compliance with the License. +%%% You may obtain a copy of the License at +%%% +%%% http://www.apache.org/licenses/LICENSE-2.0 +%%% +%%% Unless required by applicable law or agreed to in writing, software +%%% distributed under the License is distributed on an "AS IS" BASIS, +%%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%%% See the License for the specific language governing permissions and +%%% limitations under the License. +%%% +%%%------------------------------------------------------------------- +-module(mod_mqtt_session). +-behaviour(p1_server). +-define(VSN, 2). +-vsn(?VSN). + +%% API +-export([start/3, start_link/3, accept/1, route/2]). +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-include("logger.hrl"). +-include("mqtt.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). +-include_lib("public_key/include/public_key.hrl"). + +-record(state, {vsn = ?VSN :: integer(), + version :: undefined | mqtt_version(), + socket :: undefined | socket(), + peername :: undefined | peername(), + timeout = infinity :: timer(), + jid :: undefined | jid:jid(), + session_expiry = 0 :: milli_seconds(), + will :: undefined | publish(), + will_delay = 0 :: milli_seconds(), + stop_reason :: undefined | error_reason(), + acks = #{} :: acks(), + subscriptions = #{} :: subscriptions(), + topic_aliases = #{} :: topic_aliases(), + id = 0 :: non_neg_integer(), + in_flight :: undefined | publish() | pubrel(), + codec :: mqtt_codec:state(), + queue :: undefined | p1_queue:queue(publish()), + tls :: boolean(), + tls_verify :: boolean()}). + +-type acks() :: #{non_neg_integer() => pubrec()}. +-type subscriptions() :: #{binary() => {sub_opts(), non_neg_integer()}}. +-type topic_aliases() :: #{non_neg_integer() => binary()}. + +-type error_reason() :: {auth, reason_code()} | + {code, reason_code()} | + {peer_disconnected, reason_code(), binary()} | + {socket, socket_error_reason()} | + {codec, mqtt_codec:error_reason()} | + {unexpected_packet, atom()} | + {tls, inet:posix() | atom() | binary()} | + {replaced, pid()} | {resumed, pid()} | + subscribe_forbidden | publish_forbidden | + will_topic_forbidden | internal_server_error | + session_expired | idle_connection | + queue_full | shutdown | db_failure | + {payload_format_invalid, will | publish} | + session_expiry_non_zero | unknown_topic_alias. + +-type state() :: #state{}. +-type socket() :: {gen_tcp, inet:socket()} | + {fast_tls, fast_tls:tls_socket()} | + {mod_mqtt_ws, mod_mqtt_ws:socket()}. +-type peername() :: {inet:ip_address(), inet:port_number()}. +-type seconds() :: non_neg_integer(). +-type milli_seconds() :: non_neg_integer(). +-type timer() :: infinity | {milli_seconds(), integer()}. +-type socket_error_reason() :: closed | timeout | inet:posix(). + +-define(CALL_TIMEOUT, timer:minutes(1)). +-define(RELAY_TIMEOUT, timer:minutes(1)). +-define(MAX_UINT32, 4294967295). + +%%%=================================================================== +%%% API +%%%=================================================================== +start(SockMod, Socket, ListenOpts) -> + p1_server:start(?MODULE, [SockMod, Socket, ListenOpts], + ejabberd_config:fsm_limit_opts(ListenOpts)). + +start_link(SockMod, Socket, ListenOpts) -> + p1_server:start_link(?MODULE, [SockMod, Socket, ListenOpts], + ejabberd_config:fsm_limit_opts(ListenOpts)). + +-spec accept(pid()) -> ok. +accept(Pid) -> + p1_server:cast(Pid, accept). + +-spec route(pid(), term()) -> boolean(). +route(Pid, Term) -> + ejabberd_cluster:send(Pid, Term). + +-spec format_error(error_reason()) -> string(). +format_error(session_expired) -> + "Disconnected session is expired"; +format_error(idle_connection) -> + "Idle connection"; +format_error(queue_full) -> + "Message queue is overloaded"; +format_error(internal_server_error) -> + "Internal server error"; +format_error(db_failure) -> + "Database failure"; +format_error(shutdown) -> + "System shutting down"; +format_error(subscribe_forbidden) -> + "Subscribing to this topic is forbidden by service policy"; +format_error(publish_forbidden) -> + "Publishing to this topic is forbidden by service policy"; +format_error(will_topic_forbidden) -> + "Publishing to this will topic is forbidden by service policy"; +format_error(session_expiry_non_zero) -> + "Session Expiry Interval in DISCONNECT packet should have been zero"; +format_error(unknown_topic_alias) -> + "No mapping found for this Topic Alias"; +format_error({payload_format_invalid, will}) -> + "Will payload format doesn't match its indicator"; +format_error({payload_format_invalid, publish}) -> + "PUBLISH payload format doesn't match its indicator"; +format_error({peer_disconnected, Code, <<>>}) -> + format("Peer disconnected with reason: ~ts", + [mqtt_codec:format_reason_code(Code)]); +format_error({peer_disconnected, Code, Reason}) -> + format("Peer disconnected with reason: ~ts (~ts)", [Reason, Code]); +format_error({replaced, Pid}) -> + format("Replaced by ~p at ~ts", [Pid, node(Pid)]); +format_error({resumed, Pid}) -> + format("Resumed by ~p at ~ts", [Pid, node(Pid)]); +format_error({unexpected_packet, Name}) -> + format("Unexpected ~ts packet", [string:to_upper(atom_to_list(Name))]); +format_error({tls, Reason}) -> + format("TLS failed: ~ts", [format_tls_error(Reason)]); +format_error({socket, A}) -> + format("Connection failed: ~ts", [format_inet_error(A)]); +format_error({code, Code}) -> + format("Protocol error: ~ts", [mqtt_codec:format_reason_code(Code)]); +format_error({auth, Code}) -> + format("Authentication failed: ~ts", [mqtt_codec:format_reason_code(Code)]); +format_error({codec, CodecError}) -> + format("Protocol error: ~ts", [mqtt_codec:format_error(CodecError)]); +format_error(A) when is_atom(A) -> + atom_to_list(A); +format_error(Reason) -> + format("Unrecognized error: ~w", [Reason]). + +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== +init([SockMod, Socket, ListenOpts]) -> + MaxSize = proplists:get_value(max_payload_size, ListenOpts, infinity), + State1 = #state{socket = {SockMod, Socket}, + id = p1_rand:uniform(65535), + tls = proplists:get_bool(tls, ListenOpts), + tls_verify = proplists:get_bool(tls_verify, ListenOpts), + codec = mqtt_codec:new(MaxSize)}, + Timeout = timer:seconds(30), + State2 = set_timeout(State1, Timeout), + {ok, State2, Timeout}. + +handle_call({get_state, _}, From, #state{stop_reason = {resumed, Pid}} = State) -> + p1_server:reply(From, {error, {resumed, Pid}}), + noreply(State); +handle_call({get_state, Pid}, From, State) -> + case stop(State, {resumed, Pid}) of + {stop, Status, State1} -> + {stop, Status, State1#state{stop_reason = {replaced, Pid}}}; + {noreply, State1, _} -> + ?DEBUG("Transferring MQTT session state to ~p at ~ts", [Pid, node(Pid)]), + Q1 = p1_queue:file_to_ram(State1#state.queue), + p1_server:reply(From, {ok, State1#state{queue = Q1}}), + SessionExpiry = State1#state.session_expiry, + State2 = set_timeout(State1, min(SessionExpiry, ?RELAY_TIMEOUT)), + State3 = State2#state{queue = undefined, + stop_reason = {resumed, Pid}, + acks = #{}, + will = undefined, + session_expiry = 0, + topic_aliases = #{}, + subscriptions = #{}}, + noreply(State3) + end; +handle_call(Request, From, State) -> + ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), + noreply(State). + +handle_cast(accept, #state{socket = {_, Sock}} = State) -> + case peername(State) of + {ok, IPPort} -> + State1 = State#state{peername = IPPort}, + case starttls(State) of + {ok, Socket1} -> + State2 = State1#state{socket = Socket1}, + handle_info({tcp, Sock, <<>>}, State2); + {error, Why} -> + stop(State1, Why) + end; + {error, Why} -> + stop(State, {socket, Why}) + end; +handle_cast(Msg, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Msg]), + noreply(State). + +handle_info(Msg, #state{stop_reason = {resumed, Pid} = Reason} = State) -> + case Msg of + {#publish{}, _} -> + ?DEBUG("Relaying delayed publish to ~p at ~ts", [Pid, node(Pid)]), + ejabberd_cluster:send(Pid, Msg), + noreply(State); + timeout -> + stop(State, Reason); + _ -> + noreply(State) + end; +handle_info({#publish{meta = Meta} = Pkt, ExpiryTime}, State) -> + ID = next_id(State#state.id), + Meta1 = Meta#{expiry_time => ExpiryTime}, + Pkt1 = Pkt#publish{id = ID, meta = Meta1}, + State1 = State#state{id = ID}, + case send(State1, Pkt1) of + {ok, State2} -> noreply(State2); + {error, State2, Reason} -> stop(State2, Reason) + end; +handle_info({tcp, TCPSock, TCPData}, + #state{codec = Codec, socket = Socket} = State) -> + case recv_data(Socket, TCPData) of + {ok, Data} -> + case mqtt_codec:decode(Codec, Data) of + {ok, Pkt, Codec1} -> + ?DEBUG("Got MQTT packet:~n~ts", [pp(Pkt)]), + State1 = State#state{codec = Codec1}, + case handle_packet(Pkt, State1) of + {ok, State2} -> + handle_info({tcp, TCPSock, <<>>}, State2); + {error, State2, Reason} -> + stop(State2, Reason) + end; + {more, Codec1} -> + State1 = State#state{codec = Codec1}, + State2 = reset_keep_alive(State1), + activate(Socket), + noreply(State2); + {error, Why} -> + stop(State, {codec, Why}) + end; + {error, Why} -> + stop(State, Why) + end; +handle_info({tcp_closed, _Sock}, State) -> + ?DEBUG("MQTT connection reset by peer", []), + stop(State, {socket, closed}); +handle_info({tcp_error, _Sock, Reason}, State) -> + ?DEBUG("MQTT connection error: ~ts", [format_inet_error(Reason)]), + stop(State, {socket, Reason}); +handle_info(timeout, #state{socket = Socket} = State) -> + case Socket of + undefined -> + ?DEBUG("MQTT session expired", []), + stop(State#state{session_expiry = 0}, session_expired); + _ -> + ?DEBUG("MQTT connection timed out", []), + stop(State, idle_connection) + end; +handle_info({replaced, Pid}, State) -> + stop(State#state{session_expiry = 0}, {replaced, Pid}); +handle_info({timeout, _TRef, publish_will}, State) -> + noreply(publish_will(State)); +handle_info({Ref, badarg}, State) when is_reference(Ref) -> + %% TODO: figure out from where this messages comes from + noreply(State); +handle_info(Info, State) -> + ?WARNING_MSG("Unexpected info: ~p", [Info]), + noreply(State). + +-spec handle_packet(mqtt_packet(), state()) -> {ok, state()} | + {error, state(), error_reason()}. +handle_packet(#connect{proto_level = Version} = Pkt, State) -> + handle_connect(Pkt, State#state{version = Version}); +handle_packet(#publish{} = Pkt, State) -> + handle_publish(Pkt, State); +handle_packet(#puback{id = ID}, #state{in_flight = #publish{qos = 1, id = ID}} = State) -> + resend(State#state{in_flight = undefined}); +handle_packet(#puback{id = ID, code = Code}, State) -> + ?DEBUG("Ignoring unexpected PUBACK with id=~B and code '~ts'", [ID, Code]), + {ok, State}; +handle_packet(#pubrec{id = ID, code = Code}, + #state{in_flight = #publish{qos = 2, id = ID}} = State) -> + case mqtt_codec:is_error_code(Code) of + true -> + ?DEBUG("Got PUBREC with error code '~ts', " + "aborting acknowledgement", [Code]), + resend(State#state{in_flight = undefined}); + false -> + Pubrel = #pubrel{id = ID}, + send(State#state{in_flight = Pubrel}, Pubrel) + end; +handle_packet(#pubrec{id = ID, code = Code}, State) -> + case mqtt_codec:is_error_code(Code) of + true -> + ?DEBUG("Ignoring unexpected PUBREC with id=~B and code '~ts'", + [ID, Code]), + {ok, State}; + false -> + Code1 = 'packet-identifier-not-found', + ?DEBUG("Unexpected PUBREC with id=~B, " + "sending PUBREL with error code '~ts'", [ID, Code1]), + send(State, #pubrel{id = ID, code = Code1}) + end; +handle_packet(#pubcomp{id = ID}, #state{in_flight = #pubrel{id = ID}} = State) -> + resend(State#state{in_flight = undefined}); +handle_packet(#pubcomp{id = ID}, State) -> + ?DEBUG("Ignoring unexpected PUBCOMP with id=~B: most likely " + "it's a repeated response to duplicated PUBREL", [ID]), + {ok, State}; +handle_packet(#pubrel{id = ID}, State) -> + case maps:take(ID, State#state.acks) of + {_, Acks} -> + send(State#state{acks = Acks}, #pubcomp{id = ID}); + error -> + Code = 'packet-identifier-not-found', + ?DEBUG("Unexpected PUBREL with id=~B, " + "sending PUBCOMP with error code '~ts'", [ID, Code]), + Pubcomp = #pubcomp{id = ID, code = Code}, + send(State, Pubcomp) + end; +handle_packet(#subscribe{} = Pkt, State) -> + handle_subscribe(Pkt, State); +handle_packet(#unsubscribe{} = Pkt, State) -> + handle_unsubscribe(Pkt, State); +handle_packet(#pingreq{}, State) -> + send(State, #pingresp{}); +handle_packet(#disconnect{properties = #{session_expiry_interval := SE}}, + #state{session_expiry = 0} = State) when SE>0 -> + %% Protocol violation + {error, State, session_expiry_non_zero}; +handle_packet(#disconnect{code = Code, properties = Props}, + #state{jid = #jid{lserver = Server}} = State) -> + Reason = maps:get(reason_string, Props, <<>>), + Expiry = case maps:get(session_expiry_interval, Props, undefined) of + undefined -> State#state.session_expiry; + SE -> min(timer:seconds(SE), session_expiry(Server)) + end, + State1 = State#state{session_expiry = Expiry}, + State2 = case Code of + 'normal-disconnection' -> State1#state{will = undefined}; + _ -> State1 + end, + {error, State2, {peer_disconnected, Code, Reason}}; +handle_packet(Pkt, State) -> + ?WARNING_MSG("Unexpected packet:~n~ts~n** when state:~n~ts", + [pp(Pkt), pp(State)]), + {error, State, {unexpected_packet, element(1, Pkt)}}. + +terminate(_, #state{peername = undefined}) -> + ok; +terminate(Reason, State) -> + Reason1 = case Reason of + shutdown -> shutdown; + {shutdown, _} -> shutdown; + normal -> State#state.stop_reason; + {process_limit, _} -> queue_full; + _ -> internal_server_error + end, + case State#state.jid of + #jid{} -> unregister_session(State, Reason1); + undefined -> log_disconnection(State, Reason1) + end, + State1 = disconnect(State, Reason1), + publish_will(State1). + +code_change(_OldVsn, State, _Extra) -> + {ok, upgrade_state(State)}. + +%%%=================================================================== +%%% State transitions +%%%=================================================================== +-spec noreply(state()) -> {noreply, state(), non_neg_integer() | infinity}. +noreply(#state{timeout = infinity} = State) -> + {noreply, State, infinity}; +noreply(#state{timeout = {MSecs, StartTime}} = State) -> + CurrentTime = current_time(), + Timeout = max(0, MSecs - CurrentTime + StartTime), + {noreply, State, Timeout}. + +-spec stop(state(), error_reason()) -> {noreply, state(), infinity} | + {stop, normal, state()}. +stop(#state{session_expiry = 0} = State, Reason) -> + {stop, normal, State#state{stop_reason = Reason}}; +stop(#state{session_expiry = SessExp} = State, Reason) -> + case State#state.socket of + undefined -> + noreply(State); + _ -> + WillDelay = State#state.will_delay, + log_disconnection(State, Reason), + State1 = disconnect(State, Reason), + State2 = if WillDelay == 0 -> + publish_will(State1); + WillDelay < SessExp -> + erlang:start_timer(WillDelay, self(), publish_will), + State1; + true -> + State1 + end, + State3 = set_timeout(State2, SessExp), + State4 = State3#state{stop_reason = Reason}, + noreply(State4) + end. + +%% Here is the code upgrading state between different +%% code versions. This is needed when doing session resumption from +%% remote node running the version of the code with incompatible #state{} +%% record fields. Also used by code_change/3 callback. +-spec upgrade_state(tuple()) -> state(). +upgrade_state(State) -> + case element(2, State) of + ?VSN -> + State; + VSN when VSN > ?VSN -> + erlang:error({downgrade_not_supported, State}); + VSN -> + State1 = upgrade_state(State, VSN), + upgrade_state(setelement(2, State1, VSN+1)) + end. + +-spec upgrade_state(tuple(), integer()) -> tuple(). +upgrade_state(OldState, 1) -> + %% Appending 'tls' field + erlang:append_element(OldState, false); +upgrade_state(State, _VSN) -> + State. + +%%%=================================================================== +%%% Session management +%%%=================================================================== +-spec open_session(state(), jid(), boolean()) -> {ok, boolean(), state()} | + {error, state(), error_reason()}. +open_session(State, JID, _CleanStart = false) -> + USR = {_, S, _} = jid:tolower(JID), + case mod_mqtt:lookup_session(USR) of + {ok, Pid} -> + try p1_server:call(Pid, {get_state, self()}, ?CALL_TIMEOUT) of + {ok, State1} -> + State2 = upgrade_state(State1), + Q1 = case queue_type(S) of + ram -> State2#state.queue; + _ -> p1_queue:ram_to_file(State2#state.queue) + end, + Q2 = p1_queue:set_limit(Q1, queue_limit(S)), + State3 = State#state{queue = Q2, + acks = State2#state.acks, + subscriptions = State2#state.subscriptions, + id = State2#state.id, + in_flight = State2#state.in_flight}, + ?DEBUG("Resumed state from ~p at ~ts:~n~ts", + [Pid, node(Pid), pp(State3)]), + register_session(State3, JID, Pid); + {error, Why} -> + {error, State, Why} + catch exit:{Why, {p1_server, _, _}} -> + ?WARNING_MSG("Failed to copy session state from ~p at ~ts: ~ts", + [Pid, node(Pid), format_exit_reason(Why)]), + register_session(State, JID, undefined) + end; + {error, notfound} -> + register_session(State, JID, undefined); + {error, Why} -> + {error, State, Why} + end; +open_session(State, JID, _CleanStart = true) -> + register_session(State, JID, undefined). + +-spec register_session(state(), jid(), undefined | pid()) -> + {ok, boolean(), state()} | {error, state(), error_reason()}. +register_session(#state{peername = IP} = State, JID, Parent) -> + USR = {_, S, _} = jid:tolower(JID), + case mod_mqtt:open_session(USR) of + ok -> + case resubscribe(USR, State#state.subscriptions) of + ok -> + ?INFO_MSG("~ts for ~ts from ~ts", + [if is_pid(Parent) -> + io_lib:format( + "Reopened MQTT session via ~p", + [Parent]); + true -> + "Opened MQTT session" + end, + jid:encode(JID), + ejabberd_config:may_hide_data( + misc:ip_to_list(IP))]), + Q = case State#state.queue of + undefined -> + p1_queue:new(queue_type(S), queue_limit(S)); + Q1 -> + Q1 + end, + {ok, is_pid(Parent), State#state{jid = JID, queue = Q}}; + {error, Why} -> + mod_mqtt:close_session(USR), + {error, State#state{session_expiry = 0}, Why} + end; + {error, Reason} -> + ?ERROR_MSG("Failed to register MQTT session for ~ts from ~ts: ~ts", + err_args(JID, IP, Reason)), + {error, State, Reason} + end. + +-spec unregister_session(state(), error_reason()) -> ok. +unregister_session(#state{jid = #jid{} = JID, peername = IP} = State, Reason) -> + Msg = "Closing MQTT session for ~ts from ~ts: ~ts", + case Reason of + {Tag, _} when Tag == replaced; Tag == resumed -> + ?DEBUG(Msg, err_args(JID, IP, Reason)); + {socket, _} -> + ?INFO_MSG(Msg, err_args(JID, IP, Reason)); + Tag when Tag == idle_connection; Tag == session_expired; Tag == shutdown -> + ?INFO_MSG(Msg, err_args(JID, IP, Reason)); + {peer_disconnected, Code, _} -> + case mqtt_codec:is_error_code(Code) of + true -> ?WARNING_MSG(Msg, err_args(JID, IP, Reason)); + false -> ?INFO_MSG(Msg, err_args(JID, IP, Reason)) + end; + _ -> + ?WARNING_MSG(Msg, err_args(JID, IP, Reason)) + end, + USR = jid:tolower(JID), + unsubscribe(maps:keys(State#state.subscriptions), USR, #{}), + case mod_mqtt:close_session(USR) of + ok -> ok; + {error, Why} -> + ?ERROR_MSG( + "Failed to close MQTT session for ~ts from ~ts: ~ts", + err_args(JID, IP, Why)) + end; +unregister_session(_, _) -> + ok. + +%%%=================================================================== +%%% CONNECT/PUBLISH/SUBSCRIBE/UNSUBSCRIBE handlers +%%%=================================================================== +-spec handle_connect(connect(), state()) -> {ok, state()} | + {error, state(), error_reason()}. +handle_connect(#connect{clean_start = CleanStart} = Pkt, + #state{jid = undefined, peername = IP} = State) -> + case authenticate(Pkt, IP, State) of + {ok, JID} -> + case validate_will(Pkt, JID) of + ok -> + case open_session(State, JID, CleanStart) of + {ok, SessionPresent, State1} -> + State2 = set_session_properties(State1, Pkt), + ConnackProps = get_connack_properties(State2, Pkt), + Connack = #connack{session_present = SessionPresent, + properties = ConnackProps}, + case send(State2, Connack) of + {ok, State3} -> resend(State3); + {error, _, _} = Err -> Err + end; + {error, _, _} = Err -> + Err + end; + {error, Reason} -> + {error, State, Reason} + end; + {error, Code} -> + {error, State, {auth, Code}} + end. + +-spec handle_publish(publish(), state()) -> {ok, state()} | + {error, state(), error_reason()}. +handle_publish(#publish{qos = QoS, id = ID} = Publish, State) -> + case QoS == 2 andalso maps:is_key(ID, State#state.acks) of + true -> + send(State, maps:get(ID, State#state.acks)); + false -> + case validate_publish(Publish, State) of + ok -> + State1 = store_topic_alias(State, Publish), + Ret = publish(State1, Publish), + {Code, Props} = get_publish_code_props(Ret), + case Ret of + {ok, _} when QoS == 2 -> + Pkt = #pubrec{id = ID, code = Code, + properties = Props}, + Acks = maps:put(ID, Pkt, State1#state.acks), + State2 = State1#state{acks = Acks}, + send(State2, Pkt); + {error, _} when QoS == 2 -> + Pkt = #pubrec{id = ID, code = Code, + properties = Props}, + send(State1, Pkt); + _ when QoS == 1 -> + Pkt = #puback{id = ID, code = Code, + properties = Props}, + send(State1, Pkt); + _ -> + {ok, State1} + end; + {error, Why} -> + {error, State, Why} + end + end. + +-spec handle_subscribe(subscribe(), state()) -> + {ok, state()} | {error, state(), error_reason()}. +handle_subscribe(#subscribe{id = ID, filters = TopicFilters} = Pkt, State) -> + case validate_subscribe(Pkt) of + ok -> + USR = jid:tolower(State#state.jid), + SubID = maps:get(subscription_identifier, Pkt#subscribe.properties, 0), + OldSubs = State#state.subscriptions, + {Codes, NewSubs, Props} = subscribe(TopicFilters, USR, SubID), + Subs = maps:merge(OldSubs, NewSubs), + State1 = State#state{subscriptions = Subs}, + Suback = #suback{id = ID, codes = Codes, properties = Props}, + case send(State1, Suback) of + {ok, State2} -> + Pubs = select_retained(USR, NewSubs, OldSubs), + send_retained(State2, Pubs); + {error, _, _} = Err -> + Err + end; + {error, Why} -> + {error, State, Why} + end. + +-spec handle_unsubscribe(unsubscribe(), state()) -> + {ok, state()} | {error, state(), error_reason()}. +handle_unsubscribe(#unsubscribe{id = ID, filters = TopicFilters}, State) -> + USR = jid:tolower(State#state.jid), + {Codes, Subs, Props} = unsubscribe(TopicFilters, USR, State#state.subscriptions), + State1 = State#state{subscriptions = Subs}, + Unsuback = #unsuback{id = ID, codes = Codes, properties = Props}, + send(State1, Unsuback). + +%%%=================================================================== +%%% Aux functions for CONNECT/PUBLISH/SUBSCRIBE/UNSUBSCRIBE handlers +%%%=================================================================== +-spec set_session_properties(state(), connect()) -> state(). +set_session_properties(#state{version = Version, + jid = #jid{lserver = Server}} = State, + #connect{clean_start = CleanStart, + keep_alive = KeepAlive, + properties = Props} = Pkt) -> + SEMin = case CleanStart of + false when Version == ?MQTT_VERSION_4 -> infinity; + _ -> timer:seconds(maps:get(session_expiry_interval, Props, 0)) + end, + SEConfig = session_expiry(Server), + State1 = State#state{session_expiry = min(SEMin, SEConfig)}, + State2 = set_will_properties(State1, Pkt), + set_keep_alive(State2, KeepAlive). + +-spec set_will_properties(state(), connect()) -> state(). +set_will_properties(State, #connect{will = #publish{} = Will, + will_properties = Props}) -> + {WillDelay, Props1} = case maps:take(will_delay_interval, Props) of + error -> {0, Props}; + Ret -> Ret + end, + State#state{will = Will#publish{properties = Props1}, + will_delay = timer:seconds(WillDelay)}; +set_will_properties(State, _) -> + State. + +-spec get_connack_properties(state(), connect()) -> properties(). +get_connack_properties(#state{session_expiry = SessExp, jid = JID}, + #connect{client_id = ClientID, + keep_alive = KeepAlive, + properties = Props}) -> + Props1 = case ClientID of + <<>> -> #{assigned_client_identifier => JID#jid.lresource}; + _ -> #{} + end, + Props2 = case maps:find(authentication_method, Props) of + {ok, Method} -> Props1#{authentication_method => Method}; + error -> Props1 + end, + Props2#{session_expiry_interval => SessExp div 1000, + shared_subscription_available => false, + topic_alias_maximum => topic_alias_maximum(JID#jid.lserver), + server_keep_alive => KeepAlive}. + +-spec subscribe([{binary(), sub_opts()}], jid:ljid(), non_neg_integer()) -> + {[reason_code()], subscriptions(), properties()}. +subscribe(TopicFilters, USR, SubID) -> + subscribe(TopicFilters, USR, SubID, [], #{}, ok). + +-spec subscribe([{binary(), sub_opts()}], jid:ljid(), non_neg_integer(), + [reason_code()], subscriptions(), ok | {error, error_reason()}) -> + {[reason_code()], subscriptions(), properties()}. +subscribe([{TopicFilter, SubOpts}|TopicFilters], USR, SubID, Codes, Subs, Err) -> + case mod_mqtt:subscribe(USR, TopicFilter, SubOpts, SubID) of + ok -> + Code = subscribe_reason_code(SubOpts#sub_opts.qos), + subscribe(TopicFilters, USR, SubID, [Code|Codes], + maps:put(TopicFilter, {SubOpts, SubID}, Subs), Err); + {error, Why} = Err1 -> + Code = subscribe_reason_code(Why), + subscribe(TopicFilters, USR, SubID, [Code|Codes], Subs, Err1) + end; +subscribe([], _USR, _SubID, Codes, Subs, Err) -> + Props = case Err of + ok -> #{}; + {error, Why} -> + #{reason_string => format_reason_string(Why)} + end, + {lists:reverse(Codes), Subs, Props}. + +-spec unsubscribe([binary()], jid:ljid(), subscriptions()) -> + {[reason_code()], subscriptions(), properties()}. +unsubscribe(TopicFilters, USR, Subs) -> + unsubscribe(TopicFilters, USR, [], Subs, ok). + +-spec unsubscribe([binary()], jid:ljid(), + [reason_code()], subscriptions(), + ok | {error, error_reason()}) -> + {[reason_code()], subscriptions(), properties()}. +unsubscribe([TopicFilter|TopicFilters], USR, Codes, Subs, Err) -> + case mod_mqtt:unsubscribe(USR, TopicFilter) of + ok -> + unsubscribe(TopicFilters, USR, [success|Codes], + maps:remove(TopicFilter, Subs), Err); + {error, notfound} -> + unsubscribe(TopicFilters, USR, + ['no-subscription-existed'|Codes], + maps:remove(TopicFilter, Subs), Err); + {error, Why} = Err1 -> + Code = unsubscribe_reason_code(Why), + unsubscribe(TopicFilters, USR, [Code|Codes], Subs, Err1) + end; +unsubscribe([], _USR, Codes, Subs, Err) -> + Props = case Err of + ok -> #{}; + {error, Why} -> + #{reason_string => format_reason_string(Why)} + end, + {lists:reverse(Codes), Subs, Props}. + +-spec select_retained(jid:ljid(), subscriptions(), subscriptions()) -> [{publish(), seconds()}]. +select_retained(USR, NewSubs, OldSubs) -> + lists:flatten( + maps:fold( + fun(_Filter, {#sub_opts{retain_handling = 2}, _SubID}, Acc) -> + Acc; + (Filter, {#sub_opts{retain_handling = 1, qos = QoS}, SubID}, Acc) -> + case maps:is_key(Filter, OldSubs) of + true -> Acc; + false -> [mod_mqtt:select_retained(USR, Filter, QoS, SubID)|Acc] + end; + (Filter, {#sub_opts{qos = QoS}, SubID}, Acc) -> + [mod_mqtt:select_retained(USR, Filter, QoS, SubID)|Acc] + end, [], NewSubs)). + +-spec send_retained(state(), [{publish(), seconds()}]) -> + {ok, state()} | {error, state(), error_reason()}. +send_retained(State, [{#publish{meta = Meta} = Pub, Expiry}|Pubs]) -> + I = next_id(State#state.id), + Meta1 = Meta#{expiry_time => Expiry}, + Pub1 = Pub#publish{id = I, retain = true, meta = Meta1}, + case send(State#state{id = I}, Pub1) of + {ok, State1} -> + send_retained(State1, Pubs); + Err -> + Err + end; +send_retained(State, []) -> + {ok, State}. + +-spec publish(state(), publish()) -> {ok, non_neg_integer()} | + {error, error_reason()}. +publish(State, #publish{topic = Topic, properties = Props} = Pkt) -> + MessageExpiry = maps:get(message_expiry_interval, Props, ?MAX_UINT32), + ExpiryTime = min(unix_time() + MessageExpiry, ?MAX_UINT32), + USR = jid:tolower(State#state.jid), + Props1 = maps:filter( + fun(payload_format_indicator, _) -> true; + (content_type, _) -> true; + (response_topic, _) -> true; + (correlation_data, _) -> true; + (user_property, _) -> true; + (_, _) -> false + end, Props), + Topic1 = case Topic of + <<>> -> + Alias = maps:get(topic_alias, Props), + maps:get(Alias, State#state.topic_aliases); + _ -> + Topic + end, + Pkt1 = Pkt#publish{topic = Topic1, properties = Props1}, + mod_mqtt:publish(USR, Pkt1, ExpiryTime). + +-spec store_topic_alias(state(), publish()) -> state(). +store_topic_alias(State, #publish{topic = <<_, _/binary>> = Topic, + properties = #{topic_alias := Alias}}) -> + Aliases = maps:put(Alias, Topic, State#state.topic_aliases), + State#state{topic_aliases = Aliases}; +store_topic_alias(State, _) -> + State. + +%%%=================================================================== +%%% Socket management +%%%=================================================================== +-spec send(state(), mqtt_packet()) -> {ok, state()} | + {error, state(), error_reason()}. +send(State, #publish{} = Pkt) -> + case is_expired(Pkt) of + {false, Pkt1} -> + case State#state.in_flight == undefined andalso + p1_queue:is_empty(State#state.queue) of + true -> + Dup = case Pkt1#publish.qos of + 0 -> undefined; + _ -> Pkt1 + end, + State1 = State#state{in_flight = Dup}, + {ok, do_send(State1, Pkt1)}; + false -> + ?DEBUG("Queueing packet:~n~ts~n** when state:~n~ts", + [pp(Pkt), pp(State)]), + try p1_queue:in(Pkt, State#state.queue) of + Q -> + State1 = State#state{queue = Q}, + {ok, State1} + catch error:full -> + Q = p1_queue:clear(State#state.queue), + State1 = State#state{queue = Q, session_expiry = 0}, + {error, State1, queue_full} + end + end; + true -> + {ok, State} + end; +send(State, Pkt) -> + {ok, do_send(State, Pkt)}. + +-spec resend(state()) -> {ok, state()} | {error, state(), error_reason()}. +resend(#state{in_flight = undefined} = State) -> + case p1_queue:out(State#state.queue) of + {{value, #publish{qos = QoS} = Pkt}, Q} -> + case is_expired(Pkt) of + true -> + resend(State#state{queue = Q}); + {false, Pkt1} when QoS > 0 -> + State1 = State#state{in_flight = Pkt1, queue = Q}, + {ok, do_send(State1, Pkt1)}; + {false, Pkt1} -> + State1 = do_send(State#state{queue = Q}, Pkt1), + resend(State1) + end; + {empty, _} -> + {ok, State} + end; +resend(#state{in_flight = Pkt} = State) -> + {ok, do_send(State, set_dup_flag(Pkt))}. + +-spec do_send(state(), mqtt_packet()) -> state(). +do_send(#state{socket = {SockMod, Sock} = Socket} = State, Pkt) -> + ?DEBUG("Send MQTT packet:~n~ts", [pp(Pkt)]), + Data = mqtt_codec:encode(State#state.version, Pkt), + Res = SockMod:send(Sock, Data), + check_sock_result(Socket, Res), + State; +do_send(State, _Pkt) -> + State. + +-spec activate(socket()) -> ok. +activate({SockMod, Sock} = Socket) -> + Res = case SockMod of + gen_tcp -> inet:setopts(Sock, [{active, once}]); + _ -> SockMod:setopts(Sock, [{active, once}]) + end, + check_sock_result(Socket, Res). + +-spec peername(state()) -> {ok, peername()} | {error, socket_error_reason()}. +peername(#state{socket = {SockMod, Sock}}) -> + case SockMod of + gen_tcp -> inet:peername(Sock); + _ -> SockMod:peername(Sock) + end. + +-spec disconnect(state(), error_reason()) -> state(). +disconnect(#state{socket = {SockMod, Sock}} = State, Err) -> + State1 = case Err of + {auth, Code} -> + do_send(State, #connack{code = Code}); + {codec, {Tag, _, _}} when Tag == unsupported_protocol_version; + Tag == unsupported_protocol_name -> + do_send(State#state{version = ?MQTT_VERSION_4}, + #connack{code = connack_reason_code(Err)}); + _ when State#state.version == undefined -> + State; + {Tag, _} when Tag == socket; Tag == tls -> + State; + {peer_disconnected, _, _} -> + State; + _ -> + Props = #{reason_string => format_reason_string(Err)}, + case State#state.jid of + undefined -> + Code = connack_reason_code(Err), + Pkt = #connack{code = Code, properties = Props}, + do_send(State, Pkt); + _ when State#state.version == ?MQTT_VERSION_5 -> + Code = disconnect_reason_code(Err), + Pkt = #disconnect{code = Code, properties = Props}, + do_send(State, Pkt); + _ -> + State + end + end, + SockMod:close(Sock), + State1#state{socket = undefined, + version = undefined, + codec = mqtt_codec:renew(State#state.codec)}; +disconnect(State, _) -> + State. + +-spec check_sock_result(socket(), ok | {error, inet:posix()}) -> ok. +check_sock_result(_, ok) -> + ok; +check_sock_result({_, Sock}, {error, Why}) -> + self() ! {tcp_closed, Sock}, + ?DEBUG("MQTT socket error: ~p", [format_inet_error(Why)]). + +-spec starttls(state()) -> {ok, socket()} | {error, error_reason()}. +starttls(#state{socket = {gen_tcp, Socket}, tls = true}) -> + case ejabberd_pkix:get_certfile() of + {ok, Cert} -> + CAFileOpt = + case ejabberd_option:c2s_cafile(ejabberd_config:get_myname()) of + undefined -> []; + CAFile -> [{cafile, CAFile}] + end, + case fast_tls:tcp_to_tls(Socket, [{certfile, Cert}] ++ CAFileOpt) of + {ok, TLSSock} -> + {ok, {fast_tls, TLSSock}}; + {error, Why} -> + {error, {tls, Why}} + end; + error -> + {error, {tls, no_certfile}} + end; +starttls(#state{socket = Socket}) -> + {ok, Socket}. + +-spec recv_data(socket(), binary()) -> {ok, binary()} | {error, error_reason()}. +recv_data({fast_tls, Sock}, Data) -> + case fast_tls:recv_data(Sock, Data) of + {ok, _} = OK -> OK; + {error, E} when is_atom(E) -> {error, {socket, E}}; + {error, E} when is_binary(E) -> {error, {tls, E}} + end; +recv_data(_, Data) -> + {ok, Data}. + +%%%=================================================================== +%%% Formatters +%%%=================================================================== +-spec pp(any()) -> iolist(). +pp(Term) -> + io_lib_pretty:print(Term, fun pp/2). + +-spec format_inet_error(socket_error_reason()) -> string(). +format_inet_error(closed) -> + "connection closed"; +format_inet_error(timeout) -> + format_inet_error(etimedout); +format_inet_error(Reason) -> + case inet:format_error(Reason) of + "unknown POSIX error" -> atom_to_list(Reason); + Txt -> Txt + end. + +-spec format_tls_error(atom() | binary()) -> string() | binary(). +format_tls_error(no_certfile) -> + "certificate not configured"; +format_tls_error(Reason) when is_atom(Reason) -> + format_inet_error(Reason); +format_tls_error(Reason) -> + Reason. + +-spec format_exit_reason(term()) -> string(). +format_exit_reason(noproc) -> + "process is dead"; +format_exit_reason(normal) -> + "process has exited"; +format_exit_reason(killed) -> + "process has been killed"; +format_exit_reason(timeout) -> + "remote call to process timed out"; +format_exit_reason(Why) -> + format("unexpected error: ~p", [Why]). + +%% Same as format_error/1, but hides sensitive data +%% and returns result as binary +-spec format_reason_string(error_reason()) -> binary(). +format_reason_string({resumed, _}) -> + <<"Resumed by another connection">>; +format_reason_string({replaced, _}) -> + <<"Replaced by another connection">>; +format_reason_string(Err) -> + list_to_binary(format_error(Err)). + +-spec format(io:format(), list()) -> string(). +format(Fmt, Args) -> + lists:flatten(io_lib:format(Fmt, Args)). + +-spec pp(atom(), non_neg_integer()) -> [atom()] | no. +pp(state, 17) -> record_info(fields, state); +pp(Rec, Size) -> mqtt_codec:pp(Rec, Size). + +-spec publish_reason_code(error_reason()) -> reason_code(). +publish_reason_code(publish_forbidden) -> 'topic-name-invalid'; +publish_reason_code(_) -> 'implementation-specific-error'. + +-spec subscribe_reason_code(qos() | error_reason()) -> reason_code(). +subscribe_reason_code(0) -> 'granted-qos-0'; +subscribe_reason_code(1) -> 'granted-qos-1'; +subscribe_reason_code(2) -> 'granted-qos-2'; +subscribe_reason_code(subscribe_forbidden) -> 'topic-filter-invalid'; +subscribe_reason_code(_) -> 'implementation-specific-error'. + +-spec unsubscribe_reason_code(error_reason()) -> reason_code(). +unsubscribe_reason_code(_) -> 'implementation-specific-error'. + +-spec disconnect_reason_code(error_reason()) -> reason_code(). +disconnect_reason_code({code, Code}) -> Code; +disconnect_reason_code({codec, Err}) -> mqtt_codec:error_reason_code(Err); +disconnect_reason_code({unexpected_packet, _}) -> 'protocol-error'; +disconnect_reason_code({replaced, _}) -> 'session-taken-over'; +disconnect_reason_code({resumed, _}) -> 'session-taken-over'; +disconnect_reason_code(internal_server_error) -> 'implementation-specific-error'; +disconnect_reason_code(db_failure) -> 'implementation-specific-error'; +disconnect_reason_code(idle_connection) -> 'keep-alive-timeout'; +disconnect_reason_code(queue_full) -> 'quota-exceeded'; +disconnect_reason_code(shutdown) -> 'server-shutting-down'; +disconnect_reason_code(subscribe_forbidden) -> 'topic-filter-invalid'; +disconnect_reason_code(publish_forbidden) -> 'topic-name-invalid'; +disconnect_reason_code(will_topic_forbidden) -> 'topic-name-invalid'; +disconnect_reason_code({payload_format_invalid, _}) -> 'payload-format-invalid'; +disconnect_reason_code(session_expiry_non_zero) -> 'protocol-error'; +disconnect_reason_code(unknown_topic_alias) -> 'protocol-error'; +disconnect_reason_code(_) -> 'unspecified-error'. + +-spec connack_reason_code(error_reason()) -> reason_code(). +connack_reason_code({Tag, Code}) when Tag == auth; Tag == code -> Code; +connack_reason_code({codec, Err}) -> mqtt_codec:error_reason_code(Err); +connack_reason_code({unexpected_packet, _}) -> 'protocol-error'; +connack_reason_code(internal_server_error) -> 'implementation-specific-error'; +connack_reason_code(db_failure) -> 'implementation-specific-error'; +connack_reason_code(idle_connection) -> 'keep-alive-timeout'; +connack_reason_code(queue_full) -> 'quota-exceeded'; +connack_reason_code(shutdown) -> 'server-shutting-down'; +connack_reason_code(will_topic_forbidden) -> 'topic-name-invalid'; +connack_reason_code({payload_format_invalid, _}) -> 'payload-format-invalid'; +connack_reason_code(session_expiry_non_zero) -> 'protocol-error'; +connack_reason_code(_) -> 'unspecified-error'. + +%%%=================================================================== +%%% Configuration processing +%%%=================================================================== +-spec queue_type(binary()) -> ram | file. +queue_type(Host) -> + mod_mqtt_opt:queue_type(Host). + +-spec queue_limit(binary()) -> non_neg_integer() | unlimited. +queue_limit(Host) -> + mod_mqtt_opt:max_queue(Host). + +-spec session_expiry(binary()) -> milli_seconds(). +session_expiry(Host) -> + mod_mqtt_opt:session_expiry(Host). + +-spec topic_alias_maximum(binary()) -> non_neg_integer(). +topic_alias_maximum(Host) -> + mod_mqtt_opt:max_topic_aliases(Host). + +%%%=================================================================== +%%% Timings +%%%=================================================================== +-spec current_time() -> milli_seconds(). +current_time() -> + erlang:monotonic_time(millisecond). + +-spec unix_time() -> seconds(). +unix_time() -> + erlang:system_time(second). + +-spec set_keep_alive(state(), seconds()) -> state(). +set_keep_alive(State, 0) -> + ?DEBUG("Disabling MQTT keep-alive", []), + State#state{timeout = infinity}; +set_keep_alive(State, Secs) -> + Secs1 = round(Secs * 1.5), + ?DEBUG("Setting MQTT keep-alive to ~B seconds", [Secs1]), + set_timeout(State, timer:seconds(Secs1)). + +-spec reset_keep_alive(state()) -> state(). +reset_keep_alive(#state{timeout = {MSecs, _}, jid = #jid{}} = State) -> + set_timeout(State, MSecs); +reset_keep_alive(State) -> + State. + +-spec set_timeout(state(), milli_seconds()) -> state(). +set_timeout(State, MSecs) -> + Time = current_time(), + State#state{timeout = {MSecs, Time}}. + +-spec is_expired(publish()) -> true | {false, publish()}. +is_expired(#publish{meta = Meta, properties = Props} = Pkt) -> + case maps:get(expiry_time, Meta, ?MAX_UINT32) of + ?MAX_UINT32 -> + {false, Pkt}; + ExpiryTime -> + Left = ExpiryTime - unix_time(), + if Left > 0 -> + Props1 = Props#{message_expiry_interval => Left}, + {false, Pkt#publish{properties = Props1}}; + true -> + ?DEBUG("Dropping expired packet:~n~ts", [pp(Pkt)]), + true + end + end. + +%%%=================================================================== +%%% Authentication +%%%=================================================================== +-spec parse_credentials(connect()) -> {ok, jid:jid()} | {error, reason_code()}. +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 + error -> jid:make(str:sha(ClientID), Host); + J -> J + end, + parse_credentials(JID, ClientID); +parse_credentials(#connect{username = User} = Pkt) -> + try jid:decode(User) of + #jid{luser = <<>>} -> + case jid:make(User, ejabberd_config:get_myname()) of + error -> + {error, 'bad-user-name-or-password'}; + JID -> + parse_credentials(JID, Pkt#connect.client_id) + end; + JID -> + parse_credentials(JID, Pkt#connect.client_id) + catch _:{bad_jid, _} -> + {error, 'bad-user-name-or-password'} + end. + +-spec parse_credentials(jid:jid(), binary()) -> {ok, jid:jid()} | {error, reason_code()}. +parse_credentials(JID, ClientID) -> + case gen_mod:is_loaded(JID#jid.lserver, mod_mqtt) of + false -> + {error, 'server-unavailable'}; + true -> + case jid:replace_resource(JID, ClientID) of + error -> + {error, 'client-identifier-not-valid'}; + JID1 -> + {ok, JID1} + end + end. + +-spec authenticate(connect(), peername(), state()) -> {ok, jid:jid()} | {error, reason_code()}. +authenticate(Pkt, IP, State) -> + case authenticate(Pkt, State) of + {ok, JID, AuthModule} -> + ?INFO_MSG("Accepted MQTT authentication for ~ts by ~s backend from ~s", + [jid:encode(JID), + ejabberd_auth:backend_type(AuthModule), + ejabberd_config:may_hide_data(misc:ip_to_list(IP))]), + {ok, JID}; + {error, _} = Err -> + Err + end. + +-spec authenticate(connect(), state()) -> {ok, jid:jid(), module()} | {error, reason_code()}. +authenticate(#connect{password = Pass, properties = Props} = Pkt, State) -> + case parse_credentials(Pkt) of + {ok, #jid{luser = LUser, lserver = LServer} = JID} -> + case maps:find(authentication_method, Props) of + {ok, <<"X-OAUTH2">>} -> + Token = maps:get(authentication_data, Props, <<>>), + case ejabberd_oauth:check_token( + LUser, LServer, [<<"sasl_auth">>], Token) of + true -> {ok, JID, ejabberd_oauth}; + _ -> {error, 'not-authorized'} + end; + {ok, _} -> + {error, 'bad-authentication-method'}; + error -> + case Pass of + <<>> -> + case tls_auth(JID, State) of + true -> + {ok, JID, pkix}; + false -> + {error, 'not-authorized'}; + no_cert -> + case ejabberd_auth:check_password_with_authmodule( + LUser, <<>>, LServer, Pass) of + {true, AuthModule} -> {ok, JID, AuthModule}; + false -> {error, 'not-authorized'} + end + end; + _ -> + case ejabberd_auth:check_password_with_authmodule( + LUser, <<>>, LServer, Pass) of + {true, AuthModule} -> {ok, JID, AuthModule}; + false -> {error, 'not-authorized'} + end + end + end; + {error, _} = Err -> + Err + end. + +-spec tls_auth(jid:jid(), state()) -> boolean() | no_cert. +tls_auth(_JID, #state{tls_verify = false}) -> + no_cert; +tls_auth(JID, State) -> + case State#state.socket of + {fast_tls, Sock} -> + case fast_tls:get_peer_certificate(Sock, otp) of + {ok, Cert} -> + case fast_tls:get_verify_result(Sock) of + 0 -> + case get_cert_jid(Cert) of + {ok, JID2} -> + jid:remove_resource(jid:tolower(JID)) == + jid:remove_resource(jid:tolower(JID2)); + error -> + false + end; + VerifyRes -> + Reason = fast_tls:get_cert_verify_string(VerifyRes, Cert), + ?WARNING_MSG("TLS verify failed: ~s", [Reason]), + false + end; + error -> + no_cert + end; + _ -> + no_cert + end. + +get_cert_jid(Cert) -> + case Cert#'OTPCertificate'.tbsCertificate#'OTPTBSCertificate'.subject of + {rdnSequence, Attrs1} -> + Attrs = lists:flatten(Attrs1), + case lists:keyfind(?'id-at-commonName', + #'AttributeTypeAndValue'.type, Attrs) of + #'AttributeTypeAndValue'{value = {utf8String, Val}} -> + try jid:decode(Val) of + #jid{luser = <<>>} -> + case jid:make(Val, ejabberd_config:get_myname()) of + error -> + error; + JID -> + {ok, JID} + end; + JID -> + {ok, JID} + catch _:{bad_jid, _} -> + error + end; + _ -> + error + end; + _ -> + error + end. + +%%%=================================================================== +%%% Validators +%%%=================================================================== +-spec validate_will(connect(), jid:jid()) -> ok | {error, error_reason()}. +validate_will(#connect{will = undefined}, _) -> + ok; +validate_will(#connect{will = #publish{topic = Topic, payload = Payload}, + will_properties = Props}, JID) -> + case mod_mqtt:check_publish_access(Topic, jid:tolower(JID)) of + deny -> {error, will_topic_forbidden}; + allow -> validate_payload(Props, Payload, will) + end. + +-spec validate_publish(publish(), state()) -> ok | {error, error_reason()}. +validate_publish(#publish{topic = Topic, payload = Payload, + properties = Props}, State) -> + case validate_topic(Topic, Props, State) of + ok -> validate_payload(Props, Payload, publish); + Err -> Err + end. + +-spec validate_subscribe(subscribe()) -> ok | {error, error_reason()}. +validate_subscribe(#subscribe{filters = Filters}) -> + case lists:any( + fun({<<"$share/", _/binary>>, _}) -> true; + (_) -> false + end, Filters) of + true -> + {error, {code, 'shared-subscriptions-not-supported'}}; + false -> + ok + end. + +-spec validate_topic(binary(), properties(), state()) -> ok | {error, error_reason()}. +validate_topic(<<>>, Props, State) -> + case maps:get(topic_alias, Props, 0) of + 0 -> + {error, {code, 'topic-alias-invalid'}}; + Alias -> + case maps:is_key(Alias, State#state.topic_aliases) of + true -> ok; + false -> {error, unknown_topic_alias} + end + end; +validate_topic(_, #{topic_alias := Alias}, State) -> + JID = State#state.jid, + Max = topic_alias_maximum(JID#jid.lserver), + if Alias > Max -> + {error, {code, 'topic-alias-invalid'}}; + true -> + ok + end; +validate_topic(_, _, _) -> + ok. + +-spec validate_payload(properties(), binary(), will | publish) -> ok | {error, error_reason()}. +validate_payload(#{payload_format_indicator := utf8}, Payload, Type) -> + try mqtt_codec:utf8(Payload) of + _ -> ok + catch _:_ -> + {error, {payload_format_invalid, Type}} + end; +validate_payload(_, _, _) -> + ok. + +%%%=================================================================== +%%% Misc +%%%=================================================================== +-spec resubscribe(jid:ljid(), subscriptions()) -> ok | {error, error_reason()}. +resubscribe(USR, Subs) -> + case maps:fold( + fun(TopicFilter, {SubOpts, ID}, ok) -> + mod_mqtt:subscribe(USR, TopicFilter, SubOpts, ID); + (_, _, {error, _} = Err) -> + Err + end, ok, Subs) of + ok -> + ok; + {error, _} = Err1 -> + unsubscribe(maps:keys(Subs), USR, #{}), + Err1 + end. + +-spec publish_will(state()) -> state(). +publish_will(#state{will = #publish{} = Will, + jid = #jid{} = JID} = State) -> + case publish(State, Will) of + {ok, _} -> + ?DEBUG("Will of ~ts has been published to ~ts", + [jid:encode(JID), Will#publish.topic]); + {error, Why} -> + ?WARNING_MSG("Failed to publish will of ~ts to ~ts: ~ts", + [jid:encode(JID), Will#publish.topic, + format_error(Why)]) + end, + State#state{will = undefined}; +publish_will(State) -> + State. + +-spec next_id(non_neg_integer()) -> pos_integer(). +next_id(ID) -> + (ID rem 65535) + 1. + +-spec set_dup_flag(mqtt_packet()) -> mqtt_packet(). +set_dup_flag(#publish{qos = QoS} = Pkt) when QoS>0 -> + Pkt#publish{dup = true}; +set_dup_flag(Pkt) -> + Pkt. + +-spec get_publish_code_props({ok, non_neg_integer()} | + {error, error_reason()}) -> {reason_code(), properties()}. +get_publish_code_props({ok, 0}) -> + {'no-matching-subscribers', #{}}; +get_publish_code_props({ok, _}) -> + {success, #{}}; +get_publish_code_props({error, Err}) -> + Code = publish_reason_code(Err), + Reason = format_reason_string(Err), + {Code, #{reason_string => Reason}}. + +-spec err_args(undefined | jid:jid(), peername(), error_reason()) -> iolist(). +err_args(undefined, IP, Reason) -> + [ejabberd_config:may_hide_data(misc:ip_to_list(IP)), + format_error(Reason)]; +err_args(JID, IP, Reason) -> + [jid:encode(JID), + ejabberd_config:may_hide_data(misc:ip_to_list(IP)), + format_error(Reason)]. + +-spec log_disconnection(state(), error_reason()) -> ok. +log_disconnection(#state{jid = JID, peername = IP}, Reason) -> + Msg = case JID of + undefined -> "Rejected MQTT connection from ~ts: ~ts"; + _ -> "Closing MQTT connection for ~ts from ~ts: ~ts" + end, + case Reason of + {Tag, _} when Tag == replaced; Tag == resumed; Tag == socket -> + ?DEBUG(Msg, err_args(JID, IP, Reason)); + idle_connection -> + ?DEBUG(Msg, err_args(JID, IP, Reason)); + Tag when Tag == session_expired; Tag == shutdown -> + ?INFO_MSG(Msg, err_args(JID, IP, Reason)); + {peer_disconnected, Code, _} -> + case mqtt_codec:is_error_code(Code) of + true -> ?WARNING_MSG(Msg, err_args(JID, IP, Reason)); + false -> ?DEBUG(Msg, err_args(JID, IP, Reason)) + end; + _ -> + ?WARNING_MSG(Msg, err_args(JID, IP, Reason)) + end. diff --git a/src/mod_mqtt_sql.erl b/src/mod_mqtt_sql.erl new file mode 100644 index 000000000..0f2b05b35 --- /dev/null +++ b/src/mod_mqtt_sql.erl @@ -0,0 +1,178 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov +%%% @copyright (C) 2002-2025 ProcessOne, SARL. All Rights Reserved. +%%% +%%% Licensed under the Apache License, Version 2.0 (the "License"); +%%% you may not use this file except in compliance with the License. +%%% You may obtain a copy of the License at +%%% +%%% http://www.apache.org/licenses/LICENSE-2.0 +%%% +%%% Unless required by applicable law or agreed to in writing, software +%%% distributed under the License is distributed on an "AS IS" BASIS, +%%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%%% See the License for the specific language governing permissions and +%%% limitations under the License. +%%% +%%%------------------------------------------------------------------- +-module(mod_mqtt_sql). +-behaviour(mod_mqtt). + +%% API +-export([init/2, publish/6, delete_published/2, lookup_published/2]). +-export([list_topics/1]). +%% Unsupported backend API +-export([init/0]). +-export([subscribe/4, unsubscribe/2, find_subscriber/2]). +-export([open_session/1, close_session/1, lookup_session/1, get_sessions/2]). +-export([sql_schemas/0]). + +-include("logger.hrl"). +-include("ejabberd_sql_pt.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init() -> + ?ERROR_MSG("Backend 'sql' is only supported for db_type", []), + {error, db_failure}. + +init(Host, _Opts) -> + ejabberd_sql_schema:update_schema(Host, ?MODULE, sql_schemas()), + ok. + +sql_schemas() -> + [#sql_schema{ + version = 1, + tables = + [#sql_table{ + name = <<"mqtt_pub">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"resource">>, type = text}, + #sql_column{name = <<"topic">>, type = text}, + #sql_column{name = <<"qos">>, type = smallint}, + #sql_column{name = <<"payload">>, type = blob}, + #sql_column{name = <<"payload_format">>, type = smallint}, + #sql_column{name = <<"content_type">>, type = text}, + #sql_column{name = <<"response_topic">>, type = text}, + #sql_column{name = <<"correlation_data">>, type = blob}, + #sql_column{name = <<"user_properties">>, type = blob}, + #sql_column{name = <<"expiry">>, type = bigint}], + indices = [#sql_index{ + columns = [<<"topic">>, <<"server_host">>], + unique = true}]}]}]. + +publish({U, LServer, R}, Topic, Payload, QoS, Props, ExpiryTime) -> + PayloadFormat = encode_pfi(maps:get(payload_format_indicator, Props, binary)), + ResponseTopic = maps:get(response_topic, Props, <<"">>), + CorrelationData = maps:get(correlation_data, Props, <<"">>), + ContentType = maps:get(content_type, Props, <<"">>), + UserProps = encode_props(maps:get(user_property, Props, [])), + case ?SQL_UPSERT(LServer, "mqtt_pub", + ["!topic=%(Topic)s", + "!server_host=%(LServer)s", + "username=%(U)s", + "resource=%(R)s", + "payload=%(Payload)s", + "qos=%(QoS)d", + "payload_format=%(PayloadFormat)d", + "response_topic=%(ResponseTopic)s", + "correlation_data=%(CorrelationData)s", + "content_type=%(ContentType)s", + "user_properties=%(UserProps)s", + "expiry=%(ExpiryTime)d"]) of + ok -> ok; + _Err -> {error, db_failure} + end. + +delete_published({_, LServer, _}, Topic) -> + case ejabberd_sql:sql_query( + LServer, + ?SQL("delete from mqtt_pub where " + "topic=%(Topic)s and %(LServer)H")) of + {updated, _} -> ok; + _Err -> {error, db_failure} + end. + +lookup_published({_, LServer, _}, Topic) -> + case ejabberd_sql:sql_query( + LServer, + ?SQL("select @(payload)s, @(qos)d, @(payload_format)d, " + "@(content_type)s, @(response_topic)s, " + "@(correlation_data)s, @(user_properties)s, @(expiry)d " + "from mqtt_pub where topic=%(Topic)s and %(LServer)H")) of + {selected, [{Payload, QoS, PayloadFormat, ContentType, + ResponseTopic, CorrelationData, EncProps, Expiry}]} -> + try decode_props(EncProps) of + UserProps -> + try decode_pfi(PayloadFormat) of + PFI -> + Props = #{payload_format_indicator => PFI, + content_type => ContentType, + response_topic => ResponseTopic, + correlation_data => CorrelationData, + user_property => UserProps}, + {ok, {Payload, QoS, Props, Expiry}} + catch _:badarg -> + ?ERROR_MSG("Malformed value of 'payload_format' column " + "for topic '~ts'", [Topic]), + {error, db_failure} + end + catch _:badarg -> + ?ERROR_MSG("Malformed value of 'user_properties' column " + "for topic '~ts'", [Topic]), + {error, db_failure} + end; + {selected, []} -> + {error, notfound}; + _ -> + {error, db_failure} + end. + +list_topics(LServer) -> + case ejabberd_sql:sql_query( + LServer, + ?SQL("select @(topic)s from mqtt_pub where %(LServer)H")) of + {selected, Res} -> + {ok, [Topic || {Topic} <- Res]}; + _ -> + {error, db_failure} + end. + +open_session(_) -> + erlang:nif_error(unsupported_db). + +close_session(_) -> + erlang:nif_error(unsupported_db). + +lookup_session(_) -> + erlang:nif_error(unsupported_db). + +get_sessions(_, _) -> + erlang:nif_error(unsupported_db). + +subscribe(_, _, _, _) -> + erlang:nif_error(unsupported_db). + +unsubscribe(_, _) -> + erlang:nif_error(unsupported_db). + +find_subscriber(_, _) -> + erlang:nif_error(unsupported_db). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +encode_pfi(binary) -> 0; +encode_pfi(utf8) -> 1. + +decode_pfi(0) -> binary; +decode_pfi(1) -> utf8. + +encode_props([]) -> <<"">>; +encode_props(L) -> term_to_binary(L). + +decode_props(<<"">>) -> []; +decode_props(Bin) -> binary_to_term(Bin). diff --git a/src/mod_mqtt_ws.erl b/src/mod_mqtt_ws.erl new file mode 100644 index 000000000..fd1e7d871 --- /dev/null +++ b/src/mod_mqtt_ws.erl @@ -0,0 +1,168 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov +%%% @copyright (C) 2002-2025 ProcessOne, SARL. All Rights Reserved. +%%% +%%% Licensed under the Apache License, Version 2.0 (the "License"); +%%% you may not use this file except in compliance with the License. +%%% You may obtain a copy of the License at +%%% +%%% http://www.apache.org/licenses/LICENSE-2.0 +%%% +%%% Unless required by applicable law or agreed to in writing, software +%%% distributed under the License is distributed on an "AS IS" BASIS, +%%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%%% See the License for the specific language governing permissions and +%%% limitations under the License. +%%% +%%%------------------------------------------------------------------- +-module(mod_mqtt_ws). +-ifndef(GEN_SERVER). +-define(GEN_SERVER, gen_server). +-endif. +-behaviour(?GEN_SERVER). + +%% API +-export([socket_handoff/3]). +-export([start/1, start_link/1]). +-export([peername/1, setopts/2, send/2, close/1]). +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-include_lib("xmpp/include/xmpp.hrl"). +-include("ejabberd_http.hrl"). +-include("logger.hrl"). + +-define(SEND_TIMEOUT, timer:seconds(15)). + +-record(state, {socket :: socket(), + ws_pid :: pid(), + mqtt_session :: undefined | pid()}). + +-type peername() :: {inet:ip_address(), inet:port_number()}. +-type socket() :: {http_ws, pid(), peername()}. +-export_type([socket/0]). + +%%%=================================================================== +%%% API +%%%=================================================================== +socket_handoff(LocalPath, Request, Opts) -> + ejabberd_websocket:socket_handoff( + LocalPath, Request, Opts, ?MODULE, fun get_human_html_xmlel/0). + +start({#ws{http_opts = Opts}, _} = WS) -> + ?GEN_SERVER:start(?MODULE, [WS], ejabberd_config:fsm_limit_opts(Opts)). + +start_link({#ws{http_opts = Opts}, _} = WS) -> + ?GEN_SERVER:start_link(?MODULE, [WS], ejabberd_config:fsm_limit_opts(Opts)). + +-spec peername(socket()) -> {ok, peername()}. +peername({http_ws, _, IP}) -> + {ok, IP}. + +-spec setopts(socket(), list()) -> ok. +setopts(_WSock, _Opts) -> + ok. + +-spec send(socket(), iodata()) -> ok | {error, timeout | einval}. +send({http_ws, Pid, _}, Data) -> + try ?GEN_SERVER:call(Pid, {send, Data}, ?SEND_TIMEOUT) + catch exit:{timeout, {?GEN_SERVER, _, _}} -> + {error, timeout}; + exit:{_, {?GEN_SERVER, _, _}} -> + {error, einval} + end. + +-spec close(socket()) -> ok. +close({http_ws, Pid, _}) -> + ?GEN_SERVER:cast(Pid, close). + +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== +init([{#ws{ip = IP, http_opts = ListenOpts}, WsPid}]) -> + Socket = {http_ws, self(), IP}, + case mod_mqtt_session:start(?MODULE, Socket, ListenOpts) of + {ok, Pid} -> + erlang:monitor(process, Pid), + erlang:monitor(process, WsPid), + mod_mqtt_session:accept(Pid), + State = #state{socket = Socket, + ws_pid = WsPid, + mqtt_session = Pid}, + {ok, State}; + {error, Reason} -> + {stop, Reason}; + ignore -> + ignore + end. + +handle_call({send, Data}, _From, #state{ws_pid = WsPid} = State) -> + WsPid ! {data, Data}, + {reply, ok, State}; +handle_call(Request, From, State) -> + ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), + {noreply, State}. + +handle_cast(close, State) -> + {stop, normal, State#state{mqtt_session = undefined}}; +handle_cast(Request, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Request]), + {noreply, State}. + +handle_info(closed, State) -> + {stop, normal, State}; +handle_info({received, Data}, State) -> + State#state.mqtt_session ! {tcp, State#state.socket, Data}, + {noreply, State}; +handle_info({'DOWN', _, process, Pid, _}, State) + when Pid == State#state.mqtt_session orelse Pid == State#state.ws_pid -> + {stop, normal, State}; +handle_info(Info, State) -> + ?WARNING_MSG("Unexpected info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, State) -> + if State#state.mqtt_session /= undefined -> + State#state.mqtt_session ! {tcp_closed, State#state.socket}; + true -> + ok + end. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +-spec get_human_html_xmlel() -> xmlel(). +get_human_html_xmlel() -> + Heading = <<"ejabberd mod_mqtt">>, + #xmlel{name = <<"html">>, + attrs = + [{<<"xmlns">>, <<"http://www.w3.org/1999/xhtml">>}], + children = + [#xmlel{name = <<"head">>, attrs = [], + children = + [#xmlel{name = <<"title">>, attrs = [], + children = [{xmlcdata, Heading}]}]}, + #xmlel{name = <<"body">>, attrs = [], + children = + [#xmlel{name = <<"h1">>, attrs = [], + children = [{xmlcdata, Heading}]}, + #xmlel{name = <<"p">>, attrs = [], + children = + [{xmlcdata, <<"An implementation of ">>}, + #xmlel{name = <<"a">>, + attrs = + [{<<"href">>, + <<"http://tools.ietf.org/html/rfc6455">>}], + children = + [{xmlcdata, + <<"WebSocket protocol">>}]}]}, + #xmlel{name = <<"p">>, attrs = [], + children = + [{xmlcdata, + <<"This web page is only informative. To " + "use WebSocket connection you need an MQTT " + "client that supports it.">>}]}]}]}. diff --git a/src/mod_muc.erl b/src/mod_muc.erl index a0c6c34e6..bdb96be3e 100644 --- a/src/mod_muc.erl +++ b/src/mod_muc.erl @@ -5,7 +5,7 @@ %%% Created : 19 Mar 2003 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -22,1136 +22,1216 @@ %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- - -module(mod_muc). - -author('alexey@process-one.net'). - --behaviour(gen_server). - +-protocol({xep, 45, '1.25', '0.5.0', "complete", ""}). +-protocol({xep, 249, '1.2', '0.5.0', "complete", ""}). +-protocol({xep, 486, '0.1.0', '24.07', "complete", ""}). +-ifndef(GEN_SERVER). +-define(GEN_SERVER, gen_server). +-endif. +-behaviour(?GEN_SERVER). -behaviour(gen_mod). %% API --export([start_link/2, - start/2, +-export([start/2, stop/1, + start_link/2, + reload/3, + mod_doc/0, room_destroyed/4, store_room/4, + store_room/5, + store_changes/4, restore_room/3, forget_room/3, + create_room/3, create_room/5, - shutdown_rooms/1, - process_iq_disco_items/4, - broadcast_service_message/2, - export/1, - import/1, - import/3, - can_use_nick/4]). + shutdown_rooms/1, + process_disco_info/1, + process_disco_items/1, + process_vcard/1, + process_register/1, + process_iq_register/1, + process_muc_unique/1, + process_mucsub/1, + broadcast_service_message/3, + export/1, + import_info/0, + import/5, + import_start/2, + opts_to_binary/1, + find_online_room/2, + register_online_room/3, + get_online_rooms/1, + count_online_rooms/1, + register_online_user/4, + unregister_online_user/4, + iq_set_register_info/5, + count_online_rooms_by_user/3, + get_online_rooms_by_user/3, + can_use_nick/4, + get_subscribed_rooms/2, + remove_user/2, + procname/2, + route/1, unhibernate_room/3]). -%% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, - handle_info/2, terminate/2, code_change/3]). + handle_info/2, terminate/2, code_change/3, + mod_opt_type/1, mod_options/1, depends/2]). --include("ejabberd.hrl"). -include("logger.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). +-include("mod_muc.hrl"). +-include("mod_muc_room.hrl"). +-include("translate.hrl"). --include("jlib.hrl"). --record(muc_room, {name_host = {<<"">>, <<"">>} :: {binary(), binary()} | - {'_', binary()}, - opts = [] :: list() | '_'}). +-type state() :: #{hosts := [binary()], + server_host := binary(), + worker := pos_integer()}. +-type access() :: {acl:acl(), acl:acl(), acl:acl(), acl:acl(), acl:acl()}. +-type muc_room_opts() :: [{atom(), any()}]. +-export_type([access/0]). +-callback init(binary(), gen_mod:opts()) -> any(). +-callback import(binary(), binary(), [binary()]) -> ok. +-callback store_room(binary(), binary(), binary(), list(), list()|undefined) -> {atomic, any()}. +-callback store_changes(binary(), binary(), binary(), list()) -> {atomic, any()}. +-callback restore_room(binary(), binary(), binary()) -> muc_room_opts() | error | {error, atom()}. +-callback forget_room(binary(), binary(), binary()) -> {atomic, any()}. +-callback can_use_nick(binary(), binary(), jid(), binary()) -> boolean(). +-callback get_rooms(binary(), binary()) -> [#muc_room{}]. +-callback get_nick(binary(), binary(), jid()) -> binary() | error. +-callback set_nick(binary(), binary(), jid(), binary()) -> {atomic, ok | false}. +-callback register_online_room(binary(), binary(), binary(), pid()) -> any(). +-callback unregister_online_room(binary(), binary(), binary(), pid()) -> any(). +-callback find_online_room(binary(), binary(), binary()) -> {ok, pid()} | error. +-callback find_online_room_by_pid(binary(), pid()) -> {ok, binary(), binary()} | error. +-callback get_online_rooms(binary(), binary(), undefined | rsm_set()) -> [{binary(), binary(), pid()}]. +-callback count_online_rooms(binary(), binary()) -> non_neg_integer(). +-callback rsm_supported() -> boolean(). +-callback register_online_user(binary(), ljid(), binary(), binary()) -> any(). +-callback unregister_online_user(binary(), ljid(), binary(), binary()) -> any(). +-callback count_online_rooms_by_user(binary(), binary(), binary()) -> non_neg_integer(). +-callback get_online_rooms_by_user(binary(), binary(), binary()) -> [{binary(), binary()}]. +-callback get_subscribed_rooms(binary(), binary(), jid()) -> + {ok, [{jid(), binary(), [binary()]}]} | {error, db_failure}. --record(muc_online_room, - {name_host = {<<"">>, <<"">>} :: {binary(), binary()} | '$1' | - {'_', binary()} | '_', - pid = self() :: pid() | '$2' | '_' | '$1'}). - --record(muc_registered, - {us_host = {{<<"">>, <<"">>}, <<"">>} :: {{binary(), binary()}, binary()} | '$1', - nick = <<"">> :: binary()}). - --record(state, - {host = <<"">> :: binary(), - server_host = <<"">> :: binary(), - access = {none, none, none, none} :: {atom(), atom(), atom(), atom()}, - history_size = 20 :: non_neg_integer(), - default_room_opts = [] :: list(), - room_shaper = none :: shaper:shaper()}). - --define(PROCNAME, ejabberd_mod_muc). +-optional_callbacks([get_subscribed_rooms/3, + store_changes/4]). %%==================================================================== %% API %%==================================================================== -%%-------------------------------------------------------------------- -%% Function: start_link() -> {ok,Pid} | ignore | {error,Error} -%% Description: Starts the server -%%-------------------------------------------------------------------- -start_link(Host, Opts) -> - Proc = gen_mod:get_module_proc(Host, ?PROCNAME), - gen_server:start_link({local, Proc}, ?MODULE, - [Host, Opts], []). - start(Host, Opts) -> - start_supervisor(Host), - Proc = gen_mod:get_module_proc(Host, ?PROCNAME), - ChildSpec = {Proc, {?MODULE, start_link, [Host, Opts]}, - temporary, 1000, worker, [?MODULE]}, - supervisor:start_child(ejabberd_sup, ChildSpec). + 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), + Mod:init(Host, gen_mod:set_opt(hosts, MyHosts, Opts)), + RMod:init(Host, gen_mod:set_opt(hosts, MyHosts, Opts)), + load_permanent_rooms(MyHosts, Host, Opts); + Err -> + Err + end. stop(Host) -> - Rooms = shutdown_rooms(Host), - stop_supervisor(Host), - Proc = gen_mod:get_module_proc(Host, ?PROCNAME), - gen_server:call(Proc, stop), - supervisor:delete_child(ejabberd_sup, Proc), - {wait, Rooms}. + 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). -shutdown_rooms(Host) -> - MyHost = gen_mod:get_module_opt_host(Host, mod_muc, - <<"conference.@HOST@">>), - Rooms = mnesia:dirty_select(muc_online_room, - [{#muc_online_room{name_host = '$1', - pid = '$2'}, - [{'==', {element, 2, '$1'}, MyHost}], - ['$2']}]), - [Pid ! shutdown || Pid <- Rooms], - Rooms. +-spec reload(binary(), gen_mod:opts(), gen_mod:opts()) -> ok. +reload(ServerHost, NewOpts, OldOpts) -> + NewMod = gen_mod:db_mod(NewOpts, ?MODULE), + NewRMod = gen_mod:ram_db_mod(NewOpts, ?MODULE), + OldMod = gen_mod:db_mod(OldOpts, ?MODULE), + OldRMod = gen_mod:ram_db_mod(OldOpts, ?MODULE), + NewHosts = gen_mod:get_opt_hosts(NewOpts), + OldHosts = gen_mod:get_opt_hosts(OldOpts), + AddHosts = NewHosts -- OldHosts, + DelHosts = OldHosts -- NewHosts, + if NewMod /= OldMod -> + NewMod:init(ServerHost, gen_mod:set_opt(hosts, NewHosts, NewOpts)); + true -> + ok + end, + if NewRMod /= OldRMod -> + NewRMod:init(ServerHost, gen_mod:set_opt(hosts, NewHosts, NewOpts)); + true -> + ok + end, + lists:foreach( + fun(I) -> + ?GEN_SERVER:cast(procname(ServerHost, I), + {reload, AddHosts, DelHosts, NewHosts}) + end, lists:seq(1, misc:logical_processors())), + load_permanent_rooms(AddHosts, ServerHost, NewOpts), + shutdown_rooms(ServerHost, DelHosts, OldRMod), + lists:foreach( + fun(Host) -> + lists:foreach( + fun({_, _, Pid}) when node(Pid) == node() -> + mod_muc_room:config_reloaded(Pid); + (_) -> + ok + end, get_online_rooms(ServerHost, Host)) + end, misc:intersection(NewHosts, OldHosts)). + +depends(_Host, _Opts) -> + [{mod_mam, soft}]. + +start_link(Host, I) -> + Proc = procname(Host, I), + ?GEN_SERVER:start_link({local, Proc}, ?MODULE, [Host, I], + ejabberd_config:fsm_limit_opts([])). + +-spec procname(binary(), pos_integer() | {binary(), binary()}) -> atom(). +procname(Host, I) when is_integer(I) -> + binary_to_atom( + <<(atom_to_binary(?MODULE, latin1))/binary, "_", Host/binary, + "_", (integer_to_binary(I))/binary>>, utf8); +procname(Host, RoomHost) -> + Cores = misc:logical_processors(), + I = erlang:phash2(RoomHost, Cores) + 1, + procname(Host, I). + +-spec route(stanza()) -> ok. +route(Pkt) -> + To = xmpp:get_to(Pkt), + ServerHost = ejabberd_router:host_of_route(To#jid.lserver), + route(Pkt, ServerHost). + +-spec route(stanza(), binary()) -> ok. +route(Pkt, ServerHost) -> + From = xmpp:get_from(Pkt), + To = xmpp:get_to(Pkt), + Host = To#jid.lserver, + Access = mod_muc_opt:access(ServerHost), + case acl:match_rule(ServerHost, Access, From) of + allow -> + route(Pkt, Host, ServerHost); + deny -> + Lang = xmpp:get_lang(Pkt), + ErrText = ?T("Access denied by service policy"), + Err = xmpp:err_forbidden(ErrText, Lang), + ejabberd_router:route_error(Pkt, Err) + end. + +-spec route(stanza(), binary(), binary()) -> ok. +route(#iq{to = #jid{luser = <<"">>, lresource = <<"">>}} = IQ, _, _) -> + ejabberd_router:process_iq(IQ); +route(#message{lang = Lang, body = Body, type = Type, from = From, + to = #jid{luser = <<"">>, lresource = <<"">>}} = Pkt, + Host, ServerHost) -> + if Type == error -> + ok; + true -> + AccessAdmin = mod_muc_opt:access_admin(ServerHost), + case acl:match_rule(ServerHost, AccessAdmin, From) of + allow -> + Msg = xmpp:get_text(Body), + broadcast_service_message(ServerHost, Host, Msg); + deny -> + ErrText = ?T("Only service administrators are allowed " + "to send service messages"), + Err = xmpp:err_forbidden(ErrText, Lang), + ejabberd_router:route_error(Pkt, Err) + end + end; +route(Pkt, Host, ServerHost) -> + {Room, _, _} = jid:tolower(xmpp:get_to(Pkt)), + case Room of + <<"">> -> + Txt = ?T("No module is handling this query"), + Err = xmpp:err_service_unavailable(Txt, xmpp:get_lang(Pkt)), + ejabberd_router:route_error(Pkt, Err); + _ -> + RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), + case RMod:find_online_room(ServerHost, Room, Host) of + error -> + Proc = procname(ServerHost, {Room, Host}), + case whereis(Proc) of + Pid when Pid == self() -> + route_to_room(Pkt, ServerHost); + Pid when is_pid(Pid) -> + ?DEBUG("Routing to MUC worker ~p:~n~ts", [Proc, xmpp:pp(Pkt)]), + ?GEN_SERVER:cast(Pid, {route_to_room, Pkt}); + undefined -> + ?DEBUG("MUC worker ~p is dead", [Proc]), + Err = xmpp:err_internal_server_error(), + ejabberd_router:route_error(Pkt, Err) + end; + {ok, Pid} -> + mod_muc_room:route(Pid, Pkt) + end + end. + +-spec shutdown_rooms(binary()) -> [pid()]. +shutdown_rooms(ServerHost) -> + RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), + Hosts = gen_mod:get_module_opt_hosts(ServerHost, mod_muc), + shutdown_rooms(ServerHost, Hosts, RMod). + +-spec shutdown_rooms(binary(), [binary()], module()) -> [pid()]. +shutdown_rooms(ServerHost, Hosts, RMod) -> + Rooms = [RMod:get_online_rooms(ServerHost, Host, undefined) + || Host <- Hosts], + lists:flatmap( + fun({_, _, Pid}) when node(Pid) == node() -> + mod_muc_room:shutdown(Pid), + [Pid]; + (_) -> + [] + end, lists:flatten(Rooms)). %% This function is called by a room in three situations: %% A) The owner of the room destroyed it %% B) The only participant of a temporary room leaves it %% C) mod_muc:stop was called, and each room is being terminated %% In this case, the mod_muc process died before the room processes -%% So the message sending must be catched +%% So the message sending must be caught +-spec room_destroyed(binary(), binary(), pid(), binary()) -> ok. room_destroyed(Host, Room, Pid, ServerHost) -> - catch gen_mod:get_module_proc(ServerHost, ?PROCNAME) ! - {room_destroyed, {Room, Host}, Pid}, - ok. + Proc = procname(ServerHost, {Room, Host}), + ?GEN_SERVER:cast(Proc, {room_destroyed, {Room, Host}, Pid}). %% @doc Create a room. %% If Opts = default, the default room options are used. %% Else use the passed options as defined in mod_muc_room. create_room(Host, Name, From, Nick, Opts) -> - Proc = gen_mod:get_module_proc(Host, ?PROCNAME), - gen_server:call(Proc, {create, Name, From, Nick, Opts}). + ServerHost = ejabberd_router:host_of_route(Host), + Proc = procname(ServerHost, {Name, Host}), + ?GEN_SERVER:call(Proc, {create, Name, Host, From, Nick, Opts}). + +%% @doc Create a room. +%% If Opts = default, the default room options are used. +%% Else use the passed options as defined in mod_muc_room. +create_room(Host, Name, Opts) -> + ServerHost = ejabberd_router:host_of_route(Host), + Proc = procname(ServerHost, {Name, Host}), + ?GEN_SERVER:call(Proc, {create, Name, Host, Opts}). store_room(ServerHost, Host, Name, Opts) -> - LServer = jlib:nameprep(ServerHost), - store_room(LServer, Host, Name, Opts, - gen_mod:db_type(LServer, ?MODULE)). + store_room(ServerHost, Host, Name, Opts, undefined). -store_room(_LServer, Host, Name, Opts, mnesia) -> - F = fun () -> - mnesia:write(#muc_room{name_host = {Name, Host}, - opts = Opts}) - end, - mnesia:transaction(F); -store_room(_LServer, Host, Name, Opts, riak) -> - {atomic, ejabberd_riak:put(#muc_room{name_host = {Name, Host}, - opts = Opts}, - muc_room_schema())}; -store_room(LServer, Host, Name, Opts, odbc) -> - SName = ejabberd_odbc:escape(Name), - SHost = ejabberd_odbc:escape(Host), - SOpts = ejabberd_odbc:encode_term(Opts), - F = fun () -> - odbc_queries:update_t(<<"muc_room">>, - [<<"name">>, <<"host">>, <<"opts">>], - [SName, SHost, SOpts], - [<<"name='">>, SName, <<"' and host='">>, - SHost, <<"'">>]) - end, - ejabberd_odbc:sql_transaction(LServer, F). - -restore_room(ServerHost, Host, Name) -> - LServer = jlib:nameprep(ServerHost), - restore_room(LServer, Host, Name, - gen_mod:db_type(LServer, ?MODULE)). - -restore_room(_LServer, Host, Name, mnesia) -> - case catch mnesia:dirty_read(muc_room, {Name, Host}) of - [#muc_room{opts = Opts}] -> Opts; - _ -> error - end; -restore_room(_LServer, Host, Name, riak) -> - case ejabberd_riak:get(muc_room, muc_room_schema(), {Name, Host}) of - {ok, #muc_room{opts = Opts}} -> Opts; - _ -> error - end; -restore_room(LServer, Host, Name, odbc) -> - SName = ejabberd_odbc:escape(Name), - SHost = ejabberd_odbc:escape(Host), - case catch ejabberd_odbc:sql_query(LServer, - [<<"select opts from muc_room where name='">>, - SName, <<"' and host='">>, SHost, - <<"';">>]) - of - {selected, [<<"opts">>], [[Opts]]} -> - opts_to_binary(ejabberd_odbc:decode_term(Opts)); - _ -> error +maybe_store_new_room(ServerHost, Host, Name, Opts) -> + case {proplists:get_bool(persistent, Opts), proplists:get_value(subscribers, Opts, [])} of + {false, []} -> + {atomic, ok}; + {_, Subs} -> + Changes = [{add_subscription, JID, Nick, Nodes} || {JID, Nick, Nodes} <- Subs], + store_room(ServerHost, Host, Name, Opts, Changes) end. +store_room(ServerHost, Host, Name, Opts, ChangesHints) -> + LServer = jid:nameprep(ServerHost), + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:store_room(LServer, Host, Name, Opts, ChangesHints). + +store_changes(ServerHost, Host, Name, ChangesHints) -> + LServer = jid:nameprep(ServerHost), + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:store_changes(LServer, Host, Name, ChangesHints). + +restore_room(ServerHost, Host, Name) -> + LServer = jid:nameprep(ServerHost), + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:restore_room(LServer, Host, Name). + forget_room(ServerHost, Host, Name) -> - LServer = jlib:nameprep(ServerHost), - forget_room(LServer, Host, Name, - gen_mod:db_type(LServer, ?MODULE)). - -forget_room(_LServer, Host, Name, mnesia) -> - F = fun () -> mnesia:delete({muc_room, {Name, Host}}) - end, - mnesia:transaction(F); -forget_room(_LServer, Host, Name, riak) -> - {atomic, ejabberd_riak:delete(muc_room, {Name, Host})}; -forget_room(LServer, Host, Name, odbc) -> - SName = ejabberd_odbc:escape(Name), - SHost = ejabberd_odbc:escape(Host), - F = fun () -> - ejabberd_odbc:sql_query_t([<<"delete from muc_room where name='">>, - SName, <<"' and host='">>, SHost, - <<"';">>]) - end, - ejabberd_odbc:sql_transaction(LServer, F). - -process_iq_disco_items(Host, From, To, - #iq{lang = Lang} = IQ) -> - Rsm = jlib:rsm_decode(IQ), - Res = IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"query">>, - attrs = [{<<"xmlns">>, ?NS_DISCO_ITEMS}], - children = iq_disco_items(Host, From, Lang, Rsm)}]}, - ejabberd_router:route(To, From, jlib:iq_to_xml(Res)). + LServer = jid:nameprep(ServerHost), + ejabberd_hooks:run(remove_room, LServer, [LServer, Name, Host]), + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:forget_room(LServer, Host, Name). can_use_nick(_ServerHost, _Host, _JID, <<"">>) -> false; can_use_nick(ServerHost, Host, JID, Nick) -> - LServer = jlib:nameprep(ServerHost), - can_use_nick(LServer, Host, JID, Nick, - gen_mod:db_type(LServer, ?MODULE)). + LServer = jid:nameprep(ServerHost), + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:can_use_nick(LServer, Host, JID, Nick). -can_use_nick(_LServer, Host, JID, Nick, mnesia) -> - {LUser, LServer, _} = jlib:jid_tolower(JID), - LUS = {LUser, LServer}, - case catch mnesia:dirty_select(muc_registered, - [{#muc_registered{us_host = '$1', - nick = Nick, _ = '_'}, - [{'==', {element, 2, '$1'}, Host}], - ['$_']}]) - of - {'EXIT', _Reason} -> true; - [] -> true; - [#muc_registered{us_host = {U, _Host}}] -> U == LUS - end; -can_use_nick(LServer, Host, JID, Nick, riak) -> - {LUser, LServer, _} = jlib:jid_tolower(JID), - LUS = {LUser, LServer}, - case ejabberd_riak:get_by_index(muc_registered, - muc_registered_schema(), - <<"nick_host">>, {Nick, Host}) of - {ok, []} -> - true; - {ok, [#muc_registered{us_host = {U, _Host}}]} -> - U == LUS; - {error, _} -> - true - end; -can_use_nick(LServer, Host, JID, Nick, odbc) -> - SJID = - jlib:jid_to_string(jlib:jid_tolower(jlib:jid_remove_resource(JID))), - SNick = ejabberd_odbc:escape(Nick), - SHost = ejabberd_odbc:escape(Host), - case catch ejabberd_odbc:sql_query(LServer, - [<<"select jid from muc_registered ">>, - <<"where nick='">>, SNick, - <<"' and host='">>, SHost, <<"';">>]) - of - {selected, [<<"jid">>], [[SJID1]]} -> SJID == SJID1; - _ -> true - end. +-spec find_online_room(binary(), binary()) -> {ok, pid()} | error. +find_online_room(Room, Host) -> + ServerHost = ejabberd_router:host_of_route(Host), + RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), + RMod:find_online_room(ServerHost, Room, Host). + +-spec register_online_room(binary(), binary(), pid()) -> any(). +register_online_room(Room, Host, Pid) -> + ServerHost = ejabberd_router:host_of_route(Host), + RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), + RMod:register_online_room(ServerHost, Room, Host, Pid). + +-spec get_online_rooms(binary()) -> [{binary(), binary(), pid()}]. +get_online_rooms(Host) -> + ServerHost = ejabberd_router:host_of_route(Host), + get_online_rooms(ServerHost, Host). + +-spec count_online_rooms(binary()) -> non_neg_integer(). +count_online_rooms(Host) -> + ServerHost = ejabberd_router:host_of_route(Host), + count_online_rooms(ServerHost, Host). + +-spec register_online_user(binary(), ljid(), binary(), binary()) -> any(). +register_online_user(ServerHost, LJID, Name, Host) -> + RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), + RMod:register_online_user(ServerHost, LJID, Name, Host). + +-spec unregister_online_user(binary(), ljid(), binary(), binary()) -> any(). +unregister_online_user(ServerHost, LJID, Name, Host) -> + RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), + RMod:unregister_online_user(ServerHost, LJID, Name, Host). + +-spec count_online_rooms_by_user(binary(), binary(), binary()) -> non_neg_integer(). +count_online_rooms_by_user(ServerHost, LUser, LServer) -> + RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), + RMod:count_online_rooms_by_user(ServerHost, LUser, LServer). + +-spec get_online_rooms_by_user(binary(), binary(), binary()) -> [{binary(), binary()}]. +get_online_rooms_by_user(ServerHost, LUser, LServer) -> + RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), + RMod:get_online_rooms_by_user(ServerHost, LUser, LServer). %%==================================================================== %% gen_server callbacks %%==================================================================== +-spec init(list()) -> {ok, state()}. +init([Host, Worker]) -> + process_flag(trap_exit, true), + Opts = gen_mod:get_module_opts(Host, ?MODULE), + MyHosts = gen_mod:get_opt_hosts(Opts), + register_routes(Host, MyHosts, Worker), + register_iq_handlers(MyHosts, Worker), + {ok, #{server_host => Host, hosts => MyHosts, worker => Worker}}. -%%-------------------------------------------------------------------- -%% Function: init(Args) -> {ok, State} | -%% {ok, State, Timeout} | -%% ignore | -%% {stop, Reason} -%% Description: Initiates the server -%%-------------------------------------------------------------------- -init([Host, Opts]) -> - MyHost = gen_mod:get_opt_host(Host, Opts, - <<"conference.@HOST@">>), - case gen_mod:db_type(Opts) of - mnesia -> - mnesia:create_table(muc_room, - [{disc_copies, [node()]}, - {attributes, - record_info(fields, muc_room)}]), - mnesia:create_table(muc_registered, - [{disc_copies, [node()]}, - {attributes, - record_info(fields, muc_registered)}]), - update_tables(MyHost), - mnesia:add_table_index(muc_registered, nick); - _ -> - ok - end, - mnesia:create_table(muc_online_room, - [{ram_copies, [node()]}, - {attributes, record_info(fields, muc_online_room)}]), - mnesia:add_table_copy(muc_online_room, node(), ram_copies), - catch ets:new(muc_online_users, [bag, named_table, public, {keypos, 2}]), - clean_table_from_bad_node(node(), MyHost), - mnesia:subscribe(system), - Access = gen_mod:get_opt(access, Opts, fun(A) -> A end, all), - AccessCreate = gen_mod:get_opt(access_create, Opts, fun(A) -> A end, all), - AccessAdmin = gen_mod:get_opt(access_admin, Opts, fun(A) -> A end, none), - AccessPersistent = gen_mod:get_opt(access_persistent, Opts, fun(A) -> A end, all), - HistorySize = gen_mod:get_opt(history_size, Opts, fun(A) -> A end, 20), - DefRoomOpts = gen_mod:get_opt(default_room_options, Opts, fun(A) -> A end, []), - RoomShaper = gen_mod:get_opt(room_shaper, Opts, fun(A) -> A end, none), - ejabberd_router:register_route(MyHost), - load_permanent_rooms(MyHost, Host, - {Access, AccessCreate, AccessAdmin, AccessPersistent}, - HistorySize, - RoomShaper), - {ok, #state{host = MyHost, - server_host = Host, - access = {Access, AccessCreate, AccessAdmin, AccessPersistent}, - default_room_opts = DefRoomOpts, - history_size = HistorySize, - room_shaper = RoomShaper}}. - -%%-------------------------------------------------------------------- -%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | -%% {reply, Reply, State, Timeout} | -%% {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, Reply, State} | -%% {stop, Reason, State} -%% Description: Handling call messages -%%-------------------------------------------------------------------- +-spec handle_call(term(), {pid(), term()}, state()) -> + {reply, ok | {ok, pid()} | {error, any()}, state()} | + {stop, normal, ok, state()}. handle_call(stop, _From, State) -> {stop, normal, ok, State}; -handle_call({create, Room, From, Nick, Opts}, _From, - #state{host = Host, server_host = ServerHost, - access = Access, default_room_opts = DefOpts, - history_size = HistorySize, - room_shaper = RoomShaper} = State) -> - ?DEBUG("MUC: create new room '~s'~n", [Room]), +handle_call({unhibernate, Room, Host, ResetHibernationTime}, _From, + #{server_host := ServerHost} = State) -> + RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), + {reply, load_room(RMod, Host, ServerHost, Room, ResetHibernationTime), State}; +handle_call({create, Room, Host, Opts}, _From, + #{server_host := ServerHost} = State) -> + ?DEBUG("MUC: create new room '~ts'~n", [Room]), NewOpts = case Opts of - default -> DefOpts; - _ -> Opts + default -> mod_muc_opt:default_room_options(ServerHost); + _ -> Opts end, - {ok, Pid} = mod_muc_room:start( - Host, ServerHost, Access, - Room, HistorySize, - RoomShaper, From, - Nick, NewOpts), - register_room(Host, Room, Pid), - {reply, ok, State}. + RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), + case start_room(RMod, Host, ServerHost, Room, NewOpts) of + {ok, _} -> + maybe_store_new_room(ServerHost, Host, Room, NewOpts), + ejabberd_hooks:run(create_room, ServerHost, [ServerHost, Room, Host]), + {reply, ok, State}; + Err -> + {reply, Err, State} + end; +handle_call({create, Room, Host, From, Nick, Opts}, _From, + #{server_host := ServerHost} = State) -> + ?DEBUG("MUC: create new room '~ts'~n", [Room]), + NewOpts = case Opts of + default -> mod_muc_opt:default_room_options(ServerHost); + _ -> Opts + end, + RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), + case start_room(RMod, Host, ServerHost, Room, NewOpts, From, Nick) of + {ok, _} -> + maybe_store_new_room(ServerHost, Host, Room, NewOpts), + ejabberd_hooks:run(create_room, ServerHost, [ServerHost, Room, Host]), + {reply, ok, State}; + Err -> + {reply, Err, State} + end. -%%-------------------------------------------------------------------- -%% Function: handle_cast(Msg, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} -%% Description: Handling cast messages -%%-------------------------------------------------------------------- -handle_cast(_Msg, State) -> {noreply, State}. - -%%-------------------------------------------------------------------- -%% Function: handle_info(Info, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} -%% Description: Handling all non call/cast messages -%%-------------------------------------------------------------------- -handle_info({route, From, To, Packet}, - #state{host = Host, server_host = ServerHost, - access = Access, default_room_opts = DefRoomOpts, - history_size = HistorySize, - room_shaper = RoomShaper} = State) -> - case catch do_route(Host, ServerHost, Access, HistorySize, RoomShaper, - From, To, Packet, DefRoomOpts) of - {'EXIT', Reason} -> - ?ERROR_MSG("~p", [Reason]); - _ -> - ok +-spec handle_cast(term(), state()) -> {noreply, state()}. +handle_cast({route_to_room, Packet}, #{server_host := ServerHost} = State) -> + try route_to_room(Packet, ServerHost) + catch + Class:Reason:StackTrace -> + ?ERROR_MSG("Failed to route packet:~n~ts~n** ~ts", + [xmpp:pp(Packet), + misc:format_exception(2, Class, Reason, StackTrace)]) end, {noreply, State}; -handle_info({room_destroyed, RoomHost, Pid}, State) -> - F = fun () -> - mnesia:delete_object(#muc_online_room{name_host = - RoomHost, - pid = Pid}) - end, - mnesia:transaction(F), +handle_cast({room_destroyed, {Room, Host}, Pid}, + #{server_host := ServerHost} = State) -> + RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), + RMod:unregister_online_room(ServerHost, Room, Host, Pid), {noreply, State}; -handle_info({mnesia_system_event, {mnesia_down, Node}}, State) -> - clean_table_from_bad_node(Node), +handle_cast({reload, AddHosts, DelHosts, NewHosts}, + #{server_host := ServerHost, worker := Worker} = State) -> + register_routes(ServerHost, AddHosts, Worker), + register_iq_handlers(AddHosts, Worker), + unregister_routes(DelHosts, Worker), + unregister_iq_handlers(DelHosts, Worker), + {noreply, State#{hosts => NewHosts}}; +handle_cast(Msg, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Msg]), + {noreply, State}. + +-spec handle_info(term(), state()) -> {noreply, state()}. +handle_info({route, Packet}, #{server_host := ServerHost} = State) -> + %% We can only receive the packet here from other nodes + %% where mod_muc is not loaded. Such configuration + %% is *highly* discouraged + try route(Packet, ServerHost) + catch + Class:Reason:StackTrace -> + ?ERROR_MSG("Failed to route packet:~n~ts~n** ~ts", + [xmpp:pp(Packet), + misc:format_exception(2, Class, Reason, StackTrace)]) + end, {noreply, State}; -handle_info(_Info, State) -> {noreply, State}. +handle_info({room_destroyed, {Room, Host}, Pid}, State) -> + %% For backward compat + handle_cast({room_destroyed, {Room, Host}, Pid}, State); +handle_info({'DOWN', _Ref, process, Pid, _Reason}, + #{server_host := ServerHost} = State) -> + RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), + case RMod:find_online_room_by_pid(ServerHost, Pid) of + {ok, Room, Host} -> + handle_cast({room_destroyed, {Room, Host}, Pid}, State); + _ -> + {noreply, State} + end; +handle_info(Info, State) -> + ?ERROR_MSG("Unexpected info: ~p", [Info]), + {noreply, State}. -%%-------------------------------------------------------------------- -%% Function: terminate(Reason, State) -> void() -%% Description: This function is called by a gen_server when it is about to -%% terminate. It should be the opposite of Module:init/1 and do any necessary -%% cleaning up. When it returns, the gen_server terminates with Reason. -%% The return value is ignored. -%%-------------------------------------------------------------------- -terminate(_Reason, State) -> - ejabberd_router:unregister_route(State#state.host), - ok. +-spec terminate(term(), state()) -> any(). +terminate(_Reason, #{hosts := Hosts, worker := Worker}) -> + unregister_routes(Hosts, Worker), + unregister_iq_handlers(Hosts, Worker). -%%-------------------------------------------------------------------- -%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} -%% Description: Convert process state when code is changed -%%-------------------------------------------------------------------- +-spec code_change(term(), state(), term()) -> {ok, state()}. code_change(_OldVsn, State, _Extra) -> {ok, State}. %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- -start_supervisor(Host) -> - Proc = gen_mod:get_module_proc(Host, - ejabberd_mod_muc_sup), - ChildSpec = {Proc, - {ejabberd_tmp_sup, start_link, [Proc, mod_muc_room]}, - permanent, infinity, supervisor, [ejabberd_tmp_sup]}, - supervisor:start_child(ejabberd_sup, ChildSpec). +-spec register_iq_handlers([binary()], pos_integer()) -> ok. +register_iq_handlers(Hosts, 1) -> + %% Only register handlers on first worker + lists:foreach( + fun(Host) -> + gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_REGISTER, + ?MODULE, process_register), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_VCARD, + ?MODULE, process_vcard), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_MUCSUB, + ?MODULE, process_mucsub), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_MUC_UNIQUE, + ?MODULE, process_muc_unique), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO, + ?MODULE, process_disco_info), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_DISCO_ITEMS, + ?MODULE, process_disco_items) + end, Hosts); +register_iq_handlers(_, _) -> + ok. -stop_supervisor(Host) -> - Proc = gen_mod:get_module_proc(Host, - ejabberd_mod_muc_sup), - supervisor:terminate_child(ejabberd_sup, Proc), - supervisor:delete_child(ejabberd_sup, Proc). +-spec unregister_iq_handlers([binary()], pos_integer()) -> ok. +unregister_iq_handlers(Hosts, 1) -> + %% Only unregister handlers on first worker + lists:foreach( + fun(Host) -> + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_REGISTER), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_VCARD), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_MUCSUB), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_MUC_UNIQUE), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_ITEMS) + end, Hosts); +unregister_iq_handlers(_, _) -> + ok. -do_route(Host, ServerHost, Access, HistorySize, RoomShaper, - From, To, Packet, DefRoomOpts) -> - {AccessRoute, _AccessCreate, _AccessAdmin, _AccessPersistent} = Access, - case acl:match_rule(ServerHost, AccessRoute, From) of - allow -> - do_route1(Host, ServerHost, Access, HistorySize, RoomShaper, - From, To, Packet, DefRoomOpts); - _ -> - #xmlel{attrs = Attrs} = Packet, - Lang = xml:get_attr_s(<<"xml:lang">>, Attrs), - ErrText = <<"Access denied by service policy">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_FORBIDDEN(Lang, ErrText)), - ejabberd_router:route_error(To, From, Err, Packet) +-spec register_routes(binary(), [binary()], pos_integer()) -> ok. +register_routes(ServerHost, Hosts, 1) -> + %% Only register routes on first worker + lists:foreach( + fun(Host) -> + ejabberd_router:register_route( + Host, ServerHost, {apply, ?MODULE, route}) + end, Hosts); +register_routes(_, _, _) -> + ok. + +-spec unregister_routes([binary()], pos_integer()) -> ok. +unregister_routes(Hosts, 1) -> + %% Only unregister routes on first worker + lists:foreach( + fun(Host) -> + ejabberd_router:unregister_route(Host) + end, Hosts); +unregister_routes(_, _) -> + ok. + +%% Function copied from mod_muc_room.erl +-spec extract_password(presence() | iq()) -> binary() | false. +extract_password(#presence{} = Pres) -> + case xmpp:get_subtag(Pres, #muc{}) of + #muc{password = Password} when is_binary(Password) -> + Password; + _ -> + false + end; +extract_password(#iq{} = IQ) -> + case xmpp:get_subtag(IQ, #muc_subscribe{}) of + #muc_subscribe{password = Password} when Password /= <<"">> -> + Password; + _ -> + false end. +-spec unhibernate_room(binary(), binary(), binary()) -> {ok, pid()} | {error, notfound | db_failure | term()}. +unhibernate_room(ServerHost, Host, Room) -> + unhibernate_room(ServerHost, Host, Room, true). -do_route1(Host, ServerHost, Access, HistorySize, RoomShaper, - From, To, Packet, DefRoomOpts) -> - {_AccessRoute, AccessCreate, AccessAdmin, _AccessPersistent} = Access, - {Room, _, Nick} = jlib:jid_tolower(To), - #xmlel{name = Name, attrs = Attrs} = Packet, - case Room of - <<"">> -> - case Nick of - <<"">> -> - case Name of - <<"iq">> -> - case jlib:iq_query_info(Packet) of - #iq{type = get, xmlns = (?NS_DISCO_INFO) = XMLNS, - sub_el = _SubEl, lang = Lang} = - IQ -> - Info = ejabberd_hooks:run_fold(disco_info, - ServerHost, [], - [ServerHost, ?MODULE, - <<"">>, <<"">>]), - Res = IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"query">>, - attrs = - [{<<"xmlns">>, XMLNS}], - children = - iq_disco_info(Lang) ++ - Info}]}, - ejabberd_router:route(To, From, - jlib:iq_to_xml(Res)); - #iq{type = get, xmlns = ?NS_DISCO_ITEMS} = IQ -> - spawn(?MODULE, process_iq_disco_items, - [Host, From, To, IQ]); - #iq{type = get, xmlns = (?NS_REGISTER) = XMLNS, - lang = Lang, sub_el = _SubEl} = - IQ -> - Res = IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"query">>, - attrs = - [{<<"xmlns">>, XMLNS}], - children = - iq_get_register_info(ServerHost, - Host, - From, - Lang)}]}, - ejabberd_router:route(To, From, - jlib:iq_to_xml(Res)); - #iq{type = set, xmlns = (?NS_REGISTER) = XMLNS, - lang = Lang, sub_el = SubEl} = - IQ -> - case process_iq_register_set(ServerHost, Host, From, - SubEl, Lang) - of - {result, IQRes} -> - Res = IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"query">>, - attrs = - [{<<"xmlns">>, - XMLNS}], - children = IQRes}]}, - ejabberd_router:route(To, From, - jlib:iq_to_xml(Res)); - {error, Error} -> - Err = jlib:make_error_reply(Packet, Error), - ejabberd_router:route(To, From, Err) +-spec unhibernate_room(binary(), binary(), binary(), boolean()) -> {ok, pid()} | {error, notfound | db_failure | term()}. +unhibernate_room(ServerHost, Host, Room, ResetHibernationTime) -> + RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), + case RMod:find_online_room(ServerHost, Room, Host) of + error -> + Proc = procname(ServerHost, {Room, Host}), + ?GEN_SERVER:call(Proc, {unhibernate, Room, Host, ResetHibernationTime}, 20000); + {ok, _} = R2 -> R2 + end. + +-spec route_to_room(stanza(), binary()) -> ok. +route_to_room(Packet, ServerHost) -> + From = xmpp:get_from(Packet), + To = xmpp:get_to(Packet), + {Room, Host, Nick} = jid:tolower(To), + RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), + case RMod:find_online_room(ServerHost, Room, Host) of + error -> + case should_start_room(Packet) of + false -> + Lang = xmpp:get_lang(Packet), + ErrText = ?T("Conference room does not exist"), + Err = xmpp:err_item_not_found(ErrText, Lang), + ejabberd_router:route_error(Packet, Err); + StartType -> + case load_room(RMod, Host, ServerHost, Room, true) of + {error, notfound} when StartType == start -> + case check_create_room(ServerHost, Host, Room, From) of + true -> + Pass = extract_password(Packet), + case start_new_room(RMod, Host, ServerHost, Room, Pass, From, Nick) of + {ok, Pid} -> + mod_muc_room:route(Pid, Packet); + _Err -> + Err = xmpp:err_internal_server_error(), + ejabberd_router:route_error(Packet, Err) + end; + false -> + Lang = xmpp:get_lang(Packet), + ErrText = ?T("Room creation is denied by service policy"), + Err = xmpp:err_forbidden(ErrText, Lang), + ejabberd_router:route_error(Packet, Err) end; - #iq{type = get, xmlns = (?NS_VCARD) = XMLNS, - lang = Lang, sub_el = _SubEl} = - IQ -> - Res = IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"vCard">>, - attrs = - [{<<"xmlns">>, XMLNS}], - children = - iq_get_vcard(Lang)}]}, - ejabberd_router:route(To, From, - jlib:iq_to_xml(Res)); - #iq{type = get, xmlns = ?NS_MUC_UNIQUE} = IQ -> - Res = IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"unique">>, - attrs = - [{<<"xmlns">>, - ?NS_MUC_UNIQUE}], - children = - [iq_get_unique(From)]}]}, - ejabberd_router:route(To, From, - jlib:iq_to_xml(Res)); - #iq{} -> - Err = jlib:make_error_reply(Packet, - ?ERR_FEATURE_NOT_IMPLEMENTED), - ejabberd_router:route(To, From, Err); - _ -> ok - end; - <<"message">> -> - case xml:get_attr_s(<<"type">>, Attrs) of - <<"error">> -> ok; - _ -> - case acl:match_rule(ServerHost, AccessAdmin, From) - of - allow -> - Msg = xml:get_path_s(Packet, - [{elem, <<"body">>}, - cdata]), - broadcast_service_message(Host, Msg); - _ -> - Lang = xml:get_attr_s(<<"xml:lang">>, Attrs), - ErrText = - <<"Only service administrators are allowed " - "to send service messages">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_FORBIDDEN(Lang, - ErrText)), - ejabberd_router:route(To, From, Err) - end - end; - <<"presence">> -> ok - end; - _ -> - case xml:get_attr_s(<<"type">>, Attrs) of - <<"error">> -> ok; - <<"result">> -> ok; - _ -> - Err = jlib:make_error_reply( - Packet, ?ERR_ITEM_NOT_FOUND), - ejabberd_router:route(To, From, Err) + {error, notfound} -> + Lang = xmpp:get_lang(Packet), + ErrText = ?T("Conference room does not exist"), + Err = xmpp:err_item_not_found(ErrText, Lang), + ejabberd_router:route_error(Packet, Err); + {error, _} -> + Err = xmpp:err_internal_server_error(), + ejabberd_router:route_error(Packet, Err); + {ok, Pid2} -> + mod_muc_room:route(Pid2, Packet) end end; + {ok, Pid} -> + mod_muc_room:route(Pid, Packet) + end. + +-spec process_vcard(iq()) -> iq(). +process_vcard(#iq{type = get, to = To, lang = Lang, sub_els = [#vcard_temp{}]} = IQ) -> + ServerHost = ejabberd_router:host_of_route(To#jid.lserver), + VCard = case mod_muc_opt:vcard(ServerHost) of + undefined -> + #vcard_temp{fn = <<"ejabberd/mod_muc">>, + url = ejabberd_config:get_uri(), + desc = misc:get_descr(Lang, ?T("ejabberd MUC module"))}; + V -> + V + end, + xmpp:make_iq_result(IQ, VCard); +process_vcard(#iq{type = set, lang = Lang} = IQ) -> + Txt = ?T("Value 'set' of 'type' attribute is not allowed"), + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); +process_vcard(#iq{lang = Lang} = IQ) -> + Txt = ?T("No module is handling this query"), + xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)). + +-spec process_register(iq()) -> iq(). +process_register(IQ) -> + case process_iq_register(IQ) of + {result, Result} -> + xmpp:make_iq_result(IQ, Result); + {error, Err} -> + xmpp:make_error(IQ, Err) + end. + +-spec process_iq_register(iq()) -> {result, register()} | {error, stanza_error()}. +process_iq_register(#iq{type = Type, from = From, to = To, lang = Lang, + sub_els = [El = #register{}]}) -> + Host = To#jid.lserver, + RegisterDestination = jid:encode(To), + ServerHost = ejabberd_router:host_of_route(Host), + AccessRegister = mod_muc_opt:access_register(ServerHost), + case acl:match_rule(ServerHost, AccessRegister, From) of + allow -> + case Type of + get -> + {result, iq_get_register_info(ServerHost, RegisterDestination, From, Lang)}; + set -> + process_iq_register_set(ServerHost, RegisterDestination, From, El, Lang) + end; + deny -> + ErrText = ?T("Access denied by service policy"), + Err = xmpp:err_forbidden(ErrText, Lang), + {error, Err} + end. + +-spec process_disco_info(iq()) -> iq(). +process_disco_info(#iq{type = set, lang = Lang} = IQ) -> + Txt = ?T("Value 'set' of 'type' attribute is not allowed"), + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); +process_disco_info(#iq{type = get, from = From, to = To, lang = Lang, + sub_els = [#disco_info{node = <<"">>}]} = IQ) -> + ServerHost = ejabberd_router:host_of_route(To#jid.lserver), + RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), + AccessRegister = mod_muc_opt:access_register(ServerHost), + X = ejabberd_hooks:run_fold(disco_info, ServerHost, [], + [ServerHost, ?MODULE, <<"">>, Lang]), + MAMFeatures = case gen_mod:is_loaded(ServerHost, mod_mam) of + true -> [?NS_MAM_TMP, ?NS_MAM_0, ?NS_MAM_1, ?NS_MAM_2]; + false -> [] + end, + OccupantIdFeatures = case gen_mod:is_loaded(ServerHost, mod_muc_occupantid) of + true -> [?NS_OCCUPANT_ID]; + false -> [] + end, + RSMFeatures = case RMod:rsm_supported() of + true -> [?NS_RSM]; + false -> [] + end, + RegisterFeatures = case acl:match_rule(ServerHost, AccessRegister, From) of + allow -> [?NS_REGISTER]; + deny -> [] + end, + Features = [?NS_DISCO_INFO, ?NS_DISCO_ITEMS, + ?NS_MUC, ?NS_VCARD, ?NS_MUCSUB, ?NS_MUC_UNIQUE + | RegisterFeatures ++ RSMFeatures ++ MAMFeatures ++ OccupantIdFeatures], + Name = mod_muc_opt:name(ServerHost), + Identity = #identity{category = <<"conference">>, + type = <<"text">>, + name = translate:translate(Lang, Name)}, + xmpp:make_iq_result( + IQ, #disco_info{features = Features, + identities = [Identity], + xdata = X}); +process_disco_info(#iq{type = get, lang = Lang, + sub_els = [#disco_info{}]} = IQ) -> + xmpp:make_error(IQ, xmpp:err_item_not_found(?T("Node not found"), Lang)); +process_disco_info(#iq{lang = Lang} = IQ) -> + Txt = ?T("No module is handling this query"), + xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)). + +-spec process_disco_items(iq()) -> iq(). +process_disco_items(#iq{type = set, lang = Lang} = IQ) -> + Txt = ?T("Value 'set' of 'type' attribute is not allowed"), + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); +process_disco_items(#iq{type = get, from = From, to = To, lang = Lang, + sub_els = [#disco_items{node = Node, rsm = RSM}]} = IQ) -> + Host = To#jid.lserver, + ServerHost = ejabberd_router:host_of_route(Host), + MaxRoomsDiscoItems = mod_muc_opt:max_rooms_discoitems(ServerHost), + case iq_disco_items(ServerHost, Host, From, Lang, + MaxRoomsDiscoItems, Node, RSM) of + {error, Err} -> + xmpp:make_error(IQ, Err); + {result, Result} -> + xmpp:make_iq_result(IQ, Result) + end; +process_disco_items(#iq{lang = Lang} = IQ) -> + Txt = ?T("No module is handling this query"), + xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)). + +-spec process_muc_unique(iq()) -> iq(). +process_muc_unique(#iq{type = set, lang = Lang} = IQ) -> + Txt = ?T("Value 'set' of 'type' attribute is not allowed"), + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); +process_muc_unique(#iq{from = From, type = get, + sub_els = [#muc_unique{}]} = IQ) -> + Name = str:sha(term_to_binary([From, erlang:timestamp(), + p1_rand:get_string()])), + xmpp:make_iq_result(IQ, #muc_unique{name = Name}). + +-spec process_mucsub(iq()) -> iq(). +process_mucsub(#iq{type = set, lang = Lang} = IQ) -> + Txt = ?T("Value 'set' of 'type' attribute is not allowed"), + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); +process_mucsub(#iq{type = get, from = From, to = To, lang = Lang, + sub_els = [#muc_subscriptions{}]} = IQ) -> + Host = To#jid.lserver, + ServerHost = ejabberd_router:host_of_route(Host), + case get_subscribed_rooms(ServerHost, Host, From) of + {ok, Subs} -> + List = [#muc_subscription{jid = JID, nick = Nick, events = Nodes} + || {JID, Nick, Nodes} <- Subs], + xmpp:make_iq_result(IQ, #muc_subscriptions{list = List}); + {error, _} -> + Txt = ?T("Database failure"), + xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) + end; +process_mucsub(#iq{lang = Lang} = IQ) -> + Txt = ?T("No module is handling this query"), + xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)). + +-spec should_start_room(stanza()) -> start | load | false. +should_start_room(#presence{type = available}) -> + start; +should_start_room(#iq{type = T} = IQ) when T == get; T == set -> + case xmpp:has_subtag(IQ, #muc_subscribe{}) orelse + xmpp:has_subtag(IQ, #muc_owner{}) of + true -> + start; _ -> - case mnesia:dirty_read(muc_online_room, {Room, Host}) of - [] -> - Type = xml:get_attr_s(<<"type">>, Attrs), - case {Name, Type} of - {<<"presence">>, <<"">>} -> - case check_user_can_create_room(ServerHost, - AccessCreate, From, - Room) of - true -> - {ok, Pid} = start_new_room( - Host, ServerHost, Access, - Room, HistorySize, - RoomShaper, From, - Nick, DefRoomOpts), - register_room(Host, Room, Pid), - mod_muc_room:route(Pid, From, Nick, Packet), - ok; - false -> - Lang = xml:get_attr_s(<<"xml:lang">>, Attrs), - ErrText = <<"Room creation is denied by service policy">>, - Err = jlib:make_error_reply( - Packet, ?ERRT_FORBIDDEN(Lang, ErrText)), - ejabberd_router:route(To, From, Err) + load + end; +should_start_room(#message{type = T, to = #jid{lresource = <<>>}}) + when T == groupchat; T == normal-> + load; +should_start_room(#message{type = T, to = #jid{lresource = Res}}) + when Res /= <<>> andalso T /= groupchat andalso T /= error -> + load; +should_start_room(_) -> + false. + +-spec check_create_room(binary(), binary(), binary(), jid()) -> boolean(). +check_create_room(ServerHost, Host, Room, From) -> + AccessCreate = mod_muc_opt:access_create(ServerHost), + case acl:match_rule(ServerHost, AccessCreate, From) of + allow -> + case mod_muc_opt:max_room_id(ServerHost) of + Max when byte_size(Room) =< Max -> + Regexp = mod_muc_opt:regexp_room_id(ServerHost), + case re:run(Room, Regexp, [{capture, none}]) of + match -> + AccessAdmin = mod_muc_opt:access_admin(ServerHost), + case acl:match_rule(ServerHost, AccessAdmin, From) of + allow -> + true; + _ -> + ejabberd_hooks:run_fold( + check_create_room, ServerHost, true, + [ServerHost, Room, Host]) end; _ -> - Lang = xml:get_attr_s(<<"xml:lang">>, Attrs), - ErrText = <<"Conference room does not exist">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_ITEM_NOT_FOUND(Lang, - ErrText)), - ejabberd_router:route(To, From, Err) - end; - [R] -> - Pid = R#muc_online_room.pid, - ?DEBUG("MUC: send to process ~p~n", [Pid]), - mod_muc_room:route(Pid, From, Nick, Packet), - ok - end + false + end; + _ -> + false + end; + _ -> + false end. -check_user_can_create_room(ServerHost, AccessCreate, - From, RoomID) -> - case acl:match_rule(ServerHost, AccessCreate, From) of - allow -> - byte_size(RoomID) =< - gen_mod:get_module_opt(ServerHost, ?MODULE, max_room_id, - fun(infinity) -> infinity; - (I) when is_integer(I), I>0 -> I - end, infinity); - _ -> false - end. +-spec get_access(binary() | gen_mod:opts()) -> access(). +get_access(ServerHost) -> + Access = mod_muc_opt:access(ServerHost), + AccessCreate = mod_muc_opt:access_create(ServerHost), + AccessAdmin = mod_muc_opt:access_admin(ServerHost), + AccessPersistent = mod_muc_opt:access_persistent(ServerHost), + AccessMam = mod_muc_opt:access_mam(ServerHost), + {Access, AccessCreate, AccessAdmin, AccessPersistent, AccessMam}. +-spec get_rooms(binary(), binary()) -> [#muc_room{}]. get_rooms(ServerHost, Host) -> - LServer = jlib:nameprep(ServerHost), - get_rooms(LServer, Host, - gen_mod:db_type(LServer, ?MODULE)). + Mod = gen_mod:db_mod(ServerHost, ?MODULE), + Mod:get_rooms(ServerHost, Host). -get_rooms(_LServer, Host, mnesia) -> - case catch mnesia:dirty_select(muc_room, - [{#muc_room{name_host = {'_', Host}, - _ = '_'}, - [], ['$_']}]) - of - {'EXIT', Reason} -> ?ERROR_MSG("~p", [Reason]), []; - Rs -> Rs - end; -get_rooms(_LServer, Host, riak) -> - case ejabberd_riak:get(muc_room, muc_room_schema()) of - {ok, Rs} -> - lists:filter( - fun(#muc_room{name_host = {_, H}}) -> - Host == H - end, Rs); - _Err -> - [] - end; -get_rooms(LServer, Host, odbc) -> - SHost = ejabberd_odbc:escape(Host), - case catch ejabberd_odbc:sql_query(LServer, - [<<"select name, opts from muc_room ">>, - <<"where host='">>, SHost, <<"';">>]) - of - {selected, [<<"name">>, <<"opts">>], RoomOpts} -> - lists:map(fun ([Room, Opts]) -> - #muc_room{name_host = {Room, Host}, - opts = opts_to_binary( - ejabberd_odbc:decode_term(Opts))} - end, - RoomOpts); - Err -> ?ERROR_MSG("failed to get rooms: ~p", [Err]), [] +-spec load_permanent_rooms([binary()], binary(), gen_mod:opts()) -> ok. +load_permanent_rooms(Hosts, ServerHost, Opts) -> + case mod_muc_opt:preload_rooms(Opts) of + true -> + lists:foreach( + fun(Host) -> + ?DEBUG("Loading rooms at ~ts", [Host]), + lists:foreach( + fun(R) -> + {Room, _} = R#muc_room.name_host, + unhibernate_room(ServerHost, Host, Room, false) + end, get_rooms(ServerHost, Host)) + end, Hosts); + false -> + ok end. -load_permanent_rooms(Host, ServerHost, Access, HistorySize, RoomShaper) -> - lists:foreach( - fun(R) -> - {Room, Host} = R#muc_room.name_host, - case mnesia:dirty_read(muc_online_room, {Room, Host}) of - [] -> - {ok, Pid} = mod_muc_room:start( - Host, - ServerHost, - Access, - Room, - HistorySize, - RoomShaper, - R#muc_room.opts), - register_room(Host, Room, Pid); - _ -> - ok - end - end, get_rooms(ServerHost, Host)). - -start_new_room(Host, ServerHost, Access, Room, - HistorySize, RoomShaper, From, - Nick, DefRoomOpts) -> +-spec load_room(module(), binary(), binary(), binary(), boolean()) -> + {ok, pid()} | {error, notfound | term()}. +load_room(RMod, Host, ServerHost, Room, ResetHibernationTime) -> case restore_room(ServerHost, Host, Room) of - error -> - ?DEBUG("MUC: open new room '~s'~n", [Room]), - mod_muc_room:start(Host, ServerHost, Access, - Room, HistorySize, - RoomShaper, From, - Nick, DefRoomOpts); - Opts -> - ?DEBUG("MUC: restore room '~s'~n", [Room]), - mod_muc_room:start(Host, ServerHost, Access, - Room, HistorySize, - RoomShaper, Opts) + error -> + {error, notfound}; + {error, _} = Err -> + Err; + Opts0 -> + Mod = gen_mod:db_mod(ServerHost, mod_muc), + case proplists:get_bool(persistent, Opts0) of + true -> + ?DEBUG("Restore room: ~ts", [Room]), + Res2 = start_room(RMod, Host, ServerHost, Room, Opts0), + case {Res2, ResetHibernationTime} of + {{ok, _}, true} -> + NewOpts = lists:keyreplace(hibernation_time, 1, Opts0, {hibernation_time, undefined}), + store_room(ServerHost, Host, Room, NewOpts, []); + _ -> + ok + end, + Res2; + _ -> + ?DEBUG("Restore hibernated non-persistent room: ~ts", [Room]), + Res = start_room(RMod, Host, ServerHost, Room, Opts0), + case erlang:function_exported(Mod, get_subscribed_rooms, 3) of + true -> + ok; + _ -> + forget_room(ServerHost, Host, Room) + end, + Res + end end. -register_room(Host, Room, Pid) -> - F = fun() -> - mnesia:write(#muc_online_room{name_host = {Room, Host}, - pid = Pid}) - end, - mnesia:transaction(F). +start_new_room(RMod, Host, ServerHost, Room, Pass, From, Nick) -> + ?DEBUG("Open new room: ~ts", [Room]), + DefRoomOpts = mod_muc_opt:default_room_options(ServerHost), + DefRoomOpts2 = add_password_options(Pass, DefRoomOpts), + start_room(RMod, Host, ServerHost, Room, DefRoomOpts2, From, Nick). +add_password_options(false, DefRoomOpts) -> + DefRoomOpts; +add_password_options(<<>>, DefRoomOpts) -> + DefRoomOpts; +add_password_options(Pass, DefRoomOpts) when is_binary(Pass) -> + O2 = lists:keystore(password, 1, DefRoomOpts, {password, Pass}), + lists:keystore(password_protected, 1, O2, {password_protected, true}). -iq_disco_info(Lang) -> - [#xmlel{name = <<"identity">>, - attrs = - [{<<"category">>, <<"conference">>}, - {<<"type">>, <<"text">>}, - {<<"name">>, - translate:translate(Lang, <<"Chatrooms">>)}], - children = []}, - #xmlel{name = <<"feature">>, - attrs = [{<<"var">>, ?NS_DISCO_INFO}], children = []}, - #xmlel{name = <<"feature">>, - attrs = [{<<"var">>, ?NS_DISCO_ITEMS}], children = []}, - #xmlel{name = <<"feature">>, - attrs = [{<<"var">>, ?NS_MUC}], children = []}, - #xmlel{name = <<"feature">>, - attrs = [{<<"var">>, ?NS_MUC_UNIQUE}], children = []}, - #xmlel{name = <<"feature">>, - attrs = [{<<"var">>, ?NS_REGISTER}], children = []}, - #xmlel{name = <<"feature">>, - attrs = [{<<"var">>, ?NS_RSM}], children = []}, - #xmlel{name = <<"feature">>, - attrs = [{<<"var">>, ?NS_VCARD}], children = []}]. +start_room(Mod, Host, ServerHost, Room, DefOpts) -> + Access = get_access(ServerHost), + HistorySize = mod_muc_opt:history_size(ServerHost), + QueueType = mod_muc_opt:queue_type(ServerHost), + RoomShaper = mod_muc_opt:room_shaper(ServerHost), + start_room(Mod, Host, ServerHost, Access, Room, HistorySize, + RoomShaper, DefOpts, QueueType). -iq_disco_items(Host, From, Lang, none) -> - lists:zf(fun (#muc_online_room{name_host = - {Name, _Host}, - pid = Pid}) -> - case catch gen_fsm:sync_send_all_state_event(Pid, - {get_disco_item, - From, Lang}, - 100) - of - {item, Desc} -> - flush(), - {true, - #xmlel{name = <<"item">>, - attrs = - [{<<"jid">>, - jlib:jid_to_string({Name, Host, - <<"">>})}, - {<<"name">>, Desc}], - children = []}}; - _ -> false - end - end, get_vh_rooms(Host)); +start_room(Mod, Host, ServerHost, Room, DefOpts, Creator, Nick) -> + Access = get_access(ServerHost), + HistorySize = mod_muc_opt:history_size(ServerHost), + QueueType = mod_muc_opt:queue_type(ServerHost), + RoomShaper = mod_muc_opt:room_shaper(ServerHost), + start_room(Mod, Host, ServerHost, Access, Room, + HistorySize, RoomShaper, + Creator, Nick, DefOpts, QueueType). -iq_disco_items(Host, From, Lang, Rsm) -> - {Rooms, RsmO} = get_vh_rooms(Host, Rsm), - RsmOut = jlib:rsm_encode(RsmO), - lists:zf(fun (#muc_online_room{name_host = - {Name, _Host}, - pid = Pid}) -> - case catch gen_fsm:sync_send_all_state_event(Pid, - {get_disco_item, - From, Lang}, - 100) - of - {item, Desc} -> - flush(), - {true, - #xmlel{name = <<"item">>, - attrs = - [{<<"jid">>, - jlib:jid_to_string({Name, Host, - <<"">>})}, - {<<"name">>, Desc}], - children = []}}; - _ -> false - end - end, - Rooms) - ++ RsmOut. +start_room(Mod, Host, ServerHost, Access, Room, + HistorySize, RoomShaper, DefOpts, QueueType) -> + case mod_muc_room:start(Host, ServerHost, Access, Room, + HistorySize, RoomShaper, DefOpts, QueueType) of + {ok, Pid} -> + erlang:monitor(process, Pid), + Mod:register_online_room(ServerHost, Room, Host, Pid), + {ok, Pid}; + Err -> + Err + end. -get_vh_rooms(Host, #rsm_in{max=M, direction=Direction, id=I, index=Index})-> - AllRooms = lists:sort(get_vh_rooms(Host)), - Count = erlang:length(AllRooms), - Guard = case Direction of - _ when Index =/= undefined -> [{'==', {element, 2, '$1'}, Host}]; - aft -> [{'==', {element, 2, '$1'}, Host}, {'>=',{element, 1, '$1'} ,I}]; - before when I =/= []-> [{'==', {element, 2, '$1'}, Host}, {'=<',{element, 1, '$1'} ,I}]; - _ -> [{'==', {element, 2, '$1'}, Host}] +start_room(Mod, Host, ServerHost, Access, Room, HistorySize, + RoomShaper, Creator, Nick, DefOpts, QueueType) -> + case mod_muc_room:start(Host, ServerHost, Access, Room, + HistorySize, RoomShaper, + Creator, Nick, DefOpts, QueueType) of + {ok, Pid} -> + erlang:monitor(process, Pid), + Mod:register_online_room(ServerHost, Room, Host, Pid), + {ok, Pid}; + Err -> + Err + end. + +-spec iq_disco_items(binary(), binary(), jid(), binary(), integer(), binary(), + rsm_set() | undefined) -> + {result, disco_items()} | {error, stanza_error()}. +iq_disco_items(ServerHost, Host, From, Lang, MaxRoomsDiscoItems, Node, RSM) + when Node == <<"">>; Node == <<"nonemptyrooms">>; Node == <<"emptyrooms">> -> + Count = count_online_rooms(ServerHost, Host), + Query = if Node == <<"">>, RSM == undefined, Count > MaxRoomsDiscoItems -> + {only_non_empty, From, Lang}; + Node == <<"nonemptyrooms">> -> + {only_non_empty, From, Lang}; + Node == <<"emptyrooms">> -> + {0, From, Lang}; + true -> + {all, From, Lang} end, - L = lists:sort( - mnesia:dirty_select(muc_online_room, - [{#muc_online_room{name_host = '$1', _ = '_'}, - Guard, - ['$_']}])), - L2 = if - Index == undefined andalso Direction == before -> - lists:reverse(lists:sublist(lists:reverse(L), 1, M)); - Index == undefined -> - lists:sublist(L, 1, M); - Index > Count orelse Index < 0 -> - []; - true -> - lists:sublist(L, Index+1, M) - end, - if L2 == [] -> {L2, #rsm_out{count = Count}}; - true -> - H = hd(L2), - NewIndex = get_room_pos(H, AllRooms), - T = lists:last(L2), - {F, _} = H#muc_online_room.name_host, - {Last, _} = T#muc_online_room.name_host, - {L2, - #rsm_out{first = F, last = Last, count = Count, - index = NewIndex}} + MaxItems = case RSM of + undefined -> + MaxRoomsDiscoItems; + #rsm_set{max = undefined} -> + MaxRoomsDiscoItems; + #rsm_set{max = Max} when Max > MaxRoomsDiscoItems -> + MaxRoomsDiscoItems; + #rsm_set{max = Max} -> + Max + end, + RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), + RsmSupported = RMod:rsm_supported(), + GetRooms = + fun GetRooms(AccInit, Rooms) -> + {Items, HitMax, DidSkip, Last, First} = lists:foldr( + fun(_, {Acc, _, Skip, F, L}) when length(Acc) >= MaxItems -> + {Acc, true, Skip, F, L}; + ({RN, _, _} = R, {Acc, _, Skip, F, _}) -> + F2 = if F == undefined -> RN; true -> F end, + case get_room_disco_item(R, Query) of + {ok, Item} -> {[Item | Acc], false, Skip, F2, RN}; + {error, _} -> {Acc, false, true, F2, RN} + end + end, AccInit, Rooms), + if RsmSupported andalso not HitMax andalso DidSkip -> + RSM2 = case RSM of + #rsm_set{'after' = undefined, before = undefined} -> + #rsm_set{max = MaxItems - length(Items), 'after' = Last}; + #rsm_set{'after' = undefined} -> + #rsm_set{max = MaxItems - length(Items), 'before' = First}; + _ -> + #rsm_set{max = MaxItems - length(Items), 'after' = Last} + end, + GetRooms({Items, false, false, undefined, undefined}, + get_online_rooms(ServerHost, Host, RSM2)); + true -> {Items, HitMax} + end + end, + + {Items, HitMax} = + GetRooms({[], false, false, undefined, undefined}, + get_online_rooms(ServerHost, Host, RSM)), + ResRSM = case Items of + [_|_] when RSM /= undefined; HitMax -> + #disco_item{jid = #jid{luser = First}} = hd(Items), + #disco_item{jid = #jid{luser = Last}} = lists:last(Items), + #rsm_set{first = #rsm_first{data = First}, + last = Last, + count = Count}; + [] when RSM /= undefined -> + #rsm_set{count = Count}; + _ -> + undefined + end, + {result, #disco_items{node = Node, items = Items, rsm = ResRSM}}; +iq_disco_items(_ServerHost, _Host, _From, Lang, _MaxRoomsDiscoItems, _Node, _RSM) -> + {error, xmpp:err_item_not_found(?T("Node not found"), Lang)}. + +-spec get_room_disco_item({binary(), binary(), pid()}, + {mod_muc_room:disco_item_filter(), + jid(), binary()}) -> {ok, disco_item()} | + {error, timeout | notfound}. +get_room_disco_item({Name, Host, Pid}, {Filter, JID, Lang}) -> + case mod_muc_room:get_disco_item(Pid, Filter, JID, Lang) of + {ok, Desc} -> + RoomJID = jid:make(Name, Host), + {ok, #disco_item{jid = RoomJID, name = Desc}}; + {error, _} = Err -> + Err end. -%% @doc Return the position of desired room in the list of rooms. -%% The room must exist in the list. The count starts in 0. -%% @spec (Desired::muc_online_room(), Rooms::[muc_online_room()]) -> integer() -get_room_pos(Desired, Rooms) -> - get_room_pos(Desired, Rooms, 0). +-spec get_subscribed_rooms(binary(), jid()) -> {ok, [{jid(), binary(), [binary()]}]} | {error, any()}. +get_subscribed_rooms(Host, User) -> + ServerHost = ejabberd_router:host_of_route(Host), + get_subscribed_rooms(ServerHost, Host, User). -get_room_pos(Desired, [HeadRoom | _], HeadPosition) - when Desired#muc_online_room.name_host == - HeadRoom#muc_online_room.name_host -> - HeadPosition; -get_room_pos(Desired, [_ | Rooms], HeadPosition) -> - get_room_pos(Desired, Rooms, HeadPosition + 1). - -flush() -> receive _ -> flush() after 0 -> ok end. - --define(XFIELD(Type, Label, Var, Val), -%% @doc Get a pseudo unique Room Name. The Room Name is generated as a hash of -%% the requester JID, the local time and a random salt. -%% -%% "pseudo" because we don't verify that there is not a room -%% with the returned Name already created, nor mark the generated Name -%% as "already used". But in practice, it is unique enough. See -%% http://xmpp.org/extensions/xep-0045.html#createroom-unique - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, Type}, - {<<"label">>, translate:translate(Lang, Label)}, - {<<"var">>, Var}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, Val}]}]}). - -iq_get_unique(From) -> - {xmlcdata, - p1_sha:sha(term_to_binary([From, now(), - randoms:get_string()]))}. +-spec get_subscribed_rooms(binary(), binary(), jid()) -> + {ok, [{jid(), binary(), [binary()]}]} | {error, any()}. +get_subscribed_rooms(ServerHost, Host, From) -> + LServer = jid:nameprep(ServerHost), + Mod = gen_mod:db_mod(LServer, ?MODULE), + BareFrom = jid:remove_resource(From), + case erlang:function_exported(Mod, get_subscribed_rooms, 3) of + false -> + Rooms = get_online_rooms(ServerHost, Host), + {ok, lists:flatmap( + fun({Name, _, Pid}) when Pid == self() -> + USR = jid:split(BareFrom), + case erlang:get(muc_subscribers) of + #{USR := #subscriber{nodes = Nodes, nick = Nick}} -> + [{jid:make(Name, Host), Nick, Nodes}]; + _ -> + [] + end; + ({Name, _, Pid}) -> + case mod_muc_room:is_subscribed(Pid, BareFrom) of + {true, Nick, Nodes} -> + [{jid:make(Name, Host), Nick, Nodes}]; + false -> [] + end; + (_) -> + [] + end, Rooms)}; + true -> + Mod:get_subscribed_rooms(LServer, Host, BareFrom) + end. get_nick(ServerHost, Host, From) -> - LServer = jlib:nameprep(ServerHost), - get_nick(LServer, Host, From, - gen_mod:db_type(LServer, ?MODULE)). - -get_nick(_LServer, Host, From, mnesia) -> - {LUser, LServer, _} = jlib:jid_tolower(From), - LUS = {LUser, LServer}, - case catch mnesia:dirty_read(muc_registered, - {LUS, Host}) - of - {'EXIT', _Reason} -> error; - [] -> error; - [#muc_registered{nick = Nick}] -> Nick - end; -get_nick(LServer, Host, From, riak) -> - {LUser, LServer, _} = jlib:jid_tolower(From), - US = {LUser, LServer}, - case ejabberd_riak:get(muc_registered, - muc_registered_schema(), - {US, Host}) of - {ok, #muc_registered{nick = Nick}} -> Nick; - {error, _} -> error - end; -get_nick(LServer, Host, From, odbc) -> - SJID = - ejabberd_odbc:escape(jlib:jid_to_string(jlib:jid_tolower(jlib:jid_remove_resource(From)))), - SHost = ejabberd_odbc:escape(Host), - case catch ejabberd_odbc:sql_query(LServer, - [<<"select nick from muc_registered where " - "jid='">>, - SJID, <<"' and host='">>, SHost, - <<"';">>]) - of - {selected, [<<"nick">>], [[Nick]]} -> Nick; - _ -> error - end. + LServer = jid:nameprep(ServerHost), + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:get_nick(LServer, Host, From). iq_get_register_info(ServerHost, Host, From, Lang) -> - {Nick, Registered} = case get_nick(ServerHost, Host, - From) - of - error -> {<<"">>, []}; - N -> - {N, - [#xmlel{name = <<"registered">>, attrs = [], - children = []}]} + {Nick, Registered} = case get_nick(ServerHost, Host, From) of + error -> {<<"">>, false}; + N -> {N, true} end, - Registered ++ - [#xmlel{name = <<"instructions">>, attrs = [], - children = - [{xmlcdata, - translate:translate(Lang, - <<"You need a client that supports x:data " - "to register the nickname">>)}]}, - #xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_XDATA}, - {<<"type">>, <<"form">>}], - children = - [#xmlel{name = <<"title">>, attrs = [], - children = - [{xmlcdata, - <<(translate:translate(Lang, - <<"Nickname Registration at ">>))/binary, - Host/binary>>}]}, - #xmlel{name = <<"instructions">>, attrs = [], - children = - [{xmlcdata, - translate:translate(Lang, - <<"Enter nickname you want to register">>)}]}, - ?XFIELD(<<"text-single">>, <<"Nickname">>, <<"nick">>, - Nick)]}]. + Title = <<(translate:translate( + Lang, ?T("Nickname Registration at ")))/binary, Host/binary>>, + Inst = translate:translate(Lang, ?T("Enter nickname you want to register")), + Fields = muc_register:encode([{roomnick, Nick}], Lang), + X = #xdata{type = form, title = Title, + instructions = [Inst], fields = Fields}, + #register{nick = Nick, + registered = Registered, + instructions = + translate:translate( + Lang, ?T("You need a client that supports x:data " + "to register the nickname")), + xdata = X}. set_nick(ServerHost, Host, From, Nick) -> - LServer = jlib:nameprep(ServerHost), - set_nick(LServer, Host, From, Nick, - gen_mod:db_type(LServer, ?MODULE)). - -set_nick(_LServer, Host, From, Nick, mnesia) -> - {LUser, LServer, _} = jlib:jid_tolower(From), - LUS = {LUser, LServer}, - F = fun () -> - case Nick of - <<"">> -> - mnesia:delete({muc_registered, {LUS, Host}}), ok; - _ -> - Allow = case mnesia:select(muc_registered, - [{#muc_registered{us_host = - '$1', - nick = Nick, - _ = '_'}, - [{'==', {element, 2, '$1'}, - Host}], - ['$_']}]) - of - [] -> true; - [#muc_registered{us_host = {U, _Host}}] -> - U == LUS - end, - if Allow -> - mnesia:write(#muc_registered{us_host = {LUS, Host}, - nick = Nick}), - ok; - true -> false - end - end - end, - mnesia:transaction(F); -set_nick(LServer, Host, From, Nick, riak) -> - {LUser, LServer, _} = jlib:jid_tolower(From), - LUS = {LUser, LServer}, - {atomic, - case Nick of - <<"">> -> - ejabberd_riak:delete(muc_registered, {LUS, Host}); - _ -> - Allow = case ejabberd_riak:get_by_index( - muc_registered, - muc_registered_schema(), - <<"nick_host">>, {Nick, Host}) of - {ok, []} -> - true; - {ok, [#muc_registered{us_host = {U, _Host}}]} -> - U == LUS; - {error, _} -> - false - end, - if Allow -> - ejabberd_riak:put(#muc_registered{us_host = {LUS, Host}, - nick = Nick}, - muc_registered_schema(), - [{'2i', [{<<"nick_host">>, - {Nick, Host}}]}]); - true -> - false - end - end}; -set_nick(LServer, Host, From, Nick, odbc) -> - JID = - jlib:jid_to_string(jlib:jid_tolower(jlib:jid_remove_resource(From))), - SJID = ejabberd_odbc:escape(JID), - SNick = ejabberd_odbc:escape(Nick), - SHost = ejabberd_odbc:escape(Host), - F = fun () -> - case Nick of - <<"">> -> - ejabberd_odbc:sql_query_t([<<"delete from muc_registered where ">>, - <<"jid='">>, SJID, - <<"' and host='">>, Host, - <<"';">>]), - ok; - _ -> - Allow = case - ejabberd_odbc:sql_query_t([<<"select jid from muc_registered ">>, - <<"where nick='">>, - SNick, - <<"' and host='">>, - SHost, <<"';">>]) - of - {selected, [<<"jid">>], [[J]]} -> J == JID; - _ -> true - end, - if Allow -> - odbc_queries:update_t(<<"muc_registered">>, - [<<"jid">>, <<"host">>, - <<"nick">>], - [SJID, SHost, SNick], - [<<"jid='">>, SJID, - <<"' and host='">>, SHost, - <<"'">>]), - ok; - true -> false - end - end - end, - ejabberd_odbc:sql_transaction(LServer, F). + LServer = jid:nameprep(ServerHost), + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:set_nick(LServer, Host, From, Nick). iq_set_register_info(ServerHost, Host, From, Nick, Lang) -> case set_nick(ServerHost, Host, From, Nick) of - {atomic, ok} -> {result, []}; + {atomic, ok} -> {result, undefined}; {atomic, false} -> - ErrText = <<"That nickname is registered by another " - "person">>, - {error, ?ERRT_CONFLICT(Lang, ErrText)}; - _ -> {error, ?ERR_INTERNAL_SERVER_ERROR} - end. - -process_iq_register_set(ServerHost, Host, From, SubEl, - Lang) -> - #xmlel{children = Els} = SubEl, - case xml:get_subtag(SubEl, <<"remove">>) of - false -> - case xml:remove_cdata(Els) of - [#xmlel{name = <<"x">>} = XEl] -> - case {xml:get_tag_attr_s(<<"xmlns">>, XEl), - xml:get_tag_attr_s(<<"type">>, XEl)} - of - {?NS_XDATA, <<"cancel">>} -> {result, []}; - {?NS_XDATA, <<"submit">>} -> - XData = jlib:parse_xdata_submit(XEl), - case XData of - invalid -> {error, ?ERR_BAD_REQUEST}; - _ -> - case lists:keysearch(<<"nick">>, 1, XData) of - {value, {_, [Nick]}} when Nick /= <<"">> -> - iq_set_register_info(ServerHost, Host, From, - Nick, Lang); - _ -> - ErrText = - <<"You must fill in field \"Nickname\" " - "in the form">>, - {error, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)} - end - end; - _ -> {error, ?ERR_BAD_REQUEST} - end; - _ -> {error, ?ERR_BAD_REQUEST} - end; + ErrText = ?T("That nickname is registered by another person"), + {error, xmpp:err_conflict(ErrText, Lang)}; _ -> - iq_set_register_info(ServerHost, Host, From, <<"">>, - Lang) + Txt = ?T("Database failure"), + {error, xmpp:err_internal_server_error(Txt, Lang)} end. -iq_get_vcard(Lang) -> - [#xmlel{name = <<"FN">>, attrs = [], - children = [{xmlcdata, <<"ejabberd/mod_muc">>}]}, - #xmlel{name = <<"URL">>, attrs = [], - children = [{xmlcdata, ?EJABBERD_URI}]}, - #xmlel{name = <<"DESC">>, attrs = [], - children = - [{xmlcdata, - <<(translate:translate(Lang, - <<"ejabberd MUC module">>))/binary, - "\nCopyright (c) 2003-2015 ProcessOne">>}]}]. +process_iq_register_set(ServerHost, Host, From, + #register{remove = true}, Lang) -> + iq_set_register_info(ServerHost, Host, From, <<"">>, Lang); +process_iq_register_set(_ServerHost, _Host, _From, + #register{xdata = #xdata{type = cancel}}, _Lang) -> + {result, undefined}; +process_iq_register_set(ServerHost, Host, From, + #register{nick = Nick, xdata = XData}, Lang) -> + case XData of + #xdata{type = submit, fields = Fs} -> + try + Options = muc_register:decode(Fs), + N = proplists:get_value(roomnick, Options), + iq_set_register_info(ServerHost, Host, From, N, Lang) + catch _:{muc_register, Why} -> + ErrText = muc_register:format_error(Why), + {error, xmpp:err_bad_request(ErrText, Lang)} + end; + #xdata{} -> + Txt = ?T("Incorrect data form"), + {error, xmpp:err_bad_request(Txt, Lang)}; + _ when is_binary(Nick), Nick /= <<"">> -> + iq_set_register_info(ServerHost, Host, From, Nick, Lang); + _ -> + ErrText = ?T("You must fill in field \"Nickname\" in the form"), + {error, xmpp:err_not_acceptable(ErrText, Lang)} + end. - -broadcast_service_message(Host, Msg) -> +-spec broadcast_service_message(binary(), binary(), binary()) -> ok. +broadcast_service_message(ServerHost, Host, Msg) -> lists:foreach( - fun(#muc_online_room{pid = Pid}) -> - gen_fsm:send_all_state_event( - Pid, {service_message, Msg}) - end, get_vh_rooms(Host)). + fun({_, _, Pid}) -> + mod_muc_room:service_message(Pid, Msg) + end, get_online_rooms(ServerHost, Host)). +-spec get_online_rooms(binary(), binary()) -> [{binary(), binary(), pid()}]. +get_online_rooms(ServerHost, Host) -> + get_online_rooms(ServerHost, Host, undefined). -get_vh_rooms(Host) -> - mnesia:dirty_select(muc_online_room, - [{#muc_online_room{name_host = '$1', _ = '_'}, - [{'==', {element, 2, '$1'}, Host}], - ['$_']}]). +-spec get_online_rooms(binary(), binary(), undefined | rsm_set()) -> + [{binary(), binary(), pid()}]. +get_online_rooms(ServerHost, Host, RSM) -> + RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), + RMod:get_online_rooms(ServerHost, Host, RSM). +-spec count_online_rooms(binary(), binary()) -> non_neg_integer(). +count_online_rooms(ServerHost, Host) -> + RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), + RMod:count_online_rooms(ServerHost, Host). -clean_table_from_bad_node(Node) -> - F = fun() -> - Es = mnesia:select( - muc_online_room, - [{#muc_online_room{pid = '$1', _ = '_'}, - [{'==', {node, '$1'}, Node}], - ['$_']}]), - lists:foreach(fun(E) -> - mnesia:delete_object(E) - end, Es) - end, - mnesia:async_dirty(F). - -clean_table_from_bad_node(Node, Host) -> - F = fun() -> - Es = mnesia:select( - muc_online_room, - [{#muc_online_room{pid = '$1', - name_host = {'_', Host}, - _ = '_'}, - [{'==', {node, '$1'}, Node}], - ['$_']}]), - lists:foreach(fun(E) -> - mnesia:delete_object(E) - end, Es) - end, - mnesia:async_dirty(F). +-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( + lists:flatmap( fun({title, Title}) -> - {title, iolist_to_binary(Title)}; + [{title, iolist_to_binary(Title)}]; ({description, Desc}) -> - {description, iolist_to_binary(Desc)}; + [{description, iolist_to_binary(Desc)}]; ({password, Pass}) -> - {password, iolist_to_binary(Pass)}; - ({subject, Subj}) -> - {subject, iolist_to_binary(Subj)}; - ({subject_author, Author}) -> - {subject_author, iolist_to_binary(Author)}; - ({affiliations, Affs}) -> - {affiliations, lists:map( + [{password, iolist_to_binary(Pass)}]; + ({subject, [C|_] = Subj}) when is_integer(C), C >= 0, C =< 255 -> + [{subject, iolist_to_binary(Subj)}]; + ({subject_author, {AuthorNick, AuthorJID}}) -> + [{subject_author, {iolist_to_binary(AuthorNick), AuthorJID}}]; + ({subject_author, AuthorNick}) -> % ejabberd 23.04 or older + [{subject_author, {iolist_to_binary(AuthorNick), #jid{}}}]; + ({allow_private_messages, Value}) -> % ejabberd 23.04 or older + Value2 = case Value of + true -> anyone; + false -> none; + _ -> Value + end, + [{allowpm, Value2}]; + ({AffOrRole, Affs}) when (AffOrRole == affiliation) or (AffOrRole == role) -> + [{affiliations, lists:map( fun({{U, S, R}, Aff}) -> NewAff = case Aff of @@ -1164,128 +1244,666 @@ opts_to_binary(Opts) -> iolist_to_binary(S), iolist_to_binary(R)}, NewAff} - end, Affs)}; + end, Affs)}]; ({captcha_whitelist, CWList}) -> - {captcha_whitelist, lists:map( + [{captcha_whitelist, lists:map( fun({U, S, R}) -> {iolist_to_binary(U), iolist_to_binary(S), iolist_to_binary(R)} - end, CWList)}; + end, CWList)}]; + ({hats_users, HatsUsers}) -> % Update hats definitions + case lists:keymember(hats_defs, 1, Opts) of + true -> + [{hats_users, HatsUsers}]; + _ -> + {HatsDefs, HatsUsers2} = + lists:foldl(fun({Jid, UriTitleList}, {Defs, Assigns}) -> + Defs2 = + lists:foldl(fun({Uri, Title}, AccDef) -> + AccDef#{Uri => {Title, <<"">>}} + end, + Defs, + UriTitleList), + Assigns2 = + Assigns#{Jid => [ Uri || {Uri, _Title} <- UriTitleList ]}, + {Defs2, Assigns2} + end, + {maps:new(), maps:new()}, + HatsUsers), + [{hats_users, maps:to_list(HatsUsers2)}, + {hats_defs, maps:to_list(HatsDefs)}] + end; (Opt) -> - Opt + [Opt] end, Opts). -update_tables(Host) -> - update_muc_room_table(Host), - update_muc_registered_table(Host). +export(LServer) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:export(LServer). -muc_room_schema() -> - {record_info(fields, muc_room), #muc_room{}}. +import_info() -> + [{<<"muc_room">>, 4}, {<<"muc_registered">>, 4}]. -muc_registered_schema() -> - {record_info(fields, muc_registered), #muc_registered{}}. +import_start(LServer, DBType) -> + Mod = gen_mod:db_mod(DBType, ?MODULE), + Mod:init(LServer, []). -update_muc_room_table(_Host) -> - Fields = record_info(fields, muc_room), - case mnesia:table_info(muc_room, attributes) of - Fields -> - ejabberd_config:convert_table_to_binary( - muc_room, Fields, set, - fun(#muc_room{name_host = {N, _}}) -> N end, - fun(#muc_room{name_host = {N, H}, - opts = Opts} = R) -> - R#muc_room{name_host = {iolist_to_binary(N), - iolist_to_binary(H)}, - opts = opts_to_binary(Opts)} - end); - _ -> - ?INFO_MSG("Recreating muc_room table", []), - mnesia:transform_table(muc_room, ignore, Fields) - end. +import(LServer, {sql, _}, DBType, Tab, L) -> + Mod = gen_mod:db_mod(DBType, ?MODULE), + Mod:import(LServer, Tab, L). -update_muc_registered_table(_Host) -> - Fields = record_info(fields, muc_registered), - case mnesia:table_info(muc_registered, attributes) of - Fields -> - ejabberd_config:convert_table_to_binary( - muc_registered, Fields, set, - fun(#muc_registered{us_host = {_, H}}) -> H end, - fun(#muc_registered{us_host = {{U, S}, H}, - nick = Nick} = R) -> - R#muc_registered{us_host = {{iolist_to_binary(U), - iolist_to_binary(S)}, - iolist_to_binary(H)}, - nick = iolist_to_binary(Nick)} - end); - _ -> - ?INFO_MSG("Recreating muc_registered table", []), - mnesia:transform_table(muc_registered, ignore, Fields) - end. +mod_opt_type(access) -> + econf:acl(); +mod_opt_type(access_admin) -> + econf:acl(); +mod_opt_type(access_create) -> + econf:acl(); +mod_opt_type(access_persistent) -> + econf:acl(); +mod_opt_type(access_mam) -> + econf:acl(); +mod_opt_type(access_register) -> + econf:acl(); +mod_opt_type(history_size) -> + econf:non_neg_int(); +mod_opt_type(name) -> + econf:binary(); +mod_opt_type(max_room_desc) -> + econf:pos_int(infinity); +mod_opt_type(max_room_id) -> + econf:pos_int(infinity); +mod_opt_type(max_rooms_discoitems) -> + econf:non_neg_int(); +mod_opt_type(regexp_room_id) -> + econf:re([unicode]); +mod_opt_type(max_room_name) -> + econf:pos_int(infinity); +mod_opt_type(max_password) -> + econf:pos_int(infinity); +mod_opt_type(max_captcha_whitelist) -> + econf:pos_int(infinity); +mod_opt_type(max_user_conferences) -> + econf:pos_int(); +mod_opt_type(max_users) -> + econf:pos_int(); +mod_opt_type(max_users_admin_threshold) -> + econf:pos_int(); +mod_opt_type(max_users_presence) -> + econf:int(); +mod_opt_type(min_message_interval) -> + econf:number(0); +mod_opt_type(min_presence_interval) -> + econf:number(0); +mod_opt_type(preload_rooms) -> + econf:bool(); +mod_opt_type(room_shaper) -> + econf:atom(); +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(), + allowpm => + econf:enum([anyone, participants, moderators, none]), + allow_private_messages_from_visitors => + econf:enum([anyone, moderators, nobody]), + allow_query_users => econf:bool(), + allow_subscription => econf:bool(), + allow_user_invites => econf:bool(), + allow_visitor_nickchange => econf:bool(), + allow_visitor_status => econf:bool(), + allow_voice_requests => econf:bool(), + anonymous => econf:bool(), + captcha_protected => econf:bool(), + description => econf:binary(), + enable_hats => econf:bool(), + lang => econf:lang(), + logging => econf:bool(), + mam => econf:bool(), + max_users => econf:pos_int(), + members_by_default => econf:bool(), + members_only => econf:bool(), + moderated => econf:bool(), + password => econf:binary(), + password_protected => econf:bool(), + persistent => econf:bool(), + presence_broadcast => + econf:list( + econf:enum([moderator, participant, visitor])), + public => econf:bool(), + public_list => econf:bool(), + pubsub => econf:binary(), + title => econf:binary(), + vcard => econf:vcard_temp(), + vcard_xupdate => econf:binary(), + voice_request_min_interval => econf:pos_int()}); +mod_opt_type(db_type) -> + econf:db_type(?MODULE); +mod_opt_type(ram_db_type) -> + econf:db_type(?MODULE); +mod_opt_type(host) -> + econf:host(); +mod_opt_type(hosts) -> + econf:hosts(); +mod_opt_type(queue_type) -> + econf:queue_type(); +mod_opt_type(hibernation_timeout) -> + econf:timeout(second, infinity); +mod_opt_type(vcard) -> + econf:vcard_temp(). -export(_Server) -> - [{muc_room, - fun(Host, #muc_room{name_host = {Name, RoomHost}, opts = Opts}) -> - case str:suffix(Host, RoomHost) of - true -> - SName = ejabberd_odbc:escape(Name), - SRoomHost = ejabberd_odbc:escape(RoomHost), - SOpts = ejabberd_odbc:encode_term(Opts), - [[<<"delete from muc_room where name='">>, SName, - <<"' and host='">>, SRoomHost, <<"';">>], - [<<"insert into muc_room(name, host, opts) ", - "values (">>, - <<"'">>, SName, <<"', '">>, SRoomHost, - <<"', '">>, SOpts, <<"');">>]]; - false -> - [] - end - end}, - {muc_registered, - fun(Host, #muc_registered{us_host = {{U, S}, RoomHost}, - nick = Nick}) -> - case str:suffix(Host, RoomHost) of - true -> - SJID = ejabberd_odbc:escape( - jlib:jid_to_string( - jlib:make_jid(U, S, <<"">>))), - SNick = ejabberd_odbc:escape(Nick), - SRoomHost = ejabberd_odbc:escape(RoomHost), - [[<<"delete from muc_registered where jid='">>, - SJID, <<"' and host='">>, SRoomHost, <<"';">>], - [<<"insert into muc_registered(jid, host, " - "nick) values ('">>, - SJID, <<"', '">>, SRoomHost, <<"', '">>, SNick, - <<"');">>]]; - false -> - [] - end - end}]. +mod_options(Host) -> + [{access, all}, + {access_admin, none}, + {access_create, all}, + {access_persistent, all}, + {access_mam, all}, + {access_register, all}, + {db_type, ejabberd_config:default_db(Host, ?MODULE)}, + {ram_db_type, ejabberd_config:default_ram_db(Host, ?MODULE)}, + {history_size, 20}, + {host, <<"conference.", Host/binary>>}, + {hosts, []}, + {name, ?T("Chatrooms")}, + {max_room_desc, infinity}, + {max_room_id, infinity}, + {max_room_name, infinity}, + {max_password, infinity}, + {max_captcha_whitelist, infinity}, + {max_rooms_discoitems, 100}, + {max_user_conferences, 100}, + {max_users, 200}, + {max_users_admin_threshold, 5}, + {max_users_presence, 1000}, + {min_message_interval, 0}, + {min_presence_interval, 0}, + {queue_type, ejabberd_option:queue_type(Host)}, + {regexp_room_id, <<"">>}, + {room_shaper, none}, + {user_message_shaper, none}, + {user_presence_shaper, none}, + {preload_rooms, true}, + {hibernation_timeout, infinity}, + {vcard, undefined}, + {cleanup_affiliations_on_start, false}, + {default_room_options, + [{allow_change_subj,true}, + {allowpm,anyone}, + {allow_query_users,true}, + {allow_user_invites,false}, + {allow_visitor_nickchange,true}, + {allow_visitor_status,true}, + {anonymous,true}, + {captcha_protected,false}, + {lang,<<>>}, + {logging,false}, + {members_by_default,true}, + {members_only,false}, + {moderated,true}, + {password_protected,false}, + {persistent,false}, + {public,true}, + {public_list,true}, + {mam,false}, + {allow_subscription,false}, + {password,<<>>}, + {title,<<>>}, + {allow_private_messages_from_visitors,anyone}, + {max_users,200}, + {presence_broadcast,[moderator,participant,visitor]}]}]. -import(_LServer) -> - [{<<"select name, host, opts from muc_room;">>, - fun([Name, RoomHost, SOpts]) -> - Opts = opts_to_binary(ejabberd_odbc:decode_term(SOpts)), - #muc_room{name_host = {Name, RoomHost}, - opts = Opts} - end}, - {<<"select jid, host, nick from muc_registered;">>, - fun([J, RoomHost, Nick]) -> - #jid{user = U, server = S} = - jlib:string_to_jid(J), - #muc_registered{us_host = {{U, S}, RoomHost}, - nick = Nick} - end}]. - -import(_LServer, mnesia, #muc_room{} = R) -> - mnesia:dirty_write(R); -import(_LServer, mnesia, #muc_registered{} = R) -> - mnesia:dirty_write(R); -import(_LServer, riak, #muc_room{} = R) -> - ejabberd_riak:put(R, muc_room_schema()); -import(_LServer, riak, - #muc_registered{us_host = {_, Host}, nick = Nick} = R) -> - ejabberd_riak:put(R, muc_registered_schema(), - [{'2i', [{<<"nick_host">>, {Nick, Host}}]}]); -import(_, _, _) -> - pass. +mod_doc() -> + #{desc => + [?T("This module provides support for https://xmpp.org/extensions/xep-0045.html" + "[XEP-0045: Multi-User Chat]. Users can discover existing rooms, " + "join or create them. Occupants of a room can chat in public or have private chats."), "", + ?T("The MUC service allows any Jabber ID to register a nickname, so " + "nobody else can use that nickname in any room in the MUC " + "service. To register a nickname, open the Service Discovery in " + "your XMPP client and register in the MUC service."), "", + ?T("It is also possible to register a nickname in a room, so " + "nobody else can use that nickname in that room. If a nick is " + "registered in the MUC service, that nick cannot be registered in " + "any room, and vice versa: a nick that is registered in a room " + "cannot be registered at the MUC service."), "", + ?T("This module supports clustering and load balancing. One module " + "can be started per cluster node. Rooms are distributed at " + "creation time on all available MUC module instances. The " + "multi-user chat module is clustered but the rooms themselves " + "are not clustered nor fault-tolerant: if the node managing a " + "set of rooms goes down, the rooms disappear and they will be " + "recreated on an available node on first connection attempt.")], + opts => + [{access, + #{value => ?T("AccessName"), + desc => + ?T("You can specify who is allowed to use the Multi-User Chat service. " + "By default everyone is allowed to use it.")}}, + {access_admin, + #{value => ?T("AccessName"), + desc => + ?T("This option specifies who is allowed to administrate " + "the Multi-User Chat service. The default value is 'none', " + "which means that only the room creator can administer " + "their room. The administrators can send a normal message " + "to the service JID, and it will be shown in all active " + "rooms as a service message. The administrators can send a " + "groupchat message to the JID of an active room, and the " + "message will be shown in the room as a service message.")}}, + {access_create, + #{value => ?T("AccessName"), + desc => + ?T("To configure who is allowed to create new rooms at the " + "Multi-User Chat service, this option can be used. " + "The default value is 'all', which means everyone is " + "allowed to create rooms.")}}, + {access_persistent, + #{value => ?T("AccessName"), + desc => + ?T("To configure who is allowed to modify the 'persistent' room option. " + "The default value is 'all', which means everyone is allowed to " + "modify that option.")}}, + {access_mam, + #{value => ?T("AccessName"), + desc => + ?T("To configure who is allowed to modify the 'mam' room option. " + "The default value is 'all', which means everyone is allowed to " + "modify that option.")}}, + {access_register, + #{value => ?T("AccessName"), + note => "improved in 23.10", + desc => + ?T("This option specifies who is allowed to register nickname " + "within the Multi-User Chat service and rooms. The default is 'all' for " + "backward compatibility, which means that any user is allowed " + "to register any free nick in the MUC service and in the rooms.")}}, + {db_type, + #{value => "mnesia | sql", + desc => + ?T("Same as top-level _`default_db`_ option, " + "but applied to this module only.")}}, + {ram_db_type, + #{value => "mnesia | sql", + desc => + ?T("Same as top-level _`default_ram_db`_ option, " + "but applied to this module only.")}}, + {hibernation_timeout, + #{value => "infinity | Seconds", + desc => + ?T("Timeout before hibernating the room process, expressed " + "in seconds. The default value is 'infinity'.")}}, + {history_size, + #{value => ?T("Size"), + desc => + ?T("A small history of the current discussion is sent to users " + "when they enter the room. With this option you can define the " + "number of history messages to keep and send to users joining the room. " + "The value is a non-negative integer. Setting the value to '0' disables " + "the history feature and, as a result, nothing is kept in memory. " + "The default value is '20'. This value affects all rooms on the service. " + "NOTE: modern XMPP clients rely on Message Archives (XEP-0313), so feel " + "free to disable the history feature if you're only using modern clients " + "and have _`mod_mam`_ module loaded.")}}, + {host, #{desc => ?T("Deprecated. Use 'hosts' instead.")}}, + {hosts, + #{value => ?T("[Host, ...]"), + desc => + ?T("This option defines the Jabber IDs of the service. " + "If the 'hosts' option is not specified, the only Jabber ID will " + "be the hostname of the virtual host with the prefix \"conference.\". " + "The keyword '@HOST@' is replaced with the real virtual host name.")}}, + {name, + #{value => "string()", + desc => + ?T("The value of the service name. This name is only visible in some " + "clients that support https://xmpp.org/extensions/xep-0030.html" + "[XEP-0030: Service Discovery]. The default is 'Chatrooms'.")}}, + {max_room_desc, + #{value => ?T("Number"), + desc => + ?T("This option defines the maximum number of characters that " + "Room Description can have when configuring the room. " + "The default value is 'infinity'.")}}, + {max_room_id, + #{value => ?T("Number"), + desc => + ?T("This option defines the maximum number of characters that " + "Room ID can have when creating a new room. " + "The default value is 'infinity'.")}}, + {max_room_name, + #{value => ?T("Number"), + desc => + ?T("This option defines the maximum number of characters " + "that Room Name can have when configuring the room. " + "The default value is 'infinity'.")}}, + {max_password, + #{value => ?T("Number"), + note => "added in 21.01", + desc => + ?T("This option defines the maximum number of characters " + "that Password can have when configuring the room. " + "The default value is 'infinity'.")}}, + {max_captcha_whitelist, + #{value => ?T("Number"), + note => "added in 21.01", + desc => + ?T("This option defines the maximum number of characters " + "that Captcha Whitelist can have when configuring the room. " + "The default value is 'infinity'.")}}, + {max_rooms_discoitems, + #{value => ?T("Number"), + desc => + ?T("When there are more rooms than this 'Number', " + "only the non-empty ones are returned in a Service Discovery query. " + "The default value is '100'.")}}, + {max_user_conferences, + #{value => ?T("Number"), + desc => + ?T("This option defines the maximum number of rooms that any " + "given user can join. The default value is '100'. This option " + "is used to prevent possible abuses. Note that this is a soft " + "limit: some users can sometimes join more conferences in " + "cluster configurations.")}}, + {max_users, + #{value => ?T("Number"), + desc => + ?T("This option defines at the service level, the maximum " + "number of users allowed per room. It can be lowered in " + "each room configuration but cannot be increased in " + "individual room configuration. The default value is '200'.")}}, + {max_users_admin_threshold, + #{value => ?T("Number"), + desc => + ?T("This option defines the number of service admins or room " + "owners allowed to enter the room when the maximum number " + "of allowed occupants was reached. The default limit is '5'.")}}, + {max_users_presence, + #{value => ?T("Number"), + desc => + ?T("This option defines after how many users in the room, " + "it is considered overcrowded. When a MUC room is considered " + "overcrowded, presence broadcasts are limited to reduce load, " + "traffic and excessive presence \"storm\" received by participants. " + "The default value is '1000'.")}}, + {min_message_interval, + #{value => ?T("Number"), + desc => + ?T("This option defines the minimum interval between two " + "messages send by an occupant in seconds. This option " + "is global and valid for all rooms. A decimal value can be used. " + "When this option is not defined, message rate is not limited. " + "This feature can be used to protect a MUC service from occupant " + "abuses and limit number of messages that will be broadcasted by " + "the service. A good value for this minimum message interval is '0.4' second. " + "If an occupant tries to send messages faster, an error is send back " + "explaining that the message has been discarded and describing the " + "reason why the message is not acceptable.")}}, + {min_presence_interval, + #{value => ?T("Number"), + desc => + ?T("This option defines the minimum of time between presence " + "changes coming from a given occupant in seconds. " + "This option is global and valid for all rooms. A decimal " + "value can be used. When this option is not defined, no " + "restriction is applied. This option can be used to protect " + "a MUC service for occupants abuses. If an occupant tries " + "to change its presence more often than the specified interval, " + "the presence is cached by ejabberd and only the last presence " + "is broadcasted to all occupants in the room after expiration " + "of the interval delay. Intermediate presence packets are " + "silently discarded. A good value for this option is '4' seconds.")}}, + {queue_type, + #{value => "ram | file", + desc => + ?T("Same as top-level _`queue_type`_ option, but applied to this module only.")}}, + {regexp_room_id, + #{value => "string()", + desc => + ?T("This option defines the regular expression that a Room ID " + "must satisfy to allow the room creation. The default value " + "is the empty string.")}}, + {preload_rooms, + #{value => "true | false", + desc => + ?T("Whether to load all persistent rooms in memory on startup. " + "If disabled, the room is only loaded on first participant join. " + "The default is 'true'. It makes sense to disable room preloading " + "when the number of rooms is high: this will improve server startup " + "time and memory consumption.")}}, + {room_shaper, + #{value => "none | ShaperName", + desc => + ?T("This option defines shaper for the MUC rooms. " + "The default value is 'none'.")}}, + {user_message_shaper, + #{value => "none | ShaperName", + desc => + ?T("This option defines shaper for the users messages. " + "The default value is 'none'.")}}, + {user_presence_shaper, + #{value => "none | ShaperName", + desc => + ?T("This option defines shaper for the users presences. " + "The default value is 'none'.")}}, + {vcard, + #{value => ?T("vCard"), + desc => + ?T("A custom vCard of the service that will be displayed " + "by some XMPP clients in Service Discovery. The value of " + "'vCard' is a YAML map constructed from an XML representation " + "of vCard. Since the representation has no attributes, " + "the mapping is straightforward."), + example => + ["# This XML representation of vCard:", + "# ", + "# Conferences", + "# ", + "# ", + "# Elm Street", + "# ", + "# ", + "# ", + "# is translated to:", + "vcard:", + " fn: Conferences", + " adr:", + " -", + " work: true", + " street: Elm Street"]}}, + {cleanup_affiliations_on_start, + #{value => "true | false", + note => "added in 22.05", + desc => + ?T("Remove affiliations for non-existing local users on startup. " + "The default value is 'false'.")}}, + {default_room_options, + #{value => ?T("Options"), + note => "improved in 22.05", + desc => + ?T("Define the " + "default room options. Note that the creator of a room " + "can modify the options of his room at any time using an " + "XMPP client with MUC capability. The 'Options' are:")}, + [{allow_change_subj, + #{value => "true | false", + desc => + ?T("Allow occupants to change the subject. " + "The default value is 'true'.")}}, + {allowpm, + #{value => "anyone | participants | moderators | none", + desc => + ?T("Who can send private messages. " + "The default value is 'anyone'.")}}, + {allow_query_users, + #{value => "true | false", + desc => + ?T("Occupants can send IQ queries to other occupants. " + "The default value is 'true'.")}}, + {allow_user_invites, + #{value => "true | false", + desc => + ?T("Allow occupants to send invitations. " + "The default value is 'false'.")}}, + {allow_visitor_nickchange, + #{value => "true | false", + desc => ?T("Allow visitors to change nickname. " + "The default value is 'true'.")}}, + {allow_visitor_status, + #{value => "true | false", + desc => + ?T("Allow visitors to send status text in presence updates. " + "If disallowed, the status text is stripped before broadcasting " + "the presence update to all the room occupants. " + "The default value is 'true'.")}}, + {allow_voice_requests, + #{value => "true | false", + desc => + ?T("Allow visitors in a moderated room to request voice. " + "The default value is 'true'.")}}, + {anonymous, + #{value => "true | false", + desc => + ?T("The room is anonymous: occupants don't see the real " + "JIDs of other occupants. Note that the room moderators " + "can always see the real JIDs of the occupants. " + "The default value is 'true'.")}}, + {captcha_protected, + #{value => "true | false", + desc => + ?T("When a user tries to join a room where they have no " + "affiliation (not owner, admin or member), the room " + "requires them to fill a CAPTCHA challenge (see section " + "_`basic.md#captcha|CAPTCHA`_ " + "in order to accept their join in the room. " + "The default value is 'false'.")}}, + {description, + #{value => ?T("Room Description"), + desc => + ?T("Short description of the room. " + "The default value is an empty string.")}}, + {enable_hats, + #{value => "true | false", + note => "improved in 25.xx", + desc => + ?T("Allow extended roles as defined in XEP-0317 Hats. " + "Check the _`../../tutorials/muc-hats.md|MUC Hats`_ tutorial. " + "The default value is 'false'.")}}, + {lang, + #{value => ?T("Language"), + desc => + ?T("Preferred language for the discussions in the room. " + "The language format should conform to RFC 5646. " + "There is no value by default.")}}, + {logging, + #{value => "true | false", + desc => + ?T("The public messages are logged using _`mod_muc_log`_. " + "The default value is 'false'.")}}, + {members_by_default, + #{value => "true | false", + desc => + ?T("The occupants that enter the room are participants " + "by default, so they have \"voice\". " + "The default value is 'true'.")}}, + {members_only, + #{value => "true | false", + desc => + ?T("Only members of the room can enter. " + "The default value is 'false'.")}}, + {moderated, + #{value => "true | false", + desc => + ?T("Only occupants with \"voice\" can send public messages. " + "The default value is 'true'.")}}, + {password_protected, + #{value => "true | false", + desc => + ?T("The password is required to enter the room. " + "The default value is 'false'.")}}, + {password, + #{value => ?T("Password"), + desc => + ?T("Password of the room. Implies option 'password_protected' " + "set to 'true'. There is no default value.")}}, + {persistent, + #{value => "true | false", + desc => + ?T("The room persists even if the last participant leaves. " + "The default value is 'false'.")}}, + {public, + #{value => "true | false", + desc => + ?T("The room is public in the list of the MUC service, " + "so it can be discovered. MUC admins and room participants " + "will see private rooms in Service Discovery if their XMPP " + "client supports this feature. " + "The default value is 'true'.")}}, + {public_list, + #{value => "true | false", + desc => + ?T("The list of participants is public, without requiring " + "to enter the room. The default value is 'true'.")}}, + {pubsub, + #{value => ?T("PubSub Node"), + desc => + ?T("XMPP URI of associated Publish/Subscribe node. " + "The default value is an empty string.")}}, + {vcard, + #{value => ?T("vCard"), + desc => + ?T("A custom vCard for the room. See the equivalent mod_muc option." + "The default value is an empty string.")}}, + {vcard_xupdate, + #{value => "undefined | external | AvatarHash", + desc => + ?T("Set the hash of the avatar image. " + "The default value is 'undefined'.")}}, + {voice_request_min_interval, + #{value => ?T("Number"), + desc => + ?T("Minimum interval between voice requests, in seconds. " + "The default value is '1800'.")}}, + {mam, + #{value => "true | false", + desc => + ?T("Enable message archiving. Implies mod_mam is enabled. " + "The default value is 'false'.")}}, + {allow_subscription, + #{value => "true | false", + desc => + ?T("Allow users to subscribe to room events as described in " + "_`../../developer/xmpp-clients-bots/extensions/muc-sub.md|Multi-User Chat Subscriptions`_. " + "The default value is 'false'.")}}, + {title, + #{value => ?T("Room Title"), + desc => + ?T("A human-readable title of the room. " + "There is no default value")}}, + {allow_private_messages_from_visitors, + #{value => "anyone | moderators | nobody", + desc => + ?T("Visitors can send private messages to other occupants. " + "The default value is 'anyone' which means visitors " + "can send private messages to any occupant.")}}, + {max_users, + #{value => ?T("Number"), + desc => + ?T("Maximum number of occupants in the room. " + "The default value is '200'.")}}, + {presence_broadcast, + #{value => "[Role]", + desc => + ?T("List of roles for which presence is broadcasted. " + "The list can contain one or several of: 'moderator', " + "'participant', 'visitor'. The default value is shown " + "in the example below:"), + example => + ["presence_broadcast:", + " - moderator", + " - participant", + " - visitor"]}}]}]}. diff --git a/src/mod_muc_admin.erl b/src/mod_muc_admin.erl new file mode 100644 index 000000000..9a8ab60b1 --- /dev/null +++ b/src/mod_muc_admin.erl @@ -0,0 +1,2320 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_muc_admin.erl +%%% Author : Badlop +%%% Purpose : Tools for additional MUC administration +%%% Created : 8 Sep 2007 by Badlop +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_muc_admin). +-author('badlop@process-one.net'). + +-behaviour(gen_mod). + +-export([start/2, stop/1, reload/3, depends/2, mod_doc/0, + muc_online_rooms/1, muc_online_rooms_by_regex/2, + muc_register_nick/3, muc_register_nick/4, + muc_unregister_nick/2, muc_unregister_nick/3, + create_room_with_opts/4, create_room/3, destroy_room/2, + create_rooms_file/1, destroy_rooms_file/1, + rooms_unused_list/2, rooms_unused_destroy/2, + rooms_empty_list/1, rooms_empty_destroy/1, rooms_empty_destroy_restuple/1, + get_user_rooms/2, get_user_subscriptions/2, get_room_occupants/2, + get_room_occupants_number/2, send_direct_invitation/5, + change_room_option/4, get_room_options/2, + set_room_affiliation/4, set_room_affiliation/5, get_room_affiliations/2, + get_room_affiliations_v3/2, get_room_affiliation/3, + subscribe_room/4, subscribe_room/6, + subscribe_room_many/3, subscribe_room_many_v3/4, + unsubscribe_room/2, unsubscribe_room/4, get_subscribers/2, + get_room_serverhost/1, + web_menu_main/2, web_page_main/2, + web_menu_host/3, web_page_host/3, + web_menu_hostuser/4, web_page_hostuser/4, + webadmin_muc/2, + mod_opt_type/1, mod_options/1, + get_commands_spec/0, find_hosts/1, room_diagnostics/2, + get_room_pid/2, get_room_history/2]). + +-import(ejabberd_web_admin, [make_command/4, make_command_raw_value/3, make_table/4]). + +-include("logger.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). +-include("mod_muc.hrl"). +-include("mod_muc_room.hrl"). +-include("ejabberd_http.hrl"). +-include("ejabberd_web_admin.hrl"). +-include("ejabberd_commands.hrl"). +-include("translate.hrl"). + +%%---------------------------- +%% gen_mod +%%---------------------------- + +start(_Host, _Opts) -> + {ok, [{commands, get_commands_spec()}, + {hook, webadmin_menu_main, web_menu_main, 50, global}, + {hook, webadmin_page_main, web_page_main, 50, global}, + {hook, webadmin_menu_host, web_menu_host, 50}, + {hook, webadmin_page_host, web_page_host, 50}, + {hook, webadmin_menu_hostuser, web_menu_hostuser, 50}, + {hook, webadmin_page_hostuser, web_page_hostuser, 50} + ]}. + +stop(_Host) -> + ok. + +reload(_Host, _NewOpts, _OldOpts) -> + ok. + +depends(_Host, _Opts) -> + [{mod_muc, hard}]. + +%%% +%%% Register commands +%%% + +get_commands_spec() -> + [ + #ejabberd_commands{name = muc_online_rooms, tags = [muc], + desc = "List existing rooms", + longdesc = "Ask for a specific host, or `global` to use all vhosts.", + policy = admin, + module = ?MODULE, function = muc_online_rooms, + args_desc = ["MUC service, or `global` for all"], + args_example = ["conference.example.com"], + result_desc = "List of rooms JIDs", + result_example = ["room1@conference.example.com", "room2@conference.example.com"], + args = [{service, binary}], + args_rename = [{host, service}], + result = {rooms, {list, {room, string}}}}, + #ejabberd_commands{name = muc_online_rooms_by_regex, tags = [muc], + desc = "List existing rooms filtered by regexp", + longdesc = "Ask for a specific host, or `global` to use all vhosts.", + policy = admin, + module = ?MODULE, function = muc_online_rooms_by_regex, + args_desc = ["MUC service, or `global` for all", + "Regex pattern for room name"], + args_example = ["conference.example.com", "^prefix"], + result_desc = "List of rooms with summary", + result_example = [{"room1@conference.example.com", "true", 10}, + {"room2@conference.example.com", "false", 10}], + args = [{service, binary}, {regex, binary}], + args_rename = [{host, service}], + result = {rooms, {list, {room, {tuple, + [{jid, string}, + {public, string}, + {participants, integer} + ]}}}}}, + #ejabberd_commands{name = muc_register_nick, tags = [muc], + desc = "Register a nick to a User JID in a MUC service", + module = ?MODULE, function = muc_register_nick, + args_desc = ["Nick", "User JID", "Service"], + args_example = [<<"Tim">>, <<"tim@example.org">>, <<"conference.example.org">>], + args = [{nick, binary}, {jid, binary}, {service, binary}], + args_rename = [{host, service}], + result = {res, rescode}}, + #ejabberd_commands{name = muc_register_nick, tags = [muc], + desc = "Register a nick to a User JID in a MUC service", + module = ?MODULE, function = muc_register_nick, + version = 3, + note = "updated in 24.12", + args_desc = ["nick", "user name", "user host", "MUC service"], + args_example = [<<"Tim">>, <<"tim">>, <<"example.org">>, <<"conference.example.org">>], + args = [{nick, binary}, {user, binary}, {host, binary}, {service, binary}], + args_rename = [{host, service}], + result = {res, rescode}}, + #ejabberd_commands{name = muc_unregister_nick, tags = [muc], + desc = "Unregister the nick registered by that account in the MUC service", + module = ?MODULE, function = muc_unregister_nick, + args_desc = ["User JID", "MUC service"], + args_example = [<<"tim@example.org">>, <<"conference.example.org">>], + args = [{jid, binary}, {service, binary}], + args_rename = [{host, service}], + result = {res, rescode}}, + #ejabberd_commands{name = muc_unregister_nick, tags = [muc], + desc = "Unregister the nick registered by that account in the MUC service", + module = ?MODULE, function = muc_unregister_nick, + version = 3, + note = "updated in 24.12", + args_desc = ["user name", "user host", "MUC service"], + args_example = [<<"tim">>, <<"example.org">>, <<"conference.example.org">>], + args = [{user, binary}, {host, binary}, {service, binary}], + args_rename = [{host, service}], + result = {res, rescode}}, + + #ejabberd_commands{name = create_room, tags = [muc_room], + desc = "Create a MUC room name@service in host", + module = ?MODULE, function = create_room, + args_desc = ["Room name", "MUC service", "Server host"], + args_example = ["room1", "conference.example.com", "example.com"], + args = [{room, binary}, {service, binary}, + {host, binary}], + args_rename = [{name, room}], + result = {res, rescode}}, + #ejabberd_commands{name = destroy_room, tags = [muc_room], + desc = "Destroy a MUC room", + module = ?MODULE, function = destroy_room, + args_desc = ["Room name", "MUC service"], + args_example = ["room1", "conference.example.com"], + args = [{room, binary}, {service, binary}], + args_rename = [{name, room}], + result = {res, rescode}}, + #ejabberd_commands{name = create_rooms_file, tags = [muc], + desc = "Create the rooms indicated in file", + longdesc = "Provide one room JID per line. Rooms will be created after restart.", + note = "improved in 24.12", + module = ?MODULE, function = create_rooms_file, + args_desc = ["Path to the text file with one room JID per line"], + args_example = ["/home/ejabberd/rooms.txt"], + args = [{file, string}], + result = {res, rescode}}, + #ejabberd_commands{name = create_room_with_opts, tags = [muc_room, muc_sub], + desc = "Create a MUC room name@service in host with given options", + longdesc = + "Options `affiliations` and `subscribers` are lists of tuples. " + "The tuples in the list are separated with `;` and " + "the elements in each tuple are separated with `=` " + "(until ejabberd 24.12 the separators were `,` and `:` respectively). " + "Each subscriber can have one or more nodes. " + "In summary, `affiliations` is like `Type1=JID1;Type2=JID2` " + "and `subscribers` is like `JID1=Nick1=Node1A=Node1B=Node1C;JID2=Nick2=Node2`.", + note = "modified in 25.03", + module = ?MODULE, function = create_room_with_opts, + args_desc = ["Room name", "MUC service", "Server host", "List of options"], + args_example = ["room1", "conference.example.com", "localhost", + [{"members_only","true"}, + {"affiliations", "owner=user1@localhost;member=user2@localhost"}, + {"subscribers", "user3@localhost=User3=messages=subject;user4@localhost=User4=messages"}]], + args = [{room, binary}, {service, binary}, + {host, binary}, + {options, {list, + {option, {tuple, + [{name, binary}, + {value, binary} + ]}} + }}], + args_rename = [{name, room}], + result = {res, rescode}}, + #ejabberd_commands{name = destroy_rooms_file, tags = [muc], + desc = "Destroy the rooms indicated in file", + longdesc = "Provide one room JID per line.", + module = ?MODULE, function = destroy_rooms_file, + args_desc = ["Path to the text file with one room JID per line"], + args_example = ["/home/ejabberd/rooms.txt"], + args = [{file, string}], + result = {res, rescode}}, + #ejabberd_commands{name = rooms_unused_list, tags = [muc], + desc = "List the rooms that are unused for many days in the service", + longdesc = "The room recent history is used, so it's recommended " + " to wait a few days after service start before running this." + " The MUC service argument can be `global` to get all hosts.", + module = ?MODULE, function = rooms_unused_list, + args_desc = ["MUC service, or `global` for all", "Number of days"], + args_example = ["conference.example.com", 31], + result_desc = "List of unused rooms", + result_example = ["room1@conference.example.com", "room2@conference.example.com"], + args = [{service, binary}, {days, integer}], + args_rename = [{host, service}], + result = {rooms, {list, {room, string}}}}, + #ejabberd_commands{name = rooms_unused_destroy, tags = [muc], + desc = "Destroy the rooms that are unused for many days in the service", + longdesc = "The room recent history is used, so it's recommended " + " to wait a few days after service start before running this." + " The MUC service argument can be `global` to get all hosts.", + module = ?MODULE, function = rooms_unused_destroy, + args_desc = ["MUC service, or `global` for all", "Number of days"], + args_example = ["conference.example.com", 31], + result_desc = "List of unused rooms that has been destroyed", + result_example = ["room1@conference.example.com", "room2@conference.example.com"], + args = [{service, binary}, {days, integer}], + args_rename = [{host, service}], + result = {rooms, {list, {room, string}}}}, + + #ejabberd_commands{name = rooms_empty_list, tags = [muc], + desc = "List the rooms that have no messages in archive", + longdesc = "The MUC service argument can be `global` to get all hosts.", + module = ?MODULE, function = rooms_empty_list, + args_desc = ["MUC service, or `global` for all"], + args_example = ["conference.example.com"], + result_desc = "List of empty rooms", + result_example = ["room1@conference.example.com", "room2@conference.example.com"], + args = [{service, binary}], + args_rename = [{host, service}], + result = {rooms, {list, {room, string}}}}, + #ejabberd_commands{name = rooms_empty_destroy, tags = [muc], + desc = "Destroy the rooms that have no messages in archive", + longdesc = "The MUC service argument can be `global` to get all hosts.", + module = ?MODULE, function = rooms_empty_destroy, + args_desc = ["MUC service, or `global` for all"], + args_example = ["conference.example.com"], + result_desc = "List of empty rooms that have been destroyed", + result_example = ["room1@conference.example.com", "room2@conference.example.com"], + args = [{service, binary}], + args_rename = [{host, service}], + result = {rooms, {list, {room, string}}}}, + #ejabberd_commands{name = rooms_empty_destroy, tags = [muc], + desc = "Destroy the rooms that have no messages in archive", + longdesc = "The MUC service argument can be `global` to get all hosts.", + module = ?MODULE, function = rooms_empty_destroy_restuple, + version = 2, + note = "modified in 24.06", + args_desc = ["MUC service, or `global` for all"], + args_example = ["conference.example.com"], + result_desc = "List of empty rooms that have been destroyed", + result_example = {ok, <<"Destroyed rooms: 2">>}, + args = [{service, binary}], + args_rename = [{host, service}], + result = {res, restuple}}, + + #ejabberd_commands{name = get_user_rooms, tags = [muc], + desc = "Get the list of rooms where this user is occupant", + module = ?MODULE, function = get_user_rooms, + args_desc = ["Username", "Server host"], + args_example = ["tom", "example.com"], + result_example = ["room1@conference.example.com", "room2@conference.example.com"], + args = [{user, binary}, {host, binary}], + result = {rooms, {list, {room, string}}}}, + #ejabberd_commands{name = get_user_subscriptions, tags = [muc, muc_sub], + desc = "Get the list of rooms where this user is subscribed", + note = "added in 21.04", + module = ?MODULE, function = get_user_subscriptions, + args_desc = ["Username", "Server host"], + args_example = ["tom", "example.com"], + result_example = [{"room1@conference.example.com", "Tommy", ["mucsub:config"]}], + args = [{user, binary}, {host, binary}], + result = {rooms, + {list, + {room, + {tuple, + [{roomjid, string}, + {usernick, string}, + {nodes, {list, {node, string}}} + ]}} + }}}, + + #ejabberd_commands{name = get_room_occupants, tags = [muc_room], + desc = "Get the list of occupants of a MUC room", + module = ?MODULE, function = get_room_occupants, + args_desc = ["Room name", "MUC service"], + args_example = ["room1", "conference.example.com"], + result_desc = "The list of occupants with JID, nick and affiliation", + result_example = [{"user1@example.com/psi", "User 1", "owner"}], + args = [{room, binary}, {service, binary}], + args_rename = [{name, room}], + result = {occupants, {list, + {occupant, {tuple, + [{jid, string}, + {nick, string}, + {role, string} + ]}} + }}}, + + #ejabberd_commands{name = get_room_occupants_number, tags = [muc_room], + desc = "Get the number of occupants of a MUC room", + module = ?MODULE, function = get_room_occupants_number, + args_desc = ["Room name", "MUC service"], + args_example = ["room1", "conference.example.com"], + result_desc = "Number of room occupants", + result_example = 7, + args = [{room, binary}, {service, binary}], + args_rename = [{name, room}], + result = {occupants, integer}}, + + #ejabberd_commands{name = send_direct_invitation, tags = [muc_room], + desc = "Send a direct invitation to several destinations", + 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`. " + "Users JIDs are separated with `:`.", + module = ?MODULE, function = send_direct_invitation, + args_desc = ["Room name", "MUC service", "Password, or `none`", + "Reason text, or `none`", "Users JIDs separated with `:` characters"], + args_example = [<<"room1">>, <<"conference.example.com">>, + <<>>, <<"Check this out!">>, + "user2@localhost:user3@example.com"], + args = [{room, binary}, {service, binary}, {password, binary}, + {reason, binary}, {users, binary}], + args_rename = [{name, room}], + result = {res, rescode}}, + #ejabberd_commands{name = send_direct_invitation, tags = [muc_room], + desc = "Send a direct invitation to several destinations", + 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 be set to `none`.", + module = ?MODULE, function = send_direct_invitation, + version = 1, + note = "updated in 24.02", + args_desc = ["Room name", "MUC service", "Password, or `none`", + "Reason text, or `none`", "List of users JIDs"], + args_example = [<<"room1">>, <<"conference.example.com">>, + <<>>, <<"Check this out!">>, + ["user2@localhost", "user3@example.com"]], + args = [{room, binary}, {service, binary}, {password, binary}, + {reason, binary}, {users, {list, {jid, binary}}}], + args_rename = [{name, room}], + result = {res, rescode}}, + + #ejabberd_commands{name = change_room_option, tags = [muc_room], + desc = "Change an option in a MUC room", + module = ?MODULE, function = change_room_option, + args_desc = ["Room name", "MUC service", "Option name", "Value to assign"], + args_example = ["room1", "conference.example.com", "members_only", "true"], + args = [{room, binary}, {service, binary}, + {option, binary}, {value, binary}], + args_rename = [{name, room}], + result = {res, rescode}}, + #ejabberd_commands{name = get_room_options, tags = [muc_room], + desc = "Get options from a MUC room", + module = ?MODULE, function = get_room_options, + args_desc = ["Room name", "MUC service"], + args_example = ["room1", "conference.example.com"], + result_desc = "List of room options tuples with name and value", + result_example = [{"members_only", "true"}], + args = [{room, binary}, {service, binary}], + args_rename = [{name, room}], + result = {options, {list, + {option, {tuple, + [{name, string}, + {value, string} + ]}} + }}}, + #ejabberd_commands{name = subscribe_room, tags = [muc_room, muc_sub], + desc = "Subscribe to a MUC conference", + module = ?MODULE, function = subscribe_room, + args_desc = ["User JID", "a user's nick", + "the room to subscribe", "nodes separated by commas: `,`"], + args_example = ["tom@localhost", "Tom", "room1@conference.localhost", + "urn:xmpp:mucsub:nodes:messages,urn:xmpp:mucsub:nodes:affiliations"], + result_desc = "The list of nodes that has subscribed", + result_example = ["urn:xmpp:mucsub:nodes:messages", + "urn:xmpp:mucsub:nodes:affiliations"], + args = [{user, binary}, {nick, binary}, {room, binary}, + {nodes, binary}], + result = {nodes, {list, {node, string}}}}, + #ejabberd_commands{name = subscribe_room, tags = [muc_room, muc_sub], + desc = "Subscribe to a MUC conference", + module = ?MODULE, function = subscribe_room, + version = 1, + note = "updated in 24.02", + args_desc = ["User JID", "a user's nick", + "the room to subscribe", "list of nodes"], + args_example = ["tom@localhost", "Tom", "room1@conference.localhost", + ["urn:xmpp:mucsub:nodes:messages", "urn:xmpp:mucsub:nodes:affiliations"]], + result_desc = "The list of nodes that has subscribed", + result_example = ["urn:xmpp:mucsub:nodes:messages", + "urn:xmpp:mucsub:nodes:affiliations"], + args = [{user, binary}, {nick, binary}, {room, binary}, + {nodes, {list, {node, binary}}}], + result = {nodes, {list, {node, string}}}}, + #ejabberd_commands{name = subscribe_room, tags = [muc_room, muc_sub], + desc = "Subscribe to a MUC conference", + module = ?MODULE, function = subscribe_room, + version = 3, + note = "updated in 24.12", + args_desc = ["user name", "user host", "user nick", + "room name", "MUC service", "list of nodes"], + args_example = ["tom", "localhost", "Tom", "room1", "conference.localhost", + ["urn:xmpp:mucsub:nodes:messages", "urn:xmpp:mucsub:nodes:affiliations"]], + result_desc = "The list of nodes that has subscribed", + result_example = ["urn:xmpp:mucsub:nodes:messages", + "urn:xmpp:mucsub:nodes:affiliations"], + args = [{user, binary}, {host, binary}, {nick, binary}, {room, binary}, + {service, binary}, {nodes, {list, {node, binary}}}], + result = {nodes, {list, {node, string}}}}, + #ejabberd_commands{name = subscribe_room_many, tags = [muc_room, muc_sub], + desc = "Subscribe several users to a MUC conference", + note = "added in 22.05", + longdesc = "This command accepts up to 50 users at once " + "(this is configurable with the _`mod_muc_admin`_ option " + "`subscribe_room_many_max_users`)", + 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 = subscribe_room_many, tags = [muc_room, muc_sub], + desc = "Subscribe several users to a MUC conference", + longdesc = "This command accepts up to 50 users at once " + "(this is configurable with the _`mod_muc_admin`_ option " + "`subscribe_room_many_max_users`)", + module = ?MODULE, function = subscribe_room_many, + version = 1, + note = "updated in 24.02", + 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, {list, {node, binary}}}], + result = {res, rescode}}, + #ejabberd_commands{name = subscribe_room_many, tags = [muc_room, muc_sub], + desc = "Subscribe several users to a MUC conference", + longdesc = "This command accepts up to 50 users at once " + "(this is configurable with the _`mod_muc_admin`_ option " + "`subscribe_room_many_max_users`)", + module = ?MODULE, function = subscribe_room_many_v3, + version = 3, + note = "updated in 24.12", + args_desc = ["List of tuples with users name, host and nick", + "room name", + "MUC service", + "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, + [{user, binary}, + {host, binary}, + {nick, binary} + ]}} + }}, + {room, binary}, {service, binary}, + {nodes, {list, {node, binary}}}], + result = {res, rescode}}, + #ejabberd_commands{name = unsubscribe_room, tags = [muc_room, muc_sub], + desc = "Unsubscribe from a MUC conference", + module = ?MODULE, function = unsubscribe_room, + args_desc = ["User JID", "the room to subscribe"], + args_example = ["tom@localhost", "room1@conference.localhost"], + args = [{user, binary}, {room, binary}], + result = {res, rescode}}, + #ejabberd_commands{name = unsubscribe_room, tags = [muc_room, muc_sub], + desc = "Unsubscribe from a MUC conference", + module = ?MODULE, function = unsubscribe_room, + version = 3, + note = "updated in 24.12", + args_desc = ["user name", "user host", "room name", "MUC service"], + args_example = ["tom", "localhost", "room1", "conference.localhost"], + args = [{user, binary}, {host, binary}, {room, binary}, {service, binary}], + result = {res, rescode}}, + #ejabberd_commands{name = get_subscribers, tags = [muc_room, muc_sub], + desc = "List subscribers of a MUC conference", + module = ?MODULE, function = get_subscribers, + args_desc = ["Room name", "MUC service"], + args_example = ["room1", "conference.example.com"], + result_desc = "The list of users that are subscribed to that room", + result_example = ["user2@example.com", "user3@example.com"], + args = [{room, binary}, {service, binary}], + args_rename = [{name, room}], + result = {subscribers, {list, {jid, string}}}}, + #ejabberd_commands{name = set_room_affiliation, tags = [muc_room], + desc = "Change an affiliation in a MUC room", + module = ?MODULE, function = set_room_affiliation, + args_desc = ["Room name", "MUC service", "User JID", "Affiliation to set"], + args_example = ["room1", "conference.example.com", "user2@example.com", "member"], + args = [{name, binary}, {service, binary}, + {jid, binary}, {affiliation, binary}], + result = {res, rescode}}, + #ejabberd_commands{name = set_room_affiliation, tags = [muc_room], + desc = "Change an affiliation in a MUC room", + longdesc = "If affiliation is `none`, then the affiliation is removed.", + module = ?MODULE, function = set_room_affiliation, + version = 3, + note = "updated in 24.12", + args_desc = ["room name", "MUC service", "user name", "user host", "affiliation to set"], + args_example = ["room1", "conference.example.com", "sun", "localhost", "member"], + args = [{room, binary}, {service, binary}, + {user, binary}, {host, binary}, {affiliation, binary}], + result = {res, rescode}}, + + + #ejabberd_commands{name = get_room_affiliations, tags = [muc_room], + desc = "Get the list of affiliations of a MUC room", + module = ?MODULE, function = get_room_affiliations, + args_desc = ["Room name", "MUC service"], + args_example = ["room1", "conference.example.com"], + result_desc = "The list of affiliations with username, domain, affiliation and reason", + result_example = [{"user1", "example.com", member, "member"}], + args = [{name, binary}, {service, binary}], + result = {affiliations, {list, + {affiliation, {tuple, + [{username, string}, + {domain, string}, + {affiliation, atom}, + {reason, string} + ]}} + }}}, + #ejabberd_commands{name = get_room_affiliations, tags = [muc_room], + desc = "Get the list of affiliations of a MUC room", + module = ?MODULE, function = get_room_affiliations_v3, + version = 3, + note = "updated in 24.12", + args_desc = ["Room name", "MUC service"], + args_example = ["room1", "conference.example.com"], + result_desc = "The list of affiliations with jid, affiliation and reason", + result_example = [{"user1@example.com", member, "member"}], + args = [{room, binary}, {service, binary}], + result = {affiliations, {list, + {affiliation, {tuple, + [{jid, string}, + {affiliation, atom}, + {reason, string} + ]}} + }}}, + + + #ejabberd_commands{name = get_room_affiliation, tags = [muc_room], + desc = "Get affiliation of a user in MUC room", + module = ?MODULE, function = get_room_affiliation, + args_desc = ["Room name", "MUC service", "User JID"], + args_example = ["room1", "conference.example.com", "user1@example.com"], + result_desc = "Affiliation of the user", + result_example = member, + args = [{room, binary}, {service, binary}, {jid, binary}], + args_rename = [{name, room}], + result = {affiliation, atom}}, + #ejabberd_commands{name = get_room_history, tags = [muc_room], + desc = "Get history of messages stored inside MUC room state", + note = "added in 23.04", + module = ?MODULE, function = get_room_history, + args_desc = ["Room name", "MUC service"], + args_example = ["room1", "conference.example.com"], + args = [{room, binary}, {service, binary}], + args_rename = [{name, room}], + result = {history, {list, + {entry, {tuple, + [{timestamp, string}, + {message, string}]}}}}}, + + #ejabberd_commands{name = webadmin_muc, tags = [internal], + desc = "Generate WebAdmin MUC Rooms HTML", + module = ?MODULE, function = webadmin_muc, + args = [{request, any}, {lang, binary}], + result = {res, any}} + ]. + + +%%% +%%% ejabberd commands +%%% + +muc_online_rooms(ServiceArg) -> + Hosts = find_services_validate(ServiceArg, <<"serverhost">>), + lists:flatmap( + fun(Host) -> + [<> + || {Name, _, _} <- mod_muc:get_online_rooms(Host)] + end, Hosts). + +muc_online_rooms_by_regex(ServiceArg, Regex) -> + {_, P} = re:compile(Regex), + Hosts = find_services_validate(ServiceArg, <<"serverhost">>), + lists:flatmap( + fun(Host) -> + [build_summary_room(Name, RoomHost, Pid) + || {Name, RoomHost, Pid} <- mod_muc:get_online_rooms(Host), + is_name_match(Name, P)] + end, Hosts). + +is_name_match(Name, P) -> + case re:run(Name, P) of + {match, _} -> true; + nomatch -> false + end. + +build_summary_room(Name, Host, Pid) -> + C = get_room_config(Pid), + Public = C#config.public, + S = get_room_state(Pid), + Participants = maps:size(S#state.users), + {<>, + misc:atom_to_binary(Public), + Participants + }. + +muc_register_nick(Nick, User, Host, Service) -> + muc_register_nick(Nick, makeencode(User, Host), Service). + +muc_register_nick(Nick, FromBinary, Service) -> + try {get_room_serverhost(Service), jid:decode(FromBinary)} of + {ServerHost, From} -> + Lang = <<"en">>, + case mod_muc:iq_set_register_info(ServerHost, Service, From, Nick, Lang) of + {result, undefined} -> ok; + {error, #stanza_error{reason = 'conflict'}} -> + throw({error, "Nick already registered"}); + {error, _} -> + throw({error, "Database error"}) + end + catch + error:{invalid_domain, _} -> + throw({error, "Invalid value of 'service'"}); + error:{unregistered_route, _} -> + throw({error, "Unknown host in 'service'"}); + error:{bad_jid, _} -> + throw({error, "Invalid 'jid'"}); + _ -> + throw({error, "Internal error"}) + end. + +muc_unregister_nick(User, Host, Service) -> + muc_unregister_nick(makeencode(User, Host), Service). + +muc_unregister_nick(FromBinary, Service) -> + muc_register_nick(<<"">>, FromBinary, Service). + +get_user_rooms(User, Server) -> + lists:flatmap( + fun(ServerHost) -> + case gen_mod:is_loaded(ServerHost, mod_muc) of + true -> + Rooms = mod_muc:get_online_rooms_by_user( + ServerHost, jid:nodeprep(User), jid:nodeprep(Server)), + [<> + || {Name, Host} <- Rooms]; + false -> + [] + end + end, ejabberd_option:hosts()). + +get_user_subscriptions(User, Server) -> + User2 = validate_user(User, <<"user">>), + Server2 = validate_host(Server, <<"host">>), + Services = find_services(global), + UserJid = jid:make(User2, Server2), + lists:flatmap( + fun(ServerHost) -> + {ok, Rooms} = mod_muc:get_subscribed_rooms(ServerHost, UserJid), + [{jid:encode(RoomJid), UserNick, Nodes} + || {RoomJid, UserNick, Nodes} <- Rooms] + end, Services). + +%%---------------------------- +%% Ad-hoc commands +%%---------------------------- + + +%%---------------------------- +%% Web Admin +%%---------------------------- + +%% @format-begin + +%%--------------- +%% Web Admin Menu + +web_menu_main(Acc, Lang) -> + Acc ++ [{<<"muc">>, translate:translate(Lang, ?T("Multi-User Chat"))}]. + +web_menu_host(Acc, _Host, Lang) -> + Acc ++ [{<<"muc">>, translate:translate(Lang, ?T("Multi-User Chat"))}]. + +%%--------------- +%% Web Admin Page + +web_page_main(_, #request{path = [<<"muc">>], lang = Lang} = R) -> + PageTitle = translate:translate(Lang, ?T("Multi-User Chat")), + Title = ?H1GL(PageTitle, <<"modules/#mod_muc">>, <<"mod_muc">>), + Res = [make_command(webadmin_muc, R, [{<<"request">>, R}, {<<"lang">>, Lang}], [])], + {stop, Title ++ Res}; +web_page_main(Acc, _) -> + Acc. + +web_page_host(_, Host, #request{path = [<<"muc">> | RPath], lang = Lang} = R) -> + PageTitle = translate:translate(Lang, ?T("Multi-User Chat")), + Service = find_service(Host), + Level = length(RPath), + Res = webadmin_muc_host(Host, Service, RPath, R, Lang, Level, PageTitle), + {stop, Res}; +web_page_host(Acc, _, _) -> + Acc. + +%%--------------- +%% WebAdmin MUC Host Page + +webadmin_muc_host(Host, + Service, + [<<"create-room">> | RPath], + R, + _Lang, + Level, + PageTitle) -> + Title = ?H1GL(PageTitle, <<"modules/#mod_muc">>, <<"mod_muc">>), + Breadcrumb = make_breadcrumb({service_section, Level, Service, <<"Create Room">>, RPath}), + Set = [make_command(create_room, R, [{<<"service">>, Service}, {<<"host">>, Host}], []), + make_command(create_room_with_opts, + R, + [{<<"service">>, Service}, {<<"host">>, Host}], + [])], + Title ++ Breadcrumb ++ Set; +webadmin_muc_host(_Host, + Service, + [<<"nick-register">> | RPath], + R, + _Lang, + Level, + PageTitle) -> + Title = ?H1GL(PageTitle, <<"modules/#mod_muc">>, <<"mod_muc">>), + Breadcrumb = + make_breadcrumb({service_section, Level, Service, <<"Nick Register">>, RPath}), + Set = [make_command(muc_register_nick, R, [{<<"service">>, Service}], []), + make_command(muc_unregister_nick, R, [{<<"service">>, Service}], [])], + Title ++ Breadcrumb ++ Set; +webadmin_muc_host(_Host, + Service, + [<<"rooms-empty">> | RPath], + R, + _Lang, + Level, + PageTitle) -> + Title = ?H1GL(PageTitle, <<"modules/#mod_muc">>, <<"mod_muc">>), + Breadcrumb = make_breadcrumb({service_section, Level, Service, <<"Rooms Empty">>, RPath}), + Set = [make_command(rooms_empty_list, + R, + [{<<"service">>, Service}], + [{table_options, {2, RPath}}, + {result_links, [{room, room, 3 + Level, <<"">>}]}]), + make_command(rooms_empty_destroy, R, [{<<"service">>, Service}], [])], + Title ++ Breadcrumb ++ Set; +webadmin_muc_host(_Host, + Service, + [<<"rooms-unused">> | RPath], + R, + _Lang, + Level, + PageTitle) -> + Title = ?H1GL(PageTitle, <<"modules/#mod_muc">>, <<"mod_muc">>), + Breadcrumb = + make_breadcrumb({service_section, Level, Service, <<"Rooms Unused">>, RPath}), + Set = [make_command(rooms_unused_list, + R, + [{<<"service">>, Service}], + [{result_links, [{room, room, 3 + Level, <<"">>}]}]), + make_command(rooms_unused_destroy, R, [{<<"service">>, Service}], [])], + Title ++ Breadcrumb ++ Set; +webadmin_muc_host(_Host, + Service, + [<<"rooms-regex">> | RPath], + R, + _Lang, + Level, + PageTitle) -> + Title = ?H1GL(PageTitle, <<"modules/#mod_muc">>, <<"mod_muc">>), + Breadcrumb = + make_breadcrumb({service_section, Level, Service, <<"Rooms by Regex">>, RPath}), + Set = [make_command(muc_online_rooms_by_regex, + R, + [{<<"service">>, Service}], + [{result_links, [{jid, room, 3 + Level, <<"">>}]}])], + Title ++ Breadcrumb ++ Set; +webadmin_muc_host(_Host, + Service, + [<<"rooms">>, <<"room">>, Name, <<"affiliations">> | RPath], + R, + _Lang, + Level, + PageTitle) -> + Title = ?H1GL(PageTitle, <<"modules/#mod_muc">>, <<"mod_muc">>), + Breadcrumb = + make_breadcrumb({room_section, Level, Service, <<"Affiliations">>, Name, R, RPath}), + Set = [make_command(set_room_affiliation, + R, + [{<<"room">>, Name}, {<<"service">>, Service}], + [])], + Get = [make_command(get_room_affiliations, + R, + [{<<"room">>, Name}, {<<"service">>, Service}], + [{table_options, {20, RPath}}, + {result_links, [{jid, user, 3 + Level, <<"">>}]}])], + Title ++ Breadcrumb ++ Get ++ Set; +webadmin_muc_host(_Host, + Service, + [<<"rooms">>, <<"room">>, Name, <<"history">> | RPath], + R, + _Lang, + Level, + PageTitle) -> + Title = ?H1GL(PageTitle, <<"modules/#mod_muc">>, <<"mod_muc">>), + Breadcrumb = + make_breadcrumb({room_section, Level, Service, <<"History">>, Name, R, RPath}), + Get = [make_command(get_room_history, + R, + [{<<"room">>, Name}, {<<"service">>, Service}], + [{table_options, {10, RPath}}, + {result_links, [{message, paragraph, 1, <<"">>}]}])], + Title ++ Breadcrumb ++ Get; +webadmin_muc_host(_Host, + Service, + [<<"rooms">>, <<"room">>, Name, <<"invite">> | RPath], + R, + _Lang, + Level, + PageTitle) -> + Title = ?H1GL(PageTitle, <<"modules/#mod_muc">>, <<"mod_muc">>), + Breadcrumb = + make_breadcrumb({room_section, Level, Service, <<"Invite">>, Name, R, RPath}), + Set = [make_command(send_direct_invitation, + R, + [{<<"room">>, Name}, {<<"service">>, Service}], + [])], + Title ++ Breadcrumb ++ Set; +webadmin_muc_host(_Host, + Service, + [<<"rooms">>, <<"room">>, Name, <<"occupants">> | RPath], + R, + _Lang, + Level, + PageTitle) -> + Title = ?H1GL(PageTitle, <<"modules/#mod_muc">>, <<"mod_muc">>), + Breadcrumb = + make_breadcrumb({room_section, Level, Service, <<"Occupants">>, Name, R, RPath}), + Get = [make_command(get_room_occupants, + R, + [{<<"room">>, Name}, {<<"service">>, Service}], + [{table_options, {20, RPath}}, + {result_links, [{jid, user, 3 + Level, <<"">>}]}])], + Title ++ Breadcrumb ++ Get; +webadmin_muc_host(_Host, + Service, + [<<"rooms">>, <<"room">>, Name, <<"options">> | RPath], + R, + _Lang, + Level, + PageTitle) -> + Title = ?H1GL(PageTitle, <<"modules/#mod_muc">>, <<"mod_muc">>), + Breadcrumb = + make_breadcrumb({room_section, Level, Service, <<"Options">>, Name, R, RPath}), + Set = [make_command(change_room_option, + R, + [{<<"room">>, Name}, {<<"service">>, Service}], + [])], + Get = [make_command(get_room_options, + R, + [{<<"room">>, Name}, {<<"service">>, Service}], + [])], + Title ++ Breadcrumb ++ Get ++ Set; +webadmin_muc_host(_Host, + Service, + [<<"rooms">>, <<"room">>, Name, <<"subscribers">> | RPath], + R, + _Lang, + Level, + PageTitle) -> + Title = + ?H1GLraw(PageTitle, + <<"developer/xmpp-clients-bots/extensions/muc-sub/">>, + <<"MUC/Sub Extension">>), + Breadcrumb = + make_breadcrumb({room_section, Level, Service, <<"Subscribers">>, Name, R, RPath}), + Set = [make_command(subscribe_room, + R, + [{<<"room">>, Name}, {<<"service">>, Service}], + []), + make_command(unsubscribe_room, + R, + [{<<"room">>, Name}, {<<"service">>, Service}], + [{style, danger}])], + Get = [make_command(get_subscribers, + R, + [{<<"room">>, Name}, {<<"service">>, Service}], + [{table_options, {20, RPath}}, + {result_links, [{jid, user, 3 + Level, <<"">>}]}])], + Title ++ Breadcrumb ++ Get ++ Set; +webadmin_muc_host(_Host, + Service, + [<<"rooms">>, <<"room">>, Name, <<"destroy">> | RPath], + R, + _Lang, + Level, + PageTitle) -> + Title = ?H1GL(PageTitle, <<"modules/#mod_muc">>, <<"mod_muc">>), + Breadcrumb = + make_breadcrumb({room_section, Level, Service, <<"Destroy">>, Name, R, RPath}), + Set = [make_command(destroy_room, + R, + [{<<"room">>, Name}, {<<"service">>, Service}], + [{style, danger}])], + Title ++ Breadcrumb ++ Set; +webadmin_muc_host(_Host, + Service, + [<<"rooms">>, <<"room">>, Name | _RPath], + _R, + Lang, + Level, + PageTitle) -> + Title = ?H1GL(PageTitle, <<"modules/#mod_muc">>, <<"mod_muc">>), + Breadcrumb = make_breadcrumb({room, Level, Service, Name}), + MenuItems = + [{<<"affiliations/">>, <<"Affiliations">>}, + {<<"history/">>, <<"History">>}, + {<<"invite/">>, <<"Invite">>}, + {<<"occupants/">>, <<"Occupants">>}, + {<<"options/">>, <<"Options">>}, + {<<"subscribers/">>, <<"Subscribers">>}, + {<<"destroy/">>, <<"Destroy">>}], + Get = [?XE(<<"ul">>, [?LI([?ACT(MIU, MIN)]) || {MIU, MIN} <- MenuItems])], + Title ++ Breadcrumb ++ Get; +webadmin_muc_host(_Host, Service, [<<"rooms">> | RPath], R, _Lang, Level, PageTitle) -> + Title = ?H1GL(PageTitle, <<"modules/#mod_muc">>, <<"mod_muc">>), + Breadcrumb = make_breadcrumb({service_section, Level, Service, <<"Rooms">>, RPath}), + Columns = [<<"jid">>, <<"occupants">>], + Rows = + lists:map(fun(NameService) -> + #jid{user = Name} = jid:decode(NameService), + {make_command(echo, + R, + [{<<"sentence">>, jid:encode({Name, Service, <<"">>})}], + [{only, raw_and_value}, + {result_links, [{sentence, room, 3 + Level, <<"">>}]}]), + make_command(get_room_occupants_number, + R, + [{<<"room">>, Name}, {<<"service">>, Service}], + [{only, raw_and_value}])} + end, + make_command_raw_value(muc_online_rooms, R, [{<<"service">>, Service}])), + Get = [make_command(muc_online_rooms, R, [], [{only, presentation}]), + make_command(get_room_occupants_number, R, [], [{only, presentation}]), + make_table(20, RPath, Columns, Rows)], + Title ++ Breadcrumb ++ Get; +webadmin_muc_host(_Host, Service, [], _R, Lang, _Level, PageTitle) -> + Title = ?H1GL(PageTitle, <<"modules/#mod_muc">>, <<"mod_muc">>), + Breadcrumb = make_breadcrumb({service, Service}), + MenuItems = + [{<<"create-room/">>, <<"Create Room">>}, + {<<"rooms/">>, <<"Rooms">>}, + {<<"rooms-regex/">>, <<"Rooms by Regex">>}, + {<<"rooms-empty/">>, <<"Rooms Empty">>}, + {<<"rooms-unused/">>, <<"Rooms Unused">>}, + {<<"nick-register/">>, <<"Nick Register">>}], + Get = [?XE(<<"ul">>, [?LI([?ACT(MIU, MIN)]) || {MIU, MIN} <- MenuItems])], + Title ++ Breadcrumb ++ Get; +webadmin_muc_host(_Host, _Service, _RPath, _R, _Lang, _Level, _PageTitle) -> + []. + +make_breadcrumb({service, Service}) -> + make_breadcrumb([Service]); +make_breadcrumb({service_section, Level, Service, Section, RPath}) -> + make_breadcrumb([{Level, Service}, separator, Section | RPath]); +make_breadcrumb({room, Level, Service, Name}) -> + make_breadcrumb([{Level, Service}, + separator, + {Level - 1, <<"Rooms">>}, + separator, + jid:encode({Name, Service, <<"">>})]); +make_breadcrumb({room_section, Level, Service, Section, Name, R, RPath}) -> + make_breadcrumb([{Level, Service}, + separator, + {Level - 1, <<"Rooms">>}, + separator, + make_command(echo, + R, + [{<<"sentence">>, jid:encode({Name, Service, <<"">>})}], + [{only, value}, + {result_links, [{sentence, room, 3 + Level, <<"">>}]}]), + separator, + Section + | RPath]); +make_breadcrumb(Elements) -> + lists:map(fun ({xmlel, _, _, _} = Xmlel) -> + Xmlel; + (<<"sort">>) -> + ?C(<<" +">>); + (<<"page">>) -> + ?C(<<" #">>); + (separator) -> + ?C(<<" > ">>); + (Bin) when is_binary(Bin) -> + ?C(Bin); + ({Level, Bin}) when is_integer(Level) and is_binary(Bin) -> + ?AC(binary:copy(<<"../">>, Level), Bin) + end, + Elements). + +%%--------------- +%% + +%% Returns: {normal | reverse, Integer} +get_sort_query(Q) -> + case catch get_sort_query2(Q) of + {ok, Res} -> + Res; + _ -> + {normal, 1} + end. + +get_sort_query2(Q) -> + {value, {_, Binary}} = lists:keysearch(<<"sort">>, 1, Q), + Integer = list_to_integer(string:strip(binary_to_list(Binary), right, $/)), + case Integer >= 0 of + true -> + {ok, {normal, Integer}}; + false -> + {ok, {reverse, abs(Integer)}} + end. + +webadmin_muc(#request{q = Q} = R, Lang) -> + {Sort_direction, Sort_column} = get_sort_query(Q), + Host = global, + Service = find_service(Host), + Rooms_names = get_online_rooms(Service), + Rooms_infos = build_info_rooms(Rooms_names), + Rooms_sorted = sort_rooms(Sort_direction, Sort_column, Rooms_infos), + Rooms_prepared = prepare_rooms_infos(Rooms_sorted), + TList = + lists:map(fun([RoomJid | Room]) -> + JidLink = + make_command(echo, + R, + [{<<"sentence">>, RoomJid}], + [{only, value}, + {result_links, [{sentence, room, 1, <<"">>}]}]), + ?XE(<<"tr">>, [?XE(<<"td">>, [JidLink]) | [?XC(<<"td">>, E) || E <- Room]]) + end, + Rooms_prepared), + Titles = + [?T("Jabber ID"), + ?T("# participants"), + ?T("Last message"), + ?T("Public"), + ?T("Persistent"), + ?T("Logging"), + ?T("Just created"), + ?T("Room title"), + ?T("Node")], + {Titles_TR, _} = + lists:mapfoldl(fun(Title, Num_column) -> + NCS = integer_to_binary(Num_column), + TD = ?XE(<<"td">>, + [?CT(Title), + ?C(<<" ">>), + ?AC(<<"?sort=", NCS/binary>>, <<"<">>), + ?C(<<" ">>), + ?AC(<<"?sort=-", NCS/binary>>, <<">">>)]), + {TD, Num_column + 1} + end, + 1, + Titles), + [?XCT(<<"h2">>, ?T("Chatrooms")), + ?XE(<<"table">>, + [?XE(<<"thead">>, [?XE(<<"tr">>, Titles_TR)]), ?XE(<<"tbody">>, TList)])]. + +sort_rooms(Direction, Column, Rooms) -> + Rooms2 = lists:keysort(Column, Rooms), + case Direction of + normal -> + Rooms2; + reverse -> + lists:reverse(Rooms2) + end. + +build_info_rooms(Rooms) -> + [build_info_room(Room) || Room <- Rooms]. + +build_info_room({Name, Host, _ServerHost, Pid}) -> + C = get_room_config(Pid), + Title = C#config.title, + Public = C#config.public, + Persistent = C#config.persistent, + Logging = C#config.logging, + + S = get_room_state(Pid), + Just_created = S#state.just_created, + Num_participants = maps:size(S#state.users), + Node = node(Pid), + + History = S#state.history#lqueue.queue, + Ts_last_message = + case p1_queue:is_empty(History) of + true -> + <<"A long time ago">>; + false -> + Last_message1 = get_queue_last(History), + {_, _, _, Ts_last, _} = Last_message1, + xmpp_util:encode_timestamp(Ts_last) + end, + + {<>, + Num_participants, + Ts_last_message, + Public, + Persistent, + Logging, + Just_created, + Title, + Node}. + +get_queue_last(Queue) -> + List = p1_queue:to_list(Queue), + lists:last(List). + +prepare_rooms_infos(Rooms) -> + [prepare_room_info(Room) || Room <- Rooms]. + +prepare_room_info(Room_info) -> + {NameHost, + Num_participants, + Ts_last_message, + Public, + Persistent, + Logging, + Just_created, + Title, + Node} = + Room_info, + [NameHost, + integer_to_binary(Num_participants), + Ts_last_message, + misc:atom_to_binary(Public), + misc:atom_to_binary(Persistent), + misc:atom_to_binary(Logging), + justcreated_to_binary(Just_created), + Title, + misc:atom_to_binary(Node)]. + +justcreated_to_binary(J) when is_integer(J) -> + JNow = misc:usec_to_now(J), + {{Year, Month, Day}, {Hour, Minute, Second}} = calendar:now_to_local_time(JNow), + str:format("~w-~.2.0w-~.2.0w ~.2.0w:~.2.0w:~.2.0w", + [Year, Month, Day, Hour, Minute, Second]); +justcreated_to_binary(J) when is_atom(J) -> + misc:atom_to_binary(J). + +%%-------------------- +%% Web Admin Host User + +web_menu_hostuser(Acc, _Host, _Username, _Lang) -> + Acc + ++ [{<<"muc-rooms">>, <<"MUC Rooms Online">>}, + {<<"muc-affiliations">>, <<"MUC Rooms Affiliations">>}, + {<<"muc-sub">>, <<"MUC Rooms Subscriptions">>}, + {<<"muc-register">>, <<"MUC Service Registration">>}]. + +web_page_hostuser(_, Host, User, #request{path = [<<"muc-rooms">> | RPath]} = R) -> + Level = 5 + length(RPath), + Res = ?H1GL(<<"MUC Rooms Online">>, <<"modules/#mod_muc">>, <<"mod_muc">>) + ++ [make_command(get_user_rooms, + R, + [{<<"user">>, User}, {<<"host">>, Host}], + [{table_options, {2, RPath}}, + {result_links, [{room, room, Level, <<"">>}]}])], + {stop, Res}; +web_page_hostuser(_, Host, User, #request{path = [<<"muc-affiliations">>]} = R) -> + Jid = jid:encode( + jid:make(User, Host)), + Res = ?H1GL(<<"MUC Rooms Affiliations">>, <<"modules/#mod_muc">>, <<"mod_muc">>) + ++ [make_command(set_room_affiliation, R, [{<<"jid">>, Jid}], []), + make_command(get_room_affiliation, R, [{<<"jid">>, Jid}], [])], + {stop, Res}; +web_page_hostuser(_, Host, User, #request{path = [<<"muc-sub">> | RPath]} = R) -> + Title = + ?H1GLraw(<<"MUC Rooms Subscriptions">>, + <<"developer/xmpp-clients-bots/extensions/muc-sub/">>, + <<"MUC/Sub">>), + Level = 5 + length(RPath), + Set = [make_command(subscribe_room, R, [{<<"user">>, User}, {<<"host">>, Host}], []), + make_command(unsubscribe_room, R, [{<<"user">>, User}, {<<"host">>, Host}], [])], + Get = [make_command(get_user_subscriptions, + R, + [{<<"user">>, User}, {<<"host">>, Host}], + [{table_options, {20, RPath}}, + {result_links, [{roomjid, room, Level, <<"">>}]}])], + {stop, Title ++ Get ++ Set}; +web_page_hostuser(_, Host, User, #request{path = [<<"muc-register">>]} = R) -> + Jid = jid:encode( + jid:make(User, Host)), + Res = ?H1GL(<<"MUC Service Registration">>, <<"modules/#mod_muc">>, <<"mod_muc">>) + ++ [make_command(muc_register_nick, R, [{<<"jid">>, Jid}], []), + make_command(muc_unregister_nick, R, [{<<"jid">>, Jid}], [])], + {stop, Res}; +web_page_hostuser(Acc, _, _, _) -> + Acc. +%% @format-end + +%%---------------------------- +%% Create/Delete Room +%%---------------------------- + +-spec create_room(Name::binary(), Host::binary(), ServerHost::binary()) -> ok | error. +%% @doc Create a room immediately with the default options. +create_room(Name1, Host1, ServerHost) -> + create_room_with_opts(Name1, Host1, ServerHost, []). + +create_room_with_opts(Name1, Host1, ServerHost1, CustomRoomOpts) -> + ServerHost = validate_host(ServerHost1, <<"serverhost">>), + case get_room_pid_validate(Name1, Host1, <<"service">>) of + {room_not_found, Name, Host} -> + %% Get the default room options from the muc configuration + DefRoomOpts = mod_muc_opt:default_room_options(ServerHost), + %% Change default room options as required + FormattedRoomOpts = [format_room_option(Opt, Val) || {Opt, Val}<-CustomRoomOpts], + RoomOpts = lists:ukeymerge(1, + lists:keysort(1, FormattedRoomOpts), + lists:keysort(1, DefRoomOpts)), + case mod_muc:create_room(Host, Name, RoomOpts) of + ok -> + ok; + {error, _} -> + throw({error, "Unable to start room"}) + end; + {db_failure, _Name, _Host} -> + throw({error, "Database error"}); + _ -> + throw({error, "Room already exists"}) + end. + +%% Create the room only in the database. +%% It is required to restart the MUC service for the room to appear. +muc_create_room(ServerHost, {Name, Host, _}, DefRoomOpts) -> + io:format("Creating room ~ts@~ts~n", [Name, Host]), + mod_muc:store_room(ServerHost, Host, Name, DefRoomOpts). + +-spec destroy_room(Name::binary(), Host::binary()) -> ok | {error, room_not_exists}. +%% @doc Destroy the room immediately. +%% If the room has participants, they are not notified that the room was destroyed; +%% they will notice when they try to chat and receive an error that the room doesn't exist. +destroy_room(Name1, Service1) -> + case get_room_pid_validate(Name1, Service1, <<"service">>) of + {room_not_found, _, _} -> + throw({error, "Room doesn't exists"}); + {db_failure, _Name, _Host} -> + throw({error, "Database error"}); + {Pid, _, _} -> + mod_muc_room:destroy(Pid), + ok + end. + +destroy_room({N, H, SH}) -> + io:format("Destroying room: ~ts@~ts - vhost: ~ts~n", [N, H, SH]), + destroy_room(N, H). + + +%%---------------------------- +%% Destroy Rooms in File +%%---------------------------- + +%% The format of the file is: one chatroom JID per line +%% The file encoding must be UTF-8 + +destroy_rooms_file(Filename) -> + {ok, F} = file:open(Filename, [read]), + RJID = read_room(F), + Rooms = read_rooms(F, RJID, []), + file:close(F), + [destroy_room(A) || A <- Rooms], + ok. + +read_rooms(_F, eof, L) -> + L; +read_rooms(F, no_room, L) -> + RJID2 = read_room(F), + read_rooms(F, RJID2, L); +read_rooms(F, RJID, L) -> + RJID2 = read_room(F), + read_rooms(F, RJID2, [RJID | L]). + +read_room(F) -> + case io:get_line(F, "") of + eof -> eof; + String -> + case io_lib:fread("~ts", String) of + {ok, [RoomJID], _} -> split_roomjid(list_to_binary(RoomJID)); + {error, What} -> + io:format("Parse error: what: ~p~non the line: ~p~n~n", [What, String]) + end + end. + +%% This function is quite rudimentary +%% and may not be accurate +split_roomjid(RoomJID) -> + split_roomjid2(binary:split(RoomJID, <<"@">>)). +split_roomjid2([Name, Host]) -> + [_MUC_service_name, ServerHost] = binary:split(Host, <<".">>), + {Name, Host, ServerHost}; +split_roomjid2(_) -> + no_room. + +%%---------------------------- +%% Create Rooms in File +%%---------------------------- + +create_rooms_file(Filename) -> + {ok, F} = file:open(Filename, [read]), + RJID = read_room(F), + Rooms = read_rooms(F, RJID, []), + file:close(F), + HostsDetails = get_hosts_details(Rooms), + [muc_create_room(HostsDetails, A) || A <- Rooms], + ok. + +muc_create_room(HostsDetails, {_, Host, _} = RoomTuple) -> + {_Host, ServerHost, DefRoomOpts} = get_host_details(Host, HostsDetails), + muc_create_room(ServerHost, RoomTuple, DefRoomOpts). + +get_hosts_details(Rooms) -> + Hosts = lists_uniq([Host || {_, Host, _} <- Rooms]), + lists:map(fun(H) -> + SH = get_room_serverhost(H), + {H, SH, mod_muc_opt:default_room_options(SH)} + end, Hosts). + +-ifdef(OTP_BELOW_25). +lists_uniq(List) -> + lists:usort(List). +-else. +lists_uniq(List) -> + lists:uniq(List). +-endif. + +get_host_details(Host, ServerHostsDetails) -> + lists:keyfind(Host, 1, ServerHostsDetails). + +%%--------------------------------- +%% List/Delete Unused/Empty Rooms +%%--------------------------------- + +%%--------------- +%% Control + +rooms_unused_list(Service, Days) -> + rooms_report(unused, list, Service, Days). +rooms_unused_destroy(Service, Days) -> + rooms_report(unused, destroy, Service, Days). + +rooms_empty_list(Service) -> + rooms_report(empty, list, Service, 0). +rooms_empty_destroy(Service) -> + rooms_report(empty, destroy, Service, 0). + +rooms_empty_destroy_restuple(Service) -> + DestroyedRooms = rooms_report(empty, destroy, Service, 0), + NumberBin = integer_to_binary(length(DestroyedRooms)), + {ok, <<"Destroyed rooms: ", NumberBin/binary>>}. + +rooms_report(Method, Action, Service, Days) -> + {NA, NP, RP} = muc_unused(Method, Action, Service, Days), + io:format("rooms ~ts: ~p out of ~p~n", [Method, NP, NA]), + [<> || {R, H, _SH, _P} <- RP]. + +muc_unused(Method, Action, Service, Last_allowed) -> + %% Get all required info about all existing rooms + Rooms_all = get_all_rooms(Service, erlang:system_time(microsecond) - Last_allowed*24*60*60*1000), + + %% Decide which ones pass the requirements + Rooms_pass = decide_rooms(Method, Rooms_all, Last_allowed), + + Num_rooms_all = length(Rooms_all), + Num_rooms_pass = length(Rooms_pass), + + %% Perform the desired action for matching rooms + act_on_rooms(Method, Action, Rooms_pass), + + {Num_rooms_all, Num_rooms_pass, Rooms_pass}. + +%%--------------- +%% Get info + +get_online_rooms(ServiceArg) -> + Hosts = find_services(ServiceArg), + lists:flatmap( + fun(Host) -> + ServerHost = get_room_serverhost(Host), + [{RoomName, RoomHost, ServerHost, Pid} + || {RoomName, RoomHost, Pid} <- mod_muc:get_online_rooms(Host)] + end, Hosts). + +get_all_rooms(ServiceArg, Timestamp) -> + Hosts = find_services(ServiceArg), + lists:flatmap( + fun(Host) -> + get_all_rooms2(Host, Timestamp) + end, Hosts). + +get_all_rooms2(Host, Timestamp) -> + ServerHost = ejabberd_router:host_of_route(Host), + OnlineRooms = get_online_rooms(Host), + OnlineMap = lists:foldl( + fun({Room, _, _, _}, Map) -> + Map#{Room => 1} + end, #{}, OnlineRooms), + + Mod = gen_mod:db_mod(ServerHost, mod_muc), + DbRooms = + case {erlang:function_exported(Mod, get_rooms_without_subscribers, 2), + erlang:function_exported(Mod, get_hibernated_rooms_older_than, 3)} of + {_, true} -> + Mod:get_hibernated_rooms_older_than(ServerHost, Host, Timestamp); + {true, _} -> + Mod:get_rooms_without_subscribers(ServerHost, Host); + _ -> + Mod:get_rooms(ServerHost, Host) + end, + StoredRooms = lists:filtermap( + fun(#muc_room{name_host = {Room, _}, opts = Opts}) -> + case maps:is_key(Room, OnlineMap) of + true -> + false; + _ -> + {true, {Room, Host, ServerHost, Opts}} + end + end, DbRooms), + OnlineRooms ++ StoredRooms. + +get_room_config(Room_pid) -> + {ok, R} = mod_muc_room:get_config(Room_pid), + R. + +get_room_state(Room_pid) -> + {ok, R} = mod_muc_room:get_state(Room_pid), + R. + +%%--------------- +%% Decide + +decide_rooms(Method, Rooms, Last_allowed) -> + Decide = fun(R) -> decide_room(Method, R, Last_allowed) end, + lists:filter(Decide, Rooms). + +decide_room(unused, {_Room_name, _Host, ServerHost, Room_pid}, Last_allowed) -> + NodeStartTime = erlang:system_time(microsecond) - + 1000000*(erlang:monotonic_time(second)-ejabberd_config:get_node_start()), + OnlyHibernated = case mod_muc_opt:hibernation_timeout(ServerHost) of + Value when Value < Last_allowed*24*60*60*1000 -> + true; + _ -> + false + end, + {Just_created, Num_users} = + case Room_pid of + Pid when is_pid(Pid) andalso OnlyHibernated -> + {erlang:system_time(microsecond), 0}; + Pid when is_pid(Pid) -> + case mod_muc_room:get_state(Room_pid) of + {ok, #state{just_created = JC, users = U}} -> + {JC, maps:size(U)}; + _ -> + {erlang:system_time(microsecond), 0} + end; + Opts -> + case lists:keyfind(hibernation_time, 1, Opts) of + false -> + {NodeStartTime, 0}; + {_, undefined} -> + {NodeStartTime, 0}; + {_, T} -> + {T, 0} + end + end, + Last = case Just_created of + true -> + 0; + _ -> + (erlang:system_time(microsecond) + - Just_created) div 1000000 + end, + case {Num_users, seconds_to_days(Last)} of + {0, Last_days} when (Last_days >= Last_allowed) -> + true; + _ -> + false + end; +decide_room(empty, {Room_name, Host, ServerHost, Room_pid}, _Last_allowed) -> + case gen_mod:is_loaded(ServerHost, mod_mam) of + true -> + Room_options = case Room_pid of + _ when is_pid(Room_pid) -> + get_room_options(Room_pid); + Opts -> + Opts + end, + case lists:keyfind(<<"mam">>, 1, Room_options) of + {<<"mam">>, <<"true">>} -> + mod_mam:is_empty_for_room(ServerHost, Room_name, Host); + _ -> + false + end; + _ -> + false + end. + +seconds_to_days(S) -> + S div (60*60*24). + +%%--------------- +%% Act + +act_on_rooms(Method, Action, Rooms) -> + Delete = fun(Room) -> + act_on_room(Method, Action, Room) + end, + lists:foreach(Delete, Rooms). + +act_on_room(Method, destroy, {N, H, _SH, Pid}) -> + Message = iolist_to_binary(io_lib:format( + <<"Room destroyed by rooms_~s_destroy.">>, [Method])), + case Pid of + V when is_pid(V) -> + mod_muc_room:destroy(Pid, Message); + _ -> + case get_room_pid(N, H) of + Pid2 when is_pid(Pid2) -> + mod_muc_room:destroy(Pid2, Message); + _ -> + ok + end + end; +act_on_room(_Method, list, _) -> + ok. + + +%%---------------------------- +%% Change Room Option +%%---------------------------- + +get_room_occupants(Room, Host) -> + case get_room_pid_validate(Room, Host, <<"service">>) of + {Pid, _, _} when is_pid(Pid) -> get_room_occupants(Pid); + _ -> throw({error, room_not_found}) + end. + +get_room_occupants(Pid) -> + S = get_room_state(Pid), + lists:map( + fun({_LJID, Info}) -> + {jid:encode(Info#user.jid), + Info#user.nick, + atom_to_list(Info#user.role)} + end, + maps:to_list(S#state.users)). + +get_room_occupants_number(Room, Host) -> + case get_room_pid_validate(Room, Host, <<"service">>) of + {Pid, _, _} when is_pid(Pid)-> + {ok, #{occupants_number := N}} = mod_muc_room:get_info(Pid), + N; + _ -> + throw({error, room_not_found}) + end. + +%%---------------------------- +%% Send Direct Invitation +%%---------------------------- +%% http://xmpp.org/extensions/xep-0249.html + +send_direct_invitation(RoomName, RoomService, Password, Reason, UsersString) when is_binary(UsersString) -> + UsersStrings = binary:split(UsersString, <<":">>, [global]), + send_direct_invitation(RoomName, RoomService, Password, Reason, UsersStrings); +send_direct_invitation(RoomName, RoomService, Password, Reason, UsersStrings) -> + case jid:make(RoomName, RoomService) of + error -> + throw({error, "Invalid 'roomname' or 'service'"}); + RoomJid -> + XmlEl = build_invitation(Password, Reason, RoomJid), + Users = get_users_to_invite(RoomJid, UsersStrings), + [send_direct_invitation(RoomJid, UserJid, XmlEl) + || UserJid <- Users], + ok + end. + +get_users_to_invite(RoomJid, UsersStrings) -> + OccupantsTuples = get_room_occupants(RoomJid#jid.luser, + RoomJid#jid.lserver), + OccupantsJids = try [jid:decode(JidString) + || {JidString, _Nick, _} <- OccupantsTuples] + catch _:{bad_jid, _} -> throw({error, "Malformed JID of invited user"}) + end, + lists:filtermap( + fun(UserString) -> + UserJid = jid:decode(UserString), + Val = lists:all(fun(OccupantJid) -> + UserJid#jid.luser /= OccupantJid#jid.luser + orelse UserJid#jid.lserver /= OccupantJid#jid.lserver + end, + OccupantsJids), + case {UserJid#jid.luser, Val} of + {<<>>, _} -> false; + {_, true} -> {true, UserJid}; + _ -> false + end + end, + UsersStrings). + +build_invitation(Password, Reason, RoomJid) -> + Invite = #x_conference{jid = RoomJid, + password = case Password of + <<"none">> -> <<>>; + _ -> Password + end, + reason = case Reason of + <<"none">> -> <<>>; + _ -> Reason + end}, + #message{sub_els = [Invite]}. + +send_direct_invitation(FromJid, UserJid, Msg) -> + ejabberd_router:route(xmpp:set_from_to(Msg, FromJid, UserJid)). + +%%---------------------------- +%% Change Room Option +%%---------------------------- + +-spec change_room_option(Name::binary(), Service::binary(), Option::binary(), + Value::atom() | integer() | string()) -> ok | mod_muc_log_not_enabled. +%% @doc Change an option in an existing room. +%% Requires the name of the room, the MUC service where it exists, +%% the option to change (for example title or max_users), +%% and the value to assign to the new option. +%% For example: +%% `change_room_option(<<"testroom">>, <<"conference.localhost">>, <<"title">>, <<"Test Room">>)' +change_room_option(Name, Service, OptionString, ValueString) -> + case get_room_pid_validate(Name, Service, <<"service">>) of + {room_not_found, _, _} -> + throw({error, "Room not found"}); + {db_failure, _Name, _Host} -> + throw({error, "Database error"}); + {Pid, _, _} -> + {Option, Value} = format_room_option(OptionString, ValueString), + change_room_option(Pid, Option, Value) + end. + +change_room_option(Pid, Option, Value) -> + case {Option, + gen_mod:is_loaded((get_room_state(Pid))#state.server_host, mod_muc_log)} of + {logging, false} -> + mod_muc_log_not_enabled; + _ -> + Config = get_room_config(Pid), + Config2 = change_option(Option, Value, Config), + {ok, _} = mod_muc_room:set_config(Pid, Config2), + ok + end. + +format_room_option(OptionString, ValueString) -> + Option = misc:binary_to_atom(OptionString), + Value = case Option of + title -> ValueString; + description -> ValueString; + password -> ValueString; + subject ->ValueString; + subject_author ->ValueString; + max_users -> try_convert_integer(Option, ValueString); + voice_request_min_interval -> try_convert_integer(Option, ValueString); + vcard -> ValueString; + vcard_xupdate when ValueString /= <<"undefined">>, + ValueString /= <<"external">> -> + ValueString; + lang -> ValueString; + pubsub -> ValueString; + affiliations -> + [parse_affiliation_string(Opt) || Opt <- str:tokens(ValueString, <<";,">>)]; + subscribers -> + [parse_subscription_string(Opt) || Opt <- str:tokens(ValueString, <<";,">>)]; + allow_private_messages_from_visitors when + (ValueString == <<"anyone">>) or + (ValueString == <<"moderators">>) or + (ValueString == <<"nobody">>) -> binary_to_existing_atom(ValueString, utf8); + allowpm when + (ValueString == <<"anyone">>) or + (ValueString == <<"participants">>) or + (ValueString == <<"moderators">>) or + (ValueString == <<"none">>) -> binary_to_existing_atom(ValueString, utf8); + presence_broadcast when + (ValueString == <<"participant">>) or + (ValueString == <<"moderator">>) or + (ValueString == <<"visitor">>) -> binary_to_existing_atom(ValueString, utf8); + _ when ValueString == <<"true">> -> true; + _ when ValueString == <<"false">> -> false; + _ -> throw_error(Option, ValueString) + end, + {Option, Value}. + +try_convert_integer(Option, ValueString) -> + try binary_to_integer(ValueString) of + I when is_integer(I) -> I + catch _:badarg -> + throw_error(Option, ValueString) + end. + +throw_error(O, V) -> + throw({error, "Invalid value for that option", O, V}). + +parse_affiliation_string(String) -> + {Type, JidS} = case String of + %% Old syntax + <<"owner:", Jid/binary>> -> {owner, Jid}; + <<"admin:", Jid/binary>> -> {admin, Jid}; + <<"member:", Jid/binary>> -> {member, Jid}; + <<"outcast:", Jid/binary>> -> {outcast, Jid}; + %% New syntax + <<"owner=", Jid/binary>> -> {owner, Jid}; + <<"admin=", Jid/binary>> -> {admin, Jid}; + <<"member=", Jid/binary>> -> {member, Jid}; + <<"outcast=", Jid/binary>> -> {outcast, Jid}; + _ -> throw({error, "Invalid 'affiliation'"}) + end, + try jid:decode(JidS) of + #jid{luser = U, lserver = S, lresource = R} -> + {{U, S, R}, {Type, <<>>}} + catch _:{bad_jid, _} -> + throw({error, "Malformed JID in affiliation"}) + end. + +parse_subscription_string(String) -> + case str:tokens(String, <<"=:">>) of + [_] -> + throw({error, "Invalid 'subscribers' - missing nick"}); + [_, _] -> + throw({error, "Invalid 'subscribers' - missing nodes"}); + [JidS, Nick | Nodes] -> + Nodes2 = parse_nodes(Nodes, []), + try jid:decode(JidS) of + Jid -> + {Jid, Nick, Nodes2} + catch _:{bad_jid, _} -> + throw({error, "Malformed JID in 'subscribers'"}) + end + end. + +parse_nodes([], Acc) -> + Acc; +parse_nodes([<<"presence">> | Rest], Acc) -> + parse_nodes(Rest, [?NS_MUCSUB_NODES_PRESENCE | Acc]); +parse_nodes([<<"messages">> | Rest], Acc) -> + parse_nodes(Rest, [?NS_MUCSUB_NODES_MESSAGES | Acc]); +parse_nodes([<<"participants">> | Rest], Acc) -> + parse_nodes(Rest, [?NS_MUCSUB_NODES_PARTICIPANTS | Acc]); +parse_nodes([<<"affiliations">> | Rest], Acc) -> + parse_nodes(Rest, [?NS_MUCSUB_NODES_AFFILIATIONS | Acc]); +parse_nodes([<<"subject">> | Rest], Acc) -> + parse_nodes(Rest, [?NS_MUCSUB_NODES_SUBJECT | Acc]); +parse_nodes([<<"config">> | Rest], Acc) -> + parse_nodes(Rest, [?NS_MUCSUB_NODES_CONFIG | Acc]); +parse_nodes([<<"system">> | Rest], Acc) -> + parse_nodes(Rest, [?NS_MUCSUB_NODES_SYSTEM | Acc]); +parse_nodes([<<"subscribers">> | Rest], Acc) -> + parse_nodes(Rest, [?NS_MUCSUB_NODES_SUBSCRIBERS | Acc]); +parse_nodes(_, _) -> + throw({error, "Invalid 'subscribers' - unknown node name used"}). + +-spec get_room_pid_validate(binary(), binary(), binary()) -> + {pid() | room_not_found | db_failure, binary(), binary()}. +get_room_pid_validate(Name, Service, ServiceArg) -> + Name2 = validate_room(Name), + {ServerHost, Service2} = validate_muc2(Service, ServiceArg), + case mod_muc:unhibernate_room(ServerHost, Service2, Name2) of + {error, notfound} -> + {room_not_found, Name2, Service2}; + {error, db_failure} -> + {db_failure, Name2, Service2}; + {ok, Pid} -> + {Pid, Name2, Service2} + end. + +%% @doc Get the Pid of an existing MUC room, or 'room_not_found'. +-spec get_room_pid(binary(), binary()) -> pid() | room_not_found | db_failure | invalid_service | unknown_service. +get_room_pid(Name, Service) -> + try get_room_serverhost(Service) of + ServerHost -> + case mod_muc:unhibernate_room(ServerHost, Service, Name) of + {error, notfound} -> + room_not_found; + {error, db_failure} -> + db_failure; + {ok, Pid} -> + Pid + end + catch + error:{invalid_domain, _} -> + invalid_service; + error:{unregistered_route, _} -> + unknown_service + end. + +room_diagnostics(Name, Service) -> + try get_room_serverhost(Service) of + ServerHost -> + RMod = gen_mod:ram_db_mod(ServerHost, mod_muc), + case RMod:find_online_room(ServerHost, Name, Service) of + error -> + room_hibernated; + {ok, Pid} -> + case rpc:pinfo(Pid, [current_stacktrace, message_queue_len, messages]) of + [{_, R}, {_, QL}, {_, Q}] -> + #{stacktrace => R, queue_size => QL, queue => lists:sublist(Q, 10)}; + _ -> + unable_to_probe_process + end + end + catch + error:{invalid_domain, _} -> + invalid_service; + error:{unregistered_route, _} -> + unknown_service + end. + +%% It is required to put explicitly all the options because +%% the record elements are replaced at compile time. +%% So, this can't be parametrized. +change_option(Option, Value, Config) -> + case Option of + allow_change_subj -> Config#config{allow_change_subj = Value}; + allowpm -> Config#config{allowpm = Value}; + allow_private_messages_from_visitors -> Config#config{allow_private_messages_from_visitors = Value}; + allow_query_users -> Config#config{allow_query_users = Value}; + allow_subscription -> Config#config{allow_subscription = Value}; + allow_user_invites -> Config#config{allow_user_invites = Value}; + allow_visitor_nickchange -> Config#config{allow_visitor_nickchange = Value}; + allow_visitor_status -> Config#config{allow_visitor_status = Value}; + allow_voice_requests -> Config#config{allow_voice_requests = Value}; + anonymous -> Config#config{anonymous = Value}; + captcha_protected -> Config#config{captcha_protected = Value}; + description -> Config#config{description = Value}; + enable_hats -> Config#config{enable_hats = Value}; + lang -> Config#config{lang = Value}; + logging -> Config#config{logging = Value}; + mam -> Config#config{mam = Value}; + max_users -> Config#config{max_users = Value}; + members_by_default -> Config#config{members_by_default = Value}; + members_only -> Config#config{members_only = Value}; + moderated -> Config#config{moderated = Value}; + password -> Config#config{password = Value}; + password_protected -> Config#config{password_protected = Value}; + persistent -> Config#config{persistent = Value}; + presence_broadcast -> Config#config{presence_broadcast = Value}; + public -> Config#config{public = Value}; + public_list -> Config#config{public_list = Value}; + pubsub -> Config#config{pubsub = Value}; + title -> Config#config{title = Value}; + vcard -> Config#config{vcard = Value}; + vcard_xupdate -> Config#config{vcard_xupdate = Value}; + voice_request_min_interval -> Config#config{voice_request_min_interval = Value} + end. + +%%---------------------------- +%% Get Room Options +%%---------------------------- + +get_room_options(Name, Service) -> + case get_room_pid_validate(Name, Service, <<"service">>) of + {Pid, _, _} when is_pid(Pid) -> get_room_options(Pid); + _ -> [] + end. + +get_room_options(Pid) -> + Config = get_room_config(Pid), + get_options(Config). + +get_options(Config) -> + Fields = [misc:atom_to_binary(Field) || Field <- record_info(fields, config)], + [config | ValuesRaw] = tuple_to_list(Config), + Values = lists:map(fun(V) when is_atom(V) -> misc:atom_to_binary(V); + (V) when is_integer(V) -> integer_to_binary(V); + (V) when is_tuple(V); is_list(V) -> list_to_binary(hd(io_lib:format("~w", [V]))); + (V) -> V end, ValuesRaw), + lists:zip(Fields, Values). + +%%---------------------------- +%% Get Room Affiliations +%%---------------------------- + +%% @spec(Name::binary(), Service::binary()) -> +%% [{Username::string(), Domain::string(), Role::string(), Reason::string()}] +%% @doc Get the affiliations of the room Name@Service. +get_room_affiliations(Name, Service) -> + case get_room_pid_validate(Name, Service, <<"service">>) of + {Pid, _, _} when is_pid(Pid) -> + %% Get the PID of the online room, then request its state + {ok, StateData} = mod_muc_room:get_state(Pid), + Affiliations = maps:to_list(StateData#state.affiliations), + lists:map( + fun({{Uname, Domain, _Res}, {Aff, Reason}}) when is_atom(Aff)-> + {Uname, Domain, Aff, Reason}; + ({{Uname, Domain, _Res}, Aff}) when is_atom(Aff)-> + {Uname, Domain, Aff, <<>>} + end, Affiliations); + {db_failure, _Name, _Host} -> + throw({error, "Database error"}); + _ -> + throw({error, "The room does not exist."}) + end. + +%% @spec(Name::binary(), Service::binary()) -> +%% [{JID::string(), Role::string(), Reason::string()}] +%% @doc Get the affiliations of the room Name@Service. +get_room_affiliations_v3(Name, Service) -> + case get_room_pid_validate(Name, Service, <<"service">>) of + {Pid, _, _} when is_pid(Pid) -> + %% Get the PID of the online room, then request its state + {ok, StateData} = mod_muc_room:get_state(Pid), + Affiliations = maps:to_list(StateData#state.affiliations), + lists:map( + fun({{Uname, Domain, _Res}, {Aff, Reason}}) when is_atom(Aff)-> + Jid = makeencode(Uname, Domain), + {Jid, Aff, Reason}; + ({{Uname, Domain, _Res}, Aff}) when is_atom(Aff)-> + Jid = makeencode(Uname, Domain), + {Jid, Aff, <<>>} + end, Affiliations); + {db_failure, _Name, _Host} -> + throw({error, "Database error"}); + _ -> + throw({error, "The room does not exist."}) + end. + +get_room_history(Name, Service) -> + case get_room_pid_validate(Name, Service, <<"service">>) of + {Pid, _, _} when is_pid(Pid) -> + case mod_muc_room:get_state(Pid) of + {ok, StateData} -> + History = p1_queue:to_list((StateData#state.history)#lqueue.queue), + lists:map( + fun({_Nick, Packet, _HaveSubject, TimeStamp, _Size}) -> + {xmpp_util:encode_timestamp(TimeStamp), + ejabberd_web_admin:pretty_print_xml(xmpp:encode(Packet))} + end, History); + _ -> + throw({error, "Unable to fetch room state."}) + end; + {db_failure, _Name, _Host} -> + throw({error, "Database error"}); + _ -> + throw({error, "The room does not exist."}) + end. + +%%---------------------------- +%% Get Room Affiliation +%%---------------------------- + +%% @spec(Name::binary(), Service::binary(), JID::binary()) -> +%% {Affiliation::string()} +%% @doc Get affiliation of a user in the room Name@Service. + +get_room_affiliation(Name, Service, JID) -> + case get_room_pid_validate(Name, Service, <<"service">>) of + {Pid, _, _} when is_pid(Pid) -> + %% Get the PID of the online room, then request its state + {ok, StateData} = mod_muc_room:get_state(Pid), + UserJID = jid:decode(JID), + mod_muc_room:get_affiliation(UserJID, StateData); + {db_failure, _Name, _Host} -> + throw({error, "Database error"}); + _ -> + throw({error, "The room does not exist."}) + end. + +%%---------------------------- +%% Change Room Affiliation +%%---------------------------- + +set_room_affiliation(Name, Service, User, Host, AffiliationString) -> + set_room_affiliation(Name, Service, makeencode(User, Host), AffiliationString). + +%% @spec(Name, Service, JID, AffiliationString) -> ok | {error, Error} +%% Name = binary() +%% Service = binary() +%% JID = binary() +%% AffiliationString = "outcast" | "none" | "member" | "admin" | "owner" +%% @doc Set the affiliation of JID in the room Name@Service. +%% If the affiliation is 'none', the action is to remove, +%% In any other case the action will be to create the affiliation. +set_room_affiliation(Name, Service, JID, AffiliationString) -> + Affiliation = case AffiliationString of + <<"outcast">> -> outcast; + <<"none">> -> none; + <<"member">> -> member; + <<"admin">> -> admin; + <<"owner">> -> owner; + _ -> + throw({error, "Invalid affiliation"}) + end, + case get_room_pid_validate(Name, Service, <<"service">>) of + {Pid, _, _} when is_pid(Pid) -> + %% Get the PID for the online room so we can get the state of the room + case mod_muc_room:change_item(Pid, jid:decode(JID), affiliation, Affiliation, <<"">>) of + {ok, _} -> + ok; + {error, notfound} -> + throw({error, "Room doesn't exists"}); + {error, _} -> + throw({error, "Unable to perform change"}) + end; + {db_failure, _Name, _Host} -> + throw({error, "Database error"}); + _ -> + throw({error, "Room doesn't exists"}) + end. + +%%% +%%% MUC Subscription +%%% + +subscribe_room(Username, Host, Nick, Name, Service, Nodes) -> + subscribe_room(makeencode(Username, Host), Nick, + makeencode(Name, Service), Nodes). + +subscribe_room(_User, Nick, _Room, _Nodes) when Nick == <<"">> -> + throw({error, "Nickname must be set"}); +subscribe_room(User, Nick, Room, Nodes) when is_binary(Nodes) -> + NodeList = re:split(Nodes, "\\h*,\\h*"), + subscribe_room(User, Nick, Room, NodeList); +subscribe_room(User, Nick, Room, NodeList) -> + try jid:decode(Room) of + #jid{luser = Name, lserver = Host} when Name /= <<"">> -> + try jid:decode(User) of + UserJID1 -> + UserJID = jid:replace_resource(UserJID1, <<"modmucadmin">>), + case get_room_pid_validate(Name, Host, <<"service">>) of + {Pid, _, _} when is_pid(Pid) -> + case mod_muc_room:subscribe( + Pid, UserJID, Nick, NodeList) of + {ok, SubscribedNodes} -> + SubscribedNodes; + {error, Reason} -> + throw({error, binary_to_list(Reason)}) + end; + {db_failure, _Name, _Host} -> + throw({error, "Database error"}); + _ -> + throw({error, "The room does not exist"}) + end + catch _:{bad_jid, _} -> + throw({error, "Malformed user JID"}) + end; + _ -> + throw({error, "Malformed room JID"}) + catch _:{bad_jid, _} -> + throw({error, "Malformed room JID"}) + end. + +subscribe_room_many_v3(List, Name, Service, Nodes) -> + List2 = [{makeencode(User, Host), Nick} || {User, Host, Nick} <- List], + subscribe_room_many(List2, makeencode(Name, Service), Nodes). + +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, Host, Name, Service) -> + unsubscribe_room(makeencode(User, Host), + makeencode(Name, Service)). + +unsubscribe_room(User, Room) -> + try jid:decode(Room) of + #jid{luser = Name, lserver = Host} when Name /= <<"">> -> + try jid:decode(User) of + UserJID -> + case get_room_pid_validate(Name, Host, <<"service">>) of + {Pid, _, _} when is_pid(Pid) -> + case mod_muc_room:unsubscribe(Pid, UserJID) of + ok -> + ok; + {error, Reason} -> + throw({error, binary_to_list(Reason)}) + end; + {db_failure, _Name, _Host} -> + throw({error, "Database error"}); + _ -> + throw({error, "The room does not exist"}) + end + catch _:{bad_jid, _} -> + throw({error, "Malformed user JID"}) + end; + _ -> + throw({error, "Malformed room JID"}) + catch _:{bad_jid, _} -> + throw({error, "Malformed room JID"}) + end. + +get_subscribers(Name, Host) -> + case get_room_pid_validate(Name, Host, <<"service">>) of + {Pid, _, _} when is_pid(Pid) -> + {ok, JIDList} = mod_muc_room:get_subscribers(Pid), + [jid:encode(jid:remove_resource(J)) || J <- JIDList]; + {db_failure, _Name, _Host} -> + throw({error, "Database error"}); + _ -> + throw({error, "The room does not exist"}) + end. + +%%---------------------------- +%% Utils +%%---------------------------- + +makeencode(User, Host) -> + jid:encode(jid:make(User, Host)). + +-spec validate_host(Name :: binary(), ArgName::binary()) -> binary(). +validate_host(Name, ArgName) -> + case jid:nameprep(Name) of + error -> + throw({error, <<"Invalid value of '",ArgName/binary,"'">>}); + Name2 -> + case lists:member(Name2, ejabberd_option:hosts()) of + false -> + throw({error, <<"Unknown host passed in '",ArgName/binary,"'">>}); + _ -> + Name2 + end + end. + +-spec validate_user(Name :: binary(), ArgName::binary()) -> binary(). +validate_user(Name, ArgName) -> + case jid:nodeprep(Name) of + error -> + throw({error, <<"Invalid value of '",ArgName/binary,"'">>}); + Name2 -> + Name2 + end. + +-spec validate_muc(Name :: binary(), ArgName::binary()) -> binary(). +validate_muc(Name, ArgName) -> + case jid:nameprep(Name) of + error -> + throw({error, <<"Invalid value of '",ArgName/binary,"'">>}); + Name2 -> + try get_room_serverhost(Name2) of + _ -> Name2 + catch + error:{invalid_domain, _} -> + throw({error, <<"Unknown host passed in '",ArgName/binary,"'">>}); + error:{unregistered_route, _} -> + throw({error, <<"Unknown host passed in '",ArgName/binary,"'">>}) + end + end. + +-spec validate_muc2(Name :: binary(), ArgName::binary()) -> {binary(), binary()}. +validate_muc2(Name, ArgName) -> + case jid:nameprep(Name) of + error -> + throw({error, <<"Invalid value of '",ArgName/binary,"'">>}); + Name2 -> + try get_room_serverhost(Name2) of + Host -> {Host, Name2} + catch + error:{invalid_domain, _} -> + throw({error, <<"Unknown host passed in '",ArgName/binary,"'">>}); + error:{unregistered_route, _} -> + throw({error, <<"Unknown host passed in '",ArgName/binary,"'">>}) + end + end. + +-spec validate_room(Name :: binary()) -> binary(). +validate_room(Name) -> + case jid:nodeprep(Name) of + error -> + throw({error, <<"Invalid value of room name">>}); + Name2 -> + Name2 + end. + +find_service(global) -> + global; +find_service(ServerHost) -> + hd(gen_mod:get_module_opt_hosts(ServerHost, mod_muc)). + +find_services_validate(Global, _Name) when Global == global; + Global == <<"global">> -> + find_services(Global); +find_services_validate(Service, Name) -> + Service2 = validate_muc(Service, Name), + find_services(Service2). + +find_services(Global) when Global == global; + Global == <<"global">> -> + lists:flatmap( + fun(ServerHost) -> + case gen_mod:is_loaded(ServerHost, mod_muc) of + true -> + [find_service(ServerHost)]; + false -> + [] + end + end, ejabberd_option:hosts()); +find_services(Service) when is_binary(Service) -> + [Service]. + +get_room_serverhost(Service) when is_binary(Service) -> + ejabberd_router:host_of_route(Service). + +find_host(ServerHost) -> + hd(gen_mod:get_module_opt_hosts(ServerHost, mod_muc)). + +find_hosts(Global) when Global == global; + Global == <<"global">> -> + lists:flatmap( + fun(ServerHost) -> + case gen_mod:is_loaded(ServerHost, mod_muc) of + true -> + [find_host(ServerHost)]; + false -> + [] + end + end, ejabberd_option:hosts()); +find_hosts(ServerHost) -> + case gen_mod:is_loaded(ServerHost, mod_muc) of + true -> + [find_host(ServerHost)]; + false -> + [] + end. + +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`_.")], + opts => + [{subscribe_room_many_max_users, + #{value => ?T("Number"), + note => "added in 22.05", + desc => + ?T("How many users can be subscribed to a room at once using " + "the _`subscribe_room_many`_ API. " + "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_log.erl b/src/mod_muc_log.erl index 626cf745e..57a975b0b 100644 --- a/src/mod_muc_log.erl +++ b/src/mod_muc_log.erl @@ -1,11 +1,11 @@ %%%---------------------------------------------------------------------- %%% File : mod_muc_log.erl -%%% Author : Badlop@process-one.net +%%% Author : Badlop %%% Purpose : MUC room logging %%% Created : 12 Mar 2006 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -25,6 +25,8 @@ -module(mod_muc_log). +-protocol({xep, 334, '0.2', '15.09', "complete", ""}). + -author('badlop@process-one.net'). -behaviour(gen_server). @@ -32,26 +34,18 @@ -behaviour(gen_mod). %% API --export([start_link/2, start/2, stop/1, transform_module_options/1, - check_access_log/2, add_to_log/5]). +-export([start/2, stop/1, reload/3, get_url/2, + check_access_log/3, add_to_log/5]). -%% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, - handle_info/2, terminate/2, code_change/3]). + handle_info/2, terminate/2, code_change/3, + mod_opt_type/1, mod_options/1, depends/2, mod_doc/0]). --include("ejabberd.hrl"). -include("logger.hrl"). - --include("jlib.hrl"). - +-include_lib("xmpp/include/xmpp.hrl"). -include("mod_muc_room.hrl"). +-include("translate.hrl"). -%% Copied from mod_muc/mod_muc.erl --record(muc_online_room, {name_host = {<<>>, <<>>} :: {binary(), binary()}, - pid = self() :: pid()}). - --define(T(Text), translate:translate(Lang, Text)). --define(PROCNAME, ejabberd_mod_muc_log). -record(room, {jid, title, subject, subject_author, config}). -define(PLAINTEXT_CO, <<"ZZCZZ">>). @@ -74,35 +68,23 @@ %%==================================================================== %% API %%==================================================================== -%%-------------------------------------------------------------------- -%% Function: start_link() -> {ok,Pid} | ignore | {error,Error} -%% Description: Starts the server -%%-------------------------------------------------------------------- -start_link(Host, Opts) -> - Proc = gen_mod:get_module_proc(Host, ?PROCNAME), - gen_server:start_link({local, Proc}, ?MODULE, [Host, Opts], []). - start(Host, Opts) -> - Proc = gen_mod:get_module_proc(Host, ?PROCNAME), - ChildSpec = - {Proc, - {?MODULE, start_link, [Host, Opts]}, - temporary, - 1000, - worker, - [?MODULE]}, - supervisor:start_child(ejabberd_sup, ChildSpec). + gen_mod:start_child(?MODULE, Host, Opts). stop(Host) -> - Proc = gen_mod:get_module_proc(Host, ?PROCNAME), - gen_server:call(Proc, stop), - supervisor:delete_child(ejabberd_sup, Proc). + gen_mod:stop_child(?MODULE, Host). + +reload(Host, NewOpts, _OldOpts) -> + Proc = get_proc_name(Host), + gen_server:cast(Proc, {reload, NewOpts}). add_to_log(Host, Type, Data, Room, Opts) -> gen_server:cast(get_proc_name(Host), {add_to_log, Type, Data, Room, Opts}). -check_access_log(Host, From) -> +check_access_log(allow, _Host, _From) -> + allow; +check_access_log(_Acc, Host, From) -> case catch gen_server:call(get_proc_name(Host), {check_access_log, Host, From}) of @@ -110,151 +92,101 @@ check_access_log(Host, From) -> Res -> Res end. -transform_module_options(Opts) -> - lists:map( - fun({top_link, {S1, S2}}) -> - {top_link, [{S1, S2}]}; - (Opt) -> - Opt - end, Opts). +-spec get_url(any(), #state{}) -> {ok, binary()} | error. +get_url({ok, _} = Acc, _State) -> + Acc; +get_url(_Acc, #state{room = Room, host = Host, server_host = ServerHost}) -> + try mod_muc_log_opt:url(ServerHost) of + undefined -> error; + URL -> + case mod_muc_log_opt:dirname(ServerHost) of + room_jid -> + {ok, <>}; + room_name -> + {ok, <>} + end + catch + error:{module_not_loaded, _, _} -> + error + end. + +depends(_Host, _Opts) -> + [{mod_muc, hard}]. %%==================================================================== %% gen_server callbacks %%==================================================================== +init([Host|_]) -> + process_flag(trap_exit, true), + Opts = gen_mod:get_module_opts(Host, ?MODULE), + ejabberd_hooks:add(muc_log_add, Host, ?MODULE, add_to_log, 100), + ejabberd_hooks:add(muc_log_check_access_log, Host, ?MODULE, check_access_log, 100), + ejabberd_hooks:add(muc_log_get_url, Host, ?MODULE, get_url, 100), + {ok, init_state(Host, Opts)}. -%%-------------------------------------------------------------------- -%% Function: init(Args) -> {ok, State} | -%% {ok, State, Timeout} | -%% ignore | -%% {stop, Reason} -%% Description: Initiates the server -%%-------------------------------------------------------------------- -init([Host, Opts]) -> - OutDir = gen_mod:get_opt(outdir, Opts, - fun iolist_to_binary/1, - <<"www/muc">>), - DirType = gen_mod:get_opt(dirtype, Opts, - fun(subdirs) -> subdirs; - (plain) -> plain - end, subdirs), - DirName = gen_mod:get_opt(dirname, Opts, - fun(room_jid) -> room_jid; - (room_name) -> room_name - end, room_jid), - FileFormat = gen_mod:get_opt(file_format, Opts, - fun(html) -> html; - (plaintext) -> plaintext - end, html), - FilePermissions = gen_mod:get_opt(file_permissions, Opts, - fun(SubOpts) -> - F = fun({mode, Mode}, {_M, G}) -> - {Mode, G}; - ({group, Group}, {M, _G}) -> - {M, Group} - end, - lists:foldl(F, {644, 33}, SubOpts) - end, {644, 33}), - CSSFile = gen_mod:get_opt(cssfile, Opts, - fun iolist_to_binary/1, - false), - AccessLog = gen_mod:get_opt(access_log, Opts, - fun(A) when is_atom(A) -> A end, - muc_admin), - Timezone = gen_mod:get_opt(timezone, Opts, - fun(local) -> local; - (universal) -> universal - end, local), - Top_link = gen_mod:get_opt(top_link, Opts, - fun([{S1, S2}]) -> - {iolist_to_binary(S1), - iolist_to_binary(S2)} - end, {<<"/">>, <<"Home">>}), - NoFollow = gen_mod:get_opt(spam_prevention, Opts, - fun(B) when is_boolean(B) -> B end, - true), - Lang = ejabberd_config:get_option( - {language, Host}, - fun iolist_to_binary/1, - ?MYLANG), - {ok, - #logstate{host = Host, out_dir = OutDir, - dir_type = DirType, dir_name = DirName, - file_format = FileFormat, file_permissions = FilePermissions, css_file = CSSFile, - access = AccessLog, lang = Lang, timezone = Timezone, - spam_prevention = NoFollow, top_link = Top_link}}. - -%%-------------------------------------------------------------------- -%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | -%% {reply, Reply, State, Timeout} | -%% {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, Reply, State} | -%% {stop, Reason, State} -%% Description: Handling call messages -%%-------------------------------------------------------------------- handle_call({check_access_log, ServerHost, FromJID}, _From, State) -> Reply = acl:match_rule(ServerHost, State#logstate.access, FromJID), {reply, Reply, State}; handle_call(stop, _From, State) -> {stop, normal, ok, State}. -%%-------------------------------------------------------------------- -%% Function: handle_cast(Msg, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} -%% Description: Handling cast messages -%%-------------------------------------------------------------------- +handle_cast({reload, Opts}, #logstate{host = Host}) -> + {noreply, init_state(Host, Opts)}; handle_cast({add_to_log, Type, Data, Room, Opts}, State) -> case catch add_to_log2(Type, Data, Room, Opts, State) of {'EXIT', Reason} -> ?ERROR_MSG("~p", [Reason]); _ -> ok end, {noreply, State}; -handle_cast(_Msg, State) -> {noreply, State}. +handle_cast(Msg, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Msg]), + {noreply, State}. -%%-------------------------------------------------------------------- -%% Function: handle_info(Info, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} -%% Description: Handling all non call/cast messages -%%-------------------------------------------------------------------- handle_info(_Info, State) -> {noreply, State}. -%%-------------------------------------------------------------------- -%% Function: terminate(Reason, State) -> void() -%% Description: This function is called by a gen_server when it is about to -%% terminate. It should be the opposite of Module:init/1 and do any necessary -%% cleaning up. When it returns, the gen_server terminates with Reason. -%% The return value is ignored. -%%-------------------------------------------------------------------- -terminate(_Reason, _State) -> ok. +terminate(_Reason, #state{host = Host}) -> + ejabberd_hooks:delete(muc_log_add, Host, ?MODULE, add_to_log, 100), + ejabberd_hooks:delete(muc_log_check_access_log, Host, ?MODULE, check_access_log, 100), + ejabberd_hooks:delete(muc_log_get_url, Host, ?MODULE, get_url, 100), + ok. -%%-------------------------------------------------------------------- -%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} -%% Description: Convert process state when code is changed -%%-------------------------------------------------------------------- code_change(_OldVsn, State, _Extra) -> {ok, State}. %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- +init_state(Host, Opts) -> + OutDir = mod_muc_log_opt:outdir(Opts), + DirType = mod_muc_log_opt:dirtype(Opts), + DirName = mod_muc_log_opt:dirname(Opts), + FileFormat = mod_muc_log_opt:file_format(Opts), + FilePermissions = mod_muc_log_opt:file_permissions(Opts), + CSSFile = mod_muc_log_opt:cssfile(Opts), + AccessLog = mod_muc_log_opt:access_log(Opts), + Timezone = mod_muc_log_opt:timezone(Opts), + Top_link = mod_muc_log_opt:top_link(Opts), + NoFollow = mod_muc_log_opt:spam_prevention(Opts), + Lang = ejabberd_option:language(Host), + #logstate{host = Host, out_dir = OutDir, + dir_type = DirType, dir_name = DirName, + file_format = FileFormat, css_file = CSSFile, + file_permissions = FilePermissions, + access = AccessLog, lang = Lang, timezone = Timezone, + spam_prevention = NoFollow, top_link = Top_link}. + add_to_log2(text, {Nick, Packet}, Room, Opts, State) -> - case {xml:get_subtag(Packet, <<"no-store">>), - xml:get_subtag(Packet, <<"no-permanent-store">>)} - of - {false, false} -> - case {xml:get_subtag(Packet, <<"subject">>), - xml:get_subtag(Packet, <<"body">>)} - of - {false, false} -> ok; - {false, SubEl} -> - Message = {body, xml:get_tag_cdata(SubEl)}, - add_message_to_log(Nick, Message, Room, Opts, State); - {SubEl, _} -> - Message = {subject, xml:get_tag_cdata(SubEl)}, - add_message_to_log(Nick, Message, Room, Opts, State) - end; - {_, _} -> ok + case has_no_permanent_store_hint(Packet) of + false -> + case {Packet#message.subject, Packet#message.body} of + {[], []} -> ok; + {[], Body} -> + Message = {body, xmpp:get_text(Body)}, + add_message_to_log(Nick, Message, Room, Opts, State); + {Subj, _} -> + Message = {subject, xmpp:get_text(Subj)}, + add_message_to_log(Nick, Message, Room, Opts, State) + end; + true -> ok end; add_to_log2(roomconfig_change, _Occupants, Room, Opts, State) -> @@ -296,18 +228,18 @@ build_filename_string(TimeStamp, OutDir, RoomJID, {Dir, Filename, Rel} = case DirType of subdirs -> SYear = - iolist_to_binary(io_lib:format("~4..0w", + (str:format("~4..0w", [Year])), SMonth = - iolist_to_binary(io_lib:format("~2..0w", + (str:format("~2..0w", [Month])), - SDay = iolist_to_binary(io_lib:format("~2..0w", + SDay = (str:format("~2..0w", [Day])), {fjoin([SYear, SMonth]), SDay, <<"../..">>}; plain -> Date = - iolist_to_binary(io_lib:format("~4..0w-~2..0w-~2..0w", + (str:format("~4..0w-~2..0w-~2..0w", [Year, Month, Day])), @@ -327,7 +259,7 @@ build_filename_string(TimeStamp, OutDir, RoomJID, {Fd, Fn, Fnrel}. get_room_name(RoomJID) -> - JID = jlib:string_to_jid(RoomJID), JID#jid.user. + JID = jid:decode(RoomJID), JID#jid.user. %% calculate day before get_timestamp_daydiff(TimeStamp, Daydiff) -> @@ -349,38 +281,42 @@ close_previous_log(Fn, Images_dir, FileFormat) -> write_last_lines(_, _, plaintext) -> ok; write_last_lines(F, Images_dir, _FileFormat) -> -%% list_to_integer/2 was introduced in OTP R14 fw(F, <<"">>). set_filemode(Fn, {FileMode, FileGroup}) -> - ok = file:change_mode(Fn, list_to_integer(integer_to_list(FileMode), 8)), + ok = file:change_mode(Fn, list_to_integer(integer_to_list(FileMode), 8)), ok = file:change_group(Fn, FileGroup). +htmlize_nick(Nick1, html) -> + htmlize(<<"<", Nick1/binary, ">">>, html); +htmlize_nick(Nick1, plaintext) -> + htmlize(<>, plaintext). + add_message_to_log(Nick1, Message, RoomJID, Opts, State) -> #logstate{out_dir = OutDir, dir_type = DirType, @@ -391,8 +327,8 @@ add_message_to_log(Nick1, Message, RoomJID, Opts, State, Room = get_room_info(RoomJID, Opts), Nick = htmlize(Nick1, FileFormat), - Nick2 = htmlize(<<"<", Nick1/binary, ">">>, FileFormat), - Now = now(), + Nick2 = htmlize_nick(Nick1, FileFormat), + Now = erlang:timestamp(), TimeStamp = case Timezone of local -> calendar:now_to_local_time(Now); universal -> calendar:now_to_universal_time(Now) @@ -436,8 +372,8 @@ add_message_to_log(Nick1, Message, RoomJID, Opts, RoomConfig = roomconfig_to_string(Room#room.config, Lang, FileFormat), put_room_config(F, RoomConfig, Lang, FileFormat), - io_lib:format("~s
", - [?T(<<"Chatroom configuration modified">>)]); + io_lib:format("~ts
", + [tr(Lang, ?T("Chatroom configuration modified"))]); {roomconfig_change, Occupants} -> RoomConfig = roomconfig_to_string(Room#room.config, Lang, FileFormat), @@ -445,70 +381,70 @@ add_message_to_log(Nick1, Message, RoomJID, Opts, RoomOccupants = roomoccupants_to_string(Occupants, FileFormat), put_room_occupants(F, RoomOccupants, Lang, FileFormat), - io_lib:format("~s
", - [?T(<<"Chatroom configuration modified">>)]); + io_lib:format("~ts
", + [tr(Lang, ?T("Chatroom configuration modified"))]); join -> - io_lib:format("~s ~s
", - [Nick, ?T(<<"joins the room">>)]); + io_lib:format("~ts ~ts
", + [Nick, tr(Lang, ?T("joins the room"))]); leave -> - io_lib:format("~s ~s
", - [Nick, ?T(<<"leaves the room">>)]); + io_lib:format("~ts ~ts
", + [Nick, tr(Lang, ?T("leaves the room"))]); {leave, Reason} -> - io_lib:format("~s ~s: ~s
", - [Nick, ?T(<<"leaves the room">>), + io_lib:format("~ts ~ts: ~ts
", + [Nick, tr(Lang, ?T("leaves the room")), htmlize(Reason, NoFollow, FileFormat)]); - {kickban, <<"301">>, <<"">>} -> - io_lib:format("~s ~s
", - [Nick, ?T(<<"has been banned">>)]); - {kickban, <<"301">>, Reason} -> - io_lib:format("~s ~s: ~s
", - [Nick, ?T(<<"has been banned">>), + {kickban, 301, <<"">>} -> + io_lib:format("~ts ~ts
", + [Nick, tr(Lang, ?T("has been banned"))]); + {kickban, 301, Reason} -> + io_lib:format("~ts ~ts: ~ts
", + [Nick, tr(Lang, ?T("has been banned")), htmlize(Reason, FileFormat)]); - {kickban, <<"307">>, <<"">>} -> - io_lib:format("~s ~s
", - [Nick, ?T(<<"has been kicked">>)]); - {kickban, <<"307">>, Reason} -> - io_lib:format("~s ~s: ~s
", - [Nick, ?T(<<"has been kicked">>), + {kickban, 307, <<"">>} -> + io_lib:format("~ts ~ts
", + [Nick, tr(Lang, ?T("has been kicked"))]); + {kickban, 307, Reason} -> + io_lib:format("~ts ~ts: ~ts
", + [Nick, tr(Lang, ?T("has been kicked")), htmlize(Reason, FileFormat)]); - {kickban, <<"321">>, <<"">>} -> - io_lib:format("~s ~s
", + {kickban, 321, <<"">>} -> + io_lib:format("~ts ~ts
", [Nick, - ?T(<<"has been kicked because of an affiliation " - "change">>)]); - {kickban, <<"322">>, <<"">>} -> - io_lib:format("~s ~s
", + tr(Lang, ?T("has been kicked because of an affiliation " + "change"))]); + {kickban, 322, <<"">>} -> + io_lib:format("~ts ~ts
", [Nick, - ?T(<<"has been kicked because the room has " - "been changed to members-only">>)]); - {kickban, <<"332">>, <<"">>} -> - io_lib:format("~s ~s
", + tr(Lang, ?T("has been kicked because the room has " + "been changed to members-only"))]); + {kickban, 332, <<"">>} -> + io_lib:format("~ts ~ts
", [Nick, - ?T(<<"has been kicked because of a system " - "shutdown">>)]); + tr(Lang, ?T("has been kicked because of a system " + "shutdown"))]); {nickchange, OldNick} -> - io_lib:format("~s ~s ~s
", + io_lib:format("~ts ~ts ~ts
", [htmlize(OldNick, FileFormat), - ?T(<<"is now known as">>), Nick]); + tr(Lang, ?T("is now known as")), Nick]); {subject, T} -> - io_lib:format("~s~s~s
", - [Nick, ?T(<<" has set the subject to: ">>), + io_lib:format("~ts~ts~ts
", + [Nick, tr(Lang, ?T(" has set the subject to: ")), htmlize(T, NoFollow, FileFormat)]); {body, T} -> case {ejabberd_regexp:run(T, <<"^/me ">>), Nick} of {_, <<"">>} -> - io_lib:format("~s
", + io_lib:format("~ts
", [htmlize(T, NoFollow, FileFormat)]); {match, _} -> - io_lib:format("~s ~s
", + io_lib:format("~ts ~ts
", [Nick, str:substr(htmlize(T, FileFormat), 5)]); {nomatch, _} -> - io_lib:format("~s ~s
", + io_lib:format("~ts ~ts
", [Nick2, htmlize(T, NoFollow, FileFormat)]) end; {room_existence, RoomNewExistence} -> - io_lib:format("~s
", + io_lib:format("~ts
", [get_room_existence_string(RoomNewExistence, Lang)]) end, @@ -516,15 +452,15 @@ add_message_to_log(Nick1, Message, RoomJID, Opts, STime = io_lib:format("~2..0w:~2..0w:~2..0w", [Hour, Minute, Second]), {_, _, Microsecs} = Now, - STimeUnique = io_lib:format("~s.~w", + STimeUnique = io_lib:format("~ts.~w", [STime, Microsecs]), - catch fw(F, - list_to_binary( - io_lib:format("[~s] ", + maybe_print_jl(open, F, Message, FileFormat), + fw(F, io_lib:format("[~ts] ", [STimeUnique, STimeUnique, STimeUnique, STime]) - ++ Text), + ++ Text, FileFormat), + maybe_print_jl(close, F, Message, FileFormat), file:close(F), ok. @@ -532,48 +468,48 @@ add_message_to_log(Nick1, Message, RoomJID, Opts, %% Utilities get_room_existence_string(created, Lang) -> - ?T(<<"Chatroom is created">>); + tr(Lang, ?T("Chatroom is created")); get_room_existence_string(destroyed, Lang) -> - ?T(<<"Chatroom is destroyed">>); + tr(Lang, ?T("Chatroom is destroyed")); get_room_existence_string(started, Lang) -> - ?T(<<"Chatroom is started">>); + tr(Lang, ?T("Chatroom is started")); get_room_existence_string(stopped, Lang) -> - ?T(<<"Chatroom is stopped">>). + tr(Lang, ?T("Chatroom is stopped")). get_dateweek(Date, Lang) -> Weekday = case calendar:day_of_the_week(Date) of - 1 -> ?T(<<"Monday">>); - 2 -> ?T(<<"Tuesday">>); - 3 -> ?T(<<"Wednesday">>); - 4 -> ?T(<<"Thursday">>); - 5 -> ?T(<<"Friday">>); - 6 -> ?T(<<"Saturday">>); - 7 -> ?T(<<"Sunday">>) + 1 -> tr(Lang, ?T("Monday")); + 2 -> tr(Lang, ?T("Tuesday")); + 3 -> tr(Lang, ?T("Wednesday")); + 4 -> tr(Lang, ?T("Thursday")); + 5 -> tr(Lang, ?T("Friday")); + 6 -> tr(Lang, ?T("Saturday")); + 7 -> tr(Lang, ?T("Sunday")) end, {Y, M, D} = Date, Month = case M of - 1 -> ?T(<<"January">>); - 2 -> ?T(<<"February">>); - 3 -> ?T(<<"March">>); - 4 -> ?T(<<"April">>); - 5 -> ?T(<<"May">>); - 6 -> ?T(<<"June">>); - 7 -> ?T(<<"July">>); - 8 -> ?T(<<"August">>); - 9 -> ?T(<<"September">>); - 10 -> ?T(<<"October">>); - 11 -> ?T(<<"November">>); - 12 -> ?T(<<"December">>) + 1 -> tr(Lang, ?T("January")); + 2 -> tr(Lang, ?T("February")); + 3 -> tr(Lang, ?T("March")); + 4 -> tr(Lang, ?T("April")); + 5 -> tr(Lang, ?T("May")); + 6 -> tr(Lang, ?T("June")); + 7 -> tr(Lang, ?T("July")); + 8 -> tr(Lang, ?T("August")); + 9 -> tr(Lang, ?T("September")); + 10 -> tr(Lang, ?T("October")); + 11 -> tr(Lang, ?T("November")); + 12 -> tr(Lang, ?T("December")) end, - list_to_binary( + unicode:characters_to_binary( case Lang of <<"en">> -> - io_lib:format("~s, ~s ~w, ~w", [Weekday, Month, D, Y]); + io_lib:format("~ts, ~ts ~w, ~w", [Weekday, Month, D, Y]); <<"es">> -> - io_lib:format("~s ~w de ~s de ~w", + io_lib:format("~ts ~w de ~ts de ~w", [Weekday, D, Month, Y]); _ -> - io_lib:format("~s, ~w ~s ~w", [Weekday, D, Month, Y]) + io_lib:format("~ts, ~w ~ts ~w", [Weekday, D, Month, Y]) end). make_dir_rec(Dir) -> @@ -582,188 +518,23 @@ make_dir_rec(Dir) -> %% {ok, F1}=file:open("valid-xhtml10.png", [read]). %% {ok, F1b}=file:read(F1, 1000000). %% c("../../ejabberd/src/jlib.erl"). -%% jlib:encode_base64(F1b). - -image_base64(<<"powered-by-erlang.png">>) -> - <<"iVBORw0KGgoAAAANSUhEUgAAAGUAAAAfCAYAAAD+xQNoA" - "AADN0lEQVRo3u1aP0waURz+rjGRRQ+nUyRCYmJyDPTapD" - "ARaSIbTUjt1gVSh8ZW69aBAR0cWLSxCXWp59LR1jbdqKn" - "GxoQuRZZrSYyHEVM6iZMbHewROA7u3fHvkr5vOn737vcu" - "33ffu9/vcQz+gef5Cij6CkmSGABgFEH29r5SVvqIsTEOH" - "o8HkiQxDBXEOjg9PcHc3BxuUSqsI8jR0REAUFGsCCoKFY" - "WCBAN6AxyO0Z7cyMXFb6oGqSgAsIrJut9hMQlvdNbUhKW" - "shLd3HtTF4jihShgVpRaBxKKmIGX5HL920/hz/BM2+zAm" - "pn2YioQaxnECj0BiEYcrG0Tzzc8/rfudSm02jaVSm9Vr1" - "MdG8rSKKXlJ7lHrfjouCut2IrC82BDPbe/gc+xlXez7Kx" - "Ez63H4lmIN473Rh8Si1BKhRY6aEJI8pLmbjSPN0xOnBBI" - "Lmg5RC6Lg28preKOzsNmHG8R1Bf0o7GdMucUslDy1pJLG" - "2sndVVG0lq3c9vum4zmBR1kuwiYMN5ybmCYXxQg57ThFO" - "TYznzpPO+IQi+IK+jXjg/YhuIJ+cIIHg+wQJoJ+2N3jYN" - "3Olvk4ge/IU98spne+FfGtlslm16nna8fduntfDscoVjG" - "JqUgIjz686ViFUdjP4N39x9Xq638viZVtlq2tLXKncLf5" - "ticuZSWU5XOUshJKxxKtfdtdvs4OyNb/68urKvlluYizg" - "wwu5SLK8jllu1t9ihYOlzdwdpBBKSvh+vKKzHkCj1JW3y" - "1m+hSj13WjqOiJKK0qpXKhSFxJAYBvKYaZ9TjWRu4SiWi" - "2LyDtb6wghGmn5HfTml16ILGA/G5al2DW7URYTFYrOU7g" - "icQ020sYqYDM9CbdgqFd4vzHL03JfvLjk6ZgADAVCSEsJ" - "vHsdL+utNYrm2ufZDVZSkzPKaQkW8kthpyS297BvRdRzR" - "6DdTurJbPy9Ov1K6xr3HBPQuIMowR3asegUyDuU9SuUG+" - "dmIGyZ0b7FBN9St3WunyC5yMsrVv7uXzRP58s/qKn6C4q" - "lQoVxVIvd4YBwzBUFKs6ZaD27U9hEdcAN98Sx2IxykafI" - "YrizbfESoB+dd9/KF/d/wX3cJvREzl1vAAAAABJRU5Erk" - "Jggg==">>; -image_base64(<<"valid-xhtml10.png">>) -> - <<"iVBORw0KGgoAAAANSUhEUgAAAFgAAAAfCAMAAAEjEcpEA" - "AACiFBMVEUAAADe5+fOezmtra3ejEKlhELvvWO9WlrehE" - "LOe3vepaWclHvetVLGc3PerVKcCAj3vVqUjHOUe1JjlL0" - "xOUpjjL2UAAC91ueMrc7vrVKlvdbW3u+EpcbO3ufO1ucY" - "WpSMKQi9SiF7e3taWkoQEAiMczkQSoxaUkpzc3O1lEoIC" - "ACEazEhGAgIAACEYzFra2utjELWcznGnEr/7+9jY2POaz" - "HOYzGta2NShLVrlL05OUqctdacCADGa2ucAADGpVqUtc6" - "1ORg5OTmlUikYGAiUezl7YzEYEAiUczkxMTG9nEqtIRDe" - "3t4AMXu9lEoQCACMazEAKXspKSmljFrW1ta1jELOzs7n7" - "/fGxsa9pVqEOSkpY5xznL29tZxahLXOpVr/99ZrY1L/79" - "ZjUiljSikAOYTvxmMAMYScezmchFqUczGtlFp7c2utjFq" - "UlJStxt73///39/9Ce61CSkq9xsZznMbW5+9Cc62MjIxC" - "Qkrv9/fv7/fOzsbnlErWjIz/3mtCORhza1IpIRBzWjH/1" - "mtCMRhzY1L/zmvnvVpSQiHOpVJrUinntVr3zmOEc1L3xm" - "NaWlq1nFo5QkrGWim1lFoISpRSUlK1zt4hWpwASoz////" - "///8xa6WUaykAQoxKe61KSkp7nMbWtWPe5+9jWlL39/f3" - "9/fWrWNCQkLera3nvWPv7+85MRjntWPetVp7c1IxKRCUl" - "HtKORh7a1IxIRCUjHtaSiHWrVIpIQhzWinvvVpaQiH/1m" - "PWpVKMe1L/zmP/xmNrUiGErc4YGBj/73PG1ucQWpT/53O" - "9nFoQUpS1SiEQEBC9zt69vb05c6UISoxSUko5a6UICAhS" - "SkohUpS1tbXetWMAQoSUgD+kAAAA2HRSTlP/////////i" - "P9sSf//dP////////////////////////////////////" - "////////////8M////////////ef/////////////////" - "/////////////////////////////////////////////" - "//////////////////////9d/////////////////////" - "///////////////AP//////////////CP//RP////////" - "/////////////////////////////////////////////" - "///////9xPp1gAAAFvUlEQVR42pVWi18URRwfy7vsYUba" - "iqBRBFmICUQGVKcZckQeaRJQUCLeycMSfKGH0uo5NELpI" - "vGQGzokvTTA85VHKTpbRoeJnPno/p1+M7t3txj20e/Nzu" - "7Ofve7v/k9Zg4Vc+wRQMW0eyLx1ZSANeBDxVmxZZSwEUY" - "kGAewm1eIBOMRvhv1UA+q8KXIVuxGdCelFYwxAnxOrxgb" - "Y8Ti1t4VA0QHYz4x3FnVC8OVLXv9fkKGSWDoW/4lG6Vbd" - "tBblesOs+MjmEmzJKNIJWFEfEQTCWNPFKvcKEymjLO1b8" - "bwYQd1hCiiDCl5KsrDCIlhj4fSuvcpfSpgJmyv6dzeZv+" - "nMPx3dhbt94II07/JZliEtm1N2RIYPkTYshwYm245a/zk" - "WjJwcyFh6ZIcYxxmqiaDSYxhOhFUsqngi3Fzcj3ljdYDN" - "E9uzA1YD/5MhnzW1KRqF7mYG8jFYXLcfLpjOe2LA0fuGq" - "QrQHl10sdK0sFcFSOSlzF0BgXQH9h3QZDBI0ccNEhftjX" - "uippBDD2/eMRiETmwwNEYHyqhdDyo22w+3QHuNbdve5a7" - "eOkHmDVJ0ixNmfbz1h0qo/Q6GuSB2wQJQbpOjOQAl7woW" - "SRJ0m2ewhvAOUiYYtZtaZL0CZZmtmVOQttLfr/dbveLZo" - "drfrL7W75wG/JjqkQxoNTtNsTKELQpQL6/D5loaSmyTT8" - "TUhsmi8iFA0hZiyltf7OiNKdarRm5w2So2lTNdPLuIzR+" - "AiLj8VTRJaj0LmX4VhJ27f/VJV/yycilWPOrk8NkXi7Qq" - "mj5bHqVZlJKZIRk1wFzKrt0WUbnXMPJ1fk4TJ5oWBA61p" - "1V76DeIs0MX+s3GxRlA1vtw83KhgNphc1nyErLO5zcvbO" - "srq+scbZnpzc6QVFPenLwGxmC+BOfYI+DN55QYddh4Q/N" - "E/yGYYj4TOGNngQavAZnzzTovEA+kcMJ+247uYexNA+4F" - "svjmuv662jsWxPZx2xg890bYMYnTgya7bjmCiEY0qgJ0v" - "MF3c+NoFdPyzxz6V3Uxs3AOWCDchRvOsQtBrbFsrT2fhH" - "Ec7ByGzu/dA4IO0A3HdfeP9yMqAwP6NPEb6cbwn0PWVU1" - "7/FDBQh/CPIrbfcg027IZrsAT/Bf3FNWyn9RSR4cvvwn3" - "e4HFmYPDl/thYcRVi8qPEoXVUWBl6FTBFTtnqmKKg5wnl" - "F4wZ1yeLv7TiwXKektE+iDBNicWEyLpnFhfDkpJc3q2kh" - "SPyQBbE0dMJnOoDzTwGsI7cdyMkL5gWqUjCF6Txst/twx" - "Cv1WzzHoy21ZDQ1xnuDzdPDWR4knr14v0tYn3IxaMFFdi" - "MOlEOJHw1jOQ4sWt5rQopRkXZhMEi7pmeDCVWBlfUKwhM" - "Z7rsF6elKsvbwiKxgxIdewa3ErsaYomCVZFYJb0GUu3Jq" - "GUNoplBxYiYby8vLBFWef+Cri4/I1sbQ/1OtYTrNtdXS+" - "rSe7kQ52eSObL99/iErCWUjCy5W4JLygmCouGfG9x9fmx" - "17XhBuDCaOerbt538erta7TFktLvdHghZcCbcPQO33zIJ" - "G9kxF5hoVXnzTzRz0r5js8oTj6uyPkGRf346HOLcasgFe" - "xueNUWFPtuFKzjoSFYYedhwVlhsRVYWWJpltv1XPQT1Rl" - "0bjZIBlb1XujVDzY/Kj4k6Ku3+Z0jo1owjVzDpFTXe1ju" - "vBSWNFmNWGZy8LvzUl5PN4JCwyNDzbQ0aAj4Zrjz0FatG" - "JJYhvq4j7mGSpvytGFlZtHf2C4o/28Zu8z7wo7eYPfXys" - "nF0i9NnPh1t1zR7VBb9GqaOXhtTmHQdgMFXE+Z608cnpO" - "DdZdjL+TuDY44Q38kJXHhccWLoOd9uv1AwwvO+48uu+fa" - "CSJPJ1bmy6ThyvpivBmYWgjxPDPAp7JTemY/yGKFEiRt/" - "jG/2P79s8KCwoLCgoLC/khUBA5F0SfQZ+RYfpNE/4Xosm" - "q7jsZAJsAAAAASUVORK5CYII=">>; -image_base64(<<"vcss.png">>) -> - <<"iVBORw0KGgoAAAANSUhEUgAAAFgAAAAfCAMAAABUFvrSA" - "AABKVBMVEUAAAAjIx8MR51ZVUqAdlmdnZ3ejEWLDAuNjY" - "1kiMG0n2d9fX19Ghfrp1FtbW3y39+3Ph6lIRNdXV2qJBF" - "cVUhcVUhPT0/dsmpUfLr57+/u7u4/PDWZAACZAADOp1Gd" - "GxG+SyTgvnNdSySzk16+mkuxw+BOS0BOS0DOzs7MzMy4T" - "09RRDwsJBG+vr73wV6fkG6eCQRFcLSurq6/X1+ht9nXfz" - "5sepHuwV59ZTHetFjQ2+wMCQQ2ZK5tWCsmWajsz8+Sq9N" - "MPh4hVaY8MRj///////////////////////9MTEyOp9Lu" - "8vhXU1A8PDyjOSTBz+YLRJ2rLy8sLCwXTaKujEUcHByDn" - "82dfz7/zGafDw+fDw+zRSlzlMcMDAyNcji1tbXf5vIcFg" - "vATJOjAAAAY3RSTlP/8/////////////////8A//////P" - "/////ov//8//////////////z///T//////////+i////" - "//////////8w/////6IA/xAgMP//////////8////////" - "/8w0/////////+zehebAAACkUlEQVR42u2VfVPTQBDG19" - "VqC6LY+lKrRIxFQaFSBPuSvhBPF8SIUZK2J5Yav/+HcO8" - "uZdLqTCsU/nKnyWwvk1/unnt2D9ZmH+8/cMAaTRFy+ng6" - "9/yiwC/+gy8R3McGv5zHvGJEGAdR4eBgi1IbZwevIEZE2" - "4pFtBtzG1Q4AoD5zvw5pEDcJvIQV/TE3/l+H9GnNJwcdA" - "BS5wAbFQLMqI98/UReoAaOTlaJsp0zaHx7LwZvY0BUR2x" - "pWTzqam0gzY8KGzG4MhBCNGucha4QbpETy+Yk/BP85nt7" - "34AjpQLTsE4ZFpf/dnkUCglXVNYB+OfUZJHvAqAoa45Oe" - "uPgm4+Xjtv7xm4N7PMV4C61+Mrz3H2WImm3ATiWrAiwZR" - "WcUA5Ej4dgIEMxDv6yxHHcNuAutnjv2HZ1NeuycoVPh0m" - "wC834zZC9Ao5dkZZKwLVGwT+WdLw0YOZ1saEkUDoT+QGW" - "KZ0E2xpcrPakVW2KXwyUtYEtlEAj3GXD/fYwrryAdeiyG" - "qidQSw1eqtJcA8cZq4zXqhPuCBYE1fKJjh/5X6MwRm9c2" - "xf7WVdLf5oSdt64esVIwVAKC1HJ2oli8vj3L0YzC4zjkM" - "agt+arDAs6bApbL1RVlWIqrJbreqKZmh4y6VR7rAJeUYD" - "VRj9VqRXkErpJ9lbEwtE83KlIfeG4p52t7zWIMO1XcaGz" - "54uUyet+hBM7BXXDS8Xc5+8Gmmbu1xwSoGIokA3oTptQe" - "cQ4Iimm/Ew7jwbPfMi3TM91T9XVIGo+W9xC8oWpugVCXL" - "uwXijjxJ3r/6PjX7nlFua8QmyM+TO/Gja2TTc2Z95C5ua" - "ewGH6cJi6bJO6Z+TY276eH3tbgy+/3ly3Js+rj66osG/A" - "V5htgaQ9SeRAAAAAElFTkSuQmCC">>; -image_base64(<<"powered-by-ejabberd.png">>) -> - <<"iVBORw0KGgoAAAANSUhEUgAAAGUAAAAfCAMAAADJG/NaA" - "AAAw1BMVEUAAAAjBgYtBAM5AwFCAAAYGAJNAABcAABIDQ" - "5qAAAoJRV7AACFAAAoKSdJHByLAAAwLwk1NQA1MzFJKyo" - "4NxtDQQBEQT5KSCxSTgBSUBlgQ0JYSEpZWQJPUU5hYABb" - "W0ZiYClcW1poaCVwbQRpaDhzYWNsakhuZ2VrbFZ8dwCEg" - "AB3dnd4d2+OjACDhYKcmACJi4iQkpWspgCYmJm5swCmqa" - "zEwACwsbS4ub3X0QLExsPLyszW1Nnc3ODm5ugMBwAWAwP" - "Hm1IFAAAAAXRSTlMAQObYZgAAAAFiS0dEAIgFHUgAAAAJ" - "cEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQfVCRQOBA7VB" - "kCMAAACcElEQVRIx72WjXKiMBSFQalIFbNiy1pdrJZaRV" - "YR5deGwPs/VRNBSBB2OjvQO0oYjPfj5J6bCcdx8i2Uldx" - "KcDhk1HbIPwFBF/kHKJfjPSVAyIRHF9rRZ4sUX3EDdWOv" - "1+u2tESaavpnYTbv9zvd0WwDy3/QcGQXlH5uTxB1l07MJ" - "lRpsUei0JF6Qi+OHyGK7ijXxPklHe/umIllim3iUBMJDI" - "EULxxPP0TVWhhKJoN9fUpdmQLteV8aDgEAg9gIcTjL4F4" - "L+r4WVKEF+rbJdwYYAoQHY+oQjnGootyKwxapoi73WkyF" - "FySQBv988naEEp4+YMMec5VUCQDJTscEy7Kc0HsLmqNE7" - "rovDjMpIHHGYeidXn4TQcaxMYqP3RV3C8oCl2WvrlSPaN" - "pGZadRnmPGCk8ylM2okAJ4i9TEe1KersXxSl6jUt5uayi" - "IodirtcKLOaWblj50wiyMv1F9lm9TUDArGAD0FmEpvCUs" - "VoZy6dW81Fg0aDaHogQa36ekAPG5DDGsbdZrGsrzZUnzv" - "Bo1I2tLmuL69kSitAweyHKN9b3leDfQMnu3nIIKWfmXnq" - "GVKedJT6QpICbJvf2f8aOsvn68v+k7/cwUQdPoxaMoRTn" - "KFHNlKsKQphCTOa84u64vpi8bH31CqsbF6lSONRTkTyQG" - "Arq49/fEvjBwz4eDS2/JpaXRNOoXRD/VmOrDVTJJRIZCT" - "Lav3VrqbPvP3vdduGEhQJzilncbpSA4F3vsihErO+dayv" - "/sY5/yRE0GDEXCu2VoNiMlo5i+P2KlgMEvTNk2eYa5XEy" - "h12Ex17Z8vzQUR3KEPbYd6XG87eC4Ly75RneS5ZYHAAAA" - "AElFTkSuQmCC">>. +%% base64:encode(F1b). create_image_files(Images_dir) -> Filenames = [<<"powered-by-ejabberd.png">>, <<"powered-by-erlang.png">>, <<"valid-xhtml10.png">>, <<"vcss.png">>], - lists:foreach(fun (Filename) -> - Filename_full = fjoin([Images_dir, Filename]), - {ok, F} = file:open(Filename_full, [write]), - Image = jlib:decode_base64(image_base64(Filename)), - io:format(F, <<"~s">>, [Image]), - file:close(F) - end, - Filenames), - ok. + lists:foreach( + fun(Filename) -> + Src = filename:join([misc:img_dir(), Filename]), + Dst = fjoin([Images_dir, Filename]), + case file:copy(Src, Dst) of + {ok, _} -> ok; + {error, Why} -> + ?ERROR_MSG("Failed to copy ~ts to ~ts: ~ts", + [Src, Dst, file:format_error(Why)]) + end + end, Filenames). fw(F, S) -> fw(F, S, [], html). @@ -772,7 +543,7 @@ fw(F, S, FileFormat) when is_atom(FileFormat) -> fw(F, S, [], FileFormat). fw(F, S, O, FileFormat) -> - S1 = list_to_binary(io_lib:format(binary_to_list(S) ++ "~n", O)), + S1 = <<(str:format(S, O))/binary, "\n">>, S2 = case FileFormat of html -> S1; @@ -793,13 +564,13 @@ put_header(F, Room, Date, CSSFile, Lang, Hour_offset, "org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">">>), fw(F, <<"">>, + "xml:lang=\"~ts\" lang=\"~ts\">">>, [Lang, Lang]), fw(F, <<"">>), fw(F, <<"">>), - fw(F, <<"~s - ~s">>, + fw(F, <<"~ts - ~ts">>, [htmlize(Room#room.title), Date]), put_header_css(F, CSSFile), put_header_script(F), @@ -810,28 +581,28 @@ put_header(F, Room, Date, CSSFile, Lang, Hour_offset, <<"">>, + ": bold;\" href=\"~ts\">~ts">>, [Top_url, Top_text]), - fw(F, <<"
~s
">>, + fw(F, <<"
~ts
">>, [htmlize(Room#room.title)]), fw(F, - <<"~s" + <<"~ts" "">>, [Room#room.jid, Room#room.jid]), fw(F, - <<"
~s" - "< " + <<"
~ts" + "< " "^ >>">>, [Date, Date_prev, Date_next]), - case {htmlize(Room#room.subject_author), + case {htmlize(prepare_subject_author(Room#room.subject_author)), htmlize(Room#room.subject)} of {<<"">>, <<"">>} -> ok; {SuA, Su} -> - fw(F, <<"
~s~s~s
">>, - [SuA, ?T(<<" has set the subject to: ">>), Su]) + fw(F, <<"
~ts~ts~ts
">>, + [SuA, tr(Lang, ?T(" has set the subject to: ")), Su]) end, RoomConfig = roomconfig_to_string(Room#room.config, Lang, FileFormat), @@ -840,121 +611,49 @@ put_header(F, Room, Date, CSSFile, Lang, Hour_offset, RoomOccupants = roomoccupants_to_string(Occupants, FileFormat), put_room_occupants(F, RoomOccupants, Lang, FileFormat), + put_occupants_join_leave(F, Lang), Time_offset_str = case Hour_offset < 0 of true -> io_lib:format("~p", [Hour_offset]); false -> io_lib:format("+~p", [Hour_offset]) end, - fw(F, <<"
GMT~s
">>, + fw(F, <<"
GMT~ts
">>, [Time_offset_str]). -put_header_css(F, false) -> +put_header_css(F, {file, Path}) -> fw(F, <<"">>); -put_header_css(F, CSSFile) -> +put_header_css(F, {url, URL}) -> fw(F, <<"">>, - [CSSFile]). + "href=\"~ts\" media=\"all\">">>, + [URL]). put_header_script(F) -> fw(F, <<"">>). put_room_config(_F, _RoomConfig, _Lang, plaintext) -> ok; put_room_config(F, RoomConfig, Lang, _FileFormat) -> - {_, Now2, _} = now(), + {_, Now2, _} = erlang:timestamp(), fw(F, <<"
">>), fw(F, <<"
~s
">>, - [Now2, ?T(<<"Room Configuration">>)]), + "false;\">~ts
">>, + [Now2, tr(Lang, ?T("Room Configuration"))]), fw(F, <<"

~s
">>, + "y: none;\" >
~ts
">>, [Now2, RoomConfig]), fw(F, <<"
">>). @@ -963,21 +662,50 @@ put_room_occupants(_F, _RoomOccupants, _Lang, ok; put_room_occupants(F, RoomOccupants, Lang, _FileFormat) -> - {_, Now2, _} = now(), + {_, Now2, _} = erlang:timestamp(), %% htmlize %% The default behaviour is to ignore the nofollow spam prevention on links %% (NoFollow=false) fw(F, <<"
">>), fw(F, <<"
~s
">>, - [Now2, ?T(<<"Room Occupants">>)]), + "false;\">~ts
">>, + [Now2, tr(Lang, ?T("Room Occupants"))]), fw(F, <<"

~s
">>, + "y: none;\" >
~ts">>, [Now2, RoomOccupants]), fw(F, <<"">>). +put_occupants_join_leave(F, Lang) -> + fw(F, <<"
">>), + fw(F, + <<"
~ts
">>, + [tr(Lang, ?T("Show Occupants Join/Leave"))]), + fw(F, <<"
">>). + +maybe_print_jl(_Direction, _F, _Message, plaintext) -> + ok; +maybe_print_jl(Direction, F, Message, html) -> + PrintJl = case Message of + join -> true; + leave -> true; + {leave, _} -> true; + _ -> false + end, + case PrintJl of + true -> print_jl(Direction, F); + false -> ok + end. + +print_jl(Direction, F) -> + String = case Direction of + open -> "
"; + close -> "
" + end, + fw(F, io_lib:format(String, [])). + htmlize(S1) -> htmlize(S1, html). htmlize(S1, plaintext) -> @@ -1034,7 +762,7 @@ get_room_info(RoomJID, Opts) -> false -> <<"">> end, Subject = case lists:keysearch(subject, 1, Opts) of - {value, {_, S}} -> S; + {value, {_, S}} -> xmpp:get_text(S); false -> <<"">> end, SubjectAuthor = case lists:keysearch(subject_author, 1, @@ -1043,7 +771,7 @@ get_room_info(RoomJID, Opts) -> {value, {_, SA}} -> SA; false -> <<"">> end, - #room{jid = jlib:jid_to_string(RoomJID), title = Title, + #room{jid = jid:encode(RoomJID), title = Title, subject = Subject, subject_author = SubjectAuthor, config = Opts}. @@ -1056,10 +784,9 @@ roomconfig_to_string(Options, Lang, FileFormat) -> Os2 = lists:sort(Os1), Options2 = Title ++ Os2, lists:foldl(fun ({Opt, Val}, R) -> - case get_roomconfig_text(Opt) of + case get_roomconfig_text(Opt, Lang) of undefined -> R; - OptT -> - OptText = (?T(OptT)), + OptText -> R2 = case Val of false -> <<"
", @@ -1078,7 +805,7 @@ roomconfig_to_string(Options, Lang, FileFormat) -> max_users -> <<"
", OptText/binary, ": \"", - (htmlize(jlib:integer_to_binary(T), + (htmlize(integer_to_binary(T), FileFormat))/binary, "\"
">>; title -> @@ -1096,7 +823,13 @@ roomconfig_to_string(Options, Lang, FileFormat) -> allow_private_messages_from_visitors -> <<"
", OptText/binary, ": \"", - (htmlize(?T((jlib:atom_to_binary(T))), + (htmlize(tr(Lang, misc:atom_to_binary(T)), + FileFormat))/binary, + "\"
">>; + allowpm -> + <<"
", + OptText/binary, ": \"", + (htmlize(tr(Lang, misc:atom_to_binary(T)), FileFormat))/binary, "\"
">>; _ -> <<"\"", T/binary, "\"">> @@ -1107,49 +840,48 @@ roomconfig_to_string(Options, Lang, FileFormat) -> end, <<"">>, Options2). -get_roomconfig_text(title) -> <<"Room title">>; -get_roomconfig_text(persistent) -> - <<"Make room persistent">>; -get_roomconfig_text(public) -> - <<"Make room public searchable">>; -get_roomconfig_text(public_list) -> - <<"Make participants list public">>; -get_roomconfig_text(password_protected) -> - <<"Make room password protected">>; -get_roomconfig_text(password) -> <<"Password">>; -get_roomconfig_text(anonymous) -> - <<"This room is not anonymous">>; -get_roomconfig_text(members_only) -> - <<"Make room members-only">>; -get_roomconfig_text(moderated) -> - <<"Make room moderated">>; -get_roomconfig_text(members_by_default) -> - <<"Default users as participants">>; -get_roomconfig_text(allow_change_subj) -> - <<"Allow users to change the subject">>; -get_roomconfig_text(allow_private_messages) -> - <<"Allow users to send private messages">>; -get_roomconfig_text(allow_private_messages_from_visitors) -> - <<"Allow visitors to send private messages to">>; -get_roomconfig_text(allow_query_users) -> - <<"Allow users to query other users">>; -get_roomconfig_text(allow_user_invites) -> - <<"Allow users to send invites">>; -get_roomconfig_text(logging) -> <<"Enable logging">>; -get_roomconfig_text(allow_visitor_nickchange) -> - <<"Allow visitors to change nickname">>; -get_roomconfig_text(allow_visitor_status) -> - <<"Allow visitors to send status text in " - "presence updates">>; -get_roomconfig_text(captcha_protected) -> - <<"Make room captcha protected">>; -get_roomconfig_text(description) -> - <<"Room description">>; -%% get_roomconfig_text(subject) -> "Subject"; -%% get_roomconfig_text(subject_author) -> "Subject author"; -get_roomconfig_text(max_users) -> - <<"Maximum Number of Occupants">>; -get_roomconfig_text(_) -> undefined. +get_roomconfig_text(title, Lang) -> tr(Lang, ?T("Room title")); +get_roomconfig_text(persistent, Lang) -> + tr(Lang, ?T("Make room persistent")); +get_roomconfig_text(public, Lang) -> + tr(Lang, ?T("Make room public searchable")); +get_roomconfig_text(public_list, Lang) -> + tr(Lang, ?T("Make participants list public")); +get_roomconfig_text(password_protected, Lang) -> + tr(Lang, ?T("Make room password protected")); +get_roomconfig_text(password, Lang) -> tr(Lang, ?T("Password")); +get_roomconfig_text(anonymous, Lang) -> + tr(Lang, ?T("This room is not anonymous")); +get_roomconfig_text(members_only, Lang) -> + tr(Lang, ?T("Make room members-only")); +get_roomconfig_text(moderated, Lang) -> + tr(Lang, ?T("Make room moderated")); +get_roomconfig_text(members_by_default, Lang) -> + tr(Lang, ?T("Default users as participants")); +get_roomconfig_text(allow_change_subj, Lang) -> + tr(Lang, ?T("Allow users to change the subject")); +get_roomconfig_text(allowpm, Lang) -> + tr(Lang, ?T("Who can send private messages")); +get_roomconfig_text(allow_private_messages_from_visitors, Lang) -> + tr(Lang, ?T("Allow visitors to send private messages to")); +get_roomconfig_text(allow_query_users, Lang) -> + tr(Lang, ?T("Allow users to query other users")); +get_roomconfig_text(allow_user_invites, Lang) -> + tr(Lang, ?T("Allow users to send invites")); +get_roomconfig_text(logging, Lang) -> tr(Lang, ?T("Enable logging")); +get_roomconfig_text(allow_visitor_nickchange, Lang) -> + tr(Lang, ?T("Allow visitors to change nickname")); +get_roomconfig_text(allow_visitor_status, Lang) -> + tr(Lang, ?T("Allow visitors to send status text in presence updates")); +get_roomconfig_text(captcha_protected, Lang) -> + tr(Lang, ?T("Make room CAPTCHA protected")); +get_roomconfig_text(description, Lang) -> + tr(Lang, ?T("Room description")); +%% get_roomconfig_text(subject, Lang) -> "Subject"; +%% get_roomconfig_text(subject_author, Lang) -> "Subject author"; +get_roomconfig_text(max_users, Lang) -> + tr(Lang, ?T("Maximum Number of Occupants")); +get_roomconfig_text(_, _) -> undefined. %% Users = [{JID, Nick, Role}] roomoccupants_to_string(Users, _FileFormat) -> @@ -1158,10 +890,7 @@ roomoccupants_to_string(Users, _FileFormat) -> Users1 /= []], iolist_to_binary([<<"
">>, Res, <<"
">>]). -%% Users = [{JID, Nick, Role}] group_by_role(Users) -> -%% Role = atom() -%% Users = [{JID, Nick}] {Ms, Ps, Vs, Ns} = lists:foldl(fun ({JID, Nick, moderator}, {Mod, Par, Vis, Non}) -> @@ -1207,36 +936,45 @@ role_users_to_string(RoleS, Users) -> <>. get_room_occupants(RoomJIDString) -> - RoomJID = jlib:string_to_jid(RoomJIDString), + RoomJID = jid:decode(RoomJIDString), RoomName = RoomJID#jid.luser, MucService = RoomJID#jid.lserver, - StateData = get_room_state(RoomName, MucService), - [{U#user.jid, U#user.nick, U#user.role} - || {_, U} <- (?DICT):to_list(StateData#state.users)]. - --spec get_room_state(binary(), binary()) -> muc_room_state(). - -get_room_state(RoomName, MucService) -> - case mnesia:dirty_read(muc_online_room, - {RoomName, MucService}) - of - [R] -> - RoomPid = R#muc_online_room.pid, - get_room_state(RoomPid); - [] -> #state{} + case get_room_state(RoomName, MucService) of + {ok, StateData} -> + [{U#user.jid, U#user.nick, U#user.role} + || U <- maps:values(StateData#state.users)]; + error -> + [] end. --spec get_room_state(pid()) -> muc_room_state(). +prepare_subject_author({Nick, _}) -> + Nick; +prepare_subject_author(SA) -> + SA. + +-spec get_room_state(binary(), binary()) -> {ok, mod_muc_room:state()} | error. + +get_room_state(RoomName, MucService) -> + case mod_muc:find_online_room(RoomName, MucService) of + {ok, RoomPid} -> + get_room_state(RoomPid); + error -> + error + end. + +-spec get_room_state(pid()) -> {ok, mod_muc_room:state()} | error. get_room_state(RoomPid) -> - {ok, R} = gen_fsm:sync_send_all_state_event(RoomPid, - get_state), - R. + case mod_muc_room:get_state(RoomPid) of + {ok, State} -> {ok, State}; + {error, _} -> error + end. -get_proc_name(Host) -> gen_mod:get_module_proc(Host, ?PROCNAME). +get_proc_name(Host) -> + gen_mod:get_module_proc(Host, ?MODULE). calc_hour_offset(TimeHere) -> - TimeZero = calendar:now_to_universal_time(now()), + TimeZero = calendar:universal_time(), TimeHereHour = calendar:datetime_to_gregorian_seconds(TimeHere) div 3600, @@ -1245,6 +983,186 @@ calc_hour_offset(TimeHere) -> 3600, TimeHereHour - TimeZeroHour. -fjoin([]) -> <<"/">>; fjoin(FileList) -> list_to_binary(filename:join([binary_to_list(File) || File <- FileList])). + +-spec tr(binary(), binary()) -> binary(). +tr(Lang, Text) -> + translate:translate(Lang, Text). + +has_no_permanent_store_hint(Packet) -> + xmpp:has_subtag(Packet, #hint{type = 'no-store'}) orelse + xmpp:has_subtag(Packet, #hint{type = 'no-storage'}) orelse + xmpp:has_subtag(Packet, #hint{type = 'no-permanent-store'}) orelse + xmpp:has_subtag(Packet, #hint{type = 'no-permanent-storage'}). + +mod_opt_type(access_log) -> + econf:acl(); +mod_opt_type(cssfile) -> + econf:url_or_file(); +mod_opt_type(dirname) -> + econf:enum([room_jid, room_name]); +mod_opt_type(dirtype) -> + econf:enum([subdirs, plain]); +mod_opt_type(file_format) -> + econf:enum([html, plaintext]); +mod_opt_type(file_permissions) -> + econf:and_then( + econf:options( + #{mode => econf:non_neg_int(), + group => econf:non_neg_int()}), + fun(Opts) -> + {proplists:get_value(mode, Opts, 644), + proplists:get_value(group, Opts, 33)} + end); +mod_opt_type(outdir) -> + econf:directory(write); +mod_opt_type(spam_prevention) -> + econf:bool(); +mod_opt_type(timezone) -> + econf:enum([local, universal]); +mod_opt_type(url) -> + econf:url(); +mod_opt_type(top_link) -> + econf:and_then( + econf:non_empty( + econf:map(econf:binary(), econf:binary())), + fun hd/1). + +-spec mod_options(binary()) -> [{top_link, {binary(), binary()}} | + {file_permissions, + {non_neg_integer(), non_neg_integer()}} | + {atom(), any()}]. +mod_options(_) -> + [{access_log, muc_admin}, + {cssfile, {file, filename:join(misc:css_dir(), <<"muc.css">>)}}, + {dirname, room_jid}, + {dirtype, subdirs}, + {file_format, html}, + {file_permissions, {644, 33}}, + {outdir, <<"www/muc">>}, + {spam_prevention, true}, + {timezone, local}, + {url, undefined}, + {top_link, {<<"/">>, <<"Home">>}}]. + +mod_doc() -> + #{desc => + [?T("This module enables optional logging " + "of Multi-User Chat (MUC) public " + "conversations to HTML. Once you enable " + "this module, users can join a room using a " + "MUC capable XMPP client, and if they have " + "enough privileges, they can request the " + "configuration form in which they can set " + "the option to enable room logging."), "", + ?T("Features:"), "", + ?T("- Room details are added on top of each page: " + "room title, JID, author, subject and configuration."), "", + ?T("- The room JID in the generated HTML is a link " + "to join the room (using XMPP URI)."), "", + ?T("- Subject and room configuration changes are tracked " + "and displayed."), "", + ?T("- Joins, leaves, nick changes, kicks, bans and '/me' " + "are tracked and displayed, including the reason if available."), "", + ?T("- Generated HTML files are XHTML 1.0 Transitional and " + "CSS compliant."), "", + ?T("- Timestamps are self-referencing links."), "", + ?T("- Links on top for quicker navigation: " + "Previous day, Next day, Up."), "", + ?T("- CSS is used for style definition, and a custom " + "CSS file can be used."), "", + ?T("- URLs on messages and subjects are converted to hyperlinks."), "", + ?T("- Timezone used on timestamps is shown on the log files."), "", + ?T("- A custom link can be added on top of each page."), "", + ?T("The module depends on _`mod_muc`_.")], + opts => + [{access_log, + #{value => ?T("AccessName"), + desc => + ?T("This option restricts which occupants are " + "allowed to enable or disable room logging. " + "The default value is 'muc_admin'. NOTE: " + "for this default setting you need to have an " + "access rule for 'muc_admin' in order to take effect.")}}, + {cssfile, + #{value => ?T("Path | URL"), + desc => + ?T("With this option you can set whether the HTML " + "files should have a custom CSS file or if they " + "need to use the embedded CSS. Allowed values " + "are either 'Path' to local file or an 'URL' to " + "a remote file. By default a predefined CSS will " + "be embedded into the HTML page.")}}, + {dirname, + #{value => "room_jid | room_name", + desc => + ?T("Configure the name of the room directory. " + "If set to 'room_jid', the room directory name will " + "be the full room JID. Otherwise, the room directory " + "name will be only the room name, not including the " + "MUC service name. The default value is 'room_jid'.")}}, + {dirtype, + #{value => "subdirs | plain", + desc => + ?T("The type of the created directories can be specified " + "with this option. If set to 'subdirs', subdirectories " + "are created for each year and month. Otherwise, the " + "names of the log files contain the full date, and " + "there are no subdirectories. The default value is 'subdirs'.")}}, + {file_format, + #{value => "html | plaintext", + desc => + ?T("Define the format of the log files: 'html' stores " + "in HTML format, 'plaintext' stores in plain text. " + "The default value is 'html'.")}}, + {file_permissions, + #{value => "{mode: Mode, group: Group}", + desc => + ?T("Define the permissions that must be used when " + "creating the log files: the number of the mode, " + "and the numeric id of the group that will own the " + "files. The default value is shown in the example below:"), + example => + ["file_permissions:", + " mode: 644", + " group: 33"]}}, + {outdir, + #{value => ?T("Path"), + desc => + ?T("This option sets the full path to the directory " + "in which the HTML files should be stored. " + "Make sure the ejabberd daemon user has write " + "access on that directory. The default value is 'www/muc'.")}}, + {spam_prevention, + #{value => "true | false", + desc => + ?T("If set to 'true', a special attribute is added to links " + "that prevent their indexation by search engines. " + "The default value is 'true', which mean that 'nofollow' " + "attributes will be added to user submitted links.")}}, + {timezone, + #{value => "local | universal", + desc => + ?T("The time zone for the logs is configurable with " + "this option. If set to 'local', the local time, as " + "reported to Erlang emulator by the operating system, " + "will be used. Otherwise, UTC time will be used. " + "The default value is 'local'.")}}, + {url, + #{value => ?T("URL"), + desc => + ?T("A top level 'URL' where a client can access " + "logs of a particular conference. The conference name " + "is appended to the URL if 'dirname' option is set to " + "'room_name' or a conference JID is appended to the 'URL' " + "otherwise. There is no default value.")}}, + {top_link, + #{value => "{URL: Text}", + desc => + ?T("With this option you can customize the link on " + "the top right corner of each log file. " + "The default value is shown in the example below:"), + example => + ["top_link:", + " /: Home"]}}]}. diff --git a/src/mod_muc_log_opt.erl b/src/mod_muc_log_opt.erl new file mode 100644 index 000000000..fb4d0266f --- /dev/null +++ b/src/mod_muc_log_opt.erl @@ -0,0 +1,83 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_muc_log_opt). + +-export([access_log/1]). +-export([cssfile/1]). +-export([dirname/1]). +-export([dirtype/1]). +-export([file_format/1]). +-export([file_permissions/1]). +-export([outdir/1]). +-export([spam_prevention/1]). +-export([timezone/1]). +-export([top_link/1]). +-export([url/1]). + +-spec access_log(gen_mod:opts() | global | binary()) -> 'muc_admin' | acl:acl(). +access_log(Opts) when is_map(Opts) -> + gen_mod:get_opt(access_log, Opts); +access_log(Host) -> + gen_mod:get_module_opt(Host, mod_muc_log, access_log). + +-spec cssfile(gen_mod:opts() | global | binary()) -> {'file',binary()} | {'url',binary()}. +cssfile(Opts) when is_map(Opts) -> + gen_mod:get_opt(cssfile, Opts); +cssfile(Host) -> + gen_mod:get_module_opt(Host, mod_muc_log, cssfile). + +-spec dirname(gen_mod:opts() | global | binary()) -> 'room_jid' | 'room_name'. +dirname(Opts) when is_map(Opts) -> + gen_mod:get_opt(dirname, Opts); +dirname(Host) -> + gen_mod:get_module_opt(Host, mod_muc_log, dirname). + +-spec dirtype(gen_mod:opts() | global | binary()) -> 'plain' | 'subdirs'. +dirtype(Opts) when is_map(Opts) -> + gen_mod:get_opt(dirtype, Opts); +dirtype(Host) -> + gen_mod:get_module_opt(Host, mod_muc_log, dirtype). + +-spec file_format(gen_mod:opts() | global | binary()) -> 'html' | 'plaintext'. +file_format(Opts) when is_map(Opts) -> + gen_mod:get_opt(file_format, Opts); +file_format(Host) -> + gen_mod:get_module_opt(Host, mod_muc_log, file_format). + +-spec file_permissions(gen_mod:opts() | global | binary()) -> {non_neg_integer(),non_neg_integer()}. +file_permissions(Opts) when is_map(Opts) -> + gen_mod:get_opt(file_permissions, Opts); +file_permissions(Host) -> + gen_mod:get_module_opt(Host, mod_muc_log, file_permissions). + +-spec outdir(gen_mod:opts() | global | binary()) -> binary(). +outdir(Opts) when is_map(Opts) -> + gen_mod:get_opt(outdir, Opts); +outdir(Host) -> + gen_mod:get_module_opt(Host, mod_muc_log, outdir). + +-spec spam_prevention(gen_mod:opts() | global | binary()) -> boolean(). +spam_prevention(Opts) when is_map(Opts) -> + gen_mod:get_opt(spam_prevention, Opts); +spam_prevention(Host) -> + gen_mod:get_module_opt(Host, mod_muc_log, spam_prevention). + +-spec timezone(gen_mod:opts() | global | binary()) -> 'local' | 'universal'. +timezone(Opts) when is_map(Opts) -> + gen_mod:get_opt(timezone, Opts); +timezone(Host) -> + gen_mod:get_module_opt(Host, mod_muc_log, timezone). + +-spec top_link(gen_mod:opts() | global | binary()) -> {binary(),binary()}. +top_link(Opts) when is_map(Opts) -> + gen_mod:get_opt(top_link, Opts); +top_link(Host) -> + gen_mod:get_module_opt(Host, mod_muc_log, top_link). + +-spec url(gen_mod:opts() | global | binary()) -> 'undefined' | binary(). +url(Opts) when is_map(Opts) -> + gen_mod:get_opt(url, Opts); +url(Host) -> + gen_mod:get_module_opt(Host, mod_muc_log, url). + diff --git a/src/mod_muc_mnesia.erl b/src/mod_muc_mnesia.erl new file mode 100644 index 000000000..02ecb3ce8 --- /dev/null +++ b/src/mod_muc_mnesia.erl @@ -0,0 +1,496 @@ +%%%------------------------------------------------------------------- +%%% File : mod_muc_mnesia.erl +%%% Author : Evgeny Khramtsov +%%% Created : 13 Apr 2016 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_muc_mnesia). + +-behaviour(mod_muc). +-behaviour(mod_muc_room). + +%% API +-export([init/2, import/3, store_room/5, restore_room/3, forget_room/3, + can_use_nick/4, get_rooms/2, get_nick/3, set_nick/4]). +-export([register_online_room/4, unregister_online_room/4, find_online_room/3, + get_online_rooms/3, count_online_rooms/2, rsm_supported/0, + register_online_user/4, unregister_online_user/4, + count_online_rooms_by_user/3, get_online_rooms_by_user/3, + find_online_room_by_pid/2]). +-export([set_affiliation/6, set_affiliations/4, get_affiliation/5, + get_affiliations/3, search_affiliation/4]). +%% gen_server callbacks +-export([start_link/2, init/1, handle_cast/2, handle_call/3, handle_info/2, + terminate/2, code_change/3]). +-export([need_transform/1, transform/1]). + +-include("mod_muc.hrl"). +-include("logger.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). +-include_lib("stdlib/include/ms_transform.hrl"). + +-record(state, {}). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(Host, Opts) -> + Spec = {?MODULE, {?MODULE, start_link, [Host, Opts]}, + transient, 5000, worker, [?MODULE]}, + case supervisor:start_child(ejabberd_backend_sup, Spec) of + {ok, _Pid} -> ok; + %% Maybe started for a vhost which only wanted mnesia for ram + %% and this vhost wants mnesia for persitent storage too + {error, {already_started, _Pid}} -> + init([Host, Opts]); + Err -> Err + end. + +start_link(Host, Opts) -> + Name = gen_mod:get_module_proc(Host, ?MODULE), + gen_server:start_link({local, Name}, ?MODULE, [Host, Opts], []). + +store_room(_LServer, Host, Name, Opts, _) -> + F = fun () -> + mnesia:write(#muc_room{name_host = {Name, Host}, + opts = Opts}) + end, + mnesia:transaction(F). + +restore_room(_LServer, Host, Name) -> + try mnesia:dirty_read(muc_room, {Name, Host}) of + [#muc_room{opts = Opts}] -> Opts; + _ -> error + catch + _:_ -> {error, db_failure} + end. + +forget_room(_LServer, Host, Name) -> + F = fun () -> mnesia:delete({muc_room, {Name, Host}}) + end, + mnesia:transaction(F). + +can_use_nick(_LServer, ServiceOrRoom, JID, Nick) -> + {LUser, LServer, _} = jid:tolower(JID), + LUS = {LUser, LServer}, + MatchSpec = case (jid:decode(ServiceOrRoom))#jid.lserver of + ServiceOrRoom -> [{'==', {element, 2, '$1'}, ServiceOrRoom}]; + Service -> [{'orelse', + {'==', {element, 2, '$1'}, Service}, + {'==', {element, 2, '$1'}, ServiceOrRoom} }] + end, + case catch mnesia:dirty_select(muc_registered, + [{#muc_registered{us_host = '$1', + nick = Nick, _ = '_'}, + MatchSpec, + ['$_']}]) + of + {'EXIT', _Reason} -> true; + [] -> true; + [#muc_registered{us_host = {U, _Host}}] -> U == LUS + end. + +get_rooms(_LServer, Host) -> + mnesia:dirty_select(muc_room, + [{#muc_room{name_host = {'_', Host}, + _ = '_'}, + [], ['$_']}]). + +get_nick(_LServer, Host, From) -> + {LUser, LServer, _} = jid:tolower(From), + LUS = {LUser, LServer}, + case mnesia:dirty_read(muc_registered, {LUS, Host}) of + [] -> error; + [#muc_registered{nick = Nick}] -> Nick + end. + +set_nick(_LServer, ServiceOrRoom, From, Nick) -> + {LUser, LServer, _} = jid:tolower(From), + LUS = {LUser, LServer}, + F = fun () -> + case Nick of + <<"">> -> + mnesia:delete({muc_registered, {LUS, ServiceOrRoom}}), + ok; + _ -> + Service = (jid:decode(ServiceOrRoom))#jid.lserver, + MatchSpec = case (ServiceOrRoom == Service) of + true -> [{'==', {element, 2, '$1'}, ServiceOrRoom}]; + false -> [{'orelse', + {'==', {element, 2, '$1'}, Service}, + {'==', {element, 2, '$1'}, ServiceOrRoom} }] + end, + Allow = case mnesia:select( + muc_registered, + [{#muc_registered{us_host = '$1', nick = Nick, _ = '_'}, + MatchSpec, + ['$_']}]) of + [] when (ServiceOrRoom == Service) -> + NickRegistrations = mnesia:select( + muc_registered, + [{#muc_registered{us_host = '$1', nick = Nick, _ = '_'}, + [], + ['$_']}]), + not lists:any(fun({_, {_NRUS, NRServiceOrRoom}, _Nick}) -> + Service == (jid:decode(NRServiceOrRoom))#jid.lserver end, + NickRegistrations); + [] -> true; + [#muc_registered{us_host = {_U, Host}}] + when (Host == Service) and (ServiceOrRoom /= Service) -> + false; + [#muc_registered{us_host = {U, _Host}}] -> + U == LUS + end, + if Allow -> + mnesia:write(#muc_registered{ + us_host = {LUS, ServiceOrRoom}, + nick = Nick}), + ok; + true -> + false + end + end + end, + mnesia:transaction(F). + +set_affiliation(_ServerHost, _Room, _Host, _JID, _Affiliation, _Reason) -> + {error, not_implemented}. + +set_affiliations(_ServerHost, _Room, _Host, _Affiliations) -> + {error, not_implemented}. + +get_affiliation(_ServerHost, _Room, _Host, _LUser, _LServer) -> + {error, not_implemented}. + +get_affiliations(_ServerHost, _Room, _Host) -> + {error, not_implemented}. + +search_affiliation(_ServerHost, _Room, _Host, _Affiliation) -> + {error, not_implemented}. + +register_online_room(_ServerHost, Room, Host, Pid) -> + F = fun() -> + mnesia:write( + #muc_online_room{name_host = {Room, Host}, pid = Pid}) + end, + mnesia:transaction(F). + +unregister_online_room(_ServerHost, Room, Host, Pid) -> + F = fun () -> + mnesia:delete_object( + #muc_online_room{name_host = {Room, Host}, pid = Pid}) + end, + mnesia:transaction(F). + +find_online_room(_ServerHost, Room, Host) -> + find_online_room(Room, Host). + +find_online_room(Room, Host) -> + case mnesia:dirty_read(muc_online_room, {Room, Host}) of + [] -> error; + [#muc_online_room{pid = Pid}] -> {ok, Pid} + end. + +find_online_room_by_pid(_ServerHost, Pid) -> + Res = + mnesia:dirty_select( + muc_online_room, + ets:fun2ms( + fun(#muc_online_room{name_host = {Name, Host}, pid = PidS}) + when PidS == Pid -> {Name, Host} + end)), + case Res of + [{Name, Host}] -> {ok, Name, Host}; + _ -> error + end. + +count_online_rooms(_ServerHost, Host) -> + ets:select_count( + muc_online_room, + ets:fun2ms( + fun(#muc_online_room{name_host = {_, H}}) -> + H == Host + end)). + +get_online_rooms(_ServerHost, Host, + #rsm_set{max = Max, 'after' = After, before = undefined}) + when is_binary(After), After /= <<"">> -> + lists:reverse(get_online_rooms(next, {After, Host}, Host, 0, Max, [])); +get_online_rooms(_ServerHost, Host, + #rsm_set{max = Max, 'after' = undefined, before = Before}) + when is_binary(Before), Before /= <<"">> -> + get_online_rooms(prev, {Before, Host}, Host, 0, Max, []); +get_online_rooms(_ServerHost, Host, + #rsm_set{max = Max, 'after' = undefined, before = <<"">>}) -> + get_online_rooms(last, {<<"">>, Host}, Host, 0, Max, []); +get_online_rooms(_ServerHost, Host, #rsm_set{max = Max}) -> + lists:reverse(get_online_rooms(first, {<<"">>, Host}, Host, 0, Max, [])); +get_online_rooms(_ServerHost, Host, undefined) -> + mnesia:dirty_select( + muc_online_room, + ets:fun2ms( + fun(#muc_online_room{name_host = {Name, H}, pid = Pid}) + when H == Host -> {Name, Host, Pid} + end)). + +-spec get_online_rooms(prev | next | last | first, + {binary(), binary()}, binary(), + non_neg_integer(), non_neg_integer() | undefined, + [{binary(), binary(), pid()}]) -> + [{binary(), binary(), pid()}]. +get_online_rooms(_Action, _Key, _Host, Count, Max, Items) when Count >= Max -> + Items; +get_online_rooms(Action, Key, Host, Count, Max, Items) -> + Call = fun() -> + case Action of + prev -> mnesia:dirty_prev(muc_online_room, Key); + next -> mnesia:dirty_next(muc_online_room, Key); + last -> mnesia:dirty_last(muc_online_room); + first -> mnesia:dirty_first(muc_online_room) + end + end, + NewAction = case Action of + last -> prev; + first -> next; + _ -> Action + end, + try Call() of + '$end_of_table' -> + Items; + {Room, Host} = NewKey -> + case find_online_room(Room, Host) of + {ok, Pid} -> + get_online_rooms(NewAction, NewKey, Host, + Count + 1, Max, [{Room, Host, Pid}|Items]); + error -> + get_online_rooms(NewAction, NewKey, Host, + Count, Max, Items) + end; + NewKey -> + get_online_rooms(NewAction, NewKey, Host, Count, Max, Items) + catch _:{aborted, {badarg, _}} -> + Items + end. + +rsm_supported() -> + true. + +register_online_user(_ServerHost, {U, S, R}, Room, Host) -> + ets:insert(muc_online_users, + #muc_online_users{us = {U, S}, resource = R, + room = Room, host = Host}). + +unregister_online_user(_ServerHost, {U, S, R}, Room, Host) -> + ets:delete_object(muc_online_users, + #muc_online_users{us = {U, S}, resource = R, + room = Room, host = Host}). + +count_online_rooms_by_user(ServerHost, U, S) -> + MucHost = hd(gen_mod:get_module_opt_hosts(ServerHost, mod_muc)), + ets:select_count( + muc_online_users, + ets:fun2ms( + fun(#muc_online_users{us = {U1, S1}, host = Host}) -> + U == U1 andalso S == S1 andalso MucHost == Host + end)). + +get_online_rooms_by_user(ServerHost, U, S) -> + MucHost = hd(gen_mod:get_module_opt_hosts(ServerHost, mod_muc)), + ets:select( + muc_online_users, + ets:fun2ms( + fun(#muc_online_users{us = {U1, S1}, room = Room, host = Host}) + when U == U1 andalso S == S1 andalso MucHost == Host -> {Room, Host} + end)). + +import(_LServer, <<"muc_room">>, + [Name, RoomHost, SOpts, _TimeStamp]) -> + Opts = mod_muc:opts_to_binary(ejabberd_sql:decode_term(SOpts)), + mnesia:dirty_write( + #muc_room{name_host = {Name, RoomHost}, + opts = Opts}); +import(_LServer, <<"muc_registered">>, + [J, RoomHost, Nick, _TimeStamp]) -> + #jid{user = U, server = S} = jid:decode(J), + mnesia:dirty_write( + #muc_registered{us_host = {{U, S}, RoomHost}, + nick = Nick}). + +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== +init([_Host, Opts]) -> + MyHosts = mod_muc_opt:hosts(Opts), + case gen_mod:db_mod(Opts, mod_muc) of + ?MODULE -> + ejabberd_mnesia:create(?MODULE, muc_room, + [{disc_copies, [node()]}, + {attributes, + record_info(fields, muc_room)}]), + ejabberd_mnesia:create(?MODULE, muc_registered, + [{disc_copies, [node()]}, + {attributes, + record_info(fields, muc_registered)}, + {index, [nick]}]); + _ -> + ok + end, + case gen_mod:ram_db_mod(Opts, mod_muc) of + ?MODULE -> + ejabberd_mnesia:create(?MODULE, muc_online_room, + [{ram_copies, [node()]}, + {type, ordered_set}, + {attributes, record_info(fields, muc_online_room)}]), + catch ets:new(muc_online_users, [bag, named_table, public, {keypos, 2}]), + lists:foreach( + fun(MyHost) -> + clean_table_from_bad_node(node(), MyHost) + end, MyHosts), + mnesia:subscribe(system); + _ -> + ok + end, + {ok, #state{}}. + +handle_call(Request, From, State) -> + ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), + {noreply, State}. + +handle_cast(Msg, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Msg]), + {noreply, State}. + +handle_info({mnesia_system_event, {mnesia_down, Node}}, State) -> + clean_table_from_bad_node(Node), + {noreply, State}; +handle_info({mnesia_system_event, {mnesia_up, _Node}}, State) -> + {noreply, State}; +handle_info(Info, State) -> + ?WARNING_MSG("Unexpected info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +clean_table_from_bad_node(Node) -> + F = fun() -> + Es = mnesia:select( + muc_online_room, + [{#muc_online_room{pid = '$1', _ = '_'}, + [{'==', {node, '$1'}, Node}], + ['$_']}]), + lists:foreach(fun(E) -> + mnesia:delete_object(E) + end, Es) + end, + mnesia:async_dirty(F). + +clean_table_from_bad_node(Node, Host) -> + F = fun() -> + Es = mnesia:select( + muc_online_room, + [{#muc_online_room{pid = '$1', + name_host = {'_', Host}, + _ = '_'}, + [{'==', {node, '$1'}, Node}], + ['$_']}]), + lists:foreach(fun(E) -> + mnesia:delete_object(E) + end, Es) + end, + mnesia:async_dirty(F). + +need_transform({muc_room, {N, H}, _}) + when is_list(N) orelse is_list(H) -> + ?INFO_MSG("Mnesia table 'muc_room' will be converted to binary", []), + true; +need_transform({muc_room, {_N, _H}, Opts}) -> + case {lists:keymember(allow_private_messages, 1, Opts), + lists:keymember(hats_defs, 1, Opts)} of + {true, _} -> + ?INFO_MSG("Mnesia table 'muc_room' will be converted to allowpm", []), + true; + {false, false} -> + ?INFO_MSG("Mnesia table 'muc_room' will be converted to Hats 0.3.0", []), + true; + {false, true} -> + false + end; + +need_transform({muc_registered, {{U, S}, H}, Nick}) + when is_list(U) orelse is_list(S) orelse is_list(H) orelse is_list(Nick) -> + ?INFO_MSG("Mnesia table 'muc_registered' will be converted to binary", []), + true; +need_transform(_) -> + false. + +transform({muc_room, {N, H}, Opts} = R) + when is_list(N) orelse is_list(H) -> + R#muc_room{name_host = {iolist_to_binary(N), iolist_to_binary(H)}, + opts = mod_muc:opts_to_binary(Opts)}; +transform(#muc_room{opts = Opts} = R) -> + Opts2 = case lists:keyfind(allow_private_messages, 1, Opts) of + {_, Value} when is_boolean(Value) -> + Value2 = case Value of + true -> anyone; + false -> none + end, + lists:keyreplace(allow_private_messages, 1, Opts, {allowpm, Value2}); + _ -> + Opts + end, + Opts4 = + case lists:keyfind(hats_defs, 1, Opts2) of + false -> + {hats_users, HatsUsers} = lists:keyfind(hats_users, 1, Opts2), + {HatsDefs, HatsUsers2} = + lists:foldl(fun({Jid, UriTitleList}, {Defs, Assigns}) -> + Defs2 = + lists:foldl(fun({Uri, Title}, AccDef) -> + maps:put(Uri, {Title, <<"">>}, AccDef) + end, + Defs, + UriTitleList), + Assigns2 = + maps:put(Jid, + [Uri || {Uri, _Title} <- UriTitleList], + Assigns), + {Defs2, Assigns2} + end, + {maps:new(), maps:new()}, + HatsUsers), + Opts3 = + lists:keyreplace(hats_users, 1, Opts2, {hats_users, maps:to_list(HatsUsers2)}), + [{hats_defs, maps:to_list(HatsDefs)} | Opts3]; + {_, _} -> + Opts2 + end, + R#muc_room{opts = Opts4}; +transform(#muc_registered{us_host = {{U, S}, H}, nick = Nick} = R) -> + R#muc_registered{us_host = {{iolist_to_binary(U), iolist_to_binary(S)}, + iolist_to_binary(H)}, + nick = iolist_to_binary(Nick)}. diff --git a/src/mod_muc_occupantid.erl b/src/mod_muc_occupantid.erl new file mode 100644 index 000000000..1e8eabee2 --- /dev/null +++ b/src/mod_muc_occupantid.erl @@ -0,0 +1,128 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_muc_occupantid.erl +%%% Author : Badlop +%%% Purpose : Add Occupant Ids to stanzas in anonymous MUC rooms (XEP-0421) +%%% Created : +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_muc_occupantid). + +-author('badlop@process-one.net'). + +-protocol({xep, 421, '0.1.0', '23.10', "complete", ""}). + +-behaviour(gen_mod). + +-include_lib("xmpp/include/xmpp.hrl"). +-include("logger.hrl"). +-include("translate.hrl"). +-include("mod_muc_room.hrl"). + +-export([start/2, stop/1, + mod_options/1, mod_doc/0, depends/2]). +-export([filter_packet/3, remove_room/3]). + +%%% +%%% gen_mod +%%% + +start(_Host, _Opts) -> + create_table(), + {ok, [{hook, muc_filter_presence, filter_packet, 10}, + {hook, muc_filter_message, filter_packet, 10}, + {hook, remove_room, remove_room, 50}]}. + +stop(_Host) -> + ok. + +%%% +%%% Hooks +%%% + +filter_packet(Packet, State, _Nick) -> + add_occupantid_packet(Packet, State#state.jid). + +remove_room(_LServer, Name, Host) -> + delete_salt(jid:make(Name, Host)). + +%%% +%%% XEP-0421 Occupant-id +%%% + +add_occupantid_packet(Packet, RoomJid) -> + From = xmpp:get_from(Packet), + OccupantId = calculate_occupantid(From, RoomJid), + OccupantElement = #occupant_id{id = OccupantId}, + xmpp:append_subtags(xmpp:remove_subtag(Packet, OccupantElement), [OccupantElement]). + +calculate_occupantid(From, RoomJid) -> + Term = {jid:remove_resource(From), get_salt(RoomJid)}, + misc:term_to_base64(crypto:hash(sha256, io_lib:format("~p", [Term]))). + +%%% +%%% Table storing rooms' salt +%%% + +-record(muc_occupant_id, {room_jid, salt}). + +create_table() -> + ejabberd_mnesia:create(?MODULE, muc_occupant_id, + [{ram_copies, [node()]}, + {local_content, true}, + {attributes, record_info(fields, muc_occupant_id)}, + {type, set}]). + + +get_salt(RoomJid) -> + case mnesia:dirty_read(muc_occupant_id, RoomJid) of + [] -> + Salt = p1_rand:get_string(), + ok = write_salt(RoomJid, Salt), + Salt; + [#muc_occupant_id{salt = Salt}] -> + Salt + end. + +write_salt(RoomJid, Salt) -> + mnesia:dirty_write(#muc_occupant_id{room_jid = RoomJid, salt = Salt}). + +delete_salt(RoomJid) -> + mnesia:dirty_delete(muc_occupant_id, RoomJid). + +%%% +%%% Doc +%%% + +mod_options(_Host) -> + []. + +mod_doc() -> + #{desc => + [?T("This module implements " + "https://xmpp.org/extensions/xep-0421.html" + "[XEP-0421: Anonymous unique occupant identifiers for MUCs]."), "", + ?T("When the module is enabled, the feature is enabled " + "in all semi-anonymous rooms.")], + note => "added in 23.10" + }. + +depends(_, _) -> + [{mod_muc, hard}]. diff --git a/src/mod_muc_opt.erl b/src/mod_muc_opt.erl new file mode 100644 index 000000000..d4550da1a --- /dev/null +++ b/src/mod_muc_opt.erl @@ -0,0 +1,244 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_muc_opt). + +-export([access/1]). +-export([access_admin/1]). +-export([access_create/1]). +-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]). +-export([history_size/1]). +-export([host/1]). +-export([hosts/1]). +-export([max_captcha_whitelist/1]). +-export([max_password/1]). +-export([max_room_desc/1]). +-export([max_room_id/1]). +-export([max_room_name/1]). +-export([max_rooms_discoitems/1]). +-export([max_user_conferences/1]). +-export([max_users/1]). +-export([max_users_admin_threshold/1]). +-export([max_users_presence/1]). +-export([min_message_interval/1]). +-export([min_presence_interval/1]). +-export([name/1]). +-export([preload_rooms/1]). +-export([queue_type/1]). +-export([ram_db_type/1]). +-export([regexp_room_id/1]). +-export([room_shaper/1]). +-export([user_message_shaper/1]). +-export([user_presence_shaper/1]). +-export([vcard/1]). + +-spec access(gen_mod:opts() | global | binary()) -> 'all' | acl:acl(). +access(Opts) when is_map(Opts) -> + gen_mod:get_opt(access, Opts); +access(Host) -> + gen_mod:get_module_opt(Host, mod_muc, access). + +-spec access_admin(gen_mod:opts() | global | binary()) -> 'none' | acl:acl(). +access_admin(Opts) when is_map(Opts) -> + gen_mod:get_opt(access_admin, Opts); +access_admin(Host) -> + gen_mod:get_module_opt(Host, mod_muc, access_admin). + +-spec access_create(gen_mod:opts() | global | binary()) -> 'all' | acl:acl(). +access_create(Opts) when is_map(Opts) -> + gen_mod:get_opt(access_create, Opts); +access_create(Host) -> + gen_mod:get_module_opt(Host, mod_muc, access_create). + +-spec access_mam(gen_mod:opts() | global | binary()) -> 'all' | acl:acl(). +access_mam(Opts) when is_map(Opts) -> + gen_mod:get_opt(access_mam, Opts); +access_mam(Host) -> + gen_mod:get_module_opt(Host, mod_muc, access_mam). + +-spec access_persistent(gen_mod:opts() | global | binary()) -> 'all' | acl:acl(). +access_persistent(Opts) when is_map(Opts) -> + gen_mod:get_opt(access_persistent, Opts); +access_persistent(Host) -> + gen_mod:get_module_opt(Host, mod_muc, access_persistent). + +-spec access_register(gen_mod:opts() | global | binary()) -> 'all' | acl:acl(). +access_register(Opts) when is_map(Opts) -> + gen_mod:get_opt(access_register, 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); +db_type(Host) -> + gen_mod:get_module_opt(Host, mod_muc, db_type). + +-spec default_room_options(gen_mod:opts() | global | binary()) -> [{atom(),'anyone' | 'false' | 'moderators' | 'nobody' | 'none' | 'participants' | 'true' | 'undefined' | binary() | ['moderator' | 'participant' | 'visitor'] | pos_integer() | tuple()}]. +default_room_options(Opts) when is_map(Opts) -> + gen_mod:get_opt(default_room_options, Opts); +default_room_options(Host) -> + gen_mod:get_module_opt(Host, mod_muc, default_room_options). + +-spec hibernation_timeout(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +hibernation_timeout(Opts) when is_map(Opts) -> + gen_mod:get_opt(hibernation_timeout, Opts); +hibernation_timeout(Host) -> + gen_mod:get_module_opt(Host, mod_muc, hibernation_timeout). + +-spec history_size(gen_mod:opts() | global | binary()) -> non_neg_integer(). +history_size(Opts) when is_map(Opts) -> + gen_mod:get_opt(history_size, Opts); +history_size(Host) -> + gen_mod:get_module_opt(Host, mod_muc, history_size). + +-spec host(gen_mod:opts() | global | binary()) -> binary(). +host(Opts) when is_map(Opts) -> + gen_mod:get_opt(host, Opts); +host(Host) -> + gen_mod:get_module_opt(Host, mod_muc, host). + +-spec hosts(gen_mod:opts() | global | binary()) -> [binary()]. +hosts(Opts) when is_map(Opts) -> + gen_mod:get_opt(hosts, Opts); +hosts(Host) -> + gen_mod:get_module_opt(Host, mod_muc, hosts). + +-spec max_captcha_whitelist(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +max_captcha_whitelist(Opts) when is_map(Opts) -> + gen_mod:get_opt(max_captcha_whitelist, Opts); +max_captcha_whitelist(Host) -> + gen_mod:get_module_opt(Host, mod_muc, max_captcha_whitelist). + +-spec max_password(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +max_password(Opts) when is_map(Opts) -> + gen_mod:get_opt(max_password, Opts); +max_password(Host) -> + gen_mod:get_module_opt(Host, mod_muc, max_password). + +-spec max_room_desc(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +max_room_desc(Opts) when is_map(Opts) -> + gen_mod:get_opt(max_room_desc, Opts); +max_room_desc(Host) -> + gen_mod:get_module_opt(Host, mod_muc, max_room_desc). + +-spec max_room_id(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +max_room_id(Opts) when is_map(Opts) -> + gen_mod:get_opt(max_room_id, Opts); +max_room_id(Host) -> + gen_mod:get_module_opt(Host, mod_muc, max_room_id). + +-spec max_room_name(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +max_room_name(Opts) when is_map(Opts) -> + gen_mod:get_opt(max_room_name, Opts); +max_room_name(Host) -> + gen_mod:get_module_opt(Host, mod_muc, max_room_name). + +-spec max_rooms_discoitems(gen_mod:opts() | global | binary()) -> non_neg_integer(). +max_rooms_discoitems(Opts) when is_map(Opts) -> + gen_mod:get_opt(max_rooms_discoitems, Opts); +max_rooms_discoitems(Host) -> + gen_mod:get_module_opt(Host, mod_muc, max_rooms_discoitems). + +-spec max_user_conferences(gen_mod:opts() | global | binary()) -> pos_integer(). +max_user_conferences(Opts) when is_map(Opts) -> + gen_mod:get_opt(max_user_conferences, Opts); +max_user_conferences(Host) -> + gen_mod:get_module_opt(Host, mod_muc, max_user_conferences). + +-spec max_users(gen_mod:opts() | global | binary()) -> pos_integer(). +max_users(Opts) when is_map(Opts) -> + gen_mod:get_opt(max_users, Opts); +max_users(Host) -> + gen_mod:get_module_opt(Host, mod_muc, max_users). + +-spec max_users_admin_threshold(gen_mod:opts() | global | binary()) -> pos_integer(). +max_users_admin_threshold(Opts) when is_map(Opts) -> + gen_mod:get_opt(max_users_admin_threshold, Opts); +max_users_admin_threshold(Host) -> + gen_mod:get_module_opt(Host, mod_muc, max_users_admin_threshold). + +-spec max_users_presence(gen_mod:opts() | global | binary()) -> integer(). +max_users_presence(Opts) when is_map(Opts) -> + gen_mod:get_opt(max_users_presence, Opts); +max_users_presence(Host) -> + gen_mod:get_module_opt(Host, mod_muc, max_users_presence). + +-spec min_message_interval(gen_mod:opts() | global | binary()) -> number(). +min_message_interval(Opts) when is_map(Opts) -> + gen_mod:get_opt(min_message_interval, Opts); +min_message_interval(Host) -> + gen_mod:get_module_opt(Host, mod_muc, min_message_interval). + +-spec min_presence_interval(gen_mod:opts() | global | binary()) -> number(). +min_presence_interval(Opts) when is_map(Opts) -> + gen_mod:get_opt(min_presence_interval, Opts); +min_presence_interval(Host) -> + gen_mod:get_module_opt(Host, mod_muc, min_presence_interval). + +-spec name(gen_mod:opts() | global | binary()) -> binary(). +name(Opts) when is_map(Opts) -> + gen_mod:get_opt(name, Opts); +name(Host) -> + gen_mod:get_module_opt(Host, mod_muc, name). + +-spec preload_rooms(gen_mod:opts() | global | binary()) -> boolean(). +preload_rooms(Opts) when is_map(Opts) -> + gen_mod:get_opt(preload_rooms, Opts); +preload_rooms(Host) -> + gen_mod:get_module_opt(Host, mod_muc, preload_rooms). + +-spec queue_type(gen_mod:opts() | global | binary()) -> 'file' | 'ram'. +queue_type(Opts) when is_map(Opts) -> + gen_mod:get_opt(queue_type, Opts); +queue_type(Host) -> + gen_mod:get_module_opt(Host, mod_muc, queue_type). + +-spec ram_db_type(gen_mod:opts() | global | binary()) -> atom(). +ram_db_type(Opts) when is_map(Opts) -> + gen_mod:get_opt(ram_db_type, Opts); +ram_db_type(Host) -> + gen_mod:get_module_opt(Host, mod_muc, ram_db_type). + +-spec regexp_room_id(gen_mod:opts() | global | binary()) -> <<>> | misc:re_mp(). +regexp_room_id(Opts) when is_map(Opts) -> + gen_mod:get_opt(regexp_room_id, Opts); +regexp_room_id(Host) -> + gen_mod:get_module_opt(Host, mod_muc, regexp_room_id). + +-spec room_shaper(gen_mod:opts() | global | binary()) -> atom(). +room_shaper(Opts) when is_map(Opts) -> + gen_mod:get_opt(room_shaper, Opts); +room_shaper(Host) -> + gen_mod:get_module_opt(Host, mod_muc, room_shaper). + +-spec user_message_shaper(gen_mod:opts() | global | binary()) -> atom(). +user_message_shaper(Opts) when is_map(Opts) -> + gen_mod:get_opt(user_message_shaper, Opts); +user_message_shaper(Host) -> + gen_mod:get_module_opt(Host, mod_muc, user_message_shaper). + +-spec user_presence_shaper(gen_mod:opts() | global | binary()) -> atom(). +user_presence_shaper(Opts) when is_map(Opts) -> + gen_mod:get_opt(user_presence_shaper, Opts); +user_presence_shaper(Host) -> + gen_mod:get_module_opt(Host, mod_muc, user_presence_shaper). + +-spec vcard(gen_mod:opts() | global | binary()) -> 'undefined' | tuple(). +vcard(Opts) when is_map(Opts) -> + gen_mod:get_opt(vcard, Opts); +vcard(Host) -> + gen_mod:get_module_opt(Host, mod_muc, vcard). + diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl index 58ac2610b..64c91e4c0 100644 --- a/src/mod_muc_room.erl +++ b/src/mod_muc_room.erl @@ -5,7 +5,7 @@ %%% Created : 19 Mar 2003 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -27,14 +27,39 @@ -author('alexey@process-one.net'). --behaviour(gen_fsm). +-protocol({xep, 317, '0.3.1', '21.12', "complete", "0.3.1 since 25.xx"}). +-protocol({xep, 410, '1.1.0', '18.12', "complete", ""}). + +-behaviour(p1_fsm). %% External exports --export([start_link/9, - start_link/7, - start/9, - start/7, - route/4]). +-export([start_link/10, + start_link/8, + start/10, + start/8, + supervisor/1, + get_role/2, + get_affiliation/2, + is_occupant_or_admin/2, + route/2, + expand_opts/1, + config_fields/0, + destroy/1, + destroy/2, + shutdown/1, + get_config/1, + set_config/2, + get_state/1, + get_info/1, + change_item/5, + change_item_async/5, + config_reloaded/1, + subscribe/4, + unsubscribe/2, + is_subscribed/2, + get_subscribers/1, + service_message/2, + get_disco_item/4]). %% gen_fsm callbacks -export([init/1, @@ -45,16 +70,27 @@ terminate/3, code_change/4]). --include("ejabberd.hrl"). -include("logger.hrl"). - --include("jlib.hrl"). - +-include_lib("xmpp/include/xmpp.hrl"). +-include("translate.hrl"). -include("mod_muc_room.hrl"). + -define(MAX_USERS_DEFAULT_LIST, [5, 10, 20, 30, 50, 100, 200, 500, 1000, 2000, 5000]). +-define(MUC_HAT_CREATE_CMD, <<"urn:xmpp:hats:commands:create">>). +-define(MUC_HAT_DESTROY_CMD, <<"urn:xmpp:hats:commands:destroy">>). +-define(MUC_HAT_LISTHATS_CMD, <<"urn:xmpp:hats:commands:list">>). + +-define(MUC_HAT_ASSIGN_CMD, <<"urn:xmpp:hats:commands:assign">>). +-define(MUC_HAT_UNASSIGN_CMD, <<"urn:xmpp:hats:commands:unassign">>). +-define(MUC_HAT_LISTUSERS_CMD,<<"urn:xmpp:hats:commands:list-assigned">>). + +-define(MAX_HATS_USERS, 100). +-define(MAX_HATS_PER_USER, 10). +-define(CLEAN_ROOM_TIMEOUT, 30000). + %-define(DBGFSM, true). -ifdef(DBGFSM). @@ -67,677 +103,690 @@ -endif. -%% Module start with or without supervisor: --ifdef(NO_TRANSIENT_SUPERVISORS). --define(SUPERVISOR_START, - gen_fsm:start(?MODULE, [Host, ServerHost, Access, Room, HistorySize, - RoomShaper, Creator, Nick, DefRoomOpts], - ?FSMOPTS)). --else. --define(SUPERVISOR_START, - Supervisor = gen_mod:get_module_proc(ServerHost, ejabberd_mod_muc_sup), - supervisor:start_child( - Supervisor, [Host, ServerHost, Access, Room, HistorySize, RoomShaper, - Creator, Nick, DefRoomOpts])). +-type state() :: #state{}. +-type fsm_stop() :: {stop, normal, state()}. +-type fsm_next() :: {next_state, normal_state, state()}. +-type fsm_transition() :: fsm_stop() | fsm_next(). +-type disco_item_filter() :: only_non_empty | all | non_neg_integer(). +-type admin_action() :: {jid(), affiliation | role, affiliation() | role(), binary()}. +-export_type([state/0, disco_item_filter/0]). + +-callback set_affiliation(binary(), binary(), binary(), jid(), affiliation(), + binary()) -> ok | {error, any()}. +-callback set_affiliations(binary(), binary(), binary(), + affiliations()) -> ok | {error, any()}. +-callback get_affiliation(binary(), binary(), binary(), + binary(), binary()) -> {ok, affiliation()} | {error, any()}. +-callback get_affiliations(binary(), binary(), binary()) -> {ok, affiliations()} | {error, any()}. +-callback search_affiliation(binary(), binary(), binary(), affiliation()) -> + {ok, [{ljid(), {affiliation(), binary()}}]} | {error, any()}. + +-ifndef(OTP_BELOW_28). +-dialyzer([no_opaque_union]). -endif. %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- +-spec start(binary(), binary(), mod_muc:access(), binary(), non_neg_integer(), + atom(), jid(), binary(), [{atom(), term()}], ram | file) -> + {ok, pid()} | {error, any()}. start(Host, ServerHost, Access, Room, HistorySize, RoomShaper, - Creator, Nick, DefRoomOpts) -> - ?SUPERVISOR_START. - -start(Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts) -> - Supervisor = gen_mod:get_module_proc(ServerHost, ejabberd_mod_muc_sup), + Creator, Nick, DefRoomOpts, QueueType) -> supervisor:start_child( - Supervisor, [Host, ServerHost, Access, Room, HistorySize, RoomShaper, - Opts]). + supervisor(ServerHost), + [Host, ServerHost, Access, Room, HistorySize, + RoomShaper, Creator, Nick, DefRoomOpts, QueueType]). +-spec start(binary(), binary(), mod_muc:access(), binary(), non_neg_integer(), + atom(), [{atom(), term()}], ram | file) -> + {ok, pid()} | {error, any()}. +start(Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts, QueueType) -> + supervisor:start_child( + supervisor(ServerHost), + [Host, ServerHost, Access, Room, HistorySize, + RoomShaper, Opts, QueueType]). + +-spec start_link(binary(), binary(), mod_muc:access(), binary(), non_neg_integer(), + atom(), jid(), binary(), [{atom(), term()}], ram | file) -> + {ok, pid()} | {error, any()}. start_link(Host, ServerHost, Access, Room, HistorySize, RoomShaper, - Creator, Nick, DefRoomOpts) -> - gen_fsm:start_link(?MODULE, [Host, ServerHost, Access, Room, HistorySize, - RoomShaper, Creator, Nick, DefRoomOpts], + Creator, Nick, DefRoomOpts, QueueType) -> + p1_fsm:start_link(?MODULE, [Host, ServerHost, Access, Room, HistorySize, + RoomShaper, Creator, Nick, DefRoomOpts, QueueType], ?FSMOPTS). -start_link(Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts) -> - gen_fsm:start_link(?MODULE, [Host, ServerHost, Access, Room, HistorySize, - RoomShaper, Opts], +-spec start_link(binary(), binary(), mod_muc:access(), binary(), non_neg_integer(), + atom(), [{atom(), term()}], ram | file) -> + {ok, pid()} | {error, any()}. +start_link(Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts, QueueType) -> + p1_fsm:start_link(?MODULE, [Host, ServerHost, Access, Room, HistorySize, + RoomShaper, Opts, QueueType], ?FSMOPTS). +-spec supervisor(binary()) -> atom(). +supervisor(Host) -> + gen_mod:get_module_proc(Host, mod_muc_room_sup). + +-spec destroy(pid()) -> ok. +destroy(Pid) -> + p1_fsm:send_all_state_event(Pid, destroy). + +-spec destroy(pid(), binary()) -> ok. +destroy(Pid, Reason) -> + p1_fsm:send_all_state_event(Pid, {destroy, Reason}). + +-spec shutdown(pid()) -> boolean(). +shutdown(Pid) -> + ejabberd_cluster:send(Pid, shutdown). + +-spec config_reloaded(pid()) -> boolean(). +config_reloaded(Pid) -> + ejabberd_cluster:send(Pid, config_reloaded). + +-spec get_config(pid()) -> {ok, config()} | {error, notfound | timeout}. +get_config(Pid) -> + try p1_fsm:sync_send_all_state_event(Pid, get_config) + catch _:{timeout, {p1_fsm, _, _}} -> + {error, timeout}; + _:{_, {p1_fsm, _, _}} -> + {error, notfound} + end. + +-spec set_config(pid(), config()) -> {ok, config()} | {error, notfound | timeout}. +set_config(Pid, Config) -> + try p1_fsm:sync_send_all_state_event(Pid, {change_config, Config}) + catch _:{timeout, {p1_fsm, _, _}} -> + {error, timeout}; + _:{_, {p1_fsm, _, _}} -> + {error, notfound} + end. + +-spec change_item(pid(), jid(), affiliation | role, affiliation() | role(), binary()) -> + {ok, state()} | {error, notfound | timeout}. +change_item(Pid, JID, Type, AffiliationOrRole, Reason) -> + try p1_fsm:sync_send_all_state_event( + Pid, {process_item_change, {JID, Type, AffiliationOrRole, Reason}, undefined}) + catch _:{timeout, {p1_fsm, _, _}} -> + {error, timeout}; + _:{_, {p1_fsm, _, _}} -> + {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) + catch _:{timeout, {p1_fsm, _, _}} -> + {error, timeout}; + _:{_, {p1_fsm, _, _}} -> + {error, notfound} + end. + +-spec get_info(pid()) -> {ok, #{occupants_number => integer()}} | + {error, notfound | timeout}. +get_info(Pid) -> + try + {ok, p1_fsm:sync_send_all_state_event(Pid, get_info)} + catch _:{timeout, {p1_fsm, _, _}} -> + {error, timeout}; + _:{_, {p1_fsm, _, _}} -> + {error, notfound} + end. + +-spec subscribe(pid(), jid(), binary(), [binary()]) -> {ok, [binary()]} | {error, binary()}. +subscribe(Pid, JID, Nick, Nodes) -> + try p1_fsm:sync_send_all_state_event(Pid, {muc_subscribe, JID, Nick, Nodes}) + catch _:{timeout, {p1_fsm, _, _}} -> + {error, ?T("Request has timed out")}; + _:{_, {p1_fsm, _, _}} -> + {error, ?T("Conference room does not exist")} + end. + +-spec unsubscribe(pid(), jid()) -> ok | {error, binary()}. +unsubscribe(Pid, JID) -> + try p1_fsm:sync_send_all_state_event(Pid, {muc_unsubscribe, JID}) + catch _:{timeout, {p1_fsm, _, _}} -> + {error, ?T("Request has timed out")}; + exit:{normal, {p1_fsm, _, _}} -> + ok; + _:{_, {p1_fsm, _, _}} -> + {error, ?T("Conference room does not exist")} + end. + +-spec is_subscribed(pid(), jid()) -> {true, binary(), [binary()]} | false. +is_subscribed(Pid, JID) -> + try p1_fsm:sync_send_all_state_event(Pid, {is_subscribed, JID}) + catch _:{_, {p1_fsm, _, _}} -> false + end. + +-spec get_subscribers(pid()) -> {ok, [jid()]} | {error, notfound | timeout}. +get_subscribers(Pid) -> + try p1_fsm:sync_send_all_state_event(Pid, get_subscribers) + catch _:{timeout, {p1_fsm, _, _}} -> + {error, timeout}; + _:{_, {p1_fsm, _, _}} -> + {error, notfound} + end. + +-spec service_message(pid(), binary()) -> ok. +service_message(Pid, Text) -> + p1_fsm:send_all_state_event(Pid, {service_message, Text}). + +-spec get_disco_item(pid(), disco_item_filter(), jid(), binary()) -> + {ok, binary()} | {error, notfound | timeout}. +get_disco_item(Pid, Filter, JID, Lang) -> + Timeout = 100, + Time = erlang:system_time(millisecond), + Query = {get_disco_item, Filter, JID, Lang, Time+Timeout}, + try p1_fsm:sync_send_all_state_event(Pid, Query, Timeout) of + {item, Desc} -> + {ok, Desc}; + false -> + {error, notfound} + catch _:{timeout, {p1_fsm, _, _}} -> + {error, timeout}; + _:{_, {p1_fsm, _, _}} -> + {error, notfound} + end. + %%%---------------------------------------------------------------------- %%% Callback functions from gen_fsm %%%---------------------------------------------------------------------- -%%---------------------------------------------------------------------- -%% Func: init/1 -%% Returns: {ok, StateName, StateData} | -%% {ok, StateName, StateData, Timeout} | -%% ignore | -%% {stop, StopReason} -%%---------------------------------------------------------------------- -init([Host, ServerHost, Access, Room, HistorySize, RoomShaper, Creator, _Nick, DefRoomOpts]) -> +init([Host, ServerHost, Access, Room, HistorySize, + RoomShaper, Creator, _Nick, DefRoomOpts, QueueType]) -> process_flag(trap_exit, true), - Shaper = shaper:new(RoomShaper), - State = set_affiliation(Creator, owner, - #state{host = Host, server_host = ServerHost, - access = Access, room = Room, - history = lqueue_new(HistorySize), - jid = jlib:make_jid(Room, Host, <<"">>), - just_created = true, - room_shaper = Shaper}), - State1 = set_opts(DefRoomOpts, State), - if (State1#state.config)#config.persistent -> - mod_muc:store_room(State1#state.server_host, - State1#state.host, - State1#state.room, - make_opts(State1)); - true -> ok - end, - ?INFO_MSG("Created MUC room ~s@~s by ~s", - [Room, Host, jlib:jid_to_string(Creator)]), + misc:set_proc_label({?MODULE, Room, Host}), + Shaper = ejabberd_shaper:new(RoomShaper), + RoomQueue = room_queue_new(ServerHost, Shaper, QueueType), + State = set_opts(DefRoomOpts, + #state{host = Host, server_host = ServerHost, + access = Access, room = Room, + history = lqueue_new(HistorySize, QueueType), + jid = jid:make(Room, Host), + just_created = true, + room_queue = RoomQueue, + room_shaper = Shaper}), + State1 = set_affiliation(Creator, owner, State), + store_room(State1), + ?INFO_MSG("Created MUC room ~ts@~ts by ~ts", + [Room, Host, jid:encode(Creator)]), add_to_log(room_existence, created, State1), add_to_log(room_existence, started, State1), - {ok, normal_state, State1}; -init([Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts]) -> + ejabberd_hooks:run(start_room, ServerHost, [ServerHost, Room, Host]), + erlang:send_after(?CLEAN_ROOM_TIMEOUT, self(), + close_room_if_temporary_and_empty), + {ok, normal_state, reset_hibernate_timer(State1)}; +init([Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts, QueueType]) -> process_flag(trap_exit, true), - Shaper = shaper:new(RoomShaper), + misc:set_proc_label({?MODULE, Room, Host}), + Shaper = ejabberd_shaper:new(RoomShaper), + RoomQueue = room_queue_new(ServerHost, Shaper, QueueType), + Jid = jid:make(Room, Host), State = set_opts(Opts, #state{host = Host, server_host = ServerHost, access = Access, room = Room, - history = lqueue_new(HistorySize), - jid = jlib:make_jid(Room, Host, <<"">>), + history = lqueue_new(HistorySize, QueueType), + jid = Jid, + room_queue = RoomQueue, room_shaper = Shaper}), add_to_log(room_existence, started, State), - {ok, normal_state, State}. + ejabberd_hooks:run(start_room, ServerHost, [ServerHost, Room, Host]), + State1 = cleanup_affiliations(State), + State2 = + case {lists:keyfind(hibernation_time, 1, Opts), + (State1#state.config)#config.mam, + (State1#state.history)#lqueue.max} of + {{_, V}, true, L} when is_integer(V), L > 0 -> + {Msgs, _, _} = mod_mam:select(ServerHost, Jid, Jid, [], + #rsm_set{max = L, before = <<"9999999999999999">>}, + groupchat, only_messages), + Hist2 = + lists:foldl( + fun({_, TS, #forwarded{sub_els = [#message{meta = #{archive_nick := Nick}} = Msg]}}, Hist) -> + Pkt = xmpp:set_from_to(Msg, jid:replace_resource(Jid, Nick), Jid), + Size = element_size(Pkt), + lqueue_in({Nick, Pkt, false, misc:usec_to_now(TS), Size}, Hist) + end, State1#state.history, Msgs), + State1#state{history = Hist2}; + _ -> + State1 + end, + erlang:send_after(?CLEAN_ROOM_TIMEOUT, self(), + close_room_if_temporary_and_empty), + {ok, normal_state, reset_hibernate_timer(State2)}. -%%---------------------------------------------------------------------- -%% Func: StateName/2 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} -%%---------------------------------------------------------------------- -normal_state({route, From, <<"">>, - #xmlel{name = <<"message">>, attrs = Attrs, - children = Els} = - Packet}, +normal_state({route, <<"">>, + #message{from = From, type = Type, lang = Lang} = Packet}, StateData) -> - Lang = xml:get_attr_s(<<"xml:lang">>, Attrs), case is_user_online(From, StateData) orelse - is_user_allowed_message_nonparticipant(From, StateData) - of - true -> - case xml:get_attr_s(<<"type">>, Attrs) of - <<"groupchat">> -> - Activity = get_user_activity(From, StateData), - Now = now_to_usec(now()), - MinMessageInterval = - trunc(gen_mod:get_module_opt(StateData#state.server_host, - mod_muc, min_message_interval, fun(MMI) when is_number(MMI) -> MMI end, 0) - * 1000000), - Size = element_size(Packet), - {MessageShaper, MessageShaperInterval} = - shaper:update(Activity#activity.message_shaper, Size), - if Activity#activity.message /= undefined -> - ErrText = <<"Traffic rate limit is exceeded">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_RESOURCE_CONSTRAINT(Lang, - ErrText)), - ejabberd_router:route(StateData#state.jid, From, Err), - {next_state, normal_state, StateData}; - Now >= - Activity#activity.message_time + MinMessageInterval, - MessageShaperInterval == 0 -> - {RoomShaper, RoomShaperInterval} = - shaper:update(StateData#state.room_shaper, Size), - RoomQueueEmpty = - queue:is_empty(StateData#state.room_queue), - if RoomShaperInterval == 0, RoomQueueEmpty -> - NewActivity = Activity#activity{message_time = - Now, - message_shaper = - MessageShaper}, - StateData1 = store_user_activity(From, - NewActivity, - StateData), - StateData2 = StateData1#state{room_shaper = - RoomShaper}, - process_groupchat_message(From, Packet, - StateData2); - true -> - StateData1 = if RoomQueueEmpty -> - erlang:send_after(RoomShaperInterval, - self(), - process_room_queue), - StateData#state{room_shaper = - RoomShaper}; - true -> StateData - end, - NewActivity = Activity#activity{message_time = - Now, - message_shaper = - MessageShaper, - message = Packet}, - RoomQueue = queue:in({message, From}, - StateData#state.room_queue), - StateData2 = store_user_activity(From, - NewActivity, - StateData1), - StateData3 = StateData2#state{room_queue = - RoomQueue}, - {next_state, normal_state, StateData3} - end; - true -> - MessageInterval = (Activity#activity.message_time + - MinMessageInterval - - Now) - div 1000, - Interval = lists:max([MessageInterval, - MessageShaperInterval]), - erlang:send_after(Interval, self(), - {process_user_message, From}), - NewActivity = Activity#activity{message = Packet, - message_shaper = - MessageShaper}, - StateData1 = store_user_activity(From, NewActivity, - StateData), - {next_state, normal_state, StateData1} - end; - <<"error">> -> - case is_user_online(From, StateData) of - true -> - ErrorText = <<"This participant is kicked from the " - "room because he sent an error message">>, - NewState = expulse_participant(Packet, From, StateData, - translate:translate(Lang, - ErrorText)), - close_room_if_temporary_and_empty(NewState); - _ -> {next_state, normal_state, StateData} - end; - <<"chat">> -> - ErrText = - <<"It is not allowed to send private messages " - "to the conference">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_NOT_ACCEPTABLE(Lang, - ErrText)), - ejabberd_router:route(StateData#state.jid, From, Err), - {next_state, normal_state, StateData}; - Type when (Type == <<"">>) or (Type == <<"normal">>) -> - IsInvitation = is_invitation(Els), - IsVoiceRequest = is_voice_request(Els) and - is_visitor(From, StateData), - IsVoiceApprovement = is_voice_approvement(Els) and - not is_visitor(From, StateData), - if IsInvitation -> - case catch check_invitation(From, Els, Lang, StateData) - of - {error, Error} -> - Err = jlib:make_error_reply(Packet, Error), - ejabberd_router:route(StateData#state.jid, From, Err), - {next_state, normal_state, StateData}; - IJID -> - Config = StateData#state.config, - case Config#config.members_only of - true -> - case get_affiliation(IJID, StateData) of - none -> - NSD = set_affiliation(IJID, member, - StateData), - case - (NSD#state.config)#config.persistent - of - true -> - mod_muc:store_room(NSD#state.server_host, - NSD#state.host, - NSD#state.room, - make_opts(NSD)); - _ -> ok + is_subscriber(From, StateData) orelse + is_user_allowed_message_nonparticipant(From, StateData) of + true when Type == groupchat -> + Activity = get_user_activity(From, StateData), + Now = erlang:system_time(microsecond), + MinMessageInterval = trunc(mod_muc_opt:min_message_interval(StateData#state.server_host) * 1000000), + Size = element_size(Packet), + {MessageShaper, MessageShaperInterval} = + ejabberd_shaper:update(Activity#activity.message_shaper, Size), + if Activity#activity.message /= undefined -> + ErrText = ?T("Traffic rate limit is exceeded"), + Err = xmpp:err_resource_constraint(ErrText, Lang), + ejabberd_router:route_error(Packet, Err), + {next_state, normal_state, StateData}; + Now >= Activity#activity.message_time + MinMessageInterval, + MessageShaperInterval == 0 -> + {RoomShaper, RoomShaperInterval} = + ejabberd_shaper:update(StateData#state.room_shaper, Size), + RoomQueueEmpty = case StateData#state.room_queue of + undefined -> true; + RQ -> p1_queue:is_empty(RQ) + end, + if RoomShaperInterval == 0, RoomQueueEmpty -> + NewActivity = Activity#activity{ + message_time = Now, + message_shaper = MessageShaper}, + StateData1 = store_user_activity(From, + NewActivity, + StateData), + StateData2 = StateData1#state{room_shaper = + RoomShaper}, + process_groupchat_message(Packet, + StateData2); + true -> + StateData1 = if RoomQueueEmpty -> + erlang:send_after(RoomShaperInterval, + self(), + process_room_queue), + StateData#state{room_shaper = + RoomShaper}; + true -> StateData end, - {next_state, normal_state, NSD}; - _ -> {next_state, normal_state, StateData} - end; - false -> {next_state, normal_state, StateData} - end - end; - IsVoiceRequest -> - NewStateData = case - (StateData#state.config)#config.allow_voice_requests - of - true -> - MinInterval = - (StateData#state.config)#config.voice_request_min_interval, - BareFrom = - jlib:jid_remove_resource(jlib:jid_tolower(From)), - NowPriority = -now_to_usec(now()), - CleanPriority = NowPriority + - MinInterval * - 1000000, - Times = - clean_treap(StateData#state.last_voice_request_time, - CleanPriority), - case treap:lookup(BareFrom, Times) - of - error -> - Times1 = - treap:insert(BareFrom, - NowPriority, - true, Times), - NSD = - StateData#state{last_voice_request_time - = - Times1}, - send_voice_request(From, NSD), - NSD; - {ok, _, _} -> - ErrText = - <<"Please, wait for a while before sending " - "new voice request">>, - Err = - jlib:make_error_reply(Packet, - ?ERRT_NOT_ACCEPTABLE(Lang, - ErrText)), - ejabberd_router:route(StateData#state.jid, - From, Err), - StateData#state{last_voice_request_time - = Times} - end; - false -> - ErrText = - <<"Voice requests are disabled in this " - "conference">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_FORBIDDEN(Lang, - ErrText)), - ejabberd_router:route(StateData#state.jid, - From, Err), - StateData - end, - {next_state, normal_state, NewStateData}; - IsVoiceApprovement -> - NewStateData = case is_moderator(From, StateData) of - true -> - case - extract_jid_from_voice_approvement(Els) - of - error -> - ErrText = - <<"Failed to extract JID from your voice " - "request approval">>, - Err = - jlib:make_error_reply(Packet, - ?ERRT_BAD_REQUEST(Lang, - ErrText)), - ejabberd_router:route(StateData#state.jid, - From, Err), - StateData; - {ok, TargetJid} -> - case is_visitor(TargetJid, - StateData) - of - true -> - Reason = <<>>, - NSD = - set_role(TargetJid, - participant, - StateData), - catch - send_new_presence(TargetJid, - Reason, - NSD), - NSD; - _ -> StateData - end - end; - _ -> - ErrText = - <<"Only moderators can approve voice requests">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_NOT_ALLOWED(Lang, - ErrText)), - ejabberd_router:route(StateData#state.jid, - From, Err), - StateData - end, - {next_state, normal_state, NewStateData}; - true -> {next_state, normal_state, StateData} - end; - _ -> - ErrText = <<"Improper message type">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_NOT_ACCEPTABLE(Lang, - ErrText)), - ejabberd_router:route(StateData#state.jid, From, Err), - {next_state, normal_state, StateData} - end; - _ -> - case xml:get_attr_s(<<"type">>, Attrs) of - <<"error">> -> ok; - _ -> - handle_roommessage_from_nonparticipant(Packet, Lang, - StateData, From) - end, - {next_state, normal_state, StateData} + NewActivity = Activity#activity{ + message_time = Now, + message_shaper = MessageShaper, + message = Packet}, + RoomQueue = p1_queue:in({message, From}, + StateData#state.room_queue), + StateData2 = store_user_activity(From, + NewActivity, + StateData1), + StateData3 = StateData2#state{room_queue = RoomQueue}, + {next_state, normal_state, StateData3} + end; + true -> + MessageInterval = (Activity#activity.message_time + + MinMessageInterval - Now) div 1000, + Interval = lists:max([MessageInterval, + MessageShaperInterval]), + erlang:send_after(Interval, self(), + {process_user_message, From}), + NewActivity = Activity#activity{ + message = Packet, + message_shaper = MessageShaper}, + StateData1 = store_user_activity(From, NewActivity, StateData), + {next_state, normal_state, StateData1} + end; + true when Type == error -> + case is_user_online(From, StateData) of + true -> + ErrorText = ?T("It is not allowed to send error messages to the" + " room. The participant (~s) has sent an error " + "message (~s) and got kicked from the room"), + NewState = expulse_participant(Packet, From, StateData, + translate:translate(Lang, + ErrorText)), + close_room_if_temporary_and_empty(NewState); + _ -> + {next_state, normal_state, StateData} + end; + true when Type == chat -> + ErrText = ?T("It is not allowed to send private messages " + "to the conference"), + Err = xmpp:err_not_acceptable(ErrText, Lang), + ejabberd_router:route_error(Packet, Err), + {next_state, normal_state, StateData}; + true when Type == normal -> + {next_state, normal_state, + try xmpp:decode_els(Packet) of + Pkt -> process_normal_message(From, Pkt, StateData) + catch _:{xmpp_codec, Why} -> + Txt = xmpp:io_format_error(Why), + Err = xmpp:err_bad_request(Txt, Lang), + ejabberd_router:route_error(Packet, Err), + StateData + end}; + true -> + ErrText = ?T("Improper message type"), + Err = xmpp:err_not_acceptable(ErrText, Lang), + ejabberd_router:route_error(Packet, Err), + {next_state, normal_state, StateData}; + false when Type /= error -> + handle_roommessage_from_nonparticipant(Packet, StateData, From), + {next_state, normal_state, StateData}; + false -> + {next_state, normal_state, StateData} end; -normal_state({route, From, <<"">>, - #xmlel{name = <<"iq">>} = Packet}, - StateData) -> - case jlib:iq_query_info(Packet) of - #iq{type = Type, xmlns = XMLNS, lang = Lang, - sub_el = #xmlel{name = SubElName} = SubEl} = - IQ - when (XMLNS == (?NS_MUC_ADMIN)) or - (XMLNS == (?NS_MUC_OWNER)) - or (XMLNS == (?NS_DISCO_INFO)) - or (XMLNS == (?NS_DISCO_ITEMS)) - or (XMLNS == (?NS_VCARD)) - or (XMLNS == (?NS_CAPTCHA)) -> - Res1 = case XMLNS of - ?NS_MUC_ADMIN -> - process_iq_admin(From, Type, Lang, SubEl, StateData); - ?NS_MUC_OWNER -> - process_iq_owner(From, Type, Lang, SubEl, StateData); - ?NS_DISCO_INFO -> - process_iq_disco_info(From, Type, Lang, StateData); - ?NS_DISCO_ITEMS -> - process_iq_disco_items(From, Type, Lang, StateData); - ?NS_VCARD -> - process_iq_vcard(From, Type, Lang, SubEl, StateData); - ?NS_CAPTCHA -> - process_iq_captcha(From, Type, Lang, SubEl, StateData) - end, - {IQRes, NewStateData} = case Res1 of - {result, Res, SD} -> - {IQ#iq{type = result, - sub_el = - [#xmlel{name = SubElName, - attrs = - [{<<"xmlns">>, - XMLNS}], - children = Res}]}, - SD}; - {error, Error} -> - {IQ#iq{type = error, - sub_el = [SubEl, Error]}, - StateData} - end, - ejabberd_router:route(StateData#state.jid, From, - jlib:iq_to_xml(IQRes)), - case NewStateData of - stop -> {stop, normal, StateData}; - _ -> {next_state, normal_state, NewStateData} - end; - reply -> {next_state, normal_state, StateData}; - _ -> - Err = jlib:make_error_reply(Packet, - ?ERR_FEATURE_NOT_IMPLEMENTED), - ejabberd_router:route(StateData#state.jid, From, Err), - {next_state, normal_state, StateData} +normal_state({route, <<"">>, + #iq{from = From, type = Type, lang = Lang, sub_els = [_]} = IQ0}, + StateData) when Type == get; Type == set -> + try + case ejabberd_hooks:run_fold( + muc_process_iq, + StateData#state.server_host, + xmpp:set_from_to(xmpp:decode_els(IQ0), + From, StateData#state.jid), + [StateData]) of + ignore -> + {next_state, normal_state, StateData}; + {ignore, StateData2} -> + {next_state, normal_state, StateData2}; + #iq{type = T} = IQRes when T == error; T == result -> + ejabberd_router:route(IQRes), + {next_state, normal_state, StateData}; + #iq{sub_els = [SubEl]} = IQ -> + Res1 = case SubEl of + #muc_admin{} -> + process_iq_admin(From, IQ, StateData); + #muc_owner{} -> + process_iq_owner(From, IQ, StateData); + #disco_info{} -> + process_iq_disco_info(From, IQ, StateData); + #disco_items{} -> + process_iq_disco_items(From, IQ, StateData); + #vcard_temp{} -> + process_iq_vcard(From, IQ, StateData); + #muc_subscribe{} -> + process_iq_mucsub(From, IQ, StateData); + #muc_unsubscribe{} -> + process_iq_mucsub(From, IQ, StateData); + #muc_subscriptions{} -> + process_iq_mucsub(From, IQ, StateData); + #xcaptcha{} -> + process_iq_captcha(From, IQ, StateData); + #adhoc_command{} -> + process_iq_adhoc(From, IQ, StateData); + #register{} -> + mod_muc:process_iq_register(IQ); + #message_moderate{id = Id, reason = Reason} -> % moderate:1 + process_iq_moderate(From, IQ, Id, Reason, StateData); + #fasten_apply_to{id = ModerateId} = ApplyTo -> + case xmpp:get_subtag(ApplyTo, #message_moderate_21{}) of + #message_moderate_21{reason = Reason} -> % moderate:0 + process_iq_moderate(From, IQ, ModerateId, Reason, StateData); + _ -> + Txt = ?T("The feature requested is not " + "supported by the conference"), + {error, xmpp:err_service_unavailable(Txt, Lang)} + end; + _ -> + Txt = ?T("The feature requested is not " + "supported by the conference"), + {error, xmpp:err_service_unavailable(Txt, Lang)} + end, + {IQRes, NewStateData} = + case Res1 of + {result, Res, SD} -> + {xmpp:make_iq_result(IQ, Res), SD}; + {result, Res} -> + {xmpp:make_iq_result(IQ, Res), StateData}; + {ignore, SD} -> + {ignore, SD}; + {error, Error} -> + {xmpp:make_error(IQ0, Error), StateData} + end, + if IQRes /= ignore -> + ejabberd_router:route(IQRes); + true -> + ok + end, + case NewStateData of + stop -> + Conf = StateData#state.config, + {stop, normal, StateData#state{config = Conf#config{persistent = {destroying, Conf#config.persistent}}}}; + _ when NewStateData#state.just_created -> + close_room_if_temporary_and_empty(NewStateData); + _ -> + {next_state, normal_state, NewStateData} + end + end + catch _:{xmpp_codec, Why} -> + ErrTxt = xmpp:io_format_error(Why), + Err = xmpp:err_bad_request(ErrTxt, Lang), + ejabberd_router:route_error(IQ0, Err), + {next_state, normal_state, StateData} end; -normal_state({route, From, Nick, - #xmlel{name = <<"presence">>} = Packet}, - StateData) -> +normal_state({route, <<"">>, #iq{} = IQ}, StateData) -> + Err = xmpp:err_bad_request(), + ejabberd_router:route_error(IQ, Err), + case StateData#state.just_created of + true -> {stop, normal, StateData}; + _ -> {next_state, normal_state, StateData} + end; +normal_state({route, Nick, #presence{from = From} = Packet}, StateData) -> Activity = get_user_activity(From, StateData), - Now = now_to_usec(now()), + Now = erlang:system_time(microsecond), MinPresenceInterval = - trunc(gen_mod:get_module_opt(StateData#state.server_host, - mod_muc, min_presence_interval, - fun(I) when is_number(I), I>=0 -> - I - end, 0) - * 1000000), - if (Now >= - Activity#activity.presence_time + MinPresenceInterval) - and (Activity#activity.presence == undefined) -> - NewActivity = Activity#activity{presence_time = Now}, - StateData1 = store_user_activity(From, NewActivity, - StateData), - process_presence(From, Nick, Packet, StateData1); + trunc(mod_muc_opt:min_presence_interval(StateData#state.server_host) * 1000000), + if (Now >= Activity#activity.presence_time + MinPresenceInterval) + and (Activity#activity.presence == undefined) -> + NewActivity = Activity#activity{presence_time = Now}, + StateData1 = store_user_activity(From, NewActivity, + StateData), + process_presence(Nick, Packet, StateData1); true -> - if Activity#activity.presence == undefined -> - Interval = (Activity#activity.presence_time + - MinPresenceInterval - - Now) - div 1000, - erlang:send_after(Interval, self(), - {process_user_presence, From}); - true -> ok - end, - NewActivity = Activity#activity{presence = - {Nick, Packet}}, - StateData1 = store_user_activity(From, NewActivity, - StateData), - {next_state, normal_state, StateData1} + if Activity#activity.presence == undefined -> + Interval = (Activity#activity.presence_time + + MinPresenceInterval - Now) div 1000, + erlang:send_after(Interval, self(), + {process_user_presence, From}); + true -> ok + end, + NewActivity = Activity#activity{presence = {Nick, Packet}}, + StateData1 = store_user_activity(From, NewActivity, + StateData), + {next_state, normal_state, StateData1} end; -normal_state({route, From, ToNick, - #xmlel{name = <<"message">>, attrs = Attrs} = Packet}, +normal_state({route, ToNick, + #message{from = From, type = Type, lang = Lang} = Packet}, StateData) -> - Type = xml:get_attr_s(<<"type">>, Attrs), - Lang = xml:get_attr_s(<<"xml:lang">>, Attrs), - case decide_fate_message(Type, Packet, From, StateData) - of - {expulse_sender, Reason} -> - ?DEBUG(Reason, []), - ErrorText = <<"This participant is kicked from the " - "room because he sent an error message " - "to another participant">>, - NewState = expulse_participant(Packet, From, StateData, - translate:translate(Lang, ErrorText)), - {next_state, normal_state, NewState}; - forget_message -> {next_state, normal_state, StateData}; - continue_delivery -> - case - {(StateData#state.config)#config.allow_private_messages, - is_user_online(From, StateData)} - of - {true, true} -> - case Type of - <<"groupchat">> -> - ErrText = - <<"It is not allowed to send private messages " - "of type \"groupchat\"">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_BAD_REQUEST(Lang, - ErrText)), - ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, - ToNick), - From, Err); - _ -> - case find_jids_by_nick(ToNick, StateData) of - false -> - ErrText = - <<"Recipient is not in the conference room">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_ITEM_NOT_FOUND(Lang, - ErrText)), - ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, - ToNick), - From, Err); + case decide_fate_message(Packet, From, StateData) of + {expulse_sender, Reason} -> + ?DEBUG(Reason, []), + ErrorText = ?T("It is not allowed to send error messages to the" + " room. The participant (~s) has sent an error " + "message (~s) and got kicked from the room"), + NewState = expulse_participant(Packet, From, StateData, + translate:translate(Lang, ErrorText)), + {next_state, normal_state, NewState}; + forget_message -> + {next_state, normal_state, StateData}; + continue_delivery -> + case {is_user_allowed_private_message(From, StateData), + is_user_online(From, StateData) orelse + is_subscriber(From, StateData) orelse + is_user_allowed_message_nonparticipant(From, StateData)} of + {true, true} when Type == groupchat -> + ErrText = ?T("It is not allowed to send private messages " + "of type \"groupchat\""), + Err = xmpp:err_bad_request(ErrText, Lang), + ejabberd_router:route_error(Packet, Err); + {true, true} -> + case find_jids_by_nick(ToNick, StateData) of + [] -> + ErrText = ?T("Recipient is not in the conference room"), + Err = xmpp:err_item_not_found(ErrText, Lang), + ejabberd_router:route_error(Packet, Err); ToJIDs -> SrcIsVisitor = is_visitor(From, StateData), - DstIsModerator = is_moderator(hd(ToJIDs), - StateData), + DstIsModerator = is_moderator(hd(ToJIDs), StateData), PmFromVisitors = (StateData#state.config)#config.allow_private_messages_from_visitors, if SrcIsVisitor == false; PmFromVisitors == anyone; (PmFromVisitors == moderators) and - DstIsModerator -> - {ok, #user{nick = FromNick}} = - (?DICT):find(jlib:jid_tolower(From), - StateData#state.users), - FromNickJID = - jlib:jid_replace_resource(StateData#state.jid, - FromNick), - [ejabberd_router:route(FromNickJID, ToJID, Packet) - || ToJID <- ToJIDs]; + DstIsModerator -> + {FromNick, _} = get_participant_data(From, StateData), + FromNickJID = + jid:replace_resource(StateData#state.jid, + FromNick), + X = #muc_user{}, + Packet2 = xmpp:set_subtag(Packet, X), + case ejabberd_hooks:run_fold(muc_filter_message, + StateData#state.server_host, + xmpp:put_meta(Packet2, mam_ignore, true), + [StateData, FromNick]) of + drop -> + ok; + Packet3 -> + PrivMsg = xmpp:set_from(xmpp:del_meta(Packet3, mam_ignore), FromNickJID), + lists:foreach( + fun(ToJID) -> + ejabberd_router:route(xmpp:set_to(PrivMsg, ToJID)) + end, ToJIDs) + end; true -> - ErrText = - <<"It is not allowed to send private messages">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_FORBIDDEN(Lang, - ErrText)), - ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, - ToNick), - From, Err) + ErrText = ?T("You are not allowed to send private messages"), + Err = xmpp:err_forbidden(ErrText, Lang), + ejabberd_router:route_error(Packet, Err) end - end - end; - {true, false} -> - ErrText = - <<"Only occupants are allowed to send messages " - "to the conference">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_NOT_ACCEPTABLE(Lang, - ErrText)), - ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, - ToNick), - From, Err); - {false, _} -> - ErrText = - <<"It is not allowed to send private messages">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_FORBIDDEN(Lang, ErrText)), - ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, - ToNick), - From, Err) - end, + end; + {true, false} -> + ErrText = ?T("Only occupants are allowed to send messages " + "to the conference"), + Err = xmpp:err_not_acceptable(ErrText, Lang), + ejabberd_router:route_error(Packet, Err); + {false, _} -> + ErrText = ?T("You are not allowed to send private messages"), + Err = xmpp:err_forbidden(ErrText, Lang), + ejabberd_router:route_error(Packet, Err) + end, {next_state, normal_state, StateData} end; -normal_state({route, From, ToNick, - #xmlel{name = <<"iq">>, attrs = Attrs} = Packet}, - StateData) -> - Lang = xml:get_attr_s(<<"xml:lang">>, Attrs), - StanzaId = xml:get_attr_s(<<"id">>, Attrs), - case {(StateData#state.config)#config.allow_query_users, - is_user_online_iq(StanzaId, From, StateData)} - of - {true, {true, NewId, FromFull}} -> - case find_jid_by_nick(ToNick, StateData) of - false -> - case jlib:iq_query_info(Packet) of - reply -> ok; - _ -> - ErrText = <<"Recipient is not in the conference room">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_ITEM_NOT_FOUND(Lang, - ErrText)), - ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, - ToNick), - From, Err) - end; - ToJID -> - {ok, #user{nick = FromNick}} = - (?DICT):find(jlib:jid_tolower(FromFull), - StateData#state.users), - {ToJID2, Packet2} = handle_iq_vcard(FromFull, ToJID, - StanzaId, NewId, Packet), - ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, - FromNick), - ToJID2, Packet2) - end; - {_, {false, _, _}} -> - case jlib:iq_query_info(Packet) of - reply -> ok; - _ -> - ErrText = - <<"Only occupants are allowed to send queries " - "to the conference">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_NOT_ACCEPTABLE(Lang, - ErrText)), - ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, - ToNick), - From, Err) - end; - _ -> - case jlib:iq_query_info(Packet) of - reply -> ok; - _ -> - ErrText = <<"Queries to the conference members are " - "not allowed in this room">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_NOT_ALLOWED(Lang, ErrText)), - ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, - ToNick), - From, Err) - end +normal_state({route, ToNick, + #iq{from = From, lang = Lang} = Packet}, + #state{config = #config{allow_query_users = AllowQuery}} = StateData) -> + try maps:get(jid:tolower(From), StateData#state.users) of + #user{nick = FromNick} when AllowQuery orelse ToNick == FromNick -> + case find_jid_by_nick(ToNick, StateData) of + false -> + ErrText = ?T("Recipient is not in the conference room"), + Err = xmpp:err_item_not_found(ErrText, Lang), + ejabberd_router:route_error(Packet, Err); + To -> + FromJID = jid:replace_resource(StateData#state.jid, FromNick), + case direct_iq_type(Packet) of + vcard -> + ejabberd_router:route_iq( + xmpp:set_from_to(Packet, FromJID, jid:remove_resource(To)), + Packet, self()); + pubsub -> + ejabberd_router:route_iq( + xmpp:set_from_to(Packet, FromJID, jid:remove_resource(To)), + Packet, self()); + ping when ToNick == FromNick -> + %% Self-ping optimization from XEP-0410 + ejabberd_router:route(xmpp:make_iq_result(Packet)); + response -> + ejabberd_router:route(xmpp:set_from_to(Packet, FromJID, To)); + #stanza_error{} = Err -> + ejabberd_router:route_error(Packet, Err); + _OtherRequest -> + ejabberd_router:route_iq( + xmpp:set_from_to(Packet, FromJID, To), Packet, self()) + end + end; + _ -> + ErrText = ?T("Queries to the conference members are " + "not allowed in this room"), + Err = xmpp:err_not_allowed(ErrText, Lang), + ejabberd_router:route_error(Packet, Err) + catch _:{badkey, _} -> + ErrText = ?T("Only occupants are allowed to send queries " + "to the conference"), + Err = xmpp:err_not_acceptable(ErrText, Lang), + ejabberd_router:route_error(Packet, Err) end, {next_state, normal_state, StateData}; +normal_state(hibernate, StateData) -> + case maps:size(StateData#state.users) of + 0 -> + store_room_no_checks(StateData, [], true), + ?INFO_MSG("Hibernating room ~ts@~ts", [StateData#state.room, StateData#state.host]), + {stop, normal, StateData#state{hibernate_timer = hibernating}}; + _ -> + {next_state, normal_state, StateData} + end; normal_state(_Event, StateData) -> {next_state, normal_state, StateData}. -%%---------------------------------------------------------------------- -%% Func: handle_event/3 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} -%%---------------------------------------------------------------------- handle_event({service_message, Msg}, _StateName, StateData) -> - MessagePkt = #xmlel{name = <<"message">>, - attrs = [{<<"type">>, <<"groupchat">>}], - children = - [#xmlel{name = <<"body">>, attrs = [], - children = [{xmlcdata, Msg}]}]}, - lists:foreach( - fun({_LJID, Info}) -> - ejabberd_router:route( - StateData#state.jid, - Info#user.jid, - MessagePkt) - end, - ?DICT:to_list(StateData#state.users)), + MessagePkt = #message{type = groupchat, body = xmpp:mk_text(Msg)}, + send_wrapped_multiple( + StateData#state.jid, + get_users_and_subscribers_with_node(?NS_MUCSUB_NODES_MESSAGES, StateData), + MessagePkt, + ?NS_MUCSUB_NODES_MESSAGES, + StateData), NSD = add_message_to_history(<<"">>, StateData#state.jid, MessagePkt, StateData), {next_state, normal_state, NSD}; handle_event({destroy, Reason}, _StateName, StateData) -> - {result, [], stop} = destroy_room(#xmlel{name = - <<"destroy">>, - attrs = - [{<<"xmlns">>, ?NS_MUC_OWNER}], - children = - case Reason of - none -> []; - _Else -> - [#xmlel{name = - <<"reason">>, - attrs = [], - children = - [{xmlcdata, - Reason}]}] - end}, - StateData), - ?INFO_MSG("Destroyed MUC room ~s with reason: ~p", - [jlib:jid_to_string(StateData#state.jid), Reason]), + _ = destroy_room(#muc_destroy{xmlns = ?NS_MUC_OWNER, reason = Reason}, StateData), + ?INFO_MSG("Destroyed MUC room ~ts with reason: ~p", + [jid:encode(StateData#state.jid), Reason]), add_to_log(room_existence, destroyed, StateData), - {stop, shutdown, StateData}; + Conf = StateData#state.config, + {stop, shutdown, StateData#state{config = Conf#config{persistent = {destroying, Conf#config.persistent}}}}; handle_event(destroy, StateName, StateData) -> - ?INFO_MSG("Destroyed MUC room ~s", - [jlib:jid_to_string(StateData#state.jid)]), - handle_event({destroy, none}, StateName, StateData); + ?INFO_MSG("Destroyed MUC room ~ts", + [jid:encode(StateData#state.jid)]), + handle_event({destroy, <<"">>}, StateName, StateData); handle_event({set_affiliations, Affiliations}, StateName, StateData) -> - {next_state, StateName, - StateData#state{affiliations = Affiliations}}; + 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}. -%%---------------------------------------------------------------------- -%% Func: handle_sync_event/4 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {reply, Reply, NextStateName, NextStateData} | -%% {reply, Reply, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} | -%% {stop, Reason, Reply, NewStateData} -%%---------------------------------------------------------------------- -handle_sync_event({get_disco_item, JID, Lang}, _From, StateName, StateData) -> - Reply = get_roomdesc_reply(JID, StateData, - get_roomdesc_tail(StateData, Lang)), - {reply, Reply, StateName, StateData}; +handle_sync_event({get_disco_item, Filter, JID, Lang, Time}, _From, StateName, StateData) -> + Len = maps:size(StateData#state.nicks), + Reply = case (Filter == all) or (Filter == Len) or ((Filter /= 0) and (Len /= 0)) of + true -> + get_roomdesc_reply(JID, StateData, + get_roomdesc_tail(StateData, Lang)); + false -> + false + end, + CurrentTime = erlang:system_time(millisecond), + if CurrentTime < Time -> + {reply, Reply, StateName, StateData}; + true -> + {next_state, StateName, StateData} + end; +%% These two clauses are only for backward compatibility with nodes running old code +handle_sync_event({get_disco_item, JID, Lang}, From, StateName, StateData) -> + handle_sync_event({get_disco_item, any, JID, Lang}, From, StateName, StateData); +handle_sync_event({get_disco_item, Filter, JID, Lang}, From, StateName, StateData) -> + handle_sync_event({get_disco_item, Filter, JID, Lang, infinity}, From, StateName, StateData); handle_sync_event(get_config, _From, StateName, StateData) -> {reply, {ok, StateData#state.config}, StateName, @@ -745,13 +794,92 @@ handle_sync_event(get_config, _From, StateName, handle_sync_event(get_state, _From, StateName, StateData) -> {reply, {ok, StateData}, StateName, StateData}; +handle_sync_event(get_info, _From, StateName, + StateData) -> + Result = #{occupants_number => maps:size(StateData#state.users)}, + {reply, Result, StateName, StateData}; handle_sync_event({change_config, Config}, _From, StateName, StateData) -> - {result, [], NSD} = change_config(Config, StateData), + {result, undefined, NSD} = change_config(Config, StateData), {reply, {ok, NSD#state.config}, StateName, NSD}; handle_sync_event({change_state, NewStateData}, _From, StateName, _StateData) -> + Mod = gen_mod:db_mod(NewStateData#state.server_host, mod_muc), + case erlang:function_exported(Mod, get_subscribed_rooms, 3) of + true -> + ok; + _ -> + erlang:put(muc_subscribers, NewStateData#state.muc_subscribers#muc_subscribers.subscribers) + end, {reply, {ok, NewStateData}, StateName, NewStateData}; +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} + end; +handle_sync_event(get_subscribers, _From, StateName, StateData) -> + JIDs = muc_subscribers_fold( + fun(_LBareJID, #subscriber{jid = JID}, Acc) -> + [JID | Acc] + end, [], StateData#state.muc_subscribers), + {reply, {ok, JIDs}, StateName, StateData}; +handle_sync_event({muc_subscribe, From, Nick, Nodes}, _From, + StateName, StateData) -> + IQ = #iq{type = set, id = p1_rand:get_string(), + from = From, sub_els = [#muc_subscribe{nick = Nick, + events = Nodes}]}, + Config = StateData#state.config, + CaptchaRequired = Config#config.captcha_protected, + PasswordProtected = Config#config.password_protected, + MembersOnly = Config#config.members_only, + TmpConfig = Config#config{captcha_protected = false, + password_protected = false, + members_only = false}, + TmpState = StateData#state{config = TmpConfig}, + case process_iq_mucsub(From, IQ, TmpState) of + {result, #muc_subscribe{events = NewNodes}, NewState} -> + NewConfig = (NewState#state.config)#config{ + captcha_protected = CaptchaRequired, + password_protected = PasswordProtected, + members_only = MembersOnly}, + {reply, {ok, NewNodes}, StateName, + NewState#state{config = NewConfig}}; + {ignore, NewState} -> + NewConfig = (NewState#state.config)#config{ + captcha_protected = CaptchaRequired, + password_protected = PasswordProtected, + members_only = MembersOnly}, + {reply, {error, ?T("Request is ignored")}, + NewState#state{config = NewConfig}}; + {error, Err} -> + {reply, {error, get_error_text(Err)}, StateName, StateData} + end; +handle_sync_event({muc_unsubscribe, From}, _From, StateName, + #state{config = Conf} = StateData) -> + IQ = #iq{type = set, id = p1_rand:get_string(), + from = From, sub_els = [#muc_unsubscribe{}]}, + case process_iq_mucsub(From, IQ, StateData) of + {result, _, stop} -> + {stop, normal, StateData#state{config = Conf#config{persistent = {destroying, Conf#config.persistent}}}}; + {result, _, NewState} -> + {reply, ok, StateName, NewState}; + {ignore, NewState} -> + {reply, {error, ?T("Request is ignored")}, NewState}; + {error, Err} -> + {reply, {error, get_error_text(Err)}, StateName, StateData} + end; +handle_sync_event({is_subscribed, From}, _From, StateName, StateData) -> + IsSubs = try muc_subscribers_get( + jid:split(From), StateData#state.muc_subscribers) of + #subscriber{nick = Nick, nodes = Nodes} -> {true, Nick, Nodes} + catch _:{badkey, _} -> false + end, + {reply, IsSubs, StateName, StateData}; handle_sync_event(_Event, _From, StateName, StateData) -> Reply = ok, {reply, Reply, StateName, StateData}. @@ -759,15 +887,9 @@ handle_sync_event(_Event, _From, StateName, code_change(_OldVsn, StateName, StateData, _Extra) -> {ok, StateName, StateData}. -%%---------------------------------------------------------------------- -%% Func: handle_info/3 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} -%%---------------------------------------------------------------------- handle_info({process_user_presence, From}, normal_state = _StateName, StateData) -> - RoomQueueEmpty = queue:is_empty(StateData#state.room_queue), - RoomQueue = queue:in({presence, From}, StateData#state.room_queue), + RoomQueueEmpty = p1_queue:is_empty(StateData#state.room_queue), + RoomQueue = p1_queue:in({presence, From}, StateData#state.room_queue), StateData1 = StateData#state{room_queue = RoomQueue}, if RoomQueueEmpty -> StateData2 = prepare_room_queue(StateData1), @@ -777,9 +899,9 @@ handle_info({process_user_presence, From}, normal_state = _StateName, StateData) handle_info({process_user_message, From}, normal_state = _StateName, StateData) -> RoomQueueEmpty = - queue:is_empty(StateData#state.room_queue), - RoomQueue = queue:in({message, From}, - StateData#state.room_queue), + p1_queue:is_empty(StateData#state.room_queue), + RoomQueue = p1_queue:in({message, From}, + StateData#state.room_queue), StateData1 = StateData#state{room_queue = RoomQueue}, if RoomQueueEmpty -> StateData2 = prepare_room_queue(StateData1), @@ -788,7 +910,7 @@ handle_info({process_user_message, From}, end; handle_info(process_room_queue, normal_state = StateName, StateData) -> - case queue:out(StateData#state.room_queue) of + case p1_queue:out(StateData#state.room_queue) of {{value, {message, From}}, RoomQueue} -> Activity = get_user_activity(From, StateData), Packet = Activity#activity.message, @@ -797,7 +919,7 @@ handle_info(process_room_queue, StateData), StateData2 = StateData1#state{room_queue = RoomQueue}, StateData3 = prepare_room_queue(StateData2), - process_groupchat_message(From, Packet, StateData3); + process_groupchat_message(Packet, StateData3); {{value, {presence, From}}, RoomQueue} -> Activity = get_user_activity(From, StateData), {Nick, Packet} = Activity#activity.presence, @@ -806,197 +928,421 @@ handle_info(process_room_queue, StateData), StateData2 = StateData1#state{room_queue = RoomQueue}, StateData3 = prepare_room_queue(StateData2), - process_presence(From, Nick, Packet, StateData3); + process_presence(Nick, Packet, StateData3); {empty, _} -> {next_state, StateName, StateData} end; handle_info({captcha_succeed, From}, normal_state, StateData) -> - NewState = case (?DICT):find(From, - StateData#state.robots) - of - {ok, {Nick, Packet}} -> - Robots = (?DICT):store(From, passed, - StateData#state.robots), - add_new_user(From, Nick, Packet, - StateData#state{robots = Robots}); - _ -> StateData + NewState = case maps:get(From, StateData#state.robots, passed) of + {Nick, Packet} -> + Robots = maps:put(From, passed, StateData#state.robots), + add_new_user(From, Nick, Packet, + StateData#state{robots = Robots}); + passed -> + StateData end, {next_state, normal_state, NewState}; handle_info({captcha_failed, From}, normal_state, StateData) -> - NewState = case (?DICT):find(From, - StateData#state.robots) - of - {ok, {Nick, Packet}} -> - Robots = (?DICT):erase(From, StateData#state.robots), - Err = jlib:make_error_reply(Packet, - ?ERR_NOT_AUTHORIZED), - ejabberd_router:route % TODO: s/Nick/""/ - (jlib:jid_replace_resource(StateData#state.jid, - Nick), - From, Err), - StateData#state{robots = Robots}; - _ -> StateData + NewState = case maps:get(From, StateData#state.robots, passed) of + {_Nick, Packet} -> + Robots = maps:remove(From, StateData#state.robots), + Txt = ?T("The CAPTCHA verification has failed"), + Lang = xmpp:get_lang(Packet), + Err = xmpp:err_not_authorized(Txt, Lang), + ejabberd_router:route_error(Packet, Err), + StateData#state{robots = Robots}; + passed -> + StateData end, {next_state, normal_state, NewState}; +handle_info(close_room_if_temporary_and_empty, _StateName, StateData) -> + close_room_if_temporary_and_empty(StateData); handle_info(shutdown, _StateName, StateData) -> {stop, shutdown, StateData}; +handle_info({iq_reply, #iq{type = Type, sub_els = Els}, + #iq{from = From, to = To} = IQ}, StateName, StateData) -> + ejabberd_router:route( + xmpp:set_from_to( + IQ#iq{type = Type, sub_els = Els}, + To, From)), + {next_state, StateName, StateData}; +handle_info({iq_reply, timeout, IQ}, StateName, StateData) -> + Txt = ?T("Request has timed out"), + Err = xmpp:err_recipient_unavailable(Txt, IQ#iq.lang), + ejabberd_router:route_error(IQ, Err), + {next_state, StateName, StateData}; +handle_info(config_reloaded, StateName, StateData) -> + Max = mod_muc_opt:history_size(StateData#state.server_host), + History1 = StateData#state.history, + Q1 = History1#lqueue.queue, + Q2 = case p1_queue:len(Q1) of + Len when Len > Max -> + lqueue_cut(Q1, Len-Max); + _ -> + Q1 + end, + History2 = History1#lqueue{queue = Q2, max = Max}, + {next_state, StateName, StateData#state{history = History2}}; handle_info(_Info, StateName, StateData) -> {next_state, StateName, StateData}. -%%---------------------------------------------------------------------- -%% Func: terminate/3 -%% Purpose: Shutdown the fsm -%% Returns: any -%%---------------------------------------------------------------------- -terminate(Reason, _StateName, StateData) -> - ?INFO_MSG("Stopping MUC room ~s@~s", - [StateData#state.room, StateData#state.host]), - ReasonT = case Reason of - shutdown -> - <<"You are being removed from the room " - "because of a system shutdown">>; - _ -> <<"Room terminates">> - end, - ItemAttrs = [{<<"affiliation">>, <<"none">>}, - {<<"role">>, <<"none">>}], - ReasonEl = #xmlel{name = <<"reason">>, attrs = [], - children = [{xmlcdata, ReasonT}]}, - Packet = #xmlel{name = <<"presence">>, - attrs = [{<<"type">>, <<"unavailable">>}], - children = - [#xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_MUC_USER}], - children = - [#xmlel{name = <<"item">>, - attrs = ItemAttrs, - children = [ReasonEl]}, - #xmlel{name = <<"status">>, - attrs = [{<<"code">>, <<"332">>}], - children = []}]}]}, - (?DICT):fold(fun (LJID, Info, _) -> - Nick = Info#user.nick, - case Reason of - shutdown -> - ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, - Nick), - Info#user.jid, Packet); - _ -> ok - end, - tab_remove_online_user(LJID, StateData) - end, - [], StateData#state.users), - add_to_log(room_existence, stopped, StateData), - mod_muc:room_destroyed(StateData#state.host, StateData#state.room, self(), - StateData#state.server_host), - ok. +terminate(Reason, _StateName, + #state{server_host = LServer, host = Host, room = Room} = StateData) -> + try + ?INFO_MSG("Stopping MUC room ~ts@~ts", [Room, Host]), + ReasonT = case Reason of + shutdown -> + ?T("You are being removed from the room " + "because of a system shutdown"); + _ -> ?T("Room terminates") + end, + Packet = #presence{ + type = unavailable, + sub_els = [#muc_user{items = [#muc_item{affiliation = none, + reason = ReasonT, + role = none}], + status_codes = [332,110]}]}, + maps:fold( + fun(_, #user{nick = Nick, jid = JID}, _) -> + case Reason of + shutdown -> + send_wrapped(jid:replace_resource(StateData#state.jid, Nick), + JID, Packet, + ?NS_MUCSUB_NODES_PARTICIPANTS, + StateData); + _ -> ok + end, + tab_remove_online_user(JID, StateData) + end, [], get_users_and_subscribers_with_node( + ?NS_MUCSUB_NODES_PARTICIPANTS, StateData)), + + disable_hibernate_timer(StateData), + case StateData#state.hibernate_timer of + hibernating -> + ok; + _ -> + add_to_log(room_existence, stopped, StateData), + case (StateData#state.config)#config.persistent of + false -> + ejabberd_hooks:run(room_destroyed, LServer, [LServer, Room, Host, false]); + {destroying, Persistent} -> + ejabberd_hooks:run(room_destroyed, LServer, [LServer, Room, Host, Persistent]); + _ -> + ok + end + end + catch + E:R:StackTrace -> + ?ERROR_MSG("Got exception on room termination:~n** ~ts", + [misc:format_exception(2, E, R, StackTrace)]) + end. %%%---------------------------------------------------------------------- %%% Internal functions %%%---------------------------------------------------------------------- +-spec route(pid(), stanza()) -> ok. +route(Pid, Packet) -> + ?DEBUG("Routing to MUC room ~p:~n~ts", [Pid, xmpp:pp(Packet)]), + #jid{lresource = Nick} = xmpp:get_to(Packet), + p1_fsm:send_event(Pid, {route, Nick, Packet}). -route(Pid, From, ToNick, Packet) -> - gen_fsm:send_event(Pid, {route, From, ToNick, Packet}). - -process_groupchat_message(From, - #xmlel{name = <<"message">>, attrs = Attrs} = Packet, - StateData) -> - Lang = xml:get_attr_s(<<"xml:lang">>, Attrs), - case is_user_online(From, StateData) orelse +-spec process_groupchat_message(message(), state()) -> fsm_next(). +process_groupchat_message(#message{from = From, lang = Lang} = Packet, StateData) -> + IsSubscriber = is_subscriber(From, StateData), + case is_user_online(From, StateData) orelse IsSubscriber orelse is_user_allowed_message_nonparticipant(From, StateData) of true -> - {FromNick, Role} = get_participant_data(From, - StateData), - if (Role == moderator) or (Role == participant) or - ((StateData#state.config)#config.moderated == false) -> - {NewStateData1, IsAllowed} = case check_subject(Packet) - of - false -> {StateData, true}; - Subject -> - case - can_change_subject(Role, - StateData) - of - true -> - NSD = - StateData#state{subject - = - Subject, - subject_author - = - FromNick}, - case - (NSD#state.config)#config.persistent - of - true -> - mod_muc:store_room(NSD#state.server_host, - NSD#state.host, - NSD#state.room, - make_opts(NSD)); - _ -> ok - end, - {NSD, true}; - _ -> {StateData, false} - end - end, - case IsAllowed of + {FromNick, Role} = get_participant_data(From, StateData), + #config{moderated = Moderated} = StateData#state.config, + AllowedByModerationRules = + case {Role == moderator orelse Role == participant orelse + not Moderated, IsSubscriber} of + {true, _} -> true; + {_, true} -> + % We assume all subscribers are at least members + true; + _ -> + false + end, + if AllowedByModerationRules -> + Subject = check_subject(Packet), + {NewStateData1, IsAllowed} = + case Subject of + [] -> + {StateData, true}; + _ -> + case + can_change_subject(Role, + IsSubscriber, + StateData) + of + true -> + NSD = + StateData#state{subject = Subject, + subject_author = {FromNick, From}}, + store_room(NSD), + {NSD, true}; + _ -> {StateData, false} + end + end, + case IsAllowed of true -> - lists:foreach( - fun({_LJID, Info}) -> - ejabberd_router:route( - jlib:jid_replace_resource( - StateData#state.jid, - FromNick), - Info#user.jid, - Packet) - end, - ?DICT:to_list(StateData#state.users)), - NewStateData2 = case has_body_or_subject(Packet) of - true -> - add_message_to_history(FromNick, From, - Packet, - NewStateData1); - false -> - NewStateData1 - end, - {next_state, normal_state, NewStateData2}; + case + ejabberd_hooks:run_fold(muc_filter_message, + StateData#state.server_host, + Packet, + [StateData, FromNick]) + of + drop -> + {next_state, normal_state, StateData}; + NewPacket1 -> + NewPacket = xmpp:put_meta(xmpp:remove_subtag( + add_stanza_id(NewPacket1, StateData), #nick{}), + muc_sender_real_jid, From), + Node = if Subject == [] -> ?NS_MUCSUB_NODES_MESSAGES; + true -> ?NS_MUCSUB_NODES_SUBJECT + end, + NewStateData2 = check_message_for_retractions(NewPacket1, NewStateData1), + send_wrapped_multiple( + jid:replace_resource(StateData#state.jid, FromNick), + get_users_and_subscribers_with_node(Node, StateData), + NewPacket, Node, NewStateData2), + NewStateData3 = case has_body_or_subject(NewPacket) of + true -> + add_message_to_history(FromNick, From, + NewPacket, + NewStateData2); + false -> + NewStateData2 + end, + {next_state, normal_state, NewStateData3} + end; _ -> - Err = case - (StateData#state.config)#config.allow_change_subj - of + Err = case (StateData#state.config)#config.allow_change_subj of true -> - ?ERRT_FORBIDDEN(Lang, - <<"Only moderators and participants are " - "allowed to change the subject in this " - "room">>); + xmpp:err_forbidden( + ?T("Only moderators and participants are " + "allowed to change the subject in this " + "room"), Lang); _ -> - ?ERRT_FORBIDDEN(Lang, - <<"Only moderators are allowed to change " - "the subject in this room">>) + xmpp:err_forbidden( + ?T("Only moderators are allowed to change " + "the subject in this room"), Lang) end, - ejabberd_router:route(StateData#state.jid, From, - jlib:make_error_reply(Packet, Err)), + ejabberd_router:route_error(Packet, Err), {next_state, normal_state, StateData} end; true -> - ErrText = <<"Visitors are not allowed to send messages " - "to all occupants">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_FORBIDDEN(Lang, ErrText)), - ejabberd_router:route(StateData#state.jid, From, Err), + ErrText = ?T("Visitors are not allowed to send messages " + "to all occupants"), + Err = xmpp:err_forbidden(ErrText, Lang), + ejabberd_router:route_error(Packet, Err), {next_state, normal_state, StateData} end; false -> - ErrText = - <<"Only occupants are allowed to send messages " - "to the conference">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)), - ejabberd_router:route(StateData#state.jid, From, Err), + ErrText = ?T("Only occupants are allowed to send messages " + "to the conference"), + Err = xmpp:err_not_acceptable(ErrText, Lang), + ejabberd_router:route_error(Packet, Err), {next_state, normal_state, StateData} end. +-spec check_message_for_retractions(Packet :: message(), State :: state()) -> state(). +check_message_for_retractions(Packet, + #state{config = Config, room = Room, host = Host, + server_host = Server} = State) -> + case xmpp:get_subtag(Packet, #fasten_apply_to{}) of + #fasten_apply_to{id = ID} = F -> + case xmpp:get_subtag(F, #message_retract{}) of + #message_retract{} -> + #jid{luser = U, lserver = S} = xmpp:get_from(Packet), + case remove_from_history({U, S}, ID, State) of + {NewState, StanzaId} when is_integer(StanzaId) -> + case Config#config.mam of + true -> + mod_mam:remove_message_from_archive({Room, Host}, Server, StanzaId), + NewState; + _ -> + NewState + end; + {NewState, _} -> + NewState + end; + _ -> + State + end; + _ -> + State + end. + +-spec add_stanza_id(Packet :: message(), State :: state()) -> message(). +add_stanza_id(Packet, #state{jid = JID}) -> + {AddId, NewPacket} = + case xmpp:get_meta(Packet, stanza_id, false) of + false -> + GenID = erlang:system_time(microsecond), + {true, xmpp:put_meta(Packet, stanza_id, GenID)}; + _ -> + StanzaIds = xmpp:get_subtags(Packet, #stanza_id{by = #jid{}}), + HasOurStanzaId = lists:any( + fun(#stanza_id{by = JID2}) when JID == JID2 -> true; + (_) -> false + end, StanzaIds), + {not HasOurStanzaId, Packet} + end, + if + AddId -> + ID = xmpp:get_meta(NewPacket, stanza_id), + IDs = integer_to_binary(ID), + xmpp:append_subtags(NewPacket, [#stanza_id{by = JID, id = IDs}]); + true -> + Packet + end. + +-spec process_normal_message(jid(), message(), state()) -> state(). +process_normal_message(From, #message{lang = Lang} = Pkt, StateData) -> + Action = lists:foldl( + fun(_, {error, _} = Err) -> + Err; + (_, {ok, _} = Result) -> + Result; + (#muc_user{invites = [_|_] = Invites}, _) -> + case check_invitation(From, Invites, Lang, StateData) of + ok -> + {ok, Invites}; + {error, _} = Err -> + Err + end; + (#xdata{type = submit, fields = Fs}, _) -> + try {ok, muc_request:decode(Fs)} + catch _:{muc_request, Why} -> + Txt = muc_request:format_error(Why), + {error, xmpp:err_bad_request(Txt, Lang)} + end; + (_, Acc) -> + Acc + end, ok, xmpp:get_els(Pkt)), + case Action of + {ok, [#muc_invite{}|_] = Invitations} -> + lists:foldl( + fun(Invitation, AccState) -> + process_invitation(From, Pkt, Invitation, Lang, AccState) + end, StateData, Invitations); + {ok, [{role, participant}]} -> + process_voice_request(From, Pkt, StateData); + {ok, VoiceApproval} -> + process_voice_approval(From, Pkt, VoiceApproval, StateData); + {error, Err} -> + ejabberd_router:route_error(Pkt, Err), + StateData; + ok -> + StateData + end. + +-spec process_invitation(jid(), message(), muc_invite(), binary(), state()) -> state(). +process_invitation(From, Pkt, Invitation, Lang, StateData) -> + IJID = route_invitation(From, Pkt, Invitation, Lang, StateData), + Config = StateData#state.config, + case Config#config.members_only of + true -> + case get_affiliation(IJID, StateData) of + none -> + NSD = set_affiliation(IJID, member, StateData), + send_affiliation(IJID, member, StateData), + store_room(NSD), + NSD; + _ -> + StateData + end; + false -> + StateData + end. + +-spec process_voice_request(jid(), message(), state()) -> state(). +process_voice_request(From, Pkt, StateData) -> + Lang = xmpp:get_lang(Pkt), + case (StateData#state.config)#config.allow_voice_requests of + true -> + MinInterval = (StateData#state.config)#config.voice_request_min_interval, + BareFrom = jid:remove_resource(jid:tolower(From)), + NowPriority = -erlang:system_time(microsecond), + CleanPriority = NowPriority + MinInterval * 1000000, + Times = clean_treap(StateData#state.last_voice_request_time, + CleanPriority), + case treap:lookup(BareFrom, Times) of + error -> + Times1 = treap:insert(BareFrom, + NowPriority, + true, Times), + NSD = StateData#state{last_voice_request_time = Times1}, + send_voice_request(From, Lang, NSD), + NSD; + {ok, _, _} -> + ErrText = ?T("Please, wait for a while before sending " + "new voice request"), + Err = xmpp:err_resource_constraint(ErrText, Lang), + ejabberd_router:route_error(Pkt, Err), + StateData#state{last_voice_request_time = Times} + end; + false -> + ErrText = ?T("Voice requests are disabled in this conference"), + Err = xmpp:err_forbidden(ErrText, Lang), + ejabberd_router:route_error(Pkt, Err), + StateData + end. + +-spec process_voice_approval(jid(), message(), [muc_request:property()], state()) -> state(). +process_voice_approval(From, Pkt, VoiceApproval, StateData) -> + Lang = xmpp:get_lang(Pkt), + case is_moderator(From, StateData) of + true -> + case lists:keyfind(jid, 1, VoiceApproval) of + {_, TargetJid} -> + Allow = proplists:get_bool(request_allow, VoiceApproval), + case is_visitor(TargetJid, StateData) of + true when Allow -> + Reason = <<>>, + NSD = set_role(TargetJid, participant, StateData), + catch send_new_presence( + TargetJid, Reason, NSD, StateData), + NSD; + _ -> + StateData + end; + false -> + ErrText = ?T("Failed to extract JID from your voice " + "request approval"), + Err = xmpp:err_bad_request(ErrText, Lang), + ejabberd_router:route_error(Pkt, Err), + StateData + end; + false -> + ErrText = ?T("Only moderators can approve voice requests"), + Err = xmpp:err_not_allowed(ErrText, Lang), + ejabberd_router:route_error(Pkt, Err), + StateData + end. + +-spec direct_iq_type(iq()) -> vcard | ping | request | response | pubsub | stanza_error(). +direct_iq_type(#iq{type = T, sub_els = SubEls, lang = Lang}) when T == get; T == set -> + case SubEls of + [El] -> + case xmpp:get_ns(El) of + ?NS_VCARD when T == get -> vcard; + ?NS_PUBSUB when T == get -> pubsub; + ?NS_PING when T == get -> ping; + _ -> request + end; + [] -> + xmpp:err_bad_request(?T("No child elements found"), Lang); + [_|_] -> + xmpp:err_bad_request(?T("Too many child elements"), Lang) + end; +direct_iq_type(#iq{}) -> + response. + %% @doc Check if this non participant can send message to room. %% %% XEP-0045 v1.23: @@ -1004,6 +1350,7 @@ process_groupchat_message(From, %% an implementation MAY allow users with certain privileges %% (e.g., a room owner, room admin, or service-level admin) %% to send messages to the room even if those users are not occupants. +-spec is_user_allowed_message_nonparticipant(jid(), state()) -> boolean(). is_user_allowed_message_nonparticipant(JID, StateData) -> case get_service_affiliation(JID, StateData) of @@ -1011,148 +1358,225 @@ is_user_allowed_message_nonparticipant(JID, _ -> false end. -%% @doc Get information of this participant, or default values. -%% If the JID is not a participant, return values for a service message. -get_participant_data(From, StateData) -> - case (?DICT):find(jlib:jid_tolower(From), - StateData#state.users) - of - {ok, #user{nick = FromNick, role = Role}} -> - {FromNick, Role}; - error -> {<<"">>, moderator} +-spec is_user_allowed_private_message(jid(), state()) -> boolean(). +is_user_allowed_private_message(JID, StateData) -> + case {(StateData#state.config)#config.allowpm, + get_role(JID, StateData)} of + {anyone, _} -> + true; + {participants, moderator} -> + true; + {participants, participant} -> + true; + {moderators, moderator} -> + true; + {none, _} -> + false; + {_, _} -> + false end. -process_presence(From, Nick, - #xmlel{name = <<"presence">>, attrs = Attrs} = Packet, - StateData) -> - Type = xml:get_attr_s(<<"type">>, Attrs), - Lang = xml:get_attr_s(<<"xml:lang">>, Attrs), - StateData1 = case Type of - <<"unavailable">> -> - case is_user_online(From, StateData) of - true -> - NewPacket = case - {(StateData#state.config)#config.allow_visitor_status, - is_visitor(From, StateData)} - of - {false, true} -> - strip_status(Packet); - _ -> Packet - end, - NewState = add_user_presence_un(From, NewPacket, - StateData), - case (?DICT):find(Nick, StateData#state.nicks) of - {ok, [_, _ | _]} -> ok; - _ -> send_new_presence(From, NewState) - end, - Reason = case xml:get_subtag(NewPacket, - <<"status">>) - of - false -> <<"">>; - Status_el -> - xml:get_tag_cdata(Status_el) - end, - remove_online_user(From, NewState, Reason); - _ -> StateData - end; - <<"error">> -> - case is_user_online(From, StateData) of - true -> - ErrorText = - <<"This participant is kicked from the " - "room because he sent an error presence">>, - expulse_participant(Packet, From, StateData, - translate:translate(Lang, - ErrorText)); - _ -> StateData - end; - <<"">> -> - case is_user_online(From, StateData) of - true -> - case is_nick_change(From, Nick, StateData) of - true -> - case {nick_collision(From, Nick, StateData), - mod_muc:can_use_nick(StateData#state.server_host, - StateData#state.host, - From, Nick), - {(StateData#state.config)#config.allow_visitor_nickchange, - is_visitor(From, StateData)}} - of - {_, _, {false, true}} -> - ErrText = - <<"Visitors are not allowed to change their " - "nicknames in this room">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_NOT_ALLOWED(Lang, - ErrText)), - ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, - Nick), - From, Err), - StateData; - {true, _, _} -> - Lang = xml:get_attr_s(<<"xml:lang">>, - Attrs), - ErrText = - <<"That nickname is already in use by another " - "occupant">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_CONFLICT(Lang, - ErrText)), - ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, - Nick), % TODO: s/Nick/""/ - From, Err), - StateData; - {_, false, _} -> - ErrText = - <<"That nickname is registered by another " - "person">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_CONFLICT(Lang, - ErrText)), - ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, - Nick), - From, Err), - StateData; - _ -> change_nick(From, Nick, StateData) - end; - _NotNickChange -> - Stanza = case - {(StateData#state.config)#config.allow_visitor_status, - is_visitor(From, StateData)} - of - {false, true} -> - strip_status(Packet); - _Allowed -> Packet - end, - NewState = add_user_presence(From, Stanza, - StateData), - send_new_presence(From, NewState), - NewState - end; - _ -> add_new_user(From, Nick, Packet, StateData) - end; - _ -> StateData - end, - close_room_if_temporary_and_empty(StateData1). +%% @doc Get information of this participant, or default values. +%% If the JID is not a participant, return values for a service message. +-spec get_participant_data(jid(), state()) -> {binary(), role()}. +get_participant_data(From, StateData) -> + try maps:get(jid:tolower(From), StateData#state.users) of + #user{nick = FromNick, role = Role} -> + {FromNick, Role} + catch _:{badkey, _} -> + try muc_subscribers_get(jid:tolower(jid:remove_resource(From)), + StateData#state.muc_subscribers) of + #subscriber{nick = FromNick} -> + {FromNick, none} + catch _:{badkey, _} -> + {From#jid.luser, moderator} + end + end. +-spec process_presence(binary(), presence(), state()) -> fsm_transition(). +process_presence(Nick, #presence{from = From, type = Type0} = Packet0, StateData) -> + IsOnline = is_user_online(From, StateData), + if Type0 == available; + IsOnline and ((Type0 == unavailable) or (Type0 == error)) -> + case ejabberd_hooks:run_fold(muc_filter_presence, + StateData#state.server_host, + Packet0, + [StateData, Nick]) of + drop -> + {next_state, normal_state, StateData}; + #presence{} = Packet -> + close_room_if_temporary_and_empty( + do_process_presence(Nick, Packet, StateData)) + end; + true -> + {next_state, normal_state, StateData} + end. + +-spec do_process_presence(binary(), presence(), state()) -> state(). +do_process_presence(Nick, #presence{from = From, type = available, lang = Lang} = Packet, + StateData) -> + case is_user_online(From, StateData) of + false -> + add_new_user(From, Nick, Packet, StateData); + true -> + case is_nick_change(From, Nick, StateData) of + true -> + case {nick_collision(From, Nick, StateData), + mod_muc:can_use_nick(StateData#state.server_host, + jid:encode(StateData#state.jid), + From, Nick), + {(StateData#state.config)#config.allow_visitor_nickchange, + is_visitor(From, StateData)}} of + {_, _, {false, true}} -> + Packet1 = Packet#presence{sub_els = [#muc{}]}, + ErrText = ?T("Visitors are not allowed to change their " + "nicknames in this room"), + Err = xmpp:err_not_allowed(ErrText, Lang), + ejabberd_router:route_error(Packet1, Err), + StateData; + {true, _, _} -> + Packet1 = Packet#presence{sub_els = [#muc{}]}, + ErrText = ?T("That nickname is already in use by another " + "occupant"), + Err = xmpp:err_conflict(ErrText, Lang), + ejabberd_router:route_error(Packet1, Err), + StateData; + {_, false, _} -> + Packet1 = Packet#presence{sub_els = [#muc{}]}, + Err = case Nick of + <<>> -> + xmpp:err_jid_malformed(?T("Nickname can't be empty"), + Lang); + _ -> + xmpp:err_conflict(?T("That nickname is registered" + " by another person"), Lang) + end, + ejabberd_router:route_error(Packet1, Err), + StateData; + _ -> + change_nick(From, Nick, StateData) + end; + false -> + Stanza = maybe_strip_status_from_presence( + From, Packet, StateData), + NewState = add_user_presence(From, Stanza, + StateData), + case xmpp:has_subtag(Packet, #muc{}) of + true -> + send_initial_presences_and_messages( + From, Nick, Packet, NewState, StateData); + false -> + send_new_presence(From, NewState, StateData) + end, + NewState + end + end; +do_process_presence(Nick, #presence{from = From, type = unavailable} = Packet, + StateData) -> + NewPacket = case {(StateData#state.config)#config.allow_visitor_status, + is_visitor(From, StateData)} of + {false, true} -> + strip_status(Packet); + _ -> Packet + end, + NewState = add_user_presence_un(From, NewPacket, StateData), + case maps:get(Nick, StateData#state.nicks, []) of + [_, _ | _] -> + Aff = get_affiliation(From, StateData), + Item = #muc_item{affiliation = Aff, role = none, jid = From}, + Pres = xmpp:set_subtag( + Packet, #muc_user{items = [Item], + status_codes = [110]}), + send_wrapped(jid:replace_resource(StateData#state.jid, Nick), + From, Pres, ?NS_MUCSUB_NODES_PRESENCE, StateData); + _ -> + send_new_presence(From, NewState, StateData) + end, + Reason = xmpp:get_text(NewPacket#presence.status), + remove_online_user(From, NewState, Reason); +do_process_presence(_Nick, #presence{from = From, type = error, lang = Lang} = Packet, + StateData) -> + ErrorText = ?T("It is not allowed to send error messages to the" + " room. The participant (~s) has sent an error " + "message (~s) and got kicked from the room"), + expulse_participant(Packet, From, StateData, + translate:translate(Lang, ErrorText)). + +-spec maybe_strip_status_from_presence(jid(), presence(), + state()) -> presence(). +maybe_strip_status_from_presence(From, Packet, StateData) -> + case {(StateData#state.config)#config.allow_visitor_status, + is_visitor(From, StateData)} of + {false, true} -> + strip_status(Packet); + _Allowed -> Packet + end. + +-spec close_room_if_temporary_and_empty(state()) -> fsm_transition(). close_room_if_temporary_and_empty(StateData1) -> case not (StateData1#state.config)#config.persistent - andalso (?DICT):to_list(StateData1#state.users) == [] - of + andalso maps:size(StateData1#state.users) == 0 + andalso muc_subscribers_size(StateData1#state.muc_subscribers) == 0 of true -> - ?INFO_MSG("Destroyed MUC room ~s because it's temporary " + ?INFO_MSG("Destroyed MUC room ~ts because it's temporary " "and empty", - [jlib:jid_to_string(StateData1#state.jid)]), + [jid:encode(StateData1#state.jid)]), add_to_log(room_existence, destroyed, StateData1), + forget_room(StateData1), {stop, normal, StateData1}; _ -> {next_state, normal_state, StateData1} end. +-spec get_users_and_subscribers(state()) -> users(). +get_users_and_subscribers(StateData) -> + get_users_and_subscribers_aux( + StateData#state.muc_subscribers#muc_subscribers.subscribers, + StateData). + +-spec get_users_and_subscribers_with_node(binary(), state()) -> users(). +get_users_and_subscribers_with_node(Node, StateData) -> + get_users_and_subscribers_aux( + muc_subscribers_get_by_node(Node, StateData#state.muc_subscribers), + StateData). + +get_users_and_subscribers_aux(Subscribers, StateData) -> + OnlineSubscribers = maps:fold( + fun(LJID, _, Acc) -> + LBareJID = jid:remove_resource(LJID), + case is_subscriber(LBareJID, StateData) of + true -> + ?SETS:add_element(LBareJID, Acc); + false -> + Acc + end + end, ?SETS:new(), StateData#state.users), + maps:fold( + fun(LBareJID, #subscriber{nick = Nick}, Acc) -> + case ?SETS:is_element(LBareJID, OnlineSubscribers) of + false -> + maps:put(LBareJID, + #user{jid = jid:make(LBareJID), + nick = Nick, + role = none, + last_presence = undefined}, + Acc); + true -> + Acc + end + end, StateData#state.users, Subscribers). + +-spec is_user_online(jid(), state()) -> boolean(). is_user_online(JID, StateData) -> - LJID = jlib:jid_tolower(JID), - (?DICT):is_key(LJID, StateData#state.users). + LJID = jid:tolower(JID), + maps:is_key(LJID, StateData#state.users). + +-spec is_subscriber(jid(), state()) -> boolean(). +is_subscriber(JID, StateData) -> + LJID = jid:tolower(jid:remove_resource(JID)), + muc_subscribers_is_key(LJID, StateData#state.muc_subscribers). %% Check if the user is occupant of the room, or at least is an admin or owner. +-spec is_occupant_or_admin(jid(), state()) -> boolean(). is_occupant_or_admin(JID, StateData) -> FAffiliation = get_affiliation(JID, StateData), FRole = get_role(JID, StateData), @@ -1164,109 +1588,26 @@ is_occupant_or_admin(JID, StateData) -> _ -> false end. -%%% -%%% Handle IQ queries of vCard -%%% -is_user_online_iq(StanzaId, JID, StateData) - when JID#jid.lresource /= <<"">> -> - {is_user_online(JID, StateData), StanzaId, JID}; -is_user_online_iq(StanzaId, JID, StateData) - when JID#jid.lresource == <<"">> -> - try stanzaid_unpack(StanzaId) of - {OriginalId, Resource} -> - JIDWithResource = jlib:jid_replace_resource(JID, - Resource), - {is_user_online(JIDWithResource, StateData), OriginalId, - JIDWithResource} - catch - _:_ -> {is_user_online(JID, StateData), StanzaId, JID} - end. - -handle_iq_vcard(FromFull, ToJID, StanzaId, NewId, - Packet) -> - ToBareJID = jlib:jid_remove_resource(ToJID), - IQ = jlib:iq_query_info(Packet), - handle_iq_vcard2(FromFull, ToJID, ToBareJID, StanzaId, - NewId, IQ, Packet). - -handle_iq_vcard2(_FromFull, ToJID, ToBareJID, StanzaId, - _NewId, #iq{type = get, xmlns = ?NS_VCARD}, Packet) - when ToBareJID /= ToJID -> - {ToBareJID, change_stanzaid(StanzaId, ToJID, Packet)}; -handle_iq_vcard2(_FromFull, ToJID, _ToBareJID, - _StanzaId, NewId, _IQ, Packet) -> - {ToJID, change_stanzaid(NewId, Packet)}. - -stanzaid_pack(OriginalId, Resource) -> - <<"berd", - (jlib:encode_base64(<<"ejab\000", - OriginalId/binary, "\000", - Resource/binary>>))/binary>>. - -stanzaid_unpack(<<"berd", StanzaIdBase64/binary>>) -> - StanzaId = jlib:decode_base64(StanzaIdBase64), - [<<"ejab">>, OriginalId, Resource] = - str:tokens(StanzaId, <<"\000">>), - {OriginalId, Resource}. - -change_stanzaid(NewId, Packet) -> - #xmlel{name = Name, attrs = Attrs, children = Els} = - jlib:remove_attr(<<"id">>, Packet), - #xmlel{name = Name, attrs = [{<<"id">>, NewId} | Attrs], - children = Els}. - -change_stanzaid(PreviousId, ToJID, Packet) -> - NewId = stanzaid_pack(PreviousId, ToJID#jid.lresource), - change_stanzaid(NewId, Packet). - -%%% -%%% - -role_to_list(Role) -> - case Role of - moderator -> <<"moderator">>; - participant -> <<"participant">>; - visitor -> <<"visitor">>; - none -> <<"none">> - end. - -affiliation_to_list(Affiliation) -> - case Affiliation of - owner -> <<"owner">>; - admin -> <<"admin">>; - member -> <<"member">>; - outcast -> <<"outcast">>; - none -> <<"none">> - end. - -list_to_role(Role) -> - case Role of - <<"moderator">> -> moderator; - <<"participant">> -> participant; - <<"visitor">> -> visitor; - <<"none">> -> none - end. - -list_to_affiliation(Affiliation) -> - case Affiliation of - <<"owner">> -> owner; - <<"admin">> -> admin; - <<"member">> -> member; - <<"outcast">> -> outcast; - <<"none">> -> none - 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} -decide_fate_message(<<"error">>, Packet, From, - StateData) -> - PD = case check_error_kick(Packet) of +-spec decide_fate_message(message(), jid(), state()) -> + continue_delivery | forget_message | + {expulse_sender, binary()}. +decide_fate_message(#message{type = error} = Msg, + From, StateData) -> + Err = xmpp:get_error(Msg), + PD = case check_error_kick(Err) of %% If this is an error stanza and its condition matches a criteria true -> - Reason = - io_lib:format("This participant is considered a ghost " - "and is expulsed: ~s", - [jlib:jid_to_string(From)]), + Reason = str:format("This participant is considered a ghost " + "and is expulsed: ~s", + [jid:encode(From)]), {expulse_sender, Reason}; false -> continue_delivery end, @@ -1278,118 +1619,209 @@ decide_fate_message(<<"error">>, Packet, From, end; Other -> Other end; -decide_fate_message(_, _, _, _) -> continue_delivery. +decide_fate_message(_, _, _) -> continue_delivery. %% Check if the elements of this error stanza indicate %% that the sender is a dead participant. %% If so, return true to kick the participant. -check_error_kick(Packet) -> - case get_error_condition(Packet) of - <<"gone">> -> true; - <<"internal-server-error">> -> true; - <<"item-not-found">> -> true; - <<"jid-malformed">> -> true; - <<"recipient-unavailable">> -> true; - <<"redirect">> -> true; - <<"remote-server-not-found">> -> true; - <<"remote-server-timeout">> -> true; - <<"service-unavailable">> -> true; - _ -> false - end. +-spec check_error_kick(stanza_error()) -> boolean(). +check_error_kick(#stanza_error{reason = Reason}) -> + case Reason of + #gone{} -> true; + 'internal-server-error' -> true; + 'item-not-found' -> true; + 'jid-malformed' -> true; + 'recipient-unavailable' -> true; + #redirect{} -> true; + 'remote-server-not-found' -> true; + 'remote-server-timeout' -> true; + 'service-unavailable' -> true; + _ -> false + end; +check_error_kick(undefined) -> + false. -get_error_condition(Packet) -> - case catch get_error_condition2(Packet) of - {condition, ErrorCondition} -> ErrorCondition; - {'EXIT', _} -> <<"badformed error stanza">> - end. +-spec get_error_condition(stanza_error()) -> string(). +get_error_condition(#stanza_error{reason = Reason}) -> + case Reason of + #gone{} -> "gone"; + #redirect{} -> "redirect"; + Atom -> atom_to_list(Atom) + end; +get_error_condition(undefined) -> + "undefined". -get_error_condition2(Packet) -> - #xmlel{children = EEls} = xml:get_subtag(Packet, - <<"error">>), - [Condition] = [Name - || #xmlel{name = Name, - attrs = [{<<"xmlns">>, ?NS_STANZAS}], - children = []} - <- EEls], - {condition, Condition}. +-spec get_error_text(stanza_error()) -> binary(). +get_error_text(#stanza_error{text = Txt}) -> + xmpp:get_text(Txt). +-spec make_reason(stanza(), jid(), state(), binary()) -> binary(). +make_reason(Packet, From, StateData, Reason1) -> + #user{nick = FromNick} = maps:get(jid:tolower(From), StateData#state.users), + Condition = get_error_condition(xmpp:get_error(Packet)), + Reason2 = unicode:characters_to_list(Reason1), + str:format(Reason2, [FromNick, Condition]). + +-spec expulse_participant(stanza(), jid(), state(), binary()) -> + state(). expulse_participant(Packet, From, StateData, Reason1) -> - ErrorCondition = get_error_condition(Packet), - Reason2 = iolist_to_binary( - io_lib:format(binary_to_list(Reason1) ++ ": " ++ "~s", - [ErrorCondition])), + Reason2 = make_reason(Packet, From, StateData, Reason1), NewState = add_user_presence_un(From, - #xmlel{name = <<"presence">>, - attrs = - [{<<"type">>, - <<"unavailable">>}], - children = - [#xmlel{name = <<"status">>, - attrs = [], - children = - [{xmlcdata, - Reason2}]}]}, + #presence{type = unavailable, + status = xmpp:mk_text(Reason2)}, StateData), - send_new_presence(From, NewState), + LJID = jid:tolower(From), + #user{nick = Nick} = maps:get(LJID, StateData#state.users), + case maps:get(Nick, StateData#state.nicks, []) of + [_, _ | _] -> + Aff = get_affiliation(From, StateData), + Item = #muc_item{affiliation = Aff, role = none, jid = From}, + Pres = xmpp:set_subtag( + Packet, #muc_user{items = [Item], + status_codes = [110]}), + send_wrapped(jid:replace_resource(StateData#state.jid, Nick), + From, Pres, ?NS_MUCSUB_NODES_PRESENCE, StateData); + _ -> + send_new_presence(From, NewState, StateData) + end, remove_online_user(From, NewState). +-spec set_affiliation(jid(), affiliation(), state()) -> state(). set_affiliation(JID, Affiliation, StateData) -> set_affiliation(JID, Affiliation, StateData, <<"">>). +-spec set_affiliation(jid(), affiliation(), state(), binary()) -> state(). +set_affiliation(JID, Affiliation, + #state{config = #config{persistent = false}} = StateData, + Reason) -> + set_affiliation_fallback(JID, Affiliation, StateData, Reason); set_affiliation(JID, Affiliation, StateData, Reason) -> - LJID = jlib:jid_remove_resource(jlib:jid_tolower(JID)), + ServerHost = StateData#state.server_host, + Room = StateData#state.room, + Host = StateData#state.host, + Mod = gen_mod:db_mod(ServerHost, mod_muc), + case Mod:set_affiliation(ServerHost, Room, Host, JID, Affiliation, Reason) of + ok -> + StateData; + {error, _} -> + set_affiliation_fallback(JID, Affiliation, StateData, Reason) + end. + +-spec set_affiliation_fallback(jid(), affiliation(), state(), binary()) -> state(). +set_affiliation_fallback(JID, Affiliation, StateData, Reason) -> + LJID = jid:remove_resource(jid:tolower(JID)), Affiliations = case Affiliation of - none -> - (?DICT):erase(LJID, StateData#state.affiliations); - _ -> - (?DICT):store(LJID, {Affiliation, Reason}, - StateData#state.affiliations) + none -> + maps:remove(LJID, StateData#state.affiliations); + _ -> + maps:put(LJID, {Affiliation, Reason}, + StateData#state.affiliations) end, StateData#state{affiliations = Affiliations}. -get_affiliation(JID, StateData) -> - {_AccessRoute, _AccessCreate, AccessAdmin, - _AccessPersistent} = - StateData#state.access, - Res = case acl:match_rule(StateData#state.server_host, - AccessAdmin, JID) - of - allow -> owner; - _ -> - LJID = jlib:jid_tolower(JID), - case (?DICT):find(LJID, StateData#state.affiliations) of - {ok, Affiliation} -> Affiliation; - _ -> - LJID1 = jlib:jid_remove_resource(LJID), - case (?DICT):find(LJID1, StateData#state.affiliations) - of - {ok, Affiliation} -> Affiliation; - _ -> - LJID2 = setelement(1, LJID, <<"">>), - case (?DICT):find(LJID2, - StateData#state.affiliations) - of - {ok, Affiliation} -> Affiliation; - _ -> - LJID3 = jlib:jid_remove_resource(LJID2), - case (?DICT):find(LJID3, - StateData#state.affiliations) - of - {ok, Affiliation} -> Affiliation; - _ -> none - end - end - end - end - end, - case Res of - {A, _Reason} -> A; - _ -> Res +-spec set_affiliations(affiliations(), state()) -> state(). +set_affiliations(Affiliations, + #state{config = #config{persistent = false}} = StateData) -> + set_affiliations_fallback(Affiliations, StateData); +set_affiliations(Affiliations, StateData) -> + Room = StateData#state.room, + Host = StateData#state.host, + ServerHost = StateData#state.server_host, + Mod = gen_mod:db_mod(ServerHost, mod_muc), + case Mod:set_affiliations(ServerHost, Room, Host, Affiliations) of + ok -> + StateData; + {error, _} -> + set_affiliations_fallback(Affiliations, StateData) end. +-spec set_affiliations_fallback(affiliations(), state()) -> state(). +set_affiliations_fallback(Affiliations, StateData) -> + StateData#state{affiliations = Affiliations}. + +-spec get_affiliation(ljid() | jid(), state()) -> affiliation(). +get_affiliation(#jid{} = JID, StateData) -> + case get_service_affiliation(JID, StateData) of + owner -> + owner; + none -> + Aff = case do_get_affiliation(JID, StateData) of + {Affiliation, _Reason} -> Affiliation; + Affiliation -> Affiliation + end, + case {Aff, (StateData#state.config)#config.members_only} of + % Subscribers should be have members affiliation in this case + {none, true} -> + case is_subscriber(JID, StateData) of + true -> member; + _ -> none + end; + _ -> + Aff + end + end; +get_affiliation(LJID, StateData) -> + get_affiliation(jid:make(LJID), StateData). + +-spec do_get_affiliation(jid(), state()) -> affiliation() | {affiliation(), binary()}. +do_get_affiliation(JID, #state{config = #config{persistent = false}} = StateData) -> + do_get_affiliation_fallback(JID, StateData); +do_get_affiliation(JID, StateData) -> + Room = StateData#state.room, + Host = StateData#state.host, + LServer = JID#jid.lserver, + LUser = JID#jid.luser, + ServerHost = StateData#state.server_host, + Mod = gen_mod:db_mod(ServerHost, mod_muc), + case Mod:get_affiliation(ServerHost, Room, Host, LUser, LServer) of + {error, _} -> + do_get_affiliation_fallback(JID, StateData); + {ok, Affiliation} -> + Affiliation + end. + +-spec do_get_affiliation_fallback(jid(), state()) -> affiliation() | {affiliation(), binary()}. +do_get_affiliation_fallback(JID, StateData) -> + LJID = jid:tolower(JID), + try maps:get(LJID, StateData#state.affiliations) + catch _:{badkey, _} -> + BareLJID = jid:remove_resource(LJID), + try maps:get(BareLJID, StateData#state.affiliations) + catch _:{badkey, _} -> + DomainLJID = setelement(1, LJID, <<"">>), + try maps:get(DomainLJID, StateData#state.affiliations) + catch _:{badkey, _} -> + DomainBareLJID = jid:remove_resource(DomainLJID), + try maps:get(DomainBareLJID, StateData#state.affiliations) + catch _:{badkey, _} -> none + end + end + end + end. + +-spec get_affiliations(state()) -> affiliations(). +get_affiliations(#state{config = #config{persistent = false}} = StateData) -> + get_affiliations_fallback(StateData); +get_affiliations(StateData) -> + Room = StateData#state.room, + Host = StateData#state.host, + ServerHost = StateData#state.server_host, + Mod = gen_mod:db_mod(ServerHost, mod_muc), + case Mod:get_affiliations(ServerHost, Room, Host) of + {error, _} -> + get_affiliations_fallback(StateData); + {ok, Affiliations} -> + Affiliations + end. + +-spec get_affiliations_fallback(state()) -> affiliations(). +get_affiliations_fallback(StateData) -> + StateData#state.affiliations. + +-spec get_service_affiliation(jid(), state()) -> owner | none. get_service_affiliation(JID, StateData) -> {_AccessRoute, _AccessCreate, AccessAdmin, - _AccessPersistent} = + _AccessPersistent, _AccessMam} = StateData#state.access, case acl:match_rule(StateData#state.server_host, AccessAdmin, JID) @@ -1398,60 +1830,72 @@ get_service_affiliation(JID, StateData) -> _ -> none end. +-spec set_role(jid(), role(), state()) -> state(). set_role(JID, Role, StateData) -> - LJID = jlib:jid_tolower(JID), + LJID = jid:tolower(JID), LJIDs = case LJID of {U, S, <<"">>} -> - (?DICT):fold(fun (J, _, Js) -> - case J of - {U, S, _} -> [J | Js]; - _ -> Js - end - end, - [], StateData#state.users); + maps:fold(fun (J, _, Js) -> + case J of + {U, S, _} -> [J | Js]; + _ -> Js + end + end, [], StateData#state.users); _ -> - case (?DICT):is_key(LJID, StateData#state.users) of + case maps:is_key(LJID, StateData#state.users) of true -> [LJID]; _ -> [] end end, - {Users, Nicks} = case Role of - none -> - lists:foldl(fun (J, {Us, Ns}) -> - NewNs = case (?DICT):find(J, Us) - of - {ok, - #user{nick = Nick}} -> - (?DICT):erase(Nick, - Ns); - _ -> Ns - end, - {(?DICT):erase(J, Us), NewNs} - end, - {StateData#state.users, - StateData#state.nicks}, - LJIDs); - _ -> - {lists:foldl(fun (J, Us) -> - {ok, User} = (?DICT):find(J, - Us), - (?DICT):store(J, - User#user{role = - Role}, - Us) - end, - StateData#state.users, LJIDs), - StateData#state.nicks} - end, - StateData#state{users = Users, nicks = Nicks}. + {Users, Nicks} = + case Role of + none -> + lists:foldl( + fun (J, {Us, Ns}) -> + NewNs = try maps:get(J, Us) of + #user{nick = Nick} -> + maps:remove(Nick, Ns) + catch _:{badkey, _} -> + Ns + end, + {maps:remove(J, Us), NewNs} + end, + {StateData#state.users, StateData#state.nicks}, LJIDs); + _ -> + {lists:foldl( + fun (J, Us) -> + User = maps:get(J, Us), + if User#user.last_presence == undefined -> + Us; + true -> + maps:put(J, User#user{role = Role}, Us) + end + end, StateData#state.users, LJIDs), + StateData#state.nicks} + end, + Affiliation = get_affiliation(JID, StateData), + Roles = case Role of + %% Don't persist 'none' role: if someone is kicked, they will + %% maintain the same role they had *before* they were kicked, + %% unless they were banned + none when Affiliation /= outcast -> + maps:remove(jid:remove_resource(LJID), StateData#state.roles); + NewRole -> + maps:put(jid:remove_resource(LJID), + NewRole, + StateData#state.roles) + end, + StateData#state{users = Users, nicks = Nicks, roles = Roles}. +-spec get_role(jid(), state()) -> role(). get_role(JID, StateData) -> - LJID = jlib:jid_tolower(JID), - case (?DICT):find(LJID, StateData#state.users) of - {ok, #user{role = Role}} -> Role; - _ -> none + LJID = jid:tolower(JID), + try maps:get(LJID, StateData#state.users) of + #user{role = Role} -> Role + catch _:{badkey, _} -> none end. +-spec get_default_role(affiliation(), state()) -> role(). get_default_role(Affiliation, StateData) -> case Affiliation of owner -> moderator; @@ -1470,12 +1914,15 @@ get_default_role(Affiliation, StateData) -> end end. +-spec is_visitor(jid(), state()) -> boolean(). is_visitor(Jid, StateData) -> get_role(Jid, StateData) =:= visitor. +-spec is_moderator(jid(), state()) -> boolean(). is_moderator(Jid, StateData) -> get_role(Jid, StateData) =:= moderator. +-spec get_max_users(state()) -> non_neg_integer(). get_max_users(StateData) -> MaxUsers = (StateData#state.config)#config.max_users, ServiceMaxUsers = get_service_max_users(StateData), @@ -1483,53 +1930,51 @@ get_max_users(StateData) -> true -> ServiceMaxUsers end. +-spec get_service_max_users(state()) -> pos_integer(). get_service_max_users(StateData) -> - gen_mod:get_module_opt(StateData#state.server_host, - mod_muc, max_users, - fun(I) when is_integer(I), I>0 -> I end, - ?MAX_USERS_DEFAULT). + mod_muc_opt:max_users(StateData#state.server_host). +-spec get_max_users_admin_threshold(state()) -> pos_integer(). get_max_users_admin_threshold(StateData) -> - gen_mod:get_module_opt(StateData#state.server_host, - mod_muc, max_users_admin_threshold, - fun(I) when is_integer(I), I>0 -> I end, - 5). + mod_muc_opt:max_users_admin_threshold(StateData#state.server_host). +-spec room_queue_new(binary(), ejabberd_shaper:shaper(), _) -> p1_queue:queue({message | presence, jid()}) | undefined. +room_queue_new(ServerHost, Shaper, QueueType) -> + HaveRoomShaper = Shaper /= none, + HaveMessageShaper = mod_muc_opt:user_message_shaper(ServerHost) /= none, + HavePresenceShaper = mod_muc_opt:user_presence_shaper(ServerHost) /= none, + HaveMinMessageInterval = mod_muc_opt:min_message_interval(ServerHost) /= 0, + HaveMinPresenceInterval = mod_muc_opt:min_presence_interval(ServerHost) /= 0, + if HaveRoomShaper or HaveMessageShaper or HavePresenceShaper + or HaveMinMessageInterval or HaveMinPresenceInterval -> + p1_queue:new(QueueType); + true -> + undefined + end. + +-spec get_user_activity(jid(), state()) -> #activity{}. get_user_activity(JID, StateData) -> - case treap:lookup(jlib:jid_tolower(JID), + case treap:lookup(jid:tolower(JID), StateData#state.activity) of {ok, _P, A} -> A; error -> MessageShaper = - shaper:new(gen_mod:get_module_opt(StateData#state.server_host, - mod_muc, user_message_shaper, - fun(A) when is_atom(A) -> A end, - none)), + ejabberd_shaper:new(mod_muc_opt:user_message_shaper(StateData#state.server_host)), PresenceShaper = - shaper:new(gen_mod:get_module_opt(StateData#state.server_host, - mod_muc, user_presence_shaper, - fun(A) when is_atom(A) -> A end, - none)), + ejabberd_shaper:new(mod_muc_opt:user_presence_shaper(StateData#state.server_host)), #activity{message_shaper = MessageShaper, presence_shaper = PresenceShaper} end. +-spec store_user_activity(jid(), #activity{}, state()) -> state(). store_user_activity(JID, UserActivity, StateData) -> MinMessageInterval = - trunc(gen_mod:get_module_opt(StateData#state.server_host, - mod_muc, min_message_interval, - fun(I) when is_number(I), I>=0 -> I end, - 0) - * 1000), + trunc(mod_muc_opt:min_message_interval(StateData#state.server_host) * 1000), MinPresenceInterval = - trunc(gen_mod:get_module_opt(StateData#state.server_host, - mod_muc, min_presence_interval, - fun(I) when is_number(I), I>=0 -> I end, - 0) - * 1000), - Key = jlib:jid_tolower(JID), - Now = now_to_usec(now()), + trunc(mod_muc_opt:min_presence_interval(StateData#state.server_host) * 1000), + Key = jid:tolower(JID), + Now = erlang:system_time(microsecond), Activity1 = clean_treap(StateData#state.activity, {1, -Now}), Activity = case treap:lookup(Key, Activity1) of @@ -1551,10 +1996,10 @@ store_user_activity(JID, UserActivity, StateData) -> of true -> {_, MessageShaperInterval} = - shaper:update(UserActivity#activity.message_shaper, + ejabberd_shaper:update(UserActivity#activity.message_shaper, 100000), {_, PresenceShaperInterval} = - shaper:update(UserActivity#activity.presence_shaper, + ejabberd_shaper:update(UserActivity#activity.presence_shaper, 100000), Delay = lists:max([MessageShaperInterval, PresenceShaperInterval, @@ -1574,8 +2019,9 @@ store_user_activity(JID, UserActivity, StateData) -> Activity)} end end, - StateData1. + reset_hibernate_timer(StateData1). +-spec clean_treap(treap:treap(), integer() | {1, integer()}) -> treap:treap(). clean_treap(Treap, CleanPriority) -> case treap:is_empty(Treap) of true -> Treap; @@ -1587,14 +2033,15 @@ clean_treap(Treap, CleanPriority) -> end end. +-spec prepare_room_queue(state()) -> state(). prepare_room_queue(StateData) -> - case queue:out(StateData#state.room_queue) of + case p1_queue:out(StateData#state.room_queue) of {{value, {message, From}}, _RoomQueue} -> Activity = get_user_activity(From, StateData), Packet = Activity#activity.message, Size = element_size(Packet), {RoomShaper, RoomShaperInterval} = - shaper:update(StateData#state.room_shaper, Size), + ejabberd_shaper:update(StateData#state.room_shaper, Size), erlang:send_after(RoomShaperInterval, self(), process_room_queue), StateData#state{room_shaper = RoomShaper}; @@ -1603,367 +2050,442 @@ prepare_room_queue(StateData) -> {_Nick, Packet} = Activity#activity.presence, Size = element_size(Packet), {RoomShaper, RoomShaperInterval} = - shaper:update(StateData#state.room_shaper, Size), + ejabberd_shaper:update(StateData#state.room_shaper, Size), erlang:send_after(RoomShaperInterval, self(), process_room_queue), StateData#state{room_shaper = RoomShaper}; {empty, _} -> StateData end. -add_online_user(JID, Nick, Role, StateData) -> - LJID = jlib:jid_tolower(JID), - Users = (?DICT):store(LJID, - #user{jid = JID, nick = Nick, role = Role}, - StateData#state.users), +-spec update_online_user(jid(), #user{}, state()) -> state(). +update_online_user(JID, #user{nick = Nick} = User, StateData) -> + LJID = jid:tolower(JID), add_to_log(join, Nick, StateData), - Nicks = (?DICT):update(Nick, - fun (Entry) -> - case lists:member(LJID, Entry) of - true -> Entry; - false -> [LJID | Entry] - end - end, - [LJID], StateData#state.nicks), - tab_add_online_user(JID, StateData), - StateData#state{users = Users, nicks = Nicks}. + Nicks1 = try maps:get(LJID, StateData#state.users) of + #user{nick = OldNick} -> + case lists:delete( + LJID, maps:get(OldNick, StateData#state.nicks)) of + [] -> + maps:remove(OldNick, StateData#state.nicks); + LJIDs -> + maps:put(OldNick, LJIDs, StateData#state.nicks) + end + catch _:{badkey, _} -> + StateData#state.nicks + end, + Nicks = maps:update_with(Nick, + fun (LJIDs) -> [LJID|LJIDs -- [LJID]] end, + [LJID], Nicks1), + Users = maps:update_with(LJID, + fun(U) -> + U#user{nick = Nick} + end, User, StateData#state.users), + NewStateData = StateData#state{users = Users, nicks = Nicks}, + case {maps:get(LJID, StateData#state.users, error), + maps:get(LJID, NewStateData#state.users, error)} of + {#user{nick = Old}, #user{nick = New}} when Old /= New -> + send_nick_changing(JID, Old, NewStateData, true, true); + _ -> + ok + end, + NewStateData. +-spec set_subscriber(jid(), binary(), [binary()], state()) -> state(). +set_subscriber(JID, Nick, Nodes, + #state{room = Room, host = Host, server_host = ServerHost} = StateData) -> + BareJID = jid:remove_resource(JID), + LBareJID = jid:tolower(BareJID), + MUCSubscribers = + muc_subscribers_put( + #subscriber{jid = BareJID, + nick = Nick, + nodes = Nodes}, + StateData#state.muc_subscribers), + NewStateData = StateData#state{muc_subscribers = MUCSubscribers}, + store_room(NewStateData, [{add_subscription, BareJID, Nick, Nodes}]), + case not muc_subscribers_is_key(LBareJID, StateData#state.muc_subscribers) of + true -> + Packet1a = #message{ + sub_els = [#ps_event{ + items = #ps_items{ + node = ?NS_MUCSUB_NODES_SUBSCRIBERS, + items = [#ps_item{ + id = p1_rand:get_string(), + sub_els = [#muc_subscribe{jid = BareJID, nick = Nick}]}]}}]}, + Packet1b = #message{ + sub_els = [#ps_event{ + items = #ps_items{ + node = ?NS_MUCSUB_NODES_SUBSCRIBERS, + items = [#ps_item{ + id = p1_rand:get_string(), + sub_els = [#muc_subscribe{nick = Nick}]}]}}]}, + {Packet2a, Packet2b} = ejabberd_hooks:run_fold(muc_subscribed, ServerHost, {Packet1a, Packet1b}, + [ServerHost, Room, Host, BareJID, StateData]), + send_subscriptions_change_notifications(Packet2a, Packet2b, NewStateData); + _ -> + ok + end, + NewStateData. + +-spec add_online_user(jid(), binary(), role(), state()) -> state(). +add_online_user(JID, Nick, Role, StateData) -> + tab_add_online_user(JID, StateData), + User = #user{jid = JID, nick = Nick, role = Role}, + reset_hibernate_timer(update_online_user(JID, User, StateData)). + +-spec remove_online_user(jid(), state()) -> state(). remove_online_user(JID, StateData) -> remove_online_user(JID, StateData, <<"">>). +-spec remove_online_user(jid(), state(), binary()) -> state(). remove_online_user(JID, StateData, Reason) -> - LJID = jlib:jid_tolower(JID), - {ok, #user{nick = Nick}} = (?DICT):find(LJID, - StateData#state.users), + LJID = jid:tolower(JID), + #user{nick = Nick} = maps:get(LJID, StateData#state.users), add_to_log(leave, {Nick, Reason}, StateData), tab_remove_online_user(JID, StateData), - Users = (?DICT):erase(LJID, StateData#state.users), - Nicks = case (?DICT):find(Nick, StateData#state.nicks) - of - {ok, [LJID]} -> - (?DICT):erase(Nick, StateData#state.nicks); - {ok, U} -> - (?DICT):store(Nick, U -- [LJID], StateData#state.nicks); - error -> StateData#state.nicks + Users = maps:remove(LJID, StateData#state.users), + Nicks = try maps:get(Nick, StateData#state.nicks) of + [LJID] -> + maps:remove(Nick, StateData#state.nicks); + U -> + maps:put(Nick, U -- [LJID], StateData#state.nicks) + catch _:{badkey, _} -> + StateData#state.nicks end, - StateData#state{users = Users, nicks = Nicks}. + reset_hibernate_timer(StateData#state{users = Users, nicks = Nicks}). -filter_presence(#xmlel{name = <<"presence">>, - attrs = Attrs, children = Els}) -> - FEls = lists:filter(fun (El) -> - case El of - {xmlcdata, _} -> false; - #xmlel{attrs = Attrs1} -> - XMLNS = xml:get_attr_s(<<"xmlns">>, - Attrs1), - NS_MUC = ?NS_MUC, - Size = byte_size(NS_MUC), - case XMLNS of - <> -> - false; - _ -> - true - end - end - end, - Els), - #xmlel{name = <<"presence">>, attrs = Attrs, - children = FEls}. +-spec filter_presence(presence()) -> presence(). +filter_presence(Presence) -> + Els = lists:filter( + fun(El) -> + XMLNS = xmpp:get_ns(El), + case catch binary:part(XMLNS, 0, size(?NS_MUC)) of + ?NS_MUC -> false; + _ -> XMLNS /= ?NS_HATS + end + end, xmpp:get_els(Presence)), + xmpp:set_els(Presence, Els). -strip_status(#xmlel{name = <<"presence">>, - attrs = Attrs, children = Els}) -> - FEls = lists:filter(fun (#xmlel{name = <<"status">>}) -> - false; - (_) -> true - end, - Els), - #xmlel{name = <<"presence">>, attrs = Attrs, - children = FEls}. +-spec strip_status(presence()) -> presence(). +strip_status(Presence) -> + Presence#presence{status = []}. +-spec add_user_presence(jid(), presence(), state()) -> state(). add_user_presence(JID, Presence, StateData) -> - LJID = jlib:jid_tolower(JID), + LJID = jid:tolower(JID), FPresence = filter_presence(Presence), - Users = (?DICT):update(LJID, - fun (#user{} = User) -> - User#user{last_presence = FPresence} - end, - StateData#state.users), + Users = maps:update_with(LJID, + fun (#user{} = User) -> + User#user{last_presence = FPresence} + end, StateData#state.users), StateData#state{users = Users}. +-spec add_user_presence_un(jid(), presence(), state()) -> state(). add_user_presence_un(JID, Presence, StateData) -> - LJID = jlib:jid_tolower(JID), + LJID = jid:tolower(JID), FPresence = filter_presence(Presence), - Users = (?DICT):update(LJID, - fun (#user{} = User) -> - User#user{last_presence = FPresence, - role = none} - end, - StateData#state.users), + Users = maps:update_with(LJID, + fun (#user{} = User) -> + User#user{last_presence = FPresence, + role = none} + end, StateData#state.users), StateData#state{users = Users}. %% Find and return a list of the full JIDs of the users of Nick. %% Return jid record. +-spec find_jids_by_nick(binary(), state()) -> [jid()]. find_jids_by_nick(Nick, StateData) -> - case (?DICT):find(Nick, StateData#state.nicks) of - {ok, [User]} -> [jlib:make_jid(User)]; - {ok, Users} -> [jlib:make_jid(LJID) || LJID <- Users]; - error -> false - end. + Users = case maps:get(Nick, StateData#state.nicks, []) of + [] -> muc_subscribers_get_by_nick( + Nick, StateData#state.muc_subscribers); + Us -> Us + end, + [jid:make(LJID) || LJID <- Users]. %% Find and return the full JID of the user of Nick with %% highest-priority presence. Return jid record. +-spec find_jid_by_nick(binary(), state()) -> jid() | false. find_jid_by_nick(Nick, StateData) -> - case (?DICT):find(Nick, StateData#state.nicks) of - {ok, [User]} -> jlib:make_jid(User); - {ok, [FirstUser | Users]} -> - #user{last_presence = FirstPresence} = - (?DICT):fetch(FirstUser, StateData#state.users), - {LJID, _} = lists:foldl(fun (Compare, - {HighestUser, HighestPresence}) -> - #user{last_presence = P1} = - (?DICT):fetch(Compare, - StateData#state.users), - case higher_presence(P1, - HighestPresence) - of - true -> {Compare, P1}; - false -> - {HighestUser, HighestPresence} - end - end, - {FirstUser, FirstPresence}, Users), - jlib:make_jid(LJID); - error -> false + try maps:get(Nick, StateData#state.nicks) of + [User] -> jid:make(User); + [FirstUser | Users] -> + #user{last_presence = FirstPresence} = + maps:get(FirstUser, StateData#state.users), + {LJID, _} = lists:foldl( + fun(Compare, {HighestUser, HighestPresence}) -> + #user{last_presence = P1} = + maps:get(Compare, StateData#state.users), + case higher_presence(P1, HighestPresence) of + true -> {Compare, P1}; + false -> {HighestUser, HighestPresence} + end + end, {FirstUser, FirstPresence}, Users), + jid:make(LJID) + catch _:{badkey, _} -> + false end. -higher_presence(Pres1, Pres2) -> +-spec higher_presence(undefined | presence(), + undefined | presence()) -> boolean(). +higher_presence(Pres1, Pres2) when Pres1 /= undefined, Pres2 /= undefined -> Pri1 = get_priority_from_presence(Pres1), Pri2 = get_priority_from_presence(Pres2), - Pri1 > Pri2. + Pri1 > Pri2; +higher_presence(Pres1, Pres2) -> + Pres1 > Pres2. -get_priority_from_presence(PresencePacket) -> - case xml:get_subtag(PresencePacket, <<"priority">>) of - false -> 0; - SubEl -> - case catch - jlib:binary_to_integer(xml:get_tag_cdata(SubEl)) - of - P when is_integer(P) -> P; - _ -> 0 - end +-spec get_priority_from_presence(presence()) -> integer(). +get_priority_from_presence(#presence{priority = Prio}) -> + case Prio of + undefined -> 0; + _ -> Prio end. -find_nick_by_jid(Jid, StateData) -> - [{_, #user{nick = Nick}}] = lists:filter(fun ({_, - #user{jid = FJid}}) -> - FJid == Jid - end, - (?DICT):to_list(StateData#state.users)), - Nick. +-spec find_nick_by_jid(jid() | undefined, state()) -> binary(). +find_nick_by_jid(undefined, _StateData) -> + <<>>; +find_nick_by_jid(JID, StateData) -> + LJID = jid:tolower(JID), + case maps:find(LJID, StateData#state.users) of + {ok, #user{nick = Nick}} -> + Nick; + _ -> + case maps:find(LJID, (StateData#state.muc_subscribers)#muc_subscribers.subscribers) of + {ok, #subscriber{nick = Nick}} -> + Nick; + _ -> + <<>> + end + end. +-spec is_nick_change(jid(), binary(), state()) -> boolean(). is_nick_change(JID, Nick, StateData) -> - LJID = jlib:jid_tolower(JID), + LJID = jid:tolower(JID), case Nick of <<"">> -> false; _ -> - {ok, #user{nick = OldNick}} = (?DICT):find(LJID, - StateData#state.users), + #user{nick = OldNick} = maps:get(LJID, StateData#state.users), Nick /= OldNick end. +-spec nick_collision(jid(), binary(), state()) -> boolean(). nick_collision(User, Nick, StateData) -> - UserOfNick = find_jid_by_nick(Nick, StateData), - UserOfNick /= false andalso - jlib:jid_remove_resource(jlib:jid_tolower(UserOfNick)) - /= jlib:jid_remove_resource(jlib:jid_tolower(User)). + UserOfNick = case find_jid_by_nick(Nick, StateData) of + false -> + case muc_subscribers_get_by_nick(Nick, StateData#state.muc_subscribers) of + [J] -> J; + [] -> false + end; + J -> J + end, + (UserOfNick /= false andalso + jid:remove_resource(jid:tolower(UserOfNick)) + /= jid:remove_resource(jid:tolower(User))). -add_new_user(From, Nick, - #xmlel{attrs = Attrs, children = Els} = Packet, - StateData) -> - Lang = xml:get_attr_s(<<"xml:lang">>, Attrs), +-spec add_new_user(jid(), binary(), presence(), state()) -> state(); + (jid(), binary(), iq(), state()) -> {error, stanza_error()} | + {ignore, state()} | + {result, muc_subscribe(), state()}. +add_new_user(From, Nick, Packet, StateData) -> + Lang = xmpp:get_lang(Packet), MaxUsers = get_max_users(StateData), MaxAdminUsers = MaxUsers + get_max_users_admin_threshold(StateData), - NUsers = dict:fold(fun (_, _, Acc) -> Acc + 1 end, 0, - StateData#state.users), + NUsers = maps:size(StateData#state.users), Affiliation = get_affiliation(From, StateData), ServiceAffiliation = get_service_affiliation(From, StateData), - NConferences = tab_count_user(From), + NConferences = tab_count_user(From, StateData), MaxConferences = - gen_mod:get_module_opt(StateData#state.server_host, - mod_muc, max_user_conferences, - fun(I) when is_integer(I), I>0 -> I end, - 10), + mod_muc_opt:max_user_conferences(StateData#state.server_host), Collision = nick_collision(From, Nick, StateData), - case {(ServiceAffiliation == owner orelse - (Affiliation == admin orelse Affiliation == owner) - andalso NUsers < MaxAdminUsers + IsSubscribeRequest = not is_record(Packet, presence), + case {ServiceAffiliation == owner orelse + ((((Affiliation == admin orelse Affiliation == owner) + andalso NUsers < MaxAdminUsers) orelse NUsers < MaxUsers) - andalso NConferences < MaxConferences, + andalso NConferences < MaxConferences), Collision, mod_muc:can_use_nick(StateData#state.server_host, - StateData#state.host, From, Nick), - get_default_role(Affiliation, StateData)} + jid:encode(StateData#state.jid), From, Nick), + get_occupant_initial_role(From, Affiliation, StateData)} of + {false, _, _, _} when NUsers >= MaxUsers orelse NUsers >= MaxAdminUsers -> + Txt = ?T("Too many users in this conference"), + Err = xmpp:err_resource_constraint(Txt, Lang), + if not IsSubscribeRequest -> + ejabberd_router:route_error(Packet, Err), + StateData; + true -> + {error, Err} + end; + {false, _, _, _} when NConferences >= MaxConferences -> + Txt = ?T("You have joined too many conferences"), + Err = xmpp:err_resource_constraint(Txt, Lang), + if not IsSubscribeRequest -> + ejabberd_router:route_error(Packet, Err), + StateData; + true -> + {error, Err} + end; {false, _, _, _} -> - Err = jlib:make_error_reply(Packet, - ?ERR_SERVICE_UNAVAILABLE), - ejabberd_router:route % TODO: s/Nick/""/ - (jlib:jid_replace_resource(StateData#state.jid, Nick), - From, Err), - StateData; + Err = xmpp:err_service_unavailable(), + if not IsSubscribeRequest -> + ejabberd_router:route_error(Packet, Err), + StateData; + true -> + {error, Err} + end; {_, _, _, none} -> - Err = jlib:make_error_reply(Packet, - case Affiliation of - outcast -> - ErrText = - <<"You have been banned from this room">>, - ?ERRT_FORBIDDEN(Lang, ErrText); - _ -> - ErrText = - <<"Membership is required to enter this room">>, - ?ERRT_REGISTRATION_REQUIRED(Lang, - ErrText) - end), - ejabberd_router:route % TODO: s/Nick/""/ - (jlib:jid_replace_resource(StateData#state.jid, Nick), - From, Err), - StateData; + Err = case Affiliation of + outcast -> + ErrText = ?T("You have been banned from this room"), + xmpp:err_forbidden(ErrText, Lang); + _ -> + ErrText = ?T("Membership is required to enter this room"), + xmpp:err_registration_required(ErrText, Lang) + end, + if not IsSubscribeRequest -> + ejabberd_router:route_error(Packet, Err), + StateData; + true -> + {error, Err} + end; {_, true, _, _} -> - ErrText = <<"That nickname is already in use by another occupant">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_CONFLICT(Lang, ErrText)), - ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, - Nick), - From, Err), - StateData; + ErrText = ?T("That nickname is already in use by another occupant"), + Err = xmpp:err_conflict(ErrText, Lang), + if not IsSubscribeRequest -> + ejabberd_router:route_error(Packet, Err), + StateData; + true -> + {error, Err} + end; {_, _, false, _} -> - ErrText = <<"That nickname is registered by another person">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_CONFLICT(Lang, ErrText)), - ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, - Nick), - From, Err), - StateData; + Err = case Nick of + <<>> -> + xmpp:err_jid_malformed(?T("Nickname can't be empty"), + Lang); + _ -> + xmpp:err_conflict(?T("That nickname is registered" + " by another person"), Lang) + end, + if not IsSubscribeRequest -> + ejabberd_router:route_error(Packet, Err), + StateData; + true -> + {error, Err} + end; {_, _, _, Role} -> case check_password(ServiceAffiliation, Affiliation, - Els, From, StateData) + Packet, From, StateData) of true -> - NewState = add_user_presence(From, Packet, - add_online_user(From, Nick, Role, - StateData)), - if not (NewState#state.config)#config.anonymous -> - WPacket = #xmlel{name = <<"message">>, - attrs = [{<<"type">>, <<"groupchat">>}], - children = - [#xmlel{name = <<"body">>, - attrs = [], - children = - [{xmlcdata, - translate:translate(Lang, - <<"This room is not anonymous">>)}]}, - #xmlel{name = <<"x">>, - attrs = - [{<<"xmlns">>, - ?NS_MUC_USER}], - children = - [#xmlel{name = - <<"status">>, - attrs = - [{<<"code">>, - <<"100">>}], - children = - []}]}]}, - ejabberd_router:route(StateData#state.jid, From, WPacket); - true -> ok - end, - send_existing_presences(From, NewState), - send_new_presence(From, NewState), - Shift = count_stanza_shift(Nick, Els, NewState), - case send_history(From, Shift, NewState) of - true -> ok; - _ -> send_subject(From, Lang, StateData) - end, - case NewState#state.just_created of - true -> NewState#state{just_created = false}; - false -> - Robots = (?DICT):erase(From, StateData#state.robots), - NewState#state{robots = Robots} + Nodes = get_subscription_nodes(Packet), + NewStateData = + if not IsSubscribeRequest -> + NewState = add_user_presence( + From, Packet, + add_online_user(From, Nick, Role, + StateData)), + send_initial_presences_and_messages( + From, Nick, Packet, NewState, StateData), + NewState; + true -> + set_subscriber(From, Nick, Nodes, StateData) + end, + ResultState = + case NewStateData#state.just_created of + true -> + NewStateData#state{just_created = erlang:system_time(microsecond)}; + _ -> + Robots = maps:remove(From, StateData#state.robots), + NewStateData#state{robots = Robots} + end, + if not IsSubscribeRequest -> ResultState; + true -> {result, subscribe_result(Packet), ResultState} + end; + need_password -> + ErrText = ?T("A password is required to enter this room"), + Err = xmpp:err_not_authorized(ErrText, Lang), + if not IsSubscribeRequest -> + ejabberd_router:route_error(Packet, Err), + StateData; + true -> + {error, Err} end; - nopass -> - ErrText = <<"A password is required to enter this room">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_NOT_AUTHORIZED(Lang, - ErrText)), - ejabberd_router:route % TODO: s/Nick/""/ - (jlib:jid_replace_resource(StateData#state.jid, - Nick), - From, Err), - StateData; captcha_required -> - SID = xml:get_attr_s(<<"id">>, Attrs), + SID = xmpp:get_id(Packet), RoomJID = StateData#state.jid, - To = jlib:jid_replace_resource(RoomJID, Nick), + To = jid:replace_resource(RoomJID, Nick), Limiter = {From#jid.luser, From#jid.lserver}, case ejabberd_captcha:create_captcha(SID, RoomJID, To, Lang, Limiter, From) - of - {ok, ID, CaptchaEls} -> - MsgPkt = #xmlel{name = <<"message">>, - attrs = [{<<"id">>, ID}], - children = CaptchaEls}, - Robots = (?DICT):store(From, {Nick, Packet}, - StateData#state.robots), - ejabberd_router:route(RoomJID, From, MsgPkt), - StateData#state{robots = Robots}; + of + {ok, ID, Body, CaptchaEls} -> + MsgPkt = #message{from = RoomJID, + to = From, + id = ID, body = Body, + sub_els = CaptchaEls}, + Robots = maps:put(From, {Nick, Packet}, + StateData#state.robots), + ejabberd_router:route(MsgPkt), + NewState = StateData#state{robots = Robots}, + if not IsSubscribeRequest -> + NewState; + true -> + {ignore, NewState} + end; {error, limit} -> - ErrText = <<"Too many CAPTCHA requests">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_RESOURCE_CONSTRAINT(Lang, - ErrText)), - ejabberd_router:route % TODO: s/Nick/""/ - (jlib:jid_replace_resource(StateData#state.jid, - Nick), - From, Err), - StateData; + ErrText = ?T("Too many CAPTCHA requests"), + Err = xmpp:err_resource_constraint(ErrText, Lang), + if not IsSubscribeRequest -> + ejabberd_router:route_error(Packet, Err), + StateData; + true -> + {error, Err} + end; _ -> - ErrText = <<"Unable to generate a CAPTCHA">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_INTERNAL_SERVER_ERROR(Lang, - ErrText)), - ejabberd_router:route % TODO: s/Nick/""/ - (jlib:jid_replace_resource(StateData#state.jid, - Nick), - From, Err), - StateData + ErrText = ?T("Unable to generate a CAPTCHA"), + Err = xmpp:err_internal_server_error(ErrText, Lang), + if not IsSubscribeRequest -> + ejabberd_router:route_error(Packet, Err), + StateData; + true -> + {error, Err} + end end; _ -> - ErrText = <<"Incorrect password">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_NOT_AUTHORIZED(Lang, - ErrText)), - ejabberd_router:route % TODO: s/Nick/""/ - (jlib:jid_replace_resource(StateData#state.jid, - Nick), - From, Err), - StateData + ErrText = ?T("Incorrect password"), + Err = xmpp:err_not_authorized(ErrText, Lang), + if not IsSubscribeRequest -> + ejabberd_router:route_error(Packet, Err), + StateData; + true -> + {error, Err} + end end end. -check_password(owner, _Affiliation, _Els, _From, +-spec check_password(affiliation(), affiliation(), + presence() | iq(), jid(), state()) -> + boolean() | need_password | captcha_required. +check_password(owner, _Affiliation, _Packet, _From, _StateData) -> %% Don't check pass if user is owner in MUC service (access_admin option) true; -check_password(_ServiceAffiliation, Affiliation, Els, +check_password(_ServiceAffiliation, owner, _Packet, _From, + _StateData) -> + %% Don't check pass if user is owner in this room + true; +check_password(_ServiceAffiliation, Affiliation, Packet, From, StateData) -> case (StateData#state.config)#config.password_protected of false -> check_captcha(Affiliation, From, StateData); true -> - Pass = extract_password(Els), + Pass = extract_password(Packet), case Pass of - false -> nopass; + false -> need_password; _ -> case (StateData#state.config)#config.password of Pass -> true; @@ -1972,14 +2494,15 @@ check_password(_ServiceAffiliation, Affiliation, Els, end end. +-spec check_captcha(affiliation(), jid(), state()) -> true | captcha_required. check_captcha(Affiliation, From, StateData) -> case (StateData#state.config)#config.captcha_protected andalso ejabberd_captcha:is_feature_available() of true when Affiliation == none -> - case (?DICT):find(From, StateData#state.robots) of - {ok, passed} -> true; - _ -> + case maps:get(From, StateData#state.robots, error) of + passed -> true; + _ -> WList = (StateData#state.config)#config.captcha_whitelist, #jid{luser = U, lserver = S, lresource = R} = From, @@ -2000,890 +2523,888 @@ check_captcha(Affiliation, From, StateData) -> _ -> true end. -extract_password([]) -> false; -extract_password([#xmlel{attrs = Attrs} = El | Els]) -> - case xml:get_attr_s(<<"xmlns">>, Attrs) of - ?NS_MUC -> - case xml:get_subtag(El, <<"password">>) of - false -> false; - SubEl -> xml:get_tag_cdata(SubEl) - end; - _ -> extract_password(Els) +-spec extract_password(presence() | iq()) -> binary() | false. +extract_password(#presence{} = Pres) -> + case xmpp:get_subtag(Pres, #muc{}) of + #muc{password = Password} when is_binary(Password) -> + Password; + _ -> + false end; -extract_password([_ | Els]) -> extract_password(Els). - -count_stanza_shift(Nick, Els, StateData) -> - HL = lqueue_to_list(StateData#state.history), - Since = extract_history(Els, <<"since">>), - Shift0 = case Since of - false -> 0; - _ -> - Sin = calendar:datetime_to_gregorian_seconds(Since), - count_seconds_shift(Sin, HL) - end, - Seconds = extract_history(Els, <<"seconds">>), - Shift1 = case Seconds of - false -> 0; - _ -> - Sec = - calendar:datetime_to_gregorian_seconds(calendar:now_to_universal_time(now())) - - Seconds, - count_seconds_shift(Sec, HL) - end, - MaxStanzas = extract_history(Els, <<"maxstanzas">>), - Shift2 = case MaxStanzas of - false -> 0; - _ -> count_maxstanzas_shift(MaxStanzas, HL) - end, - MaxChars = extract_history(Els, <<"maxchars">>), - Shift3 = case MaxChars of - false -> 0; - _ -> count_maxchars_shift(Nick, MaxChars, HL) - end, - lists:max([Shift0, Shift1, Shift2, Shift3]). - -count_seconds_shift(Seconds, HistoryList) -> - lists:sum(lists:map(fun ({_Nick, _Packet, _HaveSubject, - TimeStamp, _Size}) -> - T = - calendar:datetime_to_gregorian_seconds(TimeStamp), - if T < Seconds -> 1; - true -> 0 - end - end, - HistoryList)). - -count_maxstanzas_shift(MaxStanzas, HistoryList) -> - S = length(HistoryList) - MaxStanzas, - if S =< 0 -> 0; - true -> S +extract_password(#iq{} = IQ) -> + case xmpp:get_subtag(IQ, #muc_subscribe{}) of + #muc_subscribe{password = Password} when Password /= <<"">> -> + Password; + _ -> + false end. -count_maxchars_shift(Nick, MaxSize, HistoryList) -> - NLen = byte_size(Nick) + 1, - Sizes = lists:map(fun ({_Nick, _Packet, _HaveSubject, - _TimeStamp, Size}) -> - Size + NLen - end, - HistoryList), - calc_shift(MaxSize, Sizes). - -calc_shift(MaxSize, Sizes) -> - Total = lists:sum(Sizes), - calc_shift(MaxSize, Total, 0, Sizes). - -calc_shift(_MaxSize, _Size, Shift, []) -> Shift; -calc_shift(MaxSize, Size, Shift, [S | TSizes]) -> - if MaxSize >= Size -> Shift; - true -> calc_shift(MaxSize, Size - S, Shift + 1, TSizes) +-spec get_history(binary(), stanza(), state()) -> [lqueue_elem()]. +get_history(Nick, Packet, #state{history = History}) -> + case xmpp:get_subtag(Packet, #muc{}) of + #muc{history = #muc_history{} = MUCHistory} -> + Now = erlang:timestamp(), + Q = History#lqueue.queue, + filter_history(Q, Now, Nick, MUCHistory); + _ -> + p1_queue:to_list(History#lqueue.queue) end. -extract_history([], _Type) -> false; -extract_history([#xmlel{attrs = Attrs} = El | Els], - Type) -> - case xml:get_attr_s(<<"xmlns">>, Attrs) of - ?NS_MUC -> - AttrVal = xml:get_path_s(El, - [{elem, <<"history">>}, {attr, Type}]), - case Type of - <<"since">> -> - case jlib:datetime_string_to_timestamp(AttrVal) of - undefined -> false; - TS -> calendar:now_to_universal_time(TS) - end; - _ -> - case catch jlib:binary_to_integer(AttrVal) of - IntVal when is_integer(IntVal) and (IntVal >= 0) -> - IntVal; - _ -> false - end - end; - _ -> extract_history(Els, Type) - end; -extract_history([_ | Els], Type) -> - extract_history(Els, Type). +-spec filter_history(p1_queue:queue(lqueue_elem()), erlang:timestamp(), + binary(), muc_history()) -> [lqueue_elem()]. +filter_history(Queue, Now, Nick, + #muc_history{since = Since, + seconds = Seconds, + maxstanzas = MaxStanzas, + maxchars = MaxChars}) -> + {History, _, _} = + lists:foldr( + fun({_, _, _, TimeStamp, Size} = Elem, + {Elems, NumStanzas, NumChars} = Acc) -> + NowDiff = timer:now_diff(Now, TimeStamp) div 1000000, + Chars = Size + byte_size(Nick) + 1, + if (NumStanzas < MaxStanzas) andalso + (TimeStamp > Since) andalso + (NowDiff =< Seconds) andalso + (NumChars + Chars =< MaxChars) -> + {[Elem|Elems], NumStanzas + 1, NumChars + Chars}; + true -> + Acc + end + end, {[], 0, 0}, p1_queue:to_list(Queue)), + History. -send_update_presence(JID, StateData) -> - send_update_presence(JID, <<"">>, StateData). +-spec is_room_overcrowded(state()) -> boolean(). +is_room_overcrowded(StateData) -> + MaxUsersPresence = mod_muc_opt:max_users_presence(StateData#state.server_host), + maps:size(StateData#state.users) > MaxUsersPresence. -send_update_presence(JID, Reason, StateData) -> - LJID = jlib:jid_tolower(JID), +-spec presence_broadcast_allowed(jid(), state()) -> boolean(). +presence_broadcast_allowed(JID, StateData) -> + Role = get_role(JID, StateData), + lists:member(Role, (StateData#state.config)#config.presence_broadcast). + +-spec send_initial_presences_and_messages( + jid(), binary(), presence(), state(), state()) -> ok. +send_initial_presences_and_messages(From, Nick, Presence, NewState, OldState) -> + advertise_entity_capabilities(From, NewState), + send_existing_presences(From, NewState), + send_self_presence(From, NewState, OldState), + History = get_history(Nick, Presence, NewState), + send_history(From, History, NewState), + send_subject(From, OldState). + +-spec advertise_entity_capabilities(jid(), state()) -> ok. +advertise_entity_capabilities(JID, State) -> + AvatarHash = (State#state.config)#config.vcard_xupdate, + DiscoInfo = make_disco_info(JID, State), + Extras = iq_disco_info_extras(<<"en">>, State, true), + DiscoInfo1 = DiscoInfo#disco_info{xdata = [Extras]}, + DiscoHash = mod_caps:compute_disco_hash(DiscoInfo1, sha), + Els1 = [#caps{hash = <<"sha-1">>, + node = ejabberd_config:get_uri(), + version = DiscoHash}], + Els2 = if is_binary(AvatarHash) -> + [#vcard_xupdate{hash = AvatarHash}|Els1]; + true -> + Els1 + end, + ejabberd_router:route(#presence{from = State#state.jid, to = JID, + id = p1_rand:get_string(), + sub_els = Els2}). + +-spec send_self_presence(jid(), state(), state()) -> ok. +send_self_presence(NJID, StateData, OldStateData) -> + send_new_presence(NJID, <<"">>, true, StateData, OldStateData). + +-spec send_update_presence(jid(), state(), state()) -> ok. +send_update_presence(JID, StateData, OldStateData) -> + send_update_presence(JID, <<"">>, StateData, OldStateData). + +-spec send_update_presence(jid(), binary(), state(), state()) -> ok. +send_update_presence(JID, Reason, StateData, OldStateData) -> + case is_room_overcrowded(StateData) of + true -> ok; + false -> send_update_presence1(JID, Reason, StateData, OldStateData) + end. + +-spec send_update_presence1(jid(), binary(), state(), state()) -> ok. +send_update_presence1(JID, Reason, StateData, OldStateData) -> + LJID = jid:tolower(JID), LJIDs = case LJID of {U, S, <<"">>} -> - (?DICT):fold(fun (J, _, Js) -> - case J of - {U, S, _} -> [J | Js]; - _ -> Js - end - end, - [], StateData#state.users); + maps:fold(fun (J, _, Js) -> + case J of + {U, S, _} -> [J | Js]; + _ -> Js + end + end, [], StateData#state.users); _ -> - case (?DICT):is_key(LJID, StateData#state.users) of + case maps:is_key(LJID, StateData#state.users) of true -> [LJID]; _ -> [] end end, lists:foreach(fun (J) -> - send_new_presence(J, Reason, StateData) + send_new_presence(J, Reason, false, StateData, + OldStateData) end, LJIDs). -send_new_presence(NJID, StateData) -> - send_new_presence(NJID, <<"">>, StateData). +-spec send_new_presence(jid(), state(), state()) -> ok. +send_new_presence(NJID, StateData, OldStateData) -> + send_new_presence(NJID, <<"">>, false, StateData, OldStateData). -send_new_presence(NJID, Reason, StateData) -> - #user{nick = Nick} = - (?DICT):fetch(jlib:jid_tolower(NJID), - StateData#state.users), +-spec send_new_presence(jid(), binary(), state(), state()) -> ok. +send_new_presence(NJID, Reason, StateData, OldStateData) -> + send_new_presence(NJID, Reason, false, StateData, OldStateData). + +-spec is_ra_changed(jid(), boolean(), state(), state()) -> boolean(). +is_ra_changed(_, _IsInitialPresence = true, _, _) -> + false; +is_ra_changed(JID, _IsInitialPresence = false, NewStateData, OldStateData) -> + NewRole = get_role(JID, NewStateData), + NewAff = get_affiliation(JID, NewStateData), + OldRole = get_role(JID, OldStateData), + OldAff = get_affiliation(JID, OldStateData), + if (NewRole == none) and (NewAff == OldAff) -> + %% A user is leaving the room; + false; + true -> + (NewRole /= OldRole) or (NewAff /= OldAff) + end. + +-spec send_new_presence(jid(), binary(), boolean(), state(), state()) -> ok. +send_new_presence(NJID, Reason, IsInitialPresence, StateData, OldStateData) -> + LNJID = jid:tolower(NJID), + #user{nick = Nick} = maps:get(LNJID, StateData#state.users), LJID = find_jid_by_nick(Nick, StateData), - {ok, - #user{jid = RealJID, role = Role, - last_presence = Presence}} = - (?DICT):find(jlib:jid_tolower(LJID), - StateData#state.users), + #user{jid = RealJID, role = Role0, + last_presence = Presence0} = UserInfo = + maps:get(jid:tolower(LJID), StateData#state.users), + {Role1, Presence1} = + case (presence_broadcast_allowed(NJID, StateData) orelse + presence_broadcast_allowed(NJID, OldStateData)) of + true -> {Role0, Presence0}; + false -> {none, #presence{type = unavailable}} + end, Affiliation = get_affiliation(LJID, StateData), - SAffiliation = affiliation_to_list(Affiliation), - SRole = role_to_list(Role), - lists:foreach(fun ({_LJID, Info}) -> - ItemAttrs = case Info#user.role == moderator orelse - (StateData#state.config)#config.anonymous - == false - of - true -> - [{<<"jid">>, - jlib:jid_to_string(RealJID)}, - {<<"affiliation">>, SAffiliation}, - {<<"role">>, SRole}]; - _ -> - [{<<"affiliation">>, SAffiliation}, - {<<"role">>, SRole}] - end, - ItemEls = case Reason of - <<"">> -> []; - _ -> - [#xmlel{name = <<"reason">>, - attrs = [], - children = - [{xmlcdata, Reason}]}] - end, - Status = case StateData#state.just_created of - true -> - [#xmlel{name = <<"status">>, - attrs = - [{<<"code">>, <<"201">>}], - children = []}]; - false -> [] - end, - Status2 = case - (StateData#state.config)#config.anonymous - == false - andalso NJID == Info#user.jid - of - true -> - [#xmlel{name = <<"status">>, - attrs = - [{<<"code">>, <<"100">>}], - children = []} - | Status]; - false -> Status - end, - Status3 = case NJID == Info#user.jid of - true -> - [#xmlel{name = <<"status">>, - attrs = - [{<<"code">>, <<"110">>}], - children = []} - | Status2]; - false -> Status2 - end, - Packet = xml:append_subtags(Presence, - [#xmlel{name = <<"x">>, - attrs = - [{<<"xmlns">>, - ?NS_MUC_USER}], - children = - [#xmlel{name = - <<"item">>, - attrs - = - ItemAttrs, - children - = - ItemEls} - | Status3]}]), - ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, - Nick), - Info#user.jid, Packet) - end, - (?DICT):to_list(StateData#state.users)). + Node1 = case is_ra_changed(NJID, IsInitialPresence, StateData, OldStateData) of + true -> ?NS_MUCSUB_NODES_AFFILIATIONS; + false -> ?NS_MUCSUB_NODES_PRESENCE + end, + Node2 = ?NS_MUCSUB_NODES_PARTICIPANTS, + UserMap = + case is_room_overcrowded(StateData) orelse + (not (presence_broadcast_allowed(NJID, StateData) orelse + presence_broadcast_allowed(NJID, OldStateData))) of + true -> + #{LNJID => UserInfo}; + false -> + %% TODO: optimize further + UM1 = get_users_and_subscribers_with_node(Node1, StateData), + UM2 = get_users_and_subscribers_with_node(Node2, StateData), + maps:merge(UM1, UM2) + end, + maps:fold( + fun(LUJID, Info, _) -> + IsSelfPresence = LNJID == LUJID, + {Role, Presence} = if IsSelfPresence -> {Role0, Presence0}; + true -> {Role1, Presence1} + end, + Item0 = #muc_item{affiliation = Affiliation, + role = Role}, + Item1 = case Info#user.role == moderator orelse + (StateData#state.config)#config.anonymous + == false orelse IsSelfPresence of + true -> Item0#muc_item{jid = RealJID}; + false -> Item0 + end, + Item = Item1#muc_item{reason = Reason}, + StatusCodes = status_codes(IsInitialPresence, IsSelfPresence, + StateData), + Pres = if Presence == undefined -> #presence{}; + true -> Presence + end, + 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), + IsSubscriber = is_subscriber(Info#user.jid, StateData), + IsOccupant = Info#user.last_presence /= undefined, + if (IsSubscriber and not IsOccupant) and + (IsInitialPresence or (Type == unavailable)) -> + send_wrapped(jid:replace_resource(StateData#state.jid, Nick), + Info#user.jid, Packet, Node2, StateData); + true -> + ok + end + end, ok, UserMap). +-spec send_existing_presences(jid(), state()) -> ok. send_existing_presences(ToJID, StateData) -> - LToJID = jlib:jid_tolower(ToJID), - {ok, #user{jid = RealToJID, role = Role}} = - (?DICT):find(LToJID, StateData#state.users), - lists:foreach(fun ({FromNick, _Users}) -> - LJID = find_jid_by_nick(FromNick, StateData), - #user{jid = FromJID, role = FromRole, - last_presence = Presence} = - (?DICT):fetch(jlib:jid_tolower(LJID), - StateData#state.users), - case RealToJID of - FromJID -> ok; - _ -> - FromAffiliation = get_affiliation(LJID, - StateData), - ItemAttrs = case Role == moderator orelse - (StateData#state.config)#config.anonymous - == false - of - true -> - [{<<"jid">>, - jlib:jid_to_string(FromJID)}, - {<<"affiliation">>, - affiliation_to_list(FromAffiliation)}, - {<<"role">>, - role_to_list(FromRole)}]; - _ -> - [{<<"affiliation">>, - affiliation_to_list(FromAffiliation)}, - {<<"role">>, - role_to_list(FromRole)}] - end, - Packet = xml:append_subtags(Presence, - [#xmlel{name = - <<"x">>, - attrs = - [{<<"xmlns">>, - ?NS_MUC_USER}], - children = - [#xmlel{name - = - <<"item">>, - attrs - = - ItemAttrs, - children - = - []}]}]), - ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, - FromNick), - RealToJID, Packet) - end - end, - (?DICT):to_list(StateData#state.nicks)). + case is_room_overcrowded(StateData) of + true -> ok; + false -> send_existing_presences1(ToJID, StateData) + end. -now_to_usec({MSec, Sec, USec}) -> - (MSec * 1000000 + Sec) * 1000000 + USec. +-spec send_existing_presences1(jid(), state()) -> ok. +send_existing_presences1(ToJID, StateData) -> + LToJID = jid:tolower(ToJID), + #user{jid = RealToJID, role = Role} = maps:get(LToJID, StateData#state.users), + maps:fold( + fun(FromNick, _Users, _) -> + LJID = find_jid_by_nick(FromNick, StateData), + #user{jid = FromJID, role = FromRole, + last_presence = Presence} = + maps:get(jid:tolower(LJID), StateData#state.users), + PresenceBroadcast = + lists:member( + FromRole, (StateData#state.config)#config.presence_broadcast), + case {RealToJID, PresenceBroadcast} of + {FromJID, _} -> ok; + {_, false} -> ok; + _ -> + FromAffiliation = get_affiliation(LJID, StateData), + Item0 = #muc_item{affiliation = FromAffiliation, + role = FromRole}, + Item = case Role == moderator orelse + (StateData#state.config)#config.anonymous + == false of + true -> Item0#muc_item{jid = FromJID}; + false -> Item0 + end, + Packet = xmpp:set_subtag( + 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 + end, ok, StateData#state.nicks). -change_nick(JID, Nick, StateData) -> - LJID = jlib:jid_tolower(JID), - {ok, #user{nick = OldNick}} = (?DICT):find(LJID, - StateData#state.users), - Users = (?DICT):update(LJID, - fun (#user{} = User) -> User#user{nick = Nick} end, - StateData#state.users), - OldNickUsers = (?DICT):fetch(OldNick, - StateData#state.nicks), - NewNickUsers = case (?DICT):find(Nick, - StateData#state.nicks) - of - {ok, U} -> U; - error -> [] - end, - SendOldUnavailable = length(OldNickUsers) == 1, - SendNewAvailable = SendOldUnavailable orelse - NewNickUsers == [], +-spec set_nick(jid(), binary(), state()) -> state(). +set_nick(JID, Nick, State) -> + LJID = jid:tolower(JID), + #user{nick = OldNick} = maps:get(LJID, State#state.users), + Users = maps:update_with(LJID, + fun (#user{} = User) -> User#user{nick = Nick} end, + State#state.users), + OldNickUsers = maps:get(OldNick, State#state.nicks), + NewNickUsers = maps:get(Nick, State#state.nicks, []), Nicks = case OldNickUsers of - [LJID] -> - (?DICT):store(Nick, [LJID | NewNickUsers], - (?DICT):erase(OldNick, StateData#state.nicks)); - [_ | _] -> - (?DICT):store(Nick, [LJID | NewNickUsers], - (?DICT):store(OldNick, OldNickUsers -- [LJID], - StateData#state.nicks)) + [LJID] -> + maps:put(Nick, [LJID | NewNickUsers -- [LJID]], + maps:remove(OldNick, State#state.nicks)); + [_ | _] -> + maps:put(Nick, [LJID | NewNickUsers -- [LJID]], + maps:put(OldNick, OldNickUsers -- [LJID], + State#state.nicks)) end, - NewStateData = StateData#state{users = Users, - nicks = Nicks}, - send_nick_changing(JID, OldNick, NewStateData, - SendOldUnavailable, SendNewAvailable), + State#state{users = Users, nicks = Nicks}. + +-spec change_nick(jid(), binary(), state()) -> state(). +change_nick(JID, Nick, StateData) -> + LJID = jid:tolower(JID), + #user{nick = OldNick} = maps:get(LJID, StateData#state.users), + OldNickUsers = maps:get(OldNick, StateData#state.nicks), + NewNickUsers = maps:get(Nick, StateData#state.nicks, []), + SendOldUnavailable = length(OldNickUsers) == 1, + SendNewAvailable = SendOldUnavailable orelse NewNickUsers == [], + NewStateData = set_nick(JID, Nick, StateData), + case presence_broadcast_allowed(JID, NewStateData) of + true -> + send_nick_changing(JID, OldNick, NewStateData, + SendOldUnavailable, SendNewAvailable); + false -> ok + end, add_to_log(nickchange, {OldNick, Nick}, StateData), NewStateData. +-spec send_nick_changing(jid(), binary(), state(), boolean(), boolean()) -> ok. send_nick_changing(JID, OldNick, StateData, SendOldUnavailable, SendNewAvailable) -> - {ok, - #user{jid = RealJID, nick = Nick, role = Role, - last_presence = Presence}} = - (?DICT):find(jlib:jid_tolower(JID), - StateData#state.users), + #user{jid = RealJID, nick = Nick, role = Role, + last_presence = Presence} = + maps:get(jid:tolower(JID), StateData#state.users), Affiliation = get_affiliation(JID, StateData), - SAffiliation = affiliation_to_list(Affiliation), - SRole = role_to_list(Role), - lists:foreach(fun ({_LJID, Info}) -> - ItemAttrs1 = case Info#user.role == moderator orelse - (StateData#state.config)#config.anonymous - == false - of - true -> - [{<<"jid">>, - jlib:jid_to_string(RealJID)}, - {<<"affiliation">>, SAffiliation}, - {<<"role">>, SRole}, - {<<"nick">>, Nick}]; - _ -> - [{<<"affiliation">>, SAffiliation}, - {<<"role">>, SRole}, - {<<"nick">>, Nick}] - end, - ItemAttrs2 = case Info#user.role == moderator orelse - (StateData#state.config)#config.anonymous - == false - of - true -> - [{<<"jid">>, - jlib:jid_to_string(RealJID)}, - {<<"affiliation">>, SAffiliation}, - {<<"role">>, SRole}]; - _ -> - [{<<"affiliation">>, SAffiliation}, - {<<"role">>, SRole}] - end, - Status110 = case JID == Info#user.jid of - true -> - [#xmlel{name = <<"status">>, - attrs = [{<<"code">>, <<"110">>}] - }]; - false -> - [] - end, - Packet1 = #xmlel{name = <<"presence">>, - attrs = - [{<<"type">>, - <<"unavailable">>}], - children = - [#xmlel{name = <<"x">>, - attrs = - [{<<"xmlns">>, - ?NS_MUC_USER}], - children = - [#xmlel{name = - <<"item">>, - attrs = - ItemAttrs1, - children = - []}, - #xmlel{name = - <<"status">>, - attrs = - [{<<"code">>, - <<"303">>}], - children = - []}|Status110]}]}, - Packet2 = xml:append_subtags(Presence, - [#xmlel{name = <<"x">>, - attrs = - [{<<"xmlns">>, - ?NS_MUC_USER}], - children = - [#xmlel{name - = - <<"item">>, - attrs - = - ItemAttrs2, - children - = - []}|Status110]}]), - if SendOldUnavailable -> - ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, - OldNick), - Info#user.jid, Packet1); - true -> ok + maps:fold( + fun(LJID, Info, _) when Presence /= undefined -> + IsSelfPresence = LJID == jid:tolower(JID), + Item0 = #muc_item{affiliation = Affiliation, role = Role}, + Item = case Info#user.role == moderator orelse + (StateData#state.config)#config.anonymous + == false orelse IsSelfPresence of + true -> Item0#muc_item{jid = RealJID}; + false -> Item0 + end, + Status110 = case IsSelfPresence of + true -> [110]; + false -> [] end, - if SendNewAvailable -> - ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, - Nick), - Info#user.jid, Packet2); - true -> ok - end - end, - (?DICT):to_list(StateData#state.users)). + Packet1 = #presence{ + type = unavailable, + sub_els = [#muc_user{ + items = [Item#muc_item{nick = Nick}], + status_codes = [303|Status110]}]}, + Packet2 = xmpp:set_subtag(Presence, + #muc_user{items = [Item], + status_codes = Status110}), + if SendOldUnavailable -> + send_wrapped( + jid:replace_resource(StateData#state.jid, OldNick), + Info#user.jid, Packet1, ?NS_MUCSUB_NODES_PRESENCE, + StateData); + true -> ok + end, + if SendNewAvailable -> + send_wrapped( + jid:replace_resource(StateData#state.jid, Nick), + Info#user.jid, Packet2, ?NS_MUCSUB_NODES_PRESENCE, + StateData); + true -> ok + end; + (_, _, _) -> + ok + end, ok, get_users_and_subscribers_with_node( + ?NS_MUCSUB_NODES_PRESENCE, StateData)). -lqueue_new(Max) -> - #lqueue{queue = queue:new(), len = 0, max = Max}. +-spec maybe_send_affiliation(jid(), affiliation(), state()) -> ok. +maybe_send_affiliation(JID, Affiliation, StateData) -> + LJID = jid:tolower(JID), + %% TODO: there should be a better way to check IsOccupant + Users = get_users_and_subscribers(StateData), + IsOccupant = case LJID of + {LUser, LServer, <<"">>} -> + #{} /= maps:filter( + fun({U, S, _}, _) -> + U == LUser andalso + S == LServer + end, Users); + {_LUser, _LServer, _LResource} -> + maps:is_key(LJID, Users) + end, + case IsOccupant of + true -> + ok; % The new affiliation is published via presence. + false -> + send_affiliation(JID, Affiliation, StateData) + end. +-spec send_affiliation(jid(), affiliation(), state()) -> ok. +send_affiliation(JID, Affiliation, StateData) -> + Item = #muc_item{jid = JID, + affiliation = Affiliation, + role = none}, + Message = #message{id = p1_rand:get_string(), + sub_els = [#muc_user{items = [Item]}]}, + Users = get_users_and_subscribers_with_node( + ?NS_MUCSUB_NODES_AFFILIATIONS, StateData), + Recipients = case (StateData#state.config)#config.anonymous of + true -> + maps:filter(fun(_, #user{role = moderator}) -> + true; + (_, _) -> + false + end, Users); + false -> + Users + end, + send_wrapped_multiple(StateData#state.jid, Recipients, Message, + ?NS_MUCSUB_NODES_AFFILIATIONS, StateData). + +-spec status_codes(boolean(), boolean(), state()) -> [pos_integer()]. +status_codes(IsInitialPresence, _IsSelfPresence = true, StateData) -> + S0 = [110], + case IsInitialPresence of + true -> + S1 = case StateData#state.just_created of + true -> [201|S0]; + _ -> S0 + end, + S2 = case (StateData#state.config)#config.anonymous of + true -> S1; + false -> [100|S1] + end, + S3 = case (StateData#state.config)#config.logging of + true -> [170|S2]; + false -> S2 + end, + S3; + false -> S0 + end; +status_codes(_IsInitialPresence, _IsSelfPresence = false, _StateData) -> []. + +-spec lqueue_new(non_neg_integer(), ram | file) -> lqueue(). +lqueue_new(Max, Type) -> + #lqueue{queue = p1_queue:new(Type), max = Max}. + +-spec lqueue_in(lqueue_elem(), lqueue()) -> lqueue(). %% If the message queue limit is set to 0, do not store messages. lqueue_in(_Item, LQ = #lqueue{max = 0}) -> LQ; %% Otherwise, rotate messages in the queue store. -lqueue_in(Item, - #lqueue{queue = Q1, len = Len, max = Max}) -> - Q2 = queue:in(Item, Q1), +lqueue_in(Item, #lqueue{queue = Q1, max = Max}) -> + Len = p1_queue:len(Q1), + Q2 = p1_queue:in(Item, Q1), if Len >= Max -> Q3 = lqueue_cut(Q2, Len - Max + 1), - #lqueue{queue = Q3, len = Max, max = Max}; - true -> #lqueue{queue = Q2, len = Len + 1, max = Max} + #lqueue{queue = Q3, max = Max}; + true -> #lqueue{queue = Q2, max = Max} end. +-spec lqueue_cut(p1_queue:queue(lqueue_elem()), non_neg_integer()) -> p1_queue:queue(lqueue_elem()). lqueue_cut(Q, 0) -> Q; lqueue_cut(Q, N) -> - {_, Q1} = queue:out(Q), lqueue_cut(Q1, N - 1). - -lqueue_to_list(#lqueue{queue = Q1}) -> - queue:to_list(Q1). - + {_, Q1} = p1_queue:out(Q), + lqueue_cut(Q1, N - 1). +-spec add_message_to_history(binary(), jid(), message(), state()) -> state(). add_message_to_history(FromNick, FromJID, Packet, StateData) -> - HaveSubject = case xml:get_subtag(Packet, <<"subject">>) - of - false -> false; - _ -> true - end, - TimeStamp = now(), - SenderJid = case - (StateData#state.config)#config.anonymous - of - true -> StateData#state.jid; - false -> FromJID - end, - TSPacket = jlib:add_delay_info(Packet, SenderJid, TimeStamp), - SPacket = - jlib:replace_from_to(jlib:jid_replace_resource(StateData#state.jid, - FromNick), - StateData#state.jid, TSPacket), - Size = element_size(SPacket), - Q1 = lqueue_in({FromNick, TSPacket, HaveSubject, - calendar:now_to_universal_time(TimeStamp), Size}, - StateData#state.history), add_to_log(text, {FromNick, Packet}, StateData), - StateData#state{history = Q1}. - -send_history(JID, Shift, StateData) -> - lists:foldl(fun ({Nick, Packet, HaveSubject, _TimeStamp, - _Size}, - B) -> - ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, - Nick), - JID, Packet), - B or HaveSubject - end, - false, - lists:nthtail(Shift, - lqueue_to_list(StateData#state.history))). - -send_subject(JID, Lang, StateData) -> - case StateData#state.subject_author of - <<"">> -> ok; - Nick -> - Subject = StateData#state.subject, - Packet = #xmlel{name = <<"message">>, - attrs = [{<<"type">>, <<"groupchat">>}], - children = - [#xmlel{name = <<"subject">>, attrs = [], - children = [{xmlcdata, Subject}]}, - #xmlel{name = <<"body">>, attrs = [], - children = - [{xmlcdata, - <>))/binary, - Subject/binary>>}]}]}, - ejabberd_router:route(StateData#state.jid, JID, Packet) + case check_subject(Packet) of + [] -> + TimeStamp = erlang:timestamp(), + AddrPacket = case (StateData#state.config)#config.anonymous of + true -> Packet; + false -> + Addresses = #addresses{ + list = [#address{type = ofrom, + jid = FromJID}]}, + xmpp:set_subtag(Packet, Addresses) + end, + TSPacket = misc:add_delay_info( + AddrPacket, StateData#state.jid, TimeStamp), + SPacket = xmpp:set_from_to( + TSPacket, + jid:replace_resource(StateData#state.jid, FromNick), + StateData#state.jid), + Size = element_size(SPacket), + Q1 = lqueue_in({FromNick, TSPacket, false, + TimeStamp, Size}, + StateData#state.history), + StateData#state{history = Q1, just_created = erlang:system_time(microsecond)}; + _ -> + StateData#state{just_created = erlang:system_time(microsecond)} end. -check_subject(Packet) -> - case xml:get_subtag(Packet, <<"subject">>) of - false -> false; - SubjEl -> xml:get_tag_cdata(SubjEl) +remove_from_history(StanzaId, #state{history = #lqueue{queue = Queue} = LQueue} = StateData) -> + NewQ = p1_queue:foldl( + fun({_, Pkt, _, _, _} = Entry, Acc) -> + case xmpp:get_meta(Pkt, stanza_id, missing) of + V when V == StanzaId -> + Acc; + _ -> + p1_queue:in(Entry, Acc) + end + end, p1_queue:new(), Queue), + StateData#state{history = LQueue#lqueue{queue = NewQ}}. + +remove_from_history({U1, S1}, OriginId, #state{history = #lqueue{queue = Queue} = LQueue} = StateData) -> + {NewQ, StanzaId} = p1_queue:foldl( + fun({_, Pkt, _, _, _} = Entry, {Q, none}) -> + case jid:tolower(xmpp:get_from(Pkt)) of + {U2, S2, _} when U1 == U2, S1 == S2 -> + case xmpp:get_subtag(Pkt, #origin_id{}) of + #origin_id{id = V} when V == OriginId -> + {Q, xmpp:get_meta(Pkt, stanza_id, missing)}; + _ -> + {p1_queue:in(Entry, Q), none} + end; + _ -> + {p1_queue:in(Entry, Q), none} + end; + (Entry, {Q, S}) -> + {p1_queue:in(Entry, Q), S} + end, {p1_queue:new(), none}, Queue), + {StateData#state{history = LQueue#lqueue{queue = NewQ}}, StanzaId}. + +-spec send_history(jid(), [lqueue_elem()], state()) -> ok. +send_history(JID, History, StateData) -> + lists:foreach( + fun({Nick, Packet, _HaveSubject, _TimeStamp, _Size}) -> + ejabberd_router:route( + xmpp:set_from_to( + Packet, + jid:replace_resource(StateData#state.jid, Nick), + JID)) + end, History). + +-spec send_subject(jid(), state()) -> ok. +send_subject(JID, #state{subject_author = {Nick, AuthorJID}} = StateData) -> + Subject = case StateData#state.subject of + [] -> [#text{}]; + [_|_] = S -> S + end, + Packet = #message{from = AuthorJID, + to = JID, type = groupchat, subject = Subject}, + case ejabberd_hooks:run_fold(muc_filter_message, + StateData#state.server_host, + xmpp:put_meta(Packet, mam_ignore, true), + [StateData, Nick]) of + drop -> + ok; + NewPacket1 -> + FromRoomNick = jid:replace_resource(StateData#state.jid, Nick), + NewPacket2 = xmpp:set_from(NewPacket1, FromRoomNick), + ejabberd_router:route(NewPacket2) end. -can_change_subject(Role, StateData) -> +-spec check_subject(message()) -> [text()]. +check_subject(#message{subject = [_|_] = Subj, body = [], + thread = undefined}) -> + Subj; +check_subject(_) -> + []. + +-spec can_change_subject(role(), boolean(), state()) -> boolean(). +can_change_subject(Role, IsSubscriber, StateData) -> case (StateData#state.config)#config.allow_change_subj of - true -> Role == moderator orelse Role == participant; + true -> Role == moderator orelse Role == participant orelse IsSubscriber == true; _ -> Role == moderator end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Admin stuff -process_iq_admin(From, set, Lang, SubEl, StateData) -> - #xmlel{children = Items} = SubEl, +-spec process_iq_admin(jid(), iq(), #state{}) -> {error, stanza_error()} | + {result, undefined, #state{}} | + {result, muc_admin()}. +process_iq_admin(_From, #iq{lang = Lang, sub_els = [#muc_admin{items = []}]}, + _StateData) -> + Txt = ?T("No 'item' element found"), + {error, xmpp:err_bad_request(Txt, Lang)}; +process_iq_admin(_From, #iq{type = get, lang = Lang, + sub_els = [#muc_admin{items = [_, _|_]}]}, + _StateData) -> + ErrText = ?T("Too many elements"), + {error, xmpp:err_bad_request(ErrText, Lang)}; +process_iq_admin(From, #iq{type = set, lang = Lang, + sub_els = [#muc_admin{items = Items}]}, + StateData) -> process_admin_items_set(From, Items, Lang, StateData); -process_iq_admin(From, get, Lang, SubEl, StateData) -> - case xml:get_subtag(SubEl, <<"item">>) of - false -> {error, ?ERR_BAD_REQUEST}; - Item -> - FAffiliation = get_affiliation(From, StateData), - FRole = get_role(From, StateData), - case xml:get_tag_attr(<<"role">>, Item) of - false -> - case xml:get_tag_attr(<<"affiliation">>, Item) of - false -> {error, ?ERR_BAD_REQUEST}; - {value, StrAffiliation} -> - case catch list_to_affiliation(StrAffiliation) of - {'EXIT', _} -> {error, ?ERR_BAD_REQUEST}; - SAffiliation -> - if (FAffiliation == owner) or - (FAffiliation == admin) or - ((FAffiliation == member) and (SAffiliation == member)) -> - Items = items_with_affiliation(SAffiliation, - StateData), - {result, Items, StateData}; - true -> - ErrText = - <<"Administrator privileges required">>, - {error, ?ERRT_FORBIDDEN(Lang, ErrText)} - end - end - end; - {value, StrRole} -> - case catch list_to_role(StrRole) of - {'EXIT', _} -> {error, ?ERR_BAD_REQUEST}; - SRole -> - if FRole == moderator -> - Items = items_with_role(SRole, StateData), - {result, Items, StateData}; - true -> - ErrText = <<"Moderator privileges required">>, - {error, ?ERRT_FORBIDDEN(Lang, ErrText)} - end - end - end +process_iq_admin(From, #iq{type = get, lang = Lang, + sub_els = [#muc_admin{items = [Item]}]}, + StateData) -> + FAffiliation = get_affiliation(From, StateData), + FRole = get_role(From, StateData), + case Item of + #muc_item{role = undefined, affiliation = undefined} -> + Txt = ?T("Neither 'role' nor 'affiliation' attribute found"), + {error, xmpp:err_bad_request(Txt, Lang)}; + #muc_item{role = undefined, affiliation = Affiliation} -> + if (FAffiliation == owner) or + (FAffiliation == admin) or + ((FAffiliation == member) and + not (StateData#state.config)#config.anonymous) -> + Items = items_with_affiliation(Affiliation, StateData), + {result, #muc_admin{items = Items}}; + true -> + ErrText = ?T("Administrator privileges required"), + {error, xmpp:err_forbidden(ErrText, Lang)} + end; + #muc_item{role = Role} -> + if FRole == moderator -> + Items = items_with_role(Role, StateData), + {result, #muc_admin{items = Items}}; + true -> + ErrText = ?T("Moderator privileges required"), + {error, xmpp:err_forbidden(ErrText, Lang)} + end end. +-spec items_with_role(role(), state()) -> [muc_item()]. items_with_role(SRole, StateData) -> lists:map(fun ({_, U}) -> user_to_item(U, StateData) end, search_role(SRole, StateData)). +-spec items_with_affiliation(affiliation(), state()) -> [muc_item()]. items_with_affiliation(SAffiliation, StateData) -> - lists:map(fun ({JID, {Affiliation, Reason}}) -> - #xmlel{name = <<"item">>, - attrs = - [{<<"affiliation">>, - affiliation_to_list(Affiliation)}, - {<<"jid">>, jlib:jid_to_string(JID)}], - children = - [#xmlel{name = <<"reason">>, attrs = [], - children = [{xmlcdata, Reason}]}]}; - ({JID, Affiliation}) -> - #xmlel{name = <<"item">>, - attrs = - [{<<"affiliation">>, - affiliation_to_list(Affiliation)}, - {<<"jid">>, jlib:jid_to_string(JID)}], - children = []} - end, - search_affiliation(SAffiliation, StateData)). + lists:map( + fun({JID, {Affiliation, Reason}}) -> + #muc_item{affiliation = Affiliation, jid = jid:make(JID), + reason = Reason}; + ({JID, Affiliation}) -> + #muc_item{affiliation = Affiliation, jid = jid:make(JID)} + end, + search_affiliation(SAffiliation, StateData)). +-spec user_to_item(#user{}, state()) -> muc_item(). user_to_item(#user{role = Role, nick = Nick, jid = JID}, StateData) -> Affiliation = get_affiliation(JID, StateData), - #xmlel{name = <<"item">>, - attrs = - [{<<"role">>, role_to_list(Role)}, - {<<"affiliation">>, affiliation_to_list(Affiliation)}, - {<<"nick">>, Nick}, - {<<"jid">>, jlib:jid_to_string(JID)}], - children = []}. + #muc_item{role = Role, + affiliation = Affiliation, + nick = Nick, + jid = JID}. +-spec search_role(role(), state()) -> [{ljid(), #user{}}]. search_role(Role, StateData) -> lists:filter(fun ({_, #user{role = R}}) -> Role == R end, - (?DICT):to_list(StateData#state.users)). + maps:to_list(StateData#state.users)). +-spec search_affiliation(affiliation(), state()) -> + [{ljid(), + affiliation() | {affiliation(), binary()}}]. +search_affiliation(Affiliation, + #state{config = #config{persistent = false}} = StateData) -> + search_affiliation_fallback(Affiliation, StateData); search_affiliation(Affiliation, StateData) -> - lists:filter(fun ({_, A}) -> - case A of - {A1, _Reason} -> Affiliation == A1; - _ -> Affiliation == A - end - end, - (?DICT):to_list(StateData#state.affiliations)). + Room = StateData#state.room, + Host = StateData#state.host, + ServerHost = StateData#state.server_host, + Mod = gen_mod:db_mod(ServerHost, mod_muc), + case Mod:search_affiliation(ServerHost, Room, Host, Affiliation) of + {ok, AffiliationList} -> + AffiliationList; + {error, _} -> + search_affiliation_fallback(Affiliation, StateData) + end. +-spec search_affiliation_fallback(affiliation(), state()) -> + [{ljid(), + affiliation() | {affiliation(), binary()}}]. +search_affiliation_fallback(Affiliation, StateData) -> + lists:filter( + fun({_, A}) -> + case A of + {A1, _Reason} -> Affiliation == A1; + _ -> Affiliation == A + end + end, maps:to_list(StateData#state.affiliations)). + +-spec process_admin_items_set(jid(), [muc_item()], binary(), + #state{}) -> {result, undefined, #state{}} | + {error, stanza_error()}. process_admin_items_set(UJID, Items, Lang, StateData) -> UAffiliation = get_affiliation(UJID, StateData), URole = get_role(UJID, StateData), - case find_changed_items(UJID, UAffiliation, URole, - Items, Lang, StateData, []) + case catch find_changed_items(UJID, UAffiliation, URole, + Items, Lang, StateData, []) of {result, Res} -> - ?INFO_MSG("Processing MUC admin query from ~s in " - "room ~s:~n ~p", - [jlib:jid_to_string(UJID), - jlib:jid_to_string(StateData#state.jid), Res]), - NSD = lists:foldl(fun (E, SD) -> - case catch case E of - {JID, affiliation, owner, _} - when JID#jid.luser == - <<"">> -> - %% If the provided JID does not have username, - %% forget the affiliation completely - SD; - {JID, role, none, Reason} -> - catch - send_kickban_presence(UJID, JID, - Reason, - <<"307">>, - SD), - set_role(JID, none, SD); - {JID, affiliation, none, - Reason} -> - case - (SD#state.config)#config.members_only - of - true -> - catch - send_kickban_presence(UJID, JID, - Reason, - <<"321">>, - none, - SD), - SD1 = - set_affiliation(JID, - none, - SD), - set_role(JID, none, - SD1); - _ -> - SD1 = - set_affiliation(JID, - none, - SD), - send_update_presence(JID, - SD1), - SD1 - end; - {JID, affiliation, outcast, - Reason} -> - catch - send_kickban_presence(UJID, JID, - Reason, - <<"301">>, - outcast, - SD), - set_affiliation(JID, - outcast, - set_role(JID, - none, - SD), - Reason); - {JID, affiliation, A, Reason} - when (A == admin) or - (A == owner) -> - SD1 = set_affiliation(JID, - A, - SD, - Reason), - SD2 = set_role(JID, - moderator, - SD1), - send_update_presence(JID, - Reason, - SD2), - SD2; - {JID, affiliation, member, - Reason} -> - SD1 = set_affiliation(JID, - member, - SD, - Reason), - SD2 = set_role(JID, - participant, - SD1), - send_update_presence(JID, - Reason, - SD2), - SD2; - {JID, role, Role, Reason} -> - SD1 = set_role(JID, Role, - SD), - catch - send_new_presence(JID, - Reason, - SD1), - SD1; - {JID, affiliation, A, - _Reason} -> - SD1 = set_affiliation(JID, - A, - SD), - send_update_presence(JID, - SD1), - SD1 - end - of - {'EXIT', ErrReason} -> - ?ERROR_MSG("MUC ITEMS SET ERR: ~p~n", - [ErrReason]), - SD; - NSD -> NSD - end - end, - StateData, lists:flatten(Res)), - case (NSD#state.config)#config.persistent of - true -> - mod_muc:store_room(NSD#state.server_host, - NSD#state.host, NSD#state.room, - make_opts(NSD)); - _ -> ok - end, - {result, [], NSD}; - Err -> Err + ?INFO_MSG("Processing MUC admin query from ~ts in " + "room ~ts:~n ~p", + [jid:encode(UJID), + jid:encode(StateData#state.jid), Res]), + case lists:foldl(process_item_change(UJID), + StateData, lists:flatten(Res)) of + {error, _} = Err -> + Err; + NSD -> + store_room(NSD), + {result, undefined, NSD} + end; + {error, Err} -> {error, Err} end. +-spec process_item_change(jid()) -> fun((admin_action(), state() | {error, stanza_error()}) -> + state() | {error, stanza_error()}). +process_item_change(UJID) -> + fun(_, {error, _} = Err) -> + Err; + (Item, SD) -> + process_item_change(Item, SD, UJID) + end. + +-spec process_item_change(admin_action(), state(), undefined | jid()) -> state() | {error, stanza_error()}. +process_item_change(Item, SD, UJID) -> + try case Item of + {JID, affiliation, owner, _} when JID#jid.luser == <<"">> -> + %% If the provided JID does not have username, + %% forget the affiliation completely + SD; + {JID, role, none, Reason} -> + send_kickban_presence(UJID, JID, Reason, 307, SD), + set_role(JID, none, SD); + {JID, affiliation, none, Reason} -> + case get_affiliation(JID, SD) of + none -> SD; + _ -> + case (SD#state.config)#config.members_only of + true -> + send_kickban_presence(UJID, JID, Reason, 321, none, SD), + maybe_send_affiliation(JID, none, SD), + unsubscribe_from_room(JID, SD), + SD1 = set_affiliation(JID, none, SD), + set_role(JID, none, SD1); + _ -> + SD1 = set_affiliation(JID, none, SD), + SD2 = case (SD1#state.config)#config.moderated of + true -> set_role(JID, visitor, SD1); + false -> set_role(JID, participant, SD1) + end, + send_update_presence(JID, Reason, SD2, SD), + maybe_send_affiliation(JID, none, SD2), + SD2 + end + end; + {JID, affiliation, outcast, Reason} -> + send_kickban_presence(UJID, JID, Reason, 301, outcast, SD), + maybe_send_affiliation(JID, outcast, SD), + unsubscribe_from_room(JID, SD), + {result, undefined, SD2} = + process_iq_mucsub(JID, + #iq{type = set, + sub_els = [#muc_unsubscribe{}]}, SD), + set_role(JID, none, set_affiliation(JID, outcast, SD2, Reason)); + {JID, affiliation, A, Reason} when (A == admin) or (A == owner) -> + SD1 = set_affiliation(JID, A, SD, Reason), + SD2 = set_role(JID, moderator, SD1), + send_update_presence(JID, Reason, SD2, SD), + maybe_send_affiliation(JID, A, SD2), + SD2; + {JID, affiliation, member, Reason} -> + SD1 = set_affiliation(JID, member, SD, Reason), + SD2 = set_role(JID, participant, SD1), + send_update_presence(JID, Reason, SD2, SD), + maybe_send_affiliation(JID, member, SD2), + SD2; + {JID, role, Role, Reason} -> + SD1 = set_role(JID, Role, SD), + send_new_presence(JID, Reason, SD1, SD), + SD1; + {JID, affiliation, A, _Reason} -> + SD1 = set_affiliation(JID, A, SD), + send_update_presence(JID, SD1, SD), + maybe_send_affiliation(JID, A, SD1), + SD1 + end + catch + E:R:StackTrace -> + FromSuffix = case UJID of + #jid{} -> + JidString = jid:encode(UJID), + <<" from ", JidString/binary>>; + undefined -> + <<"">> + end, + ?ERROR_MSG("Failed to set item ~p~ts:~n** ~ts", + [Item, + FromSuffix, + misc:format_exception(2, E, R, StackTrace)]), + {error, xmpp:err_internal_server_error()} + end. + +-spec unsubscribe_from_room(jid(), state()) -> ok | error. +unsubscribe_from_room(JID, SD) -> + case SD#state.config#config.members_only of + false -> + ok; + true -> + case mod_muc:unhibernate_room(SD#state.server_host, SD#state.host, SD#state.room) of + {error, _Reason0} -> + error; + {ok, Pid} -> + _UnsubPid = + spawn(fun() -> + case unsubscribe(Pid, JID) of + ok -> + ok; + {error, Reason} -> + ?WARNING_MSG("Failed to automatically unsubscribe expelled member from room: ~ts", + [Reason]), + error + end + end) + end + end. + +-spec find_changed_items(jid(), affiliation(), role(), + [muc_item()], binary(), state(), [admin_action()]) -> + {result, [admin_action()]}. find_changed_items(_UJID, _UAffiliation, _URole, [], _Lang, _StateData, Res) -> {result, Res}; +find_changed_items(_UJID, _UAffiliation, _URole, + [#muc_item{jid = undefined, nick = <<"">>}|_], + Lang, _StateData, _Res) -> + Txt = ?T("Neither 'jid' nor 'nick' attribute found"), + throw({error, xmpp:err_bad_request(Txt, Lang)}); +find_changed_items(_UJID, _UAffiliation, _URole, + [#muc_item{role = undefined, affiliation = undefined}|_], + Lang, _StateData, _Res) -> + Txt = ?T("Neither 'role' nor 'affiliation' attribute found"), + throw({error, xmpp:err_bad_request(Txt, Lang)}); find_changed_items(UJID, UAffiliation, URole, - [{xmlcdata, _} | Items], Lang, StateData, Res) -> - find_changed_items(UJID, UAffiliation, URole, Items, - Lang, StateData, Res); -find_changed_items(UJID, UAffiliation, URole, - [#xmlel{name = <<"item">>, attrs = Attrs} = Item - | Items], + [#muc_item{jid = J, nick = Nick, reason = Reason, + role = Role, affiliation = Affiliation}|Items], Lang, StateData, Res) -> - TJID = case xml:get_attr(<<"jid">>, Attrs) of - {value, S} -> - case jlib:string_to_jid(S) of - error -> - ErrText = iolist_to_binary( - io_lib:format(translate:translate( - Lang, - <<"Jabber ID ~s is invalid">>), - [S])), - {error, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)}; - J -> {value, [J]} - end; - _ -> - case xml:get_attr(<<"nick">>, Attrs) of - {value, N} -> - case find_jids_by_nick(N, StateData) of - false -> - ErrText = iolist_to_binary( - io_lib:format( - translate:translate( - Lang, - <<"Nickname ~s does not exist in the room">>), - [N])), - {error, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)}; - J -> {value, J} - end; - _ -> {error, ?ERR_BAD_REQUEST} - end - end, - case TJID of - {value, [JID | _] = JIDs} -> - TAffiliation = get_affiliation(JID, StateData), - TRole = get_role(JID, StateData), - case xml:get_attr(<<"role">>, Attrs) of - false -> - case xml:get_attr(<<"affiliation">>, Attrs) of - false -> {error, ?ERR_BAD_REQUEST}; - {value, StrAffiliation} -> - case catch list_to_affiliation(StrAffiliation) of - {'EXIT', _} -> - ErrText1 = iolist_to_binary( - io_lib:format( - translate:translate( - Lang, - <<"Invalid affiliation: ~s">>), - [StrAffiliation])), - {error, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText1)}; - SAffiliation -> - ServiceAf = get_service_affiliation(JID, StateData), - CanChangeRA = case can_change_ra(UAffiliation, - URole, - TAffiliation, - TRole, affiliation, - SAffiliation, - ServiceAf) - of - nothing -> nothing; - true -> true; - check_owner -> - case search_affiliation(owner, - StateData) - of - [{OJID, _}] -> - jlib:jid_remove_resource(OJID) - /= - jlib:jid_tolower(jlib:jid_remove_resource(UJID)); - _ -> true - end; - _ -> false - end, - case CanChangeRA of - nothing -> - find_changed_items(UJID, UAffiliation, URole, - Items, Lang, StateData, - Res); - true -> - Reason = xml:get_path_s(Item, - [{elem, <<"reason">>}, - cdata]), - MoreRes = [{jlib:jid_remove_resource(Jidx), - affiliation, SAffiliation, Reason} - || Jidx <- JIDs], - find_changed_items(UJID, UAffiliation, URole, - Items, Lang, StateData, - [MoreRes | Res]); - false -> {error, ?ERR_NOT_ALLOWED} - end - end - end; - {value, StrRole} -> - case catch list_to_role(StrRole) of - {'EXIT', _} -> - ErrText1 = iolist_to_binary( - io_lib:format(translate:translate( - Lang, - <<"Invalid role: ~s">>), - [StrRole])), - {error, ?ERRT_BAD_REQUEST(Lang, ErrText1)}; - SRole -> - ServiceAf = get_service_affiliation(JID, StateData), - CanChangeRA = case can_change_ra(UAffiliation, URole, - TAffiliation, TRole, - role, SRole, ServiceAf) - of - nothing -> nothing; - true -> true; - check_owner -> - case search_affiliation(owner, - StateData) - of - [{OJID, _}] -> - jlib:jid_remove_resource(OJID) - /= - jlib:jid_tolower(jlib:jid_remove_resource(UJID)); - _ -> true - end; - _ -> false - end, - case CanChangeRA of - nothing -> - find_changed_items(UJID, UAffiliation, URole, Items, - Lang, StateData, Res); - true -> - Reason = xml:get_path_s(Item, - [{elem, <<"reason">>}, - cdata]), - MoreRes = [{Jidx, role, SRole, Reason} - || Jidx <- JIDs], - find_changed_items(UJID, UAffiliation, URole, Items, - Lang, StateData, - [MoreRes | Res]); - _ -> {error, ?ERR_NOT_ALLOWED} - end + [JID | _] = JIDs = + if J /= undefined -> + [J]; + Nick /= <<"">> -> + case find_jids_by_nick(Nick, StateData) of + [] -> + ErrText = {?T("Nickname ~s does not exist in the room"), + [Nick]}, + throw({error, xmpp:err_not_acceptable(ErrText, Lang)}); + JIDList -> + JIDList end - end; - Err -> Err - end; -find_changed_items(_UJID, _UAffiliation, _URole, _Items, - _Lang, _StateData, _Res) -> - {error, ?ERR_BAD_REQUEST}. + end, + {RoleOrAff, RoleOrAffValue} = if Role == undefined -> + {affiliation, Affiliation}; + true -> + {role, Role} + end, + TAffiliation = get_affiliation(JID, StateData), + TRole = get_role(JID, StateData), + ServiceAf = get_service_affiliation(JID, StateData), + UIsSubscriber = is_subscriber(UJID, StateData), + URole1 = case {URole, UIsSubscriber} of + {none, true} -> subscriber; + {UR, _} -> UR + end, + CanChangeRA = case can_change_ra(UAffiliation, + URole1, + TAffiliation, + TRole, RoleOrAff, RoleOrAffValue, + ServiceAf) of + nothing -> nothing; + true -> true; + check_owner -> + case search_affiliation(owner, StateData) of + [{OJID, _}] -> + jid:remove_resource(OJID) + /= + jid:tolower(jid:remove_resource(UJID)); + _ -> true + end; + _ -> false + end, + case CanChangeRA of + nothing -> + find_changed_items(UJID, UAffiliation, URole, + Items, Lang, StateData, + Res); + true -> + MoreRes = case RoleOrAff of + affiliation -> + [{jid:remove_resource(Jidx), + RoleOrAff, RoleOrAffValue, Reason} + || Jidx <- JIDs]; + role -> + [{Jidx, RoleOrAff, RoleOrAffValue, Reason} + || Jidx <- JIDs] + end, + find_changed_items(UJID, UAffiliation, URole, + Items, Lang, StateData, + MoreRes ++ Res); + false -> + Txt = ?T("Changing role/affiliation is not allowed"), + throw({error, xmpp:err_not_allowed(Txt, Lang)}) + end. +-spec can_change_ra(affiliation(), role(), affiliation(), role(), + affiliation, affiliation(), affiliation()) -> boolean() | nothing | check_owner; + (affiliation(), role(), affiliation(), role(), + role, role(), affiliation()) -> boolean() | nothing | check_owner. can_change_ra(_FAffiliation, _FRole, owner, _TRole, affiliation, owner, owner) -> %% A room owner tries to add as persistent owner a @@ -2961,9 +3482,19 @@ can_change_ra(_FAffiliation, _FRole, _TAffiliation, can_change_ra(_FAffiliation, moderator, _TAffiliation, visitor, role, none, _ServiceAf) -> true; +can_change_ra(FAffiliation, subscriber, _TAffiliation, + visitor, role, none, _ServiceAf) + when (FAffiliation == owner) or + (FAffiliation == admin) -> + true; can_change_ra(_FAffiliation, moderator, _TAffiliation, visitor, role, participant, _ServiceAf) -> true; +can_change_ra(FAffiliation, subscriber, _TAffiliation, + visitor, role, participant, _ServiceAf) + when (FAffiliation == owner) or + (FAffiliation == admin) -> + true; can_change_ra(FAffiliation, _FRole, _TAffiliation, visitor, role, moderator, _ServiceAf) when (FAffiliation == owner) or @@ -2972,9 +3503,19 @@ can_change_ra(FAffiliation, _FRole, _TAffiliation, can_change_ra(_FAffiliation, moderator, _TAffiliation, participant, role, none, _ServiceAf) -> true; +can_change_ra(FAffiliation, subscriber, _TAffiliation, + participant, role, none, _ServiceAf) + when (FAffiliation == owner) or + (FAffiliation == admin) -> + true; can_change_ra(_FAffiliation, moderator, _TAffiliation, participant, role, visitor, _ServiceAf) -> true; +can_change_ra(FAffiliation, subscriber, _TAffiliation, + participant, role, visitor, _ServiceAf) + when (FAffiliation == owner) or + (FAffiliation == admin) -> + true; can_change_ra(FAffiliation, _FRole, _TAffiliation, participant, role, moderator, _ServiceAf) when (FAffiliation == owner) or @@ -3004,36 +3545,56 @@ can_change_ra(_FAffiliation, _FRole, admin, moderator, can_change_ra(admin, _FRole, _TAffiliation, moderator, role, participant, _ServiceAf) -> true; +can_change_ra(owner, moderator, TAffiliation, + moderator, role, none, _ServiceAf) + when TAffiliation /= owner -> + true; +can_change_ra(owner, subscriber, TAffiliation, + moderator, role, none, _ServiceAf) + when TAffiliation /= owner -> + true; +can_change_ra(admin, moderator, TAffiliation, + moderator, role, none, _ServiceAf) + when (TAffiliation /= owner) and + (TAffiliation /= admin) -> + true; +can_change_ra(admin, subscriber, TAffiliation, + moderator, role, none, _ServiceAf) + when (TAffiliation /= owner) and + (TAffiliation /= admin) -> + true; can_change_ra(_FAffiliation, _FRole, _TAffiliation, _TRole, role, _Value, _ServiceAf) -> false. +-spec send_kickban_presence(undefined | jid(), jid(), binary(), + pos_integer(), state()) -> ok. send_kickban_presence(UJID, JID, Reason, Code, StateData) -> NewAffiliation = get_affiliation(JID, StateData), send_kickban_presence(UJID, JID, Reason, Code, NewAffiliation, StateData). +-spec send_kickban_presence(undefined | jid(), jid(), binary(), pos_integer(), + affiliation(), state()) -> ok. send_kickban_presence(UJID, JID, Reason, Code, NewAffiliation, StateData) -> - LJID = jlib:jid_tolower(JID), + LJID = jid:tolower(JID), LJIDs = case LJID of - {U, S, <<"">>} -> - (?DICT):fold(fun (J, _, Js) -> - case J of - {U, S, _} -> [J | Js]; - _ -> Js - end - end, - [], StateData#state.users); - _ -> - case (?DICT):is_key(LJID, StateData#state.users) of - true -> [LJID]; - _ -> [] - end + {U, S, <<"">>} -> + maps:fold(fun (J, _, Js) -> + case J of + {U, S, _} -> [J | Js]; + _ -> Js + end + end, [], StateData#state.users); + _ -> + case maps:is_key(LJID, StateData#state.users) of + true -> [LJID]; + _ -> [] + end end, - lists:foreach(fun (J) -> - {ok, #user{nick = Nick}} = (?DICT):find(J, - StateData#state.users), + lists:foreach(fun (LJ) -> + #user{nick = Nick, jid = J} = maps:get(LJ, StateData#state.users), add_to_log(kickban, {Nick, Reason, Code}, StateData), tab_remove_online_user(J, StateData), send_kickban_presence1(UJID, J, Reason, Code, @@ -3041,769 +3602,506 @@ send_kickban_presence(UJID, JID, Reason, Code, NewAffiliation, end, LJIDs). +-spec send_kickban_presence1(undefined | jid(), jid(), binary(), pos_integer(), + affiliation(), state()) -> ok. send_kickban_presence1(MJID, UJID, Reason, Code, Affiliation, StateData) -> - {ok, #user{jid = RealJID, nick = Nick}} = - (?DICT):find(jlib:jid_tolower(UJID), - StateData#state.users), - SAffiliation = affiliation_to_list(Affiliation), - BannedJIDString = jlib:jid_to_string(RealJID), - case MJID /= <<"">> of - true -> - {ok, #user{nick = ActorNick}} = - (?DICT):find(jlib:jid_tolower(MJID), - StateData#state.users); - false -> - ActorNick = <<"">> - end, - lists:foreach(fun ({_LJID, Info}) -> - JidAttrList = case Info#user.role == moderator orelse - (StateData#state.config)#config.anonymous - == false - of - true -> - [{<<"jid">>, BannedJIDString}]; - false -> [] - end, - ItemAttrs = [{<<"affiliation">>, SAffiliation}, - {<<"role">>, <<"none">>}] - ++ JidAttrList, - ItemEls = case Reason of - <<"">> -> []; - _ -> - [#xmlel{name = <<"reason">>, - attrs = [], - children = - [{xmlcdata, Reason}]}] - end, - ItemElsActor = case MJID of - <<"">> -> []; - _ -> [#xmlel{name = <<"actor">>, - attrs = - [{<<"nick">>, ActorNick}]}] - end, - Packet = #xmlel{name = <<"presence">>, - attrs = - [{<<"type">>, <<"unavailable">>}], - children = - [#xmlel{name = <<"x">>, - attrs = - [{<<"xmlns">>, - ?NS_MUC_USER}], - children = - [#xmlel{name = - <<"item">>, - attrs = - ItemAttrs, - children = - ItemElsActor ++ ItemEls}, - #xmlel{name = - <<"status">>, - attrs = - [{<<"code">>, - Code}], - children = - []}]}]}, - ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, - Nick), - Info#user.jid, Packet) - end, - (?DICT):to_list(StateData#state.users)). + #user{jid = RealJID, nick = Nick} = maps:get(jid:tolower(UJID), StateData#state.users), + ActorNick = find_nick_by_jid(MJID, StateData), + %% TODO: optimize further + UserMap = + maps:merge( + get_users_and_subscribers_with_node( + ?NS_MUCSUB_NODES_AFFILIATIONS, StateData), + get_users_and_subscribers_with_node( + ?NS_MUCSUB_NODES_PARTICIPANTS, StateData)), + maps:fold( + fun(LJID, Info, _) -> + IsSelfPresence = jid:tolower(UJID) == LJID, + Item0 = #muc_item{affiliation = Affiliation, + role = none}, + Item1 = case Info#user.role == moderator orelse + (StateData#state.config)#config.anonymous + == false orelse IsSelfPresence of + true -> Item0#muc_item{jid = RealJID}; + false -> Item0 + end, + Item2 = Item1#muc_item{reason = Reason}, + Item = case ActorNick of + <<"">> -> Item2; + _ -> Item2#muc_item{actor = #muc_actor{nick = ActorNick}} + end, + Codes = if IsSelfPresence -> [110, Code]; + true -> [Code] + end, + Packet = #presence{type = unavailable, + sub_els = [#muc_user{items = [Item], + status_codes = Codes}]}, + RoomJIDNick = jid:replace_resource(StateData#state.jid, Nick), + send_wrapped(RoomJIDNick, Info#user.jid, Packet, + ?NS_MUCSUB_NODES_AFFILIATIONS, StateData), + IsSubscriber = is_subscriber(Info#user.jid, StateData), + IsOccupant = Info#user.last_presence /= undefined, + if (IsSubscriber and not IsOccupant) -> + send_wrapped(RoomJIDNick, Info#user.jid, Packet, + ?NS_MUCSUB_NODES_PARTICIPANTS, StateData); + true -> + ok + end + end, ok, UserMap). + +-spec convert_legacy_fields([xdata_field()]) -> [xdata_field()]. +convert_legacy_fields(Fs) -> + lists:map( + fun(#xdata_field{var = Var} = F) -> + NewVar = case Var of + <<"muc#roomconfig_allowvisitorstatus">> -> + <<"allow_visitor_status">>; + <<"muc#roomconfig_allowvisitornickchange">> -> + <<"allow_visitor_nickchange">>; + <<"muc#roomconfig_allowvoicerequests">> -> + <<"allow_voice_requests">>; + <<"muc#roomconfig_allow_subscription">> -> + <<"allow_subscription">>; + <<"muc#roomconfig_voicerequestmininterval">> -> + <<"voice_request_min_interval">>; + <<"muc#roomconfig_captcha_whitelist">> -> + <<"captcha_whitelist">>; + <<"muc#roomconfig_mam">> -> + <<"mam">>; + _ -> + Var + end, + F#xdata_field{var = NewVar} + end, Fs). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Owner stuff - -process_iq_owner(From, set, Lang, SubEl, StateData) -> +-spec process_iq_owner(jid(), iq(), state()) -> + {result, undefined | muc_owner()} | + {result, undefined | muc_owner(), state() | stop} | + {error, stanza_error()}. +process_iq_owner(From, #iq{type = set, lang = Lang, + sub_els = [#muc_owner{destroy = Destroy, + config = Config, + items = Items}]}, + StateData) -> FAffiliation = get_affiliation(From, StateData), - case FAffiliation of - owner -> - #xmlel{children = Els} = SubEl, - case xml:remove_cdata(Els) of - [#xmlel{name = <<"x">>} = XEl] -> - case {xml:get_tag_attr_s(<<"xmlns">>, XEl), - xml:get_tag_attr_s(<<"type">>, XEl)} - of - {?NS_XDATA, <<"cancel">>} -> {result, [], StateData}; - {?NS_XDATA, <<"submit">>} -> - case is_allowed_log_change(XEl, StateData, From) andalso - is_allowed_persistent_change(XEl, StateData, From) - andalso - is_allowed_room_name_desc_limits(XEl, StateData) - andalso - is_password_settings_correct(XEl, StateData) - of - true -> set_config(XEl, StateData); - false -> {error, ?ERR_NOT_ACCEPTABLE} - end; - _ -> {error, ?ERR_BAD_REQUEST} - end; - [#xmlel{name = <<"destroy">>} = SubEl1] -> - ?INFO_MSG("Destroyed MUC room ~s by the owner ~s", - [jlib:jid_to_string(StateData#state.jid), - jlib:jid_to_string(From)]), - add_to_log(room_existence, destroyed, StateData), - destroy_room(SubEl1, StateData); - Items -> - process_admin_items_set(From, Items, Lang, StateData) - end; - _ -> - ErrText = <<"Owner privileges required">>, - {error, ?ERRT_FORBIDDEN(Lang, ErrText)} + if FAffiliation /= owner -> + ErrText = ?T("Owner privileges required"), + {error, xmpp:err_forbidden(ErrText, Lang)}; + Destroy /= undefined, Config == undefined, Items == [] -> + ?INFO_MSG("Destroyed MUC room ~ts by the owner ~ts", + [jid:encode(StateData#state.jid), jid:encode(From)]), + add_to_log(room_existence, destroyed, StateData), + destroy_room(Destroy, StateData); + Config /= undefined, Destroy == undefined, Items == [] -> + case Config of + #xdata{type = cancel} -> + {result, undefined}; + #xdata{type = submit, fields = Fs} -> + Fs1 = convert_legacy_fields(Fs), + try muc_roomconfig:decode(Fs1) of + Options -> + case is_allowed_log_change(Options, StateData, From) andalso + is_allowed_persistent_change(Options, StateData, From) andalso + is_allowed_mam_change(Options, StateData, From) andalso + is_allowed_string_limits(Options, StateData) andalso + is_password_settings_correct(Options, StateData) of + true -> + set_config(Options, StateData, Lang); + false -> + {error, xmpp:err_not_acceptable()} + end + catch _:{muc_roomconfig, Why} -> + Txt = muc_roomconfig:format_error(Why), + {error, xmpp:err_bad_request(Txt, Lang)} + end; + _ -> + Txt = ?T("Incorrect data form"), + {error, xmpp:err_bad_request(Txt, Lang)} + end; + Items /= [], Config == undefined, Destroy == undefined -> + process_admin_items_set(From, Items, Lang, StateData); + true -> + {error, xmpp:err_bad_request()} end; -process_iq_owner(From, get, Lang, SubEl, StateData) -> +process_iq_owner(From, #iq{type = get, lang = Lang, + sub_els = [#muc_owner{destroy = Destroy, + config = Config, + items = Items}]}, + StateData) -> FAffiliation = get_affiliation(From, StateData), - case FAffiliation of - owner -> - #xmlel{children = Els} = SubEl, - case xml:remove_cdata(Els) of - [] -> get_config(Lang, StateData, From); - [Item] -> - case xml:get_tag_attr(<<"affiliation">>, Item) of - false -> {error, ?ERR_BAD_REQUEST}; - {value, StrAffiliation} -> - case catch list_to_affiliation(StrAffiliation) of - {'EXIT', _} -> - ErrText = iolist_to_binary( - io_lib:format( - translate:translate( - Lang, - <<"Invalid affiliation: ~s">>), - [StrAffiliation])), - {error, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)}; - SAffiliation -> - Items = items_with_affiliation(SAffiliation, - StateData), - {result, Items, StateData} - end - end; - _ -> {error, ?ERR_FEATURE_NOT_IMPLEMENTED} - end; - _ -> - ErrText = <<"Owner privileges required">>, - {error, ?ERRT_FORBIDDEN(Lang, ErrText)} + if FAffiliation /= owner -> + ErrText = ?T("Owner privileges required"), + {error, xmpp:err_forbidden(ErrText, Lang)}; + Destroy == undefined, Config == undefined -> + case Items of + [] -> + {result, + #muc_owner{config = get_config(Lang, StateData, From)}}; + [#muc_item{affiliation = undefined}] -> + Txt = ?T("No 'affiliation' attribute found"), + {error, xmpp:err_bad_request(Txt, Lang)}; + [#muc_item{affiliation = Affiliation}] -> + Items = items_with_affiliation(Affiliation, StateData), + {result, #muc_owner{items = Items}}; + [_|_] -> + Txt = ?T("Too many elements"), + {error, xmpp:err_bad_request(Txt, Lang)} + end; + true -> + {error, xmpp:err_bad_request()} end. -is_allowed_log_change(XEl, StateData, From) -> - case lists:keymember(<<"muc#roomconfig_enablelogging">>, - 1, jlib:parse_xdata_submit(XEl)) - of - false -> true; - true -> - allow == - mod_muc_log:check_access_log(StateData#state.server_host, - From) +-spec is_allowed_log_change(muc_roomconfig:result(), state(), jid()) -> boolean(). +is_allowed_log_change(Options, StateData, From) -> + case proplists:is_defined(enablelogging, Options) of + false -> true; + true -> + allow == + ejabberd_hooks:run_fold(muc_log_check_access_log, + StateData#state.server_host, + deny, + [StateData#state.server_host, From]) end. -is_allowed_persistent_change(XEl, StateData, From) -> - case - lists:keymember(<<"muc#roomconfig_persistentroom">>, 1, - jlib:parse_xdata_submit(XEl)) - of +-spec is_allowed_persistent_change(muc_roomconfig:result(), state(), jid()) -> boolean(). +is_allowed_persistent_change(Options, StateData, From) -> + case proplists:is_defined(persistentroom, Options) of false -> true; true -> {_AccessRoute, _AccessCreate, _AccessAdmin, - AccessPersistent} = + AccessPersistent, _AccessMam} = StateData#state.access, allow == acl:match_rule(StateData#state.server_host, AccessPersistent, From) end. -%% Check if the Room Name and Room Description defined in the Data Form +-spec is_allowed_mam_change(muc_roomconfig:result(), state(), jid()) -> boolean(). +is_allowed_mam_change(Options, StateData, From) -> + case proplists:is_defined(mam, Options) of + false -> true; + true -> + {_AccessRoute, _AccessCreate, _AccessAdmin, + _AccessPersistent, AccessMam} = + StateData#state.access, + allow == + acl:match_rule(StateData#state.server_host, + AccessMam, From) + end. + +%% Check if the string fields defined in the Data Form %% are conformant to the configured limits -is_allowed_room_name_desc_limits(XEl, StateData) -> - IsNameAccepted = case - lists:keysearch(<<"muc#roomconfig_roomname">>, 1, - jlib:parse_xdata_submit(XEl)) - of - {value, {_, [N]}} -> - byte_size(N) =< - gen_mod:get_module_opt(StateData#state.server_host, - mod_muc, max_room_name, - fun(infinity) -> infinity; - (I) when is_integer(I), - I>0 -> I - end, infinity); - _ -> true - end, - IsDescAccepted = case - lists:keysearch(<<"muc#roomconfig_roomdesc">>, 1, - jlib:parse_xdata_submit(XEl)) - of - {value, {_, [D]}} -> - byte_size(D) =< - gen_mod:get_module_opt(StateData#state.server_host, - mod_muc, max_room_desc, - fun(infinity) -> infinity; - (I) when is_integer(I), - I>0 -> - I - end, infinity); - _ -> true - end, - IsNameAccepted and IsDescAccepted. +-spec is_allowed_string_limits(muc_roomconfig:result(), state()) -> boolean(). +is_allowed_string_limits(Options, StateData) -> + RoomName = proplists:get_value(roomname, Options, <<"">>), + RoomDesc = proplists:get_value(roomdesc, Options, <<"">>), + Password = proplists:get_value(roomsecret, Options, <<"">>), + CaptchaWhitelist = proplists:get_value(captcha_whitelist, Options, []), + CaptchaWhitelistSize = lists:foldl( + fun(Jid, Sum) -> byte_size(jid:encode(Jid)) + Sum end, + 0, CaptchaWhitelist), + MaxRoomName = mod_muc_opt:max_room_name(StateData#state.server_host), + MaxRoomDesc = mod_muc_opt:max_room_desc(StateData#state.server_host), + MaxPassword = mod_muc_opt:max_password(StateData#state.server_host), + MaxCaptchaWhitelist = mod_muc_opt:max_captcha_whitelist(StateData#state.server_host), + (byte_size(RoomName) =< MaxRoomName) + andalso (byte_size(RoomDesc) =< MaxRoomDesc) + andalso (byte_size(Password) =< MaxPassword) + andalso (CaptchaWhitelistSize =< MaxCaptchaWhitelist). %% Return false if: %% "the password for a password-protected room is blank" -is_password_settings_correct(XEl, StateData) -> +-spec is_password_settings_correct(muc_roomconfig:result(), state()) -> boolean(). +is_password_settings_correct(Options, StateData) -> Config = StateData#state.config, OldProtected = Config#config.password_protected, OldPassword = Config#config.password, - NewProtected = case - lists:keysearch(<<"muc#roomconfig_passwordprotectedroom">>, - 1, jlib:parse_xdata_submit(XEl)) - of - {value, {_, [<<"1">>]}} -> true; - {value, {_, [<<"0">>]}} -> false; - _ -> undefined - end, - NewPassword = case - lists:keysearch(<<"muc#roomconfig_roomsecret">>, 1, - jlib:parse_xdata_submit(XEl)) - of - {value, {_, [P]}} -> P; - _ -> undefined - end, - case {OldProtected, NewProtected, OldPassword, - NewPassword} - of - {true, undefined, <<"">>, undefined} -> false; - {true, undefined, _, <<"">>} -> false; - {_, true, <<"">>, undefined} -> false; - {_, true, _, <<"">>} -> false; - _ -> true + NewProtected = proplists:get_value(passwordprotectedroom, Options), + NewPassword = proplists:get_value(roomsecret, Options), + case {OldProtected, NewProtected, OldPassword, NewPassword} of + {true, undefined, <<"">>, undefined} -> false; + {true, undefined, _, <<"">>} -> false; + {_, true, <<"">>, undefined} -> false; + {_, true, _, <<"">>} -> false; + _ -> true end. --define(XFIELD(Type, Label, Var, Val), - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, Type}, - {<<"label">>, translate:translate(Lang, Label)}, - {<<"var">>, Var}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, Val}]}]}). - --define(BOOLXFIELD(Label, Var, Val), - ?XFIELD(<<"boolean">>, Label, Var, - case Val of - true -> <<"1">>; - _ -> <<"0">> - end)). - --define(STRINGXFIELD(Label, Var, Val), - ?XFIELD(<<"text-single">>, Label, Var, Val)). - --define(PRIVATEXFIELD(Label, Var, Val), - ?XFIELD(<<"text-private">>, Label, Var, Val)). - --define(JIDMULTIXFIELD(Label, Var, JIDList), - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, <<"jid-multi">>}, - {<<"label">>, translate:translate(Lang, Label)}, - {<<"var">>, Var}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, jlib:jid_to_string(JID)}]} - || JID <- JIDList]}). - +-spec get_default_room_maxusers(state()) -> non_neg_integer(). get_default_room_maxusers(RoomState) -> DefRoomOpts = - gen_mod:get_module_opt(RoomState#state.server_host, - mod_muc, default_room_options, - fun(L) when is_list(L) -> L end, - []), + mod_muc_opt:default_room_options(RoomState#state.server_host), RoomState2 = set_opts(DefRoomOpts, RoomState), (RoomState2#state.config)#config.max_users. +-spec get_config(binary(), state(), jid()) -> xdata(). get_config(Lang, StateData, From) -> - {_AccessRoute, _AccessCreate, _AccessAdmin, - AccessPersistent} = + {_AccessRoute, _AccessCreate, _AccessAdmin, AccessPersistent, _AccessMam} = StateData#state.access, ServiceMaxUsers = get_service_max_users(StateData), - DefaultRoomMaxUsers = - get_default_room_maxusers(StateData), + DefaultRoomMaxUsers = get_default_room_maxusers(StateData), Config = StateData#state.config, - {MaxUsersRoomInteger, MaxUsersRoomString} = case - get_max_users(StateData) - of - N when is_integer(N) -> - {N, - jlib:integer_to_binary(N)}; - _ -> {0, <<"none">>} - end, - Res = [#xmlel{name = <<"title">>, attrs = [], - children = - [{xmlcdata, - iolist_to_binary( - io_lib:format( - translate:translate( - Lang, - <<"Configuration of room ~s">>), - [jlib:jid_to_string(StateData#state.jid)]))}]}, - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, <<"hidden">>}, - {<<"var">>, <<"FORM_TYPE">>}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = - [{xmlcdata, - <<"http://jabber.org/protocol/muc#roomconfig">>}]}]}, - ?STRINGXFIELD(<<"Room title">>, - <<"muc#roomconfig_roomname">>, (Config#config.title)), - ?STRINGXFIELD(<<"Room description">>, - <<"muc#roomconfig_roomdesc">>, - (Config#config.description))] - ++ - case acl:match_rule(StateData#state.server_host, - AccessPersistent, From) - of - allow -> - [?BOOLXFIELD(<<"Make room persistent">>, - <<"muc#roomconfig_persistentroom">>, - (Config#config.persistent))]; + MaxUsersRoom = get_max_users(StateData), + Title = str:translate_and_format( + Lang, ?T("Configuration of room ~s"), + [jid:encode(StateData#state.jid)]), + Fs = [{roomname, Config#config.title}, + {roomdesc, Config#config.description}, + {lang, Config#config.lang}] ++ + case acl:match_rule(StateData#state.server_host, AccessPersistent, From) of + allow -> [{persistentroom, Config#config.persistent}]; + deny -> [] + end ++ + [{publicroom, Config#config.public}, + {public_list, Config#config.public_list}, + {passwordprotectedroom, Config#config.password_protected}, + {roomsecret, case Config#config.password_protected of + true -> Config#config.password; + false -> <<"">> + end}, + {maxusers, MaxUsersRoom, + [if is_integer(ServiceMaxUsers) -> []; + true -> [{?T("No limit"), <<"none">>}] + end] ++ [{integer_to_binary(N), N} + || N <- lists:usort([ServiceMaxUsers, + DefaultRoomMaxUsers, + MaxUsersRoom + | ?MAX_USERS_DEFAULT_LIST]), + N =< ServiceMaxUsers]}, + {whois, if Config#config.anonymous -> moderators; + true -> anyone + end}, + {presencebroadcast, Config#config.presence_broadcast}, + {membersonly, Config#config.members_only}, + {moderatedroom, Config#config.moderated}, + {members_by_default, Config#config.members_by_default}, + {changesubject, Config#config.allow_change_subj}, + {allowpm, Config#config.allowpm}, + {allow_private_messages_from_visitors, + Config#config.allow_private_messages_from_visitors}, + {allow_query_users, Config#config.allow_query_users}, + {allowinvites, Config#config.allow_user_invites}, + {allow_visitor_status, Config#config.allow_visitor_status}, + {allow_visitor_nickchange, Config#config.allow_visitor_nickchange}, + {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}, + {enable_hats, Config#config.enable_hats}] + ++ + case ejabberd_captcha:is_feature_available() of + true -> + [{captcha_protected, Config#config.captcha_protected}, + {captcha_whitelist, + lists:map( + fun jid:make/1, + ?SETS:to_list(Config#config.captcha_whitelist))}]; + false -> + [] + end + ++ + case ejabberd_hooks:run_fold(muc_log_check_access_log, + StateData#state.server_host, + deny, + [StateData#state.server_host, From]) of + allow -> [{enablelogging, Config#config.logging}]; + deny -> [] + end, + Fields = ejabberd_hooks:run_fold(get_room_config, + StateData#state.server_host, + Fs, + [StateData, From, Lang]), + #xdata{type = form, title = Title, + fields = muc_roomconfig:encode(Fields, Lang)}. + +-spec set_config(muc_roomconfig:result(), state(), binary()) -> + {error, stanza_error()} | {result, undefined, state()}. +set_config(Options, StateData, Lang) -> + try + #config{} = Config = set_config(Options, StateData#state.config, + StateData#state.server_host, Lang), + {result, _, NSD} = Res = change_config(Config, StateData), + Type = case {(StateData#state.config)#config.logging, + Config#config.logging} + of + {true, false} -> roomconfig_change_disabledlogging; + {false, true} -> roomconfig_change_enabledlogging; + {_, _} -> roomconfig_change + end, + Users = [{U#user.jid, U#user.nick, U#user.role} + || U <- maps:values(StateData#state.users)], + add_to_log(Type, Users, NSD), + Res + catch _:{badmatch, {error, #stanza_error{}} = Err} -> + Err + end. + +-spec get_config_opt_name(pos_integer()) -> atom(). +get_config_opt_name(Pos) -> + Fs = [config|record_info(fields, config)], + lists:nth(Pos, Fs). + +-spec set_config([muc_roomconfig:property()], #config{}, + binary(), binary()) -> #config{} | {error, stanza_error()}. +set_config(Opts, Config, ServerHost, Lang) -> + lists:foldl( + fun(_, {error, _} = Err) -> Err; + ({roomname, Title}, C) -> C#config{title = Title}; + ({roomdesc, Desc}, C) -> C#config{description = Desc}; + ({changesubject, V}, C) -> C#config{allow_change_subj = V}; + ({allow_query_users, V}, C) -> C#config{allow_query_users = V}; + ({allowpm, V}, C) -> + C#config{allowpm = V}; + ({allow_private_messages_from_visitors, V}, C) -> + C#config{allow_private_messages_from_visitors = V}; + ({allow_visitor_status, V}, C) -> C#config{allow_visitor_status = V}; + ({allow_visitor_nickchange, V}, C) -> + C#config{allow_visitor_nickchange = V}; + ({publicroom, V}, C) -> C#config{public = V}; + ({public_list, V}, C) -> C#config{public_list = V}; + ({persistentroom, V}, C) -> C#config{persistent = V}; + ({moderatedroom, V}, C) -> C#config{moderated = V}; + ({members_by_default, V}, C) -> C#config{members_by_default = V}; + ({membersonly, V}, C) -> C#config{members_only = V}; + ({captcha_protected, V}, C) -> C#config{captcha_protected = V}; + ({allowinvites, V}, C) -> C#config{allow_user_invites = V}; + ({allow_subscription, V}, C) -> C#config{allow_subscription = V}; + ({passwordprotectedroom, V}, C) -> C#config{password_protected = V}; + ({roomsecret, V}, C) -> C#config{password = V}; + ({anonymous, V}, C) -> C#config{anonymous = V}; + ({presencebroadcast, V}, C) -> C#config{presence_broadcast = V}; + ({allow_voice_requests, V}, C) -> C#config{allow_voice_requests = V}; + ({voice_request_min_interval, V}, C) -> + C#config{voice_request_min_interval = V}; + ({whois, moderators}, C) -> C#config{anonymous = true}; + ({whois, anyone}, C) -> C#config{anonymous = false}; + ({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], + C#config{captcha_whitelist = ?SETS:from_list(LJIDs)}; + ({O, V} = Opt, C) -> + case ejabberd_hooks:run_fold(set_room_option, + ServerHost, + {0, undefined}, + [Opt, Lang]) of + {0, undefined} -> + ?ERROR_MSG("set_room_option hook failed for " + "option '~ts' with value ~p", [O, V]), + Txt = {?T("Failed to process option '~s'"), [O]}, + {error, xmpp:err_internal_server_error(Txt, Lang)}; + {Pos, Val} -> + setelement(Pos, C, Val) + end + end, Config, Opts). + +-spec change_config(#config{}, state()) -> {result, undefined, state()}. +change_config(Config, StateData) -> + send_config_change_info(Config, StateData), + StateData0 = StateData#state{config = Config}, + StateData1 = remove_subscriptions(StateData0), + StateData2 = + case {(StateData#state.config)#config.persistent, + Config#config.persistent} of + {WasPersistent, true} -> + if not WasPersistent -> + set_affiliations(StateData1#state.affiliations, + StateData1); + true -> + ok + end, + store_room(StateData1), + StateData1; + {true, false} -> + Affiliations = get_affiliations(StateData), + maybe_forget_room(StateData), + StateData1#state{affiliations = Affiliations}; + _ -> + StateData1 + end, + case {(StateData#state.config)#config.members_only, + Config#config.members_only} of + {false, true} -> + StateData3 = remove_nonmembers(StateData2), + {result, undefined, StateData3}; + _ -> + {result, undefined, StateData2} + end. + +-spec send_config_change_info(#config{}, state()) -> ok. +send_config_change_info(Config, #state{config = Config}) -> ok; +send_config_change_info(New, #state{config = Old} = StateData) -> + Codes = case {Old#config.logging, New#config.logging} of + {false, true} -> [170]; + {true, false} -> [171]; _ -> [] end ++ - [?BOOLXFIELD(<<"Make room public searchable">>, - <<"muc#roomconfig_publicroom">>, - (Config#config.public)), - ?BOOLXFIELD(<<"Make participants list public">>, - <<"public_list">>, (Config#config.public_list)), - ?BOOLXFIELD(<<"Make room password protected">>, - <<"muc#roomconfig_passwordprotectedroom">>, - (Config#config.password_protected)), - ?PRIVATEXFIELD(<<"Password">>, - <<"muc#roomconfig_roomsecret">>, - case Config#config.password_protected of - true -> Config#config.password; - false -> <<"">> - end), - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, <<"list-single">>}, - {<<"label">>, - translate:translate(Lang, - <<"Maximum Number of Occupants">>)}, - {<<"var">>, <<"muc#roomconfig_maxusers">>}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, MaxUsersRoomString}]}] - ++ - if is_integer(ServiceMaxUsers) -> []; - true -> - [#xmlel{name = <<"option">>, - attrs = - [{<<"label">>, - translate:translate(Lang, - <<"No limit">>)}], - children = - [#xmlel{name = <<"value">>, - attrs = [], - children = - [{xmlcdata, - <<"none">>}]}]}] - end - ++ - [#xmlel{name = <<"option">>, - attrs = - [{<<"label">>, - jlib:integer_to_binary(N)}], - children = - [#xmlel{name = <<"value">>, - attrs = [], - children = - [{xmlcdata, - jlib:integer_to_binary(N)}]}]} - || N - <- lists:usort([ServiceMaxUsers, - DefaultRoomMaxUsers, - MaxUsersRoomInteger - | ?MAX_USERS_DEFAULT_LIST]), - N =< ServiceMaxUsers]}, - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, <<"list-single">>}, - {<<"label">>, - translate:translate(Lang, - <<"Present real Jabber IDs to">>)}, - {<<"var">>, <<"muc#roomconfig_whois">>}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = - [{xmlcdata, - if Config#config.anonymous -> - <<"moderators">>; - true -> <<"anyone">> - end}]}, - #xmlel{name = <<"option">>, - attrs = - [{<<"label">>, - translate:translate(Lang, - <<"moderators only">>)}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = - [{xmlcdata, - <<"moderators">>}]}]}, - #xmlel{name = <<"option">>, - attrs = - [{<<"label">>, - translate:translate(Lang, - <<"anyone">>)}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = - [{xmlcdata, - <<"anyone">>}]}]}]}, - ?BOOLXFIELD(<<"Make room members-only">>, - <<"muc#roomconfig_membersonly">>, - (Config#config.members_only)), - ?BOOLXFIELD(<<"Make room moderated">>, - <<"muc#roomconfig_moderatedroom">>, - (Config#config.moderated)), - ?BOOLXFIELD(<<"Default users as participants">>, - <<"members_by_default">>, - (Config#config.members_by_default)), - ?BOOLXFIELD(<<"Allow users to change the subject">>, - <<"muc#roomconfig_changesubject">>, - (Config#config.allow_change_subj)), - ?BOOLXFIELD(<<"Allow users to send private messages">>, - <<"allow_private_messages">>, - (Config#config.allow_private_messages)), - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, <<"list-single">>}, - {<<"label">>, - translate:translate(Lang, - <<"Allow visitors to send private messages to">>)}, - {<<"var">>, - <<"allow_private_messages_from_visitors">>}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = - [{xmlcdata, - case - Config#config.allow_private_messages_from_visitors - of - anyone -> <<"anyone">>; - moderators -> <<"moderators">>; - nobody -> <<"nobody">> - end}]}, - #xmlel{name = <<"option">>, - attrs = - [{<<"label">>, - translate:translate(Lang, - <<"nobody">>)}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = - [{xmlcdata, <<"nobody">>}]}]}, - #xmlel{name = <<"option">>, - attrs = - [{<<"label">>, - translate:translate(Lang, - <<"moderators only">>)}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = - [{xmlcdata, - <<"moderators">>}]}]}, - #xmlel{name = <<"option">>, - attrs = - [{<<"label">>, - translate:translate(Lang, - <<"anyone">>)}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = - [{xmlcdata, - <<"anyone">>}]}]}]}, - ?BOOLXFIELD(<<"Allow users to query other users">>, - <<"allow_query_users">>, - (Config#config.allow_query_users)), - ?BOOLXFIELD(<<"Allow users to send invites">>, - <<"muc#roomconfig_allowinvites">>, - (Config#config.allow_user_invites)), - ?BOOLXFIELD(<<"Allow visitors to send status text in " - "presence updates">>, - <<"muc#roomconfig_allowvisitorstatus">>, - (Config#config.allow_visitor_status)), - ?BOOLXFIELD(<<"Allow visitors to change nickname">>, - <<"muc#roomconfig_allowvisitornickchange">>, - (Config#config.allow_visitor_nickchange)), - ?BOOLXFIELD(<<"Allow visitors to send voice requests">>, - <<"muc#roomconfig_allowvoicerequests">>, - (Config#config.allow_voice_requests)), - ?STRINGXFIELD(<<"Minimum interval between voice requests " - "(in seconds)">>, - <<"muc#roomconfig_voicerequestmininterval">>, - (jlib:integer_to_binary(Config#config.voice_request_min_interval)))] + case {Old#config.anonymous, New#config.anonymous} of + {true, false} -> [172]; + {false, true} -> [173]; + _ -> [] + end ++ - case ejabberd_captcha:is_feature_available() of - true -> - [?BOOLXFIELD(<<"Make room CAPTCHA protected">>, - <<"captcha_protected">>, - (Config#config.captcha_protected))]; - false -> [] - end - ++ - [?JIDMULTIXFIELD(<<"Exclude Jabber IDs from CAPTCHA challenge">>, - <<"muc#roomconfig_captcha_whitelist">>, - ((?SETS):to_list(Config#config.captcha_whitelist)))] - ++ - case - mod_muc_log:check_access_log(StateData#state.server_host, - From) - of - allow -> - [?BOOLXFIELD(<<"Enable logging">>, - <<"muc#roomconfig_enablelogging">>, - (Config#config.logging))]; - _ -> [] - end, - {result, - [#xmlel{name = <<"instructions">>, attrs = [], - children = - [{xmlcdata, - translate:translate(Lang, - <<"You need an x:data capable client to " - "configure room">>)}]}, - #xmlel{name = <<"x">>, - attrs = - [{<<"xmlns">>, ?NS_XDATA}, {<<"type">>, <<"form">>}], - children = Res}], - StateData}. - -set_config(XEl, StateData) -> - XData = jlib:parse_xdata_submit(XEl), - case XData of - invalid -> {error, ?ERR_BAD_REQUEST}; - _ -> - case set_xoption(XData, StateData#state.config) of - #config{} = Config -> - Res = change_config(Config, StateData), - {result, _, NSD} = Res, - Type = case {(StateData#state.config)#config.logging, - Config#config.logging} - of - {true, false} -> roomconfig_change_disabledlogging; - {false, true} -> roomconfig_change_enabledlogging; - {_, _} -> roomconfig_change - end, - Users = [{U#user.jid, U#user.nick, U#user.role} - || {_, U} <- (?DICT):to_list(StateData#state.users)], - add_to_log(Type, Users, NSD), - Res; - Err -> Err - end - end. - --define(SET_BOOL_XOPT(Opt, Val), - case Val of - <<"0">> -> - set_xoption(Opts, Config#config{Opt = false}); - <<"false">> -> - set_xoption(Opts, Config#config{Opt = false}); - <<"1">> -> set_xoption(Opts, Config#config{Opt = true}); - <<"true">> -> - set_xoption(Opts, Config#config{Opt = true}); - _ -> {error, ?ERR_BAD_REQUEST} - end). - --define(SET_NAT_XOPT(Opt, Val), - case catch jlib:binary_to_integer(Val) of - I when is_integer(I), I > 0 -> - set_xoption(Opts, Config#config{Opt = I}); - _ -> {error, ?ERR_BAD_REQUEST} - end). - --define(SET_STRING_XOPT(Opt, Val), - set_xoption(Opts, Config#config{Opt = Val})). - --define(SET_JIDMULTI_XOPT(Opt, Vals), - begin - Set = lists:foldl(fun ({U, S, R}, Set1) -> - (?SETS):add_element({U, S, R}, Set1); - (#jid{luser = U, lserver = S, lresource = R}, - Set1) -> - (?SETS):add_element({U, S, R}, Set1); - (_, Set1) -> Set1 - end, - (?SETS):empty(), Vals), - set_xoption(Opts, Config#config{Opt = Set}) - end). - -set_xoption([], Config) -> Config; -set_xoption([{<<"muc#roomconfig_roomname">>, [Val]} - | Opts], - Config) -> - ?SET_STRING_XOPT(title, Val); -set_xoption([{<<"muc#roomconfig_roomdesc">>, [Val]} - | Opts], - Config) -> - ?SET_STRING_XOPT(description, Val); -set_xoption([{<<"muc#roomconfig_changesubject">>, [Val]} - | Opts], - Config) -> - ?SET_BOOL_XOPT(allow_change_subj, Val); -set_xoption([{<<"allow_query_users">>, [Val]} | Opts], - Config) -> - ?SET_BOOL_XOPT(allow_query_users, Val); -set_xoption([{<<"allow_private_messages">>, [Val]} - | Opts], - Config) -> - ?SET_BOOL_XOPT(allow_private_messages, Val); -set_xoption([{<<"allow_private_messages_from_visitors">>, - [Val]} - | Opts], - Config) -> - case Val of - <<"anyone">> -> - ?SET_STRING_XOPT(allow_private_messages_from_visitors, - anyone); - <<"moderators">> -> - ?SET_STRING_XOPT(allow_private_messages_from_visitors, - moderators); - <<"nobody">> -> - ?SET_STRING_XOPT(allow_private_messages_from_visitors, - nobody); - _ -> {error, ?ERR_BAD_REQUEST} - end; -set_xoption([{<<"muc#roomconfig_allowvisitorstatus">>, - [Val]} - | Opts], - Config) -> - ?SET_BOOL_XOPT(allow_visitor_status, Val); -set_xoption([{<<"muc#roomconfig_allowvisitornickchange">>, - [Val]} - | Opts], - Config) -> - ?SET_BOOL_XOPT(allow_visitor_nickchange, Val); -set_xoption([{<<"muc#roomconfig_publicroom">>, [Val]} - | Opts], - Config) -> - ?SET_BOOL_XOPT(public, Val); -set_xoption([{<<"public_list">>, [Val]} | Opts], - Config) -> - ?SET_BOOL_XOPT(public_list, Val); -set_xoption([{<<"muc#roomconfig_persistentroom">>, - [Val]} - | Opts], - Config) -> - ?SET_BOOL_XOPT(persistent, Val); -set_xoption([{<<"muc#roomconfig_moderatedroom">>, [Val]} - | Opts], - Config) -> - ?SET_BOOL_XOPT(moderated, Val); -set_xoption([{<<"members_by_default">>, [Val]} | Opts], - Config) -> - ?SET_BOOL_XOPT(members_by_default, Val); -set_xoption([{<<"muc#roomconfig_membersonly">>, [Val]} - | Opts], - Config) -> - ?SET_BOOL_XOPT(members_only, Val); -set_xoption([{<<"captcha_protected">>, [Val]} | Opts], - Config) -> - ?SET_BOOL_XOPT(captcha_protected, Val); -set_xoption([{<<"muc#roomconfig_allowinvites">>, [Val]} - | Opts], - Config) -> - ?SET_BOOL_XOPT(allow_user_invites, Val); -set_xoption([{<<"muc#roomconfig_passwordprotectedroom">>, - [Val]} - | Opts], - Config) -> - ?SET_BOOL_XOPT(password_protected, Val); -set_xoption([{<<"muc#roomconfig_roomsecret">>, [Val]} - | Opts], - Config) -> - ?SET_STRING_XOPT(password, Val); -set_xoption([{<<"anonymous">>, [Val]} | Opts], - Config) -> - ?SET_BOOL_XOPT(anonymous, Val); -set_xoption([{<<"muc#roomconfig_allowvoicerequests">>, - [Val]} - | Opts], - Config) -> - ?SET_BOOL_XOPT(allow_voice_requests, Val); -set_xoption([{<<"muc#roomconfig_voicerequestmininterval">>, - [Val]} - | Opts], - Config) -> - ?SET_NAT_XOPT(voice_request_min_interval, Val); -set_xoption([{<<"muc#roomconfig_whois">>, [Val]} - | Opts], - Config) -> - case Val of - <<"moderators">> -> - ?SET_BOOL_XOPT(anonymous, - (iolist_to_binary(integer_to_list(1)))); - <<"anyone">> -> - ?SET_BOOL_XOPT(anonymous, - (iolist_to_binary(integer_to_list(0)))); - _ -> {error, ?ERR_BAD_REQUEST} - end; -set_xoption([{<<"muc#roomconfig_maxusers">>, [Val]} - | Opts], - Config) -> - case Val of - <<"none">> -> ?SET_STRING_XOPT(max_users, none); - _ -> ?SET_NAT_XOPT(max_users, Val) - end; -set_xoption([{<<"muc#roomconfig_enablelogging">>, [Val]} - | Opts], - Config) -> - ?SET_BOOL_XOPT(logging, Val); -set_xoption([{<<"muc#roomconfig_captcha_whitelist">>, - Vals} - | Opts], - Config) -> - JIDs = [jlib:string_to_jid(Val) || Val <- Vals], - ?SET_JIDMULTI_XOPT(captcha_whitelist, JIDs); -set_xoption([{<<"FORM_TYPE">>, _} | Opts], Config) -> - set_xoption(Opts, Config); -set_xoption([_ | _Opts], _Config) -> - {error, ?ERR_BAD_REQUEST}. - -change_config(Config, StateData) -> - NSD = StateData#state{config = Config}, - case {(StateData#state.config)#config.persistent, - Config#config.persistent} - of - {_, true} -> - mod_muc:store_room(NSD#state.server_host, - NSD#state.host, NSD#state.room, make_opts(NSD)); - {true, false} -> - mod_muc:forget_room(NSD#state.server_host, - NSD#state.host, NSD#state.room); - {false, false} -> ok - end, - case {(StateData#state.config)#config.members_only, - Config#config.members_only} - of - {false, true} -> - NSD1 = remove_nonmembers(NSD), {result, [], NSD1}; - _ -> {result, [], NSD} - end. - -remove_nonmembers(StateData) -> - lists:foldl(fun ({_LJID, #user{jid = JID}}, SD) -> - Affiliation = get_affiliation(JID, SD), - case Affiliation of - none -> - catch send_kickban_presence(<<"">>, JID, <<"">>, - <<"322">>, SD), - set_role(JID, none, SD); - _ -> SD - end + case Old#config{anonymous = New#config.anonymous, + logging = New#config.logging} of + New -> []; + _ -> [104] end, - StateData, (?DICT):to_list(StateData#state.users)). + if Codes /= [] -> + maps:fold( + fun(_LJID, #user{jid = JID}, _) -> + advertise_entity_capabilities(JID, StateData#state{config = New}) + end, ok, StateData#state.users), + Message = #message{type = groupchat, + id = p1_rand:get_string(), + sub_els = [#muc_user{status_codes = Codes}]}, + send_wrapped_multiple(StateData#state.jid, + get_users_and_subscribers_with_node( + ?NS_MUCSUB_NODES_CONFIG, StateData), + Message, + ?NS_MUCSUB_NODES_CONFIG, + StateData); + true -> + ok + end. -set_opts([], StateData) -> StateData; -set_opts([{Opt, Val} | Opts], StateData) -> +-spec remove_nonmembers(state()) -> state(). +remove_nonmembers(StateData) -> + maps:fold( + fun(_LJID, #user{jid = JID}, SD) -> + Affiliation = get_affiliation(JID, SD), + case Affiliation of + none -> + catch send_kickban_presence(undefined, JID, <<"">>, 322, SD), + set_role(JID, none, SD); + _ -> SD + end + end, StateData, get_users_and_subscribers(StateData)). + +-spec set_opts([{atom(), any()}], state()) -> state(). +set_opts(Opts, StateData) -> + case lists:keytake(persistent, 1, Opts) of + false -> + set_opts2(Opts, StateData); + {value, Tuple, Rest} -> + set_opts2([Tuple | Rest], StateData) + end. + +-spec set_opts2([{atom(), any()}], state()) -> state(). +set_opts2([], StateData) -> + set_vcard_xupdate(StateData); +set_opts2([{vcard, Val} | Opts], StateData) + when is_record(Val, vcard_temp) -> + %% default_room_options is setting a default room vcard + ValRaw = fxml:element_to_binary(xmpp:encode(Val)), + set_opts2([{vcard, ValRaw} | Opts], StateData); +set_opts2([{Opt, Val} | Opts], StateData) -> NSD = case Opt of title -> StateData#state{config = @@ -3821,9 +4119,9 @@ set_opts([{Opt, Val} | Opts], StateData) -> StateData#state{config = (StateData#state.config)#config{allow_query_users = Val}}; - allow_private_messages -> + allowpm -> StateData#state{config = - (StateData#state.config)#config{allow_private_messages + (StateData#state.config)#config{allowpm = Val}}; allow_private_messages_from_visitors -> StateData#state{config = @@ -3881,10 +4179,17 @@ set_opts([{Opt, Val} | Opts], StateData) -> StateData#state{config = (StateData#state.config)#config{anonymous = Val}}; + presence_broadcast -> + StateData#state{config = + (StateData#state.config)#config{presence_broadcast = + Val}}; logging -> StateData#state{config = (StateData#state.config)#config{logging = Val}}; + mam -> + StateData#state{config = + (StateData#state.config)#config{mam = Val}}; captcha_whitelist -> StateData#state{config = (StateData#state.config)#config{captcha_whitelist @@ -3910,205 +4215,713 @@ set_opts([{Opt, Val} | Opts], StateData) -> StateData#state{config = (StateData#state.config)#config{vcard = Val}}; + vcard_xupdate -> + StateData#state{config = + (StateData#state.config)#config{vcard_xupdate = + Val}}; + pubsub -> + StateData#state{config = + (StateData#state.config)#config{pubsub = Val}}; + 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}}; + subscribers -> + MUCSubscribers = + lists:foldl( + fun({JID, Nick, Nodes}, MUCSubs) -> + BareJID = + case JID of + #jid{} -> jid:remove_resource(JID); + _ -> + ?ERROR_MSG("Invalid subscriber JID in set_opts ~p", [JID]), + jid:remove_resource(jid:make(JID)) + end, + muc_subscribers_put( + #subscriber{jid = BareJID, + nick = Nick, + nodes = Nodes}, + MUCSubs) + end, muc_subscribers_new(), Val), + StateData#state{muc_subscribers = MUCSubscribers}; affiliations -> - StateData#state{affiliations = (?DICT):from_list(Val)}; - subject -> StateData#state{subject = Val}; - subject_author -> StateData#state{subject_author = Val}; - _ -> StateData + set_affiliations(maps:from_list(Val), StateData); + roles -> + StateData#state{roles = maps:from_list(Val)}; + subject -> + Subj = if Val == <<"">> -> []; + is_binary(Val) -> [#text{data = Val}]; + is_list(Val) -> Val + end, + StateData#state{subject = Subj}; + subject_author when is_tuple(Val) -> + StateData#state{subject_author = Val}; + subject_author when is_binary(Val) -> % ejabberd 23.04 or older + StateData#state{subject_author = {Val, #jid{}}}; + hats_defs -> + StateData#state{hats_defs = maps:from_list(Val)}; + hats_users -> + StateData#state{hats_users = maps:from_list(Val)}; + hibernation_time -> StateData; + Other -> + ?INFO_MSG("Unknown MUC room option, will be discarded: ~p", [Other]), + StateData end, - set_opts(Opts, NSD). + set_opts2(Opts, NSD). --define(MAKE_CONFIG_OPT(Opt), {Opt, Config#config.Opt}). +-spec set_vcard_xupdate(state()) -> state(). +set_vcard_xupdate(#state{config = + #config{vcard = VCardRaw, + vcard_xupdate = undefined} = Config} = State) + when VCardRaw /= <<"">> -> + case fxml_stream:parse_element(VCardRaw) of + {error, _} -> + State; + El -> + Hash = mod_vcard_xupdate:compute_hash(El), + State#state{config = Config#config{vcard_xupdate = Hash}} + end; +set_vcard_xupdate(State) -> + State. +get_occupant_initial_role(Jid, Affiliation, #state{roles = Roles} = StateData) -> + DefaultRole = get_default_role(Affiliation, StateData), + case (StateData#state.config)#config.moderated of + true -> + get_occupant_stored_role(Jid, Roles, DefaultRole); + false -> + DefaultRole + end. -make_opts(StateData) -> +get_occupant_stored_role(Jid, Roles, DefaultRole) -> + maps:get(jid:split(jid:remove_resource(Jid)), Roles, DefaultRole). + +-define(MAKE_CONFIG_OPT(Opt), + {get_config_opt_name(Opt), element(Opt, Config)}). + +-spec make_opts(state(), boolean()) -> [{atom(), any()}]. +make_opts(StateData, Hibernation) -> Config = StateData#state.config, - [?MAKE_CONFIG_OPT(title), ?MAKE_CONFIG_OPT(description), - ?MAKE_CONFIG_OPT(allow_change_subj), - ?MAKE_CONFIG_OPT(allow_query_users), - ?MAKE_CONFIG_OPT(allow_private_messages), - ?MAKE_CONFIG_OPT(allow_private_messages_from_visitors), - ?MAKE_CONFIG_OPT(allow_visitor_status), - ?MAKE_CONFIG_OPT(allow_visitor_nickchange), - ?MAKE_CONFIG_OPT(public), ?MAKE_CONFIG_OPT(public_list), - ?MAKE_CONFIG_OPT(persistent), - ?MAKE_CONFIG_OPT(moderated), - ?MAKE_CONFIG_OPT(members_by_default), - ?MAKE_CONFIG_OPT(members_only), - ?MAKE_CONFIG_OPT(allow_user_invites), - ?MAKE_CONFIG_OPT(password_protected), - ?MAKE_CONFIG_OPT(captcha_protected), - ?MAKE_CONFIG_OPT(password), ?MAKE_CONFIG_OPT(anonymous), - ?MAKE_CONFIG_OPT(logging), ?MAKE_CONFIG_OPT(max_users), - ?MAKE_CONFIG_OPT(allow_voice_requests), - ?MAKE_CONFIG_OPT(voice_request_min_interval), - ?MAKE_CONFIG_OPT(vcard), + Subscribers = muc_subscribers_fold( + fun(_LJID, Sub, Acc) -> + [{Sub#subscriber.jid, + Sub#subscriber.nick, + Sub#subscriber.nodes}|Acc] + end, [], StateData#state.muc_subscribers), + [?MAKE_CONFIG_OPT(#config.title), ?MAKE_CONFIG_OPT(#config.description), + ?MAKE_CONFIG_OPT(#config.allow_change_subj), + ?MAKE_CONFIG_OPT(#config.allow_query_users), + ?MAKE_CONFIG_OPT(#config.allowpm), + ?MAKE_CONFIG_OPT(#config.allow_private_messages_from_visitors), + ?MAKE_CONFIG_OPT(#config.allow_visitor_status), + ?MAKE_CONFIG_OPT(#config.allow_visitor_nickchange), + ?MAKE_CONFIG_OPT(#config.public), ?MAKE_CONFIG_OPT(#config.public_list), + ?MAKE_CONFIG_OPT(#config.persistent), + ?MAKE_CONFIG_OPT(#config.moderated), + ?MAKE_CONFIG_OPT(#config.members_by_default), + ?MAKE_CONFIG_OPT(#config.members_only), + ?MAKE_CONFIG_OPT(#config.allow_user_invites), + ?MAKE_CONFIG_OPT(#config.password_protected), + ?MAKE_CONFIG_OPT(#config.captcha_protected), + ?MAKE_CONFIG_OPT(#config.password), ?MAKE_CONFIG_OPT(#config.anonymous), + ?MAKE_CONFIG_OPT(#config.logging), ?MAKE_CONFIG_OPT(#config.max_users), + ?MAKE_CONFIG_OPT(#config.allow_voice_requests), + ?MAKE_CONFIG_OPT(#config.allow_subscription), + ?MAKE_CONFIG_OPT(#config.mam), + ?MAKE_CONFIG_OPT(#config.presence_broadcast), + ?MAKE_CONFIG_OPT(#config.voice_request_min_interval), + ?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)}, {affiliations, - (?DICT):to_list(StateData#state.affiliations)}, + maps:to_list(StateData#state.affiliations)}, + {roles, maps:to_list(StateData#state.roles)}, {subject, StateData#state.subject}, - {subject_author, StateData#state.subject_author}]. + {subject_author, StateData#state.subject_author}, + {hats_defs, maps:to_list(StateData#state.hats_defs)}, + {hats_users, maps:to_list(StateData#state.hats_users)}, + {hibernation_time, if Hibernation -> erlang:system_time(microsecond); true -> undefined end}, + {subscribers, Subscribers}]. +expand_opts(CompactOpts) -> + DefConfig = #config{}, + Fields = record_info(fields, config), + {_, Opts1} = + lists:foldl( + fun(Field, {Pos, Opts}) -> + case lists:keyfind(Field, 1, CompactOpts) of + false -> + DefV = element(Pos, DefConfig), + DefVal = case (?SETS):is_set(DefV) of + true -> (?SETS):to_list(DefV); + false -> DefV + end, + {Pos+1, [{Field, DefVal}|Opts]}; + {_, Val} -> + {Pos+1, [{Field, Val}|Opts]} + end + end, {2, []}, Fields), + SubjectAuthor = proplists:get_value(subject_author, CompactOpts, {<<"">>, #jid{}}), + Subject = proplists:get_value(subject, CompactOpts, <<"">>), + Subscribers = proplists:get_value(subscribers, CompactOpts, []), + HibernationTime = proplists:get_value(hibernation_time, CompactOpts, 0), + [{subject, Subject}, + {subject_author, SubjectAuthor}, + {subscribers, Subscribers}, + {hibernation_time, HibernationTime} + | lists:reverse(Opts1)]. + +config_fields() -> + [subject, subject_author, subscribers, hibernate_time | record_info(fields, config)]. + +-spec destroy_room(muc_destroy(), state()) -> {result, undefined, stop}. destroy_room(DEl, StateData) -> - lists:foreach(fun ({_LJID, Info}) -> - Nick = Info#user.nick, - ItemAttrs = [{<<"affiliation">>, <<"none">>}, - {<<"role">>, <<"none">>}], - Packet = #xmlel{name = <<"presence">>, - attrs = - [{<<"type">>, <<"unavailable">>}], - children = - [#xmlel{name = <<"x">>, - attrs = - [{<<"xmlns">>, - ?NS_MUC_USER}], - children = - [#xmlel{name = - <<"item">>, - attrs = - ItemAttrs, - children = - []}, - DEl]}]}, - ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, - Nick), - Info#user.jid, Packet) - end, - (?DICT):to_list(StateData#state.users)), - case (StateData#state.config)#config.persistent of - true -> - mod_muc:forget_room(StateData#state.server_host, - StateData#state.host, StateData#state.room); - false -> ok - end, - {result, [], stop}. + Destroy = DEl#muc_destroy{xmlns = ?NS_MUC_USER}, + maps:fold( + fun(_LJID, Info, _) -> + Nick = Info#user.nick, + Item = #muc_item{affiliation = none, + role = none}, + Packet = #presence{ + type = unavailable, + sub_els = [#muc_user{items = [Item], + destroy = Destroy}]}, + send_wrapped(jid:replace_resource(StateData#state.jid, Nick), + Info#user.jid, Packet, + ?NS_MUCSUB_NODES_CONFIG, StateData) + end, ok, get_users_and_subscribers_with_node( + ?NS_MUCSUB_NODES_CONFIG, StateData)), + forget_room(StateData), + {result, undefined, stop}. + +-spec forget_room(state()) -> state(). +forget_room(StateData) -> + mod_muc:forget_room(StateData#state.server_host, + StateData#state.host, + StateData#state.room), + StateData. + +-spec maybe_forget_room(state()) -> state(). +maybe_forget_room(StateData) -> + Forget = case (StateData#state.config)#config.persistent of + true -> + true; + _ -> + Mod = gen_mod:db_mod(StateData#state.server_host, mod_muc), + erlang:function_exported(Mod, get_subscribed_rooms, 3) + end, + case Forget of + true -> + forget_room(StateData); + _ -> + StateData + end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Disco --define(FEATURE(Var), - #xmlel{name = <<"feature">>, attrs = [{<<"var">>, Var}], - children = []}). - -define(CONFIG_OPT_TO_FEATURE(Opt, Fiftrue, Fiffalse), case Opt of - true -> ?FEATURE(Fiftrue); - false -> ?FEATURE(Fiffalse) + true -> Fiftrue; + false -> Fiffalse end). -process_iq_disco_info(_From, set, _Lang, _StateData) -> - {error, ?ERR_NOT_ALLOWED}; -process_iq_disco_info(_From, get, Lang, StateData) -> +-spec make_disco_info(jid(), state()) -> disco_info(). +make_disco_info(From, StateData) -> Config = StateData#state.config, - {result, - [#xmlel{name = <<"identity">>, - attrs = - [{<<"category">>, <<"conference">>}, - {<<"type">>, <<"text">>}, - {<<"name">>, get_title(StateData)}], - children = []}, - #xmlel{name = <<"feature">>, - attrs = [{<<"var">>, ?NS_VCARD}], children = []}, - #xmlel{name = <<"feature">>, - attrs = [{<<"var">>, ?NS_MUC}], children = []}, - ?CONFIG_OPT_TO_FEATURE((Config#config.public), - <<"muc_public">>, <<"muc_hidden">>), - ?CONFIG_OPT_TO_FEATURE((Config#config.persistent), - <<"muc_persistent">>, <<"muc_temporary">>), - ?CONFIG_OPT_TO_FEATURE((Config#config.members_only), - <<"muc_membersonly">>, <<"muc_open">>), - ?CONFIG_OPT_TO_FEATURE((Config#config.anonymous), - <<"muc_semianonymous">>, <<"muc_nonanonymous">>), - ?CONFIG_OPT_TO_FEATURE((Config#config.moderated), - <<"muc_moderated">>, <<"muc_unmoderated">>), - ?CONFIG_OPT_TO_FEATURE((Config#config.password_protected), - <<"muc_passwordprotected">>, <<"muc_unsecured">>)] - ++ iq_disco_info_extras(Lang, StateData), - StateData}. + ServerHost = StateData#state.server_host, + AccessRegister = mod_muc_opt:access_register(ServerHost), + Feats = [?NS_VCARD, ?NS_MUC, ?NS_DISCO_INFO, ?NS_DISCO_ITEMS, + ?NS_COMMANDS, + ?NS_MESSAGE_MODERATE_0, ?NS_MESSAGE_MODERATE_1, + ?NS_MESSAGE_RETRACT, + ?CONFIG_OPT_TO_FEATURE((Config#config.public), + <<"muc_public">>, <<"muc_hidden">>), + ?CONFIG_OPT_TO_FEATURE((Config#config.persistent), + <<"muc_persistent">>, <<"muc_temporary">>), + ?CONFIG_OPT_TO_FEATURE((Config#config.members_only), + <<"muc_membersonly">>, <<"muc_open">>), + ?CONFIG_OPT_TO_FEATURE((Config#config.anonymous), + <<"muc_semianonymous">>, <<"muc_nonanonymous">>), + ?CONFIG_OPT_TO_FEATURE((Config#config.moderated), + <<"muc_moderated">>, <<"muc_unmoderated">>), + ?CONFIG_OPT_TO_FEATURE((Config#config.password_protected), + <<"muc_passwordprotected">>, <<"muc_unsecured">>)] + ++ case acl:match_rule(ServerHost, AccessRegister, From) of + allow -> [?NS_REGISTER]; + deny -> [] + end + ++ case Config#config.allow_subscription of + true -> [?NS_MUCSUB]; + false -> [] + end + ++ case Config#config.enable_hats of + true -> [?NS_HATS]; + false -> [] + end + ++ case gen_mod:is_loaded(StateData#state.server_host, mod_muc_occupantid) of + true -> + [?NS_OCCUPANT_ID]; + _ -> + [] + end + ++ case {gen_mod:is_loaded(StateData#state.server_host, mod_mam), + Config#config.mam} of + {true, true} -> + [?NS_MAM_TMP, ?NS_MAM_0, ?NS_MAM_1, ?NS_MAM_2, ?NS_SID_0]; + _ -> + [] + end, + #disco_info{identities = [#identity{category = <<"conference">>, + type = <<"text">>, + name = (StateData#state.config)#config.title}], + features = Feats}. --define(RFIELDT(Type, Var, Val), - #xmlel{name = <<"field">>, - attrs = [{<<"type">>, Type}, {<<"var">>, Var}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, Val}]}]}). +-spec process_iq_disco_info(jid(), iq(), state()) -> + {result, disco_info()} | {error, stanza_error()}. +process_iq_disco_info(_From, #iq{type = set, lang = Lang}, _StateData) -> + Txt = ?T("Value 'set' of 'type' attribute is not allowed"), + {error, xmpp:err_not_allowed(Txt, Lang)}; +process_iq_disco_info(From, #iq{type = get, lang = Lang, + sub_els = [#disco_info{node = <<>>}]}, + StateData) -> + DiscoInfo = make_disco_info(From, StateData), + Extras = iq_disco_info_extras(Lang, StateData, false), + {result, DiscoInfo#disco_info{xdata = [Extras]}}; --define(RFIELD(Label, Var, Val), - #xmlel{name = <<"field">>, - attrs = - [{<<"label">>, translate:translate(Lang, Label)}, - {<<"var">>, Var}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, Val}]}]}). +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; -iq_disco_info_extras(Lang, StateData) -> - Len = (?DICT):size(StateData#state.users), - RoomDescription = - (StateData#state.config)#config.description, - [#xmlel{name = <<"x">>, - attrs = - [{<<"xmlns">>, ?NS_XDATA}, {<<"type">>, <<"result">>}], - children = - [?RFIELDT(<<"hidden">>, <<"FORM_TYPE">>, - <<"http://jabber.org/protocol/muc#roominfo">>), - ?RFIELD(<<"Room description">>, - <<"muc#roominfo_description">>, RoomDescription), - ?RFIELD(<<"Number of occupants">>, - <<"muc#roominfo_occupants">>, - (iolist_to_binary(integer_to_list(Len))))]}]. +process_iq_disco_info(From, #iq{type = get, lang = Lang, + sub_els = [#disco_info{node = Node}]}, + StateData) + when Node == ?MUC_HAT_CREATE_CMD; + Node == ?MUC_HAT_DESTROY_CMD; + Node == ?MUC_HAT_LISTHATS_CMD; + Node == ?MUC_HAT_ASSIGN_CMD; + Node == ?MUC_HAT_UNASSIGN_CMD; + Node == ?MUC_HAT_LISTUSERS_CMD -> + NodeName = case Node of + ?MUC_HAT_CREATE_CMD -> ?T("Create a Hat"); + ?MUC_HAT_DESTROY_CMD -> ?T("Destroy a Hat"); + ?MUC_HAT_LISTHATS_CMD -> ?T("List Hats"); + ?MUC_HAT_ASSIGN_CMD -> ?T("Assign a Hat to a User"); + ?MUC_HAT_UNASSIGN_CMD -> ?T("Remove a Hat from a User"); + ?MUC_HAT_LISTUSERS_CMD -> ?T("List Users and their Hats") + end, -process_iq_disco_items(_From, set, _Lang, _StateData) -> - {error, ?ERR_NOT_ALLOWED}; -process_iq_disco_items(From, get, _Lang, 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, NodeName)}], + 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 + true = mod_caps:is_valid_node(Node), + DiscoInfo = make_disco_info(From, StateData), + Extras = iq_disco_info_extras(Lang, StateData, true), + DiscoInfo1 = DiscoInfo#disco_info{xdata = [Extras]}, + Hash = mod_caps:compute_disco_hash(DiscoInfo1, sha), + Node = <<(ejabberd_config:get_uri())/binary, $#, Hash/binary>>, + {result, DiscoInfo1#disco_info{node = Node}} + catch _:{badmatch, _} -> + Txt = ?T("Invalid node name"), + {error, xmpp:err_item_not_found(Txt, Lang)} + end. + +-spec iq_disco_info_extras(binary(), state(), boolean()) -> xdata(). +iq_disco_info_extras(Lang, StateData, Static) -> + Config = StateData#state.config, + Fs1 = [{roomname, Config#config.title}, + {description, Config#config.description}, + {changesubject, Config#config.allow_change_subj}, + {allowinvites, Config#config.allow_user_invites}, + {allow_query_users, Config#config.allow_query_users}, + {allowpm, Config#config.allowpm}, + {lang, Config#config.lang}], + Fs2 = case Config#config.pubsub of + Node when is_binary(Node), Node /= <<"">> -> + [{pubsub, Node}|Fs1]; + _ -> + Fs1 + end, + Fs3 = case Static of + false -> + [{occupants, maps:size(StateData#state.nicks)}|Fs2]; + true -> + Fs2 + end, + Fs4 = case Config#config.logging of + true -> + case ejabberd_hooks:run_fold(muc_log_get_url, + StateData#state.server_host, + error, + [StateData]) of + {ok, URL} -> + [{logs, URL}|Fs3]; + error -> + Fs3 + end; + false -> + Fs3 + end, + Fs5 = case (StateData#state.config)#config.vcard_xupdate of + Hash when is_binary(Hash) -> + [{avatarhash, [Hash]} | Fs4]; + _ -> + Fs4 + end, + Fs6 = ejabberd_hooks:run_fold(muc_disco_info_extras, + StateData#state.server_host, + Fs5, + [StateData]), + Fs7 = case (StateData#state.config)#config.enable_hats of + true -> + HatsHash = get_hats_hash(StateData), + [{'hats#hash', [HatsHash]} | Fs6]; + false -> + Fs6 + end, + #xdata{type = result, + fields = muc_roominfo:encode(Fs7, Lang)}. + +-spec process_iq_disco_items(jid(), iq(), state()) -> + {error, stanza_error()} | {result, disco_items()}. +process_iq_disco_items(_From, #iq{type = set, lang = Lang}, _StateData) -> + Txt = ?T("Value 'set' of 'type' attribute is not allowed"), + {error, xmpp:err_not_allowed(Txt, Lang)}; +process_iq_disco_items(From, #iq{type = get, sub_els = [#disco_items{node = <<>>}]}, + StateData) -> case (StateData#state.config)#config.public_list of true -> - {result, get_mucroom_disco_items(StateData), StateData}; + {result, get_mucroom_disco_items(StateData)}; _ -> case is_occupant_or_admin(From, StateData) of true -> - {result, get_mucroom_disco_items(StateData), StateData}; - _ -> {error, ?ERR_FORBIDDEN} + {result, get_mucroom_disco_items(StateData)}; + _ -> + %% If the list of occupants is private, + %% the room MUST return an empty element + %% (http://xmpp.org/extensions/xep-0045.html#disco-roomitems) + {result, #disco_items{}} end - end. - -process_iq_captcha(_From, get, _Lang, _SubEl, - _StateData) -> - {error, ?ERR_NOT_ALLOWED}; -process_iq_captcha(_From, set, _Lang, SubEl, - StateData) -> - case ejabberd_captcha:process_reply(SubEl) of - ok -> {result, [], StateData}; - _ -> {error, ?ERR_NOT_ACCEPTABLE} - end. - -process_iq_vcard(_From, get, _Lang, _SubEl, StateData) -> - #state{config = #config{vcard = VCardRaw}} = StateData, - case xml_stream:parse_element(VCardRaw) of - #xmlel{children = VCardEls} -> - {result, VCardEls, StateData}; - {error, _} -> - {result, [], StateData} end; -process_iq_vcard(From, set, Lang, SubEl, StateData) -> +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_CREATE_CMD, + name = translate:translate( + Lang, ?T("Create a hat"))}, + #disco_item{jid = StateData#state.jid, + node = ?MUC_HAT_DESTROY_CMD, + name = translate:translate( + Lang, ?T("Destroy a hat"))}, + #disco_item{jid = StateData#state.jid, + node = ?MUC_HAT_LISTHATS_CMD, + name = translate:translate( + Lang, ?T("List hats"))}, + #disco_item{jid = StateData#state.jid, + node = ?MUC_HAT_ASSIGN_CMD, + name = translate:translate( + Lang, ?T("Assign a hat to a user"))}, + #disco_item{jid = StateData#state.jid, + node = ?MUC_HAT_UNASSIGN_CMD, + name = translate:translate( + Lang, ?T("Remove a hat from a user"))}, + #disco_item{jid = StateData#state.jid, + node = ?MUC_HAT_LISTUSERS_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_CREATE_CMD; + Node == ?MUC_HAT_DESTROY_CMD; + Node == ?MUC_HAT_LISTHATS_CMD; + Node == ?MUC_HAT_ASSIGN_CMD; + Node == ?MUC_HAT_UNASSIGN_CMD; + Node == ?MUC_HAT_LISTUSERS_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)}. + +-spec process_iq_captcha(jid(), iq(), state()) -> {error, stanza_error()} | + {result, undefined}. +process_iq_captcha(_From, #iq{type = get, lang = Lang}, _StateData) -> + Txt = ?T("Value 'get' of 'type' attribute is not allowed"), + {error, xmpp:err_not_allowed(Txt, Lang)}; +process_iq_captcha(_From, #iq{type = set, lang = Lang, sub_els = [SubEl]}, + _StateData) -> + case ejabberd_captcha:process_reply(SubEl) of + ok -> {result, undefined}; + {error, malformed} -> + Txt = ?T("Incorrect CAPTCHA submit"), + {error, xmpp:err_bad_request(Txt, Lang)}; + _ -> + Txt = ?T("The CAPTCHA verification has failed"), + {error, xmpp:err_not_allowed(Txt, Lang)} + end. + +-spec process_iq_vcard(jid(), iq(), state()) -> + {result, vcard_temp() | xmlel()} | + {result, undefined, state()} | + {error, stanza_error()}. +process_iq_vcard(_From, #iq{type = get}, StateData) -> + #state{config = #config{vcard = VCardRaw}} = StateData, + case fxml_stream:parse_element(VCardRaw) of + #xmlel{} = VCard -> + {result, VCard}; + {error, _} -> + {error, xmpp:err_item_not_found()} + end; +process_iq_vcard(From, #iq{type = set, lang = Lang, sub_els = [Pkt]}, + StateData) -> case get_affiliation(From, StateData) of owner -> - VCardRaw = xml:element_to_binary(SubEl), + SubEl = xmpp:encode(Pkt), + VCardRaw = fxml:element_to_binary(SubEl), + Hash = mod_vcard_xupdate:compute_hash(SubEl), Config = StateData#state.config, - NewConfig = Config#config{vcard = VCardRaw}, + NewConfig = Config#config{vcard = VCardRaw, vcard_xupdate = Hash}, change_config(NewConfig, StateData); _ -> - ErrText = <<"Owner privileges required">>, - {error, ?ERRT_FORBIDDEN(Lang, ErrText)} + ErrText = ?T("Owner privileges required"), + {error, xmpp:err_forbidden(ErrText, Lang)} end. +-spec process_iq_mucsub(jid(), iq(), state()) -> + {error, stanza_error()} | + {result, undefined | muc_subscribe() | muc_subscriptions(), stop | state()} | + {ignore, state()}. +process_iq_mucsub(_From, #iq{type = set, lang = Lang, + sub_els = [#muc_subscribe{}]}, + #state{just_created = Just, config = #config{allow_subscription = false}}) when Just /= true -> + {error, xmpp:err_not_allowed(?T("Subscriptions are not allowed"), Lang)}; +process_iq_mucsub(From, + #iq{type = set, lang = Lang, + sub_els = [#muc_subscribe{jid = #jid{} = SubJid} = Mucsub]}, + StateData) -> + FAffiliation = get_affiliation(From, StateData), + FRole = get_role(From, StateData), + if FRole == moderator; FAffiliation == owner; FAffiliation == admin -> + process_iq_mucsub(SubJid, + #iq{type = set, lang = Lang, + sub_els = [Mucsub#muc_subscribe{jid = undefined}]}, + StateData); + true -> + Txt = ?T("Moderator privileges required"), + {error, xmpp:err_forbidden(Txt, Lang)} + end; +process_iq_mucsub(From, + #iq{type = set, lang = Lang, + sub_els = [#muc_subscribe{nick = Nick}]} = Packet, + StateData) -> + LBareJID = jid:tolower(jid:remove_resource(From)), + try muc_subscribers_get(LBareJID, StateData#state.muc_subscribers) of + #subscriber{nick = Nick1} when Nick1 /= Nick -> + Nodes = get_subscription_nodes(Packet), + case nick_collision(From, Nick, StateData) of + true -> + ErrText = ?T("That nickname is already in use by another occupant"), + {error, xmpp:err_conflict(ErrText, Lang)}; + false -> + case mod_muc:can_use_nick(StateData#state.server_host, + jid:encode(StateData#state.jid), + From, Nick) of + false -> + Err = case Nick of + <<>> -> + xmpp:err_jid_malformed( + ?T("Nickname can't be empty"), + Lang); + _ -> + xmpp:err_conflict( + ?T("That nickname is registered" + " by another person"), Lang) + end, + {error, Err}; + true -> + NewStateData = + set_subscriber(From, Nick, Nodes, StateData), + {result, subscribe_result(Packet), NewStateData} + end + end; + #subscriber{} -> + Nodes = get_subscription_nodes(Packet), + NewStateData = set_subscriber(From, Nick, Nodes, StateData), + {result, subscribe_result(Packet), NewStateData} + catch _:{badkey, _} -> + SD2 = StateData#state{config = (StateData#state.config)#config{allow_subscription = true}}, + add_new_user(From, Nick, Packet, SD2) + end; +process_iq_mucsub(From, #iq{type = set, lang = Lang, + sub_els = [#muc_unsubscribe{jid = #jid{} = UnsubJid}]}, + StateData) -> + FAffiliation = get_affiliation(From, StateData), + FRole = get_role(From, StateData), + if FRole == moderator; FAffiliation == owner; FAffiliation == admin -> + process_iq_mucsub(UnsubJid, + #iq{type = set, lang = Lang, + sub_els = [#muc_unsubscribe{jid = undefined}]}, + StateData); + true -> + Txt = ?T("Moderator privileges required"), + {error, xmpp:err_forbidden(Txt, Lang)} + end; +process_iq_mucsub(From, #iq{type = set, sub_els = [#muc_unsubscribe{}]}, + #state{room = Room, host = Host, server_host = ServerHost} = StateData) -> + BareJID = jid:remove_resource(From), + LBareJID = jid:tolower(BareJID), + try muc_subscribers_remove_exn(LBareJID, StateData#state.muc_subscribers) of + {MUCSubscribers, #subscriber{nick = Nick}} -> + NewStateData = StateData#state{muc_subscribers = MUCSubscribers}, + store_room(NewStateData, [{del_subscription, LBareJID}]), + Packet1a = #message{ + sub_els = [#ps_event{ + items = #ps_items{ + node = ?NS_MUCSUB_NODES_SUBSCRIBERS, + items = [#ps_item{ + id = p1_rand:get_string(), + sub_els = [#muc_unsubscribe{jid = BareJID, nick = Nick}]}]}}]}, + Packet1b = #message{ + sub_els = [#ps_event{ + items = #ps_items{ + node = ?NS_MUCSUB_NODES_SUBSCRIBERS, + items = [#ps_item{ + id = p1_rand:get_string(), + sub_els = [#muc_unsubscribe{nick = Nick}]}]}}]}, + {Packet2a, Packet2b} = ejabberd_hooks:run_fold(muc_unsubscribed, ServerHost, {Packet1a, Packet1b}, + [ServerHost, Room, Host, BareJID, StateData]), + send_subscriptions_change_notifications(Packet2a, Packet2b, StateData), + NewStateData2 = case close_room_if_temporary_and_empty(NewStateData) of + {stop, normal, _} -> stop; + {next_state, normal_state, SD} -> SD + end, + {result, undefined, NewStateData2} + catch _:{badkey, _} -> + {result, undefined, StateData} + end; +process_iq_mucsub(From, #iq{type = get, lang = Lang, + sub_els = [#muc_subscriptions{}]}, + StateData) -> + FAffiliation = get_affiliation(From, StateData), + FRole = get_role(From, StateData), + IsModerator = FRole == moderator orelse FAffiliation == owner orelse + FAffiliation == admin, + case IsModerator orelse is_subscriber(From, StateData) of + true -> + ShowJid = IsModerator orelse + (StateData#state.config)#config.anonymous == false, + Subs = muc_subscribers_fold( + fun(_, #subscriber{jid = J, nick = N, nodes = Nodes}, Acc) -> + case ShowJid of + true -> + [#muc_subscription{jid = J, nick = N, events = Nodes}|Acc]; + _ -> + [#muc_subscription{nick = N, events = Nodes}|Acc] + end + end, [], StateData#state.muc_subscribers), + {result, #muc_subscriptions{list = Subs}, StateData}; + _ -> + Txt = ?T("Moderator privileges required"), + {error, xmpp:err_forbidden(Txt, Lang)} + end; +process_iq_mucsub(_From, #iq{type = get, lang = Lang}, _StateData) -> + Txt = ?T("Value 'get' of 'type' attribute is not allowed"), + {error, xmpp:err_bad_request(Txt, Lang)}. + +-spec remove_subscriptions(state()) -> state(). +remove_subscriptions(StateData) -> + if not (StateData#state.config)#config.allow_subscription -> + StateData#state{muc_subscribers = muc_subscribers_new()}; + true -> + StateData + end. + +-spec get_subscription_nodes(stanza()) -> [binary()]. +get_subscription_nodes(#iq{sub_els = [#muc_subscribe{events = Nodes}]}) -> + lists:filter( + fun(Node) -> + lists:member(Node, [?NS_MUCSUB_NODES_PRESENCE, + ?NS_MUCSUB_NODES_MESSAGES, + ?NS_MUCSUB_NODES_AFFILIATIONS, + ?NS_MUCSUB_NODES_SUBJECT, + ?NS_MUCSUB_NODES_CONFIG, + ?NS_MUCSUB_NODES_PARTICIPANTS, + ?NS_MUCSUB_NODES_SUBSCRIBERS]) + end, Nodes); +get_subscription_nodes(_) -> + []. + +-spec subscribe_result(iq()) -> muc_subscribe(). +subscribe_result(#iq{sub_els = [#muc_subscribe{nick = Nick}]} = Packet) -> + #muc_subscribe{nick = Nick, events = get_subscription_nodes(Packet)}. + +-spec get_title(state()) -> binary(). get_title(StateData) -> case (StateData#state.config)#config.title of <<"">> -> StateData#state.room; Name -> Name end. +-spec get_roomdesc_reply(jid(), state(), binary()) -> {item, binary()} | false. get_roomdesc_reply(JID, StateData, Tail) -> IsOccupantOrAdmin = is_occupant_or_admin(JID, StateData), @@ -4122,405 +4935,1061 @@ get_roomdesc_reply(JID, StateData, Tail) -> true -> false end. +-spec get_roomdesc_tail(state(), binary()) -> binary(). get_roomdesc_tail(StateData, Lang) -> Desc = case (StateData#state.config)#config.public of true -> <<"">>; - _ -> translate:translate(Lang, <<"private, ">>) + _ -> translate:translate(Lang, ?T("private, ")) end, - Len = (?DICT):fold(fun (_, _, Acc) -> Acc + 1 end, 0, - StateData#state.users), - <<" (", Desc/binary, - (iolist_to_binary(integer_to_list(Len)))/binary, ")">>. + Len = maps:size(StateData#state.nicks), + <<" (", Desc/binary, (integer_to_binary(Len))/binary, ")">>. +-spec get_mucroom_disco_items(state()) -> disco_items(). get_mucroom_disco_items(StateData) -> - lists:map(fun ({_LJID, Info}) -> - Nick = Info#user.nick, - #xmlel{name = <<"item">>, - attrs = - [{<<"jid">>, - jlib:jid_to_string({StateData#state.room, - StateData#state.host, - Nick})}, - {<<"name">>, Nick}], - children = []} - end, - (?DICT):to_list(StateData#state.users)). + Items = maps:fold( + fun(Nick, _, Acc) -> + [#disco_item{jid = jid:make(StateData#state.room, + StateData#state.host, + Nick), + name = Nick}|Acc] + end, [], StateData#state.nicks), + #disco_items{items = Items}. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% Hats + +%% @format-begin + +-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})}; + {Node, execute} + when Node == ?MUC_HAT_CREATE_CMD; + Node == ?MUC_HAT_DESTROY_CMD; + Node == ?MUC_HAT_LISTHATS_CMD; + Node == ?MUC_HAT_ASSIGN_CMD; + Node == ?MUC_HAT_UNASSIGN_CMD; + Node == ?MUC_HAT_LISTUSERS_CMD -> + {Status, Form} = process_iq_adhoc_hats(Node, StateData, Lang), + {result, + xmpp_util:make_adhoc_response(Request, + #adhoc_command{status = Status, xdata = Form})}; + {Node, complete} + when XData /= undefined andalso Node == ?MUC_HAT_CREATE_CMD; + Node == ?MUC_HAT_DESTROY_CMD; + Node == ?MUC_HAT_ASSIGN_CMD; + Node == ?MUC_HAT_UNASSIGN_CMD -> + case process_iq_adhoc_hats_complete(Node, XData, StateData, Lang) of + {ok, NewStateData} -> + {result, + xmpp_util:make_adhoc_response(Request, + #adhoc_command{status = completed}), + NewStateData}; + {error, XmlElement} -> + {error, XmlElement}; + error -> + {error, xmpp:err_bad_request()} + end; + {Node, complete} + when Node == ?MUC_HAT_CREATE_CMD; + Node == ?MUC_HAT_DESTROY_CMD; + Node == ?MUC_HAT_ASSIGN_CMD; + Node == ?MUC_HAT_UNASSIGN_CMD -> + {error, xmpp:err_bad_request()}; + {Node, _} + when Node == ?MUC_HAT_CREATE_CMD; + Node == ?MUC_HAT_DESTROY_CMD; + Node == ?MUC_HAT_LISTHATS_CMD; + Node == ?MUC_HAT_ASSIGN_CMD; + Node == ?MUC_HAT_UNASSIGN_CMD; + Node == ?MUC_HAT_LISTUSERS_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. + +process_iq_adhoc_hats(?MUC_HAT_LISTHATS_CMD, StateData, Lang) -> + Hats = get_defined_hats(StateData), + Items = + lists:map(fun({URI, Title, Hue}) -> + [#xdata_field{var = <<"hats#uri">>, values = [URI]}, + #xdata_field{var = <<"hats#title">>, values = [Title]}, + #xdata_field{var = <<"hats#hue">>, values = [Hue]}] + end, + Hats), + Form = + #xdata{title = translate:translate(Lang, ?T("Hats List")), + type = result, + reported = + [#xdata_field{label = translate:translate(Lang, ?T("Hat URI")), + var = <<"hats#uri">>}, + #xdata_field{label = translate:translate(Lang, ?T("Hat Title")), + var = <<"hats#title">>}, + #xdata_field{label = translate:translate(Lang, ?T("Hat Hue")), + var = <<"hats#hue">>}], + items = Items}, + {completed, Form}; +process_iq_adhoc_hats(?MUC_HAT_CREATE_CMD, _StateData, Lang) -> + Form = + #xdata{title = translate:translate(Lang, ?T("Create a hat")), + type = form, + fields = + [#xdata_field{type = 'text-single', + label = translate:translate(Lang, ?T("Hat URI")), + required = true, + var = <<"hats#uri">>}, + #xdata_field{type = 'text-single', + label = translate:translate(Lang, ?T("Hat Title")), + required = true, + var = <<"hats#title">>}, + #xdata_field{type = 'text-single', + label = translate:translate(Lang, ?T("Hat Hue")), + var = <<"hats#hue">>}]}, + {executing, Form}; +process_iq_adhoc_hats(?MUC_HAT_DESTROY_CMD, _StateData, Lang) -> + Form = + #xdata{title = translate:translate(Lang, ?T("Destroy a hat")), + type = form, + fields = + [#xdata_field{type = 'text-single', + label = translate:translate(Lang, ?T("Hat URI")), + required = true, + var = <<"hat">>}]}, + {executing, Form}; +process_iq_adhoc_hats(?MUC_HAT_ASSIGN_CMD, StateData, Lang) -> + Hats = get_defined_hats(StateData), + Options = + [#xdata_option{label = Title, value = Uri} + || {Uri, Title, _Hue} <- lists:keysort(2, Hats)], + Form = + #xdata{title = translate:translate(Lang, ?T("Assign a hat to a user")), + type = form, + fields = + [#xdata_field{type = 'jid-single', + label = translate:translate(Lang, ?T("Jabber ID")), + required = true, + var = <<"hats#jid">>}, + #xdata_field{type = 'list-single', + label = translate:translate(Lang, ?T("The role")), + var = <<"hat">>, + options = Options}]}, + {executing, Form}; +process_iq_adhoc_hats(?MUC_HAT_UNASSIGN_CMD, StateData, Lang) -> + Hats = get_defined_hats(StateData), + Options = + [#xdata_option{label = Title, value = Uri} + || {Uri, Title, _Hue} <- lists:keysort(2, Hats)], + 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 = <<"hats#jid">>}, + #xdata_field{type = 'list-single', + label = translate:translate(Lang, ?T("The role")), + var = <<"hat">>, + options = Options}]}, + {executing, Form}; +process_iq_adhoc_hats(?MUC_HAT_LISTUSERS_CMD, StateData, Lang) -> + Hats = get_assigned_hats(StateData), + Items = + lists:map(fun({JID, URI}) -> + {URI, Title, Hue} = get_hat_details(URI, StateData), + [#xdata_field{var = <<"hats#jid">>, values = [jid:encode(JID)]}, + #xdata_field{var = <<"hats#uri">>, values = [URI]}, + #xdata_field{var = <<"hats#title">>, values = [Title]}, + #xdata_field{var = <<"hats#hue">>, values = [Hue]}] + 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 = <<"hats#jid">>}, + #xdata_field{label = translate:translate(Lang, ?T("Hat URI")), + var = <<"hats#uri">>}, + #xdata_field{label = translate:translate(Lang, ?T("Hat Title")), + var = <<"hats#title">>}, + #xdata_field{label = translate:translate(Lang, ?T("Hat Hue")), + var = <<"hats#hue">>}], + items = Items}, + {completed, Form}; +process_iq_adhoc_hats(_, _, _) -> + {executing, aaa}. + +process_iq_adhoc_hats_complete(?MUC_HAT_CREATE_CMD, XData, StateData, _Lang) -> + URI = try + hd(xmpp_util:get_xdata_values(<<"hats#uri">>, XData)) + catch + _:_ -> + error + end, + Title = + case xmpp_util:get_xdata_values(<<"hats#title">>, XData) of + [] -> + <<"">>; + [T] -> + T + end, + Hue = try + hd(xmpp_util:get_xdata_values(<<"hats#hue">>, XData)) + catch + _:_ -> + error + end, + if (Title /= error) and (URI /= error) -> + {ok, AffectedJids, NewStateData} = create_hat(URI, Title, Hue, StateData), + store_room(NewStateData), + broadcast_hats_change(NewStateData), + [send_update_presence(AJid, NewStateData, StateData) || AJid <- AffectedJids], + {ok, NewStateData}; + true -> + error + end; +process_iq_adhoc_hats_complete(?MUC_HAT_DESTROY_CMD, XData, StateData, _Lang) -> + URI = try + hd(xmpp_util:get_xdata_values(<<"hat">>, XData)) + catch + _:_ -> + error + end, + if URI /= error -> + {ok, AffectedJids, NewStateData} = destroy_hat(URI, StateData), + store_room(NewStateData), + broadcast_hats_change(NewStateData), + [send_update_presence(AJid, NewStateData, StateData) || AJid <- AffectedJids], + {ok, NewStateData}; + true -> + error + end; +process_iq_adhoc_hats_complete(?MUC_HAT_ASSIGN_CMD, XData, StateData, Lang) -> + JID = try + jid:decode(hd(xmpp_util:get_xdata_values(<<"hats#jid">>, XData))) + catch + _:_ -> + error + end, + URI = try + hd(xmpp_util:get_xdata_values(<<"hat">>, XData)) + catch + _:_ -> + error + end, + if (JID /= error) and (URI /= error) -> + case assign_hat(JID, URI, StateData) of + {ok, NewStateData} -> + store_room(NewStateData), + send_update_presence(JID, NewStateData, StateData), + {ok, NewStateData}; + {error, size_limit} -> + Txt = ?T("Hats limit exceeded"), + {error, xmpp:err_not_allowed(Txt, Lang)} + end; + true -> + error + end; +process_iq_adhoc_hats_complete(?MUC_HAT_UNASSIGN_CMD, XData, StateData, _Lang) -> + JID = try + jid:decode(hd(xmpp_util:get_xdata_values(<<"hats#jid">>, XData))) + catch + _:_ -> + error + end, + URI = try + hd(xmpp_util:get_xdata_values(<<"hat">>, XData)) + catch + _:_ -> + error + end, + if (JID /= error) and (URI /= error) -> + {ok, NewStateData} = unassign_hat(JID, URI, StateData), + store_room(NewStateData), + send_update_presence(JID, NewStateData, StateData), + {ok, NewStateData}; + true -> + error + end. + +%% TODO +++ clean +create_hat(URI, Title, Hue, #state{hats_defs = Hats, hats_users = Users} = StateData) -> + Hats2 = maps:put(URI, {Title, Hue}, Hats), + + IsUpdate = + case maps:find(URI, Hats) of + {ok, {OldTitle, OldHue}} -> + (OldTitle /= Title) or (OldHue /= Hue); + error -> + false + end, + + AffectedJids = + case IsUpdate of + true -> + maps:fold(fun(Jid, AssignedHatsUris, ChangedAcc) -> + case lists:member(URI, AssignedHatsUris) of + false -> + ChangedAcc; + true -> + [Jid | ChangedAcc] + end + end, + [], + Users); + false -> + [] + end, + {ok, AffectedJids, StateData#state{hats_defs = Hats2}}. + +destroy_hat(URI, #state{hats_defs = Hats, hats_users = Users} = StateData) -> + Hats2 = maps:remove(URI, Hats), + {AffectedJids, Users2} = + maps:fold(fun(Jid, AssignedHatsUris, {ChangedAcc, UsersAcc}) -> + case AssignedHatsUris -- [URI] of + [] -> + {ChangedAcc, UsersAcc}; + AssignedHatsUris2 -> + {[Jid | ChangedAcc], maps:put(Jid, AssignedHatsUris2, UsersAcc)} + end + end, + {[], maps:new()}, + Users), + {ok, AffectedJids, StateData#state{hats_defs = Hats2, hats_users = Users2}}. + +broadcast_hats_change(StateData) -> + Codes = [104], + Message = + #message{type = groupchat, + id = p1_rand:get_string(), + sub_els = [#muc_user{status_codes = Codes}]}, + send_wrapped_multiple(StateData#state.jid, + get_users_and_subscribers_with_node(?NS_MUCSUB_NODES_CONFIG, StateData), + Message, + ?NS_MUCSUB_NODES_CONFIG, + StateData). + +-spec assign_hat(jid(), binary(), state()) -> {ok, state()} | {error, size_limit}. +assign_hat(JID, URI, StateData) -> + Hats = StateData#state.hats_users, + LJID = + jid:remove_resource( + jid:tolower(JID)), + UserHats = maps:get(LJID, Hats, []), + UserHats2 = lists:umerge([URI], UserHats), + USize = length(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 unassign_hat(jid(), binary(), state()) -> {ok, state()} | {error, size_limit}. +unassign_hat(JID, URI, StateData) -> + Hats = StateData#state.hats_users, + LJID = + jid:remove_resource( + jid:tolower(JID)), + UserHats = maps:get(LJID, Hats, []), + UserHats2 = lists:delete(URI, UserHats), + Hats2 = maps:put(LJID, UserHats2, Hats), + {ok, StateData#state{hats_users = Hats2}}. + +-spec get_defined_hats(state()) -> [{binary(), binary(), binary()}]. +get_defined_hats(StateData) -> + lists:map(fun({Uri, {Title, Hue}}) -> {Uri, Title, Hue} end, + maps:to_list(StateData#state.hats_defs)). + +-spec get_assigned_hats(state()) -> [{jid(), binary()}]. +get_assigned_hats(StateData) -> + lists:flatmap(fun({LJID, H}) -> + JID = jid:make(LJID), + lists:map(fun(URI) -> {JID, URI} end, H) + end, + maps:to_list(StateData#state.hats_users)). + +get_hats_hash(StateData) -> + str:sha( + misc:term_to_base64(get_assigned_hats(StateData))). + +get_hat_details(Uri, StateData) -> + lists:keyfind(Uri, 1, get_defined_hats(StateData)). + +-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 length(UserHats) of + 0 -> + Pres; + _ -> + Items = + lists:map(fun(URI) -> + {URI, Title, Hue} = get_hat_details(URI, StateData), + #muc_hat{uri = URI, + title = Title, + hue = Hue} + end, + UserHats), + xmpp:set_subtag(Pres, #muc_hats{hats = Items}) + end; + false -> + Pres + end. +%% @format-end + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +-spec process_iq_moderate(jid(), iq(), binary(), binary() | undefined, state()) -> + {result, undefined, state()} | + {error, stanza_error()}. +process_iq_moderate(_From, #iq{type = get}, _Id, _Reason, _StateData) -> + {error, xmpp:err_bad_request()}; +process_iq_moderate(From, #iq{type = set, lang = Lang}, Id, Reason, + #state{config = Config, room = Room, host = Host, + jid = JID, server_host = Server} = StateData) -> + FAffiliation = get_affiliation(From, StateData), + FRole = get_role(From, StateData), + IsModerator = FRole == moderator orelse FAffiliation == owner orelse + FAffiliation == admin, + case IsModerator of + false -> + {error, xmpp:err_forbidden( + ?T("Only moderators are allowed to retract messages"), Lang)}; + _ -> + try binary_to_integer(Id) of + StanzaId -> + case Config#config.mam of + true -> + mod_mam:remove_message_from_archive({Room, Host}, Server, StanzaId); + _ -> + ok + end, + By = jid:replace_resource(JID, find_nick_by_jid(From, StateData)), + Mod21 = #message_moderated_21{by = By, + reason = Reason, + sub_els = [#message_retract_30{}]}, + SubEl = [#fasten_apply_to{id = Id, + sub_els = [Mod21]}, + #message_retract{id = Id, + reason = Reason, + moderated = #message_moderated{by = By}}], + Packet0 = #message{type = groupchat, + from = From, + sub_els = SubEl}, + {FromNick, _Role} = get_participant_data(From, StateData), + Packet = ejabberd_hooks:run_fold(muc_filter_message, + StateData#state.server_host, + xmpp:put_meta(Packet0, mam_ignore, true), + [StateData, FromNick]), + send_wrapped_multiple(JID, + get_users_and_subscribers_with_node(?NS_MUCSUB_NODES_MESSAGES, StateData), + Packet, ?NS_MUCSUB_NODES_MESSAGES, StateData), + NSD = add_message_to_history(<<"">>, + StateData#state.jid, Packet, StateData), + {result, undefined, remove_from_history(StanzaId, NSD)} + catch _:_ -> + {error, xmpp:err_bad_request( + ?T("Stanza id is not valid"), Lang)} + end + end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Voice request support -is_voice_request(Els) -> - lists:foldl(fun (#xmlel{name = <<"x">>, attrs = Attrs} = - El, - false) -> - case xml:get_attr_s(<<"xmlns">>, Attrs) of - ?NS_XDATA -> - case jlib:parse_xdata_submit(El) of - [_ | _] = Fields -> - case {lists:keysearch(<<"FORM_TYPE">>, 1, - Fields), - lists:keysearch(<<"muc#role">>, 1, - Fields)} - of - {{value, - {_, - [<<"http://jabber.org/protocol/muc#request">>]}}, - {value, {_, [<<"participant">>]}}} -> - true; - _ -> false - end; - _ -> false - end; - _ -> false - end; - (_, Acc) -> Acc - end, - false, Els). - +-spec prepare_request_form(jid(), binary(), binary()) -> message(). prepare_request_form(Requester, Nick, Lang) -> - #xmlel{name = <<"message">>, - attrs = [{<<"type">>, <<"normal">>}], - children = - [#xmlel{name = <<"x">>, - attrs = - [{<<"xmlns">>, ?NS_XDATA}, {<<"type">>, <<"form">>}], - children = - [#xmlel{name = <<"title">>, attrs = [], - children = - [{xmlcdata, - translate:translate(Lang, - <<"Voice request">>)}]}, - #xmlel{name = <<"instructions">>, attrs = [], - children = - [{xmlcdata, - translate:translate(Lang, - <<"Either approve or decline the voice " - "request.">>)}]}, - #xmlel{name = <<"field">>, - attrs = - [{<<"var">>, <<"FORM_TYPE">>}, - {<<"type">>, <<"hidden">>}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = - [{xmlcdata, - <<"http://jabber.org/protocol/muc#request">>}]}]}, - #xmlel{name = <<"field">>, - attrs = - [{<<"var">>, <<"muc#role">>}, - {<<"type">>, <<"hidden">>}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = - [{xmlcdata, - <<"participant">>}]}]}, - ?STRINGXFIELD(<<"User JID">>, <<"muc#jid">>, - (jlib:jid_to_string(Requester))), - ?STRINGXFIELD(<<"Nickname">>, <<"muc#roomnick">>, - Nick), - ?BOOLXFIELD(<<"Grant voice to this person?">>, - <<"muc#request_allow">>, - (jlib:binary_to_atom(<<"false">>)))]}]}. + Title = translate:translate(Lang, ?T("Voice request")), + Instruction = translate:translate( + Lang, ?T("Either approve or decline the voice request.")), + Fs = muc_request:encode([{role, participant}, + {jid, Requester}, + {roomnick, Nick}, + {request_allow, false}], + Lang), + #message{type = normal, + sub_els = [#xdata{type = form, + title = Title, + instructions = [Instruction], + fields = Fs}]}. -send_voice_request(From, StateData) -> +-spec send_voice_request(jid(), binary(), state()) -> ok. +send_voice_request(From, Lang, StateData) -> Moderators = search_role(moderator, StateData), FromNick = find_nick_by_jid(From, StateData), - lists:foreach(fun ({_, User}) -> - ejabberd_router:route(StateData#state.jid, User#user.jid, - prepare_request_form(From, FromNick, - <<"">>)) - end, - Moderators). - -is_voice_approvement(Els) -> - lists:foldl(fun (#xmlel{name = <<"x">>, attrs = Attrs} = - El, - false) -> - case xml:get_attr_s(<<"xmlns">>, Attrs) of - ?NS_XDATA -> - case jlib:parse_xdata_submit(El) of - [_ | _] = Fs -> - case {lists:keysearch(<<"FORM_TYPE">>, 1, - Fs), - lists:keysearch(<<"muc#role">>, 1, - Fs), - lists:keysearch(<<"muc#request_allow">>, - 1, Fs)} - of - {{value, - {_, - [<<"http://jabber.org/protocol/muc#request">>]}}, - {value, {_, [<<"participant">>]}}, - {value, {_, [Flag]}}} - when Flag == <<"true">>; - Flag == <<"1">> -> - true; - _ -> false - end; - _ -> false - end; - _ -> false - end; - (_, Acc) -> Acc - end, - false, Els). - -extract_jid_from_voice_approvement(Els) -> - lists:foldl(fun (#xmlel{name = <<"x">>} = El, error) -> - Fields = case jlib:parse_xdata_submit(El) of - invalid -> []; - Res -> Res - end, - lists:foldl(fun ({<<"muc#jid">>, [JIDStr]}, error) -> - case jlib:string_to_jid(JIDStr) of - error -> error; - J -> {ok, J} - end; - (_, Acc) -> Acc - end, - error, Fields); - (_, Acc) -> Acc - end, - error, Els). + lists:foreach( + fun({_, User}) -> + ejabberd_router:route( + xmpp:set_from_to( + prepare_request_form(From, FromNick, Lang), + StateData#state.jid, User#user.jid)) + end, Moderators). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Invitation support - -is_invitation(Els) -> - lists:foldl(fun (#xmlel{name = <<"x">>, attrs = Attrs} = - El, - false) -> - case xml:get_attr_s(<<"xmlns">>, Attrs) of - ?NS_MUC_USER -> - case xml:get_subtag(El, <<"invite">>) of - false -> false; - _ -> true - end; - _ -> false - end; - (_, Acc) -> Acc - end, - false, Els). - -check_invitation(From, Els, Lang, StateData) -> +-spec check_invitation(jid(), [muc_invite()], binary(), state()) -> + ok | {error, stanza_error()}. +check_invitation(From, Invitations, Lang, StateData) -> FAffiliation = get_affiliation(From, StateData), - CanInvite = - (StateData#state.config)#config.allow_user_invites - orelse - FAffiliation == admin orelse FAffiliation == owner, - InviteEl = case xml:remove_cdata(Els) of - [#xmlel{name = <<"x">>, children = Els1} = XEl] -> - case xml:get_tag_attr_s(<<"xmlns">>, XEl) of - ?NS_MUC_USER -> ok; - _ -> throw({error, ?ERR_BAD_REQUEST}) - end, - case xml:remove_cdata(Els1) of - [#xmlel{name = <<"invite">>} = InviteEl1] -> InviteEl1; - _ -> throw({error, ?ERR_BAD_REQUEST}) - end; - _ -> throw({error, ?ERR_BAD_REQUEST}) - end, - JID = case - jlib:string_to_jid(xml:get_tag_attr_s(<<"to">>, - InviteEl)) - of - error -> throw({error, ?ERR_JID_MALFORMED}); - JID1 -> JID1 - end, + CanInvite = (StateData#state.config)#config.allow_user_invites orelse + FAffiliation == admin orelse FAffiliation == owner, case CanInvite of - false -> throw({error, ?ERR_NOT_ALLOWED}); - true -> - Reason = xml:get_path_s(InviteEl, - [{elem, <<"reason">>}, cdata]), - ContinueEl = case xml:get_path_s(InviteEl, - [{elem, <<"continue">>}]) - of - <<>> -> []; - Continue1 -> [Continue1] - end, - IEl = [#xmlel{name = <<"invite">>, - attrs = [{<<"from">>, jlib:jid_to_string(From)}], - children = - [#xmlel{name = <<"reason">>, attrs = [], - children = [{xmlcdata, Reason}]}] - ++ ContinueEl}], - PasswdEl = case - (StateData#state.config)#config.password_protected - of - true -> - [#xmlel{name = <<"password">>, attrs = [], - children = - [{xmlcdata, - (StateData#state.config)#config.password}]}]; - _ -> [] - end, - Body = #xmlel{name = <<"body">>, attrs = [], - children = - [{xmlcdata, - iolist_to_binary( - [io_lib:format( - translate:translate( - Lang, - <<"~s invites you to the room ~s">>), - [jlib:jid_to_string(From), - jlib:jid_to_string({StateData#state.room, - StateData#state.host, - <<"">>})]), - - case - (StateData#state.config)#config.password_protected - of - true -> - <<", ", - (translate:translate(Lang, - <<"the password is">>))/binary, - " '", - ((StateData#state.config)#config.password)/binary, - "'">>; - _ -> <<"">> - end - , - case Reason of - <<"">> -> <<"">>; - _ -> <<" (", Reason/binary, ") ">> - end])}]}, - Msg = #xmlel{name = <<"message">>, - attrs = [{<<"type">>, <<"normal">>}], - children = - [#xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_MUC_USER}], - children = IEl ++ PasswdEl}, - #xmlel{name = <<"x">>, - attrs = - [{<<"xmlns">>, ?NS_XCONFERENCE}, - {<<"jid">>, - jlib:jid_to_string({StateData#state.room, - StateData#state.host, - <<"">>})}], - children = [{xmlcdata, Reason}]}, - Body]}, - ejabberd_router:route(StateData#state.jid, JID, Msg), - JID + true -> + case lists:all( + fun(#muc_invite{to = #jid{}}) -> true; + (_) -> false + end, Invitations) of + true -> + ok; + false -> + Txt = ?T("No 'to' attribute found in the invitation"), + {error, xmpp:err_bad_request(Txt, Lang)} + end; + false -> + Txt = ?T("Invitations are not allowed in this conference"), + {error, xmpp:err_not_allowed(Txt, Lang)} end. +-spec route_invitation(jid(), message(), muc_invite(), binary(), state()) -> jid(). +route_invitation(From, Pkt, Invitation, Lang, StateData) -> + #muc_invite{to = JID, reason = Reason} = Invitation, + Invite = Invitation#muc_invite{to = undefined, from = From}, + Password = case (StateData#state.config)#config.password_protected of + true -> + (StateData#state.config)#config.password; + false -> + undefined + end, + XUser = #muc_user{password = Password, invites = [Invite]}, + XConference = #x_conference{jid = jid:make(StateData#state.room, + StateData#state.host), + reason = Reason}, + Body = iolist_to_binary( + [io_lib:format( + translate:translate( + Lang, + ?T("~s invites you to the room ~s")), + [jid:encode(From), + jid:encode({StateData#state.room, StateData#state.host, <<"">>})]), + case (StateData#state.config)#config.password_protected of + true -> + <<", ", + (translate:translate( + Lang, ?T("the password is")))/binary, + " '", + ((StateData#state.config)#config.password)/binary, + "'">>; + _ -> <<"">> + end, + case Reason of + <<"">> -> <<"">>; + _ -> <<" (", Reason/binary, ") ">> + end]), + Msg = #message{from = StateData#state.jid, + to = JID, + type = normal, + body = xmpp:mk_text(Body), + sub_els = [XUser, XConference]}, + Msg2 = ejabberd_hooks:run_fold(muc_invite, + StateData#state.server_host, + Msg, + [StateData#state.jid, StateData#state.config, + From, JID, Reason, Pkt]), + ejabberd_router:route(Msg2), + JID. + %% Handle a message sent to the room by a non-participant. %% If it is a decline, send to the inviter. %% Otherwise, an error message is sent to the sender. -handle_roommessage_from_nonparticipant(Packet, Lang, - StateData, From) -> - case catch check_decline_invitation(Packet) of - {true, Decline_data} -> - send_decline_invitation(Decline_data, - StateData#state.jid, From); - _ -> - send_error_only_occupants(Packet, Lang, - StateData#state.jid, From) +-spec handle_roommessage_from_nonparticipant(message(), state(), jid()) -> ok. +handle_roommessage_from_nonparticipant(Packet, StateData, From) -> + try xmpp:try_subtag(Packet, #muc_user{}) of + #muc_user{decline = #muc_decline{to = #jid{} = To} = Decline} = XUser -> + NewDecline = Decline#muc_decline{to = undefined, from = From}, + NewXUser = XUser#muc_user{decline = NewDecline}, + NewPacket = xmpp:set_subtag(Packet, NewXUser), + ejabberd_router:route( + xmpp:set_from_to(NewPacket, StateData#state.jid, To)); + _ -> + ErrText = ?T("Only occupants are allowed to send messages " + "to the conference"), + Err = xmpp:err_not_acceptable(ErrText, xmpp:get_lang(Packet)), + ejabberd_router:route_error(Packet, Err) + catch _:{xmpp_codec, Why} -> + Txt = xmpp:io_format_error(Why), + Err = xmpp:err_bad_request(Txt, xmpp:get_lang(Packet)), + ejabberd_router:route_error(Packet, Err) end. -%% Check in the packet is a decline. -%% If so, also returns the splitted packet. -%% This function must be catched, -%% because it crashes when the packet is not a decline message. -check_decline_invitation(Packet) -> - #xmlel{name = <<"message">>} = Packet, - XEl = xml:get_subtag(Packet, <<"x">>), - (?NS_MUC_USER) = xml:get_tag_attr_s(<<"xmlns">>, XEl), - DEl = xml:get_subtag(XEl, <<"decline">>), - ToString = xml:get_tag_attr_s(<<"to">>, DEl), - ToJID = jlib:string_to_jid(ToString), - {true, {Packet, XEl, DEl, ToJID}}. - -%% Send the decline to the inviter user. -%% The original stanza must be slightly modified. -send_decline_invitation({Packet, XEl, DEl, ToJID}, - RoomJID, FromJID) -> - FromString = - jlib:jid_to_string(jlib:jid_remove_resource(FromJID)), - #xmlel{name = <<"decline">>, attrs = DAttrs, - children = DEls} = - DEl, - DAttrs2 = lists:keydelete(<<"to">>, 1, DAttrs), - DAttrs3 = [{<<"from">>, FromString} | DAttrs2], - DEl2 = #xmlel{name = <<"decline">>, attrs = DAttrs3, - children = DEls}, - XEl2 = replace_subelement(XEl, DEl2), - Packet2 = replace_subelement(Packet, XEl2), - ejabberd_router:route(RoomJID, ToJID, Packet2). - -%% Given an element and a new subelement, -%% replace the instance of the subelement in element with the new subelement. -replace_subelement(#xmlel{name = Name, attrs = Attrs, - children = SubEls}, - NewSubEl) -> - {_, NameNewSubEl, _, _} = NewSubEl, - SubEls2 = lists:keyreplace(NameNewSubEl, 2, SubEls, NewSubEl), - #xmlel{name = Name, attrs = Attrs, children = SubEls2}. - -send_error_only_occupants(Packet, Lang, RoomJID, From) -> - ErrText = - <<"Only occupants are allowed to send messages " - "to the conference">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)), - ejabberd_router:route(RoomJID, From, Err). - %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Logging add_to_log(Type, Data, StateData) when Type == roomconfig_change_disabledlogging -> - mod_muc_log:add_to_log(StateData#state.server_host, - roomconfig_change, Data, StateData#state.jid, - make_opts(StateData)); + ejabberd_hooks:run(muc_log_add, + StateData#state.server_host, + [StateData#state.server_host, + roomconfig_change, + Data, + StateData#state.jid, + make_opts(StateData, false)]); add_to_log(Type, Data, StateData) -> case (StateData#state.config)#config.logging of true -> - mod_muc_log:add_to_log(StateData#state.server_host, - Type, Data, StateData#state.jid, - make_opts(StateData)); + ejabberd_hooks:run(muc_log_add, + StateData#state.server_host, + [StateData#state.server_host, + Type, + Data, + StateData#state.jid, + make_opts(StateData, false)]); false -> ok end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Users number checking +-spec tab_add_online_user(jid(), state()) -> any(). tab_add_online_user(JID, StateData) -> - {LUser, LServer, LResource} = jlib:jid_tolower(JID), - US = {LUser, LServer}, Room = StateData#state.room, Host = StateData#state.host, - catch ets:insert(muc_online_users, - #muc_online_users{us = US, resource = LResource, - room = Room, host = Host}). + ServerHost = StateData#state.server_host, + ejabberd_hooks:run(join_room, ServerHost, [ServerHost, Room, Host, JID]), + mod_muc:register_online_user(ServerHost, jid:tolower(JID), Room, Host). +-spec tab_remove_online_user(jid(), state()) -> any(). tab_remove_online_user(JID, StateData) -> - {LUser, LServer, LResource} = jlib:jid_tolower(JID), - US = {LUser, LServer}, Room = StateData#state.room, Host = StateData#state.host, - catch ets:delete_object(muc_online_users, - #muc_online_users{us = US, resource = LResource, - room = Room, host = Host}). + ServerHost = StateData#state.server_host, + ejabberd_hooks:run(leave_room, ServerHost, [ServerHost, Room, Host, JID]), + mod_muc:unregister_online_user(ServerHost, jid:tolower(JID), Room, Host). -tab_count_user(JID) -> - {LUser, LServer, _} = jlib:jid_tolower(JID), - US = {LUser, LServer}, - case catch ets:select(muc_online_users, - [{#muc_online_users{us = US, _ = '_'}, [], [[]]}]) - of - Res when is_list(Res) -> length(Res); - _ -> 0 +-spec tab_count_user(jid(), state()) -> non_neg_integer(). +tab_count_user(JID, StateData) -> + ServerHost = StateData#state.server_host, + {LUser, LServer, _} = jid:tolower(JID), + mod_muc:count_online_rooms_by_user(ServerHost, LUser, LServer). + +-spec element_size(stanza()) -> non_neg_integer(). +element_size(El) -> + byte_size(fxml:element_to_binary(xmpp:encode(El, ?NS_CLIENT))). + +-spec store_room(state()) -> ok. +store_room(StateData) -> + store_room(StateData, []). +store_room(StateData, ChangesHints) -> + % Let store persistent rooms or on those backends that have get_subscribed_rooms + Mod = gen_mod:db_mod(StateData#state.server_host, mod_muc), + HasGSR = erlang:function_exported(Mod, get_subscribed_rooms, 3), + case HasGSR of + true -> + ok; + _ -> + erlang:put(muc_subscribers, StateData#state.muc_subscribers#muc_subscribers.subscribers) + end, + ShouldStore = case (StateData#state.config)#config.persistent of + true -> + true; + _ -> + case ChangesHints of + [] -> + false; + _ -> + HasGSR + end + end, + if ShouldStore -> + case erlang:function_exported(Mod, store_changes, 4) of + true when ChangesHints /= [] -> + mod_muc:store_changes( + StateData#state.server_host, + StateData#state.host, StateData#state.room, + ChangesHints); + _ -> + store_room_no_checks(StateData, ChangesHints, false), + ok + end; + true -> + ok end. -element_size(El) -> - byte_size(xml:element_to_binary(El)). +-spec store_room_no_checks(state(), list(), boolean()) -> {atomic, any()}. +store_room_no_checks(StateData, ChangesHints, Hibernation) -> + mod_muc:store_room(StateData#state.server_host, + StateData#state.host, StateData#state.room, + make_opts(StateData, Hibernation), + ChangesHints). + +-spec send_subscriptions_change_notifications(stanza(), stanza(), state()) -> ok. +send_subscriptions_change_notifications(Packet, PacketWithoutJid, State) -> + {WJ, WN} = + maps:fold( + fun(_, #subscriber{jid = JID}, {WithJid, WithNick}) -> + case (State#state.config)#config.anonymous == false orelse + get_role(JID, State) == moderator orelse + get_default_role(get_affiliation(JID, State), State) == moderator of + true -> + {[JID | WithJid], WithNick}; + _ -> + {WithJid, [JID | WithNick]} + end + end, {[], []}, + muc_subscribers_get_by_node(?NS_MUCSUB_NODES_SUBSCRIBERS, + State#state.muc_subscribers)), + if WJ /= [] -> + ejabberd_router_multicast:route_multicast(State#state.jid, State#state.server_host, + WJ, Packet, false); + true -> ok + end, + if WN /= [] -> + ejabberd_router_multicast:route_multicast(State#state.jid, State#state.server_host, + WN, PacketWithoutJid, false); + true -> ok + end. + +-spec send_wrapped(jid(), jid(), stanza(), binary(), state()) -> ok. +send_wrapped(From, To, Packet, Node, State) -> + LTo = jid:tolower(To), + LBareTo = jid:tolower(jid:remove_resource(To)), + IsOffline = case maps:get(LTo, State#state.users, error) of + #user{last_presence = undefined} -> true; + error -> true; + _ -> false + end, + if IsOffline -> + try muc_subscribers_get(LBareTo, State#state.muc_subscribers) of + #subscriber{nodes = Nodes, jid = JID} -> + case lists:member(Node, Nodes) of + true -> + MamEnabled = (State#state.config)#config.mam, + Id = case xmpp:get_subtag(Packet, #stanza_id{by = #jid{}}) of + #stanza_id{id = Id2} -> + Id2; + _ -> + p1_rand:get_string() + end, + NewPacket = wrap(From, JID, Packet, Node, Id), + NewPacket2 = xmpp:put_meta(NewPacket, in_muc_mam, MamEnabled), + ejabberd_router:route( + xmpp:set_from_to(NewPacket2, State#state.jid, JID)); + false -> + ok + end + catch _:{badkey, _} -> + ok + end; + true -> + case Packet of + #presence{type = unavailable} -> + case xmpp:get_subtag(Packet, #muc_user{}) of + #muc_user{destroy = Destroy, + status_codes = Codes} -> + case Destroy /= undefined orelse + (lists:member(110,Codes) andalso + not lists:member(303, Codes)) of + true -> + ejabberd_router:route( + #presence{from = State#state.jid, to = To, + id = p1_rand:get_string(), + type = unavailable}); + false -> + ok + end; + _ -> + false + end; + _ -> + ok + end, + ejabberd_router:route(xmpp:set_from_to(Packet, From, To)) + end. + +-spec wrap(jid(), undefined | jid(), stanza(), binary(), binary()) -> message(). +wrap(From, To, Packet, Node, Id) -> + El = xmpp:set_from_to(Packet, From, To), + #message{ + id = Id, + sub_els = [#ps_event{ + items = #ps_items{ + node = Node, + items = [#ps_item{ + id = Id, + sub_els = [El]}]}}]}. + +-spec send_wrapped_multiple(jid(), users(), stanza(), binary(), state()) -> ok. +send_wrapped_multiple(From, Users, Packet, Node, State) -> + {Dir, DirSub, Wra} = + maps:fold( + fun(_, #user{jid = To, last_presence = LP}, {Direct, DirectSub, Wrapped} = Res) -> + IsOffline = LP == undefined, + LBareTo = jid:tolower(jid:remove_resource(To)), + IsSub = case muc_subscribers_find(LBareTo, State#state.muc_subscribers) of + {ok, #subscriber{nodes = Nodes}} -> + lists:member(Node, Nodes); + _ -> false + end, + if + IsOffline -> + if + IsSub -> + {Direct, DirectSub, [To | Wrapped]}; + true -> + Res + end; + IsSub -> + {Direct, [To | DirectSub], Wrapped}; + true -> + {[To | Direct], DirectSub, Wrapped} + end + end, + {[], [], []}, + Users), + DirAll = Dir ++ DirSub, + case DirAll of + [] -> ok; + _ -> + case Packet of + #presence{type = unavailable} -> + case xmpp:get_subtag(Packet, #muc_user{}) of + #muc_user{destroy = Destroy, + status_codes = Codes} -> + case Destroy /= undefined orelse + (lists:member(110,Codes) andalso + not lists:member(303, Codes)) of + true -> + ejabberd_router_multicast:route_multicast( + From, + State#state.server_host, + DirAll, + #presence{id = p1_rand:get_string(), + type = unavailable}, false); + false -> + ok + end; + _ -> + false + end; + _ -> + ok + end, + if + Dir /= [] -> + ejabberd_router_multicast:route_multicast(From, + State#state.server_host, + Dir, + Packet, + false); + true -> + ok + end, + if + DirSub /= [] -> + PacketSub = xmpp:put_meta(Packet, is_muc_subscriber, true), + ejabberd_router_multicast:route_multicast(From, + State#state.server_host, + DirSub, + PacketSub, + false); + true -> + ok + end + end, + case Wra of + [] -> ok; + _ -> + MamEnabled = (State#state.config)#config.mam, + Id = case xmpp:get_subtag(Packet, #stanza_id{by = #jid{}}) of + #stanza_id{id = Id2} -> + Id2; + _ -> + p1_rand:get_string() + end, + NewPacket = wrap(From, undefined, Packet, Node, Id), + NewPacket2 = xmpp:put_meta(NewPacket, in_muc_mam, MamEnabled), + ejabberd_router_multicast:route_multicast(State#state.jid, State#state.server_host, + Wra, NewPacket2, true) + end. + +%%%---------------------------------------------------------------------- +%%% #muc_subscribers API +%%%---------------------------------------------------------------------- + +-spec muc_subscribers_new() -> #muc_subscribers{}. +muc_subscribers_new() -> + #muc_subscribers{}. + +-spec muc_subscribers_get(ljid(), #muc_subscribers{}) -> #subscriber{}. +muc_subscribers_get({_, _, _} = LJID, MUCSubscribers) -> + maps:get(LJID, MUCSubscribers#muc_subscribers.subscribers). + +-spec muc_subscribers_find(ljid(), #muc_subscribers{}) -> + {ok, #subscriber{}} | error. +muc_subscribers_find({_, _, _} = LJID, MUCSubscribers) -> + maps:find(LJID, MUCSubscribers#muc_subscribers.subscribers). + +-spec muc_subscribers_is_key(ljid(), #muc_subscribers{}) -> boolean(). +muc_subscribers_is_key({_, _, _} = LJID, MUCSubscribers) -> + maps:is_key(LJID, MUCSubscribers#muc_subscribers.subscribers). + +-spec muc_subscribers_size(#muc_subscribers{}) -> integer(). +muc_subscribers_size(MUCSubscribers) -> + maps:size(MUCSubscribers#muc_subscribers.subscribers). + +-spec muc_subscribers_fold(Fun, Acc, #muc_subscribers{}) -> Acc when + Fun :: fun((ljid(), #subscriber{}, Acc) -> Acc). +muc_subscribers_fold(Fun, Init, MUCSubscribers) -> + maps:fold(Fun, Init, MUCSubscribers#muc_subscribers.subscribers). + +-spec muc_subscribers_get_by_nick(binary(), #muc_subscribers{}) -> [#subscriber{}]. +muc_subscribers_get_by_nick(Nick, MUCSubscribers) -> + maps:get(Nick, MUCSubscribers#muc_subscribers.subscriber_nicks, []). + +-spec muc_subscribers_get_by_node(binary(), #muc_subscribers{}) -> subscribers(). +muc_subscribers_get_by_node(Node, MUCSubscribers) -> + maps:get(Node, MUCSubscribers#muc_subscribers.subscriber_nodes, #{}). + +-spec muc_subscribers_remove_exn(ljid(), #muc_subscribers{}) -> + {#muc_subscribers{}, #subscriber{}}. +muc_subscribers_remove_exn({_, _, _} = LJID, MUCSubscribers) -> + #muc_subscribers{subscribers = Subs, + subscriber_nicks = SubNicks, + subscriber_nodes = SubNodes} = MUCSubscribers, + Subscriber = maps:get(LJID, Subs), + #subscriber{nick = Nick, nodes = Nodes} = Subscriber, + NewSubNicks = maps:remove(Nick, SubNicks), + NewSubs = maps:remove(LJID, Subs), + NewSubNodes = + lists:foldl( + fun(Node, Acc) -> + NodeSubs = maps:get(Node, Acc, #{}), + NodeSubs2 = maps:remove(LJID, NodeSubs), + maps:put(Node, NodeSubs2, Acc) + end, SubNodes, Nodes), + {#muc_subscribers{subscribers = NewSubs, + subscriber_nicks = NewSubNicks, + subscriber_nodes = NewSubNodes}, Subscriber}. + +-spec muc_subscribers_put(#subscriber{}, #muc_subscribers{}) -> + #muc_subscribers{}. +muc_subscribers_put(Subscriber, MUCSubscribers) -> + #subscriber{jid = JID, + nick = Nick, + nodes = Nodes} = Subscriber, + #muc_subscribers{subscribers = Subs, + subscriber_nicks = SubNicks, + subscriber_nodes = SubNodes} = MUCSubscribers, + LJID = jid:tolower(JID), + NewSubs = maps:put(LJID, Subscriber, Subs), + NewSubNicks = maps:put(Nick, [LJID], SubNicks), + NewSubNodes = + lists:foldl( + fun(Node, Acc) -> + NodeSubs = maps:get(Node, Acc, #{}), + NodeSubs2 = maps:put(LJID, Subscriber, NodeSubs), + maps:put(Node, NodeSubs2, Acc) + end, SubNodes, Nodes), + #muc_subscribers{subscribers = NewSubs, + subscriber_nicks = NewSubNicks, + 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 meaninful content +%% Detect messange stanzas that don't have meaningful content +-spec has_body_or_subject(message()) -> boolean(). +has_body_or_subject(#message{body = Body, subject = Subj}) -> + Body /= [] orelse Subj /= []. -has_body_or_subject(Packet) -> - [] /= lists:dropwhile(fun - (#xmlel{name = <<"body">>}) -> false; - (#xmlel{name = <<"subject">>}) -> false; - (_) -> true - end, Packet#xmlel.children). +-spec reset_hibernate_timer(state()) -> state(). +reset_hibernate_timer(State) -> + case State#state.hibernate_timer of + hibernating -> + ok; + _ -> + disable_hibernate_timer(State), + NewTimer = case {mod_muc_opt:hibernation_timeout(State#state.server_host), + maps:size(State#state.users)} of + {infinity, _} -> + none; + {Timeout, 0} -> + p1_fsm:send_event_after(Timeout, hibernate); + _ -> + none + end, + State#state{hibernate_timer = NewTimer} + end. + + +-spec disable_hibernate_timer(state()) -> ok. +disable_hibernate_timer(State) -> + case State#state.hibernate_timer of + Ref when is_reference(Ref) -> + p1_fsm:cancel_timer(Ref), + ok; + _ -> + ok + end. diff --git a/src/mod_muc_rtbl.erl b/src/mod_muc_rtbl.erl new file mode 100644 index 000000000..94186e890 --- /dev/null +++ b/src/mod_muc_rtbl.erl @@ -0,0 +1,282 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_muc_rtbl.erl +%%% Author : Paweł Chmielowski +%%% Purpose : +%%% Created : 17 kwi 2023 by Paweł Chmielowski +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_muc_rtbl). +-author("pawel@process-one.net"). + +-behaviour(gen_mod). +-behaviour(gen_server). + +-include_lib("xmpp/include/xmpp.hrl"). +-include("logger.hrl"). +-include("translate.hrl"). +-include("mod_muc_room.hrl"). + +%% API +-export([start/2, stop/1, init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3, + mod_options/1, mod_opt_type/1, mod_doc/0, depends/2]). +-export([pubsub_event_handler/1, muc_presence_filter/3, muc_process_iq/2]). + +-record(muc_rtbl, {host_id, blank = blank}). +-record(rtbl_state, {host, subscribed = false, retry_timer}). + +start(Host, _Opts) -> + gen_server:start({local, gen_mod:get_module_proc(Host, ?MODULE)}, ?MODULE, [Host], []). + +stop(Host) -> + gen_server:stop(gen_mod:get_module_proc(Host, ?MODULE)). + +init([Host]) -> + ejabberd_mnesia:create(?MODULE, muc_rtbl, + [{ram_copies, [node()]}, + {local_content, true}, + {attributes, record_info(fields, muc_rtbl)}, + {type, set}]), + ejabberd_hooks:add(local_send_to_resource_hook, Host, + ?MODULE, pubsub_event_handler, 50), + ejabberd_hooks:add(muc_filter_presence, Host, + ?MODULE, muc_presence_filter, 50), + ejabberd_hooks:add(muc_process_iq, Host, + ?MODULE, muc_process_iq, 50), + request_initial_items(Host), + {ok, #rtbl_state{host = Host}}. + +handle_call(_Request, _From, State) -> + {noreply, State}. + +handle_cast(_Request, State) -> + {noreply, State}. + +handle_info({iq_reply, IQReply, initial_items}, State) -> + State2 = parse_initial_items(State, IQReply), + {noreply, State2}; +handle_info({iq_reply, IQReply, subscription}, State) -> + State2 = parse_subscription(State, IQReply), + {noreply, State2}; +handle_info(_Request, State) -> + {noreply, State}. + +terminate(_Reason, #rtbl_state{host = Host, subscribed = Sub, retry_timer = Timer}) -> + ejabberd_hooks:delete(local_send_to_resource_hook, Host, + ?MODULE, pubsub_event_handler, 50), + ejabberd_hooks:delete(muc_filter_presence, Host, + ?MODULE, muc_presence_filter, 50), + ejabberd_hooks:delete(muc_process_iq, Host, + ?MODULE, muc_process_iq, 50), + case Sub of + true -> + Jid = service_jid(Host), + IQ = #iq{type = set, from = Jid, to = jid:make(mod_muc_rtbl_opt:rtbl_server(Host)), + sub_els = [ + #pubsub{unsubscribe = + #ps_unsubscribe{jid = Jid, node = mod_muc_rtbl_opt:rtbl_node(Host)}}]}, + ejabberd_router:route_iq(IQ, fun(_) -> ok end); + _ -> + ok + end, + misc:cancel_timer(Timer). + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +request_initial_items(Host) -> + IQ = #iq{type = get, from = service_jid(Host), + to = jid:make(mod_muc_rtbl_opt:rtbl_server(Host)), + sub_els = [ + #pubsub{items = #ps_items{node = mod_muc_rtbl_opt:rtbl_node(Host)}}]}, + ejabberd_router:route_iq(IQ, initial_items, self()). + +parse_initial_items(State, timeout) -> + ?WARNING_MSG("Fetching initial list failed: fetch timeout. Retrying in 60 seconds", []), + State#rtbl_state{retry_timer = erlang:send_after(60000, self(), fetch_list)}; +parse_initial_items(State, #iq{type = error} = IQ) -> + ?WARNING_MSG("Fetching initial list failed: ~p. Retrying in 60 seconds", + [xmpp:format_stanza_error(xmpp:get_error(IQ))]), + State#rtbl_state{retry_timer = erlang:send_after(60000, self(), fetch_list)}; +parse_initial_items(State, #iq{from = From, to = #jid{lserver = Host} = To, type = result} = IQ) -> + case xmpp:get_subtag(IQ, #pubsub{}) of + #pubsub{items = #ps_items{node = Node, items = Items}} -> + Added = lists:foldl( + fun(#ps_item{id = ID}, Acc) -> + mnesia:dirty_write(#muc_rtbl{host_id = {Host, ID}}), + maps:put(ID, true, Acc) + end, #{}, Items), + SubIQ = #iq{type = set, from = To, to = From, + sub_els = [ + #pubsub{subscribe = #ps_subscribe{jid = To, node = Node}}]}, + ejabberd_router:route_iq(SubIQ, subscription, self()), + notify_rooms(Host, Added), + State#rtbl_state{retry_timer = undefined, subscribed = true}; + _ -> + ?WARNING_MSG("Fetching initial list failed: invalid result payload", []), + State#rtbl_state{retry_timer = undefined} + end. + +parse_subscription(State, timeout) -> + ?WARNING_MSG("Subscription error: request timeout", []), + State#rtbl_state{subscribed = false}; +parse_subscription(State, #iq{type = error} = IQ) -> + ?WARNING_MSG("Subscription error: ~p", [xmpp:format_stanza_error(xmpp:get_error(IQ))]), + State#rtbl_state{subscribed = false}; +parse_subscription(State, _) -> + State. + +pubsub_event_handler(#message{from = #jid{luser = <<>>, lserver = SServer}, + to = #jid{luser = <<>>, lserver = Server, + lresource = <<"rtbl-", _/binary>>}} = Msg) -> + + SServer2 = mod_muc_rtbl_opt:rtbl_server(Server), + SNode = mod_muc_rtbl_opt:rtbl_node(Server), + if SServer == SServer2 -> + case xmpp:get_subtag(Msg, #ps_event{}) of + #ps_event{items = #ps_items{node = Node, retract = [Retract | _] = RetractList}} when Node == SNode, + is_binary(Retract) -> + [mnesia:dirty_delete(muc_rtbl, {Server, R1}) || R1 <- RetractList]; + #ps_event{items = #ps_items{node = Node, items = Items}} when Node == SNode -> + Added = lists:foldl( + fun(#ps_item{id = ID}, Acc) -> + mnesia:dirty_write(#muc_rtbl{host_id = {Server, ID}}), + maps:put(ID, true, Acc) + end, #{}, Items), + case maps:size(Added) of + 0 -> ok; + _ -> notify_rooms(Server, Added) + end; + _ -> + ok + end; + true -> + ok + end, + stop; +pubsub_event_handler(_) -> + ok. + +muc_presence_filter(#presence{from = #jid{lserver = Server} = From, lang = Lang} = Packet, _State, _Nick) -> + Blocked = + case mnesia:dirty_read(muc_rtbl, {Server, sha256(Server)}) of + [] -> + JIDs = sha256(jid:encode(jid:tolower(jid:remove_resource(From)))), + case mnesia:dirty_read(muc_rtbl, {Server, JIDs}) of + [] -> false; + _ -> true + end; + _ -> true + end, + case Blocked of + false -> Packet; + _ -> + ErrText = ?T("You have been banned from this room"), + Err = xmpp:err_forbidden(ErrText, Lang), + ejabberd_router:route_error(Packet, Err), + drop + end. + +muc_process_iq(#iq{type = set, sub_els = [{rtbl_update, Items}]}, #state{users = Users} = State0) -> + {NewState, _} = + maps:fold( + fun(_, #user{role = moderator}, {State, HostHashes}) -> + {State, HostHashes}; + ({_, S, _} = LJid, #user{jid = JID}, {State, HostHashes}) -> + {Ban, HH2} = + case maps:find(S, HostHashes) of + {ok, Sha} -> + {maps:is_key(Sha, Items), HostHashes}; + _ -> + Sha = sha256(S), + {maps:is_key(Sha, Items), maps:put(S, Sha, HostHashes)} + end, + Ban2 = + case Ban of + false -> + Sha2 = sha256(jid:encode(jid:remove_resource(LJid))), + maps:is_key(Sha2, Items); + _ -> + true + end, + case Ban2 of + true -> + {_, _, State2} = mod_muc_room:handle_event({process_item_change, + {JID, role, none, <<"Banned by RTBL">>}, + undefined}, + normal_state, State), + {State2, HH2}; + _ -> + {State, HH2} + end + end, {State0, #{}}, Users), + {stop, {ignore, NewState}}; +muc_process_iq(IQ, _State) -> + IQ. + +sha256(Data) -> + Bin = crypto:hash(sha256, Data), + str:to_hexlist(Bin). + +notify_rooms(Host, Items) -> + IQ = #iq{type = set, to = jid:make(Host), sub_els = [{rtbl_update, Items}]}, + lists:foreach( + fun(CHost) -> + lists:foreach( + fun({_, _, Pid}) when node(Pid) == node() -> + mod_muc_room:route(Pid, IQ); + (_) -> + ok + end, mod_muc:get_online_rooms(CHost)) + end, mod_muc_admin:find_hosts(Host)). + + +service_jid(Host) -> + jid:make(<<>>, Host, <<"rtbl-", (ejabberd_cluster:node_id())/binary>>). + +mod_opt_type(rtbl_server) -> + econf:domain(); +mod_opt_type(rtbl_node) -> + econf:non_empty(econf:binary()). + +mod_options(_Host) -> + [{rtbl_server, <<"xmppbl.org">>}, + {rtbl_node, <<"muc_bans_sha256">>}]. + +mod_doc() -> + #{desc => + [?T("This module implement Real-time blocklists for MUC rooms."), "", + ?T("It works by observing remote pubsub node conforming with " + "specification described in https://xmppbl.org/.")], + note => "added in 23.04", + opts => + [{rtbl_server, + #{value => ?T("Domain"), + desc => + ?T("Domain of xmpp server that serves block list. " + "The default value is 'xmppbl.org'")}}, + {rtbl_node, + #{value => "PubsubNodeName", + desc => + ?T("Name of pubsub node that should be used to track blocked users. " + "The default value is 'muc_bans_sha256'.")}}]}. + +depends(_, _) -> + [{mod_muc, hard}, {mod_pubsub, soft}]. diff --git a/src/mod_muc_rtbl_opt.erl b/src/mod_muc_rtbl_opt.erl new file mode 100644 index 000000000..b9394bd39 --- /dev/null +++ b/src/mod_muc_rtbl_opt.erl @@ -0,0 +1,20 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_muc_rtbl_opt). + +-export([rtbl_node/1]). +-export([rtbl_server/1]). + +-spec rtbl_node(gen_mod:opts() | global | binary()) -> binary(). +rtbl_node(Opts) when is_map(Opts) -> + gen_mod:get_opt(rtbl_node, Opts); +rtbl_node(Host) -> + gen_mod:get_module_opt(Host, mod_muc_rtbl, rtbl_node). + +-spec rtbl_server(gen_mod:opts() | global | binary()) -> binary(). +rtbl_server(Opts) when is_map(Opts) -> + gen_mod:get_opt(rtbl_server, Opts); +rtbl_server(Host) -> + gen_mod:get_module_opt(Host, mod_muc_rtbl, rtbl_server). + diff --git a/src/mod_muc_sql.erl b/src/mod_muc_sql.erl new file mode 100644 index 000000000..31c8703c1 --- /dev/null +++ b/src/mod_muc_sql.erl @@ -0,0 +1,647 @@ +%%%------------------------------------------------------------------- +%%% File : mod_muc_sql.erl +%%% Author : Evgeny Khramtsov +%%% Created : 13 Apr 2016 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_muc_sql). + + +-behaviour(mod_muc). +-behaviour(mod_muc_room). + +%% API +-export([init/2, store_room/5, store_changes/4, + restore_room/3, forget_room/3, + can_use_nick/4, get_rooms/2, get_nick/3, set_nick/4, + import/3, export/1]). +-export([register_online_room/4, unregister_online_room/4, find_online_room/3, + get_online_rooms/3, count_online_rooms/2, rsm_supported/0, + 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, + get_hibernated_rooms_older_than/3, + 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]). +-export([sql_schemas/0]). + +-include_lib("xmpp/include/jid.hrl"). +-include("mod_muc.hrl"). +-include("logger.hrl"). +-include("ejabberd_sql_pt.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(Host, Opts) -> + ejabberd_sql_schema:update_schema(Host, ?MODULE, sql_schemas()), + case gen_mod:ram_db_mod(Opts, mod_muc) of + ?MODULE -> + clean_tables(Host); + _ -> + ok + end. + +sql_schemas() -> + [#sql_schema{ + version = 1, + tables = + [#sql_table{ + name = <<"muc_room">>, + columns = + [#sql_column{name = <<"name">>, type = text}, + #sql_column{name = <<"host">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"opts">>, type = {text, big}}, + #sql_column{name = <<"created_at">>, type = timestamp, + default = true}], + indices = [#sql_index{ + columns = [<<"name">>, <<"host">>], + unique = true}, + #sql_index{ + columns = [<<"host">>, <<"created_at">>]}]}, + #sql_table{ + name = <<"muc_registered">>, + columns = + [#sql_column{name = <<"jid">>, type = text}, + #sql_column{name = <<"host">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"nick">>, type = text}, + #sql_column{name = <<"created_at">>, type = timestamp, + default = true}], + indices = [#sql_index{ + columns = [<<"jid">>, <<"host">>], + unique = true}, + #sql_index{ + columns = [<<"nick">>]}]}, + #sql_table{ + name = <<"muc_online_room">>, + columns = + [#sql_column{name = <<"name">>, type = text}, + #sql_column{name = <<"host">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"node">>, type = text}, + #sql_column{name = <<"pid">>, type = text}], + indices = [#sql_index{ + columns = [<<"name">>, <<"host">>], + unique = true}]}, + #sql_table{ + name = <<"muc_online_users">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server">>, type = {text, 75}}, + #sql_column{name = <<"resource">>, type = text}, + #sql_column{name = <<"name">>, type = text}, + #sql_column{name = <<"host">>, type = {text, 75}}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"node">>, type = text}], + indices = [#sql_index{ + columns = [<<"username">>, <<"server">>, + <<"resource">>, <<"name">>, + <<"host">>], + unique = true}]}, + #sql_table{ + name = <<"muc_room_subscribers">>, + columns = + [#sql_column{name = <<"room">>, type = text}, + #sql_column{name = <<"host">>, type = text}, + #sql_column{name = <<"jid">>, type = text}, + #sql_column{name = <<"nick">>, type = text}, + #sql_column{name = <<"nodes">>, type = text}, + #sql_column{name = <<"created_at">>, type = timestamp, + default = true}], + indices = [#sql_index{ + columns = [<<"host">>, <<"room">>, <<"jid">>], + unique = true}, + #sql_index{ + columns = [<<"host">>, <<"jid">>]}, + #sql_index{ + columns = [<<"jid">>]}]}]}]. + +store_room(LServer, Host, Name, Opts, ChangesHints) -> + {Subs, Opts2} = case lists:keytake(subscribers, 1, Opts) of + {value, {subscribers, S}, OptN} -> {S, OptN}; + _ -> {[], Opts} + end, + SOpts = misc:term_to_expr(Opts2), + Timestamp = case lists:keyfind(hibernation_time, 1, Opts) of + false -> <<"1970-01-02 00:00:00">>; + {_, undefined} -> <<"1970-01-02 00:00:00">>; + {_, Time} -> usec_to_sql_timestamp(Time) + end, + F = fun () -> + ?SQL_UPSERT_T( + "muc_room", + ["!name=%(Name)s", + "!host=%(Host)s", + "server_host=%(LServer)s", + "opts=%(SOpts)s", + "created_at=%(Timestamp)t"]), + case ChangesHints of + Changes when is_list(Changes) -> + [change_room(Host, Name, Change) || Change <- Changes]; + _ -> + ejabberd_sql:sql_query_t( + ?SQL("delete from muc_room_subscribers where " + "room=%(Name)s and host=%(Host)s")), + [change_room(Host, Name, {add_subscription, JID, Nick, Nodes}) + || {JID, Nick, Nodes} <- Subs] + end + end, + ejabberd_sql:sql_transaction(LServer, F). + +store_changes(LServer, Host, Name, Changes) -> + F = fun () -> + [change_room(Host, Name, Change) || Change <- Changes] + end, + ejabberd_sql:sql_transaction(LServer, F). + +change_room(Host, Room, {add_subscription, JID, Nick, Nodes}) -> + SJID = jid:encode(JID), + SNodes = misc:term_to_expr(Nodes), + ?SQL_UPSERT_T( + "muc_room_subscribers", + ["!jid=%(SJID)s", + "!host=%(Host)s", + "!room=%(Room)s", + "nick=%(Nick)s", + "nodes=%(SNodes)s"]); +change_room(Host, Room, {del_subscription, JID}) -> + SJID = jid:encode(JID), + ejabberd_sql:sql_query_t(?SQL("delete from muc_room_subscribers where " + "room=%(Room)s and host=%(Host)s and jid=%(SJID)s")); +change_room(Host, Room, Change) -> + ?ERROR_MSG("Unsupported change on room ~ts@~ts: ~p", [Room, Host, Change]). + +restore_room(LServer, Host, Name) -> + case catch ejabberd_sql:sql_query( + LServer, + ?SQL("select @(opts)s from muc_room where name=%(Name)s" + " and host=%(Host)s")) of + {selected, [{Opts}]} -> + OptsD = ejabberd_sql:decode_term(Opts), + case catch ejabberd_sql:sql_query( + LServer, + ?SQL("select @(jid)s, @(nick)s, @(nodes)s from muc_room_subscribers where room=%(Name)s" + " and host=%(Host)s")) of + {selected, []} -> + OptsR = mod_muc:opts_to_binary(OptsD), + case lists:keymember(subscribers, 1, OptsD) of + true -> + store_room(LServer, Host, Name, OptsR, undefined); + _ -> + ok + end, + OptsR; + {selected, Subs} -> + SubData = lists:map( + fun({Jid, Nick, Nodes}) -> + {jid:decode(Jid), Nick, ejabberd_sql:decode_term(Nodes)} + end, Subs), + Opts2 = lists:keystore(subscribers, 1, OptsD, {subscribers, SubData}), + mod_muc:opts_to_binary(Opts2); + _ -> + {error, db_failure} + end; + {selected, _} -> + error; + _ -> + {error, db_failure} + end. + +forget_room(LServer, Host, Name) -> + F = fun () -> + ejabberd_sql:sql_query_t( + ?SQL("delete from muc_room where name=%(Name)s" + " and host=%(Host)s")), + ejabberd_sql:sql_query_t( + ?SQL("delete from muc_room_subscribers where room=%(Name)s" + " and host=%(Host)s")) + end, + ejabberd_sql:sql_transaction(LServer, F). + +can_use_nick(LServer, ServiceOrRoom, JID, Nick) -> + SJID = jid:encode(jid:tolower(jid:remove_resource(JID))), + SqlQuery = case (jid:decode(ServiceOrRoom))#jid.lserver of + ServiceOrRoom -> + ?SQL("select @(jid)s from muc_registered " + "where nick=%(Nick)s" + " and host=%(ServiceOrRoom)s"); + Service -> + ?SQL("select @(jid)s from muc_registered " + "where nick=%(Nick)s" + " and (host=%(ServiceOrRoom)s or host=%(Service)s)") + end, + case catch ejabberd_sql:sql_query(LServer, SqlQuery) of + {selected, [{SJID1}]} -> SJID == SJID1; + _ -> true + end. + +get_rooms_without_subscribers(LServer, Host) -> + case catch ejabberd_sql:sql_query( + LServer, + ?SQL("select @(name)s, @(opts)s from muc_room" + " where host=%(Host)s")) of + {selected, RoomOpts} -> + lists:map( + fun({Room, Opts}) -> + OptsD = ejabberd_sql:decode_term(Opts), + #muc_room{name_host = {Room, Host}, + opts = mod_muc:opts_to_binary(OptsD)} + end, RoomOpts); + _Err -> + [] + end. + +get_hibernated_rooms_older_than(LServer, Host, Timestamp) -> + TimestampS = usec_to_sql_timestamp(Timestamp), + case catch ejabberd_sql:sql_query( + LServer, + ?SQL("select @(name)s, @(opts)s from muc_room" + " where host=%(Host)s and created_at < %(TimestampS)t and created_at > '1970-01-02 00:00:00'")) of + {selected, RoomOpts} -> + lists:map( + fun({Room, Opts}) -> + OptsD = ejabberd_sql:decode_term(Opts), + #muc_room{name_host = {Room, Host}, + opts = mod_muc:opts_to_binary(OptsD)} + end, RoomOpts); + _Err -> + [] + end. + +get_rooms(LServer, Host) -> + case catch ejabberd_sql:sql_query( + LServer, + ?SQL("select @(name)s, @(opts)s from muc_room" + " where host=%(Host)s")) of + {selected, RoomOpts} -> + case catch ejabberd_sql:sql_query( + LServer, + ?SQL("select @(room)s, @(jid)s, @(nick)s, @(nodes)s from muc_room_subscribers" + " where host=%(Host)s")) of + {selected, Subs} -> + SubsD = lists:foldl( + fun({Room, Jid, Nick, Nodes}, Dict) -> + Sub = {jid:decode(Jid), + Nick, ejabberd_sql:decode_term(Nodes)}, + maps:update_with( + Room, + fun(SubAcc) -> + [Sub | SubAcc] + end, + [Sub], + Dict) + end, maps:new(), Subs), + lists:map( + fun({Room, Opts}) -> + OptsD = ejabberd_sql:decode_term(Opts), + OptsD2 = case {maps:find(Room, SubsD), lists:keymember(subscribers, 1, OptsD)} of + {_, true} -> + store_room(LServer, Host, Room, mod_muc:opts_to_binary(OptsD), undefined), + OptsD; + {{ok, SubsI}, false} -> + lists:keystore(subscribers, 1, OptsD, {subscribers, SubsI}); + _ -> + OptsD + end, + #muc_room{name_host = {Room, Host}, + opts = mod_muc:opts_to_binary(OptsD2)} + end, RoomOpts); + _Err -> + [] + end; + _Err -> + [] + end. + +get_nick(LServer, Host, From) -> + SJID = jid:encode(jid:tolower(jid:remove_resource(From))), + case catch ejabberd_sql:sql_query( + LServer, + ?SQL("select @(nick)s from muc_registered where" + " jid=%(SJID)s and host=%(Host)s")) of + {selected, [{Nick}]} -> Nick; + _ -> error + end. + +set_nick(LServer, ServiceOrRoom, From, Nick) -> + JID = jid:encode(jid:tolower(jid:remove_resource(From))), + F = fun () -> + case Nick of + <<"">> -> + ejabberd_sql:sql_query_t( + ?SQL("delete from muc_registered where" + " jid=%(JID)s and host=%(ServiceOrRoom)s")), + ok; + _ -> + Service = (jid:decode(ServiceOrRoom))#jid.lserver, + SqlQuery = case (ServiceOrRoom == Service) of + true -> + ?SQL("select @(jid)s, @(host)s from muc_registered " + "where nick=%(Nick)s" + " and host=%(ServiceOrRoom)s"); + false -> + ?SQL("select @(jid)s, @(host)s from muc_registered " + "where nick=%(Nick)s" + " and (host=%(ServiceOrRoom)s or host=%(Service)s)") + end, + Allow = case ejabberd_sql:sql_query_t(SqlQuery) of + {selected, []} + when (ServiceOrRoom == Service) -> + %% Registering in the service... + %% check if nick is registered for some room in this service + {selected, NickRegistrations} = + ejabberd_sql:sql_query_t( + ?SQL("select @(jid)s, @(host)s from muc_registered " + "where nick=%(Nick)s")), + not lists:any(fun({_NRJid, NRServiceOrRoom}) -> + Service == (jid:decode(NRServiceOrRoom))#jid.lserver end, + NickRegistrations); + {selected, []} -> + %% Nick not registered in any service or room + true; + {selected, [{_J, Host}]} + when (Host == Service) and (ServiceOrRoom /= Service) -> + %% Registering in a room, but the nick is already registered in the service + false; + {selected, [{J, _Host}]} -> + %% Registering in room (or service) a nick that is + %% already registered in this room (or service) + %% Only the owner of this registration can use the nick + J == JID + end, + if Allow -> + ?SQL_UPSERT_T( + "muc_registered", + ["!jid=%(JID)s", + "!host=%(ServiceOrRoom)s", + "server_host=%(LServer)s", + "nick=%(Nick)s"]), + ok; + true -> + false + end + end + end, + ejabberd_sql:sql_transaction(LServer, F). + +set_affiliation(_ServerHost, _Room, _Host, _JID, _Affiliation, _Reason) -> + {error, not_implemented}. + +set_affiliations(_ServerHost, _Room, _Host, _Affiliations) -> + {error, not_implemented}. + +get_affiliation(_ServerHost, _Room, _Host, _LUser, _LServer) -> + {error, not_implemented}. + +get_affiliations(_ServerHost, _Room, _Host) -> + {error, not_implemented}. + +search_affiliation(_ServerHost, _Room, _Host, _Affiliation) -> + {error, not_implemented}. + +register_online_room(ServerHost, Room, Host, Pid) -> + PidS = misc:encode_pid(Pid), + NodeS = erlang:atom_to_binary(node(Pid), latin1), + case ?SQL_UPSERT(ServerHost, + "muc_online_room", + ["!name=%(Room)s", + "!host=%(Host)s", + "server_host=%(ServerHost)s", + "node=%(NodeS)s", + "pid=%(PidS)s"]) of + ok -> + ok; + Err -> + Err + end. + +unregister_online_room(ServerHost, Room, Host, Pid) -> + %% TODO: report errors + PidS = misc:encode_pid(Pid), + NodeS = erlang:atom_to_binary(node(Pid), latin1), + ejabberd_sql:sql_query( + ServerHost, + ?SQL("delete from muc_online_room where name=%(Room)s and " + "host=%(Host)s and node=%(NodeS)s and pid=%(PidS)s")). + +find_online_room(ServerHost, Room, Host) -> + case ejabberd_sql:sql_query( + ServerHost, + ?SQL("select @(pid)s, @(node)s from muc_online_room where " + "name=%(Room)s and host=%(Host)s")) of + {selected, [{PidS, NodeS}]} -> + try {ok, misc:decode_pid(PidS, NodeS)} + catch _:{bad_node, _} -> error + end; + {selected, []} -> + error; + _Err -> + error + end. + +find_online_room_by_pid(ServerHost, Pid) -> + PidS = misc:encode_pid(Pid), + NodeS = erlang:atom_to_binary(node(Pid), latin1), + case ejabberd_sql:sql_query( + ServerHost, + ?SQL("select @(name)s, @(host)s from muc_online_room where " + "node=%(NodeS)s and pid=%(PidS)s")) of + {selected, [{Room, Host}]} -> + {ok, Room, Host}; + {selected, []} -> + error; + _Err -> + error + end. + +count_online_rooms(ServerHost, Host) -> + case ejabberd_sql:sql_query( + ServerHost, + ?SQL("select @(count(*))d from muc_online_room " + "where host=%(Host)s")) of + {selected, [{Num}]} -> + Num; + _Err -> + 0 + end. + +get_online_rooms(ServerHost, Host, _RSM) -> + case ejabberd_sql:sql_query( + ServerHost, + ?SQL("select @(name)s, @(pid)s, @(node)s from muc_online_room " + "where host=%(Host)s")) of + {selected, Rows} -> + lists:flatmap( + fun({Room, PidS, NodeS}) -> + try [{Room, Host, misc:decode_pid(PidS, NodeS)}] + catch _:{bad_node, _} -> [] + end + end, Rows); + _Err -> + [] + end. + +rsm_supported() -> + false. + +register_online_user(ServerHost, {U, S, R}, Room, Host) -> + NodeS = erlang:atom_to_binary(node(), latin1), + case ?SQL_UPSERT(ServerHost, "muc_online_users", + ["!username=%(U)s", + "!server=%(S)s", + "!resource=%(R)s", + "!name=%(Room)s", + "!host=%(Host)s", + "server_host=%(ServerHost)s", + "node=%(NodeS)s"]) of + ok -> + ok; + Err -> + Err + end. + +unregister_online_user(ServerHost, {U, S, R}, Room, Host) -> + %% TODO: report errors + ejabberd_sql:sql_query( + ServerHost, + ?SQL("delete from muc_online_users where username=%(U)s and " + "server=%(S)s and resource=%(R)s and name=%(Room)s and " + "host=%(Host)s")). + +count_online_rooms_by_user(ServerHost, U, S) -> + case ejabberd_sql:sql_query( + ServerHost, + ?SQL("select @(count(*))d from muc_online_users where " + "username=%(U)s and server=%(S)s")) of + {selected, [{Num}]} -> + Num; + _Err -> + 0 + end. + +get_online_rooms_by_user(ServerHost, U, S) -> + case ejabberd_sql:sql_query( + ServerHost, + ?SQL("select @(name)s, @(host)s from muc_online_users where " + "username=%(U)s and server=%(S)s")) of + {selected, Rows} -> + Rows; + _Err -> + [] + end. + +export(_Server) -> + [{muc_room, + fun(Host, #muc_room{name_host = {Name, RoomHost}, opts = Opts}) -> + case str:suffix(Host, RoomHost) of + true -> + SOpts = misc:term_to_expr(Opts), + [?SQL("delete from muc_room where name=%(Name)s" + " and host=%(RoomHost)s;"), + ?SQL_INSERT( + "muc_room", + ["name=%(Name)s", + "host=%(RoomHost)s", + "server_host=%(Host)s", + "opts=%(SOpts)s"])]; + false -> + [] + end + end}, + {muc_registered, + fun(Host, #muc_registered{us_host = {{U, S}, RoomHost}, + nick = Nick}) -> + case str:suffix(Host, RoomHost) of + true -> + SJID = jid:encode(jid:make(U, S)), + [?SQL("delete from muc_registered where" + " jid=%(SJID)s and host=%(RoomHost)s;"), + ?SQL_INSERT( + "muc_registered", + ["jid=%(SJID)s", + "host=%(RoomHost)s", + "server_host=%(Host)s", + "nick=%(Nick)s"])]; + false -> + [] + end + end}]. + +import(_, _, _) -> + ok. + +get_subscribed_rooms(LServer, Host, Jid) -> + JidS = jid:encode(Jid), + case ejabberd_sql:sql_query( + LServer, + ?SQL("select @(room)s, @(nick)s, @(nodes)s from muc_room_subscribers " + "where jid=%(JidS)s and host=%(Host)s")) of + {selected, Subs} -> + {ok, [{jid:make(Room, Host), Nick, ejabberd_sql:decode_term(Nodes)} + || {Room, Nick, Nodes} <- Subs]}; + _Error -> + {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 +%%%=================================================================== +clean_tables(ServerHost) -> + NodeS = erlang:atom_to_binary(node(), latin1), + ?DEBUG("Cleaning SQL muc_online_room table...", []), + case ejabberd_sql:sql_query( + ServerHost, + ?SQL("delete from muc_online_room where node=%(NodeS)s")) of + {updated, _} -> + ok; + Err1 -> + ?ERROR_MSG("Failed to clean 'muc_online_room' table: ~p", [Err1]), + Err1 + end, + ?DEBUG("Cleaning SQL muc_online_users table...", []), + case ejabberd_sql:sql_query( + ServerHost, + ?SQL("delete from muc_online_users where node=%(NodeS)s")) of + {updated, _} -> + ok; + Err2 -> + ?ERROR_MSG("Failed to clean 'muc_online_users' table: ~p", [Err2]), + Err2 + end. + +usec_to_sql_timestamp(Timestamp) -> + TS = misc:usec_to_now(Timestamp), + case calendar:now_to_universal_time(TS) of + {{Year, Month, Day}, {Hour, Minute, Second}} -> + list_to_binary(io_lib:format("~4..0B-~2..0B-~2..0B " + "~2..0B:~2..0B:~2..0B", + [Year, Month, Day, Hour, Minute, Second])) + end. diff --git a/src/mod_muc_sup.erl b/src/mod_muc_sup.erl new file mode 100644 index 000000000..744e20c45 --- /dev/null +++ b/src/mod_muc_sup.erl @@ -0,0 +1,77 @@ +%%%---------------------------------------------------------------------- +%%% Created : 4 Jul 2019 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_muc_sup). +-behaviour(supervisor). + +%% API +-export([start/1, start_link/1, procname/1]). +%% Supervisor callbacks +-export([init/1]). + +%%%=================================================================== +%%% API functions +%%%=================================================================== +start(Host) -> + Spec = #{id => procname(Host), + start => {?MODULE, start_link, [Host]}, + restart => permanent, + shutdown => infinity, + type => supervisor, + modules => [?MODULE]}, + supervisor:start_child(ejabberd_gen_mod_sup, Spec). + +start_link(Host) -> + Proc = procname(Host), + supervisor:start_link({local, Proc}, ?MODULE, [Host]). + +-spec procname(binary()) -> atom(). +procname(Host) -> + gen_mod:get_module_proc(Host, ?MODULE). + +%%%=================================================================== +%%% Supervisor callbacks +%%%=================================================================== +init([Host]) -> + Cores = misc:logical_processors(), + Specs = lists:foldl( + fun(I, Acc) -> + [#{id => mod_muc:procname(Host, I), + start => {mod_muc, start_link, [Host, I]}, + restart => permanent, + shutdown => timer:minutes(1), + type => worker, + modules => [mod_muc]}|Acc] + end, [room_sup_spec(Host)], lists:seq(1, Cores)), + {ok, {{one_for_one, 10*Cores, 1}, Specs}}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +-spec room_sup_spec(binary()) -> supervisor:child_spec(). +room_sup_spec(Host) -> + Name = mod_muc_room:supervisor(Host), + #{id => Name, + start => {ejabberd_tmp_sup, start_link, [Name, mod_muc_room]}, + restart => permanent, + shutdown => infinity, + type => supervisor, + modules => [ejabberd_tmp_sup]}. diff --git a/src/mod_multicast.erl b/src/mod_multicast.erl new file mode 100644 index 000000000..369d5f92d --- /dev/null +++ b/src/mod_multicast.erl @@ -0,0 +1,1096 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_multicast.erl +%%% Author : Badlop +%%% Purpose : Extended Stanza Addressing (XEP-0033) support +%%% Created : 29 May 2007 by Badlop +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_multicast). + +-author('badlop@process-one.net'). + +-protocol({xep, 33, '1.1', '15.04', "complete", ""}). + +-behaviour(gen_server). + +-behaviour(gen_mod). + +%% API +-export([start/2, stop/1, reload/3, + user_send_packet/1]). + +%% gen_server callbacks +-export([init/1, handle_info/2, handle_call/3, + handle_cast/2, terminate/2, code_change/3]). + +-export([purge_loop/1, mod_opt_type/1, mod_options/1, depends/2, mod_doc/0]). + +-include("logger.hrl"). +-include("translate.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). + +-record(multicastc, {rserver :: binary(), + response, + ts :: integer()}). + +-type limit_value() :: {default | custom, integer()}. +-record(limits, {message :: limit_value(), + presence :: limit_value()}). + +-record(service_limits, {local :: #limits{}, + remote :: #limits{}}). + +-record(state, {lserver :: binary(), + lservice :: binary(), + access :: atom(), + service_limits :: #service_limits{}}). +-type state() :: #state{}. + +%% All the elements are of type value() + +-define(PURGE_PROCNAME, + ejabberd_mod_multicast_purgeloop). + +-define(MAXTIME_CACHE_POSITIVE, 86400). + +-define(MAXTIME_CACHE_NEGATIVE, 86400). + +-define(MAXTIME_CACHE_NEGOTIATING, 600). + +-define(CACHE_PURGE_TIMER, 86400000). + +-define(DEFAULT_LIMIT_LOCAL_MESSAGE, 100). + +-define(DEFAULT_LIMIT_LOCAL_PRESENCE, 100). + +-define(DEFAULT_LIMIT_REMOTE_MESSAGE, 20). + +-define(DEFAULT_LIMIT_REMOTE_PRESENCE, 20). + +start(LServerS, Opts) -> + gen_mod:start_child(?MODULE, LServerS, Opts). + +stop(LServerS) -> + gen_mod:stop_child(?MODULE, LServerS). + +reload(LServerS, NewOpts, OldOpts) -> + Proc = gen_mod:get_module_proc(LServerS, ?MODULE), + gen_server:cast(Proc, {reload, NewOpts, OldOpts}). + +-define(SETS, gb_sets). + +user_send_packet({#presence{} = Packet, C2SState} = Acc) -> + case xmpp:get_subtag(Packet, #addresses{}) of + #addresses{list = Addresses} -> + {CC, BCC, _Invalid, _Delivered} = partition_addresses(Addresses), + NewState = + lists:foldl( + fun(Address, St) -> + case Address#address.jid of + #jid{} = JID -> + LJID = jid:tolower(JID), + #{pres_a := PresA} = St, + A = + case Packet#presence.type of + available -> + ?SETS:add_element(LJID, PresA); + unavailable -> + ?SETS:del_element(LJID, PresA); + _ -> + PresA + end, + St#{pres_a => A}; + undefined -> + St + end + end, C2SState, CC ++ BCC), + {Packet, NewState}; + false -> + Acc + end; +user_send_packet(Acc) -> + Acc. + +%%==================================================================== +%% gen_server callbacks +%%==================================================================== + +-spec init(list()) -> {ok, state()}. +init([LServerS|_]) -> + process_flag(trap_exit, true), + Opts = gen_mod:get_module_opts(LServerS, ?MODULE), + [LServiceS|_] = gen_mod:get_opt_hosts(Opts), + Access = mod_multicast_opt:access(Opts), + SLimits = build_service_limit_record(mod_multicast_opt:limits(Opts)), + create_cache(), + try_start_loop(), + ejabberd_router_multicast:register_route(LServerS), + ejabberd_router:register_route(LServiceS, LServerS), + ejabberd_hooks:add(user_send_packet, LServerS, ?MODULE, + user_send_packet, 50), + {ok, + #state{lservice = LServiceS, lserver = LServerS, + access = Access, service_limits = SLimits}}. + +handle_call(stop, _From, State) -> + try_stop_loop(), {stop, normal, ok, State}. + +handle_cast({reload, NewOpts, NewOpts}, + #state{lserver = LServerS, lservice = OldLServiceS} = State) -> + Access = mod_multicast_opt:access(NewOpts), + SLimits = build_service_limit_record(mod_multicast_opt:limits(NewOpts)), + [NewLServiceS|_] = gen_mod:get_opt_hosts(NewOpts), + if NewLServiceS /= OldLServiceS -> + ejabberd_router:register_route(NewLServiceS, LServerS), + ejabberd_router:unregister_route(OldLServiceS); + true -> + ok + end, + {noreply, State#state{lservice = NewLServiceS, + access = Access, service_limits = SLimits}}; +handle_cast(Msg, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Msg]), + {noreply, State}. + +%%-------------------------------------------------------------------- +%% Function: handle_info(Info, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} +%% Description: Handling all non call/cast messages +%%-------------------------------------------------------------------- + +handle_info({route, #iq{} = Packet}, State) -> + case catch handle_iq(Packet, State) of + {'EXIT', Reason} -> + ?ERROR_MSG("Error when processing IQ stanza: ~p", + [Reason]); + _ -> ok + end, + {noreply, State}; +%% XEP33 allows only 'message' and 'presence' stanza type +handle_info({route, Packet}, + #state{lservice = LServiceS, lserver = LServerS, + access = Access, service_limits = SLimits} = + State) when ?is_stanza(Packet) -> + route_untrusted(LServiceS, LServerS, Access, SLimits, Packet), + {noreply, State}; +%% Handle multicast packets sent by trusted local services +handle_info({route_trusted, Destinations, Packet}, + #state{lservice = LServiceS, lserver = LServerS} = + State) -> + From = xmpp:get_from(Packet), + case catch route_trusted(LServiceS, LServerS, From, Destinations, + Packet) of + {'EXIT', Reason} -> + ?ERROR_MSG("Error in route_trusted: ~p", [Reason]); + _ -> ok + end, + {noreply, State}; +handle_info({get_host, Pid}, State) -> + Pid ! {my_host, State#state.lservice}, + {noreply, State}; +handle_info(_Info, State) -> {noreply, State}. + +terminate(_Reason, State) -> + ejabberd_hooks:delete(user_send_packet, State#state.lserver, ?MODULE, + user_send_packet, 50), + ejabberd_router_multicast:unregister_route(State#state.lserver), + ejabberd_router:unregister_route(State#state.lservice), + ok. + +code_change(_OldVsn, State, _Extra) -> {ok, State}. + +%%==================================================================== +%%% Internal functions +%%==================================================================== + +%%%------------------------ +%%% IQ Request Processing +%%%------------------------ + +handle_iq(Packet, State) -> + try + IQ = xmpp:decode_els(Packet), + case process_iq(IQ, State) of + {result, SubEl} -> + ejabberd_router:route(xmpp:make_iq_result(Packet, SubEl)); + {error, Error} -> + ejabberd_router:route_error(Packet, Error); + reply -> + To = xmpp:get_to(IQ), + LServiceS = jid:encode(To), + case Packet#iq.type of + result -> + process_iqreply_result(LServiceS, IQ); + error -> + process_iqreply_error(LServiceS, IQ) + end + end + catch _:{xmpp_codec, Why} -> + Lang = xmpp:get_lang(Packet), + Err = xmpp:err_bad_request(xmpp:io_format_error(Why), Lang), + ejabberd_router:route_error(Packet, Err) + end. + +-spec process_iq(iq(), state()) -> {result, xmpp_element()} | + {error, stanza_error()} | reply. +process_iq(#iq{type = get, lang = Lang, from = From, + sub_els = [#disco_info{}]}, State) -> + {result, iq_disco_info(From, Lang, State)}; +process_iq(#iq{type = get, sub_els = [#disco_items{}]}, _) -> + {result, #disco_items{}}; +process_iq(#iq{type = get, lang = Lang, sub_els = [#vcard_temp{}]}, State) -> + {result, iq_vcard(Lang, State)}; +process_iq(#iq{type = T}, _) when T == set; T == get -> + {error, xmpp:err_service_unavailable()}; +process_iq(_, _) -> + reply. + +iq_disco_info(From, Lang, State) -> + Name = mod_multicast_opt:name(State#state.lserver), + #disco_info{ + identities = [#identity{category = <<"service">>, + type = <<"multicast">>, + name = translate:translate(Lang, Name)}], + features = [?NS_DISCO_INFO, ?NS_DISCO_ITEMS, ?NS_VCARD, ?NS_ADDRESS], + xdata = iq_disco_info_extras(From, State)}. + +-spec iq_vcard(binary(), state()) -> #vcard_temp{}. +iq_vcard(Lang, State) -> + case mod_multicast_opt:vcard(State#state.lserver) of + undefined -> + #vcard_temp{fn = <<"ejabberd/mod_multicast">>, + url = ejabberd_config:get_uri(), + desc = misc:get_descr(Lang, ?T("ejabberd Multicast service"))}; + VCard -> + VCard + end. + +%%%------------------------- +%%% Route +%%%------------------------- + +-spec route_trusted(binary(), binary(), jid(), [jid()], stanza()) -> 'ok'. +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) -> + try route_untrusted2(LServiceS, LServerS, Access, + SLimits, Packet) + catch + adenied -> + route_error(Packet, forbidden, + ?T("Access denied by service policy")); + eadsele -> + route_error(Packet, bad_request, + ?T("No addresses element found")); + eadeles -> + route_error(Packet, bad_request, + ?T("No address elements found")); + ewxmlns -> + route_error(Packet, bad_request, + ?T("Wrong xmlns")); + etoorec -> + route_error(Packet, not_acceptable, + ?T("Too many receiver fields were specified")); + edrelay -> + route_error(Packet, forbidden, + ?T("Packet relay is denied by service policy")); + EType:EReason -> + ?ERROR_MSG("Multicast unknown error: Type: ~p~nReason: ~p", + [EType, EReason]), + route_error(Packet, internal_server_error, + ?T("Internal server error")) + end. + +-spec route_untrusted2(binary(), binary(), atom(), #service_limits{}, stanza()) -> 'ok'. +route_untrusted2(LServiceS, LServerS, Access, SLimits, Packet) -> + FromJID = xmpp:get_from(Packet), + ok = check_access(LServerS, Access, FromJID), + {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_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(#address{jid = To}) -> + ejabberd_router:route(xmpp:set_from_to(PacketWithAddresses, From, To)) + end, CC), + lists:foreach( + 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, jid:make(Service), CC, BCC, OtherCC ++ RestOfAddresses, Packet, Limits) + end + end, ok, Groups). + +%%%------------------------- +%%% Check access permission +%%%------------------------- + +check_access(LServerS, Access, From) -> + case acl:match_rule(LServerS, Access, From) of + allow -> ok; + _ -> throw(adenied) + end. + +%%%------------------------- +%%% Strip 'addresses' XML element +%%%------------------------- + +-spec strip_addresses_element(stanza()) -> {ok, stanza(), [address()]}. +strip_addresses_element(Packet) -> + case xmpp:get_subtag(Packet, #addresses{}) of + #addresses{list = Addrs} -> + PacketStripped = xmpp:remove_subtag(Packet, #addresses{}), + {ok, PacketStripped, Addrs}; + false -> + throw(eadsele) + end. + +%%%------------------------- +%%% Split 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(), integer()) -> ok. +check_limit_dests(SLimits, FromJID, Packet, NumOfAddresses) -> + SenderT = sender_type(FromJID), + Limits = get_slimit_group(SenderT, SLimits), + StanzaType = type_of_stanza(Packet), + {_Type, Limit} = get_limit_number(StanzaType, + Limits), + case NumOfAddresses > Limit of + false -> ok; + true -> throw(etoorec) + end. + + +-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 +%%%------------------------- + +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 +%%%------------------------- + +%%%------------------------- +%%% Check relay +%%%------------------------- + +-spec check_relay(binary(), binary(), #{}) -> ok. +check_relay(RS, LS, Gs) -> + 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. + +%%%------------------------- +%%% Check protocol support: Send request +%%%------------------------- + +-spec send_query_info(binary(), binary(), binary()) -> ok. +send_query_info(RServerS, LServiceS, ID) -> + case str:str(RServerS, <<"echo.">>) of + 1 -> ok; + _ -> send_query(RServerS, LServiceS, ID, #disco_info{}) + end. + +-spec send_query_items(binary(), binary(), binary()) -> ok. +send_query_items(RServerS, LServiceS, ID) -> + send_query(RServerS, LServiceS, ID, #disco_items{}). + +-spec send_query(binary(), binary(), binary(), disco_info()|disco_items()) -> ok. +send_query(RServerS, LServiceS, ID, SubEl) -> + Packet = #iq{from = stj(LServiceS), + to = stj(RServerS), + id = ID, + type = get, sub_els = [SubEl]}, + ejabberd_router:route(Packet). + +%%%------------------------- +%%% Check protocol support: Receive response: Error +%%%------------------------- + +process_iqreply_error(LServiceS, Packet) -> + FromS = jts(xmpp:get_from(Packet)), + ID = Packet#iq.id, + case str:tokens(ID, <<"/">>) of + [RServer, _] -> + case look_server(RServer) of + {cached, {_Response, {wait_for_info, ID}}, _TS} + when RServer == FromS -> + add_response(RServer, not_supported, cached); + {cached, {_Response, {wait_for_items, ID}}, _TS} + when RServer == FromS -> + add_response(RServer, not_supported, cached); + {cached, {Response, {wait_for_items_info, ID, Items}}, + _TS} -> + case lists:member(FromS, Items) of + true -> + received_awaiter( + FromS, RServer, Response, ID, Items, + LServiceS); + false -> + ok + end; + _ -> + ok + end; + _ -> + ok + end. + +%%%------------------------- +%%% Check protocol support: Receive response: Disco +%%%------------------------- + +-spec process_iqreply_result(binary(), iq()) -> any(). +process_iqreply_result(LServiceS, #iq{from = From, id = ID, sub_els = [SubEl]}) -> + case SubEl of + #disco_info{} -> + process_discoinfo_result(From, LServiceS, ID, SubEl); + #disco_items{} -> + process_discoitems_result(From, LServiceS, ID, SubEl); + _ -> + ok + end. + +%%%------------------------- +%%% Check protocol support: Receive response: Disco Info +%%%------------------------- + +process_discoinfo_result(From, LServiceS, ID, DiscoInfo) -> + FromS = jts(From), + case str:tokens(ID, <<"/">>) of + [RServer, _] -> + case look_server(RServer) of + {cached, {Response, {wait_for_info, ID} = ST}, _TS} + when RServer == FromS -> + process_discoinfo_result2( + From, FromS, LServiceS, DiscoInfo, + RServer, Response, ST); + {cached, {Response, {wait_for_items_info, ID, Items} = ST}, + _TS} -> + case lists:member(FromS, Items) of + true -> + process_discoinfo_result2( + From, FromS, LServiceS, DiscoInfo, + RServer, Response, ST); + false -> + ok + end; + _ -> + ok + end; + _ -> + ok + end. + +process_discoinfo_result2(From, FromS, LServiceS, + #disco_info{features = Feats} = DiscoInfo, + RServer, Response, ST) -> + Multicast_support = lists:member(?NS_ADDRESS, Feats), + case Multicast_support of + true -> + SenderT = sender_type(From), + RLimits = get_limits_xml(DiscoInfo, SenderT), + add_response(RServer, {multicast_supported, FromS, RLimits}, cached); + false -> + case ST of + {wait_for_info, _ID} -> + Random = p1_rand:get_string(), + ID = <>, + send_query_items(FromS, LServiceS, ID), + add_response(RServer, Response, {wait_for_items, ID}); + %% We asked a component, and it does not support XEP33 + {wait_for_items_info, ID, Items} -> + received_awaiter(FromS, RServer, Response, ID, Items, LServiceS) + end + end. + +get_limits_xml(DiscoInfo, SenderT) -> + LimitOpts = get_limits_els(DiscoInfo), + build_remote_limit_record(LimitOpts, SenderT). + +-spec get_limits_els(disco_info()) -> [{atom(), integer()}]. +get_limits_els(DiscoInfo) -> + lists:flatmap( + fun(#xdata{type = result} = X) -> + get_limits_fields(X); + (_) -> + [] + end, DiscoInfo#disco_info.xdata). + +-spec get_limits_fields(xdata()) -> [{atom(), integer()}]. +get_limits_fields(X) -> + {Head, Tail} = lists:partition( + fun(#xdata_field{var = Var, type = Type}) -> + Var == <<"FORM_TYPE">> andalso Type == hidden + end, X#xdata.fields), + case Head of + [] -> []; + _ -> get_limits_values(Tail) + end. + +-spec get_limits_values([xdata_field()]) -> [{atom(), integer()}]. +get_limits_values(Fields) -> + lists:flatmap( + fun(#xdata_field{var = Name, values = [Number]}) -> + try + [{binary_to_atom(Name, utf8), binary_to_integer(Number)}] + catch _:badarg -> + [] + end; + (_) -> + [] + end, Fields). + +%%%------------------------- +%%% Check protocol support: Receive response: Disco Items +%%%------------------------- + +process_discoitems_result(From, LServiceS, ID, #disco_items{items = Items}) -> + FromS = jts(From), + case str:tokens(ID, <<"/">>) of + [FromS = RServer, _] -> + case look_server(RServer) of + {cached, {Response, {wait_for_items, ID}}, _TS} -> + List = lists:flatmap( + fun(#disco_item{jid = #jid{luser = <<"">>, + lserver = LServer, + lresource = <<"">>}}) -> + [LServer]; + (_) -> + [] + end, Items), + case List of + [] -> + add_response(RServer, not_supported, cached); + _ -> + Random = p1_rand:get_string(), + ID2 = <>, + [send_query_info(Item, LServiceS, ID2) || Item <- List], + add_response(RServer, Response, + {wait_for_items_info, ID2, List}) + end; + _ -> + ok + end; + _ -> + ok + end. + +%%%------------------------- +%%% Check protocol support: Receive response: Received awaiter +%%%------------------------- + +received_awaiter(JID, RServer, Response, ID, JIDs, _LServiceS) -> + case lists:delete(JID, JIDs) of + [] -> + add_response(RServer, not_supported, cached); + JIDs2 -> + add_response(RServer, Response, {wait_for_items_info, ID, JIDs2}) + end. + +%%%------------------------- +%%% Cache +%%%------------------------- + +create_cache() -> + ejabberd_mnesia:create(?MODULE, multicastc, + [{ram_copies, [node()]}, + {attributes, record_info(fields, multicastc)}]). + +add_response(RServer, Response, State) -> + Secs = calendar:datetime_to_gregorian_seconds(calendar:local_time()), + mnesia:dirty_write(#multicastc{rserver = RServer, + response = {Response, State}, ts = Secs}). + +search_server_on_cache(RServer, LServerS, _LServiceS, _Maxmins) + when RServer == LServerS -> + route_single; +search_server_on_cache(RServer, _LServerS, LServiceS, _Maxmins) + when RServer == LServiceS -> + route_single; +search_server_on_cache(RServer, _LServerS, LServiceS, Maxmins) -> + case look_server(RServer) of + not_cached -> + query_info(RServer, LServiceS, not_supported), + route_single; + {cached, {Response, State}, TS} -> + Now = calendar:datetime_to_gregorian_seconds(calendar:local_time()), + Response2 = + case State of + cached -> + case is_obsolete(Response, TS, Now, Maxmins) of + false -> ok; + true -> + query_info(RServer, LServiceS, Response) + end, + Response; + _ -> + if + Now - TS > ?MAXTIME_CACHE_NEGOTIATING -> + query_info(RServer, LServiceS, not_supported), + not_supported; + true -> + Response + end + end, + case Response2 of + not_supported -> route_single; + {multicast_supported, Service, Limits} -> + {route_multicast, Service, Limits} + end + end. + +query_info(RServer, LServiceS, Response) -> + Random = p1_rand:get_string(), + ID = <>, + send_query_info(RServer, LServiceS, ID), + add_response(RServer, Response, {wait_for_info, ID}). + +look_server(RServer) -> + case mnesia:dirty_read(multicastc, RServer) of + [] -> not_cached; + [M] -> {cached, M#multicastc.response, M#multicastc.ts} + end. + +is_obsolete(Response, Ts, Now, {Max_pos, Max_neg}) -> + Max = case Response of + multicast_not_supported -> Max_neg; + _ -> Max_pos + end, + Now - Ts > Max. + +%%%------------------------- +%%% Purge cache +%%%------------------------- + +purge() -> + Maxmins_positive = (?MAXTIME_CACHE_POSITIVE), + Maxmins_negative = (?MAXTIME_CACHE_NEGATIVE), + Now = + calendar:datetime_to_gregorian_seconds(calendar:local_time()), + purge(Now, {Maxmins_positive, Maxmins_negative}). + +purge(Now, Maxmins) -> + F = fun () -> + mnesia:foldl(fun (R, _) -> + #multicastc{response = Response, ts = Ts} = + R, + case is_obsolete(Response, Ts, Now, + Maxmins) + of + true -> mnesia:delete_object(R); + false -> ok + end + end, + none, multicastc) + end, + mnesia:transaction(F). + +%%%------------------------- +%%% Purge cache loop +%%%------------------------- + +try_start_loop() -> + case lists:member(?PURGE_PROCNAME, registered()) of + true -> ok; + false -> start_loop() + end, + (?PURGE_PROCNAME) ! new_module. + +start_loop() -> + register(?PURGE_PROCNAME, + spawn(?MODULE, purge_loop, [0])), + (?PURGE_PROCNAME) ! purge_now. + +try_stop_loop() -> (?PURGE_PROCNAME) ! try_stop. + +purge_loop(NM) -> + receive + purge_now -> + purge(), + timer:send_after(?CACHE_PURGE_TIMER, ?PURGE_PROCNAME, + purge_now), + purge_loop(NM); + new_module -> purge_loop(NM + 1); + try_stop when NM > 1 -> purge_loop(NM - 1); + try_stop -> purge_loop_finished + end. + +%%%------------------------- +%%% Limits: utils +%%%------------------------- + +%% Type definitions for data structures related with XEP33 limits +%% limit() = {Name, Value} +%% Name = atom() +%% Value = {Type, Number} +%% Type = default | custom +%% Number = integer() | infinite + +list_of_limits(local) -> + [{message, ?DEFAULT_LIMIT_LOCAL_MESSAGE}, + {presence, ?DEFAULT_LIMIT_LOCAL_PRESENCE}]; +list_of_limits(remote) -> + [{message, ?DEFAULT_LIMIT_REMOTE_MESSAGE}, + {presence, ?DEFAULT_LIMIT_REMOTE_PRESENCE}]. + +build_service_limit_record(LimitOpts) -> + LimitOptsL = get_from_limitopts(LimitOpts, local), + LimitOptsR = get_from_limitopts(LimitOpts, remote), + {service_limits, build_limit_record(LimitOptsL, local), + build_limit_record(LimitOptsR, remote)}. + +get_from_limitopts(LimitOpts, SenderT) -> + case lists:keyfind(SenderT, 1, LimitOpts) of + false -> []; + {SenderT, Result} -> Result + end. + +build_remote_limit_record(LimitOpts, SenderT) -> + build_limit_record(LimitOpts, SenderT). + +-spec build_limit_record(any(), local | remote) -> #limits{}. +build_limit_record(LimitOpts, SenderT) -> + Limits = [get_limit_value(Name, Default, LimitOpts) + || {Name, Default} <- list_of_limits(SenderT)], + list_to_tuple([limits | Limits]). + +-spec get_limit_value(atom(), integer(), any()) -> limit_value(). +get_limit_value(Name, Default, LimitOpts) -> + case lists:keysearch(Name, 1, LimitOpts) of + {value, {Name, Number}} -> {custom, Number}; + false -> {default, Default} + end. + +type_of_stanza(Stanza) -> element(1, Stanza). + +-spec get_limit_number(message | presence, #limits{}) -> limit_value(). +get_limit_number(message, Limits) -> + Limits#limits.message; +get_limit_number(presence, Limits) -> + Limits#limits.presence. + +-spec get_slimit_group(local | remote, #service_limits{}) -> #limits{}. +get_slimit_group(local, SLimits) -> + SLimits#service_limits.local; +get_slimit_group(remote, SLimits) -> + SLimits#service_limits.remote. + +%%%------------------------- +%%% Limits: XEP-0128 Service Discovery Extensions +%%%------------------------- + +%% Some parts of code are borrowed from mod_muc_room.erl + +-define(RFIELDT(Type, Var, Val), + #xdata_field{type = Type, var = Var, values = [Val]}). + +-define(RFIELDV(Var, Val), + #xdata_field{var = Var, values = [Val]}). + +iq_disco_info_extras(From, State) -> + SenderT = sender_type(From), + Service_limits = State#state.service_limits, + case iq_disco_info_extras2(SenderT, Service_limits) of + [] -> []; + List_limits_xmpp -> + [#xdata{type = result, + fields = [?RFIELDT(hidden, <<"FORM_TYPE">>, ?NS_ADDRESS) + | List_limits_xmpp]}] + end. + +sender_type(From) -> + Local_hosts = ejabberd_option:hosts(), + case lists:member(From#jid.lserver, Local_hosts) of + true -> local; + false -> remote + end. + +iq_disco_info_extras2(SenderT, SLimits) -> + Limits = get_slimit_group(SenderT, SLimits), + Stanza_types = [message, presence], + lists:foldl(fun (Type_of_stanza, R) -> + case get_limit_number(Type_of_stanza, Limits) of + {custom, Number} -> + [?RFIELDV((to_binary(Type_of_stanza)), + (to_binary(Number))) + | R]; + {default, _} -> R + end + end, + [], Stanza_types). + +to_binary(A) -> list_to_binary(hd(io_lib:format("~p", [A]))). + +%%%------------------------- +%%% Error report +%%%------------------------- + +route_error(Packet, ErrType, ErrText) -> + Lang = xmpp:get_lang(Packet), + Err = make_reply(ErrType, Lang, ErrText), + ejabberd_router:route_error(Packet, Err). + +make_reply(bad_request, Lang, ErrText) -> + xmpp:err_bad_request(ErrText, Lang); +make_reply(jid_malformed, Lang, ErrText) -> + xmpp:err_jid_malformed(ErrText, Lang); +make_reply(not_acceptable, Lang, ErrText) -> + xmpp:err_not_acceptable(ErrText, Lang); +make_reply(internal_server_error, Lang, ErrText) -> + xmpp:err_internal_server_error(ErrText, Lang); +make_reply(forbidden, Lang, ErrText) -> + xmpp:err_forbidden(ErrText, Lang). + +stj(String) -> jid:decode(String). + +jts(String) -> jid:encode(String). + +depends(_Host, _Opts) -> + []. + +mod_opt_type(access) -> + econf:acl(); +mod_opt_type(name) -> + econf:binary(); +mod_opt_type(limits) -> + econf:options( + #{local => + econf:options( + #{message => econf:non_neg_int(infinite), + presence => econf:non_neg_int(infinite)}), + remote => + econf:options( + #{message => econf:non_neg_int(infinite), + presence => econf:non_neg_int(infinite)})}); +mod_opt_type(host) -> + econf:host(); +mod_opt_type(hosts) -> + econf:hosts(); +mod_opt_type(vcard) -> + econf:vcard_temp(). + +mod_options(Host) -> + [{access, all}, + {host, <<"multicast.", Host/binary>>}, + {hosts, []}, + {limits, [{local, []}, {remote, []}]}, + {vcard, undefined}, + {name, ?T("Multicast")}]. + +mod_doc() -> + #{desc => + [?T("This module implements a service for " + "https://xmpp.org/extensions/xep-0033.html" + "[XEP-0033: Extended Stanza Addressing].")], + opts => + [{access, + #{value => "Access", + desc => + ?T("The access rule to restrict who can send packets to " + "the multicast service. Default value: 'all'.")}}, + {host, + #{desc => ?T("Deprecated. Use 'hosts' instead.")}}, + {hosts, + #{value => ?T("[Host, ...]"), + desc => + [?T("This option defines the Jabber IDs of the service. " + "If the 'hosts' option is not specified, the only " + "Jabber ID will be the hostname of the virtual host " + "with the prefix \"multicast.\". The keyword '@HOST@' " + "is replaced with the real virtual host name."), + ?T("The default value is 'multicast.@HOST@'.")]}}, + {limits, + #{value => "Sender: Stanza: Number", + desc => + [?T("Specify a list of custom limits which override the " + "default ones defined in XEP-0033. Limits are defined " + "per sender type and stanza type, where:"), "", + ?T("- 'sender' can be: 'local' or 'remote'."), + ?T("- 'stanza' can be: 'message' or 'presence'."), + ?T("- 'number' can be a positive integer or 'infinite'.")], + example => + ["# Default values:", + "local:", + " message: 100", + " presence: 100", + "remote:", + " message: 20", + " presence: 20"] + }}, + {name, + #{desc => ?T("Service name to provide in the Info query to the " + "Service Discovery. Default is '\"Multicast\"'.")}}, + {vcard, + #{desc => ?T("vCard element to return when queried. " + "Default value is 'undefined'.")}}], + example => + ["# Only admins can send packets to multicast service", + "access_rules:", + " multicast:", + " - allow: admin", + "", + "# If you want to allow all your users:", + "access_rules:", + " multicast:", + " - allow", + "", + "# This allows both admins and remote users to send packets,", + "# but does not allow local users", + "acl:", + " allservers:", + " server_glob: \"*\"", + "access_rules:", + " multicast:", + " - allow: admin", + " - deny: local", + " - allow: allservers", + "", + "modules:", + " mod_multicast:", + " host: multicast.example.org", + " access: multicast", + " limits:", + " local:", + " message: 40", + " presence: infinite", + " remote:", + " message: 150"]}. diff --git a/src/mod_multicast_opt.erl b/src/mod_multicast_opt.erl new file mode 100644 index 000000000..bdf709803 --- /dev/null +++ b/src/mod_multicast_opt.erl @@ -0,0 +1,48 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_multicast_opt). + +-export([access/1]). +-export([host/1]). +-export([hosts/1]). +-export([limits/1]). +-export([name/1]). +-export([vcard/1]). + +-spec access(gen_mod:opts() | global | binary()) -> 'all' | acl:acl(). +access(Opts) when is_map(Opts) -> + gen_mod:get_opt(access, Opts); +access(Host) -> + gen_mod:get_module_opt(Host, mod_multicast, access). + +-spec host(gen_mod:opts() | global | binary()) -> binary(). +host(Opts) when is_map(Opts) -> + gen_mod:get_opt(host, Opts); +host(Host) -> + gen_mod:get_module_opt(Host, mod_multicast, host). + +-spec hosts(gen_mod:opts() | global | binary()) -> [binary()]. +hosts(Opts) when is_map(Opts) -> + gen_mod:get_opt(hosts, Opts); +hosts(Host) -> + gen_mod:get_module_opt(Host, mod_multicast, hosts). + +-spec limits(gen_mod:opts() | global | binary()) -> [{'local',[{'message','infinite' | non_neg_integer()} | {'presence','infinite' | non_neg_integer()}]} | {'remote',[{'message','infinite' | non_neg_integer()} | {'presence','infinite' | non_neg_integer()}]}]. +limits(Opts) when is_map(Opts) -> + gen_mod:get_opt(limits, Opts); +limits(Host) -> + gen_mod:get_module_opt(Host, mod_multicast, limits). + +-spec name(gen_mod:opts() | global | binary()) -> binary(). +name(Opts) when is_map(Opts) -> + gen_mod:get_opt(name, Opts); +name(Host) -> + gen_mod:get_module_opt(Host, mod_multicast, name). + +-spec vcard(gen_mod:opts() | global | binary()) -> 'undefined' | tuple(). +vcard(Opts) when is_map(Opts) -> + gen_mod:get_opt(vcard, Opts); +vcard(Host) -> + gen_mod:get_module_opt(Host, mod_multicast, vcard). + diff --git a/src/mod_offline.erl b/src/mod_offline.erl index b0582bc20..32277ba7d 100644 --- a/src/mod_offline.erl +++ b/src/mod_offline.erl @@ -5,7 +5,7 @@ %%% Created : 5 Jan 2003 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -26,1106 +26,1260 @@ -module(mod_offline). -author('alexey@process-one.net'). --define(GEN_SERVER, p1_server). --behaviour(?GEN_SERVER). + +-protocol({xep, 13, '1.2', '16.02', "complete", ""}). +-protocol({xep, 22, '1.4', '0.1.0', "complete", ""}). +-protocol({xep, 23, '1.3', '0.7.5', "complete", ""}). +-protocol({xep, 160, '1.0', '16.01', "complete", ""}). +-protocol({xep, 203, '2.0', '2.1.0', "complete", ""}). +-protocol({xep, 334, '0.2', '16.01', "complete", ""}). -behaviour(gen_mod). --export([count_offline_messages/2]). - -export([start/2, - start_link/2, stop/1, - store_packet/3, - resend_offline_messages/2, - pop_offline_messages/3, + reload/3, + store_packet/1, + store_offline_msg/1, + c2s_self_presence/1, get_sm_features/5, + get_sm_identity/5, + get_sm_items/5, + get_info/5, + handle_offline_query/1, remove_expired_messages/1, remove_old_messages/2, remove_user/2, - import/1, - import/3, - export/1, + import_info/0, + import_start/2, + import/5, + export/1, get_queue_length/2, - get_offline_els/2, - webadmin_page/3, + count_offline_messages/2, + get_offline_els/2, + find_x_expire/2, + c2s_handle_info/2, + c2s_copy_session/2, + get_offline_messages/2, + webadmin_menu_hostuser/4, + webadmin_page_hostuser/4, webadmin_user/4, - webadmin_user_parse_query/5]). + webadmin_user_parse_query/5, + c2s_handle_bind2_inline/1]). -%% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, - handle_info/2, terminate/2, code_change/3]). +-export([mod_opt_type/1, mod_options/1, mod_doc/0, depends/2]). + +-import(ejabberd_web_admin, [make_command/4, make_command/2]). + +-deprecated({get_queue_length,2}). --include("ejabberd.hrl"). -include("logger.hrl"). --include("jlib.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). -include("ejabberd_http.hrl"). -include("ejabberd_web_admin.hrl"). --record(offline_msg, - {us = {<<"">>, <<"">>} :: {binary(), binary()}, - timestamp = now() :: erlang:timestamp() | '_', - expire = now() :: erlang:timestamp() | never | '_', - from = #jid{} :: jid() | '_', - to = #jid{} :: jid() | '_', - packet = #xmlel{} :: xmlel() | '_'}). +-include("mod_offline.hrl"). --record(state, - {host = <<"">> :: binary(), - access_max_offline_messages}). - --define(PROCNAME, ejabberd_offline). - --define(OFFLINE_TABLE_LOCK_THRESHOLD, 1000). +-include("translate.hrl"). %% default value for the maximum number of user messages -define(MAX_USER_MESSAGES, infinity). -start_link(Host, Opts) -> - Proc = gen_mod:get_module_proc(Host, ?PROCNAME), - ?GEN_SERVER:start_link({local, Proc}, ?MODULE, - [Host, Opts], []). +-define(SPOOL_COUNTER_CACHE, offline_msg_counter_cache). + +-type c2s_state() :: ejabberd_c2s:state(). + +-callback init(binary(), gen_mod:opts()) -> any(). +-callback import(#offline_msg{}) -> ok. +-callback store_message(#offline_msg{}) -> ok | {error, any()}. +-callback pop_messages(binary(), binary()) -> + {ok, [#offline_msg{}]} | {error, any()}. +-callback remove_expired_messages(binary()) -> {atomic, any()}. +-callback remove_old_messages(non_neg_integer(), binary()) -> {atomic, any()}. +-callback remove_user(binary(), binary()) -> any(). +-callback read_message_headers(binary(), binary()) -> + [{non_neg_integer(), jid(), jid(), undefined | erlang:timestamp(), xmlel()}] | error. +-callback read_message(binary(), binary(), non_neg_integer()) -> + {ok, #offline_msg{}} | error. +-callback remove_message(binary(), binary(), non_neg_integer()) -> ok | {error, any()}. +-callback read_all_messages(binary(), binary()) -> [#offline_msg{}]. +-callback remove_all_messages(binary(), binary()) -> {atomic, any()}. +-callback count_messages(binary(), binary()) -> {ets_cache:tag(), non_neg_integer()}. +-callback use_cache(binary()) -> boolean(). +-callback cache_nodes(binary()) -> [node()]. +-callback remove_old_messages_batch(binary(), non_neg_integer(), pos_integer()) -> + {ok, non_neg_integer()} | {error, term()}. +-callback remove_old_messages_batch(binary(), non_neg_integer(), pos_integer(), any()) -> + {ok, any(), non_neg_integer()} | {error, term()}. + +-optional_callbacks([remove_expired_messages/1, remove_old_messages/2, + use_cache/1, cache_nodes/1, remove_old_messages_batch/3, + remove_old_messages_batch/4]). + +depends(_Host, _Opts) -> + []. start(Host, Opts) -> - Proc = gen_mod:get_module_proc(Host, ?PROCNAME), - ChildSpec = {Proc, {?MODULE, start_link, [Host, Opts]}, - temporary, 1000, worker, [?MODULE]}, - supervisor:start_child(ejabberd_sup, ChildSpec). + Mod = gen_mod:db_mod(Opts, ?MODULE), + Mod:init(Host, Opts), + init_cache(Mod, Host, Opts), + {ok, [{hook, offline_message_hook, store_packet, 50}, + {hook, c2s_self_presence, c2s_self_presence, 50}, + {hook, remove_user, remove_user, 50}, + {hook, disco_sm_features, get_sm_features, 50}, + {hook, disco_local_features, get_sm_features, 50}, + {hook, disco_sm_identity, get_sm_identity, 50}, + {hook, disco_sm_items, get_sm_items, 50}, + {hook, disco_info, get_info, 50}, + {hook, c2s_handle_info, c2s_handle_info, 50}, + {hook, c2s_copy_session, c2s_copy_session, 50}, + {hook, c2s_handle_bind2_inline, c2s_handle_bind2_inline, 50}, + {hook, webadmin_menu_hostuser, webadmin_menu_hostuser, 50}, + {hook, webadmin_page_hostuser, webadmin_page_hostuser, 50}, + {hook, webadmin_user, webadmin_user, 50}, + {hook, webadmin_user_parse_query, webadmin_user_parse_query, 50}, + {iq_handler, ejabberd_sm, ?NS_FLEX_OFFLINE, handle_offline_query}]}. -stop(Host) -> - Proc = gen_mod:get_module_proc(Host, ?PROCNAME), - ?GEN_SERVER:call(Proc, stop), - supervisor:delete_child(ejabberd_sup, Proc), +stop(_Host) -> ok. - -%%==================================================================== -%% gen_server callbacks -%%==================================================================== - -init([Host, Opts]) -> - case gen_mod:db_type(Opts) of - mnesia -> - mnesia:create_table(offline_msg, - [{disc_only_copies, [node()]}, {type, bag}, - {attributes, record_info(fields, offline_msg)}]), - update_table(); - _ -> ok - end, - ejabberd_hooks:add(offline_message_hook, Host, ?MODULE, - store_packet, 50), - ejabberd_hooks:add(resend_offline_messages_hook, Host, - ?MODULE, pop_offline_messages, 50), - ejabberd_hooks:add(remove_user, Host, - ?MODULE, remove_user, 50), - ejabberd_hooks:add(anonymous_purge_hook, Host, - ?MODULE, remove_user, 50), - ejabberd_hooks:add(disco_sm_features, Host, - ?MODULE, get_sm_features, 50), - ejabberd_hooks:add(disco_local_features, Host, - ?MODULE, get_sm_features, 50), - ejabberd_hooks:add(webadmin_page_host, Host, - ?MODULE, webadmin_page, 50), - ejabberd_hooks:add(webadmin_user, Host, - ?MODULE, webadmin_user, 50), - ejabberd_hooks:add(webadmin_user_parse_query, Host, - ?MODULE, webadmin_user_parse_query, 50), - AccessMaxOfflineMsgs = gen_mod:get_opt(access_max_user_messages, Opts, fun(A) -> A end, max_user_offline_messages), - {ok, - #state{host = Host, - access_max_offline_messages = AccessMaxOfflineMsgs}}. - - -handle_call(stop, _From, State) -> - {stop, normal, ok, State}. - - -handle_cast(_Msg, State) -> {noreply, State}. - - -handle_info(#offline_msg{us = UserServer} = Msg, State) -> - #state{host = Host, - access_max_offline_messages = AccessMaxOfflineMsgs} = State, - DBType = gen_mod:db_type(Host, ?MODULE), - Msgs = receive_all(UserServer, [Msg], DBType), - Len = length(Msgs), - MaxOfflineMsgs = get_max_user_messages(AccessMaxOfflineMsgs, - UserServer, Host), - store_offline_msg(Host, UserServer, Msgs, Len, MaxOfflineMsgs, DBType), - {noreply, State}; - -handle_info(_Info, State) -> - ?ERROR_MSG("got unexpected info: ~p", [_Info]), - {noreply, State}. - - -terminate(_Reason, State) -> - Host = State#state.host, - ejabberd_hooks:delete(offline_message_hook, Host, - ?MODULE, store_packet, 50), - ejabberd_hooks:delete(resend_offline_messages_hook, - Host, ?MODULE, pop_offline_messages, 50), - ejabberd_hooks:delete(remove_user, Host, ?MODULE, - remove_user, 50), - ejabberd_hooks:delete(anonymous_purge_hook, Host, - ?MODULE, remove_user, 50), - ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, get_sm_features, 50), - ejabberd_hooks:delete(disco_local_features, Host, ?MODULE, get_sm_features, 50), - ejabberd_hooks:delete(webadmin_page_host, Host, - ?MODULE, webadmin_page, 50), - ejabberd_hooks:delete(webadmin_user, Host, - ?MODULE, webadmin_user, 50), - ejabberd_hooks:delete(webadmin_user_parse_query, Host, - ?MODULE, webadmin_user_parse_query, 50), - ok. - - -code_change(_OldVsn, State, _Extra) -> {ok, State}. - - -store_offline_msg(_Host, US, Msgs, Len, MaxOfflineMsgs, - mnesia) -> - F = fun () -> - Count = if MaxOfflineMsgs =/= infinity -> - Len + count_mnesia_records(US); - true -> 0 - end, - if Count > MaxOfflineMsgs -> discard_warn_sender(Msgs); - true -> - if Len >= (?OFFLINE_TABLE_LOCK_THRESHOLD) -> - mnesia:write_lock_table(offline_msg); - true -> ok - end, - lists:foreach(fun (M) -> mnesia:write(M) end, Msgs) - end - end, - mnesia:transaction(F); -store_offline_msg(Host, {User, _Server}, Msgs, Len, MaxOfflineMsgs, odbc) -> - Count = if MaxOfflineMsgs =/= infinity -> - Len + count_offline_messages(User, Host); - true -> 0 - end, - if Count > MaxOfflineMsgs -> discard_warn_sender(Msgs); +reload(Host, NewOpts, OldOpts) -> + NewMod = gen_mod:db_mod(NewOpts, ?MODULE), + OldMod = gen_mod:db_mod(OldOpts, ?MODULE), + init_cache(NewMod, Host, NewOpts), + if NewMod /= OldMod -> + NewMod:init(Host, NewOpts); true -> - Query = lists:map(fun (M) -> - Username = - ejabberd_odbc:escape((M#offline_msg.to)#jid.luser), - From = M#offline_msg.from, - To = M#offline_msg.to, - Packet = - jlib:replace_from_to(From, To, - M#offline_msg.packet), - NewPacket = - jlib:add_delay_info(Packet, Host, - M#offline_msg.timestamp, - <<"Offline Storage">>), - XML = - ejabberd_odbc:escape(xml:element_to_binary(NewPacket)), - odbc_queries:add_spool_sql(Username, XML) - end, - Msgs), - odbc_queries:add_spool(Host, Query) - end; -store_offline_msg(Host, {User, _}, Msgs, Len, MaxOfflineMsgs, - riak) -> - Count = if MaxOfflineMsgs =/= infinity -> - Len + count_offline_messages(User, Host); - true -> 0 - end, - if - Count > MaxOfflineMsgs -> - discard_warn_sender(Msgs); - true -> - lists:foreach( - fun(#offline_msg{us = US, - timestamp = TS} = M) -> - ejabberd_riak:put(M, offline_msg_schema(), - [{i, TS}, {'2i', [{<<"us">>, US}]}]) - end, Msgs) + ok end. -%% Function copied from ejabberd_sm.erl: -get_max_user_messages(AccessRule, {User, Server}, Host) -> - case acl:match_rule( - Host, AccessRule, jlib:make_jid(User, Server, <<"">>)) of +init_cache(Mod, Host, Opts) -> + CacheOpts = [{max_size, mod_offline_opt:cache_size(Opts)}, + {life_time, mod_offline_opt:cache_life_time(Opts)}, + {cache_missed, false}], + case use_cache(Mod, Host) of + true -> + ets_cache:new(?SPOOL_COUNTER_CACHE, CacheOpts); + false -> + ets_cache:delete(?SPOOL_COUNTER_CACHE) + end. + +-spec use_cache(module(), binary()) -> boolean(). +use_cache(Mod, Host) -> + case erlang:function_exported(Mod, use_cache, 1) of + true -> Mod:use_cache(Host); + false -> mod_offline_opt:use_cache(Host) + end. + +-spec cache_nodes(module(), binary()) -> [node()]. +cache_nodes(Mod, Host) -> + case erlang:function_exported(Mod, cache_nodes, 1) of + true -> Mod:cache_nodes(Host); + false -> ejabberd_cluster:get_nodes() + end. + +-spec flush_cache(module(), binary(), binary()) -> ok. +flush_cache(Mod, User, Server) -> + case use_cache(Mod, Server) of + true -> + ets_cache:delete(?SPOOL_COUNTER_CACHE, + {User, Server}, + cache_nodes(Mod, Server)); + false -> + ok + end. + +-spec store_offline_msg(#offline_msg{}) -> ok | {error, full | any()}. +store_offline_msg(#offline_msg{us = {User, Server}, packet = Pkt} = Msg) -> + UseMam = use_mam_for_user(User, Server), + Mod = gen_mod:db_mod(Server, ?MODULE), + case UseMam andalso xmpp:get_meta(Pkt, mam_archived, false) of + true -> + case count_offline_messages(User, Server) of + 0 -> + store_message_in_db(Mod, Msg); + _ -> + case use_cache(Mod, Server) of + true -> + ets_cache:incr( + ?SPOOL_COUNTER_CACHE, + {User, Server}, 1, + cache_nodes(Mod, Server)); + false -> + ok + end + end; + false -> + case get_max_user_messages(User, Server) of + infinity -> + store_message_in_db(Mod, Msg); + Limit -> + Num = count_offline_messages(User, Server), + if Num < Limit -> + store_message_in_db(Mod, Msg); + true -> + {error, full} + end + end + end. + +get_max_user_messages(User, Server) -> + Access = mod_offline_opt:access_max_user_messages(Server), + case ejabberd_shaper:match(Server, Access, jid:make(User, Server)) of Max when is_integer(Max) -> Max; infinity -> infinity; _ -> ?MAX_USER_MESSAGES end. -receive_all(US, Msgs, DBType) -> - receive - #offline_msg{us = US} = Msg -> - receive_all(US, [Msg | Msgs], DBType) - after 0 -> - case DBType of - mnesia -> Msgs; - odbc -> lists:reverse(Msgs); - riak -> Msgs - end - end. - get_sm_features(Acc, _From, _To, <<"">>, _Lang) -> Feats = case Acc of {result, I} -> I; _ -> [] end, - {result, Feats ++ [?NS_FEATURE_MSGOFFLINE]}; + {result, Feats ++ [?NS_FEATURE_MSGOFFLINE, ?NS_FLEX_OFFLINE]}; get_sm_features(_Acc, _From, _To, ?NS_FEATURE_MSGOFFLINE, _Lang) -> %% override all lesser features... {result, []}; +get_sm_features(_Acc, #jid{luser = U, lserver = S}, #jid{luser = U, lserver = S}, + ?NS_FLEX_OFFLINE, _Lang) -> + {result, [?NS_FLEX_OFFLINE]}; + get_sm_features(Acc, _From, _To, _Node, _Lang) -> Acc. -need_to_store(LServer, Packet) -> - Type = xml:get_tag_attr_s(<<"type">>, Packet), - if (Type /= <<"error">>) and (Type /= <<"groupchat">>) - and (Type /= <<"headline">>) -> - case gen_mod:get_module_opt( - LServer, ?MODULE, store_empty_body, - fun(V) when is_boolean(V) -> V end, - true) of - false -> - xml:get_subtag(Packet, <<"body">>) /= false; - true -> - true +get_sm_identity(Acc, #jid{luser = U, lserver = S}, #jid{luser = U, lserver = S}, + ?NS_FLEX_OFFLINE, _Lang) -> + [#identity{category = <<"automation">>, + type = <<"message-list">>}|Acc]; +get_sm_identity(Acc, _From, _To, _Node, _Lang) -> + Acc. + +get_sm_items(_Acc, #jid{luser = U, lserver = S} = JID, + #jid{luser = U, lserver = S}, + ?NS_FLEX_OFFLINE, _Lang) -> + ejabberd_sm:route(JID, {resend_offline, false}), + Mod = gen_mod:db_mod(S, ?MODULE), + Hdrs = case Mod:read_message_headers(U, S) of + L when is_list(L) -> + L; + _ -> + [] + end, + BareJID = jid:remove_resource(JID), + {result, lists:map( + fun({Seq, From, _To, _TS, _El}) -> + Node = integer_to_binary(Seq), + #disco_item{jid = BareJID, + node = Node, + name = jid:encode(From)} + end, Hdrs)}; +get_sm_items(Acc, _From, _To, _Node, _Lang) -> + Acc. + +-spec get_info([xdata()], binary(), module(), binary(), binary()) -> [xdata()]; + ([xdata()], jid(), jid(), binary(), binary()) -> [xdata()]. +get_info(_Acc, #jid{luser = U, lserver = S} = JID, + #jid{luser = U, lserver = S}, ?NS_FLEX_OFFLINE, Lang) -> + ejabberd_sm:route(JID, {resend_offline, false}), + [#xdata{type = result, + fields = flex_offline:encode( + [{number_of_messages, count_offline_messages(U, S)}], + Lang)}]; +get_info(Acc, _From, _To, _Node, _Lang) -> + Acc. + +-spec c2s_handle_info(c2s_state(), term()) -> c2s_state(). +c2s_handle_info(State, {resend_offline, Flag}) -> + {stop, State#{resend_offline => Flag}}; +c2s_handle_info(State, _) -> + State. + +-spec c2s_copy_session(c2s_state(), c2s_state()) -> c2s_state(). +c2s_copy_session(State, #{resend_offline := Flag}) -> + State#{resend_offline => Flag}; +c2s_copy_session(State, _) -> + State. + +c2s_handle_bind2_inline({#{jid := #jid{luser = LUser, lserver = LServer}} = State, Els, Results}) -> + case mod_mam:is_archiving_enabled(LUser, LServer) of + true -> + delete_all_msgs(LUser, LServer); + false -> + ok + end, + {State, Els, Results}. + +-spec handle_offline_query(iq()) -> iq(). +handle_offline_query(#iq{from = #jid{luser = U1, lserver = S1}, + to = #jid{luser = U2, lserver = S2}, + lang = Lang, + sub_els = [#offline{}]} = IQ) + when {U1, S1} /= {U2, S2} -> + Txt = ?T("Query to another users is forbidden"), + xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)); +handle_offline_query(#iq{from = #jid{luser = U, lserver = S} = From, + to = #jid{luser = U, lserver = S} = _To, + type = Type, lang = Lang, + sub_els = [#offline{} = Offline]} = IQ) -> + case {Type, Offline} of + {get, #offline{fetch = true, items = [], purge = false}} -> + %% TODO: report database errors + handle_offline_fetch(From), + xmpp:make_iq_result(IQ); + {get, #offline{fetch = false, items = [_|_] = Items, purge = false}} -> + case handle_offline_items_view(From, Items) of + true -> xmpp:make_iq_result(IQ); + false -> xmpp:make_error(IQ, xmpp:err_item_not_found()) end; - true -> + {set, #offline{fetch = false, items = [], purge = true}} -> + case delete_all_msgs(U, S) of + {atomic, ok} -> + xmpp:make_iq_result(IQ); + _Err -> + Txt = ?T("Database failure"), + xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) + end; + {set, #offline{fetch = false, items = [_|_] = Items, purge = false}} -> + case handle_offline_items_remove(From, Items) of + true -> xmpp:make_iq_result(IQ); + false -> xmpp:make_error(IQ, xmpp:err_item_not_found()) + end; + _ -> + xmpp:make_error(IQ, xmpp:err_bad_request()) + end; +handle_offline_query(#iq{lang = Lang} = IQ) -> + Txt = ?T("No module is handling this query"), + xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)). + +-spec handle_offline_items_view(jid(), [offline_item()]) -> boolean(). +handle_offline_items_view(JID, Items) -> + {U, S, R} = jid:tolower(JID), + case use_mam_for_user(U, S) of + true -> + false; + _ -> + lists:foldl( + fun(#offline_item{node = Node, action = view}, Acc) -> + case fetch_msg_by_node(JID, Node) of + {ok, OfflineMsg} -> + case offline_msg_to_route(S, OfflineMsg) of + {route, El} -> + NewEl = set_offline_tag(El, Node), + case ejabberd_sm:get_session_pid(U, S, R) of + Pid when is_pid(Pid) -> + ejabberd_c2s:route(Pid, {route, NewEl}); + none -> + ok + end, + Acc or true; + error -> + Acc or false + end; + error -> + Acc or false + end + end, false, Items) end. + +-spec handle_offline_items_remove(jid(), [offline_item()]) -> boolean(). +handle_offline_items_remove(JID, Items) -> + {U, S, _R} = jid:tolower(JID), + case use_mam_for_user(U, S) of + true -> + false; + _ -> + lists:foldl( + fun(#offline_item{node = Node, action = remove}, Acc) -> + Acc or remove_msg_by_node(JID, Node) + end, false, Items) + end. + +-spec set_offline_tag(message(), binary()) -> message(). +set_offline_tag(Msg, Node) -> + xmpp:set_subtag(Msg, #offline{items = [#offline_item{node = Node}]}). + +-spec handle_offline_fetch(jid()) -> ok. +handle_offline_fetch(#jid{luser = U, lserver = S} = JID) -> + ejabberd_sm:route(JID, {resend_offline, false}), + lists:foreach( + fun({Node, El}) -> + El1 = set_offline_tag(El, Node), + ejabberd_router:route(El1) + end, read_messages(U, S)). + +-spec fetch_msg_by_node(jid(), binary()) -> error | {ok, #offline_msg{}}. +fetch_msg_by_node(To, Seq) -> + case catch binary_to_integer(Seq) of + I when is_integer(I), I >= 0 -> + LUser = To#jid.luser, + LServer = To#jid.lserver, + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:read_message(LUser, LServer, I); + _ -> + error + end. + +-spec remove_msg_by_node(jid(), binary()) -> boolean(). +remove_msg_by_node(To, Seq) -> + case catch binary_to_integer(Seq) of + I when is_integer(I), I>= 0 -> + LUser = To#jid.luser, + LServer = To#jid.lserver, + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:remove_message(LUser, LServer, I), + flush_cache(Mod, LUser, LServer), + true; + _ -> false end. -store_packet(From, To, Packet) -> +-spec need_to_store(binary(), message()) -> boolean(). +need_to_store(_LServer, #message{type = error}) -> false; +need_to_store(LServer, #message{type = Type} = Packet) -> + case xmpp:has_subtag(Packet, #offline{}) of + false -> + case misc:unwrap_mucsub_message(Packet) of + #message{type = groupchat} = Msg -> + need_to_store(LServer, Msg#message{type = chat}); + #message{} = Msg -> + need_to_store(LServer, Msg); + _ -> + case check_store_hint(Packet) of + store -> + true; + no_store -> + false; + none -> + Store = case Type of + groupchat -> + mod_offline_opt:store_groupchat(LServer); + headline -> + false; + _ -> + true + end, + case {misc:get_mucsub_event_type(Packet), Store, + mod_offline_opt:store_empty_body(LServer)} of + {?NS_MUCSUB_NODES_PRESENCE, _, _} -> + false; + {_, false, _} -> + false; + {_, _, true} -> + true; + {_, _, false} -> + Packet#message.body /= []; + {_, _, unless_chat_state} -> + not misc:is_standalone_chat_state(Packet) + end + end + end; + true -> + false + end. + +-spec store_packet({any(), message()}) -> {any(), message()}. +store_packet({_Action, #message{from = From, to = To} = Packet} = Acc) -> case need_to_store(To#jid.lserver, Packet) of true -> - case has_no_storage_hint(Packet) of - false -> - case check_event(From, To, Packet) of - true -> - #jid{luser = LUser, lserver = LServer} = To, - TimeStamp = now(), - #xmlel{children = Els} = Packet, - Expire = find_x_expire(TimeStamp, Els), - gen_mod:get_module_proc(To#jid.lserver, ?PROCNAME) ! - #offline_msg{us = {LUser, LServer}, - timestamp = TimeStamp, expire = Expire, - from = From, to = To, packet = Packet}, - stop; - _ -> ok - end; - _ -> ok - end; - false -> ok + case check_event(Packet) of + true -> + #jid{luser = LUser, lserver = LServer} = To, + TimeStamp = erlang:timestamp(), + Expire = find_x_expire(TimeStamp, Packet), + OffMsg = #offline_msg{us = {LUser, LServer}, + timestamp = TimeStamp, + expire = Expire, + from = From, + to = To, + packet = Packet}, + case store_offline_msg(OffMsg) of + ok -> + {offlined, Packet}; + {error, Reason} -> + discard_warn_sender(Packet, Reason), + stop + end; + _ -> + maybe_update_cache(To, Packet), + Acc + end; + false -> + maybe_update_cache(To, Packet), + Acc end. -has_no_storage_hint(Packet) -> - case xml:get_subtag(Packet, <<"no-store">>) of - #xmlel{attrs = Attrs} -> - case xml:get_attr_s(<<"xmlns">>, Attrs) of - ?NS_HINTS -> - true; - _ -> - false - end; - _ -> - false - end. - -%% Check if the packet has any content about XEP-0022 or XEP-0085 -check_event(From, To, Packet) -> - #xmlel{name = Name, attrs = Attrs, children = Els} = - Packet, - case find_x_event(Els) of - false -> true; - El -> - case xml:get_subtag(El, <<"id">>) of - false -> - case xml:get_subtag(El, <<"offline">>) of - false -> true; - _ -> - ID = case xml:get_tag_attr_s(<<"id">>, Packet) of - <<"">> -> - #xmlel{name = <<"id">>, attrs = [], - children = []}; - S -> - #xmlel{name = <<"id">>, attrs = [], - children = [{xmlcdata, S}]} - end, - ejabberd_router:route(To, From, - #xmlel{name = Name, attrs = Attrs, - children = - [#xmlel{name = <<"x">>, - attrs = - [{<<"xmlns">>, - ?NS_EVENT}], - children = - [ID, - #xmlel{name - = - <<"offline">>, - attrs - = - [], - children - = - []}]}]}), - true - end; - _ -> false - end - end. - -%% Check if the packet has subelements about XEP-0022, XEP-0085 or other -find_x_event([]) -> false; -find_x_event([{xmlcdata, _} | Els]) -> - find_x_event(Els); -find_x_event([El | Els]) -> - case xml:get_tag_attr_s(<<"xmlns">>, El) of - ?NS_EVENT -> El; - _ -> find_x_event(Els) - end. - -find_x_expire(_, []) -> never; -find_x_expire(TimeStamp, [{xmlcdata, _} | Els]) -> - find_x_expire(TimeStamp, Els); -find_x_expire(TimeStamp, [El | Els]) -> - case xml:get_tag_attr_s(<<"xmlns">>, El) of - ?NS_EXPIRE -> - Val = xml:get_tag_attr_s(<<"seconds">>, El), - case catch jlib:binary_to_integer(Val) of - {'EXIT', _} -> never; - Int when Int > 0 -> - {MegaSecs, Secs, MicroSecs} = TimeStamp, - S = MegaSecs * 1000000 + Secs + Int, - MegaSecs1 = S div 1000000, - Secs1 = S rem 1000000, - {MegaSecs1, Secs1, MicroSecs}; - _ -> never - end; - _ -> find_x_expire(TimeStamp, Els) - end. - -resend_offline_messages(User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - US = {LUser, LServer}, - F = fun () -> - Rs = mnesia:wread({offline_msg, US}), - mnesia:delete({offline_msg, US}), - Rs - end, - case mnesia:transaction(F) of - {atomic, Rs} -> - lists:foreach(fun (R) -> - ejabberd_sm ! - {route, R#offline_msg.from, R#offline_msg.to, - jlib:add_delay_info(R#offline_msg.packet, - LServer, - R#offline_msg.timestamp, - <<"Offline Storage">>)} - end, - lists:keysort(#offline_msg.timestamp, Rs)); - _ -> ok - end. - -pop_offline_messages(Ls, User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - pop_offline_messages(Ls, LUser, LServer, - gen_mod:db_type(LServer, ?MODULE)). - -pop_offline_messages(Ls, LUser, LServer, mnesia) -> - US = {LUser, LServer}, - F = fun () -> - Rs = mnesia:wread({offline_msg, US}), - mnesia:delete({offline_msg, US}), - Rs - end, - case mnesia:transaction(F) of - {atomic, Rs} -> - TS = now(), - Ls ++ - lists:map(fun (R) -> - offline_msg_to_route(LServer, R) - end, - lists:filter(fun (R) -> - case R#offline_msg.expire of - never -> true; - TimeStamp -> TS < TimeStamp - end - end, - lists:keysort(#offline_msg.timestamp, Rs))); - _ -> Ls - end; -pop_offline_messages(Ls, LUser, LServer, odbc) -> - EUser = ejabberd_odbc:escape(LUser), - case odbc_queries:get_and_del_spool_msg_t(LServer, - EUser) - of - {atomic, {selected, [<<"username">>, <<"xml">>], Rs}} -> - Ls ++ - lists:flatmap(fun ([_, XML]) -> - case xml_stream:parse_element(XML) of - {error, _Reason} -> - []; - El -> - case offline_msg_to_route(LServer, El) of - error -> - []; - RouteMsg -> - [RouteMsg] - end - end - end, - Rs); - _ -> Ls - end; -pop_offline_messages(Ls, LUser, LServer, riak) -> - case ejabberd_riak:get_by_index(offline_msg, offline_msg_schema(), - <<"us">>, {LUser, LServer}) of - {ok, Rs} -> - try - lists:foreach( - fun(#offline_msg{timestamp = T}) -> - ok = ejabberd_riak:delete(offline_msg, T) - end, Rs), - TS = now(), - Ls ++ lists:map( - fun (R) -> - offline_msg_to_route(LServer, R) - end, - lists:filter( - fun(R) -> - case R#offline_msg.expire of - never -> true; - TimeStamp -> TS < TimeStamp - end - end, - lists:keysort(#offline_msg.timestamp, Rs))) - catch _:{badmatch, _} -> - Ls - end; +-spec maybe_update_cache(jid(), message()) -> ok. +maybe_update_cache(#jid{lserver = Server, luser = User}, Packet) -> + case xmpp:get_meta(Packet, mam_archived, false) of + true -> + Mod = gen_mod:db_mod(Server, ?MODULE), + case use_mam_for_user(User, Server) andalso use_cache(Mod, Server) of + true -> + ets_cache:incr( + ?SPOOL_COUNTER_CACHE, + {User, Server}, 1, + cache_nodes(Mod, Server)); + _ -> + ok + end; _ -> - Ls + ok end. +-spec check_store_hint(message()) -> store | no_store | none. +check_store_hint(Packet) -> + case has_store_hint(Packet) of + true -> + store; + false -> + case has_no_store_hint(Packet) of + true -> + no_store; + false -> + none + end + end. + +-spec has_store_hint(message()) -> boolean(). +has_store_hint(Packet) -> + xmpp:has_subtag(Packet, #hint{type = 'store'}). + +-spec has_no_store_hint(message()) -> boolean(). +has_no_store_hint(Packet) -> + xmpp:has_subtag(Packet, #hint{type = 'no-store'}) + orelse + xmpp:has_subtag(Packet, #hint{type = 'no-storage'}). + +%% Check if the packet has any content about XEP-0022 +-spec check_event(message()) -> boolean(). +check_event(#message{from = From, to = To, id = ID, type = Type} = Msg) -> + case xmpp:get_subtag(Msg, #xevent{}) of + false -> + true; + #xevent{id = undefined, offline = false} -> + true; + #xevent{id = undefined, offline = true} -> + NewMsg = #message{from = To, to = From, id = ID, type = Type, + sub_els = [#xevent{id = ID, offline = true}]}, + ejabberd_router:route(NewMsg), + true; + % Don't store composing events + #xevent{id = V, composing = true} when V /= undefined -> + false; + % Nor composing stopped events + #xevent{id = V, composing = false, delivered = false, + displayed = false, offline = false} when V /= undefined -> + false; + % But store other received notifications + #xevent{id = V} when V /= undefined -> + true; + _ -> + false + end. + +-spec find_x_expire(erlang:timestamp(), message()) -> erlang:timestamp() | never. +find_x_expire(TimeStamp, Msg) -> + case xmpp:get_subtag(Msg, #expire{seconds = 0}) of + #expire{seconds = Int} -> + {MegaSecs, Secs, MicroSecs} = TimeStamp, + S = MegaSecs * 1000000 + Secs + Int, + MegaSecs1 = S div 1000000, + Secs1 = S rem 1000000, + {MegaSecs1, Secs1, MicroSecs}; + false -> + never + end. + +c2s_self_presence({_Pres, #{resend_offline := false}} = Acc) -> + Acc; +c2s_self_presence({#presence{type = available} = NewPres, State} = Acc) -> + NewPrio = get_priority_from_presence(NewPres), + LastPrio = case maps:get(pres_last, State, undefined) of + undefined -> -1; + LastPres -> get_priority_from_presence(LastPres) + end, + if LastPrio < 0 andalso NewPrio >= 0 -> + route_offline_messages(State); + true -> + ok + end, + Acc; +c2s_self_presence(Acc) -> + Acc. + +-spec route_offline_messages(c2s_state()) -> ok. +route_offline_messages(#{jid := #jid{luser = LUser, lserver = LServer}} = State) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + Msgs = case Mod:pop_messages(LUser, LServer) of + {ok, OffMsgs} -> + case use_mam_for_user(LUser, LServer) of + true -> + flush_cache(Mod, LUser, LServer), + lists:map( + fun({_, #message{from = From, to = To} = Msg}) -> + #offline_msg{from = From, to = To, + us = {LUser, LServer}, + packet = Msg} + end, read_mam_messages(LUser, LServer, OffMsgs)); + _ -> + flush_cache(Mod, LUser, LServer), + OffMsgs + end; + _ -> + [] + end, + lists:foreach( + fun(OffMsg) -> + route_offline_message(State, OffMsg) + end, Msgs). + +-spec route_offline_message(c2s_state(), #offline_msg{}) -> ok. +route_offline_message(#{lserver := LServer} = State, + #offline_msg{expire = Expire} = OffMsg) -> + case offline_msg_to_route(LServer, OffMsg) of + error -> + ok; + {route, Msg} -> + case is_message_expired(Expire, Msg) of + true -> + ok; + false -> + case privacy_check_packet(State, Msg, in) of + allow -> ejabberd_router:route(Msg); + deny -> ok + end + end + end. + +-spec is_message_expired(erlang:timestamp() | never, message()) -> boolean(). +is_message_expired(Expire, Msg) -> + TS = erlang:timestamp(), + Expire1 = case Expire of + undefined -> find_x_expire(TS, Msg); + _ -> Expire + end, + Expire1 /= never andalso Expire1 =< TS. + +-spec privacy_check_packet(c2s_state(), stanza(), in | out) -> allow | deny. +privacy_check_packet(#{lserver := LServer} = State, Pkt, Dir) -> + ejabberd_hooks:run_fold(privacy_check_packet, + LServer, allow, [State, Pkt, Dir]). + remove_expired_messages(Server) -> - LServer = jlib:nameprep(Server), - remove_expired_messages(LServer, - gen_mod:db_type(LServer, ?MODULE)). - -remove_expired_messages(_LServer, mnesia) -> - TimeStamp = now(), - F = fun () -> - mnesia:write_lock_table(offline_msg), - mnesia:foldl(fun (Rec, _Acc) -> - case Rec#offline_msg.expire of - never -> ok; - TS -> - if TS < TimeStamp -> - mnesia:delete_object(Rec); - true -> ok - end - end - end, - ok, offline_msg) - end, - mnesia:transaction(F); -remove_expired_messages(_LServer, odbc) -> {atomic, ok}; -remove_expired_messages(_LServer, riak) -> {atomic, ok}. + LServer = jid:nameprep(Server), + Mod = gen_mod:db_mod(LServer, ?MODULE), + case erlang:function_exported(Mod, remove_expired_messages, 1) of + true -> + Ret = Mod:remove_expired_messages(LServer), + ets_cache:clear(?SPOOL_COUNTER_CACHE), + Ret; + false -> + erlang:error(not_implemented) + end. remove_old_messages(Days, Server) -> - LServer = jlib:nameprep(Server), - remove_old_messages(Days, LServer, - gen_mod:db_type(LServer, ?MODULE)). - -remove_old_messages(Days, _LServer, mnesia) -> - {MegaSecs, Secs, _MicroSecs} = now(), - S = MegaSecs * 1000000 + Secs - 60 * 60 * 24 * Days, - MegaSecs1 = S div 1000000, - Secs1 = S rem 1000000, - TimeStamp = {MegaSecs1, Secs1, 0}, - F = fun () -> - mnesia:write_lock_table(offline_msg), - mnesia:foldl(fun (#offline_msg{timestamp = TS} = Rec, - _Acc) - when TS < TimeStamp -> - mnesia:delete_object(Rec); - (_Rec, _Acc) -> ok - end, - ok, offline_msg) - end, - mnesia:transaction(F); -remove_old_messages(_Days, _LServer, odbc) -> - {atomic, ok}; -remove_old_messages(_Days, _LServer, riak) -> - {atomic, ok}. - -remove_user(User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - remove_user(LUser, LServer, - gen_mod:db_type(LServer, ?MODULE)). - -remove_user(LUser, LServer, mnesia) -> - US = {LUser, LServer}, - F = fun () -> mnesia:delete({offline_msg, US}) end, - mnesia:transaction(F); -remove_user(LUser, LServer, odbc) -> - Username = ejabberd_odbc:escape(LUser), - odbc_queries:del_spool_msg(LServer, Username); -remove_user(LUser, LServer, riak) -> - {atomic, ejabberd_riak:delete_by_index(offline_msg, - <<"us">>, {LUser, LServer})}. - -jid_to_binary(#jid{user = U, server = S, resource = R, - luser = LU, lserver = LS, lresource = LR}) -> - #jid{user = iolist_to_binary(U), - server = iolist_to_binary(S), - resource = iolist_to_binary(R), - luser = iolist_to_binary(LU), - lserver = iolist_to_binary(LS), - lresource = iolist_to_binary(LR)}. - -update_table() -> - Fields = record_info(fields, offline_msg), - case mnesia:table_info(offline_msg, attributes) of - Fields -> - ejabberd_config:convert_table_to_binary( - offline_msg, Fields, bag, - fun(#offline_msg{us = {U, _}}) -> U end, - fun(#offline_msg{us = {U, S}, - from = From, - to = To, - packet = El} = R) -> - R#offline_msg{us = {iolist_to_binary(U), - iolist_to_binary(S)}, - from = jid_to_binary(From), - to = jid_to_binary(To), - packet = xml:to_xmlel(El)} - end); - _ -> - ?INFO_MSG("Recreating offline_msg table", []), - mnesia:transform_table(offline_msg, ignore, Fields) + LServer = jid:nameprep(Server), + Mod = gen_mod:db_mod(LServer, ?MODULE), + case erlang:function_exported(Mod, remove_old_messages, 2) of + true -> + Ret = Mod:remove_old_messages(Days, LServer), + ets_cache:clear(?SPOOL_COUNTER_CACHE), + Ret; + false -> + erlang:error(not_implemented) end. +-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), + Mod:remove_user(LUser, LServer), + flush_cache(Mod, LUser, LServer). + %% Helper functions: -%% Warn senders that their messages have been discarded: -discard_warn_sender(Msgs) -> - lists:foreach(fun (#offline_msg{from = From, to = To, - packet = Packet}) -> - ErrText = <<"Your contact offline message queue is " - "full. The message has been discarded.">>, - Lang = xml:get_tag_attr_s(<<"xml:lang">>, Packet), - Err = jlib:make_error_reply(Packet, - ?ERRT_RESOURCE_CONSTRAINT(Lang, - ErrText)), - ejabberd_router:route(To, From, Err) - end, - Msgs). +-spec check_if_message_should_be_bounced(message()) -> boolean(). +check_if_message_should_be_bounced(Packet) -> + case Packet of + #message{type = groupchat, to = #jid{lserver = LServer}} -> + mod_offline_opt:bounce_groupchat(LServer); + #message{to = #jid{lserver = LServer}} -> + case misc:is_mucsub_message(Packet) of + true -> + mod_offline_opt:bounce_groupchat(LServer); + _ -> + true + end; + _ -> + true + end. -webadmin_page(_, Host, - #request{us = _US, path = [<<"user">>, U, <<"queue">>], - q = Query, lang = Lang} = - _Request) -> - Res = user_queue(U, Host, Query, Lang), {stop, Res}; -webadmin_page(Acc, _, _) -> Acc. +%% Warn senders that their messages have been discarded: + +-spec discard_warn_sender(message(), full | any()) -> ok. +discard_warn_sender(Packet, Reason) -> + case check_if_message_should_be_bounced(Packet) of + true -> + Lang = xmpp:get_lang(Packet), + Err = case Reason of + full -> + ErrText = ?T("Your contact offline message queue is " + "full. The message has been discarded."), + xmpp:err_resource_constraint(ErrText, Lang); + _ -> + ErrText = ?T("Database failure"), + xmpp:err_internal_server_error(ErrText, Lang) + end, + ejabberd_router:route_error(Packet, Err); + _ -> + ok + end. + +%%% +%%% Commands +%%% + +get_offline_messages(User, Server) -> + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + Mod = gen_mod:db_mod(LServer, ?MODULE), + HdrsAll = case Mod:read_message_headers(LUser, LServer) of + error -> []; + L -> L + end, + format_user_queue(HdrsAll). + +%%% +%%% WebAdmin +%%% + +webadmin_menu_hostuser(Acc, _Host, _Username, _Lang) -> + Acc ++ [{<<"queue">>, <<"Offline Queue">>}]. + +webadmin_page_hostuser(_, Host, U, + #request{us = _US, path = [<<"queue">> | RPath], + lang = Lang} = R) -> + US = {U, Host}, + PageTitle = str:translate_and_format(Lang, ?T("~ts's Offline Messages Queue"), [us_to_list(US)]), + Head = ?H1GL(PageTitle, <<"modules/#mod_offline">>, <<"mod_offline">>), + Res = make_command(get_offline_messages, R, [{<<"user">>, U}, + {<<"host">>, Host}], + [{table_options, {10, RPath}}, + {result_links, [{packet, paragraph, 1, <<"">>}]}]), + {stop, Head ++ [Res]}; +webadmin_page_hostuser(Acc, _, _, _) -> Acc. get_offline_els(LUser, LServer) -> - get_offline_els(LUser, LServer, gen_mod:db_type(LServer, ?MODULE)). + [Packet || {_Seq, Packet} <- read_messages(LUser, LServer)]. -get_offline_els(LUser, LServer, DBType) - when DBType == mnesia; DBType == riak -> - Msgs = read_all_msgs(LUser, LServer, DBType), - lists:map( - fun(Msg) -> - {route, From, To, Packet} = offline_msg_to_route(LServer, Msg), - jlib:replace_from_to(From, To, Packet) - end, Msgs); -get_offline_els(LUser, LServer, odbc) -> - Username = ejabberd_odbc:escape(LUser), - case catch ejabberd_odbc:sql_query(LServer, - [<<"select xml from spool where username='">>, - Username, <<"' order by seq;">>]) of - {selected, [<<"xml">>], Rs} -> - lists:flatmap( - fun([XML]) -> - case xml_stream:parse_element(XML) of - #xmlel{} = El -> - case offline_msg_to_route(LServer, El) of - {route, _, _, NewEl} -> - [NewEl]; - error -> - [] - end; - _ -> - [] - end - end, Rs); - _ -> - [] +-spec offline_msg_to_route(binary(), #offline_msg{}) -> + {route, message()} | error. +offline_msg_to_route(LServer, #offline_msg{from = From, to = To} = R) -> + CodecOpts = ejabberd_config:codec_options(), + try xmpp:decode(R#offline_msg.packet, ?NS_CLIENT, CodecOpts) of + Pkt -> + Pkt1 = xmpp:set_from_to(Pkt, From, To), + Pkt2 = add_delay_info(Pkt1, LServer, R#offline_msg.timestamp), + {route, Pkt2} + catch _:{xmpp_codec, Why} -> + ?ERROR_MSG("Failed to decode packet ~p of user ~ts: ~ts", + [R#offline_msg.packet, jid:encode(To), + xmpp:format_error(Why)]), + error end. -offline_msg_to_route(LServer, #offline_msg{} = R) -> - {route, R#offline_msg.from, R#offline_msg.to, - jlib:add_delay_info(R#offline_msg.packet, LServer, R#offline_msg.timestamp, - <<"Offline Storage">>)}; -offline_msg_to_route(_LServer, #xmlel{} = El) -> - To = jlib:string_to_jid(xml:get_tag_attr_s(<<"to">>, El)), - From = jlib:string_to_jid(xml:get_tag_attr_s(<<"from">>, El)), - if (To /= error) and (From /= error) -> - {route, From, To, El}; - true -> - error +-spec read_messages(binary(), binary()) -> [{binary(), message()}]. +read_messages(LUser, LServer) -> + Res = case read_db_messages(LUser, LServer) of + error -> + []; + L when is_list(L) -> + L + end, + case use_mam_for_user(LUser, LServer) of + true -> + read_mam_messages(LUser, LServer, Res); + _ -> + Res end. -read_all_msgs(LUser, LServer, mnesia) -> - US = {LUser, LServer}, - lists:keysort(#offline_msg.timestamp, - mnesia:dirty_read({offline_msg, US})); -read_all_msgs(LUser, LServer, riak) -> - case ejabberd_riak:get_by_index( - offline_msg, offline_msg_schema(), - <<"us">>, {LUser, LServer}) of - {ok, Rs} -> - lists:keysort(#offline_msg.timestamp, Rs); - _Err -> - [] - end; -read_all_msgs(LUser, LServer, odbc) -> - Username = ejabberd_odbc:escape(LUser), - case catch ejabberd_odbc:sql_query(LServer, - [<<"select xml from spool where username='">>, - Username, <<"' order by seq;">>]) - of - {selected, [<<"xml">>], Rs} -> - lists:flatmap(fun ([XML]) -> - case xml_stream:parse_element(XML) of - {error, _Reason} -> []; - El -> [El] - end - end, - Rs); - _ -> [] +-spec read_db_messages(binary(), binary()) -> [{binary(), message()}] | error. +read_db_messages(LUser, LServer) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + CodecOpts = ejabberd_config:codec_options(), + case Mod:read_message_headers(LUser, LServer) of + error -> + error; + L -> + lists:flatmap( + fun({Seq, From, To, TS, El}) -> + Node = integer_to_binary(Seq), + try xmpp:decode(El, ?NS_CLIENT, CodecOpts) of + Pkt -> + Node = integer_to_binary(Seq), + Pkt1 = add_delay_info(Pkt, LServer, TS), + Pkt2 = xmpp:set_from_to(Pkt1, From, To), + [{Node, Pkt2}] + catch _:{xmpp_codec, Why} -> + ?ERROR_MSG("Failed to decode packet ~p " + "of user ~ts: ~ts", + [El, jid:encode(To), + xmpp:format_error(Why)]), + [] + end + end, L) end. -format_user_queue(Msgs, DBType) when DBType == mnesia; DBType == riak -> - lists:map(fun (#offline_msg{timestamp = TimeStamp, - from = From, to = To, - packet = - #xmlel{name = Name, attrs = Attrs, - children = Els}} = - Msg) -> - ID = jlib:encode_base64((term_to_binary(Msg))), - {{Year, Month, Day}, {Hour, Minute, Second}} = - calendar:now_to_local_time(TimeStamp), - Time = - iolist_to_binary(io_lib:format("~w-~.2.0w-~.2.0w ~.2.0w:~.2.0w:~.2.0w", - [Year, Month, Day, - Hour, Minute, - Second])), - SFrom = jlib:jid_to_string(From), - STo = jlib:jid_to_string(To), - Attrs2 = jlib:replace_from_to_attrs(SFrom, STo, Attrs), - Packet = #xmlel{name = Name, attrs = Attrs2, - children = Els}, - FPacket = ejabberd_web_admin:pretty_print_xml(Packet), - ?XE(<<"tr">>, - [?XAE(<<"td">>, [{<<"class">>, <<"valign">>}], - [?INPUT(<<"checkbox">>, <<"selected">>, ID)]), - ?XAC(<<"td">>, [{<<"class">>, <<"valign">>}], Time), - ?XAC(<<"td">>, [{<<"class">>, <<"valign">>}], SFrom), - ?XAC(<<"td">>, [{<<"class">>, <<"valign">>}], STo), - ?XAE(<<"td">>, [{<<"class">>, <<"valign">>}], - [?XC(<<"pre">>, FPacket)])]) - end, - Msgs); -format_user_queue(Msgs, odbc) -> - lists:map(fun (#xmlel{} = Msg) -> - ID = jlib:encode_base64((term_to_binary(Msg))), - Packet = Msg, - FPacket = ejabberd_web_admin:pretty_print_xml(Packet), - ?XE(<<"tr">>, - [?XAE(<<"td">>, [{<<"class">>, <<"valign">>}], - [?INPUT(<<"checkbox">>, <<"selected">>, ID)]), - ?XAE(<<"td">>, [{<<"class">>, <<"valign">>}], - [?XC(<<"pre">>, FPacket)])]) - end, - Msgs). +-spec parse_marker_messages(binary(), [#offline_msg{} | {any(), message()}]) -> + {integer() | none, [message()]}. +parse_marker_messages(LServer, ReadMsgs) -> + {Timestamp, ExtraMsgs} = lists:foldl( + fun({_Node, #message{id = <<"ActivityMarker">>, + body = [], type = error} = Msg}, {T, E}) -> + case xmpp:get_subtag(Msg, #delay{stamp = {0,0,0}}) of + #delay{stamp = Time} -> + if T == none orelse T > Time -> + {Time, E}; + true -> + {T, E} + end + end; + (#offline_msg{from = From, to = To, timestamp = TS, packet = Pkt}, + {T, E}) -> + try xmpp:decode(Pkt) of + #message{id = <<"ActivityMarker">>, + body = [], type = error} = Msg -> + TS2 = case TS of + undefined -> + case xmpp:get_subtag(Msg, #delay{stamp = {0,0,0}}) of + #delay{stamp = TS0} -> + TS0; + _ -> + erlang:timestamp() + end; + _ -> + TS + end, + if T == none orelse T > TS2 -> + {TS2, E}; + true -> + {T, E} + end; + Decoded -> + Pkt1 = add_delay_info(Decoded, LServer, TS), + {T, [xmpp:set_from_to(Pkt1, From, To) | E]} + catch _:{xmpp_codec, _Why} -> + {T, E} + end; + ({_Node, Msg}, {T, E}) -> + {T, [Msg | E]} + end, {none, []}, ReadMsgs), + Start = case {Timestamp, ExtraMsgs} of + {none, [First|_]} -> + case xmpp:get_subtag(First, #delay{stamp = {0,0,0}}) of + #delay{stamp = {Mega, Sec, Micro}} -> + {Mega, Sec, Micro+1}; + _ -> + none + end; + {none, _} -> + none; + _ -> + Timestamp + end, + {Start, ExtraMsgs}. -user_queue(User, Server, Query, Lang) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - US = {LUser, LServer}, - DBType = gen_mod:db_type(LServer, ?MODULE), - Res = user_queue_parse_query(LUser, LServer, Query, - DBType), - MsgsAll = read_all_msgs(LUser, LServer, DBType), - Msgs = get_messages_subset(US, Server, MsgsAll, - DBType), - FMsgs = format_user_queue(Msgs, DBType), - [?XC(<<"h1">>, - list_to_binary(io_lib:format(?T(<<"~s's Offline Messages Queue">>), - [us_to_list(US)])))] - ++ - case Res of - ok -> [?XREST(<<"Submitted">>)]; - nothing -> [] - end - ++ - [?XAE(<<"form">>, - [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], - [?XE(<<"table">>, - [?XE(<<"thead">>, - [?XE(<<"tr">>, - [?X(<<"td">>), ?XCT(<<"td">>, <<"Time">>), - ?XCT(<<"td">>, <<"From">>), - ?XCT(<<"td">>, <<"To">>), - ?XCT(<<"td">>, <<"Packet">>)])]), - ?XE(<<"tbody">>, - if FMsgs == [] -> - [?XE(<<"tr">>, - [?XAC(<<"td">>, [{<<"colspan">>, <<"4">>}], - <<" ">>)])]; - true -> FMsgs - end)]), - ?BR, - ?INPUTT(<<"submit">>, <<"delete">>, - <<"Delete Selected">>)])]. +-spec read_mam_messages(binary(), binary(), [#offline_msg{} | {any(), message()}]) -> + [{integer(), message()}]. +read_mam_messages(LUser, LServer, ReadMsgs) -> + {Start, ExtraMsgs} = parse_marker_messages(LServer, ReadMsgs), + AllMsgs = case Start of + none -> + ExtraMsgs; + _ -> + MaxOfflineMsgs = case get_max_user_messages(LUser, LServer) of + Number when is_integer(Number) -> + max(0, Number - length(ExtraMsgs)); + infinity -> + undefined + end, + JID = jid:make(LUser, LServer, <<>>), + {MamMsgs, _, _} = mod_mam:select(LServer, JID, JID, + [{start, Start}], + #rsm_set{max = MaxOfflineMsgs, + before = <<"9999999999999999">>}, + chat, only_messages), + MamMsgs2 = lists:map( + fun({_, _, #forwarded{sub_els = [MM | _], delay = #delay{stamp = MMT}}}) -> + add_delay_info(MM, LServer, MMT) + end, MamMsgs), -user_queue_parse_query(LUser, LServer, Query, mnesia) -> - US = {LUser, LServer}, - case lists:keysearch(<<"delete">>, 1, Query) of - {value, _} -> - Msgs = lists:keysort(#offline_msg.timestamp, - mnesia:dirty_read({offline_msg, US})), - F = fun () -> - lists:foreach(fun (Msg) -> - ID = - jlib:encode_base64((term_to_binary(Msg))), - case lists:member({<<"selected">>, - ID}, - Query) - of - true -> mnesia:delete_object(Msg); - false -> ok - end - end, - Msgs) + ExtraMsgs ++ MamMsgs2 end, - mnesia:transaction(F), - ok; - false -> nothing - end; -user_queue_parse_query(LUser, LServer, Query, riak) -> - case lists:keysearch(<<"delete">>, 1, Query) of - {value, _} -> - Msgs = read_all_msgs(LUser, LServer, riak), - lists:foreach( - fun (Msg) -> - ID = jlib:encode_base64((term_to_binary(Msg))), - case lists:member({<<"selected">>, ID}, Query) of - true -> - ejabberd_riak:delete(offline_msg, - Msg#offline_msg.timestamp); - false -> - ok - end - end, - Msgs), - ok; - false -> - nothing - end; -user_queue_parse_query(LUser, LServer, Query, odbc) -> - Username = ejabberd_odbc:escape(LUser), - case lists:keysearch(<<"delete">>, 1, Query) of - {value, _} -> - Msgs = case catch ejabberd_odbc:sql_query(LServer, - [<<"select xml, seq from spool where username='">>, - Username, - <<"' order by seq;">>]) - of - {selected, [<<"xml">>, <<"seq">>], Rs} -> - lists:flatmap(fun ([XML, Seq]) -> - case xml_stream:parse_element(XML) - of - {error, _Reason} -> []; - El -> [{El, Seq}] - end - end, - Rs); - _ -> [] + AllMsgs2 = lists:sort( + fun(A, B) -> + DA = case xmpp:get_subtag(A, #stanza_id{by = #jid{}}) of + #stanza_id{id = IDA} -> + IDA; + _ -> case xmpp:get_subtag(A, #delay{stamp = {0,0,0}}) of + #delay{stamp = STA} -> + integer_to_binary(misc:now_to_usec(STA)); + _ -> + <<"unknown">> + end end, - F = fun () -> - lists:foreach(fun ({Msg, Seq}) -> - ID = - jlib:encode_base64((term_to_binary(Msg))), - case lists:member({<<"selected">>, - ID}, - Query) - of - true -> - SSeq = - ejabberd_odbc:escape(Seq), - catch - ejabberd_odbc:sql_query(LServer, - [<<"delete from spool where username='">>, - Username, - <<"' and seq='">>, - SSeq, - <<"';">>]); - false -> ok - end - end, - Msgs) - end, - mnesia:transaction(F), - ok; - false -> nothing + DB = case xmpp:get_subtag(B, #stanza_id{by = #jid{}}) of + #stanza_id{id = IDB} -> + IDB; + _ -> case xmpp:get_subtag(B, #delay{stamp = {0,0,0}}) of + #delay{stamp = STB} -> + integer_to_binary(misc:now_to_usec(STB)); + _ -> + <<"unknown">> + end + end, + DA < DB + end, AllMsgs), + {AllMsgs3, _} = lists:mapfoldl( + fun(Msg, Counter) -> + {{Counter, Msg}, Counter + 1} + end, 1, AllMsgs2), + AllMsgs3. + +-spec count_mam_messages(binary(), binary(), [#offline_msg{} | {any(), message()}] | error) -> + {cache, integer()} | {nocache, integer()}. +count_mam_messages(_LUser, _LServer, error) -> + {nocache, 0}; +count_mam_messages(LUser, LServer, ReadMsgs) -> + {Start, ExtraMsgs} = parse_marker_messages(LServer, ReadMsgs), + case Start of + none -> + {cache, length(ExtraMsgs)}; + _ -> + MaxOfflineMsgs = case get_max_user_messages(LUser, LServer) of + Number when is_integer(Number) -> Number - length(ExtraMsgs); + infinity -> undefined + end, + JID = jid:make(LUser, LServer, <<>>), + {_, _, Count} = mod_mam:select(LServer, JID, JID, + [{start, Start}], + #rsm_set{max = MaxOfflineMsgs, + before = <<"9999999999999999">>}, + chat, only_count), + {cache, Count + length(ExtraMsgs)} end. +format_user_queue(Hdrs) -> + lists:map( + fun({_Seq, From, To, TS, El}) -> + FPacket = ejabberd_web_admin:pretty_print_xml(El), + SFrom = jid:encode(From), + STo = jid:encode(To), + Time = case TS of + undefined -> + Stamp = fxml:get_path_s(El, [{elem, <<"delay">>}, + {attr, <<"stamp">>}]), + try xmpp_util:decode_timestamp(Stamp) of + {_, _, _} = Now -> format_time(Now) + catch _:_ -> + <<"">> + end; + {_, _, _} = Now -> + format_time(Now) + end, + {Time, SFrom, STo, FPacket} + end, Hdrs). + +format_time(Now) -> + {{Year, Month, Day}, {Hour, Minute, Second}} = calendar:now_to_local_time(Now), + str:format("~w-~.2.0w-~.2.0w ~.2.0w:~.2.0w:~.2.0w", + [Year, Month, Day, Hour, Minute, Second]). + us_to_list({User, Server}) -> - jlib:jid_to_string({User, Server, <<"">>}). + jid:encode({User, Server, <<"">>}). get_queue_length(LUser, LServer) -> - get_queue_length(LUser, LServer, - gen_mod:db_type(LServer, ?MODULE)). + count_offline_messages(LUser, LServer). -get_queue_length(LUser, LServer, mnesia) -> - length(mnesia:dirty_read({offline_msg, - {LUser, LServer}})); -get_queue_length(LUser, LServer, riak) -> - case ejabberd_riak:count_by_index(offline_msg, - <<"us">>, {LUser, LServer}) of - {ok, N} -> - N; - _ -> - 0 - end; -get_queue_length(LUser, LServer, odbc) -> - Username = ejabberd_odbc:escape(LUser), - case catch ejabberd_odbc:sql_query(LServer, - [<<"select count(*) from spool where username='">>, - Username, <<"';">>]) - of - {selected, [_], [[SCount]]} -> - jlib:binary_to_integer(SCount); - _ -> 0 - end. +webadmin_user(Acc, User, Server, R) -> + Acc ++ [make_command(get_offline_count, R, [{<<"user">>, User}, {<<"host">>, Server}], + [{result_links, [{value, arg_host, 4, <<"user/", User/binary, "/queue/">>}]}] + )]. -get_messages_subset(User, Host, MsgsAll, DBType) -> - Access = gen_mod:get_module_opt(Host, ?MODULE, access_max_user_messages, - fun(A) when is_atom(A) -> A end, - max_user_offline_messages), - MaxOfflineMsgs = case get_max_user_messages(Access, - User, Host) - of - Number when is_integer(Number) -> Number; - _ -> 100 - end, - Length = length(MsgsAll), - get_messages_subset2(MaxOfflineMsgs, Length, MsgsAll, - DBType). - -get_messages_subset2(Max, Length, MsgsAll, _DBType) - when Length =< Max * 2 -> - MsgsAll; -get_messages_subset2(Max, Length, MsgsAll, DBType) - when DBType == mnesia; DBType == riak -> - FirstN = Max, - {MsgsFirstN, Msgs2} = lists:split(FirstN, MsgsAll), - MsgsLastN = lists:nthtail(Length - FirstN - FirstN, - Msgs2), - NoJID = jlib:make_jid(<<"...">>, <<"...">>, <<"">>), - IntermediateMsg = #offline_msg{timestamp = now(), - from = NoJID, to = NoJID, - packet = - #xmlel{name = <<"...">>, attrs = [], - children = []}}, - MsgsFirstN ++ [IntermediateMsg] ++ MsgsLastN; -get_messages_subset2(Max, Length, MsgsAll, odbc) -> - FirstN = Max, - {MsgsFirstN, Msgs2} = lists:split(FirstN, MsgsAll), - MsgsLastN = lists:nthtail(Length - FirstN - FirstN, - Msgs2), - IntermediateMsg = #xmlel{name = <<"...">>, attrs = [], - children = []}, - MsgsFirstN ++ [IntermediateMsg] ++ MsgsLastN. - -webadmin_user(Acc, User, Server, Lang) -> - QueueLen = get_queue_length(jlib:nodeprep(User), - jlib:nameprep(Server)), - FQueueLen = [?AC(<<"queue/">>, - (iolist_to_binary(integer_to_list(QueueLen))))], - Acc ++ - [?XCT(<<"h3">>, <<"Offline Messages:">>)] ++ - FQueueLen ++ - [?C(<<" ">>), - ?INPUTT(<<"submit">>, <<"removealloffline">>, - <<"Remove All Offline Messages">>)]. +%%% +%%% +%%% +-spec delete_all_msgs(binary(), binary()) -> {atomic, any()}. delete_all_msgs(User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - delete_all_msgs(LUser, LServer, - gen_mod:db_type(LServer, ?MODULE)). - -delete_all_msgs(LUser, LServer, mnesia) -> - US = {LUser, LServer}, - F = fun () -> - mnesia:write_lock_table(offline_msg), - lists:foreach(fun (Msg) -> mnesia:delete_object(Msg) - end, - mnesia:dirty_read({offline_msg, US})) - end, - mnesia:transaction(F); -delete_all_msgs(LUser, LServer, riak) -> - Res = ejabberd_riak:delete_by_index(offline_msg, - <<"us">>, {LUser, LServer}), - {atomic, Res}; -delete_all_msgs(LUser, LServer, odbc) -> - Username = ejabberd_odbc:escape(LUser), - odbc_queries:del_spool_msg(LServer, Username), - {atomic, ok}. + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + Mod = gen_mod:db_mod(LServer, ?MODULE), + Ret = Mod:remove_all_messages(LUser, LServer), + flush_cache(Mod, LUser, LServer), + Ret. webadmin_user_parse_query(_, <<"removealloffline">>, User, Server, _Query) -> case delete_all_msgs(User, Server) of - {aborted, Reason} -> - ?ERROR_MSG("Failed to remove offline messages: ~p", - [Reason]), - {stop, error}; - {atomic, ok} -> - ?INFO_MSG("Removed all offline messages for ~s@~s", - [User, Server]), - {stop, ok} + {atomic, ok} -> + ?INFO_MSG("Removed all offline messages for ~ts@~ts", + [User, Server]), + {stop, ok}; + Err -> + ?ERROR_MSG("Failed to remove offline messages: ~p", + [Err]), + {stop, error} end; webadmin_user_parse_query(Acc, _Action, _User, _Server, _Query) -> Acc. %% Returns as integer the number of offline messages for a given user +-spec count_offline_messages(binary(), binary()) -> non_neg_integer(). count_offline_messages(User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - DBType = gen_mod:db_type(LServer, ?MODULE), - count_offline_messages(LUser, LServer, DBType). + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + Mod = gen_mod:db_mod(LServer, ?MODULE), + case use_mam_for_user(User, Server) of + true -> + case use_cache(Mod, LServer) of + true -> + ets_cache:lookup( + ?SPOOL_COUNTER_CACHE, {LUser, LServer}, + fun() -> + Res = read_db_messages(LUser, LServer), + count_mam_messages(LUser, LServer, Res) + end); + false -> + Res = read_db_messages(LUser, LServer), + ets_cache:untag(count_mam_messages(LUser, LServer, Res)) + end; + _ -> + case use_cache(Mod, LServer) of + true -> + ets_cache:lookup( + ?SPOOL_COUNTER_CACHE, {LUser, LServer}, + fun() -> + Mod:count_messages(LUser, LServer) + end); + false -> + ets_cache:untag(Mod:count_messages(LUser, LServer)) + end + end. -count_offline_messages(LUser, LServer, mnesia) -> +-spec store_message_in_db(module(), #offline_msg{}) -> ok | {error, any()}. +store_message_in_db(Mod, #offline_msg{us = {User, Server}} = Msg) -> + case Mod:store_message(Msg) of + ok -> + case use_cache(Mod, Server) of + true -> + ets_cache:incr( + ?SPOOL_COUNTER_CACHE, + {User, Server}, 1, + cache_nodes(Mod, Server)); + false -> + ok + end; + Err -> + Err + end. + +-spec add_delay_info(message(), binary(), + undefined | erlang:timestamp()) -> message(). +add_delay_info(Packet, LServer, TS) -> + NewTS = case TS of + undefined -> erlang:timestamp(); + _ -> TS + end, + Packet1 = xmpp:put_meta(Packet, from_offline, true), + misc:add_delay_info(Packet1, jid:make(LServer), NewTS, + <<"Offline storage">>). + +-spec get_priority_from_presence(presence()) -> integer(). +get_priority_from_presence(#presence{priority = Prio}) -> + case Prio of + undefined -> 0; + _ -> Prio + end. + +export(LServer) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:export(LServer). + +import_info() -> + [{<<"spool">>, 4}]. + +import_start(LServer, DBType) -> + Mod = gen_mod:db_mod(DBType, ?MODULE), + Mod:import(LServer, []). + +import(LServer, {sql, _}, DBType, <<"spool">>, + [LUser, XML, _Seq, _TimeStamp]) -> + El = fxml_stream:parse_element(XML), + #message{from = From, to = To} = Msg = xmpp:decode(El, ?NS_CLIENT, [ignore_els]), + TS = case xmpp:get_subtag(Msg, #delay{stamp = {0,0,0}}) of + #delay{stamp = {MegaSecs, Secs, _}} -> + {MegaSecs, Secs, 0}; + false -> + erlang:timestamp() + end, US = {LUser, LServer}, - F = fun () -> - count_mnesia_records(US) - end, - case catch mnesia:async_dirty(F) of - I when is_integer(I) -> I; - _ -> 0 - end; -count_offline_messages(LUser, LServer, odbc) -> - Username = ejabberd_odbc:escape(LUser), - case catch odbc_queries:count_records_where(LServer, - <<"spool">>, - <<"where username='", - Username/binary, "'">>) - of - {selected, [_], [[Res]]} -> - jlib:binary_to_integer(Res); - _ -> 0 - end; -count_offline_messages(LUser, LServer, riak) -> - case ejabberd_riak:count_by_index( - offline_msg, <<"us">>, {LUser, LServer}) of - {ok, Res} -> - Res; - _ -> - 0 - end; -count_offline_messages(_Acc, User, Server) -> - N = count_offline_messages(User, Server), - {stop, N}. + Expire = find_x_expire(TS, Msg), + OffMsg = #offline_msg{us = US, packet = El, + from = From, to = To, + timestamp = TS, expire = Expire}, + Mod = gen_mod:db_mod(DBType, ?MODULE), + Mod:import(OffMsg). -%% Return the number of records matching a given match expression. -%% This function is intended to be used inside a Mnesia transaction. -%% The count has been written to use the fewest possible memory by -%% getting the record by small increment and by using continuation. --define(BATCHSIZE, 100). +use_mam_for_user(_User, Server) -> + mod_offline_opt:use_mam_for_storage(Server). -count_mnesia_records(US) -> - MatchExpression = #offline_msg{us = US, _ = '_'}, - case mnesia:select(offline_msg, [{MatchExpression, [], [[]]}], - ?BATCHSIZE, read) of - {Result, Cont} -> - Count = length(Result), - count_records_cont(Cont, Count); - '$end_of_table' -> - 0 - end. +mod_opt_type(access_max_user_messages) -> + econf:shaper(); +mod_opt_type(store_groupchat) -> + econf:bool(); +mod_opt_type(bounce_groupchat) -> + econf:bool(); +mod_opt_type(use_mam_for_storage) -> + econf:bool(); +mod_opt_type(store_empty_body) -> + econf:either( + unless_chat_state, + econf:bool()); +mod_opt_type(db_type) -> + econf:db_type(?MODULE); +mod_opt_type(use_cache) -> + econf:bool(); +mod_opt_type(cache_size) -> + econf:pos_int(infinity); +mod_opt_type(cache_life_time) -> + econf:timeout(second, infinity). -count_records_cont(Cont, Count) -> - case mnesia:select(Cont) of - {Result, Cont} -> - NewCount = Count + length(Result), - count_records_cont(Cont, NewCount); - '$end_of_table' -> - Count - end. +mod_options(Host) -> + [{db_type, ejabberd_config:default_db(Host, ?MODULE)}, + {access_max_user_messages, max_user_offline_messages}, + {store_empty_body, unless_chat_state}, + {use_mam_for_storage, false}, + {bounce_groupchat, false}, + {store_groupchat, false}, + {use_cache, ejabberd_option:use_cache(Host)}, + {cache_size, ejabberd_option:cache_size(Host)}, + {cache_life_time, ejabberd_option:cache_life_time(Host)}]. -offline_msg_schema() -> - {record_info(fields, offline_msg), #offline_msg{}}. - -export(_Server) -> - [{offline_msg, - fun(Host, #offline_msg{us = {LUser, LServer}, - timestamp = TimeStamp, from = From, to = To, - packet = Packet}) - when LServer == Host -> - Username = ejabberd_odbc:escape(LUser), - Packet1 = - jlib:replace_from_to(jlib:jid_to_string(From), - jlib:jid_to_string(To), Packet), - Packet2 = - jlib:add_delay_info(Packet1, LServer, TimeStamp, - <<"Offline Storage">>), - XML = - ejabberd_odbc:escape(xml:element_to_binary(Packet2)), - [[<<"delete from spool where username='">>, Username, <<"';">>], - [<<"insert into spool(username, xml) values ('">>, - Username, <<"', '">>, XML, <<"');">>]]; - (_Host, _R) -> - [] - end}]. - -import(LServer) -> - [{<<"select username, xml from spool;">>, - fun([LUser, XML]) -> - El = #xmlel{} = xml_stream:parse_element(XML), - From = #jid{} = jlib:string_to_jid( - xml:get_attr_s(<<"from">>, El#xmlel.attrs)), - To = #jid{} = jlib:string_to_jid( - xml:get_attr_s(<<"to">>, El#xmlel.attrs)), - Stamp = xml:get_path_s(El, [{elem, <<"delay">>}, - {attr, <<"stamp">>}]), - TS = case jlib:datetime_string_to_timestamp(Stamp) of - {_, _, _} = Now -> - Now; - undefined -> - now() - end, - Expire = find_x_expire(TS, El#xmlel.children), - #offline_msg{us = {LUser, LServer}, - from = From, to = To, - timestamp = TS, expire = Expire} - end}]. - -import(_LServer, mnesia, #offline_msg{} = Msg) -> - mnesia:dirty_write(Msg); -import(_LServer, riak, #offline_msg{us = US, timestamp = TS} = M) -> - ejabberd_riak:put(M, offline_msg_schema(), - [{i, TS}, {'2i', [{<<"us">>, US}]}]); -import(_, _, _) -> - pass. +mod_doc() -> + #{desc => + [?T("This module implements " + "https://xmpp.org/extensions/xep-0160.html" + "[XEP-0160: Best Practices for Handling Offline Messages] " + "and https://xmpp.org/extensions/xep-0013.html" + "[XEP-0013: Flexible Offline Message Retrieval]. " + "This means that all messages sent to an offline user " + "will be stored on the server until that user comes online " + "again. Thus it is very similar to how email works. A user " + "is considered offline if no session presence priority > 0 " + "are currently open."), "", + ?T("The _`delete_expired_messages`_ API allows to delete expired messages, " + "and _`delete_old_messages`_ API deletes older ones.")], + opts => + [{access_max_user_messages, + #{value => ?T("AccessName"), + desc => + ?T("This option defines which access rule will be " + "enforced to limit the maximum number of offline " + "messages that a user can have (quota). When a user " + "has too many offline messages, any new messages that " + "they receive are discarded, and a '' " + "error is returned to the sender. The default value is " + "'max_user_offline_messages'.")}}, + {store_empty_body, + #{value => "true | false | unless_chat_state", + desc => + ?T("Whether or not to store messages that lack a '' " + "element. The default value is 'unless_chat_state', " + "which tells ejabberd to store messages even if they " + "lack the '' element, unless they only contain a " + "chat state notification (as defined in " + "https://xmpp.org/extensions/xep-0085.html" + "[XEP-0085: Chat State Notifications].")}}, + {store_groupchat, + #{value => "true | false", + desc => + ?T("Whether or not to store groupchat messages. " + "The default value is 'false'.")}}, + {use_mam_for_storage, + #{value => "true | false", + desc => + ?T("This is an experimental option. By enabling the option, " + "this module uses the 'archive' table from _`mod_mam`_ instead " + "of its own spool table to retrieve the messages received " + "when the user was offline. This allows client " + "developers to slowly drop XEP-0160 and rely on XEP-0313 " + "instead. It also further reduces the " + "storage required when you enable MucSub. Enabling this " + "option has a known drawback for the moment: most of " + "flexible message retrieval queries don't work (those that " + "allow retrieval/deletion of messages by id), but this " + "specification is not widely used. The default value " + "is 'false' to keep former behaviour as default.")}}, + {bounce_groupchat, + #{value => "true | false", + desc => + ?T("This option is use the disable an optimization that " + "avoids bouncing error messages when groupchat messages " + "could not be stored as offline. It will reduce chat " + "room load, without any drawback in standard use cases. " + "You may change default value only if you have a custom " + "module which uses offline hook after 'mod_offline'. This " + "option can be useful for both standard MUC and MucSub, " + "but the bounce is much more likely to happen in the context " + "of MucSub, so it is even more important to have it on " + "large MucSub services. The default value is 'false', meaning " + "the optimization is enabled.")}}, + {db_type, + #{value => "mnesia | sql", + desc => + ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}, + {use_cache, + #{value => "true | false", + desc => + ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, + {cache_size, + #{value => "pos_integer() | infinity", + desc => + ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, + {cache_life_time, + #{value => "timeout()", + desc => + ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}], + example => + [{?T("This example allows power users to have as much as 5000 " + "offline messages, administrators up to 2000, and all the " + "other users up to 100:"), + ["acl:", + " admin:", + " user:", + " - admin1@localhost", + " - admin2@example.org", + " poweruser:", + " user:", + " - bob@example.org", + " - jane@example.org", + "", + "shaper_rules:", + " max_user_offline_messages:", + " - 5000: poweruser", + " - 2000: admin", + " - 100", + "", + "modules:", + " ...", + " mod_offline:", + " access_max_user_messages: max_user_offline_messages", + " ..." + ]}]}. diff --git a/src/mod_offline_mnesia.erl b/src/mod_offline_mnesia.erl new file mode 100644 index 000000000..24406c5ac --- /dev/null +++ b/src/mod_offline_mnesia.erl @@ -0,0 +1,274 @@ +%%%------------------------------------------------------------------- +%%% File : mod_offline_mnesia.erl +%%% Author : Evgeny Khramtsov +%%% Created : 15 Apr 2016 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_offline_mnesia). + +-behaviour(mod_offline). + +-export([init/2, store_message/1, pop_messages/2, remove_expired_messages/1, + remove_old_messages/2, remove_user/2, read_message_headers/2, + read_message/3, remove_message/3, read_all_messages/2, + remove_all_messages/2, count_messages/2, import/1, + remove_old_messages_batch/4]). +-export([need_transform/1, transform/1]). + +-include_lib("xmpp/include/xmpp.hrl"). +-include("mod_offline.hrl"). +-include("logger.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + ejabberd_mnesia:create(?MODULE, offline_msg, + [{disc_only_copies, [node()]}, {type, bag}, + {attributes, record_info(fields, offline_msg)}]). + +store_message(#offline_msg{packet = Pkt} = OffMsg) -> + El = xmpp:encode(Pkt), + mnesia:dirty_write(OffMsg#offline_msg{packet = El}). + +pop_messages(LUser, LServer) -> + US = {LUser, LServer}, + F = fun () -> + Rs = mnesia:wread({offline_msg, US}), + mnesia:delete({offline_msg, US}), + Rs + end, + case mnesia:transaction(F) of + {atomic, L} -> + {ok, lists:keysort(#offline_msg.timestamp, L)}; + {aborted, Reason} -> + {error, Reason} + end. + +remove_expired_messages(_LServer) -> + TimeStamp = erlang:timestamp(), + F = fun () -> + mnesia:write_lock_table(offline_msg), + mnesia:foldl(fun (Rec, _Acc) -> + case Rec#offline_msg.expire of + never -> ok; + TS -> + if TS < TimeStamp -> + mnesia:delete_object(Rec); + true -> ok + end + end + end, + ok, offline_msg) + end, + mnesia:transaction(F). + +remove_old_messages(Days, _LServer) -> + S = erlang:system_time(second) - 60 * 60 * 24 * Days, + MegaSecs1 = S div 1000000, + Secs1 = S rem 1000000, + TimeStamp = {MegaSecs1, Secs1, 0}, + F = fun () -> + mnesia:write_lock_table(offline_msg), + mnesia:foldl(fun (#offline_msg{timestamp = TS} = Rec, + _Acc) + when TS < TimeStamp -> + mnesia:delete_object(Rec); + (_Rec, _Acc) -> ok + end, + ok, offline_msg) + end, + mnesia:transaction(F). + +delete_batch('$end_of_table', _LServer, _TS, Num) -> + {Num, '$end_of_table'}; +delete_batch(LastUS, _LServer, _TS, 0) -> + {0, LastUS}; +delete_batch(none, LServer, TS, Num) -> + delete_batch(mnesia:first(offline_msg), LServer, TS, Num); +delete_batch({_, LServer2} = LastUS, LServer, TS, Num) when LServer /= LServer2 -> + delete_batch(mnesia:next(offline_msg, LastUS), LServer, TS, Num); +delete_batch(LastUS, LServer, TS, Num) -> + Left = + lists:foldl( + fun(_, 0) -> + 0; + (#offline_msg{timestamp = TS2} = O, Num2) when TS2 < TS -> + mnesia:delete_object(O), + Num2 - 1; + (_, Num2) -> + Num2 + end, Num, mnesia:wread({offline_msg, LastUS})), + case Left of + 0 -> {0, LastUS}; + _ -> delete_batch(mnesia:next(offline_msg, LastUS), LServer, TS, Left) + end. + +remove_old_messages_batch(LServer, Days, Batch, LastUS) -> + S = erlang:system_time(second) - 60 * 60 * 24 * Days, + MegaSecs1 = S div 1000000, + Secs1 = S rem 1000000, + TimeStamp = {MegaSecs1, Secs1, 0}, + R = mnesia:transaction( + fun() -> + {Num, NextUS} = delete_batch(LastUS, LServer, TimeStamp, Batch), + {Batch - Num, NextUS} + end), + case R of + {atomic, {Num, State}} -> + {ok, State, Num}; + {aborted, Err} -> + {error, Err} + end. + +remove_user(LUser, LServer) -> + US = {LUser, LServer}, + F = fun () -> mnesia:delete({offline_msg, US}) end, + mnesia:transaction(F). + +read_message_headers(LUser, LServer) -> + Msgs = mnesia:dirty_read({offline_msg, {LUser, LServer}}), + Hdrs = lists:map( + fun(#offline_msg{from = From, to = To, packet = Pkt, + timestamp = TS}) -> + Seq = now_to_integer(TS), + {Seq, From, To, TS, Pkt} + end, Msgs), + lists:keysort(1, Hdrs). + +read_message(LUser, LServer, I) -> + US = {LUser, LServer}, + TS = integer_to_now(I), + case mnesia:dirty_match_object( + offline_msg, #offline_msg{us = US, timestamp = TS, _ = '_'}) of + [Msg|_] -> + {ok, Msg}; + _ -> + error + end. + +remove_message(LUser, LServer, I) -> + US = {LUser, LServer}, + TS = integer_to_now(I), + case mnesia:dirty_match_object( + offline_msg, #offline_msg{us = US, timestamp = TS, _ = '_'}) of + [] -> + {error, notfound}; + Msgs -> + lists:foreach( + fun(Msg) -> + mnesia:dirty_delete_object(Msg) + end, Msgs) + end. + +read_all_messages(LUser, LServer) -> + US = {LUser, LServer}, + lists:keysort(#offline_msg.timestamp, + mnesia:dirty_read({offline_msg, US})). + +remove_all_messages(LUser, LServer) -> + US = {LUser, LServer}, + F = fun () -> + mnesia:write_lock_table(offline_msg), + lists:foreach(fun (Msg) -> mnesia:delete_object(Msg) end, + mnesia:dirty_read({offline_msg, US})) + end, + mnesia:transaction(F). + +count_messages(LUser, LServer) -> + US = {LUser, LServer}, + F = fun () -> + count_mnesia_records(US) + end, + {cache, case mnesia:async_dirty(F) of + I when is_integer(I) -> I; + _ -> 0 + end}. + +import(#offline_msg{} = Msg) -> + mnesia:dirty_write(Msg). + +need_transform({offline_msg, {U, S}, _, _, _, _, _}) + when is_list(U) orelse is_list(S) -> + ?INFO_MSG("Mnesia table 'offline_msg' will be converted to binary", []), + true; +need_transform({offline_msg, _, _, _, _, _, _, _}) -> + true; +need_transform(_) -> + false. + +transform({offline_msg, {U, S}, Timestamp, Expire, From, To, _, Packet}) -> + #offline_msg{us = {U, S}, timestamp = Timestamp, expire = Expire, + from = From, to = To, packet = Packet}; +transform(#offline_msg{us = {U, S}, from = From, to = To, + packet = El} = R) -> + R#offline_msg{us = {iolist_to_binary(U), iolist_to_binary(S)}, + from = jid_to_binary(From), + to = jid_to_binary(To), + packet = fxml:to_xmlel(El)}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +%% Return the number of records matching a given match expression. +%% This function is intended to be used inside a Mnesia transaction. +%% The count has been written to use the fewest possible memory by +%% getting the record by small increment and by using continuation. +-define(BATCHSIZE, 100). + +count_mnesia_records(US) -> + MatchExpression = #offline_msg{us = US, _ = '_'}, + case mnesia:select(offline_msg, [{MatchExpression, [], [[]]}], + ?BATCHSIZE, read) of + {Result, Cont} -> + Count = length(Result), + count_records_cont(Cont, Count); + '$end_of_table' -> + 0 + end. + +count_records_cont(Cont, Count) -> + case mnesia:select(Cont) of + {Result, Cont} -> + NewCount = Count + length(Result), + count_records_cont(Cont, NewCount); + '$end_of_table' -> + Count + end. + +jid_to_binary(#jid{user = U, server = S, resource = R, + luser = LU, lserver = LS, lresource = LR}) -> + #jid{user = iolist_to_binary(U), + server = iolist_to_binary(S), + resource = iolist_to_binary(R), + luser = iolist_to_binary(LU), + lserver = iolist_to_binary(LS), + lresource = iolist_to_binary(LR)}. + +now_to_integer({MS, S, US}) -> + (MS * 1000000 + S) * 1000000 + US. + +integer_to_now(Int) -> + Secs = Int div 1000000, + USec = Int rem 1000000, + MSec = Secs div 1000000, + Sec = Secs rem 1000000, + {MSec, Sec, USec}. diff --git a/src/mod_offline_opt.erl b/src/mod_offline_opt.erl new file mode 100644 index 000000000..e9ab7c71b --- /dev/null +++ b/src/mod_offline_opt.erl @@ -0,0 +1,69 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_offline_opt). + +-export([access_max_user_messages/1]). +-export([bounce_groupchat/1]). +-export([cache_life_time/1]). +-export([cache_size/1]). +-export([db_type/1]). +-export([store_empty_body/1]). +-export([store_groupchat/1]). +-export([use_cache/1]). +-export([use_mam_for_storage/1]). + +-spec access_max_user_messages(gen_mod:opts() | global | binary()) -> atom() | [ejabberd_shaper:shaper_rule()]. +access_max_user_messages(Opts) when is_map(Opts) -> + gen_mod:get_opt(access_max_user_messages, Opts); +access_max_user_messages(Host) -> + gen_mod:get_module_opt(Host, mod_offline, access_max_user_messages). + +-spec bounce_groupchat(gen_mod:opts() | global | binary()) -> boolean(). +bounce_groupchat(Opts) when is_map(Opts) -> + gen_mod:get_opt(bounce_groupchat, Opts); +bounce_groupchat(Host) -> + gen_mod:get_module_opt(Host, mod_offline, bounce_groupchat). + +-spec cache_life_time(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +cache_life_time(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_life_time, Opts); +cache_life_time(Host) -> + gen_mod:get_module_opt(Host, mod_offline, cache_life_time). + +-spec cache_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +cache_size(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_size, Opts); +cache_size(Host) -> + gen_mod:get_module_opt(Host, mod_offline, cache_size). + +-spec db_type(gen_mod:opts() | global | binary()) -> atom(). +db_type(Opts) when is_map(Opts) -> + gen_mod:get_opt(db_type, Opts); +db_type(Host) -> + gen_mod:get_module_opt(Host, mod_offline, db_type). + +-spec store_empty_body(gen_mod:opts() | global | binary()) -> 'false' | 'true' | 'unless_chat_state'. +store_empty_body(Opts) when is_map(Opts) -> + gen_mod:get_opt(store_empty_body, Opts); +store_empty_body(Host) -> + gen_mod:get_module_opt(Host, mod_offline, store_empty_body). + +-spec store_groupchat(gen_mod:opts() | global | binary()) -> boolean(). +store_groupchat(Opts) when is_map(Opts) -> + gen_mod:get_opt(store_groupchat, Opts); +store_groupchat(Host) -> + gen_mod:get_module_opt(Host, mod_offline, store_groupchat). + +-spec use_cache(gen_mod:opts() | global | binary()) -> boolean(). +use_cache(Opts) when is_map(Opts) -> + gen_mod:get_opt(use_cache, Opts); +use_cache(Host) -> + gen_mod:get_module_opt(Host, mod_offline, use_cache). + +-spec use_mam_for_storage(gen_mod:opts() | global | binary()) -> boolean(). +use_mam_for_storage(Opts) when is_map(Opts) -> + gen_mod:get_opt(use_mam_for_storage, Opts); +use_mam_for_storage(Host) -> + gen_mod:get_module_opt(Host, mod_offline, use_mam_for_storage). + diff --git a/src/mod_offline_sql.erl b/src/mod_offline_sql.erl new file mode 100644 index 000000000..9078b082c --- /dev/null +++ b/src/mod_offline_sql.erl @@ -0,0 +1,331 @@ +%%%------------------------------------------------------------------- +%%% File : mod_offline_sql.erl +%%% Author : Evgeny Khramtsov +%%% Created : 15 Apr 2016 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_offline_sql). + + +-behaviour(mod_offline). + +-export([init/2, store_message/1, pop_messages/2, remove_expired_messages/1, + remove_old_messages/2, remove_user/2, read_message_headers/2, + read_message/3, remove_message/3, read_all_messages/2, + remove_all_messages/2, count_messages/2, import/1, export/1, remove_old_messages_batch/3]). +-export([sql_schemas/0]). + +-include_lib("xmpp/include/xmpp.hrl"). +-include("mod_offline.hrl"). +-include("logger.hrl"). +-include("ejabberd_sql_pt.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(Host, _Opts) -> + ejabberd_sql_schema:update_schema(Host, ?MODULE, sql_schemas()), + ok. + +sql_schemas() -> + [#sql_schema{ + version = 1, + tables = + [#sql_table{ + name = <<"spool">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"xml">>, type = {text, big}}, + #sql_column{name = <<"seq">>, type = bigserial}, + #sql_column{name = <<"created_at">>, type = timestamp, + default = true}], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"username">>]}, + #sql_index{ + columns = [<<"created_at">>]}]}]}]. + +store_message(#offline_msg{us = {LUser, LServer}} = M) -> + From = M#offline_msg.from, + To = M#offline_msg.to, + Packet = xmpp:set_from_to(M#offline_msg.packet, From, To), + NewPacket = misc:add_delay_info( + Packet, jid:make(LServer), + M#offline_msg.timestamp, + <<"Offline Storage">>), + XML = fxml:element_to_binary( + xmpp:encode(NewPacket)), + case ejabberd_sql:sql_query( + LServer, + ?SQL_INSERT( + "spool", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "xml=%(XML)s"])) of + {updated, _} -> + ok; + _ -> + {error, db_failure} + end. + +pop_messages(LUser, LServer) -> + case get_and_del_spool_msg_t(LServer, LUser) of + {atomic, {selected, Rs}} -> + {ok, lists:flatmap( + fun({_, XML}) -> + case xml_to_offline_msg(XML) of + {ok, Msg} -> + [Msg]; + _Err -> + [] + end + end, Rs)}; + Err -> + {error, Err} + end. + +remove_expired_messages(_LServer) -> + %% TODO + {atomic, ok}. + +remove_old_messages(Days, LServer) -> + case ejabberd_sql:sql_query( + LServer, + fun(pgsql, _) -> + ejabberd_sql:sql_query_t( + ?SQL("DELETE FROM spool" + " WHERE created_at <" + " NOW() - %(Days)d * INTERVAL '1 DAY'")); + (sqlite, _) -> + ejabberd_sql:sql_query_t( + ?SQL("DELETE FROM spool" + " WHERE created_at <" + " DATETIME('now', '-%(Days)d days')")); + (_, _) -> + ejabberd_sql:sql_query_t( + ?SQL("DELETE FROM spool" + " WHERE created_at < NOW() - INTERVAL %(Days)d DAY")) + end) + of + {updated, N} -> + ?INFO_MSG("~p message(s) deleted from offline spool", [N]); + Error -> + ?ERROR_MSG("Cannot delete message in offline spool: ~p", [Error]) + end, + {atomic, ok}. + +remove_old_messages_batch(LServer, Days, Batch) -> + case ejabberd_sql:sql_query( + LServer, + fun(pgsql, _) -> + ejabberd_sql:sql_query_t( + ?SQL("DELETE FROM spool" + " WHERE created_at <" + " NOW() - %(Days)d * INTERVAL '1 DAY' LIMIT %(Batch)d")); + (sqlite, _) -> + ejabberd_sql:sql_query_t( + ?SQL("DELETE FROM spool" + " WHERE created_at <" + " DATETIME('now', '-%(Days)d days') LIMIT %(Batch)d")); + (_, _) -> + ejabberd_sql:sql_query_t( + ?SQL("DELETE FROM spool" + " WHERE created_at < NOW() - INTERVAL %(Days)d DAY LIMIT %(Batch)d")) + end) + of + {updated, N} -> + {ok, N}; + Error -> + {error, Error} + end. + +remove_user(LUser, LServer) -> + ejabberd_sql:sql_query( + LServer, + ?SQL("delete from spool where username=%(LUser)s and %(LServer)H")). + +read_message_headers(LUser, LServer) -> + case ejabberd_sql:sql_query( + LServer, + ?SQL("select @(xml)s, @(seq)d from spool" + " where username=%(LUser)s and %(LServer)H order by seq")) of + {selected, Rows} -> + lists:flatmap( + fun({XML, Seq}) -> + case xml_to_offline_msg(XML) of + {ok, #offline_msg{from = From, + to = To, + timestamp = TS, + packet = El}} -> + [{Seq, From, To, TS, El}]; + _ -> + [] + end + end, Rows); + _Err -> + error + end. + +read_message(LUser, LServer, Seq) -> + case ejabberd_sql:sql_query( + LServer, + ?SQL("select @(xml)s from spool where username=%(LUser)s" + " and %(LServer)H" + " and seq=%(Seq)d")) of + {selected, [{RawXML}|_]} -> + case xml_to_offline_msg(RawXML) of + {ok, Msg} -> + {ok, Msg}; + _ -> + error + end; + _ -> + error + end. + +remove_message(LUser, LServer, Seq) -> + ejabberd_sql:sql_query( + LServer, + ?SQL("delete from spool where username=%(LUser)s and %(LServer)H" + " and seq=%(Seq)d")), + ok. + +read_all_messages(LUser, LServer) -> + case ejabberd_sql:sql_query( + LServer, + ?SQL("select @(xml)s from spool where " + "username=%(LUser)s and %(LServer)H order by seq")) of + {selected, Rs} -> + lists:flatmap( + fun({XML}) -> + case xml_to_offline_msg(XML) of + {ok, Msg} -> [Msg]; + _ -> [] + end + end, Rs); + _ -> + [] + end. + +remove_all_messages(LUser, LServer) -> + remove_user(LUser, LServer), + {atomic, ok}. + +count_messages(LUser, LServer) -> + case catch ejabberd_sql:sql_query( + LServer, + ?SQL("select @(count(*))d from spool " + "where username=%(LUser)s and %(LServer)H")) of + {selected, [{Res}]} -> + {cache, Res}; + {selected, []} -> + {cache, 0}; + _ -> + {nocache, 0} + end. + +export(_Server) -> + [{offline_msg, + fun(Host, #offline_msg{us = {LUser, LServer}}) + when LServer == Host -> + [?SQL("delete from spool where username=%(LUser)s" + " and %(LServer)H;")]; + (_Host, _R) -> + [] + end}, + {offline_msg, + fun(Host, #offline_msg{us = {LUser, LServer}, + timestamp = TimeStamp, from = From, to = To, + packet = El}) + when LServer == Host -> + try xmpp:decode(El, ?NS_CLIENT, [ignore_els]) of + Packet -> + Packet1 = xmpp:set_from_to(Packet, From, To), + Packet2 = misc:add_delay_info( + Packet1, jid:make(LServer), + TimeStamp, <<"Offline Storage">>), + XML = fxml:element_to_binary(xmpp:encode(Packet2)), + [?SQL_INSERT( + "spool", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "xml=%(XML)s"])] + catch _:{xmpp_codec, Why} -> + ?ERROR_MSG("Failed to decode packet ~p of user ~ts@~ts: ~ts", + [El, LUser, LServer, xmpp:format_error(Why)]), + [] + end; + (_Host, _R) -> + [] + end}]. + +import(_) -> + ok. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +xml_to_offline_msg(XML) -> + case fxml_stream:parse_element(XML) of + #xmlel{} = El -> + el_to_offline_msg(El); + Err -> + ?ERROR_MSG("Got ~p when parsing XML packet ~ts", + [Err, XML]), + Err + end. + +el_to_offline_msg(El) -> + To_s = fxml:get_tag_attr_s(<<"to">>, El), + From_s = fxml:get_tag_attr_s(<<"from">>, El), + try + To = jid:decode(To_s), + From = jid:decode(From_s), + {ok, #offline_msg{us = {To#jid.luser, To#jid.lserver}, + from = From, + to = To, + packet = El}} + catch _:{bad_jid, To_s} -> + ?ERROR_MSG("Failed to get 'to' JID from offline XML ~p", [El]), + {error, bad_jid_to}; + _:{bad_jid, From_s} -> + ?ERROR_MSG("Failed to get 'from' JID from offline XML ~p", [El]), + {error, bad_jid_from} + end. + +get_and_del_spool_msg_t(LServer, LUser) -> + F = fun () -> + Result = + ejabberd_sql:sql_query_t( + ?SQL("select @(username)s, @(xml)s from spool where " + "username=%(LUser)s and %(LServer)H order by seq;")), + DResult = + ejabberd_sql:sql_query_t( + ?SQL("delete from spool where" + " username=%(LUser)s and %(LServer)H;")), + case {Result, DResult} of + {{selected, Rs}, {updated, DC}} when length(Rs) /= DC -> + ejabberd_sql:restart(concurent_insert); + _ -> + Result + end + end, + ejabberd_sql:sql_transaction(LServer, F). diff --git a/src/mod_ping.erl b/src/mod_ping.erl index 87cf6e015..b760db68c 100644 --- a/src/mod_ping.erl +++ b/src/mod_ping.erl @@ -5,7 +5,7 @@ %%% Created : 11 Jul 2009 by Brian Cully %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -27,56 +27,49 @@ -author('bjc@kublai.com'). --behavior(gen_mod). +-protocol({xep, 199, '2.0', '2.1.0', "complete", ""}). --behavior(gen_server). +-behaviour(gen_mod). + +-behaviour(gen_server). --include("ejabberd.hrl"). -include("logger.hrl"). --include("jlib.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). --define(SUPERVISOR, ejabberd_sup). - --define(DEFAULT_SEND_PINGS, false). - --define(DEFAULT_PING_INTERVAL, 60). - --define(DICT, dict). +-include("translate.hrl"). %% API --export([start_link/2, start_ping/2, stop_ping/2]). +-export([start_ping/2, stop_ping/2]). %% gen_mod callbacks --export([start/2, stop/1]). +-export([start/2, stop/1, reload/3]). %% gen_server callbacks -export([init/1, terminate/2, handle_call/3, handle_cast/2, handle_info/2, code_change/3]). -%% Hook callbacks --export([iq_ping/3, user_online/3, user_offline/3, - user_send/3]). +-export([iq_ping/1, user_online/3, user_offline/3, mod_doc/0, user_send/1, + c2s_handle_cast/2, mod_opt_type/1, mod_options/1, depends/2]). -record(state, - {host = <<"">>, - send_pings = ?DEFAULT_SEND_PINGS :: boolean(), - ping_interval = ?DEFAULT_PING_INTERVAL :: non_neg_integer(), - timeout_action = none :: none | kill, - timers = (?DICT):new() :: dict()}). + {host :: binary(), + send_pings :: boolean(), + ping_interval :: pos_integer(), + timeout_action :: none | kill, + timers :: timers()}). + +-type timers() :: #{ljid() => reference()}. %%==================================================================== %% API %%==================================================================== -start_link(Host, Opts) -> - Proc = gen_mod:get_module_proc(Host, ?MODULE), - gen_server:start_link({local, Proc}, ?MODULE, - [Host, Opts], []). - +-spec start_ping(binary(), jid()) -> ok. start_ping(Host, JID) -> Proc = gen_mod:get_module_proc(Host, ?MODULE), gen_server:cast(Proc, {start_ping, JID}). +-spec stop_ping(binary(), jid()) -> ok. stop_ping(Host, JID) -> Proc = gen_mod:get_module_proc(Host, ?MODULE), gen_server:cast(Proc, {stop_ping, JID}). @@ -85,71 +78,48 @@ stop_ping(Host, JID) -> %% gen_mod callbacks %%==================================================================== start(Host, Opts) -> - Proc = gen_mod:get_module_proc(Host, ?MODULE), - PingSpec = {Proc, {?MODULE, start_link, [Host, Opts]}, - transient, 2000, worker, [?MODULE]}, - supervisor:start_child(?SUPERVISOR, PingSpec). + gen_mod:start_child(?MODULE, Host, Opts). stop(Host) -> + gen_mod:stop_child(?MODULE, Host). + +reload(Host, NewOpts, OldOpts) -> Proc = gen_mod:get_module_proc(Host, ?MODULE), - gen_server:call(Proc, stop), - supervisor:delete_child(?SUPERVISOR, Proc). + gen_server:cast(Proc, {reload, Host, NewOpts, OldOpts}). %%==================================================================== %% gen_server callbacks %%==================================================================== -init([Host, Opts]) -> - SendPings = gen_mod:get_opt(send_pings, Opts, - fun(B) when is_boolean(B) -> B end, - ?DEFAULT_SEND_PINGS), - PingInterval = gen_mod:get_opt(ping_interval, Opts, - fun(I) when is_integer(I), I>0 -> I end, - ?DEFAULT_PING_INTERVAL), - TimeoutAction = gen_mod:get_opt(timeout_action, Opts, - fun(none) -> none; - (kill) -> kill - end, none), - IQDisc = gen_mod:get_opt(iqdisc, Opts, fun gen_iq_handler:check_type/1, - no_queue), - mod_disco:register_feature(Host, ?NS_PING), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, - ?NS_PING, ?MODULE, iq_ping, IQDisc), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, - ?NS_PING, ?MODULE, iq_ping, IQDisc), - case SendPings of - true -> - ejabberd_hooks:add(sm_register_connection_hook, Host, - ?MODULE, user_online, 100), - ejabberd_hooks:add(sm_remove_connection_hook, Host, - ?MODULE, user_offline, 100), - ejabberd_hooks:add(user_send_packet, Host, ?MODULE, - user_send, 100); - _ -> ok +init([Host|_]) -> + process_flag(trap_exit, true), + Opts = gen_mod:get_module_opts(Host, ?MODULE), + State = init_state(Host, Opts), + register_iq_handlers(Host), + case State#state.send_pings of + true -> register_hooks(Host); + false -> ok end, - {ok, - #state{host = Host, send_pings = SendPings, - ping_interval = PingInterval, - timeout_action = TimeoutAction, - timers = (?DICT):new()}}. + {ok, State}. terminate(_Reason, #state{host = Host}) -> - ejabberd_hooks:delete(sm_remove_connection_hook, Host, - ?MODULE, user_offline, 100), - ejabberd_hooks:delete(sm_register_connection_hook, Host, - ?MODULE, user_online, 100), - ejabberd_hooks:delete(user_send_packet, Host, ?MODULE, - user_send, 100), - gen_iq_handler:remove_iq_handler(ejabberd_local, Host, - ?NS_PING), - gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, - ?NS_PING), - mod_disco:unregister_feature(Host, ?NS_PING). + unregister_hooks(Host), + unregister_iq_handlers(Host). handle_call(stop, _From, State) -> {stop, normal, ok, State}; -handle_call(_Req, _From, State) -> - {reply, {error, badarg}, State}. +handle_call(Request, From, State) -> + ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), + {noreply, State}. +handle_cast({reload, Host, NewOpts, _OldOpts}, + #state{timers = Timers} = OldState) -> + NewState = init_state(Host, NewOpts), + case {NewState#state.send_pings, OldState#state.send_pings} of + {true, false} -> register_hooks(Host); + {false, true} -> unregister_hooks(Host); + _ -> ok + end, + {noreply, NewState#state{timers = Timers}}; handle_cast({start_ping, JID}, State) -> Timers = add_timer(JID, State#state.ping_interval, State#state.timers), @@ -157,90 +127,232 @@ handle_cast({start_ping, JID}, State) -> handle_cast({stop_ping, JID}, State) -> Timers = del_timer(JID, State#state.timers), {noreply, State#state{timers = Timers}}; -handle_cast({iq_pong, JID, timeout}, State) -> - Timers = del_timer(JID, State#state.timers), +handle_cast(Msg, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Msg]), + {noreply, State}. + +handle_info({iq_reply, #iq{type = error} = IQ, JID}, State) -> + Timers = case xmpp:get_error(IQ) of + #stanza_error{type=cancel, reason='service-unavailable'} -> + del_timer(JID, State#state.timers); + _ -> + State#state.timers + end, + {noreply, State#state{timers = Timers}}; +handle_info({iq_reply, #iq{}, _JID}, State) -> + {noreply, State}; +handle_info({iq_reply, timeout, JID}, State) -> ejabberd_hooks:run(user_ping_timeout, State#state.host, [JID]), - case State#state.timeout_action of - kill -> - #jid{user = User, server = Server, - resource = Resource} = - JID, - case ejabberd_sm:get_session_pid(User, Server, Resource) - of - Pid when is_pid(Pid) -> ejabberd_c2s:stop(Pid); - _ -> ok - end; - _ -> ok - end, + Timers = case State#state.timeout_action of + kill -> + #jid{user = User, server = Server, + resource = Resource} = + JID, + case ejabberd_sm:get_session_pid(User, Server, Resource) of + Pid when is_pid(Pid) -> + ejabberd_c2s:close(Pid, ping_timeout); + _ -> + ok + end, + del_timer(JID, State#state.timers); + _ -> + State#state.timers + end, {noreply, State#state{timers = Timers}}; -handle_cast(_Msg, State) -> {noreply, State}. - handle_info({timeout, _TRef, {ping, JID}}, State) -> - IQ = #iq{type = get, - sub_el = - [#xmlel{name = <<"ping">>, - attrs = [{<<"xmlns">>, ?NS_PING}], children = []}]}, - Pid = self(), - F = fun (Response) -> - gen_server:cast(Pid, {iq_pong, JID, Response}) - end, - From = jlib:make_jid(<<"">>, State#state.host, <<"">>), - ejabberd_local:route_iq(From, JID, IQ, F), - Timers = add_timer(JID, State#state.ping_interval, - State#state.timers), + Timers = case ejabberd_sm:get_session_pid(JID#jid.luser, + JID#jid.lserver, + JID#jid.lresource) of + none -> + del_timer(JID, State#state.timers); + Pid -> + ejabberd_c2s:cast(Pid, send_ping), + add_timer(JID, State#state.ping_interval, + State#state.timers) + end, {noreply, State#state{timers = Timers}}; -handle_info(_Info, State) -> {noreply, State}. +handle_info(Info, State) -> + ?WARNING_MSG("Unexpected info: ~p", [Info]), + {noreply, State}. code_change(_OldVsn, State, _Extra) -> {ok, State}. %%==================================================================== %% Hook callbacks %%==================================================================== -iq_ping(_From, _To, - #iq{type = Type, sub_el = SubEl} = IQ) -> - case {Type, SubEl} of - {get, #xmlel{name = <<"ping">>}} -> - IQ#iq{type = result, sub_el = []}; - _ -> - IQ#iq{type = error, - sub_el = [SubEl, ?ERR_FEATURE_NOT_IMPLEMENTED]} - end. +-spec iq_ping(iq()) -> iq(). +iq_ping(#iq{type = get, sub_els = [#ping{}]} = IQ) -> + xmpp:make_iq_result(IQ); +iq_ping(#iq{lang = Lang} = IQ) -> + Txt = ?T("Ping query is incorrect"), + xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)). +-spec user_online(ejabberd_sm:sid(), jid(), ejabberd_sm:info()) -> ok. user_online(_SID, JID, _Info) -> start_ping(JID#jid.lserver, JID). +-spec user_offline(ejabberd_sm:sid(), jid(), ejabberd_sm:info()) -> ok. user_offline(_SID, JID, _Info) -> - stop_ping(JID#jid.lserver, JID). + case ejabberd_sm:get_session_pid(JID#jid.luser, + JID#jid.lserver, + JID#jid.lresource) of + PID when PID =:= none; node(PID) /= node() -> + stop_ping(JID#jid.lserver, JID); + _ -> + ok + end. -user_send(JID, _From, _Packet) -> - start_ping(JID#jid.lserver, JID). +-spec user_send({stanza(), ejabberd_c2s:state()}) -> {stanza(), ejabberd_c2s:state()}. +user_send({Packet, #{jid := JID} = C2SState}) -> + start_ping(JID#jid.lserver, JID), + {Packet, C2SState}. + +-spec c2s_handle_cast(ejabberd_c2s:state(), send_ping | term()) + -> ejabberd_c2s:state() | {stop, ejabberd_c2s:state()}. +c2s_handle_cast(#{lserver := Host, jid := JID} = C2SState, send_ping) -> + From = jid:make(Host), + IQ = #iq{from = From, to = JID, type = get, sub_els = [#ping{}]}, + Proc = gen_mod:get_module_proc(Host, ?MODULE), + PingAckTimeout = mod_ping_opt:ping_ack_timeout(Host), + ejabberd_router:route_iq(IQ, JID, Proc, PingAckTimeout), + {stop, C2SState}; +c2s_handle_cast(C2SState, _Msg) -> + C2SState. %%==================================================================== %% Internal functions %%==================================================================== +init_state(Host, Opts) -> + SendPings = mod_ping_opt:send_pings(Opts), + PingInterval = mod_ping_opt:ping_interval(Opts), + TimeoutAction = mod_ping_opt:timeout_action(Opts), + #state{host = Host, + send_pings = SendPings, + ping_interval = PingInterval, + timeout_action = TimeoutAction, + timers = #{}}. + +register_hooks(Host) -> + ejabberd_hooks:add(sm_register_connection_hook, Host, + ?MODULE, user_online, 100), + ejabberd_hooks:add(sm_remove_connection_hook, Host, + ?MODULE, user_offline, 100), + ejabberd_hooks:add(user_send_packet, Host, ?MODULE, + user_send, 100), + ejabberd_hooks:add(c2s_handle_cast, Host, ?MODULE, + c2s_handle_cast, 99). + +unregister_hooks(Host) -> + ejabberd_hooks:delete(sm_remove_connection_hook, Host, + ?MODULE, user_offline, 100), + ejabberd_hooks:delete(sm_register_connection_hook, Host, + ?MODULE, user_online, 100), + ejabberd_hooks:delete(user_send_packet, Host, ?MODULE, + user_send, 100), + ejabberd_hooks:delete(c2s_handle_cast, Host, ?MODULE, + c2s_handle_cast, 99). + +register_iq_handlers(Host) -> + gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_PING, + ?MODULE, iq_ping), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_PING, + ?MODULE, iq_ping). + +unregister_iq_handlers(Host) -> + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_PING), + gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_PING). + +-spec add_timer(jid(), pos_integer(), timers()) -> timers(). add_timer(JID, Interval, Timers) -> - LJID = jlib:jid_tolower(JID), - NewTimers = case (?DICT):find(LJID, Timers) of - {ok, OldTRef} -> - cancel_timer(OldTRef), (?DICT):erase(LJID, Timers); - _ -> Timers + LJID = jid:tolower(JID), + NewTimers = case maps:find(LJID, Timers) of + {ok, OldTRef} -> + misc:cancel_timer(OldTRef), + maps:remove(LJID, Timers); + _ -> Timers end, - TRef = erlang:start_timer(Interval * 1000, self(), - {ping, JID}), - (?DICT):store(LJID, TRef, NewTimers). + TRef = erlang:start_timer(Interval, self(), {ping, JID}), + maps:put(LJID, TRef, NewTimers). +-spec del_timer(jid(), timers()) -> timers(). del_timer(JID, Timers) -> - LJID = jlib:jid_tolower(JID), - case (?DICT):find(LJID, Timers) of - {ok, TRef} -> - cancel_timer(TRef), (?DICT):erase(LJID, Timers); - _ -> Timers + LJID = jid:tolower(JID), + case maps:find(LJID, Timers) of + {ok, TRef} -> + misc:cancel_timer(TRef), + maps:remove(LJID, Timers); + _ -> Timers end. -cancel_timer(TRef) -> - case erlang:cancel_timer(TRef) of - false -> - receive {timeout, TRef, _} -> ok after 0 -> ok end; - _ -> ok - end. +depends(_Host, _Opts) -> + []. + +mod_opt_type(ping_interval) -> + econf:timeout(second); +mod_opt_type(ping_ack_timeout) -> + econf:timeout(second); +mod_opt_type(send_pings) -> + econf:bool(); +mod_opt_type(timeout_action) -> + econf:enum([none, kill]). + +mod_options(_Host) -> + [{ping_interval, timer:minutes(1)}, + {ping_ack_timeout, undefined}, + {send_pings, false}, + {timeout_action, none}]. + +mod_doc() -> + #{desc => + ?T("This module implements support for " + "https://xmpp.org/extensions/xep-0199.html" + "[XEP-0199: XMPP Ping] and periodic keepalives. " + "When this module is enabled ejabberd responds " + "correctly to ping requests, as defined by the protocol."), + opts => + [{ping_interval, + #{value => "timeout()", + desc => + ?T("How often to send pings to connected clients, " + "if option 'send_pings' is set to 'true'. If a client " + "connection does not send or receive any stanza " + "within this interval, a ping request is sent to " + "the client. The default value is '1' minute.")}}, + {ping_ack_timeout, + #{value => "timeout()", + desc => + ?T("How long to wait before deeming that a client " + "has not answered a given server ping request. NOTE: when " + "_`mod_stream_mgmt`_ is loaded and stream management is " + "enabled by a client, this value is ignored, and the " + "`ack_timeout` applies instead. " + "The default value is 'undefined'.")}}, + {send_pings, + #{value => "true | false", + desc => + ?T("If this option is set to 'true', the server " + "sends pings to connected clients that are not " + "active in a given interval defined in 'ping_interval' " + "option. This is useful to keep client connections " + "alive or checking availability. " + "The default value is 'false'.")}}, + {timeout_action, + #{value => "none | kill", + desc => + ?T("What to do when a client does not answer to a " + "server ping request in less than period defined " + "in 'ping_ack_timeout' option: " + "'kill' means destroying the underlying connection, " + "'none' means to do nothing. NOTE: when _`mod_stream_mgmt`_ " + "is loaded and stream management is enabled by " + "a client, killing the client connection doesn't 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 'none'.")}}], + example => + ["modules:", + " mod_ping:", + " send_pings: true", + " ping_interval: 4 min", + " timeout_action: kill"]}. diff --git a/src/mod_ping_opt.erl b/src/mod_ping_opt.erl new file mode 100644 index 000000000..fd0052130 --- /dev/null +++ b/src/mod_ping_opt.erl @@ -0,0 +1,34 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_ping_opt). + +-export([ping_ack_timeout/1]). +-export([ping_interval/1]). +-export([send_pings/1]). +-export([timeout_action/1]). + +-spec ping_ack_timeout(gen_mod:opts() | global | binary()) -> 'undefined' | pos_integer(). +ping_ack_timeout(Opts) when is_map(Opts) -> + gen_mod:get_opt(ping_ack_timeout, Opts); +ping_ack_timeout(Host) -> + gen_mod:get_module_opt(Host, mod_ping, ping_ack_timeout). + +-spec ping_interval(gen_mod:opts() | global | binary()) -> pos_integer(). +ping_interval(Opts) when is_map(Opts) -> + gen_mod:get_opt(ping_interval, Opts); +ping_interval(Host) -> + gen_mod:get_module_opt(Host, mod_ping, ping_interval). + +-spec send_pings(gen_mod:opts() | global | binary()) -> boolean(). +send_pings(Opts) when is_map(Opts) -> + gen_mod:get_opt(send_pings, Opts); +send_pings(Host) -> + gen_mod:get_module_opt(Host, mod_ping, send_pings). + +-spec timeout_action(gen_mod:opts() | global | binary()) -> 'kill' | 'none'. +timeout_action(Opts) when is_map(Opts) -> + gen_mod:get_opt(timeout_action, Opts); +timeout_action(Host) -> + gen_mod:get_module_opt(Host, mod_ping, timeout_action). + diff --git a/src/mod_pres_counter.erl b/src/mod_pres_counter.erl index e904ab95f..bb1b43af8 100644 --- a/src/mod_pres_counter.erl +++ b/src/mod_pres_counter.erl @@ -5,7 +5,7 @@ %%% Created : 23 Sep 2010 by Ahmed Omar %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -25,60 +25,58 @@ -module(mod_pres_counter). --behavior(gen_mod). +-behaviour(gen_mod). --export([start/2, stop/1, check_packet/6]). +-export([start/2, stop/1, reload/3, check_packet/4, + mod_opt_type/1, mod_options/1, depends/2, mod_doc/0]). --include("ejabberd.hrl"). -include("logger.hrl"). - --include("jlib.hrl"). +-include("translate.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). -record(pres_counter, {dir, start, count, logged = false}). -start(Host, _Opts) -> - ejabberd_hooks:add(privacy_check_packet, Host, ?MODULE, - check_packet, 25), +start(_Host, _Opts) -> + {ok, [{hook, privacy_check_packet, check_packet, 25}]}. + +stop(_Host) -> ok. -stop(Host) -> - ejabberd_hooks:delete(privacy_check_packet, Host, - ?MODULE, check_packet, 25), +reload(_Host, _NewOpts, _OldOpts) -> ok. -check_packet(_, _User, Server, _PrivacyList, - {From, To, #xmlel{name = Name, attrs = Attrs}}, Dir) -> - case Name of - <<"presence">> -> - IsSubscription = case xml:get_attr_s(<<"type">>, Attrs) - of - <<"subscribe">> -> true; - <<"subscribed">> -> true; - <<"unsubscribe">> -> true; - <<"unsubscribed">> -> true; - _ -> false - end, - if IsSubscription -> - JID = case Dir of - in -> To; - out -> From - end, - update(Server, JID, Dir); - true -> allow - end; - _ -> allow - end. +depends(_Host, _Opts) -> + []. + +-spec check_packet(allow | deny, ejabberd_c2s:state() | jid(), + stanza(), in | out) -> allow | deny. +check_packet(Acc, #{jid := JID}, Packet, Dir) -> + check_packet(Acc, JID, Packet, Dir); +check_packet(_, #jid{lserver = LServer}, + #presence{from = From, to = To, type = Type}, Dir) -> + IsSubscription = case Type of + subscribe -> true; + subscribed -> true; + unsubscribe -> true; + unsubscribed -> true; + _ -> false + end, + if IsSubscription -> + JID = case Dir of + in -> To; + out -> From + end, + update(LServer, JID, Dir); + true -> allow + end; +check_packet(Acc, _, _, _) -> + Acc. update(Server, JID, Dir) -> - StormCount = gen_mod:get_module_opt(Server, ?MODULE, count, - fun(I) when is_integer(I), I>0 -> I end, - 5), - TimeInterval = gen_mod:get_module_opt(Server, ?MODULE, interval, - fun(I) when is_integer(I), I>0 -> I end, - 60), - {MegaSecs, Secs, _MicroSecs} = now(), - TimeStamp = MegaSecs * 1000000 + Secs, + StormCount = mod_pres_counter_opt:count(Server), + TimeInterval = mod_pres_counter_opt:interval(Server), + TimeStamp = erlang:system_time(millisecond), case read(Dir) of undefined -> write(Dir, @@ -96,17 +94,17 @@ update(Server, JID, Dir) -> write(Dir, R#pres_counter{logged = true}), case Dir of in -> - ?WARNING_MSG("User ~s is being flooded, ignoring received " + ?WARNING_MSG("User ~ts is being flooded, ignoring received " "presence subscriptions", - [jlib:jid_to_string(JID)]); + [jid:encode(JID)]); out -> IP = ejabberd_sm:get_user_ip(JID#jid.luser, JID#jid.lserver, JID#jid.lresource), - ?WARNING_MSG("Flooder detected: ~s, on IP: ~s ignoring " + ?WARNING_MSG("Flooder detected: ~ts, on IP: ~ts ignoring " "sent presence subscriptions~n", - [jlib:jid_to_string(JID), - jlib:ip_to_list(IP)]) + [jid:encode(JID), + misc:ip_to_list(IP)]) end, {stop, deny}; true -> @@ -119,3 +117,39 @@ update(Server, JID, Dir) -> read(K) -> get({pres_counter, K}). write(K, V) -> put({pres_counter, K}, V). + +mod_opt_type(count) -> + econf:pos_int(); +mod_opt_type(interval) -> + econf:timeout(second). + +mod_options(_) -> + [{count, 5}, {interval, timer:seconds(60)}]. + +mod_doc() -> + #{desc => + ?T("This module detects flood/spam in presence " + "subscriptions traffic. If a user sends or receives " + "more of those stanzas in a given time interval, " + "the exceeding stanzas are silently dropped, and a " + "warning is logged."), + opts => + [{count, + #{value => ?T("Number"), + desc => + ?T("The number of subscription presence stanzas " + "(subscribe, unsubscribe, subscribed, unsubscribed) " + "allowed for any direction (input or output) per time " + "defined in 'interval' option. Please note that two " + "users subscribing to each other usually generate 4 " + "stanzas, so the recommended value is '4' or more. " + "The default value is '5'.")}}, + {interval, + #{value => "timeout()", + desc => + ?T("The time interval. The default value is '1' minute.")}}], + example => + ["modules:", + " mod_pres_counter:", + " count: 5", + " interval: 30 secs"]}. diff --git a/src/mod_pres_counter_opt.erl b/src/mod_pres_counter_opt.erl new file mode 100644 index 000000000..7964fe368 --- /dev/null +++ b/src/mod_pres_counter_opt.erl @@ -0,0 +1,20 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_pres_counter_opt). + +-export([count/1]). +-export([interval/1]). + +-spec count(gen_mod:opts() | global | binary()) -> pos_integer(). +count(Opts) when is_map(Opts) -> + gen_mod:get_opt(count, Opts); +count(Host) -> + gen_mod:get_module_opt(Host, mod_pres_counter, count). + +-spec interval(gen_mod:opts() | global | binary()) -> pos_integer(). +interval(Opts) when is_map(Opts) -> + gen_mod:get_opt(interval, Opts); +interval(Host) -> + gen_mod:get_module_opt(Host, mod_pres_counter, interval). + diff --git a/src/mod_privacy.erl b/src/mod_privacy.erl index c83a953c4..c28e6fd89 100644 --- a/src/mod_privacy.erl +++ b/src/mod_privacy.erl @@ -5,7 +5,7 @@ %%% Created : 21 Jul 2003 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -27,840 +27,565 @@ -author('alexey@process-one.net'). +-protocol({xep, 16, '1.6', '0.5.0', "complete", ""}). + -behaviour(gen_mod). --export([start/2, stop/1, process_iq/3, export/1, import/1, - process_iq_set/4, process_iq_get/5, get_user_list/3, - check_packet/6, remove_user/2, item_to_raw/1, - raw_to_item/1, is_list_needdb/1, updated_list/3, - item_to_xml/1, get_user_lists/2, import/3]). +-export([start/2, stop/1, reload/3, process_iq/1, export/1, + c2s_copy_session/2, push_list_update/2, disco_features/5, + check_packet/4, remove_user/2, encode_list_item/1, + get_user_lists/2, get_user_list/3, + set_list/1, set_list/4, set_default_list/3, + user_send_packet/1, mod_doc/0, + import_start/2, import_stop/2, import/5, import_info/0, + mod_opt_type/1, mod_options/1, depends/2]). -%% For mod_blocking --export([sql_add_privacy_list/2, - sql_get_default_privacy_list/2, - sql_get_default_privacy_list_t/1, - sql_get_privacy_list_data/3, - sql_get_privacy_list_data_by_id_t/1, - sql_get_privacy_list_id_t/2, - sql_set_default_privacy_list/2, - sql_set_privacy_list/2, privacy_schema/0]). +-export([webadmin_menu_hostuser/4, webadmin_page_hostuser/4]). + +-import(ejabberd_web_admin, [make_command/4, make_command/2]). --include("ejabberd.hrl"). -include("logger.hrl"). - --include("jlib.hrl"). - +-include_lib("xmpp/include/xmpp.hrl"). +-include("ejabberd_http.hrl"). +-include("ejabberd_web_admin.hrl"). -include("mod_privacy.hrl"). +-include("translate.hrl"). -privacy_schema() -> - {record_info(fields, privacy), #privacy{}}. +-define(PRIVACY_CACHE, privacy_cache). +-define(PRIVACY_LIST_CACHE, privacy_list_cache). + +-type c2s_state() :: ejabberd_c2s:state(). +-callback init(binary(), gen_mod:opts()) -> any(). +-callback import(#privacy{}) -> ok. +-callback set_default(binary(), binary(), binary()) -> + ok | {error, notfound | any()}. +-callback unset_default(binary(), binary()) -> ok | {error, any()}. +-callback remove_list(binary(), binary(), binary()) -> + ok | {error, notfound | conflict | any()}. +-callback remove_lists(binary(), binary()) -> ok | {error, any()}. +-callback set_lists(#privacy{}) -> ok | {error, any()}. +-callback set_list(binary(), binary(), binary(), [listitem()]) -> + ok | {error, any()}. +-callback get_list(binary(), binary(), binary() | default) -> + {ok, {binary(), [listitem()]}} | error | {error, any()}. +-callback get_lists(binary(), binary()) -> + {ok, #privacy{}} | error | {error, any()}. +-callback use_cache(binary()) -> boolean(). +-callback cache_nodes(binary()) -> [node()]. + +-optional_callbacks([use_cache/1, cache_nodes/1]). start(Host, Opts) -> - IQDisc = gen_mod:get_opt(iqdisc, Opts, fun gen_iq_handler:check_type/1, - one_queue), - case gen_mod:db_type(Opts) of - mnesia -> - mnesia:create_table(privacy, - [{disc_copies, [node()]}, - {attributes, record_info(fields, privacy)}]), - update_table(); - _ -> ok + Mod = gen_mod:db_mod(Opts, ?MODULE), + Mod:init(Host, Opts), + init_cache(Mod, Host, Opts), + {ok, [{hook, disco_local_features, disco_features, 50}, + {hook, c2s_copy_session, c2s_copy_session, 50}, + {hook, user_send_packet, user_send_packet, 50}, + {hook, privacy_check_packet, check_packet, 50}, + {hook, remove_user, remove_user, 50}, + {hook, webadmin_menu_hostuser, webadmin_menu_hostuser, 50}, + {hook, webadmin_page_hostuser, webadmin_page_hostuser, 50}, + {iq_handler, ejabberd_sm, ?NS_PRIVACY, process_iq}]}. + +stop(_Host) -> + ok. + +reload(Host, NewOpts, OldOpts) -> + NewMod = gen_mod:db_mod(NewOpts, ?MODULE), + OldMod = gen_mod:db_mod(OldOpts, ?MODULE), + if NewMod /= OldMod -> + NewMod:init(Host, NewOpts); + true -> + ok end, - mod_disco:register_feature(Host, ?NS_PRIVACY), - ejabberd_hooks:add(privacy_iq_get, Host, ?MODULE, - process_iq_get, 50), - ejabberd_hooks:add(privacy_iq_set, Host, ?MODULE, - process_iq_set, 50), - ejabberd_hooks:add(privacy_get_user_list, Host, ?MODULE, - get_user_list, 50), - ejabberd_hooks:add(privacy_check_packet, Host, ?MODULE, - check_packet, 50), - ejabberd_hooks:add(privacy_updated_list, Host, ?MODULE, - updated_list, 50), - ejabberd_hooks:add(remove_user, Host, ?MODULE, - remove_user, 50), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, - ?NS_PRIVACY, ?MODULE, process_iq, IQDisc). + init_cache(NewMod, Host, NewOpts). -stop(Host) -> - mod_disco:unregister_feature(Host, ?NS_PRIVACY), - ejabberd_hooks:delete(privacy_iq_get, Host, ?MODULE, - process_iq_get, 50), - ejabberd_hooks:delete(privacy_iq_set, Host, ?MODULE, - process_iq_set, 50), - ejabberd_hooks:delete(privacy_get_user_list, Host, - ?MODULE, get_user_list, 50), - ejabberd_hooks:delete(privacy_check_packet, Host, - ?MODULE, check_packet, 50), - ejabberd_hooks:delete(privacy_updated_list, Host, - ?MODULE, updated_list, 50), - ejabberd_hooks:delete(remove_user, Host, ?MODULE, - remove_user, 50), - gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, - ?NS_PRIVACY). +-spec disco_features({error, stanza_error()} | {result, [binary()]} | empty, + jid(), jid(), binary(), binary()) -> + {error, stanza_error()} | {result, [binary()]}. +disco_features({error, Err}, _From, _To, _Node, _Lang) -> + {error, Err}; +disco_features(empty, _From, _To, <<"">>, _Lang) -> + {result, [?NS_PRIVACY]}; +disco_features({result, Feats}, _From, _To, <<"">>, _Lang) -> + {result, [?NS_PRIVACY|Feats]}; +disco_features(Acc, _From, _To, _Node, _Lang) -> + Acc. -process_iq(_From, _To, IQ) -> - SubEl = IQ#iq.sub_el, - IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}. - -process_iq_get(_, From, _To, #iq{sub_el = SubEl}, - #userlist{name = Active}) -> - #jid{luser = LUser, lserver = LServer} = From, - #xmlel{children = Els} = SubEl, - case xml:remove_cdata(Els) of - [] -> process_lists_get(LUser, LServer, Active); - [#xmlel{name = Name, attrs = Attrs}] -> - case Name of - <<"list">> -> - ListName = xml:get_attr(<<"name">>, Attrs), - process_list_get(LUser, LServer, ListName); - _ -> {error, ?ERR_BAD_REQUEST} - end; - _ -> {error, ?ERR_BAD_REQUEST} - end. - -process_lists_get(LUser, LServer, Active) -> - case process_lists_get(LUser, LServer, Active, - gen_mod:db_type(LServer, ?MODULE)) - of - error -> {error, ?ERR_INTERNAL_SERVER_ERROR}; - {_Default, []} -> - {result, - [#xmlel{name = <<"query">>, - attrs = [{<<"xmlns">>, ?NS_PRIVACY}], children = []}]}; - {Default, LItems} -> - DItems = case Default of - none -> LItems; - _ -> - [#xmlel{name = <<"default">>, - attrs = [{<<"name">>, Default}], children = []} - | LItems] - end, - ADItems = case Active of - none -> DItems; - _ -> - [#xmlel{name = <<"active">>, - attrs = [{<<"name">>, Active}], children = []} - | DItems] - end, - {result, - [#xmlel{name = <<"query">>, - attrs = [{<<"xmlns">>, ?NS_PRIVACY}], - children = ADItems}]} - end. - -process_lists_get(LUser, LServer, _Active, mnesia) -> - case catch mnesia:dirty_read(privacy, {LUser, LServer}) - of - {'EXIT', _Reason} -> error; - [] -> {none, []}; - [#privacy{default = Default, lists = Lists}] -> - LItems = lists:map(fun ({N, _}) -> - #xmlel{name = <<"list">>, - attrs = [{<<"name">>, N}], - children = []} - end, - Lists), - {Default, LItems} - end; -process_lists_get(LUser, LServer, _Active, riak) -> - case ejabberd_riak:get(privacy, privacy_schema(), {LUser, LServer}) of - {ok, #privacy{default = Default, lists = Lists}} -> - LItems = lists:map(fun ({N, _}) -> - #xmlel{name = <<"list">>, - attrs = [{<<"name">>, N}], - children = []} - end, - Lists), - {Default, LItems}; - {error, notfound} -> - {none, []}; - {error, _} -> - error - end; -process_lists_get(LUser, LServer, _Active, odbc) -> - Default = case catch sql_get_default_privacy_list(LUser, - LServer) - of - {selected, [<<"name">>], []} -> none; - {selected, [<<"name">>], [[DefName]]} -> DefName; - _ -> none - end, - case catch sql_get_privacy_list_names(LUser, LServer) of - {selected, [<<"name">>], Names} -> - LItems = lists:map(fun ([N]) -> - #xmlel{name = <<"list">>, - attrs = [{<<"name">>, N}], - children = []} - end, - Names), - {Default, LItems}; - _ -> error - end. - -process_list_get(LUser, LServer, {value, Name}) -> - case process_list_get(LUser, LServer, Name, - gen_mod:db_type(LServer, ?MODULE)) - of - error -> {error, ?ERR_INTERNAL_SERVER_ERROR}; - not_found -> {error, ?ERR_ITEM_NOT_FOUND}; - Items -> - LItems = lists:map(fun item_to_xml/1, Items), - {result, - [#xmlel{name = <<"query">>, - attrs = [{<<"xmlns">>, ?NS_PRIVACY}], - children = - [#xmlel{name = <<"list">>, attrs = [{<<"name">>, Name}], - children = LItems}]}]} - end; -process_list_get(_LUser, _LServer, false) -> - {error, ?ERR_BAD_REQUEST}. - -process_list_get(LUser, LServer, Name, mnesia) -> - case catch mnesia:dirty_read(privacy, {LUser, LServer}) - of - {'EXIT', _Reason} -> error; - [] -> not_found; - [#privacy{lists = Lists}] -> - case lists:keysearch(Name, 1, Lists) of - {value, {_, List}} -> List; - _ -> not_found - end - end; -process_list_get(LUser, LServer, Name, riak) -> - case ejabberd_riak:get(privacy, privacy_schema(), {LUser, LServer}) of - {ok, #privacy{lists = Lists}} -> - case lists:keysearch(Name, 1, Lists) of - {value, {_, List}} -> List; - _ -> not_found - end; - {error, notfound} -> - not_found; - {error, _} -> - error - end; -process_list_get(LUser, LServer, Name, odbc) -> - case catch sql_get_privacy_list_id(LUser, LServer, Name) - of - {selected, [<<"id">>], []} -> not_found; - {selected, [<<"id">>], [[ID]]} -> - case catch sql_get_privacy_list_data_by_id(ID, LServer) - of - {selected, - [<<"t">>, <<"value">>, <<"action">>, <<"ord">>, - <<"match_all">>, <<"match_iq">>, <<"match_message">>, - <<"match_presence_in">>, <<"match_presence_out">>], - RItems} -> - lists:map(fun raw_to_item/1, RItems); - _ -> error - end; - _ -> error - end. - -item_to_xml(Item) -> - Attrs1 = [{<<"action">>, - action_to_list(Item#listitem.action)}, - {<<"order">>, order_to_list(Item#listitem.order)}], - Attrs2 = case Item#listitem.type of - none -> Attrs1; - Type -> - [{<<"type">>, type_to_list(Item#listitem.type)}, - {<<"value">>, value_to_list(Type, Item#listitem.value)} - | Attrs1] - end, - SubEls = case Item#listitem.match_all of - true -> []; - false -> - SE1 = case Item#listitem.match_iq of - true -> - [#xmlel{name = <<"iq">>, attrs = [], - children = []}]; - false -> [] - end, - SE2 = case Item#listitem.match_message of - true -> - [#xmlel{name = <<"message">>, attrs = [], - children = []} - | SE1]; - false -> SE1 - end, - SE3 = case Item#listitem.match_presence_in of - true -> - [#xmlel{name = <<"presence-in">>, attrs = [], - children = []} - | SE2]; - false -> SE2 - end, - SE4 = case Item#listitem.match_presence_out of - true -> - [#xmlel{name = <<"presence-out">>, attrs = [], - children = []} - | SE3]; - false -> SE3 - end, - SE4 - end, - #xmlel{name = <<"item">>, attrs = Attrs2, - children = SubEls}. - -action_to_list(Action) -> - case Action of - allow -> <<"allow">>; - deny -> <<"deny">> - end. - -order_to_list(Order) -> - iolist_to_binary(integer_to_list(Order)). - -type_to_list(Type) -> +-spec process_iq(iq()) -> iq(). +process_iq(#iq{type = Type, + from = #jid{luser = U, lserver = S}, + to = #jid{luser = U, lserver = S}} = IQ) -> case Type of - jid -> <<"jid">>; - group -> <<"group">>; - subscription -> <<"subscription">> + get -> process_iq_get(IQ); + set -> process_iq_set(IQ) + end; +process_iq(#iq{lang = Lang} = IQ) -> + Txt = ?T("Query to another users is forbidden"), + xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)). + +-spec process_iq_get(iq()) -> iq(). +process_iq_get(#iq{lang = Lang, + sub_els = [#privacy_query{default = Default, + active = Active}]} = IQ) + when Default /= undefined; Active /= undefined -> + Txt = ?T("Only element is allowed in this query"), + xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)); +process_iq_get(#iq{lang = Lang, + sub_els = [#privacy_query{lists = Lists}]} = IQ) -> + case Lists of + [] -> + process_lists_get(IQ); + [#privacy_list{name = ListName}] -> + process_list_get(IQ, ListName); + _ -> + Txt = ?T("Too many elements"), + xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)) + end; +process_iq_get(#iq{lang = Lang} = IQ) -> + Txt = ?T("No module is handling this query"), + xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)). + +-spec process_lists_get(iq()) -> iq(). +process_lists_get(#iq{from = #jid{luser = LUser, lserver = LServer}, + lang = Lang} = IQ) -> + case get_user_lists(LUser, LServer) of + {ok, #privacy{default = Default, lists = Lists}} -> + Active = xmpp:get_meta(IQ, privacy_active_list, none), + xmpp:make_iq_result( + IQ, #privacy_query{active = Active, + default = Default, + lists = [#privacy_list{name = Name} + || {Name, _} <- Lists]}); + error -> + xmpp:make_iq_result( + IQ, #privacy_query{active = none, default = none}); + {error, _} -> + Txt = ?T("Database failure"), + xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) end. -value_to_list(Type, Val) -> +-spec process_list_get(iq(), binary()) -> iq(). +process_list_get(#iq{from = #jid{luser = LUser, lserver = LServer}, + lang = Lang} = IQ, Name) -> + case get_user_list(LUser, LServer, Name) of + {ok, {_, List}} -> + Items = lists:map(fun encode_list_item/1, List), + xmpp:make_iq_result( + IQ, + #privacy_query{ + lists = [#privacy_list{name = Name, items = Items}]}); + error -> + Txt = ?T("No privacy list with this name found"), + xmpp:make_error(IQ, xmpp:err_item_not_found(Txt, Lang)); + {error, _} -> + Txt = ?T("Database failure"), + xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) + end. + +-spec encode_list_item(listitem()) -> privacy_item(). +encode_list_item(#listitem{action = Action, + order = Order, + type = Type, + match_all = MatchAll, + match_iq = MatchIQ, + match_message = MatchMessage, + match_presence_in = MatchPresenceIn, + match_presence_out = MatchPresenceOut, + value = Value}) -> + Item = #privacy_item{action = Action, + order = Order, + type = case Type of + none -> undefined; + Type -> Type + end, + value = encode_value(Type, Value)}, + case MatchAll of + true -> + Item; + false -> + Item#privacy_item{message = MatchMessage, + iq = MatchIQ, + presence_in = MatchPresenceIn, + presence_out = MatchPresenceOut} + end. + +-spec encode_value(listitem_type(), listitem_value()) -> binary(). +encode_value(Type, Val) -> case Type of - jid -> jlib:jid_to_string(Val); - group -> Val; - subscription -> - case Val of - both -> <<"both">>; - to -> <<"to">>; - from -> <<"from">>; - none -> <<"none">> - end + jid -> jid:encode(Val); + group -> Val; + subscription -> + case Val of + both -> <<"both">>; + to -> <<"to">>; + from -> <<"from">>; + none -> <<"none">> + end; + none -> <<"">> end. -list_to_action(S) -> - case S of - <<"allow">> -> allow; - <<"deny">> -> deny +-spec decode_value(jid | subscription | group | undefined, binary()) -> + listitem_value(). +decode_value(Type, Value) -> + case Type of + jid -> jid:tolower(jid:decode(Value)); + subscription -> + case Value of + <<"from">> -> from; + <<"to">> -> to; + <<"both">> -> both; + <<"none">> -> none + end; + group when Value /= <<"">> -> Value; + undefined -> none end. -process_iq_set(_, From, _To, #iq{sub_el = SubEl}) -> - #jid{luser = LUser, lserver = LServer} = From, - #xmlel{children = Els} = SubEl, - case xml:remove_cdata(Els) of - [#xmlel{name = Name, attrs = Attrs, - children = SubEls}] -> - ListName = xml:get_attr(<<"name">>, Attrs), - case Name of - <<"list">> -> - process_list_set(LUser, LServer, ListName, - xml:remove_cdata(SubEls)); - <<"active">> -> - process_active_set(LUser, LServer, ListName); - <<"default">> -> - process_default_set(LUser, LServer, ListName); - _ -> {error, ?ERR_BAD_REQUEST} - end; - _ -> {error, ?ERR_BAD_REQUEST} - end. - -process_default_set(LUser, LServer, Value) -> - case process_default_set(LUser, LServer, Value, - gen_mod:db_type(LServer, ?MODULE)) - of - {atomic, error} -> {error, ?ERR_INTERNAL_SERVER_ERROR}; - {atomic, not_found} -> {error, ?ERR_ITEM_NOT_FOUND}; - {atomic, ok} -> {result, []}; - _ -> {error, ?ERR_INTERNAL_SERVER_ERROR} - end. - -process_default_set(LUser, LServer, {value, Name}, - mnesia) -> - F = fun () -> - case mnesia:read({privacy, {LUser, LServer}}) of - [] -> not_found; - [#privacy{lists = Lists} = P] -> - case lists:keymember(Name, 1, Lists) of - true -> - mnesia:write(P#privacy{default = Name, - lists = Lists}), - ok; - false -> not_found - end - end - end, - mnesia:transaction(F); -process_default_set(LUser, LServer, {value, Name}, riak) -> - {atomic, - case ejabberd_riak:get(privacy, privacy_schema(), {LUser, LServer}) of - {ok, #privacy{lists = Lists} = P} -> - case lists:keymember(Name, 1, Lists) of - true -> - ejabberd_riak:put(P#privacy{default = Name, - lists = Lists}, - privacy_schema()); - false -> - not_found - end; - {error, _} -> - not_found - end}; -process_default_set(LUser, LServer, {value, Name}, - odbc) -> - F = fun () -> - case sql_get_privacy_list_names_t(LUser) of - {selected, [<<"name">>], []} -> not_found; - {selected, [<<"name">>], Names} -> - case lists:member([Name], Names) of - true -> sql_set_default_privacy_list(LUser, Name), ok; - false -> not_found - end - end - end, - odbc_queries:sql_transaction(LServer, F); -process_default_set(LUser, LServer, false, mnesia) -> - F = fun () -> - case mnesia:read({privacy, {LUser, LServer}}) of - [] -> ok; - [R] -> mnesia:write(R#privacy{default = none}) - end - end, - mnesia:transaction(F); -process_default_set(LUser, LServer, false, riak) -> - {atomic, - case ejabberd_riak:get(privacy, privacy_schema(), {LUser, LServer}) of - {ok, R} -> - ejabberd_riak:put(R#privacy{default = none}, privacy_schema()); - {error, _} -> - ok - end}; -process_default_set(LUser, LServer, false, odbc) -> - case catch sql_unset_default_privacy_list(LUser, - LServer) - of - {'EXIT', _Reason} -> {atomic, error}; - {error, _Reason} -> {atomic, error}; - _ -> {atomic, ok} - end. - -process_active_set(LUser, LServer, {value, Name}) -> - case process_active_set(LUser, LServer, Name, - gen_mod:db_type(LServer, ?MODULE)) - of - error -> {error, ?ERR_ITEM_NOT_FOUND}; - Items -> - NeedDb = is_list_needdb(Items), - {result, [], - #userlist{name = Name, list = Items, needdb = NeedDb}} +-spec process_iq_set(iq()) -> iq(). +process_iq_set(#iq{lang = Lang, + sub_els = [#privacy_query{default = Default, + active = Active, + lists = Lists}]} = IQ) -> + case Lists of + [#privacy_list{items = Items, name = ListName}] + when Default == undefined, Active == undefined -> + process_lists_set(IQ, ListName, Items); + [] when Default == undefined, Active /= undefined -> + process_active_set(IQ, Active); + [] when Active == undefined, Default /= undefined -> + process_default_set(IQ, Default); + _ -> + Txt = ?T("The stanza MUST contain only one element, " + "one element, or one element"), + xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)) end; -process_active_set(_LUser, _LServer, false) -> - {result, [], #userlist{}}. +process_iq_set(#iq{lang = Lang} = IQ) -> + Txt = ?T("No module is handling this query"), + xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)). -process_active_set(LUser, LServer, Name, mnesia) -> - case catch mnesia:dirty_read(privacy, {LUser, LServer}) - of - [] -> error; - [#privacy{lists = Lists}] -> - case lists:keysearch(Name, 1, Lists) of - {value, {_, List}} -> List; - false -> error - end - end; -process_active_set(LUser, LServer, Name, riak) -> - case ejabberd_riak:get(privacy, privacy_schema(), {LUser, LServer}) of - {ok, #privacy{lists = Lists}} -> - case lists:keysearch(Name, 1, Lists) of - {value, {_, List}} -> List; - false -> error - end; - {error, _} -> - error - end; -process_active_set(LUser, LServer, Name, odbc) -> - case catch sql_get_privacy_list_id(LUser, LServer, Name) - of - {selected, [<<"id">>], []} -> error; - {selected, [<<"id">>], [[ID]]} -> - case catch sql_get_privacy_list_data_by_id(ID, LServer) - of - {selected, - [<<"t">>, <<"value">>, <<"action">>, <<"ord">>, - <<"match_all">>, <<"match_iq">>, <<"match_message">>, - <<"match_presence_in">>, <<"match_presence_out">>], - RItems} -> - lists:map(fun raw_to_item/1, RItems); - _ -> error - end; - _ -> error +-spec process_default_set(iq(), none | binary()) -> iq(). +process_default_set(#iq{from = #jid{luser = LUser, lserver = LServer}, + lang = Lang} = IQ, Value) -> + case set_default_list(LUser, LServer, Value) of + ok -> + xmpp:make_iq_result(IQ); + {error, notfound} -> + Txt = ?T("No privacy list with this name found"), + xmpp:make_error(IQ, xmpp:err_item_not_found(Txt, Lang)); + {error, _} -> + Txt = ?T("Database failure"), + xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) end. -remove_privacy_list(LUser, LServer, Name, mnesia) -> - F = fun () -> - case mnesia:read({privacy, {LUser, LServer}}) of - [] -> ok; - [#privacy{default = Default, lists = Lists} = P] -> - if Name == Default -> conflict; - true -> - NewLists = lists:keydelete(Name, 1, Lists), - mnesia:write(P#privacy{lists = NewLists}) - end - end - end, - mnesia:transaction(F); -remove_privacy_list(LUser, LServer, Name, riak) -> - {atomic, - case ejabberd_riak:get(privacy, privacy_schema(), {LUser, LServer}) of - {ok, #privacy{default = Default, lists = Lists} = P} -> - if Name == Default -> - conflict; - true -> - NewLists = lists:keydelete(Name, 1, Lists), - ejabberd_riak:put(P#privacy{lists = NewLists}, - privacy_schema()) - end; - {error, _} -> - ok - end}; -remove_privacy_list(LUser, LServer, Name, odbc) -> - F = fun () -> - case sql_get_default_privacy_list_t(LUser) of - {selected, [<<"name">>], []} -> - sql_remove_privacy_list(LUser, Name), ok; - {selected, [<<"name">>], [[Default]]} -> - if Name == Default -> conflict; - true -> sql_remove_privacy_list(LUser, Name), ok - end - end - end, - odbc_queries:sql_transaction(LServer, F). +-spec process_active_set(IQ, none | binary()) -> IQ. +process_active_set(IQ, none) -> + xmpp:make_iq_result(xmpp:put_meta(IQ, privacy_active_list, none)); +process_active_set(#iq{from = #jid{luser = LUser, lserver = LServer}, + lang = Lang} = IQ, Name) -> + case get_user_list(LUser, LServer, Name) of + {ok, _} -> + xmpp:make_iq_result(xmpp:put_meta(IQ, privacy_active_list, Name)); + error -> + Txt = ?T("No privacy list with this name found"), + xmpp:make_error(IQ, xmpp:err_item_not_found(Txt, Lang)); + {error, _} -> + Txt = ?T("Database failure"), + xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) + end. -set_privacy_list(LUser, LServer, Name, List, mnesia) -> - F = fun () -> - case mnesia:wread({privacy, {LUser, LServer}}) of - [] -> - NewLists = [{Name, List}], - mnesia:write(#privacy{us = {LUser, LServer}, - lists = NewLists}); - [#privacy{lists = Lists} = P] -> - NewLists1 = lists:keydelete(Name, 1, Lists), - NewLists = [{Name, List} | NewLists1], - mnesia:write(P#privacy{lists = NewLists}) - end - end, - mnesia:transaction(F); -set_privacy_list(LUser, LServer, Name, List, riak) -> - {atomic, - case ejabberd_riak:get(privacy, privacy_schema(), {LUser, LServer}) of - {ok, #privacy{lists = Lists} = P} -> - NewLists1 = lists:keydelete(Name, 1, Lists), - NewLists = [{Name, List} | NewLists1], - ejabberd_riak:put(P#privacy{lists = NewLists}, privacy_schema()); - {error, _} -> - NewLists = [{Name, List}], - ejabberd_riak:put(#privacy{us = {LUser, LServer}, - lists = NewLists}, - privacy_schema()) - end}; -set_privacy_list(LUser, LServer, Name, List, odbc) -> - RItems = lists:map(fun item_to_raw/1, List), - F = fun () -> - ID = case sql_get_privacy_list_id_t(LUser, Name) of - {selected, [<<"id">>], []} -> - sql_add_privacy_list(LUser, Name), - {selected, [<<"id">>], [[I]]} = - sql_get_privacy_list_id_t(LUser, Name), - I; - {selected, [<<"id">>], [[I]]} -> I - end, - sql_set_privacy_list(ID, RItems), - ok - end, - odbc_queries:sql_transaction(LServer, F). +-spec set_list(privacy()) -> ok | {error, any()}. +set_list(#privacy{us = {LUser, LServer}, lists = Lists} = Privacy) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + case Mod:set_lists(Privacy) of + ok -> + Names = [Name || {Name, _} <- Lists], + delete_cache(Mod, LUser, LServer, Names); + {error, _} = Err -> + Err + end. -process_list_set(LUser, LServer, {value, Name}, Els) -> - case parse_items(Els) of - false -> {error, ?ERR_BAD_REQUEST}; - remove -> - case remove_privacy_list(LUser, LServer, Name, - gen_mod:db_type(LServer, ?MODULE)) - of - {atomic, conflict} -> {error, ?ERR_CONFLICT}; - {atomic, ok} -> - ejabberd_sm:route(jlib:make_jid(LUser, LServer, - <<"">>), - jlib:make_jid(LUser, LServer, <<"">>), - {broadcast, - {privacy_list, - #userlist{name = Name, - list = []}, - Name}}), - {result, []}; - _ -> {error, ?ERR_INTERNAL_SERVER_ERROR} - end; - List -> - case set_privacy_list(LUser, LServer, Name, List, - gen_mod:db_type(LServer, ?MODULE)) - of - {atomic, ok} -> - NeedDb = is_list_needdb(List), - ejabberd_sm:route(jlib:make_jid(LUser, LServer, - <<"">>), - jlib:make_jid(LUser, LServer, <<"">>), - {broadcast, - {privacy_list, - #userlist{name = Name, - list = List, - needdb = NeedDb}, - Name}}), - {result, []}; - _ -> {error, ?ERR_INTERNAL_SERVER_ERROR} - end +-spec process_lists_set(iq(), binary(), [privacy_item()]) -> iq(). +process_lists_set(#iq{from = #jid{luser = LUser, lserver = LServer}, + lang = Lang} = IQ, Name, []) -> + case xmpp:get_meta(IQ, privacy_active_list, none) of + Name -> + Txt = ?T("Cannot remove active list"), + xmpp:make_error(IQ, xmpp:err_conflict(Txt, Lang)); + _ -> + case remove_list(LUser, LServer, Name) of + ok -> + xmpp:make_iq_result(IQ); + {error, conflict} -> + Txt = ?T("Cannot remove default list"), + xmpp:make_error(IQ, xmpp:err_conflict(Txt, Lang)); + {error, notfound} -> + Txt = ?T("No privacy list with this name found"), + xmpp:make_error(IQ, xmpp:err_item_not_found(Txt, Lang)); + {error, _} -> + Txt = ?T("Database failure"), + Err = xmpp:err_internal_server_error(Txt, Lang), + xmpp:make_error(IQ, Err) + end end; -process_list_set(_LUser, _LServer, false, _Els) -> - {error, ?ERR_BAD_REQUEST}. +process_lists_set(#iq{from = #jid{luser = LUser, lserver = LServer} = From, + lang = Lang} = IQ, Name, Items) -> + case catch lists:map(fun decode_item/1, Items) of + {error, Why} -> + Txt = xmpp:io_format_error(Why), + xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)); + List -> + case set_list(LUser, LServer, Name, List) of + ok -> + push_list_update(From, Name), + xmpp:make_iq_result(IQ); + {error, _} -> + Txt = ?T("Database failure"), + xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) + end + end. -parse_items([]) -> remove; -parse_items(Els) -> parse_items(Els, []). +-spec push_list_update(jid(), binary()) -> ok. +push_list_update(From, Name) -> + BareFrom = jid:remove_resource(From), + lists:foreach( + fun(R) -> + To = jid:replace_resource(From, R), + IQ = #iq{type = set, from = BareFrom, to = To, + id = <<"push", (p1_rand:get_string())/binary>>, + sub_els = [#privacy_query{ + lists = [#privacy_list{name = Name}]}]}, + ejabberd_router:route(IQ) + end, ejabberd_sm:get_user_resources(From#jid.luser, From#jid.lserver)). -parse_items([], Res) -> - lists:keysort(#listitem.order, Res); -parse_items([#xmlel{name = <<"item">>, attrs = Attrs, - children = SubEls} - | Els], - Res) -> - Type = xml:get_attr(<<"type">>, Attrs), - Value = xml:get_attr(<<"value">>, Attrs), - SAction = xml:get_attr(<<"action">>, Attrs), - SOrder = xml:get_attr(<<"order">>, Attrs), - Action = case catch list_to_action(element(2, SAction)) - of - {'EXIT', _} -> false; - Val -> Val - end, - Order = case catch jlib:binary_to_integer(element(2, - SOrder)) - of - {'EXIT', _} -> false; - IntVal -> - if IntVal >= 0 -> IntVal; - true -> false - end +-spec decode_item(privacy_item()) -> listitem(). +decode_item(#privacy_item{order = Order, + action = Action, + type = T, + value = V, + message = MatchMessage, + iq = MatchIQ, + presence_in = MatchPresenceIn, + presence_out = MatchPresenceOut}) -> + Value = try decode_value(T, V) + catch _:_ -> + throw({error, {bad_attr_value, <<"value">>, + <<"item">>, ?NS_PRIVACY}}) end, - if (Action /= false) and (Order /= false) -> - I1 = #listitem{action = Action, order = Order}, - I2 = case {Type, Value} of - {{value, T}, {value, V}} -> - case T of - <<"jid">> -> - case jlib:string_to_jid(V) of - error -> false; - JID -> - I1#listitem{type = jid, - value = jlib:jid_tolower(JID)} - end; - <<"group">> -> I1#listitem{type = group, value = V}; - <<"subscription">> -> - case V of - <<"none">> -> - I1#listitem{type = subscription, - value = none}; - <<"both">> -> - I1#listitem{type = subscription, - value = both}; - <<"from">> -> - I1#listitem{type = subscription, - value = from}; - <<"to">> -> - I1#listitem{type = subscription, value = to}; - _ -> false - end - end; - {{value, _}, false} -> false; - _ -> I1 - end, - case I2 of - false -> false; - _ -> - case parse_matches(I2, xml:remove_cdata(SubEls)) of - false -> false; - I3 -> parse_items(Els, [I3 | Res]) - end - end; - true -> false - end; -parse_items(_, _Res) -> false. - -parse_matches(Item, []) -> - Item#listitem{match_all = true}; -parse_matches(Item, Els) -> parse_matches1(Item, Els). - -parse_matches1(Item, []) -> Item; -parse_matches1(Item, - [#xmlel{name = <<"message">>} | Els]) -> - parse_matches1(Item#listitem{match_message = true}, - Els); -parse_matches1(Item, [#xmlel{name = <<"iq">>} | Els]) -> - parse_matches1(Item#listitem{match_iq = true}, Els); -parse_matches1(Item, - [#xmlel{name = <<"presence-in">>} | Els]) -> - parse_matches1(Item#listitem{match_presence_in = true}, - Els); -parse_matches1(Item, - [#xmlel{name = <<"presence-out">>} | Els]) -> - parse_matches1(Item#listitem{match_presence_out = true}, - Els); -parse_matches1(_Item, [#xmlel{} | _Els]) -> false. - -is_list_needdb(Items) -> - lists:any(fun (X) -> - case X#listitem.type of - subscription -> true; - group -> true; - _ -> false - end - end, - Items). - -get_user_list(Acc, User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - {Default, Items} = get_user_list(Acc, LUser, LServer, - gen_mod:db_type(LServer, ?MODULE)), - NeedDb = is_list_needdb(Items), - #userlist{name = Default, list = Items, - needdb = NeedDb}. - -get_user_list(_, LUser, LServer, mnesia) -> - case catch mnesia:dirty_read(privacy, {LUser, LServer}) - of - [] -> {none, []}; - [#privacy{default = Default, lists = Lists}] -> - case Default of - none -> {none, []}; - _ -> - case lists:keysearch(Default, 1, Lists) of - {value, {_, List}} -> {Default, List}; - _ -> {none, []} - end - end; - _ -> {none, []} - end; -get_user_list(_, LUser, LServer, riak) -> - case ejabberd_riak:get(privacy, privacy_schema(), {LUser, LServer}) of - {ok, #privacy{default = Default, lists = Lists}} -> - case Default of - none -> {none, []}; - _ -> - case lists:keysearch(Default, 1, Lists) of - {value, {_, List}} -> {Default, List}; - _ -> {none, []} - end - end; - {error, _} -> - {none, []} - end; -get_user_list(_, LUser, LServer, odbc) -> - case catch sql_get_default_privacy_list(LUser, LServer) - of - {selected, [<<"name">>], []} -> {none, []}; - {selected, [<<"name">>], [[Default]]} -> - case catch sql_get_privacy_list_data(LUser, LServer, - Default) - of - {selected, - [<<"t">>, <<"value">>, <<"action">>, <<"ord">>, - <<"match_all">>, <<"match_iq">>, <<"match_message">>, - <<"match_presence_in">>, <<"match_presence_out">>], - RItems} -> - {Default, lists:map(fun raw_to_item/1, RItems)}; - _ -> {none, []} - end; - _ -> {none, []} + Type = case T of + undefined -> none; + _ -> T + end, + ListItem = #listitem{order = Order, + action = Action, + type = Type, + value = Value}, + if not (MatchMessage or MatchIQ or MatchPresenceIn or MatchPresenceOut) -> + ListItem#listitem{match_all = true}; + true -> + ListItem#listitem{match_iq = MatchIQ, + match_message = MatchMessage, + match_presence_in = MatchPresenceIn, + match_presence_out = MatchPresenceOut} end. -get_user_lists(User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - get_user_lists(LUser, LServer, gen_mod:db_type(LServer, ?MODULE)). +-spec c2s_copy_session(c2s_state(), c2s_state()) -> c2s_state(). +c2s_copy_session(State, #{privacy_active_list := List}) -> + State#{privacy_active_list => List}; +c2s_copy_session(State, _) -> + State. -get_user_lists(LUser, LServer, mnesia) -> - case catch mnesia:dirty_read(privacy, {LUser, LServer}) of - [#privacy{} = P] -> - {ok, P}; - _ -> - error +%% Adjust the client's state, so next packets (which can be already queued) +%% will take the active list into account. +-spec update_c2s_state_with_privacy_list(stanza(), c2s_state()) -> c2s_state(). +update_c2s_state_with_privacy_list(#iq{type = set, + to = #jid{luser = U, lserver = S, + lresource = <<"">>} = To} = IQ, + State) -> + %% Match a IQ set containing a new active privacy list + case xmpp:get_subtag(IQ, #privacy_query{}) of + #privacy_query{default = undefined, active = Active} -> + case Active of + none -> + ?DEBUG("Removing active privacy list for user: ~ts", + [jid:encode(To)]), + State#{privacy_active_list => none}; + undefined -> + State; + _ -> + case get_user_list(U, S, Active) of + {ok, _} -> + ?DEBUG("Setting active privacy list '~ts' for user: ~ts", + [Active, jid:encode(To)]), + State#{privacy_active_list => Active}; + _ -> + %% unknown privacy list name + State + end + end; + _ -> + State end; -get_user_lists(LUser, LServer, riak) -> - case ejabberd_riak:get(privacy, privacy_schema(), {LUser, LServer}) of - {ok, #privacy{} = P} -> - {ok, P}; - {error, _} -> - error +update_c2s_state_with_privacy_list(_Packet, State) -> + State. + +%% Add the active privacy list to packet metadata +-spec user_send_packet({stanza(), c2s_state()}) -> {stanza(), c2s_state()}. +user_send_packet({#iq{type = Type, + to = #jid{luser = U, lserver = S, lresource = <<"">>}, + from = #jid{luser = U, lserver = S}, + sub_els = [_]} = IQ, + #{privacy_active_list := Name} = State}) + when Type == get; Type == set -> + NewIQ = case xmpp:has_subtag(IQ, #privacy_query{}) of + true -> xmpp:put_meta(IQ, privacy_active_list, Name); + false -> IQ + end, + {NewIQ, update_c2s_state_with_privacy_list(IQ, State)}; +%% For client with no active privacy list, see if there is +%% one about to be activated in this packet and update client state +user_send_packet({Packet, State}) -> + {Packet, update_c2s_state_with_privacy_list(Packet, State)}. + +-spec set_list(binary(), binary(), binary(), [listitem()]) -> ok | {error, any()}. +set_list(LUser, LServer, Name, List) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + case Mod:set_list(LUser, LServer, Name, List) of + ok -> + delete_cache(Mod, LUser, LServer, [Name]); + {error, _} = Err -> + Err + end. + +-spec remove_list(binary(), binary(), binary()) -> + ok | {error, conflict | notfound | any()}. +remove_list(LUser, LServer, Name) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + case Mod:remove_list(LUser, LServer, Name) of + ok -> + delete_cache(Mod, LUser, LServer, [Name]); + Err -> + Err + end. + +-spec get_user_lists(binary(), binary()) -> {ok, privacy()} | error | {error, any()}. +get_user_lists(User, Server) -> + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + Mod = gen_mod:db_mod(LServer, ?MODULE), + case use_cache(Mod, LServer) of + true -> + ets_cache:lookup( + ?PRIVACY_CACHE, {LUser, LServer}, + fun() -> Mod:get_lists(LUser, LServer) end); + false -> + Mod:get_lists(LUser, LServer) + end. + +-spec get_user_list(binary(), binary(), binary() | default) -> + {ok, {binary(), [listitem()]}} | error | {error, any()}. +get_user_list(LUser, LServer, Name) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + case use_cache(Mod, LServer) of + true -> + ets_cache:lookup( + ?PRIVACY_LIST_CACHE, {LUser, LServer, Name}, + fun() -> + case ets_cache:lookup( + ?PRIVACY_CACHE, {LUser, LServer}) of + {ok, Privacy} -> + get_list_by_name(Privacy, Name); + error -> + Mod:get_list(LUser, LServer, Name) + end + end); + false -> + Mod:get_list(LUser, LServer, Name) + end. + +-spec get_list_by_name(#privacy{}, binary() | default) -> + {ok, {binary(), [listitem()]}} | error. +get_list_by_name(#privacy{default = Default} = Privacy, default) -> + get_list_by_name(Privacy, Default); +get_list_by_name(#privacy{lists = Lists}, Name) -> + case lists:keyfind(Name, 1, Lists) of + {_, List} -> {ok, {Name, List}}; + false -> error + end. + +-spec set_default_list(binary(), binary(), binary() | none) -> + ok | {error, notfound | any()}. +set_default_list(LUser, LServer, Name) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + Res = case Name of + none -> Mod:unset_default(LUser, LServer); + _ -> Mod:set_default(LUser, LServer, Name) + end, + case Res of + ok -> + delete_cache(Mod, LUser, LServer, []); + Err -> + Err + end. + +-spec check_packet(allow | deny, c2s_state() | jid(), stanza(), in | out) -> allow | deny. +check_packet(Acc, #{jid := JID} = State, Packet, Dir) -> + case maps:get(privacy_active_list, State, none) of + none -> + check_packet(Acc, JID, Packet, Dir); + ListName -> + #jid{luser = LUser, lserver = LServer} = JID, + case get_user_list(LUser, LServer, ListName) of + {ok, {_, List}} -> + do_check_packet(JID, List, Packet, Dir); + _ -> + ?DEBUG("Non-existing active list '~ts' is set " + "for user '~ts'", [ListName, jid:encode(JID)]), + check_packet(Acc, JID, Packet, Dir) + end end; -get_user_lists(LUser, LServer, odbc) -> - Default = case catch sql_get_default_privacy_list(LUser, LServer) of - {selected, [<<"name">>], []} -> - none; - {selected, [<<"name">>], [[DefName]]} -> - DefName; - _ -> - none - end, - case catch sql_get_privacy_list_names(LUser, LServer) of - {selected, [<<"name">>], Names} -> - Lists = - lists:flatmap( - fun([Name]) -> - case catch sql_get_privacy_list_data( - LUser, LServer, Name) of - {selected, - [<<"t">>, <<"value">>, <<"action">>, - <<"ord">>, <<"match_all">>, <<"match_iq">>, - <<"match_message">>, <<"match_presence_in">>, - <<"match_presence_out">>], - RItems} -> - [{Name, lists:map(fun raw_to_item/1, RItems)}]; - _ -> - [] - end - end, Names), - {ok, #privacy{default = Default, - us = {LUser, LServer}, - lists = Lists}}; - _ -> - error +check_packet(_, JID, Packet, Dir) -> + #jid{luser = LUser, lserver = LServer} = JID, + case get_user_list(LUser, LServer, default) of + {ok, {_, List}} -> + do_check_packet(JID, List, Packet, Dir); + _ -> + allow end. %% From is the sender, To is the destination. %% If Dir = out, User@Server is the sender account (From). %% If Dir = in, User@Server is the destination account (To). -check_packet(_, _User, _Server, _UserList, - {#jid{luser = <<"">>, lserver = Server} = _From, - #jid{lserver = Server} = _To, _}, - in) -> +-spec do_check_packet(jid(), [listitem()], stanza(), in | out) -> allow | deny. +do_check_packet(_, [], _, _) -> allow; -check_packet(_, _User, _Server, _UserList, - {#jid{lserver = Server} = _From, - #jid{luser = <<"">>, lserver = Server} = _To, _}, - out) -> +do_check_packet(#jid{luser = LUser, lserver = LServer}, List, Packet, Dir) -> + From = xmpp:get_from(Packet), + To = xmpp:get_to(Packet), + case {From, To} of + {#jid{luser = <<"">>, lserver = LServer}, + #jid{lserver = LServer}} when Dir == in -> + %% Allow any packets from local server + allow; + {#jid{lserver = LServer}, + #jid{luser = <<"">>, lserver = LServer}} when Dir == out -> + %% Allow any packets to local server allow; -check_packet(_, _User, _Server, _UserList, - {#jid{luser = User, lserver = Server} = _From, - #jid{luser = User, lserver = Server} = _To, _}, - _Dir) -> + {#jid{luser = LUser, lserver = LServer, lresource = <<"">>}, + #jid{luser = LUser, lserver = LServer}} when Dir == in -> + %% Allow incoming packets from user's bare jid to his full jid allow; -check_packet(_, User, Server, - #userlist{list = List, needdb = NeedDb}, - {From, To, #xmlel{name = PName, attrs = Attrs}}, Dir) -> - case List of - [] -> allow; + {#jid{luser = LUser, lserver = LServer}, + #jid{luser = LUser, lserver = LServer, lresource = <<"">>}} when Dir == out -> + %% Allow outgoing packets from user's full jid to his bare JID + allow; _ -> - PType = case PName of - <<"message">> -> message; - <<"iq">> -> iq; - <<"presence">> -> - case xml:get_attr_s(<<"type">>, Attrs) of - %% notification - <<"">> -> presence; - <<"unavailable">> -> presence; - %% subscribe, subscribed, unsubscribe, - %% unsubscribed, error, probe, or other - _ -> other - end + PType = case Packet of + #message{} -> message; + #iq{} -> iq; + #presence{type = available} -> presence; + #presence{type = unavailable} -> presence; + _ -> other end, PType2 = case {PType, Dir} of {message, in} -> message; @@ -870,47 +595,36 @@ check_packet(_, User, Server, {_, _} -> other end, LJID = case Dir of - in -> jlib:jid_tolower(From); - out -> jlib:jid_tolower(To) + in -> jid:tolower(From); + out -> jid:tolower(To) end, - {Subscription, Groups} = case NeedDb of - true -> - ejabberd_hooks:run_fold(roster_get_jid_info, - jlib:nameprep(Server), - {none, []}, - [User, Server, - LJID]); - false -> {[], []} - end, - check_packet_aux(List, PType2, LJID, Subscription, - Groups) + check_packet_aux(List, PType2, LJID, [LUser, LServer]) end. -%% Ptype = mesage | iq | presence_in | presence_out | other -check_packet_aux([], _PType, _JID, _Subscription, - _Groups) -> +-spec check_packet_aux([listitem()], + message | iq | presence_in | presence_out | other, + ljid(), [binary()] | {none | both | from | to, [binary()]}) -> + allow | deny. +%% Ptype = message | iq | presence_in | presence_out | other +check_packet_aux([], _PType, _JID, _RosterInfo) -> allow; -check_packet_aux([Item | List], PType, JID, - Subscription, Groups) -> +check_packet_aux([Item | List], PType, JID, RosterInfo) -> #listitem{type = Type, value = Value, action = Action} = Item, case is_ptype_match(Item, PType) of true -> - case Type of - none -> Action; - _ -> - case is_type_match(Type, Value, JID, Subscription, - Groups) - of - true -> Action; - false -> - check_packet_aux(List, PType, JID, Subscription, Groups) - end - end; + case is_type_match(Type, Value, JID, RosterInfo) of + {true, _} -> Action; + {false, RI} -> + check_packet_aux(List, PType, JID, RI) + end; false -> - check_packet_aux(List, PType, JID, Subscription, Groups) + check_packet_aux(List, PType, JID, RosterInfo) end. +-spec is_ptype_match(listitem(), + message | iq | presence_in | presence_out | other) -> + boolean(). is_ptype_match(Item, PType) -> case Item#listitem.match_all of true -> true; @@ -924,330 +638,283 @@ is_ptype_match(Item, PType) -> end end. -is_type_match(Type, Value, JID, Subscription, Groups) -> - case Type of - jid -> - case Value of - {<<"">>, Server, <<"">>} -> - case JID of - {_, Server, _} -> true; - _ -> false - end; - {User, Server, <<"">>} -> - case JID of - {User, Server, _} -> true; - _ -> false - end; - _ -> Value == JID - end; - subscription -> Value == Subscription; - group -> lists:member(Value, Groups) - end. +-spec is_type_match(none | jid | subscription | group, listitem_value(), + ljid(), [binary()] | {none | both | from | to, [binary()]}) -> + {boolean(), [binary()] | {none | both | from | to, [binary()]}}. +is_type_match(none, _Value, _JID, RosterInfo) -> + {true, RosterInfo}; +is_type_match(jid, Value, JID, RosterInfo) -> + case Value of + {<<"">>, Server, <<"">>} -> + case JID of + {_, Server, _} -> {true, RosterInfo}; + _ -> {false, RosterInfo} + end; + {User, Server, <<"">>} -> + case JID of + {User, Server, _} -> {true, RosterInfo}; + _ -> {false, RosterInfo} + end; + {<<"">>, Server, Resource} -> + case JID of + {_, Server, Resource} -> {true, RosterInfo}; + _ -> {false, RosterInfo} + end; + _ -> {Value == JID, RosterInfo} + end; +is_type_match(subscription, Value, JID, RosterInfo) -> + {Subscription, _} = RI = resolve_roster_info(JID, RosterInfo), + {Value == Subscription, RI}; +is_type_match(group, Group, JID, RosterInfo) -> + {_, Groups} = RI = resolve_roster_info(JID, RosterInfo), + {lists:member(Group, Groups), RI}. +-spec resolve_roster_info(ljid(), [binary()] | {none | both | from | to, [binary()]}) -> + {none | both | from | to, [binary()]}. +resolve_roster_info(JID, [LUser, LServer]) -> + {Subscription, _Ask, Groups} = + ejabberd_hooks:run_fold( + roster_get_jid_info, LServer, + {none, none, []}, + [LUser, LServer, JID]), + {Subscription, Groups}; +resolve_roster_info(_, RosterInfo) -> + RosterInfo. + +-spec remove_user(binary(), binary()) -> ok. remove_user(User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - remove_user(LUser, LServer, - gen_mod:db_type(LServer, ?MODULE)). - -remove_user(LUser, LServer, mnesia) -> - F = fun () -> mnesia:delete({privacy, {LUser, LServer}}) - end, - mnesia:transaction(F); -remove_user(LUser, LServer, riak) -> - {atomic, ejabberd_riak:delete(privacy, {LUser, LServer})}; -remove_user(LUser, LServer, odbc) -> - sql_del_privacy_lists(LUser, LServer). - -updated_list(_, #userlist{name = OldName} = Old, - #userlist{name = NewName} = New) -> - if OldName == NewName -> New; - true -> Old + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + Privacy = get_user_lists(LUser, LServer), + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:remove_lists(LUser, LServer), + case Privacy of + {ok, #privacy{lists = Lists}} -> + Names = [Name || {Name, _} <- Lists], + delete_cache(Mod, LUser, LServer, Names); + _ -> + ok end. -raw_to_item([SType, SValue, SAction, SOrder, SMatchAll, - SMatchIQ, SMatchMessage, SMatchPresenceIn, - SMatchPresenceOut]) -> - {Type, Value} = case SType of - <<"n">> -> {none, none}; - <<"j">> -> - case jlib:string_to_jid(SValue) of - #jid{} = JID -> {jid, jlib:jid_tolower(JID)} - end; - <<"g">> -> {group, SValue}; - <<"s">> -> - case SValue of - <<"none">> -> {subscription, none}; - <<"both">> -> {subscription, both}; - <<"from">> -> {subscription, from}; - <<"to">> -> {subscription, to} - end - end, - Action = case SAction of - <<"a">> -> allow; - <<"d">> -> deny - end, - Order = jlib:binary_to_integer(SOrder), - MatchAll = ejabberd_odbc:to_bool(SMatchAll), - MatchIQ = ejabberd_odbc:to_bool(SMatchIQ), - MatchMessage = ejabberd_odbc:to_bool(SMatchMessage), - MatchPresenceIn = - ejabberd_odbc:to_bool(SMatchPresenceIn), - MatchPresenceOut = - ejabberd_odbc:to_bool(SMatchPresenceOut), - #listitem{type = Type, value = Value, action = Action, - order = Order, match_all = MatchAll, match_iq = MatchIQ, - match_message = MatchMessage, - match_presence_in = MatchPresenceIn, - match_presence_out = MatchPresenceOut}. - -item_to_raw(#listitem{type = Type, value = Value, - action = Action, order = Order, match_all = MatchAll, - match_iq = MatchIQ, match_message = MatchMessage, - match_presence_in = MatchPresenceIn, - match_presence_out = MatchPresenceOut}) -> - {SType, SValue} = case Type of - none -> {<<"n">>, <<"">>}; - jid -> - {<<"j">>, - ejabberd_odbc:escape(jlib:jid_to_string(Value))}; - group -> {<<"g">>, ejabberd_odbc:escape(Value)}; - subscription -> - case Value of - none -> {<<"s">>, <<"none">>}; - both -> {<<"s">>, <<"both">>}; - from -> {<<"s">>, <<"from">>}; - to -> {<<"s">>, <<"to">>} - end - end, - SAction = case Action of - allow -> <<"a">>; - deny -> <<"d">> - end, - SOrder = iolist_to_binary(integer_to_list(Order)), - SMatchAll = if MatchAll -> <<"1">>; - true -> <<"0">> - end, - SMatchIQ = if MatchIQ -> <<"1">>; - true -> <<"0">> - end, - SMatchMessage = if MatchMessage -> <<"1">>; - true -> <<"0">> - end, - SMatchPresenceIn = if MatchPresenceIn -> <<"1">>; - true -> <<"0">> - end, - SMatchPresenceOut = if MatchPresenceOut -> <<"1">>; - true -> <<"0">> - end, - [SType, SValue, SAction, SOrder, SMatchAll, SMatchIQ, - SMatchMessage, SMatchPresenceIn, SMatchPresenceOut]. - -sql_get_default_privacy_list(LUser, LServer) -> - Username = ejabberd_odbc:escape(LUser), - odbc_queries:get_default_privacy_list(LServer, - Username). - -sql_get_default_privacy_list_t(LUser) -> - Username = ejabberd_odbc:escape(LUser), - odbc_queries:get_default_privacy_list_t(Username). - -sql_get_privacy_list_names(LUser, LServer) -> - Username = ejabberd_odbc:escape(LUser), - odbc_queries:get_privacy_list_names(LServer, Username). - -sql_get_privacy_list_names_t(LUser) -> - Username = ejabberd_odbc:escape(LUser), - odbc_queries:get_privacy_list_names_t(Username). - -sql_get_privacy_list_id(LUser, LServer, Name) -> - Username = ejabberd_odbc:escape(LUser), - SName = ejabberd_odbc:escape(Name), - odbc_queries:get_privacy_list_id(LServer, Username, - SName). - -sql_get_privacy_list_id_t(LUser, Name) -> - Username = ejabberd_odbc:escape(LUser), - SName = ejabberd_odbc:escape(Name), - odbc_queries:get_privacy_list_id_t(Username, SName). - -sql_get_privacy_list_data(LUser, LServer, Name) -> - Username = ejabberd_odbc:escape(LUser), - SName = ejabberd_odbc:escape(Name), - odbc_queries:get_privacy_list_data(LServer, Username, - SName). - -sql_get_privacy_list_data_t(LUser, Name) -> - Username = ejabberd_odbc:escape(LUser), - SName = ejabberd_odbc:escape(Name), - odbc_queries:get_privacy_list_data_t(Username, SName). - -sql_get_privacy_list_data_by_id(ID, LServer) -> - odbc_queries:get_privacy_list_data_by_id(LServer, ID). - -sql_get_privacy_list_data_by_id_t(ID) -> - odbc_queries:get_privacy_list_data_by_id_t(ID). - -sql_set_default_privacy_list(LUser, Name) -> - Username = ejabberd_odbc:escape(LUser), - SName = ejabberd_odbc:escape(Name), - odbc_queries:set_default_privacy_list(Username, SName). - -sql_unset_default_privacy_list(LUser, LServer) -> - Username = ejabberd_odbc:escape(LUser), - odbc_queries:unset_default_privacy_list(LServer, - Username). - -sql_remove_privacy_list(LUser, Name) -> - Username = ejabberd_odbc:escape(LUser), - SName = ejabberd_odbc:escape(Name), - odbc_queries:remove_privacy_list(Username, SName). - -sql_add_privacy_list(LUser, Name) -> - Username = ejabberd_odbc:escape(LUser), - SName = ejabberd_odbc:escape(Name), - odbc_queries:add_privacy_list(Username, SName). - -sql_set_privacy_list(ID, RItems) -> - odbc_queries:set_privacy_list(ID, RItems). - -sql_del_privacy_lists(LUser, LServer) -> - Username = ejabberd_odbc:escape(LUser), - Server = ejabberd_odbc:escape(LServer), - odbc_queries:del_privacy_lists(LServer, Server, - Username). - -update_table() -> - Fields = record_info(fields, privacy), - case mnesia:table_info(privacy, attributes) of - Fields -> - ejabberd_config:convert_table_to_binary( - privacy, Fields, set, - fun(#privacy{us = {U, _}}) -> U end, - fun(#privacy{us = {U, S}, default = Def, lists = Lists} = R) -> - NewLists = - lists:map( - fun({Name, Ls}) -> - NewLs = - lists:map( - fun(#listitem{value = Val} = L) -> - NewVal = - case Val of - {LU, LS, LR} -> - {iolist_to_binary(LU), - iolist_to_binary(LS), - iolist_to_binary(LR)}; - none -> none; - both -> both; - from -> from; - to -> to; - _ -> iolist_to_binary(Val) - end, - L#listitem{value = NewVal} - end, Ls), - {iolist_to_binary(Name), NewLs} - end, Lists), - NewDef = case Def of - none -> none; - _ -> iolist_to_binary(Def) - end, - NewUS = {iolist_to_binary(U), iolist_to_binary(S)}, - R#privacy{us = NewUS, default = NewDef, - lists = NewLists} - end); - _ -> - ?INFO_MSG("Recreating privacy table", []), - mnesia:transform_table(privacy, ignore, Fields) +-spec init_cache(module(), binary(), gen_mod:opts()) -> ok. +init_cache(Mod, Host, Opts) -> + case use_cache(Mod, Host) of + true -> + CacheOpts = cache_opts(Opts), + ets_cache:new(?PRIVACY_CACHE, CacheOpts), + ets_cache:new(?PRIVACY_LIST_CACHE, CacheOpts); + false -> + ets_cache:delete(?PRIVACY_CACHE), + ets_cache:delete(?PRIVACY_LIST_CACHE) end. -export(Server) -> - case ejabberd_odbc:sql_query(jlib:nameprep(Server), - [<<"select id from privacy_list order by " - "id desc limit 1;">>]) of - {selected, [<<"id">>], [[I]]} -> - put(id, jlib:binary_to_integer(I)); +-spec cache_opts(gen_mod:opts()) -> [proplists:property()]. +cache_opts(Opts) -> + MaxSize = mod_privacy_opt:cache_size(Opts), + CacheMissed = mod_privacy_opt:cache_missed(Opts), + LifeTime = mod_privacy_opt:cache_life_time(Opts), + [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}]. + +-spec use_cache(module(), binary()) -> boolean(). +use_cache(Mod, Host) -> + case erlang:function_exported(Mod, use_cache, 1) of + true -> Mod:use_cache(Host); + false -> mod_privacy_opt:use_cache(Host) + end. + +-spec cache_nodes(module(), binary()) -> [node()]. +cache_nodes(Mod, Host) -> + case erlang:function_exported(Mod, cache_nodes, 1) of + true -> Mod:cache_nodes(Host); + false -> ejabberd_cluster:get_nodes() + end. + +-spec delete_cache(module(), binary(), binary(), [binary()]) -> ok. +delete_cache(Mod, LUser, LServer, Names) -> + case use_cache(Mod, LServer) of + true -> + Nodes = cache_nodes(Mod, LServer), + ets_cache:delete(?PRIVACY_CACHE, {LUser, LServer}, Nodes), + lists:foreach( + fun(Name) -> + ets_cache:delete( + ?PRIVACY_LIST_CACHE, + {LUser, LServer, Name}, + Nodes) + end, [default|Names]); + false -> + ok + end. + +numeric_to_binary(<<0, 0, _/binary>>) -> + <<"0">>; +numeric_to_binary(<<0, _, _:6/binary, T/binary>>) -> + Res = lists:foldl( + fun(X, Sum) -> + Sum*10000 + X + end, 0, [X || <> <= T]), + integer_to_binary(Res). + +bool_to_binary(<<0>>) -> <<"0">>; +bool_to_binary(<<1>>) -> <<"1">>. + +prepare_list_data(mysql, [ID|Row]) -> + [binary_to_integer(ID)|Row]; +prepare_list_data(pgsql, [<>, + SType, SValue, SAction, SOrder, SMatchAll, + SMatchIQ, SMatchMessage, SMatchPresenceIn, + SMatchPresenceOut]) -> + [ID, SType, SValue, SAction, + numeric_to_binary(SOrder), + bool_to_binary(SMatchAll), + bool_to_binary(SMatchIQ), + bool_to_binary(SMatchMessage), + bool_to_binary(SMatchPresenceIn), + bool_to_binary(SMatchPresenceOut)]. + +prepare_id(mysql, ID) -> + binary_to_integer(ID); +prepare_id(pgsql, <>) -> + ID. + +import_info() -> + [{<<"privacy_default_list">>, 2}, + {<<"privacy_list_data">>, 10}, + {<<"privacy_list">>, 4}]. + +import_start(LServer, DBType) -> + ets:new(privacy_default_list_tmp, [private, named_table]), + ets:new(privacy_list_data_tmp, [private, named_table, bag]), + ets:new(privacy_list_tmp, [private, named_table, bag, + {keypos, #privacy.us}]), + Mod = gen_mod:db_mod(DBType, ?MODULE), + Mod:init(LServer, []). + +import(LServer, {sql, _}, _DBType, <<"privacy_default_list">>, [LUser, Name]) -> + US = {LUser, LServer}, + ets:insert(privacy_default_list_tmp, {US, Name}), + ok; +import(LServer, {sql, SQLType}, _DBType, <<"privacy_list_data">>, Row1) -> + [ID|Row] = prepare_list_data(SQLType, Row1), + case mod_privacy_sql:raw_to_item(Row) of + [Item] -> + IS = {ID, LServer}, + ets:insert(privacy_list_data_tmp, {IS, Item}), + ok; + [] -> + ok + end; +import(LServer, {sql, SQLType}, _DBType, <<"privacy_list">>, + [LUser, Name, ID, _TimeStamp]) -> + US = {LUser, LServer}, + IS = {prepare_id(SQLType, ID), LServer}, + Default = case ets:lookup(privacy_default_list_tmp, US) of + [{_, Name}] -> Name; + _ -> none + end, + case [Item || {_, Item} <- ets:lookup(privacy_list_data_tmp, IS)] of + [_|_] = Items -> + Privacy = #privacy{us = {LUser, LServer}, + default = Default, + lists = [{Name, Items}]}, + ets:insert(privacy_list_tmp, Privacy), + ets:delete(privacy_list_data_tmp, IS), + ok; _ -> - put(id, 0) - end, - [{privacy, - fun(Host, #privacy{us = {LUser, LServer}, lists = Lists, - default = Default}) - when LServer == Host -> - Username = ejabberd_odbc:escape(LUser), - if Default /= none -> - SDefault = ejabberd_odbc:escape(Default), - [[<<"delete from privacy_default_list where ">>, - <<"username='">>, Username, <<"';">>], - [<<"insert into privacy_default_list(username, " - "name) ">>, - <<"values ('">>, Username, <<"', '">>, - SDefault, <<"');">>]]; - true -> - [] - end ++ - lists:flatmap( - fun({Name, List}) -> - SName = ejabberd_odbc:escape(Name), - RItems = lists:map(fun item_to_raw/1, List), - ID = jlib:integer_to_binary(get_id()), - [[<<"delete from privacy_list where username='">>, - Username, <<"' and name='">>, - SName, <<"';">>], - [<<"insert into privacy_list(username, " - "name, id) values ('">>, - Username, <<"', '">>, SName, - <<"', '">>, ID, <<"');">>], - [<<"delete from privacy_list_data where " - "id='">>, ID, <<"';">>]] ++ - [[<<"insert into privacy_list_data(id, t, " - "value, action, ord, match_all, match_iq, " - "match_message, match_presence_in, " - "match_presence_out) values ('">>, - ID, <<"', '">>, str:join(Items, <<"', '">>), - <<"');">>] || Items <- RItems] - end, - Lists); - (_Host, _R) -> - [] - end}]. + ok + end. -get_id() -> - ID = get(id), - put(id, ID + 1), - ID + 1. +import_stop(_LServer, DBType) -> + import_next(DBType, ets:first(privacy_list_tmp)), + ets:delete(privacy_default_list_tmp), + ets:delete(privacy_list_data_tmp), + ets:delete(privacy_list_tmp), + ok. -import(LServer) -> - [{<<"select username from privacy_list;">>, - fun([LUser]) -> - Default = case sql_get_default_privacy_list_t(LUser) of - {selected, [<<"name">>], []} -> - none; - {selected, [<<"name">>], [[DefName]]} -> - DefName; - _ -> - none - end, - {selected, [<<"name">>], Names} = - sql_get_privacy_list_names_t(LUser), - Lists = lists:flatmap( - fun([Name]) -> - case sql_get_privacy_list_data_t(LUser, Name) of - {selected, _, RItems} -> - [{Name, - lists:map(fun raw_to_item/1, - RItems)}]; - _ -> - [] - end - end, Names), - #privacy{default = Default, - us = {LUser, LServer}, - lists = Lists} - end}]. +import_next(_DBType, '$end_of_table') -> + ok; +import_next(DBType, US) -> + [P|_] = Ps = ets:lookup(privacy_list_tmp, US), + Lists = lists:flatmap( + fun(#privacy{lists = Lists}) -> + Lists + end, Ps), + Privacy = P#privacy{lists = Lists}, + Mod = gen_mod:db_mod(DBType, ?MODULE), + Mod:import(Privacy), + import_next(DBType, ets:next(privacy_list_tmp, US)). -import(_LServer, mnesia, #privacy{} = P) -> - mnesia:dirty_write(P); -import(_LServer, riak, #privacy{} = P) -> - ejabberd_riak:put(P, privacy_schema()); -import(_, _, _) -> - pass. +export(LServer) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:export(LServer). + +%%% +%%% WebAdmin +%%% + +webadmin_menu_hostuser(Acc, _Host, _Username, _Lang) -> + Acc ++ [{<<"privacy">>, <<"Privacy Lists">>}]. + +webadmin_page_hostuser(_, Host, User, + #request{us = _US, path = [<<"privacy">>]} = R) -> + Res = ?H1GL(<<"Privacy Lists">>, <<"modules/#mod_privacy">>, <<"mod_privacy">>) + ++ [make_command(privacy_set, R, [{<<"user">>, User}, {<<"host">>, Host}], [])], + {stop, Res}; +webadmin_page_hostuser(Acc, _, _, _) -> Acc. + +%%% +%%% Documentation +%%% + +depends(_Host, _Opts) -> + []. + +mod_opt_type(db_type) -> + econf:db_type(?MODULE); +mod_opt_type(use_cache) -> + econf:bool(); +mod_opt_type(cache_size) -> + econf:pos_int(infinity); +mod_opt_type(cache_missed) -> + econf:bool(); +mod_opt_type(cache_life_time) -> + econf:timeout(second, infinity). + +mod_options(Host) -> + [{db_type, ejabberd_config:default_db(Host, ?MODULE)}, + {use_cache, ejabberd_option:use_cache(Host)}, + {cache_size, ejabberd_option:cache_size(Host)}, + {cache_missed, ejabberd_option:cache_missed(Host)}, + {cache_life_time, ejabberd_option:cache_life_time(Host)}]. + +mod_doc() -> + #{desc => + [?T("This module implements " + "https://xmpp.org/extensions/xep-0016.html" + "[XEP-0016: Privacy Lists]."), "", + ?T("NOTE: Nowadays modern XMPP clients rely on " + "https://xmpp.org/extensions/xep-0191.html" + "[XEP-0191: Blocking Command] which is implemented by " + "_`mod_blocking`_. However, you still need " + "'mod_privacy' loaded in order for 'mod_blocking' to work.")], + opts => + [{db_type, + #{value => "mnesia | sql", + desc => + ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}, + {use_cache, + #{value => "true | false", + desc => + ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, + {cache_size, + #{value => "pos_integer() | infinity", + desc => + ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, + {cache_missed, + #{value => "true | false", + desc => + ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, + {cache_life_time, + #{value => "timeout()", + desc => + ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}]}. diff --git a/src/mod_privacy_mnesia.erl b/src/mod_privacy_mnesia.erl new file mode 100644 index 000000000..b8657e719 --- /dev/null +++ b/src/mod_privacy_mnesia.erl @@ -0,0 +1,189 @@ +%%%------------------------------------------------------------------- +%%% File : mod_privacy_mnesia.erl +%%% Author : Evgeny Khramtsov +%%% Created : 14 Apr 2016 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_privacy_mnesia). + +-behaviour(mod_privacy). + +%% API +-export([init/2, set_default/3, unset_default/2, set_lists/1, + set_list/4, get_lists/2, get_list/3, remove_lists/2, + remove_list/3, use_cache/1, import/1]). +-export([need_transform/1, transform/1]). + +-include_lib("xmpp/include/xmpp.hrl"). +-include("mod_privacy.hrl"). +-include("logger.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + ejabberd_mnesia:create(?MODULE, privacy, + [{disc_only_copies, [node()]}, + {attributes, record_info(fields, privacy)}]). + +use_cache(Host) -> + case mnesia:table_info(privacy, storage_type) of + disc_only_copies -> + mod_privacy_opt:use_cache(Host); + _ -> + false + end. + +unset_default(LUser, LServer) -> + F = fun () -> + case mnesia:read({privacy, {LUser, LServer}}) of + [] -> ok; + [R] -> mnesia:write(R#privacy{default = none}) + end + end, + transaction(F). + +set_default(LUser, LServer, Name) -> + F = fun () -> + case mnesia:read({privacy, {LUser, LServer}}) of + [] -> + {error, notfound}; + [#privacy{lists = Lists} = P] -> + case lists:keymember(Name, 1, Lists) of + true -> + mnesia:write(P#privacy{default = Name, + lists = Lists}); + false -> + {error, notfound} + end + end + end, + transaction(F). + +remove_list(LUser, LServer, Name) -> + F = fun () -> + case mnesia:read({privacy, {LUser, LServer}}) of + [] -> + {error, notfound}; + [#privacy{default = Default, lists = Lists} = P] -> + if Name == Default -> + {error, conflict}; + true -> + NewLists = lists:keydelete(Name, 1, Lists), + mnesia:write(P#privacy{lists = NewLists}) + end + end + end, + transaction(F). + +set_lists(Privacy) -> + mnesia:dirty_write(Privacy). + +set_list(LUser, LServer, Name, List) -> + F = fun () -> + case mnesia:wread({privacy, {LUser, LServer}}) of + [] -> + NewLists = [{Name, List}], + mnesia:write(#privacy{us = {LUser, LServer}, + lists = NewLists}); + [#privacy{lists = Lists} = P] -> + NewLists1 = lists:keydelete(Name, 1, Lists), + NewLists = [{Name, List} | NewLists1], + mnesia:write(P#privacy{lists = NewLists}) + end + end, + transaction(F). + +get_list(LUser, LServer, Name) -> + case mnesia:dirty_read(privacy, {LUser, LServer}) of + [#privacy{default = Default, lists = Lists}] when Name == default -> + case lists:keyfind(Default, 1, Lists) of + {_, List} -> {ok, {Default, List}}; + false -> error + end; + [#privacy{lists = Lists}] -> + case lists:keyfind(Name, 1, Lists) of + {_, List} -> {ok, {Name, List}}; + false -> error + end; + [] -> + error + end. + +get_lists(LUser, LServer) -> + case mnesia:dirty_read(privacy, {LUser, LServer}) of + [#privacy{} = P] -> + {ok, P}; + _ -> + error + end. + +remove_lists(LUser, LServer) -> + F = fun () -> mnesia:delete({privacy, {LUser, LServer}}) end, + transaction(F). + +import(#privacy{} = P) -> + mnesia:dirty_write(P). + +need_transform({privacy, {U, S}, _, _}) when is_list(U) orelse is_list(S) -> + ?INFO_MSG("Mnesia table 'privacy' will be converted to binary", []), + true; +need_transform(_) -> + false. + +transform(#privacy{us = {U, S}, default = Def, lists = Lists} = R) -> + NewLists = lists:map( + fun({Name, Ls}) -> + NewLs = lists:map( + fun(#listitem{value = Val} = L) -> + NewVal = case Val of + {LU, LS, LR} -> + {iolist_to_binary(LU), + iolist_to_binary(LS), + iolist_to_binary(LR)}; + none -> none; + both -> both; + from -> from; + to -> to; + _ -> iolist_to_binary(Val) + end, + L#listitem{value = NewVal} + end, Ls), + {iolist_to_binary(Name), NewLs} + end, Lists), + NewDef = case Def of + none -> none; + _ -> iolist_to_binary(Def) + end, + NewUS = {iolist_to_binary(U), iolist_to_binary(S)}, + R#privacy{us = NewUS, default = NewDef, lists = NewLists}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +transaction(F) -> + case mnesia:transaction(F) of + {atomic, Result} -> + Result; + {aborted, Reason} -> + ?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]), + {error, db_failure} + end. diff --git a/src/mod_privacy_opt.erl b/src/mod_privacy_opt.erl new file mode 100644 index 000000000..acc0f2ac9 --- /dev/null +++ b/src/mod_privacy_opt.erl @@ -0,0 +1,41 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_privacy_opt). + +-export([cache_life_time/1]). +-export([cache_missed/1]). +-export([cache_size/1]). +-export([db_type/1]). +-export([use_cache/1]). + +-spec cache_life_time(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +cache_life_time(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_life_time, Opts); +cache_life_time(Host) -> + gen_mod:get_module_opt(Host, mod_privacy, cache_life_time). + +-spec cache_missed(gen_mod:opts() | global | binary()) -> boolean(). +cache_missed(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_missed, Opts); +cache_missed(Host) -> + gen_mod:get_module_opt(Host, mod_privacy, cache_missed). + +-spec cache_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +cache_size(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_size, Opts); +cache_size(Host) -> + gen_mod:get_module_opt(Host, mod_privacy, cache_size). + +-spec db_type(gen_mod:opts() | global | binary()) -> atom(). +db_type(Opts) when is_map(Opts) -> + gen_mod:get_opt(db_type, Opts); +db_type(Host) -> + gen_mod:get_module_opt(Host, mod_privacy, db_type). + +-spec use_cache(gen_mod:opts() | global | binary()) -> boolean(). +use_cache(Opts) when is_map(Opts) -> + gen_mod:get_opt(use_cache, Opts); +use_cache(Host) -> + gen_mod:get_module_opt(Host, mod_privacy, use_cache). + diff --git a/src/mod_privacy_sql.erl b/src/mod_privacy_sql.erl new file mode 100644 index 000000000..e0dc4476b --- /dev/null +++ b/src/mod_privacy_sql.erl @@ -0,0 +1,528 @@ +%%%------------------------------------------------------------------- +%%% File : mod_privacy_sql.erl +%%% Author : Evgeny Khramtsov +%%% Created : 14 Apr 2016 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_privacy_sql). + + +-behaviour(mod_privacy). + +%% API +-export([init/2, set_default/3, unset_default/2, set_lists/1, + set_list/4, get_lists/2, get_list/3, remove_lists/2, + remove_list/3, import/1, export/1]). + +-export([item_to_raw/1, raw_to_item/1]). + +-export([sql_schemas/0]). + +-include_lib("xmpp/include/xmpp.hrl"). +-include("mod_privacy.hrl"). +-include("logger.hrl"). +-include("ejabberd_sql_pt.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(Host, _Opts) -> + ejabberd_sql_schema:update_schema(Host, ?MODULE, sql_schemas()), + ok. + +sql_schemas() -> + [#sql_schema{ + version = 1, + tables = + [#sql_table{ + name = <<"privacy_default_list">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"name">>, type = text}], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"username">>], + unique = true}]}, + #sql_table{ + name = <<"privacy_list">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"name">>, type = text}, + #sql_column{name = <<"id">>, type = bigserial}, + #sql_column{name = <<"created_at">>, type = timestamp, + default = true}], + indices = [#sql_index{ + columns = [<<"id">>], + unique = true}, + #sql_index{ + columns = [<<"server_host">>, <<"username">>, + <<"name">>], + unique = true}]}, + #sql_table{ + name = <<"privacy_list_data">>, + columns = + [#sql_column{name = <<"id">>, type = bigint, + opts = [#sql_references{ + table = <<"privacy_list">>, + column = <<"id">>}]}, + #sql_column{name = <<"t">>, type = {char, 1}}, + #sql_column{name = <<"value">>, type = text}, + #sql_column{name = <<"action">>, type = {char, 1}}, + #sql_column{name = <<"ord">>, type = numeric}, + #sql_column{name = <<"match_all">>, type = boolean}, + #sql_column{name = <<"match_iq">>, type = boolean}, + #sql_column{name = <<"match_message">>, type = boolean}, + #sql_column{name = <<"match_presence_in">>, type = boolean}, + #sql_column{name = <<"match_presence_out">>, type = boolean}], + indices = [#sql_index{columns = [<<"id">>]}]}]}]. + +unset_default(LUser, LServer) -> + case unset_default_privacy_list(LUser, LServer) of + ok -> + ok; + _Err -> + {error, db_failure} + end. + +set_default(LUser, LServer, Name) -> + F = fun () -> + case get_privacy_list_names_t(LUser, LServer) of + {selected, []} -> + {error, notfound}; + {selected, Names} -> + case lists:member({Name}, Names) of + true -> + set_default_privacy_list(LUser, LServer, Name); + false -> + {error, notfound} + end + end + end, + transaction(LServer, F). + +remove_list(LUser, LServer, Name) -> + F = fun () -> + case get_default_privacy_list_t(LUser, LServer) of + {selected, []} -> + remove_privacy_list_t(LUser, LServer, Name); + {selected, [{Default}]} -> + if Name == Default -> + {error, conflict}; + true -> + remove_privacy_list_t(LUser, LServer, Name) + end + end + end, + transaction(LServer, F). + +set_lists(#privacy{us = {LUser, LServer}, + default = Default, + lists = Lists}) -> + F = fun() -> + lists:foreach( + fun({Name, List}) -> + add_privacy_list(LUser, LServer, Name), + {selected, [{I}]} = + get_privacy_list_id_t(LUser, LServer, Name), + RItems = lists:map(fun item_to_raw/1, List), + set_privacy_list(I, RItems), + if is_binary(Default) -> + set_default_privacy_list( + LUser, LServer, Default); + true -> + ok + end + end, Lists) + end, + transaction(LServer, F). + +set_list(LUser, LServer, Name, List) -> + RItems = lists:map(fun item_to_raw/1, List), + F = fun() -> + {ID, New} = case get_privacy_list_id_t(LUser, LServer, Name) of + {selected, []} -> + add_privacy_list(LUser, LServer, Name), + {selected, [{I}]} = + get_privacy_list_id_t(LUser, LServer, Name), + {I, true}; + {selected, [{I}]} -> {I, false} + end, + case New of + false -> + set_privacy_list(ID, RItems); + _ -> + set_privacy_list_new(ID, RItems) + end + end, + transaction(LServer, F). + +get_list(LUser, LServer, default) -> + case get_default_privacy_list(LUser, LServer) of + {selected, []} -> + error; + {selected, [{Default}]} -> + get_list(LUser, LServer, Default); + _Err -> + {error, db_failure} + end; +get_list(LUser, LServer, Name) -> + case get_privacy_list_data(LUser, LServer, Name) of + {selected, []} -> + error; + {selected, RItems} -> + {ok, {Name, lists:flatmap(fun raw_to_item/1, RItems)}}; + _Err -> + {error, db_failure} + end. + +get_lists(LUser, LServer) -> + case get_default_privacy_list(LUser, LServer) of + {selected, Selected} -> + Default = case Selected of + [] -> none; + [{DefName}] -> DefName + end, + case get_privacy_list_names(LUser, LServer) of + {selected, Names} -> + case lists:foldl( + fun(_, {error, _} = Err) -> + Err; + ({Name}, Acc) -> + case get_privacy_list_data(LUser, LServer, Name) of + {selected, RItems} -> + Items = lists:flatmap( + fun raw_to_item/1, + RItems), + [{Name, Items}|Acc]; + _Err -> + {error, db_failure} + end + end, [], Names) of + {error, Reason} -> + {error, Reason}; + Lists -> + {ok, #privacy{default = Default, + us = {LUser, LServer}, + lists = Lists}} + end; + _Err -> + {error, db_failure} + end; + _Err -> + {error, db_failure} + end. + +remove_lists(LUser, LServer) -> + case del_privacy_lists(LUser, LServer) of + ok -> + ok; + _Err -> + {error, db_failure} + end. + +export(Server) -> + SqlType = ejabberd_option:sql_type(Server), + case catch ejabberd_sql:sql_query(jid:nameprep(Server), + [<<"select id from privacy_list order by " + "id desc limit 1;">>]) of + {selected, [<<"id">>], [[I]]} -> + put(id, binary_to_integer(I)); + _ -> + put(id, 0) + end, + [{privacy, + fun(Host, #privacy{us = {LUser, LServer}, lists = Lists, + default = Default}) + when LServer == Host -> + if Default /= none -> + [?SQL("delete from privacy_default_list where" + " username=%(LUser)s and %(LServer)H;"), + ?SQL_INSERT( + "privacy_default_list", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "name=%(Default)s"])]; + true -> + [] + end ++ + lists:flatmap( + fun({Name, List}) -> + RItems = lists:map(fun item_to_raw/1, List), + ID = get_id(), + [?SQL("delete from privacy_list where" + " username=%(LUser)s and %(LServer)H and" + " name=%(Name)s;"), + ?SQL_INSERT( + "privacy_list", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "name=%(Name)s", + "id=%(ID)d"]), + ?SQL("delete from privacy_list_data where" + " id=%(ID)d;")] ++ + case SqlType of + pgsql -> + [?SQL("insert into privacy_list_data(id, t, " + "value, action, ord, match_all, match_iq, " + "match_message, match_presence_in, " + "match_presence_out) " + "values (%(ID)d, %(SType)s, %(SValue)s, %(SAction)s," + " %(Order)d, CAST(%(MatchAll)b as boolean), CAST(%(MatchIQ)b as boolean)," + " CAST(%(MatchMessage)b as boolean), CAST(%(MatchPresenceIn)b as boolean)," + " CAST(%(MatchPresenceOut)b as boolean));") + || {SType, SValue, SAction, Order, + MatchAll, MatchIQ, + MatchMessage, MatchPresenceIn, + MatchPresenceOut} <- RItems]; + _ -> + [?SQL("insert into privacy_list_data(id, t, " + "value, action, ord, match_all, match_iq, " + "match_message, match_presence_in, " + "match_presence_out) " + "values (%(ID)d, %(SType)s, %(SValue)s, %(SAction)s," + " %(Order)d, %(MatchAll)b, %(MatchIQ)b," + " %(MatchMessage)b, %(MatchPresenceIn)b," + " %(MatchPresenceOut)b);") + || {SType, SValue, SAction, Order, + MatchAll, MatchIQ, + MatchMessage, MatchPresenceIn, + MatchPresenceOut} <- RItems] + end + end, + Lists); + (_Host, _R) -> + [] + end}]. + +get_id() -> + ID = get(id), + put(id, ID + 1), + ID + 1. + +import(_) -> + ok. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +transaction(LServer, F) -> + case ejabberd_sql:sql_transaction(LServer, F) of + {atomic, Res} -> Res; + {aborted, _Reason} -> {error, db_failure} + end. + +raw_to_item({SType, SValue, SAction, Order, MatchAll, + MatchIQ, MatchMessage, MatchPresenceIn, + MatchPresenceOut} = Row) -> + try + {Type, Value} = case SType of + <<"n">> -> {none, none}; + <<"j">> -> + JID = jid:decode(SValue), + {jid, jid:tolower(JID)}; + <<"g">> -> {group, SValue}; + <<"s">> -> + case SValue of + <<"none">> -> {subscription, none}; + <<"both">> -> {subscription, both}; + <<"from">> -> {subscription, from}; + <<"to">> -> {subscription, to} + end + end, + Action = case SAction of + <<"a">> -> allow; + <<"d">> -> deny + end, + [#listitem{type = Type, value = Value, action = Action, + order = Order, match_all = MatchAll, match_iq = MatchIQ, + match_message = MatchMessage, + match_presence_in = MatchPresenceIn, + match_presence_out = MatchPresenceOut}] + catch _:_ -> + ?WARNING_MSG("Failed to parse row: ~p", [Row]), + [] + end. + +item_to_raw(#listitem{type = Type, value = Value, + action = Action, order = Order, match_all = MatchAll, + match_iq = MatchIQ, match_message = MatchMessage, + match_presence_in = MatchPresenceIn, + match_presence_out = MatchPresenceOut}) -> + {SType, SValue} = case Type of + none -> {<<"n">>, <<"">>}; + jid -> {<<"j">>, jid:encode(Value)}; + group -> {<<"g">>, Value}; + subscription -> + case Value of + none -> {<<"s">>, <<"none">>}; + both -> {<<"s">>, <<"both">>}; + from -> {<<"s">>, <<"from">>}; + to -> {<<"s">>, <<"to">>} + end + end, + SAction = case Action of + allow -> <<"a">>; + deny -> <<"d">> + end, + {SType, SValue, SAction, Order, MatchAll, MatchIQ, + MatchMessage, MatchPresenceIn, MatchPresenceOut}. + +get_default_privacy_list(LUser, LServer) -> + ejabberd_sql:sql_query( + LServer, + ?SQL("select @(name)s from privacy_default_list " + "where username=%(LUser)s and %(LServer)H")). + +get_default_privacy_list_t(LUser, LServer) -> + ejabberd_sql:sql_query_t( + ?SQL("select @(name)s from privacy_default_list " + "where username=%(LUser)s and %(LServer)H")). + +get_privacy_list_names(LUser, LServer) -> + ejabberd_sql:sql_query( + LServer, + ?SQL("select @(name)s from privacy_list" + " where username=%(LUser)s and %(LServer)H")). + +get_privacy_list_names_t(LUser, LServer) -> + ejabberd_sql:sql_query_t( + ?SQL("select @(name)s from privacy_list" + " where username=%(LUser)s and %(LServer)H")). + +get_privacy_list_id_t(LUser, LServer, Name) -> + ejabberd_sql:sql_query_t( + ?SQL("select @(id)d from privacy_list" + " where username=%(LUser)s and %(LServer)H and name=%(Name)s")). + +get_privacy_list_data(LUser, LServer, Name) -> + ejabberd_sql:sql_query( + LServer, + ?SQL("select @(t)s, @(value)s, @(action)s, @(ord)d, @(match_all)b, " + "@(match_iq)b, @(match_message)b, @(match_presence_in)b, " + "@(match_presence_out)b from privacy_list_data " + "where id =" + " (select id from privacy_list" + " where username=%(LUser)s and %(LServer)H and name=%(Name)s) " + "order by ord")). + +set_default_privacy_list(LUser, LServer, Name) -> + ?SQL_UPSERT_T( + "privacy_default_list", + ["!username=%(LUser)s", + "!server_host=%(LServer)s", + "name=%(Name)s"]). + +unset_default_privacy_list(LUser, LServer) -> + case ejabberd_sql:sql_query( + LServer, + ?SQL("delete from privacy_default_list" + " where username=%(LUser)s and %(LServer)H")) of + {updated, _} -> ok; + Err -> Err + end. + +remove_privacy_list_t(LUser, LServer, Name) -> + case ejabberd_sql:sql_query_t( + ?SQL("delete from privacy_list where" + " username=%(LUser)s and %(LServer)H and name=%(Name)s")) of + {updated, 0} -> {error, notfound}; + {updated, _} -> ok; + Err -> Err + end. + +add_privacy_list(LUser, LServer, Name) -> + ejabberd_sql:sql_query_t( + ?SQL_INSERT( + "privacy_list", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "name=%(Name)s"])). + +set_privacy_list_new(ID, RItems) -> + lists:foreach( + fun({SType, SValue, SAction, Order, MatchAll, MatchIQ, + MatchMessage, MatchPresenceIn, MatchPresenceOut}) -> + ejabberd_sql:sql_query_t( + ?SQL("insert into privacy_list_data(id, t, " + "value, action, ord, match_all, match_iq, " + "match_message, match_presence_in, match_presence_out) " + "values (%(ID)d, %(SType)s, %(SValue)s, %(SAction)s," + " %(Order)d, %(MatchAll)b, %(MatchIQ)b," + " %(MatchMessage)b, %(MatchPresenceIn)b," + " %(MatchPresenceOut)b)")) + end, + RItems). + +calculate_difference(List1, List2) -> + Set1 = gb_sets:from_list(List1), + Set2 = gb_sets:from_list(List2), + {gb_sets:to_list(gb_sets:subtract(Set1, Set2)), + gb_sets:to_list(gb_sets:subtract(Set2, Set1))}. + +set_privacy_list(ID, RItems) -> + case ejabberd_sql:sql_query_t( + ?SQL("select @(t)s, @(value)s, @(action)s, @(ord)d, @(match_all)b, " + "@(match_iq)b, @(match_message)b, @(match_presence_in)b, " + "@(match_presence_out)b from privacy_list_data " + "where id=%(ID)d")) of + {selected, ExistingItems} -> + {ToAdd2, ToDelete2} = calculate_difference(RItems, ExistingItems), + ToAdd3 = if + ToDelete2 /= [] -> + ejabberd_sql:sql_query_t( + ?SQL("delete from privacy_list_data where id=%(ID)d")), + RItems; + true -> + ToAdd2 + end, + lists:foreach( + fun({SType, SValue, SAction, Order, MatchAll, MatchIQ, + MatchMessage, MatchPresenceIn, MatchPresenceOut}) -> + ejabberd_sql:sql_query_t( + ?SQL("insert into privacy_list_data(id, t, " + "value, action, ord, match_all, match_iq, " + "match_message, match_presence_in, match_presence_out) " + "values (%(ID)d, %(SType)s, %(SValue)s, %(SAction)s," + " %(Order)d, %(MatchAll)b, %(MatchIQ)b," + " %(MatchMessage)b, %(MatchPresenceIn)b," + " %(MatchPresenceOut)b)")) + end, + ToAdd3); + Err -> + Err + end. + +del_privacy_lists(LUser, LServer) -> + case ejabberd_sql:sql_query( + LServer, + ?SQL("delete from privacy_list where username=%(LUser)s and %(LServer)H")) of + {updated, _} -> + case ejabberd_sql:sql_query( + LServer, + ?SQL("delete from privacy_default_list " + "where username=%(LUser)s and %(LServer)H")) of + {updated, _} -> ok; + Err -> Err + end; + Err -> + Err + end. diff --git a/src/mod_private.erl b/src/mod_private.erl index cedcb2787..6f70e1c9a 100644 --- a/src/mod_private.erl +++ b/src/mod_private.erl @@ -5,7 +5,7 @@ %%% Created : 16 Jan 2003 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -27,295 +27,613 @@ -author('alexey@process-one.net'). +-protocol({xep, 49, '1.2', '0.1.0', "complete", ""}). +-protocol({xep, 411, '0.2.0', '18.12', "complete", ""}). +-protocol({xep, 402, '1.1.3', '23.10', "complete", ""}). + -behaviour(gen_mod). --export([start/2, stop/1, process_sm_iq/3, import/3, - remove_user/2, get_data/2, export/1, import/1]). +-export([start/2, stop/1, reload/3, process_sm_iq/1, import_info/0, + remove_user/2, get_data/2, get_data/3, export/1, mod_doc/0, + import/5, import_start/2, mod_opt_type/1, set_data/2, + mod_options/1, depends/2, get_sm_features/5, pubsub_publish_item/6, + pubsub_delete_item/5, pubsub_tree_call/4]). + +-export([get_commands_spec/0, bookmarks_to_pep/2]). + +-export([webadmin_menu_hostuser/4, webadmin_page_hostuser/4]). + +-import(ejabberd_web_admin, [make_command/4, make_command/2]). --include("ejabberd.hrl"). -include("logger.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). +-include("mod_private.hrl"). +-include("ejabberd_commands.hrl"). +-include("ejabberd_http.hrl"). +-include("ejabberd_web_admin.hrl"). +-include("translate.hrl"). +-include("pubsub.hrl"). --include("jlib.hrl"). +-define(PRIVATE_CACHE, private_cache). --record(private_storage, - {usns = {<<"">>, <<"">>, <<"">>} :: {binary(), binary(), binary() | - '$1' | '_'}, - xml = #xmlel{} :: xmlel() | '_' | '$1'}). +-callback init(binary(), gen_mod:opts()) -> any(). +-callback import(binary(), binary(), [binary()]) -> ok. +-callback set_data(binary(), binary(), [{binary(), xmlel()}]) -> ok | {error, any()}. +-callback get_data(binary(), binary(), binary()) -> {ok, xmlel()} | error | {error, any()}. +-callback get_all_data(binary(), binary()) -> {ok, [xmlel()]} | error | {error, any()}. +-callback del_data(binary(), binary()) -> ok | {error, any()}. +-callback use_cache(binary()) -> boolean(). +-callback cache_nodes(binary()) -> [node()]. --define(Xmlel_Query(Attrs, Children), - #xmlel{name = <<"query">>, attrs = Attrs, - children = Children}). +-optional_callbacks([use_cache/1, cache_nodes/1]). start(Host, Opts) -> - IQDisc = gen_mod:get_opt(iqdisc, Opts, fun gen_iq_handler:check_type/1, - one_queue), - case gen_mod:db_type(Opts) of - mnesia -> - mnesia:create_table(private_storage, - [{disc_only_copies, [node()]}, - {attributes, - record_info(fields, private_storage)}]), - update_table(); - _ -> ok + Mod = gen_mod:db_mod(Opts, ?MODULE), + Mod:init(Host, Opts), + init_cache(Mod, Host, Opts), + {ok, [{commands, get_commands_spec()}, + {hook, remove_user, remove_user, 50}, + {hook, disco_sm_features, get_sm_features, 50}, + {hook, pubsub_publish_item, pubsub_publish_item, 50}, + {hook, pubsub_delete_item, pubsub_delete_item, 50}, + {hook, pubsub_tree_call, pubsub_tree_call, 50}, + {hook, webadmin_menu_hostuser, webadmin_menu_hostuser, 50}, + {hook, webadmin_page_hostuser, webadmin_page_hostuser, 50}, + {iq_handler, ejabberd_sm, ?NS_PRIVATE, process_sm_iq}]}. + +stop(_Host) -> + ok. + +reload(Host, NewOpts, OldOpts) -> + NewMod = gen_mod:db_mod(NewOpts, ?MODULE), + OldMod = gen_mod:db_mod(OldOpts, ?MODULE), + if NewMod /= OldMod -> + NewMod:init(Host, NewOpts); + true -> + ok end, - ejabberd_hooks:add(remove_user, Host, ?MODULE, - remove_user, 50), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, - ?NS_PRIVATE, ?MODULE, process_sm_iq, IQDisc). + init_cache(NewMod, Host, NewOpts). -stop(Host) -> - ejabberd_hooks:delete(remove_user, Host, ?MODULE, - remove_user, 50), - gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, - ?NS_PRIVATE). +depends(_Host, _Opts) -> + [{mod_pubsub, soft}]. -process_sm_iq(#jid{luser = LUser, lserver = LServer}, - #jid{luser = LUser, lserver = LServer}, IQ) - when IQ#iq.type == set -> - case IQ#iq.sub_el of - #xmlel{name = <<"query">>, children = Xmlels} -> - case filter_xmlels(Xmlels) of - [] -> - IQ#iq{type = error, - sub_el = [IQ#iq.sub_el, ?ERR_NOT_ACCEPTABLE]}; - Data -> - DBType = gen_mod:db_type(LServer, ?MODULE), - F = fun () -> - lists:foreach(fun (Datum) -> - set_data(LUser, LServer, - Datum, DBType) - end, - Data) - end, - case DBType of - odbc -> ejabberd_odbc:sql_transaction(LServer, F); - mnesia -> mnesia:transaction(F); - riak -> F() - end, - IQ#iq{type = result, sub_el = []} - end; - _ -> - IQ#iq{type = error, - sub_el = [IQ#iq.sub_el, ?ERR_NOT_ACCEPTABLE]} +mod_opt_type(db_type) -> + econf:db_type(?MODULE); +mod_opt_type(use_cache) -> + econf:bool(); +mod_opt_type(cache_size) -> + econf:pos_int(infinity); +mod_opt_type(cache_missed) -> + econf:bool(); +mod_opt_type(cache_life_time) -> + econf:timeout(second, infinity). + +mod_options(Host) -> + [{db_type, ejabberd_config:default_db(Host, ?MODULE)}, + {use_cache, ejabberd_option:use_cache(Host)}, + {cache_size, ejabberd_option:cache_size(Host)}, + {cache_missed, ejabberd_option:cache_missed(Host)}, + {cache_life_time, ejabberd_option:cache_life_time(Host)}]. + +mod_doc() -> + #{desc => + [?T("This module adds support for " + "https://xmpp.org/extensions/xep-0049.html" + "[XEP-0049: Private XML Storage]."), "", + ?T("Using this method, XMPP entities can store " + "private data on the server, retrieve it " + "whenever necessary and share it between multiple " + "connected clients of the same user. The data stored " + "might be anything, as long as it is a valid XML. " + "One typical usage is storing a bookmark of all user's conferences " + "(https://xmpp.org/extensions/xep-0048.html" + "[XEP-0048: Bookmarks])."), "", + ?T("It also implements the bookmark conversion described in " + "https://xmpp.org/extensions/xep-0402.html[XEP-0402: PEP Native Bookmarks]" + ", see _`bookmarks_to_pep`_ API.")], + opts => + [{db_type, + #{value => "mnesia | sql", + desc => + ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}, + {use_cache, + #{value => "true | false", + desc => + ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, + {cache_size, + #{value => "pos_integer() | infinity", + desc => + ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, + {cache_missed, + #{value => "true | false", + desc => + ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, + {cache_life_time, + #{value => "timeout()", + desc => + ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}]}. + +-spec get_sm_features({error, stanza_error()} | empty | {result, [binary()]}, + jid(), jid(), binary(), binary()) -> + {error, stanza_error()} | empty | {result, [binary()]}. +get_sm_features({error, _Error} = Acc, _From, _To, _Node, _Lang) -> + Acc; +get_sm_features(Acc, _From, To, <<"">>, _Lang) -> + case gen_mod:is_loaded(To#jid.lserver, mod_pubsub) of + true -> + {result, [?NS_BOOKMARKS_CONVERSION_0, ?NS_PEP_BOOKMARKS_COMPAT, ?NS_PEP_BOOKMARKS_COMPAT_PEP | + case Acc of + {result, Features} -> Features; + empty -> [] + end]}; + false -> + Acc end; -%% -process_sm_iq(#jid{luser = LUser, lserver = LServer}, - #jid{luser = LUser, lserver = LServer}, IQ) - when IQ#iq.type == get -> - case IQ#iq.sub_el of - #xmlel{name = <<"query">>, attrs = Attrs, - children = Xmlels} -> - case filter_xmlels(Xmlels) of - [] -> - IQ#iq{type = error, - sub_el = [IQ#iq.sub_el, ?ERR_BAD_FORMAT]}; - Data -> - case catch get_data(LUser, LServer, Data) of - {'EXIT', _Reason} -> - IQ#iq{type = error, - sub_el = - [IQ#iq.sub_el, ?ERR_INTERNAL_SERVER_ERROR]}; - Storage_Xmlels -> - IQ#iq{type = result, - sub_el = [?Xmlel_Query(Attrs, Storage_Xmlels)]} - end - end; - _ -> - IQ#iq{type = error, - sub_el = [IQ#iq.sub_el, ?ERR_BAD_FORMAT]} +get_sm_features(Acc, _From, _To, _Node, _Lang) -> + Acc. + +-spec process_sm_iq(iq()) -> iq(). +process_sm_iq(#iq{type = Type, lang = Lang, + from = #jid{luser = LUser, lserver = LServer} = From, + to = #jid{luser = LUser, lserver = LServer}, + sub_els = [#private{sub_els = Els0}]} = IQ) -> + case filter_xmlels(Els0) of + [] -> + Txt = ?T("No private data found in this query"), + xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)); + Data when Type == set -> + case set_data(From, Data) of + ok -> + xmpp:make_iq_result(IQ); + {error, #stanza_error{} = Err} -> + xmpp:make_error(IQ, Err); + {error, _} -> + Txt = ?T("Database failure"), + Err = xmpp:err_internal_server_error(Txt, Lang), + xmpp:make_error(IQ, Err) + end; + Data when Type == get -> + case get_data(LUser, LServer, Data) of + {error, _} -> + Txt = ?T("Database failure"), + Err = xmpp:err_internal_server_error(Txt, Lang), + xmpp:make_error(IQ, Err); + Els -> + xmpp:make_iq_result(IQ, #private{sub_els = Els}) + end end; -%% -process_sm_iq(_From, _To, IQ) -> - IQ#iq{type = error, - sub_el = [IQ#iq.sub_el, ?ERR_FORBIDDEN]}. +process_sm_iq(#iq{lang = Lang} = IQ) -> + Txt = ?T("Query to another users is forbidden"), + xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)). -filter_xmlels(Xmlels) -> filter_xmlels(Xmlels, []). +-spec filter_xmlels([xmlel()]) -> [{binary(), xmlel()}]. +filter_xmlels(Els) -> + lists:flatmap( + fun(#xmlel{} = El) -> + case fxml:get_tag_attr_s(<<"xmlns">>, El) of + <<"">> -> []; + NS -> [{NS, El}] + end + end, Els). -filter_xmlels([], Data) -> lists:reverse(Data); -filter_xmlels([#xmlel{attrs = Attrs} = Xmlel | Xmlels], - Data) -> - case xml:get_attr_s(<<"xmlns">>, Attrs) of - <<"">> -> []; - XmlNS -> filter_xmlels(Xmlels, [{XmlNS, Xmlel} | Data]) - end; -filter_xmlels([_ | Xmlels], Data) -> - filter_xmlels(Xmlels, Data). +-spec set_data(jid(), [{binary(), xmlel()}]) -> ok | {error, _}. +set_data(JID, Data) -> + set_data(JID, Data, true, true). -set_data(LUser, LServer, {XmlNS, Xmlel}, mnesia) -> - mnesia:write(#private_storage{usns = - {LUser, LServer, XmlNS}, - xml = Xmlel}); -set_data(LUser, LServer, {XMLNS, El}, odbc) -> - Username = ejabberd_odbc:escape(LUser), - LXMLNS = ejabberd_odbc:escape(XMLNS), - SData = ejabberd_odbc:escape(xml:element_to_binary(El)), - odbc_queries:set_private_data(LServer, Username, LXMLNS, - SData); -set_data(LUser, LServer, {XMLNS, El}, riak) -> - ejabberd_riak:put(#private_storage{usns = {LUser, LServer, XMLNS}, - xml = El}, - private_storage_schema(), - [{'2i', [{<<"us">>, {LUser, LServer}}]}]). +-spec set_data(jid(), [{binary(), xmlel()}], boolean(), boolean()) -> ok | {error, _}. +set_data(JID, Data, PublishPepStorageBookmarks, PublishPepXmppBookmarks) -> + {LUser, LServer, _} = jid:tolower(JID), + Mod = gen_mod:db_mod(LServer, ?MODULE), + case Mod:set_data(LUser, LServer, Data) of + ok -> + delete_cache(Mod, LUser, LServer, Data), + case PublishPepStorageBookmarks of + true -> publish_pep_storage_bookmarks(JID, Data); + false -> ok + end, + case PublishPepXmppBookmarks of + true -> publish_pep_native_bookmarks(JID, Data); + false -> ok + end; + {error, _} = Err -> + Err + end. +-spec get_data(binary(), binary(), [{binary(), xmlel()}]) -> [xmlel()] | {error, _}. get_data(LUser, LServer, Data) -> - get_data(LUser, LServer, - gen_mod:db_type(LServer, ?MODULE), Data, []). - -get_data(_LUser, _LServer, _DBType, [], - Storage_Xmlels) -> - lists:reverse(Storage_Xmlels); -get_data(LUser, LServer, mnesia, - [{XmlNS, Xmlel} | Data], Storage_Xmlels) -> - case mnesia:dirty_read(private_storage, - {LUser, LServer, XmlNS}) - of - [#private_storage{xml = Storage_Xmlel}] -> - get_data(LUser, LServer, mnesia, Data, - [Storage_Xmlel | Storage_Xmlels]); - _ -> - get_data(LUser, LServer, mnesia, Data, - [Xmlel | Storage_Xmlels]) - end; -get_data(LUser, LServer, odbc, [{XMLNS, El} | Els], - Res) -> - Username = ejabberd_odbc:escape(LUser), - LXMLNS = ejabberd_odbc:escape(XMLNS), - case catch odbc_queries:get_private_data(LServer, - Username, LXMLNS) - of - {selected, [<<"data">>], [[SData]]} -> - case xml_stream:parse_element(SData) of - Data when is_record(Data, xmlel) -> - get_data(LUser, LServer, odbc, Els, [Data | Res]) - end; - _ -> get_data(LUser, LServer, odbc, Els, [El | Res]) - end; -get_data(LUser, LServer, riak, [{XMLNS, El} | Els], - Res) -> - case ejabberd_riak:get(private_storage, private_storage_schema(), - {LUser, LServer, XMLNS}) of - {ok, #private_storage{xml = NewEl}} -> - get_data(LUser, LServer, riak, Els, [NewEl|Res]); - _ -> - get_data(LUser, LServer, riak, Els, [El|Res]) - end. + Mod = gen_mod:db_mod(LServer, ?MODULE), + lists:foldr( + fun(_, {error, _} = Err) -> + Err; + ({NS, El}, Els) -> + Res = case use_cache(Mod, LServer) of + true -> + ets_cache:lookup( + ?PRIVATE_CACHE, {LUser, LServer, NS}, + fun() -> Mod:get_data(LUser, LServer, NS) end); + false -> + Mod:get_data(LUser, LServer, NS) + end, + case Res of + {ok, StorageEl} -> + [StorageEl|Els]; + error -> + [El|Els]; + {error, _} = Err -> + Err + end + end, [], Data). +-spec get_data(binary(), binary()) -> [xmlel()] | {error, _}. get_data(LUser, LServer) -> - get_all_data(LUser, LServer, - gen_mod:db_type(LServer, ?MODULE)). - -get_all_data(LUser, LServer, mnesia) -> - lists:flatten( - mnesia:dirty_select(private_storage, - [{#private_storage{usns = {LUser, LServer, '_'}, - xml = '$1'}, - [], ['$1']}])); -get_all_data(LUser, LServer, odbc) -> - Username = ejabberd_odbc:escape(LUser), - case catch odbc_queries:get_private_data(LServer, Username) of - {selected, [<<"namespace">>, <<"data">>], Res} -> - lists:flatmap( - fun([_, SData]) -> - case xml_stream:parse_element(SData) of - #xmlel{} = El -> - [El]; - _ -> - [] - end - end, Res); - _ -> - [] - end; -get_all_data(LUser, LServer, riak) -> - case ejabberd_riak:get_by_index( - private_storage, private_storage_schema(), - <<"us">>, {LUser, LServer}) of - {ok, Res} -> - [El || #private_storage{xml = El} <- Res]; - _ -> - [] + Mod = gen_mod:db_mod(LServer, ?MODULE), + case Mod:get_all_data(LUser, LServer) of + {ok, Els} -> Els; + error -> []; + {error, _} = Err -> Err end. -private_storage_schema() -> - {record_info(fields, private_storage), #private_storage{}}. - +-spec remove_user(binary(), binary()) -> ok. remove_user(User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - remove_user(LUser, LServer, - gen_mod:db_type(Server, ?MODULE)). + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + Mod = gen_mod:db_mod(Server, ?MODULE), + Data = case use_cache(Mod, LServer) of + true -> + case Mod:get_all_data(LUser, LServer) of + {ok, Els} -> filter_xmlels(Els); + _ -> [] + end; + false -> + [] + end, + Mod:del_data(LUser, LServer), + delete_cache(Mod, LUser, LServer, Data). -remove_user(LUser, LServer, mnesia) -> - F = fun () -> - Namespaces = mnesia:select(private_storage, - [{#private_storage{usns = - {LUser, - LServer, - '$1'}, - _ = '_'}, - [], ['$$']}]), - lists:foreach(fun ([Namespace]) -> - mnesia:delete({private_storage, - {LUser, LServer, - Namespace}}) - end, - Namespaces) - end, - mnesia:transaction(F); -remove_user(LUser, LServer, odbc) -> - Username = ejabberd_odbc:escape(LUser), - odbc_queries:del_user_private_storage(LServer, - Username); -remove_user(LUser, LServer, riak) -> - {atomic, ejabberd_riak:delete_by_index(private_storage, - <<"us">>, {LUser, LServer})}. - -update_table() -> - Fields = record_info(fields, private_storage), - case mnesia:table_info(private_storage, attributes) of - Fields -> - ejabberd_config:convert_table_to_binary( - private_storage, Fields, set, - fun(#private_storage{usns = {U, _, _}}) -> U end, - fun(#private_storage{usns = {U, S, NS}, xml = El} = R) -> - R#private_storage{usns = {iolist_to_binary(U), - iolist_to_binary(S), - iolist_to_binary(NS)}, - xml = xml:to_xmlel(El)} - end); - _ -> - ?INFO_MSG("Recreating private_storage table", []), - mnesia:transform_table(private_storage, ignore, Fields) +%%%=================================================================== +%%% Pubsub +%%%=================================================================== +-spec publish_pep_storage_bookmarks(jid(), [{binary(), xmlel()}]) -> ok | {error, stanza_error()}. +publish_pep_storage_bookmarks(JID, Data) -> + {_, LServer, _} = LBJID = jid:remove_resource(jid:tolower(JID)), + case gen_mod:is_loaded(LServer, mod_pubsub) of + true -> + case lists:keyfind(?NS_STORAGE_BOOKMARKS, 1, Data) of + false -> ok; + {_, El} -> + case mod_pubsub:get_items(LBJID, ?NS_STORAGE_BOOKMARKS) of + {error, #stanza_error{reason = 'item-not-found'}} -> + PubOpts = [{persist_items, true}, + {access_model, whitelist}], + case mod_pubsub:publish_item( + LBJID, LServer, ?NS_STORAGE_BOOKMARKS, JID, + <<"current">>, [El], PubOpts, all) of + {result, _} -> ok; + {error, _} = Err -> Err + end; + _ -> + case mod_pubsub:publish_item( + LBJID, LServer, ?NS_STORAGE_BOOKMARKS, JID, + <<"current">>, [El], [], all) of + {result, _} -> ok; + {error, _} = Err -> Err + end + end + end; + false -> + ok end. -export(_Server) -> - [{private_storage, - fun(Host, #private_storage{usns = {LUser, LServer, XMLNS}, - xml = Data}) - when LServer == Host -> - Username = ejabberd_odbc:escape(LUser), - LXMLNS = ejabberd_odbc:escape(XMLNS), - SData = - ejabberd_odbc:escape(xml:element_to_binary(Data)), - odbc_queries:set_private_data_sql(Username, LXMLNS, - SData); - (_Host, _R) -> - [] - end}]. +-spec publish_pep_native_bookmarks(jid(), [{binary(), xmlel()}]) -> ok | {error, stanza_error()}. +publish_pep_native_bookmarks(JID, Data) -> + {_, LServer, _} = LBJID = jid:remove_resource(jid:tolower(JID)), + case gen_mod:is_loaded(LServer, mod_pubsub) of + true -> + case lists:keyfind(?NS_STORAGE_BOOKMARKS, 1, Data) of + {_, Bookmarks0} -> + Bookmarks = try xmpp:decode(Bookmarks0) of + #bookmark_storage{conference = C} -> C; + _ -> [] + catch _:{xmpp_codec, Why} -> + ?DEBUG("Failed to decode bookmarks of ~ts: ~ts", + [jid:encode(JID), xmpp:format_error(Why)]), + [] + end, + PubOpts = [{persist_items, true}, {access_model, whitelist}, {max_items, max}, {notify_retract,true}, {notify_delete,true}, {send_last_published_item, never}], + case mod_pubsub:get_items(LBJID, ?NS_PEP_BOOKMARKS) of + PepBookmarks when is_list(PepBookmarks) -> + put(mod_private_pep_update, true), + PepBookmarksMap = lists:foldl(fun pubsub_item_to_map/2, #{}, PepBookmarks), + {ToDelete, Ret} = + lists:foldl( + fun(#bookmark_conference{jid = BookmarkJID} = Bookmark, {Map2, Ret2}) -> + PB = storage_bookmark_to_xmpp_bookmark(Bookmark), + case maps:take(jid:tolower(BookmarkJID), Map2) of + {StoredBookmark, Map3} when StoredBookmark == PB -> + {Map3, Ret2}; + {_, Map4} -> + {Map4, + err_ret(Ret2, mod_pubsub:publish_item( + LBJID, LServer, ?NS_PEP_BOOKMARKS, JID, + jid:encode(BookmarkJID), [xmpp:encode(PB)], [], all))}; + _ -> + {Map2, + err_ret(Ret2, mod_pubsub:publish_item( + LBJID, LServer, ?NS_PEP_BOOKMARKS, JID, + jid:encode(BookmarkJID), [xmpp:encode(PB)], [], all))} + end + end, {PepBookmarksMap, ok}, Bookmarks), + Ret4 = + maps:fold( + fun(DeleteJid, _, Ret3) -> + err_ret(Ret3, mod_pubsub:delete_item(LBJID, ?NS_PEP_BOOKMARKS, + JID, jid:encode(DeleteJid))) + end, Ret, ToDelete), + erase(mod_private_pep_update), + Ret4; + {error, #stanza_error{reason = 'item-not-found'}} -> + put(mod_private_pep_update, true), + Ret7 = + lists:foldl( + fun(#bookmark_conference{jid = BookmarkJID} = Bookmark, Ret5) -> + PB = storage_bookmark_to_xmpp_bookmark(Bookmark), + err_ret(Ret5, mod_pubsub:publish_item( + LBJID, LServer, ?NS_PEP_BOOKMARKS, JID, + jid:encode(BookmarkJID), [xmpp:encode(PB)], PubOpts, all)) + end, ok, Bookmarks), + erase(mod_private_pep_update), + Ret7; + _ -> + ok + end; + _ -> + ok + end; + false -> + ok + end. -import(LServer) -> - [{<<"select username, namespace, data from private_storage;">>, - fun([LUser, XMLNS, XML]) -> - El = #xmlel{} = xml_stream:parse_element(XML), - #private_storage{usns = {LUser, LServer, XMLNS}, - xml = El} - end}]. +err_ret({error, _} = E, _) -> + E; +err_ret(ok, {error, _} = E) -> + E; +err_ret(_, _) -> + ok. -import(_LServer, mnesia, #private_storage{} = PS) -> - mnesia:dirty_write(PS); +-spec pubsub_publish_item(binary(), binary(), jid(), jid(), + binary(), [xmlel()]) -> any(). +pubsub_publish_item(LServer, ?NS_STORAGE_BOOKMARKS, + #jid{luser = LUser, lserver = LServer} = From, + #jid{luser = LUser, lserver = LServer}, + _ItemId, [Payload|_]) -> + set_data(From, [{?NS_STORAGE_BOOKMARKS, Payload}], false, true); +pubsub_publish_item(LServer, ?NS_PEP_BOOKMARKS, + #jid{luser = LUser, lserver = LServer} = From, + #jid{luser = LUser, lserver = LServer}, + _ItemId, _Payload) -> + NotRecursion = get(mod_private_pep_update) == undefined, + case mod_pubsub:get_items({LUser, LServer, <<>>}, ?NS_PEP_BOOKMARKS) of + Bookmarks when is_list(Bookmarks), NotRecursion -> + Bookmarks2 = lists:filtermap(fun pubsub_item_to_storage_bookmark/1, Bookmarks), + Payload = xmpp:encode(#bookmark_storage{conference = Bookmarks2}), + set_data(From, [{?NS_STORAGE_BOOKMARKS, Payload}], true, false); + _ -> + ok + end; +pubsub_publish_item(_, _, _, _, _, _) -> + ok. -import(_LServer, riak, #private_storage{usns = {LUser, LServer, _}} = PS) -> - ejabberd_riak:put(PS, private_storage_schema(), - [{'2i', [{<<"us">>, {LUser, LServer}}]}]); -import(_, _, _) -> - pass. +-spec pubsub_delete_item(binary(), binary(), jid(), jid(), binary()) -> any(). +pubsub_delete_item(LServer, ?NS_PEP_BOOKMARKS, + #jid{luser = LUser, lserver = LServer} = From, + #jid{luser = LUser, lserver = LServer}, + _ItemId) -> + NotRecursion = get(mod_private_pep_update) == undefined, + case mod_pubsub:get_items({LUser, LServer, <<>>}, ?NS_PEP_BOOKMARKS) of + Bookmarks when is_list(Bookmarks), NotRecursion -> + Bookmarks2 = lists:filtermap(fun pubsub_item_to_storage_bookmark/1, Bookmarks), + Payload = xmpp:encode(#bookmark_storage{conference = Bookmarks2}), + set_data(From, [{?NS_STORAGE_BOOKMARKS, Payload}], true, false); + _ -> + ok + end; +pubsub_delete_item(_, _, _, _, _) -> + ok. + +-spec pubsub_item_to_storage_bookmark(#pubsub_item{}) -> {true, bookmark_conference()} | false. +pubsub_item_to_storage_bookmark(#pubsub_item{itemid = {Id, _}, payload = [#xmlel{} = B | _]}) -> + try {xmpp:decode(B), jid:decode(Id)} of + {#pep_bookmarks_conference{name = Name, autojoin = AutoJoin, + nick = Nick, password = Password}, + #jid{} = Jid} -> + {true, #bookmark_conference{jid = Jid, name = Name, + autojoin = AutoJoin, nick = Nick, + password = Password}}; + {_, _} -> + false + catch + _:{xmpp_codec, Why} -> + ?DEBUG("Failed to decode bookmark element (~ts): ~ts", + [Id, xmpp:format_error(Why)]), + false; + _:{bad_jid, _} -> + ?DEBUG("Failed to decode bookmark ID (~ts)", [Id]), + false + end; +pubsub_item_to_storage_bookmark(_) -> + false. + +-spec pubsub_tree_call(Res :: any(), _Tree::any(), atom(), any()) -> any(). +pubsub_tree_call({error, #stanza_error{reason = 'item-not-found'}} = Res, Tree, get_node, + [{User, Server, _}, ?NS_PEP_BOOKMARKS] = Args) -> + case get(mod_private_in_pubsub_tree_call) of + undefined -> + put(mod_private_in_pubsub_tree_call, true), + bookmarks_to_pep(User, Server), + Res2 = apply(Tree, get_node, Args), + erase(mod_private_in_pubsub_tree_call), + Res2; + _ -> + Res + end; +pubsub_tree_call(Res, _Tree, _Function, _Args) -> + Res. + +-spec storage_bookmark_to_xmpp_bookmark(bookmark_conference()) -> pep_bookmarks_conference(). +storage_bookmark_to_xmpp_bookmark(#bookmark_conference{name = Name, autojoin = AutoJoin, nick = Nick, + password = Password}) -> + #pep_bookmarks_conference{name = Name, autojoin = AutoJoin, nick = Nick, + password = Password}. + +-spec pubsub_item_to_map(#pubsub_item{}, map()) -> map(). +pubsub_item_to_map(#pubsub_item{itemid = {Id, _}, payload = [#xmlel{} = B | _]}, Map) -> + try {xmpp:decode(B), jid:decode(Id)} of + {#pep_bookmarks_conference{} = B1, #jid{} = Jid} -> + B2 = B1#pep_bookmarks_conference{extensions = undefined}, + maps:put(jid:tolower(Jid), B2, Map); + {_, _} -> + Map + catch + _:{xmpp_codec, Why} -> + ?DEBUG("Failed to decode bookmark element (~ts): ~ts", + [Id, xmpp:format_error(Why)]), + Map; + _:{bad_jid, _} -> + ?DEBUG("Failed to decode bookmark ID (~ts)", [Id]), + Map + end; +pubsub_item_to_map(_, Map) -> + Map. + +%%%=================================================================== +%%% Commands +%%%=================================================================== +-spec get_commands_spec() -> [ejabberd_commands()]. +get_commands_spec() -> + [#ejabberd_commands{name = bookmarks_to_pep, tags = [private], + desc = "Export private XML storage bookmarks to PEP", + module = ?MODULE, function = bookmarks_to_pep, + args = [{user, binary}, {host, binary}], + args_rename = [{server, host}], + args_desc = ["Username", "Server"], + args_example = [<<"bob">>, <<"example.com">>], + result = {res, restuple}, + result_desc = "Result tuple", + result_example = {ok, <<"Bookmarks exported">>}}]. + +-spec bookmarks_to_pep(binary(), binary()) + -> {ok, binary()} | {error, binary()}. +bookmarks_to_pep(User, Server) -> + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + Mod = gen_mod:db_mod(LServer, ?MODULE), + Res = case use_cache(Mod, LServer) of + true -> + ets_cache:lookup( + ?PRIVATE_CACHE, {LUser, LServer, ?NS_STORAGE_BOOKMARKS}, + fun() -> + Mod:get_data(LUser, LServer, ?NS_STORAGE_BOOKMARKS) + end); + false -> + Mod:get_data(LUser, LServer, ?NS_STORAGE_BOOKMARKS) + end, + case Res of + {ok, El} -> + Data = [{?NS_STORAGE_BOOKMARKS, El}], + case publish_pep_storage_bookmarks(jid:make(User, Server), Data) of + ok -> + case publish_pep_native_bookmarks(jid:make(User, Server), Data) of + ok -> + {ok, <<"Bookmarks exported to PEP node">>}; + {error, Err} -> + {error, xmpp:format_stanza_error(Err)} + end; + {error, Err} -> + {error, xmpp:format_stanza_error(Err)} + + end; + _ -> + {error, <<"Cannot retrieve bookmarks from private XML storage">>} + end. + +%%%=================================================================== +%%% WebAdmin +%%%=================================================================== + +webadmin_menu_hostuser(Acc, _Host, _Username, _Lang) -> + Acc ++ [{<<"private">>, <<"Private XML Storage">>}]. + +webadmin_page_hostuser(_, Host, User, + #request{path = [<<"private">>]} = R) -> + Res = ?H1GL(<<"Private XML Storage">>, <<"modules/#mod_private">>, <<"mod_private">>) + ++ [make_command(private_set, R, [{<<"user">>, User}, {<<"host">>, Host}], []), + make_command(private_get, R, [{<<"user">>, User}, {<<"host">>, Host}], [])], + {stop, Res}; +webadmin_page_hostuser(Acc, _, _, _) -> Acc. + +%%%=================================================================== +%%% Cache +%%%=================================================================== +-spec delete_cache(module(), binary(), binary(), [{binary(), xmlel()}]) -> ok. +delete_cache(Mod, LUser, LServer, Data) -> + case use_cache(Mod, LServer) of + true -> + Nodes = cache_nodes(Mod, LServer), + lists:foreach( + fun({NS, _}) -> + ets_cache:delete(?PRIVATE_CACHE, + {LUser, LServer, NS}, + Nodes) + end, Data); + false -> + ok + end. + +-spec init_cache(module(), binary(), gen_mod:opts()) -> ok. +init_cache(Mod, Host, Opts) -> + case use_cache(Mod, Host) of + true -> + CacheOpts = cache_opts(Opts), + ets_cache:new(?PRIVATE_CACHE, CacheOpts); + false -> + ets_cache:delete(?PRIVATE_CACHE) + end. + +-spec cache_opts(gen_mod:opts()) -> [proplists:property()]. +cache_opts(Opts) -> + MaxSize = mod_private_opt:cache_size(Opts), + CacheMissed = mod_private_opt:cache_missed(Opts), + LifeTime = mod_private_opt:cache_life_time(Opts), + [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}]. + +-spec use_cache(module(), binary()) -> boolean(). +use_cache(Mod, Host) -> + case erlang:function_exported(Mod, use_cache, 1) of + true -> Mod:use_cache(Host); + false -> mod_private_opt:use_cache(Host) + end. + +-spec cache_nodes(module(), binary()) -> [node()]. +cache_nodes(Mod, Host) -> + case erlang:function_exported(Mod, cache_nodes, 1) of + true -> Mod:cache_nodes(Host); + false -> ejabberd_cluster:get_nodes() + end. + +%%%=================================================================== +%%% Import/Export +%%%=================================================================== +import_info() -> + [{<<"private_storage">>, 4}]. + +import_start(LServer, DBType) -> + Mod = gen_mod:db_mod(DBType, ?MODULE), + Mod:init(LServer, []). + +export(LServer) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:export(LServer). + +import(LServer, {sql, _}, DBType, Tab, L) -> + Mod = gen_mod:db_mod(DBType, ?MODULE), + Mod:import(LServer, Tab, L). diff --git a/src/mod_private_mnesia.erl b/src/mod_private_mnesia.erl new file mode 100644 index 000000000..42bab447f --- /dev/null +++ b/src/mod_private_mnesia.erl @@ -0,0 +1,133 @@ +%%%------------------------------------------------------------------- +%%% File : mod_private_mnesia.erl +%%% Author : Evgeny Khramtsov +%%% Created : 13 Apr 2016 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_private_mnesia). + +-behaviour(mod_private). + +%% API +-export([init/2, set_data/3, get_data/3, get_all_data/2, del_data/2, + use_cache/1, import/3]). +-export([need_transform/1, transform/1]). + +-include_lib("xmpp/include/xmpp.hrl"). +-include("mod_private.hrl"). +-include("logger.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + ejabberd_mnesia:create(?MODULE, private_storage, + [{disc_only_copies, [node()]}, + {attributes, record_info(fields, private_storage)}]). + +use_cache(Host) -> + case mnesia:table_info(private_storage, storage_type) of + disc_only_copies -> + mod_private_opt:use_cache(Host); + _ -> + false + end. + +set_data(LUser, LServer, Data) -> + F = fun () -> + lists:foreach( + fun({XmlNS, Xmlel}) -> + mnesia:write( + #private_storage{ + usns = {LUser, LServer, XmlNS}, + xml = Xmlel}) + end, Data) + end, + transaction(F). + +get_data(LUser, LServer, XmlNS) -> + case mnesia:dirty_read(private_storage, {LUser, LServer, XmlNS}) of + [#private_storage{xml = Storage_Xmlel}] -> + {ok, Storage_Xmlel}; + _ -> + error + end. + +get_all_data(LUser, LServer) -> + case lists:flatten( + mnesia:dirty_select(private_storage, + [{#private_storage{usns = {LUser, LServer, '_'}, + xml = '$1'}, + [], ['$1']}])) of + [] -> + error; + Res -> + {ok, Res} + end. + +del_data(LUser, LServer) -> + F = fun () -> + Namespaces = mnesia:select(private_storage, + [{#private_storage{usns = + {LUser, + LServer, + '$1'}, + _ = '_'}, + [], ['$$']}]), + lists:foreach(fun ([Namespace]) -> + mnesia:delete({private_storage, + {LUser, LServer, + Namespace}}) + end, + Namespaces) + end, + transaction(F). + +import(LServer, <<"private_storage">>, + [LUser, XMLNS, XML, _TimeStamp]) -> + El = #xmlel{} = fxml_stream:parse_element(XML), + PS = #private_storage{usns = {LUser, LServer, XMLNS}, xml = El}, + mnesia:dirty_write(PS). + +need_transform({private_storage, {U, S, NS}, _}) + when is_list(U) orelse is_list(S) orelse is_list(NS) -> + ?INFO_MSG("Mnesia table 'private_storage' will be converted to binary", []), + true; +need_transform(_) -> + false. + +transform(#private_storage{usns = {U, S, NS}, xml = El} = R) -> + R#private_storage{usns = {iolist_to_binary(U), + iolist_to_binary(S), + iolist_to_binary(NS)}, + xml = fxml:to_xmlel(El)}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +transaction(F) -> + case mnesia:transaction(F) of + {atomic, Res} -> + Res; + {aborted, Reason} -> + ?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]), + {error, db_failure} + end. diff --git a/src/mod_private_opt.erl b/src/mod_private_opt.erl new file mode 100644 index 000000000..71257217d --- /dev/null +++ b/src/mod_private_opt.erl @@ -0,0 +1,41 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_private_opt). + +-export([cache_life_time/1]). +-export([cache_missed/1]). +-export([cache_size/1]). +-export([db_type/1]). +-export([use_cache/1]). + +-spec cache_life_time(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +cache_life_time(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_life_time, Opts); +cache_life_time(Host) -> + gen_mod:get_module_opt(Host, mod_private, cache_life_time). + +-spec cache_missed(gen_mod:opts() | global | binary()) -> boolean(). +cache_missed(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_missed, Opts); +cache_missed(Host) -> + gen_mod:get_module_opt(Host, mod_private, cache_missed). + +-spec cache_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +cache_size(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_size, Opts); +cache_size(Host) -> + gen_mod:get_module_opt(Host, mod_private, cache_size). + +-spec db_type(gen_mod:opts() | global | binary()) -> atom(). +db_type(Opts) when is_map(Opts) -> + gen_mod:get_opt(db_type, Opts); +db_type(Host) -> + gen_mod:get_module_opt(Host, mod_private, db_type). + +-spec use_cache(gen_mod:opts() | global | binary()) -> boolean(). +use_cache(Opts) when is_map(Opts) -> + gen_mod:get_opt(use_cache, Opts); +use_cache(Host) -> + gen_mod:get_module_opt(Host, mod_private, use_cache). + diff --git a/src/mod_private_sql.erl b/src/mod_private_sql.erl new file mode 100644 index 000000000..d493b0587 --- /dev/null +++ b/src/mod_private_sql.erl @@ -0,0 +1,160 @@ +%%%------------------------------------------------------------------- +%%% File : mod_private_sql.erl +%%% Author : Evgeny Khramtsov +%%% Created : 13 Apr 2016 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_private_sql). +-behaviour(mod_private). + +%% API +-export([init/2, set_data/3, get_data/3, get_all_data/2, del_data/2, + import/3, export/1]). +-export([sql_schemas/0]). + +-include_lib("xmpp/include/xmpp.hrl"). +-include("mod_private.hrl"). +-include("ejabberd_sql_pt.hrl"). +-include("logger.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(Host, _Opts) -> + ejabberd_sql_schema:update_schema(Host, ?MODULE, sql_schemas()), + ok. + +sql_schemas() -> + [#sql_schema{ + version = 1, + tables = + [#sql_table{ + name = <<"private_storage">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"namespace">>, type = text}, + #sql_column{name = <<"data">>, type = text}, + #sql_column{name = <<"created_at">>, type = timestamp, + default = true}], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"username">>, + <<"namespace">>], + unique = true}]}]}]. + +set_data(LUser, LServer, Data) -> + F = fun() -> + lists:foreach( + fun({XMLNS, El}) -> + SData = fxml:element_to_binary(El), + ?SQL_UPSERT_T( + "private_storage", + ["!username=%(LUser)s", + "!server_host=%(LServer)s", + "!namespace=%(XMLNS)s", + "data=%(SData)s"]) + end, Data) + end, + case ejabberd_sql:sql_transaction(LServer, F) of + {atomic, ok} -> + ok; + _ -> + {error, db_failure} + end. + +get_data(LUser, LServer, XMLNS) -> + case ejabberd_sql:sql_query( + LServer, + ?SQL("select @(data)s from private_storage" + " where username=%(LUser)s and %(LServer)H" + " and namespace=%(XMLNS)s")) of + {selected, [{SData}]} -> + parse_element(LUser, LServer, SData); + {selected, []} -> + error; + _ -> + {error, db_failure} + end. + +get_all_data(LUser, LServer) -> + case ejabberd_sql:sql_query( + LServer, + ?SQL("select @(namespace)s, @(data)s from private_storage" + " where username=%(LUser)s and %(LServer)H")) of + {selected, []} -> + error; + {selected, Res} -> + {ok, lists:flatmap( + fun({_, SData}) -> + case parse_element(LUser, LServer, SData) of + {ok, El} -> [El]; + error -> [] + end + end, Res)}; + _ -> + {error, db_failure} + end. + +del_data(LUser, LServer) -> + case ejabberd_sql:sql_query( + LServer, + ?SQL("delete from private_storage" + " where username=%(LUser)s and %(LServer)H")) of + {updated, _} -> + ok; + _ -> + {error, db_failure} + end. + +export(_Server) -> + [{private_storage, + fun(Host, #private_storage{usns = {LUser, LServer, XMLNS}, + xml = Data}) + when LServer == Host -> + SData = fxml:element_to_binary(Data), + [?SQL("delete from private_storage where" + " username=%(LUser)s and %(LServer)H and namespace=%(XMLNS)s;"), + ?SQL_INSERT( + "private_storage", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "namespace=%(XMLNS)s", + "data=%(SData)s"])]; + (_Host, _R) -> + [] + end}]. + +import(_, _, _) -> + ok. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +parse_element(LUser, LServer, XML) -> + case fxml_stream:parse_element(XML) of + El when is_record(El, xmlel) -> + {ok, El}; + _ -> + ?ERROR_MSG("Malformed XML element in SQL table " + "'private_storage' for user ~ts@~ts: ~ts", + [LUser, LServer, XML]), + error + end. diff --git a/src/mod_privilege.erl b/src/mod_privilege.erl new file mode 100644 index 000000000..d614c8a35 --- /dev/null +++ b/src/mod_privilege.erl @@ -0,0 +1,666 @@ +%%%------------------------------------------------------------------- +%%% File : mod_privilege.erl +%%% Author : Anna Mukharram +%%% Purpose : XEP-0356: Privileged Entity +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_privilege). + +-author('amuhar3@gmail.com'). + +-protocol({xep, 356, '0.4.1', '24.10', "complete", ""}). + +-behaviour(gen_server). +-behaviour(gen_mod). + +%% API +-export([start/2, stop/1, reload/3, mod_opt_type/1, mod_options/1, depends/2]). +-export([mod_doc/0]). +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). +-export([component_connected/1, component_disconnected/2, + component_send_packet/1, + roster_access/2, process_message/1, + process_presence_out/1, process_presence_in/1]). + +-include("logger.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). +-include("translate.hrl"). + +-type roster_permission() :: both | get | set. +-type iq_permission() :: both | get | set. +-type presence_permission() :: managed_entity | roster. +-type message_permission() :: outgoing. +-type roster_permissions() :: [{roster_permission(), acl:acl()}]. +-type iq_permissions() :: [{iq_permission(), acl:acl()}]. +-type presence_permissions() :: [{presence_permission(), acl:acl()}]. +-type message_permissions() :: [{message_permission(), acl:acl()}]. +-type access() :: [{roster, roster_permission()} | + {iq, [privilege_namespace()]} | + {presence, presence_permission()} | + {message, message_permission()}]. +-type permissions() :: #{binary() => access()}. +-record(state, {server_host = <<"">> :: binary()}). + +%%%=================================================================== +%%% API +%%%=================================================================== +start(Host, Opts) -> + gen_mod:start_child(?MODULE, Host, Opts). + +stop(Host) -> + gen_mod:stop_child(?MODULE, Host). + +reload(_Host, _NewOpts, _OldOpts) -> + ok. + +mod_opt_type(roster) -> + econf:options( + #{both => econf:acl(), get => econf:acl(), set => econf:acl()}); +mod_opt_type(iq) -> + econf:map( + econf:binary(), + econf:options(#{both => econf:acl(), get => econf:acl(), set => econf:acl()})); +mod_opt_type(message) -> + econf:options( + #{outgoing => econf:acl()}); +mod_opt_type(presence) -> + econf:options( + #{managed_entity => econf:acl(), roster => econf:acl()}). + +mod_options(_) -> + [{roster, [{both, none}, {get, none}, {set, none}]}, + {iq, []}, + {presence, [{managed_entity, none}, {roster, none}]}, + {message, [{outgoing,none}]}]. + +mod_doc() -> + #{desc => + [?T("This module is an implementation of " + "https://xmpp.org/extensions/xep-0356.html" + "[XEP-0356: Privileged Entity]. This extension " + "allows components to have privileged access to " + "other entity data (send messages on behalf of the " + "server or on behalf of a user, get/set user roster, " + "access presence information, etc.). This may be used " + "to write powerful external components, for example " + "implementing an external " + "https://xmpp.org/extensions/xep-0163.html[PEP] or " + "https://xmpp.org/extensions/xep-0313.html[MAM] service."), "", + ?T("By default a component does not have any privileged access. " + "It is worth noting that the permissions grant access to " + "the component to a specific data type for all users of " + "the virtual host on which 'mod_privilege' is loaded."), "", + ?T("Make sure you have a listener configured to connect your " + "component. Check the section about listening ports for more " + "information."), "", + ?T("WARNING: Security issue: Privileged access gives components " + "access to sensitive data, so permission should be granted " + "carefully, only if you trust a component."), "", + ?T("NOTE: This module is complementary to _`mod_delegation`_, " + "but can also be used separately.")], + note => "improved in 24.10", + opts => + [{roster, + #{value => ?T("Options"), + desc => + ?T("This option defines roster permissions. " + "By default no permissions are given. " + "The 'Options' are:")}, + [{both, + #{value => ?T("AccessName"), + desc => + ?T("Sets read/write access to a user's roster. " + "The default value is 'none'.")}}, + {get, + #{value => ?T("AccessName"), + desc => + ?T("Sets read access to a user's roster. " + "The default value is 'none'.")}}, + {set, + #{value => ?T("AccessName"), + desc => + ?T("Sets write access to a user's roster. " + "The default value is 'none'.")}}]}, + {iq, + #{value => "{Namespace: Options}", + desc => + ?T("This option defines namespaces and their IQ permissions. " + "By default no permissions are given. " + "The 'Options' are:")}, + [{both, + #{value => ?T("AccessName"), + desc => + ?T("Allows sending IQ stanzas of type 'get' and 'set'. " + "The default value is 'none'.")}}, + {get, + #{value => ?T("AccessName"), + desc => + ?T("Allows sending IQ stanzas of type 'get'. " + "The default value is 'none'.")}}, + {set, + #{value => ?T("AccessName"), + desc => + ?T("Allows sending IQ stanzas of type 'set'. " + "The default value is 'none'.")}}]}, + {message, + #{value => ?T("Options"), + desc => + ?T("This option defines permissions for messages. " + "By default no permissions are given. " + "The 'Options' are:")}, + [{outgoing, + #{value => ?T("AccessName"), + desc => + ?T("The option defines an access rule for sending " + "outgoing messages by the component. " + "The default value is 'none'.")}}]}, + {presence, + #{value => ?T("Options"), + desc => + ?T("This option defines permissions for presences. " + "By default no permissions are given. " + "The 'Options' are:")}, + [{managed_entity, + #{value => ?T("AccessName"), + desc => + ?T("An access rule that gives permissions to " + "the component to receive server presences. " + "The default value is 'none'.")}}, + {roster, + #{value => ?T("AccessName"), + desc => + ?T("An access rule that gives permissions to " + "the component to receive the presence of both " + "the users and the contacts in their roster. " + "The default value is 'none'.")}}]}], + example => + ["modules:", + " mod_privilege:", + " iq:", + " http://jabber.org/protocol/pubsub:", + " get: all", + " roster:", + " get: all", + " presence:", + " managed_entity: all", + " message:", + " outgoing: all"]}. + +depends(_, _) -> + []. + +-spec component_connected(binary()) -> ok. +component_connected(Host) -> + lists:foreach( + fun(ServerHost) -> + Proc = gen_mod:get_module_proc(ServerHost, ?MODULE), + gen_server:cast(Proc, {component_connected, Host}) + end, ejabberd_option:hosts()). + +-spec component_disconnected(binary(), binary()) -> ok. +component_disconnected(Host, _Reason) -> + lists:foreach( + fun(ServerHost) -> + Proc = gen_mod:get_module_proc(ServerHost, ?MODULE), + gen_server:cast(Proc, {component_disconnected, Host}) + end, ejabberd_option:hosts()). + +%% +%% Message processing +%% + +-spec process_message(stanza()) -> stop | ok. +process_message(#message{from = #jid{luser = <<"">>, lresource = <<"">>} = From, + to = #jid{lresource = <<"">>} = To, + lang = Lang, type = T} = Msg) when T /= error -> + Host = From#jid.lserver, + ServerHost = To#jid.lserver, + Permissions = get_permissions(ServerHost), + case maps:find(Host, Permissions) of + {ok, Access} -> + case proplists:get_value(message, Access, none) of + outgoing -> + forward_message(Msg); + _ -> + Txt = ?T("Insufficient privilege"), + Err = xmpp:err_forbidden(Txt, Lang), + ejabberd_router:route_error(Msg, Err) + end, + stop; + error -> + %% Component is disconnected + ok + end; + +process_message(_Stanza) -> + ok. + +%% +%% IQ processing +%% + +%% @format-begin + +component_send_packet({#iq{from = From, + to = #jid{lresource = <<"">>} = To, + id = Id, + type = Type} = + IQ, + State}) + when Type /= error -> + Host = From#jid.lserver, + ServerHost = To#jid.lserver, + Permissions = get_permissions(ServerHost), + Result = + case {maps:find(Host, Permissions), get_iq_encapsulated_details(IQ)} of + {{ok, Access}, {privileged_iq, EncapType, EncapNs, EncapFrom, EncIq}} + when (EncapType == Type) and ((EncapFrom == undefined) or (EncapFrom == To)) -> + NsPermissions = proplists:get_value(iq, Access, []), + Permission = + case lists:keyfind(EncapNs, 2, NsPermissions) of + #privilege_namespace{type = AllowedType} -> + AllowedType; + _ -> + none + end, + case Permission == both + orelse Permission == get andalso Type == get + orelse Permission == set andalso Type == set + of + true -> + forward_iq(Host, To, Id, EncIq); + false -> + ?INFO_MSG("IQ not forwarded: Permission not granted to ns=~s with type=~p", + [EncapNs, Type]), + drop + end; + {error, _} -> + %% Component is disconnected + ?INFO_MSG("IQ not forwarded: Component seems disconnected", []), + drop; + {_, {privileged_iq, E, _, _, _}} when E /= Type -> + ?INFO_MSG("IQ not forwarded: The encapsulated IQ stanza type=~p " + "does not match the top-level IQ stanza type=~p", + [E, Type]), + drop; + {_, {privileged_iq, _, _, EF, _}} when (EF /= undefined) and (EF /= To) -> + ?INFO_MSG("IQ not forwarded: The FROM attribute in the encapsulated " + "IQ stanza and the TO in top-level IQ stanza do not match", + []), + drop; + {_, {unprivileged_iq}} -> + ?DEBUG("Component ~ts sent a not-wrapped IQ stanza, routing it as-is.", + [From#jid.lserver]), + IQ; + {_, {error, ErrType, _Err}} -> + ?INFO_MSG("IQ not forwarded: Component tried to send not valid IQ stanza: ~p.", + [ErrType]), + drop + end, + {Result, State}; +component_send_packet(Acc) -> + Acc. +%% @format-end + +%% +%% Roster processing +%% + +-spec roster_access({true, iq()} | false, iq()) -> {true, iq()} | false. +roster_access({true, _IQ} = Acc, _) -> + Acc; +roster_access(false, #iq{from = From, to = To, type = Type} = IQ) -> + Host = From#jid.lserver, + ServerHost = To#jid.lserver, + Permissions = get_permissions(ServerHost), + case maps:find(Host, Permissions) of + {ok, Access} -> + Permission = proplists:get_value(roster, Access, none), + case (Permission == both) + orelse (Permission == get andalso Type == get) + orelse (Permission == set andalso Type == set) of + true -> + {true, xmpp:put_meta(IQ, privilege_from, To)}; + false -> + false + end; + error -> + %% Component is disconnected + false + end. + +-spec process_presence_out({stanza(), ejabberd_c2s:state()}) -> {stanza(), ejabberd_c2s:state()}. +process_presence_out({#presence{ + from = #jid{luser = LUser, lserver = LServer} = From, + to = #jid{luser = LUser, lserver = LServer, lresource = <<"">>}, + type = Type} = Pres, C2SState}) + when Type == available; Type == unavailable -> + %% Self-presence processing + Permissions = get_permissions(LServer), + lists:foreach( + fun({Host, Access}) -> + Permission = proplists:get_value(presence, Access, none), + if Permission == roster; Permission == managed_entity -> + To = jid:make(Host), + ejabberd_router:route( + xmpp:set_from_to(Pres, From, To)); + true -> + ok + end + end, maps:to_list(Permissions)), + {Pres, C2SState}; +process_presence_out(Acc) -> + Acc. + +-spec process_presence_in({stanza(), ejabberd_c2s:state()}) -> {stanza(), ejabberd_c2s:state()}. +process_presence_in({#presence{ + from = #jid{luser = U, lserver = S} = From, + to = #jid{luser = LUser, lserver = LServer}, + type = Type} = Pres, C2SState}) + when {U, S} /= {LUser, LServer} andalso + (Type == available orelse Type == unavailable) -> + Permissions = get_permissions(LServer), + lists:foreach( + fun({Host, Access}) -> + case proplists:get_value(presence, Access, none) of + roster -> + Permission = proplists:get_value(roster, Access, none), + if Permission == both; Permission == get -> + To = jid:make(Host), + ejabberd_router:route( + xmpp:set_from_to(Pres, From, To)); + true -> + ok + end; + _ -> + ok + end + end, maps:to_list(Permissions)), + {Pres, C2SState}; +process_presence_in(Acc) -> + Acc. + +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== +init([Host|_]) -> + process_flag(trap_exit, true), + catch ets:new(?MODULE, + [named_table, public, + {heir, erlang:group_leader(), none}]), + ejabberd_hooks:add(component_connected, ?MODULE, + component_connected, 50), + ejabberd_hooks:add(component_disconnected, ?MODULE, + component_disconnected, 50), + ejabberd_hooks:add(local_send_to_resource_hook, Host, ?MODULE, + process_message, 50), + ejabberd_hooks:add(roster_remote_access, Host, ?MODULE, + roster_access, 50), + ejabberd_hooks:add(user_send_packet, Host, ?MODULE, + process_presence_out, 50), + ejabberd_hooks:add(user_receive_packet, Host, ?MODULE, + process_presence_in, 50), + ejabberd_hooks:add(component_send_packet, ?MODULE, + component_send_packet, 50), + {ok, #state{server_host = Host}}. + +handle_call(Request, From, State) -> + ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), + {noreply, State}. + +handle_cast({component_connected, Host}, State) -> + ServerHost = State#state.server_host, + From = jid:make(ServerHost), + To = jid:make(Host), + RosterPerm = get_roster_permission(ServerHost, Host), + IqNamespaces = get_iq_namespaces(ServerHost, Host), + PresencePerm = get_presence_permission(ServerHost, Host), + MessagePerm = get_message_permission(ServerHost, Host), + if RosterPerm /= none; IqNamespaces /= []; PresencePerm /= none; MessagePerm /= none -> + Priv = #privilege{perms = [#privilege_perm{access = message, + type = MessagePerm}, + #privilege_perm{access = roster, + type = RosterPerm}, + #privilege_perm{access = iq, + namespaces = IqNamespaces}, + #privilege_perm{access = presence, + type = PresencePerm}]}, + ?INFO_MSG("Granting permissions to external " + "component '~ts': roster = ~ts, presence = ~ts, " + "message = ~ts,~n iq = ~p", + [Host, RosterPerm, PresencePerm, MessagePerm, IqNamespaces]), + Msg = #message{from = From, to = To, sub_els = [Priv]}, + ejabberd_router:route(Msg), + Permissions = maps:put(Host, [{roster, RosterPerm}, + {iq, IqNamespaces}, + {presence, PresencePerm}, + {message, MessagePerm}], + get_permissions(ServerHost)), + ets:insert(?MODULE, {ServerHost, Permissions}), + {noreply, State}; + true -> + ?INFO_MSG("Granting no permissions to external component '~ts'", + [Host]), + {noreply, State} + end; +handle_cast({component_disconnected, Host}, State) -> + ServerHost = State#state.server_host, + Permissions = maps:remove(Host, get_permissions(ServerHost)), + case maps:size(Permissions) of + 0 -> ets:delete(?MODULE, ServerHost); + _ -> ets:insert(?MODULE, {ServerHost, Permissions}) + end, + {noreply, State}; +handle_cast(Msg, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Msg]), + {noreply, State}. + +handle_info(Info, State) -> + ?WARNING_MSG("Unexpected info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, State) -> + Host = State#state.server_host, + case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of + false -> + ejabberd_hooks:delete(component_send_packet, ?MODULE, + component_send_packet, 50), + ejabberd_hooks:delete(component_connected, ?MODULE, + component_connected, 50), + ejabberd_hooks:delete(component_disconnected, ?MODULE, + component_disconnected, 50); + true -> + ok + end, + ejabberd_hooks:delete(local_send_to_resource_hook, Host, ?MODULE, + process_message, 50), + ejabberd_hooks:delete(roster_remote_access, Host, ?MODULE, + roster_access, 50), + ejabberd_hooks:delete(user_send_packet, Host, ?MODULE, + process_presence_out, 50), + ejabberd_hooks:delete(user_receive_packet, Host, ?MODULE, + process_presence_in, 50), + ets:delete(?MODULE, Host). + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +-spec get_permissions(binary()) -> permissions(). +get_permissions(ServerHost) -> + case ets:lookup(?MODULE, ServerHost) of + [] -> #{}; + [{_, Permissions}] -> Permissions + end. + +%% +%% Message +%% + +-spec forward_message(message()) -> ok. +forward_message(#message{to = To} = Msg) -> + ServerHost = To#jid.lserver, + Lang = xmpp:get_lang(Msg), + CodecOpts = ejabberd_config:codec_options(), + try xmpp:try_subtag(Msg, #privilege{}) of + #privilege{forwarded = #forwarded{sub_els = [SubEl]}} -> + try xmpp:decode(SubEl, ?NS_CLIENT, CodecOpts) of + #message{} = NewMsg -> + case NewMsg#message.from of + #jid{lresource = <<"">>, lserver = ServerHost} -> + FromJID = NewMsg#message.from, + State = #{jid => FromJID}, + ejabberd_hooks:run_fold(user_send_packet, FromJID#jid.lserver, {NewMsg, State}, []), + ejabberd_router:route(NewMsg); + _ -> + Lang = xmpp:get_lang(Msg), + Txt = ?T("Invalid 'from' attribute in forwarded message"), + Err = xmpp:err_forbidden(Txt, Lang), + ejabberd_router:route_error(Msg, Err) + end; + _ -> + Txt = ?T("Message not found in forwarded payload"), + Err = xmpp:err_bad_request(Txt, Lang), + ejabberd_router:route_error(Msg, Err) + catch _:{xmpp_codec, Why} -> + Txt = xmpp:io_format_error(Why), + Err = xmpp:err_bad_request(Txt, Lang), + ejabberd_router:route_error(Msg, Err) + end; + _ -> + Txt = ?T("No element found"), + Err = xmpp:err_bad_request(Txt, Lang), + ejabberd_router:route_error(Msg, Err) + catch _:{xmpp_codec, Why} -> + Txt = xmpp:io_format_error(Why), + Err = xmpp:err_bad_request(Txt, Lang), + ejabberd_router:route_error(Msg, Err) + end. + +%% +%% IQ +%% + +%% @format-begin + +-spec get_iq_encapsulated_details(iq()) -> + {privileged_iq, iq_type(), binary(), jid(), iq()} | + {unprivileged_iq} | + {error, Why :: atom(), stanza_error()}. +get_iq_encapsulated_details(#iq{sub_els = [IqSub]} = Msg) -> + Lang = xmpp:get_lang(Msg), + try xmpp:try_subtag(Msg, #privileged_iq{}) of + #privileged_iq{iq = #iq{type = EncapsulatedType, from = From} = EncIq} -> + [IqSubSub] = xmpp:get_els(IqSub), + [Element] = xmpp:get_els(IqSubSub), + Ns = xmpp:get_ns(Element), + {privileged_iq, EncapsulatedType, Ns, From, EncIq}; + _ -> + {unprivileged_iq} + catch + _:{xmpp_codec, Why} -> + Txt = xmpp:io_format_error(Why), + Err = xmpp:err_bad_request(Txt, Lang), + {error, codec_error, Err} + end. + +-spec forward_iq(binary(), jid(), binary(), iq()) -> iq(). +forward_iq(Host, ToplevelTo, Id, Iq) -> + FromJID = ToplevelTo, + NewIq0 = Iq#iq{from = FromJID}, + xmpp:put_meta(NewIq0, privilege_iq, {Id, Host, FromJID}). +%% @format-end + +%% +%% Permissions +%% + +-spec get_roster_permission(binary(), binary()) -> roster_permission() | none. +get_roster_permission(ServerHost, Host) -> + Perms = mod_privilege_opt:roster(ServerHost), + case match_rule(ServerHost, Host, Perms, both) of + allow -> + both; + deny -> + Get = match_rule(ServerHost, Host, Perms, get), + Set = match_rule(ServerHost, Host, Perms, set), + if Get == allow, Set == allow -> both; + Get == allow -> get; + Set == allow -> set; + true -> none + end + end. + +-spec get_iq_namespaces(binary(), binary()) -> [privilege_namespace()]. +get_iq_namespaces(ServerHost, Host) -> + NsPerms = mod_privilege_opt:iq(ServerHost), + [#privilege_namespace{ns = Ns, type = get_iq_permission(ServerHost, Host, Perms)} || {Ns, Perms} <- NsPerms]. + +-spec get_iq_permission(binary(), binary(), [iq_permission()]) -> iq_permission() | none. +get_iq_permission(ServerHost, Host, Perms) -> + case match_rule(ServerHost, Host, Perms, both) of + allow -> + both; + deny -> + Get = match_rule(ServerHost, Host, Perms, get), + Set = match_rule(ServerHost, Host, Perms, set), + if Get == allow, Set == allow -> both; + Get == allow -> get; + Set == allow -> set; + true -> none + end + end. + +-spec get_message_permission(binary(), binary()) -> message_permission() | none. +get_message_permission(ServerHost, Host) -> + Perms = mod_privilege_opt:message(ServerHost), + case match_rule(ServerHost, Host, Perms, outgoing) of + allow -> outgoing; + deny -> none + end. + +-spec get_presence_permission(binary(), binary()) -> presence_permission() | none. +get_presence_permission(ServerHost, Host) -> + Perms = mod_privilege_opt:presence(ServerHost), + case match_rule(ServerHost, Host, Perms, roster) of + allow -> + roster; + deny -> + case match_rule(ServerHost, Host, Perms, managed_entity) of + allow -> managed_entity; + deny -> none + end + end. + +-ifdef(OTP_BELOW_26). +-dialyzer({no_contracts, match_rule/4}). +-endif. + +-spec match_rule(binary(), binary(), roster_permissions(), roster_permission()) -> allow | deny; + (binary(), binary(), iq_permissions(), iq_permission()) -> allow | deny; + (binary(), binary(), presence_permissions(), presence_permission()) -> allow | deny; + (binary(), binary(), message_permissions(), message_permission()) -> allow | deny. +match_rule(ServerHost, Host, Perms, Type) -> + Access = proplists:get_value(Type, Perms, none), + acl:match_rule(ServerHost, Access, jid:make(Host)). diff --git a/src/mod_privilege_opt.erl b/src/mod_privilege_opt.erl new file mode 100644 index 000000000..36bf54efa --- /dev/null +++ b/src/mod_privilege_opt.erl @@ -0,0 +1,34 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_privilege_opt). + +-export([iq/1]). +-export([message/1]). +-export([presence/1]). +-export([roster/1]). + +-spec iq(gen_mod:opts() | global | binary()) -> [{binary(),[{'both',acl:acl()} | {'get',acl:acl()} | {'set',acl:acl()}]}]. +iq(Opts) when is_map(Opts) -> + gen_mod:get_opt(iq, Opts); +iq(Host) -> + gen_mod:get_module_opt(Host, mod_privilege, iq). + +-spec message(gen_mod:opts() | global | binary()) -> [{'outgoing','none' | acl:acl()}]. +message(Opts) when is_map(Opts) -> + gen_mod:get_opt(message, Opts); +message(Host) -> + gen_mod:get_module_opt(Host, mod_privilege, message). + +-spec presence(gen_mod:opts() | global | binary()) -> [{'managed_entity','none' | acl:acl()} | {'roster','none' | acl:acl()}]. +presence(Opts) when is_map(Opts) -> + gen_mod:get_opt(presence, Opts); +presence(Host) -> + gen_mod:get_module_opt(Host, mod_privilege, presence). + +-spec roster(gen_mod:opts() | global | binary()) -> [{'both','none' | acl:acl()} | {'get','none' | acl:acl()} | {'set','none' | acl:acl()}]. +roster(Opts) when is_map(Opts) -> + gen_mod:get_opt(roster, Opts); +roster(Host) -> + gen_mod:get_module_opt(Host, mod_privilege, roster). + diff --git a/src/mod_providers.erl b/src/mod_providers.erl new file mode 100644 index 000000000..24796b65d --- /dev/null +++ b/src/mod_providers.erl @@ -0,0 +1,462 @@ +%%%------------------------------------------------------------------- +%%% File : mod_providers.erl +%%% Author : Badlop +%%% Purpose : Serve xmpp-provider-v2.json files as described by XMPP Providers +%%% Created : 7 August 2025 by Badlop +%%% +%%% +%%% ejabberd, Copyright (C) 2025 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. +%%% +%%%---------------------------------------------------------------------- + +%%-------------------------------------------------------------------- +%%| Definitions + +%% This module is based in mod_host_meta.erl + +%% @format-begin + +-module(mod_providers). + +-author('badlop@process-one.net'). + +-behaviour(gen_mod). + +-export([start/2, stop/1, reload/3, process/2, mod_opt_type/1, mod_options/1, depends/2]). +-export([mod_doc/0]). + +-include("logger.hrl"). + +-include_lib("xmpp/include/xmpp.hrl"). + +-include("ejabberd_http.hrl"). +-include("ejabberd_web_admin.hrl"). +-include("translate.hrl"). + +%%-------------------------------------------------------------------- +%%| gen_mod callbacks + +start(_Host, _Opts) -> + report_hostmeta_listener(), + ok. + +stop(_Host) -> + ok. + +reload(_Host, _NewOpts, _OldOpts) -> + report_hostmeta_listener(), + ok. + +depends(_Host, _Opts) -> + []. + +%%-------------------------------------------------------------------- +%%| HTTP handlers + +process([], + #request{method = 'GET', + host = Host, + path = Path}) -> + case lists:last(Path) of + <<"xmpp-provider-v2.json">> -> + file_json(Host) + end; +process(_Path, _Request) -> + {404, [], "Not Found"}. + +%%-------------------------------------------------------------------- +%%| JSON + +file_json(Host) -> + Content = + #{website => build_urls(Host, website), + alternativeJids => gen_mod:get_module_opt(Host, ?MODULE, alternativeJids), + busFactor => gen_mod:get_module_opt(Host, ?MODULE, busFactor), + organization => gen_mod:get_module_opt(Host, ?MODULE, organization), + passwordReset => get_password_url(Host), + serverTesting => gen_mod:get_module_opt(Host, ?MODULE, serverTesting), + maximumHttpFileUploadTotalSize => get_upload_size(Host), + maximumHttpFileUploadStorageTime => get_upload_time(Host), + maximumMessageArchiveManagementStorageTime => + gen_mod:get_module_opt(Host, ?MODULE, maximumMessageArchiveManagementStorageTime), + professionalHosting => gen_mod:get_module_opt(Host, ?MODULE, professionalHosting), + freeOfCharge => gen_mod:get_module_opt(Host, ?MODULE, freeOfCharge), + legalNotice => build_urls(Host, legalNotice), + serverLocations => gen_mod:get_module_opt(Host, ?MODULE, serverLocations), + since => gen_mod:get_module_opt(Host, ?MODULE, since)}, + {200, + [html, + {<<"Content-Type">>, <<"application/json">>}, + {<<"Access-Control-Allow-Origin">>, <<"*">>}], + [misc:json_encode(Content)]}. + +%%-------------------------------------------------------------------- +%%| Upload Size + +get_upload_size(Host) -> + case gen_mod:get_module_opt(Host, ?MODULE, maximumHttpFileUploadTotalSize) of + default_value -> + get_upload_size_mhuq(Host); + I when is_integer(I) -> + I + end. + +get_upload_size_mhuq(Host) -> + case gen_mod:is_loaded(Host, mod_http_upload_quota) of + true -> + Access = gen_mod:get_module_opt(Host, mod_http_upload_quota, access_hard_quota), + Rules = ejabberd_shaper:read_shaper_rules(Access, Host), + get_upload_size_rules(Rules); + false -> + 0 + end. + +get_upload_size_rules(Rules) -> + case lists:keysearch([{acl, all}], 2, Rules) of + {value, {Size, _}} -> + Size; + false -> + 0 + end. + +%%-------------------------------------------------------------------- +%%| Upload Time + +get_upload_time(Host) -> + case gen_mod:get_module_opt(Host, ?MODULE, maximumHttpFileUploadStorageTime) of + default_value -> + get_upload_time_mhuq(Host); + I when is_integer(I) -> + I + end. + +get_upload_time_mhuq(Host) -> + case gen_mod:is_loaded(Host, mod_http_upload_quota) of + true -> + case gen_mod:get_module_opt(Host, mod_http_upload_quota, max_days) of + infinity -> + 0; + I when is_integer(I) -> + I + end; + false -> + 0 + end. + +%%-------------------------------------------------------------------- +%%| Password URL + +get_password_url(Host) -> + build_urls(Host, get_password_url2(Host)). + +get_password_url2(Host) -> + case gen_mod:get_module_opt(Host, ?MODULE, passwordReset) of + default_value -> + get_password_url3(Host); + U when is_binary(U) -> + U + end. + +get_password_url3(Host) -> + case find_handler_port_path2(any, mod_register_web) of + [] -> + <<"">>; + [{ThisTls, Port, Path} | _] -> + Protocol = + case ThisTls of + false -> + <<"http">>; + true -> + <<"https">> + end, + <>))/binary, + "/">> + end. + +%% TODO Ya hay otra funciona como esta +find_handler_port_path2(Tls, Module) -> + lists:filtermap(fun ({{Port, _, _}, + ejabberd_http, + #{tls := ThisTls, request_handlers := Handlers}}) + when (Tls == any) or (Tls == ThisTls) -> + case lists:keyfind(Module, 2, Handlers) of + false -> + false; + {Path, Module} -> + {true, {ThisTls, Port, Path}} + end; + (_) -> + false + end, + ets:tab2list(ejabberd_listener)). + +%%-------------------------------------------------------------------- +%%| Build URLs + +build_urls(Host, Option) when is_atom(Option) -> + build_urls(Host, gen_mod:get_module_opt(Host, ?MODULE, Option)); +build_urls(_Host, <<"">>) -> + #{}; +build_urls(Host, Url) when not is_atom(Url) -> + Languages = gen_mod:get_module_opt(Host, ?MODULE, languages), + maps:from_list([{L, misc:expand_keyword(<<"@LANGUAGE_URL@">>, Url, L)} + || L <- Languages]). + +find_handler_port_path(Tls, Module) -> + lists:filtermap(fun ({{Port, _, _}, + ejabberd_http, + #{tls := ThisTls, request_handlers := Handlers}}) + when is_integer(Port) and ((Tls == any) or (Tls == ThisTls)) -> + case lists:keyfind(Module, 2, Handlers) of + false -> + false; + {Path, Module} -> + {true, {ThisTls, Port, Path}} + end; + (_) -> + false + end, + ets:tab2list(ejabberd_listener)). + +report_hostmeta_listener() -> + case {find_handler_port_path(false, ?MODULE), find_handler_port_path(true, ?MODULE)} of + {[], []} -> + ?CRITICAL_MSG("It seems you enabled ~p in 'modules' but forgot to " + "add it as a request_handler in an ejabberd_http " + "listener.", + [?MODULE]); + {[_ | _], _} -> + ?WARNING_MSG("Apparently ~p is enabled in a request_handler in a " + "non-encrypted ejabberd_http listener. If this is " + "not desired, enable 'tls' in that " + "listener, or setup a proxy encryption mechanism.", + [?MODULE]); + {[], [_ | _]} -> + ok + end. + +%%-------------------------------------------------------------------- +%%| Options + +mod_opt_type(languages) -> + econf:list( + econf:binary()); +mod_opt_type(website) -> + econf:binary(); +mod_opt_type(alternativeJids) -> + econf:list( + econf:domain(), [unique]); +mod_opt_type(busFactor) -> + econf:int(); +mod_opt_type(organization) -> + econf:enum([company, + 'commercial person', + 'private person', + governmental, + 'non-governmental']); +mod_opt_type(passwordReset) -> + econf:binary(); +mod_opt_type(serverTesting) -> + econf:bool(); +mod_opt_type(maximumHttpFileUploadTotalSize) -> + econf:int(); +mod_opt_type(maximumHttpFileUploadStorageTime) -> + econf:int(); +mod_opt_type(maximumMessageArchiveManagementStorageTime) -> + econf:int(); +mod_opt_type(professionalHosting) -> + econf:bool(); +mod_opt_type(freeOfCharge) -> + econf:bool(); +mod_opt_type(legalNotice) -> + econf:binary(); +mod_opt_type(serverLocations) -> + econf:list( + econf:binary()); +mod_opt_type(since) -> + econf:binary(). + +mod_options(Host) -> + [{languages, [ejabberd_option:language(Host)]}, + {website, <<"">>}, + {alternativeJids, []}, + {busFactor, -1}, + {organization, ''}, + {passwordReset, default_value}, + {serverTesting, false}, + {maximumHttpFileUploadTotalSize, default_value}, + {maximumHttpFileUploadStorageTime, default_value}, + {maximumMessageArchiveManagementStorageTime, 0}, + {professionalHosting, false}, + {freeOfCharge, false}, + {legalNotice, <<"">>}, + {serverLocations, []}, + {since, <<"">>}]. + +%%-------------------------------------------------------------------- +%%| Doc + +mod_doc() -> + #{desc => + [?T("This module serves JSON provider files API v2 as described by " + "https://providers.xmpp.net/provider-file-generator/[XMPP Providers]."), + "", + ?T("It attempts to fill some properties gathering values automatically from your existing ejabberd configuration. Try enabling the module, check what values are displayed, and then customize using the options."), + "", + ?T("To use this module, in addition to adding it to the 'modules' " + "section, you must also enable it in 'listen' -> 'ejabberd_http' -> " + "_`listen-options.md#request_handlers|request_handlers`_. " + "Notice you should set in _`listen.md#ejabberd_http|ejabberd_http`_ " + "the option _`listen-options.md#tls|tls`_ enabled.")], + note => "added in 25.08", + opts => + [{languages, + #{value => "[string()]", + desc => + ?T("List of language codes that your pages are available. " + "Some options define URL where the keyword '@LANGUAGE_URL@' " + "will be replaced with each of those language codes. " + "The default value is a list with the language set in the " + "option _`language`_, for example: '[en]'.")}}, + {website, + #{value => "string()", + desc => + ?T("Provider website. " + "The keyword '@LANGUAGE_URL@' is replaced with each language. " + "The default value is '\"\"'.")}}, + {alternativeJids, + #{value => "[string()]", + desc => + ?T("List of JIDs (XMPP server domains) a provider offers for " + "registration other than its main JID. " + "The default value is '[]'.")}}, + {busFactor, + #{value => "integer()", + desc => + ?T("Bus factor of the XMPP service (i.e., the minimum number of " + "team members that the service could not survive losing) or '-1' for n/a. " + "The default value is '-1'.")}}, + {organization, + #{value => "string()", + desc => + ?T("Type of organization providing the XMPP service. " + "Allowed values are: 'company', '\"commercial person\"', '\"private person\"', " + "'governmental', '\"non-governmental\"' or '\"\"'. " + "The default value is '\"\"'.")}}, + {passwordReset, + #{value => "string()", + desc => + ?T("Password reset web page (per language) used for an automatic password reset " + "(e.g., via email) or describing how to manually reset a password " + "(e.g., by contacting the provider). " + "The keyword '@LANGUAGE_URL@' is replaced with each language. " + "The default value is an URL built automatically " + "if _`mod_register_web`_ is configured as a 'request_handler', " + "or '\"\"' otherwise.")}}, + {serverTesting, + #{value => "true | false", + desc => + ?T("Whether tests against the provider's server are allowed " + "(e.g., certificate checks and uptime monitoring). " + "The default value is 'false'.")}}, + {maximumHttpFileUploadTotalSize, + #{value => "integer()", + desc => + ?T("Maximum size of all shared files in total per user (number in megabytes (MB), " + "'0' for no limit or '-1' for less than 1 MB). " + "Attention: MB is used instead of MiB (e.g., 104,857,600 bytes = 100 MiB ≈ 104 MB). " + "This property is not about the maximum size of each shared file, " + "which is already retrieved via XMPP. " + "The default value is the value of the shaper value " + "of option 'access_hard_quota' " + "from module _`mod_http_upload_quota`_, or '0' otherwise.")}}, + {maximumHttpFileUploadStorageTime, + #{value => "integer()", + desc => + ?T("Maximum storage duration of each shared file " + "(number in days, '0' for no limit or '-1' for less than 1 day). " + "The default value is the same as option 'max_days' " + "from module _`mod_http_upload_quota`_, or '0' otherwise.")}}, + {maximumMessageArchiveManagementStorageTime, + #{value => "integer()", + desc => + ?T("Maximum storage duration of each exchanged message " + "(number in days, '0' for no limit or '-1' for less than 1 day). " + "The default value is '0'.")}}, + {professionalHosting, + #{value => "true | false", + desc => + ?T("Whether the XMPP server is hosted with good internet connection speed, " + "uninterruptible power supply, access protection and regular backups. " + "The default value is 'false'.")}}, + {freeOfCharge, + #{value => "true | false", + desc => + ?T("Whether the XMPP service can be used for free. " + "The default value is 'false'.")}}, + {legalNotice, + #{value => "string()", + desc => + ?T("Legal notice web page (per language). " + "The keyword '@LANGUAGE_URL@' is replaced with each language. " + "The default value is '\"\"'.")}}, + {serverLocations, + #{value => "[string()]", + desc => + ?T("List of language codes of Server/Backup locations. " + "The default value is an empty list: '[]'.")}}, + {since, + #{value => "string()", + desc => + ?T("Date since the XMPP service is available. " + "The default value is an empty string: '\"\"'.")}}], + example => + ["listen:", + " -", + " port: 443", + " module: ejabberd_http", + " tls: true", + " request_handlers:", + " /.well-known/xmpp-provider-v2.json: mod_providers", + "", + "modules:", + " mod_providers:", + " alternativeJids: [\"example1.com\", \"example2.com\"]", + " busFactor: 1", + " freeOfCharge: true", + " languages: [ag, ao, bg, en]", + " legalNotice: \"http://@HOST@/legal/@LANGUAGE_URL@/\"", + " maximumHttpFileUploadStorageTime: 0", + " maximumHttpFileUploadTotalSize: 0", + " maximumMessageArchiveManagementStorageTime: 0", + " organization: \"non-governmental\"", + " passwordReset: \"http://@HOST@/reset/@LANGUAGE_URL@/\"", + " professionalHosting: true", + " serverLocations: [ao, bg]", + " serverTesting: true", + " since: \"2025-12-31\"", + " website: \"http://@HOST@/website/@LANGUAGE_URL@/\""]}. + +%%-------------------------------------------------------------------- + +%%| vim: set foldmethod=marker foldmarker=%%|,%%-: diff --git a/src/mod_providers_opt.erl b/src/mod_providers_opt.erl new file mode 100644 index 000000000..ad398295f --- /dev/null +++ b/src/mod_providers_opt.erl @@ -0,0 +1,111 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_providers_opt). + +-export([alternativeJids/1]). +-export([busFactor/1]). +-export([freeOfCharge/1]). +-export([languages/1]). +-export([legalNotice/1]). +-export([maximumHttpFileUploadStorageTime/1]). +-export([maximumHttpFileUploadTotalSize/1]). +-export([maximumMessageArchiveManagementStorageTime/1]). +-export([organization/1]). +-export([passwordReset/1]). +-export([professionalHosting/1]). +-export([serverLocations/1]). +-export([serverTesting/1]). +-export([since/1]). +-export([website/1]). + +-spec alternativeJids(gen_mod:opts() | global | binary()) -> [binary()]. +alternativeJids(Opts) when is_map(Opts) -> + gen_mod:get_opt(alternativeJids, Opts); +alternativeJids(Host) -> + gen_mod:get_module_opt(Host, mod_providers, alternativeJids). + +-spec busFactor(gen_mod:opts() | global | binary()) -> integer(). +busFactor(Opts) when is_map(Opts) -> + gen_mod:get_opt(busFactor, Opts); +busFactor(Host) -> + gen_mod:get_module_opt(Host, mod_providers, busFactor). + +-spec freeOfCharge(gen_mod:opts() | global | binary()) -> boolean(). +freeOfCharge(Opts) when is_map(Opts) -> + gen_mod:get_opt(freeOfCharge, Opts); +freeOfCharge(Host) -> + gen_mod:get_module_opt(Host, mod_providers, freeOfCharge). + +-spec languages(gen_mod:opts() | global | binary()) -> [binary()]. +languages(Opts) when is_map(Opts) -> + gen_mod:get_opt(languages, Opts); +languages(Host) -> + gen_mod:get_module_opt(Host, mod_providers, languages). + +-spec legalNotice(gen_mod:opts() | global | binary()) -> binary(). +legalNotice(Opts) when is_map(Opts) -> + gen_mod:get_opt(legalNotice, Opts); +legalNotice(Host) -> + gen_mod:get_module_opt(Host, mod_providers, legalNotice). + +-spec maximumHttpFileUploadStorageTime(gen_mod:opts() | global | binary()) -> 'default_value' | integer(). +maximumHttpFileUploadStorageTime(Opts) when is_map(Opts) -> + gen_mod:get_opt(maximumHttpFileUploadStorageTime, Opts); +maximumHttpFileUploadStorageTime(Host) -> + gen_mod:get_module_opt(Host, mod_providers, maximumHttpFileUploadStorageTime). + +-spec maximumHttpFileUploadTotalSize(gen_mod:opts() | global | binary()) -> 'default_value' | integer(). +maximumHttpFileUploadTotalSize(Opts) when is_map(Opts) -> + gen_mod:get_opt(maximumHttpFileUploadTotalSize, Opts); +maximumHttpFileUploadTotalSize(Host) -> + gen_mod:get_module_opt(Host, mod_providers, maximumHttpFileUploadTotalSize). + +-spec maximumMessageArchiveManagementStorageTime(gen_mod:opts() | global | binary()) -> integer(). +maximumMessageArchiveManagementStorageTime(Opts) when is_map(Opts) -> + gen_mod:get_opt(maximumMessageArchiveManagementStorageTime, Opts); +maximumMessageArchiveManagementStorageTime(Host) -> + gen_mod:get_module_opt(Host, mod_providers, maximumMessageArchiveManagementStorageTime). + +-spec organization(gen_mod:opts() | global | binary()) -> '' | 'commercial person' | 'company' | 'governmental' | 'non-governmental' | 'private person'. +organization(Opts) when is_map(Opts) -> + gen_mod:get_opt(organization, Opts); +organization(Host) -> + gen_mod:get_module_opt(Host, mod_providers, organization). + +-spec passwordReset(gen_mod:opts() | global | binary()) -> 'default_value' | binary(). +passwordReset(Opts) when is_map(Opts) -> + gen_mod:get_opt(passwordReset, Opts); +passwordReset(Host) -> + gen_mod:get_module_opt(Host, mod_providers, passwordReset). + +-spec professionalHosting(gen_mod:opts() | global | binary()) -> boolean(). +professionalHosting(Opts) when is_map(Opts) -> + gen_mod:get_opt(professionalHosting, Opts); +professionalHosting(Host) -> + gen_mod:get_module_opt(Host, mod_providers, professionalHosting). + +-spec serverLocations(gen_mod:opts() | global | binary()) -> [binary()]. +serverLocations(Opts) when is_map(Opts) -> + gen_mod:get_opt(serverLocations, Opts); +serverLocations(Host) -> + gen_mod:get_module_opt(Host, mod_providers, serverLocations). + +-spec serverTesting(gen_mod:opts() | global | binary()) -> boolean(). +serverTesting(Opts) when is_map(Opts) -> + gen_mod:get_opt(serverTesting, Opts); +serverTesting(Host) -> + gen_mod:get_module_opt(Host, mod_providers, serverTesting). + +-spec since(gen_mod:opts() | global | binary()) -> binary(). +since(Opts) when is_map(Opts) -> + gen_mod:get_opt(since, Opts); +since(Host) -> + gen_mod:get_module_opt(Host, mod_providers, since). + +-spec website(gen_mod:opts() | global | binary()) -> binary(). +website(Opts) when is_map(Opts) -> + gen_mod:get_opt(website, Opts); +website(Host) -> + gen_mod:get_module_opt(Host, mod_providers, website). + diff --git a/src/mod_proxy65.erl b/src/mod_proxy65.erl index 6eced10b8..4143defaa 100644 --- a/src/mod_proxy65.erl +++ b/src/mod_proxy65.erl @@ -5,7 +5,7 @@ %%% Created : 12 Oct 2006 by Evgeniy Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -27,58 +27,244 @@ -author('xram@jabber.ru'). +-protocol({xep, 65, '1.8', '2.0.0', "complete", ""}). + -behaviour(gen_mod). -behaviour(supervisor). %% gen_mod callbacks. --export([start/2, stop/1, transform_module_options/1]). +-export([start/2, stop/1, reload/3]). %% supervisor callbacks. -export([init/1]). -%% API. --export([start_link/2]). +-export([start_link/1, mod_opt_type/1, mod_options/1, depends/2, mod_doc/0]). -define(PROCNAME, ejabberd_mod_proxy65). +-include("translate.hrl"). + +-callback init() -> any(). +-callback register_stream(binary(), pid()) -> ok | {error, any()}. +-callback unregister_stream(binary()) -> ok | {error, any()}. +-callback activate_stream(binary(), binary(), pos_integer() | infinity, node()) -> + ok | {error, limit | conflict | notfound | term()}. + start(Host, Opts) -> case mod_proxy65_service:add_listener(Host, Opts) of - {error, _} = Err -> erlang:error(Err); - _ -> - Proc = gen_mod:get_module_proc(Host, ?PROCNAME), - ChildSpec = {Proc, {?MODULE, start_link, [Host, Opts]}, - transient, infinity, supervisor, [?MODULE]}, - supervisor:start_child(ejabberd_sup, ChildSpec) + {error, _} = Err -> + Err; + _ -> + Mod = gen_mod:ram_db_mod(global, ?MODULE), + Mod:init(), + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + ChildSpec = {Proc, {?MODULE, start_link, [Host]}, + transient, infinity, supervisor, [?MODULE]}, + supervisor:start_child(ejabberd_gen_mod_sup, ChildSpec) end. stop(Host) -> - mod_proxy65_service:delete_listener(Host), + case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of + false -> + mod_proxy65_service:delete_listener(Host); + true -> + ok + end, Proc = gen_mod:get_module_proc(Host, ?PROCNAME), - supervisor:terminate_child(ejabberd_sup, Proc), - supervisor:delete_child(ejabberd_sup, Proc). + supervisor:terminate_child(ejabberd_gen_mod_sup, Proc), + supervisor:delete_child(ejabberd_gen_mod_sup, Proc). -start_link(Host, Opts) -> +reload(Host, NewOpts, OldOpts) -> + Mod = gen_mod:ram_db_mod(global, ?MODULE), + Mod:init(), + mod_proxy65_service:reload(Host, NewOpts, OldOpts). + +start_link(Host) -> Proc = gen_mod:get_module_proc(Host, ?PROCNAME), - supervisor:start_link({local, Proc}, ?MODULE, - [Host, Opts]). + supervisor:start_link({local, Proc}, ?MODULE, [Host]). -transform_module_options(Opts) -> - mod_proxy65_service:transform_module_options(Opts). - -init([Host, Opts]) -> +init([Host]) -> Service = {mod_proxy65_service, - {mod_proxy65_service, start_link, [Host, Opts]}, + {mod_proxy65_service, start_link, [Host]}, transient, 5000, worker, [mod_proxy65_service]}, - StreamSupervisor = {ejabberd_mod_proxy65_sup, - {ejabberd_tmp_sup, start_link, - [gen_mod:get_module_proc(Host, - ejabberd_mod_proxy65_sup), - mod_proxy65_stream]}, - transient, infinity, supervisor, [ejabberd_tmp_sup]}, - StreamManager = {mod_proxy65_sm, - {mod_proxy65_sm, start_link, [Host, Opts]}, transient, - 5000, worker, [mod_proxy65_sm]}, - {ok, - {{one_for_one, 10, 1}, - [StreamManager, StreamSupervisor, Service]}}. + {ok, {{one_for_one, 10, 1}, [Service]}}. + +depends(_Host, _Opts) -> + []. + +mod_opt_type(access) -> + econf:acl(); +mod_opt_type(hostname) -> + econf:host(); +mod_opt_type(ip) -> + econf:ip(); +mod_opt_type(name) -> + econf:binary(); +mod_opt_type(port) -> + econf:port(); +mod_opt_type(max_connections) -> + econf:pos_int(infinity); +mod_opt_type(host) -> + econf:host(); +mod_opt_type(hosts) -> + econf:hosts(); +mod_opt_type(ram_db_type) -> + econf:db_type(?MODULE); +mod_opt_type(server_host) -> + econf:binary(); +mod_opt_type(auth_type) -> + econf:enum([plain, anonymous]); +mod_opt_type(recbuf) -> + econf:pos_int(); +mod_opt_type(shaper) -> + econf:shaper(); +mod_opt_type(sndbuf) -> + econf:pos_int(); +mod_opt_type(vcard) -> + econf:vcard_temp(). + +mod_options(Host) -> + [{ram_db_type, ejabberd_config:default_ram_db(Host, ?MODULE)}, + {access, all}, + {host, <<"proxy.", Host/binary>>}, + {hosts, []}, + {hostname, undefined}, + {ip, undefined}, + {port, 7777}, + {name, ?T("SOCKS5 Bytestreams")}, + {vcard, undefined}, + {max_connections, infinity}, + {auth_type, anonymous}, + {recbuf, 65536}, + {sndbuf, 65536}, + {shaper, none}]. + +mod_doc() -> + #{desc => + ?T("This module implements " + "https://xmpp.org/extensions/xep-0065.html" + "[XEP-0065: SOCKS5 Bytestreams]. It allows ejabberd " + "to act as a file transfer proxy between two XMPP clients."), + opts => + [{host, + #{desc => ?T("Deprecated. Use 'hosts' instead.")}}, + {hosts, + #{value => ?T("[Host, ...]"), + desc => + ?T("This option defines the Jabber IDs of the service. " + "If the 'hosts' option is not specified, the only Jabber ID will " + "be the hostname of the virtual host with the prefix \"proxy.\". " + "The keyword '@HOST@' is replaced with the real virtual host name.")}}, + {name, + #{value => ?T("Name"), + desc => + ?T("The value of the service name. This name is only visible in some " + "clients that support https://xmpp.org/extensions/xep-0030.html" + "[XEP-0030: Service Discovery]. The default is \"SOCKS5 Bytestreams\".")}}, + {access, + #{value => ?T("AccessName"), + desc => + ?T("Defines an access rule for file transfer initiators. " + "The default value is 'all'. You may want to restrict " + "access to the users of your server only, in order to " + "avoid abusing your proxy by the users of remote " + "servers.")}}, + {ram_db_type, + #{value => "mnesia | redis | sql", + desc => + ?T("Same as top-level _`default_ram_db`_ option, " + "but applied to this module only.")}}, + {ip, + #{value => ?T("IPAddress"), + desc => + ?T("This option specifies which network interface to listen " + "for. The default value is an IP address of the service's " + "DNS name, or, if fails, '127.0.0.1'.")}}, + {hostname, + #{value => ?T("Host"), + desc => + ?T("Defines a hostname offered by the proxy when " + "establishing a session with clients. This is useful " + "when you run the proxy behind a NAT. The keyword " + "'@HOST@' is replaced with the virtual host name. " + "The default is to use the value of 'ip' option. " + "Examples: 'proxy.mydomain.org', '200.150.100.50'.")}}, + {port, + #{value => "1..65535", + desc => + ?T("A port number to listen for incoming connections. " + "The default value is '7777'.")}}, + {auth_type, + #{value => "anonymous | plain", + desc => + ?T("SOCKS5 authentication type. " + "The default value is 'anonymous'. " + "If set to 'plain', ejabberd will use " + "authentication backend as it would " + "for SASL PLAIN.")}}, + {max_connections, + #{value => "pos_integer() | infinity", + desc => + ?T("Maximum number of active connections per file transfer " + "initiator. The default value is 'infinity'.")}}, + {shaper, + #{value => ?T("Shaper"), + desc => + ?T("This option defines a shaper for the file transfer peers. " + "A shaper with the maximum bandwidth will be selected. " + "The default is 'none', i.e. no shaper.")}}, + {recbuf, + #{value => ?T("Size"), + desc => + ?T("A size of the buffer for incoming packets. " + "If you define a shaper, set the value of this " + "option to the size of the shaper in order " + "to avoid traffic spikes in file transfers. " + "The default value is '65536' bytes.")}}, + {sndbuf, + #{value => ?T("Size"), + desc => + ?T("A size of the buffer for outgoing packets. " + "If you define a shaper, set the value of this " + "option to the size of the shaper in order " + "to avoid traffic spikes in file transfers. " + "The default value is '65536' bytes.")}}, + {vcard, + #{value => ?T("vCard"), + desc => + ?T("A custom vCard of the service that will be displayed " + "by some XMPP clients in Service Discovery. The value of " + "'vCard' is a YAML map constructed from an XML representation " + "of vCard. Since the representation has no attributes, " + "the mapping is straightforward.")}}], + example => + ["acl:", + " admin:", + " user: admin@example.org", + " proxy_users:", + " server: example.org", + "", + "access_rules:", + " proxy65_access:", + " allow: proxy_users", + "", + "shaper_rules:", + " proxy65_shaper:", + " none: admin", + " proxyrate: proxy_users", + "", + "shaper:", + " proxyrate: 10240", + "", + "modules:", + " mod_proxy65:", + " host: proxy1.example.org", + " name: \"File Transfer Proxy\"", + " ip: 200.150.100.1", + " port: 7778", + " max_connections: 5", + " access: proxy65_access", + " shaper: proxy65_shaper", + " recbuf: 10240", + " sndbuf: 10240"]}. diff --git a/src/mod_proxy65_lib.erl b/src/mod_proxy65_lib.erl index 6c5967688..1f5b25ed0 100644 --- a/src/mod_proxy65_lib.erl +++ b/src/mod_proxy65_lib.erl @@ -5,7 +5,7 @@ %%% Created : 12 Oct 2006 by Evgeniy Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/mod_proxy65_mnesia.erl b/src/mod_proxy65_mnesia.erl new file mode 100644 index 000000000..3661b62c0 --- /dev/null +++ b/src/mod_proxy65_mnesia.erl @@ -0,0 +1,159 @@ +%%%------------------------------------------------------------------- +%%% Created : 16 Jan 2017 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_proxy65_mnesia). +-behaviour(gen_server). +-behaviour(mod_proxy65). + +%% API +-export([init/0, register_stream/2, unregister_stream/1, activate_stream/4]). +-export([start_link/0]). +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-include("logger.hrl"). + +-record(bytestream, + {sha1 = <<"">> :: binary() | '$1', + target :: pid() | '_', + initiator :: pid() | '_' | undefined, + active = false :: boolean() | '_', + jid_i :: undefined | binary() | '_'}). + +-record(state, {}). + +%%%=================================================================== +%%% API +%%%=================================================================== +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +init() -> + Spec = {?MODULE, {?MODULE, start_link, []}, transient, + 5000, worker, [?MODULE]}, + supervisor:start_child(ejabberd_backend_sup, Spec). + +register_stream(SHA1, StreamPid) -> + F = fun () -> + case mnesia:read(bytestream, SHA1, write) of + [] -> + mnesia:write(#bytestream{sha1 = SHA1, + target = StreamPid}); + [#bytestream{target = Pid, initiator = undefined} = + ByteStream] when is_pid(Pid), Pid /= StreamPid -> + mnesia:write(ByteStream#bytestream{ + initiator = StreamPid}) + end + end, + case mnesia:transaction(F) of + {atomic, ok} -> + ok; + {aborted, Reason} -> + ?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]), + {error, Reason} + end. + +unregister_stream(SHA1) -> + F = fun () -> mnesia:delete({bytestream, SHA1}) end, + case mnesia:transaction(F) of + {atomic, ok} -> + ok; + {aborted, Reason} -> + ?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]), + {error, Reason} + end. + +activate_stream(SHA1, Initiator, MaxConnections, _Node) -> + case gen_server:call(?MODULE, + {activate_stream, SHA1, Initiator, MaxConnections}) of + {atomic, {ok, IPid, TPid}} -> + {ok, IPid, TPid}; + {atomic, {limit, IPid, TPid}} -> + {error, {limit, IPid, TPid}}; + {atomic, conflict} -> + {error, conflict}; + {atomic, notfound} -> + {error, notfound}; + Err -> + {error, Err} + end. + +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== +init([]) -> + ejabberd_mnesia:create(?MODULE, bytestream, + [{ram_copies, [node()]}, + {attributes, record_info(fields, bytestream)}]), + {ok, #state{}}. + +handle_call({activate_stream, SHA1, Initiator, MaxConnections}, _From, State) -> + F = fun () -> + case mnesia:read(bytestream, SHA1, write) of + [#bytestream{target = TPid, initiator = IPid} = + ByteStream] when is_pid(TPid), is_pid(IPid) -> + ActiveFlag = ByteStream#bytestream.active, + if ActiveFlag == false -> + ConnsPerJID = mnesia:select( + bytestream, + [{#bytestream{sha1 = '$1', + jid_i = Initiator, + _ = '_'}, + [], ['$1']}]), + if length(ConnsPerJID) < MaxConnections -> + mnesia:write( + ByteStream#bytestream{active = true, + jid_i = Initiator}), + {ok, IPid, TPid}; + true -> + {limit, IPid, TPid} + end; + true -> + conflict + end; + _ -> + notfound + end + end, + Reply = mnesia:transaction(F), + {reply, Reply, State}; +handle_call(Request, From, State) -> + ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), + {noreply, State}. + +handle_cast(Msg, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Msg]), + {noreply, State}. + +handle_info(Info, State) -> + ?WARNING_MSG("Unexpected info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== diff --git a/src/mod_proxy65_opt.erl b/src/mod_proxy65_opt.erl new file mode 100644 index 000000000..95f039b16 --- /dev/null +++ b/src/mod_proxy65_opt.erl @@ -0,0 +1,111 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_proxy65_opt). + +-export([access/1]). +-export([auth_type/1]). +-export([host/1]). +-export([hostname/1]). +-export([hosts/1]). +-export([ip/1]). +-export([max_connections/1]). +-export([name/1]). +-export([port/1]). +-export([ram_db_type/1]). +-export([recbuf/1]). +-export([server_host/1]). +-export([shaper/1]). +-export([sndbuf/1]). +-export([vcard/1]). + +-spec access(gen_mod:opts() | global | binary()) -> 'all' | acl:acl(). +access(Opts) when is_map(Opts) -> + gen_mod:get_opt(access, Opts); +access(Host) -> + gen_mod:get_module_opt(Host, mod_proxy65, access). + +-spec auth_type(gen_mod:opts() | global | binary()) -> 'anonymous' | 'plain'. +auth_type(Opts) when is_map(Opts) -> + gen_mod:get_opt(auth_type, Opts); +auth_type(Host) -> + gen_mod:get_module_opt(Host, mod_proxy65, auth_type). + +-spec host(gen_mod:opts() | global | binary()) -> binary(). +host(Opts) when is_map(Opts) -> + gen_mod:get_opt(host, Opts); +host(Host) -> + gen_mod:get_module_opt(Host, mod_proxy65, host). + +-spec hostname(gen_mod:opts() | global | binary()) -> 'undefined' | binary(). +hostname(Opts) when is_map(Opts) -> + gen_mod:get_opt(hostname, Opts); +hostname(Host) -> + gen_mod:get_module_opt(Host, mod_proxy65, hostname). + +-spec hosts(gen_mod:opts() | global | binary()) -> [binary()]. +hosts(Opts) when is_map(Opts) -> + gen_mod:get_opt(hosts, Opts); +hosts(Host) -> + gen_mod:get_module_opt(Host, mod_proxy65, hosts). + +-spec ip(gen_mod:opts() | global | binary()) -> 'undefined' | inet:ip_address(). +ip(Opts) when is_map(Opts) -> + gen_mod:get_opt(ip, Opts); +ip(Host) -> + gen_mod:get_module_opt(Host, mod_proxy65, ip). + +-spec max_connections(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +max_connections(Opts) when is_map(Opts) -> + gen_mod:get_opt(max_connections, Opts); +max_connections(Host) -> + gen_mod:get_module_opt(Host, mod_proxy65, max_connections). + +-spec name(gen_mod:opts() | global | binary()) -> binary(). +name(Opts) when is_map(Opts) -> + gen_mod:get_opt(name, Opts); +name(Host) -> + gen_mod:get_module_opt(Host, mod_proxy65, name). + +-spec port(gen_mod:opts() | global | binary()) -> 1..1114111. +port(Opts) when is_map(Opts) -> + gen_mod:get_opt(port, Opts); +port(Host) -> + gen_mod:get_module_opt(Host, mod_proxy65, port). + +-spec ram_db_type(gen_mod:opts() | global | binary()) -> atom(). +ram_db_type(Opts) when is_map(Opts) -> + gen_mod:get_opt(ram_db_type, Opts); +ram_db_type(Host) -> + gen_mod:get_module_opt(Host, mod_proxy65, ram_db_type). + +-spec recbuf(gen_mod:opts() | global | binary()) -> pos_integer(). +recbuf(Opts) when is_map(Opts) -> + gen_mod:get_opt(recbuf, Opts); +recbuf(Host) -> + gen_mod:get_module_opt(Host, mod_proxy65, recbuf). + +-spec server_host(gen_mod:opts() | global | binary()) -> binary(). +server_host(Opts) when is_map(Opts) -> + gen_mod:get_opt(server_host, Opts); +server_host(Host) -> + gen_mod:get_module_opt(Host, mod_proxy65, server_host). + +-spec shaper(gen_mod:opts() | global | binary()) -> atom() | [ejabberd_shaper:shaper_rule()]. +shaper(Opts) when is_map(Opts) -> + gen_mod:get_opt(shaper, Opts); +shaper(Host) -> + gen_mod:get_module_opt(Host, mod_proxy65, shaper). + +-spec sndbuf(gen_mod:opts() | global | binary()) -> pos_integer(). +sndbuf(Opts) when is_map(Opts) -> + gen_mod:get_opt(sndbuf, Opts); +sndbuf(Host) -> + gen_mod:get_module_opt(Host, mod_proxy65, sndbuf). + +-spec vcard(gen_mod:opts() | global | binary()) -> 'undefined' | tuple(). +vcard(Opts) when is_map(Opts) -> + gen_mod:get_opt(vcard, Opts); +vcard(Host) -> + gen_mod:get_module_opt(Host, mod_proxy65, vcard). + diff --git a/src/mod_proxy65_redis.erl b/src/mod_proxy65_redis.erl new file mode 100644 index 000000000..588bd55f3 --- /dev/null +++ b/src/mod_proxy65_redis.erl @@ -0,0 +1,185 @@ +%%%------------------------------------------------------------------- +%%% Author : Evgeny Khramtsov +%%% Created : 31 Mar 2017 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_proxy65_redis). +-behaviour(mod_proxy65). + +%% API +-export([init/0, register_stream/2, unregister_stream/1, activate_stream/4]). + +-include("logger.hrl"). + +-record(proxy65, {pid_t :: pid(), + pid_i :: pid() | undefined, + jid_i :: binary() | undefined}). + +%%%=================================================================== +%%% API +%%%=================================================================== +init() -> + ?DEBUG("Cleaning Redis 'proxy65' table...", []), + NodeKey = node_key(), + case ejabberd_redis:smembers(NodeKey) of + {ok, SIDs} -> + SIDKeys = [sid_key(S) || S <- SIDs], + JIDs = lists:flatmap( + fun(SIDKey) -> + case ejabberd_redis:get(SIDKey) of + {ok, Val} -> + try binary_to_term(Val) of + #proxy65{jid_i = J} when is_binary(J) -> + [jid_key(J)]; + _ -> + [] + catch _:badarg -> + [] + end; + _ -> + [] + end + end, SIDKeys), + ejabberd_redis:multi( + fun() -> + if SIDs /= [] -> + ejabberd_redis:del(SIDKeys), + if JIDs /= [] -> + ejabberd_redis:del(JIDs); + true -> + ok + end; + true -> + ok + end, + ejabberd_redis:del([NodeKey]) + end), + ok; + {error, _} -> + {error, db_failure} + end. + +register_stream(SID, Pid) -> + SIDKey = sid_key(SID), + try + {ok, Val} = ejabberd_redis:get(SIDKey), + try binary_to_term(Val) of + #proxy65{pid_i = undefined} = R -> + NewVal = term_to_binary(R#proxy65{pid_i = Pid}), + ok = ejabberd_redis:set(SIDKey, NewVal); + _ -> + {error, conflict} + catch _:badarg when Val == undefined -> + NewVal = term_to_binary(#proxy65{pid_t = Pid}), + {ok, _} = ejabberd_redis:multi( + fun() -> + ejabberd_redis:set(SIDKey, NewVal), + ejabberd_redis:sadd(node_key(), [SID]) + end), + ok; + _:badarg -> + ?ERROR_MSG("Malformed data in redis (key = '~ts'): ~p", + [SIDKey, Val]), + {error, db_failure} + end + catch _:{badmatch, {error, _}} -> + {error, db_failure} + end. + +unregister_stream(SID) -> + SIDKey = sid_key(SID), + NodeKey = node_key(), + try + {ok, Val} = ejabberd_redis:get(SIDKey), + try binary_to_term(Val) of + #proxy65{jid_i = JID} when is_binary(JID) -> + JIDKey = jid_key(JID), + {ok, _} = ejabberd_redis:multi( + fun() -> + ejabberd_redis:del([SIDKey]), + ejabberd_redis:srem(JIDKey, [SID]), + ejabberd_redis:srem(NodeKey, [SID]) + end), + ok; + _ -> + {ok, _} = ejabberd_redis:multi( + fun() -> + ejabberd_redis:del([SIDKey]), + ejabberd_redis:srem(NodeKey, [SID]) + end), + ok + catch _:badarg when Val == undefined -> + ok; + _:badarg -> + ?ERROR_MSG("Malformed data in redis (key = '~ts'): ~p", + [SIDKey, Val]), + {error, db_failure} + end + catch _:{badmatch, {error, _}} -> + {error, db_failure} + end. + +activate_stream(SID, IJID, MaxConnections, _Node) -> + SIDKey = sid_key(SID), + JIDKey = jid_key(IJID), + try + {ok, Val} = ejabberd_redis:get(SIDKey), + try binary_to_term(Val) of + #proxy65{pid_t = TPid, pid_i = IPid, + jid_i = undefined} = R when is_pid(IPid) -> + {ok, Num} = ejabberd_redis:scard(JIDKey), + if Num >= MaxConnections -> + {error, {limit, IPid, TPid}}; + true -> + NewVal = term_to_binary(R#proxy65{jid_i = IJID}), + {ok, _} = ejabberd_redis:multi( + fun() -> + ejabberd_redis:sadd(JIDKey, [SID]), + ejabberd_redis:set(SIDKey, NewVal) + end), + {ok, IPid, TPid} + end; + #proxy65{jid_i = JID} when is_binary(JID) -> + {error, conflict}; + _ -> + {error, notfound} + catch _:badarg when Val == undefined -> + {error, notfound}; + _:badarg -> + ?ERROR_MSG("Malformed data in redis (key = '~ts'): ~p", + [SIDKey, Val]), + {error, db_failure} + end + catch _:{badmatch, {error, _}} -> + {error, db_failure} + end. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +sid_key(SID) -> + <<"ejabberd:proxy65:sid:", SID/binary>>. + +jid_key(JID) -> + <<"ejabberd:proxy65:initiator:", JID/binary>>. + +node_key() -> + Node = erlang:atom_to_binary(node(), latin1), + <<"ejabberd:proxy65:node:", Node/binary>>. diff --git a/src/mod_proxy65_service.erl b/src/mod_proxy65_service.erl index 1e7735c58..2d170ed68 100644 --- a/src/mod_proxy65_service.erl +++ b/src/mod_proxy65_service.erl @@ -5,7 +5,7 @@ %%% Created : 12 Oct 2006 by Evgeniy Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -33,262 +33,259 @@ -export([init/1, handle_info/2, handle_call/3, handle_cast/2, terminate/2, code_change/3]). -%% API. --export([start_link/2, add_listener/2, transform_module_options/1, - delete_listener/1]). +-export([start_link/1, reload/3, add_listener/2, process_disco_info/1, + process_disco_items/1, process_vcard/1, process_bytestreams/1, + delete_listener/1, route/1]). --include("ejabberd.hrl"). -include("logger.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). +-include("translate.hrl"). --include("jlib.hrl"). -define(PROCNAME, ejabberd_mod_proxy65_service). --record(state, - {myhost = <<"">> :: binary(), - serverhost = <<"">> :: binary(), - name = <<"">> :: binary(), - stream_addr = [] :: [attr()], - port = 0 :: inet:port_number(), - ip = {127,0,0,1} :: inet:ip_address(), - acl = none :: atom()}). +-record(state, {myhosts = [] :: [binary()]}). %%%------------------------ %%% gen_server callbacks %%%------------------------ -start_link(Host, Opts) -> +start_link(Host) -> Proc = gen_mod:get_module_proc(Host, ?PROCNAME), - gen_server:start_link({local, Proc}, ?MODULE, - [Host, Opts], []). + gen_server:start_link({local, Proc}, ?MODULE, [Host], []). -init([Host, Opts]) -> - State = parse_options(Host, Opts), - ejabberd_router:register_route(State#state.myhost), - {ok, State}. +reload(Host, NewOpts, OldOpts) -> + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + gen_server:cast(Proc, {reload, Host, NewOpts, OldOpts}). -terminate(_Reason, #state{myhost = MyHost}) -> - ejabberd_router:unregister_route(MyHost), ok. +init([Host]) -> + process_flag(trap_exit, true), + Opts = gen_mod:get_module_opts(Host, mod_proxy65), + MyHosts = gen_mod:get_opt_hosts(Opts), + lists:foreach( + fun(MyHost) -> + gen_iq_handler:add_iq_handler(ejabberd_local, MyHost, ?NS_DISCO_INFO, + ?MODULE, process_disco_info), + gen_iq_handler:add_iq_handler(ejabberd_local, MyHost, ?NS_DISCO_ITEMS, + ?MODULE, process_disco_items), + gen_iq_handler:add_iq_handler(ejabberd_local, MyHost, ?NS_VCARD, + ?MODULE, process_vcard), + gen_iq_handler:add_iq_handler(ejabberd_local, MyHost, ?NS_BYTESTREAMS, + ?MODULE, process_bytestreams), + ejabberd_router:register_route( + MyHost, Host, {apply, ?MODULE, route}) + end, MyHosts), + {ok, #state{myhosts = MyHosts}}. -handle_info({route, From, To, - #xmlel{name = <<"iq">>} = Packet}, - State) -> - IQ = jlib:iq_query_info(Packet), - case catch process_iq(From, IQ, State) of - Result when is_record(Result, iq) -> - ejabberd_router:route(To, From, jlib:iq_to_xml(Result)); - {'EXIT', Reason} -> - ?ERROR_MSG("Error when processing IQ stanza: ~p", - [Reason]), - Err = jlib:make_error_reply(Packet, - ?ERR_INTERNAL_SERVER_ERROR), - ejabberd_router:route(To, From, Err); - _ -> ok +terminate(_Reason, #state{myhosts = MyHosts}) -> + lists:foreach( + fun(MyHost) -> + ejabberd_router:unregister_route(MyHost), + unregister_handlers(MyHost) + end, MyHosts). + +handle_info({route, Packet}, State) -> + try route(Packet) + catch + Class:Reason:StackTrace -> + ?ERROR_MSG("Failed to route packet:~n~ts~n** ~ts", + [xmpp:pp(Packet), + misc:format_exception(2, Class, Reason, StackTrace)]) end, {noreply, State}; -handle_info(_Info, State) -> {noreply, State}. +handle_info(Info, State) -> + ?WARNING_MSG("Unexpected info: ~p", [Info]), + {noreply, State}. -handle_call(get_port_ip, _From, State) -> - {reply, {port_ip, State#state.port, State#state.ip}, - State}; -handle_call(_Request, _From, State) -> - {reply, ok, State}. +handle_call(Request, From, State) -> + ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), + {noreply, State}. -handle_cast(_Request, State) -> {noreply, State}. +handle_cast({reload, ServerHost, NewOpts, OldOpts}, State) -> + NewHosts = gen_mod:get_opt_hosts(NewOpts), + OldHosts = gen_mod:get_opt_hosts(OldOpts), + lists:foreach( + fun(NewHost) -> + ejabberd_router:register_route(NewHost, ServerHost), + register_handlers(NewHost) + end, NewHosts -- OldHosts), + lists:foreach( + fun(OldHost) -> + ejabberd_router:unregister_route(OldHost), + unregister_handlers(OldHost) + end, OldHosts -- NewHosts), + {noreply, State#state{myhosts = NewHosts}}; +handle_cast(Msg, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Msg]), + {noreply, State}. code_change(_OldVsn, State, _Extra) -> {ok, State}. +-spec route(stanza()) -> ok. +route(#iq{} = IQ) -> + ejabberd_router:process_iq(IQ); +route(_) -> + ok. + %%%------------------------ %%% Listener management %%%------------------------ add_listener(Host, Opts) -> - State = parse_options(Host, Opts), - NewOpts = [Host | Opts], - ejabberd_listener:add_listener({State#state.port, - State#state.ip}, - mod_proxy65_stream, NewOpts). + {_, IP, _} = EndPoint = get_endpoint(Host), + Opts1 = gen_mod:set_opt(server_host, Host, Opts), + Opts2 = gen_mod:set_opt(ip, IP, Opts1), + ejabberd_listener:add_listener(EndPoint, mod_proxy65_stream, Opts2). delete_listener(Host) -> - Proc = gen_mod:get_module_proc(Host, ?PROCNAME), - {port_ip, Port, IP} = gen_server:call(Proc, - get_port_ip), - catch ejabberd_listener:delete_listener({Port, IP}, - mod_proxy65_stream). + ejabberd_listener:delete_listener(get_endpoint(Host), mod_proxy65_stream). %%%------------------------ %%% IQ Processing %%%------------------------ +-spec process_disco_info(iq()) -> iq(). +process_disco_info(#iq{type = set, lang = Lang} = IQ) -> + Txt = ?T("Value 'set' of 'type' attribute is not allowed"), + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); +process_disco_info(#iq{type = get, to = To, lang = Lang} = IQ) -> + Host = ejabberd_router:host_of_route(To#jid.lserver), + Name = mod_proxy65_opt:name(Host), + Info = ejabberd_hooks:run_fold(disco_info, Host, + [], [Host, ?MODULE, <<"">>, <<"">>]), + xmpp:make_iq_result( + IQ, #disco_info{xdata = Info, + identities = [#identity{category = <<"proxy">>, + type = <<"bytestreams">>, + name = translate:translate(Lang, Name)}], + features = [?NS_DISCO_INFO, ?NS_DISCO_ITEMS, + ?NS_VCARD, ?NS_BYTESTREAMS]}). -%% disco#info request -process_iq(_, - #iq{type = get, xmlns = ?NS_DISCO_INFO, lang = Lang} = - IQ, - #state{name = Name, serverhost = ServerHost}) -> - Info = ejabberd_hooks:run_fold(disco_info, ServerHost, - [], [ServerHost, ?MODULE, <<"">>, <<"">>]), - IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"query">>, - attrs = [{<<"xmlns">>, ?NS_DISCO_INFO}], - children = iq_disco_info(Lang, Name) ++ Info}]}; -%% disco#items request -process_iq(_, - #iq{type = get, xmlns = ?NS_DISCO_ITEMS} = IQ, _) -> - IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"query">>, - attrs = [{<<"xmlns">>, ?NS_DISCO_ITEMS}], - children = []}]}; -%% vCard request -process_iq(_, - #iq{type = get, xmlns = ?NS_VCARD, lang = Lang} = IQ, - _) -> - IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"vCard">>, - attrs = [{<<"xmlns">>, ?NS_VCARD}], - children = iq_vcard(Lang)}]}; -%% bytestreams info request -process_iq(JID, - #iq{type = get, sub_el = SubEl, - xmlns = ?NS_BYTESTREAMS} = - IQ, - #state{acl = ACL, stream_addr = StreamAddr, - serverhost = ServerHost}) -> +-spec process_disco_items(iq()) -> iq(). +process_disco_items(#iq{type = set, lang = Lang} = IQ) -> + Txt = ?T("Value 'set' of 'type' attribute is not allowed"), + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); +process_disco_items(#iq{type = get} = IQ) -> + xmpp:make_iq_result(IQ, #disco_items{}). + +-spec process_vcard(iq()) -> iq(). +process_vcard(#iq{type = set, lang = Lang} = IQ) -> + Txt = ?T("Value 'set' of 'type' attribute is not allowed"), + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); +process_vcard(#iq{type = get, to = To, lang = Lang} = IQ) -> + ServerHost = ejabberd_router:host_of_route(To#jid.lserver), + VCard = case mod_proxy65_opt:vcard(ServerHost) of + undefined -> + #vcard_temp{fn = <<"ejabberd/mod_proxy65">>, + url = ejabberd_config:get_uri(), + desc = misc:get_descr( + Lang, ?T("ejabberd SOCKS5 Bytestreams module"))}; + V -> + V + end, + xmpp:make_iq_result(IQ, VCard). + +-spec process_bytestreams(iq()) -> iq(). +process_bytestreams(#iq{type = get, from = JID, to = To, lang = Lang} = IQ) -> + Host = To#jid.lserver, + ServerHost = ejabberd_router:host_of_route(Host), + ACL = mod_proxy65_opt:access(ServerHost), case acl:match_rule(ServerHost, ACL, JID) of - allow -> - StreamHostEl = [#xmlel{name = <<"streamhost">>, - attrs = StreamAddr, children = []}], - IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"query">>, - attrs = [{<<"xmlns">>, ?NS_BYTESTREAMS}], - children = StreamHostEl}]}; - deny -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_FORBIDDEN]} + allow -> + StreamHost = get_streamhost(Host, ServerHost), + xmpp:make_iq_result(IQ, #bytestreams{hosts = [StreamHost]}); + deny -> + xmpp:make_error(IQ, xmpp:err_forbidden(?T("Access denied by service policy"), Lang)) end; -%% bytestream activation request -process_iq(InitiatorJID, - #iq{type = set, sub_el = SubEl, - xmlns = ?NS_BYTESTREAMS} = - IQ, - #state{acl = ACL, serverhost = ServerHost}) -> +process_bytestreams(#iq{type = set, lang = Lang, + sub_els = [#bytestreams{sid = SID}]} = IQ) + when SID == <<"">> orelse size(SID) > 128 -> + Why = {bad_attr_value, <<"sid">>, <<"query">>, ?NS_BYTESTREAMS}, + Txt = xmpp:io_format_error(Why), + xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)); +process_bytestreams(#iq{type = set, lang = Lang, + sub_els = [#bytestreams{activate = undefined}]} = IQ) -> + Why = {missing_cdata, <<"">>, <<"activate">>, ?NS_BYTESTREAMS}, + Txt = xmpp:io_format_error(Why), + xmpp:make_error(IQ, xmpp:err_jid_malformed(Txt, Lang)); +process_bytestreams(#iq{type = set, lang = Lang, from = InitiatorJID, to = To, + sub_els = [#bytestreams{activate = TargetJID, + sid = SID}]} = IQ) -> + ServerHost = ejabberd_router:host_of_route(To#jid.lserver), + ACL = mod_proxy65_opt:access(ServerHost), case acl:match_rule(ServerHost, ACL, InitiatorJID) of - allow -> - ActivateEl = xml:get_path_s(SubEl, - [{elem, <<"activate">>}]), - SID = xml:get_tag_attr_s(<<"sid">>, SubEl), - case catch - jlib:string_to_jid(xml:get_tag_cdata(ActivateEl)) - of - TargetJID - when is_record(TargetJID, jid), SID /= <<"">>, - byte_size(SID) =< 128, TargetJID /= InitiatorJID -> - Target = - jlib:jid_to_string(jlib:jid_tolower(TargetJID)), - Initiator = - jlib:jid_to_string(jlib:jid_tolower(InitiatorJID)), - SHA1 = p1_sha:sha(<>), - case mod_proxy65_sm:activate_stream(SHA1, InitiatorJID, - TargetJID, ServerHost) - of - ok -> IQ#iq{type = result, sub_el = []}; - false -> - IQ#iq{type = error, - sub_el = [SubEl, ?ERR_ITEM_NOT_FOUND]}; - limit -> - IQ#iq{type = error, - sub_el = [SubEl, ?ERR_RESOURCE_CONSTRAINT]}; - conflict -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_CONFLICT]}; - _ -> - IQ#iq{type = error, - sub_el = [SubEl, ?ERR_INTERNAL_SERVER_ERROR]} - end; - _ -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_BAD_REQUEST]} - end; - deny -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_FORBIDDEN]} - end; -%% Unknown "set" or "get" request -process_iq(_, #iq{type = Type, sub_el = SubEl} = IQ, _) - when Type == get; Type == set -> - IQ#iq{type = error, - sub_el = [SubEl, ?ERR_SERVICE_UNAVAILABLE]}; -%% IQ "result" or "error". -process_iq(_, _, _) -> ok. + allow -> + Node = ejabberd_cluster:get_node_by_id(To#jid.lresource), + Target = jid:encode(jid:tolower(TargetJID)), + Initiator = jid:encode(jid:tolower(InitiatorJID)), + SHA1 = str:sha(<>), + Mod = gen_mod:ram_db_mod(global, mod_proxy65), + MaxConnections = max_connections(ServerHost), + case Mod:activate_stream(SHA1, Initiator, MaxConnections, Node) of + {ok, InitiatorPid, TargetPid} -> + mod_proxy65_stream:activate( + {InitiatorPid, InitiatorJID}, {TargetPid, TargetJID}), + xmpp:make_iq_result(IQ); + {error, notfound} -> + Txt = ?T("Failed to activate bytestream"), + xmpp:make_error(IQ, xmpp:err_item_not_found(Txt, Lang)); + {error, {limit, InitiatorPid, TargetPid}} -> + mod_proxy65_stream:stop(InitiatorPid), + mod_proxy65_stream:stop(TargetPid), + Txt = ?T("Too many active bytestreams"), + xmpp:make_error(IQ, xmpp:err_resource_constraint(Txt, Lang)); + {error, conflict} -> + Txt = ?T("Bytestream already activated"), + xmpp:make_error(IQ, xmpp:err_conflict(Txt, Lang)); + {error, Err} -> + ?ERROR_MSG("Failed to activate bytestream from ~ts to ~ts: ~p", + [Initiator, Target, Err]), + Txt = ?T("Database failure"), + xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) + end; + deny -> + Txt = ?T("Access denied by service policy"), + xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)) + end. %%%------------------------- %%% Auxiliary functions. %%%------------------------- --define(FEATURE(Feat), - #xmlel{name = <<"feature">>, - attrs = [{<<"var">>, Feat}], children = []}). +-spec get_streamhost(binary(), binary()) -> streamhost(). +get_streamhost(Host, ServerHost) -> + {Port, IP, _} = get_endpoint(ServerHost), + HostName = case mod_proxy65_opt:hostname(ServerHost) of + undefined -> misc:ip_to_list(IP); + Val -> Val + end, + Resource = ejabberd_cluster:node_id(), + #streamhost{jid = jid:make(<<"">>, Host, Resource), + host = HostName, + port = Port}. -iq_disco_info(Lang, Name) -> - [#xmlel{name = <<"identity">>, - attrs = - [{<<"category">>, <<"proxy">>}, - {<<"type">>, <<"bytestreams">>}, - {<<"name">>, translate:translate(Lang, Name)}], - children = []}, - ?FEATURE((?NS_DISCO_INFO)), ?FEATURE((?NS_VCARD)), - ?FEATURE((?NS_BYTESTREAMS))]. +-spec get_endpoint(binary()) -> {inet:port_number(), inet:ip_address(), tcp}. +get_endpoint(Host) -> + Port = mod_proxy65_opt:port(Host), + IP = case mod_proxy65_opt:ip(Host) of + undefined -> misc:get_my_ipv4_address(); + Addr -> Addr + end, + {Port, IP, tcp}. -iq_vcard(Lang) -> - [#xmlel{name = <<"FN">>, attrs = [], - children = [{xmlcdata, <<"ejabberd/mod_proxy65">>}]}, - #xmlel{name = <<"URL">>, attrs = [], - children = [{xmlcdata, ?EJABBERD_URI}]}, - #xmlel{name = <<"DESC">>, attrs = [], - children = - [{xmlcdata, - <<(translate:translate(Lang, - <<"ejabberd SOCKS5 Bytestreams module">>))/binary, - "\nCopyright (c) 2003-2015 ProcessOne">>}]}]. +max_connections(ServerHost) -> + mod_proxy65_opt:max_connections(ServerHost). -parse_options(ServerHost, Opts) -> - MyHost = gen_mod:get_opt_host(ServerHost, Opts, - <<"proxy.@HOST@">>), - Port = gen_mod:get_opt(port, Opts, - fun(P) when is_integer(P), P>0, P<65536 -> P end, - 7777), - ACL = gen_mod:get_opt(access, Opts, fun(A) when is_atom(A) -> A end, - all), - Name = gen_mod:get_opt(name, Opts, fun iolist_to_binary/1, - <<"SOCKS5 Bytestreams">>), - IP = gen_mod:get_opt(ip, Opts, - fun(S) -> - {ok, Addr} = inet_parse:address( - binary_to_list( - iolist_to_binary(S))), - Addr - end, get_my_ip()), - HostName = gen_mod:get_opt(hostname, Opts, - fun iolist_to_binary/1, - jlib:ip_to_list(IP)), - StreamAddr = [{<<"jid">>, MyHost}, - {<<"host">>, HostName}, - {<<"port">>, jlib:integer_to_binary(Port)}], - #state{myhost = MyHost, serverhost = ServerHost, - name = Name, port = Port, ip = IP, - stream_addr = StreamAddr, acl = ACL}. +register_handlers(Host) -> + gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO, + ?MODULE, process_disco_info), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_DISCO_ITEMS, + ?MODULE, process_disco_items), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_VCARD, + ?MODULE, process_vcard), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_BYTESTREAMS, + ?MODULE, process_bytestreams). -transform_module_options(Opts) -> - lists:map( - fun({ip, IP}) when is_tuple(IP) -> - {ip, jlib:ip_to_list(IP)}; - ({hostname, IP}) when is_tuple(IP) -> - {hostname, jlib:ip_to_list(IP)}; - (Opt) -> - Opt - end, Opts). - -get_my_ip() -> - {ok, MyHostName} = inet:gethostname(), - case inet:getaddr(MyHostName, inet) of - {ok, Addr} -> Addr; - {error, _} -> {127, 0, 0, 1} - end. +unregister_handlers(Host) -> + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_ITEMS), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_VCARD), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_BYTESTREAMS). diff --git a/src/mod_proxy65_sm.erl b/src/mod_proxy65_sm.erl deleted file mode 100644 index 367f7f0bd..000000000 --- a/src/mod_proxy65_sm.erl +++ /dev/null @@ -1,173 +0,0 @@ -%%%---------------------------------------------------------------------- -%%% File : mod_proxy65_sm.erl -%%% Author : Evgeniy Khramtsov -%%% Purpose : Bytestreams manager. -%%% Created : 12 Oct 2006 by Evgeniy Khramtsov -%%% -%%% -%%% ejabberd, Copyright (C) 2002-2015 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_proxy65_sm). - --author('xram@jabber.ru'). - --behaviour(gen_server). - -%% gen_server callbacks. --export([init/1, handle_info/2, handle_call/3, - handle_cast/2, terminate/2, code_change/3]). - -%% API. --export([start_link/2, register_stream/1, - unregister_stream/1, activate_stream/4]). - --record(state, {max_connections = infinity :: non_neg_integer() | infinity}). - --include("jlib.hrl"). - --record(bytestream, - {sha1 = <<"">> :: binary() | '$1', - target :: pid() | '_', - initiator :: pid() | '_', - active = false :: boolean() | '_', - jid_i = {<<"">>, <<"">>, <<"">>} :: ljid() | '_'}). - --define(PROCNAME, ejabberd_mod_proxy65_sm). - -%% Unused callbacks. -handle_cast(_Request, State) -> {noreply, State}. - -code_change(_OldVsn, State, _Extra) -> {ok, State}. - -handle_info(_Info, State) -> {noreply, State}. - -%%---------------- - -start_link(Host, Opts) -> - Proc = gen_mod:get_module_proc(Host, ?PROCNAME), - gen_server:start_link({local, Proc}, ?MODULE, [Opts], - []). - -init([Opts]) -> - mnesia:create_table(bytestream, [{ram_copies, [node()]}, - {attributes, record_info(fields, bytestream)}]), - mnesia:add_table_copy(bytestream, node(), ram_copies), - MaxConnections = gen_mod:get_opt(max_connections, Opts, - fun(I) when is_integer(I), I>0 -> - I; - (infinity) -> - infinity - end, infinity), - {ok, #state{max_connections = MaxConnections}}. - -terminate(_Reason, _State) -> ok. - -handle_call({activate, SHA1, IJid}, _From, State) -> - MaxConns = State#state.max_connections, - F = fun () -> - case mnesia:read(bytestream, SHA1, write) of - [#bytestream{target = TPid, initiator = IPid} = - ByteStream] - when is_pid(TPid), is_pid(IPid) -> - ActiveFlag = ByteStream#bytestream.active, - if ActiveFlag == false -> - ConnsPerJID = mnesia:select(bytestream, - [{#bytestream{sha1 = - '$1', - jid_i = - IJid, - _ = '_'}, - [], ['$1']}]), - if length(ConnsPerJID) < MaxConns -> - mnesia:write(ByteStream#bytestream{active = - true, - jid_i = - IJid}), - {ok, IPid, TPid}; - true -> {limit, IPid, TPid} - end; - true -> conflict - end; - _ -> false - end - end, - Reply = mnesia:transaction(F), - {reply, Reply, State}; -handle_call(_Request, _From, State) -> - {reply, ok, State}. - -%%%---------------------- -%%% API. -%%%---------------------- -%%%--------------------------------------------------- -%%% register_stream(SHA1) -> {atomic, ok} | -%%% {atomic, error} | -%%% transaction abort -%%% SHA1 = string() -%%%--------------------------------------------------- -register_stream(SHA1) when is_binary(SHA1) -> - StreamPid = self(), - F = fun () -> - case mnesia:read(bytestream, SHA1, write) of - [] -> - mnesia:write(#bytestream{sha1 = SHA1, - target = StreamPid}); - [#bytestream{target = Pid, initiator = undefined} = - ByteStream] - when is_pid(Pid), Pid /= StreamPid -> - mnesia:write(ByteStream#bytestream{initiator = - StreamPid}); - _ -> error - end - end, - mnesia:transaction(F). - -%%%---------------------------------------------------- -%%% unregister_stream(SHA1) -> ok | transaction abort -%%% SHA1 = string() -%%%---------------------------------------------------- -unregister_stream(SHA1) when is_binary(SHA1) -> - F = fun () -> mnesia:delete({bytestream, SHA1}) end, - mnesia:transaction(F). - -%%%-------------------------------------------------------- -%%% activate_stream(SHA1, IJid, TJid, Host) -> ok | -%%% false | -%%% limit | -%%% conflict | -%%% error -%%% SHA1 = string() -%%% IJid = TJid = jid() -%%% Host = string() -%%%-------------------------------------------------------- -activate_stream(SHA1, IJid, TJid, Host) - when is_binary(SHA1) -> - Proc = gen_mod:get_module_proc(Host, ?PROCNAME), - case catch gen_server:call(Proc, {activate, SHA1, IJid}) - of - {atomic, {ok, IPid, TPid}} -> - mod_proxy65_stream:activate({IPid, IJid}, {TPid, TJid}); - {atomic, {limit, IPid, TPid}} -> - mod_proxy65_stream:stop(IPid), - mod_proxy65_stream:stop(TPid), - limit; - {atomic, conflict} -> conflict; - {atomic, false} -> false; - _ -> error - end. diff --git a/src/mod_proxy65_sql.erl b/src/mod_proxy65_sql.erl new file mode 100644 index 000000000..c05f055b7 --- /dev/null +++ b/src/mod_proxy65_sql.erl @@ -0,0 +1,159 @@ +%%%------------------------------------------------------------------- +%%% Author : Evgeny Khramtsov +%%% Created : 30 Mar 2017 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_proxy65_sql). +-behaviour(mod_proxy65). + + +%% API +-export([init/0, register_stream/2, unregister_stream/1, activate_stream/4]). +-export([sql_schemas/0]). + +-include("logger.hrl"). +-include("ejabberd_sql_pt.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init() -> + ejabberd_sql_schema:update_schema( + ejabberd_config:get_myname(), ?MODULE, sql_schemas()), + NodeS = erlang:atom_to_binary(node(), latin1), + ?DEBUG("Cleaning SQL 'proxy65' table...", []), + case ejabberd_sql:sql_query( + ejabberd_config:get_myname(), + ?SQL("delete from proxy65 where " + "node_i=%(NodeS)s or node_t=%(NodeS)s")) of + {updated, _} -> + ok; + Err -> + ?ERROR_MSG("Failed to clean 'proxy65' table: ~p", [Err]), + Err + end. + +sql_schemas() -> + [#sql_schema{ + version = 1, + tables = + [#sql_table{ + name = <<"proxy65">>, + columns = + [#sql_column{name = <<"sid">>, type = text}, + #sql_column{name = <<"pid_t">>, type = text}, + #sql_column{name = <<"pid_i">>, type = text}, + #sql_column{name = <<"node_t">>, type = text}, + #sql_column{name = <<"node_i">>, type = text}, + #sql_column{name = <<"jid_i">>, type = text}], + indices = [#sql_index{ + columns = [<<"sid">>], + unique = true}, + #sql_index{ + columns = [<<"jid_i">>]}]}]}]. + +register_stream(SID, Pid) -> + PidS = misc:encode_pid(Pid), + NodeS = erlang:atom_to_binary(node(Pid), latin1), + F = fun() -> + case ejabberd_sql:sql_query_t( + ?SQL("update proxy65 set pid_i=%(PidS)s, " + "node_i=%(NodeS)s where sid=%(SID)s")) of + {updated, 1} -> + ok; + _ -> + ejabberd_sql:sql_query_t( + ?SQL("insert into proxy65" + "(sid, pid_t, node_t, pid_i, node_i, jid_i) " + "values (%(SID)s, %(PidS)s, %(NodeS)s, '', '', '')")) + end + end, + case ejabberd_sql:sql_transaction(ejabberd_config:get_myname(), F) of + {atomic, _} -> + ok; + {aborted, Reason} -> + {error, Reason} + end. + +unregister_stream(SID) -> + F = fun() -> + ejabberd_sql:sql_query_t( + ?SQL("delete from proxy65 where sid=%(SID)s")) + end, + case ejabberd_sql:sql_transaction(ejabberd_config:get_myname(), F) of + {atomic, _} -> + ok; + {aborted, Reason} -> + {error, Reason} + end. + +activate_stream(SID, IJID, MaxConnections, _Node) -> + F = fun() -> + case ejabberd_sql:sql_query_t( + ?SQL("select @(pid_t)s, @(node_t)s, @(pid_i)s, " + "@(node_i)s, @(jid_i)s from proxy65 where " + "sid=%(SID)s")) of + {selected, [{TPidS, TNodeS, IPidS, INodeS, <<"">>}]} + when IPidS /= <<"">> -> + try {misc:decode_pid(TPidS, TNodeS), + misc:decode_pid(IPidS, INodeS)} of + {TPid, IPid} -> + case ejabberd_sql:sql_query_t( + ?SQL("update proxy65 set jid_i=%(IJID)s " + "where sid=%(SID)s")) of + {updated, 1} when is_integer(MaxConnections) -> + case ejabberd_sql:sql_query_t( + ?SQL("select @(count(*))d from proxy65 " + "where jid_i=%(IJID)s")) of + {selected, [{Num}]} when Num > MaxConnections -> + ejabberd_sql:abort({limit, IPid, TPid}); + {selected, _} -> + {ok, IPid, TPid}; + Err -> + ejabberd_sql:abort(Err) + end; + {updated, _} -> + {ok, IPid, TPid}; + Err -> + ejabberd_sql:abort(Err) + end + catch _:{bad_node, _} -> + {error, notfound} + end; + {selected, [{_, _, _, _, JID}]} when JID /= <<"">> -> + {error, conflict}; + {selected, _} -> + {error, notfound}; + Err -> + ejabberd_sql:abort(Err) + end + end, + case ejabberd_sql:sql_transaction(ejabberd_config:get_myname(), F) of + {atomic, Result} -> + Result; + {aborted, {limit, _, _} = Limit} -> + {error, Limit}; + {aborted, Reason} -> + {error, Reason} + end. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== diff --git a/src/mod_proxy65_stream.erl b/src/mod_proxy65_stream.erl index 663fbf729..c12c67b63 100644 --- a/src/mod_proxy65_stream.erl +++ b/src/mod_proxy65_stream.erl @@ -4,7 +4,7 @@ %%% Purpose : Bytestream process. %%% Created : 12 Oct 2006 by Evgeniy Khramtsov %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -26,24 +26,23 @@ -author('xram@jabber.ru'). --behaviour(gen_fsm). +-behaviour(p1_fsm). +-behaviour(ejabberd_listener). %% gen_fsm callbacks. -export([init/1, handle_event/3, handle_sync_event/4, code_change/4, handle_info/3, terminate/3]). %% gen_fsm states. --export([wait_for_init/2, wait_for_auth/2, +-export([accepting/2, wait_for_init/2, wait_for_auth/2, wait_for_request/2, wait_for_activation/2, stream_established/2]). -%% API. --export([start/2, stop/1, start_link/3, activate/2, - relay/3, socket_type/0]). +-export([start/3, stop/1, start_link/3, activate/2, + relay/3, accept/1, listen_options/0]). -include("mod_proxy65.hrl"). --include("ejabberd.hrl"). -include("logger.hrl"). -define(WAIT_TIMEOUT, 60000). @@ -54,7 +53,7 @@ sha1 = <<"">> :: binary(), host = <<"">> :: binary(), auth_type = anonymous :: plain | anonymous, - shaper = none :: shaper:shaper()}). + shaper = none :: ejabberd_shaper:shaper()}). %% Unused callbacks handle_event(_Event, StateName, StateData) -> @@ -65,66 +64,54 @@ code_change(_OldVsn, StateName, StateData, _Extra) -> %%------------------------------- -start({gen_tcp, Socket}, Opts1) -> - {[Host], Opts} = lists:partition(fun (O) -> is_binary(O) - end, - Opts1), - Supervisor = gen_mod:get_module_proc(Host, - ejabberd_mod_proxy65_sup), - supervisor:start_child(Supervisor, - [Socket, Host, Opts]). +start(gen_tcp, Socket, Opts) -> + Host = proplists:get_value(server_host, Opts), + p1_fsm:start(?MODULE, [Socket, Host], []). -start_link(Socket, Host, Opts) -> - gen_fsm:start_link(?MODULE, [Socket, Host, Opts], []). +start_link(gen_tcp, Socket, Opts) -> + Host = proplists:get_value(server_host, Opts), + p1_fsm:start_link(?MODULE, [Socket, Host], []). -init([Socket, Host, Opts]) -> +init([Socket, Host]) -> process_flag(trap_exit, true), - AuthType = gen_mod:get_opt(auth_type, Opts, - fun(plain) -> plain; - (anonymous) -> anonymous - end, anonymous), - Shaper = gen_mod:get_opt(shaper, Opts, - fun(A) when is_atom(A) -> A end, - none), - RecvBuf = gen_mod:get_opt(recbuf, Opts, - fun(I) when is_integer(I), I>0 -> I end, - 8192), - SendBuf = gen_mod:get_opt(sndbuf, Opts, - fun(I) when is_integer(I), I>0 -> I end, - 8192), + AuthType = mod_proxy65_opt:auth_type(Host), + Shaper = mod_proxy65_opt:shaper(Host), + RecvBuf = mod_proxy65_opt:recbuf(Host), + SendBuf = mod_proxy65_opt:sndbuf(Host), TRef = erlang:send_after(?WAIT_TIMEOUT, self(), stop), - inet:setopts(Socket, - [{active, true}, {recbuf, RecvBuf}, {sndbuf, SendBuf}]), - {ok, wait_for_init, + inet:setopts(Socket, [{recbuf, RecvBuf}, {sndbuf, SendBuf}]), + {ok, accepting, #state{host = Host, auth_type = AuthType, socket = Socket, shaper = Shaper, timer = TRef}}. terminate(_Reason, StateName, #state{sha1 = SHA1}) -> - catch mod_proxy65_sm:unregister_stream(SHA1), + Mod = gen_mod:ram_db_mod(global, mod_proxy65), + Mod:unregister_stream(SHA1), if StateName == stream_established -> - ?INFO_MSG("Bytestream terminated", []); + ?INFO_MSG("(~w) Bytestream terminated", [self()]); true -> ok end. %%%------------------------------ %%% API. %%%------------------------------ -socket_type() -> raw. +accept(StreamPid) -> + p1_fsm:send_event(StreamPid, accept). stop(StreamPid) -> StreamPid ! stop. activate({P1, J1}, {P2, J2}) -> - case catch {gen_fsm:sync_send_all_state_event(P1, + case catch {p1_fsm:sync_send_all_state_event(P1, get_socket), - gen_fsm:sync_send_all_state_event(P2, get_socket)} + p1_fsm:sync_send_all_state_event(P2, get_socket)} of {S1, S2} when is_port(S1), is_port(S2) -> P1 ! {activate, P2, S2, J1, J2}, P2 ! {activate, P1, S1, J1, J2}, - JID1 = jlib:jid_to_string(J1), - JID2 = jlib:jid_to_string(J2), - ?INFO_MSG("(~w:~w) Activated bytestream for ~s " - "-> ~s", + JID1 = jid:encode(J1), + JID2 = jid:encode(J2), + ?INFO_MSG("(~w:~w) Activated bytestream for ~ts " + "-> ~ts", [P1, P2, JID1, JID2]), ok; _ -> error @@ -133,6 +120,10 @@ activate({P1, J1}, {P2, J2}) -> %%%----------------------- %%% States %%%----------------------- +accepting(accept, State) -> + inet:setopts(State#state.socket, [{active, true}]), + {next_state, wait_for_init, State}. + wait_for_init(Packet, #state{socket = Socket, auth_type = AuthType} = StateData) -> @@ -154,7 +145,7 @@ wait_for_auth(Packet, #state{socket = Socket, host = Host} = StateData) -> case mod_proxy65_lib:unpack_auth_request(Packet) of {User, Pass} -> - Result = ejabberd_auth:check_password(User, Host, Pass), + Result = ejabberd_auth:check_password(User, <<"">>, Host, Pass), gen_tcp:send(Socket, mod_proxy65_lib:make_auth_reply(Result)), case Result of @@ -169,8 +160,9 @@ wait_for_request(Packet, Request = mod_proxy65_lib:unpack_request(Packet), case Request of #s5_request{sha1 = SHA1, cmd = connect} -> - case catch mod_proxy65_sm:register_stream(SHA1) of - {atomic, ok} -> + Mod = gen_mod:ram_db_mod(global, mod_proxy65), + case Mod:register_stream(SHA1, self()) of + ok -> inet:setopts(Socket, [{active, false}]), gen_tcp:send(Socket, mod_proxy65_lib:make_reply(Request)), @@ -202,15 +194,15 @@ stream_established(_Data, StateData) -> %% SOCKS5 packets. handle_info({tcp, _S, Data}, StateName, StateData) when StateName /= wait_for_activation -> - erlang:cancel_timer(StateData#state.timer), + misc:cancel_timer(StateData#state.timer), TRef = erlang:send_after(?WAIT_TIMEOUT, self(), stop), - gen_fsm:send_event(self(), Data), + p1_fsm:send_event(self(), Data), {next_state, StateName, StateData#state{timer = TRef}}; %% Activation message. handle_info({activate, PeerPid, PeerSocket, IJid, TJid}, wait_for_activation, StateData) -> erlang:monitor(process, PeerPid), - erlang:cancel_timer(StateData#state.timer), + misc:cancel_timer(StateData#state.timer), MySocket = StateData#state.socket, Shaper = StateData#state.shaper, Host = StateData#state.host, @@ -252,14 +244,19 @@ handle_sync_event(_Event, _From, StateName, %%%------------------------------------------------- relay(MySocket, PeerSocket, Shaper) -> case gen_tcp:recv(MySocket, 0) of - {ok, Data} -> - gen_tcp:send(PeerSocket, Data), - {NewShaper, Pause} = shaper:update(Shaper, byte_size(Data)), - if Pause > 0 -> timer:sleep(Pause); - true -> pass - end, - relay(MySocket, PeerSocket, NewShaper); - _ -> stopped + {ok, Data} -> + case gen_tcp:send(PeerSocket, Data) of + ok -> + {NewShaper, Pause} = ejabberd_shaper:update(Shaper, byte_size(Data)), + if Pause > 0 -> timer:sleep(Pause); + true -> pass + end, + relay(MySocket, PeerSocket, NewShaper); + {error, _} = Err -> + Err + end; + {error, _} = Err -> + Err end. %%%------------------------ @@ -278,14 +275,13 @@ select_auth_method(anonymous, AuthMethods) -> %% Obviously, we must use shaper with maximum rate. find_maxrate(Shaper, JID1, JID2, Host) -> - MaxRate1 = case acl:match_rule(Host, Shaper, JID1) of - deny -> none; - R1 -> shaper:new(R1) - end, - MaxRate2 = case acl:match_rule(Host, Shaper, JID2) of - deny -> none; - R2 -> shaper:new(R2) - end, - if MaxRate1 == none; MaxRate2 == none -> none; - true -> lists:max([MaxRate1, MaxRate2]) - end. + R1 = ejabberd_shaper:match(Host, Shaper, JID1), + R2 = ejabberd_shaper:match(Host, Shaper, JID2), + R = case ejabberd_shaper:get_max_rate(R1) >= ejabberd_shaper:get_max_rate(R2) of + true -> R1; + false -> R2 + end, + ejabberd_shaper:new(R). + +listen_options() -> + []. diff --git a/src/mod_pubsub.erl b/src/mod_pubsub.erl index 3f4a4d7ec..bd0aff20f 100644 --- a/src/mod_pubsub.erl +++ b/src/mod_pubsub.erl @@ -1,40 +1,28 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. +%%%---------------------------------------------------------------------- +%%% File : mod_pubsub.erl +%%% Author : Christophe Romain +%%% Purpose : Publish Subscribe service (XEP-0060) +%%% Created : 1 Dec 2007 by Christophe Romain %%% %%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. +%%% ejabberd, Copyright (C) 2002-2025 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. %%% -%%% The Initial Developer of the Original Code is ProcessOne. -%%% Portions created by ProcessOne are Copyright 2006-2015, ProcessOne -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2015, ProcessOne. +%%% 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. %%% -%%% @copyright 2006-2015 ProcessOne -%%% @author Christophe Romain -%%% [http://www.process-one.net/] -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== +%%% 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. +%%% +%%%---------------------------------------------------------------------- -%%% @doc The module {@module} is the core of the PubSub -%%% extension. It relies on PubSub plugins for a large part of its functions. -%%% -%%% @headerfile "pubsub.hrl" -%%% -%%% @reference See XEP-0060: Pubsub for -%%% the latest version of the PubSub specification. -%%% This module uses version 1.12 of the specification as a base. -%%% Most of the specification is implemented. -%%% Functions concerning configuration should be rewritten. -%%% %%% Support for subscription-options and multi-subscribe features was %%% added by Brian Cully (bjc AT kublai.com). Subscriptions and options are %%% stored in the pubsub_subscription table, with a link to them provided @@ -44,79 +32,74 @@ %%% XEP-0060 section 12.18. -module(mod_pubsub). - --author('christophe.romain@process-one.net'). - --version('1.13-0'). - --behaviour(gen_server). - -behaviour(gen_mod). +-behaviour(gen_server). +-author('christophe.romain@process-one.net'). +-protocol({xep, 48, '1.2', '0.5.0', "complete", ""}). +-protocol({xep, 60, '1.14', '0.5.0', "partial", ""}). +-protocol({xep, 163, '1.2', '2.0.0', "complete", ""}). +-protocol({xep, 223, '1.1.1', '2.0.0', "complete", ""}). +-protocol({xep, 248, '0.2', '2.1.0', "complete", ""}). --include("ejabberd.hrl"). -include("logger.hrl"). - --include("adhoc.hrl"). - --include("jlib.hrl"). - +-include_lib("xmpp/include/xmpp.hrl"). -include("pubsub.hrl"). +-include("mod_roster.hrl"). +-include("translate.hrl"). + +-include("ejabberd_commands.hrl"). -define(STDTREE, <<"tree">>). - -define(STDNODE, <<"flat">>). - -define(PEPNODE, <<"pep">>). %% exports for hooks --export([presence_probe/3, caps_update/3, - in_subscription/6, out_subscription/4, - on_user_offline/3, remove_user/2, - disco_local_identity/5, disco_local_features/5, - disco_local_items/5, disco_sm_identity/5, - disco_sm_features/5, disco_sm_items/5, - drop_pep_error/4]). +-export([presence_probe/3, caps_add/3, caps_update/3, + in_subscription/2, out_subscription/1, + on_self_presence/1, on_user_offline/2, remove_user/2, + disco_local_identity/5, disco_local_features/5, + disco_local_items/5, disco_sm_identity/5, + disco_sm_features/5, disco_sm_items/5, + c2s_handle_info/2]). %% exported iq handlers --export([iq_sm/3]). +-export([iq_sm/1, process_disco_info/1, process_disco_items/1, + process_pubsub/1, process_pubsub_owner/1, process_vcard/1, + process_commands/1]). %% exports for console debug manual use --export([create_node/5, - delete_node/3, - subscribe_node/5, - unsubscribe_node/5, - publish_item/6, - delete_item/4, - send_items/6, - get_items/2, - get_item/3, - get_cached_item/2, - broadcast_stanza/9, - get_configure/5, - set_configure/5, - tree_action/3, - node_action/4 - ]). +-export([create_node/5, create_node/7, delete_node/3, + subscribe_node/5, unsubscribe_node/5, publish_item/6, publish_item/8, + delete_item/4, delete_item/5, send_items/7, get_items/2, get_item/3, + get_cached_item/2, get_configure/5, set_configure/5, + tree_action/3, node_action/4, node_call/4]). %% general helpers for plugins --export([subscription_to_string/1, affiliation_to_string/1, - string_to_subscription/1, string_to_affiliation/1, - extended_error/2, extended_error/3, - rename_default_nodeplugin/0]). +-export([extended_error/2, service_jid/1, + tree/1, tree/2, plugin/2, plugins/1, config/3, + host/1, serverhost/1]). + +%% pubsub#errors +-export([err_closed_node/0, err_configuration_required/0, + err_invalid_jid/0, err_invalid_options/0, err_invalid_payload/0, + err_invalid_subid/0, err_item_forbidden/0, err_item_required/0, + err_jid_required/0, err_max_items_exceeded/0, err_max_nodes_exceeded/0, + err_nodeid_required/0, err_not_in_roster_group/0, err_not_subscribed/0, + err_payload_too_big/0, err_payload_required/0, + err_pending_subscription/0, err_precondition_not_met/0, + err_presence_subscription_required/0, err_subid_required/0, + err_too_many_subscriptions/0, err_unsupported/1, + err_unsupported_access_model/0]). %% API and gen_server callbacks --export([start_link/2, start/2, stop/1, init/1, - handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). +-export([start/2, stop/1, init/1, + handle_call/3, handle_cast/2, handle_info/2, mod_doc/0, + terminate/2, code_change/3, depends/2, mod_opt_type/1, mod_options/1]). -%% calls for parallel sending of last items --export([send_loop/1]). +%% ejabberd commands +-export([get_commands_spec/0, delete_old_items/1, delete_expired_items/0]). --export([export/1]). - --define(PROCNAME, ejabberd_mod_pubsub). - --define(LOOPNAME, ejabberd_mod_pubsub_loop). +-export([route/1]). %%==================================================================== %% API @@ -125,134 +108,131 @@ %% Function: start_link() -> {ok,Pid} | ignore | {error,Error} %% Description: Starts the server %%-------------------------------------------------------------------- --define(PLUGIN_PREFIX, <<"node_">>). --define(TREE_PREFIX, <<"nodetree_">>). - -% -export_type([ - host/0, - hostPubsub/0, - hostPEP/0, - %% - nodeIdx/0, - nodeId/0, - itemId/0, - subId/0, - payload/0, - %% - nodeOption/0, - nodeOptions/0, - subOption/0, - subOptions/0, - %% - affiliation/0, - subscription/0, - accessModel/0, - publishModel/0 -]). + host/0, + hostPubsub/0, + hostPEP/0, + %% + nodeIdx/0, + nodeId/0, + itemId/0, + subId/0, + payload/0, + %% + nodeOption/0, + nodeOptions/0, + subOption/0, + subOptions/0, + pubOption/0, + pubOptions/0, + %% + affiliation/0, + subscription/0, + accessModel/0, + publishModel/0 + ]). %% -type payload() defined here because the -type xmlel() is not accessible %% from pubsub.hrl -type(payload() :: [] | [xmlel(),...]). -export_type([ - pubsubNode/0, - pubsubState/0, - pubsubItem/0, - pubsubSubscription/0, - pubsubLastItem/0 -]). + pubsubNode/0, + pubsubState/0, + pubsubItem/0, + pubsubSubscription/0, + pubsubLastItem/0 + ]). -type(pubsubNode() :: #pubsub_node{ - nodeid :: {Host::mod_pubsub:host(), NodeId::mod_pubsub:nodeId()}, - id :: mod_pubsub:nodeIdx(), - parents :: [Parent_NodeId::mod_pubsub:nodeId()], - type :: binary(), - owners :: [Owner::ljid(),...], - options :: mod_pubsub:nodeOptions() - } -). + nodeid :: {Host::mod_pubsub:host(), Node::mod_pubsub:nodeId()}, + id :: Nidx::mod_pubsub:nodeIdx(), + parents :: [Node::mod_pubsub:nodeId()], + type :: Type::binary(), + owners :: [Owner::ljid(),...], + options :: Opts::mod_pubsub:nodeOptions() + } + ). -type(pubsubState() :: #pubsub_state{ - stateid :: {Entity::ljid(), NodeIdx::mod_pubsub:nodeIdx()}, - items :: [ItemId::mod_pubsub:itemId()], - affiliation :: mod_pubsub:affiliation(), - subscriptions :: [{mod_pubsub:subscription(), mod_pubsub:subId()}] - } -). + stateid :: {Entity::ljid(), Nidx::mod_pubsub:nodeIdx()}, + nodeidx :: Nidx::mod_pubsub:nodeIdx(), + items :: [ItemId::mod_pubsub:itemId()], + affiliation :: Affs::mod_pubsub:affiliation(), + subscriptions :: [{Sub::mod_pubsub:subscription(), SubId::mod_pubsub:subId()}] + } + ). -type(pubsubItem() :: #pubsub_item{ - itemid :: {mod_pubsub:itemId(), mod_pubsub:nodeIdx()}, - creation :: {erlang:timestamp(), ljid()}, - modification :: {erlang:timestamp(), ljid()}, - payload :: mod_pubsub:payload() - } -). + itemid :: {ItemId::mod_pubsub:itemId(), Nidx::mod_pubsub:nodeIdx()}, + nodeidx :: Nidx::mod_pubsub:nodeIdx(), + creation :: {erlang:timestamp(), ljid()}, + modification :: {erlang:timestamp(), ljid()}, + payload :: mod_pubsub:payload() + } + ). -type(pubsubSubscription() :: #pubsub_subscription{ - subid :: mod_pubsub:subId(), - options :: [] | mod_pubsub:subOptions() - } -). + subid :: SubId::mod_pubsub:subId(), + options :: [] | mod_pubsub:subOptions() + } + ). -type(pubsubLastItem() :: #pubsub_last_item{ - nodeid :: mod_pubsub:nodeIdx(), - itemid :: mod_pubsub:itemId(), - creation :: {erlang:timestamp(), ljid()}, - payload :: mod_pubsub:payload() - } -). + nodeid :: {binary(), mod_pubsub:nodeIdx()}, + itemid :: mod_pubsub:itemId(), + creation :: {erlang:timestamp(), ljid()}, + payload :: mod_pubsub:payload() + } + ). -record(state, -{ - server_host, - host, - access, - pep_mapping = [], - ignore_pep_from_offline = true, - last_item_cache = false, - max_items_node = ?MAXITEMS, - nodetree = ?STDTREE, - plugins = [?STDNODE] -}). + { + server_host, + hosts, + access, + pep_mapping = [], + ignore_pep_from_offline = true, + last_item_cache = false, + max_items_node = ?MAXITEMS, + max_subscriptions_node = undefined, + default_node_config = [], + nodetree = <<"nodetree_", (?STDTREE)/binary>>, + plugins = [?STDNODE], + db_type + }). -type(state() :: #state{ - server_host :: binary(), - host :: mod_pubsub:hostPubsub(), - access :: atom(), - pep_mapping :: [{binary(), binary()}], - ignore_pep_from_offline :: boolean(), - last_item_cache :: boolean(), - max_items_node :: non_neg_integer(), - nodetree :: binary(), - plugins :: [binary(),...] - } + server_host :: binary(), + hosts :: [mod_pubsub:hostPubsub()], + access :: atom(), + pep_mapping :: [{binary(), binary()}], + ignore_pep_from_offline :: boolean(), + last_item_cache :: boolean(), + max_items_node :: non_neg_integer()|unlimited, + max_subscriptions_node :: non_neg_integer()|undefined, + default_node_config :: [{atom(), binary()|boolean()|integer()|atom()}], + nodetree :: binary(), + plugins :: [binary(),...], + db_type :: atom() + } -). + ). - -start_link(Host, Opts) -> - Proc = gen_mod:get_module_proc(Host, ?PROCNAME), - gen_server:start_link({local, Proc}, ?MODULE, - [Host, Opts], []). +-type subs_by_depth() :: [{integer(), [{#pubsub_node{}, [{ljid(), subId(), subOptions()}]}]}]. start(Host, Opts) -> - Proc = gen_mod:get_module_proc(Host, ?PROCNAME), - ChildSpec = {Proc, {?MODULE, start_link, [Host, Opts]}, - transient, 1000, worker, [?MODULE]}, - supervisor:start_child(ejabberd_sup, ChildSpec). + gen_mod:start_child(?MODULE, Host, Opts). stop(Host) -> - Proc = gen_mod:get_module_proc(Host, ?PROCNAME), - gen_server:call(Proc, stop), - supervisor:delete_child(ejabberd_sup, Proc). + gen_mod:stop_child(?MODULE, Host). %%==================================================================== %% gen_server callbacks @@ -260,1171 +240,493 @@ stop(Host) -> %%-------------------------------------------------------------------- %% Function: init(Args) -> {ok, State} | -%% {ok, State, Timeout} | -%% ignore | -%% {stop, Reason} +%% {ok, State, Timeout} | +%% ignore | +%% {stop, Reason} %% Description: Initiates the server %%-------------------------------------------------------------------- --spec(init/1 :: -( - _:: _) - -> {ok, state()} -). +-spec init([binary() | [{_,_}],...]) -> {'ok',state()}. -init([ServerHost, Opts]) -> - ?DEBUG("pubsub init ~p ~p", [ServerHost, Opts]), - Host = gen_mod:get_opt_host(ServerHost, Opts, <<"pubsub.@HOST@">>), - Access = gen_mod:get_opt(access_createnode, Opts, - fun(A) when is_atom(A) -> A end, all), - PepOffline = gen_mod:get_opt(ignore_pep_from_offline, Opts, - fun(A) when is_boolean(A) -> A end, true), - IQDisc = gen_mod:get_opt(iqdisc, Opts, - fun(A) when is_atom(A) -> A end, one_queue), - LastItemCache = gen_mod:get_opt(last_item_cache, Opts, - fun(A) when is_boolean(A) -> A end, false), - MaxItemsNode = gen_mod:get_opt(max_items_node, Opts, - fun(A) when is_integer(A) andalso A >= 0 -> A end, ?MAXITEMS), - pubsub_index:init(Host, ServerHost, Opts), - ets:new(gen_mod:get_module_proc(Host, config), - [set, named_table]), - ets:new(gen_mod:get_module_proc(ServerHost, config), - [set, named_table]), - {Plugins, NodeTree, PepMapping} = init_plugins(Host, - ServerHost, Opts), - mnesia:create_table(pubsub_last_item, - [{ram_copies, [node()]}, - {attributes, record_info(fields, pubsub_last_item)}]), - mod_disco:register_feature(ServerHost, ?NS_PUBSUB), - ets:insert(gen_mod:get_module_proc(Host, config), - {nodetree, NodeTree}), - ets:insert(gen_mod:get_module_proc(Host, config), - {plugins, Plugins}), - ets:insert(gen_mod:get_module_proc(Host, config), - {last_item_cache, LastItemCache}), - ets:insert(gen_mod:get_module_proc(Host, config), - {max_items_node, MaxItemsNode}), - ets:insert(gen_mod:get_module_proc(ServerHost, config), - {nodetree, NodeTree}), - ets:insert(gen_mod:get_module_proc(ServerHost, config), - {plugins, Plugins}), - ets:insert(gen_mod:get_module_proc(ServerHost, config), - {last_item_cache, LastItemCache}), - ets:insert(gen_mod:get_module_proc(ServerHost, config), - {max_items_node, MaxItemsNode}), - ets:insert(gen_mod:get_module_proc(ServerHost, config), - {pep_mapping, PepMapping}), - ets:insert(gen_mod:get_module_proc(ServerHost, config), - {ignore_pep_from_offline, PepOffline}), - ets:insert(gen_mod:get_module_proc(ServerHost, config), - {host, Host}), - ejabberd_hooks:add(sm_remove_connection_hook, - ServerHost, ?MODULE, on_user_offline, 75), +init([ServerHost|_]) -> + process_flag(trap_exit, true), + Opts = gen_mod:get_module_opts(ServerHost, ?MODULE), + Hosts = gen_mod:get_opt_hosts(Opts), + Access = mod_pubsub_opt:access_createnode(Opts), + PepOffline = mod_pubsub_opt:ignore_pep_from_offline(Opts), + LastItemCache = mod_pubsub_opt:last_item_cache(Opts), + MaxItemsNode = mod_pubsub_opt:max_items_node(Opts), + MaxSubsNode = mod_pubsub_opt:max_subscriptions_node(Opts), + ejabberd_mnesia:create(?MODULE, pubsub_last_item, + [{ram_copies, [node()]}, + {attributes, record_info(fields, pubsub_last_item)}]), + DBMod = gen_mod:db_mod(Opts, ?MODULE), + AllPlugins = + lists:flatmap( + fun(Host) -> + DBMod:init(Host, ServerHost, Opts), + ejabberd_router:register_route( + Host, ServerHost, {apply, ?MODULE, route}), + {Plugins, NodeTree, PepMapping} = init_plugins(Host, ServerHost, Opts), + DefaultNodeCfg = mod_pubsub_opt:default_node_config(Opts), + lists:foreach( + fun(H) -> + T = gen_mod:get_module_proc(H, config), + try + ets:new(T, [set, named_table]), + ets:insert(T, {nodetree, NodeTree}), + ets:insert(T, {plugins, Plugins}), + ets:insert(T, {last_item_cache, LastItemCache}), + ets:insert(T, {max_items_node, MaxItemsNode}), + ets:insert(T, {max_subscriptions_node, MaxSubsNode}), + ets:insert(T, {default_node_config, DefaultNodeCfg}), + ets:insert(T, {pep_mapping, PepMapping}), + ets:insert(T, {ignore_pep_from_offline, PepOffline}), + ets:insert(T, {host, Host}), + ets:insert(T, {access, Access}) + catch error:badarg when H == ServerHost -> + ok + end + end, [Host, ServerHost]), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO, + ?MODULE, process_disco_info), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_DISCO_ITEMS, + ?MODULE, process_disco_items), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_PUBSUB, + ?MODULE, process_pubsub), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_PUBSUB_OWNER, + ?MODULE, process_pubsub_owner), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_VCARD, + ?MODULE, process_vcard), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_COMMANDS, + ?MODULE, process_commands), + Plugins + end, Hosts), + ejabberd_hooks:add(c2s_self_presence, ServerHost, + ?MODULE, on_self_presence, 75), + ejabberd_hooks:add(c2s_terminated, ServerHost, + ?MODULE, on_user_offline, 75), ejabberd_hooks:add(disco_local_identity, ServerHost, - ?MODULE, disco_local_identity, 75), + ?MODULE, disco_local_identity, 75), ejabberd_hooks:add(disco_local_features, ServerHost, - ?MODULE, disco_local_features, 75), + ?MODULE, disco_local_features, 75), ejabberd_hooks:add(disco_local_items, ServerHost, - ?MODULE, disco_local_items, 75), + ?MODULE, disco_local_items, 75), ejabberd_hooks:add(presence_probe_hook, ServerHost, - ?MODULE, presence_probe, 80), + ?MODULE, presence_probe, 80), ejabberd_hooks:add(roster_in_subscription, ServerHost, - ?MODULE, in_subscription, 50), + ?MODULE, in_subscription, 50), ejabberd_hooks:add(roster_out_subscription, ServerHost, - ?MODULE, out_subscription, 50), - ejabberd_hooks:add(remove_user, ServerHost, ?MODULE, - remove_user, 50), - ejabberd_hooks:add(anonymous_purge_hook, ServerHost, - ?MODULE, remove_user, 50), - case lists:member(?PEPNODE, Plugins) of - true -> - ejabberd_hooks:add(caps_update, ServerHost, ?MODULE, - caps_update, 80), - ejabberd_hooks:add(disco_sm_identity, ServerHost, - ?MODULE, disco_sm_identity, 75), - ejabberd_hooks:add(disco_sm_features, ServerHost, - ?MODULE, disco_sm_features, 75), - ejabberd_hooks:add(disco_sm_items, ServerHost, ?MODULE, - disco_sm_items, 75), - ejabberd_hooks:add(c2s_filter_packet_in, ServerHost, ?MODULE, - drop_pep_error, 75), - gen_iq_handler:add_iq_handler(ejabberd_sm, ServerHost, - ?NS_PUBSUB, ?MODULE, iq_sm, IQDisc), - gen_iq_handler:add_iq_handler(ejabberd_sm, ServerHost, - ?NS_PUBSUB_OWNER, ?MODULE, iq_sm, - IQDisc); - false -> ok + ?MODULE, out_subscription, 50), + ejabberd_hooks:add(remove_user, ServerHost, + ?MODULE, remove_user, 50), + ejabberd_hooks:add(c2s_handle_info, ServerHost, + ?MODULE, c2s_handle_info, 50), + case lists:member(?PEPNODE, AllPlugins) of + true -> + ejabberd_hooks:add(caps_add, ServerHost, + ?MODULE, caps_add, 80), + ejabberd_hooks:add(caps_update, ServerHost, + ?MODULE, caps_update, 80), + ejabberd_hooks:add(disco_sm_identity, ServerHost, + ?MODULE, disco_sm_identity, 75), + ejabberd_hooks:add(disco_sm_features, ServerHost, + ?MODULE, disco_sm_features, 75), + ejabberd_hooks:add(disco_sm_items, ServerHost, + ?MODULE, disco_sm_items, 75), + gen_iq_handler:add_iq_handler(ejabberd_sm, ServerHost, + ?NS_PUBSUB, ?MODULE, iq_sm), + gen_iq_handler:add_iq_handler(ejabberd_sm, ServerHost, + ?NS_PUBSUB_OWNER, ?MODULE, iq_sm); + false -> + ok end, - ejabberd_router:register_route(Host), - update_node_database(Host, ServerHost), - update_state_database(Host, ServerHost), - update_item_database_binary(), - put(server_host, ServerHost), - init_nodes(Host, ServerHost, NodeTree, Plugins), - State = #state{host = Host, server_host = ServerHost, - access = Access, pep_mapping = PepMapping, - ignore_pep_from_offline = PepOffline, - last_item_cache = LastItemCache, - max_items_node = MaxItemsNode, nodetree = NodeTree, - plugins = Plugins}, - init_send_loop(ServerHost, State), - {ok, State}. + ejabberd_commands:register_commands(ServerHost, ?MODULE, get_commands_spec()), + NodeTree = config(ServerHost, nodetree), + Plugins = config(ServerHost, plugins), + PepMapping = config(ServerHost, pep_mapping), + DBType = mod_pubsub_opt:db_type(ServerHost), + {ok, #state{hosts = Hosts, server_host = ServerHost, + access = Access, pep_mapping = PepMapping, + ignore_pep_from_offline = PepOffline, + last_item_cache = LastItemCache, + max_items_node = MaxItemsNode, nodetree = NodeTree, + plugins = Plugins, db_type = DBType}}. -init_send_loop(ServerHost, State) -> - Proc = gen_mod:get_module_proc(ServerHost, ?LOOPNAME), - SendLoop = spawn(?MODULE, send_loop, [State]), - register(Proc, SendLoop), - SendLoop. +depends(ServerHost, Opts) -> + [Host|_] = gen_mod:get_opt_hosts(Opts), + Plugins = mod_pubsub_opt:plugins(Opts), + Db = mod_pubsub_opt:db_type(Opts), + lists:flatmap( + fun(Name) -> + Plugin = plugin(Db, Name), + try apply(Plugin, depends, [Host, ServerHost, Opts]) + catch _:undef -> [] + end + end, Plugins). -%% @spec (Host, ServerHost, Opts) -> Plugins -%% Host = mod_pubsub:host() Opts = [{Key,Value}] -%% ServerHost = host() -%% Key = atom() -%% Value = term() -%% Plugins = [Plugin::string()] %% @doc Call the init/1 function for each plugin declared in the config file. %% The default plugin module is implicit. %%

The Erlang code for the plugin is located in a module called %% node_plugin. The 'node_' prefix is mandatory.

-%%

The modules are initialized in alphetical order and the list is checked -%% and sorted to ensure that each module is initialized only once.

%%

See {@link node_hometree:init/1} for an example implementation.

init_plugins(Host, ServerHost, Opts) -> - TreePlugin = - jlib:binary_to_atom(<<(?TREE_PREFIX)/binary, - (gen_mod:get_opt(nodetree, Opts, fun(A) when is_binary(A) -> A end, - ?STDTREE))/binary>>), - ?DEBUG("** tree plugin is ~p", [TreePlugin]), + TreePlugin = tree(Host, mod_pubsub_opt:nodetree(Opts)), TreePlugin:init(Host, ServerHost, Opts), - Plugins = gen_mod:get_opt(plugins, Opts, - fun(A) when is_list(A) -> A end, [?STDNODE]), - PepMapping = gen_mod:get_opt(pep_mapping, Opts, - fun(A) when is_list(A) -> A end, []), - ?DEBUG("** PEP Mapping : ~p~n", [PepMapping]), - PluginsOK = lists:foldl(fun (Name, Acc) -> - Plugin = - jlib:binary_to_atom(<<(?PLUGIN_PREFIX)/binary, - Name/binary>>), - case catch apply(Plugin, init, - [Host, ServerHost, Opts]) - of - {'EXIT', _Error} -> Acc; - _ -> - ?DEBUG("** init ~s plugin", [Name]), - [Name | Acc] - end - end, - [], Plugins), + Plugins = mod_pubsub_opt:plugins(Opts), + PepMapping = mod_pubsub_opt:pep_mapping(Opts), + PluginsOK = lists:foldl( + fun (Name, Acc) -> + Plugin = plugin(Host, Name), + apply(Plugin, init, [Host, ServerHost, Opts]), + [Name | Acc] + end, + [], Plugins), {lists:reverse(PluginsOK), TreePlugin, PepMapping}. -terminate_plugins(Host, ServerHost, Plugins, - TreePlugin) -> - lists:foreach(fun (Name) -> - ?DEBUG("** terminate ~s plugin", [Name]), - Plugin = - jlib:binary_to_atom(<<(?PLUGIN_PREFIX)/binary, - Name/binary>>), - Plugin:terminate(Host, ServerHost) - end, - Plugins), +terminate_plugins(Host, ServerHost, Plugins, TreePlugin) -> + lists:foreach( + fun (Name) -> + Plugin = plugin(Host, Name), + Plugin:terminate(Host, ServerHost) + end, + Plugins), TreePlugin:terminate(Host, ServerHost), ok. -init_nodes(Host, ServerHost, _NodeTree, Plugins) -> - case lists:member(<<"hometree">>, Plugins) of - true -> - create_node(Host, ServerHost, <<"/home">>, service_jid(Host), <<"hometree">>), - create_node(Host, ServerHost, <<"/home/", ServerHost/binary>>, service_jid(Host), - <<"hometree">>); - false -> ok - end. - - -update_item_database_binary() -> - F = fun () -> - case catch mnesia:read({pubsub_last_item, mnesia:first(pubsub_last_item)}) of - [First] when is_list(First#pubsub_last_item.itemid) -> - ?INFO_MSG("Binarization of pubsub items table...", []), - lists:foreach(fun (Id) -> - [Node] = mnesia:read({pubsub_last_item, Id}), - - ItemId = iolist_to_binary(Node#pubsub_last_item.itemid), - - ok = mnesia:delete({pubsub_last_item, Id}), - ok = mnesia:write(Node#pubsub_last_item{itemid=ItemId}) - end, - mnesia:all_keys(pubsub_last_item)); - _-> no_need - end - end, - case mnesia:transaction(F) of - {aborted, Reason} -> - ?ERROR_MSG("Failed to binarize pubsub items table: ~p", [Reason]); - {atomic, no_need} -> - ok; - {atomic, Result} -> - ?INFO_MSG("Pubsub items table has been binarized: ~p", [Result]) - end. - - -update_node_database_binary() -> - F = fun () -> - case catch mnesia:read({pubsub_node, mnesia:first(pubsub_node)}) of - [First] when is_list(First#pubsub_node.type) -> - ?INFO_MSG("Binarization of pubsub nodes table...", []), - lists:foreach(fun ({H, N}) -> - [Node] = mnesia:read({pubsub_node, {H, N}}), - - Type = iolist_to_binary(Node#pubsub_node.type), - BN = case N of - Binary when is_binary(Binary) -> - N; - _ -> - {result, BN1} = node_call(Type, path_to_node, [N]), - BN1 - end, - BP = case [case P of - Binary2 when is_binary(Binary2) -> P; - _ -> element(2, node_call(Type, path_to_node, [P])) - end - || P <- Node#pubsub_node.parents] of - [<<>>] -> []; - Parents -> Parents - end, - - BH = case H of - {U, S, R} -> {iolist_to_binary(U), iolist_to_binary(S), iolist_to_binary(R)}; - String -> iolist_to_binary(String) - end, - - Owners = [{iolist_to_binary(U), iolist_to_binary(S), iolist_to_binary(R)} || - {U, S, R} <- Node#pubsub_node.owners], - - ok = mnesia:delete({pubsub_node, {H, N}}), - ok = mnesia:write(Node#pubsub_node{nodeid = {BH, BN}, - parents = BP, - type = Type, - owners = Owners}); - (_) -> ok - end, - mnesia:all_keys(pubsub_node)); - _-> no_need - end - end, - case mnesia:transaction(F) of - {aborted, Reason} -> - ?ERROR_MSG("Failed to binarize pubsub node table: ~p", [Reason]); - {atomic, no_need} -> - ok; - {atomic, Result} -> - ?INFO_MSG("Pubsub nodes table has been binarized: ~p", [Result]) - end. - -update_node_database(Host, ServerHost) -> - mnesia:del_table_index(pubsub_node, type), - mnesia:del_table_index(pubsub_node, parentid), - case catch mnesia:table_info(pubsub_node, attributes) of - [host_node, host_parent, info] -> - ?INFO_MSG("Upgrading pubsub nodes table...", []), - F = fun () -> - {Result, LastIdx} = lists:foldl(fun ({pubsub_node, - NodeId, ParentId, - {nodeinfo, Items, - Options, - Entities}}, - {RecList, - NodeIdx}) -> - ItemsList = - lists:foldl(fun - ({item, - IID, - Publisher, - Payload}, - Acc) -> - C = - {unknown, - Publisher}, - M = - {now(), - Publisher}, - mnesia:write(#pubsub_item{itemid - = - {IID, - NodeIdx}, - creation - = - C, - modification - = - M, - payload - = - Payload}), - [{Publisher, - IID} - | Acc] - end, - [], - Items), - Owners = - dict:fold(fun - (JID, - {entity, - Aff, - Sub}, - Acc) -> - UsrItems = - lists:foldl(fun - ({P, - I}, - IAcc) -> - case - P - of - JID -> - [I - | IAcc]; - _ -> - IAcc - end - end, - [], - ItemsList), - mnesia:write({pubsub_state, - {JID, - NodeIdx}, - UsrItems, - Aff, - Sub}), - case - Aff - of - owner -> - [JID - | Acc]; - _ -> - Acc - end - end, - [], - Entities), - mnesia:delete({pubsub_node, - NodeId}), - {[#pubsub_node{nodeid - = - NodeId, - id - = - NodeIdx, - parents - = - [element(2, - ParentId)], - owners - = - Owners, - options - = - Options} - | RecList], - NodeIdx + 1} - end, - {[], 1}, - mnesia:match_object({pubsub_node, - {Host, - '_'}, - '_', - '_'})), - mnesia:write(#pubsub_index{index = node, last = LastIdx, - free = []}), - Result - end, - {atomic, NewRecords} = mnesia:transaction(F), - {atomic, ok} = mnesia:delete_table(pubsub_node), - {atomic, ok} = mnesia:create_table(pubsub_node, - [{disc_copies, [node()]}, - {attributes, - record_info(fields, - pubsub_node)}]), - FNew = fun () -> - lists:foreach(fun (Record) -> mnesia:write(Record) end, - NewRecords) - end, - case mnesia:transaction(FNew) of - {atomic, Result} -> - ?INFO_MSG("Pubsub nodes table upgraded: ~p", - [Result]); - {aborted, Reason} -> - ?ERROR_MSG("Problem upgrading Pubsub nodes table:~n~p", - [Reason]) - end; - [nodeid, parentid, type, owners, options] -> - F = fun ({pubsub_node, NodeId, {_, Parent}, Type, - Owners, Options}) -> - #pubsub_node{nodeid = NodeId, id = 0, - parents = [Parent], type = Type, - owners = Owners, options = Options} - end, - mnesia:transform_table(pubsub_node, F, - [nodeid, id, parents, type, owners, options]), - FNew = fun () -> - LastIdx = lists:foldl(fun (#pubsub_node{nodeid = - NodeId} = - PubsubNode, - NodeIdx) -> - mnesia:write(PubsubNode#pubsub_node{id - = - NodeIdx}), - lists:foreach(fun - (#pubsub_state{stateid - = - StateId} = - State) -> - {JID, - _} = - StateId, - mnesia:delete({pubsub_state, - StateId}), - mnesia:write(State#pubsub_state{stateid - = - {JID, - NodeIdx}}) - end, - mnesia:match_object(#pubsub_state{stateid - = - {'_', - NodeId}, - _ - = - '_'})), - lists:foreach(fun - (#pubsub_item{itemid - = - ItemId} = - Item) -> - {IID, - _} = - ItemId, - {M1, - M2} = - Item#pubsub_item.modification, - {C1, - C2} = - Item#pubsub_item.creation, - mnesia:delete({pubsub_item, - ItemId}), - mnesia:write(Item#pubsub_item{itemid - = - {IID, - NodeIdx}, - modification - = - {M2, - M1}, - creation - = - {C2, - C1}}) - end, - mnesia:match_object(#pubsub_item{itemid - = - {'_', - NodeId}, - _ - = - '_'})), - NodeIdx + 1 - end, - 1, - mnesia:match_object({pubsub_node, - {Host, '_'}, - '_', '_', - '_', '_', - '_'}) - ++ - mnesia:match_object({pubsub_node, - {{'_', - ServerHost, - '_'}, - '_'}, - '_', '_', - '_', '_', - '_'})), - mnesia:write(#pubsub_index{index = node, - last = LastIdx, free = []}) - end, - case mnesia:transaction(FNew) of - {atomic, Result} -> - rename_default_nodeplugin(), - ?INFO_MSG("Pubsub nodes table upgraded: ~p", - [Result]); - {aborted, Reason} -> - ?ERROR_MSG("Problem upgrading Pubsub nodes table:~n~p", - [Reason]) - end; - [nodeid, id, parent, type, owners, options] -> - F = fun ({pubsub_node, NodeId, Id, Parent, Type, Owners, - Options}) -> - #pubsub_node{nodeid = NodeId, id = Id, - parents = [Parent], type = Type, - owners = Owners, options = Options} - end, - mnesia:transform_table(pubsub_node, F, - [nodeid, id, parents, type, owners, options]), - rename_default_nodeplugin(); - _ -> ok - end, - update_node_database_binary(). - -rename_default_nodeplugin() -> - lists:foreach(fun (Node) -> - mnesia:dirty_write(Node#pubsub_node{type = - <<"hometree">>}) - end, - mnesia:dirty_match_object(#pubsub_node{type = - <<"default">>, - _ = '_'})). - -update_state_database(_Host, _ServerHost) -> - case catch mnesia:table_info(pubsub_state, attributes) of - [stateid, nodeidx, items, affiliation, subscriptions] -> - ?INFO_MSG("Upgrading pubsub states table...", []), - F = fun ({pubsub_state, {{U,S,R}, NodeID}, _NodeIdx, Items, Aff, Sub}, Acc) -> - JID = {iolist_to_binary(U), iolist_to_binary(S), iolist_to_binary(R)}, - Subs = case Sub of - none -> - []; - [] -> - []; - _ -> - {result, SubID} = pubsub_subscription:subscribe_node(JID, NodeID, []), - [{Sub, SubID}] - end, - NewState = #pubsub_state{stateid = {JID, NodeID}, - items = Items, - affiliation = Aff, - subscriptions = Subs}, - [NewState | Acc] - end, - {atomic, NewRecs} = mnesia:transaction(fun mnesia:foldl/3, - [F, [], pubsub_state]), - {atomic, ok} = mnesia:delete_table(pubsub_state), - {atomic, ok} = mnesia:create_table(pubsub_state, - [{disc_copies, [node()]}, - {attributes, record_info(fields, pubsub_state)}]), - FNew = fun () -> - lists:foreach(fun mnesia:write/1, NewRecs) - end, - case mnesia:transaction(FNew) of - {atomic, Result} -> - ?INFO_MSG("Pubsub states table upgraded: ~p", - [Result]); - {aborted, Reason} -> - ?ERROR_MSG("Problem upgrading Pubsub states table:~n~p", - [Reason]) - end; - _ -> - ok - end. - -send_loop(State) -> - receive - {presence, JID, Pid} -> - Host = State#state.host, - ServerHost = State#state.server_host, - LJID = jlib:jid_tolower(JID), - BJID = jlib:jid_remove_resource(LJID), - lists:foreach(fun (PType) -> - {result, Subscriptions} = node_action(Host, - PType, - get_entity_subscriptions, - [Host, - JID]), - lists:foreach(fun ({Node, subscribed, _, - SubJID}) -> - if (SubJID == LJID) or - (SubJID == BJID) -> - #pubsub_node{nodeid - = - {H, - N}, - type = - Type, - id = - NodeId, - options - = - Options} = - Node, - case - get_option(Options, - send_last_published_item) - of - on_sub_and_presence -> - send_items(H, - N, - NodeId, - Type, - LJID, - last); - _ -> ok - end; - true -> - % resource not concerned about that subscription - ok - end; - (_) -> ok - end, - lists:usort(Subscriptions)) - end, - State#state.plugins), - if not State#state.ignore_pep_from_offline -> - {User, Server, Resource} = jlib:jid_tolower(JID), - case catch ejabberd_c2s:get_subscribed(Pid) of - Contacts when is_list(Contacts) -> - lists:foreach(fun ({U, S, R}) -> - case S of - ServerHost -> %% local contacts - case user_resources(U, S) of - [] -> %% offline - PeerJID = - jlib:make_jid(U, S, - R), - self() ! - {presence, User, - Server, [Resource], - PeerJID}; - _ -> %% online - % this is already handled by presence probe - ok - end; - _ -> %% remote contacts - % we can not do anything in any cases - ok - end - end, - Contacts); - _ -> ok - end; - true -> ok - end, - send_loop(State); - {presence, User, Server, Resources, JID} -> - spawn(fun () -> - Host = State#state.host, - Owner = jlib:jid_remove_resource(jlib:jid_tolower(JID)), - lists:foreach(fun (#pubsub_node{nodeid = {_, Node}, - type = Type, - id = NodeId, - options = Options}) -> - case get_option(Options, - send_last_published_item) - of - on_sub_and_presence -> - lists:foreach(fun - (Resource) -> - LJID = - {User, - Server, - Resource}, - Subscribed = - case - get_option(Options, - access_model) - of - open -> - true; - presence -> - true; - whitelist -> - false; % subscribers are added manually - authorize -> - false; % likewise - roster -> - Grps = - get_option(Options, - roster_groups_allowed, - []), - {OU, - OS, - _} = - Owner, - element(2, - get_roster_info(OU, - OS, - LJID, - Grps)) - end, - if - Subscribed -> - send_items(Owner, - Node, - NodeId, - Type, - LJID, - last); - true -> - ok - end - end, - Resources); - _ -> ok - end - end, - tree_action(Host, get_nodes, - [Owner, JID])) - end), - send_loop(State); - stop -> ok - end. - %% ------- %% disco hooks handling functions %% --spec(disco_local_identity/5 :: -( - Acc :: [xmlel()], - _From :: jid(), - To :: jid(), - NodeId :: <<>> | mod_pubsub:nodeId(), - Lang :: binary()) - -> [xmlel()] -). +-spec disco_local_identity([identity()], jid(), jid(), + binary(), binary()) -> [identity()]. disco_local_identity(Acc, _From, To, <<>>, _Lang) -> - case lists:member(?PEPNODE, plugins(To#jid.lserver)) of - true -> - [#xmlel{name = <<"identity">>, - attrs = - [{<<"category">>, <<"pubsub">>}, - {<<"type">>, <<"pep">>}], - children = []} - | Acc]; - false -> Acc + case lists:member(?PEPNODE, plugins(host(To#jid.lserver))) of + true -> + [#identity{category = <<"pubsub">>, type = <<"pep">>} | Acc]; + false -> + Acc end; disco_local_identity(Acc, _From, _To, _Node, _Lang) -> Acc. --spec(disco_local_features/5 :: -( - Acc :: [xmlel()], - _From :: jid(), - To :: jid(), - NodeId :: <<>> | mod_pubsub:nodeId(), - Lang :: binary()) - -> [binary(),...] -). +-spec disco_local_features({error, stanza_error()} | {result, [binary()]} | empty, + jid(), jid(), binary(), binary()) -> + {error, stanza_error()} | {result, [binary()]} | empty. disco_local_features(Acc, _From, To, <<>>, _Lang) -> - Host = To#jid.lserver, + Host = host(To#jid.lserver), Feats = case Acc of - {result, I} -> I; - _ -> [] - end, - {result, - Feats ++ - lists:map(fun (Feature) -> - <<(?NS_PUBSUB)/binary, "#", Feature/binary>> - end, - features(Host, <<>>))}; + {result, I} -> I; + _ -> [] + end, + {result, Feats ++ [?NS_PUBSUB|[feature(F) || F <- features(Host, <<>>)]]}; disco_local_features(Acc, _From, _To, _Node, _Lang) -> Acc. +-spec disco_local_items({error, stanza_error()} | {result, [disco_item()]} | empty, + jid(), jid(), binary(), binary()) -> + {error, stanza_error()} | {result, [disco_item()]} | empty. disco_local_items(Acc, _From, _To, <<>>, _Lang) -> Acc; disco_local_items(Acc, _From, _To, _Node, _Lang) -> Acc. -%disco_sm_identity(Acc, From, To, Node, Lang) -% when is_binary(Node) -> -% disco_sm_identity(Acc, From, To, iolist_to_binary(Node), -% Lang); --spec(disco_sm_identity/5 :: -( - Acc :: empty | [xmlel()], - From :: jid(), - To :: jid(), - Node :: mod_pubsub:nodeId(), - Lang :: binary()) - -> [xmlel()] -). -disco_sm_identity(empty, From, To, Node, Lang) -> - disco_sm_identity([], From, To, Node, Lang); +-spec disco_sm_identity([identity()], jid(), jid(), + binary(), binary()) -> [identity()]. disco_sm_identity(Acc, From, To, Node, _Lang) -> - disco_identity(jlib:jid_tolower(jlib:jid_remove_resource(To)), Node, From) - ++ Acc. + disco_identity(jid:tolower(jid:remove_resource(To)), Node, From) + ++ Acc. +-spec disco_identity(host(), binary(), jid()) -> [identity()]. disco_identity(_Host, <<>>, _From) -> - [#xmlel{name = <<"identity">>, - attrs = - [{<<"category">>, <<"pubsub">>}, - {<<"type">>, <<"pep">>}], - children = []}]; + [#identity{category = <<"pubsub">>, type = <<"pep">>}]; disco_identity(Host, Node, From) -> - Action = fun (#pubsub_node{id = Idx, type = Type, - options = Options, owners = Owners}) -> - case get_allowed_items_call(Host, Idx, From, Type, Options, Owners) of - {result, _} -> - {result, - [#xmlel{name = <<"identity">>, - attrs = - [{<<"category">>, <<"pubsub">>}, - {<<"type">>, <<"pep">>}], - children = []}, - #xmlel{name = <<"identity">>, - attrs = - [{<<"category">>, <<"pubsub">>}, - {<<"type">>, <<"leaf">>} - | case get_option(Options, title) of - false -> []; - [Title] -> [{<<"name">>, Title}] - end], - children = []}]}; - _ -> {result, []} - end - end, + Action = + fun(#pubsub_node{id = Nidx, type = Type, + options = Options, owners = O}) -> + Owners = node_owners_call(Host, Type, Nidx, O), + case get_allowed_items_call(Host, Nidx, From, Type, + Options, Owners) of + {result, _} -> + {result, [#identity{category = <<"pubsub">>, type = <<"pep">>}, + #identity{category = <<"pubsub">>, type = <<"leaf">>, + name = get_option(Options, title, <<>>)}]}; + _ -> + {result, []} + end + end, case transaction(Host, Node, Action, sync_dirty) of - {result, {_, Result}} -> Result; - _ -> [] + {result, {_, Result}} -> Result; + _ -> [] end. --spec(disco_sm_features/5 :: -( - Acc :: empty | {result, Features::[Feature::binary()]}, - From :: jid(), - To :: jid(), - Node :: mod_pubsub:nodeId(), - Lang :: binary()) - -> {result, Features::[Feature::binary()]} -). -%disco_sm_features(Acc, From, To, Node, Lang) -% when is_binary(Node) -> -% disco_sm_features(Acc, From, To, iolist_to_binary(Node), -% Lang); +-spec disco_sm_features({error, stanza_error()} | {result, [binary()]} | empty, + jid(), jid(), binary(), binary()) -> + {error, stanza_error()} | {result, [binary()]}. disco_sm_features(empty, From, To, Node, Lang) -> disco_sm_features({result, []}, From, To, Node, Lang); disco_sm_features({result, OtherFeatures} = _Acc, From, To, Node, _Lang) -> {result, OtherFeatures ++ - disco_features(jlib:jid_tolower(jlib:jid_remove_resource(To)), Node, From)}; + disco_features(jid:tolower(jid:remove_resource(To)), Node, From)}; disco_sm_features(Acc, _From, _To, _Node, _Lang) -> Acc. -disco_features(_Host, <<>>, _From) -> - [?NS_PUBSUB | [<<(?NS_PUBSUB)/binary, "#", Feature/binary>> - || Feature <- features(<<"pep">>)]]; +-spec disco_features(ljid(), binary(), jid()) -> [binary()]. +disco_features(Host, <<>>, _From) -> + [?NS_PUBSUB | [feature(F) || F <- plugin_features(Host, <<"pep">>)]]; disco_features(Host, Node, From) -> - Action = fun (#pubsub_node{id = Idx, type = Type, - options = Options, owners = Owners}) -> - case get_allowed_items_call(Host, Idx, From, Type, Options, Owners) of - {result, _} -> - {result, - [?NS_PUBSUB | [<<(?NS_PUBSUB)/binary, "#", - Feature/binary>> - || Feature <- features(<<"pep">>)]]}; - _ -> {result, []} - end - end, + Action = + fun(#pubsub_node{id = Nidx, type = Type, + options = Options, owners = O}) -> + Owners = node_owners_call(Host, Type, Nidx, O), + case get_allowed_items_call(Host, Nidx, From, + Type, Options, Owners) of + {result, _} -> + {result, + [?NS_PUBSUB | [feature(F) || F <- plugin_features(Host, <<"pep">>)]]}; + _ -> + {result, []} + end + end, case transaction(Host, Node, Action, sync_dirty) of - {result, {_, Result}} -> Result; - _ -> [] + {result, {_, Result}} -> Result; + _ -> [] end. --spec(disco_sm_items/5 :: -( - Acc :: empty | {result, [xmlel()]}, - From :: jid(), - To :: jid(), - Node :: mod_pubsub:nodeId(), - Lang :: binary()) - -> {result, [xmlel()]} -). -%disco_sm_items(Acc, From, To, Node, Lang) -% when is_binary(Node) -> -% disco_sm_items(Acc, From, To, iolist_to_binary(Node), -% Lang); +-spec disco_sm_items({error, stanza_error()} | {result, [disco_item()]} | empty, + jid(), jid(), binary(), binary()) -> + {error, stanza_error()} | {result, [disco_item()]}. disco_sm_items(empty, From, To, Node, Lang) -> disco_sm_items({result, []}, From, To, Node, Lang); disco_sm_items({result, OtherItems}, From, To, Node, _Lang) -> - {result, - lists:usort(OtherItems ++ - disco_items(jlib:jid_tolower(jlib:jid_remove_resource(To)), Node, From))}; + {result, lists:usort(OtherItems ++ + disco_items(jid:tolower(jid:remove_resource(To)), Node, From))}; disco_sm_items(Acc, _From, _To, _Node, _Lang) -> Acc. --spec(disco_items/3 :: -( - Host :: mod_pubsub:host(), - Node :: mod_pubsub:nodeId(), - From :: jid()) - -> [xmlel()] -). +-spec disco_items(ljid(), binary(), jid()) -> [disco_item()]. disco_items(Host, <<>>, From) -> - Action = fun (#pubsub_node{nodeid = {_, NodeID}, - options = Options, type = Type, id = Idx, - owners = Owners}, - Acc) -> - case get_allowed_items_call(Host, Idx, From, Type, Options, Owners) of - {result, _} -> - [#xmlel{name = <<"item">>, - attrs = - [{<<"node">>, (NodeID)}, - {<<"jid">>, - case Host of - {_, _, _} -> - jlib:jid_to_string(Host); - _Host -> Host - end} - | case get_option(Options, title) of - false -> []; - [Title] -> [{<<"name">>, Title}] - end], - children = []} - | Acc]; - _ -> Acc - end - end, - case transaction(Host, Action, sync_dirty) of - {result, Items} -> Items; - _ -> [] + MaxNodes = mod_pubsub_opt:max_nodes_discoitems(serverhost(Host)), + Action = + fun(#pubsub_node{nodeid = {_, Node}, options = Options, + type = Type, id = Nidx, owners = O}, Acc) -> + Owners = node_owners_call(Host, Type, Nidx, O), + case get_allowed_items_call(Host, Nidx, From, + Type, Options, Owners) of + {result, _} -> + [#disco_item{node = Node, + jid = jid:make(Host), + name = get_option(Options, title, <<>>)} | Acc]; + _ -> + Acc + end + end, + NodeBloc = fun() -> + case tree_call(Host, get_nodes, [Host, MaxNodes]) of + Nodes when is_list(Nodes) -> + {result, lists:foldl(Action, [], Nodes)}; + Error -> + Error + end + end, + case transaction(Host, NodeBloc, sync_dirty) of + {result, Items} -> Items; + _ -> [] end; disco_items(Host, Node, From) -> - Action = fun (#pubsub_node{id = Idx, type = Type, - options = Options, owners = Owners}) -> - case get_allowed_items_call(Host, Idx, From, Type, - Options, Owners) - of - {result, Items} -> - {result, - [#xmlel{name = <<"item">>, - attrs = - [{<<"jid">>, - case Host of - {_, _, _} -> - jlib:jid_to_string(Host); - _Host -> Host - end}, - {<<"name">>, ItemID}], - children = []} - || #pubsub_item{itemid = {ItemID, _}} <- Items]}; - _ -> {result, []} - end - end, + Action = + fun(#pubsub_node{id = Nidx, type = Type, + options = Options, owners = O}) -> + Owners = node_owners_call(Host, Type, Nidx, O), + case get_allowed_items_call(Host, Nidx, From, + Type, Options, Owners) of + {result, Items} -> + {result, [#disco_item{jid = jid:make(Host), + name = ItemId} + || #pubsub_item{itemid = {ItemId, _}} <- Items]}; + _ -> + {result, []} + end + end, case transaction(Host, Node, Action, sync_dirty) of - {result, {_, Result}} -> Result; - _ -> [] + {result, {_, Result}} -> Result; + _ -> [] end. %% ------- -%% presence hooks handling functions +%% presence and session hooks handling functions %% -caps_update(#jid{luser = U, lserver = S, lresource = R}, #jid{lserver = Host} = JID, _Features) - when Host =/= S -> - presence(Host, {presence, U, S, [R], JID}); -caps_update(_From, _To, _Feature) -> +-spec caps_add(jid(), jid(), [binary()]) -> ok. +caps_add(JID, JID, Features) -> + %% Send the owner his last PEP items. + send_last_pep(JID, JID, Features); +caps_add(#jid{lserver = S1} = From, #jid{lserver = S2} = To, Features) + when S1 =/= S2 -> + %% When a remote contact goes online while the local user is offline, the + %% remote contact won't receive last items from the local user even if + %% ignore_pep_from_offline is set to false. To work around this issue a bit, + %% we'll also send the last items to remote contacts when the local user + %% connects. That's the reason to use the caps_add hook instead of the + %% presence_probe_hook for remote contacts: The latter is only called when a + %% contact becomes available; the former is also executed when the local + %% user goes online (because that triggers the contact to send a presence + %% packet with CAPS). + send_last_pep(To, From, Features); +caps_add(_From, _To, _Features) -> ok. -presence_probe(#jid{luser = U, lserver = S, lresource = R} = JID, JID, Pid) -> - presence(S, {presence, JID, Pid}), - presence(S, {presence, U, S, [R], JID}); +-spec caps_update(jid(), jid(), [binary()]) -> ok. +caps_update(From, To, Features) -> + send_last_pep(To, From, Features). + +-spec presence_probe(jid(), jid(), pid()) -> ok. presence_probe(#jid{luser = U, lserver = S}, #jid{luser = U, lserver = S}, _Pid) -> - %% ignore presence_probe from my other ressources - %% to not get duplicated last items + %% ignore presence_probe from my other resources ok; -presence_probe(#jid{luser = U, lserver = S, lresource = R}, #jid{lserver = S} = JID, _Pid) -> - presence(S, {presence, U, S, [R], JID}); -presence_probe(_Host, _JID, _Pid) -> - %% ignore presence_probe from remote contacts, - %% those are handled via caps_update +presence_probe(#jid{lserver = S} = From, #jid{lserver = S} = To, _Pid) -> + send_last_pep(To, From, unknown); +presence_probe(_From, _To, _Pid) -> + %% ignore presence_probe from remote contacts, those are handled via caps_add ok. -presence(ServerHost, Presence) -> - SendLoop = case - whereis(gen_mod:get_module_proc(ServerHost, ?LOOPNAME)) - of - undefined -> - Host = host(ServerHost), - Plugins = plugins(Host), - PepOffline = case catch - ets:lookup(gen_mod:get_module_proc(ServerHost, - config), - ignore_pep_from_offline) - of - [{ignore_pep_from_offline, PO}] -> PO; - _ -> true - end, - State = #state{host = Host, server_host = ServerHost, - ignore_pep_from_offline = PepOffline, - plugins = Plugins}, - init_send_loop(ServerHost, State); - Pid -> Pid - end, - SendLoop ! Presence. +-spec on_self_presence({presence(), ejabberd_c2s:state()}) + -> {presence(), ejabberd_c2s:state()}. +on_self_presence({_, #{pres_last := _}} = Acc) -> % Just a presence update. + Acc; +on_self_presence({#presence{type = available}, #{jid := JID}} = Acc) -> + send_last_items(JID), + Acc; +on_self_presence(Acc) -> + Acc. + +-spec on_user_offline(ejabberd_c2s:state(), atom()) -> ejabberd_c2s:state(). +on_user_offline(#{jid := JID} = C2SState, _Reason) -> + purge_offline(JID), + C2SState; +on_user_offline(C2SState, _Reason) -> + C2SState. %% ------- %% subscription hooks handling functions %% -out_subscription(User, Server, JID, subscribed) -> - Owner = jlib:make_jid(User, Server, <<"">>), - {PUser, PServer, PResource} = jlib:jid_tolower(JID), - PResources = case PResource of - <<>> -> user_resources(PUser, PServer); - _ -> [PResource] - end, - presence(Server, - {presence, PUser, PServer, PResources, Owner}), - true; -out_subscription(_, _, _, _) -> true. - -in_subscription(_, User, Server, Owner, unsubscribed, - _) -> - unsubscribe_user(jlib:make_jid(User, Server, <<"">>), - Owner), - true; -in_subscription(_, _, _, _, _, _) -> true. - -unsubscribe_user(Entity, Owner) -> - BJID = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), - Host = host(element(2, BJID)), - spawn(fun () -> - lists:foreach(fun (PType) -> - {result, Subscriptions} = - node_action(Host, PType, - get_entity_subscriptions, - [Host, Entity]), - lists:foreach(fun ({#pubsub_node{options - = - Options, - owners - = - Owners, - id = - NodeId}, - subscribed, _, - JID}) -> - case - get_option(Options, - access_model) - of - presence -> - case - lists:member(BJID, - Owners) - of - true -> - node_action(Host, - PType, - unsubscribe_node, - [NodeId, - Entity, - JID, - all]); - false -> - {result, - ok} - end; - _ -> - {result, ok} - end; - (_) -> ok - end, - Subscriptions) - end, - plugins(Host)) - end). - -%% ------- -%% packet receive hook handling function -%% - -drop_pep_error(#xmlel{name = <<"message">>, attrs = Attrs} = Packet, _JID, From, - #jid{lresource = <<"">>} = To) -> - case xml:get_attr_s(<<"type">>, Attrs) of - <<"error">> -> - case xml:get_subtag(Packet, <<"event">>) of - #xmlel{attrs = EventAttrs} -> - case xml:get_attr_s(<<"xmlns">>, EventAttrs) of - ?NS_PUBSUB_EVENT -> - ?DEBUG("Dropping PEP error message from ~s to ~s", - [jlib:jid_to_string(From), - jlib:jid_to_string(To)]), - drop; - _ -> - Packet - end; - false -> - Packet - end; - _ -> - Packet +-spec out_subscription(presence()) -> any(). +out_subscription(#presence{type = subscribed, from = From, to = To}) -> + if From#jid.lserver == To#jid.lserver -> + send_last_pep(jid:remove_resource(From), To, unknown); + true -> + ok end; -drop_pep_error(Acc, _JID, _From, _To) -> Acc. +out_subscription(_) -> + ok. + +-spec in_subscription(boolean(), presence()) -> true. +in_subscription(_, #presence{to = To, from = Owner, type = unsubscribed}) -> + unsubscribe_user(jid:remove_resource(To), Owner), + true; +in_subscription(_, _) -> + true. + +-spec unsubscribe_user(jid(), jid()) -> ok. +unsubscribe_user(Entity, Owner) -> + lists:foreach( + fun(ServerHost) -> + unsubscribe_user(ServerHost, Entity, Owner) + end, + lists:usort( + lists:foldl( + fun(UserHost, Acc) -> + case gen_mod:is_loaded(UserHost, mod_pubsub) of + true -> [UserHost|Acc]; + false -> Acc + end + end, [], [Entity#jid.lserver, Owner#jid.lserver]))). + +-spec unsubscribe_user(binary(), jid(), jid()) -> ok. +unsubscribe_user(Host, Entity, Owner) -> + BJID = jid:tolower(jid:remove_resource(Owner)), + lists:foreach( + fun (PType) -> + case node_action(Host, PType, + get_entity_subscriptions, + [Host, Entity]) of + {result, Subs} -> + lists:foreach( + fun({#pubsub_node{options = Options, + owners = O, + id = Nidx}, + subscribed, _, JID}) -> + Unsubscribe = match_option(Options, access_model, presence) + andalso lists:member(BJID, node_owners_action(Host, PType, Nidx, O)), + case Unsubscribe of + true -> + node_action(Host, PType, + unsubscribe_node, [Nidx, Entity, JID, all]); + false -> + ok + end; + (_) -> + ok + end, Subs); + _ -> + ok + end + end, plugins(Host)). %% ------- %% user remove hook handling function %% +-spec remove_user(binary(), binary()) -> ok. remove_user(User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - Entity = jlib:make_jid(LUser, LServer, <<"">>), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + Entity = jid:make(LUser, LServer), Host = host(LServer), HomeTreeBase = <<"/home/", LServer/binary, "/", LUser/binary>>, - spawn(fun () -> - lists:foreach(fun (PType) -> - {result, Subscriptions} = - node_action(Host, PType, - get_entity_subscriptions, - [Host, Entity]), - lists:foreach(fun ({#pubsub_node{id = - NodeId}, - _, _, JID}) -> - node_action(Host, - PType, - unsubscribe_node, - [NodeId, - Entity, - JID, - all]); - (_) -> ok - end, - Subscriptions), - {result, Affiliations} = - node_action(Host, PType, - get_entity_affiliations, - [Host, Entity]), - lists:foreach(fun ({#pubsub_node{nodeid - = - {H, - N}, - parents - = - []}, - owner}) -> - delete_node(H, N, - Entity); - ({#pubsub_node{nodeid - = - {H, - N}, - type = - <<"hometree">>}, - owner}) - when N == - HomeTreeBase -> - delete_node(H, N, - Entity); - ({#pubsub_node{id = - NodeId}, - publisher}) -> - node_action(Host, - PType, - set_affiliation, - [NodeId, - Entity, - none]); - (_) -> ok - end, - Affiliations) - end, - plugins(Host)) - end). + lists:foreach( + fun(PType) -> + case node_action(Host, PType, + get_entity_subscriptions, + [Host, Entity]) of + {result, Subs} -> + lists:foreach( + fun({#pubsub_node{id = Nidx}, _, _, JID}) -> + node_action(Host, PType, + unsubscribe_node, + [Nidx, Entity, JID, all]); + (_) -> + ok + end, Subs), + case node_action(Host, PType, + get_entity_affiliations, + [Host, Entity]) of + {result, Affs} -> + lists:foreach( + fun({#pubsub_node{nodeid = {H, N}, parents = []}, owner}) -> + delete_node(H, N, Entity); + ({#pubsub_node{nodeid = {H, N}, type = Type}, owner}) + when N == HomeTreeBase, Type == <<"hometree">> -> + delete_node(H, N, Entity); + ({#pubsub_node{id = Nidx}, _}) -> + case node_action(Host, PType, + get_state, + [Nidx, jid:tolower(Entity)]) of + {result, #pubsub_state{items = ItemIds}} -> + node_action(Host, PType, + remove_extra_items, + [Nidx, 0, ItemIds]), + node_action(Host, PType, + set_affiliation, + [Nidx, Entity, none]); + _ -> + ok + end + end, Affs); + _ -> + ok + end; + _ -> + ok + end + end, plugins(Host)). handle_call(server_host, _From, State) -> {reply, State#state.server_host, State}; @@ -1435,97 +737,81 @@ handle_call(pep_mapping, _From, State) -> handle_call(nodetree, _From, State) -> {reply, State#state.nodetree, State}; handle_call(stop, _From, State) -> - {stop, normal, ok, State}. - -%%-------------------------------------------------------------------- -%% Function: handle_cast(Msg, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} -%% Description: Handling cast messages -%%-------------------------------------------------------------------- -%% @private -handle_cast(_Msg, State) -> {noreply, State}. - --spec(handle_info/2 :: -( - _ :: {route, From::jid(), To::jid(), Packet::xmlel()}, - State :: state()) - -> {noreply, state()} -). - -%%-------------------------------------------------------------------- -%% Function: handle_info(Info, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} -%% Description: Handling all non call/cast messages -%%-------------------------------------------------------------------- -%% @private -handle_info({route, From, To, Packet}, - #state{server_host = ServerHost, access = Access, - plugins = Plugins} = - State) -> - case catch do_route(ServerHost, Access, Plugins, - To#jid.lserver, From, To, Packet) - of - {'EXIT', Reason} -> ?ERROR_MSG("~p", [Reason]); - _ -> ok - end, - {noreply, State}; -handle_info(_Info, State) -> + {stop, normal, ok, State}; +handle_call(Request, From, State) -> + ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. -%%-------------------------------------------------------------------- -%% Function: terminate(Reason, State) -> void() -%% Description: This function is called by a gen_server when it is about to -%% terminate. It should be the opposite of Module:init/1 and do any necessary -%% cleaning up. When it returns, the gen_server terminates with Reason. -%% The return value is ignored. -%%-------------------------------------------------------------------- -%% @private -terminate(_Reason, - #state{host = Host, server_host = ServerHost, - nodetree = TreePlugin, plugins = Plugins}) -> - ejabberd_router:unregister_route(Host), - case lists:member(?PEPNODE, Plugins) of - true -> - ejabberd_hooks:delete(caps_update, ServerHost, ?MODULE, - caps_update, 80), - ejabberd_hooks:delete(disco_sm_identity, ServerHost, - ?MODULE, disco_sm_identity, 75), - ejabberd_hooks:delete(disco_sm_features, ServerHost, - ?MODULE, disco_sm_features, 75), - ejabberd_hooks:delete(disco_sm_items, ServerHost, - ?MODULE, disco_sm_items, 75), - ejabberd_hooks:delete(c2s_filter_packet_in, ServerHost, - ?MODULE, drop_pep_error, 75), - gen_iq_handler:remove_iq_handler(ejabberd_sm, - ServerHost, ?NS_PUBSUB), - gen_iq_handler:remove_iq_handler(ejabberd_sm, - ServerHost, ?NS_PUBSUB_OWNER); - false -> ok +handle_cast(Msg, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Msg]), + {noreply, State}. + +handle_info({route, Packet}, State) -> + try route(Packet) + catch + Class:Reason:StackTrace -> + ?ERROR_MSG("Failed to route packet:~n~ts~n** ~ts", + [xmpp:pp(Packet), + misc:format_exception(2, Class, Reason, StackTrace)]) end, - ejabberd_hooks:delete(sm_remove_connection_hook, - ServerHost, ?MODULE, on_user_offline, 75), + {noreply, State}; +handle_info(Info, State) -> + ?WARNING_MSG("Unexpected info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, + #state{hosts = Hosts, server_host = ServerHost, nodetree = TreePlugin, plugins = Plugins}) -> + case lists:member(?PEPNODE, Plugins) of + true -> + ejabberd_hooks:delete(caps_add, ServerHost, + ?MODULE, caps_add, 80), + ejabberd_hooks:delete(caps_update, ServerHost, + ?MODULE, caps_update, 80), + ejabberd_hooks:delete(disco_sm_identity, ServerHost, + ?MODULE, disco_sm_identity, 75), + ejabberd_hooks:delete(disco_sm_features, ServerHost, + ?MODULE, disco_sm_features, 75), + ejabberd_hooks:delete(disco_sm_items, ServerHost, + ?MODULE, disco_sm_items, 75), + gen_iq_handler:remove_iq_handler(ejabberd_sm, + ServerHost, ?NS_PUBSUB), + gen_iq_handler:remove_iq_handler(ejabberd_sm, + ServerHost, ?NS_PUBSUB_OWNER); + false -> + ok + end, + ejabberd_hooks:delete(c2s_self_presence, ServerHost, + ?MODULE, on_self_presence, 75), + ejabberd_hooks:delete(c2s_terminated, ServerHost, + ?MODULE, on_user_offline, 75), ejabberd_hooks:delete(disco_local_identity, ServerHost, - ?MODULE, disco_local_identity, 75), + ?MODULE, disco_local_identity, 75), ejabberd_hooks:delete(disco_local_features, ServerHost, - ?MODULE, disco_local_features, 75), + ?MODULE, disco_local_features, 75), ejabberd_hooks:delete(disco_local_items, ServerHost, - ?MODULE, disco_local_items, 75), + ?MODULE, disco_local_items, 75), ejabberd_hooks:delete(presence_probe_hook, ServerHost, - ?MODULE, presence_probe, 80), - ejabberd_hooks:delete(roster_in_subscription, - ServerHost, ?MODULE, in_subscription, 50), - ejabberd_hooks:delete(roster_out_subscription, - ServerHost, ?MODULE, out_subscription, 50), - ejabberd_hooks:delete(remove_user, ServerHost, ?MODULE, - remove_user, 50), - ejabberd_hooks:delete(anonymous_purge_hook, ServerHost, - ?MODULE, remove_user, 50), - mod_disco:unregister_feature(ServerHost, ?NS_PUBSUB), - gen_mod:get_module_proc(ServerHost, ?LOOPNAME) ! stop, - terminate_plugins(Host, ServerHost, Plugins, - TreePlugin). + ?MODULE, presence_probe, 80), + ejabberd_hooks:delete(roster_in_subscription, ServerHost, + ?MODULE, in_subscription, 50), + ejabberd_hooks:delete(roster_out_subscription, ServerHost, + ?MODULE, out_subscription, 50), + ejabberd_hooks:delete(remove_user, ServerHost, + ?MODULE, remove_user, 50), + ejabberd_hooks:delete(c2s_handle_info, ServerHost, + ?MODULE, c2s_handle_info, 50), + lists:foreach( + fun(Host) -> + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_ITEMS), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_PUBSUB), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_PUBSUB_OWNER), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_VCARD), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_COMMANDS), + terminate_plugins(Host, ServerHost, Plugins, TreePlugin), + ejabberd_router:unregister_route(Host) + end, Hosts), + ejabberd_commands:unregister_commands(ServerHost, ?MODULE, get_commands_spec()). %%-------------------------------------------------------------------- %% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} @@ -1534,1047 +820,679 @@ terminate(_Reason, %% @private code_change(_OldVsn, State, _Extra) -> {ok, State}. --spec(do_route/7 :: -( - ServerHost :: binary(), - Access :: atom(), - Plugins :: [binary(),...], - Host :: mod_pubsub:hostPubsub(), - From :: jid(), - To :: jid(), - Packet :: xmlel()) - -> ok -). - %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- -do_route(ServerHost, Access, Plugins, Host, From, To, Packet) -> - #xmlel{name = Name, attrs = Attrs} = Packet, - case To of - #jid{luser = <<"">>, lresource = <<"">>} -> - case Name of - <<"iq">> -> - case jlib:iq_query_info(Packet) of - #iq{type = get, xmlns = ?NS_DISCO_INFO, sub_el = SubEl, - lang = Lang} = - IQ -> - #xmlel{attrs = QAttrs} = SubEl, - Node = xml:get_attr_s(<<"node">>, QAttrs), - Info = ejabberd_hooks:run_fold(disco_info, ServerHost, - [], - [ServerHost, ?MODULE, - <<"">>, <<"">>]), - Res = case iq_disco_info(Host, Node, From, Lang) of - {result, IQRes} -> - jlib:iq_to_xml(IQ#iq{type = result, - sub_el = - [#xmlel{name = - <<"query">>, - attrs = - QAttrs, - children = - IQRes ++ - Info}]}); - {error, Error} -> - jlib:make_error_reply(Packet, Error) - end, - ejabberd_router:route(To, From, Res); - #iq{type = get, xmlns = ?NS_DISCO_ITEMS, - sub_el = SubEl} = - IQ -> - #xmlel{attrs = QAttrs} = SubEl, - Node = xml:get_attr_s(<<"node">>, QAttrs), - Res = case iq_disco_items(Host, Node, From) of - {result, IQRes} -> - jlib:iq_to_xml(IQ#iq{type = result, - sub_el = - [#xmlel{name = - <<"query">>, - attrs = - QAttrs, - children = - IQRes}]}) -% {error, Error} -> -% jlib:make_error_reply(Packet, Error) - end, - ejabberd_router:route(To, From, Res); - #iq{type = IQType, xmlns = ?NS_PUBSUB, lang = Lang, - sub_el = SubEl} = - IQ -> - Res = case iq_pubsub(Host, ServerHost, From, IQType, - SubEl, Lang, Access, Plugins) - of - {result, IQRes} -> - jlib:iq_to_xml(IQ#iq{type = result, - sub_el = IQRes}); - {error, Error} -> - jlib:make_error_reply(Packet, Error) - end, - ejabberd_router:route(To, From, Res); - #iq{type = IQType, xmlns = ?NS_PUBSUB_OWNER, - lang = Lang, sub_el = SubEl} = - IQ -> - Res = case iq_pubsub_owner(Host, ServerHost, From, - IQType, SubEl, Lang) - of - {result, IQRes} -> - jlib:iq_to_xml(IQ#iq{type = result, - sub_el = IQRes}); - {error, Error} -> - jlib:make_error_reply(Packet, Error) - end, - ejabberd_router:route(To, From, Res); - #iq{type = get, xmlns = (?NS_VCARD) = XMLNS, - lang = Lang, sub_el = _SubEl} = - IQ -> - Res = IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"vCard">>, - attrs = [{<<"xmlns">>, XMLNS}], - children = iq_get_vcard(Lang)}]}, - ejabberd_router:route(To, From, jlib:iq_to_xml(Res)); - #iq{type = set, xmlns = ?NS_COMMANDS} = IQ -> - Res = case iq_command(Host, ServerHost, From, IQ, - Access, Plugins) - of - {error, Error} -> - jlib:make_error_reply(Packet, Error); - {result, IQRes} -> - jlib:iq_to_xml(IQ#iq{type = result, - sub_el = IQRes}) - end, - ejabberd_router:route(To, From, Res); - #iq{} -> - Err = jlib:make_error_reply(Packet, - ?ERR_FEATURE_NOT_IMPLEMENTED), - ejabberd_router:route(To, From, Err); - _ -> ok - end; - <<"message">> -> - case xml:get_attr_s(<<"type">>, Attrs) of - <<"error">> -> ok; - _ -> - case find_authorization_response(Packet) of - none -> ok; - invalid -> - ejabberd_router:route(To, From, - jlib:make_error_reply(Packet, - ?ERR_BAD_REQUEST)); - XFields -> - handle_authorization_response(Host, From, To, - Packet, XFields) - end - end; - _ -> ok - end; - _ -> - case xml:get_attr_s(<<"type">>, Attrs) of - <<"error">> -> ok; - <<"result">> -> ok; - _ -> - Err = jlib:make_error_reply(Packet, - ?ERR_ITEM_NOT_FOUND), - ejabberd_router:route(To, From, Err) - end +-spec process_disco_info(iq()) -> iq(). +process_disco_info(#iq{type = set, lang = Lang} = IQ) -> + Txt = ?T("Value 'set' of 'type' attribute is not allowed"), + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); +process_disco_info(#iq{from = From, to = To, lang = Lang, type = get, + sub_els = [#disco_info{node = Node}]} = IQ) -> + Host = To#jid.lserver, + ServerHost = ejabberd_router:host_of_route(Host), + Info = ejabberd_hooks:run_fold(disco_info, ServerHost, + [], + [ServerHost, ?MODULE, <<>>, <<>>]), + case iq_disco_info(ServerHost, Host, Node, From, Lang) of + {result, IQRes} -> + XData = IQRes#disco_info.xdata ++ Info, + xmpp:make_iq_result(IQ, IQRes#disco_info{node = Node, xdata = XData}); + {error, Error} -> + xmpp:make_error(IQ, Error) end. -command_disco_info(_Host, ?NS_COMMANDS, _From) -> - IdentityEl = #xmlel{name = <<"identity">>, - attrs = - [{<<"category">>, <<"automation">>}, - {<<"type">>, <<"command-list">>}], - children = []}, - {result, [IdentityEl]}; -command_disco_info(_Host, ?NS_PUBSUB_GET_PENDING, - _From) -> - IdentityEl = #xmlel{name = <<"identity">>, - attrs = - [{<<"category">>, <<"automation">>}, - {<<"type">>, <<"command-node">>}], - children = []}, - FeaturesEl = #xmlel{name = <<"feature">>, - attrs = [{<<"var">>, ?NS_COMMANDS}], children = []}, - {result, [IdentityEl, FeaturesEl]}. +-spec process_disco_items(iq()) -> iq(). +process_disco_items(#iq{type = set, lang = Lang} = IQ) -> + Txt = ?T("Value 'set' of 'type' attribute is not allowed"), + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); +process_disco_items(#iq{type = get, from = From, to = To, + sub_els = [#disco_items{node = Node} = SubEl]} = IQ) -> + Host = To#jid.lserver, + case iq_disco_items(Host, Node, From, SubEl#disco_items.rsm) of + {result, IQRes} -> + xmpp:make_iq_result(IQ, IQRes#disco_items{node = Node}); + {error, Error} -> + xmpp:make_error(IQ, Error) + end. +-spec process_pubsub(iq()) -> iq(). +process_pubsub(#iq{to = To} = IQ) -> + Host = To#jid.lserver, + ServerHost = ejabberd_router:host_of_route(Host), + Access = config(ServerHost, access), + case iq_pubsub(Host, Access, IQ) of + {result, IQRes} -> + xmpp:make_iq_result(IQ, IQRes); + {error, Error} -> + xmpp:make_error(IQ, Error) + end. + +-spec process_pubsub_owner(iq()) -> iq(). +process_pubsub_owner(#iq{to = To} = IQ) -> + Host = To#jid.lserver, + case iq_pubsub_owner(Host, IQ) of + {result, IQRes} -> + xmpp:make_iq_result(IQ, IQRes); + {error, Error} -> + xmpp:make_error(IQ, Error) + end. + +-spec process_vcard(iq()) -> iq(). +process_vcard(#iq{type = get, to = To, lang = Lang} = IQ) -> + ServerHost = ejabberd_router:host_of_route(To#jid.lserver), + xmpp:make_iq_result(IQ, iq_get_vcard(ServerHost, Lang)); +process_vcard(#iq{type = set, lang = Lang} = IQ) -> + Txt = ?T("Value 'set' of 'type' attribute is not allowed"), + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)). + +-spec process_commands(iq()) -> iq(). +process_commands(#iq{type = set, to = To, from = From, + sub_els = [#adhoc_command{} = Request]} = IQ) -> + Host = To#jid.lserver, + ServerHost = ejabberd_router:host_of_route(Host), + Plugins = config(ServerHost, plugins), + Access = config(ServerHost, access), + case adhoc_request(Host, ServerHost, From, Request, Access, Plugins) of + {error, Error} -> + xmpp:make_error(IQ, Error); + Response -> + xmpp:make_iq_result( + IQ, xmpp_util:make_adhoc_response(Request, Response)) + end; +process_commands(#iq{type = get, lang = Lang} = IQ) -> + Txt = ?T("Value 'get' of 'type' attribute is not allowed"), + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)). + +-spec route(stanza()) -> ok. +route(#iq{to = To} = IQ) when To#jid.lresource == <<"">> -> + ejabberd_router:process_iq(IQ); +route(Packet) -> + To = xmpp:get_to(Packet), + case To of + #jid{luser = <<>>, lresource = <<>>} -> + case Packet of + #message{type = T} when T /= error -> + case find_authorization_response(Packet) of + undefined -> + ok; + {error, Err} -> + ejabberd_router:route_error(Packet, Err); + AuthResponse -> + handle_authorization_response( + To#jid.lserver, Packet, AuthResponse) + end; + _ -> + Err = xmpp:err_service_unavailable(), + ejabberd_router:route_error(Packet, Err) + end; + _ -> + Err = xmpp:err_item_not_found(), + ejabberd_router:route_error(Packet, Err) + end. + +-spec command_disco_info(binary(), binary(), jid()) -> {result, disco_info()}. +command_disco_info(_Host, ?NS_COMMANDS, _From) -> + {result, #disco_info{identities = [#identity{category = <<"automation">>, + type = <<"command-list">>}]}}; +command_disco_info(_Host, ?NS_PUBSUB_GET_PENDING, _From) -> + {result, #disco_info{identities = [#identity{category = <<"automation">>, + type = <<"command-node">>}], + features = [?NS_COMMANDS]}}. + +-spec node_disco_info(binary(), binary(), jid()) -> {result, disco_info()} | + {error, stanza_error()}. node_disco_info(Host, Node, From) -> node_disco_info(Host, Node, From, true, true). -node_disco_info(Host, Node, From, _Identity, _Features) -> -% Action = -% fun(#pubsub_node{type = Type, id = NodeId}) -> -% I = case Identity of -% false -> -% []; -% true -> -% Types = -% case tree_call(Host, get_subnodes, [Host, Node, From]) of -% [] -> -% [<<"leaf">>]; %% No sub-nodes: it's a leaf node -% _ -> -% case node_call(Type, get_items, [NodeId, From]) of -% {result, []} -> [<<"collection">>]; -% {result, _} -> [<<"leaf">>, <<"collection">>]; -% _ -> [] -% end -% end, -% lists:map(fun(T) -> -% #xmlel{name = <<"identity">>, -% attrs = -% [{<<"category">>, -% <<"pubsub">>}, -% {<<"type">>, T}], -% children = []} -% end, Types) -% end, -% F = case Features of -% false -> -% []; -% true -> -% [#xmlel{name = <<"feature">>, -% attrs = [{<<"var">>, ?NS_PUBSUB}], -% children = []} -% | lists:map(fun (T) -> -% #xmlel{name = <<"feature">>, -% attrs = -% [{<<"var">>, -% <<(?NS_PUBSUB)/binary, -% "#", -% T/binary>>}], -% children = []} -% end, -% features(Type))] -% end, -% %% TODO: add meta-data info (spec section 5.4) -% {result, I ++ F} -% end, -% case transaction(Host, Node, Action, sync_dirty) of -% {result, {_, Result}} -> {result, Result}; -% Other -> Other -% end. - Action = fun (#pubsub_node{type = Type, id = NodeId}) -> - Types = case tree_call(Host, get_subnodes, - [Host, Node, From]) - of - [] -> [<<"leaf">>]; - _ -> - case node_call(Type, get_items, - [NodeId, From]) - of - {result, []} -> - [<<"collection">>]; - {result, _} -> - [<<"leaf">>, - <<"collection">>]; - _ -> [] - end - end, - I = lists:map(fun (T) -> - #xmlel{name = <<"identity">>, - attrs = - [{<<"category">>, - <<"pubsub">>}, - {<<"type">>, T}], - children = []} - end, - Types), - F = [#xmlel{name = <<"feature">>, - attrs = [{<<"var">>, ?NS_PUBSUB}], - children = []} - | lists:map(fun (T) -> - #xmlel{name = <<"feature">>, - attrs = - [{<<"var">>, - <<(?NS_PUBSUB)/binary, - "#", - T/binary>>}], - children = []} - end, - features(Type))], - {result, I ++ F} - end, - case transaction(Host, Node, Action, sync_dirty) of - {result, {_, Result}} -> {result, Result}; - Other -> Other - end. - -iq_disco_info(Host, SNode, From, Lang) -> - [Node | _] = case SNode of - <<>> -> [<<>>]; - _ -> str:tokens(SNode, <<"!">>) - end, - % Node = string_to_node(RealSNode), - case Node of - <<>> -> - {result, - [#xmlel{name = <<"identity">>, - attrs = - [{<<"category">>, <<"pubsub">>}, - {<<"type">>, <<"service">>}, - {<<"name">>, - translate:translate(Lang, <<"Publish-Subscribe">>)}], - children = []}, - #xmlel{name = <<"feature">>, - attrs = [{<<"var">>, ?NS_DISCO_INFO}], children = []}, - #xmlel{name = <<"feature">>, - attrs = [{<<"var">>, ?NS_DISCO_ITEMS}], children = []}, - #xmlel{name = <<"feature">>, - attrs = [{<<"var">>, ?NS_PUBSUB}], children = []}, - #xmlel{name = <<"feature">>, - attrs = [{<<"var">>, ?NS_COMMANDS}], children = []}, - #xmlel{name = <<"feature">>, - attrs = [{<<"var">>, ?NS_VCARD}], children = []}] - ++ - lists:map(fun (Feature) -> - #xmlel{name = <<"feature">>, - attrs = - [{<<"var">>, <<(?NS_PUBSUB)/binary, "#", Feature/binary>>}], - children = []} +-spec node_disco_info(binary(), binary(), jid(), boolean(), boolean()) -> + {result, disco_info()} | {error, stanza_error()}. +node_disco_info(Host, Node, _From, _Identity, _Features) -> + Action = + fun(#pubsub_node{id = Nidx, type = Type, options = Options}) -> + NodeType = case get_option(Options, node_type) of + collection -> <<"collection">>; + _ -> <<"leaf">> + end, + Affs = case node_call(Host, Type, get_node_affiliations, [Nidx]) of + {result, As} -> As; + _ -> [] end, - features(Host, Node))}; - ?NS_COMMANDS -> command_disco_info(Host, Node, From); - ?NS_PUBSUB_GET_PENDING -> - command_disco_info(Host, Node, From); - _ -> node_disco_info(Host, Node, From) + Subs = case node_call(Host, Type, get_node_subscriptions, [Nidx]) of + {result, Ss} -> Ss; + _ -> [] + end, + Meta = [{title, get_option(Options, title, <<>>)}, + {type, get_option(Options, type, <<>>)}, + {description, get_option(Options, description, <<>>)}, + {owner, [jid:make(LJID) || {LJID, Aff} <- Affs, Aff =:= owner]}, + {publisher, [jid:make(LJID) || {LJID, Aff} <- Affs, Aff =:= publisher]}, + {access_model, get_option(Options, access_model, open)}, + {publish_model, get_option(Options, publish_model, publishers)}, + {num_subscribers, length(Subs)}], + XData = #xdata{type = result, + fields = pubsub_meta_data:encode(Meta)}, + Is = [#identity{category = <<"pubsub">>, type = NodeType}], + Fs = [?NS_PUBSUB | [feature(F) || F <- plugin_features(Host, Type)]], + {result, #disco_info{identities = Is, features = Fs, xdata = [XData]}} + end, + case transaction(Host, Node, Action, sync_dirty) of + {result, {_, Result}} -> {result, Result}; + Other -> Other end. --spec(iq_disco_items/3 :: -( - Host :: mod_pubsub:host(), - NodeId :: <<>> | mod_pubsub:nodeId(), - From :: jid()) - -> {result, [xmlel()]} -). -iq_disco_items(Host, <<>>, From) -> - {result, - lists:map(fun (#pubsub_node{nodeid = {_, SubNode}, - options = Options}) -> - Attrs = case get_option(Options, title) of - false -> - [{<<"jid">>, Host} - | nodeAttr(SubNode)]; - Title -> - [{<<"jid">>, Host}, - {<<"name">>, Title} - | nodeAttr(SubNode)] - end, - #xmlel{name = <<"item">>, attrs = Attrs, - children = []} - end, - tree_action(Host, get_subnodes, [Host, <<>>, From]))}; -% case tree_action(Host, get_subnodes, [Host, <<>>, From]) of -% Nodes when is_list(Nodes) -> -% {result, -% lists:map(fun (#pubsub_node{nodeid = {_, SubNode}, -% options = Options}) -> -% Attrs = case get_option(Options, title) of -% false -> -% [{<<"jid">>, Host} -% | nodeAttr(SubNode)]; -% Title -> -% [{<<"jid">>, Host}, -% {<<"name">>, Title} -% | nodeAttr(SubNode)] -% end, -% #xmlel{name = <<"item">>, attrs = Attrs, -% children = []} -% end, -% Nodes)}; -% Other -> Other -% end; -iq_disco_items(Host, ?NS_COMMANDS, _From) -> - CommandItems = [#xmlel{name = <<"item">>, - attrs = - [{<<"jid">>, Host}, - {<<"node">>, ?NS_PUBSUB_GET_PENDING}, - {<<"name">>, <<"Get Pending">>}], - children = []}], - {result, CommandItems}; -iq_disco_items(_Host, ?NS_PUBSUB_GET_PENDING, _From) -> - CommandItems = [], {result, CommandItems}; -iq_disco_items(Host, Item, From) -> +-spec iq_disco_info(binary(), binary(), binary(), jid(), binary()) + -> {result, disco_info()} | {error, stanza_error()}. +iq_disco_info(ServerHost, Host, SNode, From, Lang) -> + [Node | _] = case SNode of + <<>> -> [<<>>]; + _ -> str:tokens(SNode, <<"!">>) + end, + case Node of + <<>> -> + Name = mod_pubsub_opt:name(ServerHost), + {result, + #disco_info{ + identities = [#identity{ + category = <<"pubsub">>, + type = <<"service">>, + name = translate:translate(Lang, Name)}], + features = [?NS_DISCO_INFO, + ?NS_DISCO_ITEMS, + ?NS_PUBSUB, + ?NS_COMMANDS, + ?NS_VCARD | + [feature(F) || F <- features(Host, Node)]]}}; + ?NS_COMMANDS -> + command_disco_info(Host, Node, From); + ?NS_PUBSUB_GET_PENDING -> + command_disco_info(Host, Node, From); + _ -> + node_disco_info(Host, Node, From) + end. + +-spec iq_disco_items(host(), binary(), jid(), undefined | rsm_set()) -> + {result, disco_items()} | {error, stanza_error()}. +iq_disco_items(Host, <<>>, _From, _RSM) -> + MaxNodes = mod_pubsub_opt:max_nodes_discoitems(serverhost(Host)), + case tree_action(Host, get_subnodes, [Host, <<>>, MaxNodes]) of + {error, #stanza_error{}} = Err -> + Err; + Nodes when is_list(Nodes) -> + Items = + lists:map( + fun(#pubsub_node{nodeid = {_, SubNode}, options = Options}) -> + case get_option(Options, title) of + false -> + #disco_item{jid = jid:make(Host), + node = SubNode}; + Title -> + #disco_item{jid = jid:make(Host), + name = Title, + node = SubNode} + end + end, Nodes), + {result, #disco_items{items = Items}} + end; +iq_disco_items(Host, ?NS_COMMANDS, _From, _RSM) -> + {result, + #disco_items{items = [#disco_item{jid = jid:make(Host), + node = ?NS_PUBSUB_GET_PENDING, + name = ?T("Get Pending")}]}}; +iq_disco_items(_Host, ?NS_PUBSUB_GET_PENDING, _From, _RSM) -> + {result, #disco_items{}}; +iq_disco_items(Host, Item, From, RSM) -> case str:tokens(Item, <<"!">>) of - [_Node, _ItemID] -> {result, []}; - [Node] -> -% Node = string_to_node(SNode), - Action = fun (#pubsub_node{id = Idx, type = Type, - options = Options, owners = Owners}) -> - NodeItems = case get_allowed_items_call(Host, Idx, - From, Type, - Options, - Owners) - of - {result, R} -> R; - _ -> [] - end, - Nodes = lists:map(fun (#pubsub_node{nodeid = - {_, SubNode}, - options = - SubOptions}) -> - Attrs = case - get_option(SubOptions, - title) - of - false -> - [{<<"jid">>, - Host} - | nodeAttr(SubNode)]; - Title -> - [{<<"jid">>, - Host}, - {<<"name">>, - Title} - | nodeAttr(SubNode)] - end, - #xmlel{name = <<"item">>, - attrs = Attrs, - children = []} - end, - tree_call(Host, get_subnodes, - [Host, Node, From])), - Items = lists:map(fun (#pubsub_item{itemid = - {RN, _}}) -> - {result, Name} = - node_call(Type, - get_item_name, - [Host, Node, - RN]), - #xmlel{name = <<"item">>, - attrs = - [{<<"jid">>, - Host}, - {<<"name">>, - Name}], - children = []} - end, - NodeItems), - {result, Nodes ++ Items} - end, - case transaction(Host, Node, Action, sync_dirty) of - {result, {_, Result}} -> {result, Result}; - Other -> Other - end + [_Node, _ItemId] -> + {result, #disco_items{}}; + [Node] -> + MaxNodes = mod_pubsub_opt:max_nodes_discoitems(serverhost(Host)), + Action = fun(#pubsub_node{id = Nidx, type = Type, options = Options, owners = O}) -> + Owners = node_owners_call(Host, Type, Nidx, O), + {NodeItems, RsmOut} = case get_allowed_items_call( + Host, Nidx, From, Type, Options, Owners, RSM) of + {result, R} -> R; + _ -> {[], undefined} + end, + case tree_call(Host, get_subnodes, [Host, Node, MaxNodes]) of + SubNodes when is_list(SubNodes) -> + Nodes = lists:map( + fun(#pubsub_node{nodeid = {_, SubNode}, options = SubOptions}) -> + case get_option(SubOptions, title) of + false -> + #disco_item{jid = jid:make(Host), + node = SubNode}; + Title -> + #disco_item{jid = jid:make(Host), + name = Title, + node = SubNode} + end + end, SubNodes), + Items = lists:flatmap( + fun(#pubsub_item{itemid = {RN, _}}) -> + case node_call(Host, Type, get_item_name, [Host, Node, RN]) of + {result, Name} -> + [#disco_item{jid = jid:make(Host), name = Name}]; + _ -> + [] + end + end, NodeItems), + {result, #disco_items{items = Nodes ++ Items, + rsm = RsmOut}}; + Error -> + Error + end + end, + case transaction(Host, Node, Action, sync_dirty) of + {result, {_, Result}} -> {result, Result}; + Other -> Other + end end. --spec(iq_sm/3 :: -( - From :: jid(), - To :: jid(), - IQ :: iq_request()) - -> iq_result() | iq_error() -). -iq_sm(From, To, #iq{type = Type, sub_el = SubEl, xmlns = XMLNS, lang = Lang} = IQ) -> - ServerHost = To#jid.lserver, - LOwner = jlib:jid_tolower(jlib:jid_remove_resource(To)), - Res = case XMLNS of - ?NS_PUBSUB -> - iq_pubsub(LOwner, ServerHost, From, Type, SubEl, Lang); - ?NS_PUBSUB_OWNER -> - iq_pubsub_owner(LOwner, ServerHost, From, Type, SubEl, - Lang) +-spec iq_sm(iq()) -> iq(). +iq_sm(#iq{to = To, sub_els = [SubEl]} = IQ) -> + LOwner = jid:tolower(jid:remove_resource(To)), + Res = case xmpp:get_ns(SubEl) of + ?NS_PUBSUB -> + iq_pubsub(LOwner, all, IQ); + ?NS_PUBSUB_OWNER -> + iq_pubsub_owner(LOwner, IQ) end, case Res of - {result, IQRes} -> IQ#iq{type = result, sub_el = IQRes}; - {error, Error} -> - IQ#iq{type = error, sub_el = [Error, SubEl]} + {result, IQRes} -> + xmpp:make_iq_result(IQ, IQRes); + {error, Error} -> + xmpp:make_error(IQ, Error) end. -iq_get_vcard(Lang) -> - [#xmlel{name = <<"FN">>, attrs = [], - children = [{xmlcdata, <<"ejabberd/mod_pubsub">>}]}, - #xmlel{name = <<"URL">>, attrs = [], - children = [{xmlcdata, ?EJABBERD_URI}]}, - #xmlel{name = <<"DESC">>, attrs = [], - children = - [{xmlcdata, - <<(translate:translate(Lang, - <<"ejabberd Publish-Subscribe module">>))/binary, - "\nCopyright (c) 2004-2015 ProcessOne">>}]}]. - --spec(iq_pubsub/6 :: -( - Host :: mod_pubsub:host(), - ServerHost :: binary(), - From :: jid(), - IQType :: 'get' | 'set', - SubEl :: xmlel(), - Lang :: binary()) - -> {result, [xmlel()]} - %%% - | {error, xmlel()} -). - -iq_pubsub(Host, ServerHost, From, IQType, SubEl, Lang) -> - iq_pubsub(Host, ServerHost, From, IQType, SubEl, Lang, all, plugins(ServerHost)). - --spec(iq_pubsub/8 :: -( - Host :: mod_pubsub:host(), - ServerHost :: binary(), - From :: jid(), - IQType :: 'get' | 'set', - SubEl :: xmlel(), - Lang :: binary(), - Access :: atom(), - Plugins :: [binary(),...]) - -> {result, [xmlel()]} - %%% - | {error, xmlel()} -). - -iq_pubsub(Host, ServerHost, From, IQType, SubEl, Lang, Access, Plugins) -> - #xmlel{children = SubEls} = SubEl, - case xml:remove_cdata(SubEls) of - [#xmlel{name = Name, attrs = Attrs, children = Els} | Rest] -> - Node = xml:get_attr_s(<<"node">>, Attrs), - case {IQType, Name} of - {set, <<"create">>} -> - Config = case Rest of - [#xmlel{name = <<"configure">>, children = C}] -> C; - _ -> [] - end, - Type = case xml:get_attr_s(<<"type">>, Attrs) of - <<>> -> hd(Plugins); - T -> T - end, - case lists:member(Type, Plugins) of - false -> - {error, - extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, - unsupported, <<"create-nodes">>)}; - true -> - create_node(Host, ServerHost, Node, From, Type, Access, Config) - end; - {set, <<"publish">>} -> - case xml:remove_cdata(Els) of - [#xmlel{name = <<"item">>, attrs = ItemAttrs, - children = Payload}] -> - ItemId = xml:get_attr_s(<<"id">>, ItemAttrs), - publish_item(Host, ServerHost, Node, From, ItemId, Payload, Access); - [] -> - {error, - extended_error(?ERR_BAD_REQUEST, <<"item-required">>)}; - _ -> - {error, - extended_error(?ERR_BAD_REQUEST, <<"invalid-payload">>)} - end; - {set, <<"retract">>} -> - ForceNotify = case xml:get_attr_s(<<"notify">>, Attrs) - of - <<"1">> -> true; - <<"true">> -> true; - _ -> false - end, - case xml:remove_cdata(Els) of - [#xmlel{name = <<"item">>, attrs = ItemAttrs}] -> - ItemId = xml:get_attr_s(<<"id">>, ItemAttrs), - delete_item(Host, Node, From, ItemId, ForceNotify); - _ -> - {error, - extended_error(?ERR_BAD_REQUEST, <<"item-required">>)} - end; - {set, <<"subscribe">>} -> - Config = case Rest of - [#xmlel{name = <<"options">>, children = C}] -> C; - _ -> [] - end, - JID = xml:get_attr_s(<<"jid">>, Attrs), - subscribe_node(Host, Node, From, JID, Config); - {set, <<"unsubscribe">>} -> - JID = xml:get_attr_s(<<"jid">>, Attrs), - SubId = xml:get_attr_s(<<"subid">>, Attrs), - unsubscribe_node(Host, Node, From, JID, SubId); - {get, <<"items">>} -> - MaxItems = xml:get_attr_s(<<"max_items">>, Attrs), - SubId = xml:get_attr_s(<<"subid">>, Attrs), - ItemIDs = lists:foldl(fun (#xmlel{name = <<"item">>, - attrs = ItemAttrs}, - Acc) -> - case xml:get_attr_s(<<"id">>, - ItemAttrs) - of - <<"">> -> Acc; - ItemID -> [ItemID | Acc] - end; - (_, Acc) -> Acc - end, - [], xml:remove_cdata(Els)), - get_items(Host, Node, From, SubId, MaxItems, ItemIDs); - {get, <<"subscriptions">>} -> - get_subscriptions(Host, Node, From, Plugins); - {get, <<"affiliations">>} -> - get_affiliations(Host, Node, From, Plugins); - {get, <<"options">>} -> - SubID = xml:get_attr_s(<<"subid">>, Attrs), - JID = xml:get_attr_s(<<"jid">>, Attrs), - get_options(Host, Node, JID, SubID, Lang); - {set, <<"options">>} -> - SubID = xml:get_attr_s(<<"subid">>, Attrs), - JID = xml:get_attr_s(<<"jid">>, Attrs), - set_options(Host, Node, JID, SubID, Els); - _ -> {error, ?ERR_FEATURE_NOT_IMPLEMENTED} - end; - Other -> - ?INFO_MSG("Too many actions: ~p", [Other]), - {error, ?ERR_BAD_REQUEST} +-spec iq_get_vcard(binary(), binary()) -> vcard_temp(). +iq_get_vcard(ServerHost, Lang) -> + case mod_pubsub_opt:vcard(ServerHost) of + undefined -> + Desc = misc:get_descr(Lang, ?T("ejabberd Publish-Subscribe module")), + #vcard_temp{fn = <<"ejabberd/mod_pubsub">>, + url = ejabberd_config:get_uri(), + desc = Desc}; + VCard -> + VCard end. - --spec(iq_pubsub_owner/6 :: -( - Host :: mod_pubsub:host(), - ServerHost :: binary(), - From :: jid(), - IQType :: 'get' | 'set', - SubEl :: xmlel(), - Lang :: binary()) - -> {result, [xmlel()]} - %%% - | {error, xmlel()} -). -iq_pubsub_owner(Host, ServerHost, From, IQType, SubEl, Lang) -> - #xmlel{children = SubEls} = SubEl, - Action = xml:remove_cdata(SubEls), - case Action of - [#xmlel{name = Name, attrs = Attrs, children = Els}] -> - Node = xml:get_attr_s(<<"node">>, Attrs), - case {IQType, Name} of - {get, <<"configure">>} -> - get_configure(Host, ServerHost, Node, From, Lang); - {set, <<"configure">>} -> - set_configure(Host, Node, From, Els, Lang); - {get, <<"default">>} -> - get_default(Host, Node, From, Lang); - {set, <<"delete">>} -> delete_node(Host, Node, From); - {set, <<"purge">>} -> purge_node(Host, Node, From); - {get, <<"subscriptions">>} -> - get_subscriptions(Host, Node, From); - {set, <<"subscriptions">>} -> - set_subscriptions(Host, Node, From, - xml:remove_cdata(Els)); - {get, <<"affiliations">>} -> - get_affiliations(Host, Node, From); - {set, <<"affiliations">>} -> - set_affiliations(Host, Node, From, xml:remove_cdata(Els)); - _ -> {error, ?ERR_FEATURE_NOT_IMPLEMENTED} - end; - _ -> - ?INFO_MSG("Too many actions: ~p", [Action]), - {error, ?ERR_BAD_REQUEST} +-spec iq_pubsub(binary() | ljid(), atom(), iq()) -> + {result, pubsub()} | {error, stanza_error()}. +iq_pubsub(Host, Access, #iq{from = From, type = IQType, lang = Lang, + sub_els = [SubEl]}) -> + case {IQType, SubEl} of + {set, #pubsub{create = Node, configure = Configure, + _ = undefined}} when is_binary(Node) -> + ServerHost = serverhost(Host), + Plugins = config(ServerHost, plugins), + Config = case Configure of + {_, XData} -> decode_node_config(XData, Host, Lang); + undefined -> [] + end, + Type = hd(Plugins), + case Config of + {error, _} = Err -> + Err; + _ -> + create_node(Host, ServerHost, Node, From, Type, Access, Config) + end; + {set, #pubsub{publish = #ps_publish{node = Node, items = Items}, + publish_options = XData, configure = _, _ = undefined}} -> + ServerHost = serverhost(Host), + case Items of + [#ps_item{id = ItemId, sub_els = Payload}] -> + case decode_publish_options(XData, Lang) of + {error, _} = Err -> + Err; + PubOpts -> + publish_item(Host, ServerHost, Node, From, ItemId, + Payload, PubOpts, Access) + end; + [] -> + publish_item(Host, ServerHost, Node, From, <<>>, [], [], Access); + _ -> + {error, extended_error(xmpp:err_bad_request(), err_invalid_payload())} + end; + {set, #pubsub{retract = #ps_retract{node = Node, notify = Notify, items = Items}, + _ = undefined}} -> + case Items of + [#ps_item{id = ItemId}] -> + if ItemId /= <<>> -> + delete_item(Host, Node, From, ItemId, Notify); + true -> + {error, extended_error(xmpp:err_bad_request(), + err_item_required())} + end; + [] -> + {error, extended_error(xmpp:err_bad_request(), err_item_required())}; + _ -> + {error, extended_error(xmpp:err_bad_request(), err_invalid_payload())} + end; + {set, #pubsub{subscribe = #ps_subscribe{node = Node, jid = JID}, + options = Options, _ = undefined}} -> + Config = case Options of + #ps_options{xdata = XData, jid = undefined, node = <<>>} -> + decode_subscribe_options(XData, Lang); + #ps_options{xdata = _XData, jid = #jid{}} -> + Txt = ?T("Attribute 'jid' is not allowed here"), + {error, xmpp:err_bad_request(Txt, Lang)}; + #ps_options{xdata = _XData} -> + Txt = ?T("Attribute 'node' is not allowed here"), + {error, xmpp:err_bad_request(Txt, Lang)}; + _ -> + [] + end, + case Config of + {error, _} = Err -> + Err; + _ -> + subscribe_node(Host, Node, From, JID, Config) + end; + {set, #pubsub{unsubscribe = #ps_unsubscribe{node = Node, jid = JID, subid = SubId}, + _ = undefined}} -> + unsubscribe_node(Host, Node, From, JID, SubId); + {get, #pubsub{items = #ps_items{node = Node, + max_items = MaxItems, + subid = SubId, + items = Items}, + rsm = RSM, _ = undefined}} -> + ItemIds = [ItemId || #ps_item{id = ItemId} <- Items, ItemId /= <<>>], + get_items(Host, Node, From, SubId, MaxItems, ItemIds, RSM); + {get, #pubsub{subscriptions = {Node, _}, _ = undefined}} -> + Plugins = config(serverhost(Host), plugins), + get_subscriptions(Host, Node, From, Plugins); + {get, #pubsub{affiliations = {Node, _}, _ = undefined}} -> + Plugins = config(serverhost(Host), plugins), + get_affiliations(Host, Node, From, Plugins); + {_, #pubsub{options = #ps_options{jid = undefined}, _ = undefined}} -> + {error, extended_error(xmpp:err_bad_request(), err_jid_required())}; + {_, #pubsub{options = #ps_options{node = <<>>}, _ = undefined}} -> + {error, extended_error(xmpp:err_bad_request(), err_nodeid_required())}; + {get, #pubsub{options = #ps_options{node = Node, subid = SubId, jid = JID}, + _ = undefined}} -> + get_options(Host, Node, JID, SubId, Lang); + {set, #pubsub{options = #ps_options{node = Node, subid = SubId, + jid = JID, xdata = XData}, + _ = undefined}} -> + case decode_subscribe_options(XData, Lang) of + {error, _} = Err -> + Err; + Config -> + set_options(Host, Node, JID, SubId, Config) + end; + {set, #pubsub{}} -> + {error, xmpp:err_bad_request()}; + _ -> + {error, xmpp:err_feature_not_implemented()} end. -iq_command(Host, ServerHost, From, IQ, Access, Plugins) -> - case adhoc:parse_request(IQ) of - Req when is_record(Req, adhoc_request) -> - case adhoc_request(Host, ServerHost, From, Req, Access, - Plugins) - of - Resp when is_record(Resp, adhoc_response) -> - {result, [adhoc:produce_response(Req, Resp)]}; - Error -> Error - end; - Err -> Err +-spec iq_pubsub_owner(binary() | ljid(), iq()) -> {result, pubsub_owner() | undefined} | + {error, stanza_error()}. +iq_pubsub_owner(Host, #iq{type = IQType, from = From, + lang = Lang, sub_els = [SubEl]}) -> + case {IQType, SubEl} of + {get, #pubsub_owner{configure = {Node, undefined}, _ = undefined}} -> + ServerHost = serverhost(Host), + get_configure(Host, ServerHost, Node, From, Lang); + {set, #pubsub_owner{configure = {Node, XData}, _ = undefined}} -> + case XData of + undefined -> + {error, xmpp:err_bad_request(?T("No data form found"), Lang)}; + #xdata{type = cancel} -> + {result, #pubsub_owner{}}; + #xdata{type = submit} -> + case decode_node_config(XData, Host, Lang) of + {error, _} = Err -> + Err; + Config -> + set_configure(Host, Node, From, Config, Lang) + end; + #xdata{} -> + {error, xmpp:err_bad_request(?T("Incorrect data form"), Lang)} + end; + {get, #pubsub_owner{default = {Node, undefined}, _ = undefined}} -> + get_default(Host, Node, From, Lang); + {set, #pubsub_owner{delete = {Node, _}, _ = undefined}} -> + delete_node(Host, Node, From); + {set, #pubsub_owner{purge = Node, _ = undefined}} when Node /= undefined -> + purge_node(Host, Node, From); + {get, #pubsub_owner{subscriptions = {Node, []}, _ = undefined}} -> + get_subscriptions(Host, Node, From); + {set, #pubsub_owner{subscriptions = {Node, Subs}, _ = undefined}} -> + set_subscriptions(Host, Node, From, Subs); + {get, #pubsub_owner{affiliations = {Node, []}, _ = undefined}} -> + get_affiliations(Host, Node, From); + {set, #pubsub_owner{affiliations = {Node, Affs}, _ = undefined}} -> + set_affiliations(Host, Node, From, Affs); + {_, #pubsub_owner{}} -> + {error, xmpp:err_bad_request()}; + _ -> + {error, xmpp:err_feature_not_implemented()} end. -%% @doc

Processes an Ad Hoc Command.

+-spec adhoc_request(binary(), binary(), jid(), adhoc_command(), + atom(), [binary()]) -> adhoc_command() | {error, stanza_error()}. adhoc_request(Host, _ServerHost, Owner, - #adhoc_request{node = ?NS_PUBSUB_GET_PENDING, - lang = Lang, action = <<"execute">>, - xdata = false}, + #adhoc_command{node = ?NS_PUBSUB_GET_PENDING, lang = Lang, + action = execute, xdata = undefined}, _Access, Plugins) -> send_pending_node_form(Host, Owner, Lang, Plugins); adhoc_request(Host, _ServerHost, Owner, - #adhoc_request{node = ?NS_PUBSUB_GET_PENDING, - action = <<"execute">>, xdata = XData}, + #adhoc_command{node = ?NS_PUBSUB_GET_PENDING, lang = Lang, + action = execute, xdata = #xdata{} = XData} = Request, _Access, _Plugins) -> - ParseOptions = case XData of - #xmlel{name = <<"x">>} = XEl -> - case jlib:parse_xdata_submit(XEl) of - invalid -> {error, ?ERR_BAD_REQUEST}; - XData2 -> - case set_xoption(Host, XData2, []) of - NewOpts when is_list(NewOpts) -> - {result, NewOpts}; - Err -> Err - end - end; - _ -> - ?INFO_MSG("Bad XForm: ~p", [XData]), - {error, ?ERR_BAD_REQUEST} - end, - case ParseOptions of - {result, XForm} -> - case lists:keysearch(node, 1, XForm) of - {value, {_, Node}} -> - send_pending_auth_events(Host, Node, Owner); - false -> - {error, - extended_error(?ERR_BAD_REQUEST, <<"bad-payload">>)} - end; - Error -> Error + case decode_get_pending(XData, Lang) of + {error, _} = Err -> + Err; + Config -> + Node = proplists:get_value(node, Config), + case send_pending_auth_events(Host, Node, Owner, Lang) of + ok -> + xmpp_util:make_adhoc_response( + Request, #adhoc_command{status = completed}); + Err -> + Err + end end; adhoc_request(_Host, _ServerHost, _Owner, - #adhoc_request{action = <<"cancel">>}, _Access, - _Plugins) -> - #adhoc_response{status = canceled}; -adhoc_request(Host, ServerHost, Owner, - #adhoc_request{action = <<>>} = R, Access, Plugins) -> - adhoc_request(Host, ServerHost, Owner, - R#adhoc_request{action = <<"execute">>}, Access, - Plugins); -adhoc_request(_Host, _ServerHost, _Owner, Other, - _Access, _Plugins) -> + #adhoc_command{action = cancel}, _Access, _Plugins) -> + #adhoc_command{status = canceled}; +adhoc_request(_Host, _ServerHost, _Owner, Other, _Access, _Plugins) -> ?DEBUG("Couldn't process ad hoc command:~n~p", [Other]), - {error, ?ERR_ITEM_NOT_FOUND}. + {error, xmpp:err_item_not_found()}. -%% @spec (Host, Owner, Lang, Plugins) -> iqRes() -%% @doc

Sends the process pending subscriptions XForm for Host to -%% Owner.

-send_pending_node_form(Host, Owner, _Lang, Plugins) -> - Filter = fun (Plugin) -> - lists:member(<<"get-pending">>, features(Plugin)) - end, +-spec send_pending_node_form(binary(), jid(), binary(), + [binary()]) -> adhoc_command() | {error, stanza_error()}. +send_pending_node_form(Host, Owner, Lang, Plugins) -> + Filter = fun (Type) -> + lists:member(<<"get-pending">>, plugin_features(Host, Type)) + end, case lists:filter(Filter, Plugins) of - [] -> {error, ?ERR_FEATURE_NOT_IMPLEMENTED}; - Ps -> - XOpts = lists:map(fun (Node) -> - #xmlel{name = <<"option">>, attrs = [], - children = - [#xmlel{name = <<"value">>, - attrs = [], - children = - [{xmlcdata, Node}]}]} - end, - get_pending_nodes(Host, Owner, Ps)), - XForm = #xmlel{name = <<"x">>, - attrs = - [{<<"xmlns">>, ?NS_XDATA}, - {<<"type">>, <<"form">>}], - children = - [#xmlel{name = <<"field">>, - attrs = - [{<<"type">>, <<"list-single">>}, - {<<"var">>, <<"pubsub#node">>}], - children = lists:usort(XOpts)}]}, - #adhoc_response{status = executing, - defaultaction = <<"execute">>, elements = [XForm]} + [] -> + Err = extended_error(xmpp:err_feature_not_implemented(), + err_unsupported('get-pending')), + {error, Err}; + Ps -> + case get_pending_nodes(Host, Owner, Ps) of + {ok, Nodes} -> + Form = [{node, <<>>, lists:zip(Nodes, Nodes)}], + XForm = #xdata{type = form, + fields = pubsub_get_pending:encode(Form, Lang)}, + #adhoc_command{status = executing, action = execute, + xdata = XForm}; + Err -> + Err + end end. +-spec get_pending_nodes(binary(), jid(), [binary()]) -> {ok, [binary()]} | + {error, stanza_error()}. get_pending_nodes(Host, Owner, Plugins) -> Tr = fun (Type) -> - case node_call(Type, get_pending_nodes, [Host, Owner]) - of - {result, Nodes} -> Nodes; - _ -> [] - end + case node_call(Host, Type, get_pending_nodes, [Host, Owner]) of + {result, Nodes} -> Nodes; + _ -> [] + end end, - case transaction(fun () -> - {result, lists:flatmap(Tr, Plugins)} - end, - sync_dirty) - of - {result, Res} -> Res; - Err -> Err + Action = fun() -> {result, lists:flatmap(Tr, Plugins)} end, + case transaction(Host, Action, sync_dirty) of + {result, Res} -> {ok, Res}; + Err -> Err end. -%% @spec (Host, Node, Owner) -> iqRes() %% @doc

Send a subscription approval form to Owner for all pending %% subscriptions on Host and Node.

-send_pending_auth_events(Host, Node, Owner) -> - ?DEBUG("Sending pending auth events for ~s on " - "~s:~s", - [jlib:jid_to_string(Owner), Host, Node]), - Action = fun (#pubsub_node{id = NodeID, type = Type}) -> - case lists:member(<<"get-pending">>, features(Type)) of - true -> - case node_call(Type, get_affiliation, - [NodeID, Owner]) - of - {result, owner} -> - node_call(Type, get_node_subscriptions, - [NodeID]); - _ -> {error, ?ERR_FORBIDDEN} - end; - false -> {error, ?ERR_FEATURE_NOT_IMPLEMENTED} - end - end, +-spec send_pending_auth_events(binary(), binary(), jid(), + binary()) -> ok | {error, stanza_error()}. +send_pending_auth_events(Host, Node, Owner, Lang) -> + ?DEBUG("Sending pending auth events for ~ts on ~ts:~ts", + [jid:encode(Owner), Host, Node]), + Action = + fun(#pubsub_node{id = Nidx, type = Type}) -> + case lists:member(<<"get-pending">>, plugin_features(Host, Type)) of + true -> + case node_call(Host, Type, get_affiliation, [Nidx, Owner]) of + {result, owner} -> + node_call(Host, Type, get_node_subscriptions, [Nidx]); + _ -> + {error, xmpp:err_forbidden( + ?T("Owner privileges required"), Lang)} + end; + false -> + {error, extended_error(xmpp:err_feature_not_implemented(), + err_unsupported('get-pending'))} + end + end, case transaction(Host, Node, Action, sync_dirty) of - {result, {N, Subscriptions}} -> - lists:foreach(fun ({J, pending, _SubID}) -> - send_authorization_request(N, jlib:make_jid(J)); - ({J, pending}) -> - send_authorization_request(N, jlib:make_jid(J)); - (_) -> ok - end, - Subscriptions), - #adhoc_response{}; - Err -> Err + {result, {N, Subs}} -> + lists:foreach( + fun({J, pending, _SubId}) -> send_authorization_request(N, jid:make(J)); + ({J, pending}) -> send_authorization_request(N, jid:make(J)); + (_) -> ok + end, Subs); + Err -> + Err end. %%% authorization handling - -send_authorization_request(#pubsub_node{owners = Owners, nodeid = {Host, Node}}, - Subscriber) -> +-spec send_authorization_request(#pubsub_node{}, jid()) -> ok. +send_authorization_request(#pubsub_node{nodeid = {Host, Node}, + type = Type, id = Nidx, owners = O}, + Subscriber) -> + %% TODO: pass lang to this function Lang = <<"en">>, - Stanza = #xmlel{name = <<"message">>, attrs = [], - children = - [#xmlel{name = <<"x">>, - attrs = - [{<<"xmlns">>, ?NS_XDATA}, - {<<"type">>, <<"form">>}], - children = - [#xmlel{name = <<"title">>, attrs = [], - children = - [{xmlcdata, - translate:translate(Lang, - <<"PubSub subscriber request">>)}]}, - #xmlel{name = <<"instructions">>, - attrs = [], - children = - [{xmlcdata, - translate:translate(Lang, - <<"Choose whether to approve this entity's " - "subscription.">>)}]}, - #xmlel{name = <<"field">>, - attrs = - [{<<"var">>, <<"FORM_TYPE">>}, - {<<"type">>, <<"hidden">>}], - children = - [#xmlel{name = <<"value">>, - attrs = [], - children = - [{xmlcdata, - ?NS_PUBSUB_SUB_AUTH}]}]}, - #xmlel{name = <<"field">>, - attrs = - [{<<"var">>, <<"pubsub#node">>}, - {<<"type">>, - <<"text-single">>}, - {<<"label">>, - translate:translate(Lang, - <<"Node ID">>)}], - children = - [#xmlel{name = <<"value">>, - attrs = [], - children = - [{xmlcdata, Node}]}]}, - #xmlel{name = <<"field">>, - attrs = - [{<<"var">>, - <<"pubsub#subscriber_jid">>}, - {<<"type">>, <<"jid-single">>}, - {<<"label">>, - translate:translate(Lang, - <<"Subscriber Address">>)}], - children = - [#xmlel{name = <<"value">>, - attrs = [], - children = - [{xmlcdata, - jlib:jid_to_string(Subscriber)}]}]}, - #xmlel{name = <<"field">>, - attrs = - [{<<"var">>, - <<"pubsub#allow">>}, - {<<"type">>, <<"boolean">>}, - {<<"label">>, - translate:translate(Lang, - <<"Allow this Jabber ID to subscribe to " - "this pubsub node?">>)}], - children = - [#xmlel{name = <<"value">>, - attrs = [], - children = - [{xmlcdata, - <<"false">>}]}]}]}]}, - lists:foreach(fun (Owner) -> - ejabberd_router:route(service_jid(Host), - jlib:make_jid(Owner), Stanza) - end, - Owners). + Fs = pubsub_subscribe_authorization:encode( + [{node, Node}, + {subscriber_jid, Subscriber}, + {allow, false}], + Lang), + X = #xdata{type = form, + title = translate:translate( + Lang, ?T("PubSub subscriber request")), + instructions = [translate:translate( + Lang, + ?T("Choose whether to approve this entity's " + "subscription."))], + fields = Fs}, + Stanza = #message{from = service_jid(Host), sub_els = [X]}, + lists:foreach( + fun (Owner) -> + ejabberd_router:route(xmpp:set_to(Stanza, jid:make(Owner))) + end, node_owners_action(Host, Type, Nidx, O)). +-spec find_authorization_response(message()) -> undefined | + pubsub_subscribe_authorization:result() | + {error, stanza_error()}. find_authorization_response(Packet) -> - #xmlel{children = Els} = Packet, - XData1 = lists:map(fun (#xmlel{name = <<"x">>, - attrs = XAttrs} = - XEl) -> - case xml:get_attr_s(<<"xmlns">>, XAttrs) of - ?NS_XDATA -> - case xml:get_attr_s(<<"type">>, XAttrs) of - <<"cancel">> -> none; - _ -> jlib:parse_xdata_submit(XEl) - end; - _ -> none - end; - (_) -> none - end, - xml:remove_cdata(Els)), - XData = lists:filter(fun (E) -> E /= none end, XData1), - case XData of - [invalid] -> invalid; - [] -> none; - [XFields] when is_list(XFields) -> - ?DEBUG("XFields: ~p", [XFields]), - case lists:keysearch(<<"FORM_TYPE">>, 1, XFields) of - {value, {_, [?NS_PUBSUB_SUB_AUTH]}} -> XFields; - _ -> invalid - end + case xmpp:get_subtag(Packet, #xdata{type = form}) of + #xdata{type = cancel} -> + undefined; + #xdata{type = submit, fields = Fs} -> + try pubsub_subscribe_authorization:decode(Fs) of + Result -> Result + catch _:{pubsub_subscribe_authorization, Why} -> + Lang = xmpp:get_lang(Packet), + Txt = pubsub_subscribe_authorization:format_error(Why), + {error, xmpp:err_bad_request(Txt, Lang)} + end; + #xdata{} -> + {error, xmpp:err_bad_request()}; + false -> + undefined end. -%% @spec (Host, JID, Node, Subscription) -> void -%% Host = mod_pubsub:host() -%% JID = jlib:jid() -%% SNode = string() -%% Subscription = atom() | {atom(), mod_pubsub:subid()} + %% @doc Send a message to JID with the supplied Subscription -%% TODO : ask Christophe's opinion +-spec send_authorization_approval(binary(), jid(), binary(), subscribed | none) -> ok. send_authorization_approval(Host, JID, SNode, Subscription) -> - SubAttrs = case Subscription of -% {S, SID} -> -% [{<<"subscription">>, subscription_to_string(S)}, -% {<<"subid">>, SID}]; - S -> [{<<"subscription">>, subscription_to_string(S)}] - end, - Stanza = event_stanza([#xmlel{name = <<"subscription">>, - attrs = - [{<<"jid">>, jlib:jid_to_string(JID)} - | nodeAttr(SNode)] - ++ SubAttrs, - children = []}]), - ejabberd_router:route(service_jid(Host), JID, Stanza). + Event = #ps_event{subscription = + #ps_subscription{jid = JID, + node = SNode, + type = Subscription}}, + Stanza = #message{from = service_jid(Host), to = JID, sub_els = [Event]}, + ejabberd_router:route(Stanza). -handle_authorization_response(Host, From, To, Packet, XFields) -> - case {lists:keysearch(<<"pubsub#node">>, 1, XFields), - lists:keysearch(<<"pubsub#subscriber_jid">>, 1, XFields), - lists:keysearch(<<"pubsub#allow">>, 1, XFields)} - of - {{value, {_, [Node]}}, {value, {_, [SSubscriber]}}, - {value, {_, [SAllow]}}} -> -% Node = string_to_node(SNode), - Subscriber = jlib:string_to_jid(SSubscriber), - Allow = case SAllow of - <<"1">> -> true; - <<"true">> -> true; - _ -> false - end, - Action = fun (#pubsub_node{type = Type, owners = Owners, - id = NodeId}) -> - IsApprover = - lists:member(jlib:jid_tolower(jlib:jid_remove_resource(From)), - Owners), - {result, Subscriptions} = node_call(Type, - get_subscriptions, - [NodeId, - Subscriber]), - if not IsApprover -> {error, ?ERR_FORBIDDEN}; - true -> - update_auth(Host, Node, Type, NodeId, - Subscriber, Allow, Subscriptions) - end - end, - case transaction(Host, Node, Action, sync_dirty) of - {error, Error} -> - ejabberd_router:route(To, From, - jlib:make_error_reply(Packet, Error)); - {result, {_, _NewSubscription}} -> - %% XXX: notify about subscription state change, section 12.11 - ok; - _ -> - ejabberd_router:route(To, From, - jlib:make_error_reply(Packet, - ?ERR_INTERNAL_SERVER_ERROR)) - end; - _ -> - ejabberd_router:route(To, From, - jlib:make_error_reply(Packet, - ?ERR_NOT_ACCEPTABLE)) +-spec handle_authorization_response(binary(), message(), + pubsub_subscribe_authorization:result()) -> ok. +handle_authorization_response(Host, #message{from = From} = Packet, Response) -> + Node = proplists:get_value(node, Response), + Subscriber = proplists:get_value(subscriber_jid, Response), + Allow = proplists:get_value(allow, Response), + Lang = xmpp:get_lang(Packet), + FromLJID = jid:tolower(jid:remove_resource(From)), + Action = + fun(#pubsub_node{type = Type, id = Nidx, owners = O}) -> + Owners = node_owners_call(Host, Type, Nidx, O), + case lists:member(FromLJID, Owners) of + true -> + case node_call(Host, Type, get_subscriptions, [Nidx, Subscriber]) of + {result, Subs} -> + update_auth(Host, Node, Type, Nidx, Subscriber, Allow, Subs); + {error, _} = Err -> + Err + end; + false -> + {error, xmpp:err_forbidden(?T("Owner privileges required"), Lang)} + end + end, + case transaction(Host, Node, Action, sync_dirty) of + {error, Error} -> + ejabberd_router:route_error(Packet, Error); + {result, {_, _NewSubscription}} -> + %% XXX: notify about subscription state change, section 12.11 + ok end. -update_auth(Host, Node, Type, NodeId, Subscriber, Allow, - Subscriptions) -> - Subscription = lists:filter(fun ({pending, _}) -> true; - (_) -> false - end, - Subscriptions), - case Subscription of - [{pending, SubID}] -> - NewSubscription = case Allow of - true -> subscribed; - false -> none - end, - node_call(Type, set_subscriptions, - [NodeId, Subscriber, NewSubscription, SubID]), - send_authorization_approval(Host, Subscriber, Node, - NewSubscription), - {result, ok}; - _ -> {error, ?ERR_UNEXPECTED_REQUEST} +-spec update_auth(binary(), binary(), _, _, jid() | error, boolean(), _) -> + {result, ok} | {error, stanza_error()}. +update_auth(Host, Node, Type, Nidx, Subscriber, Allow, Subs) -> + Sub= lists:filter(fun + ({pending, _}) -> true; + (_) -> false + end, + Subs), + case Sub of + [{pending, SubId}|_] -> + NewSub = case Allow of + true -> subscribed; + false -> none + end, + node_call(Host, Type, set_subscriptions, [Nidx, Subscriber, NewSub, SubId]), + send_authorization_approval(Host, Subscriber, Node, NewSub), + {result, ok}; + _ -> + Txt = ?T("No pending subscriptions found"), + {error, xmpp:err_unexpected_request(Txt, ejabberd_option:language())} end. --define(XFIELD(Type, Label, Var, Val), - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, Type}, - {<<"label">>, translate:translate(Lang, Label)}, - {<<"var">>, Var}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, Val}]}]}). - --define(BOOLXFIELD(Label, Var, Val), - ?XFIELD(<<"boolean">>, Label, Var, - case Val of - true -> <<"1">>; - _ -> <<"0">> - end)). - --define(STRINGXFIELD(Label, Var, Val), - ?XFIELD(<<"text-single">>, Label, Var, Val)). - --define(STRINGMXFIELD(Label, Var, Vals), - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, <<"text-multi">>}, - {<<"label">>, translate:translate(Lang, Label)}, - {<<"var">>, Var}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, V}]} - || V <- Vals]}). - --define(XFIELDOPT(Type, Label, Var, Val, Opts), - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, Type}, - {<<"label">>, translate:translate(Lang, Label)}, - {<<"var">>, Var}], - children = - lists:map(fun (Opt) -> - #xmlel{name = <<"option">>, attrs = [], - children = - [#xmlel{name = <<"value">>, - attrs = [], - children = - [{xmlcdata, Opt}]}]} - end, - Opts) - ++ - [#xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, Val}]}]}). - --define(LISTXFIELD(Label, Var, Val, Opts), - ?XFIELDOPT(<<"list-single">>, Label, Var, Val, Opts)). - --define(LISTMXFIELD(Label, Var, Vals, Opts), -%% @spec (Host::host(), ServerHost::host(), Node::pubsubNode(), Owner::jid(), NodeType::nodeType()) -> -%% {error, Reason::stanzaError()} | -%% {result, []} %% @doc

Create new pubsub nodes

%%

In addition to method-specific error conditions, there are several general reasons why the node creation request might fail:

%%
    %%
  • The service does not support node creation.
  • %%
  • Only entities that are registered with the service are allowed to create nodes but the requesting entity is not registered.
  • %%
  • The requesting entity does not have sufficient privileges to create nodes.
  • -%%
  • The requested NodeID already exists.
  • -%%
  • The request did not include a NodeID and "instant nodes" are not supported.
  • +%%
  • The requested Node already exists.
  • +%%
  • The request did not include a Node and "instant nodes" are not supported.
  • %%
%%

ote: node creation is a particular case, error return code is evaluated at many places:

%%
    @@ -2584,257 +1502,182 @@ update_auth(Host, Node, Type, NodeId, Subscriber, Allow, %%
  • nodetree create_node checks if nodeid already exists
  • %%
  • node plugin create_node just sets default affiliation/subscription
  • %%
- #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, <<"list-multi">>}, - {<<"label">>, translate:translate(Lang, Label)}, - {<<"var">>, Var}], - children = - lists:map(fun (Opt) -> - #xmlel{name = <<"option">>, attrs = [], - children = - [#xmlel{name = <<"value">>, - attrs = [], - children = - [{xmlcdata, Opt}]}]} - end, - Opts) - ++ - lists:map(fun (Val) -> - #xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, Val}]} - end, - Vals)}). - --spec(create_node/5 :: -( - Host :: mod_pubsub:host(), - ServerHost :: binary(), - Node :: <<>> | mod_pubsub:nodeId(), - Owner :: jid(), - Type :: binary()) - -> {result, [xmlel(),...]} - %%% - | {error, xmlel()} -). - +-spec create_node(host(), binary(), binary(), jid(), + binary()) -> {result, pubsub()} | {error, stanza_error()}. create_node(Host, ServerHost, Node, Owner, Type) -> create_node(Host, ServerHost, Node, Owner, Type, all, []). --spec(create_node/7 :: -( - Host :: mod_pubsub:host(), - ServerHost :: binary(), - Node :: <<>> | mod_pubsub:nodeId(), - Owner :: jid(), - Type :: binary(), - Access :: atom(), - Configuration :: [xmlel()]) - -> {result, [xmlel(),...]} - %%% - | {error, xmlel()} -). +-spec create_node(host(), binary(), binary(), jid(), binary(), + atom(), [{binary(), [binary()]}]) -> {result, pubsub()} | {error, stanza_error()}. create_node(Host, ServerHost, <<>>, Owner, Type, Access, Configuration) -> - case lists:member(<<"instant-nodes">>, features(Type)) of - true -> - NewNode = randoms:get_string(), - case create_node(Host, ServerHost, NewNode, Owner, Type, - Access, Configuration) - of - {result, _} -> - {result, - [#xmlel{name = <<"pubsub">>, - attrs = [{<<"xmlns">>, ?NS_PUBSUB}], - children = - [#xmlel{name = <<"create">>, - attrs = nodeAttr(NewNode), - children = []}]}]}; - Error -> Error - end; - false -> - {error, - extended_error(?ERR_NOT_ACCEPTABLE, - <<"nodeid-required">>)} + case lists:member(<<"instant-nodes">>, plugin_features(Host, Type)) of + true -> + Node = p1_rand:get_string(), + case create_node(Host, ServerHost, Node, Owner, Type, Access, Configuration) of + {result, _} -> + {result, #pubsub{create = Node}}; + Error -> + Error + end; + false -> + {error, extended_error(xmpp:err_not_acceptable(), err_nodeid_required())} end; create_node(Host, ServerHost, Node, Owner, GivenType, Access, Configuration) -> Type = select_type(ServerHost, Host, Node, GivenType), - ParseOptions = case xml:remove_cdata(Configuration) of - [] -> {result, node_options(Type)}; - [#xmlel{name = <<"x">>} = XEl] -> - case jlib:parse_xdata_submit(XEl) of - invalid -> {error, ?ERR_BAD_REQUEST}; - XData -> - case set_xoption(Host, XData, node_options(Type)) - of - NewOpts when is_list(NewOpts) -> - {result, NewOpts}; - Err -> Err - end - end; - _ -> - ?INFO_MSG("Node ~p; bad configuration: ~p", - [Node, Configuration]), - {error, ?ERR_BAD_REQUEST} - end, - case ParseOptions of - {result, NodeOptions} -> - CreateNode = - fun() -> - Parent = case node_call(Type, node_to_path, [Node]) of - {result, [Node]} -> <<>>; - {result, Path} -> element(2, node_call(Type, path_to_node, [lists:sublist(Path, length(Path)-1)])) - end, - Parents = case Parent of - <<>> -> []; - _ -> [Parent] - end, - case node_call(Type, create_node_permission, [Host, ServerHost, Node, Parent, Owner, Access]) of - {result, true} -> - case tree_call(Host, create_node, [Host, Node, Type, Owner, NodeOptions, Parents]) of - {ok, NodeId} -> - ParentTree = tree_call(Host, get_parentnodes_tree, [Host, Node, Owner]), - SubsByDepth = [{Depth, [{N, get_node_subs(N)} || N <- Nodes]} || {Depth, Nodes} <- ParentTree], - case node_call(Type, create_node, [NodeId, Owner]) of - {result, Result} -> {result, {NodeId, SubsByDepth, Result}}; - Error -> Error - end; - {error, {virtual, NodeId}} -> - case node_call(Type, create_node, [NodeId, Owner]) of - {result, Result} -> {result, {NodeId, [], Result}}; + NodeOptions = merge_config( + [node_config(Node, ServerHost), + Configuration, node_options(Host, Type)]), + CreateNode = + fun() -> + Parent = case node_call(Host, Type, node_to_path, [Node]) of + {result, [Node]} -> + <<>>; + {result, Path} -> + element(2, node_call(Host, Type, path_to_node, + [lists:sublist(Path, length(Path)-1)])) + end, + Parents = case Parent of + <<>> -> []; + _ -> [Parent] + end, + case node_call(Host, Type, create_node_permission, + [Host, ServerHost, Node, Parent, Owner, Access]) of + {result, true} -> + case tree_call(Host, create_node, + [Host, Node, Type, Owner, NodeOptions, Parents]) + of + {ok, Nidx} -> + case get_node_subs_by_depth(Host, Node, Owner) of + {result, SubsByDepth} -> + case node_call(Host, Type, create_node, [Nidx, Owner]) of + {result, Result} -> {result, {Nidx, SubsByDepth, Result}}; Error -> Error end; Error -> Error end; - _ -> - {error, ?ERR_FORBIDDEN} - end - end, - Reply = [#xmlel{name = <<"pubsub">>, - attrs = [{<<"xmlns">>, ?NS_PUBSUB}], - children = [#xmlel{name = <<"create">>, - attrs = nodeAttr(Node), - children = []}]}], - case transaction(CreateNode, transaction) of - {result, {NodeId, SubsByDepth, {Result, broadcast}}} -> - broadcast_created_node(Host, Node, NodeId, Type, NodeOptions, SubsByDepth), - ejabberd_hooks:run(pubsub_create_node, ServerHost, [ServerHost, Host, Node, NodeId, NodeOptions]), - case Result of - default -> {result, Reply}; - _ -> {result, Result} - end; - {result, {NodeId, _SubsByDepth, default}} -> - ejabberd_hooks:run(pubsub_create_node, ServerHost, [ServerHost, Host, Node, NodeId, NodeOptions]), - {result, Reply}; - {result, {NodeId, _SubsByDepth, Result}} -> - ejabberd_hooks:run(pubsub_create_node, ServerHost, [ServerHost, Host, Node, NodeId, NodeOptions]), - {result, Result}; - Error -> - %% in case we change transaction to sync_dirty... - %% node_call(Type, delete_node, [Host, Node]), - %% tree_call(Host, delete_node, [Host, Node]), - Error + {error, {virtual, Nidx}} -> + case node_call(Host, Type, create_node, [Nidx, Owner]) of + {result, Result} -> {result, {Nidx, [], Result}}; + Error -> Error + end; + Error -> + Error + end; + {result, _} -> + Txt = ?T("You're not allowed to create nodes"), + {error, xmpp:err_forbidden(Txt, ejabberd_option:language())}; + Err -> + Err + end + end, + Reply = #pubsub{create = Node}, + case transaction(Host, CreateNode, transaction) of + {result, {Nidx, SubsByDepth, {Result, broadcast}}} -> + broadcast_created_node(Host, Node, Nidx, Type, NodeOptions, SubsByDepth), + ejabberd_hooks:run(pubsub_create_node, ServerHost, + [ServerHost, Host, Node, Nidx, NodeOptions]), + case Result of + default -> {result, Reply}; + _ -> {result, Result} + end; + {result, {Nidx, _SubsByDepth, Result}} -> + ejabberd_hooks:run(pubsub_create_node, ServerHost, + [ServerHost, Host, Node, Nidx, NodeOptions]), + case Result of + default -> {result, Reply}; + _ -> {result, Result} end; Error -> + %% in case we change transaction to sync_dirty... + %% node_call(Host, Type, delete_node, [Host, Node]), + %% tree_call(Host, delete_node, [Host, Node]), Error end. -%% @spec (Host, Node, Owner) -> -%% {error, Reason} | {result, []} -%% Host = host() -%% Node = pubsubNode() -%% Owner = jid() -%% Reason = stanzaError() --spec(delete_node/3 :: -( - Host :: mod_pubsub:host(), - Node :: mod_pubsub:nodeId(), - Owner :: jid()) - -> {result, [xmlel(),...]} - %%% - | {error, xmlel()} -). -%% @doc

Delete specified node and all childs.

+%% @doc

Delete specified node and all children.

%%

There are several reasons why the node deletion request might fail:

%%
    %%
  • The requesting entity does not have sufficient privileges to delete the node.
  • %%
  • The node is the root collection node, which cannot be deleted.
  • %%
  • The specified node does not exist.
  • %%
+-spec delete_node(host(), binary(), jid()) -> {result, pubsub_owner()} | {error, stanza_error()}. delete_node(_Host, <<>>, _Owner) -> - {error, ?ERR_NOT_ALLOWED}; + {error, xmpp:err_not_allowed(?T("No node specified"), ejabberd_option:language())}; delete_node(Host, Node, Owner) -> - Action = fun(#pubsub_node{type = Type, id = NodeId}) -> - case node_call(Type, get_affiliation, [NodeId, Owner]) of - {result, owner} -> - ParentTree = tree_call(Host, get_parentnodes_tree, [Host, Node, service_jid(Host)]), - SubsByDepth = [{Depth, [{N, get_node_subs(N)} || N <- Nodes]} || {Depth, Nodes} <- ParentTree], - Removed = tree_call(Host, delete_node, [Host, Node]), - case node_call(Type, delete_node, [Removed]) of - {result, Res} -> {result, {SubsByDepth, Res}}; - Error -> Error - end; - _ -> - %% Entity is not an owner - {error, ?ERR_FORBIDDEN} - end - end, - Reply = [], - ServerHost = get(server_host), + Action = + fun(#pubsub_node{type = Type, id = Nidx}) -> + case node_call(Host, Type, get_affiliation, [Nidx, Owner]) of + {result, owner} -> + case get_node_subs_by_depth(Host, Node, service_jid(Host)) of + {result, SubsByDepth} -> + case tree_call(Host, delete_node, [Host, Node]) of + Removed when is_list(Removed) -> + case node_call(Host, Type, delete_node, [Removed]) of + {result, Res} -> {result, {SubsByDepth, Res}}; + Error -> Error + end; + Error -> + Error + end; + Error -> + Error + end; + {result, _} -> + Lang = ejabberd_option:language(), + {error, xmpp:err_forbidden(?T("Owner privileges required"), Lang)}; + Error -> + Error + end + end, + Reply = undefined, + ServerHost = serverhost(Host), case transaction(Host, Node, Action, transaction) of - {result, {_TNode, {SubsByDepth, {Result, broadcast, Removed}}}} -> - lists:foreach(fun({RNode, _RSubscriptions}) -> - {RH, RN} = RNode#pubsub_node.nodeid, - NodeId = RNode#pubsub_node.id, - Type = RNode#pubsub_node.type, - Options = RNode#pubsub_node.options, - broadcast_removed_node(RH, RN, NodeId, Type, Options, SubsByDepth), - ejabberd_hooks:run(pubsub_delete_node, ServerHost, [ServerHost, RH, RN, NodeId]) - end, Removed), + {result, {_, {SubsByDepth, {Result, broadcast, Removed}}}} -> + lists:foreach(fun ({RNode, _RSubs}) -> + {RH, RN} = RNode#pubsub_node.nodeid, + RNidx = RNode#pubsub_node.id, + RType = RNode#pubsub_node.type, + ROptions = RNode#pubsub_node.options, + unset_cached_item(RH, RNidx), + broadcast_removed_node(RH, RN, RNidx, RType, ROptions, SubsByDepth), + ejabberd_hooks:run(pubsub_delete_node, + ServerHost, + [ServerHost, RH, RN, RNidx]) + end, + Removed), case Result of default -> {result, Reply}; _ -> {result, Result} end; - {result, {_TNode, {_, {Result, Removed}}}} -> - lists:foreach(fun({RNode, _RSubscriptions}) -> - {RH, RN} = RNode#pubsub_node.nodeid, - NodeId = RNode#pubsub_node.id, - ejabberd_hooks:run(pubsub_delete_node, ServerHost, [ServerHost, RH, RN, NodeId]) - end, Removed), + {result, {_, {_, {Result, Removed}}}} -> + lists:foreach(fun ({RNode, _RSubs}) -> + {RH, RN} = RNode#pubsub_node.nodeid, + RNidx = RNode#pubsub_node.id, + unset_cached_item(RH, RNidx), + ejabberd_hooks:run(pubsub_delete_node, + ServerHost, + [ServerHost, RH, RN, RNidx]) + end, + Removed), case Result of default -> {result, Reply}; _ -> {result, Result} end; - {result, {TNode, {_, default}}} -> - NodeId = TNode#pubsub_node.id, - ejabberd_hooks:run(pubsub_delete_node, ServerHost, [ServerHost, Host, Node, NodeId]), - {result, Reply}; {result, {TNode, {_, Result}}} -> - NodeId = TNode#pubsub_node.id, - ejabberd_hooks:run(pubsub_delete_node, ServerHost, [ServerHost, Host, Node, NodeId]), - {result, Result}; + Nidx = TNode#pubsub_node.id, + unset_cached_item(Host, Nidx), + ejabberd_hooks:run(pubsub_delete_node, ServerHost, + [ServerHost, Host, Node, Nidx]), + case Result of + default -> {result, Reply}; + _ -> {result, Result} + end; Error -> Error end. -%% @spec (Host, Node, From, JID, Configuration) -> -%% {error, Reason::stanzaError()} | -%% {result, []} -%% Host = host() -%% Node = pubsubNode() -%% From = jid() -%% JID = jid() --spec(subscribe_node/5 :: -( - Host :: mod_pubsub:host(), - Node :: mod_pubsub:nodeId(), - From :: jid(), - JID :: binary(), - Configuration :: [xmlel()]) - -> {result, [xmlel(),...]} - %%% - | {error, xmlel()} -). %% @see node_hometree:subscribe_node/5 %% @doc

Accepts or rejects subcription requests on a PubSub node.

%%

There are several reasons why the subscription request might fail:

@@ -2850,108 +1693,101 @@ delete_node(Host, Node, Owner) -> %%
  • The node does not support subscriptions.
  • %%
  • The node does not exist.
  • %% +-spec subscribe_node(host(), binary(), jid(), jid(), [{binary(), [binary()]}]) -> + {result, pubsub()} | {error, stanza_error()}. subscribe_node(Host, Node, From, JID, Configuration) -> - SubOpts = case - pubsub_subscription:parse_options_xform(Configuration) - of - {result, GoodSubOpts} -> GoodSubOpts; - _ -> invalid - end, - Subscriber = case jlib:string_to_jid(JID) of - error -> {<<"">>, <<"">>, <<"">>}; - J -> - case jlib:jid_tolower(J) of - error -> {<<"">>, <<"">>, <<"">>}; - J1 -> J1 - end - end, - Action = fun (#pubsub_node{options = Options, - owners = Owners, type = Type, id = NodeId}) -> - Features = features(Type), - SubscribeFeature = lists:member(<<"subscribe">>, Features), - OptionsFeature = lists:member(<<"subscription-options">>, Features), - HasOptions = not (SubOpts == []), - SubscribeConfig = get_option(Options, subscribe), - AccessModel = get_option(Options, access_model), - SendLast = get_option(Options, send_last_published_item), - AllowedGroups = get_option(Options, roster_groups_allowed, []), - {PresenceSubscription, RosterGroup} = - get_presence_and_roster_permissions(Host, Subscriber, - Owners, AccessModel, AllowedGroups), - if not SubscribeFeature -> - {error, - extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, - unsupported, <<"subscribe">>)}; - not SubscribeConfig -> - {error, - extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, - unsupported, <<"subscribe">>)}; - HasOptions andalso not OptionsFeature -> - {error, - extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, - unsupported, - <<"subscription-options">>)}; - SubOpts == invalid -> - {error, - extended_error(?ERR_BAD_REQUEST, - <<"invalid-options">>)}; - true -> - node_call(Type, subscribe_node, - [NodeId, From, Subscriber, AccessModel, - SendLast, PresenceSubscription, - RosterGroup, SubOpts]) - end - end, - Reply = fun (Subscription) -> - SubAttrs = case Subscription of - {subscribed, SubId} -> - [{<<"subscription">>, - subscription_to_string(subscribed)}, - {<<"subid">>, SubId}, {<<"node">>, Node}]; - Other -> - [{<<"subscription">>, - subscription_to_string(Other)}, - {<<"node">>, Node}] - end, - Fields = [{<<"jid">>, jlib:jid_to_string(Subscriber)} - | SubAttrs], - [#xmlel{name = <<"pubsub">>, - attrs = [{<<"xmlns">>, ?NS_PUBSUB}], - children = - [#xmlel{name = <<"subscription">>, - attrs = Fields, children = []}]}] + SubModule = subscription_plugin(Host), + SubOpts = case SubModule:parse_options_xform(Configuration) of + {result, GoodSubOpts} -> GoodSubOpts; + _ -> invalid + end, + Subscriber = jid:tolower(JID), + Action = fun (#pubsub_node{options = Options, type = Type, id = Nidx, owners = O}) -> + Features = plugin_features(Host, Type), + SubscribeFeature = lists:member(<<"subscribe">>, Features), + OptionsFeature = lists:member(<<"subscription-options">>, Features), + HasOptions = not (SubOpts == []), + SubscribeConfig = get_option(Options, subscribe), + AccessModel = get_option(Options, access_model), + SendLast = get_option(Options, send_last_published_item), + AllowedGroups = get_option(Options, roster_groups_allowed, []), + CanSubscribe = case get_max_subscriptions_node(Host) of + Max when is_integer(Max) -> + case node_call(Host, Type, get_node_subscriptions, [Nidx]) of + {result, NodeSubs} -> + SubsNum = lists:foldl( + fun ({_, subscribed, _}, Acc) -> Acc+1; + (_, Acc) -> Acc + end, 0, NodeSubs), + SubsNum < Max; + _ -> + true + end; + _ -> + true end, + if not SubscribeFeature -> + {error, extended_error(xmpp:err_feature_not_implemented(), + err_unsupported('subscribe'))}; + not SubscribeConfig -> + {error, extended_error(xmpp:err_feature_not_implemented(), + err_unsupported('subscribe'))}; + HasOptions andalso not OptionsFeature -> + {error, extended_error(xmpp:err_feature_not_implemented(), + err_unsupported('subscription-options'))}; + SubOpts == invalid -> + {error, extended_error(xmpp:err_bad_request(), + err_invalid_options())}; + not CanSubscribe -> + %% fallback to closest XEP compatible result, assume we are not allowed to subscribe + {error, extended_error(xmpp:err_not_allowed(), + err_closed_node())}; + true -> + Owners = node_owners_call(Host, Type, Nidx, O), + {PS, RG} = get_presence_and_roster_permissions(Host, JID, + Owners, AccessModel, AllowedGroups), + node_call(Host, Type, subscribe_node, + [Nidx, From, Subscriber, AccessModel, + SendLast, PS, RG, SubOpts]) + end + end, + Reply = fun (Subscription) -> + Sub = case Subscription of + {subscribed, SubId} -> + #ps_subscription{jid = JID, type = subscribed, subid = SubId}; + Other -> + #ps_subscription{jid = JID, type = Other} + end, + #pubsub{subscription = Sub#ps_subscription{node = Node}} + end, case transaction(Host, Node, Action, sync_dirty) of - {result, - {TNode, {Result, subscribed, SubId, send_last}}} -> - NodeId = TNode#pubsub_node.id, - Type = TNode#pubsub_node.type, - send_items(Host, Node, NodeId, Type, Subscriber, last), - case Result of - default -> {result, Reply({subscribed, SubId})}; - _ -> {result, Result} - end; - {result, {_TNode, {default, subscribed, SubId}}} -> - {result, Reply({subscribed, SubId})}; - {result, {_TNode, {Result, subscribed, _SubId}}} -> - {result, Result}; - {result, {TNode, {default, pending, _SubId}}} -> - send_authorization_request(TNode, Subscriber), - {result, Reply(pending)}; - {result, {TNode, {Result, pending}}} -> - send_authorization_request(TNode, Subscriber), - {result, Result}; - {result, {_, Result}} -> {result, Result}; - Error -> Error + {result, {TNode, {Result, subscribed, SubId, send_last}}} -> + Nidx = TNode#pubsub_node.id, + Type = TNode#pubsub_node.type, + Options = TNode#pubsub_node.options, + send_items(Host, Node, Nidx, Type, Options, Subscriber, last), + ServerHost = serverhost(Host), + ejabberd_hooks:run(pubsub_subscribe_node, ServerHost, + [ServerHost, Host, Node, Subscriber, SubId]), + case Result of + default -> {result, Reply({subscribed, SubId})}; + _ -> {result, Result} + end; + {result, {_TNode, {default, subscribed, SubId}}} -> + {result, Reply({subscribed, SubId})}; + {result, {_TNode, {Result, subscribed, _SubId}}} -> + {result, Result}; + {result, {TNode, {default, pending, _SubId}}} -> + send_authorization_request(TNode, JID), + {result, Reply(pending)}; + {result, {TNode, {Result, pending}}} -> + send_authorization_request(TNode, JID), + {result, Result}; + {result, {_, Result}} -> + {result, Result}; + Error -> Error end. -%% @spec (Host, Noce, From, JID, SubId) -> {error, Reason} | {result, []} -%% Host = host() -%% Node = pubsubNode() -%% From = jid() -%% JID = string() -%% SubId = string() -%% Reason = stanzaError() %% @doc

    Unsubscribe JID from the Node.

    %%

    There are several reasons why the unsubscribe request might fail:

    %%
      @@ -2961,42 +1797,22 @@ subscribe_node(Host, Node, From, JID, Configuration) -> %%
    • The node does not exist.
    • %%
    • The request specifies a subscription ID that is not valid or current.
    • %%
    --spec(unsubscribe_node/5 :: -( - Host :: mod_pubsub:host(), - Node :: mod_pubsub:nodeId(), - From :: jid(), - JID :: binary() | ljid(), - SubId :: mod_pubsub:subId()) - -> {result, []} - %%% - | {error, xmlel()} -). -unsubscribe_node(Host, Node, From, JID, SubId) - when is_binary(JID) -> - Subscriber = case jlib:string_to_jid(JID) of - error -> {<<"">>, <<"">>, <<"">>}; - J -> - case jlib:jid_tolower(J) of - error -> {<<"">>, <<"">>, <<"">>}; - J1 -> J1 - end - end, - unsubscribe_node(Host, Node, From, Subscriber, SubId); -unsubscribe_node(Host, Node, From, Subscriber, SubId) -> - Action = fun (#pubsub_node{type = Type, id = NodeId}) -> - node_call(Type, unsubscribe_node, - [NodeId, From, Subscriber, SubId]) - end, +-spec unsubscribe_node(host(), binary(), jid(), jid(), binary()) -> + {result, undefined} | {error, stanza_error()}. +unsubscribe_node(Host, Node, From, JID, SubId) -> + Subscriber = jid:tolower(JID), + Action = fun (#pubsub_node{type = Type, id = Nidx}) -> + node_call(Host, Type, unsubscribe_node, [Nidx, From, Subscriber, SubId]) + end, case transaction(Host, Node, Action, sync_dirty) of - {result, {_, default}} -> {result, []}; -% {result, {_, Result}} -> {result, Result}; - Error -> Error + {result, {_, default}} -> + ServerHost = serverhost(Host), + ejabberd_hooks:run(pubsub_unsubscribe_node, ServerHost, + [ServerHost, Host, Node, Subscriber, SubId]), + {result, undefined}; + Error -> Error end. -%% @spec (Host::host(), ServerHost::host(), JID::jid(), Node::pubsubNode(), ItemId::string(), Payload::term()) -> -%% {error, Reason::stanzaError()} | -%% {result, []} %% @doc

    Publish item to a PubSub node.

    %%

    The permission to publish an item must be verified by the plugin implementation.

    %%

    There are several reasons why the publish request might fail:

    @@ -3008,147 +1824,109 @@ unsubscribe_node(Host, Node, From, Subscriber, SubId) -> %%
  • The item contains more than one payload element or the namespace of the root payload element does not match the configured namespace for the node.
  • %%
  • The request does not match the node configuration.
  • %% --spec(publish_item/6 :: -( - Host :: mod_pubsub:host(), - ServerHost :: binary(), - Node :: mod_pubsub:nodeId(), - Publisher :: jid(), - ItemId :: <<>> | mod_pubsub:itemId(), - Payload :: mod_pubsub:payload()) - -> {result, [xmlel(),...]} - %%% - | {error, xmlel()} -). +-spec publish_item(host(), binary(), binary(), jid(), binary(), + [xmlel()]) -> {result, pubsub()} | {error, stanza_error()}. publish_item(Host, ServerHost, Node, Publisher, ItemId, Payload) -> - publish_item(Host, ServerHost, Node, Publisher, ItemId, Payload, all). -publish_item(Host, ServerHost, Node, Publisher, <<>>, Payload, Access) -> - publish_item(Host, ServerHost, Node, Publisher, uniqid(), Payload, Access); -publish_item(Host, ServerHost, Node, Publisher, ItemId, Payload, Access) -> - Action = fun (#pubsub_node{options = Options, type = Type, id = NodeId}) -> - Features = features(Type), - PublishFeature = lists:member(<<"publish">>, Features), - PublishModel = get_option(Options, publish_model), - DeliverPayloads = get_option(Options, deliver_payloads), - PersistItems = get_option(Options, persist_items), - MaxItems = max_items(Host, Options), - PayloadCount = payload_xmlelements(Payload), - PayloadSize = byte_size(term_to_binary(Payload)) - 2, - PayloadMaxSize = get_option(Options, max_payload_size), - if not PublishFeature -> - {error, - extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, - unsupported, <<"publish">>)}; - PayloadSize > PayloadMaxSize -> - {error, - extended_error(?ERR_NOT_ACCEPTABLE, <<"payload-too-big">>)}; - (PayloadCount == 0) and (Payload == []) -> - {error, - extended_error(?ERR_BAD_REQUEST, <<"payload-required">>)}; - (PayloadCount > 1) or (PayloadCount == 0) -> - {error, - extended_error(?ERR_BAD_REQUEST, <<"invalid-payload">>)}; - (DeliverPayloads == false) and (PersistItems == false) and - (PayloadSize > 0) -> - {error, - extended_error(?ERR_BAD_REQUEST, <<"item-forbidden">>)}; - ((DeliverPayloads == true) or (PersistItems == true)) and - (PayloadSize == 0) -> - {error, - extended_error(?ERR_BAD_REQUEST, <<"item-required">>)}; - true -> - node_call(Type, publish_item, [NodeId, Publisher, PublishModel, MaxItems, ItemId, Payload]) - end - end, - ejabberd_hooks:run(pubsub_publish_item, ServerHost, [ServerHost, Node, Publisher, service_jid(Host), ItemId, Payload]), - Reply = [#xmlel{name = <<"pubsub">>, - attrs = [{<<"xmlns">>, ?NS_PUBSUB}], - children = - [#xmlel{name = <<"publish">>, attrs = nodeAttr(Node), - children = - [#xmlel{name = <<"item">>, - attrs = itemAttr(ItemId), - children = []}]}]}], + publish_item(Host, ServerHost, Node, Publisher, ItemId, Payload, [], all). +publish_item(Host, ServerHost, Node, Publisher, <<>>, Payload, PubOpts, Access) -> + publish_item(Host, ServerHost, Node, Publisher, uniqid(), Payload, PubOpts, Access); +publish_item(Host, ServerHost, Node, Publisher, ItemId, Payload, PubOpts, Access) -> + Action = fun (#pubsub_node{options = Options, type = Type, id = Nidx}) -> + Features = plugin_features(Host, Type), + PublishFeature = lists:member(<<"publish">>, Features), + PublishModel = get_option(Options, publish_model), + DeliverPayloads = get_option(Options, deliver_payloads), + PersistItems = get_option(Options, persist_items), + MaxItems = max_items(Host, Options), + PayloadCount = payload_xmlelements(Payload), + PayloadSize = byte_size(term_to_binary(Payload)) - 2, + PayloadMaxSize = get_option(Options, max_payload_size), + PreconditionsMet = preconditions_met(PubOpts, Options), + if not PublishFeature -> + {error, extended_error(xmpp:err_feature_not_implemented(), + err_unsupported(publish))}; + not PreconditionsMet -> + {error, extended_error(xmpp:err_conflict(), + err_precondition_not_met())}; + PayloadSize > PayloadMaxSize -> + {error, extended_error(xmpp:err_not_acceptable(), + err_payload_too_big())}; + (DeliverPayloads or PersistItems) and (PayloadCount == 0) -> + {error, extended_error(xmpp:err_bad_request(), + err_item_required())}; + (DeliverPayloads or PersistItems) and (PayloadCount > 1) -> + {error, extended_error(xmpp:err_bad_request(), + err_invalid_payload())}; + (not (DeliverPayloads or PersistItems)) and (PayloadCount > 0) -> + {error, extended_error(xmpp:err_bad_request(), + err_item_forbidden())}; + true -> + node_call(Host, Type, publish_item, + [Nidx, Publisher, PublishModel, MaxItems, ItemId, Payload, PubOpts]) + end + end, + Reply = #pubsub{publish = #ps_publish{node = Node, + items = [#ps_item{id = ItemId}]}}, case transaction(Host, Node, Action, sync_dirty) of {result, {TNode, {Result, Broadcast, Removed}}} -> - NodeId = TNode#pubsub_node.id, + Nidx = TNode#pubsub_node.id, Type = TNode#pubsub_node.type, Options = TNode#pubsub_node.options, + BrPayload = case Broadcast of + broadcast -> Payload; + PluginPayload -> PluginPayload + end, + set_cached_item(Host, Nidx, ItemId, Publisher, BrPayload), case get_option(Options, deliver_notifications) of - true -> - BroadcastPayload = case Broadcast of - default -> Payload; - broadcast -> Payload; - PluginPayload -> PluginPayload - end, - broadcast_publish_item(Host, Node, NodeId, Type, Options, - Removed, ItemId, jlib:jid_tolower(Publisher), - BroadcastPayload); - false -> - ok - end, - set_cached_item(Host, NodeId, ItemId, Publisher, Payload), + true -> + broadcast_publish_item(Host, Node, Nidx, Type, Options, ItemId, + Publisher, BrPayload, Removed); + false -> + ok + end, + ejabberd_hooks:run(pubsub_publish_item, ServerHost, + [ServerHost, Node, Publisher, service_jid(Host), ItemId, BrPayload]), case Result of default -> {result, Reply}; _ -> {result, Result} end; {result, {TNode, {default, Removed}}} -> - NodeId = TNode#pubsub_node.id, + Nidx = TNode#pubsub_node.id, Type = TNode#pubsub_node.type, Options = TNode#pubsub_node.options, - broadcast_retract_items(Host, Node, NodeId, Type, Options, Removed), - set_cached_item(Host, NodeId, ItemId, Publisher, Payload), + broadcast_retract_items(Host, Publisher, Node, Nidx, Type, Options, Removed), + set_cached_item(Host, Nidx, ItemId, Publisher, Payload), {result, Reply}; {result, {TNode, {Result, Removed}}} -> - NodeId = TNode#pubsub_node.id, + Nidx = TNode#pubsub_node.id, Type = TNode#pubsub_node.type, Options = TNode#pubsub_node.options, - broadcast_retract_items(Host, Node, NodeId, Type, Options, Removed), - set_cached_item(Host, NodeId, ItemId, Publisher, Payload), + broadcast_retract_items(Host, Publisher, Node, Nidx, Type, Options, Removed), + set_cached_item(Host, Nidx, ItemId, Publisher, Payload), {result, Result}; {result, {_, default}} -> {result, Reply}; {result, {_, Result}} -> {result, Result}; - {error, ?ERR_ITEM_NOT_FOUND} -> - %% handles auto-create feature - %% for automatic node creation. we'll take the default node type: - %% first listed into the plugins configuration option, or pep + {error, #stanza_error{reason = 'item-not-found'}} -> Type = select_type(ServerHost, Host, Node), - case lists:member(<<"auto-create">>, features(Type)) of + case lists:member(<<"auto-create">>, plugin_features(Host, Type)) of true -> - case create_node(Host, ServerHost, Node, Publisher, Type, Access, []) of - {result, [#xmlel{name = <<"pubsub">>, - attrs = [{<<"xmlns">>, ?NS_PUBSUB}], - children = - [#xmlel{name = <<"create">>, - attrs = [{<<"node">>, NewNode}], - children = []}]}]} -> - publish_item(Host, ServerHost, NewNode, - Publisher, ItemId, Payload); + case create_node(Host, ServerHost, Node, Publisher, Type, Access, PubOpts) of + {result, #pubsub{create = NewNode}} -> + publish_item(Host, ServerHost, NewNode, Publisher, ItemId, + Payload, PubOpts, Access); _ -> - {error, ?ERR_ITEM_NOT_FOUND} + {error, xmpp:err_item_not_found()} end; false -> - {error, ?ERR_ITEM_NOT_FOUND} + Txt = ?T("Automatic node creation is not enabled"), + {error, xmpp:err_item_not_found(Txt, ejabberd_option:language())} end; Error -> Error end. -%% @spec (Host::host(), JID::jid(), Node::pubsubNode(), ItemId::string()) -> -%% {error, Reason::stanzaError()} | -%% {result, []} --spec(delete_item/4 :: -( - Host :: mod_pubsub:host(), - Node :: mod_pubsub:nodeId(), - Publisher :: jid(), - ItemId :: mod_pubsub:itemId()) - -> {result, []} - %%% - | {error, xmlel()} -). %% @doc

    Delete item from a PubSub node.

    %%

    The permission to delete an item must be verified by the plugin implementation.

    %%

    There are several reasons why the item retraction request might fail:

    @@ -3160,64 +1938,57 @@ publish_item(Host, ServerHost, Node, Publisher, ItemId, Payload, Access) -> %%
  • The node does not support persistent items.
  • %%
  • The service does not support the deletion of items.
  • %% +-spec delete_item(host(), binary(), jid(), binary()) -> {result, undefined} | + {error, stanza_error()}. delete_item(Host, Node, Publisher, ItemId) -> delete_item(Host, Node, Publisher, ItemId, false). - - -delete_item(_, <<"">>, _, _, _) -> - {error, - extended_error(?ERR_BAD_REQUEST, <<"node-required">>)}; +delete_item(_, <<>>, _, _, _) -> + {error, extended_error(xmpp:err_bad_request(), err_nodeid_required())}; delete_item(Host, Node, Publisher, ItemId, ForceNotify) -> - Action = fun (#pubsub_node{options = Options, type = Type, id = NodeId}) -> - Features = features(Type), - PersistentFeature = lists:member(<<"persistent-items">>, Features), - DeleteFeature = lists:member(<<"delete-items">>, Features), - PublishModel = get_option(Options, publish_model), - if %%-> iq_pubsub just does that matchs - %% %% Request does not specify an item - %% {error, extended_error(?ERR_BAD_REQUEST, "item-required")}; - not PersistentFeature -> - {error, - extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, - unsupported, - <<"persistent-items">>)}; - not DeleteFeature -> - {error, - extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, - unsupported, <<"delete-items">>)}; - true -> - node_call(Type, delete_item, - [NodeId, Publisher, PublishModel, ItemId]) - end - end, - Reply = [], + Action = fun (#pubsub_node{options = Options, type = Type, id = Nidx}) -> + Features = plugin_features(Host, Type), + PersistentFeature = lists:member(<<"persistent-items">>, Features), + DeleteFeature = lists:member(<<"delete-items">>, Features), + PublishModel = get_option(Options, publish_model), + if %%-> iq_pubsub just does that matches + %% %% Request does not specify an item + %% {error, extended_error(?ERR_BAD_REQUEST, "item-required")}; + not PersistentFeature -> + {error, extended_error(xmpp:err_feature_not_implemented(), + err_unsupported('persistent-items'))}; + not DeleteFeature -> + {error, extended_error(xmpp:err_feature_not_implemented(), + err_unsupported('delete-items'))}; + true -> + node_call(Host, Type, delete_item, [Nidx, Publisher, PublishModel, ItemId]) + end + end, + Reply = undefined, case transaction(Host, Node, Action, sync_dirty) of - {result, {TNode, {Result, broadcast}}} -> - NodeId = TNode#pubsub_node.id, - Type = TNode#pubsub_node.type, - Options = TNode#pubsub_node.options, - broadcast_retract_items(Host, Node, NodeId, Type, - Options, [ItemId], ForceNotify), - case get_cached_item(Host, NodeId) of - #pubsub_item{itemid = {ItemId, NodeId}} -> - unset_cached_item(Host, NodeId); - _ -> ok - end, - case Result of - default -> {result, Reply}; - _ -> {result, Result} - end; - {result, {_, default}} -> {result, Reply}; - {result, {_, Result}} -> {result, Result}; - Error -> Error + {result, {TNode, {Result, broadcast}}} -> + Nidx = TNode#pubsub_node.id, + Type = TNode#pubsub_node.type, + Options = TNode#pubsub_node.options, + ServerHost = serverhost(Host), + ejabberd_hooks:run(pubsub_delete_item, ServerHost, + [ServerHost, Node, Publisher, service_jid(Host), ItemId]), + broadcast_retract_items(Host, Publisher, Node, Nidx, Type, Options, [ItemId], ForceNotify), + case get_cached_item(Host, Nidx) of + #pubsub_item{itemid = {ItemId, Nidx}} -> unset_cached_item(Host, Nidx); + _ -> ok + end, + case Result of + default -> {result, Reply}; + _ -> {result, Result} + end; + {result, {_, default}} -> + {result, Reply}; + {result, {_, Result}} -> + {result, Result}; + Error -> + Error end. -%% @spec (Host, JID, Node) -> -%% {error, Reason} | {result, []} -%% Host = host() -%% Node = pubsubNode() -%% JID = jid() -%% Reason = stanzaError() %% @doc

    Delete all items of specified node owned by JID.

    %%

    There are several reasons why the node purge request might fail:

    %%
      @@ -3226,1123 +1997,815 @@ delete_item(Host, Node, Publisher, ItemId, ForceNotify) -> %%
    • The node is not configured to persist items.
    • %%
    • The specified node does not exist.
    • %%
    --spec(purge_node/3 :: -( - Host :: mod_pubsub:host(), - Node :: mod_pubsub:nodeId(), - Owner :: jid()) - -> {result, []} - %%% - | {error, xmlel()} -). +-spec purge_node(mod_pubsub:host(), binary(), jid()) -> {result, undefined} | + {error, stanza_error()}. purge_node(Host, Node, Owner) -> - Action = fun (#pubsub_node{options = Options, type = Type, id = NodeId}) -> - Features = features(Type), - PurgeFeature = lists:member(<<"purge-nodes">>, Features), - PersistentFeature = lists:member(<<"persistent-items">>, Features), - PersistentConfig = get_option(Options, persist_items), - if not PurgeFeature -> - {error, - extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, - unsupported, <<"purge-nodes">>)}; - not PersistentFeature -> - {error, - extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, - unsupported, - <<"persistent-items">>)}; - not PersistentConfig -> - {error, - extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, - unsupported, - <<"persistent-items">>)}; - true -> node_call(Type, purge_node, [NodeId, Owner]) - end - end, - Reply = [], - case transaction(Host, Node, Action, sync_dirty) of - {result, {TNode, {Result, broadcast}}} -> - NodeId = TNode#pubsub_node.id, - Type = TNode#pubsub_node.type, - Options = TNode#pubsub_node.options, - broadcast_purge_node(Host, Node, NodeId, Type, Options), - unset_cached_item(Host, NodeId), - case Result of - default -> {result, Reply}; - _ -> {result, Result} - end; - {result, {_, default}} -> {result, Reply}; - {result, {_, Result}} -> {result, Result}; - Error -> Error + Action = fun (#pubsub_node{options = Options, type = Type, id = Nidx}) -> + Features = plugin_features(Host, Type), + PurgeFeature = lists:member(<<"purge-nodes">>, Features), + PersistentFeature = lists:member(<<"persistent-items">>, Features), + PersistentConfig = get_option(Options, persist_items), + if not PurgeFeature -> + {error, extended_error(xmpp:err_feature_not_implemented(), + err_unsupported('purge-nodes'))}; + not PersistentFeature -> + {error, extended_error(xmpp:err_feature_not_implemented(), + err_unsupported('persistent-items'))}; + not PersistentConfig -> + {error, extended_error(xmpp:err_feature_not_implemented(), + err_unsupported('persistent-items'))}; + true -> node_call(Host, Type, purge_node, [Nidx, Owner]) + end + end, + Reply = undefined, + case transaction(Host, Node, Action, transaction) of + {result, {TNode, {Result, broadcast}}} -> + Nidx = TNode#pubsub_node.id, + Type = TNode#pubsub_node.type, + Options = TNode#pubsub_node.options, + broadcast_purge_node(Host, Node, Nidx, Type, Options), + unset_cached_item(Host, Nidx), + case Result of + default -> {result, Reply}; + _ -> {result, Result} + end; + {result, {_, default}} -> + {result, Reply}; + {result, {_, Result}} -> + {result, Result}; + Error -> + Error end. %% @doc

    Return the items of a given node.

    %%

    The number of items to return is limited by MaxItems.

    %%

    The permission are not checked in this function.

    -%% @todo We probably need to check that the user doing the query has the right -%% to read the items. --spec(get_items/6 :: -( - Host :: mod_pubsub:host(), - Node :: mod_pubsub:nodeId(), - From :: jid(), - SubId :: mod_pubsub:subId(), - SMaxItems :: binary(), - ItemIDs :: [mod_pubsub:itemId()]) - -> {result, [xmlel(),...]} - %%% - | {error, xmlel()} -). -get_items(Host, Node, From, SubId, SMaxItems, ItemIDs) -> - MaxItems = if SMaxItems == <<"">> -> - get_max_items_node(Host); - true -> - case catch jlib:binary_to_integer(SMaxItems) of - {'EXIT', _} -> {error, ?ERR_BAD_REQUEST}; - Val -> Val - end - end, - case MaxItems of - {error, Error} -> {error, Error}; - _ -> - Action = fun (#pubsub_node{options = Options, type = Type, id = NodeId, - owners = Owners}) -> - Features = features(Type), - RetreiveFeature = lists:member(<<"retrieve-items">>, Features), - PersistentFeature = lists:member(<<"persistent-items">>, Features), - AccessModel = get_option(Options, access_model), - AllowedGroups = get_option(Options, roster_groups_allowed, []), - {PresenceSubscription, RosterGroup} = - get_presence_and_roster_permissions(Host, From, Owners, - AccessModel, AllowedGroups), - if not RetreiveFeature -> - {error, - extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, - unsupported, - <<"retrieve-items">>)}; - not PersistentFeature -> - {error, - extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, - unsupported, - <<"persistent-items">>)}; - true -> - node_call(Type, get_items, - [NodeId, From, AccessModel, - PresenceSubscription, RosterGroup, - SubId]) - end - end, - case transaction(Host, Node, Action, sync_dirty) of - {result, {_, Items}} -> - SendItems = case ItemIDs of - [] -> Items; - _ -> - lists:filter(fun (#pubsub_item{itemid = - {ItemId, - _}}) -> - lists:member(ItemId, - ItemIDs) - end, - Items) - end, - {result, - [#xmlel{name = <<"pubsub">>, - attrs = [{<<"xmlns">>, ?NS_PUBSUB}], - children = - [#xmlel{name = <<"items">>, attrs = nodeAttr(Node), - children = - itemsEls(lists:sublist(SendItems, - MaxItems))}]}]}; - Error -> Error - end +-spec get_items(host(), binary(), jid(), binary(), + undefined | non_neg_integer(), [binary()], undefined | rsm_set()) -> + {result, pubsub()} | {error, stanza_error()}. +get_items(Host, Node, From, SubId, MaxItems, ItemIds, undefined) + when MaxItems =/= undefined -> + get_items(Host, Node, From, SubId, MaxItems, ItemIds, + #rsm_set{max = MaxItems, before = <<>>}); +get_items(Host, Node, From, SubId, _MaxItems, ItemIds, RSM) -> + Action = + fun(#pubsub_node{options = Options, type = Type, + id = Nidx, owners = O}) -> + Features = plugin_features(Host, Type), + RetreiveFeature = lists:member(<<"retrieve-items">>, Features), + PersistentFeature = lists:member(<<"persistent-items">>, Features), + AccessModel = get_option(Options, access_model), + AllowedGroups = get_option(Options, roster_groups_allowed, []), + if not RetreiveFeature -> + {error, extended_error(xmpp:err_feature_not_implemented(), + err_unsupported('retrieve-items'))}; + not PersistentFeature -> + {error, extended_error(xmpp:err_feature_not_implemented(), + err_unsupported('persistent-items'))}; + true -> + Owners = node_owners_call(Host, Type, Nidx, O), + {PS, RG} = get_presence_and_roster_permissions( + Host, From, Owners, AccessModel, AllowedGroups), + case ItemIds of + [ItemId] -> + NotFound = xmpp:err_item_not_found(), + case node_call(Host, Type, get_item, + [Nidx, ItemId, From, AccessModel, PS, RG, undefined]) + of + {error, NotFound} -> {result, {[], undefined}}; + Result -> Result + end; + _ -> + node_call(Host, Type, get_items, + [Nidx, From, AccessModel, PS, RG, SubId, RSM]) + end + end + end, + case transaction(Host, Node, Action, sync_dirty) of + {result, {TNode, {Items, RsmOut}}} -> + SendItems = case ItemIds of + [] -> + Items; + _ -> + lists:filter( + fun(#pubsub_item{itemid = {ItemId, _}}) -> + lists:member(ItemId, ItemIds) + end, Items) + end, + Options = TNode#pubsub_node.options, + {result, #pubsub{items = items_els(Node, Options, SendItems), + rsm = RsmOut}}; + {result, {TNode, Item}} -> + Options = TNode#pubsub_node.options, + {result, #pubsub{items = items_els(Node, Options, [Item])}}; + Error -> + Error end. +%% Seems like this function broken get_items(Host, Node) -> - Action = fun (#pubsub_node{type = Type, id = NodeId}) -> - node_call(Type, get_items, [NodeId, service_jid(Host)]) - end, + Action = fun (#pubsub_node{type = Type, id = Nidx}) -> + node_call(Host, Type, get_items, [Nidx, service_jid(Host), undefined]) + end, case transaction(Host, Node, Action, sync_dirty) of - {result, {_, Items}} -> Items; - Error -> Error + {result, {_, {Items, _}}} -> Items; + Error -> Error end. +%% This function is broken too? get_item(Host, Node, ItemId) -> - Action = fun (#pubsub_node{type = Type, id = NodeId}) -> - node_call(Type, get_item, [NodeId, ItemId]) - end, + Action = fun (#pubsub_node{type = Type, id = Nidx}) -> + node_call(Host, Type, get_item, [Nidx, ItemId]) + end, case transaction(Host, Node, Action, sync_dirty) of - {result, {_, Items}} -> Items; - Error -> Error + {result, {_, Items}} -> Items; + Error -> Error end. -get_allowed_items_call(Host, NodeIdx, From, Type, Options, Owners) -> +-spec get_allowed_items_call(host(), nodeIdx(), jid(), + binary(), nodeOptions(), [ljid()]) -> {result, [#pubsub_item{}]} | + {error, stanza_error()}. +get_allowed_items_call(Host, Nidx, From, Type, Options, Owners) -> + case get_allowed_items_call(Host, Nidx, From, Type, Options, Owners, undefined) of + {result, {Items, _RSM}} -> {result, Items}; + Error -> Error + end. + +-spec get_allowed_items_call(host(), nodeIdx(), jid(), + binary(), nodeOptions(), [ljid()], + undefined | rsm_set()) -> + {result, {[#pubsub_item{}], undefined | rsm_set()}} | + {error, stanza_error()}. +get_allowed_items_call(Host, Nidx, From, Type, Options, Owners, RSM) -> AccessModel = get_option(Options, access_model), AllowedGroups = get_option(Options, roster_groups_allowed, []), - {PresenceSubscription, RosterGroup} = - get_presence_and_roster_permissions(Host, From, Owners, AccessModel, - AllowedGroups), - node_call(Type, get_items, - [NodeIdx, From, AccessModel, PresenceSubscription, RosterGroup, undefined]). + {PS, RG} = get_presence_and_roster_permissions(Host, From, Owners, AccessModel, AllowedGroups), + node_call(Host, Type, get_items, [Nidx, From, AccessModel, PS, RG, undefined, RSM]). -%% @spec (Host, Node, NodeId, Type, LJID, Number) -> any() -%% Host = pubsubHost() -%% Node = pubsubNode() -%% NodeId = pubsubNodeId() -%% Type = pubsubNodeType() -%% LJID = {U, S, []} -%% Number = last | integer() -%% @doc

    Resend the items of a node to the user.

    -%% @todo use cache-last-item feature -send_items(Host, Node, NodeId, Type, LJID, last) -> - case get_cached_item(Host, NodeId) of - undefined -> - send_items(Host, Node, NodeId, Type, LJID, 1); - LastItem -> - {ModifNow, ModifUSR} = - LastItem#pubsub_item.modification, - Stanza = event_stanza_with_delay([#xmlel{name = - <<"items">>, - attrs = nodeAttr(Node), - children = - itemsEls([LastItem])}], - ModifNow, ModifUSR), - dispatch_items(Host, LJID, Node, Stanza) +-spec get_last_items(host(), binary(), nodeIdx(), ljid(), last | integer()) -> [#pubsub_item{}]. +get_last_items(Host, Type, Nidx, LJID, last) -> + % hack to handle section 6.1.7 of XEP-0060 + get_last_items(Host, Type, Nidx, LJID, 1); +get_last_items(Host, Type, Nidx, LJID, 1) -> + case get_cached_item(Host, Nidx) of + undefined -> + case node_action(Host, Type, get_last_items, [Nidx, LJID, 1]) of + {result, Items} -> Items; + _ -> [] + end; + LastItem -> + [LastItem] end; -send_items(Host, Node, NodeId, Type, LJID, Number) -> - ToSend = case node_action(Host, Type, get_items, - [NodeId, LJID]) - of - {result, []} -> []; - {result, Items} -> - case Number of - N when N > 0 -> lists:sublist(Items, N); - _ -> Items - end; - _ -> [] - end, - Stanza = case ToSend of - [] -> - undefined; - [LastItem] -> - {ModifNow, ModifUSR} = - LastItem#pubsub_item.modification, - event_stanza_with_delay([#xmlel{name = <<"items">>, - attrs = nodeAttr(Node), - children = - itemsEls(ToSend)}], - ModifNow, ModifUSR); - _ -> - event_stanza([#xmlel{name = <<"items">>, - attrs = nodeAttr(Node), - children = itemsEls(ToSend)}]) - end, - dispatch_items(Host, LJID, Node, Stanza). - --spec(dispatch_items/4 :: -( - From :: mod_pubsub:host(), - To :: jid(), - Node :: mod_pubsub:nodeId(), - Stanza :: xmlel() | undefined) - -> any() -). - -dispatch_items(_From, _To, _Node, _Stanza = undefined) -> ok; -dispatch_items({FromU, FromS, FromR} = From, {ToU, ToS, ToR} = To, Node, - Stanza) -> - C2SPid = case ejabberd_sm:get_session_pid(ToU, ToS, ToR) of - ToPid when is_pid(ToPid) -> ToPid; - _ -> - R = user_resource(FromU, FromS, FromR), - case ejabberd_sm:get_session_pid(FromU, FromS, R) of - FromPid when is_pid(FromPid) -> FromPid; - _ -> undefined - end - end, - if C2SPid == undefined -> ok; - true -> - ejabberd_c2s:send_filtered(C2SPid, - {pep_message, <>}, - service_jid(From), jlib:make_jid(To), - Stanza) +get_last_items(Host, Type, Nidx, LJID, Count) when Count > 1 -> + case node_action(Host, Type, get_last_items, [Nidx, LJID, Count]) of + {result, Items} -> Items; + _ -> [] end; -dispatch_items(From, To, _Node, Stanza) -> - ejabberd_router:route(service_jid(From), jlib:make_jid(To), Stanza). +get_last_items(_Host, _Type, _Nidx, _LJID, _Count) -> + []. + +-spec get_only_item(host(), binary(), nodeIdx(), ljid()) -> [#pubsub_item{}]. +get_only_item(Host, Type, Nidx, LJID) -> + case get_cached_item(Host, Nidx) of + undefined -> + case node_action(Host, Type, get_only_item, [Nidx, LJID]) of + {result, Items} when length(Items) < 2 -> + Items; + {result, Items} -> + [hd(lists:keysort(#pubsub_item.modification, Items))]; + _ -> [] + end; + LastItem -> + [LastItem] + end. -%% @spec (Host, JID, Plugins) -> {error, Reason} | {result, Response} -%% Host = host() -%% JID = jid() -%% Plugins = [Plugin::string()] -%% Reason = stanzaError() -%% Response = [pubsubIQResponse()] %% @doc

    Return the list of affiliations as an XMPP response.

    --spec(get_affiliations/4 :: -( - Host :: mod_pubsub:host(), - Node :: mod_pubsub:nodeId(), - JID :: jid(), - Plugins :: [binary()]) - -> {result, [xmlel(),...]} - %%% - | {error, xmlel()} -). -get_affiliations(Host, <<>>, JID, Plugins) - when is_list(Plugins) -> - Result = lists:foldl(fun (Type, {Status, Acc}) -> - Features = features(Type), - RetrieveFeature = - lists:member(<<"retrieve-affiliations">>, Features), - if not RetrieveFeature -> - {{error, - extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, - unsupported, - <<"retrieve-affiliations">>)}, - Acc}; +-spec get_affiliations(host(), binary(), jid(), [binary()]) -> + {result, pubsub()} | {error, stanza_error()}. +get_affiliations(Host, Node, JID, Plugins) when is_list(Plugins) -> + Result = + lists:foldl( + fun(Type, {Status, Acc}) -> + Features = plugin_features(Host, Type), + RetrieveFeature = lists:member(<<"retrieve-affiliations">>, Features), + if not RetrieveFeature -> + {{error, extended_error(xmpp:err_feature_not_implemented(), + err_unsupported('retrieve-affiliations'))}, + Acc}; + true -> + case node_action(Host, Type, + get_entity_affiliations, + [Host, JID]) of + {result, Affs} -> + {Status, [Affs | Acc]}; + {error, _} = Err -> + {Err, Acc} + end + end + end, {ok, []}, Plugins), + case Result of + {ok, Affs} -> + Entities = lists:flatmap( + fun({_, none}) -> + []; + ({#pubsub_node{nodeid = {_, NodeId}}, Aff}) -> + if (Node == <<>>) or (Node == NodeId) -> + [#ps_affiliation{node = NodeId, + type = Aff}]; true -> - {result, Affiliations} = - node_action(Host, Type, - get_entity_affiliations, - [Host, JID]), - {Status, [Affiliations | Acc]} - end - end, - {ok, []}, Plugins), - case Result of - {ok, Affiliations} -> - Entities = lists:flatmap(fun ({_, none}) -> []; - ({#pubsub_node{nodeid = {_, Node}}, - Affiliation}) -> - [#xmlel{name = <<"affiliation">>, - attrs = - [{<<"affiliation">>, - affiliation_to_string(Affiliation)} - | nodeAttr(Node)], - children = []}] - end, - lists:usort(lists:flatten(Affiliations))), - {result, - [#xmlel{name = <<"pubsub">>, - attrs = [{<<"xmlns">>, ?NS_PUBSUB}], - children = - [#xmlel{name = <<"affiliations">>, attrs = [], - children = Entities}]}]}; - {Error, _} -> Error - end; -get_affiliations(Host, NodeId, JID, Plugins) - when is_list(Plugins) -> - Result = lists:foldl(fun (Type, {Status, Acc}) -> - Features = features(Type), - RetrieveFeature = - lists:member(<<"retrieve-affiliations">>, - Features), - if not RetrieveFeature -> - {{error, - extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, - unsupported, - <<"retrieve-affiliations">>)}, - Acc}; - true -> - {result, Affiliations} = - node_action(Host, Type, - get_entity_affiliations, - [Host, JID]), - {Status, [Affiliations | Acc]} - end - end, - {ok, []}, Plugins), - case Result of - {ok, Affiliations} -> - Entities = lists:flatmap(fun ({_, none}) -> []; - ({#pubsub_node{nodeid = {_, Node}}, - Affiliation}) - when NodeId == Node -> - [#xmlel{name = <<"affiliation">>, - attrs = - [{<<"affiliation">>, - affiliation_to_string(Affiliation)} - | nodeAttr(Node)], - children = []}]; - (_) -> [] - end, - lists:usort(lists:flatten(Affiliations))), - {result, - [#xmlel{name = <<"pubsub">>, - attrs = [{<<"xmlns">>, ?NS_PUBSUB}], - children = - [#xmlel{name = <<"affiliations">>, attrs = [], - children = Entities}]}]}; - {Error, _} -> Error - end. - --spec(get_affiliations/3 :: -( - Host :: mod_pubsub:host(), - Node :: mod_pubsub:nodeId(), - JID :: jid()) - -> {result, [xmlel(),...]} - %%% - | {error, xmlel()} -). -get_affiliations(Host, Node, JID) -> - Action = fun (#pubsub_node{type = Type, id = NodeId}) -> - Features = features(Type), - RetrieveFeature = - lists:member(<<"modify-affiliations">>, Features), - {result, Affiliation} = node_call(Type, get_affiliation, - [NodeId, JID]), - if not RetrieveFeature -> - {error, - extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, - unsupported, - <<"modify-affiliations">>)}; - Affiliation /= owner -> {error, ?ERR_FORBIDDEN}; - true -> node_call(Type, get_node_affiliations, [NodeId]) - end - end, - case transaction(Host, Node, Action, sync_dirty) of - {result, {_, []}} -> {error, ?ERR_ITEM_NOT_FOUND}; - {result, {_, Affiliations}} -> - Entities = lists:flatmap(fun ({_, none}) -> []; - ({AJID, Affiliation}) -> - [#xmlel{name = <<"affiliation">>, - attrs = - [{<<"jid">>, - jlib:jid_to_string(AJID)}, - {<<"affiliation">>, - affiliation_to_string(Affiliation)}], - children = []}] - end, - Affiliations), - {result, - [#xmlel{name = <<"pubsub">>, - attrs = [{<<"xmlns">>, ?NS_PUBSUB_OWNER}], - children = - [#xmlel{name = <<"affiliations">>, - attrs = nodeAttr(Node), children = Entities}]}]}; - Error -> Error - end. - --spec(set_affiliations/4 :: -( - Host :: mod_pubsub:host(), - Node :: mod_pubsub:nodeId(), - From :: jid(), - EntitiesEls :: [xmlel()]) - -> {result, []} - %%% - | {error, xmlel()} -). -set_affiliations(Host, Node, From, EntitiesEls) -> - Owner = jlib:jid_tolower(jlib:jid_remove_resource(From)), - Entities = lists:foldl(fun (El, Acc) -> - case Acc of - error -> error; - _ -> - case El of - #xmlel{name = <<"affiliation">>, - attrs = Attrs} -> - JID = - jlib:string_to_jid(xml:get_attr_s(<<"jid">>, - Attrs)), - Affiliation = - string_to_affiliation(xml:get_attr_s(<<"affiliation">>, - Attrs)), - if (JID == error) or - (Affiliation == false) -> - error; - true -> - [{jlib:jid_tolower(JID), - Affiliation} - | Acc] - end - end - end - end, - [], EntitiesEls), - case Entities of - error -> {error, ?ERR_BAD_REQUEST}; - _ -> - Action = fun (#pubsub_node{owners = Owners, type = Type, - id = NodeId} = - N) -> - case lists:member(Owner, Owners) of - true -> - OwnerJID = jlib:make_jid(Owner), - FilteredEntities = case Owners of - [Owner] -> - [E - || E <- Entities, - element(1, E) =/= - OwnerJID]; - _ -> Entities - end, - lists:foreach(fun ({JID, Affiliation}) -> - node_call(Type, - set_affiliation, - [NodeId, JID, - Affiliation]), - case Affiliation of - owner -> - NewOwner = - jlib:jid_tolower(jlib:jid_remove_resource(JID)), - NewOwners = - [NewOwner - | Owners], - tree_call(Host, - set_node, - [N#pubsub_node{owners - = - NewOwners}]); - none -> - OldOwner = - jlib:jid_tolower(jlib:jid_remove_resource(JID)), - case - lists:member(OldOwner, - Owners) - of - true -> - NewOwners = - Owners -- - [OldOwner], - tree_call(Host, - set_node, - [N#pubsub_node{owners - = - NewOwners}]); - _ -> ok - end; - _ -> ok - end - end, - FilteredEntities), - {result, []}; - _ -> {error, ?ERR_FORBIDDEN} - end - end, - case transaction(Host, Node, Action, sync_dirty) of - {result, {_, Result}} -> {result, Result}; - Other -> Other - end - end. - -get_options(Host, Node, JID, SubID, Lang) -> - Action = fun (#pubsub_node{type = Type, id = NodeID}) -> - case lists:member(<<"subscription-options">>, features(Type)) of - true -> - get_options_helper(JID, Lang, Node, NodeID, SubID, Type); - false -> - {error, - extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, - unsupported, - <<"subscription-options">>)} - end - end, - case transaction(Host, Node, Action, sync_dirty) of - {result, {_Node, XForm}} -> {result, [XForm]}; - Error -> Error - end. - -get_options_helper(JID, Lang, Node, NodeID, SubID, Type) -> - Subscriber = case jlib:string_to_jid(JID) of - error -> {<<"">>, <<"">>, <<"">>}; - J -> case jlib:jid_tolower(J) of - error -> {<<"">>, <<"">>, <<"">>}; - J1 -> J1 - end - end, - {result, Subs} = node_call(Type, get_subscriptions, - [NodeID, Subscriber]), - SubIDs = lists:foldl(fun ({subscribed, SID}, Acc) -> - [SID | Acc]; - (_, Acc) -> Acc - end, - [], Subs), - case {SubID, SubIDs} of - {_, []} -> - {error, - extended_error(?ERR_NOT_ACCEPTABLE, <<"not-subscribed">>)}; - {<<>>, [SID]} -> - read_sub(Subscriber, Node, NodeID, SID, Lang); - {<<>>, _} -> - {error, - extended_error(?ERR_NOT_ACCEPTABLE, <<"subid-required">>)}; - {_, _} -> - ValidSubId = lists:member(SubID, SubIDs), - if ValidSubId -> - read_sub(Subscriber, Node, NodeID, SubID, Lang); - true -> - {error, - extended_error(?ERR_NOT_ACCEPTABLE, <<"invalid-subid">>)} - end - end. - -read_sub(Subscriber, Node, NodeID, SubID, Lang) -> - Children = case pubsub_subscription:get_subscription(Subscriber, NodeID, SubID) of - {error, notfound} -> - []; - {result, #pubsub_subscription{options = Options}} -> - {result, XdataEl} = pubsub_subscription:get_options_xform(Lang, Options), - [XdataEl] - end, - OptionsEl = #xmlel{name = <<"options">>, - attrs = - [{<<"jid">>, jlib:jid_to_string(Subscriber)}, - {<<"subid">>, SubID} - | nodeAttr(Node)], - children = Children}, - PubsubEl = #xmlel{name = <<"pubsub">>, - attrs = [{<<"xmlns">>, ?NS_PUBSUB}], - children = [OptionsEl]}, - {result, PubsubEl}. - -set_options(Host, Node, JID, SubID, Configuration) -> - Action = fun (#pubsub_node{type = Type, id = NodeID}) -> - case lists:member(<<"subscription-options">>, - features(Type)) - of - true -> - set_options_helper(Configuration, JID, NodeID, SubID, - Type); - false -> - {error, - extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, - unsupported, - <<"subscription-options">>)} - end - end, - case transaction(Host, Node, Action, sync_dirty) of - {result, {_Node, Result}} -> {result, Result}; - Error -> Error - end. - -set_options_helper(Configuration, JID, NodeID, SubID, Type) -> - SubOpts = case pubsub_subscription:parse_options_xform(Configuration) of - {result, GoodSubOpts} -> GoodSubOpts; - _ -> invalid - end, - Subscriber = case jlib:string_to_jid(JID) of - error -> {<<"">>, <<"">>, <<"">>}; - J -> jlib:jid_tolower(J) - end, - {result, Subs} = node_call(Type, get_subscriptions, - [NodeID, Subscriber]), - SubIDs = lists:foldl(fun ({subscribed, SID}, Acc) -> - [SID | Acc]; - (_, Acc) -> Acc - end, - [], Subs), - case {SubID, SubIDs} of - {_, []} -> - {error, - extended_error(?ERR_NOT_ACCEPTABLE, - <<"not-subscribed">>)}; - {<<>>, [SID]} -> - write_sub(Subscriber, NodeID, SID, SubOpts); - {<<>>, _} -> - {error, - extended_error(?ERR_NOT_ACCEPTABLE, - <<"subid-required">>)}; - {_, _} -> write_sub(Subscriber, NodeID, SubID, SubOpts) - end. - -write_sub(_Subscriber, _NodeID, _SubID, invalid) -> - {error, extended_error(?ERR_BAD_REQUEST, <<"invalid-options">>)}; -write_sub(Subscriber, NodeID, SubID, Options) -> - case pubsub_subscription:set_subscription(Subscriber, NodeID, SubID, Options) of - {error, notfound} -> - {error, extended_error(?ERR_NOT_ACCEPTABLE, <<"invalid-subid">>)}; - {result, _} -> - {result, []} - end. - -%% @spec (Host, Node, JID, Plugins) -> {error, Reason} | {result, Response} -%% Host = host() -%% Node = pubsubNode() -%% JID = jid() -%% Plugins = [Plugin::string()] -%% Reason = stanzaError() -%% Response = [pubsubIQResponse()] -%% @doc

    Return the list of subscriptions as an XMPP response.

    -get_subscriptions(Host, Node, JID, Plugins) when is_list(Plugins) -> - Result = lists:foldl( - fun(Type, {Status, Acc}) -> - Features = features(Type), - RetrieveFeature = lists:member(<<"retrieve-subscriptions">>, Features), - if - not RetrieveFeature -> - %% Service does not support retreive subscriptions - {{error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, <<"retrieve-subscriptions">>)}, Acc}; - true -> - Subscriber = jlib:jid_remove_resource(JID), - {result, Subscriptions} = node_action(Host, Type, get_entity_subscriptions, [Host, Subscriber]), - {Status, [Subscriptions|Acc]} - end - end, {ok, []}, Plugins), - case Result of - {ok, Subscriptions} -> - Entities = lists:flatmap(fun ({_, none}) -> []; - ({#pubsub_node{nodeid = {_, SubsNode}}, - Subscription}) -> - case Node of - <<>> -> - [#xmlel{name = - <<"subscription">>, - attrs = - [{<<"subscription">>, - subscription_to_string(Subscription)} - | nodeAttr(SubsNode)], - children = []}]; - SubsNode -> - [#xmlel{name = - <<"subscription">>, - attrs = - [{<<"subscription">>, - subscription_to_string(Subscription)}], - children = []}]; - _ -> [] - end; - ({_, none, _}) -> []; - ({#pubsub_node{nodeid = {_, SubsNode}}, - Subscription, SubID, SubJID}) -> - case Node of - <<>> -> - [#xmlel{name = - <<"subscription">>, - attrs = - [{<<"jid">>, - jlib:jid_to_string(SubJID)}, - {<<"subid">>, - SubID}, - {<<"subscription">>, - subscription_to_string(Subscription)} - | nodeAttr(SubsNode)], - children = []}]; - SubsNode -> - [#xmlel{name = - <<"subscription">>, - attrs = - [{<<"jid">>, - jlib:jid_to_string(SubJID)}, - {<<"subid">>, - SubID}, - {<<"subscription">>, - subscription_to_string(Subscription)}], - children = []}]; - _ -> [] - end; - ({#pubsub_node{nodeid = {_, SubsNode}}, - Subscription, SubJID}) -> - case Node of - <<>> -> - [#xmlel{name = - <<"subscription">>, - attrs = - [{<<"jid">>, - jlib:jid_to_string(SubJID)}, - {<<"subscription">>, - subscription_to_string(Subscription)} - | nodeAttr(SubsNode)], - children = []}]; - SubsNode -> - [#xmlel{name = - <<"subscription">>, - attrs = - [{<<"jid">>, - jlib:jid_to_string(SubJID)}, - {<<"subscription">>, - subscription_to_string(Subscription)}], - children = []}]; - _ -> [] - end - end, - lists:usort(lists:flatten(Subscriptions))), - {result, - [#xmlel{name = <<"pubsub">>, - attrs = [{<<"xmlns">>, ?NS_PUBSUB}], - children = - [#xmlel{name = <<"subscriptions">>, attrs = [], - children = Entities}]}]}; - {Error, _} -> Error - end. - -get_subscriptions(Host, Node, JID) -> - Action = fun (#pubsub_node{type = Type, id = NodeId}) -> - Features = features(Type), - RetrieveFeature = - lists:member(<<"manage-subscriptions">>, Features), - {result, Affiliation} = node_call(Type, get_affiliation, - [NodeId, JID]), - if not RetrieveFeature -> - {error, - extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, - unsupported, - <<"manage-subscriptions">>)}; - Affiliation /= owner -> {error, ?ERR_FORBIDDEN}; - true -> - node_call(Type, get_node_subscriptions, [NodeId]) - end - end, - case transaction(Host, Node, Action, sync_dirty) of - {result, {_, Subscriptions}} -> - Entities = lists:flatmap(fun ({_, none}) -> []; - ({_, pending, _}) -> []; - ({AJID, Subscription}) -> - [#xmlel{name = <<"subscription">>, - attrs = - [{<<"jid">>, - jlib:jid_to_string(AJID)}, - {<<"subscription">>, - subscription_to_string(Subscription)}], - children = []}]; - ({AJID, Subscription, SubId}) -> - [#xmlel{name = <<"subscription">>, - attrs = - [{<<"jid">>, - jlib:jid_to_string(AJID)}, - {<<"subscription">>, - subscription_to_string(Subscription)}, - {<<"subid">>, SubId}], - children = []}] - end, - Subscriptions), - {result, - [#xmlel{name = <<"pubsub">>, - attrs = [{<<"xmlns">>, ?NS_PUBSUB_OWNER}], - children = - [#xmlel{name = <<"subscriptions">>, - attrs = nodeAttr(Node), children = Entities}]}]}; - Error -> Error - end. - -set_subscriptions(Host, Node, From, EntitiesEls) -> - Owner = - jlib:jid_tolower(jlib:jid_remove_resource(From)), - Entities = lists:foldl(fun (El, Acc) -> - case Acc of - error -> error; - _ -> - case El of - #xmlel{name = <<"subscription">>, - attrs = Attrs} -> - JID = - jlib:string_to_jid(xml:get_attr_s(<<"jid">>, - Attrs)), - Subscription = - string_to_subscription(xml:get_attr_s(<<"subscription">>, - Attrs)), - SubId = - xml:get_attr_s(<<"subid">>, - Attrs), - if (JID == error) or - (Subscription == false) -> - error; - true -> - [{jlib:jid_tolower(JID), - Subscription, SubId} - | Acc] - end - end - end - end, - [], EntitiesEls), - case Entities of - error -> {error, ?ERR_BAD_REQUEST}; - _ -> - Notify = fun (JID, Sub, _SubId) -> - Stanza = #xmlel{name = <<"message">>, attrs = [], - children = - [#xmlel{name = <<"pubsub">>, - attrs = - [{<<"xmlns">>, - ?NS_PUBSUB}], - children = - [#xmlel{name = - <<"subscription">>, - attrs = - [{<<"jid">>, - jlib:jid_to_string(JID)}, - {<<"subscription">>, - subscription_to_string(Sub)} - | nodeAttr(Node)], - children = - []}]}]}, - ejabberd_router:route(service_jid(Host), - jlib:make_jid(JID), Stanza) - end, - Action = fun (#pubsub_node{owners = Owners, type = Type, - id = NodeId}) -> - case lists:member(Owner, Owners) of - true -> - Result = lists:foldl(fun ({JID, Subscription, - SubId}, - Acc) -> - case - node_call(Type, - set_subscriptions, - [NodeId, - JID, - Subscription, - SubId]) - of - {error, Err} -> - [{error, - Err} - | Acc]; - _ -> - Notify(JID, - Subscription, - SubId), - Acc - end - end, - [], Entities), - case Result of - [] -> {result, []}; - _ -> {error, ?ERR_NOT_ACCEPTABLE} + [] end; - _ -> {error, ?ERR_FORBIDDEN} - end - end, - case transaction(Host, Node, Action, sync_dirty) of - {result, {_, Result}} -> {result, Result}; - Other -> Other - end + (_) -> + [] + end, lists:usort(lists:flatten(Affs))), + {result, #pubsub{affiliations = {<<>>, Entities}}}; + {Error, _} -> + Error end. --spec(get_presence_and_roster_permissions/5 :: -( - Host :: mod_pubsub:host(), - From :: ljid(), - Owners :: [ljid(),...], - AccessModel :: mod_pubsub:accessModel(), - AllowedGroups :: [binary()]) - -> {PresenceSubscription::boolean(), RosterGroup::boolean()} -). +-spec get_affiliations(host(), binary(), jid()) -> + {result, pubsub_owner()} | {error, stanza_error()}. +get_affiliations(Host, Node, JID) -> + Action = + fun(#pubsub_node{type = Type, id = Nidx}) -> + Features = plugin_features(Host, Type), + RetrieveFeature = lists:member(<<"modify-affiliations">>, Features), + {result, Affiliation} = node_call(Host, Type, get_affiliation, [Nidx, JID]), + if not RetrieveFeature -> + {error, extended_error(xmpp:err_feature_not_implemented(), + err_unsupported('modify-affiliations'))}; + Affiliation /= owner -> + {error, xmpp:err_forbidden(?T("Owner privileges required"), ejabberd_option:language())}; + true -> + node_call(Host, Type, get_node_affiliations, [Nidx]) + end + end, + case transaction(Host, Node, Action, sync_dirty) of + {result, {_, []}} -> + {error, xmpp:err_item_not_found()}; + {result, {_, Affs}} -> + Entities = lists:flatmap( + fun({_, none}) -> + []; + ({AJID, Aff}) -> + [#ps_affiliation{jid = AJID, type = Aff}] + end, Affs), + {result, #pubsub_owner{affiliations = {Node, Entities}}}; + Error -> + Error + end. +-spec set_affiliations(host(), binary(), jid(), [ps_affiliation()]) -> + {result, undefined} | {error, stanza_error()}. +set_affiliations(Host, Node, From, Affs) -> + Owner = jid:tolower(jid:remove_resource(From)), + Action = + fun(#pubsub_node{type = Type, id = Nidx, owners = O, options = Options} = N) -> + Owners = node_owners_call(Host, Type, Nidx, O), + case lists:member(Owner, Owners) of + true -> + AccessModel = get_option(Options, access_model), + OwnerJID = jid:make(Owner), + FilteredAffs = + case Owners of + [Owner] -> + [Aff || Aff <- Affs, + Aff#ps_affiliation.jid /= OwnerJID]; + _ -> + Affs + end, + lists:foreach( + fun(#ps_affiliation{jid = JID, type = Affiliation}) -> + node_call(Host, Type, set_affiliation, [Nidx, JID, Affiliation]), + case Affiliation of + owner -> + NewOwner = jid:tolower(jid:remove_resource(JID)), + NewOwners = [NewOwner | Owners], + tree_call(Host, + set_node, + [N#pubsub_node{owners = NewOwners}]); + none -> + OldOwner = jid:tolower(jid:remove_resource(JID)), + case lists:member(OldOwner, Owners) of + true -> + NewOwners = Owners -- [OldOwner], + tree_call(Host, + set_node, + [N#pubsub_node{owners = NewOwners}]); + _ -> + ok + end; + _ -> + ok + end, + case AccessModel of + whitelist when Affiliation /= owner, + Affiliation /= publisher, + Affiliation /= member -> + node_action(Host, Type, + unsubscribe_node, + [Nidx, OwnerJID, JID, + all]); + _ -> + ok + end + end, FilteredAffs), + {result, undefined}; + _ -> + {error, xmpp:err_forbidden( + ?T("Owner privileges required"), ejabberd_option:language())} + end + end, + case transaction(Host, Node, Action, sync_dirty) of + {result, {_, Result}} -> {result, Result}; + Other -> Other + end. + +-spec get_options(binary(), binary(), jid(), binary(), binary()) -> + {result, xdata()} | {error, stanza_error()}. +get_options(Host, Node, JID, SubId, Lang) -> + Action = fun (#pubsub_node{type = Type, id = Nidx}) -> + case lists:member(<<"subscription-options">>, plugin_features(Host, Type)) of + true -> + get_options_helper(Host, JID, Lang, Node, Nidx, SubId, Type); + false -> + {error, extended_error(xmpp:err_feature_not_implemented(), + err_unsupported('subscription-options'))} + end + end, + case transaction(Host, Node, Action, sync_dirty) of + {result, {_Node, XForm}} -> {result, XForm}; + Error -> Error + end. + +-spec get_options_helper(binary(), jid(), binary(), binary(), _, binary(), + binary()) -> {result, pubsub()} | {error, stanza_error()}. +get_options_helper(Host, JID, Lang, Node, Nidx, SubId, Type) -> + Subscriber = jid:tolower(JID), + case node_call(Host, Type, get_subscriptions, [Nidx, Subscriber]) of + {result, Subs} -> + SubIds = [Id || {Sub, Id} <- Subs, Sub == subscribed], + case {SubId, SubIds} of + {_, []} -> + {error, extended_error(xmpp:err_not_acceptable(), + err_not_subscribed())}; + {<<>>, [SID]} -> + read_sub(Host, Node, Nidx, Subscriber, SID, Lang); + {<<>>, _} -> + {error, extended_error(xmpp:err_not_acceptable(), + err_subid_required())}; + {_, _} -> + ValidSubId = lists:member(SubId, SubIds), + if ValidSubId -> + read_sub(Host, Node, Nidx, Subscriber, SubId, Lang); + true -> + {error, extended_error(xmpp:err_not_acceptable(), + err_invalid_subid())} + end + end; + {error, _} = Error -> + Error + end. + +-spec read_sub(binary(), binary(), nodeIdx(), ljid(), binary(), binary()) -> {result, pubsub()}. +read_sub(Host, Node, Nidx, Subscriber, SubId, Lang) -> + SubModule = subscription_plugin(Host), + XData = case SubModule:get_subscription(Subscriber, Nidx, SubId) of + {error, notfound} -> + undefined; + {result, #pubsub_subscription{options = Options}} -> + {result, X} = SubModule:get_options_xform(Lang, Options), + X + end, + {result, #pubsub{options = #ps_options{jid = jid:make(Subscriber), + subid = SubId, + node = Node, + xdata = XData}}}. + +-spec set_options(binary(), binary(), jid(), binary(), + [{binary(), [binary()]}]) -> + {result, undefined} | {error, stanza_error()}. +set_options(Host, Node, JID, SubId, Configuration) -> + Action = fun (#pubsub_node{type = Type, id = Nidx}) -> + case lists:member(<<"subscription-options">>, plugin_features(Host, Type)) of + true -> + set_options_helper(Host, Configuration, JID, Nidx, SubId, Type); + false -> + {error, extended_error(xmpp:err_feature_not_implemented(), + err_unsupported('subscription-options'))} + end + end, + case transaction(Host, Node, Action, sync_dirty) of + {result, {_Node, Result}} -> {result, Result}; + Error -> Error + end. + +-spec set_options_helper(binary(), [{binary(), [binary()]}], jid(), + nodeIdx(), binary(), binary()) -> + {result, undefined} | {error, stanza_error()}. +set_options_helper(Host, Configuration, JID, Nidx, SubId, Type) -> + SubModule = subscription_plugin(Host), + SubOpts = case SubModule:parse_options_xform(Configuration) of + {result, GoodSubOpts} -> GoodSubOpts; + _ -> invalid + end, + Subscriber = jid:tolower(JID), + case node_call(Host, Type, get_subscriptions, [Nidx, Subscriber]) of + {result, Subs} -> + SubIds = [Id || {Sub, Id} <- Subs, Sub == subscribed], + case {SubId, SubIds} of + {_, []} -> + {error, extended_error(xmpp:err_not_acceptable(), err_not_subscribed())}; + {<<>>, [SID]} -> + write_sub(Host, Nidx, Subscriber, SID, SubOpts); + {<<>>, _} -> + {error, extended_error(xmpp:err_not_acceptable(), err_subid_required())}; + {_, _} -> + write_sub(Host, Nidx, Subscriber, SubId, SubOpts) + end; + {error, _} = Err -> + Err + end. + +-spec write_sub(binary(), nodeIdx(), ljid(), binary(), _) -> {result, undefined} | + {error, stanza_error()}. +write_sub(_Host, _Nidx, _Subscriber, _SubId, invalid) -> + {error, extended_error(xmpp:err_bad_request(), err_invalid_options())}; +write_sub(_Host, _Nidx, _Subscriber, _SubId, []) -> + {result, undefined}; +write_sub(Host, Nidx, Subscriber, SubId, Options) -> + SubModule = subscription_plugin(Host), + case SubModule:set_subscription(Subscriber, Nidx, SubId, Options) of + {result, _} -> {result, undefined}; + {error, _} -> {error, extended_error(xmpp:err_not_acceptable(), + err_invalid_subid())} + end. + +%% @doc

    Return the list of subscriptions as an XMPP response.

    +-spec get_subscriptions(host(), binary(), jid(), [binary()]) -> + {result, pubsub()} | {error, stanza_error()}. +get_subscriptions(Host, Node, JID, Plugins) when is_list(Plugins) -> + Result = lists:foldl(fun (Type, {Status, Acc}) -> + Features = plugin_features(Host, Type), + RetrieveFeature = lists:member(<<"retrieve-subscriptions">>, Features), + if not RetrieveFeature -> + {{error, extended_error(xmpp:err_feature_not_implemented(), + err_unsupported('retrieve-subscriptions'))}, + Acc}; + true -> + Subscriber = jid:remove_resource(JID), + case node_action(Host, Type, + get_entity_subscriptions, + [Host, Subscriber]) of + {result, Subs} -> + {Status, [Subs | Acc]}; + {error, _} = Err -> + {Err, Acc} + end + end + end, {ok, []}, Plugins), + case Result of + {ok, Subs} -> + Entities = lists:flatmap(fun + ({#pubsub_node{nodeid = {_, SubsNode}}, Sub}) -> + case Node of + <<>> -> + [#ps_subscription{jid = jid:remove_resource(JID), + node = SubsNode, type = Sub}]; + SubsNode -> + [#ps_subscription{jid = jid:remove_resource(JID), + type = Sub}]; + _ -> + [] + end; + ({#pubsub_node{nodeid = {_, SubsNode}}, Sub, SubId, SubJID}) -> + case Node of + <<>> -> + [#ps_subscription{jid = SubJID, + subid = SubId, + type = Sub, + node = SubsNode}]; + SubsNode -> + [#ps_subscription{jid = SubJID, + subid = SubId, + type = Sub}]; + _ -> + [] + end; + ({#pubsub_node{nodeid = {_, SubsNode}}, Sub, SubJID}) -> + case Node of + <<>> -> + [#ps_subscription{jid = SubJID, + type = Sub, + node = SubsNode}]; + SubsNode -> + [#ps_subscription{jid = SubJID, type = Sub}]; + _ -> + [] + end + end, + lists:usort(lists:flatten(Subs))), + {result, #pubsub{subscriptions = {<<>>, Entities}}}; + {Error, _} -> + Error + end. + +-spec get_subscriptions(host(), binary(), jid()) -> {result, pubsub_owner()} | + {error, stanza_error()}. +get_subscriptions(Host, Node, JID) -> + Action = fun(#pubsub_node{type = Type, id = Nidx}) -> + Features = plugin_features(Host, Type), + RetrieveFeature = lists:member(<<"manage-subscriptions">>, Features), + case node_call(Host, Type, get_affiliation, [Nidx, JID]) of + {result, Affiliation} -> + if not RetrieveFeature -> + {error, extended_error(xmpp:err_feature_not_implemented(), + err_unsupported('manage-subscriptions'))}; + Affiliation /= owner -> + Lang = ejabberd_option:language(), + {error, xmpp:err_forbidden(?T("Owner privileges required"), Lang)}; + true -> + node_call(Host, Type, get_node_subscriptions, [Nidx]) + end; + Error -> + Error + end + end, + case transaction(Host, Node, Action, sync_dirty) of + {result, {_, Subs}} -> + Entities = + lists:flatmap( + fun({_, none}) -> + []; + ({_, pending, _}) -> + []; + ({AJID, Sub}) -> + [#ps_subscription{jid = AJID, type = Sub}]; + ({AJID, Sub, SubId}) -> + [#ps_subscription{jid = AJID, type = Sub, subid = SubId}] + end, Subs), + {result, #pubsub_owner{subscriptions = {Node, Entities}}}; + Error -> + Error + end. + +-spec get_subscriptions_for_send_last(host(), binary(), atom(), jid(), ljid(), ljid()) -> + [{#pubsub_node{}, subId(), ljid()}]. +get_subscriptions_for_send_last(Host, PType, sql, JID, LJID, BJID) -> + case node_action(Host, PType, + get_entity_subscriptions_for_send_last, + [Host, JID]) of + {result, Subs} -> + [{Node, SubId, SubJID} + || {Node, Sub, SubId, SubJID} <- Subs, + Sub =:= subscribed, (SubJID == LJID) or (SubJID == BJID)]; + _ -> + [] + end; +%% sql version already filter result by on_sub_and_presence +get_subscriptions_for_send_last(Host, PType, _, JID, LJID, BJID) -> + case node_action(Host, PType, + get_entity_subscriptions, + [Host, JID]) of + {result, Subs} -> + [{Node, SubId, SubJID} + || {Node, Sub, SubId, SubJID} <- Subs, + Sub =:= subscribed, (SubJID == LJID) or (SubJID == BJID), + match_option(Node, send_last_published_item, on_sub_and_presence)]; + _ -> + [] + end. + +-spec set_subscriptions(host(), binary(), jid(), [ps_subscription()]) -> + {result, undefined} | {error, stanza_error()}. +set_subscriptions(Host, Node, From, Entities) -> + Owner = jid:tolower(jid:remove_resource(From)), + Notify = fun(#ps_subscription{jid = JID, type = Sub}) -> + Stanza = #message{ + from = service_jid(Host), + to = JID, + sub_els = [#ps_event{ + subscription = #ps_subscription{ + jid = JID, + type = Sub, + node = Node}}]}, + ejabberd_router:route(Stanza) + end, + Action = + fun(#pubsub_node{type = Type, id = Nidx, owners = O}) -> + Owners = node_owners_call(Host, Type, Nidx, O), + case lists:member(Owner, Owners) of + true -> + Result = + lists:foldl( + fun(_, {error, _} = Err) -> + Err; + (#ps_subscription{jid = JID, type = Sub, + subid = SubId} = Entity, _) -> + case node_call(Host, Type, + set_subscriptions, + [Nidx, JID, Sub, SubId]) of + {error, _} = Err -> + Err; + _ -> + Notify(Entity) + end + end, ok, Entities), + case Result of + ok -> {result, undefined}; + {error, _} = Err -> Err + end; + _ -> + {error, xmpp:err_forbidden( + ?T("Owner privileges required"), ejabberd_option:language())} + + end + end, + case transaction(Host, Node, Action, sync_dirty) of + {result, {_, Result}} -> {result, Result}; + Other -> Other + end. + +-spec get_presence_and_roster_permissions( + host(), jid(), [ljid()], accessModel(), + [binary()]) -> {boolean(), boolean()}. get_presence_and_roster_permissions(Host, From, Owners, AccessModel, AllowedGroups) -> if (AccessModel == presence) or (AccessModel == roster) -> - case Host of - {User, Server, _} -> - get_roster_info(User, Server, From, AllowedGroups); - _ -> - [{OUser, OServer, _} | _] = Owners, - get_roster_info(OUser, OServer, From, AllowedGroups) - end; - true -> {true, true} + case Host of + {User, Server, _} -> + get_roster_info(User, Server, From, AllowedGroups); + _ -> + [{OUser, OServer, _} | _] = Owners, + get_roster_info(OUser, OServer, From, AllowedGroups) + end; + true -> + {true, true} end. -%% @spec (OwnerUser, OwnerServer, {SubscriberUser, SubscriberServer, SubscriberResource}, AllowedGroups) -%% -> {PresenceSubscription, RosterGroup} -get_roster_info(_, _, {<<"">>, <<"">>, _}, _) -> +-spec get_roster_info(binary(), binary(), ljid() | jid(), [binary()]) -> {boolean(), boolean()}. +get_roster_info(_, _, {<<>>, <<>>, _}, _) -> {false, false}; -get_roster_info(OwnerUser, OwnerServer, - {SubscriberUser, SubscriberServer, _}, AllowedGroups) -> - {Subscription, Groups} = - ejabberd_hooks:run_fold(roster_get_jid_info, - OwnerServer, {none, []}, - [OwnerUser, OwnerServer, - {SubscriberUser, SubscriberServer, <<"">>}]), +get_roster_info(OwnerUser, OwnerServer, {SubscriberUser, SubscriberServer, _}, AllowedGroups) -> + LJID = {SubscriberUser, SubscriberServer, <<>>}, + {Subscription, _Ask, Groups} = ejabberd_hooks:run_fold(roster_get_jid_info, + OwnerServer, {none, none, []}, + [OwnerUser, OwnerServer, LJID]), PresenceSubscription = Subscription == both orelse - Subscription == from orelse - {OwnerUser, OwnerServer} == - {SubscriberUser, SubscriberServer}, + Subscription == from orelse + {OwnerUser, OwnerServer} == {SubscriberUser, SubscriberServer}, RosterGroup = lists:any(fun (Group) -> - lists:member(Group, AllowedGroups) - end, - Groups), + lists:member(Group, AllowedGroups) + end, + Groups), {PresenceSubscription, RosterGroup}; -get_roster_info(OwnerUser, OwnerServer, JID, - AllowedGroups) -> - get_roster_info(OwnerUser, OwnerServer, - jlib:jid_tolower(JID), AllowedGroups). +get_roster_info(OwnerUser, OwnerServer, JID, AllowedGroups) -> + get_roster_info(OwnerUser, OwnerServer, jid:tolower(JID), AllowedGroups). -string_to_affiliation(<<"owner">>) -> owner; -string_to_affiliation(<<"publisher">>) -> publisher; -string_to_affiliation(<<"member">>) -> member; -string_to_affiliation(<<"outcast">>) -> outcast; -string_to_affiliation(<<"none">>) -> none; -string_to_affiliation(_) -> false. +-spec preconditions_met(pubsub_publish_options:result(), + pubsub_node_config:result()) -> boolean(). +preconditions_met(PubOpts, NodeOpts) -> + lists:all(fun(Opt) -> lists:member(Opt, NodeOpts) end, PubOpts). -string_to_subscription(<<"subscribed">>) -> subscribed; -string_to_subscription(<<"pending">>) -> pending; -string_to_subscription(<<"unconfigured">>) -> - unconfigured; -string_to_subscription(<<"none">>) -> none; -string_to_subscription(_) -> false. +-spec service_jid(jid() | ljid() | binary()) -> jid(). +service_jid(#jid{} = Jid) -> Jid; +service_jid({U, S, R}) -> jid:make(U, S, R); +service_jid(Host) -> jid:make(Host). -affiliation_to_string(owner) -> <<"owner">>; -affiliation_to_string(publisher) -> <<"publisher">>; -affiliation_to_string(member) -> <<"member">>; -affiliation_to_string(outcast) -> <<"outcast">>; -affiliation_to_string(_) -> <<"none">>. - -subscription_to_string(subscribed) -> <<"subscribed">>; -subscription_to_string(pending) -> <<"pending">>; -subscription_to_string(unconfigured) -> <<"unconfigured">>; -subscription_to_string(_) -> <<"none">>. - --spec(service_jid/1 :: -( - Host :: mod_pubsub:host()) - -> jid() -). -service_jid(Host) -> - case Host of - {U, S, _} -> {jid, U, S, <<"">>, U, S, <<"">>}; - _ -> {jid, <<"">>, Host, <<"">>, <<"">>, Host, <<"">>} - end. - -%% @spec (LJID, NotifyType, Depth, NodeOptions, SubOptions) -> boolean() -%% LJID = jid() -%% NotifyType = items | nodes -%% Depth = integer() -%% NodeOptions = [{atom(), term()}] -%% SubOptions = [{atom(), term()}] %% @doc

    Check if a notification must be delivered or not based on %% node and subscription options.

    -is_to_deliver(LJID, NotifyType, Depth, NodeOptions, - SubOptions) -> +-spec is_to_deliver(ljid(), items | nodes, integer(), nodeOptions(), subOptions()) -> boolean(). +is_to_deliver(LJID, NotifyType, Depth, NodeOptions, SubOptions) -> sub_to_deliver(LJID, NotifyType, Depth, SubOptions) - andalso node_to_deliver(LJID, NodeOptions). + andalso node_to_deliver(LJID, NodeOptions). +-spec sub_to_deliver(ljid(), items | nodes, integer(), subOptions()) -> boolean(). sub_to_deliver(_LJID, NotifyType, Depth, SubOptions) -> lists:all(fun (Option) -> - sub_option_can_deliver(NotifyType, Depth, Option) - end, - SubOptions). + sub_option_can_deliver(NotifyType, Depth, Option) + end, + SubOptions). +-spec node_to_deliver(ljid(), nodeOptions()) -> boolean(). +node_to_deliver(LJID, NodeOptions) -> + presence_can_deliver(LJID, get_option(NodeOptions, presence_based_delivery)). + +-spec sub_option_can_deliver(items | nodes, integer(), _) -> boolean(). sub_option_can_deliver(items, _, {subscription_type, nodes}) -> false; sub_option_can_deliver(nodes, _, {subscription_type, items}) -> false; -sub_option_can_deliver(_, _, {subscription_depth, all}) -> true; -sub_option_can_deliver(_, Depth, {subscription_depth, D}) -> Depth =< D; -sub_option_can_deliver(_, _, {deliver, false}) -> false; -sub_option_can_deliver(_, _, {expire, When}) -> now() < When; -sub_option_can_deliver(_, _, _) -> true. +sub_option_can_deliver(_, _, {subscription_depth, all}) -> true; +sub_option_can_deliver(_, Depth, {subscription_depth, D}) -> Depth =< D; +sub_option_can_deliver(_, _, {deliver, false}) -> false; +sub_option_can_deliver(_, _, {expire, When}) -> erlang:timestamp() < When; +sub_option_can_deliver(_, _, _) -> true. -node_to_deliver(LJID, NodeOptions) -> - PresenceDelivery = get_option(NodeOptions, presence_based_delivery), - presence_can_deliver(LJID, PresenceDelivery). - --spec(presence_can_deliver/2 :: -( - Entity :: ljid(), - _ :: boolean()) - -> boolean() -). -presence_can_deliver(_, false) -> true; +-spec presence_can_deliver(ljid(), boolean()) -> boolean(). +presence_can_deliver(_, false) -> + true; presence_can_deliver({User, Server, Resource}, true) -> - case mnesia:dirty_match_object({session, '_', '_', {User, Server}, '_', '_'}) of - [] -> false; - Ss -> - lists:foldl(fun(_, true) -> true; - ({session, _, _ , _, undefined, _}, _Acc) -> false; - ({session, _, {_, _, R}, _, _Priority, _}, _Acc) -> - case Resource of - <<>> -> true; - R -> true; - _ -> false - end - end, false, Ss) + case ejabberd_sm:get_user_present_resources(User, Server) of + [] -> + false; + Ss -> + lists:foldl(fun + (_, true) -> + true; + ({_, R}, _Acc) -> + case Resource of + <<>> -> true; + R -> true; + _ -> false + end + end, + false, Ss) end. --spec(state_can_deliver/2 :: -( - Entity::ljid(), - SubOptions :: mod_pubsub:subOptions() | []) - -> [ljid()] -). +-spec state_can_deliver(ljid(), subOptions()) -> [ljid()]. state_can_deliver({U, S, R}, []) -> [{U, S, R}]; state_can_deliver({U, S, R}, SubOptions) -> - %% Check SubOptions for 'show_values' - case lists:keysearch('show_values', 1, SubOptions) of - %% If not in suboptions, item can be delivered, case doesn't apply - false -> [{U, S, R}]; - %% If in a suboptions ... - {_, {_, ShowValues}} -> - %% Get subscriber resources - Resources = case R of - %% If the subscriber JID is a bare one, get all its resources - <<>> -> user_resources(U, S); - %% If the subscriber JID is a full one, use its resource - R -> [R] - end, - %% For each resource, test if the item is allowed to be delivered - %% based on resource state - lists:foldl( - fun(Resource, Acc) -> - get_resource_state({U, S, Resource}, ShowValues, Acc) - end, [], Resources) + case lists:keysearch(show_values, 1, SubOptions) of + %% If not in suboptions, item can be delivered, case doesn't apply + false -> [{U, S, R}]; + %% If in a suboptions ... + {_, {_, ShowValues}} -> + Resources = case R of + %% If the subscriber JID is a bare one, get all its resources + <<>> -> user_resources(U, S); + %% If the subscriber JID is a full one, use its resource + R -> [R] + end, + lists:foldl(fun (Resource, Acc) -> + get_resource_state({U, S, Resource}, ShowValues, Acc) + end, + [], Resources) end. --spec(get_resource_state/3 :: -( - Entity :: ljid(), - ShowValues :: [binary()], - JIDs :: [ljid()]) - -> [ljid()] -). +-spec get_resource_state(ljid(), [binary()], [ljid()]) -> [ljid()]. get_resource_state({U, S, R}, ShowValues, JIDs) -> case ejabberd_sm:get_session_pid(U, S, R) of - %% If no PID, item can be delivered - none -> lists:append([{U, S, R}], JIDs); - %% If PID ... - Pid -> - %% Get user resource state - %% TODO : add a catch clause - Show = case ejabberd_c2s:get_presence(Pid) of - {_, _, <<"available">>, _} -> <<"online">>; - {_, _, State, _} -> State - end, - %% Is current resource state listed in 'show-values' suboption ? - case lists:member(Show, ShowValues) of %andalso Show =/= "online" of - %% If yes, item can be delivered - true -> lists:append([{U, S, R}], JIDs); - %% If no, item can't be delivered - false -> JIDs - end + none -> + %% If no PID, item can be delivered + lists:append([{U, S, R}], JIDs); + Pid -> + Show = case ejabberd_c2s:get_presence(Pid) of + #presence{type = unavailable} -> <<"unavailable">>; + #presence{show = undefined} -> <<"online">>; + #presence{show = Sh} -> atom_to_binary(Sh, latin1) + end, + case lists:member(Show, ShowValues) of + %% If yes, item can be delivered + true -> lists:append([{U, S, R}], JIDs); + %% If no, item can't be delivered + false -> JIDs + end end. --spec(payload_xmlelements/1 :: -( - Payload :: mod_pubsub:payload()) - -> Count :: non_neg_integer() -). -%% @spec (Payload) -> int() -%% Payload = term() -%% @doc

    Count occurence of XML elements in payload.

    -payload_xmlelements(Payload) -> payload_xmlelements(Payload, 0). +-spec payload_xmlelements([xmlel()]) -> non_neg_integer(). +payload_xmlelements(Payload) -> + payload_xmlelements(Payload, 0). + +-spec payload_xmlelements([xmlel()], non_neg_integer()) -> non_neg_integer(). payload_xmlelements([], Count) -> Count; payload_xmlelements([#xmlel{} | Tail], Count) -> payload_xmlelements(Tail, Count + 1); payload_xmlelements([_ | Tail], Count) -> payload_xmlelements(Tail, Count). -%% @spec (Els) -> stanza() -%% Els = [xmlelement()] -%% @doc

    Build pubsub event stanza

    -event_stanza(Els) -> - #xmlel{name = <<"message">>, attrs = [], - children = - [#xmlel{name = <<"event">>, - attrs = [{<<"xmlns">>, ?NS_PUBSUB_EVENT}], - children = Els}]}. - -event_stanza_with_delay(Els, ModifNow, ModifUSR) -> - jlib:add_delay_info(event_stanza(Els), ModifUSR, ModifNow). +-spec items_els(binary(), nodeOptions(), [#pubsub_item{}]) -> ps_items(). +items_els(Node, Options, Items) -> + Els = case get_option(Options, itemreply) of + publisher -> + [#ps_item{id = ItemId, sub_els = Payload, publisher = jid:encode(USR)} + || #pubsub_item{itemid = {ItemId, _}, payload = Payload, modification = {_, USR}} + <- Items]; + _ -> + [#ps_item{id = ItemId, sub_els = Payload} + || #pubsub_item{itemid = {ItemId, _}, payload = Payload} + <- Items] + end, + #ps_items{node = Node, items = Els}. %%%%%% broadcast functions -broadcast_publish_item(Host, Node, NodeId, Type, NodeOptions, Removed, ItemId, From, Payload) -> +-spec broadcast_publish_item(host(), binary(), nodeIdx(), binary(), + nodeOptions(), binary(), jid(), [xmlel()], _) -> + {result, boolean()}. +broadcast_publish_item(Host, Node, Nidx, Type, NodeOptions, ItemId, From, Payload, Removed) -> case get_collection_subscriptions(Host, Node) of - SubsByDepth when is_list(SubsByDepth) -> - Content = case get_option(NodeOptions, deliver_payloads) of - true -> Payload; - false -> [] - end, - Stanza = event_stanza( - [#xmlel{name = <<"items">>, attrs = nodeAttr(Node), - children = [#xmlel{name = <<"item">>, attrs = itemAttr(ItemId), - children = Content}]}]), - broadcast_stanza(Host, From, Node, NodeId, Type, + {result, SubsByDepth} -> + ItemPublisher = case get_option(NodeOptions, itemreply) of + publisher -> jid:encode(From); + _ -> <<>> + end, + ItemPayload = case get_option(NodeOptions, deliver_payloads) of + true -> Payload; + false -> [] + end, + ItemsEls = #ps_items{node = Node, + items = [#ps_item{id = ItemId, + publisher = ItemPublisher, + sub_els = ItemPayload}]}, + Stanza = #message{ sub_els = [#ps_event{items = ItemsEls}]}, + broadcast_stanza(Host, From, Node, Nidx, Type, NodeOptions, SubsByDepth, items, Stanza, true), case Removed of [] -> @@ -4350,12 +2813,15 @@ broadcast_publish_item(Host, Node, NodeId, Type, NodeOptions, Removed, ItemId, F _ -> case get_option(NodeOptions, notify_retract) of true -> - RetractStanza = event_stanza( - [#xmlel{name = <<"items">>, attrs = nodeAttr(Node), - children = [#xmlel{name = <<"retract">>, attrs = itemAttr(RId)} || RId <- Removed]}]), - broadcast_stanza(Host, Node, NodeId, Type, - NodeOptions, SubsByDepth, - items, RetractStanza, true); + RetractStanza = #message{ + sub_els = + [#ps_event{ + items = #ps_items{ + node = Node, + retract = Removed}}]}, + broadcast_stanza(Host, Node, Nidx, Type, + NodeOptions, SubsByDepth, + items, RetractStanza, true); _ -> ok end @@ -4365,20 +2831,28 @@ broadcast_publish_item(Host, Node, NodeId, Type, NodeOptions, Removed, ItemId, F {result, false} end. -broadcast_retract_items(Host, Node, NodeId, Type, NodeOptions, ItemIds) -> - broadcast_retract_items(Host, Node, NodeId, Type, NodeOptions, ItemIds, false). -broadcast_retract_items(_Host, _Node, _NodeId, _Type, _NodeOptions, [], _ForceNotify) -> +-spec broadcast_retract_items(host(), jid(), binary(), nodeIdx(), binary(), + nodeOptions(), [itemId()]) -> {result, boolean()}. +broadcast_retract_items(Host, Publisher, Node, Nidx, Type, NodeOptions, ItemIds) -> + broadcast_retract_items(Host, Publisher, Node, Nidx, Type, NodeOptions, ItemIds, false). + +-spec broadcast_retract_items(host(), jid(), binary(), nodeIdx(), binary(), + nodeOptions(), [itemId()], boolean()) -> {result, boolean()}. +broadcast_retract_items(_Host, _Publisher, _Node, _Nidx, _Type, _NodeOptions, [], _ForceNotify) -> {result, false}; -broadcast_retract_items(Host, Node, NodeId, Type, NodeOptions, ItemIds, ForceNotify) -> +broadcast_retract_items(Host, Publisher, Node, Nidx, Type, NodeOptions, ItemIds, ForceNotify) -> case (get_option(NodeOptions, notify_retract) or ForceNotify) of true -> case get_collection_subscriptions(Host, Node) of - SubsByDepth when is_list(SubsByDepth) -> - Stanza = event_stanza( - [#xmlel{name = <<"items">>, attrs = nodeAttr(Node), - children = [#xmlel{name = <<"retract">>, attrs = itemAttr(ItemId)} || ItemId <- ItemIds]}]), - broadcast_stanza(Host, Node, NodeId, Type, - NodeOptions, SubsByDepth, items, Stanza, true), + {result, SubsByDepth} -> + Stanza = #message{ + sub_els = + [#ps_event{ + items = #ps_items{ + node = Node, + retract = ItemIds}}]}, + broadcast_stanza(Host, Publisher, Node, Nidx, Type, + NodeOptions, SubsByDepth, items, Stanza, true), {result, true}; _ -> {result, false} @@ -4387,15 +2861,15 @@ broadcast_retract_items(Host, Node, NodeId, Type, NodeOptions, ItemIds, ForceNot {result, false} end. -broadcast_purge_node(Host, Node, NodeId, Type, NodeOptions) -> +-spec broadcast_purge_node(host(), binary(), nodeIdx(), binary(), nodeOptions()) -> {result, boolean()}. +broadcast_purge_node(Host, Node, Nidx, Type, NodeOptions) -> case get_option(NodeOptions, notify_retract) of true -> case get_collection_subscriptions(Host, Node) of - SubsByDepth when is_list(SubsByDepth) -> - Stanza = event_stanza( - [#xmlel{name = <<"purge">>, attrs = nodeAttr(Node)}]), - broadcast_stanza(Host, Node, NodeId, Type, - NodeOptions, SubsByDepth, nodes, Stanza, false), + {result, SubsByDepth} -> + Stanza = #message{sub_els = [#ps_event{purge = Node}]}, + broadcast_stanza(Host, Node, Nidx, Type, + NodeOptions, SubsByDepth, nodes, Stanza, false), {result, true}; _ -> {result, false} @@ -4404,46 +2878,53 @@ broadcast_purge_node(Host, Node, NodeId, Type, NodeOptions) -> {result, false} end. -broadcast_removed_node(Host, Node, NodeId, Type, NodeOptions, SubsByDepth) -> +-spec broadcast_removed_node(host(), binary(), nodeIdx(), binary(), + nodeOptions(), subs_by_depth()) -> {result, boolean()}. +broadcast_removed_node(Host, Node, Nidx, Type, NodeOptions, SubsByDepth) -> case get_option(NodeOptions, notify_delete) of true -> case SubsByDepth of [] -> {result, false}; _ -> - Stanza = event_stanza( - [#xmlel{name = <<"delete">>, attrs = nodeAttr(Node)}]), - broadcast_stanza(Host, Node, NodeId, Type, - NodeOptions, SubsByDepth, nodes, Stanza, false), + Stanza = #message{sub_els = [#ps_event{delete = {Node, <<>>}}]}, + broadcast_stanza(Host, Node, Nidx, Type, + NodeOptions, SubsByDepth, nodes, Stanza, false), {result, true} end; _ -> {result, false} end. +-spec broadcast_created_node(host(), binary(), nodeIdx(), binary(), + nodeOptions(), subs_by_depth()) -> {result, boolean()}. broadcast_created_node(_, _, _, _, _, []) -> {result, false}; -broadcast_created_node(Host, Node, NodeId, Type, NodeOptions, SubsByDepth) -> - Stanza = event_stanza([#xmlel{name = <<"create">>, attrs = nodeAttr(Node)}]), - broadcast_stanza(Host, Node, NodeId, Type, NodeOptions, SubsByDepth, nodes, Stanza, true), +broadcast_created_node(Host, Node, Nidx, Type, NodeOptions, SubsByDepth) -> + Stanza = #message{sub_els = [#ps_event{create = Node}]}, + broadcast_stanza(Host, Node, Nidx, Type, NodeOptions, SubsByDepth, nodes, Stanza, true), {result, true}. -broadcast_config_notification(Host, Node, NodeId, Type, NodeOptions, Lang) -> +-spec broadcast_config_notification(host(), binary(), nodeIdx(), binary(), + nodeOptions(), binary()) -> {result, boolean()}. +broadcast_config_notification(Host, Node, Nidx, Type, NodeOptions, Lang) -> case get_option(NodeOptions, notify_config) of true -> case get_collection_subscriptions(Host, Node) of - SubsByDepth when is_list(SubsByDepth) -> + {result, SubsByDepth} -> Content = case get_option(NodeOptions, deliver_payloads) of true -> - [#xmlel{name = <<"x">>, attrs = [{<<"xmlns">>, ?NS_XDATA}, {<<"type">>, <<"result">>}], - children = get_configure_xfields(Type, NodeOptions, Lang, [])}]; + #xdata{type = result, + fields = get_configure_xfields( + Type, NodeOptions, Lang, [])}; false -> - [] + undefined end, - Stanza = event_stanza( - [#xmlel{name = <<"configuration">>, attrs = nodeAttr(Node), children = Content}]), - broadcast_stanza(Host, Node, NodeId, Type, - NodeOptions, SubsByDepth, nodes, Stanza, false), + Stanza = #message{ + sub_els = [#ps_event{ + configuration = {Node, Content}}]}, + broadcast_stanza(Host, Node, Nidx, Type, + NodeOptions, SubsByDepth, nodes, Stanza, false), {result, true}; _ -> {result, false} @@ -4452,362 +2933,531 @@ broadcast_config_notification(Host, Node, NodeId, Type, NodeOptions, Lang) -> {result, false} end. +-spec get_collection_subscriptions(host(), nodeId()) -> {result, subs_by_depth()} | + {error, stanza_error()}. get_collection_subscriptions(Host, Node) -> - Action = fun() -> - {result, lists:map(fun({Depth, Nodes}) -> - {Depth, [{N, get_node_subs(N)} || N <- Nodes]} - end, tree_call(Host, get_parentnodes_tree, [Host, Node, service_jid(Host)]))} - end, - case transaction(Action, sync_dirty) of - {result, CollSubs} -> CollSubs; - _ -> [] + Action = fun() -> get_node_subs_by_depth(Host, Node, service_jid(Host)) end, + transaction(Host, Action, sync_dirty). + +-spec get_node_subs_by_depth(host(), nodeId(), jid()) -> {result, subs_by_depth()} | + {error, stanza_error()}. +get_node_subs_by_depth(Host, Node, From) -> + case tree_call(Host, get_parentnodes_tree, [Host, Node, From]) of + ParentTree when is_list(ParentTree) -> + {result, + lists:filtermap( + fun({Depth, Nodes}) -> + case lists:filtermap( + fun(N) -> + case get_node_subs(Host, N) of + {result, Result} -> {true, {N, Result}}; + _ -> false + end + end, Nodes) of + [] -> false; + Subs -> {true, {Depth, Subs}} + end + end, ParentTree)}; + Error -> + Error end. -get_node_subs(#pubsub_node{type = Type, - id = NodeID}) -> - case node_call(Type, get_node_subscriptions, [NodeID]) of - {result, Subs} -> get_options_for_subs(NodeID, Subs); +-spec get_node_subs(host(), #pubsub_node{}) -> {result, [{ljid(), subId(), subOptions()}]} | + {error, stanza_error()}. +get_node_subs(Host, #pubsub_node{type = Type, id = Nidx}) -> + WithOptions = lists:member(<<"subscription-options">>, plugin_features(Host, Type)), + case node_call(Host, Type, get_node_subscriptions, [Nidx]) of + {result, Subs} -> {result, get_options_for_subs(Host, Nidx, Subs, WithOptions)}; Other -> Other end. -get_options_for_subs(NodeID, Subs) -> +-spec get_options_for_subs(host(), nodeIdx(), + [{ljid(), subscription(), subId()}], + boolean()) -> + [{ljid(), subId(), subOptions()}]. +get_options_for_subs(_Host, _Nidx, Subs, false) -> lists:foldl(fun({JID, subscribed, SubID}, Acc) -> - case pubsub_subscription:read_subscription(JID, NodeID, SubID) of - {error, notfound} -> [{JID, SubID, []} | Acc]; - #pubsub_subscription{options = Options} -> [{JID, SubID, Options} | Acc]; - _ -> Acc - end; - (_, Acc) -> - Acc - end, [], Subs). + [{JID, SubID, []} | Acc]; + (_, Acc) -> + Acc + end, [], Subs); +get_options_for_subs(Host, Nidx, Subs, true) -> + SubModule = subscription_plugin(Host), + lists:foldl(fun({JID, subscribed, SubID}, Acc) -> + case SubModule:get_subscription(JID, Nidx, SubID) of + #pubsub_subscription{options = Options} -> [{JID, SubID, Options} | Acc]; + {error, notfound} -> [{JID, SubID, []} | Acc] + end; + (_, Acc) -> + Acc + end, [], Subs). -broadcast_stanza(Host, _Node, _NodeId, _Type, NodeOptions, SubsByDepth, NotifyType, BaseStanza, SHIM) -> +-spec broadcast_stanza(host(), nodeId(), nodeIdx(), binary(), + nodeOptions(), subs_by_depth(), + items | nodes, stanza(), boolean()) -> ok. +broadcast_stanza(Host, _Node, _Nidx, _Type, NodeOptions, SubsByDepth, NotifyType, BaseStanza, SHIM) -> NotificationType = get_option(NodeOptions, notification_type, headline), - BroadcastAll = get_option(NodeOptions, broadcast_all_resources), %% XXX this is not standard, but usefull - From = service_jid(Host), - Stanza = case NotificationType of - normal -> BaseStanza; - MsgType -> add_message_type(BaseStanza, iolist_to_binary(atom_to_list(MsgType))) - end, + BroadcastAll = get_option(NodeOptions, broadcast_all_resources), %% XXX this is not standard, but useful + Stanza = add_message_type( + xmpp:set_from(BaseStanza, service_jid(Host)), + NotificationType), %% Handles explicit subscriptions SubIDsByJID = subscribed_nodes_by_jid(NotifyType, SubsByDepth), - lists:foreach(fun ({LJID, NodeName, SubIDs}) -> - LJIDs = case BroadcastAll of - true -> - {U, S, _} = LJID, - [{U, S, R} || R <- user_resources(U, S)]; - false -> - [LJID] - end, - %% Determine if the stanza should have SHIM ('SubID' and 'name') headers - StanzaToSend = case {SHIM, SubIDs} of - {false, _} -> - Stanza; - %% If there's only one SubID, don't add it - {true, [_]} -> - add_shim_headers(Stanza, collection_shim(NodeName)); - {true, SubIDs} -> - add_shim_headers(Stanza, lists:append(collection_shim(NodeName), subid_shim(SubIDs))) - end, - lists:foreach(fun(To) -> - ejabberd_router:route(From, jlib:make_jid(To), StanzaToSend) - end, LJIDs) - end, SubIDsByJID). + lists:foreach(fun ({LJID, _NodeName, SubIDs}) -> + LJIDs = case BroadcastAll of + true -> + {U, S, _} = LJID, + [{U, S, R} || R <- user_resources(U, S)]; + false -> + [LJID] + end, + %% Determine if the stanza should have SHIM ('SubID' and 'name') headers + StanzaToSend = case {SHIM, SubIDs} of + {false, _} -> + Stanza; + %% If there's only one SubID, don't add it + {true, [_]} -> + Stanza; + {true, SubIDs} -> + add_shim_headers(Stanza, subid_shim(SubIDs)) + end, + lists:foreach(fun(To) -> + ejabberd_router:route( + xmpp:set_to(xmpp:put_meta(StanzaToSend, ignore_sm_bounce, true), + jid:make(To))) + end, LJIDs) + end, SubIDsByJID). -broadcast_stanza({LUser, LServer, LResource}, Publisher, Node, NodeId, Type, NodeOptions, SubsByDepth, NotifyType, BaseStanza, SHIM) -> - broadcast_stanza({LUser, LServer, LResource}, Node, NodeId, Type, NodeOptions, SubsByDepth, NotifyType, BaseStanza, SHIM), +-spec broadcast_stanza(host(), jid(), nodeId(), nodeIdx(), binary(), + nodeOptions(), subs_by_depth(), items | nodes, + stanza(), boolean()) -> ok. +broadcast_stanza({LUser, LServer, LResource}, Publisher, Node, Nidx, Type, NodeOptions, SubsByDepth, NotifyType, BaseStanza, SHIM) -> + broadcast_stanza({LUser, LServer, <<>>}, Node, Nidx, Type, NodeOptions, SubsByDepth, NotifyType, BaseStanza, SHIM), %% Handles implicit presence subscriptions SenderResource = user_resource(LUser, LServer, LResource), - case ejabberd_sm:get_session_pid(LUser, LServer, SenderResource) of - C2SPid when is_pid(C2SPid) -> - Stanza = case get_option(NodeOptions, notification_type, headline) of - normal -> BaseStanza; - MsgType -> add_message_type(BaseStanza, iolist_to_binary(atom_to_list(MsgType))) - end, - %% set the from address on the notification to the bare JID of the account owner - %% Also, add "replyto" if entity has presence subscription to the account owner - %% See XEP-0163 1.1 section 4.3.1 - ejabberd_c2s:broadcast(C2SPid, - {pep_message, <<((Node))/binary, "+notify">>}, - _Sender = jlib:make_jid(LUser, LServer, <<"">>), - _StanzaToSend = add_extended_headers(Stanza, - _ReplyTo = extended_headers([jlib:jid_to_string(Publisher)]))); - _ -> - ?DEBUG("~p@~p has no session; can't deliver ~p to contacts", [LUser, LServer, BaseStanza]) - end; -broadcast_stanza(Host, _Publisher, Node, NodeId, Type, NodeOptions, SubsByDepth, NotifyType, BaseStanza, SHIM) -> - broadcast_stanza(Host, Node, NodeId, Type, NodeOptions, SubsByDepth, NotifyType, BaseStanza, SHIM). + NotificationType = get_option(NodeOptions, notification_type, headline), + %% set the from address on the notification to the bare JID of the account owner + %% Also, add "replyto" if entity has presence subscription to the account owner + %% See XEP-0163 1.1 section 4.3.1 + Owner = jid:make(LUser, LServer), + FromBareJid = xmpp:set_from(BaseStanza, Owner), + Stanza = add_extended_headers( + add_message_type(FromBareJid, NotificationType), + extended_headers([Publisher])), + Pred = fun(To) -> delivery_permitted(Owner, To, NodeOptions) end, + ejabberd_sm:route(jid:make(LUser, LServer, SenderResource), + {pep_message, <<((Node))/binary, "+notify">>, Stanza, Pred}); +broadcast_stanza(Host, _Publisher, Node, Nidx, Type, NodeOptions, SubsByDepth, NotifyType, BaseStanza, SHIM) -> + broadcast_stanza(Host, Node, Nidx, Type, NodeOptions, SubsByDepth, NotifyType, BaseStanza, SHIM). -subscribed_nodes_by_jid(NotifyType, SubsByDepth) -> - NodesToDeliver = fun(Depth, Node, Subs, Acc) -> - NodeName = case Node#pubsub_node.nodeid of - {_, N} -> N; - Other -> Other +-spec c2s_handle_info(ejabberd_c2s:state(), term()) -> ejabberd_c2s:state(). +c2s_handle_info(#{lserver := LServer} = C2SState, + {pep_message, Feature, Packet, Pred}) when is_function(Pred) -> + [maybe_send_pep_stanza(LServer, USR, Caps, Feature, Packet) + || {USR, Caps} <- mod_caps:list_features(C2SState), Pred(USR)], + {stop, C2SState}; +c2s_handle_info(#{lserver := LServer} = C2SState, + {pep_message, Feature, Packet, {_, _, _} = USR}) -> + case mod_caps:get_user_caps(USR, C2SState) of + {ok, Caps} -> maybe_send_pep_stanza(LServer, USR, Caps, Feature, Packet); + error -> ok + end, + {stop, C2SState}; +c2s_handle_info(C2SState, _) -> + C2SState. + +-spec send_items(host(), nodeId(), nodeIdx(), binary(), + nodeOptions(), ljid(), last | integer()) -> ok. +send_items(Host, Node, Nidx, Type, Options, LJID, Number) -> + send_items(Host, Node, Nidx, Type, Options, Host, LJID, LJID, Number). +send_items(Host, Node, Nidx, Type, Options, Publisher, SubLJID, ToLJID, Number) -> + Items = case max_items(Host, Options) of + 1 -> + get_only_item(Host, Type, Nidx, SubLJID); + _ -> + get_last_items(Host, Type, Nidx, SubLJID, Number) end, - NodeOptions = Node#pubsub_node.options, - lists:foldl(fun({LJID, SubID, SubOptions}, {JIDs, Recipients}) -> - case is_to_deliver(LJID, NotifyType, Depth, NodeOptions, SubOptions) of - true -> - %% If is to deliver : - case state_can_deliver(LJID, SubOptions) of - [] -> {JIDs, Recipients}; - JIDsToDeliver -> - lists:foldl( - fun(JIDToDeliver, {JIDsAcc, RecipientsAcc}) -> - case lists:member(JIDToDeliver, JIDs) of - %% check if the JIDs co-accumulator contains the Subscription Jid, - false -> - %% - if not, - %% - add the Jid to JIDs list co-accumulator ; - %% - create a tuple of the Jid, NodeId, and SubID (as list), - %% and add the tuple to the Recipients list co-accumulator - {[JIDToDeliver | JIDsAcc], [{JIDToDeliver, NodeName, [SubID]} | RecipientsAcc]}; - true -> - %% - if the JIDs co-accumulator contains the Jid - %% get the tuple containing the Jid from the Recipient list co-accumulator - {_, {JIDToDeliver, NodeName1, SubIDs}} = lists:keysearch(JIDToDeliver, 1, RecipientsAcc), - %% delete the tuple from the Recipients list - % v1 : Recipients1 = lists:keydelete(LJID, 1, Recipients), - % v2 : Recipients1 = lists:keyreplace(LJID, 1, Recipients, {LJID, NodeId1, [SubID | SubIDs]}), - %% add the SubID to the SubIDs list in the tuple, - %% and add the tuple back to the Recipients list co-accumulator - % v1.1 : {JIDs, lists:append(Recipients1, [{LJID, NodeId1, lists:append(SubIDs, [SubID])}])} - % v1.2 : {JIDs, [{LJID, NodeId1, [SubID | SubIDs]} | Recipients1]} - % v2: {JIDs, Recipients1} - {JIDsAcc, lists:keyreplace(JIDToDeliver, 1, RecipientsAcc, {JIDToDeliver, NodeName1, [SubID | SubIDs]})} - end - end, {JIDs, Recipients}, JIDsToDeliver) - end; + case Items of + [] -> + ok; + Items -> + Delay = case Number of + last -> % handle section 6.1.7 of XEP-0060 + [Last] = Items, + {Stamp, _USR} = Last#pubsub_item.modification, + [#delay{stamp = Stamp}]; + _ -> + [] + end, + Stanza = #message{ + sub_els = [#ps_event{items = items_els(Node, Options, Items)} + | Delay]}, + NotificationType = get_option(Options, notification_type, headline), + send_stanza(Publisher, ToLJID, Node, + add_message_type(Stanza, NotificationType)) + end. + +-spec send_stanza(host(), ljid(), binary(), stanza()) -> ok. +send_stanza({LUser, LServer, _} = Publisher, USR, Node, BaseStanza) -> + Stanza = xmpp:set_from(BaseStanza, jid:make(LUser, LServer)), + USRs = case USR of + {PUser, PServer, <<>>} -> + [{PUser, PServer, PRessource} + || PRessource <- user_resources(PUser, PServer)]; + _ -> + [USR] + end, + lists:foreach( + fun(To) -> + ejabberd_sm:route( + jid:make(Publisher), + {pep_message, <<((Node))/binary, "+notify">>, + add_extended_headers( + Stanza, extended_headers([jid:make(Publisher)])), + To}) + end, USRs); +send_stanza(Host, USR, _Node, Stanza) -> + ejabberd_router:route( + xmpp:set_from_to(Stanza, service_jid(Host), jid:make(USR))). + +-spec maybe_send_pep_stanza(binary(), ljid(), caps(), binary(), stanza()) -> ok. +maybe_send_pep_stanza(LServer, USR, Caps, Feature, Packet) -> + Features = mod_caps:get_features(LServer, Caps), + case lists:member(Feature, Features) of + true -> + ejabberd_router:route(xmpp:set_to(Packet, jid:make(USR))); + false -> + ok + end. + +-spec send_last_items(jid()) -> ok. +send_last_items(JID) -> + ServerHost = JID#jid.lserver, + Host = host(ServerHost), + DBType = config(ServerHost, db_type), + LJID = jid:tolower(JID), + BJID = jid:remove_resource(LJID), + lists:foreach( + fun(PType) -> + Subs = get_subscriptions_for_send_last(Host, PType, DBType, JID, LJID, BJID), + lists:foreach( + fun({#pubsub_node{nodeid = {_, Node}, type = Type, id = Nidx, + options = Options}, _, SubJID}) + when Type == PType-> + send_items(Host, Node, Nidx, PType, Options, Host, SubJID, LJID, 1); + (_) -> + ok + end, + lists:usort(Subs)) + end, config(ServerHost, plugins)). +% pep_from_offline hack can not work anymore, as sender c2s does not +% exists when sender is offline, so we can't get match receiver caps +% does it make sens to send PEP from an offline contact anyway ? +% case config(ServerHost, ignore_pep_from_offline) of +% false -> +% Roster = ejabberd_hooks:run_fold(roster_get, ServerHost, [], +% [{JID#jid.luser, ServerHost}]), +% lists:foreach( +% fun(#roster{jid = {U, S, R}, subscription = Sub}) +% when Sub == both orelse Sub == from, +% S == ServerHost -> +% case user_resources(U, S) of +% [] -> send_last_pep(jid:make(U, S, R), JID); +% _ -> ok %% this is already handled by presence probe +% end; +% (_) -> +% ok %% we can not do anything in any cases +% end, Roster); +% true -> +% ok +% end. +send_last_pep(From, To, Features) -> + ServerHost = From#jid.lserver, + Host = host(ServerHost), + Publisher = jid:tolower(From), + Owner = jid:remove_resource(Publisher), + NotifyNodes = + case Features of + _ when is_list(Features) -> + lists:filtermap( + fun(V) -> + Vs = byte_size(V) - 7, + case V of + <> -> + {true, NotNode}; + _ -> + false + end + end, Features); + _ -> + unknown + end, + case tree_action(Host, get_nodes, [Owner, infinity]) of + Nodes when is_list(Nodes) -> + lists:foreach( + fun(#pubsub_node{nodeid = {_, Node}, type = Type, id = Nidx, options = Options}) -> + MaybeNotify = + case NotifyNodes of + unknown -> true; + _ -> lists:member(Node, NotifyNodes) + end, + case MaybeNotify andalso match_option(Options, send_last_published_item, on_sub_and_presence) of + true -> + case delivery_permitted(From, To, Options) of + true -> + LJID = jid:tolower(To), + send_items(Owner, Node, Nidx, Type, Options, + Publisher, LJID, LJID, 1); + false -> + ok + end; + _ -> + ok + end + end, Nodes); + _ -> + ok + end. + +-spec subscribed_nodes_by_jid(items | nodes, subs_by_depth()) -> [{ljid(), binary(), subId()}]. +subscribed_nodes_by_jid(NotifyType, SubsByDepth) -> + NodesToDeliver = fun (Depth, Node, Subs, Acc) -> + NodeName = case Node#pubsub_node.nodeid of + {_, N} -> N; + Other -> Other + end, + NodeOptions = Node#pubsub_node.options, + lists:foldl(fun({LJID, SubID, SubOptions}, {JIDs, Recipients}) -> + case is_to_deliver(LJID, NotifyType, Depth, NodeOptions, SubOptions) of + true -> + case state_can_deliver(LJID, SubOptions) of + [] -> {JIDs, Recipients}; + [LJID] -> {JIDs, [{LJID, NodeName, [SubID]} | Recipients]}; + JIDsToDeliver -> + lists:foldl( + fun(JIDToDeliver, {JIDsAcc, RecipientsAcc}) -> + case lists:member(JIDToDeliver, JIDs) of + %% check if the JIDs co-accumulator contains the Subscription Jid, + false -> + %% - if not, + %% - add the Jid to JIDs list co-accumulator ; + %% - create a tuple of the Jid, Nidx, and SubID (as list), + %% and add the tuple to the Recipients list co-accumulator + {[JIDToDeliver | JIDsAcc], + [{JIDToDeliver, NodeName, [SubID]} + | RecipientsAcc]}; + true -> + %% - if the JIDs co-accumulator contains the Jid + %% get the tuple containing the Jid from the Recipient list co-accumulator + {_, {JIDToDeliver, NodeName1, SubIDs}} = + lists:keysearch(JIDToDeliver, 1, RecipientsAcc), + %% delete the tuple from the Recipients list + % v1 : Recipients1 = lists:keydelete(LJID, 1, Recipients), + % v2 : Recipients1 = lists:keyreplace(LJID, 1, Recipients, {LJID, Nidx1, [SubID | SubIDs]}), + %% add the SubID to the SubIDs list in the tuple, + %% and add the tuple back to the Recipients list co-accumulator + % v1.1 : {JIDs, lists:append(Recipients1, [{LJID, Nidx1, lists:append(SubIDs, [SubID])}])} + % v1.2 : {JIDs, [{LJID, Nidx1, [SubID | SubIDs]} | Recipients1]} + % v2: {JIDs, Recipients1} + {JIDsAcc, + lists:keyreplace(JIDToDeliver, 1, + RecipientsAcc, + {JIDToDeliver, NodeName1, + [SubID | SubIDs]})} + end + end, {JIDs, Recipients}, JIDsToDeliver) + end; false -> {JIDs, Recipients} - end - end, Acc, Subs) + end + end, Acc, Subs) end, DepthsToDeliver = fun({Depth, SubsByNode}, Acc1) -> - lists:foldl(fun({Node, Subs}, Acc2) -> - NodesToDeliver(Depth, Node, Subs, Acc2) - end, Acc1, SubsByNode) + lists:foldl(fun({Node, Subs}, Acc2) -> + NodesToDeliver(Depth, Node, Subs, Acc2) + end, Acc1, SubsByNode) end, {_, JIDSubs} = lists:foldl(DepthsToDeliver, {[], []}, SubsByDepth), JIDSubs. +-spec delivery_permitted(jid() | ljid(), jid() | ljid(), nodeOptions()) -> boolean(). +delivery_permitted(From, To, Options) -> + LFrom = jid:tolower(From), + LTo = jid:tolower(To), + RecipientIsOwner = jid:remove_resource(LFrom) == jid:remove_resource(LTo), + %% TODO: Fix the 'whitelist'/'authorize' cases for last PEP notifications. + %% Currently, only node owners receive those. + case get_option(Options, access_model) of + open -> true; + presence -> true; + whitelist -> RecipientIsOwner; + authorize -> RecipientIsOwner; + roster -> + Grps = get_option(Options, roster_groups_allowed, []), + {LUser, LServer, _} = LFrom, + {_, IsInGrp} = get_roster_info(LUser, LServer, LTo, Grps), + IsInGrp + end. + +-spec user_resources(binary(), binary()) -> [binary()]. user_resources(User, Server) -> ejabberd_sm:get_user_resources(User, Server). +-spec user_resource(binary(), binary(), binary()) -> binary(). user_resource(User, Server, <<>>) -> case user_resources(User, Server) of [R | _] -> R; _ -> <<>> end; -user_resource(_, _, Resource) -> Resource. +user_resource(_, _, Resource) -> + Resource. %%%%%%% Configuration handling - -%%

    There are several reasons why the default node configuration options request might fail:

    -%%
      -%%
    • The service does not support node configuration.
    • -%%
    • The service does not support retrieval of default node configuration.
    • -%%
    +-spec get_configure(host(), binary(), binary(), jid(), + binary()) -> {error, stanza_error()} | {result, pubsub_owner()}. get_configure(Host, ServerHost, Node, From, Lang) -> - Action = fun (#pubsub_node{options = Options, - type = Type, id = NodeId}) -> - case node_call(Type, get_affiliation, [NodeId, From]) of - {result, owner} -> - Groups = ejabberd_hooks:run_fold(roster_groups, - ServerHost, [], - [ServerHost]), - {result, - [#xmlel{name = <<"pubsub">>, - attrs = [{<<"xmlns">>, ?NS_PUBSUB_OWNER}], - children = - [#xmlel{name = <<"configure">>, - attrs = nodeAttr(Node), - children = - [#xmlel{name = <<"x">>, - attrs = - [{<<"xmlns">>, - ?NS_XDATA}, - {<<"type">>, - <<"form">>}], - children = - get_configure_xfields(Type, - Options, - Lang, - Groups)}]}]}]}; - _ -> {error, ?ERR_FORBIDDEN} - end - end, + Action = fun (#pubsub_node{options = Options, type = Type, id = Nidx}) -> + case node_call(Host, Type, get_affiliation, [Nidx, From]) of + {result, owner} -> + Groups = ejabberd_hooks:run_fold(roster_groups, ServerHost, [], [ServerHost]), + Fs = get_configure_xfields(Type, Options, Lang, Groups), + {result, #pubsub_owner{ + configure = + {Node, #xdata{type = form, fields = Fs}}}}; + {result, _} -> + {error, xmpp:err_forbidden(?T("Owner privileges required"), Lang)}; + Error -> + Error + end + end, case transaction(Host, Node, Action, sync_dirty) of - {result, {_, Result}} -> {result, Result}; - Other -> Other + {result, {_, Result}} -> {result, Result}; + Other -> Other end. +-spec get_default(host(), binary(), jid(), binary()) -> {result, pubsub_owner()}. get_default(Host, Node, _From, Lang) -> - Type = select_type(Host, Host, Node), - Options = node_options(Type), -%% Get node option -%% The result depend of the node type plugin system. - {result, - [#xmlel{name = <<"pubsub">>, - attrs = [{<<"xmlns">>, ?NS_PUBSUB_OWNER}], - children = - [#xmlel{name = <<"default">>, attrs = [], - children = - [#xmlel{name = <<"x">>, - attrs = - [{<<"xmlns">>, ?NS_XDATA}, - {<<"type">>, <<"form">>}], - children = - get_configure_xfields(Type, Options, - Lang, [])}]}]}]}. + Type = select_type(serverhost(Host), Host, Node), + Options = node_options(Host, Type), + Fs = get_configure_xfields(Type, Options, Lang, []), + {result, #pubsub_owner{default = {<<>>, #xdata{type = form, fields = Fs}}}}. +-spec match_option(#pubsub_node{} | [{atom(), any()}], atom(), any()) -> boolean(). +match_option(Node, Var, Val) when is_record(Node, pubsub_node) -> + match_option(Node#pubsub_node.options, Var, Val); +match_option(Options, Var, Val) when is_list(Options) -> + get_option(Options, Var) == Val; +match_option(_, _, _) -> + false. + +-spec get_option([{atom(), any()}], atom()) -> any(). get_option([], _) -> false; -get_option(Options, Var) -> - get_option(Options, Var, false). +get_option(Options, Var) -> get_option(Options, Var, false). +-spec get_option([{atom(), any()}], atom(), any()) -> any(). get_option(Options, Var, Def) -> case lists:keysearch(Var, 1, Options) of - {value, {_Val, Ret}} -> Ret; - _ -> Def + {value, {_Val, Ret}} -> Ret; + _ -> Def end. -%% Get default options from the module plugin. -node_options(Type) -> - Module = - jlib:binary_to_atom(<<(?PLUGIN_PREFIX)/binary, - Type/binary>>), - case catch Module:options() of - {'EXIT', {undef, _}} -> - DefaultModule = - jlib:binary_to_atom(<<(?PLUGIN_PREFIX)/binary, - (?STDNODE)/binary>>), - DefaultModule:options(); - Result -> Result +-spec node_options(host(), binary()) -> [{atom(), any()}]. +node_options(Host, Type) -> + ConfigOpts = config(Host, default_node_config), + PluginOpts = node_plugin_options(Host, Type), + merge_config([ConfigOpts, PluginOpts]). + +-spec node_plugin_options(host(), binary()) -> [{atom(), any()}]. +node_plugin_options(Host, Type) -> + Module = plugin(Host, Type), + case {lists:member(Type, config(Host, plugins)), catch Module:options()} of + {true, Opts} when is_list(Opts) -> + Opts; + {_, _} -> + DefaultModule = plugin(Host, ?STDNODE), + DefaultModule:options() end. -%% @spec (Host, Options) -> MaxItems -%% Host = host() -%% Options = [Option] -%% Option = {Key::atom(), Value::term()} -%% MaxItems = integer() | unlimited +-spec node_owners_action(host(), binary(), nodeIdx(), [ljid()]) -> [ljid()]. +node_owners_action(Host, Type, Nidx, []) -> + case node_action(Host, Type, get_node_affiliations, [Nidx]) of + {result, Affs} -> [LJID || {LJID, Aff} <- Affs, Aff =:= owner]; + _ -> [] + end; +node_owners_action(_Host, _Type, _Nidx, Owners) -> + Owners. + +-spec node_owners_call(host(), binary(), nodeIdx(), [ljid()]) -> [ljid()]. +node_owners_call(Host, Type, Nidx, []) -> + case node_call(Host, Type, get_node_affiliations, [Nidx]) of + {result, Affs} -> [LJID || {LJID, Aff} <- Affs, Aff =:= owner]; + _ -> [] + end; +node_owners_call(_Host, _Type, _Nidx, Owners) -> + Owners. + +node_config(Node, ServerHost) -> + Opts = mod_pubsub_opt:force_node_config(ServerHost), + node_config(Node, ServerHost, Opts). + +node_config(Node, ServerHost, [{RE, Opts}|NodeOpts]) -> + case re:run(Node, RE) of + {match, _} -> + Opts; + nomatch -> + node_config(Node, ServerHost, NodeOpts) + end; +node_config(_, _, []) -> + []. + %% @doc

    Return the maximum number of items for a given node.

    %%

    Unlimited means that there is no limit in the number of items that can %% be stored.

    -%% @todo In practice, the current data structure means that we cannot manage -%% millions of items on a given node. This should be addressed in a new -%% version. +-spec max_items(host(), [{atom(), any()}]) -> non_neg_integer() | unlimited. max_items(Host, Options) -> case get_option(Options, persist_items) of - true -> - case get_option(Options, max_items) of - false -> unlimited; - Result when Result < 0 -> 0; - Result -> Result - end; - false -> - case get_option(Options, send_last_published_item) of - never -> 0; - _ -> - case is_last_item_cache_enabled(Host) of - true -> 0; - false -> 1 - end - end + true -> + case get_option(Options, max_items) of + I when is_integer(I), I < 0 -> 0; + I when is_integer(I) -> I; + _ -> get_max_items_node(Host) + end; + false -> + case get_option(Options, send_last_published_item) of + never -> + 0; + _ -> + case is_last_item_cache_enabled(Host) of + true -> 0; + false -> 1 + end + end end. --define(BOOL_CONFIG_FIELD(Label, Var), - ?BOOLXFIELD(Label, - <<"pubsub#", - (iolist_to_binary(atom_to_list(Var)))/binary>>, - (get_option(Options, Var)))). - --define(STRING_CONFIG_FIELD(Label, Var), - ?STRINGXFIELD(Label, - <<"pubsub#", - (iolist_to_binary(atom_to_list(Var)))/binary>>, - (get_option(Options, Var, <<"">>)))). - --define(INTEGER_CONFIG_FIELD(Label, Var), - ?STRINGXFIELD(Label, - <<"pubsub#", - (iolist_to_binary(atom_to_list(Var)))/binary>>, - (iolist_to_binary(integer_to_list(get_option(Options, - Var)))))). - --define(JLIST_CONFIG_FIELD(Label, Var, Opts), - ?LISTXFIELD(Label, - <<"pubsub#", - (iolist_to_binary(atom_to_list(Var)))/binary>>, - (jlib:jid_to_string(get_option(Options, Var))), - [jlib:jid_to_string(O) || O <- Opts])). - --define(ALIST_CONFIG_FIELD(Label, Var, Opts), - ?LISTXFIELD(Label, - <<"pubsub#", - (iolist_to_binary(atom_to_list(Var)))/binary>>, - (iolist_to_binary(atom_to_list(get_option(Options, - Var)))), - [iolist_to_binary(atom_to_list(O)) || O <- Opts])). - --define(LISTM_CONFIG_FIELD(Label, Var, Opts), - ?LISTMXFIELD(Label, - <<"pubsub#", - (iolist_to_binary(atom_to_list(Var)))/binary>>, - (get_option(Options, Var)), Opts)). - --define(NLIST_CONFIG_FIELD(Label, Var), - ?STRINGMXFIELD(Label, - <<"pubsub#", - (iolist_to_binary(atom_to_list(Var)))/binary>>, - get_option(Options, Var, []))). +-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) -> - [?XFIELD(<<"hidden">>, <<"">>, <<"FORM_TYPE">>, - (?NS_PUBSUB_NODE_CONFIG)), - ?BOOL_CONFIG_FIELD(<<"Deliver payloads with event notifications">>, - deliver_payloads), - ?BOOL_CONFIG_FIELD(<<"Deliver event notifications">>, - deliver_notifications), - ?BOOL_CONFIG_FIELD(<<"Notify subscribers when the node configuratio" - "n changes">>, - notify_config), - ?BOOL_CONFIG_FIELD(<<"Notify subscribers when the node is " - "deleted">>, - notify_delete), - ?BOOL_CONFIG_FIELD(<<"Notify subscribers when items are removed " - "from the node">>, - notify_retract), - ?BOOL_CONFIG_FIELD(<<"Persist items to storage">>, - persist_items), - ?STRING_CONFIG_FIELD(<<"A friendly name for the node">>, - title), - ?INTEGER_CONFIG_FIELD(<<"Max # of items to persist">>, - max_items), - ?BOOL_CONFIG_FIELD(<<"Whether to allow subscriptions">>, - subscribe), - ?ALIST_CONFIG_FIELD(<<"Specify the access model">>, - access_model, - [open, authorize, presence, roster, whitelist]), - ?LISTM_CONFIG_FIELD(<<"Roster groups allowed to subscribe">>, - roster_groups_allowed, Groups), - ?ALIST_CONFIG_FIELD(<<"Specify the publisher model">>, - publish_model, [publishers, subscribers, open]), - ?BOOL_CONFIG_FIELD(<<"Purge all items when the relevant publisher " - "goes offline">>, - purge_offline), - ?ALIST_CONFIG_FIELD(<<"Specify the event message type">>, - notification_type, [headline, normal]), - ?INTEGER_CONFIG_FIELD(<<"Max payload size in bytes">>, - max_payload_size), - ?ALIST_CONFIG_FIELD(<<"When to send the last published item">>, - send_last_published_item, - [never, on_sub, on_sub_and_presence]), - ?BOOL_CONFIG_FIELD(<<"Only deliver notifications to available " - "users">>, - presence_based_delivery), - ?NLIST_CONFIG_FIELD(<<"The collections with which a node is " - "affiliated">>, - collection)]. + pubsub_node_config:encode( + lists:filtermap( + fun({roster_groups_allowed, Value}) -> + {true, {roster_groups_allowed, Value, Groups}}; + ({sql, _}) -> false; + ({rsm, _}) -> false; + ({Item, infinity}) when Item == max_items; + Item == item_expire; + Item == children_max -> + {true, {Item, max}}; + (_) -> true + end, Options), + Lang). %%

    There are several reasons why the node configuration request might fail:

    %%
      @@ -4817,314 +3467,281 @@ get_configure_xfields(_Type, Options, Lang, Groups) -> %%
    • The node has no configuration options.
    • %%
    • The specified node does not exist.
    • %%
    -set_configure(Host, Node, From, Els, Lang) -> - case xml:remove_cdata(Els) of - [#xmlel{name = <<"x">>} = XEl] -> - case {xml:get_tag_attr_s(<<"xmlns">>, XEl), - xml:get_tag_attr_s(<<"type">>, XEl)} - of - {?NS_XDATA, <<"cancel">>} -> {result, []}; - {?NS_XDATA, <<"submit">>} -> - Action = fun (#pubsub_node{options = Options, - type = Type, id = NodeId} = - N) -> - case node_call(Type, get_affiliation, - [NodeId, From]) - of - {result, owner} -> - case jlib:parse_xdata_submit(XEl) of - invalid -> {error, ?ERR_BAD_REQUEST}; - XData -> - OldOpts = case Options of - [] -> - node_options(Type); - _ -> Options - end, - case set_xoption(Host, XData, - OldOpts) - of - NewOpts - when is_list(NewOpts) -> - case tree_call(Host, - set_node, - [N#pubsub_node{options - = - NewOpts}]) - of - ok -> {result, ok}; - Err -> Err - end; - Err -> Err - end - end; - _ -> {error, ?ERR_FORBIDDEN} - end - end, - case transaction(Host, Node, Action, transaction) of - {result, {TNode, ok}} -> - NodeId = TNode#pubsub_node.id, - Type = TNode#pubsub_node.type, - Options = TNode#pubsub_node.options, - broadcast_config_notification(Host, Node, NodeId, Type, - Options, Lang), - {result, []}; - Other -> Other - end; - _ -> {error, ?ERR_BAD_REQUEST} - end; - _ -> {error, ?ERR_BAD_REQUEST} +-spec set_configure(host(), binary(), jid(), [{binary(), [binary()]}], + binary()) -> {result, undefined} | {error, stanza_error()}. +set_configure(_Host, <<>>, _From, _Config, _Lang) -> + {error, extended_error(xmpp:err_bad_request(), err_nodeid_required())}; +set_configure(Host, Node, From, Config, Lang) -> + Action = + fun(#pubsub_node{options = Options, type = Type, id = Nidx} = N) -> + case node_call(Host, Type, get_affiliation, [Nidx, From]) of + {result, owner} -> + OldOpts = case Options of + [] -> node_options(Host, Type); + _ -> Options + end, + NewOpts = merge_config( + [node_config(Node, serverhost(Host)), + Config, OldOpts]), + case tree_call(Host, + set_node, + [N#pubsub_node{options = NewOpts}]) of + {result, Nidx} -> {result, NewOpts}; + ok -> {result, NewOpts}; + Err -> Err + end; + {result, _} -> + {error, xmpp:err_forbidden( + ?T("Owner privileges required"), Lang)}; + Error -> + Error + end + end, + case transaction(Host, Node, Action, transaction) of + {result, {TNode, Options}} -> + Nidx = TNode#pubsub_node.id, + Type = TNode#pubsub_node.type, + broadcast_config_notification(Host, Node, Nidx, Type, Options, Lang), + {result, undefined}; + Other -> + Other end. -add_opt(Key, Value, Opts) -> - Opts1 = lists:keydelete(Key, 1, Opts), - [{Key, Value} | Opts1]. +-spec merge_config([[proplists:property()]]) -> [proplists:property()]. +merge_config(ListOfConfigs) -> + lists:ukeysort(1, lists:flatten(ListOfConfigs)). --define(SET_BOOL_XOPT(Opt, Val), - BoolVal = case Val of - <<"0">> -> false; - <<"1">> -> true; - <<"false">> -> false; - <<"true">> -> true; - _ -> error - end, - case BoolVal of - error -> {error, ?ERR_NOT_ACCEPTABLE}; - _ -> - set_xoption(Host, Opts, add_opt(Opt, BoolVal, NewOpts)) - end). +-spec decode_node_config(undefined | xdata(), binary(), binary()) -> + pubsub_node_config:result() | + {error, stanza_error()}. +decode_node_config(undefined, _, _) -> + []; +decode_node_config(#xdata{fields = Fs}, Host, Lang) -> + try + Config = pubsub_node_config:decode(Fs), + 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} -> + Config; + {true, true, false} -> + erlang:error( + {pubsub_node_config, + {bad_var_value, <<"pubsub#max_payload_size">>, + ?NS_PUBSUB_NODE_CONFIG}}); + {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">>, + ?NS_PUBSUB_NODE_CONFIG}}) + end + catch _:{pubsub_node_config, Why} -> + Txt = pubsub_node_config:format_error(Why), + {error, xmpp:err_resource_constraint(Txt, Lang)} + end. --define(SET_STRING_XOPT(Opt, Val), - set_xoption(Host, Opts, add_opt(Opt, Val, NewOpts))). +-spec decode_subscribe_options(undefined | xdata(), binary()) -> + pubsub_subscribe_options:result() | + {error, stanza_error()}. +decode_subscribe_options(undefined, _) -> + []; +decode_subscribe_options(#xdata{fields = Fs}, Lang) -> + try pubsub_subscribe_options:decode(Fs) + catch _:{pubsub_subscribe_options, Why} -> + Txt = pubsub_subscribe_options:format_error(Why), + {error, xmpp:err_resource_constraint(Txt, Lang)} + end. --define(SET_INTEGER_XOPT(Opt, Val, Min, Max), - case catch jlib:binary_to_integer(Val) of - IVal when is_integer(IVal), IVal >= Min, IVal =< Max -> - set_xoption(Host, Opts, add_opt(Opt, IVal, NewOpts)); - _ -> {error, ?ERR_NOT_ACCEPTABLE} - end). +-spec decode_publish_options(undefined | xdata(), binary()) -> + pubsub_publish_options:result() | + {error, stanza_error()}. +decode_publish_options(undefined, _) -> + []; +decode_publish_options(#xdata{fields = Fs}, Lang) -> + try pubsub_publish_options:decode(Fs) + catch _:{pubsub_publish_options, Why} -> + Txt = pubsub_publish_options:format_error(Why), + {error, xmpp:err_resource_constraint(Txt, Lang)} + end. --define(SET_ALIST_XOPT(Opt, Val, Vals), - case lists:member(Val, - [iolist_to_binary(atom_to_list(V)) || V <- Vals]) - of - true -> - set_xoption(Host, Opts, - add_opt(Opt, jlib:binary_to_atom(Val), NewOpts)); - false -> {error, ?ERR_NOT_ACCEPTABLE} - end). +-spec decode_get_pending(xdata(), binary()) -> + pubsub_get_pending:result() | + {error, stanza_error()}. +decode_get_pending(#xdata{fields = Fs}, Lang) -> + try pubsub_get_pending:decode(Fs) + catch _:{pubsub_get_pending, Why} -> + Txt = pubsub_get_pending:format_error(Why), + {error, xmpp:err_resource_constraint(Txt, Lang)} + end. --define(SET_LIST_XOPT(Opt, Val), - set_xoption(Host, Opts, add_opt(Opt, Val, NewOpts))). +-spec check_opt_range(atom(), [proplists:property()], + 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. -set_xoption(_Host, [], NewOpts) -> NewOpts; -set_xoption(Host, [{<<"FORM_TYPE">>, _} | Opts], - NewOpts) -> - set_xoption(Host, Opts, NewOpts); -set_xoption(Host, - [{<<"pubsub#roster_groups_allowed">>, Value} | Opts], - NewOpts) -> - ?SET_LIST_XOPT(roster_groups_allowed, Value); -set_xoption(Host, - [{<<"pubsub#deliver_payloads">>, [Val]} | Opts], - NewOpts) -> - ?SET_BOOL_XOPT(deliver_payloads, Val); -set_xoption(Host, - [{<<"pubsub#deliver_notifications">>, [Val]} | Opts], - NewOpts) -> - ?SET_BOOL_XOPT(deliver_notifications, Val); -set_xoption(Host, - [{<<"pubsub#notify_config">>, [Val]} | Opts], - NewOpts) -> - ?SET_BOOL_XOPT(notify_config, Val); -set_xoption(Host, - [{<<"pubsub#notify_delete">>, [Val]} | Opts], - NewOpts) -> - ?SET_BOOL_XOPT(notify_delete, Val); -set_xoption(Host, - [{<<"pubsub#notify_retract">>, [Val]} | Opts], - NewOpts) -> - ?SET_BOOL_XOPT(notify_retract, Val); -set_xoption(Host, - [{<<"pubsub#persist_items">>, [Val]} | Opts], - NewOpts) -> - ?SET_BOOL_XOPT(persist_items, Val); -set_xoption(Host, - [{<<"pubsub#max_items">>, [Val]} | Opts], NewOpts) -> - MaxItems = get_max_items_node(Host), - ?SET_INTEGER_XOPT(max_items, Val, 0, MaxItems); -set_xoption(Host, - [{<<"pubsub#subscribe">>, [Val]} | Opts], NewOpts) -> - ?SET_BOOL_XOPT(subscribe, Val); -set_xoption(Host, - [{<<"pubsub#access_model">>, [Val]} | Opts], NewOpts) -> - ?SET_ALIST_XOPT(access_model, Val, - [open, authorize, presence, roster, whitelist]); -set_xoption(Host, - [{<<"pubsub#publish_model">>, [Val]} | Opts], - NewOpts) -> - ?SET_ALIST_XOPT(publish_model, Val, - [publishers, subscribers, open]); -set_xoption(Host, - [{<<"pubsub#notification_type">>, [Val]} | Opts], - NewOpts) -> - ?SET_ALIST_XOPT(notification_type, Val, - [headline, normal]); -set_xoption(Host, - [{<<"pubsub#node_type">>, [Val]} | Opts], NewOpts) -> - ?SET_ALIST_XOPT(node_type, Val, [leaf, collection]); -set_xoption(Host, - [{<<"pubsub#max_payload_size">>, [Val]} | Opts], - NewOpts) -> - ?SET_INTEGER_XOPT(max_payload_size, Val, 0, - (?MAX_PAYLOAD_SIZE)); -set_xoption(Host, - [{<<"pubsub#send_last_published_item">>, [Val]} | Opts], - NewOpts) -> - ?SET_ALIST_XOPT(send_last_published_item, Val, - [never, on_sub, on_sub_and_presence]); -set_xoption(Host, - [{<<"pubsub#presence_based_delivery">>, [Val]} | Opts], - NewOpts) -> - ?SET_BOOL_XOPT(presence_based_delivery, Val); -set_xoption(Host, - [{<<"pubsub#purge_offline">>, [Val]} | Opts], - NewOpts) -> - ?SET_BOOL_XOPT(purge_offline, Val); -set_xoption(Host, [{<<"pubsub#title">>, Value} | Opts], - NewOpts) -> - ?SET_STRING_XOPT(title, Value); -set_xoption(Host, [{<<"pubsub#type">>, Value} | Opts], - NewOpts) -> - ?SET_STRING_XOPT(type, Value); -set_xoption(Host, - [{<<"pubsub#body_xslt">>, Value} | Opts], NewOpts) -> - ?SET_STRING_XOPT(body_xslt, Value); -set_xoption(Host, - [{<<"pubsub#collection">>, Value} | Opts], NewOpts) -> -% NewValue = [string_to_node(V) || V <- Value], - ?SET_LIST_XOPT(collection, Value); -set_xoption(Host, [{<<"pubsub#node">>, [Value]} | Opts], - NewOpts) -> -% NewValue = string_to_node(Value), - ?SET_LIST_XOPT(node, Value); -set_xoption(Host, [_ | Opts], NewOpts) -> - set_xoption(Host, Opts, NewOpts). - -get_max_items_node({_, ServerHost, _}) -> - get_max_items_node(ServerHost); +-spec get_max_items_node(host()) -> unlimited | non_neg_integer(). get_max_items_node(Host) -> - case catch ets:lookup(gen_mod:get_module_proc(Host, - config), - max_items_node) - of - [{max_items_node, Integer}] -> Integer; - _ -> ?MAXITEMS - end. + 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) -> + config(Host, max_subscriptions_node, undefined). %%%% last item cache handling - -is_last_item_cache_enabled({_, ServerHost, _}) -> - is_last_item_cache_enabled(ServerHost); +-spec is_last_item_cache_enabled(host()) -> boolean(). is_last_item_cache_enabled(Host) -> - case catch ets:lookup(gen_mod:get_module_proc(Host, - config), - last_item_cache) - of - [{last_item_cache, true}] -> true; - _ -> false + config(Host, last_item_cache, false). + +-spec set_cached_item(host(), nodeIdx(), binary(), jid(), [xmlel()]) -> ok. +set_cached_item({_, ServerHost, _}, Nidx, ItemId, Publisher, Payload) -> + set_cached_item(ServerHost, Nidx, ItemId, Publisher, Payload); +set_cached_item(Host, Nidx, ItemId, Publisher, Payload) -> + case is_last_item_cache_enabled(Host) of + true -> + Stamp = {erlang:timestamp(), jid:tolower(jid:remove_resource(Publisher))}, + Item = #pubsub_last_item{nodeid = {Host, Nidx}, + itemid = ItemId, + creation = Stamp, + payload = Payload}, + mnesia:dirty_write(Item); + _ -> + ok end. -set_cached_item({_, ServerHost, _}, NodeId, ItemId, - Publisher, Payload) -> - set_cached_item(ServerHost, NodeId, ItemId, Publisher, - Payload); -set_cached_item(Host, NodeId, ItemId, Publisher, - Payload) -> +-spec unset_cached_item(host(), nodeIdx()) -> ok. +unset_cached_item({_, ServerHost, _}, Nidx) -> + unset_cached_item(ServerHost, Nidx); +unset_cached_item(Host, Nidx) -> case is_last_item_cache_enabled(Host) of - true -> - mnesia:dirty_write({pubsub_last_item, NodeId, ItemId, - {now(), - jlib:jid_tolower(jlib:jid_remove_resource(Publisher))}, - Payload}); - _ -> ok + true -> mnesia:dirty_delete({pubsub_last_item, {Host, Nidx}}); + _ -> ok end. -unset_cached_item({_, ServerHost, _}, NodeId) -> - unset_cached_item(ServerHost, NodeId); -unset_cached_item(Host, NodeId) -> +-spec get_cached_item(host(), nodeIdx()) -> undefined | #pubsub_item{}. +get_cached_item({_, ServerHost, _}, Nidx) -> + get_cached_item(ServerHost, Nidx); +get_cached_item(Host, Nidx) -> case is_last_item_cache_enabled(Host) of - true -> mnesia:dirty_delete({pubsub_last_item, NodeId}); - _ -> ok - end. - --spec(get_cached_item/2 :: -( - Host :: mod_pubsub:host(), - NodeIdx :: mod_pubsub:nodeIdx()) - -> undefined | mod_pubsub:pubsubItem() -). -get_cached_item({_, ServerHost, _}, NodeId) -> - get_cached_item(ServerHost, NodeId); -get_cached_item(Host, NodeIdx) -> - case is_last_item_cache_enabled(Host) of - true -> - case mnesia:dirty_read({pubsub_last_item, NodeIdx}) of - [#pubsub_last_item{itemid = ItemId, creation = Creation, payload = Payload}] -> -% [{pubsub_last_item, NodeId, ItemId, Creation, -% Payload}] -> - #pubsub_item{itemid = {ItemId, NodeIdx}, - payload = Payload, creation = Creation, - modification = Creation}; - _ -> undefined - end; - _ -> undefined + true -> + case mnesia:dirty_read({pubsub_last_item, {Host, Nidx}}) of + [#pubsub_last_item{itemid = ItemId, creation = Creation, payload = Payload}] -> + #pubsub_item{itemid = {ItemId, Nidx}, + payload = Payload, creation = Creation, + modification = Creation}; + _ -> + undefined + end; + _ -> + undefined end. %%%% plugin handling - +-spec host(binary()) -> binary(). host(ServerHost) -> - case catch - ets:lookup(gen_mod:get_module_proc(ServerHost, config), - host) - of - [{host, Host}] -> Host; - _ -> <<"pubsub.", ServerHost/binary>> + config(ServerHost, host, <<"pubsub.", ServerHost/binary>>). + +-spec serverhost(host()) -> binary(). +serverhost({_U, ServerHost, _R})-> + serverhost(ServerHost); +serverhost(Host) -> + ejabberd_router:host_of_route(Host). + +-spec tree(host()) -> atom(). +tree(Host) -> + case config(Host, nodetree) of + undefined -> tree(Host, ?STDTREE); + Tree -> Tree end. +-spec tree(host() | atom(), binary()) -> atom(). +tree(_Host, <<"virtual">>) -> + nodetree_virtual; % special case, virtual does not use any backend +tree(Host, Name) -> + submodule(Host, <<"nodetree">>, Name). + +-spec plugin(host() | atom(), binary()) -> atom(). +plugin(Host, Name) -> + submodule(Host, <<"node">>, Name). + +-spec plugins(host()) -> [binary()]. plugins(Host) -> - case catch ets:lookup(gen_mod:get_module_proc(Host, - config), - plugins) - of - [{plugins, []}] -> [?STDNODE]; - [{plugins, PL}] -> PL; - _ -> [?STDNODE] + case config(Host, plugins) of + undefined -> [?STDNODE]; + [] -> [?STDNODE]; + Plugins -> Plugins end. -select_type(ServerHost, Host, Node, Type) -> - SelectedType = case Host of - {_User, _Server, _Resource} -> - case catch - ets:lookup(gen_mod:get_module_proc(ServerHost, - config), - pep_mapping) - of - [{pep_mapping, PM}] -> - proplists:get_value(Node, PM, ?PEPNODE); - _ -> ?PEPNODE - end; - _ -> Type - end, - ConfiguredTypes = plugins(ServerHost), - case lists:member(SelectedType, ConfiguredTypes) of - true -> SelectedType; - false -> hd(ConfiguredTypes) +-spec subscription_plugin(host() | atom()) -> atom(). +subscription_plugin(Host) -> + submodule(Host, <<"pubsub">>, <<"subscription">>). + +-spec submodule(host() | atom(), binary(), binary()) -> atom(). +submodule(Db, Type, Name) when is_atom(Db) -> + case Db of + mnesia -> ejabberd:module_name([<<"pubsub">>, Type, Name]); + _ -> ejabberd:module_name([<<"pubsub">>, Type, Name, misc:atom_to_binary(Db)]) + end; +submodule(Host, Type, Name) -> + Db = mod_pubsub_opt:db_type(serverhost(Host)), + submodule(Db, Type, Name). + +-spec config(binary(), any()) -> any(). +config(ServerHost, Key) -> + config(ServerHost, Key, undefined). + +-spec config(host(), any(), any()) -> any(). +config({_User, Host, _Resource}, Key, Default) -> + config(Host, Key, Default); +config(ServerHost, Key, Default) -> + case catch ets:lookup(gen_mod:get_module_proc(ServerHost, config), Key) of + [{Key, Value}] -> Value; + _ -> Default end. +-spec select_type(binary(), host(), binary(), binary()) -> binary(). +select_type(ServerHost, {_User, _Server, _Resource}, Node, _Type) -> + case config(ServerHost, pep_mapping) of + undefined -> ?PEPNODE; + Mapping -> proplists:get_value(Node, Mapping, ?PEPNODE) + end; +select_type(ServerHost, _Host, _Node, Type) -> + case config(ServerHost, plugins) of + undefined -> + Type; + Plugins -> + case lists:member(Type, Plugins) of + true -> Type; + false -> hd(Plugins) + end + end. + +-spec select_type(binary(), host(), binary()) -> binary(). select_type(ServerHost, Host, Node) -> - select_type(ServerHost, Host, Node, - hd(plugins(ServerHost))). + select_type(ServerHost, Host, Node, hd(plugins(Host))). +-spec feature(binary()) -> binary(). +feature(<<"rsm">>) -> ?NS_RSM; +feature(Feature) -> <<(?NS_PUBSUB)/binary, "#", Feature/binary>>. + +-spec features() -> [binary()]. features() -> [% see plugin "access-authorize", % OPTIONAL <<"access-open">>, % OPTIONAL this relates to access_model option in node_hometree @@ -5132,6 +3749,7 @@ features() -> <<"access-whitelist">>, % OPTIONAL <<"collections">>, % RECOMMENDED <<"config-node">>, % RECOMMENDED + <<"config-node-max">>, <<"create-and-configure">>, % RECOMMENDED <<"item-ids">>, % RECOMMENDED <<"last-published">>, % RECOMMENDED @@ -5139,173 +3757,320 @@ features() -> <<"presence-notifications">>, % OPTIONAL <<"presence-subscribe">>, % RECOMMENDED <<"publisher-affiliation">>, % RECOMMENDED - <<"retrieve-default">>]. + <<"publish-only-affiliation">>, % OPTIONAL + <<"publish-options">>, % OPTIONAL + <<"retrieve-default">>, + <<"shim">>]. % RECOMMENDED - % see plugin "retrieve-items", % RECOMMENDED - % see plugin "retrieve-subscriptions", % RECOMMENDED - %TODO "shim", % OPTIONAL - % see plugin "subscribe", % REQUIRED - % see plugin "subscription-options", % OPTIONAL - % see plugin "subscription-notifications" % OPTIONAL - -features(Type) -> - Module = - jlib:binary_to_atom(<<(?PLUGIN_PREFIX)/binary, - Type/binary>>), - features() ++ - case catch Module:features() of +% see plugin "retrieve-items", % RECOMMENDED +% see plugin "retrieve-subscriptions", % RECOMMENDED +% see plugin "subscribe", % REQUIRED +% see plugin "subscription-options", % OPTIONAL +% see plugin "subscription-notifications" % OPTIONAL +-spec plugin_features(host(), binary()) -> [binary()]. +plugin_features(Host, Type) -> + Module = plugin(Host, Type), + case catch Module:features() of {'EXIT', {undef, _}} -> []; Result -> Result - end. + end. +-spec features(binary(), binary()) -> [binary()]. features(Host, <<>>) -> lists:usort(lists:foldl(fun (Plugin, Acc) -> - Acc ++ features(Plugin) - end, - [], plugins(Host))); -features(Host, Node) -> + Acc ++ plugin_features(Host, Plugin) + end, + features(), plugins(Host))); +features(Host, Node) when is_binary(Node) -> Action = fun (#pubsub_node{type = Type}) -> - {result, features(Type)} - end, + {result, plugin_features(Host, Type)} + end, case transaction(Host, Node, Action, sync_dirty) of - {result, Features} -> - lists:usort(features() ++ Features); - _ -> features() + {result, Features} -> lists:usort(features() ++ Features); + _ -> features() end. %% @doc

    node tree plugin call.

    +-spec tree_call(host(), atom(), list()) -> {error, stanza_error() | {virtual, nodeIdx()}} | any(). tree_call({_User, Server, _Resource}, Function, Args) -> tree_call(Server, Function, Args); tree_call(Host, Function, Args) -> - ?DEBUG("tree_call ~p ~p ~p", [Host, Function, Args]), - Module = case catch - ets:lookup(gen_mod:get_module_proc(Host, config), - nodetree) - of - [{nodetree, N}] -> N; - _ -> - jlib:binary_to_atom(<<(?TREE_PREFIX)/binary, - (?STDTREE)/binary>>) - end, - catch apply(Module, Function, Args). + Tree = tree(Host), + ?DEBUG("Tree_call apply(~ts, ~ts, ~p) @ ~ts", [Tree, Function, Args, Host]), + Res = apply(Tree, Function, Args), + Res2 = ejabberd_hooks:run_fold(pubsub_tree_call, Host, Res, [Tree, Function, Args]), + case Res2 of + {error, #stanza_error{}} = Err -> + Err; + {error, {virtual, _}} = Err -> + Err; + {error, _} -> + ErrTxt = ?T("Database failure"), + Lang = ejabberd_option:language(), + {error, xmpp:err_internal_server_error(ErrTxt, Lang)}; + Other -> + Other + end. +-spec tree_action(host(), atom(), list()) -> {error, stanza_error() | {virtual, nodeIdx()}} | any(). tree_action(Host, Function, Args) -> - ?DEBUG("tree_action ~p ~p ~p", [Host, Function, Args]), - Fun = fun () -> tree_call(Host, Function, Args) end, - catch mnesia:sync_dirty(Fun). + ?DEBUG("Tree_action ~p ~p ~p", [Host, Function, Args]), + ServerHost = serverhost(Host), + DBType = mod_pubsub_opt:db_type(ServerHost), + Fun = fun () -> + try tree_call(Host, Function, Args) + catch + Class:Reason:StackTrace when DBType == sql -> + ejabberd_sql:abort({exception, Class, Reason, StackTrace}) + end + end, + Ret = case DBType of + mnesia -> + mnesia:sync_dirty(Fun); + sql -> + ejabberd_sql:sql_bloc(ServerHost, Fun); + _ -> + Fun() + end, + get_tree_action_result(Ret). + +-spec get_tree_action_result(any()) -> {error, stanza_error() | {virtual, nodeIdx()}} | any(). +get_tree_action_result({atomic, Result}) -> + Result; +get_tree_action_result({aborted, {exception, Class, Reason, StackTrace}}) -> + ?ERROR_MSG("Transaction aborted:~n** ~ts", + [misc:format_exception(2, Class, Reason, StackTrace)]), + get_tree_action_result({error, db_failure}); +get_tree_action_result({aborted, Reason}) -> + ?ERROR_MSG("Transaction aborted: ~p~n", [Reason]), + get_tree_action_result({error, db_failure}); +get_tree_action_result({error, #stanza_error{}} = Err) -> + Err; +get_tree_action_result({error, {virtual, _}} = Err) -> + Err; +get_tree_action_result({error, _}) -> + ErrTxt = ?T("Database failure"), + Lang = ejabberd_option:language(), + {error, xmpp:err_internal_server_error(ErrTxt, Lang)}; +get_tree_action_result(Other) -> + %% This is very risky, but tree plugins design is really bad + Other. %% @doc

    node plugin call.

    -node_call(Type, Function, Args) -> - ?DEBUG("node_call ~p ~p ~p", [Type, Function, Args]), - Module = - jlib:binary_to_atom(<<(?PLUGIN_PREFIX)/binary, - Type/binary>>), - case apply(Module, Function, Args) of - {result, Result} -> {result, Result}; - {error, Error} -> {error, Error}; - {'EXIT', {undef, Undefined}} -> - case Type of - ?STDNODE -> {error, {undef, Undefined}}; - _ -> node_call(?STDNODE, Function, Args) - end; - {'EXIT', Reason} -> {error, Reason}; - Result -> - {result, - Result} %% any other return value is forced as result +-spec node_call(host(), binary(), atom(), list()) -> {result, any()} | {error, stanza_error()}. +node_call(Host, Type, Function, Args) -> + ?DEBUG("Node_call ~p ~p ~p", [Type, Function, Args]), + Module = plugin(Host, Type), + case erlang:function_exported(Module, Function, length(Args)) of + true -> + case apply(Module, Function, Args) of + {result, Result} -> + {result, Result}; + #pubsub_state{} = Result -> + {result, Result}; + {error, #stanza_error{}} = Err -> + Err; + {error, _} -> + ErrTxt = ?T("Database failure"), + Lang = ejabberd_option:language(), + {error, xmpp:err_internal_server_error(ErrTxt, Lang)} + end; + false when Type /= ?STDNODE -> + node_call(Host, ?STDNODE, Function, Args); + false -> + %% Let it crash with the stacktrace + apply(Module, Function, Args) end. +-spec node_action(host(), binary(), atom(), list()) -> {result, any()} | {error, stanza_error()}. node_action(Host, Type, Function, Args) -> - ?DEBUG("node_action ~p ~p ~p ~p", - [Host, Type, Function, Args]), - transaction(fun () -> node_call(Type, Function, Args) - end, - sync_dirty). + ?DEBUG("Node_action ~p ~p ~p ~p", [Host, Type, Function, Args]), + transaction(Host, fun() -> node_call(Host, Type, Function, Args) end, sync_dirty). %% @doc

    plugin transaction handling.

    +-spec transaction(host(), binary(), fun((#pubsub_node{}) -> _), transaction | sync_dirty) -> + {result, any()} | {error, stanza_error()}. transaction(Host, Node, Action, Trans) -> - transaction(fun () -> - case tree_call(Host, get_node, [Host, Node]) of - N when is_record(N, pubsub_node) -> - case Action(N) of - {result, Result} -> {result, {N, Result}}; - {atomic, {result, Result}} -> - {result, {N, Result}}; - Other -> Other - end; - Error -> Error - end - end, - Trans). + transaction( + Host, + fun() -> + case tree_call(Host, get_node, [Host, Node]) of + N when is_record(N, pubsub_node) -> + case Action(N) of + {result, Result} -> {result, {N, Result}}; + {atomic, {result, Result}} -> {result, {N, Result}}; + Other -> Other + end; + Error -> + Error + end + end, + Trans). -transaction(Host, Action, Trans) -> - transaction(fun () -> - {result, - lists:foldl(Action, [], - tree_call(Host, get_nodes, [Host]))} - end, - Trans). +-spec transaction(host(), fun(), transaction | sync_dirty) -> + {result, any()} | {error, stanza_error()}. +transaction(Host, Fun, Trans) -> + ServerHost = serverhost(Host), + DBType = mod_pubsub_opt:db_type(ServerHost), + do_transaction(ServerHost, Fun, Trans, DBType). -transaction(Fun, Trans) -> - case catch mnesia:Trans(Fun) of - {result, Result} -> {result, Result}; - {error, Error} -> {error, Error}; - {atomic, {result, Result}} -> {result, Result}; - {atomic, {error, Error}} -> {error, Error}; - {aborted, Reason} -> - ?ERROR_MSG("transaction return internal error: ~p~n", - [{aborted, Reason}]), - {error, ?ERR_INTERNAL_SERVER_ERROR}; - {'EXIT', Reason} -> - ?ERROR_MSG("transaction return internal error: ~p~n", - [{'EXIT', Reason}]), - {error, ?ERR_INTERNAL_SERVER_ERROR}; - Other -> - ?ERROR_MSG("transaction return internal error: ~p~n", - [Other]), - {error, ?ERR_INTERNAL_SERVER_ERROR} - end. +-spec do_transaction(binary(), fun(), transaction | sync_dirty, atom()) -> + {result, any()} | {error, stanza_error()}. +do_transaction(ServerHost, Fun, Trans, DBType) -> + F = fun() -> + try Fun() + catch + Class:Reason:StackTrace when (DBType == mnesia andalso + Trans == transaction) orelse + DBType == sql -> + Ex = {exception, Class, Reason, StackTrace}, + case DBType of + mnesia -> mnesia:abort(Ex); + sql -> ejabberd_sql:abort(Ex) + end + end + end, + Res = case DBType of + mnesia -> + mnesia:Trans(F); + sql -> + SqlFun = case Trans of + transaction -> sql_transaction; + _ -> sql_bloc + end, + ejabberd_sql:SqlFun(ServerHost, F); + _ -> + F() + end, + get_transaction_response(Res). + +-spec get_transaction_response(any()) -> {result, any()} | {error, stanza_error()}. +get_transaction_response({result, _} = Result) -> + Result; +get_transaction_response({error, #stanza_error{}} = Err) -> + Err; +get_transaction_response({atomic, Result}) -> + get_transaction_response(Result); +get_transaction_response({aborted, Err}) -> + get_transaction_response(Err); +get_transaction_response({error, _}) -> + Lang = ejabberd_option:language(), + {error, xmpp:err_internal_server_error(?T("Database failure"), Lang)}; +get_transaction_response({exception, Class, Reason, StackTrace}) -> + ?ERROR_MSG("Transaction aborted:~n** ~ts", + [misc:format_exception(2, Class, Reason, StackTrace)]), + get_transaction_response({error, db_failure}); +get_transaction_response(Err) -> + ?ERROR_MSG("Transaction error: ~p", [Err]), + get_transaction_response({error, db_failure}). %%%% helpers %% Add pubsub-specific error element -extended_error(Error, Ext) -> - extended_error(Error, Ext, - [{<<"xmlns">>, ?NS_PUBSUB_ERRORS}]). +-spec extended_error(stanza_error(), ps_error()) -> stanza_error(). +extended_error(StanzaErr, PubSubErr) -> + StanzaErr#stanza_error{sub_els = [PubSubErr]}. -extended_error(Error, unsupported, Feature) -> -%% Give a uniq identifier - extended_error(Error, <<"unsupported">>, - [{<<"xmlns">>, ?NS_PUBSUB_ERRORS}, - {<<"feature">>, Feature}]); -extended_error(#xmlel{name = Error, attrs = Attrs, - children = SubEls}, - Ext, ExtAttrs) -> - #xmlel{name = Error, attrs = Attrs, - children = - lists:reverse([#xmlel{name = Ext, attrs = ExtAttrs, - children = []} - | SubEls])}. +-spec err_closed_node() -> ps_error(). +err_closed_node() -> + #ps_error{type = 'closed-node'}. --spec(uniqid/0 :: () -> mod_pubsub:itemId()). +-spec err_configuration_required() -> ps_error(). +err_configuration_required() -> + #ps_error{type = 'configuration-required'}. + +-spec err_invalid_jid() -> ps_error(). +err_invalid_jid() -> + #ps_error{type = 'invalid-jid'}. + +-spec err_invalid_options() -> ps_error(). +err_invalid_options() -> + #ps_error{type = 'invalid-options'}. + +-spec err_invalid_payload() -> ps_error(). +err_invalid_payload() -> + #ps_error{type = 'invalid-payload'}. + +-spec err_invalid_subid() -> ps_error(). +err_invalid_subid() -> + #ps_error{type = 'invalid-subid'}. + +-spec err_item_forbidden() -> ps_error(). +err_item_forbidden() -> + #ps_error{type = 'item-forbidden'}. + +-spec err_item_required() -> ps_error(). +err_item_required() -> + #ps_error{type = 'item-required'}. + +-spec err_jid_required() -> ps_error(). +err_jid_required() -> + #ps_error{type = 'jid-required'}. + +-spec err_max_items_exceeded() -> ps_error(). +err_max_items_exceeded() -> + #ps_error{type = 'max-items-exceeded'}. + +-spec err_max_nodes_exceeded() -> ps_error(). +err_max_nodes_exceeded() -> + #ps_error{type = 'max-nodes-exceeded'}. + +-spec err_nodeid_required() -> ps_error(). +err_nodeid_required() -> + #ps_error{type = 'nodeid-required'}. + +-spec err_not_in_roster_group() -> ps_error(). +err_not_in_roster_group() -> + #ps_error{type = 'not-in-roster-group'}. + +-spec err_not_subscribed() -> ps_error(). +err_not_subscribed() -> + #ps_error{type = 'not-subscribed'}. + +-spec err_payload_too_big() -> ps_error(). +err_payload_too_big() -> + #ps_error{type = 'payload-too-big'}. + +-spec err_payload_required() -> ps_error(). +err_payload_required() -> + #ps_error{type = 'payload-required'}. + +-spec err_pending_subscription() -> ps_error(). +err_pending_subscription() -> + #ps_error{type = 'pending-subscription'}. + +-spec err_precondition_not_met() -> ps_error(). +err_precondition_not_met() -> + #ps_error{type = 'precondition-not-met'}. + +-spec err_presence_subscription_required() -> ps_error(). +err_presence_subscription_required() -> + #ps_error{type = 'presence-subscription-required'}. + +-spec err_subid_required() -> ps_error(). +err_subid_required() -> + #ps_error{type = 'subid-required'}. + +-spec err_too_many_subscriptions() -> ps_error(). +err_too_many_subscriptions() -> + #ps_error{type = 'too-many-subscriptions'}. + +-spec err_unsupported(ps_feature()) -> ps_error(). +err_unsupported(Feature) -> + #ps_error{type = 'unsupported', feature = Feature}. + +-spec err_unsupported_access_model() -> ps_error(). +err_unsupported_access_model() -> + #ps_error{type = 'unsupported-access-model'}. + +-spec uniqid() -> mod_pubsub:itemId(). uniqid() -> - {T1, T2, T3} = now(), - iolist_to_binary(io_lib:fwrite("~.16B~.16B~.16B", [T1, T2, T3])). + {T1, T2, T3} = erlang:timestamp(), + (str:format("~.16B~.16B~.16B", [T1, T2, T3])). -nodeAttr(Node) -> [{<<"node">>, Node}]. - -itemAttr([]) -> []; -itemAttr(ItemId) -> [{<<"id">>, ItemId}]. - -itemsEls(Items) -> - lists:map(fun (#pubsub_item{itemid = {ItemId, _}, payload = Payload}) -> - #xmlel{name = <<"item">>, attrs = itemAttr(ItemId), children = Payload} - end, Items). - -add_message_type(#xmlel{name = <<"message">>, attrs = Attrs, children = Els}, - Type) -> - #xmlel{name = <<"message">>, - attrs = [{<<"type">>, Type} | Attrs], children = Els}; -add_message_type(XmlEl, _Type) -> XmlEl. +-spec add_message_type(message(), message_type()) -> message(). +add_message_type(#message{} = Message, Type) -> + Message#message{type = Type}. %% Place of changed at the bottom of the stanza %% cf. http://xmpp.org/extensions/xep-0060.html#publisher-publish-success-subid @@ -5313,234 +4078,463 @@ add_message_type(XmlEl, _Type) -> XmlEl. %% "[SHIM Headers] SHOULD be included after the event notification information %% (i.e., as the last child of the stanza)". -add_shim_headers(Stanza, HeaderEls) -> - add_headers(Stanza, <<"headers">>, ?NS_SHIM, HeaderEls). +-spec add_shim_headers(stanza(), [{binary(), binary()}]) -> stanza(). +add_shim_headers(Stanza, Headers) -> + xmpp:set_subtag(Stanza, #shim{headers = Headers}). -add_extended_headers(Stanza, HeaderEls) -> - add_headers(Stanza, <<"addresses">>, ?NS_ADDRESS, - HeaderEls). +-spec add_extended_headers(stanza(), [address()]) -> stanza(). +add_extended_headers(Stanza, Addrs) -> + xmpp:set_subtag(Stanza, #addresses{list = Addrs}). -add_headers(#xmlel{name = Name, attrs = Attrs, children = Els}, - HeaderName, HeaderNS, HeaderEls) -> - HeaderEl = #xmlel{name = HeaderName, - attrs = [{<<"xmlns">>, HeaderNS}], - children = HeaderEls}, - #xmlel{name = Name, attrs = Attrs, - children = lists:append(Els, [HeaderEl])}. - -%% Removed multiple
    Foo
    elements -%% Didn't seem compliant, but not sure. Confirmation required. -%% cf. http://xmpp.org/extensions/xep-0248.html#notify -%% -%% "If an item is published to a node which is also included by a collection, -%% and an entity is subscribed to that collection with a subscription type of -%% "items" (Is there a way to check that currently ?), then the notifications -%% generated by the service MUST contain additional information. The -%% element contained in the notification message MUST specify the node -%% identifier of the node that generated the notification (not the collection) -%% and the element MUST contain a SHIM header that specifies the node -%% identifier of the collection". - -collection_shim(Node) -> - [#xmlel{name = <<"header">>, - attrs = [{<<"name">>, <<"Collection">>}], - children = [{xmlcdata, Node}]}]. - -subid_shim(SubIDs) -> - [#xmlel{name = <<"header">>, - attrs = [{<<"name">>, <<"SubID">>}], - children = [{xmlcdata, SubID}]} - || SubID <- SubIDs]. +-spec subid_shim([binary()]) -> [{binary(), binary()}]. +subid_shim(SubIds) -> + [{<<"SubId">>, SubId} || SubId <- SubIds]. %% The argument is a list of Jids because this function could be used %% with the 'pubsub#replyto' (type=jid-multi) node configuration. +-spec extended_headers([jid()]) -> [address()]. extended_headers(Jids) -> - [#xmlel{name = <<"address">>, - attrs = [{<<"type">>, <<"replyto">>}, {<<"jid">>, Jid}], - children = []} - || Jid <- Jids]. + [#address{type = replyto, jid = Jid} || Jid <- Jids]. -on_user_offline(_, JID, _) -> - {User, Server, Resource} = jlib:jid_tolower(JID), - case ejabberd_sm:get_user_resources(User, Server) of - [] -> purge_offline({User, Server, Resource}); - _ -> true - end. - -purge_offline({User, Server, _} = LJID) -> - Host = host(element(2, LJID)), +-spec purge_offline(jid()) -> ok. +purge_offline(#jid{lserver = Host} = JID) -> Plugins = plugins(Host), - Result = lists:foldl(fun (Type, {Status, Acc}) -> - case lists:member(<<"retrieve-affiliations">>, - features(Type)) - of - false -> - {{error, - extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, - unsupported, - <<"retrieve-affiliations">>)}, - Acc}; - true -> - {result, Affiliations} = - node_action(Host, Type, - get_entity_affiliations, - [Host, LJID]), - {Status, [Affiliations | Acc]} - end - end, - {ok, []}, Plugins), + Result = lists:foldl( + fun(Type, {Status, Acc}) -> + Features = plugin_features(Host, Type), + case lists:member(<<"retrieve-affiliations">>, plugin_features(Host, Type)) of + false -> + {{error, extended_error(xmpp:err_feature_not_implemented(), + err_unsupported('retrieve-affiliations'))}, + Acc}; + true -> + Items = lists:member(<<"retract-items">>, Features) + andalso lists:member(<<"persistent-items">>, Features), + if Items -> + case node_action(Host, Type, + get_entity_affiliations, [Host, JID]) of + {result, Affs} -> + {Status, [Affs | Acc]}; + {error, _} = Err -> + {Err, Acc} + end; + true -> + {Status, Acc} + end + end + end, {ok, []}, Plugins), case Result of - {ok, Affiliations} -> - lists:foreach(fun ({#pubsub_node{nodeid = {_, NodeId}, - options = Options, type = Type}, - Affiliation}) - when Affiliation == owner orelse - Affiliation == publisher -> - Action = fun (#pubsub_node{type = NType, - id = NodeIdx}) -> - node_call(NType, get_items, - [NodeIdx, - service_jid(Host)]) - end, - case transaction(Host, NodeId, Action, - sync_dirty) - of - {result, {_, []}} -> true; - {result, {_, Items}} -> - Features = features(Type), - case {lists:member(<<"retract-items">>, - Features), - lists:member(<<"persistent-items">>, - Features), - get_option(Options, persist_items), - get_option(Options, purge_offline)} - of - {true, true, true, true} -> - ForceNotify = get_option(Options, - notify_retract), - lists:foreach(fun - (#pubsub_item{itemid - = - {ItemId, - _}, - modification - = - {_, - Modification}}) -> - case - Modification - of - {User, Server, - _} -> - delete_item(Host, - NodeId, - LJID, - ItemId, - ForceNotify); - _ -> true - end; - (_) -> true - end, - Items); - _ -> true - end; - Error -> Error - end; - (_) -> true - end, - lists:usort(lists:flatten(Affiliations))); - {Error, _} -> ?DEBUG("on_user_offline ~p", [Error]) + {ok, Affs} -> + lists:foreach( + fun ({Node, Affiliation}) -> + Options = Node#pubsub_node.options, + Publisher = lists:member(Affiliation, [owner,publisher,publish_only]), + Open = (get_option(Options, publish_model) == open), + Purge = (get_option(Options, purge_offline) + andalso get_option(Options, persist_items)), + if (Publisher or Open) and Purge -> + purge_offline(Host, JID, Node); + true -> + ok + end + end, lists:usort(lists:flatten(Affs))); + _ -> + ok end. +-spec purge_offline(host(), jid(), #pubsub_node{}) -> ok | {error, stanza_error()}. +purge_offline(Host, #jid{luser = User, lserver = Server, lresource = Resource} = JID, Node) -> + Nidx = Node#pubsub_node.id, + Type = Node#pubsub_node.type, + Options = Node#pubsub_node.options, + case node_action(Host, Type, get_items, [Nidx, service_jid(Host), undefined]) of + {result, {[], _}} -> + ok; + {result, {Items, _}} -> + PublishModel = get_option(Options, publish_model), + ForceNotify = get_option(Options, notify_retract), + {_, NodeId} = Node#pubsub_node.nodeid, + lists:foreach( + fun(#pubsub_item{itemid = {ItemId, _}, modification = {_, {U, S, R}}}) + when (U == User) and (S == Server) and (R == Resource) -> + case node_action(Host, Type, delete_item, [Nidx, {U, S, <<>>}, PublishModel, ItemId]) of + {result, {_, broadcast}} -> + broadcast_retract_items(Host, JID, NodeId, Nidx, Type, Options, [ItemId], ForceNotify), + case get_cached_item(Host, Nidx) of + #pubsub_item{itemid = {ItemId, Nidx}} -> unset_cached_item(Host, Nidx); + _ -> ok + end; + _ -> + ok + end; + (_) -> + true + end, Items); + {error, #stanza_error{}} = Err -> + Err; + _ -> + Txt = ?T("Database failure"), + Lang = ejabberd_option:language(), + {error, xmpp:err_internal_server_error(Txt, Lang)} + end. -%% REVIEW: -%% * this code takes NODEID from Itemid2, and forgets about Nodeidx -%% * this code assumes Payload only contains one xmlelement() -%% * PUBLISHER is taken from Creation -export(_Server) -> - [{pubsub_item, - fun(_Host, #pubsub_item{itemid = {Itemid1, Itemid2}, - %nodeidx = _Nodeidx, - creation = {{C1, C2, C3}, Cusr}, - modification = {{M1, M2, M3}, _Musr}, - payload = Payload}) -> - ITEMID = ejabberd_odbc:escape(Itemid1), - NODEID = integer_to_list(Itemid2), - CREATION = ejabberd_odbc:escape( - string:join([string:right(integer_to_list(I),6,$0)||I<-[C1,C2,C3]],":")), - MODIFICATION = ejabberd_odbc:escape( - string:join([string:right(integer_to_list(I),6,$0)||I<-[M1,M2,M3]],":")), - PUBLISHER = ejabberd_odbc:escape(jlib:jid_to_string(Cusr)), - [PayloadEl] = [El || {xmlelement,_,_,_} = El <- Payload], - PAYLOAD = ejabberd_odbc:escape(xml:element_to_binary(PayloadEl)), - ["delete from pubsub_item where itemid='", ITEMID, "';\n" - "insert into pubsub_item(itemid,nodeid,creation,modification,publisher,payload) \n" - " values ('", ITEMID, "', ", NODEID, ", '", CREATION, "', '", - MODIFICATION, "', '", PUBLISHER, "', '", PAYLOAD, "');\n"]; - (_Host, _R) -> - [] - end}, -%% REVIEW: -%% * From the mnesia table, the #pubsub_state.items is not used in ODBC -%% * Right now AFFILIATION is the first letter of Affiliation -%% * Right now SUBSCRIPTIONS expects only one Subscription -%% * Right now SUBSCRIPTIONS letter is the first letter of Subscription - {pubsub_state, - fun(_Host, #pubsub_state{stateid = {Jid, Stateid}, - %nodeidx = Nodeidx, - items = _Items, - affiliation = Affiliation, - subscriptions = Subscriptions}) -> - STATEID = integer_to_list(Stateid), - JID = ejabberd_odbc:escape(jlib:jid_to_string(Jid)), - NODEID = "unknown", %% TODO: integer_to_list(Nodeidx), - AFFILIATION = string:substr(atom_to_list(Affiliation),1,1), - SUBSCRIPTIONS = parse_subscriptions(Subscriptions), - ["delete from pubsub_state where stateid='", STATEID, "';\n" - "insert into pubsub_state(stateid,jid,nodeid,affiliation,subscriptions) \n" - " values (", STATEID, ", '", JID, "', ", NODEID, ", '", - AFFILIATION, "', '", SUBSCRIPTIONS, "');\n"]; - (_Host, _R) -> - [] - end}, +-spec delete_old_items(non_neg_integer()) -> ok | error. +delete_old_items(N) -> + 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}) -> + case node_action(Host, Type, + remove_extra_items, + [Nidx, N]) of + {result, _} -> + ok; + {error, _} -> + error + end + end, Nodes); + _ -> + [error] + end + end, ejabberd_option:hosts()), + case lists:member(error, Results) of + true -> + error; + false -> + ok + end. -%% REVIEW: -%% * Parents is not migrated to PARENTs -%% * Probably some option VALs are not correctly represented in mysql - {pubsub_node, - fun(_Host, #pubsub_node{nodeid = {Hostid, Nodeid}, - id = Id, - parents = _Parents, - type = Type, - owners = Owners, - options = Options}) -> - HOST = case Hostid of - {U,S,R} -> ejabberd_odbc:escape(jlib:jid_to_string({U,S,R})); - _ -> ejabberd_odbc:escape(Hostid) - end, - NODE = ejabberd_odbc:escape(Nodeid), - NODEID = integer_to_list(Id), - PARENT = "", - TYPE = ejabberd_odbc:escape(Type++"_odbc"), - ["delete from pubsub_node where nodeid='", NODEID, "';\n" - "insert into pubsub_node(host,node,nodeid,parent,type) \n" - " values ('", HOST, "', '", NODE, "', ", NODEID, ", '", PARENT, "', '", TYPE, "');\n" - "delete from pubsub_node_option where nodeid='", NODEID, "';\n", - [["insert into pubsub_node_option(nodeid,name,val)\n" - " values (", NODEID, ", '", atom_to_list(Name), "', '", - io_lib:format("~p", [Val]), "');\n"] || {Name,Val} <- Options], - "delete from pubsub_node_owner where nodeid='", NODEID, "';\n", - [["insert into pubsub_node_owner(nodeid,owner)\n" - " values (", NODEID, ", '", jlib:jid_to_string(Usr), "');\n"] || Usr <- Owners],"\n"]; - (_Host, _R) -> - [] - 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. -parse_subscriptions([]) -> - ""; -parse_subscriptions([{State, Item}]) -> - STATE = case State of - subscribed -> "s" - end, - string:join([STATE, Item],":"). +-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(). +mod_opt_type(access_createnode) -> + econf:acl(); +mod_opt_type(name) -> + econf:binary(); +mod_opt_type(ignore_pep_from_offline) -> + econf:bool(); +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) -> + econf:non_neg_int(); +mod_opt_type(force_node_config) -> + econf:map( + econf:glob(), + econf:map( + econf:atom(), + econf:either( + econf:int(), + econf:atom()), + [{return, orddict}, unique])); +mod_opt_type(default_node_config) -> + econf:map( + econf:atom(), + econf:either( + econf:int(), + econf:atom()), + [unique]); +mod_opt_type(nodetree) -> + econf:binary(); +mod_opt_type(pep_mapping) -> + econf:map(econf:binary(), econf:binary()); +mod_opt_type(plugins) -> + econf:list( + econf:enum([<<"flat">>, <<"pep">>]), + [unique]); +mod_opt_type(host) -> + econf:host(); +mod_opt_type(hosts) -> + econf:hosts(); +mod_opt_type(db_type) -> + econf:db_type(?MODULE); +mod_opt_type(vcard) -> + econf:vcard_temp(). + +mod_options(Host) -> + [{access_createnode, all}, + {db_type, ejabberd_config:default_db(Host, ?MODULE)}, + {host, <<"pubsub.", Host/binary>>}, + {hosts, []}, + {name, ?T("Publish-Subscribe")}, + {vcard, undefined}, + {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, []}, + {plugins, [?STDNODE]}, + {max_subscriptions_node, undefined}, + {default_node_config, []}, + {force_node_config, []}]. + +mod_doc() -> + #{desc => + [?T("This module offers a service for " + "https://xmpp.org/extensions/xep-0060.html" + "[XEP-0060: Publish-Subscribe]. The functionality in " + "'mod_pubsub' can be extended using plugins. " + "The plugin that implements PEP " + "(https://xmpp.org/extensions/xep-0163.html" + "[XEP-0163: Personal Eventing via Pubsub]) " + "is enabled in the default ejabberd configuration file, " + "and it requires _`mod_caps`_.")], + opts => + [{access_createnode, + #{value => "AccessName", + desc => + ?T("This option restricts which users are allowed to " + "create pubsub nodes using 'acl' and 'access'. " + "By default any account in the local ejabberd server " + "is allowed to create pubsub nodes. " + "The default value is: 'all'.")}}, + {db_type, + #{value => "mnesia | sql", + desc => + ?T("Same as top-level _`default_db`_ option, but applied to " + "this module only.")}}, + {default_node_config, + #{value => "List of Key:Value", + desc => + ?T("To override default node configuration, regardless " + "of node plugin. Value is a list of key-value " + "definition. Node configuration still uses default " + "configuration defined by node plugin, and overrides " + "any items by value defined in this configurable list.")}}, + {force_node_config, + #{value => "List of Node and the list of its Key:Value", + desc => + ?T("Define the configuration for given nodes. " + "The default value is: '[]'."), + example => + ["force_node_config:", + " ## Avoid buggy clients to make their bookmarks public", + " storage:bookmarks:", + " access_model: whitelist"]}}, + {host, + #{desc => ?T("Deprecated. Use 'hosts' instead.")}}, + {hosts, + #{value => ?T("[Host, ...]"), + desc => + ?T("This option defines the Jabber IDs of the service. " + "If the 'hosts' option is not specified, the only Jabber " + "ID will be the hostname of the virtual host with the " + "prefix \"pubsub.\". The keyword '@HOST@' is replaced " + "with the real virtual host name.")}}, + {ignore_pep_from_offline, + #{value => "false | true", + desc => + ?T("To specify whether or not we should get last " + "published PEP items from users in our roster which " + "are offline when we connect. Value is 'true' or " + "'false'. If not defined, pubsub assumes true so we " + "only get last items of online contacts.")}}, + {last_item_cache, + #{value => "false | true", + desc => + ?T("To specify whether or not pubsub should cache last " + "items. Value is 'true' or 'false'. If not defined, " + "pubsub does not cache last items. On systems with not" + " so many nodes, caching last items speeds up pubsub " + "and allows you to raise the 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: '1000'.")}}, + {max_nodes_discoitems, + #{value => "pos_integer() | infinity", + desc => + ?T("The maximum number of nodes to return in a " + "discoitem response. The default value is: '100'.")}}, + {max_subscriptions_node, + #{value => "MaxSubs", + desc => + ?T("Define the maximum number of subscriptions managed " + "by a node. " + "Default value is no limitation: 'undefined'.")}}, + {name, + #{value => ?T("Name"), + desc => + ?T("The value of the service name. This name is only visible " + "in some clients that support " + "https://xmpp.org/extensions/xep-0030.html" + "[XEP-0030: Service Discovery]. " + "The default is 'vCard User Search'.")}}, + {nodetree, + #{value => "Nodetree", + desc => + [?T("To specify which nodetree to use. If not defined, the " + "default pubsub nodetree is used: 'tree'. Only one " + "nodetree can be used per host, and is shared by all " + "node plugins."), + ?T("- 'tree' nodetree store node configuration and " + "relations on the database. 'flat' nodes are stored " + "without any relationship, and 'hometree' nodes can " + "have child nodes."), + ?T("- 'virtual' nodetree does not store nodes on database. " + "This saves resources on systems with tons of nodes. " + "If using the 'virtual' nodetree, you can only enable " + "those node plugins: '[flat, pep]' or '[flat]'; any " + "other plugins configuration will not work. Also, all " + "nodes will have the default configuration, and this " + "can not be changed. Using 'virtual' nodetree requires " + "to start from a clean database, it will not work if " + "you used the default 'tree' nodetree before.")]}}, + {pep_mapping, + #{value => "List of Key:Value", + desc => + ?T("In this option you can provide a list of key-value to choose " + "defined node plugins on given PEP namespace. " + "The following example will use 'node_tune' instead of " + "'node_pep' for every PEP node with the tune namespace:"), + example => + ["modules:", + " ...", + " mod_pubsub:", + " pep_mapping:", + " http://jabber.org/protocol/tune: tune", + " ..."] + }}, + {plugins, + #{value => "[Plugin, ...]", + desc => [?T("To specify which pubsub node plugins to use. " + "The first one in the list is used by default. " + "If this option is not defined, the default plugins " + "list is: '[flat]'. PubSub clients can define which " + "plugin to use when creating a node: " + "add 'type=\'plugin-name\'' attribute " + "to the 'create' stanza element."), + ?T("- 'flat' plugin handles the default behaviour and " + "follows standard XEP-0060 implementation."), + ?T("- 'pep' plugin adds extension to handle Personal " + "Eventing Protocol (XEP-0163) to the PubSub engine. " + "When enabled, PEP is handled automatically.")]}}, + {vcard, + #{value => ?T("vCard"), + desc => + ?T("A custom vCard of the server that will be displayed by " + "some XMPP clients in Service Discovery. The value of " + "'vCard' is a YAML map constructed from an XML " + "representation of vCard. Since the representation has " + "no attributes, the mapping is straightforward."), + example => + ["# This XML representation of vCard:", + "# ", + "# Conferences", + "# ", + "# ", + "# Elm Street", + "# ", + "# ", + "# ", + "# is translated to:", + "vcard:", + " fn: Conferences", + " adr:", + " -", + " work: true", + " street: Elm Street"]}} + ], + example => + [{?T("Example of configuration that uses flat nodes as default, " + "and allows use of flat, hometree and pep nodes:"), + ["modules:", + " mod_pubsub:", + " access_createnode: pubsub_createnode", + " max_subscriptions_node: 100", + " default_node_config:", + " notification_type: normal", + " notify_retract: false", + " max_items: 4", + " plugins:", + " - flat", + " - pep"]}, + {?T("Using relational database requires using mod_pubsub with " + "db_type 'sql'. Only flat, hometree and pep plugins supports " + "SQL. The following example shows previous configuration " + "with SQL usage:"), + ["modules:", + " mod_pubsub:", + " db_type: sql", + " access_createnode: pubsub_createnode", + " ignore_pep_from_offline: true", + " last_item_cache: false", + " plugins:", + " - flat", + " - pep"]} + ]}. diff --git a/src/mod_pubsub_mnesia.erl b/src/mod_pubsub_mnesia.erl new file mode 100644 index 000000000..a1dbc2ff3 --- /dev/null +++ b/src/mod_pubsub_mnesia.erl @@ -0,0 +1,32 @@ +%%%---------------------------------------------------------------------- +%%% ejabberd, Copyright (C) 2002-2025 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_pubsub_mnesia). + +%% API +-export([init/3]). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(Host, ServerHost, Opts) -> + pubsub_index:init(Host, ServerHost, Opts). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== diff --git a/src/mod_pubsub_odbc.erl b/src/mod_pubsub_odbc.erl deleted file mode 100644 index 3b8ae682a..000000000 --- a/src/mod_pubsub_odbc.erl +++ /dev/null @@ -1,5119 +0,0 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. -%%% -%%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. -%%% -%%% -%%% The Initial Developer of the Original Code is ProcessOne. -%%% Portions created by ProcessOne are Copyright 2006-2015, ProcessOne -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2015, ProcessOne. -%%% -%%% @copyright 2006-2015 ProcessOne -%%% @author Christophe Romain -%%% [http://www.process-one.net/] -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== - -%%% @doc The module {@module} is the core of the PubSub -%%% extension. It relies on PubSub plugins for a large part of its functions. -%%% -%%% @headerfile "pubsub.hrl" -%%% -%%% @reference See XEP-0060: Pubsub for -%%% the latest version of the PubSub specification. -%%% This module uses version 1.12 of the specification as a base. -%%% Most of the specification is implemented. -%%% Functions concerning configuration should be rewritten. -%%% -%%% Support for subscription-options and multi-subscribe features was -%%% added by Brian Cully (bjc AT kublai.com). Subscriptions and options are -%%% stored in the pubsub_subscription table, with a link to them provided -%%% by the subscriptions field of pubsub_state. For information on -%%% subscription-options and mulit-subscribe see XEP-0060 sections 6.1.6, -%%% 6.2.3.1, 6.2.3.5, and 6.3. For information on subscription leases see -%%% XEP-0060 section 12.18. - --module(mod_pubsub_odbc). - --author('christophe.romain@process-one.net'). - --version('1.13-0'). - --behaviour(gen_server). - --behaviour(gen_mod). - --include("ejabberd.hrl"). --include("logger.hrl"). - --include("adhoc.hrl"). - --include("jlib.hrl"). - --include("pubsub.hrl"). - --define(STDTREE, <<"tree">>). - --define(STDNODE, <<"flat">>). - --define(PEPNODE, <<"pep">>). - -%% exports for hooks --export([presence_probe/3, caps_update/3, - in_subscription/6, out_subscription/4, - on_user_offline/3, remove_user/2, - disco_local_identity/5, disco_local_features/5, - disco_local_items/5, disco_sm_identity/5, - disco_sm_features/5, disco_sm_items/5, - drop_pep_error/4]). - -%% exported iq handlers --export([iq_sm/3]). - -%% exports for console debug manual use --export([create_node/5, - delete_node/3, - subscribe_node/5, - unsubscribe_node/5, - publish_item/6, - delete_item/4, - send_items/6, - get_items/2, - get_item/3, - get_cached_item/2, - broadcast_stanza/9, - get_configure/5, - set_configure/5, - tree_action/3, - node_action/4 - ]). - -%% general helpers for plugins --export([subscription_to_string/1, affiliation_to_string/1, - string_to_subscription/1, string_to_affiliation/1, - extended_error/2, extended_error/3, - escape/1]). - -%% API and gen_server callbacks --export([start_link/2, start/2, stop/1, init/1, - handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). - -%% calls for parallel sending of last items --export([send_loop/1]). - --define(PROCNAME, ejabberd_mod_pubsub_odbc). - --define(LOOPNAME, ejabberd_mod_pubsub_loop). - -%%==================================================================== -%% API -%%==================================================================== -%%-------------------------------------------------------------------- -%% Function: start_link() -> {ok,Pid} | ignore | {error,Error} -%% Description: Starts the server -%%-------------------------------------------------------------------- --define(PLUGIN_PREFIX, <<"node_">>). - --define(TREE_PREFIX, <<"nodetree_">>). - --define(ODBC_SUFFIX, <<"_odbc">>). - -% --export_type([ - host/0, - hostPubsub/0, - hostPEP/0, - %% - nodeIdx/0, - nodeId/0, - itemId/0, - subId/0, - payload/0, - %% - nodeOption/0, - nodeOptions/0, - subOption/0, - subOptions/0, - %% - affiliation/0, - subscription/0, - accessModel/0, - publishModel/0 -]). - -%% -type payload() defined here because the -type xmlel() is not accessible -%% from pubsub.hrl --type(payload() :: [] | [xmlel(),...]). - --export_type([ - pubsubNode/0, - pubsubState/0, - pubsubItem/0, - pubsubSubscription/0, - pubsubLastItem/0 -]). - --type(pubsubNode() :: - #pubsub_node{ - nodeid :: {Host::mod_pubsub:host(), NodeId::mod_pubsub:nodeId()}, - id :: mod_pubsub:nodeIdx(), - parents :: [Parent_NodeId::mod_pubsub:nodeId()], - type :: binary(), - owners :: [Owner::ljid(),...], - options :: mod_pubsub:nodeOptions() - } -). - --type(pubsubState() :: - #pubsub_state{ - stateid :: {Entity::ljid(), NodeIdx::mod_pubsub:nodeIdx()}, - items :: [ItemId::mod_pubsub:itemId()], - affiliation :: mod_pubsub:affiliation(), - subscriptions :: [{mod_pubsub:subscription(), mod_pubsub:subId()}] - } -). - --type(pubsubItem() :: - #pubsub_item{ - itemid :: {mod_pubsub:itemId(), mod_pubsub:nodeIdx()}, - creation :: {erlang:timestamp(), ljid()}, - modification :: {erlang:timestamp(), ljid()}, - payload :: mod_pubsub:payload() - } -). - --type(pubsubSubscription() :: - #pubsub_subscription{ - subid :: mod_pubsub:subId(), - options :: [] | mod_pubsub:subOptions() - } -). - --type(pubsubLastItem() :: - #pubsub_last_item{ - nodeid :: mod_pubsub:nodeIdx(), - itemid :: mod_pubsub:itemId(), - creation :: {erlang:timestamp(), ljid()}, - payload :: mod_pubsub:payload() - } -). - --record(state, -{ - server_host, - host, - access, - pep_mapping = [], - ignore_pep_from_offline = true, - last_item_cache = false, - max_items_node = ?MAXITEMS, - nodetree = ?STDTREE, - plugins = [?STDNODE] -}). - --type(state() :: - #state{ - server_host :: binary(), - host :: mod_pubsub:hostPubsub(), - access :: atom(), - pep_mapping :: [{binary(), binary()}], - ignore_pep_from_offline :: boolean(), - last_item_cache :: boolean(), - max_items_node :: non_neg_integer(), - nodetree :: binary(), - plugins :: [binary(),...] - } - -). - - -start_link(Host, Opts) -> - Proc = gen_mod:get_module_proc(Host, ?PROCNAME), - gen_server:start_link({local, Proc}, ?MODULE, - [Host, Opts], []). - -start(Host, Opts) -> - Proc = gen_mod:get_module_proc(Host, ?PROCNAME), - ChildSpec = {Proc, {?MODULE, start_link, [Host, Opts]}, - transient, 1000, worker, [?MODULE]}, - supervisor:start_child(ejabberd_sup, ChildSpec). - -stop(Host) -> - Proc = gen_mod:get_module_proc(Host, ?PROCNAME), - gen_server:call(Proc, stop), - supervisor:delete_child(ejabberd_sup, Proc). - -%%==================================================================== -%% gen_server callbacks -%%==================================================================== - -%%-------------------------------------------------------------------- -%% Function: init(Args) -> {ok, State} | -%% {ok, State, Timeout} | -%% ignore | -%% {stop, Reason} -%% Description: Initiates the server -%%-------------------------------------------------------------------- --spec(init/1 :: -( - _:: _) - -> {ok, state()} -). - -init([ServerHost, Opts]) -> - ?DEBUG("pubsub init ~p ~p", [ServerHost, Opts]), - Host = gen_mod:get_opt_host(ServerHost, Opts, <<"pubsub.@HOST@">>), - Access = gen_mod:get_opt(access_createnode, Opts, - fun(A) when is_atom(A) -> A end, all), - PepOffline = gen_mod:get_opt(ignore_pep_from_offline, Opts, - fun(A) when is_boolean(A) -> A end, true), - IQDisc = gen_mod:get_opt(iqdisc, Opts, - fun(A) when is_atom(A) -> A end, one_queue), - LastItemCache = gen_mod:get_opt(last_item_cache, Opts, - fun(A) when is_boolean(A) -> A end, false), - MaxItemsNode = gen_mod:get_opt(max_items_node, Opts, - fun(A) when is_integer(A) andalso A >= 0 -> A end, ?MAXITEMS), - pubsub_index:init(Host, ServerHost, Opts), - ets:new(gen_mod:get_module_proc(Host, config), - [set, named_table]), - ets:new(gen_mod:get_module_proc(ServerHost, config), - [set, named_table]), - {Plugins, NodeTree, PepMapping} = init_plugins(Host, - ServerHost, Opts), - mnesia:create_table(pubsub_last_item, - [{ram_copies, [node()]}, - {attributes, record_info(fields, pubsub_last_item)}]), - mod_disco:register_feature(ServerHost, ?NS_PUBSUB), - ets:insert(gen_mod:get_module_proc(Host, config), - {nodetree, NodeTree}), - ets:insert(gen_mod:get_module_proc(Host, config), - {plugins, Plugins}), - ets:insert(gen_mod:get_module_proc(Host, config), - {last_item_cache, LastItemCache}), - ets:insert(gen_mod:get_module_proc(Host, config), - {max_items_node, MaxItemsNode}), - ets:insert(gen_mod:get_module_proc(ServerHost, config), - {nodetree, NodeTree}), - ets:insert(gen_mod:get_module_proc(ServerHost, config), - {plugins, Plugins}), - ets:insert(gen_mod:get_module_proc(ServerHost, config), - {last_item_cache, LastItemCache}), - ets:insert(gen_mod:get_module_proc(ServerHost, config), - {max_items_node, MaxItemsNode}), - ets:insert(gen_mod:get_module_proc(ServerHost, config), - {pep_mapping, PepMapping}), - ets:insert(gen_mod:get_module_proc(ServerHost, config), - {ignore_pep_from_offline, PepOffline}), - ets:insert(gen_mod:get_module_proc(ServerHost, config), - {host, Host}), - ejabberd_hooks:add(sm_remove_connection_hook, - ServerHost, ?MODULE, on_user_offline, 75), - ejabberd_hooks:add(disco_local_identity, ServerHost, - ?MODULE, disco_local_identity, 75), - ejabberd_hooks:add(disco_local_features, ServerHost, - ?MODULE, disco_local_features, 75), - ejabberd_hooks:add(disco_local_items, ServerHost, - ?MODULE, disco_local_items, 75), - ejabberd_hooks:add(presence_probe_hook, ServerHost, - ?MODULE, presence_probe, 80), - ejabberd_hooks:add(roster_in_subscription, ServerHost, - ?MODULE, in_subscription, 50), - ejabberd_hooks:add(roster_out_subscription, ServerHost, - ?MODULE, out_subscription, 50), - ejabberd_hooks:add(remove_user, ServerHost, ?MODULE, - remove_user, 50), - ejabberd_hooks:add(anonymous_purge_hook, ServerHost, - ?MODULE, remove_user, 50), - case lists:member(?PEPNODE, Plugins) of - true -> - ejabberd_hooks:add(caps_update, ServerHost, ?MODULE, - caps_update, 80), - ejabberd_hooks:add(disco_sm_identity, ServerHost, - ?MODULE, disco_sm_identity, 75), - ejabberd_hooks:add(disco_sm_features, ServerHost, - ?MODULE, disco_sm_features, 75), - ejabberd_hooks:add(disco_sm_items, ServerHost, ?MODULE, - disco_sm_items, 75), - ejabberd_hooks:add(c2s_filter_packet_in, ServerHost, ?MODULE, - drop_pep_error, 75), - gen_iq_handler:add_iq_handler(ejabberd_sm, ServerHost, - ?NS_PUBSUB, ?MODULE, iq_sm, IQDisc), - gen_iq_handler:add_iq_handler(ejabberd_sm, ServerHost, - ?NS_PUBSUB_OWNER, ?MODULE, iq_sm, - IQDisc); - false -> ok - end, - ejabberd_router:register_route(Host), - put(server_host, ServerHost), - init_nodes(Host, ServerHost, NodeTree, Plugins), - State = #state{host = Host, server_host = ServerHost, - access = Access, pep_mapping = PepMapping, - ignore_pep_from_offline = PepOffline, - last_item_cache = LastItemCache, - max_items_node = MaxItemsNode, nodetree = NodeTree, - plugins = Plugins}, - init_send_loop(ServerHost, State), - {ok, State}. - -init_send_loop(ServerHost, State) -> - Proc = gen_mod:get_module_proc(ServerHost, ?LOOPNAME), - SendLoop = spawn(?MODULE, send_loop, [State]), - register(Proc, SendLoop), - SendLoop. - -%% @spec (Host, ServerHost, Opts) -> Plugins -%% Host = mod_pubsub:host() Opts = [{Key,Value}] -%% ServerHost = host() -%% Key = atom() -%% Value = term() -%% Plugins = [Plugin::string()] -%% @doc Call the init/1 function for each plugin declared in the config file. -%% The default plugin module is implicit. -%%

    The Erlang code for the plugin is located in a module called -%% node_plugin. The 'node_' prefix is mandatory.

    -%%

    The modules are initialized in alphetical order and the list is checked -%% and sorted to ensure that each module is initialized only once.

    -%%

    See {@link node_hometree:init/1} for an example implementation.

    -init_plugins(Host, ServerHost, Opts) -> - TreePlugin = - jlib:binary_to_atom(<<(?TREE_PREFIX)/binary, - (gen_mod:get_opt(nodetree, Opts, fun(A) when is_binary(A) -> A end, - ?STDTREE))/binary, - (?ODBC_SUFFIX)/binary>>), - ?DEBUG("** tree plugin is ~p", [TreePlugin]), - TreePlugin:init(Host, ServerHost, Opts), - Plugins = gen_mod:get_opt(plugins, Opts, - fun(A) when is_list(A) -> A end, [?STDNODE]), - PepMapping = gen_mod:get_opt(pep_mapping, Opts, - fun(A) when is_list(A) -> A end, []), - ?DEBUG("** PEP Mapping : ~p~n", [PepMapping]), - PluginsOK = lists:foldl(fun (Name, Acc) -> - Plugin = - jlib:binary_to_atom(<<(?PLUGIN_PREFIX)/binary, - Name/binary, - (?ODBC_SUFFIX)/binary>>), - case catch apply(Plugin, init, - [Host, ServerHost, Opts]) - of - {'EXIT', _Error} -> Acc; - _ -> - ?DEBUG("** init ~s plugin", [Name]), - [Name | Acc] - end - end, - [], Plugins), - {lists:reverse(PluginsOK), TreePlugin, PepMapping}. - -terminate_plugins(Host, ServerHost, Plugins, - TreePlugin) -> - lists:foreach(fun (Name) -> - ?DEBUG("** terminate ~s plugin", [Name]), - Plugin = - jlib:binary_to_atom(<<(?PLUGIN_PREFIX)/binary, - Name/binary, - (?ODBC_SUFFIX)/binary>>), - Plugin:terminate(Host, ServerHost) - end, - Plugins), - TreePlugin:terminate(Host, ServerHost), - ok. - -init_nodes(Host, ServerHost, _NodeTree, Plugins) -> - case lists:member(<<"hometree_odbc">>, Plugins) of - true -> - create_node(Host, ServerHost, <<"/home">>, service_jid(Host), <<"hometree_odbc">>), - create_node(Host, ServerHost, <<"/home/", ServerHost/binary>>, service_jid(Host), - <<"hometree_odbc">>); - false -> ok - end. - -send_loop(State) -> - receive - {presence, JID, Pid} -> - Host = State#state.host, - ServerHost = State#state.server_host, - LJID = jlib:jid_tolower(JID), - BJID = jlib:jid_remove_resource(LJID), - lists:foreach(fun (PType) -> - Subscriptions = case catch node_action(Host, - PType, - get_entity_subscriptions_for_send_last, - [Host, JID]) of - {result, S} -> S; - _ -> [] - end, - lists:foreach(fun ({Node, subscribed, _, - SubJID}) -> - if (SubJID == LJID) or - (SubJID == BJID) -> - #pubsub_node{nodeid - = - {H, - N}, - type = - Type, - id = - NodeId} = - Node, - send_items(H, - N, - NodeId, - Type, - LJID, - last); - true -> - % resource not concerned about that subscription - ok - end; - (_) -> ok - end, - lists:usort(Subscriptions)) - end, - State#state.plugins), - if not State#state.ignore_pep_from_offline -> - {User, Server, Resource} = jlib:jid_tolower(JID), - case catch ejabberd_c2s:get_subscribed(Pid) of - Contacts when is_list(Contacts) -> - lists:foreach(fun ({U, S, R}) -> - case S of - ServerHost -> %% local contacts - case user_resources(U, S) of - [] -> %% offline - PeerJID = - jlib:make_jid(U, S, - R), - self() ! - {presence, User, - Server, [Resource], - PeerJID}; - _ -> %% online - % this is already handled by presence probe - ok - end; - _ -> %% remote contacts - % we can not do anything in any cases - ok - end - end, - Contacts); - _ -> ok - end; - true -> ok - end, - send_loop(State); - {presence, User, Server, Resources, JID} -> - spawn(fun () -> - Host = State#state.host, - Owner = jlib:jid_remove_resource(jlib:jid_tolower(JID)), - lists:foreach(fun (#pubsub_node{nodeid = {_, Node}, - type = Type, - id = NodeId, - options = Options}) -> - case get_option(Options, - send_last_published_item) - of - on_sub_and_presence -> - lists:foreach(fun - (Resource) -> - LJID = - {User, - Server, - Resource}, - Subscribed = - case - get_option(Options, - access_model) - of - open -> - true; - presence -> - true; - whitelist -> - false; % subscribers are added manually - authorize -> - false; % likewise - roster -> - Grps = - get_option(Options, - roster_groups_allowed, - []), - {OU, - OS, - _} = - Owner, - element(2, - get_roster_info(OU, - OS, - LJID, - Grps)) - end, - if - Subscribed -> - send_items(Owner, - Node, - NodeId, - Type, - LJID, - last); - true -> - ok - end - end, - Resources); - _ -> ok - end - end, - tree_action(Host, get_nodes, - [Owner, JID])) - end), - send_loop(State); - stop -> ok - end. - -%% ------- -%% disco hooks handling functions -%% - --spec(disco_local_identity/5 :: -( - Acc :: [xmlel()], - _From :: jid(), - To :: jid(), - NodeId :: <<>> | mod_pubsub:nodeId(), - Lang :: binary()) - -> [xmlel()] -). -disco_local_identity(Acc, _From, To, <<>>, _Lang) -> - case lists:member(?PEPNODE, plugins(To#jid.lserver)) of - true -> - [#xmlel{name = <<"identity">>, - attrs = - [{<<"category">>, <<"pubsub">>}, - {<<"type">>, <<"pep">>}], - children = []} - | Acc]; - false -> Acc - end; -disco_local_identity(Acc, _From, _To, _Node, _Lang) -> - Acc. - --spec(disco_local_features/5 :: -( - Acc :: [xmlel()], - _From :: jid(), - To :: jid(), - NodeId :: <<>> | mod_pubsub:nodeId(), - Lang :: binary()) - -> [binary(),...] -). -disco_local_features(Acc, _From, To, <<>>, _Lang) -> - Host = To#jid.lserver, - Feats = case Acc of - {result, I} -> I; - _ -> [] - end, - {result, - Feats ++ - lists:map(fun (Feature) -> - <<(?NS_PUBSUB)/binary, "#", Feature/binary>> - end, - features(Host, <<>>))}; -disco_local_features(Acc, _From, _To, _Node, _Lang) -> - Acc. - -disco_local_items(Acc, _From, _To, <<>>, _Lang) -> Acc; -disco_local_items(Acc, _From, _To, _Node, _Lang) -> Acc. - -%disco_sm_identity(Acc, From, To, Node, Lang) -% when is_binary(Node) -> -% disco_sm_identity(Acc, From, To, iolist_to_binary(Node), -% Lang); --spec(disco_sm_identity/5 :: -( - Acc :: empty | [xmlel()], - From :: jid(), - To :: jid(), - Node :: mod_pubsub:nodeId(), - Lang :: binary()) - -> [xmlel()] -). -disco_sm_identity(empty, From, To, Node, Lang) -> - disco_sm_identity([], From, To, Node, Lang); -disco_sm_identity(Acc, From, To, Node, _Lang) -> - disco_identity(jlib:jid_tolower(jlib:jid_remove_resource(To)), Node, From) - ++ Acc. - -disco_identity(_Host, <<>>, _From) -> - [#xmlel{name = <<"identity">>, - attrs = - [{<<"category">>, <<"pubsub">>}, - {<<"type">>, <<"pep">>}], - children = []}]; -disco_identity(Host, Node, From) -> - Action = fun (#pubsub_node{id = Idx, type = Type, - options = Options}) -> - Owners = node_owners_call(Type, Idx), - case get_allowed_items_call(Host, Idx, From, Type, Options, Owners) of - {result, _} -> - {result, - [#xmlel{name = <<"identity">>, - attrs = - [{<<"category">>, <<"pubsub">>}, - {<<"type">>, <<"pep">>}], - children = []}, - #xmlel{name = <<"identity">>, - attrs = - [{<<"category">>, <<"pubsub">>}, - {<<"type">>, <<"leaf">>} - | case get_option(Options, title) of - false -> []; - [Title] -> [{<<"name">>, Title}] - end], - children = []}]}; - _ -> {result, []} - end - end, - case transaction(Host, Node, Action, sync_dirty) of - {result, {_, Result}} -> Result; - _ -> [] - end. - --spec(disco_sm_features/5 :: -( - Acc :: empty | {result, Features::[Feature::binary()]}, - From :: jid(), - To :: jid(), - Node :: mod_pubsub:nodeId(), - Lang :: binary()) - -> {result, Features::[Feature::binary()]} -). -%disco_sm_features(Acc, From, To, Node, Lang) -% when is_binary(Node) -> -% disco_sm_features(Acc, From, To, iolist_to_binary(Node), -% Lang); -disco_sm_features(empty, From, To, Node, Lang) -> - disco_sm_features({result, []}, From, To, Node, Lang); -disco_sm_features({result, OtherFeatures} = _Acc, From, To, Node, _Lang) -> - {result, - OtherFeatures ++ - disco_features(jlib:jid_tolower(jlib:jid_remove_resource(To)), Node, From)}; -disco_sm_features(Acc, _From, _To, _Node, _Lang) -> Acc. - -disco_features(_Host, <<>>, _From) -> - [?NS_PUBSUB | [<<(?NS_PUBSUB)/binary, "#", Feature/binary>> - || Feature <- features(<<"pep">>)]]; -disco_features(Host, Node, From) -> - Action = fun (#pubsub_node{id = Idx, type = Type, - options = Options}) -> - Owners = node_owners_call(Type, Idx), - case get_allowed_items_call(Host, Idx, From, Type, Options, Owners) of - {result, _} -> - {result, - [?NS_PUBSUB | [<<(?NS_PUBSUB)/binary, "#", - Feature/binary>> - || Feature <- features(<<"pep">>)]]}; - _ -> {result, []} - end - end, - case transaction(Host, Node, Action, sync_dirty) of - {result, {_, Result}} -> Result; - _ -> [] - end. - --spec(disco_sm_items/5 :: -( - Acc :: empty | {result, [xmlel()]}, - From :: jid(), - To :: jid(), - Node :: mod_pubsub:nodeId(), - Lang :: binary()) - -> {result, [xmlel()]} -). -%disco_sm_items(Acc, From, To, Node, Lang) -% when is_binary(Node) -> -% disco_sm_items(Acc, From, To, iolist_to_binary(Node), -% Lang); -disco_sm_items(empty, From, To, Node, Lang) -> - disco_sm_items({result, []}, From, To, Node, Lang); -disco_sm_items({result, OtherItems}, From, To, Node, _Lang) -> - {result, - lists:usort(OtherItems ++ - disco_items(jlib:jid_tolower(jlib:jid_remove_resource(To)), Node, From))}; -disco_sm_items(Acc, _From, _To, _Node, _Lang) -> Acc. - --spec(disco_items/3 :: -( - Host :: mod_pubsub:host(), - Node :: mod_pubsub:nodeId(), - From :: jid()) - -> [xmlel()] -). -disco_items(Host, <<>>, From) -> - Action = fun (#pubsub_node{nodeid = {_, NodeID}, - options = Options, type = Type, id = Idx}, - Acc) -> - Owners = node_owners_call(Type, Idx), - case get_allowed_items_call(Host, Idx, From, Type, Options, Owners) of - {result, _} -> - [#xmlel{name = <<"item">>, - attrs = - [{<<"node">>, (NodeID)}, - {<<"jid">>, - case Host of - {_, _, _} -> - jlib:jid_to_string(Host); - _Host -> Host - end} - | case get_option(Options, title) of - false -> []; - [Title] -> [{<<"name">>, Title}] - end], - children = []} - | Acc]; - _ -> Acc - end - end, - case transaction_on_nodes(Host, Action, sync_dirty) of - {result, Items} -> Items; - _ -> [] - end; -disco_items(Host, Node, From) -> - Action = fun (#pubsub_node{id = Idx, type = Type, - options = Options}) -> - Owners = node_owners_call(Type, Idx), - case get_allowed_items_call(Host, Idx, From, Type, - Options, Owners) - of - {result, Items} -> - {result, - [#xmlel{name = <<"item">>, - attrs = - [{<<"jid">>, - case Host of - {_, _, _} -> - jlib:jid_to_string(Host); - _Host -> Host - end}, - {<<"name">>, ItemID}], - children = []} - || #pubsub_item{itemid = {ItemID, _}} <- Items]}; - _ -> {result, []} - end - end, - case transaction(Host, Node, Action, sync_dirty) of - {result, {_, Result}} -> Result; - _ -> [] - end. - -%% ------- -%% presence hooks handling functions -%% - -caps_update(#jid{luser = U, lserver = S, lresource = R}, #jid{lserver = Host} = JID, _Features) - when Host =/= S -> - presence(Host, {presence, U, S, [R], JID}); -caps_update(_From, _To, _Feature) -> - ok. - -presence_probe(#jid{luser = U, lserver = S, lresource = R} = JID, JID, Pid) -> - presence(S, {presence, JID, Pid}), - presence(S, {presence, U, S, [R], JID}); -presence_probe(#jid{luser = U, lserver = S}, #jid{luser = U, lserver = S}, _Pid) -> - %% ignore presence_probe from my other ressources - %% to not get duplicated last items - ok; -presence_probe(#jid{luser = U, lserver = S, lresource = R}, #jid{lserver = S} = JID, _Pid) -> - presence(S, {presence, U, S, [R], JID}); -presence_probe(_Host, _JID, _Pid) -> - %% ignore presence_probe from remote contacts, - %% those are handled via caps_update - ok. - -presence(ServerHost, Presence) -> - SendLoop = case - whereis(gen_mod:get_module_proc(ServerHost, ?LOOPNAME)) - of - undefined -> - Host = host(ServerHost), - Plugins = plugins(Host), - PepOffline = case catch - ets:lookup(gen_mod:get_module_proc(ServerHost, - config), - ignore_pep_from_offline) - of - [{ignore_pep_from_offline, PO}] -> PO; - _ -> true - end, - State = #state{host = Host, server_host = ServerHost, - ignore_pep_from_offline = PepOffline, - plugins = Plugins}, - init_send_loop(ServerHost, State); - Pid -> Pid - end, - SendLoop ! Presence. - -%% ------- -%% subscription hooks handling functions -%% - -out_subscription(User, Server, JID, subscribed) -> - Owner = jlib:make_jid(User, Server, <<"">>), - {PUser, PServer, PResource} = jlib:jid_tolower(JID), - PResources = case PResource of - <<>> -> user_resources(PUser, PServer); - _ -> [PResource] - end, - presence(Server, - {presence, PUser, PServer, PResources, Owner}), - true; -out_subscription(_, _, _, _) -> true. - -in_subscription(_, User, Server, Owner, unsubscribed, - _) -> - unsubscribe_user(jlib:make_jid(User, Server, <<"">>), - Owner), - true; -in_subscription(_, _, _, _, _, _) -> true. - -unsubscribe_user(Entity, Owner) -> - BJID = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), - Host = host(element(2, BJID)), - spawn(fun () -> - lists:foreach(fun (PType) -> - case node_action(Host, PType, - get_entity_subscriptions, - [Host, Entity]) of - {result, Subscriptions} -> - lists:foreach(fun ({#pubsub_node{options - = - Options, - id = - NodeId}, - subscribed, _, - JID}) -> - case - get_option(Options, - access_model) - of - presence -> - case - lists:member(BJID, - node_owners(Host, PType, NodeId)) - of - true -> - node_action(Host, - PType, - unsubscribe_node, - [NodeId, - Entity, - JID, - all]); - false -> - {result, - ok} - end; - _ -> - {result, ok} - end; - (_) -> ok - end, - Subscriptions); - Error -> - ?DEBUG("Error at node_action: ~p", [Error]) - end - end, - plugins(Host)) - end). - -%% ------- -%% packet receive hook handling function -%% - -drop_pep_error(#xmlel{name = <<"message">>, attrs = Attrs} = Packet, _JID, From, - #jid{lresource = <<"">>} = To) -> - case xml:get_attr_s(<<"type">>, Attrs) of - <<"error">> -> - case xml:get_subtag(Packet, <<"event">>) of - #xmlel{attrs = EventAttrs} -> - case xml:get_attr_s(<<"xmlns">>, EventAttrs) of - ?NS_PUBSUB_EVENT -> - ?DEBUG("Dropping PEP error message from ~s to ~s", - [jlib:jid_to_string(From), - jlib:jid_to_string(To)]), - drop; - _ -> - Packet - end; - false -> - Packet - end; - _ -> - Packet - end; -drop_pep_error(Acc, _JID, _From, _To) -> Acc. - -%% ------- -%% user remove hook handling function -%% - -remove_user(User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - Entity = jlib:make_jid(LUser, LServer, <<"">>), - Host = host(LServer), - HomeTreeBase = <<"/home/", LServer/binary, "/", LUser/binary>>, - spawn(fun () -> - lists:foreach(fun (PType) -> - {result, Subscriptions} = - node_action(Host, PType, - get_entity_subscriptions, - [Host, Entity]), - lists:foreach(fun ({#pubsub_node{id = - NodeId}, - _, _, JID}) -> - node_action(Host, - PType, - unsubscribe_node, - [NodeId, - Entity, - JID, - all]); - (_) -> ok - end, - Subscriptions), - {result, Affiliations} = - node_action(Host, PType, - get_entity_affiliations, - [Host, Entity]), - lists:foreach(fun ({#pubsub_node{nodeid - = - {H, - N}, - parents - = - []}, - owner}) -> - delete_node(H, N, - Entity); - ({#pubsub_node{nodeid - = - {H, - N}, - type = - <<"hometree">>}, - owner}) - when N == - HomeTreeBase -> - delete_node(H, N, - Entity); - ({#pubsub_node{id = - NodeId}, - publisher}) -> - node_action(Host, - PType, - set_affiliation, - [NodeId, - Entity, - none]); - (_) -> ok - end, - Affiliations) - end, - plugins(Host)) - end). - -handle_call(server_host, _From, State) -> - {reply, State#state.server_host, State}; -handle_call(plugins, _From, State) -> - {reply, State#state.plugins, State}; -handle_call(pep_mapping, _From, State) -> - {reply, State#state.pep_mapping, State}; -handle_call(nodetree, _From, State) -> - {reply, State#state.nodetree, State}; -handle_call(stop, _From, State) -> - {stop, normal, ok, State}. - -%%-------------------------------------------------------------------- -%% Function: handle_cast(Msg, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} -%% Description: Handling cast messages -%%-------------------------------------------------------------------- -%% @private -handle_cast(_Msg, State) -> {noreply, State}. - --spec(handle_info/2 :: -( - _ :: {route, From::jid(), To::jid(), Packet::xmlel()}, - State :: state()) - -> {noreply, state()} -). - -%%-------------------------------------------------------------------- -%% Function: handle_info(Info, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} -%% Description: Handling all non call/cast messages -%%-------------------------------------------------------------------- -%% @private -handle_info({route, From, To, Packet}, - #state{server_host = ServerHost, access = Access, - plugins = Plugins} = - State) -> - case catch do_route(ServerHost, Access, Plugins, - To#jid.lserver, From, To, Packet) - of - {'EXIT', Reason} -> ?ERROR_MSG("~p", [Reason]); - _ -> ok - end, - {noreply, State}; -handle_info(_Info, State) -> - {noreply, State}. - -%%-------------------------------------------------------------------- -%% Function: terminate(Reason, State) -> void() -%% Description: This function is called by a gen_server when it is about to -%% terminate. It should be the opposite of Module:init/1 and do any necessary -%% cleaning up. When it returns, the gen_server terminates with Reason. -%% The return value is ignored. -%%-------------------------------------------------------------------- -%% @private -terminate(_Reason, - #state{host = Host, server_host = ServerHost, - nodetree = TreePlugin, plugins = Plugins}) -> - ejabberd_router:unregister_route(Host), - case lists:member(?PEPNODE, Plugins) of - true -> - ejabberd_hooks:delete(caps_update, ServerHost, ?MODULE, - caps_update, 80), - ejabberd_hooks:delete(disco_sm_identity, ServerHost, - ?MODULE, disco_sm_identity, 75), - ejabberd_hooks:delete(disco_sm_features, ServerHost, - ?MODULE, disco_sm_features, 75), - ejabberd_hooks:delete(disco_sm_items, ServerHost, - ?MODULE, disco_sm_items, 75), - ejabberd_hooks:delete(c2s_filter_packet_in, ServerHost, - ?MODULE, drop_pep_error, 75), - gen_iq_handler:remove_iq_handler(ejabberd_sm, - ServerHost, ?NS_PUBSUB), - gen_iq_handler:remove_iq_handler(ejabberd_sm, - ServerHost, ?NS_PUBSUB_OWNER); - false -> ok - end, - ejabberd_hooks:delete(sm_remove_connection_hook, - ServerHost, ?MODULE, on_user_offline, 75), - ejabberd_hooks:delete(disco_local_identity, ServerHost, - ?MODULE, disco_local_identity, 75), - ejabberd_hooks:delete(disco_local_features, ServerHost, - ?MODULE, disco_local_features, 75), - ejabberd_hooks:delete(disco_local_items, ServerHost, - ?MODULE, disco_local_items, 75), - ejabberd_hooks:delete(presence_probe_hook, ServerHost, - ?MODULE, presence_probe, 80), - ejabberd_hooks:delete(roster_in_subscription, - ServerHost, ?MODULE, in_subscription, 50), - ejabberd_hooks:delete(roster_out_subscription, - ServerHost, ?MODULE, out_subscription, 50), - ejabberd_hooks:delete(remove_user, ServerHost, ?MODULE, - remove_user, 50), - ejabberd_hooks:delete(anonymous_purge_hook, ServerHost, - ?MODULE, remove_user, 50), - mod_disco:unregister_feature(ServerHost, ?NS_PUBSUB), - gen_mod:get_module_proc(ServerHost, ?LOOPNAME) ! stop, - terminate_plugins(Host, ServerHost, Plugins, - TreePlugin). - -%%-------------------------------------------------------------------- -%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} -%% Description: Convert process state when code is changed -%%-------------------------------------------------------------------- -%% @private -code_change(_OldVsn, State, _Extra) -> {ok, State}. - --spec(do_route/7 :: -( - ServerHost :: binary(), - Access :: atom(), - Plugins :: [binary(),...], - Host :: mod_pubsub:hostPubsub(), - From :: jid(), - To :: jid(), - Packet :: xmlel()) - -> ok -). - -%%-------------------------------------------------------------------- -%%% Internal functions -%%-------------------------------------------------------------------- -do_route(ServerHost, Access, Plugins, Host, From, To, Packet) -> - #xmlel{name = Name, attrs = Attrs} = Packet, - case To of - #jid{luser = <<"">>, lresource = <<"">>} -> - case Name of - <<"iq">> -> - case jlib:iq_query_info(Packet) of - #iq{type = get, xmlns = ?NS_DISCO_INFO, sub_el = SubEl, - lang = Lang} = - IQ -> - #xmlel{attrs = QAttrs} = SubEl, - Node = xml:get_attr_s(<<"node">>, QAttrs), - Info = ejabberd_hooks:run_fold(disco_info, ServerHost, - [], - [ServerHost, ?MODULE, - <<"">>, <<"">>]), - Res = case iq_disco_info(Host, Node, From, Lang) of - {result, IQRes} -> - jlib:iq_to_xml(IQ#iq{type = result, - sub_el = - [#xmlel{name = - <<"query">>, - attrs = - QAttrs, - children = - IQRes ++ - Info}]}); - {error, Error} -> - jlib:make_error_reply(Packet, Error) - end, - ejabberd_router:route(To, From, Res); - #iq{type = get, xmlns = ?NS_DISCO_ITEMS, - sub_el = SubEl} = - IQ -> - #xmlel{attrs = QAttrs} = SubEl, - Node = xml:get_attr_s(<<"node">>, QAttrs), - Rsm = jlib:rsm_decode(IQ), - Res = case iq_disco_items(Host, Node, From, Rsm) of - {result, IQRes} -> - jlib:iq_to_xml(IQ#iq{type = result, - sub_el = - [#xmlel{name = - <<"query">>, - attrs = - QAttrs, - children = - IQRes}]}) -% {error, Error} -> -% jlib:make_error_reply(Packet, Error) - end, - ejabberd_router:route(To, From, Res); - #iq{type = IQType, xmlns = ?NS_PUBSUB, lang = Lang, - sub_el = SubEl} = - IQ -> - Res = case iq_pubsub(Host, ServerHost, From, IQType, - SubEl, Lang, Access, Plugins) - of - {result, IQRes} -> - jlib:iq_to_xml(IQ#iq{type = result, - sub_el = IQRes}); - {error, Error} -> - jlib:make_error_reply(Packet, Error) - end, - ejabberd_router:route(To, From, Res); - #iq{type = IQType, xmlns = ?NS_PUBSUB_OWNER, - lang = Lang, sub_el = SubEl} = - IQ -> - Res = case iq_pubsub_owner(Host, ServerHost, From, - IQType, SubEl, Lang) - of - {result, IQRes} -> - jlib:iq_to_xml(IQ#iq{type = result, - sub_el = IQRes}); - {error, Error} -> - jlib:make_error_reply(Packet, Error) - end, - ejabberd_router:route(To, From, Res); - #iq{type = get, xmlns = (?NS_VCARD) = XMLNS, - lang = Lang, sub_el = _SubEl} = - IQ -> - Res = IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"vCard">>, - attrs = [{<<"xmlns">>, XMLNS}], - children = iq_get_vcard(Lang)}]}, - ejabberd_router:route(To, From, jlib:iq_to_xml(Res)); - #iq{type = set, xmlns = ?NS_COMMANDS} = IQ -> - Res = case iq_command(Host, ServerHost, From, IQ, - Access, Plugins) - of - {error, Error} -> - jlib:make_error_reply(Packet, Error); - {result, IQRes} -> - jlib:iq_to_xml(IQ#iq{type = result, - sub_el = IQRes}) - end, - ejabberd_router:route(To, From, Res); - #iq{} -> - Err = jlib:make_error_reply(Packet, - ?ERR_FEATURE_NOT_IMPLEMENTED), - ejabberd_router:route(To, From, Err); - _ -> ok - end; - <<"message">> -> - case xml:get_attr_s(<<"type">>, Attrs) of - <<"error">> -> ok; - _ -> - case find_authorization_response(Packet) of - none -> ok; - invalid -> - ejabberd_router:route(To, From, - jlib:make_error_reply(Packet, - ?ERR_BAD_REQUEST)); - XFields -> - handle_authorization_response(Host, From, To, - Packet, XFields) - end - end; - _ -> ok - end; - _ -> - case xml:get_attr_s(<<"type">>, Attrs) of - <<"error">> -> ok; - <<"result">> -> ok; - _ -> - Err = jlib:make_error_reply(Packet, - ?ERR_ITEM_NOT_FOUND), - ejabberd_router:route(To, From, Err) - end - end. - -command_disco_info(_Host, ?NS_COMMANDS, _From) -> - IdentityEl = #xmlel{name = <<"identity">>, - attrs = - [{<<"category">>, <<"automation">>}, - {<<"type">>, <<"command-list">>}], - children = []}, - {result, [IdentityEl]}; -command_disco_info(_Host, ?NS_PUBSUB_GET_PENDING, - _From) -> - IdentityEl = #xmlel{name = <<"identity">>, - attrs = - [{<<"category">>, <<"automation">>}, - {<<"type">>, <<"command-node">>}], - children = []}, - FeaturesEl = #xmlel{name = <<"feature">>, - attrs = [{<<"var">>, ?NS_COMMANDS}], children = []}, - {result, [IdentityEl, FeaturesEl]}. - -node_disco_info(Host, Node, From) -> - node_disco_info(Host, Node, From, true, true). - -node_disco_info(Host, Node, From, _Identity, _Features) -> -% Action = -% fun(#pubsub_node{type = Type, id = NodeId}) -> -% I = case Identity of -% false -> -% []; -% true -> -% Types = -% case tree_call(Host, get_subnodes, [Host, Node, From]) of -% [] -> -% [<<"leaf">>]; %% No sub-nodes: it's a leaf node -% _ -> -% case node_call(Type, get_items, [NodeId, From, none]) of -% {result, []} -> [<<"collection">>]; -% {result, _} -> [<<"leaf">>, <<"collection">>]; -% _ -> [] -% end -% end, -% lists:map(fun(T) -> -% #xmlel{name = <<"identity">>, -% attrs = -% [{<<"category">>, -% <<"pubsub">>}, -% {<<"type">>, T}], -% children = []} -% end, Types) -% end, -% F = case Features of -% false -> -% []; -% true -> -% [#xmlel{name = <<"feature">>, -% attrs = [{<<"var">>, ?NS_PUBSUB}], -% children = []} -% | lists:map(fun -% (<<"rsm">>)-> -% #xmlel{name = <<"feature">>, -% attrs = [{<<"var">>, ?NS_RSM}]}; -% (T) -> -% #xmlel{name = <<"feature">>, -% attrs = -% [{<<"var">>, -% <<(?NS_PUBSUB)/binary, -% "#", -% T/binary>>}], -% children = []} -% end, -% features(Type))] -% end, -% %% TODO: add meta-data info (spec section 5.4) -% {result, I ++ F} -% end, -% case transaction(Host, Node, Action, sync_dirty) of -% {result, {_, Result}} -> {result, Result}; -% Other -> Other -% end. - Action = fun (#pubsub_node{type = Type, id = NodeId}) -> - Types = case tree_call(Host, get_subnodes, - [Host, Node, From]) - of - [] -> [<<"leaf">>]; - _ -> - case node_call(Type, get_items, - [NodeId, From, none]) - of - {result, []} -> - [<<"collection">>]; - {result, _} -> - [<<"leaf">>, - <<"collection">>]; - _ -> [] - end - end, - I = lists:map(fun (T) -> - #xmlel{name = <<"identity">>, - attrs = - [{<<"category">>, - <<"pubsub">>}, - {<<"type">>, T}], - children = []} - end, - Types), - F = [#xmlel{name = <<"feature">>, - attrs = [{<<"var">>, ?NS_PUBSUB}], - children = []} - | lists:map(fun - (<<"rsm">>)-> - #xmlel{name = <<"feature">>, - attrs = [{<<"var">>, ?NS_RSM}]}; - (T) -> - #xmlel{name = <<"feature">>, - attrs = - [{<<"var">>, - <<(?NS_PUBSUB)/binary, - "#", - T/binary>>}], - children = []} - end, - features(Type))], - {result, I ++ F} - end, - case transaction(Host, Node, Action, sync_dirty) of - {result, {_, Result}} -> {result, Result}; - Other -> Other - end. - -iq_disco_info(Host, SNode, From, Lang) -> - [Node | _] = case SNode of - <<>> -> [<<>>]; - _ -> str:tokens(SNode, <<"!">>) - end, - % Node = string_to_node(RealSNode), - case Node of - <<>> -> - {result, - [#xmlel{name = <<"identity">>, - attrs = - [{<<"category">>, <<"pubsub">>}, - {<<"type">>, <<"service">>}, - {<<"name">>, - translate:translate(Lang, <<"Publish-Subscribe">>)}], - children = []}, - #xmlel{name = <<"feature">>, - attrs = [{<<"var">>, ?NS_DISCO_INFO}], children = []}, - #xmlel{name = <<"feature">>, - attrs = [{<<"var">>, ?NS_DISCO_ITEMS}], children = []}, - #xmlel{name = <<"feature">>, - attrs = [{<<"var">>, ?NS_PUBSUB}], children = []}, - #xmlel{name = <<"feature">>, - attrs = [{<<"var">>, ?NS_COMMANDS}], children = []}, - #xmlel{name = <<"feature">>, - attrs = [{<<"var">>, ?NS_VCARD}], children = []}] - ++ - lists:map(fun - (<<"rsm">>)-> - #xmlel{name = <<"feature">>, - attrs = [{<<"var">>, ?NS_RSM}]}; - (Feature) -> - #xmlel{name = <<"feature">>, - attrs = - [{<<"var">>, <<(?NS_PUBSUB)/binary, "#", Feature/binary>>}], - children = []} - end, - features(Host, Node))}; - ?NS_COMMANDS -> command_disco_info(Host, Node, From); - ?NS_PUBSUB_GET_PENDING -> - command_disco_info(Host, Node, From); - _ -> node_disco_info(Host, Node, From) - end. - --spec(iq_disco_items/4 :: -( - Host :: mod_pubsub:host(), - NodeId :: <<>> | mod_pubsub:nodeId(), - From :: jid(), - Rsm :: any()) - -> {result, [xmlel()]} -). -iq_disco_items(Host, <<>>, From, _RSM) -> - {result, - lists:map(fun (#pubsub_node{nodeid = {_, SubNode}, - options = Options}) -> - Attrs = case get_option(Options, title) of - false -> - [{<<"jid">>, Host} - | nodeAttr(SubNode)]; - Title -> - [{<<"jid">>, Host}, - {<<"name">>, Title} - | nodeAttr(SubNode)] - end, - #xmlel{name = <<"item">>, attrs = Attrs, - children = []} - end, - tree_action(Host, get_subnodes, [Host, <<>>, From]))}; -% case tree_action(Host, get_subnodes, [Host, <<>>, From]) of -% Nodes when is_list(Nodes) -> -% {result, -% lists:map(fun (#pubsub_node{nodeid = {_, SubNode}, -% options = Options}) -> -% Attrs = case get_option(Options, title) of -% false -> -% [{<<"jid">>, Host} -% | nodeAttr(SubNode)]; -% Title -> -% [{<<"jid">>, Host}, -% {<<"name">>, Title} -% | nodeAttr(SubNode)] -% end, -% #xmlel{name = <<"item">>, attrs = Attrs, -% children = []} -% end, -% Nodes)}; -% Other -> Other -% end; -iq_disco_items(Host, ?NS_COMMANDS, _From, _RSM) -> - CommandItems = [#xmlel{name = <<"item">>, - attrs = - [{<<"jid">>, Host}, - {<<"node">>, ?NS_PUBSUB_GET_PENDING}, - {<<"name">>, <<"Get Pending">>}], - children = []}], - {result, CommandItems}; -iq_disco_items(_Host, ?NS_PUBSUB_GET_PENDING, _From, _RSM) -> - CommandItems = [], {result, CommandItems}; -iq_disco_items(Host, Item, From, RSM) -> - case str:tokens(Item, <<"!">>) of - [_Node, _ItemID] -> {result, []}; - [Node] -> -% Node = string_to_node(SNode), - Action = fun (#pubsub_node{id = Idx, type = Type, - options = Options}) -> - Owners = node_owners_call(Type, Idx), - {NodeItems, RsmOut} = case get_allowed_items_call(Host, Idx, From, Type, Options, Owners, RSM) of - {result, R} -> R; - _ -> {[], none} - end, - Nodes = lists:map(fun (#pubsub_node{nodeid = - {_, SubNode}, - options = - SubOptions}) -> - Attrs = case - get_option(SubOptions, - title) - of - false -> - [{<<"jid">>, - Host} - | nodeAttr(SubNode)]; - Title -> - [{<<"jid">>, - Host}, - {<<"name">>, - Title} - | nodeAttr(SubNode)] - end, - #xmlel{name = <<"item">>, - attrs = Attrs, - children = []} - end, - tree_call(Host, get_subnodes, - [Host, Node, From])), - Items = lists:map(fun (#pubsub_item{itemid = - {RN, _}}) -> - {result, Name} = - node_call(Type, - get_item_name, - [Host, Node, - RN]), - #xmlel{name = <<"item">>, - attrs = - [{<<"jid">>, - Host}, - {<<"name">>, - Name}], - children = []} - end, - NodeItems), - {result, Nodes ++ Items ++ jlib:rsm_encode(RsmOut)} - end, - case transaction(Host, Node, Action, sync_dirty) of - {result, {_, Result}} -> {result, Result}; - Other -> Other - end - end. - --spec(iq_sm/3 :: -( - From :: jid(), - To :: jid(), - IQ :: iq_request()) - -> iq_result() | iq_error() -). -iq_sm(From, To, #iq{type = Type, sub_el = SubEl, xmlns = XMLNS, lang = Lang} = IQ) -> - ServerHost = To#jid.lserver, - LOwner = jlib:jid_tolower(jlib:jid_remove_resource(To)), - Res = case XMLNS of - ?NS_PUBSUB -> - iq_pubsub(LOwner, ServerHost, From, Type, SubEl, Lang); - ?NS_PUBSUB_OWNER -> - iq_pubsub_owner(LOwner, ServerHost, From, Type, SubEl, - Lang) - end, - case Res of - {result, IQRes} -> IQ#iq{type = result, sub_el = IQRes}; - {error, Error} -> - IQ#iq{type = error, sub_el = [Error, SubEl]} - end. - -iq_get_vcard(Lang) -> - [#xmlel{name = <<"FN">>, attrs = [], - children = [{xmlcdata, <<"ejabberd/mod_pubsub">>}]}, - #xmlel{name = <<"URL">>, attrs = [], - children = [{xmlcdata, ?EJABBERD_URI}]}, - #xmlel{name = <<"DESC">>, attrs = [], - children = - [{xmlcdata, - <<(translate:translate(Lang, - <<"ejabberd Publish-Subscribe module">>))/binary, - "\nCopyright (c) 2004-2015 ProcessOne">>}]}]. - --spec(iq_pubsub/6 :: -( - Host :: mod_pubsub:host(), - ServerHost :: binary(), - From :: jid(), - IQType :: 'get' | 'set', - SubEl :: xmlel(), - Lang :: binary()) - -> {result, [xmlel()]} - %%% - | {error, xmlel()} -). - -iq_pubsub(Host, ServerHost, From, IQType, SubEl, Lang) -> - iq_pubsub(Host, ServerHost, From, IQType, SubEl, Lang, all, plugins(ServerHost)). - --spec(iq_pubsub/8 :: -( - Host :: mod_pubsub:host(), - ServerHost :: binary(), - From :: jid(), - IQType :: 'get' | 'set', - SubEl :: xmlel(), - Lang :: binary(), - Access :: atom(), - Plugins :: [binary(),...]) - -> {result, [xmlel()]} - %%% - | {error, xmlel()} -). - -iq_pubsub(Host, ServerHost, From, IQType, SubEl, Lang, Access, Plugins) -> - #xmlel{children = SubEls} = SubEl, - case xml:remove_cdata(SubEls) of - [#xmlel{name = Name, attrs = Attrs, children = Els} | Rest] -> - Node = xml:get_attr_s(<<"node">>, Attrs), - case {IQType, Name} of - {set, <<"create">>} -> - Config = case Rest of - [#xmlel{name = <<"configure">>, children = C}] -> C; - _ -> [] - end, - Type = case xml:get_attr_s(<<"type">>, Attrs) of - <<>> -> hd(Plugins); - T -> T - end, - case lists:member(Type, Plugins) of - false -> - {error, - extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, - unsupported, <<"create-nodes">>)}; - true -> - create_node(Host, ServerHost, Node, From, Type, Access, Config) - end; - {set, <<"publish">>} -> - case xml:remove_cdata(Els) of - [#xmlel{name = <<"item">>, attrs = ItemAttrs, - children = Payload}] -> - ItemId = xml:get_attr_s(<<"id">>, ItemAttrs), - publish_item(Host, ServerHost, Node, From, ItemId, Payload, Access); - [] -> - {error, - extended_error(?ERR_BAD_REQUEST, <<"item-required">>)}; - _ -> - {error, - extended_error(?ERR_BAD_REQUEST, <<"invalid-payload">>)} - end; - {set, <<"retract">>} -> - ForceNotify = case xml:get_attr_s(<<"notify">>, Attrs) - of - <<"1">> -> true; - <<"true">> -> true; - _ -> false - end, - case xml:remove_cdata(Els) of - [#xmlel{name = <<"item">>, attrs = ItemAttrs}] -> - ItemId = xml:get_attr_s(<<"id">>, ItemAttrs), - delete_item(Host, Node, From, ItemId, ForceNotify); - _ -> - {error, - extended_error(?ERR_BAD_REQUEST, <<"item-required">>)} - end; - {set, <<"subscribe">>} -> - Config = case Rest of - [#xmlel{name = <<"options">>, children = C}] -> C; - _ -> [] - end, - JID = xml:get_attr_s(<<"jid">>, Attrs), - subscribe_node(Host, Node, From, JID, Config); - {set, <<"unsubscribe">>} -> - JID = xml:get_attr_s(<<"jid">>, Attrs), - SubId = xml:get_attr_s(<<"subid">>, Attrs), - unsubscribe_node(Host, Node, From, JID, SubId); - {get, <<"items">>} -> - MaxItems = xml:get_attr_s(<<"max_items">>, Attrs), - SubId = xml:get_attr_s(<<"subid">>, Attrs), - ItemIDs = lists:foldl(fun (#xmlel{name = <<"item">>, - attrs = ItemAttrs}, - Acc) -> - case xml:get_attr_s(<<"id">>, - ItemAttrs) - of - <<"">> -> Acc; - ItemID -> [ItemID | Acc] - end; - (_, Acc) -> Acc - end, - [], xml:remove_cdata(Els)), - RSM = jlib:rsm_decode(SubEl), - get_items(Host, Node, From, SubId, MaxItems, ItemIDs, RSM); - {get, <<"subscriptions">>} -> - get_subscriptions(Host, Node, From, Plugins); - {get, <<"affiliations">>} -> - get_affiliations(Host, Node, From, Plugins); - {get, <<"options">>} -> - SubID = xml:get_attr_s(<<"subid">>, Attrs), - JID = xml:get_attr_s(<<"jid">>, Attrs), - get_options(Host, Node, JID, SubID, Lang); - {set, <<"options">>} -> - SubID = xml:get_attr_s(<<"subid">>, Attrs), - JID = xml:get_attr_s(<<"jid">>, Attrs), - set_options(Host, Node, JID, SubID, Els); - _ -> {error, ?ERR_FEATURE_NOT_IMPLEMENTED} - end; - Other -> - ?INFO_MSG("Too many actions: ~p", [Other]), - {error, ?ERR_BAD_REQUEST} - end. - - --spec(iq_pubsub_owner/6 :: -( - Host :: mod_pubsub:host(), - ServerHost :: binary(), - From :: jid(), - IQType :: 'get' | 'set', - SubEl :: xmlel(), - Lang :: binary()) - -> {result, [xmlel()]} - %%% - | {error, xmlel()} -). -iq_pubsub_owner(Host, ServerHost, From, IQType, SubEl, Lang) -> - #xmlel{children = SubEls} = SubEl, - Action = lists:filter(fun(#xmlel{name = <<"set">>, _ = '_'}) -> false; - (_) -> true - end, xml:remove_cdata(SubEls)), - case Action of - [#xmlel{name = Name, attrs = Attrs, children = Els}] -> - Node = xml:get_attr_s(<<"node">>, Attrs), - case {IQType, Name} of - {get, <<"configure">>} -> - get_configure(Host, ServerHost, Node, From, Lang); - {set, <<"configure">>} -> - set_configure(Host, Node, From, Els, Lang); - {get, <<"default">>} -> - get_default(Host, Node, From, Lang); - {set, <<"delete">>} -> delete_node(Host, Node, From); - {set, <<"purge">>} -> purge_node(Host, Node, From); - {get, <<"subscriptions">>} -> - get_subscriptions(Host, Node, From); - {set, <<"subscriptions">>} -> - set_subscriptions(Host, Node, From, - xml:remove_cdata(Els)); - {get, <<"affiliations">>} -> - get_affiliations(Host, Node, From); - {set, <<"affiliations">>} -> - set_affiliations(Host, Node, From, xml:remove_cdata(Els)); - _ -> {error, ?ERR_FEATURE_NOT_IMPLEMENTED} - end; - _ -> - ?INFO_MSG("Too many actions: ~p", [Action]), - {error, ?ERR_BAD_REQUEST} - end. - -iq_command(Host, ServerHost, From, IQ, Access, Plugins) -> - case adhoc:parse_request(IQ) of - Req when is_record(Req, adhoc_request) -> - case adhoc_request(Host, ServerHost, From, Req, Access, - Plugins) - of - Resp when is_record(Resp, adhoc_response) -> - {result, [adhoc:produce_response(Req, Resp)]}; - Error -> Error - end; - Err -> Err - end. - -%% @doc

    Processes an Ad Hoc Command.

    -adhoc_request(Host, _ServerHost, Owner, - #adhoc_request{node = ?NS_PUBSUB_GET_PENDING, - lang = Lang, action = <<"execute">>, - xdata = false}, - _Access, Plugins) -> - send_pending_node_form(Host, Owner, Lang, Plugins); -adhoc_request(Host, _ServerHost, Owner, - #adhoc_request{node = ?NS_PUBSUB_GET_PENDING, - action = <<"execute">>, xdata = XData}, - _Access, _Plugins) -> - ParseOptions = case XData of - #xmlel{name = <<"x">>} = XEl -> - case jlib:parse_xdata_submit(XEl) of - invalid -> {error, ?ERR_BAD_REQUEST}; - XData2 -> - case set_xoption(Host, XData2, []) of - NewOpts when is_list(NewOpts) -> - {result, NewOpts}; - Err -> Err - end - end; - _ -> - ?INFO_MSG("Bad XForm: ~p", [XData]), - {error, ?ERR_BAD_REQUEST} - end, - case ParseOptions of - {result, XForm} -> - case lists:keysearch(node, 1, XForm) of - {value, {_, Node}} -> - send_pending_auth_events(Host, Node, Owner); - false -> - {error, - extended_error(?ERR_BAD_REQUEST, <<"bad-payload">>)} - end; - Error -> Error - end; -adhoc_request(_Host, _ServerHost, _Owner, - #adhoc_request{action = <<"cancel">>}, _Access, - _Plugins) -> - #adhoc_response{status = canceled}; -adhoc_request(Host, ServerHost, Owner, - #adhoc_request{action = <<>>} = R, Access, Plugins) -> - adhoc_request(Host, ServerHost, Owner, - R#adhoc_request{action = <<"execute">>}, Access, - Plugins); -adhoc_request(_Host, _ServerHost, _Owner, Other, - _Access, _Plugins) -> - ?DEBUG("Couldn't process ad hoc command:~n~p", [Other]), - {error, ?ERR_ITEM_NOT_FOUND}. - -%% @spec (Host, Owner, Lang, Plugins) -> iqRes() -%% @doc

    Sends the process pending subscriptions XForm for Host to -%% Owner.

    -send_pending_node_form(Host, Owner, _Lang, Plugins) -> - Filter = fun (Plugin) -> - lists:member(<<"get-pending">>, features(Plugin)) - end, - case lists:filter(Filter, Plugins) of - [] -> {error, ?ERR_FEATURE_NOT_IMPLEMENTED}; - Ps -> - XOpts = lists:map(fun (Node) -> - #xmlel{name = <<"option">>, attrs = [], - children = - [#xmlel{name = <<"value">>, - attrs = [], - children = - [{xmlcdata, Node}]}]} - end, - get_pending_nodes(Host, Owner, Ps)), - XForm = #xmlel{name = <<"x">>, - attrs = - [{<<"xmlns">>, ?NS_XDATA}, - {<<"type">>, <<"form">>}], - children = - [#xmlel{name = <<"field">>, - attrs = - [{<<"type">>, <<"list-single">>}, - {<<"var">>, <<"pubsub#node">>}], - children = lists:usort(XOpts)}]}, - #adhoc_response{status = executing, - defaultaction = <<"execute">>, elements = [XForm]} - end. - -get_pending_nodes(Host, Owner, Plugins) -> - Tr = fun (Type) -> - case node_call(Type, get_pending_nodes, [Host, Owner]) - of - {result, Nodes} -> Nodes; - _ -> [] - end - end, - case transaction(Host, - fun () -> - {result, lists:flatmap(Tr, Plugins)} - end, - sync_dirty) - of - {result, Res} -> Res; - Err -> Err - end. - -%% @spec (Host, Node, Owner) -> iqRes() -%% @doc

    Send a subscription approval form to Owner for all pending -%% subscriptions on Host and Node.

    -send_pending_auth_events(Host, Node, Owner) -> - ?DEBUG("Sending pending auth events for ~s on " - "~s:~s", - [jlib:jid_to_string(Owner), Host, Node]), - Action = fun (#pubsub_node{id = NodeID, type = Type}) -> - case lists:member(<<"get-pending">>, features(Type)) of - true -> - case node_call(Type, get_affiliation, - [NodeID, Owner]) - of - {result, owner} -> - node_call(Type, get_node_subscriptions, - [NodeID]); - _ -> {error, ?ERR_FORBIDDEN} - end; - false -> {error, ?ERR_FEATURE_NOT_IMPLEMENTED} - end - end, - case transaction(Host, Node, Action, sync_dirty) of - {result, {N, Subscriptions}} -> - lists:foreach(fun ({J, pending, _SubID}) -> - send_authorization_request(N, jlib:make_jid(J)); - ({J, pending}) -> - send_authorization_request(N, jlib:make_jid(J)); - (_) -> ok - end, - Subscriptions), - #adhoc_response{}; - Err -> Err - end. - -%%% authorization handling - -send_authorization_request(#pubsub_node{nodeid = {Host, Node}, - type = Type, id = NodeId}, - Subscriber) -> - Lang = <<"en">>, - Stanza = #xmlel{name = <<"message">>, attrs = [], - children = - [#xmlel{name = <<"x">>, - attrs = - [{<<"xmlns">>, ?NS_XDATA}, - {<<"type">>, <<"form">>}], - children = - [#xmlel{name = <<"title">>, attrs = [], - children = - [{xmlcdata, - translate:translate(Lang, - <<"PubSub subscriber request">>)}]}, - #xmlel{name = <<"instructions">>, - attrs = [], - children = - [{xmlcdata, - translate:translate(Lang, - <<"Choose whether to approve this entity's " - "subscription.">>)}]}, - #xmlel{name = <<"field">>, - attrs = - [{<<"var">>, <<"FORM_TYPE">>}, - {<<"type">>, <<"hidden">>}], - children = - [#xmlel{name = <<"value">>, - attrs = [], - children = - [{xmlcdata, - ?NS_PUBSUB_SUB_AUTH}]}]}, - #xmlel{name = <<"field">>, - attrs = - [{<<"var">>, <<"pubsub#node">>}, - {<<"type">>, - <<"text-single">>}, - {<<"label">>, - translate:translate(Lang, - <<"Node ID">>)}], - children = - [#xmlel{name = <<"value">>, - attrs = [], - children = - [{xmlcdata, Node}]}]}, - #xmlel{name = <<"field">>, - attrs = - [{<<"var">>, - <<"pubsub#subscriber_jid">>}, - {<<"type">>, <<"jid-single">>}, - {<<"label">>, - translate:translate(Lang, - <<"Subscriber Address">>)}], - children = - [#xmlel{name = <<"value">>, - attrs = [], - children = - [{xmlcdata, - jlib:jid_to_string(Subscriber)}]}]}, - #xmlel{name = <<"field">>, - attrs = - [{<<"var">>, - <<"pubsub#allow">>}, - {<<"type">>, <<"boolean">>}, - {<<"label">>, - translate:translate(Lang, - <<"Allow this Jabber ID to subscribe to " - "this pubsub node?">>)}], - children = - [#xmlel{name = <<"value">>, - attrs = [], - children = - [{xmlcdata, - <<"false">>}]}]}]}]}, - lists:foreach(fun (Owner) -> - ejabberd_router:route(service_jid(Host), - jlib:make_jid(Owner), Stanza) - end, - node_owners(Host, Type, NodeId)). - -find_authorization_response(Packet) -> - #xmlel{children = Els} = Packet, - XData1 = lists:map(fun (#xmlel{name = <<"x">>, - attrs = XAttrs} = - XEl) -> - case xml:get_attr_s(<<"xmlns">>, XAttrs) of - ?NS_XDATA -> - case xml:get_attr_s(<<"type">>, XAttrs) of - <<"cancel">> -> none; - _ -> jlib:parse_xdata_submit(XEl) - end; - _ -> none - end; - (_) -> none - end, - xml:remove_cdata(Els)), - XData = lists:filter(fun (E) -> E /= none end, XData1), - case XData of - [invalid] -> invalid; - [] -> none; - [XFields] when is_list(XFields) -> - ?DEBUG("XFields: ~p", [XFields]), - case lists:keysearch(<<"FORM_TYPE">>, 1, XFields) of - {value, {_, [?NS_PUBSUB_SUB_AUTH]}} -> XFields; - _ -> invalid - end - end. -%% @spec (Host, JID, Node, Subscription) -> void -%% Host = mod_pubsub:host() -%% JID = jlib:jid() -%% SNode = string() -%% Subscription = atom() | {atom(), mod_pubsub:subid()} -%% @doc Send a message to JID with the supplied Subscription -%% TODO : ask Christophe's opinion -send_authorization_approval(Host, JID, SNode, Subscription) -> - SubAttrs = case Subscription of -% {S, SID} -> -% [{<<"subscription">>, subscription_to_string(S)}, -% {<<"subid">>, SID}]; - S -> [{<<"subscription">>, subscription_to_string(S)}] - end, - Stanza = event_stanza([#xmlel{name = <<"subscription">>, - attrs = - [{<<"jid">>, jlib:jid_to_string(JID)} - | nodeAttr(SNode)] - ++ SubAttrs, - children = []}]), - ejabberd_router:route(service_jid(Host), JID, Stanza). - -handle_authorization_response(Host, From, To, Packet, XFields) -> - case {lists:keysearch(<<"pubsub#node">>, 1, XFields), - lists:keysearch(<<"pubsub#subscriber_jid">>, 1, XFields), - lists:keysearch(<<"pubsub#allow">>, 1, XFields)} - of - {{value, {_, [Node]}}, {value, {_, [SSubscriber]}}, - {value, {_, [SAllow]}}} -> -% Node = string_to_node(SNode), - Subscriber = jlib:string_to_jid(SSubscriber), - Allow = case SAllow of - <<"1">> -> true; - <<"true">> -> true; - _ -> false - end, - Action = fun (#pubsub_node{type = Type, - id = NodeId}) -> - IsApprover = - lists:member(jlib:jid_tolower(jlib:jid_remove_resource(From)), - node_owners_call(Type, NodeId)), - {result, Subscriptions} = node_call(Type, - get_subscriptions, - [NodeId, - Subscriber]), - if not IsApprover -> {error, ?ERR_FORBIDDEN}; - true -> - update_auth(Host, Node, Type, NodeId, - Subscriber, Allow, Subscriptions) - end - end, - case transaction(Host, Node, Action, sync_dirty) of - {error, Error} -> - ejabberd_router:route(To, From, - jlib:make_error_reply(Packet, Error)); - {result, {_, _NewSubscription}} -> - %% XXX: notify about subscription state change, section 12.11 - ok; - _ -> - ejabberd_router:route(To, From, - jlib:make_error_reply(Packet, - ?ERR_INTERNAL_SERVER_ERROR)) - end; - _ -> - ejabberd_router:route(To, From, - jlib:make_error_reply(Packet, - ?ERR_NOT_ACCEPTABLE)) - end. - -update_auth(Host, Node, Type, NodeId, Subscriber, Allow, - Subscriptions) -> - Subscription = lists:filter(fun ({pending, _}) -> true; - (_) -> false - end, - Subscriptions), - case Subscription of - [{pending, SubID}] -> - NewSubscription = case Allow of - true -> subscribed; - false -> none - end, - node_call(Type, set_subscriptions, - [NodeId, Subscriber, NewSubscription, SubID]), - send_authorization_approval(Host, Subscriber, Node, - NewSubscription), - {result, ok}; - _ -> {error, ?ERR_UNEXPECTED_REQUEST} - end. - --define(XFIELD(Type, Label, Var, Val), - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, Type}, - {<<"label">>, translate:translate(Lang, Label)}, - {<<"var">>, Var}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, Val}]}]}). - --define(BOOLXFIELD(Label, Var, Val), - ?XFIELD(<<"boolean">>, Label, Var, - case Val of - true -> <<"1">>; - _ -> <<"0">> - end)). - --define(STRINGXFIELD(Label, Var, Val), - ?XFIELD(<<"text-single">>, Label, Var, Val)). - --define(STRINGMXFIELD(Label, Var, Vals), - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, <<"text-multi">>}, - {<<"label">>, translate:translate(Lang, Label)}, - {<<"var">>, Var}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, V}]} - || V <- Vals]}). - --define(XFIELDOPT(Type, Label, Var, Val, Opts), - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, Type}, - {<<"label">>, translate:translate(Lang, Label)}, - {<<"var">>, Var}], - children = - lists:map(fun (Opt) -> - #xmlel{name = <<"option">>, attrs = [], - children = - [#xmlel{name = <<"value">>, - attrs = [], - children = - [{xmlcdata, Opt}]}]} - end, - Opts) - ++ - [#xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, Val}]}]}). - --define(LISTXFIELD(Label, Var, Val, Opts), - ?XFIELDOPT(<<"list-single">>, Label, Var, Val, Opts)). - --define(LISTMXFIELD(Label, Var, Vals, Opts), -%% @spec (Host::host(), ServerHost::host(), Node::pubsubNode(), Owner::jid(), NodeType::nodeType()) -> -%% {error, Reason::stanzaError()} | -%% {result, []} -%% @doc

    Create new pubsub nodes

    -%%

    In addition to method-specific error conditions, there are several general reasons why the node creation request might fail:

    -%%
      -%%
    • The service does not support node creation.
    • -%%
    • Only entities that are registered with the service are allowed to create nodes but the requesting entity is not registered.
    • -%%
    • The requesting entity does not have sufficient privileges to create nodes.
    • -%%
    • The requested NodeID already exists.
    • -%%
    • The request did not include a NodeID and "instant nodes" are not supported.
    • -%%
    -%%

    ote: node creation is a particular case, error return code is evaluated at many places:

    -%%
      -%%
    • iq_pubsub checks if service supports node creation (type exists)
    • -%%
    • create_node checks if instant nodes are supported
    • -%%
    • create_node asks node plugin if entity have sufficient privilege
    • -%%
    • nodetree create_node checks if nodeid already exists
    • -%%
    • node plugin create_node just sets default affiliation/subscription
    • -%%
    - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, <<"list-multi">>}, - {<<"label">>, translate:translate(Lang, Label)}, - {<<"var">>, Var}], - children = - lists:map(fun (Opt) -> - #xmlel{name = <<"option">>, attrs = [], - children = - [#xmlel{name = <<"value">>, - attrs = [], - children = - [{xmlcdata, Opt}]}]} - end, - Opts) - ++ - lists:map(fun (Val) -> - #xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, Val}]} - end, - Vals)}). - --spec(create_node/5 :: -( - Host :: mod_pubsub:host(), - ServerHost :: binary(), - Node :: <<>> | mod_pubsub:nodeId(), - Owner :: jid(), - Type :: binary()) - -> {result, [xmlel(),...]} - %%% - | {error, xmlel()} -). - -create_node(Host, ServerHost, Node, Owner, Type) -> - create_node(Host, ServerHost, Node, Owner, Type, all, []). - --spec(create_node/7 :: -( - Host :: mod_pubsub:host(), - ServerHost :: binary(), - Node :: <<>> | mod_pubsub:nodeId(), - Owner :: jid(), - Type :: binary(), - Access :: atom(), - Configuration :: [xmlel()]) - -> {result, [xmlel(),...]} - %%% - | {error, xmlel()} -). -create_node(Host, ServerHost, <<>>, Owner, Type, Access, Configuration) -> - case lists:member(<<"instant-nodes">>, features(Type)) of - true -> - NewNode = randoms:get_string(), - case create_node(Host, ServerHost, NewNode, Owner, Type, - Access, Configuration) - of - {result, _} -> - {result, - [#xmlel{name = <<"pubsub">>, - attrs = [{<<"xmlns">>, ?NS_PUBSUB}], - children = - [#xmlel{name = <<"create">>, - attrs = nodeAttr(NewNode), - children = []}]}]}; - Error -> Error - end; - false -> - {error, - extended_error(?ERR_NOT_ACCEPTABLE, - <<"nodeid-required">>)} - end; -create_node(Host, ServerHost, Node, Owner, GivenType, Access, Configuration) -> - Type = select_type(ServerHost, Host, Node, GivenType), - ParseOptions = case xml:remove_cdata(Configuration) of - [] -> {result, node_options(Type)}; - [#xmlel{name = <<"x">>} = XEl] -> - case jlib:parse_xdata_submit(XEl) of - invalid -> {error, ?ERR_BAD_REQUEST}; - XData -> - case set_xoption(Host, XData, node_options(Type)) - of - NewOpts when is_list(NewOpts) -> - {result, NewOpts}; - Err -> Err - end - end; - _ -> - ?INFO_MSG("Node ~p; bad configuration: ~p", - [Node, Configuration]), - {error, ?ERR_BAD_REQUEST} - end, - case ParseOptions of - {result, NodeOptions} -> - CreateNode = - fun() -> - Parent = case node_call(Type, node_to_path, [Node]) of - {result, [Node]} -> <<>>; - {result, Path} -> element(2, node_call(Type, path_to_node, [lists:sublist(Path, length(Path)-1)])) - end, - Parents = case Parent of - <<>> -> []; - _ -> [Parent] - end, - case node_call(Type, create_node_permission, [Host, ServerHost, Node, Parent, Owner, Access]) of - {result, true} -> - case tree_call(Host, create_node, [Host, Node, Type, Owner, NodeOptions, Parents]) of - {ok, NodeId} -> - ParentTree = tree_call(Host, get_parentnodes_tree, [Host, Node, Owner]), - SubsByDepth = [{Depth, [{N, get_node_subs(N)} || N <- Nodes]} || {Depth, Nodes} <- ParentTree], - case node_call(Type, create_node, [NodeId, Owner]) of - {result, Result} -> {result, {NodeId, SubsByDepth, Result}}; - Error -> Error - end; - {error, {virtual, NodeId}} -> - case node_call(Type, create_node, [NodeId, Owner]) of - {result, Result} -> {result, {NodeId, [], Result}}; - Error -> Error - end; - Error -> - Error - end; - _ -> - {error, ?ERR_FORBIDDEN} - end - end, - Reply = [#xmlel{name = <<"pubsub">>, - attrs = [{<<"xmlns">>, ?NS_PUBSUB}], - children = [#xmlel{name = <<"create">>, - attrs = nodeAttr(Node), - children = []}]}], - case transaction(Host, CreateNode, transaction) of - {result, {NodeId, SubsByDepth, {Result, broadcast}}} -> - broadcast_created_node(Host, Node, NodeId, Type, NodeOptions, SubsByDepth), - ejabberd_hooks:run(pubsub_create_node, ServerHost, [ServerHost, Host, Node, NodeId, NodeOptions]), - case Result of - default -> {result, Reply}; - _ -> {result, Result} - end; - {result, {NodeId, _SubsByDepth, default}} -> - ejabberd_hooks:run(pubsub_create_node, ServerHost, [ServerHost, Host, Node, NodeId, NodeOptions]), - {result, Reply}; - {result, {NodeId, _SubsByDepth, _Result}} -> - ejabberd_hooks:run(pubsub_create_node, ServerHost, [ServerHost, Host, Node, NodeId, NodeOptions]), - {result, Reply}; - Error -> - %% in case we change transaction to sync_dirty... - %% node_call(Type, delete_node, [Host, Node]), - %% tree_call(Host, delete_node, [Host, Node]), - Error - end; - Error -> - Error - end. - -%% @spec (Host, Node, Owner) -> -%% {error, Reason} | {result, []} -%% Host = host() -%% Node = pubsubNode() -%% Owner = jid() -%% Reason = stanzaError() --spec(delete_node/3 :: -( - Host :: mod_pubsub:host(), - Node :: mod_pubsub:nodeId(), - Owner :: jid()) - -> {result, [xmlel(),...]} - %%% - | {error, xmlel()} -). -%% @doc

    Delete specified node and all childs.

    -%%

    There are several reasons why the node deletion request might fail:

    -%%
      -%%
    • The requesting entity does not have sufficient privileges to delete the node.
    • -%%
    • The node is the root collection node, which cannot be deleted.
    • -%%
    • The specified node does not exist.
    • -%%
    -delete_node(_Host, <<>>, _Owner) -> - {error, ?ERR_NOT_ALLOWED}; -delete_node(Host, Node, Owner) -> - Action = fun(#pubsub_node{type = Type, id = NodeId}) -> - case node_call(Type, get_affiliation, [NodeId, Owner]) of - {result, owner} -> - ParentTree = tree_call(Host, get_parentnodes_tree, [Host, Node, service_jid(Host)]), - SubsByDepth = [{Depth, [{N, get_node_subs(N)} || N <- Nodes]} || {Depth, Nodes} <- ParentTree], - Removed = tree_call(Host, delete_node, [Host, Node]), - case node_call(Type, delete_node, [Removed]) of - {result, Res} -> {result, {SubsByDepth, Res}}; - Error -> Error - end; - _ -> - %% Entity is not an owner - {error, ?ERR_FORBIDDEN} - end - end, - Reply = [], - ServerHost = get(server_host), - case transaction(Host, Node, Action, transaction) of - {result, {_TNode, {SubsByDepth, {Result, broadcast, Removed}}}} -> - lists:foreach(fun({RNode, _RSubscriptions}) -> - {RH, RN} = RNode#pubsub_node.nodeid, - NodeId = RNode#pubsub_node.id, - Type = RNode#pubsub_node.type, - Options = RNode#pubsub_node.options, - broadcast_removed_node(RH, RN, NodeId, Type, Options, SubsByDepth), - ejabberd_hooks:run(pubsub_delete_node, ServerHost, [ServerHost, RH, RN, NodeId]) - end, Removed), - case Result of - default -> {result, Reply}; - _ -> {result, Result} - end; - {result, {_TNode, {_, {Result, Removed}}}} -> - lists:foreach(fun({RNode, _RSubscriptions}) -> - {RH, RN} = RNode#pubsub_node.nodeid, - NodeId = RNode#pubsub_node.id, - ejabberd_hooks:run(pubsub_delete_node, ServerHost, [ServerHost, RH, RN, NodeId]) - end, Removed), - case Result of - default -> {result, Reply}; - _ -> {result, Result} - end; - {result, {TNode, {_, default}}} -> - NodeId = TNode#pubsub_node.id, - ejabberd_hooks:run(pubsub_delete_node, ServerHost, [ServerHost, Host, Node, NodeId]), - {result, Reply}; - {result, {TNode, {_, Result}}} -> - NodeId = TNode#pubsub_node.id, - ejabberd_hooks:run(pubsub_delete_node, ServerHost, [ServerHost, Host, Node, NodeId]), - {result, Result}; - Error -> - Error - end. - -%% @spec (Host, Node, From, JID, Configuration) -> -%% {error, Reason::stanzaError()} | -%% {result, []} -%% Host = host() -%% Node = pubsubNode() -%% From = jid() -%% JID = jid() --spec(subscribe_node/5 :: -( - Host :: mod_pubsub:host(), - Node :: mod_pubsub:nodeId(), - From :: jid(), - JID :: binary(), - Configuration :: [xmlel()]) - -> {result, [xmlel(),...]} - %%% - | {error, xmlel()} -). -%% @see node_hometree:subscribe_node/5 -%% @doc

    Accepts or rejects subcription requests on a PubSub node.

    -%%

    There are several reasons why the subscription request might fail:

    -%%
      -%%
    • The bare JID portions of the JIDs do not match.
    • -%%
    • The node has an access model of "presence" and the requesting entity is not subscribed to the owner's presence.
    • -%%
    • The node has an access model of "roster" and the requesting entity is not in one of the authorized roster groups.
    • -%%
    • The node has an access model of "whitelist" and the requesting entity is not on the whitelist.
    • -%%
    • The service requires payment for subscriptions to the node.
    • -%%
    • The requesting entity is anonymous and the service does not allow anonymous entities to subscribe.
    • -%%
    • The requesting entity has a pending subscription.
    • -%%
    • The requesting entity is blocked from subscribing (e.g., because having an affiliation of outcast).
    • -%%
    • The node does not support subscriptions.
    • -%%
    • The node does not exist.
    • -%%
    -subscribe_node(Host, Node, From, JID, Configuration) -> - SubOpts = case - pubsub_subscription_odbc:parse_options_xform(Configuration) - of - {result, GoodSubOpts} -> GoodSubOpts; - _ -> invalid - end, - Subscriber = case jlib:string_to_jid(JID) of - error -> {<<"">>, <<"">>, <<"">>}; - J -> - case jlib:jid_tolower(J) of - error -> {<<"">>, <<"">>, <<"">>}; - J1 -> J1 - end - end, - Action = fun (#pubsub_node{options = Options, - type = Type, id = NodeId}) -> - Features = features(Type), - SubscribeFeature = lists:member(<<"subscribe">>, Features), - OptionsFeature = lists:member(<<"subscription-options">>, Features), - HasOptions = not (SubOpts == []), - SubscribeConfig = get_option(Options, subscribe), - AccessModel = get_option(Options, access_model), - SendLast = get_option(Options, send_last_published_item), - AllowedGroups = get_option(Options, roster_groups_allowed, []), - Owners = node_owners_call(Type, NodeId), - {PresenceSubscription, RosterGroup} = - get_presence_and_roster_permissions(Host, Subscriber, - Owners, AccessModel, AllowedGroups), - if not SubscribeFeature -> - {error, - extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, - unsupported, <<"subscribe">>)}; - not SubscribeConfig -> - {error, - extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, - unsupported, <<"subscribe">>)}; - HasOptions andalso not OptionsFeature -> - {error, - extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, - unsupported, - <<"subscription-options">>)}; - SubOpts == invalid -> - {error, - extended_error(?ERR_BAD_REQUEST, - <<"invalid-options">>)}; - true -> - node_call(Type, subscribe_node, - [NodeId, From, Subscriber, AccessModel, - SendLast, PresenceSubscription, - RosterGroup, SubOpts]) - end - end, - Reply = fun (Subscription) -> - SubAttrs = case Subscription of - {subscribed, SubId} -> - [{<<"subscription">>, - subscription_to_string(subscribed)}, - {<<"subid">>, SubId}, {<<"node">>, Node}]; - Other -> - [{<<"subscription">>, - subscription_to_string(Other)}, - {<<"node">>, Node}] - end, - Fields = [{<<"jid">>, jlib:jid_to_string(Subscriber)} - | SubAttrs], - [#xmlel{name = <<"pubsub">>, - attrs = [{<<"xmlns">>, ?NS_PUBSUB}], - children = - [#xmlel{name = <<"subscription">>, - attrs = Fields, children = []}]}] - end, - case transaction(Host, Node, Action, sync_dirty) of - {result, - {TNode, {Result, subscribed, SubId, send_last}}} -> - NodeId = TNode#pubsub_node.id, - Type = TNode#pubsub_node.type, - send_items(Host, Node, NodeId, Type, Subscriber, last), - case Result of - default -> {result, Reply({subscribed, SubId})}; - _ -> {result, Result} - end; - {result, {_TNode, {default, subscribed, SubId}}} -> - {result, Reply({subscribed, SubId})}; - {result, {_TNode, {Result, subscribed, _SubId}}} -> - {result, Result}; - {result, {TNode, {default, pending, _SubId}}} -> - send_authorization_request(TNode, Subscriber), - {result, Reply(pending)}; - {result, {TNode, {Result, pending}}} -> - send_authorization_request(TNode, Subscriber), - {result, Result}; - {result, {_, Result}} -> {result, Result}; - Error -> Error - end. - -%% @spec (Host, Noce, From, JID, SubId) -> {error, Reason} | {result, []} -%% Host = host() -%% Node = pubsubNode() -%% From = jid() -%% JID = string() -%% SubId = string() -%% Reason = stanzaError() -%% @doc

    Unsubscribe JID from the Node.

    -%%

    There are several reasons why the unsubscribe request might fail:

    -%%
      -%%
    • The requesting entity has multiple subscriptions to the node but does not specify a subscription ID.
    • -%%
    • The request does not specify an existing subscriber.
    • -%%
    • The requesting entity does not have sufficient privileges to unsubscribe the specified JID.
    • -%%
    • The node does not exist.
    • -%%
    • The request specifies a subscription ID that is not valid or current.
    • -%%
    --spec(unsubscribe_node/5 :: -( - Host :: mod_pubsub:host(), - Node :: mod_pubsub:nodeId(), - From :: jid(), - JID :: binary() | ljid(), - SubId :: mod_pubsub:subId()) - -> {result, []} - %%% - | {error, xmlel()} -). -unsubscribe_node(Host, Node, From, JID, SubId) - when is_binary(JID) -> - Subscriber = case jlib:string_to_jid(JID) of - error -> {<<"">>, <<"">>, <<"">>}; - J -> - case jlib:jid_tolower(J) of - error -> {<<"">>, <<"">>, <<"">>}; - J1 -> J1 - end - end, - unsubscribe_node(Host, Node, From, Subscriber, SubId); -unsubscribe_node(Host, Node, From, Subscriber, SubId) -> - Action = fun (#pubsub_node{type = Type, id = NodeId}) -> - node_call(Type, unsubscribe_node, - [NodeId, From, Subscriber, SubId]) - end, - case transaction(Host, Node, Action, sync_dirty) of - {result, {_, default}} -> {result, []}; -% {result, {_, Result}} -> {result, Result}; - Error -> Error - end. - -%% @spec (Host::host(), ServerHost::host(), JID::jid(), Node::pubsubNode(), ItemId::string(), Payload::term()) -> -%% {error, Reason::stanzaError()} | -%% {result, []} -%% @doc

    Publish item to a PubSub node.

    -%%

    The permission to publish an item must be verified by the plugin implementation.

    -%%

    There are several reasons why the publish request might fail:

    -%%
      -%%
    • The requesting entity does not have sufficient privileges to publish.
    • -%%
    • The node does not support item publication.
    • -%%
    • The node does not exist.
    • -%%
    • The payload size exceeds a service-defined limit.
    • -%%
    • The item contains more than one payload element or the namespace of the root payload element does not match the configured namespace for the node.
    • -%%
    • The request does not match the node configuration.
    • -%%
    --spec(publish_item/6 :: -( - Host :: mod_pubsub:host(), - ServerHost :: binary(), - Node :: mod_pubsub:nodeId(), - Publisher :: jid(), - ItemId :: <<>> | mod_pubsub:itemId(), - Payload :: mod_pubsub:payload()) - -> {result, [xmlel(),...]} - %%% - | {error, xmlel()} -). -publish_item(Host, ServerHost, Node, Publisher, ItemId, Payload) -> - publish_item(Host, ServerHost, Node, Publisher, ItemId, Payload, all). -publish_item(Host, ServerHost, Node, Publisher, <<>>, Payload, Access) -> - publish_item(Host, ServerHost, Node, Publisher, uniqid(), Payload, Access); -publish_item(Host, ServerHost, Node, Publisher, ItemId, Payload, Access) -> - Action = fun (#pubsub_node{options = Options, type = Type, id = NodeId}) -> - Features = features(Type), - PublishFeature = lists:member(<<"publish">>, Features), - PublishModel = get_option(Options, publish_model), - DeliverPayloads = get_option(Options, deliver_payloads), - PersistItems = get_option(Options, persist_items), - MaxItems = max_items(Host, Options), - PayloadCount = payload_xmlelements(Payload), - PayloadSize = byte_size(term_to_binary(Payload)) - 2, - PayloadMaxSize = get_option(Options, max_payload_size), - if not PublishFeature -> - {error, - extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, - unsupported, <<"publish">>)}; - PayloadSize > PayloadMaxSize -> - {error, - extended_error(?ERR_NOT_ACCEPTABLE, <<"payload-too-big">>)}; - (PayloadCount == 0) and (Payload == []) -> - {error, - extended_error(?ERR_BAD_REQUEST, <<"payload-required">>)}; - (PayloadCount > 1) or (PayloadCount == 0) -> - {error, - extended_error(?ERR_BAD_REQUEST, <<"invalid-payload">>)}; - (DeliverPayloads == false) and (PersistItems == false) and - (PayloadSize > 0) -> - {error, - extended_error(?ERR_BAD_REQUEST, <<"item-forbidden">>)}; - ((DeliverPayloads == true) or (PersistItems == true)) and - (PayloadSize == 0) -> - {error, - extended_error(?ERR_BAD_REQUEST, <<"item-required">>)}; - true -> - node_call(Type, publish_item, [NodeId, Publisher, PublishModel, MaxItems, ItemId, Payload]) - end - end, - ejabberd_hooks:run(pubsub_publish_item, ServerHost, [ServerHost, Node, Publisher, service_jid(Host), ItemId, Payload]), - Reply = [#xmlel{name = <<"pubsub">>, - attrs = [{<<"xmlns">>, ?NS_PUBSUB}], - children = - [#xmlel{name = <<"publish">>, attrs = nodeAttr(Node), - children = - [#xmlel{name = <<"item">>, - attrs = itemAttr(ItemId), - children = []}]}]}], - case transaction(Host, Node, Action, sync_dirty) of - {result, {TNode, {Result, Broadcast, Removed}}} -> - NodeId = TNode#pubsub_node.id, - Type = TNode#pubsub_node.type, - Options = TNode#pubsub_node.options, - case get_option(Options, deliver_notifications) of - true -> - BroadcastPayload = case Broadcast of - default -> Payload; - broadcast -> Payload; - PluginPayload -> PluginPayload - end, - broadcast_publish_item(Host, Node, NodeId, Type, Options, - Removed, ItemId, jlib:jid_tolower(Publisher), - BroadcastPayload); - false -> - ok - end, - set_cached_item(Host, NodeId, ItemId, Publisher, Payload), - case Result of - default -> {result, Reply}; - _ -> {result, Result} - end; - {result, {TNode, {default, Removed}}} -> - NodeId = TNode#pubsub_node.id, - Type = TNode#pubsub_node.type, - Options = TNode#pubsub_node.options, - broadcast_retract_items(Host, Node, NodeId, Type, Options, Removed), - set_cached_item(Host, NodeId, ItemId, Publisher, Payload), - {result, Reply}; - {result, {TNode, {Result, Removed}}} -> - NodeId = TNode#pubsub_node.id, - Type = TNode#pubsub_node.type, - Options = TNode#pubsub_node.options, - broadcast_retract_items(Host, Node, NodeId, Type, Options, Removed), - set_cached_item(Host, NodeId, ItemId, Publisher, Payload), - {result, Result}; - {result, {_, default}} -> - {result, Reply}; - {result, {_, Result}} -> - {result, Result}; - {error, ?ERR_ITEM_NOT_FOUND} -> - %% handles auto-create feature - %% for automatic node creation. we'll take the default node type: - %% first listed into the plugins configuration option, or pep - Type = select_type(ServerHost, Host, Node), - case lists:member(<<"auto-create">>, features(Type)) of - true -> - case create_node(Host, ServerHost, Node, Publisher, Type, Access, []) of - {result, [#xmlel{name = <<"pubsub">>, - attrs = [{<<"xmlns">>, ?NS_PUBSUB}], - children = - [#xmlel{name = <<"create">>, - attrs = [{<<"node">>, NewNode}], - children = []}]}]} -> - publish_item(Host, ServerHost, NewNode, - Publisher, ItemId, Payload); - _ -> - {error, ?ERR_ITEM_NOT_FOUND} - end; - false -> - {error, ?ERR_ITEM_NOT_FOUND} - end; - Error -> - Error - end. - -%% @spec (Host::host(), JID::jid(), Node::pubsubNode(), ItemId::string()) -> -%% {error, Reason::stanzaError()} | -%% {result, []} --spec(delete_item/4 :: -( - Host :: mod_pubsub:host(), - Node :: mod_pubsub:nodeId(), - Publisher :: jid(), - ItemId :: mod_pubsub:itemId()) - -> {result, []} - %%% - | {error, xmlel()} -). -%% @doc

    Delete item from a PubSub node.

    -%%

    The permission to delete an item must be verified by the plugin implementation.

    -%%

    There are several reasons why the item retraction request might fail:

    -%%
      -%%
    • The publisher does not have sufficient privileges to delete the requested item.
    • -%%
    • The node or item does not exist.
    • -%%
    • The request does not specify a node.
    • -%%
    • The request does not include an element or the element does not specify an ItemId.
    • -%%
    • The node does not support persistent items.
    • -%%
    • The service does not support the deletion of items.
    • -%%
    -delete_item(Host, Node, Publisher, ItemId) -> - delete_item(Host, Node, Publisher, ItemId, false). - - -delete_item(_, <<"">>, _, _, _) -> - {error, - extended_error(?ERR_BAD_REQUEST, <<"node-required">>)}; -delete_item(Host, Node, Publisher, ItemId, ForceNotify) -> - Action = fun (#pubsub_node{options = Options, type = Type, id = NodeId}) -> - Features = features(Type), - PersistentFeature = lists:member(<<"persistent-items">>, Features), - DeleteFeature = lists:member(<<"delete-items">>, Features), - PublishModel = get_option(Options, publish_model), - if %%-> iq_pubsub just does that matchs - %% %% Request does not specify an item - %% {error, extended_error(?ERR_BAD_REQUEST, "item-required")}; - not PersistentFeature -> - {error, - extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, - unsupported, - <<"persistent-items">>)}; - not DeleteFeature -> - {error, - extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, - unsupported, <<"delete-items">>)}; - true -> - node_call(Type, delete_item, - [NodeId, Publisher, PublishModel, ItemId]) - end - end, - Reply = [], - case transaction(Host, Node, Action, sync_dirty) of - {result, {TNode, {Result, broadcast}}} -> - NodeId = TNode#pubsub_node.id, - Type = TNode#pubsub_node.type, - Options = TNode#pubsub_node.options, - broadcast_retract_items(Host, Node, NodeId, Type, - Options, [ItemId], ForceNotify), - case get_cached_item(Host, NodeId) of - #pubsub_item{itemid = {ItemId, NodeId}} -> - unset_cached_item(Host, NodeId); - _ -> ok - end, - case Result of - default -> {result, Reply}; - _ -> {result, Result} - end; - {result, {_, default}} -> {result, Reply}; - {result, {_, Result}} -> {result, Result}; - Error -> Error - end. - -%% @spec (Host, JID, Node) -> -%% {error, Reason} | {result, []} -%% Host = host() -%% Node = pubsubNode() -%% JID = jid() -%% Reason = stanzaError() -%% @doc

    Delete all items of specified node owned by JID.

    -%%

    There are several reasons why the node purge request might fail:

    -%%
      -%%
    • The node or service does not support node purging.
    • -%%
    • The requesting entity does not have sufficient privileges to purge the node.
    • -%%
    • The node is not configured to persist items.
    • -%%
    • The specified node does not exist.
    • -%%
    --spec(purge_node/3 :: -( - Host :: mod_pubsub:host(), - Node :: mod_pubsub:nodeId(), - Owner :: jid()) - -> {result, []} - %%% - | {error, xmlel()} -). -purge_node(Host, Node, Owner) -> - Action = fun (#pubsub_node{options = Options, type = Type, id = NodeId}) -> - Features = features(Type), - PurgeFeature = lists:member(<<"purge-nodes">>, Features), - PersistentFeature = lists:member(<<"persistent-items">>, Features), - PersistentConfig = get_option(Options, persist_items), - if not PurgeFeature -> - {error, - extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, - unsupported, <<"purge-nodes">>)}; - not PersistentFeature -> - {error, - extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, - unsupported, - <<"persistent-items">>)}; - not PersistentConfig -> - {error, - extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, - unsupported, - <<"persistent-items">>)}; - true -> node_call(Type, purge_node, [NodeId, Owner]) - end - end, - Reply = [], - case transaction(Host, Node, Action, sync_dirty) of - {result, {TNode, {Result, broadcast}}} -> - NodeId = TNode#pubsub_node.id, - Type = TNode#pubsub_node.type, - Options = TNode#pubsub_node.options, - broadcast_purge_node(Host, Node, NodeId, Type, Options), - unset_cached_item(Host, NodeId), - case Result of - default -> {result, Reply}; - _ -> {result, Result} - end; - {result, {_, default}} -> {result, Reply}; - {result, {_, Result}} -> {result, Result}; - Error -> Error - end. - -%% @doc

    Return the items of a given node.

    -%%

    The number of items to return is limited by MaxItems.

    -%%

    The permission are not checked in this function.

    -%% @todo We probably need to check that the user doing the query has the right -%% to read the items. --spec(get_items/7 :: -( - Host :: mod_pubsub:host(), - Node :: mod_pubsub:nodeId(), - From :: jid(), - SubId :: mod_pubsub:subId(), - SMaxItems :: binary(), - ItemIDs :: [mod_pubsub:itemId()], - Rsm :: any()) - -> {result, [xmlel(),...]} - %%% - | {error, xmlel()} -). -get_items(Host, Node, From, SubId, SMaxItems, ItemIDs, RSM) -> - MaxItems = if SMaxItems == <<"">> -> - get_max_items_node(Host); - true -> - case catch jlib:binary_to_integer(SMaxItems) of - {'EXIT', _} -> {error, ?ERR_BAD_REQUEST}; - Val -> Val - end - end, - case MaxItems of - {error, Error} -> {error, Error}; - _ -> - Action = fun (#pubsub_node{options = Options, type = Type, id = NodeId}) -> - Features = features(Type), - RetreiveFeature = lists:member(<<"retrieve-items">>, Features), - PersistentFeature = lists:member(<<"persistent-items">>, Features), - AccessModel = get_option(Options, access_model), - AllowedGroups = get_option(Options, roster_groups_allowed, []), - Owners = node_owners_call(Type, NodeId), - {PresenceSubscription, RosterGroup} = - get_presence_and_roster_permissions(Host, From, Owners, - AccessModel, AllowedGroups), - if not RetreiveFeature -> - {error, - extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, - unsupported, - <<"retrieve-items">>)}; - not PersistentFeature -> - {error, - extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, - unsupported, - <<"persistent-items">>)}; - true -> - node_call(Type, get_items, - [NodeId, From, AccessModel, - PresenceSubscription, RosterGroup, - SubId, RSM]) - end - end, - case transaction(Host, Node, Action, sync_dirty) of - {result, {_, {Items, RSMOut}}} -> - SendItems = case ItemIDs of - [] -> Items; - _ -> - lists:filter(fun (#pubsub_item{itemid = - {ItemId, - _}}) -> - lists:member(ItemId, - ItemIDs) - end, - Items) - end, - {result, - [#xmlel{name = <<"pubsub">>, - attrs = [{<<"xmlns">>, ?NS_PUBSUB}], - children = - [#xmlel{name = <<"items">>, attrs = nodeAttr(Node), - children = - itemsEls(lists:sublist(SendItems, MaxItems))} - | jlib:rsm_encode(RSMOut)]}]}; - Error -> Error - end - end. - -get_items(Host, Node) -> - Action = fun (#pubsub_node{type = Type, id = NodeId}) -> - node_call(Type, get_items, [NodeId, service_jid(Host)]) - end, - case transaction(Host, Node, Action, sync_dirty) of - {result, {_, Items}} -> Items; - Error -> Error - end. - -get_item(Host, Node, ItemId) -> - Action = fun (#pubsub_node{type = Type, id = NodeId}) -> - node_call(Type, get_item, [NodeId, ItemId]) - end, - case transaction(Host, Node, Action, sync_dirty) of - {result, {_, Items}} -> Items; - Error -> Error - end. - -get_allowed_items_call(Host, NodeIdx, From, Type, Options, Owners) -> - case get_allowed_items_call(Host, NodeIdx, From, Type, Options, Owners, none) of - {result, {I, _}} -> {result, I}; - Error -> Error - end. -get_allowed_items_call(Host, NodeIdx, From, Type, Options, Owners, RSM) -> - AccessModel = get_option(Options, access_model), - AllowedGroups = get_option(Options, roster_groups_allowed, []), - {PresenceSubscription, RosterGroup} = - get_presence_and_roster_permissions(Host, From, Owners, AccessModel, - AllowedGroups), - node_call(Type, get_items, - [NodeIdx, From, AccessModel, PresenceSubscription, RosterGroup, undefined, RSM]). - -%% @spec (Host, Node, NodeId, Type, LJID, Number) -> any() -%% Host = pubsubHost() -%% Node = pubsubNode() -%% NodeId = pubsubNodeId() -%% Type = pubsubNodeType() -%% LJID = {U, S, []} -%% Number = last | integer() -%% @doc

    Resend the items of a node to the user.

    -%% @todo use cache-last-item feature -send_items(Host, Node, NodeId, Type, LJID, last) -> - Stanza = case get_cached_item(Host, NodeId) of - undefined -> - % special ODBC optimization, works only with node_hometree_odbc, node_flat_odbc and node_pep_odbc - case node_action(Host, Type, get_last_items, [NodeId, LJID, 1]) of - {result, [LastItem]} -> - {ModifNow, ModifUSR} = LastItem#pubsub_item.modification, - event_stanza_with_delay( - [#xmlel{name = <<"items">>, attrs = nodeAttr(Node), - children = itemsEls([LastItem])}], ModifNow, ModifUSR); - _ -> - event_stanza( - [#xmlel{name = <<"items">>, attrs = nodeAttr(Node), - children = itemsEls([])}]) - end; - LastItem -> - {ModifNow, ModifUSR} = - LastItem#pubsub_item.modification, - event_stanza_with_delay([#xmlel{name = - <<"items">>, - attrs = nodeAttr(Node), - children = - itemsEls([LastItem])}], - ModifNow, ModifUSR) - end, - dispatch_items(Host, LJID, Node, Stanza); -send_items(Host, Node, NodeId, Type, LJID, Number) -> - ToSend = case node_action(Host, Type, get_items, - [NodeId, LJID]) - of - {result, []} -> []; - {result, Items} -> - case Number of - N when N > 0 -> lists:sublist(Items, N); - _ -> Items - end; - _ -> [] - end, - Stanza = case ToSend of - [] -> - undefined; - [LastItem] -> - {ModifNow, ModifUSR} = - LastItem#pubsub_item.modification, - event_stanza_with_delay([#xmlel{name = <<"items">>, - attrs = nodeAttr(Node), - children = - itemsEls(ToSend)}], - ModifNow, ModifUSR); - _ -> - event_stanza([#xmlel{name = <<"items">>, - attrs = nodeAttr(Node), - children = itemsEls(ToSend)}]) - end, - dispatch_items(Host, LJID, Node, Stanza). - --spec(dispatch_items/4 :: -( - From :: mod_pubsub:host(), - To :: jid(), - Node :: mod_pubsub:nodeId(), - Stanza :: xmlel() | undefined) - -> any() -). - -dispatch_items(_From, _To, _Node, _Stanza = undefined) -> ok; -dispatch_items({FromU, FromS, FromR} = From, {ToU, ToS, ToR} = To, Node, - Stanza) -> - C2SPid = case ejabberd_sm:get_session_pid(ToU, ToS, ToR) of - ToPid when is_pid(ToPid) -> ToPid; - _ -> - R = user_resource(FromU, FromS, FromR), - case ejabberd_sm:get_session_pid(FromU, FromS, R) of - FromPid when is_pid(FromPid) -> FromPid; - _ -> undefined - end - end, - if C2SPid == undefined -> ok; - true -> - ejabberd_c2s:send_filtered(C2SPid, - {pep_message, <>}, - service_jid(From), jlib:make_jid(To), - Stanza) - end; -dispatch_items(From, To, _Node, Stanza) -> - ejabberd_router:route(service_jid(From), jlib:make_jid(To), Stanza). - -%% @spec (Host, JID, Plugins) -> {error, Reason} | {result, Response} -%% Host = host() -%% JID = jid() -%% Plugins = [Plugin::string()] -%% Reason = stanzaError() -%% Response = [pubsubIQResponse()] -%% @doc

    Return the list of affiliations as an XMPP response.

    --spec(get_affiliations/4 :: -( - Host :: mod_pubsub:host(), - Node :: mod_pubsub:nodeId(), - JID :: jid(), - Plugins :: [binary()]) - -> {result, [xmlel(),...]} - %%% - | {error, xmlel()} -). -get_affiliations(Host, <<>>, JID, Plugins) - when is_list(Plugins) -> - Result = lists:foldl(fun (Type, {Status, Acc}) -> - Features = features(Type), - RetrieveFeature = - lists:member(<<"retrieve-affiliations">>, Features), - if not RetrieveFeature -> - {{error, - extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, - unsupported, - <<"retrieve-affiliations">>)}, - Acc}; - true -> - {result, Affiliations} = - node_action(Host, Type, - get_entity_affiliations, - [Host, JID]), - {Status, [Affiliations | Acc]} - end - end, - {ok, []}, Plugins), - case Result of - {ok, Affiliations} -> - Entities = lists:flatmap(fun ({_, none}) -> []; - ({#pubsub_node{nodeid = {_, Node}}, - Affiliation}) -> - [#xmlel{name = <<"affiliation">>, - attrs = - [{<<"affiliation">>, - affiliation_to_string(Affiliation)} - | nodeAttr(Node)], - children = []}] - end, - lists:usort(lists:flatten(Affiliations))), - {result, - [#xmlel{name = <<"pubsub">>, - attrs = [{<<"xmlns">>, ?NS_PUBSUB}], - children = - [#xmlel{name = <<"affiliations">>, attrs = [], - children = Entities}]}]}; - {Error, _} -> Error - end; -get_affiliations(Host, NodeId, JID, Plugins) - when is_list(Plugins) -> - Result = lists:foldl(fun (Type, {Status, Acc}) -> - Features = features(Type), - RetrieveFeature = - lists:member(<<"retrieve-affiliations">>, - Features), - if not RetrieveFeature -> - {{error, - extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, - unsupported, - <<"retrieve-affiliations">>)}, - Acc}; - true -> - {result, Affiliations} = - node_action(Host, Type, - get_entity_affiliations, - [Host, JID]), - {Status, [Affiliations | Acc]} - end - end, - {ok, []}, Plugins), - case Result of - {ok, Affiliations} -> - Entities = lists:flatmap(fun ({_, none}) -> []; - ({#pubsub_node{nodeid = {_, Node}}, - Affiliation}) - when NodeId == Node -> - [#xmlel{name = <<"affiliation">>, - attrs = - [{<<"affiliation">>, - affiliation_to_string(Affiliation)} - | nodeAttr(Node)], - children = []}]; - (_) -> [] - end, - lists:usort(lists:flatten(Affiliations))), - {result, - [#xmlel{name = <<"pubsub">>, - attrs = [{<<"xmlns">>, ?NS_PUBSUB}], - children = - [#xmlel{name = <<"affiliations">>, attrs = [], - children = Entities}]}]}; - {Error, _} -> Error - end. - --spec(get_affiliations/3 :: -( - Host :: mod_pubsub:host(), - Node :: mod_pubsub:nodeId(), - JID :: jid()) - -> {result, [xmlel(),...]} - %%% - | {error, xmlel()} -). -get_affiliations(Host, Node, JID) -> - Action = fun (#pubsub_node{type = Type, id = NodeId}) -> - Features = features(Type), - RetrieveFeature = - lists:member(<<"modify-affiliations">>, Features), - {result, Affiliation} = node_call(Type, get_affiliation, - [NodeId, JID]), - if not RetrieveFeature -> - {error, - extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, - unsupported, - <<"modify-affiliations">>)}; - Affiliation /= owner -> {error, ?ERR_FORBIDDEN}; - true -> node_call(Type, get_node_affiliations, [NodeId]) - end - end, - case transaction(Host, Node, Action, sync_dirty) of - {result, {_, []}} -> {error, ?ERR_ITEM_NOT_FOUND}; - {result, {_, Affiliations}} -> - Entities = lists:flatmap(fun ({_, none}) -> []; - ({AJID, Affiliation}) -> - [#xmlel{name = <<"affiliation">>, - attrs = - [{<<"jid">>, - jlib:jid_to_string(AJID)}, - {<<"affiliation">>, - affiliation_to_string(Affiliation)}], - children = []}] - end, - Affiliations), - {result, - [#xmlel{name = <<"pubsub">>, - attrs = [{<<"xmlns">>, ?NS_PUBSUB_OWNER}], - children = - [#xmlel{name = <<"affiliations">>, - attrs = nodeAttr(Node), children = Entities}]}]}; - Error -> Error - end. - --spec(set_affiliations/4 :: -( - Host :: mod_pubsub:host(), - Node :: mod_pubsub:nodeId(), - From :: jid(), - EntitiesEls :: [xmlel()]) - -> {result, []} - %%% - | {error, xmlel()} -). -set_affiliations(Host, Node, From, EntitiesEls) -> - Owner = jlib:jid_tolower(jlib:jid_remove_resource(From)), - Entities = lists:foldl(fun (El, Acc) -> - case Acc of - error -> error; - _ -> - case El of - #xmlel{name = <<"affiliation">>, - attrs = Attrs} -> - JID = - jlib:string_to_jid(xml:get_attr_s(<<"jid">>, - Attrs)), - Affiliation = - string_to_affiliation(xml:get_attr_s(<<"affiliation">>, - Attrs)), - if (JID == error) or - (Affiliation == false) -> - error; - true -> - [{jlib:jid_tolower(JID), - Affiliation} - | Acc] - end - end - end - end, - [], EntitiesEls), - case Entities of - error -> {error, ?ERR_BAD_REQUEST}; - _ -> - Action = fun (#pubsub_node{type = Type, - id = NodeId}) -> - Owners = node_owners_call(Type, NodeId), - case lists:member(Owner, Owners) of - true -> - OwnerJID = jlib:make_jid(Owner), - FilteredEntities = case Owners of - [Owner] -> - [E - || E <- Entities, - element(1, E) =/= - OwnerJID]; - _ -> Entities - end, - lists:foreach(fun ({JID, Affiliation}) -> - node_call(Type, set_affiliation, [NodeId, JID, Affiliation]) - end, - FilteredEntities), - {result, []}; - _ -> {error, ?ERR_FORBIDDEN} - end - end, - case transaction(Host, Node, Action, sync_dirty) of - {result, {_, Result}} -> {result, Result}; - Other -> Other - end - end. - -get_options(Host, Node, JID, SubID, Lang) -> - Action = fun (#pubsub_node{type = Type, id = NodeID}) -> - case lists:member(<<"subscription-options">>, features(Type)) of - true -> - get_options_helper(JID, Lang, Node, NodeID, SubID, Type); - false -> - {error, - extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, - unsupported, - <<"subscription-options">>)} - end - end, - case transaction(Host, Node, Action, sync_dirty) of - {result, {_Node, XForm}} -> {result, [XForm]}; - Error -> Error - end. - -get_options_helper(JID, Lang, Node, NodeID, SubID, Type) -> - Subscriber = case jlib:string_to_jid(JID) of - error -> {<<"">>, <<"">>, <<"">>}; - J -> case jlib:jid_tolower(J) of - error -> {<<"">>, <<"">>, <<"">>}; - J1 -> J1 - end - end, - {result, Subs} = node_call(Type, get_subscriptions, - [NodeID, Subscriber]), - SubIDs = lists:foldl(fun ({subscribed, SID}, Acc) -> - [SID | Acc]; - (_, Acc) -> Acc - end, - [], Subs), - case {SubID, SubIDs} of - {_, []} -> - {error, - extended_error(?ERR_NOT_ACCEPTABLE, <<"not-subscribed">>)}; - {<<>>, [SID]} -> - read_sub(Subscriber, Node, NodeID, SID, Lang); - {<<>>, _} -> - {error, - extended_error(?ERR_NOT_ACCEPTABLE, <<"subid-required">>)}; - {_, _} -> - ValidSubId = lists:member(SubID, SubIDs), - if ValidSubId -> - read_sub(Subscriber, Node, NodeID, SubID, Lang); - true -> - {error, - extended_error(?ERR_NOT_ACCEPTABLE, <<"invalid-subid">>)} - end - end. - -read_sub(Subscriber, Node, NodeID, SubID, Lang) -> - Children = case pubsub_subscription_odbc:get_subscription(Subscriber, NodeID, SubID) of - {error, notfound} -> - []; - {result, #pubsub_subscription{options = Options}} -> - {result, XdataEl} = pubsub_subscription_odbc:get_options_xform(Lang, Options), - [XdataEl] - end, - OptionsEl = #xmlel{name = <<"options">>, - attrs = - [{<<"jid">>, jlib:jid_to_string(Subscriber)}, - {<<"subid">>, SubID} - | nodeAttr(Node)], - children = Children}, - PubsubEl = #xmlel{name = <<"pubsub">>, - attrs = [{<<"xmlns">>, ?NS_PUBSUB}], - children = [OptionsEl]}, - {result, PubsubEl}. - -set_options(Host, Node, JID, SubID, Configuration) -> - Action = fun (#pubsub_node{type = Type, id = NodeID}) -> - case lists:member(<<"subscription-options">>, - features(Type)) - of - true -> - set_options_helper(Configuration, JID, NodeID, SubID, - Type); - false -> - {error, - extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, - unsupported, - <<"subscription-options">>)} - end - end, - case transaction(Host, Node, Action, sync_dirty) of - {result, {_Node, Result}} -> {result, Result}; - Error -> Error - end. - -set_options_helper(Configuration, JID, NodeID, SubID, Type) -> - SubOpts = case pubsub_subscription_odbc:parse_options_xform(Configuration) of - {result, GoodSubOpts} -> GoodSubOpts; - _ -> invalid - end, - Subscriber = case jlib:string_to_jid(JID) of - error -> {<<"">>, <<"">>, <<"">>}; - J -> jlib:jid_tolower(J) - end, - {result, Subs} = node_call(Type, get_subscriptions, - [NodeID, Subscriber]), - SubIDs = lists:foldl(fun ({subscribed, SID}, Acc) -> - [SID | Acc]; - (_, Acc) -> Acc - end, - [], Subs), - case {SubID, SubIDs} of - {_, []} -> - {error, - extended_error(?ERR_NOT_ACCEPTABLE, - <<"not-subscribed">>)}; - {<<>>, [SID]} -> - write_sub(Subscriber, NodeID, SID, SubOpts); - {<<>>, _} -> - {error, - extended_error(?ERR_NOT_ACCEPTABLE, - <<"subid-required">>)}; - {_, _} -> write_sub(Subscriber, NodeID, SubID, SubOpts) - end. - -write_sub(_Subscriber, _NodeID, _SubID, invalid) -> - {error, extended_error(?ERR_BAD_REQUEST, <<"invalid-options">>)}; -write_sub(Subscriber, NodeID, SubID, Options) -> - case pubsub_subscription_odbc:set_subscription(Subscriber, NodeID, SubID, Options) of - {error, notfound} -> - {error, extended_error(?ERR_NOT_ACCEPTABLE, <<"invalid-subid">>)}; - {result, _} -> - {result, []} - end. - -%% @spec (Host, Node, JID, Plugins) -> {error, Reason} | {result, Response} -%% Host = host() -%% Node = pubsubNode() -%% JID = jid() -%% Plugins = [Plugin::string()] -%% Reason = stanzaError() -%% Response = [pubsubIQResponse()] -%% @doc

    Return the list of subscriptions as an XMPP response.

    -get_subscriptions(Host, Node, JID, Plugins) when is_list(Plugins) -> - Result = lists:foldl( - fun(Type, {Status, Acc}) -> - Features = features(Type), - RetrieveFeature = lists:member(<<"retrieve-subscriptions">>, Features), - if - not RetrieveFeature -> - %% Service does not support retreive subscriptions - {{error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, <<"retrieve-subscriptions">>)}, Acc}; - true -> - Subscriber = jlib:jid_remove_resource(JID), - {result, Subscriptions} = node_action(Host, Type, get_entity_subscriptions, [Host, Subscriber]), - {Status, [Subscriptions|Acc]} - end - end, {ok, []}, Plugins), - case Result of - {ok, Subscriptions} -> - Entities = lists:flatmap(fun ({_, none}) -> []; - ({#pubsub_node{nodeid = {_, SubsNode}}, - Subscription}) -> - case Node of - <<>> -> - [#xmlel{name = - <<"subscription">>, - attrs = - [{<<"subscription">>, - subscription_to_string(Subscription)} - | nodeAttr(SubsNode)], - children = []}]; - SubsNode -> - [#xmlel{name = - <<"subscription">>, - attrs = - [{<<"subscription">>, - subscription_to_string(Subscription)}], - children = []}]; - _ -> [] - end; - ({_, none, _}) -> []; - ({#pubsub_node{nodeid = {_, SubsNode}}, - Subscription, SubID, SubJID}) -> - case Node of - <<>> -> - [#xmlel{name = - <<"subscription">>, - attrs = - [{<<"jid">>, - jlib:jid_to_string(SubJID)}, - {<<"subid">>, - SubID}, - {<<"subscription">>, - subscription_to_string(Subscription)} - | nodeAttr(SubsNode)], - children = []}]; - SubsNode -> - [#xmlel{name = - <<"subscription">>, - attrs = - [{<<"jid">>, - jlib:jid_to_string(SubJID)}, - {<<"subid">>, - SubID}, - {<<"subscription">>, - subscription_to_string(Subscription)}], - children = []}]; - _ -> [] - end; - ({#pubsub_node{nodeid = {_, SubsNode}}, - Subscription, SubJID}) -> - case Node of - <<>> -> - [#xmlel{name = - <<"subscription">>, - attrs = - [{<<"jid">>, - jlib:jid_to_string(SubJID)}, - {<<"subscription">>, - subscription_to_string(Subscription)} - | nodeAttr(SubsNode)], - children = []}]; - SubsNode -> - [#xmlel{name = - <<"subscription">>, - attrs = - [{<<"jid">>, - jlib:jid_to_string(SubJID)}, - {<<"subscription">>, - subscription_to_string(Subscription)}], - children = []}]; - _ -> [] - end - end, - lists:usort(lists:flatten(Subscriptions))), - {result, - [#xmlel{name = <<"pubsub">>, - attrs = [{<<"xmlns">>, ?NS_PUBSUB}], - children = - [#xmlel{name = <<"subscriptions">>, attrs = [], - children = Entities}]}]}; - {Error, _} -> Error - end. - -get_subscriptions(Host, Node, JID) -> - Action = fun (#pubsub_node{type = Type, id = NodeId}) -> - Features = features(Type), - RetrieveFeature = - lists:member(<<"manage-subscriptions">>, Features), - {result, Affiliation} = node_call(Type, get_affiliation, - [NodeId, JID]), - if not RetrieveFeature -> - {error, - extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, - unsupported, - <<"manage-subscriptions">>)}; - Affiliation /= owner -> {error, ?ERR_FORBIDDEN}; - true -> - node_call(Type, get_node_subscriptions, [NodeId]) - end - end, - case transaction(Host, Node, Action, sync_dirty) of - {result, {_, Subscriptions}} -> - Entities = lists:flatmap(fun ({_, none}) -> []; - ({_, pending, _}) -> []; - ({AJID, Subscription}) -> - [#xmlel{name = <<"subscription">>, - attrs = - [{<<"jid">>, - jlib:jid_to_string(AJID)}, - {<<"subscription">>, - subscription_to_string(Subscription)}], - children = []}]; - ({AJID, Subscription, SubId}) -> - [#xmlel{name = <<"subscription">>, - attrs = - [{<<"jid">>, - jlib:jid_to_string(AJID)}, - {<<"subscription">>, - subscription_to_string(Subscription)}, - {<<"subid">>, SubId}], - children = []}] - end, - Subscriptions), - {result, - [#xmlel{name = <<"pubsub">>, - attrs = [{<<"xmlns">>, ?NS_PUBSUB_OWNER}], - children = - [#xmlel{name = <<"subscriptions">>, - attrs = nodeAttr(Node), children = Entities}]}]}; - Error -> Error - end. - -set_subscriptions(Host, Node, From, EntitiesEls) -> - Owner = - jlib:jid_tolower(jlib:jid_remove_resource(From)), - Entities = lists:foldl(fun (El, Acc) -> - case Acc of - error -> error; - _ -> - case El of - #xmlel{name = <<"subscription">>, - attrs = Attrs} -> - JID = - jlib:string_to_jid(xml:get_attr_s(<<"jid">>, - Attrs)), - Subscription = - string_to_subscription(xml:get_attr_s(<<"subscription">>, - Attrs)), - SubId = - xml:get_attr_s(<<"subid">>, - Attrs), - if (JID == error) or - (Subscription == false) -> - error; - true -> - [{jlib:jid_tolower(JID), - Subscription, SubId} - | Acc] - end - end - end - end, - [], EntitiesEls), - case Entities of - error -> {error, ?ERR_BAD_REQUEST}; - _ -> - Notify = fun (JID, Sub, _SubId) -> - Stanza = #xmlel{name = <<"message">>, attrs = [], - children = - [#xmlel{name = <<"pubsub">>, - attrs = - [{<<"xmlns">>, - ?NS_PUBSUB}], - children = - [#xmlel{name = - <<"subscription">>, - attrs = - [{<<"jid">>, - jlib:jid_to_string(JID)}, - {<<"subscription">>, - subscription_to_string(Sub)} - | nodeAttr(Node)], - children = - []}]}]}, - ejabberd_router:route(service_jid(Host), - jlib:make_jid(JID), Stanza) - end, - Action = fun (#pubsub_node{type = Type, - id = NodeId}) -> - case lists:member(Owner, node_owners_call(Type, NodeId)) of - true -> - Result = lists:foldl(fun ({JID, Subscription, - SubId}, - Acc) -> - case - node_call(Type, - set_subscriptions, - [NodeId, - JID, - Subscription, - SubId]) - of - {error, Err} -> - [{error, - Err} - | Acc]; - _ -> - Notify(JID, - Subscription, - SubId), - Acc - end - end, - [], Entities), - case Result of - [] -> {result, []}; - _ -> {error, ?ERR_NOT_ACCEPTABLE} - end; - _ -> {error, ?ERR_FORBIDDEN} - end - end, - case transaction(Host, Node, Action, sync_dirty) of - {result, {_, Result}} -> {result, Result}; - Other -> Other - end - end. - --spec(get_presence_and_roster_permissions/5 :: -( - Host :: mod_pubsub:host(), - From :: ljid(), - Owners :: [ljid(),...], - AccessModel :: mod_pubsub:accessModel(), - AllowedGroups :: [binary()]) - -> {PresenceSubscription::boolean(), RosterGroup::boolean()} -). - -get_presence_and_roster_permissions(Host, From, Owners, AccessModel, AllowedGroups) -> - if (AccessModel == presence) or (AccessModel == roster) -> - case Host of - {User, Server, _} -> - get_roster_info(User, Server, From, AllowedGroups); - _ -> - [{OUser, OServer, _} | _] = Owners, - get_roster_info(OUser, OServer, From, AllowedGroups) - end; - true -> {true, true} - end. - -%% @spec (OwnerUser, OwnerServer, {SubscriberUser, SubscriberServer, SubscriberResource}, AllowedGroups) -%% -> {PresenceSubscription, RosterGroup} -get_roster_info(_, _, {<<"">>, <<"">>, _}, _) -> - {false, false}; -get_roster_info(OwnerUser, OwnerServer, - {SubscriberUser, SubscriberServer, _}, AllowedGroups) -> - {Subscription, Groups} = - ejabberd_hooks:run_fold(roster_get_jid_info, - OwnerServer, {none, []}, - [OwnerUser, OwnerServer, - {SubscriberUser, SubscriberServer, <<"">>}]), - PresenceSubscription = Subscription == both orelse - Subscription == from orelse - {OwnerUser, OwnerServer} == - {SubscriberUser, SubscriberServer}, - RosterGroup = lists:any(fun (Group) -> - lists:member(Group, AllowedGroups) - end, - Groups), - {PresenceSubscription, RosterGroup}; -get_roster_info(OwnerUser, OwnerServer, JID, - AllowedGroups) -> - get_roster_info(OwnerUser, OwnerServer, - jlib:jid_tolower(JID), AllowedGroups). - -string_to_affiliation(<<"owner">>) -> owner; -string_to_affiliation(<<"publisher">>) -> publisher; -string_to_affiliation(<<"member">>) -> member; -string_to_affiliation(<<"outcast">>) -> outcast; -string_to_affiliation(<<"none">>) -> none; -string_to_affiliation(_) -> false. - -string_to_subscription(<<"subscribed">>) -> subscribed; -string_to_subscription(<<"pending">>) -> pending; -string_to_subscription(<<"unconfigured">>) -> - unconfigured; -string_to_subscription(<<"none">>) -> none; -string_to_subscription(_) -> false. - -affiliation_to_string(owner) -> <<"owner">>; -affiliation_to_string(publisher) -> <<"publisher">>; -affiliation_to_string(member) -> <<"member">>; -affiliation_to_string(outcast) -> <<"outcast">>; -affiliation_to_string(_) -> <<"none">>. - -subscription_to_string(subscribed) -> <<"subscribed">>; -subscription_to_string(pending) -> <<"pending">>; -subscription_to_string(unconfigured) -> <<"unconfigured">>; -subscription_to_string(_) -> <<"none">>. - --spec(service_jid/1 :: -( - Host :: mod_pubsub:host()) - -> jid() -). -service_jid(Host) -> - case Host of - {U, S, _} -> {jid, U, S, <<"">>, U, S, <<"">>}; - _ -> {jid, <<"">>, Host, <<"">>, <<"">>, Host, <<"">>} - end. - -%% @spec (LJID, NotifyType, Depth, NodeOptions, SubOptions) -> boolean() -%% LJID = jid() -%% NotifyType = items | nodes -%% Depth = integer() -%% NodeOptions = [{atom(), term()}] -%% SubOptions = [{atom(), term()}] -%% @doc

    Check if a notification must be delivered or not based on -%% node and subscription options.

    -is_to_deliver(LJID, NotifyType, Depth, NodeOptions, - SubOptions) -> - sub_to_deliver(LJID, NotifyType, Depth, SubOptions) - andalso node_to_deliver(LJID, NodeOptions). - -sub_to_deliver(_LJID, NotifyType, Depth, SubOptions) -> - lists:all(fun (Option) -> - sub_option_can_deliver(NotifyType, Depth, Option) - end, - SubOptions). - -sub_option_can_deliver(items, _, {subscription_type, nodes}) -> false; -sub_option_can_deliver(nodes, _, {subscription_type, items}) -> false; -sub_option_can_deliver(_, _, {subscription_depth, all}) -> true; -sub_option_can_deliver(_, Depth, {subscription_depth, D}) -> Depth =< D; -sub_option_can_deliver(_, _, {deliver, false}) -> false; -sub_option_can_deliver(_, _, {expire, When}) -> now() < When; -sub_option_can_deliver(_, _, _) -> true. - -node_to_deliver(LJID, NodeOptions) -> - PresenceDelivery = get_option(NodeOptions, presence_based_delivery), - presence_can_deliver(LJID, PresenceDelivery). - --spec(presence_can_deliver/2 :: -( - Entity :: ljid(), - _ :: boolean()) - -> boolean() -). -presence_can_deliver(_, false) -> true; -presence_can_deliver({User, Server, Resource}, true) -> - case mnesia:dirty_match_object({session, '_', '_', {User, Server}, '_', '_'}) of - [] -> false; - Ss -> - lists:foldl(fun(_, true) -> true; - ({session, _, _ , _, undefined, _}, _Acc) -> false; - ({session, _, {_, _, R}, _, _Priority, _}, _Acc) -> - case Resource of - [] -> true; - R -> true; - _ -> false - end - end, false, Ss) - end. - --spec(state_can_deliver/2 :: -( - Entity::ljid(), - SubOptions :: mod_pubsub:subOptions() | []) - -> [ljid()] -). -state_can_deliver({U, S, R}, []) -> [{U, S, R}]; -state_can_deliver({U, S, R}, SubOptions) -> - %% Check SubOptions for 'show_values' - case lists:keysearch('show_values', 1, SubOptions) of - %% If not in suboptions, item can be delivered, case doesn't apply - false -> [{U, S, R}]; - %% If in a suboptions ... - {_, {_, ShowValues}} -> - %% Get subscriber resources - Resources = case R of - %% If the subscriber JID is a bare one, get all its resources - <<>> -> user_resources(U, S); - %% If the subscriber JID is a full one, use its resource - R -> [R] - end, - %% For each resource, test if the item is allowed to be delivered - %% based on resource state - lists:foldl( - fun(Resource, Acc) -> - get_resource_state({U, S, Resource}, ShowValues, Acc) - end, [], Resources) - end. - --spec(get_resource_state/3 :: -( - Entity :: ljid(), - ShowValues :: [binary()], - JIDs :: [ljid()]) - -> [ljid()] -). -get_resource_state({U, S, R}, ShowValues, JIDs) -> - case ejabberd_sm:get_session_pid(U, S, R) of - %% If no PID, item can be delivered - none -> lists:append([{U, S, R}], JIDs); - %% If PID ... - Pid -> - %% Get user resource state - %% TODO : add a catch clause - Show = case ejabberd_c2s:get_presence(Pid) of - {_, _, <<"available">>, _} -> <<"online">>; - {_, _, State, _} -> State - end, - %% Is current resource state listed in 'show-values' suboption ? - case lists:member(Show, ShowValues) of %andalso Show =/= "online" of - %% If yes, item can be delivered - true -> lists:append([{U, S, R}], JIDs); - %% If no, item can't be delivered - false -> JIDs - end - end. - --spec(payload_xmlelements/1 :: -( - Payload :: mod_pubsub:payload()) - -> Count :: non_neg_integer() -). -%% @spec (Payload) -> int() -%% Payload = term() -%% @doc

    Count occurence of XML elements in payload.

    -payload_xmlelements(Payload) -> payload_xmlelements(Payload, 0). -payload_xmlelements([], Count) -> Count; -payload_xmlelements([#xmlel{} | Tail], Count) -> - payload_xmlelements(Tail, Count + 1); -payload_xmlelements([_ | Tail], Count) -> - payload_xmlelements(Tail, Count). - -%% @spec (Els) -> stanza() -%% Els = [xmlelement()] -%% @doc

    Build pubsub event stanza

    -event_stanza(Els) -> - #xmlel{name = <<"message">>, attrs = [], - children = - [#xmlel{name = <<"event">>, - attrs = [{<<"xmlns">>, ?NS_PUBSUB_EVENT}], - children = Els}]}. - -event_stanza_with_delay(Els, ModifNow, ModifUSR) -> - jlib:add_delay_info(event_stanza(Els), ModifUSR, ModifNow). - -%%%%%% broadcast functions - -broadcast_publish_item(Host, Node, NodeId, Type, NodeOptions, Removed, ItemId, From, Payload) -> - case get_collection_subscriptions(Host, Node) of - SubsByDepth when is_list(SubsByDepth) -> - Content = case get_option(NodeOptions, deliver_payloads) of - true -> Payload; - false -> [] - end, - Stanza = event_stanza( - [#xmlel{name = <<"items">>, attrs = nodeAttr(Node), - children = [#xmlel{name = <<"item">>, attrs = itemAttr(ItemId), - children = Content}]}]), - broadcast_stanza(Host, From, Node, NodeId, Type, - NodeOptions, SubsByDepth, items, Stanza, true), - case Removed of - [] -> - ok; - _ -> - case get_option(NodeOptions, notify_retract) of - true -> - RetractStanza = event_stanza( - [#xmlel{name = <<"items">>, attrs = nodeAttr(Node), - children = [#xmlel{name = <<"retract">>, attrs = itemAttr(RId)} || RId <- Removed]}]), - broadcast_stanza(Host, Node, NodeId, Type, - NodeOptions, SubsByDepth, - items, RetractStanza, true); - _ -> - ok - end - end, - {result, true}; - _ -> - {result, false} - end. - -broadcast_retract_items(Host, Node, NodeId, Type, NodeOptions, ItemIds) -> - broadcast_retract_items(Host, Node, NodeId, Type, NodeOptions, ItemIds, false). -broadcast_retract_items(_Host, _Node, _NodeId, _Type, _NodeOptions, [], _ForceNotify) -> - {result, false}; -broadcast_retract_items(Host, Node, NodeId, Type, NodeOptions, ItemIds, ForceNotify) -> - case (get_option(NodeOptions, notify_retract) or ForceNotify) of - true -> - case get_collection_subscriptions(Host, Node) of - SubsByDepth when is_list(SubsByDepth) -> - Stanza = event_stanza( - [#xmlel{name = <<"items">>, attrs = nodeAttr(Node), - children = [#xmlel{name = <<"retract">>, attrs = itemAttr(ItemId)} || ItemId <- ItemIds]}]), - broadcast_stanza(Host, Node, NodeId, Type, - NodeOptions, SubsByDepth, items, Stanza, true), - {result, true}; - _ -> - {result, false} - end; - _ -> - {result, false} - end. - -broadcast_purge_node(Host, Node, NodeId, Type, NodeOptions) -> - case get_option(NodeOptions, notify_retract) of - true -> - case get_collection_subscriptions(Host, Node) of - SubsByDepth when is_list(SubsByDepth) -> - Stanza = event_stanza( - [#xmlel{name = <<"purge">>, attrs = nodeAttr(Node)}]), - broadcast_stanza(Host, Node, NodeId, Type, - NodeOptions, SubsByDepth, nodes, Stanza, false), - {result, true}; - _ -> - {result, false} - end; - _ -> - {result, false} - end. - -broadcast_removed_node(Host, Node, NodeId, Type, NodeOptions, SubsByDepth) -> - case get_option(NodeOptions, notify_delete) of - true -> - case SubsByDepth of - [] -> - {result, false}; - _ -> - Stanza = event_stanza( - [#xmlel{name = <<"delete">>, attrs = nodeAttr(Node)}]), - broadcast_stanza(Host, Node, NodeId, Type, - NodeOptions, SubsByDepth, nodes, Stanza, false), - {result, true} - end; - _ -> - {result, false} - end. - -broadcast_created_node(_, _, _, _, _, []) -> - {result, false}; -broadcast_created_node(Host, Node, NodeId, Type, NodeOptions, SubsByDepth) -> - Stanza = event_stanza([#xmlel{name = <<"create">>, attrs = nodeAttr(Node)}]), - broadcast_stanza(Host, Node, NodeId, Type, NodeOptions, SubsByDepth, nodes, Stanza, true), - {result, true}. - -broadcast_config_notification(Host, Node, NodeId, Type, NodeOptions, Lang) -> - case get_option(NodeOptions, notify_config) of - true -> - case get_collection_subscriptions(Host, Node) of - SubsByDepth when is_list(SubsByDepth) -> - Content = case get_option(NodeOptions, deliver_payloads) of - true -> - [#xmlel{name = <<"x">>, attrs = [{<<"xmlns">>, ?NS_XDATA}, {<<"type">>, <<"result">>}], - children = get_configure_xfields(Type, NodeOptions, Lang, [])}]; - false -> - [] - end, - Stanza = event_stanza( - [#xmlel{name = <<"configuration">>, attrs = nodeAttr(Node), children = Content}]), - broadcast_stanza(Host, Node, NodeId, Type, - NodeOptions, SubsByDepth, nodes, Stanza, false), - {result, true}; - _ -> - {result, false} - end; - _ -> - {result, false} - end. - -get_collection_subscriptions(Host, Node) -> - Action = fun() -> - {result, lists:map(fun({Depth, Nodes}) -> - {Depth, [{N, get_node_subs(N)} || N <- Nodes]} - end, tree_call(Host, get_parentnodes_tree, [Host, Node, service_jid(Host)]))} - end, - case transaction(Host, Action, sync_dirty) of - {result, CollSubs} -> CollSubs; - _ -> [] - end. - -get_node_subs(#pubsub_node{type = Type, - id = NodeID}) -> - case node_call(Type, get_node_subscriptions, [NodeID]) of - {result, Subs} -> get_options_for_subs(NodeID, Subs); - Other -> Other - end. - -get_options_for_subs(NodeID, Subs) -> - lists:foldl(fun({JID, subscribed, SubID}, Acc) -> - case pubsub_subscription_odbc:get_subscription(JID, NodeID, SubID) of - {error, notfound} -> [{JID, SubID, []} | Acc]; - {result, #pubsub_subscription{options = Options}} -> [{JID, SubID, Options} | Acc]; - _ -> Acc - end; - (_, Acc) -> - Acc - end, [], Subs). - -broadcast_stanza(Host, _Node, _NodeId, _Type, NodeOptions, SubsByDepth, NotifyType, BaseStanza, SHIM) -> - NotificationType = get_option(NodeOptions, notification_type, headline), - BroadcastAll = get_option(NodeOptions, broadcast_all_resources), %% XXX this is not standard, but usefull - From = service_jid(Host), - Stanza = case NotificationType of - normal -> BaseStanza; - MsgType -> add_message_type(BaseStanza, iolist_to_binary(atom_to_list(MsgType))) - end, - %% Handles explicit subscriptions - SubIDsByJID = subscribed_nodes_by_jid(NotifyType, SubsByDepth), - lists:foreach(fun ({LJID, NodeName, SubIDs}) -> - LJIDs = case BroadcastAll of - true -> - {U, S, _} = LJID, - [{U, S, R} || R <- user_resources(U, S)]; - false -> - [LJID] - end, - %% Determine if the stanza should have SHIM ('SubID' and 'name') headers - StanzaToSend = case {SHIM, SubIDs} of - {false, _} -> - Stanza; - %% If there's only one SubID, don't add it - {true, [_]} -> - add_shim_headers(Stanza, collection_shim(NodeName)); - {true, SubIDs} -> - add_shim_headers(Stanza, lists:append(collection_shim(NodeName), subid_shim(SubIDs))) - end, - lists:foreach(fun(To) -> - ejabberd_router:route(From, jlib:make_jid(To), StanzaToSend) - end, LJIDs) - end, SubIDsByJID). - -broadcast_stanza({LUser, LServer, LResource}, Publisher, Node, NodeId, Type, NodeOptions, SubsByDepth, NotifyType, BaseStanza, SHIM) -> - broadcast_stanza({LUser, LServer, LResource}, Node, NodeId, Type, NodeOptions, SubsByDepth, NotifyType, BaseStanza, SHIM), - %% Handles implicit presence subscriptions - SenderResource = user_resource(LUser, LServer, LResource), - case ejabberd_sm:get_session_pid(LUser, LServer, SenderResource) of - C2SPid when is_pid(C2SPid) -> - Stanza = case get_option(NodeOptions, notification_type, headline) of - normal -> BaseStanza; - MsgType -> add_message_type(BaseStanza, iolist_to_binary(atom_to_list(MsgType))) - end, - %% set the from address on the notification to the bare JID of the account owner - %% Also, add "replyto" if entity has presence subscription to the account owner - %% See XEP-0163 1.1 section 4.3.1 - ejabberd_c2s:broadcast(C2SPid, - {pep_message, <<((Node))/binary, "+notify">>}, - _Sender = jlib:make_jid(LUser, LServer, <<"">>), - _StanzaToSend = add_extended_headers(Stanza, - _ReplyTo = extended_headers([jlib:jid_to_string(Publisher)]))); - _ -> - ?DEBUG("~p@~p has no session; can't deliver ~p to contacts", [LUser, LServer, BaseStanza]) - end; -broadcast_stanza(Host, _Publisher, Node, NodeId, Type, NodeOptions, SubsByDepth, NotifyType, BaseStanza, SHIM) -> - broadcast_stanza(Host, Node, NodeId, Type, NodeOptions, SubsByDepth, NotifyType, BaseStanza, SHIM). - -subscribed_nodes_by_jid(NotifyType, SubsByDepth) -> - NodesToDeliver = fun(Depth, Node, Subs, Acc) -> - NodeName = case Node#pubsub_node.nodeid of - {_, N} -> N; - Other -> Other - end, - NodeOptions = Node#pubsub_node.options, - lists:foldl(fun({LJID, SubID, SubOptions}, {JIDs, Recipients}) -> - case is_to_deliver(LJID, NotifyType, Depth, NodeOptions, SubOptions) of - true -> - %% If is to deliver : - case state_can_deliver(LJID, SubOptions) of - [] -> {JIDs, Recipients}; - JIDsToDeliver -> - lists:foldl( - fun(JIDToDeliver, {JIDsAcc, RecipientsAcc}) -> - case lists:member(JIDToDeliver, JIDs) of - %% check if the JIDs co-accumulator contains the Subscription Jid, - false -> - %% - if not, - %% - add the Jid to JIDs list co-accumulator ; - %% - create a tuple of the Jid, NodeId, and SubID (as list), - %% and add the tuple to the Recipients list co-accumulator - {[JIDToDeliver | JIDsAcc], [{JIDToDeliver, NodeName, [SubID]} | RecipientsAcc]}; - true -> - %% - if the JIDs co-accumulator contains the Jid - %% get the tuple containing the Jid from the Recipient list co-accumulator - {_, {JIDToDeliver, NodeName1, SubIDs}} = lists:keysearch(JIDToDeliver, 1, RecipientsAcc), - %% delete the tuple from the Recipients list - % v1 : Recipients1 = lists:keydelete(LJID, 1, Recipients), - % v2 : Recipients1 = lists:keyreplace(LJID, 1, Recipients, {LJID, NodeId1, [SubID | SubIDs]}), - %% add the SubID to the SubIDs list in the tuple, - %% and add the tuple back to the Recipients list co-accumulator - % v1.1 : {JIDs, lists:append(Recipients1, [{LJID, NodeId1, lists:append(SubIDs, [SubID])}])} - % v1.2 : {JIDs, [{LJID, NodeId1, [SubID | SubIDs]} | Recipients1]} - % v2: {JIDs, Recipients1} - {JIDsAcc, lists:keyreplace(JIDToDeliver, 1, RecipientsAcc, {JIDToDeliver, NodeName1, [SubID | SubIDs]})} - end - end, {JIDs, Recipients}, JIDsToDeliver) - end; - false -> - {JIDs, Recipients} - end - end, Acc, Subs) - end, - DepthsToDeliver = fun({Depth, SubsByNode}, Acc1) -> - lists:foldl(fun({Node, Subs}, Acc2) -> - NodesToDeliver(Depth, Node, Subs, Acc2) - end, Acc1, SubsByNode) - end, - {_, JIDSubs} = lists:foldl(DepthsToDeliver, {[], []}, SubsByDepth), - JIDSubs. - -user_resources(User, Server) -> - ejabberd_sm:get_user_resources(User, Server). - -user_resource(User, Server, <<>>) -> - case user_resources(User, Server) of - [R | _] -> R; - _ -> <<>> - end; -user_resource(_, _, Resource) -> Resource. - -%%%%%%% Configuration handling - -%%

    There are several reasons why the default node configuration options request might fail:

    -%%
      -%%
    • The service does not support node configuration.
    • -%%
    • The service does not support retrieval of default node configuration.
    • -%%
    -get_configure(Host, ServerHost, Node, From, Lang) -> - Action = fun (#pubsub_node{options = Options, - type = Type, id = NodeId}) -> - case node_call(Type, get_affiliation, [NodeId, From]) of - {result, owner} -> - Groups = ejabberd_hooks:run_fold(roster_groups, - ServerHost, [], - [ServerHost]), - {result, - [#xmlel{name = <<"pubsub">>, - attrs = [{<<"xmlns">>, ?NS_PUBSUB_OWNER}], - children = - [#xmlel{name = <<"configure">>, - attrs = nodeAttr(Node), - children = - [#xmlel{name = <<"x">>, - attrs = - [{<<"xmlns">>, - ?NS_XDATA}, - {<<"type">>, - <<"form">>}], - children = - get_configure_xfields(Type, - Options, - Lang, - Groups)}]}]}]}; - _ -> {error, ?ERR_FORBIDDEN} - end - end, - case transaction(Host, Node, Action, sync_dirty) of - {result, {_, Result}} -> {result, Result}; - Other -> Other - end. - -get_default(Host, Node, _From, Lang) -> - Type = select_type(Host, Host, Node), - Options = node_options(Type), -%% Get node option -%% The result depend of the node type plugin system. - {result, - [#xmlel{name = <<"pubsub">>, - attrs = [{<<"xmlns">>, ?NS_PUBSUB_OWNER}], - children = - [#xmlel{name = <<"default">>, attrs = [], - children = - [#xmlel{name = <<"x">>, - attrs = - [{<<"xmlns">>, ?NS_XDATA}, - {<<"type">>, <<"form">>}], - children = - get_configure_xfields(Type, Options, - Lang, [])}]}]}]}. - -get_option([], _) -> false; -get_option(Options, Var) -> - get_option(Options, Var, false). - -get_option(Options, Var, Def) -> - case lists:keysearch(Var, 1, Options) of - {value, {_Val, Ret}} -> Ret; - _ -> Def - end. - -%% Get default options from the module plugin. -node_options(Type) -> - Module = - jlib:binary_to_atom(<<(?PLUGIN_PREFIX)/binary, - Type/binary, - (?ODBC_SUFFIX)/binary>>), - case catch Module:options() of - {'EXIT', {undef, _}} -> - DefaultModule = - jlib:binary_to_atom(<<(?PLUGIN_PREFIX)/binary, - (?STDNODE)/binary, - (?ODBC_SUFFIX)/binary>>), - DefaultModule:options(); - Result -> Result - end. - -%% @spec (Host, Options) -> MaxItems -%% Host = host() -%% Options = [Option] -%% Option = {Key::atom(), Value::term()} -%% MaxItems = integer() | unlimited -%% @doc

    Return the maximum number of items for a given node.

    -%%

    Unlimited means that there is no limit in the number of items that can -%% be stored.

    -%% @todo In practice, the current data structure means that we cannot manage -%% millions of items on a given node. This should be addressed in a new -%% version. -max_items(Host, Options) -> - case get_option(Options, persist_items) of - true -> - case get_option(Options, max_items) of - false -> unlimited; - Result when Result < 0 -> 0; - Result -> Result - end; - false -> - case get_option(Options, send_last_published_item) of - never -> 0; - _ -> - case is_last_item_cache_enabled(Host) of - true -> 0; - false -> 1 - end - end - end. - --define(BOOL_CONFIG_FIELD(Label, Var), - ?BOOLXFIELD(Label, - <<"pubsub#", - (iolist_to_binary(atom_to_list(Var)))/binary>>, - (get_option(Options, Var)))). - --define(STRING_CONFIG_FIELD(Label, Var), - ?STRINGXFIELD(Label, - <<"pubsub#", - (iolist_to_binary(atom_to_list(Var)))/binary>>, - (get_option(Options, Var, <<"">>)))). - --define(INTEGER_CONFIG_FIELD(Label, Var), - ?STRINGXFIELD(Label, - <<"pubsub#", - (iolist_to_binary(atom_to_list(Var)))/binary>>, - (iolist_to_binary(integer_to_list(get_option(Options, - Var)))))). - --define(JLIST_CONFIG_FIELD(Label, Var, Opts), - ?LISTXFIELD(Label, - <<"pubsub#", - (iolist_to_binary(atom_to_list(Var)))/binary>>, - (jlib:jid_to_string(get_option(Options, Var))), - [jlib:jid_to_string(O) || O <- Opts])). - --define(ALIST_CONFIG_FIELD(Label, Var, Opts), - ?LISTXFIELD(Label, - <<"pubsub#", - (iolist_to_binary(atom_to_list(Var)))/binary>>, - (iolist_to_binary(atom_to_list(get_option(Options, - Var)))), - [iolist_to_binary(atom_to_list(O)) || O <- Opts])). - --define(LISTM_CONFIG_FIELD(Label, Var, Opts), - ?LISTMXFIELD(Label, - <<"pubsub#", - (iolist_to_binary(atom_to_list(Var)))/binary>>, - (get_option(Options, Var)), Opts)). - --define(NLIST_CONFIG_FIELD(Label, Var), - ?STRINGMXFIELD(Label, - <<"pubsub#", - (iolist_to_binary(atom_to_list(Var)))/binary>>, - get_option(Options, Var, []))). - -get_configure_xfields(_Type, Options, Lang, Groups) -> - [?XFIELD(<<"hidden">>, <<"">>, <<"FORM_TYPE">>, - (?NS_PUBSUB_NODE_CONFIG)), - ?BOOL_CONFIG_FIELD(<<"Deliver payloads with event notifications">>, - deliver_payloads), - ?BOOL_CONFIG_FIELD(<<"Deliver event notifications">>, - deliver_notifications), - ?BOOL_CONFIG_FIELD(<<"Notify subscribers when the node configuratio" - "n changes">>, - notify_config), - ?BOOL_CONFIG_FIELD(<<"Notify subscribers when the node is " - "deleted">>, - notify_delete), - ?BOOL_CONFIG_FIELD(<<"Notify subscribers when items are removed " - "from the node">>, - notify_retract), - ?BOOL_CONFIG_FIELD(<<"Persist items to storage">>, - persist_items), - ?STRING_CONFIG_FIELD(<<"A friendly name for the node">>, - title), - ?INTEGER_CONFIG_FIELD(<<"Max # of items to persist">>, - max_items), - ?BOOL_CONFIG_FIELD(<<"Whether to allow subscriptions">>, - subscribe), - ?ALIST_CONFIG_FIELD(<<"Specify the access model">>, - access_model, - [open, authorize, presence, roster, whitelist]), - ?LISTM_CONFIG_FIELD(<<"Roster groups allowed to subscribe">>, - roster_groups_allowed, Groups), - ?ALIST_CONFIG_FIELD(<<"Specify the publisher model">>, - publish_model, [publishers, subscribers, open]), - ?BOOL_CONFIG_FIELD(<<"Purge all items when the relevant publisher " - "goes offline">>, - purge_offline), - ?ALIST_CONFIG_FIELD(<<"Specify the event message type">>, - notification_type, [headline, normal]), - ?INTEGER_CONFIG_FIELD(<<"Max payload size in bytes">>, - max_payload_size), - ?ALIST_CONFIG_FIELD(<<"When to send the last published item">>, - send_last_published_item, - [never, on_sub, on_sub_and_presence]), - ?BOOL_CONFIG_FIELD(<<"Only deliver notifications to available " - "users">>, - presence_based_delivery), - ?NLIST_CONFIG_FIELD(<<"The collections with which a node is " - "affiliated">>, - collection)]. - -%%

    There are several reasons why the node configuration request might fail:

    -%%
      -%%
    • The service does not support node configuration.
    • -%%
    • The requesting entity does not have sufficient privileges to configure the node.
    • -%%
    • The request did not specify a node.
    • -%%
    • The node has no configuration options.
    • -%%
    • The specified node does not exist.
    • -%%
    -set_configure(Host, Node, From, Els, Lang) -> - case xml:remove_cdata(Els) of - [#xmlel{name = <<"x">>} = XEl] -> - case {xml:get_tag_attr_s(<<"xmlns">>, XEl), - xml:get_tag_attr_s(<<"type">>, XEl)} - of - {?NS_XDATA, <<"cancel">>} -> {result, []}; - {?NS_XDATA, <<"submit">>} -> - Action = fun (#pubsub_node{options = Options, - type = Type, id = NodeId} = - N) -> - case node_call(Type, get_affiliation, - [NodeId, From]) - of - {result, owner} -> - case jlib:parse_xdata_submit(XEl) of - invalid -> {error, ?ERR_BAD_REQUEST}; - XData -> - OldOpts = case Options of - [] -> - node_options(Type); - _ -> Options - end, - case set_xoption(Host, XData, - OldOpts) - of - NewOpts - when is_list(NewOpts) -> - case tree_call(Host, - set_node, - [N#pubsub_node{options - = - NewOpts}]) - of - ok -> {result, ok}; - Err -> Err - end; - Err -> Err - end - end; - _ -> {error, ?ERR_FORBIDDEN} - end - end, - case transaction(Host, Node, Action, transaction) of - {result, {TNode, ok}} -> - NodeId = TNode#pubsub_node.id, - Type = TNode#pubsub_node.type, - Options = TNode#pubsub_node.options, - broadcast_config_notification(Host, Node, NodeId, Type, - Options, Lang), - {result, []}; - Other -> Other - end; - _ -> {error, ?ERR_BAD_REQUEST} - end; - _ -> {error, ?ERR_BAD_REQUEST} - end. - -add_opt(Key, Value, Opts) -> - Opts1 = lists:keydelete(Key, 1, Opts), - [{Key, Value} | Opts1]. - --define(SET_BOOL_XOPT(Opt, Val), - BoolVal = case Val of - <<"0">> -> false; - <<"1">> -> true; - <<"false">> -> false; - <<"true">> -> true; - _ -> error - end, - case BoolVal of - error -> {error, ?ERR_NOT_ACCEPTABLE}; - _ -> - set_xoption(Host, Opts, add_opt(Opt, BoolVal, NewOpts)) - end). - --define(SET_STRING_XOPT(Opt, Val), - set_xoption(Host, Opts, add_opt(Opt, Val, NewOpts))). - --define(SET_INTEGER_XOPT(Opt, Val, Min, Max), - case catch jlib:binary_to_integer(Val) of - IVal when is_integer(IVal), IVal >= Min, IVal =< Max -> - set_xoption(Host, Opts, add_opt(Opt, IVal, NewOpts)); - _ -> {error, ?ERR_NOT_ACCEPTABLE} - end). - --define(SET_ALIST_XOPT(Opt, Val, Vals), - case lists:member(Val, - [iolist_to_binary(atom_to_list(V)) || V <- Vals]) - of - true -> - set_xoption(Host, Opts, - add_opt(Opt, jlib:binary_to_atom(Val), NewOpts)); - false -> {error, ?ERR_NOT_ACCEPTABLE} - end). - --define(SET_LIST_XOPT(Opt, Val), - set_xoption(Host, Opts, add_opt(Opt, Val, NewOpts))). - -set_xoption(_Host, [], NewOpts) -> NewOpts; -set_xoption(Host, [{<<"FORM_TYPE">>, _} | Opts], - NewOpts) -> - set_xoption(Host, Opts, NewOpts); -set_xoption(Host, - [{<<"pubsub#roster_groups_allowed">>, Value} | Opts], - NewOpts) -> - ?SET_LIST_XOPT(roster_groups_allowed, Value); -set_xoption(Host, - [{<<"pubsub#deliver_payloads">>, [Val]} | Opts], - NewOpts) -> - ?SET_BOOL_XOPT(deliver_payloads, Val); -set_xoption(Host, - [{<<"pubsub#deliver_notifications">>, [Val]} | Opts], - NewOpts) -> - ?SET_BOOL_XOPT(deliver_notifications, Val); -set_xoption(Host, - [{<<"pubsub#notify_config">>, [Val]} | Opts], - NewOpts) -> - ?SET_BOOL_XOPT(notify_config, Val); -set_xoption(Host, - [{<<"pubsub#notify_delete">>, [Val]} | Opts], - NewOpts) -> - ?SET_BOOL_XOPT(notify_delete, Val); -set_xoption(Host, - [{<<"pubsub#notify_retract">>, [Val]} | Opts], - NewOpts) -> - ?SET_BOOL_XOPT(notify_retract, Val); -set_xoption(Host, - [{<<"pubsub#persist_items">>, [Val]} | Opts], - NewOpts) -> - ?SET_BOOL_XOPT(persist_items, Val); -set_xoption(Host, - [{<<"pubsub#max_items">>, [Val]} | Opts], NewOpts) -> - MaxItems = get_max_items_node(Host), - ?SET_INTEGER_XOPT(max_items, Val, 0, MaxItems); -set_xoption(Host, - [{<<"pubsub#subscribe">>, [Val]} | Opts], NewOpts) -> - ?SET_BOOL_XOPT(subscribe, Val); -set_xoption(Host, - [{<<"pubsub#access_model">>, [Val]} | Opts], NewOpts) -> - ?SET_ALIST_XOPT(access_model, Val, - [open, authorize, presence, roster, whitelist]); -set_xoption(Host, - [{<<"pubsub#publish_model">>, [Val]} | Opts], - NewOpts) -> - ?SET_ALIST_XOPT(publish_model, Val, - [publishers, subscribers, open]); -set_xoption(Host, - [{<<"pubsub#notification_type">>, [Val]} | Opts], - NewOpts) -> - ?SET_ALIST_XOPT(notification_type, Val, - [headline, normal]); -set_xoption(Host, - [{<<"pubsub#node_type">>, [Val]} | Opts], NewOpts) -> - ?SET_ALIST_XOPT(node_type, Val, [leaf, collection]); -set_xoption(Host, - [{<<"pubsub#max_payload_size">>, [Val]} | Opts], - NewOpts) -> - ?SET_INTEGER_XOPT(max_payload_size, Val, 0, - (?MAX_PAYLOAD_SIZE)); -set_xoption(Host, - [{<<"pubsub#send_last_published_item">>, [Val]} | Opts], - NewOpts) -> - ?SET_ALIST_XOPT(send_last_published_item, Val, - [never, on_sub, on_sub_and_presence]); -set_xoption(Host, - [{<<"pubsub#presence_based_delivery">>, [Val]} | Opts], - NewOpts) -> - ?SET_BOOL_XOPT(presence_based_delivery, Val); -set_xoption(Host, - [{<<"pubsub#purge_offline">>, [Val]} | Opts], - NewOpts) -> - ?SET_BOOL_XOPT(purge_offline, Val); -set_xoption(Host, [{<<"pubsub#title">>, Value} | Opts], - NewOpts) -> - ?SET_STRING_XOPT(title, Value); -set_xoption(Host, [{<<"pubsub#type">>, Value} | Opts], - NewOpts) -> - ?SET_STRING_XOPT(type, Value); -set_xoption(Host, - [{<<"pubsub#body_xslt">>, Value} | Opts], NewOpts) -> - ?SET_STRING_XOPT(body_xslt, Value); -set_xoption(Host, - [{<<"pubsub#collection">>, Value} | Opts], NewOpts) -> -% NewValue = [string_to_node(V) || V <- Value], - ?SET_LIST_XOPT(collection, Value); -set_xoption(Host, [{<<"pubsub#node">>, [Value]} | Opts], - NewOpts) -> -% NewValue = string_to_node(Value), - ?SET_LIST_XOPT(node, Value); -set_xoption(Host, [_ | Opts], NewOpts) -> - set_xoption(Host, Opts, NewOpts). - -get_max_items_node({_, ServerHost, _}) -> - get_max_items_node(ServerHost); -get_max_items_node(Host) -> - case catch ets:lookup(gen_mod:get_module_proc(Host, - config), - max_items_node) - of - [{max_items_node, Integer}] -> Integer; - _ -> ?MAXITEMS - end. - -%%%% last item cache handling - -is_last_item_cache_enabled({_, ServerHost, _}) -> - is_last_item_cache_enabled(ServerHost); -is_last_item_cache_enabled(Host) -> - case catch ets:lookup(gen_mod:get_module_proc(Host, - config), - last_item_cache) - of - [{last_item_cache, true}] -> true; - _ -> false - end. - -set_cached_item({_, ServerHost, _}, NodeId, ItemId, - Publisher, Payload) -> - set_cached_item(ServerHost, NodeId, ItemId, Publisher, - Payload); -set_cached_item(Host, NodeId, ItemId, Publisher, - Payload) -> - case is_last_item_cache_enabled(Host) of - true -> - mnesia:dirty_write({pubsub_last_item, NodeId, ItemId, - {now(), - jlib:jid_tolower(jlib:jid_remove_resource(Publisher))}, - Payload}); - _ -> ok - end. - -unset_cached_item({_, ServerHost, _}, NodeId) -> - unset_cached_item(ServerHost, NodeId); -unset_cached_item(Host, NodeId) -> - case is_last_item_cache_enabled(Host) of - true -> mnesia:dirty_delete({pubsub_last_item, NodeId}); - _ -> ok - end. - --spec(get_cached_item/2 :: -( - Host :: mod_pubsub:host(), - NodeIdx :: mod_pubsub:nodeIdx()) - -> undefined | mod_pubsub:pubsubItem() -). -get_cached_item({_, ServerHost, _}, NodeId) -> - get_cached_item(ServerHost, NodeId); -get_cached_item(Host, NodeIdx) -> - case is_last_item_cache_enabled(Host) of - true -> - case mnesia:dirty_read({pubsub_last_item, NodeIdx}) of - [#pubsub_last_item{itemid = ItemId, creation = Creation, payload = Payload}] -> -% [{pubsub_last_item, NodeId, ItemId, Creation, -% Payload}] -> - #pubsub_item{itemid = {ItemId, NodeIdx}, - payload = Payload, creation = Creation, - modification = Creation}; - _ -> undefined - end; - _ -> undefined - end. - -%%%% plugin handling - -host(ServerHost) -> - case catch - ets:lookup(gen_mod:get_module_proc(ServerHost, config), - host) - of - [{host, Host}] -> Host; - _ -> <<"pubsub.", ServerHost/binary>> - end. - -plugins(Host) -> - case catch ets:lookup(gen_mod:get_module_proc(Host, - config), - plugins) - of - [{plugins, []}] -> [?STDNODE]; - [{plugins, PL}] -> PL; - _ -> [?STDNODE] - end. - -select_type(ServerHost, Host, Node, Type) -> - SelectedType = case Host of - {_User, _Server, _Resource} -> - case catch - ets:lookup(gen_mod:get_module_proc(ServerHost, - config), - pep_mapping) - of - [{pep_mapping, PM}] -> - proplists:get_value(Node, PM, ?PEPNODE); - _ -> ?PEPNODE - end; - _ -> Type - end, - ConfiguredTypes = plugins(ServerHost), - case lists:member(SelectedType, ConfiguredTypes) of - true -> SelectedType; - false -> hd(ConfiguredTypes) - end. - -select_type(ServerHost, Host, Node) -> - select_type(ServerHost, Host, Node, - hd(plugins(ServerHost))). - -features() -> - [% see plugin "access-authorize", % OPTIONAL - <<"access-open">>, % OPTIONAL this relates to access_model option in node_hometree - <<"access-presence">>, % OPTIONAL this relates to access_model option in node_pep - <<"access-whitelist">>, % OPTIONAL - <<"collections">>, % RECOMMENDED - <<"config-node">>, % RECOMMENDED - <<"create-and-configure">>, % RECOMMENDED - <<"item-ids">>, % RECOMMENDED - <<"last-published">>, % RECOMMENDED - <<"member-affiliation">>, % RECOMMENDED - <<"presence-notifications">>, % OPTIONAL - <<"presence-subscribe">>, % RECOMMENDED - <<"publisher-affiliation">>, % RECOMMENDED - <<"retrieve-default">>]. - - % see plugin "retrieve-items", % RECOMMENDED - % see plugin "retrieve-subscriptions", % RECOMMENDED - %TODO "shim", % OPTIONAL - % see plugin "subscribe", % REQUIRED - % see plugin "subscription-options", % OPTIONAL - % see plugin "subscription-notifications" % OPTIONAL - -features(Type) -> - Module = - jlib:binary_to_atom(<<(?PLUGIN_PREFIX)/binary, - Type/binary, - (?ODBC_SUFFIX)/binary>>), - features() ++ - case catch Module:features() of - {'EXIT', {undef, _}} -> []; - Result -> Result - end. - -features(Host, <<>>) -> - lists:usort(lists:foldl(fun (Plugin, Acc) -> - Acc ++ features(Plugin) - end, - [], plugins(Host))); -features(Host, Node) -> - Action = fun (#pubsub_node{type = Type}) -> - {result, features(Type)} - end, - case transaction(Host, Node, Action, sync_dirty) of - {result, Features} -> - lists:usort(features() ++ Features); - _ -> features() - end. - -%% @spec (Host, Type, NodeId) -> [ljid()] -%% NodeId = pubsubNodeId() -%% @doc

    Return list of node owners.

    -node_owners(Host, Type, NodeId) -> - case node_action(Host, Type, get_node_affiliations, [NodeId]) of - {result, Affiliations} -> - lists:foldl( - fun({LJID, owner}, Acc) -> [LJID|Acc]; - (_, Acc) -> Acc - end, [], Affiliations); - _ -> - [] - end. -node_owners_call(Type, NodeId) -> - case node_call(Type, get_node_affiliations, [NodeId]) of - {result, Affiliations} -> - lists:foldl( - fun({LJID, owner}, Acc) -> [LJID|Acc]; - (_, Acc) -> Acc - end, [], Affiliations); - _ -> - [] - end. - -%% @doc

    node tree plugin call.

    -tree_call({_User, Server, _Resource}, Function, Args) -> - tree_call(Server, Function, Args); -tree_call(Host, Function, Args) -> - ?DEBUG("tree_call ~p ~p ~p", [Host, Function, Args]), - Module = case catch - ets:lookup(gen_mod:get_module_proc(Host, config), - nodetree) - of - [{nodetree, N}] -> N; - _ -> - jlib:binary_to_atom(<<(?TREE_PREFIX)/binary, - (?STDTREE)/binary, - (?ODBC_SUFFIX)/binary>>) - end, - catch apply(Module, Function, Args). - -tree_action(Host, Function, Args) -> - ?DEBUG("tree_action ~p ~p ~p", [Host, Function, Args]), - Fun = fun () -> tree_call(Host, Function, Args) end, - case catch ejabberd_odbc:sql_bloc(odbc_conn(Host), Fun) of - {atomic, Result} -> - Result; - {aborted, Reason} -> - ?ERROR_MSG("transaction return internal error: ~p~n",[{aborted, Reason}]), - {error, ?ERR_INTERNAL_SERVER_ERROR} - end. - -%% @doc

    node plugin call.

    -node_call(Type, Function, Args) -> - ?DEBUG("node_call ~p ~p ~p", [Type, Function, Args]), - Module = - jlib:binary_to_atom(<<(?PLUGIN_PREFIX)/binary, - Type/binary, - (?ODBC_SUFFIX)/binary>>), - case apply(Module, Function, Args) of - {result, Result} -> {result, Result}; - {error, Error} -> {error, Error}; - {'EXIT', {undef, Undefined}} -> - case Type of - ?STDNODE -> {error, {undef, Undefined}}; - _ -> node_call(?STDNODE, Function, Args) - end; - {'EXIT', Reason} -> {error, Reason}; - Result -> - {result, - Result} %% any other return value is forced as result - end. - -node_action(Host, Type, Function, Args) -> - ?DEBUG("node_action ~p ~p ~p ~p", - [Host, Type, Function, Args]), - transaction(Host, fun () -> node_call(Type, Function, Args) end, - sync_dirty). - -%% @doc

    plugin transaction handling.

    -transaction(Host, Node, Action, Trans) -> - transaction(Host, fun () -> - case tree_call(Host, get_node, [Host, Node]) of - N when is_record(N, pubsub_node) -> - case Action(N) of - {result, Result} -> {result, {N, Result}}; - {atomic, {result, Result}} -> - {result, {N, Result}}; - Other -> Other - end; - Error -> Error - end - end, - Trans). - -transaction_on_nodes(Host, Action, Trans) -> - transaction(Host, fun () -> - {result, - lists:foldl(Action, [], - tree_call(Host, get_nodes, [Host]))} - end, - Trans). - -transaction(Host, Fun, Trans) -> - transaction_retry(Host, Fun, Trans, 2). -transaction_retry(Host, Fun, Trans, Count) -> - SqlFun = case Trans of - transaction -> sql_transaction; - _ -> sql_bloc - end, - case catch ejabberd_odbc:SqlFun(odbc_conn(Host), Fun) of - {result, Result} -> {result, Result}; - {error, Error} -> {error, Error}; - {atomic, {result, Result}} -> {result, Result}; - {atomic, {error, Error}} -> {error, Error}; - {aborted, Reason} -> - ?ERROR_MSG("transaction return internal error: ~p~n", - [{aborted, Reason}]), - {error, ?ERR_INTERNAL_SERVER_ERROR}; - {'EXIT', {timeout, _} = Reason} -> - case Count of - 0 -> - ?ERROR_MSG("transaction return internal error: ~p~n", [{'EXIT', Reason}]), - {error, ?ERR_INTERNAL_SERVER_ERROR}; - N -> - erlang:yield(), - transaction_retry(Host, Fun, Trans, N-1) - end; - {'EXIT', Reason} -> - ?ERROR_MSG("transaction return internal error: ~p~n", - [{'EXIT', Reason}]), - {error, ?ERR_INTERNAL_SERVER_ERROR}; - Other -> - ?ERROR_MSG("transaction return internal error: ~p~n", - [Other]), - {error, ?ERR_INTERNAL_SERVER_ERROR} - end. - -odbc_conn({_U, Host, _R})-> Host; -odbc_conn(<<$., Host/binary>>) -> Host; -odbc_conn(<<_, Host/binary>>) -> odbc_conn(Host). - -%% escape value for database storage -escape({_U, _H, _R}=JID)-> - ejabberd_odbc:escape(jlib:jid_to_string(JID)); -escape(Value)-> - ejabberd_odbc:escape(Value). - -%%%% helpers - -%% Add pubsub-specific error element -extended_error(Error, Ext) -> - extended_error(Error, Ext, - [{<<"xmlns">>, ?NS_PUBSUB_ERRORS}]). - -extended_error(Error, unsupported, Feature) -> -%% Give a uniq identifier - extended_error(Error, <<"unsupported">>, - [{<<"xmlns">>, ?NS_PUBSUB_ERRORS}, - {<<"feature">>, Feature}]); -extended_error(#xmlel{name = Error, attrs = Attrs, - children = SubEls}, - Ext, ExtAttrs) -> - #xmlel{name = Error, attrs = Attrs, - children = - lists:reverse([#xmlel{name = Ext, attrs = ExtAttrs, - children = []} - | SubEls])}. - --spec(uniqid/0 :: () -> mod_pubsub:itemId()). -uniqid() -> - {T1, T2, T3} = now(), - iolist_to_binary(io_lib:fwrite("~.16B~.16B~.16B", [T1, T2, T3])). - -nodeAttr(Node) -> [{<<"node">>, Node}]. - -itemAttr([]) -> []; -itemAttr(ItemId) -> [{<<"id">>, ItemId}]. - -itemsEls(Items) -> - lists:map(fun (#pubsub_item{itemid = {ItemId, _}, payload = Payload}) -> - #xmlel{name = <<"item">>, attrs = itemAttr(ItemId), children = Payload} - end, Items). - -add_message_type(#xmlel{name = <<"message">>, attrs = Attrs, children = Els}, - Type) -> - #xmlel{name = <<"message">>, - attrs = [{<<"type">>, Type} | Attrs], children = Els}; -add_message_type(XmlEl, _Type) -> XmlEl. - -%% Place of changed at the bottom of the stanza -%% cf. http://xmpp.org/extensions/xep-0060.html#publisher-publish-success-subid -%% -%% "[SHIM Headers] SHOULD be included after the event notification information -%% (i.e., as the last child of the stanza)". - -add_shim_headers(Stanza, HeaderEls) -> - add_headers(Stanza, <<"headers">>, ?NS_SHIM, HeaderEls). - -add_extended_headers(Stanza, HeaderEls) -> - add_headers(Stanza, <<"addresses">>, ?NS_ADDRESS, - HeaderEls). - -add_headers(#xmlel{name = Name, attrs = Attrs, children = Els}, - HeaderName, HeaderNS, HeaderEls) -> - HeaderEl = #xmlel{name = HeaderName, - attrs = [{<<"xmlns">>, HeaderNS}], - children = HeaderEls}, - #xmlel{name = Name, attrs = Attrs, - children = lists:append(Els, [HeaderEl])}. - -%% Removed multiple
    Foo
    elements -%% Didn't seem compliant, but not sure. Confirmation required. -%% cf. http://xmpp.org/extensions/xep-0248.html#notify -%% -%% "If an item is published to a node which is also included by a collection, -%% and an entity is subscribed to that collection with a subscription type of -%% "items" (Is there a way to check that currently ?), then the notifications -%% generated by the service MUST contain additional information. The -%% element contained in the notification message MUST specify the node -%% identifier of the node that generated the notification (not the collection) -%% and the element MUST contain a SHIM header that specifies the node -%% identifier of the collection". - -collection_shim(Node) -> - [#xmlel{name = <<"header">>, - attrs = [{<<"name">>, <<"Collection">>}], - children = [{xmlcdata, Node}]}]. - -subid_shim(SubIDs) -> - [#xmlel{name = <<"header">>, - attrs = [{<<"name">>, <<"SubID">>}], - children = [{xmlcdata, SubID}]} - || SubID <- SubIDs]. - -%% The argument is a list of Jids because this function could be used -%% with the 'pubsub#replyto' (type=jid-multi) node configuration. - -extended_headers(Jids) -> - [#xmlel{name = <<"address">>, - attrs = [{<<"type">>, <<"replyto">>}, {<<"jid">>, Jid}], - children = []} - || Jid <- Jids]. - -on_user_offline(_, JID, _) -> - {User, Server, Resource} = jlib:jid_tolower(JID), - case ejabberd_sm:get_user_resources(User, Server) of - [] -> purge_offline({User, Server, Resource}); - _ -> true - end. - -purge_offline({User, Server, _} = LJID) -> - Host = host(element(2, LJID)), - Plugins = plugins(Host), - Result = lists:foldl(fun (Type, {Status, Acc}) -> - case lists:member(<<"retrieve-affiliations">>, - features(Type)) - of - false -> - {{error, - extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, - unsupported, - <<"retrieve-affiliations">>)}, - Acc}; - true -> - {result, Affiliations} = - node_action(Host, Type, - get_entity_affiliations, - [Host, LJID]), - {Status, [Affiliations | Acc]} - end - end, - {ok, []}, Plugins), - case Result of - {ok, Affiliations} -> - lists:foreach(fun ({#pubsub_node{nodeid = {_, NodeId}, - options = Options, type = Type}, - Affiliation}) - when Affiliation == owner orelse - Affiliation == publisher -> - Action = fun (#pubsub_node{type = NType, - id = NodeIdx}) -> - node_call(NType, get_items, - [NodeIdx, - service_jid(Host)]) - end, - case transaction(Host, NodeId, Action, - sync_dirty) - of - {result, {_, []}} -> true; - {result, {_, Items}} -> - Features = features(Type), - case {lists:member(<<"retract-items">>, - Features), - lists:member(<<"persistent-items">>, - Features), - get_option(Options, persist_items), - get_option(Options, purge_offline)} - of - {true, true, true, true} -> - ForceNotify = get_option(Options, - notify_retract), - lists:foreach(fun - (#pubsub_item{itemid - = - {ItemId, - _}, - modification - = - {_, - Modification}}) -> - case - Modification - of - {User, Server, - _} -> - delete_item(Host, - NodeId, - LJID, - ItemId, - ForceNotify); - _ -> true - end; - (_) -> true - end, - Items); - _ -> true - end; - Error -> Error - end; - (_) -> true - end, - lists:usort(lists:flatten(Affiliations))); - {Error, _} -> ?DEBUG("on_user_offline ~p", [Error]) - end. diff --git a/src/mod_pubsub_opt.erl b/src/mod_pubsub_opt.erl new file mode 100644 index 000000000..612abf35b --- /dev/null +++ b/src/mod_pubsub_opt.erl @@ -0,0 +1,125 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_pubsub_opt). + +-export([access_createnode/1]). +-export([db_type/1]). +-export([default_node_config/1]). +-export([force_node_config/1]). +-export([host/1]). +-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]). +-export([name/1]). +-export([nodetree/1]). +-export([pep_mapping/1]). +-export([plugins/1]). +-export([vcard/1]). + +-spec access_createnode(gen_mod:opts() | global | binary()) -> 'all' | acl:acl(). +access_createnode(Opts) when is_map(Opts) -> + gen_mod:get_opt(access_createnode, Opts); +access_createnode(Host) -> + gen_mod:get_module_opt(Host, mod_pubsub, access_createnode). + +-spec db_type(gen_mod:opts() | global | binary()) -> atom(). +db_type(Opts) when is_map(Opts) -> + gen_mod:get_opt(db_type, Opts); +db_type(Host) -> + gen_mod:get_module_opt(Host, mod_pubsub, db_type). + +-spec default_node_config(gen_mod:opts() | global | binary()) -> [{atom(),atom() | integer()}]. +default_node_config(Opts) when is_map(Opts) -> + gen_mod:get_opt(default_node_config, Opts); +default_node_config(Host) -> + gen_mod:get_module_opt(Host, mod_pubsub, default_node_config). + +-spec force_node_config(gen_mod:opts() | global | binary()) -> [{misc:re_mp(),[{atom(),atom() | integer()}]}]. +force_node_config(Opts) when is_map(Opts) -> + gen_mod:get_opt(force_node_config, Opts); +force_node_config(Host) -> + gen_mod:get_module_opt(Host, mod_pubsub, force_node_config). + +-spec host(gen_mod:opts() | global | binary()) -> binary(). +host(Opts) when is_map(Opts) -> + gen_mod:get_opt(host, Opts); +host(Host) -> + gen_mod:get_module_opt(Host, mod_pubsub, host). + +-spec hosts(gen_mod:opts() | global | binary()) -> [binary()]. +hosts(Opts) when is_map(Opts) -> + gen_mod:get_opt(hosts, Opts); +hosts(Host) -> + gen_mod:get_module_opt(Host, mod_pubsub, hosts). + +-spec ignore_pep_from_offline(gen_mod:opts() | global | binary()) -> boolean(). +ignore_pep_from_offline(Opts) when is_map(Opts) -> + gen_mod:get_opt(ignore_pep_from_offline, Opts); +ignore_pep_from_offline(Host) -> + gen_mod:get_module_opt(Host, mod_pubsub, ignore_pep_from_offline). + +-spec last_item_cache(gen_mod:opts() | global | binary()) -> boolean(). +last_item_cache(Opts) when is_map(Opts) -> + gen_mod:get_opt(last_item_cache, Opts); +last_item_cache(Host) -> + gen_mod:get_module_opt(Host, mod_pubsub, last_item_cache). + +-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) -> + gen_mod:get_module_opt(Host, mod_pubsub, max_items_node). + +-spec max_nodes_discoitems(gen_mod:opts() | global | binary()) -> 'infinity' | non_neg_integer(). +max_nodes_discoitems(Opts) when is_map(Opts) -> + gen_mod:get_opt(max_nodes_discoitems, Opts); +max_nodes_discoitems(Host) -> + gen_mod:get_module_opt(Host, mod_pubsub, max_nodes_discoitems). + +-spec max_subscriptions_node(gen_mod:opts() | global | binary()) -> 'undefined' | non_neg_integer(). +max_subscriptions_node(Opts) when is_map(Opts) -> + gen_mod:get_opt(max_subscriptions_node, Opts); +max_subscriptions_node(Host) -> + gen_mod:get_module_opt(Host, mod_pubsub, max_subscriptions_node). + +-spec name(gen_mod:opts() | global | binary()) -> binary(). +name(Opts) when is_map(Opts) -> + gen_mod:get_opt(name, Opts); +name(Host) -> + gen_mod:get_module_opt(Host, mod_pubsub, name). + +-spec nodetree(gen_mod:opts() | global | binary()) -> binary(). +nodetree(Opts) when is_map(Opts) -> + gen_mod:get_opt(nodetree, Opts); +nodetree(Host) -> + gen_mod:get_module_opt(Host, mod_pubsub, nodetree). + +-spec pep_mapping(gen_mod:opts() | global | binary()) -> [{binary(),binary()}]. +pep_mapping(Opts) when is_map(Opts) -> + gen_mod:get_opt(pep_mapping, Opts); +pep_mapping(Host) -> + gen_mod:get_module_opt(Host, mod_pubsub, pep_mapping). + +-spec plugins(gen_mod:opts() | global | binary()) -> [binary()]. +plugins(Opts) when is_map(Opts) -> + gen_mod:get_opt(plugins, Opts); +plugins(Host) -> + gen_mod:get_module_opt(Host, mod_pubsub, plugins). + +-spec vcard(gen_mod:opts() | global | binary()) -> 'undefined' | tuple(). +vcard(Opts) when is_map(Opts) -> + gen_mod:get_opt(vcard, Opts); +vcard(Host) -> + gen_mod:get_module_opt(Host, mod_pubsub, vcard). + diff --git a/src/mod_pubsub_serverinfo.erl b/src/mod_pubsub_serverinfo.erl new file mode 100644 index 000000000..45a24a31e --- /dev/null +++ b/src/mod_pubsub_serverinfo.erl @@ -0,0 +1,391 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_pubsub_serverinfo.erl +%%% Author : Stefan Strigler +%%% Purpose : Exposes server information over Pub/Sub +%%% Created : 26 Dec 2023 by Guus der Kinderen +%%% +%%% +%%% ejabberd, Copyright (C) 2023 - 2025 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_pubsub_serverinfo). +-author('stefan@strigler.de'). + +-protocol({xep, 485, '0.1.1', '25.07', "complete", ""}). + +-behaviour(gen_mod). +-behaviour(gen_server). + +-include("logger.hrl"). +-include("translate.hrl"). + +-include_lib("xmpp/include/xmpp.hrl"). + +%% gen_mod callbacks. +-export([start/2, stop/1, depends/2, mod_options/1, mod_opt_type/1, get_local_features/5, mod_doc/0]). +-export([init/1, handle_cast/2, handle_call/3, handle_info/2, terminate/2]). +-export([in_auth_result/3, out_auth_result/2, get_info/5]). + +-define(NS_URN_SERVERINFO, <<"urn:xmpp:serverinfo:0">>). +-define(PUBLIC_HOSTS_URL, <<"https://data.xmpp.net/providers/v2/providers-Ds.json">>). + +-record(state, {host, pubsub_host, node, monitors = #{}, timer = undefined, public_hosts = []}). + +%% @format-begin + +start(Host, Opts) -> + case pubsub_host(Host, Opts) of + {error, _Reason} = Error -> + Error; + PubsubHost -> + ejabberd_hooks:add(disco_local_features, Host, ?MODULE, get_local_features, 50), + ejabberd_hooks:add(disco_info, Host, ?MODULE, get_info, 50), + ejabberd_hooks:add(s2s_out_auth_result, Host, ?MODULE, out_auth_result, 50), + ejabberd_hooks:add(s2s_in_auth_result, Host, ?MODULE, in_auth_result, 50), + gen_mod:start_child(?MODULE, Host, PubsubHost) + end. + +stop(Host) -> + ejabberd_hooks:delete(disco_local_features, Host, ?MODULE, get_local_features, 50), + ejabberd_hooks:delete(disco_info, Host, ?MODULE, get_info, 50), + ejabberd_hooks:delete(s2s_out_auth_result, Host, ?MODULE, out_auth_result, 50), + ejabberd_hooks:delete(s2s_in_auth_result, Host, ?MODULE, in_auth_result, 50), + gen_mod:stop_child(?MODULE, Host). + +init([Host, PubsubHost]) -> + TRef = + timer:send_interval( + timer:minutes(5), self(), update_pubsub), + Monitors = init_monitors(Host), + PublicHosts = fetch_public_hosts(), + State = + #state{host = Host, + pubsub_host = PubsubHost, + node = <<"serverinfo">>, + timer = TRef, + monitors = Monitors, + public_hosts = PublicHosts}, + self() ! update_pubsub, + {ok, State}. + +-spec init_monitors(binary()) -> map(). +init_monitors(Host) -> + lists:foldl(fun(Domain, Monitors) -> + RefIn = make_ref(), % just dummies + RefOut = make_ref(), + maps:merge(#{RefIn => {incoming, {Host, Domain, true}}, + RefOut => {outgoing, {Host, Domain, true}}}, + Monitors) + end, + #{}, + ejabberd_option:hosts() -- [Host]). + +-spec fetch_public_hosts() -> list(). +fetch_public_hosts() -> + try + {ok, {{_, 200, _}, _Headers, Body}} = + httpc:request(get, {?PUBLIC_HOSTS_URL, []}, [{timeout, 1000}], [{body_format, binary}]), + case misc:json_decode(Body) of + PublicHosts when is_list(PublicHosts) -> + PublicHosts; + Other -> + ?WARNING_MSG("Parsed JSON for public hosts was not a list: ~p", [Other]), + [] + end + catch + E:R -> + ?WARNING_MSG("Failed fetching public hosts (~p): ~p", [E, R]), + [] + end. + +handle_cast({Event, Domain, Pid}, #state{host = Host, monitors = Mons} = State) + when Event == register_in; Event == register_out -> + Ref = monitor(process, Pid), + IsPublic = check_if_public(Domain, State), + NewMons = maps:put(Ref, {event_to_dir(Event), {Host, Domain, IsPublic}}, Mons), + {noreply, State#state{monitors = NewMons}}; +handle_cast(_, State) -> + {noreply, State}. + +event_to_dir(register_in) -> + incoming; +event_to_dir(register_out) -> + outgoing. + +handle_call(pubsub_host, _From, #state{pubsub_host = PubsubHost} = State) -> + {reply, {ok, PubsubHost}, State}; +handle_call(_Request, _From, State) -> + {noreply, State}. + +handle_info({iq_reply, IQReply, {LServer, RServer}}, #state{monitors = Mons} = State) -> + case IQReply of + #iq{type = result, sub_els = [El]} -> + case xmpp:decode(El) of + #disco_info{features = Features} -> + case lists:member(?NS_URN_SERVERINFO, Features) of + true -> + NewMons = + maps:fold(fun (Ref, {Dir, {LServer0, RServer0, _}}, Acc) + when LServer == LServer0, RServer == RServer0 -> + maps:put(Ref, + {Dir, {LServer, RServer, true}}, + Acc); + (Ref, Other, Acc) -> + maps:put(Ref, Other, Acc) + end, + #{}, + Mons), + {noreply, State#state{monitors = NewMons}}; + _ -> + {noreply, State} + end; + _ -> + {noreply, State} + end; + _ -> + {noreply, State} + end; +handle_info(update_pubsub, State) -> + update_pubsub(State), + {noreply, State}; +handle_info({'DOWN', Mon, process, _Pid, _Info}, #state{monitors = Mons} = State) -> + {noreply, State#state{monitors = maps:remove(Mon, Mons)}}; +handle_info(_Request, State) -> + {noreply, State}. + +terminate(_Reason, #state{monitors = Mons, timer = Timer}) -> + case is_reference(Timer) of + true -> + case erlang:cancel_timer(Timer) of + false -> + receive + {timeout, Timer, _} -> + ok + after 0 -> + ok + end; + _ -> + ok + end; + _ -> + ok + end, + maps:fold(fun(Mon, _, _) -> demonitor(Mon) end, ok, Mons). + +depends(_Host, _Opts) -> + [{mod_pubsub, hard}]. + +mod_options(_Host) -> + [{pubsub_host, undefined}]. + +mod_opt_type(pubsub_host) -> + econf:either(undefined, econf:host()). + +mod_doc() -> + #{desc => + [?T("This module adds support for " + "https://xmpp.org/extensions/xep-0485.html[XEP-0485: PubSub Server Information] " + "to expose S2S information over the Pub/Sub service."), + "", + ?T("Active S2S connections are published to a local PubSub node. " + "Currently the node name is hardcoded as '\"serverinfo\"'."), + "", + ?T("Connections that support this feature are exposed with their domain names, " + "otherwise they are shown as anonymous nodes. " + "At startup a list of well known public servers is fetched. " + "Those are not shown as anonymous even if they don't support this feature."), + "", + ?T("Please note that the module only shows S2S connections established while the module is running. " + "If you install the module at runtime, run _`stop_s2s_connections`_ API or restart ejabberd " + "to force S2S reconnections that the module will detect and publish."), + "", + ?T("This module depends on _`mod_pubsub`_ and _`mod_disco`_.")], + note => "added in 25.07", + opts => + [{pubsub_host, + #{value => "undefined | string()", + desc => + ?T("Use this local PubSub host to advertise S2S connections. " + "This must be a host local to this service handled by _`mod_pubsub`_. " + "This option is only needed if your configuration has more than one host in mod_pubsub's 'hosts' option. " + "The default value is the first host defined in mod_pubsub 'hosts' option.")}}], + example => + ["modules:", " mod_pubsub_serverinfo:", " pubsub_host: custom.pubsub.domain.local"]}. + +in_auth_result(#{server_host := Host, remote_server := RServer} = State, true, _Server) -> + gen_server:cast( + gen_mod:get_module_proc(Host, ?MODULE), {register_in, RServer, self()}), + State; +in_auth_result(State, _, _) -> + State. + +out_auth_result(#{server_host := Host, remote_server := RServer} = State, true) -> + gen_server:cast( + gen_mod:get_module_proc(Host, ?MODULE), {register_out, RServer, self()}), + State; +out_auth_result(State, _) -> + State. + +check_if_public(Domain, State) -> + maybe_send_disco_info(is_public(Domain, State) orelse is_monitored(Domain, State), + Domain, + State). + +is_public(Domain, #state{public_hosts = PublicHosts}) -> + lists:member(Domain, PublicHosts). + +is_monitored(Domain, #state{host = Host, monitors = Mons}) -> + maps:size( + maps:filter(fun (_Ref, {_Dir, {LServer, RServer, IsPublic}}) + when LServer == Host, RServer == Domain -> + IsPublic; + (_Ref, _Other) -> + false + end, + Mons)) + =/= 0. + +maybe_send_disco_info(true, _Domain, _State) -> + true; +maybe_send_disco_info(false, Domain, #state{host = Host}) -> + Proc = gen_mod:get_module_proc(Host, ?MODULE), + IQ = #iq{type = get, + from = jid:make(Host), + to = jid:make(Domain), + sub_els = [#disco_info{}]}, + ejabberd_router:route_iq(IQ, {Host, Domain}, Proc), + false. + +update_pubsub(#state{host = Host, + pubsub_host = PubsubHost, + node = Node, + monitors = Mons}) -> + Map = maps:fold(fun(_, {Dir, {MyDomain, Target, IsPublic}}, Acc) -> + maps:update_with(MyDomain, + fun(Acc2) -> + maps:update_with(Target, + fun({Types, _}) -> + {Types#{Dir => true}, IsPublic} + end, + {#{Dir => true}, IsPublic}, + Acc2) + end, + #{Target => {#{Dir => true}, IsPublic}}, + Acc) + end, + #{}, + Mons), + Domains = + maps:fold(fun(MyDomain, Targets, Acc) -> + Remote = + maps:fold(fun (Remote, {Types, true}, Acc2) -> + [#pubsub_serverinfo_remote_domain{name = Remote, + type = + maps:keys(Types)} + | Acc2]; + (_HiddenRemote, {Types, false}, Acc2) -> + [#pubsub_serverinfo_remote_domain{type = + maps:keys(Types)} + | Acc2] + end, + [], + Targets), + [#pubsub_serverinfo_domain{name = MyDomain, remote_domain = Remote} | Acc] + end, + [], + Map), + + PubOpts = [{persist_items, true}, {max_items, 1}, {access_model, open}], + ?DEBUG("Publishing serverinfo pubsub item on ~s: ~p", [PubsubHost, Domains]), + mod_pubsub:publish_item(PubsubHost, + Host, + Node, + jid:make(Host), + <<"current">>, + [xmpp:encode(#pubsub_serverinfo{domain = Domains})], + PubOpts, + all). + +get_local_features({error, _} = Acc, _From, _To, _Node, _Lang) -> + Acc; +get_local_features(Acc, _From, _To, Node, _Lang) when Node == <<>> -> + case Acc of + {result, Features} -> + {result, [?NS_URN_SERVERINFO | Features]}; + empty -> + {result, [?NS_URN_SERVERINFO]} + end; +get_local_features(Acc, _From, _To, _Node, _Lang) -> + Acc. + +get_info(Acc, Host, Mod, Node, Lang) + when Mod == undefined orelse Mod == mod_disco, Node == <<"">> -> + case mod_disco:get_info(Acc, Host, Mod, Node, Lang) of + [#xdata{fields = Fields} = XD | Rest] -> + PubsubHost = pubsub_host(Host), + NodeField = + #xdata_field{var = <<"serverinfo-pubsub-node">>, + values = [<<"xmpp:", PubsubHost/binary, "?;node=serverinfo">>]}, + {stop, [XD#xdata{fields = Fields ++ [NodeField]} | Rest]}; + _ -> + Acc + end; +get_info(Acc, Host, Mod, Node, _Lang) when Node == <<"">>, is_atom(Mod) -> + PubsubHost = pubsub_host(Host), + [#xdata{type = result, + fields = + [#xdata_field{type = hidden, + var = <<"FORM_TYPE">>, + values = [?NS_SERVERINFO]}, + #xdata_field{var = <<"serverinfo-pubsub-node">>, + values = [<<"xmpp:", PubsubHost/binary, "?;node=serverinfo">>]}]} + | Acc]; +get_info(Acc, _Host, _Mod, _Node, _Lang) -> + Acc. + +pubsub_host(Host) -> + {ok, PubsubHost} = + gen_server:call( + gen_mod:get_module_proc(Host, ?MODULE), pubsub_host), + PubsubHost. + +pubsub_host(Host, Opts) -> + case gen_mod:get_opt(pubsub_host, Opts) of + undefined -> + PubsubHost = hd(get_mod_pubsub_hosts(Host)), + ?INFO_MSG("No pubsub_host in configuration for ~p, choosing ~s", [?MODULE, PubsubHost]), + PubsubHost; + PubsubHost -> + case check_pubsub_host_exists(Host, PubsubHost) of + true -> + PubsubHost; + false -> + {error, {pubsub_host_does_not_exist, PubsubHost}} + end + end. + +check_pubsub_host_exists(Host, PubsubHost) -> + lists:member(PubsubHost, get_mod_pubsub_hosts(Host)). + +get_mod_pubsub_hosts(Host) -> + case gen_mod:get_module_opt(Host, mod_pubsub, hosts) of + [] -> + [gen_mod:get_module_opt(Host, mod_pubsub, host)]; + PubsubHosts -> + PubsubHosts + end. diff --git a/src/mod_pubsub_serverinfo_opt.erl b/src/mod_pubsub_serverinfo_opt.erl new file mode 100644 index 000000000..731715f3c --- /dev/null +++ b/src/mod_pubsub_serverinfo_opt.erl @@ -0,0 +1,13 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_pubsub_serverinfo_opt). + +-export([pubsub_host/1]). + +-spec pubsub_host(gen_mod:opts() | global | binary()) -> 'undefined' | binary(). +pubsub_host(Opts) when is_map(Opts) -> + gen_mod:get_opt(pubsub_host, Opts); +pubsub_host(Host) -> + gen_mod:get_module_opt(Host, mod_pubsub_serverinfo, pubsub_host). + diff --git a/src/mod_pubsub_sql.erl b/src/mod_pubsub_sql.erl new file mode 100644 index 000000000..59b22c110 --- /dev/null +++ b/src/mod_pubsub_sql.erl @@ -0,0 +1,117 @@ +%%%---------------------------------------------------------------------- +%%% ejabberd, Copyright (C) 2002-2025 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_pubsub_sql). + +%% API +-export([init/3]). +-export([sql_schemas/0]). + +-include("ejabberd_sql_pt.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, ServerHost, _Opts) -> + ejabberd_sql_schema:update_schema(ServerHost, ?MODULE, sql_schemas()), + ok. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +sql_schemas() -> + [#sql_schema{ + version = 1, + tables = + [#sql_table{ + name = <<"pubsub_node">>, + columns = + [#sql_column{name = <<"host">>, type = text}, + #sql_column{name = <<"node">>, type = text}, + #sql_column{name = <<"parent">>, type = text, + default = true}, + #sql_column{name = <<"plugin">>, type = text}, + #sql_column{name = <<"nodeid">>, type = bigserial}], + indices = [#sql_index{ + columns = [<<"nodeid">>], + unique = true}, + #sql_index{ + columns = [<<"parent">>]}, + #sql_index{ + columns = [<<"host">>, <<"node">>], + unique = true}]}, + #sql_table{ + name = <<"pubsub_node_option">>, + columns = + [#sql_column{name = <<"nodeid">>, type = bigint, + opts = [#sql_references{ + table = <<"pubsub_node">>, + column = <<"nodeid">>}]}, + #sql_column{name = <<"name">>, type = text}, + #sql_column{name = <<"val">>, type = text}], + indices = [#sql_index{columns = [<<"nodeid">>]}]}, + #sql_table{ + name = <<"pubsub_node_owner">>, + columns = + [#sql_column{name = <<"nodeid">>, type = bigint, + opts = [#sql_references{ + table = <<"pubsub_node">>, + column = <<"nodeid">>}]}, + #sql_column{name = <<"owner">>, type = text}], + indices = [#sql_index{columns = [<<"nodeid">>]}]}, + #sql_table{ + name = <<"pubsub_state">>, + columns = + [#sql_column{name = <<"nodeid">>, type = bigint, + opts = [#sql_references{ + table = <<"pubsub_node">>, + column = <<"nodeid">>}]}, + #sql_column{name = <<"jid">>, type = text}, + #sql_column{name = <<"affiliation">>, type = {char, 1}}, + #sql_column{name = <<"subscriptions">>, type = text, + default = true}, + #sql_column{name = <<"stateid">>, type = bigserial}], + indices = [#sql_index{columns = [<<"stateid">>], + unique = true}, + #sql_index{columns = [<<"jid">>]}, + #sql_index{columns = [<<"nodeid">>, <<"jid">>], + unique = true}]}, + #sql_table{ + name = <<"pubsub_item">>, + columns = + [#sql_column{name = <<"nodeid">>, type = bigint, + opts = [#sql_references{ + table = <<"pubsub_node">>, + column = <<"nodeid">>}]}, + #sql_column{name = <<"itemid">>, type = text}, + #sql_column{name = <<"publisher">>, type = text}, + #sql_column{name = <<"creation">>, type = {text, 32}}, + #sql_column{name = <<"modification">>, type = {text, 32}}, + #sql_column{name = <<"payload">>, type = {text, big}, + default = true}], + indices = [#sql_index{columns = [<<"nodeid">>, <<"itemid">>], + unique = true}, + #sql_index{columns = [<<"itemid">>]}]}, + #sql_table{ + name = <<"pubsub_subscription_opt">>, + columns = + [#sql_column{name = <<"subid">>, type = text}, + #sql_column{name = <<"opt_name">>, type = {text, 32}}, + #sql_column{name = <<"opt_value">>, type = text}], + indices = [#sql_index{columns = [<<"subid">>, <<"opt_name">>], + unique = true}]}]}]. diff --git a/src/mod_push.erl b/src/mod_push.erl new file mode 100644 index 000000000..fb5ba1be4 --- /dev/null +++ b/src/mod_push.erl @@ -0,0 +1,799 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_push.erl +%%% Author : Holger Weiss +%%% Purpose : Push Notifications (XEP-0357) +%%% Created : 15 Jul 2017 by Holger Weiss +%%% +%%% +%%% ejabberd, Copyright (C) 2017-2025 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_push). +-author('holger@zedat.fu-berlin.de'). +-protocol({xep, 357, '0.2', '17.08', "complete", ""}). + +-behaviour(gen_mod). + +%% gen_mod callbacks. +-export([start/2, stop/1, reload/3, mod_opt_type/1, mod_options/1, depends/2]). +-export([mod_doc/0]). +%% ejabberd_hooks callbacks. +-export([disco_sm_features/5, c2s_session_pending/1, c2s_copy_session/2, + c2s_session_resumed/1, c2s_handle_cast/2, c2s_stanza/3, mam_message/7, + offline_message/1, remove_user/2]). + +%% gen_iq_handler callback. +-export([process_iq/1]). + +%% ejabberd command. +-export([get_commands_spec/0, delete_old_sessions/1]). + +%% API (used by mod_push_keepalive). +-export([notify/3, notify/5, notify/7, is_incoming_chat_msg/1]). + +%% For IQ callbacks +-export([delete_session/3]). + +-include("ejabberd_commands.hrl"). +-include("logger.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). +-include("translate.hrl"). + +-define(PUSH_CACHE, push_cache). + +-type c2s_state() :: ejabberd_c2s:state(). +-type push_session_id() :: erlang:timestamp(). +-type push_session() :: {push_session_id(), ljid(), binary(), xdata()}. +-type err_reason() :: notfound | db_failure. +-type direction() :: send | recv | undefined. + +-callback init(binary(), gen_mod:opts()) + -> any(). +-callback store_session(binary(), binary(), push_session_id(), jid(), binary(), + xdata()) + -> {ok, push_session()} | {error, err_reason()}. +-callback lookup_session(binary(), binary(), jid(), binary()) + -> {ok, push_session()} | {error, err_reason()}. +-callback lookup_session(binary(), binary(), push_session_id()) + -> {ok, push_session()} | {error, err_reason()}. +-callback lookup_sessions(binary(), binary(), jid()) + -> {ok, [push_session()]} | {error, err_reason()}. +-callback lookup_sessions(binary(), binary()) + -> {ok, [push_session()]} | {error, err_reason()}. +-callback lookup_sessions(binary()) + -> {ok, [push_session()]} | {error, err_reason()}. +-callback delete_session(binary(), binary(), push_session_id()) + -> ok | {error, err_reason()}. +-callback delete_old_sessions(binary() | global, erlang:timestamp()) + -> ok | {error, err_reason()}. +-callback use_cache(binary()) + -> boolean(). +-callback cache_nodes(binary()) + -> [node()]. + +-optional_callbacks([use_cache/1, cache_nodes/1]). + +%%-------------------------------------------------------------------- +%% gen_mod callbacks. +%%-------------------------------------------------------------------- +-spec start(binary(), gen_mod:opts()) -> {ok, [gen_mod:registration()]}. +start(Host, Opts) -> + Mod = gen_mod:db_mod(Opts, ?MODULE), + Mod:init(Host, Opts), + init_cache(Mod, Host, Opts), + {ok, [{commands, get_commands_spec()}, + {iq_handler, ejabberd_sm, ?NS_PUSH_0, process_iq}, + {hook, disco_sm_features, disco_sm_features, 50}, + {hook, c2s_session_pending, c2s_session_pending, 50}, + {hook, c2s_copy_session, c2s_copy_session, 50}, + {hook, c2s_session_resumed, c2s_session_resumed, 50}, + {hook, c2s_handle_cast, c2s_handle_cast, 50}, + {hook, c2s_handle_send, c2s_stanza, 50}, + {hook, store_mam_message, mam_message, 50}, + {hook, offline_message_hook, offline_message, 55}, + {hook, remove_user, remove_user, 50}]}. + + +-spec stop(binary()) -> ok. +stop(_Host) -> + ok. + +-spec reload(binary(), gen_mod:opts(), gen_mod:opts()) -> ok. +reload(Host, NewOpts, OldOpts) -> + NewMod = gen_mod:db_mod(NewOpts, ?MODULE), + OldMod = gen_mod:db_mod(OldOpts, ?MODULE), + if NewMod /= OldMod -> + NewMod:init(Host, NewOpts); + true -> + ok + end, + init_cache(NewMod, Host, NewOpts), + ok. + +-spec depends(binary(), gen_mod:opts()) -> [{module(), hard | soft}]. +depends(_Host, _Opts) -> + []. + +-spec mod_opt_type(atom()) -> econf:validator(). +mod_opt_type(notify_on) -> + econf:enum([messages, all]); +mod_opt_type(include_sender) -> + econf:bool(); +mod_opt_type(include_body) -> + econf:either( + econf:bool(), + econf:binary()); +mod_opt_type(db_type) -> + econf:db_type(?MODULE); +mod_opt_type(use_cache) -> + econf:bool(); +mod_opt_type(cache_size) -> + econf:pos_int(infinity); +mod_opt_type(cache_missed) -> + econf:bool(); +mod_opt_type(cache_life_time) -> + econf:timeout(second, infinity). + +-spec mod_options(binary()) -> [{atom(), any()}]. +mod_options(Host) -> + [{notify_on, all}, + {include_sender, false}, + {include_body, <<"New message">>}, + {db_type, ejabberd_config:default_db(Host, ?MODULE)}, + {use_cache, ejabberd_option:use_cache(Host)}, + {cache_size, ejabberd_option:cache_size(Host)}, + {cache_missed, ejabberd_option:cache_missed(Host)}, + {cache_life_time, ejabberd_option:cache_life_time(Host)}]. + +mod_doc() -> + #{desc => + ?T("This module implements the XMPP server's part of " + "the push notification solution specified in " + "https://xmpp.org/extensions/xep-0357.html" + "[XEP-0357: Push Notifications]. It does not generate, " + "for example, APNS or FCM notifications directly. " + "Instead, it's designed to work with so-called " + "\"app servers\" operated by third-party vendors of " + "mobile apps. Those app servers will usually trigger " + "notification delivery to the user's mobile device using " + "platform-dependent backend services such as FCM or APNS."), + opts => + [{notify_on, + #{value => "messages | all", + note => "added in 23.10", + desc => + ?T("If this option is set to 'messages', notifications are " + "generated only for actual chat messages with a body text " + "(or some encrypted payload). If it's set to 'all', any " + "kind of XMPP stanza will trigger a notification. If " + "unsure, it's strongly recommended to stick to 'all', " + "which is the default value.")}}, + {include_sender, + #{value => "true | false", + desc => + ?T("If this option is set to 'true', the sender's JID " + "is included with push notifications generated for " + "incoming messages with a body. " + "The default value is 'false'.")}}, + {include_body, + #{value => "true | false | Text", + desc => + ?T("If this option is set to 'true', the message text " + "is included with push notifications generated for " + "incoming messages with a body. The option can instead " + "be set to a static 'Text', in which case the specified " + "text will be included in place of the actual message " + "body. This can be useful to signal the app server " + "whether the notification was triggered by a message " + "with body (as opposed to other types of traffic) " + "without leaking actual message contents. " + "The default value is \"New message\".")}}, + {db_type, + #{value => "mnesia | sql", + desc => + ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}, + {use_cache, + #{value => "true | false", + desc => + ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, + {cache_size, + #{value => "pos_integer() | infinity", + desc => + ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, + {cache_missed, + #{value => "true | false", + desc => + ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, + {cache_life_time, + #{value => "timeout()", + desc => + ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}]}. + +%%-------------------------------------------------------------------- +%% ejabberd command callback. +%%-------------------------------------------------------------------- +-spec get_commands_spec() -> [ejabberd_commands()]. +get_commands_spec() -> + [#ejabberd_commands{name = delete_old_push_sessions, tags = [purge], + desc = "Remove push sessions older than DAYS", + module = ?MODULE, function = delete_old_sessions, + args = [{days, integer}], + result = {res, rescode}}]. + +-spec delete_old_sessions(non_neg_integer()) -> ok | any(). +delete_old_sessions(Days) -> + CurrentTime = erlang:system_time(microsecond), + Diff = Days * 24 * 60 * 60 * 1000000, + TimeStamp = misc:usec_to_now(CurrentTime - Diff), + DBTypes = lists:usort( + lists:map( + fun(Host) -> + case mod_push_opt:db_type(Host) of + sql -> {sql, Host}; + Other -> {Other, global} + end + end, ejabberd_option:hosts())), + Results = lists:map( + fun({DBType, Host}) -> + Mod = gen_mod:db_mod(DBType, ?MODULE), + Mod:delete_old_sessions(Host, TimeStamp) + end, DBTypes), + ets_cache:clear(?PUSH_CACHE, ejabberd_cluster:get_nodes()), + case lists:filter(fun(Res) -> Res /= ok end, Results) of + [] -> + ?INFO_MSG("Deleted push sessions older than ~B days", [Days]), + ok; + [{error, Reason} | _] -> + ?ERROR_MSG("Error while deleting old push sessions: ~p", [Reason]), + Reason + end. + +%%-------------------------------------------------------------------- +%% Service discovery. +%%-------------------------------------------------------------------- +-spec disco_sm_features(empty | {result, [binary()]} | {error, stanza_error()}, + jid(), jid(), binary(), binary()) + -> {result, [binary()]} | {error, stanza_error()}. +disco_sm_features(empty, From, To, Node, Lang) -> + disco_sm_features({result, []}, From, To, Node, Lang); +disco_sm_features({result, OtherFeatures}, + #jid{luser = U, lserver = S}, + #jid{luser = U, lserver = S}, <<"">>, _Lang) -> + {result, [?NS_PUSH_0 | OtherFeatures]}; +disco_sm_features(Acc, _From, _To, _Node, _Lang) -> + Acc. + +%%-------------------------------------------------------------------- +%% IQ handlers. +%%-------------------------------------------------------------------- +-spec process_iq(iq()) -> iq(). +process_iq(#iq{type = get, lang = Lang} = IQ) -> + Txt = ?T("Value 'get' of 'type' attribute is not allowed"), + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); +process_iq(#iq{lang = Lang, sub_els = [#push_enable{node = <<>>}]} = IQ) -> + Txt = ?T("Enabling push without 'node' attribute is not supported"), + xmpp:make_error(IQ, xmpp:err_feature_not_implemented(Txt, Lang)); +process_iq(#iq{from = #jid{lserver = LServer} = JID, + to = #jid{lserver = LServer}, + lang = Lang, + sub_els = [#push_enable{jid = PushJID, + node = Node, + xdata = XData}]} = IQ) -> + case enable(JID, PushJID, Node, XData) of + ok -> + xmpp:make_iq_result(IQ); + {error, db_failure} -> + Txt = ?T("Database failure"), + xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)); + {error, notfound} -> + Txt = ?T("User session not found"), + xmpp:make_error(IQ, xmpp:err_item_not_found(Txt, Lang)) + end; +process_iq(#iq{from = #jid{lserver = LServer} = JID, + to = #jid{lserver = LServer}, + lang = Lang, + sub_els = [#push_disable{jid = PushJID, + node = Node}]} = IQ) -> + case disable(JID, PushJID, Node) of + ok -> + xmpp:make_iq_result(IQ); + {error, db_failure} -> + Txt = ?T("Database failure"), + xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)); + {error, notfound} -> + Txt = ?T("Push record not found"), + xmpp:make_error(IQ, xmpp:err_item_not_found(Txt, Lang)) + end; +process_iq(IQ) -> + xmpp:make_error(IQ, xmpp:err_not_allowed()). + +-spec enable(jid(), jid(), binary(), xdata()) -> ok | {error, err_reason()}. +enable(#jid{luser = LUser, lserver = LServer, lresource = LResource} = JID, + PushJID, Node, XData) -> + case ejabberd_sm:get_session_sid(LUser, LServer, LResource) of + {ID, PID} -> + case store_session(LUser, LServer, ID, PushJID, Node, XData) of + {ok, _} -> + ?INFO_MSG("Enabling push notifications for ~ts", + [jid:encode(JID)]), + ejabberd_c2s:cast(PID, {push_enable, ID}), + ejabberd_sm:set_user_info(LUser, LServer, LResource, + push_id, ID); + {error, _} = Err -> + ?ERROR_MSG("Cannot enable push for ~ts: database error", + [jid:encode(JID)]), + Err + end; + none -> + ?WARNING_MSG("Cannot enable push for ~ts: session not found", + [jid:encode(JID)]), + {error, notfound} + end. + +-spec disable(jid(), jid(), binary() | undefined) -> ok | {error, err_reason()}. +disable(#jid{luser = LUser, lserver = LServer, lresource = LResource} = JID, + PushJID, Node) -> + case ejabberd_sm:get_session_pid(LUser, LServer, LResource) of + PID when is_pid(PID) -> + ?INFO_MSG("Disabling push notifications for ~ts", + [jid:encode(JID)]), + ejabberd_sm:del_user_info(LUser, LServer, LResource, push_id), + ejabberd_c2s:cast(PID, push_disable); + none -> + ?WARNING_MSG("Session not found while disabling push for ~ts", + [jid:encode(JID)]) + end, + if Node /= <<>> -> + delete_session(LUser, LServer, PushJID, Node); + true -> + delete_sessions(LUser, LServer, PushJID) + end. + +%%-------------------------------------------------------------------- +%% Hook callbacks. +%%-------------------------------------------------------------------- +-spec c2s_stanza(c2s_state(), xmpp_element() | xmlel(), term()) -> c2s_state(). +c2s_stanza(State, #stream_error{}, _SendResult) -> + State; +c2s_stanza(#{push_enabled := true, mgmt_state := pending} = State, + Pkt, _SendResult) -> + ?DEBUG("Notifying client of stanza", []), + notify(State, Pkt, get_direction(Pkt)), + State; +c2s_stanza(State, _Pkt, _SendResult) -> + State. + +-spec mam_message(message() | drop, binary(), binary(), jid(), + binary(), chat | groupchat, recv | send) -> message(). +mam_message(#message{} = Pkt, LUser, LServer, _Peer, _Nick, chat, Dir) -> + case lookup_sessions(LUser, LServer) of + {ok, [_|_] = Clients} -> + case drop_online_sessions(LUser, LServer, Clients) of + [_|_] = Clients1 -> + ?DEBUG("Notifying ~ts@~ts of MAM message", [LUser, LServer]), + notify(LUser, LServer, Clients1, Pkt, Dir); + [] -> + ok + end; + _ -> + ok + end, + Pkt; +mam_message(Pkt, _LUser, _LServer, _Peer, _Nick, _Type, _Dir) -> + Pkt. + +-spec offline_message({any(), message()}) -> {any(), message()}. +offline_message({offlined, #message{meta = #{mam_archived := true}}} = Acc) -> + Acc; % Push notification was triggered via MAM. +offline_message({offlined, + #message{to = #jid{luser = LUser, + lserver = LServer}} = Pkt} = Acc) -> + case lookup_sessions(LUser, LServer) of + {ok, [_|_] = Clients} -> + ?DEBUG("Notifying ~ts@~ts of offline message", [LUser, LServer]), + notify(LUser, LServer, Clients, Pkt, recv); + _ -> + ok + end, + Acc; +offline_message(Acc) -> + Acc. + +-spec c2s_session_pending(c2s_state()) -> c2s_state(). +c2s_session_pending(#{push_enabled := true, mgmt_queue := Queue} = State) -> + case p1_queue:len(Queue) of + Len when Len > 0 -> + ?DEBUG("Notifying client of unacknowledged stanza(s)", []), + {Pkt, Dir} = case mod_stream_mgmt:queue_find( + fun is_incoming_chat_msg/1, Queue) of + none -> {none, undefined}; + Pkt0 -> {Pkt0, get_direction(Pkt0)} + end, + notify(State, Pkt, Dir), + State; + 0 -> + State + end; +c2s_session_pending(State) -> + State. + +-spec c2s_copy_session(c2s_state(), c2s_state()) -> c2s_state(). +c2s_copy_session(State, #{push_enabled := true, + push_session_id := ID}) -> + State#{push_enabled => true, + push_session_id => ID}; +c2s_copy_session(State, _) -> + State. + +-spec c2s_session_resumed(c2s_state()) -> c2s_state(). +c2s_session_resumed(#{push_session_id := ID, + user := U, server := S, resource := R} = State) -> + ejabberd_sm:set_user_info(U, S, R, push_id, ID), + State; +c2s_session_resumed(State) -> + State. + +-spec c2s_handle_cast(c2s_state(), any()) -> c2s_state() | {stop, c2s_state()}. +c2s_handle_cast(State, {push_enable, ID}) -> + {stop, State#{push_enabled => true, + push_session_id => ID}}; +c2s_handle_cast(State, push_disable) -> + State1 = maps:remove(push_enabled, State), + State2 = maps:remove(push_session_id, State1), + {stop, State2}; +c2s_handle_cast(State, _Msg) -> + State. + +-spec remove_user(binary(), binary()) -> ok | {error, err_reason()}. +remove_user(LUser, LServer) -> + ?INFO_MSG("Removing any push sessions of ~ts@~ts", [LUser, LServer]), + Mod = gen_mod:db_mod(LServer, ?MODULE), + LookupFun = fun() -> Mod:lookup_sessions(LUser, LServer) end, + delete_sessions(LUser, LServer, LookupFun, Mod). + +%%-------------------------------------------------------------------- +%% Generate push notifications. +%%-------------------------------------------------------------------- +-spec notify(c2s_state(), xmpp_element() | xmlel() | none, direction()) -> ok. +notify(#{jid := #jid{luser = LUser, lserver = LServer}} = State, Pkt, Dir) -> + case lookup_session(LUser, LServer, State) of + {ok, Client} -> + notify(LUser, LServer, [Client], Pkt, Dir); + _Err -> + ok + end. + +-spec notify(binary(), binary(), [push_session()], + xmpp_element() | xmlel() | none, direction()) -> ok. +notify(LUser, LServer, Clients, Pkt, Dir) -> + lists:foreach( + fun({ID, PushLJID, Node, XData}) -> + HandleResponse = + fun(#iq{type = result}) -> + ?DEBUG("~ts accepted notification for ~ts@~ts (~ts)", + [jid:encode(PushLJID), LUser, LServer, Node]); + (#iq{type = error} = IQ) -> + case inspect_error(IQ) of + {wait, Reason} -> + ?INFO_MSG("~ts rejected notification for " + "~ts@~ts (~ts) temporarily: ~ts", + [jid:encode(PushLJID), LUser, + LServer, Node, Reason]); + {Type, Reason} -> + spawn(?MODULE, delete_session, + [LUser, LServer, ID]), + ?WARNING_MSG("~ts rejected notification for " + "~ts@~ts (~ts), disabling push: ~ts " + "(~ts)", + [jid:encode(PushLJID), LUser, + LServer, Node, Reason, Type]) + end; + (timeout) -> + ?DEBUG("Timeout sending notification for ~ts@~ts (~ts) " + "to ~ts", + [LUser, LServer, Node, jid:encode(PushLJID)]), + ok % Hmm. + end, + notify(LServer, PushLJID, Node, XData, Pkt, Dir, HandleResponse) + end, Clients). + +-spec notify(binary(), ljid(), binary(), xdata(), + xmpp_element() | xmlel() | none, direction(), + fun((iq() | timeout) -> any())) -> ok. +notify(LServer, PushLJID, Node, XData, Pkt0, Dir, HandleResponse) -> + Pkt = unwrap_message(Pkt0), + From = jid:make(LServer), + case {make_summary(LServer, Pkt, Dir), mod_push_opt:notify_on(LServer)} of + {undefined, messages} -> + ?DEBUG("Suppressing notification for stanza without payload", []), + ok; + {Summary, _NotifyOn} -> + Item = #ps_item{sub_els = [#push_notification{xdata = Summary}]}, + PubSub = #pubsub{publish = #ps_publish{node = Node, items = [Item]}, + publish_options = XData}, + IQ = #iq{type = set, + from = From, + to = jid:make(PushLJID), + id = p1_rand:get_string(), + sub_els = [PubSub]}, + ejabberd_router:route_iq(IQ, HandleResponse) + end. + +%%-------------------------------------------------------------------- +%% Miscellaneous. +%%-------------------------------------------------------------------- +-spec is_incoming_chat_msg(stanza()) -> boolean(). +is_incoming_chat_msg(#message{} = Msg) -> + case get_direction(Msg) of + recv -> get_body_text(unwrap_message(Msg)) /= none; + send -> false + end; +is_incoming_chat_msg(_Stanza) -> + false. + +%%-------------------------------------------------------------------- +%% Internal functions. +%%-------------------------------------------------------------------- +-spec store_session(binary(), binary(), push_session_id(), jid(), binary(), + xdata()) -> {ok, push_session()} | {error, err_reason()}. +store_session(LUser, LServer, ID, PushJID, Node, XData) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + delete_session(LUser, LServer, PushJID, Node), + case use_cache(Mod, LServer) of + true -> + ets_cache:delete(?PUSH_CACHE, {LUser, LServer}, + cache_nodes(Mod, LServer)), + ets_cache:update( + ?PUSH_CACHE, + {LUser, LServer, ID}, {ok, {ID, PushJID, Node, XData}}, + fun() -> + Mod:store_session(LUser, LServer, ID, PushJID, Node, + XData) + end, cache_nodes(Mod, LServer)); + false -> + Mod:store_session(LUser, LServer, ID, PushJID, Node, XData) + end. + +-spec lookup_session(binary(), binary(), c2s_state()) + -> {ok, push_session()} | error | {error, err_reason()}. +lookup_session(LUser, LServer, #{push_session_id := ID}) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + case use_cache(Mod, LServer) of + true -> + ets_cache:lookup( + ?PUSH_CACHE, {LUser, LServer, ID}, + fun() -> Mod:lookup_session(LUser, LServer, ID) end); + false -> + Mod:lookup_session(LUser, LServer, ID) + end. + +-spec lookup_sessions(binary(), binary()) -> {ok, [push_session()]} | {error, err_reason()}. +lookup_sessions(LUser, LServer) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + case use_cache(Mod, LServer) of + true -> + ets_cache:lookup( + ?PUSH_CACHE, {LUser, LServer}, + fun() -> Mod:lookup_sessions(LUser, LServer) end); + false -> + Mod:lookup_sessions(LUser, LServer) + end. + +-spec delete_session(binary(), binary(), push_session_id()) + -> ok | {error, db_failure}. +delete_session(LUser, LServer, ID) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + case Mod:delete_session(LUser, LServer, ID) of + ok -> + case use_cache(Mod, LServer) of + true -> + ets_cache:delete(?PUSH_CACHE, {LUser, LServer}, + cache_nodes(Mod, LServer)), + ets_cache:delete(?PUSH_CACHE, {LUser, LServer, ID}, + cache_nodes(Mod, LServer)); + false -> + ok + end; + {error, _} = Err -> + Err + end. + +-spec delete_session(binary(), binary(), jid(), binary()) -> ok | {error, err_reason()}. +delete_session(LUser, LServer, PushJID, Node) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + case Mod:lookup_session(LUser, LServer, PushJID, Node) of + {ok, {ID, _, _, _}} -> + delete_session(LUser, LServer, ID); + error -> + {error, notfound}; + {error, _} = Err -> + Err + end. + +-spec delete_sessions(binary(), binary(), jid()) -> ok | {error, err_reason()}. +delete_sessions(LUser, LServer, PushJID) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + LookupFun = fun() -> Mod:lookup_sessions(LUser, LServer, PushJID) end, + delete_sessions(LUser, LServer, LookupFun, Mod). + +-spec delete_sessions(binary(), binary(), fun(() -> any()), module()) + -> ok | {error, err_reason()}. +delete_sessions(LUser, LServer, LookupFun, Mod) -> + case LookupFun() of + {ok, []} -> + {error, notfound}; + {ok, Clients} -> + case use_cache(Mod, LServer) of + true -> + ets_cache:delete(?PUSH_CACHE, {LUser, LServer}, + cache_nodes(Mod, LServer)); + false -> + ok + end, + lists:foreach( + fun({ID, _, _, _}) -> + ok = Mod:delete_session(LUser, LServer, ID), + case use_cache(Mod, LServer) of + true -> + ets_cache:delete(?PUSH_CACHE, + {LUser, LServer, ID}, + cache_nodes(Mod, LServer)); + false -> + ok + end + end, Clients); + {error, _} = Err -> + Err + end. + +-spec drop_online_sessions(binary(), binary(), [push_session()]) + -> [push_session()]. +drop_online_sessions(LUser, LServer, Clients) -> + OnlineIDs = lists:filtermap( + fun({_, Info}) -> + case proplists:get_value(push_id, Info) of + OnlineID = {_, _, _} -> + {true, OnlineID}; + undefined -> + false + end + end, ejabberd_sm:get_user_info(LUser, LServer)), + [Client || {ID, _, _, _} = Client <- Clients, + not lists:member(ID, OnlineIDs)]. + +-spec make_summary(binary(), xmpp_element() | xmlel() | none, direction()) + -> xdata() | undefined. +make_summary(Host, #message{from = From0} = Pkt, recv) -> + case {mod_push_opt:include_sender(Host), + mod_push_opt:include_body(Host)} of + {false, false} -> + undefined; + {IncludeSender, IncludeBody} -> + case get_body_text(Pkt) of + none -> + undefined; + Text -> + Fields1 = case IncludeBody of + StaticText when is_binary(StaticText) -> + [{'last-message-body', StaticText}]; + true -> + [{'last-message-body', Text}]; + false -> + [] + end, + Fields2 = case IncludeSender of + true -> + From = jid:remove_resource(From0), + [{'last-message-sender', From} | Fields1]; + false -> + Fields1 + end, + #xdata{type = submit, fields = push_summary:encode(Fields2)} + end + end; +make_summary(_Host, _Pkt, _Dir) -> + undefined. + +-spec unwrap_message(Stanza) -> Stanza when Stanza :: stanza() | none. +unwrap_message(#message{meta = #{carbon_copy := true}} = Msg) -> + misc:unwrap_carbon(Msg); +unwrap_message(#message{type = normal} = Msg) -> + case misc:unwrap_mucsub_message(Msg) of + #message{} = InnerMsg -> + InnerMsg; + false -> + Msg + end; +unwrap_message(Stanza) -> + Stanza. + +-spec get_direction(stanza()) -> direction(). +get_direction(#message{meta = #{carbon_copy := true}, + from = #jid{luser = U, lserver = S}, + to = #jid{luser = U, lserver = S}}) -> + send; +get_direction(#message{}) -> + recv; +get_direction(_Stanza) -> + undefined. + +-spec get_body_text(message()) -> binary() | none. +get_body_text(#message{body = Body} = Msg) -> + case xmpp:get_text(Body) of + Text when byte_size(Text) > 0 -> + Text; + <<>> -> + case body_is_encrypted(Msg) of + true -> + <<"(encrypted)">>; + false -> + none + end + end. + +-spec body_is_encrypted(message()) -> boolean(). +body_is_encrypted(#message{sub_els = MsgEls}) -> + case lists:keyfind(<<"encrypted">>, #xmlel.name, MsgEls) of + #xmlel{children = EncEls} -> + lists:keyfind(<<"payload">>, #xmlel.name, EncEls) /= false; + false -> + false + end. + +-spec inspect_error(iq()) -> {atom(), binary()}. +inspect_error(IQ) -> + case xmpp:get_error(IQ) of + #stanza_error{type = Type} = Err -> + {Type, xmpp:format_stanza_error(Err)}; + undefined -> + {undefined, <<"unrecognized error">>} + end. + +%%-------------------------------------------------------------------- +%% Caching. +%%-------------------------------------------------------------------- +-spec init_cache(module(), binary(), gen_mod:opts()) -> ok. +init_cache(Mod, Host, Opts) -> + case use_cache(Mod, Host) of + true -> + CacheOpts = cache_opts(Opts), + ets_cache:new(?PUSH_CACHE, CacheOpts); + false -> + ets_cache:delete(?PUSH_CACHE) + end. + +-spec cache_opts(gen_mod:opts()) -> [proplists:property()]. +cache_opts(Opts) -> + MaxSize = mod_push_opt:cache_size(Opts), + CacheMissed = mod_push_opt:cache_missed(Opts), + LifeTime = mod_push_opt:cache_life_time(Opts), + [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}]. + +-spec use_cache(module(), binary()) -> boolean(). +use_cache(Mod, Host) -> + case erlang:function_exported(Mod, use_cache, 1) of + true -> Mod:use_cache(Host); + false -> mod_push_opt:use_cache(Host) + end. + +-spec cache_nodes(module(), binary()) -> [node()]. +cache_nodes(Mod, Host) -> + case erlang:function_exported(Mod, cache_nodes, 1) of + true -> Mod:cache_nodes(Host); + false -> ejabberd_cluster:get_nodes() + end. diff --git a/src/mod_push_keepalive.erl b/src/mod_push_keepalive.erl new file mode 100644 index 000000000..33bd2b53e --- /dev/null +++ b/src/mod_push_keepalive.erl @@ -0,0 +1,255 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_push_keepalive.erl +%%% Author : Holger Weiss +%%% Purpose : Keep pending XEP-0198 sessions alive with XEP-0357 +%%% Created : 15 Jul 2017 by Holger Weiss +%%% +%%% +%%% ejabberd, Copyright (C) 2017-2025 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_push_keepalive). +-author('holger@zedat.fu-berlin.de'). + +-behaviour(gen_mod). + +%% gen_mod callbacks. +-export([start/2, stop/1, reload/3, mod_opt_type/1, mod_options/1, depends/2]). +-export([mod_doc/0]). +%% ejabberd_hooks callbacks. +-export([ejabberd_started/0, c2s_session_pending/1, c2s_session_resumed/1, + c2s_copy_session/2, c2s_handle_cast/2, c2s_handle_info/2, + c2s_stanza/3]). + +-include("logger.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). +-include("translate.hrl"). + +-define(PUSH_BEFORE_TIMEOUT_PERIOD, 120000). % 2 minutes. + +-type c2s_state() :: ejabberd_c2s:state(). + +%%-------------------------------------------------------------------- +%% gen_mod callbacks. +%%-------------------------------------------------------------------- +-spec start(binary(), gen_mod:opts()) -> {ok, [gen_mod:registration()]}. +start(_Host, _Opts) -> + {ok, + [{hook, c2s_session_pending, c2s_session_pending, 50}, + {hook, c2s_session_resumed, c2s_session_resumed, 50}, + {hook, c2s_copy_session, c2s_copy_session, 50}, + {hook, c2s_handle_cast, c2s_handle_cast, 40}, + {hook, c2s_handle_info, c2s_handle_info, 50}, + {hook, c2s_handle_send, c2s_stanza, 50}, + %% Wait for ejabberd_pkix before running our ejabberd_started/0, so that we + %% don't initiate s2s connections before certificates are loaded: + {hook, ejabberd_started, ejabberd_started, 90, global}]}. + +-spec stop(binary()) -> ok. +stop(_Host) -> + ok. + +-spec reload(binary(), gen_mod:opts(), gen_mod:opts()) -> ok. +reload(_Host, _NewOpts, _OldOpts) -> + ok. + +-spec depends(binary(), gen_mod:opts()) -> [{module(), hard | soft}]. +depends(_Host, _Opts) -> + [{mod_push, hard}, + {mod_client_state, soft}, + {mod_stream_mgmt, soft}]. + +-spec mod_opt_type(atom()) -> econf:validator(). +mod_opt_type(resume_timeout) -> + econf:either( + econf:int(0, 0), + econf:timeout(second)); +mod_opt_type(wake_on_start) -> + econf:bool(); +mod_opt_type(wake_on_timeout) -> + econf:bool(). + +mod_options(_Host) -> + [{resume_timeout, timer:hours(72)}, + {wake_on_start, false}, + {wake_on_timeout, true}]. + +mod_doc() -> + #{desc => + [?T("This module tries to keep the stream management " + "session (see _`mod_stream_mgmt`_) of a disconnected " + "mobile client alive if the client enabled push " + "notifications for that session. However, the normal " + "session resumption timeout is restored once a push " + "notification is issued, so the session will be closed " + "if the client doesn't respond to push notifications."), "", + ?T("The module depends on _`mod_push`_.")], + opts => + [{resume_timeout, + #{value => "timeout()", + desc => + ?T("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 _`mod_stream_mgmt`_ " + "is restored. " + "The default value is '72' hours.")}}, + {wake_on_start, + #{value => "true | false", + desc => + ?T("If this option is set to 'true', notifications " + "are generated for **all** registered push clients " + "during server startup. This option should not be " + "enabled on servers with many push clients as it " + "can generate significant load on the involved push " + "services and the server itself. " + "The default value is 'false'.")}}, + {wake_on_timeout, + #{value => "true | false", + desc => + ?T("If this option is set to 'true', a notification " + "is generated shortly before the session would time " + "out as per the 'resume_timeout' option. " + "The default value is 'true'.")}}]}. + +%%-------------------------------------------------------------------- +%% Hook callbacks. +%%-------------------------------------------------------------------- +-spec c2s_stanza(c2s_state(), xmpp_element() | xmlel(), term()) -> c2s_state(). +c2s_stanza(#{push_enabled := true, mgmt_state := pending} = State, + Pkt, _SendResult) -> + case mod_push:is_incoming_chat_msg(Pkt) of + true -> + maybe_restore_resume_timeout(State); + false -> + State + end; +c2s_stanza(State, _Pkt, _SendResult) -> + State. + +-spec c2s_session_pending(c2s_state()) -> c2s_state(). +c2s_session_pending(#{push_enabled := true, mgmt_queue := Queue} = State) -> + case mod_stream_mgmt:queue_find(fun mod_push:is_incoming_chat_msg/1, + Queue) of + none -> + State1 = maybe_adjust_resume_timeout(State), + maybe_start_wakeup_timer(State1); + _Msg -> + State + end; +c2s_session_pending(State) -> + State. + +-spec c2s_session_resumed(c2s_state()) -> c2s_state(). +c2s_session_resumed(#{push_enabled := true} = State) -> + maybe_restore_resume_timeout(State); +c2s_session_resumed(State) -> + State. + +-spec c2s_copy_session(c2s_state(), c2s_state()) -> c2s_state(). +c2s_copy_session(State, #{push_enabled := true, + push_resume_timeout := ResumeTimeout, + push_wake_on_timeout := WakeOnTimeout} = OldState) -> + State1 = case maps:find(push_resume_timeout_orig, OldState) of + {ok, Val} -> + State#{push_resume_timeout_orig => Val}; + error -> + State + end, + State1#{push_resume_timeout => ResumeTimeout, + push_wake_on_timeout => WakeOnTimeout}; +c2s_copy_session(State, _) -> + State. + +-spec c2s_handle_cast(c2s_state(), any()) -> c2s_state(). +c2s_handle_cast(#{lserver := LServer} = State, {push_enable, _ID}) -> + ResumeTimeout = mod_push_keepalive_opt:resume_timeout(LServer), + WakeOnTimeout = mod_push_keepalive_opt:wake_on_timeout(LServer), + State#{push_resume_timeout => ResumeTimeout, + push_wake_on_timeout => WakeOnTimeout}; +c2s_handle_cast(State, push_disable) -> + State1 = maps:remove(push_resume_timeout, State), + maps:remove(push_wake_on_timeout, State1); +c2s_handle_cast(State, _Msg) -> + State. + +-spec c2s_handle_info(c2s_state(), any()) -> c2s_state() | {stop, c2s_state()}. +c2s_handle_info(#{push_enabled := true, mgmt_state := pending, + jid := JID} = State, {timeout, _, push_keepalive}) -> + ?INFO_MSG("Waking ~ts before session times out", [jid:encode(JID)]), + mod_push:notify(State, none, undefined), + {stop, State}; +c2s_handle_info(State, _) -> + State. + +-spec ejabberd_started() -> ok. +ejabberd_started() -> + Pred = fun(Host) -> + gen_mod:is_loaded(Host, ?MODULE) andalso + mod_push_keepalive_opt:wake_on_start(Host) + end, + [wake_all(Host) || Host <- ejabberd_config:get_option(hosts), Pred(Host)], + ok. + +%%-------------------------------------------------------------------- +%% Internal functions. +%%-------------------------------------------------------------------- +-spec maybe_adjust_resume_timeout(c2s_state()) -> c2s_state(). +maybe_adjust_resume_timeout(#{push_resume_timeout := undefined} = State) -> + State; +maybe_adjust_resume_timeout(#{push_resume_timeout := Timeout} = State) -> + OrigTimeout = mod_stream_mgmt:get_resume_timeout(State), + ?DEBUG("Adjusting resume timeout to ~B seconds", [Timeout div 1000]), + State1 = mod_stream_mgmt:set_resume_timeout(State, Timeout), + State1#{push_resume_timeout_orig => OrigTimeout}. + +-spec maybe_restore_resume_timeout(c2s_state()) -> c2s_state(). +maybe_restore_resume_timeout(#{push_resume_timeout_orig := Timeout} = State) -> + ?DEBUG("Restoring resume timeout to ~B seconds", [Timeout div 1000]), + State1 = mod_stream_mgmt:set_resume_timeout(State, Timeout), + maps:remove(push_resume_timeout_orig, State1); +maybe_restore_resume_timeout(State) -> + State. + +-spec maybe_start_wakeup_timer(c2s_state()) -> c2s_state(). +maybe_start_wakeup_timer(#{push_wake_on_timeout := true, + push_resume_timeout := ResumeTimeout} = State) + when is_integer(ResumeTimeout), ResumeTimeout > ?PUSH_BEFORE_TIMEOUT_PERIOD -> + WakeTimeout = ResumeTimeout - ?PUSH_BEFORE_TIMEOUT_PERIOD, + ?DEBUG("Scheduling wake-up timer to fire in ~B seconds", [WakeTimeout div 1000]), + erlang:start_timer(WakeTimeout, self(), push_keepalive), + State; +maybe_start_wakeup_timer(State) -> + State. + +-spec wake_all(binary()) -> ok. +wake_all(LServer) -> + ?INFO_MSG("Waking all push clients on ~ts", [LServer]), + Mod = gen_mod:db_mod(LServer, mod_push), + case Mod:lookup_sessions(LServer) of + {ok, Sessions} -> + IgnoreResponse = fun(_) -> ok end, + lists:foreach(fun({_, PushLJID, Node, XData}) -> + mod_push:notify(LServer, PushLJID, Node, + XData, none, undefined, + IgnoreResponse) + end, Sessions); + error -> + ok + end. diff --git a/src/mod_push_keepalive_opt.erl b/src/mod_push_keepalive_opt.erl new file mode 100644 index 000000000..82b1d51bb --- /dev/null +++ b/src/mod_push_keepalive_opt.erl @@ -0,0 +1,27 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_push_keepalive_opt). + +-export([resume_timeout/1]). +-export([wake_on_start/1]). +-export([wake_on_timeout/1]). + +-spec resume_timeout(gen_mod:opts() | global | binary()) -> non_neg_integer(). +resume_timeout(Opts) when is_map(Opts) -> + gen_mod:get_opt(resume_timeout, Opts); +resume_timeout(Host) -> + gen_mod:get_module_opt(Host, mod_push_keepalive, resume_timeout). + +-spec wake_on_start(gen_mod:opts() | global | binary()) -> boolean(). +wake_on_start(Opts) when is_map(Opts) -> + gen_mod:get_opt(wake_on_start, Opts); +wake_on_start(Host) -> + gen_mod:get_module_opt(Host, mod_push_keepalive, wake_on_start). + +-spec wake_on_timeout(gen_mod:opts() | global | binary()) -> boolean(). +wake_on_timeout(Opts) when is_map(Opts) -> + gen_mod:get_opt(wake_on_timeout, Opts); +wake_on_timeout(Host) -> + gen_mod:get_module_opt(Host, mod_push_keepalive, wake_on_timeout). + diff --git a/src/mod_push_mnesia.erl b/src/mod_push_mnesia.erl new file mode 100644 index 000000000..6a5f068b9 --- /dev/null +++ b/src/mod_push_mnesia.erl @@ -0,0 +1,211 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_push_mnesia.erl +%%% Author : Holger Weiss +%%% Purpose : Mnesia backend for Push Notifications (XEP-0357) +%%% Created : 15 Jul 2017 by Holger Weiss +%%% +%%% +%%% ejabberd, Copyright (C) 2017-2025 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_push_mnesia). +-author('holger@zedat.fu-berlin.de'). + +-behaviour(mod_push). + +%% API +-export([init/2, store_session/6, lookup_session/4, lookup_session/3, + lookup_sessions/3, lookup_sessions/2, lookup_sessions/1, + delete_session/3, delete_old_sessions/2, transform/1]). + +-include_lib("stdlib/include/ms_transform.hrl"). +-include("logger.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). +-include("mod_push.hrl"). + +%%%------------------------------------------------------------------- +%%% API +%%%------------------------------------------------------------------- +init(_Host, _Opts) -> + ejabberd_mnesia:create(?MODULE, push_session, + [{disc_only_copies, [node()]}, + {type, bag}, + {attributes, record_info(fields, push_session)}]). + +store_session(LUser, LServer, TS, PushJID, Node, XData) -> + US = {LUser, LServer}, + PushLJID = jid:tolower(PushJID), + MaxSessions = ejabberd_sm:get_max_user_sessions(LUser, LServer), + F = fun() -> + enforce_max_sessions(US, MaxSessions), + mnesia:write(#push_session{us = US, + timestamp = TS, + service = PushLJID, + node = Node, + xml = encode_xdata(XData)}) + end, + case mnesia:transaction(F) of + {atomic, ok} -> + {ok, {TS, PushLJID, Node, XData}}; + {aborted, E} -> + ?ERROR_MSG("Cannot store push session for ~ts@~ts: ~p", + [LUser, LServer, E]), + {error, db_failure} + end. + +lookup_session(LUser, LServer, PushJID, Node) -> + PushLJID = jid:tolower(PushJID), + MatchSpec = ets:fun2ms( + fun(#push_session{us = {U, S}, service = P, node = N} = Rec) + when U == LUser, + S == LServer, + P == PushLJID, + N == Node -> + Rec + end), + case mnesia:dirty_select(push_session, MatchSpec) of + [#push_session{timestamp = TS, xml = El}] -> + {ok, {TS, PushLJID, Node, decode_xdata(El)}}; + [] -> + ?DEBUG("No push session found for ~ts@~ts (~p, ~ts)", + [LUser, LServer, PushJID, Node]), + {error, notfound} + end. + +lookup_session(LUser, LServer, TS) -> + MatchSpec = ets:fun2ms( + fun(#push_session{us = {U, S}, timestamp = T} = Rec) + when U == LUser, + S == LServer, + T == TS -> + Rec + end), + case mnesia:dirty_select(push_session, MatchSpec) of + [#push_session{service = PushLJID, node = Node, xml = El}] -> + {ok, {TS, PushLJID, Node, decode_xdata(El)}}; + [] -> + ?DEBUG("No push session found for ~ts@~ts (~p)", + [LUser, LServer, TS]), + {error, notfound} + end. + +lookup_sessions(LUser, LServer, PushJID) -> + PushLJID = jid:tolower(PushJID), + MatchSpec = ets:fun2ms( + fun(#push_session{us = {U, S}, service = P} = Rec) + when U == LUser, + S == LServer, + P == PushLJID -> + Rec + end), + Records = mnesia:dirty_select(push_session, MatchSpec), + {ok, records_to_sessions(Records)}. + +lookup_sessions(LUser, LServer) -> + Records = mnesia:dirty_read(push_session, {LUser, LServer}), + {ok, records_to_sessions(Records)}. + +lookup_sessions(LServer) -> + MatchSpec = ets:fun2ms( + fun(#push_session{us = {_U, S}} = Rec) + when S == LServer -> + Rec + end), + Records = mnesia:dirty_select(push_session, MatchSpec), + {ok, records_to_sessions(Records)}. + +delete_session(LUser, LServer, TS) -> + MatchSpec = ets:fun2ms( + fun(#push_session{us = {U, S}, timestamp = T} = Rec) + when U == LUser, + S == LServer, + T == TS -> + Rec + end), + F = fun() -> + Recs = mnesia:select(push_session, MatchSpec), + lists:foreach(fun mnesia:delete_object/1, Recs) + end, + case mnesia:transaction(F) of + {atomic, ok} -> + ok; + {aborted, E} -> + ?ERROR_MSG("Cannot delete push session of ~ts@~ts: ~p", + [LUser, LServer, E]), + {error, db_failure} + end. + +delete_old_sessions(_LServer, Time) -> + DelIfOld = fun(#push_session{timestamp = T} = Rec, ok) when T < Time -> + mnesia:delete_object(Rec); + (_Rec, ok) -> + ok + end, + F = fun() -> + mnesia:foldl(DelIfOld, ok, push_session) + end, + case mnesia:transaction(F) of + {atomic, ok} -> + ok; + {aborted, E} -> + ?ERROR_MSG("Cannot delete old push sessions: ~p", [E]), + {error, db_failure} + end. + +transform({push_session, US, TS, Service, Node, XData}) -> + ?INFO_MSG("Transforming push_session Mnesia table", []), + #push_session{us = US, timestamp = TS, service = Service, + node = Node, xml = encode_xdata(XData)}. + +%%-------------------------------------------------------------------- +%% Internal functions. +%%-------------------------------------------------------------------- +-spec enforce_max_sessions({binary(), binary()}, non_neg_integer() | infinity) + -> ok. +enforce_max_sessions(_US, infinity) -> + ok; +enforce_max_sessions({U, S} = US, MaxSessions) -> + case mnesia:wread({push_session, US}) of + Recs when length(Recs) >= MaxSessions -> + Recs1 = lists:sort(fun(#push_session{timestamp = TS1}, + #push_session{timestamp = TS2}) -> + TS1 >= TS2 + end, Recs), + OldRecs = lists:nthtail(MaxSessions - 1, Recs1), + ?INFO_MSG("Disabling old push session(s) of ~ts@~ts", [U, S]), + lists:foreach(fun(Rec) -> mnesia:delete_object(Rec) end, OldRecs); + _ -> + ok + end. + +decode_xdata(undefined) -> + undefined; +decode_xdata(El) -> + xmpp:decode(El). + +encode_xdata(undefined) -> + undefined; +encode_xdata(XData) -> + xmpp:encode(XData). + +records_to_sessions(Records) -> + [{TS, PushLJID, Node, decode_xdata(El)} + || #push_session{timestamp = TS, + service = PushLJID, + node = Node, + xml = El} <- Records]. diff --git a/src/mod_push_opt.erl b/src/mod_push_opt.erl new file mode 100644 index 000000000..db6c55389 --- /dev/null +++ b/src/mod_push_opt.erl @@ -0,0 +1,62 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_push_opt). + +-export([cache_life_time/1]). +-export([cache_missed/1]). +-export([cache_size/1]). +-export([db_type/1]). +-export([include_body/1]). +-export([include_sender/1]). +-export([notify_on/1]). +-export([use_cache/1]). + +-spec cache_life_time(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +cache_life_time(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_life_time, Opts); +cache_life_time(Host) -> + gen_mod:get_module_opt(Host, mod_push, cache_life_time). + +-spec cache_missed(gen_mod:opts() | global | binary()) -> boolean(). +cache_missed(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_missed, Opts); +cache_missed(Host) -> + gen_mod:get_module_opt(Host, mod_push, cache_missed). + +-spec cache_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +cache_size(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_size, Opts); +cache_size(Host) -> + gen_mod:get_module_opt(Host, mod_push, cache_size). + +-spec db_type(gen_mod:opts() | global | binary()) -> atom(). +db_type(Opts) when is_map(Opts) -> + gen_mod:get_opt(db_type, Opts); +db_type(Host) -> + gen_mod:get_module_opt(Host, mod_push, db_type). + +-spec include_body(gen_mod:opts() | global | binary()) -> boolean() | binary(). +include_body(Opts) when is_map(Opts) -> + gen_mod:get_opt(include_body, Opts); +include_body(Host) -> + gen_mod:get_module_opt(Host, mod_push, include_body). + +-spec include_sender(gen_mod:opts() | global | binary()) -> boolean(). +include_sender(Opts) when is_map(Opts) -> + gen_mod:get_opt(include_sender, Opts); +include_sender(Host) -> + gen_mod:get_module_opt(Host, mod_push, include_sender). + +-spec notify_on(gen_mod:opts() | global | binary()) -> 'all' | 'messages'. +notify_on(Opts) when is_map(Opts) -> + gen_mod:get_opt(notify_on, Opts); +notify_on(Host) -> + gen_mod:get_module_opt(Host, mod_push, notify_on). + +-spec use_cache(gen_mod:opts() | global | binary()) -> boolean(). +use_cache(Opts) when is_map(Opts) -> + gen_mod:get_opt(use_cache, Opts); +use_cache(Host) -> + gen_mod:get_module_opt(Host, mod_push, use_cache). + diff --git a/src/mod_push_sql.erl b/src/mod_push_sql.erl new file mode 100644 index 000000000..a36e50f8e --- /dev/null +++ b/src/mod_push_sql.erl @@ -0,0 +1,275 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_push_sql.erl +%%% Author : Evgeniy Khramtsov +%%% Purpose : +%%% Created : 26 Oct 2017 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2017-2025 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_push_sql). +-behaviour(mod_push). + +%% API +-export([init/2, store_session/6, lookup_session/4, lookup_session/3, + lookup_sessions/3, lookup_sessions/2, lookup_sessions/1, + delete_session/3, delete_old_sessions/2, export/1]). +-export([sql_schemas/0]). + +-include_lib("xmpp/include/xmpp.hrl"). +-include("logger.hrl"). +-include("ejabberd_sql_pt.hrl"). +-include("mod_push.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(Host, _Opts) -> + ejabberd_sql_schema:update_schema(Host, ?MODULE, sql_schemas()), + ok. + +sql_schemas() -> + [#sql_schema{ + version = 1, + tables = + [#sql_table{ + name = <<"push_session">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"timestamp">>, type = bigint}, + #sql_column{name = <<"service">>, type = text}, + #sql_column{name = <<"node">>, type = text}, + #sql_column{name = <<"xml">>, type = text}], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"username">>, + <<"timestamp">>], + unique = true}, + #sql_index{ + columns = [<<"server_host">>, <<"username">>, + <<"service">>, <<"node">>], + unique = true}]}]}]. + +store_session(LUser, LServer, NowTS, PushJID, Node, XData) -> + XML = encode_xdata(XData), + TS = misc:now_to_usec(NowTS), + PushLJID = jid:tolower(PushJID), + Service = jid:encode(PushLJID), + MaxSessions = ejabberd_sm:get_max_user_sessions(LUser, LServer), + enforce_max_sessions(LUser, LServer, MaxSessions), + case ?SQL_UPSERT(LServer, "push_session", + ["!username=%(LUser)s", + "!server_host=%(LServer)s", + "timestamp=%(TS)d", + "!service=%(Service)s", + "!node=%(Node)s", + "xml=%(XML)s"]) of + ok -> + {ok, {NowTS, PushLJID, Node, XData}}; + _Err -> + {error, db_failure} + end. + +lookup_session(LUser, LServer, PushJID, Node) -> + PushLJID = jid:tolower(PushJID), + Service = jid:encode(PushLJID), + case ejabberd_sql:sql_query( + LServer, + ?SQL("select @(timestamp)d, @(xml)s from push_session " + "where username=%(LUser)s and %(LServer)H " + "and service=%(Service)s " + "and node=%(Node)s")) of + {selected, [{TS, XML}]} -> + NowTS = misc:usec_to_now(TS), + XData = decode_xdata(XML, LUser, LServer), + {ok, {NowTS, PushLJID, Node, XData}}; + {selected, []} -> + {error, notfound}; + _Err -> + {error, db_failure} + end. + +lookup_session(LUser, LServer, NowTS) -> + TS = misc:now_to_usec(NowTS), + case ejabberd_sql:sql_query( + LServer, + ?SQL("select @(service)s, @(node)s, @(xml)s " + "from push_session where username=%(LUser)s and %(LServer)H " + "and timestamp=%(TS)d")) of + {selected, [{Service, Node, XML}]} -> + PushLJID = jid:tolower(jid:decode(Service)), + XData = decode_xdata(XML, LUser, LServer), + {ok, {NowTS, PushLJID, Node, XData}}; + {selected, []} -> + {error, notfound}; + _Err -> + {error, db_failure} + end. + +lookup_sessions(LUser, LServer, PushJID) -> + PushLJID = jid:tolower(PushJID), + Service = jid:encode(PushLJID), + case ejabberd_sql:sql_query( + LServer, + ?SQL("select @(timestamp)d, @(xml)s, @(node)s from push_session " + "where username=%(LUser)s and %(LServer)H " + "and service=%(Service)s")) of + {selected, Rows} -> + {ok, lists:map( + fun({TS, XML, Node}) -> + NowTS = misc:usec_to_now(TS), + XData = decode_xdata(XML, LUser, LServer), + {NowTS, PushLJID, Node, XData} + end, Rows)}; + _Err -> + {error, db_failure} + end. + +lookup_sessions(LUser, LServer) -> + case ejabberd_sql:sql_query( + LServer, + ?SQL("select @(timestamp)d, @(xml)s, @(node)s, @(service)s " + "from push_session " + "where username=%(LUser)s and %(LServer)H")) of + {selected, Rows} -> + {ok, lists:map( + fun({TS, XML, Node, Service}) -> + NowTS = misc:usec_to_now(TS), + XData = decode_xdata(XML, LUser, LServer), + PushLJID = jid:tolower(jid:decode(Service)), + {NowTS, PushLJID,Node, XData} + end, Rows)}; + _Err -> + {error, db_failure} + end. + +lookup_sessions(LServer) -> + case ejabberd_sql:sql_query( + LServer, + ?SQL("select @(username)s, @(timestamp)d, @(xml)s, " + "@(node)s, @(service)s from push_session " + "where %(LServer)H")) of + {selected, Rows} -> + {ok, lists:map( + fun({LUser, TS, XML, Node, Service}) -> + NowTS = misc:usec_to_now(TS), + XData = decode_xdata(XML, LUser, LServer), + PushLJID = jid:tolower(jid:decode(Service)), + {NowTS, PushLJID, Node, XData} + end, Rows)}; + _Err -> + {error, db_failure} + end. + +delete_session(LUser, LServer, NowTS) -> + TS = misc:now_to_usec(NowTS), + case ejabberd_sql:sql_query( + LServer, + ?SQL("delete from push_session where " + "username=%(LUser)s and %(LServer)H and timestamp=%(TS)d")) of + {updated, _} -> + ok; + _Err -> + {error, db_failure} + end. + +delete_old_sessions(LServer, Time) -> + TS = misc:now_to_usec(Time), + case ejabberd_sql:sql_query( + LServer, + ?SQL("delete from push_session where timestamp<%(TS)d " + "and %(LServer)H")) of + {updated, _} -> + ok; + _Err -> + {error, db_failure} + end. + +export(_Server) -> + [{push_session, + fun(Host, #push_session{us = {LUser, LServer}, + timestamp = NowTS, + service = PushLJID, + node = Node, + xml = XData}) + when LServer == Host -> + TS = misc:now_to_usec(NowTS), + Service = jid:encode(PushLJID), + XML = encode_xdata(XData), + [?SQL("delete from push_session where " + "username=%(LUser)s and %(LServer)H and " + "timestamp=%(TS)d and " + "service=%(Service)s and node=%(Node)s and " + "xml=%(XML)s;"), + ?SQL_INSERT( + "push_session", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "timestamp=%(TS)d", + "service=%(Service)s", + "node=%(Node)s", + "xml=%(XML)s"])]; + (_Host, _R) -> + [] + end}]. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +enforce_max_sessions(_LUser, _LServer, infinity) -> + ok; +enforce_max_sessions(LUser, LServer, MaxSessions) -> + case lookup_sessions(LUser, LServer) of + {ok, Sessions} when length(Sessions) >= MaxSessions -> + ?INFO_MSG("Disabling old push session(s) of ~ts@~ts", + [LUser, LServer]), + Sessions1 = lists:sort(fun({TS1, _, _, _}, {TS2, _, _, _}) -> + TS1 >= TS2 + end, Sessions), + OldSessions = lists:nthtail(MaxSessions - 1, Sessions1), + lists:foreach(fun({TS, _, _, _}) -> + delete_session(LUser, LServer, TS) + end, OldSessions); + _ -> + ok + end. + +decode_xdata(<<>>, _LUser, _LServer) -> + undefined; +decode_xdata(XML, LUser, LServer) -> + case fxml_stream:parse_element(XML) of + #xmlel{} = El -> + try xmpp:decode(El) + catch _:{xmpp_codec, Why} -> + ?ERROR_MSG("Failed to decode ~ts for user ~ts@~ts " + "from table 'push_session': ~ts", + [XML, LUser, LServer, xmpp:format_error(Why)]), + undefined + end; + Err -> + ?ERROR_MSG("Failed to decode ~ts for user ~ts@~ts from " + "table 'push_session': ~p", + [XML, LUser, LServer, Err]), + undefined + end. + +encode_xdata(undefined) -> + <<>>; +encode_xdata(XData) -> + fxml:element_to_binary(xmpp:encode(XData)). diff --git a/src/mod_register.erl b/src/mod_register.erl index cd68af936..793c3c54d 100644 --- a/src/mod_register.erl +++ b/src/mod_register.erl @@ -5,7 +5,7 @@ %%% Created : 8 Dec 2002 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -27,452 +27,422 @@ -author('alexey@process-one.net'). +-protocol({xep, 77, '2.4', '0.1.0', "complete", ""}). + -behaviour(gen_mod). --export([start/2, stop/1, stream_feature_register/2, - unauthenticated_iq_register/4, try_register/5, - process_iq/3, send_registration_notifications/3, - transform_options/1, transform_module_options/1]). +-export([start/2, stop/1, reload/3, stream_feature_register/2, + 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("ejabberd.hrl"). -include("logger.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). +-include("translate.hrl"). --include("jlib.hrl"). - -start(Host, Opts) -> - IQDisc = gen_mod:get_opt(iqdisc, Opts, fun gen_iq_handler:check_type/1, - one_queue), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, - ?NS_REGISTER, ?MODULE, process_iq, IQDisc), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, - ?NS_REGISTER, ?MODULE, process_iq, IQDisc), - ejabberd_hooks:add(c2s_stream_features, Host, ?MODULE, - stream_feature_register, 50), - ejabberd_hooks:add(c2s_unauthenticated_iq, Host, - ?MODULE, unauthenticated_iq_register, 50), - mnesia:create_table(mod_register_ip, +start(_Host, _Opts) -> + ejabberd_mnesia:create(?MODULE, mod_register_ip, [{ram_copies, [node()]}, {local_content, true}, {attributes, [key, value]}]), - mnesia:add_table_copy(mod_register_ip, node(), - ram_copies), + {ok, [{iq_handler, ejabberd_local, ?NS_REGISTER, process_iq}, + {iq_handler, ejabberd_sm, ?NS_REGISTER, process_iq}, + {hook, c2s_pre_auth_features, stream_feature_register, 50}, + {hook, c2s_unauthenticated_packet, c2s_unauthenticated_packet, 50}]}. + +stop(_Host) -> ok. -stop(Host) -> - ejabberd_hooks:delete(c2s_stream_features, Host, - ?MODULE, stream_feature_register, 50), - ejabberd_hooks:delete(c2s_unauthenticated_iq, Host, - ?MODULE, unauthenticated_iq_register, 50), - gen_iq_handler:remove_iq_handler(ejabberd_local, Host, - ?NS_REGISTER), - gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, - ?NS_REGISTER). +reload(_Host, _NewOpts, _OldOpts) -> + ok. -stream_feature_register(Acc, _Host) -> - [#xmlel{name = <<"register">>, - attrs = [{<<"xmlns">>, ?NS_FEATURE_IQREGISTER}], - children = []} - | Acc]. +depends(_Host, _Opts) -> + []. -unauthenticated_iq_register(_Acc, Server, - #iq{xmlns = ?NS_REGISTER} = IQ, IP) -> - Address = case IP of - {A, _Port} -> A; - _ -> undefined - end, - ResIQ = process_iq(jlib:make_jid(<<"">>, <<"">>, - <<"">>), - jlib:make_jid(<<"">>, Server, <<"">>), IQ, Address), - Res1 = jlib:replace_from_to(jlib:make_jid(<<"">>, - Server, <<"">>), - jlib:make_jid(<<"">>, <<"">>, <<"">>), - jlib:iq_to_xml(ResIQ)), - jlib:remove_attr(<<"to">>, Res1); -unauthenticated_iq_register(Acc, _Server, _IQ, _IP) -> - Acc. +-spec stream_feature_register([xmpp_element()], binary()) -> [xmpp_element()]. +stream_feature_register(Acc, Host) -> + case {mod_register_opt:access(Host), + mod_register_opt:ip_access(Host), + mod_register_opt:redirect_url(Host)} of + {none, _, undefined} -> Acc; + {_, none, undefined} -> Acc; + {_, _, _} -> [#feature_register{}|Acc] + end. -process_iq(From, To, IQ) -> - process_iq(From, To, IQ, jlib:jid_tolower(From)). +c2s_unauthenticated_packet(#{ip := IP, server := Server} = State, + #iq{type = T, sub_els = [_]} = IQ) + when T == set; T == get -> + try xmpp:try_subtag(IQ, #register{}) of + #register{} = Register -> + {Address, _} = IP, + IQ1 = xmpp:set_els(IQ, [Register]), + IQ2 = xmpp:set_from_to(IQ1, jid:make(<<>>), jid:make(Server)), + ResIQ = process_iq(IQ2, Address), + ResIQ1 = xmpp:set_from_to(ResIQ, jid:make(Server), undefined), + {stop, ejabberd_c2s:send(State, ResIQ1)}; + false -> + State + catch _:{xmpp_codec, Why} -> + Txt = xmpp:io_format_error(Why), + Lang = maps:get(lang, State), + Err = make_stripped_error(IQ, xmpp:err_bad_request(Txt, Lang)), + {stop, ejabberd_c2s:send(State, Err)} + end; +c2s_unauthenticated_packet(State, _) -> + State. -process_iq(From, To, - #iq{type = Type, lang = Lang, sub_el = SubEl, id = ID} = - IQ, - Source) -> - IsCaptchaEnabled = case - gen_mod:get_module_opt(To#jid.lserver, ?MODULE, - captcha_protected, - fun(B) when is_boolean(B) -> B end, - false) - of - true -> true; - _ -> false - end, - case Type of - set -> - UTag = xml:get_subtag(SubEl, <<"username">>), - PTag = xml:get_subtag(SubEl, <<"password">>), - RTag = xml:get_subtag(SubEl, <<"remove">>), - Server = To#jid.lserver, - Access = gen_mod:get_module_opt(Server, ?MODULE, access, - fun(A) when is_atom(A) -> A end, - all), - AllowRemove = allow == - acl:match_rule(Server, Access, From), - if (UTag /= false) and (RTag /= false) and - AllowRemove -> - User = xml:get_tag_cdata(UTag), - case From of - #jid{user = User, lserver = Server} -> - ejabberd_auth:remove_user(User, Server), - IQ#iq{type = result, sub_el = []}; - _ -> - if PTag /= false -> - Password = xml:get_tag_cdata(PTag), - case ejabberd_auth:remove_user(User, Server, - Password) - of - ok -> IQ#iq{type = result, sub_el = []}; - %% TODO FIXME: This piece of - %% code does not work since - %% the code have been changed - %% to allow several auth - %% modules. lists:foreach can - %% only return ok: - not_allowed -> - IQ#iq{type = error, - sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; - not_exists -> - IQ#iq{type = error, - sub_el = - [SubEl, ?ERR_ITEM_NOT_FOUND]}; - _ -> - IQ#iq{type = error, - sub_el = - [SubEl, - ?ERR_INTERNAL_SERVER_ERROR]} - end; - true -> - IQ#iq{type = error, - sub_el = [SubEl, ?ERR_BAD_REQUEST]} - end - end; - (UTag == false) and (RTag /= false) and AllowRemove -> - case From of - #jid{user = User, lserver = Server, - resource = Resource} -> - ResIQ = #iq{type = result, xmlns = ?NS_REGISTER, - id = ID, sub_el = []}, - ejabberd_router:route(jlib:make_jid(User, Server, - Resource), - jlib:make_jid(User, Server, - Resource), - jlib:iq_to_xml(ResIQ)), - ejabberd_auth:remove_user(User, Server), - ignore; - _ -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]} - end; - (UTag /= false) and (PTag /= false) -> - User = xml:get_tag_cdata(UTag), - Password = xml:get_tag_cdata(PTag), - try_register_or_set_password(User, Server, Password, - From, IQ, SubEl, Source, Lang, - not IsCaptchaEnabled); - IsCaptchaEnabled -> - case ejabberd_captcha:process_reply(SubEl) of - ok -> - case process_xdata_submit(SubEl) of - {ok, User, Password} -> - try_register_or_set_password(User, Server, - Password, From, IQ, - SubEl, Source, Lang, - true); - _ -> - IQ#iq{type = error, - sub_el = [SubEl, ?ERR_BAD_REQUEST]} - end; - {error, malformed} -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_BAD_REQUEST]}; - _ -> - ErrText = <<"The CAPTCHA verification has failed">>, - IQ#iq{type = error, - sub_el = [SubEl, ?ERRT_NOT_ALLOWED(Lang, ErrText)]} - end; - true -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_BAD_REQUEST]} - end; - get -> - {IsRegistered, UsernameSubels, QuerySubels} = case From - of - #jid{user = User, - lserver = - Server} -> - case - ejabberd_auth:is_user_exists(User, - Server) - of - true -> - {true, - [{xmlcdata, - User}], - [#xmlel{name - = - <<"registered">>, - attrs - = - [], - children - = - []}]}; - false -> - {false, - [{xmlcdata, - User}], - []} - end; - _ -> {false, [], []} - end, - if IsCaptchaEnabled and not IsRegistered -> - TopInstrEl = #xmlel{name = <<"instructions">>, - attrs = [], - children = - [{xmlcdata, - translate:translate(Lang, - <<"You need a client that supports x:data " - "and CAPTCHA to register">>)}]}, - InstrEl = #xmlel{name = <<"instructions">>, attrs = [], - children = - [{xmlcdata, - translate:translate(Lang, - <<"Choose a username and password to register " - "with this server">>)}]}, - UField = #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, <<"text-single">>}, - {<<"label">>, - translate:translate(Lang, <<"User">>)}, - {<<"var">>, <<"username">>}], - children = - [#xmlel{name = <<"required">>, attrs = [], - children = []}]}, - PField = #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, <<"text-private">>}, - {<<"label">>, - translate:translate(Lang, - <<"Password">>)}, - {<<"var">>, <<"password">>}], - children = - [#xmlel{name = <<"required">>, attrs = [], - children = []}]}, - case ejabberd_captcha:create_captcha_x(ID, To, Lang, - Source, - [InstrEl, UField, - PField]) - of - {ok, CaptchaEls} -> - IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"query">>, - attrs = - [{<<"xmlns">>, - <<"jabber:iq:register">>}], - children = - [TopInstrEl | CaptchaEls]}]}; - {error, limit} -> - ErrText = <<"Too many CAPTCHA requests">>, - IQ#iq{type = error, - sub_el = - [SubEl, - ?ERRT_RESOURCE_CONSTRAINT(Lang, ErrText)]}; - _Err -> - ErrText = <<"Unable to generate a CAPTCHA">>, - IQ#iq{type = error, - sub_el = - [SubEl, - ?ERRT_INTERNAL_SERVER_ERROR(Lang, ErrText)]} - end; - true -> - IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"query">>, - attrs = - [{<<"xmlns">>, - <<"jabber:iq:register">>}], - children = - [#xmlel{name = <<"instructions">>, - attrs = [], - children = - [{xmlcdata, - translate:translate(Lang, - <<"Choose a username and password to register " - "with this server">>)}]}, - #xmlel{name = <<"username">>, - attrs = [], - children = UsernameSubels}, - #xmlel{name = <<"password">>, - attrs = [], children = []} - | QuerySubels]}]} - end +process_iq(#iq{from = From} = IQ) -> + process_iq(IQ, jid:tolower(From)). + +process_iq(#iq{from = From, to = To} = IQ, Source) -> + IsCaptchaEnabled = + case mod_register_opt:captcha_protected(To#jid.lserver) of + true -> true; + false -> false + end, + Server = To#jid.lserver, + Access = mod_register_opt:access_remove(Server), + Remove = case {acl:match_rule(Server, Access, From), From#jid.lserver} of + {allow, Server} -> + allow; + {_, _} -> + deny + end, + process_iq(IQ, Source, IsCaptchaEnabled, Remove == allow). + +process_iq(#iq{type = set, lang = Lang, + sub_els = [#register{remove = true}]} = IQ, + _Source, _IsCaptchaEnabled, _AllowRemove = false) -> + Txt = ?T("Access denied by service policy"), + make_stripped_error(IQ, xmpp:err_forbidden(Txt, Lang)); +process_iq(#iq{type = set, lang = Lang, to = To, from = From, + sub_els = [#register{remove = true, + username = User, + password = Password}]} = IQ, + _Source, _IsCaptchaEnabled, _AllowRemove = true) -> + Server = To#jid.lserver, + if is_binary(User) -> + case From of + #jid{user = User, lserver = Server} -> + ResIQ = xmpp:make_iq_result(IQ), + ejabberd_router:route(ResIQ), + ejabberd_auth:remove_user(User, Server), + ignore; + _ -> + if is_binary(Password) -> + case ejabberd_auth:check_password( + User, <<"">>, Server, Password) of + true -> + ResIQ = xmpp:make_iq_result(IQ), + ejabberd_router:route(ResIQ), + ejabberd_auth:remove_user(User, Server), + ignore; + false -> + Txt = ?T("Incorrect password"), + make_stripped_error( + IQ, xmpp:err_forbidden(Txt, Lang)) + end; + true -> + Txt = ?T("No 'password' found in this query"), + make_stripped_error(IQ, xmpp:err_bad_request(Txt, Lang)) + end + end; + true -> + case From of + #jid{luser = LUser, lserver = Server} -> + ResIQ = xmpp:make_iq_result(IQ), + ejabberd_router:route(xmpp:set_from_to(ResIQ, From, From)), + ejabberd_auth:remove_user(LUser, Server), + ignore; + _ -> + Txt = ?T("The query is only allowed from local users"), + make_stripped_error(IQ, xmpp:err_not_allowed(Txt, Lang)) + end + end; +process_iq(#iq{type = set, to = To, + sub_els = [#register{username = User, + password = Password}]} = IQ, + Source, IsCaptchaEnabled, _AllowRemove) when is_binary(User), + is_binary(Password) -> + Server = To#jid.lserver, + try_register_or_set_password( + User, Server, Password, IQ, Source, not IsCaptchaEnabled); +process_iq(#iq{type = set, to = To, + lang = Lang, sub_els = [#register{xdata = #xdata{} = X}]} = IQ, + Source, true, _AllowRemove) -> + Server = To#jid.lserver, + XdataC = xmpp_util:set_xdata_field( + #xdata_field{ + var = <<"FORM_TYPE">>, + type = hidden, values = [?NS_CAPTCHA]}, + X), + case ejabberd_captcha:process_reply(XdataC) of + ok -> + case process_xdata_submit(X) of + {ok, User, Password} -> + try_register_or_set_password( + User, Server, Password, IQ, Source, true); + _ -> + Txt = ?T("Incorrect data form"), + make_stripped_error(IQ, xmpp:err_bad_request(Txt, Lang)) + end; + {error, malformed} -> + Txt = ?T("Incorrect CAPTCHA submit"), + make_stripped_error(IQ, xmpp:err_bad_request(Txt, Lang)); + _ -> + ErrText = ?T("The CAPTCHA verification has failed"), + make_stripped_error(IQ, xmpp:err_not_allowed(ErrText, Lang)) + end; +process_iq(#iq{type = set} = IQ, _Source, _IsCaptchaEnabled, _AllowRemove) -> + make_stripped_error(IQ, xmpp:err_bad_request()); +process_iq(#iq{type = get, from = From, to = To, id = ID, lang = Lang} = IQ, + Source, IsCaptchaEnabled, _AllowRemove) -> + Server = To#jid.lserver, + {IsRegistered, Username} = + case From of + #jid{user = User, lserver = Server} -> + case ejabberd_auth:user_exists(User, Server) of + true -> + {true, User}; + false -> + {false, User} + end; + _ -> + {false, <<"">>} + end, + Instr = translate:translate( + Lang, ?T("Choose a username and password to register " + "with this server")), + URL = mod_register_opt:redirect_url(Server), + if (URL /= undefined) and not IsRegistered -> + Desc = str:translate_and_format(Lang, ?T("To register, visit ~s"), [URL]), + xmpp:make_iq_result( + IQ, #register{instructions = Desc, + sub_els = [#oob_x{url = URL}]}); + IsCaptchaEnabled and not IsRegistered -> + TopInstr = translate:translate( + Lang, ?T("You need a client that supports x:data " + "and CAPTCHA to register")), + UField = #xdata_field{type = 'text-single', + label = translate:translate(Lang, ?T("User")), + var = <<"username">>, + required = true}, + PField = #xdata_field{type = 'text-private', + label = translate:translate(Lang, ?T("Password")), + var = <<"password">>, + required = true}, + X = #xdata{type = form, instructions = [Instr], + fields = [UField, PField]}, + case ejabberd_captcha:create_captcha_x(ID, To, Lang, Source, X) of + {ok, CaptchaEls} -> + {value, XdataC, CaptchaEls2} = lists:keytake(xdata, 1, CaptchaEls), + Xdata = xmpp_util:set_xdata_field( + #xdata_field{ + var = <<"FORM_TYPE">>, + type = hidden, values = [?NS_REGISTER]}, + XdataC), + xmpp:make_iq_result( + IQ, #register{instructions = TopInstr, + sub_els = [Xdata | CaptchaEls2]}); + {error, limit} -> + ErrText = ?T("Too many CAPTCHA requests"), + make_stripped_error( + IQ, xmpp:err_resource_constraint(ErrText, Lang)); + _Err -> + ErrText = ?T("Unable to generate a CAPTCHA"), + make_stripped_error( + IQ, xmpp:err_internal_server_error(ErrText, Lang)) + end; + true -> + xmpp:make_iq_result( + IQ, + #register{instructions = Instr, + username = Username, + password = <<"">>, + registered = IsRegistered}) end. try_register_or_set_password(User, Server, Password, - From, IQ, SubEl, Source, Lang, CaptchaSucceed) -> - case From of - #jid{user = User, lserver = Server} -> - try_set_password(User, Server, Password, IQ, SubEl, - Lang); - _ when CaptchaSucceed -> - case check_from(From, Server) of - allow -> - case try_register(User, Server, Password, Source, Lang) - of - ok -> IQ#iq{type = result, sub_el = []}; - {error, Error} -> - IQ#iq{type = error, sub_el = [SubEl, Error]} - end; - deny -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_FORBIDDEN]} - end; - _ -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]} + #iq{from = From, lang = Lang} = IQ, + Source, CaptchaSucceed) -> + case {jid:nodeprep(User), From} of + {error, _} -> + Err = xmpp:err_jid_malformed(format_error(invalid_jid), Lang), + make_stripped_error(IQ, Err); + {UserP, #jid{user = User2, lserver = Server}} when UserP == User2 -> + try_set_password(User, Server, Password, IQ); + _ when CaptchaSucceed -> + case check_from(From, Server) of + allow -> + case try_register(User, Server, Password, Source, ?MODULE, Lang) of + ok -> + xmpp:make_iq_result(IQ); + {error, Error} -> + make_stripped_error(IQ, Error) + end; + deny -> + Txt = ?T("Access denied by service policy"), + make_stripped_error(IQ, xmpp:err_forbidden(Txt, Lang)) + end; + _ -> + make_stripped_error(IQ, xmpp:err_not_allowed()) end. -%% @doc Try to change password and return IQ response -try_set_password(User, Server, Password, IQ, SubEl, - Lang) -> +try_set_password(User, Server, Password) -> case is_strong_password(Server, Password) of - true -> - case ejabberd_auth:set_password(User, Server, Password) - of - ok -> IQ#iq{type = result, sub_el = []}; - {error, empty_password} -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_BAD_REQUEST]}; - {error, not_allowed} -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; - {error, invalid_jid} -> - IQ#iq{type = error, - sub_el = [SubEl, ?ERR_ITEM_NOT_FOUND]}; - _ -> - IQ#iq{type = error, - sub_el = [SubEl, ?ERR_INTERNAL_SERVER_ERROR]} - end; - false -> - ErrText = <<"The password is too weak">>, - IQ#iq{type = error, - sub_el = [SubEl, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)]} + true -> + ejabberd_auth:set_password(User, Server, Password); + error_preparing_password -> + {error, invalid_password}; + false -> + {error, weak_password} end. -try_register(User, Server, Password, SourceRaw, Lang) -> - case jlib:is_nodename(User) of - false -> {error, ?ERR_BAD_REQUEST}; - _ -> - JID = jlib:make_jid(User, Server, <<"">>), - Access = gen_mod:get_module_opt(Server, ?MODULE, access, - fun(A) when is_atom(A) -> A end, - all), - IPAccess = get_ip_access(Server), - case {acl:match_rule(Server, Access, JID), - check_ip_access(SourceRaw, IPAccess)} - of - {deny, _} -> {error, ?ERR_FORBIDDEN}; - {_, deny} -> {error, ?ERR_FORBIDDEN}; - {allow, allow} -> - Source = may_remove_resource(SourceRaw), - case check_timeout(Source) of - true -> - case is_strong_password(Server, Password) of +try_set_password(User, Server, Password, #iq{lang = Lang, meta = M} = IQ) -> + case try_set_password(User, Server, Password) of + ok -> + ?INFO_MSG("~ts has changed password from ~ts", + [jid:encode({User, Server, <<"">>}), + ejabberd_config:may_hide_data( + misc:ip_to_list(maps:get(ip, M, {0,0,0,0})))]), + xmpp:make_iq_result(IQ); + {error, not_allowed} -> + Txt = ?T("Changing password is not allowed"), + make_stripped_error(IQ, xmpp:err_not_allowed(Txt, Lang)); + {error, invalid_jid = Why} -> + make_stripped_error(IQ, xmpp:err_jid_malformed(format_error(Why), Lang)); + {error, invalid_password = Why} -> + make_stripped_error(IQ, xmpp:err_not_allowed(format_error(Why), Lang)); + {error, weak_password = Why} -> + make_stripped_error(IQ, xmpp:err_not_acceptable(format_error(Why), Lang)); + {error, db_failure = Why} -> + make_stripped_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 -> + {error, invalid_jid}; + true -> + case check_access(User, Server, SourceRaw) of + deny -> + {error, eaccess}; + allow -> + Source = may_remove_resource(SourceRaw), + case check_timeout(Source) of true -> - case ejabberd_auth:try_register(User, Server, - Password) - of - {atomic, ok} -> - send_welcome_message(JID), - send_registration_notifications( - ?MODULE, JID, Source), - ok; - Error -> - remove_timeout(Source), - case Error of - {atomic, exists} -> {error, ?ERR_CONFLICT}; - {error, invalid_jid} -> - {error, ?ERR_JID_MALFORMED}; - {error, not_allowed} -> - {error, ?ERR_NOT_ALLOWED}; - {error, _Reason} -> - {error, ?ERR_INTERNAL_SERVER_ERROR} - end + case is_strong_password(Server, Password) of + true -> + case ejabberd_auth:try_register( + User, Server, Password) of + ok -> + ok; + {error, _} = Err -> + remove_timeout(Source), + Err + end; + false -> + remove_timeout(Source), + {error, weak_password}; + error_preparing_password -> + remove_timeout(Source), + {error, invalid_password} end; false -> - ErrText = <<"The password is too weak">>, - {error, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)} - end; - false -> - ErrText = - <<"Users are not allowed to register accounts " - "so quickly">>, - {error, ?ERRT_RESOURCE_CONSTRAINT(Lang, ErrText)} - end - end + {error, wait} + end + end end. +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), + ?INFO_MSG("The account ~ts was registered from IP address ~ts", + [jid:encode({User, Server, <<"">>}), + ejabberd_config:may_hide_data(ip_to_string(Source))]), + send_welcome_message(JID), + send_registration_notifications(?MODULE, JID, Source); + {error, invalid_jid = Why} -> + {error, xmpp:err_jid_malformed(format_error(Why), Lang)}; + {error, eaccess = Why} -> + {error, xmpp:err_forbidden(format_error(Why), Lang)}; + {error, wait = Why} -> + {error, xmpp:err_resource_constraint(format_error(Why), Lang)}; + {error, weak_password = Why} -> + {error, xmpp:err_not_acceptable(format_error(Why), Lang)}; + {error, invalid_password = Why} -> + {error, xmpp:err_not_acceptable(format_error(Why), Lang)}; + {error, not_allowed = Why} -> + {error, xmpp:err_not_allowed(format_error(Why), Lang)}; + {error, exists = Why} -> + {error, xmpp:err_conflict(format_error(Why), Lang)}; + {error, db_failure = Why} -> + {error, xmpp:err_internal_server_error(format_error(Why), Lang)} + end. + +format_error(invalid_jid) -> + ?T("Malformed username"); +format_error(eaccess) -> + ?T("Access denied by service policy"); +format_error(wait) -> + ?T("Users are not allowed to register accounts so quickly"); +format_error(weak_password) -> + ?T("The password is too weak"); +format_error(invalid_password) -> + ?T("The password contains unacceptable characters"); +format_error(not_allowed) -> + ?T("Not allowed"); +format_error(exists) -> + ?T("User already exists"); +format_error(db_failure) -> + ?T("Database failure"); +format_error(Unexpected) -> + list_to_binary(io_lib:format(?T("Unexpected error condition: ~p"), [Unexpected])). + send_welcome_message(JID) -> Host = JID#jid.lserver, - case gen_mod:get_module_opt(Host, ?MODULE, welcome_message, - fun(Opts) -> - S = proplists:get_value( - subject, Opts, <<>>), - B = proplists:get_value( - body, Opts, <<>>), - {iolist_to_binary(S), - iolist_to_binary(B)} - end, {<<"">>, <<"">>}) - of + case mod_register_opt:welcome_message(Host) of {<<"">>, <<"">>} -> ok; {Subj, Body} -> - ejabberd_router:route(jlib:make_jid(<<"">>, Host, - <<"">>), - JID, - #xmlel{name = <<"message">>, - attrs = [{<<"type">>, <<"normal">>}], - children = - [#xmlel{name = <<"subject">>, - attrs = [], - children = - [{xmlcdata, Subj}]}, - #xmlel{name = <<"body">>, - attrs = [], - children = - [{xmlcdata, Body}]}]}); - _ -> ok + ejabberd_router:route( + #message{from = jid:make(Host), + to = JID, + type = chat, + subject = xmpp:mk_text(Subj), + body = xmpp:mk_text(Body)}) end. send_registration_notifications(Mod, UJID, Source) -> Host = UJID#jid.lserver, - case gen_mod:get_module_opt( - Host, Mod, registration_watchers, - fun(Ss) -> - [#jid{} = jlib:string_to_jid(iolist_to_binary(S)) - || S <- Ss] - end, []) of + case mod_register_opt:registration_watchers(Host) of [] -> ok; JIDs when is_list(JIDs) -> Body = - iolist_to_binary(io_lib:format("[~s] The account ~s was registered from " + (str:format("[~s] The account ~s was registered from " "IP address ~s on node ~w using ~p.", [get_time_string(), - jlib:jid_to_string(UJID), - ip_to_string(Source), node(), - Mod])), + jid:encode(UJID), + ejabberd_config:may_hide_data( + ip_to_string(Source)), + node(), Mod])), lists:foreach( fun(JID) -> ejabberd_router:route( - jlib:make_jid(<<"">>, Host, <<"">>), - JID, - #xmlel{name = <<"message">>, - attrs = [{<<"type">>, <<"chat">>}], - children = [#xmlel{name = <<"body">>, - attrs = [], - children = [{xmlcdata,Body}]}]}) + #message{from = jid:make(Host), + to = JID, + type = chat, + body = xmpp:mk_text(Body)}) end, JIDs) end. @@ -480,25 +450,14 @@ check_from(#jid{user = <<"">>, server = <<"">>}, _Server) -> allow; check_from(JID, Server) -> - Access = gen_mod:get_module_opt(Server, ?MODULE, access_from, - fun(A) when is_atom(A) -> A end, - none), + Access = mod_register_opt:access_from(Server), acl:match_rule(Server, Access, JID). check_timeout(undefined) -> true; check_timeout(Source) -> - Timeout = ejabberd_config:get_option( - registration_timeout, - fun(TO) when is_integer(TO), TO > 0 -> - TO; - (infinity) -> - infinity; - (unlimited) -> - infinity - end, 600), + Timeout = ejabberd_option:registration_timeout(), if is_integer(Timeout) -> - {MSec, Sec, _USec} = now(), - Priority = -(MSec * 1000000 + Sec), + Priority = -erlang:system_time(millisecond), CleanPriority = Priority + Timeout, F = fun () -> Treap = case mnesia:read(mod_register_ip, treap, write) @@ -521,8 +480,7 @@ check_timeout(Source) -> case mnesia:transaction(F) of {atomic, Res} -> Res; {aborted, Reason} -> - ?ERROR_MSG("mod_register: timeout check error: ~p~n", - [Reason]), + ?ERROR_MSG("timeout check error: ~p~n", [Reason]), true end; true -> true @@ -541,15 +499,7 @@ clean_treap(Treap, CleanPriority) -> remove_timeout(undefined) -> true; remove_timeout(Source) -> - Timeout = ejabberd_config:get_option( - registration_timeout, - fun(TO) when is_integer(TO), TO > 0 -> - TO; - (infinity) -> - infinity; - (unlimited) -> - infinity - end, 600), + Timeout = ejabberd_option:registration_timeout(), if is_integer(Timeout) -> F = fun () -> Treap = case mnesia:read(mod_register_ip, treap, write) @@ -564,7 +514,7 @@ remove_timeout(Source) -> case mnesia:transaction(F) of {atomic, ok} -> ok; {aborted, Reason} -> - ?ERROR_MSG("mod_register: timeout remove error: " + ?ERROR_MSG("Mod_register: timeout remove error: " "~p~n", [Reason]), ok @@ -572,8 +522,10 @@ remove_timeout(Source) -> true -> ok end. +ip_to_string({_, _, _} = USR) -> + jid:encode(USR); ip_to_string(Source) when is_tuple(Source) -> - jlib:ip_to_list(Source); + misc:ip_to_list(Source); ip_to_string(undefined) -> <<"undefined">>; ip_to_string(_) -> <<"unknown">>. @@ -584,99 +536,189 @@ write_time({{Y, Mo, D}, {H, Mi, S}}) -> io_lib:format("~w-~.2.0w-~.2.0w ~.2.0w:~.2.0w:~.2.0w", [Y, Mo, D, H, Mi, S]). -process_xdata_submit(El) -> - case xml:get_subtag(El, <<"x">>) of - false -> error; - Xdata -> - Fields = jlib:parse_xdata_submit(Xdata), - case catch {proplists:get_value(<<"username">>, Fields), - proplists:get_value(<<"password">>, Fields)} - of - {[User | _], [Pass | _]} -> {ok, User, Pass}; - _ -> error - end +process_xdata_submit(X) -> + case {xmpp_util:get_xdata_values(<<"username">>, X), + xmpp_util:get_xdata_values(<<"password">>, X)} of + {[User], [Pass]} -> {ok, User, Pass}; + _ -> error end. is_strong_password(Server, Password) -> - LServer = jlib:nameprep(Server), - case gen_mod:get_module_opt(LServer, ?MODULE, password_strength, - fun(N) when is_number(N), N>=0 -> N end, - 0) of + case jid:resourceprep(Password) of + PP when is_binary(PP) -> + is_strong_password2(Server, Password); + error -> + error_preparing_password + end. + +is_strong_password2(Server, Password) -> + LServer = jid:nameprep(Server), + case mod_register_opt:password_strength(LServer) of 0 -> true; Entropy -> ejabberd_auth:entropy(Password) >= Entropy end. -transform_options(Opts) -> - Opts1 = transform_ip_access(Opts), - transform_module_options(Opts1). - -transform_ip_access(Opts) -> - try - {value, {modules, ModOpts}, Opts1} = lists:keytake(modules, 1, Opts), - {value, {?MODULE, RegOpts}, ModOpts1} = lists:keytake(?MODULE, 1, ModOpts), - {value, {ip_access, L}, RegOpts1} = lists:keytake(ip_access, 1, RegOpts), - true = is_list(L), - ?WARNING_MSG("Old 'ip_access' format detected. " - "The old format is still supported " - "but it is better to fix your config: " - "use access rules instead.", []), - ACLs = lists:flatmap( - fun({Action, S}) -> - ACLName = jlib:binary_to_atom( - iolist_to_binary( - ["ip_", S])), - [{Action, ACLName}, - {acl, ACLName, {ip, S}}] - end, L), - Access = {access, mod_register_networks, - [{Action, ACLName} || {Action, ACLName} <- ACLs]}, - [ACL || {acl, _, _} = ACL <- ACLs] ++ - [Access, - {modules, - [{mod_register, - [{ip_access, mod_register_networks}|RegOpts1]} - | ModOpts1]}|Opts1] - catch error:{badmatch, false} -> - Opts - end. - -transform_module_options(Opts) -> - lists:flatmap( - fun({welcome_message, {Subj, Body}}) -> - ?WARNING_MSG("Old 'welcome_message' format detected. " - "The old format is still supported " - "but it is better to fix your config: " - "change it to {welcome_message, " - "[{subject, Subject}, {body, Body}]}", - []), - [{welcome_message, [{subject, Subj}, {body, Body}]}]; - (Opt) -> - [Opt] - end, Opts). +make_stripped_error(IQ, Err) -> + xmpp:make_error(xmpp:remove_subtag(IQ, #register{}), Err). %%% %%% ip_access management %%% may_remove_resource({_, _, _} = From) -> - jlib:jid_remove_resource(From); + jid:remove_resource(From); may_remove_resource(From) -> From. get_ip_access(Host) -> - gen_mod:get_module_opt(Host, ?MODULE, ip_access, - fun(A) when is_atom(A) -> A end, - all). + mod_register_opt:ip_access(Host). -check_ip_access({User, Server, Resource}, IPAccess) -> +check_ip_access(Server, {User, Server, Resource}, IPAccess) -> case ejabberd_sm:get_user_ip(User, Server, Resource) of {IPAddress, _PortNumber} -> - check_ip_access(IPAddress, IPAccess); + check_ip_access(Server, IPAddress, IPAccess); _ -> deny end; -check_ip_access(undefined, _IPAccess) -> +check_ip_access(_Server, undefined, _IPAccess) -> deny; -check_ip_access(IPAddress, IPAccess) -> - acl:match_rule(global, IPAccess, IPAddress). +check_ip_access(Server, IPAddress, IPAccess) -> + acl:match_rule(Server, IPAccess, IPAddress). + +check_access(User, Server, Source) -> + JID = jid:make(User, Server), + Access = mod_register_opt:access(Server), + IPAccess = get_ip_access(Server), + case acl:match_rule(Server, Access, JID) of + allow -> check_ip_access(Server, Source, IPAccess); + deny -> deny + end. + +mod_opt_type(access) -> + econf:acl(); +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) -> + econf:acl(); +mod_opt_type(password_strength) -> + econf:number(0); +mod_opt_type(registration_watchers) -> + econf:list(econf:jid()); +mod_opt_type(welcome_message) -> + econf:and_then( + econf:options( + #{subject => econf:binary(), + body => econf:binary()}), + fun(Opts) -> + {proplists:get_value(subject, Opts, <<>>), + proplists:get_value(body, Opts, <<>>)} + end); +mod_opt_type(redirect_url) -> + econf:url(). + +-spec mod_options(binary()) -> [{welcome_message, {binary(), binary()}} | + {atom(), term()}]. +mod_options(_Host) -> + [{access, all}, + {access_from, none}, + {access_remove, all}, + {allow_modules, all}, + {captcha_protected, false}, + {ip_access, all}, + {password_strength, 0}, + {registration_watchers, []}, + {redirect_url, undefined}, + {welcome_message, {<<>>, <<>>}}]. + +mod_doc() -> + #{desc => + [?T("This module adds support for https://xmpp.org/extensions/xep-0077.html" + "[XEP-0077: In-Band Registration]. " + "This protocol enables end users to use an XMPP client to:"), "", + ?T("* Register a new account on the server."), "", + ?T("* Change the password from an existing account on the server."), "", + ?T("* Delete an existing account on the server."), "", + ?T("This module reads also the top-level _`registration_timeout`_ " + "option defined globally for the server, " + "so please check that option documentation too.")], + opts => + [{access, + #{value => ?T("AccessName"), + desc => + ?T("Specify rules to restrict what usernames can be registered. " + "If a rule returns 'deny' on the requested username, " + "registration of that user name is denied. There are no " + "restrictions by default. " + "If 'AccessName' is 'none', then registering new accounts using " + "In-Band Registration is disabled and the corresponding " + "stream feature is not announced to clients.")}}, + {access_from, + #{value => ?T("AccessName"), + desc => + ?T("By default, ejabberd doesn't allow the client to register new accounts " + "from s2s or existing c2s sessions. You can change it by defining " + "access rule in this option. Use with care: allowing registration " + "from s2s leads to uncontrolled massive accounts creation by rogue users.")}}, + {access_remove, + #{value => ?T("AccessName"), + 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 => + ?T("Protect registrations with _`basic.md#captcha|CAPTCHA`_. " + "The default is 'false'.")}}, + {ip_access, + #{value => ?T("AccessName"), + desc => + ?T("Define rules to allow or deny account registration depending " + "on the IP address of the XMPP client. The 'AccessName' should " + "be of type 'ip'. The default value is 'all'.")}}, + {password_strength, + #{value => "Entropy", + desc => + ?T("This option sets the minimum " + "https://en.wikipedia.org/wiki/Entropy_(information_theory)" + "[Shannon entropy] for passwords. The value 'Entropy' is a " + "number of bits of entropy. The recommended minimum is 32 bits. " + "The default is '0', i.e. no checks are performed.")}}, + {registration_watchers, + #{value => "[JID, ...]", + desc => + ?T("This option defines a list of JIDs which will be notified each " + "time a new account is registered.")}}, + {redirect_url, + #{value => ?T("URL"), + desc => + ?T("This option enables registration redirection as described in " + "https://xmpp.org/extensions/xep-0077.html#redirect" + "[XEP-0077: In-Band Registration: Redirection].")}}, + {welcome_message, + #{value => "{subject: Subject, body: Body}", + desc => + ?T("Set a welcome message that is sent to each newly registered account. " + "The message will have subject 'Subject' and text 'Body'."), + example => + ["modules:", + " mod_register:", + " welcome_message:", + " subject: \"Welcome!\"", + " body: |-", + " Hi!", + " Welcome to this XMPP server"]}} + ]}. diff --git a/src/mod_register_opt.erl b/src/mod_register_opt.erl new file mode 100644 index 000000000..e7236424c --- /dev/null +++ b/src/mod_register_opt.erl @@ -0,0 +1,76 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_register_opt). + +-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]). +-export([redirect_url/1]). +-export([registration_watchers/1]). +-export([welcome_message/1]). + +-spec access(gen_mod:opts() | global | binary()) -> 'all' | acl:acl(). +access(Opts) when is_map(Opts) -> + gen_mod:get_opt(access, Opts); +access(Host) -> + gen_mod:get_module_opt(Host, mod_register, access). + +-spec access_from(gen_mod:opts() | global | binary()) -> 'none' | acl:acl(). +access_from(Opts) when is_map(Opts) -> + gen_mod:get_opt(access_from, Opts); +access_from(Host) -> + gen_mod:get_module_opt(Host, mod_register, access_from). + +-spec access_remove(gen_mod:opts() | global | binary()) -> 'all' | acl:acl(). +access_remove(Opts) when is_map(Opts) -> + gen_mod:get_opt(access_remove, 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); +captcha_protected(Host) -> + gen_mod:get_module_opt(Host, mod_register, captcha_protected). + +-spec ip_access(gen_mod:opts() | global | binary()) -> 'all' | acl:acl(). +ip_access(Opts) when is_map(Opts) -> + gen_mod:get_opt(ip_access, Opts); +ip_access(Host) -> + gen_mod:get_module_opt(Host, mod_register, ip_access). + +-spec password_strength(gen_mod:opts() | global | binary()) -> number(). +password_strength(Opts) when is_map(Opts) -> + gen_mod:get_opt(password_strength, Opts); +password_strength(Host) -> + gen_mod:get_module_opt(Host, mod_register, password_strength). + +-spec redirect_url(gen_mod:opts() | global | binary()) -> 'undefined' | binary(). +redirect_url(Opts) when is_map(Opts) -> + gen_mod:get_opt(redirect_url, Opts); +redirect_url(Host) -> + gen_mod:get_module_opt(Host, mod_register, redirect_url). + +-spec registration_watchers(gen_mod:opts() | global | binary()) -> [jid:jid()]. +registration_watchers(Opts) when is_map(Opts) -> + gen_mod:get_opt(registration_watchers, Opts); +registration_watchers(Host) -> + gen_mod:get_module_opt(Host, mod_register, registration_watchers). + +-spec welcome_message(gen_mod:opts() | global | binary()) -> {binary(),binary()}. +welcome_message(Opts) when is_map(Opts) -> + gen_mod:get_opt(welcome_message, Opts); +welcome_message(Host) -> + gen_mod:get_module_opt(Host, mod_register, welcome_message). + diff --git a/src/mod_register_web.erl b/src/mod_register_web.erl index fb94923f6..b91b4637b 100644 --- a/src/mod_register_web.erl +++ b/src/mod_register_web.erl @@ -5,7 +5,7 @@ %%% Created : 4 May 2008 by Badlop %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -23,126 +23,124 @@ %%% %%%---------------------------------------------------------------------- -%%% IDEAS: -%%% -%%% * Implement those options, already present in mod_register: -%%% + access -%%% + captcha_protected -%%% + password_strength -%%% + welcome_message -%%% + registration_timeout -%%% -%%% * Improve this module to allow each virtual host to have different -%%% options. See http://support.process-one.net/browse/EJAB-561 -%%% -%%% * Check that all the text is translatable. -%%% -%%% * Add option to use a custom CSS file, or custom CSS lines. -%%% -%%% * Don't hardcode the "register" path in URL. -%%% -%%% * Allow private email during register, and store in custom table. -%%% * Optionally require private email to register. -%%% * Optionally require email confirmation to register. -%%% * Allow to set a private email address anytime. -%%% * Allow to recover password using private email to confirm (mod_passrecover) -%%% * Optionally require invitation -%%% * Optionally register request is forwarded to admin, no account created. - -module(mod_register_web). -author('badlop@process-one.net'). -behaviour(gen_mod). --export([start/2, stop/1, process/2]). +-export([start/2, stop/1, reload/3, process/2, mod_options/1, depends/2]). +-export([mod_doc/0]). --include("ejabberd.hrl"). -include("logger.hrl"). --include("jlib.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). -include("ejabberd_http.hrl"). -include("ejabberd_web_admin.hrl"). +-include("translate.hrl"). + %%%---------------------------------------------------------------------- %%% gen_mod callbacks %%%---------------------------------------------------------------------- start(_Host, _Opts) -> - %% case gen_mod:get_opt(docroot, Opts, fun(A) -> A end, undefined) of + %% case mod_register_web_opt:docroot(Opts, fun(A) -> A end, undefined) of ok. stop(_Host) -> ok. +reload(_Host, _NewOpts, _OldOpts) -> + ok. + +depends(_Host, _Opts) -> + [{mod_register, hard}]. + %%%---------------------------------------------------------------------- %%% HTTP handlers %%%---------------------------------------------------------------------- -process([], #request{method = 'GET', lang = Lang}) -> +process(Path, #request{raw_path = RawPath} = Request) -> + Continue = case Path of + [E] -> + binary:match(E, <<".">>) /= nomatch; + _ -> + false + end, + case Continue orelse binary:at(RawPath, size(RawPath) - 1) == $/ of + true -> + process2(Path, Request); + _ -> + {301, [{<<"Location">>, <>}], <<>>} + end. + +process2([], #request{method = 'GET', lang = Lang}) -> index_page(Lang); -process([<<"register.css">>], +process2([<<"register.css">>], #request{method = 'GET'}) -> serve_css(); -process([<<"new">>], +process2([Section], #request{method = 'GET', lang = Lang, host = Host, - ip = IP}) -> - {Addr, _Port} = IP, form_new_get(Host, Lang, Addr); -process([<<"delete">>], - #request{method = 'GET', lang = Lang, host = Host}) -> - form_del_get(Host, Lang); -process([<<"change_password">>], - #request{method = 'GET', lang = Lang, host = Host}) -> - form_changepass_get(Host, Lang); -process([<<"new">>], + ip = {Addr, _Port}}) -> + Host2 = case ejabberd_router:is_my_host(Host) of + true -> + Host; + false -> + <<"">> + end, + case Section of + <<"new">> -> form_new_get(Host2, Lang, Addr); + <<"delete">> -> form_del_get(Host2, Lang); + <<"change_password">> -> form_changepass_get(Host2, Lang); + _ -> {404, [], "Not Found"} + end; +process2([<<"new">>], #request{method = 'POST', q = Q, ip = {Ip, _Port}, - lang = Lang, host = Host}) -> - case form_new_post(Q, Host) of + lang = Lang, host = _HTTPHost}) -> + case form_new_post(Q, Ip) of {success, ok, {Username, Host, _Password}} -> - Jid = jlib:make_jid(Username, Host, <<"">>), + Jid = jid:make(Username, Host), mod_register:send_registration_notifications(?MODULE, Jid, Ip), - Text = (?T(<<"Your Jabber account was successfully " - "created.">>)), + Text = translate:translate(Lang, ?T("Your XMPP account was successfully registered.")), {200, [], Text}; Error -> ErrorText = - list_to_binary([?T(<<"There was an error creating the account: ">>), - ?T(get_error_text(Error))]), + list_to_binary([translate:translate(Lang, ?T("There was an error creating the account: ")), + translate:translate(Lang, get_error_text(Error))]), {404, [], ErrorText} end; -process([<<"delete">>], +process2([<<"delete">>], #request{method = 'POST', q = Q, lang = Lang, - host = Host}) -> - case form_del_post(Q, Host) of + host = _HTTPHost}) -> + case form_del_post(Q) of {atomic, ok} -> - Text = (?T(<<"Your Jabber account was successfully " - "deleted.">>)), + Text = translate:translate(Lang, ?T("Your XMPP account was successfully unregistered.")), {200, [], Text}; Error -> ErrorText = - list_to_binary([?T(<<"There was an error deleting the account: ">>), - ?T(get_error_text(Error))]), + list_to_binary([translate:translate(Lang, ?T("There was an error deleting the account: ")), + translate:translate(Lang, get_error_text(Error))]), {404, [], ErrorText} end; %% TODO: Currently only the first vhost is usable. The web request record %% should include the host where the POST was sent. -process([<<"change_password">>], +process2([<<"change_password">>], #request{method = 'POST', q = Q, lang = Lang, - host = Host}) -> - case form_changepass_post(Q, Host) of + host = _HTTPHost}) -> + case form_changepass_post(Q) of {atomic, ok} -> - Text = (?T(<<"The password of your Jabber account " - "was successfully changed.">>)), + Text = translate:translate(Lang, ?T("The password of your XMPP account was successfully changed.")), {200, [], Text}; Error -> ErrorText = - list_to_binary([?T(<<"There was an error changing the password: ">>), - ?T(get_error_text(Error))]), + list_to_binary([translate:translate(Lang, ?T("There was an error changing the password: ")), + translate:translate(Lang, get_error_text(Error))]), {404, [], ErrorText} end; -process(_Path, _Request) -> +process2(_Path, _Request) -> {404, [], "Not Found"}. %%%---------------------------------------------------------------------- @@ -150,10 +148,14 @@ process(_Path, _Request) -> %%%---------------------------------------------------------------------- serve_css() -> - {200, - [{<<"Content-Type">>, <<"text/css">>}, last_modified(), - cache_control_public()], - css()}. + case css() of + {ok, CSS} -> + {200, + [{<<"Content-Type">>, <<"text/css">>}, last_modified(), + cache_control_public()], CSS}; + error -> + {404, [], "CSS not found"} + end. last_modified() -> {<<"Last-Modified">>, @@ -162,33 +164,47 @@ last_modified() -> cache_control_public() -> {<<"Cache-Control">>, <<"public">>}. +-spec css() -> {ok, binary()} | error. css() -> - <<"html,body {\nbackground: white;\nmargin: " - "0;\npadding: 0;\nheight: 100%;\n}">>. + Dir = misc:css_dir(), + File = filename:join(Dir, "register.css"), + case file:read_file(File) of + {ok, Data} -> + {ok, Data}; + {error, Why} -> + ?ERROR_MSG("Failed to read ~ts: ~ts", [File, file:format_error(Why)]), + error + end. + +meta() -> + ?XA(<<"meta">>, + [{<<"name">>, <<"viewport">>}, + {<<"content">>, <<"width=device-width, initial-scale=1">>}]). %%%---------------------------------------------------------------------- %%% Index page %%%---------------------------------------------------------------------- index_page(Lang) -> - HeadEls = [?XCT(<<"title">>, - <<"Jabber Account Registration">>), + HeadEls = [meta(), + ?XCT(<<"title">>, + ?T("XMPP Account Registration")), ?XA(<<"link">>, - [{<<"href">>, <<"/register/register.css">>}, + [{<<"href">>, <<"register.css">>}, {<<"type">>, <<"text/css">>}, {<<"rel">>, <<"stylesheet">>}])], Els = [?XACT(<<"h1">>, [{<<"class">>, <<"title">>}, {<<"style">>, <<"text-align:center;">>}], - <<"Jabber Account Registration">>), + ?T("XMPP Account Registration")), ?XE(<<"ul">>, [?XE(<<"li">>, - [?ACT(<<"new">>, <<"Register a Jabber account">>)]), + [?ACT(<<"new/">>, ?T("Register an XMPP account"))]), ?XE(<<"li">>, - [?ACT(<<"change_password">>, <<"Change Password">>)]), + [?ACT(<<"change_password/">>, ?T("Change Password"))]), ?XE(<<"li">>, - [?ACT(<<"delete">>, - <<"Unregister a Jabber account">>)])])], + [?ACT(<<"delete/">>, + ?T("Unregister an XMPP account"))])])], {200, [{<<"Server">>, <<"ejabberd">>}, {<<"Content-Type">>, <<"text/html">>}], @@ -199,73 +215,84 @@ index_page(Lang) -> %%%---------------------------------------------------------------------- form_new_get(Host, Lang, IP) -> - CaptchaEls = build_captcha_li_list(Lang, IP), - HeadEls = [?XCT(<<"title">>, - <<"Register a Jabber account">>), + try build_captcha_li_list(Lang, IP) of + CaptchaEls -> + form_new_get2(Host, Lang, CaptchaEls) + catch + throw:Result -> + ?DEBUG("Unexpected result when creating a captcha: ~p", [Result]), + ejabberd_web:error(not_allowed) + end. + +form_new_get2(Host, Lang, CaptchaEls) -> + HeadEls = [meta(), + ?XCT(<<"title">>, + ?T("Register an XMPP account")), ?XA(<<"link">>, - [{<<"href">>, <<"/register/register.css">>}, + [{<<"href">>, <<"../register.css">>}, {<<"type">>, <<"text/css">>}, {<<"rel">>, <<"stylesheet">>}])], Els = [?XACT(<<"h1">>, [{<<"class">>, <<"title">>}, {<<"style">>, <<"text-align:center;">>}], - <<"Register a Jabber account">>), + ?T("Register an XMPP account")), ?XCT(<<"p">>, - <<"This page allows to create a Jabber " - "account in this Jabber server. Your " - "JID (Jabber IDentifier) will be of the " - "form: username@server. Please read carefully " - "the instructions to fill correctly the " - "fields.">>), + ?T("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.")), ?XAE(<<"form">>, [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], [?XE(<<"ol">>, ([?XE(<<"li">>, - [?CT(<<"Username:">>), ?C(<<" ">>), + [?CT(?T("Username:")), ?C(<<" ">>), ?INPUTS(<<"text">>, <<"username">>, <<"">>, <<"20">>), ?BR, ?XE(<<"ul">>, [?XCT(<<"li">>, - <<"This is case insensitive: macbeth is " - "the same that MacBeth and Macbeth.">>), + ?T("This is case insensitive: macbeth is " + "the same that MacBeth and Macbeth.")), ?XC(<<"li">>, - <<(?T(<<"Characters not allowed:">>))/binary, + <<(translate:translate(Lang, ?T("Characters not allowed:")))/binary, " \" & ' / : < > @ ">>)])]), ?XE(<<"li">>, - [?CT(<<"Server:">>), ?C(<<" ">>), ?C(Host)]), + [?CT(?T("Server:")), ?C(<<" ">>), + ?INPUTS(<<"text">>, <<"host">>, Host, <<"20">>)]), ?XE(<<"li">>, - [?CT(<<"Password:">>), ?C(<<" ">>), + [?CT(?T("Password:")), ?C(<<" ">>), ?INPUTS(<<"password">>, <<"password">>, <<"">>, <<"20">>), ?BR, ?XE(<<"ul">>, [?XCT(<<"li">>, - <<"Don't tell your password to anybody, " - "not even the administrators of the Jabber " - "server.">>), + ?T("Don't tell your password to anybody, " + "not even the administrators of the XMPP " + "server.")), ?XCT(<<"li">>, - <<"You can later change your password using " - "a Jabber client.">>), + ?T("You can later change your password using " + "an XMPP client.")), ?XCT(<<"li">>, - <<"Some Jabber clients can store your password " - "in your computer. Use that feature only " - "if you trust your computer is safe.">>), + ?T("Some XMPP clients can store your password " + "in the computer, but you should do this only " + "in your personal computer for safety reasons.")), ?XCT(<<"li">>, - <<"Memorize your password, or write it " - "in a paper placed in a safe place. In " - "Jabber there isn't an automated way " - "to recover your password if you forget " - "it.">>)])]), + ?T("Memorize your password, or write it " + "in a paper placed in a safe place. In " + "XMPP there isn't an automated way " + "to recover your password if you forget " + "it."))])]), ?XE(<<"li">>, - [?CT(<<"Password Verification:">>), ?C(<<" ">>), + [?CT(?T("Password Verification:")), ?C(<<" ">>), ?INPUTS(<<"password">>, <<"password2">>, <<"">>, <<"20">>)])] ++ CaptchaEls ++ [?XE(<<"li">>, [?INPUTT(<<"submit">>, <<"register">>, - <<"Register">>)])]))])], + ?T("Register"))])]))])], {200, [{<<"Server">>, <<"ejabberd">>}, {<<"Content-Type">>, <<"text/html">>}], @@ -277,13 +304,13 @@ form_new_get(Host, Lang, IP) -> %%% Formulary new POST %%%---------------------------------------------------------------------- -form_new_post(Q, Host) -> +form_new_post(Q, Ip) -> case catch get_register_parameters(Q) of - [Username, Password, Password, Id, Key] -> - form_new_post(Username, Host, Password, {Id, Key}); - [_Username, _Password, _Password2, false, false] -> + [Username, Host, Password, Password, Id, Key] -> + form_new_post(Username, Host, Password, {Id, Key}, Ip); + [_Username, _Host, _Password, _Password2, false, false] -> {error, passwords_not_identical}; - [_Username, _Password, _Password2, Id, Key] -> + [_Username, _Host, _Password, _Password2, Id, Key] -> ejabberd_captcha:check_captcha(Id, Key), {error, passwords_not_identical}; _ -> {error, wrong_parameters} @@ -296,16 +323,15 @@ get_register_parameters(Q) -> false -> false end end, - [<<"username">>, <<"password">>, <<"password2">>, + [<<"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. @@ -327,15 +353,18 @@ build_captcha_li_list2(Lang, IP) -> To = #jid{user = <<"">>, server = <<"test">>, resource = <<"">>}, Args = [], - case ejabberd_captcha:create_captcha(SID, From, To, - Lang, IP, Args) - of - {ok, Id, _} -> - {_, {CImg, CText, CId, CKey}} = - ejabberd_captcha:build_captcha_html(Id, Lang), - [?XE(<<"li">>, - [CText, ?C(<<" ">>), CId, CKey, ?BR, CImg])]; - _ -> [] + case ejabberd_captcha:create_captcha( + SID, From, To, Lang, IP, Args) of + {ok, Id, _, _} -> + case ejabberd_captcha:build_captcha_html(Id, Lang) of + {_, {CImg, CText, CId, CKey}} -> + [?XE(<<"li">>, + [CText, ?C(<<" ">>), CId, CKey, ?BR, CImg])]; + Error -> + throw(Error) + end; + Error -> + throw(Error) end. %%%---------------------------------------------------------------------- @@ -343,39 +372,41 @@ build_captcha_li_list2(Lang, IP) -> %%%---------------------------------------------------------------------- form_changepass_get(Host, Lang) -> - HeadEls = [?XCT(<<"title">>, <<"Change Password">>), + HeadEls = [meta(), + ?XCT(<<"title">>, ?T("Change Password")), ?XA(<<"link">>, - [{<<"href">>, <<"/register/register.css">>}, + [{<<"href">>, <<"../register.css">>}, {<<"type">>, <<"text/css">>}, {<<"rel">>, <<"stylesheet">>}])], Els = [?XACT(<<"h1">>, [{<<"class">>, <<"title">>}, {<<"style">>, <<"text-align:center;">>}], - <<"Change Password">>), + ?T("Change Password")), ?XAE(<<"form">>, [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], [?XE(<<"ol">>, [?XE(<<"li">>, - [?CT(<<"Username:">>), ?C(<<" ">>), + [?CT(?T("Username:")), ?C(<<" ">>), ?INPUTS(<<"text">>, <<"username">>, <<"">>, <<"20">>)]), ?XE(<<"li">>, - [?CT(<<"Server:">>), ?C(<<" ">>), ?C(Host)]), + [?CT(?T("Server:")), ?C(<<" ">>), + ?INPUTS(<<"text">>, <<"host">>, Host, <<"20">>)]), ?XE(<<"li">>, - [?CT(<<"Old Password:">>), ?C(<<" ">>), + [?CT(?T("Old Password:")), ?C(<<" ">>), ?INPUTS(<<"password">>, <<"passwordold">>, <<"">>, <<"20">>)]), ?XE(<<"li">>, - [?CT(<<"New Password:">>), ?C(<<" ">>), + [?CT(?T("New Password:")), ?C(<<" ">>), ?INPUTS(<<"password">>, <<"password">>, <<"">>, <<"20">>)]), ?XE(<<"li">>, - [?CT(<<"Password Verification:">>), ?C(<<" ">>), + [?CT(?T("Password Verification:")), ?C(<<" ">>), ?INPUTS(<<"password">>, <<"password2">>, <<"">>, <<"20">>)]), ?XE(<<"li">>, [?INPUTT(<<"submit">>, <<"changepass">>, - <<"Change Password">>)])])])], + ?T("Change Password"))])])])], {200, [{<<"Server">>, <<"ejabberd">>}, {<<"Content-Type">>, <<"text/html">>}], @@ -385,12 +416,12 @@ form_changepass_get(Host, Lang) -> %%% Formulary change password POST %%%---------------------------------------------------------------------- -form_changepass_post(Q, Host) -> +form_changepass_post(Q) -> case catch get_changepass_parameters(Q) of - [Username, PasswordOld, Password, Password] -> + [Username, Host, PasswordOld, Password, Password] -> try_change_password(Username, Host, PasswordOld, Password); - [_Username, _PasswordOld, _Password, _Password2] -> + [_Username, _Host, _PasswordOld, _Password, _Password2] -> {error, passwords_not_identical}; _ -> {error, wrong_parameters} end. @@ -404,7 +435,7 @@ get_changepass_parameters(Q) -> {value, {_Key, Value}} = lists:keysearch(Key, 1, Q), Value end, - [<<"username">>, <<"passwordold">>, <<"password">>, + [<<"username">>, <<"host">>, <<"passwordold">>, <<"password">>, <<"password2">>]). try_change_password(Username, Host, PasswordOld, @@ -430,13 +461,13 @@ change_password(Username, Host, PasswordOld, end. check_account_exists(Username, Host) -> - case ejabberd_auth:is_user_exists(Username, Host) of + case ejabberd_auth:user_exists(Username, Host) of true -> account_exists; false -> account_doesnt_exist end. check_password(Username, Host, Password) -> - case ejabberd_auth:check_password(Username, Host, + case ejabberd_auth:check_password(Username, <<"">>, Host, Password) of true -> password_correct; @@ -448,63 +479,66 @@ check_password(Username, Host, Password) -> %%%---------------------------------------------------------------------- form_del_get(Host, Lang) -> - HeadEls = [?XCT(<<"title">>, - <<"Unregister a Jabber account">>), + HeadEls = [meta(), + ?XCT(<<"title">>, + ?T("Unregister an XMPP account")), ?XA(<<"link">>, - [{<<"href">>, <<"/register/register.css">>}, + [{<<"href">>, <<"../register.css">>}, {<<"type">>, <<"text/css">>}, {<<"rel">>, <<"stylesheet">>}])], Els = [?XACT(<<"h1">>, [{<<"class">>, <<"title">>}, {<<"style">>, <<"text-align:center;">>}], - <<"Unregister a Jabber account">>), + ?T("Unregister an XMPP account")), ?XCT(<<"p">>, - <<"This page allows to unregister a Jabber " - "account in this Jabber server.">>), + ?T("This page allows to unregister an XMPP " + "account in this XMPP server.")), ?XAE(<<"form">>, [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], [?XE(<<"ol">>, [?XE(<<"li">>, - [?CT(<<"Username:">>), ?C(<<" ">>), + [?CT(?T("Username:")), ?C(<<" ">>), ?INPUTS(<<"text">>, <<"username">>, <<"">>, <<"20">>)]), ?XE(<<"li">>, - [?CT(<<"Server:">>), ?C(<<" ">>), ?C(Host)]), + [?CT(?T("Server:")), ?C(<<" ">>), + ?INPUTS(<<"text">>, <<"host">>, Host, <<"20">>)]), ?XE(<<"li">>, - [?CT(<<"Password:">>), ?C(<<" ">>), + [?CT(?T("Password:")), ?C(<<" ">>), ?INPUTS(<<"password">>, <<"password">>, <<"">>, <<"20">>)]), ?XE(<<"li">>, [?INPUTT(<<"submit">>, <<"unregister">>, - <<"Unregister">>)])])])], + ?T("Unregister"))])])])], {200, [{<<"Server">>, <<"ejabberd">>}, {<<"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) -> - Access = gen_mod:get_module_opt(Host, mod_register, access, - fun(A) when is_atom(A) -> A end, - all), - case jlib:make_jid(Username, Host, <<"">>) of - error -> {error, invalid_jid}; - JID -> - case acl:match_rule(Host, Access, JID) of - deny -> {error, not_allowed}; - allow -> register_account2(Username, Host, Password) - end +register_account(Username, Host, Password, Ip) -> + try mod_register_opt:access(Host) of + Access -> + case jid:make(Username, Host) of + error -> {error, invalid_jid}; + JID -> + case acl:match_rule(Host, Access, JID) of + deny -> {error, not_allowed}; + 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 - {atomic, Res} -> - {success, Res, {Username, Host, Password}}; + ok -> + {success, ok, {Username, Host, Password}}; Other -> Other end. @@ -512,9 +546,9 @@ register_account2(Username, Host, Password) -> %%% Formulary delete POST %%%---------------------------------------------------------------------- -form_del_post(Q, Host) -> +form_del_post(Q) -> case catch get_unregister_parameters(Q) of - [Username, Password] -> + [Username, Host, Password] -> try_unregister_account(Username, Host, Password); _ -> {error, wrong_parameters} end. @@ -528,7 +562,7 @@ get_unregister_parameters(Q) -> {value, {_Key, Value}} = lists:keysearch(Key, 1, Q), Value end, - [<<"username">>, <<"password">>]). + [<<"username">>, <<"host">>, <<"password">>]). try_unregister_account(Username, Host, Password) -> try unregister_account(Username, Host, Password) of @@ -552,24 +586,56 @@ unregister_account(Username, Host, Password) -> %%%---------------------------------------------------------------------- get_error_text({error, captcha_non_valid}) -> - <<"The captcha you entered is wrong">>; -get_error_text({success, exists, _}) -> - get_error_text({atomic, exists}); -get_error_text({atomic, exists}) -> - <<"The account already exists">>; + ?T("The captcha you entered is wrong"); +get_error_text({error, exists}) -> + ?T("The account already exists"); get_error_text({error, password_incorrect}) -> - <<"Incorrect password">>; -get_error_text({error, invalid_jid}) -> - <<"The username is not valid">>; -get_error_text({error, not_allowed}) -> - <<"Not allowed">>; + ?T("Incorrect password"); +get_error_text({error, host_unknown}) -> + ?T("Host unknown"); get_error_text({error, account_doesnt_exist}) -> - <<"Account doesn't exist">>; + ?T("Account doesn't exist"); get_error_text({error, account_exists}) -> - <<"The account was not deleted">>; + ?T("The account was not unregistered"); get_error_text({error, password_not_changed}) -> - <<"The password was not changed">>; + ?T("The password was not changed"); get_error_text({error, passwords_not_identical}) -> - <<"The passwords are different">>; + ?T("The passwords are different"); get_error_text({error, wrong_parameters}) -> - <<"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(_) -> + []. + +mod_doc() -> + #{desc => + [?T("This module provides a web page where users can:"), "", + ?T("- Register a new account on the server."), "", + ?T("- Change the password from an existing account on the server."), "", + ?T("- Unregister an existing account on the server."), "", + ?T("This module supports _`basic.md#captcha|CAPTCHA`_ " + "to register a new account. " + "To enable this feature, configure the " + "top-level _`captcha_cmd`_ and " + "top-level _`captcha_url`_ options."), "", + ?T("As an example usage, the users of the host 'localhost' can " + "visit the page: 'https://localhost:5280/register/' It is " + "important to include the last / character in the URL, " + "otherwise the subpages URL will be incorrect."), "", + ?T("This module is enabled in 'listen' -> 'ejabberd_http' -> " + "_`listen-options.md#request_handlers|request_handlers`_, " + "no need to enable in 'modules'."), + ?T("The module depends on _`mod_register`_ where all the " + "configuration is performed.")], + example => + ["listen:", + " -", + " port: 5280", + " module: ejabberd_http", + " request_handlers:", + " /register: mod_register_web", + "", + "modules:", + " mod_register: {}"]}. diff --git a/src/mod_roster.erl b/src/mod_roster.erl index e60337cda..0f032cb02 100644 --- a/src/mod_roster.erl +++ b/src/mod_roster.erl @@ -5,7 +5,7 @@ %%% Created : 11 Dec 2002 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -34,757 +34,544 @@ -module(mod_roster). +-protocol({xep, 237, '1.3', '2.1.0', "complete", ""}). + -author('alexey@process-one.net'). -behaviour(gen_mod). --export([start/2, stop/1, process_iq/3, export/1, import/1, - process_local_iq/3, get_user_roster/2, import/3, - get_subscription_lists/3, get_roster/2, - get_in_pending_subscriptions/3, in_subscription/6, - out_subscription/4, set_items/3, remove_user/2, - get_jid_info/4, item_to_xml/1, webadmin_page/3, - webadmin_user/4, get_versioning_feature/2, - roster_versioning_enabled/1, roster_version/2, - record_to_string/1, groups_to_string/1]). +-export([start/2, stop/1, reload/3, process_iq/1, export/1, + import_info/0, process_local_iq/1, get_user_roster_items/2, + import/5, get_roster/2, push_item/3, + import_start/2, import_stop/2, is_subscribed/2, + c2s_self_presence/1, in_subscription/2, + out_subscription/1, set_items/3, remove_user/2, + get_jid_info/4, encode_item/1, get_versioning_feature/2, + roster_version/2, mod_doc/0, + mod_opt_type/1, mod_options/1, set_roster/1, del_roster/3, + process_rosteritems/5, + depends/2, set_item_and_notify_clients/3]). + +-export([webadmin_page_hostuser/4, webadmin_menu_hostuser/4, webadmin_user/4]). + +-import(ejabberd_web_admin, [make_command/4, make_command_raw_value/3, make_table/4]). --include("ejabberd.hrl"). -include("logger.hrl"). - --include("jlib.hrl"). - +-include_lib("xmpp/include/xmpp.hrl"). -include("mod_roster.hrl"). - -include("ejabberd_http.hrl"). - -include("ejabberd_web_admin.hrl"). +-include("translate.hrl"). + +-define(ROSTER_CACHE, roster_cache). +-define(ROSTER_ITEM_CACHE, roster_item_cache). +-define(ROSTER_VERSION_CACHE, roster_version_cache). +-define(SM_MIX_ANNOTATE, roster_mix_annotate). + +-type c2s_state() :: ejabberd_c2s:state(). -export_type([subscription/0]). +-callback init(binary(), gen_mod:opts()) -> any(). +-callback import(binary(), binary(), #roster{} | [binary()]) -> ok. +-callback read_roster_version(binary(), binary()) -> {ok, binary()} | error. +-callback write_roster_version(binary(), binary(), boolean(), binary()) -> any(). +-callback get_roster(binary(), binary()) -> {ok, [#roster{}]} | error. +-callback get_roster_item(binary(), binary(), ljid()) -> {ok, #roster{}} | error. +-callback read_subscription_and_groups(binary(), binary(), ljid()) + -> {ok, {subscription(), ask(), [binary()]}} | error. +-callback roster_subscribe(binary(), binary(), ljid(), #roster{}) -> any(). +-callback transaction(binary(), fun(() -> T)) -> {atomic, T} | {aborted, any()}. +-callback remove_user(binary(), binary()) -> any(). +-callback update_roster(binary(), binary(), ljid(), #roster{}) -> any(). +-callback del_roster(binary(), binary(), ljid()) -> any(). +-callback use_cache(binary(), roster | roster_version) -> boolean(). +-callback cache_nodes(binary()) -> [node()]. + +-optional_callbacks([use_cache/2, cache_nodes/1]). + start(Host, Opts) -> - IQDisc = gen_mod:get_opt(iqdisc, Opts, fun gen_iq_handler:check_type/1, - one_queue), - case gen_mod:db_type(Opts) of - mnesia -> - mnesia:create_table(roster, - [{disc_copies, [node()]}, - {attributes, record_info(fields, roster)}]), - mnesia:create_table(roster_version, - [{disc_copies, [node()]}, - {attributes, - record_info(fields, roster_version)}]), - update_tables(), - mnesia:add_table_index(roster, us), - mnesia:add_table_index(roster_version, us); - _ -> ok + Mod = gen_mod:db_mod(Opts, ?MODULE), + Mod:init(Host, Opts), + init_cache(Mod, Host, Opts), + {ok, [{hook, roster_get, get_user_roster_items, 50}, + {hook, roster_in_subscription, in_subscription, 50}, + {hook, roster_out_subscription, out_subscription, 50}, + {hook, roster_get_jid_info, get_jid_info, 50}, + {hook, remove_user, remove_user, 50}, + {hook, c2s_self_presence, c2s_self_presence, 50}, + {hook, c2s_post_auth_features, get_versioning_feature, 50}, + {hook, webadmin_menu_hostuser, webadmin_menu_hostuser, 50}, + {hook, webadmin_page_hostuser, webadmin_page_hostuser, 50}, + {hook, webadmin_user, webadmin_user, 50}, + {iq_handler, ejabberd_sm, ?NS_ROSTER, process_iq}]}. + +stop(_Host) -> + ok. + +reload(Host, NewOpts, OldOpts) -> + NewMod = gen_mod:db_mod(NewOpts, ?MODULE), + OldMod = gen_mod:db_mod(OldOpts, ?MODULE), + if NewMod /= OldMod -> + NewMod:init(Host, NewOpts); + true -> + ok end, - ejabberd_hooks:add(roster_get, Host, ?MODULE, - get_user_roster, 50), - ejabberd_hooks:add(roster_in_subscription, Host, - ?MODULE, in_subscription, 50), - ejabberd_hooks:add(roster_out_subscription, Host, - ?MODULE, out_subscription, 50), - ejabberd_hooks:add(roster_get_subscription_lists, Host, - ?MODULE, get_subscription_lists, 50), - ejabberd_hooks:add(roster_get_jid_info, Host, ?MODULE, - get_jid_info, 50), - ejabberd_hooks:add(remove_user, Host, ?MODULE, - remove_user, 50), - ejabberd_hooks:add(anonymous_purge_hook, Host, ?MODULE, - remove_user, 50), - ejabberd_hooks:add(resend_subscription_requests_hook, - Host, ?MODULE, get_in_pending_subscriptions, 50), - ejabberd_hooks:add(roster_get_versioning_feature, Host, - ?MODULE, get_versioning_feature, 50), - ejabberd_hooks:add(webadmin_page_host, Host, ?MODULE, - webadmin_page, 50), - ejabberd_hooks:add(webadmin_user, Host, ?MODULE, - webadmin_user, 50), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, - ?NS_ROSTER, ?MODULE, process_iq, IQDisc). + init_cache(NewMod, Host, NewOpts). -stop(Host) -> - ejabberd_hooks:delete(roster_get, Host, ?MODULE, - get_user_roster, 50), - ejabberd_hooks:delete(roster_in_subscription, Host, - ?MODULE, in_subscription, 50), - ejabberd_hooks:delete(roster_out_subscription, Host, - ?MODULE, out_subscription, 50), - ejabberd_hooks:delete(roster_get_subscription_lists, - Host, ?MODULE, get_subscription_lists, 50), - ejabberd_hooks:delete(roster_get_jid_info, Host, - ?MODULE, get_jid_info, 50), - ejabberd_hooks:delete(remove_user, Host, ?MODULE, - remove_user, 50), - ejabberd_hooks:delete(anonymous_purge_hook, Host, - ?MODULE, remove_user, 50), - ejabberd_hooks:delete(resend_subscription_requests_hook, - Host, ?MODULE, get_in_pending_subscriptions, 50), - ejabberd_hooks:delete(roster_get_versioning_feature, - Host, ?MODULE, get_versioning_feature, 50), - ejabberd_hooks:delete(webadmin_page_host, Host, ?MODULE, - webadmin_page, 50), - ejabberd_hooks:delete(webadmin_user, Host, ?MODULE, - webadmin_user, 50), - gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, - ?NS_ROSTER). +depends(_Host, _Opts) -> + []. -process_iq(From, To, IQ) when ((From#jid.luser == <<"">>) andalso (From#jid.resource == <<"">>)) -> - process_iq_manager(From, To, IQ); - -process_iq(From, To, IQ) -> - #iq{sub_el = SubEl} = IQ, - #jid{lserver = LServer} = From, - case lists:member(LServer, ?MYHOSTS) of - true -> process_local_iq(From, To, IQ); - _ -> - IQ#iq{type = error, - sub_el = [SubEl, ?ERR_ITEM_NOT_FOUND]} +-spec process_iq(iq()) -> iq(). +process_iq(#iq{from = #jid{luser = U, lserver = S}, + to = #jid{luser = U, lserver = S}} = IQ) -> + process_local_iq(IQ); +process_iq(#iq{lang = Lang, to = To} = IQ) -> + case ejabberd_hooks:run_fold(roster_remote_access, + To#jid.lserver, false, [IQ]) of + false -> + Txt = ?T("Query to another users is forbidden"), + xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)); + {true, IQ1} -> + process_local_iq(IQ1) end. -process_local_iq(From, To, #iq{type = Type} = IQ) -> - case Type of - set -> try_process_iq_set(From, To, IQ); - get -> process_iq_get(From, To, IQ) - end. +-spec process_local_iq(iq()) -> iq(). +process_local_iq(#iq{type = set,lang = Lang, + sub_els = [#roster_query{ + items = [#roster_item{ask = Ask}]}]} = IQ) + when Ask /= undefined -> + Txt = ?T("Possessing 'ask' attribute is not allowed by RFC6121"), + xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)); +process_local_iq(#iq{type = set, from = From, lang = Lang, + sub_els = [#roster_query{ + items = [#roster_item{} = Item]}]} = IQ) -> + case has_duplicated_groups(Item#roster_item.groups) of + true -> + Txt = ?T("Duplicated groups are not allowed by RFC6121"), + xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)); + false -> + From1 = case xmpp:get_meta(IQ, privilege_from, none) of + #jid{} = PrivFrom -> + PrivFrom; + none -> + From + end, + #jid{lserver = LServer} = From1, + Access = mod_roster_opt:access(LServer), + case acl:match_rule(LServer, Access, From) of + deny -> + Txt = ?T("Access denied by service policy"), + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); + allow -> + process_iq_set(IQ) + end + end; +process_local_iq(#iq{type = set, lang = Lang, + sub_els = [#roster_query{items = [_|_]}]} = IQ) -> + Txt = ?T("Multiple elements are not allowed by RFC6121"), + xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)); +process_local_iq(#iq{type = get, lang = Lang, + sub_els = [#roster_query{items = Items}]} = IQ) -> + case Items of + [] -> + process_iq_get(IQ); + [_|_] -> + Txt = ?T("The query must not contain elements"), + xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)) + end; +process_local_iq(#iq{lang = Lang} = IQ) -> + Txt = ?T("No module is handling this query"), + xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)). +-spec roster_hash([#roster{}]) -> binary(). roster_hash(Items) -> - p1_sha:sha(term_to_binary(lists:sort([R#roster{groups = - lists:sort(Grs)} - || R = #roster{groups = Grs} + str:sha(term_to_binary(lists:sort([R#roster_item{groups = lists:sort(Grs)} + || R = #roster_item{groups = Grs} <- Items]))). -roster_versioning_enabled(Host) -> - gen_mod:get_module_opt(Host, ?MODULE, versioning, - fun(B) when is_boolean(B) -> B end, - false). - -roster_version_on_db(Host) -> - gen_mod:get_module_opt(Host, ?MODULE, store_current_id, - fun(B) when is_boolean(B) -> B end, - false). - %% Returns a list that may contain an xmlelement with the XEP-237 feature if it's enabled. +-spec get_versioning_feature([xmpp_element()], binary()) -> [xmpp_element()]. get_versioning_feature(Acc, Host) -> - case roster_versioning_enabled(Host) of + case gen_mod:is_loaded(Host, ?MODULE) of + true -> + case mod_roster_opt:versioning(Host) of true -> - Feature = #xmlel{name = <<"ver">>, - attrs = [{<<"xmlns">>, ?NS_ROSTER_VER}], - children = []}, - [Feature | Acc]; - false -> [] + [#rosterver_feature{}|Acc]; + false -> + Acc + end; + false -> + Acc end. +-spec roster_version(binary(), binary()) -> undefined | binary(). roster_version(LServer, LUser) -> - US = {LUser, LServer}, - case roster_version_on_db(LServer) of + case mod_roster_opt:store_current_id(LServer) of true -> case read_roster_version(LUser, LServer) of - error -> not_found; - V -> V + error -> undefined; + {ok, V} -> V end; false -> - roster_hash(ejabberd_hooks:run_fold(roster_get, LServer, - [], [US])) + roster_hash(run_roster_get_hook(LUser, LServer)) end. +-spec read_roster_version(binary(), binary()) -> {ok, binary()} | error. read_roster_version(LUser, LServer) -> - read_roster_version(LUser, LServer, - gen_mod:db_type(LServer, ?MODULE)). - -read_roster_version(LUser, LServer, mnesia) -> - US = {LUser, LServer}, - case mnesia:dirty_read(roster_version, US) of - [#roster_version{version = V}] -> V; - [] -> error - end; -read_roster_version(LUser, LServer, odbc) -> - Username = ejabberd_odbc:escape(LUser), - case odbc_queries:get_roster_version(LServer, Username) - of - {selected, [<<"version">>], [[Version]]} -> Version; - {selected, [<<"version">>], []} -> error - end; -read_roster_version(LServer, LUser, riak) -> - case ejabberd_riak:get(roster_version, roster_version_schema(), - {LUser, LServer}) of - {ok, #roster_version{version = V}} -> V; - _Err -> error - end. + ets_cache:lookup( + ?ROSTER_VERSION_CACHE, {LUser, LServer}, + fun() -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:read_roster_version(LUser, LServer) + end). +-spec write_roster_version(binary(), binary()) -> binary(). write_roster_version(LUser, LServer) -> write_roster_version(LUser, LServer, false). +-spec write_roster_version_t(binary(), binary()) -> binary(). write_roster_version_t(LUser, LServer) -> write_roster_version(LUser, LServer, true). +-spec write_roster_version(binary(), binary(), boolean()) -> binary(). write_roster_version(LUser, LServer, InTransaction) -> - Ver = p1_sha:sha(term_to_binary(now())), - write_roster_version(LUser, LServer, InTransaction, Ver, - gen_mod:db_type(LServer, ?MODULE)), + Ver = str:sha(term_to_binary(erlang:unique_integer())), + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:write_roster_version(LUser, LServer, InTransaction, Ver), + if InTransaction -> ok; + true -> + ets_cache:delete(?ROSTER_VERSION_CACHE, {LUser, LServer}, + cache_nodes(Mod, LServer)) + end, Ver. -write_roster_version(LUser, LServer, InTransaction, Ver, - mnesia) -> - US = {LUser, LServer}, - if InTransaction -> - mnesia:write(#roster_version{us = US, version = Ver}); - true -> - mnesia:dirty_write(#roster_version{us = US, - version = Ver}) - end; -write_roster_version(LUser, LServer, InTransaction, Ver, - odbc) -> - Username = ejabberd_odbc:escape(LUser), - EVer = ejabberd_odbc:escape(Ver), - if InTransaction -> - odbc_queries:set_roster_version(Username, EVer); - true -> - odbc_queries:sql_transaction(LServer, - fun () -> - odbc_queries:set_roster_version(Username, - EVer) - end) - end; -write_roster_version(LUser, LServer, _InTransaction, Ver, - riak) -> - US = {LUser, LServer}, - ejabberd_riak:put(#roster_version{us = US, version = Ver}, - roster_version_schema()). - -%% Load roster from DB only if neccesary. -%% It is neccesary if +%% Load roster from DB only if necessary. +%% It is necessary if %% - roster versioning is disabled in server OR %% - roster versioning is not used by the client OR %% - roster versioning is used by server and client, BUT the server isn't storing versions on db OR %% - the roster version from client don't match current version. -process_iq_get(From, To, #iq{sub_el = SubEl} = IQ) -> - LUser = From#jid.luser, - LServer = From#jid.lserver, - US = {LUser, LServer}, - try {ItemsToSend, VersionToSend} = case - {xml:get_tag_attr(<<"ver">>, SubEl), - roster_versioning_enabled(LServer), - roster_version_on_db(LServer)} - of - {{value, RequestedVersion}, true, - true} -> - case read_roster_version(LUser, - LServer) - of - error -> - RosterVersion = - write_roster_version(LUser, - LServer), - {lists:map(fun item_to_xml/1, - ejabberd_hooks:run_fold(roster_get, - To#jid.lserver, - [], - [US])), - RosterVersion}; - RequestedVersion -> - {false, false}; - NewVersion -> - {lists:map(fun item_to_xml/1, - ejabberd_hooks:run_fold(roster_get, - To#jid.lserver, - [], - [US])), - NewVersion} - end; - {{value, RequestedVersion}, true, - false} -> - RosterItems = - ejabberd_hooks:run_fold(roster_get, - To#jid.lserver, - [], - [US]), - case roster_hash(RosterItems) of - RequestedVersion -> - {false, false}; - New -> - {lists:map(fun item_to_xml/1, - RosterItems), - New} - end; - _ -> - {lists:map(fun item_to_xml/1, - ejabberd_hooks:run_fold(roster_get, - To#jid.lserver, - [], - [US])), - false} - end, - IQ#iq{type = result, - sub_el = - case {ItemsToSend, VersionToSend} of - {false, false} -> []; - {Items, false} -> - [#xmlel{name = <<"query">>, - attrs = [{<<"xmlns">>, ?NS_ROSTER}], - children = Items}]; - {Items, Version} -> - [#xmlel{name = <<"query">>, - attrs = - [{<<"xmlns">>, ?NS_ROSTER}, - {<<"ver">>, Version}], - children = Items}] - end} - catch - _:_ -> - IQ#iq{type = error, - sub_el = [SubEl, ?ERR_INTERNAL_SERVER_ERROR]} - end. - -get_user_roster(Acc, {LUser, LServer}) -> - Items = get_roster(LUser, LServer), - lists:filter(fun (#roster{subscription = none, - ask = in}) -> - false; - (_) -> true - end, - Items) - ++ Acc. - -get_roster(LUser, LServer) -> - get_roster(LUser, LServer, - gen_mod:db_type(LServer, ?MODULE)). - -get_roster(LUser, LServer, mnesia) -> - US = {LUser, LServer}, - case catch mnesia:dirty_index_read(roster, US, - #roster.us) - of - Items when is_list(Items)-> Items; - _ -> [] - end; -get_roster(LUser, LServer, riak) -> - case ejabberd_riak:get_by_index(roster, roster_schema(), - <<"us">>, {LUser, LServer}) of - {ok, Items} -> Items; - _Err -> [] - end; -get_roster(LUser, LServer, odbc) -> - Username = ejabberd_odbc:escape(LUser), - case catch odbc_queries:get_roster(LServer, Username) of - {selected, - [<<"username">>, <<"jid">>, <<"nick">>, - <<"subscription">>, <<"ask">>, <<"askmessage">>, - <<"server">>, <<"subscribe">>, <<"type">>], - Items} - when is_list(Items) -> - JIDGroups = case catch - odbc_queries:get_roster_jid_groups(LServer, - Username) - of - {selected, [<<"jid">>, <<"grp">>], JGrps} - when is_list(JGrps) -> - JGrps; - _ -> [] - end, - GroupsDict = lists:foldl(fun ([J, G], Acc) -> - dict:append(J, G, Acc) - end, - dict:new(), JIDGroups), - RItems = lists:flatmap(fun (I) -> - case raw_to_record(LServer, I) of - %% Bad JID in database: - error -> []; - R -> - SJID = - jlib:jid_to_string(R#roster.jid), - Groups = case dict:find(SJID, - GroupsDict) - of - {ok, Gs} -> Gs; - error -> [] - end, - [R#roster{groups = Groups}] - end - end, - Items), - RItems; - _ -> [] - end. - -item_to_xml(Item) -> - Attrs1 = [{<<"jid">>, - jlib:jid_to_string(Item#roster.jid)}], - Attrs2 = case Item#roster.name of - <<"">> -> Attrs1; - Name -> [{<<"name">>, Name} | Attrs1] - end, - Attrs3 = case Item#roster.subscription of - none -> [{<<"subscription">>, <<"none">>} | Attrs2]; - from -> [{<<"subscription">>, <<"from">>} | Attrs2]; - to -> [{<<"subscription">>, <<"to">>} | Attrs2]; - both -> [{<<"subscription">>, <<"both">>} | Attrs2]; - remove -> [{<<"subscription">>, <<"remove">>} | Attrs2] - end, - Attrs4 = case ask_to_pending(Item#roster.ask) of - out -> [{<<"ask">>, <<"subscribe">>} | Attrs3]; - both -> [{<<"ask">>, <<"subscribe">>} | Attrs3]; - _ -> Attrs3 - end, - SubEls1 = lists:map(fun (G) -> - #xmlel{name = <<"group">>, attrs = [], - children = [{xmlcdata, G}]} - end, - Item#roster.groups), - SubEls = SubEls1 ++ Item#roster.xs, - #xmlel{name = <<"item">>, attrs = Attrs4, - children = SubEls}. - -get_roster_by_jid_t(LUser, LServer, LJID) -> - DBType = gen_mod:db_type(LServer, ?MODULE), - get_roster_by_jid_t(LUser, LServer, LJID, DBType). - -get_roster_by_jid_t(LUser, LServer, LJID, mnesia) -> - case mnesia:read({roster, {LUser, LServer, LJID}}) of - [] -> - #roster{usj = {LUser, LServer, LJID}, - us = {LUser, LServer}, jid = LJID}; - [I] -> - I#roster{jid = LJID, name = <<"">>, groups = [], - xs = []} - end; -get_roster_by_jid_t(LUser, LServer, LJID, odbc) -> - Username = ejabberd_odbc:escape(LUser), - SJID = ejabberd_odbc:escape(jlib:jid_to_string(LJID)), - {selected, - [<<"username">>, <<"jid">>, <<"nick">>, - <<"subscription">>, <<"ask">>, <<"askmessage">>, - <<"server">>, <<"subscribe">>, <<"type">>], - Res} = - odbc_queries:get_roster_by_jid(LServer, Username, SJID), - case Res of - [] -> - #roster{usj = {LUser, LServer, LJID}, - us = {LUser, LServer}, jid = LJID}; - [I] -> - R = raw_to_record(LServer, I), - case R of - %% Bad JID in database: - error -> - #roster{usj = {LUser, LServer, LJID}, - us = {LUser, LServer}, jid = LJID}; - _ -> - R#roster{usj = {LUser, LServer, LJID}, - us = {LUser, LServer}, jid = LJID, name = <<"">>} - end - end; -get_roster_by_jid_t(LUser, LServer, LJID, riak) -> - case ejabberd_riak:get(roster, roster_schema(), {LUser, LServer, LJID}) of - {ok, I} -> - I#roster{jid = LJID, name = <<"">>, groups = [], - xs = []}; - {error, notfound} -> - #roster{usj = {LUser, LServer, LJID}, - us = {LUser, LServer}, jid = LJID}; - Err -> - exit(Err) - end. - -try_process_iq_set(From, To, #iq{sub_el = SubEl} = IQ) -> - #jid{server = Server} = From, - Access = gen_mod:get_module_opt(Server, ?MODULE, access, fun(A) when is_atom(A) -> A end, all), - case acl:match_rule(Server, Access, From) of - deny -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; - allow -> - process_iq_set(From, To, IQ) - end. - -process_iq_set(From, To, #iq{sub_el = SubEl, id = Id} = IQ) -> - #xmlel{children = Els} = SubEl, - Managed = is_managed_from_id(Id), - lists:foreach(fun (El) -> process_item_set(From, To, El, Managed) - end, - Els), - IQ#iq{type = result, sub_el = []}. - -process_item_set(From, To, - #xmlel{attrs = Attrs, children = Els}, Managed) -> - JID1 = jlib:string_to_jid(xml:get_attr_s(<<"jid">>, - Attrs)), - #jid{user = User, luser = LUser, lserver = LServer} = - From, - case JID1 of - error -> ok; - _ -> - LJID = jlib:jid_tolower(JID1), - F = fun () -> - Item = get_roster_by_jid_t(LUser, LServer, LJID), - Item1 = process_item_attrs_managed(Item, Attrs, Managed), - Item2 = process_item_els(Item1, Els), - case Item2#roster.subscription of - remove -> del_roster_t(LUser, LServer, LJID); - _ -> update_roster_t(LUser, LServer, LJID, Item2) - end, - send_itemset_to_managers(From, Item2, Managed), - Item3 = ejabberd_hooks:run_fold(roster_process_item, - LServer, Item2, - [LServer]), - case roster_version_on_db(LServer) of - true -> write_roster_version_t(LUser, LServer); - false -> ok - end, - {Item, Item3} - end, - case transaction(LServer, F) of - {atomic, {OldItem, Item}} -> - push_item(User, LServer, To, Item), - case Item#roster.subscription of - remove -> - send_unsubscribing_presence(From, OldItem), ok; - _ -> ok +-spec process_iq_get(iq()) -> iq(). +process_iq_get(#iq{to = To, from = From, + sub_els = [#roster_query{ver = RequestedVersion, mix_annotate = MixEnabled}]} = IQ) -> + LUser = To#jid.luser, + LServer = To#jid.lserver, + {ItemsToSend, VersionToSend} = + case {mod_roster_opt:versioning(LServer), + mod_roster_opt:store_current_id(LServer)} of + {true, true} when RequestedVersion /= undefined -> + case read_roster_version(LUser, LServer) of + error -> + RosterVersion = write_roster_version(LUser, LServer), + {run_roster_get_hook(LUser, LServer), RosterVersion}; + {ok, RequestedVersion} -> + {false, false}; + {ok, NewVersion} -> + {run_roster_get_hook(LUser, LServer), NewVersion} + end; + {true, false} when RequestedVersion /= undefined -> + RosterItems = run_roster_get_hook(LUser, LServer), + case roster_hash(RosterItems) of + RequestedVersion -> + {false, false}; + New -> + {RosterItems, New} end; - E -> - ?DEBUG("ROSTER: roster item set error: ~p~n", [E]), ok - end - end; -process_item_set(_From, _To, _, _Managed) -> ok. - -process_item_attrs(Item, [{Attr, Val} | Attrs]) -> - case Attr of - <<"jid">> -> - case jlib:string_to_jid(Val) of - error -> process_item_attrs(Item, Attrs); - JID1 -> - JID = {JID1#jid.luser, JID1#jid.lserver, - JID1#jid.lresource}, - process_item_attrs(Item#roster{jid = JID}, Attrs) - end; - <<"name">> -> - process_item_attrs(Item#roster{name = Val}, Attrs); - <<"subscription">> -> - case Val of - <<"remove">> -> - process_item_attrs(Item#roster{subscription = remove}, - Attrs); - _ -> process_item_attrs(Item, Attrs) - end; - <<"ask">> -> process_item_attrs(Item, Attrs); - _ -> process_item_attrs(Item, Attrs) - end; -process_item_attrs(Item, []) -> Item. - -process_item_els(Item, - [#xmlel{name = Name, attrs = Attrs, children = SEls} - | Els]) -> - case Name of - <<"group">> -> - Groups = [xml:get_cdata(SEls) | Item#roster.groups], - process_item_els(Item#roster{groups = Groups}, Els); - _ -> - case xml:get_attr_s(<<"xmlns">>, Attrs) of - <<"">> -> process_item_els(Item, Els); _ -> - XEls = [#xmlel{name = Name, attrs = Attrs, - children = SEls} - | Item#roster.xs], - process_item_els(Item#roster{xs = XEls}, Els) - end - end; -process_item_els(Item, [{xmlcdata, _} | Els]) -> - process_item_els(Item, Els); -process_item_els(Item, []) -> Item. + {run_roster_get_hook(LUser, LServer), false} + end, + % Store that MIX annotation is enabled (for roster pushes) + set_mix_annotation_enabled(From, MixEnabled), + % Only include element when MIX annotation is enabled + Items = case ItemsToSend of + false -> false; + FullItems -> process_items_mix(FullItems, MixEnabled) + end, + xmpp:make_iq_result( + IQ, + case {Items, VersionToSend} of + {false, false} -> + undefined; + {Items, false} -> + #roster_query{items = Items}; + {Items, Version} -> + #roster_query{items = Items, + ver = Version} + end). -push_item(User, Server, From, Item) -> - ejabberd_sm:route(jlib:make_jid(<<"">>, <<"">>, <<"">>), - jlib:make_jid(User, Server, <<"">>), - {broadcast, {item, Item#roster.jid, - Item#roster.subscription}}), - case roster_versioning_enabled(Server) of - true -> - push_item_version(Server, User, From, Item, - roster_version(Server, User)); - false -> - lists:foreach(fun (Resource) -> - push_item(User, Server, Resource, From, Item) - end, - ejabberd_sm:get_user_resources(User, Server)) +-spec run_roster_get_hook(binary(), binary()) -> [#roster_item{}]. +run_roster_get_hook(LUser, LServer) -> + ejabberd_hooks:run_fold(roster_get, LServer, [], [{LUser, LServer}]). + +-spec get_filtered_roster(binary(), binary()) -> [#roster{}]. +get_filtered_roster(LUser, LServer) -> + lists:filter( + fun (#roster{subscription = none, ask = in}) -> false; + (_) -> true + end, + get_roster(LUser, LServer)). + +-spec get_user_roster_items([#roster_item{}], {binary(), binary()}) -> [#roster_item{}]. +get_user_roster_items(Acc, {LUser, LServer}) -> + lists:map(fun encode_item/1, get_filtered_roster(LUser, LServer)) ++ Acc. + +-spec get_roster(binary(), binary()) -> [#roster{}]. +get_roster(LUser, LServer) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + R = case use_cache(Mod, LServer, roster) of + true -> + ets_cache:lookup( + ?ROSTER_CACHE, {LUser, LServer}, + fun() -> Mod:get_roster(LUser, LServer) end); + false -> + Mod:get_roster(LUser, LServer) + end, + case R of + {ok, Items} -> Items; + error -> [] end. -push_item(User, Server, Resource, From, Item) -> - push_item(User, Server, Resource, From, Item, - not_found). - -push_item(User, Server, Resource, From, Item, - RosterVersion) -> - ExtraAttrs = case RosterVersion of - not_found -> []; - _ -> [{<<"ver">>, RosterVersion}] - end, - ResIQ = #iq{type = set, xmlns = ?NS_ROSTER, -%% @doc Roster push, calculate and include the version attribute. -%% TODO: don't push to those who didn't load roster - id = <<"push", (randoms:get_string())/binary>>, - sub_el = - [#xmlel{name = <<"query">>, - attrs = [{<<"xmlns">>, ?NS_ROSTER} | ExtraAttrs], - children = [item_to_xml(Item)]}]}, - ejabberd_router:route(From, - jlib:make_jid(User, Server, Resource), - jlib:iq_to_xml(ResIQ)). - -push_item_version(Server, User, From, Item, - RosterVersion) -> - lists:foreach(fun (Resource) -> - push_item(User, Server, Resource, From, Item, - RosterVersion) - end, - ejabberd_sm:get_user_resources(User, Server)). - -get_subscription_lists(Acc, User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - DBType = gen_mod:db_type(LServer, ?MODULE), - Items = get_subscription_lists(Acc, LUser, LServer, - DBType), - fill_subscription_lists(LServer, Items, [], []). - -get_subscription_lists(_, LUser, LServer, mnesia) -> - US = {LUser, LServer}, - case mnesia:dirty_index_read(roster, US, #roster.us) of - Items when is_list(Items) -> Items; - _ -> [] - end; -get_subscription_lists(_, LUser, LServer, odbc) -> - Username = ejabberd_odbc:escape(LUser), - case catch odbc_queries:get_roster(LServer, Username) of - {selected, - [<<"username">>, <<"jid">>, <<"nick">>, - <<"subscription">>, <<"ask">>, <<"askmessage">>, - <<"server">>, <<"subscribe">>, <<"type">>], - Items} - when is_list(Items) -> - lists:map(fun(I) -> raw_to_record(LServer, I) end, Items); - _ -> [] - end; -get_subscription_lists(_, LUser, LServer, riak) -> - case ejabberd_riak:get_by_index(roster, roster_schema(), - <<"us">>, {LUser, LServer}) of - {ok, Items} -> Items; - _Err -> [] +-spec get_roster_item(binary(), binary(), ljid()) -> #roster{}. +get_roster_item(LUser, LServer, LJID) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + case Mod:get_roster_item(LUser, LServer, LJID) of + {ok, Item} -> + Item; + error -> + LBJID = jid:remove_resource(LJID), + #roster{usj = {LUser, LServer, LBJID}, + us = {LUser, LServer}, jid = LBJID} end. -fill_subscription_lists(LServer, [#roster{} = I | Is], - F, T) -> - J = element(3, I#roster.usj), - case I#roster.subscription of - both -> - fill_subscription_lists(LServer, Is, [J | F], [J | T]); - from -> - fill_subscription_lists(LServer, Is, [J | F], T); - to -> fill_subscription_lists(LServer, Is, F, [J | T]); - _ -> fill_subscription_lists(LServer, Is, F, T) - end; -fill_subscription_lists(LServer, [RawI | Is], F, T) -> - I = raw_to_record(LServer, RawI), - case I of - %% Bad JID in database: - error -> fill_subscription_lists(LServer, Is, F, T); - _ -> fill_subscription_lists(LServer, [I | Is], F, T) - end; -fill_subscription_lists(_LServer, [], F, T) -> {F, T}. +-spec get_subscription_and_groups(binary(), binary(), ljid()) -> + {subscription(), ask(), [binary()]}. +get_subscription_and_groups(LUser, LServer, LJID) -> + LBJID = jid:remove_resource(LJID), + Mod = gen_mod:db_mod(LServer, ?MODULE), + Res = case use_cache(Mod, LServer, roster) of + true -> + ets_cache:lookup( + ?ROSTER_ITEM_CACHE, {LUser, LServer, LBJID}, + fun() -> + Items = get_roster(LUser, LServer), + case lists:keyfind(LBJID, #roster.jid, Items) of + #roster{subscription = Sub, + ask = Ask, + groups = Groups} -> + {ok, {Sub, Ask, Groups}}; + false -> + error + end + end); + false -> + case Mod:read_subscription_and_groups(LUser, LServer, LBJID) of + {ok, {Sub, Groups}} -> + %% Backward compatibility for third-party backends + {ok, {Sub, none, Groups}}; + Other -> + Other + end + end, + case Res of + {ok, SubAndGroups} -> + SubAndGroups; + error -> + {none, none, []} + end. +-spec set_roster(#roster{}) -> {atomic | aborted, any()}. +set_roster(#roster{us = {LUser, LServer}, jid = LJID} = Item) -> + transaction( + LUser, LServer, [LJID], + fun() -> + update_roster_t(LUser, LServer, LJID, Item) + end). + +-spec del_roster(binary(), binary(), ljid()) -> {atomic | aborted, any()}. +del_roster(LUser, LServer, LJID) -> + transaction( + LUser, LServer, [LJID], + fun() -> + del_roster_t(LUser, LServer, LJID) + end). + +-spec encode_item(#roster{}) -> roster_item(). +encode_item(Item) -> + #roster_item{jid = jid:make(Item#roster.jid), + name = Item#roster.name, + subscription = Item#roster.subscription, + ask = case ask_to_pending(Item#roster.ask) of + out -> subscribe; + both -> subscribe; + _ -> undefined + end, + groups = Item#roster.groups}. + +-spec decode_item(roster_item(), #roster{}, boolean()) -> #roster{}. +decode_item(#roster_item{subscription = remove} = Item, R, _) -> + R#roster{jid = jid:tolower(Item#roster_item.jid), + name = <<"">>, + subscription = remove, + ask = none, + groups = [], + askmessage = <<"">>, + xs = []}; +decode_item(Item, R, Managed) -> + R#roster{jid = jid:tolower(Item#roster_item.jid), + name = Item#roster_item.name, + subscription = case Item#roster_item.subscription of + Sub when Managed -> Sub; + _ -> R#roster.subscription + end, + groups = Item#roster_item.groups}. + +-spec process_iq_set(iq()) -> iq(). +process_iq_set(#iq{from = _From, to = To, lang = Lang, + sub_els = [#roster_query{items = [QueryItem]}]} = IQ) -> + case set_item_and_notify_clients(To, QueryItem, false) of + ok -> + xmpp:make_iq_result(IQ); + {error, _} -> + Txt = ?T("Database failure"), + Err = xmpp:err_internal_server_error(Txt, Lang), + xmpp:make_error(IQ, Err) + end. + +-spec set_item_and_notify_clients(jid(), #roster_item{}, boolean()) -> ok | {error, any()}. +set_item_and_notify_clients(To, #roster_item{jid = PeerJID} = RosterItem, + OverrideSubscription) -> + #jid{luser = LUser, lserver = LServer} = To, + PeerLJID = jid:tolower(PeerJID), + F = fun () -> + Item1 = get_roster_item(LUser, LServer, PeerLJID), + Item2 = decode_item(RosterItem, Item1, OverrideSubscription), + Item3 = ejabberd_hooks:run_fold(roster_process_item, + LServer, Item2, + [LServer]), + case Item3#roster.subscription of + remove -> del_roster_t(LUser, LServer, PeerLJID); + _ -> update_roster_t(LUser, LServer, PeerLJID, Item3) + end, + case mod_roster_opt:store_current_id(LServer) of + true -> write_roster_version_t(LUser, LServer); + false -> ok + end, + {Item1, Item3} + end, + case transaction(LUser, LServer, [PeerLJID], F) of + {atomic, {OldItem, NewItem}} -> + push_item(To, encode_item(OldItem), encode_item(NewItem)), + case NewItem#roster.subscription of + remove -> + send_unsubscribing_presence(To, OldItem); + _ -> + ok + end; + {aborted, Reason} -> + {error, Reason} + end. + +-spec push_item(jid(), #roster_item{}, #roster_item{}) -> ok. +push_item(To, OldItem, NewItem) -> + #jid{luser = LUser, lserver = LServer} = To, + Ver = case mod_roster_opt:versioning(LServer) of + true -> roster_version(LServer, LUser); + false -> undefined + end, + lists:foreach( + fun(Resource) -> + To1 = jid:replace_resource(To, Resource), + push_item(To1, OldItem, NewItem, Ver) + end, ejabberd_sm:get_user_resources(LUser, LServer)). + +-spec push_item(jid(), #roster_item{}, #roster_item{}, undefined | binary()) -> ok. +push_item(To, OldItem, NewItem, Ver) -> + route_presence_change(To, OldItem, NewItem), + [Item] = process_items_mix([NewItem], To), + IQ = #iq{type = set, to = To, + from = jid:remove_resource(To), + id = <<"push", (p1_rand:get_string())/binary>>, + sub_els = [#roster_query{ver = Ver, + items = [Item]}]}, + ejabberd_router:route(IQ). + +-spec route_presence_change(jid(), #roster_item{}, #roster_item{}) -> ok. +route_presence_change(From, OldItem, NewItem) -> + OldSub = OldItem#roster_item.subscription, + NewSub = NewItem#roster_item.subscription, + To = NewItem#roster_item.jid, + NewIsFrom = NewSub == both orelse NewSub == from, + OldIsFrom = OldSub == both orelse OldSub == from, + if NewIsFrom andalso not OldIsFrom -> + case ejabberd_sm:get_session_pid( + From#jid.luser, From#jid.lserver, From#jid.lresource) of + none -> + ok; + Pid -> + ejabberd_c2s:resend_presence(Pid, To) + end; + OldIsFrom andalso not NewIsFrom -> + PU = #presence{from = From, to = To, type = unavailable}, + case ejabberd_hooks:run_fold( + privacy_check_packet, allow, + [From, PU, out]) of + deny -> + ok; + allow -> + ejabberd_router:route(PU) + end; + true -> + ok + end. + +-spec ask_to_pending(ask()) -> none | in | out | both. ask_to_pending(subscribe) -> out; ask_to_pending(unsubscribe) -> none; ask_to_pending(Ask) -> Ask. +-spec roster_subscribe_t(binary(), binary(), ljid(), #roster{}) -> any(). roster_subscribe_t(LUser, LServer, LJID, Item) -> - DBType = gen_mod:db_type(LServer, ?MODULE), - roster_subscribe_t(LUser, LServer, LJID, Item, DBType). + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:roster_subscribe(LUser, LServer, LJID, Item). -roster_subscribe_t(_LUser, _LServer, _LJID, Item, - mnesia) -> - mnesia:write(Item); -roster_subscribe_t(LUser, LServer, LJID, Item, odbc) -> - ItemVals = record_to_string(Item), - Username = ejabberd_odbc:escape(LUser), - SJID = ejabberd_odbc:escape(jlib:jid_to_string(LJID)), - odbc_queries:roster_subscribe(LServer, Username, SJID, - ItemVals); -roster_subscribe_t(LUser, LServer, _LJID, Item, riak) -> - ejabberd_riak:put(Item, roster_schema(), - [{'2i', [{<<"us">>, {LUser, LServer}}]}]). - -transaction(LServer, F) -> - case gen_mod:db_type(LServer, ?MODULE) of - mnesia -> mnesia:transaction(F); - odbc -> ejabberd_odbc:sql_transaction(LServer, F); - riak -> {atomic, F()} +-spec transaction(binary(), binary(), [ljid()], fun(() -> T)) -> {atomic, T} | {aborted, any()}. +transaction(LUser, LServer, LJIDs, F) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + case Mod:transaction(LServer, F) of + {atomic, _} = Result -> + delete_cache(Mod, LUser, LServer, LJIDs), + Result; + Err -> + Err end. -in_subscription(_, User, Server, JID, Type, Reason) -> +-spec in_subscription(boolean(), presence()) -> boolean(). +in_subscription(_, #presence{from = JID, to = To, + sub_els = SubEls, + type = Type, status = Status}) -> + #jid{user = User, server = Server} = To, + Reason = if Type == subscribe -> xmpp:get_text(Status); + true -> <<"">> + end, process_subscription(in, User, Server, JID, Type, - Reason). + Reason, SubEls). -out_subscription(User, Server, JID, Type) -> - process_subscription(out, User, Server, JID, Type, <<"">>). - -get_roster_by_jid_with_groups_t(LUser, LServer, LJID) -> - DBType = gen_mod:db_type(LServer, ?MODULE), - get_roster_by_jid_with_groups_t(LUser, LServer, LJID, - DBType). - -get_roster_by_jid_with_groups_t(LUser, LServer, LJID, - mnesia) -> - case mnesia:read({roster, {LUser, LServer, LJID}}) of - [] -> - #roster{usj = {LUser, LServer, LJID}, - us = {LUser, LServer}, jid = LJID}; - [I] -> I - end; -get_roster_by_jid_with_groups_t(LUser, LServer, LJID, - odbc) -> - Username = ejabberd_odbc:escape(LUser), - SJID = ejabberd_odbc:escape(jlib:jid_to_string(LJID)), - case odbc_queries:get_roster_by_jid(LServer, Username, - SJID) - of - {selected, - [<<"username">>, <<"jid">>, <<"nick">>, - <<"subscription">>, <<"ask">>, <<"askmessage">>, - <<"server">>, <<"subscribe">>, <<"type">>], - [I]} -> - R = raw_to_record(LServer, I), - Groups = case odbc_queries:get_roster_groups(LServer, - Username, SJID) - of - {selected, [<<"grp">>], JGrps} when is_list(JGrps) -> - [JGrp || [JGrp] <- JGrps]; - _ -> [] - end, - R#roster{groups = Groups}; - {selected, - [<<"username">>, <<"jid">>, <<"nick">>, - <<"subscription">>, <<"ask">>, <<"askmessage">>, - <<"server">>, <<"subscribe">>, <<"type">>], - []} -> - #roster{usj = {LUser, LServer, LJID}, - us = {LUser, LServer}, jid = LJID} - end; -get_roster_by_jid_with_groups_t(LUser, LServer, LJID, riak) -> - case ejabberd_riak:get(roster, roster_schema(), {LUser, LServer, LJID}) of - {ok, I} -> - I; - {error, notfound} -> - #roster{usj = {LUser, LServer, LJID}, - us = {LUser, LServer}, jid = LJID}; - Err -> - exit(Err) - end. +-spec out_subscription(presence()) -> boolean(). +out_subscription(#presence{from = From, to = JID, type = Type}) -> + #jid{user = User, server = Server} = From, + process_subscription(out, User, Server, JID, Type, <<"">>, []). +-spec process_subscription(in | out, binary(), binary(), jid(), + subscribe | subscribed | unsubscribe | unsubscribed, + binary(), [fxml:xmlel()]) -> boolean(). process_subscription(Direction, User, Server, JID1, - Type, Reason) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - LJID = jlib:jid_tolower(JID1), + Type, Reason, SubEls) -> + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + LJID = jid:tolower(jid:remove_resource(JID1)), F = fun () -> - Item = get_roster_by_jid_with_groups_t(LUser, LServer, - LJID), + Item = get_roster_item(LUser, LServer, LJID), NewState = case Direction of out -> out_state_change(Item#roster.subscription, @@ -805,53 +592,57 @@ process_subscription(Direction, User, Server, JID1, _ -> <<"">> end, case NewState of - none -> {none, AutoReply}; - {none, none} - when Item#roster.subscription == none, - Item#roster.ask == in -> - del_roster_t(LUser, LServer, LJID), {none, AutoReply}; - {Subscription, Pending} -> - NewItem = Item#roster{subscription = Subscription, - ask = Pending, - askmessage = - iolist_to_binary(AskMessage)}, - roster_subscribe_t(LUser, LServer, LJID, NewItem), - case roster_version_on_db(LServer) of - true -> write_roster_version_t(LUser, LServer); - false -> ok - end, - {{push, NewItem}, AutoReply} + none -> + {none, AutoReply}; + {none, none} when Item#roster.subscription == none, + Item#roster.ask == in -> + del_roster_t(LUser, LServer, LJID), {none, AutoReply}; + {Subscription, Pending} -> + NewItem = Item#roster{subscription = Subscription, + ask = Pending, + name = get_nick_subels(SubEls, Item#roster.name), + xs = SubEls, + askmessage = AskMessage}, + roster_subscribe_t(LUser, LServer, LJID, NewItem), + case mod_roster_opt:store_current_id(LServer) of + true -> write_roster_version_t(LUser, LServer); + false -> ok + end, + {{push, Item, NewItem}, AutoReply} end end, - case transaction(LServer, F) of - {atomic, {Push, AutoReply}} -> - case AutoReply of - none -> ok; - _ -> - T = case AutoReply of - subscribed -> <<"subscribed">>; - unsubscribed -> <<"unsubscribed">> + case transaction(LUser, LServer, [LJID], F) of + {atomic, {Push, AutoReply}} -> + case AutoReply of + none -> ok; + _ -> + ejabberd_router:route( + #presence{type = AutoReply, + from = jid:make(User, Server), + to = JID1}) + end, + case Push of + {push, OldItem, NewItem} -> + if NewItem#roster.subscription == none, + NewItem#roster.ask == in -> + ok; + true -> + push_item(jid:make(User, Server), + encode_item(OldItem), + encode_item(NewItem)) end, - ejabberd_router:route(jlib:make_jid(User, Server, - <<"">>), - JID1, - #xmlel{name = <<"presence">>, - attrs = [{<<"type">>, T}], - children = []}) - end, - case Push of - {push, Item} -> - if Item#roster.subscription == none, - Item#roster.ask == in -> - ok; - true -> - push_item(User, Server, - jlib:make_jid(User, Server, <<"">>), Item) - end, - true; - none -> false - end; - _ -> false + true; + none -> + false + end; + _ -> + false + end. + +get_nick_subels(SubEls, Default) -> + case xmpp:get_subtag(#presence{sub_els = SubEls}, #nick{}) of + {nick, N} -> N; + _ -> Default end. %% in_state_change(Subscription, Pending, Type) -> NewState @@ -909,6 +700,30 @@ in_state_change(both, none, subscribe) -> none; in_state_change(both, none, subscribed) -> none; in_state_change(both, none, unsubscribe) -> {to, none}; in_state_change(both, none, unsubscribed) -> + {from, none}; +% Invalid states that can occurs from roster modification from API +in_state_change(to, out, subscribe) -> {to, in}; +in_state_change(to, out, subscribed) -> none; +in_state_change(to, out, unsubscribe) -> none; +in_state_change(to, out, unsubscribed) -> {none, none}; +in_state_change(to, both, subscribe) -> none; +in_state_change(to, both, subscribed) -> none; +in_state_change(to, both, unsubscribe) -> {to, none}; +in_state_change(to, both, unsubscribed) -> {none, in}; +in_state_change(from, in, subscribe) -> none; +in_state_change(from, in, subscribed) -> {both, none}; +in_state_change(from, in, unsubscribe) -> + {none, none}; +in_state_change(from, in, unsubscribed) -> none; +in_state_change(from, both, subscribe) -> none; +in_state_change(from, both, subscribed) -> {both, none}; +in_state_change(from, both, unsubscribe) -> {none, out}; +in_state_change(from, both, unsubscribed) -> + {from, none}; +in_state_change(both, _, subscribe) -> none; +in_state_change(both, _, subscribed) -> none; +in_state_change(both, _, unsubscribe) -> {to, none}; +in_state_change(both, _, unsubscribed) -> {from, none}. out_state_change(none, none, subscribe) -> {none, out}; @@ -916,8 +731,7 @@ out_state_change(none, none, subscribed) -> none; out_state_change(none, none, unsubscribe) -> none; out_state_change(none, none, unsubscribed) -> none; out_state_change(none, out, subscribe) -> - {none, - out}; %% We need to resend query (RFC3921, section 9.2) + {none, out}; %% We need to resend query (RFC3921, section 9.2) out_state_change(none, out, subscribed) -> none; out_state_change(none, out, unsubscribe) -> {none, none}; @@ -956,6 +770,32 @@ out_state_change(both, none, subscribed) -> none; out_state_change(both, none, unsubscribe) -> {from, none}; out_state_change(both, none, unsubscribed) -> + {to, none}; +% Invalid states that can occurs from roster modification from API +out_state_change(to, out, subscribe) -> none; +out_state_change(to, out, subscribed) -> {both, none}; +out_state_change(to, out, unsubscribe) -> {none, none}; +out_state_change(to, out, unsubscribed) -> none; +out_state_change(to, both, subscribe) -> none; +out_state_change(to, both, subscribed) -> {both, none}; +out_state_change(to, both, unsubscribe) -> {none, in}; +out_state_change(to, both, unsubscribed) -> {to, none}; +out_state_change(from, in, subscribe) -> {from, out}; +out_state_change(from, in, subscribed) -> none; +out_state_change(from, in, unsubscribe) -> none; +out_state_change(from, in, unsubscribed) -> + {none, none}; +out_state_change(from, both, subscribe) -> none; +out_state_change(from, both, subscribed) -> none; +out_state_change(from, both, unsubscribe) -> + {from, none}; +out_state_change(from, both, unsubscribed) -> + {none, out}; +out_state_change(both, _, subscribe) -> none; +out_state_change(both, _, subscribed) -> none; +out_state_change(both, _, unsubscribe) -> + {from, none}; +out_state_change(both, _, unsubscribed) -> {to, none}. in_auto_reply(from, none, subscribe) -> subscribed; @@ -969,39 +809,28 @@ in_auto_reply(from, out, unsubscribe) -> unsubscribed; in_auto_reply(both, none, unsubscribe) -> unsubscribed; in_auto_reply(_, _, _) -> none. +-spec remove_user(binary(), binary()) -> ok. remove_user(User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - send_unsubscription_to_rosteritems(LUser, LServer), - remove_user(LUser, LServer, - gen_mod:db_type(LServer, ?MODULE)). - -remove_user(LUser, LServer, mnesia) -> - US = {LUser, LServer}, - F = fun () -> - lists:foreach(fun (R) -> mnesia:delete_object(R) end, - mnesia:index_read(roster, US, #roster.us)) - end, - mnesia:transaction(F); -remove_user(LUser, LServer, odbc) -> - Username = ejabberd_odbc:escape(LUser), - odbc_queries:del_user_roster_t(LServer, Username), - ok; -remove_user(LUser, LServer, riak) -> - {atomic, ejabberd_riak:delete_by_index(roster, <<"us">>, {LUser, LServer})}. + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + Items = get_filtered_roster(LUser, LServer), + send_unsubscription_to_rosteritems(LUser, LServer, Items), + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:remove_user(LUser, LServer), + delete_cache(Mod, LUser, LServer, [Item#roster.jid || Item <- Items]). %% For each contact with Subscription: %% Both or From, send a "unsubscribed" presence stanza; %% Both or To, send a "unsubscribe" presence stanza. -send_unsubscription_to_rosteritems(LUser, LServer) -> - RosterItems = get_user_roster([], {LUser, LServer}), - From = jlib:make_jid({LUser, LServer, <<"">>}), +-spec send_unsubscription_to_rosteritems(binary(), binary(), [#roster{}]) -> ok. +send_unsubscription_to_rosteritems(LUser, LServer, RosterItems) -> + From = jid:make({LUser, LServer, <<"">>}), lists:foreach(fun (RosterItem) -> send_unsubscribing_presence(From, RosterItem) end, RosterItems). -%% @spec (From::jid(), Item::roster()) -> ok +-spec send_unsubscribing_presence(jid(), #roster{}) -> ok. send_unsubscribing_presence(From, Item) -> IsTo = case Item#roster.subscription of both -> true; @@ -1014,753 +843,460 @@ send_unsubscribing_presence(From, Item) -> _ -> false end, if IsTo -> - send_presence_type(jlib:jid_remove_resource(From), - jlib:make_jid(Item#roster.jid), - <<"unsubscribe">>); + ejabberd_router:route( + #presence{type = unsubscribe, + from = jid:remove_resource(From), + to = jid:make(Item#roster.jid)}); true -> ok end, if IsFrom -> - send_presence_type(jlib:jid_remove_resource(From), - jlib:make_jid(Item#roster.jid), - <<"unsubscribed">>); + ejabberd_router:route( + #presence{type = unsubscribed, + from = jid:remove_resource(From), + to = jid:make(Item#roster.jid)}); true -> ok - end, - ok. + end. -send_presence_type(From, To, Type) -> - ejabberd_router:route(From, To, - #xmlel{name = <<"presence">>, - attrs = [{<<"type">>, Type}], children = []}). +%%%=================================================================== +%%% MIX +%%%=================================================================== + +-spec remove_mix_channel([#roster_item{}]) -> [#roster_item{}]. +remove_mix_channel(Items) -> + lists:map( + fun(Item) -> + Item#roster_item{mix_channel = undefined} + end, Items). + +-spec process_items_mix([#roster_item{}], boolean() | jid()) -> [#roster_item{}]. +process_items_mix(Items, true) -> Items; +process_items_mix(Items, false) -> remove_mix_channel(Items); +process_items_mix(Items, JID) -> process_items_mix(Items, is_mix_annotation_enabled(JID)). + +-spec is_mix_annotation_enabled(jid()) -> boolean(). +is_mix_annotation_enabled(#jid{luser = User, lserver = Host, lresource = Res}) -> + case ejabberd_sm:get_user_info(User, Host, Res) of + offline -> false; + Info -> + case lists:keyfind(?SM_MIX_ANNOTATE, 1, Info) of + {_, true} -> true; + _ -> false + end + end. + +-spec set_mix_annotation_enabled(jid(), boolean()) -> ok | {error, any()}. +set_mix_annotation_enabled(#jid{luser = U, lserver = Host, lresource = R} = JID, false) -> + case is_mix_annotation_enabled(JID) of + true -> + ?DEBUG("Disabling roster MIX annotation for ~ts@~ts/~ts", [U, Host, R]), + case ejabberd_sm:del_user_info(U, Host, R, ?SM_MIX_ANNOTATE) of + ok -> ok; + {error, Reason} = Err -> + ?ERROR_MSG("Failed to disable roster MIX annotation for ~ts@~ts/~ts: ~p", + [U, Host, R, Reason]), + Err + end; + false -> ok + end; +set_mix_annotation_enabled(#jid{luser = U, lserver = Host, lresource = R}, true)-> + ?DEBUG("Enabling roster MIX annotation for ~ts@~ts/~ts", [U, Host, R]), + case ejabberd_sm:set_user_info(U, Host, R, ?SM_MIX_ANNOTATE, true) of + ok -> ok; + {error, Reason} = Err -> + ?ERROR_MSG("Failed to enable roster MIX annotation for ~ts@~ts/~ts: ~p", + [U, Host, R, Reason]), + Err + end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -set_items(User, Server, SubEl) -> - #xmlel{children = Els} = SubEl, - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), +-spec set_items(binary(), binary(), roster_query()) -> {atomic, ok} | {aborted, any()}. +set_items(User, Server, #roster_query{items = Items}) -> + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + LJIDs = [jid:tolower(Item#roster_item.jid) || Item <- Items], F = fun () -> - lists:foreach(fun (El) -> - process_item_set_t(LUser, LServer, El) - end, - Els) + lists:foreach( + fun(Item) -> + process_item_set_t(LUser, LServer, Item) + end, Items) end, - transaction(LServer, F). + transaction(LUser, LServer, LJIDs, F). +-spec update_roster_t(binary(), binary(), ljid(), #roster{}) -> any(). update_roster_t(LUser, LServer, LJID, Item) -> - DBType = gen_mod:db_type(LServer, ?MODULE), - update_roster_t(LUser, LServer, LJID, Item, DBType). - -update_roster_t(_LUser, _LServer, _LJID, Item, - mnesia) -> - mnesia:write(Item); -update_roster_t(LUser, LServer, LJID, Item, odbc) -> - Username = ejabberd_odbc:escape(LUser), - SJID = ejabberd_odbc:escape(jlib:jid_to_string(LJID)), - ItemVals = record_to_string(Item), - ItemGroups = groups_to_string(Item), - odbc_queries:update_roster(LServer, Username, SJID, ItemVals, - ItemGroups); -update_roster_t(LUser, LServer, _LJID, Item, riak) -> - ejabberd_riak:put(Item, roster_schema(), - [{'2i', [{<<"us">>, {LUser, LServer}}]}]). + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:update_roster(LUser, LServer, LJID, Item). +-spec del_roster_t(binary(), binary(), ljid()) -> any(). del_roster_t(LUser, LServer, LJID) -> - DBType = gen_mod:db_type(LServer, ?MODULE), - del_roster_t(LUser, LServer, LJID, DBType). + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:del_roster(LUser, LServer, LJID). -del_roster_t(LUser, LServer, LJID, mnesia) -> - mnesia:delete({roster, {LUser, LServer, LJID}}); -del_roster_t(LUser, LServer, LJID, odbc) -> - Username = ejabberd_odbc:escape(LUser), - SJID = ejabberd_odbc:escape(jlib:jid_to_string(LJID)), - odbc_queries:del_roster(LServer, Username, SJID); -del_roster_t(LUser, LServer, LJID, riak) -> - ejabberd_riak:delete(roster, {LUser, LServer, LJID}). - -process_item_set_t(LUser, LServer, - #xmlel{attrs = Attrs, children = Els}) -> - JID1 = jlib:string_to_jid(xml:get_attr_s(<<"jid">>, - Attrs)), - case JID1 of - error -> ok; - _ -> - JID = {JID1#jid.user, JID1#jid.server, - JID1#jid.resource}, - LJID = {JID1#jid.luser, JID1#jid.lserver, - JID1#jid.lresource}, - Item = #roster{usj = {LUser, LServer, LJID}, - us = {LUser, LServer}, jid = JID}, - Item1 = process_item_attrs_ws(Item, Attrs), - Item2 = process_item_els(Item1, Els), - case Item2#roster.subscription of - remove -> del_roster_t(LUser, LServer, LJID); - _ -> update_roster_t(LUser, LServer, LJID, Item2) - end +-spec process_item_set_t(binary(), binary(), roster_item()) -> any(). +process_item_set_t(LUser, LServer, #roster_item{jid = JID1} = QueryItem) -> + JID = {JID1#jid.user, JID1#jid.server, <<>>}, + LJID = {JID1#jid.luser, JID1#jid.lserver, <<>>}, + Item = #roster{usj = {LUser, LServer, LJID}, + us = {LUser, LServer}, jid = JID}, + Item2 = decode_item(QueryItem, Item, _Managed = true), + case Item2#roster.subscription of + remove -> del_roster_t(LUser, LServer, LJID); + _ -> update_roster_t(LUser, LServer, LJID, Item2) end; process_item_set_t(_LUser, _LServer, _) -> ok. -process_item_attrs_ws(Item, [{Attr, Val} | Attrs]) -> - case Attr of - <<"jid">> -> - case jlib:string_to_jid(Val) of - error -> process_item_attrs_ws(Item, Attrs); - JID1 -> - JID = {JID1#jid.luser, JID1#jid.lserver, - JID1#jid.lresource}, - process_item_attrs_ws(Item#roster{jid = JID}, Attrs) - end; - <<"name">> -> - process_item_attrs_ws(Item#roster{name = Val}, Attrs); - <<"subscription">> -> - case Val of - <<"remove">> -> - process_item_attrs_ws(Item#roster{subscription = - remove}, - Attrs); - <<"none">> -> - process_item_attrs_ws(Item#roster{subscription = none}, - Attrs); - <<"both">> -> - process_item_attrs_ws(Item#roster{subscription = both}, - Attrs); - <<"from">> -> - process_item_attrs_ws(Item#roster{subscription = from}, - Attrs); - <<"to">> -> - process_item_attrs_ws(Item#roster{subscription = to}, - Attrs); - _ -> process_item_attrs_ws(Item, Attrs) - end; - <<"ask">> -> process_item_attrs_ws(Item, Attrs); - _ -> process_item_attrs_ws(Item, Attrs) +-spec c2s_self_presence({presence(), c2s_state()}) -> {presence(), c2s_state()}. +c2s_self_presence({_, #{pres_last := _}} = Acc) -> + Acc; +c2s_self_presence({#presence{type = available} = Pkt, State}) -> + Prio = get_priority_from_presence(Pkt), + if Prio >= 0 -> + State1 = resend_pending_subscriptions(State), + {Pkt, State1}; + true -> + {Pkt, State} end; -process_item_attrs_ws(Item, []) -> Item. +c2s_self_presence(Acc) -> + Acc. -get_in_pending_subscriptions(Ls, User, Server) -> - LServer = jlib:nameprep(Server), - get_in_pending_subscriptions(Ls, User, Server, - gen_mod:db_type(LServer, ?MODULE)). +-spec resend_pending_subscriptions(c2s_state()) -> c2s_state(). +resend_pending_subscriptions(#{jid := JID} = State) -> + BareJID = jid:remove_resource(JID), + Result = get_roster(JID#jid.luser, JID#jid.lserver), + lists:foldl( + fun(#roster{ask = Ask} = R, AccState) when Ask == in; Ask == both -> + Message = R#roster.askmessage, + Status = if is_binary(Message) -> (Message); + true -> <<"">> + end, + Sub = #presence{from = jid:make(R#roster.jid), + to = BareJID, + type = subscribe, + sub_els = R#roster.xs, + status = xmpp:mk_text(Status)}, + ejabberd_c2s:send(AccState, Sub); + (_, AccState) -> + AccState + end, State, Result). -get_in_pending_subscriptions(Ls, User, Server, DBType) - when DBType == mnesia; DBType == riak -> - JID = jlib:make_jid(User, Server, <<"">>), - Result = get_roster(JID#jid.luser, JID#jid.lserver, DBType), - Ls ++ lists:map(fun (R) -> - Message = R#roster.askmessage, - Status = if is_binary(Message) -> (Message); - true -> <<"">> - end, - #xmlel{name = <<"presence">>, - attrs = - [{<<"from">>, - jlib:jid_to_string(R#roster.jid)}, - {<<"to">>, jlib:jid_to_string(JID)}, - {<<"type">>, <<"subscribe">>}], - children = - [#xmlel{name = <<"status">>, - attrs = [], - children = - [{xmlcdata, Status}]}]} - end, - lists:filter(fun (R) -> - case R#roster.ask of - in -> true; - both -> true; - _ -> false - end - end, - Result)); -get_in_pending_subscriptions(Ls, User, Server, odbc) -> - JID = jlib:make_jid(User, Server, <<"">>), - LUser = JID#jid.luser, - LServer = JID#jid.lserver, - Username = ejabberd_odbc:escape(LUser), - case catch odbc_queries:get_roster(LServer, Username) of - {selected, - [<<"username">>, <<"jid">>, <<"nick">>, - <<"subscription">>, <<"ask">>, <<"askmessage">>, - <<"server">>, <<"subscribe">>, <<"type">>], - Items} - when is_list(Items) -> - Ls ++ - lists:map(fun (R) -> - Message = R#roster.askmessage, - #xmlel{name = <<"presence">>, - attrs = - [{<<"from">>, - jlib:jid_to_string(R#roster.jid)}, - {<<"to">>, jlib:jid_to_string(JID)}, - {<<"type">>, <<"subscribe">>}], - children = - [#xmlel{name = <<"status">>, - attrs = [], - children = - [{xmlcdata, Message}]}]} - end, - lists:flatmap(fun (I) -> - case raw_to_record(LServer, I) of - %% Bad JID in database: - error -> []; - R -> - case R#roster.ask of - in -> [R]; - both -> [R]; - _ -> [] - end - end - end, - Items)); - _ -> Ls +-spec get_priority_from_presence(presence()) -> integer(). +get_priority_from_presence(#presence{priority = Prio}) -> + case Prio of + undefined -> 0; + _ -> Prio end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -read_subscription_and_groups(User, Server, LJID) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - read_subscription_and_groups(LUser, LServer, LJID, - gen_mod:db_type(LServer, ?MODULE)). - -read_subscription_and_groups(LUser, LServer, LJID, - mnesia) -> - case catch mnesia:dirty_read(roster, - {LUser, LServer, LJID}) - of - [#roster{subscription = Subscription, - groups = Groups}] -> - {Subscription, Groups}; - _ -> error - end; -read_subscription_and_groups(LUser, LServer, LJID, - odbc) -> - Username = ejabberd_odbc:escape(LUser), - SJID = ejabberd_odbc:escape(jlib:jid_to_string(LJID)), - case catch odbc_queries:get_subscription(LServer, - Username, SJID) - of - {selected, [<<"subscription">>], [[SSubscription]]} -> - Subscription = case SSubscription of - <<"B">> -> both; - <<"T">> -> to; - <<"F">> -> from; - _ -> none - end, - Groups = case catch - odbc_queries:get_rostergroup_by_jid(LServer, Username, - SJID) - of - {selected, [<<"grp">>], JGrps} when is_list(JGrps) -> - [JGrp || [JGrp] <- JGrps]; - _ -> [] - end, - {Subscription, Groups}; - _ -> error - end; -read_subscription_and_groups(LUser, LServer, LJID, - riak) -> - case ejabberd_riak:get(roster, roster_schema(), {LUser, LServer, LJID}) of - {ok, #roster{subscription = Subscription, - groups = Groups}} -> - {Subscription, Groups}; - _ -> - error - end. - +-spec get_jid_info({subscription(), ask(), [binary()]}, binary(), binary(), jid()) + -> {subscription(), ask(), [binary()]}. get_jid_info(_, User, Server, JID) -> - LJID = jlib:jid_tolower(JID), - case read_subscription_and_groups(User, Server, LJID) of - {Subscription, Groups} -> {Subscription, Groups}; - error -> - LRJID = jlib:jid_tolower(jlib:jid_remove_resource(JID)), - if LRJID == LJID -> {none, []}; - true -> - case read_subscription_and_groups(User, Server, LRJID) - of - {Subscription, Groups} -> {Subscription, Groups}; - error -> {none, []} - end - end - end. + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + LJID = jid:tolower(JID), + get_subscription_and_groups(LUser, LServer, LJID). -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -raw_to_record(LServer, - [User, SJID, Nick, SSubscription, SAsk, SAskMessage, - _SServer, _SSubscribe, _SType]) -> - case jlib:string_to_jid(SJID) of - error -> error; - JID -> - LJID = jlib:jid_tolower(JID), - Subscription = case SSubscription of - <<"B">> -> both; - <<"T">> -> to; - <<"F">> -> from; - _ -> none - end, - Ask = case SAsk of - <<"S">> -> subscribe; - <<"U">> -> unsubscribe; - <<"B">> -> both; - <<"O">> -> out; - <<"I">> -> in; - _ -> none - end, - #roster{usj = {User, LServer, LJID}, - us = {User, LServer}, jid = LJID, name = Nick, - subscription = Subscription, ask = Ask, - askmessage = SAskMessage} - end. - -record_to_string(#roster{us = {User, _Server}, - jid = JID, name = Name, subscription = Subscription, - ask = Ask, askmessage = AskMessage}) -> - Username = ejabberd_odbc:escape(User), - SJID = - ejabberd_odbc:escape(jlib:jid_to_string(jlib:jid_tolower(JID))), - Nick = ejabberd_odbc:escape(Name), - SSubscription = case Subscription of - both -> <<"B">>; - to -> <<"T">>; - from -> <<"F">>; - none -> <<"N">> - end, - SAsk = case Ask of - subscribe -> <<"S">>; - unsubscribe -> <<"U">>; - both -> <<"B">>; - out -> <<"O">>; - in -> <<"I">>; - none -> <<"N">> - end, - SAskMessage = ejabberd_odbc:escape(AskMessage), - [Username, SJID, Nick, SSubscription, SAsk, SAskMessage, - <<"N">>, <<"">>, <<"item">>]. - -groups_to_string(#roster{us = {User, _Server}, - jid = JID, groups = Groups}) -> - Username = ejabberd_odbc:escape(User), - SJID = - ejabberd_odbc:escape(jlib:jid_to_string(jlib:jid_tolower(JID))), - lists:foldl(fun (<<"">>, Acc) -> Acc; - (Group, Acc) -> - G = ejabberd_odbc:escape(Group), - [[Username, SJID, G] | Acc] - end, - [], Groups). - -update_tables() -> - update_roster_table(), - update_roster_version_table(). - -update_roster_table() -> - Fields = record_info(fields, roster), - case mnesia:table_info(roster, attributes) of - Fields -> - ejabberd_config:convert_table_to_binary( - roster, Fields, set, - fun(#roster{usj = {U, _, _}}) -> U end, - fun(#roster{usj = {U, S, {LU, LS, LR}}, - us = {U1, S1}, - jid = {U2, S2, R2}, - name = Name, - groups = Gs, - askmessage = Ask, - xs = Xs} = R) -> - R#roster{usj = {iolist_to_binary(U), - iolist_to_binary(S), - {iolist_to_binary(LU), - iolist_to_binary(LS), - iolist_to_binary(LR)}}, - us = {iolist_to_binary(U1), - iolist_to_binary(S1)}, - jid = {iolist_to_binary(U2), - iolist_to_binary(S2), - iolist_to_binary(R2)}, - name = iolist_to_binary(Name), - groups = [iolist_to_binary(G) || G <- Gs], - askmessage = try iolist_to_binary(Ask) - catch _:_ -> <<"">> end, - xs = [xml:to_xmlel(X) || X <- Xs]} - end); - _ -> - ?INFO_MSG("Recreating roster table", []), - mnesia:transform_table(roster, ignore, Fields) - end. - -%% Convert roster table to support virtual host -%% Convert roster table: xattrs fields become -update_roster_version_table() -> - Fields = record_info(fields, roster_version), - case mnesia:table_info(roster_version, attributes) of - Fields -> - ejabberd_config:convert_table_to_binary( - roster_version, Fields, set, - fun(#roster_version{us = {U, _}}) -> U end, - fun(#roster_version{us = {U, S}, version = Ver} = R) -> - R#roster_version{us = {iolist_to_binary(U), - iolist_to_binary(S)}, - version = iolist_to_binary(Ver)} - end); - _ -> - ?INFO_MSG("Recreating roster_version table", []), - mnesia:transform_table(roster_version, ignore, Fields) - end. - -webadmin_page(_, Host, - #request{us = _US, path = [<<"user">>, U, <<"roster">>], - q = Query, lang = Lang} = - _Request) -> - Res = user_roster(U, Host, Query, Lang), {stop, Res}; -webadmin_page(Acc, _, _) -> Acc. - -user_roster(User, Server, Query, Lang) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - US = {LUser, LServer}, - Items1 = get_roster(LUser, LServer), - Res = user_roster_parse_query(User, Server, Items1, - Query), - Items = get_roster(LUser, LServer), - SItems = lists:sort(Items), - FItems = case SItems of - [] -> [?CT(<<"None">>)]; - _ -> - [?XE(<<"table">>, - [?XE(<<"thead">>, - [?XE(<<"tr">>, - [?XCT(<<"td">>, <<"Jabber ID">>), - ?XCT(<<"td">>, <<"Nickname">>), - ?XCT(<<"td">>, <<"Subscription">>), - ?XCT(<<"td">>, <<"Pending">>), - ?XCT(<<"td">>, <<"Groups">>)])]), - ?XE(<<"tbody">>, - (lists:map(fun (R) -> - Groups = lists:flatmap(fun - (Group) -> - [?C(Group), - ?BR] - end, - R#roster.groups), - Pending = - ask_to_pending(R#roster.ask), - TDJID = - build_contact_jid_td(R#roster.jid), - ?XE(<<"tr">>, - [TDJID, - ?XAC(<<"td">>, - [{<<"class">>, - <<"valign">>}], - (R#roster.name)), - ?XAC(<<"td">>, - [{<<"class">>, - <<"valign">>}], - (iolist_to_binary(atom_to_list(R#roster.subscription)))), - ?XAC(<<"td">>, - [{<<"class">>, - <<"valign">>}], - (iolist_to_binary(atom_to_list(Pending)))), - ?XAE(<<"td">>, - [{<<"class">>, - <<"valign">>}], - Groups), - if Pending == in -> - ?XAE(<<"td">>, - [{<<"class">>, - <<"valign">>}], - [?INPUTT(<<"submit">>, - <<"validate", - (ejabberd_web_admin:term_to_id(R#roster.jid))/binary>>, - <<"Validate">>)]); - true -> ?X(<<"td">>) - end, - ?XAE(<<"td">>, - [{<<"class">>, - <<"valign">>}], - [?INPUTT(<<"submit">>, - <<"remove", - (ejabberd_web_admin:term_to_id(R#roster.jid))/binary>>, - <<"Remove">>)])]) - end, - SItems)))])] - end, - [?XC(<<"h1">>, - (<<(?T(<<"Roster of ">>))/binary, (us_to_list(US))/binary>>))] - ++ - case Res of - ok -> [?XREST(<<"Submitted">>)]; - error -> [?XREST(<<"Bad format">>)]; - nothing -> [] - end - ++ - [?XAE(<<"form">>, - [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], - (FItems ++ - [?P, ?INPUT(<<"text">>, <<"newjid">>, <<"">>), - ?C(<<" ">>), - ?INPUTT(<<"submit">>, <<"addjid">>, - <<"Add Jabber ID">>)]))]. - -build_contact_jid_td(RosterJID) -> - ContactJID = jlib:make_jid(RosterJID), - JIDURI = case {ContactJID#jid.luser, - ContactJID#jid.lserver} - of - {<<"">>, _} -> <<"">>; - {CUser, CServer} -> - case lists:member(CServer, ?MYHOSTS) of - false -> <<"">>; - true -> - <<"/admin/server/", CServer/binary, "/user/", - CUser/binary, "/">> - end - end, - case JIDURI of - <<>> -> - ?XAC(<<"td">>, [{<<"class">>, <<"valign">>}], - (jlib:jid_to_string(RosterJID))); - URI when is_binary(URI) -> - ?XAE(<<"td">>, [{<<"class">>, <<"valign">>}], - [?AC(JIDURI, (jlib:jid_to_string(RosterJID)))]) - end. - -user_roster_parse_query(User, Server, Items, Query) -> - case lists:keysearch(<<"addjid">>, 1, Query) of - {value, _} -> - case lists:keysearch(<<"newjid">>, 1, Query) of - {value, {_, SJID}} -> - case jlib:string_to_jid(SJID) of - JID when is_record(JID, jid) -> - user_roster_subscribe_jid(User, Server, JID), ok; - error -> error - end; - false -> error - end; - false -> - case catch user_roster_item_parse_query(User, Server, - Items, Query) - of - submitted -> ok; - {'EXIT', _Reason} -> error; - _ -> nothing - end - end. - -user_roster_subscribe_jid(User, Server, JID) -> - out_subscription(User, Server, JID, subscribe), - UJID = jlib:make_jid(User, Server, <<"">>), - ejabberd_router:route(UJID, JID, - #xmlel{name = <<"presence">>, - attrs = [{<<"type">>, <<"subscribe">>}], - children = []}). - -user_roster_item_parse_query(User, Server, Items, - Query) -> - lists:foreach(fun (R) -> - JID = R#roster.jid, - case lists:keysearch(<<"validate", - (ejabberd_web_admin:term_to_id(JID))/binary>>, - 1, Query) - of - {value, _} -> - JID1 = jlib:make_jid(JID), - out_subscription(User, Server, JID1, - subscribed), - UJID = jlib:make_jid(User, Server, <<"">>), - ejabberd_router:route(UJID, JID1, - #xmlel{name = - <<"presence">>, - attrs = - [{<<"type">>, - <<"subscribed">>}], - children = []}), - throw(submitted); - false -> - case lists:keysearch(<<"remove", - (ejabberd_web_admin:term_to_id(JID))/binary>>, - 1, Query) - of - {value, _} -> - UJID = jlib:make_jid(User, Server, - <<"">>), - process_iq_set(UJID, UJID, - #iq{type = set, - sub_el = - #xmlel{name = - <<"query">>, - attrs = - [{<<"xmlns">>, - ?NS_ROSTER}], - children = - [#xmlel{name - = - <<"item">>, - attrs - = - [{<<"jid">>, - jlib:jid_to_string(JID)}, - {<<"subscription">>, - <<"remove">>}], - children - = - []}]}}), - throw(submitted); - false -> ok - end - end - end, - Items), - nothing. - -us_to_list({User, Server}) -> - jlib:jid_to_string({User, Server, <<"">>}). - -webadmin_user(Acc, _User, _Server, Lang) -> - Acc ++ - [?XE(<<"h3">>, [?ACT(<<"roster/">>, <<"Roster">>)])]. - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -%% Implement XEP-0321 Remote Roster Management - -process_iq_manager(From, To, IQ) -> - %% Check what access is allowed for From to To - MatchDomain = From#jid.lserver, - case is_domain_managed(MatchDomain, To#jid.lserver) of - true -> - process_iq_manager2(MatchDomain, To, IQ); - false -> - #iq{sub_el = SubEl} = IQ, - IQ#iq{type = error, sub_el = [SubEl, ?ERR_BAD_REQUEST]} - end. - -process_iq_manager2(MatchDomain, To, IQ) -> - %% If IQ is SET, filter the input IQ - IQFiltered = maybe_filter_request(MatchDomain, IQ), - %% Call the standard function with reversed JIDs - IdInitial = IQFiltered#iq.id, - ResIQ = process_iq(To, To, IQFiltered#iq{id = <<"roster-remotely-managed">>}), - %% Filter the output IQ - filter_stanza(MatchDomain, ResIQ#iq{id = IdInitial}). - -is_domain_managed(ContactHost, UserHost) -> - Managers = gen_mod:get_module_opt(UserHost, ?MODULE, managers, - fun(B) when is_list(B) -> B end, - []), - lists:member(ContactHost, Managers). - -maybe_filter_request(MatchDomain, IQ) when IQ#iq.type == set -> - filter_stanza(MatchDomain, IQ); -maybe_filter_request(_MatchDomain, IQ) -> - IQ. - -filter_stanza(_MatchDomain, #iq{sub_el = []} = IQ) -> - IQ; -filter_stanza(MatchDomain, #iq{sub_el = [SubEl | _]} = IQ) -> - #iq{sub_el = SubElFiltered} = IQRes = - filter_stanza(MatchDomain, IQ#iq{sub_el = SubEl}), - IQRes#iq{sub_el = [SubElFiltered]}; -filter_stanza(MatchDomain, #iq{sub_el = SubEl} = IQ) -> - #xmlel{name = Type, attrs = Attrs, children = Items} = SubEl, - ItemsFiltered = lists:filter( - fun(Item) -> - is_item_of_domain(MatchDomain, Item) end, Items), - SubElFiltered = #xmlel{name=Type, attrs = Attrs, children = ItemsFiltered}, - IQ#iq{sub_el = SubElFiltered}. - -is_item_of_domain(MatchDomain, #xmlel{} = El) -> - lists:any(fun(Attr) -> is_jid_of_domain(MatchDomain, Attr) end, El#xmlel.attrs); -is_item_of_domain(_MatchDomain, {xmlcdata, _}) -> - false. - -is_jid_of_domain(MatchDomain, {<<"jid">>, JIDString}) -> - case jlib:string_to_jid(JIDString) of - JID when JID#jid.lserver == MatchDomain -> true; - _ -> false - end; -is_jid_of_domain(_, _) -> - false. - -process_item_attrs_managed(Item, Attrs, true) -> - process_item_attrs_ws(Item, Attrs); -process_item_attrs_managed(Item, _Attrs, false) -> - process_item_attrs(Item, _Attrs). - -send_itemset_to_managers(_From, _Item, true) -> - ok; -send_itemset_to_managers(From, Item, false) -> - {_, UserHost} = Item#roster.us, - {_ContactUser, ContactHost, _ContactResource} = Item#roster.jid, - %% Check if the component is an allowed manager - IsManager = is_domain_managed(ContactHost, UserHost), - case IsManager of - true -> push_item(<<"">>, ContactHost, <<"">>, From, Item); - false -> ok - end. - -is_managed_from_id(<<"roster-remotely-managed">>) -> +%% Check if `From` is subscriberd to `To`s presence +%% note 1: partial subscriptions are also considered, i.e. +%% `To` has already sent a subscription request to `From` +%% note 2: it's assumed a user is subscribed to self +%% note 3: `To` MUST be a local user, `From` can be any user +-spec is_subscribed(jid(), jid()) -> boolean(). +is_subscribed(#jid{luser = LUser, lserver = LServer}, + #jid{luser = LUser, lserver = LServer}) -> true; -is_managed_from_id(_Id) -> - false. +is_subscribed(From, #jid{luser = LUser, lserver = LServer}) -> + {Sub, Ask, _} = ejabberd_hooks:run_fold( + roster_get_jid_info, LServer, + {none, none, []}, + [LUser, LServer, From]), + (Sub /= none) orelse (Ask == subscribe) + orelse (Ask == out) orelse (Ask == both). -roster_schema() -> - {record_info(fields, roster), #roster{}}. +process_rosteritems(ActionS, SubsS, AsksS, UsersS, ContactsS) -> + LServer = ejabberd_config:get_myname(), + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:process_rosteritems(ActionS, SubsS, AsksS, UsersS, ContactsS). -roster_version_schema() -> - {record_info(fields, roster_version), #roster_version{}}. +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -export(_Server) -> - [{roster, - fun(Host, #roster{usj = {LUser, LServer, LJID}} = R) - when LServer == Host -> - Username = ejabberd_odbc:escape(LUser), - SJID = ejabberd_odbc:escape(jlib:jid_to_string(LJID)), - ItemVals = record_to_string(R), - ItemGroups = groups_to_string(R), - odbc_queries:update_roster_sql(Username, SJID, - ItemVals, ItemGroups); - (_Host, _R) -> - [] - end}, - {roster_version, - fun(Host, #roster_version{us = {LUser, LServer}, version = Ver}) - when LServer == Host -> - Username = ejabberd_odbc:escape(LUser), - SVer = ejabberd_odbc:escape(Ver), - [[<<"delete from roster_version where username='">>, - Username, <<"';">>], - [<<"insert into roster_version(username, version) values('">>, - Username, <<"', '">>, SVer, <<"');">>]]; - (_Host, _R) -> - [] - end}]. +%%% @format-begin -import(LServer) -> - [{<<"select username, jid, nick, subscription, " - "ask, askmessage, server, subscribe, type from rosterusers;">>, - fun([LUser, JID|_] = Row) -> - Item = raw_to_record(LServer, Row), - Username = ejabberd_odbc:escape(LUser), - SJID = ejabberd_odbc:escape(JID), - {selected, _, Rows} = - ejabberd_odbc:sql_query_t( - [<<"select grp from rostergroups where username='">>, - Username, <<"' and jid='">>, SJID, <<"'">>]), - Groups = [Grp || [Grp] <- Rows], - Item#roster{groups = Groups} - end}, - {<<"select username, version from roster_version;">>, - fun([LUser, Ver]) -> - #roster_version{us = {LUser, LServer}, version = Ver} - end}]. +webadmin_menu_hostuser(Acc, _Host, _Username, _Lang) -> + Acc ++ [{<<"roster">>, <<"Roster">>}]. -import(_LServer, mnesia, #roster{} = R) -> - mnesia:dirty_write(R); -import(_LServer, mnesia, #roster_version{} = RV) -> - mnesia:dirty_write(RV); -import(_LServer, riak, #roster{us = {LUser, LServer}} = R) -> - ejabberd_riak:put(R, roster_schema(), - [{'2i', [{<<"us">>, {LUser, LServer}}]}]); -import(_LServer, riak, #roster_version{} = RV) -> - ejabberd_riak:put(RV, roster_version_schema()); -import(_, _, _) -> - pass. +webadmin_page_hostuser(_, Host, Username, #request{path = [<<"roster">> | RPath]} = R) -> + Head = ?H1GL(<<"Roster">>, <<"modules/#mod_roster">>, <<"mod_roster">>), + %% Execute twice: first to perform the action, the second to get new roster + _ = make_webadmin_roster_table(Host, Username, R, RPath), + RV2 = make_webadmin_roster_table(Host, Username, R, RPath), + Set = [make_command(add_rosteritem, + R, + [{<<"localuser">>, Username}, {<<"localhost">>, Host}], + []), + make_command(push_roster, R, [{<<"user">>, Username}, {<<"host">>, Host}], [])], + Get = [make_command(get_roster, R, [], [{only, presentation}]), + make_command(delete_rosteritem, R, [], [{only, presentation}]), + RV2], + {stop, Head ++ Get ++ Set}; +webadmin_page_hostuser(Acc, _, _, _) -> + Acc. + +make_webadmin_roster_table(Host, Username, R, RPath) -> + Contacts = + case make_command_raw_value(get_roster, R, [{<<"user">>, Username}, {<<"host">>, Host}]) + of + Cs when is_list(Cs) -> + Cs; + _ -> + [] + end, + Level = 5 + length(RPath), + Columns = + [<<"jid">>, <<"nick">>, <<"subscription">>, <<"pending">>, <<"groups">>, <<"">>], + Rows = + lists:map(fun({Jid, Nick, Subscriptions, Pending, Groups}) -> + {JidSplit, ProblematicBin} = + try jid:decode(Jid) of + #jid{} = J -> + {jid:split(J), <<"">>} + catch + _:{bad_jid, _} -> + ?INFO_MSG("Error parsing contact of ~s@~s that is invalid JID: ~s", + [Username, Host, Jid]), + {{<<"000--error-parsing-jid">>, <<"localhost">>, <<"">>}, + <<", Error parsing JID: ", Jid/binary>>} + end, + {make_command(echo, + R, + [{<<"sentence">>, jid:encode(JidSplit)}], + [{only, raw_and_value}, + {result_links, [{sentence, user, Level, <<"">>}]}]), + ?C(<>), + ?C(Subscriptions), + ?C(Pending), + ?C(Groups), + make_command(delete_rosteritem, + R, + [{<<"localuser">>, Username}, + {<<"localhost">>, Host}, + {<<"user">>, element(1, JidSplit)}, + {<<"host">>, element(2, JidSplit)}], + [{only, button}, + {style, danger}, + {input_name_append, + [Username, + Host, + element(1, JidSplit), + element(2, JidSplit)]}])} + end, + lists:keysort(1, Contacts)), + Table = make_table(20, RPath, Columns, Rows), + ?XE(<<"blockquote">>, [Table]). + +webadmin_user(Acc, User, Server, R) -> + Acc + ++ [make_command(get_roster_count, + R, + [{<<"user">>, User}, {<<"host">>, Server}], + [{result_links, + [{value, arg_host, 4, <<"user/", User/binary, "/roster/">>}]}])]. +%%% @format-end + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +-spec has_duplicated_groups([binary()]) -> boolean(). +has_duplicated_groups(Groups) -> + GroupsPrep = lists:usort([jid:resourceprep(G) || G <- Groups]), + not (length(GroupsPrep) == length(Groups)). + +-spec init_cache(module(), binary(), gen_mod:opts()) -> ok. +init_cache(Mod, Host, Opts) -> + CacheOpts = cache_opts(Opts), + case use_cache(Mod, Host, roster_version) of + true -> + ets_cache:new(?ROSTER_VERSION_CACHE, CacheOpts); + false -> + ets_cache:delete(?ROSTER_VERSION_CACHE) + end, + case use_cache(Mod, Host, roster) of + true -> + ets_cache:new(?ROSTER_CACHE, CacheOpts), + ets_cache:new(?ROSTER_ITEM_CACHE, CacheOpts); + false -> + ets_cache:delete(?ROSTER_CACHE), + ets_cache:delete(?ROSTER_ITEM_CACHE) + end. + +-spec cache_opts(gen_mod:opts()) -> [proplists:property()]. +cache_opts(Opts) -> + MaxSize = mod_roster_opt:cache_size(Opts), + CacheMissed = mod_roster_opt:cache_missed(Opts), + LifeTime = mod_roster_opt:cache_life_time(Opts), + [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}]. + +-spec use_cache(module(), binary(), roster | roster_version) -> boolean(). +use_cache(Mod, Host, Table) -> + case erlang:function_exported(Mod, use_cache, 2) of + true -> Mod:use_cache(Host, Table); + false -> mod_roster_opt:use_cache(Host) + end. + +-spec cache_nodes(module(), binary()) -> [node()]. +cache_nodes(Mod, Host) -> + case erlang:function_exported(Mod, cache_nodes, 1) of + true -> Mod:cache_nodes(Host); + false -> ejabberd_cluster:get_nodes() + end. + +-spec delete_cache(module(), binary(), binary(), [ljid()]) -> ok. +delete_cache(Mod, LUser, LServer, LJIDs) -> + case use_cache(Mod, LServer, roster_version) of + true -> + ets_cache:delete(?ROSTER_VERSION_CACHE, {LUser, LServer}, + cache_nodes(Mod, LServer)); + false -> + ok + end, + case use_cache(Mod, LServer, roster) of + true -> + Nodes = cache_nodes(Mod, LServer), + ets_cache:delete(?ROSTER_CACHE, {LUser, LServer}, Nodes), + lists:foreach( + fun(LJID) -> + ets_cache:delete( + ?ROSTER_ITEM_CACHE, + {LUser, LServer, jid:remove_resource(LJID)}, + Nodes) + end, LJIDs); + false -> + ok + end. + +export(LServer) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:export(LServer). + +import_info() -> + [{<<"roster_version">>, 2}, + {<<"rostergroups">>, 3}, + {<<"rosterusers">>, 10}]. + +import_start(LServer, DBType) -> + Mod = gen_mod:db_mod(DBType, ?MODULE), + ets:new(rostergroups_tmp, [private, named_table, bag]), + Mod:init(LServer, []), + ok. + +import_stop(_LServer, _DBType) -> + ets:delete(rostergroups_tmp), + ok. + +row_length() -> + case ejabberd_sql:use_new_schema() of + true -> 10; + false -> 9 + end. + +import(LServer, {sql, _}, _DBType, <<"rostergroups">>, [LUser, SJID, Group]) -> + LJID = jid:tolower(jid:decode(SJID)), + ets:insert(rostergroups_tmp, {{LUser, LServer, LJID}, Group}), + ok; +import(LServer, {sql, _}, DBType, <<"rosterusers">>, Row) -> + I = mod_roster_sql:raw_to_record(LServer, lists:sublist(Row, row_length())), + Groups = [G || {_, G} <- ets:lookup(rostergroups_tmp, I#roster.usj)], + RosterItem = I#roster{groups = Groups}, + Mod = gen_mod:db_mod(DBType, ?MODULE), + Mod:import(LServer, <<"rosterusers">>, RosterItem); +import(LServer, {sql, _}, DBType, <<"roster_version">>, [LUser, Ver]) -> + Mod = gen_mod:db_mod(DBType, ?MODULE), + Mod:import(LServer, <<"roster_version">>, [LUser, Ver]). + +mod_opt_type(access) -> + econf:acl(); +mod_opt_type(store_current_id) -> + econf:bool(); +mod_opt_type(versioning) -> + econf:bool(); +mod_opt_type(db_type) -> + econf:db_type(?MODULE); +mod_opt_type(use_cache) -> + econf:bool(); +mod_opt_type(cache_size) -> + econf:pos_int(infinity); +mod_opt_type(cache_missed) -> + econf:bool(); +mod_opt_type(cache_life_time) -> + econf:timeout(second, infinity). + +mod_options(Host) -> + [{access, all}, + {store_current_id, false}, + {versioning, false}, + {db_type, ejabberd_config:default_db(Host, ?MODULE)}, + {use_cache, ejabberd_option:use_cache(Host)}, + {cache_size, ejabberd_option:cache_size(Host)}, + {cache_missed, ejabberd_option:cache_missed(Host)}, + {cache_life_time, ejabberd_option:cache_life_time(Host)}]. + +mod_doc() -> + #{desc => + ?T("This module implements roster management as " + "defined in https://tools.ietf.org/html/rfc6121#section-2" + "[RFC6121 Section 2]. The module also adds support for " + "https://xmpp.org/extensions/xep-0237.html" + "[XEP-0237: Roster Versioning]."), + opts => + [{access, + #{value => ?T("AccessName"), + desc => + ?T("This option can be configured to specify " + "rules to restrict roster management. " + "If the rule returns 'deny' on the requested " + "user name, that user cannot modify their personal " + "roster, i.e. they cannot add/remove/modify contacts " + "or send presence subscriptions. " + "The default value is 'all', i.e. no restrictions.")}}, + {versioning, + #{value => "true | false", + desc => + ?T("Enables/disables Roster Versioning. " + "The default value is 'false'.")}}, + {store_current_id, + #{value => "true | false", + desc => + ?T("If this option is set to 'true', the current " + "roster version number is stored on the database. " + "If set to 'false', the roster version number is " + "calculated on the fly each time. Enabling this " + "option reduces the load for both ejabberd and the database. " + "This option does not affect the client in any way. " + "This option is only useful if option 'versioning' is " + "set to 'true'. The default value is 'false'. " + "IMPORTANT: if you use _`mod_shared_roster`_ or " + " _`mod_shared_roster_ldap`_, you must set the value " + "of the option to 'false'.")}}, + {db_type, + #{value => "mnesia | sql", + desc => + ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}, + {use_cache, + #{value => "true | false", + desc => + ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, + {cache_size, + #{value => "pos_integer() | infinity", + desc => + ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, + {cache_missed, + #{value => "true | false", + desc => + ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, + {cache_life_time, + #{value => "timeout()", + desc => + ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}], + example => + ["modules:", + " mod_roster:", + " versioning: true", + " store_current_id: false"]}. diff --git a/src/mod_roster_mnesia.erl b/src/mod_roster_mnesia.erl new file mode 100644 index 000000000..d8d4bd1c9 --- /dev/null +++ b/src/mod_roster_mnesia.erl @@ -0,0 +1,297 @@ +%%%------------------------------------------------------------------- +%%% File : mod_roster_mnesia.erl +%%% Author : Evgeny Khramtsov +%%% Created : 13 Apr 2016 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_roster_mnesia). + +-behaviour(mod_roster). + +%% API +-export([init/2, read_roster_version/2, write_roster_version/4, + get_roster/2, get_roster_item/3, roster_subscribe/4, + remove_user/2, update_roster/4, del_roster/3, transaction/2, + read_subscription_and_groups/3, import/3, create_roster/1, + process_rosteritems/5, + use_cache/2]). +-export([need_transform/1, transform/1]). + +-include("mod_roster.hrl"). +-include("logger.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + ejabberd_mnesia:create(?MODULE, roster, + [{disc_only_copies, [node()]}, + {attributes, record_info(fields, roster)}, + {index, [us]}]), + ejabberd_mnesia:create(?MODULE, roster_version, + [{disc_only_copies, [node()]}, + {attributes, + record_info(fields, roster_version)}]). + +use_cache(Host, Table) -> + case mnesia:table_info(Table, storage_type) of + disc_only_copies -> + mod_roster_opt:use_cache(Host); + _ -> + false + end. + +read_roster_version(LUser, LServer) -> + US = {LUser, LServer}, + case mnesia:dirty_read(roster_version, US) of + [#roster_version{version = V}] -> {ok, V}; + [] -> error + end. + +write_roster_version(LUser, LServer, InTransaction, Ver) -> + US = {LUser, LServer}, + if InTransaction -> + mnesia:write(#roster_version{us = US, version = Ver}); + true -> + mnesia:dirty_write(#roster_version{us = US, version = Ver}) + end. + +get_roster(LUser, LServer) -> + {ok, mnesia:dirty_index_read(roster, {LUser, LServer}, #roster.us)}. + +get_roster_item(LUser, LServer, LJID) -> + case mnesia:read({roster, {LUser, LServer, LJID}}) of + [I] -> {ok, I}; + [] -> error + end. + +roster_subscribe(_LUser, _LServer, _LJID, Item) -> + mnesia:write(Item). + +remove_user(LUser, LServer) -> + US = {LUser, LServer}, + F = fun () -> + lists:foreach( + fun (R) -> mnesia:delete_object(R) end, + mnesia:index_read(roster, US, #roster.us)) + end, + mnesia:transaction(F). + +update_roster(_LUser, _LServer, _LJID, Item) -> + mnesia:write(Item). + +del_roster(LUser, LServer, LJID) -> + mnesia:delete({roster, {LUser, LServer, LJID}}). + +read_subscription_and_groups(LUser, LServer, LJID) -> + case mnesia:dirty_read(roster, {LUser, LServer, LJID}) of + [#roster{subscription = Subscription, ask = Ask, groups = Groups}] -> + {ok, {Subscription, Ask, Groups}}; + _ -> + error + end. + +transaction(_LServer, F) -> + mnesia:transaction(F). + +create_roster(RItem) -> + mnesia:dirty_write(RItem). + +import(_LServer, <<"rosterusers">>, #roster{} = R) -> + mnesia:dirty_write(R); +import(LServer, <<"roster_version">>, [LUser, Ver]) -> + RV = #roster_version{us = {LUser, LServer}, version = Ver}, + mnesia:dirty_write(RV). + +need_transform({roster, {U, S, _}, _, _, _, _, _, _, _, _}) + when is_list(U) orelse is_list(S) -> + ?INFO_MSG("Mnesia table 'roster' will be converted to binary", []), + true; +need_transform({roster_version, {U, S}, Ver}) + when is_list(U) orelse is_list(S) orelse is_list(Ver) -> + ?INFO_MSG("Mnesia table 'roster_version' will be converted to binary", []), + true; +need_transform(_) -> + false. + +transform(#roster{usj = {U, S, {LU, LS, LR}}, + us = {U1, S1}, + jid = {U2, S2, R2}, + name = Name, + groups = Gs, + askmessage = Ask, + xs = Xs} = R) -> + R#roster{usj = {iolist_to_binary(U), iolist_to_binary(S), + {iolist_to_binary(LU), + iolist_to_binary(LS), + iolist_to_binary(LR)}}, + us = {iolist_to_binary(U1), iolist_to_binary(S1)}, + jid = {iolist_to_binary(U2), + iolist_to_binary(S2), + iolist_to_binary(R2)}, + name = iolist_to_binary(Name), + groups = [iolist_to_binary(G) || G <- Gs], + askmessage = try iolist_to_binary(Ask) + catch _:_ -> <<"">> end, + xs = [fxml:to_xmlel(X) || X <- Xs]}; +transform(#roster_version{us = {U, S}, version = Ver} = R) -> + R#roster_version{us = {iolist_to_binary(U), iolist_to_binary(S)}, + version = iolist_to_binary(Ver)}. + +%%%=================================================================== + +process_rosteritems(ActionS, SubsS, AsksS, UsersS, ContactsS) -> + Action = case ActionS of + "list" -> list; + "delete" -> delete + end, + Subs = lists:foldl( + fun(any, _) -> [none, from, to, both]; + (Sub, Subs) -> [Sub | Subs] + end, + [], + [list_to_atom(S) || S <- string:tokens(SubsS, ":")] + ), + Asks = lists:foldl( + fun(any, _) -> [none, out, in]; + (Ask, Asks) -> [Ask | Asks] + end, + [], + [list_to_atom(S) || S <- string:tokens(AsksS, ":")] + ), + Users = lists:foldl( + fun("any", _) -> ["*", "*@*"]; + (U, Us) -> [U | Us] + end, + [], + [S || S <- string:tokens(UsersS, ":")] + ), + Contacts = lists:foldl( + fun("any", _) -> ["*", "*@*"]; + (U, Us) -> [U | Us] + end, + [], + [S || S <- string:tokens(ContactsS, ":")] + ), + rosteritem_purge({Action, Subs, Asks, Users, Contacts}). + +rosteritem_purge(Options) -> + Num_rosteritems = mnesia:table_info(roster, size), + io:format("There are ~p roster items in total.~n", [Num_rosteritems]), + Key = mnesia:dirty_first(roster), + rip(Key, Options, {0, Num_rosteritems, 0, 0}, []). + +rip('$end_of_table', _Options, Counters, Res) -> + print_progress_line(Counters), + Res; +rip(Key, Options, {Pr, NT, NV, ND}, Res) -> + Key_next = mnesia:dirty_next(roster, Key), + {Action, _, _, _, _} = Options, + {ND2, Res2} = case decide_rip(Key, Options) of + true -> + Jids = apply_action(Action, Key), + {ND+1, [Jids | Res]}; + false -> + {ND, Res} + end, + NV2 = NV+1, + Pr2 = print_progress_line({Pr, NT, NV2, ND2}), + rip(Key_next, Options, {Pr2, NT, NV2, ND2}, Res2). + +apply_action(list, Key) -> + {User, Server, JID} = Key, + {RUser, RServer, _} = JID, + Jid1string = <>, + Jid2string = <>, + io:format("Matches: ~ts ~ts~n", [Jid1string, Jid2string]), + {Jid1string, Jid2string}; +apply_action(delete, Key) -> + R = apply_action(list, Key), + mnesia:dirty_delete(roster, Key), + R. + +print_progress_line({_Pr, 0, _NV, _ND}) -> + ok; +print_progress_line({Pr, NT, NV, ND}) -> + Pr2 = trunc((NV/NT)*100), + case Pr == Pr2 of + true -> + ok; + false -> + io:format("Progress ~p% - visited ~p - deleted ~p~n", [Pr2, NV, ND]) + end, + Pr2. + +decide_rip(Key, {_Action, Subs, Asks, User, Contact}) -> + case catch mnesia:dirty_read(roster, Key) of + [RI] -> + lists:member(RI#roster.subscription, Subs) + andalso lists:member(RI#roster.ask, Asks) + andalso decide_rip_jid(RI#roster.us, User) + andalso decide_rip_jid(RI#roster.jid, Contact); + _ -> + false + end. + +%% Returns true if the server of the JID is included in the servers +decide_rip_jid({UName, UServer, _UResource}, Match_list) -> + decide_rip_jid({UName, UServer}, Match_list); +decide_rip_jid({UName, UServer}, Match_list) -> + lists:any( + fun(Match_string) -> + MJID = jid:decode(list_to_binary(Match_string)), + MName = MJID#jid.luser, + MServer = MJID#jid.lserver, + Is_server = is_glob_match(UServer, MServer), + case MName of + <<>> when UName == <<>> -> + Is_server; + <<>> -> + false; + _ -> + Is_server + andalso is_glob_match(UName, MName) + end + end, + Match_list). + +%% Copied from ejabberd-2.0.0/src/acl.erl +is_regexp_match(String, RegExp) -> + case ejabberd_regexp:run(String, RegExp) of + nomatch -> + false; + match -> + true; + {error, ErrDesc} -> + io:format( + "Wrong regexp ~p in ACL: ~p", + [RegExp, ErrDesc]), + false + end. +is_glob_match(String, <<"!", Glob/binary>>) -> + not is_regexp_match(String, ejabberd_regexp:sh_to_awk(Glob)); +is_glob_match(String, Glob) -> + is_regexp_match(String, ejabberd_regexp:sh_to_awk(Glob)). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== diff --git a/src/mod_roster_opt.erl b/src/mod_roster_opt.erl new file mode 100644 index 000000000..4275bf4e2 --- /dev/null +++ b/src/mod_roster_opt.erl @@ -0,0 +1,62 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_roster_opt). + +-export([access/1]). +-export([cache_life_time/1]). +-export([cache_missed/1]). +-export([cache_size/1]). +-export([db_type/1]). +-export([store_current_id/1]). +-export([use_cache/1]). +-export([versioning/1]). + +-spec access(gen_mod:opts() | global | binary()) -> 'all' | acl:acl(). +access(Opts) when is_map(Opts) -> + gen_mod:get_opt(access, Opts); +access(Host) -> + gen_mod:get_module_opt(Host, mod_roster, access). + +-spec cache_life_time(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +cache_life_time(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_life_time, Opts); +cache_life_time(Host) -> + gen_mod:get_module_opt(Host, mod_roster, cache_life_time). + +-spec cache_missed(gen_mod:opts() | global | binary()) -> boolean(). +cache_missed(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_missed, Opts); +cache_missed(Host) -> + gen_mod:get_module_opt(Host, mod_roster, cache_missed). + +-spec cache_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +cache_size(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_size, Opts); +cache_size(Host) -> + gen_mod:get_module_opt(Host, mod_roster, cache_size). + +-spec db_type(gen_mod:opts() | global | binary()) -> atom(). +db_type(Opts) when is_map(Opts) -> + gen_mod:get_opt(db_type, Opts); +db_type(Host) -> + gen_mod:get_module_opt(Host, mod_roster, db_type). + +-spec store_current_id(gen_mod:opts() | global | binary()) -> boolean(). +store_current_id(Opts) when is_map(Opts) -> + gen_mod:get_opt(store_current_id, Opts); +store_current_id(Host) -> + gen_mod:get_module_opt(Host, mod_roster, store_current_id). + +-spec use_cache(gen_mod:opts() | global | binary()) -> boolean(). +use_cache(Opts) when is_map(Opts) -> + gen_mod:get_opt(use_cache, Opts); +use_cache(Host) -> + gen_mod:get_module_opt(Host, mod_roster, use_cache). + +-spec versioning(gen_mod:opts() | global | binary()) -> boolean(). +versioning(Opts) when is_map(Opts) -> + gen_mod:get_opt(versioning, Opts); +versioning(Host) -> + gen_mod:get_module_opt(Host, mod_roster, versioning). + diff --git a/src/mod_roster_sql.erl b/src/mod_roster_sql.erl new file mode 100644 index 000000000..44d507e5e --- /dev/null +++ b/src/mod_roster_sql.erl @@ -0,0 +1,459 @@ +%%%------------------------------------------------------------------- +%%% File : mod_roster_sql.erl +%%% Author : Evgeny Khramtsov +%%% Created : 14 Apr 2016 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_roster_sql). + + +-behaviour(mod_roster). + +%% API +-export([init/2, read_roster_version/2, write_roster_version/4, + get_roster/2, get_roster_item/3, roster_subscribe/4, + read_subscription_and_groups/3, remove_user/2, + update_roster/4, del_roster/3, transaction/2, + process_rosteritems/5, + import/3, export/1, raw_to_record/2]). +-export([sql_schemas/0]). + +-include("mod_roster.hrl"). +-include("ejabberd_sql_pt.hrl"). +-include("logger.hrl"). +-include_lib("xmpp/include/jid.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(Host, _Opts) -> + ejabberd_sql_schema:update_schema(Host, ?MODULE, sql_schemas()), + ok. + +sql_schemas() -> + [#sql_schema{ + version = 1, + tables = + [#sql_table{ + name = <<"rosterusers">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"jid">>, type = text}, + #sql_column{name = <<"nick">>, type = text}, + #sql_column{name = <<"subscription">>, type = {char, 1}}, + #sql_column{name = <<"ask">>, type = {char, 1}}, + #sql_column{name = <<"askmessage">>, type = text}, + #sql_column{name = <<"server">>, type = {char, 1}}, + #sql_column{name = <<"subscribe">>, type = text}, + #sql_column{name = <<"type">>, type = text}, + #sql_column{name = <<"created_at">>, type = timestamp, + default = true}], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"username">>, + <<"jid">>], + unique = true}, + #sql_index{ + columns = [<<"server_host">>, <<"jid">>]}]}, + #sql_table{ + name = <<"rostergroups">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"jid">>, type = text}, + #sql_column{name = <<"grp">>, type = text}], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"username">>, + <<"jid">>]}]}, + #sql_table{ + name = <<"roster_version">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"version">>, type = text}], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"username">>], + unique = true}]}]}]. + +read_roster_version(LUser, LServer) -> + case ejabberd_sql:sql_query( + LServer, + ?SQL("select @(version)s from roster_version" + " where username = %(LUser)s and %(LServer)H")) of + {selected, [{Version}]} -> {ok, Version}; + {selected, []} -> error; + _ -> {error, db_failure} + end. + +write_roster_version(LUser, LServer, InTransaction, Ver) -> + if InTransaction -> + set_roster_version(LUser, LServer, Ver); + true -> + transaction( + LServer, + fun () -> set_roster_version(LUser, LServer, Ver) end) + end. + +get_roster(LUser, LServer) -> + case ejabberd_sql:sql_query( + LServer, + ?SQL("select @(username)s, @(jid)s, @(nick)s, @(subscription)s, " + "@(ask)s, @(askmessage)s, @(server)s, @(subscribe)s, " + "@(type)s from rosterusers " + "where username=%(LUser)s and %(LServer)H")) of + {selected, Items} when is_list(Items) -> + JIDGroups = case get_roster_jid_groups(LServer, LUser) of + {selected, JGrps} when is_list(JGrps) -> + JGrps; + _ -> + [] + end, + GroupsDict = lists:foldl(fun({J, G}, Acc) -> + Gs = maps:get(J, Acc, []), + maps:put(J, [G | Gs], Acc) + end, + maps:new(), JIDGroups), + {ok, lists:flatmap( + fun(I) -> + case raw_to_record(LServer, I) of + %% Bad JID in database: + error -> []; + R -> + SJID = jid:encode(R#roster.jid), + Groups = maps:get(SJID, GroupsDict, []), + [R#roster{groups = Groups}] + end + end, Items)}; + _ -> + error + end. + +roster_subscribe(_LUser, _LServer, _LJID, Item) -> + ItemVals = record_to_row(Item), + roster_subscribe(ItemVals). + +transaction(LServer, F) -> + ejabberd_sql:sql_transaction(LServer, F). + +get_roster_item(LUser, LServer, LJID) -> + SJID = jid:encode(LJID), + case get_roster_by_jid(LServer, LUser, SJID) of + {selected, [I]} -> + case raw_to_record(LServer, I) of + error -> + error; + R -> + Groups = case get_roster_groups(LServer, LUser, SJID) of + {selected, JGrps} when is_list(JGrps) -> + [JGrp || {JGrp} <- JGrps]; + _ -> [] + end, + {ok, R#roster{groups = Groups}} + end; + {selected, []} -> + error + end. + +remove_user(LUser, LServer) -> + transaction( + LServer, + fun () -> + ejabberd_sql:sql_query_t( + ?SQL("delete from rosterusers" + " where username=%(LUser)s and %(LServer)H")), + ejabberd_sql:sql_query_t( + ?SQL("delete from rostergroups" + " where username=%(LUser)s and %(LServer)H")) + end), + ok. + +update_roster(LUser, LServer, LJID, Item) -> + SJID = jid:encode(LJID), + ItemVals = record_to_row(Item), + ItemGroups = Item#roster.groups, + roster_subscribe(ItemVals), + ejabberd_sql:sql_query_t( + ?SQL("delete from rostergroups" + " where username=%(LUser)s and %(LServer)H and jid=%(SJID)s")), + lists:foreach( + fun(ItemGroup) -> + ejabberd_sql:sql_query_t( + ?SQL_INSERT( + "rostergroups", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "jid=%(SJID)s", + "grp=%(ItemGroup)s"])) + end, + ItemGroups). + +del_roster(LUser, LServer, LJID) -> + SJID = jid:encode(LJID), + ejabberd_sql:sql_query_t( + ?SQL("delete from rosterusers" + " where username=%(LUser)s and %(LServer)H and jid=%(SJID)s")), + ejabberd_sql:sql_query_t( + ?SQL("delete from rostergroups" + " where username=%(LUser)s and %(LServer)H and jid=%(SJID)s")). + +read_subscription_and_groups(LUser, LServer, LJID) -> + SJID = jid:encode(LJID), + case get_subscription(LServer, LUser, SJID) of + {selected, [{SSubscription, SAsk}]} -> + Subscription = decode_subscription(LUser, LServer, SSubscription), + Ask = decode_ask(LUser, LServer, SAsk), + Groups = case get_rostergroup_by_jid(LServer, LUser, SJID) of + {selected, JGrps} when is_list(JGrps) -> + [JGrp || {JGrp} <- JGrps]; + _ -> [] + end, + {ok, {Subscription, Ask, Groups}}; + _ -> + error + end. + +export(_Server) -> + [{roster, + fun(Host, #roster{usj = {_LUser, LServer, _LJID}} = R) + when LServer == Host -> + ItemVals = record_to_row(R), + ItemGroups = R#roster.groups, + update_roster_sql(ItemVals, ItemGroups); + (_Host, _R) -> + [] + end}, + {roster_version, + fun(Host, #roster_version{us = {LUser, LServer}, version = Ver}) + when LServer == Host -> + [?SQL("delete from roster_version" + " where username=%(LUser)s and %(LServer)H;"), + ?SQL_INSERT( + "roster_version", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "version=%(Ver)s"])]; + (_Host, _R) -> + [] + end}]. + +import(_, _, _) -> + ok. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +set_roster_version(LUser, LServer, Version) -> + ?SQL_UPSERT_T( + "roster_version", + ["!username=%(LUser)s", + "!server_host=%(LServer)s", + "version=%(Version)s"]). + +get_roster_jid_groups(LServer, LUser) -> + ejabberd_sql:sql_query( + LServer, + ?SQL("select @(jid)s, @(grp)s from rostergroups where " + "username=%(LUser)s and %(LServer)H")). + +get_roster_groups(LServer, LUser, SJID) -> + ejabberd_sql:sql_query_t( + ?SQL("select @(grp)s from rostergroups" + " where username=%(LUser)s and %(LServer)H and jid=%(SJID)s")). + +roster_subscribe({LUser, LServer, SJID, Name, SSubscription, SAsk, AskMessage}) -> + ?SQL_UPSERT_T( + "rosterusers", + ["!username=%(LUser)s", + "!server_host=%(LServer)s", + "!jid=%(SJID)s", + "nick=%(Name)s", + "subscription=%(SSubscription)s", + "ask=%(SAsk)s", + "askmessage=%(AskMessage)s", + "server='N'", + "subscribe=''", + "type='item'"]). + +get_roster_by_jid(LServer, LUser, SJID) -> + ejabberd_sql:sql_query_t( + ?SQL("select @(username)s, @(jid)s, @(nick)s, @(subscription)s," + " @(ask)s, @(askmessage)s, @(server)s, @(subscribe)s," + " @(type)s from rosterusers" + " where username=%(LUser)s and %(LServer)H and jid=%(SJID)s")). + +get_rostergroup_by_jid(LServer, LUser, SJID) -> + ejabberd_sql:sql_query( + LServer, + ?SQL("select @(grp)s from rostergroups" + " where username=%(LUser)s and %(LServer)H and jid=%(SJID)s")). + +get_subscription(LServer, LUser, SJID) -> + ejabberd_sql:sql_query( + LServer, + ?SQL("select @(subscription)s, @(ask)s from rosterusers " + "where username=%(LUser)s and %(LServer)H and jid=%(SJID)s")). + +update_roster_sql({LUser, LServer, SJID, Name, SSubscription, SAsk, AskMessage}, + ItemGroups) -> + [?SQL("delete from rosterusers where" + " username=%(LUser)s and %(LServer)H and jid=%(SJID)s;"), + ?SQL_INSERT( + "rosterusers", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "jid=%(SJID)s", + "nick=%(Name)s", + "subscription=%(SSubscription)s", + "ask=%(SAsk)s", + "askmessage=%(AskMessage)s", + "server='N'", + "subscribe=''", + "type='item'"]), + ?SQL("delete from rostergroups where" + " username=%(LUser)s and %(LServer)H and jid=%(SJID)s;")] + ++ + [?SQL_INSERT( + "rostergroups", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "jid=%(SJID)s", + "grp=%(ItemGroup)s"]) + || ItemGroup <- ItemGroups]. + +raw_to_record(LServer, + [User, LServer, SJID, Nick, SSubscription, SAsk, SAskMessage, + SServer, SSubscribe, SType]) -> + raw_to_record(LServer, + {User, LServer, SJID, Nick, SSubscription, SAsk, SAskMessage, + SServer, SSubscribe, SType}); +raw_to_record(LServer, + {User, SJID, Nick, SSubscription, SAsk, SAskMessage, + SServer, SSubscribe, SType}) -> + raw_to_record(LServer, + {User, LServer, SJID, Nick, SSubscription, SAsk, SAskMessage, + SServer, SSubscribe, SType}); +raw_to_record(LServer, + {User, LServer, SJID, Nick, SSubscription, SAsk, SAskMessage, + _SServer, _SSubscribe, _SType}) -> + try jid:decode(SJID) of + JID -> + LJID = jid:tolower(JID), + Subscription = decode_subscription(User, LServer, SSubscription), + Ask = decode_ask(User, LServer, SAsk), + #roster{usj = {User, LServer, LJID}, + us = {User, LServer}, jid = LJID, name = Nick, + subscription = Subscription, ask = Ask, + askmessage = SAskMessage} + catch _:{bad_jid, _} -> + ?ERROR_MSG("~ts", [format_row_error(User, LServer, {jid, SJID})]), + error + end. + +record_to_row( + #roster{us = {LUser, LServer}, + jid = JID, name = Name, subscription = Subscription, + ask = Ask, askmessage = AskMessage}) -> + SJID = jid:encode(jid:tolower(JID)), + SSubscription = case Subscription of + both -> <<"B">>; + to -> <<"T">>; + from -> <<"F">>; + none -> <<"N">> + end, + SAsk = case Ask of + subscribe -> <<"S">>; + unsubscribe -> <<"U">>; + both -> <<"B">>; + out -> <<"O">>; + in -> <<"I">>; + none -> <<"N">> + end, + {LUser, LServer, SJID, Name, SSubscription, SAsk, AskMessage}. + +decode_subscription(User, Server, S) -> + case S of + <<"B">> -> both; + <<"T">> -> to; + <<"F">> -> from; + <<"N">> -> none; + <<"">> -> none; + _ -> + ?ERROR_MSG("~ts", [format_row_error(User, Server, {subscription, S})]), + none + end. + +decode_ask(User, Server, A) -> + case A of + <<"S">> -> subscribe; + <<"U">> -> unsubscribe; + <<"B">> -> both; + <<"O">> -> out; + <<"I">> -> in; + <<"N">> -> none; + <<"">> -> none; + _ -> + ?ERROR_MSG("~ts", [format_row_error(User, Server, {ask, A})]), + none + end. + +format_row_error(User, Server, Why) -> + [case Why of + {jid, JID} -> ["Malformed 'jid' field with value '", JID, "'"]; + {subscription, Sub} -> ["Malformed 'subscription' field with value '", Sub, "'"]; + {ask, Ask} -> ["Malformed 'ask' field with value '", Ask, "'"] + end, + " detected for ", User, "@", Server, " in table 'rosterusers'"]. + +process_rosteritems(ActionS, SubsS, AsksS, UsersS, ContactsS) -> + process_rosteritems_sql(ActionS, list_to_atom(SubsS), list_to_atom(AsksS), + list_to_binary(UsersS), list_to_binary(ContactsS)). + +process_rosteritems_sql(ActionS, Subscription, Ask, SLocalJID, SJID) -> + [LUser, LServer] = binary:split(SLocalJID, <<"@">>), + SSubscription = case Subscription of + any -> <<"_">>; + both -> <<"B">>; + to -> <<"T">>; + from -> <<"F">>; + none -> <<"N">> + end, + SAsk = case Ask of + any -> <<"_">>; + subscribe -> <<"S">>; + unsubscribe -> <<"U">>; + both -> <<"B">>; + out -> <<"O">>; + in -> <<"I">>; + none -> <<"N">> + end, + {selected, List} = ejabberd_sql:sql_query( + LServer, + ?SQL("select @(username)s, @(jid)s from rosterusers " + "where username LIKE %(LUser)s" + " and %(LServer)H" + " and jid LIKE %(SJID)s" + " and subscription LIKE %(SSubscription)s" + " and ask LIKE %(SAsk)s")), + case ActionS of + "delete" -> [mod_roster:del_roster(User, LServer, jid:tolower(jid:decode(Contact))) || {User, Contact} <- List]; + "list" -> ok + end, + List. diff --git a/src/mod_s2s_bidi.erl b/src/mod_s2s_bidi.erl new file mode 100644 index 000000000..7b9556028 --- /dev/null +++ b/src/mod_s2s_bidi.erl @@ -0,0 +1,147 @@ +%%%------------------------------------------------------------------- +%%% Created : 20 Oct 2024 by Pawel Chmielowski +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_s2s_bidi). +-behaviour(gen_mod). +-protocol({xep, 288, '1.0.1', '24.10', "complete", ""}). + +%% gen_mod API +-export([start/2, stop/1, reload/3, depends/2, mod_options/1]). +-export([mod_doc/0]). +%% Hooks +-export([s2s_in_packet/2, s2s_out_packet/2, + s2s_in_features/2, s2s_in_auth_result/3, s2s_out_unauthenticated_features/2, s2s_in_handle_info/2]). + +-include_lib("xmpp/include/xmpp.hrl"). +-include("logger.hrl"). +-include("translate.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +start(_Host, _Opts) -> + {ok, [{hook, s2s_in_pre_auth_features, s2s_in_features, 50}, + {hook, s2s_in_post_auth_features, s2s_in_features, 50}, + {hook, s2s_in_unauthenticated_packet, s2s_in_packet, 50}, + {hook, s2s_in_authenticated_packet, s2s_in_packet, 50}, + {hook, s2s_in_handle_info, s2s_in_handle_info, 50}, + {hook, s2s_in_auth_result, s2s_in_auth_result, 50}, + {hook, s2s_out_unauthenticated_features, s2s_out_unauthenticated_features, 50}, + {hook, s2s_out_packet, s2s_out_packet, 50}]}. + +stop(_Host) -> + ok. + +reload(_Host, _NewOpts, _OldOpts) -> + ok. + +depends(_Host, _Opts) -> + []. + +mod_options(_Host) -> + []. + +mod_doc() -> + #{desc => + [?T("The module adds support for " + "https://xmpp.org/extensions/xep-0288.html" + "[XEP-0288: Bidirectional Server-to-Server Connections] that allows using " + "single s2s connection to communicate in both directions.")], + note => "added in 24.10", + opts => [], + example => + ["modules:", + " mod_s2s_bidi: {}"]}. + +s2s_in_features(Acc, _) -> + [#s2s_bidi_feature{}|Acc]. + +s2s_in_packet(State, #s2s_bidi{}) -> + {stop, State#{bidi_enabled => true}}; +s2s_in_packet(State, _) -> + State. + +s2s_out_unauthenticated_features(#{db_verify := _} = State, _) -> + State; +s2s_out_unauthenticated_features(State, #stream_features{} = Pkt) -> + try xmpp:try_subtag(Pkt, #s2s_bidi{}) of + #s2s_bidi{} -> + ejabberd_s2s_out:send(State#{bidi_enabled => true}, #s2s_bidi{}); + _ -> + State + catch _:{xmpp_codec, _Why} -> + State + end; +s2s_out_unauthenticated_features(State, _Pkt) -> + State. + +s2s_out_packet(#{bidi_enabled := true, ip := {IP, _}} = State, Pkt0) + when ?is_stanza(Pkt0) -> + To = xmpp:get_to(Pkt0), + case check_from_to(State, xmpp:get_from(Pkt0), To) of + ok -> + Pkt = xmpp:put_meta(Pkt0, ip, IP), + LServer = ejabberd_router:host_of_route(To#jid.lserver), + State1 = ejabberd_hooks:run_fold(s2s_in_authenticated_packet, + LServer, State, [Pkt]), + {Pkt1, State2} = ejabberd_hooks:run_fold(s2s_receive_packet, LServer, + {Pkt, State1}, []), + case Pkt1 of + drop -> ok; + _ -> ejabberd_router:route(Pkt1) + end, + {stop, State2}; + {error, Err} -> + {stop, ejabberd_s2s_out:send(State, Err)} + end; +s2s_out_packet(#{db_verify := _} = State, #stream_features{}) -> + State; +s2s_out_packet(State, #stream_features{} = Pkt) -> + try xmpp:try_subtag(Pkt, #s2s_bidi_feature{}) of + #s2s_bidi_feature{} -> + ejabberd_s2s_out:send(State#{bidi_enabled => true}, #s2s_bidi{}) + catch _:{xmpp_codec, _Why} -> + State + end; +s2s_out_packet(State, _Pkt) -> + State. + +s2s_in_handle_info(State, {route, Pkt}) when ?is_stanza(Pkt) -> + {stop, ejabberd_s2s_in:send(State, Pkt)}; +s2s_in_handle_info(State, _Info) -> + State. + +check_from_to(#{remote_server := RServer}, #jid{lserver = FromServer}, + #jid{lserver = ToServer}) -> + if + RServer /= FromServer -> {error, xmpp:serr_invalid_from()}; + true -> + case ejabberd_router:is_my_route(ToServer) of + false -> {error, xmpp:serr_host_unknown()}; + _ -> ok + end + end. + +s2s_in_auth_result(#{server := LServer, bidi_enabled := true} = State, true, RServer) -> + ejabberd_s2s:register_connection({LServer, RServer}), + State; +s2s_in_auth_result(State, _, _) -> + State. diff --git a/src/mod_s2s_dialback.erl b/src/mod_s2s_dialback.erl new file mode 100644 index 000000000..f6128b573 --- /dev/null +++ b/src/mod_s2s_dialback.erl @@ -0,0 +1,353 @@ +%%%------------------------------------------------------------------- +%%% Created : 16 Dec 2016 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_s2s_dialback). +-behaviour(gen_mod). +-protocol({xep, 220, '1.1.1', '17.03', "complete", ""}). +-protocol({xep, 185, '1.0', '17.03', "complete", ""}). + +%% gen_mod API +-export([start/2, stop/1, reload/3, depends/2, mod_opt_type/1, mod_options/1]). +-export([mod_doc/0]). +%% Hooks +-export([s2s_out_auth_result/2, s2s_out_downgraded/2, + s2s_in_packet/2, s2s_out_packet/2, s2s_in_recv/3, + s2s_in_features/2, s2s_out_init/2, s2s_out_closed/2, + s2s_out_tls_verify/2]). + +-include_lib("xmpp/include/xmpp.hrl"). +-include("logger.hrl"). +-include("translate.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +start(_Host, _Opts) -> + {ok, [{hook, s2s_out_init, s2s_out_init, 50}, + {hook, s2s_out_closed, s2s_out_closed, 50}, + {hook, s2s_in_pre_auth_features, s2s_in_features, 50}, + {hook, s2s_in_post_auth_features, s2s_in_features, 50}, + {hook, s2s_in_handle_recv, s2s_in_recv, 50}, + {hook, s2s_in_unauthenticated_packet, s2s_in_packet, 50}, + {hook, s2s_in_authenticated_packet, s2s_in_packet, 50}, + {hook, s2s_out_packet, s2s_out_packet, 50}, + {hook, s2s_out_downgraded, s2s_out_downgraded, 50}, + {hook, s2s_out_auth_result, s2s_out_auth_result, 50}, + {hook, s2s_out_tls_verify, s2s_out_tls_verify, 50}]}. + +stop(_Host) -> + ok. + +reload(_Host, _NewOpts, _OldOpts) -> + ok. + +depends(_Host, _Opts) -> + []. + +mod_opt_type(access) -> + econf:acl(). + +mod_options(_Host) -> + [{access, all}]. + +mod_doc() -> + #{desc => + [?T("The module adds support for " + "https://xmpp.org/extensions/xep-0220.html" + "[XEP-0220: Server Dialback] to provide server identity " + "verification based on DNS."), "", + ?T("WARNING: DNS-based verification is vulnerable to " + "https://en.wikipedia.org/wiki/DNS_spoofing" + "[DNS cache poisoning], so modern servers rely on " + "verification based on PKIX certificates. Thus this module " + "is only recommended for backward compatibility " + "with servers running outdated software or non-TLS servers, " + "or those with invalid certificates (as long as you accept " + "the risks, e.g. you assume that the remote server has " + "an invalid certificate due to poor administration and " + "not because it's compromised).")], + opts => + [{access, + #{value => ?T("AccessName"), + desc => + ?T("An access rule that can be used to restrict " + "dialback for some servers. The default value " + "is 'all'.")}}], + example => + ["modules:", + " mod_s2s_dialback:", + " access:", + " allow:", + " server: legacy.domain.tld", + " server: invalid-cert.example.org", + " deny: all"]}. + +s2s_in_features(Acc, _) -> + [#db_feature{errors = true}|Acc]. + +s2s_out_init({ok, State}, Opts) -> + case proplists:get_value(db_verify, Opts) of + {StreamID, Key, Pid} -> + %% This is an outbound s2s connection created at step 1. + %% The purpose of this connection is to verify dialback key ONLY. + %% The connection is not registered in s2s table and thus is not + %% seen by anyone. + %% The connection will be closed immediately after receiving the + %% verification response (at step 3) + {ok, State#{db_verify => {StreamID, Key, Pid}}}; + undefined -> + {ok, State#{db_enabled => true}} + end; +s2s_out_init(Acc, _Opts) -> + Acc. + +s2s_out_closed(#{server := LServer, + remote_server := RServer, + lang := Lang, + db_verify := {StreamID, _Key, _Pid}} = State, Reason) -> + %% Outbound s2s verificating connection (created at step 1) is + %% closed suddenly without receiving the response. + %% Building a response on our own + Response = #db_verify{from = RServer, to = LServer, + id = StreamID, type = error, + sub_els = [mk_error(Reason, Lang)]}, + s2s_out_packet(State, Response); +s2s_out_closed(State, _Reason) -> + State. + +s2s_out_auth_result(#{db_verify := _} = State, _) -> + %% The temporary outbound s2s connect (intended for verification) + %% has passed authentication state (either successfully or not, no matter) + %% and at this point we can send verification request as described + %% in section 2.1.2, step 2 + {stop, send_verify_request(State)}; +s2s_out_auth_result(#{db_enabled := true, + socket := Socket, ip := IP, + server := LServer, + remote_server := RServer} = State, {false, _}) -> + %% SASL authentication has failed, retrying with dialback + %% Sending dialback request, section 2.1.1, step 1 + ?INFO_MSG("(~ts) Retrying with s2s dialback authentication: ~ts -> ~ts (~ts)", + [xmpp_socket:pp(Socket), LServer, RServer, + ejabberd_config:may_hide_data(misc:ip_to_list(IP))]), + State1 = maps:remove(stop_reason, State#{on_route => queue}), + {stop, send_db_request(State1)}; +s2s_out_auth_result(State, _) -> + State. + +s2s_out_downgraded(#{db_verify := _} = State, _) -> + %% The verifying outbound s2s connection detected non-RFC compliant + %% server, send verification request immediately without auth phase, + %% section 2.1.2, step 2 + {stop, send_verify_request(State)}; +s2s_out_downgraded(#{db_enabled := true, + socket := Socket, ip := IP, + server := LServer, + remote_server := RServer} = State, _) -> + %% non-RFC compliant server detected, send dialback request instantly, + %% section 2.1.1, step 1 + ?INFO_MSG("(~ts) Trying s2s dialback authentication with " + "non-RFC compliant server: ~ts -> ~ts (~ts)", + [xmpp_socket:pp(Socket), LServer, RServer, + ejabberd_config:may_hide_data(misc:ip_to_list(IP))]), + {stop, send_db_request(State)}; +s2s_out_downgraded(State, _) -> + State. + +s2s_in_packet(#{stream_id := StreamID, lang := Lang} = State, + #db_result{from = From, to = To, key = Key, type = undefined}) -> + %% Received dialback request, section 2.2.1, step 1 + try + ok = check_from_to(From, To), + %% We're creating a temporary outbound s2s connection to + %% send verification request and to receive verification response + {ok, Pid} = ejabberd_s2s_out:start( + To, From, [{db_verify, {StreamID, Key, self()}}]), + ejabberd_s2s_out:connect(Pid), + {stop, State} + catch _:{badmatch, {error, Reason}} -> + {stop, + send_db_result(State, + #db_verify{from = From, to = To, type = error, + sub_els = [mk_error(Reason, Lang)]})} + end; +s2s_in_packet(State, #db_verify{to = To, from = From, key = Key, + id = StreamID, type = undefined}) -> + %% Received verification request, section 2.2.2, step 2 + Type = case make_key(To, From, StreamID) of + Key -> valid; + _ -> invalid + end, + Response = #db_verify{from = To, to = From, id = StreamID, type = Type}, + {stop, ejabberd_s2s_in:send(State, Response)}; +s2s_in_packet(State, Pkt) when is_record(Pkt, db_result); + is_record(Pkt, db_verify) -> + ?WARNING_MSG("Got stray dialback packet:~n~ts", [xmpp:pp(Pkt)]), + State; +s2s_in_packet(State, _) -> + State. + +s2s_in_recv(#{lang := Lang} = State, El, {error, Why}) -> + case xmpp:get_name(El) of + Tag when Tag == <<"db:result">>; + Tag == <<"db:verify">> -> + case xmpp:get_type(El) of + T when T /= <<"valid">>, + T /= <<"invalid">>, + T /= <<"error">> -> + Err = xmpp:make_error(El, mk_error({codec_error, Why}, Lang)), + {stop, ejabberd_s2s_in:send(State, Err)}; + _ -> + State + end; + _ -> + State + end; +s2s_in_recv(State, _El, _Pkt) -> + State. + +s2s_out_packet(#{server := LServer, + remote_server := RServer, + db_verify := {StreamID, _Key, Pid}} = State, + #db_verify{from = RServer, to = LServer, + id = StreamID, type = Type} = Response) + when Type /= undefined -> + %% Received verification response, section 2.1.2, step 3 + %% This is a response for the request sent at step 2 + ejabberd_s2s_in:update_state( + Pid, fun(S) -> send_db_result(S, Response) end), + %% At this point the connection is no longer needed and we can terminate it + ejabberd_s2s_out:stop_async(self()), + State; +s2s_out_packet(#{server := LServer, remote_server := RServer} = State, + #db_result{to = LServer, from = RServer, + type = Type} = Result) when Type /= undefined -> + %% Received dialback response, section 2.1.1, step 4 + %% This is a response to the request sent at step 1 + State1 = maps:remove(db_enabled, State), + case Type of + valid -> + State2 = ejabberd_s2s_out:handle_auth_success(<<"dialback">>, State1), + ejabberd_s2s_out:establish(State2); + _ -> + Reason = str:format("Peer responded with error: ~s", + [format_error(Result)]), + ejabberd_s2s_out:handle_auth_failure( + <<"dialback">>, {auth, Reason}, State1) + end; +s2s_out_packet(State, Pkt) when is_record(Pkt, db_result); + is_record(Pkt, db_verify) -> + ?WARNING_MSG("Got stray dialback packet:~n~ts", [xmpp:pp(Pkt)]), + State; +s2s_out_packet(State, _) -> + State. + +-spec s2s_out_tls_verify(boolean(), ejabberd_s2s_out:state()) -> boolean(). +s2s_out_tls_verify(_, #{server_host := ServerHost, remote_server := RServer}) -> + Access = mod_s2s_dialback_opt:access(ServerHost), + case acl:match_rule(ServerHost, Access, jid:make(RServer)) of + allow -> false; + deny -> true + end. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +-spec make_key(binary(), binary(), binary()) -> binary(). +make_key(From, To, StreamID) -> + Secret = ejabberd_config:get_shared_key(), + str:to_hexlist( + misc:crypto_hmac(sha256, str:to_hexlist(crypto:hash(sha256, Secret)), + [To, " ", From, " ", StreamID])). + +-spec send_verify_request(ejabberd_s2s_out:state()) -> ejabberd_s2s_out:state(). +send_verify_request(#{server := LServer, + remote_server := RServer, + db_verify := {StreamID, Key, _Pid}} = State) -> + Request = #db_verify{from = LServer, to = RServer, + key = Key, id = StreamID}, + ejabberd_s2s_out:send(State, Request). + +-spec send_db_request(ejabberd_s2s_out:state()) -> ejabberd_s2s_out:state(). +send_db_request(#{server := LServer, + remote_server := RServer, + stream_remote_id := StreamID} = State) -> + Key = make_key(LServer, RServer, StreamID), + ejabberd_s2s_out:send(State, #db_result{from = LServer, + to = RServer, + key = Key}). + +-spec send_db_result(ejabberd_s2s_in:state(), db_verify()) -> ejabberd_s2s_in:state(). +send_db_result(State, #db_verify{from = From, to = To, + type = Type, sub_els = Els}) -> + %% Sending dialback response, section 2.2.1, step 4 + %% This is a response to the request received at step 1 + Response = #db_result{from = To, to = From, type = Type, sub_els = Els}, + State1 = ejabberd_s2s_in:send(State, Response), + case Type of + valid -> + State2 = ejabberd_s2s_in:handle_auth_success( + From, <<"dialback">>, undefined, State1), + ejabberd_s2s_in:establish(State2); + _ -> + Reason = str:format("Verification failed: ~s", + [format_error(Response)]), + ejabberd_s2s_in:handle_auth_failure( + From, <<"dialback">>, Reason, State1) + end. + +-spec check_from_to(binary(), binary()) -> ok | {error, forbidden | host_unknown}. +check_from_to(From, To) -> + case ejabberd_router:is_my_route(To) of + false -> {error, host_unknown}; + true -> + LServer = ejabberd_router:host_of_route(To), + case ejabberd_s2s:allow_host(LServer, From) of + true -> ok; + false -> {error, forbidden} + end + end. + +-spec mk_error(term(), binary()) -> stanza_error(). +mk_error(forbidden, Lang) -> + xmpp:err_forbidden(?T("Access denied by service policy"), Lang); +mk_error(host_unknown, Lang) -> + xmpp:err_not_allowed(?T("Host unknown"), Lang); +mk_error({codec_error, Why}, Lang) -> + xmpp:err_bad_request(xmpp:io_format_error(Why), Lang); +mk_error({_Class, _Reason} = Why, Lang) -> + Txt = xmpp_stream_out:format_error(Why), + xmpp:err_remote_server_not_found(Txt, Lang); +mk_error(_, _) -> + xmpp:err_internal_server_error(). + +-spec format_error(db_result()) -> binary(). +format_error(#db_result{type = invalid}) -> + <<"invalid dialback key">>; +format_error(#db_result{type = error} = Result) -> + case xmpp:get_error(Result) of + #stanza_error{} = Err -> + xmpp:format_stanza_error(Err); + undefined -> + <<"unrecognized error">> + end; +format_error(_) -> + <<"unexpected dialback result">>. diff --git a/src/mod_s2s_dialback_opt.erl b/src/mod_s2s_dialback_opt.erl new file mode 100644 index 000000000..6f91c4dd1 --- /dev/null +++ b/src/mod_s2s_dialback_opt.erl @@ -0,0 +1,13 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_s2s_dialback_opt). + +-export([access/1]). + +-spec access(gen_mod:opts() | global | binary()) -> 'all' | acl:acl(). +access(Opts) when is_map(Opts) -> + gen_mod:get_opt(access, Opts); +access(Host) -> + gen_mod:get_module_opt(Host, mod_s2s_dialback, access). + diff --git a/src/mod_scram_upgrade.erl b/src/mod_scram_upgrade.erl new file mode 100644 index 000000000..37af47b46 --- /dev/null +++ b/src/mod_scram_upgrade.erl @@ -0,0 +1,133 @@ +%%%------------------------------------------------------------------- +%%% Created : 20 Oct 2024 by Pawel Chmielowski +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_scram_upgrade). +-behaviour(gen_mod). +-protocol({xep, 480, '0.2.0', '24.10', "complete", ""}). + +%% gen_mod API +-export([start/2, stop/1, reload/3, depends/2, mod_options/1, mod_opt_type/1]). +-export([mod_doc/0]). +%% Hooks +-export([c2s_inline_features/3, c2s_handle_sasl2_inline/1, + c2s_handle_sasl2_task_next/4, c2s_handle_sasl2_task_data/3]). + +-include_lib("xmpp/include/xmpp.hrl"). +-include_lib("xmpp/include/scram.hrl"). +-include("logger.hrl"). +-include("translate.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +start(_Host, _Opts) -> + {ok, [{hook, c2s_inline_features, c2s_inline_features, 50}, + {hook, c2s_handle_sasl2_inline, c2s_handle_sasl2_inline, 10}, + {hook, c2s_handle_sasl2_task_next, c2s_handle_sasl2_task_next, 10}, + {hook, c2s_handle_sasl2_task_data, c2s_handle_sasl2_task_data, 10}]}. + +stop(_Host) -> + ok. + +reload(_Host, _NewOpts, _OldOpts) -> + ok. + +depends(_Host, _Opts) -> + []. + +mod_opt_type(offered_upgrades) -> + econf:list(econf:enum([sha256, sha512])). + +mod_options(_Host) -> + [{offered_upgrades, [sha256, sha512]}]. + +mod_doc() -> + #{desc => + [?T("The module adds support for " + "https://xmpp.org/extensions/xep-0480.html" + "[XEP-0480: SASL Upgrade Tasks] that allows users to upgrade " + "passwords to more secure representation.")], + note => "added in 24.10", + opts => [{offered_upgrades, + #{value => "list(sha256, sha512)", + desc => ?T("List with upgrade types that should be offered")}}], + example => + ["modules:", + " mod_scram_upgrade:", + " offered_upgrades:", + " - sha256", + " - sha512"]}. + +c2s_inline_features({Sasl, Bind, Extra}, Host, State) -> + KnowTypes = case State of + #{sasl2_password_fun := Fun} -> + case Fun(<<>>) of + {Pass, _} -> lists:filtermap( + fun(#scram{hash = sha256}) -> {true, sha256}; + (#scram{hash = sha512}) -> {true, sha512}; + (_) -> false + end, Pass); + _ -> [] + end; + _ -> [] + end, + Methods = lists:filtermap( + fun(sha256) -> {true, #sasl_upgrade{cdata = <<"UPGR-SCRAM-SHA-256">>}}; + (sha512) -> {true, #sasl_upgrade{cdata = <<"UPGR-SCRAM-SHA-512">>}} + end, mod_scram_upgrade_opt:offered_upgrades(Host) -- KnowTypes), + {Sasl, Bind, Methods ++ Extra}. + +c2s_handle_sasl2_inline({State, Els, _Results} = Acc) -> + case lists:keyfind(sasl_upgrade, 1, Els) of + false -> + Acc; + #sasl_upgrade{cdata = Type} -> + {stop, {State, {continue, [Type]}, []}} + end. + +c2s_handle_sasl2_task_next({_, State}, Task, _Els, _InlineEls) -> + Algo = case Task of + <<"UPGR-SCRAM-SHA-256">> -> sha256; + <<"UPGR-SCRAM-SHA-512">> -> sha512 + end, + Salt = p1_rand:bytes(16), + {task_data, [#scram_upgrade_salt{cdata = Salt, iterations = 4096}], + State#{scram_upgrade => {Algo, Salt, 4096}}}. + +c2s_handle_sasl2_task_data({_, #{user := User, server := Server, + scram_upgrade := {Algo, Salt, Iter}} = State}, + Els, InlineEls) -> + case xmpp:get_subtag(#sasl2_task_data{sub_els = Els}, #scram_upgrade_hash{}) of + #scram_upgrade_hash{data = SaltedPassword} -> + StoredKey = scram:stored_key(Algo, scram:client_key(Algo, SaltedPassword)), + ServerKey = scram:server_key(Algo, SaltedPassword), + ejabberd_auth:set_password_instance(User, Server, + #scram{hash = Algo, iterationcount = Iter, + salt = base64:encode(Salt), + serverkey = base64:encode(ServerKey), + storedkey = base64:encode(StoredKey)}), + State2 = maps:remove(scram_upgrade, State), + InlineEls2 = lists:keydelete(sasl_upgrade, 1, InlineEls), + {State3, NewEls, Results} = ejabberd_c2s:handle_sasl2_inline(InlineEls2, State2), + {success, NewEls, Results, State3}; + _ -> + {abort, State} + end. diff --git a/src/mod_scram_upgrade_opt.erl b/src/mod_scram_upgrade_opt.erl new file mode 100644 index 000000000..abd6bad4b --- /dev/null +++ b/src/mod_scram_upgrade_opt.erl @@ -0,0 +1,13 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_scram_upgrade_opt). + +-export([offered_upgrades/1]). + +-spec offered_upgrades(gen_mod:opts() | global | binary()) -> ['sha256' | 'sha512']. +offered_upgrades(Opts) when is_map(Opts) -> + gen_mod:get_opt(offered_upgrades, Opts); +offered_upgrades(Host) -> + gen_mod:get_module_opt(Host, mod_scram_upgrade, offered_upgrades). + diff --git a/src/mod_service_log.erl b/src/mod_service_log.erl index 7a23b4013..533ff517f 100644 --- a/src/mod_service_log.erl +++ b/src/mod_service_log.erl @@ -5,7 +5,7 @@ %%% Created : 24 Aug 2003 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -29,69 +29,70 @@ -behaviour(gen_mod). --export([start/2, - stop/1, - log_user_send/3, - log_user_receive/4]). +-export([start/2, stop/1, log_user_send/1, mod_options/1, + log_user_receive/1, mod_opt_type/1, depends/2, mod_doc/0]). --include("ejabberd.hrl"). -include("logger.hrl"). +-include("translate.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). --include("jlib.hrl"). +start(_Host, _Opts) -> + {ok, [{hook, user_send_packet, log_user_send, 50}, + {hook, user_receive_packet, log_user_receive, 50}]}. -start(Host, _Opts) -> - ejabberd_hooks:add(user_send_packet, Host, ?MODULE, - log_user_send, 50), - ejabberd_hooks:add(user_receive_packet, Host, ?MODULE, - log_user_receive, 50), +stop(_Host) -> ok. -stop(Host) -> - ejabberd_hooks:delete(user_send_packet, Host, ?MODULE, - log_user_send, 50), - ejabberd_hooks:delete(user_receive_packet, Host, - ?MODULE, log_user_receive, 50), - ok. +depends(_Host, _Opts) -> + []. -log_user_send(From, To, Packet) -> - log_packet(From, To, Packet, From#jid.lserver). +-spec log_user_send({stanza(), ejabberd_c2s:state()}) -> {stanza(), ejabberd_c2s:state()}. +log_user_send({Packet, C2SState}) -> + From = xmpp:get_from(Packet), + log_packet(Packet, From#jid.lserver), + {Packet, C2SState}. -log_user_receive(_JID, From, To, Packet) -> - log_packet(From, To, Packet, To#jid.lserver). +-spec log_user_receive({stanza(), ejabberd_c2s:state()}) -> {stanza(), ejabberd_c2s:state()}. +log_user_receive({Packet, C2SState}) -> + To = xmpp:get_to(Packet), + log_packet(Packet, To#jid.lserver), + {Packet, C2SState}. -log_packet(From, To, - #xmlel{name = Name, attrs = Attrs, children = Els}, - Host) -> - Loggers = gen_mod:get_module_opt(Host, ?MODULE, loggers, - fun(L) -> - lists:map( - fun(S) -> - B = iolist_to_binary(S), - N = jlib:nameprep(B), - if N /= error -> - N - end - end, L) - end, []), - ServerJID = #jid{user = <<"">>, server = Host, - resource = <<"">>, luser = <<"">>, lserver = Host, - lresource = <<"">>}, - NewAttrs = - jlib:replace_from_to_attrs(jlib:jid_to_string(From), - jlib:jid_to_string(To), Attrs), - FixedPacket = #xmlel{name = Name, attrs = NewAttrs, - children = Els}, - lists:foreach(fun (Logger) -> - ejabberd_router:route(ServerJID, - #jid{user = <<"">>, - server = Logger, - resource = <<"">>, - luser = <<"">>, - lserver = Logger, - lresource = <<"">>}, - #xmlel{name = <<"route">>, - attrs = [], - children = - [FixedPacket]}) - end, - Loggers). +-spec log_packet(stanza(), binary()) -> ok. +log_packet(Packet, Host) -> + Loggers = mod_service_log_opt:loggers(Host), + ForwardedMsg = #message{from = jid:make(Host), + id = p1_rand:get_string(), + sub_els = [#forwarded{ + sub_els = [Packet]}]}, + lists:foreach( + fun(Logger) -> + ejabberd_router:route(xmpp:set_to(ForwardedMsg, jid:make(Logger))) + end, Loggers). + +mod_opt_type(loggers) -> + econf:list(econf:domain()). + +mod_options(_) -> + [{loggers, []}]. + +mod_doc() -> + #{desc => + ?T("This module forwards copies of all stanzas " + "to remote XMPP servers or components. " + "Every stanza is encapsulated into " + "element as described in " + "https://xmpp.org/extensions/xep-0297.html" + "[XEP-0297: Stanza Forwarding]."), + opts => + [{loggers, + #{value => "[Domain, ...]", + desc => + ?T("A list of servers or connected components " + "to which stanzas will be forwarded.")}}], + example => + ["modules:", + " mod_service_log:", + " loggers:", + " - xmpp-server.tld", + " - component.domain.tld"]}. diff --git a/src/mod_service_log_opt.erl b/src/mod_service_log_opt.erl new file mode 100644 index 000000000..34eae49a6 --- /dev/null +++ b/src/mod_service_log_opt.erl @@ -0,0 +1,13 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_service_log_opt). + +-export([loggers/1]). + +-spec loggers(gen_mod:opts() | global | binary()) -> [binary()]. +loggers(Opts) when is_map(Opts) -> + gen_mod:get_opt(loggers, Opts); +loggers(Host) -> + gen_mod:get_module_opt(Host, mod_service_log, loggers). + diff --git a/src/mod_shared_roster.erl b/src/mod_shared_roster.erl index 16800ded4..1c9e6f88f 100644 --- a/src/mod_shared_roster.erl +++ b/src/mod_shared_roster.erl @@ -5,7 +5,7 @@ %%% Created : 5 Mar 2005 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -29,21 +29,23 @@ -behaviour(gen_mod). --export([start/2, stop/1, item_to_xml/1, export/1, import/1, - webadmin_menu/3, webadmin_page/3, get_user_roster/2, - get_subscription_lists/3, get_jid_info/4, import/3, - process_item/2, in_subscription/6, out_subscription/4, - user_available/1, unset_presence/4, register_user/2, - remove_user/2, list_groups/1, create_group/2, - create_group/3, delete_group/2, get_group_opts/2, - set_group_opts/3, get_group_users/2, - get_group_explicit_users/2, is_user_in_group/3, - add_user_to_group/3, remove_user_from_group/3]). +-export([start/2, stop/1, reload/3, export/1, + import_info/0, webadmin_menu/3, webadmin_page/3, + get_user_roster/2, + get_jid_info/4, import/5, process_item/2, import_start/2, + in_subscription/2, out_subscription/1, c2s_self_presence/1, + unset_presence/4, register_user/2, remove_user/2, + list_groups/1, create_group/2, create_group/3, + delete_group/2, get_group_opts/2, set_group_opts/3, + get_group_users/2, get_group_explicit_users/2, + is_user_in_group/3, add_user_to_group/3, opts_to_binary/1, + remove_user_from_group/3, mod_opt_type/1, mod_options/1, mod_doc/0, depends/2]). + +-import(ejabberd_web_admin, [make_command/4, make_command_raw_value/3, make_table/2, make_table/4]). --include("ejabberd.hrl"). -include("logger.hrl"). --include("jlib.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). -include("mod_roster.hrl"). @@ -51,180 +53,174 @@ -include("ejabberd_web_admin.hrl"). --record(sr_group, {group_host = {<<"">>, <<"">>} :: {'$1' | binary(), '$2' | binary()}, - opts = [] :: list() | '_' | '$2'}). +-include("mod_shared_roster.hrl"). --record(sr_user, {us = {<<"">>, <<"">>} :: {binary(), binary()}, - group_host = {<<"">>, <<"">>} :: {binary(), binary()}}). +-include("translate.hrl"). + +-type group_options() :: [{atom(), any()}]. +-callback init(binary(), gen_mod:opts()) -> any(). +-callback import(binary(), binary(), [binary()]) -> ok. +-callback list_groups(binary()) -> [binary()]. +-callback groups_with_opts(binary()) -> [{binary(), group_options()}]. +-callback create_group(binary(), binary(), group_options()) -> {atomic, any()}. +-callback delete_group(binary(), binary()) -> {atomic, any()}. +-callback get_group_opts(binary(), binary()) -> group_options() | error. +-callback set_group_opts(binary(), binary(), group_options()) -> {atomic, any()}. +-callback get_user_groups({binary(), binary()}, binary()) -> [binary()]. +-callback get_group_explicit_users(binary(), binary()) -> [{binary(), binary()}]. +-callback get_user_displayed_groups(binary(), binary(), group_options()) -> + [{binary(), group_options()}]. +-callback is_user_in_group({binary(), binary()}, binary(), binary()) -> boolean(). +-callback add_user_to_group(binary(), {binary(), binary()}, binary()) -> any(). +-callback remove_user_from_group(binary(), {binary(), binary()}, binary()) -> {atomic, any()}. +-callback use_cache(binary()) -> boolean(). +-callback cache_nodes(binary()) -> [node()]. + +-optional_callbacks([use_cache/1, cache_nodes/1]). + +-define(GROUP_OPTS_CACHE, shared_roster_group_opts_cache). +-define(USER_GROUPS_CACHE, shared_roster_user_groups_cache). +-define(GROUP_EXPLICIT_USERS_CACHE, shared_roster_group_explicit_cache). +-define(SPECIAL_GROUPS_CACHE, shared_roster_special_groups_cache). start(Host, Opts) -> - case gen_mod:db_type(Opts) of - mnesia -> - mnesia:create_table(sr_group, - [{disc_copies, [node()]}, - {attributes, record_info(fields, sr_group)}]), - mnesia:create_table(sr_user, - [{disc_copies, [node()]}, {type, bag}, - {attributes, record_info(fields, sr_user)}]), - update_tables(), - mnesia:add_table_index(sr_user, group_host); - _ -> ok + Mod = gen_mod:db_mod(Opts, ?MODULE), + Mod:init(Host, Opts), + init_cache(Mod, Host, Opts), + {ok, [{hook, webadmin_menu_host, webadmin_menu, 70}, + {hook, webadmin_page_host, webadmin_page, 50}, + {hook, roster_get, get_user_roster, 70}, + {hook, roster_in_subscription, in_subscription, 30}, + {hook, roster_out_subscription, out_subscription, 30}, + {hook, roster_get_jid_info, get_jid_info, 70}, + {hook, roster_process_item, process_item, 50}, + {hook, c2s_self_presence, c2s_self_presence, 50}, + {hook, unset_presence_hook, unset_presence, 50}, + {hook, register_user, register_user, 50}, + {hook, remove_user, remove_user, 50}]}. + +stop(_Host) -> + ok. + +reload(Host, NewOpts, OldOpts) -> + NewMod = gen_mod:db_mod(NewOpts, ?MODULE), + OldMod = gen_mod:db_mod(OldOpts, ?MODULE), + if + NewMod /= OldMod -> + NewMod:init(Host, NewOpts); + true -> + ok end, - ejabberd_hooks:add(webadmin_menu_host, Host, ?MODULE, - webadmin_menu, 70), - ejabberd_hooks:add(webadmin_page_host, Host, ?MODULE, - webadmin_page, 50), - ejabberd_hooks:add(roster_get, Host, ?MODULE, - get_user_roster, 70), - ejabberd_hooks:add(roster_in_subscription, Host, - ?MODULE, in_subscription, 30), - ejabberd_hooks:add(roster_out_subscription, Host, - ?MODULE, out_subscription, 30), - ejabberd_hooks:add(roster_get_subscription_lists, Host, - ?MODULE, get_subscription_lists, 70), - ejabberd_hooks:add(roster_get_jid_info, Host, ?MODULE, - get_jid_info, 70), - ejabberd_hooks:add(roster_process_item, Host, ?MODULE, - process_item, 50), - ejabberd_hooks:add(user_available_hook, Host, ?MODULE, - user_available, 50), - ejabberd_hooks:add(unset_presence_hook, Host, ?MODULE, - unset_presence, 50), - ejabberd_hooks:add(register_user, Host, ?MODULE, - register_user, 50), - ejabberd_hooks:add(anonymous_purge_hook, Host, ?MODULE, - remove_user, 50), - ejabberd_hooks:add(remove_user, Host, ?MODULE, - remove_user, 50). + init_cache(NewMod, Host, NewOpts), + ok. -%%ejabberd_hooks:add(remove_user, Host, -%% ?MODULE, remove_user, 50), +depends(_Host, _Opts) -> + []. -stop(Host) -> - ejabberd_hooks:delete(webadmin_menu_host, Host, ?MODULE, - webadmin_menu, 70), - ejabberd_hooks:delete(webadmin_page_host, Host, ?MODULE, - webadmin_page, 50), - ejabberd_hooks:delete(roster_get, Host, ?MODULE, - get_user_roster, 70), - ejabberd_hooks:delete(roster_in_subscription, Host, - ?MODULE, in_subscription, 30), - ejabberd_hooks:delete(roster_out_subscription, Host, - ?MODULE, out_subscription, 30), - ejabberd_hooks:delete(roster_get_subscription_lists, - Host, ?MODULE, get_subscription_lists, 70), - ejabberd_hooks:delete(roster_get_jid_info, Host, - ?MODULE, get_jid_info, 70), - ejabberd_hooks:delete(roster_process_item, Host, - ?MODULE, process_item, 50), - ejabberd_hooks:delete(user_available_hook, Host, - ?MODULE, user_available, 50), - ejabberd_hooks:delete(unset_presence_hook, Host, - ?MODULE, unset_presence, 50), - ejabberd_hooks:delete(register_user, Host, ?MODULE, - register_user, 50), - ejabberd_hooks:delete(anonymous_purge_hook, Host, - ?MODULE, remove_user, 50), -%%ejabberd_hooks:delete(remove_user, Host, -%% ?MODULE, remove_user, 50), - ejabberd_hooks:delete(remove_user, Host, ?MODULE, - remove_user, - 50).%%ejabberd_hooks:delete(remove_user, Host, - %% ?MODULE, remove_user, 50), +-spec init_cache(module(), binary(), gen_mod:opts()) -> ok. +init_cache(Mod, Host, Opts) -> + NumHosts = length(ejabberd_option:hosts()), + ets_cache:new(?SPECIAL_GROUPS_CACHE, [{max_size, NumHosts * 4}]), + case use_cache(Mod, Host) of + true -> + CacheOpts = cache_opts(Opts), + ets_cache:new(?GROUP_OPTS_CACHE, CacheOpts), + ets_cache:new(?USER_GROUPS_CACHE, CacheOpts), + ets_cache:new(?GROUP_EXPLICIT_USERS_CACHE, CacheOpts); + false -> + ets_cache:delete(?GROUP_OPTS_CACHE), + ets_cache:delete(?USER_GROUPS_CACHE), + ets_cache:delete(?GROUP_EXPLICIT_USERS_CACHE) + end. -get_user_roster(Items, US) -> - {U, S} = US, - DisplayedGroups = get_user_displayed_groups(US), - SRUsers = lists:foldl(fun (Group, Acc1) -> - GroupName = get_group_name(S, Group), - lists:foldl(fun (User, Acc2) -> - if User == US -> Acc2; - true -> - dict:append(User, - GroupName, - Acc2) - end - end, - Acc1, get_group_users(S, Group)) - end, - dict:new(), DisplayedGroups), - {NewItems1, SRUsersRest} = lists:mapfoldl(fun (Item, - SRUsers1) -> - {_, _, {U1, S1, _}} = - Item#roster.usj, - US1 = {U1, S1}, - case dict:find(US1, - SRUsers1) - of - {ok, _GroupNames} -> - {Item#roster{subscription - = - both, - ask = - none}, - dict:erase(US1, - SRUsers1)}; - error -> - {Item, SRUsers1} - end - end, - SRUsers, Items), - ModVcard = get_vcard_module(S), - SRItems = [#roster{usj = {U, S, {U1, S1, <<"">>}}, - us = US, jid = {U1, S1, <<"">>}, - name = get_rosteritem_name(ModVcard, U1, S1), - subscription = both, ask = none, groups = GroupNames} - || {{U1, S1}, GroupNames} <- dict:to_list(SRUsersRest)], +-spec cache_opts(gen_mod:opts()) -> [proplists:property()]. +cache_opts(Opts) -> + MaxSize = mod_shared_roster_opt:cache_size(Opts), + CacheMissed = mod_shared_roster_opt:cache_missed(Opts), + LifeTime = mod_shared_roster_opt:cache_life_time(Opts), + [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}]. + +-spec use_cache(module(), binary()) -> boolean(). +use_cache(Mod, Host) -> + case erlang:function_exported(Mod, use_cache, 1) of + true -> Mod:use_cache(Host); + false -> mod_shared_roster_opt:use_cache(Host) + end. + +-spec cache_nodes(module(), binary()) -> [node()]. +cache_nodes(Mod, Host) -> + case erlang:function_exported(Mod, cache_nodes, 1) of + true -> Mod:cache_nodes(Host); + false -> ejabberd_cluster:get_nodes() + end. + +-spec get_user_roster([#roster_item{}], {binary(), binary()}) -> [#roster_item{}]. +get_user_roster(Items, {_, S} = US) -> + {DisplayedGroups, Cache} = get_user_displayed_groups(US), + SRUsers = lists:foldl( + fun(Group, Acc1) -> + GroupLabel = get_group_label_cached(S, Group, Cache), + lists:foldl( + fun(User, Acc2) -> + if User == US -> Acc2; + true -> + dict:append(User, GroupLabel, Acc2) + end + end, + Acc1, get_group_users_cached(S, Group, Cache)) + end, + dict:new(), DisplayedGroups), + {NewItems1, SRUsersRest} = lists:mapfoldl( + fun(Item = #roster_item{jid = #jid{luser = User1, lserver = Server1}}, SRUsers1) -> + US1 = {User1, Server1}, + case dict:find(US1, SRUsers1) of + {ok, GroupLabels} -> + {Item#roster_item{subscription = both, + groups = Item#roster_item.groups ++ GroupLabels, + ask = undefined}, + dict:erase(US1, SRUsers1)}; + error -> + {Item, SRUsers1} + end + end, + SRUsers, Items), + SRItems = [#roster_item{jid = jid:make(U1, S1), + name = get_rosteritem_name(U1, S1), + subscription = both, ask = undefined, + groups = GroupLabels} + || {{U1, S1}, GroupLabels} <- dict:to_list(SRUsersRest)], SRItems ++ NewItems1. -get_vcard_module(Server) -> - Modules = gen_mod:loaded_modules(Server), - [M - || M <- Modules, - (M == mod_vcard) or (M == mod_vcard_ldap)]. - -get_rosteritem_name([], _, _) -> <<"">>; -get_rosteritem_name([ModVcard], U, S) -> - From = jlib:make_jid(<<"">>, S, jlib:atom_to_binary(?MODULE)), - To = jlib:make_jid(U, S, <<"">>), - case lists:member(To#jid.lserver, ?MYHOSTS) of +get_rosteritem_name(U, S) -> + case gen_mod:is_loaded(S, mod_vcard) of true -> - IQ = {iq, <<"">>, get, <<"vcard-temp">>, <<"">>, - #xmlel{name = <<"vCard">>, - attrs = [{<<"xmlns">>, <<"vcard-temp">>}], - children = []}}, - IQ_Vcard = ModVcard:process_sm_iq(From, To, IQ), - case catch get_rosteritem_name_vcard(IQ_Vcard#iq.sub_el) of - {'EXIT', Err} -> - ?ERROR_MSG("Error found when trying to get the " - "vCard of ~s@~s in ~p:~n ~p", - [U, S, ModVcard, Err]), - <<"">>; - NickName -> - NickName - end; + SubEls = mod_vcard:get_vcard(U, S), + get_rosteritem_name_vcard(SubEls); false -> <<"">> end. -get_rosteritem_name_vcard([]) -> <<"">>; -get_rosteritem_name_vcard([Vcard]) -> - case xml:get_path_s(Vcard, +-spec get_rosteritem_name_vcard([xmlel()]) -> binary(). +get_rosteritem_name_vcard([Vcard|_]) -> + case fxml:get_path_s(Vcard, [{elem, <<"NICKNAME">>}, cdata]) of <<"">> -> - xml:get_path_s(Vcard, [{elem, <<"FN">>}, cdata]); + fxml:get_path_s(Vcard, [{elem, <<"FN">>}, cdata]); Nickname -> Nickname - end. + end; +get_rosteritem_name_vcard(_) -> + <<"">>. %% This function rewrites the roster entries when moving or renaming %% them in the user contact list. +-spec process_item(#roster{}, binary()) -> #roster{}. process_item(RosterItem, Host) -> USFrom = {UserFrom, ServerFrom} = RosterItem#roster.us, {UserTo, ServerTo, ResourceTo} = RosterItem#roster.jid, NameTo = RosterItem#roster.name, USTo = {UserTo, ServerTo}, - DisplayedGroups = get_user_displayed_groups(USFrom), + {DisplayedGroups, Cache} = get_user_displayed_groups(USFrom), CommonGroups = lists:filter(fun (Group) -> is_user_in_group(USTo, Group, Host) end, @@ -233,12 +229,12 @@ process_item(RosterItem, Host) -> [] -> RosterItem; %% Roster item cannot be removed: We simply reset the original groups: _ when RosterItem#roster.subscription == remove -> - GroupNames = lists:map(fun (Group) -> - get_group_name(Host, Group) + GroupLabels = lists:map(fun (Group) -> + get_group_label_cached(Host, Group, Cache) end, CommonGroups), RosterItem#roster{subscription = both, ask = none, - groups = GroupNames}; + groups = GroupLabels}; %% Both users have at least a common shared group, %% So each user can see the other _ -> @@ -248,14 +244,11 @@ process_item(RosterItem, Host) -> %% If it doesn't, then remove this user from any %% existing roster groups. [] -> - mod_roster:out_subscription(UserTo, ServerTo, - jlib:make_jid(UserFrom, ServerFrom, - <<"">>), - unsubscribe), - mod_roster:in_subscription(aaaa, UserFrom, ServerFrom, - jlib:make_jid(UserTo, ServerTo, - <<"">>), - unsubscribe, <<"">>), + Pres = #presence{from = jid:make(UserTo, ServerTo), + to = jid:make(UserFrom, ServerFrom), + type = unsubscribe}, + mod_roster:out_subscription(Pres), + mod_roster:in_subscription(false, Pres), RosterItem#roster{subscription = both, ask = none}; %% If so, it means the user wants to add that contact %% to his personal roster @@ -278,108 +271,91 @@ set_new_rosteritems(UserFrom, ServerFrom, UserTo, RIFrom = build_roster_record(UserFrom, ServerFrom, UserTo, ServerTo, NameTo, GroupsFrom), set_item(UserFrom, ServerFrom, ResourceTo, RIFrom), - JIDTo = jlib:make_jid(UserTo, ServerTo, <<"">>), - JIDFrom = jlib:make_jid(UserFrom, ServerFrom, <<"">>), + JIDTo = jid:make(UserTo, ServerTo), + JIDFrom = jid:make(UserFrom, ServerFrom), RITo = build_roster_record(UserTo, ServerTo, UserFrom, ServerFrom, UserFrom, []), set_item(UserTo, ServerTo, <<"">>, RITo), - mod_roster:out_subscription(UserFrom, ServerFrom, JIDTo, - subscribe), - mod_roster:in_subscription(aaa, UserTo, ServerTo, - JIDFrom, subscribe, <<"">>), - mod_roster:out_subscription(UserTo, ServerTo, JIDFrom, - subscribed), - mod_roster:in_subscription(aaa, UserFrom, ServerFrom, - JIDTo, subscribed, <<"">>), - mod_roster:out_subscription(UserTo, ServerTo, JIDFrom, - subscribe), - mod_roster:in_subscription(aaa, UserFrom, ServerFrom, - JIDTo, subscribe, <<"">>), - mod_roster:out_subscription(UserFrom, ServerFrom, JIDTo, - subscribed), - mod_roster:in_subscription(aaa, UserTo, ServerTo, - JIDFrom, subscribed, <<"">>), + mod_roster:out_subscription( + #presence{from = JIDFrom, to = JIDTo, type = subscribe}), + mod_roster:in_subscription( + false, #presence{to = JIDTo, from = JIDFrom, type = subscribe}), + mod_roster:out_subscription( + #presence{from = JIDTo, to = JIDFrom, type = subscribed}), + mod_roster:in_subscription( + false, #presence{to = JIDFrom, from = JIDTo, type = subscribed}), + mod_roster:out_subscription( + #presence{from = JIDTo, to = JIDFrom, type = subscribe}), + mod_roster:in_subscription( + false, #presence{to = JIDFrom, from = JIDTo, type = subscribe}), + mod_roster:out_subscription( + #presence{from = JIDFrom, to = JIDTo, type = subscribed}), + mod_roster:in_subscription( + false, #presence{to = JIDTo, from = JIDFrom, type = subscribed}), RIFrom. set_item(User, Server, Resource, Item) -> - ResIQ = #iq{type = set, xmlns = ?NS_ROSTER, - id = <<"push", (randoms:get_string())/binary>>, - sub_el = - [#xmlel{name = <<"query">>, - attrs = [{<<"xmlns">>, ?NS_ROSTER}], - children = [mod_roster:item_to_xml(Item)]}]}, - ejabberd_router:route(jlib:make_jid(User, Server, - Resource), - jlib:make_jid(<<"">>, Server, <<"">>), - jlib:iq_to_xml(ResIQ)). + ResIQ = #iq{from = jid:make(User, Server, Resource), + to = jid:make(Server), + type = set, id = <<"push", (p1_rand:get_string())/binary>>, + sub_els = [#roster_query{ + items = [mod_roster:encode_item(Item)]}]}, + ejabberd_router:route(ResIQ). -get_subscription_lists({F, T}, User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - US = {LUser, LServer}, - DisplayedGroups = get_user_displayed_groups(US), - SRUsers = lists:usort(lists:flatmap(fun (Group) -> - get_group_users(LServer, Group) - end, - DisplayedGroups)), - SRJIDs = [{U1, S1, <<"">>} || {U1, S1} <- SRUsers], - {lists:usort(SRJIDs ++ F), lists:usort(SRJIDs ++ T)}. - -get_jid_info({Subscription, Groups}, User, Server, +-spec get_jid_info({subscription(), ask(), [binary()]}, binary(), binary(), jid()) + -> {subscription(), ask(), [binary()]}. +get_jid_info({Subscription, Ask, Groups}, User, Server, JID) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), US = {LUser, LServer}, - {U1, S1, _} = jlib:jid_tolower(JID), + {U1, S1, _} = jid:tolower(JID), US1 = {U1, S1}, - DisplayedGroups = get_user_displayed_groups(US), - SRUsers = lists:foldl(fun (Group, Acc1) -> - lists:foldl(fun (User1, Acc2) -> - dict:append(User1, - get_group_name(LServer, - Group), - Acc2) - end, - Acc1, - get_group_users(LServer, Group)) - end, - dict:new(), DisplayedGroups), + {DisplayedGroups, Cache} = get_user_displayed_groups(US), + SRUsers = lists:foldl( + fun(Group, Acc1) -> + GroupLabel = get_group_label_cached(LServer, Group, Cache), %++ + lists:foldl( + fun(User1, Acc2) -> + dict:append(User1, GroupLabel, Acc2) + end, Acc1, get_group_users_cached(LServer, Group, Cache)) + end, + dict:new(), DisplayedGroups), case dict:find(US1, SRUsers) of - {ok, GroupNames} -> - NewGroups = if Groups == [] -> GroupNames; + {ok, GroupLabels} -> + NewGroups = if Groups == [] -> GroupLabels; true -> Groups end, - {both, NewGroups}; - error -> {Subscription, Groups} + {both, none, NewGroups}; + error -> {Subscription, Ask, Groups} end. -in_subscription(Acc, User, Server, JID, Type, - _Reason) -> +-spec in_subscription(boolean(), presence()) -> boolean(). +in_subscription(Acc, #presence{to = To, from = JID, type = Type}) -> + #jid{user = User, server = Server} = To, process_subscription(in, User, Server, JID, Type, Acc). -out_subscription(UserFrom, ServerFrom, JIDTo, - unsubscribed) -> - #jid{luser = UserTo, lserver = ServerTo} = JIDTo, - JIDFrom = jlib:make_jid(UserFrom, ServerFrom, <<"">>), - mod_roster:out_subscription(UserTo, ServerTo, JIDFrom, - unsubscribe), - mod_roster:in_subscription(aaaa, UserFrom, ServerFrom, - JIDTo, unsubscribe, <<"">>), - process_subscription(out, UserFrom, ServerFrom, JIDTo, - unsubscribed, false); -out_subscription(User, Server, JID, Type) -> - process_subscription(out, User, Server, JID, Type, - false). +-spec out_subscription(presence()) -> boolean(). +out_subscription(#presence{from = From, to = To, type = unsubscribed} = Pres) -> + #jid{user = User, server = Server} = From, + mod_roster:out_subscription(Pres#presence{type = unsubscribe}), + mod_roster:in_subscription(false, xmpp:set_from_to( + Pres#presence{type = unsubscribe}, + To, From)), + process_subscription(out, User, Server, To, unsubscribed, false); +out_subscription(#presence{from = From, to = To, type = Type}) -> + #jid{user = User, server = Server} = From, + process_subscription(out, User, Server, To, Type, false). process_subscription(Direction, User, Server, JID, _Type, Acc) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), US = {LUser, LServer}, {U1, S1, _} = - jlib:jid_tolower(jlib:jid_remove_resource(JID)), + jid:tolower(jid:remove_resource(JID)), US1 = {U1, S1}, - DisplayedGroups = get_user_displayed_groups(US), + {DisplayedGroups, _} = get_user_displayed_groups(US), SRUsers = lists:usort(lists:flatmap(fun (Group) -> get_group_users(LServer, Group) end, @@ -394,235 +370,154 @@ process_subscription(Direction, User, Server, JID, end. list_groups(Host) -> - list_groups(Host, gen_mod:db_type(Host, ?MODULE)). - -list_groups(Host, mnesia) -> - mnesia:dirty_select(sr_group, - [{#sr_group{group_host = {'$1', '$2'}, _ = '_'}, - [{'==', '$2', Host}], ['$1']}]); -list_groups(Host, riak) -> - case ejabberd_riak:get_keys_by_index(sr_group, <<"host">>, Host) of - {ok, Gs} -> - [G || {G, _} <- Gs]; - _ -> - [] - end; -list_groups(Host, odbc) -> - case ejabberd_odbc:sql_query(Host, - [<<"select name from sr_group;">>]) - of - {selected, [<<"name">>], Rs} -> [G || [G] <- Rs]; - _ -> [] - end. + Mod = gen_mod:db_mod(Host, ?MODULE), + Mod:list_groups(Host). groups_with_opts(Host) -> - groups_with_opts(Host, gen_mod:db_type(Host, ?MODULE)). - -groups_with_opts(Host, mnesia) -> - Gs = mnesia:dirty_select(sr_group, - [{#sr_group{group_host = {'$1', Host}, opts = '$2', - _ = '_'}, - [], [['$1', '$2']]}]), - lists:map(fun ([G, O]) -> {G, O} end, Gs); -groups_with_opts(Host, riak) -> - case ejabberd_riak:get_by_index(sr_group, sr_group_schema(), - <<"host">>, Host) of - {ok, Rs} -> - [{G, O} || #sr_group{group_host = {G, _}, opts = O} <- Rs]; - _ -> - [] - end; -groups_with_opts(Host, odbc) -> - case ejabberd_odbc:sql_query(Host, - [<<"select name, opts from sr_group;">>]) - of - {selected, [<<"name">>, <<"opts">>], Rs} -> - [{G, opts_to_binary(ejabberd_odbc:decode_term(Opts))} - || [G, Opts] <- Rs]; - _ -> [] - end. + Mod = gen_mod:db_mod(Host, ?MODULE), + Mod:groups_with_opts(Host). create_group(Host, Group) -> create_group(Host, Group, []). create_group(Host, Group, Opts) -> - create_group(Host, Group, Opts, - gen_mod:db_type(Host, ?MODULE)). - -create_group(Host, Group, Opts, mnesia) -> - R = #sr_group{group_host = {Group, Host}, opts = Opts}, - F = fun () -> mnesia:write(R) end, - mnesia:transaction(F); -create_group(Host, Group, Opts, riak) -> - {atomic, ejabberd_riak:put(#sr_group{group_host = {Group, Host}, - opts = Opts}, - sr_group_schema(), - [{'2i', [{<<"host">>, Host}]}])}; -create_group(Host, Group, Opts, odbc) -> - SGroup = ejabberd_odbc:escape(Group), - SOpts = ejabberd_odbc:encode_term(Opts), - F = fun () -> - odbc_queries:update_t(<<"sr_group">>, - [<<"name">>, <<"opts">>], [SGroup, SOpts], - [<<"name='">>, SGroup, <<"'">>]) - end, - ejabberd_odbc:sql_transaction(Host, F). - -delete_group(Host, Group) -> - delete_group(Host, Group, - gen_mod:db_type(Host, ?MODULE)). - -delete_group(Host, Group, mnesia) -> - GroupHost = {Group, Host}, - F = fun () -> - mnesia:delete({sr_group, GroupHost}), - Users = mnesia:index_read(sr_user, GroupHost, - #sr_user.group_host), - lists:foreach(fun (UserEntry) -> - mnesia:delete_object(UserEntry) - end, - Users) - end, - mnesia:transaction(F); -delete_group(Host, Group, riak) -> - try - ok = ejabberd_riak:delete(sr_group, {Group, Host}), - ok = ejabberd_riak:delete_by_index(sr_user, <<"group_host">>, - {Group, Host}), - {atomic, ok} - catch _:{badmatch, Err} -> - {atomic, Err} - end; -delete_group(Host, Group, odbc) -> - SGroup = ejabberd_odbc:escape(Group), - F = fun () -> - ejabberd_odbc:sql_query_t([<<"delete from sr_group where name='">>, - SGroup, <<"';">>]), - ejabberd_odbc:sql_query_t([<<"delete from sr_user where grp='">>, - SGroup, <<"';">>]) - end, - case ejabberd_odbc:sql_transaction(Host, F) of - {atomic,{updated,_}} -> {atomic, ok}; - Res -> Res + case jid:nameprep(Group) of + error -> + {error, invalid_group_name}; + LGroup -> + case jid:nameprep(Host) of + error -> + {error, invalid_group_host}; + LHost -> + create_group2(LHost, LGroup, Opts) + end end. -get_group_opts(Host, Group) -> - get_group_opts(Host, Group, - gen_mod:db_type(Host, ?MODULE)). +create_group2(Host, Group, Opts) -> + Mod = gen_mod:db_mod(Host, ?MODULE), + case proplists:get_value(all_users, Opts, false) orelse + proplists:get_value(online_users, Opts, false) of + true -> + update_wildcard_cache(Host, Group, Opts); + _ -> + ok + end, + case use_cache(Mod, Host) of + true -> + ets_cache:delete(?GROUP_OPTS_CACHE, {Host, Group}, cache_nodes(Mod, Host)), + ets_cache:insert(?GROUP_OPTS_CACHE, {Host, Group}, Opts, cache_nodes(Mod, Host)); + _ -> + ok + end, + Mod:create_group(Host, Group, Opts). -get_group_opts(Host, Group, mnesia) -> - case catch mnesia:dirty_read(sr_group, {Group, Host}) of - [#sr_group{opts = Opts}] -> Opts; - _ -> error - end; -get_group_opts(Host, Group, riak) -> - case ejabberd_riak:get(sr_group, sr_group_schema(), {Group, Host}) of - {ok, #sr_group{opts = Opts}} -> Opts; - _ -> error - end; -get_group_opts(Host, Group, odbc) -> - SGroup = ejabberd_odbc:escape(Group), - case catch ejabberd_odbc:sql_query(Host, - [<<"select opts from sr_group where name='">>, - SGroup, <<"';">>]) - of - {selected, [<<"opts">>], [[SOpts]]} -> - opts_to_binary(ejabberd_odbc:decode_term(SOpts)); - _ -> error +delete_group(Host, Group) -> + Mod = gen_mod:db_mod(Host, ?MODULE), + update_wildcard_cache(Host, Group, []), + case use_cache(Mod, Host) of + true -> + ets_cache:delete(?GROUP_OPTS_CACHE, {Host, Group}, cache_nodes(Mod, Host)), + ets_cache:clear(?USER_GROUPS_CACHE, cache_nodes(Mod, Host)), + ets_cache:delete(?GROUP_EXPLICIT_USERS_CACHE, {Host, Group}, cache_nodes(Mod, Host)); + _ -> + ok + end, + Mod:delete_group(Host, Group). + +get_groups_opts_cached(Host1, Group1, Cache) -> + {Host, Group} = split_grouphost(Host1, Group1), + Key = {Group, Host}, + case Cache of + #{Key := Opts} -> + {Opts, Cache}; + _ -> + Opts = get_group_opts_int(Host, Group), + {Opts, Cache#{Key => Opts}} + end. + +get_group_opts(Host1, Group1) -> + {Host, Group} = split_grouphost(Host1, Group1), + get_group_opts_int(Host, Group). + +get_group_opts_int(Host, Group) -> + Mod = gen_mod:db_mod(Host, ?MODULE), + Res = case use_cache(Mod, Host) of + true -> + ets_cache:lookup( + ?GROUP_OPTS_CACHE, {Host, Group}, + fun() -> + case Mod:get_group_opts(Host, Group) of + error -> error; + V -> {cache, V} + end + end); + false -> + Mod:get_group_opts(Host, Group) + end, + case Res of + {ok, Opts} -> Opts; + error -> error end. set_group_opts(Host, Group, Opts) -> - set_group_opts(Host, Group, Opts, - gen_mod:db_type(Host, ?MODULE)). - -set_group_opts(Host, Group, Opts, mnesia) -> - R = #sr_group{group_host = {Group, Host}, opts = Opts}, - F = fun () -> mnesia:write(R) end, - mnesia:transaction(F); -set_group_opts(Host, Group, Opts, riak) -> - {atomic, ejabberd_riak:put(#sr_group{group_host = {Group, Host}, - opts = Opts}, - sr_group_schema(), - [{'2i', [{<<"host">>, Host}]}])}; -set_group_opts(Host, Group, Opts, odbc) -> - SGroup = ejabberd_odbc:escape(Group), - SOpts = ejabberd_odbc:encode_term(Opts), - F = fun () -> - odbc_queries:update_t(<<"sr_group">>, - [<<"name">>, <<"opts">>], [SGroup, SOpts], - [<<"name='">>, SGroup, <<"'">>]) - end, - ejabberd_odbc:sql_transaction(Host, F). + Mod = gen_mod:db_mod(Host, ?MODULE), + update_wildcard_cache(Host, Group, Opts), + case use_cache(Mod, Host) of + true -> + ets_cache:delete(?GROUP_OPTS_CACHE, {Host, Group}, cache_nodes(Mod, Host)), + ets_cache:insert(?GROUP_OPTS_CACHE, {Host, Group}, Opts, cache_nodes(Mod, Host)); + _ -> + ok + end, + Mod:set_group_opts(Host, Group, Opts). get_user_groups(US) -> Host = element(2, US), - DBType = gen_mod:db_type(Host, ?MODULE), - get_user_groups(US, Host, DBType) ++ - get_special_users_groups(Host). + Mod = gen_mod:db_mod(Host, ?MODULE), + UG = case use_cache(Mod, Host) of + true -> + ets_cache:lookup( + ?USER_GROUPS_CACHE, {Host, US}, + fun() -> + {cache, Mod:get_user_groups(US, Host)} + end); + false -> + Mod:get_user_groups(US, Host) + end, + UG ++ get_groups_with_wildcards(Host, both). -get_user_groups(US, Host, mnesia) -> - case catch mnesia:dirty_read(sr_user, US) of - Rs when is_list(Rs) -> - [Group - || #sr_user{group_host = {Group, H}} <- Rs, H == Host]; - _ -> [] - end; -get_user_groups(US, Host, riak) -> - case ejabberd_riak:get_by_index(sr_user, sr_user_schema(), <<"us">>, US) of - {ok, Rs} -> - [Group || #sr_user{group_host = {Group, H}} <- Rs, H == Host]; - _ -> - [] - end; -get_user_groups(US, Host, odbc) -> - SJID = make_jid_s(US), - case catch ejabberd_odbc:sql_query(Host, - [<<"select grp from sr_user where jid='">>, - SJID, <<"';">>]) - of - {selected, [<<"grp">>], Rs} -> [G || [G] <- Rs]; - _ -> [] +get_group_opt_cached(Host, Group, Opt, Default, Cache) -> + case get_groups_opts_cached(Host, Group, Cache) of + {error, _} -> Default; + {Opts, _} -> + proplists:get_value(Opt, Opts, Default) end. -is_group_enabled(Host1, Group1) -> - {Host, Group} = split_grouphost(Host1, Group1), - case get_group_opts(Host, Group) of - error -> false; - Opts -> not lists:member(disabled, Opts) - end. - -%% @spec (Host::string(), Group::string(), Opt::atom(), Default) -> OptValue | Default +-spec get_group_opt(Host::binary(), Group::binary(), displayed_groups | label, Default) -> + OptValue::any() | Default. get_group_opt(Host, Group, Opt, Default) -> case get_group_opts(Host, Group) of error -> Default; Opts -> - case lists:keysearch(Opt, 1, Opts) of - {value, {_, Val}} -> Val; - false -> Default - end + proplists:get_value(Opt, Opts, Default) end. get_online_users(Host) -> lists:usort([{U, S} || {U, S, _} <- ejabberd_sm:get_vh_session_list(Host)]). +get_group_users_cached(Host1, Group1, Cache) -> + {Host, Group} = split_grouphost(Host1, Group1), + {Opts, _} = get_groups_opts_cached(Host, Group, Cache), + get_group_users(Host, Group, Opts). + get_group_users(Host1, Group1) -> {Host, Group} = split_grouphost(Host1, Group1), - case get_group_opt(Host, Group, all_users, false) of - true -> ejabberd_auth:get_vh_registered_users(Host); - false -> [] - end - ++ - case get_group_opt(Host, Group, online_users, false) of - true -> get_online_users(Host); - false -> [] - end - ++ get_group_explicit_users(Host, Group). + get_group_users(Host, Group, get_group_opts(Host, Group)). get_group_users(Host, Group, GroupOpts) -> case proplists:get_value(all_users, GroupOpts, false) of -%% @spec (Host::string(), Group::string()) -> [{User::string(), Server::string()}] - true -> ejabberd_auth:get_vh_registered_users(Host); + true -> ejabberd_auth:get_users(Host); false -> [] end ++ @@ -634,83 +529,90 @@ get_group_users(Host, Group, GroupOpts) -> ++ get_group_explicit_users(Host, Group). get_group_explicit_users(Host, Group) -> - get_group_explicit_users(Host, Group, - gen_mod:db_type(Host, ?MODULE)). - -get_group_explicit_users(Host, Group, mnesia) -> - Read = (catch mnesia:dirty_index_read(sr_user, - {Group, Host}, #sr_user.group_host)), - case Read of - Rs when is_list(Rs) -> [R#sr_user.us || R <- Rs]; - _ -> [] - end; -get_group_explicit_users(Host, Group, riak) -> - case ejabberd_riak:get_by_index(sr_user, sr_user_schema(), - <<"group_host">>, {Group, Host}) of - {ok, Rs} -> - [R#sr_user.us || R <- Rs]; - _ -> - [] - end; -get_group_explicit_users(Host, Group, odbc) -> - SGroup = ejabberd_odbc:escape(Group), - case catch ejabberd_odbc:sql_query(Host, - [<<"select jid from sr_user where grp='">>, - SGroup, <<"';">>]) - of - {selected, [<<"jid">>], Rs} -> - lists:map(fun ([JID]) -> - {U, S, _} = - jlib:jid_tolower(jlib:string_to_jid(JID)), - {U, S} - end, - Rs); - _ -> [] + Mod = gen_mod:db_mod(Host, ?MODULE), + case use_cache(Mod, Host) of + true -> + ets_cache:lookup( + ?GROUP_EXPLICIT_USERS_CACHE, {Host, Group}, + fun() -> + {cache, Mod:get_group_explicit_users(Host, Group)} + end); + false -> + Mod:get_group_explicit_users(Host, Group) end. -get_group_name(Host1, Group1) -> - {Host, Group} = split_grouphost(Host1, Group1), - get_group_opt(Host, Group, name, Group). +get_group_label_cached(Host, Group, Cache) -> + get_group_opt_cached(Host, Group, label, Group, Cache). -%% Get list of names of groups that have @all@/@online@/etc in the memberlist -get_special_users_groups(Host) -> -%% Get list of names of groups that have @online@ in the memberlist - lists:filter(fun (Group) -> - get_group_opt(Host, Group, all_users, false) orelse - get_group_opt(Host, Group, online_users, false) - end, - list_groups(Host)). +-spec update_wildcard_cache(binary(), binary(), list()) -> ok. +update_wildcard_cache(Host, Group, NewOpts) -> + Mod = gen_mod:db_mod(Host, ?MODULE), + Online = get_groups_with_wildcards(Host, online), + Both = get_groups_with_wildcards(Host, both), + IsOnline = proplists:get_value(online_users, NewOpts, false), + IsAll = proplists:get_value(all_users, NewOpts, false), + + OnlineUpdated = lists:member(Group, Online) /= IsOnline, + BothUpdated = lists:member(Group, Both) /= (IsOnline orelse IsAll), + + if + OnlineUpdated -> + NewOnline = case IsOnline of + true -> [Group | Online]; + _ -> Online -- [Group] + end, + ets_cache:update(?SPECIAL_GROUPS_CACHE, {Host, online}, + {ok, NewOnline}, fun() -> ok end, cache_nodes(Mod, Host)); + true -> ok + end, + if + BothUpdated -> + NewBoth = case IsOnline orelse IsAll of + true -> [Group | Both]; + _ -> Both -- [Group] + end, + ets_cache:update(?SPECIAL_GROUPS_CACHE, {Host, both}, + {ok, NewBoth}, fun() -> ok end, cache_nodes(Mod, Host)); + true -> ok + end, + ok. + +-spec get_groups_with_wildcards(binary(), online | both) -> list(binary()). +get_groups_with_wildcards(Host, Type) -> + Res = + ets_cache:lookup( + ?SPECIAL_GROUPS_CACHE, {Host, Type}, + fun() -> + Res = lists:filtermap( + fun({Group, Opts}) -> + case proplists:get_value(online_users, Opts, false) orelse + (Type == both andalso proplists:get_value(all_users, Opts, false)) of + true -> {true, Group}; + false -> false + end + end, + groups_with_opts(Host)), + {cache, {ok, Res}} + end), + case Res of + {ok, List} -> List; + _ -> [] + end. -get_special_users_groups_online(Host) -> %% Given two lists of groupnames and their options, %% return the list of displayed groups to the second list - lists:filter(fun (Group) -> - get_group_opt(Host, Group, online_users, false) - end, - list_groups(Host)). - displayed_groups(GroupsOpts, SelectedGroupsOpts) -> + DisplayedGroups = lists:usort(lists:flatmap( + fun + ({_Group, Opts}) -> + [G || G <- proplists:get_value(displayed_groups, Opts, []), + not lists:member(disabled, Opts)] + end, SelectedGroupsOpts)), + [G || G <- DisplayedGroups, not lists:member(disabled, proplists:get_value(G, GroupsOpts, []))]. + %% Given a list of group names with options, %% for those that have @all@ in memberlist, %% get the list of groups displayed - DisplayedGroups = lists:usort(lists:flatmap(fun - ({_Group, Opts}) -> - [G - || G - <- proplists:get_value(displayed_groups, - Opts, - []), - not - lists:member(disabled, - Opts)] - end, - SelectedGroupsOpts)), - [G - || G <- DisplayedGroups, - not - lists:member(disabled, - proplists:get_value(G, GroupsOpts, []))]. - get_special_displayed_groups(GroupsOpts) -> Groups = lists:filter(fun ({_Group, Opts}) -> proplists:get_value(all_users, Opts, false) @@ -722,159 +624,92 @@ get_special_displayed_groups(GroupsOpts) -> %% for the list of groups of that server that user is member %% get the list of groups displayed get_user_displayed_groups(LUser, LServer, GroupsOpts) -> - Groups = get_user_displayed_groups(LUser, LServer, - GroupsOpts, - gen_mod:db_type(LServer, ?MODULE)), + Mod = gen_mod:db_mod(LServer, ?MODULE), + Groups = Mod:get_user_displayed_groups(LUser, LServer, GroupsOpts), displayed_groups(GroupsOpts, Groups). -get_user_displayed_groups(LUser, LServer, GroupsOpts, - mnesia) -> - case catch mnesia:dirty_read(sr_user, {LUser, LServer}) - of - Rs when is_list(Rs) -> - [{Group, proplists:get_value(Group, GroupsOpts, [])} - || #sr_user{group_host = {Group, H}} <- Rs, - H == LServer]; - _ -> [] - end; -get_user_displayed_groups(LUser, LServer, GroupsOpts, - riak) -> - case ejabberd_riak:get_by_index(sr_user, sr_user_schema(), - <<"us">>, {LUser, LServer}) of - {ok, Rs} -> - [{Group, proplists:get_value(Group, GroupsOpts, [])} - || #sr_user{group_host = {Group, _}} <- Rs]; - _ -> - [] - end; -get_user_displayed_groups(LUser, LServer, GroupsOpts, - odbc) -> - SJID = make_jid_s(LUser, LServer), - case catch ejabberd_odbc:sql_query(LServer, - [<<"select grp from sr_user where jid='">>, - SJID, <<"';">>]) - of - {selected, [<<"grp">>], Rs} -> - [{Group, proplists:get_value(Group, GroupsOpts, [])} - || [Group] <- Rs]; - _ -> [] - end. - %% @doc Get the list of groups that are displayed to this user get_user_displayed_groups(US) -> Host = element(2, US), - DisplayedGroups1 = lists:usort(lists:flatmap(fun - (Group) -> - case - is_group_enabled(Host, - Group) - of - true -> - get_group_opt(Host, - Group, - displayed_groups, - []); - false -> [] - end - end, - get_user_groups(US))), - [Group - || Group <- DisplayedGroups1, - is_group_enabled(Host, Group)]. + {Groups, Cache} = + lists:foldl( + fun(Group, {Groups, Cache}) -> + case get_groups_opts_cached(Host, Group, Cache) of + {error, Cache2} -> + {Groups, Cache2}; + {Opts, Cache3} -> + case lists:member(disabled, Opts) of + false -> + {proplists:get_value(displayed_groups, Opts, []) ++ Groups, Cache3}; + _ -> + {Groups, Cache3} + end + end + end, {[], #{}}, get_user_groups(US)), + lists:foldl( + fun(Group, {Groups0, Cache0}) -> + case get_groups_opts_cached(Host, Group, Cache0) of + {error, Cache1} -> + {Groups0, Cache1}; + {Opts, Cache2} -> + case lists:member(disabled, Opts) of + false -> + {[Group|Groups0], Cache2}; + _ -> + {Groups0, Cache2} + end + end + end, {[], Cache}, lists:usort(Groups)). is_user_in_group(US, Group, Host) -> - is_user_in_group(US, Group, Host, - gen_mod:db_type(Host, ?MODULE)). - -is_user_in_group(US, Group, Host, mnesia) -> - case catch mnesia:dirty_match_object(#sr_user{us = US, - group_host = {Group, Host}}) - of - [] -> lists:member(US, get_group_users(Host, Group)); - _ -> true - end; -is_user_in_group(US, Group, Host, riak) -> - case ejabberd_riak:get_by_index(sr_user, sr_user_schema(), <<"us">>, US) of - {ok, Rs} -> - case lists:any( - fun(#sr_user{group_host = {G, H}}) -> - (Group == G) and (Host == H) - end, Rs) of - false -> - lists:member(US, get_group_users(Host, Group)); - true -> - true - end; - _Err -> - false - end; -is_user_in_group(US, Group, Host, odbc) -> - SJID = make_jid_s(US), - SGroup = ejabberd_odbc:escape(Group), - case catch ejabberd_odbc:sql_query(Host, - [<<"select * from sr_user where jid='">>, - SJID, <<"' and grp='">>, SGroup, - <<"';">>]) - of - {selected, _, []} -> - lists:member(US, get_group_users(Host, Group)); - _ -> true + Mod = gen_mod:db_mod(Host, ?MODULE), + case Mod:is_user_in_group(US, Group, Host) of + false -> + lists:member(US, get_group_users(Host, Group)); + true -> + true end. -%% @spec (Host::string(), {User::string(), Server::string()}, Group::string()) -> {atomic, ok} +-spec add_user_to_group(Host::binary(), {User::binary(), Server::binary()}, + Group::binary()) -> {atomic, ok} | error. add_user_to_group(Host, US, Group) -> + {_LUser, LServer} = US, + case lists:member(LServer, ejabberd_config:get_option(hosts)) of + true -> add_user_to_group2(Host, US, Group); + false -> + ?INFO_MSG("Attempted adding to shared roster user of inexistent vhost ~ts", [LServer]), + error + end. +add_user_to_group2(Host, US, Group) -> {LUser, LServer} = US, - case ejabberd_regexp:run(LUser, <<"^@.+@$">>) of + case ejabberd_regexp:run(LUser, <<"^@.+@\$">>) of match -> - GroupOpts = (?MODULE):get_group_opts(Host, Group), + GroupOpts = get_group_opts(Host, Group), MoreGroupOpts = case LUser of <<"@all@">> -> [{all_users, true}]; <<"@online@">> -> [{online_users, true}]; _ -> [] end, - (?MODULE):set_group_opts(Host, Group, - GroupOpts ++ MoreGroupOpts); + set_group_opts(Host, Group, + GroupOpts ++ MoreGroupOpts); nomatch -> DisplayedToGroups = displayed_to_groups(Group, Host), DisplayedGroups = get_displayed_groups(Group, LServer), push_user_to_displayed(LUser, LServer, Group, Host, both, DisplayedToGroups), push_displayed_to_user(LUser, LServer, Host, both, DisplayedGroups), - broadcast_user_to_displayed(LUser, LServer, Host, both, DisplayedToGroups), - broadcast_displayed_to_user(LUser, LServer, Host, both, DisplayedGroups), - add_user_to_group(Host, US, Group, gen_mod:db_type(Host, ?MODULE)) + Mod = gen_mod:db_mod(Host, ?MODULE), + Mod:add_user_to_group(Host, US, Group), + case use_cache(Mod, Host) of + true -> + ets_cache:delete(?USER_GROUPS_CACHE, {Host, US}, cache_nodes(Mod, Host)), + ets_cache:delete(?GROUP_EXPLICIT_USERS_CACHE, {Host, Group}, cache_nodes(Mod, Host)); + false -> + ok + end end. -add_user_to_group(Host, US, Group, mnesia) -> - R = #sr_user{us = US, group_host = {Group, Host}}, - F = fun () -> mnesia:write(R) end, - mnesia:transaction(F); -add_user_to_group(Host, US, Group, riak) -> - {atomic, ejabberd_riak:put( - #sr_user{us = US, group_host = {Group, Host}}, - sr_user_schema(), - [{i, {US, {Group, Host}}}, - {'2i', [{<<"us">>, US}, - {<<"group_host">>, {Group, Host}}]}])}; -add_user_to_group(Host, US, Group, odbc) -> - SJID = make_jid_s(US), - SGroup = ejabberd_odbc:escape(Group), - F = fun () -> - odbc_queries:update_t(<<"sr_user">>, - [<<"jid">>, <<"grp">>], [SJID, SGroup], - [<<"jid='">>, SJID, <<"' and grp='">>, - SGroup, <<"'">>]) - end, - ejabberd_odbc:sql_transaction(Host, F). - get_displayed_groups(Group, LServer) -> - GroupsOpts = groups_with_opts(LServer), - GroupOpts = proplists:get_value(Group, GroupsOpts, []), - proplists:get_value(displayed_groups, GroupOpts, []). - -broadcast_displayed_to_user(LUser, LServer, Host, Subscription, DisplayedGroups) -> - [broadcast_members_to_user(LUser, LServer, DGroup, Host, - Subscription) - || DGroup <- DisplayedGroups]. + get_group_opt(LServer, Group, displayed_groups, []). push_displayed_to_user(LUser, LServer, Host, Subscription, DisplayedGroups) -> [push_members_to_user(LUser, LServer, DGroup, Host, @@ -883,9 +718,9 @@ push_displayed_to_user(LUser, LServer, Host, Subscription, DisplayedGroups) -> remove_user_from_group(Host, US, Group) -> {LUser, LServer} = US, - case ejabberd_regexp:run(LUser, <<"^@.+@$">>) of + case ejabberd_regexp:run(LUser, <<"^@.+@\$">>) of match -> - GroupOpts = (?MODULE):get_group_opts(Host, Group), + GroupOpts = get_group_opts(Host, Group), NewGroupOpts = case LUser of <<"@all@">> -> lists:filter(fun (X) -> X /= {all_users, true} @@ -896,10 +731,17 @@ remove_user_from_group(Host, US, Group) -> end, GroupOpts) end, - (?MODULE):set_group_opts(Host, Group, NewGroupOpts); + set_group_opts(Host, Group, NewGroupOpts); nomatch -> - Result = remove_user_from_group(Host, US, Group, - gen_mod:db_type(Host, ?MODULE)), + Mod = gen_mod:db_mod(Host, ?MODULE), + Result = Mod:remove_user_from_group(Host, US, Group), + case use_cache(Mod, Host) of + true -> + ets_cache:delete(?USER_GROUPS_CACHE, {Host, US}, cache_nodes(Mod, Host)), + ets_cache:delete(?GROUP_EXPLICIT_USERS_CACHE, {Host, Group}, cache_nodes(Mod, Host)); + false -> + ok + end, DisplayedToGroups = displayed_to_groups(Group, Host), DisplayedGroups = get_displayed_groups(Group, LServer), push_user_to_displayed(LUser, LServer, Group, Host, remove, DisplayedToGroups), @@ -907,54 +749,34 @@ remove_user_from_group(Host, US, Group) -> Result end. -remove_user_from_group(Host, US, Group, mnesia) -> - R = #sr_user{us = US, group_host = {Group, Host}}, - F = fun () -> mnesia:delete_object(R) end, - mnesia:transaction(F); -remove_user_from_group(Host, US, Group, riak) -> - {atomic, ejabberd_riak:delete(sr_group, {US, {Group, Host}})}; -remove_user_from_group(Host, US, Group, odbc) -> - SJID = make_jid_s(US), - SGroup = ejabberd_odbc:escape(Group), - F = fun () -> - ejabberd_odbc:sql_query_t([<<"delete from sr_user where jid='">>, - SJID, <<"' and grp='">>, SGroup, - <<"';">>]), - ok - end, - ejabberd_odbc:sql_transaction(Host, F). - push_members_to_user(LUser, LServer, Group, Host, Subscription) -> - GroupsOpts = groups_with_opts(LServer), - GroupOpts = proplists:get_value(Group, GroupsOpts, []), - GroupName = proplists:get_value(name, GroupOpts, Group), + GroupOpts = get_group_opts(LServer, Group), + GroupLabel = proplists:get_value(label, GroupOpts, Group), %++ Members = get_group_users(Host, Group), lists:foreach(fun ({U, S}) -> - push_roster_item(LUser, LServer, U, S, GroupName, + N = get_rosteritem_name(U, S), + push_roster_item(LUser, LServer, U, S, N, GroupLabel, Subscription) end, Members). -broadcast_members_to_user(LUser, LServer, Group, Host, Subscription) -> - Members = get_group_users(Host, Group), - lists:foreach( - fun({U, S}) -> - broadcast_subscription(U, S, {LUser, LServer, <<"">>}, Subscription) - end, Members). - +-spec register_user(binary(), binary()) -> ok. register_user(User, Server) -> Groups = get_user_groups({User, Server}), [push_user_to_displayed(User, Server, Group, Server, both, displayed_to_groups(Group, Server)) - || Group <- Groups]. + || Group <- Groups], + ok. +-spec remove_user(binary(), binary()) -> ok. remove_user(User, Server) -> push_user_to_members(User, Server, remove). push_user_to_members(User, Server, Subscription) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + RosterName = get_rosteritem_name(LUser, LServer), GroupsOpts = groups_with_opts(LServer), SpecialGroups = get_special_displayed_groups(GroupsOpts), @@ -965,12 +787,13 @@ push_user_to_members(User, Server, Subscription) -> Group), GroupOpts = proplists:get_value(Group, GroupsOpts, []), - GroupName = proplists:get_value(name, GroupOpts, + GroupLabel = proplists:get_value(label, GroupOpts, Group), lists:foreach(fun ({U, S}) -> push_roster_item(U, S, LUser, LServer, - GroupName, + RosterName, + GroupLabel, Subscription) end, get_group_users(LServer, Group, @@ -979,389 +802,476 @@ push_user_to_members(User, Server, Subscription) -> lists:usort(SpecialGroups ++ UserGroups)). push_user_to_displayed(LUser, LServer, Group, Host, Subscription, DisplayedToGroupsOpts) -> - GroupsOpts = groups_with_opts(Host), - GroupOpts = proplists:get_value(Group, GroupsOpts, []), - GroupName = proplists:get_value(name, GroupOpts, Group), + GroupLabel = get_group_opt(Host, Group, label, Group), %++ [push_user_to_group(LUser, LServer, GroupD, Host, - GroupName, Subscription) - || {GroupD, _Opts} <- DisplayedToGroupsOpts]. - -broadcast_user_to_displayed(LUser, LServer, Host, Subscription, DisplayedToGroupsOpts) -> - [broadcast_user_to_group(LUser, LServer, GroupD, Host, Subscription) - || {GroupD, _Opts} <- DisplayedToGroupsOpts]. + GroupLabel, Subscription) + || GroupD <- DisplayedToGroupsOpts]. push_user_to_group(LUser, LServer, Group, Host, - GroupName, Subscription) -> + GroupLabel, Subscription) -> + RosterName = get_rosteritem_name(LUser, LServer), lists:foreach(fun ({U, S}) when (U == LUser) and (S == LServer) -> ok; ({U, S}) -> - push_roster_item(U, S, LUser, LServer, GroupName, - Subscription) + case lists:member(S, ejabberd_option:hosts()) of + true -> + push_roster_item(U, S, LUser, LServer, RosterName, GroupLabel, + Subscription); + _ -> + ok + end end, get_group_users(Host, Group)). -broadcast_user_to_group(LUser, LServer, Group, Host, Subscription) -> - lists:foreach( - fun({U, S}) when (U == LUser) and (S == LServer) -> ok; - ({U, S}) -> - broadcast_subscription(LUser, LServer, {U, S, <<"">>}, Subscription) - end, get_group_users(Host, Group)). - %% Get list of groups to which this group is displayed displayed_to_groups(GroupName, LServer) -> GroupsOpts = groups_with_opts(LServer), - lists:filter(fun ({_Group, Opts}) -> + Gs = lists:filter(fun ({_Group, Opts}) -> lists:member(GroupName, proplists:get_value(displayed_groups, Opts, [])) end, - GroupsOpts). + GroupsOpts), + [Name || {Name, _} <- Gs]. push_item(User, Server, Item) -> - Stanza = jlib:iq_to_xml(#iq{type = set, - xmlns = ?NS_ROSTER, - id = <<"push", (randoms:get_string())/binary>>, - sub_el = - [#xmlel{name = <<"query">>, - attrs = [{<<"xmlns">>, ?NS_ROSTER}], - children = [item_to_xml(Item)]}]}), - lists:foreach(fun (Resource) -> - JID = jlib:make_jid(User, Server, Resource), - ejabberd_router:route(JID, JID, Stanza) - end, - ejabberd_sm:get_user_resources(User, Server)). + mod_roster:push_item(jid:make(User, Server), + Item#roster_item{subscription = none}, + Item). -push_roster_item(User, Server, ContactU, ContactS, - GroupName, Subscription) -> - Item = #roster{usj = - {User, Server, {ContactU, ContactS, <<"">>}}, - us = {User, Server}, jid = {ContactU, ContactS, <<"">>}, - name = <<"">>, subscription = Subscription, ask = none, - groups = [GroupName]}, +push_roster_item(User, Server, ContactU, ContactS, ContactN, + GroupLabel, Subscription) -> + Item = #roster_item{jid = jid:make(ContactU, ContactS), + name = ContactN, subscription = Subscription, ask = undefined, + groups = [GroupLabel]}, push_item(User, Server, Item). -item_to_xml(Item) -> - Attrs1 = [{<<"jid">>, - jlib:jid_to_string(Item#roster.jid)}], - Attrs2 = case Item#roster.name of - <<"">> -> Attrs1; - Name -> [{<<"name">>, Name} | Attrs1] - end, - Attrs3 = case Item#roster.subscription of - none -> [{<<"subscription">>, <<"none">>} | Attrs2]; - from -> [{<<"subscription">>, <<"from">>} | Attrs2]; - to -> [{<<"subscription">>, <<"to">>} | Attrs2]; - both -> [{<<"subscription">>, <<"both">>} | Attrs2]; - remove -> [{<<"subscription">>, <<"remove">>} | Attrs2] - end, - Attrs4 = case ask_to_pending(Item#roster.ask) of - out -> [{<<"ask">>, <<"subscribe">>} | Attrs3]; - both -> [{<<"ask">>, <<"subscribe">>} | Attrs3]; - _ -> Attrs3 - end, - SubEls1 = lists:map(fun (G) -> - #xmlel{name = <<"group">>, attrs = [], - children = [{xmlcdata, G}]} - end, - Item#roster.groups), - SubEls = SubEls1 ++ Item#roster.xs, - #xmlel{name = <<"item">>, attrs = Attrs4, - children = SubEls}. +-spec c2s_self_presence({presence(), ejabberd_c2s:state()}) + -> {presence(), ejabberd_c2s:state()}. +c2s_self_presence(Acc) -> + Acc. -ask_to_pending(subscribe) -> out; -ask_to_pending(unsubscribe) -> none; -ask_to_pending(Ask) -> Ask. - -user_available(New) -> - LUser = New#jid.luser, - LServer = New#jid.lserver, +-spec unset_presence(binary(), binary(), binary(), binary()) -> ok. +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("user_available for ~p @ ~p (~p resources)", - [LUser, LServer, length(Resources)]), - case length(Resources) of - %% first session for this user - 1 -> - UserGroups = get_user_groups({LUser, LServer}), - lists:foreach(fun (OG) -> - ?DEBUG("user_available: pushing ~p @ ~p grp ~p", - [LUser, LServer, OG]), - DisplayedToGroups = displayed_to_groups(OG, LServer), - DisplayedGroups = get_displayed_groups(OG, LServer), - broadcast_displayed_to_user(LUser, LServer, LServer, both, DisplayedGroups), - broadcast_user_to_displayed(LUser, LServer, LServer, both, DisplayedToGroups) - end, - UserGroups); - _ -> ok - end. - -unset_presence(LUser, LServer, Resource, Status) -> - Resources = ejabberd_sm:get_user_resources(LUser, - LServer), - ?DEBUG("unset_presence for ~p @ ~p / ~p -> ~p " + ?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 -> - OnlineGroups = get_special_users_groups_online(LServer), - lists:foreach(fun (OG) -> - push_user_to_displayed(LUser, LServer, OG, - LServer, remove, displayed_to_groups(OG, LServer)), - push_displayed_to_user(LUser, LServer, - LServer, remove, displayed_to_groups(OG, LServer)) - end, - OnlineGroups); + lists:foreach( + fun(OG) -> + DisplayedToGroups = displayed_to_groups(OG, LServer), + push_user_to_displayed(LUser, LServer, OG, + LServer, remove, DisplayedToGroups), + push_displayed_to_user(LUser, LServer, + LServer, remove, DisplayedToGroups) + end, get_groups_with_wildcards(LServer, online)); _ -> ok end. %%--------------------- -%% Web Admin +%% Web Admin: Page Frontend %%--------------------- +%% @format-begin + webadmin_menu(Acc, _Host, Lang) -> - [{<<"shared-roster">>, ?T(<<"Shared Roster Groups">>)} - | Acc]. + [{<<"shared-roster">>, translate:translate(Lang, ?T("Shared Roster Groups"))} | Acc]. -webadmin_page(_, Host, - #request{us = _US, path = [<<"shared-roster">>], - q = Query, lang = Lang} = - _Request) -> - Res = list_shared_roster_groups(Host, Query, Lang), - {stop, Res}; -webadmin_page(_, Host, - #request{us = _US, path = [<<"shared-roster">>, Group], - q = Query, lang = Lang} = - _Request) -> - Res = shared_roster_group(Host, Group, Query, Lang), - {stop, Res}; -webadmin_page(Acc, _, _) -> Acc. +webadmin_page(_, + Host, + #request{us = _US, + path = [<<"shared-roster">> | RPath], + lang = Lang} = + R) -> + PageTitle = translate:translate(Lang, ?T("Shared Roster Groups")), + Head = ?H1GL(PageTitle, <<"modules/#mod_shared_roster">>, <<"mod_shared_roster">>), + Level = length(RPath), + Res = case check_group_exists(Host, RPath) of + true -> + webadmin_page_backend(Host, RPath, R, Lang, Level); + false -> + [?XREST(<<"Group does not exist.">>)] + end, + {stop, Head ++ Res}; +webadmin_page(Acc, _, _) -> + Acc. -list_shared_roster_groups(Host, Query, Lang) -> - Res = list_sr_groups_parse_query(Host, Query), - SRGroups = (?MODULE):list_groups(Host), - FGroups = (?XAE(<<"table">>, [], - [?XE(<<"tbody">>, - (lists:map(fun (Group) -> - ?XE(<<"tr">>, - [?XE(<<"td">>, - [?INPUT(<<"checkbox">>, - <<"selected">>, - Group)]), - ?XE(<<"td">>, - [?AC(<>, - Group)])]) - end, - lists:sort(SRGroups)) - ++ - [?XE(<<"tr">>, - [?X(<<"td">>), - ?XE(<<"td">>, - [?INPUT(<<"text">>, <<"namenew">>, - <<"">>)]), - ?XE(<<"td">>, - [?INPUTT(<<"submit">>, <<"addnew">>, - <<"Add New">>)])])]))])), - (?H1GL((?T(<<"Shared Roster Groups">>)), - <<"modsharedroster">>, <<"mod_shared_roster">>)) - ++ - case Res of - ok -> [?XREST(<<"Submitted">>)]; - error -> [?XREST(<<"Bad format">>)]; - nothing -> [] - end - ++ - [?XAE(<<"form">>, - [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], - [FGroups, ?BR, - ?INPUTT(<<"submit">>, <<"delete">>, - <<"Delete Selected">>)])]. +check_group_exists(Host, [<<"group">>, Id | _]) -> + case get_group_opts(Host, Id) of + error -> + false; + _ -> + true + end; +check_group_exists(_, _) -> + true. -list_sr_groups_parse_query(Host, Query) -> - case lists:keysearch(<<"addnew">>, 1, Query) of - {value, _} -> list_sr_groups_parse_addnew(Host, Query); - _ -> - case lists:keysearch(<<"delete">>, 1, Query) of - {value, _} -> list_sr_groups_parse_delete(Host, Query); - _ -> nothing - end - end. +%%--------------------- +%% Web Admin: Page Backend +%%--------------------- -list_sr_groups_parse_addnew(Host, Query) -> - case lists:keysearch(<<"namenew">>, 1, Query) of - {value, {_, Group}} when Group /= <<"">> -> - (?MODULE):create_group(Host, Group), ok; - _ -> error - end. +webadmin_page_backend(Host, [<<"group">>, Id, <<"info">> | RPath], R, _Lang, Level) -> + Breadcrumb = + make_breadcrumb({group_section, + Level, + <<"Groups of ", Host/binary>>, + Id, + <<"Information">>, + RPath}), + SetLabel = + make_command(srg_set_info, + R, + [{<<"host">>, Host}, {<<"group">>, Id}, {<<"key">>, <<"label">>}], + [{only, without_presentation}, {input_name_append, [Id, Host, <<"label">>]}]), + SetDescription = + make_command(srg_set_info, + R, + [{<<"host">>, Host}, {<<"group">>, Id}, {<<"key">>, <<"description">>}], + [{only, without_presentation}, + {input_name_append, [Id, Host, <<"description">>]}]), + SetAll = + make_command(srg_set_info, + R, + [{<<"host">>, Host}, + {<<"group">>, Id}, + {<<"key">>, <<"all_users">>}, + {<<"value">>, <<"true">>}], + [{only, button}, + {input_name_append, [Id, Host, <<"all_users">>, <<"true">>]}]), + UnsetAll = + make_command(srg_set_info, + R, + [{<<"host">>, Host}, + {<<"group">>, Id}, + {<<"key">>, <<"all_users">>}, + {<<"value">>, <<"false">>}], + [{only, button}, + {input_name_append, [Id, Host, <<"all_users">>, <<"false">>]}]), + SetOnline = + make_command(srg_set_info, + R, + [{<<"host">>, Host}, + {<<"group">>, Id}, + {<<"key">>, <<"online_users">>}, + {<<"value">>, <<"true">>}], + [{only, button}, + {input_name_append, [Id, Host, <<"online_users">>, <<"true">>]}]), + UnsetOnline = + make_command(srg_set_info, + R, + [{<<"host">>, Host}, + {<<"group">>, Id}, + {<<"key">>, <<"online_users">>}, + {<<"value">>, <<"false">>}], + [{only, button}, + {input_name_append, [Id, Host, <<"online_users">>, <<"false">>]}]), + GetInfo = + make_command_raw_value(srg_get_info, R, [{<<"group">>, Id}, {<<"host">>, Host}]), + AllElement = + case proplists:get_value(<<"all_users">>, GetInfo, not_found) of + "true" -> + {?C("Unset @all@: "), UnsetAll}; + _ -> + {?C("Set @all@: "), SetAll} + end, + OnlineElement = + case proplists:get_value(<<"online_users">>, GetInfo, not_found) of + "true" -> + {?C("Unset @online@: "), UnsetOnline}; + _ -> + {?C("Set @online@: "), SetOnline} + end, + Types = + [{?C("Set label: "), SetLabel}, + {?C("Set description: "), SetDescription}, + AllElement, + OnlineElement], + Get = [?BR, + make_command(srg_get_info, R, [{<<"host">>, Host}, {<<"group">>, Id}], []), + make_command(srg_set_info, R, [], [{only, presentation}]), + make_table(20, [], [{<<"">>, right}, <<"">>], Types)], + Breadcrumb ++ Get; +webadmin_page_backend(Host, + [<<"group">>, Id, <<"displayed">> | RPath], + R, + _Lang, + Level) -> + Breadcrumb = + make_breadcrumb({group_section, + Level, + <<"Groups of ", Host/binary>>, + Id, + <<"Displayed Groups">>, + RPath}), + AddDisplayed = + make_command(srg_add_displayed, R, [{<<"host">>, Host}, {<<"group">>, Id}], []), + _ = make_webadmin_displayed_table(Host, Id, R), + DisplayedTable = make_webadmin_displayed_table(Host, Id, R), + Get = [?BR, + make_command(srg_get_displayed, R, [], [{only, presentation}]), + make_command(srg_del_displayed, R, [], [{only, presentation}]), + ?XE(<<"blockquote">>, [DisplayedTable]), + AddDisplayed], + Breadcrumb ++ Get; +webadmin_page_backend(Host, [<<"group">>, Id, <<"members">> | RPath], R, _Lang, Level) -> + Breadcrumb = + make_breadcrumb({group_section, + Level, + <<"Groups of ", Host/binary>>, + Id, + <<"Members">>, + RPath}), + UserAdd = make_command(srg_user_add, R, [{<<"grouphost">>, Host}, {<<"group">>, Id}], []), + _ = make_webadmin_members_table(Host, Id, R), + MembersTable = make_webadmin_members_table(Host, Id, R), + Get = [make_command(srg_get_members, R, [], [{only, presentation}]), + make_command(srg_user_del, R, [], [{only, presentation}]), + ?XE(<<"blockquote">>, [MembersTable]), + UserAdd], + Breadcrumb ++ Get; +webadmin_page_backend(Host, [<<"group">>, Id, <<"delete">> | RPath], R, _Lang, Level) -> + Breadcrumb = + make_breadcrumb({group_section, + Level, + <<"Groups of ", Host/binary>>, + Id, + <<"Delete">>, + RPath}), + Get = [make_command(srg_delete, + R, + [{<<"host">>, Host}, {<<"group">>, Id}], + [{style, danger}])], + Breadcrumb ++ Get; +webadmin_page_backend(Host, [<<"group">>, Id | _RPath], _R, _Lang, Level) -> + Breadcrumb = make_breadcrumb({group, Level, <<"Groups of ", Host/binary>>, Id}), + MenuItems = + [{<<"info/">>, <<"Information">>}, + {<<"members/">>, <<"Members">>}, + {<<"displayed/">>, <<"Displayed Groups">>}, + {<<"delete/">>, <<"Delete">>}], + Get = [?XE(<<"ul">>, [?LI([?AC(MIU, MIN)]) || {MIU, MIN} <- MenuItems])], + Breadcrumb ++ Get; +webadmin_page_backend(Host, RPath, R, _Lang, Level) -> + Breadcrumb = make_breadcrumb({groups, <<"Groups of ", Host/binary>>}), + _ = make_webadmin_srg_table(Host, R, 3 + Level, RPath), + Set = [make_command(srg_add, R, [{<<"host">>, Host}], []), + make_command(srg_create, R, [{<<"host">>, Host}], [])], + RV2 = make_webadmin_srg_table(Host, R, 3 + Level, RPath), + Get = [make_command(srg_list, R, [{<<"host">>, Host}], [{only, presentation}]), + make_command(srg_get_info, R, [{<<"host">>, Host}], [{only, presentation}]), + make_command(srg_delete, R, [{<<"host">>, Host}], [{only, presentation}]), + ?XE(<<"blockquote">>, [RV2])], + Breadcrumb ++ Get ++ Set. -list_sr_groups_parse_delete(Host, Query) -> - SRGroups = (?MODULE):list_groups(Host), - lists:foreach(fun (Group) -> - case lists:member({<<"selected">>, Group}, Query) of - true -> (?MODULE):delete_group(Host, Group); - _ -> ok - end - end, - SRGroups), - ok. +%%--------------------- +%% Web Admin: Table Generation +%%--------------------- -shared_roster_group(Host, Group, Query, Lang) -> - Res = shared_roster_group_parse_query(Host, Group, - Query), - GroupOpts = (?MODULE):get_group_opts(Host, Group), - Name = get_opt(GroupOpts, name, <<"">>), - Description = get_opt(GroupOpts, description, <<"">>), - AllUsers = get_opt(GroupOpts, all_users, false), - OnlineUsers = get_opt(GroupOpts, online_users, false), - DisplayedGroups = get_opt(GroupOpts, displayed_groups, - []), - Members = (?MODULE):get_group_explicit_users(Host, - Group), - FMembers = iolist_to_binary( - [if AllUsers -> <<"@all@\n">>; - true -> <<"">> - end, - if OnlineUsers -> <<"@online@\n">>; - true -> <<"">> - end, - [[us_to_list(Member), $\n] || Member <- Members]]), - FDisplayedGroups = [<> || DG <- DisplayedGroups], - DescNL = length(ejabberd_regexp:split(Description, - <<"\n">>)), - FGroup = (?XAE(<<"table">>, - [{<<"class">>, <<"withtextareas">>}], - [?XE(<<"tbody">>, - [?XE(<<"tr">>, - [?XCT(<<"td">>, <<"Name:">>), - ?XE(<<"td">>, - [?INPUT(<<"text">>, <<"name">>, Name)])]), - ?XE(<<"tr">>, - [?XCT(<<"td">>, <<"Description:">>), - ?XE(<<"td">>, - [?TEXTAREA(<<"description">>, - jlib:integer_to_binary(lists:max([3, - DescNL])), - <<"20">>, Description)])]), - ?XE(<<"tr">>, - [?XCT(<<"td">>, <<"Members:">>), - ?XE(<<"td">>, - [?TEXTAREA(<<"members">>, - jlib:integer_to_binary(lists:max([3, - byte_size(FMembers)])), - <<"20">>, FMembers)])]), - ?XE(<<"tr">>, - [?XCT(<<"td">>, <<"Displayed Groups:">>), - ?XE(<<"td">>, - [?TEXTAREA(<<"dispgroups">>, - jlib:integer_to_binary(lists:max([3, length(FDisplayedGroups)])), - <<"20">>, - list_to_binary(FDisplayedGroups))])])])])), - (?H1GL((?T(<<"Shared Roster Groups">>)), - <<"modsharedroster">>, <<"mod_shared_roster">>)) - ++ - [?XC(<<"h2">>, <<(?T(<<"Group ">>))/binary, Group/binary>>)] ++ - case Res of - ok -> [?XREST(<<"Submitted">>)]; - error -> [?XREST(<<"Bad format">>)]; - nothing -> [] - end - ++ - [?XAE(<<"form">>, - [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], - [FGroup, ?BR, - ?INPUTT(<<"submit">>, <<"submit">>, <<"Submit">>)])]. +make_webadmin_srg_table(Host, R, Level, RPath) -> + Groups = + case make_command_raw_value(srg_list, R, [{<<"host">>, Host}]) of + Gs when is_list(Gs) -> + Gs; + _ -> + [] + end, + Columns = + [<<"id">>, + <<"label">>, + <<"description">>, + <<"all">>, + <<"online">>, + {<<"members">>, right}, + {<<"displayed">>, right}, + <<"">>], + Rows = + [{make_command(echo3, + R, + [{<<"first">>, Id}, {<<"second">>, Host}, {<<"sentence">>, Id}], + [{only, value}, {result_links, [{sentence, shared_roster, Level, <<"">>}]}]), + make_command(echo3, + R, + [{<<"first">>, Id}, + {<<"second">>, Host}, + {<<"sentence">>, + iolist_to_binary(proplists:get_value(<<"label">>, + make_command_raw_value(srg_get_info, + R, + [{<<"group">>, + Id}, + {<<"host">>, + Host}]), + ""))}], + [{only, value}, + {result_links, [{sentence, shared_roster, Level, <<"info">>}]}]), + make_command(echo3, + R, + [{<<"first">>, Id}, + {<<"second">>, Host}, + {<<"sentence">>, + iolist_to_binary(proplists:get_value(<<"description">>, + make_command_raw_value(srg_get_info, + R, + [{<<"group">>, + Id}, + {<<"host">>, + Host}]), + ""))}], + [{only, value}, + {result_links, [{sentence, shared_roster, Level, <<"info">>}]}]), + make_command(echo3, + R, + [{<<"first">>, Id}, + {<<"second">>, Host}, + {<<"sentence">>, + iolist_to_binary(proplists:get_value(<<"all_users">>, + make_command_raw_value(srg_get_info, + R, + [{<<"group">>, + Id}, + {<<"host">>, + Host}]), + ""))}], + [{only, value}, + {result_links, [{sentence, shared_roster, Level, <<"info">>}]}]), + make_command(echo3, + R, + [{<<"first">>, Id}, + {<<"second">>, Host}, + {<<"sentence">>, + iolist_to_binary(proplists:get_value(<<"online_users">>, + make_command_raw_value(srg_get_info, + R, + [{<<"group">>, + Id}, + {<<"host">>, + Host}]), + ""))}], + [{only, value}, + {result_links, [{sentence, shared_roster, Level, <<"info">>}]}]), + make_command(echo3, + R, + [{<<"first">>, Id}, + {<<"second">>, Host}, + {<<"sentence">>, + integer_to_binary(length(make_command_raw_value(srg_get_members, + R, + [{<<"group">>, Id}, + {<<"host">>, Host}])))}], + [{only, value}, + {result_links, [{sentence, shared_roster, Level, <<"members">>}]}]), + make_command(echo3, + R, + [{<<"first">>, Id}, + {<<"second">>, Host}, + {<<"sentence">>, + integer_to_binary(length(make_command_raw_value(srg_get_displayed, + R, + [{<<"group">>, Id}, + {<<"host">>, Host}])))}], + [{only, value}, + {result_links, [{sentence, shared_roster, Level, <<"displayed">>}]}]), + make_command(srg_delete, + R, + [{<<"group">>, Id}, {<<"host">>, Host}], + [{only, button}, {style, danger}, {input_name_append, [Id, Host]}])} + || Id <- Groups], + make_table(20, RPath, Columns, Rows). -shared_roster_group_parse_query(Host, Group, Query) -> - case lists:keysearch(<<"submit">>, 1, Query) of - {value, _} -> - {value, {_, Name}} = lists:keysearch(<<"name">>, 1, - Query), - {value, {_, Description}} = - lists:keysearch(<<"description">>, 1, Query), - {value, {_, SMembers}} = lists:keysearch(<<"members">>, - 1, Query), - {value, {_, SDispGroups}} = - lists:keysearch(<<"dispgroups">>, 1, Query), - NameOpt = if Name == <<"">> -> []; - true -> [{name, Name}] - end, - DescriptionOpt = if Description == <<"">> -> []; - true -> [{description, Description}] - end, - DispGroups = str:tokens(SDispGroups, <<"\r\n">>), - DispGroupsOpt = if DispGroups == [] -> []; - true -> [{displayed_groups, DispGroups}] - end, - OldMembers = (?MODULE):get_group_explicit_users(Host, - Group), - SJIDs = str:tokens(SMembers, <<", \r\n">>), - NewMembers = lists:foldl(fun (_SJID, error) -> error; - (SJID, USs) -> - case SJID of - <<"@all@">> -> USs; - <<"@online@">> -> USs; - _ -> - case jlib:string_to_jid(SJID) - of - JID - when is_record(JID, - jid) -> - [{JID#jid.luser, - JID#jid.lserver} - | USs]; - error -> error - end - end - end, - [], SJIDs), - AllUsersOpt = case lists:member(<<"@all@">>, SJIDs) of - true -> [{all_users, true}]; - false -> [] - end, - OnlineUsersOpt = case lists:member(<<"@online@">>, - SJIDs) - of - true -> [{online_users, true}]; - false -> [] - end, - CurrentDisplayedGroups = get_displayed_groups(Group, Host), - AddedDisplayedGroups = DispGroups -- CurrentDisplayedGroups, - RemovedDisplayedGroups = CurrentDisplayedGroups -- DispGroups, - displayed_groups_update(OldMembers, RemovedDisplayedGroups, remove), - displayed_groups_update(OldMembers, AddedDisplayedGroups, both), - (?MODULE):set_group_opts(Host, Group, - NameOpt ++ - DispGroupsOpt ++ - DescriptionOpt ++ - AllUsersOpt ++ OnlineUsersOpt), - if NewMembers == error -> error; - true -> - AddedMembers = NewMembers -- OldMembers, - RemovedMembers = OldMembers -- NewMembers, - lists:foreach(fun (US) -> - (?MODULE):remove_user_from_group(Host, - US, - Group) - end, - RemovedMembers), - lists:foreach(fun (US) -> - (?MODULE):add_user_to_group(Host, US, - Group) - end, - AddedMembers), - ok - end; - _ -> nothing - end. +make_webadmin_members_table(Host, Id, R) -> + Members = + case make_command_raw_value(srg_get_members, R, [{<<"host">>, Host}, {<<"group">>, Id}]) + of + Ms when is_list(Ms) -> + Ms; + _ -> + [] + end, + make_table([<<"member">>, <<"">>], + [{make_command(echo, + R, + [{<<"sentence">>, Jid}], + [{only, value}, {result_links, [{sentence, user, 6, <<"">>}]}]), + make_command(srg_user_del, + R, + [{<<"user">>, + element(1, + jid:split( + jid:decode(Jid)))}, + {<<"host">>, + element(2, + jid:split( + jid:decode(Jid)))}, + {<<"group">>, Id}, + {<<"grouphost">>, Host}], + [{only, button}, + {style, danger}, + {input_name_append, + [element(1, + jid:split( + jid:decode(Jid))), + element(2, + jid:split( + jid:decode(Jid))), + Id, + Host]}])} + || Jid <- Members]). -get_opt(Opts, Opt, Default) -> - case lists:keysearch(Opt, 1, Opts) of - {value, {_, Val}} -> Val; - false -> Default - end. +make_webadmin_displayed_table(Host, Id, R) -> + Displayed = + case make_command_raw_value(srg_get_displayed, R, [{<<"host">>, Host}, {<<"group">>, Id}]) + of + Ms when is_list(Ms) -> + Ms; + _ -> + [] + end, + make_table([<<"group">>, <<"">>], + [{make_command(echo3, + R, + [{<<"first">>, ThisId}, + {<<"second">>, Host}, + {<<"sentence">>, ThisId}], + [{only, value}, + {result_links, [{sentence, shared_roster, 6, <<"">>}]}]), + make_command(srg_del_displayed, + R, + [{<<"group">>, Id}, {<<"host">>, Host}, {<<"del">>, ThisId}], + [{only, button}, + {style, danger}, + {input_name_append, [Id, Host, ThisId]}])} + || ThisId <- Displayed]). -us_to_list({User, Server}) -> - jlib:jid_to_string({User, Server, <<"">>}). +make_breadcrumb({groups, Service}) -> + make_breadcrumb([Service]); +make_breadcrumb({group, Level, Service, Name}) -> + make_breadcrumb([{Level, Service}, separator, Name]); +make_breadcrumb({group_section, Level, Service, Name, Section, RPath}) -> + make_breadcrumb([{Level, Service}, separator, {Level - 2, Name}, separator, Section + | RPath]); +make_breadcrumb(Elements) -> + lists:map(fun ({xmlel, _, _, _} = Xmlel) -> + Xmlel; + (<<"sort">>) -> + ?C(<<" +">>); + (<<"page">>) -> + ?C(<<" #">>); + (separator) -> + ?C(<<" > ">>); + (Bin) when is_binary(Bin) -> + ?C(Bin); + ({Level, Bin}) when is_integer(Level) and is_binary(Bin) -> + ?AC(binary:copy(<<"../">>, Level), Bin) + end, + Elements). +%% @format-end split_grouphost(Host, Group) -> case str:tokens(Group, <<"@">>) of @@ -1369,36 +1279,12 @@ split_grouphost(Host, Group) -> [_] -> {Host, Group} end. -broadcast_subscription(User, Server, ContactJid, Subscription) -> - ejabberd_sm:route( - jlib:make_jid(<<"">>, Server, <<"">>), - jlib:make_jid(User, Server, <<"">>), - {broadcast, {item, ContactJid, - Subscription}}). - -displayed_groups_update(Members, DisplayedGroups, Subscription) -> - lists:foreach(fun({U, S}) -> - push_displayed_to_user(U, S, S, Subscription, DisplayedGroups), - case Subscription of - both -> - broadcast_displayed_to_user(U, S, S, to, DisplayedGroups), - broadcast_displayed_to_user(U, S, S, from, DisplayedGroups); - Subscr -> - broadcast_displayed_to_user(U, S, S, Subscr, DisplayedGroups) - end - end, Members). - -make_jid_s(U, S) -> - ejabberd_odbc:escape(jlib:jid_to_string(jlib:jid_tolower(jlib:make_jid(U, - S, - <<"">>)))). - -make_jid_s({U, S}) -> make_jid_s(U, S). - opts_to_binary(Opts) -> lists:map( - fun({name, Name}) -> - {name, iolist_to_binary(Name)}; + fun({label, Label}) -> + {label, iolist_to_binary(Label)}; + ({name, Label}) -> % For SQL backwards compat with ejabberd 20.03 and older + {label, iolist_to_binary(Label)}; ({description, Desc}) -> {description, iolist_to_binary(Desc)}; ({displayed_groups, Gs}) -> @@ -1407,102 +1293,129 @@ opts_to_binary(Opts) -> Opt end, Opts). -sr_group_schema() -> - {record_info(fields, sr_group), #sr_group{}}. +export(LServer) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:export(LServer). -sr_user_schema() -> - {record_info(fields, sr_user), #sr_user{}}. +import_info() -> + [{<<"sr_group">>, 3}, {<<"sr_user">>, 3}]. -update_tables() -> - update_sr_group_table(), - update_sr_user_table(). +import_start(LServer, DBType) -> + Mod = gen_mod:db_mod(DBType, ?MODULE), + Mod:init(LServer, []). -update_sr_group_table() -> - Fields = record_info(fields, sr_group), - case mnesia:table_info(sr_group, attributes) of - Fields -> - ejabberd_config:convert_table_to_binary( - sr_group, Fields, set, - fun(#sr_group{group_host = {G, _}}) -> G end, - fun(#sr_group{group_host = {G, H}, - opts = Opts} = R) -> - R#sr_group{group_host = {iolist_to_binary(G), - iolist_to_binary(H)}, - opts = opts_to_binary(Opts)} - end); - _ -> - ?INFO_MSG("Recreating sr_group table", []), - mnesia:transform_table(sr_group, ignore, Fields) - end. +import(LServer, {sql, _}, DBType, Tab, L) -> + Mod = gen_mod:db_mod(DBType, ?MODULE), + Mod:import(LServer, Tab, L). -update_sr_user_table() -> - Fields = record_info(fields, sr_user), - case mnesia:table_info(sr_user, attributes) of - Fields -> - ejabberd_config:convert_table_to_binary( - sr_user, Fields, bag, - fun(#sr_user{us = {U, _}}) -> U end, - fun(#sr_user{us = {U, S}, group_host = {G, H}} = R) -> - R#sr_user{us = {iolist_to_binary(U), iolist_to_binary(S)}, - group_host = {iolist_to_binary(G), - iolist_to_binary(H)}} - end); - _ -> - ?INFO_MSG("Recreating sr_user table", []), - mnesia:transform_table(sr_user, ignore, Fields) - end. +mod_opt_type(db_type) -> + econf:db_type(?MODULE); +mod_opt_type(use_cache) -> + econf:bool(); +mod_opt_type(cache_size) -> + econf:pos_int(infinity); +mod_opt_type(cache_missed) -> + econf:bool(); +mod_opt_type(cache_life_time) -> + econf:timeout(second, infinity). -export(_Server) -> - [{sr_group, - fun(Host, #sr_group{group_host = {Group, LServer}, opts = Opts}) - when LServer == Host -> - SGroup = ejabberd_odbc:escape(Group), - SOpts = ejabberd_odbc:encode_term(Opts), - [[<<"delete from sr_group where name='">>, Group, <<"';">>], - [<<"insert into sr_group(name, opts) values ('">>, - SGroup, <<"', '">>, SOpts, <<"');">>]]; - (_Host, _R) -> - [] - end}, - {sr_user, - fun(Host, #sr_user{us = {U, S}, group_host = {Group, LServer}}) - when LServer == Host -> - SGroup = ejabberd_odbc:escape(Group), - SJID = ejabberd_odbc:escape( - jlib:jid_to_string( - jlib:jid_tolower( - jlib:make_jid(U, S, <<"">>)))), - [[<<"delete from sr_user where jid='">>, SJID, - <<"'and grp='">>, Group, <<"';">>], - [<<"insert into sr_user(jid, grp) values ('">>, - SJID, <<"', '">>, SGroup, <<"');">>]]; - (_Host, _R) -> - [] - end}]. +mod_options(Host) -> + [{db_type, ejabberd_config:default_db(Host, ?MODULE)}, + {use_cache, ejabberd_option:use_cache(Host)}, + {cache_size, ejabberd_option:cache_size(Host)}, + {cache_missed, ejabberd_option:cache_missed(Host)}, + {cache_life_time, ejabberd_option:cache_life_time(Host)}]. -import(LServer) -> - [{<<"select name, opts from sr_group;">>, - fun([Group, SOpts]) -> - #sr_group{group_host = {Group, LServer}, - opts = ejabberd_odbc:decode_term(SOpts)} - end}, - {<<"select jid, grp from sr_user;">>, - fun([SJID, Group]) -> - #jid{luser = U, lserver = S} = jlib:string_to_jid(SJID), - #sr_user{us = {U, S}, group_host = {Group, LServer}} - end}]. - -import(_LServer, mnesia, #sr_group{} = G) -> - mnesia:dirty_write(G); - -import(_LServer, mnesia, #sr_user{} = U) -> - mnesia:dirty_write(U); -import(_LServer, riak, #sr_group{group_host = {_, Host}} = G) -> - ejabberd_riak:put(G, sr_group_schema(), [{'2i', [{<<"host">>, Host}]}]); -import(_LServer, riak, #sr_user{us = US, group_host = {Group, Host}} = User) -> - ejabberd_riak:put(User, sr_user_schema(), - [{i, {US, {Group, Host}}}, - {'2i', [{<<"us">>, US}, - {<<"group_host">>, {Group, Host}}]}]); -import(_, _, _) -> - pass. +mod_doc() -> + #{desc => + [?T("This module enables you to create shared roster groups: " + "groups of accounts that can see members from (other) groups " + "in their rosters."), "", + ?T("The big advantages of this feature are that end users do not " + "need to manually add all users to their rosters, and that they " + "cannot permanently delete users from the shared roster groups. " + "A shared roster group can have members from any XMPP server, " + "but the presence will only be available from and to members of " + "the same virtual host where the group is created. It still " + "allows the users to have / add their own contacts, as it does " + "not replace the standard roster. Instead, the shared roster " + "contacts are merged to the relevant users at retrieval time. " + "The standard user rosters thus stay unmodified."), "", + ?T("Shared roster groups can be edited via the Web Admin, " + "and some API commands called 'srg_', for example _`srg_add`_ API. " + "Each group has a unique name and those parameters:"), "", + ?T("- Label: Used in the rosters where this group is displayed."),"", + ?T("- Description: of the group, which has no effect."), "", + ?T("- Members: A list of JIDs of group members, entered one per " + "line in the Web Admin. The special member directive '@all@' " + "represents all the registered users in the virtual host; " + "which is only recommended for a small server with just a few " + "hundred users. The special member directive '@online@' " + "represents the online users in the virtual host. With those " + "two directives, the actual list of members in those shared " + "rosters is generated dynamically at retrieval time."), "", + ?T("- Displayed: A list of groups that will be in the " + "rosters of this group's members. A group of other vhost can " + "be identified with 'groupid@vhost'."), "", + ?T("This module depends on _`mod_roster`_. " + "If not enabled, roster queries will return 503 errors.")], + opts => + [{db_type, + #{value => "mnesia | sql", + desc => + ?T("Same as top-level _`default_db`_ option, " + "but applied to this module only.")}}, + {use_cache, + #{value => "true | false", + desc => + ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, + {cache_size, + #{value => "pos_integer() | infinity", + desc => + ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, + {cache_missed, + #{value => "true | false", + desc => + ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, + {cache_life_time, + #{value => "timeout()", + desc => + ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}], + example => + [{?T("Take the case of a computer club that wants all its members " + "seeing each other in their rosters. To achieve this, they " + "need to create a shared roster group similar to this one:"), + ["Name: club_members", + "Label: Club Members", + "Description: Members from the computer club", + "Members: member1@example.org, member2@example.org, member3@example.org", + "Displayed Groups: club_members"]}, + {?T("In another case we have a company which has three divisions: " + "Management, Marketing and Sales. All group members should see " + "all other members in their rosters. Additionally, all managers " + "should have all marketing and sales people in their roster. " + "Simultaneously, all marketeers and the whole sales team " + "should see all managers. This scenario can be achieved by " + "creating shared roster groups as shown in the following lists:"), + ["First list:", + "Name: management", + "Label: Management", + "Description: Management", + "Members: manager1@example.org, manager2@example.org", + "Displayed: management, marketing, sales", + "", + "Second list:", + "Name: marketing", + "Label: Marketing", + "Description: Marketing", + "Members: marketeer1@example.org, marketeer2@example.org, marketeer3@example.org", + "Displayed: management, marketing", + "", + "Third list:", + "Name: sales", + "Label: Sales", + "Description: Sales", + "Members: salesman1@example.org, salesman2@example.org, salesman3@example.org", + "Displayed: management, sales" + ]} + ]}. diff --git a/src/mod_shared_roster_ldap.erl b/src/mod_shared_roster_ldap.erl index af85e4d40..509334be1 100644 --- a/src/mod_shared_roster_ldap.erl +++ b/src/mod_shared_roster_ldap.erl @@ -7,7 +7,7 @@ %%% Created : 5 Mar 2005 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -31,30 +31,27 @@ -behaviour(gen_mod). %% API --export([start_link/2, start/2, stop/1]). +-export([start/2, stop/1, reload/3]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --export([get_user_roster/2, get_subscription_lists/3, - get_jid_info/4, process_item/2, in_subscription/6, - out_subscription/4]). +-export([get_user_roster/2, + get_jid_info/4, process_item/2, in_subscription/2, + out_subscription/1, mod_opt_type/1, mod_options/1, + depends/2, mod_doc/0]). --include("ejabberd.hrl"). -include("logger.hrl"). --include("jlib.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). -include("mod_roster.hrl"). - -include("eldap.hrl"). +-include("translate.hrl"). --define(CACHE_SIZE, 1000). - --define(USER_CACHE_VALIDITY, 300). - --define(GROUP_CACHE_VALIDITY, 300). - --define(LDAP_SEARCH_TIMEOUT, 5). +-define(USER_CACHE, shared_roster_ldap_user_cache). +-define(GROUP_CACHE, shared_roster_ldap_group_cache). +-define(DISPLAYED_CACHE, shared_roster_ldap_displayed_cache). +-define(LDAP_SEARCH_TIMEOUT, 5). %% Timeout for LDAP search queries in seconds -record(state, {host = <<"">> :: binary(), @@ -74,73 +71,68 @@ user_desc = <<"">> :: binary(), user_uid = <<"">> :: binary(), uid_format = <<"">> :: binary(), - uid_format_re = <<"">> :: binary(), + uid_format_re :: undefined | misc:re_mp(), filter = <<"">> :: binary(), ufilter = <<"">> :: binary(), rfilter = <<"">> :: binary(), gfilter = <<"">> :: binary(), - auth_check = true :: boolean(), - user_cache_size = ?CACHE_SIZE :: non_neg_integer(), - group_cache_size = ?CACHE_SIZE :: non_neg_integer(), - user_cache_validity = ?USER_CACHE_VALIDITY :: non_neg_integer(), - group_cache_validity = ?GROUP_CACHE_VALIDITY :: non_neg_integer()}). + user_jid_attr = <<"">> :: binary(), + auth_check = true :: boolean()}). -record(group_info, {desc, members}). %%==================================================================== %% API %%==================================================================== -start_link(Host, Opts) -> - Proc = gen_mod:get_module_proc(Host, ?MODULE), - gen_server:start_link({local, Proc}, ?MODULE, - [Host, Opts], []). - start(Host, Opts) -> - Proc = gen_mod:get_module_proc(Host, ?MODULE), - ChildSpec = {Proc, {?MODULE, start_link, [Host, Opts]}, - permanent, 1000, worker, [?MODULE]}, - supervisor:start_child(ejabberd_sup, ChildSpec). + gen_mod:start_child(?MODULE, Host, Opts). stop(Host) -> + gen_mod:stop_child(?MODULE, Host). + +reload(Host, NewOpts, _OldOpts) -> + case init_cache(Host, NewOpts) of + true -> + ets_cache:setopts(?USER_CACHE, cache_opts(Host, NewOpts)), + ets_cache:setopts(?GROUP_CACHE, cache_opts(Host, NewOpts)), + ets_cache:setopts(?DISPLAYED_CACHE, cache_opts(Host, NewOpts)); + false -> + ok + end, Proc = gen_mod:get_module_proc(Host, ?MODULE), - supervisor:terminate_child(ejabberd_sup, Proc), - supervisor:delete_child(ejabberd_sup, Proc). + gen_server:cast(Proc, {set_state, parse_options(Host, NewOpts)}). + +depends(_Host, _Opts) -> + [{mod_roster, hard}]. %%-------------------------------------------------------------------- %% Hooks %%-------------------------------------------------------------------- -get_user_roster(Items, {U, S} = US) -> +-spec get_user_roster([#roster_item{}], {binary(), binary()}) -> [#roster_item{}]. +get_user_roster(Items, US) -> SRUsers = get_user_to_groups_map(US, true), - {NewItems1, SRUsersRest} = lists:mapfoldl(fun (Item, - SRUsers1) -> - {_, _, {U1, S1, _}} = - Item#roster.usj, - US1 = {U1, S1}, - case dict:find(US1, - SRUsers1) - of - {ok, _GroupNames} -> - {Item#roster{subscription - = - both, - ask = - none}, - dict:erase(US1, - SRUsers1)}; - error -> - {Item, SRUsers1} - end - end, - SRUsers, Items), - SRItems = [#roster{usj = {U, S, {U1, S1, <<"">>}}, - us = US, jid = {U1, S1, <<"">>}, - name = get_user_name(U1, S1), subscription = both, - ask = none, groups = GroupNames} + {NewItems1, SRUsersRest} = lists:mapfoldl( + fun(Item = #roster_item{jid = #jid{luser = U1, lserver = S1}}, SRUsers1) -> + US1 = {U1, S1}, + case dict:find(US1, SRUsers1) of + {ok, GroupNames} -> + {Item#roster_item{subscription = both, + groups = Item#roster_item.groups ++ GroupNames}, + dict:erase(US1, SRUsers1)}; + error -> + {Item, SRUsers1} + end + end, + SRUsers, Items), + SRItems = [#roster_item{jid = jid:make(U1, S1), + name = get_user_name(U1, S1), subscription = both, + ask = undefined, groups = GroupNames} || {{U1, S1}, GroupNames} <- dict:to_list(SRUsersRest)], SRItems ++ NewItems1. %% This function in use to rewrite the roster entries when moving or renaming %% them in the user contact list. +-spec process_item(#roster{}, binary()) -> #roster{}. process_item(RosterItem, _Host) -> USFrom = RosterItem#roster.us, {User, Server, _Resource} = RosterItem#roster.jid, @@ -156,24 +148,14 @@ process_item(RosterItem, _Host) -> _ -> RosterItem#roster{subscription = both, ask = none} end. -get_subscription_lists({F, T}, User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - US = {LUser, LServer}, - DisplayedGroups = get_user_displayed_groups(US), - SRUsers = lists:usort(lists:flatmap(fun (Group) -> - get_group_users(LServer, Group) - end, - DisplayedGroups)), - SRJIDs = [{U1, S1, <<"">>} || {U1, S1} <- SRUsers], - {lists:usort(SRJIDs ++ F), lists:usort(SRJIDs ++ T)}. - -get_jid_info({Subscription, Groups}, User, Server, +-spec get_jid_info({subscription(), ask(), [binary()]}, binary(), binary(), jid()) + -> {subscription(), ask(), [binary()]}. +get_jid_info({Subscription, Ask, Groups}, User, Server, JID) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), US = {LUser, LServer}, - {U1, S1, _} = jlib:jid_tolower(JID), + {U1, S1, _} = jid:tolower(JID), US1 = {U1, S1}, SRUsers = get_user_to_groups_map(US, false), case dict:find(US1, SRUsers) of @@ -181,25 +163,27 @@ get_jid_info({Subscription, Groups}, User, Server, NewGroups = if Groups == [] -> GroupNames; true -> Groups end, - {both, NewGroups}; - error -> {Subscription, Groups} + {both, none, NewGroups}; + error -> {Subscription, Ask, Groups} end. -in_subscription(Acc, User, Server, JID, Type, - _Reason) -> +-spec in_subscription(boolean(), presence()) -> boolean(). +in_subscription(Acc, #presence{to = To, from = JID, type = Type}) -> + #jid{user = User, server = Server} = To, process_subscription(in, User, Server, JID, Type, Acc). -out_subscription(User, Server, JID, Type) -> - process_subscription(out, User, Server, JID, Type, - false). +-spec out_subscription(presence()) -> boolean(). +out_subscription(#presence{from = From, to = JID, type = Type}) -> + #jid{user = User, server = Server} = From, + process_subscription(out, User, Server, JID, Type, false). process_subscription(Direction, User, Server, JID, _Type, Acc) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), US = {LUser, LServer}, {U1, S1, _} = - jlib:jid_tolower(jlib:jid_remove_resource(JID)), + jid:tolower(jid:remove_resource(JID)), US1 = {U1, S1}, DisplayedGroups = get_user_displayed_groups(US), SRUsers = lists:usort(lists:flatmap(fun (Group) -> @@ -218,22 +202,17 @@ process_subscription(Direction, User, Server, JID, %%==================================================================== %% gen_server callbacks %%==================================================================== -init([Host, Opts]) -> +init([Host|_]) -> + process_flag(trap_exit, true), + Opts = gen_mod:get_module_opts(Host, ?MODULE), State = parse_options(Host, Opts), - cache_tab:new(shared_roster_ldap_user, - [{max_size, State#state.user_cache_size}, {lru, false}, - {life_time, State#state.user_cache_validity}]), - cache_tab:new(shared_roster_ldap_group, - [{max_size, State#state.group_cache_size}, {lru, false}, - {life_time, State#state.group_cache_validity}]), + init_cache(Host, Opts), ejabberd_hooks:add(roster_get, Host, ?MODULE, get_user_roster, 70), ejabberd_hooks:add(roster_in_subscription, Host, ?MODULE, in_subscription, 30), ejabberd_hooks:add(roster_out_subscription, Host, ?MODULE, out_subscription, 30), - ejabberd_hooks:add(roster_get_subscription_lists, Host, - ?MODULE, get_subscription_lists, 70), ejabberd_hooks:add(roster_get_jid_info, Host, ?MODULE, get_jid_info, 70), ejabberd_hooks:add(roster_process_item, Host, ?MODULE, @@ -246,12 +225,19 @@ init([Host, Opts]) -> handle_call(get_state, _From, State) -> {reply, {ok, State}, State}; -handle_call(_Request, _From, State) -> - {reply, {error, badarg}, State}. +handle_call(Request, From, State) -> + ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), + {noreply, State}. -handle_cast(_Msg, State) -> {noreply, State}. +handle_cast({set_state, NewState}, _State) -> + {noreply, NewState}; +handle_cast(Msg, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Msg]), + {noreply, State}. -handle_info(_Info, State) -> {noreply, State}. +handle_info(Info, State) -> + ?WARNING_MSG("Unexpected info: ~p", [Info]), + {noreply, State}. terminate(_Reason, State) -> Host = State#state.host, @@ -261,8 +247,6 @@ terminate(_Reason, State) -> ?MODULE, in_subscription, 30), ejabberd_hooks:delete(roster_out_subscription, Host, ?MODULE, out_subscription, 30), - ejabberd_hooks:delete(roster_get_subscription_lists, - Host, ?MODULE, get_subscription_lists, 70), ejabberd_hooks:delete(roster_get_jid_info, Host, ?MODULE, get_jid_info, 70), ejabberd_hooks:delete(roster_process_item, Host, @@ -273,16 +257,9 @@ code_change(_OldVsn, State, _Extra) -> {ok, State}. %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- -%% For a given user, map all his shared roster contacts to groups they are -%% members of. Skip the user himself iff SkipUS is true. + get_user_to_groups_map({_, Server} = US, SkipUS) -> DisplayedGroups = get_user_displayed_groups(US), -%% Pass given FilterParseArgs to eldap_filter:parse, and if successful, run and -%% return the resulting filter, retrieving given AttributesList. Return the -%% result entries. On any error silently return an empty list of results. -%% -%% Eldap server ID and base DN for the query are both retrieved from the State -%% record. lists:foldl(fun (Group, Dict1) -> GroupName = get_group_name(Server, Group), lists:foldl(fun (Contact, Dict) -> @@ -320,6 +297,13 @@ eldap_search(State, FilterParseArgs, AttributesList) -> get_user_displayed_groups({User, Host}) -> {ok, State} = eldap_utils:get_state(Host, ?MODULE), + ets_cache:lookup(?DISPLAYED_CACHE, + {User, Host}, + fun () -> + search_user_displayed_groups(State, User) + end). + +search_user_displayed_groups(State, User) -> GroupAttr = State#state.group_attr, Entries = eldap_search(State, [eldap_filter:do_sub(State#state.rfilter, @@ -337,21 +321,20 @@ get_user_displayed_groups({User, Host}) -> get_group_users(Host, Group) -> {ok, State} = eldap_utils:get_state(Host, ?MODULE), - case cache_tab:dirty_lookup(shared_roster_ldap_group, - {Group, Host}, - fun () -> search_group_info(State, Group) end) - of - {ok, #group_info{members = Members}} + case ets_cache:lookup(?GROUP_CACHE, + {Group, Host}, + fun () -> search_group_info(State, Group) end) of + {ok, #group_info{members = Members}} when Members /= undefined -> - Members; - _ -> [] + Members; + _ -> [] end. get_group_name(Host, Group) -> {ok, State} = eldap_utils:get_state(Host, ?MODULE), - case cache_tab:dirty_lookup(shared_roster_ldap_group, - {Group, Host}, - fun () -> search_group_info(State, Group) end) + case ets_cache:lookup(?GROUP_CACHE, + {Group, Host}, + fun () -> search_group_info(State, Group) end) of {ok, #group_info{desc = GroupName}} when GroupName /= undefined -> @@ -361,9 +344,9 @@ get_group_name(Host, Group) -> get_user_name(User, Host) -> {ok, State} = eldap_utils:get_state(Host, ?MODULE), - case cache_tab:dirty_lookup(shared_roster_ldap_user, - {User, Host}, - fun () -> search_user_name(State, User) end) + case ets_cache:lookup(?USER_CACHE, + {User, Host}, + fun () -> search_user_name(State, User) end) of {ok, UserName} -> UserName; error -> User @@ -371,7 +354,7 @@ get_user_name(User, Host) -> search_group_info(State, Group) -> Extractor = case State#state.uid_format_re of - <<"">> -> + undefined -> fun (UID) -> catch eldap_utils:get_user_part(UID, State#state.uid_format) @@ -383,82 +366,70 @@ search_group_info(State, Group) -> end end, AuthChecker = case State#state.auth_check of - true -> fun ejabberd_auth:is_user_exists/2; + true -> fun ejabberd_auth:user_exists/2; _ -> fun (_U, _S) -> true end end, - Host = State#state.host, case eldap_search(State, [eldap_filter:do_sub(State#state.gfilter, [{<<"%g">>, Group}])], [State#state.group_attr, State#state.group_desc, State#state.uid]) of - [] -> error; + [] -> + error; LDAPEntries -> - {GroupDesc, MembersLists} = lists:foldl(fun - (#eldap_entry{attributes = - Attrs}, - {DescAcc, JIDsAcc}) -> - case - {eldap_utils:get_ldap_attr(State#state.group_attr, - Attrs), - eldap_utils:get_ldap_attr(State#state.group_desc, - Attrs), - lists:keysearch(State#state.uid, - 1, - Attrs)} - of - {ID, Desc, - {value, - {GroupMemberAttr, - Members}}} - when ID /= <<"">>, - GroupMemberAttr - == - State#state.uid -> - JIDs = - lists:foldl(fun - ({ok, - UID}, - L) -> - PUID = - jlib:nodeprep(UID), - case - PUID - of - error -> - L; - _ -> - case - AuthChecker(PUID, - Host) - of - true -> - [{PUID, - Host} - | L]; - _ -> - L - end - end; - (_, - L) -> - L - end, - [], - lists:map(Extractor, - Members)), - {Desc, - [JIDs - | JIDsAcc]}; - _ -> - {DescAcc, JIDsAcc} - end - end, - {Group, []}, LDAPEntries), - {ok, - #group_info{desc = GroupDesc, - members = lists:usort(lists:flatten(MembersLists))}} + {GroupDesc, MembersLists} = lists:foldl(fun(Entry, Acc) -> + extract_members(State, Extractor, AuthChecker, Entry, Acc) + end, + {Group, []}, LDAPEntries), + {ok, #group_info{desc = GroupDesc, members = lists:usort(lists:flatten(MembersLists))}} + end. + +get_member_jid(#state{user_jid_attr = <<>>}, UID, Host) -> + {jid:nodeprep(UID), Host}; +get_member_jid(#state{user_jid_attr = UserJIDAttr, user_uid = UIDAttr} = State, + UID, Host) -> + Entries = eldap_search(State, + [eldap_filter:do_sub(<<"(", UIDAttr/binary, "=%u)">>, + [{<<"%u">>, UID}])], + [UserJIDAttr]), + case Entries of + [#eldap_entry{attributes = [{UserJIDAttr, [MemberJID | _]}]} | _] -> + try jid:decode(MemberJID) of + #jid{luser = U, lserver = S} -> {U, S} + catch + error:{bad_jid, _} -> {error, Host} + end; + _ -> + {error, error} + end. + +extract_members(State, Extractor, AuthChecker, #eldap_entry{attributes = Attrs}, {DescAcc, JIDsAcc}) -> + Host = State#state.host, + case {eldap_utils:get_ldap_attr(State#state.group_attr, Attrs), + eldap_utils:get_ldap_attr(State#state.group_desc, Attrs), + lists:keysearch(State#state.uid, 1, Attrs)} of + {ID, Desc, {value, {GroupMemberAttr, Members}}} when ID /= <<"">>, + GroupMemberAttr == State#state.uid -> + JIDs = lists:foldl( + fun({ok, UID}, L) -> + {MemberUID, MemberHost} = get_member_jid(State, UID, Host), + case MemberUID of + error -> + L; + _ -> + case AuthChecker(MemberUID, MemberHost) of + true -> + [{MemberUID, MemberHost} | L]; + _ -> + L + end + end; + (_, L) -> L + end, [], lists:map(Extractor, Members)), + {Desc, [JIDs | JIDsAcc]}; + _ -> + {DescAcc, JIDsAcc} end. search_user_name(State, User) -> @@ -489,62 +460,24 @@ get_user_part_re(String, Pattern) -> end. parse_options(Host, Opts) -> - Eldap_ID = jlib:atom_to_binary(gen_mod:get_module_proc(Host, ?MODULE)), - Cfg = eldap_utils:get_config(Host, Opts), - GroupAttr = gen_mod:get_opt(ldap_groupattr, Opts, - fun iolist_to_binary/1, - <<"cn">>), - GroupDesc = gen_mod:get_opt(ldap_groupdesc, Opts, - fun iolist_to_binary/1, - GroupAttr), - UserDesc = gen_mod:get_opt(ldap_userdesc, Opts, - fun iolist_to_binary/1, - <<"cn">>), - UserUID = gen_mod:get_opt(ldap_useruid, Opts, - fun iolist_to_binary/1, - <<"cn">>), - UIDAttr = gen_mod:get_opt(ldap_memberattr, Opts, - fun iolist_to_binary/1, - <<"memberUid">>), - UIDAttrFormat = gen_mod:get_opt(ldap_memberattr_format, Opts, - fun iolist_to_binary/1, - <<"%u">>), - UIDAttrFormatRe = gen_mod:get_opt(ldap_memberattr_format_re, Opts, - fun(S) -> - Re = iolist_to_binary(S), - {ok, MP} = re:compile(Re), - MP - end, <<"">>), - AuthCheck = gen_mod:get_opt(ldap_auth_check, Opts, - fun(on) -> true; - (off) -> false; - (false) -> false; - (true) -> true - end, true), - UserCacheValidity = eldap_utils:get_opt( - {ldap_user_cache_validity, Host}, Opts, - fun(I) when is_integer(I), I>0 -> I end, - ?USER_CACHE_VALIDITY), - GroupCacheValidity = eldap_utils:get_opt( - {ldap_group_cache_validity, Host}, Opts, - fun(I) when is_integer(I), I>0 -> I end, - ?GROUP_CACHE_VALIDITY), - UserCacheSize = eldap_utils:get_opt( - {ldap_user_cache_size, Host}, Opts, - fun(I) when is_integer(I), I>0 -> I end, - ?CACHE_SIZE), - GroupCacheSize = eldap_utils:get_opt( - {ldap_group_cache_size, Host}, Opts, - fun(I) when is_integer(I), I>0 -> I end, - ?CACHE_SIZE), - ConfigFilter = eldap_utils:get_opt({ldap_filter, Host}, Opts, - fun check_filter/1, <<"">>), - ConfigUserFilter = eldap_utils:get_opt({ldap_ufilter, Host}, Opts, - fun check_filter/1, <<"">>), - ConfigGroupFilter = eldap_utils:get_opt({ldap_gfilter, Host}, Opts, - fun check_filter/1, <<"">>), - RosterFilter = eldap_utils:get_opt({ldap_rfilter, Host}, Opts, - fun check_filter/1, <<"">>), + Eldap_ID = misc:atom_to_binary(gen_mod:get_module_proc(Host, ?MODULE)), + Cfg = ?eldap_config(mod_shared_roster_ldap_opt, Opts), + GroupAttr = mod_shared_roster_ldap_opt:ldap_groupattr(Opts), + GroupDesc = case mod_shared_roster_ldap_opt:ldap_groupdesc(Opts) of + undefined -> GroupAttr; + GD -> GD + end, + UserDesc = mod_shared_roster_ldap_opt:ldap_userdesc(Opts), + UserUID = mod_shared_roster_ldap_opt:ldap_useruid(Opts), + UIDAttr = mod_shared_roster_ldap_opt:ldap_memberattr(Opts), + UIDAttrFormat = mod_shared_roster_ldap_opt:ldap_memberattr_format(Opts), + UIDAttrFormatRe = mod_shared_roster_ldap_opt:ldap_memberattr_format_re(Opts), + JIDAttr = mod_shared_roster_ldap_opt:ldap_userjidattr(Opts), + AuthCheck = mod_shared_roster_ldap_opt:ldap_auth_check(Opts), + ConfigFilter = mod_shared_roster_ldap_opt:ldap_filter(Opts), + ConfigUserFilter = mod_shared_roster_ldap_opt:ldap_ufilter(Opts), + ConfigGroupFilter = mod_shared_roster_ldap_opt:ldap_gfilter(Opts), + RosterFilter = mod_shared_roster_ldap_opt:ldap_rfilter(Opts), SubFilter = <<"(&(", UIDAttr/binary, "=", UIDAttrFormat/binary, ")(", GroupAttr/binary, "=%g))">>, UserSubFilter = case ConfigUserFilter of @@ -584,18 +517,276 @@ parse_options(Host, Opts) -> base = Cfg#eldap_config.base, deref_aliases = Cfg#eldap_config.deref_aliases, uid = UIDAttr, + user_jid_attr = JIDAttr, group_attr = GroupAttr, group_desc = GroupDesc, user_desc = UserDesc, user_uid = UserUID, uid_format = UIDAttrFormat, uid_format_re = UIDAttrFormatRe, filter = Filter, ufilter = UserFilter, rfilter = RosterFilter, - gfilter = GroupFilter, auth_check = AuthCheck, - user_cache_size = UserCacheSize, - user_cache_validity = UserCacheValidity, - group_cache_size = GroupCacheSize, - group_cache_validity = GroupCacheValidity}. + gfilter = GroupFilter, auth_check = AuthCheck}. -check_filter(F) -> - NewF = iolist_to_binary(F), - {ok, _} = eldap_filter:parse(NewF), - NewF. +init_cache(Host, Opts) -> + UseCache = use_cache(Host, Opts), + case UseCache of + true -> + CacheOpts = cache_opts(Host, Opts), + ets_cache:new(?USER_CACHE, CacheOpts), + ets_cache:new(?GROUP_CACHE, CacheOpts), + ets_cache:new(?DISPLAYED_CACHE, CacheOpts); + false -> + ets_cache:delete(?USER_CACHE), + ets_cache:delete(?GROUP_CACHE), + ets_cache:delete(?DISPLAYED_CACHE) + end, + UseCache. + +use_cache(_Host, Opts) -> + mod_shared_roster_ldap_opt:use_cache(Opts). + +cache_opts(_Host, Opts) -> + MaxSize = mod_shared_roster_ldap_opt:cache_size(Opts), + CacheMissed = mod_shared_roster_ldap_opt:cache_missed(Opts), + LifeTime = mod_shared_roster_ldap_opt:cache_life_time(Opts), + [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}]. + +mod_opt_type(ldap_auth_check) -> + econf:bool(); +mod_opt_type(ldap_gfilter) -> + econf:ldap_filter(); +mod_opt_type(ldap_groupattr) -> + econf:binary(); +mod_opt_type(ldap_groupdesc) -> + econf:binary(); +mod_opt_type(ldap_memberattr) -> + econf:binary(); +mod_opt_type(ldap_memberattr_format) -> + econf:binary(); +mod_opt_type(ldap_memberattr_format_re) -> + econf:re(); +mod_opt_type(ldap_rfilter) -> + econf:ldap_filter(); +mod_opt_type(ldap_ufilter) -> + econf:ldap_filter(); +mod_opt_type(ldap_userdesc) -> + econf:binary(); +mod_opt_type(ldap_useruid) -> + econf:binary(); +mod_opt_type(ldap_userjidattr) -> + econf:binary(); +mod_opt_type(ldap_backups) -> + econf:list(econf:domain(), [unique]); +mod_opt_type(ldap_base) -> + econf:binary(); +mod_opt_type(ldap_deref_aliases) -> + econf:enum([never, searching, finding, always]); +mod_opt_type(ldap_encrypt) -> + econf:enum([tls, starttls, none]); +mod_opt_type(ldap_filter) -> + econf:ldap_filter(); +mod_opt_type(ldap_password) -> + econf:binary(); +mod_opt_type(ldap_port) -> + econf:port(); +mod_opt_type(ldap_rootdn) -> + econf:binary(); +mod_opt_type(ldap_servers) -> + econf:list(econf:domain(), [unique]); +mod_opt_type(ldap_tls_cacertfile) -> + econf:pem(); +mod_opt_type(ldap_tls_certfile) -> + econf:pem(); +mod_opt_type(ldap_tls_depth) -> + econf:non_neg_int(); +mod_opt_type(ldap_tls_verify) -> + econf:enum([hard, soft, false]); +mod_opt_type(ldap_uids) -> + econf:either( + econf:list( + econf:and_then( + econf:binary(), + fun(U) -> {U, <<"%u">>} end)), + econf:map(econf:binary(), econf:binary(), [unique])); +mod_opt_type(use_cache) -> + econf:bool(); +mod_opt_type(cache_size) -> + econf:pos_int(infinity); +mod_opt_type(cache_missed) -> + econf:bool(); +mod_opt_type(cache_life_time) -> + econf:timeout(second, infinity). + +-spec mod_options(binary()) -> [{ldap_uids, [{binary(), binary()}]} | + {atom(), any()}]. +mod_options(Host) -> + [{ldap_auth_check, true}, + {ldap_gfilter, <<"">>}, + {ldap_groupattr, <<"cn">>}, + {ldap_groupdesc, undefined}, + {ldap_memberattr, <<"memberUid">>}, + {ldap_memberattr_format, <<"%u">>}, + {ldap_memberattr_format_re, undefined}, + {ldap_rfilter, <<"">>}, + {ldap_ufilter, <<"">>}, + {ldap_userdesc, <<"cn">>}, + {ldap_useruid, <<"cn">>}, + {ldap_userjidattr, <<"">>}, + {ldap_backups, ejabberd_option:ldap_backups(Host)}, + {ldap_base, ejabberd_option:ldap_base(Host)}, + {ldap_uids, ejabberd_option:ldap_uids(Host)}, + {ldap_deref_aliases, ejabberd_option:ldap_deref_aliases(Host)}, + {ldap_encrypt, ejabberd_option:ldap_encrypt(Host)}, + {ldap_password, ejabberd_option:ldap_password(Host)}, + {ldap_port, ejabberd_option:ldap_port(Host)}, + {ldap_rootdn, ejabberd_option:ldap_rootdn(Host)}, + {ldap_servers, ejabberd_option:ldap_servers(Host)}, + {ldap_filter, ejabberd_option:ldap_filter(Host)}, + {ldap_tls_certfile, ejabberd_option:ldap_tls_certfile(Host)}, + {ldap_tls_cacertfile, ejabberd_option:ldap_tls_cacertfile(Host)}, + {ldap_tls_depth, ejabberd_option:ldap_tls_depth(Host)}, + {ldap_tls_verify, ejabberd_option:ldap_tls_verify(Host)}, + {use_cache, ejabberd_option:use_cache(Host)}, + {cache_size, ejabberd_option:cache_size(Host)}, + {cache_missed, ejabberd_option:cache_missed(Host)}, + {cache_life_time, ejabberd_option:cache_life_time(Host)}]. + +mod_doc() -> + #{desc => + [?T("This module lets the server administrator automatically " + "populate users' rosters (contact lists) with entries based on " + "users and groups defined in an LDAP-based directory."), "", + ?T("NOTE: 'mod_shared_roster_ldap' depends on 'mod_roster' being " + "enabled. Roster queries will return '503' errors if " + "'mod_roster' is not enabled."), "", + ?T("The module accepts many configuration options. Some of them, " + "if unspecified, default to the values specified for the top " + "level of configuration. This lets you avoid specifying, for " + "example, the bind password in multiple places."), "", + ?T("- Filters: 'ldap_rfilter', 'ldap_ufilter', 'ldap_gfilter', " + "'ldap_filter'. These options specify LDAP filters used to " + "query for shared roster information. All of them are run " + "against the ldap_base."), + ?T("- Attributes: 'ldap_groupattr', 'ldap_groupdesc', " + "'ldap_memberattr', 'ldap_userdesc', 'ldap_useruid'. These " + "options specify the names of the attributes which hold " + "interesting data in the entries returned by running filters " + "specified with the filter options."), + ?T("- Control parameters: 'ldap_auth_check', " + "'ldap_group_cache_validity', 'ldap_memberattr_format', " + "'ldap_memberattr_format_re', 'ldap_user_cache_validity'. " + "These parameters control the behaviour of the module."), + ?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 _`ldap.md#ldap-connection|LDAP Connection`_ " + "section for more information about them."), "", + ?T("Check also the _`ldap.md#ldap-examples|Configuration examples`_ " + "section to get details about " + "retrieving the roster, " + "and configuration examples including Flat DIT and Deep DIT.")], + opts => + [ + %% Filters: + {ldap_rfilter, + #{desc => + ?T("So called \"Roster Filter\". Used to find names of " + "all \"shared roster\" groups. See also the " + "'ldap_groupattr' parameter. If unspecified, defaults to " + "the top-level parameter of the same name. You must " + "specify it in some place in the configuration, there is " + "no default.")}}, + {ldap_gfilter, + #{desc => + ?T("\"Group Filter\", used when retrieving human-readable " + "name (a.k.a. \"Display Name\") and the members of a " + "group. See also the parameters 'ldap_groupattr', " + "'ldap_groupdesc' and 'ldap_memberattr'. If unspecified, " + "defaults to the top-level parameter of the same name. " + "If that one also is unspecified, then the filter is " + "constructed exactly like \"User Filter\".")}}, + {ldap_ufilter, + #{desc => + ?T("\"User Filter\", used for retrieving the human-readable " + "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 " + "_`ldap.md#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 " + "_`ldap.md#filters|Filters`_ section.")}}, + %% Attributes: + {ldap_groupattr, + #{desc => + ?T("The name of the attribute that holds the group name, and " + "that is used to differentiate between them. Retrieved " + "from results of the \"Roster Filter\" " + "and \"Group Filter\". Defaults to 'cn'.")}}, + + {ldap_groupdesc, + #{desc => + ?T("The name of the attribute which holds the human-readable " + "group name in the objects you use to represent groups. " + "Retrieved from results of the \"Group Filter\". " + "Defaults to whatever 'ldap_groupattr' is set.")}}, + + {ldap_memberattr, + #{desc => + ?T("The name of the attribute which holds the IDs of the " + "members of a group. Retrieved from results of the " + "\"Group Filter\". Defaults to 'memberUid'. The name of " + "the attribute differs depending on the objectClass you " + "use for your group objects, for example: " + "'posixGroup' -> 'memberUid'; 'groupOfNames' -> 'member'; " + "'groupOfUniqueNames' -> 'uniqueMember'.")}}, + {ldap_userdesc, + #{desc => + ?T("The name of the attribute which holds the human-readable " + "user name. Retrieved from results of the " + "\"User Filter\". Defaults to 'cn'.")}}, + {ldap_useruid, + #{desc => + ?T("The name of the attribute which holds the ID of a roster " + "item. Value of this attribute in the roster item objects " + "needs to match the ID retrieved from the " + "'ldap_memberattr' attribute of a group object. " + "Retrieved from results of the \"User Filter\". " + "Defaults to 'cn'.")}}, + {ldap_userjidattr, + #{desc => + ?T("The name of the attribute which is used to map user id " + "to XMPP jid. If not specified (and that is default value " + "of this option), user jid will be created from user id and " + " this module host.")}}, + %% Control parameters: + {ldap_memberattr_format, + #{desc => + ?T("A globbing format for extracting user ID from the value " + "of the attribute named by 'ldap_memberattr'. Defaults " + "to '%u', which means that the whole value is the member " + "ID. If you change it to something different, you may " + "also need to specify the User and Group Filters " + "manually; see section Filters.")}}, + + {ldap_memberattr_format_re, + #{desc => + ?T("A regex for extracting user ID from the value of the " + "attribute named by 'ldap_memberattr'. Check the LDAP " + "_`ldap.md#control-parameters|Control Parameters`_ section.")}}, + {ldap_auth_check, + #{value => "true | false", + desc => + ?T("Whether the module should check (via the ejabberd " + "authentication subsystem) for existence of each user in " + "the shared LDAP roster. Set to 'false' if you want to " + "disable the check. Default value is 'true'.")}}] ++ + [{Opt, + #{desc => + {?T("Same as top-level _`~s`_ option, but " + "applied to this module only."), [Opt]}}} + || Opt <- [ldap_backups, ldap_base, ldap_uids, ldap_deref_aliases, + ldap_encrypt, ldap_password, ldap_port, ldap_rootdn, + ldap_servers, ldap_tls_certfile, ldap_tls_cacertfile, + ldap_tls_depth, ldap_tls_verify, use_cache, cache_size, + cache_missed, cache_life_time]]}. diff --git a/src/mod_shared_roster_ldap_opt.erl b/src/mod_shared_roster_ldap_opt.erl new file mode 100644 index 000000000..d4657222e --- /dev/null +++ b/src/mod_shared_roster_ldap_opt.erl @@ -0,0 +1,216 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_shared_roster_ldap_opt). + +-export([cache_life_time/1]). +-export([cache_missed/1]). +-export([cache_size/1]). +-export([ldap_auth_check/1]). +-export([ldap_backups/1]). +-export([ldap_base/1]). +-export([ldap_deref_aliases/1]). +-export([ldap_encrypt/1]). +-export([ldap_filter/1]). +-export([ldap_gfilter/1]). +-export([ldap_groupattr/1]). +-export([ldap_groupdesc/1]). +-export([ldap_memberattr/1]). +-export([ldap_memberattr_format/1]). +-export([ldap_memberattr_format_re/1]). +-export([ldap_password/1]). +-export([ldap_port/1]). +-export([ldap_rfilter/1]). +-export([ldap_rootdn/1]). +-export([ldap_servers/1]). +-export([ldap_tls_cacertfile/1]). +-export([ldap_tls_certfile/1]). +-export([ldap_tls_depth/1]). +-export([ldap_tls_verify/1]). +-export([ldap_ufilter/1]). +-export([ldap_uids/1]). +-export([ldap_userdesc/1]). +-export([ldap_userjidattr/1]). +-export([ldap_useruid/1]). +-export([use_cache/1]). + +-spec cache_life_time(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +cache_life_time(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_life_time, Opts); +cache_life_time(Host) -> + gen_mod:get_module_opt(Host, mod_shared_roster_ldap, cache_life_time). + +-spec cache_missed(gen_mod:opts() | global | binary()) -> boolean(). +cache_missed(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_missed, Opts); +cache_missed(Host) -> + gen_mod:get_module_opt(Host, mod_shared_roster_ldap, cache_missed). + +-spec cache_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +cache_size(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_size, Opts); +cache_size(Host) -> + gen_mod:get_module_opt(Host, mod_shared_roster_ldap, cache_size). + +-spec ldap_auth_check(gen_mod:opts() | global | binary()) -> boolean(). +ldap_auth_check(Opts) when is_map(Opts) -> + gen_mod:get_opt(ldap_auth_check, Opts); +ldap_auth_check(Host) -> + gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_auth_check). + +-spec ldap_backups(gen_mod:opts() | global | binary()) -> [binary()]. +ldap_backups(Opts) when is_map(Opts) -> + gen_mod:get_opt(ldap_backups, Opts); +ldap_backups(Host) -> + gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_backups). + +-spec ldap_base(gen_mod:opts() | global | binary()) -> binary(). +ldap_base(Opts) when is_map(Opts) -> + gen_mod:get_opt(ldap_base, Opts); +ldap_base(Host) -> + gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_base). + +-spec ldap_deref_aliases(gen_mod:opts() | global | binary()) -> 'always' | 'finding' | 'never' | 'searching'. +ldap_deref_aliases(Opts) when is_map(Opts) -> + gen_mod:get_opt(ldap_deref_aliases, Opts); +ldap_deref_aliases(Host) -> + gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_deref_aliases). + +-spec ldap_encrypt(gen_mod:opts() | global | binary()) -> 'none' | 'starttls' | 'tls'. +ldap_encrypt(Opts) when is_map(Opts) -> + gen_mod:get_opt(ldap_encrypt, Opts); +ldap_encrypt(Host) -> + gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_encrypt). + +-spec ldap_filter(gen_mod:opts() | global | binary()) -> binary(). +ldap_filter(Opts) when is_map(Opts) -> + gen_mod:get_opt(ldap_filter, Opts); +ldap_filter(Host) -> + gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_filter). + +-spec ldap_gfilter(gen_mod:opts() | global | binary()) -> binary(). +ldap_gfilter(Opts) when is_map(Opts) -> + gen_mod:get_opt(ldap_gfilter, Opts); +ldap_gfilter(Host) -> + gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_gfilter). + +-spec ldap_groupattr(gen_mod:opts() | global | binary()) -> binary(). +ldap_groupattr(Opts) when is_map(Opts) -> + gen_mod:get_opt(ldap_groupattr, Opts); +ldap_groupattr(Host) -> + gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_groupattr). + +-spec ldap_groupdesc(gen_mod:opts() | global | binary()) -> 'undefined' | binary(). +ldap_groupdesc(Opts) when is_map(Opts) -> + gen_mod:get_opt(ldap_groupdesc, Opts); +ldap_groupdesc(Host) -> + gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_groupdesc). + +-spec ldap_memberattr(gen_mod:opts() | global | binary()) -> binary(). +ldap_memberattr(Opts) when is_map(Opts) -> + gen_mod:get_opt(ldap_memberattr, Opts); +ldap_memberattr(Host) -> + gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_memberattr). + +-spec ldap_memberattr_format(gen_mod:opts() | global | binary()) -> binary(). +ldap_memberattr_format(Opts) when is_map(Opts) -> + gen_mod:get_opt(ldap_memberattr_format, Opts); +ldap_memberattr_format(Host) -> + gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_memberattr_format). + +-spec ldap_memberattr_format_re(gen_mod:opts() | global | binary()) -> 'undefined' | misc:re_mp(). +ldap_memberattr_format_re(Opts) when is_map(Opts) -> + gen_mod:get_opt(ldap_memberattr_format_re, Opts); +ldap_memberattr_format_re(Host) -> + gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_memberattr_format_re). + +-spec ldap_password(gen_mod:opts() | global | binary()) -> binary(). +ldap_password(Opts) when is_map(Opts) -> + gen_mod:get_opt(ldap_password, Opts); +ldap_password(Host) -> + gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_password). + +-spec ldap_port(gen_mod:opts() | global | binary()) -> 1..1114111. +ldap_port(Opts) when is_map(Opts) -> + gen_mod:get_opt(ldap_port, Opts); +ldap_port(Host) -> + gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_port). + +-spec ldap_rfilter(gen_mod:opts() | global | binary()) -> binary(). +ldap_rfilter(Opts) when is_map(Opts) -> + gen_mod:get_opt(ldap_rfilter, Opts); +ldap_rfilter(Host) -> + gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_rfilter). + +-spec ldap_rootdn(gen_mod:opts() | global | binary()) -> binary(). +ldap_rootdn(Opts) when is_map(Opts) -> + gen_mod:get_opt(ldap_rootdn, Opts); +ldap_rootdn(Host) -> + gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_rootdn). + +-spec ldap_servers(gen_mod:opts() | global | binary()) -> [binary()]. +ldap_servers(Opts) when is_map(Opts) -> + gen_mod:get_opt(ldap_servers, Opts); +ldap_servers(Host) -> + gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_servers). + +-spec ldap_tls_cacertfile(gen_mod:opts() | global | binary()) -> binary(). +ldap_tls_cacertfile(Opts) when is_map(Opts) -> + gen_mod:get_opt(ldap_tls_cacertfile, Opts); +ldap_tls_cacertfile(Host) -> + gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_tls_cacertfile). + +-spec ldap_tls_certfile(gen_mod:opts() | global | binary()) -> binary(). +ldap_tls_certfile(Opts) when is_map(Opts) -> + gen_mod:get_opt(ldap_tls_certfile, Opts); +ldap_tls_certfile(Host) -> + gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_tls_certfile). + +-spec ldap_tls_depth(gen_mod:opts() | global | binary()) -> non_neg_integer(). +ldap_tls_depth(Opts) when is_map(Opts) -> + gen_mod:get_opt(ldap_tls_depth, Opts); +ldap_tls_depth(Host) -> + gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_tls_depth). + +-spec ldap_tls_verify(gen_mod:opts() | global | binary()) -> 'false' | 'hard' | 'soft'. +ldap_tls_verify(Opts) when is_map(Opts) -> + gen_mod:get_opt(ldap_tls_verify, Opts); +ldap_tls_verify(Host) -> + gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_tls_verify). + +-spec ldap_ufilter(gen_mod:opts() | global | binary()) -> binary(). +ldap_ufilter(Opts) when is_map(Opts) -> + gen_mod:get_opt(ldap_ufilter, Opts); +ldap_ufilter(Host) -> + gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_ufilter). + +-spec ldap_uids(gen_mod:opts() | global | binary()) -> [{binary(),binary()}]. +ldap_uids(Opts) when is_map(Opts) -> + gen_mod:get_opt(ldap_uids, Opts); +ldap_uids(Host) -> + gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_uids). + +-spec ldap_userdesc(gen_mod:opts() | global | binary()) -> binary(). +ldap_userdesc(Opts) when is_map(Opts) -> + gen_mod:get_opt(ldap_userdesc, Opts); +ldap_userdesc(Host) -> + gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_userdesc). + +-spec ldap_userjidattr(gen_mod:opts() | global | binary()) -> binary(). +ldap_userjidattr(Opts) when is_map(Opts) -> + gen_mod:get_opt(ldap_userjidattr, Opts); +ldap_userjidattr(Host) -> + gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_userjidattr). + +-spec ldap_useruid(gen_mod:opts() | global | binary()) -> binary(). +ldap_useruid(Opts) when is_map(Opts) -> + gen_mod:get_opt(ldap_useruid, Opts); +ldap_useruid(Host) -> + gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_useruid). + +-spec use_cache(gen_mod:opts() | global | binary()) -> boolean(). +use_cache(Opts) when is_map(Opts) -> + gen_mod:get_opt(use_cache, Opts); +use_cache(Host) -> + gen_mod:get_module_opt(Host, mod_shared_roster_ldap, use_cache). + diff --git a/src/mod_shared_roster_mnesia.erl b/src/mod_shared_roster_mnesia.erl new file mode 100644 index 000000000..028584459 --- /dev/null +++ b/src/mod_shared_roster_mnesia.erl @@ -0,0 +1,181 @@ +%%%------------------------------------------------------------------- +%%% File : mod_shared_roster_mnesia.erl +%%% Author : Evgeny Khramtsov +%%% Created : 14 Apr 2016 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_shared_roster_mnesia). + +-behaviour(mod_shared_roster). + +%% API +-export([init/2, list_groups/1, groups_with_opts/1, create_group/3, + delete_group/2, get_group_opts/2, set_group_opts/3, + get_user_groups/2, get_group_explicit_users/2, + get_user_displayed_groups/3, is_user_in_group/3, + add_user_to_group/3, remove_user_from_group/3, import/3]). +-export([need_transform/1, transform/1, use_cache/1]). + +-include("mod_roster.hrl"). +-include("mod_shared_roster.hrl"). +-include("logger.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + ejabberd_mnesia:create(?MODULE, sr_group, + [{disc_copies, [node()]}, + {attributes, record_info(fields, sr_group)}]), + ejabberd_mnesia:create(?MODULE, sr_user, + [{disc_copies, [node()]}, {type, bag}, + {attributes, record_info(fields, sr_user)}, + {index, [group_host]}]). + +list_groups(Host) -> + mnesia:dirty_select(sr_group, + [{#sr_group{group_host = {'$1', '$2'}, _ = '_'}, + [{'==', '$2', Host}], ['$1']}]). + +-spec use_cache(binary()) -> boolean(). +use_cache(_Host) -> + false. + +groups_with_opts(Host) -> + Gs = mnesia:dirty_select(sr_group, + [{#sr_group{group_host = {'$1', Host}, opts = '$2', + _ = '_'}, + [], [['$1', '$2']]}]), + lists:map(fun ([G, O]) -> {G, O} end, Gs). + +create_group(Host, Group, Opts) -> + R = #sr_group{group_host = {Group, Host}, opts = Opts}, + F = fun () -> mnesia:write(R) end, + mnesia:transaction(F). + +delete_group(Host, Group) -> + GroupHost = {Group, Host}, + F = fun () -> + mnesia:delete({sr_group, GroupHost}), + Users = mnesia:index_read(sr_user, GroupHost, + #sr_user.group_host), + lists:foreach(fun (UserEntry) -> + mnesia:delete_object(UserEntry) + end, + Users) + end, + mnesia:transaction(F). + +get_group_opts(Host, Group) -> + case catch mnesia:dirty_read(sr_group, {Group, Host}) of + [#sr_group{opts = Opts}] -> {ok, Opts}; + _ -> error + end. + +set_group_opts(Host, Group, Opts) -> + R = #sr_group{group_host = {Group, Host}, opts = Opts}, + F = fun () -> mnesia:write(R) end, + mnesia:transaction(F). + +get_user_groups(US, Host) -> + case catch mnesia:dirty_read(sr_user, US) of + Rs when is_list(Rs) -> + [Group || #sr_user{group_host = {Group, H}} <- Rs, H == Host]; + _ -> + [] + end. + +get_group_explicit_users(Host, Group) -> + Read = (catch mnesia:dirty_index_read(sr_user, + {Group, Host}, #sr_user.group_host)), + case Read of + Rs when is_list(Rs) -> [R#sr_user.us || R <- Rs]; + _ -> [] + end. + +get_user_displayed_groups(LUser, LServer, GroupsOpts) -> + case catch mnesia:dirty_read(sr_user, {LUser, LServer}) of + Rs when is_list(Rs) -> + [{Group, proplists:get_value(Group, GroupsOpts, [])} + || #sr_user{group_host = {Group, H}} <- Rs, + H == LServer]; + _ -> + [] + end. + +is_user_in_group(US, Group, Host) -> + case mnesia:dirty_match_object( + #sr_user{us = US, group_host = {Group, Host}}) of + [] -> false; + _ -> true + end. + +add_user_to_group(Host, US, Group) -> + R = #sr_user{us = US, group_host = {Group, Host}}, + F = fun () -> mnesia:write(R) end, + mnesia:transaction(F). + +remove_user_from_group(Host, US, Group) -> + R = #sr_user{us = US, group_host = {Group, Host}}, + F = fun () -> mnesia:delete_object(R) end, + mnesia:transaction(F). + +import(LServer, <<"sr_group">>, [Group, SOpts, _TimeStamp]) -> + G = #sr_group{group_host = {Group, LServer}, + opts = ejabberd_sql:decode_term(SOpts)}, + mnesia:dirty_write(G); +import(LServer, <<"sr_user">>, [SJID, Group, _TimeStamp]) -> + #jid{luser = U, lserver = S} = jid:decode(SJID), + User = #sr_user{us = {U, S}, group_host = {Group, LServer}}, + mnesia:dirty_write(User). + +need_transform({sr_group, {G, H}, _}) + when is_list(G) orelse is_list(H) -> + ?INFO_MSG("Mnesia table 'sr_group' will be converted to binary", []), + true; +need_transform({sr_user, {U, S}, {G, H}}) + when is_list(U) orelse is_list(S) orelse is_list(G) orelse is_list(H) -> + ?INFO_MSG("Mnesia table 'sr_user' will be converted to binary", []), + true; +need_transform({sr_group, {_, _}, [{name, _} | _]}) -> + ?INFO_MSG("Mnesia table 'sr_group' will be converted from option Name to Label", []), + true; +need_transform(_) -> + false. + +transform(#sr_group{group_host = {G, _H}, opts = Opts} = R) + when is_binary(G) -> + Opts2 = case proplists:get_value(name, Opts, false) of + false -> Opts; + Name -> [{label, Name} | proplists:delete(name, Opts)] + end, + R#sr_group{opts = Opts2}; +transform(#sr_group{group_host = {G, H}, opts = Opts} = R) -> + R#sr_group{group_host = {iolist_to_binary(G), iolist_to_binary(H)}, + opts = mod_shared_roster:opts_to_binary(Opts)}; +transform(#sr_user{us = {U, S}, group_host = {G, H}} = R) -> + R#sr_user{us = {iolist_to_binary(U), iolist_to_binary(S)}, + group_host = {iolist_to_binary(G), iolist_to_binary(H)}}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== diff --git a/src/mod_shared_roster_opt.erl b/src/mod_shared_roster_opt.erl new file mode 100644 index 000000000..825196e2c --- /dev/null +++ b/src/mod_shared_roster_opt.erl @@ -0,0 +1,41 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_shared_roster_opt). + +-export([cache_life_time/1]). +-export([cache_missed/1]). +-export([cache_size/1]). +-export([db_type/1]). +-export([use_cache/1]). + +-spec cache_life_time(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +cache_life_time(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_life_time, Opts); +cache_life_time(Host) -> + gen_mod:get_module_opt(Host, mod_shared_roster, cache_life_time). + +-spec cache_missed(gen_mod:opts() | global | binary()) -> boolean(). +cache_missed(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_missed, Opts); +cache_missed(Host) -> + gen_mod:get_module_opt(Host, mod_shared_roster, cache_missed). + +-spec cache_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +cache_size(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_size, Opts); +cache_size(Host) -> + gen_mod:get_module_opt(Host, mod_shared_roster, cache_size). + +-spec db_type(gen_mod:opts() | global | binary()) -> atom(). +db_type(Opts) when is_map(Opts) -> + gen_mod:get_opt(db_type, Opts); +db_type(Host) -> + gen_mod:get_module_opt(Host, mod_shared_roster, db_type). + +-spec use_cache(gen_mod:opts() | global | binary()) -> boolean(). +use_cache(Opts) when is_map(Opts) -> + gen_mod:get_opt(use_cache, Opts); +use_cache(Host) -> + gen_mod:get_module_opt(Host, mod_shared_roster, use_cache). + diff --git a/src/mod_shared_roster_sql.erl b/src/mod_shared_roster_sql.erl new file mode 100644 index 000000000..4d582a1bf --- /dev/null +++ b/src/mod_shared_roster_sql.erl @@ -0,0 +1,249 @@ +%%%------------------------------------------------------------------- +%%% File : mod_shared_roster_sql.erl +%%% Author : Evgeny Khramtsov +%%% Created : 14 Apr 2016 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_shared_roster_sql). + + +-behaviour(mod_shared_roster). + +%% API +-export([init/2, list_groups/1, groups_with_opts/1, create_group/3, + delete_group/2, get_group_opts/2, set_group_opts/3, + get_user_groups/2, get_group_explicit_users/2, + get_user_displayed_groups/3, is_user_in_group/3, + add_user_to_group/3, remove_user_from_group/3, import/3, + export/1]). +-export([sql_schemas/0]). + +-include_lib("xmpp/include/jid.hrl"). +-include("mod_roster.hrl"). +-include("mod_shared_roster.hrl"). +-include("ejabberd_sql_pt.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(Host, _Opts) -> + ejabberd_sql_schema:update_schema(Host, ?MODULE, sql_schemas()), + ok. + +sql_schemas() -> + [#sql_schema{ + version = 1, + tables = + [#sql_table{ + name = <<"sr_group">>, + columns = + [#sql_column{name = <<"name">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"opts">>, type = text}, + #sql_column{name = <<"created_at">>, type = timestamp, + default = true}], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"name">>], + unique = true}]}, + #sql_table{ + name = <<"sr_user">>, + columns = + [#sql_column{name = <<"jid">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"grp">>, type = text}, + #sql_column{name = <<"created_at">>, type = timestamp, + default = true}], + indices = [#sql_index{ + columns = [<<"server_host">>, + <<"jid">>, <<"grp">>], + unique = true}, + #sql_index{ + columns = [<<"server_host">>, <<"grp">>]}]}]}]. + +list_groups(Host) -> + case ejabberd_sql:sql_query( + Host, + ?SQL("select @(name)s from sr_group where %(Host)H")) of + {selected, Rs} -> [G || {G} <- Rs]; + _ -> [] + end. + +groups_with_opts(Host) -> + case ejabberd_sql:sql_query( + Host, + ?SQL("select @(name)s, @(opts)s from sr_group where %(Host)H")) + of + {selected, Rs} -> + [{G, mod_shared_roster:opts_to_binary(ejabberd_sql:decode_term(Opts))} + || {G, Opts} <- Rs]; + _ -> [] + end. + +create_group(Host, Group, Opts) -> + SOpts = misc:term_to_expr(Opts), + F = fun () -> + ?SQL_UPSERT_T( + "sr_group", + ["!name=%(Group)s", + "!server_host=%(Host)s", + "opts=%(SOpts)s"]) + end, + ejabberd_sql:sql_transaction(Host, F). + +delete_group(Host, Group) -> + F = fun () -> + ejabberd_sql:sql_query_t( + ?SQL("delete from sr_group where name=%(Group)s and %(Host)H")), + ejabberd_sql:sql_query_t( + ?SQL("delete from sr_user where grp=%(Group)s and %(Host)H")) + end, + case ejabberd_sql:sql_transaction(Host, F) of + {atomic,{updated,_}} -> {atomic, ok}; + Res -> Res + end. + +get_group_opts(Host, Group) -> + case catch ejabberd_sql:sql_query( + Host, + ?SQL("select @(opts)s from sr_group" + " where name=%(Group)s and %(Host)H")) of + {selected, [{SOpts}]} -> + {ok, mod_shared_roster:opts_to_binary(ejabberd_sql:decode_term(SOpts))}; + _ -> error + end. + +set_group_opts(Host, Group, Opts) -> + SOpts = misc:term_to_expr(Opts), + F = fun () -> + ?SQL_UPSERT_T( + "sr_group", + ["!name=%(Group)s", + "!server_host=%(Host)s", + "opts=%(SOpts)s"]) + end, + ejabberd_sql:sql_transaction(Host, F). + +get_user_groups(US, Host) -> + SJID = make_jid_s(US), + case catch ejabberd_sql:sql_query( + Host, + ?SQL("select @(grp)s from sr_user" + " where jid=%(SJID)s and %(Host)H")) of + {selected, Rs} -> [G || {G} <- Rs]; + _ -> [] + end. + +get_group_explicit_users(Host, Group) -> + case catch ejabberd_sql:sql_query( + Host, + ?SQL("select @(jid)s from sr_user" + " where grp=%(Group)s and %(Host)H")) of + {selected, Rs} -> + lists:map( + fun({JID}) -> + {U, S, _} = jid:tolower(jid:decode(JID)), + {U, S} + end, Rs); + _ -> + [] + end. + +get_user_displayed_groups(LUser, LServer, GroupsOpts) -> + SJID = make_jid_s(LUser, LServer), + case catch ejabberd_sql:sql_query( + LServer, + ?SQL("select @(grp)s from sr_user" + " where jid=%(SJID)s and %(LServer)H")) of + {selected, Rs} -> + [{Group, proplists:get_value(Group, GroupsOpts, [])} + || {Group} <- Rs]; + _ -> [] + end. + +is_user_in_group(US, Group, Host) -> + SJID = make_jid_s(US), + case catch ejabberd_sql:sql_query( + Host, + ?SQL("select @(jid)s from sr_user where jid=%(SJID)s" + " and %(Host)H and grp=%(Group)s")) of + {selected, []} -> false; + _ -> true + end. + +add_user_to_group(Host, US, Group) -> + SJID = make_jid_s(US), + ejabberd_sql:sql_query( + Host, + ?SQL_INSERT( + "sr_user", + ["jid=%(SJID)s", + "server_host=%(Host)s", + "grp=%(Group)s"])). + +remove_user_from_group(Host, US, Group) -> + SJID = make_jid_s(US), + F = fun () -> + ejabberd_sql:sql_query_t( + ?SQL("delete from sr_user where jid=%(SJID)s and %(Host)H" + " and grp=%(Group)s")), + ok + end, + ejabberd_sql:sql_transaction(Host, F). + +export(_Server) -> + [{sr_group, + fun(Host, #sr_group{group_host = {Group, LServer}, opts = Opts}) + when LServer == Host -> + SOpts = misc:term_to_expr(Opts), + [?SQL("delete from sr_group where name=%(Group)s and %(Host)H;"), + ?SQL_INSERT( + "sr_group", + ["name=%(Group)s", + "server_host=%(Host)s", + "opts=%(SOpts)s"])]; + (_Host, _R) -> + [] + end}, + {sr_user, + fun(Host, #sr_user{us = {U, S}, group_host = {Group, LServer}}) + when LServer == Host -> + SJID = make_jid_s(U, S), + [?SQL("select @(jid)s from sr_user where jid=%(SJID)s" + " and %(Host)H and grp=%(Group)s;"), + ?SQL_INSERT( + "sr_user", + ["jid=%(SJID)s", + "server_host=%(Host)s", + "grp=%(Group)s"])]; + (_Host, _R) -> + [] + end}]. + +import(_, _, _) -> + ok. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +make_jid_s(U, S) -> + jid:encode(jid:tolower(jid:make(U, S))). + +make_jid_s({U, S}) -> make_jid_s(U, S). diff --git a/src/mod_sic.erl b/src/mod_sic.erl index ed44f8500..f781c1690 100644 --- a/src/mod_sic.erl +++ b/src/mod_sic.erl @@ -5,7 +5,7 @@ %%% Created : 6 Mar 2010 by Karim Gemayel %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -25,67 +25,78 @@ -module(mod_sic). +-protocol({xep, 279, '0.2', '2.1.3', "complete", ""}). + -author('karim.gemayel@process-one.net'). -behaviour(gen_mod). --export([start/2, stop/1, process_local_iq/3, - process_sm_iq/3]). +-export([start/2, stop/1, reload/3, process_local_iq/1, + process_sm_iq/1, mod_options/1, depends/2, mod_doc/0]). --include("ejabberd.hrl"). -include("logger.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). +-include("translate.hrl"). --include("jlib.hrl"). +start(_Host, _Opts) -> + {ok, [{iq_handler, ejabberd_local, ?NS_SIC_0, process_local_iq}, + {iq_handler, ejabberd_sm, ?NS_SIC_0, process_sm_iq}, + {iq_handler, ejabberd_local, ?NS_SIC_1, process_local_iq}, + {iq_handler, ejabberd_sm, ?NS_SIC_1, process_sm_iq}]}. --define(NS_SIC, <<"urn:xmpp:sic:0">>). +stop(_Host) -> + ok. -start(Host, Opts) -> - IQDisc = gen_mod:get_opt(iqdisc, Opts, fun gen_iq_handler:check_type/1, - one_queue), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, - ?NS_SIC, ?MODULE, process_local_iq, IQDisc), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, - ?NS_SIC, ?MODULE, process_sm_iq, IQDisc). +reload(_Host, _NewOpts, _OldOpts) -> + ok. -stop(Host) -> - gen_iq_handler:remove_iq_handler(ejabberd_local, Host, - ?NS_SIC), - gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, - ?NS_SIC). +depends(_Host, _Opts) -> + []. -process_local_iq(#jid{user = User, server = Server, - resource = Resource}, - _To, #iq{type = get, sub_el = _SubEl} = IQ) -> +process_local_iq(#iq{from = #jid{user = User, server = Server, + resource = Resource}, + type = get} = IQ) -> get_ip({User, Server, Resource}, IQ); -process_local_iq(_From, _To, - #iq{type = set, sub_el = SubEl} = IQ) -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}. +process_local_iq(#iq{type = set, lang = Lang} = IQ) -> + Txt = ?T("Value 'set' of 'type' attribute is not allowed"), + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)). -process_sm_iq(#jid{user = User, server = Server, - resource = Resource}, - #jid{user = User, server = Server}, - #iq{type = get, sub_el = _SubEl} = IQ) -> +process_sm_iq(#iq{from = #jid{user = User, server = Server, + resource = Resource}, + to = #jid{user = User, server = Server}, + type = get} = IQ) -> get_ip({User, Server, Resource}, IQ); -process_sm_iq(_From, _To, - #iq{type = get, sub_el = SubEl} = IQ) -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_FORBIDDEN]}; -process_sm_iq(_From, _To, - #iq{type = set, sub_el = SubEl} = IQ) -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}. +process_sm_iq(#iq{type = get, lang = Lang} = IQ) -> + Txt = ?T("Query to another users is forbidden"), + xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)); +process_sm_iq(#iq{type = set, lang = Lang} = IQ) -> + Txt = ?T("Value 'set' of 'type' attribute is not allowed"), + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)). get_ip({User, Server, Resource}, - #iq{sub_el = - #xmlel{name = Name, attrs = Attrs} = SubEl} = - IQ) -> + #iq{lang = Lang, sub_els = [#sic{xmlns = NS}]} = IQ) -> case ejabberd_sm:get_user_ip(User, Server, Resource) of - {IP, _} when is_tuple(IP) -> - IQ#iq{type = result, - sub_el = - [#xmlel{name = Name, attrs = Attrs, - children = - [{xmlcdata, - iolist_to_binary(jlib:ip_to_list(IP))}]}]}; - _ -> - IQ#iq{type = error, - sub_el = [SubEl, ?ERR_INTERNAL_SERVER_ERROR]} + {IP, Port} when is_tuple(IP) -> + Result = case NS of + ?NS_SIC_0 -> #sic{ip = IP, xmlns = NS}; + ?NS_SIC_1 -> #sic{ip = IP, port = Port, xmlns = NS} + end, + xmpp:make_iq_result(IQ, Result); + _ -> + Txt = ?T("User session not found"), + xmpp:make_error(IQ, xmpp:err_item_not_found(Txt, Lang)) end. + +mod_options(_Host) -> + []. + +mod_doc() -> + #{desc => + [?T("This module adds support for " + "https://xmpp.org/extensions/xep-0279.html" + "[XEP-0279: Server IP Check]. This protocol enables " + "a client to discover its external IP address."), "", + ?T("WARNING: The protocol extension is deferred and seems " + "like there are no clients supporting it, so using this " + "module is not recommended and, furthermore, the module " + "might be removed in the future.")]}. diff --git a/src/mod_sip.erl b/src/mod_sip.erl index f7f2b8ed0..aa98be2cc 100644 --- a/src/mod_sip.erl +++ b/src/mod_sip.erl @@ -1,12 +1,11 @@ %%%------------------------------------------------------------------- -%%% @author Evgeny Khramtsov -%%% @copyright (C) 2014, Evgeny Khramtsov -%%% @doc -%%% -%%% @end +%%% File : mod_sip.erl +%%% Author : Evgeny Khramtsov +%%% Purpose : SIP RFC-3261 %%% Created : 21 Apr 2014 by Evgeny Khramtsov %%% -%%% ejabberd, Copyright (C) 2014-2015 ProcessOne +%%% +%%% ejabberd, Copyright (C) 2014-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -21,22 +20,41 @@ %%% 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_sip). +-module(mod_sip). +-protocol({rfc, 3261}). + +-include("logger.hrl"). +-include("translate.hrl"). + +-ifndef(SIP). +-export([start/2, stop/1, depends/2, mod_options/1, mod_doc/0]). +start(_, _) -> + ?CRITICAL_MSG("ejabberd is not compiled with SIP support", []), + {error, sip_not_compiled}. +stop(_) -> + ok. +depends(_, _) -> + []. +mod_options(_) -> + []. +mod_doc() -> + #{desc => [?T("SIP support has not been enabled.")]}. +-else. -behaviour(gen_mod). -behaviour(esip). %% API --export([start/2, stop/1, make_response/2, is_my_host/1, at_my_host/1]). +-export([start/2, stop/1, reload/3, + make_response/2, is_my_host/1, at_my_host/1]). -%% esip_callbacks --export([data_in/2, data_out/2, message_in/2, message_out/2, - request/2, request/3, response/2, locate/1]). +-export([data_in/2, data_out/2, message_in/2, + message_out/2, request/2, request/3, response/2, + locate/1, mod_opt_type/1, mod_options/1, depends/2, + mod_doc/0]). --include("ejabberd.hrl"). --include("logger.hrl"). -include_lib("esip/include/esip.hrl"). %%%=================================================================== @@ -46,7 +64,8 @@ start(_Host, _Opts) -> ejabberd:start_app(esip), esip:set_config_value(max_server_transactions, 10000), esip:set_config_value(max_client_transactions, 10000), - esip:set_config_value(software, <<"ejabberd ", (?VERSION)/binary>>), + esip:set_config_value( + software, <<"ejabberd ", (ejabberd_option:version())/binary>>), esip:set_config_value(module, ?MODULE), Spec = {mod_sip_registrar, {mod_sip_registrar, start_link, []}, transient, 2000, worker, [mod_sip_registrar]}, @@ -54,18 +73,24 @@ start(_Host, _Opts) -> {ejabberd_tmp_sup, start_link, [mod_sip_proxy_sup, mod_sip_proxy]}, permanent, infinity, supervisor, [ejabberd_tmp_sup]}, - supervisor:start_child(ejabberd_sup, Spec), - supervisor:start_child(ejabberd_sup, TmpSupSpec), + supervisor:start_child(ejabberd_gen_mod_sup, Spec), + supervisor:start_child(ejabberd_gen_mod_sup, TmpSupSpec), ok. stop(_Host) -> ok. +reload(_Host, _NewOpts, _OldOpts) -> + ok. + +depends(_Host, _Opts) -> + []. + data_in(Data, #sip_socket{type = Transport, addr = {MyIP, MyPort}, peer = {PeerIP, PeerPort}}) -> ?DEBUG( - "SIP [~p/in] ~s:~p -> ~s:~p:~n~s", + "SIP [~p/in] ~ts:~p -> ~ts:~p:~n~ts", [Transport, inet_parse:ntoa(PeerIP), PeerPort, inet_parse:ntoa(MyIP), MyPort, Data]). @@ -73,7 +98,7 @@ data_out(Data, #sip_socket{type = Transport, addr = {MyIP, MyPort}, peer = {PeerIP, PeerPort}}) -> ?DEBUG( - "SIP [~p/out] ~s:~p -> ~s:~p:~n~s", + "SIP [~p/out] ~ts:~p -> ~ts:~p:~n~ts", [Transport, inet_parse:ntoa(MyIP), MyPort, inet_parse:ntoa(PeerIP), PeerPort, Data]). @@ -130,7 +155,7 @@ request(Req, SIPSock, TrID, Action) -> mod_sip_proxy:route(Req, SIPSock, TrID, Pid), {mod_sip_proxy, route, [Pid]}; Err -> - ?INFO_MSG("failed to proxy request ~p: ~p", [Req, Err]), + ?WARNING_MSG("Failed to proxy request ~p: ~p", [Req, Err]), Err end; {proxy_auth, LServer} -> @@ -159,8 +184,8 @@ locate(_SIPMsg) -> ok. find(#uri{user = User, host = Host}) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Host), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Host), if LUser == <<"">> -> to_me; true -> @@ -191,7 +216,7 @@ action(#sip{method = <<"REGISTER">>, type = request, hdrs = Hdrs, true -> register; false -> - {auth, jlib:nameprep(ToURI#uri.host)} + {auth, jid:nameprep(ToURI#uri.host)} end; false -> deny @@ -222,7 +247,7 @@ action(#sip{method = Method, hdrs = Hdrs, type = request} = Req, SIPSock) -> true -> find(ToURI); false -> - LServer = jlib:nameprep(FromURI#uri.host), + LServer = jid:nameprep(FromURI#uri.host), {relay, LServer} end; false -> @@ -249,8 +274,8 @@ check_auth(#sip{method = Method, hdrs = Hdrs, body = Body}, AuthHdr, _SIPSock) - from end, {_, #uri{user = User, host = Host}, _} = esip:get_hdr(Issuer, Hdrs), - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Host), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Host), case lists:filter( fun({_, Params}) -> Username = esip:get_param(<<"username">>, Params), @@ -262,8 +287,12 @@ check_auth(#sip{method = Method, hdrs = Hdrs, body = Body}, AuthHdr, _SIPSock) - case ejabberd_auth:get_password_s(LUser, LServer) of <<"">> -> false; - Password -> - esip:check_auth(Auth, Method, Body, Password) + Password when is_binary(Password) -> + esip:check_auth(Auth, Method, Body, Password); + _ScramedPassword -> + ?ERROR_MSG("Unable to authenticate ~ts@~ts against SCRAM'ed " + "password", [LUser, LServer]), + false end; [] -> false @@ -294,7 +323,131 @@ make_response(Req, Resp) -> esip:make_response(Req, Resp, esip:make_tag()). at_my_host(#uri{host = Host}) -> - is_my_host(jlib:nameprep(Host)). + is_my_host(jid:nameprep(Host)). is_my_host(LServer) -> gen_mod:is_loaded(LServer, ?MODULE). + +mod_opt_type(always_record_route) -> + econf:bool(); +mod_opt_type(flow_timeout_tcp) -> + econf:timeout(second); +mod_opt_type(flow_timeout_udp) -> + econf:timeout(second); +mod_opt_type(record_route) -> + econf:sip_uri(); +mod_opt_type(routes) -> + econf:list(econf:sip_uri()); +mod_opt_type(via) -> + econf:list( + fun(L) when is_list(L) -> + (econf:and_then( + econf:options( + #{type => econf:enum([tcp, tls, udp]), + host => econf:domain(), + port => econf:port()}, + [{required, [type, host]}]), + fun(Opts) -> + Type = proplists:get_value(type, Opts), + Host = proplists:get_value(host, Opts), + Port = proplists:get_value(port, Opts), + {Type, {Host, Port}} + end))(L); + (U) -> + (econf:and_then( + econf:url([tls, tcp, udp]), + fun(URI) -> + {ok, Type, _UserInfo, Host, Port, _, _} = + misc:uri_parse(URI), + {list_to_atom(Type), {unicode:characters_to_binary(Host), Port}} + end))(U) + end, [unique]). + +-spec mod_options(binary()) -> [{via, [{tcp | tls | udp, {binary(), 1..65535}}]} | + {atom(), term()}]. +mod_options(Host) -> + Route = #uri{scheme = <<"sip">>, + host = Host, + params = [{<<"lr">>, <<>>}]}, + [{always_record_route, true}, + {flow_timeout_tcp, timer:seconds(120)}, + {flow_timeout_udp, timer:seconds(29)}, + {record_route, Route}, + {routes, [Route]}, + {via, []}]. + +mod_doc() -> + #{desc => + [?T("This module adds SIP proxy/registrar support " + "for the corresponding virtual host."), "", + ?T("NOTE: It is not enough to just load this module. " + "You should also configure listeners and DNS records " + "properly. For details see the section about the " + "_`listen.md#ejabberd_sip|ejabberd_sip`_ listen module " + "in the ejabberd Documentation.")], + opts => + [{always_record_route, + #{value => "true | false", + desc => + ?T("Always insert \"Record-Route\" header into " + "SIP messages. With this approach it is possible to bypass " + "NATs/firewalls a bit more easily. " + "The default value is 'true'.")}}, + {flow_timeout_tcp, + #{value => "timeout()", + desc => + ?T("The option sets a keep-alive timer for " + "https://tools.ietf.org/html/rfc5626[SIP outbound] " + "TCP connections. The default value is '2' minutes.")}}, + {flow_timeout_udp, + #{value => "timeout()", + desc => + ?T("The options sets a keep-alive timer for " + "https://tools.ietf.org/html/rfc5626[SIP outbound] " + "UDP connections. The default value is '29' seconds.")}}, + {record_route, + #{value => ?T("URI"), + desc => + ?T("When the option 'always_record_route' is set to " + "'true' or when https://tools.ietf.org/html/rfc5626" + "[SIP outbound] is utilized, ejabberd inserts " + "\"Record-Route\" header field with this 'URI' into " + "a SIP message. The default is a SIP URI constructed " + "from the virtual host on which the module is loaded.")}}, + {routes, + #{value => "[URI, ...]", + desc => + ?T("You can set a list of SIP URIs of routes pointing " + "to this SIP proxy server. The default is a list containing " + "a single SIP URI constructed from the virtual host " + "on which the module is loaded.")}}, + {via, + #{value => "[URI, ...]", + desc => + ?T("A list to construct \"Via\" headers for " + "inserting them into outgoing SIP messages. " + "This is useful if you're running your SIP proxy " + "in a non-standard network topology. Every 'URI' " + "element in the list must be in the form of " + "\"scheme://host:port\", where \"transport\" " + "must be 'tls', 'tcp', or 'udp', \"host\" must " + "be a domain name or an IP address and \"port\" " + "must be an internet port number. Note that all " + "parts of the 'URI' are mandatory (e.g. you " + "cannot omit \"port\" or \"scheme\").")}}], + example => + ["modules:", + " mod_sip:", + " always_record_route: false", + " record_route: \"sip:example.com;lr\"", + " routes:", + " - \"sip:example.com;lr\"", + " - \"sip:sip.example.com;lr\"", + " flow_timeout_udp: 30 sec", + " flow_timeout_tcp: 1 min", + " via:", + " - tls://sip-tls.example.com:5061", + " - tcp://sip-tcp.example.com:5060", + " - udp://sip-udp.example.com:5060"]}. + +-endif. diff --git a/src/mod_sip_opt.erl b/src/mod_sip_opt.erl new file mode 100644 index 000000000..e160d2e12 --- /dev/null +++ b/src/mod_sip_opt.erl @@ -0,0 +1,48 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_sip_opt). + +-export([always_record_route/1]). +-export([flow_timeout_tcp/1]). +-export([flow_timeout_udp/1]). +-export([record_route/1]). +-export([routes/1]). +-export([via/1]). + +-spec always_record_route(gen_mod:opts() | global | binary()) -> boolean(). +always_record_route(Opts) when is_map(Opts) -> + gen_mod:get_opt(always_record_route, Opts); +always_record_route(Host) -> + gen_mod:get_module_opt(Host, mod_sip, always_record_route). + +-spec flow_timeout_tcp(gen_mod:opts() | global | binary()) -> pos_integer(). +flow_timeout_tcp(Opts) when is_map(Opts) -> + gen_mod:get_opt(flow_timeout_tcp, Opts); +flow_timeout_tcp(Host) -> + gen_mod:get_module_opt(Host, mod_sip, flow_timeout_tcp). + +-spec flow_timeout_udp(gen_mod:opts() | global | binary()) -> pos_integer(). +flow_timeout_udp(Opts) when is_map(Opts) -> + gen_mod:get_opt(flow_timeout_udp, Opts); +flow_timeout_udp(Host) -> + gen_mod:get_module_opt(Host, mod_sip, flow_timeout_udp). + +-spec record_route(gen_mod:opts() | global | binary()) -> esip:uri(). +record_route(Opts) when is_map(Opts) -> + gen_mod:get_opt(record_route, Opts); +record_route(Host) -> + gen_mod:get_module_opt(Host, mod_sip, record_route). + +-spec routes(gen_mod:opts() | global | binary()) -> [esip:uri()]. +routes(Opts) when is_map(Opts) -> + gen_mod:get_opt(routes, Opts); +routes(Host) -> + gen_mod:get_module_opt(Host, mod_sip, routes). + +-spec via(gen_mod:opts() | global | binary()) -> [{'tcp' | 'tls' | 'udp',{binary(),1..65535}}]. +via(Opts) when is_map(Opts) -> + gen_mod:get_opt(via, Opts); +via(Host) -> + gen_mod:get_module_opt(Host, mod_sip, via). + diff --git a/src/mod_sip_proxy.erl b/src/mod_sip_proxy.erl index 6168d997c..8c5d8348c 100644 --- a/src/mod_sip_proxy.erl +++ b/src/mod_sip_proxy.erl @@ -1,12 +1,11 @@ %%%------------------------------------------------------------------- -%%% @author Evgeny Khramtsov -%%% @copyright (C) 2014, Evgeny Khramtsov -%%% @doc -%%% -%%% @end +%%% File : mod_sip_proxy.erl +%%% Author : Evgeny Khramtsov +%%% Purpose : %%% Created : 21 Apr 2014 by Evgeny Khramtsov %%% -%%% ejabberd, Copyright (C) 2014-2015 ProcessOne +%%% +%%% ejabberd, Copyright (C) 2014-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -21,22 +20,24 @@ %%% 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_sip_proxy). --define(GEN_FSM, p1_fsm). --behaviour(?GEN_FSM). +-ifndef(SIP). +-export([]). +-else. +-behaviour(p1_fsm). %% API -export([start/2, start_link/2, route/3, route/4]). -%% gen_fsm callbacks --export([init/1, wait_for_request/2, wait_for_response/2, - handle_event/3, handle_sync_event/4, - handle_info/3, terminate/3, code_change/4]). +-export([init/1, wait_for_request/2, + wait_for_response/2, handle_event/3, + handle_sync_event/4, handle_info/3, terminate/3, + code_change/4]). --include("ejabberd.hrl"). -include("logger.hrl"). -include_lib("esip/include/esip.hrl"). @@ -47,7 +48,7 @@ orig_trid, responses = [] :: [#sip{}], tr_ids = [] :: list(), - orig_req :: #sip{}}). + orig_req = #sip{} :: #sip{}}). %%%=================================================================== %%% API @@ -56,10 +57,10 @@ start(LServer, Opts) -> supervisor:start_child(mod_sip_proxy_sup, [LServer, Opts]). start_link(LServer, Opts) -> - ?GEN_FSM:start_link(?MODULE, [LServer, Opts], []). + p1_fsm:start_link(?MODULE, [LServer, Opts], []). route(SIPMsg, _SIPSock, TrID, Pid) -> - ?GEN_FSM:send_event(Pid, {SIPMsg, TrID}). + p1_fsm:send_event(Pid, {SIPMsg, TrID}). route(#sip{hdrs = Hdrs} = Req, LServer, Opts) -> case proplists:get_bool(authenticated, Opts) of @@ -247,8 +248,8 @@ connect(#sip{hdrs = Hdrs} = Req, Opts) -> {_, ToURI, _} = esip:get_hdr('to', Hdrs), case mod_sip:at_my_host(ToURI) of true -> - LUser = jlib:nodeprep(ToURI#uri.user), - LServer = jlib:nameprep(ToURI#uri.host), + LUser = jid:nodeprep(ToURI#uri.user), + LServer = jid:nameprep(ToURI#uri.host), case mod_sip_registrar:find_sockets(LUser, LServer) of [_|_] = SIPSocks -> {ok, SIPSocks}; @@ -268,11 +269,10 @@ cancel_pending_transactions(State) -> lists:foreach(fun esip:cancel/1, State#state.tr_ids). add_certfile(LServer, Opts) -> - case ejabberd_config:get_option({domain_certfile, LServer}, - fun iolist_to_binary/1) of - CertFile when is_binary(CertFile), CertFile /= <<"">> -> + case ejabberd_pkix:get_certfile(LServer) of + {ok, CertFile} -> [{certfile, CertFile}|Opts]; - _ -> + error -> Opts end. @@ -297,8 +297,7 @@ add_record_route_and_set_uri(URI, LServer, #sip{hdrs = Hdrs} = Req) -> case need_record_route(LServer) of true -> RR_URI = get_configured_record_route(LServer), - {MSecs, Secs, _} = now(), - TS = list_to_binary(integer_to_list(MSecs*1000000 + Secs)), + TS = (integer_to_binary(erlang:system_time(second))), Sign = make_sign(TS, Hdrs), User = <>, NewRR_URI = RR_URI#uri{user = User}, @@ -316,11 +315,7 @@ is_request_within_dialog(#sip{hdrs = Hdrs}) -> esip:has_param(<<"tag">>, Params). need_record_route(LServer) -> - gen_mod:get_module_opt( - LServer, mod_sip, always_record_route, - fun(true) -> true; - (false) -> false - end, true). + mod_sip_opt:always_record_route(LServer). make_sign(TS, Hdrs) -> {_, #uri{user = FUser, host = FServer}, FParams} = esip:get_hdr('from', Hdrs), @@ -331,16 +326,15 @@ make_sign(TS, Hdrs) -> LTServer = safe_nameprep(TServer), FromTag = esip:get_param(<<"tag">>, FParams), CallID = esip:get_hdr('call-id', Hdrs), - SharedKey = ejabberd_config:get_option(shared_key, fun(V) -> V end), - p1_sha:sha([SharedKey, LFUser, LFServer, LTUser, LTServer, + SharedKey = ejabberd_config:get_shared_key(), + str:sha([SharedKey, LFUser, LFServer, LTUser, LTServer, FromTag, CallID, TS]). is_signed_by_me(TS_Sign, Hdrs) -> try [TSBin, Sign] = str:tokens(TS_Sign, <<"-">>), - TS = list_to_integer(binary_to_list(TSBin)), - {MSecs, Secs, _} = now(), - NowTS = MSecs*1000000 + Secs, + TS = (binary_to_integer(TSBin)), + NowTS = erlang:system_time(second), true = (NowTS - TS) =< ?SIGN_LIFETIME, Sign == make_sign(TSBin, Hdrs) catch _:_ -> @@ -348,41 +342,13 @@ is_signed_by_me(TS_Sign, Hdrs) -> end. get_configured_vias(LServer) -> - gen_mod:get_module_opt( - LServer, mod_sip, via, - fun(L) -> - lists:map( - fun(Opts) -> - Type = proplists:get_value(type, Opts), - Host = proplists:get_value(host, Opts), - Port = proplists:get_value(port, Opts), - true = (Type == tcp) or (Type == tls) or (Type == udp), - true = is_binary(Host) and (Host /= <<"">>), - true = (is_integer(Port) - and (Port > 0) and (Port < 65536)) - or (Port == undefined), - {Type, {Host, Port}} - end, L) - end, []). + mod_sip_opt:via(LServer). get_configured_record_route(LServer) -> - gen_mod:get_module_opt( - LServer, mod_sip, record_route, - fun(IOList) -> - S = iolist_to_binary(IOList), - #uri{} = esip:decode_uri(S) - end, #uri{host = LServer, params = [{<<"lr">>, <<"">>}]}). + mod_sip_opt:record_route(LServer). get_configured_routes(LServer) -> - gen_mod:get_module_opt( - LServer, mod_sip, routes, - fun(L) -> - lists:map( - fun(IOList) -> - S = iolist_to_binary(IOList), - #uri{} = esip:decode_uri(S) - end, L) - end, [#uri{host = LServer, params = [{<<"lr">>, <<"">>}]}]). + mod_sip_opt:routes(LServer). mark_transaction_as_complete(TrID, State) -> NewTrIDs = lists:delete(TrID, State#state.tr_ids), @@ -410,7 +376,7 @@ choose_best_response(#state{responses = Responses} = State) -> %% Just compare host part only. cmp_uri(#uri{host = H1}, #uri{host = H2}) -> - jlib:nameprep(H1) == jlib:nameprep(H2). + jid:nameprep(H1) == jid:nameprep(H2). is_my_route(URI, URIs) -> lists:any(fun(U) -> cmp_uri(URI, U) end, URIs). @@ -439,20 +405,22 @@ prepare_request(LServer, #sip{hdrs = Hdrs} = Req) -> Hdrs3 = lists:filter( fun({'proxy-authorization', {_, Params}}) -> Realm = esip:unquote(esip:get_param(<<"realm">>, Params)), - not mod_sip:is_my_host(jlib:nameprep(Realm)); + not mod_sip:is_my_host(jid:nameprep(Realm)); (_) -> true end, Hdrs2), Req#sip{hdrs = Hdrs3}. safe_nodeprep(S) -> - case jlib:nodeprep(S) of + case jid:nodeprep(S) of error -> S; S1 -> S1 end. safe_nameprep(S) -> - case jlib:nameprep(S) of + case jid:nameprep(S) of error -> S; S1 -> S1 end. + +-endif. diff --git a/src/mod_sip_registrar.erl b/src/mod_sip_registrar.erl index a534c61ce..ade4c0be0 100644 --- a/src/mod_sip_registrar.erl +++ b/src/mod_sip_registrar.erl @@ -1,12 +1,11 @@ %%%------------------------------------------------------------------- -%%% @author Evgeny Khramtsov -%%% @copyright (C) 2014, Evgeny Khramtsov -%%% @doc -%%% -%%% @end +%%% File : mod_sip_registrar.erl +%%% Author : Evgeny Khramtsov +%%% Purpose : %%% Created : 23 Apr 2014 by Evgeny Khramtsov %%% -%%% ejabberd, Copyright (C) 2014-2015 ProcessOne +%%% +%%% ejabberd, Copyright (C) 2014-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -21,11 +20,17 @@ %%% 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_sip_registrar). --define(GEN_SERVER, p1_server). +-ifndef(SIP). +-export([]). +-else. +-ifndef(GEN_SERVER). +-define(GEN_SERVER, gen_server). +-endif. -behaviour(?GEN_SERVER). %% API @@ -35,22 +40,19 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --include("ejabberd.hrl"). -include("logger.hrl"). -include_lib("esip/include/esip.hrl"). -define(CALL_TIMEOUT, timer:seconds(30)). -define(DEFAULT_EXPIRES, 3600). --define(FLOW_TIMEOUT_UDP, 29). --define(FLOW_TIMEOUT_TCP, 120). -record(sip_session, {us = {<<"">>, <<"">>} :: {binary(), binary()}, socket = #sip_socket{} :: #sip_socket{}, call_id = <<"">> :: binary(), cseq = 0 :: non_neg_integer(), - timestamp = now() :: erlang:timestamp(), + timestamp = erlang:timestamp() :: erlang:timestamp(), contact :: {binary(), #uri{}, [{binary(), binary()}]}, - flow_tref :: reference(), + flow_tref :: reference() | undefined, reg_tref = make_ref() :: reference(), conn_mref = make_ref() :: reference(), expires = 0 :: non_neg_integer()}). @@ -65,8 +67,8 @@ start_link() -> request(#sip{hdrs = Hdrs} = Req, SIPSock) -> {_, #uri{user = U, host = S}, _} = esip:get_hdr('to', Hdrs), - LUser = jlib:nodeprep(U), - LServer = jlib:nameprep(S), + LUser = jid:nodeprep(U), + LServer = jid:nameprep(S), {PeerIP, _} = SIPSock#sip_socket.peer, US = {LUser, LServer}, CallID = esip:get_hdr('call-id', Hdrs), @@ -78,7 +80,7 @@ request(#sip{hdrs = Hdrs} = Req, SIPSock) -> [<<"*">>] when Expires == 0 -> case unregister_session(US, CallID, CSeq) of {ok, ContactsWithExpires} -> - ?INFO_MSG("unregister SIP session for user ~s@~s from ~s", + ?INFO_MSG("Unregister SIP session for user ~ts@~ts from ~ts", [LUser, LServer, inet_parse:ntoa(PeerIP)]), Cs = prepare_contacts_to_send(ContactsWithExpires), mod_sip:make_response( @@ -112,7 +114,7 @@ request(#sip{hdrs = Hdrs} = Req, SIPSock) -> IsOutboundSupported, ContactsWithExpires) of {ok, Res} -> - ?INFO_MSG("~s SIP session for user ~s@~s from ~s", + ?INFO_MSG("~ts SIP session for user ~ts@~ts from ~ts", [Res, LUser, LServer, inet_parse:ntoa(PeerIP)]), Cs = prepare_contacts_to_send(ContactsWithExpires), @@ -178,14 +180,13 @@ ping(SIPSocket) -> %%% gen_server callbacks %%%=================================================================== init([]) -> + process_flag(trap_exit, true), update_table(), - mnesia:create_table(sip_session, + ejabberd_mnesia:create(?MODULE, sip_session, [{ram_copies, [node()]}, {type, bag}, - {attributes, record_info(fields, sip_session)}]), - mnesia:add_table_index(sip_session, conn_mref), - mnesia:add_table_index(sip_session, socket), - mnesia:add_table_copy(sip_session, node(), ram_copies), + {attributes, record_info(fields, sip_session)}, + {index, [conn_mref,socket]}]), {ok, #state{}}. handle_call({write, Sessions, Supported}, _From, State) -> @@ -197,11 +198,12 @@ handle_call({delete, US, CallID, CSeq}, _From, State) -> handle_call({ping, SIPSocket}, _From, State) -> Res = process_ping(SIPSocket), {reply, Res, State}; -handle_call(_Request, _From, State) -> - Reply = ok, - {reply, Reply, State}. +handle_call(Request, From, State) -> + ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), + {noreply, State}. -handle_cast(_Msg, State) -> +handle_cast(Msg, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. handle_info({write, Sessions, Supported}, State) -> @@ -221,8 +223,8 @@ handle_info({'DOWN', MRef, process, _Pid, _Reason}, State) -> ok end, {noreply, State}; -handle_info(_Info, State) -> - ?ERROR_MSG("got unexpected info: ~p", [_Info]), +handle_info(Info, State) -> + ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, _State) -> @@ -242,7 +244,7 @@ register_session(US, SIPSocket, CallID, CSeq, IsOutboundSupported, socket = SIPSocket, call_id = CallID, cseq = CSeq, - timestamp = now(), + timestamp = erlang:timestamp(), contact = Contact, expires = Expires} end, ContactsWithExpires), @@ -355,7 +357,7 @@ min_expires() -> 60. to_integer(Bin, Min, Max) -> - case catch list_to_integer(binary_to_list(Bin)) of + case catch (binary_to_integer(Bin)) of N when N >= Min, N =< Max -> {ok, N}; _ -> @@ -490,15 +492,12 @@ need_ob_hdrs(Contacts, _IsOutboundSupported = true) -> end, Contacts). get_flow_timeout(LServer, #sip_socket{type = Type}) -> - {Option, Default} = - case Type of - udp -> {flow_timeout_udp, ?FLOW_TIMEOUT_UDP}; - _ -> {flow_timeout_tcp, ?FLOW_TIMEOUT_TCP} - end, - gen_mod:get_module_opt( - LServer, mod_sip, Option, - fun(I) when is_integer(I), I>0 -> I end, - Default). + case Type of + udp -> + mod_sip_opt:flow_timeout_udp(LServer) div 1000; + _ -> + mod_sip_opt:flow_timeout_tcp(LServer) div 1000 + end. update_table() -> Fields = record_info(fields, sip_session), @@ -550,8 +549,8 @@ close_socket(#sip_session{socket = SIPSocket}) -> delete_session(#sip_session{reg_tref = RegTRef, flow_tref = FlowTRef, conn_mref = MRef} = Session) -> - erlang:cancel_timer(RegTRef), - catch erlang:cancel_timer(FlowTRef), + misc:cancel_timer(RegTRef), + misc:cancel_timer(FlowTRef), catch erlang:demonitor(MRef, [flush]), mnesia:dirty_delete_object(Session). @@ -569,13 +568,10 @@ process_ping(SIPSocket) -> mnesia:dirty_delete_object(Session), Timeout = get_flow_timeout(LServer, SIPSocket), NewTRef = set_timer(Session, Timeout), - case mnesia:dirty_write( - Session#sip_session{flow_tref = NewTRef}) of - ok -> - pong; - _Err -> - pang - end; + mnesia:dirty_write(Session#sip_session{flow_tref = NewTRef}), + pong; (_, Acc) -> Acc end, ErrResponse, Sessions). + +-endif. diff --git a/src/mod_stats.erl b/src/mod_stats.erl index 4317e9e92..184407e8c 100644 --- a/src/mod_stats.erl +++ b/src/mod_stats.erl @@ -5,7 +5,7 @@ %%% Created : 11 Jan 2003 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -27,75 +27,57 @@ -author('alexey@process-one.net'). +-protocol({xep, 39, '0.6.0', '0.1.0', "complete", ""}). + -behaviour(gen_mod). --export([start/2, stop/1, process_local_iq/3]). +-export([start/2, stop/1, reload/3, process_iq/1, + mod_options/1, depends/2, mod_doc/0]). --include("ejabberd.hrl"). -include("logger.hrl"). --include("jlib.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). +-include("translate.hrl"). -start(Host, Opts) -> - IQDisc = gen_mod:get_opt(iqdisc, Opts, fun gen_iq_handler:check_type/1, - one_queue), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, - ?NS_STATS, ?MODULE, process_local_iq, IQDisc). +start(_Host, _Opts) -> + {ok, [{iq_handler, ejabberd_local, ?NS_STATS, process_iq}]}. -stop(Host) -> - gen_iq_handler:remove_iq_handler(ejabberd_local, Host, - ?NS_STATS). +stop(_Host) -> + ok. -process_local_iq(_From, To, - #iq{id = _ID, type = Type, xmlns = XMLNS, - sub_el = SubEl} = - IQ) -> - case Type of - set -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; - get -> - #xmlel{children = Els} = SubEl, - Node = str:tokens(xml:get_tag_attr_s(<<"node">>, SubEl), - <<"/">>), - Names = get_names(Els, []), - case get_local_stats(To#jid.server, Node, Names) of - {result, Res} -> - IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"query">>, - attrs = [{<<"xmlns">>, XMLNS}], - children = Res}]}; - {error, Error} -> - IQ#iq{type = error, sub_el = [SubEl, Error]} - end +reload(_Host, _NewOpts, _OldOpts) -> + ok. + +depends(_Host, _Opts) -> + []. + +process_iq(#iq{type = set, lang = Lang} = IQ) -> + Txt = ?T("Value 'set' of 'type' attribute is not allowed"), + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); +process_iq(#iq{type = get, to = To, lang = Lang, + sub_els = [#stats{} = Stats]} = IQ) -> + Node = str:tokens(Stats#stats.node, <<"/">>), + Names = [Name || #stat{name = Name} <- Stats#stats.list], + case get_local_stats(To#jid.server, Node, Names, Lang) of + {result, List} -> + xmpp:make_iq_result(IQ, Stats#stats{list = List}); + {error, Error} -> + xmpp:make_error(IQ, Error) end. -get_names([], Res) -> Res; -get_names([#xmlel{name = <<"stat">>, attrs = Attrs} - | Els], - Res) -> - Name = xml:get_attr_s(<<"name">>, Attrs), - case Name of - <<"">> -> get_names(Els, Res); - _ -> get_names(Els, [Name | Res]) - end; -get_names([_ | Els], Res) -> get_names(Els, Res). +-define(STAT(Name), #stat{name = Name}). --define(STAT(Name), - #xmlel{name = <<"stat">>, attrs = [{<<"name">>, Name}], - children = []}). - -get_local_stats(_Server, [], []) -> +get_local_stats(_Server, [], [], _Lang) -> {result, [?STAT(<<"users/online">>), ?STAT(<<"users/total">>), ?STAT(<<"users/all-hosts/online">>), ?STAT(<<"users/all-hosts/total">>)]}; -get_local_stats(Server, [], Names) -> +get_local_stats(Server, [], Names, _Lang) -> {result, lists:map(fun (Name) -> get_local_stat(Server, [], Name) end, Names)}; get_local_stats(_Server, [<<"running nodes">>, _], - []) -> + [], _Lang) -> {result, [?STAT(<<"time/uptime">>), ?STAT(<<"time/cputime">>), ?STAT(<<"users/online">>), @@ -104,154 +86,140 @@ get_local_stats(_Server, [<<"running nodes">>, _], ?STAT(<<"transactions/restarted">>), ?STAT(<<"transactions/logged">>)]}; get_local_stats(_Server, [<<"running nodes">>, ENode], - Names) -> + Names, Lang) -> case search_running_node(ENode) of - false -> {error, ?ERR_ITEM_NOT_FOUND}; + false -> + Txt = ?T("No running node found"), + {error, xmpp:err_item_not_found(Txt, Lang)}; Node -> {result, lists:map(fun (Name) -> get_node_stat(Node, Name) end, Names)} end; -get_local_stats(_Server, _, _) -> - {error, ?ERR_FEATURE_NOT_IMPLEMENTED}. +get_local_stats(_Server, _, _, Lang) -> + Txt = ?T("No statistics found for this item"), + {error, xmpp:err_feature_not_implemented(Txt, Lang)}. --define(STATVAL(Val, Unit), - #xmlel{name = <<"stat">>, - attrs = - [{<<"name">>, Name}, {<<"units">>, Unit}, - {<<"value">>, Val}], - children = []}). +-define(STATVAL(Val, Unit), #stat{name = Name, units = Unit, value = Val}). -define(STATERR(Code, Desc), - #xmlel{name = <<"stat">>, attrs = [{<<"name">>, Name}], - children = - [#xmlel{name = <<"error">>, - attrs = [{<<"code">>, Code}], - children = [{xmlcdata, Desc}]}]}). + #stat{name = Name, + error = #stat_error{code = Code, reason = Desc}}). get_local_stat(Server, [], Name) when Name == <<"users/online">> -> case catch ejabberd_sm:get_vh_session_list(Server) of {'EXIT', _Reason} -> - ?STATERR(<<"500">>, <<"Internal Server Error">>); + ?STATERR(500, <<"Internal Server Error">>); Users -> - ?STATVAL((iolist_to_binary(integer_to_list(length(Users)))), + ?STATVAL((integer_to_binary(length(Users))), <<"users">>) end; get_local_stat(Server, [], Name) when Name == <<"users/total">> -> case catch - ejabberd_auth:get_vh_registered_users_number(Server) + ejabberd_auth:count_users(Server) of {'EXIT', _Reason} -> - ?STATERR(<<"500">>, <<"Internal Server Error">>); + ?STATERR(500, <<"Internal Server Error">>); NUsers -> - ?STATVAL((iolist_to_binary(integer_to_list(NUsers))), + ?STATVAL((integer_to_binary(NUsers)), <<"users">>) end; get_local_stat(_Server, [], Name) when Name == <<"users/all-hosts/online">> -> - case catch mnesia:table_info(session, size) of - {'EXIT', _Reason} -> - ?STATERR(<<"500">>, <<"Internal Server Error">>); - Users -> - ?STATVAL((iolist_to_binary(integer_to_list(Users))), - <<"users">>) - end; + Users = ejabberd_sm:connected_users_number(), + ?STATVAL((integer_to_binary(Users)), <<"users">>); get_local_stat(_Server, [], Name) when Name == <<"users/all-hosts/total">> -> NumUsers = lists:foldl(fun (Host, Total) -> - ejabberd_auth:get_vh_registered_users_number(Host) + ejabberd_auth:count_users(Host) + Total end, - 0, ?MYHOSTS), - ?STATVAL((iolist_to_binary(integer_to_list(NumUsers))), + 0, ejabberd_option:hosts()), + ?STATVAL((integer_to_binary(NumUsers)), <<"users">>); get_local_stat(_Server, _, Name) -> - ?STATERR(<<"404">>, <<"Not Found">>). + ?STATERR(404, <<"Not Found">>). get_node_stat(Node, Name) when Name == <<"time/uptime">> -> - case catch rpc:call(Node, erlang, statistics, + case catch ejabberd_cluster:call(Node, erlang, statistics, [wall_clock]) of {badrpc, _Reason} -> - ?STATERR(<<"500">>, <<"Internal Server Error">>); + ?STATERR(500, <<"Internal Server Error">>); CPUTime -> - ?STATVAL(list_to_binary( - io_lib:format("~.3f", - [element(1, CPUTime) / 1000])), + ?STATVAL(str:format("~.3f", [element(1, CPUTime) / 1000]), <<"seconds">>) end; get_node_stat(Node, Name) when Name == <<"time/cputime">> -> - case catch rpc:call(Node, erlang, statistics, [runtime]) + case catch ejabberd_cluster:call(Node, erlang, statistics, [runtime]) of {badrpc, _Reason} -> - ?STATERR(<<"500">>, <<"Internal Server Error">>); + ?STATERR(500, <<"Internal Server Error">>); RunTime -> - ?STATVAL(list_to_binary( - io_lib:format("~.3f", - [element(1, RunTime) / 1000])), + ?STATVAL(str:format("~.3f", [element(1, RunTime) / 1000]), <<"seconds">>) end; get_node_stat(Node, Name) when Name == <<"users/online">> -> - case catch rpc:call(Node, ejabberd_sm, + case catch ejabberd_cluster:call(Node, ejabberd_sm, dirty_get_my_sessions_list, []) of {badrpc, _Reason} -> - ?STATERR(<<"500">>, <<"Internal Server Error">>); + ?STATERR(500, <<"Internal Server Error">>); Users -> - ?STATVAL((iolist_to_binary(integer_to_list(length(Users)))), + ?STATVAL((integer_to_binary(length(Users))), <<"users">>) end; get_node_stat(Node, Name) when Name == <<"transactions/committed">> -> - case catch rpc:call(Node, mnesia, system_info, + case catch ejabberd_cluster:call(Node, mnesia, system_info, [transaction_commits]) of {badrpc, _Reason} -> - ?STATERR(<<"500">>, <<"Internal Server Error">>); + ?STATERR(500, <<"Internal Server Error">>); Transactions -> - ?STATVAL((iolist_to_binary(integer_to_list(Transactions))), + ?STATVAL((integer_to_binary(Transactions)), <<"transactions">>) end; get_node_stat(Node, Name) when Name == <<"transactions/aborted">> -> - case catch rpc:call(Node, mnesia, system_info, + case catch ejabberd_cluster:call(Node, mnesia, system_info, [transaction_failures]) of {badrpc, _Reason} -> - ?STATERR(<<"500">>, <<"Internal Server Error">>); + ?STATERR(500, <<"Internal Server Error">>); Transactions -> - ?STATVAL((iolist_to_binary(integer_to_list(Transactions))), + ?STATVAL((integer_to_binary(Transactions)), <<"transactions">>) end; get_node_stat(Node, Name) when Name == <<"transactions/restarted">> -> - case catch rpc:call(Node, mnesia, system_info, + case catch ejabberd_cluster:call(Node, mnesia, system_info, [transaction_restarts]) of {badrpc, _Reason} -> - ?STATERR(<<"500">>, <<"Internal Server Error">>); + ?STATERR(500, <<"Internal Server Error">>); Transactions -> - ?STATVAL((iolist_to_binary(integer_to_list(Transactions))), + ?STATVAL((integer_to_binary(Transactions)), <<"transactions">>) end; get_node_stat(Node, Name) when Name == <<"transactions/logged">> -> - case catch rpc:call(Node, mnesia, system_info, + case catch ejabberd_cluster:call(Node, mnesia, system_info, [transaction_log_writes]) of {badrpc, _Reason} -> - ?STATERR(<<"500">>, <<"Internal Server Error">>); + ?STATERR(500, <<"Internal Server Error">>); Transactions -> - ?STATVAL((iolist_to_binary(integer_to_list(Transactions))), + ?STATVAL((integer_to_binary(Transactions)), <<"transactions">>) end; get_node_stat(_, Name) -> - ?STATERR(<<"404">>, <<"Not Found">>). + ?STATERR(404, <<"Not Found">>). search_running_node(SNode) -> search_running_node(SNode, @@ -263,3 +231,26 @@ search_running_node(SNode, [Node | Nodes]) -> SNode -> Node; _ -> search_running_node(SNode, Nodes) end. + +mod_options(_Host) -> + []. + +mod_doc() -> + #{desc => + [?T("This module adds support for " + "https://xmpp.org/extensions/xep-0039.html" + "[XEP-0039: Statistics Gathering]. This protocol " + "allows you to retrieve the following statistics " + "from your ejabberd server:"), "", + ?T("- Total number of registered users on the current " + "virtual host (users/total)."), "", + ?T("- Total number of registered users on all virtual " + "hosts (users/all-hosts/total)."), "", + ?T("- Total number of online users on the current " + "virtual host (users/online)."), "", + ?T("- Total number of online users on all virtual " + "hosts (users/all-hosts/online)."), "", + ?T("NOTE: The protocol extension is deferred and seems " + "like even a few clients that were supporting it " + "are now abandoned. So using this module makes " + "very little sense.")]}. diff --git a/src/mod_stream_mgmt.erl b/src/mod_stream_mgmt.erl new file mode 100644 index 000000000..f3a641a7a --- /dev/null +++ b/src/mod_stream_mgmt.erl @@ -0,0 +1,1027 @@ +%%%------------------------------------------------------------------- +%%% Author : Holger Weiss +%%% Created : 25 Dec 2016 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_stream_mgmt). +-behaviour(gen_mod). +-author('holger@zedat.fu-berlin.de'). +-protocol({xep, 198, '1.5.2', '14.05', "complete", ""}). + +%% gen_mod API +-export([start/2, stop/1, reload/3, depends/2, mod_opt_type/1, mod_options/1]). +-export([mod_doc/0]). +%% hooks +-export([c2s_stream_started/2, c2s_stream_features/2, + c2s_authenticated_packet/2, c2s_unauthenticated_packet/2, + c2s_unbinded_packet/2, c2s_closed/2, c2s_terminated/2, + c2s_handle_send/3, c2s_handle_info/2, c2s_handle_cast/2, + c2s_handle_call/3, c2s_handle_recv/3, c2s_inline_features/3, + c2s_handle_sasl2_inline/1, c2s_handle_sasl2_inline_post/3, + c2s_handle_bind2_inline/1]). +%% adjust pending session timeout / access queue +-export([get_resume_timeout/1, set_resume_timeout/2, queue_find/2]). + +%% for sasl2 inline resume +-export([has_resume_data/2, post_resume_tasks/1]). + +-include_lib("xmpp/include/xmpp.hrl"). +-include("logger.hrl"). +-include_lib("p1_utils/include/p1_queue.hrl"). +-include("translate.hrl"). + +-define(STREAM_MGMT_CACHE, stream_mgmt_cache). + +-define(is_sm_packet(Pkt), + is_record(Pkt, sm_enable) or + is_record(Pkt, sm_resume) or + is_record(Pkt, sm_a) or + is_record(Pkt, sm_r)). + +-type state() :: ejabberd_c2s:state(). +-type queue() :: p1_queue:queue({non_neg_integer(), erlang:timestamp(), xmpp_element() | xmlel()}). +-type id() :: binary(). +-type error_reason() :: session_not_found | session_timed_out | + session_is_dead | session_has_exited | + session_was_killed | session_copy_timed_out | + invalid_previd. + +%%%=================================================================== +%%% API +%%%=================================================================== +start(_Host, Opts) -> + init_cache(Opts), + {ok, [{hook, c2s_stream_started, c2s_stream_started, 50}, + {hook, c2s_post_auth_features, c2s_stream_features, 50}, + {hook, c2s_inline_features, c2s_inline_features, 50}, + {hook, c2s_unauthenticated_packet, c2s_unauthenticated_packet, 50}, + {hook, c2s_unbinded_packet, c2s_unbinded_packet, 50}, + {hook, c2s_authenticated_packet, c2s_authenticated_packet, 50}, + {hook, c2s_handle_send, c2s_handle_send, 50}, + {hook, c2s_handle_recv, c2s_handle_recv, 50}, + {hook, c2s_handle_info, c2s_handle_info, 50}, + {hook, c2s_handle_cast, c2s_handle_cast, 50}, + {hook, c2s_handle_call, c2s_handle_call, 50}, + {hook, c2s_handle_sasl2_inline, c2s_handle_sasl2_inline, 50}, + {hook, c2s_handle_sasl2_inline_post, c2s_handle_sasl2_inline_post, 50}, + {hook, c2s_handle_bind2_inline, c2s_handle_bind2_inline, 50}, + {hook, c2s_closed, c2s_closed, 50}, + {hook, c2s_terminated, c2s_terminated, 50}]}. + +stop(_Host) -> + ok. + +reload(_Host, NewOpts, _OldOpts) -> + init_cache(NewOpts), + ?WARNING_MSG("Module ~ts is reloaded, but new configuration will take " + "effect for newly created client connections only", [?MODULE]). + +depends(_Host, _Opts) -> + []. + +c2s_stream_started(#{lserver := LServer} = State, _StreamStart) -> + State1 = maps:remove(mgmt_options, State), + ResumeTimeout = get_configured_resume_timeout(LServer), + MaxResumeTimeout = get_max_resume_timeout(LServer, ResumeTimeout), + State1#{mgmt_state => inactive, + mgmt_queue_type => get_queue_type(LServer), + mgmt_max_queue => get_max_ack_queue(LServer), + mgmt_timeout => ResumeTimeout, + mgmt_max_timeout => MaxResumeTimeout, + mgmt_ack_timeout => get_ack_timeout(LServer), + mgmt_resend => get_resend_on_timeout(LServer), + mgmt_stanzas_in => 0, + mgmt_stanzas_out => 0, + mgmt_stanzas_req => 0}; +c2s_stream_started(State, _StreamStart) -> + State. + +c2s_stream_features(Acc, Host) -> + case gen_mod:is_loaded(Host, ?MODULE) of + true -> + [#feature_sm{xmlns = ?NS_STREAM_MGMT_2}, + #feature_sm{xmlns = ?NS_STREAM_MGMT_3}|Acc]; + false -> + Acc + end. + +c2s_inline_features({Sasl, Bind, Extra} = Acc, Host, _State) -> + case gen_mod:is_loaded(Host, ?MODULE) of + true -> + {[#feature_sm{xmlns = ?NS_STREAM_MGMT_3} | Sasl], + [#bind2_feature{var = ?NS_STREAM_MGMT_3} | Bind], + Extra}; + false -> + Acc + end. + +c2s_handle_sasl2_inline({State, Els, Results} = Acc) -> + case lists:keytake(sm_resume, 1, Els) of + {value, Resume, Rest} -> + case has_resume_data(State, Resume) of + {ok, NewState, Resumed} -> + Rest2 = lists:keydelete(bind2_bind, 1, Rest), + {NewState, Rest2, [Resumed | Results]}; + {error, ResumeError, _Reason} -> + {State, Els, [ResumeError | Results]} + end; + _ -> + Acc + end. + +c2s_handle_sasl2_inline_post(State, _Els, Results) -> + case lists:keyfind(sm_resumed, 1, Results) of + false -> + State; + _ -> + post_resume_tasks(State) + end. + +c2s_handle_bind2_inline({State, Els, Results}) -> + case lists:keyfind(sm_enable, 1, Els) of + #sm_enable{xmlns = XMLNS} = Pkt -> + {State2, Res} = handle_enable_int(State#{mgmt_xmlns => XMLNS}, Pkt), + {State2, Els, [Res | Results]}; + _ -> + {State, Els, Results} + end. + +c2s_unauthenticated_packet(#{lang := Lang} = State, Pkt) when ?is_sm_packet(Pkt) -> + %% XEP-0198 says: "For client-to-server connections, the client MUST NOT + %% attempt to enable stream management until after it has completed Resource + %% Binding unless it is resuming a previous session". However, it also + %% says: "Stream management errors SHOULD be considered recoverable", so we + %% won't bail out. + Err = #sm_failed{reason = 'not-authorized', + text = xmpp:mk_text(?T("Unauthorized"), Lang), + xmlns = ?NS_STREAM_MGMT_3}, + {stop, send(State, Err)}; +c2s_unauthenticated_packet(State, _Pkt) -> + State. + +c2s_unbinded_packet(State, #sm_resume{} = Pkt) -> + case handle_resume(State, Pkt) of + {ok, ResumedState} -> + {stop, ResumedState}; + {error, State1} -> + {stop, State1} + end; +c2s_unbinded_packet(State, Pkt) when ?is_sm_packet(Pkt) -> + c2s_unauthenticated_packet(State, Pkt); +c2s_unbinded_packet(State, _Pkt) -> + State. + +c2s_authenticated_packet(#{mgmt_state := MgmtState} = State, Pkt) + when ?is_sm_packet(Pkt) -> + if MgmtState == pending; MgmtState == active -> + {stop, perform_stream_mgmt(Pkt, State)}; + true -> + {stop, negotiate_stream_mgmt(Pkt, State)} + end; +c2s_authenticated_packet(State, Pkt) -> + update_num_stanzas_in(State, Pkt). + +c2s_handle_recv(#{mgmt_state := MgmtState, + lang := Lang} = State, El, {error, Why}) -> + Xmlns = xmpp:get_ns(El), + IsStanza = xmpp:is_stanza(El), + if Xmlns == ?NS_STREAM_MGMT_2; Xmlns == ?NS_STREAM_MGMT_3 -> + Txt = xmpp:io_format_error(Why), + Err = #sm_failed{reason = 'bad-request', + text = xmpp:mk_text(Txt, Lang), + xmlns = Xmlns}, + send(State, Err); + IsStanza andalso (MgmtState == pending orelse MgmtState == active) -> + State1 = update_num_stanzas_in(State, El), + case xmpp:get_type(El) of + <<"result">> -> State1; + <<"error">> -> State1; + _ -> + State1#{mgmt_force_enqueue => true} + end; + true -> + State + end; +c2s_handle_recv(State, _, _) -> + State. + +c2s_handle_send(#{mgmt_state := MgmtState, mod := Mod, + lang := Lang} = State, Pkt, SendResult) + when MgmtState == pending; MgmtState == active; MgmtState == resumed -> + IsStanza = xmpp:is_stanza(Pkt), + case Pkt of + _ when IsStanza -> + case need_to_enqueue(State, Pkt) of + {true, State1} -> + case mgmt_queue_add(State1, Pkt) of + #{mgmt_max_queue := exceeded} = State2 -> + State3 = State2#{mgmt_resend => false}, + Err = xmpp:serr_policy_violation( + ?T("Too many unacked stanzas"), Lang), + send(State3, Err); + State2 when SendResult == ok -> + send_rack(State2); + State2 -> + State2 + end; + {false, State1} -> + State1 + end; + #stream_error{} -> + case MgmtState of + resumed -> + State; + active -> + State; + pending -> + Mod:stop_async(self()), + {stop, State#{stop_reason => {stream, {out, Pkt}}}} + end; + _ -> + State + end; +c2s_handle_send(State, _Pkt, _Result) -> + State. + +c2s_handle_cast(#{mgmt_state := active} = State, send_ping) -> + {stop, send_rack(State)}; +c2s_handle_cast(#{mgmt_state := pending} = State, send_ping) -> + {stop, State}; +c2s_handle_cast(State, _Msg) -> + State. + +c2s_handle_call(#{mgmt_id := MgmtID, mgmt_queue := Queue, mod := Mod} = State, + {resume_session, MgmtID}, From) -> + State1 = State#{mgmt_queue => p1_queue:file_to_ram(Queue)}, + Mod:reply(From, {resume, State1}), + {stop, State#{mgmt_state => resumed, mgmt_queue => p1_queue:clear(Queue)}}; +c2s_handle_call(#{mod := Mod} = State, {resume_session, _}, From) -> + Mod:reply(From, {error, session_not_found}), + {stop, State}; +c2s_handle_call(State, _Call, _From) -> + State. + +c2s_handle_info(#{mgmt_ack_timer := TRef, jid := JID, mod := Mod} = State, + {timeout, TRef, ack_timeout}) -> + ?DEBUG("Timed out waiting for stream management acknowledgement of ~ts", + [jid:encode(JID)]), + State1 = Mod:close(State), + State2 = State1#{stop_reason => {socket, ack_timeout}}, + {stop, transition_to_pending(State2, ack_timeout)}; +c2s_handle_info(#{mgmt_state := pending, lang := Lang, + mgmt_pending_timer := TRef, jid := JID, mod := Mod} = State, + {timeout, TRef, pending_timeout}) -> + ?DEBUG("Timed out waiting for resumption of stream for ~ts", + [jid:encode(JID)]), + Txt = ?T("Timed out waiting for stream resumption"), + Err = xmpp:serr_connection_timeout(Txt, Lang), + Mod:stop_async(self()), + {stop, State#{mgmt_state => timeout, + stop_reason => {stream, {out, Err}}}}; +c2s_handle_info(State, {_Ref, {resume, #{jid := JID} = OldState}}) -> + %% This happens if the resume_session/1 request timed out; the new session + %% now receives the late response. + ?DEBUG("Received old session state for ~ts after failed resumption", + [jid:encode(JID)]), + route_unacked_stanzas(OldState#{mgmt_resend => false}), + {stop, State}; +c2s_handle_info(State, {timeout, _, Timeout}) when Timeout == ack_timeout; + Timeout == pending_timeout -> + %% Late arrival of an already cancelled timer: we just ignore it. + %% This might happen because misc:cancel_timer/1 doesn't guarantee + %% timer cancellation in the case when p1_server is used. + {stop, State}; +c2s_handle_info(State, _) -> + State. + +c2s_closed(State, {stream, _}) -> + State; +c2s_closed(#{mgmt_state := active} = State, Reason) -> + {stop, transition_to_pending(State, Reason)}; +c2s_closed(State, _Reason) -> + State. + +c2s_terminated(#{mgmt_state := resumed, sid := SID, jid := JID} = State, _Reason) -> + ?DEBUG("Closing former stream of resumed session for ~ts", + [jid:encode(JID)]), + {U, S, R} = jid:tolower(JID), + ejabberd_sm:close_session(SID, U, S, R), + route_late_queue_after_resume(State), + ejabberd_c2s:bounce_message_queue(SID, JID), + {stop, State}; +c2s_terminated(#{mgmt_state := MgmtState, mgmt_stanzas_in := In, + mgmt_id := MgmtID, jid := JID} = State, _Reason) -> + case MgmtState of + timeout -> + store_stanzas_in(jid:tolower(JID), MgmtID, In); + _ -> + ok + end, + route_unacked_stanzas(State), + State; +c2s_terminated(State, _Reason) -> + State. + +%%%=================================================================== +%%% Adjust pending session timeout / access queue +%%%=================================================================== +-spec get_resume_timeout(state()) -> non_neg_integer(). +get_resume_timeout(#{mgmt_timeout := Timeout}) -> + Timeout. + +-spec set_resume_timeout(state(), non_neg_integer()) -> state(). +set_resume_timeout(#{mgmt_timeout := Timeout} = State, Timeout) -> + State; +set_resume_timeout(State, Timeout) -> + State1 = restart_pending_timer(State, Timeout), + State1#{mgmt_timeout => Timeout}. + +-spec queue_find(fun((stanza()) -> boolean()), queue()) + -> stanza() | none. +queue_find(Pred, Queue) -> + case p1_queue:out(Queue) of + {{value, {_, _, Pkt}}, Queue1} -> + case Pred(Pkt) of + true -> + Pkt; + false -> + queue_find(Pred, Queue1) + end; + {empty, _Queue1} -> + none + end. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +-spec negotiate_stream_mgmt(xmpp_element(), state()) -> state(). +negotiate_stream_mgmt(Pkt, #{lang := Lang} = State) -> + Xmlns = xmpp:get_ns(Pkt), + case Pkt of + #sm_enable{} -> + handle_enable(State#{mgmt_xmlns => Xmlns}, Pkt); + _ when is_record(Pkt, sm_a); + is_record(Pkt, sm_r); + is_record(Pkt, sm_resume) -> + Txt = ?T("Stream management is not enabled"), + Err = #sm_failed{reason = 'unexpected-request', + text = xmpp:mk_text(Txt, Lang), + xmlns = Xmlns}, + send(State, Err) + end. + +-spec perform_stream_mgmt(xmpp_element(), state()) -> state(). +perform_stream_mgmt(Pkt, #{mgmt_xmlns := Xmlns, lang := Lang} = State) -> + case xmpp:get_ns(Pkt) of + Xmlns -> + case Pkt of + #sm_r{} -> + handle_r(State); + #sm_a{} -> + handle_a(State, Pkt); + _ when is_record(Pkt, sm_enable); + is_record(Pkt, sm_resume) -> + Txt = ?T("Stream management is already enabled"), + send(State, #sm_failed{reason = 'unexpected-request', + text = xmpp:mk_text(Txt, Lang), + xmlns = Xmlns}) + end; + _ -> + Txt = ?T("Unsupported version"), + send(State, #sm_failed{reason = 'unexpected-request', + text = xmpp:mk_text(Txt, Lang), + xmlns = Xmlns}) + end. + +-spec handle_enable_int(state(), sm_enable()) -> {state(), sm_enabled()}. +handle_enable_int(#{mgmt_timeout := DefaultTimeout, + mgmt_queue_type := QueueType, + mgmt_max_timeout := MaxTimeout, + mgmt_xmlns := Xmlns, jid := JID} = State, + #sm_enable{resume = Resume, max = Max}) -> + State1 = State#{mgmt_id => make_id()}, + Timeout = if Resume == false -> + 0; + Max /= undefined, Max > 0, Max*1000 =< MaxTimeout -> + Max*1000; + true -> + DefaultTimeout + end, + Res = if Timeout > 0 -> + ?DEBUG("Stream management with resumption enabled for ~ts", + [jid:encode(JID)]), + #sm_enabled{xmlns = Xmlns, + id = encode_id(State1), + resume = true, + max = Timeout div 1000}; + true -> + ?DEBUG("Stream management without resumption enabled for ~ts", + [jid:encode(JID)]), + #sm_enabled{xmlns = Xmlns} + end, + State2 = State1#{mgmt_state => active, + mgmt_queue => p1_queue:new(QueueType), + mgmt_timeout => Timeout}, + {State2, Res}. + +-spec handle_enable(state(), sm_enable()) -> state(). +handle_enable(State, Enable) -> + {State2, Res} = handle_enable_int(State, Enable), + send(State2, Res). + +-spec handle_r(state()) -> state(). +handle_r(#{mgmt_xmlns := Xmlns, mgmt_stanzas_in := H} = State) -> + Res = #sm_a{xmlns = Xmlns, h = H}, + send(State, Res). + +-spec handle_a(state(), sm_a()) -> state(). +handle_a(State, #sm_a{h = H}) -> + State1 = check_h_attribute(State, H), + resend_rack(State1). + +-spec handle_resume(state(), sm_resume()) -> {ok, state()} | {error, state()}. +handle_resume(#{user := User, lserver := LServer} = State, + #sm_resume{} = Resume) -> + case has_resume_data(State, Resume) of + {ok, ResumedState, ResumedEl} -> + State2 = send(ResumedState, ResumedEl), + {ok, post_resume_tasks(State2)}; + {error, El, Reason} -> + log_resumption_error(User, LServer, Reason), + {error, send(State, El)} + end. + +-spec has_resume_data(state(), sm_resume()) -> + {ok, state(), sm_resumed()} | {error, sm_failed(), error_reason()}. +has_resume_data(#{lang := Lang} = State, + #sm_resume{h = H, previd = PrevID, xmlns = Xmlns}) -> + case inherit_session_state(State, PrevID) of + {ok, InheritedState} -> + State1 = check_h_attribute(InheritedState, H), + #{mgmt_xmlns := AttrXmlns, mgmt_stanzas_in := AttrH} = State1, + {ok, State1, #sm_resumed{xmlns = AttrXmlns, + h = AttrH, + previd = PrevID}}; + {error, Err, InH} -> + {error, #sm_failed{reason = 'item-not-found', + text = xmpp:mk_text(format_error(Err), Lang), + h = InH, xmlns = Xmlns}, Err}; + {error, Err} -> + {error, #sm_failed{reason = 'item-not-found', + text = xmpp:mk_text(format_error(Err), Lang), + xmlns = Xmlns}, Err} + end. + +-spec post_resume_tasks(state()) -> state(). +post_resume_tasks(#{lserver := LServer, socket := Socket, jid := JID, + mgmt_xmlns := AttrXmlns} = State) -> + State3 = resend_unacked_stanzas(State), + State4 = send(State3, #sm_r{xmlns = AttrXmlns}), + State5 = ejabberd_hooks:run_fold(c2s_session_resumed, LServer, State4, []), + ?INFO_MSG("(~ts) Resumed session for ~ts", + [xmpp_socket:pp(Socket), jid:encode(JID)]), + State5. + +-spec transition_to_pending(state(), _) -> state(). +transition_to_pending(#{mgmt_state := active, mod := Mod, + mgmt_timeout := 0} = State, _Reason) -> + Mod:stop_async(self()), + State; +transition_to_pending(#{mgmt_state := active, jid := JID, socket := Socket, + lserver := LServer, mgmt_timeout := Timeout} = State, + Reason) -> + State1 = cancel_ack_timer(State), + ?INFO_MSG("(~ts) Closing c2s connection for ~ts: ~ts; " + "waiting ~B seconds for stream resumption", + [xmpp_socket:pp(Socket), jid:encode(JID), + format_reason(State, Reason), Timeout div 1000]), + TRef = erlang:start_timer(Timeout, self(), pending_timeout), + State2 = State1#{mgmt_state => pending, mgmt_pending_timer => TRef}, + ejabberd_hooks:run_fold(c2s_session_pending, LServer, State2, []); +transition_to_pending(State, _Reason) -> + State. + +-spec check_h_attribute(state(), non_neg_integer()) -> state(). +check_h_attribute(#{mgmt_stanzas_out := NumStanzasOut, jid := JID, + lang := Lang} = State, H) + when H > NumStanzasOut -> + ?WARNING_MSG("~ts acknowledged ~B stanzas, but only ~B were sent", + [jid:encode(JID), H, NumStanzasOut]), + State1 = State#{mgmt_resend => false}, + Err = xmpp:serr_undefined_condition( + ?T("Client acknowledged more stanzas than sent by server"), Lang), + send(State1, Err); +check_h_attribute(#{mgmt_stanzas_out := NumStanzasOut, jid := JID} = State, H) -> + ?DEBUG("~ts acknowledged ~B of ~B stanzas", + [jid:encode(JID), H, NumStanzasOut]), + mgmt_queue_drop(State, H). + +-spec update_num_stanzas_in(state(), xmpp_element() | xmlel()) -> state(). +update_num_stanzas_in(#{mgmt_state := MgmtState, + mgmt_stanzas_in := NumStanzasIn} = State, El) + when MgmtState == active; MgmtState == pending -> + NewNum = case {xmpp:is_stanza(El), NumStanzasIn} of + {true, 4294967295} -> + 0; + {true, Num} -> + Num + 1; + {false, Num} -> + Num + end, + State#{mgmt_stanzas_in => NewNum}; +update_num_stanzas_in(State, _El) -> + State. + +-spec send_rack(state()) -> state(). +send_rack(#{mgmt_ack_timer := _} = State) -> + State; +send_rack(#{mgmt_xmlns := Xmlns, + mgmt_stanzas_out := NumStanzasOut} = State) -> + State1 = State#{mgmt_stanzas_req => NumStanzasOut}, + State2 = start_ack_timer(State1), + send(State2, #sm_r{xmlns = Xmlns}). + +-spec resend_rack(state()) -> state(). +resend_rack(#{mgmt_ack_timer := _, + mgmt_queue := Queue, + mgmt_stanzas_out := NumStanzasOut, + mgmt_stanzas_req := NumStanzasReq} = State) -> + State1 = cancel_ack_timer(State), + case NumStanzasReq < NumStanzasOut andalso not p1_queue:is_empty(Queue) of + true -> send_rack(State1); + false -> State1 + end; +resend_rack(State) -> + State. + +-spec mgmt_queue_add(state(), xmlel() | xmpp_element()) -> state(). +mgmt_queue_add(#{mgmt_stanzas_out := NumStanzasOut, + mgmt_queue := Queue} = State, Pkt) -> + NewNum = case NumStanzasOut of + 4294967295 -> 0; + Num -> Num + 1 + end, + Queue1 = p1_queue:in({NewNum, erlang:timestamp(), Pkt}, Queue), + State1 = State#{mgmt_queue => Queue1, mgmt_stanzas_out => NewNum}, + check_queue_length(State1). + +-spec mgmt_queue_drop(state(), non_neg_integer()) -> state(). +mgmt_queue_drop(#{mgmt_queue := Queue} = State, NumHandled) -> + NewQueue = p1_queue:dropwhile( + fun({N, _T, _E}) -> N =< NumHandled end, Queue), + State#{mgmt_queue => NewQueue}. + +-spec check_queue_length(state()) -> state(). +check_queue_length(#{mgmt_max_queue := Limit} = State) + when Limit == infinity; Limit == exceeded -> + State; +check_queue_length(#{mgmt_queue := Queue, mgmt_max_queue := Limit} = State) -> + case p1_queue:len(Queue) > Limit of + true -> + State#{mgmt_max_queue => exceeded}; + false -> + State + end. + +-spec route_late_queue_after_resume(state()) -> ok. +route_late_queue_after_resume(#{mgmt_queue := Queue, jid := JID}) + when ?qlen(Queue) > 0 -> + ?DEBUG("Re-routing ~B late queued packets to ~ts", + [p1_queue:len(Queue), jid:encode(JID)]), + p1_queue:foreach( + fun({_, _Time, Pkt}) -> + ejabberd_router:route(Pkt) + end, Queue); +route_late_queue_after_resume(_State) -> + ok. + +-spec resend_unacked_stanzas(state()) -> state(). +resend_unacked_stanzas(#{mgmt_state := MgmtState, + mgmt_queue := Queue, + jid := JID} = State) + when (MgmtState == active orelse + MgmtState == pending orelse + MgmtState == timeout) andalso ?qlen(Queue) > 0 -> + ?DEBUG("Resending ~B unacknowledged stanza(s) to ~ts", + [p1_queue:len(Queue), jid:encode(JID)]), + p1_queue:foldl( + fun({_, Time, Pkt}, AccState) -> + Pkt1 = add_resent_delay_info(AccState, Pkt, Time), + Pkt2 = if ?is_stanza(Pkt1) -> + xmpp:put_meta(Pkt1, mgmt_is_resent, true); + true -> + Pkt1 + end, + send(AccState, Pkt2) + end, State, Queue); +resend_unacked_stanzas(State) -> + State. + +-spec route_unacked_stanzas(state()) -> ok. +route_unacked_stanzas(#{mgmt_state := MgmtState, + mgmt_resend := MgmtResend, + lang := Lang, user := User, + jid := JID, lserver := LServer, + mgmt_queue := Queue, + resource := Resource} = State) + when (MgmtState == active orelse + MgmtState == pending orelse + MgmtState == timeout) andalso ?qlen(Queue) > 0 -> + ResendOnTimeout = case MgmtResend of + Resend when is_boolean(Resend) -> + Resend; + if_offline -> + case ejabberd_sm:get_user_resources(User, LServer) of + [Resource] -> + %% Same resource opened new session + true; + [] -> true; + _ -> false + end + end, + ?DEBUG("Re-routing ~B unacknowledged stanza(s) to ~ts", + [p1_queue:len(Queue), jid:encode(JID)]), + ModOfflineEnabled = gen_mod:is_loaded(LServer, mod_offline), + p1_queue:foreach( + fun({_, _Time, #presence{from = From}}) -> + ?DEBUG("Dropping presence stanza from ~ts", [jid:encode(From)]); + ({_, _Time, #iq{} = El}) -> + Txt = ?T("User session terminated"), + ejabberd_router:route_error( + El, xmpp:err_service_unavailable(Txt, Lang)); + ({_, _Time, #message{from = From, meta = #{carbon_copy := true}}}) -> + %% XEP-0280 says: "When a receiving server attempts to deliver a + %% forked message, and that message bounces with an error for + %% any reason, the receiving server MUST NOT forward that error + %% back to the original sender." Resending such a stanza could + %% easily lead to unexpected results as well. + ?DEBUG("Dropping forwarded message stanza from ~ts", + [jid:encode(From)]); + ({_, Time, #message{} = Msg}) -> + case {ModOfflineEnabled, ResendOnTimeout, + xmpp:get_meta(Msg, mam_archived, false)} of + Val when Val == {true, true, false}; + Val == {true, true, true}; + Val == {false, true, false} -> + NewEl = add_resent_delay_info(State, Msg, Time), + ejabberd_router:route(NewEl); + {_, _, true} -> + ?DEBUG("Dropping archived message stanza from ~s", + [jid:encode(xmpp:get_from(Msg))]); + _ -> + Txt = ?T("User session terminated"), + ejabberd_router:route_error( + Msg, xmpp:err_service_unavailable(Txt, Lang)) + end; + ({_, _Time, El}) -> + %% Raw element of type 'error' resulting from a validation error + %% We cannot pass it to the router, it will generate an error + ?DEBUG("Do not route raw element from ack queue: ~p", [El]) + end, Queue); +route_unacked_stanzas(_State) -> + ok. + +-spec inherit_session_state(state(), binary()) -> {ok, state()} | + {error, error_reason()} | + {error, error_reason(), non_neg_integer()}. +inherit_session_state(#{user := U, server := S, + mgmt_queue_type := QueueType} = State, PrevID) -> + case decode_id(PrevID) of + {ok, {R, MgmtID}} -> + case ejabberd_sm:get_session_sid(U, S, R) of + none -> + case pop_stanzas_in({U, S, R}, MgmtID) of + error -> + {error, session_not_found}; + {ok, H} -> + {error, session_timed_out, H} + end; + {_, OldPID} = OldSID -> + try resume_session(OldPID, MgmtID, State) of + {resume, #{mgmt_xmlns := Xmlns, + mgmt_queue := Queue, + mgmt_timeout := Timeout, + mgmt_stanzas_in := NumStanzasIn, + mgmt_stanzas_out := NumStanzasOut} = OldState} -> + State1 = ejabberd_c2s:copy_state(State, OldState), + Queue1 = case QueueType of + ram -> Queue; + _ -> p1_queue:ram_to_file(Queue) + end, + State2 = State1#{sid => ejabberd_sm:make_sid(), + mgmt_id => MgmtID, + mgmt_xmlns => Xmlns, + mgmt_queue => Queue1, + mgmt_timeout => Timeout, + mgmt_stanzas_in => NumStanzasIn, + mgmt_stanzas_out => NumStanzasOut, + mgmt_state => active}, + State3 = ejabberd_c2s:open_session(State2), + ejabberd_c2s:stop_async(OldPID), + {ok, State3}; + {error, Msg} -> + {error, Msg} + catch exit:{noproc, _} -> + {error, session_is_dead}; + exit:{normal, _} -> + {error, session_has_exited}; + exit:{shutdown, _} -> + {error, session_has_exited}; + exit:{killed, _} -> + {error, session_was_killed}; + exit:{timeout, _} -> + ejabberd_sm:close_session(OldSID, U, S, R), + ejabberd_c2s:stop_async(OldPID), + {error, session_copy_timed_out} + end + end; + error -> + {error, invalid_previd} + end. + +-spec resume_session(pid(), id(), state()) -> {resume, state()} | + {error, error_reason()}. +resume_session(PID, MgmtID, _State) -> + ejabberd_c2s:call(PID, {resume_session, MgmtID}, timer:seconds(15)). + +-spec add_resent_delay_info(state(), stanza(), erlang:timestamp()) -> stanza(); + (state(), xmlel(), erlang:timestamp()) -> xmlel(). +add_resent_delay_info(#{lserver := LServer}, El, Time) + when is_record(El, message); is_record(El, presence) -> + misc:add_delay_info(El, jid:make(LServer), Time, <<"Resent">>); +add_resent_delay_info(_State, El, _Time) -> + %% TODO + El. + +-spec send(state(), xmpp_element()) -> state(). +send(#{mod := Mod} = State, Pkt) -> + Mod:send(State, Pkt). + +-spec restart_pending_timer(state(), non_neg_integer()) -> state(). +restart_pending_timer(#{mgmt_pending_timer := TRef} = State, NewTimeout) -> + misc:cancel_timer(TRef), + NewTRef = erlang:start_timer(NewTimeout, self(), pending_timeout), + State#{mgmt_pending_timer => NewTRef}; +restart_pending_timer(State, _NewTimeout) -> + State. + +-spec start_ack_timer(state()) -> state(). +start_ack_timer(#{mgmt_ack_timeout := infinity} = State) -> + State; +start_ack_timer(#{mgmt_ack_timeout := AckTimeout} = State) -> + TRef = erlang:start_timer(AckTimeout, self(), ack_timeout), + State#{mgmt_ack_timer => TRef}. + +-spec cancel_ack_timer(state()) -> state(). +cancel_ack_timer(#{mgmt_ack_timer := TRef} = State) -> + misc:cancel_timer(TRef), + maps:remove(mgmt_ack_timer, State); +cancel_ack_timer(State) -> + State. + +-spec need_to_enqueue(state(), xmlel() | stanza()) -> {boolean(), state()}. +need_to_enqueue(State, Pkt) when ?is_stanza(Pkt) -> + {not xmpp:get_meta(Pkt, mgmt_is_resent, false), State}; +need_to_enqueue(#{mgmt_force_enqueue := true} = State, #xmlel{}) -> + State1 = maps:remove(mgmt_force_enqueue, State), + State2 = maps:remove(mgmt_is_resent, State1), + {true, State2}; +need_to_enqueue(State, _) -> + {false, State}. + +-spec make_id() -> id(). +make_id() -> + p1_rand:bytes(8). + +-spec encode_id(state()) -> binary(). +encode_id(#{mgmt_id := MgmtID, resource := Resource}) -> + misc:term_to_base64({Resource, MgmtID}). + +-spec decode_id(binary()) -> {ok, {binary(), id()}} | error. +decode_id(Encoded) -> + case misc:base64_to_term(Encoded) of + {term, {Resource, MgmtID}} when is_binary(Resource), + is_binary(MgmtID) -> + {ok, {Resource, MgmtID}}; + _ -> + error + end. + +%%%=================================================================== +%%% Formatters and Logging +%%%=================================================================== +-spec format_error(error_reason()) -> binary(). +format_error(session_not_found) -> + ?T("Previous session not found"); +format_error(session_timed_out) -> + ?T("Previous session timed out"); +format_error(session_is_dead) -> + ?T("Previous session PID is dead"); +format_error(session_has_exited) -> + ?T("Previous session PID has exited"); +format_error(session_was_killed) -> + ?T("Previous session PID has been killed"); +format_error(session_copy_timed_out) -> + ?T("Session state copying timed out"); +format_error(invalid_previd) -> + ?T("Invalid 'previd' value"). + +-spec format_reason(state(), term()) -> binary(). +format_reason(_, ack_timeout) -> + <<"Timed out waiting for stream acknowledgement">>; +format_reason(#{stop_reason := {socket, ack_timeout}} = State, _) -> + format_reason(State, ack_timeout); +format_reason(State, Reason) -> + ejabberd_c2s:format_reason(State, Reason). + +-spec log_resumption_error(binary(), binary(), error_reason()) -> ok. +log_resumption_error(User, Server, Reason) + when Reason == invalid_previd -> + ?WARNING_MSG("Cannot resume session for ~ts@~ts: ~ts", + [User, Server, format_error(Reason)]); +log_resumption_error(User, Server, Reason) -> + ?INFO_MSG("Cannot resume session for ~ts@~ts: ~ts", + [User, Server, format_error(Reason)]). + +%%%=================================================================== +%%% Cache-like storage for last handled stanzas +%%%=================================================================== +init_cache(Opts) -> + ets_cache:new(?STREAM_MGMT_CACHE, cache_opts(Opts)). + +cache_opts(Opts) -> + [{max_size, mod_stream_mgmt_opt:cache_size(Opts)}, + {life_time, mod_stream_mgmt_opt:cache_life_time(Opts)}, + {type, ordered_set}]. + +-spec store_stanzas_in(ljid(), id(), non_neg_integer()) -> boolean(). +store_stanzas_in(LJID, MgmtID, Num) -> + ets_cache:insert(?STREAM_MGMT_CACHE, {LJID, MgmtID}, Num, + ejabberd_cluster:get_nodes()). + +-spec pop_stanzas_in(ljid(), id()) -> {ok, non_neg_integer()} | error. +pop_stanzas_in(LJID, MgmtID) -> + case ets_cache:lookup(?STREAM_MGMT_CACHE, {LJID, MgmtID}) of + {ok, Val} -> + ets_cache:match_delete(?STREAM_MGMT_CACHE, {LJID, '_'}, + ejabberd_cluster:get_nodes()), + {ok, Val}; + error -> + error + end. + +%%%=================================================================== +%%% Configuration processing +%%%=================================================================== +get_max_ack_queue(Host) -> + mod_stream_mgmt_opt:max_ack_queue(Host). + +get_configured_resume_timeout(Host) -> + mod_stream_mgmt_opt:resume_timeout(Host). + +get_max_resume_timeout(Host, ResumeTimeout) -> + case mod_stream_mgmt_opt:max_resume_timeout(Host) of + undefined -> ResumeTimeout; + Max when Max >= ResumeTimeout -> Max; + _ -> ResumeTimeout + end. + +get_ack_timeout(Host) -> + mod_stream_mgmt_opt:ack_timeout(Host). + +get_resend_on_timeout(Host) -> + mod_stream_mgmt_opt:resend_on_timeout(Host). + +get_queue_type(Host) -> + mod_stream_mgmt_opt:queue_type(Host). + +mod_opt_type(max_ack_queue) -> + econf:pos_int(infinity); +mod_opt_type(resume_timeout) -> + econf:either( + econf:int(0, 0), + econf:timeout(second)); +mod_opt_type(max_resume_timeout) -> + econf:either( + econf:int(0, 0), + econf:timeout(second)); +mod_opt_type(ack_timeout) -> + econf:timeout(second, infinity); +mod_opt_type(resend_on_timeout) -> + econf:either( + if_offline, + econf:bool()); +mod_opt_type(cache_size) -> + econf:pos_int(infinity); +mod_opt_type(cache_life_time) -> + econf:timeout(second, infinity); +mod_opt_type(queue_type) -> + econf:queue_type(). + +mod_options(Host) -> + [{max_ack_queue, 5000}, + {resume_timeout, timer:seconds(300)}, + {max_resume_timeout, undefined}, + {ack_timeout, timer:seconds(60)}, + {cache_size, ejabberd_option:cache_size(Host)}, + {cache_life_time, timer:hours(48)}, + {resend_on_timeout, false}, + {queue_type, ejabberd_option:queue_type(Host)}]. + +mod_doc() -> + #{desc => + ?T("This module adds support for " + "https://xmpp.org/extensions/xep-0198.html" + "[XEP-0198: Stream Management]. This protocol allows " + "active management of an XML stream between two XMPP " + "entities, including features for stanza acknowledgments " + "and stream resumption."), + opts => + [{max_ack_queue, + #{value => ?T("Size"), + desc => + ?T("This option specifies the maximum number of " + "unacknowledged stanzas queued for possible " + "retransmission. When the limit is exceeded, " + "the client session is terminated. The allowed " + "values are positive integers and 'infinity'. " + "You should be careful when setting this value " + "as it should not be set too low, otherwise, " + "you could kill sessions in a loop, before they " + "get the chance to finish proper session initiation. " + "It should definitely be set higher that the size " + "of the offline queue (for example at least 3 times " + "the value of the max offline queue and never lower " + "than '1000'). The default value is '5000'.")}}, + {resume_timeout, + #{value => "timeout()", + desc => + ?T("This option configures the (default) period of time " + "until a session times out if the connection is lost. " + "During this period of time, a client may resume its " + "session. Note that the client may request a different " + "timeout value, see the 'max_resume_timeout' option. " + "Setting it to '0' effectively disables session resumption. " + "The default value is '5' minutes.")}}, + {max_resume_timeout, + #{value => "timeout()", + desc => + ?T("A client may specify the period of time until a session " + "times out if the connection is lost. During this period " + "of time, the client may resume its session. This option " + "limits the period of time a client is permitted to request. " + "It must be set to a timeout equal to or larger than the " + "default 'resume_timeout'. By default, it is set to the " + "same value as the 'resume_timeout' option.")}}, + {ack_timeout, + #{value => "timeout()", + desc => + ?T("A time to wait for stanza acknowledgments. " + "Setting it to 'infinity' effectively disables the timeout. " + "The default value is '1' minute.")}}, + {resend_on_timeout, + #{value => "true | false | if_offline", + desc => + ?T("If this option is set to 'true', any message stanzas " + "that weren't acknowledged by the client will be resent " + "on session timeout. This behavior might often be desired, " + "but could have unexpected results under certain circumstances. " + "For example, a message that was sent to two resources might " + "get resent to one of them if the other one timed out. " + "Therefore, the default value for this option is 'false', " + "which tells ejabberd to generate an error message instead. " + "As an alternative, the option may be set to 'if_offline'. " + "In this case, unacknowledged messages are resent only if " + "no other resource is online when the session times out. " + "Otherwise, error messages are generated.")}}, + {queue_type, + #{value => "ram | file", + desc => + ?T("Same as top-level _`queue_type`_ option, but applied to this module only.")}}, + {cache_size, + #{value => "pos_integer() | infinity", + desc => + ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, + {cache_life_time, + #{value => "timeout()", + desc => + ?T("Same as top-level _`cache_life_time`_ option, " + "but applied to this module only. " + "The default value is '48 hours'.")}}]}. diff --git a/src/mod_stream_mgmt_opt.erl b/src/mod_stream_mgmt_opt.erl new file mode 100644 index 000000000..58d4fe1e7 --- /dev/null +++ b/src/mod_stream_mgmt_opt.erl @@ -0,0 +1,62 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_stream_mgmt_opt). + +-export([ack_timeout/1]). +-export([cache_life_time/1]). +-export([cache_size/1]). +-export([max_ack_queue/1]). +-export([max_resume_timeout/1]). +-export([queue_type/1]). +-export([resend_on_timeout/1]). +-export([resume_timeout/1]). + +-spec ack_timeout(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +ack_timeout(Opts) when is_map(Opts) -> + gen_mod:get_opt(ack_timeout, Opts); +ack_timeout(Host) -> + gen_mod:get_module_opt(Host, mod_stream_mgmt, ack_timeout). + +-spec cache_life_time(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +cache_life_time(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_life_time, Opts); +cache_life_time(Host) -> + gen_mod:get_module_opt(Host, mod_stream_mgmt, cache_life_time). + +-spec cache_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +cache_size(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_size, Opts); +cache_size(Host) -> + gen_mod:get_module_opt(Host, mod_stream_mgmt, cache_size). + +-spec max_ack_queue(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +max_ack_queue(Opts) when is_map(Opts) -> + gen_mod:get_opt(max_ack_queue, Opts); +max_ack_queue(Host) -> + gen_mod:get_module_opt(Host, mod_stream_mgmt, max_ack_queue). + +-spec max_resume_timeout(gen_mod:opts() | global | binary()) -> 'undefined' | non_neg_integer(). +max_resume_timeout(Opts) when is_map(Opts) -> + gen_mod:get_opt(max_resume_timeout, Opts); +max_resume_timeout(Host) -> + gen_mod:get_module_opt(Host, mod_stream_mgmt, max_resume_timeout). + +-spec queue_type(gen_mod:opts() | global | binary()) -> 'file' | 'ram'. +queue_type(Opts) when is_map(Opts) -> + gen_mod:get_opt(queue_type, Opts); +queue_type(Host) -> + gen_mod:get_module_opt(Host, mod_stream_mgmt, queue_type). + +-spec resend_on_timeout(gen_mod:opts() | global | binary()) -> 'false' | 'if_offline' | 'true'. +resend_on_timeout(Opts) when is_map(Opts) -> + gen_mod:get_opt(resend_on_timeout, Opts); +resend_on_timeout(Host) -> + gen_mod:get_module_opt(Host, mod_stream_mgmt, resend_on_timeout). + +-spec resume_timeout(gen_mod:opts() | global | binary()) -> non_neg_integer(). +resume_timeout(Opts) when is_map(Opts) -> + gen_mod:get_opt(resume_timeout, Opts); +resume_timeout(Host) -> + gen_mod:get_module_opt(Host, mod_stream_mgmt, resume_timeout). + diff --git a/src/mod_stun_disco.erl b/src/mod_stun_disco.erl new file mode 100644 index 000000000..c210868e7 --- /dev/null +++ b/src/mod_stun_disco.erl @@ -0,0 +1,714 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_stun_disco.erl +%%% Author : Holger Weiss +%%% Purpose : External Service Discovery (XEP-0215) +%%% Created : 18 Apr 2020 by Holger Weiss +%%% +%%% +%%% ejabberd, Copyright (C) 2020-2025 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_stun_disco). +-author('holger@zedat.fu-berlin.de'). +-protocol({xep, 215, '0.7', '20.04', "complete", ""}). + +-behaviour(gen_server). +-behaviour(gen_mod). + +%% gen_mod callbacks. +-export([start/2, + stop/1, + reload/3, + mod_opt_type/1, + mod_options/1, + depends/2]). +-export([mod_doc/0]). + +%% gen_server callbacks. +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). + +%% ejabberd_hooks callbacks. +-export([disco_local_features/5, stun_get_password/3]). + +%% gen_iq_handler callback. +-export([process_iq/1]). + +-include("logger.hrl"). +-include("translate.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). + +-define(STUN_MODULE, ejabberd_stun). + +-type host_or_hash() :: binary() | {hash, binary()}. +-type service_type() :: stun | stuns | turn | turns | undefined. + +-record(request, + {host :: binary() | inet:ip_address() | undefined, + port :: 0..65535 | undefined, + transport :: udp | tcp | undefined, + type :: service_type(), + restricted :: true | undefined}). + +-record(state, + {host :: binary(), + services :: [service()], + secret :: binary(), + ttl :: non_neg_integer()}). + +-type request() :: #request{}. +-type state() :: #state{}. + +%%-------------------------------------------------------------------- +%% gen_mod callbacks. +%%-------------------------------------------------------------------- +-spec start(binary(), gen_mod:opts()) -> {ok, pid()} | {error, any()}. +start(Host, Opts) -> + Proc = get_proc_name(Host), + gen_mod:start_child(?MODULE, Host, Opts, Proc). + +-spec stop(binary()) -> ok | {error, any()}. +stop(Host) -> + Proc = get_proc_name(Host), + gen_mod:stop_child(Proc). + +-spec reload(binary(), gen_mod:opts(), gen_mod:opts()) -> ok. +reload(Host, NewOpts, OldOpts) -> + cast(Host, {reload, NewOpts, OldOpts}). + +-spec depends(binary(), gen_mod:opts()) -> [{module(), hard | soft}]. +depends(_Host, _Opts) -> + []. + +-spec mod_opt_type(atom()) -> econf:validator(). +mod_opt_type(access) -> + econf:acl(); +mod_opt_type(credentials_lifetime) -> + econf:timeout(second); +mod_opt_type(offer_local_services) -> + econf:bool(); +mod_opt_type(secret) -> + econf:binary(); +mod_opt_type(services) -> + econf:list( + econf:and_then( + econf:options( + #{host => econf:either(econf:ip(), econf:binary()), + port => econf:port(), + type => econf:enum([stun, turn, stuns, turns]), + transport => econf:enum([tcp, udp]), + restricted => econf:bool()}, + [{required, [host]}]), + fun(Opts) -> + DefPort = fun(stun) -> 3478; + (turn) -> 3478; + (stuns) -> 5349; + (turns) -> 5349 + end, + DefTrns = fun(stun) -> udp; + (turn) -> udp; + (stuns) -> tcp; + (turns) -> tcp + end, + DefRstr = fun(stun) -> false; + (turn) -> true; + (stuns) -> false; + (turns) -> true + end, + Host = proplists:get_value(host, Opts), + Type = proplists:get_value(type, Opts, stun), + Port = proplists:get_value(port, Opts, DefPort(Type)), + Trns = proplists:get_value(transport, Opts, DefTrns(Type)), + Rstr = proplists:get_value(restricted, Opts, DefRstr(Type)), + #service{host = Host, + port = Port, + type = Type, + transport = Trns, + restricted = Rstr} + end)). + +-spec mod_options(binary()) -> [{services, [tuple()]} | {atom(), any()}]. +mod_options(_Host) -> + [{access, local}, + {credentials_lifetime, timer:hours(12)}, + {offer_local_services, true}, + {secret, undefined}, + {services, []}]. + +mod_doc() -> + #{desc => + ?T("This module allows XMPP clients to discover STUN/TURN services " + "and to obtain temporary credentials for using them as per " + "https://xmpp.org/extensions/xep-0215.html" + "[XEP-0215: External Service Discovery]."), + note => "added in 20.04", + opts => + [{access, + #{value => ?T("AccessName"), + desc => + ?T("This option defines which access rule will be used to " + "control who is allowed to discover STUN/TURN services " + "and to request temporary credentials. The default value " + "is 'local'.")}}, + {credentials_lifetime, + #{value => "timeout()", + desc => + ?T("The lifetime of temporary credentials offered to " + "clients. If ejabberd's built-in TURN service is used, " + "TURN relays allocated using temporary credentials will " + "be terminated shortly after the credentials expired. The " + "default value is '12 hours'. Note that restarting the " + "ejabberd node invalidates any temporary credentials " + "offered before the restart unless a 'secret' is " + "specified (see below).")}}, + {offer_local_services, + #{value => "true | false", + desc => + ?T("This option specifies whether local STUN/TURN services " + "configured as ejabberd listeners should be announced " + "automatically. Note that this will not include " + "TLS-enabled services, which must be configured manually " + "using the 'services' option (see below). For " + "non-anonymous TURN services, temporary credentials will " + "be offered to the client. The default value is " + "'true'.")}}, + {secret, + #{value => ?T("Text"), + desc => + ?T("The secret used for generating temporary credentials. If " + "this option isn't specified, a secret will be " + "auto-generated. However, a secret must be specified " + "explicitly if non-anonymous TURN services running on " + "other ejabberd nodes and/or external TURN 'services' are " + "configured. Also note that auto-generated secrets are " + "lost when the node is restarted, which invalidates any " + "credentials offered before the restart. Therefore, it's " + "recommended to explicitly specify a secret if clients " + "cache retrieved credentials (for later use) across " + "service restarts.")}}, + {services, + #{value => "[Service, ...]", + example => + ["services:", + " -", + " host: 203.0.113.3", + " port: 3478", + " type: stun", + " transport: udp", + " restricted: false", + " -", + " host: 203.0.113.3", + " port: 3478", + " type: turn", + " transport: udp", + " restricted: true", + " -", + " host: 2001:db8::3", + " port: 3478", + " type: stun", + " transport: udp", + " restricted: false", + " -", + " host: 2001:db8::3", + " port: 3478", + " type: turn", + " transport: udp", + " restricted: true", + " -", + " host: server.example.com", + " port: 5349", + " type: turns", + " transport: tcp", + " restricted: true"], + desc => + ?T("The list of services offered to clients. This list can " + "include STUN/TURN services running on any ejabberd node " + "and/or external services. However, if any listed TURN " + "service not running on the local ejabberd node requires " + "authentication, a 'secret' must be specified explicitly, " + "and must be shared with that service. This will only " + "work with ejabberd's built-in STUN/TURN server and with " + "external servers that support the same " + "https://tools.ietf.org/html/draft-uberti-behave-turn-rest-00" + "[REST API For Access To TURN Services]. Unless the " + "'offer_local_services' is set to 'false', the explicitly " + "listed services will be offered in addition to those " + "announced automatically.")}, + [{host, + #{value => ?T("Host"), + desc => + ?T("The hostname or IP address the STUN/TURN service is " + "listening on. For non-TLS services, it's recommended " + "to specify an IP address (to avoid additional DNS " + "lookup latency on the client side). For TLS services, " + "the hostname (or IP address) should match the " + "certificate. Specifying the 'host' option is " + "mandatory.")}}, + {port, + #{value => "1..65535", + desc => + ?T("The port number the STUN/TURN service is listening " + "on. The default port number is 3478 for non-TLS " + "services and 5349 for TLS services.")}}, + {type, + #{value => "stun | turn | stuns | turns", + desc => + ?T("The type of service. Must be 'stun' or 'turn' for " + "non-TLS services, 'stuns' or 'turns' for TLS services. " + "The default type is 'stun'.")}}, + {transport, + #{value => "tcp | udp", + desc => + ?T("The transport protocol supported by the service. The " + "default is 'udp' for non-TLS services and 'tcp' for " + "TLS services.")}}, + {restricted, + #{value => "true | false", + desc => + ?T("This option determines whether temporary credentials " + "for accessing the service are offered. The default is " + "'false' for STUN/STUNS services and 'true' for " + "TURN/TURNS services.")}}]}]}. + +%%-------------------------------------------------------------------- +%% gen_server callbacks. +%%-------------------------------------------------------------------- +-spec init(list()) -> {ok, state()}. +init([Host, Opts]) -> + process_flag(trap_exit, true), + Services = get_configured_services(Opts), + Secret = get_configured_secret(Opts), + TTL = get_configured_ttl(Opts), + register_iq_handlers(Host), + register_hooks(Host), + {ok, #state{host = Host, services = Services, secret = Secret, ttl = TTL}}. + +-spec handle_call(term(), {pid(), term()}, state()) + -> {reply, {turn_disco, [service()] | binary()}, state()} | + {noreply, state()}. +handle_call({get_services, JID, #request{host = ReqHost, + port = ReqPort, + type = ReqType, + transport = ReqTrns, + restricted = ReqRstr}}, _From, + #state{host = Host, + services = List0, + secret = Secret, + ttl = TTL} = State) -> + ?DEBUG("Getting STUN/TURN service list for ~ts", [jid:encode(JID)]), + Hash = <<(hash(jid:encode(JID)))/binary, (hash(Host))/binary>>, + List = lists:filtermap( + fun(#service{host = H, port = P, type = T, restricted = R}) + when (ReqHost /= undefined) and (H /= ReqHost); + (ReqPort /= undefined) and (P /= ReqPort); + (ReqType /= undefined) and (T /= ReqType); + (ReqTrns /= undefined) and (T /= ReqTrns); + (ReqRstr /= undefined) and (R /= ReqRstr) -> + false; + (#service{restricted = false}) -> + true; + (#service{restricted = true} = Service) -> + {true, add_credentials(Service, Hash, Secret, TTL)} + end, List0), + ?INFO_MSG("Offering STUN/TURN services to ~ts (~s)", + [jid:encode(JID), Hash]), + {reply, {turn_disco, List}, State}; +handle_call({get_password, Username}, _From, #state{secret = Secret} = State) -> + ?DEBUG("Getting STUN/TURN password for ~ts", [Username]), + Password = make_password(Username, Secret), + {reply, {turn_disco, Password}, State}; +handle_call(Request, From, State) -> + ?ERROR_MSG("Got unexpected request from ~p: ~p", [From, Request]), + {noreply, State}. + +-spec handle_cast(term(), state()) -> {noreply, state()}. +handle_cast({reload, NewOpts, _OldOpts}, #state{host = Host} = State) -> + ?DEBUG("Reloading STUN/TURN discovery configuration for ~ts", [Host]), + Services = get_configured_services(NewOpts), + Secret = get_configured_secret(NewOpts), + TTL = get_configured_ttl(NewOpts), + {noreply, State#state{services = Services, secret = Secret, ttl = TTL}}; +handle_cast(Request, State) -> + ?ERROR_MSG("Got unexpected request: ~p", [Request]), + {noreply, State}. + +-spec handle_info(term(), state()) -> {noreply, state()}. +handle_info(Info, State) -> + ?ERROR_MSG("Got unexpected info: ~p", [Info]), + {noreply, State}. + +-spec terminate(normal | shutdown | {shutdown, term()} | term(), state()) -> ok. +terminate(Reason, #state{host = Host}) -> + ?DEBUG("Stopping STUN/TURN discovery process for ~ts: ~p", + [Host, Reason]), + unregister_hooks(Host), + unregister_iq_handlers(Host). + +-spec code_change({down, term()} | term(), state(), term()) -> {ok, state()}. +code_change(_OldVsn, #state{host = Host} = State, _Extra) -> + ?DEBUG("Updating STUN/TURN discovery process for ~ts", [Host]), + {ok, State}. + +%%-------------------------------------------------------------------- +%% Register/unregister hooks. +%%-------------------------------------------------------------------- +-spec register_hooks(binary()) -> ok. +register_hooks(Host) -> + ejabberd_hooks:add(disco_local_features, Host, ?MODULE, + disco_local_features, 50), + ejabberd_hooks:add(stun_get_password, ?MODULE, + stun_get_password, 50). + +-spec unregister_hooks(binary()) -> ok. +unregister_hooks(Host) -> + ejabberd_hooks:delete(disco_local_features, Host, ?MODULE, + disco_local_features, 50), + case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of + false -> + ejabberd_hooks:delete(stun_get_password, ?MODULE, + stun_get_password, 50); + true -> + ok + end. + +%%-------------------------------------------------------------------- +%% Hook callbacks. +%%-------------------------------------------------------------------- +-spec disco_local_features(mod_disco:features_acc(), jid(), jid(), binary(), + binary()) -> mod_disco:features_acc(). +disco_local_features(empty, From, To, Node, Lang) -> + disco_local_features({result, []}, From, To, Node, Lang); +disco_local_features({result, OtherFeatures} = Acc, From, + #jid{lserver = LServer}, <<"">>, _Lang) -> + Access = mod_stun_disco_opt:access(LServer), + case acl:match_rule(LServer, Access, From) of + allow -> + ?DEBUG("Announcing feature to ~ts", [jid:encode(From)]), + {result, [?NS_EXTDISCO_2 | OtherFeatures]}; + deny -> + ?DEBUG("Not announcing feature to ~ts", [jid:encode(From)]), + Acc + end; +disco_local_features(Acc, _From, _To, _Node, _Lang) -> + Acc. + +-spec stun_get_password(any(), binary(), binary()) + -> binary() | {stop, binary()}. +stun_get_password(<<>>, Username, _Realm) -> + case binary:split(Username, <<$:>>) of + [Expiration, <<_UserHash:8/binary, HostHash:8/binary>>] -> + try binary_to_integer(Expiration) of + ExpireTime -> + case erlang:system_time(second) of + Now when Now < ExpireTime -> + ?DEBUG("Looking up password for: ~ts", [Username]), + {stop, get_password(Username, HostHash)}; + Now when Now >= ExpireTime -> + ?INFO_MSG("Credentials expired: ~ts", [Username]), + {stop, <<>>} + end + catch _:badarg -> + ?DEBUG("Non-numeric expiration field: ~ts", [Username]), + <<>> + end; + _ -> + ?DEBUG("Not an ephemeral username: ~ts", [Username]), + <<>> + end; +stun_get_password(Acc, _Username, _Realm) -> + Acc. + +%%-------------------------------------------------------------------- +%% IQ handlers. +%%-------------------------------------------------------------------- +-spec register_iq_handlers(binary()) -> ok. +register_iq_handlers(Host) -> + gen_iq_handler:add_iq_handler(ejabberd_local, Host, + ?NS_EXTDISCO_2, ?MODULE, process_iq). + +-spec unregister_iq_handlers(binary()) -> ok. +unregister_iq_handlers(Host) -> + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_EXTDISCO_2). + +-spec process_iq(iq()) -> iq(). +process_iq(#iq{type = get, + sub_els = [#services{type = ReqType}]} = IQ) -> + Request = #request{type = ReqType}, + process_iq_get(IQ, Request); +process_iq(#iq{type = get, + sub_els = [#credentials{ + services = [#service{ + host = ReqHost, + port = ReqPort, + type = ReqType, + transport = ReqTrns, + name = <<>>, + username = <<>>, + password = <<>>, + expires = undefined, + restricted = undefined, + action = undefined, + xdata = undefined}]}]} = IQ) -> + % Accepting the 'transport' request attribute is an ejabberd extension. + Request = #request{host = ReqHost, + port = ReqPort, + type = ReqType, + transport = ReqTrns, + restricted = true}, + process_iq_get(IQ, Request); +process_iq(#iq{type = set, lang = Lang} = IQ) -> + Txt = ?T("Value 'set' of 'type' attribute is not allowed"), + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); +process_iq(#iq{lang = Lang} = IQ) -> + Txt = ?T("No module is handling this query"), + xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)). + +-spec process_iq_get(iq(), request()) -> iq(). +process_iq_get(#iq{from = From, to = #jid{lserver = Host}, lang = Lang} = IQ, + #request{restricted = Restricted} = Request) -> + Access = mod_stun_disco_opt:access(Host), + case acl:match_rule(Host, Access, From) of + allow -> + ?DEBUG("Performing external service discovery for ~ts", + [jid:encode(From)]), + case get_services(Host, From, Request) of + {ok, Services} when Restricted -> % A request. + xmpp:make_iq_result(IQ, #credentials{services = Services}); + {ok, Services} -> + xmpp:make_iq_result(IQ, #services{list = Services}); + {error, timeout} -> % Has been logged already. + Txt = ?T("Service list retrieval timed out"), + Err = xmpp:err_internal_server_error(Txt, Lang), + xmpp:make_error(IQ, Err) + end; + deny -> + ?DEBUG("Won't perform external service discovery for ~ts", + [jid:encode(From)]), + Txt = ?T("Access denied by service policy"), + xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)) + end. + +%%-------------------------------------------------------------------- +%% Internal functions. +%%-------------------------------------------------------------------- +-spec get_configured_services(gen_mod:opts()) -> [service()]. +get_configured_services(Opts) -> + LocalServices = case mod_stun_disco_opt:offer_local_services(Opts) of + true -> + ?DEBUG("Discovering local services", []), + find_local_services(); + false -> + ?DEBUG("Won't discover local services", []), + [] + end, + dedup(LocalServices ++ mod_stun_disco_opt:services(Opts)). + +-spec get_configured_secret(gen_mod:opts()) -> binary(). +get_configured_secret(Opts) -> + case mod_stun_disco_opt:secret(Opts) of + undefined -> + ?DEBUG("Auto-generating secret", []), + new_secret(); + Secret -> + ?DEBUG("Using configured secret", []), + Secret + end. + +-spec get_configured_ttl(gen_mod:opts()) -> non_neg_integer(). +get_configured_ttl(Opts) -> + mod_stun_disco_opt:credentials_lifetime(Opts) div 1000. + +-spec new_secret() -> binary(). +new_secret() -> + p1_rand:bytes(20). + +-spec add_credentials(service(), binary(), binary(), non_neg_integer()) + -> service(). +add_credentials(Service, Hash, Secret, TTL) -> + ExpireAt = erlang:system_time(second) + TTL, + Username = make_username(ExpireAt, Hash), + Password = make_password(Username, Secret), + ?DEBUG("Created ephemeral credentials: ~s | ~s", [Username, Password]), + Service#service{username = Username, + password = Password, + expires = seconds_to_timestamp(ExpireAt)}. + +-spec make_username(non_neg_integer(), binary()) -> binary(). +make_username(ExpireAt, Hash) -> + <<(integer_to_binary(ExpireAt))/binary, $:, Hash/binary>>. + +-spec make_password(binary(), binary()) -> binary(). +make_password(Username, Secret) -> + base64:encode(misc:crypto_hmac(sha, Secret, Username)). + +-spec get_password(binary(), binary()) -> binary(). +get_password(Username, HostHash) -> + try call({hash, HostHash}, {get_password, Username}) of + {turn_disco, Password} -> + Password + catch + exit:{timeout, _} -> + ?ERROR_MSG("Asking ~ts for password timed out", [HostHash]), + <<>>; + exit:{noproc, _} -> % Can be triggered by bogus Username. + ?DEBUG("Cannot retrieve password for ~ts", [Username]), + <<>> + end. + +-spec get_services(binary(), jid(), request()) + -> {ok, [service()]} | {error, timeout}. +get_services(Host, JID, Request) -> + try call(Host, {get_services, JID, Request}) of + {turn_disco, Services} -> + {ok, Services} + catch + exit:{timeout, _} -> + ?ERROR_MSG("Asking ~ts for services timed out", [Host]), + {error, timeout} + end. + +-spec find_local_services() -> [service()]. +find_local_services() -> + ParseListener = fun(Listener) -> parse_listener(Listener) end, + lists:flatmap(ParseListener, ejabberd_option:listen()). + +-spec parse_listener(ejabberd_listener:listener()) -> [service()]. +parse_listener({_EndPoint, ?STUN_MODULE, #{tls := true}}) -> + ?DEBUG("Ignoring TLS-enabled STUN/TURN listener", []), + []; % Avoid certificate hostname issues. +parse_listener({{Port, _Addr, Transport}, ?STUN_MODULE, Opts}) -> + case get_listener_ips(Opts) of + {undefined, undefined} -> + ?INFO_MSG("Won't auto-announce STUN/TURN service on port ~B (~s) " + "without public IP address, please specify " + "'turn_ipv4_address' and optionally 'turn_ipv6_address'", + [Port, Transport]), + []; + {IPv4Addr, IPv6Addr} -> + lists:flatmap( + fun(undefined) -> + []; + (Addr) -> + StunService = #service{host = Addr, + port = Port, + transport = Transport, + restricted = false, + type = stun}, + case Opts of + #{use_turn := true} -> + ?INFO_MSG("Going to offer STUN/TURN service: " + "~s (~s)", + [addr_to_str(Addr, Port), Transport]), + [StunService, + #service{host = Addr, + port = Port, + transport = Transport, + restricted = is_restricted(Opts), + type = turn}]; + #{use_turn := false} -> + ?INFO_MSG("Going to offer STUN service: " + "~s (~s)", + [addr_to_str(Addr, Port), Transport]), + [StunService] + end + end, [IPv4Addr, IPv6Addr]) + end; +parse_listener({_EndPoint, Module, _Opts}) -> + ?DEBUG("Ignoring ~s listener", [Module]), + []. + +-spec get_listener_ips(map()) -> {inet:ip4_address() | undefined, + inet:ip6_address() | undefined}. +get_listener_ips(#{ip := {0, 0, 0, 0}} = Opts) -> + {get_turn_ipv4_addr(Opts), undefined}; +get_listener_ips(#{ip := {0, 0, 0, 0, 0, 0, 0, 0}} = Opts) -> + {get_turn_ipv4_addr(Opts), get_turn_ipv6_addr(Opts)}; % Assume dual-stack. +get_listener_ips(#{ip := {127, _, _, _}} = Opts) -> + {get_turn_ipv4_addr(Opts), undefined}; +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}) -> + {undefined, IP}. + +-spec get_turn_ipv4_addr(map()) -> inet:ip4_address() | undefined. +get_turn_ipv4_addr(#{turn_ipv4_address := {_, _, _, _} = TurnIP}) -> + TurnIP; +get_turn_ipv4_addr(#{turn_ipv4_address := undefined}) -> + case misc:get_my_ipv4_address() of + {127, _, _, _} -> + undefined; + IP -> + IP + end. + +-spec get_turn_ipv6_addr(map()) -> inet:ip6_address() | undefined. +get_turn_ipv6_addr(#{turn_ipv6_address := {_, _, _, _, _, _, _, _} = TurnIP}) -> + TurnIP; +get_turn_ipv6_addr(#{turn_ipv6_address := undefined}) -> + case misc:get_my_ipv6_address() of + {0, 0, 0, 0, 0, 0, 0, 1} -> + undefined; + IP -> + IP + end. + +-spec is_restricted(map()) -> boolean(). +is_restricted(#{auth_type := user}) -> + true; +is_restricted(#{auth_type := anonymous}) -> + false. + +-spec call(host_or_hash(), term()) -> term(). +call(Host, Request) -> + Proc = get_proc_name(Host), + gen_server:call(Proc, Request, timer:seconds(15)). + +-spec cast(host_or_hash(), term()) -> ok. +cast(Host, Request) -> + Proc = get_proc_name(Host), + gen_server:cast(Proc, Request). + +-spec get_proc_name(host_or_hash()) -> atom(). +get_proc_name(Host) when is_binary(Host) -> + get_proc_name({hash, hash(Host)}); +get_proc_name({hash, HostHash}) -> + gen_mod:get_module_proc(HostHash, ?MODULE). + +-spec hash(binary()) -> binary(). +hash(Host) -> + str:to_hexlist(binary_part(crypto:hash(sha, Host), 0, 4)). + +-spec dedup(list()) -> list(). +dedup([]) -> []; +dedup([H | T]) -> [H | [E || E <- dedup(T), E /= H]]. + +-spec seconds_to_timestamp(non_neg_integer()) -> erlang:timestamp(). +seconds_to_timestamp(Seconds) -> + {Seconds div 1000000, Seconds rem 1000000, 0}. + +-spec addr_to_str(inet:ip_address(), 0..65535) -> iolist(). +addr_to_str({_, _, _, _, _, _, _, _} = Addr, Port) -> + [$[, inet_parse:ntoa(Addr), $], $:, integer_to_list(Port)]; +addr_to_str({_, _, _, _} = Addr, Port) -> + [inet_parse:ntoa(Addr), $:, integer_to_list(Port)]. diff --git a/src/mod_stun_disco_opt.erl b/src/mod_stun_disco_opt.erl new file mode 100644 index 000000000..43b8102e6 --- /dev/null +++ b/src/mod_stun_disco_opt.erl @@ -0,0 +1,41 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_stun_disco_opt). + +-export([access/1]). +-export([credentials_lifetime/1]). +-export([offer_local_services/1]). +-export([secret/1]). +-export([services/1]). + +-spec access(gen_mod:opts() | global | binary()) -> 'local' | acl:acl(). +access(Opts) when is_map(Opts) -> + gen_mod:get_opt(access, Opts); +access(Host) -> + gen_mod:get_module_opt(Host, mod_stun_disco, access). + +-spec credentials_lifetime(gen_mod:opts() | global | binary()) -> pos_integer(). +credentials_lifetime(Opts) when is_map(Opts) -> + gen_mod:get_opt(credentials_lifetime, Opts); +credentials_lifetime(Host) -> + gen_mod:get_module_opt(Host, mod_stun_disco, credentials_lifetime). + +-spec offer_local_services(gen_mod:opts() | global | binary()) -> boolean(). +offer_local_services(Opts) when is_map(Opts) -> + gen_mod:get_opt(offer_local_services, Opts); +offer_local_services(Host) -> + gen_mod:get_module_opt(Host, mod_stun_disco, offer_local_services). + +-spec secret(gen_mod:opts() | global | binary()) -> 'undefined' | binary(). +secret(Opts) when is_map(Opts) -> + gen_mod:get_opt(secret, Opts); +secret(Host) -> + gen_mod:get_module_opt(Host, mod_stun_disco, secret). + +-spec services(gen_mod:opts() | global | binary()) -> [tuple()]. +services(Opts) when is_map(Opts) -> + gen_mod:get_opt(services, Opts); +services(Host) -> + gen_mod:get_module_opt(Host, mod_stun_disco, services). + diff --git a/src/mod_time.erl b/src/mod_time.erl index c82fde41c..fbebf03a7 100644 --- a/src/mod_time.erl +++ b/src/mod_time.erl @@ -1,12 +1,12 @@ %%%---------------------------------------------------------------------- %%% File : mod_time.erl %%% Author : Alexey Shchepin -%%% Purpose : +%%% Purpose : %%% Purpose : %%% Created : 18 Jan 2003 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -28,82 +28,49 @@ -author('alexey@process-one.net'). +-protocol({xep, 202, '2.0', '2.1.0', "complete", ""}). + -behaviour(gen_mod). --export([start/2, stop/1, process_local_iq90/3, - process_local_iq/3]). +-export([start/2, stop/1, reload/3, process_local_iq/1, + mod_options/1, depends/2, mod_doc/0]). - % TODO: Remove once XEP-0090 is Obsolete - --include("ejabberd.hrl"). -include("logger.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). +-include("translate.hrl"). --include("jlib.hrl"). +start(_Host, _Opts) -> + {ok, [{iq_handler, ejabberd_local, ?NS_TIME, process_local_iq}]}. -start(Host, Opts) -> - IQDisc = gen_mod:get_opt(iqdisc, Opts, fun gen_iq_handler:check_type/1, - one_queue), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, - ?NS_TIME90, ?MODULE, process_local_iq90, - IQDisc), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, - ?NS_TIME, ?MODULE, process_local_iq, IQDisc). +stop(_Host) -> + ok. -stop(Host) -> - gen_iq_handler:remove_iq_handler(ejabberd_local, Host, - ?NS_TIME90), - gen_iq_handler:remove_iq_handler(ejabberd_local, Host, - ?NS_TIME). +reload(_Host, _NewOpts, _OldOpts) -> + ok. -%% TODO: Remove this function once XEP-0090 is Obsolete -process_local_iq90(_From, _To, - #iq{type = Type, sub_el = SubEl} = IQ) -> - case Type of - set -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; - get -> - UTC = jlib:timestamp_to_iso(calendar:universal_time()), - IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"query">>, - attrs = [{<<"xmlns">>, ?NS_TIME90}], - children = - [#xmlel{name = <<"utc">>, attrs = [], - children = [{xmlcdata, UTC}]}]}]} - end. +-spec process_local_iq(iq()) -> iq(). +process_local_iq(#iq{type = set, lang = Lang} = IQ) -> + Txt = ?T("Value 'set' of 'type' attribute is not allowed"), + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); +process_local_iq(#iq{type = get} = IQ) -> + Now = os:timestamp(), + Now_universal = calendar:now_to_universal_time(Now), + Now_local = calendar:universal_time_to_local_time(Now_universal), + Seconds_diff = + calendar:datetime_to_gregorian_seconds(Now_local) - + calendar:datetime_to_gregorian_seconds(Now_universal), + {Hd, Md, _} = calendar:seconds_to_time(abs(Seconds_diff)), + xmpp:make_iq_result(IQ, #time{tzo = {Hd, Md}, utc = Now}). -process_local_iq(_From, _To, - #iq{type = Type, sub_el = SubEl} = IQ) -> - case Type of - set -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; - get -> - Now = now(), - Now_universal = calendar:now_to_universal_time(Now), - Now_local = calendar:now_to_local_time(Now), - {UTC, UTC_diff} = jlib:timestamp_to_iso(Now_universal, - utc), - Seconds_diff = - calendar:datetime_to_gregorian_seconds(Now_local) - - calendar:datetime_to_gregorian_seconds(Now_universal), - {Hd, Md, _} = - calendar:seconds_to_time(abs(Seconds_diff)), - {_, TZO_diff} = jlib:timestamp_to_iso({{0, 1, 1}, - {0, 0, 0}}, - {sign(Seconds_diff), {Hd, Md}}), - IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"time">>, - attrs = [{<<"xmlns">>, ?NS_TIME}], - children = - [#xmlel{name = <<"tzo">>, attrs = [], - children = [{xmlcdata, TZO_diff}]}, - #xmlel{name = <<"utc">>, attrs = [], - children = - [{xmlcdata, - <>}]}]}]} - end. +depends(_Host, _Opts) -> + []. -sign(N) when N < 0 -> <<"-">>; -sign(_) -> <<"+">>. +mod_options(_Host) -> + []. + +mod_doc() -> + #{desc => + ?T("This module adds support for " + "https://xmpp.org/extensions/xep-0202.html" + "[XEP-0202: Entity Time]. In other words, " + "the module reports server's system time.")}. diff --git a/src/mod_vcard.erl b/src/mod_vcard.erl index 7d2860ee6..740ca41d2 100644 --- a/src/mod_vcard.erl +++ b/src/mod_vcard.erl @@ -5,7 +5,7 @@ %%% Created : 2 Jan 2003 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -27,106 +27,175 @@ -author('alexey@process-one.net'). +-protocol({xep, 54, '1.2', '0.1.0', "complete", ""}). +-protocol({xep, 55, '1.3', '0.1.0', "complete", ""}). +-protocol({xep, 153, '1.1', '17.09', "complete", ""}). + +-behaviour(gen_server). -behaviour(gen_mod). --export([start/2, init/3, stop/1, get_sm_features/5, - process_local_iq/3, process_sm_iq/3, reindex_vcards/0, - remove_user/2, export/1, import/1, import/3]). +-export([start/2, stop/1, get_sm_features/5, mod_options/1, mod_doc/0, + process_local_iq/1, process_sm_iq/1, string2lower/1, + remove_user/2, export/1, import_info/0, import/5, import_start/2, + depends/2, process_search/1, process_vcard/1, get_vcard/2, + disco_items/5, disco_features/5, disco_identity/5, + vcard_iq_set/1, mod_opt_type/1, set_vcard/3, make_vcard_search/4]). +-export([init/1, handle_call/3, handle_cast/2, + handle_info/2, terminate/2, code_change/3]). +-export([route/1]). +-export([webadmin_menu_hostuser/4, webadmin_page_hostuser/4]). + +-import(ejabberd_web_admin, [make_command/4, make_command/2, make_table/2]). --include("ejabberd.hrl"). -include("logger.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). +-include("mod_vcard.hrl"). +-include("translate.hrl"). --include("jlib.hrl"). +-include("ejabberd_http.hrl"). +-include("ejabberd_web_admin.hrl"). --define(JUD_MATCHES, 30). +-define(VCARD_CACHE, vcard_cache). --record(vcard_search, - {us, user, luser, fn, lfn, family, lfamily, given, - lgiven, middle, lmiddle, nickname, lnickname, bday, - lbday, ctry, lctry, locality, llocality, email, lemail, - orgname, lorgname, orgunit, lorgunit}). +-callback init(binary(), gen_mod:opts()) -> any(). +-callback stop(binary()) -> any(). +-callback import(binary(), binary(), [binary()]) -> ok. +-callback get_vcard(binary(), binary()) -> {ok, [xmlel()]} | error. +-callback set_vcard(binary(), binary(), + xmlel(), #vcard_search{}) -> {atomic, any()}. +-callback search_fields(binary()) -> [{binary(), binary()}]. +-callback search_reported(binary()) -> [{binary(), binary()}]. +-callback search(binary(), [{binary(), [binary()]}], boolean(), + infinity | pos_integer()) -> [{binary(), binary()}]. +-callback remove_user(binary(), binary()) -> {atomic, any()}. +-callback is_search_supported(binary()) -> boolean(). +-callback use_cache(binary()) -> boolean(). +-callback cache_nodes(binary()) -> [node()]. --record(vcard, {us = {<<"">>, <<"">>} :: {binary(), binary()} | binary(), - vcard = #xmlel{} :: xmlel()}). +-optional_callbacks([use_cache/1, cache_nodes/1]). --define(PROCNAME, ejabberd_mod_vcard). +-record(state, {hosts :: [binary()], server_host :: binary()}). +%%==================================================================== +%% gen_mod callbacks +%%==================================================================== start(Host, Opts) -> - case gen_mod:db_type(Opts) of - mnesia -> - mnesia:create_table(vcard, - [{disc_only_copies, [node()]}, - {attributes, record_info(fields, vcard)}]), - mnesia:create_table(vcard_search, - [{disc_copies, [node()]}, - {attributes, - record_info(fields, vcard_search)}]), - update_tables(), - mnesia:add_table_index(vcard_search, luser), - mnesia:add_table_index(vcard_search, lfn), - mnesia:add_table_index(vcard_search, lfamily), - mnesia:add_table_index(vcard_search, lgiven), - mnesia:add_table_index(vcard_search, lmiddle), - mnesia:add_table_index(vcard_search, lnickname), - mnesia:add_table_index(vcard_search, lbday), - mnesia:add_table_index(vcard_search, lctry), - mnesia:add_table_index(vcard_search, llocality), - mnesia:add_table_index(vcard_search, lemail), - mnesia:add_table_index(vcard_search, lorgname), - mnesia:add_table_index(vcard_search, lorgunit); - _ -> ok - end, - ejabberd_hooks:add(remove_user, Host, ?MODULE, - remove_user, 50), - IQDisc = gen_mod:get_opt(iqdisc, Opts, fun gen_iq_handler:check_type/1, - one_queue), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, - ?NS_VCARD, ?MODULE, process_local_iq, IQDisc), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, - ?NS_VCARD, ?MODULE, process_sm_iq, IQDisc), - ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, - get_sm_features, 50), - MyHost = gen_mod:get_opt_host(Host, Opts, - <<"vjud.@HOST@">>), - Search = gen_mod:get_opt(search, Opts, - fun(B) when is_boolean(B) -> B end, - true), - register(gen_mod:get_module_proc(Host, ?PROCNAME), - spawn(?MODULE, init, [MyHost, Host, Search])). - -init(Host, ServerHost, Search) -> - case Search of - false -> loop(Host, ServerHost); - _ -> - ejabberd_router:register_route(Host), - loop(Host, ServerHost) - end. - -loop(Host, ServerHost) -> - receive - {route, From, To, Packet} -> - case catch do_route(ServerHost, From, To, Packet) of - {'EXIT', Reason} -> ?ERROR_MSG("~p", [Reason]); - _ -> ok - end, - loop(Host, ServerHost); - stop -> ejabberd_router:unregister_route(Host), ok; - _ -> loop(Host, ServerHost) - end. + gen_mod:start_child(?MODULE, Host, Opts). stop(Host) -> - ejabberd_hooks:delete(remove_user, Host, ?MODULE, - remove_user, 50), - gen_iq_handler:remove_iq_handler(ejabberd_local, Host, - ?NS_VCARD), - gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, - ?NS_VCARD), - ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, - get_sm_features, 50), - Proc = gen_mod:get_module_proc(Host, ?PROCNAME), - Proc ! stop, - {wait, Proc}. + gen_mod:stop_child(?MODULE, Host). +%%==================================================================== +%% gen_server callbacks +%%==================================================================== +init([Host|_]) -> + process_flag(trap_exit, true), + Opts = gen_mod:get_module_opts(Host, ?MODULE), + Mod = gen_mod:db_mod(Opts, ?MODULE), + Mod:init(Host, Opts), + init_cache(Mod, Host, Opts), + ejabberd_hooks:add(remove_user, Host, ?MODULE, + remove_user, 50), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, + ?NS_VCARD, ?MODULE, process_local_iq), + gen_iq_handler:add_iq_handler(ejabberd_sm, Host, + ?NS_VCARD, ?MODULE, process_sm_iq), + ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, + get_sm_features, 50), + ejabberd_hooks:add(vcard_iq_set, Host, ?MODULE, vcard_iq_set, 50), + ejabberd_hooks:add(webadmin_menu_hostuser, Host, ?MODULE, webadmin_menu_hostuser, 50), + ejabberd_hooks:add(webadmin_page_hostuser, Host, ?MODULE, webadmin_page_hostuser, 50), + MyHosts = gen_mod:get_opt_hosts(Opts), + Search = mod_vcard_opt:search(Opts), + if Search -> + lists:foreach( + fun(MyHost) -> + ejabberd_hooks:add( + disco_local_items, MyHost, ?MODULE, disco_items, 100), + ejabberd_hooks:add( + disco_local_features, MyHost, ?MODULE, disco_features, 100), + ejabberd_hooks:add( + disco_local_identity, MyHost, ?MODULE, disco_identity, 100), + gen_iq_handler:add_iq_handler( + ejabberd_local, MyHost, ?NS_SEARCH, ?MODULE, process_search), + gen_iq_handler:add_iq_handler( + ejabberd_local, MyHost, ?NS_VCARD, ?MODULE, process_vcard), + gen_iq_handler:add_iq_handler( + ejabberd_local, MyHost, ?NS_DISCO_ITEMS, mod_disco, + process_local_iq_items), + gen_iq_handler:add_iq_handler( + ejabberd_local, MyHost, ?NS_DISCO_INFO, mod_disco, + process_local_iq_info), + case Mod:is_search_supported(Host) of + false -> + ?WARNING_MSG("vCard search functionality is " + "not implemented for ~ts backend", + [mod_vcard_opt:db_type(Opts)]); + true -> + ejabberd_router:register_route( + MyHost, Host, {apply, ?MODULE, route}) + end + end, MyHosts); + true -> + ok + end, + {ok, #state{hosts = MyHosts, server_host = Host}}. + +handle_call(Call, From, State) -> + ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Call]), + {noreply, State}. + +handle_cast(Cast, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Cast]), + {noreply, State}. + +handle_info({route, Packet}, State) -> + try route(Packet) + catch + Class:Reason:StackTrace -> + ?ERROR_MSG("Failed to route packet:~n~ts~n** ~ts", + [xmpp:pp(Packet), + misc:format_exception(2, Class, Reason, StackTrace)]) + end, + {noreply, State}; +handle_info(Info, State) -> + ?WARNING_MSG("Unexpected info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, #state{hosts = MyHosts, server_host = Host}) -> + ejabberd_hooks:delete(remove_user, Host, ?MODULE, remove_user, 50), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_VCARD), + gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_VCARD), + ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, get_sm_features, 50), + ejabberd_hooks:delete(vcard_iq_set, Host, ?MODULE, vcard_iq_set, 50), + ejabberd_hooks:delete(webadmin_menu_hostuser, Host, ?MODULE, webadmin_menu_hostuser, 50), + ejabberd_hooks:delete(webadmin_page_hostuser, Host, ?MODULE, webadmin_page_hostuser, 50), + Mod = gen_mod:db_mod(Host, ?MODULE), + Mod:stop(Host), + lists:foreach( + fun(MyHost) -> + ejabberd_router:unregister_route(MyHost), + ejabberd_hooks:delete(disco_local_items, MyHost, ?MODULE, disco_items, 100), + ejabberd_hooks:delete(disco_local_features, MyHost, ?MODULE, disco_features, 100), + ejabberd_hooks:delete(disco_local_identity, MyHost, ?MODULE, disco_identity, 100), + gen_iq_handler:remove_iq_handler(ejabberd_local, MyHost, ?NS_SEARCH), + gen_iq_handler:remove_iq_handler(ejabberd_local, MyHost, ?NS_VCARD), + gen_iq_handler:remove_iq_handler(ejabberd_local, MyHost, ?NS_DISCO_ITEMS), + gen_iq_handler:remove_iq_handler(ejabberd_local, MyHost, ?NS_DISCO_INFO) + end, MyHosts). + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +-spec route(stanza()) -> ok. +route(#iq{} = IQ) -> + ejabberd_router:process_iq(IQ); +route(_) -> + ok. + +-spec get_sm_features({error, stanza_error()} | empty | {result, [binary()]}, + jid(), jid(), binary(), binary()) -> + {error, stanza_error()} | empty | {result, [binary()]}. get_sm_features({error, _Error} = Acc, _From, _To, _Node, _Lang) -> Acc; @@ -135,129 +204,164 @@ get_sm_features(Acc, _From, _To, Node, _Lang) -> <<"">> -> case Acc of {result, Features} -> - {result, [?NS_DISCO_INFO, ?NS_VCARD | Features]}; - empty -> {result, [?NS_DISCO_INFO, ?NS_VCARD]} + {result, [?NS_VCARD | Features]}; + empty -> {result, [?NS_VCARD]} end; _ -> Acc end. -process_local_iq(_From, _To, - #iq{type = Type, lang = Lang, sub_el = SubEl} = IQ) -> - case Type of - set -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; - get -> - IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"vCard">>, - attrs = [{<<"xmlns">>, ?NS_VCARD}], - children = - [#xmlel{name = <<"FN">>, attrs = [], - children = - [{xmlcdata, <<"ejabberd">>}]}, - #xmlel{name = <<"URL">>, attrs = [], - children = [{xmlcdata, ?EJABBERD_URI}]}, - #xmlel{name = <<"DESC">>, attrs = [], - children = - [{xmlcdata, - <<(translate:translate(Lang, - <<"Erlang Jabber Server">>))/binary, - "\nCopyright (c) 2002-2015 ProcessOne">>}]}, - #xmlel{name = <<"BDAY">>, attrs = [], - children = - [{xmlcdata, <<"2002-11-16">>}]}]}]} +-spec process_local_iq(iq()) -> iq(). +process_local_iq(#iq{type = set, lang = Lang} = IQ) -> + Txt = ?T("Value 'set' of 'type' attribute is not allowed"), + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); +process_local_iq(#iq{type = get, to = To, lang = Lang} = IQ) -> + ServerHost = ejabberd_router:host_of_route(To#jid.lserver), + VCard = case mod_vcard_opt:vcard(ServerHost) of + undefined -> + #vcard_temp{fn = <<"ejabberd">>, + url = ejabberd_config:get_uri(), + desc = misc:get_descr(Lang, ?T("Erlang XMPP Server")), + bday = <<"2002-11-16">>}; + V -> + V + end, + xmpp:make_iq_result(IQ, VCard). + +-spec process_sm_iq(iq()) -> iq(). +process_sm_iq(#iq{type = set, lang = Lang, from = From} = IQ) -> + #jid{lserver = LServer} = From, + case lists:member(LServer, ejabberd_option:hosts()) of + true -> + case ejabberd_hooks:run_fold(vcard_iq_set, LServer, IQ, []) of + drop -> ignore; + #stanza_error{} = Err -> xmpp:make_error(IQ, Err); + _ -> xmpp:make_iq_result(IQ) + end; + false -> + Txt = ?T("The query is only allowed from local users"), + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)) + end; +process_sm_iq(#iq{type = get, from = From, to = To, lang = Lang} = IQ) -> + #jid{luser = LUser, lserver = LServer} = To, + case get_vcard(LUser, LServer) of + error -> + Txt = ?T("Database failure"), + xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)); + [] -> + xmpp:make_iq_result(IQ, #vcard_temp{}); + Els -> + IQ#iq{type = result, to = From, from = To, sub_els = Els} end. -process_sm_iq(From, To, - #iq{type = Type, sub_el = SubEl} = IQ) -> - case Type of - set -> - #jid{user = User, lserver = LServer} = From, - case lists:member(LServer, ?MYHOSTS) of - true -> - set_vcard(User, LServer, SubEl), - IQ#iq{type = result, sub_el = []}; - false -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]} - end; - get -> - #jid{luser = LUser, lserver = LServer} = To, - case get_vcard(LUser, LServer) of - error -> - IQ#iq{type = error, - sub_el = [SubEl, ?ERR_INTERNAL_SERVER_ERROR]}; - [] -> - IQ#iq{type = result, - sub_el = [#xmlel{name = <<"vCard">>, - attrs = [{<<"xmlns">>, ?NS_VCARD}], - children = []}]}; - Els -> IQ#iq{type = result, sub_el = Els} - end - end. +-spec process_vcard(iq()) -> iq(). +process_vcard(#iq{type = set, lang = Lang} = IQ) -> + Txt = ?T("Value 'set' of 'type' attribute is not allowed"), + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); +process_vcard(#iq{type = get, lang = Lang} = IQ) -> + xmpp:make_iq_result( + IQ, #vcard_temp{fn = <<"ejabberd/mod_vcard">>, + url = ejabberd_config:get_uri(), + desc = misc:get_descr(Lang, ?T("ejabberd vCard module"))}). +-spec process_search(iq()) -> iq(). +process_search(#iq{type = get, to = To, lang = Lang} = IQ) -> + ServerHost = ejabberd_router:host_of_route(To#jid.lserver), + xmpp:make_iq_result(IQ, mk_search_form(To, ServerHost, Lang)); +process_search(#iq{type = set, to = To, lang = Lang, + sub_els = [#search{xdata = #xdata{type = submit, + fields = Fs}}]} = IQ) -> + ServerHost = ejabberd_router:host_of_route(To#jid.lserver), + ResultXData = search_result(Lang, To, ServerHost, Fs), + xmpp:make_iq_result(IQ, #search{xdata = ResultXData}); +process_search(#iq{type = set, lang = Lang} = IQ) -> + Txt = ?T("Incorrect data form"), + xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)). + +-spec disco_items({error, stanza_error()} | {result, [disco_item()]} | empty, + jid(), jid(), binary(), binary()) -> + {error, stanza_error()} | {result, [disco_item()]}. +disco_items(empty, _From, _To, <<"">>, _Lang) -> + {result, []}; +disco_items(empty, _From, _To, _Node, Lang) -> + {error, xmpp:err_item_not_found(?T("No services available"), Lang)}; +disco_items(Acc, _From, _To, _Node, _Lang) -> + Acc. + +-spec disco_features({error, stanza_error()} | {result, [binary()]} | empty, + jid(), jid(), binary(), binary()) -> + {error, stanza_error()} | {result, [binary()]}. +disco_features({error, _Error} = Acc, _From, _To, _Node, _Lang) -> + Acc; +disco_features(Acc, _From, _To, <<"">>, _Lang) -> + Features = case Acc of + {result, Fs} -> Fs; + empty -> [] + end, + {result, [?NS_DISCO_INFO, ?NS_DISCO_ITEMS, + ?NS_VCARD, ?NS_SEARCH | Features]}; +disco_features(empty, _From, _To, _Node, Lang) -> + Txt = ?T("No features available"), + {error, xmpp:err_item_not_found(Txt, Lang)}; +disco_features(Acc, _From, _To, _Node, _Lang) -> + Acc. + +-spec disco_identity([identity()], jid(), jid(), + binary(), binary()) -> [identity()]. +disco_identity(Acc, _From, To, <<"">>, Lang) -> + Host = ejabberd_router:host_of_route(To#jid.lserver), + Name = mod_vcard_opt:name(Host), + [#identity{category = <<"directory">>, + type = <<"user">>, + name = translate:translate(Lang, Name)}|Acc]; +disco_identity(Acc, _From, _To, _Node, _Lang) -> + Acc. + +-spec get_vcard(binary(), binary()) -> [xmlel()] | error. get_vcard(LUser, LServer) -> - get_vcard(LUser, LServer, - gen_mod:db_type(LServer, ?MODULE)). - -get_vcard(LUser, LServer, mnesia) -> - US = {LUser, LServer}, - F = fun () -> mnesia:read({vcard, US}) end, - case mnesia:transaction(F) of - {atomic, Rs} -> - lists:map(fun (R) -> R#vcard.vcard end, Rs); - {aborted, _Reason} -> error - end; -get_vcard(LUser, LServer, odbc) -> - Username = ejabberd_odbc:escape(LUser), - case catch odbc_queries:get_vcard(LServer, Username) of - {selected, [<<"vcard">>], [[SVCARD]]} -> - case xml_stream:parse_element(SVCARD) of - {error, _Reason} -> error; - VCARD -> [VCARD] - end; - {selected, [<<"vcard">>], []} -> []; - _ -> error - end; -get_vcard(LUser, LServer, riak) -> - case ejabberd_riak:get(vcard, vcard_schema(), {LUser, LServer}) of - {ok, R} -> - [R#vcard.vcard]; - {error, notfound} -> - []; - _ -> - error + Mod = gen_mod:db_mod(LServer, ?MODULE), + Result = case use_cache(Mod, LServer) of + true -> + ets_cache:lookup( + ?VCARD_CACHE, {LUser, LServer}, + fun() -> Mod:get_vcard(LUser, LServer) end); + false -> + Mod:get_vcard(LUser, LServer) + end, + case Result of + {ok, Els} -> Els; + error -> error end. -set_vcard(User, LServer, VCARD) -> - FN = xml:get_path_s(VCARD, [{elem, <<"FN">>}, cdata]), - Family = xml:get_path_s(VCARD, +-spec make_vcard_search(binary(), binary(), binary(), xmlel()) -> #vcard_search{}. +make_vcard_search(User, LUser, LServer, VCARD) -> + FN = fxml:get_path_s(VCARD, [{elem, <<"FN">>}, cdata]), + Family = fxml:get_path_s(VCARD, [{elem, <<"N">>}, {elem, <<"FAMILY">>}, cdata]), - Given = xml:get_path_s(VCARD, + Given = fxml:get_path_s(VCARD, [{elem, <<"N">>}, {elem, <<"GIVEN">>}, cdata]), - Middle = xml:get_path_s(VCARD, + Middle = fxml:get_path_s(VCARD, [{elem, <<"N">>}, {elem, <<"MIDDLE">>}, cdata]), - Nickname = xml:get_path_s(VCARD, + Nickname = fxml:get_path_s(VCARD, [{elem, <<"NICKNAME">>}, cdata]), - BDay = xml:get_path_s(VCARD, + BDay = fxml:get_path_s(VCARD, [{elem, <<"BDAY">>}, cdata]), - CTRY = xml:get_path_s(VCARD, + CTRY = fxml:get_path_s(VCARD, [{elem, <<"ADR">>}, {elem, <<"CTRY">>}, cdata]), - Locality = xml:get_path_s(VCARD, + Locality = fxml:get_path_s(VCARD, [{elem, <<"ADR">>}, {elem, <<"LOCALITY">>}, cdata]), - EMail1 = xml:get_path_s(VCARD, + EMail1 = fxml:get_path_s(VCARD, [{elem, <<"EMAIL">>}, {elem, <<"USERID">>}, cdata]), - EMail2 = xml:get_path_s(VCARD, + EMail2 = fxml:get_path_s(VCARD, [{elem, <<"EMAIL">>}, cdata]), - OrgName = xml:get_path_s(VCARD, + OrgName = fxml:get_path_s(VCARD, [{elem, <<"ORG">>}, {elem, <<"ORGNAME">>}, cdata]), - OrgUnit = xml:get_path_s(VCARD, + OrgUnit = fxml:get_path_s(VCARD, [{elem, <<"ORG">>}, {elem, <<"ORGUNIT">>}, cdata]), EMail = case EMail1 of <<"">> -> EMail2; _ -> EMail1 end, - LUser = jlib:nodeprep(User), LFN = string2lower(FN), LFamily = string2lower(Family), LGiven = string2lower(Given), @@ -269,893 +373,389 @@ set_vcard(User, LServer, VCARD) -> LEMail = string2lower(EMail), LOrgName = string2lower(OrgName), LOrgUnit = string2lower(OrgUnit), - if (LUser == error) -> - {error, badarg}; - true -> - case gen_mod:db_type(LServer, ?MODULE) of - mnesia -> - US = {LUser, LServer}, - F = fun () -> - mnesia:write(#vcard{us = US, vcard = VCARD}), - mnesia:write(#vcard_search{us = US, - user = {User, LServer}, - luser = LUser, fn = FN, - lfn = LFN, - family = Family, - lfamily = LFamily, - given = Given, - lgiven = LGiven, - middle = Middle, - lmiddle = LMiddle, - nickname = Nickname, - lnickname = LNickname, - bday = BDay, - lbday = LBDay, - ctry = CTRY, - lctry = LCTRY, - locality = Locality, - llocality = LLocality, - email = EMail, - lemail = LEMail, - orgname = OrgName, - lorgname = LOrgName, - orgunit = OrgUnit, - lorgunit = LOrgUnit}) - end, - mnesia:transaction(F); - riak -> - US = {LUser, LServer}, - ejabberd_riak:put(#vcard{us = US, vcard = VCARD}, - vcard_schema(), - [{'2i', [{<<"user">>, User}, - {<<"luser">>, LUser}, - {<<"fn">>, FN}, - {<<"lfn">>, LFN}, - {<<"family">>, Family}, - {<<"lfamily">>, LFamily}, - {<<"given">>, Given}, - {<<"lgiven">>, LGiven}, - {<<"middle">>, Middle}, - {<<"lmiddle">>, LMiddle}, - {<<"nickname">>, Nickname}, - {<<"lnickname">>, LNickname}, - {<<"bday">>, BDay}, - {<<"lbday">>, LBDay}, - {<<"ctry">>, CTRY}, - {<<"lctry">>, LCTRY}, - {<<"locality">>, Locality}, - {<<"llocality">>, LLocality}, - {<<"email">>, EMail}, - {<<"lemail">>, LEMail}, - {<<"orgname">>, OrgName}, - {<<"lorgname">>, LOrgName}, - {<<"orgunit">>, OrgUnit}, - {<<"lorgunit">>, LOrgUnit}]}]); - odbc -> - Username = ejabberd_odbc:escape(User), - LUsername = ejabberd_odbc:escape(LUser), - SVCARD = - ejabberd_odbc:escape(xml:element_to_binary(VCARD)), - SFN = ejabberd_odbc:escape(FN), - SLFN = ejabberd_odbc:escape(LFN), - SFamily = ejabberd_odbc:escape(Family), - SLFamily = ejabberd_odbc:escape(LFamily), - SGiven = ejabberd_odbc:escape(Given), - SLGiven = ejabberd_odbc:escape(LGiven), - SMiddle = ejabberd_odbc:escape(Middle), - SLMiddle = ejabberd_odbc:escape(LMiddle), - SNickname = ejabberd_odbc:escape(Nickname), - SLNickname = ejabberd_odbc:escape(LNickname), - SBDay = ejabberd_odbc:escape(BDay), - SLBDay = ejabberd_odbc:escape(LBDay), - SCTRY = ejabberd_odbc:escape(CTRY), - SLCTRY = ejabberd_odbc:escape(LCTRY), - SLocality = ejabberd_odbc:escape(Locality), - SLLocality = ejabberd_odbc:escape(LLocality), - SEMail = ejabberd_odbc:escape(EMail), - SLEMail = ejabberd_odbc:escape(LEMail), - SOrgName = ejabberd_odbc:escape(OrgName), - SLOrgName = ejabberd_odbc:escape(LOrgName), - SOrgUnit = ejabberd_odbc:escape(OrgUnit), - SLOrgUnit = ejabberd_odbc:escape(LOrgUnit), - odbc_queries:set_vcard(LServer, LUsername, SBDay, SCTRY, - SEMail, SFN, SFamily, SGiven, SLBDay, - SLCTRY, SLEMail, SLFN, SLFamily, - SLGiven, SLLocality, SLMiddle, - SLNickname, SLOrgName, SLOrgUnit, - SLocality, SMiddle, SNickname, SOrgName, - SOrgUnit, SVCARD, Username) - end, - ejabberd_hooks:run(vcard_set, LServer, - [LUser, LServer, VCARD]) + US = {LUser, LServer}, + #vcard_search{us = US, + user = {User, LServer}, + luser = LUser, fn = FN, + lfn = LFN, + family = Family, + lfamily = LFamily, + given = Given, + lgiven = LGiven, + middle = Middle, + lmiddle = LMiddle, + nickname = Nickname, + lnickname = LNickname, + bday = BDay, + lbday = LBDay, + ctry = CTRY, + lctry = LCTRY, + locality = Locality, + llocality = LLocality, + email = EMail, + lemail = LEMail, + orgname = OrgName, + lorgname = LOrgName, + orgunit = OrgUnit, + lorgunit = LOrgUnit}. + +-spec vcard_iq_set(iq()) -> iq() | {stop, stanza_error()}. +vcard_iq_set(#iq{from = #jid{user = FromUser, lserver = FromLServer}, + to = #jid{user = ToUser, lserver = ToLServer}, + lang = Lang}) + when (FromUser /= ToUser) or (FromLServer /= ToLServer) -> + Txt = ?T("User not allowed to perform an IQ set on another user's vCard."), + {stop, xmpp:err_forbidden(Txt, Lang)}; +vcard_iq_set(#iq{from = From, lang = Lang, sub_els = [VCard]} = IQ) -> + #jid{user = User, lserver = LServer} = From, + case set_vcard(User, LServer, VCard) of + {error, badarg} -> + %% Should not be here? + Txt = ?T("Nodeprep has failed"), + {stop, xmpp:err_internal_server_error(Txt, Lang)}; + {error, not_implemented} -> + Txt = ?T("Updating the vCard is not supported by the vCard storage backend"), + {stop, xmpp:err_feature_not_implemented(Txt, Lang)}; + ok -> + IQ + end; +vcard_iq_set(Acc) -> + Acc. + +-spec set_vcard(binary(), binary(), xmlel() | vcard_temp()) -> + {error, badarg | not_implemented | binary()} | ok. +set_vcard(User, LServer, VCARD) -> + case jid:nodeprep(User) of + error -> + {error, badarg}; + LUser -> + VCardEl = xmpp:encode(VCARD), + VCardSearch = make_vcard_search(User, LUser, LServer, VCardEl), + Mod = gen_mod:db_mod(LServer, ?MODULE), + case Mod:set_vcard(LUser, LServer, VCardEl, VCardSearch) of + {atomic, ok} -> + ets_cache:delete(?VCARD_CACHE, {LUser, LServer}, + cache_nodes(Mod, LServer)), + ok; + {atomic, Error} -> + {error, Error} + end end. +-spec string2lower(binary()) -> binary(). string2lower(String) -> - case stringprep:tolower(String) of + case stringprep:tolower_nofilter(String) of Lower when is_binary(Lower) -> Lower; - error -> str:to_lower(String) + error -> String end. --define(TLFIELD(Type, Label, Var), - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, Type}, - {<<"label">>, translate:translate(Lang, Label)}, - {<<"var">>, Var}], - children = []}). +-spec mk_tfield(binary(), binary(), binary()) -> xdata_field(). +mk_tfield(Label, Var, Lang) -> + #xdata_field{type = 'text-single', + label = translate:translate(Lang, Label), + var = Var}. --define(FORM(JID), - [#xmlel{name = <<"instructions">>, attrs = [], - children = - [{xmlcdata, - translate:translate(Lang, - <<"You need an x:data capable client to " - "search">>)}]}, - #xmlel{name = <<"x">>, - attrs = - [{<<"xmlns">>, ?NS_XDATA}, {<<"type">>, <<"form">>}], - children = - [#xmlel{name = <<"title">>, attrs = [], - children = - [{xmlcdata, - <<(translate:translate(Lang, - <<"Search users in ">>))/binary, - (jlib:jid_to_string(JID))/binary>>}]}, - #xmlel{name = <<"instructions">>, attrs = [], - children = - [{xmlcdata, - translate:translate(Lang, - <<"Fill in the form to search for any matching " - "Jabber User (Add * to the end of field " - "to match substring)">>)}]}, - ?TLFIELD(<<"text-single">>, <<"User">>, <<"user">>), - ?TLFIELD(<<"text-single">>, <<"Full Name">>, <<"fn">>), - ?TLFIELD(<<"text-single">>, <<"Name">>, <<"first">>), - ?TLFIELD(<<"text-single">>, <<"Middle Name">>, - <<"middle">>), - ?TLFIELD(<<"text-single">>, <<"Family Name">>, - <<"last">>), - ?TLFIELD(<<"text-single">>, <<"Nickname">>, <<"nick">>), - ?TLFIELD(<<"text-single">>, <<"Birthday">>, <<"bday">>), - ?TLFIELD(<<"text-single">>, <<"Country">>, <<"ctry">>), - ?TLFIELD(<<"text-single">>, <<"City">>, <<"locality">>), - ?TLFIELD(<<"text-single">>, <<"Email">>, <<"email">>), - ?TLFIELD(<<"text-single">>, <<"Organization Name">>, - <<"orgname">>), - ?TLFIELD(<<"text-single">>, <<"Organization Unit">>, - <<"orgunit">>)]}]). +-spec mk_field(binary(), binary()) -> xdata_field(). +mk_field(Var, Val) -> + #xdata_field{var = Var, values = [Val]}. -do_route(ServerHost, From, To, Packet) -> - #jid{user = User, resource = Resource} = To, - if (User /= <<"">>) or (Resource /= <<"">>) -> - Err = jlib:make_error_reply(Packet, - ?ERR_SERVICE_UNAVAILABLE), - ejabberd_router:route(To, From, Err); - true -> - IQ = jlib:iq_query_info(Packet), - case IQ of - #iq{type = Type, xmlns = ?NS_SEARCH, lang = Lang, - sub_el = SubEl} -> - case Type of - set -> - XDataEl = find_xdata_el(SubEl), - case XDataEl of - false -> - Err = jlib:make_error_reply(Packet, - ?ERR_BAD_REQUEST), - ejabberd_router:route(To, From, Err); - _ -> - XData = jlib:parse_xdata_submit(XDataEl), - case XData of - invalid -> - Err = jlib:make_error_reply(Packet, - ?ERR_BAD_REQUEST), - ejabberd_router:route(To, From, Err); - _ -> - ResIQ = IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"query">>, - attrs = - [{<<"xmlns">>, - ?NS_SEARCH}], - children = - [#xmlel{name = - <<"x">>, - attrs = - [{<<"xmlns">>, - ?NS_XDATA}, - {<<"type">>, - <<"result">>}], - children - = - search_result(Lang, - To, - ServerHost, - XData)}]}]}, - ejabberd_router:route(To, From, - jlib:iq_to_xml(ResIQ)) - end - end; - get -> - ResIQ = IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"query">>, - attrs = - [{<<"xmlns">>, - ?NS_SEARCH}], - children = ?FORM(To)}]}, - ejabberd_router:route(To, From, jlib:iq_to_xml(ResIQ)) - end; - #iq{type = Type, xmlns = ?NS_DISCO_INFO, lang = Lang} -> - case Type of - set -> - Err = jlib:make_error_reply(Packet, ?ERR_NOT_ALLOWED), - ejabberd_router:route(To, From, Err); - get -> - Info = ejabberd_hooks:run_fold(disco_info, ServerHost, - [], - [ServerHost, ?MODULE, - <<"">>, <<"">>]), - ResIQ = IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"query">>, - attrs = - [{<<"xmlns">>, - ?NS_DISCO_INFO}], - children = - [#xmlel{name = - <<"identity">>, - attrs = - [{<<"category">>, - <<"directory">>}, - {<<"type">>, - <<"user">>}, - {<<"name">>, - translate:translate(Lang, - <<"vCard User Search">>)}], - children = []}, - #xmlel{name = - <<"feature">>, - attrs = - [{<<"var">>, - ?NS_DISCO_INFO}], - children = []}, - #xmlel{name = - <<"feature">>, - attrs = - [{<<"var">>, - ?NS_SEARCH}], - children = []}, - #xmlel{name = - <<"feature">>, - attrs = - [{<<"var">>, - ?NS_VCARD}], - children = []}] - ++ Info}]}, - ejabberd_router:route(To, From, jlib:iq_to_xml(ResIQ)) - end; - #iq{type = Type, xmlns = ?NS_DISCO_ITEMS} -> - case Type of - set -> - Err = jlib:make_error_reply(Packet, ?ERR_NOT_ALLOWED), - ejabberd_router:route(To, From, Err); - get -> - ResIQ = IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"query">>, - attrs = - [{<<"xmlns">>, - ?NS_DISCO_ITEMS}], - children = []}]}, - ejabberd_router:route(To, From, jlib:iq_to_xml(ResIQ)) - end; - #iq{type = get, xmlns = ?NS_VCARD, lang = Lang} -> - ResIQ = IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"vCard">>, - attrs = [{<<"xmlns">>, ?NS_VCARD}], - children = iq_get_vcard(Lang)}]}, - ejabberd_router:route(To, From, jlib:iq_to_xml(ResIQ)); - _ -> - Err = jlib:make_error_reply(Packet, - ?ERR_SERVICE_UNAVAILABLE), - ejabberd_router:route(To, From, Err) - end +-spec mk_search_form(jid(), binary(), binary()) -> search(). +mk_search_form(JID, ServerHost, Lang) -> + Title = <<(translate:translate(Lang, ?T("Search users in ")))/binary, + (jid:encode(JID))/binary>>, + Mod = gen_mod:db_mod(ServerHost, ?MODULE), + SearchFields = Mod:search_fields(ServerHost), + Fs = [mk_tfield(Label, Var, Lang) || {Label, Var} <- SearchFields], + X = #xdata{type = form, + title = Title, + instructions = [make_instructions(Mod, Lang)], + fields = Fs}, + #search{instructions = + translate:translate( + Lang, ?T("You need an x:data capable client to search")), + xdata = X}. + +-spec make_instructions(module(), binary()) -> binary(). +make_instructions(Mod, Lang) -> + Fill = translate:translate( + Lang, + ?T("Fill in the form to search for any matching " + "XMPP User")), + Add = translate:translate( + Lang, + ?T(" (Add * to the end of field to match substring)")), + case Mod of + mod_vcard_mnesia -> Fill; + _ -> str:concat(Fill, Add) end. -iq_get_vcard(Lang) -> - [#xmlel{name = <<"FN">>, attrs = [], - children = [{xmlcdata, <<"ejabberd/mod_vcard">>}]}, - #xmlel{name = <<"URL">>, attrs = [], - children = [{xmlcdata, ?EJABBERD_URI}]}, - #xmlel{name = <<"DESC">>, attrs = [], - children = - [{xmlcdata, - <<(translate:translate(Lang, - <<"ejabberd vCard module">>))/binary, - "\nCopyright (c) 2003-2015 ProcessOne">>}]}]. +-spec search_result(binary(), jid(), binary(), [xdata_field()]) -> xdata(). +search_result(Lang, JID, ServerHost, XFields) -> + Mod = gen_mod:db_mod(ServerHost, ?MODULE), + Reported = [mk_tfield(Label, Var, Lang) || + {Label, Var} <- Mod:search_reported(ServerHost)], + #xdata{type = result, + title = <<(translate:translate(Lang, + ?T("Search Results for ")))/binary, + (jid:encode(JID))/binary>>, + reported = Reported, + items = lists:map(fun (Item) -> item_to_field(Item) end, + search(ServerHost, XFields))}. -find_xdata_el(#xmlel{children = SubEls}) -> - find_xdata_el1(SubEls). +-spec item_to_field([{binary(), binary()}]) -> [xdata_field()]. +item_to_field(Items) -> + [mk_field(Var, Value) || {Var, Value} <- Items]. -find_xdata_el1([]) -> false; -find_xdata_el1([#xmlel{name = Name, attrs = Attrs, - children = SubEls} - | Els]) -> - case xml:get_attr_s(<<"xmlns">>, Attrs) of - ?NS_XDATA -> - #xmlel{name = Name, attrs = Attrs, children = SubEls}; - _ -> find_xdata_el1(Els) - end; -find_xdata_el1([_ | Els]) -> find_xdata_el1(Els). - --define(LFIELD(Label, Var), - #xmlel{name = <<"field">>, - attrs = - [{<<"label">>, translate:translate(Lang, Label)}, - {<<"var">>, Var}], - children = []}). - -search_result(Lang, JID, ServerHost, Data) -> - [#xmlel{name = <<"title">>, attrs = [], - children = - [{xmlcdata, - <<(translate:translate(Lang, - <<"Search Results for ">>))/binary, - (jlib:jid_to_string(JID))/binary>>}]}, - #xmlel{name = <<"reported">>, attrs = [], - children = - [?TLFIELD(<<"text-single">>, <<"Jabber ID">>, - <<"jid">>), - ?TLFIELD(<<"text-single">>, <<"Full Name">>, <<"fn">>), - ?TLFIELD(<<"text-single">>, <<"Name">>, <<"first">>), - ?TLFIELD(<<"text-single">>, <<"Middle Name">>, - <<"middle">>), - ?TLFIELD(<<"text-single">>, <<"Family Name">>, - <<"last">>), - ?TLFIELD(<<"text-single">>, <<"Nickname">>, <<"nick">>), - ?TLFIELD(<<"text-single">>, <<"Birthday">>, <<"bday">>), - ?TLFIELD(<<"text-single">>, <<"Country">>, <<"ctry">>), - ?TLFIELD(<<"text-single">>, <<"City">>, <<"locality">>), - ?TLFIELD(<<"text-single">>, <<"Email">>, <<"email">>), - ?TLFIELD(<<"text-single">>, <<"Organization Name">>, - <<"orgname">>), - ?TLFIELD(<<"text-single">>, <<"Organization Unit">>, - <<"orgunit">>)]}] - ++ - lists:map(fun (R) -> record_to_item(ServerHost, R) end, - search(ServerHost, Data)). - --define(FIELD(Var, Val), - #xmlel{name = <<"field">>, attrs = [{<<"var">>, Var}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, Val}]}]}). - -record_to_item(LServer, - [Username, FN, Family, Given, Middle, Nickname, BDay, - CTRY, Locality, EMail, OrgName, OrgUnit]) -> - #xmlel{name = <<"item">>, attrs = [], - children = - [?FIELD(<<"jid">>, - <>), - ?FIELD(<<"fn">>, FN), ?FIELD(<<"last">>, Family), - ?FIELD(<<"first">>, Given), - ?FIELD(<<"middle">>, Middle), - ?FIELD(<<"nick">>, Nickname), ?FIELD(<<"bday">>, BDay), - ?FIELD(<<"ctry">>, CTRY), - ?FIELD(<<"locality">>, Locality), - ?FIELD(<<"email">>, EMail), - ?FIELD(<<"orgname">>, OrgName), - ?FIELD(<<"orgunit">>, OrgUnit)]}; -record_to_item(_LServer, #vcard_search{} = R) -> - {User, Server} = R#vcard_search.user, - #xmlel{name = <<"item">>, attrs = [], - children = - [?FIELD(<<"jid">>, <>), - ?FIELD(<<"fn">>, (R#vcard_search.fn)), - ?FIELD(<<"last">>, (R#vcard_search.family)), - ?FIELD(<<"first">>, (R#vcard_search.given)), - ?FIELD(<<"middle">>, (R#vcard_search.middle)), - ?FIELD(<<"nick">>, (R#vcard_search.nickname)), - ?FIELD(<<"bday">>, (R#vcard_search.bday)), - ?FIELD(<<"ctry">>, (R#vcard_search.ctry)), - ?FIELD(<<"locality">>, (R#vcard_search.locality)), - ?FIELD(<<"email">>, (R#vcard_search.email)), - ?FIELD(<<"orgname">>, (R#vcard_search.orgname)), - ?FIELD(<<"orgunit">>, (R#vcard_search.orgunit))]}. - -search(LServer, Data) -> - DBType = gen_mod:db_type(LServer, ?MODULE), - MatchSpec = make_matchspec(LServer, Data, DBType), - AllowReturnAll = gen_mod:get_module_opt(LServer, ?MODULE, allow_return_all, - fun(B) when is_boolean(B) -> B end, - false), - search(LServer, MatchSpec, AllowReturnAll, DBType). - -search(LServer, MatchSpec, AllowReturnAll, mnesia) -> - if (MatchSpec == #vcard_search{_ = '_'}) and - not AllowReturnAll -> - []; - true -> - case catch mnesia:dirty_select(vcard_search, - [{MatchSpec, [], ['$_']}]) - of - {'EXIT', Reason} -> ?ERROR_MSG("~p", [Reason]), []; - Rs -> - case gen_mod:get_module_opt(LServer, ?MODULE, matches, - fun(infinity) -> infinity; - (I) when is_integer(I), - I>0 -> - I - end, ?JUD_MATCHES) of - infinity -> - Rs; - Val -> - lists:sublist(Rs, Val) - end - end - end; -search(LServer, MatchSpec, AllowReturnAll, odbc) -> - if (MatchSpec == <<"">>) and not AllowReturnAll -> []; - true -> - Limit = case gen_mod:get_module_opt(LServer, ?MODULE, matches, - fun(infinity) -> infinity; - (I) when is_integer(I), - I>0 -> - I - end, ?JUD_MATCHES) of - infinity -> - <<"">>; - Val -> - [<<" LIMIT ">>, - jlib:integer_to_binary(Val)] - end, - case catch ejabberd_odbc:sql_query(LServer, - [<<"select username, fn, family, given, " - "middle, nickname, bday, ctry, " - "locality, email, orgname, orgunit " - "from vcard_search ">>, - MatchSpec, Limit, <<";">>]) - of - {selected, - [<<"username">>, <<"fn">>, <<"family">>, <<"given">>, - <<"middle">>, <<"nickname">>, <<"bday">>, <<"ctry">>, - <<"locality">>, <<"email">>, <<"orgname">>, - <<"orgunit">>], - Rs} - when is_list(Rs) -> - Rs; - Error -> ?ERROR_MSG("~p", [Error]), [] - end - end; -search(_LServer, _MatchSpec, _AllowReturnAll, riak) -> - []. - -make_matchspec(LServer, Data, mnesia) -> - GlobMatch = #vcard_search{_ = '_'}, - Match = filter_fields(Data, GlobMatch, LServer, mnesia), - Match; -make_matchspec(LServer, Data, odbc) -> - filter_fields(Data, <<"">>, LServer, odbc); -make_matchspec(_LServer, _Data, riak) -> - []. - -filter_fields([], Match, _LServer, mnesia) -> Match; -filter_fields([], Match, _LServer, odbc) -> - case Match of - <<"">> -> <<"">>; - _ -> [<<" where ">>, Match] - end; -filter_fields([{SVar, [Val]} | Ds], Match, LServer, - mnesia) - when is_binary(Val) and (Val /= <<"">>) -> - LVal = string2lower(Val), - NewMatch = case SVar of - <<"user">> -> - case gen_mod:get_module_opt(LServer, ?MODULE, - search_all_hosts, - fun(B) when is_boolean(B) -> - B - end, true) - of - true -> Match#vcard_search{luser = make_val(LVal)}; - false -> - Host = find_my_host(LServer), - Match#vcard_search{us = {make_val(LVal), Host}} - end; - <<"fn">> -> Match#vcard_search{lfn = make_val(LVal)}; - <<"last">> -> - Match#vcard_search{lfamily = make_val(LVal)}; - <<"first">> -> - Match#vcard_search{lgiven = make_val(LVal)}; - <<"middle">> -> - Match#vcard_search{lmiddle = make_val(LVal)}; - <<"nick">> -> - Match#vcard_search{lnickname = make_val(LVal)}; - <<"bday">> -> - Match#vcard_search{lbday = make_val(LVal)}; - <<"ctry">> -> - Match#vcard_search{lctry = make_val(LVal)}; - <<"locality">> -> - Match#vcard_search{llocality = make_val(LVal)}; - <<"email">> -> - Match#vcard_search{lemail = make_val(LVal)}; - <<"orgname">> -> - Match#vcard_search{lorgname = make_val(LVal)}; - <<"orgunit">> -> - Match#vcard_search{lorgunit = make_val(LVal)}; - _ -> Match - end, - filter_fields(Ds, NewMatch, LServer, mnesia); -filter_fields([{SVar, [Val]} | Ds], Match, LServer, - odbc) - when is_binary(Val) and (Val /= <<"">>) -> - LVal = string2lower(Val), - NewMatch = case SVar of - <<"user">> -> make_val(Match, <<"lusername">>, LVal); - <<"fn">> -> make_val(Match, <<"lfn">>, LVal); - <<"last">> -> make_val(Match, <<"lfamily">>, LVal); - <<"first">> -> make_val(Match, <<"lgiven">>, LVal); - <<"middle">> -> make_val(Match, <<"lmiddle">>, LVal); - <<"nick">> -> make_val(Match, <<"lnickname">>, LVal); - <<"bday">> -> make_val(Match, <<"lbday">>, LVal); - <<"ctry">> -> make_val(Match, <<"lctry">>, LVal); - <<"locality">> -> - make_val(Match, <<"llocality">>, LVal); - <<"email">> -> make_val(Match, <<"lemail">>, LVal); - <<"orgname">> -> make_val(Match, <<"lorgname">>, LVal); - <<"orgunit">> -> make_val(Match, <<"lorgunit">>, LVal); - _ -> Match - end, - filter_fields(Ds, NewMatch, LServer, odbc); -filter_fields([_ | Ds], Match, LServer, DBType) -> - filter_fields(Ds, Match, LServer, DBType). - -make_val(Match, Field, Val) -> - Condition = case str:suffix(<<"*">>, Val) of - true -> - Val1 = str:substr(Val, 1, byte_size(Val) - 1), - SVal = <<(ejabberd_odbc:escape_like(Val1))/binary, - "%">>, - [Field, <<" LIKE '">>, SVal, <<"'">>]; - _ -> - SVal = ejabberd_odbc:escape(Val), - [Field, <<" = '">>, SVal, <<"'">>] - end, - case Match of - <<"">> -> Condition; - _ -> [Match, <<" and ">>, Condition] - end. - -make_val(Val) -> - case str:suffix(<<"*">>, Val) of - true -> [str:substr(Val, 1, byte_size(Val) - 1)] ++ '_'; - _ -> Val - end. - -find_my_host(LServer) -> - Parts = str:tokens(LServer, <<".">>), - find_my_host(Parts, ?MYHOSTS). - -find_my_host([], _Hosts) -> ?MYNAME; -find_my_host([_ | Tail] = Parts, Hosts) -> - Domain = parts_to_string(Parts), - case lists:member(Domain, Hosts) of - true -> Domain; - false -> find_my_host(Tail, Hosts) - end. - -parts_to_string(Parts) -> - str:strip(list_to_binary( - lists:map(fun (S) -> <> end, Parts)), - right, $.). +-spec search(binary(), [xdata_field()]) -> [binary()]. +search(LServer, XFields) -> + Data = [{Var, Vals} || #xdata_field{var = Var, values = Vals} <- XFields], + Mod = gen_mod:db_mod(LServer, ?MODULE), + AllowReturnAll = mod_vcard_opt:allow_return_all(LServer), + MaxMatch = mod_vcard_opt:matches(LServer), + Mod:search(LServer, Data, AllowReturnAll, MaxMatch). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -set_vcard_t(R, _) -> - US = R#vcard.us, - User = US, - VCARD = R#vcard.vcard, - FN = xml:get_path_s(VCARD, [{elem, <<"FN">>}, cdata]), - Family = xml:get_path_s(VCARD, - [{elem, <<"N">>}, {elem, <<"FAMILY">>}, cdata]), - Given = xml:get_path_s(VCARD, - [{elem, <<"N">>}, {elem, <<"GIVEN">>}, cdata]), - Middle = xml:get_path_s(VCARD, - [{elem, <<"N">>}, {elem, <<"MIDDLE">>}, cdata]), - Nickname = xml:get_path_s(VCARD, - [{elem, <<"NICKNAME">>}, cdata]), - BDay = xml:get_path_s(VCARD, - [{elem, <<"BDAY">>}, cdata]), - CTRY = xml:get_path_s(VCARD, - [{elem, <<"ADR">>}, {elem, <<"CTRY">>}, cdata]), - Locality = xml:get_path_s(VCARD, - [{elem, <<"ADR">>}, {elem, <<"LOCALITY">>}, - cdata]), - EMail = xml:get_path_s(VCARD, - [{elem, <<"EMAIL">>}, cdata]), - OrgName = xml:get_path_s(VCARD, - [{elem, <<"ORG">>}, {elem, <<"ORGNAME">>}, cdata]), - OrgUnit = xml:get_path_s(VCARD, - [{elem, <<"ORG">>}, {elem, <<"ORGUNIT">>}, cdata]), - {LUser, _LServer} = US, - LFN = string2lower(FN), - LFamily = string2lower(Family), - LGiven = string2lower(Given), - LMiddle = string2lower(Middle), - LNickname = string2lower(Nickname), - LBDay = string2lower(BDay), - LCTRY = string2lower(CTRY), - LLocality = string2lower(Locality), - LEMail = string2lower(EMail), - LOrgName = string2lower(OrgName), - LOrgUnit = string2lower(OrgUnit), - mnesia:write(#vcard_search{us = US, user = User, - luser = LUser, fn = FN, lfn = LFN, - family = Family, lfamily = LFamily, - given = Given, lgiven = LGiven, - middle = Middle, lmiddle = LMiddle, - nickname = Nickname, - lnickname = LNickname, bday = BDay, - lbday = LBDay, ctry = CTRY, lctry = LCTRY, - locality = Locality, - llocality = LLocality, email = EMail, - lemail = LEMail, orgname = OrgName, - lorgname = LOrgName, orgunit = OrgUnit, - lorgunit = LOrgUnit}). - -reindex_vcards() -> - F = fun () -> mnesia:foldl(fun set_vcard_t/2, [], vcard) - end, - mnesia:transaction(F). - +-spec remove_user(binary(), binary()) -> ok. remove_user(User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - remove_user(LUser, LServer, - gen_mod:db_type(LServer, ?MODULE)). + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:remove_user(LUser, LServer), + ets_cache:delete(?VCARD_CACHE, {LUser, LServer}, cache_nodes(Mod, LServer)). -remove_user(LUser, LServer, mnesia) -> - US = {LUser, LServer}, - F = fun () -> - mnesia:delete({vcard, US}), - mnesia:delete({vcard_search, US}) - end, - mnesia:transaction(F); -remove_user(LUser, LServer, odbc) -> - Username = ejabberd_odbc:escape(LUser), - ejabberd_odbc:sql_transaction(LServer, - [[<<"delete from vcard where username='">>, - Username, <<"';">>], - [<<"delete from vcard_search where lusername='">>, - Username, <<"';">>]]); -remove_user(LUser, LServer, riak) -> - {atomic, ejabberd_riak:delete(vcard, {LUser, LServer})}. - -update_tables() -> - update_vcard_table(), - update_vcard_search_table(). - -update_vcard_table() -> - Fields = record_info(fields, vcard), - case mnesia:table_info(vcard, attributes) of - Fields -> - ejabberd_config:convert_table_to_binary( - vcard, Fields, set, - fun(#vcard{us = {U, _}}) -> U end, - fun(#vcard{us = {U, S}, vcard = El} = R) -> - R#vcard{us = {iolist_to_binary(U), - iolist_to_binary(S)}, - vcard = xml:to_xmlel(El)} - end); - _ -> - ?INFO_MSG("Recreating vcard table", []), - mnesia:transform_table(vcard, ignore, Fields) +-spec init_cache(module(), binary(), gen_mod:opts()) -> ok. +init_cache(Mod, Host, Opts) -> + case use_cache(Mod, Host) of + true -> + CacheOpts = cache_opts(Host, Opts), + ets_cache:new(?VCARD_CACHE, CacheOpts); + false -> + ets_cache:delete(?VCARD_CACHE) end. -update_vcard_search_table() -> - Fields = record_info(fields, vcard_search), - case mnesia:table_info(vcard_search, attributes) of - Fields -> - ejabberd_config:convert_table_to_binary( - vcard_search, Fields, set, - fun(#vcard_search{us = {U, _}}) -> U end, - fun(#vcard_search{} = VS) -> - [vcard_search | L] = tuple_to_list(VS), - NewL = lists:map( - fun({U, S}) -> - {iolist_to_binary(U), - iolist_to_binary(S)}; - (Str) -> - iolist_to_binary(Str) - end, L), - list_to_tuple([vcard_search | NewL]) - end); - _ -> - ?INFO_MSG("Recreating vcard_search table", []), - mnesia:transform_table(vcard_search, ignore, Fields) +-spec cache_opts(binary(), gen_mod:opts()) -> [proplists:property()]. +cache_opts(_Host, Opts) -> + MaxSize = mod_vcard_opt:cache_size(Opts), + CacheMissed = mod_vcard_opt:cache_missed(Opts), + LifeTime = mod_vcard_opt:cache_life_time(Opts), + [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}]. + +-spec use_cache(module(), binary()) -> boolean(). +use_cache(Mod, Host) -> + case erlang:function_exported(Mod, use_cache, 1) of + true -> Mod:use_cache(Host); + false -> mod_vcard_opt:use_cache(Host) end. -vcard_schema() -> - {record_info(fields, vcard), #vcard{}}. +-spec cache_nodes(module(), binary()) -> [node()]. +cache_nodes(Mod, Host) -> + case erlang:function_exported(Mod, cache_nodes, 1) of + true -> Mod:cache_nodes(Host); + false -> ejabberd_cluster:get_nodes() + end. -export(_Server) -> - [{vcard, - fun(Host, #vcard{us = {LUser, LServer}, vcard = VCARD}) - when LServer == Host -> - Username = ejabberd_odbc:escape(LUser), - SVCARD = - ejabberd_odbc:escape(xml:element_to_binary(VCARD)), - [[<<"delete from vcard where username='">>, Username, <<"';">>], - [<<"insert into vcard(username, vcard) values ('">>, - Username, <<"', '">>, SVCARD, <<"');">>]]; - (_Host, _R) -> - [] - end}, - {vcard_search, - fun(Host, #vcard_search{user = {User, LServer}, luser = LUser, - fn = FN, lfn = LFN, family = Family, - lfamily = LFamily, given = Given, - lgiven = LGiven, middle = Middle, - lmiddle = LMiddle, nickname = Nickname, - lnickname = LNickname, bday = BDay, - lbday = LBDay, ctry = CTRY, lctry = LCTRY, - locality = Locality, llocality = LLocality, - email = EMail, lemail = LEMail, - orgname = OrgName, lorgname = LOrgName, - orgunit = OrgUnit, lorgunit = LOrgUnit}) - when LServer == Host -> - Username = ejabberd_odbc:escape(User), - LUsername = ejabberd_odbc:escape(LUser), - SFN = ejabberd_odbc:escape(FN), - SLFN = ejabberd_odbc:escape(LFN), - SFamily = ejabberd_odbc:escape(Family), - SLFamily = ejabberd_odbc:escape(LFamily), - SGiven = ejabberd_odbc:escape(Given), - SLGiven = ejabberd_odbc:escape(LGiven), - SMiddle = ejabberd_odbc:escape(Middle), - SLMiddle = ejabberd_odbc:escape(LMiddle), - SNickname = ejabberd_odbc:escape(Nickname), - SLNickname = ejabberd_odbc:escape(LNickname), - SBDay = ejabberd_odbc:escape(BDay), - SLBDay = ejabberd_odbc:escape(LBDay), - SCTRY = ejabberd_odbc:escape(CTRY), - SLCTRY = ejabberd_odbc:escape(LCTRY), - SLocality = ejabberd_odbc:escape(Locality), - SLLocality = ejabberd_odbc:escape(LLocality), - SEMail = ejabberd_odbc:escape(EMail), - SLEMail = ejabberd_odbc:escape(LEMail), - SOrgName = ejabberd_odbc:escape(OrgName), - SLOrgName = ejabberd_odbc:escape(LOrgName), - SOrgUnit = ejabberd_odbc:escape(OrgUnit), - SLOrgUnit = ejabberd_odbc:escape(LOrgUnit), - [[<<"delete from vcard_search where lusername='">>, - LUsername, <<"';">>], - [<<"insert into vcard_search( username, " - "lusername, fn, lfn, family, lfamily, " - " given, lgiven, middle, lmiddle, " - "nickname, lnickname, bday, lbday, " - "ctry, lctry, locality, llocality, " - " email, lemail, orgname, lorgname, " - "orgunit, lorgunit)values (">>, - <<" '">>, Username, <<"', '">>, LUsername, - <<"', '">>, SFN, <<"', '">>, SLFN, - <<"', '">>, SFamily, <<"', '">>, SLFamily, - <<"', '">>, SGiven, <<"', '">>, SLGiven, - <<"', '">>, SMiddle, <<"', '">>, SLMiddle, - <<"', '">>, SNickname, <<"', '">>, SLNickname, - <<"', '">>, SBDay, <<"', '">>, SLBDay, - <<"', '">>, SCTRY, <<"', '">>, SLCTRY, - <<"', '">>, SLocality, <<"', '">>, SLLocality, - <<"', '">>, SEMail, <<"', '">>, SLEMail, - <<"', '">>, SOrgName, <<"', '">>, SLOrgName, - <<"', '">>, SOrgUnit, <<"', '">>, SLOrgUnit, - <<"');">>]]; - (_Host, _R) -> - [] - end}]. +import_info() -> + [{<<"vcard">>, 3}, {<<"vcard_search">>, 24}]. -import(LServer) -> - [{<<"select username, vcard from vcard;">>, - fun([LUser, SVCard]) -> - #xmlel{} = VCARD = xml_stream:parse_element(SVCard), - #vcard{us = {LUser, LServer}, vcard = VCARD} - end}, - {<<"select username, lusername, fn, lfn, family, lfamily, " - "given, lgiven, middle, lmiddle, nickname, lnickname, " - "bday, lbday, ctry, lctry, locality, llocality, email, " - "lemail, orgname, lorgname, orgunit, lorgunit from vcard_search;">>, - fun([User, LUser, FN, LFN, - Family, LFamily, Given, LGiven, - Middle, LMiddle, Nickname, LNickname, - BDay, LBDay, CTRY, LCTRY, Locality, LLocality, - EMail, LEMail, OrgName, LOrgName, OrgUnit, LOrgUnit]) -> - #vcard_search{us = {LUser, LServer}, - user = {User, LServer}, luser = LUser, - fn = FN, lfn = LFN, family = Family, - lfamily = LFamily, given = Given, - lgiven = LGiven, middle = Middle, - lmiddle = LMiddle, nickname = Nickname, - lnickname = LNickname, bday = BDay, - lbday = LBDay, ctry = CTRY, lctry = LCTRY, - locality = Locality, llocality = LLocality, - email = EMail, lemail = LEMail, - orgname = OrgName, lorgname = LOrgName, - orgunit = OrgUnit, lorgunit = LOrgUnit} - end}]. +import_start(LServer, DBType) -> + Mod = gen_mod:db_mod(DBType, ?MODULE), + Mod:init(LServer, []). -import(_LServer, mnesia, #vcard{} = VCard) -> - mnesia:dirty_write(VCard); -import(_LServer, mnesia, #vcard_search{} = S) -> - mnesia:dirty_write(S); -import(_LServer, riak, #vcard{us = {LUser, _}, vcard = El} = VCard) -> - FN = xml:get_path_s(El, [{elem, <<"FN">>}, cdata]), - Family = xml:get_path_s(El, - [{elem, <<"N">>}, {elem, <<"FAMILY">>}, cdata]), - Given = xml:get_path_s(El, - [{elem, <<"N">>}, {elem, <<"GIVEN">>}, cdata]), - Middle = xml:get_path_s(El, - [{elem, <<"N">>}, {elem, <<"MIDDLE">>}, cdata]), - Nickname = xml:get_path_s(El, - [{elem, <<"NICKNAME">>}, cdata]), - BDay = xml:get_path_s(El, - [{elem, <<"BDAY">>}, cdata]), - CTRY = xml:get_path_s(El, - [{elem, <<"ADR">>}, {elem, <<"CTRY">>}, cdata]), - Locality = xml:get_path_s(El, - [{elem, <<"ADR">>}, {elem, <<"LOCALITY">>}, - cdata]), - EMail1 = xml:get_path_s(El, - [{elem, <<"EMAIL">>}, {elem, <<"USERID">>}, cdata]), - EMail2 = xml:get_path_s(El, - [{elem, <<"EMAIL">>}, cdata]), - OrgName = xml:get_path_s(El, - [{elem, <<"ORG">>}, {elem, <<"ORGNAME">>}, cdata]), - OrgUnit = xml:get_path_s(El, - [{elem, <<"ORG">>}, {elem, <<"ORGUNIT">>}, cdata]), - EMail = case EMail1 of - <<"">> -> EMail2; - _ -> EMail1 - end, - LFN = string2lower(FN), - LFamily = string2lower(Family), - LGiven = string2lower(Given), - LMiddle = string2lower(Middle), - LNickname = string2lower(Nickname), - LBDay = string2lower(BDay), - LCTRY = string2lower(CTRY), - LLocality = string2lower(Locality), - LEMail = string2lower(EMail), - LOrgName = string2lower(OrgName), - LOrgUnit = string2lower(OrgUnit), - ejabberd_riak:put(VCard, vcard_schema(), - [{'2i', [{<<"user">>, LUser}, - {<<"luser">>, LUser}, - {<<"fn">>, FN}, - {<<"lfn">>, LFN}, - {<<"family">>, Family}, - {<<"lfamily">>, LFamily}, - {<<"given">>, Given}, - {<<"lgiven">>, LGiven}, - {<<"middle">>, Middle}, - {<<"lmiddle">>, LMiddle}, - {<<"nickname">>, Nickname}, - {<<"lnickname">>, LNickname}, - {<<"bday">>, BDay}, - {<<"lbday">>, LBDay}, - {<<"ctry">>, CTRY}, - {<<"lctry">>, LCTRY}, - {<<"locality">>, Locality}, - {<<"llocality">>, LLocality}, - {<<"email">>, EMail}, - {<<"lemail">>, LEMail}, - {<<"orgname">>, OrgName}, - {<<"lorgname">>, LOrgName}, - {<<"orgunit">>, OrgUnit}, - {<<"lorgunit">>, LOrgUnit}]}]); -import(_LServer, riak, #vcard_search{}) -> - ok; -import(_, _, _) -> - pass. +import(LServer, {sql, _}, DBType, Tab, L) -> + Mod = gen_mod:db_mod(DBType, ?MODULE), + Mod:import(LServer, Tab, L). + +export(LServer) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:export(LServer). + +%%% +%%% WebAdmin +%%% + +webadmin_menu_hostuser(Acc, _Host, _Username, _Lang) -> + Acc ++ [{<<"vcard">>, <<"vCard">>}]. + +webadmin_page_hostuser(_, Host, User, + #request{path = [<<"vcard">>]} = R) -> + Head = ?H1GL(<<"vCard">>, <<"modules/#mod_vcard">>, <<"mod_vcard">>), + Set = [make_command(set_nickname, R, [{<<"user">>, User}, {<<"host">>, Host}], []), + make_command(set_vcard, R, [{<<"user">>, User}, {<<"host">>, Host}], []), + make_command(set_vcard2, R, [{<<"user">>, User}, {<<"host">>, Host}], []), + make_command(set_vcard2_multi, R, [{<<"user">>, User}, {<<"host">>, Host}], [])], + timer:sleep(100), % setting vcard takes a while, let's delay the get commands + FieldNames = [<<"VERSION">>, <<"FN">>, <<"NICKNAME">>, <<"BDAY">>], + FieldNames2 = + [{<<"N">>, <<"FAMILY">>}, + {<<"N">>, <<"GIVEN">>}, + {<<"N">>, <<"MIDDLE">>}, + {<<"ADR">>, <<"CTRY">>}, + {<<"ADR">>, <<"LOCALITY">>}, + {<<"EMAIL">>, <<"USERID">>}], + Get = [make_command(get_vcard, R, [{<<"user">>, User}, {<<"host">>, Host}], []), + ?XE(<<"blockquote">>, + [make_table([<<"name">>, <<"value">>], + [{?C(FieldName), + make_command(get_vcard, + R, + [{<<"user">>, User}, + {<<"host">>, Host}, + {<<"name">>, FieldName}], + [{only, value}])} + || FieldName <- FieldNames])]), + make_command(get_vcard2, R, [{<<"user">>, User}, {<<"host">>, Host}], []), + ?XE(<<"blockquote">>, + [make_table([<<"name">>, <<"subname">>, <<"value">>], + [{?C(FieldName), + ?C(FieldSubName), + make_command(get_vcard2, + R, + [{<<"user">>, User}, + {<<"host">>, Host}, + {<<"name">>, FieldName}, + {<<"subname">>, FieldSubName}], + [{only, value}])} + || {FieldName, FieldSubName} <- FieldNames2])]), + make_command(get_vcard2_multi, R, [{<<"user">>, User}, {<<"host">>, Host}], [])], + {stop, Head ++ Get ++ Set}; +webadmin_page_hostuser(Acc, _, _, _) -> Acc. + +%%% +%%% Documentation +%%% + +depends(_Host, _Opts) -> + []. + +mod_opt_type(allow_return_all) -> + econf:bool(); +mod_opt_type(name) -> + econf:binary(); +mod_opt_type(matches) -> + econf:pos_int(infinity); +mod_opt_type(search) -> + econf:bool(); +mod_opt_type(host) -> + econf:host(); +mod_opt_type(hosts) -> + econf:hosts(); +mod_opt_type(db_type) -> + econf:db_type(?MODULE); +mod_opt_type(use_cache) -> + econf:bool(); +mod_opt_type(cache_size) -> + econf:pos_int(infinity); +mod_opt_type(cache_missed) -> + econf:bool(); +mod_opt_type(cache_life_time) -> + econf:timeout(second, infinity); +mod_opt_type(vcard) -> + econf:vcard_temp(). + +mod_options(Host) -> + [{allow_return_all, false}, + {host, <<"vjud.", Host/binary>>}, + {hosts, []}, + {matches, 30}, + {search, false}, + {name, ?T("vCard User Search")}, + {vcard, undefined}, + {db_type, ejabberd_config:default_db(Host, ?MODULE)}, + {use_cache, ejabberd_option:use_cache(Host)}, + {cache_size, ejabberd_option:cache_size(Host)}, + {cache_missed, ejabberd_option:cache_missed(Host)}, + {cache_life_time, ejabberd_option:cache_life_time(Host)}]. + +mod_doc() -> + #{desc => + ?T("This module allows end users to store and retrieve " + "their vCard, and to retrieve other users vCards, " + "as defined in https://xmpp.org/extensions/xep-0054.html" + "[XEP-0054: vcard-temp]. The module also implements an " + "uncomplicated Jabber User Directory based on the vCards " + "of these users. Moreover, it enables the server to send " + "its vCard when queried."), + opts => + [{allow_return_all, + #{value => "true | false", + desc => + ?T("This option enables you to specify if search " + "operations with empty input fields should return " + "all users who added some information to their vCard. " + "The default value is 'false'.")}}, + {host, + #{desc => ?T("Deprecated. Use 'hosts' instead.")}}, + {hosts, + #{value => ?T("[Host, ...]"), + desc => + ?T("This option defines the Jabber IDs of the service. " + "If the 'hosts' option is not specified, the only Jabber ID will " + "be the hostname of the virtual host with the prefix \"vjud.\". " + "The keyword '@HOST@' is replaced with the real virtual host name.")}}, + {name, + #{value => ?T("Name"), + desc => + ?T("The value of the service name. This name is only visible in some " + "clients that support https://xmpp.org/extensions/xep-0030.html" + "[XEP-0030: Service Discovery]. The default is 'vCard User Search'.")}}, + {matches, + #{value => "pos_integer() | infinity", + desc => + ?T("With this option, the number of reported search results " + "can be limited. If the option's value is set to 'infinity', " + "all search results are reported. The default value is '30'.")}}, + {search, + #{value => "true | false", + desc => + ?T("This option specifies whether the search functionality " + "is enabled or not. If disabled, the options 'hosts', 'name' " + "and 'vcard' will be ignored and the Jabber User Directory " + "service will not appear in the Service Discovery item list. " + "The default value is 'false'.")}}, + {db_type, + #{value => "mnesia | sql | ldap", + desc => + ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}, + {use_cache, + #{value => "true | false", + desc => + ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, + {cache_size, + #{value => "pos_integer() | infinity", + desc => + ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, + {cache_missed, + #{value => "true | false", + desc => + ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, + {cache_life_time, + #{value => "timeout()", + desc => + ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}, + {vcard, + #{value => ?T("vCard"), + desc => + ?T("A custom vCard of the server that will be displayed " + "by some XMPP clients in Service Discovery. The value of " + "'vCard' is a YAML map constructed from an XML representation " + "of vCard. Since the representation has no attributes, " + "the mapping is straightforward."), + example => + ["# This XML representation of vCard:", + "# ", + "# ", + "# Conferences", + "# ", + "# ", + "# Elm Street", + "# ", + "# ", + "# ", + "# is translated to:", + "# ", + "vcard:", + " fn: Conferences", + " adr:", + " -", + " work: true", + " street: Elm Street"]}}]}. diff --git a/src/mod_vcard_ldap.erl b/src/mod_vcard_ldap.erl index 61db38976..3ecf39ba1 100644 --- a/src/mod_vcard_ldap.erl +++ b/src/mod_vcard_ldap.erl @@ -1,11 +1,10 @@ -%%%---------------------------------------------------------------------- +%%%------------------------------------------------------------------- %%% File : mod_vcard_ldap.erl -%%% Author : Alexey Shchepin -%%% Purpose : Support for VCards from LDAP storage. -%%% Created : 2 Jan 2003 by Alexey Shchepin +%%% Author : Evgeny Khramtsov +%%% Created : 29 Jul 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -25,34 +24,32 @@ -module(mod_vcard_ldap). --author('alexey@process-one.net'). - -behaviour(gen_server). +-behaviour(mod_vcard). --behaviour(gen_mod). +%% API +-export([start_link/2]). +-export([init/2, stop/1, get_vcard/2, set_vcard/4, search/4, + remove_user/2, import/3, search_fields/1, search_reported/1, + mod_opt_type/1, mod_options/1, mod_doc/0]). +-export([is_search_supported/1]). -%% gen_server callbacks. --export([init/1, handle_info/2, handle_call/3, - handle_cast/2, terminate/2, code_change/3]). +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). --export([start/2, start_link/2, stop/1, - get_sm_features/5, process_local_iq/3, process_sm_iq/3, - remove_user/1, route/4, transform_module_options/1]). - --include("ejabberd.hrl"). -include("logger.hrl"). - -include("eldap.hrl"). - --include("jlib.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). +-include("translate.hrl"). -define(PROCNAME, ejabberd_mod_vcard_ldap). -record(state, {serverhost = <<"">> :: binary(), - myhost = <<"">> :: binary(), + myhosts = [] :: [binary()], eldap_id = <<"">> :: binary(), - search = true :: boolean(), + search = false :: boolean(), servers = [] :: [binary()], backups = [] :: [binary()], port = ?LDAP_PORT :: inet:port_number(), @@ -60,8 +57,8 @@ dn = <<"">> :: binary(), base = <<"">> :: binary(), password = <<"">> :: binary(), - uids = [] :: [{binary()} | {binary(), binary()}], - vcard_map = [] :: [{binary(), binary(), [binary()]}], + uids = [] :: [{binary(), binary()}], + vcard_map = [] :: [{binary(), [{binary(), [binary()]}]}], vcard_map_attrs = [] :: [binary()], user_filter = <<"">> :: binary(), search_filter :: eldap:filter(), @@ -71,200 +68,153 @@ deref_aliases = never :: never | searching | finding | always, matches = 0 :: non_neg_integer()}). --define(VCARD_MAP, - [{<<"NICKNAME">>, <<"%u">>, []}, - {<<"FN">>, <<"%s">>, [<<"displayName">>]}, - {<<"FAMILY">>, <<"%s">>, [<<"sn">>]}, - {<<"GIVEN">>, <<"%s">>, [<<"givenName">>]}, - {<<"MIDDLE">>, <<"%s">>, [<<"initials">>]}, - {<<"ORGNAME">>, <<"%s">>, [<<"o">>]}, - {<<"ORGUNIT">>, <<"%s">>, [<<"ou">>]}, - {<<"CTRY">>, <<"%s">>, [<<"c">>]}, - {<<"LOCALITY">>, <<"%s">>, [<<"l">>]}, - {<<"STREET">>, <<"%s">>, [<<"street">>]}, - {<<"REGION">>, <<"%s">>, [<<"st">>]}, - {<<"PCODE">>, <<"%s">>, [<<"postalCode">>]}, - {<<"TITLE">>, <<"%s">>, [<<"title">>]}, - {<<"URL">>, <<"%s">>, [<<"labeleduri">>]}, - {<<"DESC">>, <<"%s">>, [<<"description">>]}, - {<<"TEL">>, <<"%s">>, [<<"telephoneNumber">>]}, - {<<"EMAIL">>, <<"%s">>, [<<"mail">>]}, - {<<"BDAY">>, <<"%s">>, [<<"birthDay">>]}, - {<<"ROLE">>, <<"%s">>, [<<"employeeType">>]}, - {<<"PHOTO">>, <<"%s">>, [<<"jpegPhoto">>]}]). +%%%=================================================================== +%%% API +%%%=================================================================== +start_link(Host, Opts) -> + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + gen_server:start_link({local, Proc}, ?MODULE, [Host, Opts], []). --define(SEARCH_FIELDS, - [{<<"User">>, <<"%u">>}, - {<<"Full Name">>, <<"displayName">>}, - {<<"Given Name">>, <<"givenName">>}, - {<<"Middle Name">>, <<"initials">>}, - {<<"Family Name">>, <<"sn">>}, - {<<"Nickname">>, <<"%u">>}, - {<<"Birthday">>, <<"birthDay">>}, - {<<"Country">>, <<"c">>}, {<<"City">>, <<"l">>}, - {<<"Email">>, <<"mail">>}, - {<<"Organization Name">>, <<"o">>}, - {<<"Organization Unit">>, <<"ou">>}]). - --define(SEARCH_REPORTED, - [{<<"Full Name">>, <<"FN">>}, - {<<"Given Name">>, <<"FIRST">>}, - {<<"Middle Name">>, <<"MIDDLE">>}, - {<<"Family Name">>, <<"LAST">>}, - {<<"Nickname">>, <<"NICK">>}, - {<<"Birthday">>, <<"BDAY">>}, - {<<"Country">>, <<"CTRY">>}, - {<<"City">>, <<"LOCALITY">>}, - {<<"Email">>, <<"EMAIL">>}, - {<<"Organization Name">>, <<"ORGNAME">>}, - {<<"Organization Unit">>, <<"ORGUNIT">>}]). - -handle_cast(_Request, State) -> {noreply, State}. - -code_change(_OldVsn, State, _Extra) -> {ok, State}. - -start(Host, Opts) -> +init(Host, Opts) -> Proc = gen_mod:get_module_proc(Host, ?PROCNAME), ChildSpec = {Proc, {?MODULE, start_link, [Host, Opts]}, transient, 1000, worker, [?MODULE]}, - supervisor:start_child(ejabberd_sup, ChildSpec). + supervisor:start_child(ejabberd_backend_sup, ChildSpec). stop(Host) -> Proc = gen_mod:get_module_proc(Host, ?PROCNAME), - gen_server:call(Proc, stop), - supervisor:terminate_child(ejabberd_sup, Proc), - supervisor:delete_child(ejabberd_sup, Proc). + supervisor:terminate_child(ejabberd_backend_sup, Proc), + supervisor:delete_child(ejabberd_backend_sup, Proc), + ok. -terminate(_Reason, State) -> - Host = State#state.serverhost, - gen_iq_handler:remove_iq_handler(ejabberd_local, Host, - ?NS_VCARD), - gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, - ?NS_VCARD), - ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, - get_sm_features, 50), - case State#state.search of - true -> - ejabberd_router:unregister_route(State#state.myhost); - _ -> ok +is_search_supported(_LServer) -> + true. + +get_vcard(LUser, LServer) -> + {ok, State} = eldap_utils:get_state(LServer, ?PROCNAME), + VCardMap = State#state.vcard_map, + case find_ldap_user(LUser, State) of + #eldap_entry{attributes = Attributes} -> + VCard = ldap_attributes_to_vcard(Attributes, VCardMap, + {LUser, LServer}), + {ok, [xmpp:encode(VCard)]}; + _ -> + {ok, []} end. -start_link(Host, Opts) -> - Proc = gen_mod:get_module_proc(Host, ?PROCNAME), - gen_server:start_link({local, Proc}, ?MODULE, - [Host, Opts], []). +set_vcard(_LUser, _LServer, _VCard, _VCardSearch) -> + {atomic, not_implemented}. +search_fields(LServer) -> + {ok, State} = eldap_utils:get_state(LServer, ?PROCNAME), + State#state.search_fields. + +search_reported(LServer) -> + {ok, State} = eldap_utils:get_state(LServer, ?PROCNAME), + State#state.search_reported. + +search(LServer, Data, _AllowReturnAll, MaxMatch) -> + {ok, State} = eldap_utils:get_state(LServer, ?PROCNAME), + Base = State#state.base, + SearchFilter = State#state.search_filter, + Eldap_ID = State#state.eldap_id, + UIDs = State#state.uids, + ReportedAttrs = State#state.search_reported_attrs, + Filter = eldap:'and'([SearchFilter, + eldap_utils:make_filter(Data, UIDs)]), + case eldap_pool:search(Eldap_ID, + [{base, Base}, {filter, Filter}, {limit, MaxMatch}, + {deref_aliases, State#state.deref_aliases}, + {attributes, ReportedAttrs}]) + of + #eldap_search_result{entries = E} -> + search_items(E, State); + _ -> + [] + end. + +search_items(Entries, State) -> + LServer = State#state.serverhost, + SearchReported = State#state.search_reported, + VCardMap = State#state.vcard_map, + UIDs = State#state.uids, + Attributes = lists:map(fun (E) -> + #eldap_entry{attributes = Attrs} = E, Attrs + end, + Entries), + lists:filtermap( + fun(Attrs) -> + case eldap_utils:find_ldap_attrs(UIDs, Attrs) of + {U, UIDAttrFormat} -> + case eldap_utils:get_user_part(U, UIDAttrFormat) of + {ok, Username} -> + case ejabberd_auth:user_exists(Username, + LServer) of + true -> + RFields = lists:map( + fun({_, VCardName}) -> + {VCardName, + map_vcard_attr(VCardName, + Attrs, + VCardMap, + {Username, + ejabberd_config:get_myname()})} + end, + SearchReported), + J = <>, + {true, [{<<"jid">>, J} | RFields]}; + _ -> + false + end; + _ -> + false + end; + <<"">> -> + false + end + end, Attributes). + +remove_user(_User, _Server) -> + {atomic, not_implemented}. + +import(_, _, _) -> + ok. + +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== init([Host, Opts]) -> + process_flag(trap_exit, true), State = parse_options(Host, Opts), - IQDisc = gen_mod:get_opt(iqdisc, Opts, fun gen_iq_handler:check_type/1, - one_queue), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, - ?NS_VCARD, ?MODULE, process_local_iq, IQDisc), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, - ?NS_VCARD, ?MODULE, process_sm_iq, IQDisc), - ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, - get_sm_features, 50), eldap_pool:start_link(State#state.eldap_id, State#state.servers, State#state.backups, State#state.port, State#state.dn, State#state.password, State#state.tls_options), - case State#state.search of - true -> - ejabberd_router:register_route(State#state.myhost); - _ -> ok - end, {ok, State}. -handle_info({route, From, To, Packet}, State) -> - case catch do_route(State, From, To, Packet) of - Pid when is_pid(Pid) -> ok; - _ -> - Err = jlib:make_error_reply(Packet, - ?ERR_INTERNAL_SERVER_ERROR), - ejabberd_router:route(To, From, Err) - end, - {noreply, State}; -handle_info(_Info, State) -> {noreply, State}. - -get_sm_features({error, _Error} = Acc, _From, _To, - _Node, _Lang) -> - Acc; -get_sm_features(Acc, _From, _To, Node, _Lang) -> - case Node of - <<"">> -> - case Acc of - {result, Features} -> {result, [?NS_VCARD | Features]}; - empty -> {result, [?NS_VCARD]} - end; - _ -> Acc - end. - -process_local_iq(_From, _To, - #iq{type = Type, lang = Lang, sub_el = SubEl} = IQ) -> - case Type of - set -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; - get -> - IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"vCard">>, - attrs = [{<<"xmlns">>, ?NS_VCARD}], - children = - [#xmlel{name = <<"FN">>, attrs = [], - children = - [{xmlcdata, <<"ejabberd">>}]}, - #xmlel{name = <<"URL">>, attrs = [], - children = [{xmlcdata, ?EJABBERD_URI}]}, - #xmlel{name = <<"DESC">>, attrs = [], - children = - [{xmlcdata, - <<(translate:translate(Lang, - <<"Erlang Jabber Server">>))/binary, - "\nCopyright (c) 2002-2015 ProcessOne">>}]}, - #xmlel{name = <<"BDAY">>, attrs = [], - children = - [{xmlcdata, <<"2002-11-16">>}]}]}]} - end. - -process_sm_iq(_From, #jid{lserver = LServer} = To, - #iq{sub_el = SubEl} = IQ) -> - case catch process_vcard_ldap(To, IQ, LServer) of - {'EXIT', _} -> - IQ#iq{type = error, - sub_el = [SubEl, ?ERR_INTERNAL_SERVER_ERROR]}; - Other -> Other - end. - -process_vcard_ldap(To, IQ, Server) -> - {ok, State} = eldap_utils:get_state(Server, ?PROCNAME), - #iq{type = Type, sub_el = SubEl} = IQ, - case Type of - set -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; - get -> - #jid{luser = LUser} = To, - LServer = State#state.serverhost, - case ejabberd_auth:is_user_exists(LUser, LServer) of - true -> - VCardMap = State#state.vcard_map, - case find_ldap_user(LUser, State) of - #eldap_entry{attributes = Attributes} -> - Vcard = ldap_attributes_to_vcard(Attributes, VCardMap, - {LUser, LServer}), - IQ#iq{type = result, sub_el = Vcard}; - _ -> IQ#iq{type = result, sub_el = []} - end; - _ -> IQ#iq{type = result, sub_el = []} - end - end. - handle_call(get_state, _From, State) -> {reply, {ok, State}, State}; -handle_call(stop, _From, State) -> - {stop, normal, ok, State}; -handle_call(_Request, _From, State) -> - {reply, bad_request, State}. +handle_call(Request, From, State) -> + ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), + {noreply, State}. +handle_cast(Msg, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Msg]), + {noreply, State}. + +handle_info(Info, State) -> + ?WARNING_MSG("Unexpected info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== find_ldap_user(User, State) -> Base = State#state.base, RFC2254_Filter = State#state.user_filter, @@ -286,422 +236,65 @@ find_ldap_user(User, State) -> end. ldap_attributes_to_vcard(Attributes, VCardMap, UD) -> - Attrs = lists:map(fun ({VCardName, _, _}) -> - {stringprep:tolower(VCardName), - map_vcard_attr(VCardName, Attributes, VCardMap, - UD)} - end, - VCardMap), - Elts = [ldap_attribute_to_vcard(vCard, Attr) - || Attr <- Attrs], - NElts = [ldap_attribute_to_vcard(vCardN, Attr) - || Attr <- Attrs], - OElts = [ldap_attribute_to_vcard(vCardO, Attr) - || Attr <- Attrs], - AElts = [ldap_attribute_to_vcard(vCardA, Attr) - || Attr <- Attrs], - [#xmlel{name = <<"vCard">>, - attrs = [{<<"xmlns">>, ?NS_VCARD}], - children = - lists:append([X || X <- Elts, X /= none], - [#xmlel{name = <<"N">>, attrs = [], - children = [X || X <- NElts, X /= none]}, - #xmlel{name = <<"ORG">>, attrs = [], - children = [X || X <- OElts, X /= none]}, - #xmlel{name = <<"ADR">>, attrs = [], - children = - [X || X <- AElts, X /= none]}])}]. + Attrs = lists:map( + fun({VCardName, _}) -> + {VCardName, map_vcard_attr(VCardName, Attributes, VCardMap, UD)} + end, VCardMap), + lists:foldl(fun ldap_attribute_to_vcard/2, #vcard_temp{}, Attrs). -ldap_attribute_to_vcard(vCard, {<<"fn">>, Value}) -> - #xmlel{name = <<"FN">>, attrs = [], - children = [{xmlcdata, Value}]}; -ldap_attribute_to_vcard(vCard, - {<<"nickname">>, Value}) -> - #xmlel{name = <<"NICKNAME">>, attrs = [], - children = [{xmlcdata, Value}]}; -ldap_attribute_to_vcard(vCard, {<<"title">>, Value}) -> - #xmlel{name = <<"TITLE">>, attrs = [], - children = [{xmlcdata, Value}]}; -ldap_attribute_to_vcard(vCard, {<<"bday">>, Value}) -> - #xmlel{name = <<"BDAY">>, attrs = [], - children = [{xmlcdata, Value}]}; -ldap_attribute_to_vcard(vCard, {<<"url">>, Value}) -> - #xmlel{name = <<"URL">>, attrs = [], - children = [{xmlcdata, Value}]}; -ldap_attribute_to_vcard(vCard, {<<"desc">>, Value}) -> - #xmlel{name = <<"DESC">>, attrs = [], - children = [{xmlcdata, Value}]}; -ldap_attribute_to_vcard(vCard, {<<"role">>, Value}) -> - #xmlel{name = <<"ROLE">>, attrs = [], - children = [{xmlcdata, Value}]}; -ldap_attribute_to_vcard(vCard, {<<"tel">>, Value}) -> - #xmlel{name = <<"TEL">>, attrs = [], - children = - [#xmlel{name = <<"VOICE">>, attrs = [], children = []}, - #xmlel{name = <<"WORK">>, attrs = [], children = []}, - #xmlel{name = <<"NUMBER">>, attrs = [], - children = [{xmlcdata, Value}]}]}; -ldap_attribute_to_vcard(vCard, {<<"email">>, Value}) -> - #xmlel{name = <<"EMAIL">>, attrs = [], - children = - [#xmlel{name = <<"INTERNET">>, attrs = [], - children = []}, - #xmlel{name = <<"PREF">>, attrs = [], children = []}, - #xmlel{name = <<"USERID">>, attrs = [], - children = [{xmlcdata, Value}]}]}; -ldap_attribute_to_vcard(vCard, {<<"photo">>, Value}) -> - #xmlel{name = <<"PHOTO">>, attrs = [], - children = - [#xmlel{name = <<"TYPE">>, attrs = [], - children = [{xmlcdata, <<"image/jpeg">>}]}, - #xmlel{name = <<"BINVAL">>, attrs = [], - children = [{xmlcdata, jlib:encode_base64(Value)}]}]}; -ldap_attribute_to_vcard(vCardN, - {<<"family">>, Value}) -> - #xmlel{name = <<"FAMILY">>, attrs = [], - children = [{xmlcdata, Value}]}; -ldap_attribute_to_vcard(vCardN, {<<"given">>, Value}) -> - #xmlel{name = <<"GIVEN">>, attrs = [], - children = [{xmlcdata, Value}]}; -ldap_attribute_to_vcard(vCardN, - {<<"middle">>, Value}) -> - #xmlel{name = <<"MIDDLE">>, attrs = [], - children = [{xmlcdata, Value}]}; -ldap_attribute_to_vcard(vCardO, - {<<"orgname">>, Value}) -> - #xmlel{name = <<"ORGNAME">>, attrs = [], - children = [{xmlcdata, Value}]}; -ldap_attribute_to_vcard(vCardO, - {<<"orgunit">>, Value}) -> - #xmlel{name = <<"ORGUNIT">>, attrs = [], - children = [{xmlcdata, Value}]}; -ldap_attribute_to_vcard(vCardA, - {<<"locality">>, Value}) -> - #xmlel{name = <<"LOCALITY">>, attrs = [], - children = [{xmlcdata, Value}]}; -ldap_attribute_to_vcard(vCardA, - {<<"street">>, Value}) -> - #xmlel{name = <<"STREET">>, attrs = [], - children = [{xmlcdata, Value}]}; -ldap_attribute_to_vcard(vCardA, {<<"ctry">>, Value}) -> - #xmlel{name = <<"CTRY">>, attrs = [], - children = [{xmlcdata, Value}]}; -ldap_attribute_to_vcard(vCardA, - {<<"region">>, Value}) -> - #xmlel{name = <<"REGION">>, attrs = [], - children = [{xmlcdata, Value}]}; -ldap_attribute_to_vcard(vCardA, {<<"pcode">>, Value}) -> - #xmlel{name = <<"PCODE">>, attrs = [], - children = [{xmlcdata, Value}]}; -ldap_attribute_to_vcard(_, _) -> none. - --define(TLFIELD(Type, Label, Var), - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, Type}, - {<<"label">>, translate:translate(Lang, Label)}, - {<<"var">>, Var}], - children = []}). - --define(FORM(JID, SearchFields), - [#xmlel{name = <<"instructions">>, attrs = [], - children = - [{xmlcdata, - translate:translate(Lang, - <<"You need an x:data capable client to " - "search">>)}]}, - #xmlel{name = <<"x">>, - attrs = - [{<<"xmlns">>, ?NS_XDATA}, {<<"type">>, <<"form">>}], - children = - [#xmlel{name = <<"title">>, attrs = [], - children = - [{xmlcdata, - <<(translate:translate(Lang, - <<"Search users in ">>))/binary, - (jlib:jid_to_string(JID))/binary>>}]}, - #xmlel{name = <<"instructions">>, attrs = [], - children = - [{xmlcdata, - translate:translate(Lang, - <<"Fill in fields to search for any matching " - "Jabber User">>)}]}] - ++ - lists:map(fun ({X, Y}) -> - ?TLFIELD(<<"text-single">>, X, Y) - end, - SearchFields)}]). - -do_route(State, From, To, Packet) -> - spawn(?MODULE, route, [State, From, To, Packet]). - -route(State, From, To, Packet) -> - #jid{user = User, resource = Resource} = To, - ServerHost = State#state.serverhost, - if (User /= <<"">>) or (Resource /= <<"">>) -> - Err = jlib:make_error_reply(Packet, - ?ERR_SERVICE_UNAVAILABLE), - ejabberd_router:route(To, From, Err); - true -> - IQ = jlib:iq_query_info(Packet), - case IQ of - #iq{type = Type, xmlns = ?NS_SEARCH, lang = Lang, - sub_el = SubEl} -> - case Type of - set -> - XDataEl = find_xdata_el(SubEl), - case XDataEl of - false -> - Err = jlib:make_error_reply(Packet, - ?ERR_BAD_REQUEST), - ejabberd_router:route(To, From, Err); - _ -> - XData = jlib:parse_xdata_submit(XDataEl), - case XData of - invalid -> - Err = jlib:make_error_reply(Packet, - ?ERR_BAD_REQUEST), - ejabberd_router:route(To, From, Err); - _ -> - ResIQ = IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"query">>, - attrs = - [{<<"xmlns">>, - ?NS_SEARCH}], - children = - [#xmlel{name = - <<"x">>, - attrs = - [{<<"xmlns">>, - ?NS_XDATA}, - {<<"type">>, - <<"result">>}], - children - = - search_result(Lang, - To, - State, - XData)}]}]}, - ejabberd_router:route(To, From, - jlib:iq_to_xml(ResIQ)) - end - end; - get -> - SearchFields = State#state.search_fields, - ResIQ = IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"query">>, - attrs = - [{<<"xmlns">>, - ?NS_SEARCH}], - children = - ?FORM(To, SearchFields)}]}, - ejabberd_router:route(To, From, jlib:iq_to_xml(ResIQ)) - end; - #iq{type = Type, xmlns = ?NS_DISCO_INFO, lang = Lang} -> - case Type of - set -> - Err = jlib:make_error_reply(Packet, ?ERR_NOT_ALLOWED), - ejabberd_router:route(To, From, Err); - get -> - Info = ejabberd_hooks:run_fold(disco_info, ServerHost, - [], - [ServerHost, ?MODULE, - <<"">>, <<"">>]), - ResIQ = IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"query">>, - attrs = - [{<<"xmlns">>, - ?NS_DISCO_INFO}], - children = - [#xmlel{name = - <<"identity">>, - attrs = - [{<<"category">>, - <<"directory">>}, - {<<"type">>, - <<"user">>}, - {<<"name">>, - translate:translate(Lang, - <<"vCard User Search">>)}], - children = []}, - #xmlel{name = - <<"feature">>, - attrs = - [{<<"var">>, - ?NS_SEARCH}], - children = []}, - #xmlel{name = - <<"feature">>, - attrs = - [{<<"var">>, - ?NS_VCARD}], - children = []}] - ++ Info}]}, - ejabberd_router:route(To, From, jlib:iq_to_xml(ResIQ)) - end; - #iq{type = Type, xmlns = ?NS_DISCO_ITEMS} -> - case Type of - set -> - Err = jlib:make_error_reply(Packet, ?ERR_NOT_ALLOWED), - ejabberd_router:route(To, From, Err); - get -> - ResIQ = IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"query">>, - attrs = - [{<<"xmlns">>, - ?NS_DISCO_ITEMS}], - children = []}]}, - ejabberd_router:route(To, From, jlib:iq_to_xml(ResIQ)) - end; - #iq{type = get, xmlns = ?NS_VCARD, lang = Lang} -> - ResIQ = IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"vCard">>, - attrs = [{<<"xmlns">>, ?NS_VCARD}], - children = iq_get_vcard(Lang)}]}, - ejabberd_router:route(To, From, jlib:iq_to_xml(ResIQ)); - _ -> - Err = jlib:make_error_reply(Packet, - ?ERR_SERVICE_UNAVAILABLE), - ejabberd_router:route(To, From, Err) - end +-spec ldap_attribute_to_vcard({binary(), binary()}, vcard_temp()) -> vcard_temp(). +ldap_attribute_to_vcard({Attr, Value}, V) -> + Ts = V#vcard_temp.tel, + Es = V#vcard_temp.email, + N = case V#vcard_temp.n of + undefined -> #vcard_name{}; + _ -> V#vcard_temp.n + end, + O = case V#vcard_temp.org of + undefined -> #vcard_org{}; + _ -> V#vcard_temp.org + end, + A = case V#vcard_temp.adr of + [] -> #vcard_adr{}; + As -> hd(As) + end, + case str:to_lower(Attr) of + <<"fn">> -> V#vcard_temp{fn = Value}; + <<"nickname">> -> V#vcard_temp{nickname = Value}; + <<"title">> -> V#vcard_temp{title = Value}; + <<"bday">> -> V#vcard_temp{bday = Value}; + <<"url">> -> V#vcard_temp{url = Value}; + <<"desc">> -> V#vcard_temp{desc = Value}; + <<"role">> -> V#vcard_temp{role = Value}; + <<"tel">> -> V#vcard_temp{tel = [#vcard_tel{number = Value}|Ts]}; + <<"email">> -> V#vcard_temp{email = [#vcard_email{userid = Value}|Es]}; + <<"photo">> -> V#vcard_temp{photo = #vcard_photo{binval = Value, + type = photo_type(Value)}}; + <<"family">> -> V#vcard_temp{n = N#vcard_name{family = Value}}; + <<"given">> -> V#vcard_temp{n = N#vcard_name{given = Value}}; + <<"middle">> -> V#vcard_temp{n = N#vcard_name{middle = Value}}; + <<"orgname">> -> V#vcard_temp{org = O#vcard_org{name = Value}}; + <<"orgunit">> -> V#vcard_temp{org = O#vcard_org{units = [Value]}}; + <<"locality">> -> V#vcard_temp{adr = [A#vcard_adr{locality = Value}]}; + <<"street">> -> V#vcard_temp{adr = [A#vcard_adr{street = Value}]}; + <<"ctry">> -> V#vcard_temp{adr = [A#vcard_adr{ctry = Value}]}; + <<"region">> -> V#vcard_temp{adr = [A#vcard_adr{region = Value}]}; + <<"pcode">> -> V#vcard_temp{adr = [A#vcard_adr{pcode = Value}]}; + _ -> V end. -iq_get_vcard(Lang) -> - [#xmlel{name = <<"FN">>, attrs = [], - children = [{xmlcdata, <<"ejabberd/mod_vcard">>}]}, - #xmlel{name = <<"URL">>, attrs = [], - children = [{xmlcdata, ?EJABBERD_URI}]}, - #xmlel{name = <<"DESC">>, attrs = [], - children = - [{xmlcdata, - <<(translate:translate(Lang, - <<"ejabberd vCard module">>))/binary, - "\nCopyright (c) 2003-2015 ProcessOne">>}]}]. - --define(LFIELD(Label, Var), - #xmlel{name = <<"field">>, - attrs = - [{<<"label">>, translate:translate(Lang, Label)}, - {<<"var">>, Var}], - children = []}). - -search_result(Lang, JID, State, Data) -> - SearchReported = State#state.search_reported, - Header = [#xmlel{name = <<"title">>, attrs = [], - children = - [{xmlcdata, - <<(translate:translate(Lang, - <<"Search Results for ">>))/binary, - (jlib:jid_to_string(JID))/binary>>}]}, - #xmlel{name = <<"reported">>, attrs = [], - children = - [?TLFIELD(<<"text-single">>, <<"Jabber ID">>, - <<"jid">>)] - ++ - lists:map(fun ({Name, Value}) -> - ?TLFIELD(<<"text-single">>, Name, - Value) - end, - SearchReported)}], - case search(State, Data) of - error -> Header; - Result -> Header ++ Result - end. - --define(FIELD(Var, Val), - #xmlel{name = <<"field">>, attrs = [{<<"var">>, Var}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, Val}]}]}). - -search(State, Data) -> - Base = State#state.base, - SearchFilter = State#state.search_filter, - Eldap_ID = State#state.eldap_id, - UIDs = State#state.uids, - Limit = State#state.matches, - ReportedAttrs = State#state.search_reported_attrs, - Filter = eldap:'and'([SearchFilter, - eldap_utils:make_filter(Data, UIDs)]), - case eldap_pool:search(Eldap_ID, - [{base, Base}, {filter, Filter}, {limit, Limit}, - {deref_aliases, State#state.deref_aliases}, - {attributes, ReportedAttrs}]) - of - #eldap_search_result{entries = E} -> - search_items(E, State); - _ -> error - end. - -search_items(Entries, State) -> - LServer = State#state.serverhost, - SearchReported = State#state.search_reported, - VCardMap = State#state.vcard_map, - UIDs = State#state.uids, - Attributes = lists:map(fun (E) -> - #eldap_entry{attributes = Attrs} = E, Attrs - end, - Entries), - lists:flatmap(fun (Attrs) -> - case eldap_utils:find_ldap_attrs(UIDs, Attrs) of - {U, UIDAttrFormat} -> - case eldap_utils:get_user_part(U, UIDAttrFormat) - of - {ok, Username} -> - case - ejabberd_auth:is_user_exists(Username, - LServer) - of - true -> - RFields = lists:map(fun ({_, - VCardName}) -> - {VCardName, - map_vcard_attr(VCardName, - Attrs, - VCardMap, - {Username, - ?MYNAME})} - end, - SearchReported), - Result = [?FIELD(<<"jid">>, - <>)] - ++ - [?FIELD(Name, Value) - || {Name, Value} - <- RFields], - [#xmlel{name = <<"item">>, - attrs = [], - children = Result}]; - _ -> [] - end; - _ -> [] - end; - <<"">> -> [] - end - end, - Attributes). - -remove_user(_User) -> true. - -%%%----------------------- -%%% Auxiliary functions. -%%%----------------------- +-spec photo_type(binary()) -> binary(). +photo_type(Value) -> + Type = eimp:get_type(Value), + <<"image/", (atom_to_binary(Type, latin1))/binary>>. map_vcard_attr(VCardName, Attributes, Pattern, UD) -> - Res = lists:filter(fun ({Name, _, _}) -> - eldap_utils:case_insensitive_match(Name, - VCardName) - end, - Pattern), + Res = lists:filter( + fun({Name, _}) -> + eldap_utils:case_insensitive_match(Name, VCardName) + end, Pattern), case Res of - [{_, Str, Attrs}] -> + [{_, [{Str, Attrs}|_]}] -> process_pattern(Str, UD, [eldap_utils:get_ldap_attr(X, Attributes) || X <- Attrs]); @@ -713,48 +306,65 @@ process_pattern(Str, {User, Domain}, AttrValues) -> [{<<"%u">>, User}, {<<"%d">>, Domain}] ++ [{<<"%s">>, V, 1} || V <- AttrValues]). -find_xdata_el(#xmlel{children = SubEls}) -> - find_xdata_el1(SubEls). +default_vcard_map() -> + [{<<"NICKNAME">>, [{<<"%u">>, []}]}, + {<<"FN">>, [{<<"%s">>, [<<"displayName">>]}]}, + {<<"FAMILY">>, [{<<"%s">>, [<<"sn">>]}]}, + {<<"GIVEN">>, [{<<"%s">>, [<<"givenName">>]}]}, + {<<"MIDDLE">>, [{<<"%s">>, [<<"initials">>]}]}, + {<<"ORGNAME">>, [{<<"%s">>, [<<"o">>]}]}, + {<<"ORGUNIT">>, [{<<"%s">>, [<<"ou">>]}]}, + {<<"CTRY">>, [{<<"%s">>, [<<"c">>]}]}, + {<<"LOCALITY">>, [{<<"%s">>, [<<"l">>]}]}, + {<<"STREET">>, [{<<"%s">>, [<<"street">>]}]}, + {<<"REGION">>, [{<<"%s">>, [<<"st">>]}]}, + {<<"PCODE">>, [{<<"%s">>, [<<"postalCode">>]}]}, + {<<"TITLE">>, [{<<"%s">>, [<<"title">>]}]}, + {<<"URL">>, [{<<"%s">>, [<<"labeleduri">>]}]}, + {<<"DESC">>, [{<<"%s">>, [<<"description">>]}]}, + {<<"TEL">>, [{<<"%s">>, [<<"telephoneNumber">>]}]}, + {<<"EMAIL">>, [{<<"%s">>, [<<"mail">>]}]}, + {<<"BDAY">>, [{<<"%s">>, [<<"birthDay">>]}]}, + {<<"ROLE">>, [{<<"%s">>, [<<"employeeType">>]}]}, + {<<"PHOTO">>, [{<<"%s">>, [<<"jpegPhoto">>]}]}]. -find_xdata_el1([]) -> false; -find_xdata_el1([#xmlel{name = Name, attrs = Attrs, - children = SubEls} - | Els]) -> - case xml:get_attr_s(<<"xmlns">>, Attrs) of - ?NS_XDATA -> - #xmlel{name = Name, attrs = Attrs, children = SubEls}; - _ -> find_xdata_el1(Els) - end; -find_xdata_el1([_ | Els]) -> find_xdata_el1(Els). +default_search_fields() -> + [{?T("User"), <<"%u">>}, + {?T("Full Name"), <<"displayName">>}, + {?T("Given Name"), <<"givenName">>}, + {?T("Middle Name"), <<"initials">>}, + {?T("Family Name"), <<"sn">>}, + {?T("Nickname"), <<"%u">>}, + {?T("Birthday"), <<"birthDay">>}, + {?T("Country"), <<"c">>}, + {?T("City"), <<"l">>}, + {?T("Email"), <<"mail">>}, + {?T("Organization Name"), <<"o">>}, + {?T("Organization Unit"), <<"ou">>}]. + +default_search_reported() -> + [{?T("Full Name"), <<"FN">>}, + {?T("Given Name"), <<"FIRST">>}, + {?T("Middle Name"), <<"MIDDLE">>}, + {?T("Family Name"), <<"LAST">>}, + {?T("Nickname"), <<"NICK">>}, + {?T("Birthday"), <<"BDAY">>}, + {?T("Country"), <<"CTRY">>}, + {?T("City"), <<"LOCALITY">>}, + {?T("Email"), <<"EMAIL">>}, + {?T("Organization Name"), <<"ORGNAME">>}, + {?T("Organization Unit"), <<"ORGUNIT">>}]. parse_options(Host, Opts) -> - MyHost = gen_mod:get_opt_host(Host, Opts, - <<"vjud.@HOST@">>), - Search = gen_mod:get_opt(search, Opts, - fun(B) when is_boolean(B) -> B end, - true), - Matches = gen_mod:get_opt(matches, Opts, - fun(infinity) -> 0; - (I) when is_integer(I), I>0 -> I - end, 30), - Eldap_ID = jlib:atom_to_binary(gen_mod:get_module_proc(Host, ?PROCNAME)), - Cfg = eldap_utils:get_config(Host, Opts), - UIDsTemp = eldap_utils:get_opt( - {ldap_uids, Host}, Opts, - fun(Us) -> - lists:map( - fun({U, P}) -> - {iolist_to_binary(U), - iolist_to_binary(P)}; - ({U}) -> - {iolist_to_binary(U)} - end, Us) - end, [{<<"uid">>, <<"%u">>}]), + MyHosts = gen_mod:get_opt_hosts(Opts), + Search = mod_vcard_opt:search(Opts), + Matches = mod_vcard_opt:matches(Opts), + Eldap_ID = misc:atom_to_binary(gen_mod:get_module_proc(Host, ?PROCNAME)), + Cfg = ?eldap_config(mod_vcard_ldap_opt, Opts), + UIDsTemp = mod_vcard_ldap_opt:ldap_uids(Opts), UIDs = eldap_utils:uids_domain_subst(Host, UIDsTemp), SubFilter = eldap_utils:generate_subfilter(UIDs), - UserFilter = case eldap_utils:get_opt( - {ldap_filter, Host}, Opts, - fun check_filter/1, <<"">>) of + UserFilter = case mod_vcard_ldap_opt:ldap_filter(Opts) of <<"">> -> SubFilter; F -> @@ -763,48 +373,28 @@ parse_options(Host, Opts) -> {ok, SearchFilter} = eldap_filter:parse(eldap_filter:do_sub(UserFilter, [{<<"%u">>, <<"*">>}])), - VCardMap = gen_mod:get_opt(ldap_vcard_map, Opts, - fun(Ls) -> - lists:map( - fun({S, [{P, L}]}) -> - {iolist_to_binary(S), - iolist_to_binary(P), - [iolist_to_binary(E) - || E <- L]} - end, Ls) - end, ?VCARD_MAP), - SearchFields = gen_mod:get_opt(ldap_search_fields, Opts, - fun(Ls) -> - [{iolist_to_binary(S), - iolist_to_binary(P)} - || {S, P} <- Ls] - end, ?SEARCH_FIELDS), - SearchReported = gen_mod:get_opt(ldap_search_reported, Opts, - fun(Ls) -> - [{iolist_to_binary(S), - iolist_to_binary(P)} - || {S, P} <- Ls] - end, ?SEARCH_REPORTED), + VCardMap = mod_vcard_ldap_opt:ldap_vcard_map(Opts), + SearchFields = mod_vcard_ldap_opt:ldap_search_fields(Opts), + SearchReported = mod_vcard_ldap_opt:ldap_search_reported(Opts), UIDAttrs = [UAttr || {UAttr, _} <- UIDs], - VCardMapAttrs = lists:usort(lists:append([A - || {_, _, A} <- VCardMap]) - ++ UIDAttrs), - SearchReportedAttrs = lists:usort(lists:flatmap(fun ({_, - N}) -> - case - lists:keysearch(N, - 1, - VCardMap) - of - {value, - {_, _, L}} -> - L; - _ -> [] - end - end, - SearchReported) - ++ UIDAttrs), - #state{serverhost = Host, myhost = MyHost, + VCardMapAttrs = lists:usort( + lists:flatten( + lists:map( + fun({_, Map}) -> + [Attrs || {_, Attrs} <- Map] + end, VCardMap) ++ UIDAttrs)), + SearchReportedAttrs = lists:usort( + lists:flatten( + lists:map( + fun ({_, N}) -> + case lists:keyfind(N, 1, VCardMap) of + {_, Map} -> + [Attrs || {_, Attrs} <- Map]; + false -> + [] + end + end, SearchReported) ++ UIDAttrs)), + #state{serverhost = Host, myhosts = MyHosts, eldap_id = Eldap_ID, search = Search, servers = Cfg#eldap_config.servers, backups = Cfg#eldap_config.backups, @@ -822,21 +412,169 @@ parse_options(Host, Opts) -> search_reported_attrs = SearchReportedAttrs, matches = Matches}. -transform_module_options(Opts) -> - lists:map( - fun({ldap_vcard_map, Map}) -> - NewMap = lists:map( - fun({Field, Pattern, Attrs}) -> - {Field, [{Pattern, Attrs}]}; - (Opt) -> - Opt - end, Map), - {ldap_vcard_map, NewMap}; - (Opt) -> - Opt - end, Opts). +mod_opt_type(ldap_search_fields) -> + econf:map( + econf:binary(), + econf:binary()); +mod_opt_type(ldap_search_reported) -> + econf:map( + econf:binary(), + econf:binary()); +mod_opt_type(ldap_vcard_map) -> + econf:map( + econf:binary(), + econf:map( + econf:binary(), + econf:list( + econf:binary()))); +mod_opt_type(ldap_backups) -> + econf:list(econf:domain(), [unique]); +mod_opt_type(ldap_base) -> + econf:binary(); +mod_opt_type(ldap_deref_aliases) -> + econf:enum([never, searching, finding, always]); +mod_opt_type(ldap_encrypt) -> + econf:enum([tls, starttls, none]); +mod_opt_type(ldap_filter) -> + econf:ldap_filter(); +mod_opt_type(ldap_password) -> + econf:binary(); +mod_opt_type(ldap_port) -> + econf:port(); +mod_opt_type(ldap_rootdn) -> + econf:binary(); +mod_opt_type(ldap_servers) -> + econf:list(econf:domain(), [unique]); +mod_opt_type(ldap_tls_cacertfile) -> + econf:pem(); +mod_opt_type(ldap_tls_certfile) -> + econf:pem(); +mod_opt_type(ldap_tls_depth) -> + econf:non_neg_int(); +mod_opt_type(ldap_tls_verify) -> + econf:enum([hard, soft, false]); +mod_opt_type(ldap_uids) -> + econf:either( + econf:list( + econf:and_then( + econf:binary(), + fun(U) -> {U, <<"%u">>} end)), + econf:map(econf:binary(), econf:binary(), [unique])). -check_filter(F) -> - NewF = iolist_to_binary(F), - {ok, _} = eldap_filter:parse(NewF), - NewF. +-spec mod_options(binary()) -> [{ldap_uids, [{binary(), binary()}]} | + {atom(), any()}]. +mod_options(Host) -> + [{ldap_search_fields, default_search_fields()}, + {ldap_search_reported, default_search_reported()}, + {ldap_vcard_map, default_vcard_map()}, + {ldap_backups, ejabberd_option:ldap_backups(Host)}, + {ldap_base, ejabberd_option:ldap_base(Host)}, + {ldap_uids, ejabberd_option:ldap_uids(Host)}, + {ldap_deref_aliases, ejabberd_option:ldap_deref_aliases(Host)}, + {ldap_encrypt, ejabberd_option:ldap_encrypt(Host)}, + {ldap_password, ejabberd_option:ldap_password(Host)}, + {ldap_port, ejabberd_option:ldap_port(Host)}, + {ldap_rootdn, ejabberd_option:ldap_rootdn(Host)}, + {ldap_servers, ejabberd_option:ldap_servers(Host)}, + {ldap_filter, ejabberd_option:ldap_filter(Host)}, + {ldap_tls_certfile, ejabberd_option:ldap_tls_certfile(Host)}, + {ldap_tls_cacertfile, ejabberd_option:ldap_tls_cacertfile(Host)}, + {ldap_tls_depth, ejabberd_option:ldap_tls_depth(Host)}, + {ldap_tls_verify, ejabberd_option:ldap_tls_verify(Host)}]. + +mod_doc() -> + #{opts => + [{ldap_search_fields, + #{value => "{Name: Attribute, ...}", + desc => + ?T("This option defines the search form and the LDAP " + "attributes to search within. 'Name' is the name of a " + "search form field which will be automatically " + "translated by using the translation files " + "(see 'msgs/*.msg' for available words). " + "'Attribute' is the LDAP attribute or the pattern '%u'."), + example => + [{?T("The default is:"), + ["User: \"%u\"", + "\"Full Name\": displayName", + "\"Given Name\": givenName", + "\"Middle Name\": initials", + "\"Family Name\": sn", + "Nickname: \"%u\"", + "Birthday: birthDay", + "Country: c", + "City: l", + "Email: mail", + "\"Organization Name\": o", + "\"Organization Unit\": ou"] + }]}}, + {ldap_search_reported, + #{value => "{SearchField: VcardField}, ...}", + desc => + ?T("This option defines which search fields should be " + "reported. 'SearchField' is the name of a search form " + "field which will be automatically translated by using " + "the translation files (see 'msgs/*.msg' for available " + "words). 'VcardField' is the vCard field name defined " + "in the 'ldap_vcard_map' option."), + example => + [{?T("The default is:"), + ["\"Full Name\": FN", + "\"Given Name\": FIRST", + "\"Middle Name\": MIDDLE", + "\"Family Name\": LAST", + "\"Nickname\": NICKNAME", + "\"Birthday\": BDAY", + "\"Country\": CTRY", + "\"City\": LOCALITY", + "\"Email\": EMAIL", + "\"Organization Name\": ORGNAME", + "\"Organization Unit\": ORGUNIT"] + }]}}, + {ldap_vcard_map, + #{value => "{Name: {Pattern, LDAPattributes}, ...}", + desc => + ?T("With this option you can set the table that maps LDAP " + "attributes to vCard fields. 'Name' is the type name of " + "the vCard as defined in " + "https://tools.ietf.org/html/rfc2426[RFC 2426]. " + "'Pattern' is a string which contains " + "pattern variables '%u', '%d' or '%s'. " + "'LDAPattributes' is the list containing LDAP attributes. " + "The pattern variables '%s' will be sequentially replaced " + "with the values of LDAP attributes from " + "'List_of_LDAP_attributes', '%u' will be replaced with " + "the user part of a JID, and '%d' will be replaced with " + "the domain part of a JID."), + example => + [{?T("The default is:"), + ["NICKNAME: {\"%u\": []}", + "FN: {\"%s\": [displayName]}", + "LAST: {\"%s\": [sn]}", + "FIRST: {\"%s\": [givenName]}", + "MIDDLE: {\"%s\": [initials]}", + "ORGNAME: {\"%s\": [o]}", + "ORGUNIT: {\"%s\": [ou]}", + "CTRY: {\"%s\": [c]}", + "LOCALITY: {\"%s\": [l]}", + "STREET: {\"%s\": [street]}", + "REGION: {\"%s\": [st]}", + "PCODE: {\"%s\": [postalCode]}", + "TITLE: {\"%s\": [title]}", + "URL: {\"%s\": [labeleduri]}", + "DESC: {\"%s\": [description]}", + "TEL: {\"%s\": [telephoneNumber]}", + "EMAIL: {\"%s\": [mail]}", + "BDAY: {\"%s\": [birthDay]}", + "ROLE: {\"%s\": [employeeType]}", + "PHOTO: {\"%s\": [jpegPhoto]}"] + }]}}] ++ + [{Opt, + #{desc => + {?T("Same as top-level _`~s`_ option, but " + "applied to this module only."), [Opt]}}} + || Opt <- [ldap_base, ldap_servers, ldap_uids, + ldap_deref_aliases, ldap_encrypt, ldap_password, + ldap_port, ldap_rootdn, ldap_filter, + ldap_tls_certfile, ldap_tls_cacertfile, + ldap_tls_depth, ldap_tls_verify, ldap_backups]]}. diff --git a/src/mod_vcard_ldap_opt.erl b/src/mod_vcard_ldap_opt.erl new file mode 100644 index 000000000..2d2af6f2b --- /dev/null +++ b/src/mod_vcard_ldap_opt.erl @@ -0,0 +1,125 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_vcard_ldap_opt). + +-export([ldap_backups/1]). +-export([ldap_base/1]). +-export([ldap_deref_aliases/1]). +-export([ldap_encrypt/1]). +-export([ldap_filter/1]). +-export([ldap_password/1]). +-export([ldap_port/1]). +-export([ldap_rootdn/1]). +-export([ldap_search_fields/1]). +-export([ldap_search_reported/1]). +-export([ldap_servers/1]). +-export([ldap_tls_cacertfile/1]). +-export([ldap_tls_certfile/1]). +-export([ldap_tls_depth/1]). +-export([ldap_tls_verify/1]). +-export([ldap_uids/1]). +-export([ldap_vcard_map/1]). + +-spec ldap_backups(gen_mod:opts() | global | binary()) -> [binary()]. +ldap_backups(Opts) when is_map(Opts) -> + gen_mod:get_opt(ldap_backups, Opts); +ldap_backups(Host) -> + gen_mod:get_module_opt(Host, mod_vcard, ldap_backups). + +-spec ldap_base(gen_mod:opts() | global | binary()) -> binary(). +ldap_base(Opts) when is_map(Opts) -> + gen_mod:get_opt(ldap_base, Opts); +ldap_base(Host) -> + gen_mod:get_module_opt(Host, mod_vcard, ldap_base). + +-spec ldap_deref_aliases(gen_mod:opts() | global | binary()) -> 'always' | 'finding' | 'never' | 'searching'. +ldap_deref_aliases(Opts) when is_map(Opts) -> + gen_mod:get_opt(ldap_deref_aliases, Opts); +ldap_deref_aliases(Host) -> + gen_mod:get_module_opt(Host, mod_vcard, ldap_deref_aliases). + +-spec ldap_encrypt(gen_mod:opts() | global | binary()) -> 'none' | 'starttls' | 'tls'. +ldap_encrypt(Opts) when is_map(Opts) -> + gen_mod:get_opt(ldap_encrypt, Opts); +ldap_encrypt(Host) -> + gen_mod:get_module_opt(Host, mod_vcard, ldap_encrypt). + +-spec ldap_filter(gen_mod:opts() | global | binary()) -> binary(). +ldap_filter(Opts) when is_map(Opts) -> + gen_mod:get_opt(ldap_filter, Opts); +ldap_filter(Host) -> + gen_mod:get_module_opt(Host, mod_vcard, ldap_filter). + +-spec ldap_password(gen_mod:opts() | global | binary()) -> binary(). +ldap_password(Opts) when is_map(Opts) -> + gen_mod:get_opt(ldap_password, Opts); +ldap_password(Host) -> + gen_mod:get_module_opt(Host, mod_vcard, ldap_password). + +-spec ldap_port(gen_mod:opts() | global | binary()) -> 1..1114111. +ldap_port(Opts) when is_map(Opts) -> + gen_mod:get_opt(ldap_port, Opts); +ldap_port(Host) -> + gen_mod:get_module_opt(Host, mod_vcard, ldap_port). + +-spec ldap_rootdn(gen_mod:opts() | global | binary()) -> binary(). +ldap_rootdn(Opts) when is_map(Opts) -> + gen_mod:get_opt(ldap_rootdn, Opts); +ldap_rootdn(Host) -> + gen_mod:get_module_opt(Host, mod_vcard, ldap_rootdn). + +-spec ldap_search_fields(gen_mod:opts() | global | binary()) -> [{binary(),binary()}]. +ldap_search_fields(Opts) when is_map(Opts) -> + gen_mod:get_opt(ldap_search_fields, Opts); +ldap_search_fields(Host) -> + gen_mod:get_module_opt(Host, mod_vcard, ldap_search_fields). + +-spec ldap_search_reported(gen_mod:opts() | global | binary()) -> [{binary(),binary()}]. +ldap_search_reported(Opts) when is_map(Opts) -> + gen_mod:get_opt(ldap_search_reported, Opts); +ldap_search_reported(Host) -> + gen_mod:get_module_opt(Host, mod_vcard, ldap_search_reported). + +-spec ldap_servers(gen_mod:opts() | global | binary()) -> [binary()]. +ldap_servers(Opts) when is_map(Opts) -> + gen_mod:get_opt(ldap_servers, Opts); +ldap_servers(Host) -> + gen_mod:get_module_opt(Host, mod_vcard, ldap_servers). + +-spec ldap_tls_cacertfile(gen_mod:opts() | global | binary()) -> binary(). +ldap_tls_cacertfile(Opts) when is_map(Opts) -> + gen_mod:get_opt(ldap_tls_cacertfile, Opts); +ldap_tls_cacertfile(Host) -> + gen_mod:get_module_opt(Host, mod_vcard, ldap_tls_cacertfile). + +-spec ldap_tls_certfile(gen_mod:opts() | global | binary()) -> binary(). +ldap_tls_certfile(Opts) when is_map(Opts) -> + gen_mod:get_opt(ldap_tls_certfile, Opts); +ldap_tls_certfile(Host) -> + gen_mod:get_module_opt(Host, mod_vcard, ldap_tls_certfile). + +-spec ldap_tls_depth(gen_mod:opts() | global | binary()) -> non_neg_integer(). +ldap_tls_depth(Opts) when is_map(Opts) -> + gen_mod:get_opt(ldap_tls_depth, Opts); +ldap_tls_depth(Host) -> + gen_mod:get_module_opt(Host, mod_vcard, ldap_tls_depth). + +-spec ldap_tls_verify(gen_mod:opts() | global | binary()) -> 'false' | 'hard' | 'soft'. +ldap_tls_verify(Opts) when is_map(Opts) -> + gen_mod:get_opt(ldap_tls_verify, Opts); +ldap_tls_verify(Host) -> + gen_mod:get_module_opt(Host, mod_vcard, ldap_tls_verify). + +-spec ldap_uids(gen_mod:opts() | global | binary()) -> [{binary(),binary()}]. +ldap_uids(Opts) when is_map(Opts) -> + gen_mod:get_opt(ldap_uids, Opts); +ldap_uids(Host) -> + gen_mod:get_module_opt(Host, mod_vcard, ldap_uids). + +-spec ldap_vcard_map(gen_mod:opts() | global | binary()) -> [{binary(),[{binary(),[binary()]}]}]. +ldap_vcard_map(Opts) when is_map(Opts) -> + gen_mod:get_opt(ldap_vcard_map, Opts); +ldap_vcard_map(Host) -> + gen_mod:get_module_opt(Host, mod_vcard, ldap_vcard_map). + diff --git a/src/mod_vcard_mnesia.erl b/src/mod_vcard_mnesia.erl new file mode 100644 index 000000000..e70b13fc0 --- /dev/null +++ b/src/mod_vcard_mnesia.erl @@ -0,0 +1,285 @@ +%%%------------------------------------------------------------------- +%%% File : mod_vcard_mnesia.erl +%%% Author : Evgeny Khramtsov +%%% Created : 13 Apr 2016 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_vcard_mnesia). + +-behaviour(mod_vcard). + +%% API +-export([init/2, stop/1, import/3, get_vcard/2, set_vcard/4, search/4, + search_fields/1, search_reported/1, remove_user/2]). +-export([is_search_supported/1]). +-export([need_transform/1, transform/1]). +-export([mod_opt_type/1, mod_options/1, mod_doc/0]). + +-include_lib("xmpp/include/xmpp.hrl"). +-include("mod_vcard.hrl"). +-include("logger.hrl"). +-include("translate.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + ejabberd_mnesia:create(?MODULE, vcard, + [{disc_only_copies, [node()]}, + {attributes, record_info(fields, vcard)}]), + ejabberd_mnesia:create(?MODULE, vcard_search, + [{disc_copies, [node()]}, + {attributes, + record_info(fields, vcard_search)}, + {index, [ luser, lfn, lfamily, + lgiven, lmiddle, lnickname, + lbday, lctry, llocality, + lemail, lorgname, lorgunit + ]}]). + +stop(_Host) -> + ok. + +is_search_supported(_ServerHost) -> + true. + +get_vcard(LUser, LServer) -> + US = {LUser, LServer}, + Rs = mnesia:dirty_read(vcard, US), + {ok, lists:map(fun (R) -> R#vcard.vcard end, Rs)}. + +set_vcard(LUser, LServer, VCARD, VCardSearch) -> + US = {LUser, LServer}, + F = fun () -> + mnesia:write(#vcard{us = US, vcard = VCARD}), + mnesia:write(VCardSearch) + end, + mnesia:transaction(F). + +search(LServer, Data, AllowReturnAll, MaxMatch) -> + MatchSpec = make_matchspec(LServer, Data), + if (MatchSpec == #vcard_search{_ = '_'}) and + not AllowReturnAll -> + []; + true -> + case catch mnesia:dirty_select(vcard_search, + [{MatchSpec, [], ['$_']}]) of + {'EXIT', Reason} -> + ?ERROR_MSG("~p", [Reason]), []; + Rs -> + Fields = lists:map(fun record_to_item/1, Rs), + case MaxMatch of + infinity -> + Fields; + Val -> + lists:sublist(Fields, Val) + end + end + end. + +search_fields(_LServer) -> + [{?T("User"), <<"user">>}, + {?T("Full Name"), <<"fn">>}, + {?T("Name"), <<"first">>}, + {?T("Middle Name"), <<"middle">>}, + {?T("Family Name"), <<"last">>}, + {?T("Nickname"), <<"nick">>}, + {?T("Birthday"), <<"bday">>}, + {?T("Country"), <<"ctry">>}, + {?T("City"), <<"locality">>}, + {?T("Email"), <<"email">>}, + {?T("Organization Name"), <<"orgname">>}, + {?T("Organization Unit"), <<"orgunit">>}]. + +search_reported(_LServer) -> + [{?T("Jabber ID"), <<"jid">>}, + {?T("Full Name"), <<"fn">>}, + {?T("Name"), <<"first">>}, + {?T("Middle Name"), <<"middle">>}, + {?T("Family Name"), <<"last">>}, + {?T("Nickname"), <<"nick">>}, + {?T("Birthday"), <<"bday">>}, + {?T("Country"), <<"ctry">>}, + {?T("City"), <<"locality">>}, + {?T("Email"), <<"email">>}, + {?T("Organization Name"), <<"orgname">>}, + {?T("Organization Unit"), <<"orgunit">>}]. + +remove_user(LUser, LServer) -> + US = {LUser, LServer}, + F = fun () -> + mnesia:delete({vcard, US}), + mnesia:delete({vcard_search, US}) + end, + mnesia:transaction(F). + +import(LServer, <<"vcard">>, [LUser, XML, _TimeStamp]) -> + #xmlel{} = El = fxml_stream:parse_element(XML), + VCard = #vcard{us = {LUser, LServer}, vcard = El}, + mnesia:dirty_write(VCard); +import(LServer, <<"vcard_search">>, + [User, LUser, FN, LFN, + Family, LFamily, Given, LGiven, + Middle, LMiddle, Nickname, LNickname, + BDay, LBDay, CTRY, LCTRY, Locality, LLocality, + EMail, LEMail, OrgName, LOrgName, OrgUnit, LOrgUnit]) -> + mnesia:dirty_write( + #vcard_search{us = {LUser, LServer}, + user = {User, LServer}, luser = LUser, + fn = FN, lfn = LFN, family = Family, + lfamily = LFamily, given = Given, + lgiven = LGiven, middle = Middle, + lmiddle = LMiddle, nickname = Nickname, + lnickname = LNickname, bday = BDay, + lbday = LBDay, ctry = CTRY, lctry = LCTRY, + locality = Locality, llocality = LLocality, + email = EMail, lemail = LEMail, + orgname = OrgName, lorgname = LOrgName, + orgunit = OrgUnit, lorgunit = LOrgUnit}). + +need_transform({vcard, {U, S}, _}) when is_list(U) orelse is_list(S) -> + ?INFO_MSG("Mnesia table 'vcard' will be converted to binary", []), + true; +need_transform(R) when element(1, R) == vcard_search -> + case element(2, R) of + {U, S} when is_list(U) orelse is_list(S) -> + ?INFO_MSG("Mnesia table 'vcard_search' will be converted to binary", []), + true; + _ -> + false + end; +need_transform(_) -> + false. + +transform(#vcard{us = {U, S}, vcard = El} = R) -> + R#vcard{us = {iolist_to_binary(U), iolist_to_binary(S)}, + vcard = fxml:to_xmlel(El)}; +transform(#vcard_search{} = VS) -> + [vcard_search | L] = tuple_to_list(VS), + NewL = lists:map( + fun({U, S}) -> + {iolist_to_binary(U), iolist_to_binary(S)}; + (Str) -> + iolist_to_binary(Str) + end, L), + list_to_tuple([vcard_search | NewL]). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +make_matchspec(LServer, Data) -> + GlobMatch = #vcard_search{_ = '_'}, + Match = filter_fields(Data, GlobMatch, LServer), + Match. + +filter_fields([], Match, _LServer) -> + Match; +filter_fields([{SVar, [Val]} | Ds], Match, LServer) + when is_binary(Val) and (Val /= <<"">>) -> + LVal = mod_vcard:string2lower(Val), + NewMatch = case SVar of + <<"user">> -> + case mod_vcard_mnesia_opt:search_all_hosts(LServer) of + true -> Match#vcard_search{luser = make_val(LVal)}; + false -> + Host = find_my_host(LServer), + Match#vcard_search{us = {make_val(LVal), Host}} + end; + <<"fn">> -> Match#vcard_search{lfn = make_val(LVal)}; + <<"last">> -> + Match#vcard_search{lfamily = make_val(LVal)}; + <<"first">> -> + Match#vcard_search{lgiven = make_val(LVal)}; + <<"middle">> -> + Match#vcard_search{lmiddle = make_val(LVal)}; + <<"nick">> -> + Match#vcard_search{lnickname = make_val(LVal)}; + <<"bday">> -> + Match#vcard_search{lbday = make_val(LVal)}; + <<"ctry">> -> + Match#vcard_search{lctry = make_val(LVal)}; + <<"locality">> -> + Match#vcard_search{llocality = make_val(LVal)}; + <<"email">> -> + Match#vcard_search{lemail = make_val(LVal)}; + <<"orgname">> -> + Match#vcard_search{lorgname = make_val(LVal)}; + <<"orgunit">> -> + Match#vcard_search{lorgunit = make_val(LVal)}; + _ -> Match + end, + filter_fields(Ds, NewMatch, LServer); +filter_fields([_ | Ds], Match, LServer) -> + filter_fields(Ds, Match, LServer). + +make_val(Val) -> + case str:suffix(<<"*">>, Val) of + true -> [str:substr(Val, 1, byte_size(Val) - 1)] ++ '_'; + _ -> Val + end. + +find_my_host(LServer) -> + Parts = str:tokens(LServer, <<".">>), + find_my_host(Parts, ejabberd_option:hosts()). + +find_my_host([], _Hosts) -> ejabberd_config:get_myname(); +find_my_host([_ | Tail] = Parts, Hosts) -> + Domain = parts_to_string(Parts), + case lists:member(Domain, Hosts) of + true -> Domain; + false -> find_my_host(Tail, Hosts) + end. + +parts_to_string(Parts) -> + str:strip(list_to_binary( + lists:map(fun (S) -> <> end, Parts)), + right, $.). + +-spec record_to_item(#vcard_search{}) -> [{binary(), binary()}]. +record_to_item(R) -> + {User, Server} = R#vcard_search.user, + [{<<"jid">>, <>}, + {<<"fn">>, (R#vcard_search.fn)}, + {<<"last">>, (R#vcard_search.family)}, + {<<"first">>, (R#vcard_search.given)}, + {<<"middle">>, (R#vcard_search.middle)}, + {<<"nick">>, (R#vcard_search.nickname)}, + {<<"bday">>, (R#vcard_search.bday)}, + {<<"ctry">>, (R#vcard_search.ctry)}, + {<<"locality">>, (R#vcard_search.locality)}, + {<<"email">>, (R#vcard_search.email)}, + {<<"orgname">>, (R#vcard_search.orgname)}, + {<<"orgunit">>, (R#vcard_search.orgunit)}]. + +mod_opt_type(search_all_hosts) -> + econf:bool(). + +mod_options(_) -> + [{search_all_hosts, true}]. + +mod_doc() -> + #{opts => + [{search_all_hosts, + #{value => "true | false", + desc => + ?T("Whether to perform search on all " + "virtual hosts or not. The default " + "value is 'true'.")}}]}. diff --git a/src/mod_vcard_mnesia_opt.erl b/src/mod_vcard_mnesia_opt.erl new file mode 100644 index 000000000..a128c086d --- /dev/null +++ b/src/mod_vcard_mnesia_opt.erl @@ -0,0 +1,13 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_vcard_mnesia_opt). + +-export([search_all_hosts/1]). + +-spec search_all_hosts(gen_mod:opts() | global | binary()) -> boolean(). +search_all_hosts(Opts) when is_map(Opts) -> + gen_mod:get_opt(search_all_hosts, Opts); +search_all_hosts(Host) -> + gen_mod:get_module_opt(Host, mod_vcard, search_all_hosts). + diff --git a/src/mod_vcard_opt.erl b/src/mod_vcard_opt.erl new file mode 100644 index 000000000..3a7cc7754 --- /dev/null +++ b/src/mod_vcard_opt.erl @@ -0,0 +1,90 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_vcard_opt). + +-export([allow_return_all/1]). +-export([cache_life_time/1]). +-export([cache_missed/1]). +-export([cache_size/1]). +-export([db_type/1]). +-export([host/1]). +-export([hosts/1]). +-export([matches/1]). +-export([name/1]). +-export([search/1]). +-export([use_cache/1]). +-export([vcard/1]). + +-spec allow_return_all(gen_mod:opts() | global | binary()) -> boolean(). +allow_return_all(Opts) when is_map(Opts) -> + gen_mod:get_opt(allow_return_all, Opts); +allow_return_all(Host) -> + gen_mod:get_module_opt(Host, mod_vcard, allow_return_all). + +-spec cache_life_time(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +cache_life_time(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_life_time, Opts); +cache_life_time(Host) -> + gen_mod:get_module_opt(Host, mod_vcard, cache_life_time). + +-spec cache_missed(gen_mod:opts() | global | binary()) -> boolean(). +cache_missed(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_missed, Opts); +cache_missed(Host) -> + gen_mod:get_module_opt(Host, mod_vcard, cache_missed). + +-spec cache_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +cache_size(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_size, Opts); +cache_size(Host) -> + gen_mod:get_module_opt(Host, mod_vcard, cache_size). + +-spec db_type(gen_mod:opts() | global | binary()) -> atom(). +db_type(Opts) when is_map(Opts) -> + gen_mod:get_opt(db_type, Opts); +db_type(Host) -> + gen_mod:get_module_opt(Host, mod_vcard, db_type). + +-spec host(gen_mod:opts() | global | binary()) -> binary(). +host(Opts) when is_map(Opts) -> + gen_mod:get_opt(host, Opts); +host(Host) -> + gen_mod:get_module_opt(Host, mod_vcard, host). + +-spec hosts(gen_mod:opts() | global | binary()) -> [binary()]. +hosts(Opts) when is_map(Opts) -> + gen_mod:get_opt(hosts, Opts); +hosts(Host) -> + gen_mod:get_module_opt(Host, mod_vcard, hosts). + +-spec matches(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +matches(Opts) when is_map(Opts) -> + gen_mod:get_opt(matches, Opts); +matches(Host) -> + gen_mod:get_module_opt(Host, mod_vcard, matches). + +-spec name(gen_mod:opts() | global | binary()) -> binary(). +name(Opts) when is_map(Opts) -> + gen_mod:get_opt(name, Opts); +name(Host) -> + gen_mod:get_module_opt(Host, mod_vcard, name). + +-spec search(gen_mod:opts() | global | binary()) -> boolean(). +search(Opts) when is_map(Opts) -> + gen_mod:get_opt(search, Opts); +search(Host) -> + gen_mod:get_module_opt(Host, mod_vcard, search). + +-spec use_cache(gen_mod:opts() | global | binary()) -> boolean(). +use_cache(Opts) when is_map(Opts) -> + gen_mod:get_opt(use_cache, Opts); +use_cache(Host) -> + gen_mod:get_module_opt(Host, mod_vcard, use_cache). + +-spec vcard(gen_mod:opts() | global | binary()) -> 'undefined' | tuple(). +vcard(Opts) when is_map(Opts) -> + gen_mod:get_opt(vcard, Opts); +vcard(Host) -> + gen_mod:get_module_opt(Host, mod_vcard, vcard). + diff --git a/src/mod_vcard_sql.erl b/src/mod_vcard_sql.erl new file mode 100644 index 000000000..18456f402 --- /dev/null +++ b/src/mod_vcard_sql.erl @@ -0,0 +1,404 @@ +%%%------------------------------------------------------------------- +%%% File : mod_vcard_sql.erl +%%% Author : Evgeny Khramtsov +%%% Created : 13 Apr 2016 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_vcard_sql). + + +-behaviour(mod_vcard). + +%% API +-export([init/2, stop/1, get_vcard/2, set_vcard/4, search/4, remove_user/2, + search_fields/1, search_reported/1, import/3, export/1]). +-export([is_search_supported/1]). +-export([sql_schemas/0]). + +-include_lib("xmpp/include/xmpp.hrl"). +-include("mod_vcard.hrl"). +-include("logger.hrl"). +-include("ejabberd_sql_pt.hrl"). +-include("translate.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(Host, _Opts) -> + ejabberd_sql_schema:update_schema(Host, ?MODULE, sql_schemas()), + ok. + +sql_schemas() -> + [#sql_schema{ + version = 1, + tables = + [#sql_table{ + name = <<"vcard">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"vcard">>, type = {text, big}}, + #sql_column{name = <<"created_at">>, type = timestamp, + default = true}], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"username">>], + unique = true}]}, + #sql_table{ + name = <<"vcard_search">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"lusername">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"fn">>, type = text}, + #sql_column{name = <<"lfn">>, type = text}, + #sql_column{name = <<"family">>, type = text}, + #sql_column{name = <<"lfamily">>, type = text}, + #sql_column{name = <<"given">>, type = text}, + #sql_column{name = <<"lgiven">>, type = text}, + #sql_column{name = <<"middle">>, type = text}, + #sql_column{name = <<"lmiddle">>, type = text}, + #sql_column{name = <<"nickname">>, type = text}, + #sql_column{name = <<"lnickname">>, type = text}, + #sql_column{name = <<"bday">>, type = text}, + #sql_column{name = <<"lbday">>, type = text}, + #sql_column{name = <<"ctry">>, type = text}, + #sql_column{name = <<"lctry">>, type = text}, + #sql_column{name = <<"locality">>, type = text}, + #sql_column{name = <<"llocality">>, type = text}, + #sql_column{name = <<"email">>, type = text}, + #sql_column{name = <<"lemail">>, type = text}, + #sql_column{name = <<"orgname">>, type = text}, + #sql_column{name = <<"lorgname">>, type = text}, + #sql_column{name = <<"orgunit">>, type = text}, + #sql_column{name = <<"lorgunit">>, type = text}], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"lusername">>], + unique = true}, + #sql_index{ + columns = [<<"server_host">>, <<"lfn">>]}, + #sql_index{ + columns = [<<"server_host">>, <<"lfamily">>]}, + #sql_index{ + columns = [<<"server_host">>, <<"lgiven">>]}, + #sql_index{ + columns = [<<"server_host">>, <<"lmiddle">>]}, + #sql_index{ + columns = [<<"server_host">>, <<"lnickname">>]}, + #sql_index{ + columns = [<<"server_host">>, <<"lbday">>]}, + #sql_index{ + columns = [<<"server_host">>, <<"lctry">>]}, + #sql_index{ + columns = [<<"server_host">>, <<"llocality">>]}, + #sql_index{ + columns = [<<"server_host">>, <<"lemail">>]}, + #sql_index{ + columns = [<<"server_host">>, <<"lorgname">>]}, + #sql_index{ + columns = [<<"server_host">>, <<"lorgunit">>]}]}]}]. + +stop(_Host) -> + ok. + +is_search_supported(_LServer) -> + true. + +get_vcard(LUser, LServer) -> + case ejabberd_sql:sql_query( + LServer, + ?SQL("select @(vcard)s from vcard" + " where username=%(LUser)s and %(LServer)H")) of + {selected, [{SVCARD}]} -> + case fxml_stream:parse_element(SVCARD) of + {error, _Reason} -> error; + VCARD -> {ok, [VCARD]} + end; + {selected, []} -> {ok, []}; + _ -> error + end. + +set_vcard(LUser, LServer, VCARD, + #vcard_search{user = {User, _}, + fn = FN, + lfn = LFN, + family = Family, + lfamily = LFamily, + given = Given, + lgiven = LGiven, + middle = Middle, + lmiddle = LMiddle, + nickname = Nickname, + lnickname = LNickname, + bday = BDay, + lbday = LBDay, + ctry = CTRY, + lctry = LCTRY, + locality = Locality, + llocality = LLocality, + email = EMail, + lemail = LEMail, + orgname = OrgName, + lorgname = LOrgName, + orgunit = OrgUnit, + lorgunit = LOrgUnit}) -> + SVCARD = fxml:element_to_binary(VCARD), + ejabberd_sql:sql_transaction( + LServer, + fun() -> + ?SQL_UPSERT(LServer, "vcard", + ["!username=%(LUser)s", + "!server_host=%(LServer)s", + "vcard=%(SVCARD)s"]), + ?SQL_UPSERT(LServer, "vcard_search", + ["username=%(User)s", + "!lusername=%(LUser)s", + "!server_host=%(LServer)s", + "fn=%(FN)s", + "lfn=%(LFN)s", + "family=%(Family)s", + "lfamily=%(LFamily)s", + "given=%(Given)s", + "lgiven=%(LGiven)s", + "middle=%(Middle)s", + "lmiddle=%(LMiddle)s", + "nickname=%(Nickname)s", + "lnickname=%(LNickname)s", + "bday=%(BDay)s", + "lbday=%(LBDay)s", + "ctry=%(CTRY)s", + "lctry=%(LCTRY)s", + "locality=%(Locality)s", + "llocality=%(LLocality)s", + "email=%(EMail)s", + "lemail=%(LEMail)s", + "orgname=%(OrgName)s", + "lorgname=%(LOrgName)s", + "orgunit=%(OrgUnit)s", + "lorgunit=%(LOrgUnit)s"]) + end). + +search(LServer, Data, AllowReturnAll, MaxMatch) -> + MatchSpec = make_matchspec(LServer, Data), + if (MatchSpec == <<"">>) and not AllowReturnAll -> []; + true -> + Limit = case MaxMatch of + infinity -> + <<"">>; + Val -> + [<<" LIMIT ">>, integer_to_binary(Val)] + end, + case catch ejabberd_sql:sql_query( + LServer, + [<<"select username, fn, family, given, " + "middle, nickname, bday, ctry, " + "locality, email, orgname, orgunit " + "from vcard_search ">>, + MatchSpec, Limit, <<";">>]) of + {selected, + [<<"username">>, <<"fn">>, <<"family">>, <<"given">>, + <<"middle">>, <<"nickname">>, <<"bday">>, <<"ctry">>, + <<"locality">>, <<"email">>, <<"orgname">>, + <<"orgunit">>], Rs} when is_list(Rs) -> + [row_to_item(LServer, R) || R <- Rs]; + Error -> + ?ERROR_MSG("~p", [Error]), [] + end + end. + +search_fields(_LServer) -> + [{?T("User"), <<"user">>}, + {?T("Full Name"), <<"fn">>}, + {?T("Name"), <<"first">>}, + {?T("Middle Name"), <<"middle">>}, + {?T("Family Name"), <<"last">>}, + {?T("Nickname"), <<"nick">>}, + {?T("Birthday"), <<"bday">>}, + {?T("Country"), <<"ctry">>}, + {?T("City"), <<"locality">>}, + {?T("Email"), <<"email">>}, + {?T("Organization Name"), <<"orgname">>}, + {?T("Organization Unit"), <<"orgunit">>}]. + +search_reported(_LServer) -> + [{?T("Jabber ID"), <<"jid">>}, + {?T("Full Name"), <<"fn">>}, + {?T("Name"), <<"first">>}, + {?T("Middle Name"), <<"middle">>}, + {?T("Family Name"), <<"last">>}, + {?T("Nickname"), <<"nick">>}, + {?T("Birthday"), <<"bday">>}, + {?T("Country"), <<"ctry">>}, + {?T("City"), <<"locality">>}, + {?T("Email"), <<"email">>}, + {?T("Organization Name"), <<"orgname">>}, + {?T("Organization Unit"), <<"orgunit">>}]. + +remove_user(LUser, LServer) -> + ejabberd_sql:sql_transaction( + LServer, + fun() -> + ejabberd_sql:sql_query_t( + ?SQL("delete from vcard" + " where username=%(LUser)s and %(LServer)H")), + ejabberd_sql:sql_query_t( + ?SQL("delete from vcard_search" + " where lusername=%(LUser)s and %(LServer)H")) + end). + +export(_Server) -> + [{vcard, + fun(Host, #vcard{us = {LUser, LServer}, vcard = VCARD}) + when LServer == Host -> + SVCARD = fxml:element_to_binary(VCARD), + [?SQL("delete from vcard" + " where username=%(LUser)s and %(LServer)H;"), + ?SQL_INSERT("vcard", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "vcard=%(SVCARD)s"])]; + (_Host, _R) -> + [] + end}, + {vcard_search, + fun(Host, #vcard_search{user = {User, LServer}, luser = LUser, + fn = FN, lfn = LFN, family = Family, + lfamily = LFamily, given = Given, + lgiven = LGiven, middle = Middle, + lmiddle = LMiddle, nickname = Nickname, + lnickname = LNickname, bday = BDay, + lbday = LBDay, ctry = CTRY, lctry = LCTRY, + locality = Locality, llocality = LLocality, + email = EMail, lemail = LEMail, + orgname = OrgName, lorgname = LOrgName, + orgunit = OrgUnit, lorgunit = LOrgUnit}) + when LServer == Host -> + [?SQL("delete from vcard_search" + " where lusername=%(LUser)s and %(LServer)H;"), + ?SQL_INSERT("vcard_search", + ["username=%(User)s", + "lusername=%(LUser)s", + "server_host=%(LServer)s", + "fn=%(FN)s", + "lfn=%(LFN)s", + "family=%(Family)s", + "lfamily=%(LFamily)s", + "given=%(Given)s", + "lgiven=%(LGiven)s", + "middle=%(Middle)s", + "lmiddle=%(LMiddle)s", + "nickname=%(Nickname)s", + "lnickname=%(LNickname)s", + "bday=%(BDay)s", + "lbday=%(LBDay)s", + "ctry=%(CTRY)s", + "lctry=%(LCTRY)s", + "locality=%(Locality)s", + "llocality=%(LLocality)s", + "email=%(EMail)s", + "lemail=%(LEMail)s", + "orgname=%(OrgName)s", + "lorgname=%(LOrgName)s", + "orgunit=%(OrgUnit)s", + "lorgunit=%(LOrgUnit)s"])]; + (_Host, _R) -> + [] + end}]. + +import(_, _, _) -> + ok. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +make_matchspec(LServer, Data) -> + filter_fields(Data, <<"">>, LServer). + +filter_fields([], Match, LServer) -> + case ejabberd_sql:use_new_schema() of + true -> + SQLType = ejabberd_option:sql_type(LServer), + SServer = ejabberd_sql:to_string_literal(SQLType, LServer), + case Match of + <<"">> -> [<<"where server_host=">>, SServer]; + _ -> [<<" where server_host=">>, SServer, <<" and ">>, Match] + end; + false -> + case Match of + <<"">> -> <<"">>; + _ -> [<<" where ">>, Match] + end + end; +filter_fields([{SVar, [Val]} | Ds], Match, LServer) + when is_binary(Val) and (Val /= <<"">>) -> + LVal = mod_vcard:string2lower(Val), + NewMatch = case SVar of + <<"user">> -> make_val(LServer, Match, <<"lusername">>, LVal); + <<"fn">> -> make_val(LServer, Match, <<"lfn">>, LVal); + <<"last">> -> make_val(LServer, Match, <<"lfamily">>, LVal); + <<"first">> -> make_val(LServer, Match, <<"lgiven">>, LVal); + <<"middle">> -> make_val(LServer, Match, <<"lmiddle">>, LVal); + <<"nick">> -> make_val(LServer, Match, <<"lnickname">>, LVal); + <<"bday">> -> make_val(LServer, Match, <<"lbday">>, LVal); + <<"ctry">> -> make_val(LServer, Match, <<"lctry">>, LVal); + <<"locality">> -> + make_val(LServer, Match, <<"llocality">>, LVal); + <<"email">> -> make_val(LServer, Match, <<"lemail">>, LVal); + <<"orgname">> -> make_val(LServer, Match, <<"lorgname">>, LVal); + <<"orgunit">> -> make_val(LServer, Match, <<"lorgunit">>, LVal); + _ -> Match + end, + filter_fields(Ds, NewMatch, LServer); +filter_fields([_ | Ds], Match, LServer) -> + filter_fields(Ds, Match, LServer). + +make_val(LServer, Match, Field, Val) -> + Condition = case str:suffix(<<"*">>, Val) of + true -> + Val1 = str:substr(Val, 1, byte_size(Val) - 1), + SVal = <<(ejabberd_sql:escape( + ejabberd_sql:escape_like_arg_circumflex( + Val1)))/binary, + "%">>, + [Field, <<" LIKE '">>, SVal, <<"' ESCAPE '^'">>]; + _ -> + SQLType = ejabberd_option:sql_type(LServer), + SVal = ejabberd_sql:to_string_literal(SQLType, Val), + [Field, <<" = ">>, SVal] + end, + case Match of + <<"">> -> Condition; + _ -> [Match, <<" and ">>, Condition] + end. + +row_to_item(LServer, [Username, FN, Family, Given, Middle, Nickname, BDay, + CTRY, Locality, EMail, OrgName, OrgUnit]) -> + [{<<"jid">>, <>}, + {<<"fn">>, FN}, + {<<"last">>, Family}, + {<<"first">>, Given}, + {<<"middle">>, Middle}, + {<<"nick">>, Nickname}, + {<<"bday">>, BDay}, + {<<"ctry">>, CTRY}, + {<<"locality">>, Locality}, + {<<"email">>, EMail}, + {<<"orgname">>, OrgName}, + {<<"orgunit">>, OrgUnit}]. diff --git a/src/mod_vcard_xupdate.erl b/src/mod_vcard_xupdate.erl index 97d9abbb4..70ed707b2 100644 --- a/src/mod_vcard_xupdate.erl +++ b/src/mod_vcard_xupdate.erl @@ -3,231 +3,240 @@ %%% Author : Igor Goryachev %%% Purpose : Add avatar hash in presence on behalf of client (XEP-0153) %%% Created : 9 Mar 2007 by Igor Goryachev +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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_vcard_xupdate). - -behaviour(gen_mod). %% gen_mod callbacks --export([start/2, stop/1]). +-export([start/2, stop/1, reload/3]). -%% hooks --export([update_presence/3, vcard_set/3, export/1, import/1, import/3]). +-export([update_presence/1, vcard_set/1, remove_user/2, mod_doc/0, + user_send_packet/1, mod_opt_type/1, mod_options/1, depends/2]). +%% API +-export([compute_hash/1]). --include("ejabberd.hrl"). -include("logger.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). +-include("translate.hrl"). --include("jlib.hrl"). - --record(vcard_xupdate, {us = {<<>>, <<>>} :: {binary(), binary()}, - hash = <<>> :: binary()}). +-define(VCARD_XUPDATE_CACHE, vcard_xupdate_cache). %%==================================================================== %% gen_mod callbacks %%==================================================================== start(Host, Opts) -> - case gen_mod:db_type(Opts) of - mnesia -> - mnesia:create_table(vcard_xupdate, - [{disc_copies, [node()]}, - {attributes, - record_info(fields, vcard_xupdate)}]), - update_table(); - _ -> ok - end, - ejabberd_hooks:add(c2s_update_presence, Host, ?MODULE, - update_presence, 100), - ejabberd_hooks:add(vcard_set, Host, ?MODULE, vcard_set, - 100), + init_cache(Host, Opts), + {ok, [{hook, c2s_self_presence, update_presence, 100}, + {hook, user_send_packet, user_send_packet, 50}, + {hook, vcard_iq_set, vcard_set, 90}, + {hook, remove_user, remove_user, 50}]}. + +stop(_Host) -> ok. -stop(Host) -> - ejabberd_hooks:delete(c2s_update_presence, Host, - ?MODULE, update_presence, 100), - ejabberd_hooks:delete(vcard_set, Host, ?MODULE, - vcard_set, 100), - ok. +reload(Host, NewOpts, _OldOpts) -> + init_cache(Host, NewOpts). + +depends(_Host, _Opts) -> + [{mod_vcard, hard}]. %%==================================================================== %% Hooks %%==================================================================== - -update_presence(#xmlel{name = <<"presence">>, attrs = Attrs} = Packet, - User, Host) -> - case xml:get_attr_s(<<"type">>, Attrs) of - <<>> -> presence_with_xupdate(Packet, User, Host); - _ -> Packet +-spec update_presence({presence(), ejabberd_c2s:state()}) + -> {presence(), ejabberd_c2s:state()}. +update_presence({#presence{type = available} = Pres, + #{jid := #jid{luser = LUser, lserver = LServer}} = State}) -> + case xmpp:get_subtag(Pres, #vcard_xupdate{}) of + #vcard_xupdate{hash = <<>>} -> + %% XEP-0398 forbids overwriting vcard:x:update + %% tags with empty element + {Pres, State}; + _ -> + Pres1 = case get_xupdate(LUser, LServer) of + undefined -> xmpp:remove_subtag(Pres, #vcard_xupdate{}); + XUpdate -> xmpp:set_subtag(Pres, XUpdate) + end, + {Pres1, State} end; -update_presence(Packet, _User, _Host) -> Packet. +update_presence(Acc) -> + Acc. -vcard_set(LUser, LServer, VCARD) -> - US = {LUser, LServer}, - case xml:get_path_s(VCARD, - [{elem, <<"PHOTO">>}, {elem, <<"BINVAL">>}, cdata]) - of - <<>> -> remove_xupdate(LUser, LServer); - BinVal -> - add_xupdate(LUser, LServer, - p1_sha:sha(jlib:decode_base64(BinVal))) - end, - ejabberd_sm:force_update_presence(US). +-spec user_send_packet({presence(), ejabberd_c2s:state()}) + -> {presence(), ejabberd_c2s:state()}. +user_send_packet({#presence{type = available, + to = #jid{luser = U, lserver = S, + lresource = <<"">>}}, + #{jid := #jid{luser = U, lserver = S}}} = Acc) -> + %% This is processed by update_presence/2 explicitly, we don't + %% want to call this multiple times for performance reasons + Acc; +user_send_packet(Acc) -> + update_presence(Acc). + +-spec vcard_set(iq()) -> iq(). +vcard_set(#iq{from = #jid{luser = LUser, lserver = LServer}} = IQ) -> + ets_cache:delete(?VCARD_XUPDATE_CACHE, {LUser, LServer}, ejabberd_cluster:get_nodes()), + ejabberd_sm:reset_vcard_xupdate_resend_presence({LUser, LServer}), + IQ; +vcard_set(Acc) -> + Acc. + +-spec remove_user(binary(), binary()) -> ok. +remove_user(User, Server) -> + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + ets_cache:delete(?VCARD_XUPDATE_CACHE, {LUser, LServer}, ejabberd_cluster:get_nodes()). %%==================================================================== %% Storage %%==================================================================== - -add_xupdate(LUser, LServer, Hash) -> - add_xupdate(LUser, LServer, Hash, - gen_mod:db_type(LServer, ?MODULE)). - -add_xupdate(LUser, LServer, Hash, mnesia) -> - F = fun () -> - mnesia:write(#vcard_xupdate{us = {LUser, LServer}, - hash = Hash}) - end, - mnesia:transaction(F); -add_xupdate(LUser, LServer, Hash, riak) -> - {atomic, ejabberd_riak:put(#vcard_xupdate{us = {LUser, LServer}, - hash = Hash}, - vcard_xupdate_schema())}; -add_xupdate(LUser, LServer, Hash, odbc) -> - Username = ejabberd_odbc:escape(LUser), - SHash = ejabberd_odbc:escape(Hash), - F = fun () -> - odbc_queries:update_t(<<"vcard_xupdate">>, - [<<"username">>, <<"hash">>], - [Username, SHash], - [<<"username='">>, Username, <<"'">>]) - end, - ejabberd_odbc:sql_transaction(LServer, F). - +-spec get_xupdate(binary(), binary()) -> vcard_xupdate() | undefined. get_xupdate(LUser, LServer) -> - get_xupdate(LUser, LServer, - gen_mod:db_type(LServer, ?MODULE)). - -get_xupdate(LUser, LServer, mnesia) -> - case mnesia:dirty_read(vcard_xupdate, {LUser, LServer}) - of - [#vcard_xupdate{hash = Hash}] -> Hash; - _ -> undefined - end; -get_xupdate(LUser, LServer, riak) -> - case ejabberd_riak:get(vcard_xupdate, vcard_xupdate_schema(), - {LUser, LServer}) of - {ok, #vcard_xupdate{hash = Hash}} -> Hash; - _ -> undefined - end; -get_xupdate(LUser, LServer, odbc) -> - Username = ejabberd_odbc:escape(LUser), - case ejabberd_odbc:sql_query(LServer, - [<<"select hash from vcard_xupdate where " - "username='">>, - Username, <<"';">>]) - of - {selected, [<<"hash">>], [[Hash]]} -> Hash; - _ -> undefined + Result = case use_cache(LServer) of + true -> + ets_cache:lookup( + ?VCARD_XUPDATE_CACHE, {LUser, LServer}, + fun() -> db_get_xupdate(LUser, LServer) end); + false -> + db_get_xupdate(LUser, LServer) + end, + case Result of + {ok, external} -> undefined; + {ok, Hash} -> #vcard_xupdate{hash = Hash}; + error -> #vcard_xupdate{} end. -remove_xupdate(LUser, LServer) -> - remove_xupdate(LUser, LServer, - gen_mod:db_type(LServer, ?MODULE)). - -remove_xupdate(LUser, LServer, mnesia) -> - F = fun () -> - mnesia:delete({vcard_xupdate, {LUser, LServer}}) - end, - mnesia:transaction(F); -remove_xupdate(LUser, LServer, riak) -> - {atomic, ejabberd_riak:delete(vcard_xupdate, {LUser, LServer})}; -remove_xupdate(LUser, LServer, odbc) -> - Username = ejabberd_odbc:escape(LUser), - F = fun () -> - ejabberd_odbc:sql_query_t([<<"delete from vcard_xupdate where username='">>, - Username, <<"';">>]) - end, - ejabberd_odbc:sql_transaction(LServer, F). - -%%%---------------------------------------------------------------------- -%%% Presence stanza rebuilding -%%%---------------------------------------------------------------------- - -presence_with_xupdate(#xmlel{name = <<"presence">>, - attrs = Attrs, children = Els}, - User, Host) -> - XPhotoEl = build_xphotoel(User, Host), - Els2 = presence_with_xupdate2(Els, [], XPhotoEl), - #xmlel{name = <<"presence">>, attrs = Attrs, - children = Els2}. - -presence_with_xupdate2([], Els2, XPhotoEl) -> - lists:reverse([XPhotoEl | Els2]); -%% This clause assumes that the x element contains only the XMLNS attribute: -presence_with_xupdate2([#xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_VCARD_UPDATE}]} - | Els], - Els2, XPhotoEl) -> - presence_with_xupdate2(Els, Els2, XPhotoEl); -presence_with_xupdate2([El | Els], Els2, XPhotoEl) -> - presence_with_xupdate2(Els, [El | Els2], XPhotoEl). - -build_xphotoel(User, Host) -> - Hash = get_xupdate(User, Host), - PhotoSubEls = case Hash of - Hash when is_binary(Hash) -> [{xmlcdata, Hash}]; - _ -> [] - end, - PhotoEl = [#xmlel{name = <<"photo">>, attrs = [], - children = PhotoSubEls}], - #xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_VCARD_UPDATE}], - children = PhotoEl}. - -vcard_xupdate_schema() -> - {record_info(fields, vcard_xupdate), #vcard_xupdate{}}. - -update_table() -> - Fields = record_info(fields, vcard_xupdate), - case mnesia:table_info(vcard_xupdate, attributes) of - Fields -> - ejabberd_config:convert_table_to_binary( - vcard_xupdate, Fields, set, - fun(#vcard_xupdate{us = {U, _}}) -> U end, - fun(#vcard_xupdate{us = {U, S}, hash = Hash} = R) -> - R#vcard_xupdate{us = {iolist_to_binary(U), - iolist_to_binary(S)}, - hash = iolist_to_binary(Hash)} - end); - _ -> - ?INFO_MSG("Recreating vcard_xupdate table", []), - mnesia:transform_table(vcard_xupdate, ignore, Fields) +-spec db_get_xupdate(binary(), binary()) -> {ok, binary() | external} | error. +db_get_xupdate(LUser, LServer) -> + case mod_vcard:get_vcard(LUser, LServer) of + [VCard] -> + {ok, compute_hash(VCard)}; + _ -> + error end. -export(_Server) -> - [{vcard_xupdate, - fun(Host, #vcard_xupdate{us = {LUser, LServer}, hash = Hash}) - when LServer == Host -> - Username = ejabberd_odbc:escape(LUser), - SHash = ejabberd_odbc:escape(Hash), - [[<<"delete from vcard_xupdate where username='">>, - Username, <<"';">>], - [<<"insert into vcard_xupdate(username, " - "hash) values ('">>, - Username, <<"', '">>, SHash, <<"');">>]]; - (_Host, _R) -> - [] - end}]. +-spec init_cache(binary(), gen_mod:opts()) -> ok. +init_cache(Host, Opts) -> + case use_cache(Host) of + true -> + CacheOpts = cache_opts(Opts), + ets_cache:new(?VCARD_XUPDATE_CACHE, CacheOpts); + false -> + ets_cache:delete(?VCARD_XUPDATE_CACHE) + end. -import(LServer) -> - [{<<"select username, hash from vcard_xupdate;">>, - fun([LUser, Hash]) -> - #vcard_xupdate{us = {LUser, LServer}, hash = Hash} - end}]. +-spec cache_opts(gen_mod:opts()) -> [proplists:property()]. +cache_opts(Opts) -> + MaxSize = mod_vcard_xupdate_opt:cache_size(Opts), + CacheMissed = mod_vcard_xupdate_opt:cache_missed(Opts), + LifeTime = mod_vcard_xupdate_opt:cache_life_time(Opts), + [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}]. -import(_LServer, mnesia, #vcard_xupdate{} = R) -> - mnesia:dirty_write(R); -import(_LServer, riak, #vcard_xupdate{} = R) -> - ejabberd_riak:put(R, vcard_xupdate_schema()); -import(_, _, _) -> - pass. +-spec use_cache(binary()) -> boolean(). +use_cache(Host) -> + mod_vcard_xupdate_opt:use_cache(Host). + +-spec compute_hash(xmlel()) -> binary() | external. +compute_hash(VCard) -> + case fxml:get_subtag(VCard, <<"PHOTO">>) of + false -> + <<>>; + Photo -> + try xmpp:decode(Photo, ?NS_VCARD, []) of + #vcard_photo{binval = <<_, _/binary>> = BinVal} -> + str:sha(BinVal); + #vcard_photo{extval = <<_, _/binary>>} -> + external; + _ -> + <<>> + catch _:{xmpp_codec, _} -> + <<>> + end + end. + +%%==================================================================== +%% Options +%%==================================================================== +mod_opt_type(use_cache) -> + econf:bool(); +mod_opt_type(cache_size) -> + econf:pos_int(infinity); +mod_opt_type(cache_missed) -> + econf:bool(); +mod_opt_type(cache_life_time) -> + econf:timeout(second, infinity). + +mod_options(Host) -> + [{use_cache, ejabberd_option:use_cache(Host)}, + {cache_size, ejabberd_option:cache_size(Host)}, + {cache_missed, ejabberd_option:cache_missed(Host)}, + {cache_life_time, ejabberd_option:cache_life_time(Host)}]. + +mod_doc() -> + #{desc => + [?T("The user's client can store an avatar in the " + "user vCard. The vCard-Based Avatars protocol " + "(https://xmpp.org/extensions/xep-0153.html[XEP-0153]) " + "provides a method for clients to inform the contacts " + "what is the avatar hash value. However, simple or small " + "clients may not implement that protocol."), "", + ?T("If this module is enabled, all the outgoing client presence " + "stanzas get automatically the avatar hash on behalf of the " + "client. So, the contacts receive the presence stanzas with " + "the 'Update Data' described in " + "https://xmpp.org/extensions/xep-0153.html[XEP-0153] as if the " + "client would had inserted it itself. If the client had already " + "included such element in the presence stanza, it is replaced " + "with the element generated by ejabberd."), "", + ?T("By enabling this module, each vCard modification produces " + "a hash recalculation, and each presence sent by a client " + "produces hash retrieval and a presence stanza rewrite. " + "For this reason, enabling this module will introduce a " + "computational overhead in servers with clients that change " + "frequently their presence. However, the overhead is significantly " + "reduced by the use of caching, so you probably don't want " + "to set 'use_cache' to 'false'."), "", + ?T("The module depends on _`mod_vcard`_."), "", + ?T("NOTE: Nowadays https://xmpp.org/extensions/xep-0153.html" + "[XEP-0153] is used mostly as \"read-only\", i.e. modern " + "clients don't publish their avatars inside vCards. Thus " + "in the majority of cases the module is only used along " + "with _`mod_avatar`_ for providing backward compatibility.")], + opts => + [{use_cache, + #{value => "true | false", + desc => + ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, + {cache_size, + #{value => "pos_integer() | infinity", + desc => + ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, + {cache_missed, + #{value => "true | false", + desc => + ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, + {cache_life_time, + #{value => "timeout()", + desc => + ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}]}. diff --git a/src/mod_vcard_xupdate_opt.erl b/src/mod_vcard_xupdate_opt.erl new file mode 100644 index 000000000..a51e6884f --- /dev/null +++ b/src/mod_vcard_xupdate_opt.erl @@ -0,0 +1,34 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_vcard_xupdate_opt). + +-export([cache_life_time/1]). +-export([cache_missed/1]). +-export([cache_size/1]). +-export([use_cache/1]). + +-spec cache_life_time(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +cache_life_time(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_life_time, Opts); +cache_life_time(Host) -> + gen_mod:get_module_opt(Host, mod_vcard_xupdate, cache_life_time). + +-spec cache_missed(gen_mod:opts() | global | binary()) -> boolean(). +cache_missed(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_missed, Opts); +cache_missed(Host) -> + gen_mod:get_module_opt(Host, mod_vcard_xupdate, cache_missed). + +-spec cache_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +cache_size(Opts) when is_map(Opts) -> + gen_mod:get_opt(cache_size, Opts); +cache_size(Host) -> + gen_mod:get_module_opt(Host, mod_vcard_xupdate, cache_size). + +-spec use_cache(gen_mod:opts() | global | binary()) -> boolean(). +use_cache(Opts) when is_map(Opts) -> + gen_mod:get_opt(use_cache, Opts); +use_cache(Host) -> + gen_mod:get_module_opt(Host, mod_vcard_xupdate, use_cache). + diff --git a/src/mod_version.erl b/src/mod_version.erl index e46262a2a..e10168c84 100644 --- a/src/mod_version.erl +++ b/src/mod_version.erl @@ -5,7 +5,7 @@ %%% Created : 18 Jan 2003 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -27,64 +27,70 @@ -author('alexey@process-one.net'). +-protocol({xep, 92, '1.1', '0.1.0', "complete", ""}). + -behaviour(gen_mod). --export([start/2, stop/1, process_local_iq/3]). +-export([start/2, stop/1, reload/3, process_local_iq/1, + mod_opt_type/1, mod_options/1, depends/2, mod_doc/0]). --include("ejabberd.hrl"). -include("logger.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). +-include("translate.hrl"). --include("jlib.hrl"). - -start(Host, Opts) -> - IQDisc = gen_mod:get_opt(iqdisc, Opts, fun gen_iq_handler:check_type/1, - one_queue), +start(Host, _Opts) -> gen_iq_handler:add_iq_handler(ejabberd_local, Host, - ?NS_VERSION, ?MODULE, process_local_iq, - IQDisc). + ?NS_VERSION, ?MODULE, process_local_iq). stop(Host) -> gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_VERSION). -process_local_iq(_From, To, - #iq{id = _ID, type = Type, xmlns = _XMLNS, - sub_el = SubEl} = - IQ) -> - case Type of - set -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; - get -> - Host = To#jid.lserver, - OS = case gen_mod:get_module_opt(Host, ?MODULE, show_os, - fun(B) when is_boolean(B) -> B end, - true) - of - true -> [get_os()]; - false -> [] - end, - IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"query">>, - attrs = [{<<"xmlns">>, ?NS_VERSION}], - children = - [#xmlel{name = <<"name">>, attrs = [], - children = - [{xmlcdata, <<"ejabberd">>}]}, - #xmlel{name = <<"version">>, attrs = [], - children = [{xmlcdata, ?VERSION}]}] - ++ OS}]} - end. +reload(_Host, _NewOpts, _OldOpts) -> + ok. + +process_local_iq(#iq{type = set, lang = Lang} = IQ) -> + Txt = ?T("Value 'set' of 'type' attribute is not allowed"), + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); +process_local_iq(#iq{type = get, to = To} = IQ) -> + Host = To#jid.lserver, + OS = case mod_version_opt:show_os(Host) of + true -> get_os(); + false -> undefined + end, + xmpp:make_iq_result(IQ, #version{name = <<"ejabberd">>, + ver = ejabberd_option:version(), + os = OS}). get_os() -> {Osfamily, Osname} = os:type(), OSType = list_to_binary([atom_to_list(Osfamily), $/, atom_to_list(Osname)]), OSVersion = case os:version() of {Major, Minor, Release} -> - iolist_to_binary(io_lib:format("~w.~w.~w", + (str:format("~w.~w.~w", [Major, Minor, Release])); VersionString -> VersionString end, - OS = <>, - #xmlel{name = <<"os">>, attrs = [], - children = [{xmlcdata, OS}]}. + <>. + +depends(_Host, _Opts) -> + []. + +mod_opt_type(show_os) -> + econf:bool(). + +mod_options(_Host) -> + [{show_os, true}]. + +mod_doc() -> + #{desc => + ?T("This module implements " + "https://xmpp.org/extensions/xep-0092.html" + "[XEP-0092: Software Version]. Consequently, " + "it answers ejabberd's version when queried."), + opts => + [{show_os, + #{value => "true | false", + desc => + ?T("Should the operating system be revealed or not. " + "The default value is 'true'.")}}]}. diff --git a/src/mod_version_opt.erl b/src/mod_version_opt.erl new file mode 100644 index 000000000..78d6231fb --- /dev/null +++ b/src/mod_version_opt.erl @@ -0,0 +1,13 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_version_opt). + +-export([show_os/1]). + +-spec show_os(gen_mod:opts() | global | binary()) -> boolean(). +show_os(Opts) when is_map(Opts) -> + gen_mod:get_opt(show_os, Opts); +show_os(Host) -> + gen_mod:get_module_opt(Host, mod_version, show_os). + diff --git a/src/mqtt_codec.erl b/src/mqtt_codec.erl new file mode 100644 index 000000000..1ef474dd5 --- /dev/null +++ b/src/mqtt_codec.erl @@ -0,0 +1,1402 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov +%%% @copyright (C) 2002-2025 ProcessOne, SARL. All Rights Reserved. +%%% +%%% Licensed under the Apache License, Version 2.0 (the "License"); +%%% you may not use this file except in compliance with the License. +%%% You may obtain a copy of the License at +%%% +%%% http://www.apache.org/licenses/LICENSE-2.0 +%%% +%%% Unless required by applicable law or agreed to in writing, software +%%% distributed under the License is distributed on an "AS IS" BASIS, +%%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%%% See the License for the specific language governing permissions and +%%% limitations under the License. +%%% +%%%------------------------------------------------------------------- +-module(mqtt_codec). + +%% API +-export([new/1, new/2, renew/1, decode/2, encode/2]). +-export([pp/1, pp/2, format_error/1, format_reason_code/1]). +-export([error_reason_code/1, is_error_code/1]). +%% Validators +-export([topic/1, topic_filter/1, qos/1, utf8/1]). +-export([decode_varint/1]). + +-include("mqtt.hrl"). + +-define(MAX_UINT16, 65535). +-define(MAX_UINT32, 4294967295). +-define(MAX_VARINT, 268435456). + +-record(codec_state, {version :: undefined | mqtt_version(), + type :: undefined | non_neg_integer(), + flags :: undefined | non_neg_integer(), + size :: undefined | non_neg_integer(), + max_size :: pos_integer() | infinity, + buf = <<>> :: binary()}). + +-type error_reason() :: bad_varint | + {payload_too_big, integer()} | + {bad_packet_type, char()} | + {bad_packet, atom()} | + {unexpected_packet, atom()} | + {bad_reason_code, atom(), char()} | + {bad_properties, atom()} | + {bad_property, atom(), atom()} | + {duplicated_property, atom(), atom()} | + bad_will_topic_or_message | + bad_connect_username_or_password | + bad_publish_id_or_payload | + {bad_topic_filters, atom()} | + {bad_qos, char()} | + bad_topic | bad_topic_filter | bad_utf8_string | + {unsupported_protocol_name, binary(), binary()} | + {unsupported_protocol_version, char(), iodata()} | + {{bad_flag, atom()}, char(), term()} | + {{bad_flags, atom()}, char(), char()}. + +-opaque state() :: #codec_state{}. +-export_type([state/0, error_reason/0]). + +%%%=================================================================== +%%% API +%%%=================================================================== +-spec new(pos_integer() | infinity) -> state(). +new(MaxSize) -> + new(MaxSize, undefined). + +-spec new(pos_integer() | infinity, undefined | mqtt_version()) -> state(). +new(MaxSize, Version) -> + #codec_state{max_size = MaxSize, version = Version}. + +-spec renew(state()) -> state(). +renew(#codec_state{version = Version, max_size = MaxSize}) -> + #codec_state{version = Version, max_size = MaxSize}. + +-spec decode(state(), binary()) -> {ok, mqtt_packet(), state()} | + {more, state()} | + {error, error_reason()}. +decode(#codec_state{size = undefined, buf = Buf} = State, Data) -> + Buf1 = <>, + case Buf1 of + <> -> + try + case decode_varint(Data1) of + {Len, _} when Len >= State#codec_state.max_size -> + err({payload_too_big, State#codec_state.max_size}); + {Len, Data2} when size(Data2) >= Len -> + <> = Data2, + Version = State#codec_state.version, + Pkt = decode_pkt(Version, Type, Flags, Payload), + State1 = case Pkt of + #connect{proto_level = V} -> + State#codec_state{version = V}; + _ -> + State + end, + {ok, Pkt, State1#codec_state{buf = Data3}}; + {Len, Data2} -> + {more, State#codec_state{type = Type, + flags = Flags, + size = Len, + buf = Data2}}; + more -> + {more, State#codec_state{buf = Buf1}} + end + catch _:{?MODULE, Why} -> + {error, Why} + end; + <<>> -> + {more, State} + end; +decode(#codec_state{size = Len, buf = Buf, + version = Version, + type = Type, flags = Flags} = State, Data) -> + Buf1 = <>, + if size(Buf1) >= Len -> + <> = Buf1, + try + Pkt = decode_pkt(Version, Type, Flags, Payload), + State1 = case Pkt of + #connect{proto_level = V} -> + State#codec_state{version = V}; + _ -> + State + end, + {ok, Pkt, State1#codec_state{type = undefined, + flags = undefined, + size = undefined, + buf = Data1}} + catch _:{?MODULE, Why} -> + {error, Why} + end; + true -> + {more, State#codec_state{buf = Buf1}} + end. + +-spec encode(mqtt_version(), mqtt_packet()) -> binary(). +encode(Version, Pkt) -> + case Pkt of + #connect{proto_level = Version} -> encode_connect(Pkt); + #connack{} -> encode_connack(Version, Pkt); + #publish{} -> encode_publish(Version, Pkt); + #puback{} -> encode_puback(Version, Pkt); + #pubrec{} -> encode_pubrec(Version, Pkt); + #pubrel{} -> encode_pubrel(Version, Pkt); + #pubcomp{} -> encode_pubcomp(Version, Pkt); + #subscribe{} -> encode_subscribe(Version, Pkt); + #suback{} -> encode_suback(Version, Pkt); + #unsubscribe{} -> encode_unsubscribe(Version, Pkt); + #unsuback{} -> encode_unsuback(Version, Pkt); + #pingreq{} -> encode_pingreq(); + #pingresp{} -> encode_pingresp(); + #disconnect{} -> encode_disconnect(Version, Pkt); + #auth{} -> encode_auth(Pkt) + end. + +-spec pp(any()) -> iolist(). +pp(Term) -> + io_lib_pretty:print(Term, fun pp/2). + +-spec format_error(error_reason()) -> string(). +format_error({payload_too_big, Max}) -> + format("Payload exceeds ~B bytes", [Max]); +format_error(bad_varint) -> + "Variable Integer is out of boundaries"; +format_error({bad_packet_type, Type}) -> + format("Unexpected packet type: ~B", [Type]); +format_error({bad_packet, Name}) -> + format("Malformed ~ts packet", [string:to_upper(atom_to_list(Name))]); +format_error({unexpected_packet, Name}) -> + format("Unexpected ~ts packet", [string:to_upper(atom_to_list(Name))]); +format_error({bad_reason_code, Name, Code}) -> + format("Unexpected reason code in ~ts code: ~B", + [string:to_upper(atom_to_list(Name)), Code]); +format_error({bad_properties, Name}) -> + format("Malformed properties of ~ts packet", + [string:to_upper(atom_to_list(Name))]); +format_error({bad_property, Pkt, Prop}) -> + format("Malformed property ~ts of ~ts packet", + [Prop, string:to_upper(atom_to_list(Pkt))]); +format_error({duplicated_property, Pkt, Prop}) -> + format("Property ~ts is included more than once into ~ts packet", + [Prop, string:to_upper(atom_to_list(Pkt))]); +format_error(bad_will_topic_or_message) -> + "Malformed Will Topic or Will Message"; +format_error(bad_connect_username_or_password) -> + "Malformed username or password of CONNECT packet"; +format_error(bad_publish_id_or_payload) -> + "Malformed id or payload of PUBLISH packet"; +format_error({bad_topic_filters, Name}) -> + format("Malformed topic filters of ~ts packet", + [string:to_upper(atom_to_list(Name))]); +format_error({bad_qos, Q}) -> + format_got_expected("Malformed QoS value", Q, "0, 1 or 2"); +format_error(bad_topic) -> + "Malformed topic"; +format_error(bad_topic_filter) -> + "Malformed topic filter"; +format_error(bad_utf8_string) -> + "Malformed UTF-8 string"; +format_error({unsupported_protocol_name, Got, Expected}) -> + format_got_expected("Unsupported protocol name", Got, Expected); +format_error({unsupported_protocol_version, Got, Expected}) -> + format_got_expected("Unsupported protocol version", Got, Expected); +format_error({{bad_flag, Name}, Got, Expected}) -> + Txt = "Unexpected " ++ atom_to_list(Name) ++ " flag", + format_got_expected(Txt, Got, Expected); +format_error({{bad_flags, Name}, Got, Expected}) -> + Txt = "Unexpected " ++ string:to_upper(atom_to_list(Name)) ++ " flags", + format_got_expected(Txt, Got, Expected); +format_error(Reason) -> + format("Unexpected error: ~w", [Reason]). + +-spec error_reason_code(error_reason()) -> reason_code(). +error_reason_code({unsupported_protocol_name, _, _}) -> + 'unsupported-protocol-version'; +error_reason_code({unsupported_protocol_version, _, _}) -> + 'unsupported-protocol-version'; +error_reason_code({payload_too_big, _}) -> 'packet-too-large'; +error_reason_code({unexpected_packet, _}) -> 'protocol-error'; +error_reason_code(_) -> 'malformed-packet'. + +-spec format_reason_code(reason_code()) -> string(). +format_reason_code('success') -> "Success"; +format_reason_code('normal-disconnection') -> "Normal disconnection"; +format_reason_code('granted-qos-0') -> "Granted QoS 0"; +format_reason_code('granted-qos-1') -> "Granted QoS 1"; +format_reason_code('granted-qos-2') -> "Granted QoS 2"; +format_reason_code('no-matching-subscribers') -> "No matching subscribers"; +format_reason_code('no-subscription-existed') -> "No subscription existed"; +format_reason_code('continue-authentication') -> "Continue authentication"; +format_reason_code('re-authenticate') -> "Re-authenticate"; +format_reason_code('unspecified-error') -> "Unspecified error"; +format_reason_code('malformed-packet') -> "Malformed Packet"; +format_reason_code('protocol-error') -> "Protocol Error"; +format_reason_code('bad-user-name-or-password') -> "Bad User Name or Password"; +format_reason_code('not-authorized') -> "Not authorized"; +format_reason_code('server-unavailable') -> "Server unavailable"; +format_reason_code('server-busy') -> "Server busy"; +format_reason_code('banned') -> "Banned"; +format_reason_code('server-shutting-down') -> "Server shutting down"; +format_reason_code('bad-authentication-method') -> "Bad authentication method"; +format_reason_code('keep-alive-timeout') -> "Keep Alive timeout"; +format_reason_code('session-taken-over') -> "Session taken over"; +format_reason_code('topic-filter-invalid') -> "Topic Filter invalid"; +format_reason_code('topic-name-invalid') -> "Topic Name invalid"; +format_reason_code('packet-identifier-in-use') -> "Packet Identifier in use"; +format_reason_code('receive-maximum-exceeded') -> "Receive Maximum exceeded"; +format_reason_code('topic-alias-invalid') -> "Topic Alias invalid"; +format_reason_code('packet-too-large') -> "Packet too large"; +format_reason_code('message-rate-too-high') -> "Message rate too high"; +format_reason_code('quota-exceeded') -> "Quota exceeded"; +format_reason_code('administrative-action') -> "Administrative action"; +format_reason_code('payload-format-invalid') -> "Payload format invalid"; +format_reason_code('retain-not-supported') -> "Retain not supported"; +format_reason_code('qos-not-supported') -> "QoS not supported"; +format_reason_code('use-another-server') -> "Use another server"; +format_reason_code('server-moved') -> "Server moved"; +format_reason_code('connection-rate-exceeded') -> "Connection rate exceeded"; +format_reason_code('maximum-connect-time') -> "Maximum connect time"; +format_reason_code('unsupported-protocol-version') -> + "Unsupported Protocol Version"; +format_reason_code('client-identifier-not-valid') -> + "Client Identifier not valid"; +format_reason_code('packet-identifier-not-found') -> + "Packet Identifier not found"; +format_reason_code('disconnect-with-will-message') -> + "Disconnect with Will Message"; +format_reason_code('implementation-specific-error') -> + "Implementation specific error"; +format_reason_code('shared-subscriptions-not-supported') -> + "Shared Subscriptions not supported"; +format_reason_code('subscription-identifiers-not-supported') -> + "Subscription Identifiers not supported"; +format_reason_code('wildcard-subscriptions-not-supported') -> + "Wildcard Subscriptions not supported"; +format_reason_code(Code) -> + format("Unexpected error: ~w", [Code]). + +-spec is_error_code(char() | reason_code()) -> boolean(). +is_error_code('success') -> false; +is_error_code('normal-disconnection') -> false; +is_error_code('granted-qos-0') -> false; +is_error_code('granted-qos-1') -> false; +is_error_code('granted-qos-2') -> false; +is_error_code('disconnect-with-will-message') -> false; +is_error_code('no-matching-subscribers') -> false; +is_error_code('no-subscription-existed') -> false; +is_error_code('continue-authentication') -> false; +is_error_code('re-authenticate') -> false; +is_error_code(Code) when is_integer(Code) -> Code >= 128; +is_error_code(_) -> true. + +%%%=================================================================== +%%% Decoder +%%%=================================================================== +-spec decode_varint(binary()) -> {non_neg_integer(), binary()} | more. +decode_varint(Data) -> + decode_varint(Data, 0, 1). + +-spec decode_varint(binary(), non_neg_integer(), pos_integer()) -> + {non_neg_integer(), binary()} | more. +decode_varint(<>, Val, Mult) -> + NewVal = Val + (C band 127) * Mult, + NewMult = Mult*128, + if NewMult > ?MAX_VARINT -> + err(bad_varint); + (C band 128) == 0 -> + {NewVal, Data}; + true -> + decode_varint(Data, NewVal, NewMult) + end; +decode_varint(_, _, _) -> + more. + +-spec decode_pkt(mqtt_version() | undefined, + non_neg_integer(), non_neg_integer(), binary()) -> mqtt_packet(). +decode_pkt(undefined, 1, Flags, Data) -> + decode_connect(Flags, Data); +decode_pkt(Version, Type, Flags, Data) when Version /= undefined, Type>1 -> + case Type of + 2 -> decode_connack(Version, Flags, Data); + 3 -> decode_publish(Version, Flags, Data); + 4 -> decode_puback(Version, Flags, Data); + 5 -> decode_pubrec(Version, Flags, Data); + 6 -> decode_pubrel(Version, Flags, Data); + 7 -> decode_pubcomp(Version, Flags, Data); + 8 -> decode_subscribe(Version, Flags, Data); + 9 -> decode_suback(Version, Flags, Data); + 10 -> decode_unsubscribe(Version, Flags, Data); + 11 -> decode_unsuback(Version, Flags, Data); + 12 -> decode_pingreq(Flags, Data); + 13 -> decode_pingresp(Flags, Data); + 14 -> decode_disconnect(Version, Flags, Data); + 15 when Version == ?MQTT_VERSION_5 -> decode_auth(Flags, Data); + _ -> err({bad_packet_type, Type}) + end; +decode_pkt(_, Type, _, _) -> + err({unexpected_packet, decode_packet_type(Type)}). + +-spec decode_connect(non_neg_integer(), binary()) -> connect(). +decode_connect(Flags, <>) -> + assert(Proto, <<"MQTT">>, unsupported_protocol_name), + if ProtoLevel == ?MQTT_VERSION_4; ProtoLevel == ?MQTT_VERSION_5 -> + decode_connect(ProtoLevel, Flags, Data); + true -> + err({unsupported_protocol_version, ProtoLevel, "4 or 5"}) + end; +decode_connect(_, _) -> + err({bad_packet, connect}). + +-spec decode_connect(mqtt_version(), non_neg_integer(), binary()) -> connect(). +decode_connect(Version, Flags, + <>) -> + assert(Flags, 0, {bad_flags, connect}), + assert(Reserved, 0, {bad_flag, reserved}), + {Props, Data1} = case Version of + ?MQTT_VERSION_5 -> decode_props(connect, Data); + ?MQTT_VERSION_4 -> {#{}, Data} + end, + case Data1 of + <> -> + {Will, WillProps, Data3} = + decode_will(Version, WillFlag, WillRetain, WillQoS, Data2), + {Username, Password} = decode_user_pass(UserFlag, PassFlag, Data3), + #connect{proto_level = Version, + will = Will, + will_properties = WillProps, + properties = Props, + clean_start = dec_bool(CleanStart), + keep_alive = KeepAlive, + client_id = utf8(ClientID), + username = utf8(Username), + password = Password}; + _ -> + err({bad_packet, connect}) + end; +decode_connect(_, _, _) -> + err({bad_packet, connect}). + +-spec decode_connack(mqtt_version(), non_neg_integer(), binary()) -> connack(). +decode_connack(Version, Flags, <<0:7, SessionPresent:1, Data/binary>>) -> + assert(Flags, 0, {bad_flags, connack}), + {Code, PropMap} = decode_code_with_props(Version, connack, Data), + #connack{session_present = dec_bool(SessionPresent), + code = Code, properties = PropMap}; +decode_connack(_, _, _) -> + err({bad_packet, connack}). + +-spec decode_publish(mqtt_version(), non_neg_integer(), binary()) -> publish(). +decode_publish(Version, Flags, <>) -> + Retain = Flags band 1, + QoS = qos((Flags bsr 1) band 3), + DUP = Flags band 8, + {ID, Props, Payload} = decode_id_props_payload(Version, QoS, Data), + #publish{dup = dec_bool(DUP), + qos = QoS, + retain = dec_bool(Retain), + topic = topic(Topic, Props), + id = ID, + properties = Props, + payload = Payload}; +decode_publish(_, _, _) -> + err({bad_packet, publish}). + +-spec decode_puback(mqtt_version(), non_neg_integer(), binary()) -> puback(). +decode_puback(Version, Flags, <>) when ID>0 -> + assert(Flags, 0, {bad_flags, puback}), + {Code, PropMap} = decode_code_with_props(Version, puback, Data), + #puback{id = ID, code = Code, properties = PropMap}; +decode_puback(_, _, _) -> + err({bad_packet, puback}). + +-spec decode_pubrec(mqtt_version(), non_neg_integer(), binary()) -> pubrec(). +decode_pubrec(Version, Flags, <>) when ID>0 -> + assert(Flags, 0, {bad_flags, pubrec}), + {Code, PropMap} = decode_code_with_props(Version, pubrec, Data), + #pubrec{id = ID, code = Code, properties = PropMap}; +decode_pubrec(_, _, _) -> + err({bad_packet, pubrec}). + +-spec decode_pubrel(mqtt_version(), non_neg_integer(), binary()) -> pubrel(). +decode_pubrel(Version, Flags, <>) when ID>0 -> + assert(Flags, 2, {bad_flags, pubrel}), + {Code, PropMap} = decode_code_with_props(Version, pubrel, Data), + #pubrel{id = ID, code = Code, properties = PropMap}; +decode_pubrel(_, _, _) -> + err({bad_packet, pubrel}). + +-spec decode_pubcomp(mqtt_version(), non_neg_integer(), binary()) -> pubcomp(). +decode_pubcomp(Version, Flags, <>) when ID>0 -> + assert(Flags, 0, {bad_flags, pubcomp}), + {Code, PropMap} = decode_code_with_props(Version, pubcomp, Data), + #pubcomp{id = ID, code = Code, properties = PropMap}; +decode_pubcomp(_, _, _) -> + err({bad_packet, pubcomp}). + +-spec decode_subscribe(mqtt_version(), non_neg_integer(), binary()) -> subscribe(). +decode_subscribe(Version, Flags, <>) when ID>0 -> + assert(Flags, 2, {bad_flags, subscribe}), + case Version of + ?MQTT_VERSION_4 -> + Filters = decode_subscribe_filters(Data), + #subscribe{id = ID, filters = Filters}; + ?MQTT_VERSION_5 -> + {Props, Payload} = decode_props(subscribe, Data), + Filters = decode_subscribe_filters(Payload), + #subscribe{id = ID, filters = Filters, properties = Props} + end; +decode_subscribe(_, _, _) -> + err({bad_packet, subscribe}). + +-spec decode_suback(mqtt_version(), non_neg_integer(), binary()) -> suback(). +decode_suback(Version, Flags, <>) when ID>0 -> + assert(Flags, 0, {bad_flags, suback}), + case Version of + ?MQTT_VERSION_4 -> + #suback{id = ID, + codes = decode_suback_codes(Data)}; + ?MQTT_VERSION_5 -> + {PropMap, Tail} = decode_props(suback, Data), + #suback{id = ID, + codes = decode_suback_codes(Tail), + properties = PropMap} + end; +decode_suback(_, _, _) -> + err({bad_packet, suback}). + +-spec decode_unsubscribe(mqtt_version(), non_neg_integer(), binary()) -> unsubscribe(). +decode_unsubscribe(Version, Flags, <>) when ID>0 -> + assert(Flags, 2, {bad_flags, unsubscribe}), + case Version of + ?MQTT_VERSION_4 -> + Filters = decode_unsubscribe_filters(Data), + #unsubscribe{id = ID, filters = Filters}; + ?MQTT_VERSION_5 -> + {Props, Payload} = decode_props(unsubscribe, Data), + Filters = decode_unsubscribe_filters(Payload), + #unsubscribe{id = ID, filters = Filters, properties = Props} + end; +decode_unsubscribe(_, _, _) -> + err({bad_packet, unsubscribe}). + +-spec decode_unsuback(mqtt_version(), non_neg_integer(), binary()) -> unsuback(). +decode_unsuback(Version, Flags, <>) when ID>0 -> + assert(Flags, 0, {bad_flags, unsuback}), + case Version of + ?MQTT_VERSION_4 -> + #unsuback{id = ID}; + ?MQTT_VERSION_5 -> + {PropMap, Tail} = decode_props(unsuback, Data), + #unsuback{id = ID, + codes = decode_unsuback_codes(Tail), + properties = PropMap} + end; +decode_unsuback(_, _, _) -> + err({bad_packet, unsuback}). + +-spec decode_pingreq(non_neg_integer(), binary()) -> pingreq(). +decode_pingreq(Flags, <<>>) -> + assert(Flags, 0, {bad_flags, pingreq}), + #pingreq{}; +decode_pingreq(_, _) -> + err({bad_packet, pingreq}). + +-spec decode_pingresp(non_neg_integer(), binary()) -> pingresp(). +decode_pingresp(Flags, <<>>) -> + assert(Flags, 0, {bad_flags, pingresp}), + #pingresp{}; +decode_pingresp(_, _) -> + err({bad_packet, pingresp}). + +-spec decode_disconnect(mqtt_version(), non_neg_integer(), binary()) -> disconnect(). +decode_disconnect(Version, Flags, Payload) -> + assert(Flags, 0, {bad_flags, disconnect}), + {Code, PropMap} = decode_code_with_props(Version, disconnect, Payload), + #disconnect{code = Code, properties = PropMap}. + +-spec decode_auth(non_neg_integer(), binary()) -> auth(). +decode_auth(Flags, Payload) -> + assert(Flags, 0, {bad_flags, auth}), + {Code, PropMap} = decode_code_with_props(?MQTT_VERSION_5, auth, Payload), + #auth{code = Code, properties = PropMap}. + +-spec decode_packet_type(char()) -> atom(). +decode_packet_type(1) -> connect; +decode_packet_type(2) -> connack; +decode_packet_type(3) -> publish; +decode_packet_type(4) -> puback; +decode_packet_type(5) -> pubrec; +decode_packet_type(6) -> pubrel; +decode_packet_type(7) -> pubcomp; +decode_packet_type(8) -> subscribe; +decode_packet_type(9) -> suback; +decode_packet_type(10) -> unsubscribe; +decode_packet_type(11) -> unsuback; +decode_packet_type(12) -> pingreq; +decode_packet_type(13) -> pingresp; +decode_packet_type(14) -> disconnect; +decode_packet_type(15) -> auth; +decode_packet_type(T) -> err({bad_packet_type, T}). + +-spec decode_will(mqtt_version(), 0|1, 0|1, qos(), binary()) -> + {undefined | publish(), properties(), binary()}. +decode_will(_, 0, WillRetain, WillQoS, Data) -> + assert(WillRetain, 0, {bad_flag, will_retain}), + assert(WillQoS, 0, {bad_flag, will_qos}), + {undefined, #{}, Data}; +decode_will(Version, 1, WillRetain, WillQoS, Data) -> + {Props, Data1} = case Version of + ?MQTT_VERSION_5 -> decode_props(connect, Data); + ?MQTT_VERSION_4 -> {#{}, Data} + end, + case Data1 of + <> -> + {#publish{retain = dec_bool(WillRetain), + qos = qos(WillQoS), + topic = topic(Topic), + payload = Message}, + Props, Data2}; + _ -> + err(bad_will_topic_or_message) + end. + +-spec decode_user_pass(non_neg_integer(), non_neg_integer(), + binary()) -> {binary(), binary()}. +decode_user_pass(1, 0, <>) -> + {utf8(User), <<>>}; +decode_user_pass(1, 1, <>) -> + {utf8(User), Pass}; +decode_user_pass(0, Flag, <<>>) -> + assert(Flag, 0, {bad_flag, password}), + {<<>>, <<>>}; +decode_user_pass(_, _, _) -> + err(bad_connect_username_or_password). + +-spec decode_id_props_payload(mqtt_version(), non_neg_integer(), binary()) -> + {undefined | non_neg_integer(), properties(), binary()}. +decode_id_props_payload(Version, 0, Data) -> + case Version of + ?MQTT_VERSION_4 -> + {undefined, #{}, Data}; + ?MQTT_VERSION_5 -> + {Props, Payload} = decode_props(publish, Data), + {undefined, Props, Payload} + end; +decode_id_props_payload(Version, _, <>) when ID>0 -> + case Version of + ?MQTT_VERSION_4 -> + {ID, #{}, Data}; + ?MQTT_VERSION_5 -> + {Props, Payload} = decode_props(publish, Data), + {ID, Props, Payload} + end; +decode_id_props_payload(_, _, _) -> + err(bad_publish_id_or_payload). + +-spec decode_subscribe_filters(binary()) -> [{binary(), sub_opts()}]. +decode_subscribe_filters(<>) -> + assert(Reserved, 0, {bad_flag, reserved}), + case RH of + 3 -> err({{bad_flag, retain_handling}, RH, "0, 1 or 2"}); + _ -> ok + end, + Opts = #sub_opts{qos = qos(QoS), + no_local = dec_bool(NL), + retain_as_published = dec_bool(RAP), + retain_handling = RH}, + [{topic_filter(Filter), Opts}|decode_subscribe_filters(Tail)]; +decode_subscribe_filters(<<>>) -> + []; +decode_subscribe_filters(_) -> + err({bad_topic_filters, subscribe}). + +-spec decode_unsubscribe_filters(binary()) -> [binary()]. +decode_unsubscribe_filters(<>) -> + [topic_filter(Filter)|decode_unsubscribe_filters(Tail)]; +decode_unsubscribe_filters(<<>>) -> + []; +decode_unsubscribe_filters(_) -> + err({bad_topic_filters, unsubscribe}). + +-spec decode_suback_codes(binary()) -> [reason_code()]. +decode_suback_codes(<>) -> + [decode_suback_code(Code)|decode_suback_codes(Data)]; +decode_suback_codes(<<>>) -> + []. + +-spec decode_unsuback_codes(binary()) -> [reason_code()]. +decode_unsuback_codes(<>) -> + [decode_unsuback_code(Code)|decode_unsuback_codes(Data)]; +decode_unsuback_codes(<<>>) -> + []. + +-spec decode_utf8_pair(binary()) -> {utf8_pair(), binary()}. +decode_utf8_pair(<>) -> + {{utf8(Name), utf8(Val)}, Tail}; +decode_utf8_pair(_) -> + err(bad_utf8_pair). + +-spec decode_props(atom(), binary()) -> {properties(), binary()}. +decode_props(Pkt, Data) -> + try + {Len, Data1} = decode_varint(Data), + <> = Data1, + {decode_props(Pkt, PData, #{}), Tail} + catch _:{badmatch, _} -> + err({bad_properties, Pkt}) + end. + +-spec decode_props(atom(), binary(), properties()) -> properties(). +decode_props(_, <<>>, Props) -> + Props; +decode_props(Pkt, Data, Props) -> + {Type, Payload} = decode_varint(Data), + {Name, Val, Tail} = decode_prop(Pkt, Type, Payload), + Props1 = maps:update_with( + Name, + fun(Vals) when is_list(Val) -> + Vals ++ Val; + (_) -> + err({duplicated_property, Pkt, Name}) + end, Val, Props), + decode_props(Pkt, Tail, Props1). + +-spec decode_prop(atom(), char(), binary()) -> {property(), term(), binary()}. +decode_prop(_, 18, <>) -> + {assigned_client_identifier, utf8(Data), Bin}; +decode_prop(_, 22, <>) -> + {authentication_data, Data, Bin}; +decode_prop(_, 21, <>) -> + {authentication_method, utf8(Data), Bin}; +decode_prop(_, 3, <>) -> + {content_type, utf8(Data), Bin}; +decode_prop(_, 9, <>) -> + {correlation_data, Data, Bin}; +decode_prop(_, 39, <>) when Size>0 -> + {maximum_packet_size, Size, Bin}; +decode_prop(Pkt, 36, <>) -> + {maximum_qos, + case QoS of + 0 -> 0; + 1 -> 1; + _ -> err({bad_property, Pkt, maximum_qos}) + end, Bin}; +decode_prop(_, 2, <>) -> + {message_expiry_interval, I, Bin}; +decode_prop(Pkt, 1, <>) -> + {payload_format_indicator, + case I of + 0 -> binary; + 1 -> utf8; + _ -> err({bad_property, Pkt, payload_format_indicator}) + end, Bin}; +decode_prop(_, 31, <>) -> + {reason_string, utf8(Data), Bin}; +decode_prop(_, 33, <>) when Max>0 -> + {receive_maximum, Max, Bin}; +decode_prop(Pkt, 23, Data) -> + decode_bool_prop(Pkt, request_problem_information, Data); +decode_prop(Pkt, 25, Data) -> + decode_bool_prop(Pkt, request_response_information, Data); +decode_prop(_, 26, <>) -> + {response_information, utf8(Data), Bin}; +decode_prop(_, 8, <>) -> + {response_topic, topic(Data), Bin}; +decode_prop(Pkt, 37, Data) -> + decode_bool_prop(Pkt, retain_available, Data); +decode_prop(_, 19, <>) -> + {server_keep_alive, Secs, Bin}; +decode_prop(_, 28, <>) -> + {server_reference, utf8(Data), Bin}; +decode_prop(_, 17, <>) -> + {session_expiry_interval, I, Bin}; +decode_prop(Pkt, 42, Data) -> + decode_bool_prop(Pkt, shared_subscription_available, Data); +decode_prop(Pkt, 11, Data) when Pkt == publish; Pkt == subscribe -> + case decode_varint(Data) of + {ID, Bin} when Pkt == publish -> + {subscription_identifier, [ID], Bin}; + {ID, Bin} when Pkt == subscribe -> + {subscription_identifier, ID, Bin}; + _ -> + err({bad_property, publish, subscription_identifier}) + end; +decode_prop(Pkt, 41, Data) -> + decode_bool_prop(Pkt, subscription_identifiers_available, Data); +decode_prop(_, 35, <>) when Alias>0 -> + {topic_alias, Alias, Bin}; +decode_prop(_, 34, <>) -> + {topic_alias_maximum, Max, Bin}; +decode_prop(_, 38, Data) -> + {Pair, Bin} = decode_utf8_pair(Data), + {user_property, [Pair], Bin}; +decode_prop(Pkt, 40, Data) -> + decode_bool_prop(Pkt, wildcard_subscription_available, Data); +decode_prop(_, 24, <>) -> + {will_delay_interval, I, Bin}; +decode_prop(Pkt, _, _) -> + err({bad_properties, Pkt}). + +decode_bool_prop(Pkt, Name, <>) -> + case Val of + 0 -> {Name, false, Bin}; + 1 -> {Name, true, Bin}; + _ -> err({bad_property, Pkt, Name}) + end; +decode_bool_prop(Pkt, Name, _) -> + err({bad_property, Pkt, Name}). + +-spec decode_code_with_props(mqtt_version(), atom(), binary()) -> + {reason_code(), properties()}. +decode_code_with_props(_, connack, <>) -> + {decode_connack_code(Code), + case Props of + <<>> -> + #{}; + _ -> + {PropMap, <<>>} = decode_props(connack, Props), + PropMap + end}; +decode_code_with_props(_, Pkt, <<>>) -> + {decode_reason_code(Pkt, 0), #{}}; +decode_code_with_props(?MQTT_VERSION_5, Pkt, <>) -> + {decode_reason_code(Pkt, Code), #{}}; +decode_code_with_props(?MQTT_VERSION_5, Pkt, <>) -> + {PropMap, <<>>} = decode_props(Pkt, Props), + {decode_reason_code(Pkt, Code), PropMap}; +decode_code_with_props(_, Pkt, _) -> + err({bad_packet, Pkt}). + +-spec decode_pubcomp_code(char()) -> reason_code(). +decode_pubcomp_code(0) -> 'success'; +decode_pubcomp_code(146) -> 'packet-identifier-not-found'; +decode_pubcomp_code(Code) -> err({bad_reason_code, pubcomp, Code}). + +-spec decode_pubrec_code(char()) -> reason_code(). +decode_pubrec_code(0) -> 'success'; +decode_pubrec_code(16) -> 'no-matching-subscribers'; +decode_pubrec_code(128) -> 'unspecified-error'; +decode_pubrec_code(131) -> 'implementation-specific-error'; +decode_pubrec_code(135) -> 'not-authorized'; +decode_pubrec_code(144) -> 'topic-name-invalid'; +decode_pubrec_code(145) -> 'packet-identifier-in-use'; +decode_pubrec_code(151) -> 'quota-exceeded'; +decode_pubrec_code(153) -> 'payload-format-invalid'; +decode_pubrec_code(Code) -> err({bad_reason_code, pubrec, Code}). + +-spec decode_disconnect_code(char()) -> reason_code(). +decode_disconnect_code(0) -> 'normal-disconnection'; +decode_disconnect_code(4) -> 'disconnect-with-will-message'; +decode_disconnect_code(128) -> 'unspecified-error'; +decode_disconnect_code(129) -> 'malformed-packet'; +decode_disconnect_code(130) -> 'protocol-error'; +decode_disconnect_code(131) -> 'implementation-specific-error'; +decode_disconnect_code(135) -> 'not-authorized'; +decode_disconnect_code(137) -> 'server-busy'; +decode_disconnect_code(139) -> 'server-shutting-down'; +decode_disconnect_code(140) -> 'bad-authentication-method'; +decode_disconnect_code(141) -> 'keep-alive-timeout'; +decode_disconnect_code(142) -> 'session-taken-over'; +decode_disconnect_code(143) -> 'topic-filter-invalid'; +decode_disconnect_code(144) -> 'topic-name-invalid'; +decode_disconnect_code(147) -> 'receive-maximum-exceeded'; +decode_disconnect_code(148) -> 'topic-alias-invalid'; +decode_disconnect_code(149) -> 'packet-too-large'; +decode_disconnect_code(150) -> 'message-rate-too-high'; +decode_disconnect_code(151) -> 'quota-exceeded'; +decode_disconnect_code(152) -> 'administrative-action'; +decode_disconnect_code(153) -> 'payload-format-invalid'; +decode_disconnect_code(154) -> 'retain-not-supported'; +decode_disconnect_code(155) -> 'qos-not-supported'; +decode_disconnect_code(156) -> 'use-another-server'; +decode_disconnect_code(157) -> 'server-moved'; +decode_disconnect_code(158) -> 'shared-subscriptions-not-supported'; +decode_disconnect_code(159) -> 'connection-rate-exceeded'; +decode_disconnect_code(160) -> 'maximum-connect-time'; +decode_disconnect_code(161) -> 'subscription-identifiers-not-supported'; +decode_disconnect_code(162) -> 'wildcard-subscriptions-not-supported'; +decode_disconnect_code(Code) -> err({bad_reason_code, disconnect, Code}). + +-spec decode_auth_code(char()) -> reason_code(). +decode_auth_code(0) -> 'success'; +decode_auth_code(24) -> 'continue-authentication'; +decode_auth_code(25) -> 're-authenticate'; +decode_auth_code(Code) -> err({bad_reason_code, auth, Code}). + +-spec decode_suback_code(char()) -> 0..2 | reason_code(). +decode_suback_code(0) -> 0; +decode_suback_code(1) -> 1; +decode_suback_code(2) -> 2; +decode_suback_code(128) -> 'unspecified-error'; +decode_suback_code(131) -> 'implementation-specific-error'; +decode_suback_code(135) -> 'not-authorized'; +decode_suback_code(143) -> 'topic-filter-invalid'; +decode_suback_code(145) -> 'packet-identifier-in-use'; +decode_suback_code(151) -> 'quota-exceeded'; +decode_suback_code(158) -> 'shared-subscriptions-not-supported'; +decode_suback_code(161) -> 'subscription-identifiers-not-supported'; +decode_suback_code(162) -> 'wildcard-subscriptions-not-supported'; +decode_suback_code(Code) -> err({bad_reason_code, suback, Code}). + +-spec decode_unsuback_code(char()) -> reason_code(). +decode_unsuback_code(0) -> 'success'; +decode_unsuback_code(17) -> 'no-subscription-existed'; +decode_unsuback_code(128) -> 'unspecified-error'; +decode_unsuback_code(131) -> 'implementation-specific-error'; +decode_unsuback_code(135) -> 'not-authorized'; +decode_unsuback_code(143) -> 'topic-filter-invalid'; +decode_unsuback_code(145) -> 'packet-identifier-in-use'; +decode_unsuback_code(Code) -> err({bad_reason_code, unsuback, Code}). + +-spec decode_puback_code(char()) -> reason_code(). +decode_puback_code(0) -> 'success'; +decode_puback_code(16) -> 'no-matching-subscribers'; +decode_puback_code(128) -> 'unspecified-error'; +decode_puback_code(131) -> 'implementation-specific-error'; +decode_puback_code(135) -> 'not-authorized'; +decode_puback_code(144) -> 'topic-name-invalid'; +decode_puback_code(145) -> 'packet-identifier-in-use'; +decode_puback_code(151) -> 'quota-exceeded'; +decode_puback_code(153) -> 'payload-format-invalid'; +decode_puback_code(Code) -> err({bad_reason_code, puback, Code}). + +-spec decode_pubrel_code(char()) -> reason_code(). +decode_pubrel_code(0) -> 'success'; +decode_pubrel_code(146) -> 'packet-identifier-not-found'; +decode_pubrel_code(Code) -> err({bad_reason_code, pubrel, Code}). + +-spec decode_connack_code(char()) -> reason_code(). +decode_connack_code(0) -> 'success'; +decode_connack_code(1) -> 'unsupported-protocol-version'; +decode_connack_code(2) -> 'client-identifier-not-valid'; +decode_connack_code(3) -> 'server-unavailable'; +decode_connack_code(4) -> 'bad-user-name-or-password'; +decode_connack_code(5) -> 'not-authorized'; +decode_connack_code(128) -> 'unspecified-error'; +decode_connack_code(129) -> 'malformed-packet'; +decode_connack_code(130) -> 'protocol-error'; +decode_connack_code(131) -> 'implementation-specific-error'; +decode_connack_code(132) -> 'unsupported-protocol-version'; +decode_connack_code(133) -> 'client-identifier-not-valid'; +decode_connack_code(134) -> 'bad-user-name-or-password'; +decode_connack_code(135) -> 'not-authorized'; +decode_connack_code(136) -> 'server-unavailable'; +decode_connack_code(137) -> 'server-busy'; +decode_connack_code(138) -> 'banned'; +decode_connack_code(140) -> 'bad-authentication-method'; +decode_connack_code(144) -> 'topic-name-invalid'; +decode_connack_code(149) -> 'packet-too-large'; +decode_connack_code(151) -> 'quota-exceeded'; +decode_connack_code(153) -> 'payload-format-invalid'; +decode_connack_code(154) -> 'retain-not-supported'; +decode_connack_code(155) -> 'qos-not-supported'; +decode_connack_code(156) -> 'use-another-server'; +decode_connack_code(157) -> 'server-moved'; +decode_connack_code(159) -> 'connection-rate-exceeded'; +decode_connack_code(Code) -> err({bad_reason_code, connack, Code}). + +-spec decode_reason_code(atom(), char()) -> reason_code(). +decode_reason_code(pubcomp, Code) -> decode_pubcomp_code(Code); +decode_reason_code(pubrec, Code) -> decode_pubrec_code(Code); +decode_reason_code(disconnect, Code) -> decode_disconnect_code(Code); +decode_reason_code(auth, Code) -> decode_auth_code(Code); +decode_reason_code(puback, Code) -> decode_puback_code(Code); +decode_reason_code(pubrel, Code) -> decode_pubrel_code(Code); +decode_reason_code(connack, Code) -> decode_connack_code(Code). + +%%%=================================================================== +%%% Encoder +%%%=================================================================== +encode_connect(#connect{proto_level = Version, properties = Props, + will = Will, will_properties = WillProps, + clean_start = CleanStart, + keep_alive = KeepAlive, client_id = ClientID, + username = Username, password = Password}) -> + UserFlag = Username /= <<>>, + PassFlag = UserFlag andalso Password /= <<>>, + WillFlag = is_record(Will, publish), + WillRetain = WillFlag andalso Will#publish.retain, + WillQoS = if WillFlag -> Will#publish.qos; + true -> 0 + end, + Header = <<4:16, "MQTT", Version, (enc_bool(UserFlag)):1, + (enc_bool(PassFlag)):1, (enc_bool(WillRetain)):1, + WillQoS:2, (enc_bool(WillFlag)):1, + (enc_bool(CleanStart)):1, 0:1, + KeepAlive:16>>, + EncClientID = <<(size(ClientID)):16, ClientID/binary>>, + EncWill = encode_will(Will), + EncUserPass = encode_user_pass(Username, Password), + Payload = case Version of + ?MQTT_VERSION_5 -> + [Header, encode_props(Props), EncClientID, + if WillFlag -> encode_props(WillProps); + true -> <<>> + end, + EncWill, EncUserPass]; + _ -> + [Header, EncClientID, EncWill, EncUserPass] + end, + <<1:4, 0:4, (encode_with_len(Payload))/binary>>. + +encode_connack(Version, #connack{session_present = SP, + code = Code, properties = Props}) -> + Payload = [enc_bool(SP), + encode_connack_code(Version, Code), + encode_props(Version, Props)], + <<2:4, 0:4, (encode_with_len(Payload))/binary>>. + +encode_publish(Version, #publish{qos = QoS, retain = Retain, dup = Dup, + topic = Topic, id = ID, payload = Payload, + properties = Props}) -> + Data1 = <<(size(Topic)):16, Topic/binary>>, + Data2 = case QoS of + 0 -> <<>>; + _ when ID>0 -> <> + end, + Data3 = encode_props(Version, Props), + Data4 = encode_with_len([Data1, Data2, Data3, Payload]), + <<3:4, (enc_bool(Dup)):1, QoS:2, (enc_bool(Retain)):1, Data4/binary>>. + +encode_puback(Version, #puback{id = ID, code = Code, + properties = Props}) when ID>0 -> + Data = encode_code_with_props(Version, Code, Props), + <<4:4, 0:4, (encode_with_len([<>|Data]))/binary>>. + +encode_pubrec(Version, #pubrec{id = ID, code = Code, + properties = Props}) when ID>0 -> + Data = encode_code_with_props(Version, Code, Props), + <<5:4, 0:4, (encode_with_len([<>|Data]))/binary>>. + +encode_pubrel(Version, #pubrel{id = ID, code = Code, + properties = Props}) when ID>0 -> + Data = encode_code_with_props(Version, Code, Props), + <<6:4, 2:4, (encode_with_len([<>|Data]))/binary>>. + +encode_pubcomp(Version, #pubcomp{id = ID, code = Code, + properties = Props}) when ID>0 -> + Data = encode_code_with_props(Version, Code, Props), + <<7:4, 0:4, (encode_with_len([<>|Data]))/binary>>. + +encode_subscribe(Version, #subscribe{id = ID, + filters = [_|_] = Filters, + properties = Props}) when ID>0 -> + EncFilters = [<<(size(Filter)):16, Filter/binary, + (encode_subscription_options(SubOpts))>> || + {Filter, SubOpts} <- Filters], + Payload = [<>, encode_props(Version, Props), EncFilters], + <<8:4, 2:4, (encode_with_len(Payload))/binary>>. + +encode_suback(Version, #suback{id = ID, codes = Codes, + properties = Props}) when ID>0 -> + Payload = [<>, encode_props(Version, Props) + |[encode_reason_code(Code) || Code <- Codes]], + <<9:4, 0:4, (encode_with_len(Payload))/binary>>. + +encode_unsubscribe(Version, #unsubscribe{id = ID, + filters = [_|_] = Filters, + properties = Props}) when ID>0 -> + EncFilters = [<<(size(Filter)):16, Filter/binary>> || Filter <- Filters], + Payload = [<>, encode_props(Version, Props), EncFilters], + <<10:4, 2:4, (encode_with_len(Payload))/binary>>. + +encode_unsuback(Version, #unsuback{id = ID, codes = Codes, + properties = Props}) when ID>0 -> + EncCodes = case Version of + ?MQTT_VERSION_5 -> + [encode_reason_code(Code) || Code <- Codes]; + ?MQTT_VERSION_4 -> + [] + end, + Payload = [<>, encode_props(Version, Props)|EncCodes], + <<11:4, 0:4, (encode_with_len(Payload))/binary>>. + +encode_pingreq() -> + <<12:4, 0:4, 0>>. + +encode_pingresp() -> + <<13:4, 0:4, 0>>. + +encode_disconnect(Version, #disconnect{code = Code, properties = Props}) -> + Data = encode_code_with_props(Version, Code, Props), + <<14:4, 0:4, (encode_with_len(Data))/binary>>. + +encode_auth(#auth{code = Code, properties = Props}) -> + Data = encode_code_with_props(?MQTT_VERSION_5, Code, Props), + <<15:4, 0:4, (encode_with_len(Data))/binary>>. + +-spec encode_with_len(iodata()) -> binary(). +encode_with_len(IOData) -> + Data = iolist_to_binary(IOData), + Len = encode_varint(size(Data)), + <>. + +-spec encode_varint(non_neg_integer()) -> binary(). +encode_varint(X) when X < 128 -> + <<0:1, X:7>>; +encode_varint(X) when X < ?MAX_VARINT -> + <<1:1, (X rem 128):7, (encode_varint(X div 128))/binary>>. + +-spec encode_props(mqtt_version(), properties()) -> binary(). +encode_props(?MQTT_VERSION_5, Props) -> + encode_props(Props); +encode_props(?MQTT_VERSION_4, _) -> + <<>>. + +-spec encode_props(properties()) -> binary(). +encode_props(Props) -> + encode_with_len( + maps:fold( + fun(Name, Val, Acc) -> + [encode_prop(Name, Val)|Acc] + end, [], Props)). + +-spec encode_prop(property(), term()) -> iodata(). +encode_prop(assigned_client_identifier, <<>>) -> + <<>>; +encode_prop(assigned_client_identifier, ID) -> + <<18, (size(ID)):16, ID/binary>>; +encode_prop(authentication_data, <<>>) -> + <<>>; +encode_prop(authentication_data, Data) -> + <<22, (size(Data)):16, Data/binary>>; +encode_prop(authentication_method, <<>>) -> + <<>>; +encode_prop(authentication_method, M) -> + <<21, (size(M)):16, M/binary>>; +encode_prop(content_type, <<>>) -> + <<>>; +encode_prop(content_type, T) -> + <<3, (size(T)):16, T/binary>>; +encode_prop(correlation_data, <<>>) -> + <<>>; +encode_prop(correlation_data, Data) -> + <<9, (size(Data)):16, Data/binary>>; +encode_prop(maximum_packet_size, Size) when Size>0, Size= + <<39, Size:32>>; +encode_prop(maximum_qos, QoS) when QoS>=0, QoS<2 -> + <<36, QoS>>; +encode_prop(message_expiry_interval, I) when I>=0, I= + <<2, I:32>>; +encode_prop(payload_format_indicator, binary) -> + <<>>; +encode_prop(payload_format_indicator, utf8) -> + <<1, 1>>; +encode_prop(reason_string, <<>>) -> + <<>>; +encode_prop(reason_string, S) -> + <<31, (size(S)):16, S/binary>>; +encode_prop(receive_maximum, Max) when Max>0, Max= + <<33, Max:16>>; +encode_prop(request_problem_information, true) -> + <<>>; +encode_prop(request_problem_information, false) -> + <<23, 0>>; +encode_prop(request_response_information, false) -> + <<>>; +encode_prop(request_response_information, true) -> + <<25, 1>>; +encode_prop(response_information, <<>>) -> + <<>>; +encode_prop(response_information, S) -> + <<26, (size(S)):16, S/binary>>; +encode_prop(response_topic, <<>>) -> + <<>>; +encode_prop(response_topic, T) -> + <<8, (size(T)):16, T/binary>>; +encode_prop(retain_available, true) -> + <<>>; +encode_prop(retain_available, false) -> + <<37, 0>>; +encode_prop(server_keep_alive, Secs) when Secs>=0, Secs= + <<19, Secs:16>>; +encode_prop(server_reference, <<>>) -> + <<>>; +encode_prop(server_reference, S) -> + <<28, (size(S)):16, S/binary>>; +encode_prop(session_expiry_interval, I) when I>=0, I= + <<17, I:32>>; +encode_prop(shared_subscription_available, true) -> + <<>>; +encode_prop(shared_subscription_available, false) -> + <<42, 0>>; +encode_prop(subscription_identifier, [_|_] = IDs) -> + [encode_prop(subscription_identifier, ID) || ID <- IDs]; +encode_prop(subscription_identifier, ID) when ID>0, ID + <<11, (encode_varint(ID))/binary>>; +encode_prop(subscription_identifiers_available, true) -> + <<>>; +encode_prop(subscription_identifiers_available, false) -> + <<41, 0>>; +encode_prop(topic_alias, Alias) when Alias>0, Alias= + <<35, Alias:16>>; +encode_prop(topic_alias_maximum, 0) -> + <<>>; +encode_prop(topic_alias_maximum, Max) when Max>0, Max= + <<34, Max:16>>; +encode_prop(user_property, Pairs) -> + [<<38, (encode_utf8_pair(Pair))/binary>> || Pair <- Pairs]; +encode_prop(wildcard_subscription_available, true) -> + <<>>; +encode_prop(wildcard_subscription_available, false) -> + <<40, 0>>; +encode_prop(will_delay_interval, 0) -> + <<>>; +encode_prop(will_delay_interval, I) when I>0, I= + <<24, I:32>>. + +-spec encode_user_pass(binary(), binary()) -> binary(). +encode_user_pass(User, Pass) when User /= <<>> andalso Pass /= <<>> -> + <<(size(User)):16, User/binary, (size(Pass)):16, Pass/binary>>; +encode_user_pass(User, _) when User /= <<>> -> + <<(size(User)):16, User/binary>>; +encode_user_pass(_, _) -> + <<>>. + +-spec encode_will(undefined | publish()) -> binary(). +encode_will(#publish{topic = Topic, payload = Payload}) -> + <<(size(Topic)):16, Topic/binary, + (size(Payload)):16, Payload/binary>>; +encode_will(undefined) -> + <<>>. + +encode_subscription_options(#sub_opts{qos = QoS, + no_local = NL, + retain_as_published = RAP, + retain_handling = RH}) + when QoS>=0, RH>=0, QoS<3, RH<3 -> + (RH bsl 4) bor (enc_bool(RAP) bsl 3) bor (enc_bool(NL) bsl 2) bor QoS. + +-spec encode_code_with_props(mqtt_version(), reason_code(), properties()) -> [binary()]. +encode_code_with_props(Version, Code, Props) -> + if Version == ?MQTT_VERSION_4 orelse + (Code == success andalso Props == #{}) -> + []; + Props == #{} -> + [encode_reason_code(Code)]; + true -> + [encode_reason_code(Code), encode_props(Props)] + end. + +-spec encode_utf8_pair({binary(), binary()}) -> binary(). +encode_utf8_pair({Key, Val}) -> + <<(size(Key)):16, Key/binary, (size(Val)):16, Val/binary>>. + +-spec encode_connack_code(mqtt_version(), atom()) -> char(). +encode_connack_code(?MQTT_VERSION_5, Reason) -> encode_reason_code(Reason); +encode_connack_code(_, success) -> 0; +encode_connack_code(_, 'unsupported-protocol-version') -> 1; +encode_connack_code(_, 'client-identifier-not-valid') -> 2; +encode_connack_code(_, 'server-unavailable') -> 3; +encode_connack_code(_, 'bad-user-name-or-password') -> 4; +encode_connack_code(_, 'not-authorized') -> 5; +encode_connack_code(_, _) -> 128. + +-spec encode_reason_code(char() | reason_code()) -> char(). +encode_reason_code('success') -> 0; +encode_reason_code('normal-disconnection') -> 0; +encode_reason_code('granted-qos-0') -> 0; +encode_reason_code('granted-qos-1') -> 1; +encode_reason_code('granted-qos-2') -> 2; +encode_reason_code('disconnect-with-will-message') -> 4; +encode_reason_code('no-matching-subscribers') -> 16; +encode_reason_code('no-subscription-existed') -> 17; +encode_reason_code('continue-authentication') -> 24; +encode_reason_code('re-authenticate') -> 25; +encode_reason_code('unspecified-error') -> 128; +encode_reason_code('malformed-packet') -> 129; +encode_reason_code('protocol-error') -> 130; +encode_reason_code('implementation-specific-error') -> 131; +encode_reason_code('unsupported-protocol-version') -> 132; +encode_reason_code('client-identifier-not-valid') -> 133; +encode_reason_code('bad-user-name-or-password') -> 134; +encode_reason_code('not-authorized') -> 135; +encode_reason_code('server-unavailable') -> 136; +encode_reason_code('server-busy') -> 137; +encode_reason_code('banned') -> 138; +encode_reason_code('server-shutting-down') -> 139; +encode_reason_code('bad-authentication-method') -> 140; +encode_reason_code('keep-alive-timeout') -> 141; +encode_reason_code('session-taken-over') -> 142; +encode_reason_code('topic-filter-invalid') -> 143; +encode_reason_code('topic-name-invalid') -> 144; +encode_reason_code('packet-identifier-in-use') -> 145; +encode_reason_code('packet-identifier-not-found') -> 146; +encode_reason_code('receive-maximum-exceeded') -> 147; +encode_reason_code('topic-alias-invalid') -> 148; +encode_reason_code('packet-too-large') -> 149; +encode_reason_code('message-rate-too-high') -> 150; +encode_reason_code('quota-exceeded') -> 151; +encode_reason_code('administrative-action') -> 152; +encode_reason_code('payload-format-invalid') -> 153; +encode_reason_code('retain-not-supported') -> 154; +encode_reason_code('qos-not-supported') -> 155; +encode_reason_code('use-another-server') -> 156; +encode_reason_code('server-moved') -> 157; +encode_reason_code('shared-subscriptions-not-supported') -> 158; +encode_reason_code('connection-rate-exceeded') -> 159; +encode_reason_code('maximum-connect-time') -> 160; +encode_reason_code('subscription-identifiers-not-supported') -> 161; +encode_reason_code('wildcard-subscriptions-not-supported') -> 162; +encode_reason_code(Code) when is_integer(Code) -> Code. + +%%%=================================================================== +%%% Formatters +%%%=================================================================== +-spec pp(atom(), non_neg_integer()) -> [atom()] | no. +pp(codec_state, 6) -> record_info(fields, codec_state); +pp(connect, 9) -> record_info(fields, connect); +pp(connack, 3) -> record_info(fields, connack); +pp(publish, 8) -> record_info(fields, publish); +pp(puback, 3) -> record_info(fields, puback); +pp(pubrec, 3) -> record_info(fields, pubrec); +pp(pubrel, 4) -> record_info(fields, pubrel); +pp(pubcomp, 3) -> record_info(fields, pubcomp); +pp(subscribe, 4) -> record_info(fields, subscribe); +pp(suback, 3) -> record_info(fields, suback); +pp(unsubscribe, 3) -> record_info(fields, unsubscribe); +pp(unsuback, 1) -> record_info(fields, unsuback); +pp(pingreq, 1) -> record_info(fields, pingreq); +pp(pingresp, 0) -> record_info(fields, pingresp); +pp(disconnect, 2) -> record_info(fields, disconnect); +pp(sub_opts, 4) -> record_info(fields, sub_opts); +pp(_, _) -> no. + +-spec format(io:format(), list()) -> string(). +format(Fmt, Args) -> + lists:flatten(io_lib:format(Fmt, Args)). + +format_got_expected(Txt, Got, Expected) -> + FmtGot = term_format(Got), + FmtExp = term_format(Expected), + format("~ts: " ++ FmtGot ++ " (expected: " ++ FmtExp ++ ")", + [Txt, Got, Expected]). + +term_format(I) when is_integer(I) -> + "~B"; +term_format(B) when is_binary(B) -> + term_format(binary_to_list(B)); +term_format(A) when is_atom(A) -> + term_format(atom_to_list(A)); +term_format(T) -> + case io_lib:printable_latin1_list(T) of + true -> "~ts"; + false -> "~w" + end. + +%%%=================================================================== +%%% Validators +%%%=================================================================== +-spec assert(T, any(), any()) -> T. +assert(Got, Got, _) -> + Got; +assert(Got, Expected, Reason) -> + err({Reason, Got, Expected}). + +-spec qos(qos()) -> qos(). +qos(QoS) when is_integer(QoS), QoS>=0, QoS<3 -> + QoS; +qos(QoS) -> + err({bad_qos, QoS}). + +-spec topic(binary()) -> binary(). +topic(Topic) -> + topic(Topic, #{}). + +-spec topic(binary(), properties()) -> binary(). +topic(<<>>, Props) -> + case maps:is_key(topic_alias, Props) of + true -> <<>>; + false -> err(bad_topic) + end; +topic(Bin, _) when is_binary(Bin) -> + ok = check_topic(Bin), + ok = check_utf8(Bin), + Bin; +topic(_, _) -> + err(bad_topic). + +-spec topic_filter(binary()) -> binary(). +topic_filter(<<>>) -> + err(bad_topic_filter); +topic_filter(Bin) when is_binary(Bin) -> + ok = check_topic_filter(Bin, $/), + ok = check_utf8(Bin), + Bin; +topic_filter(_) -> + err(bad_topic_filter). + +-spec utf8(binary()) -> binary(). +utf8(Bin) -> + ok = check_utf8(Bin), + ok = check_zero(Bin), + Bin. + +-spec check_topic(binary()) -> ok. +check_topic(<>) when H == $#; H == $+; H == 0 -> + err(bad_topic); +check_topic(<<_, T/binary>>) -> + check_topic(T); +check_topic(<<>>) -> + ok. + +-spec check_topic_filter(binary(), char()) -> ok. +check_topic_filter(<<>>, _) -> + ok; +check_topic_filter(_, $#) -> + err(bad_topic_filter); +check_topic_filter(<<$#, _/binary>>, C) when C /= $/ -> + err(bad_topic_filter); +check_topic_filter(<<$+, _/binary>>, C) when C /= $/ -> + err(bad_topic_filter); +check_topic_filter(<>, $+) when C /= $/ -> + err(bad_topic_filter); +check_topic_filter(<<0, _/binary>>, _) -> + err(bad_topic_filter); +check_topic_filter(<>, _) -> + check_topic_filter(T, H). + +-spec check_utf8(binary()) -> ok. +check_utf8(Bin) -> + case unicode:characters_to_binary(Bin, utf8) of + UTF8Str when is_binary(UTF8Str) -> + ok; + _ -> + err(bad_utf8_string) + end. + +-spec check_zero(binary()) -> ok. +check_zero(<<0, _/binary>>) -> + err(bad_utf8_string); +check_zero(<<_, T/binary>>) -> + check_zero(T); +check_zero(<<>>) -> + ok. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +-spec dec_bool(non_neg_integer()) -> boolean(). +dec_bool(0) -> false; +dec_bool(_) -> true. + +-spec enc_bool(boolean()) -> 0..1. +enc_bool(true) -> 1; +enc_bool(false) -> 0. + +-spec err(any()) -> no_return(). +err(Reason) -> + erlang:error({?MODULE, Reason}). diff --git a/src/node.template b/src/node.template deleted file mode 100644 index 89b4a310a..000000000 --- a/src/node.template +++ /dev/null @@ -1,194 +0,0 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. -%%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. -%%% -%%% The Initial Developer of the Original Code is ProcessOne. -%%% Portions created by ProcessOne are Copyright 2006-2015, ProcessOne -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2015, ProcessOne. -%%% -%%% @copyright 2006-2015 ProcessOne -%%% @author Christophe romain -%%% [http://www.process-one.net/] -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== - --module(__TO_BE_DEFINED__). --author(__TO_BE_DEFINED__). - --include("pubsub.hrl"). --include("jlib.hrl"). - --behaviour(gen_pubsub_node). - -%% Note on function definition -%% included is all defined plugin function -%% it's possible not to define some function at all -%% in that case, warning will be generated at compilation -%% and function call will fail, -%% then mod_pubsub will call function from node_hometree -%% (this makes code cleaner, but execution a little bit longer) - -%% API definition --export([init/3, terminate/2, - options/0, features/0, - create_node_permission/6, - create_node/2, - delete_node/1, - purge_node/2, - subscribe_node/8, - unsubscribe_node/4, - publish_item/6, - delete_item/4, - remove_extra_items/3, - get_entity_affiliations/2, - get_node_affiliations/1, - get_affiliation/2, - set_affiliation/3, - get_entity_subscriptions/2, - get_node_subscriptions/1, - get_subscriptions/2, - set_subscriptions/4, - get_pending_nodes/2, - get_states/1, - get_state/2, - set_state/1, - get_items/6, - get_items/2, - get_item/7, - get_item/2, - set_item/1, - get_item_name/3 - ]). - - -init(Host, ServerHost, Opts) -> - node_hometree:init(Host, ServerHost, Opts). - -terminate(Host, ServerHost) -> - node_hometree:terminate(Host, ServerHost). - -options() -> - [{deliver_payloads, true}, - {notify_config, false}, - {notify_delete, false}, - {notify_retract, true}, - {purge_offline, false}, - {persist_items, true}, - {max_items, ?MAXITEMS}, - {subscribe, true}, - {access_model, open}, - {roster_groups_allowed, []}, - {publish_model, publishers}, - {notification_type, headline}, - {max_payload_size, ?MAX_PAYLOAD_SIZE}, - {send_last_published_item, on_sub_and_presence}, - {deliver_notifications, true}, - {presence_based_delivery, false}]. - -features() -> - ["create-nodes", - "delete-nodes", - "delete-items", - "instant-nodes", - "outcast-affiliation", - "persistent-items", - "publish", - "purge-nodes", - "retract-items", - "retrieve-affiliations", - "retrieve-items", - "retrieve-subscriptions", - "subscribe", - "subscription-notifications" - ]. - -create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access) -> - node_hometree:create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access). - -create_node(NodeId, Owner) -> - node_hometree:create_node(NodeId, Owner). - -delete_node(Removed) -> - node_hometree:delete_node(Removed). - -subscribe_node(NodeId, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options) -> - node_hometree:subscribe_node(NodeId, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options). - -unsubscribe_node(NodeId, Sender, Subscriber, SubID) -> - node_hometree:unsubscribe_node(NodeId, Sender, Subscriber, SubID). - -publish_item(NodeId, Publisher, Model, MaxItems, ItemId, Payload) -> - node_hometree:publish_item(NodeId, Publisher, Model, MaxItems, ItemId, Payload). - -remove_extra_items(NodeId, MaxItems, ItemIds) -> - node_hometree:remove_extra_items(NodeId, MaxItems, ItemIds). - -delete_item(NodeId, Publisher, PublishModel, ItemId) -> - node_hometree:delete_item(NodeId, Publisher, PublishModel, ItemId). - -purge_node(NodeId, Owner) -> - node_hometree:purge_node(NodeId, Owner). - -get_entity_affiliations(Host, Owner) -> - node_hometree:get_entity_affiliations(Host, Owner). - -get_node_affiliations(NodeId) -> - node_hometree:get_node_affiliations(NodeId). - -get_affiliation(NodeId, Owner) -> - node_hometree:get_affiliation(NodeId, Owner). - -set_affiliation(NodeId, Owner, Affiliation) -> - node_hometree:set_affiliation(NodeId, Owner, Affiliation). - -get_entity_subscriptions(Host, Owner) -> - node_hometree:get_entity_subscriptions(Host, Owner). - -get_node_subscriptions(NodeId) -> - node_hometree:get_node_subscriptions(NodeId). - -get_subscriptions(NodeId, Owner) -> - node_hometree:get_subscriptions(NodeId, Owner). - -set_subscriptions(NodeId, Owner, Subscription, SubId) -> - node_hometree:set_subscriptions(NodeId, Owner, Subscription, SubId). - -get_pending_nodes(Host, Owner) -> - node_hometree:get_pending_nodes(Host, Owner). - -get_states(NodeId) -> - node_hometree:get_states(NodeId). - -get_state(NodeId, JID) -> - node_hometree:get_state(NodeId, JID). - -set_state(State) -> - node_hometree:set_state(State). - -get_items(NodeId, From) -> - node_hometree:get_items(NodeId, From). - -get_items(NodeId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> - node_hometree:get_items(NodeId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId). - -get_item(NodeId, ItemId) -> - node_hometree:get_item(NodeId, ItemId). - -get_item(NodeId, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> - node_hometree:get_item(NodeId, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId). - -set_item(Item) -> - node_hometree:set_item(Item). - -get_item_name(Host, Node, Id) -> - node_hometree:get_item_name(Host, Node, Id). diff --git a/src/node_buddy.erl b/src/node_buddy.erl deleted file mode 100644 index e7ab2799c..000000000 --- a/src/node_buddy.erl +++ /dev/null @@ -1,184 +0,0 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. -%%% -%%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. -%%% -%%% -%%% The Initial Developer of the Original Code is ProcessOne. -%%% Portions created by ProcessOne are Copyright 2006-2015, ProcessOne -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2015, ProcessOne. -%%% -%%% -%%% @copyright 2006-2015 ProcessOne -%%% @author Christophe romain -%%% [http://www.process-one.net/] -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== - --module(node_buddy). - --author('christophe.romain@process-one.net'). - --include("pubsub.hrl"). - --include("jlib.hrl"). - --behaviour(gen_pubsub_node). - -%% Note on function definition -%% included is all defined plugin function -%% it's possible not to define some function at all -%% in that case, warning will be generated at compilation -%% and function call will fail, -%% then mod_pubsub will call function from node_hometree -%% (this makes code cleaner, but execution a little bit longer) - -%% API definition --export([init/3, terminate/2, options/0, features/0, - create_node_permission/6, create_node/2, delete_node/1, - purge_node/2, subscribe_node/8, unsubscribe_node/4, - publish_item/6, delete_item/4, remove_extra_items/3, - get_entity_affiliations/2, get_node_affiliations/1, - get_affiliation/2, set_affiliation/3, - get_entity_subscriptions/2, get_node_subscriptions/1, - get_subscriptions/2, set_subscriptions/4, - get_pending_nodes/2, get_states/1, get_state/2, - set_state/1, get_items/6, get_items/2, get_item/7, - get_item/2, set_item/1, get_item_name/3, node_to_path/1, - path_to_node/1]). - -init(Host, ServerHost, Opts) -> - node_hometree:init(Host, ServerHost, Opts). - -terminate(Host, ServerHost) -> - node_hometree:terminate(Host, ServerHost). - -options() -> - [{deliver_payloads, true}, {notify_config, false}, - {notify_delete, false}, {notify_retract, true}, - {purge_offline, false}, {persist_items, true}, - {max_items, ?MAXITEMS}, {subscribe, true}, - {access_model, presence}, {roster_groups_allowed, []}, - {publish_model, publishers}, - {notification_type, headline}, - {max_payload_size, ?MAX_PAYLOAD_SIZE}, - {send_last_published_item, never}, - {deliver_notifications, true}, - {presence_based_delivery, false}]. - -features() -> - [<<"create-nodes">>, <<"delete-nodes">>, - <<"delete-items">>, <<"instant-nodes">>, <<"item-ids">>, - <<"outcast-affiliation">>, <<"persistent-items">>, - <<"publish">>, <<"purge-nodes">>, <<"retract-items">>, - <<"retrieve-affiliations">>, <<"retrieve-items">>, - <<"retrieve-subscriptions">>, <<"subscribe">>, - <<"subscription-notifications">>]. - -create_node_permission(Host, ServerHost, Node, - ParentNode, Owner, Access) -> - node_hometree:create_node_permission(Host, ServerHost, - Node, ParentNode, Owner, Access). - -create_node(NodeId, Owner) -> - node_hometree:create_node(NodeId, Owner). - -delete_node(Removed) -> - node_hometree:delete_node(Removed). - -subscribe_node(NodeId, Sender, Subscriber, AccessModel, - SendLast, PresenceSubscription, RosterGroup, Options) -> - node_hometree:subscribe_node(NodeId, Sender, Subscriber, - AccessModel, SendLast, PresenceSubscription, - RosterGroup, Options). - -unsubscribe_node(NodeId, Sender, Subscriber, SubID) -> - node_hometree:unsubscribe_node(NodeId, Sender, - Subscriber, SubID). - -publish_item(NodeId, Publisher, Model, MaxItems, ItemId, - Payload) -> - node_hometree:publish_item(NodeId, Publisher, Model, - MaxItems, ItemId, Payload). - -remove_extra_items(NodeId, MaxItems, ItemIds) -> - node_hometree:remove_extra_items(NodeId, MaxItems, - ItemIds). - -delete_item(NodeId, Publisher, PublishModel, ItemId) -> - node_hometree:delete_item(NodeId, Publisher, - PublishModel, ItemId). - -purge_node(NodeId, Owner) -> - node_hometree:purge_node(NodeId, Owner). - -get_entity_affiliations(Host, Owner) -> - node_hometree:get_entity_affiliations(Host, Owner). - -get_node_affiliations(NodeId) -> - node_hometree:get_node_affiliations(NodeId). - -get_affiliation(NodeId, Owner) -> - node_hometree:get_affiliation(NodeId, Owner). - -set_affiliation(NodeId, Owner, Affiliation) -> - node_hometree:set_affiliation(NodeId, Owner, - Affiliation). - -get_entity_subscriptions(Host, Owner) -> - node_hometree:get_entity_subscriptions(Host, Owner). - -get_node_subscriptions(NodeId) -> - node_hometree:get_node_subscriptions(NodeId). - -get_subscriptions(NodeId, Owner) -> - node_hometree:get_subscriptions(NodeId, Owner). - -set_subscriptions(NodeId, Owner, Subscription, SubId) -> - node_hometree:set_subscriptions(NodeId, Owner, - Subscription, SubId). - -get_pending_nodes(Host, Owner) -> - node_hometree:get_pending_nodes(Host, Owner). - -get_states(NodeId) -> node_hometree:get_states(NodeId). - -get_state(NodeId, JID) -> - node_hometree:get_state(NodeId, JID). - -set_state(State) -> node_hometree:set_state(State). - -get_items(NodeId, From) -> - node_hometree:get_items(NodeId, From). - -get_items(NodeId, JID, AccessModel, - PresenceSubscription, RosterGroup, SubId) -> - node_hometree:get_items(NodeId, JID, AccessModel, - PresenceSubscription, RosterGroup, SubId). - -get_item(NodeId, ItemId) -> - node_hometree:get_item(NodeId, ItemId). - -get_item(NodeId, ItemId, JID, AccessModel, - PresenceSubscription, RosterGroup, SubId) -> - node_hometree:get_item(NodeId, ItemId, JID, AccessModel, - PresenceSubscription, RosterGroup, SubId). - -set_item(Item) -> node_hometree:set_item(Item). - -get_item_name(Host, Node, Id) -> - node_hometree:get_item_name(Host, Node, Id). - -node_to_path(Node) -> node_flat:node_to_path(Node). - -path_to_node(Path) -> node_flat:path_to_node(Path). diff --git a/src/node_club.erl b/src/node_club.erl deleted file mode 100644 index f91723030..000000000 --- a/src/node_club.erl +++ /dev/null @@ -1,184 +0,0 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. -%%% -%%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. -%%% -%%% -%%% The Initial Developer of the Original Code is ProcessOne. -%%% Portions created by ProcessOne are Copyright 2006-2015, ProcessOne -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2015, ProcessOne. -%%% -%%% -%%% @copyright 2006-2015 ProcessOne -%%% @author Christophe romain -%%% [http://www.process-one.net/] -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== - --module(node_club). - --author('christophe.romain@process-one.net'). - --include("pubsub.hrl"). - --include("jlib.hrl"). - --behaviour(gen_pubsub_node). - -%% Note on function definition -%% included is all defined plugin function -%% it's possible not to define some function at all -%% in that case, warning will be generated at compilation -%% and function call will fail, -%% then mod_pubsub will call function from node_hometree -%% (this makes code cleaner, but execution a little bit longer) - -%% API definition --export([init/3, terminate/2, options/0, features/0, - create_node_permission/6, create_node/2, delete_node/1, - purge_node/2, subscribe_node/8, unsubscribe_node/4, - publish_item/6, delete_item/4, remove_extra_items/3, - get_entity_affiliations/2, get_node_affiliations/1, - get_affiliation/2, set_affiliation/3, - get_entity_subscriptions/2, get_node_subscriptions/1, - get_subscriptions/2, set_subscriptions/4, - get_pending_nodes/2, get_states/1, get_state/2, - set_state/1, get_items/6, get_items/2, get_item/7, - get_item/2, set_item/1, get_item_name/3, node_to_path/1, - path_to_node/1]). - -init(Host, ServerHost, Opts) -> - node_hometree:init(Host, ServerHost, Opts). - -terminate(Host, ServerHost) -> - node_hometree:terminate(Host, ServerHost). - -options() -> - [{deliver_payloads, true}, {notify_config, false}, - {notify_delete, false}, {notify_retract, true}, - {purge_offline, false}, {persist_items, true}, - {max_items, ?MAXITEMS}, {subscribe, true}, - {access_model, authorize}, {roster_groups_allowed, []}, - {publish_model, publishers}, - {notification_type, headline}, - {max_payload_size, ?MAX_PAYLOAD_SIZE}, - {send_last_published_item, never}, - {deliver_notifications, true}, - {presence_based_delivery, false}]. - -features() -> - [<<"create-nodes">>, <<"delete-nodes">>, - <<"delete-items">>, <<"instant-nodes">>, - <<"outcast-affiliation">>, <<"persistent-items">>, - <<"publish">>, <<"purge-nodes">>, <<"retract-items">>, - <<"retrieve-affiliations">>, <<"retrieve-items">>, - <<"retrieve-subscriptions">>, <<"subscribe">>, - <<"subscription-notifications">>]. - -create_node_permission(Host, ServerHost, Node, - ParentNode, Owner, Access) -> - node_hometree:create_node_permission(Host, ServerHost, - Node, ParentNode, Owner, Access). - -create_node(NodeId, Owner) -> - node_hometree:create_node(NodeId, Owner). - -delete_node(Removed) -> - node_hometree:delete_node(Removed). - -subscribe_node(NodeId, Sender, Subscriber, AccessModel, - SendLast, PresenceSubscription, RosterGroup, Options) -> - node_hometree:subscribe_node(NodeId, Sender, Subscriber, - AccessModel, SendLast, PresenceSubscription, - RosterGroup, Options). - -unsubscribe_node(NodeId, Sender, Subscriber, SubID) -> - node_hometree:unsubscribe_node(NodeId, Sender, - Subscriber, SubID). - -publish_item(NodeId, Publisher, Model, MaxItems, ItemId, - Payload) -> - node_hometree:publish_item(NodeId, Publisher, Model, - MaxItems, ItemId, Payload). - -remove_extra_items(NodeId, MaxItems, ItemIds) -> - node_hometree:remove_extra_items(NodeId, MaxItems, - ItemIds). - -delete_item(NodeId, Publisher, PublishModel, ItemId) -> - node_hometree:delete_item(NodeId, Publisher, - PublishModel, ItemId). - -purge_node(NodeId, Owner) -> - node_hometree:purge_node(NodeId, Owner). - -get_entity_affiliations(Host, Owner) -> - node_hometree:get_entity_affiliations(Host, Owner). - -get_node_affiliations(NodeId) -> - node_hometree:get_node_affiliations(NodeId). - -get_affiliation(NodeId, Owner) -> - node_hometree:get_affiliation(NodeId, Owner). - -set_affiliation(NodeId, Owner, Affiliation) -> - node_hometree:set_affiliation(NodeId, Owner, - Affiliation). - -get_entity_subscriptions(Host, Owner) -> - node_hometree:get_entity_subscriptions(Host, Owner). - -get_node_subscriptions(NodeId) -> - node_hometree:get_node_subscriptions(NodeId). - -get_subscriptions(NodeId, Owner) -> - node_hometree:get_subscriptions(NodeId, Owner). - -set_subscriptions(NodeId, Owner, Subscription, SubId) -> - node_hometree:set_subscriptions(NodeId, Owner, - Subscription, SubId). - -get_pending_nodes(Host, Owner) -> - node_hometree:get_pending_nodes(Host, Owner). - -get_states(NodeId) -> node_hometree:get_states(NodeId). - -get_state(NodeId, JID) -> - node_hometree:get_state(NodeId, JID). - -set_state(State) -> node_hometree:set_state(State). - -get_items(NodeId, From) -> - node_hometree:get_items(NodeId, From). - -get_items(NodeId, JID, AccessModel, - PresenceSubscription, RosterGroup, SubId) -> - node_hometree:get_items(NodeId, JID, AccessModel, - PresenceSubscription, RosterGroup, SubId). - -get_item(NodeId, ItemId) -> - node_hometree:get_item(NodeId, ItemId). - -get_item(NodeId, ItemId, JID, AccessModel, - PresenceSubscription, RosterGroup, SubId) -> - node_hometree:get_item(NodeId, ItemId, JID, AccessModel, - PresenceSubscription, RosterGroup, SubId). - -set_item(Item) -> node_hometree:set_item(Item). - -get_item_name(Host, Node, Id) -> - node_hometree:get_item_name(Host, Node, Id). - -node_to_path(Node) -> node_flat:node_to_path(Node). - -path_to_node(Path) -> node_flat:path_to_node(Path). diff --git a/src/node_dag.erl b/src/node_dag.erl deleted file mode 100644 index 9a36a4c4a..000000000 --- a/src/node_dag.erl +++ /dev/null @@ -1,163 +0,0 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. -%%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. -%%% -%%% @author Brian Cully -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== - --module(node_dag). - --author('bjc@kublai.com'). - --include("pubsub.hrl"). - --include("jlib.hrl"). - --behaviour(gen_pubsub_node). - -%% API definition --export([init/3, terminate/2, options/0, features/0, - create_node_permission/6, create_node/2, delete_node/1, - purge_node/2, subscribe_node/8, unsubscribe_node/4, - publish_item/6, delete_item/4, remove_extra_items/3, - get_entity_affiliations/2, get_node_affiliations/1, - get_affiliation/2, set_affiliation/3, - get_entity_subscriptions/2, get_node_subscriptions/1, - get_subscriptions/2, set_subscriptions/4, - get_pending_nodes/2, get_states/1, get_state/2, - set_state/1, get_items/6, get_items/2, get_item/7, - get_item/2, set_item/1, get_item_name/3, node_to_path/1, - path_to_node/1]). - -init(Host, ServerHost, Opts) -> - node_hometree:init(Host, ServerHost, Opts). - -terminate(Host, ServerHost) -> - node_hometree:terminate(Host, ServerHost). - -options() -> - [{node_type, leaf} | node_hometree:options()]. - -features() -> - [<<"multi-collection">> | node_hometree:features()]. - -create_node_permission(_Host, _ServerHost, _Node, - _ParentNode, _Owner, _Access) -> - {result, true}. - -create_node(NodeID, Owner) -> - node_hometree:create_node(NodeID, Owner). - -delete_node(Removed) -> - node_hometree:delete_node(Removed). - -subscribe_node(NodeID, Sender, Subscriber, AccessModel, - SendLast, PresenceSubscription, RosterGroup, Options) -> - node_hometree:subscribe_node(NodeID, Sender, Subscriber, - AccessModel, SendLast, PresenceSubscription, - RosterGroup, Options). - -unsubscribe_node(NodeID, Sender, Subscriber, SubID) -> - node_hometree:unsubscribe_node(NodeID, Sender, - Subscriber, SubID). - -publish_item(NodeID, Publisher, Model, MaxItems, ItemID, - Payload) -> - case nodetree_dag:get_node(NodeID) of - #pubsub_node{options = Options} -> - case find_opt(node_type, Options) of - collection -> - {error, - ?ERR_EXTENDED((?ERR_NOT_ALLOWED), <<"publish">>)}; - _ -> - node_hometree:publish_item(NodeID, Publisher, Model, - MaxItems, ItemID, Payload) - end; - Err -> Err - end. - -find_opt(_, []) -> false; -find_opt(Option, [{Option, Value} | _]) -> Value; -find_opt(Option, [_ | T]) -> find_opt(Option, T). - -remove_extra_items(NodeID, MaxItems, ItemIDs) -> - node_hometree:remove_extra_items(NodeID, MaxItems, - ItemIDs). - -delete_item(NodeID, Publisher, PublishModel, ItemID) -> - node_hometree:delete_item(NodeID, Publisher, - PublishModel, ItemID). - -purge_node(NodeID, Owner) -> - node_hometree:purge_node(NodeID, Owner). - -get_entity_affiliations(Host, Owner) -> - node_hometree:get_entity_affiliations(Host, Owner). - -get_node_affiliations(NodeID) -> - node_hometree:get_node_affiliations(NodeID). - -get_affiliation(NodeID, Owner) -> - node_hometree:get_affiliation(NodeID, Owner). - -set_affiliation(NodeID, Owner, Affiliation) -> - node_hometree:set_affiliation(NodeID, Owner, - Affiliation). - -get_entity_subscriptions(Host, Owner) -> - node_hometree:get_entity_subscriptions(Host, Owner). - -get_node_subscriptions(NodeID) -> - node_hometree:get_node_subscriptions(NodeID). - -get_subscriptions(NodeID, Owner) -> - node_hometree:get_subscriptions(NodeID, Owner). - -set_subscriptions(NodeID, Owner, Subscription, SubID) -> - node_hometree:set_subscriptions(NodeID, Owner, - Subscription, SubID). - -get_pending_nodes(Host, Owner) -> - node_hometree:get_pending_nodes(Host, Owner). - -get_states(NodeID) -> node_hometree:get_states(NodeID). - -get_state(NodeID, JID) -> - node_hometree:get_state(NodeID, JID). - -set_state(State) -> node_hometree:set_state(State). - -get_items(NodeID, From) -> - node_hometree:get_items(NodeID, From). - -get_items(NodeID, JID, AccessModel, - PresenceSubscription, RosterGroup, SubID) -> - node_hometree:get_items(NodeID, JID, AccessModel, - PresenceSubscription, RosterGroup, SubID). - -get_item(NodeID, ItemID) -> - node_hometree:get_item(NodeID, ItemID). - -get_item(NodeID, ItemID, JID, AccessModel, - PresenceSubscription, RosterGroup, SubID) -> - node_hometree:get_item(NodeID, ItemID, JID, AccessModel, - PresenceSubscription, RosterGroup, SubID). - -set_item(Item) -> node_hometree:set_item(Item). - -get_item_name(Host, Node, ID) -> - node_hometree:get_item_name(Host, Node, ID). - -node_to_path(Node) -> node_hometree:node_to_path(Node). - -path_to_node(Path) -> node_hometree:path_to_node(Path). diff --git a/src/node_dispatch.erl b/src/node_dispatch.erl deleted file mode 100644 index 3bf20cc63..000000000 --- a/src/node_dispatch.erl +++ /dev/null @@ -1,176 +0,0 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. -%%% -%%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. -%%% -%%% -%%% The Initial Developer of the Original Code is ProcessOne. -%%% Portions created by ProcessOne are Copyright 2006-2015, ProcessOne -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2015, ProcessOne. -%%% -%%% -%%% @copyright 2006-2015 ProcessOne -%%% @author Christophe romain -%%% [http://www.process-one.net/] -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== - --module(node_dispatch). - --author('christophe.romain@process-one.net'). - --include("pubsub.hrl"). - --include("jlib.hrl"). - --behaviour(gen_pubsub_node). - -%%% @doc

    The {@module} module is a PubSub plugin whose -%%% goal is to republished each published item to all its children.

    -%%%

    Users cannot subscribe to this node, but are supposed to subscribe to -%%% its children.

    -%%% This module can not work with virtual nodetree - -%% API definition --export([init/3, terminate/2, options/0, features/0, - create_node_permission/6, create_node/2, delete_node/1, - purge_node/2, subscribe_node/8, unsubscribe_node/4, - publish_item/6, delete_item/4, remove_extra_items/3, - get_entity_affiliations/2, get_node_affiliations/1, - get_affiliation/2, set_affiliation/3, - get_entity_subscriptions/2, get_node_subscriptions/1, - get_subscriptions/2, set_subscriptions/4, - get_pending_nodes/2, get_states/1, get_state/2, - set_state/1, get_items/6, get_items/2, get_item/7, - get_item/2, set_item/1, get_item_name/3, node_to_path/1, - path_to_node/1]). - -init(Host, ServerHost, Opts) -> - node_hometree:init(Host, ServerHost, Opts). - -terminate(Host, ServerHost) -> - node_hometree:terminate(Host, ServerHost). - -options() -> - [{deliver_payloads, true}, {notify_config, false}, - {notify_delete, false}, {notify_retract, true}, - {purge_offline, false}, {persist_items, true}, - {max_items, ?MAXITEMS}, {subscribe, true}, - {access_model, open}, {roster_groups_allowed, []}, - {publish_model, publishers}, - {notification_type, headline}, - {max_payload_size, ?MAX_PAYLOAD_SIZE}, - {send_last_published_item, never}, - {deliver_notifications, true}, - {presence_based_delivery, false}]. - -features() -> - [<<"create-nodes">>, <<"delete-nodes">>, - <<"instant-nodes">>, <<"outcast-affiliation">>, - <<"persistent-items">>, <<"publish">>, - <<"retrieve-items">>]. - -create_node_permission(Host, ServerHost, Node, - ParentNode, Owner, Access) -> - node_hometree:create_node_permission(Host, ServerHost, - Node, ParentNode, Owner, Access). - -create_node(NodeId, Owner) -> - node_hometree:create_node(NodeId, Owner). - -delete_node(Removed) -> - node_hometree:delete_node(Removed). - -subscribe_node(_NodeId, _Sender, _Subscriber, - _AccessModel, _SendLast, _PresenceSubscription, - _RosterGroup, _Options) -> - {error, ?ERR_FORBIDDEN}. - -unsubscribe_node(_NodeId, _Sender, _Subscriber, - _SubID) -> - {error, ?ERR_FORBIDDEN}. - -publish_item(NodeId, Publisher, Model, MaxItems, ItemId, - Payload) -> - lists:foreach(fun (SubNode) -> - node_hometree:publish_item(SubNode#pubsub_node.id, - Publisher, Model, MaxItems, - ItemId, Payload) - end, - nodetree_tree:get_subnodes(NodeId, Publisher, - Publisher)). - -remove_extra_items(_NodeId, _MaxItems, ItemIds) -> - {result, {ItemIds, []}}. - -delete_item(_NodeId, _Publisher, _PublishModel, - _ItemId) -> - {error, ?ERR_ITEM_NOT_FOUND}. - -purge_node(_NodeId, _Owner) -> {error, ?ERR_FORBIDDEN}. - -get_entity_affiliations(_Host, _Owner) -> {result, []}. - -get_node_affiliations(_NodeId) -> {result, []}. - -get_affiliation(_NodeId, _Owner) -> {result, []}. - -set_affiliation(NodeId, Owner, Affiliation) -> - node_hometree:set_affiliation(NodeId, Owner, - Affiliation). - -get_entity_subscriptions(_Host, _Owner) -> {result, []}. - -get_node_subscriptions(NodeId) -> - node_hometree:get_node_subscriptions(NodeId). - -get_subscriptions(_NodeId, _Owner) -> {result, []}. - -set_subscriptions(NodeId, Owner, Subscription, SubId) -> - node_hometree:set_subscriptions(NodeId, Owner, - Subscription, SubId). - -get_pending_nodes(Host, Owner) -> - node_hometree:get_pending_nodes(Host, Owner). - -get_states(NodeId) -> node_hometree:get_states(NodeId). - -get_state(NodeId, JID) -> - node_hometree:get_state(NodeId, JID). - -set_state(State) -> node_hometree:set_state(State). - -get_items(NodeId, From) -> - node_hometree:get_items(NodeId, From). - -get_items(NodeId, JID, AccessModel, - PresenceSubscription, RosterGroup, SubId) -> - node_hometree:get_items(NodeId, JID, AccessModel, - PresenceSubscription, RosterGroup, SubId). - -get_item(NodeId, ItemId) -> - node_hometree:get_item(NodeId, ItemId). - -get_item(NodeId, ItemId, JID, AccessModel, - PresenceSubscription, RosterGroup, SubId) -> - node_hometree:get_item(NodeId, ItemId, JID, AccessModel, - PresenceSubscription, RosterGroup, SubId). - -set_item(Item) -> node_hometree:set_item(Item). - -get_item_name(Host, Node, Id) -> - node_hometree:get_item_name(Host, Node, Id). - -node_to_path(Node) -> node_flat:node_to_path(Node). - -path_to_node(Path) -> node_flat:path_to_node(Path). diff --git a/src/node_flat.erl b/src/node_flat.erl index ff37f13e5..7093d4beb 100644 --- a/src/node_flat.erl +++ b/src/node_flat.erl @@ -1,186 +1,995 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. -%%% +%%%---------------------------------------------------------------------- +%%% File : node_flat.erl +%%% Author : Christophe Romain +%%% Purpose : Standard PubSub node plugin +%%% Created : 1 Dec 2007 by Christophe Romain %%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. -%%% %%% -%%% The Initial Developer of the Original Code is ProcessOne. -%%% Portions created by ProcessOne are Copyright 2006-2015, ProcessOne -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2015, ProcessOne. +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% -%%% @copyright 2006-2015 ProcessOne -%%% @author Christophe Romain -%%% [http://www.process-one.net/] -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== +%%% 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. +%%% +%%%---------------------------------------------------------------------- + +%%% @doc The module {@module} is the default PubSub plugin. +%%%

    It is used as a default for all unknown PubSub node type. It can serve +%%% as a developer basis and reference to build its own custom pubsub node +%%% types.

    +%%%

    PubSub plugin nodes are using the {@link gen_node} behaviour.

    -module(node_flat). - +-behaviour(gen_pubsub_node). -author('christophe.romain@process-one.net'). -include("pubsub.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). --include("jlib.hrl"). - --behaviour(gen_pubsub_node). - -%% API definition -export([init/3, terminate/2, options/0, features/0, - create_node_permission/6, create_node/2, delete_node/1, - purge_node/2, subscribe_node/8, unsubscribe_node/4, - publish_item/6, delete_item/4, remove_extra_items/3, - get_entity_affiliations/2, get_node_affiliations/1, - get_affiliation/2, set_affiliation/3, - get_entity_subscriptions/2, get_node_subscriptions/1, - get_subscriptions/2, set_subscriptions/4, - get_pending_nodes/2, get_states/1, get_state/2, - set_state/1, get_items/6, get_items/2, get_item/7, - get_item/2, set_item/1, get_item_name/3, node_to_path/1, - path_to_node/1]). + 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_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, + get_subscriptions/2, set_subscriptions/4, + get_pending_nodes/2, get_states/1, get_state/2, + set_state/1, get_items/7, get_items/3, get_item/7, + get_last_items/3, get_only_item/2, + get_item/2, set_item/1, get_item_name/3, node_to_path/1, + path_to_node/1, can_fetch_item/2, is_subscribed/1, transform/1]). -init(Host, ServerHost, Opts) -> - node_hometree:init(Host, ServerHost, Opts). +init(_Host, _ServerHost, _Opts) -> + %pubsub_subscription:init(Host, ServerHost, Opts), + ejabberd_mnesia:create(?MODULE, pubsub_state, + [{disc_copies, [node()]}, {index, [nodeidx]}, + {type, ordered_set}, + {attributes, record_info(fields, pubsub_state)}]), + ejabberd_mnesia:create(?MODULE, pubsub_item, + [{disc_only_copies, [node()]}, {index, [nodeidx]}, + {attributes, record_info(fields, pubsub_item)}]), + ejabberd_mnesia:create(?MODULE, pubsub_orphan, + [{disc_copies, [node()]}, + {attributes, record_info(fields, pubsub_orphan)}]), + ItemsFields = record_info(fields, pubsub_item), + case mnesia:table_info(pubsub_item, attributes) of + ItemsFields -> ok; + _ -> mnesia:transform_table(pubsub_item, ignore, ItemsFields) + end, + ok. -terminate(Host, ServerHost) -> - node_hometree:terminate(Host, ServerHost). +terminate(_Host, _ServerHost) -> + ok. options() -> - [{deliver_payloads, true}, {notify_config, false}, - {notify_delete, false}, {notify_retract, true}, - {purge_offline, false}, {persist_items, true}, - {max_items, ?MAXITEMS}, {subscribe, true}, - {access_model, open}, {roster_groups_allowed, []}, - {publish_model, publishers}, - {notification_type, headline}, - {max_payload_size, ?MAX_PAYLOAD_SIZE}, - {send_last_published_item, on_sub_and_presence}, - {deliver_notifications, true}, - {presence_based_delivery, false}]. + [{deliver_payloads, true}, + {notify_config, false}, + {notify_delete, false}, + {notify_retract, true}, + {purge_offline, false}, + {persist_items, true}, + {max_items, ?MAXITEMS}, + {subscribe, true}, + {access_model, open}, + {roster_groups_allowed, []}, + {publish_model, publishers}, + {notification_type, headline}, + {max_payload_size, ?MAX_PAYLOAD_SIZE}, + {send_last_published_item, on_sub_and_presence}, + {deliver_notifications, true}, + {presence_based_delivery, false}, + {itemreply, none}]. -features() -> node_hometree:features(). +features() -> + [<<"create-nodes">>, + <<"auto-create">>, + <<"access-authorize">>, + <<"delete-nodes">>, + <<"delete-items">>, + <<"get-pending">>, + <<"instant-nodes">>, + <<"manage-subscriptions">>, + <<"modify-affiliations">>, + <<"outcast-affiliation">>, + <<"persistent-items">>, + <<"multi-items">>, + <<"publish">>, + <<"publish-only-affiliation">>, + <<"publish-options">>, + <<"purge-nodes">>, + <<"retract-items">>, + <<"retrieve-affiliations">>, + <<"retrieve-items">>, + <<"retrieve-subscriptions">>, + <<"subscribe">>, + %%<<"subscription-options">>, + <<"subscription-notifications">>]. -%% use same code as node_hometree, but do not limite node to -%% the home/localhost/user/... hierarchy -%% any node is allowed -create_node_permission(Host, ServerHost, _Node, - _ParentNode, Owner, Access) -> - LOwner = jlib:jid_tolower(Owner), +%% @doc Checks if the current user has the permission to create the requested node +%%

    In flat node, any unused node name is allowed. The access parameter is also +%% checked. This parameter depends on the value of the +%% access_createnode ACL value in ejabberd config file.

    +create_node_permission(Host, ServerHost, _Node, _ParentNode, Owner, Access) -> + LOwner = jid:tolower(Owner), Allowed = case LOwner of - {<<"">>, Host, <<"">>} -> - true; % pubsub service always allowed - _ -> - acl:match_rule(ServerHost, Access, LOwner) =:= allow - end, + {<<"">>, Host, <<"">>} -> + true; % pubsub service always allowed + _ -> + acl:match_rule(ServerHost, Access, LOwner) =:= allow + end, {result, Allowed}. -create_node(NodeId, Owner) -> - node_hometree:create_node(NodeId, Owner). +create_node(Nidx, Owner) -> + OwnerKey = jid:tolower(jid:remove_resource(Owner)), + set_state(#pubsub_state{stateid = {OwnerKey, Nidx}, + nodeidx = Nidx, affiliation = owner}), + {result, {default, broadcast}}. -delete_node(Removed) -> - node_hometree:delete_node(Removed). +delete_node(Nodes) -> + Tr = fun (#pubsub_state{stateid = {J, _}, subscriptions = Ss}) -> + lists:map(fun (S) -> {J, S} end, Ss) + end, + Reply = lists:map(fun (#pubsub_node{id = Nidx} = PubsubNode) -> + {result, States} = get_states(Nidx), + lists:foreach(fun (State) -> + del_items(Nidx, State#pubsub_state.items), + del_state(State#pubsub_state{items = []}) + end, States), + del_orphan_items(Nidx), + {PubsubNode, lists:flatmap(Tr, States)} + end, Nodes), + {result, {default, broadcast, Reply}}. -subscribe_node(NodeId, Sender, Subscriber, AccessModel, - SendLast, PresenceSubscription, RosterGroup, Options) -> - node_hometree:subscribe_node(NodeId, Sender, Subscriber, - AccessModel, SendLast, PresenceSubscription, - RosterGroup, Options). +%% @doc

    Accepts or rejects subcription requests on a PubSub node.

    +%%

    The mechanism works as follow: +%%

      +%%
    • The main PubSub module prepares the subscription and passes the +%% result of the preparation as a record.
    • +%%
    • This function gets the prepared record and several other parameters and +%% can decide to:
        +%%
      • reject the subscription;
      • +%%
      • allow it as is, letting the main module perform the database +%% persistence;
      • +%%
      • allow it, modifying the record. The main module will store the +%% modified record;
      • +%%
      • allow it, but perform the needed persistence operations.
      +%%

    +%%

    The selected behaviour depends on the return parameter: +%%

      +%%
    • {error, Reason}: an IQ error result will be returned. No +%% subscription will actually be performed.
    • +%%
    • true: Subscribe operation is allowed, based on the +%% unmodified record passed in parameter SubscribeResult. If this +%% parameter contains an error, no subscription will be performed.
    • +%%
    • {true, PubsubState}: Subscribe operation is allowed, but +%% the {@link mod_pubsub:pubsubState()} record returned replaces the value +%% passed in parameter SubscribeResult.
    • +%%
    • {true, done}: Subscribe operation is allowed, but the +%% {@link mod_pubsub:pubsubState()} will be considered as already stored and +%% no further persistence operation will be performed. This case is used, +%% when the plugin module is doing the persistence by itself or when it want +%% to completely disable persistence.
    +%%

    +%%

    In the default plugin module, the record is unchanged.

    +subscribe_node(Nidx, Sender, Subscriber, AccessModel, + SendLast, PresenceSubscription, RosterGroup, _Options) -> + SubKey = jid:tolower(Subscriber), + GenKey = jid:remove_resource(SubKey), + Authorized = jid:tolower(jid:remove_resource(Sender)) == GenKey, + GenState = get_state(Nidx, GenKey), + SubState = case SubKey of + GenKey -> GenState; + _ -> get_state(Nidx, SubKey) + end, + Affiliation = GenState#pubsub_state.affiliation, + Subscriptions = SubState#pubsub_state.subscriptions, + Whitelisted = lists:member(Affiliation, [member, publisher, owner]), + PendingSubscription = lists:any(fun + ({pending, _}) -> true; + (_) -> false + end, + Subscriptions), + Owner = Affiliation == owner, + if not Authorized -> + {error, + mod_pubsub:extended_error((xmpp:err_bad_request()), mod_pubsub:err_invalid_jid())}; + (Affiliation == outcast) or (Affiliation == publish_only) -> + {error, xmpp:err_forbidden()}; + PendingSubscription -> + {error, + mod_pubsub:extended_error((xmpp:err_not_authorized()), mod_pubsub:err_pending_subscription())}; + (AccessModel == presence) and (not PresenceSubscription) and (not Owner) -> + {error, + mod_pubsub:extended_error((xmpp:err_not_authorized()), mod_pubsub:err_presence_subscription_required())}; + (AccessModel == roster) and (not RosterGroup) and (not Owner) -> + {error, + mod_pubsub:extended_error((xmpp:err_not_authorized()), mod_pubsub:err_not_in_roster_group())}; + (AccessModel == whitelist) and (not Whitelisted) and (not Owner) -> + {error, + mod_pubsub:extended_error((xmpp:err_not_allowed()), mod_pubsub:err_closed_node())}; + %%MustPay -> + %% % Payment is required for a subscription + %% {error, ?ERR_PAYMENT_REQUIRED}; + %%ForbiddenAnonymous -> + %% % Requesting entity is anonymous + %% {error, xmpp:err_forbidden()}; + true -> + %%SubId = pubsub_subscription:add_subscription(Subscriber, Nidx, Options), + {NewSub, SubId} = case Subscriptions of + [{subscribed, Id}|_] -> + {subscribed, Id}; + [] -> + Id = pubsub_subscription:make_subid(), + Sub = case AccessModel of + authorize -> pending; + _ -> subscribed + end, + set_state(SubState#pubsub_state{subscriptions = + [{Sub, Id} | Subscriptions]}), + {Sub, Id} + end, + case {NewSub, SendLast} of + {subscribed, never} -> + {result, {default, subscribed, SubId}}; + {subscribed, _} -> + {result, {default, subscribed, SubId, send_last}}; + {_, _} -> + {result, {default, pending, SubId}} + end + end. -unsubscribe_node(NodeId, Sender, Subscriber, SubID) -> - node_hometree:unsubscribe_node(NodeId, Sender, - Subscriber, SubID). +%% @doc

    Unsubscribe the Subscriber from the Node.

    +unsubscribe_node(Nidx, Sender, Subscriber, SubId) -> + SubKey = jid:tolower(Subscriber), + GenKey = jid:remove_resource(SubKey), + Authorized = jid:tolower(jid:remove_resource(Sender)) == GenKey, + GenState = get_state(Nidx, GenKey), + SubState = case SubKey of + GenKey -> GenState; + _ -> get_state(Nidx, SubKey) + end, + Subscriptions = lists:filter(fun + ({_Sub, _SubId}) -> true; + (_SubId) -> false + end, + SubState#pubsub_state.subscriptions), + SubIdExists = case SubId of + <<>> -> false; + Binary when is_binary(Binary) -> true; + _ -> false + end, + if + %% Requesting entity is prohibited from unsubscribing entity + not Authorized -> + {error, xmpp:err_forbidden()}; + %% Entity did not specify SubId + %%SubId == "", ?? -> + %% {error, mod_pubsub:extended_error(xmpp:err_bad_request(), "subid-required")}; + %% Invalid subscription identifier + %%InvalidSubId -> + %% {error, mod_pubsub:extended_error(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; + %% Requesting entity is not a subscriber + Subscriptions == [] -> + {error, + mod_pubsub:extended_error(xmpp:err_unexpected_request(), mod_pubsub:err_not_subscribed())}; + %% Subid supplied, so use that. + SubIdExists -> + Sub = first_in_list(fun + ({_, S}) when S == SubId -> true; + (_) -> false + end, + SubState#pubsub_state.subscriptions), + case Sub of + {value, S} -> + delete_subscriptions(SubState, [S]), + {result, default}; + false -> + {error, + mod_pubsub:extended_error(xmpp:err_unexpected_request(), mod_pubsub:err_not_subscribed())} + end; + %% Asking to remove all subscriptions to the given node + SubId == all -> + delete_subscriptions(SubState, Subscriptions), + {result, default}; + %% No subid supplied, but there's only one matching subscription + length(Subscriptions) == 1 -> + delete_subscriptions(SubState, Subscriptions), + {result, default}; + %% No subid and more than one possible subscription match. + true -> + {error, + mod_pubsub:extended_error((xmpp:err_bad_request()), mod_pubsub:err_subid_required())} + end. -publish_item(NodeId, Publisher, Model, MaxItems, ItemId, - Payload) -> - node_hometree:publish_item(NodeId, Publisher, Model, - MaxItems, ItemId, Payload). +delete_subscriptions(SubState, Subscriptions) -> + NewSubs = lists:foldl(fun ({Subscription, SubId}, Acc) -> + %%pubsub_subscription:delete_subscription(SubKey, Nidx, SubId), + Acc -- [{Subscription, SubId}] + end, SubState#pubsub_state.subscriptions, Subscriptions), + case {SubState#pubsub_state.affiliation, NewSubs} of + {none, []} -> del_state(SubState); + _ -> set_state(SubState#pubsub_state{subscriptions = NewSubs}) + end. -remove_extra_items(NodeId, MaxItems, ItemIds) -> - node_hometree:remove_extra_items(NodeId, MaxItems, - ItemIds). +%% @doc

    Publishes the item passed as parameter.

    +%%

    The mechanism works as follow: +%%

      +%%
    • The main PubSub module prepares the item to publish and passes the +%% result of the preparation as a {@link mod_pubsub:pubsubItem()} record.
    • +%%
    • This function gets the prepared record and several other parameters and can decide to:
        +%%
      • reject the publication;
      • +%%
      • allow the publication as is, letting the main module perform the database persistence;
      • +%%
      • allow the publication, modifying the record. The main module will store the modified record;
      • +%%
      • allow it, but perform the needed persistence operations.
      +%%

    +%%

    The selected behaviour depends on the return parameter: +%%

      +%%
    • {error, Reason}: an iq error result will be return. No +%% publication is actually performed.
    • +%%
    • true: Publication operation is allowed, based on the +%% unmodified record passed in parameter Item. If the Item +%% parameter contains an error, no subscription will actually be +%% performed.
    • +%%
    • {true, Item}: Publication operation is allowed, but the +%% {@link mod_pubsub:pubsubItem()} record returned replaces the value passed +%% in parameter Item. The persistence will be performed by the main +%% module.
    • +%%
    • {true, done}: Publication operation is allowed, but the +%% {@link mod_pubsub:pubsubItem()} will be considered as already stored and +%% no further persistence operation will be performed. This case is used, +%% when the plugin module is doing the persistence by itself or when it want +%% to completely disable persistence.
    +%%

    +%%

    In the default plugin module, the record is unchanged.

    +publish_item(Nidx, Publisher, PublishModel, MaxItems, ItemId, Payload, + _PubOpts) -> + SubKey = jid:tolower(Publisher), + GenKey = jid:remove_resource(SubKey), + GenState = get_state(Nidx, GenKey), + SubState = case SubKey of + GenKey -> GenState; + _ -> get_state(Nidx, SubKey) + end, + Affiliation = GenState#pubsub_state.affiliation, + Subscribed = case PublishModel of + subscribers -> is_subscribed(GenState#pubsub_state.subscriptions) orelse + is_subscribed(SubState#pubsub_state.subscriptions); + _ -> undefined + end, + if not ((PublishModel == open) or + (PublishModel == publishers) and + ((Affiliation == owner) + or (Affiliation == publisher) + or (Affiliation == publish_only)) + or (Subscribed == true)) -> + {error, xmpp:err_forbidden()}; + true -> + if MaxItems > 0; + MaxItems == unlimited -> + Now = erlang:timestamp(), + case get_item(Nidx, ItemId) of + {result, #pubsub_item{creation = {_, GenKey}} = OldItem} -> + set_item(OldItem#pubsub_item{ + modification = {Now, SubKey}, + payload = Payload}), + {result, {default, broadcast, []}}; + % Allow node owner to modify any item, he can also delete it and recreate + {result, #pubsub_item{creation = {CreationTime, _}} = OldItem} when Affiliation == owner-> + set_item(OldItem#pubsub_item{ + creation = {CreationTime, GenKey}, + modification = {Now, SubKey}, + payload = Payload}), + {result, {default, broadcast, []}}; + {result, _} -> + {error, xmpp:err_forbidden()}; + _ -> + Items = [ItemId | GenState#pubsub_state.items], + {result, {NI, OI}} = remove_extra_items(Nidx, MaxItems, Items), + set_state(GenState#pubsub_state{items = NI}), + set_item(#pubsub_item{ + itemid = {ItemId, Nidx}, + nodeidx = Nidx, + creation = {Now, GenKey}, + modification = {Now, SubKey}, + payload = Payload}), + {result, {default, broadcast, OI}} + end; + true -> + {result, {default, broadcast, []}} + end + end. -delete_item(NodeId, Publisher, PublishModel, ItemId) -> - node_hometree:delete_item(NodeId, Publisher, - PublishModel, ItemId). +remove_extra_items(Nidx, MaxItems) -> + {result, States} = get_states(Nidx), + Records = States ++ mnesia:read({pubsub_orphan, Nidx}), + ItemIds = lists:flatmap(fun(#pubsub_state{items = Is}) -> + Is; + (#pubsub_orphan{items = Is}) -> + Is + end, Records), + remove_extra_items(Nidx, MaxItems, ItemIds). -purge_node(NodeId, Owner) -> - node_hometree:purge_node(NodeId, Owner). +%% @doc

    This function is used to remove extra items, most notably when the +%% maximum number of items has been reached.

    +%%

    This function is used internally by the core PubSub module, as no +%% permission check is performed.

    +%%

    In the default plugin module, the oldest items are removed, but other +%% rules can be used.

    +%%

    If another PubSub plugin wants to delegate the item removal (and if the +%% plugin is using the default pubsub storage), it can implements this function like this: +%% ```remove_extra_items(Nidx, MaxItems, ItemIds) -> +%% node_default:remove_extra_items(Nidx, MaxItems, ItemIds).'''

    +remove_extra_items(_Nidx, unlimited, ItemIds) -> + {result, {ItemIds, []}}; +remove_extra_items(Nidx, MaxItems, ItemIds) -> + NewItems = lists:sublist(ItemIds, MaxItems), + OldItems = lists:nthtail(length(NewItems), 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

    Triggers item deletion.

    +%%

    Default plugin: The user performing the deletion must be the node owner +%% or a publisher, or PublishModel being open.

    +delete_item(Nidx, Publisher, PublishModel, ItemId) -> + SubKey = jid:tolower(Publisher), + GenKey = jid:remove_resource(SubKey), + GenState = get_state(Nidx, GenKey), + #pubsub_state{affiliation = Affiliation, items = Items} = GenState, + Allowed = Affiliation == publisher orelse + Affiliation == owner orelse + (PublishModel == open andalso + case get_item(Nidx, ItemId) of + {result, #pubsub_item{creation = {_, GenKey}}} -> true; + _ -> false + end), + if not Allowed -> + {error, xmpp:err_forbidden()}; + true -> + case lists:member(ItemId, Items) of + true -> + del_item(Nidx, ItemId), + set_state(GenState#pubsub_state{items = lists:delete(ItemId, Items)}), + {result, {default, broadcast}}; + false -> + case Affiliation of + owner -> + {result, States} = get_states(Nidx), + Records = States ++ mnesia:read({pubsub_orphan, Nidx}), + lists:foldl(fun + (#pubsub_state{items = RI} = S, Res) -> + case lists:member(ItemId, RI) of + true -> + NI = lists:delete(ItemId, RI), + del_item(Nidx, ItemId), + mnesia:write(S#pubsub_state{items = NI}), + {result, {default, broadcast}}; + false -> + Res + end; + (#pubsub_orphan{items = RI} = S, Res) -> + case lists:member(ItemId, RI) of + true -> + NI = lists:delete(ItemId, RI), + del_item(Nidx, ItemId), + mnesia:write(S#pubsub_orphan{items = NI}), + {result, {default, broadcast}}; + false -> + Res + end + end, + {error, xmpp:err_item_not_found()}, Records); + _ -> + {error, xmpp:err_forbidden()} + end + end + end. + +purge_node(Nidx, Owner) -> + SubKey = jid:tolower(Owner), + GenKey = jid:remove_resource(SubKey), + GenState = get_state(Nidx, GenKey), + case GenState of + #pubsub_state{affiliation = owner} -> + {result, States} = get_states(Nidx), + lists:foreach(fun + (#pubsub_state{items = []}) -> + ok; + (#pubsub_state{items = Items} = S) -> + del_items(Nidx, Items), + set_state(S#pubsub_state{items = []}) + end, + States), + del_orphan_items(Nidx), + {result, {default, broadcast}}; + _ -> + {error, xmpp:err_forbidden()} + end. + +%% @doc

    Return the current affiliations for the given user

    +%%

    The default module reads affiliations in the main Mnesia +%% pubsub_state table. If a plugin stores its data in the same +%% table, it should return an empty list, as the affiliation will be read by +%% the default PubSub module. Otherwise, it should return its own affiliation, +%% that will be added to the affiliation stored in the main +%% pubsub_state table.

    get_entity_affiliations(Host, Owner) -> - node_hometree:get_entity_affiliations(Host, Owner). + SubKey = jid:tolower(Owner), + GenKey = jid:remove_resource(SubKey), + States = mnesia:match_object(#pubsub_state{stateid = {GenKey, '_'}, _ = '_'}), + NodeTree = mod_pubsub:tree(Host), + Reply = lists:foldl(fun (#pubsub_state{stateid = {_, N}, affiliation = A}, Acc) -> + case NodeTree:get_node(N) of + #pubsub_node{nodeid = {Host, _}} = Node -> [{Node, A} | Acc]; + _ -> Acc + end + end, + [], States), + {result, Reply}. -get_node_affiliations(NodeId) -> - node_hometree:get_node_affiliations(NodeId). +get_node_affiliations(Nidx) -> + {result, States} = get_states(Nidx), + Tr = fun (#pubsub_state{stateid = {J, _}, affiliation = A}) -> {J, A} end, + {result, lists:map(Tr, States)}. -get_affiliation(NodeId, Owner) -> - node_hometree:get_affiliation(NodeId, Owner). +get_affiliation(Nidx, Owner) -> + SubKey = jid:tolower(Owner), + GenKey = jid:remove_resource(SubKey), + #pubsub_state{affiliation = Affiliation} = get_state(Nidx, GenKey), + {result, Affiliation}. -set_affiliation(NodeId, Owner, Affiliation) -> - node_hometree:set_affiliation(NodeId, Owner, - Affiliation). +set_affiliation(Nidx, Owner, Affiliation) -> + SubKey = jid:tolower(Owner), + GenKey = jid:remove_resource(SubKey), + GenState = get_state(Nidx, GenKey), + case {Affiliation, GenState#pubsub_state.subscriptions} of + {none, []} -> {result, del_state(GenState)}; + _ -> {result, set_state(GenState#pubsub_state{affiliation = Affiliation})} + end. +%% @doc

    Return the current subscriptions for the given user

    +%%

    The default module reads subscriptions in the main Mnesia +%% pubsub_state table. If a plugin stores its data in the same +%% table, it should return an empty list, as the affiliation will be read by +%% the default PubSub module. Otherwise, it should return its own affiliation, +%% that will be added to the affiliation stored in the main +%% pubsub_state table.

    get_entity_subscriptions(Host, Owner) -> - node_hometree:get_entity_subscriptions(Host, Owner). + {U, D, _} = SubKey = jid:tolower(Owner), + GenKey = jid:remove_resource(SubKey), + States = case SubKey of + GenKey -> + mnesia:match_object(#pubsub_state{stateid = {{U, D, '_'}, '_'}, _ = '_'}); + _ -> + mnesia:match_object(#pubsub_state{stateid = {GenKey, '_'}, _ = '_'}) + ++ + mnesia:match_object(#pubsub_state{stateid = {SubKey, '_'}, _ = '_'}) + end, + NodeTree = mod_pubsub:tree(Host), + Reply = lists:foldl(fun (#pubsub_state{stateid = {J, N}, subscriptions = Ss}, Acc) -> + case NodeTree:get_node(N) of + #pubsub_node{nodeid = {Host, _}} = Node -> + lists:foldl(fun ({Sub, SubId}, Acc2) -> + [{Node, Sub, SubId, J} | Acc2] + end, + Acc, Ss); + _ -> + Acc + end + end, + [], States), + {result, Reply}. -get_node_subscriptions(NodeId) -> - node_hometree:get_node_subscriptions(NodeId). +get_node_subscriptions(Nidx) -> + {result, States} = get_states(Nidx), + Tr = fun (#pubsub_state{stateid = {J, _}, subscriptions = Subscriptions}) -> + lists:foldl(fun ({S, SubId}, Acc) -> + [{J, S, SubId} | Acc] + end, + [], Subscriptions) + end, + {result, lists:flatmap(Tr, States)}. -get_subscriptions(NodeId, Owner) -> - node_hometree:get_subscriptions(NodeId, Owner). +get_subscriptions(Nidx, Owner) -> + SubKey = jid:tolower(Owner), + SubState = get_state(Nidx, SubKey), + {result, SubState#pubsub_state.subscriptions}. -set_subscriptions(NodeId, Owner, Subscription, SubId) -> - node_hometree:set_subscriptions(NodeId, Owner, - Subscription, SubId). +set_subscriptions(Nidx, Owner, Subscription, SubId) -> + SubKey = jid:tolower(Owner), + SubState = get_state(Nidx, SubKey), + case {SubId, SubState#pubsub_state.subscriptions} of + {_, []} -> + case Subscription of + none -> + {error, + mod_pubsub:extended_error((xmpp:err_bad_request()), mod_pubsub:err_not_subscribed())}; + _ -> + new_subscription(Nidx, Owner, Subscription, SubState) + end; + {<<>>, [{_, SID}]} -> + case Subscription of + none -> unsub_with_subid(SubState, SID); + _ -> replace_subscription({Subscription, SID}, SubState) + end; + {<<>>, [_ | _]} -> + {error, + mod_pubsub:extended_error((xmpp:err_bad_request()), mod_pubsub:err_subid_required())}; + _ -> + case Subscription of + none -> unsub_with_subid(SubState, SubId); + _ -> replace_subscription({Subscription, SubId}, SubState) + end + end. +replace_subscription(NewSub, SubState) -> + NewSubs = replace_subscription(NewSub, SubState#pubsub_state.subscriptions, []), + {result, set_state(SubState#pubsub_state{subscriptions = NewSubs})}. + +replace_subscription(_, [], Acc) -> Acc; +replace_subscription({Sub, SubId}, [{_, SubId} | T], Acc) -> + replace_subscription({Sub, SubId}, T, [{Sub, SubId} | Acc]). + +new_subscription(_Nidx, _Owner, Sub, SubState) -> + %%SubId = pubsub_subscription:add_subscription(Owner, Nidx, []), + SubId = pubsub_subscription:make_subid(), + Subs = SubState#pubsub_state.subscriptions, + set_state(SubState#pubsub_state{subscriptions = [{Sub, SubId} | Subs]}), + {result, {Sub, SubId}}. + +unsub_with_subid(SubState, SubId) -> + %%pubsub_subscription:delete_subscription(SubState#pubsub_state.stateid, Nidx, SubId), + NewSubs = [{S, Sid} + || {S, Sid} <- SubState#pubsub_state.subscriptions, + SubId =/= Sid], + case {NewSubs, SubState#pubsub_state.affiliation} of + {[], none} -> {result, del_state(SubState)}; + _ -> {result, set_state(SubState#pubsub_state{subscriptions = NewSubs})} + end. + +%% @doc

    Returns a list of Owner's nodes on Host with pending +%% subscriptions.

    get_pending_nodes(Host, Owner) -> - node_hometree:get_pending_nodes(Host, Owner). + GenKey = jid:remove_resource(jid:tolower(Owner)), + States = mnesia:match_object(#pubsub_state{stateid = {GenKey, '_'}, + affiliation = owner, + _ = '_'}), + NodeIdxs = [Nidx || #pubsub_state{stateid = {_, Nidx}} <- States], + NodeTree = mod_pubsub:tree(Host), + Reply = mnesia:foldl(fun (#pubsub_state{stateid = {_, Nidx}} = S, Acc) -> + case lists:member(Nidx, NodeIdxs) of + true -> + case get_nodes_helper(NodeTree, S) of + {value, Node} -> [Node | Acc]; + false -> Acc + end; + false -> + Acc + end + end, + [], pubsub_state), + {result, Reply}. -get_states(NodeId) -> node_hometree:get_states(NodeId). +get_nodes_helper(NodeTree, #pubsub_state{stateid = {_, N}, subscriptions = Subs}) -> + HasPending = fun + ({pending, _}) -> true; + (pending) -> true; + (_) -> false + end, + case lists:any(HasPending, Subs) of + true -> + case NodeTree:get_node(N) of + #pubsub_node{nodeid = {_, Node}} -> {value, Node}; + _ -> false + end; + false -> + false + end. -get_state(NodeId, JID) -> - node_hometree:get_state(NodeId, JID). +%% @doc Returns the list of stored states for a given node. +%%

    For the default PubSub module, states are stored in Mnesia database.

    +%%

    We can consider that the pubsub_state table have been created by the main +%% mod_pubsub module.

    +%%

    PubSub plugins can store the states where they wants (for example in a +%% relational database).

    +%%

    If a PubSub plugin wants to delegate the states storage to the default node, +%% they can implement this function like this: +%% ```get_states(Nidx) -> +%% node_default:get_states(Nidx).'''

    +get_states(Nidx) -> + States = case catch mnesia:index_read(pubsub_state, Nidx, #pubsub_state.nodeidx) of + List when is_list(List) -> List; + _ -> [] + end, + {result, States}. -set_state(State) -> node_hometree:set_state(State). +%% @doc

    Returns a state (one state list), given its reference.

    +get_state(Nidx, Key) -> + StateId = {Key, Nidx}, + case catch mnesia:read({pubsub_state, StateId}) of + [State] when is_record(State, pubsub_state) -> State; + _ -> #pubsub_state{stateid = StateId, nodeidx = Nidx} + end. -get_items(NodeId, From) -> - node_hometree:get_items(NodeId, From). +%% @doc

    Write a state into database.

    +set_state(State) when is_record(State, pubsub_state) -> + mnesia:write(State). +%set_state(_) -> {error, ?ERR_INTERNAL_SERVER_ERROR}. -get_items(NodeId, JID, AccessModel, - PresenceSubscription, RosterGroup, SubId) -> - node_hometree:get_items(NodeId, JID, AccessModel, - PresenceSubscription, RosterGroup, SubId). +%% @doc

    Delete a state from database.

    +del_state(#pubsub_state{stateid = {Key, Nidx}, items = Items}) -> + case Items of + [] -> + ok; + _ -> + Orphan = #pubsub_orphan{nodeid = Nidx, items = + case mnesia:read({pubsub_orphan, Nidx}) of + [#pubsub_orphan{items = ItemIds}] -> + lists:usort(ItemIds++Items); + _ -> + Items + end}, + mnesia:write(Orphan) + end, + mnesia:delete({pubsub_state, {Key, Nidx}}). -get_item(NodeId, ItemId) -> - node_hometree:get_item(NodeId, ItemId). +%% @doc Returns the list of stored items for a given node. +%%

    For the default PubSub module, items are stored in Mnesia database.

    +%%

    We can consider that the pubsub_item table have been created by the main +%% mod_pubsub module.

    +%%

    PubSub plugins can store the items where they wants (for example in a +%% relational database), or they can even decide not to persist any items.

    +get_items(Nidx, _From, undefined) -> + RItems = lists:keysort(#pubsub_item.creation, + mnesia:index_read(pubsub_item, Nidx, #pubsub_item.nodeidx)), + {result, {RItems, undefined}}; -get_item(NodeId, ItemId, JID, AccessModel, - PresenceSubscription, RosterGroup, SubId) -> - node_hometree:get_item(NodeId, ItemId, JID, AccessModel, - PresenceSubscription, RosterGroup, SubId). +get_items(Nidx, _From, #rsm_set{max = Max, index = IncIndex, + 'after' = After, before = Before}) -> + case lists:keysort(#pubsub_item.creation, + mnesia:index_read(pubsub_item, Nidx, #pubsub_item.nodeidx)) of + [] -> + {result, {[], #rsm_set{count = 0}}}; + RItems -> + Count = length(RItems), + Limit = case Max of + undefined -> ?MAXITEMS; + _ -> Max + end, + {Offset, ItemsPage} = + case {IncIndex, Before, After} of + {undefined, undefined, undefined} -> + {0, lists:sublist(RItems, Limit)}; + {I, undefined, undefined} -> + SubList = lists:nthtail(I, RItems), + {I, lists:sublist(SubList, Limit)}; + {_, <<>>, undefined} -> + %% 2.5 Requesting the Last Page in a Result Set + SubList = lists:reverse(RItems), + {Count-Limit, lists:reverse(lists:sublist(SubList, Limit))}; + {_, Stamp, undefined} -> + BeforeNow = encode_stamp(Stamp), + {NewIndex, SubList} = extract_sublist(before_now, BeforeNow, + 0, lists:reverse(RItems)), + {Count-NewIndex-Limit, lists:reverse(lists:sublist(SubList, Limit))}; + {_, undefined, Stamp} -> + AfterNow = encode_stamp(Stamp), + {NewIndex, SubList} = extract_sublist(after_now, AfterNow, + 0, RItems), + {NewIndex, lists:sublist(SubList, Limit)} + end, + Rsm = rsm_page(Count, IncIndex, Offset, ItemsPage), + {result, {ItemsPage, Rsm}} + end. -set_item(Item) -> node_hometree:set_item(Item). +get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId, RSM) -> + SubKey = jid:tolower(JID), + GenKey = jid:remove_resource(SubKey), + GenState = get_state(Nidx, GenKey), + SubState = get_state(Nidx, SubKey), + Affiliation = GenState#pubsub_state.affiliation, + BareSubscriptions = GenState#pubsub_state.subscriptions, + FullSubscriptions = SubState#pubsub_state.subscriptions, + Whitelisted = can_fetch_item(Affiliation, BareSubscriptions) orelse + can_fetch_item(Affiliation, FullSubscriptions), + if %%SubId == "", ?? -> + %% Entity has multiple subscriptions to the node but does not specify a subscription ID + %{error, mod_pubsub:extended_error(xmpp:err_bad_request(), "subid-required")}; + %%InvalidSubId -> + %% Entity is subscribed but specifies an invalid subscription ID + %{error, mod_pubsub:extended_error(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; + (Affiliation == outcast) or (Affiliation == publish_only) -> + {error, xmpp:err_forbidden()}; + (AccessModel == presence) and not PresenceSubscription -> + {error, + mod_pubsub:extended_error((xmpp:err_not_authorized()), mod_pubsub:err_presence_subscription_required())}; + (AccessModel == roster) and not RosterGroup -> + {error, + mod_pubsub:extended_error((xmpp:err_not_authorized()), mod_pubsub:err_not_in_roster_group())}; + (AccessModel == whitelist) and not Whitelisted -> + {error, + mod_pubsub:extended_error((xmpp:err_not_allowed()), mod_pubsub:err_closed_node())}; + (AccessModel == authorize) and not Whitelisted -> + {error, xmpp:err_forbidden()}; + %%MustPay -> + %% % Payment is required for a subscription + %% {error, ?ERR_PAYMENT_REQUIRED}; + true -> + get_items(Nidx, JID, RSM) + end. -get_item_name(Host, Node, Id) -> - node_hometree:get_item_name(Host, Node, Id). +extract_sublist(A, Now, Index, [#pubsub_item{creation = {Creation, _}} | RItems]) + when ((A == before_now) and (Creation >= Now)) + or ((A == after_now) and (Creation =< Now)) -> + extract_sublist(A, Now, Index+1, RItems); +extract_sublist(_, _, Index, RItems) -> + {Index, RItems}. -node_to_path(Node) -> [(Node)]. +get_only_item(Nidx, From) -> + get_last_items(Nidx, From, 1). + +get_last_items(Nidx, _From, Count) when Count > 0 -> + Items = mnesia:index_read(pubsub_item, Nidx, #pubsub_item.nodeidx), + LastItems = lists:reverse(lists:keysort(#pubsub_item.modification, Items)), + {result, lists:sublist(LastItems, Count)}; +get_last_items(_Nidx, _From, _Count) -> + {result, []}. + +%% @doc

    Returns an item (one item list), given its reference.

    + +get_item(Nidx, ItemId) -> + case mnesia:read({pubsub_item, {ItemId, Nidx}}) of + [Item] when is_record(Item, pubsub_item) -> {result, Item}; + _ -> {error, xmpp:err_item_not_found()} + end. + +get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId) -> + SubKey = jid:tolower(JID), + GenKey = jid:remove_resource(SubKey), + GenState = get_state(Nidx, GenKey), + Affiliation = GenState#pubsub_state.affiliation, + Subscriptions = GenState#pubsub_state.subscriptions, + Whitelisted = can_fetch_item(Affiliation, Subscriptions), + if %%SubId == "", ?? -> + %% Entity has multiple subscriptions to the node but does not specify a subscription ID + %{error, mod_pubsub:extended_error(xmpp:err_bad_request(), "subid-required")}; + %%InvalidSubId -> + %% Entity is subscribed but specifies an invalid subscription ID + %{error, mod_pubsub:extended_error(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; + (Affiliation == outcast) or (Affiliation == publish_only) -> + {error, xmpp:err_forbidden()}; + (AccessModel == presence) and not PresenceSubscription -> + {error, + mod_pubsub:extended_error((xmpp:err_not_authorized()), mod_pubsub:err_presence_subscription_required())}; + (AccessModel == roster) and not RosterGroup -> + {error, + mod_pubsub:extended_error((xmpp:err_not_authorized()), mod_pubsub:err_not_in_roster_group())}; + (AccessModel == whitelist) and not Whitelisted -> + {error, + mod_pubsub:extended_error((xmpp:err_not_allowed()), mod_pubsub:err_closed_node())}; + (AccessModel == authorize) and not Whitelisted -> + {error, xmpp:err_forbidden()}; + %%MustPay -> + %% % Payment is required for a subscription + %% {error, ?ERR_PAYMENT_REQUIRED}; + true -> + get_item(Nidx, ItemId) + end. + +%% @doc

    Write an item into database.

    +set_item(Item) when is_record(Item, pubsub_item) -> + mnesia:write(Item). +%set_item(_) -> {error, ?ERR_INTERNAL_SERVER_ERROR}. + +%% @doc

    Delete an item from database.

    +del_item(Nidx, ItemId) -> + mnesia:delete({pubsub_item, {ItemId, Nidx}}). + +del_items(Nidx, ItemIds) -> + lists:foreach(fun (ItemId) -> del_item(Nidx, ItemId) + end, + ItemIds). + +del_orphan_items(Nidx) -> + case mnesia:read({pubsub_orphan, Nidx}) of + [#pubsub_orphan{items = ItemIds}] -> + del_items(Nidx, ItemIds), + mnesia:delete({pubsub_orphan, Nidx}); + _ -> + ok + end. + +get_item_name(_Host, _Node, Id) -> + {result, Id}. + +%% @doc

    Return the path of the node. In flat it's just node id.

    +node_to_path(Node) -> + {result, [Node]}. path_to_node(Path) -> - case Path of - % default slot - [Node] -> iolist_to_binary(Node); - % handle old possible entries, used when migrating database content to new format - [Node | _] when is_binary(Node) -> - iolist_to_binary(str:join([<<"">> | Path], <<"/">>)); - % default case (used by PEP for example) - _ -> iolist_to_binary(Path) + {result, + case Path of + %% default slot + [Node] -> iolist_to_binary(Node); + %% handle old possible entries, used when migrating database content to new format + [Node | _] when is_binary(Node) -> + iolist_to_binary(str:join([<<"">> | Path], <<"/">>)); + %% default case (used by PEP for example) + _ -> iolist_to_binary(Path) + end}. + +can_fetch_item(owner, _) -> true; +can_fetch_item(member, _) -> true; +can_fetch_item(publisher, _) -> true; +can_fetch_item(publish_only, _) -> false; +can_fetch_item(outcast, _) -> false; +can_fetch_item(none, Subscriptions) -> is_subscribed(Subscriptions). +%can_fetch_item(_Affiliation, _Subscription) -> false. + +is_subscribed(Subscriptions) -> + lists:any(fun + ({subscribed, _SubId}) -> true; + (_) -> false + end, + Subscriptions). + +first_in_list(_Pred, []) -> + false; +first_in_list(Pred, [H | T]) -> + case Pred(H) of + true -> {value, H}; + _ -> first_in_list(Pred, T) end. + +rsm_page(Count, _, _, []) -> + #rsm_set{count = Count}; +rsm_page(Count, Index, Offset, Items) -> + FirstItem = hd(Items), + LastItem = lists:last(Items), + First = decode_stamp(element(1, FirstItem#pubsub_item.creation)), + Last = decode_stamp(element(1, LastItem#pubsub_item.creation)), + #rsm_set{count = Count, index = Index, + first = #rsm_first{index = Offset, data = First}, + last = Last}. + +encode_stamp(Stamp) -> + try xmpp_util:decode_timestamp(Stamp) + catch _:{bad_timestamp, _} -> + Stamp % We should return a proper error to the client instead. + end. +decode_stamp(Stamp) -> + xmpp_util:encode_timestamp(Stamp). + +transform({pubsub_state, {Id, Nidx}, Is, A, Ss}) -> + {pubsub_state, {Id, Nidx}, Nidx, Is, A, Ss}; +transform({pubsub_item, {Id, Nidx}, C, M, P}) -> + {pubsub_item, {Id, Nidx}, Nidx, C, M, P}; +transform(Rec) -> + Rec. diff --git a/src/node_flat_odbc.erl b/src/node_flat_odbc.erl deleted file mode 100644 index 1baf38e71..000000000 --- a/src/node_flat_odbc.erl +++ /dev/null @@ -1,213 +0,0 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. -%%% -%%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. -%%% -%%% -%%% The Initial Developer of the Original Code is ProcessOne. -%%% Portions created by ProcessOne are Copyright 2006-2015, ProcessOne -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2015, ProcessOne. -%%% -%%% @copyright 2006-2015 ProcessOne -%%% @author Christophe Romain -%%% [http://www.process-one.net/] -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== - --module(node_flat_odbc). - --author('christophe.romain@process-one.net'). - --include("pubsub.hrl"). - --include("jlib.hrl"). - --behaviour(gen_pubsub_node). - -%% API definition --export([init/3, terminate/2, options/0, features/0, - create_node_permission/6, create_node/2, delete_node/1, - purge_node/2, subscribe_node/8, unsubscribe_node/4, - publish_item/6, delete_item/4, remove_extra_items/3, - get_entity_affiliations/2, get_node_affiliations/1, - get_affiliation/2, set_affiliation/3, - get_entity_subscriptions/2, - get_entity_subscriptions_for_send_last/2, - get_node_subscriptions/1, get_subscriptions/2, - set_subscriptions/4, get_pending_nodes/2, get_states/1, - get_state/2, set_state/1, get_items/7, get_items/6, - get_items/3, get_items/2, get_item/7, get_item/2, - set_item/1, get_item_name/3, get_last_items/3, - node_to_path/1, path_to_node/1]). - -init(Host, ServerHost, Opts) -> - node_hometree_odbc:init(Host, ServerHost, Opts). - -terminate(Host, ServerHost) -> - node_hometree_odbc:terminate(Host, ServerHost). - -options() -> - [{deliver_payloads, true}, - {notify_config, false}, - {notify_delete, false}, - {notify_retract, true}, - {purge_offline, false}, - {persist_items, true}, - {max_items, ?MAXITEMS}, - {subscribe, true}, - {access_model, open}, - {roster_groups_allowed, []}, - {publish_model, publishers}, - {notification_type, headline}, - {max_payload_size, ?MAX_PAYLOAD_SIZE}, - {send_last_published_item, on_sub_and_presence}, - {deliver_notifications, true}, - {presence_based_delivery, false}, {odbc, true}, - {rsm, true}]. - -features() -> node_hometree_odbc:features(). - -%% use same code as node_hometree_odbc, but do not limite node to -%% the home/localhost/user/... hierarchy -%% any node is allowed -create_node_permission(Host, ServerHost, _Node, - _ParentNode, Owner, Access) -> - LOwner = jlib:jid_tolower(Owner), - Allowed = case LOwner of - {<<"">>, Host, <<"">>} -> - true; % pubsub service always allowed - _ -> - acl:match_rule(ServerHost, Access, LOwner) =:= allow - end, - {result, Allowed}. - -create_node(NodeId, Owner) -> - node_hometree_odbc:create_node(NodeId, Owner). - -delete_node(Removed) -> - node_hometree_odbc:delete_node(Removed). - -subscribe_node(NodeId, Sender, Subscriber, AccessModel, - SendLast, PresenceSubscription, RosterGroup, Options) -> - node_hometree_odbc:subscribe_node(NodeId, Sender, - Subscriber, AccessModel, SendLast, - PresenceSubscription, RosterGroup, - Options). - -unsubscribe_node(NodeId, Sender, Subscriber, SubID) -> - node_hometree_odbc:unsubscribe_node(NodeId, Sender, - Subscriber, SubID). - -publish_item(NodeId, Publisher, Model, MaxItems, ItemId, - Payload) -> - node_hometree_odbc:publish_item(NodeId, Publisher, - Model, MaxItems, ItemId, Payload). - -remove_extra_items(NodeId, MaxItems, ItemIds) -> - node_hometree_odbc:remove_extra_items(NodeId, MaxItems, - ItemIds). - -delete_item(NodeId, Publisher, PublishModel, ItemId) -> - node_hometree_odbc:delete_item(NodeId, Publisher, - PublishModel, ItemId). - -purge_node(NodeId, Owner) -> - node_hometree_odbc:purge_node(NodeId, Owner). - -get_entity_affiliations(Host, Owner) -> - node_hometree_odbc:get_entity_affiliations(Host, Owner). - -get_node_affiliations(NodeId) -> - node_hometree_odbc:get_node_affiliations(NodeId). - -get_affiliation(NodeId, Owner) -> - node_hometree_odbc:get_affiliation(NodeId, Owner). - -set_affiliation(NodeId, Owner, Affiliation) -> - node_hometree_odbc:set_affiliation(NodeId, Owner, - Affiliation). - -get_entity_subscriptions(Host, Owner) -> - node_hometree_odbc:get_entity_subscriptions(Host, - Owner). - -get_entity_subscriptions_for_send_last(Host, Owner) -> - node_hometree_odbc:get_entity_subscriptions_for_send_last(Host, - Owner). - -get_node_subscriptions(NodeId) -> - node_hometree_odbc:get_node_subscriptions(NodeId). - -get_subscriptions(NodeId, Owner) -> - node_hometree_odbc:get_subscriptions(NodeId, Owner). - -set_subscriptions(NodeId, Owner, Subscription, SubId) -> - node_hometree_odbc:set_subscriptions(NodeId, Owner, - Subscription, SubId). - -get_pending_nodes(Host, Owner) -> - node_hometree_odbc:get_pending_nodes(Host, Owner). - -get_states(NodeId) -> - node_hometree_odbc:get_states(NodeId). - -get_state(NodeId, JID) -> - node_hometree_odbc:get_state(NodeId, JID). - -set_state(State) -> node_hometree_odbc:set_state(State). - -get_items(NodeId, From) -> - node_hometree_odbc:get_items(NodeId, From). - -get_items(NodeId, From, RSM) -> - node_hometree_odbc:get_items(NodeId, From, RSM). - -get_items(NodeId, JID, AccessModel, - PresenceSubscription, RosterGroup, SubId) -> - get_items(NodeId, JID, AccessModel, - PresenceSubscription, RosterGroup, SubId, none). - -get_items(NodeId, JID, AccessModel, - PresenceSubscription, RosterGroup, SubId, RSM) -> - node_hometree_odbc:get_items(NodeId, JID, AccessModel, - PresenceSubscription, RosterGroup, SubId, RSM). - -get_item(NodeId, ItemId) -> - node_hometree_odbc:get_item(NodeId, ItemId). - -get_item(NodeId, ItemId, JID, AccessModel, - PresenceSubscription, RosterGroup, SubId) -> - node_hometree_odbc:get_item(NodeId, ItemId, JID, - AccessModel, PresenceSubscription, RosterGroup, - SubId). - -set_item(Item) -> node_hometree_odbc:set_item(Item). - -get_item_name(Host, Node, Id) -> - node_hometree_odbc:get_item_name(Host, Node, Id). - -get_last_items(NodeId, From, Count) -> - node_hometree_odbc:get_last_items(NodeId, From, Count). - -node_to_path(Node) -> [(Node)]. - -path_to_node(Path) -> - case Path of - % default slot - [Node] -> iolist_to_binary(Node); - % handle old possible entries, used when migrating database content to new format - [Node | _] when is_binary(Node) -> - iolist_to_binary(str:join([<<"">> | Path], <<"/">>)); - % default case (used by PEP for example) - _ -> iolist_to_binary(Path) - end. diff --git a/src/node_flat_sql.erl b/src/node_flat_sql.erl new file mode 100644 index 000000000..acfdf3331 --- /dev/null +++ b/src/node_flat_sql.erl @@ -0,0 +1,1106 @@ +%%%---------------------------------------------------------------------- +%%% File : node_flat_sql.erl +%%% Author : Christophe Romain +%%% Purpose : Standard PubSub node plugin with ODBC backend +%%% Created : 1 Dec 2007 by Christophe Romain +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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. +%%% +%%%---------------------------------------------------------------------- + +%%% @doc The module {@module} is the default PubSub plugin. +%%%

    It is used as a default for all unknown PubSub node type. It can serve +%%% as a developer basis and reference to build its own custom pubsub node +%%% types.

    +%%%

    PubSub plugin nodes are using the {@link gen_node} behaviour.

    + +-module(node_flat_sql). +-behaviour(gen_pubsub_node). +-author('christophe.romain@process-one.net'). + + +-include("pubsub.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). +-include("ejabberd_sql_pt.hrl"). +-include("translate.hrl"). + +-export([init/3, terminate/2, options/0, features/0, + 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_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, + get_subscriptions/2, set_subscriptions/4, + get_pending_nodes/2, get_states/1, get_state/2, + set_state/1, get_items/7, get_items/3, get_item/7, + get_item/2, set_item/1, get_item_name/3, node_to_path/1, + path_to_node/1, + get_entity_subscriptions_for_send_last/2, get_last_items/3, + get_only_item/2]). + +-export([decode_jid/1, encode_jid/1, encode_jid_like/1, + decode_affiliation/1, decode_subscriptions/1, + encode_affiliation/1, encode_subscriptions/1, + encode_host/1, encode_host_like/1]). + +init(_Host, _ServerHost, _Opts) -> + %%pubsub_subscription_sql:init(Host, ServerHost, Opts), + ok. + +terminate(_Host, _ServerHost) -> + ok. + +options() -> + [{sql, true}, {rsm, true} | node_flat:options()]. + +features() -> + [<<"rsm">> | node_flat:features()]. + +create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access) -> + node_flat:create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access). + +create_node(Nidx, Owner) -> + {_U, _S, _R} = OwnerKey = jid:tolower(jid:remove_resource(Owner)), + J = encode_jid(OwnerKey), + A = encode_affiliation(owner), + S = encode_subscriptions([]), + ejabberd_sql:sql_query_t( + ?SQL("insert into pubsub_state(" + "nodeid, jid, affiliation, subscriptions) " + "values (%(Nidx)d, %(J)s, %(A)s, %(S)s)")), + {result, {default, broadcast}}. + +delete_node(Nodes) -> + Reply = lists:map( + fun(#pubsub_node{id = Nidx} = PubsubNode) -> + Subscriptions = + case ejabberd_sql:sql_query_t( + ?SQL("select @(jid)s, @(subscriptions)s " + "from pubsub_state where nodeid=%(Nidx)d")) of + {selected, RItems} -> + [{decode_jid(SJID), decode_subscriptions(Subs)} + || {SJID, Subs} <- RItems]; + _ -> + [] + end, + {PubsubNode, Subscriptions} + end, Nodes), + {result, {default, broadcast, Reply}}. + +subscribe_node(Nidx, Sender, Subscriber, AccessModel, + SendLast, PresenceSubscription, RosterGroup, _Options) -> + SubKey = jid:tolower(Subscriber), + GenKey = jid:remove_resource(SubKey), + Authorized = jid:tolower(jid:remove_resource(Sender)) == GenKey, + {Affiliation, Subscriptions} = select_affiliation_subscriptions(Nidx, GenKey, SubKey), + Whitelisted = lists:member(Affiliation, [member, publisher, owner]), + PendingSubscription = lists:any(fun + ({pending, _}) -> true; + (_) -> false + end, + Subscriptions), + Owner = Affiliation == owner, + if not Authorized -> + {error, mod_pubsub:extended_error( + xmpp:err_bad_request(), mod_pubsub:err_invalid_jid())}; + (Affiliation == outcast) or (Affiliation == publish_only) -> + {error, xmpp:err_forbidden()}; + PendingSubscription -> + {error, mod_pubsub:extended_error( + xmpp:err_not_authorized(), + mod_pubsub:err_pending_subscription())}; + (AccessModel == presence) and (not PresenceSubscription) and (not Owner) -> + {error, mod_pubsub:extended_error( + xmpp:err_not_authorized(), + mod_pubsub:err_presence_subscription_required())}; + (AccessModel == roster) and (not RosterGroup) and (not Owner) -> + {error, mod_pubsub:extended_error( + xmpp:err_not_authorized(), + mod_pubsub:err_not_in_roster_group())}; + (AccessModel == whitelist) and (not Whitelisted) and (not Owner) -> + {error, mod_pubsub:extended_error( + xmpp:err_not_allowed(), mod_pubsub:err_closed_node())}; + %%MustPay -> + %% % Payment is required for a subscription + %% {error, ?ERR_PAYMENT_REQUIRED}; + %%ForbiddenAnonymous -> + %% % Requesting entity is anonymous + %% {error, ?ERR_FORBIDDEN}; + true -> + %%{result, SubId} = pubsub_subscription_sql:subscribe_node(Subscriber, Nidx, Options), + {NewSub, SubId} = case Subscriptions of + [{subscribed, Id}|_] -> + {subscribed, Id}; + [] -> + Id = pubsub_subscription_sql:make_subid(), + Sub = case AccessModel of + authorize -> pending; + _ -> subscribed + end, + update_subscription(Nidx, SubKey, [{Sub, Id} | Subscriptions]), + {Sub, Id} + end, + case {NewSub, SendLast} of + {subscribed, never} -> + {result, {default, subscribed, SubId}}; + {subscribed, _} -> + {result, {default, subscribed, SubId, send_last}}; + {_, _} -> + {result, {default, pending, SubId}} + end + end. + +unsubscribe_node(Nidx, Sender, Subscriber, SubId) -> + SubKey = jid:tolower(Subscriber), + GenKey = jid:remove_resource(SubKey), + Authorized = jid:tolower(jid:remove_resource(Sender)) == GenKey, + {Affiliation, Subscriptions} = select_affiliation_subscriptions(Nidx, SubKey), + SubIdExists = case SubId of + <<>> -> false; + Binary when is_binary(Binary) -> true; + _ -> false + end, + if + %% Requesting entity is prohibited from unsubscribing entity + not Authorized -> + {error, xmpp:err_forbidden()}; + %% Entity did not specify SubId + %%SubId == "", ?? -> + %% {error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")}; + %% Invalid subscription identifier + %%InvalidSubId -> + %% {error, ?ERR_EXTENDED(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; + %% Requesting entity is not a subscriber + Subscriptions == [] -> + {error, mod_pubsub:extended_error( + xmpp:err_unexpected_request(), + mod_pubsub:err_not_subscribed())}; + %% Subid supplied, so use that. + SubIdExists -> + Sub = first_in_list(fun + ({_, S}) when S == SubId -> true; + (_) -> false + end, + Subscriptions), + case Sub of + {value, S} -> + delete_subscription(SubKey, Nidx, S, Affiliation, Subscriptions), + {result, default}; + false -> + {error, mod_pubsub:extended_error( + xmpp:err_unexpected_request(), + mod_pubsub:err_not_subscribed())} + end; + %% Asking to remove all subscriptions to the given node + SubId == all -> + [delete_subscription(SubKey, Nidx, S, Affiliation, Subscriptions) + || S <- Subscriptions], + {result, default}; + %% No subid supplied, but there's only one matching subscription + length(Subscriptions) == 1 -> + delete_subscription(SubKey, Nidx, hd(Subscriptions), Affiliation, Subscriptions), + {result, default}; + %% No subid and more than one possible subscription match. + true -> + {error, mod_pubsub:extended_error( + xmpp:err_bad_request(), mod_pubsub:err_subid_required())} + end. + +delete_subscription(SubKey, Nidx, {Subscription, SubId}, Affiliation, Subscriptions) -> + NewSubs = Subscriptions -- [{Subscription, SubId}], + %%pubsub_subscription_sql:unsubscribe_node(SubKey, Nidx, SubId), + case {Affiliation, NewSubs} of + {none, []} -> del_state(Nidx, SubKey); + _ -> update_subscription(Nidx, SubKey, NewSubs) + end. + +publish_item(Nidx, Publisher, PublishModel, MaxItems, ItemId, Payload, + _PubOpts) -> + SubKey = jid:tolower(Publisher), + GenKey = jid:remove_resource(SubKey), + {Affiliation, Subscriptions} = select_affiliation_subscriptions(Nidx, GenKey, SubKey), + Subscribed = case PublishModel of + subscribers -> node_flat:is_subscribed(Subscriptions); + _ -> undefined + end, + if not ((PublishModel == open) or + (PublishModel == publishers) and + ((Affiliation == owner) + or (Affiliation == publisher) + or (Affiliation == publish_only)) + or (Subscribed == true)) -> + {error, xmpp:err_forbidden()}; + true -> + if MaxItems > 0; + MaxItems == unlimited -> + Now = erlang:timestamp(), + case get_item(Nidx, ItemId) of + {result, #pubsub_item{creation = {_, GenKey}} = OldItem} -> + set_item(OldItem#pubsub_item{ + modification = {Now, SubKey}, + payload = Payload}), + {result, {default, broadcast, []}}; + % Allow node owner to modify any item, he can also delete it and recreate + {result, #pubsub_item{creation = {CreationTime, _}} = OldItem} when Affiliation == owner-> + set_item(OldItem#pubsub_item{ + creation = {CreationTime, GenKey}, + modification = {Now, SubKey}, + payload = Payload}), + {result, {default, broadcast, []}}; + {result, _} -> + {error, xmpp:err_forbidden()}; + _ -> + OldIds = maybe_remove_extra_items(Nidx, MaxItems, + GenKey, ItemId), + set_item(#pubsub_item{ + itemid = {ItemId, Nidx}, + creation = {Now, GenKey}, + modification = {Now, SubKey}, + payload = Payload}), + {result, {default, broadcast, OldIds}} + end; + true -> + {result, {default, broadcast, []}} + end + end. + +remove_extra_items(Nidx, MaxItems) -> + remove_extra_items(Nidx, MaxItems, itemids(Nidx)). + +remove_extra_items(_Nidx, unlimited, ItemIds) -> + {result, {ItemIds, []}}; +remove_extra_items(Nidx, MaxItems, ItemIds) -> + NewItems = lists:sublist(ItemIds, MaxItems), + OldItems = lists:nthtail(length(NewItems), 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), + {result, Affiliation} = get_affiliation(Nidx, GenKey), + Allowed = Affiliation == publisher orelse + Affiliation == owner orelse + (PublishModel == open andalso + case get_item(Nidx, ItemId) of + {result, #pubsub_item{creation = {_, GenKey}}} -> true; + _ -> false + end), + if not Allowed -> + {error, xmpp:err_forbidden()}; + true -> + Items = itemids(Nidx, GenKey), + case lists:member(ItemId, Items) of + true -> + case del_item(Nidx, ItemId) of + {updated, 1} -> {result, {default, broadcast}}; + _ -> {error, xmpp:err_item_not_found()} + end; + false -> + case Affiliation of + owner -> + case del_item(Nidx, ItemId) of + {updated, 1} -> {result, {default, broadcast}}; + _ -> {error, xmpp:err_item_not_found()} + end; + _ -> + {error, xmpp:err_forbidden()} + end + end + end. + +purge_node(Nidx, Owner) -> + SubKey = jid:tolower(Owner), + GenKey = jid:remove_resource(SubKey), + GenState = get_state(Nidx, GenKey), + case GenState of + #pubsub_state{affiliation = owner} -> + {result, States} = get_states(Nidx), + lists:foreach(fun + (#pubsub_state{items = []}) -> ok; + (#pubsub_state{items = Items}) -> del_items(Nidx, Items) + end, + States), + {result, {default, broadcast}}; + _ -> + {error, xmpp:err_forbidden()} + end. + +get_entity_affiliations(Host, Owner) -> + SubKey = jid:tolower(Owner), + GenKey = jid:remove_resource(SubKey), + H = encode_host(Host), + J = encode_jid(GenKey), + {result, + case ejabberd_sql:sql_query_t( + ?SQL("select @(node)s, @(plugin)s, @(i.nodeid)d, @(affiliation)s " + "from pubsub_state i, pubsub_node n where " + "i.nodeid = n.nodeid and jid=%(J)s and host=%(H)s")) of + {selected, RItems} -> + [{nodetree_tree_sql:raw_to_node(Host, {N, <<"">>, T, I}), + decode_affiliation(A)} || {N, T, I, A} <- RItems]; + _ -> + [] + end}. + +get_node_affiliations(Nidx) -> + {result, + case ejabberd_sql:sql_query_t( + ?SQL("select @(jid)s, @(affiliation)s from pubsub_state " + "where nodeid=%(Nidx)d")) of + {selected, RItems} -> + [{decode_jid(J), decode_affiliation(A)} || {J, A} <- RItems]; + _ -> + [] + end}. + +get_affiliation(Nidx, Owner) -> + SubKey = jid:tolower(Owner), + GenKey = jid:remove_resource(SubKey), + J = encode_jid(GenKey), + {result, + case ejabberd_sql:sql_query_t( + ?SQL("select @(affiliation)s from pubsub_state " + "where nodeid=%(Nidx)d and jid=%(J)s")) of + {selected, [{A}]} -> + decode_affiliation(A); + _ -> + none + end}. + +set_affiliation(Nidx, Owner, Affiliation) -> + SubKey = jid:tolower(Owner), + GenKey = jid:remove_resource(SubKey), + {_, Subscriptions} = select_affiliation_subscriptions(Nidx, GenKey), + case {Affiliation, Subscriptions} of + {none, []} -> {result, del_state(Nidx, GenKey)}; + _ -> {result, update_affiliation(Nidx, GenKey, Affiliation)} + end. + +get_entity_subscriptions(Host, Owner) -> + SubKey = jid:tolower(Owner), + GenKey = jid:remove_resource(SubKey), + H = encode_host(Host), + GJ = encode_jid(GenKey), + Query = case SubKey of + GenKey -> + GJLike = <<(encode_jid_like(GenKey))/binary, "/%">>, + ?SQL("select @(node)s, @(plugin)s, @(i.nodeid)d, @(jid)s, @(subscriptions)s " + "from pubsub_state i, pubsub_node n " + "where i.nodeid = n.nodeid and " + "(jid=%(GJ)s or jid like %(GJLike)s %ESCAPE) and host=%(H)s"); + _ -> + SJ = encode_jid(SubKey), + ?SQL("select @(node)s, @(plugin)s, @(i.nodeid)d, @(jid)s, @(subscriptions)s " + "from pubsub_state i, pubsub_node n " + "where i.nodeid = n.nodeid and " + "jid in (%(SJ)s, %(GJ)s) and host=%(H)s") + end, + {result, + case ejabberd_sql:sql_query_t(Query) of + {selected, RItems} -> + lists:foldl( + fun({N, T, I, J, S}, Acc) -> + Node = nodetree_tree_sql:raw_to_node(Host, {N, <<"">>, T, I}), + Jid = decode_jid(J), + lists:foldl( + fun({Sub, SubId}, Acc2) -> + [{Node, Sub, SubId, Jid} | Acc2] + end, Acc, decode_subscriptions(S)) + end, [], RItems); + _ -> + [] + end}. + +-spec get_entity_subscriptions_for_send_last(Host :: mod_pubsub:hostPubsub(), + Owner :: jid()) -> + {result, [{mod_pubsub:pubsubNode(), + mod_pubsub:subscription(), + mod_pubsub:subId(), + ljid()}]}. + +get_entity_subscriptions_for_send_last(Host, Owner) -> + SubKey = jid:tolower(Owner), + GenKey = jid:remove_resource(SubKey), + H = encode_host(Host), + GJ = encode_jid(GenKey), + Query = case SubKey of + GenKey -> + GJLike = <<(encode_jid_like(GenKey))/binary, "/%">>, + ?SQL("select @(node)s, @(plugin)s, @(i.nodeid)d, @(jid)s, @(subscriptions)s " + "from pubsub_state i, pubsub_node n, pubsub_node_option o " + "where i.nodeid = n.nodeid and n.nodeid = o.nodeid and " + "name='send_last_published_item' and val='on_sub_and_presence' and " + "(jid=%(GJ)s or jid like %(GJLike)s %ESCAPE) and host=%(H)s"); + _ -> + SJ = encode_jid(SubKey), + ?SQL("select @(node)s, @(plugin)s, @(i.nodeid)d, @(jid)s, @(subscriptions)s " + "from pubsub_state i, pubsub_node n, pubsub_node_option o " + "where i.nodeid = n.nodeid and n.nodeid = o.nodeid and " + "name='send_last_published_item' and val='on_sub_and_presence' and " + "jid in (%(SJ)s, %(GJ)s) and host=%(H)s") + end, + {result, + case ejabberd_sql:sql_query_t(Query) of + {selected, RItems} -> + lists:foldl( + fun ({N, T, I, J, S}, Acc) -> + Node = nodetree_tree_sql:raw_to_node(Host, {N, <<"">>, T, I}), + Jid = decode_jid(J), + lists:foldl( + fun ({Sub, SubId}, Acc2) -> + [{Node, Sub, SubId, Jid}| Acc2] + end, Acc, decode_subscriptions(S)) + end, [], RItems); + _ -> + [] + end}. + +get_node_subscriptions(Nidx) -> + {result, + case ejabberd_sql:sql_query_t( + ?SQL("select @(jid)s, @(subscriptions)s from pubsub_state " + "where nodeid=%(Nidx)d")) of + {selected, RItems} -> + lists:foldl( + fun ({J, S}, Acc) -> + Jid = decode_jid(J), + lists:foldl( + fun ({Sub, SubId}, Acc2) -> + [{Jid, Sub, SubId} | Acc2] + end, Acc, decode_subscriptions(S)) + end, [], RItems); + _ -> + [] + end}. + +get_subscriptions(Nidx, Owner) -> + SubKey = jid:tolower(Owner), + J = encode_jid(SubKey), + {result, + case ejabberd_sql:sql_query_t( + ?SQL("select @(subscriptions)s from pubsub_state" + " where nodeid=%(Nidx)d and jid=%(J)s")) of + {selected, [{S}]} -> + decode_subscriptions(S); + _ -> + [] + end}. + +set_subscriptions(Nidx, Owner, Subscription, SubId) -> + SubKey = jid:tolower(Owner), + SubState = get_state_without_itemids(Nidx, SubKey), + case {SubId, SubState#pubsub_state.subscriptions} of + {_, []} -> + case Subscription of + none -> + {error, mod_pubsub:extended_error( + xmpp:err_bad_request(), + mod_pubsub:err_not_subscribed())}; + _ -> + new_subscription(Nidx, Owner, Subscription, SubState) + end; + {<<>>, [{_, SID}]} -> + case Subscription of + none -> unsub_with_subid(Nidx, SID, SubState); + _ -> replace_subscription({Subscription, SID}, SubState) + end; + {<<>>, [_ | _]} -> + {error, mod_pubsub:extended_error( + xmpp:err_bad_request(), + mod_pubsub:err_subid_required())}; + _ -> + case Subscription of + none -> unsub_with_subid(Nidx, SubId, SubState); + _ -> replace_subscription({Subscription, SubId}, SubState) + end + end. + +replace_subscription(NewSub, SubState) -> + NewSubs = replace_subscription(NewSub, SubState#pubsub_state.subscriptions, []), + {result, set_state(SubState#pubsub_state{subscriptions = NewSubs})}. + +replace_subscription(_, [], Acc) -> Acc; +replace_subscription({Sub, SubId}, [{_, SubId} | T], Acc) -> + replace_subscription({Sub, SubId}, T, [{Sub, SubId} | Acc]). + +new_subscription(_Nidx, _Owner, Subscription, SubState) -> + %%{result, SubId} = pubsub_subscription_sql:subscribe_node(Owner, Nidx, []), + SubId = pubsub_subscription_sql:make_subid(), + Subscriptions = [{Subscription, SubId} | SubState#pubsub_state.subscriptions], + set_state(SubState#pubsub_state{subscriptions = Subscriptions}), + {result, {Subscription, SubId}}. + +unsub_with_subid(Nidx, SubId, SubState) -> + %%pubsub_subscription_sql:unsubscribe_node(SubState#pubsub_state.stateid, Nidx, SubId), + NewSubs = [{S, Sid} + || {S, Sid} <- SubState#pubsub_state.subscriptions, + SubId =/= Sid], + case {NewSubs, SubState#pubsub_state.affiliation} of + {[], none} -> {result, del_state(Nidx, element(1, SubState#pubsub_state.stateid))}; + _ -> {result, set_state(SubState#pubsub_state{subscriptions = NewSubs})} + end. + +get_pending_nodes(Host, Owner) -> + GenKey = encode_jid(jid:remove_resource(jid:tolower(Owner))), + PendingIdxs = case ejabberd_sql:sql_query_t( + ?SQL("select @(nodeid)d from pubsub_state " + "where subscriptions like '%p%' and affiliation='o'" + "and jid=%(GenKey)s")) of + {selected, RItems} -> + [Nidx || {Nidx} <- RItems]; + _ -> + [] + end, + NodeTree = mod_pubsub:tree(Host), + Reply = lists:foldl(fun(Nidx, Acc) -> + case NodeTree:get_node(Nidx) of + #pubsub_node{nodeid = {_, Node}} -> [Node | Acc]; + _ -> Acc + end + end, + [], PendingIdxs), + {result, Reply}. + +get_states(Nidx) -> + case ejabberd_sql:sql_query_t( + ?SQL("select @(jid)s, @(affiliation)s, @(subscriptions)s " + "from pubsub_state where nodeid=%(Nidx)d")) of + {selected, RItems} -> + {result, + lists:map( + fun({SJID, Aff, Subs}) -> + JID = decode_jid(SJID), + #pubsub_state{stateid = {JID, Nidx}, + nodeidx = Nidx, + items = itemids(Nidx, JID), + affiliation = decode_affiliation(Aff), + subscriptions = decode_subscriptions(Subs)} + end, RItems)}; + _ -> + {result, []} + end. + +get_state(Nidx, JID) -> + State = get_state_without_itemids(Nidx, JID), + {SJID, _} = State#pubsub_state.stateid, + State#pubsub_state{items = itemids(Nidx, SJID)}. + +-spec get_state_without_itemids(Nidx :: mod_pubsub:nodeIdx(), Key :: ljid()) -> + mod_pubsub:pubsubState(). + +get_state_without_itemids(Nidx, JID) -> + J = encode_jid(JID), + case ejabberd_sql:sql_query_t( + ?SQL("select @(jid)s, @(affiliation)s, @(subscriptions)s " + "from pubsub_state " + "where nodeid=%(Nidx)d and jid=%(J)s")) of + {selected, [{SJID, Aff, Subs}]} -> + #pubsub_state{stateid = {decode_jid(SJID), Nidx}, + nodeidx = Nidx, + affiliation = decode_affiliation(Aff), + subscriptions = decode_subscriptions(Subs)}; + _ -> + #pubsub_state{stateid = {JID, Nidx}, nodeidx = Nidx} + end. + +set_state(State) -> + {_, Nidx} = State#pubsub_state.stateid, + set_state(Nidx, State). + +set_state(Nidx, State) -> + {JID, _} = State#pubsub_state.stateid, + J = encode_jid(JID), + S = encode_subscriptions(State#pubsub_state.subscriptions), + A = encode_affiliation(State#pubsub_state.affiliation), + ?SQL_UPSERT_T( + "pubsub_state", + ["!nodeid=%(Nidx)d", + "!jid=%(J)s", + "affiliation=%(A)s", + "subscriptions=%(S)s" + ]), + ok. + +del_state(Nidx, JID) -> + J = encode_jid(JID), + catch ejabberd_sql:sql_query_t( + ?SQL("delete from pubsub_state" + " where jid=%(J)s and nodeid=%(Nidx)d")), + ok. + +get_items(Nidx, _From, undefined) -> + SNidx = misc:i2l(Nidx), + case ejabberd_sql:sql_query_t( + [<<"select itemid, publisher, creation, modification, payload", + " from pubsub_item where nodeid='", SNidx/binary, "'", + " order by creation asc">>]) of + {selected, _, AllItems} -> + {result, {[raw_to_item(Nidx, RItem) || RItem <- AllItems], undefined}}; + _ -> + {result, {[], undefined}} + end; +get_items(Nidx, _From, #rsm_set{max = Max, index = IncIndex, + 'after' = After, before = Before}) -> + Count = case catch ejabberd_sql:sql_query_t( + ?SQL("select @(count(itemid))d from pubsub_item" + " where nodeid=%(Nidx)d")) of + {selected, [{C}]} -> C; + _ -> 0 + end, + Offset = case {IncIndex, Before, After} of + {I, undefined, undefined} when is_integer(I) -> I; + _ -> 0 + end, + Limit = case Max of + undefined -> ?MAXITEMS; + _ -> Max + end, + Filters = rsm_filters(misc:i2l(Nidx), Before, After), + Query = fun(mssql, _) -> + ejabberd_sql:sql_query_t( + [<<"select top ", (integer_to_binary(Limit))/binary, + " itemid, publisher, creation, modification, payload", + " from pubsub_item", Filters/binary>>]); + %OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY; + (_, _) -> + ejabberd_sql:sql_query_t( + [<<"select itemid, publisher, creation, modification, payload", + " from pubsub_item", Filters/binary, + " limit ", (integer_to_binary(Limit))/binary, + " offset ", (integer_to_binary(Offset))/binary>>]) + end, + case ejabberd_sql:sql_query_t(Query) of + {selected, _, []} -> + {result, {[], #rsm_set{count = Count}}}; + {selected, [<<"itemid">>, <<"publisher">>, <<"creation">>, + <<"modification">>, <<"payload">>], RItems} -> + Rsm = rsm_page(Count, IncIndex, Offset, RItems), + {result, {[raw_to_item(Nidx, RItem) || RItem <- RItems], Rsm}}; + _ -> + {result, {[], undefined}} + end. + +get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId, RSM) -> + SubKey = jid:tolower(JID), + GenKey = jid:remove_resource(SubKey), + {Affiliation, Subscriptions} = select_affiliation_subscriptions(Nidx, GenKey, SubKey), + Whitelisted = node_flat:can_fetch_item(Affiliation, Subscriptions), + if %%SubId == "", ?? -> + %% Entity has multiple subscriptions to the node but does not specify a subscription ID + %{error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")}; + %%InvalidSubId -> + %% Entity is subscribed but specifies an invalid subscription ID + %{error, ?ERR_EXTENDED(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; + (Affiliation == outcast) or (Affiliation == publish_only) -> + {error, xmpp:err_forbidden()}; + (AccessModel == presence) and not PresenceSubscription -> + {error, mod_pubsub:extended_error( + xmpp:err_not_authorized(), + mod_pubsub:err_presence_subscription_required())}; + (AccessModel == roster) and not RosterGroup -> + {error, mod_pubsub:extended_error( + xmpp:err_not_authorized(), + mod_pubsub:err_not_in_roster_group())}; + (AccessModel == whitelist) and not Whitelisted -> + {error, mod_pubsub:extended_error( + xmpp:err_not_allowed(), mod_pubsub:err_closed_node())}; + (AccessModel == authorize) and not Whitelisted -> + {error, xmpp:err_forbidden()}; + %%MustPay -> + %% % Payment is required for a subscription + %% {error, ?ERR_PAYMENT_REQUIRED}; + true -> + get_items(Nidx, JID, RSM) + end. + +get_last_items(Nidx, _From, Limit) -> + SNidx = misc:i2l(Nidx), + Query = fun(mssql, _) -> + ejabberd_sql:sql_query_t( + [<<"select top ", (integer_to_binary(Limit))/binary, + " itemid, publisher, creation, modification, payload", + " from pubsub_item where nodeid='", SNidx/binary, + "' order by modification desc">>]); + (_, _) -> + ejabberd_sql:sql_query_t( + [<<"select itemid, publisher, creation, modification, payload", + " from pubsub_item where nodeid='", SNidx/binary, + "' order by modification desc ", + " limit ", (integer_to_binary(Limit))/binary>>]) + end, + case catch ejabberd_sql:sql_query_t(Query) of + {selected, [<<"itemid">>, <<"publisher">>, <<"creation">>, + <<"modification">>, <<"payload">>], RItems} -> + {result, [raw_to_item(Nidx, RItem) || RItem <- RItems]}; + _ -> + {result, []} + end. + +get_only_item(Nidx, _From) -> + SNidx = misc:i2l(Nidx), + Query = fun(mssql, _) -> + ejabberd_sql:sql_query_t( + [<<"select itemid, publisher, creation, modification, payload", + " from pubsub_item where nodeid='", SNidx/binary, "'">>]); + (_, _) -> + ejabberd_sql:sql_query_t( + [<<"select itemid, publisher, creation, modification, payload", + " from pubsub_item where nodeid='", SNidx/binary, "'">>]) + end, + case catch ejabberd_sql:sql_query_t(Query) of + {selected, [<<"itemid">>, <<"publisher">>, <<"creation">>, + <<"modification">>, <<"payload">>], RItems} -> + {result, [raw_to_item(Nidx, RItem) || RItem <- RItems]}; + _ -> + {result, []} + end. + +get_item(Nidx, ItemId) -> + case catch ejabberd_sql:sql_query_t( + ?SQL("select @(itemid)s, @(publisher)s, @(creation)s," + " @(modification)s, @(payload)s from pubsub_item" + " where nodeid=%(Nidx)d and itemid=%(ItemId)s")) + of + {selected, [RItem]} -> + {result, raw_to_item(Nidx, RItem)}; + {selected, []} -> + {error, xmpp:err_item_not_found()}; + {'EXIT', _} -> + {error, xmpp:err_internal_server_error(?T("Database failure"), ejabberd_option:language())} + end. + +get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId) -> + SubKey = jid:tolower(JID), + GenKey = jid:remove_resource(SubKey), + {Affiliation, Subscriptions} = select_affiliation_subscriptions(Nidx, GenKey, SubKey), + Whitelisted = node_flat:can_fetch_item(Affiliation, Subscriptions), + if %%SubId == "", ?? -> + %% Entity has multiple subscriptions to the node but does not specify a subscription ID + %{error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")}; + %%InvalidSubId -> + %% Entity is subscribed but specifies an invalid subscription ID + %{error, ?ERR_EXTENDED(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; + (Affiliation == outcast) or (Affiliation == publish_only) -> + {error, xmpp:err_forbidden()}; + (AccessModel == presence) and not PresenceSubscription -> + {error, mod_pubsub:extended_error( + xmpp:err_not_authorized(), + mod_pubsub:err_presence_subscription_required())}; + (AccessModel == roster) and not RosterGroup -> + {error, mod_pubsub:extended_error( + xmpp:err_not_authorized(), + mod_pubsub:err_not_in_roster_group())}; + (AccessModel == whitelist) and not Whitelisted -> + {error, mod_pubsub:extended_error( + xmpp:err_not_allowed(), mod_pubsub:err_closed_node())}; + (AccessModel == authorize) and not Whitelisted -> + {error, xmpp:err_forbidden()}; + %%MustPay -> + %% % Payment is required for a subscription + %% {error, ?ERR_PAYMENT_REQUIRED}; + true -> + get_item(Nidx, ItemId) + end. + +set_item(Item) -> + {ItemId, Nidx} = Item#pubsub_item.itemid, + {C, _} = Item#pubsub_item.creation, + {M, JID} = Item#pubsub_item.modification, + P = encode_jid(JID), + Payload = Item#pubsub_item.payload, + XML = str:join([fxml:element_to_binary(X) || X<-Payload], <<>>), + SM = encode_now(M), + SC = encode_now(C), + ?SQL_UPSERT_T( + "pubsub_item", + ["!nodeid=%(Nidx)d", + "!itemid=%(ItemId)s", + "publisher=%(P)s", + "modification=%(SM)s", + "payload=%(XML)s", + "-creation=%(SC)s" + ]), + ok. + +del_item(Nidx, ItemId) -> + catch ejabberd_sql:sql_query_t( + ?SQL("delete from pubsub_item where itemid=%(ItemId)s" + " and nodeid=%(Nidx)d")). + +del_items(_, []) -> + ok; +del_items(Nidx, [ItemId]) -> + del_item(Nidx, ItemId); +del_items(Nidx, ItemIds) -> + I = str:join([ejabberd_sql:to_string_literal_t(X) || X <- ItemIds], <<",">>), + SNidx = misc:i2l(Nidx), + catch + ejabberd_sql:sql_query_t([<<"delete from pubsub_item where itemid in (">>, + I, <<") and nodeid='">>, SNidx, <<"';">>]). + +get_item_name(_Host, _Node, Id) -> + {result, Id}. + +node_to_path(Node) -> + node_flat:node_to_path(Node). + +path_to_node(Path) -> + node_flat:path_to_node(Path). + + +first_in_list(_Pred, []) -> + false; +first_in_list(Pred, [H | T]) -> + case Pred(H) of + true -> {value, H}; + _ -> first_in_list(Pred, T) + end. + +itemids(Nidx) -> + case catch + ejabberd_sql:sql_query_t( + ?SQL("select @(itemid)s from pubsub_item where " + "nodeid=%(Nidx)d order by modification desc")) + of + {selected, RItems} -> + [ItemId || {ItemId} <- RItems]; + _ -> + [] + end. + +itemids(Nidx, {_U, _S, _R} = JID) -> + SJID = encode_jid(JID), + SJIDLike = <<(encode_jid_like(JID))/binary, "/%">>, + case catch + ejabberd_sql:sql_query_t( + ?SQL("select @(itemid)s from pubsub_item where " + "nodeid=%(Nidx)d and (publisher=%(SJID)s" + " or publisher like %(SJIDLike)s %ESCAPE) " + "order by modification desc")) + of + {selected, RItems} -> + [ItemId || {ItemId} <- RItems]; + _ -> + [] + end. + +select_affiliation_subscriptions(Nidx, JID) -> + J = encode_jid(JID), + case catch + ejabberd_sql:sql_query_t( + ?SQL("select @(affiliation)s, @(subscriptions)s from " + " pubsub_state where nodeid=%(Nidx)d and jid=%(J)s")) + of + {selected, [{A, S}]} -> + {decode_affiliation(A), decode_subscriptions(S)}; + _ -> + {none, []} + end. + +select_affiliation_subscriptions(Nidx, JID, JID) -> + select_affiliation_subscriptions(Nidx, JID); +select_affiliation_subscriptions(Nidx, GenKey, SubKey) -> + GJ = encode_jid(GenKey), + SJ = encode_jid(SubKey), + case catch ejabberd_sql:sql_query_t( + ?SQL("select @(jid)s, @(affiliation)s, @(subscriptions)s from " + " pubsub_state where nodeid=%(Nidx)d and jid in (%(GJ)s, %(SJ)s)")) + of + {selected, Res} -> + lists:foldr( + fun({Jid, A, S}, {_, Subs}) when Jid == GJ -> + {decode_affiliation(A), Subs ++ decode_subscriptions(S)}; + ({_, _, S}, {Aff, Subs}) -> + {Aff, Subs ++ decode_subscriptions(S)} + end, {none, []}, Res); + _ -> + {none, []} + end. + +update_affiliation(Nidx, JID, Affiliation) -> + J = encode_jid(JID), + A = encode_affiliation(Affiliation), + ?SQL_UPSERT_T( + "pubsub_state", + ["!nodeid=%(Nidx)d", + "!jid=%(J)s", + "affiliation=%(A)s", + "-subscriptions=''" + ]). + +update_subscription(Nidx, JID, Subscription) -> + J = encode_jid(JID), + S = encode_subscriptions(Subscription), + ?SQL_UPSERT_T( + "pubsub_state", + ["!nodeid=%(Nidx)d", + "!jid=%(J)s", + "subscriptions=%(S)s", + "-affiliation='n'" + ]). + +-spec maybe_remove_extra_items(mod_pubsub:nodeIdx(), + non_neg_integer() | unlimited, ljid(), + mod_pubsub:itemId()) -> [mod_pubsub:itemId()]. +maybe_remove_extra_items(_Nidx, unlimited, _GenKey, _ItemId) -> + []; +maybe_remove_extra_items(Nidx, MaxItems, GenKey, ItemId) -> + ItemIds = [ItemId | itemids(Nidx, GenKey)], + {result, {_NewIds, OldIds}} = remove_extra_items(Nidx, MaxItems, ItemIds), + OldIds. + +-spec decode_jid(SJID :: binary()) -> ljid(). +decode_jid(SJID) -> + jid:tolower(jid:decode(SJID)). + +-spec decode_affiliation(Arg :: binary()) -> atom(). +decode_affiliation(<<"o">>) -> owner; +decode_affiliation(<<"p">>) -> publisher; +decode_affiliation(<<"u">>) -> publish_only; +decode_affiliation(<<"m">>) -> member; +decode_affiliation(<<"c">>) -> outcast; +decode_affiliation(_) -> none. + +-spec decode_subscription(Arg :: binary()) -> atom(). +decode_subscription(<<"s">>) -> subscribed; +decode_subscription(<<"p">>) -> pending; +decode_subscription(<<"u">>) -> unconfigured; +decode_subscription(_) -> none. + +-spec decode_subscriptions(Subscriptions :: binary()) -> [] | [{atom(), binary()},...]. +decode_subscriptions(Subscriptions) -> + lists:foldl(fun (Subscription, Acc) -> + case str:tokens(Subscription, <<":">>) of + [S, SubId] -> [{decode_subscription(S), SubId} | Acc]; + _ -> Acc + end + end, + [], str:tokens(Subscriptions, <<",">>)). + +-spec encode_jid(JID :: ljid()) -> binary(). +encode_jid(JID) -> + jid:encode(JID). + +-spec encode_jid_like(JID :: ljid()) -> binary(). +encode_jid_like(JID) -> + ejabberd_sql:escape_like_arg(jid:encode(JID)). + +-spec encode_host(Host :: host()) -> binary(). +encode_host({_U, _S, _R} = LJID) -> encode_jid(LJID); +encode_host(Host) -> Host. + +-spec encode_host_like(Host :: host()) -> binary(). +encode_host_like({_U, _S, _R} = LJID) -> encode_jid_like(LJID); +encode_host_like(Host) -> + ejabberd_sql:escape_like_arg(Host). + +-spec encode_affiliation(Arg :: atom()) -> binary(). +encode_affiliation(owner) -> <<"o">>; +encode_affiliation(publisher) -> <<"p">>; +encode_affiliation(publish_only) -> <<"u">>; +encode_affiliation(member) -> <<"m">>; +encode_affiliation(outcast) -> <<"c">>; +encode_affiliation(_) -> <<"n">>. + +-spec encode_subscription(Arg :: atom()) -> binary(). +encode_subscription(subscribed) -> <<"s">>; +encode_subscription(pending) -> <<"p">>; +encode_subscription(unconfigured) -> <<"u">>; +encode_subscription(_) -> <<"n">>. + +-spec encode_subscriptions(Subscriptions :: [] | [{atom(), binary()},...]) -> binary(). +encode_subscriptions(Subscriptions) -> + str:join([<<(encode_subscription(S))/binary, ":", SubId/binary>> + || {S, SubId} <- Subscriptions], <<",">>). + +%%% record getter/setter + +raw_to_item(Nidx, [ItemId, SJID, Creation, Modification, XML]) -> + raw_to_item(Nidx, {ItemId, SJID, Creation, Modification, XML}); +raw_to_item(Nidx, {ItemId, SJID, Creation, Modification, XML}) -> + JID = decode_jid(SJID), + Payload = case fxml_stream:parse_element(XML) of + {error, _Reason} -> []; + El -> [El] + end, + #pubsub_item{itemid = {ItemId, Nidx}, + nodeidx = Nidx, + creation = {decode_now(Creation), jid:remove_resource(JID)}, + modification = {decode_now(Modification), JID}, + payload = Payload}. + +rsm_filters(SNidx, undefined, undefined) -> + <<" where nodeid='", SNidx/binary, "'", + " order by creation asc">>; +rsm_filters(SNidx, undefined, After) -> + <<" where nodeid='", SNidx/binary, "'", + " and creation>'", (encode_stamp(After))/binary, "'", + " order by creation asc">>; +rsm_filters(SNidx, <<>>, undefined) -> + %% 2.5 Requesting the Last Page in a Result Set + <<" where nodeid='", SNidx/binary, "'", + " order by creation desc">>; +rsm_filters(SNidx, Before, undefined) -> + <<" where nodeid='", SNidx/binary, "'", + " and creation<'", (encode_stamp(Before))/binary, "'", + " order by creation desc">>. + +rsm_page(Count, Index, Offset, Items) -> + First = decode_stamp(lists:nth(3, hd(Items))), + Last = decode_stamp(lists:nth(3, lists:last(Items))), + #rsm_set{count = Count, index = Index, + first = #rsm_first{index = Offset, data = First}, + last = Last}. + +encode_stamp(Stamp) -> + try xmpp_util:decode_timestamp(Stamp) of + Now -> + encode_now(Now) + catch _:{bad_timestamp, _} -> + Stamp % We should return a proper error to the client instead. + end. +decode_stamp(Stamp) -> + xmpp_util:encode_timestamp(decode_now(Stamp)). + +encode_now({T1, T2, T3}) -> + <<(misc:i2l(T1, 6))/binary, ":", + (misc:i2l(T2, 6))/binary, ":", + (misc:i2l(T3, 6))/binary>>. +decode_now(NowStr) -> + [MS, S, US] = binary:split(NowStr, <<":">>, [global]), + {binary_to_integer(MS), binary_to_integer(S), binary_to_integer(US)}. diff --git a/src/node_hometree.erl b/src/node_hometree.erl deleted file mode 100644 index 6f3c4de74..000000000 --- a/src/node_hometree.erl +++ /dev/null @@ -1,1329 +0,0 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. -%%% -%%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. -%%% -%%% -%%% The Initial Developer of the Original Code is ProcessOne. -%%% Portions created by ProcessOne are Copyright 2006-2015, ProcessOne -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2015, ProcessOne. -%%% -%%% -%%% @copyright 2006-2015 ProcessOne -%%% @author Christophe Romain -%%% [http://www.process-one.net/] -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== - -%%% @todo The item table should be handled by the plugin, but plugin that do -%%% not want to manage it should be able to use the default behaviour. -%%% @todo Plugin modules should be able to register to receive presence update -%%% send to pubsub. - -%%% @doc The module {@module} is the default PubSub plugin. -%%%

    It is used as a default for all unknown PubSub node type. It can serve -%%% as a developer basis and reference to build its own custom pubsub node -%%% types.

    -%%%

    PubSub plugin nodes are using the {@link gen_node} behaviour.

    -%%%

    The API isn't stabilized yet. The pubsub plugin -%%% development is still a work in progress. However, the system is already -%%% useable and useful as is. Please, send us comments, feedback and -%%% improvements.

    - --module(node_hometree). - --author('christophe.romain@process-one.net'). - --include("pubsub.hrl"). - --include("jlib.hrl"). - --behaviour(gen_pubsub_node). - -%% API definition --export([init/3, terminate/2, options/0, features/0, - create_node_permission/6, create_node/2, delete_node/1, - purge_node/2, subscribe_node/8, unsubscribe_node/4, - publish_item/6, delete_item/4, remove_extra_items/3, - get_entity_affiliations/2, get_node_affiliations/1, - get_affiliation/2, set_affiliation/3, - get_entity_subscriptions/2, get_node_subscriptions/1, - get_subscriptions/2, set_subscriptions/4, - get_pending_nodes/2, get_states/1, get_state/2, - set_state/1, get_items/6, get_items/2, get_item/7, - get_item/2, set_item/1, get_item_name/3, node_to_path/1, - path_to_node/1]). - -%% ================ -%% API definition -%% ================ - -%% @spec (Host, ServerHost, Options) -> ok -%% Host = string() -%% ServerHost = string() -%% Options = [{atom(), term()}] -%% @doc

    Called during pubsub modules initialisation. Any pubsub plugin must -%% implement this function. It can return anything.

    -%%

    This function is mainly used to trigger the setup task necessary for the -%% plugin. It can be used for example by the developer to create the specific -%% module database schema if it does not exists yet.

    -init(_Host, _ServerHost, _Options) -> - pubsub_subscription:init(), - mnesia:create_table(pubsub_state, - [{disc_copies, [node()]}, - {attributes, record_info(fields, pubsub_state)}]), - mnesia:create_table(pubsub_item, - [{disc_only_copies, [node()]}, - {attributes, record_info(fields, pubsub_item)}]), - ItemsFields = record_info(fields, pubsub_item), - case mnesia:table_info(pubsub_item, attributes) of - ItemsFields -> ok; - _ -> - mnesia:transform_table(pubsub_item, ignore, ItemsFields) - end, - ok. - -%% @spec (Host, ServerHost) -> ok -%% Host = string() -%% ServerHost = string() -%% @doc

    Called during pubsub modules termination. Any pubsub plugin must -%% implement this function. It can return anything.

    -terminate(_Host, _ServerHost) -> ok. - --spec(options/0 :: () -> NodeOptions::mod_pubsub:nodeOptions()). - -%% @spec () -> Options -%% Options = [mod_pubsub:nodeOption()] -%% @doc Returns the default pubsub node options. -%%

    Example of function return value:

    -%% ``` -%% [{deliver_payloads, true}, -%% {notify_config, false}, -%% {notify_delete, false}, -%% {notify_retract, true}, -%% {persist_items, true}, -%% {max_items, 10}, -%% {subscribe, true}, -%% {access_model, open}, -%% {publish_model, publishers}, -%% {max_payload_size, 100000}, -%% {send_last_published_item, never}, -%% {presence_based_delivery, false}]''' -options() -> - [{deliver_payloads, true}, {notify_config, false}, - {notify_delete, false}, {notify_retract, true}, - {purge_offline, false}, {persist_items, true}, - {max_items, ?MAXITEMS}, {subscribe, true}, - {access_model, open}, {roster_groups_allowed, []}, - {publish_model, publishers}, - {notification_type, headline}, - {max_payload_size, ?MAX_PAYLOAD_SIZE}, - {send_last_published_item, on_sub_and_presence}, - {deliver_notifications, true}, - {presence_based_delivery, false}]. - -%% @spec () -> Features -%% Features = [string()] -%% @doc Returns the node features --spec(features/0 :: () -> Features::[binary(),...]). -features() -> - [<<"create-nodes">>, <<"auto-create">>, - <<"access-authorize">>, <<"delete-nodes">>, - <<"delete-items">>, <<"get-pending">>, - <<"instant-nodes">>, <<"manage-subscriptions">>, - <<"modify-affiliations">>, <<"multi-subscribe">>, - <<"outcast-affiliation">>, <<"persistent-items">>, - <<"publish">>, <<"purge-nodes">>, <<"retract-items">>, - <<"retrieve-affiliations">>, <<"retrieve-items">>, - <<"retrieve-subscriptions">>, <<"subscribe">>, - <<"subscription-notifications">>, - <<"subscription-options">>]. - -%% @spec (Host, ServerHost, NodeId, ParentNodeId, Owner, Access) -> {result, Allowed} -%% Host = mod_pubsub:hostPubsub() -%% ServerHost = string() -%% NodeId = mod_pubsub:nodeId() -%% ParentNodeId = mod_pubsub:nodeId() -%% Owner = mod_pubsub:jid() -%% Access = all | atom() -%% Allowed = boolean() -%% @doc Checks if the current user has the permission to create the requested node -%%

    In {@link node_default}, the permission is decided by the place in the -%% hierarchy where the user is creating the node. The access parameter is also -%% checked in the default module. This parameter depends on the value of the -%% access_createnode ACL value in ejabberd config file.

    -%%

    This function also check that node can be created a a children of its -%% parent node

    -%%

    PubSub plugins can redefine the PubSub node creation rights as they -%% which. They can simply delegate this check to the {@link node_default} -%% module by implementing this function like this: -%% ```check_create_user_permission(Host, ServerHost, NodeId, ParentNodeId, Owner, Access) -> -%% node_default:check_create_user_permission(Host, ServerHost, NodeId, ParentNodeId, Owner, Access).'''

    --spec(create_node_permission/6 :: -( - Host :: mod_pubsub:host(), - ServerHost :: binary(), - NodeId :: mod_pubsub:nodeId(), - _ParentNodeId :: mod_pubsub:nodeId(), - Owner :: jid(), - Access :: atom()) - -> {result, boolean()} -). - -create_node_permission(Host, ServerHost, NodeId, _ParentNodeId, Owner, Access) -> - LOwner = jlib:jid_tolower(Owner), - {User, Server, _Resource} = LOwner, - Allowed = case LOwner of - {<<"">>, Host, <<"">>} -> - true; % pubsub service always allowed - _ -> - case acl:match_rule(ServerHost, Access, LOwner) of - allow -> - case node_to_path(NodeId) of - [<<"home">>, Server, User | _] -> true; - _ -> false - end; - _ -> false - end - end, - {result, Allowed}. - -%% @spec (NodeIdx, Owner) -> {result, {default, broadcast}} -%% NodeIdx = mod_pubsub:nodeIdx() -%% Owner = mod_pubsub:jid() -%% @doc

    --spec(create_node/2 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - Owner :: jid()) - -> {result, {default, broadcast}} -). - -create_node(NodeIdx, Owner) -> - OwnerKey = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), - set_state(#pubsub_state{stateid = {OwnerKey, NodeIdx}, affiliation = owner}), - {result, {default, broadcast}}. - -%% @spec (Nodes) -> {result, {default, broadcast, Reply}} -%% Nodes = [mod_pubsub:pubsubNode()] -%% Reply = [{mod_pubsub:pubsubNode(), -%% [{mod_pubsub:ljid(), [{mod_pubsub:subscription(), mod_pubsub:subId()}]}]}] -%% @doc

    purge items of deleted nodes after effective deletion.

    --spec(delete_node/1 :: -( - Nodes :: [mod_pubsub:pubsubNode(),...]) - -> {result, - {default, broadcast, - [{mod_pubsub:pubsubNode(), - [{ljid(), [{mod_pubsub:subscription(), mod_pubsub:subId()}]},...]},...] - } - } -). -delete_node(Nodes) -> - Tr = fun (#pubsub_state{stateid = {J, _}, subscriptions = Ss}) -> - lists:map(fun (S) -> {J, S} end, Ss) - end, - Reply = lists:map(fun (#pubsub_node{id = NodeIdx} = PubsubNode) -> - {result, States} = get_states(NodeIdx), - lists:foreach(fun (#pubsub_state{stateid = {LJID, _}, items = Items}) -> - del_items(NodeIdx, Items), - del_state(NodeIdx, LJID) - end, States), - {PubsubNode, lists:flatmap(Tr, States)} - end, Nodes), - {result, {default, broadcast, Reply}}. - -%% @spec (NodeIdx, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options) -> {error, Reason} | {result, Result} -%% NodeIdx = mod_pubsub:nodeIdx() -%% Sender = mod_pubsub:jid() -%% Subscriber = mod_pubsub:jid() -%% AccessModel = mod_pubsub:accessModel() -%% SendLast = atom() -%% PresenceSubscription = boolean() -%% RosterGroup = boolean() -%% Options = [mod_pubsub:nodeOption()] -%% Reason = mod_pubsub:stanzaError() -%% Result = {result, {default, subscribed, mod_pubsub:subId()}} -%% | {result, {default, subscribed, mod_pubsub:subId(), send_last}} -%% | {result, {default, pending, mod_pubsub:subId()}} -%% -%% @doc

    Accepts or rejects subcription requests on a PubSub node.

    -%%

    The mechanism works as follow: -%%

      -%%
    • The main PubSub module prepares the subscription and passes the -%% result of the preparation as a record.
    • -%%
    • This function gets the prepared record and several other parameters and -%% can decide to:
        -%%
      • reject the subscription;
      • -%%
      • allow it as is, letting the main module perform the database -%% persistance;
      • -%%
      • allow it, modifying the record. The main module will store the -%% modified record;
      • -%%
      • allow it, but perform the needed persistance operations.
      -%%

    -%%

    The selected behaviour depends on the return parameter: -%%

      -%%
    • {error, Reason}: an IQ error result will be returned. No -%% subscription will actually be performed.
    • -%%
    • true: Subscribe operation is allowed, based on the -%% unmodified record passed in parameter SubscribeResult. If this -%% parameter contains an error, no subscription will be performed.
    • -%%
    • {true, PubsubState}: Subscribe operation is allowed, but -%% the {@link mod_pubsub:pubsubState()} record returned replaces the value -%% passed in parameter SubscribeResult.
    • -%%
    • {true, done}: Subscribe operation is allowed, but the -%% {@link mod_pubsub:pubsubState()} will be considered as already stored and -%% no further persistance operation will be performed. This case is used, -%% when the plugin module is doing the persistance by itself or when it want -%% to completly disable persistance.
    -%%

    -%%

    In the default plugin module, the record is unchanged.

    --spec(subscribe_node/8 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - Sender :: jid(), - Subscriber :: ljid(), - AccessModel :: mod_pubsub:accessModel(), - SendLast :: 'never' | 'on_sub' | 'on_sub_and_presence', - PresenceSubscription :: boolean(), - RosterGroup :: boolean(), - Options :: mod_pubsub:subOptions()) - -> {result, {default, subscribed, mod_pubsub:subId()}} - | {result, {default, subscribed, mod_pubsub:subId(), send_last}} - | {result, {default, pending, mod_pubsub:subId()}} - %%% - | {error, xmlel()} -). -subscribe_node(NodeIdx, Sender, Subscriber, AccessModel, - SendLast, PresenceSubscription, RosterGroup, Options) -> - SubKey = jlib:jid_tolower(Subscriber), - GenKey = jlib:jid_remove_resource(SubKey), - Authorized = - jlib:jid_tolower(jlib:jid_remove_resource(Sender)) == - GenKey, - GenState = get_state(NodeIdx, GenKey), - SubState = case SubKey of - GenKey -> GenState; - _ -> get_state(NodeIdx, SubKey) - end, - Affiliation = GenState#pubsub_state.affiliation, - Subscriptions = SubState#pubsub_state.subscriptions, - Whitelisted = lists:member(Affiliation, - [member, publisher, owner]), - PendingSubscription = lists:any(fun ({pending, _}) -> - true; - (_) -> false - end, - Subscriptions), - if not Authorized -> - {error, - ?ERR_EXTENDED((?ERR_BAD_REQUEST), <<"invalid-jid">>)}; - Affiliation == outcast -> {error, ?ERR_FORBIDDEN}; - PendingSubscription -> - {error, - ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), - <<"pending-subscription">>)}; - (AccessModel == presence) and - not PresenceSubscription -> - {error, - ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), - <<"presence-subscription-required">>)}; - (AccessModel == roster) and not RosterGroup -> - {error, - ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), - <<"not-in-roster-group">>)}; - (AccessModel == whitelist) and not Whitelisted -> - {error, - ?ERR_EXTENDED((?ERR_NOT_ALLOWED), <<"closed-node">>)}; - %%MustPay -> - %% % Payment is required for a subscription - %% {error, ?ERR_PAYMENT_REQUIRED}; - %%ForbiddenAnonymous -> - %% % Requesting entity is anonymous - %% {error, ?ERR_FORBIDDEN}; - true -> - SubId = pubsub_subscription:add_subscription(Subscriber, NodeIdx, Options), - NewSub = case AccessModel of - authorize -> pending; - _ -> subscribed - end, - set_state(SubState#pubsub_state{subscriptions = - [{NewSub, SubId} | Subscriptions]}), - case {NewSub, SendLast} of - {subscribed, never} -> - {result, {default, subscribed, SubId}}; - {subscribed, _} -> - {result, {default, subscribed, SubId, send_last}}; - {_, _} -> {result, {default, pending, SubId}} - end - end. - -%% @spec (NodeIdx, Sender, Subscriber, SubId) -> {error, Reason} | {result, default} -%% NodeIdx = mod_pubsub:nodeIdx() -%% Sender = mod_pubsub:jid() -%% Subscriber = mod_pubsub:jid() -%% SubId = mod_pubsub:subId() -%% Reason = mod_pubsub:stanzaError() -%% @doc

    Unsubscribe the Subscriber from the Node.

    --spec(unsubscribe_node/4 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - Sender :: jid(), - Subscriber :: ljid(), - SubId :: subId()) - -> {result, default} - % - | {error, xmlel()} -). - -unsubscribe_node(NodeIdx, Sender, Subscriber, SubId) -> - SubKey = jlib:jid_tolower(Subscriber), - GenKey = jlib:jid_remove_resource(SubKey), - Authorized = - jlib:jid_tolower(jlib:jid_remove_resource(Sender)) == - GenKey, - GenState = get_state(NodeIdx, GenKey), - SubState = case SubKey of - GenKey -> GenState; - _ -> get_state(NodeIdx, SubKey) - end, - Subscriptions = lists:filter(fun ({_Sub, _SubId}) -> - true; - (_SubId) -> false - end, - SubState#pubsub_state.subscriptions), - SubIdExists = case SubId of - <<>> -> false; - Binary when is_binary(Binary) -> true; - _ -> false - end, - if - %% Requesting entity is prohibited from unsubscribing entity - not Authorized -> {error, ?ERR_FORBIDDEN}; - %% Entity did not specify SubId - %%SubId == "", ?? -> - %% {error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")}; - %% Invalid subscription identifier - %%InvalidSubId -> - %% {error, ?ERR_EXTENDED(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; - %% Requesting entity is not a subscriber - Subscriptions == [] -> - {error, - ?ERR_EXTENDED((?ERR_UNEXPECTED_REQUEST_CANCEL), - <<"not-subscribed">>)}; - %% Subid supplied, so use that. - SubIdExists -> - Sub = first_in_list(fun (S) -> - case S of - {_Sub, SubId} -> true; - _ -> false - end - end, - SubState#pubsub_state.subscriptions), - case Sub of - {value, S} -> - delete_subscriptions(SubKey, NodeIdx, [S], SubState), - {result, default}; - false -> - {error, - ?ERR_EXTENDED((?ERR_UNEXPECTED_REQUEST_CANCEL), - <<"not-subscribed">>)} - end; - %% Asking to remove all subscriptions to the given node - SubId == all -> - delete_subscriptions(SubKey, NodeIdx, Subscriptions, SubState), - {result, default}; - %% No subid supplied, but there's only one matching subscription - length(Subscriptions) == 1 -> - delete_subscriptions(SubKey, NodeIdx, Subscriptions, SubState), - {result, default}; - %% No subid and more than one possible subscription match. - true -> - {error, - ?ERR_EXTENDED((?ERR_BAD_REQUEST), <<"subid-required">>)} - end. - --spec(delete_subscriptions/4 :: -( - SubKey :: ljid(), - NodeIdx :: mod_pubsub:nodeIdx(), - Subscriptions :: [{mod_pubsub:subscription(), mod_pubsub:subId()}], - SubState :: mod_pubsub:pubsubState()) - -> ok -). -delete_subscriptions(SubKey, NodeIdx, Subscriptions, SubState) -> - NewSubs = lists:foldl(fun ({Subscription, SubId}, Acc) -> - pubsub_subscription:delete_subscription(SubKey, NodeIdx, SubId), - Acc -- [{Subscription, SubId}] - end, SubState#pubsub_state.subscriptions, Subscriptions), - case {SubState#pubsub_state.affiliation, NewSubs} of - {none, []} -> del_state(NodeIdx, SubKey); - _ -> set_state(SubState#pubsub_state{subscriptions = NewSubs}) - end. - -%% @spec (NodeIdx, Publisher, PublishModel, MaxItems, ItemId, Payload) -> -%% {result, {default, broadcast, ItemIds}} | {error, Reason} -%% NodeIdx = mod_pubsub:nodeIdx() -%% Publisher = mod_pubsub:jid() -%% PublishModel = atom() -%% MaxItems = integer() -%% ItemId = mod_pubsub:itemId() -%% Payload = mod_pubsub:payload() -%% ItemIds = [mod_pubsub:itemId()] | [] -%% Reason = mod_pubsub:stanzaError() -%% @doc

    Publishes the item passed as parameter.

    -%%

    The mechanism works as follow: -%%

      -%%
    • The main PubSub module prepares the item to publish and passes the -%% result of the preparation as a {@link mod_pubsub:pubsubItem()} record.
    • -%%
    • This function gets the prepared record and several other parameters and can decide to:
        -%%
      • reject the publication;
      • -%%
      • allow the publication as is, letting the main module perform the database persistance;
      • -%%
      • allow the publication, modifying the record. The main module will store the modified record;
      • -%%
      • allow it, but perform the needed persistance operations.
      -%%

    -%%

    The selected behaviour depends on the return parameter: -%%

      -%%
    • {error, Reason}: an iq error result will be return. No -%% publication is actually performed.
    • -%%
    • true: Publication operation is allowed, based on the -%% unmodified record passed in parameter Item. If the Item -%% parameter contains an error, no subscription will actually be -%% performed.
    • -%%
    • {true, Item}: Publication operation is allowed, but the -%% {@link mod_pubsub:pubsubItem()} record returned replaces the value passed -%% in parameter Item. The persistance will be performed by the main -%% module.
    • -%%
    • {true, done}: Publication operation is allowed, but the -%% {@link mod_pubsub:pubsubItem()} will be considered as already stored and -%% no further persistance operation will be performed. This case is used, -%% when the plugin module is doing the persistance by itself or when it want -%% to completly disable persistance.
    -%%

    -%%

    In the default plugin module, the record is unchanged.

    --spec(publish_item/6 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - Publisher :: jid(), - PublishModel :: mod_pubsub:publishModel(), - Max_Items :: non_neg_integer(), - ItemId :: <<>> | mod_pubsub:itemId(), - Payload :: mod_pubsub:payload()) - -> {result, {default, broadcast, [mod_pubsub:itemId()]}} - %%% - | {error, xmlel()} -). - -publish_item(NodeIdx, Publisher, PublishModel, MaxItems, ItemId, Payload) -> - SubKey = jlib:jid_tolower(Publisher), - GenKey = jlib:jid_remove_resource(SubKey), - GenState = get_state(NodeIdx, GenKey), - SubState = case SubKey of - GenKey -> GenState; - _ -> get_state(NodeIdx, SubKey) - end, - Affiliation = GenState#pubsub_state.affiliation, - Subscribed = case PublishModel of - subscribers -> - is_subscribed(SubState#pubsub_state.subscriptions); - _ -> undefined - end, - if not - ((PublishModel == open) or - (PublishModel == publishers) and - ((Affiliation == owner) or (Affiliation == publisher)) - or (Subscribed == true)) -> - {error, ?ERR_FORBIDDEN}; - true -> - if MaxItems > 0 -> - Now = now(), - PubId = {Now, SubKey}, - Item = case get_item(NodeIdx, ItemId) of - {result, OldItem} -> - OldItem#pubsub_item{modification = PubId, - payload = Payload}; - _ -> - #pubsub_item{itemid = {ItemId, NodeIdx}, - creation = {Now, GenKey}, - modification = PubId, - payload = Payload} - end, - Items = [ItemId | GenState#pubsub_state.items -- - [ItemId]], - {result, {NI, OI}} = remove_extra_items(NodeIdx, - MaxItems, Items), - set_item(Item), - set_state(GenState#pubsub_state{items = NI}), - {result, {default, broadcast, OI}}; - true -> {result, {default, broadcast, []}} - end - end. - -%% @spec (NodeIdx, MaxItems, ItemIds) -> {result, {NewItemIds,OldItemIds}} -%% NodeIdx = mod_pubsub:nodeIdx() -%% MaxItems = integer() | unlimited -%% ItemIds = [mod_pubsub:itemId()] -%% NewItemIds = [mod_pubsub:itemId()] -%% OldItemIds = [mod_pubsub:itemId()] | [] -%% @doc

    This function is used to remove extra items, most notably when the -%% maximum number of items has been reached.

    -%%

    This function is used internally by the core PubSub module, as no -%% permission check is performed.

    -%%

    In the default plugin module, the oldest items are removed, but other -%% rules can be used.

    -%%

    If another PubSub plugin wants to delegate the item removal (and if the -%% plugin is using the default pubsub storage), it can implements this function like this: -%% ```remove_extra_items(NodeIdx, MaxItems, ItemIds) -> -%% node_default:remove_extra_items(NodeIdx, MaxItems, ItemIds).'''

    --spec(remove_extra_items/3 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - Max_Items :: unlimited | non_neg_integer(), - ItemIds :: [mod_pubsub:itemId()]) - -> {result, - {NewItems::[mod_pubsub:itemId()], - OldItems::[mod_pubsub:itemId()]} - } -). -remove_extra_items(_NodeIdx, unlimited, ItemIds) -> - {result, {ItemIds, []}}; -remove_extra_items(NodeIdx, MaxItems, ItemIds) -> - NewItems = lists:sublist(ItemIds, MaxItems), - OldItems = lists:nthtail(length(NewItems), ItemIds), - del_items(NodeIdx, OldItems), - {result, {NewItems, OldItems}}. - -%% @spec (NodeIdx, Publisher, PublishModel, ItemId) -> -%% {result, {default, broadcast}} | {error, Reason} -%% NodeIdx = mod_pubsub:nodeIdx() -%% Publisher = mod_pubsub:jid() -%% PublishModel = atom() -%% ItemId = mod_pubsub:itemId() -%% Reason = mod_pubsub:stanzaError() -%% @doc

    Triggers item deletion.

    -%%

    Default plugin: The user performing the deletion must be the node owner -%% or a publisher, or PublishModel being open.

    --spec(delete_item/4 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - Publisher :: jid(), - PublishModel :: mod_pubsub:publishModel(), - ItemId :: <<>> | mod_pubsub:itemId()) - -> {result, {default, broadcast}} - %%% - | {error, xmlel()} -). -delete_item(NodeIdx, Publisher, PublishModel, ItemId) -> - SubKey = jlib:jid_tolower(Publisher), - GenKey = jlib:jid_remove_resource(SubKey), - GenState = get_state(NodeIdx, GenKey), - #pubsub_state{affiliation = Affiliation, items = Items} = GenState, - Allowed = Affiliation == publisher orelse - Affiliation == owner orelse - PublishModel == open orelse - case get_item(NodeIdx, ItemId) of - {result, #pubsub_item{creation = {_, GenKey}}} -> true; - _ -> false - end, - if not Allowed -> {error, ?ERR_FORBIDDEN}; - true -> - case lists:member(ItemId, Items) of - true -> - del_item(NodeIdx, ItemId), - set_state(GenState#pubsub_state{items = lists:delete(ItemId, Items)}), - {result, {default, broadcast}}; - false -> - case Affiliation of - owner -> - {result, States} = get_states(NodeIdx), - lists:foldl(fun (#pubsub_state{items = PI} = S, Res) -> - case lists:member(ItemId, PI) of - true -> - del_item(NodeIdx, ItemId), - set_state(S#pubsub_state{items - = lists:delete(ItemId, PI)}), - {result, {default, broadcast}}; - false -> Res - end; - (_, Res) -> Res - end, - {error, ?ERR_ITEM_NOT_FOUND}, States); - _ -> {error, ?ERR_ITEM_NOT_FOUND} - end - end - end. - -%% @spec (NodeIdx, Owner) -> {error, Reason} | {result, {default, broadcast}} -%% NodeIdx = mod_pubsub:nodeIdx() -%% Owner = mod_pubsub:jid() -%% Reason = mod_pubsub:stanzaError() --spec(purge_node/2 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - Owner :: jid()) - -> {result, {default, broadcast}} - | {error, xmlel()} -). - -purge_node(NodeIdx, Owner) -> - SubKey = jlib:jid_tolower(Owner), - GenKey = jlib:jid_remove_resource(SubKey), - GenState = get_state(NodeIdx, GenKey), - case GenState of - #pubsub_state{affiliation = owner} -> - {result, States} = get_states(NodeIdx), - lists:foreach(fun (#pubsub_state{items = []}) -> ok; - (#pubsub_state{items = Items} = S) -> - del_items(NodeIdx, Items), - set_state(S#pubsub_state{items = []}) - end, - States), - {result, {default, broadcast}}; - _ -> {error, ?ERR_FORBIDDEN} - end. - -%% @spec (Host, Owner) -> {result, Reply} -%% Host = mod_pubsub:hostPubsub() -%% Owner = mod_pubsub:jid() -%% Reply = [] | [{mod_pubsub:pubsubNode(), mod_pubsub:affiliation()}] -%% @doc

    Return the current affiliations for the given user

    -%%

    The default module reads affiliations in the main Mnesia -%% pubsub_state table. If a plugin stores its data in the same -%% table, it should return an empty list, as the affiliation will be read by -%% the default PubSub module. Otherwise, it should return its own affiliation, -%% that will be added to the affiliation stored in the main -%% pubsub_state table.

    --spec(get_entity_affiliations/2 :: -( - Host :: mod_pubsub:host(), - Owner :: jid()) - -> {result, [{mod_pubsub:pubsubNode(), mod_pubsub:affiliation()}]} -). - -get_entity_affiliations(Host, Owner) -> - SubKey = jlib:jid_tolower(Owner), - GenKey = jlib:jid_remove_resource(SubKey), - States = mnesia:match_object(#pubsub_state{stateid = {GenKey, '_'}, _ = '_'}), - NodeTree = case catch - ets:lookup(gen_mod:get_module_proc(Host, config), - nodetree) - of - [{nodetree, N}] -> N; - _ -> nodetree_tree - end, - Reply = lists:foldl(fun (#pubsub_state{stateid = {_, N}, affiliation = A}, - Acc) -> - case NodeTree:get_node(N) of - #pubsub_node{nodeid = {Host, _}} = Node -> - [{Node, A} | Acc]; - _ -> Acc - end - end, - [], States), - {result, Reply}. - --spec(get_node_affiliations/1 :: -( - NodeIdx::mod_pubsub:nodeIdx()) - -> {result, [{ljid(), mod_pubsub:affiliation()}]} -). - -get_node_affiliations(NodeIdx) -> - {result, States} = get_states(NodeIdx), - Tr = fun (#pubsub_state{stateid = {J, _}, - affiliation = A}) -> - {J, A} - end, - {result, lists:map(Tr, States)}. - --spec(get_affiliation/2 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - Owner :: jid()) - -> {result, mod_pubsub:affiliation()} -). - -get_affiliation(NodeIdx, Owner) -> - SubKey = jlib:jid_tolower(Owner), - GenKey = jlib:jid_remove_resource(SubKey), - #pubsub_state{affiliation = Affiliation} = get_state(NodeIdx, GenKey), - {result, Affiliation}. - --spec(set_affiliation/3 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - Owner :: ljid(), - Affiliation :: mod_pubsub:affiliation()) - -> ok -). -set_affiliation(NodeIdx, Owner, Affiliation) -> - SubKey = jlib:jid_tolower(Owner), - GenKey = jlib:jid_remove_resource(SubKey), - GenState = get_state(NodeIdx, GenKey), - case {Affiliation, GenState#pubsub_state.subscriptions} of - {none, []} -> del_state(NodeIdx, GenKey); - _ -> set_state(GenState#pubsub_state{affiliation = Affiliation}) - end. - -%% @spec (Host, Owner) -> -%% {'result', [] -%% | [{Node, Subscription, SubId, Entity}] -%% | [{Node, Subscription, Entity}]} -%% Host = mod_pubsub:hostPubsub() -%% Owner = mod_pubsub:jid() -%% Node = mod_pubsub:pubsubNode() -%% Subscription = mod_pubsub:subscription() -%% SubId = mod_pubsub:subId() -%% Entity = mod_pubsub:ljid() -%% @doc

    Return the current subscriptions for the given user

    -%%

    The default module reads subscriptions in the main Mnesia -%% pubsub_state table. If a plugin stores its data in the same -%% table, it should return an empty list, as the affiliation will be read by -%% the default PubSub module. Otherwise, it should return its own affiliation, -%% that will be added to the affiliation stored in the main -%% pubsub_state table.

    --spec(get_entity_subscriptions/2 :: -( - Host :: mod_pubsub:host(), - Owner :: jid()) - -> {result, - [{mod_pubsub:pubsubNode(), - mod_pubsub:subscription(), - mod_pubsub:subId(), - ljid()}] - } -). - -get_entity_subscriptions(Host, Owner) -> - {U, D, _} = SubKey = jlib:jid_tolower(Owner), - GenKey = jlib:jid_remove_resource(SubKey), - States = case SubKey of - GenKey -> - mnesia:match_object(#pubsub_state{stateid = - {{U, D, '_'}, '_'}, - _ = '_'}); - _ -> - mnesia:match_object(#pubsub_state{stateid = - {GenKey, '_'}, - _ = '_'}) - ++ - mnesia:match_object(#pubsub_state{stateid = - {SubKey, '_'}, - _ = '_'}) - end, - NodeTree = case catch - ets:lookup(gen_mod:get_module_proc(Host, config), - nodetree) - of - [{nodetree, N}] -> N; - _ -> nodetree_tree - end, - Reply = lists:foldl(fun (#pubsub_state{stateid = {J, N}, subscriptions = Ss}, - Acc) -> - case NodeTree:get_node(N) of - #pubsub_node{nodeid = {Host, _}} = Node -> - lists:foldl(fun ({Sub, SubId}, Acc2) -> - [{Node, Sub, SubId, J} | Acc2] - end, - Acc, Ss); - _ -> Acc - end - end, - [], States), - {result, Reply}. - --spec(get_node_subscriptions/1 :: -( - NodeIdx::mod_pubsub:nodeIdx()) - -> {result, - [{ljid(), mod_pubsub:subscription(), mod_pubsub:subId()}] | - [{ljid(), none},...] - } -). -get_node_subscriptions(NodeIdx) -> - {result, States} = get_states(NodeIdx), - Tr = fun (#pubsub_state{stateid = {J, _}, - subscriptions = Subscriptions}) -> - case Subscriptions of - [_ | _] -> - lists:foldl(fun ({S, SubId}, Acc) -> - [{J, S, SubId} | Acc] - end, - [], Subscriptions); - [] -> []; - _ -> [{J, none}] - end - end, - {result, lists:flatmap(Tr, States)}. - --spec(get_subscriptions/2 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - Owner :: ljid()) - -> {result, [{mod_pubsub:subscription(), mod_pubsub:subId()}]} -). -get_subscriptions(NodeIdx, Owner) -> - SubKey = jlib:jid_tolower(Owner), - SubState = get_state(NodeIdx, SubKey), - {result, SubState#pubsub_state.subscriptions}. - --spec(set_subscriptions/4 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - Owner :: jid(), - Subscription :: mod_pubsub:subscription(), - SubId :: mod_pubsub:subId()) - -> ok - %%% - | {error, xmlel()} -). - -set_subscriptions(NodeIdx, Owner, Subscription, SubId) -> - SubKey = jlib:jid_tolower(Owner), - SubState = get_state(NodeIdx, SubKey), - case {SubId, SubState#pubsub_state.subscriptions} of - {_, []} -> - case Subscription of - none -> - {error, - ?ERR_EXTENDED((?ERR_BAD_REQUEST), - <<"not-subscribed">>)}; - _ -> - new_subscription(NodeIdx, Owner, Subscription, SubState) - end; - {<<>>, [{_, SID}]} -> - case Subscription of - none -> unsub_with_subid(NodeIdx, SID, SubState); - _ -> replace_subscription({Subscription, SID}, SubState) - end; - {<<>>, [_ | _]} -> - {error, - ?ERR_EXTENDED((?ERR_BAD_REQUEST), - <<"subid-required">>)}; - _ -> - case Subscription of - none -> unsub_with_subid(NodeIdx, SubId, SubState); - _ -> - replace_subscription({Subscription, SubId}, SubState) - end - end. - -replace_subscription(NewSub, SubState) -> - NewSubs = replace_subscription(NewSub, SubState#pubsub_state.subscriptions, []), - set_state(SubState#pubsub_state{subscriptions = NewSubs}). - -replace_subscription(_, [], Acc) -> Acc; -replace_subscription({Sub, SubId}, [{_, SubID} | T], Acc) -> - replace_subscription({Sub, SubId}, T, [{Sub, SubID} | Acc]). - -new_subscription(NodeId, Owner, Subscription, SubState) -> - SubId = pubsub_subscription:add_subscription(Owner, NodeId, []), - Subscriptions = SubState#pubsub_state.subscriptions, - set_state(SubState#pubsub_state{subscriptions = - [{Subscription, SubId} | Subscriptions]}), - {Subscription, SubId}. - --spec(unsub_with_subid/3 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - SubId :: mod_pubsub:subId(), - SubState :: mod_pubsub:pubsubState()) - -> ok -). -unsub_with_subid(NodeIdx, SubId, #pubsub_state{stateid = {Entity, _}} = SubState) -> - pubsub_subscription:delete_subscription(SubState#pubsub_state.stateid, - NodeIdx, SubId), - NewSubs = lists:filter(fun ({_, SID}) -> SubId =/= SID - end, - SubState#pubsub_state.subscriptions), - case {NewSubs, SubState#pubsub_state.affiliation} of - {[], none} -> - del_state(NodeIdx, Entity); - _ -> - set_state(SubState#pubsub_state{subscriptions = NewSubs}) - end. - -%% TODO : doc -%% @spec (Host, Owner) -> {result, Reply} | {error, Reason} -%% Host = mod_pubsub:hostPubsub() -%% Owner = mod_pubsub:jid() -%% Reply = [] | [mod_pubsub:nodeId()] -%% @doc

    Returns a list of Owner's nodes on Host with pending -%% subscriptions.

    --spec(get_pending_nodes/2 :: -( - Host :: mod_pubsub:host(), - Owner :: jid()) - -> {result, [mod_pubsub:nodeId()]} -). - -get_pending_nodes(Host, Owner) -> - GenKey = jlib:jid_remove_resource(jlib:jid_tolower(Owner)), - States = mnesia:match_object(#pubsub_state{stateid = - {GenKey, '_'}, - affiliation = owner, _ = '_'}), - NodeIDs = [ID - || #pubsub_state{stateid = {_, ID}} <- States], - NodeTree = case catch - ets:lookup(gen_mod:get_module_proc(Host, config), - nodetree) - of - [{nodetree, N}] -> N; - _ -> nodetree_tree - end, - Reply = mnesia:foldl(fun (#pubsub_state{stateid = {_, NID}} = S, - Acc) -> - case lists:member(NID, NodeIDs) of - true -> - case get_nodes_helper(NodeTree, S) of - {value, Node} -> [Node | Acc]; - false -> Acc - end; - false -> Acc - end - end, - [], pubsub_state), - {result, Reply}. - --spec(get_nodes_helper/2 :: -( - NodeTree :: module(), - Pubsub_State :: mod_pubsub:pubsubState()) - -> {value, NodeId::mod_pubsub:nodeId()} - | false - -). -get_nodes_helper(NodeTree, #pubsub_state{stateid = {_, N}, subscriptions = Subs}) -> - HasPending = fun ({pending, _}) -> true; - (pending) -> true; - (_) -> false - end, - case lists:any(HasPending, Subs) of - true -> - case NodeTree:get_node(N) of - #pubsub_node{nodeid = {_, Node}} -> {value, Node}; - _ -> false - end; - false -> false - end. - -%% @spec (NodeIdx) -> {result, States} -%% NodeIdx = mod_pubsub:nodeIdx() -%% States = [] | [mod_pubsub:pubsubState()] -%% @doc Returns the list of stored states for a given node. -%%

    For the default PubSub module, states are stored in Mnesia database.

    -%%

    We can consider that the pubsub_state table have been created by the main -%% mod_pubsub module.

    -%%

    PubSub plugins can store the states where they wants (for example in a -%% relational database).

    -%%

    If a PubSub plugin wants to delegate the states storage to the default node, -%% they can implement this function like this: -%% ```get_states(NodeIdx) -> -%% node_default:get_states(NodeIdx).'''

    --spec(get_states/1 :: -( - NodeIdx::mod_pubsub:nodeIdx()) - -> {result, [mod_pubsub:pubsubState()]} -). - -get_states(NodeIdx) -> - States = case catch mnesia:match_object( - #pubsub_state{stateid = {'_', NodeIdx}, _ = '_'}) of - List when is_list(List) -> List; - _ -> [] - end, - {result, States}. - -%% @spec (NodeIdx, JID) -> State -%% NodeIdx = mod_pubsub:nodeIdx() -%% JID = mod_pubsub:jid() -%% State = mod_pubsub:pubsubState() -%% @doc

    Returns a state (one state list), given its reference.

    --spec(get_state/2 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - JID :: ljid()) - -> mod_pubsub:pubsubState() -). - -get_state(NodeIdx, JID) -> - StateId = {JID, NodeIdx}, - case catch mnesia:read({pubsub_state, StateId}) of - [State] when is_record(State, pubsub_state) -> State; - _ -> #pubsub_state{stateid=StateId} - end. - -%% @spec (State) -> ok | {error, Reason} -%% State = mod_pubsub:pubsubState() -%% Reason = mod_pubsub:stanzaError() -%% @doc

    Write a state into database.

    --spec(set_state/1 :: -( - State::mod_pubsub:pubsubState()) - -> ok -). -set_state(State) when is_record(State, pubsub_state) -> - mnesia:write(State). -%set_state(_) -> {error, ?ERR_INTERNAL_SERVER_ERROR}. - -%% @spec (NodeIdx, JID) -> ok | {error, Reason} -%% NodeIdx = mod_pubsub:nodeIdx() -%% JID = mod_pubsub:jid() -%% Reason = mod_pubsub:stanzaError() -%% @doc

    Delete a state from database.

    --spec(del_state/2 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - JID :: ljid()) - -> ok -). -del_state(NodeIdx, JID) -> - mnesia:delete({pubsub_state, {JID, NodeIdx}}). - -%% @spec (NodeIdx, From) -> {result, Items} -%% NodeIdx = mod_pubsub:nodeIdx() -%% From = mod_pubsub:jid() -%% Items = [] | [mod_pubsub:pubsubItem()] -%% @doc Returns the list of stored items for a given node. -%%

    For the default PubSub module, items are stored in Mnesia database.

    -%%

    We can consider that the pubsub_item table have been created by the main -%% mod_pubsub module.

    -%%

    PubSub plugins can store the items where they wants (for example in a -%% relational database), or they can even decide not to persist any items.

    -%%

    If a PubSub plugin wants to delegate the item storage to the default node, -%% they can implement this function like this: -%% ```get_items(NodeIdx, From) -> -%% node_default:get_items(NodeIdx, From).'''

    --spec(get_items/2 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - _From :: jid()) - -> {result, [mod_pubsub:pubsubItem()]} -). - -get_items(NodeIdx, _From) -> - Items = mnesia:match_object(#pubsub_item{itemid = {'_', NodeIdx}, _ = '_'}), - {result, lists:reverse(lists:keysort(#pubsub_item.modification, Items))}. - --spec(get_items/6 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - JID :: jid(), - AccessModel :: mod_pubsub:accessModel(), - Presence_Subscription :: boolean(), - RosterGroup :: boolean(), - _SubId :: mod_pubsub:subId()) - -> {result, [mod_pubsub:pubsubItem()]} - %%% - | {error, xmlel()} -). - -get_items(NodeIdx, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId) -> - SubKey = jlib:jid_tolower(JID), - GenKey = jlib:jid_remove_resource(SubKey), - GenState = get_state(NodeIdx, GenKey), - SubState = get_state(NodeIdx, SubKey), - Affiliation = GenState#pubsub_state.affiliation, - Subscriptions = SubState#pubsub_state.subscriptions, - Whitelisted = can_fetch_item(Affiliation, Subscriptions), - if %%SubId == "", ?? -> - %% Entity has multiple subscriptions to the node but does not specify a subscription ID - %{error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")}; - %%InvalidSubId -> - %% Entity is subscribed but specifies an invalid subscription ID - %{error, ?ERR_EXTENDED(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; - GenState#pubsub_state.affiliation == outcast -> - {error, ?ERR_FORBIDDEN}; - (AccessModel == presence) and - not PresenceSubscription -> - {error, - ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), - <<"presence-subscription-required">>)}; - (AccessModel == roster) and not RosterGroup -> - {error, - ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), - <<"not-in-roster-group">>)}; - (AccessModel == whitelist) and not Whitelisted -> - {error, - ?ERR_EXTENDED((?ERR_NOT_ALLOWED), <<"closed-node">>)}; - (AccessModel == authorize) and not Whitelisted -> - {error, ?ERR_FORBIDDEN}; - %%MustPay -> - %% % Payment is required for a subscription - %% {error, ?ERR_PAYMENT_REQUIRED}; - true -> get_items(NodeIdx, JID) - end. - -%% @spec (NodeIdx, ItemId) -> {result, Item} | {error, 'item-not-found'} -%% NodeIdx = mod_pubsub:nodeIdx() -%% ItemId = mod_pubsub:itemId() -%% Item = mod_pubsub:pubsubItem() -%% @doc

    Returns an item (one item list), given its reference.

    --spec(get_item/2 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - ItemId :: mod_pubsub:itemId()) - -> {result, mod_pubsub:pubsubItem()} - | {error, xmlel()} -). - -get_item(NodeIdx, ItemId) -> - case mnesia:read({pubsub_item, {ItemId, NodeIdx}}) of - [Item] when is_record(Item, pubsub_item) -> {result, Item}; - _ -> {error, ?ERR_ITEM_NOT_FOUND} - end. - -%% @spec (NodeIdx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> {result, Item} | {error, Reason} -%% NodeIdx = mod_pubsub:nodeIdx() -%% ItemId = mod_pubsub:itemId() -%% JID = mod_pubsub:jid() -%% AccessModel = mod_pubsub:accessModel() -%% PresenceSubscription = boolean() -%% RosterGroup = boolean() -%% SubId = mod_pubsub:subId() -%% Item = mod_pubsub:pubsubItem() -%% Reason = mod_pubsub:stanzaError() | 'item-not-found' --spec(get_item/7 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - ItemId :: mod_pubsub:itemId(), - JID :: jid(), - AccessModel :: mod_pubsub:accessModel(), - PresenceSubscription :: boolean(), - RosterGroup :: boolean(), - SubId :: mod_pubsub:subId()) - -> {result, mod_pubsub:pubsubItem()} - | {error, xmlel()} -). - -get_item(NodeIdx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, - _SubId) -> - SubKey = jlib:jid_tolower(JID), - GenKey = jlib:jid_remove_resource(SubKey), - GenState = get_state(NodeIdx, GenKey), - Affiliation = GenState#pubsub_state.affiliation, - Subscriptions = GenState#pubsub_state.subscriptions, - Whitelisted = can_fetch_item(Affiliation, Subscriptions), - if %%SubId == "", ?? -> - %% Entity has multiple subscriptions to the node but does not specify a subscription ID - %{error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")}; - %%InvalidSubId -> - %% Entity is subscribed but specifies an invalid subscription ID - %{error, ?ERR_EXTENDED(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; - GenState#pubsub_state.affiliation == outcast -> - {error, ?ERR_FORBIDDEN}; - (AccessModel == presence) and - not PresenceSubscription -> - {error, - ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), - <<"presence-subscription-required">>)}; - (AccessModel == roster) and not RosterGroup -> - {error, - ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), <<"not-in-roster-group">>)}; - (AccessModel == whitelist) and not Whitelisted -> - {error, - ?ERR_EXTENDED((?ERR_NOT_ALLOWED), <<"closed-node">>)}; - (AccessModel == authorize) and not Whitelisted -> - {error, ?ERR_FORBIDDEN}; - %%MustPay -> - %% % Payment is required for a subscription - %% {error, ?ERR_PAYMENT_REQUIRED}; - true -> get_item(NodeIdx, ItemId) - end. - -%% @spec (Item) -> ok | {error, Reason} -%% Item = mod_pubsub:pubsubItem() -%% Reason = mod_pubsub:stanzaError() -%% @doc

    Write an item into database.

    --spec(set_item/1 :: -( - Item::mod_pubsub:pubsubItem()) - -> ok -). -set_item(Item) when is_record(Item, pubsub_item) -> - mnesia:write(Item). -%set_item(_) -> {error, ?ERR_INTERNAL_SERVER_ERROR}. - -%% @spec (NodeIdx, ItemId) -> ok | {error, Reason} -%% NodeIdx = mod_pubsub:nodeIdx() -%% ItemId = mod_pubsub:itemId() -%% Reason = mod_pubsub:stanzaError() -%% @doc

    Delete an item from database.

    --spec(del_item/2 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - ItemId :: mod_pubsub:itemId()) - -> ok -). -del_item(NodeIdx, ItemId) -> - mnesia:delete({pubsub_item, {ItemId, NodeIdx}}). - --spec(del_items/2 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - ItemIds :: [mod_pubsub:pubsubItem(),...]) - -> ok -). - -del_items(NodeIdx, ItemIds) -> - lists:foreach(fun (ItemId) -> del_item(NodeIdx, ItemId) - end, - ItemIds). - -get_item_name(_Host, _Node, Id) -> Id. - -%% @doc

    Return the name of the node if known: Default is to return -%% node id.

    --spec(node_to_path/1 :: -( - Node::binary()) - -> [binary()] -). -node_to_path(Node) -> str:tokens((Node), <<"/">>). - --spec(path_to_node/1 :: -( - Path :: [binary()]) - -> binary() -). - -path_to_node([]) -> <<>>; -path_to_node(Path) -> - iolist_to_binary(str:join([<<"">> | Path], <<"/">>)). - -%% @spec (Affiliation, Subscription) -> true | false -%% Affiliation = owner | member | publisher | outcast | none -%% Subscription = subscribed | none -%% @doc Determines if the combination of Affiliation and Subscribed -%% are allowed to get items from a node. -can_fetch_item(owner, _) -> true; -can_fetch_item(member, _) -> true; -can_fetch_item(publisher, _) -> true; -can_fetch_item(outcast, _) -> false; -can_fetch_item(none, Subscriptions) -> - is_subscribed(Subscriptions). -%can_fetch_item(_Affiliation, _Subscription) -> false. - -is_subscribed(Subscriptions) -> - lists:any(fun ({subscribed, _SubId}) -> true; - (_) -> false - end, - Subscriptions). - -%% Returns the first item where Pred() is true in List -first_in_list(_Pred, []) -> false; -first_in_list(Pred, [H | T]) -> - case Pred(H) of - true -> {value, H}; - _ -> first_in_list(Pred, T) - end. diff --git a/src/node_hometree_odbc.erl b/src/node_hometree_odbc.erl deleted file mode 100644 index 0678b9898..000000000 --- a/src/node_hometree_odbc.erl +++ /dev/null @@ -1,1672 +0,0 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. -%%% -%%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. -%%% -%%% -%%% The Initial Developer of the Original Code is ProcessOne. -%%% Portions created by ProcessOne are Copyright 2006-2015, ProcessOne -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2015, ProcessOne. -%%% -%%% -%%% @copyright 2006-2015 ProcessOne -%%% @author Christophe Romain -%%% [http://www.process-one.net/] -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== - -%%% @todo The item table should be handled by the plugin, but plugin that do -%%% not want to manage it should be able to use the default behaviour. -%%% @todo Plugin modules should be able to register to receive presence update -%%% send to pubsub. - -%%% @doc The module {@module} is the default PubSub plugin. -%%%

    It is used as a default for all unknown PubSub node type. It can serve -%%% as a developer basis and reference to build its own custom pubsub node -%%% types.

    -%%%

    PubSub plugin nodes are using the {@link gen_node} behaviour.

    -%%%

    The API isn't stabilized yet. The pubsub plugin -%%% development is still a work in progress. However, the system is already -%%% useable and useful as is. Please, send us comments, feedback and -%%% improvements.

    - --module(node_hometree_odbc). - --author('christophe.romain@process-one.net'). - --include("pubsub.hrl"). - --include("jlib.hrl"). - --define(PUBSUB, mod_pubsub_odbc). - --behaviour(gen_pubsub_node). - -%% API definition --export([init/3, terminate/2, options/0, features/0, - create_node_permission/6, create_node/2, delete_node/1, - purge_node/2, subscribe_node/8, unsubscribe_node/4, - publish_item/6, delete_item/4, remove_extra_items/3, - get_entity_affiliations/2, get_node_affiliations/1, - get_affiliation/2, set_affiliation/3, - get_entity_subscriptions/2, - get_entity_subscriptions_for_send_last/2, - get_node_subscriptions/1, get_subscriptions/2, - set_subscriptions/4, get_pending_nodes/2, get_states/1, - get_state/2, set_state/1, get_items/7, get_items/6, - get_items/3, get_items/2, get_item/7, get_item/2, - set_item/1, get_item_name/3, get_last_items/3, - path_to_node/1, node_to_path/1]). - --export([decode_jid/1, - decode_affiliation/1, decode_subscriptions/1, - encode_jid/1, encode_affiliation/1, - encode_subscriptions/1]). - -%% ================ -%% API definition -%% ================ - -%% @spec (Host, ServerHost, Opts) -> any() -%% Host = mod_pubsub:host() -%% ServerHost = mod_pubsub:host() -%% Opts = list() -%% @doc

    Called during pubsub modules initialisation. Any pubsub plugin must -%% implement this function. It can return anything.

    -%%

    This function is mainly used to trigger the setup task necessary for the -%% plugin. It can be used for example by the developer to create the specific -%% module database schema if it does not exists yet.

    -init(_Host, _ServerHost, _Opts) -> - pubsub_subscription_odbc:init(), ok. - -%% @spec (Host, ServerHost) -> any() -%% Host = mod_pubsub:host() -%% ServerHost = host() -%% @doc

    Called during pubsub modules termination. Any pubsub plugin must -%% implement this function. It can return anything.

    -terminate(_Host, _ServerHost) -> ok. - -%% @spec () -> [Option] -%% Option = mod_pubsub:nodeOption() -%% @doc Returns the default pubsub node options. -%%

    Example of function return value:

    -%% ``` -%% [{deliver_payloads, true}, -%% {notify_config, false}, -%% {notify_delete, false}, -%% {notify_retract, true}, -%% {persist_items, true}, -%% {max_items, 10}, -%% {subscribe, true}, -%% {access_model, open}, -%% {publish_model, publishers}, -%% {max_payload_size, 100000}, -%% {send_last_published_item, never}, -%% {presence_based_delivery, false}]''' --spec(options/0 :: () -> NodeOptions::mod_pubsub:nodeOptions()). -options() -> - [{deliver_payloads, true}, {notify_config, false}, - {notify_delete, false}, {notify_retract, true}, - {purge_offline, false}, {persist_items, true}, - {max_items, ?MAXITEMS}, {subscribe, true}, - {access_model, open}, {roster_groups_allowed, []}, - {publish_model, publishers}, - {notification_type, headline}, - {max_payload_size, ?MAX_PAYLOAD_SIZE}, - {send_last_published_item, on_sub_and_presence}, - {deliver_notifications, true}, - {presence_based_delivery, false}, {odbc, true}, - {rsm, true}]. - -%% @spec () -> [] -%% @doc Returns the node features --spec(features/0 :: () -> Features::[Feature::binary(),...]). -features() -> - [<<"create-nodes">>, <<"auto-create">>, - <<"access-authorize">>, <<"delete-nodes">>, - <<"delete-items">>, <<"get-pending">>, - <<"instant-nodes">>, <<"manage-subscriptions">>, - <<"modify-affiliations">>, <<"multi-subscribe">>, - <<"outcast-affiliation">>, <<"persistent-items">>, - <<"publish">>, <<"purge-nodes">>, <<"retract-items">>, - <<"retrieve-affiliations">>, <<"retrieve-items">>, - <<"retrieve-subscriptions">>, <<"subscribe">>, - <<"subscription-notifications">>, - <<"subscription-options">>, <<"rsm">>]. - -%% @spec (Host, ServerHost, Node, ParentNode, Owner, Access) -> bool() -%% Host = mod_pubsub:host() -%% ServerHost = mod_pubsub:host() -%% Node = mod_pubsub:pubsubNode() -%% ParentNode = mod_pubsub:pubsubNode() -%% Owner = mod_pubsub:jid() -%% Access = all | atom() -%% @doc Checks if the current user has the permission to create the requested node -%%

    In {@link node_default}, the permission is decided by the place in the -%% hierarchy where the user is creating the node. The access parameter is also -%% checked in the default module. This parameter depends on the value of the -%% access_createnode ACL value in ejabberd config file.

    -%%

    This function also check that node can be created a a children of its -%% parent node

    -%%

    PubSub plugins can redefine the PubSub node creation rights as they -%% which. They can simply delegate this check to the {@link node_default} -%% module by implementing this function like this: -%% ```check_create_user_permission(Host, ServerHost, Node, ParentNode, Owner, Access) -> -%% node_default:check_create_user_permission(Host, ServerHost, Node, ParentNode, Owner, Access).'''

    --spec(create_node_permission/6 :: -( - Host :: mod_pubsub:host(), - ServerHost :: binary(), - Node :: mod_pubsub:nodeId(), - _ParentNode :: _, - Owner :: jid(), - Access :: atom()) - -> {result, boolean()} -). -create_node_permission(Host, ServerHost, Node, _ParentNode, Owner, Access) -> - LOwner = jlib:jid_tolower(Owner), - {User, Server, _Resource} = LOwner, - Allowed = case LOwner of - {<<"">>, Host, <<"">>} -> - true; % pubsub service always allowed - _ -> - case acl:match_rule(ServerHost, Access, LOwner) of - allow -> - case node_to_path(Node) of - [<<"home">>, Server, User | _] -> true; - _ -> false - end; - _ -> false - end - end, - {result, Allowed}. - -%% @spec (NodeId, Owner) -> -%% {result, Result} | exit -%% NodeId = mod_pubsub:pubsubNodeId() -%% Owner = mod_pubsub:jid() -%% @doc

    --spec(create_node/2 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - Owner :: jid()) - -> {result, {default, broadcast}} -). -create_node(NodeIdx, Owner) -> - OwnerKey = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), - State = #pubsub_state{stateid = {OwnerKey, NodeIdx}, affiliation = owner}, - catch - ejabberd_odbc:sql_query_t([<<"insert into pubsub_state(nodeid, jid, " - "affiliation, subscriptions) values(">>, - state_to_raw(NodeIdx, State), <<");">>]), - {result, {default, broadcast}}. - -%% @spec (Removed) -> ok -%% Removed = [mod_pubsub:pubsubNode()] -%% @doc

    purge items of deleted nodes after effective deletion.

    --spec(delete_node/1 :: -( - Removed :: [mod_pubsub:pubsubNode(),...]) - -> {result, {default, broadcast, _}} -). -delete_node(Removed) -> - Reply = lists:map(fun (#pubsub_node{id = NodeId} = - PubsubNode) -> - Subscriptions = case catch - ejabberd_odbc:sql_query_t([<<"select jid, subscriptions from pubsub_state " - "where nodeid='">>, - NodeId, - <<"';">>]) - of - {selected, - [<<"jid">>, - <<"subscriptions">>], - RItems} -> - lists:map(fun ([SJID, - Subscriptions]) -> - {decode_jid(SJID), - decode_subscriptions(Subscriptions)} - end, - RItems); - _ -> [] - end, - {PubsubNode, Subscriptions} - end, - Removed), - {result, {default, broadcast, Reply}}. - -%% @spec (NodeId, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options) -> -%% {error, Reason} | {result, Result} -%% @doc

    Accepts or rejects subcription requests on a PubSub node.

    -%%

    The mechanism works as follow: -%%

      -%%
    • The main PubSub module prepares the subscription and passes the -%% result of the preparation as a record.
    • -%%
    • This function gets the prepared record and several other parameters and -%% can decide to:
        -%%
      • reject the subscription;
      • -%%
      • allow it as is, letting the main module perform the database -%% persistance;
      • -%%
      • allow it, modifying the record. The main module will store the -%% modified record;
      • -%%
      • allow it, but perform the needed persistance operations.
      -%%

    -%%

    The selected behaviour depends on the return parameter: -%%

      -%%
    • {error, Reason}: an IQ error result will be returned. No -%% subscription will actually be performed.
    • -%%
    • true: Subscribe operation is allowed, based on the -%% unmodified record passed in parameter SubscribeResult. If this -%% parameter contains an error, no subscription will be performed.
    • -%%
    • {true, PubsubState}: Subscribe operation is allowed, but -%% the {@link mod_pubsub:pubsubState()} record returned replaces the value -%% passed in parameter SubscribeResult.
    • -%%
    • {true, done}: Subscribe operation is allowed, but the -%% {@link mod_pubsub:pubsubState()} will be considered as already stored and -%% no further persistance operation will be performed. This case is used, -%% when the plugin module is doing the persistance by itself or when it want -%% to completly disable persistance.
    -%%

    -%%

    In the default plugin module, the record is unchanged.

    --spec(subscribe_node/8 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - Sender :: jid(), - Subscriber :: ljid(), - AccessModel :: mod_pubsub:accessModel(), - SendLast :: 'never' | 'on_sub' | 'on_sub_and_presence', - PresenceSubscription :: boolean(), - RosterGroup :: boolean(), - Options :: mod_pubsub:subOptions()) - -> {result, {default, subscribed, mod_pubsub:subId()}} - | {result, {default, subscribed, mod_pubsub:subId(), send_last}} - | {result, {default, pending, mod_pubsub:subId()}} - %%% - | {error, _} - | {error, _, binary()} -). -subscribe_node(NodeId, Sender, Subscriber, AccessModel, - SendLast, PresenceSubscription, RosterGroup, Options) -> - SubKey = jlib:jid_tolower(Subscriber), - GenKey = jlib:jid_remove_resource(SubKey), - Authorized = - jlib:jid_tolower(jlib:jid_remove_resource(Sender)) == - GenKey, - {Affiliation, Subscriptions} = - select_affiliation_subscriptions(NodeId, GenKey, - SubKey), - Whitelisted = lists:member(Affiliation, - [member, publisher, owner]), - PendingSubscription = lists:any(fun ({pending, _}) -> - true; - (_) -> false - end, - Subscriptions), - if not Authorized -> - {error, - ?ERR_EXTENDED((?ERR_BAD_REQUEST), <<"invalid-jid">>)}; - Affiliation == outcast -> {error, ?ERR_FORBIDDEN}; - PendingSubscription -> - {error, - ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), - <<"pending-subscription">>)}; - (AccessModel == presence) and - not PresenceSubscription -> - {error, - ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), - <<"presence-subscription-required">>)}; - (AccessModel == roster) and not RosterGroup -> - {error, - ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), - <<"not-in-roster-group">>)}; - (AccessModel == whitelist) and not Whitelisted -> - {error, - ?ERR_EXTENDED((?ERR_NOT_ALLOWED), <<"closed-node">>)}; - %%MustPay -> - %% % Payment is required for a subscription - %% {error, ?ERR_PAYMENT_REQUIRED}; - %%ForbiddenAnonymous -> - %% % Requesting entity is anonymous - %% {error, ?ERR_FORBIDDEN}; - true -> - {result, SubId} = pubsub_subscription_odbc:subscribe_node(Subscriber, NodeId, Options), - NewSub = case AccessModel of - authorize -> pending; - _ -> subscribed - end, - update_subscription(NodeId, SubKey, - [{NewSub, SubId} | Subscriptions]), - case {NewSub, SendLast} of - {subscribed, never} -> - {result, {default, subscribed, SubId}}; - {subscribed, _} -> - {result, {default, subscribed, SubId, send_last}}; - {_, _} -> {result, {default, pending, SubId}} - end - end. - -%% @spec (NodeId, Sender, Subscriber, SubId) -> -%% {error, Reason} | {result, []} -%% NodeId = mod_pubsub:pubsubNodeId() -%% Sender = mod_pubsub:jid() -%% Subscriber = mod_pubsub:jid() -%% SubId = mod_pubsub:subid() -%% Reason = mod_pubsub:stanzaError() -%% @doc

    Unsubscribe the Subscriber from the Node.

    --spec(unsubscribe_node/4 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - Sender :: jid(), - Subscriber :: jid(), - SubId :: subId()) - -> {result, default} - % - | {error, _} - | {error, _, binary()} -). -unsubscribe_node(NodeId, Sender, Subscriber, SubId) -> - SubKey = jlib:jid_tolower(Subscriber), - GenKey = jlib:jid_remove_resource(SubKey), - Authorized = - jlib:jid_tolower(jlib:jid_remove_resource(Sender)) == - GenKey, - {Affiliation, Subscriptions} = - select_affiliation_subscriptions(NodeId, SubKey), - SubIdExists = case SubId of - <<>> -> false; - List when is_binary(List) -> true; - _ -> false - end, - if - %% Requesting entity is prohibited from unsubscribing entity - not Authorized -> {error, ?ERR_FORBIDDEN}; - %% Entity did not specify SubId - %%SubId == "", ?? -> - %% {error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")}; - %% Invalid subscription identifier - %%InvalidSubId -> - %% {error, ?ERR_EXTENDED(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; - %% Requesting entity is not a subscriber - Subscriptions == [] -> - {error, - ?ERR_EXTENDED((?ERR_UNEXPECTED_REQUEST_CANCEL), - <<"not-subscribed">>)}; - %% Subid supplied, so use that. - SubIdExists -> - Sub = first_in_list(fun (S) -> - case S of - {_Sub, SubId} -> true; - _ -> false - end - end, - Subscriptions), - case Sub of - {value, S} -> - delete_subscription(SubKey, NodeId, S, Affiliation, - Subscriptions), - {result, default}; - false -> - {error, - ?ERR_EXTENDED((?ERR_UNEXPECTED_REQUEST_CANCEL), - <<"not-subscribed">>)} - end; - %% Asking to remove all subscriptions to the given node - SubId == all -> - [delete_subscription(SubKey, NodeId, S, Affiliation, - Subscriptions) - || S <- Subscriptions], - {result, default}; - %% No subid supplied, but there's only one matching - %% subscription, so use that. - length(Subscriptions) == 1 -> - delete_subscription(SubKey, NodeId, hd(Subscriptions), - Affiliation, Subscriptions), - {result, default}; - %% No subid and more than one possible subscription match. - true -> - {error, - ?ERR_EXTENDED((?ERR_BAD_REQUEST), <<"subid-required">>)} - end. - -%-spec(delete_subscriptions/5 :: -%( -% SubKey :: ljid(), -% NodeIdx :: mod_pubsub:nodeIdx(), -% _ :: {mod_pubsub:subscription(), mod_pubsub:subId()}, -% SubState :: mod_pubsub:pubsubState(), -% Subscriptions :: [{mod_pubsub:subscription(), mod_pubsub:subId()}]) -% -> ok -%). -delete_subscription(SubKey, NodeIdx, - {Subscription, SubId}, Affiliation, Subscriptions) -> - NewSubs = Subscriptions -- [{Subscription, SubId}], - pubsub_subscription_odbc:unsubscribe_node(SubKey, NodeIdx, SubId), - case {Affiliation, NewSubs} of - {none, []} -> del_state(NodeIdx, SubKey); - _ -> update_subscription(NodeIdx, SubKey, NewSubs) - end. - -%% @spec (NodeId, Publisher, PublishModel, MaxItems, ItemId, Payload) -> -%% {true, PubsubItem} | {result, Reply} -%% NodeId = mod_pubsub:pubsubNodeId() -%% Publisher = mod_pubsub:jid() -%% PublishModel = atom() -%% MaxItems = integer() -%% ItemId = string() -%% Payload = term() -%% @doc

    Publishes the item passed as parameter.

    -%%

    The mechanism works as follow: -%%

      -%%
    • The main PubSub module prepares the item to publish and passes the -%% result of the preparation as a {@link mod_pubsub:pubsubItem()} record.
    • -%%
    • This function gets the prepared record and several other parameters and can decide to:
        -%%
      • reject the publication;
      • -%%
      • allow the publication as is, letting the main module perform the database persistance;
      • -%%
      • allow the publication, modifying the record. The main module will store the modified record;
      • -%%
      • allow it, but perform the needed persistance operations.
      -%%

    -%%

    The selected behaviour depends on the return parameter: -%%

      -%%
    • {error, Reason}: an iq error result will be return. No -%% publication is actually performed.
    • -%%
    • true: Publication operation is allowed, based on the -%% unmodified record passed in parameter Item. If the Item -%% parameter contains an error, no subscription will actually be -%% performed.
    • -%%
    • {true, Item}: Publication operation is allowed, but the -%% {@link mod_pubsub:pubsubItem()} record returned replaces the value passed -%% in parameter Item. The persistance will be performed by the main -%% module.
    • -%%
    • {true, done}: Publication operation is allowed, but the -%% {@link mod_pubsub:pubsubItem()} will be considered as already stored and -%% no further persistance operation will be performed. This case is used, -%% when the plugin module is doing the persistance by itself or when it want -%% to completly disable persistance.
    -%%

    -%%

    In the default plugin module, the record is unchanged.

    - --spec(publish_item/6 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - Publisher :: jid(), - PublishModel :: mod_pubsub:publishModel(), - Max_Items :: non_neg_integer(), - ItemId :: <<>> | mod_pubsub:itemId(), - Payload :: mod_pubsub:payload()) - -> {result, {default, broadcast, [mod_pubsub:itemId()]}} - %%% - | {error, _} -). -publish_item(NodeIdx, Publisher, PublishModel, MaxItems, ItemId, Payload) -> - SubKey = jlib:jid_tolower(Publisher), - GenKey = jlib:jid_remove_resource(SubKey), - {Affiliation, Subscriptions} = - select_affiliation_subscriptions(NodeIdx, GenKey, SubKey), - Subscribed = case PublishModel of - subscribers -> is_subscribed(Subscriptions); - _ -> undefined - end, - if not - ((PublishModel == open) or - (PublishModel == publishers) and - ((Affiliation == owner) or (Affiliation == publisher)) - or (Subscribed == true)) -> - {error, ?ERR_FORBIDDEN}; - true -> - if MaxItems > 0 -> - PubId = {now(), SubKey}, - set_item(#pubsub_item{itemid = {ItemId, NodeIdx}, - creation = {now(), GenKey}, - modification = PubId, - payload = Payload}), - Items = [ItemId | itemids(NodeIdx, GenKey) -- [ItemId]], - {result, {_, OI}} = remove_extra_items(NodeIdx, MaxItems, Items), - {result, {default, broadcast, OI}}; - true -> {result, {default, broadcast, []}} - end - end. - -%% @spec (NodeId, MaxItems, ItemIds) -> {NewItemIds,OldItemIds} -%% NodeId = mod_pubsub:pubsubNodeId() -%% MaxItems = integer() | unlimited -%% ItemIds = [ItemId::string()] -%% NewItemIds = [ItemId::string()] -%% @doc

    This function is used to remove extra items, most notably when the -%% maximum number of items has been reached.

    -%%

    This function is used internally by the core PubSub module, as no -%% permission check is performed.

    -%%

    In the default plugin module, the oldest items are removed, but other -%% rules can be used.

    -%%

    If another PubSub plugin wants to delegate the item removal (and if the -%% plugin is using the default pubsub storage), it can implements this function like this: -%% ```remove_extra_items(NodeId, MaxItems, ItemIds) -> -%% node_default:remove_extra_items(NodeId, MaxItems, ItemIds).'''

    -remove_extra_items(_NodeId, unlimited, ItemIds) -> - {result, {ItemIds, []}}; -remove_extra_items(NodeId, MaxItems, ItemIds) -> - NewItems = lists:sublist(ItemIds, MaxItems), - OldItems = lists:nthtail(length(NewItems), ItemIds), - del_items(NodeId, OldItems), - {result, {NewItems, OldItems}}. - -%% @spec (NodeId, Publisher, PublishModel, ItemId) -> -%% {error, Reason::stanzaError()} | -%% {result, []} -%% NodeId = mod_pubsub:pubsubNodeId() -%% Publisher = mod_pubsub:jid() -%% PublishModel = atom() -%% ItemId = string() -%% @doc

    Triggers item deletion.

    -%%

    Default plugin: The user performing the deletion must be the node owner -%% or a publisher.

    --spec(delete_item/4 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - Publisher :: jid(), - PublishModel :: mod_pubsub:publishModel(), - ItemId :: <<>> | mod_pubsub:itemId()) - -> {result, {default, broadcast}} - %%% - | {error, _} -). -delete_item(NodeIdx, Publisher, PublishModel, ItemId) -> - SubKey = jlib:jid_tolower(Publisher), - GenKey = jlib:jid_remove_resource(SubKey), - {result, Affiliation} = get_affiliation(NodeIdx, GenKey), - Allowed = Affiliation == publisher orelse - Affiliation == owner orelse - PublishModel == open orelse - case get_item(NodeIdx, ItemId) of - {result, #pubsub_item{creation = {_, GenKey}}} -> true; - _ -> false - end, - if not Allowed -> {error, ?ERR_FORBIDDEN}; - true -> - case del_item(NodeIdx, ItemId) of - {updated, 1} -> {result, {default, broadcast}}; - _ -> {error, ?ERR_ITEM_NOT_FOUND} - end - end. - -%% @spec (NodeId, Owner) -> -%% {error, Reason::stanzaError()} | -%% {result, {default, broadcast}} -%% NodeId = mod_pubsub:pubsubNodeId() -%% Owner = mod_pubsub:jid() --spec(purge_node/2 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - Owner :: jid()) - -> {result, {default, broadcast}} - | {error, _} -). -purge_node(NodeIdx, Owner) -> - SubKey = jlib:jid_tolower(Owner), - GenKey = jlib:jid_remove_resource(SubKey), - GenState = get_state(NodeIdx, GenKey), - case GenState of - #pubsub_state{affiliation = owner} -> - {result, States} = get_states(NodeIdx), - lists:foreach(fun (#pubsub_state{items = []}) -> ok; - (#pubsub_state{items = Items}) -> - del_items(NodeIdx, Items) - end, - States), - {result, {default, broadcast}}; - _ -> {error, ?ERR_FORBIDDEN} - end. - -%% @spec (Host, JID) -> [{Node,Affiliation}] -%% Host = host() -%% JID = mod_pubsub:jid() -%% @doc

    Return the current affiliations for the given user

    -%%

    The default module reads affiliations in the main Mnesia -%% pubsub_state table. If a plugin stores its data in the same -%% table, it should return an empty list, as the affiliation will be read by -%% the default PubSub module. Otherwise, it should return its own affiliation, -%% that will be added to the affiliation stored in the main -%% pubsub_state table.

    --spec(get_entity_affiliations/2 :: -( - Host :: mod_pubsub:hostPubsub(), - Owner :: jid()) - -> {result, [{mod_pubsub:pubsubNode(), mod_pubsub:affiliation()}]} -). -get_entity_affiliations(Host, Owner) -> - SubKey = jlib:jid_tolower(Owner), - GenKey = jlib:jid_remove_resource(SubKey), - H = (?PUBSUB):escape(Host), - J = encode_jid(GenKey), - Reply = case catch - ejabberd_odbc:sql_query_t([<<"select node, type, i.nodeid, affiliation " - "from pubsub_state i, pubsub_node n where " - "i.nodeid = n.nodeid and jid='">>, - J, <<"' and host='">>, H, - <<"';">>]) - of - {selected, - [<<"node">>, <<"type">>, <<"nodeid">>, - <<"affiliation">>], - RItems} -> - lists:map(fun ([N, T, I, A]) -> - Node = nodetree_tree_odbc:raw_to_node(Host, - [N, - <<"">>, - T, - I]), - {Node, decode_affiliation(A)} - end, - RItems); - _ -> [] - end, - {result, Reply}. - --spec(get_node_affiliations/1 :: -( - NodeIdx::mod_pubsub:nodeIdx()) - -> {result, [{ljid(), mod_pubsub:affiliation()}]} -). -get_node_affiliations(NodeIdx) -> - Reply = case catch - ejabberd_odbc:sql_query_t([<<"select jid, affiliation from pubsub_state " - "where nodeid='">>, - NodeIdx, <<"';">>]) - of - {selected, [<<"jid">>, <<"affiliation">>], RItems} -> - lists:map(fun ([J, A]) -> - {decode_jid(J), decode_affiliation(A)} - end, - RItems); - _ -> [] - end, - {result, Reply}. - --spec(get_affiliation/2 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - Owner :: ljid()) - -> {result, mod_pubsub:affiliation()} -). - -get_affiliation(NodeIdx, Owner) -> - SubKey = jlib:jid_tolower(Owner), - GenKey = jlib:jid_remove_resource(SubKey), - J = encode_jid(GenKey), - Reply = case catch - ejabberd_odbc:sql_query_t([<<"select affiliation from pubsub_state " - "where nodeid='">>, - NodeIdx, <<"' and jid='">>, J, - <<"';">>]) - of - {selected, [<<"affiliation">>], [[A]]} -> - decode_affiliation(A); - _ -> none - end, - {result, Reply}. - --spec(set_affiliation/3 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - Owner :: ljid(), - Affiliation :: mod_pubsub:affiliation()) - -> ok -). -set_affiliation(NodeIdx, Owner, Affiliation) -> - SubKey = jlib:jid_tolower(Owner), - GenKey = jlib:jid_remove_resource(SubKey), - {_, Subscriptions} = select_affiliation_subscriptions(NodeIdx, GenKey), - case {Affiliation, Subscriptions} of - {none, none} -> del_state(NodeIdx, GenKey); - _ -> update_affiliation(NodeIdx, GenKey, Affiliation) - end. - -%% @spec (Host, Owner) -> [{Node,Subscription}] -%% Host = host() -%% Owner = mod_pubsub:jid() -%% @doc

    Return the current subscriptions for the given user

    -%%

    The default module reads subscriptions in the main Mnesia -%% pubsub_state table. If a plugin stores its data in the same -%% table, it should return an empty list, as the affiliation will be read by -%% the default PubSub module. Otherwise, it should return its own affiliation, -%% that will be added to the affiliation stored in the main -%% pubsub_state table.

    - --spec(get_entity_subscriptions/2 :: -( - Host :: mod_pubsub:host(), - Owner :: jid()) - -> {result, - [{mod_pubsub:pubsubNode(), - mod_pubsub:subscription(), - mod_pubsub:subId(), - ljid()}] - } -). -get_entity_subscriptions(Host, Owner) -> - SubKey = jlib:jid_tolower(Owner), - GenKey = jlib:jid_remove_resource(SubKey), - H = (?PUBSUB):escape(Host), - SJ = encode_jid(SubKey), - GJ = encode_jid(GenKey), - Query = case SubKey of - GenKey -> - [<<"select node, type, i.nodeid, jid, subscriptio" - "ns from pubsub_state i, pubsub_node " - "n where i.nodeid = n.nodeid and jid " - "like '">>, - GJ, <<"%' and host='">>, H, <<"';">>]; - _ -> - [<<"select node, type, i.nodeid, jid, subscriptio" - "ns from pubsub_state i, pubsub_node " - "n where i.nodeid = n.nodeid and jid " - "in ('">>, - SJ, <<"', '">>, GJ, <<"') and host='">>, H, <<"';">>] - end, - Reply = case catch ejabberd_odbc:sql_query_t(Query) of - {selected, - [<<"node">>, <<"type">>, <<"nodeid">>, <<"jid">>, - <<"subscriptions">>], - RItems} -> - lists:foldl(fun ([N, T, I, J, S], Acc) -> - Node = - nodetree_tree_odbc:raw_to_node(Host, - [N, - <<"">>, - T, - I]), - Jid = decode_jid(J), - case decode_subscriptions(S) of - [] -> [{Node, none, Jid} | Acc]; - Subs -> - lists:foldl(fun ({Sub, SubId}, Acc2) -> - [{Node, Sub, SubId, Jid} | Acc2] - end, - Acc, Subs) - end - end, - [], RItems); - _ -> [] - end, - {result, Reply}. - -%% do the same as get_entity_subscriptions but filter result only to -%% nodes having send_last_published_item=on_sub_and_presence -%% as this call avoid seeking node, it must return node and type as well --spec(get_entity_subscriptions_for_send_last/2 :: -( - Host :: mod_pubsub:hostPubsub(), - Owner :: jid()) - -> {result, - [{mod_pubsub:pubsubNode(), - mod_pubsub:subscription(), - mod_pubsub:subId(), - ljid()}] - } -). - -get_entity_subscriptions_for_send_last(Host, Owner) -> - SubKey = jlib:jid_tolower(Owner), - GenKey = jlib:jid_remove_resource(SubKey), - H = (?PUBSUB):escape(Host), - SJ = encode_jid(SubKey), - GJ = encode_jid(GenKey), - Query = case SubKey of - GenKey -> - [<<"select node, type, i.nodeid, jid, subscriptio" - "ns from pubsub_state i, pubsub_node " - "n, pubsub_node_option o where i.nodeid " - "= n.nodeid and n.nodeid = o.nodeid and " - "name='send_last_published_item' and " - "val='on_sub_and_presence' and jid like " - "'">>, - GJ, <<"%' and host='">>, H, <<"';">>]; - _ -> - [<<"select node, type, i.nodeid, jid, subscriptio" - "ns from pubsub_state i, pubsub_node " - "n, pubsub_node_option o where i.nodeid " - "= n.nodeid and n.nodeid = o.nodeid and " - "name='send_last_published_item' and " - "val='on_sub_and_presence' and jid in " - "('">>, - SJ, <<"', '">>, GJ, <<"') and host='">>, H, <<"';">>] - end, - Reply = case catch ejabberd_odbc:sql_query_t(Query) of - {selected, - [<<"node">>, <<"type">>, <<"nodeid">>, <<"jid">>, - <<"subscriptions">>], - RItems} -> - lists:foldl(fun ([N, T, I, J, S], Acc) -> - Node = - nodetree_tree_odbc:raw_to_node(Host, - [N, - <<"">>, - T, - I]), - Jid = decode_jid(J), - case decode_subscriptions(S) of - [] -> [{Node, none, Jid} | Acc]; - Subs -> - lists:foldl(fun ({Sub, SubId}, Acc2) -> - [{Node, Sub, SubId, Jid}| Acc2] - end, - Acc, Subs) - end - end, - [], RItems); - _ -> [] - end, - {result, Reply}. - --spec(get_node_subscriptions/1 :: -( - NodeIdx::mod_pubsub:nodeIdx()) - -> {result, [{ljid(), mod_pubsub:subscription(), mod_pubsub:subId()}]} -). -get_node_subscriptions(NodeIdx) -> - Reply = case catch - ejabberd_odbc:sql_query_t([<<"select jid, subscriptions from pubsub_state " - "where nodeid='">>, - NodeIdx, <<"';">>]) - of - {selected, [<<"jid">>, <<"subscriptions">>], RItems} -> - lists:foldl(fun ([J, S], Acc) -> - Jid = decode_jid(J), - case decode_subscriptions(S) of - [] -> [{Jid, none} | Acc]; - Subs -> - lists:foldl(fun ({Sub, SubId}, Acc2) -> - [{Jid, Sub, SubId} | Acc2] - end, - Acc, Subs) - end - end, - [], RItems); - _ -> [] - end, - {result, Reply}. - --spec(get_subscriptions/2 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - Owner :: ljid()) - -> {result, [{mod_pubsub:subscription(), mod_pubsub:subId()}]} -). -get_subscriptions(NodeIdx, Owner) -> - SubKey = jlib:jid_tolower(Owner), - J = encode_jid(SubKey), - Reply = case catch - ejabberd_odbc:sql_query_t([<<"select subscriptions from pubsub_state " - "where nodeid='">>, - NodeIdx, <<"' and jid='">>, J, - <<"';">>]) - of - {selected, [<<"subscriptions">>], [[S]]} -> - decode_subscriptions(S); - _ -> [] - end, - {result, Reply}. - --spec(set_subscriptions/4 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - Owner :: jid(), - Subscription :: mod_pubsub:subscription(), - SubId :: mod_pubsub:subId()) - -> _ - %%% - | {error, xmlel()} -). -set_subscriptions(NodeIdx, Owner, Subscription, SubId) -> - SubKey = jlib:jid_tolower(Owner), - SubState = get_state_without_itemids(NodeIdx, SubKey), - case {SubId, SubState#pubsub_state.subscriptions} of - {_, []} -> - case Subscription of - none -> - {error, - ?ERR_EXTENDED((?ERR_BAD_REQUEST), - <<"not-subscribed">>)}; - _ -> - new_subscription(NodeIdx, Owner, Subscription, SubState) - end; - {<<"">>, [{_, SID}]} -> - case Subscription of - none -> unsub_with_subid(NodeIdx, SID, SubState); - _ -> replace_subscription({Subscription, SID}, SubState) - end; - {<<"">>, [_ | _]} -> - {error, - ?ERR_EXTENDED((?ERR_BAD_REQUEST), - <<"subid-required">>)}; - _ -> - case Subscription of - none -> unsub_with_subid(NodeIdx, SubId, SubState); - _ -> - replace_subscription({Subscription, SubId}, SubState) - end - end. - --spec(replace_subscription/2 :: -( - NewSub :: {mod_pubsub:subscription(), mod_pubsub:subId()}, - SubState :: mod_pubsub:pubsubState()) - -> {result, []} -). -replace_subscription(NewSub, SubState) -> - NewSubs = replace_subscription(NewSub, SubState#pubsub_state.subscriptions, []), - set_state(SubState#pubsub_state{subscriptions = NewSubs}). - -replace_subscription(_, [], Acc) -> Acc; -replace_subscription({Sub, SubId}, [{_, SubID} | T], Acc) -> - replace_subscription({Sub, SubId}, T, [{Sub, SubID} | Acc]). - --spec(new_subscription/4 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - Owner :: jid(), - Subscription :: mod_pubsub:subscription(), - SubState :: mod_pubsub:pubsubState()) - -> {mod_pubsub:subscription(), mod_pubsub:subId()} - %%% - | {error, xmlel()} -). - -new_subscription(NodeIdx, Owner, Subscription, SubState) -> - case pubsub_subscription_odbc:subscribe_node(Owner, NodeIdx, []) of - {result, SubId} -> - Subscriptions = SubState#pubsub_state.subscriptions, - set_state(SubState#pubsub_state{subscriptions = - [{Subscription, SubId} | Subscriptions]}), - {Subscription, SubId}; - _ -> {error, ?ERR_INTERNAL_SERVER_ERROR} - end. - --spec(unsub_with_subid/3 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - SubId :: mod_pubsub:subId(), - SubState :: mod_pubsub:pubsubState()) - -> ok -). -unsub_with_subid(NodeIdx, SubId, SubState) -> - pubsub_subscription_odbc:unsubscribe_node(SubState#pubsub_state.stateid, - NodeIdx, SubId), - NewSubs = lists:filter(fun ({_, SID}) -> SubId =/= SID - end, - SubState#pubsub_state.subscriptions), - case {NewSubs, SubState#pubsub_state.affiliation} of - {[], none} -> - del_state(NodeIdx, - element(1, SubState#pubsub_state.stateid)); - _ -> - set_state(SubState#pubsub_state{subscriptions = NewSubs}) - end. - -%% @spec (Host, Owner) -> {result, [Node]} | {error, Reason} -%% Host = host() -%% Owner = jid() -%% Node = pubsubNode() -%% @doc

    Returns a list of Owner's nodes on Host with pending -%% subscriptions.

    --spec(get_pending_nodes/2 :: -( - Host :: mod_pubsub:hostPubsub(), - Owner :: jid()) - -> {result, [mod_pubsub:nodeId()]} -). -get_pending_nodes(Host, Owner) -> - GenKey = jlib:jid_remove_resource(jlib:jid_tolower(Owner)), - States = mnesia:match_object(#pubsub_state{stateid = - {GenKey, '_'}, - affiliation = owner, _ = '_'}), - NodeIDs = [ID - || #pubsub_state{stateid = {_, ID}} <- States], - NodeTree = case catch - ets:lookup(gen_mod:get_module_proc(Host, config), - nodetree) - of - [{nodetree, N}] -> N; - _ -> nodetree_tree_odbc - end, - Reply = mnesia:foldl(fun (#pubsub_state{stateid = - {_, NID}} = - S, - Acc) -> - case lists:member(NID, NodeIDs) of - true -> - case get_nodes_helper(NodeTree, S) of - {value, Node} -> [Node | Acc]; - false -> Acc - end; - false -> Acc - end - end, - [], pubsub_state), - {result, Reply}. - -get_nodes_helper(NodeTree, - #pubsub_state{stateid = {_, N}, - subscriptions = Subs}) -> - HasPending = fun ({pending, _}) -> true; - (pending) -> true; - (_) -> false - end, - case lists:any(HasPending, Subs) of - true -> - case NodeTree:get_node(N) of - #pubsub_node{nodeid = {_, Node}} -> {value, Node}; - _ -> false - end; - false -> false - end. - -%% @spec (NodeId) -> [States] | [] -%% NodeId = mod_pubsub:pubsubNodeId() -%% @doc Returns the list of stored states for a given node. -%%

    For the default PubSub module, states are stored in Mnesia database.

    -%%

    We can consider that the pubsub_state table have been created by the main -%% mod_pubsub module.

    -%%

    PubSub plugins can store the states where they wants (for example in a -%% relational database).

    -%%

    If a PubSub plugin wants to delegate the states storage to the default node, -%% they can implement this function like this: -%% ```get_states(NodeId) -> -%% node_default:get_states(NodeId).'''

    --spec(get_states/1 :: -( - NodeIdx::mod_pubsub:nodeIdx()) - -> {result, [mod_pubsub:pubsubState()]} -). -get_states(NodeIdx) -> - case catch - ejabberd_odbc:sql_query_t([<<"select jid, affiliation, subscriptions " - "from pubsub_state where nodeid='">>, - NodeIdx, <<"';">>]) - of - {selected, - [<<"jid">>, <<"affiliation">>, <<"subscriptions">>], - RItems} -> - {result, - lists:map(fun ([SJID, Affiliation, Subscriptions]) -> - #pubsub_state{stateid = {decode_jid(SJID), NodeIdx}, - items = itemids(NodeIdx, SJID), - affiliation = decode_affiliation(Affiliation), - subscriptions = decode_subscriptions(Subscriptions)} - end, - RItems)}; - _ -> {result, []} - end. - -%% @spec (NodeId, JID) -> [State] | [] -%% NodeId = mod_pubsub:pubsubNodeId() -%% JID = mod_pubsub:jid() -%% State = mod_pubsub:pubsubItems() -%% @doc

    Returns a state (one state list), given its reference.

    - --spec(get_state/2 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - JID :: ljid()) - -> mod_pubsub:pubsubState() -). -get_state(NodeIdx, JID) -> - State = get_state_without_itemids(NodeIdx, JID), - {SJID, _} = State#pubsub_state.stateid, - State#pubsub_state{items = itemids(NodeIdx, SJID)}. - --spec(get_state_without_itemids/2 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - JID :: jid()) - -> mod_pubsub:pubsubState() -). -get_state_without_itemids(NodeIdx, JID) -> - J = encode_jid(JID), - case catch - ejabberd_odbc:sql_query_t([<<"select jid, affiliation, subscriptions " - "from pubsub_state where jid='">>, - J, <<"' and nodeid='">>, NodeIdx, - <<"';">>]) - of - {selected, - [<<"jid">>, <<"affiliation">>, <<"subscriptions">>], - [[SJID, Affiliation, Subscriptions]]} -> - #pubsub_state{stateid = {decode_jid(SJID), NodeIdx}, - affiliation = decode_affiliation(Affiliation), - subscriptions = decode_subscriptions(Subscriptions)}; - _ -> #pubsub_state{stateid = {JID, NodeIdx}} - end. - -%% @spec (State) -> ok | {error, Reason::stanzaError()} -%% State = mod_pubsub:pubsubStates() -%% @doc

    Write a state into database.

    - --spec(set_state/1 :: -( - State :: mod_pubsub:pubsubState()) - -> {result, []} -). -set_state(State) -> - {_, NodeIdx} = State#pubsub_state.stateid, - set_state(NodeIdx, State). - --spec(set_state/2 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - State :: mod_pubsub:pubsubState()) - -> {result, []} -). -set_state(NodeIdx, State) -> - {JID, _} = State#pubsub_state.stateid, - J = encode_jid(JID), - S = encode_subscriptions(State#pubsub_state.subscriptions), - A = encode_affiliation(State#pubsub_state.affiliation), - case catch - ejabberd_odbc:sql_query_t([<<"update pubsub_state set subscriptions='">>, - S, <<"', affiliation='">>, A, - <<"' where nodeid='">>, NodeIdx, - <<"' and jid='">>, J, <<"';">>]) - of - {updated, 1} -> ok; - _ -> - catch - ejabberd_odbc:sql_query_t([<<"insert into pubsub_state(nodeid, jid, " - "affiliation, subscriptions) values('">>, - NodeIdx, <<"', '">>, J, <<"', '">>, A, - <<"', '">>, S, <<"');">>]) - end, - {result, []}. - -%% @spec (NodeId, JID) -> ok | {error, Reason::stanzaError()} -%% NodeId = mod_pubsub:pubsubNodeId() -%% JID = mod_pubsub:jid() -%% @doc

    Delete a state from database.

    -del_state(NodeId, JID) -> - J = encode_jid(JID), - catch - ejabberd_odbc:sql_query_t([<<"delete from pubsub_state where jid='">>, - J, <<"' and nodeid='">>, NodeId, <<"';">>]), - ok. - -%% @spec (NodeId, From) -> {[Items],RsmOut} | [] -%% NodeId = mod_pubsub:pubsubNodeId() -%% Items = mod_pubsub:pubsubItems() -%% @doc Returns the list of stored items for a given node. -%%

    For the default PubSub module, items are stored in Mnesia database.

    -%%

    We can consider that the pubsub_item table have been created by the main -%% mod_pubsub module.

    -%%

    PubSub plugins can store the items where they wants (for example in a -%% relational database), or they can even decide not to persist any items.

    -%%

    If a PubSub plugin wants to delegate the item storage to the default node, -%% they can implement this function like this: -%% ```get_items(NodeId, From) -> -%% node_default:get_items(NodeId, From).'''

    -get_items(NodeId, _From) -> - case catch - ejabberd_odbc:sql_query_t([<<"select itemid, publisher, creation, " - "modification, payload from pubsub_item " - "where nodeid='">>, - NodeId, - <<"' order by modification desc;">>]) - of - {selected, - [<<"itemid">>, <<"publisher">>, <<"creation">>, - <<"modification">>, <<"payload">>], - RItems} -> - {result, - lists:map(fun (RItem) -> raw_to_item(NodeId, RItem) end, - RItems)}; - _ -> {result, []} - end. - -get_items(NodeId, From, none) -> - MaxItems = case catch - ejabberd_odbc:sql_query_t([<<"select val from pubsub_node_option where " - "nodeid='">>, - NodeId, - <<"' and name='max_items';">>]) - of - {selected, [<<"val">>], [[Value]]} -> - Tokens = element(2, - erl_scan:string(binary_to_list(<>))), - element(2, erl_parse:parse_term(Tokens)); - _ -> ?MAXITEMS - end, - get_items(NodeId, From, #rsm_in{max = MaxItems}); -get_items(NodeId, _From, - #rsm_in{max = M, direction = Direction, id = I, - index = IncIndex}) -> - Max = (?PUBSUB):escape(jlib:i2l(M)), - {Way, Order} = case Direction of - aft -> {<<"<">>, <<"desc">>}; - before when I == <<>> -> {<<"is not">>, <<"asc">>}; - before -> {<<">">>, <<"asc">>}; - _ when IncIndex =/= undefined -> - {<<"<">>, <<"desc">>}; % using index - _ -> {<<"is not">>, <<"desc">>}% Can be better - end, - [AttrName, Id] = case I of - undefined when IncIndex =/= undefined -> - case catch - ejabberd_odbc:sql_query_t([<<"select modification from pubsub_item " - "pi where exists ( select count(*) as " - "count1 from pubsub_item where nodeid='">>, - NodeId, - <<"' and modification > pi.modification " - "having count1 = ">>, - (?PUBSUB):escape(jlib:i2l(IncIndex)), - <<" );">>]) - of - {selected, [_], [[O]]} -> - [<<"modification">>, <<"'", O/binary, "'">>]; - _ -> [<<"modification">>, <<"null">>] - end; - undefined -> [<<"modification">>, <<"null">>]; - <<>> -> [<<"modification">>, <<"null">>]; - I -> - [A, B] = str:tokens((?PUBSUB):escape(jlib:i2l(I)), - <<"@">>), - [A, <<"'", B/binary, "'">>] - end, - Count = case catch - ejabberd_odbc:sql_query_t([<<"select count(*) from pubsub_item where " - "nodeid='">>, - NodeId, <<"';">>]) - of - {selected, [_], [[C]]} -> C; - _ -> <<"0">> - end, - case catch - ejabberd_odbc:sql_query_t([<<"select itemid, publisher, creation, " - "modification, payload from pubsub_item " - "where nodeid='">>, - NodeId, <<"' and ">>, AttrName, <<" ">>, - Way, <<" ">>, Id, <<" order by ">>, - AttrName, <<" ">>, Order, <<" limit ">>, - jlib:i2l(Max), <<" ;">>]) - of - {selected, - [<<"itemid">>, <<"publisher">>, <<"creation">>, - <<"modification">>, <<"payload">>], - RItems} -> - case RItems of - [[_, _, _, F, _]|_] -> - Index = case catch - ejabberd_odbc:sql_query_t([<<"select count(*) from pubsub_item where " - "nodeid='">>, - NodeId, <<"' and ">>, - AttrName, <<" > '">>, - F, <<"';">>]) - of - %{selected, [_], [{C}, {In}]} -> [string:strip(C, both, $"), string:strip(In, both, $")]; - {selected, [_], [[In]]} -> In; - _ -> <<"0">> - end, - [_, _, _, L, _] = lists:last(RItems), - RsmOut = #rsm_out{count = Count, index = Index, - first = <<"modification@", F/binary>>, - last = <<"modification@", (jlib:i2l(L))/binary>>}, - {result, {[raw_to_item(NodeId, RItem) || RItem <- RItems], RsmOut}}; - [] -> {result, {[], #rsm_out{count = Count}}}; - 0 -> {result, {[], #rsm_out{count = Count}}} - end; - _ -> {result, {[], none}} - end. - -get_items(NodeId, JID, AccessModel, - PresenceSubscription, RosterGroup, SubId) -> - get_items(NodeId, JID, AccessModel, - PresenceSubscription, RosterGroup, SubId, none). - -get_items(NodeId, JID, AccessModel, - PresenceSubscription, RosterGroup, _SubId, RSM) -> - SubKey = jlib:jid_tolower(JID), - GenKey = jlib:jid_remove_resource(SubKey), - {Affiliation, Subscriptions} = - select_affiliation_subscriptions(NodeId, GenKey, - SubKey), - Whitelisted = can_fetch_item(Affiliation, - Subscriptions), - if %%SubId == "", ?? -> - %% Entity has multiple subscriptions to the node but does not specify a subscription ID - %{error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")}; - %%InvalidSubId -> - %% Entity is subscribed but specifies an invalid subscription ID - %{error, ?ERR_EXTENDED(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; - Affiliation == outcast -> {error, ?ERR_FORBIDDEN}; - (AccessModel == presence) and - not PresenceSubscription -> - {error, - ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), - <<"presence-subscription-required">>)}; - (AccessModel == roster) and not RosterGroup -> - {error, - ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), - <<"not-in-roster-group">>)}; - (AccessModel == whitelist) and not Whitelisted -> - {error, - ?ERR_EXTENDED((?ERR_NOT_ALLOWED), <<"closed-node">>)}; - (AccessModel == authorize) and not Whitelisted -> - {error, ?ERR_FORBIDDEN}; - %%MustPay -> - %% % Payment is required for a subscription - %% {error, ?ERR_PAYMENT_REQUIRED}; - true -> get_items(NodeId, JID, RSM) - end. - -get_last_items(NodeId, _From, Count) -> - case catch - ejabberd_odbc:sql_query_t([<<"select itemid, publisher, creation, " - "modification, payload from pubsub_item " - "where nodeid='">>, - NodeId, - <<"' order by modification desc limit ">>, - jlib:i2l(Count), <<";">>]) - of - {selected, - [<<"itemid">>, <<"publisher">>, <<"creation">>, - <<"modification">>, <<"payload">>], - RItems} -> - {result, - lists:map(fun (RItem) -> raw_to_item(NodeId, RItem) end, - RItems)}; - _ -> {result, []} - end. - -%% @spec (NodeId, ItemId) -> [Item] | [] -%% NodeId = mod_pubsub:pubsubNodeId() -%% ItemId = string() -%% Item = mod_pubsub:pubsubItems() -%% @doc

    Returns an item (one item list), given its reference.

    -get_item(NodeId, ItemId) -> - I = (?PUBSUB):escape(ItemId), - case catch - ejabberd_odbc:sql_query_t([<<"select itemid, publisher, creation, " - "modification, payload from pubsub_item " - "where nodeid='">>, - NodeId, <<"' and itemid='">>, I, - <<"';">>]) - of - {selected, - [<<"itemid">>, <<"publisher">>, <<"creation">>, - <<"modification">>, <<"payload">>], - [RItem]} -> - {result, raw_to_item(NodeId, RItem)}; - _ -> {error, ?ERR_ITEM_NOT_FOUND} - end. - -get_item(NodeId, ItemId, JID, AccessModel, - PresenceSubscription, RosterGroup, _SubId) -> - SubKey = jlib:jid_tolower(JID), - GenKey = jlib:jid_remove_resource(SubKey), - {Affiliation, Subscriptions} = - select_affiliation_subscriptions(NodeId, GenKey, - SubKey), - Whitelisted = can_fetch_item(Affiliation, - Subscriptions), - if %%SubId == "", ?? -> - %% Entity has multiple subscriptions to the node but does not specify a subscription ID - %{error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")}; - %%InvalidSubId -> - %% Entity is subscribed but specifies an invalid subscription ID - %{error, ?ERR_EXTENDED(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; - Affiliation == outcast -> {error, ?ERR_FORBIDDEN}; - (AccessModel == presence) and - not PresenceSubscription -> - {error, - ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), - <<"presence-subscription-required">>)}; - (AccessModel == roster) and not RosterGroup -> - {error, - ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), - <<"not-in-roster-group">>)}; - (AccessModel == whitelist) and not Whitelisted -> - {error, - ?ERR_EXTENDED((?ERR_NOT_ALLOWED), <<"closed-node">>)}; - (AccessModel == authorize) and not Whitelisted -> - {error, ?ERR_FORBIDDEN}; - %%MustPay -> - %% % Payment is required for a subscription - %% {error, ?ERR_PAYMENT_REQUIRED}; - true -> get_item(NodeId, ItemId) - end. - -%% @spec (Item) -> ok | {error, Reason::stanzaError()} -%% Item = mod_pubsub:pubsubItems() -%% @doc

    Write an item into database.

    -set_item(Item) -> - {ItemId, NodeId} = Item#pubsub_item.itemid, - I = (?PUBSUB):escape(ItemId), - {C, _} = Item#pubsub_item.creation, - {M, JID} = Item#pubsub_item.modification, - P = encode_jid(JID), - Payload = Item#pubsub_item.payload, - XML = (?PUBSUB):escape(str:join([xml:element_to_binary(X) || X<-Payload], <<>>)), - S = fun ({T1, T2, T3}) -> - str:join([jlib:i2l(T1, 6), jlib:i2l(T2, 6), jlib:i2l(T3, 6)], <<":">>) - end, - case catch - ejabberd_odbc:sql_query_t([<<"update pubsub_item set publisher='">>, - P, <<"', modification='">>, S(M), - <<"', payload='">>, XML, - <<"' where nodeid='">>, NodeId, - <<"' and itemid='">>, I, <<"';">>]) - of - {updated, 1} -> ok; - _ -> - catch - ejabberd_odbc:sql_query_t([<<"insert into pubsub_item (nodeid, itemid, " - "publisher, creation, modification, payload) " - "values('">>, - NodeId, <<"', '">>, I, <<"', '">>, P, - <<"', '">>, S(C), <<"', '">>, S(M), - <<"', '">>, XML, <<"');">>]) - end, - {result, []}. - -%% @spec (NodeId, ItemId) -> ok | {error, Reason::stanzaError()} -%% NodeId = mod_pubsub:pubsubNodeId() -%% ItemId = string() -%% @doc

    Delete an item from database.

    -del_item(NodeId, ItemId) -> - I = (?PUBSUB):escape(ItemId), - catch - ejabberd_odbc:sql_query_t([<<"delete from pubsub_item where itemid='">>, - I, <<"' and nodeid='">>, NodeId, <<"';">>]). - -del_items(_, []) -> ok; -del_items(NodeId, [ItemId]) -> del_item(NodeId, ItemId); -del_items(NodeId, ItemIds) -> - I = str:join([[<<"'">>, (?PUBSUB):escape(X), <<"'">>] - || X <- ItemIds], - <<",">>), - catch - ejabberd_odbc:sql_query_t([<<"delete from pubsub_item where itemid " - "in (">>, - I, <<") and nodeid='">>, NodeId, <<"';">>]). - -get_item_name(_Host, _Node, Id) -> Id. - -node_to_path(Node) -> str:tokens((Node), <<"/">>). - -path_to_node([]) -> <<>>; -path_to_node(Path) -> - iolist_to_binary(str:join([<<"">> | Path], <<"/">>)). - -%% @spec (Affiliation, Subscription) -> true | false -%% Affiliation = owner | member | publisher | outcast | none -%% Subscription = subscribed | none -%% @doc Determines if the combination of Affiliation and Subscribed -%% are allowed to get items from a node. -can_fetch_item(owner, _) -> true; -can_fetch_item(member, _) -> true; -can_fetch_item(publisher, _) -> true; -can_fetch_item(outcast, _) -> false; -can_fetch_item(none, Subscriptions) -> is_subscribed(Subscriptions). - -is_subscribed(Subscriptions) -> - lists:any(fun ({subscribed, _SubId}) -> true; - (_) -> false - end, - Subscriptions). - -%% Returns the first item where Pred() is true in List -first_in_list(_Pred, []) -> false; -first_in_list(Pred, [H | T]) -> - case Pred(H) of - true -> {value, H}; - _ -> first_in_list(Pred, T) - end. - -itemids(NodeId, {U, S, R}) -> - itemids(NodeId, encode_jid({U, S, R})); -itemids(NodeId, SJID) -> - case catch - ejabberd_odbc:sql_query_t([<<"select itemid from pubsub_item where " - "nodeid='">>, - NodeId, <<"' and publisher like '">>, - SJID, - <<"%' order by modification desc;">>]) - of - {selected, [<<"itemid">>], RItems} -> - lists:map(fun ([ItemId]) -> ItemId end, RItems); - _ -> [] - end. - -select_affiliation_subscriptions(NodeId, JID) -> - J = encode_jid(JID), - case catch - ejabberd_odbc:sql_query_t([<<"select affiliation,subscriptions from " - "pubsub_state where nodeid='">>, - NodeId, <<"' and jid='">>, J, <<"';">>]) - of - {selected, [<<"affiliation">>, <<"subscriptions">>], - [[A, S]]} -> - {decode_affiliation(A), decode_subscriptions(S)}; - _ -> {none, []} - end. - -select_affiliation_subscriptions(NodeId, JID, JID) -> - select_affiliation_subscriptions(NodeId, JID); -select_affiliation_subscriptions(NodeId, GenKey, - SubKey) -> - {result, Affiliation} = get_affiliation(NodeId, GenKey), - {result, Subscriptions} = get_subscriptions(NodeId, - SubKey), - {Affiliation, Subscriptions}. - -update_affiliation(NodeId, JID, Affiliation) -> - J = encode_jid(JID), - A = encode_affiliation(Affiliation), - case catch - ejabberd_odbc:sql_query_t([<<"update pubsub_state set affiliation='">>, - A, <<"' where nodeid='">>, NodeId, - <<"' and jid='">>, J, <<"';">>]) - of - {updated, 1} -> ok; - _ -> - catch - ejabberd_odbc:sql_query_t([<<"insert into pubsub_state(nodeid, jid, " - "affiliation, subscriptions) values('">>, - NodeId, <<"', '">>, J, <<"', '">>, A, - <<"', '');">>]) - end. - -update_subscription(NodeId, JID, Subscription) -> - J = encode_jid(JID), - S = encode_subscriptions(Subscription), - case catch - ejabberd_odbc:sql_query_t([<<"update pubsub_state set subscriptions='">>, - S, <<"' where nodeid='">>, NodeId, - <<"' and jid='">>, J, <<"';">>]) - of - {updated, 1} -> ok; - _ -> - catch - ejabberd_odbc:sql_query_t([<<"insert into pubsub_state(nodeid, jid, " - "affiliation, subscriptions) values('">>, - NodeId, <<"', '">>, J, <<"', 'n', '">>, - S, <<"');">>]) - end. - -decode_jid(JID) -> - jlib:jid_tolower(jlib:string_to_jid(JID)). - -decode_affiliation(<<"o">>) -> owner; -decode_affiliation(<<"p">>) -> publisher; -decode_affiliation(<<"m">>) -> member; -decode_affiliation(<<"c">>) -> outcast; -decode_affiliation(_) -> none. - -decode_subscription(<<"s">>) -> subscribed; -decode_subscription(<<"p">>) -> pending; -decode_subscription(<<"u">>) -> unconfigured; -decode_subscription(_) -> none. - -decode_subscriptions(Subscriptions) -> - lists:foldl(fun (Subscription, Acc) -> - case str:tokens(Subscription, <<":">>) of - [S, SubId] -> [{decode_subscription(S), SubId} | Acc]; - _ -> Acc - end - end, - [], str:tokens(Subscriptions, <<",">>)). - -encode_jid(JID) -> - (?PUBSUB):escape(JID). - -encode_affiliation(owner) -> <<"o">>; -encode_affiliation(publisher) -> <<"p">>; -encode_affiliation(member) -> <<"m">>; -encode_affiliation(outcast) -> <<"c">>; -encode_affiliation(_) -> <<"n">>. - -encode_subscription(subscribed) -> <<"s">>; -encode_subscription(pending) -> <<"p">>; -encode_subscription(unconfigured) -> <<"u">>; -encode_subscription(_) -> <<"n">>. - -encode_subscriptions(Subscriptions) -> - str:join(lists:map(fun ({S, SubId}) -> - <<(encode_subscription(S))/binary, ":", - SubId/binary>> - end, - Subscriptions), - <<",">>). - -%%% record getter/setter - -state_to_raw(NodeId, State) -> - {JID, _} = State#pubsub_state.stateid, - J = encode_jid(JID), - A = encode_affiliation(State#pubsub_state.affiliation), - S = - encode_subscriptions(State#pubsub_state.subscriptions), - [<<"'">>, NodeId, <<"', '">>, J, <<"', '">>, A, - <<"', '">>, S, <<"'">>]. - -raw_to_item(NodeId, - [ItemId, SJID, Creation, Modification, XML]) -> - JID = decode_jid(SJID), - ToTime = fun (Str) -> - [T1, T2, T3] = str:tokens(Str, <<":">>), - {jlib:l2i(T1), jlib:l2i(T2), jlib:l2i(T3)} - end, - Payload = case xml_stream:parse_element(XML) of - {error, _Reason} -> []; - El -> [El] - end, - #pubsub_item{itemid = {ItemId, NodeId}, - creation = {ToTime(Creation), JID}, - modification = {ToTime(Modification), JID}, - payload = Payload}. diff --git a/src/node_mb.erl b/src/node_mb.erl deleted file mode 100644 index efcacf6e8..000000000 --- a/src/node_mb.erl +++ /dev/null @@ -1,191 +0,0 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. -%%% -%%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. -%%% -%%% -%%% The Initial Developer of the Original Code is ProcessOne. -%%% Portions created by ProcessOne are Copyright 2006-2015, ProcessOne -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2015, ProcessOne. -%%% -%%% -%%% @copyright 2006-2015 ProcessOne -%%% @author Eric Cestari -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== - -%%% @doc The module {@module} is the pep microblog PubSub plugin. -%%%

    To be used, mod_pubsub must be configured : -%%% {mod_pubsub, [ % requires mod_caps -%%% {access_createnode, pubsub_createnode}, -%%% {plugins, ["default", "pep","mb"]}, -%%% {pep_mapping, [{"urn:xmpp:microblog", "mb"}]} -%%% ]}, -%%%

    -%%%

    PubSub plugin nodes are using the {@link gen_pubsub_node} behaviour.

    - --module(node_mb). - --author('eric@ohmforce.com'). - --include("ejabberd.hrl"). --include("logger.hrl"). - --include("pubsub.hrl"). - --include("jlib.hrl"). - --behaviour(gen_pubsub_node). - -%% API definition --export([init/3, terminate/2, options/0, features/0, - create_node_permission/6, create_node/2, delete_node/1, - purge_node/2, subscribe_node/8, unsubscribe_node/4, - publish_item/6, delete_item/4, remove_extra_items/3, - get_entity_affiliations/2, get_node_affiliations/1, - get_affiliation/2, set_affiliation/3, - get_entity_subscriptions/2, get_node_subscriptions/1, - get_subscriptions/2, set_subscriptions/4, - get_pending_nodes/2, get_states/1, get_state/2, - set_state/1, get_items/6, get_items/2, get_item/7, - get_item/2, set_item/1, get_item_name/3, node_to_path/1, - path_to_node/1]). - -init(Host, ServerHost, Opts) -> - node_pep:init(Host, ServerHost, Opts). - -terminate(Host, ServerHost) -> - node_pep:terminate(Host, ServerHost), ok. - -options() -> - [{deliver_payloads, true}, {notify_config, false}, - {notify_delete, false}, {notify_retract, false}, - {purge_offline, false}, {persist_items, true}, - {max_items, ?MAXITEMS}, {subscribe, true}, - {access_model, presence}, {roster_groups_allowed, []}, - {publish_model, publishers}, - {notification_type, headline}, - {max_payload_size, ?MAX_PAYLOAD_SIZE}, - {send_last_published_item, on_sub_and_presence}, - {deliver_notifications, true}, - {presence_based_delivery, true}]. - -features() -> - [<<"create-nodes">>, %* - <<"auto-create">>, %* - <<"auto-subscribe">>, %* - <<"delete-nodes">>, %* - <<"delete-items">>, %* - <<"filtered-notifications">>, %* - <<"modify-affiliations">>, <<"outcast-affiliation">>, - <<"persistent-items">>, - <<"publish">>, %* - <<"purge-nodes">>, <<"retract-items">>, - <<"retrieve-affiliations">>, - <<"retrieve-items">>, %* - <<"retrieve-subscriptions">>, <<"subscribe">>]. - -create_node_permission(Host, ServerHost, Node, - ParentNode, Owner, Access) -> - node_pep:create_node_permission(Host, ServerHost, Node, - ParentNode, Owner, Access). - -create_node(NodeId, Owner) -> - node_pep:create_node(NodeId, Owner). - -delete_node(Removed) -> node_pep:delete_node(Removed). - -subscribe_node(NodeId, Sender, Subscriber, AccessModel, - SendLast, PresenceSubscription, RosterGroup, Options) -> - node_pep:subscribe_node(NodeId, Sender, Subscriber, - AccessModel, SendLast, PresenceSubscription, - RosterGroup, Options). - -unsubscribe_node(NodeId, Sender, Subscriber, SubID) -> - node_pep:unsubscribe_node(NodeId, Sender, Subscriber, - SubID). - -publish_item(NodeId, Publisher, Model, MaxItems, ItemId, - Payload) -> - node_pep:publish_item(NodeId, Publisher, Model, - MaxItems, ItemId, Payload). - -remove_extra_items(NodeId, MaxItems, ItemIds) -> - node_pep:remove_extra_items(NodeId, MaxItems, ItemIds). - -delete_item(NodeId, Publisher, PublishModel, ItemId) -> - node_pep:delete_item(NodeId, Publisher, PublishModel, - ItemId). - -purge_node(NodeId, Owner) -> - node_pep:purge_node(NodeId, Owner). - -get_entity_affiliations(Host, Owner) -> - node_pep:get_entity_affiliations(Host, Owner). - -get_node_affiliations(NodeId) -> - node_pep:get_node_affiliations(NodeId). - -get_affiliation(NodeId, Owner) -> - node_pep:get_affiliation(NodeId, Owner). - -set_affiliation(NodeId, Owner, Affiliation) -> - node_pep:set_affiliation(NodeId, Owner, Affiliation). - -get_entity_subscriptions(Host, Owner) -> - node_pep:get_entity_subscriptions(Host, Owner). - -get_node_subscriptions(NodeId) -> - node_pep:get_node_subscriptions(NodeId). - -get_subscriptions(NodeId, Owner) -> - node_pep:get_subscriptions(NodeId, Owner). - -set_subscriptions(NodeId, Owner, Subscription, SubId) -> - node_pep:set_subscriptions(NodeId, Owner, Subscription, - SubId). - -get_pending_nodes(Host, Owner) -> - node_hometree:get_pending_nodes(Host, Owner). - -get_states(NodeId) -> node_pep:get_states(NodeId). - -get_state(NodeId, JID) -> - node_pep:get_state(NodeId, JID). - -set_state(State) -> node_pep:set_state(State). - -get_items(NodeId, From) -> - node_pep:get_items(NodeId, From). - -get_items(NodeId, JID, AccessModel, - PresenceSubscription, RosterGroup, SubId) -> - node_pep:get_items(NodeId, JID, AccessModel, - PresenceSubscription, RosterGroup, SubId). - -get_item(NodeId, ItemId) -> - node_pep:get_item(NodeId, ItemId). - -get_item(NodeId, ItemId, JID, AccessModel, - PresenceSubscription, RosterGroup, SubId) -> - node_pep:get_item(NodeId, ItemId, JID, AccessModel, - PresenceSubscription, RosterGroup, SubId). - -set_item(Item) -> node_pep:set_item(Item). - -get_item_name(Host, Node, Id) -> - node_pep:get_item_name(Host, Node, Id). - -node_to_path(Node) -> node_pep:node_to_path(Node). - -path_to_node(Path) -> node_pep:path_to_node(Path). diff --git a/src/node_pep.erl b/src/node_pep.erl index f6575dafa..3d208c73b 100644 --- a/src/node_pep.erl +++ b/src/node_pep.erl @@ -1,500 +1,267 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. -%%% -%%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. -%%% -%%% -%%% The Initial Developer of the Original Code is ProcessOne. -%%% Portions created by ProcessOne are Copyright 2006-2015, ProcessOne -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2015, ProcessOne. +%%%---------------------------------------------------------------------- +%%% File : node_pep.erl +%%% Author : Christophe Romain +%%% Purpose : Standard PubSub PEP plugin +%%% Created : 1 Dec 2007 by Christophe Romain %%% %%% -%%% @copyright 2006-2015 ProcessOne -%%% @author Christophe Romain -%%% [http://www.process-one.net/] -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== +%%% ejabberd, Copyright (C) 2002-2025 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. +%%% +%%%---------------------------------------------------------------------- %%% @doc The module {@module} is the pep PubSub plugin. %%%

    PubSub plugin nodes are using the {@link gen_pubsub_node} behaviour.

    -module(node_pep). - +-behaviour(gen_pubsub_node). -author('christophe.romain@process-one.net'). --include("ejabberd.hrl"). --include("logger.hrl"). +-protocol({xep, 384, '0.8.3', '21.12', "complete", ""}). -include("pubsub.hrl"). --include("jlib.hrl"). - --behaviour(gen_pubsub_node). - -%% API definition -export([init/3, terminate/2, options/0, features/0, - create_node_permission/6, create_node/2, delete_node/1, - purge_node/2, subscribe_node/8, unsubscribe_node/4, - publish_item/6, delete_item/4, remove_extra_items/3, - get_entity_affiliations/2, get_node_affiliations/1, - get_affiliation/2, set_affiliation/3, - get_entity_subscriptions/2, get_node_subscriptions/1, - get_subscriptions/2, set_subscriptions/4, - get_pending_nodes/2, get_states/1, get_state/2, - set_state/1, get_items/6, get_items/2, get_item/7, - get_item/2, set_item/1, get_item_name/3, node_to_path/1, - path_to_node/1]). + 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_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, + get_subscriptions/2, set_subscriptions/4, + get_pending_nodes/2, get_states/1, get_state/2, + set_state/1, get_items/7, get_items/3, get_item/7, + get_last_items/3, get_only_item/2, + get_item/2, set_item/1, get_item_name/3, node_to_path/1, + path_to_node/1, depends/3]). + +depends(_Host, _ServerHost, _Opts) -> + [{mod_caps, hard}]. init(Host, ServerHost, Opts) -> - node_hometree:init(Host, ServerHost, Opts), - complain_if_modcaps_disabled(ServerHost), + node_flat:init(Host, ServerHost, Opts), ok. terminate(Host, ServerHost) -> - node_hometree:terminate(Host, ServerHost), ok. + node_flat:terminate(Host, ServerHost), + ok. --spec(options/0 :: () -> NodeOptions::mod_pubsub:nodeOptions()). options() -> - [{deliver_payloads, true}, {notify_config, false}, - {notify_delete, false}, {notify_retract, false}, - {purge_offline, false}, {persist_items, true}, - {max_items, 1}, {subscribe, true}, - {access_model, presence}, {roster_groups_allowed, []}, - {publish_model, publishers}, - {notification_type, headline}, - {max_payload_size, ?MAX_PAYLOAD_SIZE}, - {send_last_published_item, on_sub_and_presence}, - {deliver_notifications, true}, - {presence_based_delivery, true}]. + [{deliver_payloads, true}, + {notify_config, false}, + {notify_delete, false}, + {notify_retract, false}, + {purge_offline, false}, + {persist_items, true}, + {max_items, 1}, + {subscribe, true}, + {access_model, presence}, + {roster_groups_allowed, []}, + {publish_model, publishers}, + {notification_type, headline}, + {max_payload_size, ?MAX_PAYLOAD_SIZE}, + {send_last_published_item, on_sub_and_presence}, + {deliver_notifications, true}, + {presence_based_delivery, true}, + {itemreply, none}]. --spec(features/0 :: () -> Features::[binary(),...]). features() -> - [<<"create-nodes">>, %* - <<"auto-create">>, %* - <<"auto-subscribe">>, %* - <<"delete-nodes">>, %* - <<"delete-items">>, %* - <<"filtered-notifications">>, %* - <<"modify-affiliations">>, <<"outcast-affiliation">>, - <<"persistent-items">>, - <<"publish">>, %* - <<"purge-nodes">>, <<"retract-items">>, - <<"retrieve-affiliations">>, - <<"retrieve-items">>, %* - <<"retrieve-subscriptions">>, <<"subscribe">>]. - - --spec(create_node_permission/6 :: -( - Host :: mod_pubsub:hostPEP(), - ServerHost :: binary(), - NodeId :: mod_pubsub:nodeId(), - _ParentNodeId :: mod_pubsub:nodeId(), - Owner :: jid(), - Access :: atom()) - -> {result, boolean()} -). + [<<"create-nodes">>, + <<"auto-create">>, + <<"auto-subscribe">>, + <<"config-node">>, + <<"config-node-max">>, + <<"delete-nodes">>, + <<"delete-items">>, + <<"filtered-notifications">>, + <<"item-ids">>, + <<"modify-affiliations">>, + <<"multi-items">>, + <<"outcast-affiliation">>, + <<"persistent-items">>, + <<"publish">>, + <<"publish-options">>, + <<"purge-nodes">>, + <<"retract-items">>, + <<"retrieve-affiliations">>, + <<"retrieve-items">>, + <<"retrieve-subscriptions">>, + <<"subscribe">>]. create_node_permission(Host, ServerHost, _Node, _ParentNode, Owner, Access) -> - LOwner = jlib:jid_tolower(Owner), + LOwner = jid:tolower(Owner), {User, Server, _Resource} = LOwner, Allowed = case LOwner of - {<<"">>, Host, <<"">>} -> - true; % pubsub service always allowed + {<<"">>, Host, <<"">>} -> + true; % pubsub service always allowed + _ -> + case acl:match_rule(ServerHost, Access, LOwner) of + allow -> + case Host of + {User, Server, _} -> true; + _ -> false + end; _ -> - case acl:match_rule(ServerHost, Access, LOwner) of - allow -> - case Host of - {User, Server, _} -> true; - _ -> false - end; - E -> ?DEBUG("Create not allowed : ~p~n", [E]), false - end - end, + false + end + end, {result, Allowed}. --spec(create_node/2 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - Owner :: jid()) - -> {result, {default, broadcast}} -). -create_node(NodeIdx, Owner) -> - node_hometree:create_node(NodeIdx, Owner). +create_node(Nidx, Owner) -> + node_flat:create_node(Nidx, Owner). --spec(delete_node/1 :: -( - Nodes :: [mod_pubsub:pubsubNode(),...]) - -> {result, - {[], - [{mod_pubsub:pubsubNode(), - [{ljid(), [{mod_pubsub:subscription(), mod_pubsub:subId()}]},...]},...] - } - } -). +delete_node(Nodes) -> + node_flat:delete_node(Nodes). -delete_node(Removed) -> - case node_hometree:delete_node(Removed) of - {result, {_, _, Result}} -> {result, {[], Result}}; - Error -> Error +subscribe_node(Nidx, Sender, Subscriber, AccessModel, + SendLast, PresenceSubscription, RosterGroup, Options) -> + node_flat:subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast, + PresenceSubscription, RosterGroup, Options). + +unsubscribe_node(Nidx, Sender, Subscriber, SubId) -> + case node_flat:unsubscribe_node(Nidx, Sender, Subscriber, SubId) of + {error, Error} -> {error, Error}; + {result, _} -> {result, default} end. --spec(subscribe_node/8 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - Sender :: jid(), - Subscriber :: ljid(), - AccessModel :: mod_pubsub:accessModel(), - SendLast :: 'never' | 'on_sub' | 'on_sub_and_presence', - PresenceSubscription :: boolean(), - RosterGroup :: boolean(), - Options :: mod_pubsub:subOptions()) - -> {result, {default, subscribed, mod_pubsub:subId()}} - | {result, {default, subscribed, mod_pubsub:subId(), send_last}} - | {result, {default, pending, mod_pubsub:subId()}} - %%% - | {error, xmlel()} -). -subscribe_node(NodeIdx, Sender, Subscriber, AccessModel, SendLast, - PresenceSubscription, RosterGroup, Options) -> - node_hometree:subscribe_node(NodeIdx, Sender, Subscriber, AccessModel, - SendLast, PresenceSubscription, RosterGroup, Options). +publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload, PubOpts) -> + node_flat:publish_item(Nidx, Publisher, Model, MaxItems, ItemId, + Payload, PubOpts). --spec(unsubscribe_node/4 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - Sender :: jid(), - Subscriber :: ljid(), - SubId :: subId()) - -> {result, default} - % - | {error, xmlel()} -). -unsubscribe_node(NodeIdx, Sender, Subscriber, SubID) -> - case node_hometree:unsubscribe_node(NodeIdx, Sender, Subscriber, SubID) of - {error, Error} -> {error, Error}; - {result, _} -> {result, []} - end. +remove_extra_items(Nidx, MaxItems) -> + node_flat:remove_extra_items(Nidx, MaxItems). --spec(publish_item/6 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - Publisher :: jid(), - PublishModel :: mod_pubsub:publishModel(), - Max_Items :: non_neg_integer(), - ItemId :: <<>> | mod_pubsub:itemId(), - Payload :: mod_pubsub:payload()) - -> {result, {default, broadcast, [mod_pubsub:itemId()]}} - %%% - | {error, xmlel()} -). -publish_item(NodeIdx, Publisher, Model, MaxItems, ItemId, Payload) -> - node_hometree:publish_item(NodeIdx, Publisher, Model, MaxItems, ItemId, Payload). +remove_extra_items(Nidx, MaxItems, ItemIds) -> + node_flat:remove_extra_items(Nidx, MaxItems, ItemIds). --spec(remove_extra_items/3 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - Max_Items :: unlimited | non_neg_integer(), - ItemIds :: [mod_pubsub:itemId()]) - -> {result, - {NewItems::[mod_pubsub:itemId()], - OldItems::[mod_pubsub:itemId()]} - } -). -remove_extra_items(NodeId, MaxItems, ItemIds) -> - node_hometree:remove_extra_items(NodeId, 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). --spec(delete_item/4 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - Publisher :: jid(), - PublishModel :: mod_pubsub:publishModel(), - ItemId :: <<>> | mod_pubsub:itemId()) - -> {result, {default, broadcast}} - %%% - | {error, xmlel()} -). -delete_item(NodeIdx, Publisher, PublishModel, ItemId) -> - node_hometree:delete_item(NodeIdx, Publisher, PublishModel, ItemId). +purge_node(Nidx, Owner) -> + node_flat:purge_node(Nidx, Owner). --spec(purge_node/2 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - Owner :: jid()) - -> {result, {default, broadcast}} - | {error, xmlel()} -). -purge_node(NodeIdx, Owner) -> - node_hometree:purge_node(NodeIdx, Owner). - --spec(get_entity_affiliations/2 :: -( - Host :: mod_pubsub:hostPEP(), - Owner :: jid()) - -> {result, [{mod_pubsub:pubsubNode(), mod_pubsub:affiliation()}]} -). -get_entity_affiliations(_Host, Owner) -> - {_, D, _} = SubKey = jlib:jid_tolower(Owner), - SubKey = jlib:jid_tolower(Owner), - GenKey = jlib:jid_remove_resource(SubKey), - States = mnesia:match_object(#pubsub_state{stateid = - {GenKey, '_'}, - _ = '_'}), - NodeTree = case catch - ets:lookup(gen_mod:get_module_proc(D, config), nodetree) - of - [{nodetree, N}] -> N; - _ -> nodetree_tree - end, - Reply = lists:foldl(fun (#pubsub_state{stateid = {_, N}, - affiliation = A}, - Acc) -> - case NodeTree:get_node(N) of - #pubsub_node{nodeid = {{_, D, _}, _}} = - Node -> - [{Node, A} | Acc]; - _ -> Acc - end - end, - [], States), +get_entity_affiliations(Host, Owner) -> + {_, D, _} = SubKey = jid:tolower(Owner), + SubKey = jid:tolower(Owner), + GenKey = jid:remove_resource(SubKey), + States = mnesia:match_object(#pubsub_state{stateid = {GenKey, '_'}, _ = '_'}), + NodeTree = mod_pubsub:tree(Host), + Reply = lists:foldl(fun (#pubsub_state{stateid = {_, N}, affiliation = A}, Acc) -> + case NodeTree:get_node(N) of + #pubsub_node{nodeid = {{_, D, _}, _}} = Node -> [{Node, A} | Acc]; + _ -> Acc + end + end, + [], States), {result, Reply}. --spec(get_node_affiliations/1 :: -( - NodeIdx::mod_pubsub:nodeIdx()) - -> {result, [{ljid(), mod_pubsub:affiliation()}]} -). -get_node_affiliations(NodeIdx) -> - node_hometree:get_node_affiliations(NodeIdx). +get_node_affiliations(Nidx) -> + node_flat:get_node_affiliations(Nidx). --spec(get_affiliation/2 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - Owner :: jid()) - -> {result, mod_pubsub:affiliation()} -). -get_affiliation(NodeIdx, Owner) -> - node_hometree:get_affiliation(NodeIdx, Owner). +get_affiliation(Nidx, Owner) -> + node_flat:get_affiliation(Nidx, Owner). --spec(set_affiliation/3 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - Owner :: ljid(), - Affiliation :: mod_pubsub:affiliation()) - -> ok -). -set_affiliation(NodeIdx, Owner, Affiliation) -> - node_hometree:set_affiliation(NodeIdx, Owner, Affiliation). +set_affiliation(Nidx, Owner, Affiliation) -> + node_flat:set_affiliation(Nidx, Owner, Affiliation). --spec(get_entity_subscriptions/2 :: -( - Host :: mod_pubsub:hostPEP(), - Owner :: jid()) - -> {result, - [{mod_pubsub:pubsubNode(), - mod_pubsub:subscription(), - mod_pubsub:subId(), - ljid()}] - } -). -get_entity_subscriptions(_Host, Owner) -> - {U, D, _} = SubKey = jlib:jid_tolower(Owner), - GenKey = jlib:jid_remove_resource(SubKey), +get_entity_subscriptions(Host, Owner) -> + {U, D, _} = SubKey = jid:tolower(Owner), + GenKey = jid:remove_resource(SubKey), States = case SubKey of - GenKey -> - mnesia:match_object(#pubsub_state{stateid = - {{U, D, '_'}, '_'}, - _ = '_'}); - _ -> - mnesia:match_object(#pubsub_state{stateid = - {GenKey, '_'}, - _ = '_'}) - ++ - mnesia:match_object(#pubsub_state{stateid = - {SubKey, '_'}, - _ = '_'}) - end, - NodeTree = case catch - ets:lookup(gen_mod:get_module_proc(D, config), nodetree) - of - [{nodetree, N}] -> N; - _ -> nodetree_tree - end, - Reply = lists:foldl(fun (#pubsub_state{stateid = {J, N}, - subscriptions = Ss}, - Acc) -> - case NodeTree:get_node(N) of - #pubsub_node{nodeid = {{_, D, _}, _}} = Node -> - lists:foldl(fun - ({subscribed, SubID}, Acc2) -> - [{Node, subscribed, SubID, J} | Acc2]; - ({pending, _SubID}, Acc2) -> - [{Node, pending, J} | Acc2]; - (S, Acc2) -> - [{Node, S, J} | Acc2] - end, Acc, Ss); - _ -> Acc - end - end, - [], States), + GenKey -> + mnesia:match_object(#pubsub_state{stateid = {{U, D, '_'}, '_'}, _ = '_'}); + _ -> + mnesia:match_object(#pubsub_state{stateid = {GenKey, '_'}, _ = '_'}) + ++ + mnesia:match_object(#pubsub_state{stateid = {SubKey, '_'}, _ = '_'}) + end, + NodeTree = mod_pubsub:tree(Host), + Reply = lists:foldl(fun (#pubsub_state{stateid = {J, N}, subscriptions = Ss}, Acc) -> + case NodeTree:get_node(N) of + #pubsub_node{nodeid = {{_, D, _}, _}} = Node -> + lists:foldl(fun + ({subscribed, SubId}, Acc2) -> + [{Node, subscribed, SubId, J} | Acc2]; + ({pending, _SubId}, Acc2) -> + [{Node, pending, J} | Acc2]; + (S, Acc2) -> + [{Node, S, J} | Acc2] + end, + Acc, Ss); + _ -> + Acc + end + end, + [], States), {result, Reply}. --spec(get_node_subscriptions/1 :: -( - NodeIdx::mod_pubsub:nodeIdx()) - -> {result, - [{ljid(), mod_pubsub:subscription(), mod_pubsub:subId()}] | - [{ljid(), none},...] - } -). -get_node_subscriptions(NodeIdx) -> - node_hometree:get_node_subscriptions(NodeIdx). +get_node_subscriptions(Nidx) -> + node_flat:get_node_subscriptions(Nidx). --spec(get_subscriptions/2 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - Owner :: ljid()) - -> {result, [{mod_pubsub:subscription(), mod_pubsub:subId()}]} -). -get_subscriptions(NodeIdx, Owner) -> - node_hometree:get_subscriptions(NodeIdx, Owner). +get_subscriptions(Nidx, Owner) -> + node_flat:get_subscriptions(Nidx, Owner). --spec(set_subscriptions/4 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - Owner :: jid(), - Subscription :: mod_pubsub:subscription(), - SubId :: mod_pubsub:subId()) - -> ok - %%% - | {error, xmlel()} -). -set_subscriptions(NodeIdx, Owner, Subscription, SubId) -> - node_hometree:set_subscriptions(NodeIdx, Owner, Subscription, SubId). +set_subscriptions(Nidx, Owner, Subscription, SubId) -> + node_flat:set_subscriptions(Nidx, Owner, Subscription, SubId). --spec(get_pending_nodes/2 :: -( - Host :: mod_pubsub:hostPubsub(), - Owner :: jid()) - -> {result, [mod_pubsub:nodeId()]} -). get_pending_nodes(Host, Owner) -> - node_hometree:get_pending_nodes(Host, Owner). + node_flat:get_pending_nodes(Host, Owner). --spec(get_states/1 :: -( - NodeIdx::mod_pubsub:nodeIdx()) - -> {result, [mod_pubsub:pubsubState()]} -). -get_states(NodeIdx) -> node_hometree:get_states(NodeIdx). +get_states(Nidx) -> + node_flat:get_states(Nidx). --spec(get_state/2 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - JID :: ljid()) - -> mod_pubsub:pubsubState() -). -get_state(NodeIdx, JID) -> - node_hometree:get_state(NodeIdx, JID). +get_state(Nidx, JID) -> + node_flat:get_state(Nidx, JID). --spec(set_state/1 :: -( - State::mod_pubsub:pubsubState()) - -> ok -). -set_state(State) -> node_hometree:set_state(State). +set_state(State) -> + node_flat:set_state(State). --spec(get_items/2 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - _From :: jid()) - -> {result, [mod_pubsub:pubsubItem()]} -). -get_items(NodeIdx, From) -> - node_hometree:get_items(NodeIdx, From). +get_items(Nidx, From, RSM) -> + node_flat:get_items(Nidx, From, RSM). --spec(get_items/6 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - JID :: jid(), - AccessModel :: mod_pubsub:accessModel(), - Presence_Subscription :: boolean(), - RosterGroup :: boolean(), - _SubId :: mod_pubsub:subId()) - -> {result, [mod_pubsub:pubsubItem()]} - %%% - | {error, xmlel()} -). -get_items(NodeIdx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> - node_hometree:get_items(NodeIdx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId). +get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, RSM) -> + node_flat:get_items(Nidx, JID, AccessModel, + PresenceSubscription, RosterGroup, SubId, RSM). --spec(get_item/2 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - ItemId :: mod_pubsub:itemId()) - -> {result, mod_pubsub:pubsubItem()} - | {error, xmlel()} -). -get_item(NodeIdx, ItemId) -> - node_hometree:get_item(NodeIdx, ItemId). +get_last_items(Nidx, From, Count) -> + node_flat:get_last_items(Nidx, From, Count). --spec(get_item/7 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - ItemId :: mod_pubsub:itemId(), - JID :: jid(), - AccessModel :: mod_pubsub:accessModel(), - PresenceSubscription :: boolean(), - RosterGroup :: boolean(), - SubId :: mod_pubsub:subId()) - -> {result, mod_pubsub:pubsubItem()} - | {error, xmlel()} -). -get_item(NodeIdx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, - SubId) -> - node_hometree:get_item(NodeIdx, ItemId, JID, AccessModel, PresenceSubscription, - RosterGroup, SubId). +get_only_item(Nidx, From) -> + node_flat:get_only_item(Nidx, From). --spec(set_item/1 :: -( - Item::mod_pubsub:pubsubItem()) - -> ok -). -set_item(Item) -> node_hometree:set_item(Item). +get_item(Nidx, ItemId) -> + node_flat:get_item(Nidx, ItemId). + +get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> + node_flat:get_item(Nidx, ItemId, JID, AccessModel, + PresenceSubscription, RosterGroup, SubId). + +set_item(Item) -> + node_flat:set_item(Item). get_item_name(Host, Node, Id) -> - node_hometree:get_item_name(Host, Node, Id). + node_flat:get_item_name(Host, Node, Id). -node_to_path(Node) -> node_flat:node_to_path(Node). +node_to_path(Node) -> + node_flat:node_to_path(Node). -path_to_node(Path) -> node_flat:path_to_node(Path). - -%%% -%%% Internal -%%% - -%% @doc Check mod_caps is enabled, otherwise show warning. -%% The PEP plugin for mod_pubsub requires mod_caps to be enabled in the host. -%% Check that the mod_caps module is enabled in that Jabber Host -%% If not, show a warning message in the ejabberd log file. -complain_if_modcaps_disabled(ServerHost) -> - case gen_mod:is_loaded(ServerHost, mod_caps) of - false -> - ?WARNING_MSG("The PEP plugin is enabled in mod_pubsub " - "of host ~p. This plugin requires mod_caps " - "to be enabled, but it isn't.", - [ServerHost]); - true -> ok - end. +path_to_node(Path) -> + node_flat:path_to_node(Path). diff --git a/src/node_pep_odbc.erl b/src/node_pep_odbc.erl deleted file mode 100644 index 39b936aad..000000000 --- a/src/node_pep_odbc.erl +++ /dev/null @@ -1,448 +0,0 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. -%%% -%%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. -%%% -%%% -%%% The Initial Developer of the Original Code is ProcessOne. -%%% Portions created by ProcessOne are Copyright 2006-2015, ProcessOne -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2015, ProcessOne. -%%% -%%% -%%% @copyright 2006-2015 ProcessOne -%%% @author Christophe Romain -%%% [http://www.process-one.net/] -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== - -%%% @doc The module {@module} is the pep PubSub plugin. -%%%

    PubSub plugin nodes are using the {@link gen_pubsub_node} behaviour.

    - --module(node_pep_odbc). - --author('christophe.romain@process-one.net'). - --include("ejabberd.hrl"). --include("logger.hrl"). - --include("pubsub.hrl"). - --include("jlib.hrl"). - --define(PUBSUB, mod_pubsub_odbc). - --behaviour(gen_pubsub_node). - -%% API definition --export([init/3, terminate/2, options/0, features/0, - create_node_permission/6, create_node/2, delete_node/1, - purge_node/2, subscribe_node/8, unsubscribe_node/4, - publish_item/6, delete_item/4, remove_extra_items/3, - get_entity_affiliations/2, get_node_affiliations/1, - get_affiliation/2, set_affiliation/3, - get_entity_subscriptions/2, - get_entity_subscriptions_for_send_last/2, - get_node_subscriptions/1, get_subscriptions/2, - set_subscriptions/4, get_pending_nodes/2, get_states/1, - get_state/2, set_state/1, get_items/7, get_items/6, - get_items/3, get_items/2, get_item/7, get_item/2, - set_item/1, get_item_name/3, get_last_items/3, - node_to_path/1, path_to_node/1]). - -init(Host, ServerHost, Opts) -> - node_hometree_odbc:init(Host, ServerHost, Opts), - complain_if_modcaps_disabled(ServerHost), - ok. - -terminate(Host, ServerHost) -> - node_hometree_odbc:terminate(Host, ServerHost), ok. - --spec(options/0 :: () -> NodeOptions::mod_pubsub:nodeOptions()). -options() -> - [{odbc, true}, - {deliver_payloads, true}, - {notify_config, false}, - {notify_delete, false}, - {notify_retract, false}, - {purge_offline, false}, - {persist_items, true}, - {max_items, 1}, - {subscribe, true}, - {access_model, presence}, - {roster_groups_allowed, []}, - {publish_model, publishers}, - {notification_type, headline}, - {max_payload_size, ?MAX_PAYLOAD_SIZE}, - {send_last_published_item, on_sub_and_presence}, - {deliver_notifications, true}, - {presence_based_delivery, true}]. - --spec(features/0 :: () -> Features::[binary(),...]). -features() -> - [<<"create-nodes">>, %* - <<"auto-create">>, %* - <<"auto-subscribe">>, %* - <<"delete-nodes">>, %* - <<"delete-items">>, %* - <<"filtered-notifications">>, %* - <<"modify-affiliations">>, <<"outcast-affiliation">>, - <<"persistent-items">>, - <<"publish">>, %* - <<"purge-nodes">>, <<"retract-items">>, - <<"retrieve-affiliations">>, - <<"retrieve-items">>, %* - <<"retrieve-subscriptions">>, <<"subscribe">>]. - --spec(create_node_permission/6 :: -( - Host :: mod_pubsub:hostPEP(), - ServerHost :: binary(), - NodeId :: mod_pubsub:nodeId(), - _ParentNodeId :: mod_pubsub:nodeId(), - Owner :: jid(), - Access :: atom()) - -> {result, boolean()} -). - -create_node_permission(Host, ServerHost, _NodeId, _ParentNode, Owner, Access) -> - LOwner = jlib:jid_tolower(Owner), - {User, Server, _Resource} = LOwner, - Allowed = case LOwner of - {<<"">>, Host, <<"">>} -> - true; % pubsub service always allowed - _ -> - case acl:match_rule(ServerHost, Access, LOwner) of - allow -> - case Host of - {User, Server, _} -> true; - _ -> false - end; - E -> ?DEBUG("Create not allowed : ~p~n", [E]), false - end - end, - {result, Allowed}. - --spec(create_node/2 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - Owner :: jid()) - -> {result, []} -). -create_node(NodeIdx, Owner) -> - case node_hometree_odbc:create_node(NodeIdx, Owner) of - {result, _} -> {result, []}; - Error -> Error - end. - --spec(delete_node/1 :: -( - Removed :: [mod_pubsub:pubsubNode(),...]) - -> {result, - {[], - [{mod_pubsub:pubsubNode(), - [{ljid(), [{mod_pubsub:subscription(), mod_pubsub:subId()}]}]}] - } - } -). -delete_node(Removed) -> - case node_hometree_odbc:delete_node(Removed) of - {result, {_, _, Result}} -> {result, {[], Result}}; - Error -> Error - end. - --spec(subscribe_node/8 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - Sender :: jid(), - Subscriber :: ljid(), - AccessModel :: mod_pubsub:accessModel(), - SendLast :: 'never' | 'on_sub' | 'on_sub_and_presence', - PresenceSubscription :: boolean(), - RosterGroup :: boolean(), - Options :: mod_pubsub:subOptions()) - -> {result, {default, subscribed, mod_pubsub:subId()}} - | {result, {default, subscribed, mod_pubsub:subId(), send_last}} - | {result, {default, pending, mod_pubsub:subId()}} - %%% - | {error, _} - | {error, _, binary()} -). -subscribe_node(NodeId, Sender, Subscriber, AccessModel, - SendLast, PresenceSubscription, RosterGroup, Options) -> - node_hometree_odbc:subscribe_node(NodeId, Sender, - Subscriber, AccessModel, SendLast, - PresenceSubscription, RosterGroup, - Options). - - --spec(unsubscribe_node/4 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - Sender :: jid(), - Subscriber :: jid(), - SubId :: subId()) - -> {result, []} - % - | {error, _} - | {error, _, binary()} -). -unsubscribe_node(NodeIdx, Sender, Subscriber, SubID) -> - case node_hometree_odbc:unsubscribe_node(NodeIdx, Sender, Subscriber, SubID) of - {error, Error} -> {error, Error}; - {result, _} -> {result, []} - end. - --spec(publish_item/6 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - Publisher :: jid(), - PublishModel :: mod_pubsub:publishModel(), - Max_Items :: non_neg_integer(), - ItemId :: <<>> | mod_pubsub:itemId(), - Payload :: mod_pubsub:payload()) - -> {result, {default, broadcast, [mod_pubsub:itemId()]}} - %%% - | {error, _} -). -publish_item(NodeIdx, Publisher, Model, MaxItems, ItemId, Payload) -> - node_hometree_odbc:publish_item(NodeIdx, Publisher, - Model, MaxItems, ItemId, Payload). - -remove_extra_items(NodeId, MaxItems, ItemIds) -> - node_hometree_odbc:remove_extra_items(NodeId, MaxItems, - ItemIds). - --spec(delete_item/4 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - Publisher :: jid(), - PublishModel :: mod_pubsub:publishModel(), - ItemId :: <<>> | mod_pubsub:itemId()) - -> {result, {default, broadcast}} - %%% - | {error, _} -). -delete_item(NodeId, Publisher, PublishModel, ItemId) -> - node_hometree_odbc:delete_item(NodeId, Publisher, - PublishModel, ItemId). - --spec(purge_node/2 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - Owner :: jid()) - -> {result, {default, broadcast}} - | {error, _} -). -purge_node(NodeIdx, Owner) -> - node_hometree_odbc:purge_node(NodeIdx, Owner). - --spec(get_entity_affiliations/2 :: -( - Host :: mod_pubsub:hostPubsub(), - Owner :: jid()) - -> {result, [{mod_pubsub:pubsubNode(), mod_pubsub:affiliation()}]} -). -get_entity_affiliations(_Host, Owner) -> - OwnerKey = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), - node_hometree_odbc:get_entity_affiliations(OwnerKey, Owner). - --spec(get_node_affiliations/1 :: -( - NodeIdx::mod_pubsub:nodeIdx()) - -> {result, [{ljid(), mod_pubsub:affiliation()}]} -). -get_node_affiliations(NodeIdx) -> - node_hometree_odbc:get_node_affiliations(NodeIdx). - --spec(get_affiliation/2 :: -( - NodeIdx :: mod_pubsub:nodeIdx(), - Owner :: ljid()) - -> {result, mod_pubsub:affiliation()} -). -get_affiliation(NodeIdx, Owner) -> - node_hometree_odbc:get_affiliation(NodeIdx, Owner). - -set_affiliation(NodeId, Owner, Affiliation) -> - node_hometree_odbc:set_affiliation(NodeId, Owner, Affiliation). - -get_entity_subscriptions(_Host, Owner) -> - SubKey = jlib:jid_tolower(Owner), - GenKey = jlib:jid_remove_resource(SubKey), - Host = (?PUBSUB):escape(element(2, SubKey)), - SJ = node_hometree_odbc:encode_jid(SubKey), - GJ = node_hometree_odbc:encode_jid(GenKey), - Query = case SubKey of - GenKey -> - [<<"select host, node, type, i.nodeid, jid, " - "subscriptions from pubsub_state i, pubsub_nod" - "e n where i.nodeid = n.nodeid and jid " - "like '">>, - GJ, <<"%' and host like '%@">>, Host, <<"';">>]; - _ -> - [<<"select host, node, type, i.nodeid, jid, " - "subscriptions from pubsub_state i, pubsub_nod" - "e n where i.nodeid = n.nodeid and jid " - "in ('">>, - SJ, <<"', '">>, GJ, <<"') and host like '%@">>, Host, - <<"';">>] - end, - Reply = case catch ejabberd_odbc:sql_query_t(Query) of - {selected, - [<<"host">>, <<"node">>, <<"type">>, <<"nodeid">>, - <<"jid">>, <<"subscriptions">>], - RItems} -> - lists:map(fun ([H, N, T, I, J, S]) -> - O = node_hometree_odbc:decode_jid(H), - Node = nodetree_tree_odbc:raw_to_node(O, - [N, - <<"">>, - T, - I]), - {Node, - node_hometree_odbc:decode_subscriptions(S), - node_hometree_odbc:decode_jid(J)} - end, - RItems); - _ -> [] - end, - {result, Reply}. - -get_entity_subscriptions_for_send_last(_Host, Owner) -> - SubKey = jlib:jid_tolower(Owner), - GenKey = jlib:jid_remove_resource(SubKey), - Host = (?PUBSUB):escape(element(2, SubKey)), - SJ = node_hometree_odbc:encode_jid(SubKey), - GJ = node_hometree_odbc:encode_jid(GenKey), - Query = case SubKey of - GenKey -> - [<<"select host, node, type, i.nodeid, jid, " - "subscriptions from pubsub_state i, pubsub_nod" - "e n, pubsub_node_option o where i.nodeid " - "= n.nodeid and n.nodeid = o.nodeid and " - "name='send_last_published_item' and " - "val='on_sub_and_presence' and jid like " - "'">>, - GJ, <<"%' and host like '%@">>, Host, <<"';">>]; - _ -> - [<<"select host, node, type, i.nodeid, jid, " - "subscriptions from pubsub_state i, pubsub_nod" - "e n, pubsub_node_option o where i.nodeid " - "= n.nodeid and n.nodeid = o.nodeid and " - "name='send_last_published_item' and " - "val='on_sub_and_presence' and jid in " - "('">>, - SJ, <<"', '">>, GJ, <<"') and host like '%@">>, Host, - <<"';">>] - end, - Reply = case catch ejabberd_odbc:sql_query_t(Query) of - {selected, - [<<"host">>, <<"node">>, <<"type">>, <<"nodeid">>, - <<"jid">>, <<"subscriptions">>], - RItems} -> - lists:map(fun ([H, N, T, I, J, S]) -> - O = node_hometree_odbc:decode_jid(H), - Node = nodetree_tree_odbc:raw_to_node(O, - [N, - <<"">>, - T, - I]), - {Node, - node_hometree_odbc:decode_subscriptions(S), - node_hometree_odbc:decode_jid(J)} - end, - RItems); - _ -> [] - end, - {result, Reply}. - -get_node_subscriptions(NodeId) -> - node_hometree_odbc:get_node_subscriptions(NodeId). - -get_subscriptions(NodeId, Owner) -> - node_hometree_odbc:get_subscriptions(NodeId, Owner). - -set_subscriptions(NodeId, Owner, Subscription, SubId) -> - node_hometree_odbc:set_subscriptions(NodeId, Owner, - Subscription, SubId). - -get_pending_nodes(Host, Owner) -> - node_hometree_odbc:get_pending_nodes(Host, Owner). - -get_states(NodeId) -> - node_hometree_odbc:get_states(NodeId). - -get_state(NodeId, JID) -> - node_hometree_odbc:get_state(NodeId, JID). - -set_state(State) -> node_hometree_odbc:set_state(State). - -get_items(NodeId, From) -> - node_hometree_odbc:get_items(NodeId, From). - -get_items(NodeId, From, RSM) -> - node_hometree_odbc:get_items(NodeId, From, RSM). - -get_items(NodeId, JID, AccessModel, - PresenceSubscription, RosterGroup, SubId) -> - get_items(NodeId, JID, AccessModel, - PresenceSubscription, RosterGroup, SubId, none). - -get_items(NodeId, JID, AccessModel, - PresenceSubscription, RosterGroup, SubId, RSM) -> - node_hometree_odbc:get_items(NodeId, JID, AccessModel, - PresenceSubscription, RosterGroup, SubId, RSM). - -get_last_items(NodeId, JID, Count) -> - node_hometree_odbc:get_last_items(NodeId, JID, Count). - -get_item(NodeId, ItemId) -> - node_hometree_odbc:get_item(NodeId, ItemId). - -get_item(NodeId, ItemId, JID, AccessModel, - PresenceSubscription, RosterGroup, SubId) -> - node_hometree_odbc:get_item(NodeId, ItemId, JID, - AccessModel, PresenceSubscription, RosterGroup, - SubId). - -set_item(Item) -> node_hometree_odbc:set_item(Item). - -get_item_name(Host, Node, Id) -> - node_hometree_odbc:get_item_name(Host, Node, Id). - -node_to_path(Node) -> node_flat_odbc:node_to_path(Node). - -path_to_node(Path) -> node_flat_odbc:path_to_node(Path). - -%%% -%%% Internal -%%% - -%% @doc Check mod_caps is enabled, otherwise show warning. -%% The PEP plugin for mod_pubsub requires mod_caps to be enabled in the host. -%% Check that the mod_caps module is enabled in that Jabber Host -%% If not, show a warning message in the ejabberd log file. -complain_if_modcaps_disabled(ServerHost) -> - Modules = ejabberd_config:get_option({modules, - ServerHost}, - fun(Ms) when is_list(Ms) -> Ms end), - ModCaps = [mod_caps_enabled - || {mod_caps, _Opts} <- Modules], - case ModCaps of - [] -> - ?WARNING_MSG("The PEP plugin is enabled in mod_pubsub " - "of host ~p. This plugin requires mod_caps " - "to be enabled, but it isn't.", - [ServerHost]); - _ -> ok - end. diff --git a/src/node_pep_sql.erl b/src/node_pep_sql.erl new file mode 100644 index 000000000..5d35c7bfe --- /dev/null +++ b/src/node_pep_sql.erl @@ -0,0 +1,249 @@ +%%%---------------------------------------------------------------------- +%%% File : node_pep_sql.erl +%%% Author : Christophe Romain +%%% Purpose : Standard PubSub PEP plugin with ODBC backend +%%% Created : 1 Dec 2007 by Christophe Romain +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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. +%%% +%%%---------------------------------------------------------------------- + +%%% @doc The module {@module} is the pep PubSub plugin. +%%%

    PubSub plugin nodes are using the {@link gen_pubsub_node} behaviour.

    + +-module(node_pep_sql). +-behaviour(gen_pubsub_node). +-author('christophe.romain@process-one.net'). + + +-include("pubsub.hrl"). +-include("ejabberd_sql_pt.hrl"). + +-export([init/3, terminate/2, options/0, features/0, + 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_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, + get_subscriptions/2, set_subscriptions/4, + get_pending_nodes/2, get_states/1, get_state/2, + set_state/1, get_items/7, get_items/3, get_item/7, + get_item/2, set_item/1, get_item_name/3, node_to_path/1, + path_to_node/1, depends/3, + get_entity_subscriptions_for_send_last/2, get_last_items/3, + get_only_item/2]). + +depends(_Host, _ServerHost, _Opts) -> + [{mod_caps, hard}]. + +init(Host, ServerHost, Opts) -> + node_flat_sql:init(Host, ServerHost, Opts), + ok. + +terminate(Host, ServerHost) -> + node_flat_sql:terminate(Host, ServerHost), + ok. + +options() -> + [{sql, true}, {rsm, true} | node_pep:options()]. + +features() -> + [<<"rsm">> | node_pep:features()]. + +create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access) -> + node_pep:create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access). + +create_node(Nidx, Owner) -> + node_flat_sql:create_node(Nidx, Owner), + {result, {default, broadcast}}. + +delete_node(Nodes) -> + node_flat_sql:delete_node(Nodes). + +subscribe_node(Nidx, Sender, Subscriber, AccessModel, + SendLast, PresenceSubscription, RosterGroup, Options) -> + node_flat_sql:subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast, + PresenceSubscription, RosterGroup, Options). + + +unsubscribe_node(Nidx, Sender, Subscriber, SubId) -> + case node_flat_sql:unsubscribe_node(Nidx, Sender, Subscriber, SubId) of + {error, Error} -> {error, Error}; + {result, _} -> {result, default} + end. + +publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload, PubOpts) -> + node_flat_sql:publish_item(Nidx, Publisher, Model, MaxItems, ItemId, + Payload, PubOpts). + +remove_extra_items(Nidx, MaxItems) -> + node_flat_sql: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). + +purge_node(Nidx, Owner) -> + node_flat_sql:purge_node(Nidx, Owner). + +get_entity_affiliations(_Host, Owner) -> + OwnerKey = jid:tolower(jid:remove_resource(Owner)), + node_flat_sql:get_entity_affiliations(OwnerKey, Owner). + +get_node_affiliations(Nidx) -> + node_flat_sql:get_node_affiliations(Nidx). + +get_affiliation(Nidx, Owner) -> + node_flat_sql:get_affiliation(Nidx, Owner). + +set_affiliation(Nidx, Owner, Affiliation) -> + node_flat_sql:set_affiliation(Nidx, Owner, Affiliation). + +get_entity_subscriptions(_Host, Owner) -> + SubKey = jid:tolower(Owner), + GenKey = jid:remove_resource(SubKey), + HLike = <<"%@", (node_flat_sql:encode_host_like(element(2, SubKey)))/binary>>, + GJ = node_flat_sql:encode_jid(GenKey), + Query = case SubKey of + GenKey -> + GJLike = <<(node_flat_sql:encode_jid_like(GenKey))/binary, "/%">>, + ?SQL("select @(host)s, @(node)s, @(plugin)s, @(i.nodeid)d, @(jid)s, @(subscriptions)s " + "from pubsub_state i, pubsub_node n " + "where i.nodeid = n.nodeid and " + "(jid=%(GJ)s or jid like %(GJLike)s %ESCAPE) and host like %(HLike)s %ESCAPE"); + _ -> + SJ = node_flat_sql:encode_jid(SubKey), + ?SQL("select @(host)s, @(node)s, @(plugin)s, @(i.nodeid)d, @(jid)s, @(subscriptions)s " + "from pubsub_state i, pubsub_node n " + "where i.nodeid = n.nodeid and " + "jid in (%(SJ)s,%(GJ)s) and host like %(HLike)s %ESCAPE") + end, + {result, + case ejabberd_sql:sql_query_t(Query) of + {selected, RItems} -> + lists:foldl( + fun({H, N, T, I, J, S}, Acc) -> + O = node_flat_sql:decode_jid(H), + Node = nodetree_tree_sql:raw_to_node(O, {N, <<"">>, T, I}), + Jid = node_flat_sql:decode_jid(J), + lists:foldl( + fun({Sub, SubId}, Acc2) -> + [{Node, Sub, SubId, Jid} | Acc2] + end, Acc, node_flat_sql:decode_subscriptions(S)) + end, [], RItems); + _ -> + [] + end}. + +get_entity_subscriptions_for_send_last(_Host, Owner) -> + SubKey = jid:tolower(Owner), + GenKey = jid:remove_resource(SubKey), + HLike = <<"%@", (node_flat_sql:encode_host_like(element(2, SubKey)))/binary>>, + GJ = node_flat_sql:encode_jid(GenKey), + Query = case SubKey of + GenKey -> + GJLike = <<(node_flat_sql:encode_jid_like(GenKey))/binary, "/%">>, + ?SQL("select @(host)s, @(node)s, @(plugin)s, @(i.nodeid)d, @(jid)s, @(subscriptions)s " + "from pubsub_state i, pubsub_node n, pubsub_node_option o " + "where i.nodeid = n.nodeid and n.nodeid = o.nodeid and " + "name='send_last_published_item' and val='on_sub_and_presence' and " + "(jid=%(GJ)s or jid like %(GJLike)s %ESCAPE) and host like %(HLike)s %ESCAPE"); + _ -> + SJ = node_flat_sql:encode_jid(SubKey), + ?SQL("select @(host)s, @(node)s, @(plugin)s, @(i.nodeid)d, @(jid)s, @(subscriptions)s " + "from pubsub_state i, pubsub_node n, pubsub_node_option o " + "where i.nodeid = n.nodeid and n.nodeid = o.nodeid and " + "name='send_last_published_item' and val='on_sub_and_presence' and " + "jid in (%(SJ)s,%(GJ)s) and host like %(HLike)s %ESCAPE") + end, + {result, + case ejabberd_sql:sql_query_t(Query) of + {selected, RItems} -> + lists:foldl( + fun ({H, N, T, I, J, S}, Acc) -> + O = node_flat_sql:decode_jid(H), + Node = nodetree_tree_sql:raw_to_node(O, {N, <<"">>, T, I}), + Jid = node_flat_sql:decode_jid(J), + lists:foldl( + fun ({Sub, SubId}, Acc2) -> + [{Node, Sub, SubId, Jid}| Acc2] + end, Acc, node_flat_sql:decode_subscriptions(S)) + end, [], RItems); + _ -> + [] + end}. + +get_node_subscriptions(Nidx) -> + node_flat_sql:get_node_subscriptions(Nidx). + +get_subscriptions(Nidx, Owner) -> + node_flat_sql:get_subscriptions(Nidx, Owner). + +set_subscriptions(Nidx, Owner, Subscription, SubId) -> + node_flat_sql:set_subscriptions(Nidx, Owner, Subscription, SubId). + +get_pending_nodes(Host, Owner) -> + node_flat_sql:get_pending_nodes(Host, Owner). + +get_states(Nidx) -> + node_flat_sql:get_states(Nidx). + +get_state(Nidx, JID) -> + node_flat_sql:get_state(Nidx, JID). + +set_state(State) -> + node_flat_sql:set_state(State). + +get_items(Nidx, From, RSM) -> + node_flat_sql:get_items(Nidx, From, RSM). + +get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, RSM) -> + node_flat_sql:get_items(Nidx, JID, AccessModel, + PresenceSubscription, RosterGroup, SubId, RSM). + +get_last_items(Nidx, JID, Count) -> + node_flat_sql:get_last_items(Nidx, JID, Count). + +get_only_item(Nidx, JID) -> + node_flat_sql:get_only_item(Nidx, JID). + +get_item(Nidx, ItemId) -> + node_flat_sql:get_item(Nidx, ItemId). + +get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> + node_flat_sql:get_item(Nidx, ItemId, JID, AccessModel, + PresenceSubscription, RosterGroup, SubId). + +set_item(Item) -> + node_flat_sql:set_item(Item). + +get_item_name(Host, Node, Id) -> + node_flat_sql:get_item_name(Host, Node, Id). + +node_to_path(Node) -> + node_flat_sql:node_to_path(Node). + +path_to_node(Path) -> + node_flat_sql:path_to_node(Path). diff --git a/src/node_private.erl b/src/node_private.erl deleted file mode 100644 index 12459e595..000000000 --- a/src/node_private.erl +++ /dev/null @@ -1,184 +0,0 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. -%%% -%%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. -%%% -%%% -%%% The Initial Developer of the Original Code is ProcessOne. -%%% Portions created by ProcessOne are Copyright 2006-2015, ProcessOne -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2015, ProcessOne. -%%% -%%% -%%% @copyright 2006-2015 ProcessOne -%%% @author Christophe romain -%%% [http://www.process-one.net/] -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== - --module(node_private). - --author('christophe.romain@process-one.net'). - --include("pubsub.hrl"). - --include("jlib.hrl"). - --behaviour(gen_pubsub_node). - -%% Note on function definition -%% included is all defined plugin function -%% it's possible not to define some function at all -%% in that case, warning will be generated at compilation -%% and function call will fail, -%% then mod_pubsub will call function from node_hometree -%% (this makes code cleaner, but execution a little bit longer) - -%% API definition --export([init/3, terminate/2, options/0, features/0, - create_node_permission/6, create_node/2, delete_node/1, - purge_node/2, subscribe_node/8, unsubscribe_node/4, - publish_item/6, delete_item/4, remove_extra_items/3, - get_entity_affiliations/2, get_node_affiliations/1, - get_affiliation/2, set_affiliation/3, - get_entity_subscriptions/2, get_node_subscriptions/1, - get_subscriptions/2, set_subscriptions/4, - get_pending_nodes/2, get_states/1, get_state/2, - set_state/1, get_items/6, get_items/2, get_item/7, - get_item/2, set_item/1, get_item_name/3, node_to_path/1, - path_to_node/1]). - -init(Host, ServerHost, Opts) -> - node_hometree:init(Host, ServerHost, Opts). - -terminate(Host, ServerHost) -> - node_hometree:terminate(Host, ServerHost). - -options() -> - [{deliver_payloads, true}, {notify_config, false}, - {notify_delete, false}, {notify_retract, true}, - {purge_offline, false}, {persist_items, true}, - {max_items, ?MAXITEMS}, {subscribe, true}, - {access_model, whitelist}, {roster_groups_allowed, []}, - {publish_model, publishers}, - {notification_type, headline}, - {max_payload_size, ?MAX_PAYLOAD_SIZE}, - {send_last_published_item, never}, - {deliver_notifications, false}, - {presence_based_delivery, false}]. - -features() -> - [<<"create-nodes">>, <<"delete-nodes">>, - <<"delete-items">>, <<"instant-nodes">>, - <<"outcast-affiliation">>, <<"persistent-items">>, - <<"publish">>, <<"purge-nodes">>, <<"retract-items">>, - <<"retrieve-affiliations">>, <<"retrieve-items">>, - <<"retrieve-subscriptions">>, <<"subscribe">>, - <<"subscription-notifications">>]. - -create_node_permission(Host, ServerHost, Node, - ParentNode, Owner, Access) -> - node_hometree:create_node_permission(Host, ServerHost, - Node, ParentNode, Owner, Access). - -create_node(NodeId, Owner) -> - node_hometree:create_node(NodeId, Owner). - -delete_node(Removed) -> - node_hometree:delete_node(Removed). - -subscribe_node(NodeId, Sender, Subscriber, AccessModel, - SendLast, PresenceSubscription, RosterGroup, Options) -> - node_hometree:subscribe_node(NodeId, Sender, Subscriber, - AccessModel, SendLast, PresenceSubscription, - RosterGroup, Options). - -unsubscribe_node(NodeId, Sender, Subscriber, SubID) -> - node_hometree:unsubscribe_node(NodeId, Sender, - Subscriber, SubID). - -publish_item(NodeId, Publisher, Model, MaxItems, ItemId, - Payload) -> - node_hometree:publish_item(NodeId, Publisher, Model, - MaxItems, ItemId, Payload). - -remove_extra_items(NodeId, MaxItems, ItemIds) -> - node_hometree:remove_extra_items(NodeId, MaxItems, - ItemIds). - -delete_item(NodeId, Publisher, PublishModel, ItemId) -> - node_hometree:delete_item(NodeId, Publisher, - PublishModel, ItemId). - -purge_node(NodeId, Owner) -> - node_hometree:purge_node(NodeId, Owner). - -get_entity_affiliations(Host, Owner) -> - node_hometree:get_entity_affiliations(Host, Owner). - -get_node_affiliations(NodeId) -> - node_hometree:get_node_affiliations(NodeId). - -get_affiliation(NodeId, Owner) -> - node_hometree:get_affiliation(NodeId, Owner). - -set_affiliation(NodeId, Owner, Affiliation) -> - node_hometree:set_affiliation(NodeId, Owner, - Affiliation). - -get_entity_subscriptions(Host, Owner) -> - node_hometree:get_entity_subscriptions(Host, Owner). - -get_node_subscriptions(NodeId) -> - node_hometree:get_node_subscriptions(NodeId). - -get_subscriptions(NodeId, Owner) -> - node_hometree:get_subscriptions(NodeId, Owner). - -set_subscriptions(NodeId, Owner, Subscription, SubId) -> - node_hometree:set_subscriptions(NodeId, Owner, - Subscription, SubId). - -get_pending_nodes(Host, Owner) -> - node_hometree:get_pending_nodes(Host, Owner). - -get_states(NodeId) -> node_hometree:get_states(NodeId). - -get_state(NodeId, JID) -> - node_hometree:get_state(NodeId, JID). - -set_state(State) -> node_hometree:set_state(State). - -get_items(NodeId, From) -> - node_hometree:get_items(NodeId, From). - -get_items(NodeId, JID, AccessModel, - PresenceSubscription, RosterGroup, SubId) -> - node_hometree:get_items(NodeId, JID, AccessModel, - PresenceSubscription, RosterGroup, SubId). - -get_item(NodeId, ItemId) -> - node_hometree:get_item(NodeId, ItemId). - -get_item(NodeId, ItemId, JID, AccessModel, - PresenceSubscription, RosterGroup, SubId) -> - node_hometree:get_item(NodeId, ItemId, JID, AccessModel, - PresenceSubscription, RosterGroup, SubId). - -set_item(Item) -> node_hometree:set_item(Item). - -get_item_name(Host, Node, Id) -> - node_hometree:get_item_name(Host, Node, Id). - -node_to_path(Node) -> node_flat:node_to_path(Node). - -path_to_node(Path) -> node_flat:path_to_node(Path). diff --git a/src/node_public.erl b/src/node_public.erl deleted file mode 100644 index cbb7baffb..000000000 --- a/src/node_public.erl +++ /dev/null @@ -1,186 +0,0 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. -%%% -%%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. -%%% -%%% -%%% The Initial Developer of the Original Code is ProcessOne. -%%% Portions created by ProcessOne are Copyright 2006-2015, ProcessOne -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2015, ProcessOne. -%%% -%%% -%%% @copyright 2006-2015 ProcessOne -%%% @author Christophe romain -%%% [http://www.process-one.net/] -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== - --module(node_public). - --author('christophe.romain@process-one.net'). - --include("pubsub.hrl"). - --include("jlib.hrl"). - --behaviour(gen_pubsub_node). - -%% Note on function definition -%% included is all defined plugin function -%% it's possible not to define some function at all -%% in that case, warning will be generated at compilation -%% and function call will fail, -%% then mod_pubsub will call function from node_hometree -%% (this makes code cleaner, but execution a little bit longer) - -%% API definition --export([init/3, terminate/2, options/0, features/0, - create_node_permission/6, create_node/2, delete_node/1, - purge_node/2, subscribe_node/8, unsubscribe_node/4, - publish_item/6, delete_item/4, remove_extra_items/3, - get_entity_affiliations/2, get_node_affiliations/1, - get_affiliation/2, set_affiliation/3, - get_entity_subscriptions/2, get_node_subscriptions/1, - get_subscriptions/2, set_subscriptions/4, - get_pending_nodes/2, get_states/1, get_state/2, - set_state/1, get_items/6, get_items/2, get_item/7, - get_item/2, set_item/1, get_item_name/3, node_to_path/1, - path_to_node/1]). - -init(Host, ServerHost, Opts) -> - node_hometree:init(Host, ServerHost, Opts). - -terminate(Host, ServerHost) -> - node_hometree:terminate(Host, ServerHost). - -options() -> - [{deliver_payloads, true}, {notify_config, false}, - {notify_delete, false}, {notify_retract, true}, - {purge_offline, false}, {persist_items, true}, - {max_items, ?MAXITEMS}, {subscribe, true}, - {access_model, open}, {roster_groups_allowed, []}, - {publish_model, publishers}, - {notification_type, headline}, - {max_payload_size, ?MAX_PAYLOAD_SIZE}, - {send_last_published_item, never}, - {deliver_notifications, true}, - {presence_based_delivery, false}]. - -features() -> - [<<"create-nodes">>, <<"delete-nodes">>, - <<"delete-items">>, <<"instant-nodes">>, - <<"outcast-affiliation">>, <<"persistent-items">>, - <<"publish">>, <<"purge-nodes">>, <<"retract-items">>, - <<"retrieve-affiliations">>, <<"retrieve-items">>, - <<"retrieve-subscriptions">>, <<"subscribe">>, - <<"subscription-notifications">>]. - -create_node_permission(Host, ServerHost, Node, - ParentNode, Owner, Access) -> - node_hometree:create_node_permission(Host, ServerHost, - Node, ParentNode, Owner, Access). - -create_node(NodeId, Owner) -> - node_hometree:create_node(NodeId, Owner). - -delete_node(Removed) -> - node_hometree:delete_node(Removed). - -subscribe_node(NodeId, Sender, Subscriber, AccessModel, - SendLast, PresenceSubscription, RosterGroup, Options) -> - node_hometree:subscribe_node(NodeId, Sender, Subscriber, - AccessModel, SendLast, PresenceSubscription, - RosterGroup, Options). - -unsubscribe_node(NodeId, Sender, Subscriber, SubID) -> - node_hometree:unsubscribe_node(NodeId, Sender, - Subscriber, SubID). - -publish_item(NodeId, Publisher, Model, MaxItems, ItemId, - Payload) -> - node_hometree:publish_item(NodeId, Publisher, Model, - MaxItems, ItemId, Payload). - -remove_extra_items(NodeId, MaxItems, ItemIds) -> - node_hometree:remove_extra_items(NodeId, MaxItems, - ItemIds). - -delete_item(NodeId, Publisher, PublishModel, ItemId) -> - node_hometree:delete_item(NodeId, Publisher, - PublishModel, ItemId). - -purge_node(NodeId, Owner) -> - node_hometree:purge_node(NodeId, Owner). - -get_entity_affiliations(Host, Owner) -> - node_hometree:get_entity_affiliations(Host, Owner). - -get_node_affiliations(NodeId) -> - node_hometree:get_node_affiliations(NodeId). - -get_affiliation(NodeId, Owner) -> - node_hometree:get_affiliation(NodeId, Owner). - -set_affiliation(NodeId, Owner, Affiliation) -> - node_hometree:set_affiliation(NodeId, Owner, - Affiliation). - -get_entity_subscriptions(Host, Owner) -> - node_hometree:get_entity_subscriptions(Host, Owner). - -get_node_subscriptions(NodeId) -> - node_hometree:get_node_subscriptions(NodeId). - -get_subscriptions(NodeId, Owner) -> - node_hometree:get_subscriptions(NodeId, Owner). - -set_subscriptions(NodeId, Owner, Subscription, SubId) -> - node_hometree:set_subscriptions(NodeId, Owner, - Subscription, SubId). - -get_pending_nodes(Host, Owner) -> - node_hometree:get_pending_nodes(Host, Owner). - -get_states(NodeId) -> node_hometree:get_states(NodeId). - -get_state(NodeId, JID) -> - node_hometree:get_state(NodeId, JID). - -set_state(State) -> node_hometree:set_state(State). - -get_items(NodeId, From) -> - node_hometree:get_items(NodeId, From). - -get_items(NodeId, JID, AccessModel, - PresenceSubscription, RosterGroup, SubId) -> - node_hometree:get_items(NodeId, JID, AccessModel, - PresenceSubscription, RosterGroup, SubId). - -get_item(NodeId, ItemId) -> - node_hometree:get_item(NodeId, ItemId). - -get_item(NodeId, ItemId, JID, AccessModel, - PresenceSubscription, RosterGroup, SubId) -> - node_hometree:get_item(NodeId, ItemId, JID, AccessModel, - PresenceSubscription, RosterGroup, SubId). - -set_item(Item) -> node_hometree:set_item(Item). - -%% @doc

    Return the name of the node if known: Default is to return -%% node id.

    -get_item_name(Host, Node, Id) -> - node_hometree:get_item_name(Host, Node, Id). - -node_to_path(Node) -> node_flat:node_to_path(Node). - -path_to_node(Path) -> node_flat:path_to_node(Path). diff --git a/src/nodetree_dag.erl b/src/nodetree_dag.erl deleted file mode 100644 index e8ad8b141..000000000 --- a/src/nodetree_dag.erl +++ /dev/null @@ -1,330 +0,0 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. -%%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. -%%% -%%% @author Brian Cully -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== - --module(nodetree_dag). - --author('bjc@kublai.com'). - -%% API --export([init/3, terminate/2, options/0, set_node/1, - get_node/3, get_node/2, get_node/1, get_nodes/2, - get_nodes/1, get_parentnodes/3, get_parentnodes_tree/3, - get_subnodes/3, get_subnodes_tree/3, create_node/6, - delete_node/2]). - --include_lib("stdlib/include/qlc.hrl"). - --include("ejabberd.hrl"). --include("logger.hrl"). - --include("pubsub.hrl"). - --include("jlib.hrl"). - --behaviour(gen_pubsub_nodetree). - --define(DEFAULT_NODETYPE, leaf). - --define(DEFAULT_PARENTS, []). - --define(DEFAULT_CHILDREN, []). - --compile(export_all). - -%%==================================================================== -%% API -%%==================================================================== -init(Host, ServerHost, Opts) -> - nodetree_tree:init(Host, ServerHost, Opts). - -terminate(Host, ServerHost) -> - nodetree_tree:terminate(Host, ServerHost). - --spec(create_node/6 :: -( - Key :: mod_pubsub:hostPubsub(), - NodeID :: mod_pubsub:nodeId(), - Type :: binary(), - Owner :: jid(), - Options :: mod_pubsub:nodeOptions(), - Parents :: [mod_pubsub:nodeId()]) - -> {ok, NodeIdx::mod_pubsub:nodeIdx()} - | {error, xmlel()} -). -create_node(Key, NodeID, Type, Owner, Options, Parents) -> - OwnerJID = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), - case find_node(Key, NodeID) of - false -> - NodeIdx = pubsub_index:new(node), - N = #pubsub_node{nodeid = oid(Key, NodeID), id = NodeIdx, - type = Type, parents = Parents, owners = [OwnerJID], - options = Options}, - case set_node(N) of - ok -> {ok, NodeIdx}; - Other -> Other - end; - _ -> {error, ?ERR_CONFLICT} - end. - --spec(set_node/1 :: -( - PubsubNode::mod_pubsub:pubsubNode()) - -> ok - %%% - | {error, xmlel()} -). -set_node(#pubsub_node{nodeid = {Key, _}, owners = Owners, options = Options} = - Node) -> - Parents = find_opt(collection, ?DEFAULT_PARENTS, Options), - case validate_parentage(Key, Owners, Parents) of - true -> - mnesia:write(Node#pubsub_node{parents = Parents}); - Other -> Other - end. - --spec(delete_node/2 :: -( - Key :: mod_pubsub:hostPubsub(), - NodeID :: mod_pubsub:nodeId()) - -> [mod_pubsub:pubsubNode(),...] - %%% - | {error, xmlel()} -). -delete_node(Key, NodeID) -> - case find_node(Key, NodeID) of - false -> {error, ?ERR_ITEM_NOT_FOUND}; - Node -> - lists:foreach(fun (#pubsub_node{options = Opts} = - Child) -> - NewOpts = remove_config_parent(NodeID, Opts), - Parents = find_opt(collection, ?DEFAULT_PARENTS, - NewOpts), - ok = mnesia:write(pubsub_node, - Child#pubsub_node{parents = - Parents, - options = - NewOpts}, - write) - end, - get_subnodes(Key, NodeID)), - pubsub_index:free(node, Node#pubsub_node.id), - mnesia:delete_object(pubsub_node, Node, write), - [Node] - end. - -options() -> nodetree_tree:options(). - -get_node(Host, NodeID, _From) -> get_node(Host, NodeID). - --spec(get_node/2 :: -( - Host :: mod_pubsub:hostPubsub(), - NodeID :: mod_pubsub:nodeId()) - -> mod_pubsub:pubsubNode() - %%% - | {error, xmlel} -). -get_node(Host, NodeID) -> - case find_node(Host, NodeID) of - false -> {error, ?ERR_ITEM_NOT_FOUND}; - Node -> Node - end. - --spec(get_node/1 :: -( - NodeIdx::mod_pubsub:nodeIdx()) - -> mod_pubsub:pubsubNode() - | {error, xmlel()} -). -get_node(NodeId) -> nodetree_tree:get_node(NodeId). - -get_nodes(Key, From) -> - nodetree_tree:get_nodes(Key, From). - --spec(get_nodes/1 :: -( - Host::mod_pubsub:host()) - -> [mod_pubsub:pubsubNode()] -). -get_nodes(Key) -> nodetree_tree:get_nodes(Key). - --spec(get_parentnodes/3 :: -( - Host :: mod_pubsub:hostPubsub(), - NodeID :: mod_pubsub:nodeId(), - _From :: _) - -> [mod_pubsub:pubsubNode()] - %%% - | {error, xmlel()} -). -get_parentnodes(Host, NodeID, _From) -> - case find_node(Host, NodeID) of - false -> {error, ?ERR_ITEM_NOT_FOUND}; - #pubsub_node{parents = Parents} -> - Q = qlc:q([N - || #pubsub_node{nodeid = {NHost, NNode}} = N - <- mnesia:table(pubsub_node), - Parent <- Parents, Host == NHost, Parent == NNode]), - qlc:e(Q) - end. - -get_parentnodes_tree(Host, NodeID, _From) -> - Pred = fun (NID, #pubsub_node{nodeid = {_, NNodeID}}) -> - NID == NNodeID - end, - Tr = fun (#pubsub_node{parents = Parents}) -> Parents - end, - traversal_helper(Pred, Tr, Host, [NodeID]). - -get_subnodes(Host, NodeID, _From) -> - get_subnodes(Host, NodeID). - --spec(get_subnodes/2 :: -( - Host :: mod_pubsub:hostPubsub(), - NodeId :: mod_pubsub:nodeId()) - -> [mod_pubsub:pubsubNode()] -). -get_subnodes(Host, <<>>) -> - get_subnodes_helper(Host, <<>>); -get_subnodes(Host, NodeID) -> - case find_node(Host, NodeID) of - false -> {error, ?ERR_ITEM_NOT_FOUND}; - _ -> get_subnodes_helper(Host, NodeID) - end. - --spec(get_subnodes_helper/2 :: -( - Host :: mod_pubsub:hostPubsub(), - NodeID :: mod_pubsub:nodeId()) - -> [mod_pubsub:pubsubNode()] -). -get_subnodes_helper(Host, NodeID) -> - Q = qlc:q([Node - || #pubsub_node{nodeid = {NHost, _}, - parents = Parents} = - Node - <- mnesia:table(pubsub_node), - Host == NHost, lists:member(NodeID, Parents)]), - qlc:e(Q). - -get_subnodes_tree(Host, NodeID, From) -> - Pred = fun (NID, #pubsub_node{parents = Parents}) -> - lists:member(NID, Parents) - end, - Tr = fun (#pubsub_node{nodeid = {_, N}}) -> [N] end, - traversal_helper(Pred, Tr, 1, Host, [NodeID], - [{0, [get_node(Host, NodeID, From)]}]). - -%%==================================================================== -%% Internal functions -%%==================================================================== -oid(Key, Name) -> {Key, Name}. - -%% Key = jlib:jid() | host() -%% NodeID = string() --spec(find_node/2 :: -( - Key :: mod_pubsub:hostPubsub(), - NodeID :: mod_pubsub:nodeId()) - -> mod_pubsub:pubsubNode() | false -). -find_node(Key, NodeID) -> - case mnesia:read(pubsub_node, oid(Key, NodeID), read) of - [] -> false; - [Node] -> Node - end. - -%% Key = jlib:jid() | host() -%% Default = term() -%% Options = [{Key = atom(), Value = term()}] -find_opt(Key, Default, Options) -> - case lists:keysearch(Key, 1, Options) of - {value, {Key, Val}} -> Val; - _ -> Default - end. - --spec(traversal_helper/4 :: -( - Pred :: fun(), - Tr :: fun(), - Host :: mod_pubsub:hostPubsub(), - NodeId :: [mod_pubsub:pubsubNode(),...]) - -> [{Depth::non_neg_integer(), Nodes::[mod_pubsub:pubsubNode(),...]}] -). - -traversal_helper(Pred, Tr, Host, NodeIDs) -> - traversal_helper(Pred, Tr, 0, Host, NodeIDs, []). - -traversal_helper(_Pred, _Tr, _Depth, _Host, [], Acc) -> - Acc; -traversal_helper(Pred, Tr, Depth, Host, NodeIDs, Acc) -> - Q = qlc:q([Node - || #pubsub_node{nodeid = {NHost, _}} = Node - <- mnesia:table(pubsub_node), - NodeID <- NodeIDs, Host == NHost, Pred(NodeID, Node)]), - Nodes = qlc:e(Q), - IDs = lists:flatmap(Tr, Nodes), - traversal_helper(Pred, Tr, Depth + 1, Host, IDs, - [{Depth, Nodes} | Acc]). - -remove_config_parent(NodeID, Options) -> - remove_config_parent(NodeID, Options, []). - -remove_config_parent(_NodeID, [], Acc) -> - lists:reverse(Acc); -remove_config_parent(NodeID, [{collection, Parents} | T], Acc) -> - remove_config_parent(NodeID, T, - [{collection, lists:delete(NodeID, Parents)} | Acc]); -remove_config_parent(NodeID, [H | T], Acc) -> - remove_config_parent(NodeID, T, [H | Acc]). - --spec(validate_parentage/3 :: -( - Key :: mod_pubsub:hostPubsub(), - Owners :: [ljid(),...], - Parent_NodeIds :: [mod_pubsub:nodeId()]) - -> true - %%% - | {error, xmlel()} -). -validate_parentage(_Key, _Owners, []) -> true; -validate_parentage(Key, Owners, [[] | T]) -> - validate_parentage(Key, Owners, T); -validate_parentage(Key, Owners, [<<>> | T]) -> - validate_parentage(Key, Owners, T); -validate_parentage(Key, Owners, [ParentID | T]) -> - case find_node(Key, ParentID) of - false -> {error, ?ERR_ITEM_NOT_FOUND}; - #pubsub_node{owners = POwners, options = POptions} -> - NodeType = find_opt(node_type, ?DEFAULT_NODETYPE, POptions), - MutualOwners = [O || O <- Owners, PO <- POwners, O == PO], - case {MutualOwners, NodeType} of - {[], _} -> {error, ?ERR_FORBIDDEN}; - {_, collection} -> validate_parentage(Key, Owners, T); - {_, _} -> {error, ?ERR_NOT_ALLOWED} - end - end. - -%% @spec (Host) -> jid() -%% Host = host() -%% @doc

    Generate pubsub service JID.

    -service_jid(Host) -> - case Host of - {U, S, _} -> jlib:make_jid(U, S, <<>>); - _ -> jlib:make_jid(<<>>, Host, <<>>) - end. diff --git a/src/nodetree_tree.erl b/src/nodetree_tree.erl index 7a9b46bcc..facb4fd74 100644 --- a/src/nodetree_tree.erl +++ b/src/nodetree_tree.erl @@ -1,29 +1,27 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. -%%% -%%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. -%%% -%%% -%%% The Initial Developer of the Original Code is ProcessOne. -%%% Portions created by ProcessOne are Copyright 2006-2015, ProcessOne -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2015, ProcessOne. +%%%---------------------------------------------------------------------- +%%% File : nodetree_tree.erl +%%% Author : Christophe Romain +%%% Purpose : Standard node tree plugin +%%% Created : 1 Dec 2007 by Christophe Romain %%% %%% -%%% @copyright 2006-2015 ProcessOne -%%% @author Christophe Romain -%%% [http://www.process-one.net/] -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== +%%% ejabberd, Copyright (C) 2002-2025 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. +%%% +%%%---------------------------------------------------------------------- %%% @doc The module {@module} is the default PubSub node tree plugin. %%%

    It is used as a default for all unknown PubSub node type. It can serve @@ -32,291 +30,230 @@ %%%

    PubSub node tree plugins are using the {@link gen_nodetree} behaviour.

    %%%

    The API isn't stabilized yet. The pubsub plugin %%% development is still a work in progress. However, the system is already -%%% useable and useful as is. Please, send us comments, feedback and +%%% usable and useful as is. Please, send us comments, feedback and %%% improvements.

    -module(nodetree_tree). - +-behaviour(gen_pubsub_nodetree). -author('christophe.romain@process-one.net'). -include_lib("stdlib/include/qlc.hrl"). +-include_lib("stdlib/include/ms_transform.hrl"). -include("pubsub.hrl"). - --include("jlib.hrl"). - --behaviour(gen_pubsub_nodetree). +-include_lib("xmpp/include/xmpp.hrl"). +-include("translate.hrl"). -export([init/3, terminate/2, options/0, set_node/1, - get_node/3, get_node/2, get_node/1, get_nodes/2, - get_nodes/1, get_parentnodes/3, get_parentnodes_tree/3, - get_subnodes/3, get_subnodes_tree/3, create_node/6, - delete_node/2]). + get_node/3, get_node/2, get_node/1, get_nodes/2, + get_nodes/1, get_all_nodes/1, + get_parentnodes/3, get_parentnodes_tree/3, + get_subnodes/3, get_subnodes_tree/3, create_node/6, + delete_node/2]). -%% ================ -%% API definition -%% ================ - -%% @spec (Host, ServerHost, Options) -> ok -%% Host = string() -%% ServerHost = string() -%% Options = [{atom(), term()}] -%% @doc

    Called during pubsub modules initialisation. Any pubsub plugin must -%% implement this function. It can return anything.

    -%%

    This function is mainly used to trigger the setup task necessary for the -%% plugin. It can be used for example by the developer to create the specific -%% module database schema if it does not exists yet.

    init(_Host, _ServerHost, _Options) -> - mnesia:create_table(pubsub_node, - [{disc_copies, [node()]}, - {attributes, record_info(fields, pubsub_node)}]), - mnesia:add_table_index(pubsub_node, id), - NodesFields = record_info(fields, pubsub_node), - case mnesia:table_info(pubsub_node, attributes) of - NodesFields -> ok; - _ -> ok - end, + ejabberd_mnesia:create(?MODULE, pubsub_node, + [{disc_copies, [node()]}, + {attributes, record_info(fields, pubsub_node)}, + {index, [id]}]), %% mnesia:transform_table(pubsub_state, ignore, StatesFields) ok. -%% @spec (Host, ServerHost) -> ok -%% Host = string() -%% ServerHost = string() -%% @spec () -> Options -%% Options = [mod_pubsub:nodeOption()] -%% @doc Returns the default pubsub node tree options. -terminate(_Host, _ServerHost) -> ok. +terminate(_Host, _ServerHost) -> + ok. -options() -> [{virtual_tree, false}]. +options() -> + [{virtual_tree, false}]. -%% @spec (Node) -> ok | {error, Reason} -%% Node = mod_pubsub:pubsubNode() -%% Reason = mod_pubsub:stanzaError() --spec(set_node/1 :: -( - Node::mod_pubsub:pubsubNode()) - -> ok -). set_node(Node) when is_record(Node, pubsub_node) -> mnesia:write(Node). -%set_node(_) -> {error, ?ERR_INTERNAL_SERVER_ERROR}. -get_node(Host, Node, _From) -> get_node(Host, Node). +get_node(Host, Node, _From) -> + get_node(Host, Node). -%% @spec (Host, NodeId) -> Node | {error, Reason} -%% Host = mod_pubsub:host() -%% NodeId = mod_pubsub:nodeId() -%% Node = mod_pubsub:pubsubNode() -%% Reason = mod_pubsub:stanzaError() --spec(get_node/2 :: -( - Host :: mod_pubsub:host(), - NodeId :: mod_pubsub:nodeId()) - -> mod_pubsub:pubsubNode() - | {error, xmlel()} -). -get_node(Host, NodeId) -> - case catch mnesia:read({pubsub_node, {Host, NodeId}}) of - [Record] when is_record(Record, pubsub_node) -> Record; - [] -> {error, ?ERR_ITEM_NOT_FOUND} -% Error -> Error +get_node(Host, Node) -> + case mnesia:read({pubsub_node, {Host, Node}}) of + [#pubsub_node{} = Record] -> fixup_node(Record); + _ -> {error, xmpp:err_item_not_found(?T("Node not found"), ejabberd_option:language())} end. --spec(get_node/1 :: -( - NodeIdx::mod_pubsub:nodeIdx()) - -> mod_pubsub:pubsubNode() - | {error, xmlel()} -). -get_node(NodeIdx) -> - case catch mnesia:index_read(pubsub_node, NodeIdx, #pubsub_node.id) of - [Record] when is_record(Record, pubsub_node) -> Record; - [] -> {error, ?ERR_ITEM_NOT_FOUND} -% Error -> Error +get_node(Nidx) -> + case mnesia:index_read(pubsub_node, Nidx, #pubsub_node.id) of + [#pubsub_node{} = Record] -> fixup_node(Record); + _ -> {error, xmpp:err_item_not_found(?T("Node not found"), ejabberd_option:language())} end. -get_nodes(Host, _From) -> get_nodes(Host). - -%% @spec (Host) -> Nodes | {error, Reason} -%% Host = mod_pubsub:host() -%% Nodes = [mod_pubsub:pubsubNode()] -%% Reason = {aborted, atom()} --spec(get_nodes/1 :: -( - Host::mod_pubsub:host()) - -> [mod_pubsub:pubsubNode()] -). get_nodes(Host) -> - mnesia:match_object(#pubsub_node{nodeid = {Host, '_'}, _ = '_'}). + get_nodes(Host, infinity). -%% @spec (Host, Node, From) -> [] -%% Host = mod_pubsub:host() -%% NodeId = mod_pubsub:nodeId() -%% From = mod_pubsub:jid() -%% @doc

    Default node tree does not handle parents, return empty list.

    -get_parentnodes(_Host, _NodeId, _From) -> []. - -%% @spec (Host, NodeId, From) -> [{Depth, Node}] | [] -%% Host = mod_pubsub:host() -%% NodeId = mod_pubsub:nodeId() -%% From = mod_pubsub:jid() -%% Depth = integer() -%% Node = mod_pubsub:pubsubNode() -%% @doc

    Default node tree does not handle parents, return a list -%% containing just this node.

    --spec(get_parentnodes_tree/3 :: -( - Host :: mod_pubsub:host(), - NodeId :: mod_pubsub:nodeId(), - From :: jid()) - -> [{0, [mod_pubsub:pubsubNode(),...]}] -). -get_parentnodes_tree(Host, NodeId, From) -> - case get_node(Host, NodeId, From) of - Node when is_record(Node, pubsub_node) -> [{0, [Node]}]; - _Error -> [] +get_nodes(Host, infinity) -> + Nodes = mnesia:match_object(#pubsub_node{nodeid = {Host, '_'}, _ = '_'}), + [fixup_node(N) || N <- Nodes]; +get_nodes(Host, Limit) -> + case mnesia:select( + pubsub_node, + ets:fun2ms( + fun(#pubsub_node{nodeid = {H, _}} = Node) when H == Host -> + Node + end), Limit, read) of + '$end_of_table' -> []; + {Nodes, _} -> [fixup_node(N) || N <- Nodes] end. -%% @spec (Host, NodeId, From) -> Nodes -%% Host = mod_pubsub:host() -%% NodeId = mod_pubsub:nodeId() -%% From = mod_pubsub:jid() -%% Nodes = [mod_pubsub:pubsubNode()] -get_subnodes(Host, NodeId, _From) -> - get_subnodes(Host, NodeId). +get_all_nodes({_U, _S, _R} = Owner) -> + Host = jid:tolower(jid:remove_resource(Owner)), + Nodes = mnesia:match_object(#pubsub_node{nodeid = {Host, '_'}, _ = '_'}), + [fixup_node(N) || N <- Nodes]; +get_all_nodes(Host) -> + Nodes = mnesia:match_object(#pubsub_node{nodeid = {Host, '_'}, _ = '_'}) + ++ mnesia:match_object(#pubsub_node{nodeid = {{'_', Host, '_'}, '_'}, + _ = '_'}), + [fixup_node(N) || N <- Nodes]. --spec(get_subnodes/2 :: -( - Host :: mod_pubsub:host(), - NodeId :: mod_pubsub:nodeId()) - -> [mod_pubsub:pubsubNode()] -). -get_subnodes(Host, <<>>) -> - Q = qlc:q([N - || #pubsub_node{nodeid = {NHost, _}, - parents = Parents} = - N - <- mnesia:table(pubsub_node), - Host == NHost, Parents == []]), +get_parentnodes(Host, Node, _From) -> + case catch mnesia:read({pubsub_node, {Host, Node}}) of + [Record] when is_record(Record, pubsub_node) -> + Record#pubsub_node.parents; + _ -> + [] + end. + +get_parentnodes_tree(Host, Node, _From) -> + get_parentnodes_tree(Host, Node, 0, []). +get_parentnodes_tree(Host, Node, Level, Acc) -> + case catch mnesia:read({pubsub_node, {Host, Node}}) of + [#pubsub_node{} = Record0] -> + Record = fixup_node(Record0), + Tree = [{Level, [Record]}|Acc], + case Record#pubsub_node.parents of + [Parent] -> get_parentnodes_tree(Host, Parent, Level+1, Tree); + _ -> Tree + end; + _ -> + Acc + end. + +get_subnodes(Host, <<>>, infinity) -> + Nodes = mnesia:match_object(#pubsub_node{nodeid = {Host, '_'}, parents = [], _ = '_'}), + [fixup_node(N) || N <- Nodes]; +get_subnodes(Host, <<>>, Limit) -> + case mnesia:select( + pubsub_node, + ets:fun2ms( + fun(#pubsub_node{nodeid = {H, _}, parents = []} = Node) when H == Host -> + Node + end), Limit, read) of + '$end_of_table' -> []; + {Nodes, _} -> [fixup_node(N) || N <- Nodes] + end; +get_subnodes(Host, Node, infinity) -> + Q = qlc:q([fixup_node(N) + || #pubsub_node{nodeid = {NHost, _}, + parents = Parents} = + N + <- mnesia:table(pubsub_node), + Host == NHost, lists:member(Node, Parents)]), qlc:e(Q); -get_subnodes(Host, Node) -> - Q = qlc:q([N - || #pubsub_node{nodeid = {NHost, _}, - parents = Parents} = - N - <- mnesia:table(pubsub_node), - Host == NHost, lists:member(Node, Parents)]), - qlc:e(Q). +get_subnodes(Host, Node, Limit) -> + case mnesia:select( + pubsub_node, + ets:fun2ms( + fun(#pubsub_node{nodeid = {H, _}, parents = Ps} = N) + when H == Host andalso Ps /= [] -> N + end), Limit, read) of + '$end_of_table' -> []; + {Nodes, _} -> + lists:filtermap( + fun(#pubsub_node{parents = Parents} = N2) -> + case lists:member(Node, Parents) of + true -> {true, fixup_node(N2)}; + _ -> false + end + end, Nodes) + end. get_subnodes_tree(Host, Node, _From) -> get_subnodes_tree(Host, Node). -%% @spec (Host, NodeId) -> Nodes -%% Host = mod_pubsub:host() -%% NodeId = mod_pubsub:nodeId() -%% Nodes = [] | [mod_pubsub:pubsubNode()] --spec(get_subnodes_tree/2 :: -( - Host :: mod_pubsub:host(), - NodeId :: mod_pubsub:nodeId()) - -> [mod_pubsub:pubsubNode()] -). -get_subnodes_tree(Host, NodeId) -> - case get_node(Host, NodeId) of - {error, _} -> []; - Rec -> - BasePlugin = jlib:binary_to_atom(<<"node_", - (Rec#pubsub_node.type)/binary>>), - BasePath = BasePlugin:node_to_path(NodeId), - mnesia:foldl(fun (#pubsub_node{nodeid = {H, N}} = R, - Acc) -> - Plugin = jlib:binary_to_atom(<<"node_", - (R#pubsub_node.type)/binary>>), - Path = Plugin:node_to_path(N), - case lists:prefix(BasePath, Path) and (H == Host) of - true -> [R | Acc]; - false -> Acc - end - end, - [], pubsub_node) +get_subnodes_tree(Host, Node) -> + case get_node(Host, Node) of + {error, _} -> + []; + Rec -> + BasePlugin = misc:binary_to_atom(<<"node_", + (Rec#pubsub_node.type)/binary>>), + {result, BasePath} = BasePlugin:node_to_path(Node), + mnesia:foldl(fun (#pubsub_node{nodeid = {H, N}} = R, Acc) -> + Plugin = misc:binary_to_atom(<<"node_", + (R#pubsub_node.type)/binary>>), + {result, Path} = Plugin:node_to_path(N), + case lists:prefix(BasePath, Path) and (H == Host) of + true -> [R | Acc]; + false -> Acc + end + end, + [], pubsub_node) end. -%% @spec (Host, NodeId, Type, Owner, Options, Parents) -> -%% {ok, NodeIdx} | {error, Reason} -%% Host = mod_pubsub:host() -%% NodeId = mod_pubsub:nodeId() -%% Type = mod_pubsub:nodeType() -%% Owner = mod_pubsub:jid() -%% Options = [mod_pubsub:nodeOption()] -%% Parents = [] | [mod_pubsub:nodeId()] -%% NodeIdx = mod_pubsub:nodeIdx() -%% Reason = mod_pubsub:stanzaError() --spec(create_node/6 :: -( - Host :: mod_pubsub:host(), - NodeId :: mod_pubsub:nodeId(), - Type :: binary(), - Owner :: jid(), - Options :: mod_pubsub:nodeOptions(), - Parents :: [mod_pubsub:nodeId()]) - -> {ok, NodeIdx::mod_pubsub:nodeIdx()} - %%% - | {error, xmlel()} -). -create_node(Host, NodeId, Type, Owner, Options, Parents) -> - BJID = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), - case catch mnesia:read({pubsub_node, {Host, NodeId}}) of - [] -> - ParentExists = case Host of - {_U, _S, _R} -> - %% This is special case for PEP handling - %% PEP does not uses hierarchy - true; - _ -> - case Parents of - [] -> true; - [Parent | _] -> - case catch mnesia:read({pubsub_node, - {Host, Parent}}) - of - [#pubsub_node{owners = - [{[], Host, []}]}] -> - true; - [#pubsub_node{owners = Owners}] -> - lists:member(BJID, Owners); - _ -> false - end; - _ -> false - end - end, - case ParentExists of - true -> - NodeIdx = pubsub_index:new(node), - mnesia:write(#pubsub_node{nodeid = {Host, NodeId}, - id = NodeIdx, parents = Parents, - type = Type, owners = [BJID], - options = Options}), - {ok, NodeIdx}; - false -> {error, ?ERR_FORBIDDEN} - end; - _ -> {error, ?ERR_CONFLICT} +create_node(Host, Node, Type, Owner, Options, Parents) -> + BJID = jid:tolower(jid:remove_resource(Owner)), + case mnesia:read({pubsub_node, {Host, Node}}) of + [] -> + ParentExists = case Host of + {_U, _S, _R} -> + %% This is special case for PEP handling + %% PEP does not uses hierarchy + true; + _ -> + case Parents of + [] -> + true; + [Parent | _] -> + case catch mnesia:read({pubsub_node, {Host, Parent}}) of + [#pubsub_node{owners = [{<<>>, Host, <<>>}]}] -> + true; + [#pubsub_node{owners = Owners}] -> + lists:member(BJID, Owners); + _ -> + false + end; + _ -> + false + end + end, + case ParentExists of + true -> + Nidx = pubsub_index:new(node), + mnesia:write(#pubsub_node{nodeid = {Host, Node}, + id = Nidx, parents = Parents, + type = Type, owners = [BJID], + options = Options}), + {ok, Nidx}; + false -> + {error, xmpp:err_forbidden()} + end; + _ -> + {error, xmpp:err_conflict(?T("Node already exists"), ejabberd_option:language())} end. -%% @spec (Host, NodeId) -> Removed -%% Host = mod_pubsub:host() -%% NodeId = mod_pubsub:nodeId() -%% Removed = [mod_pubsub:pubsubNode()] --spec(delete_node/2 :: -( - Host :: mod_pubsub:host(), - NodeId :: mod_pubsub:nodeId()) - -> [mod_pubsub:pubsubNode(),...] -). -delete_node(Host, NodeId) -> - Removed = get_subnodes_tree(Host, NodeId), - lists:foreach(fun (#pubsub_node{nodeid = {_, SubNodeId}, id = SubNodeIdx}) -> - pubsub_index:free(node, SubNodeIdx), - mnesia:delete({pubsub_node, {Host, SubNodeId}}) - end, - Removed), +delete_node(Host, Node) -> + Removed = get_subnodes_tree(Host, Node), + lists:foreach(fun (#pubsub_node{nodeid = {_, SubNode}, id = SubNidx}) -> + pubsub_index:free(node, SubNidx), + mnesia:delete({pubsub_node, {Host, SubNode}}) + end, + Removed), Removed. + +fixup_node(#pubsub_node{options = Options} = Node) -> + Res = lists:splitwith( + fun({max_items, infinity}) -> false; + (_) -> true + end, Options), + Options2 = case Res of + {Before, [_ | After]} -> + Before ++ [{max_items, max} | After]; + {Rest, []} -> + Rest + end, + Node#pubsub_node{options = Options2}. diff --git a/src/nodetree_tree_odbc.erl b/src/nodetree_tree_odbc.erl deleted file mode 100644 index eb966109b..000000000 --- a/src/nodetree_tree_odbc.erl +++ /dev/null @@ -1,472 +0,0 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. -%%% -%%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. -%%% -%%% -%%% The Initial Developer of the Original Code is ProcessOne. -%%% Portions created by ProcessOne are Copyright 2006-2015, ProcessOne -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2015, ProcessOne. -%%% -%%% -%%% @copyright 2006-2015 ProcessOne -%%% @author Christophe Romain -%%% [http://www.process-one.net/] -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== - -%%% @doc The module {@module} is the default PubSub node tree plugin. -%%%

    It is used as a default for all unknown PubSub node type. It can serve -%%% as a developer basis and reference to build its own custom pubsub node tree -%%% types.

    -%%%

    PubSub node tree plugins are using the {@link gen_nodetree} behaviour.

    -%%%

    The API isn't stabilized yet. The pubsub plugin -%%% development is still a work in progress. However, the system is already -%%% useable and useful as is. Please, send us comments, feedback and -%%% improvements.

    - --module(nodetree_tree_odbc). - --author('christophe.romain@process-one.net'). - --include("pubsub.hrl"). - --include("jlib.hrl"). - --define(PUBSUB, mod_pubsub_odbc). - --define(PLUGIN_PREFIX, <<"node_">>). --define(ODBC_SUFFIX, <<"_odbc">>). - --behaviour(gen_pubsub_nodetree). - --export([init/3, terminate/2, options/0, set_node/1, - get_node/3, get_node/2, get_node/1, get_nodes/2, - get_nodes/1, get_parentnodes/3, get_parentnodes_tree/3, - get_subnodes/3, get_subnodes_tree/3, create_node/6, - delete_node/2]). - --export([raw_to_node/2]). - -%% ================ -%% API definition -%% ================ - -%% @spec (Host, ServerHost, Opts) -> any() -%% Host = mod_pubsub:host() -%% ServerHost = host() -%% Opts = list() -%% @doc

    Called during pubsub modules initialisation. Any pubsub plugin must -%% implement this function. It can return anything.

    -%%

    This function is mainly used to trigger the setup task necessary for the -%% plugin. It can be used for example by the developer to create the specific -%% module database schema if it does not exists yet.

    -%% @spec () -> [Option] -%% Option = mod_pubsub:nodetreeOption() -%% @doc Returns the default pubsub node tree options. -%% @spec (Host, Node, From) -> pubsubNode() | {error, Reason} -%% Host = mod_pubsub:host() -%% Node = mod_pubsub:pubsubNode() -init(_Host, _ServerHost, _Opts) -> ok. - -terminate(_Host, _ServerHost) -> ok. - -options() -> [{virtual_tree, false}, {odbc, true}]. - -get_node(Host, Node, _From) -> get_node(Host, Node). - --spec(get_node/2 :: -( - Host :: mod_pubsub:host(), - NodeId :: mod_pubsub:nodeId()) - -> mod_pubsub:pubsubNode() - | {error, _} -). -get_node(Host, Node) -> - H = (?PUBSUB):escape(Host), - N = (?PUBSUB):escape(Node), - case catch - ejabberd_odbc:sql_query_t([<<"select node, parent, type, nodeid from " - "pubsub_node where host='">>, - H, <<"' and node='">>, N, <<"';">>]) - of - {selected, - [<<"node">>, <<"parent">>, <<"type">>, <<"nodeid">>], - [RItem]} -> - raw_to_node(Host, RItem); - {'EXIT', _Reason} -> - {error, ?ERR_INTERNAL_SERVER_ERROR}; - _ -> {error, ?ERR_ITEM_NOT_FOUND} - end. - --spec(get_node/1 :: -( - NodeIdx::mod_pubsub:nodeIdx()) - -> mod_pubsub:pubsubNode() - | {error, _} -). -get_node(NodeIdx) -> - case catch - ejabberd_odbc:sql_query_t([<<"select host, node, parent, type from " - "pubsub_node where nodeid='">>, - NodeIdx, <<"';">>]) - of - {selected, - [<<"host">>, <<"node">>, <<"parent">>, <<"type">>], - [[Host, Node, Parent, Type]]} -> - raw_to_node(Host, [Node, Parent, Type, NodeIdx]); - {'EXIT', _Reason} -> - {error, ?ERR_INTERNAL_SERVER_ERROR}; - _ -> {error, ?ERR_ITEM_NOT_FOUND} - end. - -%% @spec (Host, From) -> [pubsubNode()] | {error, Reason} -%% Host = mod_pubsub:host() | mod_pubsub:jid() -get_nodes(Host, _From) -> get_nodes(Host). - --spec(get_nodes/1 :: -( - Host::mod_pubsub:host()) - -> [mod_pubsub:pubsubNode()] -). -get_nodes(Host) -> - H = (?PUBSUB):escape(Host), - case catch - ejabberd_odbc:sql_query_t([<<"select node, parent, type, nodeid from " - "pubsub_node where host='">>, - H, <<"';">>]) - of - {selected, - [<<"node">>, <<"parent">>, <<"type">>, <<"nodeid">>], - RItems} -> - lists:map(fun (Item) -> raw_to_node(Host, Item) end, - RItems); - _ -> [] - end. - -%% @spec (Host, Node, From) -> [{Depth, Record}] | {error, Reason} -%% Host = mod_pubsub:host() | mod_pubsub:jid() -%% Node = mod_pubsub:pubsubNode() -%% From = mod_pubsub:jid() -%% Depth = integer() -%% Record = pubsubNode() -%% @doc

    Default node tree does not handle parents, return empty list.

    -%% @spec (Host, Node, From) -> [{Depth, Record}] | {error, Reason} -%% Host = mod_pubsub:host() | mod_pubsub:jid() -%% Node = mod_pubsub:pubsubNode() -%% From = mod_pubsub:jid() -%% Depth = integer() -%% Record = pubsubNode() -%% @doc

    Default node tree does not handle parents, return a list -%% containing just this node.

    -get_parentnodes(_Host, _Node, _From) -> []. - --spec(get_parentnodes_tree/3 :: -( - Host :: mod_pubsub:host(), - NodeId :: mod_pubsub:nodeId(), - From :: jid()) - -> [{0, [mod_pubsub:pubsubNode(),...]}] -). - -get_parentnodes_tree(Host, Node, From) -> - case get_node(Host, Node, From) of - N when is_record(N, pubsub_node) -> [{0, [N]}]; - _Error -> [] - end. - -get_subnodes(Host, Node, _From) -> - get_subnodes(Host, Node). - -%% @spec (Host, Index) -> [pubsubNode()] | {error, Reason} -%% Host = mod_pubsub:host() -%% Node = mod_pubsub:pubsubNode() --spec(get_subnodes/2 :: -( - Host :: mod_pubsub:host(), - NodeId :: mod_pubsub:nodeId()) - -> [mod_pubsub:pubsubNode()] -). -get_subnodes(Host, Node) -> - H = (?PUBSUB):escape(Host), - N = (?PUBSUB):escape(Node), - case catch - ejabberd_odbc:sql_query_t([<<"select node, parent, type, nodeid from " - "pubsub_node where host='">>, - H, <<"' and parent='">>, N, <<"';">>]) - of - {selected, - [<<"node">>, <<"parent">>, <<"type">>, <<"nodeid">>], - RItems} -> - lists:map(fun (Item) -> raw_to_node(Host, Item) end, - RItems); - _ -> [] - end. - -get_subnodes_tree(Host, Node, _From) -> - get_subnodes_tree(Host, Node). - -%% @spec (Host, Index) -> [pubsubNode()] | {error, Reason} -%% Host = mod_pubsub:host() -%% Node = mod_pubsub:pubsubNode() --spec(get_subnodes_tree/2 :: -( - Host :: mod_pubsub:host(), - NodeId :: mod_pubsub:nodeId()) - -> [mod_pubsub:pubsubNode()] -). - -get_subnodes_tree(Host, Node) -> - H = (?PUBSUB):escape(Host), - N = (?PUBSUB):escape(Node), - case catch - ejabberd_odbc:sql_query_t([<<"select node, parent, type, nodeid from " - "pubsub_node where host='">>, - H, <<"' and node like '">>, N, <<"%';">>]) - of - {selected, - [<<"node">>, <<"parent">>, <<"type">>, <<"nodeid">>], - RItems} -> - lists:map(fun (Item) -> raw_to_node(Host, Item) end, - RItems); - _ -> [] - end. - -%% @spec (Host, Node, Type, Owner, Options, Parents) -> ok | {error, Reason} -%% Host = mod_pubsub:host() | mod_pubsub:jid() -%% Node = mod_pubsub:pubsubNode() -%% NodeType = mod_pubsub:nodeType() -%% Owner = mod_pubsub:jid() -%% Options = list() -%% Parents = list() - --spec(create_node/6 :: -( - Host :: mod_pubsub:host(), - NodeId :: mod_pubsub:nodeId(), - Type :: binary(), - Owner :: jid(), - Options :: mod_pubsub:nodeOptions(), - Parents :: [mod_pubsub:nodeId()]) - -> {ok, NodeIdx::mod_pubsub:nodeIdx()} - %%% - | {error, _} -). - -create_node(Host, Node, Type, Owner, Options, Parents) -> - BJID = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), - case nodeid(Host, Node) of - {error, ?ERR_ITEM_NOT_FOUND} -> - ParentExists = case Host of - {_U, _S, _R} -> - %% This is special case for PEP handling - %% PEP does not uses hierarchy - true; - _ -> - case Parents of - [] -> true; - [Parent | _] -> - case nodeid(Host, Parent) of - {result, PNodeId} -> - case nodeowners(PNodeId) of - [{<<>>, Host, <<>>}] -> true; - Owners -> - lists:member(BJID, Owners) - end; - _ -> false - end; - _ -> false - end - end, - case ParentExists of - true -> - case set_node(#pubsub_node{nodeid = {Host, Node}, - parents = Parents, type = Type, - options = Options}) - of - {result, NodeId} -> {ok, NodeId}; - Other -> Other - end; - false -> {error, ?ERR_FORBIDDEN} - end; - {result, _} -> {error, ?ERR_CONFLICT}; - Error -> Error - end. - -%% @spec (Host, Node) -> [mod_pubsub:node()] -%% Host = mod_pubsub:host() | mod_pubsub:jid() -%% Node = mod_pubsub:pubsubNode() --spec(delete_node/2 :: -( - Host :: mod_pubsub:host(), - NodeId :: mod_pubsub:nodeId()) - -> [mod_pubsub:pubsubNode()] -). - -delete_node(Host, Node) -> - H = (?PUBSUB):escape(Host), - N = (?PUBSUB):escape(Node), - Removed = get_subnodes_tree(Host, Node), - catch - ejabberd_odbc:sql_query_t([<<"delete from pubsub_node where host='">>, - H, <<"' and node like '">>, N, <<"%';">>]), - Removed. - -%% helpers --spec(raw_to_node/2 :: -( - Host :: mod_pubsub:host(), - _ :: {NodeId::mod_pubsub:nodeId(), - Parent::mod_pubsub:nodeId(), - Type::binary(), - NodeIdx::mod_pubsub:nodeIdx()}) - -> mod_pubsub:pubsubNode() -). -raw_to_node(Host, [Node, Parent, Type, NodeIdx]) -> - Options = case catch - ejabberd_odbc:sql_query_t([<<"select name,val from pubsub_node_option " - "where nodeid='">>, - NodeIdx, <<"';">>]) - of - {selected, [<<"name">>, <<"val">>], ROptions} -> - DbOpts = lists:map(fun ([Key, Value]) -> - RKey = - jlib:binary_to_atom(Key), - Tokens = element(2, - erl_scan:string( - binary_to_list(<>))), - RValue = element(2, - erl_parse:parse_term(Tokens)), - {RKey, RValue} - end, - ROptions), - Module = - jlib:binary_to_atom(<<(?PLUGIN_PREFIX)/binary, - Type/binary, - (?ODBC_SUFFIX)/binary>>), - StdOpts = Module:options(), - lists:foldl(fun ({Key, Value}, Acc) -> - lists:keyreplace(Key, 1, Acc, - {Key, Value}) - end, - StdOpts, DbOpts); - _ -> [] - end, - Parents = case Parent of - <<>> -> []; - _ -> [Parent] - end, - #pubsub_node{nodeid = - {Host, Node}, - parents = Parents, - id = NodeIdx, type = Type, options = Options}. - -% @spec (NodeRecord) -> ok | {error, Reason} -%% Record = mod_pubsub:pubsub_node() --spec(set_node/1 :: -( - Record::mod_pubsub:pubsubNode()) - -> {result, NodeIdx::mod_pubsub:nodeIdx()} - %%% - | {error, _} -). -set_node(Record) -> - {Host, Node} = Record#pubsub_node.nodeid, - Parent = case Record#pubsub_node.parents of - [] -> <<>>; - [First | _] -> First - end, - Type = Record#pubsub_node.type, - H = (?PUBSUB):escape(Host), - N = (?PUBSUB):escape(Node), - P = (?PUBSUB):escape(Parent), - NodeIdx = case nodeid(Host, Node) of - {result, OldNodeIdx} -> - catch - ejabberd_odbc:sql_query_t([<<"delete from pubsub_node_option where " - "nodeid='">>, - OldNodeIdx, <<"';">>]), - catch - ejabberd_odbc:sql_query_t([<<"update pubsub_node set host='">>, - H, <<"' node='">>, N, - <<"' parent='">>, P, - <<"' type='">>, Type, - <<"' where nodeid='">>, - OldNodeIdx, <<"';">>]), - OldNodeIdx; - _ -> - catch - ejabberd_odbc:sql_query_t([<<"insert into pubsub_node(host, node, " - "parent, type) values('">>, - H, <<"', '">>, N, <<"', '">>, P, - <<"', '">>, Type, <<"');">>]), - case nodeid(Host, Node) of - {result, NewNodeIdx} -> NewNodeIdx; - _ -> none % this should not happen - end - end, - case NodeIdx of - none -> {error, ?ERR_INTERNAL_SERVER_ERROR}; - _ -> - lists:foreach(fun ({Key, Value}) -> - SKey = iolist_to_binary(atom_to_list(Key)), - SValue = - (?PUBSUB):escape(list_to_binary(lists:flatten(io_lib:fwrite("~p", - [Value])))), - catch - ejabberd_odbc:sql_query_t([<<"insert into pubsub_node_option(nodeid, " - "name, val) values('">>, - NodeIdx, <<"', '">>, - SKey, <<"', '">>, - SValue, <<"');">>]) - end, - Record#pubsub_node.options), - {result, NodeIdx} - end. - --spec(nodeid/2 :: -( - Host :: mod_pubsub:host(), - NodeId :: mod_pubsub:nodeId()) - -> {result, NodeIdx::mod_pubsub:nodeIdx()} - %%% - | {error, _} -). - -nodeid(Host, NodeId) -> - H = (?PUBSUB):escape(Host), - N = (?PUBSUB):escape(NodeId), - case catch - ejabberd_odbc:sql_query_t([<<"select nodeid from pubsub_node where " - "host='">>, - H, <<"' and node='">>, N, <<"';">>]) - of - {selected, [<<"nodeid">>], [[NodeIdx]]} -> - {result, NodeIdx}; - {'EXIT', _Reason} -> - {error, ?ERR_INTERNAL_SERVER_ERROR}; - _ -> {error, ?ERR_ITEM_NOT_FOUND} - end. - --spec(nodeowners/1 :: -( - NodeIdx::mod_pubsub:nodeIdx()) - -> Node_Owners::[ljid()] -). - -nodeowners(NodeIdx) -> - {result, Res} = node_hometree_odbc:get_node_affiliations(NodeIdx), - lists:foldl(fun ({LJID, owner}, Acc) -> [LJID | Acc]; - (_, Acc) -> Acc - end, - [], Res). diff --git a/src/nodetree_tree_sql.erl b/src/nodetree_tree_sql.erl new file mode 100644 index 000000000..09959099e --- /dev/null +++ b/src/nodetree_tree_sql.erl @@ -0,0 +1,379 @@ +%%%---------------------------------------------------------------------- +%%% File : nodetree_tree_sql.erl +%%% Author : Christophe Romain +%%% Purpose : Standard node tree plugin with ODBC backend +%%% Created : 1 Dec 2007 by Christophe Romain +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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. +%%% +%%%---------------------------------------------------------------------- + +%%% @doc The module {@module} is the default PubSub node tree plugin. +%%%

    It is used as a default for all unknown PubSub node type. It can serve +%%% as a developer basis and reference to build its own custom pubsub node tree +%%% types.

    +%%%

    PubSub node tree plugins are using the {@link gen_nodetree} behaviour.

    +%%%

    The API isn't stabilized yet. The pubsub plugin +%%% development is still a work in progress. However, the system is already +%%% usable and useful as is. Please, send us comments, feedback and +%%% improvements.

    + +-module(nodetree_tree_sql). +-behaviour(gen_pubsub_nodetree). +-author('christophe.romain@process-one.net'). + + +-include("pubsub.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). +-include("ejabberd_sql_pt.hrl"). +-include("translate.hrl"). + +-export([init/3, terminate/2, options/0, set_node/1, + get_node/3, get_node/2, get_node/1, get_nodes/2, + get_nodes/1, get_all_nodes/1, + get_parentnodes/3, get_parentnodes_tree/3, + get_subnodes/3, get_subnodes_tree/3, create_node/6, + delete_node/2]). + +-export([raw_to_node/2]). + +init(_Host, _ServerHost, _Opts) -> + ok. + +terminate(_Host, _ServerHost) -> + ok. + +options() -> + [{sql, true} | nodetree_tree:options()]. + +set_node(Record) when is_record(Record, pubsub_node) -> + {Host, Node} = Record#pubsub_node.nodeid, + Parent = case Record#pubsub_node.parents of + [] -> <<>>; + [First | _] -> First + end, + Type = Record#pubsub_node.type, + H = node_flat_sql:encode_host(Host), + Nidx = case nodeidx(Host, Node) of + {result, OldNidx} -> + catch + ejabberd_sql:sql_query_t( + ?SQL("delete from pubsub_node_option " + "where nodeid=%(OldNidx)d")), + catch + ejabberd_sql:sql_query_t( + ?SQL("update pubsub_node set" + " host=%(H)s, node=%(Node)s," + " parent=%(Parent)s, plugin=%(Type)s " + "where nodeid=%(OldNidx)d")), + OldNidx; + {error, not_found} -> + catch + ejabberd_sql:sql_query_t( + ?SQL("insert into pubsub_node(host, node, parent, plugin) " + "values(%(H)s, %(Node)s, %(Parent)s, %(Type)s)")), + case nodeidx(Host, Node) of + {result, NewNidx} -> NewNidx; + {error, not_found} -> none; % this should not happen + {error, _} -> db_error + end; + {error, _} -> + db_error + end, + case Nidx of + db_error -> + {error, xmpp:err_internal_server_error(?T("Database failure"), ejabberd_option:language())}; + none -> + Txt = ?T("Node index not found"), + {error, xmpp:err_internal_server_error(Txt, ejabberd_option:language())}; + _ -> + lists:foreach(fun ({Key, Value}) -> + SKey = iolist_to_binary(atom_to_list(Key)), + SValue = misc:term_to_expr(Value), + catch + ejabberd_sql:sql_query_t( + ?SQL("insert into pubsub_node_option(nodeid, name, val) " + "values (%(Nidx)d, %(SKey)s, %(SValue)s)")) + end, + Record#pubsub_node.options), + {result, Nidx} + end. + +get_node(Host, Node, _From) -> + get_node(Host, Node). + +get_node(Host, Node) -> + H = node_flat_sql:encode_host(Host), + case catch + ejabberd_sql:sql_query_t( + ?SQL("select @(node)s, @(parent)s, @(plugin)s, @(nodeid)d from pubsub_node " + "where host=%(H)s and node=%(Node)s")) + of + {selected, [RItem]} -> + raw_to_node(Host, RItem); + {'EXIT', _Reason} -> + {error, xmpp:err_internal_server_error(?T("Database failure"), ejabberd_option:language())}; + _ -> + {error, xmpp:err_item_not_found(?T("Node not found"), ejabberd_option:language())} + end. + +get_node(Nidx) -> + case catch + ejabberd_sql:sql_query_t( + ?SQL("select @(host)s, @(node)s, @(parent)s, @(plugin)s from pubsub_node " + "where nodeid=%(Nidx)d")) + of + {selected, [{Host, Node, Parent, Type}]} -> + raw_to_node(Host, {Node, Parent, Type, Nidx}); + {'EXIT', _Reason} -> + {error, xmpp:err_internal_server_error(?T("Database failure"), ejabberd_option:language())}; + _ -> + {error, xmpp:err_item_not_found(?T("Node not found"), ejabberd_option:language())} + end. + +get_nodes(Host) -> + get_nodes(Host, infinity). + +get_nodes(Host, Limit) -> + H = node_flat_sql:encode_host(Host), + Query = fun(mssql, _) when is_integer(Limit), Limit>=0 -> + ejabberd_sql:sql_query_t( + ?SQL("select top %(Limit)d @(node)s, @(parent)s, @(plugin)s, @(nodeid)d " + "from pubsub_node where host=%(H)s")); + (_, _) when is_integer(Limit), Limit>=0 -> + ejabberd_sql:sql_query_t( + ?SQL("select @(node)s, @(parent)s, @(plugin)s, @(nodeid)d " + "from pubsub_node where host=%(H)s limit %(Limit)d")); + (_, _) -> + ejabberd_sql:sql_query_t( + ?SQL("select @(node)s, @(parent)s, @(plugin)s, @(nodeid)d " + "from pubsub_node where host=%(H)s")) + end, + case ejabberd_sql:sql_query_t(Query) of + {selected, RItems} -> + [raw_to_node(Host, Item) || Item <- RItems]; + _ -> + [] + end. + +get_all_nodes({_U, _S, _R} = JID) -> + SubKey = jid:tolower(JID), + GenKey = jid:remove_resource(SubKey), + EncKey = node_flat_sql:encode_jid(GenKey), + Pattern = <<(node_flat_sql:encode_jid_like(GenKey))/binary, "/%">>, + case ejabberd_sql:sql_query_t( + ?SQL("select @(node)s, @(parent)s, @(plugin)s, @(nodeid)d " + "from pubsub_node where host=%(EncKey)s " + "or host like %(Pattern)s %ESCAPE")) of + {selected, RItems} -> + [raw_to_node(GenKey, Item) || Item <- RItems]; + _ -> + [] + end; +get_all_nodes(Host) -> + Pattern1 = <<"%@", Host/binary>>, + Pattern2 = <<"%@", Host/binary, "/%">>, + case ejabberd_sql:sql_query_t( + ?SQL("select @(node)s, @(parent)s, @(plugin)s, @(nodeid)d " + "from pubsub_node where host=%(Host)s " + "or host like %(Pattern1)s " + "or host like %(Pattern2)s %ESCAPE")) of + {selected, RItems} -> + [raw_to_node(Host, Item) || Item <- RItems]; + _ -> + [] + end. + +get_parentnodes(Host, Node, _From) -> + case get_node(Host, Node) of + Record when is_record(Record, pubsub_node) -> + Record#pubsub_node.parents; + _ -> + [] + end. + +get_parentnodes_tree(Host, Node, _From) -> + get_parentnodes_tree(Host, Node, 0, []). +get_parentnodes_tree(Host, Node, Level, Acc) -> + case get_node(Host, Node) of + Record when is_record(Record, pubsub_node) -> + Tree = [{Level, [Record]}|Acc], + case Record#pubsub_node.parents of + [Parent] -> get_parentnodes_tree(Host, Parent, Level+1, Tree); + _ -> Tree + end; + _ -> + Acc + end. + +get_subnodes(Host, Node, Limit) -> + H = node_flat_sql:encode_host(Host), + Query = fun(mssql, _) when is_integer(Limit), Limit>=0 -> + ejabberd_sql:sql_query_t( + ?SQL("select top %(Limit)d @(node)s, @(parent)s, @(plugin)s, @(nodeid)d " + "from pubsub_node where host=%(H)s and parent=%(Node)s")); + (_, _) when is_integer(Limit), Limit>=0 -> + ejabberd_sql:sql_query_t( + ?SQL("select @(node)s, @(parent)s, @(plugin)s, @(nodeid)d " + "from pubsub_node where host=%(H)s and parent=%(Node)s " + "limit %(Limit)d")); + (_, _) -> + ejabberd_sql:sql_query_t( + ?SQL("select @(node)s, @(parent)s, @(plugin)s, @(nodeid)d " + "from pubsub_node where host=%(H)s and parent=%(Node)s")) + end, + case ejabberd_sql:sql_query_t(Query) of + {selected, RItems} -> + [raw_to_node(Host, Item) || Item <- RItems]; + _ -> + [] + end. + +get_subnodes_tree(Host, Node, _From) -> + get_subnodes_tree(Host, Node). + +get_subnodes_tree(Host, Node) -> + case get_node(Host, Node) of + {error, _} -> + []; + Rec -> + Type = Rec#pubsub_node.type, + H = node_flat_sql:encode_host(Host), + N = <<(ejabberd_sql:escape_like_arg(Node))/binary, "/%">>, + Sub = case catch + ejabberd_sql:sql_query_t( + ?SQL("select @(node)s, @(parent)s, @(plugin)s, @(nodeid)d from pubsub_node " + "where host=%(H)s and plugin=%(Type)s and" + " (parent=%(Node)s or parent like %(N)s %ESCAPE)")) + of + {selected, RItems} -> + [raw_to_node(Host, Item) || Item <- RItems]; + _ -> + [] + end, + [Rec|Sub] + end. + +create_node(Host, Node, Type, Owner, Options, Parents) -> + BJID = jid:tolower(jid:remove_resource(Owner)), + case nodeidx(Host, Node) of + {error, not_found} -> + ParentExists = case Host of + {_U, _S, _R} -> + %% This is special case for PEP handling + %% PEP does not uses hierarchy + true; + _ -> + case Parents of + [] -> + true; + [Parent | _] -> + case nodeidx(Host, Parent) of + {result, PNode} -> + case nodeowners(PNode) of + [{<<>>, Host, <<>>}] -> true; + Owners -> lists:member(BJID, Owners) + end; + _ -> + false + end; + _ -> + false + end + end, + case ParentExists of + true -> + case set_node(#pubsub_node{nodeid = {Host, Node}, + parents = Parents, type = Type, + options = Options}) + of + {result, Nidx} -> {ok, Nidx}; + Other -> Other + end; + false -> + {error, xmpp:err_forbidden()} + end; + {result, _} -> + {error, xmpp:err_conflict(?T("Node already exists"), ejabberd_option:language())}; + {error, db_fail} -> + {error, xmpp:err_internal_server_error(?T("Database failure"), ejabberd_option:language())} + end. + +delete_node(Host, Node) -> + lists:map( + fun(Rec) -> + Nidx = Rec#pubsub_node.id, + catch ejabberd_sql:sql_query_t( + ?SQL("delete from pubsub_node where nodeid=%(Nidx)d")), + Rec + end, get_subnodes_tree(Host, Node)). + +%% helpers +raw_to_node(Host, [Node, Parent, Type, Nidx]) -> + raw_to_node(Host, {Node, Parent, Type, binary_to_integer(Nidx)}); +raw_to_node(Host, {Node, Parent, Type, Nidx}) -> + Options = case catch + ejabberd_sql:sql_query_t( + ?SQL("select @(name)s, @(val)s from pubsub_node_option " + "where nodeid=%(Nidx)d")) + of + {selected, ROptions} -> + DbOpts = lists:map( + fun({<<"max_items">>, <<"infinity">>}) -> + {max_items, max}; + ({Key, Value}) -> + RKey = misc:binary_to_atom(Key), + Tokens = element(2, erl_scan:string(binary_to_list(<>))), + RValue = element(2, erl_parse:parse_term(Tokens)), + {RKey, RValue} + end, + ROptions), + Module = misc:binary_to_atom(<<"node_", Type/binary, "_sql">>), + StdOpts = Module:options(), + lists:foldl(fun ({Key, Value}, Acc) -> + lists:keystore(Key, 1, Acc, {Key, Value}) + end, + StdOpts, DbOpts); + _ -> + [] + end, + Parents = case Parent of + <<>> -> []; + _ -> [Parent] + end, + #pubsub_node{nodeid = {Host, Node}, id = Nidx, + parents = Parents, type = Type, options = Options}. + +nodeidx(Host, Node) -> + H = node_flat_sql:encode_host(Host), + case catch + ejabberd_sql:sql_query_t( + ?SQL("select @(nodeid)d from pubsub_node " + "where host=%(H)s and node=%(Node)s")) + of + {selected, [{Nidx}]} -> + {result, Nidx}; + {'EXIT', _Reason} -> + {error, db_fail}; + _ -> + {error, not_found} + end. + +nodeowners(Nidx) -> + {result, Res} = node_flat_sql:get_node_affiliations(Nidx), + [LJID || {LJID, Aff} <- Res, Aff =:= owner]. diff --git a/src/nodetree_virtual.erl b/src/nodetree_virtual.erl index abe88560f..18eb9ed30 100644 --- a/src/nodetree_virtual.erl +++ b/src/nodetree_virtual.erl @@ -1,168 +1,125 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. -%%% -%%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. -%%% -%%% -%%% The Initial Developer of the Original Code is ProcessOne. -%%% Portions created by ProcessOne are Copyright 2006-2015, ProcessOne -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2015, ProcessOne. +%%%---------------------------------------------------------------------- +%%% File : nodetree_virtual.erl +%%% Author : Christophe Romain +%%% Purpose : Standard node tree plugin using no storage backend +%%% Created : 1 Dec 2007 by Christophe Romain %%% %%% -%%% @copyright 2006-2015 ProcessOne -%%% @author Christophe Romain -%%% [http://www.process-one.net/] -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== +%%% ejabberd, Copyright (C) 2002-2025 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. +%%% +%%%---------------------------------------------------------------------- %%% @doc The module {@module} is the PubSub node tree plugin that -%%% allow virtual nodes handling. +%%% allow virtual nodes handling. This prevent storage of nodes. %%%

    PubSub node tree plugins are using the {@link gen_nodetree} behaviour.

    %%%

    This plugin development is still a work in progress. Due to optimizations in %%% mod_pubsub, this plugin can not work anymore without altering functioning. %%% Please, send us comments, feedback and improvements.

    -module(nodetree_virtual). - +-behaviour(gen_pubsub_nodetree). -author('christophe.romain@process-one.net'). -include("pubsub.hrl"). --include("jlib.hrl"). - --behaviour(gen_pubsub_nodetree). - -export([init/3, terminate/2, options/0, set_node/1, - get_node/3, get_node/2, get_node/1, get_nodes/2, - get_nodes/1, get_parentnodes/3, get_parentnodes_tree/3, - get_subnodes/3, get_subnodes_tree/3, create_node/6, - delete_node/2]). + get_node/3, get_node/2, get_node/1, get_nodes/2, + get_nodes/1, get_all_nodes/1, + get_parentnodes/3, get_parentnodes_tree/3, + get_subnodes/3, get_subnodes_tree/3, create_node/6, + delete_node/2]). -%% ================ -%% API definition -%% ================ +init(_Host, _ServerHost, _Opts) -> + ok. -%% @spec (Host, ServerHost, Opts) -> any() -%% Host = mod_pubsub:host() -%% ServerHost = host() -%% Opts = list() -%% @doc

    Called during pubsub modules initialisation. Any pubsub plugin must -%% implement this function. It can return anything.

    -%%

    This function is mainly used to trigger the setup task necessary for the -%% plugin. It can be used for example by the developer to create the specific -%% module database schema if it does not exists yet.

    -%% @spec () -> [Option] -%% Option = mod_pubsub:nodetreeOption() -%% @doc

    Returns the default pubsub node tree options.

    -%% @spec (NodeRecord) -> ok | {error, Reason} -%% NodeRecord = mod_pubsub:pubsub_node() -%% @doc

    No node record is stored on database. Just do nothing.

    -%% @spec (Host, Node, From) -> pubsubNode() -%% Host = mod_pubsub:host() -%% Node = mod_pubsub:pubsubNode() -%% From = mod_pubsub:jid() -%% @doc

    Virtual node tree does not handle a node database. Any node is considered -%% as existing. Node record contains default values.

    -init(_Host, _ServerHost, _Opts) -> ok. +terminate(_Host, _ServerHost) -> + ok. -terminate(_Host, _ServerHost) -> ok. +options() -> + [{virtual_tree, true}]. -options() -> [{virtual_tree, true}]. +set_node(_Node) -> + ok. -set_node(_NodeRecord) -> ok. +get_node(Host, Node, _From) -> + get_node(Host, Node). -get_node(Host, Node, _From) -> get_node(Host, Node). +get_node(Host, Node) -> + Nidx = nodeidx(Host, Node), + node_record(Host, Node, Nidx). -get_node(Host, Node) -> get_node({Host, Node}). +get_node(Nidx) -> + {Host, Node} = nodeid(Nidx), + node_record(Host, Node, Nidx). -get_node({Host, _} = NodeId) -> - Record = #pubsub_node{nodeid = NodeId, id = NodeId}, - Module = jlib:binary_to_atom(<<"node_", - (Record#pubsub_node.type)/binary>>), - Options = Module:options(), - Owners = [{<<"">>, Host, <<"">>}], - Record#pubsub_node{owners = Owners, options = Options}. +get_nodes(Host) -> + get_nodes(Host, infinity). -%% @spec (Host, From) -> [pubsubNode()] -%% Host = mod_pubsub:host() | mod_pubsub:jid() -%% From = mod_pubsub:jid() -%% @doc

    Virtual node tree does not handle a node database. Any node is considered -%% as existing. Nodes list can not be determined.

    -%% @spec (Host, Node, From) -> [pubsubNode()] -%% Host = mod_pubsub:host() -%% Node = mod_pubsub:pubsubNode() -%% From = mod_pubsub:jid() -%% @doc

    Virtual node tree does not handle parent/child. Child list is empty.

    -%% @spec (Host, Node, From) -> [pubsubNode()] -%% Host = mod_pubsub:host() -%% Node = mod_pubsub:pubsubNode() -%% From = mod_pubsub:jid() -%% @doc

    Virtual node tree does not handle parent/child. Child list is empty.

    -%% @spec (Host, Node, From) -> [pubsubNode()] -%% Host = mod_pubsub:host() -%% Node = mod_pubsub:pubsubNode() -%% From = mod_pubsub:jid() -%% @doc

    Virtual node tree does not handle parent/child. Child list is empty.

    -get_nodes(Host, _From) -> get_nodes(Host). +get_nodes(_Host, _Limit) -> + []. -get_nodes(_Host) -> []. +get_all_nodes(_Host) -> + []. -get_parentnodes(_Host, _Node, _From) -> []. +get_parentnodes(_Host, _Node, _From) -> + []. --spec(get_parentnodes_tree/3 :: -( - Host :: mod_pubsub:host(), - NodeId :: mod_pubsub:nodeId(), - From :: jid()) - -> [{0, [mod_pubsub:pubsubNode(),...]}] -). -get_parentnodes_tree(Host, NodeId, From) -> - case get_node(Host, NodeId, From) of - Node when is_record(Node, pubsub_node) -> [{0, [Node]}]; - _Error -> [] - end. +get_parentnodes_tree(Host, Node, From) -> + [{0, [get_node(Host, Node, From)]}]. -get_subnodes(Host, Node, _From) -> - get_subnodes(Host, Node). -%% @spec (Host, Node, From) -> [pubsubNode()] -%% Host = mod_pubsub:host() -%% Node = mod_pubsub:pubsubNode() -%% From = mod_pubsub:jid() -%% @doc

    Virtual node tree does not handle parent/child. Child list is empty.

    - -get_subnodes(_Host, _Node) -> []. +get_subnodes(_Host, _Node, _From) -> + []. get_subnodes_tree(Host, Node, _From) -> get_subnodes_tree(Host, Node). -%% @spec (Host, Node, Type, Owner, Options, Parents) -> ok -%% Host = mod_pubsub:host() -%% Node = mod_pubsub:pubsubNode() -%% Type = mod_pubsub:nodeType() -%% Owner = mod_pubsub:jid() -%% Options = list() -%% @doc

    No node record is stored on database. Any valid node -%% is considered as already created.

    -%%

    default allowed nodes: /home/host/user/any/node/name

    -%% @spec (Host, Node) -> [mod_pubsub:node()] -%% Host = mod_pubsub:host() -%% Node = mod_pubsub:pubsubNode() -%% @doc

    Virtual node tree does not handle parent/child. -%% node deletion just affects the corresponding node.

    -get_subnodes_tree(_Host, _Node) -> []. +get_subnodes_tree(_Host, _Node) -> + []. -create_node(Host, Node, _Type, _Owner, _Options, - _Parents) -> - {error, {virtual, {Host, Node}}}. +create_node(Host, Node, _Type, _Owner, _Options, _Parents) -> + {error, {virtual, nodeidx(Host, Node)}}. -delete_node(Host, Node) -> [get_node(Host, Node)]. +delete_node(Host, Node) -> + [get_node(Host, Node)]. + +%% internal helper + +node_record({U,S,R}, Node, Nidx) -> + Host = mod_pubsub:host(S), + Type = <<"pep">>, + Module = mod_pubsub:plugin(Host, Type), + #pubsub_node{nodeid = {{U,S,R},Node}, id = Nidx, type = Type, + owners = [{U,S,R}], + options = Module:options()}; +node_record(Host, Node, Nidx) -> + [Type|_] = mod_pubsub:plugins(Host), + Module = mod_pubsub:plugin(Host, Type), + #pubsub_node{nodeid = {Host, Node}, id = Nidx, type = Type, + owners = [{<<"">>, Host, <<"">>}], + options = Module:options()}. + +nodeidx({U,S,R}, Node) -> + JID = jid:encode(jid:make(U,S,R)), + <>; +nodeidx(Host, Node) -> + <>. +nodeid(Nidx) -> + [Head, Node] = binary:split(Nidx, <<":">>), + case jid:decode(Head) of + {jid,<<>>,Host,<<>>,_,_,_} -> {Host, Node}; + {jid,U,S,R,_,_,_} -> {{U,S,R}, Node} + end. diff --git a/src/odbc_queries.erl b/src/odbc_queries.erl deleted file mode 100644 index 1fa16b896..000000000 --- a/src/odbc_queries.erl +++ /dev/null @@ -1,956 +0,0 @@ -%%%---------------------------------------------------------------------- -%%% File : odbc_queries.erl -%%% Author : Mickael Remond -%%% Purpose : ODBC queries dependind on back-end -%%% Created : by Mickael Remond -%%% -%%% -%%% ejabberd, Copyright (C) 2002-2015 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(odbc_queries). - --author("mremond@process-one.net"). - --export([get_db_type/0, update_t/4, sql_transaction/2, - get_last/2, set_last_t/4, del_last/2, get_password/2, - set_password_t/3, add_user/3, del_user/2, - del_user_return_password/3, list_users/1, list_users/2, - users_number/1, users_number/2, add_spool_sql/2, - add_spool/2, get_and_del_spool_msg_t/2, del_spool_msg/2, - get_roster/2, get_roster_jid_groups/2, - get_roster_groups/3, del_user_roster_t/2, - get_roster_by_jid/3, get_rostergroup_by_jid/3, - del_roster/3, del_roster_sql/2, update_roster/5, - update_roster_sql/4, roster_subscribe/4, - get_subscription/3, set_private_data/4, - set_private_data_sql/3, get_private_data/3, get_private_data/2, - del_user_private_storage/2, get_default_privacy_list/2, - get_default_privacy_list_t/1, get_privacy_list_names/2, - get_privacy_list_names_t/1, get_privacy_list_id/3, - get_privacy_list_id_t/2, get_privacy_list_data/3, - get_privacy_list_data_by_id/2, get_privacy_list_data_t/2, - get_privacy_list_data_by_id_t/1, - set_default_privacy_list/2, - unset_default_privacy_list/2, - remove_privacy_list/2, - add_privacy_list/2, - set_privacy_list/2, - del_privacy_lists/3, - set_vcard/26, - get_vcard/2, - escape/1, - count_records_where/3, - get_roster_version/2, - set_roster_version/2]). - -%% We have only two compile time options for db queries: -%-define(generic, true). -%-define(mssql, true). --ifndef(mssql). - --undef(generic). - --define(generic, true). - --endif. - --include("ejabberd.hrl"). --include("logger.hrl"). - -%% Almost a copy of string:join/2. -%% We use this version because string:join/2 is relatively -%% new function (introduced in R12B-0). -join([], _Sep) -> []; -join([H | T], Sep) -> [H, [[Sep, X] || X <- T]]. - -%% ----------------- -%% Generic queries --ifdef(generic). - -get_db_type() -> generic. - -%% Safe atomic update. -update_t(Table, Fields, Vals, Where) -> - UPairs = lists:zipwith(fun (A, B) -> - <> - end, - Fields, Vals), - case ejabberd_odbc:sql_query_t([<<"update ">>, Table, - <<" set ">>, join(UPairs, <<", ">>), - <<" where ">>, Where, <<";">>]) - of - {updated, 1} -> ok; - _ -> - Res = ejabberd_odbc:sql_query_t([<<"insert into ">>, Table, - <<"(">>, join(Fields, <<", ">>), - <<") values ('">>, join(Vals, <<"', '">>), - <<"');">>]), - case Res of - {updated,1} -> ok; - _ -> Res - end - end. - -update(LServer, Table, Fields, Vals, Where) -> - UPairs = lists:zipwith(fun (A, B) -> - <> - end, - Fields, Vals), - case ejabberd_odbc:sql_query(LServer, - [<<"update ">>, Table, <<" set ">>, - join(UPairs, <<", ">>), <<" where ">>, Where, - <<";">>]) - of - {updated, 1} -> ok; - _ -> - Res = ejabberd_odbc:sql_query(LServer, - [<<"insert into ">>, Table, <<"(">>, - join(Fields, <<", ">>), <<") values ('">>, - join(Vals, <<"', '">>), <<"');">>]), - case Res of - {updated,1} -> ok; - _ -> Res - end - end. - -%% F can be either a fun or a list of queries -%% TODO: We should probably move the list of queries transaction -%% wrapper from the ejabberd_odbc module to this one (odbc_queries) -sql_transaction(LServer, F) -> - ejabberd_odbc:sql_transaction(LServer, F). - -get_last(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"select seconds, state from last where " - "username='">>, - Username, <<"'">>]). - -set_last_t(LServer, Username, Seconds, State) -> - update(LServer, <<"last">>, - [<<"username">>, <<"seconds">>, <<"state">>], - [Username, Seconds, State], - [<<"username='">>, Username, <<"'">>]). - -del_last(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"delete from last where username='">>, Username, - <<"'">>]). - -get_password(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"select password from users where username='">>, - Username, <<"';">>]). - -set_password_t(LServer, Username, Pass) -> - ejabberd_odbc:sql_transaction(LServer, - fun () -> - update_t(<<"users">>, - [<<"username">>, - <<"password">>], - [Username, Pass], - [<<"username='">>, Username, - <<"'">>]) - end). - -add_user(LServer, Username, Pass) -> - ejabberd_odbc:sql_query(LServer, - [<<"insert into users(username, password) " - "values ('">>, - Username, <<"', '">>, Pass, <<"');">>]). - -del_user(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"delete from users where username='">>, Username, - <<"';">>]). - -del_user_return_password(_LServer, Username, Pass) -> - P = - ejabberd_odbc:sql_query_t([<<"select password from users where username='">>, - Username, <<"';">>]), - ejabberd_odbc:sql_query_t([<<"delete from users where username='">>, - Username, <<"' and password='">>, Pass, - <<"';">>]), - P. - -list_users(LServer) -> - ejabberd_odbc:sql_query(LServer, - [<<"select username from users">>]). - -list_users(LServer, [{from, Start}, {to, End}]) - when is_integer(Start) and is_integer(End) -> - list_users(LServer, - [{limit, End - Start + 1}, {offset, Start - 1}]); -list_users(LServer, - [{prefix, Prefix}, {from, Start}, {to, End}]) - when is_binary(Prefix) and is_integer(Start) and - is_integer(End) -> - list_users(LServer, - [{prefix, Prefix}, {limit, End - Start + 1}, - {offset, Start - 1}]); -list_users(LServer, [{limit, Limit}, {offset, Offset}]) - when is_integer(Limit) and is_integer(Offset) -> - ejabberd_odbc:sql_query(LServer, - [list_to_binary( - io_lib:format( - "select username from users " ++ - "order by username " ++ - "limit ~w offset ~w", - [Limit, Offset]))]); -list_users(LServer, - [{prefix, Prefix}, {limit, Limit}, {offset, Offset}]) - when is_binary(Prefix) and is_integer(Limit) and - is_integer(Offset) -> - ejabberd_odbc:sql_query(LServer, - [list_to_binary( - io_lib:format( - "select username from users " ++ - "where username like '~s%' " ++ - "order by username " ++ - "limit ~w offset ~w ", - [Prefix, Limit, Offset]))]). - -users_number(LServer) -> - Type = ejabberd_config:get_option({odbc_type, LServer}, - fun(pgsql) -> pgsql; - (mysql) -> mysql; - (odbc) -> odbc - end, odbc), - case Type of - pgsql -> - case - ejabberd_config:get_option( - {pgsql_users_number_estimate, LServer}, - fun(V) when is_boolean(V) -> V end, - false) - of - true -> - ejabberd_odbc:sql_query(LServer, - [<<"select reltuples from pg_class where " - "oid = 'users'::regclass::oid">>]); - _ -> - ejabberd_odbc:sql_query(LServer, - [<<"select count(*) from users">>]) - end; - _ -> - ejabberd_odbc:sql_query(LServer, - [<<"select count(*) from users">>]) - end. - -users_number(LServer, [{prefix, Prefix}]) - when is_binary(Prefix) -> - ejabberd_odbc:sql_query(LServer, - [list_to_binary( - io_lib:fwrite( - "select count(*) from users " ++ - %% Warning: Escape prefix at higher level to prevent SQL - %% injection. - "where username like '~s%'", - [Prefix]))]); -users_number(LServer, []) -> - users_number(LServer). - - -add_spool_sql(Username, XML) -> - [<<"insert into spool(username, xml) values ('">>, - Username, <<"', '">>, XML, <<"');">>]. - -add_spool(LServer, Queries) -> - ejabberd_odbc:sql_transaction(LServer, Queries). - -get_and_del_spool_msg_t(LServer, Username) -> - F = fun () -> - Result = - ejabberd_odbc:sql_query_t([<<"select username, xml from spool where " - "username='">>, - Username, - <<"' order by seq;">>]), - ejabberd_odbc:sql_query_t([<<"delete from spool where username='">>, - Username, <<"';">>]), - Result - end, - ejabberd_odbc:sql_transaction(LServer, F). - -del_spool_msg(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"delete from spool where username='">>, Username, - <<"';">>]). - -get_roster(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"select username, jid, nick, subscription, " - "ask, askmessage, server, subscribe, " - "type from rosterusers where username='">>, - Username, <<"'">>]). - -get_roster_jid_groups(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"select jid, grp from rostergroups where " - "username='">>, - Username, <<"'">>]). - -get_roster_groups(_LServer, Username, SJID) -> - ejabberd_odbc:sql_query_t([<<"select grp from rostergroups where username='">>, - Username, <<"' and jid='">>, SJID, <<"';">>]). - -del_user_roster_t(LServer, Username) -> - ejabberd_odbc:sql_transaction(LServer, - fun () -> - ejabberd_odbc:sql_query_t([<<"delete from rosterusers where " - "username='">>, - Username, - <<"';">>]), - ejabberd_odbc:sql_query_t([<<"delete from rostergroups where " - "username='">>, - Username, - <<"';">>]) - end). - -get_roster_by_jid(_LServer, Username, SJID) -> - ejabberd_odbc:sql_query_t([<<"select username, jid, nick, subscription, " - "ask, askmessage, server, subscribe, " - "type from rosterusers where username='">>, - Username, <<"' and jid='">>, SJID, <<"';">>]). - -get_rostergroup_by_jid(LServer, Username, SJID) -> - ejabberd_odbc:sql_query(LServer, - [<<"select grp from rostergroups where username='">>, - Username, <<"' and jid='">>, SJID, <<"'">>]). - -del_roster(_LServer, Username, SJID) -> - ejabberd_odbc:sql_query_t([<<"delete from rosterusers where " - "username='">>, - Username, <<"' and jid='">>, SJID, - <<"';">>]), - ejabberd_odbc:sql_query_t([<<"delete from rostergroups where " - "username='">>, - Username, <<"' and jid='">>, SJID, - <<"';">>]). - -del_roster_sql(Username, SJID) -> - [[<<"delete from rosterusers where " - "username='">>, - Username, <<"' and jid='">>, SJID, <<"';">>], - [<<"delete from rostergroups where " - "username='">>, - Username, <<"' and jid='">>, SJID, <<"';">>]]. - -update_roster(_LServer, Username, SJID, ItemVals, - ItemGroups) -> - update_t(<<"rosterusers">>, - [<<"username">>, <<"jid">>, <<"nick">>, - <<"subscription">>, <<"ask">>, <<"askmessage">>, - <<"server">>, <<"subscribe">>, <<"type">>], - ItemVals, - [<<"username='">>, Username, <<"' and jid='">>, SJID, - <<"'">>]), - ejabberd_odbc:sql_query_t([<<"delete from rostergroups where " - "username='">>, - Username, <<"' and jid='">>, SJID, - <<"';">>]), - lists:foreach(fun (ItemGroup) -> - ejabberd_odbc:sql_query_t([<<"insert into rostergroups( " - " username, jid, grp) values ('">>, - join(ItemGroup, - <<"', '">>), - <<"');">>]) - end, - ItemGroups). - -update_roster_sql(Username, SJID, ItemVals, - ItemGroups) -> - [[<<"delete from rosterusers where " - "username='">>, - Username, <<"' and jid='">>, SJID, <<"';">>], - [<<"insert into rosterusers( " - " username, jid, nick, " - " subscription, ask, askmessage, " - " server, subscribe, type) " - "values ('">>, - join(ItemVals, <<"', '">>), <<"');">>], - [<<"delete from rostergroups where " - "username='">>, - Username, <<"' and jid='">>, SJID, <<"';">>]] - ++ - [[<<"insert into rostergroups( " - " username, jid, grp) values ('">>, - join(ItemGroup, <<"', '">>), <<"');">>] - || ItemGroup <- ItemGroups]. - -roster_subscribe(_LServer, Username, SJID, ItemVals) -> - update_t(<<"rosterusers">>, - [<<"username">>, <<"jid">>, <<"nick">>, - <<"subscription">>, <<"ask">>, <<"askmessage">>, - <<"server">>, <<"subscribe">>, <<"type">>], - ItemVals, - [<<"username='">>, Username, <<"' and jid='">>, SJID, - <<"'">>]). - -get_subscription(LServer, Username, SJID) -> - ejabberd_odbc:sql_query(LServer, - [<<"select subscription from rosterusers " - "where username='">>, - Username, <<"' and jid='">>, SJID, <<"'">>]). - -set_private_data(_LServer, Username, LXMLNS, SData) -> - update_t(<<"private_storage">>, - [<<"username">>, <<"namespace">>, <<"data">>], - [Username, LXMLNS, SData], - [<<"username='">>, Username, <<"' and namespace='">>, - LXMLNS, <<"'">>]). - -set_private_data_sql(Username, LXMLNS, SData) -> - [[<<"delete from private_storage where username='">>, - Username, <<"' and namespace='">>, LXMLNS, <<"';">>], - [<<"insert into private_storage(username, " - "namespace, data) values ('">>, - Username, <<"', '">>, LXMLNS, <<"', '">>, SData, - <<"');">>]]. - -get_private_data(LServer, Username, LXMLNS) -> - ejabberd_odbc:sql_query(LServer, - [<<"select data from private_storage where " - "username='">>, - Username, <<"' and namespace='">>, LXMLNS, - <<"';">>]). - -get_private_data(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"select namespace, data from private_storage " - "where username='">>, Username, <<"';">>]). - -del_user_private_storage(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"delete from private_storage where username='">>, - Username, <<"';">>]). - -set_vcard(LServer, LUsername, SBDay, SCTRY, SEMail, SFN, - SFamily, SGiven, SLBDay, SLCTRY, SLEMail, SLFN, - SLFamily, SLGiven, SLLocality, SLMiddle, SLNickname, - SLOrgName, SLOrgUnit, SLocality, SMiddle, SNickname, - SOrgName, SOrgUnit, SVCARD, Username) -> - ejabberd_odbc:sql_transaction(LServer, - fun () -> - update_t(<<"vcard">>, - [<<"username">>, - <<"vcard">>], - [LUsername, SVCARD], - [<<"username='">>, LUsername, - <<"'">>]), - update_t(<<"vcard_search">>, - [<<"username">>, - <<"lusername">>, <<"fn">>, - <<"lfn">>, <<"family">>, - <<"lfamily">>, <<"given">>, - <<"lgiven">>, <<"middle">>, - <<"lmiddle">>, - <<"nickname">>, - <<"lnickname">>, <<"bday">>, - <<"lbday">>, <<"ctry">>, - <<"lctry">>, <<"locality">>, - <<"llocality">>, - <<"email">>, <<"lemail">>, - <<"orgname">>, - <<"lorgname">>, - <<"orgunit">>, - <<"lorgunit">>], - [Username, LUsername, SFN, - SLFN, SFamily, SLFamily, - SGiven, SLGiven, SMiddle, - SLMiddle, SNickname, - SLNickname, SBDay, SLBDay, - SCTRY, SLCTRY, SLocality, - SLLocality, SEMail, SLEMail, - SOrgName, SLOrgName, - SOrgUnit, SLOrgUnit], - [<<"lusername='">>, - LUsername, <<"'">>]) - end). - -get_vcard(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"select vcard from vcard where username='">>, - Username, <<"';">>]). - -get_default_privacy_list(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"select name from privacy_default_list " - "where username='">>, - Username, <<"';">>]). - -get_default_privacy_list_t(Username) -> - ejabberd_odbc:sql_query_t([<<"select name from privacy_default_list " - "where username='">>, - Username, <<"';">>]). - -get_privacy_list_names(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"select name from privacy_list where " - "username='">>, - Username, <<"';">>]). - -get_privacy_list_names_t(Username) -> - ejabberd_odbc:sql_query_t([<<"select name from privacy_list where " - "username='">>, - Username, <<"';">>]). - -get_privacy_list_id(LServer, Username, SName) -> - ejabberd_odbc:sql_query(LServer, - [<<"select id from privacy_list where username='">>, - Username, <<"' and name='">>, SName, <<"';">>]). - -get_privacy_list_id_t(Username, SName) -> - ejabberd_odbc:sql_query_t([<<"select id from privacy_list where username='">>, - Username, <<"' and name='">>, SName, <<"';">>]). - -get_privacy_list_data(LServer, Username, SName) -> - ejabberd_odbc:sql_query(LServer, - [<<"select t, value, action, ord, match_all, " - "match_iq, match_message, match_presence_in, " - "match_presence_out from privacy_list_data " - "where id = (select id from privacy_list " - "where username='">>, - Username, <<"' and name='">>, SName, - <<"') order by ord;">>]). - -get_privacy_list_data_t(Username, SName) -> - ejabberd_odbc:sql_query_t([<<"select t, value, action, ord, match_all, " - "match_iq, match_message, match_presence_in, " - "match_presence_out from privacy_list_data " - "where id = (select id from privacy_list " - "where username='">>, - Username, <<"' and name='">>, SName, - <<"') order by ord;">>]). - -get_privacy_list_data_by_id(LServer, ID) -> - ejabberd_odbc:sql_query(LServer, - [<<"select t, value, action, ord, match_all, " - "match_iq, match_message, match_presence_in, " - "match_presence_out from privacy_list_data " - "where id='">>, - ID, <<"' order by ord;">>]). - -get_privacy_list_data_by_id_t(ID) -> - ejabberd_odbc:sql_query_t([<<"select t, value, action, ord, match_all, " - "match_iq, match_message, match_presence_in, " - "match_presence_out from privacy_list_data " - "where id='">>, - ID, <<"' order by ord;">>]). - -set_default_privacy_list(Username, SName) -> - update_t(<<"privacy_default_list">>, - [<<"username">>, <<"name">>], [Username, SName], - [<<"username='">>, Username, <<"'">>]). - -unset_default_privacy_list(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"delete from privacy_default_list " - " where username='">>, - Username, <<"';">>]). - -remove_privacy_list(Username, SName) -> - ejabberd_odbc:sql_query_t([<<"delete from privacy_list where username='">>, - Username, <<"' and name='">>, SName, <<"';">>]). - -add_privacy_list(Username, SName) -> - ejabberd_odbc:sql_query_t([<<"insert into privacy_list(username, name) " - "values ('">>, - Username, <<"', '">>, SName, <<"');">>]). - -set_privacy_list(ID, RItems) -> - ejabberd_odbc:sql_query_t([<<"delete from privacy_list_data where " - "id='">>, - ID, <<"';">>]), - lists:foreach(fun (Items) -> - ejabberd_odbc:sql_query_t([<<"insert into privacy_list_data(id, t, " - "value, action, ord, match_all, match_iq, " - "match_message, match_presence_in, match_prese" - "nce_out ) values ('">>, - ID, <<"', '">>, - join(Items, <<"', '">>), - <<"');">>]) - end, - RItems). - -del_privacy_lists(LServer, Server, Username) -> -%% Characters to escape -%% Count number of records in a table given a where clause - ejabberd_odbc:sql_query(LServer, - [<<"delete from privacy_list where username='">>, - Username, <<"';">>]), - ejabberd_odbc:sql_query(LServer, - [<<"delete from privacy_list_data where " - "value='">>, - <>, - <<"';">>]), - ejabberd_odbc:sql_query(LServer, - [<<"delete from privacy_default_list where " - "username='">>, - Username, <<"';">>]). - -escape($\000) -> <<"\\0">>; -escape($\n) -> <<"\\n">>; -escape($\t) -> <<"\\t">>; -escape($\b) -> <<"\\b">>; -escape($\r) -> <<"\\r">>; -escape($') -> <<"''">>; -escape($") -> <<"\\\"">>; -escape($\\) -> <<"\\\\">>; -escape(C) -> <>. - -count_records_where(LServer, Table, WhereClause) -> - ejabberd_odbc:sql_query(LServer, - [<<"select count(*) from ">>, Table, <<" ">>, - WhereClause, <<";">>]). - -get_roster_version(LServer, LUser) -> - ejabberd_odbc:sql_query(LServer, - [<<"select version from roster_version where " - "username = '">>, - LUser, <<"'">>]). - -set_roster_version(LUser, Version) -> - update_t(<<"roster_version">>, - [<<"username">>, <<"version">>], [LUser, Version], - [<<"username = '">>, LUser, <<"'">>]). - --endif. - -%% ----------------- -%% MSSQL queries --ifdef(mssql). - -%% Queries can be either a fun or a list of queries -get_db_type() -> mssql. - -sql_transaction(LServer, Queries) - when is_list(Queries) -> - F = fun () -> - lists:foreach(fun (Query) -> - ejabberd_odbc:sql_query(LServer, Query) - end, - Queries) - end, - {atomic, catch F()}; -sql_transaction(_LServer, FQueries) -> - {atomic, catch FQueries()}. - -get_last(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.get_last '">>, Username, <<"'">>]). - -set_last_t(LServer, Username, Seconds, State) -> - Result = ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.set_last '">>, Username, - <<"', '">>, Seconds, <<"', '">>, State, - <<"'">>]), - {atomic, Result}. - -del_last(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.del_last '">>, Username, <<"'">>]). - -get_password(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.get_password '">>, Username, - <<"'">>]). - -set_password_t(LServer, Username, Pass) -> - Result = ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.set_password '">>, - Username, <<"', '">>, Pass, <<"'">>]), - {atomic, Result}. - -add_user(LServer, Username, Pass) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.add_user '">>, Username, <<"', '">>, - Pass, <<"'">>]). - -del_user(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.del_user '">>, Username, <<"'">>]). - -del_user_return_password(LServer, Username, Pass) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.del_user_return_password '">>, - Username, <<"'">>]), - Pass. - -list_users(LServer) -> - ejabberd_odbc:sql_query(LServer, - <<"EXECUTE dbo.list_users">>). - -list_users(LServer, _) -> list_users(LServer). - -users_number(LServer) -> - ejabberd_odbc:sql_query(LServer, - <<"select count(*) from users with (nolock)">>). - -users_number(LServer, _) -> users_number(LServer). - -add_spool_sql(Username, XML) -> - [<<"EXECUTE dbo.add_spool '">>, Username, <<"' , '">>, - XML, <<"'">>]. - -add_spool(LServer, Queries) -> - lists:foreach(fun (Query) -> - ejabberd_odbc:sql_query(LServer, Query) - end, - Queries). - -get_and_del_spool_msg_t(LServer, Username) -> - [Result] = case ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.get_and_del_spool_msg '">>, - Username, <<"'">>]) - of - Rs when is_list(Rs) -> - lists:filter(fun ({selected, _Header, _Row}) -> true; - ({updated, _N}) -> false - end, - Rs); - Rs -> [Rs] - end, - {atomic, Result}. - -del_spool_msg(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.del_spool_msg '">>, Username, - <<"'">>]). - -get_roster(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.get_roster '">>, Username, - <<"'">>]). - -get_roster_jid_groups(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.get_roster_jid_groups '">>, - Username, <<"'">>]). - -get_roster_groups(LServer, Username, SJID) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.get_roster_groups '">>, Username, - <<"' , '">>, SJID, <<"'">>]). - -del_user_roster_t(LServer, Username) -> - Result = ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.del_user_roster '">>, - Username, <<"'">>]), - {atomic, Result}. - -get_roster_by_jid(LServer, Username, SJID) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.get_roster_by_jid '">>, Username, - <<"' , '">>, SJID, <<"'">>]). - -get_rostergroup_by_jid(LServer, Username, SJID) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.get_rostergroup_by_jid '">>, - Username, <<"' , '">>, SJID, <<"'">>]). - -del_roster(LServer, Username, SJID) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.del_roster '">>, Username, - <<"', '">>, SJID, <<"'">>]). - -del_roster_sql(Username, SJID) -> - [<<"EXECUTE dbo.del_roster '">>, Username, <<"', '">>, - SJID, <<"'">>]. - -update_roster(LServer, Username, SJID, ItemVals, - ItemGroups) -> - Query1 = [<<"EXECUTE dbo.del_roster '">>, Username, - <<"', '">>, SJID, <<"' ">>], - ejabberd_odbc:sql_query(LServer, lists:flatten(Query1)), - Query2 = [<<"EXECUTE dbo.add_roster_user ">>, ItemVals], - ejabberd_odbc:sql_query(LServer, lists:flatten(Query2)), - Query3 = [<<"EXECUTE dbo.del_roster_groups '">>, - Username, <<"', '">>, SJID, <<"' ">>], - ejabberd_odbc:sql_query(LServer, lists:flatten(Query3)), - lists:foreach(fun (ItemGroup) -> - Query = [<<"EXECUTE dbo.add_roster_group ">>, - ItemGroup], - ejabberd_odbc:sql_query(LServer, lists:flatten(Query)) - end, - ItemGroups). - -update_roster_sql(Username, SJID, ItemVals, - ItemGroups) -> - [<<"BEGIN TRANSACTION ">>, - <<"EXECUTE dbo.del_roster_groups '">>, Username, - <<"','">>, SJID, <<"' ">>, - <<"EXECUTE dbo.add_roster_user ">>, ItemVals, <<" ">>] - ++ - [lists:flatten(<<"EXECUTE dbo.add_roster_group ">>, - ItemGroup, <<" ">>) - || ItemGroup <- ItemGroups] - ++ [<<"COMMIT">>]. - -roster_subscribe(LServer, _Username, _SJID, ItemVals) -> - catch ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.add_roster_user ">>, - ItemVals]). - -get_subscription(LServer, Username, SJID) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.get_subscription '">>, Username, - <<"' , '">>, SJID, <<"'">>]). - -set_private_data(LServer, Username, LXMLNS, SData) -> - ejabberd_odbc:sql_query(LServer, - set_private_data_sql(Username, LXMLNS, SData)). - -set_private_data_sql(Username, LXMLNS, SData) -> - [<<"EXECUTE dbo.set_private_data '">>, Username, - <<"' , '">>, LXMLNS, <<"' , '">>, SData, <<"'">>]. - -get_private_data(LServer, Username, LXMLNS) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.get_private_data '">>, Username, - <<"' , '">>, LXMLNS, <<"'">>]). - -del_user_private_storage(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.del_user_storage '">>, Username, - <<"'">>]). - -set_vcard(LServer, LUsername, SBDay, SCTRY, SEMail, SFN, - SFamily, SGiven, SLBDay, SLCTRY, SLEMail, SLFN, - SLFamily, SLGiven, SLLocality, SLMiddle, SLNickname, - SLOrgName, SLOrgUnit, SLocality, SMiddle, SNickname, - SOrgName, SOrgUnit, SVCARD, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.set_vcard '">>, SVCARD, <<"' , '">>, - Username, <<"' , '">>, LUsername, <<"' , '">>, SFN, - <<"' , '">>, SLFN, <<"' , '">>, SFamily, - <<"' , '">>, SLFamily, <<"' , '">>, SGiven, - <<"' , '">>, SLGiven, <<"' , '">>, SMiddle, - <<"' , '">>, SLMiddle, <<"' , '">>, SNickname, - <<"' , '">>, SLNickname, <<"' , '">>, SBDay, - <<"' , '">>, SLBDay, <<"' , '">>, SCTRY, - <<"' , '">>, SLCTRY, <<"' , '">>, SLocality, - <<"' , '">>, SLLocality, <<"' , '">>, SEMail, - <<"' , '">>, SLEMail, <<"' , '">>, SOrgName, - <<"' , '">>, SLOrgName, <<"' , '">>, SOrgUnit, - <<"' , '">>, SLOrgUnit, <<"'">>]). - -get_vcard(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.get_vcard '">>, Username, <<"'">>]). - -get_default_privacy_list(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.get_default_privacy_list '">>, - Username, <<"'">>]). - -get_default_privacy_list_t(Username) -> - ejabberd_odbc:sql_query_t([<<"EXECUTE dbo.get_default_privacy_list '">>, - Username, <<"'">>]). - -get_privacy_list_names(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.get_privacy_list_names '">>, - Username, <<"'">>]). - -get_privacy_list_names_t(Username) -> - ejabberd_odbc:sql_query_t([<<"EXECUTE dbo.get_privacy_list_names '">>, - Username, <<"'">>]). - -get_privacy_list_id(LServer, Username, SName) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.get_privacy_list_id '">>, Username, - <<"' , '">>, SName, <<"'">>]). - -get_privacy_list_id_t(Username, SName) -> - ejabberd_odbc:sql_query_t([<<"EXECUTE dbo.get_privacy_list_id '">>, - Username, <<"' , '">>, SName, <<"'">>]). - -get_privacy_list_data(LServer, Username, SName) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.get_privacy_list_data '">>, - Username, <<"' , '">>, SName, <<"'">>]). - -get_privacy_list_data_by_id(LServer, ID) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.get_privacy_list_data_by_id '">>, - ID, <<"'">>]). - -get_privacy_list_data_by_id_t(ID) -> - ejabberd_odbc:sql_query_t([<<"EXECUTE dbo.get_privacy_list_data_by_id '">>, - ID, <<"'">>]). - -set_default_privacy_list(Username, SName) -> - ejabberd_odbc:sql_query_t([<<"EXECUTE dbo.set_default_privacy_list '">>, - Username, <<"' , '">>, SName, <<"'">>]). - -unset_default_privacy_list(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.unset_default_privacy_list '">>, - Username, <<"'">>]). - -remove_privacy_list(Username, SName) -> - ejabberd_odbc:sql_query_t([<<"EXECUTE dbo.remove_privacy_list '">>, - Username, <<"' , '">>, SName, <<"'">>]). - -add_privacy_list(Username, SName) -> - ejabberd_odbc:sql_query_t([<<"EXECUTE dbo.add_privacy_list '">>, - Username, <<"' , '">>, SName, <<"'">>]). - -set_privacy_list(ID, RItems) -> - ejabberd_odbc:sql_query_t([<<"EXECUTE dbo.del_privacy_list_by_id '">>, - ID, <<"'">>]), - lists:foreach(fun (Items) -> - ejabberd_odbc:sql_query_t([<<"EXECUTE dbo.set_privacy_list '">>, - ID, <<"', '">>, - join(Items, <<"', '">>), - <<"'">>]) - end, - RItems). - -del_privacy_lists(LServer, Server, Username) -> -%% Characters to escape -%% Count number of records in a table given a where clause - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.del_privacy_lists @Server='">>, - Server, <<"' @username='">>, Username, <<"'">>]). - -escape($\000) -> <<"\\0">>; -escape($\t) -> <<"\\t">>; -escape($\b) -> <<"\\b">>; -escape($\r) -> <<"\\r">>; -escape($') -> <<"''">>; -escape($") -> <<"\\\"">>; -escape(C) -> C. - -count_records_where(LServer, Table, WhereClause) -> - ejabberd_odbc:sql_query(LServer, - [<<"select count(*) from ">>, Table, - <<" with (nolock) ">>, WhereClause]). - -get_roster_version(LServer, LUser) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.get_roster_version '">>, LUser, - <<"'">>]). - -set_roster_version(Username, Version) -> - LServer = (?MYNAME), - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.set_roster_version '">>, Username, - <<"', '">>, Version, <<"'">>]). - --endif. diff --git a/src/prosody2ejabberd.erl b/src/prosody2ejabberd.erl new file mode 100644 index 000000000..045abdf90 --- /dev/null +++ b/src/prosody2ejabberd.erl @@ -0,0 +1,550 @@ +%%%------------------------------------------------------------------- +%%% File : prosody2ejabberd.erl +%%% Author : Evgeny Khramtsov +%%% Created : 20 Jan 2016 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(prosody2ejabberd). + +%% API +-export([from_dir/1]). + +-include_lib("xmpp/include/scram.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). +-include("logger.hrl"). +-include("mod_roster.hrl"). +-include("mod_offline.hrl"). +-include("mod_privacy.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +from_dir(ProsodyDir) -> + case code:ensure_loaded(luerl) of + {module, _} -> + case file:list_dir(ProsodyDir) of + {ok, HostDirs} -> + lists:foreach( + fun(HostDir) -> + Host = list_to_binary(HostDir), + lists:foreach( + fun(SubDir) -> + Path = filename:join( + [ProsodyDir, HostDir, SubDir]), + convert_dir(Path, Host, SubDir) + end, ["vcard", "accounts", "roster", + "private", "config", "offline", + "privacy", "pep", "pubsub"]) + end, HostDirs); + {error, Why} = Err -> + ?ERROR_MSG("Failed to list ~ts: ~ts", + [ProsodyDir, file:format_error(Why)]), + Err + end; + {error, _} = Err -> + ?ERROR_MSG("The file 'luerl.beam' is not found: maybe " + "ejabberd is not compiled with Lua support", []), + Err + end. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +convert_dir(Path, Host, Type) -> + case file:list_dir(Path) of + {ok, Files} -> + lists:foreach( + fun(File) -> + FilePath = filename:join(Path, File), + case Type of + "pep" -> + case filelib:is_dir(FilePath) of + true -> + JID = list_to_binary(File ++ "@" ++ Host), + convert_dir(FilePath, JID, "pubsub"); + false -> + ok + end; + _ -> + case eval_file(FilePath) of + {ok, Data} -> + Name = iolist_to_binary(filename:rootname(File)), + convert_data(misc:uri_decode(Host), Type, + misc:uri_decode(Name), Data); + Err -> + Err + end + end + end, Files); + {error, enoent} -> + ok; + {error, Why} = Err -> + ?ERROR_MSG("Failed to list ~ts: ~ts", + [Path, file:format_error(Why)]), + Err + end. + +eval_file(Path) -> + case file:read_file(Path) of + {ok, Data} -> + State0 = luerl:init(), + State1 = luerl:set_table([item], + fun([X], State) -> {[X], State} end, + State0), + NewData = case filename:extension(Path) of + ".list" -> + <<"return {", Data/binary, "};">>; + _ -> + Data + end, + case luerl:eval(NewData, State1) of + {ok, _} = Res -> + Res; + {error, Why, _} = Err -> + ?ERROR_MSG("Failed to eval ~ts: ~p", [Path, Why]), + Err + end; + {error, Why} = Err -> + ?ERROR_MSG("Failed to read file ~ts: ~ts", + [Path, file:format_error(Why)]), + Err + end. + +maybe_get_scram_auth(Data) -> + case proplists:get_value(<<"iteration_count">>, Data, no_ic) of + IC when is_number(IC) -> + #scram{ + storedkey = misc:hex_to_base64(proplists:get_value(<<"stored_key">>, Data, <<"">>)), + serverkey = misc:hex_to_base64(proplists:get_value(<<"server_key">>, Data, <<"">>)), + salt = base64:encode(proplists:get_value(<<"salt">>, Data, <<"">>)), + iterationcount = round(IC) + }; + _ -> <<"">> + end. + +convert_data(Host, "accounts", User, [Data]) -> + Password = case proplists:get_value(<<"password">>, Data, no_pass) of + no_pass -> + maybe_get_scram_auth(Data); + Pass when is_binary(Pass) -> + Pass + end, + case ejabberd_auth:try_register(User, Host, Password) of + ok -> + ok; + Err -> + ?ERROR_MSG("Failed to register user ~ts@~ts: ~p", + [User, Host, Err]), + Err + end; +convert_data(Host, "roster", User, [Data]) -> + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Host), + Rosters = + lists:flatmap( + fun({<<"pending">>, L}) -> + convert_pending_item(LUser, LServer, L); + ({S, L}) when is_binary(S) -> + convert_roster_item(LUser, LServer, S, L); + (_) -> + [] + end, Data), + lists:foreach(fun mod_roster:set_roster/1, Rosters); +convert_data(Host, "private", User, [Data]) -> + PrivData = lists:flatmap( + fun({_TagXMLNS, Raw}) -> + case deserialize(Raw) of + [El] -> + XMLNS = fxml:get_tag_attr_s(<<"xmlns">>, El), + [{XMLNS, El}]; + _ -> + [] + end + end, Data), + mod_private:set_data(jid:make(User, Host), PrivData); +convert_data(Host, "vcard", User, [Data]) -> + LServer = jid:nameprep(Host), + case deserialize(Data) of + [VCard] -> + mod_vcard:set_vcard(User, LServer, VCard); + _ -> + ok + end; +convert_data(_Host, "config", _User, [Data]) -> + RoomJID1 = case proplists:get_value(<<"jid">>, Data, not_found) of + not_found -> proplists:get_value(<<"_jid">>, Data, room_jid_not_found); + A when is_binary(A) -> A + end, + RoomJID = jid:decode(RoomJID1), + Config = proplists:get_value(<<"_data">>, Data, []), + RoomCfg = convert_room_config(Data), + case proplists:get_bool(<<"persistent">>, Config) of + true when RoomJID /= error -> + mod_muc:store_room(find_serverhost(RoomJID#jid.lserver), RoomJID#jid.lserver, + RoomJID#jid.luser, RoomCfg); + _ -> + ok + end; +convert_data(Host, "offline", User, [Data]) -> + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Host), + lists:foreach( + fun({_, RawXML}) -> + case deserialize(RawXML) of + [El] -> + case el_to_offline_msg(LUser, LServer, El) of + [Msg] -> ok = mod_offline:store_offline_msg(Msg); + [] -> ok + end; + _ -> + ok + end + end, Data); +convert_data(Host, "privacy", User, [Data]) -> + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Host), + Lists = proplists:get_value(<<"lists">>, Data, []), + Priv = #privacy{ + us = {LUser, LServer}, + default = proplists:get_value(<<"default">>, Data, none), + lists = lists:flatmap( + fun({Name, Vals}) -> + Items = proplists:get_value(<<"items">>, Vals, []), + case lists:map(fun convert_privacy_item/1, + Items) of + [] -> []; + ListItems -> [{Name, ListItems}] + end + end, Lists)}, + mod_privacy:set_list(Priv); +convert_data(HostStr, "pubsub", Node, [Data]) -> + case decode_pubsub_host(HostStr) of + Host when is_binary(Host); + is_tuple(Host) -> + Type = node_type(Host), + NodeData = convert_node_config(HostStr, Data), + DefaultConfig = mod_pubsub:config(Host, default_node_config, []), + Owner = proplists:get_value(owner, NodeData), + Options = lists:foldl( + fun({_Opt, undefined}, Acc) -> + Acc; + ({Opt, Val}, Acc) -> + lists:keystore(Opt, 1, Acc, {Opt, Val}) + end, DefaultConfig, proplists:get_value(options, NodeData)), + case mod_pubsub:tree_action(Host, create_node, [Host, Node, Type, Owner, Options, []]) of + {ok, Nidx} -> + case mod_pubsub:node_action(Host, Type, create_node, [Nidx, Owner]) of + {result, _} -> + Access = open, % always allow subscriptions proplists:get_value(access_model, Options), + Publish = open, % always allow publications proplists:get_value(publish_model, Options), + MaxItems = proplists:get_value(max_items, Options), + Affiliations = proplists:get_value(affiliations, NodeData), + Subscriptions = proplists:get_value(subscriptions, NodeData), + Items = proplists:get_value(items, NodeData), + [mod_pubsub:node_action(Host, Type, set_affiliation, + [Nidx, Entity, Aff]) + || {Entity, Aff} <- Affiliations, Entity =/= Owner], + [mod_pubsub:node_action(Host, Type, subscribe_node, + [Nidx, jid:make(Entity), Entity, Access, never, [], [], []]) + || Entity <- Subscriptions], + [mod_pubsub:node_action(Host, Type, publish_item, + [Nidx, Publisher, Publish, MaxItems, ItemId, Payload, []]) + || {ItemId, Publisher, Payload} <- Items]; + Error -> + Error + end; + Error -> + ?ERROR_MSG("Failed to import pubsub node ~ts on ~p:~n~p", + [Node, Host, NodeData]), + Error + end; + Error -> + ?ERROR_MSG("Failed to import pubsub node: ~p", [Error]), + Error + end; +convert_data(_Host, _Type, _User, _Data) -> + ok. + +convert_pending_item(LUser, LServer, LuaList) -> + lists:flatmap( + fun({S, true}) -> + try jid:decode(S) of + J -> + LJID = jid:tolower(J), + [#roster{usj = {LUser, LServer, LJID}, + us = {LUser, LServer}, + jid = LJID, + ask = in}] + catch _:{bad_jid, _} -> + [] + end; + (_) -> + [] + end, LuaList). + +convert_roster_item(LUser, LServer, JIDstring, LuaList) -> + try jid:decode(JIDstring) of + JID -> + LJID = jid:tolower(JID), + InitR = #roster{usj = {LUser, LServer, LJID}, + us = {LUser, LServer}, + jid = LJID}, + lists:foldl( + fun({<<"groups">>, Val}, [R]) -> + Gs = lists:flatmap( + fun({G, true}) -> [G]; + (_) -> [] + end, Val), + [R#roster{groups = Gs}]; + ({<<"subscription">>, Sub}, [R]) -> + [R#roster{subscription = misc:binary_to_atom(Sub)}]; + ({<<"ask">>, <<"subscribe">>}, [R]) -> + [R#roster{ask = out}]; + ({<<"name">>, Name}, [R]) -> + [R#roster{name = Name}]; + ({<<"persist">>, false}, _) -> + []; + ({<<"approved">>, _}, [R]) -> + [R]; + (A, [R]) -> + io:format("Warning: roster of user ~ts@~ts includes unknown " + "attribute:~n ~p~nand that one is discarded.~n", + [LUser, LServer, A]), + [R] + end, [InitR], LuaList) + catch _:{bad_jid, _} -> + [] + end. + +convert_room_affiliations(Data) -> + lists:flatmap( + fun({J, Aff}) -> + try jid:decode(J) of + #jid{luser = U, lserver = S} -> + [{{U, S, <<>>}, misc:binary_to_atom(Aff)}] + catch _:{bad_jid, _} -> + [] + end + end, proplists:get_value(<<"_affiliations">>, Data, [])). + +convert_room_config(Data) -> + Config = proplists:get_value(<<"_data">>, Data, []), + Pass = case proplists:get_value(<<"password">>, Config, <<"">>) of + <<"">> -> + []; + Password -> + [{password_protected, true}, + {password, Password}] + end, + Subj = try jid:decode( + proplists:get_value( + <<"subject_from">>, Config, <<"">>)) of + #jid{lresource = Nick} when Nick /= <<"">> -> + [{subject, proplists:get_value(<<"subject">>, Config, <<"">>)}, + {subject_author, Nick}] + catch _:{bad_jid, _} -> + [] + end, + Anonymous = case proplists:get_value(<<"whois">>, Config, <<"moderators">>) of + <<"moderators">> -> true; + _ -> false + end, + [{affiliations, convert_room_affiliations(Data)}, + {allow_change_subj, proplists:get_bool(<<"changesubject">>, Config)}, + {mam, proplists:get_bool(<<"archiving">>, Config)}, + {description, proplists:get_value(<<"description">>, Config, <<"">>)}, + {members_only, proplists:get_bool(<<"members_only">>, Config)}, + {moderated, proplists:get_bool(<<"moderated">>, Config)}, + {persistent, proplists:get_bool(<<"persistent">>, Config)}, + {anonymous, Anonymous}] ++ Pass ++ Subj. + +convert_privacy_item({_, Item}) -> + Action = proplists:get_value(<<"action">>, Item, <<"allow">>), + Order = proplists:get_value(<<"order">>, Item, 0), + T = misc:binary_to_atom(proplists:get_value(<<"type">>, Item, <<"none">>)), + V = proplists:get_value(<<"value">>, Item, <<"">>), + MatchIQ = proplists:get_bool(<<"iq">>, Item), + MatchMsg = proplists:get_bool(<<"message">>, Item), + MatchPresIn = proplists:get_bool(<<"presence-in">>, Item), + MatchPresOut = proplists:get_bool(<<"presence-out">>, Item), + MatchAll = if (MatchIQ == false) and (MatchMsg == false) and + (MatchPresIn == false) and (MatchPresOut == false) -> + true; + true -> + false + end, + {Type, Value} = try case T of + none -> {T, none}; + group -> {T, V}; + jid -> {T, jid:tolower(jid:decode(V))}; + subscription -> {T, misc:binary_to_atom(V)} + end + catch _:_ -> + {none, none} + end, + #listitem{type = Type, + value = Value, + action = misc:binary_to_atom(Action), + order = erlang:trunc(Order), + match_all = MatchAll, + match_iq = MatchIQ, + match_message = MatchMsg, + match_presence_in = MatchPresIn, + match_presence_out = MatchPresOut}. + +decode_pubsub_host(Host) -> + try jid:decode(Host) of + #jid{luser = <<>>, lserver = LServer} -> LServer; + #jid{luser = LUser, lserver = LServer} -> {LUser, LServer, <<>>} + catch _:{bad_jid, _} -> bad_jid + end. + +node_type({_U, _S, _R}) -> <<"pep">>; +node_type(Host) -> hd(mod_pubsub:plugins(Host)). + +max_items(Config, Default) -> + case round(proplists:get_value(<<"max_items">>, Config, Default)) of + I when I =< 0 -> Default; + I -> I + end. + +convert_node_affiliations(Data) -> + lists:flatmap( + fun({J, Aff}) -> + try jid:decode(J) of + JID -> + [{JID, misc:binary_to_atom(Aff)}] + catch _:{bad_jid, _} -> + [] + end + end, proplists:get_value(<<"affiliations">>, Data, [])). + +convert_node_subscriptions(Data) -> + lists:flatmap( + fun({J, true}) -> + try jid:decode(J) of + JID -> + [jid:tolower(JID)] + catch _:{bad_jid, _} -> + [] + end; + (_) -> + [] + end, proplists:get_value(<<"subscribers">>, Data, [])). + +convert_node_items(Host, Data) -> + Authors = proplists:get_value(<<"data_author">>, Data, []), + lists:flatmap( + fun({ItemId, Item}) -> + try jid:decode(proplists:get_value(ItemId, Authors, Host)) of + JID -> + [El] = deserialize(Item), + [{ItemId, JID, El#xmlel.children}] + catch _:{bad_jid, _} -> + [] + end + end, proplists:get_value(<<"data">>, Data, [])). + +convert_node_config(Host, Data) -> + Config = proplists:get_value(<<"config">>, Data, []), + [{affiliations, convert_node_affiliations(Data)}, + {subscriptions, convert_node_subscriptions(Data)}, + {owner, jid:decode(proplists:get_value(<<"creator">>, Config, Host))}, + {items, convert_node_items(Host, Data)}, + {options, [ + {deliver_notifications, + proplists:get_value(<<"deliver_notifications">>, Config, true)}, + {deliver_payloads, + proplists:get_value(<<"deliver_payloads">>, Config, true)}, + {persist_items, + proplists:get_value(<<"persist_items">>, Config, true)}, + {max_items, + max_items(Config, 10)}, + {access_model, + misc:binary_to_atom(proplists:get_value(<<"access_model">>, Config, <<"open">>))}, + {publish_model, + misc:binary_to_atom(proplists:get_value(<<"publish_model">>, Config, <<"publishers">>))}, + {title, + proplists:get_value(<<"title">>, Config, <<"">>)} + ]} + ]. + +el_to_offline_msg(LUser, LServer, #xmlel{attrs = Attrs} = El) -> + try + TS = xmpp_util:decode_timestamp( + fxml:get_attr_s(<<"stamp">>, Attrs)), + Attrs1 = lists:filter( + fun({<<"stamp">>, _}) -> false; + ({<<"stamp_legacy">>, _}) -> false; + (_) -> true + end, Attrs), + El1 = El#xmlel{attrs = Attrs1}, + case xmpp:decode(El1, ?NS_CLIENT, [ignore_els]) of + #message{from = #jid{} = From, to = #jid{} = To} = Packet -> + [#offline_msg{ + us = {LUser, LServer}, + timestamp = TS, + expire = never, + from = From, + to = To, + packet = Packet}]; + _ -> + [] + end + catch _:{bad_timestamp, _} -> + []; + _:{bad_jid, _} -> + []; + _:{xmpp_codec, _} -> + [] + end. + +find_serverhost(Host) -> + [ServerHost] = + lists:filter( + fun(ServerHost) -> + case gen_mod:is_loaded(ServerHost, mod_muc) of + true -> + lists:member(Host, gen_mod:get_module_opt_hosts(ServerHost, mod_muc)); + false -> + false + end + end, ejabberd_option:hosts()), + ServerHost. + +deserialize(L) -> + deserialize(L, #xmlel{}, []). + +deserialize([{Other, _}|T], El, Acc) + when (Other == <<"key">>) + or (Other == <<"when">>) + or (Other == <<"with">>) -> + deserialize(T, El, Acc); +deserialize([{<<"attr">>, Attrs}|T], El, Acc) -> + deserialize(T, El#xmlel{attrs = Attrs ++ El#xmlel.attrs}, Acc); +deserialize([{<<"name">>, Name}|T], El, Acc) -> + deserialize(T, El#xmlel{name = Name}, Acc); +deserialize([{_, S}|T], #xmlel{children = Els} = El, Acc) when is_binary(S) -> + deserialize(T, El#xmlel{children = [{xmlcdata, S}|Els]}, Acc); +deserialize([{_, L}|T], #xmlel{children = Els} = El, Acc) when is_list(L) -> + deserialize(T, El#xmlel{children = deserialize(L) ++ Els}, Acc); +deserialize([], #xmlel{children = Els} = El, Acc) -> + [El#xmlel{children = lists:reverse(Els)}|Acc]. diff --git a/src/proxy_protocol.erl b/src/proxy_protocol.erl new file mode 100644 index 000000000..4ce0d31b4 --- /dev/null +++ b/src/proxy_protocol.erl @@ -0,0 +1,184 @@ +%%%---------------------------------------------------------------------- +%%% File : ejabberd_http.erl +%%% Author : Paweł Chmielowski +%%% Purpose : +%%% Created : 27 Nov 2018 by Paweł Chmielowski +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(proxy_protocol). +-author("pawel@process-one.net"). + +%% API +-export([decode/3]). + +decode(SockMod, Socket, Timeout) -> + V = SockMod:recv(Socket, 6, Timeout), + case V of + {ok, <<"PROXY ">>} -> + decode_v1(SockMod, Socket, Timeout); + {ok, <<16#0d, 16#0a, 16#0d, 16#0a, 16#00, 16#0d>>} -> + decode_v2(SockMod, Socket, Timeout); + _ -> + {error, eproto} + end. + +decode_v1(SockMod, Socket, Timeout) -> + case read_until_rn(SockMod, Socket, <<>>, false, Timeout) of + {error, _} = Err -> + Err; + Val -> + case binary:split(Val, <<" ">>, [global]) of + [<<"TCP4">>, SAddr, DAddr, SPort, DPort] -> + try {inet_parse:ipv4strict_address(binary_to_list(SAddr)), + inet_parse:ipv4strict_address(binary_to_list(DAddr)), + binary_to_integer(SPort), + binary_to_integer(DPort)} + of + {{ok, DA}, {ok, SA}, DP, SP} -> + {{SA, SP}, {DA, DP}}; + _ -> + {error, eproto} + catch + error:badarg -> + {error, eproto} + end; + [<<"TCP6">>, SAddr, DAddr, SPort, DPort] -> + try {inet_parse:ipv6strict_address(binary_to_list(SAddr)), + inet_parse:ipv6strict_address(binary_to_list(DAddr)), + binary_to_integer(SPort), + binary_to_integer(DPort)} + of + {{ok, DA}, {ok, SA}, DP, SP} -> + {{SA, SP}, {DA, DP}}; + _ -> + {error, eproto} + catch + error:badarg -> + {error, eproto} + end; + [<<"UNKNOWN">> | _] -> + {undefined, undefined} + end + end. + +decode_v2(SockMod, Socket, Timeout) -> + case SockMod:recv(Socket, 10, Timeout) of + {error, _} = Err -> + Err; + {ok, <<16#0a, 16#51, 16#55, 16#49, 16#54, 16#0a, + 2:4, Command:4, Transport:8, AddrLen:16/big-unsigned-integer>>} -> + case SockMod:recv(Socket, AddrLen, Timeout) of + {error, _} = Err -> + Err; + {ok, Data} -> + case Command of + 0 -> + case {inet:sockname(Socket), inet:peername(Socket)} of + {{ok, SA}, {ok, DA}} -> + {SA, DA}; + {{error, _} = E, _} -> + E; + {_, {error, _} = E} -> + E + end; + 1 -> + case Transport of + % UNSPEC or UNIX + V when V == 0; V == 16#31; V == 16#32 -> + {{unknown, unknown}, {unknown, unknown}}; + % IPV4 over TCP or UDP + V when V == 16#11; V == 16#12 -> + case Data of + <> -> + {{{S1, S2, S3, S4}, SP}, + {{D1, D2, D3, D4}, DP}}; + _ -> + {error, eproto} + end; + % IPV6 over TCP or UDP + V when V == 16#21; V == 16#22 -> + case Data of + <> -> + {{{S1, S2, S3, S4, S5, S6, S7, S8}, SP}, + {{D1, D2, D3, D4, D5, D6, D7, D8}, DP}}; + _ -> + {error, eproto} + end + end; + _ -> + {error, eproto} + end + end; + <<16#0a, 16#51, 16#55, 16#49, 16#54, 16#0a, _/binary>> -> + {error, eproto}; + _ -> + {error, eproto} + end. + +read_until_rn(_SockMod, _Socket, Data, _, _) when size(Data) > 107 -> + {error, eproto}; +read_until_rn(SockMod, Socket, Data, true, Timeout) -> + case SockMod:recv(Socket, 1, Timeout) of + {ok, <<"\n">>} -> + Data; + {ok, <<"\r">>} -> + read_until_rn(SockMod, Socket, <>, + true, Timeout); + {ok, Other} -> + read_until_rn(SockMod, Socket, <>, + false, Timeout); + {error, _} = Err -> + Err + end; +read_until_rn(SockMod, Socket, Data, false, Timeout) -> + case SockMod:recv(Socket, 2, Timeout) of + {ok, <<"\r\n">>} -> + Data; + {ok, <>} -> + read_until_rn(SockMod, Socket, <>, + true, Timeout); + {ok, Other} -> + read_until_rn(SockMod, Socket, <>, + false, Timeout); + {error, _} = Err -> + Err + end. diff --git a/src/pubsub_db_odbc.erl b/src/pubsub_db_odbc.erl deleted file mode 100644 index f22ee1968..000000000 --- a/src/pubsub_db_odbc.erl +++ /dev/null @@ -1,151 +0,0 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. -%%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. -%%% -%%% The Initial Developer of the Original Code is ProcessOne. -%%% Portions created by ProcessOne are Copyright 2006-2015, ProcessOne -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2015, ProcessOne. -%%% -%%% @author Pablo Polvorin -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== --module(pubsub_db_odbc). - --author("pablo.polvorin@process-one.net"). - --include("pubsub.hrl"). - --export([add_subscription/1, read_subscription/1, - delete_subscription/1, update_subscription/1]). - -%% TODO: Those -spec lines produce errors in old Erlang versions. -%% They can be enabled again in ejabberd 3.0 because it uses R12B or higher. -%% -spec read_subscription(SubID :: string()) -> {ok, #pubsub_subscription{}} | notfound. -read_subscription(SubID) -> - case - ejabberd_odbc:sql_query_t([<<"select opt_name, opt_value from pubsub_subscr" - "iption_opt where subid = '">>, - ejabberd_odbc:escape(SubID), <<"'">>]) - of - {selected, [<<"opt_name">>, <<"opt_value">>], []} -> - notfound; - {selected, [<<"opt_name">>, <<"opt_value">>], - Options} -> - {ok, - #pubsub_subscription{subid = SubID, - options = - lists:map(fun subscription_opt_from_odbc/1, - Options)}} - end. - -%% -spec delete_subscription(SubID :: string()) -> ok. -delete_subscription(SubID) -> -%% -spec update_subscription(#pubsub_subscription{}) -> ok . -%% -spec add_subscription(#pubsub_subscription{}) -> ok. -%% -------------- Internal utilities ----------------------- - ejabberd_odbc:sql_query_t([<<"delete from pubsub_subscription_opt " - "where subid = '">>, - ejabberd_odbc:escape(SubID), <<"'">>]), - ok. - -update_subscription(#pubsub_subscription{subid = - SubId} = - Sub) -> - delete_subscription(SubId), add_subscription(Sub). - -add_subscription(#pubsub_subscription{subid = SubId, - options = Opts}) -> - EscapedSubId = ejabberd_odbc:escape(SubId), - lists:foreach(fun (Opt) -> - {OdbcOptName, OdbcOptValue} = - subscription_opt_to_odbc(Opt), - ejabberd_odbc:sql_query_t([<<"insert into pubsub_subscription_opt(subid, " - "opt_name, opt_value)values ('">>, - EscapedSubId, <<"','">>, - OdbcOptName, <<"','">>, - OdbcOptValue, <<"')">>]) - end, - Opts), - ok. - -subscription_opt_from_odbc({<<"DELIVER">>, Value}) -> - {deliver, odbc_to_boolean(Value)}; -subscription_opt_from_odbc({<<"DIGEST">>, Value}) -> - {digest, odbc_to_boolean(Value)}; -subscription_opt_from_odbc({<<"DIGEST_FREQUENCY">>, - Value}) -> - {digest_frequency, odbc_to_integer(Value)}; -subscription_opt_from_odbc({<<"EXPIRE">>, Value}) -> - {expire, odbc_to_timestamp(Value)}; -subscription_opt_from_odbc({<<"INCLUDE_BODY">>, - Value}) -> - {include_body, odbc_to_boolean(Value)}; -%%TODO: might be > than 1 show_values value??. -%% need to use compact all in only 1 opt. -subscription_opt_from_odbc({<<"SHOW_VALUES">>, - Value}) -> - {show_values, Value}; -subscription_opt_from_odbc({<<"SUBSCRIPTION_TYPE">>, - Value}) -> - {subscription_type, - case Value of - <<"items">> -> items; - <<"nodes">> -> nodes - end}; -subscription_opt_from_odbc({<<"SUBSCRIPTION_DEPTH">>, - Value}) -> - {subscription_depth, - case Value of - <<"all">> -> all; - N -> odbc_to_integer(N) - end}. - -subscription_opt_to_odbc({deliver, Bool}) -> - {<<"DELIVER">>, boolean_to_odbc(Bool)}; -subscription_opt_to_odbc({digest, Bool}) -> - {<<"DIGEST">>, boolean_to_odbc(Bool)}; -subscription_opt_to_odbc({digest_frequency, Int}) -> - {<<"DIGEST_FREQUENCY">>, integer_to_odbc(Int)}; -subscription_opt_to_odbc({expire, Timestamp}) -> - {<<"EXPIRE">>, timestamp_to_odbc(Timestamp)}; -subscription_opt_to_odbc({include_body, Bool}) -> - {<<"INCLUDE_BODY">>, boolean_to_odbc(Bool)}; -subscription_opt_to_odbc({show_values, Values}) -> - {<<"SHOW_VALUES">>, Values}; -subscription_opt_to_odbc({subscription_type, Type}) -> - {<<"SUBSCRIPTION_TYPE">>, - case Type of - items -> <<"items">>; - nodes -> <<"nodes">> - end}; -subscription_opt_to_odbc({subscription_depth, Depth}) -> - {<<"SUBSCRIPTION_DEPTH">>, - case Depth of - all -> <<"all">>; - N -> integer_to_odbc(N) - end}. - -integer_to_odbc(N) -> - iolist_to_binary(integer_to_list(N)). - -boolean_to_odbc(true) -> <<"1">>; -boolean_to_odbc(false) -> <<"0">>. - -timestamp_to_odbc(T) -> jlib:now_to_utc_string(T). - -odbc_to_integer(N) -> jlib:binary_to_integer(N). - -odbc_to_boolean(B) -> B == <<"1">>. - -odbc_to_timestamp(T) -> - jlib:datetime_string_to_timestamp(T). diff --git a/src/pubsub_db_sql.erl b/src/pubsub_db_sql.erl new file mode 100644 index 000000000..7a789e9ea --- /dev/null +++ b/src/pubsub_db_sql.erl @@ -0,0 +1,198 @@ +%%%---------------------------------------------------------------------- +%%% File : pubsub_db_sql.erl +%%% Author : Pablo Polvorin +%%% Purpose : Provide helpers for PubSub ODBC backend +%%% Created : 7 Aug 2009 by Pablo Polvorin +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(pubsub_db_sql). + + +-author("pablo.polvorin@process-one.net"). + +-include("pubsub.hrl"). +-include("ejabberd_sql_pt.hrl"). + +-export([add_subscription/1, read_subscription/1, + delete_subscription/1, update_subscription/1]). +-export([export/1]). + +-spec read_subscription(SubID :: mod_pubsub:subId()) -> {ok, #pubsub_subscription{}} | notfound. +read_subscription(SubID) -> + case + ejabberd_sql:sql_query_t( + ?SQL("select @(opt_name)s, @(opt_value)s from pubsub_subscription_opt where subid = %(SubID)s")) + of + {selected, []} -> + notfound; + {selected, Options} -> + {ok, + #pubsub_subscription{subid = SubID, + options = lists:map(fun subscription_opt_from_sql/1, Options)}} + end. + +-spec delete_subscription(SubID :: mod_pubsub:subId()) -> ok. +delete_subscription(SubID) -> + ejabberd_sql:sql_query_t( + ?SQL("delete from pubsub_subscription_opt " + "where subid = %(SubID)s")), + ok. + +-spec update_subscription(#pubsub_subscription{}) -> ok. +update_subscription(#pubsub_subscription{subid = SubId} = Sub) -> + delete_subscription(SubId), add_subscription(Sub). + +-spec add_subscription(#pubsub_subscription{}) -> ok. +add_subscription(#pubsub_subscription{subid = SubId, options = Opts}) -> + lists:foreach( + fun(Opt) -> + {OdbcOptName, OdbcOptValue} = subscription_opt_to_sql(Opt), + ejabberd_sql:sql_query_t( + ?SQL("insert into pubsub_subscription_opt(subid, " + "opt_name, opt_value) values " + "(%(SubId)s, %(OdbcOptName)s, %(OdbcOptValue)s)")) + end, + Opts), + ok. + +subscription_opt_from_sql({<<"DELIVER">>, Value}) -> + {deliver, sql_to_boolean(Value)}; +subscription_opt_from_sql({<<"DIGEST">>, Value}) -> + {digest, sql_to_boolean(Value)}; +subscription_opt_from_sql({<<"DIGEST_FREQUENCY">>, Value}) -> + {digest_frequency, sql_to_integer(Value)}; +subscription_opt_from_sql({<<"EXPIRE">>, Value}) -> + {expire, sql_to_timestamp(Value)}; +subscription_opt_from_sql({<<"INCLUDE_BODY">>, Value}) -> + {include_body, sql_to_boolean(Value)}; +%%TODO: might be > than 1 show_values value??. +%% need to use compact all in only 1 opt. +subscription_opt_from_sql({<<"SHOW_VALUES">>, Value}) -> + {show_values, Value}; +subscription_opt_from_sql({<<"SUBSCRIPTION_TYPE">>, Value}) -> + {subscription_type, + case Value of + <<"items">> -> items; + <<"nodes">> -> nodes + end}; +subscription_opt_from_sql({<<"SUBSCRIPTION_DEPTH">>, Value}) -> + {subscription_depth, + case Value of + <<"all">> -> all; + N -> sql_to_integer(N) + end}. + +subscription_opt_to_sql({deliver, Bool}) -> + {<<"DELIVER">>, boolean_to_sql(Bool)}; +subscription_opt_to_sql({digest, Bool}) -> + {<<"DIGEST">>, boolean_to_sql(Bool)}; +subscription_opt_to_sql({digest_frequency, Int}) -> + {<<"DIGEST_FREQUENCY">>, integer_to_sql(Int)}; +subscription_opt_to_sql({expire, Timestamp}) -> + {<<"EXPIRE">>, timestamp_to_sql(Timestamp)}; +subscription_opt_to_sql({include_body, Bool}) -> + {<<"INCLUDE_BODY">>, boolean_to_sql(Bool)}; +subscription_opt_to_sql({show_values, Values}) -> + {<<"SHOW_VALUES">>, Values}; +subscription_opt_to_sql({subscription_type, Type}) -> + {<<"SUBSCRIPTION_TYPE">>, + case Type of + items -> <<"items">>; + nodes -> <<"nodes">> + end}; +subscription_opt_to_sql({subscription_depth, Depth}) -> + {<<"SUBSCRIPTION_DEPTH">>, + case Depth of + all -> <<"all">>; + N -> integer_to_sql(N) + end}. + +integer_to_sql(N) -> integer_to_binary(N). + +boolean_to_sql(true) -> <<"1">>; +boolean_to_sql(false) -> <<"0">>. + +timestamp_to_sql(T) -> xmpp_util:encode_timestamp(T). + +sql_to_integer(N) -> binary_to_integer(N). + +sql_to_boolean(B) -> B == <<"1">>. + +sql_to_timestamp(T) -> xmpp_util:decode_timestamp(T). + +export(_Server) -> + [{pubsub_node, + fun(_Host, #pubsub_node{nodeid = {Host, Node}, id = Nidx, + parents = Parents, type = Type, + options = Options}) -> + H = node_flat_sql:encode_host(Host), + Parent = case Parents of + [] -> <<>>; + [First | _] -> First + end, + [?SQL("delete from pubsub_node where nodeid=%(Nidx)d;"), + ?SQL("delete from pubsub_node_option where nodeid=%(Nidx)d;"), + ?SQL("delete from pubsub_node_owner where nodeid=%(Nidx)d;"), + ?SQL("delete from pubsub_state where nodeid=%(Nidx)d;"), + ?SQL("delete from pubsub_item where nodeid=%(Nidx)d;"), + ?SQL("insert into pubsub_node(host,node,nodeid,parent,plugin)" + " values (%(H)s, %(Node)s, %(Nidx)d, %(Parent)s, %(Type)s);")] + ++ lists:map( + fun ({Key, Value}) -> + SKey = iolist_to_binary(atom_to_list(Key)), + SValue = misc:term_to_expr(Value), + ?SQL("insert into pubsub_node_option(nodeid,name,val)" + " values (%(Nidx)d, %(SKey)s, %(SValue)s);") + end, Options); + (_Host, _R) -> + [] + end}, + {pubsub_state, + fun(_Host, #pubsub_state{stateid = {JID, Nidx}, + affiliation = Affiliation, + subscriptions = Subscriptions}) -> + J = jid:encode(JID), + S = node_flat_sql:encode_subscriptions(Subscriptions), + A = node_flat_sql:encode_affiliation(Affiliation), + [?SQL("insert into pubsub_state(nodeid,jid,affiliation,subscriptions)" + " values (%(Nidx)d, %(J)s, %(A)s, %(S)s);")]; + (_Host, _R) -> + [] + end}, + {pubsub_item, + fun(_Host, #pubsub_item{itemid = {ItemId, Nidx}, + creation = {C, _}, + modification = {M, JID}, + payload = Payload}) -> + P = jid:encode(JID), + XML = str:join([fxml:element_to_binary(X) || X<-Payload], <<>>), + SM = encode_now(M), + SC = encode_now(C), + [?SQL("insert into pubsub_item(itemid,nodeid,creation,modification,publisher,payload)" + " values (%(ItemId)s, %(Nidx)d, %(SC)s, %(SM)s, %(P)s, %(XML)s);")]; + (_Host, _R) -> + [] + end}]. + +encode_now({T1, T2, T3}) -> + <<(misc:i2l(T1, 6))/binary, ":", + (misc:i2l(T2, 6))/binary, ":", + (misc:i2l(T3, 6))/binary>>. diff --git a/src/pubsub_index.erl b/src/pubsub_index.erl index 94efa0e96..0c34ea63b 100644 --- a/src/pubsub_index.erl +++ b/src/pubsub_index.erl @@ -1,35 +1,32 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. -%%% -%%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. -%%% -%%% -%%% The Initial Developer of the Original Code is ProcessOne. -%%% Portions created by ProcessOne are Copyright 2006-2015, ProcessOne -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2015, ProcessOne. +%%%---------------------------------------------------------------------- +%%% File : pubsub_index.erl +%%% Author : Christophe Romain +%%% Purpose : Provide uniq integer index for pubsub node +%%% Created : 30 Apr 2009 by Christophe Romain %%% %%% -%%% @copyright 2006-2015 ProcessOne -%%% @author Christophe Romain -%%% [http://www.process-one.net/] -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== +%%% ejabberd, Copyright (C) 2002-2025 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. +%%% +%%%---------------------------------------------------------------------- %% important note: %% new/1 and free/2 MUST be called inside a transaction bloc -module(pubsub_index). - -author('christophe.romain@process-one.net'). -include("pubsub.hrl"). @@ -37,31 +34,31 @@ -export([init/3, new/1, free/2]). init(_Host, _ServerHost, _Opts) -> - mnesia:create_table(pubsub_index, - [{disc_copies, [node()]}, - {attributes, record_info(fields, pubsub_index)}]). + ejabberd_mnesia:create(?MODULE, pubsub_index, + [{disc_copies, [node()]}, + {attributes, record_info(fields, pubsub_index)}]). new(Index) -> case mnesia:read({pubsub_index, Index}) of - [I] -> - case I#pubsub_index.free of - [] -> - Id = I#pubsub_index.last + 1, - mnesia:write(I#pubsub_index{last = Id}), - Id; - [Id | Free] -> - mnesia:write(I#pubsub_index{free = Free}), Id - end; - _ -> - mnesia:write(#pubsub_index{index = Index, last = 1, - free = []}), - 1 + [I] -> + case I#pubsub_index.free of + [] -> + Id = I#pubsub_index.last + 1, + mnesia:write(I#pubsub_index{last = Id}), + Id; + [Id | Free] -> + mnesia:write(I#pubsub_index{free = Free}), Id + end; + _ -> + mnesia:write(#pubsub_index{index = Index, last = 1, free = []}), + 1 end. free(Index, Id) -> case mnesia:read({pubsub_index, Index}) of - [I] -> - Free = I#pubsub_index.free, - mnesia:write(I#pubsub_index{free = [Id | Free]}); - _ -> ok + [I] -> + Free = I#pubsub_index.free, + mnesia:write(I#pubsub_index{free = [Id | Free]}); + _ -> + ok end. diff --git a/src/pubsub_migrate.erl b/src/pubsub_migrate.erl new file mode 100644 index 000000000..8d9fc6198 --- /dev/null +++ b/src/pubsub_migrate.erl @@ -0,0 +1,532 @@ +%%%---------------------------------------------------------------------- +%%% File : pubsub_migrate.erl +%%% Author : Christophe Romain +%%% Purpose : Migration/Upgrade code put out of mod_pubsub +%%% Created : 26 Jul 2014 by Christophe Romain +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(pubsub_migrate). +-dialyzer({no_return, report_and_stop/2}). +-include("pubsub.hrl"). +-include("logger.hrl"). + +-export([update_node_database/2, update_state_database/2]). +-export([update_item_database/2, update_lastitem_database/2]). + +update_item_database(_Host, _ServerHost) -> + convert_list_items(). + +update_node_database(Host, ServerHost) -> + mnesia:del_table_index(pubsub_node, type), + mnesia:del_table_index(pubsub_node, parentid), + case catch mnesia:table_info(pubsub_node, attributes) of + [host_node, host_parent, info] -> + ?INFO_MSG("Upgrading pubsub nodes table...", []), + F = fun () -> + {Result, LastIdx} = lists:foldl(fun ({pubsub_node, + NodeId, ParentId, + {nodeinfo, Items, + Options, + Entities}}, + {RecList, + NodeIdx}) -> + ItemsList = + lists:foldl(fun + ({item, + IID, + Publisher, + Payload}, + Acc) -> + C = + {unknown, + Publisher}, + M = + {erlang:timestamp(), + Publisher}, + mnesia:write(#pubsub_item{itemid + = + {IID, + NodeIdx}, + creation + = + C, + modification + = + M, + payload + = + Payload}), + [{Publisher, + IID} + | Acc] + end, + [], + Items), + Owners = + dict:fold(fun + (JID, + {entity, + Aff, + Sub}, + Acc) -> + UsrItems = + lists:foldl(fun + ({P, + I}, + IAcc) -> + case + P + of + JID -> + [I + | IAcc]; + _ -> + IAcc + end + end, + [], + ItemsList), + mnesia:write({pubsub_state, + {JID, + NodeIdx}, + UsrItems, + Aff, + Sub}), + case + Aff + of + owner -> + [JID + | Acc]; + _ -> + Acc + end + end, + [], + Entities), + mnesia:delete({pubsub_node, + NodeId}), + {[#pubsub_node{nodeid + = + NodeId, + id + = + NodeIdx, + parents + = + [element(2, + ParentId)], + owners + = + Owners, + options + = + Options} + | RecList], + NodeIdx + 1} + end, + {[], 1}, + mnesia:match_object({pubsub_node, + {Host, + '_'}, + '_', + '_'})), + mnesia:write(#pubsub_index{index = node, last = LastIdx, + free = []}), + Result + end, + {atomic, NewRecords} = mnesia:transaction(F), + {atomic, ok} = mnesia:delete_table(pubsub_node), + {atomic, ok} = ejabberd_mnesia:create(?MODULE, pubsub_node, + [{disc_copies, [node()]}, + {attributes, + record_info(fields, + pubsub_node)}]), + FNew = fun () -> + lists:foreach(fun (Record) -> mnesia:write(Record) end, + NewRecords) + end, + case mnesia:transaction(FNew) of + {atomic, Result} -> + ?INFO_MSG("Pubsub nodes table upgraded: ~p", + [Result]); + {aborted, Reason} -> + ?ERROR_MSG("Problem upgrading Pubsub nodes table:~n~p", + [Reason]) + end; + [nodeid, parentid, type, owners, options] -> + F = fun ({pubsub_node, NodeId, {_, Parent}, Type, + Owners, Options}) -> + #pubsub_node{nodeid = NodeId, id = 0, + parents = [Parent], type = Type, + owners = Owners, options = Options} + end, + mnesia:transform_table(pubsub_node, F, + [nodeid, id, parents, type, owners, options]), + FNew = fun () -> + LastIdx = lists:foldl(fun (#pubsub_node{nodeid = + NodeId} = + PubsubNode, + NodeIdx) -> + mnesia:write(PubsubNode#pubsub_node{id + = + NodeIdx}), + lists:foreach(fun + (#pubsub_state{stateid + = + StateId} = + State) -> + {JID, + _} = + StateId, + mnesia:delete({pubsub_state, + StateId}), + mnesia:write(State#pubsub_state{stateid + = + {JID, + NodeIdx}}) + end, + mnesia:match_object(#pubsub_state{stateid + = + {'_', + NodeId}, + _ + = + '_'})), + lists:foreach(fun + (#pubsub_item{itemid + = + ItemId} = + Item) -> + {IID, + _} = + ItemId, + {M1, + M2} = + Item#pubsub_item.modification, + {C1, + C2} = + Item#pubsub_item.creation, + mnesia:delete({pubsub_item, + ItemId}), + mnesia:write(Item#pubsub_item{itemid + = + {IID, + NodeIdx}, + modification + = + {M2, + M1}, + creation + = + {C2, + C1}}) + end, + mnesia:match_object(#pubsub_item{itemid + = + {'_', + NodeId}, + _ + = + '_'})), + NodeIdx + 1 + end, + 1, + mnesia:match_object({pubsub_node, + {Host, '_'}, + '_', '_', + '_', '_', + '_'}) + ++ + mnesia:match_object({pubsub_node, + {{'_', + ServerHost, + '_'}, + '_'}, + '_', '_', + '_', '_', + '_'})), + mnesia:write(#pubsub_index{index = node, + last = LastIdx, free = []}) + end, + case mnesia:transaction(FNew) of + {atomic, Result} -> + rename_default_nodeplugin(), + ?INFO_MSG("Pubsub nodes table upgraded: ~p", + [Result]); + {aborted, Reason} -> + ?ERROR_MSG("Problem upgrading Pubsub nodes table:~n~p", + [Reason]) + end; + [nodeid, id, parent, type, owners, options] -> + F = fun ({pubsub_node, NodeId, Id, Parent, Type, Owners, + Options}) -> + #pubsub_node{nodeid = NodeId, id = Id, + parents = [Parent], type = Type, + owners = Owners, options = Options} + end, + mnesia:transform_table(pubsub_node, F, + [nodeid, id, parents, type, owners, options]), + rename_default_nodeplugin(); + _ -> ok + end, + convert_list_nodes(). + +rename_default_nodeplugin() -> + lists:foreach(fun (Node) -> + mnesia:dirty_write(Node#pubsub_node{type = + <<"hometree">>}) + end, + mnesia:dirty_match_object(#pubsub_node{type = + <<"default">>, + _ = '_'})). + +update_state_database(_Host, _ServerHost) -> +% useless starting from ejabberd 17.04 +% case catch mnesia:table_info(pubsub_state, attributes) of +% [stateid, nodeidx, items, affiliation, subscriptions] -> +% ?INFO_MSG("Upgrading pubsub states table...", []), +% F = fun ({pubsub_state, {{U,S,R}, NodeID}, _NodeIdx, Items, Aff, Sub}, Acc) -> +% JID = {U,S,R}, +% Subs = case Sub of +% none -> +% []; +% [] -> +% []; +% _ -> +% SubID = pubsub_subscription:make_subid(), +% [{Sub, SubID}] +% end, +% NewState = #pubsub_state{stateid = {JID, NodeID}, +% items = Items, +% affiliation = Aff, +% subscriptions = Subs}, +% [NewState | Acc] +% end, +% {atomic, NewRecs} = mnesia:transaction(fun mnesia:foldl/3, +% [F, [], pubsub_state]), +% {atomic, ok} = mnesia:delete_table(pubsub_state), +% {atomic, ok} = ejabberd_mnesia:create(?MODULE, pubsub_state, +% [{disc_copies, [node()]}, +% {attributes, record_info(fields, pubsub_state)}]), +% FNew = fun () -> +% lists:foreach(fun mnesia:write/1, NewRecs) +% end, +% case mnesia:transaction(FNew) of +% {atomic, Result} -> +% ?INFO_MSG("Pubsub states table upgraded: ~p", +% [Result]); +% {aborted, Reason} -> +% ?ERROR_MSG("Problem upgrading Pubsub states table:~n~p", +% [Reason]) +% end; +% _ -> +% ok +% end, + convert_list_subscriptions(), + convert_list_states(). + +update_lastitem_database(_Host, _ServerHost) -> + convert_list_lasts(). + +%% binarization from old 2.1.x + +convert_list_items() -> + convert_list_records( + pubsub_item, + record_info(fields, pubsub_item), + fun(#pubsub_item{itemid = {I, _}}) -> I end, + fun(#pubsub_item{itemid = {I, Nidx}, + creation = {C, CKey}, + modification = {M, MKey}, + payload = Els} = R) -> + R#pubsub_item{itemid = {bin(I), Nidx}, + creation = {C, binusr(CKey)}, + modification = {M, binusr(MKey)}, + payload = [fxml:to_xmlel(El) || El<-Els]} + end). + +convert_list_states() -> + convert_list_records( + pubsub_state, + record_info(fields, pubsub_state), + fun(#pubsub_state{stateid = {{U,_,_}, _}}) -> U end, + fun(#pubsub_state{stateid = {U, Nidx}, + items = Is, + affiliation = A, + subscriptions = Ss} = R) -> + R#pubsub_state{stateid = {binusr(U), Nidx}, + items = [bin(I) || I<-Is], + affiliation = A, + subscriptions = [{S,bin(Sid)} || {S,Sid}<-Ss]} + end). + +convert_list_nodes() -> + convert_list_records( + pubsub_node, + record_info(fields, pubsub_node), + fun(#pubsub_node{nodeid = {{U,_,_}, _}}) -> U; + (#pubsub_node{nodeid = {H, _}}) -> H end, + fun(#pubsub_node{nodeid = {H, N}, + id = I, + parents = Ps, + type = T, + owners = Os, + options = Opts} = R) -> + R#pubsub_node{nodeid = {binhost(H), bin(N)}, + id = I, + parents = [bin(P) || P<-Ps], + type = bin(T), + owners = [binusr(O) || O<-Os], + options = Opts} + end). + +convert_list_subscriptions() -> + [convert_list_records( + pubsub_subscription, + record_info(fields, pubsub_subscription), + fun(#pubsub_subscription{subid = I}) -> I end, + fun(#pubsub_subscription{subid = I, + options = Opts} = R) -> + R#pubsub_subscription{subid = bin(I), + options = Opts} + end) || lists:member(pubsub_subscription, mnesia:system_info(tables))]. + +convert_list_lasts() -> + convert_list_records( + pubsub_last_item, + record_info(fields, pubsub_last_item), + fun(#pubsub_last_item{itemid = I}) -> I end, + fun(#pubsub_last_item{itemid = I, + nodeid = Nidx, + creation = {C, CKey}, + payload = Payload} = R) -> + R#pubsub_last_item{itemid = bin(I), + nodeid = Nidx, + creation = {C, binusr(CKey)}, + payload = fxml:to_xmlel(Payload)} + end). + +%% internal tools + +convert_list_records(Tab, Fields, DetectFun, ConvertFun) -> + case mnesia:table_info(Tab, attributes) of + Fields -> + convert_table_to_binary( + Tab, Fields, set, DetectFun, ConvertFun); + _ -> + ?INFO_MSG("Recreating ~p table", [Tab]), + mnesia:transform_table(Tab, ignore, Fields), + convert_list_records(Tab, Fields, DetectFun, ConvertFun) + end. + +binhost({U,S,R}) -> binusr({U,S,R}); +binhost(L) -> bin(L). + +binusr({U,S,R}) -> {bin(U), bin(S), bin(R)}. + +bin(L) -> iolist_to_binary(L). + +%% The code should be updated to support new ejabberd_mnesia +%% transform functions (i.e. need_transform/1 and transform/1) +convert_table_to_binary(Tab, Fields, Type, DetectFun, ConvertFun) -> + case is_table_still_list(Tab, DetectFun) of + true -> + ?INFO_MSG("Converting '~ts' table from strings to binaries.", [Tab]), + TmpTab = list_to_atom(atom_to_list(Tab) ++ "_tmp_table"), + catch mnesia:delete_table(TmpTab), + case ejabberd_mnesia:create(?MODULE, TmpTab, + [{disc_only_copies, [node()]}, + {type, Type}, + {local_content, true}, + {record_name, Tab}, + {attributes, Fields}]) of + {atomic, ok} -> + mnesia:transform_table(Tab, ignore, Fields), + case mnesia:transaction( + fun() -> + mnesia:write_lock_table(TmpTab), + mnesia:foldl( + fun(R, _) -> + NewR = ConvertFun(R), + mnesia:dirty_write(TmpTab, NewR) + end, ok, Tab) + end) of + {atomic, ok} -> + mnesia:clear_table(Tab), + case mnesia:transaction( + fun() -> + mnesia:write_lock_table(Tab), + mnesia:foldl( + fun(R, _) -> + mnesia:dirty_write(R) + end, ok, TmpTab) + end) of + {atomic, ok} -> + mnesia:delete_table(TmpTab); + Err -> + report_and_stop(Tab, Err) + end; + Err -> + report_and_stop(Tab, Err) + end; + Err -> + report_and_stop(Tab, Err) + end; + false -> + ok + end. + +is_table_still_list(Tab, DetectFun) -> + is_table_still_list(Tab, DetectFun, mnesia:dirty_first(Tab)). + +is_table_still_list(_Tab, _DetectFun, '$end_of_table') -> + false; +is_table_still_list(Tab, DetectFun, Key) -> + Rs = mnesia:dirty_read(Tab, Key), + Res = lists:foldl(fun(_, true) -> + true; + (_, false) -> + false; + (R, _) -> + case DetectFun(R) of + '$next' -> + '$next'; + El -> + is_list(El) + end + end, '$next', Rs), + case Res of + true -> + true; + false -> + false; + '$next' -> + is_table_still_list(Tab, DetectFun, mnesia:dirty_next(Tab, Key)) + end. + +report_and_stop(Tab, Err) -> + ErrTxt = lists:flatten( + io_lib:format( + "Failed to convert '~ts' table to binary: ~p", + [Tab, Err])), + ?CRITICAL_MSG(ErrTxt, []), + ejabberd:halt(). diff --git a/src/pubsub_subscription.erl b/src/pubsub_subscription.erl index 1e3f18e7b..6db643af6 100644 --- a/src/pubsub_subscription.erl +++ b/src/pubsub_subscription.erl @@ -1,269 +1,176 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. +%%%---------------------------------------------------------------------- +%%% File : pubsub_subscription.erl +%%% Author : Brian Cully +%%% Purpose : Handle pubsub subscriptions options +%%% Created : 29 May 2009 by Brian Cully %%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. %%% -%%% The Initial Developer of the Original Code is ProcessOne. -%%% Portions created by ProcessOne are Copyright 2006-2015, ProcessOne -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2015, ProcessOne. +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% -%%% @author Brian Cully -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== +%%% 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(pubsub_subscription). -author("bjc@kublai.com"). %% API --export([init/0, subscribe_node/3, unsubscribe_node/3, - get_subscription/3, set_subscription/4, - get_options_xform/2, parse_options_xform/1]). +-export([init/3, subscribe_node/3, unsubscribe_node/3, + get_subscription/3, set_subscription/4, + make_subid/0, + get_options_xform/2, parse_options_xform/1]). % Internal function also exported for use in transactional bloc from pubsub plugins -export([add_subscription/3, delete_subscription/3, - read_subscription/3, write_subscription/4]). + read_subscription/3, write_subscription/4]). -include("pubsub.hrl"). - --include("jlib.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). +-include("translate.hrl"). -define(PUBSUB_DELIVER, <<"pubsub#deliver">>). - -define(PUBSUB_DIGEST, <<"pubsub#digest">>). - --define(PUBSUB_DIGEST_FREQUENCY, - <<"pubsub#digest_frequency">>). - +-define(PUBSUB_DIGEST_FREQUENCY, <<"pubsub#digest_frequency">>). -define(PUBSUB_EXPIRE, <<"pubsub#expire">>). - -define(PUBSUB_INCLUDE_BODY, <<"pubsub#include_body">>). - -define(PUBSUB_SHOW_VALUES, <<"pubsub#show-values">>). - --define(PUBSUB_SUBSCRIPTION_TYPE, - <<"pubsub#subscription_type">>). - --define(PUBSUB_SUBSCRIPTION_DEPTH, - <<"pubsub#subscription_depth">>). - --define(DELIVER_LABEL, - <<"Whether an entity wants to receive or " - "disable notifications">>). - --define(DIGEST_LABEL, - <<"Whether an entity wants to receive digests " - "(aggregations) of notifications or all " - "notifications individually">>). - --define(DIGEST_FREQUENCY_LABEL, - <<"The minimum number of milliseconds between " - "sending any two notification digests">>). - --define(EXPIRE_LABEL, - <<"The DateTime at which a leased subscription " - "will end or has ended">>). - --define(INCLUDE_BODY_LABEL, - <<"Whether an entity wants to receive an " - "XMPP message body in addition to the " - "payload format">>). - --define(SHOW_VALUES_LABEL, - <<"The presence states for which an entity " - "wants to receive notifications">>). - --define(SUBSCRIPTION_TYPE_LABEL, - <<"Type of notification to receive">>). - --define(SUBSCRIPTION_DEPTH_LABEL, - <<"Depth from subscription for which to " - "receive notifications">>). - --define(SHOW_VALUE_AWAY_LABEL, - <<"XMPP Show Value of Away">>). - --define(SHOW_VALUE_CHAT_LABEL, - <<"XMPP Show Value of Chat">>). - --define(SHOW_VALUE_DND_LABEL, - <<"XMPP Show Value of DND (Do Not Disturb)">>). - --define(SHOW_VALUE_ONLINE_LABEL, - <<"Mere Availability in XMPP (No Show Value)">>). - --define(SHOW_VALUE_XA_LABEL, - <<"XMPP Show Value of XA (Extended Away)">>). - --define(SUBSCRIPTION_TYPE_VALUE_ITEMS_LABEL, - <<"Receive notification of new items only">>). - --define(SUBSCRIPTION_TYPE_VALUE_NODES_LABEL, - <<"Receive notification of new nodes only">>). - --define(SUBSCRIPTION_DEPTH_VALUE_ONE_LABEL, - <<"Receive notification from direct child " - "nodes only">>). - --define(SUBSCRIPTION_DEPTH_VALUE_ALL_LABEL, - <<"Receive notification from all descendent " - "nodes">>). +-define(PUBSUB_SUBSCRIPTION_TYPE, <<"pubsub#subscription_type">>). +-define(PUBSUB_SUBSCRIPTION_DEPTH, <<"pubsub#subscription_depth">>). +-define(DELIVER_LABEL, <<"Whether an entity wants to receive or disable notifications">>). +-define(DIGEST_LABEL, <<"Whether an entity wants to receive digests " + "(aggregations) of notifications or all notifications individually">>). +-define(DIGEST_FREQUENCY_LABEL, <<"The minimum number of milliseconds between " + "sending any two notification digests">>). +-define(EXPIRE_LABEL, <<"The DateTime at which a leased subscription will end or has ended">>). +-define(INCLUDE_BODY_LABEL, <<"Whether an entity wants to receive an " + "XMPP message body in addition to the payload format">>). +-define(SHOW_VALUES_LABEL, <<"The presence states for which an entity wants to receive notifications">>). +-define(SUBSCRIPTION_TYPE_LABEL, <<"Type of notification to receive">>). +-define(SUBSCRIPTION_DEPTH_LABEL, <<"Depth from subscription for which to receive notifications">>). +-define(SHOW_VALUE_AWAY_LABEL, <<"XMPP Show Value of Away">>). +-define(SHOW_VALUE_CHAT_LABEL, <<"XMPP Show Value of Chat">>). +-define(SHOW_VALUE_DND_LABEL, <<"XMPP Show Value of DND (Do Not Disturb)">>). +-define(SHOW_VALUE_ONLINE_LABEL, <<"Mere Availability in XMPP (No Show Value)">>). +-define(SHOW_VALUE_XA_LABEL, <<"XMPP Show Value of XA (Extended Away)">>). +-define(SUBSCRIPTION_TYPE_VALUE_ITEMS_LABEL, <<"Receive notification of new items only">>). +-define(SUBSCRIPTION_TYPE_VALUE_NODES_LABEL, <<"Receive notification of new nodes only">>). +-define(SUBSCRIPTION_DEPTH_VALUE_ONE_LABEL, <<"Receive notification from direct child nodes only">>). +-define(SUBSCRIPTION_DEPTH_VALUE_ALL_LABEL, <<"Receive notification from all descendent nodes">>). %%==================================================================== %% API %%==================================================================== -init() -> ok = create_table(). +init(_Host, _ServerHost, _Opts) -> ok = create_table(). -subscribe_node(JID, NodeID, Options) -> - case catch mnesia:sync_dirty(fun add_subscription/3, - [JID, NodeID, Options]) - of - {'EXIT', {aborted, Error}} -> Error; - {error, Error} -> {error, Error}; - Result -> {result, Result} +subscribe_node(JID, NodeId, Options) -> + case catch mnesia:sync_dirty(fun add_subscription/3, [JID, NodeId, Options]) + of + {'EXIT', {aborted, Error}} -> Error; + {error, Error} -> {error, Error}; + Result -> {result, Result} end. -unsubscribe_node(JID, NodeID, SubID) -> - case catch mnesia:sync_dirty(fun delete_subscription/3, - [JID, NodeID, SubID]) - of - {'EXIT', {aborted, Error}} -> Error; - {error, Error} -> {error, Error}; - Result -> {result, Result} +unsubscribe_node(JID, NodeId, SubID) -> + case catch mnesia:sync_dirty(fun delete_subscription/3, [JID, NodeId, SubID]) + of + {'EXIT', {aborted, Error}} -> Error; + {error, Error} -> {error, Error}; + Result -> {result, Result} end. -get_subscription(JID, NodeID, SubID) -> - case catch mnesia:sync_dirty(fun read_subscription/3, - [JID, NodeID, SubID]) - of - {'EXIT', {aborted, Error}} -> Error; - {error, Error} -> {error, Error}; - Result -> {result, Result} +get_subscription(JID, NodeId, SubID) -> + case catch mnesia:sync_dirty(fun read_subscription/3, [JID, NodeId, SubID]) + of + {'EXIT', {aborted, Error}} -> Error; + {error, Error} -> {error, Error}; + Result -> {result, Result} end. -set_subscription(JID, NodeID, SubID, Options) -> - case catch mnesia:sync_dirty(fun write_subscription/4, - [JID, NodeID, SubID, Options]) - of - {'EXIT', {aborted, Error}} -> Error; - {error, Error} -> {error, Error}; - Result -> {result, Result} +set_subscription(JID, NodeId, SubID, Options) -> + case catch mnesia:sync_dirty(fun write_subscription/4, [JID, NodeId, SubID, Options]) + of + {'EXIT', {aborted, Error}} -> Error; + {error, Error} -> {error, Error}; + Result -> {result, Result} end. get_options_xform(Lang, Options) -> - Keys = [deliver, show_values, subscription_type, - subscription_depth], - XFields = [get_option_xfield(Lang, Key, Options) - || Key <- Keys], + Keys = [deliver, show_values, subscription_type, subscription_depth], + XFields = [get_option_xfield(Lang, Key, Options) || Key <- Keys], {result, - #xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_XDATA}], - children = - [#xmlel{name = <<"field">>, - attrs = - [{<<"var">>, <<"FORM_TYPE">>}, - {<<"type">>, <<"hidden">>}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = - [{xmlcdata, ?NS_PUBSUB_SUB_OPTIONS}]}]}] - ++ XFields}}. + #xdata{type = form, + fields = [#xdata_field{type = hidden, + var = <<"FORM_TYPE">>, + values = [?NS_PUBSUB_SUB_OPTIONS]}| + XFields]}}. parse_options_xform(XFields) -> - case xml:remove_cdata(XFields) of - [#xmlel{name = <<"x">>} = XEl] -> - case jlib:parse_xdata_submit(XEl) of - XData when is_list(XData) -> - Opts = set_xoption(XData, []), - {result, Opts}; - Other -> Other - end; - _ -> {result, []} - end. + Opts = set_xoption(XFields, []), + {result, Opts}. %%==================================================================== %% Internal functions %%==================================================================== create_table() -> - case mnesia:create_table(pubsub_subscription, - [{disc_copies, [node()]}, - {attributes, - record_info(fields, pubsub_subscription)}, - {type, set}]) - of - {atomic, ok} -> ok; - {aborted, {already_exists, _}} -> ok; - Other -> Other + case ejabberd_mnesia:create(?MODULE, pubsub_subscription, + [{disc_copies, [node()]}, + {attributes, + record_info(fields, pubsub_subscription)}, + {type, set}]) + of + {atomic, ok} -> ok; + {aborted, {already_exists, _}} -> ok; + Other -> Other end. --spec(add_subscription/3 :: -( - _JID :: ljid(), - _NodeID :: mod_pubsub:nodeIdx(), - Options :: [] | mod_pubsub:subOptions()) - -> SubId :: mod_pubsub:subId() -). +-spec add_subscription(_JID :: ljid(), _NodeId :: mod_pubsub:nodeIdx(), + Options :: [] | mod_pubsub:subOptions()) -> + SubId :: mod_pubsub:subId(). -add_subscription(_JID, _NodeID, []) -> make_subid(); -add_subscription(_JID, _NodeID, Options) -> +add_subscription(_JID, _NodeId, []) -> make_subid(); +add_subscription(_JID, _NodeId, Options) -> SubID = make_subid(), - mnesia:write(#pubsub_subscription{subid = SubID, - options = Options}), + mnesia:write(#pubsub_subscription{subid = SubID, options = Options}), SubID. --spec(delete_subscription/3 :: -( - _JID :: _, - _NodeID :: _, - SubId :: mod_pubsub:subId()) - -> ok -). +-spec delete_subscription(_JID :: _, _NodeId :: _, SubId :: mod_pubsub:subId()) -> ok. -delete_subscription(_JID, _NodeID, SubID) -> +delete_subscription(_JID, _NodeId, SubID) -> mnesia:delete({pubsub_subscription, SubID}). --spec(read_subscription/3 :: -( - _JID :: ljid(), - _NodeID :: _, - SubID :: mod_pubsub:subId()) - -> mod_pubsub:pubsubSubscription() - | {error, notfound} -). +-spec read_subscription(_JID :: ljid(), _NodeId :: _, SubID :: mod_pubsub:subId()) -> + mod_pubsub:pubsubSubscription() | {error, notfound}. -read_subscription(_JID, _NodeID, SubID) -> +read_subscription(_JID, _NodeId, SubID) -> case mnesia:read({pubsub_subscription, SubID}) of - [Sub] -> Sub; - _ -> {error, notfound} + [Sub] -> Sub; + _ -> {error, notfound} end. --spec(write_subscription/4 :: -( - _JID :: ljid(), - _NodeID :: _, - SubID :: mod_pubsub:subId(), - Options :: mod_pubsub:subOptions()) - -> ok -). +-spec write_subscription(_JID :: ljid(), _NodeId :: _, SubID :: mod_pubsub:subId(), + Options :: mod_pubsub:subOptions()) -> ok. -write_subscription(_JID, _NodeID, SubID, Options) -> - mnesia:write(#pubsub_subscription{subid = SubID, - options = Options}). +write_subscription(_JID, _NodeId, SubID, Options) -> + mnesia:write(#pubsub_subscription{subid = SubID, options = Options}). --spec(make_subid/0 :: () -> SubId::mod_pubsub:subId()). +-spec make_subid() -> SubId::mod_pubsub:subId(). make_subid() -> - {T1, T2, T3} = now(), - iolist_to_binary(io_lib:fwrite("~.16B~.16B~.16B", [T1, T2, T3])). + {T1, T2, T3} = erlang:timestamp(), + (str:format("~.16B~.16B~.16B", [T1, T2, T3])). %% %% Subscription XForm processing. @@ -274,189 +181,142 @@ make_subid() -> set_xoption([], Opts) -> Opts; set_xoption([{Var, Value} | T], Opts) -> NewOpts = case var_xfield(Var) of - {error, _} -> Opts; - Key -> - Val = val_xfield(Key, Value), - lists:keystore(Key, 1, Opts, {Key, Val}) - end, + {error, _} -> Opts; + Key -> + Val = val_xfield(Key, Value), + lists:keystore(Key, 1, Opts, {Key, Val}) + end, set_xoption(T, NewOpts). %% Return the options list's key for an XForm var. %% Convert Values for option list's Key. var_xfield(?PUBSUB_DELIVER) -> deliver; var_xfield(?PUBSUB_DIGEST) -> digest; -var_xfield(?PUBSUB_DIGEST_FREQUENCY) -> - digest_frequency; +var_xfield(?PUBSUB_DIGEST_FREQUENCY) -> digest_frequency; var_xfield(?PUBSUB_EXPIRE) -> expire; var_xfield(?PUBSUB_INCLUDE_BODY) -> include_body; var_xfield(?PUBSUB_SHOW_VALUES) -> show_values; -var_xfield(?PUBSUB_SUBSCRIPTION_TYPE) -> - subscription_type; -var_xfield(?PUBSUB_SUBSCRIPTION_DEPTH) -> - subscription_depth; +var_xfield(?PUBSUB_SUBSCRIPTION_TYPE) -> subscription_type; +var_xfield(?PUBSUB_SUBSCRIPTION_DEPTH) -> subscription_depth; var_xfield(_) -> {error, badarg}. -val_xfield(deliver, [Val]) -> xopt_to_bool(Val); -%val_xfield(digest, [Val]) -> xopt_to_bool(Val); -%val_xfield(digest_frequency, [Val]) -> -% jlib:binary_to_integer(Val); -%val_xfield(expire, [Val]) -> -% jlib:datetime_string_to_timestamp(Val); -%val_xfield(include_body, [Val]) -> xopt_to_bool(Val); +val_xfield(deliver = Opt, [Val]) -> xopt_to_bool(Opt, Val); +val_xfield(digest = Opt, [Val]) -> xopt_to_bool(Opt, Val); +val_xfield(digest_frequency = Opt, [Val]) -> + case catch binary_to_integer(Val) of + N when is_integer(N) -> N; + _ -> + Txt = {?T("Value of '~s' should be integer"), [Opt]}, + {error, xmpp:err_not_acceptable(Txt, ejabberd_option:language())} + end; +val_xfield(expire = Opt, [Val]) -> + try xmpp_util:decode_timestamp(Val) + catch _:{bad_timestamp, _} -> + Txt = {?T("Value of '~s' should be datetime string"), [Opt]}, + {error, xmpp:err_not_acceptable(Txt, ejabberd_option:language())} + end; +val_xfield(include_body = Opt, [Val]) -> xopt_to_bool(Opt, Val); val_xfield(show_values, Vals) -> Vals; val_xfield(subscription_type, [<<"items">>]) -> items; val_xfield(subscription_type, [<<"nodes">>]) -> nodes; val_xfield(subscription_depth, [<<"all">>]) -> all; -val_xfield(subscription_depth, [Depth]) -> - case catch jlib:binary_to_integer(Depth) of - N when is_integer(N) -> N; - _ -> {error, ?ERR_NOT_ACCEPTABLE} +val_xfield(subscription_depth = Opt, [Depth]) -> + case catch binary_to_integer(Depth) of + N when is_integer(N) -> N; + _ -> + Txt = {?T("Value of '~s' should be integer"), [Opt]}, + {error, xmpp:err_not_acceptable(Txt, ejabberd_option:language())} end. %% Convert XForm booleans to Erlang booleans. -xopt_to_bool(<<"0">>) -> false; -xopt_to_bool(<<"1">>) -> true; -xopt_to_bool(<<"false">>) -> false; -xopt_to_bool(<<"true">>) -> true; -xopt_to_bool(_) -> {error, ?ERR_NOT_ACCEPTABLE}. - --spec(get_option_xfield/3 :: -( - Lang :: binary(), - Key :: atom(), - Options :: mod_pubsub:subOptions()) - -> xmlel() -). +xopt_to_bool(_, <<"0">>) -> false; +xopt_to_bool(_, <<"1">>) -> true; +xopt_to_bool(_, <<"false">>) -> false; +xopt_to_bool(_, <<"true">>) -> true; +xopt_to_bool(Option, _) -> + Txt = {?T("Value of '~s' should be boolean"), [Option]}, + {error, xmpp:err_not_acceptable(Txt, ejabberd_option:language())}. %% Return a field for an XForm for Key, with data filled in, if %% applicable, from Options. get_option_xfield(Lang, Key, Options) -> Var = xfield_var(Key), Label = xfield_label(Key), - {Type, OptEls} = type_and_options(xfield_type(Key), - Lang), + {Type, OptEls} = type_and_options(xfield_type(Key), Lang), Vals = case lists:keysearch(Key, 1, Options) of - {value, {_, Val}} -> - [tr_xfield_values(Vals) - || Vals <- xfield_val(Key, Val)]; - false -> [] - end, - #xmlel{name = <<"field">>, - attrs = - [{<<"var">>, Var}, {<<"type">>, Type}, - {<<"label">>, translate:translate(Lang, Label)}], - children = OptEls ++ Vals}. + {value, {_, Val}} -> + [xfield_val(Key, Val)]; + false -> + [] + end, + #xdata_field{type = Type, var = Var, + label = translate:translate(Lang, Label), + values = Vals, + options = OptEls}. type_and_options({Type, Options}, Lang) -> {Type, [tr_xfield_options(O, Lang) || O <- Options]}; type_and_options(Type, _Lang) -> {Type, []}. tr_xfield_options({Value, Label}, Lang) -> - #xmlel{name = <<"option">>, - attrs = - [{<<"label">>, translate:translate(Lang, Label)}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, Value}]}]}. - -tr_xfield_values(Value) -> -%% Return the XForm variable name for a subscription option key. -%% Return the XForm variable type for a subscription option key. - #xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, Value}]}. - --spec(xfield_var/1 :: -( - Var :: 'deliver' -% | 'digest' -% | 'digest_frequency' -% | 'expire' -% | 'include_body' - | 'show_values' - | 'subscription_type' - | 'subscription_depth') - -> binary() -). + #xdata_option{label = translate:translate(Lang, Label), + value = Value}. xfield_var(deliver) -> ?PUBSUB_DELIVER; %xfield_var(digest) -> ?PUBSUB_DIGEST; -%xfield_var(digest_frequency) -> -% ?PUBSUB_DIGEST_FREQUENCY; +%xfield_var(digest_frequency) -> ?PUBSUB_DIGEST_FREQUENCY; %xfield_var(expire) -> ?PUBSUB_EXPIRE; %xfield_var(include_body) -> ?PUBSUB_INCLUDE_BODY; xfield_var(show_values) -> ?PUBSUB_SHOW_VALUES; -xfield_var(subscription_type) -> - ?PUBSUB_SUBSCRIPTION_TYPE; -xfield_var(subscription_depth) -> - ?PUBSUB_SUBSCRIPTION_DEPTH. +xfield_var(subscription_type) -> ?PUBSUB_SUBSCRIPTION_TYPE; +xfield_var(subscription_depth) -> ?PUBSUB_SUBSCRIPTION_DEPTH. -xfield_type(deliver) -> <<"boolean">>; -%xfield_type(digest) -> <<"boolean">>; -%xfield_type(digest_frequency) -> <<"text-single">>; -%xfield_type(expire) -> <<"text-single">>; -%xfield_type(include_body) -> <<"boolean">>; +xfield_type(deliver) -> boolean; +%xfield_type(digest) -> boolean; +%xfield_type(digest_frequency) -> 'text-single'; +%xfield_type(expire) -> 'text-single'; +%xfield_type(include_body) -> boolean; xfield_type(show_values) -> - {<<"list-multi">>, - [{<<"away">>, ?SHOW_VALUE_AWAY_LABEL}, - {<<"chat">>, ?SHOW_VALUE_CHAT_LABEL}, - {<<"dnd">>, ?SHOW_VALUE_DND_LABEL}, - {<<"online">>, ?SHOW_VALUE_ONLINE_LABEL}, - {<<"xa">>, ?SHOW_VALUE_XA_LABEL}]}; + {'list-multi', + [{<<"away">>, ?SHOW_VALUE_AWAY_LABEL}, + {<<"chat">>, ?SHOW_VALUE_CHAT_LABEL}, + {<<"dnd">>, ?SHOW_VALUE_DND_LABEL}, + {<<"online">>, ?SHOW_VALUE_ONLINE_LABEL}, + {<<"xa">>, ?SHOW_VALUE_XA_LABEL}]}; xfield_type(subscription_type) -> - {<<"list-single">>, - [{<<"items">>, ?SUBSCRIPTION_TYPE_VALUE_ITEMS_LABEL}, - {<<"nodes">>, ?SUBSCRIPTION_TYPE_VALUE_NODES_LABEL}]}; + {'list-single', + [{<<"items">>, ?SUBSCRIPTION_TYPE_VALUE_ITEMS_LABEL}, + {<<"nodes">>, ?SUBSCRIPTION_TYPE_VALUE_NODES_LABEL}]}; xfield_type(subscription_depth) -> - {<<"list-single">>, - [{<<"1">>, ?SUBSCRIPTION_DEPTH_VALUE_ONE_LABEL}, - {<<"all">>, ?SUBSCRIPTION_DEPTH_VALUE_ALL_LABEL}]}. + {'list-single', + [{<<"1">>, ?SUBSCRIPTION_DEPTH_VALUE_ONE_LABEL}, + {<<"all">>, ?SUBSCRIPTION_DEPTH_VALUE_ALL_LABEL}]}. %% Return the XForm variable label for a subscription option key. xfield_label(deliver) -> ?DELIVER_LABEL; %xfield_label(digest) -> ?DIGEST_LABEL; -%xfield_label(digest_frequency) -> -% ?DIGEST_FREQUENCY_LABEL; +%xfield_label(digest_frequency) -> ?DIGEST_FREQUENCY_LABEL; %xfield_label(expire) -> ?EXPIRE_LABEL; %xfield_label(include_body) -> ?INCLUDE_BODY_LABEL; xfield_label(show_values) -> ?SHOW_VALUES_LABEL; %% Return the XForm value for a subscription option key. %% Convert erlang booleans to XForms. -xfield_label(subscription_type) -> - ?SUBSCRIPTION_TYPE_LABEL; -xfield_label(subscription_depth) -> - ?SUBSCRIPTION_DEPTH_LABEL. - --spec(xfield_val/2 :: -( - Field :: 'deliver' -% | 'digest' -% | 'digest_frequency' -% | 'expire' -% | 'include_body' - | 'show_values' - | 'subscription_type' - | 'subscription_depth', - Val :: boolean() - | binary() - | integer() - | [binary()]) -% | erlang:timestamp()) - -> [binary()] -). +xfield_label(subscription_type) -> ?SUBSCRIPTION_TYPE_LABEL; +xfield_label(subscription_depth) -> ?SUBSCRIPTION_DEPTH_LABEL. xfield_val(deliver, Val) -> [bool_to_xopt(Val)]; %xfield_val(digest, Val) -> [bool_to_xopt(Val)]; %xfield_val(digest_frequency, Val) -> -% [iolist_to_binary(integer_to_list(Val))]; +% [integer_to_binary(Val))]; %xfield_val(expire, Val) -> % [jlib:now_to_utc_string(Val)]; -%%xfield_val(include_body, Val) -> [bool_to_xopt(Val)]; +%xfield_val(include_body, Val) -> [bool_to_xopt(Val)]; xfield_val(show_values, Val) -> Val; xfield_val(subscription_type, items) -> [<<"items">>]; xfield_val(subscription_type, nodes) -> [<<"nodes">>]; xfield_val(subscription_depth, all) -> [<<"all">>]; xfield_val(subscription_depth, N) -> - [iolist_to_binary(integer_to_list(N))]. + [integer_to_binary(N)]. bool_to_xopt(true) -> <<"true">>; diff --git a/src/pubsub_subscription_odbc.erl b/src/pubsub_subscription_odbc.erl deleted file mode 100644 index b54733aaa..000000000 --- a/src/pubsub_subscription_odbc.erl +++ /dev/null @@ -1,386 +0,0 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. -%%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. -%%% -%%% The Initial Developer of the Original Code is ProcessOne. -%%% Portions created by ProcessOne are Copyright 2006-2015, ProcessOne -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2015, ProcessOne. -%%% -%%% @author Pablo Polvorin -%%% @author based on pubsub_subscription.erl by Brian Cully -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== - --module(pubsub_subscription_odbc). - --author("pablo.polvorin@process-one.net"). - -%% API --export([init/0, subscribe_node/3, unsubscribe_node/3, - get_subscription/3, set_subscription/4, - get_options_xform/2, parse_options_xform/1]). - --include("pubsub.hrl"). - --include("jlib.hrl"). - --define(PUBSUB_DELIVER, <<"pubsub#deliver">>). - --define(PUBSUB_DIGEST, <<"pubsub#digest">>). - --define(PUBSUB_DIGEST_FREQUENCY, - <<"pubsub#digest_frequency">>). - --define(PUBSUB_EXPIRE, <<"pubsub#expire">>). - --define(PUBSUB_INCLUDE_BODY, <<"pubsub#include_body">>). - --define(PUBSUB_SHOW_VALUES, <<"pubsub#show-values">>). - --define(PUBSUB_SUBSCRIPTION_TYPE, - <<"pubsub#subscription_type">>). - --define(PUBSUB_SUBSCRIPTION_DEPTH, - <<"pubsub#subscription_depth">>). - --define(DELIVER_LABEL, - <<"Whether an entity wants to receive or " - "disable notifications">>). - --define(DIGEST_LABEL, - <<"Whether an entity wants to receive digests " - "(aggregations) of notifications or all " - "notifications individually">>). - --define(DIGEST_FREQUENCY_LABEL, - <<"The minimum number of milliseconds between " - "sending any two notification digests">>). - --define(EXPIRE_LABEL, - <<"The DateTime at which a leased subscription " - "will end or has ended">>). - --define(INCLUDE_BODY_LABEL, - <<"Whether an entity wants to receive an " - "XMPP message body in addition to the " - "payload format">>). - --define(SHOW_VALUES_LABEL, - <<"The presence states for which an entity " - "wants to receive notifications">>). - --define(SUBSCRIPTION_TYPE_LABEL, - <<"Type of notification to receive">>). - --define(SUBSCRIPTION_DEPTH_LABEL, - <<"Depth from subscription for which to " - "receive notifications">>). - --define(SHOW_VALUE_AWAY_LABEL, - <<"XMPP Show Value of Away">>). - --define(SHOW_VALUE_CHAT_LABEL, - <<"XMPP Show Value of Chat">>). - --define(SHOW_VALUE_DND_LABEL, - <<"XMPP Show Value of DND (Do Not Disturb)">>). - --define(SHOW_VALUE_ONLINE_LABEL, - <<"Mere Availability in XMPP (No Show Value)">>). - --define(SHOW_VALUE_XA_LABEL, - <<"XMPP Show Value of XA (Extended Away)">>). - --define(SUBSCRIPTION_TYPE_VALUE_ITEMS_LABEL, - <<"Receive notification of new items only">>). - --define(SUBSCRIPTION_TYPE_VALUE_NODES_LABEL, - <<"Receive notification of new nodes only">>). - --define(SUBSCRIPTION_DEPTH_VALUE_ONE_LABEL, - <<"Receive notification from direct child " - "nodes only">>). - --define(SUBSCRIPTION_DEPTH_VALUE_ALL_LABEL, - <<"Receive notification from all descendent " - "nodes">>). - --define(DB_MOD, pubsub_db_odbc). -%%==================================================================== -%% API -%%==================================================================== - -init() -> ok = create_table(). - --spec(subscribe_node/3 :: -( - _JID :: _, - _NodeID :: _, - Options :: mod_pubsub:subOptions()) - -> {result, mod_pubsub:subId()} -). -subscribe_node(_JID, _NodeID, Options) -> - SubID = make_subid(), - (?DB_MOD):add_subscription(#pubsub_subscription{subid = - SubID, - options = Options}), - {result, SubID}. - --spec(unsubscribe_node/3 :: -( - _JID :: _, - _NodeID :: _, - SubID :: mod_pubsub:subId()) - -> {result, mod_pubsub:subscription()} - | {error, notfound} -). -unsubscribe_node(_JID, _NodeID, SubID) -> - case (?DB_MOD):read_subscription(SubID) of - {ok, Sub} -> - (?DB_MOD):delete_subscription(SubID), {result, Sub}; - notfound -> {error, notfound} - end. - --spec(get_subscription/3 :: -( - _JID :: _, - _NodeID :: _, - SubId :: mod_pubsub:subId()) - -> {result, mod_pubsub:subscription()} - | {error, notfound} -). -get_subscription(_JID, _NodeID, SubID) -> - case (?DB_MOD):read_subscription(SubID) of - {ok, Sub} -> {result, Sub}; - notfound -> {error, notfound} - end. - --spec(set_subscription/4 :: -( - _JID :: _, - _NodeID :: _, - SubId :: mod_pubsub:subId(), - Options :: mod_pubsub:subOptions()) - -> {result, ok} -). -set_subscription(_JID, _NodeID, SubID, Options) -> - case (?DB_MOD):read_subscription(SubID) of - {ok, _} -> - (?DB_MOD):update_subscription(#pubsub_subscription{subid - = SubID, - options = - Options}), - {result, ok}; - notfound -> - (?DB_MOD):add_subscription(#pubsub_subscription{subid = - SubID, - options = Options}), - {result, ok} - end. - -get_options_xform(Lang, Options) -> - Keys = [deliver, show_values, subscription_type, subscription_depth], - XFields = [get_option_xfield(Lang, Key, Options) - || Key <- Keys], - {result, - #xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_XDATA}], - children = - [#xmlel{name = <<"field">>, - attrs = - [{<<"var">>, <<"FORM_TYPE">>}, - {<<"type">>, <<"hidden">>}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = - [{xmlcdata, ?NS_PUBSUB_SUB_OPTIONS}]}]}] - ++ XFields}}. - -parse_options_xform(XFields) -> - case xml:remove_cdata(XFields) of - [#xmlel{name = <<"x">>} = XEl] -> - case jlib:parse_xdata_submit(XEl) of - XData when is_list(XData) -> - Opts = set_xoption(XData, []), - {result, Opts}; - Other -> Other - end; - _ -> {result, []} - end. - -%%==================================================================== -%% Internal functions -%%==================================================================== -create_table() -> ok. - --spec(make_subid/0 :: () -> mod_pubsub:subId()). -make_subid() -> - {T1, T2, T3} = now(), - iolist_to_binary(io_lib:fwrite("~.16B~.16B~.16B", [T1, T2, T3])). - -%% -%% Subscription XForm processing. -%% - -%% Return processed options, with types converted and so forth, using -%% Opts as defaults. -set_xoption([], Opts) -> Opts; -set_xoption([{Var, Value} | T], Opts) -> - NewOpts = case var_xfield(Var) of - {error, _} -> Opts; - Key -> - Val = val_xfield(Key, Value), - lists:keystore(Key, 1, Opts, {Key, Val}) - end, - set_xoption(T, NewOpts). - -%% Return the options list's key for an XForm var. -%% Convert Values for option list's Key. -var_xfield(?PUBSUB_DELIVER) -> deliver; -%var_xfield(?PUBSUB_DIGEST) -> digest; -%var_xfield(?PUBSUB_DIGEST_FREQUENCY) -> -% digest_frequency; -%var_xfield(?PUBSUB_EXPIRE) -> expire; -%var_xfield(?PUBSUB_INCLUDE_BODY) -> include_body; -var_xfield(?PUBSUB_SHOW_VALUES) -> show_values; -var_xfield(?PUBSUB_SUBSCRIPTION_TYPE) -> - subscription_type; -var_xfield(?PUBSUB_SUBSCRIPTION_DEPTH) -> - subscription_depth; -var_xfield(_) -> {error, badarg}. - -val_xfield(deliver, [Val]) -> xopt_to_bool(Val); -%val_xfield(digest, [Val]) -> xopt_to_bool(Val); -%val_xfield(digest_frequency, [Val]) -> -% jlib:binary_to_integer(Val); -%val_xfield(expire, [Val]) -> -% jlib:datetime_string_to_timestamp(Val); -%val_xfield(include_body, [Val]) -> xopt_to_bool(Val); -val_xfield(show_values, Vals) -> Vals; -val_xfield(subscription_type, [<<"items">>]) -> items; -val_xfield(subscription_type, [<<"nodes">>]) -> nodes; -val_xfield(subscription_depth, [<<"all">>]) -> all; -val_xfield(subscription_depth, [Depth]) -> - case catch jlib:binary_to_integer(Depth) of - N when is_integer(N) -> N; - _ -> {error, ?ERR_NOT_ACCEPTABLE} - end. - -%% Convert XForm booleans to Erlang booleans. -xopt_to_bool(<<"0">>) -> false; -xopt_to_bool(<<"1">>) -> true; -xopt_to_bool(<<"false">>) -> false; -xopt_to_bool(<<"true">>) -> true; -xopt_to_bool(_) -> {error, ?ERR_NOT_ACCEPTABLE}. - -%% Return a field for an XForm for Key, with data filled in, if -%% applicable, from Options. -get_option_xfield(Lang, Key, Options) -> - Var = xfield_var(Key), - Label = xfield_label(Key), - {Type, OptEls} = type_and_options(xfield_type(Key), - Lang), - Vals = case lists:keysearch(Key, 1, Options) of - {value, {_, Val}} -> - [tr_xfield_values(Vals) - || Vals <- xfield_val(Key, Val)]; - false -> [] - end, - #xmlel{name = <<"field">>, - attrs = - [{<<"var">>, Var}, {<<"type">>, Type}, - {<<"label">>, translate:translate(Lang, Label)}], - children = OptEls ++ Vals}. - -type_and_options({Type, Options}, Lang) -> - {Type, [tr_xfield_options(O, Lang) || O <- Options]}; -type_and_options(Type, _Lang) -> {Type, []}. - -tr_xfield_options({Value, Label}, Lang) -> - #xmlel{name = <<"option">>, - attrs = - [{<<"label">>, translate:translate(Lang, Label)}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, Value}]}]}. - -tr_xfield_values(Value) -> -%% Return the XForm variable name for a subscription option key. -%% Return the XForm variable type for a subscription option key. - #xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, Value}]}. - -xfield_var(deliver) -> ?PUBSUB_DELIVER; -%xfield_var(digest) -> ?PUBSUB_DIGEST; -%xfield_var(digest_frequency) -> -% ?PUBSUB_DIGEST_FREQUENCY; -%xfield_var(expire) -> ?PUBSUB_EXPIRE; -%xfield_var(include_body) -> ?PUBSUB_INCLUDE_BODY; -xfield_var(show_values) -> ?PUBSUB_SHOW_VALUES; -xfield_var(subscription_type) -> - ?PUBSUB_SUBSCRIPTION_TYPE; -xfield_var(subscription_depth) -> - ?PUBSUB_SUBSCRIPTION_DEPTH. - -xfield_type(deliver) -> <<"boolean">>; -%xfield_type(digest) -> <<"boolean">>; -%xfield_type(digest_frequency) -> <<"text-single">>; -%xfield_type(expire) -> <<"text-single">>; -%xfield_type(include_body) -> <<"boolean">>; -xfield_type(show_values) -> - {<<"list-multi">>, - [{<<"away">>, ?SHOW_VALUE_AWAY_LABEL}, - {<<"chat">>, ?SHOW_VALUE_CHAT_LABEL}, - {<<"dnd">>, ?SHOW_VALUE_DND_LABEL}, - {<<"online">>, ?SHOW_VALUE_ONLINE_LABEL}, - {<<"xa">>, ?SHOW_VALUE_XA_LABEL}]}; -xfield_type(subscription_type) -> - {<<"list-single">>, - [{<<"items">>, ?SUBSCRIPTION_TYPE_VALUE_ITEMS_LABEL}, - {<<"nodes">>, ?SUBSCRIPTION_TYPE_VALUE_NODES_LABEL}]}; -xfield_type(subscription_depth) -> - {<<"list-single">>, - [{<<"1">>, ?SUBSCRIPTION_DEPTH_VALUE_ONE_LABEL}, - {<<"all">>, ?SUBSCRIPTION_DEPTH_VALUE_ALL_LABEL}]}. - -%% Return the XForm variable label for a subscription option key. -xfield_label(deliver) -> ?DELIVER_LABEL; -%xfield_label(digest) -> ?DIGEST_LABEL; -%xfield_label(digest_frequency) -> -% ?DIGEST_FREQUENCY_LABEL; -%xfield_label(expire) -> ?EXPIRE_LABEL; -%xfield_label(include_body) -> ?INCLUDE_BODY_LABEL; -xfield_label(show_values) -> ?SHOW_VALUES_LABEL; -%% Return the XForm value for a subscription option key. -%% Convert erlang booleans to XForms. -xfield_label(subscription_type) -> - ?SUBSCRIPTION_TYPE_LABEL; -xfield_label(subscription_depth) -> - ?SUBSCRIPTION_DEPTH_LABEL. - -xfield_val(deliver, Val) -> [bool_to_xopt(Val)]; -%xfield_val(digest, Val) -> [bool_to_xopt(Val)]; -%xfield_val(digest_frequency, Val) -> -% [iolist_to_binary(integer_to_list(Val))]; -%xfield_val(expire, Val) -> -% [jlib:now_to_utc_string(Val)]; -%xfield_val(include_body, Val) -> [bool_to_xopt(Val)]; -xfield_val(show_values, Val) -> Val; -xfield_val(subscription_type, items) -> [<<"items">>]; -xfield_val(subscription_type, nodes) -> [<<"nodes">>]; -xfield_val(subscription_depth, all) -> [<<"all">>]; -xfield_val(subscription_depth, N) -> - [iolist_to_binary(integer_to_list(N))]. - -bool_to_xopt(false) -> <<"false">>; -bool_to_xopt(true) -> <<"true">>. diff --git a/src/pubsub_subscription_sql.erl b/src/pubsub_subscription_sql.erl new file mode 100644 index 000000000..8f1361b47 --- /dev/null +++ b/src/pubsub_subscription_sql.erl @@ -0,0 +1,287 @@ +%%%---------------------------------------------------------------------- +%%% File : pubsub_subscription_sql.erl +%%% Author : Pablo Polvorin +%%% Purpose : Handle pubsub subscriptions options with ODBC backend +%%% based on pubsub_subscription.erl by Brian Cully +%%% Created : 7 Aug 2009 by Pablo Polvorin +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(pubsub_subscription_sql). +-author("pablo.polvorin@process-one.net"). + +%% API +-export([init/3, subscribe_node/3, unsubscribe_node/3, + get_subscription/3, set_subscription/4, + make_subid/0, + get_options_xform/2, parse_options_xform/1]). + +-include("pubsub.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). +-include("translate.hrl"). + +-define(PUBSUB_DELIVER, <<"pubsub#deliver">>). +-define(PUBSUB_DIGEST, <<"pubsub#digest">>). +-define(PUBSUB_DIGEST_FREQUENCY, <<"pubsub#digest_frequency">>). +-define(PUBSUB_EXPIRE, <<"pubsub#expire">>). +-define(PUBSUB_INCLUDE_BODY, <<"pubsub#include_body">>). +-define(PUBSUB_SHOW_VALUES, <<"pubsub#show-values">>). +-define(PUBSUB_SUBSCRIPTION_TYPE, <<"pubsub#subscription_type">>). +-define(PUBSUB_SUBSCRIPTION_DEPTH, <<"pubsub#subscription_depth">>). +-define(DELIVER_LABEL, <<"Whether an entity wants to receive or disable notifications">>). +-define(DIGEST_LABEL, <<"Whether an entity wants to receive digests " + "(aggregations) of notifications or all notifications individually">>). +-define(DIGEST_FREQUENCY_LABEL, <<"The minimum number of milliseconds between " + "sending any two notification digests">>). +-define(EXPIRE_LABEL, <<"The DateTime at which a leased subscription will end or has ended">>). +-define(INCLUDE_BODY_LABEL, <<"Whether an entity wants to receive an " + "XMPP message body in addition to the payload format">>). +-define(SHOW_VALUES_LABEL, <<"The presence states for which an entity wants to receive notifications">>). +-define(SUBSCRIPTION_TYPE_LABEL, <<"Type of notification to receive">>). +-define(SUBSCRIPTION_DEPTH_LABEL, <<"Depth from subscription for which to receive notifications">>). +-define(SHOW_VALUE_AWAY_LABEL, <<"XMPP Show Value of Away">>). +-define(SHOW_VALUE_CHAT_LABEL, <<"XMPP Show Value of Chat">>). +-define(SHOW_VALUE_DND_LABEL, <<"XMPP Show Value of DND (Do Not Disturb)">>). +-define(SHOW_VALUE_ONLINE_LABEL, <<"Mere Availability in XMPP (No Show Value)">>). +-define(SHOW_VALUE_XA_LABEL, <<"XMPP Show Value of XA (Extended Away)">>). +-define(SUBSCRIPTION_TYPE_VALUE_ITEMS_LABEL, <<"Receive notification of new items only">>). +-define(SUBSCRIPTION_TYPE_VALUE_NODES_LABEL, <<"Receive notification of new nodes only">>). +-define(SUBSCRIPTION_DEPTH_VALUE_ONE_LABEL, <<"Receive notification from direct child nodes only">>). +-define(SUBSCRIPTION_DEPTH_VALUE_ALL_LABEL, <<"Receive notification from all descendent nodes">>). + +-define(DB_MOD, pubsub_db_sql). +%%==================================================================== +%% API +%%==================================================================== + +init(_Host, _ServerHost, _Opts) -> ok = create_table(). + +-spec subscribe_node(_JID :: _, _NodeId :: _, Options :: [] | mod_pubsub:subOptions()) -> + {result, mod_pubsub:subId()}. + +subscribe_node(_JID, _NodeId, Options) -> + SubID = make_subid(), + (?DB_MOD):add_subscription(#pubsub_subscription{subid = SubID, options = Options}), + {result, SubID}. + +-spec unsubscribe_node(_JID :: _, _NodeId :: _, SubID :: mod_pubsub:subId()) -> + {result, mod_pubsub:subscription()} | {error, notfound}. + +unsubscribe_node(_JID, _NodeId, SubID) -> + case (?DB_MOD):read_subscription(SubID) of + {ok, Sub} -> (?DB_MOD):delete_subscription(SubID), {result, Sub}; + notfound -> {error, notfound} + end. + +-spec get_subscription(_JID :: _, _NodeId :: _, SubId :: mod_pubsub:subId()) -> + {result, mod_pubsub:subscription()} | {error, notfound}. + +get_subscription(_JID, _NodeId, SubID) -> + case (?DB_MOD):read_subscription(SubID) of + {ok, Sub} -> {result, Sub}; + notfound -> {error, notfound} + end. + +-spec set_subscription(_JID :: _, _NodeId :: _, SubId :: mod_pubsub:subId(), + Options :: mod_pubsub:subOptions()) -> {result, ok}. +set_subscription(_JID, _NodeId, SubID, Options) -> + case (?DB_MOD):read_subscription(SubID) of + {ok, _} -> + (?DB_MOD):update_subscription(#pubsub_subscription{subid = SubID, + options = Options}), + {result, ok}; + notfound -> + (?DB_MOD):add_subscription(#pubsub_subscription{subid = SubID, + options = Options}), + {result, ok} + end. + +get_options_xform(Lang, Options) -> + Keys = [deliver, show_values, subscription_type, subscription_depth], + XFields = [get_option_xfield(Lang, Key, Options) || Key <- Keys], + {result, + #xdata{type = form, + fields = [#xdata_field{type = hidden, + var = <<"FORM_TYPE">>, + values = [?NS_PUBSUB_SUB_OPTIONS]}| + XFields]}}. + +parse_options_xform(XFields) -> + Opts = set_xoption(XFields, []), + {result, Opts}. + +%%==================================================================== +%% Internal functions +%%==================================================================== +create_table() -> ok. + +-spec make_subid() -> mod_pubsub:subId(). +make_subid() -> + {T1, T2, T3} = erlang:timestamp(), + (str:format("~.16B~.16B~.16B", [T1, T2, T3])). + +%% +%% Subscription XForm processing. +%% + +%% Return processed options, with types converted and so forth, using +%% Opts as defaults. +set_xoption([], Opts) -> Opts; +set_xoption([{Var, Value} | T], Opts) -> + NewOpts = case var_xfield(Var) of + {error, _} -> Opts; + Key -> + Val = val_xfield(Key, Value), + lists:keystore(Key, 1, Opts, {Key, Val}) + end, + set_xoption(T, NewOpts). + +%% Return the options list's key for an XForm var. +%% Convert Values for option list's Key. +var_xfield(?PUBSUB_DELIVER) -> deliver; +var_xfield(?PUBSUB_DIGEST) -> digest; +var_xfield(?PUBSUB_DIGEST_FREQUENCY) -> digest_frequency; +var_xfield(?PUBSUB_EXPIRE) -> expire; +var_xfield(?PUBSUB_INCLUDE_BODY) -> include_body; +var_xfield(?PUBSUB_SHOW_VALUES) -> show_values; +var_xfield(?PUBSUB_SUBSCRIPTION_TYPE) -> subscription_type; +var_xfield(?PUBSUB_SUBSCRIPTION_DEPTH) -> subscription_depth; +var_xfield(_) -> {error, badarg}. + +val_xfield(deliver = Opt, [Val]) -> xopt_to_bool(Opt, Val); +val_xfield(digest = Opt, [Val]) -> xopt_to_bool(Opt, Val); +val_xfield(digest_frequency = Opt, [Val]) -> + case catch binary_to_integer(Val) of + N when is_integer(N) -> N; + _ -> + Txt = {?T("Value of '~s' should be integer"), [Opt]}, + {error, xmpp:err_not_acceptable(Txt, ejabberd_option:language())} + end; +val_xfield(expire = Opt, [Val]) -> + try xmpp_util:decode_timestamp(Val) + catch _:{bad_timestamp, _} -> + Txt = {?T("Value of '~s' should be datetime string"), [Opt]}, + {error, xmpp:err_not_acceptable(Txt, ejabberd_option:language())} + end; +val_xfield(include_body = Opt, [Val]) -> xopt_to_bool(Opt, Val); +val_xfield(show_values, Vals) -> Vals; +val_xfield(subscription_type, [<<"items">>]) -> items; +val_xfield(subscription_type, [<<"nodes">>]) -> nodes; +val_xfield(subscription_depth, [<<"all">>]) -> all; +val_xfield(subscription_depth = Opt, [Depth]) -> + case catch binary_to_integer(Depth) of + N when is_integer(N) -> N; + _ -> + Txt = {?T("Value of '~s' should be integer"), [Opt]}, + {error, xmpp:err_not_acceptable(Txt, ejabberd_option:language())} + end. + +%% Convert XForm booleans to Erlang booleans. +xopt_to_bool(_, <<"0">>) -> false; +xopt_to_bool(_, <<"1">>) -> true; +xopt_to_bool(_, <<"false">>) -> false; +xopt_to_bool(_, <<"true">>) -> true; +xopt_to_bool(Option, _) -> + Txt = {?T("Value of '~s' should be boolean"), [Option]}, + {error, xmpp:err_not_acceptable(Txt, ejabberd_option:language())}. + +%% Return a field for an XForm for Key, with data filled in, if +%% applicable, from Options. +get_option_xfield(Lang, Key, Options) -> + Var = xfield_var(Key), + Label = xfield_label(Key), + {Type, OptEls} = type_and_options(xfield_type(Key), Lang), + Vals = case lists:keysearch(Key, 1, Options) of + {value, {_, Val}} -> + [xfield_val(Key, Val)]; + false -> + [] + end, + #xdata_field{type = Type, var = Var, + label = translate:translate(Lang, Label), + values = Vals, + options = OptEls}. + +type_and_options({Type, Options}, Lang) -> + {Type, [tr_xfield_options(O, Lang) || O <- Options]}; +type_and_options(Type, _Lang) -> {Type, []}. + +tr_xfield_options({Value, Label}, Lang) -> + #xdata_option{label = translate:translate(Lang, Label), + value = Value}. + +xfield_var(deliver) -> ?PUBSUB_DELIVER; +%xfield_var(digest) -> ?PUBSUB_DIGEST; +%xfield_var(digest_frequency) -> ?PUBSUB_DIGEST_FREQUENCY; +%xfield_var(expire) -> ?PUBSUB_EXPIRE; +%xfield_var(include_body) -> ?PUBSUB_INCLUDE_BODY; +xfield_var(show_values) -> ?PUBSUB_SHOW_VALUES; +xfield_var(subscription_type) -> ?PUBSUB_SUBSCRIPTION_TYPE; +xfield_var(subscription_depth) -> ?PUBSUB_SUBSCRIPTION_DEPTH. + +xfield_type(deliver) -> boolean; +%xfield_type(digest) -> boolean; +%xfield_type(digest_frequency) -> 'text-single'; +%xfield_type(expire) -> 'text-single'; +%xfield_type(include_body) -> boolean; +xfield_type(show_values) -> + {'list-multi', + [{<<"away">>, ?SHOW_VALUE_AWAY_LABEL}, + {<<"chat">>, ?SHOW_VALUE_CHAT_LABEL}, + {<<"dnd">>, ?SHOW_VALUE_DND_LABEL}, + {<<"online">>, ?SHOW_VALUE_ONLINE_LABEL}, + {<<"xa">>, ?SHOW_VALUE_XA_LABEL}]}; +xfield_type(subscription_type) -> + {'list-single', + [{<<"items">>, ?SUBSCRIPTION_TYPE_VALUE_ITEMS_LABEL}, + {<<"nodes">>, ?SUBSCRIPTION_TYPE_VALUE_NODES_LABEL}]}; +xfield_type(subscription_depth) -> + {'list-single', + [{<<"1">>, ?SUBSCRIPTION_DEPTH_VALUE_ONE_LABEL}, + {<<"all">>, ?SUBSCRIPTION_DEPTH_VALUE_ALL_LABEL}]}. + +%% Return the XForm variable label for a subscription option key. +xfield_label(deliver) -> ?DELIVER_LABEL; +%xfield_label(digest) -> ?DIGEST_LABEL; +%xfield_label(digest_frequency) -> ?DIGEST_FREQUENCY_LABEL; +%xfield_label(expire) -> ?EXPIRE_LABEL; +%xfield_label(include_body) -> ?INCLUDE_BODY_LABEL; +xfield_label(show_values) -> ?SHOW_VALUES_LABEL; +%% Return the XForm value for a subscription option key. +%% Convert erlang booleans to XForms. +xfield_label(subscription_type) -> ?SUBSCRIPTION_TYPE_LABEL; +xfield_label(subscription_depth) -> ?SUBSCRIPTION_DEPTH_LABEL. + +xfield_val(deliver, Val) -> [bool_to_xopt(Val)]; +%xfield_val(digest, Val) -> [bool_to_xopt(Val)]; +%xfield_val(digest_frequency, Val) -> +% [integer_to_binary(Val))]; +%xfield_val(expire, Val) -> +% [jlib:now_to_utc_string(Val)]; +%xfield_val(include_body, Val) -> [bool_to_xopt(Val)]; +xfield_val(show_values, Val) -> Val; +xfield_val(subscription_type, items) -> [<<"items">>]; +xfield_val(subscription_type, nodes) -> [<<"nodes">>]; +xfield_val(subscription_depth, all) -> [<<"all">>]; +xfield_val(subscription_depth, N) -> + [integer_to_binary(N)]. + +bool_to_xopt(false) -> <<"false">>; +bool_to_xopt(true) -> <<"true">>. diff --git a/src/rest.erl b/src/rest.erl new file mode 100644 index 000000000..b456fdaac --- /dev/null +++ b/src/rest.erl @@ -0,0 +1,237 @@ +%%%---------------------------------------------------------------------- +%%% File : rest.erl +%%% Author : Christophe Romain +%%% Purpose : Generic REST client +%%% Created : 16 Oct 2014 by Christophe Romain +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(rest). + +-export([start/1, stop/1, get/2, get/3, post/4, delete/2, + put/4, patch/4, request/6, with_retry/4, + encode_json/1]). + +-include("logger.hrl"). + +-define(HTTP_TIMEOUT, 10000). +-define(CONNECT_TIMEOUT, 8000). +-define(CONTENT_TYPE, "application/json"). + +start(Host) -> + application:start(inets), + Size = ejabberd_option:ext_api_http_pool_size(Host), + Proxy = case {ejabberd_option:rest_proxy(Host), + ejabberd_option:rest_proxy_port(Host)} of + {<<>>, _} -> + []; + {Host, Port} -> + [{proxy, {{binary_to_list(Host), Port}, []}}] + end, + httpc:set_options([{max_sessions, Size}] ++ Proxy). + +stop(_Host) -> + ok. + +with_retry(Method, Args, MaxRetries, Backoff) -> + with_retry(Method, Args, 0, MaxRetries, Backoff). +with_retry(Method, Args, Retries, MaxRetries, Backoff) -> + case apply(?MODULE, Method, Args) of + %% Only retry on timeout errors + {error, {http_error,{error,Error}}} + when Retries < MaxRetries + andalso (Error == 'timeout' orelse Error == 'connect_timeout') -> + timer:sleep(round(math:pow(2, Retries)) * Backoff), + with_retry(Method, Args, Retries+1, MaxRetries, Backoff); + Result -> + Result + end. + +get(Server, Path) -> + request(Server, get, Path, [], ?CONTENT_TYPE, <<>>). +get(Server, Path, Params) -> + request(Server, get, Path, Params, ?CONTENT_TYPE, <<>>). + +delete(Server, Path) -> + request(Server, delete, Path, [], ?CONTENT_TYPE, <<>>). + +post(Server, Path, Params, Content) -> + Data = encode_json(Content), + request(Server, post, Path, Params, ?CONTENT_TYPE, Data). + +put(Server, Path, Params, Content) -> + Data = encode_json(Content), + request(Server, put, Path, Params, ?CONTENT_TYPE, Data). + +patch(Server, Path, Params, Content) -> + Data = encode_json(Content), + request(Server, patch, Path, Params, ?CONTENT_TYPE, Data). + +request(Server, Method, Path, _Params, _Mime, {error, Error}) -> + ejabberd_hooks:run(backend_api_error, Server, + [Server, Method, Path, Error]), + {error, Error}; +request(Server, Method, Path, Params, Mime, Data) -> + {Query, Opts} = case Params of + {_, _} -> Params; + _ -> {Params, []} + end, + URI = to_list(url(Server, Path, Query)), + HttpOpts = case {ejabberd_option:rest_proxy_username(Server), + ejabberd_option:rest_proxy_password(Server)} of + {"", _} -> [{connect_timeout, ?CONNECT_TIMEOUT}, + {timeout, ?HTTP_TIMEOUT}]; + {User, Pass} -> + [{connect_timeout, ?CONNECT_TIMEOUT}, + {timeout, ?HTTP_TIMEOUT}, + {proxy_auth, {User, Pass}}] + end, + Hdrs = [{"connection", "keep-alive"}, + {"Accept", "application/json"}, + {"User-Agent", "ejabberd"}] + ++ custom_headers(Server), + Req = if + (Method =:= post) orelse (Method =:= patch) orelse (Method =:= put) orelse (Method =:= delete) -> + {URI, Hdrs, to_list(Mime), Data}; + true -> + {URI, Hdrs} + end, + Begin = os:timestamp(), + ejabberd_hooks:run(backend_api_call, Server, [Server, Method, Path]), + Result = try httpc:request(Method, Req, HttpOpts, [{body_format, binary}]) of + {ok, {{_, Code, _}, RetHdrs, Body}} -> + try decode_json(Body) of + JSon -> + case proplists:get_bool(return_headers, Opts) of + true -> {ok, Code, RetHdrs, JSon}; + false -> {ok, Code, JSon} + end + catch + _:Reason -> + {error, {invalid_json, Body, Reason}} + end; + {error, Reason} -> + {error, {http_error, {error, Reason}}} + catch + exit:Reason -> + {error, {http_error, {error, Reason}}} + end, + case Result of + {error, {http_error, {error, timeout}}} -> + ejabberd_hooks:run(backend_api_timeout, Server, + [Server, Method, Path]); + {error, {http_error, {error, connect_timeout}}} -> + ejabberd_hooks:run(backend_api_timeout, Server, + [Server, Method, Path]); + {error, Error} -> + ejabberd_hooks:run(backend_api_error, Server, + [Server, Method, Path, Error]); + _ -> + End = os:timestamp(), + Elapsed = timer:now_diff(End, Begin) div 1000, %% time in ms + ejabberd_hooks:run(backend_api_response_time, Server, + [Server, Method, Path, Elapsed]) + end, + Result. + +%%%---------------------------------------------------------------------- +%%% HTTP helpers +%%%---------------------------------------------------------------------- + +to_list(V) when is_binary(V) -> + binary_to_list(V); +to_list(V) when is_list(V) -> + V. + +encode_json(Content) -> + case catch misc:json_encode(Content) of + {'EXIT', Reason} -> + {error, {invalid_payload, Content, Reason}}; + Encoded -> + Encoded + end. + +decode_json(<<>>) -> []; +decode_json(<<" ">>) -> []; +decode_json(<<"\r\n">>) -> []; +decode_json(Data) -> misc:json_decode(Data). + +custom_headers(Server) -> + case ejabberd_option:ext_api_headers(Server) of + <<>> -> + []; + Hdrs -> + lists:foldr(fun(Hdr, Acc) -> + case binary:split(Hdr, <<":">>) of + [K, V] -> [{binary_to_list(K), binary_to_list(V)}|Acc]; + _ -> Acc + end + end, [], binary:split(Hdrs, <<",">>)) + end. + +base_url(Server, Path) -> + BPath = case iolist_to_binary(Path) of + <<$/, Ok/binary>> -> Ok; + Ok -> Ok + end, + Url = case BPath of + <<"http", _/binary>> -> BPath; + _ -> + Base = ejabberd_option:ext_api_url(Server), + case binary:last(Base) of + $/ -> <>; + _ -> <> + end + end, + case binary:last(Url) of + 47 -> binary_part(Url, 0, size(Url)-1); + _ -> 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], + <<$&, Encoded0/binary>> = iolist_to_binary(L), + Encoded = uri_hack(Encoded0), + <>. +url(Server, Path, Params) -> + case binary:split(base_url(Server, Path), <<"?">>) of + [Url] -> + url(Url, Params); + [Url, Extra] -> + Custom = [list_to_tuple(binary:split(P, <<"=">>)) + || P <- binary:split(Extra, <<"&">>, [global])], + url(Url, Custom++Params) + end. diff --git a/src/scram.erl b/src/scram.erl deleted file mode 100644 index c8c85e8d5..000000000 --- a/src/scram.erl +++ /dev/null @@ -1,86 +0,0 @@ -%%%---------------------------------------------------------------------- -%%% File : scram.erl -%%% Author : Stephen Röttger -%%% Purpose : SCRAM (RFC 5802) -%%% Created : 7 Aug 2011 by Stephen Röttger -%%% -%%% -%%% ejabberd, Copyright (C) 2002-2015 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(scram). - --author('stephen.roettger@googlemail.com'). - -%% External exports -%% ejabberd doesn't implement SASLPREP, so we use the similar RESOURCEPREP instead --export([salted_password/3, stored_key/1, server_key/1, - server_signature/2, client_signature/2, client_key/1, - client_key/2]). - --spec salted_password(binary(), binary(), non_neg_integer()) -> binary(). - -salted_password(Password, Salt, IterationCount) -> - hi(jlib:resourceprep(Password), Salt, IterationCount). - --spec client_key(binary()) -> binary(). - -client_key(SaltedPassword) -> - crypto:sha_mac(SaltedPassword, <<"Client Key">>). - --spec stored_key(binary()) -> binary(). - -stored_key(ClientKey) -> p1_sha:sha1(ClientKey). - --spec server_key(binary()) -> binary(). - -server_key(SaltedPassword) -> - crypto:sha_mac(SaltedPassword, <<"Server Key">>). - --spec client_signature(binary(), binary()) -> binary(). - -client_signature(StoredKey, AuthMessage) -> - crypto:sha_mac(StoredKey, AuthMessage). - --spec client_key(binary(), binary()) -> binary(). - -client_key(ClientProof, ClientSignature) -> - list_to_binary(lists:zipwith(fun (X, Y) -> X bxor Y end, - binary_to_list(ClientProof), - binary_to_list(ClientSignature))). - --spec server_signature(binary(), binary()) -> binary(). - -server_signature(ServerKey, AuthMessage) -> - crypto:sha_mac(ServerKey, AuthMessage). - -hi(Password, Salt, IterationCount) -> - U1 = crypto:sha_mac(Password, <>), - list_to_binary(lists:zipwith(fun (X, Y) -> X bxor Y end, - binary_to_list(U1), - binary_to_list(hi_round(Password, U1, - IterationCount - 1)))). - -hi_round(Password, UPrev, 1) -> - crypto:sha_mac(Password, UPrev); -hi_round(Password, UPrev, IterationCount) -> - U = crypto:sha_mac(Password, UPrev), - list_to_binary(lists:zipwith(fun (X, Y) -> X bxor Y end, - binary_to_list(U), - binary_to_list(hi_round(Password, U, - IterationCount - 1)))). diff --git a/src/shaper.erl b/src/shaper.erl deleted file mode 100644 index a85c4f111..000000000 --- a/src/shaper.erl +++ /dev/null @@ -1,142 +0,0 @@ -%%%---------------------------------------------------------------------- -%%% File : shaper.erl -%%% Author : Alexey Shchepin -%%% Purpose : Functions to control connections traffic -%%% Created : 9 Feb 2003 by Alexey Shchepin -%%% -%%% -%%% ejabberd, Copyright (C) 2002-2015 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(shaper). - --author('alexey@process-one.net'). - --export([start/0, new/1, new1/1, update/2, get_max_rate/1, - transform_options/1, load_from_config/0]). - --include("ejabberd.hrl"). --include("logger.hrl"). - --record(maxrate, {maxrate = 0 :: integer(), - lastrate = 0.0 :: float(), - lasttime = 0 :: integer()}). - --record(shaper, {name :: {atom(), global}, - maxrate :: integer()}). - --type shaper() :: none | #maxrate{}. - --export_type([shaper/0]). - --spec start() -> ok. - -start() -> - mnesia:create_table(shaper, - [{ram_copies, [node()]}, - {local_content, true}, - {attributes, record_info(fields, shaper)}]), - mnesia:add_table_copy(shaper, node(), ram_copies), - load_from_config(), - ok. - --spec load_from_config() -> ok | {error, any()}. - -load_from_config() -> - Shapers = ejabberd_config:get_option( - shaper, fun(V) -> V end, []), - case mnesia:transaction( - fun() -> - lists:foreach( - fun({Name, MaxRate}) -> - mnesia:write(#shaper{name = {Name, global}, - maxrate = MaxRate}) - end, Shapers) - end) of - {atomic, ok} -> - ok; - Err -> - {error, Err} - end. - --spec get_max_rate(atom()) -> none | non_neg_integer(). - -get_max_rate(none) -> - none; -get_max_rate(Name) -> - case ets:lookup(shaper, {Name, global}) of - [#shaper{maxrate = R}] -> - R; - [] -> - none - end. - --spec new(atom()) -> shaper(). - -new(none) -> - none; -new(Name) -> - MaxRate = case ets:lookup(shaper, {Name, global}) of - [#shaper{maxrate = R}] -> - R; - [] -> - none - end, - new1(MaxRate). - --spec new1(none | integer()) -> shaper(). - -new1(none) -> none; -new1(MaxRate) -> - #maxrate{maxrate = MaxRate, lastrate = 0.0, - lasttime = now_to_usec(now())}. - --spec update(shaper(), integer()) -> {shaper(), integer()}. - -update(none, _Size) -> {none, 0}; -update(#maxrate{} = State, Size) -> - MinInterv = 1000 * Size / - (2 * State#maxrate.maxrate - State#maxrate.lastrate), - Interv = (now_to_usec(now()) - State#maxrate.lasttime) / - 1000, - ?DEBUG("State: ~p, Size=~p~nM=~p, I=~p~n", - [State, Size, MinInterv, Interv]), - Pause = if MinInterv > Interv -> - 1 + trunc(MinInterv - Interv); - true -> 0 - end, - NextNow = now_to_usec(now()) + Pause * 1000, - {State#maxrate{lastrate = - (State#maxrate.lastrate + - 1000000 * Size / (NextNow - State#maxrate.lasttime)) - / 2, - lasttime = NextNow}, - Pause}. - -transform_options(Opts) -> - lists:foldl(fun transform_options/2, [], Opts). - -transform_options({OptName, Name, {maxrate, N}}, Opts) when OptName == shaper -> - [{shaper, [{Name, N}]}|Opts]; -transform_options({OptName, Name, none}, Opts) when OptName == shaper -> - [{shaper, [{Name, none}]}|Opts]; -transform_options(Opt, Opts) -> - [Opt|Opts]. - -now_to_usec({MSec, Sec, USec}) -> - (MSec * 1000000 + Sec) * 1000000 + USec. diff --git a/src/str.erl b/src/str.erl index 80d7b05b0..6dafcfda6 100644 --- a/src/str.erl +++ b/src/str.erl @@ -5,7 +5,7 @@ %%% Created : 23 Feb 2012 by Evgeniy Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -64,7 +64,11 @@ to_float/1, prefix/2, suffix/2, - to_integer/1]). + format/2, + to_integer/1, + sha/1, + to_hexlist/1, + translate_and_format/3]). %%%=================================================================== %%% API @@ -92,7 +96,10 @@ rchr(B, C) -> -spec str(binary(), binary()) -> non_neg_integer(). str(B1, B2) -> - string:str(binary_to_list(B1), binary_to_list(B2)). + case binary:match(B1, B2) of + {R, _Len} -> R+1; + _ -> 0 + end. -spec rstr(binary(), binary()) -> non_neg_integer(). @@ -112,7 +119,7 @@ cspan(B1, B2) -> -spec copies(binary(), non_neg_integer()) -> binary(). copies(B, N) -> - iolist_to_binary(string:copies(binary_to_list(B), N)). + binary:copy(B, N). -spec words(binary()) -> pos_integer(). @@ -200,7 +207,7 @@ join(L, Sep) -> -spec substr(binary(), pos_integer()) -> binary(). substr(B, N) -> - iolist_to_binary(string:substr(binary_to_list(B), N)). + binary_part(B, N-1, byte_size(B)-N+1). -spec chr(binary(), char()) -> non_neg_integer(). @@ -220,7 +227,7 @@ chars(C, N) -> -spec substr(binary(), pos_integer(), non_neg_integer()) -> binary(). substr(B, S, E) -> - iolist_to_binary(string:substr(binary_to_list(B), S, E)). + binary_part(B, S-1, E). -spec strip(binary(), both | left | right, char()) -> binary(). @@ -277,6 +284,30 @@ prefix(Prefix, B) -> suffix(B1, B2) -> lists:suffix(binary_to_list(B1), binary_to_list(B2)). +-spec format(io:format(), list()) -> binary(). + +format(Format, Args) -> + unicode:characters_to_binary(io_lib:format(Format, Args)). + +-spec translate_and_format(binary(), binary(), list()) -> binary(). + +translate_and_format(Lang, Format, Args) -> + format(unicode:characters_to_list(translate:translate(Lang, Format)), Args). + + +-spec sha(iodata()) -> binary(). + +sha(Text) -> + Bin = crypto:hash(sha, Text), + to_hexlist(Bin). + +-spec to_hexlist(binary()) -> binary(). + +to_hexlist(S) when is_list(S) -> + to_hexlist(iolist_to_binary(S)); +to_hexlist(Bin) when is_binary(Bin) -> + << <<(digit_to_xchar(N div 16)), (digit_to_xchar(N rem 16))>> || <> <= Bin >>. + %%%=================================================================== %%% Internal functions %%%=================================================================== @@ -284,3 +315,6 @@ join_s([], _Sep) -> []; join_s([H|T], Sep) -> [H, [[Sep, X] || X <- T]]. + +digit_to_xchar(D) when (D >= 0) and (D < 10) -> D + $0; +digit_to_xchar(D) -> D + $a - 10. diff --git a/src/translate.erl b/src/translate.erl index 9e48e0b7a..5adde6e24 100644 --- a/src/translate.erl +++ b/src/translate.erl @@ -5,7 +5,7 @@ %%% Created : 6 Jan 2003 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -27,107 +27,147 @@ -author('alexey@process-one.net'). --export([start/0, load_dir/1, load_file/2, - translate/2]). +-behaviour(gen_server). + +-export([start_link/0, reload/0, translate/2]). +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). --include("ejabberd.hrl"). -include("logger.hrl"). +-include_lib("kernel/include/file.hrl"). -start() -> - ets:new(translations, [named_table, public]), - Dir = case os:getenv("EJABBERD_MSGS_PATH") of - false -> - case code:priv_dir(ejabberd) of - {error, _} -> ?MSGS_DIR; - Path -> filename:join([Path, "msgs"]) - end; - Path -> Path - end, - load_dir(iolist_to_binary(Dir)), - ok. +-define(ZERO_DATETIME, {{0,0,0}, {0,0,0}}). --spec load_dir(binary()) -> ok. +-type error_reason() :: file:posix() | {integer(), module(), term()} | + badarg | terminated | system_limit | bad_file | + bad_encoding. -load_dir(Dir) -> - case file:list_dir(Dir) of - {ok, Files} -> - MsgFiles = lists:filter(fun (FN) -> - case length(FN) > 4 of - true -> - string:substr(FN, length(FN) - 3) - == ".msg"; - _ -> false - end - end, - Files), - lists:foreach(fun (FNS) -> - FN = list_to_binary(FNS), - LP = ascii_tolower(str:substr(FN, 1, - byte_size(FN) - 4)), - L = case str:tokens(LP, <<".">>) of - [Language] -> Language; - [Language, _Project] -> Language - end, - load_file(L, <>) - end, - MsgFiles), - ok; - {error, Reason} -> ?ERROR_MSG("~p", [Reason]) +-record(state, {}). + +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +init([]) -> + process_flag(trap_exit, true), + case load() of + ok -> + xmpp:set_tr_callback({?MODULE, translate}), + {ok, #state{}}; + {error, Reason} -> + {stop, Reason} end. -load_file(Lang, File) -> - case file:open(File, [read]) of - {ok, Fd} -> - io:setopts(Fd, [{encoding,latin1}]), - load_file_loop(Fd, 1, File, Lang), - file:close(Fd); - Error -> - ExitText = iolist_to_binary([File, ": ", - file:format_error(Error)]), - ?ERROR_MSG("Problem loading translation file ~n~s", - [ExitText]), - exit(ExitText) +handle_call(Request, From, State) -> + ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), + {noreply, State}. + +handle_cast(Msg, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Msg]), + {noreply, State}. + +handle_info(Info, State) -> + ?WARNING_MSG("Unexpected info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, _State) -> + xmpp:set_tr_callback(undefined). + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +-spec reload() -> ok | {error, error_reason()}. +reload() -> + load(true). + +-spec load() -> ok | {error, error_reason()}. +load() -> + load(false). + +-spec load(boolean()) -> ok | {error, error_reason()}. +load(ForceCacheRebuild) -> + {MsgsDirMTime, MsgsDir} = get_msg_dir(), + {CacheMTime, CacheFile} = get_cache_file(), + {FilesMTime, MsgFiles} = get_msg_files(MsgsDir), + LastModified = lists:max([MsgsDirMTime, FilesMTime]), + if ForceCacheRebuild orelse CacheMTime < LastModified -> + case load(MsgFiles, MsgsDir) of + ok -> dump_to_file(CacheFile); + Err -> Err + end; + true -> + case ets:file2tab(CacheFile) of + {ok, _} -> + ok; + {error, {read_error, {file_error, _, enoent}}} -> + load(MsgFiles, MsgsDir); + {error, {read_error, {file_error, _, Reason}}} -> + ?WARNING_MSG("Failed to read translation cache from ~ts: ~ts", + [CacheFile, format_error(Reason)]), + load(MsgFiles, MsgsDir); + {error, Reason} -> + ?WARNING_MSG("Failed to read translation cache from ~ts: ~p", + [CacheFile, Reason]), + load(MsgFiles, MsgsDir) + end end. -load_file_loop(Fd, Line, File, Lang) -> - case io:read(Fd, '', Line) of - {ok,{Orig, Trans}, NextLine} -> - Trans1 = case Trans of - <<"">> -> Orig; - _ -> Trans - end, - ets:insert(translations, - {{Lang, iolist_to_binary(Orig)}, - iolist_to_binary(Trans1)}), +-spec load([file:filename()], file:filename()) -> ok | {error, error_reason()}. +load(Files, Dir) -> + try ets:new(translations, [named_table, public]) of + _ -> ok + catch _:badarg -> ok + end, + case Files of + [] -> + ?WARNING_MSG("No translation files found in ~ts, " + "check directory access", + [Dir]); + _ -> + ?INFO_MSG("Building language translation cache", []), + Objs = lists:flatten(misc:pmap(fun load_file/1, Files)), + case lists:keyfind(error, 1, Objs) of + false -> + ets:delete_all_objects(translations), + ets:insert(translations, Objs), + ?DEBUG("Language translation cache built successfully", []); + {error, File, Reason} -> + ?ERROR_MSG("Failed to read translation file ~ts: ~ts", + [File, format_error(Reason)]), + {error, Reason} + end + end. - load_file_loop(Fd, NextLine, File, Lang); - {ok,_, _NextLine} -> - ExitText = iolist_to_binary([File, - " approximately in the line ", - Line]), - ?ERROR_MSG("Problem loading translation file ~n~s", - [ExitText]), - exit(ExitText); - {error, - {_LineNumber, erl_parse, _ParseMessage} = Reason} -> - ExitText = iolist_to_binary([File, - " approximately in the line ", - file:format_error(Reason)]), - ?ERROR_MSG("Problem loading translation file ~n~s", - [ExitText]), - exit(ExitText); - {error, Reason} -> - ExitText = iolist_to_binary([File, ": ", - file:format_error(Reason)]), - ?ERROR_MSG("Problem loading translation file ~n~s", - [ExitText]), - exit(ExitText); - {eof,_Line} -> - ok +-spec load_file(file:filename()) -> [{{binary(), binary()}, binary()} | + {error, file:filename(), error_reason()}]. +load_file(File) -> + Lang = lang_of_file(File), + try file:consult(File) of + {ok, Lines} -> + lists:map( + fun({In, Out}) -> + try {unicode:characters_to_binary(In), + unicode:characters_to_binary(Out)} of + {InB, OutB} when is_binary(InB), is_binary(OutB) -> + {{Lang, InB}, OutB}; + _ -> + {error, File, bad_encoding} + catch _:badarg -> + {error, File, bad_encoding} + end; + (_) -> + {error, File, bad_file} + end, Lines); + {error, Reason} -> + [{error, File, Reason}] + catch _:{case_clause, {error, _}} -> + %% At the moment of the writing there was a bug in + %% file:consult_stream/3 - it doesn't process {error, term()} + %% result from io:read/3 + [{error, File, bad_file}] end. -spec translate(binary(), binary()) -> binary(). - translate(Lang, Msg) -> LLang = ascii_tolower(Lang), case ets:lookup(translations, {LLang, Msg}) of @@ -148,8 +188,9 @@ translate(Lang, Msg) -> end end. +-spec translate(binary()) -> binary(). translate(Msg) -> - case ?MYLANG of + case ejabberd_option:language() of <<"en">> -> Msg; Lang -> LLang = ascii_tolower(Lang), @@ -172,10 +213,93 @@ translate(Msg) -> end end. -ascii_tolower(B) -> - iolist_to_binary(ascii_tolower_s(binary_to_list(B))). +-spec ascii_tolower(list() | binary()) -> binary(). +ascii_tolower(B) when is_binary(B) -> + << <<(if X >= $A, X =< $Z -> + X + 32; + true -> + X + end)>> || <> <= B >>; +ascii_tolower(S) -> + ascii_tolower(unicode:characters_to_binary(S)). -ascii_tolower_s([C | Cs]) when C >= $A, C =< $Z -> - [C + ($a - $A) | ascii_tolower_s(Cs)]; -ascii_tolower_s([C | Cs]) -> [C | ascii_tolower_s(Cs)]; -ascii_tolower_s([]) -> []. +-spec get_msg_dir() -> {calendar:datetime(), file:filename()}. +get_msg_dir() -> + Dir = misc:msgs_dir(), + case file:read_file_info(Dir) of + {ok, #file_info{mtime = MTime}} -> + {MTime, Dir}; + {error, Reason} -> + ?ERROR_MSG("Failed to read directory ~ts: ~ts", + [Dir, format_error(Reason)]), + {?ZERO_DATETIME, Dir} + end. + +-spec get_msg_files(file:filename()) -> {calendar:datetime(), [file:filename()]}. +get_msg_files(MsgsDir) -> + Res = filelib:fold_files( + MsgsDir, ".+\\.msg", false, + fun(File, {MTime, Files} = Acc) -> + case xmpp_lang:is_valid(lang_of_file(File)) of + true -> + case file:read_file_info(File) of + {ok, #file_info{mtime = Time}} -> + {lists:max([MTime, Time]), [File|Files]}; + {error, Reason} -> + ?ERROR_MSG("Failed to read translation file ~ts: ~ts", + [File, format_error(Reason)]), + Acc + end; + false -> + ?WARNING_MSG("Ignoring translation file ~ts: file name " + "must be a valid language tag", + [File]), + Acc + end + end, {?ZERO_DATETIME, []}), + case Res of + {_, []} -> + case file:list_dir(MsgsDir) of + {ok, _} -> ok; + {error, Reason} -> + ?ERROR_MSG("Failed to read directory ~ts: ~ts", + [MsgsDir, format_error(Reason)]) + end; + _ -> + ok + end, + Res. + +-spec get_cache_file() -> {calendar:datetime(), file:filename()}. +get_cache_file() -> + MnesiaDir = mnesia:system_info(directory), + CacheFile = filename:join(MnesiaDir, "translations.cache"), + CacheMTime = case file:read_file_info(CacheFile) of + {ok, #file_info{mtime = Time}} -> Time; + {error, _} -> ?ZERO_DATETIME + end, + {CacheMTime, CacheFile}. + +-spec dump_to_file(file:filename()) -> ok. +dump_to_file(CacheFile) -> + case ets:tab2file(translations, CacheFile) of + ok -> ok; + {error, Reason} -> + ?WARNING_MSG("Failed to create translation cache in ~ts: ~p", + [CacheFile, Reason]) + end. + +-spec lang_of_file(file:filename()) -> binary(). +lang_of_file(FileName) -> + BaseName = filename:basename(FileName), + ascii_tolower(filename:rootname(BaseName)). + +-spec format_error(error_reason()) -> string(). +format_error(bad_file) -> + "corrupted or invalid translation file"; +format_error(bad_encoding) -> + "cannot translate from UTF-8"; +format_error({_, _, _} = Reason) -> + "at line " ++ file:format_error(Reason); +format_error(Reason) -> + file:format_error(Reason). diff --git a/src/win32_dns.erl b/src/win32_dns.erl index f0e4b5f28..f26f4cd34 100644 --- a/src/win32_dns.erl +++ b/src/win32_dns.erl @@ -5,7 +5,7 @@ %%% Created : 5 Mar 2009 by Geoff Cant %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -26,7 +26,6 @@ -module(win32_dns). -export([get_nameservers/0]). --include("ejabberd.hrl"). -include("logger.hrl"). -define(IF_KEY, "\\hklm\\system\\CurrentControlSet\\Services\\TcpIp\\Parameters\\Interfaces"). @@ -39,10 +38,9 @@ get_nameservers() -> is_good_ns(Addr) -> element(1, - inet_res:nnslookup("a.root-servers.net", in, any, [{Addr,53}], - timer:seconds(5) - ) - ) =:= ok. + inet_res:nnslookup("a.root-servers.net", in, a, [{Addr,53}], + timer:seconds(5))) + =:= ok. reg() -> {ok, R} = win32reg:open([read]), diff --git a/src/xml_compress.erl b/src/xml_compress.erl new file mode 100644 index 000000000..21b9044a0 --- /dev/null +++ b/src/xml_compress.erl @@ -0,0 +1,963 @@ +-module(xml_compress). +-export([encode/3, decode/3]). + +% This file was generated by xml_compress_gen +% +% Rules used: +% +% [{<<"eu.siacs.conversations.axolotl">>,<<"key">>, +% [{<<"prekey">>,[<<"true">>]},{<<"rid">>,[]}], +% []}, +% {<<"jabber:client">>,<<"message">>, +% [{<<"from">>,[j2,{j1}]}, +% {<<"id">>,[]}, +% {<<"to">>,[j1,j2,{j1}]}, +% {<<"type">>,[<<"chat">>,<<"groupchat">>,<<"normal">>]}, +% {<<"xml:lang">>,[<<"en">>]}], +% []}, +% {<<"urn:xmpp:hints">>,<<"store">>,[],[]}, +% {<<"jabber:client">>,<<"body">>,[], +% [<<73,32,115,101,110,116,32,121,111,117,32,97,110,32,79,77,69,77,79,32,101, +% 110,99,114,121,112,116,101,100,32,109,101,115,115,97,103,101,32,98,117, +% 116,32,121,111,117,114,32,99,108,105,101,110,116,32,100,111,101,115,110, +% 226,128,153,116,32,115,101,101,109,32,116,111,32,115,117,112,112,111, +% 114,116,32,116,104,97,116,46,32,70,105,110,100,32,109,111,114,101,32, +% 105,110,102,111,114,109,97,116,105,111,110,32,111,110,32,104,116,116, +% 112,115,58,47,47,99,111,110,118,101,114,115,97,116,105,111,110,115,46, +% 105,109,47,111,109,101,109,111>>]}, +% {<<"urn:xmpp:sid:0">>,<<"origin-id">>,[{<<"id">>,[]}],[]}, +% {<<"urn:xmpp:chat-markers:0">>,<<"markable">>,[],[]}, +% {<<"eu.siacs.conversations.axolotl">>,<<"encrypted">>,[],[]}, +% {<<"eu.siacs.conversations.axolotl">>,<<"header">>,[{<<"sid">>,[]}],[]}, +% {<<"eu.siacs.conversations.axolotl">>,<<"iv">>,[],[]}, +% {<<"eu.siacs.conversations.axolotl">>,<<"payload">>,[],[]}, +% {<<"urn:xmpp:eme:0">>,<<"encryption">>, +% [{<<"name">>,[<<"OMEMO">>]}, +% {<<"namespace">>,[<<"eu.siacs.conversations.axolotl">>]}], +% []}, +% {<<"urn:xmpp:delay">>,<<"delay">>,[{<<"from">>,[j1]},{<<"stamp">>,[]}],[]}, +% {<<"http://jabber.org/protocol/address">>,<<"address">>, +% [{<<"jid">>,[{j1}]},{<<"type">>,[<<"ofrom">>]}], +% []}, +% {<<"http://jabber.org/protocol/address">>,<<"addresses">>,[],[]}, +% {<<"urn:xmpp:chat-markers:0">>,<<"displayed">>, +% [{<<"id">>,[]},{<<"sender">>,[{j1},{j2}]}], +% []}, +% {<<"urn:xmpp:mam:tmp">>,<<"archived">>,[{<<"by">>,[]},{<<"id">>,[]}],[]}, +% {<<"urn:xmpp:sid:0">>,<<"stanza-id">>,[{<<"by">>,[]},{<<"id">>,[]}],[]}, +% {<<"urn:xmpp:receipts">>,<<"request">>,[],[]}, +% {<<"urn:xmpp:chat-markers:0">>,<<"received">>,[{<<"id">>,[]}],[]}, +% {<<"urn:xmpp:receipts">>,<<"received">>,[{<<"id">>,[]}],[]}, +% {<<"http://jabber.org/protocol/chatstates">>,<<"active">>,[],[]}, +% {<<"http://jabber.org/protocol/muc#user">>,<<"invite">>, +% [{<<"from">>,[{j1}]}], +% []}, +% {<<"http://jabber.org/protocol/muc#user">>,<<"reason">>,[],[]}, +% {<<"http://jabber.org/protocol/muc#user">>,<<"x">>,[],[]}, +% {<<"jabber:x:conference">>,<<"x">>,[{<<"jid">>,[j2]}],[]}, +% {<<"jabber:client">>,<<"subject">>,[],[]}, +% {<<"jabber:client">>,<<"thread">>,[],[]}, +% {<<"http://jabber.org/protocol/pubsub#event">>,<<"event">>,[],[]}, +% {<<"http://jabber.org/protocol/pubsub#event">>,<<"item">>,[{<<"id">>,[]}],[]}, +% {<<"http://jabber.org/protocol/pubsub#event">>,<<"items">>, +% [{<<"node">>,[<<"urn:xmpp:mucsub:nodes:messages">>]}], +% []}, +% {<<"p1:push:custom">>,<<"x">>,[{<<"key">>,[]},{<<"value">>,[]}],[]}, +% {<<"p1:pushed">>,<<"x">>,[],[]}, +% {<<"urn:xmpp:message-correct:0">>,<<"replace">>,[{<<"id">>,[]}],[]}, +% {<<"http://jabber.org/protocol/chatstates">>,<<"composing">>,[],[]}] + +encode(El, J1, J2) -> + encode_child(El, <<"jabber:client">>, + J1, J2, byte_size(J1), byte_size(J2), <<1:8>>). + +encode_attr({<<"xmlns">>, _}, Acc) -> + Acc; +encode_attr({N, V}, Acc) -> + <>. + +encode_attrs(Attrs, Acc) -> + lists:foldl(fun encode_attr/2, Acc, Attrs). + +encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> + E1 = if + PNs == Ns -> encode_attrs(Attrs, <>); + true -> encode_attrs(Attrs, <>) + end, + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>. + +encode_child({xmlel, Name, Attrs, Children}, PNs, J1, J2, J1L, J2L, Pfx) -> + case lists:keyfind(<<"xmlns">>, 1, Attrs) of + false -> + encode(PNs, PNs, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx); + {_, Ns} -> + encode(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) + end; +encode_child({xmlcdata, Data}, _PNs, _J1, _J2, _J1L, _J2L, Pfx) -> + <>. + +encode_children(Children, PNs, J1, J2, J1L, J2L, Pfx) -> + lists:foldl( + fun(Child, Acc) -> + encode_child(Child, PNs, J1, J2, J1L, J2L, Acc) + end, Pfx, Children). + +encode_string(Data) -> + <> = <<(byte_size(Data)):16/unsigned-big-integer>>, + case {V1, V2, V3} of + {0, 0, V3} -> + <>; + {0, V2, V3} -> + <<(V3 bor 64):8, V2:8, Data/binary>>; + _ -> + <<(V3 bor 64):8, (V2 bor 64):8, V1:8, Data/binary>> + end. + +encode(PNs, <<"eu.siacs.conversations.axolotl">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> + case Name of + <<"key">> -> + E = lists:foldl(fun + ({<<"prekey">>, AVal}, Acc) -> + case AVal of + <<"true">> -> <>; + _ -> <> + end; + ({<<"rid">>, AVal}, Acc) -> + <>; + (Attr, Acc) -> encode_attr(Attr, Acc) + end, <>, Attrs), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + <<"encrypted">> -> + E = encode_attrs(Attrs, <>), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + <<"header">> -> + E = lists:foldl(fun + ({<<"sid">>, AVal}, Acc) -> + <>; + (Attr, Acc) -> encode_attr(Attr, Acc) + end, <>, Attrs), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + <<"iv">> -> + E = encode_attrs(Attrs, <>), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + <<"payload">> -> + E = encode_attrs(Attrs, <>), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) +end; +encode(PNs, <<"jabber:client">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> + case Name of + <<"message">> -> + E = lists:foldl(fun + ({<<"from">>, AVal}, Acc) -> + case AVal of + J2 -> <>; + <> -> <>; + _ -> <> + end; + ({<<"id">>, AVal}, Acc) -> + <>; + ({<<"to">>, AVal}, Acc) -> + case AVal of + J1 -> <>; + J2 -> <>; + <> -> <>; + _ -> <> + end; + ({<<"type">>, AVal}, Acc) -> + case AVal of + <<"chat">> -> <>; + <<"groupchat">> -> <>; + <<"normal">> -> <>; + _ -> <> + end; + ({<<"xml:lang">>, AVal}, Acc) -> + case AVal of + <<"en">> -> <>; + _ -> <> + end; + (Attr, Acc) -> encode_attr(Attr, Acc) + end, <>, Attrs), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + <<"body">> -> + E = encode_attrs(Attrs, <>), + E2 = lists:foldl(fun + ({xmlcdata, <<73,32,115,101,110,116,32,121,111,117,32,97,110,32,79,77,69, + 77,79,32,101,110,99,114,121,112,116,101,100,32,109,101,115, + 115,97,103,101,32,98,117,116,32,121,111,117,114,32,99,108, + 105,101,110,116,32,100,111,101,115,110,226,128,153,116,32, + 115,101,101,109,32,116,111,32,115,117,112,112,111,114,116,32, + 116,104,97,116,46,32,70,105,110,100,32,109,111,114,101,32, + 105,110,102,111,114,109,97,116,105,111,110,32,111,110,32,104, + 116,116,112,115,58,47,47,99,111,110,118,101,114,115,97,116, + 105,111,110,115,46,105,109,47,111,109,101,109,111>>}, Acc) -> <>; + (El, Acc) -> encode_child(El, Ns, J1, J2, J1L, J2L, Acc) + end, <>, Children), + <>; + <<"subject">> -> + E = encode_attrs(Attrs, <>), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + <<"thread">> -> + E = encode_attrs(Attrs, <>), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) +end; +encode(PNs, <<"urn:xmpp:hints">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> + case Name of + <<"store">> -> + E = encode_attrs(Attrs, <>), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) +end; +encode(PNs, <<"urn:xmpp:sid:0">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> + case Name of + <<"origin-id">> -> + E = lists:foldl(fun + ({<<"id">>, AVal}, Acc) -> + <>; + (Attr, Acc) -> encode_attr(Attr, Acc) + end, <>, Attrs), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + <<"stanza-id">> -> + E = lists:foldl(fun + ({<<"by">>, AVal}, Acc) -> + <>; + ({<<"id">>, AVal}, Acc) -> + <>; + (Attr, Acc) -> encode_attr(Attr, Acc) + end, <>, Attrs), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) +end; +encode(PNs, <<"urn:xmpp:chat-markers:0">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> + case Name of + <<"markable">> -> + E = encode_attrs(Attrs, <>), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + <<"displayed">> -> + E = lists:foldl(fun + ({<<"id">>, AVal}, Acc) -> + <>; + ({<<"sender">>, AVal}, Acc) -> + case AVal of + <> -> <>; + <> -> <>; + _ -> <> + end; + (Attr, Acc) -> encode_attr(Attr, Acc) + end, <>, Attrs), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + <<"received">> -> + E = lists:foldl(fun + ({<<"id">>, AVal}, Acc) -> + <>; + (Attr, Acc) -> encode_attr(Attr, Acc) + end, <>, Attrs), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) +end; +encode(PNs, <<"urn:xmpp:eme:0">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> + case Name of + <<"encryption">> -> + E = lists:foldl(fun + ({<<"name">>, AVal}, Acc) -> + case AVal of + <<"OMEMO">> -> <>; + _ -> <> + end; + ({<<"namespace">>, AVal}, Acc) -> + case AVal of + <<"eu.siacs.conversations.axolotl">> -> <>; + _ -> <> + end; + (Attr, Acc) -> encode_attr(Attr, Acc) + end, <>, Attrs), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) +end; +encode(PNs, <<"urn:xmpp:delay">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> + case Name of + <<"delay">> -> + E = lists:foldl(fun + ({<<"from">>, AVal}, Acc) -> + case AVal of + J1 -> <>; + _ -> <> + end; + ({<<"stamp">>, AVal}, Acc) -> + <>; + (Attr, Acc) -> encode_attr(Attr, Acc) + end, <>, Attrs), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) +end; +encode(PNs, <<"http://jabber.org/protocol/address">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> + case Name of + <<"address">> -> + E = lists:foldl(fun + ({<<"jid">>, AVal}, Acc) -> + case AVal of + <> -> <>; + _ -> <> + end; + ({<<"type">>, AVal}, Acc) -> + case AVal of + <<"ofrom">> -> <>; + _ -> <> + end; + (Attr, Acc) -> encode_attr(Attr, Acc) + end, <>, Attrs), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + <<"addresses">> -> + E = encode_attrs(Attrs, <>), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) +end; +encode(PNs, <<"urn:xmpp:mam:tmp">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> + case Name of + <<"archived">> -> + E = lists:foldl(fun + ({<<"by">>, AVal}, Acc) -> + <>; + ({<<"id">>, AVal}, Acc) -> + <>; + (Attr, Acc) -> encode_attr(Attr, Acc) + end, <>, Attrs), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) +end; +encode(PNs, <<"urn:xmpp:receipts">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> + case Name of + <<"request">> -> + E = encode_attrs(Attrs, <>), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + <<"received">> -> + E = lists:foldl(fun + ({<<"id">>, AVal}, Acc) -> + <>; + (Attr, Acc) -> encode_attr(Attr, Acc) + end, <>, Attrs), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) +end; +encode(PNs, <<"http://jabber.org/protocol/chatstates">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> + case Name of + <<"active">> -> + E = encode_attrs(Attrs, <>), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + <<"composing">> -> + E = encode_attrs(Attrs, <>), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) +end; +encode(PNs, <<"http://jabber.org/protocol/muc#user">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> + case Name of + <<"invite">> -> + E = lists:foldl(fun + ({<<"from">>, AVal}, Acc) -> + case AVal of + <> -> <>; + _ -> <> + end; + (Attr, Acc) -> encode_attr(Attr, Acc) + end, <>, Attrs), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + <<"reason">> -> + E = encode_attrs(Attrs, <>), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + <<"x">> -> + E = encode_attrs(Attrs, <>), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) +end; +encode(PNs, <<"jabber:x:conference">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> + case Name of + <<"x">> -> + E = lists:foldl(fun + ({<<"jid">>, AVal}, Acc) -> + case AVal of + J2 -> <>; + _ -> <> + end; + (Attr, Acc) -> encode_attr(Attr, Acc) + end, <>, Attrs), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) +end; +encode(PNs, <<"http://jabber.org/protocol/pubsub#event">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> + case Name of + <<"event">> -> + E = encode_attrs(Attrs, <>), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + <<"item">> -> + E = lists:foldl(fun + ({<<"id">>, AVal}, Acc) -> + <>; + (Attr, Acc) -> encode_attr(Attr, Acc) + end, <>, Attrs), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + <<"items">> -> + E = lists:foldl(fun + ({<<"node">>, AVal}, Acc) -> + case AVal of + <<"urn:xmpp:mucsub:nodes:messages">> -> <>; + _ -> <> + end; + (Attr, Acc) -> encode_attr(Attr, Acc) + end, <>, Attrs), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) +end; +encode(PNs, <<"p1:push:custom">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> + case Name of + <<"x">> -> + E = lists:foldl(fun + ({<<"key">>, AVal}, Acc) -> + <>; + ({<<"value">>, AVal}, Acc) -> + <>; + (Attr, Acc) -> encode_attr(Attr, Acc) + end, <>, Attrs), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) +end; +encode(PNs, <<"p1:pushed">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> + case Name of + <<"x">> -> + E = encode_attrs(Attrs, <>), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) +end; +encode(PNs, <<"urn:xmpp:message-correct:0">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> + case Name of + <<"replace">> -> + E = lists:foldl(fun + ({<<"id">>, AVal}, Acc) -> + <>; + (Attr, Acc) -> encode_attr(Attr, Acc) + end, <>, Attrs), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) +end; +encode(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> + encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx). + +decode(<<$<, _/binary>> = Data, _J1, _J2) -> + fxml_stream:parse_element(Data); +decode(<<1:8, Rest/binary>>, J1, J2) -> + try decode(Rest, <<"jabber:client">>, J1, J2, false) of + {El, _} -> El + catch throw:loop_detected -> + {error, {loop_detected, <<"Compressed data corrupted">>}} + end. + +decode_string(Data) -> + case Data of + <<0:2, L:6, Str:L/binary, Rest/binary>> -> + {Str, Rest}; + <<1:2, L1:6, 0:2, L2:6, Rest/binary>> -> + L = L2*64 + L1, + <> = Rest, + {Str, Rest2}; + <<1:2, L1:6, 1:2, L2:6, L3:8, Rest/binary>> -> + L = (L3*64 + L2)*64 + L1, + <> = Rest, + {Str, Rest2} + end. + +decode_child(<<1:8, Rest/binary>>, _PNs, _J1, _J2, _) -> + {Text, Rest2} = decode_string(Rest), + {{xmlcdata, Text}, Rest2}; +decode_child(<<2:8, Rest/binary>>, PNs, J1, J2, _) -> + {Name, Rest2} = decode_string(Rest), + {Attrs, Rest3} = decode_attrs(Rest2), + {Children, Rest4} = decode_children(Rest3, PNs, J1, J2), + {{xmlel, Name, Attrs, Children}, Rest4}; +decode_child(<<3:8, Rest/binary>>, PNs, J1, J2, _) -> + {Ns, Rest2} = decode_string(Rest), + {Name, Rest3} = decode_string(Rest2), + {Attrs, Rest4} = decode_attrs(Rest3), + {Children, Rest5} = decode_children(Rest4, Ns, J1, J2), + {{xmlel, Name, add_ns(PNs, Ns, Attrs), Children}, Rest5}; +decode_child(<<4:8, Rest/binary>>, _PNs, _J1, _J2, _) -> + {stop, Rest}; +decode_child(_Other, _PNs, _J1, _J2, true) -> + throw(loop_detected); +decode_child(Other, PNs, J1, J2, _) -> + decode(Other, PNs, J1, J2, true). + +decode_children(Data, PNs, J1, J2) -> + prefix_map(fun(Data2) -> decode(Data2, PNs, J1, J2, false) end, Data). + +decode_attr(<<1:8, Rest/binary>>) -> + {Name, Rest2} = decode_string(Rest), + {Val, Rest3} = decode_string(Rest2), + {{Name, Val}, Rest3}; +decode_attr(<<2:8, Rest/binary>>) -> + {stop, Rest}. + +decode_attrs(Data) -> + prefix_map(fun decode_attr/1, Data). + +prefix_map(F, Data) -> + prefix_map(F, Data, []). + +prefix_map(F, Data, Acc) -> + case F(Data) of + {stop, Rest} -> + {lists:reverse(Acc), Rest}; + {Val, Rest} -> + prefix_map(F, Rest, [Val | Acc]) + end. + +add_ns(Ns, Ns, Attrs) -> + Attrs; +add_ns(_, Ns, Attrs) -> + [{<<"xmlns">>, Ns} | Attrs]. + +decode(<<5:8, Rest/binary>>, PNs, J1, J2, _) -> + Ns = <<"eu.siacs.conversations.axolotl">>, + {Attrs, Rest2} = prefix_map(fun + (<<3:8, Rest3/binary>>) -> + {{<<"prekey">>, <<"true">>}, Rest3}; + (<<4:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"prekey">>, AVal}, Rest4}; + (<<5:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"rid">>, AVal}, Rest4}; + (<<2:8, Rest3/binary>>) -> + {stop, Rest3}; + (Data) -> + decode_attr(Data) + end, Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"key">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; +decode(<<12:8, Rest/binary>>, PNs, J1, J2, _) -> + Ns = <<"eu.siacs.conversations.axolotl">>, + {Attrs, Rest2} = decode_attrs(Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"encrypted">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; +decode(<<13:8, Rest/binary>>, PNs, J1, J2, _) -> + Ns = <<"eu.siacs.conversations.axolotl">>, + {Attrs, Rest2} = prefix_map(fun + (<<3:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"sid">>, AVal}, Rest4}; + (<<2:8, Rest3/binary>>) -> + {stop, Rest3}; + (Data) -> + decode_attr(Data) + end, Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"header">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; +decode(<<14:8, Rest/binary>>, PNs, J1, J2, _) -> + Ns = <<"eu.siacs.conversations.axolotl">>, + {Attrs, Rest2} = decode_attrs(Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"iv">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; +decode(<<15:8, Rest/binary>>, PNs, J1, J2, _) -> + Ns = <<"eu.siacs.conversations.axolotl">>, + {Attrs, Rest2} = decode_attrs(Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"payload">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; +decode(<<6:8, Rest/binary>>, PNs, J1, J2, _) -> + Ns = <<"jabber:client">>, + {Attrs, Rest2} = prefix_map(fun + (<<3:8, Rest3/binary>>) -> + {{<<"from">>, J2}, Rest3}; + (<<4:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"from">>, <>}, Rest4}; + (<<5:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"from">>, AVal}, Rest4}; + (<<6:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"id">>, AVal}, Rest4}; + (<<7:8, Rest3/binary>>) -> + {{<<"to">>, J1}, Rest3}; + (<<8:8, Rest3/binary>>) -> + {{<<"to">>, J2}, Rest3}; + (<<9:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"to">>, <>}, Rest4}; + (<<10:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"to">>, AVal}, Rest4}; + (<<11:8, Rest3/binary>>) -> + {{<<"type">>, <<"chat">>}, Rest3}; + (<<12:8, Rest3/binary>>) -> + {{<<"type">>, <<"groupchat">>}, Rest3}; + (<<13:8, Rest3/binary>>) -> + {{<<"type">>, <<"normal">>}, Rest3}; + (<<14:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"type">>, AVal}, Rest4}; + (<<15:8, Rest3/binary>>) -> + {{<<"xml:lang">>, <<"en">>}, Rest3}; + (<<16:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"xml:lang">>, AVal}, Rest4}; + (<<2:8, Rest3/binary>>) -> + {stop, Rest3}; + (Data) -> + decode_attr(Data) + end, Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"message">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; +decode(<<8:8, Rest/binary>>, PNs, J1, J2, _) -> + Ns = <<"jabber:client">>, + {Attrs, Rest2} = decode_attrs(Rest), + {Children, Rest6} = prefix_map(fun (<<9:8, Rest5/binary>>) -> + {{xmlcdata, <<73,32,115,101,110,116,32,121,111,117,32,97,110,32,79,77,69, + 77,79,32,101,110,99,114,121,112,116,101,100,32,109,101,115, + 115,97,103,101,32,98,117,116,32,121,111,117,114,32,99,108, + 105,101,110,116,32,100,111,101,115,110,226,128,153,116,32, + 115,101,101,109,32,116,111,32,115,117,112,112,111,114,116, + 32,116,104,97,116,46,32,70,105,110,100,32,109,111,114,101, + 32,105,110,102,111,114,109,97,116,105,111,110,32,111,110, + 32,104,116,116,112,115,58,47,47,99,111,110,118,101,114,115, + 97,116,105,111,110,115,46,105,109,47,111,109,101,109,111>>}, Rest5}; + (Other) -> + decode_child(Other, Ns, J1, J2, false) + end, Rest2), + {{xmlel, <<"body">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; +decode(<<31:8, Rest/binary>>, PNs, J1, J2, _) -> + Ns = <<"jabber:client">>, + {Attrs, Rest2} = decode_attrs(Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"subject">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; +decode(<<32:8, Rest/binary>>, PNs, J1, J2, _) -> + Ns = <<"jabber:client">>, + {Attrs, Rest2} = decode_attrs(Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"thread">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; +decode(<<7:8, Rest/binary>>, PNs, J1, J2, _) -> + Ns = <<"urn:xmpp:hints">>, + {Attrs, Rest2} = decode_attrs(Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"store">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; +decode(<<10:8, Rest/binary>>, PNs, J1, J2, _) -> + Ns = <<"urn:xmpp:sid:0">>, + {Attrs, Rest2} = prefix_map(fun + (<<3:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"id">>, AVal}, Rest4}; + (<<2:8, Rest3/binary>>) -> + {stop, Rest3}; + (Data) -> + decode_attr(Data) + end, Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"origin-id">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; +decode(<<22:8, Rest/binary>>, PNs, J1, J2, _) -> + Ns = <<"urn:xmpp:sid:0">>, + {Attrs, Rest2} = prefix_map(fun + (<<3:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"by">>, AVal}, Rest4}; + (<<4:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"id">>, AVal}, Rest4}; + (<<2:8, Rest3/binary>>) -> + {stop, Rest3}; + (Data) -> + decode_attr(Data) + end, Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"stanza-id">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; +decode(<<11:8, Rest/binary>>, PNs, J1, J2, _) -> + Ns = <<"urn:xmpp:chat-markers:0">>, + {Attrs, Rest2} = decode_attrs(Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"markable">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; +decode(<<20:8, Rest/binary>>, PNs, J1, J2, _) -> + Ns = <<"urn:xmpp:chat-markers:0">>, + {Attrs, Rest2} = prefix_map(fun + (<<3:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"id">>, AVal}, Rest4}; + (<<4:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"sender">>, <>}, Rest4}; + (<<5:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"sender">>, <>}, Rest4}; + (<<6:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"sender">>, AVal}, Rest4}; + (<<2:8, Rest3/binary>>) -> + {stop, Rest3}; + (Data) -> + decode_attr(Data) + end, Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"displayed">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; +decode(<<24:8, Rest/binary>>, PNs, J1, J2, _) -> + Ns = <<"urn:xmpp:chat-markers:0">>, + {Attrs, Rest2} = prefix_map(fun + (<<3:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"id">>, AVal}, Rest4}; + (<<2:8, Rest3/binary>>) -> + {stop, Rest3}; + (Data) -> + decode_attr(Data) + end, Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"received">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; +decode(<<16:8, Rest/binary>>, PNs, J1, J2, _) -> + Ns = <<"urn:xmpp:eme:0">>, + {Attrs, Rest2} = prefix_map(fun + (<<3:8, Rest3/binary>>) -> + {{<<"name">>, <<"OMEMO">>}, Rest3}; + (<<4:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"name">>, AVal}, Rest4}; + (<<5:8, Rest3/binary>>) -> + {{<<"namespace">>, <<"eu.siacs.conversations.axolotl">>}, Rest3}; + (<<6:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"namespace">>, AVal}, Rest4}; + (<<2:8, Rest3/binary>>) -> + {stop, Rest3}; + (Data) -> + decode_attr(Data) + end, Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"encryption">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; +decode(<<17:8, Rest/binary>>, PNs, J1, J2, _) -> + Ns = <<"urn:xmpp:delay">>, + {Attrs, Rest2} = prefix_map(fun + (<<3:8, Rest3/binary>>) -> + {{<<"from">>, J1}, Rest3}; + (<<4:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"from">>, AVal}, Rest4}; + (<<5:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"stamp">>, AVal}, Rest4}; + (<<2:8, Rest3/binary>>) -> + {stop, Rest3}; + (Data) -> + decode_attr(Data) + end, Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"delay">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; +decode(<<18:8, Rest/binary>>, PNs, J1, J2, _) -> + Ns = <<"http://jabber.org/protocol/address">>, + {Attrs, Rest2} = prefix_map(fun + (<<3:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"jid">>, <>}, Rest4}; + (<<4:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"jid">>, AVal}, Rest4}; + (<<5:8, Rest3/binary>>) -> + {{<<"type">>, <<"ofrom">>}, Rest3}; + (<<6:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"type">>, AVal}, Rest4}; + (<<2:8, Rest3/binary>>) -> + {stop, Rest3}; + (Data) -> + decode_attr(Data) + end, Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"address">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; +decode(<<19:8, Rest/binary>>, PNs, J1, J2, _) -> + Ns = <<"http://jabber.org/protocol/address">>, + {Attrs, Rest2} = decode_attrs(Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"addresses">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; +decode(<<21:8, Rest/binary>>, PNs, J1, J2, _) -> + Ns = <<"urn:xmpp:mam:tmp">>, + {Attrs, Rest2} = prefix_map(fun + (<<3:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"by">>, AVal}, Rest4}; + (<<4:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"id">>, AVal}, Rest4}; + (<<2:8, Rest3/binary>>) -> + {stop, Rest3}; + (Data) -> + decode_attr(Data) + end, Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"archived">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; +decode(<<23:8, Rest/binary>>, PNs, J1, J2, _) -> + Ns = <<"urn:xmpp:receipts">>, + {Attrs, Rest2} = decode_attrs(Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"request">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; +decode(<<25:8, Rest/binary>>, PNs, J1, J2, _) -> + Ns = <<"urn:xmpp:receipts">>, + {Attrs, Rest2} = prefix_map(fun + (<<3:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"id">>, AVal}, Rest4}; + (<<2:8, Rest3/binary>>) -> + {stop, Rest3}; + (Data) -> + decode_attr(Data) + end, Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"received">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; +decode(<<26:8, Rest/binary>>, PNs, J1, J2, _) -> + Ns = <<"http://jabber.org/protocol/chatstates">>, + {Attrs, Rest2} = decode_attrs(Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"active">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; +decode(<<39:8, Rest/binary>>, PNs, J1, J2, _) -> + Ns = <<"http://jabber.org/protocol/chatstates">>, + {Attrs, Rest2} = decode_attrs(Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"composing">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; +decode(<<27:8, Rest/binary>>, PNs, J1, J2, _) -> + Ns = <<"http://jabber.org/protocol/muc#user">>, + {Attrs, Rest2} = prefix_map(fun + (<<3:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"from">>, <>}, Rest4}; + (<<4:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"from">>, AVal}, Rest4}; + (<<2:8, Rest3/binary>>) -> + {stop, Rest3}; + (Data) -> + decode_attr(Data) + end, Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"invite">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; +decode(<<28:8, Rest/binary>>, PNs, J1, J2, _) -> + Ns = <<"http://jabber.org/protocol/muc#user">>, + {Attrs, Rest2} = decode_attrs(Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"reason">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; +decode(<<29:8, Rest/binary>>, PNs, J1, J2, _) -> + Ns = <<"http://jabber.org/protocol/muc#user">>, + {Attrs, Rest2} = decode_attrs(Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"x">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; +decode(<<30:8, Rest/binary>>, PNs, J1, J2, _) -> + Ns = <<"jabber:x:conference">>, + {Attrs, Rest2} = prefix_map(fun + (<<3:8, Rest3/binary>>) -> + {{<<"jid">>, J2}, Rest3}; + (<<4:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"jid">>, AVal}, Rest4}; + (<<2:8, Rest3/binary>>) -> + {stop, Rest3}; + (Data) -> + decode_attr(Data) + end, Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"x">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; +decode(<<33:8, Rest/binary>>, PNs, J1, J2, _) -> + Ns = <<"http://jabber.org/protocol/pubsub#event">>, + {Attrs, Rest2} = decode_attrs(Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"event">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; +decode(<<34:8, Rest/binary>>, PNs, J1, J2, _) -> + Ns = <<"http://jabber.org/protocol/pubsub#event">>, + {Attrs, Rest2} = prefix_map(fun + (<<3:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"id">>, AVal}, Rest4}; + (<<2:8, Rest3/binary>>) -> + {stop, Rest3}; + (Data) -> + decode_attr(Data) + end, Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"item">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; +decode(<<35:8, Rest/binary>>, PNs, J1, J2, _) -> + Ns = <<"http://jabber.org/protocol/pubsub#event">>, + {Attrs, Rest2} = prefix_map(fun + (<<3:8, Rest3/binary>>) -> + {{<<"node">>, <<"urn:xmpp:mucsub:nodes:messages">>}, Rest3}; + (<<4:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"node">>, AVal}, Rest4}; + (<<2:8, Rest3/binary>>) -> + {stop, Rest3}; + (Data) -> + decode_attr(Data) + end, Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"items">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; +decode(<<36:8, Rest/binary>>, PNs, J1, J2, _) -> + Ns = <<"p1:push:custom">>, + {Attrs, Rest2} = prefix_map(fun + (<<3:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"key">>, AVal}, Rest4}; + (<<4:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"value">>, AVal}, Rest4}; + (<<2:8, Rest3/binary>>) -> + {stop, Rest3}; + (Data) -> + decode_attr(Data) + end, Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"x">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; +decode(<<37:8, Rest/binary>>, PNs, J1, J2, _) -> + Ns = <<"p1:pushed">>, + {Attrs, Rest2} = decode_attrs(Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"x">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; +decode(<<38:8, Rest/binary>>, PNs, J1, J2, _) -> + Ns = <<"urn:xmpp:message-correct:0">>, + {Attrs, Rest2} = prefix_map(fun + (<<3:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"id">>, AVal}, Rest4}; + (<<2:8, Rest3/binary>>) -> + {stop, Rest3}; + (Data) -> + decode_attr(Data) + end, Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"replace">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; +decode(Other, PNs, J1, J2, Loop) -> + decode_child(Other, PNs, J1, J2, Loop). + diff --git a/test/README b/test/README index 99e5eec12..8254230d2 100644 --- a/test/README +++ b/test/README @@ -1,11 +1,12 @@ -You need MySQL, PostgreSQL and Riak up and running. +You need MySQL, MSSQL, PostgreSQL and Redis up and running. MySQL should be accepting TCP connections on localhost:3306. +MSSQL should be accepting TCP connections on localhost:1433. PostgreSQL should be accepting TCP connections on localhost:5432. -Riak should be accepting TCP connections on localhost:8087. +Redis should be accepting TCP connections on localhost:6379. MySQL and PostgreSQL should grant full access to user 'ejabberd_test' with password 'ejabberd_test' on database 'ejabberd_test'. -Riak should be configured with leveldb as a database backend and -pz -should be pointed to the directory with ejabberd BEAM files. +MSSQL should grant full access to user 'ejabberd_test' with +password 'ejabberd_Test1' on database 'ejabberd_test'. Here is a quick setup example: @@ -16,6 +17,8 @@ $ psql template1 template1=# CREATE USER ejabberd_test WITH PASSWORD 'ejabberd_test'; template1=# CREATE DATABASE ejabberd_test; template1=# GRANT ALL PRIVILEGES ON DATABASE ejabberd_test TO ejabberd_test; +# If you disabled the update_sql_schema option, create the schema manually: +# $ psql ejabberd_test -f sql/pg.sql ------------------- MySQL @@ -24,27 +27,25 @@ $ mysql mysql> CREATE USER 'ejabberd_test'@'localhost' IDENTIFIED BY 'ejabberd_test'; mysql> CREATE DATABASE ejabberd_test; mysql> GRANT ALL ON ejabberd_test.* TO 'ejabberd_test'@'localhost'; +# If you disabled the update_sql_schema option, create the schema manually: +# $ mysql ejabberd_test < sql/mysql.sql ------------------- - Riak + MS SQL Server ------------------- -$ cat /etc/riak/vm.args -... -## Map/Reduce path --pz /path/to/ejabberd/ebin -... - -For version < 2.x: - -$ cat /etc/riak/app.config: -... - {riak_kv, [ - {storage_backend, riak_kv_eleveldb_backend}, -... - -For version >= 2.x: - -$ cat /etc/riak/riak.conf: -... -storage_backend = leveldb -... +$ sqlcmd -U SA -P ejabberd_Test1 -S localhost +1> CREATE DATABASE ejabberd_test; +2> GO +1> USE ejabberd_test; +2> GO +Changed database context to 'ejabberd_test'. +1> CREATE LOGIN ejabberd_test WITH PASSWORD = 'ejabberd_Test1'; +2> GO +1> CREATE USER ejabberd_test FOR LOGIN ejabberd_test; +2> GO +1> GRANT ALL TO ejabberd_test; +2> GO +The ALL permission is deprecated and maintained only for compatibility. It DOES NOT imply ALL permissions defined on the entity. +1> GRANT CONTROL ON SCHEMA ::dbo TO ejabberd_test; +2> GO +$ sqlcmd -U ejabberd_test -P ejabberd_Test1 -S localhost -d ejabberd_test -i sql/mssql.sql diff --git a/test/announce_tests.erl b/test/announce_tests.erl new file mode 100644 index 000000000..724baba27 --- /dev/null +++ b/test/announce_tests.erl @@ -0,0 +1,76 @@ +%%%------------------------------------------------------------------- +%%% Author : Evgeny Khramtsov +%%% Created : 16 Nov 2016 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(announce_tests). + +%% API +-compile(export_all). +-import(suite, [server_jid/1, send_recv/2, recv_message/1, disconnect/1, + send/2, wait_for_master/1, wait_for_slave/1]). + +-include("suite.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +%%%=================================================================== +%%% Single user tests +%%%=================================================================== +single_cases() -> + {announce_single, [sequence], []}. + +%%%=================================================================== +%%% Master-slave tests +%%%=================================================================== +master_slave_cases() -> + {announce_master_slave, [sequence], + [master_slave_test(set_motd)]}. + +set_motd_master(Config) -> + ServerJID = server_jid(Config), + MotdJID = jid:replace_resource(ServerJID, <<"announce/motd">>), + Body = xmpp:mk_text(<<"motd">>), + #presence{} = send_recv(Config, #presence{}), + wait_for_slave(Config), + send(Config, #message{to = MotdJID, body = Body}), + #message{from = ServerJID, body = Body} = recv_message(Config), + disconnect(Config). + +set_motd_slave(Config) -> + ServerJID = server_jid(Config), + Body = xmpp:mk_text(<<"motd">>), + #presence{} = send_recv(Config, #presence{}), + wait_for_master(Config), + #message{from = ServerJID, body = Body} = recv_message(Config), + disconnect(Config). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +single_test(T) -> + list_to_atom("announce_" ++ atom_to_list(T)). + +master_slave_test(T) -> + {list_to_atom("announce_" ++ atom_to_list(T)), [parallel], + [list_to_atom("announce_" ++ atom_to_list(T) ++ "_master"), + list_to_atom("announce_" ++ atom_to_list(T) ++ "_slave")]}. diff --git a/test/antispam_tests.erl b/test/antispam_tests.erl new file mode 100644 index 000000000..8d7bd2472 --- /dev/null +++ b/test/antispam_tests.erl @@ -0,0 +1,290 @@ +%%%------------------------------------------------------------------- +%%% Author : Stefan Strigler +%%% Created : 8 May 2025 by Stefan Strigler +%%% +%%% +%%% ejabberd, Copyright (C) 2025 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(antispam_tests). + +-compile(export_all). + +-import(suite, [recv_presence/1, send_recv/2, my_jid/1, muc_room_jid/1, + send/2, recv_message/1, recv_iq/1, muc_jid/1, + alt_room_jid/1, wait_for_slave/1, wait_for_master/1, + disconnect/1, put_event/2, get_event/1, peer_muc_jid/1, + my_muc_jid/1, get_features/2, set_opt/3]). +-include("suite.hrl"). +-include("mod_antispam.hrl"). + +%% @format-begin + +%%%=================================================================== +%%% API +%%%=================================================================== +%%%=================================================================== +%%% Single tests +%%%=================================================================== + +single_cases() -> + {antispam_single, + [sequence], + [single_test(block_by_jid), + single_test(block_by_url), + single_test(blocked_jid_is_cached), + single_test(uncache_blocked_jid), + single_test(check_blocked_domain), + single_test(unblock_domain), + single_test(empty_domain_list), + single_test(block_domain_globally), + single_test(check_domain_blocked_globally), + single_test(unblock_domain_in_vhost), + single_test(unblock_domain_globally), + single_test(block_domain_in_vhost), + single_test(unblock_domain_in_vhost2), + single_test(jid_cache), + single_test(rtbl_domains), + single_test(rtbl_domains_whitelisted), + single_test(spam_dump_file)]}. + +%%%=================================================================== + +block_by_jid(Config) -> + is_spam(message_hello(<<"spammer_jid">>, <<"localhost">>, Config)). + +block_by_url(Config) -> + From = jid:make(<<"spammer">>, <<"localhost">>, <<"spam_client">>), + To = my_jid(Config), + is_not_spam(message_hello(<<"spammer">>, <<"localhost">>, Config)), + is_spam(message(From, To, <<"hello world\nhttps://spam.domain.url">>)). + +blocked_jid_is_cached(Config) -> + is_spam(message_hello(<<"spammer">>, <<"localhost">>, Config)). + +uncache_blocked_jid(Config) -> + Host = ?config(server, Config), + Spammer = jid:make(<<"spammer">>, <<"localhost">>, <<"">>), + mod_antispam:drop_from_spam_filter_cache(Host, jid:to_string(Spammer)), + is_not_spam(message_hello(<<"spammer">>, <<"localhost">>, Config)). + +check_blocked_domain(Config) -> + is_spam(message_hello(<<"other_spammer">>, <<"spam_domain.org">>, Config)). + +unblock_domain(Config) -> + Host = ?config(server, Config), + ?match({ok, _}, mod_antispam:remove_blocked_domain(Host, <<"spam_domain.org">>)), + ?match([], mod_antispam:get_blocked_domains(Host)), + is_not_spam(message_hello(<<"spammer">>, <<"spam_domain.org">>, Config)). + +%%%=================================================================== + +empty_domain_list(Config) -> + Host = ?config(server, Config), + ?match([], mod_antispam:get_blocked_domains(Host)), + SpamFrom = jid:make(<<"spammer">>, <<"spam.domain">>, <<"spam_client">>), + To = my_jid(Config), + Msg = message(SpamFrom, To, <<"hello world">>), + is_not_spam(Msg). + +block_domain_globally(Config) -> + ?match({ok, _}, mod_antispam:add_blocked_domain(<<"global">>, <<"spam.domain">>)), + SpamFrom = jid:make(<<"spammer">>, <<"spam.domain">>, <<"spam_client">>), + To = my_jid(Config), + is_spam(message(SpamFrom, To, <<"hello world">>)). + +check_domain_blocked_globally(_Config) -> + Vhosts = [H || H <- ejabberd_option:hosts(), gen_mod:is_loaded(H, mod_antispam)], + NumVhosts = length(Vhosts), + ?match(NumVhosts, length(lists:filter(has_spam_domain(<<"spam.domain">>), Vhosts))). + +unblock_domain_in_vhost(Config) -> + Host = ?config(server, Config), + ?match({ok, _}, mod_antispam:remove_blocked_domain(Host, <<"spam.domain">>)), + ?match([], mod_antispam:get_blocked_domains(Host)), + SpamFrom = jid:make(<<"spammer">>, <<"spam.domain">>, <<"spam_client">>), + To = my_jid(Config), + is_not_spam(message(SpamFrom, To, <<"hello world">>)). + +unblock_domain_globally(_Config) -> + Vhosts = [H || H <- ejabberd_option:hosts(), gen_mod:is_loaded(H, mod_antispam)], + NumVhosts = length(Vhosts), + ?match(NumVhosts, length(lists:filter(has_spam_domain(<<"spam.domain">>), Vhosts)) + 1), + ?match({ok, _}, mod_antispam:remove_blocked_domain(<<"global">>, <<"spam.domain">>)), + ?match([], lists:filter(has_spam_domain(<<"spam.domain">>), Vhosts)). + +block_domain_in_vhost(Config) -> + Host = ?config(server, Config), + Vhosts = [H || H <- ejabberd_option:hosts(), gen_mod:is_loaded(H, mod_antispam)], + ?match({ok, _}, mod_antispam:add_blocked_domain(Host, <<"spam.domain">>)), + ?match([Host], lists:filter(has_spam_domain(<<"spam.domain">>), Vhosts)), + SpamFrom = jid:make(<<"spammer">>, <<"spam.domain">>, <<"spam_client">>), + To = my_jid(Config), + is_spam(message(SpamFrom, To, <<"hello world">>)). + +unblock_domain_in_vhost2(Config) -> + Host = ?config(server, Config), + ?match({ok, _}, mod_antispam:remove_blocked_domain(Host, <<"spam.domain">>)), + SpamFrom = jid:make(<<"spammer">>, <<"spam.domain">>, <<"spam_client">>), + To = my_jid(Config), + is_not_spam(message(SpamFrom, To, <<"hello world">>)), + disconnect(Config). + +%%%=================================================================== + +jid_cache(Config) -> + Host = ?config(server, Config), + SpamFrom = jid:make(<<"spammer">>, Host, <<"spam_client">>), + is_not_spam(message_hello(<<"spammer">>, Host, Config)), + mod_antispam:add_to_spam_filter_cache(Host, jid:to_string(SpamFrom)), + is_spam(message_hello(<<"spammer">>, Host, Config)), + mod_antispam:drop_from_spam_filter_cache(Host, jid:to_string(SpamFrom)), + is_not_spam(message_hello(<<"spammer">>, Host, Config)), + disconnect(Config). + +%%%=================================================================== + +rtbl_domains(Config) -> + Host = ?config(server, Config), + RTBLHost = + jid:to_string( + suite:pubsub_jid(Config)), + RTBLDomainsNode = <<"spam_source_domains">>, + OldOpts = gen_mod:get_module_opts(Host, mod_antispam), + NewOpts = + maps:merge(OldOpts, + #{rtbl_services => [#rtbl_service{host = RTBLHost, node = RTBLDomainsNode}]}), + Owner = jid:make(?config(user, Config), ?config(server, Config), <<>>), + {result, _} = + mod_pubsub:create_node(RTBLHost, + ?config(server, Config), + RTBLDomainsNode, + Owner, + <<"flat">>), + {result, _} = + mod_pubsub:publish_item(RTBLHost, + ?config(server, Config), + RTBLDomainsNode, + Owner, + <<"spam.source.domain">>, + [xmpp:encode(#ps_item{id = <<"spam.source.domain">>, + sub_els = []})]), + mod_antispam:reload(Host, OldOpts, NewOpts), + ?match({ok, _}, mod_antispam:remove_blocked_domain(Host, <<"spam_domain.org">>)), + ?retry(100, + 10, + ?match([<<"spam.source.domain">>], mod_antispam:get_blocked_domains(Host))), + {result, _} = + mod_pubsub:publish_item(RTBLHost, + ?config(server, Config), + RTBLDomainsNode, + Owner, + <<"spam.source.another">>, + [xmpp:encode(#ps_item{id = <<"spam.source.another">>, + sub_els = []})]), + ?retry(100, 10, ?match(true, (has_spam_domain(<<"spam.source.another">>))(Host))), + {result, _} = + mod_pubsub:delete_item(RTBLHost, RTBLDomainsNode, Owner, <<"spam.source.another">>, true), + ?retry(100, 10, ?match(false, (has_spam_domain(<<"spam.source.another">>))(Host))), + {result, _} = mod_pubsub:delete_node(RTBLHost, RTBLDomainsNode, Owner), + disconnect(Config). + +rtbl_domains_whitelisted(Config) -> + Host = ?config(server, Config), + RTBLHost = + jid:to_string( + suite:pubsub_jid(Config)), + RTBLDomainsNode = <<"spam_source_domains">>, + OldOpts = gen_mod:get_module_opts(Host, mod_antispam), + NewOpts = + maps:merge(OldOpts, + #{rtbl_services => [#rtbl_service{host = RTBLHost, node = RTBLDomainsNode}]}), + Owner = jid:make(?config(user, Config), ?config(server, Config), <<>>), + {result, _} = + mod_pubsub:create_node(RTBLHost, + ?config(server, Config), + RTBLDomainsNode, + Owner, + <<"flat">>), + {result, _} = + mod_pubsub:publish_item(RTBLHost, + ?config(server, Config), + RTBLDomainsNode, + Owner, + <<"whitelisted.domain">>, + [xmpp:encode(#ps_item{id = <<"whitelisted.domain">>, + sub_els = []})]), + mod_antispam:reload(Host, OldOpts, NewOpts), + {result, _} = + mod_pubsub:publish_item(RTBLHost, + ?config(server, Config), + RTBLDomainsNode, + Owner, + <<"yetanother.domain">>, + [xmpp:encode(#ps_item{id = <<"yetanother.domain">>, + sub_els = []})]), + ?retry(100, 10, ?match(true, (has_spam_domain(<<"yetanother.domain">>))(Host))), + %% we assume that the previous "whitelisted.domain" pubsub item has been consumed by now, so we + %% can check that it doesn't exist + ?match(false, (has_spam_domain(<<"whitelisted.domain">>))(Host)), + {result, _} = mod_pubsub:delete_node(RTBLHost, RTBLDomainsNode, Owner), + disconnect(Config). + +%%%=================================================================== + +spam_dump_file(Config) -> + {ok, CWD} = file:get_cwd(), + Filename = filename:join([CWD, "spam.log"]), + ?retry(100, 100, ?match(true, size(get_bytes(Filename)) > 0)), + From = jid:make(<<"spammer_jid">>, <<"localhost">>, <<"spam_client">>), + To = my_jid(Config), + is_spam(message(From, To, <<"A very specific spam message">>)), + ?retry(100, + 100, + ?match({match, _}, re:run(get_bytes(Filename), <<"A very specific spam message">>))). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +single_test(T) -> + list_to_atom("antispam_" ++ atom_to_list(T)). + +has_spam_domain(Domain) -> + fun(Host) -> lists:member(Domain, mod_antispam:get_blocked_domains(Host)) end. + +is_not_spam(Msg) -> + ?match({Msg, undefined}, mod_antispam_filter:s2s_receive_packet({Msg, undefined})). + +is_spam(Spam) -> + ?match({stop, {drop, undefined}}, + mod_antispam_filter:s2s_receive_packet({Spam, undefined})). + +message_hello(Username, Host, Config) -> + SpamFrom = jid:make(Username, Host, <<"spam_client">>), + To = my_jid(Config), + message(SpamFrom, To, <<"hello world">>). + +message(From, To, BodyText) -> + #message{from = From, + to = To, + type = chat, + body = [#text{data = BodyText}]}. + +get_bytes(Filename) -> + {ok, Bytes} = file:read_file(Filename), + Bytes. diff --git a/test/carbons_tests.erl b/test/carbons_tests.erl new file mode 100644 index 000000000..eabd3af2a --- /dev/null +++ b/test/carbons_tests.erl @@ -0,0 +1,211 @@ +%%%------------------------------------------------------------------- +%%% Author : Evgeny Khramtsov +%%% Created : 16 Nov 2016 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(carbons_tests). + +%% API +-compile(export_all). +-import(suite, [is_feature_advertised/2, disconnect/1, send_recv/2, + recv_presence/1, send/2, get_event/1, recv_message/1, + my_jid/1, wait_for_slave/1, wait_for_master/1, + put_event/2]). + +-include("suite.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +%%%=================================================================== +%%% Single user tests +%%%=================================================================== +single_cases() -> + {carbons_single, [sequence], + [single_test(feature_enabled), + single_test(unsupported_iq)]}. + +feature_enabled(Config) -> + true = is_feature_advertised(Config, ?NS_CARBONS_2), + disconnect(Config). + +unsupported_iq(Config) -> + lists:foreach( + fun({Type, SubEl}) -> + #iq{type = error} = + send_recv(Config, #iq{type = Type, sub_els = [SubEl]}) + end, [{Type, SubEl} || + Type <- [get, set], + SubEl <- [#carbons_sent{forwarded = #forwarded{}}, + #carbons_received{forwarded = #forwarded{}}, + #carbons_private{}]] ++ + [{get, SubEl} || SubEl <- [#carbons_enable{}, #carbons_disable{}]]), + disconnect(Config). + +%%%=================================================================== +%%% Master-slave tests +%%%=================================================================== +master_slave_cases() -> + {carbons_master_slave, [sequence], + [master_slave_test(send_recv), + master_slave_test(enable_disable)]}. + +send_recv_master(Config) -> + Peer = ?config(peer, Config), + prepare_master(Config), + ct:comment("Waiting for the peer to be ready"), + ready = get_event(Config), + send_messages(Config), + ct:comment("Waiting for the peer to disconnect"), + #presence{from = Peer, type = unavailable} = recv_presence(Config), + disconnect(Config). + +send_recv_slave(Config) -> + prepare_slave(Config), + ok = enable(Config), + put_event(Config, ready), + recv_carbons(Config), + disconnect(Config). + +enable_disable_master(Config) -> + prepare_master(Config), + ct:comment("Waiting for the peer to be ready"), + ready = get_event(Config), + send_messages(Config), + disconnect(Config). + +enable_disable_slave(Config) -> + Peer = ?config(peer, Config), + prepare_slave(Config), + ok = enable(Config), + ok = disable(Config), + put_event(Config, ready), + ct:comment("Waiting for the peer to disconnect"), + #presence{from = Peer, type = unavailable} = recv_presence(Config), + disconnect(Config). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +single_test(T) -> + list_to_atom("carbons_" ++ atom_to_list(T)). + +master_slave_test(T) -> + {list_to_atom("carbons_" ++ atom_to_list(T)), [parallel], + [list_to_atom("carbons_" ++ atom_to_list(T) ++ "_master"), + list_to_atom("carbons_" ++ atom_to_list(T) ++ "_slave")]}. + +prepare_master(Config) -> + MyJID = my_jid(Config), + Peer = ?config(peer, Config), + #presence{from = MyJID} = send_recv(Config, #presence{priority = 10}), + wait_for_slave(Config), + ct:comment("Receiving initial presence from the peer"), + #presence{from = Peer} = recv_presence(Config), + Config. + +prepare_slave(Config) -> + Peer = ?config(peer, Config), + MyJID = my_jid(Config), + ok = enable(Config), + wait_for_master(Config), + #presence{from = MyJID} = send_recv(Config, #presence{priority = 5}), + ct:comment("Receiving initial presence from the peer"), + #presence{from = Peer} = recv_presence(Config), + Config. + +send_messages(Config) -> + Server = ?config(server, Config), + MyJID = my_jid(Config), + JID = jid:make(p1_rand:get_string(), Server), + lists:foreach( + fun({send, #message{type = Type} = Msg}) -> + I = send(Config, Msg#message{to = JID}), + if Type /= error -> + #message{id = I, type = error} = recv_message(Config); + true -> + ok + end; + ({recv, #message{} = Msg}) -> + ejabberd_router:route( + Msg#message{from = JID, to = MyJID}), + ct:comment("Receiving message ~s", [xmpp:pp(Msg)]), + #message{} = recv_message(Config) + end, message_iterator(Config)). + +recv_carbons(Config) -> + Peer = ?config(peer, Config), + BarePeer = jid:remove_resource(Peer), + MyJID = my_jid(Config), + lists:foreach( + fun({_, #message{sub_els = [#hint{type = 'no-copy'}]}}) -> + ok; + ({_, #message{sub_els = [#carbons_private{}]}}) -> + ok; + ({_, #message{type = T}}) when T /= normal, T /= chat -> + ok; + ({Dir, #message{type = T, body = Body} = M}) + when (T == chat) or (T == normal andalso Body /= []) -> + ct:comment("Receiving carbon ~s", [xmpp:pp(M)]), + #message{from = BarePeer, to = MyJID} = CarbonMsg = + recv_message(Config), + case Dir of + send -> + #carbons_sent{forwarded = #forwarded{sub_els = [El]}} = + xmpp:get_subtag(CarbonMsg, #carbons_sent{}), + #message{body = Body} = xmpp:decode(El); + recv -> + #carbons_received{forwarded = #forwarded{sub_els = [El]}}= + xmpp:get_subtag(CarbonMsg, #carbons_received{}), + #message{body = Body} = xmpp:decode(El) + end; + (_) -> + false + end, message_iterator(Config)). + +enable(Config) -> + case send_recv( + Config, #iq{type = set, + sub_els = [#carbons_enable{}]}) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +disable(Config) -> + case send_recv( + Config, #iq{type = set, + sub_els = [#carbons_disable{}]}) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +message_iterator(_Config) -> + [{Dir, #message{type = Type, body = Body, sub_els = Els}} + || Dir <- [send, recv], + Type <- [error, chat, normal, groupchat, headline], + Body <- [[], xmpp:mk_text(<<"body">>)], + Els <- [[], + [#hint{type = 'no-copy'}], + [#carbons_private{}]]]. diff --git a/test/commands_tests.erl b/test/commands_tests.erl new file mode 100644 index 000000000..7b0675c3f --- /dev/null +++ b/test/commands_tests.erl @@ -0,0 +1,447 @@ +%%%------------------------------------------------------------------- +%%% Author : Badlop +%%% Created : 2 Jul 2024 by Badlop +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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. +%%% +%%%------------------------------------------------------------------- + +%%%% definitions + +-module(commands_tests). + +-compile(export_all). + +-include("suite.hrl"). + +%%%================================== +%%%% setup + +-ifdef(OTP_BELOW_24). + +single_cases() -> + {commands_single, [sequence], []}. + +-else. + +single_cases() -> + {commands_single, + [sequence], + [single_test(setup), + single_test(ejabberdctl), + single_test(http_integer), + single_test(http_string), + single_test(http_binary), + single_test(http_atom), + single_test(http_rescode), + single_test(http_restuple), + single_test(http_list), + single_test(http_tuple), + single_test(http_list_tuple), + single_test(http_list_tuple_map), + single_test(adhoc_list_commands), + single_test(adhoc_apiversion), + single_test(adhoc_apizero), + single_test(adhoc_apione), + single_test(adhoc_integer), + single_test(adhoc_string), + single_test(adhoc_binary), + single_test(adhoc_tuple), + single_test(adhoc_list), + single_test(adhoc_list_tuple), + single_test(adhoc_atom), + single_test(adhoc_rescode), + single_test(adhoc_restuple), + %%single_test(adhoc_all), + single_test(clean)]}. + +-endif. + +%% @format-begin + +single_test(T) -> + list_to_atom("commands_" ++ atom_to_list(T)). + +setup(_Config) -> + M = <<"mod_example">>, + clean(_Config), + case execute(module_install, [M]) of + ok -> + ok; + {error, not_available} -> + ?match(ok, execute(modules_update_specs, [])), + ?match(ok, execute(module_install, [M])) + end, + Installed = execute(modules_installed, []), + ?match(true, lists:keymember(mod_example, 1, Installed)). + +clean(_Config) -> + M = <<"mod_example">>, + execute(module_uninstall, [M]), + Installed = execute(modules_installed, []), + ?match(false, lists:keymember(mod_example, 1, Installed)). + +%%%================================== +%%%% ejabberdctl + +%% TODO: find a way to read and check the output printed in the command line +ejabberdctl(_Config) -> + R = ejabberd_ctl:process(["modules_installed"]), + ct:pal("ejabberdctl: R ~p", [R]), + ct:pal("ejabberdctl: Q ~p", [os:cmd("ejabberdctl modules_installed")]), + Installed = execute(modules_installed, []), + ?match(true, lists:keymember(mod_example, 1, Installed)). + +execute(Name, Args) -> + ejabberd_commands:execute_command2(Name, Args, #{caller_module => ejabberd_ctl}, 1000000). + +%%%================================== +%%%% mod_http_api + +http_integer(Config) -> + Integer = 123456789, + ?match(Integer, query(Config, "command_test_integer", #{arg_integer => Integer})). + +http_string(Config) -> + S = "This is a string.", + B = iolist_to_binary(S), + ?match(B, query(Config, "command_test_string", #{arg_string => S})), + ?match(B, query(Config, "command_test_string", #{arg_string => B})). + +http_binary(Config) -> + B = <<"This is a binary.">>, + ?match(B, query(Config, "command_test_binary", #{arg_binary => B})). + +%% mod_http_api doesn't handle 'atom' result format +%% and formats the result as a binary by default +http_atom(Config) -> + S = "Some string.", + B = <<"Some string.">>, + ?match(B, query(Config, "command_test_atom", #{arg_string => S})), + ?match(B, query(Config, "command_test_atom", #{arg_string => B})). + +http_rescode(Config) -> + ?match(0, query(Config, "command_test_rescode", #{code => "true"})), + ?match(0, query(Config, "command_test_rescode", #{code => "ok"})), + ?match(1, query(Config, "command_test_rescode", #{code => "problem"})), + ?match(1, query(Config, "command_test_rescode", #{code => "error"})). + +http_restuple(Config) -> + ?match(<<"Deleted 0 users: []">>, query(Config, "delete_old_users", #{days => 99})), + ?match(<<"Good">>, + query(Config, "command_test_restuple", #{code => "true", text => "Good"})), + ?match(<<"OK!!">>, + query(Config, "command_test_restuple", #{code => "ok", text => "OK!!"})). + +http_list(Config) -> + ListS = ["one", "first", "primary"], + ListB = lists:sort([<<"one">>, <<"first">>, <<"primary">>]), + ?match(ListB, query(Config, "command_test_list", #{arg_list => ListS})), + ?match(ListB, query(Config, "command_test_list", #{arg_list => ListB})). + +http_tuple(Config) -> + MapA = + #{element1 => "one", + element2 => "first", + element3 => "primary"}, + MapB = + #{<<"element1">> => <<"one">>, + <<"element2">> => <<"first">>, + <<"element3">> => <<"primary">>}, + ?match(MapB, query(Config, "command_test_tuple", #{arg_tuple => MapA})), + ?match(MapB, query(Config, "command_test_tuple", #{arg_tuple => MapB})). + +http_list_tuple(Config) -> + LTA = [#{element1 => "one", element2 => "uno"}, + #{element1 => "two", element2 => "dos"}, + #{element1 => "three", element2 => "tres"}], + LTB = lists:sort([#{<<"element1">> => <<"one">>, <<"element2">> => <<"uno">>}, + #{<<"element1">> => <<"two">>, <<"element2">> => <<"dos">>}, + #{<<"element1">> => <<"three">>, <<"element2">> => <<"tres">>}]), + ?match(LTB, query(Config, "command_test_list_tuple", #{arg_list => LTA})), + ?match(LTB, query(Config, "command_test_list_tuple", #{arg_list => LTB})). + +http_list_tuple_map(Config) -> + LTA = #{<<"one">> => <<"uno">>, + <<"two">> => <<"dos">>, + <<"three">> => <<"tres">>}, + LTB = lists:sort([#{<<"element1">> => <<"one">>, <<"element2">> => <<"uno">>}, + #{<<"element1">> => <<"two">>, <<"element2">> => <<"dos">>}, + #{<<"element1">> => <<"three">>, <<"element2">> => <<"tres">>}]), + ?match(LTB, lists:sort(query(Config, "command_test_list_tuple", #{arg_list => LTA}))). + +%%% internal functions + +query(Config, Tail, Map) -> + BodyQ = misc:json_encode(Map), + Body = make_query(Config, Tail, BodyQ), + misc:json_decode(Body). + +make_query(Config, Tail, BodyQ) -> + ?match({ok, {{"HTTP/1.1", 200, _}, _, Body}}, + httpc:request(post, + {page(Config, Tail), [], "application/json", BodyQ}, + [], + [{body_format, binary}]), + Body). + +page(Config, Tail) -> + Server = ?config(server_host, Config), + Port = ct:get_config(web_port, 5280), + "http://" ++ Server ++ ":" ++ integer_to_list(Port) ++ "/api/" ++ Tail. + +%%%================================== +%%%% ad-hoc + +%%% list commands + +adhoc_list_commands(Config) -> + {ok, Result} = get_items(Config, <<"api-commands">>), + {value, #disco_item{name = <<"command_test_binary">>}} = + lists:keysearch(<<"command_test_binary">>, #disco_item.name, Result), + suite:disconnect(Config). + +get_items(Config, Node) -> + case suite:send_recv(Config, + #iq{type = get, + to = server_jid(Config), + sub_els = [#disco_items{node = Node}]}) + of + #iq{type = result, sub_els = [#disco_items{node = Node, items = Items}]} -> + {ok, Items}; + #iq{type = result, sub_els = []} -> + {empty, []}; + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +%%% apiversion + +adhoc_apiversion(Config) -> + Node = <<"api-commands/command_test_apiversion">>, + ArgFields = make_fields_args([]), + ResFields = make_fields_res([{<<"apiversion">>, <<"2">>}]), + {ok, Sid, _FormFields} = get_form(Config, Node), + ?match({ok, ResFields}, set_form(Config, Node, Sid, ArgFields)), + suite:disconnect(Config). + +%%% apizero + +adhoc_apizero(Config) -> + Node = <<"api-commands/command_test_apizero">>, + ArgFields = make_fields_args([]), + ResFields = make_fields_res([{<<"apiversion">>, <<"0">>}]), + {ok, Sid, _FormFields} = get_form(Config, Node), + ?match({ok, ResFields}, set_form(Config, Node, Sid, ArgFields)), + suite:disconnect(Config). + +%%% apione + +adhoc_apione(Config) -> + Node = <<"api-commands/command_test_apione">>, + ArgFields = make_fields_args([]), + ResFields = make_fields_res([{<<"apiversion">>, <<"1">>}]), + {ok, Sid, _FormFields} = get_form(Config, Node), + ?match({ok, ResFields}, set_form(Config, Node, Sid, ArgFields)), + suite:disconnect(Config). + +%%% integer + +adhoc_integer(Config) -> + Node = <<"api-commands/command_test_integer">>, + ArgFields = make_fields_args([{<<"arg_integer">>, <<"12345">>}]), + ResFields = make_fields_res([{<<"res_integer">>, <<"12345">>}]), + {ok, Sid, _FormFields} = get_form(Config, Node), + ?match({ok, ResFields}, set_form(Config, Node, Sid, ArgFields)), + suite:disconnect(Config). + +%%% string + +adhoc_string(Config) -> + Node = <<"api-commands/command_test_string">>, + ArgFields = make_fields_args([{<<"arg_string">>, <<"Some string.">>}]), + ResFields = make_fields_res([{<<"res_string">>, <<"Some string.">>}]), + {ok, Sid, _FormFields} = get_form(Config, Node), + ?match({ok, ResFields}, set_form(Config, Node, Sid, ArgFields)), + suite:disconnect(Config). + +%%% binary + +adhoc_binary(Config) -> + Node = <<"api-commands/command_test_binary">>, + ArgFields = make_fields_args([{<<"arg_binary">>, <<"Some binary.">>}]), + ResFields = make_fields_res([{<<"res_string">>, <<"Some binary.">>}]), + {ok, Sid, _FormFields} = get_form(Config, Node), + ?match({ok, ResFields}, set_form(Config, Node, Sid, ArgFields)), + suite:disconnect(Config). + +%%% tuple + +adhoc_tuple(Config) -> + Node = <<"api-commands/command_test_tuple">>, + ArgFields = make_fields_args([{<<"arg_tuple">>, <<"one:two:three">>}]), + {ok, Sid, _FormFields} = get_form(Config, Node), + ?match({ok, + [{xdata_field, + <<"res_tuple">>, + 'text-single', + <<"res_tuple">>, + false, + <<" {element1 : element2 : element3}">>, + [<<"one : two : three">>], + [], + []}]}, + set_form(Config, Node, Sid, ArgFields)), + suite:disconnect(Config). + +%%% list + +adhoc_list(Config) -> + Node = <<"api-commands/command_test_list">>, + ArgFields = make_fields_args([{<<"arg_list">>, [<<"one">>, <<"first">>, <<"primary">>]}]), + ResFields = + make_fields_res([{<<"res_list">>, lists:sort([<<"one">>, <<"first">>, <<"primary">>])}]), + {ok, Sid, _FormFields} = get_form(Config, Node), + ?match({ok, ResFields}, set_form(Config, Node, Sid, ArgFields)), + suite:disconnect(Config). + +%%% list_tuple + +adhoc_list_tuple(Config) -> + Node = <<"api-commands/command_test_list_tuple">>, + ArgFields = + make_fields_args([{<<"arg_list">>, [<<"one:uno">>, <<"two:dos">>, <<"three:tres">>]}]), + ResFields = + make_fields_res([{<<"res_list">>, + lists:sort([<<"one : uno">>, <<"two : dos">>, <<"three : tres">>])}]), + {ok, Sid, _FormFields} = get_form(Config, Node), + ?match({ok, ResFields}, set_form(Config, Node, Sid, ArgFields)), + suite:disconnect(Config). + +%%% atom + +adhoc_atom(Config) -> + Node = <<"api-commands/command_test_atom">>, + ArgFields = make_fields_args([{<<"arg_string">>, <<"a_test_atom">>}]), + ResFields = make_fields_res([{<<"res_atom">>, <<"a_test_atom">>}]), + {ok, Sid, _FormFields} = get_form(Config, Node), + ?match({ok, ResFields}, set_form(Config, Node, Sid, ArgFields)), + suite:disconnect(Config). + +%%% rescode + +adhoc_rescode(Config) -> + Node = <<"api-commands/command_test_rescode">>, + ArgFields = make_fields_args([{<<"code">>, <<"ok">>}]), + ResFields = make_fields_res([{<<"res_atom">>, <<"0">>}]), + {ok, Sid, _FormFields} = get_form(Config, Node), + ?match({ok, ResFields}, set_form(Config, Node, Sid, ArgFields)), + suite:disconnect(Config). + +%%% restuple + +adhoc_restuple(Config) -> + Node = <<"api-commands/command_test_restuple">>, + ArgFields = + make_fields_args([{<<"code">>, <<"ok">>}, {<<"text">>, <<"Just a result text">>}]), + ResFields = make_fields_res([{<<"res_atom">>, <<"Just a result text">>}]), + {ok, Sid, _FormFields} = get_form(Config, Node), + ?match({ok, ResFields}, set_form(Config, Node, Sid, ArgFields)), + suite:disconnect(Config). + +%%% internal functions + +server_jid(Config) -> + jid:make(<<>>, ?config(server, Config), <<>>). + +make_fields_args(Fields) -> + lists:map(fun ({Var, Values}) when is_list(Values) -> + #xdata_field{label = Var, + var = Var, + required = true, + type = 'text-multi', + values = Values}; + ({Var, Value}) -> + #xdata_field{label = Var, + var = Var, + required = true, + type = 'text-single', + values = [Value]} + end, + Fields). + +make_fields_res(Fields) -> + lists:map(fun ({Var, Values}) when is_list(Values) -> + #xdata_field{label = Var, + var = Var, + type = 'text-multi', + values = Values}; + ({Var, Value}) -> + #xdata_field{label = Var, + var = Var, + type = 'text-single', + values = [Value]} + end, + Fields). + +get_form(Config, Node) -> + case suite:send_recv(Config, + #iq{type = set, + to = server_jid(Config), + sub_els = [#adhoc_command{node = Node}]}) + of + #iq{type = result, + sub_els = + [#adhoc_command{node = Node, + action = execute, + status = executing, + sid = Sid, + actions = #adhoc_actions{execute = complete, complete = true}, + xdata = #xdata{fields = Fields}}]} -> + {ok, Sid, [F || F <- Fields, F#xdata_field.type /= fixed]}; + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +set_form(Config, Node, Sid, ArgFields) -> + Xdata = #xdata{type = submit, fields = ArgFields}, + case suite:send_recv(Config, + #iq{type = set, + to = server_jid(Config), + sub_els = + [#adhoc_command{node = Node, + action = complete, + sid = Sid, + xdata = Xdata}]}) + of + #iq{type = result, + sub_els = + [#adhoc_command{node = Node, + action = execute, + status = completed, + sid = Sid, + xdata = #xdata{fields = ResFields}}]} -> + ResFields2 = [F || F <- ResFields, F#xdata_field.type /= fixed], + {ok, ResFields2 -- ArgFields}; + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +%%%================================== + +%%% vim: set foldmethod=marker foldmarker=%%%%,%%%=: diff --git a/test/configtest_tests.erl b/test/configtest_tests.erl new file mode 100644 index 000000000..3763b8645 --- /dev/null +++ b/test/configtest_tests.erl @@ -0,0 +1,187 @@ +%%%------------------------------------------------------------------- +%%% Author : Badlop +%%% Created : 5 Feb 2025 by Badlop +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(configtest_tests). + +-compile(export_all). + +-include("suite.hrl"). + +%%%================================== + +single_cases() -> + {configtest_single, + [sequence], + [single_test(macro_over_keyword), + single_test(keyword_inside_macro), + single_test(macro_and_keyword), + single_test(macro_double), + single_test(keyword_double), + + single_test(macro_toplevel_global_atom), + single_test(macro_toplevel_global_string), + single_test(macro_toplevel_global_string_inside), + single_test(macro_toplevel_local_atom), + single_test(macro_toplevel_local_string), + single_test(macro_toplevel_local_string_inside), + + single_test(keyword_toplevel_global_atom), + single_test(keyword_toplevel_global_string), + single_test(keyword_toplevel_global_string_inside), + single_test(keyword_toplevel_local_atom), + single_test(keyword_toplevel_local_string), + single_test(keyword_toplevel_local_string_inside), + + single_test(macro_module_atom), + single_test(macro_module_string), + single_test(macro_module_string_inside), + + single_test(keyword_module_atom), + single_test(keyword_module_string), + single_test(keyword_module_string_inside), + + single_test(toplevel_global_predefined), + single_test(toplevel_local_predefined), + single_test(module_predefined)]}. + +%% Interactions + +macro_over_keyword(_) -> + toplevel_global(macro, macro_over_keyword). + +keyword_inside_macro(_) -> + toplevel_global(<<"+macro+/-keyword-">>, keyword_inside_macro). + +macro_and_keyword(_) -> + toplevel_global(<<"+macro+&-keyword-">>, macro_and_keyword). + +macro_double(_) -> + toplevel_global(<<"macro--macro">>, macro_double). + +keyword_double(_) -> + toplevel_global(<<"keyword--keyword">>, keyword_double). + +%% Macro Toplevel + +macro_toplevel_global_atom(_) -> + toplevel_global(mtga, mtga). + +macro_toplevel_global_string(_) -> + toplevel_global(<<"Mtgs">>, mtgs). + +macro_toplevel_global_string_inside(_) -> + toplevel_global(<<"Mtgsi">>, mtgsi). + +macro_toplevel_local_atom(_) -> + toplevel_local(mtla, mtla). + +macro_toplevel_local_string(_) -> + toplevel_local(<<"Mtls">>, mtls). + +macro_toplevel_local_string_inside(_) -> + toplevel_local(<<"Mtlsi">>, mtlsi). + +%% Keyword Toplevel + +keyword_toplevel_global_atom(_) -> + toplevel_global(ktga, ktga). + +keyword_toplevel_global_string(_) -> + toplevel_global(<<"Ktgs">>, ktgs). + +keyword_toplevel_global_string_inside(_) -> + toplevel_global(<<"Ktgsi">>, ktgsi). + +keyword_toplevel_local_atom(_) -> + toplevel_local(ktla, ktla). + +keyword_toplevel_local_string(_) -> + toplevel_local(<<"Ktls">>, ktls). + +keyword_toplevel_local_string_inside(_) -> + toplevel_local(<<"Ktlsi">>, ktlsi). + +%% Macro Module + +macro_module_atom(_) -> + module(mma, mma). + +macro_module_string(_) -> + module(<<"Mms">>, mms). + +macro_module_string_inside(_) -> + module(<<"Mmsi">>, mmsi). + +%% Keyword Module + +keyword_module_atom(_) -> + module(kma, kma). + +keyword_module_string(_) -> + module(<<"Kms">>, kms). + +keyword_module_string_inside(_) -> + module(<<"Kmsi">>, kmsi). + +%% Predefined + +toplevel_global_predefined(_) -> + Semver = ejabberd_option:version(), + Version = misc:semver_to_xxyy(Semver), + String = <<"tgp - semver: ", Semver/binary, ", version: ", Version/binary>>, + toplevel_global(String, tgp). + +toplevel_local_predefined(_) -> + Semver = ejabberd_option:version(), + Version = misc:semver_to_xxyy(Semver), + String = <<"tlp - semver: ", Semver/binary, ", version: ", Version/binary>>, + toplevel_local(String, tlp). + +module_predefined(_) -> + Host = <<"configtest.localhost">>, + Semver = ejabberd_option:version(), + Version = misc:semver_to_xxyy(Semver), + String = <<"mp - host: ", Host/binary, ", semver: ", Semver/binary, ", version: ", Version/binary>>, + module(String, predefined_keywords). + +%%%================================== +%%%% internal functions + +single_test(T) -> + list_to_atom("configtest_" ++ atom_to_list(T)). + +toplevel_global(Result, Option) -> + ?match(Result, ejabberd_config:get_option(Option)). + +toplevel_local(Result, Option) -> + Host = <<"configtest.localhost">>, + ?match(Result, ejabberd_config:get_option({Option, Host})). + +module(Result, Option) -> + Host = <<"configtest.localhost">>, + Module = mod_configtest, + ?match(Result, gen_mod:get_module_opt(Host, Module, Option)). + +%%%================================== + +%%% vim: set foldmethod=marker foldmarker=%%%%,%%%=: diff --git a/test/csi_tests.erl b/test/csi_tests.erl new file mode 100644 index 000000000..f2b61abff --- /dev/null +++ b/test/csi_tests.erl @@ -0,0 +1,162 @@ +%%%------------------------------------------------------------------- +%%% Author : Evgeny Khramtsov +%%% Created : 16 Nov 2016 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(csi_tests). + +%% API +-compile(export_all). +-import(suite, [disconnect/1, wait_for_slave/1, wait_for_master/1, + send/2, send_recv/2, recv_presence/1, recv_message/1, + server_jid/1]). + +-include("suite.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +%%%=================================================================== +%%% Single user tests +%%%=================================================================== +single_cases() -> + {csi_single, [sequence], + [single_test(feature_enabled)]}. + +feature_enabled(Config) -> + true = ?config(csi, Config), + disconnect(Config). + +%%%=================================================================== +%%% Master-slave tests +%%%=================================================================== +master_slave_cases() -> + {csi_master_slave, [sequence], + [master_slave_test(all)]}. + +all_master(Config) -> + Peer = ?config(peer, Config), + Presence = #presence{to = Peer}, + ChatState = #message{to = Peer, thread = #message_thread{data = <<"1">>}, + sub_els = [#chatstate{type = active}]}, + Message = ChatState#message{body = [#text{data = <<"body">>}]}, + PepPayload = xmpp:encode(#presence{}), + PepOne = #message{ + to = Peer, + sub_els = + [#ps_event{ + items = + #ps_items{ + node = <<"foo-1">>, + items = + [#ps_item{ + id = <<"pep-1">>, + sub_els = [PepPayload]}]}}]}, + PepTwo = #message{ + to = Peer, + sub_els = + [#ps_event{ + items = + #ps_items{ + node = <<"foo-2">>, + items = + [#ps_item{ + id = <<"pep-2">>, + sub_els = [PepPayload]}]}}]}, + %% Wait for the slave to become inactive. + wait_for_slave(Config), + %% Should be queued (but see below): + send(Config, Presence), + %% Should replace the previous presence in the queue: + send(Config, Presence#presence{type = unavailable}), + %% The following two PEP stanzas should be queued (but see below): + send(Config, PepOne), + send(Config, PepTwo), + %% The following two PEP stanzas should replace the previous two: + send(Config, PepOne), + send(Config, PepTwo), + %% Should be queued (but see below): + send(Config, ChatState), + %% Should replace the previous chat state in the queue: + send(Config, ChatState#message{sub_els = [#chatstate{type = composing}]}), + %% Should be sent immediately, together with the queued stanzas: + send(Config, Message), + %% Wait for the slave to become active. + wait_for_slave(Config), + %% Should be delivered, as the client is active again: + send(Config, ChatState), + disconnect(Config). + +all_slave(Config) -> + Peer = ?config(peer, Config), + change_client_state(Config, inactive), + wait_for_master(Config), + #presence{from = Peer, type = unavailable, sub_els = [#delay{}]} = + recv_presence(Config), + #message{ + from = Peer, + sub_els = + [#ps_event{ + items = + #ps_items{ + node = <<"foo-1">>, + items = + [#ps_item{ + id = <<"pep-1">>}]}}, + #delay{}]} = recv_message(Config), + #message{ + from = Peer, + sub_els = + [#ps_event{ + items = + #ps_items{ + node = <<"foo-2">>, + items = + [#ps_item{ + id = <<"pep-2">>}]}}, + #delay{}]} = recv_message(Config), + #message{from = Peer, thread = #message_thread{data = <<"1">>}, + sub_els = [#chatstate{type = composing}, + #delay{}]} = recv_message(Config), + #message{from = Peer, thread = #message_thread{data = <<"1">>}, + body = [#text{data = <<"body">>}], + sub_els = [#chatstate{type = active}]} = recv_message(Config), + change_client_state(Config, active), + wait_for_master(Config), + #message{from = Peer, thread = #message_thread{data = <<"1">>}, + sub_els = [#chatstate{type = active}]} = recv_message(Config), + disconnect(Config). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +single_test(T) -> + list_to_atom("csi_" ++ atom_to_list(T)). + +master_slave_test(T) -> + {list_to_atom("csi_" ++ atom_to_list(T)), [parallel], + [list_to_atom("csi_" ++ atom_to_list(T) ++ "_master"), + list_to_atom("csi_" ++ atom_to_list(T) ++ "_slave")]}. + +change_client_state(Config, NewState) -> + send(Config, #csi{type = NewState}), + send_recv(Config, #iq{type = get, to = server_jid(Config), + sub_els = [#ping{}]}). diff --git a/test/docker/README.md b/test/docker/README.md new file mode 100644 index 000000000..7caaa4bb7 --- /dev/null +++ b/test/docker/README.md @@ -0,0 +1,56 @@ +# Docker database images to run ejabberd tests + +## Starting databases + +You can start the Docker environment with Docker Compose, from ejabberd repository root. + +The following command will launch MySQL, MSSQL, PostgreSQL, Redis and keep the console +attached to it. + +``` +mkdir test/docker/db/mysql/data +mkdir test/docker/db/postgres/data +(cd test/docker; docker compose up) +``` + +You can stop all the databases with CTRL-C. + +## Creating database for MSSQL + +The following commands will create the necessary login, user and database, will grant rights on the database in MSSQL and create the ejabberd schema: + +``` +docker exec ejabberd-mssql /opt/mssql-tools18/bin/sqlcmd -U SA -P ejabberd_Test1 -S localhost -i /initdb_mssql.sql -C +docker exec ejabberd-mssql /opt/mssql-tools18/bin/sqlcmd -U SA -P ejabberd_Test1 -S localhost -d ejabberd_test -i /mssql.sql -C +``` + +## Running tests + +Before running the test, you can ensure there is no running instance of Erlang common test tool. You can run the following +command, especially if all test are skipped with an `eaddrinuse` error: + +``` +pkill -9 ct_run +``` + +You can run tests with (from ejabberd repository root): + +``` +make test +``` + +## Cleaning up the test environment + +You can fully clean up the environment with: + +``` +(cd test/docker; docker compose down) +``` + +If you want to clean the data, you can remove the data volumes after the `docker-compose down` command: + +``` +docker volume rm ejabberd-mysqldata +docker volume rm ejabberd-mssqldata +docker volume rm ejabberd-pgsqldata +``` diff --git a/test/docker/db/mssql/initdb/initdb_mssql.sql b/test/docker/db/mssql/initdb/initdb_mssql.sql new file mode 100644 index 000000000..8c7acc708 --- /dev/null +++ b/test/docker/db/mssql/initdb/initdb_mssql.sql @@ -0,0 +1,31 @@ +SET ANSI_NULLS ON; +SET NOCOUNT ON; +SET QUOTED_IDENTIFIER ON; +SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + +USE [master]; +GO + +-- prevent creation when already exists +IF DB_ID('ejabberd_test') IS NOT NULL +BEGIN +SET NOEXEC ON; +END + +CREATE DATABASE ejabberd_test; +GO + +USE ejabberd_test; +GO + +CREATE LOGIN ejabberd_test WITH PASSWORD = 'ejabberd_Test1', CHECK_POLICY = OFF; +GO + +CREATE USER ejabberd_test FOR LOGIN ejabberd_test; +GO + +GRANT ALL TO ejabberd_test; +GO + +GRANT CONTROL ON SCHEMA ::dbo TO ejabberd_test; +GO diff --git a/test/docker/docker-compose.yml b/test/docker/docker-compose.yml new file mode 100644 index 000000000..bdc470a63 --- /dev/null +++ b/test/docker/docker-compose.yml @@ -0,0 +1,59 @@ +version: '3.7' + +services: + mysql: + image: mysql:latest + container_name: ejabberd-mysql + volumes: + - mysqldata:/var/lib/mysql + - ../../sql/mysql.sql:/docker-entrypoint-initdb.d/mysql.sql:ro + restart: always + ports: + - 3306:3306 + environment: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: ejabberd_test + MYSQL_USER: ejabberd_test + MYSQL_PASSWORD: ejabberd_test + + mssql: + image: mcr.microsoft.com/mssql/server + container_name: ejabberd-mssql + volumes: + - mssqldata:/var/opt/mssql + - ./db/mssql/initdb/initdb_mssql.sql:/initdb_mssql.sql:ro + - ../../sql/mssql.sql:/mssql.sql:ro + - ../../sql/mssql.new.sql:/mssql.new.sql:ro + restart: always + ports: + - 1433:1433 + environment: + ACCEPT_EULA: Y + SA_PASSWORD: ejabberd_Test1 + + postgres: + image: postgres:latest + container_name: ejabberd-postgres + volumes: + - pgsqldata:/var/lib/postgresql/data + - ../../sql/pg.sql:/docker-entrypoint-initdb.d/pg.sql:ro + ports: + - 5432:5432 + environment: + POSTGRES_PASSWORD: ejabberd_test + POSTGRES_USER: ejabberd_test + POSTGRES_DB: ejabberd_test + + redis: + image: redis:latest + container_name: ejabberd-redis + ports: + - 6379:6379 + +volumes: + mysqldata: + name: ejabberd-mysqldata + mssqldata: + name: ejabberd-mssqldata + pgsqldata: + name: ejabberd-pgsqldata diff --git a/test/ejabberd_SUITE.erl b/test/ejabberd_SUITE.erl index 380ed606f..ca465689d 100644 --- a/test/ejabberd_SUITE.erl +++ b/test/ejabberd_SUITE.erl @@ -1,31 +1,47 @@ %%%------------------------------------------------------------------- -%%% @author Evgeniy Khramtsov -%%% @copyright (C) 2013, Evgeniy Khramtsov -%%% @doc -%%% -%%% @end +%%% Author : Evgeny Khramtsov %%% Created : 2 Jun 2013 by Evgeniy Khramtsov -%%%------------------------------------------------------------------- +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(ejabberd_SUITE). - -compile(export_all). --import(suite, [init_config/1, connect/1, disconnect/1, - recv/0, send/2, send_recv/2, my_jid/1, server_jid/1, - pubsub_jid/1, proxy_jid/1, muc_jid/1, - muc_room_jid/1, get_features/2, re_register/1, - is_feature_advertised/2, subscribe_to_events/1, - is_feature_advertised/3, set_opt/3, auth_SASL/2, - wait_for_master/1, wait_for_slave/1, - make_iq_result/1, start_event_relay/0, +-import(suite, [init_config/1, connect/1, disconnect/1, recv_message/1, + recv/1, recv_presence/1, send/2, send_recv/2, my_jid/1, + server_jid/1, pubsub_jid/1, proxy_jid/1, muc_jid/1, + muc_room_jid/1, my_muc_jid/1, peer_muc_jid/1, + mix_jid/1, mix_room_jid/1, get_features/2, recv_iq/1, + re_register/1, is_feature_advertised/2, subscribe_to_events/1, + is_feature_advertised/3, set_opt/3, + auth_SASL/2, auth_SASL/3, auth_SASL/4, + wait_for_master/1, wait_for_slave/1, flush/1, + make_iq_result/1, start_event_relay/0, alt_room_jid/1, stop_event_relay/1, put_event/2, get_event/1, - bind/1, auth/1, open_session/1, zlib/1, starttls/1, - close_socket/1]). - + bind/1, auth/1, auth/2, open_session/1, open_session/2, + zlib/1, starttls/1, starttls/2, close_socket/1, init_stream/1, + auth_legacy/2, auth_legacy/3, tcp_connect/1, send_text/2, + set_roster/3, del_roster/1]). -include("suite.hrl"). suite() -> - [{timetrap, {seconds,20}}]. + [{timetrap, {seconds, 120}}]. init_per_suite(Config) -> NewConfig = init_config(Config), @@ -35,58 +51,164 @@ init_per_suite(Config) -> LDIFFile = filename:join([DataDir, "ejabberd.ldif"]), {ok, _} = file:copy(ExtAuthScript, filename:join([CWD, "extauth.py"])), {ok, _} = ldap_srv:start(LDIFFile), - ok = application:start(ejabberd), + inet_db:add_host({127,0,0,1}, [binary_to_list(?S2S_VHOST), + binary_to_list(?MNESIA_VHOST), + binary_to_list(?UPLOAD_VHOST)]), + inet_db:set_domain(binary_to_list(p1_rand:get_string())), + inet_db:set_lookup([file, native]), + start_ejabberd(NewConfig), NewConfig. -end_per_suite(_Config) -> - ok. +start_ejabberd(_) -> + TestBeams = case filelib:is_dir("../../test/") of + true -> "../../test/"; + _ -> "../../lib/ejabberd/test/" + end, + application:set_env(ejabberd, external_beams, TestBeams), + {ok, _} = application:ensure_all_started(ejabberd, transient). -init_per_group(no_db, Config) -> +end_per_suite(_Config) -> + application:stop(ejabberd). + +init_per_group(Group, Config) -> + case lists:member(Group, ?BACKENDS) of + false -> + %% Not a backend related group, do default init: + do_init_per_group(Group, Config); + true -> + case proplists:get_value(backends, Config) of + all -> + %% All backends enabled + do_init_per_group(Group, Config); + Backends -> + %% Skipped backends that were not explicitly enabled + case lists:member(Group, Backends) of + true -> + do_init_per_group(Group, Config); + false -> + {skip, {disabled_backend, Group}} + end + end + end. + +do_init_per_group(no_db, Config) -> re_register(Config), - Config; -init_per_group(mnesia, Config) -> + set_opt(persistent_room, false, Config); +do_init_per_group(mnesia, Config) -> mod_muc:shutdown_rooms(?MNESIA_VHOST), set_opt(server, ?MNESIA_VHOST, Config); -init_per_group(mysql, Config) -> - case catch ejabberd_odbc:sql_query(?MYSQL_VHOST, [<<"select 1;">>]) of +do_init_per_group(redis, Config) -> + mod_muc:shutdown_rooms(?REDIS_VHOST), + set_opt(server, ?REDIS_VHOST, Config); +do_init_per_group(mysql, Config) -> + case catch ejabberd_sql:sql_query(?MYSQL_VHOST, [<<"select 1;">>]) of {selected, _, _} -> mod_muc:shutdown_rooms(?MYSQL_VHOST), - create_sql_tables(mysql, ?config(base_dir, Config)), + update_sql(?MYSQL_VHOST, Config), + stop_temporary_modules(?MYSQL_VHOST), set_opt(server, ?MYSQL_VHOST, Config); Err -> {skip, {mysql_not_available, Err}} end; -init_per_group(pgsql, Config) -> - case catch ejabberd_odbc:sql_query(?PGSQL_VHOST, [<<"select 1;">>]) of +do_init_per_group(mssql, Config) -> + case catch ejabberd_sql:sql_query(?MSSQL_VHOST, [<<"select 1;">>]) of + {selected, _, _} -> + mod_muc:shutdown_rooms(?MSSQL_VHOST), + update_sql(?MSSQL_VHOST, Config), + stop_temporary_modules(?MSSQL_VHOST), + set_opt(server, ?MSSQL_VHOST, Config); + Err -> + {skip, {mssql_not_available, Err}} + end; +do_init_per_group(pgsql, Config) -> + case catch ejabberd_sql:sql_query(?PGSQL_VHOST, [<<"select 1;">>]) of {selected, _, _} -> mod_muc:shutdown_rooms(?PGSQL_VHOST), - create_sql_tables(pgsql, ?config(base_dir, Config)), + update_sql(?PGSQL_VHOST, Config), + stop_temporary_modules(?PGSQL_VHOST), set_opt(server, ?PGSQL_VHOST, Config); Err -> {skip, {pgsql_not_available, Err}} end; -init_per_group(ldap, Config) -> - set_opt(server, ?LDAP_VHOST, Config); -init_per_group(extauth, Config) -> - set_opt(server, ?EXTAUTH_VHOST, Config); -init_per_group(riak, Config) -> - case ejabberd_riak:is_connected() of - true -> - mod_muc:shutdown_rooms(?RIAK_VHOST), - NewConfig = set_opt(server, ?RIAK_VHOST, Config), - clear_riak_tables(NewConfig); - Err -> - {skip, {riak_not_available, Err}} +do_init_per_group(sqlite, Config) -> + case catch ejabberd_sql:sql_query(?SQLITE_VHOST, [<<"select 1;">>]) of + {selected, _, _} -> + mod_muc:shutdown_rooms(?SQLITE_VHOST), + set_opt(server, ?SQLITE_VHOST, Config); + Err -> + {skip, {sqlite_not_available, Err}} end; -init_per_group(_GroupName, Config) -> +do_init_per_group(ldap, Config) -> + set_opt(server, ?LDAP_VHOST, Config); +do_init_per_group(extauth, Config) -> + set_opt(server, ?EXTAUTH_VHOST, Config); +do_init_per_group(s2s, Config) -> + ejabberd_config:set_option({s2s_use_starttls, ?COMMON_VHOST}, required), + ejabberd_config:set_option(ca_file, "ca.pem"), + Port = ?config(s2s_port, Config), + set_opt(server, ?COMMON_VHOST, + set_opt(xmlns, ?NS_SERVER, + set_opt(type, server, + set_opt(server_port, Port, + set_opt(stream_from, ?S2S_VHOST, + set_opt(lang, <<"">>, Config)))))); +do_init_per_group(component, Config) -> + Server = ?config(server, Config), + Port = ?config(component_port, Config), + set_opt(xmlns, ?NS_COMPONENT, + set_opt(server, <<"component.", Server/binary>>, + set_opt(type, component, + set_opt(server_port, Port, + set_opt(stream_version, undefined, + set_opt(lang, <<"">>, Config)))))); +do_init_per_group(GroupName, Config) -> Pid = start_event_relay(), - set_opt(event_relay, Pid, Config). + NewConfig = set_opt(event_relay, Pid, Config), + case GroupName of + anonymous -> set_opt(anonymous, true, NewConfig); + _ -> NewConfig + end. + +stop_temporary_modules(Host) -> + Modules = [mod_shared_roster], + [gen_mod:stop_module(Host, M) || M <- Modules]. end_per_group(mnesia, _Config) -> ok; -end_per_group(mysql, _Config) -> +end_per_group(redis, _Config) -> ok; -end_per_group(pgsql, _Config) -> +end_per_group(mysql, Config) -> + Query = "SELECT COUNT(*) FROM information_schema.tables WHERE table_name = 'mqtt_pub';", + case catch ejabberd_sql:sql_query(?MYSQL_VHOST, [Query]) of + {selected, _, [[<<"0">>]]} -> + ok; + {selected, _, [[<<"1">>]]} -> + clear_sql_tables(mysql, Config); + Other -> + ct:fail({failed_to_check_table_existence, mysql, Other}) + end, + ok; +end_per_group(mssql, Config) -> + Query = "SELECT * FROM sys.tables WHERE name = 'mqtt_pub'", + case catch ejabberd_sql:sql_query(?MSSQL_VHOST, [Query]) of + {selected, [t]} -> + clear_sql_tables(mssql, Config); + Other -> + ct:fail({failed_to_check_table_existence, mssql, Other}) + end, + ok; +end_per_group(pgsql, Config) -> + Query = "SELECT EXISTS (SELECT 0 FROM information_schema.tables WHERE table_name = 'mqtt_pub');", + case catch ejabberd_sql:sql_query(?PGSQL_VHOST, [Query]) of + {selected, [t]} -> + clear_sql_tables(pgsql, Config); + {selected, _, [[<<"t">>]]} -> + clear_sql_tables(pgsql, Config); + Other -> + ct:fail({failed_to_check_table_existence, pgsql, Other}) + end, + ok; +end_per_group(sqlite, _Config) -> ok; end_per_group(no_db, _Config) -> ok; @@ -94,71 +216,139 @@ end_per_group(ldap, _Config) -> ok; end_per_group(extauth, _Config) -> ok; -end_per_group(riak, _Config) -> +end_per_group(component, _Config) -> ok; +end_per_group(s2s, Config) -> + Server = ?config(server, Config), + ejabberd_config:set_option({s2s_use_starttls, Server}, false); end_per_group(_GroupName, Config) -> stop_event_relay(Config), - ok. + set_opt(anonymous, false, Config). init_per_testcase(stop_ejabberd, Config) -> - open_session(bind(auth(connect(Config)))); + NewConfig = set_opt(resource, <<"">>, + set_opt(anonymous, true, Config)), + open_session(bind(auth(connect(NewConfig)))); init_per_testcase(TestCase, OrigConfig) -> - subscribe_to_events(OrigConfig), - Server = ?config(server, OrigConfig), - Resource = ?config(resource, OrigConfig), - MasterResource = ?config(master_resource, OrigConfig), - SlaveResource = ?config(slave_resource, OrigConfig), + ct:print(80, "Testcase '~p' starting", [TestCase]), Test = atom_to_list(TestCase), IsMaster = lists:suffix("_master", Test), IsSlave = lists:suffix("_slave", Test), + if IsMaster or IsSlave -> + subscribe_to_events(OrigConfig); + true -> + ok + end, + TestGroup = proplists:get_value( + name, ?config(tc_group_properties, OrigConfig)), + Server = ?config(server, OrigConfig), + Resource = case TestGroup of + anonymous -> + <<"">>; + legacy_auth -> + p1_rand:get_string(); + _ -> + ?config(resource, OrigConfig) + end, + MasterResource = ?config(master_resource, OrigConfig), + SlaveResource = ?config(slave_resource, OrigConfig), + Mode = if IsSlave -> slave; + IsMaster -> master; + true -> single + end, IsCarbons = lists:prefix("carbons_", Test), - User = if IsMaster or IsCarbons -> <<"test_master">>; - IsSlave -> <<"test_slave">>; - true -> <<"test_single">> + IsReplaced = lists:prefix("replaced_", Test), + User = if IsReplaced -> <<"test_single!#$%^*()`~+-;_=[]{}|\\">>; + IsCarbons and not (IsMaster or IsSlave) -> + <<"test_single!#$%^*()`~+-;_=[]{}|\\">>; + IsMaster or IsCarbons -> <<"test_master!#$%^*()`~+-;_=[]{}|\\">>; + IsSlave -> <<"test_slave!#$%^*()`~+-;_=[]{}|\\">>; + true -> <<"test_single!#$%^*()`~+-;_=[]{}|\\">> end, + Nick = if IsSlave -> ?config(slave_nick, OrigConfig); + IsMaster -> ?config(master_nick, OrigConfig); + true -> ?config(nick, OrigConfig) + end, MyResource = if IsMaster and IsCarbons -> MasterResource; IsSlave and IsCarbons -> SlaveResource; true -> Resource end, Slave = if IsCarbons -> - jlib:make_jid(<<"test_master">>, Server, SlaveResource); + jid:make(<<"test_master!#$%^*()`~+-;_=[]{}|\\">>, Server, SlaveResource); + IsReplaced -> + jid:make(User, Server, Resource); true -> - jlib:make_jid(<<"test_slave">>, Server, Resource) + jid:make(<<"test_slave!#$%^*()`~+-;_=[]{}|\\">>, Server, Resource) end, Master = if IsCarbons -> - jlib:make_jid(<<"test_master">>, Server, MasterResource); + jid:make(<<"test_master!#$%^*()`~+-;_=[]{}|\\">>, Server, MasterResource); + IsReplaced -> + jid:make(User, Server, Resource); true -> - jlib:make_jid(<<"test_master">>, Server, Resource) + jid:make(<<"test_master!#$%^*()`~+-;_=[]{}|\\">>, Server, Resource) end, - Config = set_opt(user, User, - set_opt(slave, Slave, - set_opt(master, Master, - set_opt(resource, MyResource, OrigConfig)))), - case TestCase of - test_connect -> + Config1 = set_opt(user, User, + set_opt(slave, Slave, + set_opt(master, Master, + set_opt(resource, MyResource, + set_opt(nick, Nick, + set_opt(mode, Mode, OrigConfig)))))), + Config2 = if IsSlave -> + set_opt(peer_nick, ?config(master_nick, Config1), Config1); + IsMaster -> + set_opt(peer_nick, ?config(slave_nick, Config1), Config1); + true -> + Config1 + end, + Config = if IsSlave -> set_opt(peer, Master, Config2); + IsMaster -> set_opt(peer, Slave, Config2); + true -> Config2 + end, + case Test of + "test_connect" ++ _ -> Config; - test_auth -> + "webadmin_" ++ _ -> + Config; + "test_legacy_auth_feature" -> + connect(Config); + "test_legacy_auth" ++ _ -> + init_stream(set_opt(stream_version, undefined, Config)); + "test_auth" ++ _ -> connect(Config); - test_starttls -> + "test_starttls" ++ _ -> connect(Config); - test_zlib -> + "test_zlib" -> + auth(connect(starttls(connect(Config)))); + "test_register" -> connect(Config); - test_register -> + "auth_md5" -> connect(Config); - auth_md5 -> + "auth_plain" -> connect(Config); - auth_plain -> - connect(Config); - test_bind -> + "auth_external" ++ _ -> + connect(Config); + "unauthenticated_" ++ _ -> + connect(Config); + "test_bind" -> auth(connect(Config)); - sm_resume -> + "sm_resume" -> auth(connect(Config)); - test_open_session -> + "sm_resume_failed" -> + auth(connect(Config)); + "test_open_session" -> bind(auth(connect(Config))); + "replaced" ++ _ -> + auth(connect(Config)); + "antispam" ++ _ -> + Password = ?config(password, Config), + ejabberd_auth:try_register(User, Server, Password), + open_session(bind(auth(connect(Config)))); _ when IsMaster or IsSlave -> Password = ?config(password, Config), ejabberd_auth:try_register(User, Server, Password), open_session(bind(auth(connect(Config)))); + _ when TestGroup == s2s_tests -> + auth(connect(starttls(connect(Config)))); _ -> open_session(bind(auth(connect(Config)))) end. @@ -166,165 +356,301 @@ init_per_testcase(TestCase, OrigConfig) -> end_per_testcase(_TestCase, _Config) -> ok. +legacy_auth_tests() -> + {legacy_auth, [parallel], + [test_legacy_auth_feature, + test_legacy_auth, + test_legacy_auth_digest, + test_legacy_auth_no_resource, + test_legacy_auth_bad_jid, + test_legacy_auth_fail]}. + no_db_tests() -> - [{generic, [sequence], - [test_connect, + [{anonymous, [parallel], + [test_connect_bad_xml, + test_connect_unexpected_xml, + test_connect_unknown_ns, + test_connect_bad_xmlns, + test_connect_bad_ns_stream, + test_connect_bad_lang, + test_connect_bad_to, + test_connect_missing_to, + test_connect, + unauthenticated_iq, + unauthenticated_message, + unauthenticated_presence, test_starttls, - test_zlib, test_auth, + test_zlib, test_bind, test_open_session, - presence, + codec_failure, + unsupported_query, + bad_nonza, + invalid_from, ping, version, time, stats, - sm, - sm_resume, disco]}, - {test_proxy65, [parallel], - [proxy65_master, proxy65_slave]}]. + {presence_and_s2s, [sequence], + [test_auth_fail, + presence, + s2s_dialback, + s2s_optional, + s2s_required]}, + auth_external, + auth_external_no_jid, + auth_external_no_user, + auth_external_malformed_jid, + auth_external_wrong_jid, + auth_external_wrong_server, + auth_external_invalid_cert, + commands_tests:single_cases(), + configtest_tests:single_cases(), + jidprep_tests:single_cases(), + sm_tests:single_cases(), + sm_tests:master_slave_cases(), + muc_tests:single_cases(), + muc_tests:master_slave_cases(), + proxy65_tests:single_cases(), + proxy65_tests:master_slave_cases(), + stundisco_tests:single_cases(), + replaced_tests:master_slave_cases(), + upload_tests:single_cases(), + carbons_tests:single_cases(), + carbons_tests:master_slave_cases()]. -db_tests(riak) -> - %% No support for mod_pubsub +db_tests(DB) when DB == mnesia; DB == redis -> [{single_user, [sequence], [test_register, + legacy_auth_tests(), auth_plain, auth_md5, presence_broadcast, last, - roster_get, - private, - privacy, - blocking, - vcard, + antispam_tests:single_cases(), + webadmin_tests:single_cases(), + roster_tests:single_cases(), + private_tests:single_cases(), + privacy_tests:single_cases(), + vcard_tests:single_cases(), + pubsub_tests:single_cases(), + muc_tests:single_cases(), + offline_tests:single_cases(), + mam_tests:single_cases(), + csi_tests:single_cases(), + push_tests:single_cases(), + test_pass_change, test_unregister]}, - {test_muc_register, [sequence], - [muc_register_master, muc_register_slave]}, - {test_roster_subscribe, [parallel], - [roster_subscribe_master, - roster_subscribe_slave]}, - {test_offline, [sequence], - [offline_master, offline_slave]}, - {test_muc, [parallel], - [muc_master, muc_slave]}, - {test_announce, [sequence], - [announce_master, announce_slave]}, - {test_vcard_xupdate, [parallel], - [vcard_xupdate_master, vcard_xupdate_slave]}, - {test_roster_remove, [parallel], - [roster_remove_master, - roster_remove_slave]}]; -db_tests(mnesia) -> + muc_tests:master_slave_cases(), + privacy_tests:master_slave_cases(), + pubsub_tests:master_slave_cases(), + roster_tests:master_slave_cases(), + offline_tests:master_slave_cases(DB), + mam_tests:master_slave_cases(), + vcard_tests:master_slave_cases(), + announce_tests:master_slave_cases(), + csi_tests:master_slave_cases(), + push_tests:master_slave_cases()]; +db_tests(DB) -> [{single_user, [sequence], [test_register, + legacy_auth_tests(), auth_plain, auth_md5, presence_broadcast, last, - roster_get, - roster_ver, - private, - privacy, - blocking, - vcard, - pubsub, + webadmin_tests:single_cases(), + roster_tests:single_cases(), + private_tests:single_cases(), + privacy_tests:single_cases(), + vcard_tests:single_cases(), + pubsub_tests:single_cases(), + muc_tests:single_cases(), + offline_tests:single_cases(), + mam_tests:single_cases(), + push_tests:single_cases(), + test_pass_change, test_unregister]}, - {test_muc_register, [sequence], - [muc_register_master, muc_register_slave]}, - {test_roster_subscribe, [parallel], - [roster_subscribe_master, - roster_subscribe_slave]}, - {test_offline, [sequence], - [offline_master, offline_slave]}, - {test_carbons, [parallel], - [carbons_master, carbons_slave]}, - {test_client_state, [parallel], - [client_state_master, client_state_slave]}, - {test_muc, [parallel], - [muc_master, muc_slave]}, - {test_announce, [sequence], - [announce_master, announce_slave]}, - {test_vcard_xupdate, [parallel], - [vcard_xupdate_master, vcard_xupdate_slave]}, - {test_roster_remove, [parallel], - [roster_remove_master, - roster_remove_slave]}]; -db_tests(_) -> - %% No support for carboncopy - [{single_user, [sequence], - [test_register, - auth_plain, - auth_md5, - presence_broadcast, - last, - roster_get, - roster_ver, - private, - privacy, - blocking, - vcard, - pubsub, - test_unregister]}, - {test_muc_register, [sequence], - [muc_register_master, muc_register_slave]}, - {test_roster_subscribe, [parallel], - [roster_subscribe_master, - roster_subscribe_slave]}, - {test_offline, [sequence], - [offline_master, offline_slave]}, - {test_muc, [parallel], - [muc_master, muc_slave]}, - {test_announce, [sequence], - [announce_master, announce_slave]}, - {test_vcard_xupdate, [parallel], - [vcard_xupdate_master, vcard_xupdate_slave]}, - {test_roster_remove, [parallel], - [roster_remove_master, - roster_remove_slave]}]. + muc_tests:master_slave_cases(), + privacy_tests:master_slave_cases(), + pubsub_tests:master_slave_cases(), + roster_tests:master_slave_cases(), + offline_tests:master_slave_cases(DB), + mam_tests:master_slave_cases(), + vcard_tests:master_slave_cases(), + announce_tests:master_slave_cases(), + push_tests:master_slave_cases()]. ldap_tests() -> [{ldap_tests, [sequence], [test_auth, - vcard_get]}]. + test_auth_fail, + vcard_get, + ldap_shared_roster_get]}]. extauth_tests() -> [{extauth_tests, [sequence], [test_auth, + test_auth_fail, test_unregister]}]. +component_tests() -> + [{component_connect, [parallel], + [test_connect_bad_xml, + test_connect_unexpected_xml, + test_connect_unknown_ns, + test_connect_bad_xmlns, + test_connect_bad_ns_stream, + test_connect_missing_to, + test_connect, + test_auth, + test_auth_fail]}, + {component_tests, [sequence], + [test_missing_from, + test_missing_to, + test_invalid_from, + test_component_send, + bad_nonza, + codec_failure]}]. + +s2s_tests() -> + [{s2s_connect, [parallel], + [test_connect_bad_xml, + test_connect_unexpected_xml, + test_connect_unknown_ns, + test_connect_bad_xmlns, + test_connect_bad_ns_stream, + test_connect, + test_connect_s2s_starttls_required, + test_starttls, + test_connect_s2s_unauthenticated_iq, + test_auth_starttls]}, + {s2s_tests, [sequence], + [test_missing_from, + test_missing_to, + test_invalid_from, + bad_nonza, + codec_failure]}]. + groups() -> [{ldap, [sequence], ldap_tests()}, {extauth, [sequence], extauth_tests()}, {no_db, [sequence], no_db_tests()}, + {component, [sequence], component_tests()}, + {s2s, [sequence], s2s_tests()}, {mnesia, [sequence], db_tests(mnesia)}, + {redis, [sequence], db_tests(redis)}, {mysql, [sequence], db_tests(mysql)}, + {mssql, [sequence], db_tests(mssql)}, {pgsql, [sequence], db_tests(pgsql)}, - {riak, [sequence], db_tests(riak)}]. + {sqlite, [sequence], db_tests(sqlite)}]. all() -> [{group, ldap}, {group, no_db}, {group, mnesia}, + {group, redis}, {group, mysql}, + {group, mssql}, {group, pgsql}, + {group, sqlite}, {group, extauth}, - {group, riak}, + {group, component}, + {group, s2s}, stop_ejabberd]. stop_ejabberd(Config) -> ok = application:stop(ejabberd), - #stream_error{reason = 'system-shutdown'} = recv(), - {xmlstreamend, <<"stream:stream">>} = recv(), + ?recv1(#stream_error{reason = 'system-shutdown'}), + case suite:recv(Config) of + {xmlstreamend, <<"stream:stream">>} -> + ok; + closed -> + ok; + Other -> + suite:match_failure([Other], [closed]) + end, Config. +test_connect_bad_xml(Config) -> + Config0 = tcp_connect(Config), + send_text(Config0, <<"<'/>">>), + Version = ?config(stream_version, Config0), + ?recv1(#stream_start{version = Version}), + ?recv1(#stream_error{reason = 'not-well-formed'}), + ?recv1({xmlstreamend, <<"stream:stream">>}), + close_socket(Config0). + +test_connect_unexpected_xml(Config) -> + Config0 = tcp_connect(Config), + send(Config0, #caps{}), + Version = ?config(stream_version, Config0), + ?recv1(#stream_start{version = Version}), + ?recv1(#stream_error{reason = 'invalid-xml'}), + ?recv1({xmlstreamend, <<"stream:stream">>}), + close_socket(Config0). + +test_connect_unknown_ns(Config) -> + Config0 = init_stream(set_opt(xmlns, <<"wrong">>, Config)), + ?recv1(#stream_error{reason = 'invalid-xml'}), + ?recv1({xmlstreamend, <<"stream:stream">>}), + close_socket(Config0). + +test_connect_bad_xmlns(Config) -> + NS = case ?config(type, Config) of + client -> ?NS_SERVER; + _ -> ?NS_CLIENT + end, + Config0 = init_stream(set_opt(xmlns, NS, Config)), + ?recv1(#stream_error{reason = 'invalid-namespace'}), + ?recv1({xmlstreamend, <<"stream:stream">>}), + close_socket(Config0). + +test_connect_bad_ns_stream(Config) -> + Config0 = init_stream(set_opt(ns_stream, <<"wrong">>, Config)), + ?recv1(#stream_error{reason = 'invalid-namespace'}), + ?recv1({xmlstreamend, <<"stream:stream">>}), + close_socket(Config0). + +test_connect_bad_lang(Config) -> + Lang = iolist_to_binary(lists:duplicate(36, $x)), + Config0 = init_stream(set_opt(lang, Lang, Config)), + ?recv1(#stream_error{reason = 'invalid-xml'}), + ?recv1({xmlstreamend, <<"stream:stream">>}), + close_socket(Config0). + +test_connect_bad_to(Config) -> + Config0 = init_stream(set_opt(server, <<"wrong.com">>, Config)), + ?recv1(#stream_error{reason = 'host-unknown'}), + ?recv1({xmlstreamend, <<"stream:stream">>}), + close_socket(Config0). + +test_connect_missing_to(Config) -> + Config0 = init_stream(set_opt(server, <<"">>, Config)), + ?recv1(#stream_error{reason = 'improper-addressing'}), + ?recv1({xmlstreamend, <<"stream:stream">>}), + close_socket(Config0). + test_connect(Config) -> disconnect(connect(Config)). +test_connect_s2s_starttls_required(Config) -> + Config1 = connect(Config), + send(Config1, #presence{}), + ?recv1(#stream_error{reason = 'policy-violation'}), + ?recv1({xmlstreamend, <<"stream:stream">>}), + close_socket(Config1). + +test_connect_s2s_unauthenticated_iq(Config) -> + Config1 = connect(starttls(connect(Config))), + unauthenticated_iq(Config1). + test_starttls(Config) -> case ?config(starttls, Config) of true -> - disconnect(starttls(Config)); + disconnect(connect(starttls(Config))); _ -> {skipped, 'starttls_not_available'} end. @@ -352,8 +678,8 @@ test_register(Config) -> register(Config) -> #iq{type = result, - sub_els = [#register{username = none, - password = none}]} = + sub_els = [#register{username = <<>>, + password = <<>>}]} = send_recv(Config, #iq{type = get, to = server_jid(Config), sub_els = [#register{}]}), #iq{type = result, sub_els = []} = @@ -364,6 +690,25 @@ register(Config) -> password = ?config(password, Config)}]}), Config. +test_pass_change(Config) -> + case ?config(register, Config) of + true -> + #iq{type = result, sub_els = []} = + send_recv( + Config, + #iq{type = set, + sub_els = [#register{username = ?config(user, Config), + password = ?config(password, Config)}]}), + #iq{type = result, sub_els = []} = + send_recv( + Config, + #iq{type = set, + sub_els = [#register{username = str:to_upper(?config(user, Config)), + password = ?config(password, Config)}]}); + _ -> + {skipped, 'registration_not_available'} + end. + test_unregister(Config) -> case ?config(register, Config) of true -> @@ -379,9 +724,104 @@ try_unregister(Config) -> Config, #iq{type = set, sub_els = [#register{remove = true}]}), - #stream_error{reason = conflict} = recv(), + ?recv1(#stream_error{reason = conflict}), Config. +unauthenticated_presence(Config) -> + unauthenticated_packet(Config, #presence{}). + +unauthenticated_message(Config) -> + unauthenticated_packet(Config, #message{}). + +unauthenticated_iq(Config) -> + IQ = #iq{type = get, sub_els = [#disco_info{}]}, + unauthenticated_packet(Config, IQ). + +unauthenticated_packet(Config, Pkt) -> + From = my_jid(Config), + To = server_jid(Config), + send(Config, xmpp:set_from_to(Pkt, From, To)), + #stream_error{reason = 'not-authorized'} = recv(Config), + {xmlstreamend, <<"stream:stream">>} = recv(Config), + close_socket(Config). + +bad_nonza(Config) -> + %% Unsupported and invalid nonza should be silently dropped. + send(Config, #caps{}), + send(Config, #stanza_error{type = wrong}), + disconnect(Config). + +invalid_from(Config) -> + send(Config, #message{from = jid:make(p1_rand:get_string())}), + ?recv1(#stream_error{reason = 'invalid-from'}), + ?recv1({xmlstreamend, <<"stream:stream">>}), + close_socket(Config). + +test_missing_from(Config) -> + Server = server_jid(Config), + send(Config, #message{to = Server}), + ?recv1(#stream_error{reason = 'improper-addressing'}), + ?recv1({xmlstreamend, <<"stream:stream">>}), + close_socket(Config). + +test_missing_to(Config) -> + Server = server_jid(Config), + send(Config, #message{from = Server}), + ?recv1(#stream_error{reason = 'improper-addressing'}), + ?recv1({xmlstreamend, <<"stream:stream">>}), + close_socket(Config). + +test_invalid_from(Config) -> + From = jid:make(p1_rand:get_string()), + To = jid:make(p1_rand:get_string()), + send(Config, #message{from = From, to = To}), + ?recv1(#stream_error{reason = 'invalid-from'}), + ?recv1({xmlstreamend, <<"stream:stream">>}), + close_socket(Config). + +test_component_send(Config) -> + To = jid:make(?COMMON_VHOST), + From = server_jid(Config), + #iq{type = result, from = To, to = From} = + send_recv(Config, #iq{type = get, to = To, from = From, + sub_els = [#ping{}]}), + disconnect(Config). + +s2s_dialback(Config) -> + Server = ?config(server, Config), + ejabberd_s2s:stop_s2s_connections(), + ejabberd_config:set_option({s2s_use_starttls, Server}, false), + ejabberd_config:set_option({s2s_use_starttls, ?MNESIA_VHOST}, false), + ejabberd_config:set_option(ca_file, pkix:get_cafile()), + s2s_ping(Config). + +s2s_optional(Config) -> + Server = ?config(server, Config), + ejabberd_s2s:stop_s2s_connections(), + ejabberd_config:set_option({s2s_use_starttls, Server}, optional), + ejabberd_config:set_option({s2s_use_starttls, ?MNESIA_VHOST}, optional), + ejabberd_config:set_option(ca_file, pkix:get_cafile()), + s2s_ping(Config). + +s2s_required(Config) -> + Server = ?config(server, Config), + ejabberd_s2s:stop_s2s_connections(), + gen_mod:stop_module(Server, mod_s2s_dialback), + gen_mod:stop_module(?MNESIA_VHOST, mod_s2s_dialback), + ejabberd_config:set_option({s2s_use_starttls, Server}, required), + ejabberd_config:set_option({s2s_use_starttls, ?MNESIA_VHOST}, required), + ejabberd_config:set_option(ca_file, "ca.pem"), + s2s_ping(Config). + +s2s_ping(Config) -> + From = my_jid(Config), + To = jid:make(?MNESIA_VHOST), + ID = p1_rand:get_string(), + ejabberd_s2s:route(#iq{from = From, to = To, id = ID, + type = get, sub_els = [#ping{}]}), + #iq{type = result, id = ID, sub_els = []} = recv_iq(Config), + disconnect(Config). + auth_md5(Config) -> Mechs = ?config(mechs, Config), case lists:member(<<"DIGEST-MD5">>, Mechs) of @@ -402,87 +842,135 @@ auth_plain(Config) -> {skipped, 'PLAIN_not_available'} end. +auth_external(Config0) -> + Config = connect(starttls(Config0)), + disconnect(auth_SASL(<<"EXTERNAL">>, Config)). + +auth_external_no_jid(Config0) -> + Config = connect(starttls(Config0)), + disconnect(auth_SASL(<<"EXTERNAL">>, Config, _ShoudFail = false, + {<<"">>, <<"">>, <<"">>})). + +auth_external_no_user(Config0) -> + Config = set_opt(user, <<"">>, connect(starttls(Config0))), + disconnect(auth_SASL(<<"EXTERNAL">>, Config)). + +auth_external_malformed_jid(Config0) -> + Config = connect(starttls(Config0)), + disconnect(auth_SASL(<<"EXTERNAL">>, Config, _ShouldFail = true, + {<<"">>, <<"@">>, <<"">>})). + +auth_external_wrong_jid(Config0) -> + Config = set_opt(user, <<"wrong">>, + connect(starttls(Config0))), + disconnect(auth_SASL(<<"EXTERNAL">>, Config, _ShouldFail = true)). + +auth_external_wrong_server(Config0) -> + Config = connect(starttls(Config0)), + disconnect(auth_SASL(<<"EXTERNAL">>, Config, _ShouldFail = true, + {<<"">>, <<"wrong.com">>, <<"">>})). + +auth_external_invalid_cert(Config0) -> + Config = connect(starttls( + set_opt(certfile, "self-signed-cert.pem", Config0))), + disconnect(auth_SASL(<<"EXTERNAL">>, Config, _ShouldFail = true)). + +test_legacy_auth_feature(Config) -> + true = ?config(legacy_auth, Config), + disconnect(Config). + +test_legacy_auth(Config) -> + disconnect(auth_legacy(Config, _Digest = false)). + +test_legacy_auth_digest(Config) -> + disconnect(auth_legacy(Config, _Digest = true)). + +test_legacy_auth_no_resource(Config0) -> + Config = set_opt(resource, <<"">>, Config0), + disconnect(auth_legacy(Config, _Digest = false, _ShouldFail = true)). + +test_legacy_auth_bad_jid(Config0) -> + Config = set_opt(user, <<"@">>, Config0), + disconnect(auth_legacy(Config, _Digest = false, _ShouldFail = true)). + +test_legacy_auth_fail(Config0) -> + Config = set_opt(user, <<"wrong">>, Config0), + disconnect(auth_legacy(Config, _Digest = false, _ShouldFail = true)). + test_auth(Config) -> disconnect(auth(Config)). +test_auth_starttls(Config) -> + disconnect(auth(connect(starttls(Config)))). + +test_auth_fail(Config0) -> + Config = set_opt(user, <<"wrong">>, + set_opt(password, <<"wrong">>, Config0)), + disconnect(auth(Config, _ShouldFail = true)). + test_bind(Config) -> disconnect(bind(Config)). test_open_session(Config) -> - disconnect(open_session(Config)). + disconnect(open_session(Config, true)). -roster_get(Config) -> - #iq{type = result, sub_els = [#roster{items = []}]} = - send_recv(Config, #iq{type = get, sub_els = [#roster{}]}), +codec_failure(Config) -> + JID = my_jid(Config), + #iq{type = error} = + send_recv(Config, #iq{type = wrong, from = JID, to = JID}), disconnect(Config). -roster_ver(Config) -> - %% Get initial "ver" - #iq{type = result, sub_els = [#roster{ver = Ver1, items = []}]} = - send_recv(Config, #iq{type = get, - sub_els = [#roster{ver = <<"">>}]}), - %% Should receive empty IQ-result - #iq{type = result, sub_els = []} = - send_recv(Config, #iq{type = get, - sub_els = [#roster{ver = Ver1}]}), - %% Attempting to subscribe to server's JID - send(Config, #presence{type = subscribe, to = server_jid(Config)}), - %% Receive a single roster push with the new "ver" - #iq{type = set, sub_els = [#roster{ver = Ver2}]} = recv(), - %% Requesting roster with the previous "ver". Should receive Ver2 again - #iq{type = result, sub_els = [#roster{ver = Ver2}]} = - send_recv(Config, #iq{type = get, - sub_els = [#roster{ver = Ver1}]}), - %% Now requesting roster with the newest "ver". Should receive empty IQ. - #iq{type = result, sub_els = []} = - send_recv(Config, #iq{type = get, - sub_els = [#roster{ver = Ver2}]}), +unsupported_query(Config) -> + ServerJID = server_jid(Config), + #iq{type = error} = send_recv(Config, #iq{type = get, to = ServerJID}), + #iq{type = error} = send_recv(Config, #iq{type = get, to = ServerJID, + sub_els = [#caps{}]}), + #iq{type = error} = send_recv(Config, #iq{type = get, to = ServerJID, + sub_els = [#roster_query{}, + #disco_info{}, + #privacy_query{}]}), disconnect(Config). presence(Config) -> - send(Config, #presence{}), JID = my_jid(Config), - #presence{from = JID, to = JID} = recv(), + #presence{from = JID, to = JID} = send_recv(Config, #presence{}), disconnect(Config). presence_broadcast(Config) -> - Feature = <<"p1:tmp:", (randoms:get_string())/binary>>, - Ver = crypto:sha(["client", $/, "bot", $/, "en", $/, - "ejabberd_ct", $<, Feature, $<]), + Feature = <<"p1:tmp:", (p1_rand:get_string())/binary>>, + Ver = crypto:hash(sha, ["client", $/, "bot", $/, "en", $/, + "ejabberd_ct", $<, Feature, $<]), B64Ver = base64:encode(Ver), Node = <<(?EJABBERD_CT_URI)/binary, $#, B64Ver/binary>>, Server = ?config(server, Config), - ServerJID = server_jid(Config), Info = #disco_info{identities = [#identity{category = <<"client">>, type = <<"bot">>, lang = <<"en">>, name = <<"ejabberd_ct">>}], node = Node, features = [Feature]}, - Caps = #caps{hash = <<"sha-1">>, node = ?EJABBERD_CT_URI, ver = Ver}, + Caps = #caps{hash = <<"sha-1">>, node = ?EJABBERD_CT_URI, version = B64Ver}, send(Config, #presence{sub_els = [Caps]}), JID = my_jid(Config), %% We receive: %% 1) disco#info iq request for CAPS %% 2) welcome message %% 3) presence broadcast - {IQ, _, _} = ?recv3(#iq{type = get, - from = ServerJID, - sub_els = [#disco_info{node = Node}]}, - #message{type = normal}, - #presence{from = JID, to = JID}), + IQ = #iq{type = get, + from = JID, + sub_els = [#disco_info{node = Node}]} = recv_iq(Config), + #message{type = chat, + subject = [#text{lang = <<"en">>,data = <<"Welcome!">>}]} = recv_message(Config), + #presence{from = JID, to = JID} = recv_presence(Config), send(Config, #iq{type = result, id = IQ#iq.id, - to = ServerJID, sub_els = [Info]}), + to = JID, sub_els = [Info]}), %% We're trying to read our feature from ejabberd database %% with exponential back-off as our IQ response may be delayed. [Feature] = lists:foldl( fun(Time, []) -> timer:sleep(Time), - mod_caps:get_features( - Server, - mod_caps:read_caps( - [xmpp_codec:encode(Caps)])); + mod_caps:get_features(Server, Caps); (_, Acc) -> Acc end, [], [0, 100, 200, 2000, 5000, 10000]), @@ -527,69 +1015,6 @@ disco(Config) -> end, Items), disconnect(Config). -sm(Config) -> - Server = ?config(server, Config), - ServerJID = jlib:make_jid(<<"">>, Server, <<"">>), - Msg = #message{to = ServerJID, body = [#text{data = <<"body">>}]}, - true = ?config(sm, Config), - %% Enable the session management with resumption enabled - send(Config, #sm_enable{resume = true, xmlns = ?NS_STREAM_MGMT_3}), - #sm_enabled{id = ID, resume = true} = recv(), - %% Initial request; 'h' should be 0. - send(Config, #sm_r{xmlns = ?NS_STREAM_MGMT_3}), - #sm_a{h = 0} = recv(), - %% sending two messages and requesting again; 'h' should be 3. - send(Config, Msg), - send(Config, Msg), - send(Config, Msg), - send(Config, #sm_r{xmlns = ?NS_STREAM_MGMT_3}), - #sm_a{h = 3} = recv(), - close_socket(Config), - {save_config, set_opt(sm_previd, ID, Config)}. - -sm_resume(Config) -> - {sm, SMConfig} = ?config(saved_config, Config), - ID = ?config(sm_previd, SMConfig), - Server = ?config(server, Config), - ServerJID = jlib:make_jid(<<"">>, Server, <<"">>), - MyJID = my_jid(Config), - Txt = #text{data = <<"body">>}, - Msg = #message{from = ServerJID, to = MyJID, body = [Txt]}, - %% Route message. The message should be queued by the C2S process. - ejabberd_router:route(ServerJID, MyJID, xmpp_codec:encode(Msg)), - send(Config, #sm_resume{previd = ID, h = 0, xmlns = ?NS_STREAM_MGMT_3}), - #sm_resumed{previd = ID, h = 3} = recv(), - #message{from = ServerJID, to = MyJID, body = [Txt]} = recv(), - #sm_r{} = recv(), - send(Config, #sm_a{h = 1, xmlns = ?NS_STREAM_MGMT_3}), - disconnect(Config). - -private(Config) -> - Conference = #bookmark_conference{name = <<"Some name">>, - autojoin = true, - jid = jlib:make_jid( - <<"some">>, - <<"some.conference.org">>, - <<>>)}, - Storage = #bookmark_storage{conference = [Conference]}, - StorageXMLOut = xmpp_codec:encode(Storage), - #iq{type = error} = - send_recv(Config, #iq{type = get, sub_els = [#private{}], - to = server_jid(Config)}), - #iq{type = result, sub_els = []} = - send_recv( - Config, #iq{type = set, - sub_els = [#private{xml_els = [StorageXMLOut]}]}), - #iq{type = result, - sub_els = [#private{xml_els = [StorageXMLIn]}]} = - send_recv( - Config, - #iq{type = get, - sub_els = [#private{xml_els = [xmpp_codec:encode( - #bookmark_storage{})]}]}), - Storage = xmpp_codec:decode(StorageXMLIn), - disconnect(Config). - last(Config) -> true = is_feature_advertised(Config, ?NS_LAST), #iq{type = result, sub_els = [#last{}]} = @@ -597,1056 +1022,100 @@ last(Config) -> to = server_jid(Config)}), disconnect(Config). -privacy(Config) -> - true = is_feature_advertised(Config, ?NS_PRIVACY), - #iq{type = result, sub_els = [#privacy{}]} = - send_recv(Config, #iq{type = get, sub_els = [#privacy{}]}), - JID = <<"tybalt@example.com">>, - I1 = send(Config, - #iq{type = set, - sub_els = [#privacy{ - lists = [#privacy_list{ - name = <<"public">>, - items = - [#privacy_item{ - type = jid, - order = 3, - action = deny, - kinds = ['presence-in'], - value = JID}]}]}]}), - {Push1, _} = - ?recv2( - #iq{type = set, - sub_els = [#privacy{ - lists = [#privacy_list{ - name = <<"public">>}]}]}, - #iq{type = result, id = I1, sub_els = []}), - send(Config, make_iq_result(Push1)), - #iq{type = result, sub_els = []} = - send_recv(Config, #iq{type = set, - sub_els = [#privacy{active = <<"public">>}]}), - #iq{type = result, sub_els = []} = - send_recv(Config, #iq{type = set, - sub_els = [#privacy{default = <<"public">>}]}), - #iq{type = result, - sub_els = [#privacy{default = <<"public">>, - active = <<"public">>, - lists = [#privacy_list{name = <<"public">>}]}]} = - send_recv(Config, #iq{type = get, sub_els = [#privacy{}]}), - #iq{type = result, sub_els = []} = - send_recv(Config, - #iq{type = set, sub_els = [#privacy{default = none}]}), - #iq{type = result, sub_els = []} = - send_recv(Config, #iq{type = set, sub_els = [#privacy{active = none}]}), - I2 = send(Config, #iq{type = set, - sub_els = [#privacy{ - lists = - [#privacy_list{ - name = <<"public">>}]}]}), - {Push2, _} = - ?recv2( - #iq{type = set, - sub_els = [#privacy{ - lists = [#privacy_list{ - name = <<"public">>}]}]}, - #iq{type = result, id = I2, sub_els = []}), - send(Config, make_iq_result(Push2)), - disconnect(Config). - -blocking(Config) -> - true = is_feature_advertised(Config, ?NS_BLOCKING), - JID = jlib:make_jid(<<"romeo">>, <<"montague.net">>, <<>>), - #iq{type = result, sub_els = [#block_list{}]} = - send_recv(Config, #iq{type = get, sub_els = [#block_list{}]}), - I1 = send(Config, #iq{type = set, - sub_els = [#block{items = [JID]}]}), - {Push1, Push2, _} = - ?recv3( - #iq{type = set, - sub_els = [#privacy{lists = [#privacy_list{}]}]}, - #iq{type = set, - sub_els = [#block{items = [JID]}]}, - #iq{type = result, id = I1, sub_els = []}), - send(Config, make_iq_result(Push1)), - send(Config, make_iq_result(Push2)), - I2 = send(Config, #iq{type = set, - sub_els = [#unblock{items = [JID]}]}), - {Push3, Push4, _} = - ?recv3( - #iq{type = set, - sub_els = [#privacy{lists = [#privacy_list{}]}]}, - #iq{type = set, - sub_els = [#unblock{items = [JID]}]}, - #iq{type = result, id = I2, sub_els = []}), - send(Config, make_iq_result(Push3)), - send(Config, make_iq_result(Push4)), - disconnect(Config). - -vcard(Config) -> - true = is_feature_advertised(Config, ?NS_VCARD), - VCard = - #vcard{fn = <<"Peter Saint-Andre">>, - n = #vcard_name{family = <<"Saint-Andre">>, - given = <<"Peter">>}, - nickname = <<"stpeter">>, - bday = <<"1966-08-06">>, - adr = [#vcard_adr{work = true, - extadd = <<"Suite 600">>, - street = <<"1899 Wynkoop Street">>, - locality = <<"Denver">>, - region = <<"CO">>, - pcode = <<"80202">>, - ctry = <<"USA">>}, - #vcard_adr{home = true, - locality = <<"Denver">>, - region = <<"CO">>, - pcode = <<"80209">>, - ctry = <<"USA">>}], - tel = [#vcard_tel{work = true,voice = true, - number = <<"303-308-3282">>}, - #vcard_tel{home = true,voice = true, - number = <<"303-555-1212">>}], - email = [#vcard_email{internet = true,pref = true, - userid = <<"stpeter@jabber.org">>}], - jabberid = <<"stpeter@jabber.org">>, - title = <<"Executive Director">>,role = <<"Patron Saint">>, - org = #vcard_org{name = <<"XMPP Standards Foundation">>}, - url = <<"http://www.xmpp.org/xsf/people/stpeter.shtml">>, - desc = <<"More information about me is located on my " - "personal website: http://www.saint-andre.com/">>}, - #iq{type = result, sub_els = []} = - send_recv(Config, #iq{type = set, sub_els = [VCard]}), - %% TODO: check if VCard == VCard1. - #iq{type = result, sub_els = [_VCard1]} = - send_recv(Config, #iq{type = get, sub_els = [#vcard{}]}), - disconnect(Config). - vcard_get(Config) -> true = is_feature_advertised(Config, ?NS_VCARD), %% TODO: check if VCard corresponds to LDIF data from ejabberd.ldif #iq{type = result, sub_els = [_VCard]} = - send_recv(Config, #iq{type = get, sub_els = [#vcard{}]}), + send_recv(Config, #iq{type = get, sub_els = [#vcard_temp{}]}), disconnect(Config). -vcard_xupdate_master(Config) -> - Img = <<137, "PNG\r\n", 26, $\n>>, - ImgHash = p1_sha:sha(Img), - MyJID = my_jid(Config), - Peer = ?config(slave, Config), - wait_for_slave(Config), - send(Config, #presence{}), - ?recv2(#presence{from = MyJID, type = undefined}, - #presence{from = Peer, type = undefined}), - VCard = #vcard{photo = #vcard_photo{type = <<"image/png">>, binval = Img}}, - I1 = send(Config, #iq{type = set, sub_els = [VCard]}), - ?recv2(#iq{type = result, sub_els = [], id = I1}, - #presence{from = MyJID, type = undefined, - sub_els = [#vcard_xupdate{photo = ImgHash}]}), - I2 = send(Config, #iq{type = set, sub_els = [#vcard{}]}), - ?recv3(#iq{type = result, sub_els = [], id = I2}, - #presence{from = MyJID, type = undefined, - sub_els = [#vcard_xupdate{photo = undefined}]}, - #presence{from = Peer, type = unavailable}), - disconnect(Config). - -vcard_xupdate_slave(Config) -> - Img = <<137, "PNG\r\n", 26, $\n>>, - ImgHash = p1_sha:sha(Img), - MyJID = my_jid(Config), - Peer = ?config(master, Config), - send(Config, #presence{}), - #presence{from = MyJID, type = undefined} = recv(), - wait_for_master(Config), - #presence{from = Peer, type = undefined} = recv(), - #presence{from = Peer, type = undefined, - sub_els = [#vcard_xupdate{photo = ImgHash}]} = recv(), - #presence{from = Peer, type = undefined, - sub_els = [#vcard_xupdate{photo = undefined}]} = recv(), +ldap_shared_roster_get(Config) -> + Item = #roster_item{jid = jid:decode(<<"user2@ldap.localhost">>), name = <<"Test User 2">>, + groups = [<<"group1">>], subscription = both}, + #iq{type = result, sub_els = [#roster_query{items = [Item]}]} = + send_recv(Config, #iq{type = get, sub_els = [#roster_query{}]}), disconnect(Config). stats(Config) -> - #iq{type = result, sub_els = [#stats{stat = Stats}]} = + #iq{type = result, sub_els = [#stats{list = Stats}]} = send_recv(Config, #iq{type = get, sub_els = [#stats{}], to = server_jid(Config)}), lists:foreach( fun(#stat{} = Stat) -> #iq{type = result, sub_els = [_|_]} = send_recv(Config, #iq{type = get, - sub_els = [#stats{stat = [Stat]}], + sub_els = [#stats{list = [Stat]}], to = server_jid(Config)}) end, Stats), disconnect(Config). -pubsub(Config) -> - Features = get_features(Config, pubsub_jid(Config)), - true = lists:member(?NS_PUBSUB, Features), - %% Publish element within node "presence" - ItemID = randoms:get_string(), - Node = <<"presence">>, - Item = #pubsub_item{id = ItemID, - xml_els = [xmpp_codec:encode(#presence{})]}, - #iq{type = result, - sub_els = [#pubsub{publish = #pubsub_publish{ - node = Node, - items = [#pubsub_item{id = ItemID}]}}]} = - send_recv(Config, - #iq{type = set, to = pubsub_jid(Config), - sub_els = [#pubsub{publish = #pubsub_publish{ - node = Node, - items = [Item]}}]}), - %% Subscribe to node "presence" - I1 = send(Config, - #iq{type = set, to = pubsub_jid(Config), - sub_els = [#pubsub{subscribe = #pubsub_subscribe{ - node = Node, - jid = my_jid(Config)}}]}), - ?recv2( - #message{sub_els = [#pubsub_event{}, #delay{}, #legacy_delay{}]}, - #iq{type = result, id = I1}), - %% Get subscriptions - true = lists:member(?PUBSUB("retrieve-subscriptions"), Features), - #iq{type = result, - sub_els = - [#pubsub{subscriptions = - {none, [#pubsub_subscription{node = Node}]}}]} = - send_recv(Config, #iq{type = get, to = pubsub_jid(Config), - sub_els = [#pubsub{subscriptions = {none, []}}]}), - %% Get affiliations - true = lists:member(?PUBSUB("retrieve-affiliations"), Features), - #iq{type = result, - sub_els = [#pubsub{ - affiliations = - [#pubsub_affiliation{node = Node, type = owner}]}]} = - send_recv(Config, #iq{type = get, to = pubsub_jid(Config), - sub_els = [#pubsub{affiliations = []}]}), - %% Get subscription options - true = lists:member(?PUBSUB("subscription-options"), Features), - #iq{type = result, sub_els = [#pubsub{options = #pubsub_options{ - node = Node}}]} = - send_recv(Config, - #iq{type = get, to = pubsub_jid(Config), - sub_els = [#pubsub{options = #pubsub_options{ - node = Node, - jid = my_jid(Config)}}]}), - %% Fetching published items from node "presence" - #iq{type = result, - sub_els = [#pubsub{items = #pubsub_items{ - node = Node, - items = [Item]}}]} = - send_recv(Config, - #iq{type = get, to = pubsub_jid(Config), - sub_els = [#pubsub{items = #pubsub_items{node = Node}}]}), - %% Deleting the item from the node - true = lists:member(?PUBSUB("delete-items"), Features), - I2 = send(Config, - #iq{type = set, to = pubsub_jid(Config), - sub_els = [#pubsub{retract = #pubsub_retract{ - node = Node, - items = [#pubsub_item{id = ItemID}]}}]}), - ?recv2( - #iq{type = result, id = I2, sub_els = []}, - #message{sub_els = [#pubsub_event{ - items = [#pubsub_event_items{ - node = Node, - retract = [ItemID]}]}, - #shim{headers = [{<<"Collection">>, Node}]}]}), - %% Unsubscribe from node "presence" - #iq{type = result, sub_els = []} = - send_recv(Config, - #iq{type = set, to = pubsub_jid(Config), - sub_els = [#pubsub{unsubscribe = #pubsub_unsubscribe{ - node = Node, - jid = my_jid(Config)}}]}), - disconnect(Config). - -roster_subscribe_master(Config) -> - send(Config, #presence{}), - #presence{} = recv(), - wait_for_slave(Config), - Peer = ?config(slave, Config), - LPeer = jlib:jid_remove_resource(Peer), - send(Config, #presence{type = subscribe, to = LPeer}), - Push1 = #iq{type = set, - sub_els = [#roster{items = [#roster_item{ - ask = subscribe, - subscription = none, - jid = LPeer}]}]} = recv(), - send(Config, make_iq_result(Push1)), - {Push2, _} = ?recv2( - #iq{type = set, - sub_els = [#roster{items = [#roster_item{ - subscription = to, - jid = LPeer}]}]}, - #presence{type = subscribed, from = LPeer}), - send(Config, make_iq_result(Push2)), - #presence{type = undefined, from = Peer} = recv(), - %% BUG: ejabberd sends previous push again. Is it ok? - Push3 = #iq{type = set, - sub_els = [#roster{items = [#roster_item{ - subscription = to, - jid = LPeer}]}]} = recv(), - send(Config, make_iq_result(Push3)), - #presence{type = subscribe, from = LPeer} = recv(), - send(Config, #presence{type = subscribed, to = LPeer}), - Push4 = #iq{type = set, - sub_els = [#roster{items = [#roster_item{ - subscription = both, - jid = LPeer}]}]} = recv(), - send(Config, make_iq_result(Push4)), - %% Move into a group - Groups = [<<"A">>, <<"B">>], - Item = #roster_item{jid = LPeer, groups = Groups}, - I1 = send(Config, #iq{type = set, sub_els = [#roster{items = [Item]}]}), - {Push5, _} = ?recv2( - #iq{type = set, - sub_els = - [#roster{items = [#roster_item{ - jid = LPeer, - subscription = both}]}]}, - #iq{type = result, id = I1, sub_els = []}), - send(Config, make_iq_result(Push5)), - #iq{sub_els = [#roster{items = [#roster_item{groups = G1}]}]} = Push5, - Groups = lists:sort(G1), - wait_for_slave(Config), - #presence{type = unavailable, from = Peer} = recv(), - disconnect(Config). - -roster_subscribe_slave(Config) -> - send(Config, #presence{}), - #presence{} = recv(), - wait_for_master(Config), - Peer = ?config(master, Config), - LPeer = jlib:jid_remove_resource(Peer), - #presence{type = subscribe, from = LPeer} = recv(), - send(Config, #presence{type = subscribed, to = LPeer}), - Push1 = #iq{type = set, - sub_els = [#roster{items = [#roster_item{ - subscription = from, - jid = LPeer}]}]} = recv(), - send(Config, make_iq_result(Push1)), - send(Config, #presence{type = subscribe, to = LPeer}), - Push2 = #iq{type = set, - sub_els = [#roster{items = [#roster_item{ - ask = subscribe, - subscription = from, - jid = LPeer}]}]} = recv(), - send(Config, make_iq_result(Push2)), - {Push3, _} = ?recv2( - #iq{type = set, - sub_els = [#roster{items = [#roster_item{ - subscription = both, - jid = LPeer}]}]}, - #presence{type = subscribed, from = LPeer}), - send(Config, make_iq_result(Push3)), - #presence{type = undefined, from = Peer} = recv(), - wait_for_master(Config), - disconnect(Config). - -roster_remove_master(Config) -> - MyJID = my_jid(Config), - Peer = ?config(slave, Config), - LPeer = jlib:jid_remove_resource(Peer), - Groups = [<<"A">>, <<"B">>], - wait_for_slave(Config), - send(Config, #presence{}), - ?recv2(#presence{from = MyJID, type = undefined}, - #presence{from = Peer, type = undefined}), - %% The peer removed us from its roster. - {Push1, Push2, _, _, _} = - ?recv5( - %% TODO: I guess this can be optimized, we don't need - %% to send transient roster push with subscription = 'to'. - #iq{type = set, - sub_els = - [#roster{items = [#roster_item{ - jid = LPeer, - subscription = to}]}]}, - #iq{type = set, - sub_els = - [#roster{items = [#roster_item{ - jid = LPeer, - subscription = none}]}]}, - #presence{type = unsubscribe, from = LPeer}, - #presence{type = unsubscribed, from = LPeer}, - #presence{type = unavailable, from = Peer}), - send(Config, make_iq_result(Push1)), - send(Config, make_iq_result(Push2)), - #iq{sub_els = [#roster{items = [#roster_item{groups = G1}]}]} = Push1, - #iq{sub_els = [#roster{items = [#roster_item{groups = G2}]}]} = Push2, - Groups = lists:sort(G1), Groups = lists:sort(G2), - disconnect(Config). - -roster_remove_slave(Config) -> - MyJID = my_jid(Config), - Peer = ?config(master, Config), - LPeer = jlib:jid_remove_resource(Peer), - send(Config, #presence{}), - #presence{from = MyJID, type = undefined} = recv(), - wait_for_master(Config), - #presence{from = Peer, type = undefined} = recv(), - %% Remove the peer from roster. - Item = #roster_item{jid = LPeer, subscription = remove}, - I = send(Config, #iq{type = set, sub_els = [#roster{items = [Item]}]}), - {Push, _, _} = ?recv3( - #iq{type = set, - sub_els = - [#roster{items = [#roster_item{ - jid = LPeer, - subscription = remove}]}]}, - #iq{type = result, id = I, sub_els = []}, - #presence{type = unavailable, from = Peer}), - send(Config, make_iq_result(Push)), - disconnect(Config). - -proxy65_master(Config) -> - Proxy = proxy_jid(Config), - MyJID = my_jid(Config), - Peer = ?config(slave, Config), - wait_for_slave(Config), - send(Config, #presence{}), - #presence{from = MyJID, type = undefined} = recv(), - true = is_feature_advertised(Config, ?NS_BYTESTREAMS, Proxy), - #iq{type = result, sub_els = [#bytestreams{hosts = [StreamHost]}]} = - send_recv( - Config, - #iq{type = get, sub_els = [#bytestreams{}], to = Proxy}), - SID = randoms:get_string(), - Data = crypto:rand_bytes(1024), - put_event(Config, {StreamHost, SID, Data}), - Socks5 = socks5_connect(StreamHost, {SID, MyJID, Peer}), - wait_for_slave(Config), - #iq{type = result, sub_els = []} = - send_recv(Config, - #iq{type = set, to = Proxy, - sub_els = [#bytestreams{activate = Peer, sid = SID}]}), - socks5_send(Socks5, Data), - %%#presence{type = unavailable, from = Peer} = recv(), - disconnect(Config). - -proxy65_slave(Config) -> - MyJID = my_jid(Config), - Peer = ?config(master, Config), - send(Config, #presence{}), - #presence{from = MyJID, type = undefined} = recv(), - wait_for_master(Config), - {StreamHost, SID, Data} = get_event(Config), - Socks5 = socks5_connect(StreamHost, {SID, Peer, MyJID}), - wait_for_master(Config), - socks5_recv(Socks5, Data), - disconnect(Config). - -muc_master(Config) -> - MyJID = my_jid(Config), - PeerJID = ?config(slave, Config), - PeerBareJID = jlib:jid_remove_resource(PeerJID), - PeerJIDStr = jlib:jid_to_string(PeerJID), - MUC = muc_jid(Config), - Room = muc_room_jid(Config), - MyNick = ?config(master_nick, Config), - MyNickJID = jlib:jid_replace_resource(Room, MyNick), - PeerNick = ?config(slave_nick, Config), - PeerNickJID = jlib:jid_replace_resource(Room, PeerNick), - Subject = ?config(room_subject, Config), - Localhost = jlib:make_jid(<<"">>, <<"localhost">>, <<"">>), - true = is_feature_advertised(Config, ?NS_MUC, MUC), - %% Joining - send(Config, #presence{to = MyNickJID, sub_els = [#muc{}]}), - %% As per XEP-0045 we MUST receive stanzas in the following order: - %% 1. In-room presence from other occupants - %% 2. In-room presence from the joining entity itself (so-called "self-presence") - %% 3. Room history (if any) - %% 4. The room subject - %% 5. Live messages, presence updates, new user joins, etc. - %% As this is the newly created room, we receive only the 2nd stanza. - #presence{ - from = MyNickJID, - sub_els = [#vcard_xupdate{}, - #muc_user{ - status_codes = Codes, - items = [#muc_item{role = moderator, - jid = MyJID, - affiliation = owner}]}]} = recv(), - %% 110 -> Inform user that presence refers to itself - %% 201 -> Inform user that a new room has been created - [110, 201] = lists:sort(Codes), - %% Request the configuration - #iq{type = result, sub_els = [#muc_owner{config = #xdata{} = RoomCfg}]} = - send_recv(Config, #iq{type = get, sub_els = [#muc_owner{}], - to = Room}), - NewFields = - lists:flatmap( - fun(#xdata_field{var = Var, values = OrigVals}) -> - Vals = case Var of - <<"FORM_TYPE">> -> - OrigVals; - <<"muc#roomconfig_roomname">> -> - [<<"Test room">>]; - <<"muc#roomconfig_roomdesc">> -> - [<<"Trying to break the server">>]; - <<"muc#roomconfig_persistentroom">> -> - [<<"1">>]; - <<"members_by_default">> -> - [<<"0">>]; - <<"muc#roomconfig_allowvoicerequests">> -> - [<<"1">>]; - <<"public_list">> -> - [<<"1">>]; - <<"muc#roomconfig_publicroom">> -> - [<<"1">>]; - _ -> - [] - end, - if Vals /= [] -> - [#xdata_field{values = Vals, var = Var}]; - true -> - [] - end - end, RoomCfg#xdata.fields), - NewRoomCfg = #xdata{type = submit, fields = NewFields}, - %% BUG: We should not receive any sub_els! - #iq{type = result, sub_els = [_|_]} = - send_recv(Config, #iq{type = set, to = Room, - sub_els = [#muc_owner{config = NewRoomCfg}]}), - %% Set subject - send(Config, #message{to = Room, type = groupchat, - body = [#text{data = Subject}]}), - #message{from = MyNickJID, type = groupchat, - body = [#text{data = Subject}]} = recv(), - %% Sending messages (and thus, populating history for our peer) - lists:foreach( - fun(N) -> - Text = #text{data = jlib:integer_to_binary(N)}, - I = send(Config, #message{to = Room, body = [Text], - type = groupchat}), - #message{from = MyNickJID, id = I, - type = groupchat, - body = [Text]} = recv() - end, lists:seq(1, 5)), - %% Inviting the peer - send(Config, #message{to = Room, type = normal, - sub_els = - [#muc_user{ - invites = - [#muc_invite{to = PeerJID}]}]}), - %% Peer is joining - #presence{from = PeerNickJID, - sub_els = [#vcard_xupdate{}, - #muc_user{ - items = [#muc_item{role = visitor, - jid = PeerJID, - affiliation = none}]}]} = recv(), - %% Receiving a voice request - #message{from = Room, - sub_els = [#xdata{type = form, - instructions = [_], - fields = VoiceReqFs}]} = recv(), - %% Approving the voice request - ReplyVoiceReqFs = - lists:map( - fun(#xdata_field{var = Var, values = OrigVals}) -> - Vals = case {Var, OrigVals} of - {<<"FORM_TYPE">>, - [<<"http://jabber.org/protocol/muc#request">>]} -> - OrigVals; - {<<"muc#role">>, [<<"participant">>]} -> - [<<"participant">>]; - {<<"muc#jid">>, [PeerJIDStr]} -> - [PeerJIDStr]; - {<<"muc#roomnick">>, [PeerNick]} -> - [PeerNick]; - {<<"muc#request_allow">>, [<<"0">>]} -> - [<<"1">>] - end, - #xdata_field{values = Vals, var = Var} - end, VoiceReqFs), - send(Config, #message{to = Room, - sub_els = [#xdata{type = submit, - fields = ReplyVoiceReqFs}]}), - %% Peer is becoming a participant - #presence{from = PeerNickJID, - sub_els = [#vcard_xupdate{}, - #muc_user{ - items = [#muc_item{role = participant, - jid = PeerJID, - affiliation = none}]}]} = recv(), - %% Receive private message from the peer - #message{from = PeerNickJID, body = [#text{data = Subject}]} = recv(), - %% Granting membership to the peer and localhost server - I1 = send(Config, - #iq{type = set, to = Room, - sub_els = - [#muc_admin{ - items = [#muc_item{jid = Localhost, - affiliation = member}, - #muc_item{nick = PeerNick, - jid = PeerBareJID, - affiliation = member}]}]}), - %% Peer became a member - #presence{from = PeerNickJID, - sub_els = [#vcard_xupdate{}, - #muc_user{ - items = [#muc_item{affiliation = member, - jid = PeerJID, - role = participant}]}]} = recv(), - %% BUG: We should not receive any sub_els! - #iq{type = result, id = I1, sub_els = [_|_]} = recv(), - %% Receive groupchat message from the peer - #message{type = groupchat, from = PeerNickJID, - body = [#text{data = Subject}]} = recv(), - %% Kick the peer - I2 = send(Config, - #iq{type = set, to = Room, - sub_els = [#muc_admin{ - items = [#muc_item{nick = PeerNick, - role = none}]}]}), - %% Got notification the peer is kicked - %% 307 -> Inform user that he or she has been kicked from the room - #presence{from = PeerNickJID, type = unavailable, - sub_els = [#muc_user{ - status_codes = [307], - items = [#muc_item{affiliation = member, - jid = PeerJID, - role = none}]}]} = recv(), - %% BUG: We should not receive any sub_els! - #iq{type = result, id = I2, sub_els = [_|_]} = recv(), - %% Destroying the room - I3 = send(Config, - #iq{type = set, to = Room, - sub_els = [#muc_owner{ - destroy = #muc_owner_destroy{ - reason = Subject}}]}), - %% Kicked off - #presence{from = MyNickJID, type = unavailable, - sub_els = [#muc_user{items = [#muc_item{role = none, - affiliation = none}], - destroy = #muc_user_destroy{ - reason = Subject}}]} = recv(), - %% BUG: We should not receive any sub_els! - #iq{type = result, id = I3, sub_els = [_|_]} = recv(), - disconnect(Config). - -muc_slave(Config) -> - MyJID = my_jid(Config), - MyBareJID = jlib:jid_remove_resource(MyJID), - PeerJID = ?config(master, Config), - MUC = muc_jid(Config), - Room = muc_room_jid(Config), - MyNick = ?config(slave_nick, Config), - MyNickJID = jlib:jid_replace_resource(Room, MyNick), - PeerNick = ?config(master_nick, Config), - PeerNickJID = jlib:jid_replace_resource(Room, PeerNick), - Subject = ?config(room_subject, Config), - Localhost = jlib:make_jid(<<"">>, <<"localhost">>, <<"">>), - %% Receive an invite from the peer - #message{from = Room, type = normal, - sub_els = - [#muc_user{invites = - [#muc_invite{from = PeerJID}]}]} = recv(), - %% But before joining we discover the MUC service first - %% to check if the room is in the disco list - #iq{type = result, - sub_els = [#disco_items{items = [#disco_item{jid = Room}]}]} = - send_recv(Config, #iq{type = get, to = MUC, - sub_els = [#disco_items{}]}), - %% Now check if the peer is in the room. We check this via disco#items - #iq{type = result, - sub_els = [#disco_items{items = [#disco_item{jid = PeerNickJID, - name = PeerNick}]}]} = - send_recv(Config, #iq{type = get, to = Room, - sub_els = [#disco_items{}]}), - %% Now joining - send(Config, #presence{to = MyNickJID, sub_els = [#muc{}]}), - %% First presence is from the participant, i.e. from the peer - #presence{ - from = PeerNickJID, - sub_els = [#vcard_xupdate{}, - #muc_user{ - status_codes = [], - items = [#muc_item{role = moderator, - affiliation = owner}]}]} = recv(), - %% The next is the self-presence (code 110 means it) - #presence{ - from = MyNickJID, - sub_els = [#vcard_xupdate{}, - #muc_user{ - status_codes = [110], - items = [#muc_item{role = visitor, - affiliation = none}]}]} = recv(), - %% Receive the room subject - #message{from = PeerNickJID, type = groupchat, - body = [#text{data = Subject}], - sub_els = [#delay{}, #legacy_delay{}]} = recv(), - %% Receive MUC history - lists:foreach( - fun(N) -> - Text = #text{data = jlib:integer_to_binary(N)}, - #message{from = PeerNickJID, - type = groupchat, - body = [Text], - sub_els = [#delay{}, #legacy_delay{}]} = recv() - end, lists:seq(1, 5)), - %% Sending a voice request - VoiceReq = #xdata{ - type = submit, - fields = - [#xdata_field{ - var = <<"FORM_TYPE">>, - values = [<<"http://jabber.org/protocol/muc#request">>]}, - #xdata_field{ - var = <<"muc#role">>, - type = 'text-single', - values = [<<"participant">>]}]}, - send(Config, #message{to = Room, sub_els = [VoiceReq]}), - %% Becoming a participant - #presence{from = MyNickJID, - sub_els = [#vcard_xupdate{}, - #muc_user{ - items = [#muc_item{role = participant, - affiliation = none}]}]} = recv(), - %% Sending private message to the peer - send(Config, #message{to = PeerNickJID, - body = [#text{data = Subject}]}), - %% Becoming a member - #presence{from = MyNickJID, - sub_els = [#vcard_xupdate{}, - #muc_user{ - items = [#muc_item{role = participant, - affiliation = member}]}]} = recv(), - %% Retrieving a member list - #iq{type = result, sub_els = [#muc_admin{items = MemberList}]} = - send_recv(Config, - #iq{type = get, to = Room, - sub_els = - [#muc_admin{items = [#muc_item{affiliation = member}]}]}), - [#muc_item{affiliation = member, - jid = Localhost}, - #muc_item{affiliation = member, - jid = MyBareJID}] = lists:keysort(#muc_item.jid, MemberList), - %% Sending groupchat message - send(Config, #message{to = Room, type = groupchat, - body = [#text{data = Subject}]}), - %% Receive this message back - #message{type = groupchat, from = MyNickJID, - body = [#text{data = Subject}]} = recv(), - %% We're kicked off - %% 307 -> Inform user that he or she has been kicked from the room - #presence{from = MyNickJID, type = unavailable, - sub_els = [#muc_user{ - status_codes = [307], - items = [#muc_item{affiliation = member, - role = none}]}]} = recv(), - disconnect(Config). - -muc_register_nick(Config, MUC, PrevNick, Nick) -> - {Registered, PrevNickVals} = if PrevNick /= <<"">> -> - {true, [PrevNick]}; - true -> - {false, []} - end, - %% Request register form - #iq{type = result, - sub_els = [#register{registered = Registered, - xdata = #xdata{type = form, - fields = FsWithoutNick}}]} = - send_recv(Config, #iq{type = get, to = MUC, - sub_els = [#register{}]}), - %% Check if 'nick' field presents - #xdata_field{type = 'text-single', - var = <<"nick">>, - values = PrevNickVals} = - lists:keyfind(<<"nick">>, #xdata_field.var, FsWithoutNick), - X = #xdata{type = submit, - fields = [#xdata_field{var = <<"nick">>, values = [Nick]}]}, - %% Submitting form - #iq{type = result, sub_els = [_|_]} = - send_recv(Config, #iq{type = set, to = MUC, - sub_els = [#register{xdata = X}]}), - %% Check if the nick was registered - #iq{type = result, - sub_els = [#register{registered = true, - xdata = #xdata{type = form, - fields = FsWithNick}}]} = - send_recv(Config, #iq{type = get, to = MUC, - sub_els = [#register{}]}), - #xdata_field{type = 'text-single', var = <<"nick">>, - values = [Nick]} = - lists:keyfind(<<"nick">>, #xdata_field.var, FsWithNick). - -muc_register_master(Config) -> - MUC = muc_jid(Config), - %% Register nick "master1" - muc_register_nick(Config, MUC, <<"">>, <<"master1">>), - %% Unregister nick "master1" via jabber:register - #iq{type = result, sub_els = [_|_]} = - send_recv(Config, #iq{type = set, to = MUC, - sub_els = [#register{remove = true}]}), - %% Register nick "master2" - muc_register_nick(Config, MUC, <<"">>, <<"master2">>), - %% Now register nick "master" - muc_register_nick(Config, MUC, <<"master2">>, <<"master">>), - disconnect(Config). - -muc_register_slave(Config) -> - MUC = muc_jid(Config), - %% Trying to register occupied nick "master" - X = #xdata{type = submit, - fields = [#xdata_field{var = <<"nick">>, - values = [<<"master">>]}]}, - #iq{type = error} = - send_recv(Config, #iq{type = set, to = MUC, - sub_els = [#register{xdata = X}]}), - disconnect(Config). - -announce_master(Config) -> - MyJID = my_jid(Config), - ServerJID = server_jid(Config), - MotdJID = jlib:jid_replace_resource(ServerJID, <<"announce/motd">>), - MotdText = #text{data = <<"motd">>}, - send(Config, #presence{}), - #presence{from = MyJID} = recv(), - %% Set message of the day - send(Config, #message{to = MotdJID, body = [MotdText]}), - %% Receive this message back - #message{from = ServerJID, body = [MotdText]} = recv(), - disconnect(Config). - -announce_slave(Config) -> - MyJID = my_jid(Config), - ServerJID = server_jid(Config), - MotdDelJID = jlib:jid_replace_resource(ServerJID, <<"announce/motd/delete">>), - MotdText = #text{data = <<"motd">>}, - send(Config, #presence{}), - ?recv2(#presence{from = MyJID}, - #message{from = ServerJID, body = [MotdText]}), - %% Delete message of the day - send(Config, #message{to = MotdDelJID}), - disconnect(Config). - -offline_master(Config) -> - Peer = ?config(slave, Config), - LPeer = jlib:jid_remove_resource(Peer), - send(Config, #message{to = LPeer, - body = [#text{data = <<"body">>}], - subject = [#text{data = <<"subject">>}]}), - disconnect(Config). - -offline_slave(Config) -> - Peer = ?config(master, Config), - send(Config, #presence{}), - {_, #message{sub_els = SubEls}} = - ?recv2(#presence{}, - #message{from = Peer, - body = [#text{data = <<"body">>}], - subject = [#text{data = <<"subject">>}]}), - true = lists:keymember(delay, 1, SubEls), - true = lists:keymember(legacy_delay, 1, SubEls), - disconnect(Config). - -carbons_master(Config) -> - MyJID = my_jid(Config), - MyBareJID = jlib:jid_remove_resource(MyJID), - Peer = ?config(slave, Config), - Txt = #text{data = <<"body">>}, - true = is_feature_advertised(Config, ?NS_CARBONS_2), - send(Config, #presence{priority = 10}), - #presence{from = MyJID} = recv(), - wait_for_slave(Config), - #presence{from = Peer} = recv(), - %% Enable carbons - #iq{type = result, sub_els = []} = - send_recv(Config, - #iq{type = set, - sub_els = [#carbons_enable{}]}), - %% Send a message to bare and full JID - send(Config, #message{to = MyBareJID, type = chat, body = [Txt]}), - send(Config, #message{to = MyJID, type = chat, body = [Txt]}), - send(Config, #message{to = MyBareJID, type = chat, body = [Txt], - sub_els = [#carbons_private{}]}), - send(Config, #message{to = MyJID, type = chat, body = [Txt], - sub_els = [#carbons_private{}]}), - %% Receive the messages back - ?recv4(#message{from = MyJID, to = MyBareJID, type = chat, - body = [Txt], sub_els = []}, - #message{from = MyJID, to = MyJID, type = chat, - body = [Txt], sub_els = []}, - #message{from = MyJID, to = MyBareJID, type = chat, - body = [Txt], sub_els = [#carbons_private{}]}, - #message{from = MyJID, to = MyJID, type = chat, - body = [Txt], sub_els = [#carbons_private{}]}), - %% Disable carbons - #iq{type = result, sub_els = []} = - send_recv(Config, - #iq{type = set, - sub_els = [#carbons_disable{}]}), - wait_for_slave(Config), - %% Repeat the same and leave - send(Config, #message{to = MyBareJID, type = chat, body = [Txt]}), - send(Config, #message{to = MyJID, type = chat, body = [Txt]}), - send(Config, #message{to = MyBareJID, type = chat, body = [Txt], - sub_els = [#carbons_private{}]}), - send(Config, #message{to = MyJID, type = chat, body = [Txt], - sub_els = [#carbons_private{}]}), - ?recv4(#message{from = MyJID, to = MyBareJID, type = chat, - body = [Txt], sub_els = []}, - #message{from = MyJID, to = MyJID, type = chat, - body = [Txt], sub_els = []}, - #message{from = MyJID, to = MyBareJID, type = chat, - body = [Txt], sub_els = [#carbons_private{}]}, - #message{from = MyJID, to = MyJID, type = chat, - body = [Txt], sub_els = [#carbons_private{}]}), - disconnect(Config). - -carbons_slave(Config) -> - MyJID = my_jid(Config), - MyBareJID = jlib:jid_remove_resource(MyJID), - Peer = ?config(master, Config), - Txt = #text{data = <<"body">>}, - wait_for_master(Config), - send(Config, #presence{priority = 5}), - ?recv2(#presence{from = MyJID}, #presence{from = Peer}), - %% Enable carbons - #iq{type = result, sub_els = []} = - send_recv(Config, - #iq{type = set, - sub_els = [#carbons_enable{}]}), - %% Receive messages sent by the peer - ?recv4( - #message{from = MyBareJID, to = MyJID, type = chat, - sub_els = - [#carbons_sent{ - forwarded = #forwarded{ - sub_els = - [#message{from = Peer, - to = MyBareJID, - type = chat, - body = [Txt]}]}}]}, - #message{from = MyBareJID, to = MyJID, type = chat, - sub_els = - [#carbons_sent{ - forwarded = #forwarded{ - sub_els = - [#message{from = Peer, - to = Peer, - type = chat, - body = [Txt]}]}}]}, - #message{from = MyBareJID, to = MyJID, type = chat, - sub_els = - [#carbons_received{ - forwarded = #forwarded{ - sub_els = - [#message{from = Peer, - to = MyBareJID, - type = chat, - body = [Txt]}]}}]}, - #message{from = MyBareJID, to = MyJID, type = chat, - sub_els = - [#carbons_received{ - forwarded = #forwarded{ - sub_els = - [#message{from = Peer, - to = Peer, - type = chat, - body = [Txt]}]}}]}), - %% Disable carbons - #iq{type = result, sub_els = []} = - send_recv(Config, - #iq{type = set, - sub_els = [#carbons_disable{}]}), - wait_for_master(Config), - %% Now we should receive nothing but presence unavailable from the peer - #presence{from = Peer, type = unavailable} = recv(), - disconnect(Config). - -client_state_master(Config) -> - Peer = ?config(slave, Config), - Presence = #presence{to = Peer}, - Message = #message{to = Peer, thread = <<"1">>, - sub_els = [#chatstate{type = active}]}, - wait_for_slave(Config), - %% Should be queued (but see below): - send(Config, Presence), - %% Should be sent immediately, together with the previous presence: - send(Config, Message#message{body = [#text{data = <<"body">>}]}), - %% Should be dropped: - send(Config, Message), - %% Should be queued (but see below): - send(Config, Presence), - %% Should replace the previous presence in the queue: - send(Config, Presence#presence{type = unavailable}), - wait_for_slave(Config), - %% Should be sent immediately, as the client is active again. - send(Config, Message), - disconnect(Config). - -client_state_slave(Config) -> - true = ?config(csi, Config), - Peer = ?config(master, Config), - send(Config, #csi{type = inactive}), - wait_for_master(Config), - #presence{from = Peer, sub_els = [#vcard_xupdate{}|_]} = recv(), - #message{from = Peer, thread = <<"1">>, sub_els = [#chatstate{type = active}], - body = [#text{data = <<"body">>}]} = recv(), - wait_for_master(Config), - send(Config, #csi{type = active}), - ?recv2(#presence{from = Peer, type = unavailable, - sub_els = [#delay{}, #legacy_delay{}]}, - #message{from = Peer, thread = <<"1">>, - sub_els = [#chatstate{type = active}]}), - disconnect(Config). - %%%=================================================================== %%% Aux functions %%%=================================================================== bookmark_conference() -> #bookmark_conference{name = <<"Some name">>, autojoin = true, - jid = jlib:make_jid( + jid = jid:make( <<"some">>, <<"some.conference.org">>, <<>>)}. -socks5_connect(#streamhost{host = Host, port = Port}, - {SID, JID1, JID2}) -> - Hash = p1_sha:sha([SID, jlib:jid_to_string(JID1), jlib:jid_to_string(JID2)]), - {ok, Sock} = gen_tcp:connect(binary_to_list(Host), Port, - [binary, {active, false}]), - Init = <>, - InitAck = <>, - Req = <>, - Resp = <>, - gen_tcp:send(Sock, Init), - {ok, InitAck} = gen_tcp:recv(Sock, size(InitAck)), - gen_tcp:send(Sock, Req), - {ok, Resp} = gen_tcp:recv(Sock, size(Resp)), - Sock. +'$handle_undefined_function'(F, [Config]) when is_list(Config) -> + case re:split(atom_to_list(F), "_", [{return, list}, {parts, 2}]) of + [M, T] -> + Module = list_to_atom(M ++ "_tests"), + Function = list_to_atom(T), + case erlang:function_exported(Module, Function, 1) of + true -> + Module:Function(Config); + false -> + erlang:error({undef, F}) + end; + _ -> + erlang:error({undef, F}) + end; +'$handle_undefined_function'(_, _) -> + erlang:error(undef). -socks5_send(Sock, Data) -> - ok = gen_tcp:send(Sock, Data). - -socks5_recv(Sock, Data) -> - {ok, Data} = gen_tcp:recv(Sock, size(Data)). %%%=================================================================== %%% SQL stuff %%%=================================================================== -create_sql_tables(Type, BaseDir) -> +update_sql(Host, Config) -> + case ?config(update_sql, Config) of + true -> + mod_admin_update_sql:update_sql(Host); + false -> ok + end. + +schema_suffix(Config) -> + case ejabberd_sql:use_new_schema() of + true -> + case ?config(update_sql, Config) of + true -> ".sql"; + _ -> ".new.sql" + end; + _ -> ".sql" + end. + +clear_sql_tables(sqlite, _Config) -> + ok; +clear_sql_tables(Type, Config) -> + BaseDir = ?config(base_dir, Config), {VHost, File} = case Type of - mysql -> - {?MYSQL_VHOST, "mysql.sql"}; - pgsql -> - {?PGSQL_VHOST, "pg.sql"} + mysql -> {?MYSQL_VHOST, "mysql" ++ schema_suffix(Config)}; + mssql -> {?MSSQL_VHOST, "mssql" ++ schema_suffix(Config)}; + pgsql -> {?PGSQL_VHOST, "pg" ++ schema_suffix(Config)} end, SQLFile = filename:join([BaseDir, "sql", File]), CreationQueries = read_sql_queries(SQLFile), - DropTableQueries = drop_table_queries(CreationQueries), - case ejabberd_odbc:sql_transaction( - VHost, DropTableQueries ++ CreationQueries) of + ClearTableQueries = clear_table_queries(CreationQueries), + case ejabberd_sql:sql_transaction( + VHost, ClearTableQueries) of {atomic, ok} -> ok; Err -> - ct:fail({failed_to_create_sql_tables, Type, Err}) + ct:fail({failed_to_clear_sql_tables, Type, Err}) end. read_sql_queries(File) -> @@ -1657,12 +1126,22 @@ read_sql_queries(File) -> ct:fail({open_file_failed, File, Err}) end. -drop_table_queries(Queries) -> +clear_table_queries(Queries) -> lists:foldl( fun(Query, Acc) -> case split(str:to_lower(Query)) of [<<"create">>, <<"table">>, Table|_] -> - [<<"DROP TABLE IF EXISTS ", Table/binary, ";">>|Acc]; + GlobalRamTables = [<<"bosh">>, + <<"oauth_client">>, + <<"oauth_token">>, + <<"proxy65">>, + <<"route">>], + case lists:member(Table, GlobalRamTables) of + true -> + Acc; + false -> + [<<"DELETE FROM ", Table/binary, ";">>|Acc] + end; _ -> Acc end @@ -1702,16 +1181,3 @@ split(Data) -> (_) -> true end, re:split(Data, <<"\s">>)). - -clear_riak_tables(Config) -> - User = ?config(user, Config), - Server = ?config(server, Config), - Room = muc_room_jid(Config), - {URoom, SRoom, _} = jlib:jid_tolower(Room), - ejabberd_auth:remove_user(User, Server), - ejabberd_auth:remove_user(<<"test_slave">>, Server), - ejabberd_auth:remove_user(<<"test_master">>, Server), - mod_muc:forget_room(Server, URoom, SRoom), - ejabberd_riak:delete(muc_registered, {{<<"test_slave">>, Server}, SRoom}), - ejabberd_riak:delete(muc_registered, {{<<"test_master">>, Server}, SRoom}), - Config. diff --git a/test/ejabberd_SUITE_data/ca.key b/test/ejabberd_SUITE_data/ca.key new file mode 100644 index 000000000..cc59087c6 --- /dev/null +++ b/test/ejabberd_SUITE_data/ca.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEA5WxWkSLK3iadpy2v57FVc7pK307aWHQqirg+q5PreRB1nLsr +oW+TaXfgB5B1/GTFStnSbmczqpkuWyi4hIB9ZzM62kWuOpZPx0+w5hHx73VWxpsr +YgaBkoQsn8BF84PfmRDNG76TOacuoLzeqnN1deWDgOGQ9a7ZesOQLuZBPF6oysfK +OpAR035fQM6XaaR8Ti6Ko53DkCzw8MiySrAHJOkgxhmX11+hUMjldWCEiRs1VL/g +rolajqe3B+wu0UdonZ/QUeVk4KRnDIAIJSKw8XmgcB4oI5cUrnDnOmv2784RgJZs +ZxuGF0e5mz5v8BqXqKiFwH/CD1inUpMA89MATQIDAQABAoIBAQCc2O1x+ixhplrg +AZ8iMp2uKe2oL5udH4Y6Im5OFSnGMdeGmHviuYo5b8gMw9m1/RrY6oQwEIRFHMaR +cgx8IfAaDu8sbLkJutu98qCJGjmiMUFrNIh7UuFgztZHPUdVjZHfbpobXrX+k2qQ +X6+HLrpeKNQ3136oSKrMgEjhl2+AGhe/uqFGw+nwCNzY3BnAJOWS8pipgV0IQ1Eo +AdJU8SoW/LToo5RTZNodPhyqLl10D1tRJ8WSAndAkvaoMRHJasYQDrmz449+QiTZ +SLRf9n/TtcKJQTaqwskV/dOdygeBUKnZQhq663TKgTWcTxF1dA5T3QxXv/7p+8Ow +9GxuxBjBAoGBAPRjb8OCLD8EAtxFXWRWBH5GWF3vGnDIq5FkPaue0uyDaw+TLgJE +AKV7Ik0IRRZkUdc/xix22Bg83L0ErOD2qLHgZuUvuXtiv+Dq/D2BIb5M3zQy8giA +vxdlE5O9i8aG647P+ACGOpYZ7a/K645HGxqOZpf8ZRmST5VzNY7qVxb9AoGBAPBS +4Bo66VMWf6BLd8RIK3DzOf0TWRRMCAwX9kCNTG22TX79imJHWB5lWQQam4yp4Cya +wo08DT3YcffURW9bJTF2q+JZHMqlEr8q9kcjIJu8uQ7X9N4JsUfCcWaBSHHBNgx/ +coved2h02NFcJmV3HuF2l/miah6p9rPJmGnvG1eRAoGBAKIEqju7OQot5peRhPDX +9fKhQERGGAldgCDLi/cTPFKAbaHNuVrXKnaKw5q+OM83gupo5UDlKS4oa08Eongi +DoSeeJjIovch6IN8Re2ghnZbED7S55KriARChlAUAW6EU/ZB+fCfDIgmeGVq6e9R +RK6+aVWphn0Feq1hy8gLo+EhAoGBAI/hvmRV4v2o2a5ZoJH2d3O/W3eGTu3U+3hq +HDfXoOuKmukt2N0wQ7SnDt1jJL/ZsOpjmZk/W9osLUeoYg3ibuknWI9CtPcqT4f+ +q8Y5ZLt5CP63EtagzO/enVA2lO3uNHLVFvpgrfLvCiSGXEKhR+7KtwBxWcGUFqzb +RJIf4qnRAoGAR+c24S4MtVuw6+UVKyLxhjB6iDTvJijdIr/+ofbeM5TQHGsYzZzP +HHNdZ5ECz5eDnaNzvAs4CCuy+75cqlUhAgzrLlCj+dJN/fYEJsD6AjWdto3Zorig +XBFM8FtXP7VRjFNwCCbdhrFOcmgbAtz3ReS6Ts6drSw7OgyeDajam1U= +-----END RSA PRIVATE KEY----- diff --git a/test/ejabberd_SUITE_data/ca.pem b/test/ejabberd_SUITE_data/ca.pem new file mode 100644 index 000000000..089238d62 --- /dev/null +++ b/test/ejabberd_SUITE_data/ca.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDazCCAlOgAwIBAgIUUynLQejEU8NykU/YNfL1dyC7vxcwDQYJKoZIhvcNAQEL +BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0xODA5MjQxMzE4MjRaFw00NjAy +MDkxMzE4MjRaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw +HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQDlbFaRIsreJp2nLa/nsVVzukrfTtpYdCqKuD6rk+t5 +EHWcuyuhb5Npd+AHkHX8ZMVK2dJuZzOqmS5bKLiEgH1nMzraRa46lk/HT7DmEfHv +dVbGmytiBoGShCyfwEXzg9+ZEM0bvpM5py6gvN6qc3V15YOA4ZD1rtl6w5Au5kE8 +XqjKx8o6kBHTfl9AzpdppHxOLoqjncOQLPDwyLJKsAck6SDGGZfXX6FQyOV1YISJ +GzVUv+CuiVqOp7cH7C7RR2idn9BR5WTgpGcMgAglIrDxeaBwHigjlxSucOc6a/bv +zhGAlmxnG4YXR7mbPm/wGpeoqIXAf8IPWKdSkwDz0wBNAgMBAAGjUzBRMB0GA1Ud +DgQWBBQGU3AZGF8ahVEnpfHB5ETAW5uIBzAfBgNVHSMEGDAWgBQGU3AZGF8ahVEn +pfHB5ETAW5uIBzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAK +jIEjOh7k1xaEMBygQob9XGLmyLgmw1GEvWx7wiDpcdHXuAH9mLC4NPNSjOXPNK2V +u4dh1KHy1z+dHJbt2apXejxtiwlcMWmPDF2EtKjstUN+KXecG7vjReArs71T9ir/ +7Xfwfg6TKD3H7efYFJaBb7d/lyneNP1Ive/rkRsGqCglkoX4ajcAm7MLkkFD8TCP +NqFc7SdA4OsaeYiUmjnyTUDbKgG0bDAXymhsUzd6Pa9kKQx+dH4GPiCoNoypCXD7 +RZSlETNGZ0vdxCjpdvT4eYxSIalG4rAU85turqPF/ovdzUzb72Sta0L5Hrf0rLa/ +um3+Xel8qI+p3kErAG2v +-----END CERTIFICATE----- diff --git a/test/ejabberd_SUITE_data/cert.pem b/test/ejabberd_SUITE_data/cert.pem index d6429dd68..7b82b3ca7 100644 --- a/test/ejabberd_SUITE_data/cert.pem +++ b/test/ejabberd_SUITE_data/cert.pem @@ -1,51 +1,54 @@ -----BEGIN CERTIFICATE----- -MIIDETCCAcmgAwIBAgIEUbsa1zANBgkqhkiG9w0BAQsFADAAMCIYDzIwMTMwNjE0 -MTMzMDAwWhgPMjAyMzA2MTIxMzMwMDhaMAAwggFSMA0GCSqGSIb3DQEBAQUAA4IB -PwAwggE6AoIBMQCXdtt12OFu2j8tlF4x2Da/kbxyMxFnovJXHNzpx7CE/cGthAR5 -w7Cl92pECog2/d6ryIcjqzzCyCeOVQxIaE3Qz8z6+5UjKh3V/j6CKxcK5g1ER7Qe -UgpE00ahHzvOpVANtrkYPGC0SFuTFL+PaylH4HW1xBSc1HD5/w7S1k1pDTz9x8ZC -Z7JOb6NoYsz+rnmWYY2HOG6pyAyQBapIjgzCamgTStA6jTSgoXmCri/dZnJpqjZc -V6AW7feNmMElhPvL30Cb3QB+9ODjN3pDXRR+Jqilu8ZSrpcvcFHOyKt943id1oC+ -Qu8orA0/kVInX7IuV//TciKzcH5FWz75Kb7hORPzH8M2DQcIKqKKVIwNVeJLKmcG -RcUGsgTaz2j0JTa6YLJoczuasDWgRMT0goQpAgMBAAGjLzAtMAwGA1UdEwEB/wQC -MAAwHQYDVR0OBBYEFBW6Si5OY8NPLagdth/JD8R18WMnMA0GCSqGSIb3DQEBCwUA -A4IBMQAPiHxamUumu203pSVwvpWkpgKKOC2EswyFWQbNC6DWQ3LUkiR7MCiFViYt -yiIyEh9wtfymWNF9uwaR2nVrJD5mK9Rt7xDiaT5ZOgNjLzmLeYqSlG41mCU1bmqg -VbxmI1hvPvv3gQ/+WM0lBC6gPGJbVbzlWAIQ1cmevtL1KqOMveZl3VBPxDJD/K9c -Rbrtx2nBKFDEl6hBljz6gsn4o8pxH3CO7qWpgY/MLwqQzEtTKYnaS9ecywNvj+/F -ZE4SMoekw6AGRyE14/3i2xW6EmIpxVU4O6ahEFq6r6ZFbdtWnog5vT0y+/tRMgXp -kCw8puxT2VsYNeJNOybW1IcyN5yluS/FY8iJokdL1JwvhVBVIWaim+T6iwrva7wC -q1E9Nj30F8UbEkbkNqOdC3UlHQW4 +MIIEjTCCA3WgAwIBAgIBATANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJBVTET +MBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQ +dHkgTHRkMB4XDTE4MDkyNDEzMTgyNFoXDTQ2MDIwOTEzMTgyNFowWTELMAkGA1UE +BhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdp +ZGdpdHMgUHR5IEx0ZDESMBAGA1UEAxMJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA1oQMN4MZ/wEf4SM7chwHZ+ymQ5Knt45VZ0jmgpnK +Fx0p+eJoNegvvwY/80NWTmcgbGnqruJiOh5AEUNDtCD5G/70oz2WHgZBZkuLsopE +a/2sDmwxvUbv1f/mD8iHcDaWUvKAy4TUHFeHDQL28HJom9E7bgYadeuhebwZcsbu +lPFePw+fWM7jLWxkMYClfsdzsBrgerbZVPnAuj77cGXZSQ6p96jOPiJ/mjOVCwWJ +tdlqwme2AC4AwKYdWzc3Ysw8lES/ubMa+lP1Eh9aI8edpHIlC5nYNLVTWa4Xw6Ct +AvqzKtNNJzwypbR3fcDXaWvvO3GY3wOHVC/wyCsL8SXc7QIDAQABo4IBcjCCAW4w +CQYDVR0TBAIwADAsBglghkgBhvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2Vy +dGlmaWNhdGUwHQYDVR0OBBYEFFvDi47v5xJKOsgQo8MP4JzY6cC/MB8GA1UdIwQY +MBaAFAZTcBkYXxqFUSel8cHkRMBbm4gHMDMGA1UdHwQsMCowKKAmoCSGImh0dHA6 +Ly9sb2NhbGhvc3Q6NTI4MC9kYXRhL2NybC5kZXIwNgYIKwYBBQUHAQEEKjAoMCYG +CCsGAQUFBzABhhpodHRwOi8vbG9jYWxob3N0OjUyODAvb2NzcDALBgNVHQ8EBAMC +BeAwJwYDVR0lBCAwHgYIKwYBBQUHAwkGCCsGAQUFBwMBBggrBgEFBQcDAjBQBgNV +HREESTBHggsqLmxvY2FsaG9zdKA4BggrBgEFBQcIBaAsDCp0ZXN0X3NpbmdsZSEj +JCVeKigpYH4rLTtfPVtde318XEBsb2NhbGhvc3QwDQYJKoZIhvcNAQELBQADggEB +AEW8qvdyBMOSjCwJ1G178xsxf8Adw/9QN2ftBGKCo1C3YtmP5CvipChq5FTrOvRz +XjoQxbKhlqEumkZQkfmLiM/DLbkFeNqGWpuy14lkyIPUknaLKNCJX++pXsJrPLGR +btWnlB0cb+pLIB/UkG8OIpW07pNOZxHdHoHInRMMs89kgsmhIpn5OamzPWK/bqTB +YjAPIdmdkYk9oxWfgjpJ4BG2PbGS6CnjA29j7vebuQ4ebVpFBMI9w77PY3NcuMK7 +ML6MV6ez/+nPpz+E4zRxsVxmVAbSaiFDW3G3efAybDeT5QW1x/oJm2SpsJNIGHcp +RecYNo9esOTG+Bg6wypg4WA= -----END CERTIFICATE----- -----BEGIN RSA PRIVATE KEY----- -MIIFegIBAAKCATEAl3bbddjhbto/LZReMdg2v5G8cjMRZ6LyVxzc6cewhP3BrYQE -ecOwpfdqRAqINv3eq8iHI6s8wsgnjlUMSGhN0M/M+vuVIyod1f4+gisXCuYNREe0 -HlIKRNNGoR87zqVQDba5GDxgtEhbkxS/j2spR+B1tcQUnNRw+f8O0tZNaQ08/cfG -QmeyTm+jaGLM/q55lmGNhzhuqcgMkAWqSI4MwmpoE0rQOo00oKF5gq4v3WZyaao2 -XFegFu33jZjBJYT7y99Am90AfvTg4zd6Q10UfiaopbvGUq6XL3BRzsirfeN4ndaA -vkLvKKwNP5FSJ1+yLlf/03Iis3B+RVs++Sm+4TkT8x/DNg0HCCqiilSMDVXiSypn -BkXFBrIE2s9o9CU2umCyaHM7mrA1oETE9IKEKQIDAQABAoIBMG8X8a4FbowFLhO7 -YD+FC9sFBMhqZpiyLrfwZqReID3bdeRUEYhSHU4OI/ZWF0Tmfh1Xjq992Koxbrn5 -7XFqd7DxybJJN0E8kfe0bJrDCjqnNBHh2d3nZLrIkGR7aT2PiSEV5bs+BdwVun0t -2bdS7UtX+l5gvJGvTFJBXtkL8GleGV822Vc5gdIAFkXpOdyPkoTXdpw4qwqBnL8/ -TXMYBIgCrXMhEawcNbgPu4iFev2idoU9vXc7ZYD7+8jWB5LJ34cNngguGrnOjLoE -9c3nZy6uYhhMWtcrSsQlrbN5MtY8w2fPH8nhfA651IxXXVxEajd24t2Csttnl7Vz -WS5c+oPaWwt67naMrYCWG3q1zWhDqZUAulZR4DzWzGP+idLS/ojCRdTZO9D1O+XP -fPi0wJECgZkAxh0rTSMCyrJ3VJqEgSPw3yAa1R9cdrTRvV4vRf13Dh8REaHtWt8W -JeT5WLXL7dOii1St1Fgjo82+4iMqx3PQ2eR/1I6dA7Uy71PaSQTCQnupca2Xx8nT -5KcrASBkDAudiKog01eC+zYrW+CbUb9AogMZLJzZinlWQ36pJVkWd9SOv25Eqcv6 -zJEmzYKpnow/m8WKNogVGpUCgZkAw7hQxs5VYVLp2XtDqRSmxfJsfsbUVo7tZnSU -wmejgeNRs7415ZuT142k7qBImrFdYzFcfh2OZnf6D/VIz4Rl7u5YRYRCha/HOGIy -wTe1huDckJ6lH/BkZ/6f9WSzXnNSNeXQY14WymU5V5qYCAdwECSf+xNuBYNwzA7o -vOxPE690w3Ox2qghzRjzsBqAMgvqSyKlBpoMckUCgZhyKgD39IL5V5qYcGqHGLUH -fzK3OdlItq5e19WaGZPv2Us2w/9JbGEQ+UAPNMQNivWSIPwC77+p9zhWjDlssnrZ -9WkMjhpBNrvhWorhpRJkyWo9jfF3OgEXNJX9kjLVFiRzysYbw8RBC1g1G9ulYfbW -5b4uDTz3JTDmuCi00v+1khGoktySlG80TzjzGKayLNPC6jTZc9XleQKBmA0STUrJ -0wf5+qZMxjsPpwfHZhmde+cACrjyBlFpjJELNpSzmnPoTRpzWlWZnN/AAsWyMUQ3 -AyCy2J+iOSeq5wfrITgbWjoFgF+yp0MiTlxgvjpmbg7RBlOvvM0t2ZDwUMhKvf00 -9n6z/f1s1MSMgp6BY7HoHUv++FSYllCv06Qz7q9zFajN29wP046qZm9xPkegW7cy -KKylAoGYTg94GOWlUTz7Pe9PDrDSFVEAi0LcDmul0ntorvEFDvU2pCRK14gyvl9O -IJKVyYcDAqA3uvT+zMAniuf8KXNUCcYeEpfzpT+e2eznhczO8hI14M5U0X0LA8P2 -vn0Y+yUWb9Ppu/dcjvaUA+qR/UTHqjAlAr3hFTKRxXFoGwwzTXCXvZGKOnzJRTpj -LpjI1RG7Weeoyx/8qDs= +MIIEpgIBAAKCAQEA1oQMN4MZ/wEf4SM7chwHZ+ymQ5Knt45VZ0jmgpnKFx0p+eJo +NegvvwY/80NWTmcgbGnqruJiOh5AEUNDtCD5G/70oz2WHgZBZkuLsopEa/2sDmwx +vUbv1f/mD8iHcDaWUvKAy4TUHFeHDQL28HJom9E7bgYadeuhebwZcsbulPFePw+f +WM7jLWxkMYClfsdzsBrgerbZVPnAuj77cGXZSQ6p96jOPiJ/mjOVCwWJtdlqwme2 +AC4AwKYdWzc3Ysw8lES/ubMa+lP1Eh9aI8edpHIlC5nYNLVTWa4Xw6CtAvqzKtNN +JzwypbR3fcDXaWvvO3GY3wOHVC/wyCsL8SXc7QIDAQABAoIBAQDUwGX1cHsJ5C2f +9ndwtsfJlHVZs0vPysR9CVpE0Q4TWoNVJ+0++abRB/vI4lHotHL90xZEmJXfGj1k +YZf2QHWQBI7Qj7Yg1Qdr0yUbz/IIQLCyJTA3jvEzBvc/VByveBQi9Aw0zOopqc1x +ZC1RT8bcMumEN11q8mVV/O4oXZAl+mQIbRRt6JIsRtoW8hpB1e2ipHItDMNpSnzA +6PqcddDyDDePgi5lMOaeV9un60A6pI/+uvmw16R1Io+DyYRnxds3HJ/ccI0Co1P1 +khA75QLdnoniYO+oQrq/wGvm+Uq1seh6iuj+SOWvCdB03vPmGYxPKMSW9AtX8xbJ +J9lboi3pAoGBAPBaiUYn9F+Zt9oJTHhAimZgs1ub5xVEFwVhYJtFBT3E1rQWRKuf +kiU1JRq7TB3MGaC4zGi2ql12KV3AqFhwLKG6sKtlo/IJhJfe3DgWmBVYBBifkgYs +mxmA6opgyjbjDEMn6RA+Jov5H267AsnaB4cCB1Jjra6GIdIoMvPghHZXAoGBAOR6 +7VC6E+YX5VJPCZiN0h0aBT+Hl4drYQKvZHp5N8RIBkvmcQHEJgsrUKdirFZEXW6y +WvepwI4C/Xl61y64/DZ7rum/gpAEPdzSkefKysHAiqkMRcIpjiRxTPJ547ZJycjP +E+jzcYfLwQvCW9ZiYl+KdYRbpqBFQC8aWqixFxRbAoGBAJQTsy79vpiHY7V4tRwA +50NboCR4UE3RvT0bWSFPzILZmk0oyvXRQYCa1Vk6uxJAhCl4sLZyk1MxURrpbs3N +jjG1itKNtAuRwZavPo1vnhLIPv3MkXIsWQHFYroOF4bpKszU8cmIAMeLm8nkfTtO +kASlQ02HC6HSEVQgYAPP9svRAoGBANiOnwKl7Bhpy8TQ/zJmMaG9uP23IeuL3l4y +KdVfsXjMH5OvLqtS5BAwFPkiMGBv2fMC/+/AKK8xrFiJEw3I7d0iK+6Hw1OHga8c +soh1kOpF+ecyp6fZxU1LSniFCU0M8UHw7Fke7RueBzKDHJK9m6oczTgPuoYsPSKo +IwfDGjIDAoGBAMJVkInntV8oDPT1WYpOAZ3Z0myCDZVBbjxx8kE4RSJIsFeNSiTO +nhLWCqoG11PVTUzhpYItCjp4At/dG8OQY7WWm0DJJQB38fEqA6JKWpgeWwUdkk8j +anCrNUBEuzt3UPSZ17DGCw2+J+mwsg1nevaFIXy0gN2zPtTBWtacznPL -----END RSA PRIVATE KEY----- diff --git a/test/ejabberd_SUITE_data/configtest.yml b/test/ejabberd_SUITE_data/configtest.yml new file mode 100644 index 000000000..cdd8b0023 --- /dev/null +++ b/test/ejabberd_SUITE_data/configtest.yml @@ -0,0 +1,119 @@ + +define_macro: + CONFIGTEST_CONFIG: + modules: + mod_mam: {} + mod_muc: {} + +## Interactions + +define_macro: + MOK: macro + +define_keyword: + MOK: keyword + +macro_over_keyword: MOK + +## + +define_keyword: + KIM_KEYWORD: "-keyword-" + +define_macro: + KIM_MACRO: "+macro+/@KIM_KEYWORD@" + +keyword_inside_macro: KIM_MACRO + +## + +define_macro: + MAK_MACRO: "+macro+" + +define_keyword: + MAK_KEYWORD: "-keyword-" + +macro_and_keyword: "@MAK_MACRO@&@MAK_KEYWORD@" + +## + +define_macro: + MD: "macro" + +macro_double: "@MD@--@MD@" + +## + +define_keyword: + KD: "keyword" + +keyword_double: "@KD@--@KD@" + +## Macro Toplevel + +define_macro: + MTGA: mtga + MTGS: "Mtgs" + MTLA: mtla + MTLS: "Mtls" + +mtga: MTGA +mtgs: MTGS +mtgsi: "@MTGS@i" + +host_config: + configtest.localhost: + mtla: MTLA + mtls: MTLS + mtlsi: "@MTLS@i" + +## Keyword Toplevel + +define_keyword: + KTGA: ktga + KTLA: ktla + KTGS: "Ktgs" + KTLS: "Ktls" + +ktga: KTGA +ktgs: KTGS +ktgsi: "@KTGS@i" + +host_config: + configtest.localhost: + ktla: KTLA + ktls: KTLS + ktlsi: "@KTLS@i" + +## Macro Module +## Keyword Module +## Predefined Module + +define_macro: + MMA: mma + MMS: "Mms" + +define_keyword: + KMA: kma + KMS: "Kms" + +append_host_config: + configtest.localhost: + modules: + mod_configtest: + mma: MMA + mms: MMS + mmsi: "@MMS@i" + kma: KMA + kms: KMS + kmsi: "@KMS@i" + predefined_keywords: "mp - host: @HOST@, semver: @SEMVER@, version: @VERSION@" + +## Predefined + +tgp: "tgp - semver: @SEMVER@, version: @VERSION@" + +host_config: + configtest.localhost: + tlp: "tlp - semver: @SEMVER@, version: @VERSION@" + diff --git a/test/ejabberd_SUITE_data/ejabberd.cfg b/test/ejabberd_SUITE_data/ejabberd.cfg deleted file mode 100644 index 458de2c7d..000000000 --- a/test/ejabberd_SUITE_data/ejabberd.cfg +++ /dev/null @@ -1,136 +0,0 @@ -{loglevel, 4}. -{hosts, ["localhost", - "mnesia.localhost", - "mysql.localhost", - "pgsql.localhost", - "extauth.localhost", - "ldap.localhost"]}. -{define_macro, 'CERTFILE', "cert.pem"}. -{listen, - [ - {5222, ejabberd_c2s, [ - {access, c2s}, - {shaper, c2s_shaper}, - starttls, zlib, - {certfile, 'CERTFILE'}, - {max_stanza_size, 65536} - ]}, - {5269, ejabberd_s2s_in, [ - {shaper, s2s_shaper}, - {max_stanza_size, 131072} - ]}, - {5280, ejabberd_http, [ - captcha - ]} - ]}. -{shaper, normal, {maxrate, 1000}}. -{shaper, fast, {maxrate, 50000}}. -{max_fsm_queue, 1000}. -{acl, local, {user_regexp, ""}}. -{access, max_user_sessions, [{10, all}]}. -{access, max_user_offline_messages, [{5000, admin}, {100, all}]}. -{access, local, [{allow, local}]}. -{access, c2s, [{deny, blocked}, - {allow, all}]}. -{access, c2s_shaper, [{none, admin}, - {normal, all}]}. -{access, s2s_shaper, [{fast, all}]}. -{access, announce, [{allow, admin}]}. -{access, configure, [{allow, admin}]}. -{access, muc_admin, [{allow, admin}]}. -{access, muc_create, [{allow, local}]}. -{access, muc, [{allow, all}]}. -{access, pubsub_createnode, [{allow, local}]}. -{access, register, [{allow, all}]}. -{registration_timeout, infinity}. -{language, "en"}. -{modules, - [ - {mod_adhoc, []}, - {mod_configure, []}, - {mod_disco, []}, - {mod_ping, []}, - {mod_proxy65, []}, - {mod_register, [ - {welcome_message, {"Welcome!", - "Hi.\nWelcome to this XMPP server."}} - ]}, - {mod_stats, []}, - {mod_time, []}, - {mod_version, []} -]}. -{host_config, "localhost", [{auth_method, internal}]}. -{host_config, "extauth.localhost", - [{auth_method, external}, - {extauth_program, "python extauth.py"}]}. -{host_config, "mnesia.localhost", - [{auth_method, internal}, - {{add, modules}, [{mod_announce, [{db_type, internal}]}, - {mod_blocking, [{db_type, internal}]}, - {mod_caps, [{db_type, internal}]}, - {mod_last, [{db_type, internal}]}, - {mod_muc, [{db_type, internal}]}, - {mod_offline, [{db_type, internal}]}, - {mod_privacy, [{db_type, internal}]}, - {mod_private, [{db_type, internal}]}, - {mod_pubsub, [{access_createnode, pubsub_createnode}, - {ignore_pep_from_offline, true}, - {last_item_cache, false}, - {plugins, ["flat", "hometree", "pep"]}]}, - {mod_roster, [{db_type, internal}]}, - {mod_vcard, [{db_type, internal}]}]} - ]}. -{host_config, "mysql.localhost", - [{auth_method, odbc}, - {odbc_pool_size, 1}, - {odbc_server, {mysql, "localhost", "ejabberd_test", - "ejabberd_test", "ejabberd_test"}}, - {{add, modules}, [{mod_announce, [{db_type, odbc}]}, - {mod_blocking, [{db_type, odbc}]}, - {mod_caps, [{db_type, odbc}]}, - {mod_last, [{db_type, odbc}]}, - {mod_muc, [{db_type, odbc}]}, - {mod_offline, [{db_type, odbc}]}, - {mod_privacy, [{db_type, odbc}]}, - {mod_private, [{db_type, odbc}]}, - {mod_pubsub_odbc, [{access_createnode, pubsub_createnode}, - {ignore_pep_from_offline, true}, - {last_item_cache, false}, - {plugins, ["flat", "hometree", "pep"]}]}, - {mod_roster, [{db_type, odbc}]}, - {mod_vcard, [{db_type, odbc}]}]} - ]}. -{host_config, "pgsql.localhost", - [{auth_method, odbc}, - {odbc_pool_size, 1}, - {odbc_server, {pgsql, "localhost", "ejabberd_test", - "ejabberd_test", "ejabberd_test"}}, - {{add, modules}, [{mod_announce, [{db_type, odbc}]}, - {mod_blocking, [{db_type, odbc}]}, - {mod_caps, [{db_type, odbc}]}, - {mod_last, [{db_type, odbc}]}, - {mod_muc, [{db_type, odbc}]}, - {mod_offline, [{db_type, odbc}]}, - {mod_privacy, [{db_type, odbc}]}, - {mod_private, [{db_type, odbc}]}, - {mod_pubsub_odbc, [{access_createnode, pubsub_createnode}, - {ignore_pep_from_offline, true}, - {last_item_cache, false}, - {plugins, ["flat", "hometree", "pep"]}]}, - {mod_roster, [{db_type, odbc}]}, - {mod_vcard, [{db_type, odbc}]}]} - ]}. -{host_config, "ldap.localhost", - [{auth_method, ldap}, - {ldap_servers, ["localhost"]}, - {ldap_port, 1389}, - {ldap_rootdn, "cn=admin,dc=localhost"}, - {ldap_password, "password"}, - {ldap_base, "ou=users,dc=localhost"}, - {{add, modules}, [{mod_vcard_ldap, []}]} - ]}. - -%%% Local Variables: -%%% mode: erlang -%%% End: -%%% vim: set filetype=erlang tabstop=8 foldmarker=%%%',%%%. foldmethod=marker: diff --git a/test/ejabberd_SUITE_data/ejabberd.extauth.yml b/test/ejabberd_SUITE_data/ejabberd.extauth.yml new file mode 100644 index 000000000..11a67d2cc --- /dev/null +++ b/test/ejabberd_SUITE_data/ejabberd.extauth.yml @@ -0,0 +1,5 @@ +define_macro: + EXTAUTH_CONFIG: + queue_type: ram + extauth_program: "python3 extauth.py" + auth_method: external diff --git a/test/ejabberd_SUITE_data/ejabberd.ldap.yml b/test/ejabberd_SUITE_data/ejabberd.ldap.yml new file mode 100644 index 000000000..a60d227da --- /dev/null +++ b/test/ejabberd_SUITE_data/ejabberd.ldap.yml @@ -0,0 +1,36 @@ +define_macro: + LDAP_CONFIG: + queue_type: ram + ldap_servers: + - "localhost" + ldap_rootdn: "cn=admin,dc=localhost" + ldap_port: 1389 + ldap_password: "password" + ldap_base: "ou=users,dc=localhost" + auth_method: ldap + modules: + mod_vcard: + db_type: ldap + mod_roster: [] # mod_roster is required by mod_shared_roster + mod_shared_roster_ldap: + ldap_auth_check: off + ldap_base: "dc=localhost" + ldap_rfilter: "(objectClass=posixGroup)" + ldap_gfilter: "(&(objectClass=posixGroup)(cn=%g))" + ldap_memberattr: "memberUid" + ldap_ufilter: "(uid=%u)" + ldap_userdesc: "cn" + mod_adhoc: [] + mod_configure: [] + mod_disco: [] + mod_ping: [] + mod_proxy65: + port: PROXY_PORT + mod_register: + welcome_message: + subject: "Welcome!" + body: "Hi. +Welcome to this XMPP server." + mod_stats: [] + mod_time: [] + mod_version: [] diff --git a/test/ejabberd_SUITE_data/ejabberd.ldif b/test/ejabberd_SUITE_data/ejabberd.ldif index 0d2d60638..a98036480 100644 --- a/test/ejabberd_SUITE_data/ejabberd.ldif +++ b/test/ejabberd_SUITE_data/ejabberd.ldif @@ -10,26 +10,43 @@ dn: ou=users,dc=localhost ou: users objectClass: organizationalUnit +dn: ou=groups,dc=localhost +ou: groups +objectClass: organizationalUnit + dn: uid=test_single,ou=users,dc=localhost -uid: test_single -mail: test_single@localhost +uid: test_single!#$%^*()`~+-;_=[]{}|\ +mail: test_single!#$%^*()`~+-;_=[]{}|\@localhost objectClass: person jpegPhoto:: /9g= cn: Test Single -password: password +password: password!@#$%^&*()'"`~<>+-/;:_=[]{}|\ dn: uid=test_master,ou=users,dc=localhost -uid: test_master -mail: test_master@localhost +uid: test_master!#$%^*()`~+-;_=[]{}|\ +mail: test_master!#$%^*()`~+-;_=[]{}|\@localhost objectClass: person jpegPhoto:: /9g= cn: Test Master -password: password +password: password!@#$%^&*()'"`~<>+-/;:_=[]{}|\ dn: uid=test_slave,ou=users,dc=localhost -uid: test_slave -mail: test_slave@localhost +uid: test_slave!#$%^*()`~+-;_=[]{}|\ +mail: test_slave!#$%^*()`~+-;_=[]{}|\@localhost objectClass: person jpegPhoto:: /9g= cn: Test Slave -password: password +password: password!@#$%^&*()'"`~<>+-/;:_=[]{}|\ + +dn: uid=user2,ou=users,dc=localhost +uid: user2 +mail: user2@localhost +objectClass: person +cn: Test User 2 +password: password!@#$%^&*()'"`~<>+-/;:_=[]{}|\ + +dn: cn=group1,ou=groups,dc=localhost +objectClass: posixGroup +memberUid: test_single!#$%^*()`~+-;_=[]{}|\ +memberUid: user2 +cn: group1 diff --git a/test/ejabberd_SUITE_data/ejabberd.mnesia.yml b/test/ejabberd_SUITE_data/ejabberd.mnesia.yml new file mode 100644 index 000000000..56fdf5e6e --- /dev/null +++ b/test/ejabberd_SUITE_data/ejabberd.mnesia.yml @@ -0,0 +1,74 @@ +define_macro: + MNESIA_CONFIG: + queue_type: ram + auth_method: internal + modules: + mod_announce: + db_type: internal + access: local + mod_antispam: + rtbl_services: + - "pubsub.mnesia.localhost" + spam_jids_file: spam_jids.txt + spam_domains_file: spam_domains.txt + spam_urls_file: spam_urls.txt + whitelist_domains_file: whitelist_domains.txt + spam_dump_file: spam.log + mod_blocking: [] + mod_caps: + db_type: internal + mod_last: + db_type: internal + mod_muc: + db_type: internal + vcard: VCARD + mod_muc_occupantid: [] + mod_offline: + db_type: internal + mod_privacy: + db_type: internal + mod_private: + db_type: internal + mod_pubsub: + access_createnode: pubsub_createnode + ignore_pep_from_offline: true + last_item_cache: false + plugins: + - "flat" + - "pep" + vcard: VCARD + mod_roster: + versioning: true + store_current_id: true + db_type: internal + mod_mam: + db_type: internal + mod_vcard: + db_type: internal + vcard: VCARD + mod_vcard_xupdate: [] + mod_client_state: + queue_presence: true + queue_chat_states: true + queue_pep: true + mod_adhoc: [] + mod_configure: [] + mod_disco: [] + mod_ping: [] + mod_proxy65: + port: PROXY_PORT + mod_push: + include_body: false + mod_push_keepalive: [] + mod_s2s_dialback: [] + mod_stream_mgmt: + resume_timeout: 3 + mod_legacy_auth: [] + mod_register: + welcome_message: + subject: "Welcome!" + body: "Hi. +Welcome to this XMPP server." + mod_stats: [] + mod_time: [] + mod_version: [] diff --git a/test/ejabberd_SUITE_data/ejabberd.mssql.yml b/test/ejabberd_SUITE_data/ejabberd.mssql.yml new file mode 100644 index 000000000..1458cafa4 --- /dev/null +++ b/test/ejabberd_SUITE_data/ejabberd.mssql.yml @@ -0,0 +1,80 @@ +define_macro: + MSSQL_CONFIG: + sql_username: MSSQL_USER + sql_type: mssql + sql_server: MSSQL_SERVER + sql_port: MSSQL_PORT + sql_pool_size: 1 + sql_password: MSSQL_PASS + sql_database: MSSQL_DB + auth_method: sql + sm_db_type: sql + modules: + mod_announce: + db_type: sql + access: local + mod_blocking: [] + mod_caps: + db_type: sql + mod_last: + db_type: sql + mod_muc: + db_type: sql + ram_db_type: sql + vcard: VCARD + mod_muc_occupantid: [] + mod_offline: + use_cache: true + db_type: sql + mod_privacy: + db_type: sql + mod_private: + db_type: sql + mod_pubsub: + db_type: sql + access_createnode: pubsub_createnode + ignore_pep_from_offline: true + last_item_cache: false + plugins: + - "flat" + - "pep" + vcard: VCARD + mod_roster: + versioning: true + store_current_id: true + db_type: sql + mod_mam: + db_type: sql + mod_vcard: + db_type: sql + vcard: VCARD + mod_vcard_xupdate: [] + mod_adhoc: [] + mod_configure: [] + mod_disco: [] + mod_ping: [] + mod_proxy65: [] + mod_push: + db_type: sql + include_body: false + mod_push_keepalive: [] + mod_s2s_dialback: [] + mod_stream_mgmt: + resume_timeout: 3 + mod_legacy_auth: [] + mod_register: + welcome_message: + subject: "Welcome!" + body: "Hi. +Welcome to this XMPP server." + mod_stats: [] + mod_time: [] + mod_version: [] + mod_mix: + db_type: sql + mod_mix_pam: + db_type: sql + mod_mqtt: + db_type: sql + mod_shared_roster: + db_type: sql diff --git a/test/ejabberd_SUITE_data/ejabberd.mysql.yml b/test/ejabberd_SUITE_data/ejabberd.mysql.yml new file mode 100644 index 000000000..91705ee68 --- /dev/null +++ b/test/ejabberd_SUITE_data/ejabberd.mysql.yml @@ -0,0 +1,81 @@ +define_macro: + MYSQL_CONFIG: + sql_username: MYSQL_USER + sql_type: mysql + sql_server: MYSQL_SERVER + sql_port: MYSQL_PORT + sql_pool_size: 1 + sql_password: MYSQL_PASS + sql_database: MYSQL_DB + auth_method: sql + sm_db_type: sql + modules: + mod_announce: + db_type: sql + access: local + mod_blocking: [] + mod_caps: + db_type: sql + mod_last: + db_type: sql + mod_muc: + db_type: sql + ram_db_type: sql + vcard: VCARD + mod_muc_occupantid: [] + mod_offline: + use_cache: true + db_type: sql + mod_privacy: + db_type: sql + mod_private: + db_type: sql + mod_pubsub: + db_type: sql + access_createnode: pubsub_createnode + ignore_pep_from_offline: true + last_item_cache: false + plugins: + - "flat" + - "pep" + vcard: VCARD + mod_roster: + versioning: true + store_current_id: true + db_type: sql + mod_mam: + db_type: sql + mod_vcard: + db_type: sql + vcard: VCARD + mod_vcard_xupdate: [] + mod_adhoc: [] + mod_configure: [] + mod_disco: [] + mod_ping: [] + mod_proxy65: + port: PROXY_PORT + mod_push: + db_type: sql + include_body: false + mod_push_keepalive: [] + mod_s2s_dialback: [] + mod_stream_mgmt: + resume_timeout: 3 + mod_legacy_auth: [] + mod_register: + welcome_message: + subject: "Welcome!" + body: "Hi. +Welcome to this XMPP server." + mod_stats: [] + mod_time: [] + mod_version: [] + mod_mix: + db_type: sql + mod_mix_pam: + db_type: sql + mod_mqtt: + db_type: sql + mod_shared_roster: + db_type: sql diff --git a/test/ejabberd_SUITE_data/ejabberd.pgsql.yml b/test/ejabberd_SUITE_data/ejabberd.pgsql.yml new file mode 100644 index 000000000..16d8b1d27 --- /dev/null +++ b/test/ejabberd_SUITE_data/ejabberd.pgsql.yml @@ -0,0 +1,81 @@ +define_macro: + PGSQL_CONFIG: + sql_username: PGSQL_USER + sql_type: pgsql + sql_server: PGSQL_SERVER + sql_port: PGSQL_PORT + sql_pool_size: 1 + sql_password: PGSQL_PASS + sql_database: PGSQL_DB + auth_method: sql + sm_db_type: sql + modules: + mod_announce: + db_type: sql + access: local + mod_blocking: [] + mod_caps: + db_type: sql + mod_last: + db_type: sql + mod_muc: + db_type: sql + ram_db_type: sql + vcard: VCARD + mod_muc_occupantid: [] + mod_offline: + use_cache: true + db_type: sql + mod_privacy: + db_type: sql + mod_private: + db_type: sql + mod_pubsub: + db_type: sql + access_createnode: pubsub_createnode + ignore_pep_from_offline: true + last_item_cache: false + plugins: + - "flat" + - "pep" + vcard: VCARD + mod_roster: + versioning: true + store_current_id: true + db_type: sql + mod_mam: + db_type: sql + mod_vcard: + db_type: sql + vcard: VCARD + mod_vcard_xupdate: [] + mod_adhoc: [] + mod_configure: [] + mod_disco: [] + mod_ping: [] + mod_proxy65: + port: PROXY_PORT + mod_push: + db_type: sql + include_body: false + mod_push_keepalive: [] + mod_s2s_dialback: [] + mod_stream_mgmt: + resume_timeout: 3 + mod_legacy_auth: [] + mod_register: + welcome_message: + subject: "Welcome!" + body: "Hi. +Welcome to this XMPP server." + mod_stats: [] + mod_time: [] + mod_version: [] + mod_mix: + db_type: sql + mod_mix_pam: + db_type: sql + mod_mqtt: + db_type: sql + mod_shared_roster: + db_type: sql diff --git a/test/ejabberd_SUITE_data/ejabberd.redis.yml b/test/ejabberd_SUITE_data/ejabberd.redis.yml new file mode 100644 index 000000000..fb1ba435f --- /dev/null +++ b/test/ejabberd_SUITE_data/ejabberd.redis.yml @@ -0,0 +1,75 @@ +define_macro: + REDIS_CONFIG: + queue_type: ram + auth_method: internal + sm_db_type: redis + modules: + mod_announce: + db_type: internal + access: local + mod_antispam: + rtbl_services: + - "pubsub.redis.localhost" + spam_jids_file: spam_jids.txt + spam_domains_file: spam_domains.txt + spam_urls_file: spam_urls.txt + whitelist_domains_file: whitelist_domains.txt + spam_dump_file: spam.log + mod_blocking: [] + mod_caps: + db_type: internal + mod_last: + db_type: internal + mod_muc: + db_type: internal + vcard: VCARD + mod_muc_occupantid: [] + mod_offline: + db_type: internal + mod_privacy: + db_type: internal + mod_private: + db_type: internal + mod_pubsub: + access_createnode: pubsub_createnode + ignore_pep_from_offline: true + last_item_cache: false + plugins: + - "flat" + - "pep" + vcard: VCARD + mod_roster: + versioning: true + store_current_id: true + db_type: internal + mod_mam: + db_type: internal + mod_vcard: + db_type: internal + vcard: VCARD + mod_vcard_xupdate: [] + mod_client_state: + queue_presence: true + queue_chat_states: true + queue_pep: true + mod_adhoc: [] + mod_configure: [] + mod_disco: [] + mod_ping: [] + mod_proxy65: + port: PROXY_PORT + mod_push: + include_body: false + mod_push_keepalive: [] + mod_s2s_dialback: [] + mod_stream_mgmt: + resume_timeout: 3 + mod_legacy_auth: [] + mod_register: + welcome_message: + subject: "Welcome!" + body: "Hi. +Welcome to this XMPP server." + mod_stats: [] + mod_time: [] + mod_version: [] diff --git a/test/ejabberd_SUITE_data/ejabberd.sqlite.yml b/test/ejabberd_SUITE_data/ejabberd.sqlite.yml new file mode 100644 index 000000000..11420ef6c --- /dev/null +++ b/test/ejabberd_SUITE_data/ejabberd.sqlite.yml @@ -0,0 +1,70 @@ +define_macro: + SQLITE_CONFIG: + auth_stored_password_types: + - plain + - scram_sha256 + sql_type: sqlite + sql_pool_size: 1 + auth_method: sql + sm_db_type: sql + modules: + mod_announce: + db_type: sql + access: local + mod_blocking: [] + mod_caps: + db_type: sql + mod_last: + db_type: sql + mod_muc: + db_type: sql + ram_db_type: sql + vcard: VCARD + mod_muc_occupantid: [] + mod_offline: + db_type: sql + mod_privacy: + db_type: sql + mod_private: + db_type: sql + mod_pubsub: + db_type: sql + access_createnode: pubsub_createnode + ignore_pep_from_offline: true + last_item_cache: false + plugins: + - "flat" + - "pep" + vcard: VCARD + mod_roster: + versioning: true + store_current_id: true + db_type: sql + mod_mam: + db_type: sql + mod_vcard: + db_type: sql + vcard: VCARD + mod_vcard_xupdate: [] + mod_adhoc: [] + mod_configure: [] + mod_disco: [] + mod_ping: [] + mod_proxy65: + port: PROXY_PORT + mod_push: + db_type: sql + include_body: false + mod_push_keepalive: [] + mod_s2s_dialback: [] + mod_stream_mgmt: + resume_timeout: 3 + mod_legacy_auth: [] + mod_register: + welcome_message: + subject: "Welcome!" + body: "Hi. +Welcome to this XMPP server." + mod_stats: [] + mod_time: [] + mod_version: [] diff --git a/test/ejabberd_SUITE_data/ejabberd.yml b/test/ejabberd_SUITE_data/ejabberd.yml index 9bd8a8b0a..812bea841 100644 --- a/test/ejabberd_SUITE_data/ejabberd.yml +++ b/test/ejabberd_SUITE_data/ejabberd.yml @@ -1,317 +1,175 @@ -host_config: - "pgsql.localhost": - odbc_username: "ejabberd_test" - odbc_type: pgsql - odbc_server: "localhost" - odbc_port: 5432 - odbc_pool_size: 1 - odbc_password: "ejabberd_test" - odbc_database: "ejabberd_test" - auth_method: odbc - modules: - mod_announce: - db_type: odbc - access: local - mod_blocking: - db_type: odbc - mod_caps: - db_type: odbc - mod_last: - db_type: odbc - mod_muc: - db_type: odbc - mod_offline: - db_type: odbc - mod_privacy: - db_type: odbc - mod_private: - db_type: odbc - mod_pubsub_odbc: - access_createnode: pubsub_createnode - ignore_pep_from_offline: true - last_item_cache: false - plugins: - - "flat" - - "hometree" - - "pep" - mod_roster: - versioning: true - store_current_id: true - db_type: odbc - mod_vcard: - db_type: odbc - mod_vcard_xupdate: - db_type: odbc - mod_adhoc: [] - mod_configure: [] - mod_disco: [] - mod_ping: [] - mod_proxy65: [] - mod_register: - welcome_message: - subject: "Welcome!" - body: "Hi. -Welcome to this XMPP server." - mod_stats: [] - mod_time: [] - mod_version: [] - "mysql.localhost": - odbc_username: "ejabberd_test" - odbc_type: mysql - odbc_server: "localhost" - odbc_port: 3306 - odbc_pool_size: 1 - odbc_password: "ejabberd_test" - odbc_database: "ejabberd_test" - auth_method: odbc - modules: - mod_announce: - db_type: odbc - access: local - mod_blocking: - db_type: odbc - mod_caps: - db_type: odbc - mod_last: - db_type: odbc - mod_muc: - db_type: odbc - mod_offline: - db_type: odbc - mod_privacy: - db_type: odbc - mod_private: - db_type: odbc - mod_pubsub_odbc: - access_createnode: pubsub_createnode - ignore_pep_from_offline: true - last_item_cache: false - plugins: - - "flat" - - "hometree" - - "pep" - mod_roster: - versioning: true - store_current_id: true - db_type: odbc - mod_vcard: - db_type: odbc - mod_vcard_xupdate: - db_type: odbc - mod_adhoc: [] - mod_configure: [] - mod_disco: [] - mod_ping: [] - mod_proxy65: [] - mod_register: - welcome_message: - subject: "Welcome!" - body: "Hi. -Welcome to this XMPP server." - mod_stats: [] - mod_time: [] - mod_version: [] - "mnesia.localhost": - auth_method: internal - modules: - mod_announce: - db_type: internal - access: local - mod_blocking: - db_type: internal - mod_caps: - db_type: internal - mod_last: - db_type: internal - mod_muc: - db_type: internal - mod_offline: - db_type: internal - mod_privacy: - db_type: internal - mod_private: - db_type: internal - mod_pubsub: - access_createnode: pubsub_createnode - ignore_pep_from_offline: true - last_item_cache: false - plugins: - - "flat" - - "hometree" - - "pep" - mod_roster: - versioning: true - store_current_id: true - db_type: internal - mod_vcard: - db_type: internal - mod_vcard_xupdate: - db_type: internal - mod_carboncopy: - db_type: internal - mod_client_state: - drop_chat_states: true - queue_presence: true - mod_adhoc: [] - mod_configure: [] - mod_disco: [] - mod_ping: [] - mod_proxy65: [] - mod_register: - welcome_message: - subject: "Welcome!" - body: "Hi. -Welcome to this XMPP server." - mod_stats: [] - mod_time: [] - mod_version: [] - "riak.localhost": - auth_method: riak - modules: - mod_announce: - db_type: riak - access: local - mod_blocking: - db_type: riak - mod_caps: - db_type: riak - mod_last: - db_type: riak - mod_muc: - db_type: riak - mod_offline: - db_type: riak - mod_privacy: - db_type: riak - mod_private: - db_type: riak - mod_roster: - versioning: true - store_current_id: true - db_type: riak - mod_vcard: - db_type: riak - mod_vcard_xupdate: - db_type: riak - mod_adhoc: [] - mod_configure: [] - mod_disco: [] - mod_ping: [] - mod_proxy65: [] - mod_register: - welcome_message: - subject: "Welcome!" - body: "Hi. -Welcome to this XMPP server." - mod_stats: [] - mod_time: [] - mod_version: [] - "localhost": - auth_method: internal - "ldap.localhost": - ldap_servers: - - "localhost" - ldap_rootdn: "cn=admin,dc=localhost" - ldap_port: 1389 - ldap_password: "password" - ldap_base: "ou=users,dc=localhost" - auth_method: ldap - modules: - mod_vcard_ldap: [] - mod_adhoc: [] - mod_configure: [] - mod_disco: [] - mod_ping: [] - mod_proxy65: [] - mod_register: - welcome_message: - subject: "Welcome!" - body: "Hi. -Welcome to this XMPP server." - mod_stats: [] - mod_time: [] - mod_version: [] - "extauth.localhost": - extauth_program: "python extauth.py" - auth_method: external -hosts: - - "localhost" - - "mnesia.localhost" - - "mysql.localhost" - - "pgsql.localhost" - - "extauth.localhost" - - "ldap.localhost" - - "riak.localhost" -access: - announce: - admin: allow - c2s: - blocked: deny - all: allow - c2s_shaper: - admin: none - all: normal - configure: - admin: allow - local: - local: allow - max_user_offline_messages: - admin: 5000 - all: 100 - max_user_sessions: - all: 10 - muc: - all: allow - muc_admin: - admin: allow - muc_create: - local: allow - pubsub_createnode: - local: allow - register: - all: allow - s2s_shaper: - all: fast -acl: - local: +include_config_file: + - macros.yml + - configtest.yml + - ejabberd.extauth.yml + - ejabberd.ldap.yml + - ejabberd.mnesia.yml + - ejabberd.mysql.yml + - ejabberd.mssql.yml + - ejabberd.pgsql.yml + - ejabberd.redis.yml + - ejabberd.sqlite.yml + +host_config: + configtest.localhost: CONFIGTEST_CONFIG + pgsql.localhost: PGSQL_CONFIG + sqlite.localhost: SQLITE_CONFIG + mysql.localhost: MYSQL_CONFIG + mssql.localhost: MSSQL_CONFIG + mnesia.localhost: MNESIA_CONFIG + redis.localhost: REDIS_CONFIG + ldap.localhost: LDAP_CONFIG + extauth.localhost: EXTAUTH_CONFIG + localhost: + auth_method: + - internal + - anonymous + +hosts: + - localhost + - configtest.localhost + - mnesia.localhost + - redis.localhost + - mysql.localhost + - mssql.localhost + - pgsql.localhost + - extauth.localhost + - ldap.localhost + - sqlite.localhost + +shaper_rules: + c2s_shaper: + none: admin + normal: all + max_user_offline_messages: + infinity: all + max_user_sessions: + 10: all + s2s_shaper: + fast: all + +access_rules: + announce: + allow: admin + c2s: + deny: blocked + allow: all + configure: + allow: admin + local: + allow: local + muc: + allow: all + muc_admin: + allow: admin + muc_create: + allow: local + pubsub_createnode: + allow: local + register: + allow: all + +acl: + local: user_regexp: "" -define_macro: - CERTFILE: "cert.pem" -language: "en" -listen: - - - port: 5222 + admin: + user: "admin" +language: en +listen: + - + port: C2S_PORT module: ejabberd_c2s max_stanza_size: 65536 - certfile: CERTFILE zlib: true starttls: true + tls_verify: true shaper: c2s_shaper access: c2s - - - port: 5269 + - + port: S2S_PORT module: ejabberd_s2s_in - - - port: 5280 + - + port: WEB_PORT module: ejabberd_http - captcha: true -loglevel: 4 + request_handlers: + "/admin": ejabberd_web_admin + "/api": mod_http_api + "/upload": mod_http_upload + "/captcha": ejabberd_captcha + - + port: STUN_PORT + module: ejabberd_stun + transport: udp + use_turn: true + turn_ipv4_address: "203.0.113.3" + - + port: COMPONENT_PORT + module: ejabberd_service + password: PASSWORD +loglevel: LOGLEVEL max_fsm_queue: 1000 -modules: +queue_type: file +modules: mod_adhoc: [] + mod_adhoc_api: [] + mod_admin_extra: [] + mod_admin_update_sql: [] + mod_announce: [] mod_configure: [] mod_disco: [] mod_ping: [] - mod_proxy65: [] - mod_register: - welcome_message: + mod_proxy65: + port: PROXY_PORT + vcard: VCARD + mod_muc: + vcard: VCARD + mod_muc_occupantid: [] + mod_muc_admin: [] + mod_carboncopy: [] + mod_jidprep: [] + mod_mam: [] + mod_last: [] + mod_register: + welcome_message: subject: "Welcome!" body: "Hi. Welcome to this XMPP server." mod_stats: [] + mod_s2s_dialback: [] + mod_legacy_auth: [] + mod_stream_mgmt: + max_ack_queue: 10 + resume_timeout: 3 + mod_stun_disco: + secret: "cryptic" + services: + - + host: "example.com" + type: turns mod_time: [] mod_version: [] + mod_http_upload: + docroot: PRIV_DIR + put_url: PUT_URL + get_url: GET_URL + max_size: 10000 + vcard: VCARD registration_timeout: infinity -shaper: +s2s_use_starttls: false +ca_file: CAFILE +c2s_cafile: CAFILE +outgoing_s2s_port: S2S_PORT +shaper: fast: 50000 - normal: 1000 + normal: 10000 +certfiles: + - CERTFILE + +new_sql_schema: NEW_SCHEMA + +update_sql_schema: UPDATE_SQL_SCHEMA + +api_permissions: + "public commands": + who: all + what: "*" diff --git a/test/ejabberd_SUITE_data/extauth.py b/test/ejabberd_SUITE_data/extauth.py index 7f32eb8be..e34208ed7 100755 --- a/test/ejabberd_SUITE_data/extauth.py +++ b/test/ejabberd_SUITE_data/extauth.py @@ -1,34 +1,56 @@ +"""extauth dummy script for ejabberd testing.""" + import sys import struct +def read_from_stdin(read_bytes): + """Read buffer from standard input.""" + if hasattr(sys.stdin, 'buffer'): + return sys.stdin.buffer.read(read_bytes) + return sys.stdin.read(read_bytes) + def read(): - (pkt_size,) = struct.unpack('>H', sys.stdin.read(2)) - pkt = sys.stdin.read(pkt_size).split(':') - cmd = pkt[0] - args_num = len(pkt) - 1 - if cmd == 'auth' and args_num == 3: + """Read input and process the command.""" + (pkt_size,) = struct.unpack('>H', read_from_stdin(2)) + pkt = sys.stdin.read(pkt_size) + cmd = pkt.split(':')[0] + if cmd == 'auth': + user, _, _ = pkt.split(':', 3)[1:] + if user == "wrong": + write(False) + else: + write(True) + elif cmd == 'isuser': + user, _ = pkt.split(':', 2)[1:] + if user == "wrong": + write(False) + else: + write(True) + elif cmd == 'setpass': + user, _, _ = pkt.split(':', 3)[1:] write(True) - elif cmd == 'isuser' and args_num == 2: + elif cmd == 'tryregister': + user, _, _ = pkt.split(':', 3)[1:] write(True) - elif cmd == 'setpass' and args_num == 3: + elif cmd == 'removeuser': + user, _ = pkt.split(':', 2)[1:] write(True) - elif cmd == 'tryregister' and args_num == 3: - write(True) - elif cmd == 'removeuser' and args_num == 2: - write(True) - elif cmd == 'removeuser3' and args_num == 3: + elif cmd == 'removeuser3': + user, _, _ = pkt.split(':', 3)[1:] write(True) else: write(False) read() def write(result): + """write result to standard output.""" if result: sys.stdout.write('\x00\x02\x00\x01') else: sys.stdout.write('\x00\x02\x00\x00') sys.stdout.flush() + if __name__ == "__main__": try: read() diff --git a/test/ejabberd_SUITE_data/gencerts.sh b/test/ejabberd_SUITE_data/gencerts.sh new file mode 100755 index 000000000..6975fe422 --- /dev/null +++ b/test/ejabberd_SUITE_data/gencerts.sh @@ -0,0 +1,20 @@ +#!/bin/sh +# Update openssl.cnf if needed (in particular section [alt_names]) + +rm -rf ssl +mkdir -p ssl/newcerts +touch ssl/index.txt +echo 01 > ssl/serial +echo 1000 > ssl/crlnumber +openssl genrsa -out ca.key 2048 +openssl req -new -days 10000 -x509 -key ca.key -out ca.pem -batch +openssl genrsa -out ssl/client.key +openssl req -new -key ssl/client.key -out ssl/client.csr -config openssl.cnf -batch -subj /C=AU/ST=Some-State/O=Internet\ Widgits\ Pty\ Ltd/CN=localhost +openssl ca -keyfile ca.key -cert ca.pem -in ssl/client.csr -out ssl/client.crt -config openssl.cnf -days 10000 -batch -notext -policy policy_anything +openssl req -new -key ssl/client.key -out ssl/self-signed-client.csr -batch -subj /C=AU/ST=Some-State/O=Internet\ Widgits\ Pty\ Ltd/CN=localhost +openssl x509 -req -in ssl/self-signed-client.csr -signkey ssl/client.key -out ssl/self-signed-client.crt -days 10000 +cat ssl/client.crt > cert.pem +cat ssl/self-signed-client.crt > self-signed-cert.pem +cat ssl/client.key >> cert.pem +cat ssl/client.key >> self-signed-cert.pem +rm -rf ssl diff --git a/test/ejabberd_SUITE_data/macros.yml b/test/ejabberd_SUITE_data/macros.yml new file mode 100644 index 000000000..5391562ba --- /dev/null +++ b/test/ejabberd_SUITE_data/macros.yml @@ -0,0 +1,135 @@ +define_macro: + CERTFILE: cert.pem + CAFILE: ca.pem + C2S_PORT: @@c2s_port@@ + S2S_PORT: @@s2s_port@@ + WEB_PORT: @@web_port@@ + STUN_PORT: @@stun_port@@ + COMPONENT_PORT: @@component_port@@ + PROXY_PORT: @@proxy_port@@ + PASSWORD: >- + @@password@@ + LOGLEVEL: @@loglevel@@ + PRIV_DIR: "@@priv_dir@@" + PUT_URL: "http://upload.@HOST@:@@web_port@@/upload" + GET_URL: "http://upload.@HOST@:@@web_port@@/upload" + NEW_SCHEMA: @@new_schema@@ + UPDATE_SQL_SCHEMA: @@update_sql_schema@@ + MYSQL_USER: "@@mysql_user@@" + MYSQL_SERVER: "@@mysql_server@@" + MYSQL_PORT: @@mysql_port@@ + MYSQL_PASS: "@@mysql_pass@@" + MYSQL_DB: "@@mysql_db@@" + MSSQL_USER: "@@mssql_user@@" + MSSQL_SERVER: "@@mssql_server@@" + MSSQL_PORT: @@mssql_port@@ + MSSQL_PASS: "@@mssql_pass@@" + MSSQL_DB: "@@mssql_db@@" + PGSQL_USER: "@@pgsql_user@@" + PGSQL_SERVER: "@@pgsql_server@@" + PGSQL_PORT: @@pgsql_port@@ + PGSQL_PASS: "@@pgsql_pass@@" + PGSQL_DB: "@@pgsql_db@@" + VCARD: + version: "1.0" + fn: Full Name + n: + family: Family + given: Given + middle: Middle + prefix: Prefix + suffix: Suffix + nickname: Nickname + photo: + type: image/png + extval: https://domain.tld/photo.png + binval: >- + iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQAAAAA3bvkkAA + AACklEQVR4AWNoAAAAggCBTBfX3wAAAABJRU5ErkJggg== + bday: 2000-01-01 + adr: + - + home: true + work: true + postal: true + parcel: true + dom: true + intl: true + pref: true + pobox: Pobox + extadd: Extadd + street: Street + locality: Locality + region: Region + pcode: Pcode + ctry: Ctry + label: + - + home: true + work: true + postal: true + parcel: true + dom: true + intl: true + pref: true + line: + - Line1 + - Line2 + tel: + - + home: true + work: true + voice: true + fax: true + pager: true + msg: true + cell: true + video: true + bbs: true + modem: true + isdn: true + pcs: true + pref: true + number: +7-900-01-02 + email: + - + home: true + work: true + internet: true + pref: true + x400: true + userid: user@domain.tld + jabberid: user@domain.tld + mailer: Mailer + tz: TZ + geo: + lat: "12.0" + lon: "21.0" + title: Title + role: Role + logo: + type: image/png + extval: https://domain.tld/logo.png + binval: >- + iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQAAAAA3bvkkAA + AACklEQVR4AWNoAAAAggCBTBfX3wAAAABJRU5ErkJggg== + categories: + - Cat1 + - Cat2 + note: Note + prodid: ProdID + rev: Rev + sort_string: SortString + sound: + phonetic: Phonetic + extval: https://domain.tld/sound.ogg + binval: >- + iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQAAAAA3bvkkAA + AACklEQVR4AWNoAAAAggCBTBfX3wAAAABJRU5ErkJggg== + uid: UID + url: https://domain.tld + class: public + key: + type: Type + cred: Cred + desc: Desc diff --git a/test/ejabberd_SUITE_data/openssl.cnf b/test/ejabberd_SUITE_data/openssl.cnf new file mode 100644 index 000000000..7003c68fc --- /dev/null +++ b/test/ejabberd_SUITE_data/openssl.cnf @@ -0,0 +1,322 @@ +# +# OpenSSL example configuration file. +# This is mostly being used for generation of certificate requests. +# + +# This definition stops the following lines choking if HOME isn't +# defined. +HOME = . +RANDFILE = $ENV::HOME/.rnd + +# Extra OBJECT IDENTIFIER info: +#oid_file = $ENV::HOME/.oid +oid_section = new_oids + +# To use this configuration file with the "-extfile" option of the +# "openssl x509" utility, name here the section containing the +# X.509v3 extensions to use: +extensions = v3_req +# (Alternatively, use a configuration file that has only +# X.509v3 extensions in its main [= default] section.) + +[ new_oids ] +# We can add new OIDs in here for use by 'ca' and 'req'. +# Add a simple OID like this: +# testoid1=1.2.3.4 +# Or use config file substitution like this: +# testoid2=${testoid1}.5.6 + +#################################################################### +[ ca ] +default_ca = CA_default # The default ca section + +#################################################################### +[ CA_default ] + +#dir = ./demoCA # Where everything is kept +dir = ssl +certs = $dir/certs # Where the issued certs are kept +crl_dir = $dir/crl # Where the issued crl are kept +database = $dir/index.txt # database index file. +#unique_subject = no # Set to 'no' to allow creation of + # several certificates with same subject. +new_certs_dir = $dir/newcerts # default place for new certs. + +certificate = $dir/cacert.pem # The CA certificate +serial = $dir/serial # The current serial number +crlnumber = $dir/crlnumber # the current crl number + # must be commented out to leave a V1 CRL +crl = $dir/crl.pem # The current CRL +private_key = $dir/private/cakey.pem# The private key +RANDFILE = $dir/private/.rand # private random number file + +x509_extensions = usr_cert # The extensions to add to the cert + +# Comment out the following two lines for the "traditional" +# (and highly broken) format. +name_opt = ca_default # Subject Name options +cert_opt = ca_default # Certificate field options + +# Extension copying option: use with caution. +copy_extensions = copy + +# Extensions to add to a CRL. Note: Netscape communicator chokes on V2 CRLs +# so this is commented out by default to leave a V1 CRL. +# crlnumber must also be commented out to leave a V1 CRL. +# crl_extensions = crl_ext + +default_days = 365 # how long to certify for +default_crl_days= 30 # how long before next CRL +default_md = sha256 # which md to use. +preserve = no # keep passed DN ordering + +# A few difference way of specifying how similar the request should look +# For type CA, the listed attributes must be the same, and the optional +# and supplied fields are just that :-) +policy = policy_match + +# For the CA policy +[ policy_match ] +countryName = match +stateOrProvinceName = match +organizationName = match +organizationalUnitName = optional +commonName = optional +emailAddress = optional + +# For the 'anything' policy +# At this point in time, you must list all acceptable 'object' +# types. +[ policy_anything ] +countryName = optional +stateOrProvinceName = optional +localityName = optional +organizationName = optional +organizationalUnitName = optional +commonName = optional +emailAddress = optional + +#################################################################### +[ req ] +default_bits = 1024 +default_keyfile = privkey.pem +distinguished_name = req_distinguished_name +attributes = req_attributes +x509_extensions = v3_ca # The extensions to add to the self signed cert + +# Passwords for private keys if not present they will be prompted for +# input_password = secret +# output_password = secret + +# This sets a mask for permitted string types. There are several options. +# default: PrintableString, T61String, BMPString. +# pkix : PrintableString, BMPString. +# utf8only: only UTF8Strings. +# nombstr : PrintableString, T61String (no BMPStrings or UTF8Strings). +# MASK:XXXX a literal mask value. +# WARNING: current versions of Netscape crash on BMPStrings or UTF8Strings +# so use this option with caution! +string_mask = nombstr + +req_extensions = v3_req # The extensions to add to a certificate request + +[ req_distinguished_name ] +countryName = Country Name (2 letter code) +countryName_default = AU +countryName_min = 2 +countryName_max = 2 + +stateOrProvinceName = State or Province Name (full name) +stateOrProvinceName_default = Some-State + +localityName = Locality Name (eg, city) + +0.organizationName = Organization Name (eg, company) +0.organizationName_default = Internet Widgits Pty Ltd + +# we can do this but it is not needed normally :-) +#1.organizationName = Second Organization Name (eg, company) +#1.organizationName_default = World Wide Web Pty Ltd + +organizationalUnitName = Organizational Unit Name (eg, section) +#organizationalUnitName_default = + +commonName = Common Name (eg, YOUR name) +commonName_max = 64 + +emailAddress = Email Address +emailAddress_max = 64 + +# SET-ex3 = SET extension number 3 + +[ req_attributes ] +challengePassword = A challenge password +challengePassword_min = 4 +challengePassword_max = 20 + +unstructuredName = An optional company name + +[ usr_cert ] + +# These extensions are added when 'ca' signs a request. + +# This goes against PKIX guidelines but some CAs do it and some software +# requires this to avoid interpreting an end user certificate as a CA. + +basicConstraints=CA:FALSE + +# Here are some examples of the usage of nsCertType. If it is omitted +# the certificate can be used for anything *except* object signing. + +# This is OK for an SSL server. +# nsCertType = server + +# For an object signing certificate this would be used. +# nsCertType = objsign + +# For normal client use this is typical +# nsCertType = client, email + +# and for everything including object signing: +# nsCertType = client, email, objsign + +# This is typical in keyUsage for a client certificate. +# keyUsage = nonRepudiation, digitalSignature, keyEncipherment + +# This will be displayed in Netscape's comment listbox. +nsComment = "OpenSSL Generated Certificate" + +# PKIX recommendations harmless if included in all certificates. +subjectKeyIdentifier=hash +authorityKeyIdentifier=keyid,issuer + +# This stuff is for subjectAltName and issuerAltname. +# Import the email address. +# subjectAltName=email:copy +# An alternative to produce certificates that aren't +# deprecated according to PKIX. +# subjectAltName=email:move + +# Copy subject details +# issuerAltName=issuer:copy + +#nsCaRevocationUrl = http://www.domain.dom/ca-crl.pem +#nsBaseUrl +#nsRevocationUrl +#nsRenewalUrl +#nsCaPolicyUrl +#nsSslServerName + +crlDistributionPoints = URI:http://localhost:5280/data/crl.der +authorityInfoAccess = OCSP;URI:http://localhost:5280/ocsp + +[ v3_req ] + +# Extensions to add to a certificate request + +basicConstraints = CA:FALSE +keyUsage = nonRepudiation, digitalSignature, keyEncipherment +extendedKeyUsage = OCSPSigning,serverAuth,clientAuth +subjectAltName = @alt_names + +[alt_names] +DNS.1 = *.localhost +otherName.1 = 1.3.6.1.5.5.7.8.5;UTF8:"test_single!#$%^*()`~+-;_=[]{}|\\@localhost" + +[ v3_ca ] +crlDistributionPoints = URI:http://localhost:5280/data/crl.der + +# Extensions for a typical CA + + +# PKIX recommendation. + +subjectKeyIdentifier=hash + +authorityKeyIdentifier=keyid:always,issuer:always + +# This is what PKIX recommends but some broken software chokes on critical +# extensions. +#basicConstraints = critical,CA:true +# So we do this instead. +basicConstraints = CA:true + +# Key usage: this is typical for a CA certificate. However since it will +# prevent it being used as an test self-signed certificate it is best +# left out by default. +# keyUsage = cRLSign, keyCertSign + +# Some might want this also +# nsCertType = sslCA, emailCA + +# Include email address in subject alt name: another PKIX recommendation +# subjectAltName=email:copy +# Copy issuer details +# issuerAltName=issuer:copy + +# DER hex encoding of an extension: beware experts only! +# obj=DER:02:03 +# Where 'obj' is a standard or added object +# You can even override a supported extension: +# basicConstraints= critical, DER:30:03:01:01:FF + +[ crl_ext ] + +# CRL extensions. +# Only issuerAltName and authorityKeyIdentifier make any sense in a CRL. + +# issuerAltName=issuer:copy +authorityKeyIdentifier=keyid:always,issuer:always + +[ proxy_cert_ext ] +# These extensions should be added when creating a proxy certificate + +# This goes against PKIX guidelines but some CAs do it and some software +# requires this to avoid interpreting an end user certificate as a CA. + +basicConstraints=CA:FALSE + +# Here are some examples of the usage of nsCertType. If it is omitted +# the certificate can be used for anything *except* object signing. + +# This is OK for an SSL server. +# nsCertType = server + +# For an object signing certificate this would be used. +# nsCertType = objsign + +# For normal client use this is typical +# nsCertType = client, email + +# and for everything including object signing: +# nsCertType = client, email, objsign + +# This is typical in keyUsage for a client certificate. +# keyUsage = nonRepudiation, digitalSignature, keyEncipherment + +# This will be displayed in Netscape's comment listbox. +nsComment = "OpenSSL Generated Certificate" + +# PKIX recommendations harmless if included in all certificates. +subjectKeyIdentifier=hash +authorityKeyIdentifier=keyid,issuer:always + +# This stuff is for subjectAltName and issuerAltname. +# Import the email address. +# subjectAltName=email:copy +# An alternative to produce certificates that aren't +# deprecated according to PKIX. +# subjectAltName=email:move + +# Copy subject details +# issuerAltName=issuer:copy + +#nsCaRevocationUrl = http://www.domain.dom/ca-crl.pem +#nsBaseUrl +#nsRevocationUrl +#nsRenewalUrl +#nsCaPolicyUrl +#nsSslServerName + +# This really needs to be in place for it to be a proxy certificate. +proxyCertInfo=critical,language:id-ppl-anyLanguage,pathlen:3,policy:foo diff --git a/test/ejabberd_SUITE_data/self-signed-cert.pem b/test/ejabberd_SUITE_data/self-signed-cert.pem new file mode 100644 index 000000000..29fc38d36 --- /dev/null +++ b/test/ejabberd_SUITE_data/self-signed-cert.pem @@ -0,0 +1,47 @@ +-----BEGIN CERTIFICATE----- +MIIDOTCCAiECFHMoNo36Xx0BWkzS8nwvCPGnHnHRMA0GCSqGSIb3DQEBCwUAMFkx +CzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRl +cm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0xODA5 +MjQxMzE4MjRaFw00NjAyMDkxMzE4MjRaMFkxCzAJBgNVBAYTAkFVMRMwEQYDVQQI +DApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQx +EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBANaEDDeDGf8BH+EjO3IcB2fspkOSp7eOVWdI5oKZyhcdKfniaDXoL78GP/ND +Vk5nIGxp6q7iYjoeQBFDQ7Qg+Rv+9KM9lh4GQWZLi7KKRGv9rA5sMb1G79X/5g/I +h3A2llLygMuE1BxXhw0C9vByaJvRO24GGnXroXm8GXLG7pTxXj8Pn1jO4y1sZDGA +pX7Hc7Aa4Hq22VT5wLo++3Bl2UkOqfeozj4if5ozlQsFibXZasJntgAuAMCmHVs3 +N2LMPJREv7mzGvpT9RIfWiPHnaRyJQuZ2DS1U1muF8OgrQL6syrTTSc8MqW0d33A +12lr7ztxmN8Dh1Qv8MgrC/El3O0CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAhM+Q +qt4IlM1SMb74L5GO2JKGVSUbZmaFJEZcjlrcHkw+Tfc5SMxaj7JpTPg7OGNY1L/3 +HnUDdaDRZ5xVOxUF7gTBWDAgkO7En5YfvvEYXUYUk7wwpFrqUqQpluqQIxr+Zf6l +pZFLhKIANa4wayKtZ9v4uBtRjnm9Hj7gQHeWN9sueIq7d4HO5lubYlzu1+6qeP+L +M0ciNhsUPypCwVcLPB+1Eo925QBwAhXsvPD9yKFQg1M7XxcJSy0w3DwWQsTTsEbk +8c/vIF/IhkOJHQDTKa+VSJM+hZgmx/PsyVdbWRSCAusiZpjHKhzzTCNEloGp/Vbm +5y/OeAK2TGPTg9I91w== +-----END CERTIFICATE----- +-----BEGIN RSA PRIVATE KEY----- +MIIEpgIBAAKCAQEA1oQMN4MZ/wEf4SM7chwHZ+ymQ5Knt45VZ0jmgpnKFx0p+eJo +NegvvwY/80NWTmcgbGnqruJiOh5AEUNDtCD5G/70oz2WHgZBZkuLsopEa/2sDmwx +vUbv1f/mD8iHcDaWUvKAy4TUHFeHDQL28HJom9E7bgYadeuhebwZcsbulPFePw+f +WM7jLWxkMYClfsdzsBrgerbZVPnAuj77cGXZSQ6p96jOPiJ/mjOVCwWJtdlqwme2 +AC4AwKYdWzc3Ysw8lES/ubMa+lP1Eh9aI8edpHIlC5nYNLVTWa4Xw6CtAvqzKtNN +JzwypbR3fcDXaWvvO3GY3wOHVC/wyCsL8SXc7QIDAQABAoIBAQDUwGX1cHsJ5C2f +9ndwtsfJlHVZs0vPysR9CVpE0Q4TWoNVJ+0++abRB/vI4lHotHL90xZEmJXfGj1k +YZf2QHWQBI7Qj7Yg1Qdr0yUbz/IIQLCyJTA3jvEzBvc/VByveBQi9Aw0zOopqc1x +ZC1RT8bcMumEN11q8mVV/O4oXZAl+mQIbRRt6JIsRtoW8hpB1e2ipHItDMNpSnzA +6PqcddDyDDePgi5lMOaeV9un60A6pI/+uvmw16R1Io+DyYRnxds3HJ/ccI0Co1P1 +khA75QLdnoniYO+oQrq/wGvm+Uq1seh6iuj+SOWvCdB03vPmGYxPKMSW9AtX8xbJ +J9lboi3pAoGBAPBaiUYn9F+Zt9oJTHhAimZgs1ub5xVEFwVhYJtFBT3E1rQWRKuf +kiU1JRq7TB3MGaC4zGi2ql12KV3AqFhwLKG6sKtlo/IJhJfe3DgWmBVYBBifkgYs +mxmA6opgyjbjDEMn6RA+Jov5H267AsnaB4cCB1Jjra6GIdIoMvPghHZXAoGBAOR6 +7VC6E+YX5VJPCZiN0h0aBT+Hl4drYQKvZHp5N8RIBkvmcQHEJgsrUKdirFZEXW6y +WvepwI4C/Xl61y64/DZ7rum/gpAEPdzSkefKysHAiqkMRcIpjiRxTPJ547ZJycjP +E+jzcYfLwQvCW9ZiYl+KdYRbpqBFQC8aWqixFxRbAoGBAJQTsy79vpiHY7V4tRwA +50NboCR4UE3RvT0bWSFPzILZmk0oyvXRQYCa1Vk6uxJAhCl4sLZyk1MxURrpbs3N +jjG1itKNtAuRwZavPo1vnhLIPv3MkXIsWQHFYroOF4bpKszU8cmIAMeLm8nkfTtO +kASlQ02HC6HSEVQgYAPP9svRAoGBANiOnwKl7Bhpy8TQ/zJmMaG9uP23IeuL3l4y +KdVfsXjMH5OvLqtS5BAwFPkiMGBv2fMC/+/AKK8xrFiJEw3I7d0iK+6Hw1OHga8c +soh1kOpF+ecyp6fZxU1LSniFCU0M8UHw7Fke7RueBzKDHJK9m6oczTgPuoYsPSKo +IwfDGjIDAoGBAMJVkInntV8oDPT1WYpOAZ3Z0myCDZVBbjxx8kE4RSJIsFeNSiTO +nhLWCqoG11PVTUzhpYItCjp4At/dG8OQY7WWm0DJJQB38fEqA6JKWpgeWwUdkk8j +anCrNUBEuzt3UPSZ17DGCw2+J+mwsg1nevaFIXy0gN2zPtTBWtacznPL +-----END RSA PRIVATE KEY----- diff --git a/test/ejabberd_SUITE_data/spam_domains.txt b/test/ejabberd_SUITE_data/spam_domains.txt new file mode 100644 index 000000000..c081998b7 --- /dev/null +++ b/test/ejabberd_SUITE_data/spam_domains.txt @@ -0,0 +1 @@ +spam_domain.org diff --git a/test/ejabberd_SUITE_data/spam_jids.txt b/test/ejabberd_SUITE_data/spam_jids.txt new file mode 100644 index 000000000..2b911fd82 --- /dev/null +++ b/test/ejabberd_SUITE_data/spam_jids.txt @@ -0,0 +1 @@ +spammer_jid@localhost diff --git a/test/ejabberd_SUITE_data/spam_urls.txt b/test/ejabberd_SUITE_data/spam_urls.txt new file mode 100644 index 000000000..857172d46 --- /dev/null +++ b/test/ejabberd_SUITE_data/spam_urls.txt @@ -0,0 +1 @@ +https://spam.domain.url diff --git a/test/ejabberd_SUITE_data/whitelist_domains.txt b/test/ejabberd_SUITE_data/whitelist_domains.txt new file mode 100644 index 000000000..b953fb7f6 --- /dev/null +++ b/test/ejabberd_SUITE_data/whitelist_domains.txt @@ -0,0 +1 @@ +whitelisted.domain diff --git a/test/ejabberd_test_options.erl b/test/ejabberd_test_options.erl new file mode 100644 index 000000000..10cfcab3e --- /dev/null +++ b/test/ejabberd_test_options.erl @@ -0,0 +1,114 @@ +%%%---------------------------------------------------------------------- +%%% ejabberd, Copyright (C) 2002-2025 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(ejabberd_test_options). +-behaviour(ejabberd_config). + +-export([opt_type/1, options/0, globals/0, doc/0]). + +%%%=================================================================== +%%% API +%%%=================================================================== +-spec opt_type(atom()) -> econf:validator(). + +opt_type(macro_over_keyword) -> + econf:atom(); + +opt_type(keyword_inside_macro) -> + econf:binary(); + +opt_type(macro_and_keyword) -> + econf:binary(); + +opt_type(macro_double) -> + econf:binary(); + +opt_type(keyword_double) -> + econf:binary(); + +opt_type(mtga) -> + econf:atom(); + +opt_type(mtgs) -> + econf:binary(); + +opt_type(mtgsi) -> + econf:binary(); + +opt_type(mtla) -> + econf:atom(); + +opt_type(mtls) -> + econf:binary(); + +opt_type(mtlsi) -> + econf:binary(); + +opt_type(ktga) -> + econf:atom(); + +opt_type(ktgs) -> + econf:binary(); + +opt_type(ktgsi) -> + econf:binary(); + +opt_type(ktla) -> + econf:atom(); + +opt_type(ktls) -> + econf:binary(); + +opt_type(ktlsi) -> + econf:binary(); + +opt_type(tgp) -> + econf:binary(); + +opt_type(tlp) -> + econf:binary(). + +options() -> + [{macro_over_keyword, undefined}, + {keyword_inside_macro, undefined}, + {macro_and_keyword, undefined}, + {macro_double, undefined}, + {keyword_double, undefined}, + {mtga, undefined}, + {mtgs, undefined}, + {mtgsi, undefined}, + {mtla, undefined}, + {mtls, undefined}, + {mtlsi, undefined}, + {ktga, undefined}, + {ktgs, undefined}, + {ktgsi, undefined}, + {ktla, undefined}, + {ktls, undefined}, + {ktlsi, undefined}, + {tgp, undefined}, + {tlp, undefined} + ]. + +-spec globals() -> [atom()]. +globals() -> + []. + +doc() -> + ejabberd_options_doc:doc(). + diff --git a/test/elixir-config/attr_test.exs b/test/elixir-config/attr_test.exs new file mode 100644 index 000000000..c5cab5bd8 --- /dev/null +++ b/test/elixir-config/attr_test.exs @@ -0,0 +1,87 @@ +defmodule Ejabberd.Config.AttrTest do + use ExUnit.Case, async: true + + alias Ejabberd.Config.Attr + + test "extract attrs from single line block" do + block = quote do + @active false + end + + block_res = Attr.extract_attrs_from_block_with_defaults(block) + assert {:active, false} in block_res + end + + test "extract attrs from multi line block" do + block = quote do + @active false + @opts [http: true] + end + + block_res = Attr.extract_attrs_from_block_with_defaults(block) + assert {:active, false} in block_res + assert {:opts, [http: true]} in block_res + end + + test "inserts correctly defaults attr when missing in block" do + block = quote do + @active false + @opts [http: true] + end + + block_res = Attr.extract_attrs_from_block_with_defaults(block) + + assert {:active, false} in block_res + assert {:git, ""} in block_res + assert {:name, ""} in block_res + assert {:opts, [http: true]} in block_res + assert {:dependency, []} in block_res + end + + test "inserts all defaults attr when passed an empty block" do + block = quote do + end + + block_res = Attr.extract_attrs_from_block_with_defaults(block) + + assert {:active, true} in block_res + assert {:git, ""} in block_res + assert {:name, ""} in block_res + assert {:opts, []} in block_res + assert {:dependency, []} in block_res + end + + test "validates attrs and returns errors, if any" do + block = quote do + @not_supported_attr true + @active "false" + @opts [http: true] + end + + block_res = + block + |> Attr.extract_attrs_from_block_with_defaults + |> Attr.validate + + assert {:ok, {:opts, [http: true]}} in block_res + assert {:ok, {:git, ""}} in block_res + assert {:error, {:not_supported_attr, true}, :attr_not_supported} in block_res + assert {:error, {:active, "false"}, :type_not_supported} in block_res + end + + test "returns the correct type for an attribute" do + assert :boolean == Attr.get_type_for_attr(:active) + assert :string == Attr.get_type_for_attr(:git) + assert :string == Attr.get_type_for_attr(:name) + assert :list == Attr.get_type_for_attr(:opts) + assert :list == Attr.get_type_for_attr(:dependency) + end + + test "returns the correct default for an attribute" do + assert true == Attr.get_default_for_attr(:active) + assert "" == Attr.get_default_for_attr(:git) + assert "" == Attr.get_default_for_attr(:name) + assert [] == Attr.get_default_for_attr(:opts) + assert [] == Attr.get_default_for_attr(:dependency) + end +end diff --git a/test/elixir-config/config_test.exs b/test/elixir-config/config_test.exs new file mode 100644 index 000000000..8970e02e6 --- /dev/null +++ b/test/elixir-config/config_test.exs @@ -0,0 +1,65 @@ +defmodule Ejabberd.ConfigTest do + use ExUnit.Case + + alias Ejabberd.Config + alias Ejabberd.Config.Store + + setup_all do + pid = Process.whereis(Ejabberd.Config.Store) + unless pid != nil and Process.alive?(pid) do + Store.start_link + + File.cd("test/elixir-config/shared") + config_file_path = File.cwd! <> "/ejabberd.exs" + Config.init(config_file_path) + end + + {:ok, %{}} + end + + test "extracts successfully the module name from config file" do + assert [Ejabberd.ConfigFile] == Store.get(:module_name) + end + + test "extracts successfully general opts from config file" do + [general] = Store.get(:general) + shaper = [normal: 1000, fast: 50000, max_fsm_queue: 1000] + assert [loglevel: 4, language: "en", hosts: ["localhost"], shaper: shaper] == general + end + + test "extracts successfully listeners from config file" do + [listen] = Store.get(:listeners) + assert :ejabberd_c2s == listen.module + assert [port: 5222, max_stanza_size: 65536, shaper: :c2s_shaper, access: :c2s] == listen.attrs[:opts] + end + + test "extracts successfully modules from config file" do + [module] = Store.get(:modules) + assert :mod_adhoc == module.module + assert [] == module.attrs[:opts] + end + + test "extracts successfully hooks from config file" do + [register_hook] = Store.get(:hooks) + + assert :register_user == register_hook.hook + assert [host: "localhost"] == register_hook.opts + assert is_function(register_hook.fun) + end + + # TODO: When enabled, this test causes the evaluation of a different config file, so + # the other tests, that uses the store, are compromised because the data is different. + # So, until a good way is found, this test should remain disabed. + # + # test "init/2 with force:true re-initializes the config store with new data" do + # config_file_path = File.cwd! <> "/ejabberd_different_from_default.exs" + # Config.init(config_file_path, true) + # + # assert [Ejabberd.ConfigFile] == Store.get(:module_name) + # assert [[loglevel: 4, language: "en", hosts: ["localhost"]]] == Store.get(:general) + # assert [] == Store.get(:modules) + # assert [] == Store.get(:listeners) + # + # Store.stop + # end +end diff --git a/test/elixir-config/ejabberd_logger.exs b/test/elixir-config/ejabberd_logger.exs new file mode 100644 index 000000000..594909289 --- /dev/null +++ b/test/elixir-config/ejabberd_logger.exs @@ -0,0 +1,49 @@ +defmodule Ejabberd.Config.EjabberdLoggerTest do + use ExUnit.Case + + import ExUnit.CaptureIO + + alias Ejabberd.Config + alias Ejabberd.Config.Store + alias Ejabberd.Config.Validation + alias Ejabberd.Config.EjabberdLogger + + setup_all do + pid = Process.whereis(Ejabberd.Config.Store) + unless pid != nil and Process.alive?(pid) do + Store.start_link + + File.cd("test/elixir-config/shared") + config_file_path = File.cwd! <> "/ejabberd_for_validation.exs" + Config.init(config_file_path) + end + + {:ok, %{}} + end + + test "outputs correctly when attr is not supported" do + error_msg = "[ WARN ] Annotation @attr_not_supported is not supported.\n" + + [_mod_configure, mod_time] = Store.get(:modules) + fun = fn -> + mod_time + |> Validation.validate + |> EjabberdLogger.log_errors + end + + assert capture_io(fun) == error_msg + end + + test "outputs correctly when dependency is not found" do + error_msg = "[ WARN ] Module :mod_adhoc was not found, but is required as a dependency.\n" + + [mod_configure, _mod_time] = Store.get(:modules) + fun = fn -> + mod_configure + |> Validation.validate + |> EjabberdLogger.log_errors + end + + assert capture_io(fun) == error_msg + end +end diff --git a/test/elixir-config/shared/ejabberd.exs b/test/elixir-config/shared/ejabberd.exs new file mode 100644 index 000000000..5d0243bb5 --- /dev/null +++ b/test/elixir-config/shared/ejabberd.exs @@ -0,0 +1,31 @@ +defmodule Ejabberd.ConfigFile do + use Ejabberd.Config + + def start do + [loglevel: 4, + language: "en", + hosts: ["localhost"], + shaper: shaper] + end + + defp shaper do + [normal: 1000, + fast: 50000, + max_fsm_queue: 1000] + end + + listen :ejabberd_c2s do + @opts [ + port: 5222, + max_stanza_size: 65536, + shaper: :c2s_shaper, + access: :c2s] + end + + module :mod_adhoc do + end + + hook :register_user, [host: "localhost"], fn(user, server) -> + info("User registered: #{user} on #{server}") + end +end diff --git a/test/elixir-config/shared/ejabberd_different_from_default.exs b/test/elixir-config/shared/ejabberd_different_from_default.exs new file mode 100644 index 000000000..a39409683 --- /dev/null +++ b/test/elixir-config/shared/ejabberd_different_from_default.exs @@ -0,0 +1,9 @@ +defmodule Ejabberd.ConfigFile do + use Ejabberd.Config + + def start do + [loglevel: 4, + language: "en", + hosts: ["localhost"]] + end +end diff --git a/test/elixir-config/shared/ejabberd_for_validation.exs b/test/elixir-config/shared/ejabberd_for_validation.exs new file mode 100644 index 000000000..e47d925a9 --- /dev/null +++ b/test/elixir-config/shared/ejabberd_for_validation.exs @@ -0,0 +1,18 @@ +defmodule Ejabberd.ConfigFile do + use Ejabberd.Config + + def start do + [loglevel: 4, + language: "en", + hosts: ["localhost"]] + end + + module :mod_time do + @attr_not_supported true + end + + module :mod_configure do + @dependency [:mod_adhoc] + end + +end diff --git a/test/elixir-config/validation_test.exs b/test/elixir-config/validation_test.exs new file mode 100644 index 000000000..ca94d2705 --- /dev/null +++ b/test/elixir-config/validation_test.exs @@ -0,0 +1,31 @@ +defmodule Ejabberd.Config.ValidationTest do + use ExUnit.Case + + alias Ejabberd.Config + alias Ejabberd.Config.Store + alias Ejabberd.Config.Validation + + setup_all do + pid = Process.whereis(Ejabberd.Config.Store) + unless pid != nil and Process.alive?(pid) do + Store.start_link + + File.cd("test/elixir-config/shared") + config_file_path = File.cwd! <> "/ejabberd_for_validation.exs" + Config.init(config_file_path) + end + + {:ok, %{}} + end + + test "validates correctly the modules" do + [mod_configure, mod_time] = Store.get(:modules) + + [{:error, _mod, errors}] = Validation.validate(mod_configure) + assert %{dependency: [mod_adhoc: :not_found]} == errors + + [{:error, _mod, errors}] = Validation.validate(mod_time) + assert %{attribute: [{{:attr_not_supported, true}, :attr_not_supported}]} == errors + + end +end diff --git a/test/example_tests.erl b/test/example_tests.erl new file mode 100644 index 000000000..5fd0a86ff --- /dev/null +++ b/test/example_tests.erl @@ -0,0 +1,67 @@ +%%%------------------------------------------------------------------- +%%% Author : Evgeny Khramtsov +%%% Created : 16 Nov 2016 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(example_tests). + +%% API +-compile(export_all). +-import(suite, []). + +-include("suite.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +%%%=================================================================== +%%% Single user tests +%%%=================================================================== +single_cases() -> + {example_single, [sequence], + [single_test(foo)]}. + +foo(Config) -> + Config. + +%%%=================================================================== +%%% Master-slave tests +%%%=================================================================== +master_slave_cases() -> + {example_master_slave, [sequence], + [master_slave_test(foo)]}. + +foo_master(Config) -> + Config. + +foo_slave(Config) -> + Config. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +single_test(T) -> + list_to_atom("example_" ++ atom_to_list(T)). + +master_slave_test(T) -> + {list_to_atom("example_" ++ atom_to_list(T)), [parallel], + [list_to_atom("example_" ++ atom_to_list(T) ++ "_master"), + list_to_atom("example_" ++ atom_to_list(T) ++ "_slave")]}. diff --git a/test/jidprep_tests.erl b/test/jidprep_tests.erl new file mode 100644 index 000000000..f18e150fb --- /dev/null +++ b/test/jidprep_tests.erl @@ -0,0 +1,62 @@ +%%%------------------------------------------------------------------- +%%% Author : Holger Weiss +%%% Created : 11 Sep 2019 by Holger Weiss +%%% +%%% +%%% ejabberd, Copyright (C) 2019-2025 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(jidprep_tests). + +%% API +-compile(export_all). +-import(suite, [send_recv/2, disconnect/1, is_feature_advertised/2, + server_jid/1]). + +-include("suite.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +%%%=================================================================== +%%% Single user tests +%%%=================================================================== +single_cases() -> + {jidprep_single, [sequence], + [single_test(feature_enabled), + single_test(normalize_jid)]}. + +feature_enabled(Config) -> + true = is_feature_advertised(Config, ?NS_JIDPREP_0), + disconnect(Config). + +normalize_jid(Config) -> + ServerJID = server_jid(Config), + OrigJID = jid:decode(<<"Romeo@Example.COM/Orchard">>), + NormJID = jid:decode(<<"romeo@example.com/Orchard">>), + Request = #jidprep{jid = OrigJID}, + #iq{type = result, sub_els = [#jidprep{jid = NormJID}]} = + send_recv(Config, #iq{type = get, to = ServerJID, + sub_els = [Request]}), + disconnect(Config). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +single_test(T) -> + list_to_atom("jidprep_" ++ atom_to_list(T)). diff --git a/test/json_test.erl b/test/json_test.erl new file mode 100644 index 000000000..7f16cf7a7 --- /dev/null +++ b/test/json_test.erl @@ -0,0 +1,76 @@ +-module(json_test). + +-compile(export_all). + +-include_lib("eunit/include/eunit.hrl"). + +%% @format-begin + +encode_binary_test() -> + Binary = <<"This is an error text.">>, + Encoded = <<"\"This is an error text.\"">>, + ?assertMatch(Encoded, misc:json_encode(Binary)). + +-ifdef(OTP_BELOW_26). + +%% OTP 25 or lower +encode_map_test() -> + Map = #{name => <<"room">>, + service => <<"conference">>, + jid => jid:encode({<<"user">>, <<"server">>, <<"">>}), + affiliation => member}, + Encoded = + <<"{\"service\":\"conference\",\"name\":\"room\",\"jid\":\"user@server\",\"affiliation\":\"member\"}">>, + ?assertMatch(Encoded, misc:json_encode(Map)). + +-endif. + +-ifdef(OTP_BELOW_27). + +-ifndef(OTP_BELOW_26). + +%% OTP 26 +encode_map_test() -> + Map = #{name => <<"room">>, + service => <<"conference">>, + jid => jid:encode({<<"user">>, <<"server">>, <<"">>}), + affiliation => member}, + Encoded = + <<"{\"affiliation\":\"member\",\"jid\":\"user@server\",\"service\":\"conference\",\"name\":\"room\"}">>, + ?assertMatch(Encoded, misc:json_encode(Map)). + +-endif. +-endif. + +-ifndef(OTP_BELOW_27). + +%% OTP 27 or higher or higher +encode_map_test() -> + Map = #{name => <<"room">>, + service => <<"conference">>, + jid => jid:encode({<<"user">>, <<"server">>, <<"">>}), + affiliation => member}, + Encoded27 = + <<"{\"name\":\"room\",\"service\":\"conference\",\"jid\":\"user@server\",\"affiliation\":\"member\"}">>, + ?assertMatch(Encoded27, misc:json_encode(Map)). + +-endif. + +decode_test() -> + Encoded = + <<"{\"affiliation\":\"member\",\"jid\":\"user@server\",\"service\":\"conference\",\"name\":\"room\"}">>, + TupleList = + #{<<"affiliation">> => <<"member">>, + <<"jid">> => <<"user@server">>, + <<"name">> => <<"room">>, + <<"service">> => <<"conference">>}, + ?assertMatch(TupleList, misc:json_decode(Encoded)). + +decode_maps_test() -> + Encoded = + <<"{\"affiliation\":\"member\",\"jid\":\"user@server\",\"service\":\"conference\",\"name\":\"room\"}">>, + Map = #{<<"affiliation">> => misc:atom_to_binary(member), + <<"jid">> => jid:encode({<<"user">>, <<"server">>, <<"">>}), + <<"name">> => <<"room">>, + <<"service">> => <<"conference">>}, + ?assertMatch(Map, misc:json_decode(Encoded)). diff --git a/test/ldap_srv.erl b/test/ldap_srv.erl index 132f53e04..2d0cea988 100644 --- a/test/ldap_srv.erl +++ b/test/ldap_srv.erl @@ -1,11 +1,28 @@ %%%------------------------------------------------------------------- -%%% @author Evgeniy Khramtsov -%%% @copyright (C) 2013, Evgeniy Khramtsov -%%% @doc -%%% Simple LDAP server intended for LDAP modules testing -%%% @end +%%% Author : Evgeny Khramtsov %%% Created : 21 Jun 2013 by Evgeniy Khramtsov -%%%------------------------------------------------------------------- +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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. +%%% +%%%---------------------------------------------------------------------- + +%%% Simple LDAP server intended for LDAP modules testing + -module(ldap_srv). -behaviour(gen_server). @@ -28,7 +45,7 @@ -define(ERROR_MSG(Fmt, Args), error_logger:error_msg(Fmt, Args)). -define(TCP_SEND_TIMEOUT, 32000). --define(SERVER, ?MODULE). +-define(SERVER, ?MODULE). -record(state, {listener = make_ref() :: reference()}). @@ -54,7 +71,7 @@ init([LDIFFile]) -> case load_ldif(LDIFFile) of {ok, Tree} -> ?INFO_MSG("LDIF tree loaded, " - "ready to accept connections", []), + "ready to accept connections at ~B", [1389]), {_Pid, MRef} = spawn_monitor( fun() -> accept(ListenSocket, Tree) end @@ -105,7 +122,7 @@ accept(ListenSocket, Tree) -> process(Socket, Tree) -> case gen_tcp:recv(Socket, 0) of {ok, B} -> - case asn1rt:decode('ELDAPv3', 'LDAPMessage', B) of + case 'ELDAPv3':decode('LDAPMessage', B) of {ok, Msg} -> Replies = process_msg(Msg, Tree), Id = Msg#'LDAPMessage'.messageID, @@ -114,8 +131,8 @@ process(Socket, Tree) -> Reply = #'LDAPMessage'{messageID = Id, protocolOp = ReplyOp}, %%?DEBUG("sent:~n~p", [Reply]), - {ok, Bytes} = asn1rt:encode( - 'ELDAPv3', 'LDAPMessage', Reply), + {ok, Bytes} = 'ELDAPv3':encode( + 'LDAPMessage', Reply), gen_tcp:send(Socket, Bytes) end, Replies), process(Socket, Tree); diff --git a/test/mam_tests.erl b/test/mam_tests.erl new file mode 100644 index 000000000..27988bf5e --- /dev/null +++ b/test/mam_tests.erl @@ -0,0 +1,689 @@ +%%%------------------------------------------------------------------- +%%% Author : Evgeny Khramtsov +%%% Created : 14 Nov 2016 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(mam_tests). + +%% API +-compile(export_all). +-import(suite, [get_features/1, disconnect/1, my_jid/1, send_recv/2, + wait_for_slave/1, server_jid/1, send/2, get_features/2, + wait_for_master/1, recv_message/1, recv_iq/1, muc_room_jid/1, + muc_jid/1, is_feature_advertised/3, get_event/1, put_event/2]). + +-include("suite.hrl"). +-define(VERSIONS, [?NS_MAM_TMP, ?NS_MAM_0, ?NS_MAM_1, ?NS_MAM_2]). + +%%%=================================================================== +%%% API +%%%=================================================================== +%%%=================================================================== +%%% Single user tests +%%%=================================================================== +single_cases() -> + {mam_single, [sequence], + [single_test(feature_enabled), + single_test(get_set_prefs), + single_test(get_form), + single_test(fake_by)]}. + +feature_enabled(Config) -> + BareMyJID = jid:remove_resource(my_jid(Config)), + RequiredFeatures = sets:from_list(?VERSIONS), + ServerFeatures = sets:from_list(get_features(Config)), + UserFeatures = sets:from_list(get_features(Config, BareMyJID)), + MUCFeatures = get_features(Config, muc_jid(Config)), + ct:comment("Checking if all MAM server features are enabled"), + true = sets:is_subset(RequiredFeatures, ServerFeatures), + ct:comment("Checking if all MAM user features are enabled"), + true = sets:is_subset(RequiredFeatures, UserFeatures), + ct:comment("Checking if all MAM conference service features are enabled"), + true = lists:member(?NS_MAM_1, MUCFeatures), + true = lists:member(?NS_MAM_2, MUCFeatures), + clean(disconnect(Config)). + +fake_by(Config) -> + BareServerJID = server_jid(Config), + FullServerJID = jid:replace_resource(BareServerJID, p1_rand:get_string()), + FullMyJID = my_jid(Config), + BareMyJID = jid:remove_resource(FullMyJID), + Fakes = lists:flatmap( + fun(JID) -> + [#mam_archived{id = p1_rand:get_string(), by = JID}, + #stanza_id{id = p1_rand:get_string(), by = JID}] + end, [BareServerJID, FullServerJID, BareMyJID, FullMyJID]), + Body = xmpp:mk_text(<<"body">>), + ForeignJID = jid:make(p1_rand:get_string()), + Archived = #mam_archived{id = p1_rand:get_string(), by = ForeignJID}, + StanzaID = #stanza_id{id = p1_rand:get_string(), by = ForeignJID}, + #message{body = Body, sub_els = SubEls} = + send_recv(Config, #message{to = FullMyJID, + body = Body, + sub_els = [Archived, StanzaID|Fakes]}), + ct:comment("Checking if only foreign tags present"), + [ForeignJID, ForeignJID] = lists:flatmap( + fun(#mam_archived{by = By}) -> [By]; + (#stanza_id{by = By}) -> [By]; + (_) -> [] + end, SubEls), + clean(disconnect(Config)). + +get_set_prefs(Config) -> + Range = [{JID, #mam_prefs{xmlns = NS, + default = Default, + always = Always, + never = Never}} || + JID <- [undefined, server_jid(Config)], + NS <- ?VERSIONS, + Default <- [always, never, roster], + Always <- [[], [jid:decode(<<"foo@bar.baz">>)]], + Never <- [[], [jid:decode(<<"baz@bar.foo">>)]]], + lists:foreach( + fun({To, Prefs}) -> + NS = Prefs#mam_prefs.xmlns, + #iq{type = result, sub_els = [Prefs]} = + send_recv(Config, #iq{type = set, to = To, + sub_els = [Prefs]}), + #iq{type = result, sub_els = [Prefs]} = + send_recv(Config, #iq{type = get, to = To, + sub_els = [#mam_prefs{xmlns = NS}]}) + end, Range), + clean(disconnect(Config)). + +get_form(Config) -> + ServerJID = server_jid(Config), + Range = [{JID, NS} || JID <- [undefined, ServerJID], + NS <- ?VERSIONS -- [?NS_MAM_TMP]], + lists:foreach( + fun({To, NS}) -> + #iq{type = result, + sub_els = [#mam_query{xmlns = NS, + xdata = #xdata{} = X}]} = + send_recv(Config, #iq{type = get, to = To, + sub_els = [#mam_query{xmlns = NS}]}), + [NS] = xmpp_util:get_xdata_values(<<"FORM_TYPE">>, X), + true = xmpp_util:has_xdata_var(<<"with">>, X), + true = xmpp_util:has_xdata_var(<<"start">>, X), + true = xmpp_util:has_xdata_var(<<"end">>, X) + end, Range), + clean(disconnect(Config)). + +%%%=================================================================== +%%% Master-slave tests +%%%=================================================================== +master_slave_cases() -> + {mam_master_slave, [sequence], + [master_slave_test(archived_and_stanza_id), + master_slave_test(query_all), + master_slave_test(query_with), + master_slave_test(query_rsm_max), + master_slave_test(query_rsm_after), + master_slave_test(query_rsm_before), + master_slave_test(muc), + master_slave_test(mucsub), + master_slave_test(mucsub_from_muc), + master_slave_test(mucsub_from_muc_non_persistent)]}. + +archived_and_stanza_id_master(Config) -> + #presence{} = send_recv(Config, #presence{}), + wait_for_slave(Config), + send_messages(Config, lists:seq(1, 5)), + clean(disconnect(Config)). + +archived_and_stanza_id_slave(Config) -> + ok = set_default(Config, always), + #presence{} = send_recv(Config, #presence{}), + wait_for_master(Config), + recv_messages(Config, lists:seq(1, 5)), + clean(disconnect(Config)). + +query_all_master(Config) -> + Peer = ?config(peer, Config), + MyJID = my_jid(Config), + ok = set_default(Config, always), + #presence{} = send_recv(Config, #presence{}), + wait_for_slave(Config), + send_messages(Config, lists:seq(1, 5)), + query_all(Config, MyJID, Peer), + clean(disconnect(Config)). + +query_all_slave(Config) -> + Peer = ?config(peer, Config), + MyJID = my_jid(Config), + ok = set_default(Config, always), + #presence{} = send_recv(Config, #presence{}), + wait_for_master(Config), + recv_messages(Config, lists:seq(1, 5)), + query_all(Config, Peer, MyJID), + clean(disconnect(Config)). + +query_with_master(Config) -> + Peer = ?config(peer, Config), + MyJID = my_jid(Config), + ok = set_default(Config, always), + #presence{} = send_recv(Config, #presence{}), + wait_for_slave(Config), + send_messages(Config, lists:seq(1, 5)), + query_with(Config, MyJID, Peer), + clean(disconnect(Config)). + +query_with_slave(Config) -> + Peer = ?config(peer, Config), + MyJID = my_jid(Config), + ok = set_default(Config, always), + #presence{} = send_recv(Config, #presence{}), + wait_for_master(Config), + recv_messages(Config, lists:seq(1, 5)), + query_with(Config, Peer, MyJID), + clean(disconnect(Config)). + +query_rsm_max_master(Config) -> + Peer = ?config(peer, Config), + MyJID = my_jid(Config), + ok = set_default(Config, always), + #presence{} = send_recv(Config, #presence{}), + wait_for_slave(Config), + send_messages(Config, lists:seq(1, 5)), + query_rsm_max(Config, MyJID, Peer), + clean(disconnect(Config)). + +query_rsm_max_slave(Config) -> + Peer = ?config(peer, Config), + MyJID = my_jid(Config), + ok = set_default(Config, always), + #presence{} = send_recv(Config, #presence{}), + wait_for_master(Config), + recv_messages(Config, lists:seq(1, 5)), + query_rsm_max(Config, Peer, MyJID), + clean(disconnect(Config)). + +query_rsm_after_master(Config) -> + Peer = ?config(peer, Config), + MyJID = my_jid(Config), + ok = set_default(Config, always), + #presence{} = send_recv(Config, #presence{}), + wait_for_slave(Config), + send_messages(Config, lists:seq(1, 5)), + query_rsm_after(Config, MyJID, Peer), + clean(disconnect(Config)). + +query_rsm_after_slave(Config) -> + Peer = ?config(peer, Config), + MyJID = my_jid(Config), + ok = set_default(Config, always), + #presence{} = send_recv(Config, #presence{}), + wait_for_master(Config), + recv_messages(Config, lists:seq(1, 5)), + query_rsm_after(Config, Peer, MyJID), + clean(disconnect(Config)). + +query_rsm_before_master(Config) -> + Peer = ?config(peer, Config), + MyJID = my_jid(Config), + ok = set_default(Config, always), + #presence{} = send_recv(Config, #presence{}), + wait_for_slave(Config), + send_messages(Config, lists:seq(1, 5)), + query_rsm_before(Config, MyJID, Peer), + clean(disconnect(Config)). + +query_rsm_before_slave(Config) -> + Peer = ?config(peer, Config), + MyJID = my_jid(Config), + ok = set_default(Config, always), + #presence{} = send_recv(Config, #presence{}), + wait_for_master(Config), + recv_messages(Config, lists:seq(1, 5)), + query_rsm_before(Config, Peer, MyJID), + clean(disconnect(Config)). + +muc_master(Config) -> + Room = muc_room_jid(Config), + %% Joining + ok = muc_tests:join_new(Config), + %% MAM feature should not be advertised at this point, + %% because MAM is not enabled so far + false = is_feature_advertised(Config, ?NS_MAM_1, Room), + false = is_feature_advertised(Config, ?NS_MAM_2, Room), + %% Fill in some history + send_messages_to_room(Config, lists:seq(1, 21)), + %% We now should be able to retrieve those via MAM, even though + %% MAM is disabled. However, only last 20 messages should be received. + recv_messages_from_room(Config, lists:seq(2, 21)), + %% Now enable MAM for the conference + %% Retrieve config first + CfgOpts = muc_tests:get_config(Config), + %% Find the MAM field in the config + true = proplists:is_defined(mam, CfgOpts), + %% Enable MAM + [104] = muc_tests:set_config(Config, [{mam, true}]), + %% Check if MAM has been enabled + true = is_feature_advertised(Config, ?NS_MAM_1, Room), + true = is_feature_advertised(Config, ?NS_MAM_2, Room), + %% We now sending some messages again + send_messages_to_room(Config, lists:seq(1, 5)), + %% And retrieve them via MAM again. + recv_messages_from_room(Config, lists:seq(1, 5)), + put_event(Config, disconnect), + muc_tests:leave(Config), + clean(disconnect(Config)). + +muc_slave(Config) -> + disconnect = get_event(Config), + clean(disconnect(Config)). + +mucsub_master(Config) -> + Room = muc_room_jid(Config), + Peer = ?config(peer, Config), + wait_for_slave(Config), + ct:comment("Joining muc room"), + ok = muc_tests:join_new(Config), + + ct:comment("Enabling mam in room"), + CfgOpts = muc_tests:get_config(Config), + %% Find the MAM field in the config + ?match(true, proplists:is_defined(mam, CfgOpts)), + ?match(true, proplists:is_defined(allow_subscription, CfgOpts)), + %% Enable MAM + [104] = muc_tests:set_config(Config, [{mam, true}, {allow_subscription, true}]), + + ct:comment("Subscribing peer to room"), + ?send_recv(#iq{to = Room, type = set, sub_els = [ + #muc_subscribe{jid = Peer, nick = <<"peer">>, + events = [?NS_MUCSUB_NODES_MESSAGES]} + ]}, #iq{type = result}), + + ct:comment("Sending messages to room"), + send_messages_to_room(Config, lists:seq(1, 5)), + + ct:comment("Retrieving messages from room mam storage"), + recv_messages_from_room(Config, lists:seq(1, 5)), + + ct:comment("Cleaning up"), + put_event(Config, ready), + ready = get_event(Config), + muc_tests:leave(Config), + clean(disconnect(Config)). + +mucsub_slave(Config) -> + Room = muc_room_jid(Config), + MyJID = my_jid(Config), + MyJIDBare = jid:remove_resource(MyJID), + ok = set_default(Config, always), + send_recv(Config, #presence{}), + wait_for_master(Config), + + ct:comment("Receiving mucsub events"), + lists:foreach( + fun(N) -> + Body = xmpp:mk_text(integer_to_binary(N)), + Msg = ?match(#message{from = Room, type = normal} = Msg, recv_message(Config), Msg), + PS = ?match(#ps_event{items = #ps_items{node = ?NS_MUCSUB_NODES_MESSAGES, items = [ + #ps_item{} = PS + ]}}, xmpp:get_subtag(Msg, #ps_event{}), PS), + ?match(#message{type = groupchat, body = Body}, xmpp:get_subtag(PS, #message{})) + end, lists:seq(1, 5)), + + ct:comment("Retrieving personal mam archive"), + QID = p1_rand:get_string(), + I = send(Config, #iq{type = set, + sub_els = [#mam_query{xmlns = ?NS_MAM_2, id = QID}]}), + lists:foreach( + fun(N) -> + Body = xmpp:mk_text(integer_to_binary(N)), + Forw = ?match(#message{ + to = MyJID, from = MyJIDBare, + sub_els = [#mam_result{ + xmlns = ?NS_MAM_2, + queryid = QID, + sub_els = [#forwarded{ + delay = #delay{}} = Forw]}]}, + recv_message(Config), Forw), + IMsg = ?match(#message{ + to = MyJIDBare, from = Room} = IMsg, xmpp:get_subtag(Forw, #message{}), IMsg), + + PS = ?match(#ps_event{items = #ps_items{node = ?NS_MUCSUB_NODES_MESSAGES, items = [ + #ps_item{} = PS + ]}}, xmpp:get_subtag(IMsg, #ps_event{}), PS), + ?match(#message{type = groupchat, body = Body}, xmpp:get_subtag(PS, #message{})) + end, lists:seq(1, 5)), + RSM = ?match(#iq{from = MyJIDBare, id = I, type = result, + sub_els = [#mam_fin{xmlns = ?NS_MAM_2, + rsm = RSM, + complete = true}]}, recv_iq(Config), RSM), + match_rsm_count(RSM, 5), + + % Wait for master exit + ready = get_event(Config), + % Unsubscribe yourself + ?send_recv(#iq{to = Room, type = set, sub_els = [ + #muc_unsubscribe{} + ]}, #iq{type = result}), + put_event(Config, ready), + clean(disconnect(Config)). + +mucsub_from_muc_master(Config) -> + mucsub_master(Config). + +mucsub_from_muc_slave(Config) -> + Server = ?config(server, Config), + gen_mod:update_module(Server, mod_mam, #{user_mucsub_from_muc_archive => true}), + Config2 = mucsub_slave(Config), + gen_mod:update_module(Server, mod_mam, #{user_mucsub_from_muc_archive => false}), + Config2. + +mucsub_from_muc_non_persistent_master(Config) -> + Config1 = lists:keystore(persistent_room, 1, Config, {persistent_room, false}), + Config2 = mucsub_from_muc_master(Config1), + lists:keydelete(persistent_room, 1, Config2). + +mucsub_from_muc_non_persistent_slave(Config) -> + Config1 = lists:keystore(persistent_room, 1, Config, {persistent_room, false}), + Config2 = mucsub_from_muc_slave(Config1), + lists:keydelete(persistent_room, 1, Config2). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +single_test(T) -> + list_to_atom("mam_" ++ atom_to_list(T)). + +master_slave_test(T) -> + {list_to_atom("mam_" ++ atom_to_list(T)), [parallel], + [list_to_atom("mam_" ++ atom_to_list(T) ++ "_master"), + list_to_atom("mam_" ++ atom_to_list(T) ++ "_slave")]}. + +clean(Config) -> + {U, S, _} = jid:tolower(my_jid(Config)), + mod_mam:remove_user(U, S), + Config. + +set_default(Config, Default) -> + lists:foreach( + fun(NS) -> + ct:comment("Setting default preferences of '~s' to '~s'", + [NS, Default]), + #iq{type = result, + sub_els = [#mam_prefs{xmlns = NS, default = Default}]} = + send_recv(Config, #iq{type = set, + sub_els = [#mam_prefs{xmlns = NS, + default = Default}]}) + end, ?VERSIONS). + +send_messages(Config, Range) -> + Peer = ?config(peer, Config), + send_message_extra(Config, 0, <<"to-retract-1">>, []), + lists:foreach( + fun + (1) -> + send_message_extra(Config, 1, <<"retraction-1">>, [#message_retract{id = <<"to-retract-1">>}]); + (N) -> + Body = xmpp:mk_text(integer_to_binary(N)), + send(Config, #message{to = Peer, body = Body}) + end, Range). + +send_message_extra(Config, N, Id, Sub) -> + Peer = ?config(peer, Config), + Body = xmpp:mk_text(integer_to_binary(N)), + send(Config, #message{id = Id, to = Peer, body = Body, sub_els = Sub}). + +recv_messages(Config, Range) -> + Peer = ?config(peer, Config), + lists:foreach( + fun(N) -> + Body = xmpp:mk_text(integer_to_binary(N)), + #message{from = Peer, body = Body} = Msg = + recv_message(Config), + #mam_archived{by = BareMyJID} = + xmpp:get_subtag(Msg, #mam_archived{}), + #stanza_id{by = BareMyJID} = + xmpp:get_subtag(Msg, #stanza_id{}) + end, [0 | Range]). + +recv_archived_messages(Config, From, To, QID, Range) -> + MyJID = my_jid(Config), + lists:foreach( + fun(N) -> + ct:comment("Retrieving ~pth message in range ~p", + [N, Range]), + Body = xmpp:mk_text(integer_to_binary(N)), + #message{to = MyJID, + sub_els = + [#mam_result{ + queryid = QID, + sub_els = + [#forwarded{ + delay = #delay{}, + sub_els = [El]}]}]} = recv_message(Config), + #message{from = From, to = To, + body = Body} = xmpp:decode(El) + end, Range). + +maybe_recv_iq_result(Config, ?NS_MAM_0, I) -> + #iq{type = result, id = I} = recv_iq(Config); +maybe_recv_iq_result(_, _, _) -> + ok. + +query_iq_type(?NS_MAM_TMP) -> get; +query_iq_type(_) -> set. + +send_query(Config, #mam_query{xmlns = NS} = Query) -> + Type = query_iq_type(NS), + I = send(Config, #iq{type = Type, sub_els = [Query]}), + maybe_recv_iq_result(Config, NS, I), + I. + +recv_fin(Config, I, _QueryID, NS, IsComplete) when NS == ?NS_MAM_1; NS == ?NS_MAM_2 -> + ct:comment("Receiving fin iq for namespace '~s'", [NS]), + #iq{type = result, id = I, + sub_els = [#mam_fin{xmlns = NS, + complete = Complete, + rsm = RSM}]} = recv_iq(Config), + ct:comment("Checking if complete is ~s", [IsComplete]), + ?match(IsComplete, Complete), + RSM; +recv_fin(Config, I, QueryID, ?NS_MAM_TMP = NS, _IsComplete) -> + ct:comment("Receiving fin iq for namespace '~s'", [NS]), + #iq{type = result, id = I, + sub_els = [#mam_query{xmlns = NS, + rsm = RSM, + id = QueryID}]} = recv_iq(Config), + RSM; +recv_fin(Config, _, QueryID, ?NS_MAM_0 = NS, IsComplete) -> + ct:comment("Receiving fin message for namespace '~s'", [NS]), + #message{} = FinMsg = recv_message(Config), + #mam_fin{xmlns = NS, + id = QueryID, + complete = Complete, + rsm = RSM} = xmpp:get_subtag(FinMsg, #mam_fin{xmlns = NS}), + ct:comment("Checking if complete is ~s", [IsComplete]), + ?match(IsComplete, Complete), + RSM. + +send_messages_to_room(Config, Range) -> + MyNick = ?config(master_nick, Config), + Room = muc_room_jid(Config), + MyNickJID = jid:replace_resource(Room, MyNick), + lists:foreach( + fun(N) -> + Body = xmpp:mk_text(integer_to_binary(N)), + #message{from = MyNickJID, + type = groupchat, + body = Body} = + send_recv(Config, #message{to = Room, body = Body, + type = groupchat}) + end, Range). + +recv_messages_from_room(Config, Range) -> + MyNick = ?config(master_nick, Config), + Room = muc_room_jid(Config), + MyNickJID = jid:replace_resource(Room, MyNick), + MyJID = my_jid(Config), + QID = p1_rand:get_string(), + I = send(Config, #iq{type = set, to = Room, + sub_els = [#mam_query{xmlns = ?NS_MAM_2, id = QID}]}), + lists:foreach( + fun(N) -> + Body = xmpp:mk_text(integer_to_binary(N)), + #message{ + to = MyJID, from = Room, + sub_els = + [#mam_result{ + xmlns = ?NS_MAM_2, + queryid = QID, + sub_els = + [#forwarded{ + delay = #delay{}, + sub_els = [El]}]}]} = recv_message(Config), + #message{from = MyNickJID, + type = groupchat, + body = Body} = xmpp:decode(El) + end, Range), + #iq{from = Room, id = I, type = result, + sub_els = [#mam_fin{xmlns = ?NS_MAM_2, + rsm = RSM, + complete = true}]} = recv_iq(Config), + match_rsm_count(RSM, length(Range)). + +query_all(Config, From, To) -> + lists:foreach( + fun(NS) -> + query_all(Config, From, To, NS) + end, ?VERSIONS). + +query_all(Config, From, To, NS) -> + QID = p1_rand:get_string(), + Range = lists:seq(1, 5), + ID = send_query(Config, #mam_query{xmlns = NS, id = QID}), + recv_archived_messages(Config, From, To, QID, Range), + RSM = recv_fin(Config, ID, QID, NS, _Complete = true), + match_rsm_count(RSM, 5). + +query_with(Config, From, To) -> + lists:foreach( + fun(NS) -> + query_with(Config, From, To, NS) + end, ?VERSIONS). + +query_with(Config, From, To, NS) -> + Peer = ?config(peer, Config), + BarePeer = jid:remove_resource(Peer), + QID = p1_rand:get_string(), + Range = lists:seq(1, 5), + lists:foreach( + fun(JID) -> + ct:comment("Sending query with jid ~s", [jid:encode(JID)]), + Query = if NS == ?NS_MAM_TMP -> + #mam_query{xmlns = NS, with = JID, id = QID}; + true -> + Fs = mam_query:encode([{with, JID}]), + #mam_query{xmlns = NS, id = QID, + xdata = #xdata{type = submit, + fields = Fs}} + end, + ID = send_query(Config, Query), + recv_archived_messages(Config, From, To, QID, Range), + RSM = recv_fin(Config, ID, QID, NS, true), + match_rsm_count(RSM, 5) + end, [Peer, BarePeer]). + +query_rsm_max(Config, From, To) -> + lists:foreach( + fun(NS) -> + query_rsm_max(Config, From, To, NS) + end, ?VERSIONS). + +query_rsm_max(Config, From, To, NS) -> + lists:foreach( + fun(Max) -> + QID = p1_rand:get_string(), + Range = lists:sublist(lists:seq(1, Max), 5), + Query = #mam_query{xmlns = NS, id = QID, rsm = #rsm_set{max = Max}}, + ID = send_query(Config, Query), + recv_archived_messages(Config, From, To, QID, Range), + IsComplete = Max >= 5, + RSM = recv_fin(Config, ID, QID, NS, IsComplete), + match_rsm_count(RSM, 5) + end, lists:seq(0, 6)). + +query_rsm_after(Config, From, To) -> + lists:foreach( + fun(NS) -> + query_rsm_after(Config, From, To, NS) + end, ?VERSIONS). + +query_rsm_after(Config, From, To, NS) -> + lists:foldl( + fun(Range, #rsm_first{data = After}) -> + ct:comment("Retrieving ~p messages after '~s'", + [length(Range), After]), + QID = p1_rand:get_string(), + Query = #mam_query{xmlns = NS, id = QID, + rsm = #rsm_set{'after' = After}}, + ID = send_query(Config, Query), + recv_archived_messages(Config, From, To, QID, Range), + RSM = #rsm_set{first = First} = + recv_fin(Config, ID, QID, NS, true), + match_rsm_count(RSM, 5), + First + end, #rsm_first{data = undefined}, + [lists:seq(N, 5) || N <- lists:seq(1, 6)]). + +query_rsm_before(Config, From, To) -> + lists:foreach( + fun(NS) -> + query_rsm_before(Config, From, To, NS), + query_last_message(Config, From, To, NS) + end, ?VERSIONS). + +query_rsm_before(Config, From, To, NS) -> + lists:foldl( + fun(Range, Before) -> + ct:comment("Retrieving ~p messages before '~s'", + [length(Range), Before]), + QID = p1_rand:get_string(), + Query = #mam_query{xmlns = NS, id = QID, + rsm = #rsm_set{before = Before}}, + ID = send_query(Config, Query), + recv_archived_messages(Config, From, To, QID, Range), + RSM = #rsm_set{last = Last} = + recv_fin(Config, ID, QID, NS, true), + match_rsm_count(RSM, 5), + Last + end, <<"">>, lists:reverse([lists:seq(1, N) || N <- lists:seq(0, 5)])). + +query_last_message(Config, From, To, NS) -> + ct:comment("Retrieving last message", []), + QID = p1_rand:get_string(), + Query = #mam_query{xmlns = NS, id = QID, + rsm = #rsm_set{before = <<>>, max = 1}}, + ID = send_query(Config, Query), + recv_archived_messages(Config, From, To, QID, [5]), + RSM = ?match(#rsm_set{} = RSM, recv_fin(Config, ID, QID, NS, false), RSM), + match_rsm_count(RSM, 5). + +match_rsm_count(#rsm_set{count = undefined}, _) -> + %% The backend doesn't support counting + ok; +match_rsm_count(#rsm_set{count = Count1}, Count2) -> + ct:comment("Checking if RSM 'count' is ~p", [Count2]), + ?match(Count2, Count1). diff --git a/test/mod_configtest.erl b/test/mod_configtest.erl new file mode 100644 index 000000000..a4ca36f33 --- /dev/null +++ b/test/mod_configtest.erl @@ -0,0 +1,45 @@ +-module(mod_configtest). +-behaviour(gen_mod). + +-export([start/2, stop/1, reload/3, mod_opt_type/1, mod_options/1, depends/2, mod_doc/0]). + +start(_Host, _Opts) -> + ok. + +stop(_Host) -> + ok. + +reload(_Host, _NewOpts, _OldOpts) -> + ok. + +depends(_Host, _Opts) -> + []. + +mod_opt_type(mma) -> + econf:atom(); +mod_opt_type(mms) -> + econf:binary(); +mod_opt_type(mmsi) -> + econf:binary(); + +mod_opt_type(kma) -> + econf:atom(); +mod_opt_type(kms) -> + econf:binary(); +mod_opt_type(kmsi) -> + econf:binary(); + +mod_opt_type(predefined_keywords) -> + econf:binary(). + +mod_options(_) -> + [{mma, undefined}, + {mms, undefined}, + {mmsi, undefined}, + {kma, undefined}, + {kms, undefined}, + {kmsi, undefined}, + {predefined_keywords, undefined}]. + +mod_doc() -> + #{}. diff --git a/test/muc_tests.erl b/test/muc_tests.erl new file mode 100644 index 000000000..ae249691d --- /dev/null +++ b/test/muc_tests.erl @@ -0,0 +1,2008 @@ +%%%------------------------------------------------------------------- +%%% Author : Evgeny Khramtsov +%%% Created : 15 Oct 2016 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(muc_tests). + +%% API +-compile(export_all). +-import(suite, [recv_presence/1, send_recv/2, my_jid/1, muc_room_jid/1, + send/2, recv_message/1, recv_iq/1, muc_jid/1, + alt_room_jid/1, wait_for_slave/1, wait_for_master/1, + disconnect/1, put_event/2, get_event/1, peer_muc_jid/1, + my_muc_jid/1, get_features/2, set_opt/3]). +-include("suite.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +%%%=================================================================== +%%% Single tests +%%%=================================================================== +single_cases() -> + {muc_single, [sequence], + [single_test(service_presence_error), + single_test(service_message_error), + single_test(service_unknown_ns_iq_error), + single_test(service_iq_set_error), + single_test(service_improper_iq_error), + single_test(service_features), + single_test(service_disco_info_node_error), + single_test(service_disco_items), + single_test(service_unique), + single_test(service_vcard), + single_test(configure_non_existent), + single_test(cancel_configure_non_existent), + single_test(service_subscriptions), + single_test(set_room_affiliation)]}. + +service_presence_error(Config) -> + Service = muc_jid(Config), + ServiceResource = jid:replace_resource(Service, p1_rand:get_string()), + lists:foreach( + fun(To) -> + send(Config, #presence{type = error, to = To}), + lists:foreach( + fun(Type) -> + #presence{type = error} = Err = + send_recv(Config, #presence{type = Type, to = To}), + #stanza_error{reason = 'service-unavailable'} = + xmpp:get_error(Err) + end, [available, unavailable]) + end, [Service, ServiceResource]), + disconnect(Config). + +service_message_error(Config) -> + Service = muc_jid(Config), + send(Config, #message{type = error, to = Service}), + lists:foreach( + fun(Type) -> + #message{type = error} = Err1 = + send_recv(Config, #message{type = Type, to = Service}), + #stanza_error{reason = 'forbidden'} = xmpp:get_error(Err1) + end, [chat, normal, headline, groupchat]), + ServiceResource = jid:replace_resource(Service, p1_rand:get_string()), + send(Config, #message{type = error, to = ServiceResource}), + lists:foreach( + fun(Type) -> + #message{type = error} = Err2 = + send_recv(Config, #message{type = Type, to = ServiceResource}), + #stanza_error{reason = 'service-unavailable'} = xmpp:get_error(Err2) + end, [chat, normal, headline, groupchat]), + disconnect(Config). + +service_unknown_ns_iq_error(Config) -> + Service = muc_jid(Config), + ServiceResource = jid:replace_resource(Service, p1_rand:get_string()), + lists:foreach( + fun(To) -> + send(Config, #iq{type = result, to = To}), + send(Config, #iq{type = error, to = To}), + lists:foreach( + fun(Type) -> + #iq{type = error} = Err1 = + send_recv(Config, #iq{type = Type, to = To, + sub_els = [#presence{}]}), + #stanza_error{reason = 'service-unavailable'} = + xmpp:get_error(Err1) + end, [set, get]) + end, [Service, ServiceResource]), + disconnect(Config). + +service_iq_set_error(Config) -> + Service = muc_jid(Config), + lists:foreach( + fun(SubEl) -> + send(Config, #iq{type = result, to = Service, + sub_els = [SubEl]}), + #iq{type = error} = Err2 = + send_recv(Config, #iq{type = set, to = Service, + sub_els = [SubEl]}), + #stanza_error{reason = 'not-allowed'} = + xmpp:get_error(Err2) + end, [#disco_items{}, #disco_info{}, #vcard_temp{}, + #muc_unique{}, #muc_subscriptions{}]), + disconnect(Config). + +service_improper_iq_error(Config) -> + Service = muc_jid(Config), + lists:foreach( + fun(SubEl) -> + send(Config, #iq{type = result, to = Service, + sub_els = [SubEl]}), + lists:foreach( + fun(Type) -> + #iq{type = error} = Err3 = + send_recv(Config, #iq{type = Type, to = Service, + sub_els = [SubEl]}), + #stanza_error{reason = Reason} = xmpp:get_error(Err3), + true = Reason /= 'internal-server-error' + end, [set, get]) + end, [#disco_item{jid = Service}, + #identity{category = <<"category">>, type = <<"type">>}, + #vcard_email{}, #muc_subscribe{nick = ?config(nick, Config)}]), + disconnect(Config). + +service_features(Config) -> + ServerHost = ?config(server_host, Config), + MUC = muc_jid(Config), + Features = sets:from_list(get_features(Config, MUC)), + MAMFeatures = case gen_mod:is_loaded(ServerHost, mod_mam) of + true -> [?NS_MAM_TMP, ?NS_MAM_0, ?NS_MAM_1]; + false -> [] + end, + RequiredFeatures = sets:from_list( + [?NS_DISCO_INFO, ?NS_DISCO_ITEMS, + ?NS_REGISTER, ?NS_MUC, + ?NS_VCARD, ?NS_MUCSUB, ?NS_MUC_UNIQUE + | MAMFeatures]), + ct:comment("Checking if all needed disco features are set"), + true = sets:is_subset(RequiredFeatures, Features), + disconnect(Config). + +service_disco_info_node_error(Config) -> + MUC = muc_jid(Config), + Node = p1_rand:get_string(), + #iq{type = error} = Err = + send_recv(Config, #iq{type = get, to = MUC, + sub_els = [#disco_info{node = Node}]}), + #stanza_error{reason = 'item-not-found'} = xmpp:get_error(Err), + disconnect(Config). + +service_disco_items(Config) -> + #jid{server = Service} = muc_jid(Config), + Rooms = lists:sort( + lists:map( + fun(I) -> + RoomName = integer_to_binary(I), + jid:make(RoomName, Service) + end, lists:seq(1, 5))), + lists:foreach( + fun(Room) -> + ok = join_new(Config, Room) + end, Rooms), + Items = disco_items(Config), + Rooms = [J || #disco_item{jid = J} <- Items], + lists:foreach( + fun(Room) -> + ok = leave(Config, Room) + end, Rooms), + [] = disco_items(Config), + disconnect(Config). + +service_vcard(Config) -> + MUC = muc_jid(Config), + ct:comment("Retrieving vCard from ~s", [jid:encode(MUC)]), + VCard = mod_muc_opt:vcard(?config(server, Config)), + #iq{type = result, sub_els = [VCard]} = + send_recv(Config, #iq{type = get, to = MUC, sub_els = [#vcard_temp{}]}), + disconnect(Config). + +service_unique(Config) -> + MUC = muc_jid(Config), + ct:comment("Requesting muc unique from ~s", [jid:encode(MUC)]), + #iq{type = result, sub_els = [#muc_unique{name = Name}]} = + send_recv(Config, #iq{type = get, to = MUC, sub_els = [#muc_unique{}]}), + ct:comment("Checking if unique name is set in the response"), + <<_, _/binary>> = Name, + disconnect(Config). + +configure_non_existent(Config) -> + [_|_] = get_config(Config), + disconnect(Config). + +cancel_configure_non_existent(Config) -> + Room = muc_room_jid(Config), + #iq{type = result, sub_els = []} = + send_recv(Config, + #iq{to = Room, type = set, + sub_els = [#muc_owner{config = #xdata{type = cancel}}]}), + disconnect(Config). + +service_subscriptions(Config) -> + MUC = #jid{server = Service} = muc_jid(Config), + Rooms = lists:sort( + lists:map( + fun(I) -> + RoomName = integer_to_binary(I), + jid:make(RoomName, Service) + end, lists:seq(1, 5))), + lists:foreach( + fun(Room) -> + ok = join_new(Config, Room), + [104] = set_config(Config, [{allow_subscription, true}], Room), + [] = subscribe(Config, [], Room) + end, Rooms), + #iq{type = result, sub_els = [#muc_subscriptions{list = JIDs}]} = + send_recv(Config, #iq{type = get, to = MUC, + sub_els = [#muc_subscriptions{}]}), + Rooms = lists:sort([J || #muc_subscription{jid = J, events = []} <- JIDs]), + lists:foreach( + fun(Room) -> + ok = unsubscribe(Config, Room), + ok = leave(Config, Room) + end, Rooms), + disconnect(Config). + +set_room_affiliation(Config) -> + #jid{server = RoomService} = muc_jid(Config), + RoomName = <<"set_room_affiliation">>, + RoomJID = jid:make(RoomName, RoomService), + MyJID = my_jid(Config), + PeerJID = jid:remove_resource(?config(slave, Config)), + + ct:pal("joining room ~p", [RoomJID]), + ok = join_new(Config, RoomJID), + + ct:pal("setting affiliation in room ~p to 'member' for ~p", [RoomJID, PeerJID]), + ServerHost = ?config(server_host, Config), + WebPort = ct:get_config(web_port, 5280), + RequestURL = "http://" ++ ServerHost ++ ":" ++ integer_to_list(WebPort) ++ "/api/set_room_affiliation", + Headers = [{"X-Admin", "true"}], + ContentType = "application/json", + Body = misc:json_encode(#{room => RoomName, service => RoomService, + user => PeerJID#jid.luser, host => PeerJID#jid.lserver, + affiliation => member}), + {ok, {{_, 200, _}, _, _}} = httpc:request(post, {RequestURL, Headers, ContentType, Body}, [], []), + + #message{id = _, from = RoomJID, to = MyJID, sub_els = [ + #muc_user{items = [ + #muc_item{affiliation = member, role = none, jid = PeerJID}]}]} = recv_message(Config), + + ok = leave(Config, RoomJID), + disconnect(Config). + +%%%=================================================================== +%%% Master-slave tests +%%%=================================================================== +master_slave_cases() -> + {muc_master_slave, [sequence], + [master_slave_test(register), + master_slave_test(groupchat_msg), + master_slave_test(private_msg), + master_slave_test(set_subject), + master_slave_test(history), + master_slave_test(invite), + master_slave_test(invite_members_only), + master_slave_test(invite_password_protected), + master_slave_test(voice_request), + master_slave_test(change_role), + master_slave_test(kick), + master_slave_test(change_affiliation), + master_slave_test(destroy), + master_slave_test(vcard), + master_slave_test(nick_change), + master_slave_test(config_title_desc), + master_slave_test(config_public_list), + master_slave_test(config_password), + master_slave_test(config_whois), + master_slave_test(config_members_only), + master_slave_test(config_moderated), + master_slave_test(config_private_messages), + master_slave_test(config_query), + master_slave_test(config_allow_invites), + master_slave_test(config_visitor_status), + master_slave_test(config_allow_voice_requests), + master_slave_test(config_voice_request_interval), + master_slave_test(config_visitor_nickchange), + master_slave_test(join_conflict), + master_slave_test(duplicate_occupantid) + ]}. + +duplicate_occupantid_master(Config) -> + Room = muc_room_jid(Config), + PeerJID = ?config(slave, Config), + PeerNick = ?config(slave_nick, Config), + PeerNickJID = jid:replace_resource(Room, PeerNick), + ok = join_new(Config), + wait_for_slave(Config), + Pres = ?match(#presence{from = PeerNickJID, type = available} = Pres, + recv_presence(Config), Pres), + ?match(#muc_user{items = [#muc_item{jid = PeerJID, + role = participant, + affiliation = none}]}, + xmpp:get_subtag(Pres, #muc_user{})), + OccupantId = ?match([#occupant_id{id = Id}], xmpp:get_subtags(Pres, #occupant_id{}), Id), + Pres2 = ?match(#presence{from = PeerNickJID, type = available} = Pres2, + recv_presence(Config), Pres2), + ?match([#occupant_id{id = OccupantId}], xmpp:get_subtags(Pres2, #occupant_id{})), + Body = xmpp:mk_text(<<"test-1">>), + Msg = ?match(#message{type = groupchat, from = PeerNickJID, + body = Body} = Msg, recv_message(Config), Msg), + ?match([#occupant_id{id = OccupantId}], xmpp:get_subtags(Msg, #occupant_id{})), + recv_muc_presence(Config, PeerNickJID, unavailable), + ok = leave(Config), + disconnect(Config). + +duplicate_occupantid_slave(Config) -> + Room = muc_room_jid(Config), + MyNick = ?config(slave_nick, Config), + MyNickJID = jid:replace_resource(Room, MyNick), + PeerNick = ?config(master_nick, Config), + PeerNickJID = jid:replace_resource(Room, PeerNick), + wait_for_master(Config), + send(Config, #presence{to = MyNickJID, sub_els = [#muc{}]}), + ?match(#presence{from = Room, type = available}, recv_presence(Config)), + OccupantId = case recv_presence(Config) of + #presence{from = MyNickJID, type = available} = Pres -> + recv_muc_presence(Config, PeerNickJID, available), + ?match([#occupant_id{id = Id}], xmpp:get_subtags(Pres, #occupant_id{}), Id); + #presence{from = PeerNickJID, type = available} -> + Pres2 = ?match(#presence{from = MyNickJID, type = available} = Pres2, + recv_presence(Config), Pres2), + ?match([#occupant_id{id = Id}], xmpp:get_subtags(Pres2, #occupant_id{}), Id) + end, + ?match(#message{type = groupchat, from = Room}, recv_message(Config)), + send(Config, #presence{to = Room, sub_els = [#occupant_id{id = <<"fake1">>}, + #occupant_id{id = <<"fake2">>}]}), + Pres3 = ?match(#presence{from = MyNickJID, type = available} = Pres3, + recv_presence(Config), Pres3), + ?match([#occupant_id{id = OccupantId}], xmpp:get_subtags(Pres3, #occupant_id{})), + Body = xmpp:mk_text(<<"test-1">>), + send(Config, #message{to = Room, type = groupchat, body = Body, + sub_els = [#occupant_id{id = <<"fake1">>}, + #occupant_id{id = <<"fake2">>}]}), + Msg = ?match(#message{type = groupchat, from = MyNickJID, + body = Body} = Msg, recv_message(Config), Msg), + ?match([#occupant_id{id = OccupantId}], xmpp:get_subtags(Msg, #occupant_id{})), + ok = leave(Config), + disconnect(Config). + +join_conflict_master(Config) -> + ok = join_new(Config), + put_event(Config, join), + ct:comment("Waiting for 'leave' command from the slave"), + leave = get_event(Config), + ok = leave(Config), + disconnect(Config). + +join_conflict_slave(Config) -> + NewConfig = set_opt(nick, ?config(peer_nick, Config), Config), + ct:comment("Waiting for 'join' command from the master"), + join = get_event(Config), + ct:comment("Fail trying to join the room with conflicting nick"), + #stanza_error{reason = 'conflict'} = join(NewConfig), + put_event(Config, leave), + disconnect(NewConfig). + +groupchat_msg_master(Config) -> + Room = muc_room_jid(Config), + PeerJID = ?config(slave, Config), + PeerNick = ?config(slave_nick, Config), + PeerNickJID = jid:replace_resource(Room, PeerNick), + MyNick = ?config(nick, Config), + MyNickJID = jid:replace_resource(Room, MyNick), + ok = master_join(Config), + lists:foreach( + fun(I) -> + Body = xmpp:mk_text(integer_to_binary(I)), + send(Config, #message{type = groupchat, to = Room, + body = Body}), + #message{type = groupchat, from = MyNickJID, + body = Body} = recv_message(Config) + end, lists:seq(1, 5)), + #muc_user{items = [#muc_item{jid = PeerJID, + role = none, + affiliation = none}]} = + recv_muc_presence(Config, PeerNickJID, unavailable), + ok = leave(Config), + disconnect(Config). + +groupchat_msg_slave(Config) -> + Room = muc_room_jid(Config), + PeerNick = ?config(master_nick, Config), + PeerNickJID = jid:replace_resource(Room, PeerNick), + {[], _, _} = slave_join(Config), + lists:foreach( + fun(I) -> + Body = xmpp:mk_text(integer_to_binary(I)), + #message{type = groupchat, from = PeerNickJID, + body = Body} = recv_message(Config) + end, lists:seq(1, 5)), + ok = leave(Config), + disconnect(Config). + +private_msg_master(Config) -> + Room = muc_room_jid(Config), + PeerJID = ?config(slave, Config), + PeerNick = ?config(slave_nick, Config), + PeerNickJID = jid:replace_resource(Room, PeerNick), + ok = master_join(Config), + lists:foreach( + fun(I) -> + Body = xmpp:mk_text(integer_to_binary(I)), + send(Config, #message{type = chat, to = PeerNickJID, + body = Body}) + end, lists:seq(1, 5)), + #muc_user{items = [#muc_item{jid = PeerJID, + role = none, + affiliation = none}]} = + recv_muc_presence(Config, PeerNickJID, unavailable), + ct:comment("Fail trying to send a private message to non-existing occupant"), + send(Config, #message{type = chat, to = PeerNickJID}), + #message{from = PeerNickJID, type = error} = ErrMsg = recv_message(Config), + #stanza_error{reason = 'item-not-found'} = xmpp:get_error(ErrMsg), + ok = leave(Config), + disconnect(Config). + +private_msg_slave(Config) -> + Room = muc_room_jid(Config), + PeerNick = ?config(master_nick, Config), + PeerNickJID = jid:replace_resource(Room, PeerNick), + {[], _, _} = slave_join(Config), + lists:foreach( + fun(I) -> + Body = xmpp:mk_text(integer_to_binary(I)), + #message{type = chat, from = PeerNickJID, + body = Body} = recv_message(Config) + end, lists:seq(1, 5)), + ok = leave(Config), + disconnect(Config). + +set_subject_master(Config) -> + Room = muc_room_jid(Config), + PeerJID = ?config(slave, Config), + PeerNick = ?config(slave_nick, Config), + PeerNickJID = jid:replace_resource(Room, PeerNick), + Subject1 = xmpp:mk_text(?config(room_subject, Config)), + Subject2 = xmpp:mk_text(<<"new-", (?config(room_subject, Config))/binary>>), + ok = master_join(Config), + ct:comment("Setting 1st subject"), + send(Config, #message{type = groupchat, to = Room, + subject = Subject1}), + #message{type = groupchat, from = MyNickJID, + subject = Subject1} = recv_message(Config), + ct:comment("Waiting for the slave to leave"), + recv_muc_presence(Config, PeerNickJID, unavailable), + ct:comment("Setting 2nd subject"), + send(Config, #message{type = groupchat, to = Room, + subject = Subject2}), + #message{type = groupchat, from = MyNickJID, + subject = Subject2} = recv_message(Config), + ct:comment("Asking the slave to join"), + put_event(Config, join), + recv_muc_presence(Config, PeerNickJID, available), + ct:comment("Receiving 1st subject set by the slave"), + #message{type = groupchat, from = PeerNickJID, + subject = Subject1} = recv_message(Config), + ct:comment("Disallow subject change"), + [104] = set_config(Config, [{changesubject, false}]), + ct:comment("Waiting for the slave to leave"), + #muc_user{items = [#muc_item{jid = PeerJID, + role = none, + affiliation = none}]} = + recv_muc_presence(Config, PeerNickJID, unavailable), + ok = leave(Config), + disconnect(Config). + +set_subject_slave(Config) -> + Room = muc_room_jid(Config), + MyNickJID = my_muc_jid(Config), + PeerNick = ?config(master_nick, Config), + PeerNickJID = jid:replace_resource(Room, PeerNick), + Subject1 = xmpp:mk_text(?config(room_subject, Config)), + Subject2 = xmpp:mk_text(<<"new-", (?config(room_subject, Config))/binary>>), + {[], _, _} = slave_join(Config), + ct:comment("Receiving 1st subject set by the master"), + #message{type = groupchat, from = PeerNickJID, + subject = Subject1} = recv_message(Config), + ok = leave(Config), + ct:comment("Waiting for 'join' command from the master"), + join = get_event(Config), + {[], SubjMsg2, _} = join(Config), + ct:comment("Checking if the master has set 2nd subject during our absence"), + #message{type = groupchat, from = PeerNickJID, + subject = Subject2} = SubjMsg2, + ct:comment("Setting 1st subject"), + send(Config, #message{to = Room, type = groupchat, subject = Subject1}), + #message{type = groupchat, from = MyNickJID, + subject = Subject1} = recv_message(Config), + ct:comment("Waiting for the master to disallow subject change"), + [104] = recv_config_change_message(Config), + ct:comment("Fail trying to change the subject"), + send(Config, #message{to = Room, type = groupchat, subject = Subject2}), + #message{from = Room, type = error} = ErrMsg = recv_message(Config), + #stanza_error{reason = 'forbidden'} = xmpp:get_error(ErrMsg), + ok = leave(Config), + disconnect(Config). + +history_master(Config) -> + Room = muc_room_jid(Config), + ServerHost = ?config(server_host, Config), + MyNick = ?config(nick, Config), + MyNickJID = jid:replace_resource(Room, MyNick), + PeerNickJID = peer_muc_jid(Config), + Size = mod_muc_opt:history_size(iolist_to_binary(ServerHost)), + ok = join_new(Config), + ct:comment("Putting ~p+1 messages in the history", [Size]), + %% Only Size messages will be stored + lists:foreach( + fun(I) -> + Body = xmpp:mk_text(integer_to_binary(I)), + send(Config, #message{to = Room, type = groupchat, + body = Body}), + #message{type = groupchat, from = MyNickJID, + body = Body} = recv_message(Config) + end, lists:seq(0, Size)), + put_event(Config, join), + lists:foreach( + fun(Type) -> + recv_muc_presence(Config, PeerNickJID, Type) + end, [available, unavailable, + available, unavailable, + available, unavailable, + available, unavailable]), + ok = leave(Config), + disconnect(Config). + +history_slave(Config) -> + Room = muc_room_jid(Config), + PeerNick = ?config(peer_nick, Config), + PeerNickJID = jid:replace_resource(Room, PeerNick), + ServerHost = ?config(server_host, Config), + Size = mod_muc_opt:history_size(iolist_to_binary(ServerHost)), + ct:comment("Waiting for 'join' command from the master"), + join = get_event(Config), + {History, _, _} = join(Config), + ct:comment("Checking ordering of history events"), + BodyList = [binary_to_integer(xmpp:get_text(Body)) + || #message{type = groupchat, from = From, + body = Body} <- History, + From == PeerNickJID], + BodyList = lists:seq(1, Size), + ok = leave(Config), + %% If the client wishes to receive no history, it MUST set the 'maxchars' + %% attribute to a value of "0" (zero) + %% (http://xmpp.org/extensions/xep-0045.html#enter-managehistory) + ct:comment("Checking if maxchars=0 yields to no history"), + {[], _, _} = join(Config, #muc{history = #muc_history{maxchars = 0}}), + ok = leave(Config), + ct:comment("Receiving only 10 last stanzas"), + {History10, _, _} = join(Config, + #muc{history = #muc_history{maxstanzas = 10}}), + BodyList10 = [binary_to_integer(xmpp:get_text(Body)) + || #message{type = groupchat, from = From, + body = Body} <- History10, + From == PeerNickJID], + BodyList10 = lists:nthtail(Size-10, lists:seq(1, Size)), + ok = leave(Config), + #delay{stamp = TS} = xmpp:get_subtag(hd(History), #delay{}), + ct:comment("Receiving all history without the very first element"), + {HistoryWithoutFirst, _, _} = join(Config, + #muc{history = #muc_history{since = TS}}), + BodyListWithoutFirst = [binary_to_integer(xmpp:get_text(Body)) + || #message{type = groupchat, from = From, + body = Body} <- HistoryWithoutFirst, + From == PeerNickJID], + BodyListWithoutFirst = lists:nthtail(1, lists:seq(1, Size)), + ok = leave(Config), + disconnect(Config). + +invite_master(Config) -> + Room = muc_room_jid(Config), + PeerJID = ?config(peer, Config), + ok = join_new(Config), + wait_for_slave(Config), + %% Inviting the peer + send(Config, #message{to = Room, type = normal, + sub_els = + [#muc_user{ + invites = + [#muc_invite{to = PeerJID}]}]}), + #message{from = Room} = DeclineMsg = recv_message(Config), + #muc_user{decline = #muc_decline{from = PeerJID}} = + xmpp:get_subtag(DeclineMsg, #muc_user{}), + ok = leave(Config), + disconnect(Config). + +invite_slave(Config) -> + Room = muc_room_jid(Config), + wait_for_master(Config), + PeerJID = ?config(master, Config), + #message{from = Room, type = normal} = Msg = recv_message(Config), + #muc_user{invites = [#muc_invite{from = PeerJID}]} = + xmpp:get_subtag(Msg, #muc_user{}), + %% Decline invitation + send(Config, + #message{to = Room, + sub_els = [#muc_user{ + decline = #muc_decline{to = PeerJID}}]}), + disconnect(Config). + +invite_members_only_master(Config) -> + Room = muc_room_jid(Config), + PeerJID = ?config(slave, Config), + ok = join_new(Config), + %% Setting the room to members-only + [_|_] = set_config(Config, [{membersonly, true}]), + wait_for_slave(Config), + %% Inviting the peer + send(Config, #message{to = Room, type = normal, + sub_els = + [#muc_user{ + invites = + [#muc_invite{to = PeerJID}]}]}), + #message{from = Room, type = normal} = AffMsg = recv_message(Config), + #muc_user{items = [#muc_item{jid = PeerJID, affiliation = member}]} = + xmpp:get_subtag(AffMsg, #muc_user{}), + ok = leave(Config), + disconnect(Config). + +invite_members_only_slave(Config) -> + Room = muc_room_jid(Config), + wait_for_master(Config), + %% Receiving invitation + #message{from = Room, type = normal} = recv_message(Config), + disconnect(Config). + +invite_password_protected_master(Config) -> + Room = muc_room_jid(Config), + PeerJID = ?config(slave, Config), + Password = p1_rand:get_string(), + ok = join_new(Config), + [104] = set_config(Config, [{passwordprotectedroom, true}, + {roomsecret, Password}]), + put_event(Config, Password), + %% Inviting the peer + send(Config, #message{to = Room, type = normal, + sub_els = + [#muc_user{ + invites = + [#muc_invite{to = PeerJID}]}]}), + ok = leave(Config), + disconnect(Config). + +invite_password_protected_slave(Config) -> + Room = muc_room_jid(Config), + Password = get_event(Config), + %% Receiving invitation + #message{from = Room, type = normal} = Msg = recv_message(Config), + #muc_user{password = Password} = xmpp:get_subtag(Msg, #muc_user{}), + disconnect(Config). + +voice_request_master(Config) -> + Room = muc_room_jid(Config), + PeerJID = ?config(slave, Config), + PeerNick = ?config(slave_nick, Config), + PeerNickJID = jid:replace_resource(Room, PeerNick), + ok = join_new(Config), + [104] = set_config(Config, [{members_by_default, false}]), + wait_for_slave(Config), + #muc_user{ + items = [#muc_item{role = visitor, + jid = PeerJID, + affiliation = none}]} = + recv_muc_presence(Config, PeerNickJID, available), + ct:comment("Receiving voice request"), + #message{from = Room, type = normal} = VoiceReq = recv_message(Config), + #xdata{type = form, fields = Fs} = xmpp:get_subtag(VoiceReq, #xdata{}), + [{jid, PeerJID}, + {request_allow, false}, + {role, participant}, + {roomnick, PeerNick}] = lists:sort(muc_request:decode(Fs)), + ct:comment("Approving voice request"), + ApprovalFs = muc_request:encode([{jid, PeerJID}, {role, participant}, + {roomnick, PeerNick}, {request_allow, true}]), + send(Config, #message{to = Room, sub_els = [#xdata{type = submit, + fields = ApprovalFs}]}), + #muc_user{ + items = [#muc_item{role = participant, + jid = PeerJID, + affiliation = none}]} = + recv_muc_presence(Config, PeerNickJID, available), + ct:comment("Waiting for the slave to leave"), + recv_muc_presence(Config, PeerNickJID, unavailable), + ok = leave(Config), + disconnect(Config). + +voice_request_slave(Config) -> + Room = muc_room_jid(Config), + MyJID = my_jid(Config), + MyNick = ?config(nick, Config), + MyNickJID = jid:replace_resource(Room, MyNick), + wait_for_master(Config), + {[], _, _} = join(Config, visitor), + ct:comment("Requesting voice"), + Fs = muc_request:encode([{role, participant}]), + X = #xdata{type = submit, fields = Fs}, + send(Config, #message{to = Room, sub_els = [X]}), + ct:comment("Waiting to become a participant"), + #muc_user{ + items = [#muc_item{role = participant, + jid = MyJID, + affiliation = none}]} = + recv_muc_presence(Config, MyNickJID, available), + ok = leave(Config), + disconnect(Config). + +change_role_master(Config) -> + Room = muc_room_jid(Config), + MyJID = my_jid(Config), + MyNick = ?config(nick, Config), + PeerJID = ?config(slave, Config), + PeerNick = ?config(slave_nick, Config), + PeerNickJID = jid:replace_resource(Room, PeerNick), + ok = join_new(Config), + ct:comment("Waiting for the slave to join"), + wait_for_slave(Config), + #muc_user{items = [#muc_item{role = participant, + jid = PeerJID, + affiliation = none}]} = + recv_muc_presence(Config, PeerNickJID, available), + lists:foreach( + fun(Role) -> + ct:comment("Checking if the slave is not in the roles list"), + case get_role(Config, Role) of + [#muc_item{jid = MyJID, affiliation = owner, + role = moderator, nick = MyNick}] when Role == moderator -> + ok; + [] -> + ok + end, + Reason = p1_rand:get_string(), + put_event(Config, {Role, Reason}), + ok = set_role(Config, Role, Reason), + ct:comment("Receiving role change to ~s", [Role]), + #muc_user{ + items = [#muc_item{role = Role, + affiliation = none, + reason = Reason}]} = + recv_muc_presence(Config, PeerNickJID, available), + [#muc_item{role = Role, affiliation = none, + nick = PeerNick}|_] = get_role(Config, Role) + end, [visitor, participant, moderator]), + put_event(Config, disconnect), + recv_muc_presence(Config, PeerNickJID, unavailable), + ok = leave(Config), + disconnect(Config). + +change_role_slave(Config) -> + wait_for_master(Config), + {[], _, _} = join(Config), + change_role_slave(Config, get_event(Config)). + +change_role_slave(Config, {Role, Reason}) -> + Room = muc_room_jid(Config), + MyNick = ?config(slave_nick, Config), + MyNickJID = jid:replace_resource(Room, MyNick), + ct:comment("Receiving role change to ~s", [Role]), + #muc_user{status_codes = Codes, + items = [#muc_item{role = Role, + affiliation = none, + reason = Reason}]} = + recv_muc_presence(Config, MyNickJID, available), + true = lists:member(110, Codes), + change_role_slave(Config, get_event(Config)); +change_role_slave(Config, disconnect) -> + ok = leave(Config), + disconnect(Config). + +change_affiliation_master(Config) -> + Room = muc_room_jid(Config), + MyJID = my_jid(Config), + MyBareJID = jid:remove_resource(MyJID), + MyNick = ?config(nick, Config), + PeerJID = ?config(slave, Config), + PeerBareJID = jid:remove_resource(PeerJID), + PeerNick = ?config(slave_nick, Config), + PeerNickJID = jid:replace_resource(Room, PeerNick), + ok = join_new(Config), + ct:comment("Waiting for the slave to join"), + wait_for_slave(Config), + #muc_user{items = [#muc_item{role = participant, + jid = PeerJID, + affiliation = none}]} = + recv_muc_presence(Config, PeerNickJID, available), + lists:foreach( + fun({Aff, Role, Status}) -> + ct:comment("Checking if slave is not in affiliation list"), + case get_affiliation(Config, Aff) of + [#muc_item{jid = MyBareJID, + affiliation = owner}] when Aff == owner -> + ok; + [] -> + ok + end, + Reason = p1_rand:get_string(), + put_event(Config, {Aff, Role, Status, Reason}), + ok = set_affiliation(Config, Aff, Reason), + ct:comment("Receiving affiliation change to ~s", [Aff]), + #muc_user{ + items = [#muc_item{role = Role, + affiliation = Aff, + actor = Actor, + reason = Reason}]} = + recv_muc_presence(Config, PeerNickJID, Status), + if Aff == outcast -> + ct:comment("Checking if actor is set"), + #muc_actor{nick = MyNick} = Actor; + true -> + ok + end, + Affs = get_affiliation(Config, Aff), + ct:comment("Checking if the affiliation was correctly set"), + case lists:keyfind(PeerBareJID, #muc_item.jid, Affs) of + false when Aff == none -> + ok; + #muc_item{affiliation = Aff} -> + ok + end + end, [{member, participant, available}, {none, visitor, available}, + {admin, moderator, available}, {owner, moderator, available}, + {outcast, none, unavailable}]), + ok = leave(Config), + disconnect(Config). + +change_affiliation_slave(Config) -> + wait_for_master(Config), + {[], _, _} = join(Config), + change_affiliation_slave(Config, get_event(Config)). + +change_affiliation_slave(Config, {Aff, Role, Status, Reason}) -> + Room = muc_room_jid(Config), + PeerNick = ?config(master_nick, Config), + MyNick = ?config(nick, Config), + MyNickJID = jid:replace_resource(Room, MyNick), + ct:comment("Receiving affiliation change to ~s", [Aff]), + if Aff == outcast -> + #presence{from = Room, type = unavailable} = recv_presence(Config); + true -> + ok + end, + #muc_user{status_codes = Codes, + items = [#muc_item{role = Role, + actor = Actor, + affiliation = Aff, + reason = Reason}]} = + recv_muc_presence(Config, MyNickJID, Status), + true = lists:member(110, Codes), + if Aff == outcast -> + ct:comment("Checking for status code '301' (banned)"), + true = lists:member(301, Codes), + ct:comment("Checking if actor is set"), + #muc_actor{nick = PeerNick} = Actor, + disconnect(Config); + true -> + change_affiliation_slave(Config, get_event(Config)) + end. + +kick_master(Config) -> + Room = muc_room_jid(Config), + MyNick = ?config(nick, Config), + PeerJID = ?config(slave, Config), + PeerNick = ?config(slave_nick, Config), + PeerNickJID = jid:replace_resource(Room, PeerNick), + Reason = <<"Testing">>, + ok = join_new(Config), + ct:comment("Waiting for the slave to join"), + wait_for_slave(Config), + #muc_user{items = [#muc_item{role = participant, + jid = PeerJID, + affiliation = none}]} = + recv_muc_presence(Config, PeerNickJID, available), + [#muc_item{role = participant, affiliation = none, + nick = PeerNick}|_] = get_role(Config, participant), + ct:comment("Kicking slave"), + ok = set_role(Config, none, Reason), + ct:comment("Receiving role change to 'none'"), + #muc_user{ + status_codes = Codes, + items = [#muc_item{role = none, + affiliation = none, + actor = #muc_actor{nick = MyNick}, + reason = Reason}]} = + recv_muc_presence(Config, PeerNickJID, unavailable), + [] = get_role(Config, participant), + ct:comment("Checking if the code is '307' (kicked)"), + true = lists:member(307, Codes), + ok = leave(Config), + disconnect(Config). + +kick_slave(Config) -> + Room = muc_room_jid(Config), + PeerNick = ?config(master_nick, Config), + MyNick = ?config(nick, Config), + MyNickJID = jid:replace_resource(Room, MyNick), + Reason = <<"Testing">>, + wait_for_master(Config), + {[], _, _} = join(Config), + ct:comment("Receiving role change to 'none'"), + #presence{from = Room, type = unavailable} = recv_presence(Config), + #muc_user{status_codes = Codes, + items = [#muc_item{role = none, + affiliation = none, + actor = #muc_actor{nick = PeerNick}, + reason = Reason}]} = + recv_muc_presence(Config, MyNickJID, unavailable), + ct:comment("Checking if codes '110' (self-presence) " + "and '307' (kicked) are present"), + true = lists:member(110, Codes), + true = lists:member(307, Codes), + disconnect(Config). + +destroy_master(Config) -> + Reason = <<"Testing">>, + Room = muc_room_jid(Config), + AltRoom = alt_room_jid(Config), + PeerJID = ?config(peer, Config), + PeerNick = ?config(slave_nick, Config), + PeerNickJID = jid:replace_resource(Room, PeerNick), + MyNick = ?config(nick, Config), + MyNickJID = jid:replace_resource(Room, MyNick), + ok = join_new(Config), + ct:comment("Waiting for slave to join"), + wait_for_slave(Config), + #muc_user{items = [#muc_item{role = participant, + jid = PeerJID, + affiliation = none}]} = + recv_muc_presence(Config, PeerNickJID, available), + wait_for_slave(Config), + ok = destroy(Config, Reason), + ct:comment("Receiving destruction presence"), + #presence{from = Room, type = unavailable} = recv_presence(Config), + #muc_user{items = [#muc_item{role = none, + affiliation = none}], + destroy = #muc_destroy{jid = AltRoom, + reason = Reason}} = + recv_muc_presence(Config, MyNickJID, unavailable), + disconnect(Config). + +destroy_slave(Config) -> + Reason = <<"Testing">>, + Room = muc_room_jid(Config), + AltRoom = alt_room_jid(Config), + MyNick = ?config(nick, Config), + MyNickJID = jid:replace_resource(Room, MyNick), + wait_for_master(Config), + {[], _, _} = join(Config), + #stanza_error{reason = 'forbidden'} = destroy(Config, Reason), + wait_for_master(Config), + ct:comment("Receiving destruction presence"), + #presence{from = Room, type = unavailable} = recv_presence(Config), + #muc_user{items = [#muc_item{role = none, + affiliation = none}], + destroy = #muc_destroy{jid = AltRoom, + reason = Reason}} = + recv_muc_presence(Config, MyNickJID, unavailable), + disconnect(Config). + +vcard_master(Config) -> + Room = muc_room_jid(Config), + PeerNick = ?config(slave_nick, Config), + PeerNickJID = jid:replace_resource(Room, PeerNick), + FN = p1_rand:get_string(), + VCard = #vcard_temp{fn = FN}, + ok = join_new(Config), + ct:comment("Waiting for slave to join"), + wait_for_slave(Config), + #muc_user{items = [#muc_item{role = participant, + affiliation = none}]} = + recv_muc_presence(Config, PeerNickJID, available), + #stanza_error{reason = 'item-not-found'} = get_vcard(Config), + ok = set_vcard(Config, VCard), + VCard = get_vcard(Config), + put_event(Config, VCard), + recv_muc_presence(Config, PeerNickJID, unavailable), + leave = get_event(Config), + ok = leave(Config), + disconnect(Config). + +vcard_slave(Config) -> + wait_for_master(Config), + {[], _, _} = join(Config), + [104] = recv_config_change_message(Config), + VCard = get_event(Config), + VCard = get_vcard(Config), + #stanza_error{reason = 'forbidden'} = set_vcard(Config, VCard), + ok = leave(Config), + VCard = get_vcard(Config), + put_event(Config, leave), + disconnect(Config). + +nick_change_master(Config) -> + NewNick = p1_rand:get_string(), + PeerJID = ?config(peer, Config), + PeerNickJID = peer_muc_jid(Config), + ok = master_join(Config), + put_event(Config, {new_nick, NewNick}), + ct:comment("Waiting for nickchange presence from the slave"), + #muc_user{status_codes = Codes, + items = [#muc_item{jid = PeerJID, + nick = NewNick}]} = + recv_muc_presence(Config, PeerNickJID, unavailable), + ct:comment("Checking if code '303' (nick change) is set"), + true = lists:member(303, Codes), + ct:comment("Waiting for updated presence from the slave"), + PeerNewNickJID = jid:replace_resource(PeerNickJID, NewNick), + recv_muc_presence(Config, PeerNewNickJID, available), + ct:comment("Waiting for the slave to leave"), + recv_muc_presence(Config, PeerNewNickJID, unavailable), + ok = leave(Config), + disconnect(Config). + +nick_change_slave(Config) -> + MyJID = my_jid(Config), + MyNickJID = my_muc_jid(Config), + {[], _, _} = slave_join(Config), + {new_nick, NewNick} = get_event(Config), + MyNewNickJID = jid:replace_resource(MyNickJID, NewNick), + ct:comment("Sending new presence"), + send(Config, #presence{to = MyNewNickJID}), + ct:comment("Receiving nickchange self-presence"), + #muc_user{status_codes = Codes1, + items = [#muc_item{role = participant, + jid = MyJID, + nick = NewNick}]} = + recv_muc_presence(Config, MyNickJID, unavailable), + ct:comment("Checking if codes '110' (self-presence) and " + "'303' (nickchange) are present"), + lists:member(110, Codes1), + lists:member(303, Codes1), + ct:comment("Receiving self-presence update"), + #muc_user{status_codes = Codes2, + items = [#muc_item{jid = MyJID, + role = participant}]} = + recv_muc_presence(Config, MyNewNickJID, available), + ct:comment("Checking if code '110' (self-presence) is set"), + lists:member(110, Codes2), + NewConfig = set_opt(nick, NewNick, Config), + ok = leave(NewConfig), + disconnect(NewConfig). + +config_title_desc_master(Config) -> + Title = p1_rand:get_string(), + Desc = p1_rand:get_string(), + Room = muc_room_jid(Config), + PeerNick = ?config(slave_nick, Config), + PeerNickJID = jid:replace_resource(Room, PeerNick), + ok = master_join(Config), + [104] = set_config(Config, [{roomname, Title}, {roomdesc, Desc}]), + RoomCfg = get_config(Config), + Title = proplists:get_value(roomname, RoomCfg), + Desc = proplists:get_value(roomdesc, RoomCfg), + recv_muc_presence(Config, PeerNickJID, unavailable), + ok = leave(Config), + disconnect(Config). + +config_title_desc_slave(Config) -> + {[], _, _} = slave_join(Config), + [104] = recv_config_change_message(Config), + ok = leave(Config), + disconnect(Config). + +config_public_list_master(Config) -> + Room = muc_room_jid(Config), + PeerNick = ?config(slave_nick, Config), + PeerNickJID = jid:replace_resource(Room, PeerNick), + ok = join_new(Config), + wait_for_slave(Config), + recv_muc_presence(Config, PeerNickJID, available), + lists:member(<<"muc_public">>, get_features(Config, Room)), + [104] = set_config(Config, [{public_list, false}, + {publicroom, false}]), + recv_muc_presence(Config, PeerNickJID, unavailable), + lists:member(<<"muc_hidden">>, get_features(Config, Room)), + wait_for_slave(Config), + ok = leave(Config), + disconnect(Config). + +config_public_list_slave(Config) -> + Room = muc_room_jid(Config), + wait_for_master(Config), + PeerNick = ?config(peer_nick, Config), + PeerNickJID = peer_muc_jid(Config), + [#disco_item{jid = Room}] = disco_items(Config), + [#disco_item{jid = PeerNickJID, + name = PeerNick}] = disco_room_items(Config), + {[], _, _} = join(Config), + [104] = recv_config_change_message(Config), + ok = leave(Config), + [] = disco_items(Config), + [] = disco_room_items(Config), + wait_for_master(Config), + disconnect(Config). + +config_password_master(Config) -> + Password = p1_rand:get_string(), + Room = muc_room_jid(Config), + PeerNick = ?config(slave_nick, Config), + PeerNickJID = jid:replace_resource(Room, PeerNick), + ok = join_new(Config), + lists:member(<<"muc_unsecured">>, get_features(Config, Room)), + [104] = set_config(Config, [{passwordprotectedroom, true}, + {roomsecret, Password}]), + lists:member(<<"muc_passwordprotected">>, get_features(Config, Room)), + put_event(Config, Password), + recv_muc_presence(Config, PeerNickJID, available), + recv_muc_presence(Config, PeerNickJID, unavailable), + ok = leave(Config), + disconnect(Config). + +config_password_slave(Config) -> + Password = get_event(Config), + #stanza_error{reason = 'not-authorized'} = join(Config), + #stanza_error{reason = 'not-authorized'} = + join(Config, #muc{password = p1_rand:get_string()}), + {[], _, _} = join(Config, #muc{password = Password}), + ok = leave(Config), + disconnect(Config). + +config_whois_master(Config) -> + Room = muc_room_jid(Config), + PeerNickJID = peer_muc_jid(Config), + MyNickJID = my_muc_jid(Config), + ok = master_join(Config), + lists:member(<<"muc_semianonymous">>, get_features(Config, Room)), + [172] = set_config(Config, [{whois, anyone}]), + lists:member(<<"muc_nonanonymous">>, get_features(Config, Room)), + recv_muc_presence(Config, PeerNickJID, unavailable), + recv_muc_presence(Config, PeerNickJID, available), + send(Config, #presence{to = Room}), + recv_muc_presence(Config, MyNickJID, available), + [173] = set_config(Config, [{whois, moderators}]), + recv_muc_presence(Config, PeerNickJID, unavailable), + ok = leave(Config), + disconnect(Config). + +config_whois_slave(Config) -> + PeerJID = ?config(peer, Config), + PeerNickJID = peer_muc_jid(Config), + {[], _, _} = slave_join(Config), + ct:comment("Checking if the room becomes non-anonymous (code '172')"), + [172] = recv_config_change_message(Config), + ct:comment("Re-joining in order to check status codes"), + ok = leave(Config), + {[], _, Codes} = join(Config), + ct:comment("Checking if code '100' (non-anonymous) present"), + true = lists:member(100, Codes), + ct:comment("Receiving presence from peer with JID exposed"), + #muc_user{items = [#muc_item{jid = PeerJID}]} = + recv_muc_presence(Config, PeerNickJID, available), + ct:comment("Waiting for the room to become anonymous again (code '173')"), + [173] = recv_config_change_message(Config), + ok = leave(Config), + disconnect(Config). + +config_members_only_master(Config) -> + Room = muc_room_jid(Config), + PeerJID = ?config(peer, Config), + PeerBareJID = jid:remove_resource(PeerJID), + PeerNickJID = peer_muc_jid(Config), + ok = master_join(Config), + lists:member(<<"muc_open">>, get_features(Config, Room)), + [104] = set_config(Config, [{membersonly, true}]), + #muc_user{status_codes = Codes, + items = [#muc_item{jid = PeerJID, + affiliation = none, + role = none}]} = + recv_muc_presence(Config, PeerNickJID, unavailable), + ct:comment("Checking if code '322' (non-member) is set"), + true = lists:member(322, Codes), + lists:member(<<"muc_membersonly">>, get_features(Config, Room)), + ct:comment("Waiting for slave to fail joining the room"), + set_member = get_event(Config), + ok = set_affiliation(Config, member, p1_rand:get_string()), + #message{from = Room, type = normal} = Msg = recv_message(Config), + #muc_user{items = [#muc_item{jid = PeerBareJID, + affiliation = member}]} = + xmpp:get_subtag(Msg, #muc_user{}), + ct:comment("Asking peer to join"), + put_event(Config, join), + ct:comment("Waiting for peer to join"), + recv_muc_presence(Config, PeerNickJID, available), + ok = set_affiliation(Config, none, p1_rand:get_string()), + ct:comment("Waiting for peer to be kicked"), + #muc_user{status_codes = NewCodes, + items = [#muc_item{affiliation = none, + role = none}]} = + recv_muc_presence(Config, PeerNickJID, unavailable), + ct:comment("Checking if code '321' (became non-member in " + "members-only room) is set"), + true = lists:member(321, NewCodes), + ok = leave(Config), + disconnect(Config). + +config_members_only_slave(Config) -> + Room = muc_room_jid(Config), + MyJID = my_jid(Config), + MyNickJID = my_muc_jid(Config), + {[], _, _} = slave_join(Config), + [104] = recv_config_change_message(Config), + ct:comment("Getting kicked because the room has become members-only"), + #presence{from = Room, type = unavailable} = recv_presence(Config), + #muc_user{status_codes = Codes, + items = [#muc_item{jid = MyJID, + role = none, + affiliation = none}]} = + recv_muc_presence(Config, MyNickJID, unavailable), + ct:comment("Checking if the code '110' (self-presence) " + "and '322' (non-member) is set"), + true = lists:member(110, Codes), + true = lists:member(322, Codes), + ct:comment("Fail trying to join members-only room"), + #stanza_error{reason = 'registration-required'} = join(Config), + ct:comment("Asking the peer to set us member"), + put_event(Config, set_member), + ct:comment("Waiting for the peer to ask for join"), + join = get_event(Config), + {[], _, _} = join(Config, participant, member), + #presence{from = Room, type = unavailable} = recv_presence(Config), + #muc_user{status_codes = NewCodes, + items = [#muc_item{jid = MyJID, + role = none, + affiliation = none}]} = + recv_muc_presence(Config, MyNickJID, unavailable), + ct:comment("Checking if the code '110' (self-presence) " + "and '321' (became non-member in members-only room) is set"), + true = lists:member(110, NewCodes), + true = lists:member(321, NewCodes), + disconnect(Config). + +config_moderated_master(Config) -> + Room = muc_room_jid(Config), + PeerNickJID = peer_muc_jid(Config), + ok = master_join(Config), + lists:member(<<"muc_moderated">>, get_features(Config, Room)), + ok = set_role(Config, visitor, p1_rand:get_string()), + #muc_user{items = [#muc_item{role = visitor}]} = + recv_muc_presence(Config, PeerNickJID, available), + set_unmoderated = get_event(Config), + [104] = set_config(Config, [{moderatedroom, false}]), + #message{from = PeerNickJID, type = groupchat} = recv_message(Config), + recv_muc_presence(Config, PeerNickJID, unavailable), + lists:member(<<"muc_unmoderated">>, get_features(Config, Room)), + ok = leave(Config), + disconnect(Config). + +config_moderated_slave(Config) -> + Room = muc_room_jid(Config), + MyNickJID = my_muc_jid(Config), + {[], _, _} = slave_join(Config), + #muc_user{items = [#muc_item{role = visitor}]} = + recv_muc_presence(Config, MyNickJID, available), + send(Config, #message{to = Room, type = groupchat}), + ErrMsg = #message{from = Room, type = error} = recv_message(Config), + #stanza_error{reason = 'forbidden'} = xmpp:get_error(ErrMsg), + put_event(Config, set_unmoderated), + [104] = recv_config_change_message(Config), + send(Config, #message{to = Room, type = groupchat}), + #message{from = MyNickJID, type = groupchat} = recv_message(Config), + ok = leave(Config), + disconnect(Config). + +config_private_messages_master(Config) -> + PeerNickJID = peer_muc_jid(Config), + ok = master_join(Config), + ct:comment("Waiting for a private message from the slave"), + #message{from = PeerNickJID, type = chat} = recv_message(Config), + ok = set_role(Config, visitor, <<>>), + ct:comment("Waiting for the peer to become a visitor"), + recv_muc_presence(Config, PeerNickJID, available), + ct:comment("Waiting for a private message from the slave"), + #message{from = PeerNickJID, type = chat} = recv_message(Config), + [104] = set_config(Config, [{allow_private_messages_from_visitors, moderators}]), + ct:comment("Waiting for a private message from the slave"), + #message{from = PeerNickJID, type = chat} = recv_message(Config), + [104] = set_config(Config, [{allow_private_messages_from_visitors, nobody}]), + wait_for_slave(Config), + [104] = set_config(Config, [{allow_private_messages_from_visitors, anyone}, + {allowpm, none}]), + ct:comment("Fail trying to send a private message"), + send(Config, #message{to = PeerNickJID, type = chat}), + #message{from = PeerNickJID, type = error} = ErrMsg = recv_message(Config), + #stanza_error{reason = 'forbidden'} = xmpp:get_error(ErrMsg), + ok = set_role(Config, participant, <<>>), + ct:comment("Waiting for the peer to become a participant"), + recv_muc_presence(Config, PeerNickJID, available), + ct:comment("Waiting for the peer to leave"), + recv_muc_presence(Config, PeerNickJID, unavailable), + ok = leave(Config), + disconnect(Config). + +config_private_messages_slave(Config) -> + MyNickJID = my_muc_jid(Config), + PeerNickJID = peer_muc_jid(Config), + {[], _, _} = slave_join(Config), + ct:comment("Sending a private message"), + send(Config, #message{to = PeerNickJID, type = chat}), + ct:comment("Waiting to become a visitor"), + #muc_user{items = [#muc_item{role = visitor}]} = + recv_muc_presence(Config, MyNickJID, available), + ct:comment("Sending a private message"), + send(Config, #message{to = PeerNickJID, type = chat}), + [104] = recv_config_change_message(Config), + ct:comment("Sending a private message"), + send(Config, #message{to = PeerNickJID, type = chat}), + [104] = recv_config_change_message(Config), + ct:comment("Fail trying to send a private message"), + send(Config, #message{to = PeerNickJID, type = chat}), + #message{from = PeerNickJID, type = error} = ErrMsg1 = recv_message(Config), + #stanza_error{reason = 'forbidden'} = xmpp:get_error(ErrMsg1), + wait_for_master(Config), + [104] = recv_config_change_message(Config), + ct:comment("Waiting to become a participant again"), + #muc_user{items = [#muc_item{role = participant}]} = + recv_muc_presence(Config, MyNickJID, available), + ct:comment("Fail trying to send a private message"), + send(Config, #message{to = PeerNickJID, type = chat}), + #message{from = PeerNickJID, type = error} = ErrMsg2 = recv_message(Config), + #stanza_error{reason = 'forbidden'} = xmpp:get_error(ErrMsg2), + ok = leave(Config), + disconnect(Config). + +config_query_master(Config) -> + PeerNickJID = peer_muc_jid(Config), + ok = join_new(Config), + wait_for_slave(Config), + recv_muc_presence(Config, PeerNickJID, available), + ct:comment("Receiving IQ query from the slave"), + #iq{type = get, from = PeerNickJID, id = I, + sub_els = [#ping{}]} = recv_iq(Config), + send(Config, #iq{type = result, to = PeerNickJID, id = I}), + [104] = set_config(Config, [{allow_query_users, false}]), + ct:comment("Fail trying to send IQ"), + #iq{type = error, from = PeerNickJID} = Err = + send_recv(Config, #iq{type = get, to = PeerNickJID, + sub_els = [#ping{}]}), + #stanza_error{reason = 'not-allowed'} = xmpp:get_error(Err), + recv_muc_presence(Config, PeerNickJID, unavailable), + ok = leave(Config), + disconnect(Config). + +config_query_slave(Config) -> + PeerNickJID = peer_muc_jid(Config), + wait_for_master(Config), + ct:comment("Checking if IQ queries are denied from non-occupants"), + #iq{type = error, from = PeerNickJID} = Err1 = + send_recv(Config, #iq{type = get, to = PeerNickJID, + sub_els = [#ping{}]}), + #stanza_error{reason = 'not-acceptable'} = xmpp:get_error(Err1), + {[], _, _} = join(Config), + ct:comment("Sending IQ to the master"), + #iq{type = result, from = PeerNickJID, sub_els = []} = + send_recv(Config, #iq{to = PeerNickJID, type = get, sub_els = [#ping{}]}), + [104] = recv_config_change_message(Config), + ct:comment("Fail trying to send IQ"), + #iq{type = error, from = PeerNickJID} = Err2 = + send_recv(Config, #iq{type = get, to = PeerNickJID, + sub_els = [#ping{}]}), + #stanza_error{reason = 'not-allowed'} = xmpp:get_error(Err2), + ok = leave(Config), + disconnect(Config). + +config_allow_invites_master(Config) -> + Room = muc_room_jid(Config), + PeerJID = ?config(peer, Config), + PeerNickJID = peer_muc_jid(Config), + ok = master_join(Config), + [104] = set_config(Config, [{allowinvites, true}]), + ct:comment("Receiving an invitation from the slave"), + #message{from = Room, type = normal} = recv_message(Config), + [104] = set_config(Config, [{allowinvites, false}]), + send_invitation = get_event(Config), + ct:comment("Sending an invitation"), + send(Config, #message{to = Room, type = normal, + sub_els = + [#muc_user{ + invites = + [#muc_invite{to = PeerJID}]}]}), + recv_muc_presence(Config, PeerNickJID, unavailable), + ok = leave(Config), + disconnect(Config). + +config_allow_invites_slave(Config) -> + Room = muc_room_jid(Config), + PeerJID = ?config(peer, Config), + InviteMsg = #message{to = Room, type = normal, + sub_els = + [#muc_user{ + invites = + [#muc_invite{to = PeerJID}]}]}, + {[], _, _} = slave_join(Config), + [104] = recv_config_change_message(Config), + ct:comment("Sending an invitation"), + send(Config, InviteMsg), + [104] = recv_config_change_message(Config), + ct:comment("Fail sending an invitation"), + send(Config, InviteMsg), + #message{from = Room, type = error} = Err = recv_message(Config), + #stanza_error{reason = 'not-allowed'} = xmpp:get_error(Err), + ct:comment("Checking if the master is still able to send invitations"), + put_event(Config, send_invitation), + #message{from = Room, type = normal} = recv_message(Config), + ok = leave(Config), + disconnect(Config). + +config_visitor_status_master(Config) -> + PeerNickJID = peer_muc_jid(Config), + Status = xmpp:mk_text(p1_rand:get_string()), + ok = join_new(Config), + [104] = set_config(Config, [{members_by_default, false}]), + ct:comment("Asking the slave to join as a visitor"), + put_event(Config, {join, Status}), + #muc_user{items = [#muc_item{role = visitor}]} = + recv_muc_presence(Config, PeerNickJID, available), + ct:comment("Receiving status change from the visitor"), + #presence{from = PeerNickJID, status = Status} = recv_presence(Config), + [104] = set_config(Config, [{allow_visitor_status, false}]), + ct:comment("Receiving status change with stripped"), + #presence{from = PeerNickJID, status = []} = recv_presence(Config), + ct:comment("Waiting for the slave to leave"), + recv_muc_presence(Config, PeerNickJID, unavailable), + ok = leave(Config), + disconnect(Config). + +config_visitor_status_slave(Config) -> + Room = muc_room_jid(Config), + MyNickJID = my_muc_jid(Config), + ct:comment("Waiting for 'join' command from the master"), + {join, Status} = get_event(Config), + {[], _, _} = join(Config, visitor, none), + ct:comment("Sending status change"), + send(Config, #presence{to = Room, status = Status}), + #presence{from = MyNickJID, status = Status} = recv_presence(Config), + [104] = recv_config_change_message(Config), + ct:comment("Sending status change again"), + send(Config, #presence{to = Room, status = Status}), + #presence{from = MyNickJID, status = []} = recv_presence(Config), + ok = leave(Config), + disconnect(Config). + +config_allow_voice_requests_master(Config) -> + PeerNickJID = peer_muc_jid(Config), + ok = join_new(Config), + [104] = set_config(Config, [{members_by_default, false}]), + ct:comment("Asking the slave to join as a visitor"), + put_event(Config, join), + #muc_user{items = [#muc_item{role = visitor}]} = + recv_muc_presence(Config, PeerNickJID, available), + [104] = set_config(Config, [{allow_voice_requests, false}]), + ct:comment("Waiting for the slave to leave"), + recv_muc_presence(Config, PeerNickJID, unavailable), + ok = leave(Config), + disconnect(Config). + +config_allow_voice_requests_slave(Config) -> + Room = muc_room_jid(Config), + ct:comment("Waiting for 'join' command from the master"), + join = get_event(Config), + {[], _, _} = join(Config, visitor), + [104] = recv_config_change_message(Config), + ct:comment("Fail sending voice request"), + Fs = muc_request:encode([{role, participant}]), + X = #xdata{type = submit, fields = Fs}, + send(Config, #message{to = Room, sub_els = [X]}), + #message{from = Room, type = error} = Err = recv_message(Config), + #stanza_error{reason = 'forbidden'} = xmpp:get_error(Err), + ok = leave(Config), + disconnect(Config). + +config_voice_request_interval_master(Config) -> + Room = muc_room_jid(Config), + PeerJID = ?config(peer, Config), + PeerNick = ?config(peer_nick, Config), + PeerNickJID = peer_muc_jid(Config), + ok = join_new(Config), + [104] = set_config(Config, [{members_by_default, false}]), + ct:comment("Asking the slave to join as a visitor"), + put_event(Config, join), + #muc_user{items = [#muc_item{role = visitor}]} = + recv_muc_presence(Config, PeerNickJID, available), + [104] = set_config(Config, [{voice_request_min_interval, 5}]), + ct:comment("Receiving a voice request from slave"), + #message{from = Room, type = normal} = recv_message(Config), + ct:comment("Deny voice request at first"), + Fs = muc_request:encode([{jid, PeerJID}, {role, participant}, + {roomnick, PeerNick}, {request_allow, false}]), + send(Config, #message{to = Room, sub_els = [#xdata{type = submit, + fields = Fs}]}), + put_event(Config, denied), + ct:comment("Waiting for repeated voice request from the slave"), + #message{from = Room, type = normal} = recv_message(Config), + ct:comment("Waiting for the slave to leave"), + recv_muc_presence(Config, PeerNickJID, unavailable), + ok = leave(Config), + disconnect(Config). + +config_voice_request_interval_slave(Config) -> + Room = muc_room_jid(Config), + Fs = muc_request:encode([{role, participant}]), + X = #xdata{type = submit, fields = Fs}, + ct:comment("Waiting for 'join' command from the master"), + join = get_event(Config), + {[], _, _} = join(Config, visitor), + [104] = recv_config_change_message(Config), + ct:comment("Sending voice request"), + send(Config, #message{to = Room, sub_els = [X]}), + ct:comment("Waiting for the master to deny our voice request"), + denied = get_event(Config), + ct:comment("Requesting voice again"), + send(Config, #message{to = Room, sub_els = [X]}), + ct:comment("Receiving voice request error because we're sending to fast"), + #message{from = Room, type = error} = Err = recv_message(Config), + #stanza_error{reason = 'resource-constraint'} = xmpp:get_error(Err), + ct:comment("Waiting for 5 seconds"), + timer:sleep(timer:seconds(5)), + ct:comment("Repeating again"), + send(Config, #message{to = Room, sub_els = [X]}), + ok = leave(Config), + disconnect(Config). + +config_visitor_nickchange_master(Config) -> + PeerNickJID = peer_muc_jid(Config), + ok = join_new(Config), + [104] = set_config(Config, [{members_by_default, false}]), + ct:comment("Asking the slave to join as a visitor"), + put_event(Config, join), + ct:comment("Waiting for the slave to join"), + #muc_user{items = [#muc_item{role = visitor}]} = + recv_muc_presence(Config, PeerNickJID, available), + [104] = set_config(Config, [{allow_visitor_nickchange, false}]), + ct:comment("Waiting for the slave to leave"), + recv_muc_presence(Config, PeerNickJID, unavailable), + ok = leave(Config), + disconnect(Config). + +config_visitor_nickchange_slave(Config) -> + NewNick = p1_rand:get_string(), + MyNickJID = my_muc_jid(Config), + MyNewNickJID = jid:replace_resource(MyNickJID, NewNick), + ct:comment("Waiting for 'join' command from the master"), + join = get_event(Config), + {[], _, _} = join(Config, visitor), + [104] = recv_config_change_message(Config), + ct:comment("Fail trying to change nickname"), + send(Config, #presence{to = MyNewNickJID}), + #presence{from = MyNewNickJID, type = error} = Err = recv_presence(Config), + #stanza_error{reason = 'not-allowed'} = xmpp:get_error(Err), + ok = leave(Config), + disconnect(Config). + +register_master(Config) -> + MUC = muc_jid(Config), + %% Register nick "master1" + register_nick(Config, MUC, <<"">>, <<"master1">>), + %% Unregister nick "master1" via jabber:register + #iq{type = result, sub_els = []} = + send_recv(Config, #iq{type = set, to = MUC, + sub_els = [#register{remove = true}]}), + %% Register nick "master2" + register_nick(Config, MUC, <<"">>, <<"master2">>), + %% Now register nick "master" + register_nick(Config, MUC, <<"master2">>, <<"master">>), + %% Wait for slave to fail trying to register nick "master" + wait_for_slave(Config), + wait_for_slave(Config), + %% Now register empty ("") nick, which means we're unregistering + register_nick(Config, MUC, <<"master">>, <<"">>), + disconnect(Config). + +register_slave(Config) -> + MUC = muc_jid(Config), + wait_for_master(Config), + %% Trying to register occupied nick "master" + Fs = muc_register:encode([{roomnick, <<"master">>}]), + X = #xdata{type = submit, fields = Fs}, + #iq{type = error} = + send_recv(Config, #iq{type = set, to = MUC, + sub_els = [#register{xdata = X}]}), + wait_for_master(Config), + disconnect(Config). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +single_test(T) -> + list_to_atom("muc_" ++ atom_to_list(T)). + +master_slave_test(T) -> + {list_to_atom("muc_" ++ atom_to_list(T)), [parallel], + [list_to_atom("muc_" ++ atom_to_list(T) ++ "_master"), + list_to_atom("muc_" ++ atom_to_list(T) ++ "_slave")]}. + +recv_muc_presence(Config, From, Type) -> + Pres = #presence{from = From, type = Type} = recv_presence(Config), + xmpp:get_subtag(Pres, #muc_user{}). + +join_new(Config) -> + join_new(Config, muc_room_jid(Config)). + +join_new(Config, Room) -> + MyJID = my_jid(Config), + MyNick = ?config(nick, Config), + MyNickJID = jid:replace_resource(Room, MyNick), + ct:comment("Joining new room ~p", [Room]), + send(Config, #presence{to = MyNickJID, sub_els = [#muc{}]}), + #presence{from = Room, type = available} = recv_presence(Config), + %% As per XEP-0045 we MUST receive stanzas in the following order: + %% 1. In-room presence from other occupants + %% 2. In-room presence from the joining entity itself (so-called "self-presence") + %% 3. Room history (if any) + %% 4. The room subject + %% 5. Live messages, presence updates, new user joins, etc. + %% As this is the newly created room, we receive only the 2nd and 4th stanza. + #muc_user{ + status_codes = Codes, + items = [#muc_item{role = moderator, + jid = MyJID, + affiliation = owner}]} = + recv_muc_presence(Config, MyNickJID, available), + ct:comment("Checking if codes '110' (self-presence) and " + "'201' (new room) is set"), + true = lists:member(110, Codes), + true = lists:member(201, Codes), + ct:comment("Receiving empty room subject"), + #message{from = Room, type = groupchat, body = [], + subject = [#text{data = <<>>}]} = recv_message(Config), + case ?config(persistent_room, Config) of + true -> + [104] = set_config(Config, [{persistentroom, true}], Room), + ok; + false -> + ok + end. + +recv_history_and_subject(Config) -> + ct:comment("Receiving room history and/or subject"), + recv_history_and_subject(Config, []). + +recv_history_and_subject(Config, History) -> + Room = muc_room_jid(Config), + #message{type = groupchat, subject = Subj, + body = Body, thread = Thread} = Msg = recv_message(Config), + case xmpp:get_subtag(Msg, #delay{}) of + #delay{from = Room} -> + recv_history_and_subject(Config, [Msg|History]); + false when Subj /= [], Body == [], Thread == undefined -> + {lists:reverse(History), Msg} + end. + +join(Config) -> + join(Config, participant, none, #muc{}). + +join(Config, Role) when is_atom(Role) -> + join(Config, Role, none, #muc{}); +join(Config, #muc{} = SubEl) -> + join(Config, participant, none, SubEl). + +join(Config, Role, Aff) when is_atom(Role), is_atom(Aff) -> + join(Config, Role, Aff, #muc{}); +join(Config, Role, #muc{} = SubEl) when is_atom(Role) -> + join(Config, Role, none, SubEl). + +join(Config, Role, Aff, SubEls) when is_list(SubEls) -> + ct:comment("Joining existing room as ~s/~s", [Aff, Role]), + MyJID = my_jid(Config), + Room = muc_room_jid(Config), + MyNick = ?config(nick, Config), + MyNickJID = jid:replace_resource(Room, MyNick), + PeerNick = ?config(peer_nick, Config), + PeerNickJID = jid:replace_resource(Room, PeerNick), + send(Config, #presence{to = MyNickJID, sub_els = SubEls}), + case recv_presence(Config) of + #presence{type = error, from = MyNickJID} = Err -> + xmpp:get_subtag(Err, #stanza_error{}); + #presence{from = Room, type = available} -> + case recv_presence(Config) of + #presence{type = available, from = PeerNickJID} = Pres -> + #muc_user{items = [#muc_item{role = moderator, + affiliation = owner}]} = + xmpp:get_subtag(Pres, #muc_user{}), + ct:comment("Receiving initial self-presence"), + #muc_user{status_codes = Codes, + items = [#muc_item{role = Role, + jid = MyJID, + affiliation = Aff}]} = + recv_muc_presence(Config, MyNickJID, available), + ct:comment("Checking if code '110' (self-presence) is set"), + true = lists:member(110, Codes), + {History, Subj} = recv_history_and_subject(Config), + {History, Subj, Codes}; + #presence{type = available, from = MyNickJID} = Pres -> + #muc_user{status_codes = Codes, + items = [#muc_item{role = Role, + jid = MyJID, + affiliation = Aff}]} = + xmpp:get_subtag(Pres, #muc_user{}), + ct:comment("Checking if code '110' (self-presence) is set"), + true = lists:member(110, Codes), + {History, Subj} = recv_history_and_subject(Config), + {empty, History, Subj, Codes} + end + end; +join(Config, Role, Aff, SubEl) -> + join(Config, Role, Aff, [SubEl]). + +leave(Config) -> + leave(Config, muc_room_jid(Config)). + +leave(Config, Room) -> + MyJID = my_jid(Config), + MyNick = ?config(nick, Config), + MyNickJID = jid:replace_resource(Room, MyNick), + Mode = ?config(mode, Config), + IsPersistent = ?config(persistent_room, Config), + if Mode /= slave, IsPersistent -> + [104] = set_config(Config, [{persistentroom, false}], Room); + true -> + ok + end, + ct:comment("Leaving the room"), + send(Config, #presence{to = MyNickJID, type = unavailable}), + #presence{from = Room, type = unavailable} = recv_presence(Config), + #muc_user{ + status_codes = Codes, + items = [#muc_item{role = none, jid = MyJID}]} = + recv_muc_presence(Config, MyNickJID, unavailable), + ct:comment("Checking if code '110' (self-presence) is set"), + true = lists:member(110, Codes), + ok. + +get_config(Config) -> + ct:comment("Get room config"), + Room = muc_room_jid(Config), + case send_recv(Config, + #iq{type = get, to = Room, + sub_els = [#muc_owner{}]}) of + #iq{type = result, + sub_els = [#muc_owner{config = #xdata{type = form} = X}]} -> + muc_roomconfig:decode(X#xdata.fields); + #iq{type = error} = Err -> + xmpp:get_subtag(Err, #stanza_error{}) + end. + +set_config(Config, RoomConfig) -> + set_config(Config, RoomConfig, muc_room_jid(Config)). + +set_config(Config, RoomConfig, Room) -> + ct:comment("Set room config: ~p", [RoomConfig]), + Fs = case RoomConfig of + [] -> []; + _ -> muc_roomconfig:encode(RoomConfig) + end, + case send_recv(Config, + #iq{type = set, to = Room, + sub_els = [#muc_owner{config = #xdata{type = submit, + fields = Fs}}]}) of + #iq{type = result, sub_els = []} -> + #presence{from = Room, type = available} = recv_presence(Config), + #message{from = Room, type = groupchat} = Msg = recv_message(Config), + #muc_user{status_codes = Codes} = xmpp:get_subtag(Msg, #muc_user{}), + lists:sort(Codes); + #iq{type = error} = Err -> + xmpp:get_subtag(Err, #stanza_error{}) + end. + +create_persistent(Config) -> + [_|_] = get_config(Config), + [] = set_config(Config, [{persistentroom, true}], false), + ok. + +destroy(Config) -> + destroy(Config, <<>>). + +destroy(Config, Reason) -> + Room = muc_room_jid(Config), + AltRoom = alt_room_jid(Config), + ct:comment("Destroying a room"), + case send_recv(Config, + #iq{type = set, to = Room, + sub_els = [#muc_owner{destroy = #muc_destroy{ + reason = Reason, + jid = AltRoom}}]}) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = Err -> + xmpp:get_subtag(Err, #stanza_error{}) + end. + +disco_items(Config) -> + MUC = muc_jid(Config), + ct:comment("Performing disco#items request to ~s", [jid:encode(MUC)]), + #iq{type = result, from = MUC, sub_els = [DiscoItems]} = + send_recv(Config, #iq{type = get, to = MUC, + sub_els = [#disco_items{}]}), + lists:keysort(#disco_item.jid, DiscoItems#disco_items.items). + +disco_room_items(Config) -> + Room = muc_room_jid(Config), + #iq{type = result, from = Room, sub_els = [DiscoItems]} = + send_recv(Config, #iq{type = get, to = Room, + sub_els = [#disco_items{}]}), + DiscoItems#disco_items.items. + +get_affiliations(Config, Aff) -> + Room = muc_room_jid(Config), + case send_recv(Config, + #iq{type = get, to = Room, + sub_els = [#muc_admin{items = [#muc_item{affiliation = Aff}]}]}) of + #iq{type = result, sub_els = [#muc_admin{items = Items}]} -> + Items; + #iq{type = error} = Err -> + xmpp:get_subtag(Err, #stanza_error{}) + end. + +master_join(Config) -> + Room = muc_room_jid(Config), + PeerJID = ?config(slave, Config), + PeerNick = ?config(slave_nick, Config), + PeerNickJID = jid:replace_resource(Room, PeerNick), + ok = join_new(Config), + wait_for_slave(Config), + #muc_user{items = [#muc_item{jid = PeerJID, + role = participant, + affiliation = none}]} = + recv_muc_presence(Config, PeerNickJID, available), + ok. + +slave_join(Config) -> + wait_for_master(Config), + join(Config). + +set_role(Config, Role, Reason) -> + ct:comment("Changing role to ~s", [Role]), + Room = muc_room_jid(Config), + PeerNick = ?config(slave_nick, Config), + case send_recv( + Config, + #iq{type = set, to = Room, + sub_els = + [#muc_admin{ + items = [#muc_item{role = Role, + reason = Reason, + nick = PeerNick}]}]}) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = Err -> + xmpp:get_subtag(Err, #stanza_error{}) + end. + +get_role(Config, Role) -> + ct:comment("Requesting list for role '~s'", [Role]), + Room = muc_room_jid(Config), + case send_recv( + Config, + #iq{type = get, to = Room, + sub_els = [#muc_admin{ + items = [#muc_item{role = Role}]}]}) of + #iq{type = result, sub_els = [#muc_admin{items = Items}]} -> + lists:keysort(#muc_item.affiliation, Items); + #iq{type = error} = Err -> + xmpp:get_subtag(Err, #stanza_error{}) + end. + +set_affiliation(Config, Aff, Reason) -> + ct:comment("Changing affiliation to ~s", [Aff]), + Room = muc_room_jid(Config), + PeerJID = ?config(slave, Config), + PeerBareJID = jid:remove_resource(PeerJID), + case send_recv( + Config, + #iq{type = set, to = Room, + sub_els = + [#muc_admin{ + items = [#muc_item{affiliation = Aff, + reason = Reason, + jid = PeerBareJID}]}]}) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = Err -> + xmpp:get_subtag(Err, #stanza_error{}) + end. + +get_affiliation(Config, Aff) -> + ct:comment("Requesting list for affiliation '~s'", [Aff]), + Room = muc_room_jid(Config), + case send_recv( + Config, + #iq{type = get, to = Room, + sub_els = [#muc_admin{ + items = [#muc_item{affiliation = Aff}]}]}) of + #iq{type = result, sub_els = [#muc_admin{items = Items}]} -> + Items; + #iq{type = error} = Err -> + xmpp:get_subtag(Err, #stanza_error{}) + end. + +set_vcard(Config, VCard) -> + Room = muc_room_jid(Config), + ct:comment("Setting vCard for ~s", [jid:encode(Room)]), + case send_recv(Config, #iq{type = set, to = Room, + sub_els = [VCard]}) of + #iq{type = result, sub_els = []} -> + [104] = recv_config_change_message(Config), + ok; + #iq{type = error} = Err -> + xmpp:get_subtag(Err, #stanza_error{}) + end. + +get_vcard(Config) -> + Room = muc_room_jid(Config), + ct:comment("Retrieving vCard from ~s", [jid:encode(Room)]), + case send_recv(Config, #iq{type = get, to = Room, + sub_els = [#vcard_temp{}]}) of + #iq{type = result, sub_els = [VCard]} -> + VCard; + #iq{type = error} = Err -> + xmpp:get_subtag(Err, #stanza_error{}) + end. + +recv_config_change_message(Config) -> + ct:comment("Receiving configuration change notification message"), + Room = muc_room_jid(Config), + #presence{from = Room, type = available} = recv_presence(Config), + #message{type = groupchat, from = Room} = Msg = recv_message(Config), + #muc_user{status_codes = Codes} = xmpp:get_subtag(Msg, #muc_user{}), + lists:sort(Codes). + +register_nick(Config, MUC, PrevNick, Nick) -> + PrevRegistered = if PrevNick /= <<"">> -> true; + true -> false + end, + NewRegistered = if Nick /= <<"">> -> true; + true -> false + end, + ct:comment("Requesting registration form"), + #iq{type = result, + sub_els = [#register{registered = PrevRegistered, + xdata = #xdata{type = form, + fields = FsWithoutNick}}]} = + send_recv(Config, #iq{type = get, to = MUC, + sub_els = [#register{}]}), + ct:comment("Checking if previous nick is registered"), + PrevNick = proplists:get_value( + roomnick, muc_register:decode(FsWithoutNick)), + X = #xdata{type = submit, fields = muc_register:encode([{roomnick, Nick}])}, + ct:comment("Submitting registration form"), + #iq{type = result, sub_els = []} = + send_recv(Config, #iq{type = set, to = MUC, + sub_els = [#register{xdata = X}]}), + ct:comment("Checking if new nick was registered"), + #iq{type = result, + sub_els = [#register{registered = NewRegistered, + xdata = #xdata{type = form, + fields = FsWithNick}}]} = + send_recv(Config, #iq{type = get, to = MUC, + sub_els = [#register{}]}), + Nick = proplists:get_value( + roomnick, muc_register:decode(FsWithNick)). + +subscribe(Config, Events, Room) -> + MyNick = ?config(nick, Config), + case send_recv(Config, + #iq{type = set, to = Room, + sub_els = [#muc_subscribe{nick = MyNick, + events = Events}]}) of + #iq{type = result, sub_els = [#muc_subscribe{events = ResEvents}]} -> + lists:sort(ResEvents); + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +unsubscribe(Config, Room) -> + case send_recv(Config, #iq{type = set, to = Room, + sub_els = [#muc_unsubscribe{}]}) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. diff --git a/test/offline_tests.erl b/test/offline_tests.erl new file mode 100644 index 000000000..d859da622 --- /dev/null +++ b/test/offline_tests.erl @@ -0,0 +1,537 @@ +%%%------------------------------------------------------------------- +%%% Author : Evgeny Khramtsov +%%% Created : 7 Nov 2016 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(offline_tests). + +%% API +-compile(export_all). +-import(suite, [send/2, disconnect/1, my_jid/1, send_recv/2, recv_message/1, + get_features/1, recv/1, get_event/1, server_jid/1, + wait_for_master/1, wait_for_slave/1, + connect/1, open_session/1, bind/1, auth/1]). +-include("suite.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +single_cases() -> + {offline_single, [sequence], + [single_test(feature_enabled), + single_test(check_identity), + single_test(send_non_existent), + single_test(view_non_existent), + single_test(remove_non_existent), + single_test(view_non_integer), + single_test(remove_non_integer), + single_test(malformed_iq), + single_test(wrong_user), + single_test(unsupported_iq)]}. + +feature_enabled(Config) -> + Features = get_features(Config), + ct:comment("Checking if offline features are set"), + true = lists:member(?NS_FEATURE_MSGOFFLINE, Features), + true = lists:member(?NS_FLEX_OFFLINE, Features), + disconnect(Config). + +check_identity(Config) -> + #iq{type = result, + sub_els = [#disco_info{ + node = ?NS_FLEX_OFFLINE, + identities = Ids}]} = + send_recv(Config, #iq{type = get, + sub_els = [#disco_info{ + node = ?NS_FLEX_OFFLINE}]}), + true = lists:any( + fun(#identity{category = <<"automation">>, + type = <<"message-list">>}) -> true; + (_) -> false + end, Ids), + disconnect(Config). + +send_non_existent(Config) -> + Server = ?config(server, Config), + To = jid:make(<<"non-existent">>, Server), + #message{type = error} = Err = send_recv(Config, #message{to = To}), + #stanza_error{reason = 'service-unavailable'} = xmpp:get_error(Err), + disconnect(Config). + +view_non_existent(Config) -> + #stanza_error{reason = 'item-not-found'} = view(Config, [rand_string()], false), + disconnect(Config). + +remove_non_existent(Config) -> + ok = remove(Config, [rand_string()]), + disconnect(Config). + +view_non_integer(Config) -> + #stanza_error{reason = 'item-not-found'} = view(Config, [<<"foo">>], false), + disconnect(Config). + +remove_non_integer(Config) -> + #stanza_error{reason = 'item-not-found'} = remove(Config, [<<"foo">>]), + disconnect(Config). + +malformed_iq(Config) -> + Item = #offline_item{node = rand_string()}, + Range = [{Type, SubEl} || Type <- [set, get], + SubEl <- [#offline{items = [], _ = false}, + #offline{items = [Item], _ = true}]] + ++ [{set, #offline{items = [], fetch = true, purge = false}}, + {set, #offline{items = [Item], fetch = true, purge = false}}, + {get, #offline{items = [], fetch = false, purge = true}}, + {get, #offline{items = [Item], fetch = false, purge = true}}], + lists:foreach( + fun({Type, SubEl}) -> + #iq{type = error} = Err = + send_recv(Config, #iq{type = Type, sub_els = [SubEl]}), + #stanza_error{reason = 'bad-request'} = xmpp:get_error(Err) + end, Range), + disconnect(Config). + +wrong_user(Config) -> + Server = ?config(server, Config), + To = jid:make(<<"foo">>, Server), + Item = #offline_item{node = rand_string()}, + Range = [{Type, Items, Purge, Fetch} || + Type <- [set, get], + Items <- [[], [Item]], + Purge <- [false, true], + Fetch <- [false, true]], + lists:foreach( + fun({Type, Items, Purge, Fetch}) -> + #iq{type = error} = Err = + send_recv(Config, #iq{type = Type, to = To, + sub_els = [#offline{items = Items, + purge = Purge, + fetch = Fetch}]}), + #stanza_error{reason = 'forbidden'} = xmpp:get_error(Err) + end, Range), + disconnect(Config). + +unsupported_iq(Config) -> + Item = #offline_item{node = rand_string()}, + lists:foreach( + fun(Type) -> + #iq{type = error} = Err = + send_recv(Config, #iq{type = Type, sub_els = [Item]}), + #stanza_error{reason = 'service-unavailable'} = xmpp:get_error(Err) + end, [set, get]), + disconnect(Config). + +%%%=================================================================== +%%% Master-slave tests +%%%=================================================================== +master_slave_cases(_DB) -> + {offline_master_slave, [sequence], + [master_slave_test(flex), + master_slave_test(send_all), + master_slave_test(from_mam), + master_slave_test(mucsub_mam)]}. + +flex_master(Config) -> + send_messages(Config, 5), + disconnect(Config). + +flex_slave(Config) -> + wait_for_master(Config), + peer_down = get_event(Config), + 5 = get_number(Config), + Nodes = get_nodes(Config), + %% Since headers are received we can send initial presence without a risk + %% of getting offline messages flood + #presence{} = send_recv(Config, #presence{}), + ct:comment("Checking fetch"), + Nodes = fetch(Config, lists:seq(1, 5)), + ct:comment("Fetching 2nd and 4th message"), + [2, 4] = view(Config, [lists:nth(2, Nodes), lists:nth(4, Nodes)]), + ct:comment("Deleting 2nd and 4th message"), + ok = remove(Config, [lists:nth(2, Nodes), lists:nth(4, Nodes)]), + ct:comment("Checking if messages were deleted"), + [1, 3, 5] = view(Config, [lists:nth(1, Nodes), + lists:nth(3, Nodes), + lists:nth(5, Nodes)]), + ct:comment("Purging everything left"), + ok = purge(Config), + ct:comment("Checking if there are no offline messages"), + 0 = get_number(Config), + clean(disconnect(Config)). + +from_mam_master(Config) -> + C2 = lists:keystore(mam_enabled, 1, Config, {mam_enabled, true}), + C3 = send_all_master(C2), + lists:keydelete(mam_enabled, 1, C3). + +from_mam_slave(Config) -> + Server = ?config(server, Config), + gen_mod:update_module(Server, mod_offline, #{use_mam_for_storage => true}), + ok = mam_tests:set_default(Config, always), + C2 = lists:keystore(mam_enabled, 1, Config, {mam_enabled, true}), + C3 = send_all_slave(C2), + gen_mod:update_module(Server, mod_offline, #{use_mam_for_storage => false}), + C4 = lists:keydelete(mam_enabled, 1, C3), + mam_tests:clean(C4). + +mucsub_mam_master(Config) -> + Room = suite:muc_room_jid(Config), + Peer = ?config(peer, Config), + wait_for_slave(Config), + ct:comment("Joining muc room"), + ok = muc_tests:join_new(Config), + + ct:comment("Enabling mam in room"), + CfgOpts = muc_tests:get_config(Config), + %% Find the MAM field in the config + ?match(true, proplists:is_defined(mam, CfgOpts)), + ?match(true, proplists:is_defined(allow_subscription, CfgOpts)), + %% Enable MAM + [104] = muc_tests:set_config(Config, [{mam, true}, {allow_subscription, true}]), + + ct:comment("Subscribing peer to room"), + ?send_recv(#iq{to = Room, type = set, sub_els = [ + #muc_subscribe{jid = Peer, nick = <<"peer">>, + events = [?NS_MUCSUB_NODES_MESSAGES]} + ]}, #iq{type = result}), + + ?match(#message{type = groupchat}, + send_recv(Config, #message{type = groupchat, to = Room, body = xmpp:mk_text(<<"1">>)})), + ?match(#message{type = groupchat}, + send_recv(Config, #message{type = groupchat, to = Room, body = xmpp:mk_text(<<"2">>), + sub_els = [#hint{type = 'no-store'}]})), + ?match(#message{type = groupchat}, + send_recv(Config, #message{type = groupchat, to = Room, body = xmpp:mk_text(<<"3">>)})), + + ct:comment("Cleaning up"), + suite:put_event(Config, ready), + ready = get_event(Config), + muc_tests:leave(Config), + mam_tests:clean(clean(disconnect(Config))). + +mucsub_mam_slave(Config) -> + Server = ?config(server, Config), + gen_mod:update_module(Server, mod_offline, #{use_mam_for_storage => true}), + gen_mod:update_module(Server, mod_mam, #{user_mucsub_from_muc_archive => true}), + + Room = suite:muc_room_jid(Config), + ok = mam_tests:set_default(Config, always), + #presence{} = send_recv(Config, #presence{}), + send(Config, #presence{type = unavailable}), + + wait_for_master(Config), + ready = get_event(Config), + ct:sleep(100), + + ct:comment("Receiving offline messages"), + + ?match(#presence{}, suite:send_recv(Config, #presence{})), + + lists:foreach( + fun(N) -> + Body = xmpp:mk_text(integer_to_binary(N)), + Msg = ?match(#message{from = Room, type = normal} = Msg, recv_message(Config), Msg), + PS = ?match(#ps_event{items = #ps_items{node = ?NS_MUCSUB_NODES_MESSAGES, items = [ + #ps_item{} = PS + ]}}, xmpp:get_subtag(Msg, #ps_event{}), PS), + ?match(#message{type = groupchat, body = Body}, xmpp:get_subtag(PS, #message{})) + end, [1, 3]), + + % Unsubscribe yourself + ?send_recv(#iq{to = Room, type = set, sub_els = [ + #muc_unsubscribe{} + ]}, #iq{type = result}), + suite:put_event(Config, ready), + mam_tests:clean(clean(disconnect(Config))), + gen_mod:update_module(Server, mod_offline, #{use_mam_for_storage => false}), + gen_mod:update_module(Server, mod_mam, #{user_mucsub_from_muc_archive => false}). + +send_all_master(Config) -> + wait_for_slave(Config), + Peer = ?config(peer, Config), + BarePeer = jid:remove_resource(Peer), + {Deliver, Errors} = message_iterator(Config), + N = lists:foldl( + fun(#message{type = error} = Msg, Acc) -> + send(Config, Msg#message{to = BarePeer}), + Acc; + (Msg, Acc) -> + I = send(Config, Msg#message{to = BarePeer}), + case {xmpp:get_subtag(Msg, #offline{}), xmpp:get_subtag(Msg, #xevent{})} of + {#offline{}, _} -> + ok; + {_, #xevent{offline = true, id = undefined}} -> + ct:comment("Receiving event-reply for:~n~s", + [xmpp:pp(Msg)]), + #message{} = Reply = recv_message(Config), + #xevent{id = I} = xmpp:get_subtag(Reply, #xevent{}); + _ -> + ok + end, + Acc + 1 + end, 0, Deliver), + lists:foreach( + fun(#message{type = headline} = Msg) -> + send(Config, Msg#message{to = BarePeer}); + (Msg) -> + #message{type = error} = Err = + send_recv(Config, Msg#message{to = BarePeer}), + #stanza_error{reason = 'service-unavailable'} = xmpp:get_error(Err) + end, Errors), + ok = wait_for_complete(Config, N), + disconnect(Config). + +send_all_slave(Config) -> + ServerJID = server_jid(Config), + Peer = ?config(peer, Config), + #presence{} = send_recv(Config, #presence{}), + send(Config, #presence{type = unavailable}), + wait_for_master(Config), + peer_down = get_event(Config), + #presence{} = send_recv(Config, #presence{}), + {Deliver, _Errors} = message_iterator(Config), + lists:foreach( + fun(#message{type = error}) -> + ok; + (#message{type = Type, body = Body, subject = Subject} = Msg) -> + ct:comment("Receiving message:~n~s", [xmpp:pp(Msg)]), + #message{from = Peer, + type = Type, + body = Body, + subject = Subject} = RecvMsg = recv_message(Config), + ct:comment("Checking if delay tag is correctly set"), + #delay{from = ServerJID} = xmpp:get_subtag(RecvMsg, #delay{}) + end, Deliver), + disconnect(Config). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +single_test(T) -> + list_to_atom("offline_" ++ atom_to_list(T)). + +master_slave_test(T) -> + {list_to_atom("offline_" ++ atom_to_list(T)), [parallel], + [list_to_atom("offline_" ++ atom_to_list(T) ++ "_master"), + list_to_atom("offline_" ++ atom_to_list(T) ++ "_slave")]}. + +clean(Config) -> + {U, S, _} = jid:tolower(my_jid(Config)), + mod_offline:remove_user(U, S), + Config. + +send_messages(Config, Num) -> + send_messages(Config, Num, normal, []). + +send_messages(Config, Num, Type, SubEls) -> + wait_for_slave(Config), + Peer = ?config(peer, Config), + BarePeer = jid:remove_resource(Peer), + lists:foreach( + fun(I) -> + Body = integer_to_binary(I), + send(Config, + #message{to = BarePeer, + type = Type, + body = [#text{data = Body}], + subject = [#text{data = <<"subject">>}], + sub_els = SubEls}) + end, lists:seq(1, Num)), + ct:comment("Waiting for all messages to be delivered to offline spool"), + ok = wait_for_complete(Config, Num). + +recv_messages(Config, Num) -> + wait_for_master(Config), + peer_down = get_event(Config), + Peer = ?config(peer, Config), + #presence{} = send_recv(Config, #presence{}), + lists:foreach( + fun(I) -> + Text = integer_to_binary(I), + #message{sub_els = SubEls, + from = Peer, + body = [#text{data = Text}], + subject = [#text{data = <<"subject">>}]} = + recv_message(Config), + true = lists:keymember(delay, 1, SubEls) + end, lists:seq(1, Num)), + clean(disconnect(Config)). + +get_number(Config) -> + ct:comment("Getting offline message number"), + #iq{type = result, + sub_els = [#disco_info{ + node = ?NS_FLEX_OFFLINE, + xdata = [X]}]} = + send_recv(Config, #iq{type = get, + sub_els = [#disco_info{ + node = ?NS_FLEX_OFFLINE}]}), + Form = flex_offline:decode(X#xdata.fields), + proplists:get_value(number_of_messages, Form). + +get_nodes(Config) -> + MyJID = my_jid(Config), + MyBareJID = jid:remove_resource(MyJID), + Peer = ?config(peer, Config), + Peer_s = jid:encode(Peer), + ct:comment("Getting headers"), + #iq{type = result, + sub_els = [#disco_items{ + node = ?NS_FLEX_OFFLINE, + items = DiscoItems}]} = + send_recv(Config, #iq{type = get, + sub_els = [#disco_items{ + node = ?NS_FLEX_OFFLINE}]}), + ct:comment("Checking if headers are correct"), + lists:sort( + lists:map( + fun(#disco_item{jid = J, name = P, node = N}) + when (J == MyBareJID) and (P == Peer_s) -> + N + end, DiscoItems)). + +fetch(Config, Range) -> + ID = send(Config, #iq{type = get, sub_els = [#offline{fetch = true}]}), + Nodes = lists:map( + fun(I) -> + Text = integer_to_binary(I), + #message{body = Body, sub_els = SubEls} = recv(Config), + [#text{data = Text}] = Body, + #offline{items = [#offline_item{node = Node}]} = + lists:keyfind(offline, 1, SubEls), + #delay{} = lists:keyfind(delay, 1, SubEls), + Node + end, Range), + #iq{id = ID, type = result, sub_els = []} = recv(Config), + Nodes. + +view(Config, Nodes) -> + view(Config, Nodes, true). + +view(Config, Nodes, NeedReceive) -> + Items = lists:map( + fun(Node) -> + #offline_item{action = view, node = Node} + end, Nodes), + I = send(Config, + #iq{type = get, sub_els = [#offline{items = Items}]}), + Range = if NeedReceive -> + lists:map( + fun(Node) -> + #message{body = [#text{data = Text}], + sub_els = SubEls} = recv(Config), + #offline{items = [#offline_item{node = Node}]} = + lists:keyfind(offline, 1, SubEls), + binary_to_integer(Text) + end, Nodes); + true -> + [] + end, + case recv(Config) of + #iq{id = I, type = result, sub_els = []} -> Range; + #iq{id = I, type = error} = Err -> xmpp:get_error(Err) + end. + +remove(Config, Nodes) -> + Items = lists:map( + fun(Node) -> + #offline_item{action = remove, node = Node} + end, Nodes), + case send_recv(Config, #iq{type = set, + sub_els = [#offline{items = Items}]}) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +purge(Config) -> + case send_recv(Config, #iq{type = set, + sub_els = [#offline{purge = true}]}) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +wait_for_complete(_Config, 0) -> + ok; +wait_for_complete(Config, N) -> + {U, S, _} = jid:tolower(?config(peer, Config)), + lists:foldl( + fun(_Time, ok) -> + ok; + (Time, Acc) -> + timer:sleep(Time), + case mod_offline:count_offline_messages(U, S) of + N -> ok; + _ -> Acc + end + end, error, [0, 100, 200, 2000, 5000, 10000]). + +xevent_stored(#message{body = [], subject = []}, _) -> false; +xevent_stored(#message{type = T}, _) when T /= chat, T /= normal -> false; +xevent_stored(_, #xevent{id = undefined}) -> true; +xevent_stored(_, #xevent{offline = true}) -> true; +xevent_stored(_, #xevent{delivered = true}) -> true; +xevent_stored(_, #xevent{displayed = true}) -> true; +xevent_stored(_, _) -> false. + +message_iterator(Config) -> + ServerJID = server_jid(Config), + ChatStates = [[#chatstate{type = composing}]], + Offline = [[#offline{}]], + Hints = [[#hint{type = T}] || T <- [store, 'no-store']], + XEvent = [[#xevent{id = ID, offline = OfflineFlag}] + || ID <- [undefined, rand_string()], + OfflineFlag <- [false, true]], + Delay = [[#delay{stamp = p1_time_compat:timestamp(), from = ServerJID}]], + AllEls = [Els1 ++ Els2 || Els1 <- [[]] ++ ChatStates ++ Delay ++ Hints ++ Offline, + Els2 <- [[]] ++ XEvent], + All = [#message{type = Type, body = Body, subject = Subject, sub_els = Els} + || %%Type <- [chat], + Type <- [error, chat, normal, groupchat, headline], + Body <- [[], xmpp:mk_text(<<"body">>)], + Subject <- [[], xmpp:mk_text(<<"subject">>)], + Els <- AllEls], + MamEnabled = ?config(mam_enabled, Config) == true, + lists:partition( + fun(#message{type = error}) -> true; + (#message{type = groupchat}) -> false; + (#message{sub_els = [#hint{type = store}|_]}) when MamEnabled -> true; + (#message{sub_els = [#hint{type = 'no-store'}|_]}) -> false; + (#message{sub_els = [#offline{}|_]}) when not MamEnabled -> false; + (#message{sub_els = [#hint{type = store}, #xevent{} = Event | _]} = Msg) when not MamEnabled -> + xevent_stored(Msg#message{body = body, type = chat}, Event); + (#message{sub_els = [#xevent{} = Event]} = Msg) when not MamEnabled -> + xevent_stored(Msg, Event); + (#message{sub_els = [_, #xevent{} = Event | _]} = Msg) when not MamEnabled -> + xevent_stored(Msg, Event); + (#message{sub_els = [#xevent{id = I}]}) when I /= undefined, not MamEnabled -> false; + (#message{sub_els = [#hint{type = store}|_]}) -> true; + (#message{sub_els = [#hint{type = 'no-store'}|_]}) -> false; + (#message{body = [], subject = []}) -> false; + (#message{type = Type}) -> (Type == chat) or (Type == normal); + (_) -> false + end, All). + +rand_string() -> + integer_to_binary(p1_rand:uniform((1 bsl 31)-1)). diff --git a/test/privacy_tests.erl b/test/privacy_tests.erl new file mode 100644 index 000000000..51782dcf4 --- /dev/null +++ b/test/privacy_tests.erl @@ -0,0 +1,891 @@ +%%%------------------------------------------------------------------- +%%% Author : Evgeny Khramtsov +%%% Created : 18 Oct 2016 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(privacy_tests). + +%% API +-compile(export_all). +-import(suite, [disconnect/1, send_recv/2, get_event/1, put_event/2, + recv_iq/1, recv_presence/1, recv_message/1, recv/1, + send/2, my_jid/1, server_jid/1, get_features/1, + set_roster/3, del_roster/1, get_roster/1]). +-include("suite.hrl"). +-include("mod_roster.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +%%%=================================================================== +%%% Single cases +%%%=================================================================== +single_cases() -> + {privacy_single, [sequence], + [single_test(feature_enabled), + single_test(set_get_list), + single_test(get_list_non_existent), + single_test(get_empty_lists), + single_test(set_default), + single_test(del_default), + single_test(set_default_non_existent), + single_test(set_active), + single_test(del_active), + single_test(set_active_non_existent), + single_test(remove_list), + single_test(remove_default_list), + single_test(remove_active_list), + single_test(remove_list_non_existent), + single_test(allow_local_server), + single_test(malformed_iq_query), + single_test(malformed_get), + single_test(malformed_set), + single_test(malformed_type_value), + single_test(set_get_block)]}. + +feature_enabled(Config) -> + Features = get_features(Config), + true = lists:member(?NS_PRIVACY, Features), + true = lists:member(?NS_BLOCKING, Features), + disconnect(Config). + +set_get_list(Config) -> + ListName = <<"set-get-list">>, + Items = [#privacy_item{order = 0, action = deny, + type = jid, value = <<"user@jabber.org">>, + iq = true}, + #privacy_item{order = 1, action = allow, + type = group, value = <<"group">>, + message = true}, + #privacy_item{order = 2, action = allow, + type = subscription, value = <<"both">>, + presence_in = true}, + #privacy_item{order = 3, action = deny, + type = subscription, value = <<"from">>, + presence_out = true}, + #privacy_item{order = 4, action = deny, + type = subscription, value = <<"to">>, + iq = true, message = true}, + #privacy_item{order = 5, action = deny, + type = subscription, value = <<"none">>, + _ = true}, + #privacy_item{order = 6, action = deny}], + ok = set_items(Config, ListName, Items), + #privacy_list{name = ListName, items = Items1} = get_list(Config, ListName), + Items = lists:keysort(#privacy_item.order, Items1), + del_privacy(disconnect(Config)). + +get_list_non_existent(Config) -> + ListName = <<"get-list-non-existent">>, + #stanza_error{reason = 'item-not-found'} = get_list(Config, ListName), + disconnect(Config). + +get_empty_lists(Config) -> + #privacy_query{default = none, + active = none, + lists = []} = get_lists(Config), + disconnect(Config). + +set_default(Config) -> + ListName = <<"set-default">>, + Item = #privacy_item{order = 0, action = deny}, + ok = set_items(Config, ListName, [Item]), + ok = set_default(Config, ListName), + #privacy_query{default = ListName} = get_lists(Config), + del_privacy(disconnect(Config)). + +del_default(Config) -> + ListName = <<"del-default">>, + Item = #privacy_item{order = 0, action = deny}, + ok = set_items(Config, ListName, [Item]), + ok = set_default(Config, ListName), + #privacy_query{default = ListName} = get_lists(Config), + ok = set_default(Config, none), + #privacy_query{default = none} = get_lists(Config), + del_privacy(disconnect(Config)). + +set_default_non_existent(Config) -> + ListName = <<"set-default-non-existent">>, + #stanza_error{reason = 'item-not-found'} = set_default(Config, ListName), + disconnect(Config). + +set_active(Config) -> + ListName = <<"set-active">>, + Item = #privacy_item{order = 0, action = deny}, + ok = set_items(Config, ListName, [Item]), + ok = set_active(Config, ListName), + #privacy_query{active = ListName} = get_lists(Config), + del_privacy(disconnect(Config)). + +del_active(Config) -> + ListName = <<"del-active">>, + Item = #privacy_item{order = 0, action = deny}, + ok = set_items(Config, ListName, [Item]), + ok = set_active(Config, ListName), + #privacy_query{active = ListName} = get_lists(Config), + ok = set_active(Config, none), + #privacy_query{active = none} = get_lists(Config), + del_privacy(disconnect(Config)). + +set_active_non_existent(Config) -> + ListName = <<"set-active-non-existent">>, + #stanza_error{reason = 'item-not-found'} = set_active(Config, ListName), + disconnect(Config). + +remove_list(Config) -> + ListName = <<"remove-list">>, + Item = #privacy_item{order = 0, action = deny}, + ok = set_items(Config, ListName, [Item]), + ok = del_list(Config, ListName), + #privacy_query{lists = []} = get_lists(Config), + del_privacy(disconnect(Config)). + +remove_active_list(Config) -> + ListName = <<"remove-active-list">>, + Item = #privacy_item{order = 0, action = deny}, + ok = set_items(Config, ListName, [Item]), + ok = set_active(Config, ListName), + #stanza_error{reason = 'conflict'} = del_list(Config, ListName), + del_privacy(disconnect(Config)). + +remove_default_list(Config) -> + ListName = <<"remove-default-list">>, + Item = #privacy_item{order = 0, action = deny}, + ok = set_items(Config, ListName, [Item]), + ok = set_default(Config, ListName), + #stanza_error{reason = 'conflict'} = del_list(Config, ListName), + del_privacy(disconnect(Config)). + +remove_list_non_existent(Config) -> + ListName = <<"remove-list-non-existent">>, + #stanza_error{reason = 'item-not-found'} = del_list(Config, ListName), + disconnect(Config). + +allow_local_server(Config) -> + ListName = <<"allow-local-server">>, + Item = #privacy_item{order = 0, action = deny}, + ok = set_items(Config, ListName, [Item]), + ok = set_active(Config, ListName), + %% Whatever privacy rules are set, we should always communicate + %% with our home server + server_send_iqs(Config), + server_recv_iqs(Config), + send_stanzas_to_server_resource(Config), + del_privacy(disconnect(Config)). + +malformed_iq_query(Config) -> + lists:foreach( + fun(Type) -> + #iq{type = error} = + send_recv(Config, + #iq{type = Type, + sub_els = [#privacy_list{name = <<"foo">>}]}) + end, [get, set]), + disconnect(Config). + +malformed_get(Config) -> + JID = jid:make(p1_rand:get_string()), + Item = #block_item{jid = JID}, + lists:foreach( + fun(SubEl) -> + #iq{type = error} = + send_recv(Config, #iq{type = get, sub_els = [SubEl]}) + end, [#privacy_query{active = none}, + #privacy_query{default = none}, + #privacy_query{lists = [#privacy_list{name = <<"1">>}, + #privacy_list{name = <<"2">>}]}, + #block{items = [Item]}, #unblock{items = [Item]}, + #block{}, #unblock{}]), + disconnect(Config). + +malformed_set(Config) -> + lists:foreach( + fun(SubEl) -> + #iq{type = error} = + send_recv(Config, #iq{type = set, sub_els = [SubEl]}) + end, [#privacy_query{active = none, default = none}, + #privacy_query{lists = [#privacy_list{name = <<"1">>}, + #privacy_list{name = <<"2">>}]}, + #block{}, + #block_list{}, + #block_list{ + items = [#block_item{ + jid = jid:make(p1_rand:get_string())}]}]), + disconnect(Config). + +malformed_type_value(Config) -> + Item = #privacy_item{order = 0, action = deny}, + #stanza_error{reason = 'bad-request'} = + set_items(Config, <<"malformed-jid">>, + [Item#privacy_item{type = jid, value = <<"@bad">>}]), + #stanza_error{reason = 'bad-request'} = + set_items(Config, <<"malformed-group">>, + [Item#privacy_item{type = group, value = <<"">>}]), + #stanza_error{reason = 'bad-request'} = + set_items(Config, <<"malformed-subscription">>, + [Item#privacy_item{type = subscription, value = <<"bad">>}]), + disconnect(Config). + +set_get_block(Config) -> + J1 = jid:make(p1_rand:get_string(), p1_rand:get_string()), + J2 = jid:make(p1_rand:get_string(), p1_rand:get_string()), + {ok, ListName} = set_block(Config, [J1, J2]), + JIDs = get_block(Config), + JIDs = lists:sort([J1, J2]), + {ok, ListName} = set_unblock(Config, [J2, J1]), + [] = get_block(Config), + del_privacy(disconnect(Config)). + +%%%=================================================================== +%%% Master-slave cases +%%%=================================================================== +master_slave_cases() -> + {privacy_master_slave, [sequence], + [master_slave_test(deny_bare_jid), + master_slave_test(deny_full_jid), + master_slave_test(deny_server_bare_jid), + master_slave_test(deny_server_full_jid), + master_slave_test(deny_group), + master_slave_test(deny_sub_both), + master_slave_test(deny_sub_from), + master_slave_test(deny_sub_to), + master_slave_test(deny_sub_none), + master_slave_test(deny_all), + master_slave_test(deny_offline), + master_slave_test(block), + master_slave_test(unblock), + master_slave_test(unblock_all)]}. + +deny_bare_jid_master(Config) -> + PeerJID = ?config(peer, Config), + PeerBareJID = jid:remove_resource(PeerJID), + deny_master(Config, {jid, jid:encode(PeerBareJID)}). + +deny_bare_jid_slave(Config) -> + deny_slave(Config). + +deny_full_jid_master(Config) -> + PeerJID = ?config(peer, Config), + deny_master(Config, {jid, jid:encode(PeerJID)}). + +deny_full_jid_slave(Config) -> + deny_slave(Config). + +deny_server_bare_jid_master(Config) -> + {_, Server, _} = jid:tolower(?config(peer, Config)), + deny_master(Config, {jid, Server}). + +deny_server_bare_jid_slave(Config) -> + deny_slave(Config). + +deny_server_full_jid_master(Config) -> + {_, Server, Resource} = jid:tolower(?config(peer, Config)), + deny_master(Config, {jid, jid:encode({<<"">>, Server, Resource})}). + +deny_server_full_jid_slave(Config) -> + deny_slave(Config). + +deny_group_master(Config) -> + Group = p1_rand:get_string(), + deny_master(Config, {group, Group}). + +deny_group_slave(Config) -> + deny_slave(Config). + +deny_sub_both_master(Config) -> + deny_master(Config, {subscription, <<"both">>}). + +deny_sub_both_slave(Config) -> + deny_slave(Config, 2). + +deny_sub_from_master(Config) -> + deny_master(Config, {subscription, <<"from">>}). + +deny_sub_from_slave(Config) -> + deny_slave(Config, 1). + +deny_sub_to_master(Config) -> + deny_master(Config, {subscription, <<"to">>}). + +deny_sub_to_slave(Config) -> + deny_slave(Config, 2). + +deny_sub_none_master(Config) -> + deny_master(Config, {subscription, <<"none">>}). + +deny_sub_none_slave(Config) -> + deny_slave(Config). + +deny_all_master(Config) -> + deny_master(Config, {undefined, <<"">>}). + +deny_all_slave(Config) -> + deny_slave(Config). + +deny_master(Config, {Type, Value}) -> + Sub = if Type == subscription -> + erlang:binary_to_atom(Value, utf8); + true -> + both + end, + Groups = if Type == group -> [Value]; + true -> [] + end, + set_roster(Config, Sub, Groups), + lists:foreach( + fun(Opts) -> + ct:pal("Set list for ~s, ~s, ~w", [Type, Value, Opts]), + ListName = p1_rand:get_string(), + Item = #privacy_item{order = 0, + action = deny, + iq = proplists:get_bool(iq, Opts), + message = proplists:get_bool(message, Opts), + presence_in = proplists:get_bool(presence_in, Opts), + presence_out = proplists:get_bool(presence_out, Opts), + type = Type, + value = Value}, + ok = set_items(Config, ListName, [Item]), + ok = set_active(Config, ListName), + put_event(Config, Opts), + case is_presence_in_blocked(Opts) of + true -> ok; + false -> recv_presences(Config) + end, + case is_iq_in_blocked(Opts) of + true -> ok; + false -> recv_iqs(Config) + end, + case is_message_in_blocked(Opts) of + true -> ok; + false -> recv_messages(Config) + end, + ct:comment("Waiting for 'send' command from the slave"), + send = get_event(Config), + case is_presence_out_blocked(Opts) of + true -> check_presence_blocked(Config, 'not-acceptable'); + false -> ok + end, + case is_iq_out_blocked(Opts) of + true -> check_iq_blocked(Config, 'not-acceptable'); + false -> send_iqs(Config) + end, + case is_message_out_blocked(Opts) of + true -> check_message_blocked(Config, 'not-acceptable'); + false -> send_messages(Config) + end, + case is_other_blocked(Opts) of + true -> + check_other_blocked(Config, 'not-acceptable', Value); + false -> ok + end, + ct:comment("Waiting for slave to finish processing our stanzas"), + done = get_event(Config) + end, + [[iq], [message], [presence_in], [presence_out], + [iq, message, presence_in, presence_out], []]), + put_event(Config, disconnect), + clean_up(disconnect(Config)). + +deny_slave(Config) -> + deny_slave(Config, 0). + +deny_slave(Config, RosterPushesCount) -> + set_roster(Config, both, []), + deny_slave(Config, RosterPushesCount, get_event(Config)). + +deny_slave(Config, RosterPushesCount, disconnect) -> + recv_roster_pushes(Config, RosterPushesCount), + clean_up(disconnect(Config)); +deny_slave(Config, RosterPushesCount, Opts) -> + send_presences(Config), + case is_iq_in_blocked(Opts) of + true -> check_iq_blocked(Config, 'service-unavailable'); + false -> send_iqs(Config) + end, + case is_message_in_blocked(Opts) of + true -> check_message_blocked(Config, 'service-unavailable'); + false -> send_messages(Config) + end, + put_event(Config, send), + case is_iq_out_blocked(Opts) of + true -> ok; + false -> recv_iqs(Config) + end, + case is_message_out_blocked(Opts) of + true -> ok; + false -> recv_messages(Config) + end, + put_event(Config, done), + deny_slave(Config, RosterPushesCount, get_event(Config)). + +deny_offline_master(Config) -> + set_roster(Config, both, []), + ListName = <<"deny-offline">>, + Item = #privacy_item{order = 0, action = deny}, + ok = set_items(Config, ListName, [Item]), + ok = set_default(Config, ListName), + NewConfig = disconnect(Config), + put_event(NewConfig, send), + ct:comment("Waiting for the slave to finish"), + done = get_event(NewConfig), + clean_up(NewConfig). + +deny_offline_slave(Config) -> + set_roster(Config, both, []), + ct:comment("Waiting for 'send' command from the master"), + send = get_event(Config), + send_presences(Config), + check_iq_blocked(Config, 'service-unavailable'), + check_message_blocked(Config, 'service-unavailable'), + put_event(Config, done), + clean_up(disconnect(Config)). + +block_master(Config) -> + PeerJID = ?config(peer, Config), + set_roster(Config, both, []), + {ok, _} = set_block(Config, [PeerJID]), + check_presence_blocked(Config, 'not-acceptable'), + check_iq_blocked(Config, 'not-acceptable'), + check_message_blocked(Config, 'not-acceptable'), + check_other_blocked(Config, 'not-acceptable', other), + %% We should always be able to communicate with our home server + server_send_iqs(Config), + server_recv_iqs(Config), + send_stanzas_to_server_resource(Config), + put_event(Config, send), + done = get_event(Config), + clean_up(disconnect(Config)). + +block_slave(Config) -> + set_roster(Config, both, []), + ct:comment("Waiting for 'send' command from master"), + send = get_event(Config), + send_presences(Config), + check_iq_blocked(Config, 'service-unavailable'), + check_message_blocked(Config, 'service-unavailable'), + put_event(Config, done), + clean_up(disconnect(Config)). + +unblock_master(Config) -> + PeerJID = ?config(peer, Config), + set_roster(Config, both, []), + {ok, ListName} = set_block(Config, [PeerJID]), + {ok, ListName} = set_unblock(Config, [PeerJID]), + put_event(Config, send), + recv_presences(Config), + recv_iqs(Config), + recv_messages(Config), + clean_up(disconnect(Config)). + +unblock_slave(Config) -> + set_roster(Config, both, []), + ct:comment("Waiting for 'send' command from master"), + send = get_event(Config), + send_presences(Config), + send_iqs(Config), + send_messages(Config), + clean_up(disconnect(Config)). + +unblock_all_master(Config) -> + PeerJID = ?config(peer, Config), + set_roster(Config, both, []), + {ok, ListName} = set_block(Config, [PeerJID]), + {ok, ListName} = set_unblock(Config, []), + put_event(Config, send), + recv_presences(Config), + recv_iqs(Config), + recv_messages(Config), + clean_up(disconnect(Config)). + +unblock_all_slave(Config) -> + set_roster(Config, both, []), + ct:comment("Waiting for 'send' command from master"), + send = get_event(Config), + send_presences(Config), + send_iqs(Config), + send_messages(Config), + clean_up(disconnect(Config)). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +single_test(T) -> + list_to_atom("privacy_" ++ atom_to_list(T)). + +master_slave_test(T) -> + {list_to_atom("privacy_" ++ atom_to_list(T)), [parallel], + [list_to_atom("privacy_" ++ atom_to_list(T) ++ "_master"), + list_to_atom("privacy_" ++ atom_to_list(T) ++ "_slave")]}. + +set_items(Config, Name, Items) -> + ct:comment("Setting privacy list ~s with items = ~p", [Name, Items]), + case send_recv( + Config, + #iq{type = set, sub_els = [#privacy_query{ + lists = [#privacy_list{ + name = Name, + items = Items}]}]}) of + #iq{type = result, sub_els = []} -> + ct:comment("Receiving privacy list push"), + #iq{type = set, id = ID, + sub_els = [#privacy_query{lists = [#privacy_list{ + name = Name}]}]} = + recv_iq(Config), + send(Config, #iq{type = result, id = ID}), + ok; + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +get_list(Config, Name) -> + ct:comment("Requesting privacy list ~s", [Name]), + case send_recv(Config, + #iq{type = get, + sub_els = [#privacy_query{ + lists = [#privacy_list{name = Name}]}]}) of + #iq{type = result, sub_els = [#privacy_query{lists = [List]}]} -> + List; + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +get_lists(Config) -> + ct:comment("Requesting privacy lists"), + case send_recv(Config, #iq{type = get, sub_els = [#privacy_query{}]}) of + #iq{type = result, sub_els = [SubEl]} -> + SubEl; + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +del_list(Config, Name) -> + case send_recv( + Config, + #iq{type = set, sub_els = [#privacy_query{ + lists = [#privacy_list{ + name = Name}]}]}) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +set_active(Config, Name) -> + ct:comment("Setting active privacy list ~s", [Name]), + case send_recv( + Config, + #iq{type = set, sub_els = [#privacy_query{active = Name}]}) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +set_default(Config, Name) -> + ct:comment("Setting default privacy list ~s", [Name]), + case send_recv( + Config, + #iq{type = set, sub_els = [#privacy_query{default = Name}]}) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +get_block(Config) -> + case send_recv(Config, #iq{type = get, sub_els = [#block_list{}]}) of + #iq{type = result, sub_els = [#block_list{items = Items}]} -> + lists:sort([JID || #block_item{jid = JID} <- Items]); + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +set_block(Config, JIDs) -> + Items = [#block_item{jid = JID} || JID <- JIDs], + case send_recv(Config, #iq{type = set, + sub_els = [#block{items = Items}]}) of + #iq{type = result, sub_els = []} -> + {#iq{id = I1, sub_els = [#block{items = Items1}]}, + #iq{id = I2, sub_els = [#privacy_query{lists = Lists}]}} = + ?recv2(#iq{type = set, sub_els = [#block{}]}, + #iq{type = set, sub_els = [#privacy_query{}]}), + send(Config, #iq{type = result, id = I1}), + send(Config, #iq{type = result, id = I2}), + ct:comment("Checking if all JIDs present in the push"), + true = lists:sort(Items) == lists:sort(Items1), + ct:comment("Getting name of the corresponding privacy list"), + [#privacy_list{name = Name}] = Lists, + {ok, Name}; + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +set_unblock(Config, JIDs) -> + ct:comment("Unblocking ~p", [JIDs]), + Items = [#block_item{jid = JID} || JID <- JIDs], + case send_recv(Config, #iq{type = set, + sub_els = [#unblock{items = Items}]}) of + #iq{type = result, sub_els = []} -> + {#iq{id = I1, sub_els = [#unblock{items = Items1}]}, + #iq{id = I2, sub_els = [#privacy_query{lists = Lists}]}} = + ?recv2(#iq{type = set, sub_els = [#unblock{}]}, + #iq{type = set, sub_els = [#privacy_query{}]}), + send(Config, #iq{type = result, id = I1}), + send(Config, #iq{type = result, id = I2}), + ct:comment("Checking if all JIDs present in the push"), + true = lists:sort(Items) == lists:sort(Items1), + ct:comment("Getting name of the corresponding privacy list"), + [#privacy_list{name = Name}] = Lists, + {ok, Name}; + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +del_privacy(Config) -> + {U, S, _} = jid:tolower(my_jid(Config)), + ct:comment("Removing all privacy data"), + mod_privacy:remove_user(U, S), + Config. + +clean_up(Config) -> + del_privacy(del_roster(Config)). + +check_iq_blocked(Config, Reason) -> + PeerJID = ?config(peer, Config), + ct:comment("Checking if all IQs are blocked"), + lists:foreach( + fun(Type) -> + send(Config, #iq{type = Type, to = PeerJID}) + end, [error, result]), + lists:foreach( + fun(Type) -> + #iq{type = error} = Err = + send_recv(Config, #iq{type = Type, to = PeerJID, + sub_els = [#ping{}]}), + #stanza_error{reason = Reason} = xmpp:get_error(Err) + end, [set, get]). + +check_message_blocked(Config, Reason) -> + PeerJID = ?config(peer, Config), + ct:comment("Checking if all messages are blocked"), + %% TODO: do something with headline and groupchat. + %% The hack from 64d96778b452aad72349b21d2ac94e744617b07a + %% screws this up. + lists:foreach( + fun(Type) -> + send(Config, #message{type = Type, to = PeerJID}) + end, [error]), + lists:foreach( + fun(Type) -> + #message{type = error} = Err = + send_recv(Config, #message{type = Type, to = PeerJID}), + #stanza_error{reason = Reason} = xmpp:get_error(Err) + end, [chat, normal]). + +check_presence_blocked(Config, Reason) -> + PeerJID = ?config(peer, Config), + ct:comment("Checking if all presences are blocked"), + lists:foreach( + fun(Type) -> + #presence{type = error} = Err = + send_recv(Config, #presence{type = Type, to = PeerJID}), + #stanza_error{reason = Reason} = xmpp:get_error(Err) + end, [available, unavailable]). + +recv_roster_pushes(_Config, 0) -> + ok; +recv_roster_pushes(Config, Count) -> + receive + #iq{type = set, sub_els = [#roster_query{}]} -> + recv_roster_pushes(Config, Count - 1) + end. + +recv_err_and_roster_pushes(Config, Count) -> + recv_roster_pushes(Config, Count), + recv_presence(Config). + +check_other_blocked(Config, Reason, Subscription) -> + PeerJID = ?config(peer, Config), + ct:comment("Checking if subscriptions and presence-errors are blocked"), + send(Config, #presence{type = error, to = PeerJID}), + {ErrorFor, PushFor} = case Subscription of + <<"both">> -> + {[subscribe, subscribed], + [unsubscribe, unsubscribed]}; + <<"from">> -> + {[subscribe, subscribed, unsubscribe], + [subscribe, unsubscribe, unsubscribed]}; + <<"to">> -> + {[unsubscribe], + [subscribed, unsubscribe, unsubscribed]}; + <<"none">> -> + {[subscribe, subscribed, unsubscribe, unsubscribed], + [subscribe, unsubscribe]}; + _ -> + {[subscribe, subscribed, unsubscribe, unsubscribed], + [unsubscribe, unsubscribed]} + end, + lists:foreach( + fun(Type) -> + send(Config, #presence{type = Type, to = PeerJID}), + Count = case lists:member(Type, PushFor) of true -> 1; _ -> 0 end, + case lists:member(Type, ErrorFor) of + true -> + Err = recv_err_and_roster_pushes(Config, Count), + #stanza_error{reason = Reason} = xmpp:get_error(Err); + _ -> + recv_roster_pushes(Config, Count) + end + end, [subscribe, subscribed, unsubscribe, unsubscribed]). + +send_presences(Config) -> + PeerJID = ?config(peer, Config), + ct:comment("Sending all types of presences to the peer"), + lists:foreach( + fun(Type) -> + send(Config, #presence{type = Type, to = PeerJID}) + end, [available, unavailable]). + +send_iqs(Config) -> + PeerJID = ?config(peer, Config), + ct:comment("Sending all types of IQs to the peer"), + lists:foreach( + fun(Type) -> + send(Config, #iq{type = Type, to = PeerJID}) + end, [set, get, error, result]). + +send_messages(Config) -> + PeerJID = ?config(peer, Config), + ct:comment("Sending all types of messages to the peer"), + lists:foreach( + fun(Type) -> + send(Config, #message{type = Type, to = PeerJID}) + end, [chat, error, groupchat, headline, normal]). + +recv_presences(Config) -> + PeerJID = ?config(peer, Config), + lists:foreach( + fun(Type) -> + #presence{type = Type, from = PeerJID} = + recv_presence(Config) + end, [available, unavailable]). + +recv_iqs(Config) -> + PeerJID = ?config(peer, Config), + lists:foreach( + fun(Type) -> + #iq{type = Type, from = PeerJID} = recv_iq(Config) + end, [set, get, error, result]). + +recv_messages(Config) -> + PeerJID = ?config(peer, Config), + lists:foreach( + fun(Type) -> + #message{type = Type, from = PeerJID} = recv_message(Config) + end, [chat, error, groupchat, headline, normal]). + +match_all(Opts) -> + IQ = proplists:get_bool(iq, Opts), + Message = proplists:get_bool(message, Opts), + PresenceIn = proplists:get_bool(presence_in, Opts), + PresenceOut = proplists:get_bool(presence_out, Opts), + not (IQ or Message or PresenceIn or PresenceOut). + +is_message_in_blocked(Opts) -> + proplists:get_bool(message, Opts) or match_all(Opts). + +is_message_out_blocked(Opts) -> + match_all(Opts). + +is_iq_in_blocked(Opts) -> + proplists:get_bool(iq, Opts) or match_all(Opts). + +is_iq_out_blocked(Opts) -> + match_all(Opts). + +is_presence_in_blocked(Opts) -> + proplists:get_bool(presence_in, Opts) or match_all(Opts). + +is_presence_out_blocked(Opts) -> + proplists:get_bool(presence_out, Opts) or match_all(Opts). + +is_other_blocked(Opts) -> + %% 'other' means subscriptions and presence-errors + match_all(Opts). + +server_send_iqs(Config) -> + ServerJID = server_jid(Config), + MyJID = my_jid(Config), + ct:comment("Sending IQs from ~s to ~s", + [jid:encode(ServerJID), jid:encode(MyJID)]), + lists:foreach( + fun(Type) -> + ejabberd_router:route( + #iq{from = ServerJID, to = MyJID, type = Type}) + end, [error, result]), + lists:foreach( + fun(Type) -> + ejabberd_local:route_iq( + #iq{from = ServerJID, to = MyJID, type = Type}, + fun(#iq{type = result, sub_els = []}) -> ok; + (IQ) -> ct:fail({unexpected_iq_result, IQ}) + end) + end, [set, get]). + +server_recv_iqs(Config) -> + ServerJID = server_jid(Config), + ct:comment("Receiving IQs from ~s", [jid:encode(ServerJID)]), + lists:foreach( + fun(Type) -> + #iq{type = Type, from = ServerJID} = recv_iq(Config) + end, [error, result]), + lists:foreach( + fun(Type) -> + #iq{type = Type, from = ServerJID, id = I} = recv_iq(Config), + send(Config, #iq{to = ServerJID, type = result, id = I}) + end, [set, get]). + +send_stanzas_to_server_resource(Config) -> + ServerJID = server_jid(Config), + ServerJIDResource = jid:replace_resource(ServerJID, <<"resource">>), + %% All stanzas sent should be handled by local_send_to_resource_hook + %% and should be bounced with item-not-found error + ct:comment("Sending IQs to ~s", [jid:encode(ServerJIDResource)]), + lists:foreach( + fun(Type) -> + #iq{type = error} = Err = + send_recv(Config, #iq{type = Type, to = ServerJIDResource}), + #stanza_error{reason = 'item-not-found'} = xmpp:get_error(Err) + end, [set, get]), + ct:comment("Sending messages to ~s", [jid:encode(ServerJIDResource)]), + lists:foreach( + fun(Type) -> + #message{type = error} = Err = + send_recv(Config, #message{type = Type, to = ServerJIDResource}), + #stanza_error{reason = 'item-not-found'} = xmpp:get_error(Err) + end, [normal, chat, groupchat, headline]), + ct:comment("Sending presences to ~s", [jid:encode(ServerJIDResource)]), + lists:foreach( + fun(Type) -> + #presence{type = error} = Err = + send_recv(Config, #presence{type = Type, to = ServerJIDResource}), + #stanza_error{reason = 'item-not-found'} = xmpp:get_error(Err) + end, [available, unavailable]). diff --git a/test/private_tests.erl b/test/private_tests.erl new file mode 100644 index 000000000..e7077f4ba --- /dev/null +++ b/test/private_tests.erl @@ -0,0 +1,121 @@ +%%%------------------------------------------------------------------- +%%% Author : Evgeny Khramtsov +%%% Created : 23 Nov 2018 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(private_tests). + +%% API +-compile(export_all). +-import(suite, [my_jid/1, server_jid/1, is_feature_advertised/3, + send_recv/2, disconnect/1]). + +-include("suite.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +%%%=================================================================== +%%% Single user tests +%%%=================================================================== +single_cases() -> + {private_single, [sequence], + [single_test(test_features), + single_test(test_no_namespace), + single_test(test_set_get), + single_test(test_published)]}. + +test_features(Config) -> + Server = jid:encode(server_jid(Config)), + MyJID = my_jid(Config), + case gen_mod:is_loaded(Server, mod_pubsub) of + true -> + true = is_feature_advertised(Config, ?NS_BOOKMARKS_CONVERSION_0, + jid:remove_resource(MyJID)); + false -> + ok + end, + disconnect(Config). + +test_no_namespace(Config) -> + WrongEl = #xmlel{name = <<"wrong">>}, + #iq{type = error} = + send_recv(Config, #iq{type = get, + sub_els = [#private{sub_els = [WrongEl]}]}), + disconnect(Config). + +test_set_get(Config) -> + Storage = bookmark_storage(), + StorageXMLOut = xmpp:encode(Storage), + #iq{type = result, sub_els = []} = + send_recv( + Config, #iq{type = set, + sub_els = [#private{sub_els = [StorageXMLOut]}]}), + #iq{type = result, + sub_els = [#private{sub_els = [StorageXMLIn]}]} = + send_recv( + Config, + #iq{type = get, + sub_els = [#private{sub_els = [xmpp:encode( + #bookmark_storage{})]}]}), + Storage = xmpp:decode(StorageXMLIn), + disconnect(Config). + +test_published(Config) -> + Server = jid:encode(server_jid(Config)), + case gen_mod:is_loaded(Server, mod_pubsub) of + true -> + Storage = bookmark_storage(), + Node = xmpp:get_ns(Storage), + #iq{type = result, + sub_els = [#pubsub{items = #ps_items{node = Node, items = Items}}]} = + send_recv( + Config, + #iq{type = get, + sub_els = [#pubsub{items = #ps_items{node = Node}}]}), + [#ps_item{sub_els = [StorageXMLIn]}] = Items, + Storage = xmpp:decode(StorageXMLIn), + #iq{type = result, sub_els = []} = + send_recv(Config, + #iq{type = set, + sub_els = [#pubsub_owner{delete = {Node, <<>>}}]}), + #iq{type = result, sub_els = []} = + send_recv(Config, + #iq{type = set, + sub_els = [#pubsub_owner{delete = {?NS_PEP_BOOKMARKS, <<>>}}]}); + false -> + ok + end, + disconnect(Config). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +single_test(T) -> + list_to_atom("private_" ++ atom_to_list(T)). + +conference_bookmark() -> + #bookmark_conference{ + name = <<"Some name">>, + autojoin = true, + jid = jid:make(<<"some">>, <<"some.conference.org">>)}. + +bookmark_storage() -> + #bookmark_storage{conference = [conference_bookmark()]}. diff --git a/test/proxy65_tests.erl b/test/proxy65_tests.erl new file mode 100644 index 000000000..612a926fb --- /dev/null +++ b/test/proxy65_tests.erl @@ -0,0 +1,129 @@ +%%%------------------------------------------------------------------- +%%% Author : Evgeny Khramtsov +%%% Created : 16 Nov 2016 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(proxy65_tests). + +%% API +-compile(export_all). +-import(suite, [disconnect/1, is_feature_advertised/3, proxy_jid/1, + my_jid/1, wait_for_slave/1, wait_for_master/1, + send_recv/2, put_event/2, get_event/1]). + +-include("suite.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +%%%=================================================================== +%%% Single user tests +%%%=================================================================== +single_cases() -> + {proxy65_single, [sequence], + [single_test(feature_enabled), + single_test(service_vcard)]}. + +feature_enabled(Config) -> + true = is_feature_advertised(Config, ?NS_BYTESTREAMS, proxy_jid(Config)), + disconnect(Config). + +service_vcard(Config) -> + JID = proxy_jid(Config), + ct:comment("Retrieving vCard from ~s", [jid:encode(JID)]), + VCard = mod_proxy65_opt:vcard(?config(server, Config)), + #iq{type = result, sub_els = [VCard]} = + send_recv(Config, #iq{type = get, to = JID, sub_els = [#vcard_temp{}]}), + disconnect(Config). + +%%%=================================================================== +%%% Master-slave tests +%%%=================================================================== +master_slave_cases() -> + {proxy65_master_slave, [sequence], + [master_slave_test(all)]}. + +all_master(Config) -> + Proxy = proxy_jid(Config), + MyJID = my_jid(Config), + Peer = ?config(slave, Config), + wait_for_slave(Config), + #presence{} = send_recv(Config, #presence{}), + #iq{type = result, sub_els = [#bytestreams{hosts = [StreamHost]}]} = + send_recv( + Config, + #iq{type = get, sub_els = [#bytestreams{}], to = Proxy}), + SID = p1_rand:get_string(), + Data = p1_rand:bytes(1024), + put_event(Config, {StreamHost, SID, Data}), + Socks5 = socks5_connect(StreamHost, {SID, MyJID, Peer}), + wait_for_slave(Config), + #iq{type = result, sub_els = []} = + send_recv(Config, + #iq{type = set, to = Proxy, + sub_els = [#bytestreams{activate = Peer, sid = SID}]}), + socks5_send(Socks5, Data), + disconnect(Config). + +all_slave(Config) -> + MyJID = my_jid(Config), + Peer = ?config(master, Config), + #presence{} = send_recv(Config, #presence{}), + wait_for_master(Config), + {StreamHost, SID, Data} = get_event(Config), + Socks5 = socks5_connect(StreamHost, {SID, Peer, MyJID}), + wait_for_master(Config), + socks5_recv(Socks5, Data), + disconnect(Config). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +single_test(T) -> + list_to_atom("proxy65_" ++ atom_to_list(T)). + +master_slave_test(T) -> + {list_to_atom("proxy65_" ++ atom_to_list(T)), [parallel], + [list_to_atom("proxy65_" ++ atom_to_list(T) ++ "_master"), + list_to_atom("proxy65_" ++ atom_to_list(T) ++ "_slave")]}. + +socks5_connect(#streamhost{host = Host, port = Port}, + {SID, JID1, JID2}) -> + Hash = p1_sha:sha([SID, jid:encode(JID1), jid:encode(JID2)]), + {ok, Sock} = gen_tcp:connect(binary_to_list(Host), Port, + [binary, {active, false}]), + Init = <>, + InitAck = <>, + Req = <>, + Resp = <>, + gen_tcp:send(Sock, Init), + {ok, InitAck} = gen_tcp:recv(Sock, size(InitAck)), + gen_tcp:send(Sock, Req), + {ok, Resp} = gen_tcp:recv(Sock, size(Resp)), + Sock. + +socks5_send(Sock, Data) -> + ok = gen_tcp:send(Sock, Data). + +socks5_recv(Sock, Data) -> + {ok, Data} = gen_tcp:recv(Sock, size(Data)). diff --git a/test/pubsub_tests.erl b/test/pubsub_tests.erl new file mode 100644 index 000000000..1cb02f020 --- /dev/null +++ b/test/pubsub_tests.erl @@ -0,0 +1,765 @@ +%%%------------------------------------------------------------------- +%%% Author : Evgeny Khramtsov +%%% Created : 16 Nov 2016 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(pubsub_tests). + +%% API +-compile(export_all). +-import(suite, [pubsub_jid/1, send_recv/2, get_features/2, disconnect/1, + put_event/2, get_event/1, wait_for_master/1, wait_for_slave/1, + recv_message/1, my_jid/1, send/2, recv_presence/1, recv/1]). + +-include("suite.hrl"). +-include_lib("stdlib/include/assert.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +%%%=================================================================== +%%% Single user tests +%%%=================================================================== +single_cases() -> + {pubsub_single, [sequence], + [single_test(test_features), + single_test(test_vcard), + single_test(test_create), + single_test(test_configure), + single_test(test_delete), + single_test(test_get_affiliations), + single_test(test_get_subscriptions), + single_test(test_create_instant), + single_test(test_default), + single_test(test_create_configure), + single_test(test_publish), + single_test(test_auto_create), + single_test(test_get_items), + single_test(test_delete_item), + single_test(test_purge), + single_test(test_subscribe), + single_test(test_subscribe_max_item_1), + single_test(test_unsubscribe)]}. + +test_features(Config) -> + PJID = pubsub_jid(Config), + AllFeatures = sets:from_list(get_features(Config, PJID)), + NeededFeatures = sets:from_list( + [?NS_PUBSUB, + ?PUBSUB("access-open"), + ?PUBSUB("access-authorize"), + ?PUBSUB("create-nodes"), + ?PUBSUB("instant-nodes"), + ?PUBSUB("config-node"), + ?PUBSUB("retrieve-default"), + ?PUBSUB("create-and-configure"), + ?PUBSUB("publish"), + ?PUBSUB("auto-create"), + ?PUBSUB("retrieve-items"), + ?PUBSUB("delete-items"), + ?PUBSUB("subscribe"), + ?PUBSUB("retrieve-affiliations"), + ?PUBSUB("modify-affiliations"), + ?PUBSUB("retrieve-subscriptions"), + ?PUBSUB("manage-subscriptions"), + ?PUBSUB("purge-nodes"), + ?PUBSUB("delete-nodes")]), + true = sets:is_subset(NeededFeatures, AllFeatures), + disconnect(Config). + +test_vcard(Config) -> + JID = pubsub_jid(Config), + ct:comment("Retrieving vCard from ~s", [jid:encode(JID)]), + VCard = mod_pubsub_opt:vcard(?config(server, Config)), + #iq{type = result, sub_els = [VCard]} = + send_recv(Config, #iq{type = get, to = JID, sub_els = [#vcard_temp{}]}), + disconnect(Config). + +test_create(Config) -> + Node = ?config(pubsub_node, Config), + Node = create_node(Config, Node), + disconnect(Config). + +test_create_instant(Config) -> + Node = create_node(Config, <<>>), + delete_node(Config, Node), + disconnect(Config). + +test_configure(Config) -> + Node = ?config(pubsub_node, Config), + NodeTitle = ?config(pubsub_node_title, Config), + NodeConfig = get_node_config(Config, Node), + MyNodeConfig = set_opts(NodeConfig, + [{title, NodeTitle}]), + set_node_config(Config, Node, MyNodeConfig), + NewNodeConfig = get_node_config(Config, Node), + NodeTitle = proplists:get_value(title, NewNodeConfig, <<>>), + disconnect(Config). + +test_default(Config) -> + get_default_node_config(Config), + disconnect(Config). + +test_create_configure(Config) -> + NodeTitle = ?config(pubsub_node_title, Config), + DefaultNodeConfig = get_default_node_config(Config), + CustomNodeConfig = set_opts(DefaultNodeConfig, + [{title, NodeTitle}]), + Node = create_node(Config, <<>>, CustomNodeConfig), + NodeConfig = get_node_config(Config, Node), + NodeTitle = proplists:get_value(title, NodeConfig, <<>>), + delete_node(Config, Node), + disconnect(Config). + +test_publish(Config) -> + Node = create_node(Config, <<>>), + publish_item(Config, Node), + delete_node(Config, Node), + disconnect(Config). + +test_auto_create(Config) -> + Node = p1_rand:get_string(), + publish_item(Config, Node), + delete_node(Config, Node), + disconnect(Config). + +test_get_items(Config) -> + Node = create_node(Config, <<>>), + ItemsIn = [publish_item(Config, Node) || _ <- lists:seq(1, 5)], + ItemsOut = get_items(Config, Node), + true = [I || #ps_item{id = I} <- lists:sort(ItemsIn)] + == [I || #ps_item{id = I} <- lists:sort(ItemsOut)], + delete_node(Config, Node), + disconnect(Config). + +test_delete_item(Config) -> + Node = create_node(Config, <<>>), + #ps_item{id = I} = publish_item(Config, Node), + [#ps_item{id = I}] = get_items(Config, Node), + delete_item(Config, Node, I), + [] = get_items(Config, Node), + delete_node(Config, Node), + disconnect(Config). + +test_subscribe(Config) -> + Node = create_node(Config, <<>>), + #ps_subscription{type = subscribed} = subscribe_node(Config, Node), + [#ps_subscription{node = Node}] = get_subscriptions(Config), + delete_node(Config, Node), + disconnect(Config). + +test_subscribe_max_item_1(Config) -> + DefaultNodeConfig = get_default_node_config(Config), + CustomNodeConfig = set_opts(DefaultNodeConfig, + [{max_items, 1}]), + Node = create_node(Config, <<>>, CustomNodeConfig), + #ps_subscription{type = subscribed} = subscribe_node(Config, Node), + [#ps_subscription{node = Node}] = get_subscriptions(Config), + delete_node(Config, Node), + disconnect(Config). + +test_unsubscribe(Config) -> + Node = create_node(Config, <<>>), + subscribe_node(Config, Node), + [#ps_subscription{node = Node}] = get_subscriptions(Config), + unsubscribe_node(Config, Node), + [] = get_subscriptions(Config), + delete_node(Config, Node), + disconnect(Config). + +test_get_affiliations(Config) -> + Nodes = lists:sort([create_node(Config, <<>>) || _ <- lists:seq(1, 5)]), + Affs = get_affiliations(Config), + ?assertEqual(Nodes, lists:sort([Node || #ps_affiliation{node = Node, + type = owner} <- Affs])), + [delete_node(Config, Node) || Node <- Nodes], + disconnect(Config). + +test_get_subscriptions(Config) -> + Nodes = lists:sort([create_node(Config, <<>>) || _ <- lists:seq(1, 5)]), + [subscribe_node(Config, Node) || Node <- Nodes], + Subs = get_subscriptions(Config), + ?assertEqual(Nodes, lists:sort([Node || #ps_subscription{node = Node} <- Subs])), + [delete_node(Config, Node) || Node <- Nodes], + disconnect(Config). + +test_purge(Config) -> + Node = create_node(Config, <<>>), + ItemsIn = [publish_item(Config, Node) || _ <- lists:seq(1, 5)], + ItemsOut = get_items(Config, Node), + true = [I || #ps_item{id = I} <- lists:sort(ItemsIn)] + == [I || #ps_item{id = I} <- lists:sort(ItemsOut)], + purge_node(Config, Node), + [] = get_items(Config, Node), + delete_node(Config, Node), + disconnect(Config). + +test_delete(Config) -> + Node = ?config(pubsub_node, Config), + delete_node(Config, Node), + disconnect(Config). + +%%%=================================================================== +%%% Master-slave tests +%%%=================================================================== +master_slave_cases() -> + {pubsub_master_slave, [sequence], + [master_slave_test(publish), + master_slave_test(subscriptions), + master_slave_test(affiliations), + master_slave_test(authorize)]}. + +publish_master(Config) -> + Node = create_node(Config, <<>>), + put_event(Config, Node), + ready = get_event(Config), + #ps_item{id = ID} = publish_item(Config, Node), + #ps_item{id = ID} = get_event(Config), + delete_node(Config, Node), + disconnect(Config). + +publish_slave(Config) -> + Node = get_event(Config), + subscribe_node(Config, Node), + put_event(Config, ready), + #message{ + sub_els = + [#ps_event{ + items = #ps_items{node = Node, + items = [Item]}}]} = recv_message(Config), + put_event(Config, Item), + disconnect(Config). + +subscriptions_master(Config) -> + Peer = ?config(slave, Config), + Node = ?config(pubsub_node, Config), + Node = create_node(Config, Node), + [] = get_subscriptions(Config, Node), + wait_for_slave(Config), + lists:foreach( + fun(Type) -> + ok = set_subscriptions(Config, Node, [{Peer, Type}]), + #ps_item{} = publish_item(Config, Node), + case get_subscriptions(Config, Node) of + [] when Type == none; Type == pending -> + ok; + [#ps_subscription{jid = Peer, type = Type}] -> + ok + end + end, [subscribed, unconfigured, pending, none]), + delete_node(Config, Node), + disconnect(Config). + +subscriptions_slave(Config) -> + wait_for_master(Config), + MyJID = my_jid(Config), + Node = ?config(pubsub_node, Config), + lists:foreach( + fun(subscribed = Type) -> + ?recv2(#message{ + sub_els = + [#ps_event{ + subscription = #ps_subscription{ + node = Node, + jid = MyJID, + type = Type}}]}, + #message{sub_els = [#ps_event{}]}); + (Type) -> + #message{ + sub_els = + [#ps_event{ + subscription = #ps_subscription{ + node = Node, + jid = MyJID, + type = Type}}]} = + recv_message(Config) + end, [subscribed, unconfigured, pending, none]), + disconnect(Config). + +affiliations_master(Config) -> + Peer = ?config(slave, Config), + BarePeer = jid:remove_resource(Peer), + lists:foreach( + fun(Aff) -> + Node = <<(atom_to_binary(Aff, utf8))/binary, + $-, (p1_rand:get_string())/binary>>, + create_node(Config, Node, default_node_config(Config)), + #ps_item{id = I} = publish_item(Config, Node), + ok = set_affiliations(Config, Node, [{Peer, Aff}]), + Affs = get_affiliations(Config, Node), + case lists:keyfind(BarePeer, #ps_affiliation.jid, Affs) of + false when Aff == none -> + ok; + #ps_affiliation{type = Aff} -> + ok + end, + put_event(Config, {Aff, Node, I}), + wait_for_slave(Config), + delete_node(Config, Node) + end, [outcast, none, member, publish_only, publisher, owner]), + put_event(Config, disconnect), + disconnect(Config). + +affiliations_slave(Config) -> + affiliations_slave(Config, get_event(Config)). + +affiliations_slave(Config, {outcast, Node, ItemID}) -> + #stanza_error{reason = 'forbidden'} = subscribe_node(Config, Node), + #stanza_error{} = unsubscribe_node(Config, Node), + #stanza_error{reason = 'forbidden'} = get_items(Config, Node), + #stanza_error{reason = 'forbidden'} = publish_item(Config, Node), + #stanza_error{reason = 'forbidden'} = delete_item(Config, Node, ItemID), + #stanza_error{reason = 'forbidden'} = purge_node(Config, Node), + #stanza_error{reason = 'forbidden'} = get_node_config(Config, Node), + #stanza_error{reason = 'forbidden'} = + set_node_config(Config, Node, default_node_config(Config)), + #stanza_error{reason = 'forbidden'} = get_subscriptions(Config, Node), + #stanza_error{reason = 'forbidden'} = + set_subscriptions(Config, Node, [{my_jid(Config), subscribed}]), + #stanza_error{reason = 'forbidden'} = get_affiliations(Config, Node), + #stanza_error{reason = 'forbidden'} = + set_affiliations(Config, Node, [{?config(master, Config), outcast}, + {my_jid(Config), owner}]), + #stanza_error{reason = 'forbidden'} = delete_node(Config, Node), + wait_for_master(Config), + affiliations_slave(Config, get_event(Config)); +affiliations_slave(Config, {none, Node, ItemID}) -> + #ps_subscription{type = subscribed} = subscribe_node(Config, Node), + ok = unsubscribe_node(Config, Node), + %% This violates the affiliation char from section 4.1 + [_|_] = get_items(Config, Node), + #stanza_error{reason = 'forbidden'} = publish_item(Config, Node), + #stanza_error{reason = 'forbidden'} = delete_item(Config, Node, ItemID), + #stanza_error{reason = 'forbidden'} = purge_node(Config, Node), + #stanza_error{reason = 'forbidden'} = get_node_config(Config, Node), + #stanza_error{reason = 'forbidden'} = + set_node_config(Config, Node, default_node_config(Config)), + #stanza_error{reason = 'forbidden'} = get_subscriptions(Config, Node), + #stanza_error{reason = 'forbidden'} = + set_subscriptions(Config, Node, [{my_jid(Config), subscribed}]), + #stanza_error{reason = 'forbidden'} = get_affiliations(Config, Node), + #stanza_error{reason = 'forbidden'} = + set_affiliations(Config, Node, [{?config(master, Config), outcast}, + {my_jid(Config), owner}]), + #stanza_error{reason = 'forbidden'} = delete_node(Config, Node), + wait_for_master(Config), + affiliations_slave(Config, get_event(Config)); +affiliations_slave(Config, {member, Node, ItemID}) -> + #ps_subscription{type = subscribed} = subscribe_node(Config, Node), + ok = unsubscribe_node(Config, Node), + [_|_] = get_items(Config, Node), + #stanza_error{reason = 'forbidden'} = publish_item(Config, Node), + #stanza_error{reason = 'forbidden'} = delete_item(Config, Node, ItemID), + #stanza_error{reason = 'forbidden'} = purge_node(Config, Node), + #stanza_error{reason = 'forbidden'} = get_node_config(Config, Node), + #stanza_error{reason = 'forbidden'} = + set_node_config(Config, Node, default_node_config(Config)), + #stanza_error{reason = 'forbidden'} = get_subscriptions(Config, Node), + #stanza_error{reason = 'forbidden'} = + set_subscriptions(Config, Node, [{my_jid(Config), subscribed}]), + #stanza_error{reason = 'forbidden'} = get_affiliations(Config, Node), + #stanza_error{reason = 'forbidden'} = + set_affiliations(Config, Node, [{?config(master, Config), outcast}, + {my_jid(Config), owner}]), + #stanza_error{reason = 'forbidden'} = delete_node(Config, Node), + wait_for_master(Config), + affiliations_slave(Config, get_event(Config)); +affiliations_slave(Config, {publish_only, Node, ItemID}) -> + #stanza_error{reason = 'forbidden'} = subscribe_node(Config, Node), + #stanza_error{} = unsubscribe_node(Config, Node), + #stanza_error{reason = 'forbidden'} = get_items(Config, Node), + #ps_item{id = _MyItemID} = publish_item(Config, Node), + %% BUG: This should be fixed + %% ?match(ok, delete_item(Config, Node, MyItemID)), + #stanza_error{reason = 'forbidden'} = delete_item(Config, Node, ItemID), + #stanza_error{reason = 'forbidden'} = purge_node(Config, Node), + #stanza_error{reason = 'forbidden'} = get_node_config(Config, Node), + #stanza_error{reason = 'forbidden'} = + set_node_config(Config, Node, default_node_config(Config)), + #stanza_error{reason = 'forbidden'} = get_subscriptions(Config, Node), + #stanza_error{reason = 'forbidden'} = + set_subscriptions(Config, Node, [{my_jid(Config), subscribed}]), + #stanza_error{reason = 'forbidden'} = get_affiliations(Config, Node), + #stanza_error{reason = 'forbidden'} = + set_affiliations(Config, Node, [{?config(master, Config), outcast}, + {my_jid(Config), owner}]), + #stanza_error{reason = 'forbidden'} = delete_node(Config, Node), + wait_for_master(Config), + affiliations_slave(Config, get_event(Config)); +affiliations_slave(Config, {publisher, Node, _ItemID}) -> + #ps_subscription{type = subscribed} = subscribe_node(Config, Node), + ok = unsubscribe_node(Config, Node), + [_|_] = get_items(Config, Node), + #ps_item{id = MyItemID} = publish_item(Config, Node), + ok = delete_item(Config, Node, MyItemID), + %% BUG: this should be fixed + %% #stanza_error{reason = 'forbidden'} = delete_item(Config, Node, ItemID), + #stanza_error{reason = 'forbidden'} = purge_node(Config, Node), + #stanza_error{reason = 'forbidden'} = get_node_config(Config, Node), + #stanza_error{reason = 'forbidden'} = + set_node_config(Config, Node, default_node_config(Config)), + #stanza_error{reason = 'forbidden'} = get_subscriptions(Config, Node), + #stanza_error{reason = 'forbidden'} = + set_subscriptions(Config, Node, [{my_jid(Config), subscribed}]), + #stanza_error{reason = 'forbidden'} = get_affiliations(Config, Node), + #stanza_error{reason = 'forbidden'} = + set_affiliations(Config, Node, [{?config(master, Config), outcast}, + {my_jid(Config), owner}]), + #stanza_error{reason = 'forbidden'} = delete_node(Config, Node), + wait_for_master(Config), + affiliations_slave(Config, get_event(Config)); +affiliations_slave(Config, {owner, Node, ItemID}) -> + MyJID = my_jid(Config), + Peer = ?config(master, Config), + #ps_subscription{type = subscribed} = subscribe_node(Config, Node), + ok = unsubscribe_node(Config, Node), + [_|_] = get_items(Config, Node), + #ps_item{id = MyItemID} = publish_item(Config, Node), + ok = delete_item(Config, Node, MyItemID), + ok = delete_item(Config, Node, ItemID), + ok = purge_node(Config, Node), + [_|_] = get_node_config(Config, Node), + ok = set_node_config(Config, Node, default_node_config(Config)), + ok = set_subscriptions(Config, Node, []), + [] = get_subscriptions(Config, Node), + ok = set_affiliations(Config, Node, [{Peer, outcast}, {MyJID, owner}]), + [_, _] = get_affiliations(Config, Node), + ok = delete_node(Config, Node), + wait_for_master(Config), + affiliations_slave(Config, get_event(Config)); +affiliations_slave(Config, disconnect) -> + disconnect(Config). + +authorize_master(Config) -> + send(Config, #presence{}), + #presence{} = recv_presence(Config), + Peer = ?config(slave, Config), + PJID = pubsub_jid(Config), + NodeConfig = set_opts(default_node_config(Config), + [{access_model, authorize}]), + Node = ?config(pubsub_node, Config), + Node = create_node(Config, Node, NodeConfig), + wait_for_slave(Config), + #message{sub_els = [#xdata{fields = F1}]} = recv_message(Config), + C1 = pubsub_subscribe_authorization:decode(F1), + Node = proplists:get_value(node, C1), + Peer = proplists:get_value(subscriber_jid, C1), + %% Deny it at first + Deny = #xdata{type = submit, + fields = pubsub_subscribe_authorization:encode( + [{node, Node}, + {subscriber_jid, Peer}, + {allow, false}])}, + send(Config, #message{to = PJID, sub_els = [Deny]}), + %% We should not have any subscriptions + [] = get_subscriptions(Config, Node), + wait_for_slave(Config), + #message{sub_els = [#xdata{fields = F2}]} = recv_message(Config), + C2 = pubsub_subscribe_authorization:decode(F2), + Node = proplists:get_value(node, C2), + Peer = proplists:get_value(subscriber_jid, C2), + %% Now we accept is as the peer is very insisting ;) + Approve = #xdata{type = submit, + fields = pubsub_subscribe_authorization:encode( + [{node, Node}, + {subscriber_jid, Peer}, + {allow, true}])}, + send(Config, #message{to = PJID, sub_els = [Approve]}), + wait_for_slave(Config), + delete_node(Config, Node), + disconnect(Config). + +authorize_slave(Config) -> + Node = ?config(pubsub_node, Config), + MyJID = my_jid(Config), + wait_for_master(Config), + #ps_subscription{type = pending} = subscribe_node(Config, Node), + %% We're denied at first + #message{ + sub_els = + [#ps_event{ + subscription = #ps_subscription{type = none, + jid = MyJID}}]} = + recv_message(Config), + wait_for_master(Config), + #ps_subscription{type = pending} = subscribe_node(Config, Node), + %% Now much better! + #message{ + sub_els = + [#ps_event{ + subscription = #ps_subscription{type = subscribed, + jid = MyJID}}]} = + recv_message(Config), + wait_for_master(Config), + disconnect(Config). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +single_test(T) -> + list_to_atom("pubsub_" ++ atom_to_list(T)). + +master_slave_test(T) -> + {list_to_atom("pubsub_" ++ atom_to_list(T)), [parallel], + [list_to_atom("pubsub_" ++ atom_to_list(T) ++ "_master"), + list_to_atom("pubsub_" ++ atom_to_list(T) ++ "_slave")]}. + +set_opts(Config, Options) -> + lists:foldl( + fun({Opt, Val}, Acc) -> + lists:keystore(Opt, 1, Acc, {Opt, Val}) + end, Config, Options). + +create_node(Config, Node) -> + create_node(Config, Node, undefined). + +create_node(Config, Node, Options) -> + PJID = pubsub_jid(Config), + NodeConfig = if is_list(Options) -> + #xdata{type = submit, + fields = pubsub_node_config:encode(Options)}; + true -> + undefined + end, + case send_recv(Config, + #iq{type = set, to = PJID, + sub_els = [#pubsub{create = Node, + configure = {<<>>, NodeConfig}}]}) of + #iq{type = result, sub_els = [#pubsub{create = NewNode}]} -> + NewNode; + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) + end. + +delete_node(Config, Node) -> + PJID = pubsub_jid(Config), + case send_recv(Config, + #iq{type = set, to = PJID, + sub_els = [#pubsub_owner{delete = {Node, <<>>}}]}) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) + end. + +purge_node(Config, Node) -> + PJID = pubsub_jid(Config), + case send_recv(Config, + #iq{type = set, to = PJID, + sub_els = [#pubsub_owner{purge = Node}]}) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) + end. + +get_default_node_config(Config) -> + PJID = pubsub_jid(Config), + case send_recv(Config, + #iq{type = get, to = PJID, + sub_els = [#pubsub_owner{default = {<<>>, undefined}}]}) of + #iq{type = result, + sub_els = [#pubsub_owner{default = {<<>>, NodeConfig}}]} -> + pubsub_node_config:decode(NodeConfig#xdata.fields); + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) + end. + +get_node_config(Config, Node) -> + PJID = pubsub_jid(Config), + case send_recv(Config, + #iq{type = get, to = PJID, + sub_els = [#pubsub_owner{configure = {Node, undefined}}]}) of + #iq{type = result, + sub_els = [#pubsub_owner{configure = {Node, NodeConfig}}]} -> + pubsub_node_config:decode(NodeConfig#xdata.fields); + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) + end. + +set_node_config(Config, Node, Options) -> + PJID = pubsub_jid(Config), + NodeConfig = #xdata{type = submit, + fields = pubsub_node_config:encode(Options)}, + case send_recv(Config, + #iq{type = set, to = PJID, + sub_els = [#pubsub_owner{configure = + {Node, NodeConfig}}]}) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) + end. + +publish_item(Config, Node) -> + PJID = pubsub_jid(Config), + ItemID = p1_rand:get_string(), + Item = #ps_item{id = ItemID, sub_els = [xmpp:encode(#presence{id = ItemID})]}, + case send_recv(Config, + #iq{type = set, to = PJID, + sub_els = [#pubsub{publish = #ps_publish{ + node = Node, + items = [Item]}}]}) of + #iq{type = result, + sub_els = [#pubsub{publish = #ps_publish{ + node = Node, + items = [#ps_item{id = ItemID}]}}]} -> + Item; + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) + end. + +get_items(Config, Node) -> + PJID = pubsub_jid(Config), + case send_recv(Config, + #iq{type = get, to = PJID, + sub_els = [#pubsub{items = #ps_items{node = Node}}]}) of + #iq{type = result, + sub_els = [#pubsub{items = #ps_items{node = Node, items = Items}}]} -> + Items; + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) + end. + +delete_item(Config, Node, I) -> + PJID = pubsub_jid(Config), + case send_recv(Config, + #iq{type = set, to = PJID, + sub_els = [#pubsub{retract = + #ps_retract{ + node = Node, + items = [#ps_item{id = I}]}}]}) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) + end. + +subscribe_node(Config, Node) -> + PJID = pubsub_jid(Config), + MyJID = my_jid(Config), + case send_recv(Config, + #iq{type = set, to = PJID, + sub_els = [#pubsub{subscribe = #ps_subscribe{ + node = Node, + jid = MyJID}}]}) of + #iq{type = result, + sub_els = [#pubsub{ + subscription = #ps_subscription{ + node = Node, + jid = MyJID} = Sub}]} -> + Sub; + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) + end. + +unsubscribe_node(Config, Node) -> + PJID = pubsub_jid(Config), + MyJID = my_jid(Config), + case send_recv(Config, + #iq{type = set, to = PJID, + sub_els = [#pubsub{ + unsubscribe = #ps_unsubscribe{ + node = Node, + jid = MyJID}}]}) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) + end. + +get_affiliations(Config) -> + PJID = pubsub_jid(Config), + case send_recv(Config, + #iq{type = get, to = PJID, + sub_els = [#pubsub{affiliations = {<<>>, []}}]}) of + #iq{type = result, + sub_els = [#pubsub{affiliations = {<<>>, Affs}}]} -> + Affs; + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) + end. + +get_affiliations(Config, Node) -> + PJID = pubsub_jid(Config), + case send_recv(Config, + #iq{type = get, to = PJID, + sub_els = [#pubsub_owner{affiliations = {Node, []}}]}) of + #iq{type = result, + sub_els = [#pubsub_owner{affiliations = {Node, Affs}}]} -> + Affs; + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) + end. + +set_affiliations(Config, Node, JTs) -> + PJID = pubsub_jid(Config), + Affs = [#ps_affiliation{jid = J, type = T} || {J, T} <- JTs], + case send_recv(Config, + #iq{type = set, to = PJID, + sub_els = [#pubsub_owner{affiliations = + {Node, Affs}}]}) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) + end. + +get_subscriptions(Config) -> + PJID = pubsub_jid(Config), + case send_recv(Config, + #iq{type = get, to = PJID, + sub_els = [#pubsub{subscriptions = {<<>>, []}}]}) of + #iq{type = result, sub_els = [#pubsub{subscriptions = {<<>>, Subs}}]} -> + Subs; + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) + end. + +get_subscriptions(Config, Node) -> + PJID = pubsub_jid(Config), + case send_recv(Config, + #iq{type = get, to = PJID, + sub_els = [#pubsub_owner{subscriptions = {Node, []}}]}) of + #iq{type = result, + sub_els = [#pubsub_owner{subscriptions = {Node, Subs}}]} -> + Subs; + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) + end. + +set_subscriptions(Config, Node, JTs) -> + PJID = pubsub_jid(Config), + Subs = [#ps_subscription{jid = J, type = T} || {J, T} <- JTs], + case send_recv(Config, + #iq{type = set, to = PJID, + sub_els = [#pubsub_owner{subscriptions = + {Node, Subs}}]}) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) + end. + +default_node_config(Config) -> + [{title, ?config(pubsub_node_title, Config)}, + {notify_delete, false}, + {send_last_published_item, never}]. diff --git a/test/push_tests.erl b/test/push_tests.erl new file mode 100644 index 000000000..9e400cccc --- /dev/null +++ b/test/push_tests.erl @@ -0,0 +1,234 @@ +%%%------------------------------------------------------------------- +%%% Author : Holger Weiss +%%% Created : 15 Jul 2017 by Holger Weiss +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(push_tests). + +%% API +-compile(export_all). +-import(suite, [close_socket/1, connect/1, disconnect/1, get_event/1, + get_features/2, make_iq_result/1, my_jid/1, put_event/2, recv/1, + recv_iq/1, recv_message/1, self_presence/2, send/2, send_recv/2, + server_jid/1]). + +-include("suite.hrl"). + +-define(PUSH_NODE, <<"d3v1c3">>). +-define(PUSH_XDATA_FIELDS, + [#xdata_field{var = <<"FORM_TYPE">>, + values = [?NS_PUBSUB_PUBLISH_OPTIONS]}, + #xdata_field{var = <<"secret">>, + values = [<<"c0nf1d3nt14l">>]}]). + +%%%=================================================================== +%%% API +%%%=================================================================== +%%%=================================================================== +%%% Single user tests +%%%=================================================================== +single_cases() -> + {push_single, [sequence], + [single_test(feature_enabled), + single_test(unsupported_iq)]}. + +feature_enabled(Config) -> + BareMyJID = jid:remove_resource(my_jid(Config)), + Features = get_features(Config, BareMyJID), + true = lists:member(?NS_PUSH_0, Features), + disconnect(Config). + +unsupported_iq(Config) -> + PushJID = my_jid(Config), + lists:foreach( + fun(SubEl) -> + #iq{type = error} = + send_recv(Config, #iq{type = get, sub_els = [SubEl]}) + end, [#push_enable{jid = PushJID}, #push_disable{jid = PushJID}]), + disconnect(Config). + +%%%=================================================================== +%%% Master-slave tests +%%%=================================================================== +master_slave_cases() -> + {push_master_slave, [sequence], + [master_slave_test(sm), + master_slave_test(offline), + master_slave_test(mam)]}. + +sm_master(Config) -> + ct:comment("Waiting for the slave to close the socket"), + peer_down = get_event(Config), + ct:comment("Waiting a bit in order to test the keepalive feature"), + ct:sleep(5000), % Without mod_push_keepalive, the session would time out. + ct:comment("Sending message to the slave"), + send_test_message(Config), + ct:comment("Handling push notification"), + handle_notification(Config), + ct:comment("Receiving bounced message from the slave"), + #message{type = error} = recv_message(Config), + ct:comment("Closing the connection"), + disconnect(Config). + +sm_slave(Config) -> + ct:comment("Enabling push notifications"), + ok = enable_push(Config), + ct:comment("Enabling stream management"), + ok = enable_sm(Config), + ct:comment("Closing the socket"), + close_socket(Config). + +offline_master(Config) -> + ct:comment("Waiting for the slave to be ready"), + ready = get_event(Config), + ct:comment("Sending message to the slave"), + send_test_message(Config), % No push notification, slave is online. + ct:comment("Waiting for the slave to disconnect"), + peer_down = get_event(Config), + ct:comment("Sending message to offline storage"), + send_test_message(Config), + ct:comment("Handling push notification for offline message"), + handle_notification(Config), + ct:comment("Closing the connection"), + disconnect(Config). + +offline_slave(Config) -> + ct:comment("Re-enabling push notifications"), + ok = enable_push(Config), + ct:comment("Letting the master know that we're ready"), + put_event(Config, ready), + ct:comment("Receiving message from the master"), + recv_test_message(Config), + ct:comment("Closing the connection"), + disconnect(Config). + +mam_master(Config) -> + ct:comment("Waiting for the slave to be ready"), + ready = get_event(Config), + ct:comment("Sending message to the slave"), + send_test_message(Config), + ct:comment("Handling push notification for MAM message"), + handle_notification(Config), + ct:comment("Closing the connection"), + disconnect(Config). + +mam_slave(Config) -> + self_presence(Config, available), + ct:comment("Receiving message from offline storage"), + recv_test_message(Config), + %% Don't re-enable push notifications, otherwise the notification would be + %% suppressed while the slave is online. + ct:comment("Enabling MAM"), + ok = enable_mam(Config), + ct:comment("Letting the master know that we're ready"), + put_event(Config, ready), + ct:comment("Receiving message from the master"), + recv_test_message(Config), + ct:comment("Waiting for the master to disconnect"), + peer_down = get_event(Config), + ct:comment("Disabling push notifications"), + ok = disable_push(Config), + ct:comment("Closing the connection and cleaning up"), + clean(disconnect(Config)). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +single_test(T) -> + list_to_atom("push_" ++ atom_to_list(T)). + +master_slave_test(T) -> + {list_to_atom("push_" ++ atom_to_list(T)), [parallel], + [list_to_atom("push_" ++ atom_to_list(T) ++ "_master"), + list_to_atom("push_" ++ atom_to_list(T) ++ "_slave")]}. + +enable_sm(Config) -> + send(Config, #sm_enable{xmlns = ?NS_STREAM_MGMT_3, resume = true}), + case recv(Config) of + #sm_enabled{resume = true} -> + ok; + #sm_failed{reason = Reason} -> + Reason + end. + +enable_mam(Config) -> + case send_recv( + Config, #iq{type = set, sub_els = [#mam_prefs{xmlns = ?NS_MAM_1, + default = always}]}) of + #iq{type = result} -> + ok; + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +enable_push(Config) -> + %% Usually, the push JID would be a server JID (such as push.example.com). + %% We specify the peer's full user JID instead, so the push notifications + %% will be sent to the peer. + PushJID = ?config(peer, Config), + XData = #xdata{type = submit, fields = ?PUSH_XDATA_FIELDS}, + case send_recv( + Config, #iq{type = set, + sub_els = [#push_enable{jid = PushJID, + node = ?PUSH_NODE, + xdata = XData}]}) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +disable_push(Config) -> + PushJID = ?config(peer, Config), + case send_recv( + Config, #iq{type = set, + sub_els = [#push_disable{jid = PushJID, + node = ?PUSH_NODE}]}) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +send_test_message(Config) -> + Peer = ?config(peer, Config), + Msg = #message{to = Peer, body = [#text{data = <<"test">>}]}, + send(Config, Msg). + +recv_test_message(Config) -> + Peer = ?config(peer, Config), + #message{from = Peer, + body = [#text{data = <<"test">>}]} = recv_message(Config). + +handle_notification(Config) -> + From = server_jid(Config), + Item = #ps_item{sub_els = [xmpp:encode(#push_notification{})]}, + Publish = #ps_publish{node = ?PUSH_NODE, items = [Item]}, + XData = #xdata{type = submit, fields = ?PUSH_XDATA_FIELDS}, + PubSub = #pubsub{publish = Publish, publish_options = XData}, + IQ = #iq{type = set, from = From, sub_els = [PubSub]} = recv_iq(Config), + send(Config, make_iq_result(IQ)). + +clean(Config) -> + {U, S, _} = jid:tolower(my_jid(Config)), + mod_push:remove_user(U, S), + mod_mam:remove_user(U, S), + Config. diff --git a/test/replaced_tests.erl b/test/replaced_tests.erl new file mode 100644 index 000000000..37e22b3ac --- /dev/null +++ b/test/replaced_tests.erl @@ -0,0 +1,70 @@ +%%%------------------------------------------------------------------- +%%% Author : Evgeny Khramtsov +%%% Created : 16 Nov 2016 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(replaced_tests). + +%% API +-compile(export_all). +-import(suite, [bind/1, wait_for_slave/1, wait_for_master/1, recv/1, + close_socket/1, disconnect/1]). + +-include("suite.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +%%%=================================================================== +%%% Single user tests +%%%=================================================================== +single_cases() -> + {replaced_single, [sequence], []}. + +%%%=================================================================== +%%% Master-slave tests +%%%=================================================================== +master_slave_cases() -> + {replaced_master_slave, [sequence], + [master_slave_test(conflict)]}. + +conflict_master(Config0) -> + Config = bind(Config0), + wait_for_slave(Config), + #stream_error{reason = conflict} = recv(Config), + {xmlstreamend, <<"stream:stream">>} = recv(Config), + close_socket(Config). + +conflict_slave(Config0) -> + wait_for_master(Config0), + Config = bind(Config0), + disconnect(Config). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +single_test(T) -> + list_to_atom("replaced_" ++ atom_to_list(T)). + +master_slave_test(T) -> + {list_to_atom("replaced_" ++ atom_to_list(T)), [parallel], + [list_to_atom("replaced_" ++ atom_to_list(T) ++ "_master"), + list_to_atom("replaced_" ++ atom_to_list(T) ++ "_slave")]}. diff --git a/test/roster_tests.erl b/test/roster_tests.erl new file mode 100644 index 000000000..8d096eea3 --- /dev/null +++ b/test/roster_tests.erl @@ -0,0 +1,592 @@ +%%%------------------------------------------------------------------- +%%% Author : Evgeny Khramtsov +%%% Created : 22 Oct 2016 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(roster_tests). + +%% API +-compile(export_all). +-import(suite, [send_recv/2, recv_iq/1, send/2, disconnect/1, del_roster/1, + del_roster/2, make_iq_result/1, wait_for_slave/1, + wait_for_master/1, recv_presence/1, self_presence/2, + put_event/2, get_event/1, match_failure/2, get_roster/1]). +-include("suite.hrl"). +-include("mod_roster.hrl"). + +-record(state, {subscription = none :: none | from | to | both, + peer_available = false, + pending_in = false :: boolean(), + pending_out = false :: boolean()}). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_TestCase, Config) -> + Config. + +stop(_TestCase, Config) -> + Config. + +%%%=================================================================== +%%% Single user tests +%%%=================================================================== +single_cases() -> + {roster_single, [sequence], + [single_test(feature_enabled), + single_test(iq_set_many_items), + single_test(iq_set_duplicated_groups), + single_test(iq_get_item), + single_test(iq_unexpected_element), + single_test(iq_set_ask), + single_test(set_item), + single_test(version)]}. + +feature_enabled(Config) -> + ct:comment("Checking if roster versioning stream feature is set"), + true = ?config(rosterver, Config), + disconnect(Config). + +set_item(Config) -> + JID = jid:decode(<<"nurse@example.com">>), + Item = #roster_item{jid = JID}, + {V1, Item} = set_items(Config, [Item]), + {V1, [Item]} = get_items(Config), + ItemWithGroups = Item#roster_item{groups = [<<"G1">>, <<"G2">>]}, + {V2, ItemWithGroups} = set_items(Config, [ItemWithGroups]), + {V2, [ItemWithGroups]} = get_items(Config), + {V3, Item} = set_items(Config, [Item]), + {V3, [Item]} = get_items(Config), + ItemWithName = Item#roster_item{name = <<"some name">>}, + {V4, ItemWithName} = set_items(Config, [ItemWithName]), + {V4, [ItemWithName]} = get_items(Config), + ItemRemoved = Item#roster_item{subscription = remove}, + {V5, ItemRemoved} = set_items(Config, [ItemRemoved]), + {V5, []} = get_items(Config), + del_roster(disconnect(Config), JID). + +iq_set_many_items(Config) -> + J1 = jid:decode(<<"nurse1@example.com">>), + J2 = jid:decode(<<"nurse2@example.com">>), + ct:comment("Trying to send roster-set with many elements"), + Items = [#roster_item{jid = J1}, #roster_item{jid = J2}], + #stanza_error{reason = 'bad-request'} = set_items(Config, Items), + disconnect(Config). + +iq_set_duplicated_groups(Config) -> + JID = jid:decode(<<"nurse@example.com">>), + G = p1_rand:get_string(), + ct:comment("Trying to send roster-set with duplicated groups"), + Item = #roster_item{jid = JID, groups = [G, G]}, + #stanza_error{reason = 'bad-request'} = set_items(Config, [Item]), + disconnect(Config). + +iq_set_ask(Config) -> + JID = jid:decode(<<"nurse@example.com">>), + ct:comment("Trying to send roster-set with 'ask' included"), + Item = #roster_item{jid = JID, ask = subscribe}, + #stanza_error{reason = 'bad-request'} = set_items(Config, [Item]), + disconnect(Config). + +iq_get_item(Config) -> + JID = jid:decode(<<"nurse@example.com">>), + ct:comment("Trying to send roster-get with element"), + #iq{type = error} = Err3 = + send_recv(Config, #iq{type = get, + sub_els = [#roster_query{ + items = [#roster_item{jid = JID}]}]}), + #stanza_error{reason = 'bad-request'} = xmpp:get_error(Err3), + disconnect(Config). + +iq_unexpected_element(Config) -> + JID = jid:decode(<<"nurse@example.com">>), + ct:comment("Trying to send IQs with unexpected element"), + lists:foreach( + fun(Type) -> + #iq{type = error} = Err4 = + send_recv(Config, #iq{type = Type, + sub_els = [#roster_item{jid = JID}]}), + #stanza_error{reason = 'service-unavailable'} = xmpp:get_error(Err4) + end, [get, set]), + disconnect(Config). + +version(Config) -> + JID = jid:decode(<<"nurse@example.com">>), + ct:comment("Requesting roster"), + {InitialVersion, _} = get_items(Config, <<"">>), + ct:comment("Requesting roster with initial version"), + {empty, []} = get_items(Config, InitialVersion), + ct:comment("Adding JID to the roster"), + {NewVersion, _} = set_items(Config, [#roster_item{jid = JID}]), + ct:comment("Requesting roster with initial version"), + {NewVersion, _} = get_items(Config, InitialVersion), + ct:comment("Requesting roster with new version"), + {empty, []} = get_items(Config, NewVersion), + del_roster(disconnect(Config), JID). + +%%%=================================================================== +%%% Master-slave tests +%%%=================================================================== +master_slave_cases() -> + {roster_master_slave, [sequence], + [master_slave_test(subscribe)]}. + +subscribe_master(Config) -> + Actions = actions(), + process_subscriptions_master(Config, Actions), + del_roster(disconnect(Config)). + +subscribe_slave(Config) -> + process_subscriptions_slave(Config), + del_roster(disconnect(Config)). + +process_subscriptions_master(Config, Actions) -> + EnumeratedActions = lists:zip(lists:seq(1, length(Actions)), Actions), + self_presence(Config, available), + Peer = ?config(peer, Config), + lists:foldl( + fun({N, {Dir, Type}}, State) -> + if Dir == out -> put_event(Config, {N, in, Type}); + Dir == in -> put_event(Config, {N, out, Type}) + end, + Roster = get_roster(Config), + ct:pal("Performing ~s-~s (#~p) " + "in state:~n~s~nwith roster:~n~s", + [Dir, Type, N, pp(State), pp(Roster)]), + check_roster(Roster, Config, State), + wait_for_slave(Config), + Id = mk_id(N, Dir, Type), + NewState = transition(Id, Config, Dir, Type, State), + wait_for_slave(Config), + send_recv(Config, #iq{type = get, to = Peer, id = Id, + sub_els = [#ping{}]}), + check_roster_item(Config, NewState), + NewState + end, #state{}, EnumeratedActions), + put_event(Config, done), + wait_for_slave(Config), + Config. + +process_subscriptions_slave(Config) -> + self_presence(Config, available), + process_subscriptions_slave(Config, get_event(Config), #state{}). + +process_subscriptions_slave(Config, done, _State) -> + wait_for_master(Config), + Config; +process_subscriptions_slave(Config, {N, Dir, Type}, State) -> + Roster = get_roster(Config), + ct:pal("Performing ~s-~s (#~p) " + "in state:~n~s~nwith roster:~n~s", + [Dir, Type, N, pp(State), pp(Roster)]), + check_roster(Roster, Config, State), + wait_for_master(Config), + NewState = transition(mk_id(N, Dir, Type), Config, Dir, Type, State), + wait_for_master(Config), + send(Config, xmpp:make_iq_result(recv_iq(Config))), + check_roster_item(Config, NewState), + process_subscriptions_slave(Config, get_event(Config), NewState). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +single_test(T) -> + list_to_atom("roster_" ++ atom_to_list(T)). + +master_slave_test(T) -> + {list_to_atom("roster_" ++ atom_to_list(T)), [parallel], + [list_to_atom("roster_" ++ atom_to_list(T) ++ "_master"), + list_to_atom("roster_" ++ atom_to_list(T) ++ "_slave")]}. + +get_items(Config) -> + get_items(Config, <<"">>). + +get_items(Config, Version) -> + case send_recv(Config, #iq{type = get, + sub_els = [#roster_query{ver = Version}]}) of + #iq{type = result, + sub_els = [#roster_query{ver = NewVersion, items = 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) -> + lists:keyfind(JID, #roster_item.jid, Items); + _ -> + false + end. + +set_items(Config, Items) -> + case send_recv(Config, #iq{type = set, + sub_els = [#roster_query{items = Items}]}) of + #iq{type = result, sub_els = []} -> + recv_push(Config); + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +recv_push(Config) -> + ct:comment("Receiving roster push"), + Push = #iq{type = set, + sub_els = [#roster_query{ver = Ver, items = [PushItem]}]} + = recv_iq(Config), + send(Config, make_iq_result(Push)), + {Ver, PushItem}. + +recv_push(Config, Subscription, Ask) -> + PeerJID = ?config(peer, Config), + PeerBareJID = jid:remove_resource(PeerJID), + Match = #roster_item{jid = PeerBareJID, + subscription = Subscription, + ask = Ask, + groups = [], + name = <<"">>}, + ct:comment("Receiving roster push"), + Push = #iq{type = set, sub_els = [#roster_query{items = [Item]}]} = + recv_iq(Config), + case Item of + Match -> send(Config, make_iq_result(Push)); + _ -> match_failure(Item, Match) + end. + +recv_presence(Config, Type) -> + PeerJID = ?config(peer, Config), + case recv_presence(Config) of + #presence{from = PeerJID, type = Type} -> ok; + Pres -> match_failure(Pres, #presence{from = PeerJID, type = Type}) + end. + +recv_subscription(Config, Type) -> + PeerJID = ?config(peer, Config), + PeerBareJID = jid:remove_resource(PeerJID), + case recv_presence(Config) of + #presence{from = PeerBareJID, type = Type} -> ok; + Pres -> match_failure(Pres, #presence{from = PeerBareJID, type = Type}) + end. + +pp(Term) -> + io_lib_pretty:print(Term, fun pp/2). + +pp(state, N) -> + Fs = record_info(fields, state), + try N = length(Fs), Fs + catch _:_ -> no end; +pp(roster, N) -> + Fs = record_info(fields, roster), + try N = length(Fs), Fs + catch _:_ -> no end; +pp(_, _) -> no. + +mk_id(N, Dir, Type) -> + list_to_binary([integer_to_list(N), $-, atom_to_list(Dir), + $-, atom_to_list(Type)]). + +check_roster([], _Config, _State) -> + ok; +check_roster([Roster], _Config, State) -> + case {Roster#roster.subscription == State#state.subscription, + Roster#roster.ask, State#state.pending_in, State#state.pending_out} of + {true, both, true, true} -> ok; + {true, in, true, false} -> ok; + {true, out, false, true} -> ok; + {true, none, false, false} -> ok; + _ -> + ct:fail({roster_mismatch, State, Roster}) + end. + +check_roster_item(Config, State) -> + Peer = jid:remove_resource(?config(peer, Config)), + RosterItem = case get_item(Config, Peer) of + false -> #roster_item{}; + Item -> Item + end, + case {RosterItem#roster_item.subscription == State#state.subscription, + RosterItem#roster_item.ask, State#state.pending_out} of + {true, subscribe, true} -> ok; + {true, undefined, false} -> ok; + _ -> ct:fail({roster_item_mismatch, State, RosterItem}) + end. + +%% RFC6121, A.2.1 +transition(Id, Config, out, subscribe, + #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> + PeerJID = ?config(peer, Config), + PeerBareJID = jid:remove_resource(PeerJID), + send(Config, #presence{id = Id, to = PeerBareJID, type = subscribe}), + case {Sub, Out, In} of + {none, false, _} -> + recv_push(Config, none, subscribe), + State#state{pending_out = true}; + {none, true, false} -> + %% BUG: we should not receive roster push here + recv_push(Config, none, subscribe), + State; + {from, false, false} -> + recv_push(Config, from, subscribe), + State#state{pending_out = true}; + _ -> + State + end; +%% RFC6121, A.2.2 +transition(Id, Config, out, unsubscribe, + #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> + PeerJID = ?config(peer, Config), + PeerBareJID = jid:remove_resource(PeerJID), + send(Config, #presence{id = Id, to = PeerBareJID, type = unsubscribe}), + case {Sub, Out, In} of + {none, true, _} -> + recv_push(Config, none, undefined), + State#state{pending_out = false}; + {to, false, _} -> + recv_push(Config, none, undefined), + recv_presence(Config, unavailable), + State#state{subscription = none, peer_available = false}; + {from, true, false} -> + recv_push(Config, from, undefined), + State#state{pending_out = false}; + {both, false, false} -> + recv_push(Config, from, undefined), + recv_presence(Config, unavailable), + State#state{subscription = from, peer_available = false}; + _ -> + State + end; +%% RFC6121, A.2.3 +transition(Id, Config, out, subscribed, + #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> + PeerJID = ?config(peer, Config), + PeerBareJID = jid:remove_resource(PeerJID), + send(Config, #presence{id = Id, to = PeerBareJID, type = subscribed}), + case {Sub, Out, In} of + {none, false, true} -> + recv_push(Config, from, undefined), + State#state{subscription = from, pending_in = false}; + {none, true, true} -> + recv_push(Config, from, subscribe), + State#state{subscription = from, pending_in = false}; + {to, false, true} -> + recv_push(Config, both, undefined), + State#state{subscription = both, pending_in = false}; + {to, false, _} -> + %% BUG: we should not transition to 'both' state + recv_push(Config, both, undefined), + State#state{subscription = both}; + _ -> + State + end; +%% RFC6121, A.2.4 +transition(Id, Config, out, unsubscribed, + #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> + PeerJID = ?config(peer, Config), + PeerBareJID = jid:remove_resource(PeerJID), + send(Config, #presence{id = Id, to = PeerBareJID, type = unsubscribed}), + case {Sub, Out, In} of + {none, false, true} -> + State#state{subscription = none, pending_in = false}; + {none, true, true} -> + recv_push(Config, none, subscribe), + State#state{subscription = none, pending_in = false}; + {to, _, true} -> + State#state{pending_in = false}; + {from, false, _} -> + recv_push(Config, none, undefined), + State#state{subscription = none}; + {from, true, _} -> + recv_push(Config, none, subscribe), + State#state{subscription = none}; + {both, _, _} -> + recv_push(Config, to, undefined), + State#state{subscription = to}; + _ -> + State + end; +%% RFC6121, A.3.1 +transition(_, Config, in, subscribe = Type, + #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> + case {Sub, Out, In} of + {none, false, false} -> + recv_subscription(Config, Type), + State#state{pending_in = true}; + {none, true, false} -> + recv_push(Config, none, subscribe), + recv_subscription(Config, Type), + State#state{pending_in = true}; + {to, false, false} -> + %% BUG: we should not receive roster push in this state! + recv_push(Config, to, undefined), + recv_subscription(Config, Type), + State#state{pending_in = true}; + _ -> + State + end; +%% RFC6121, A.3.2 +transition(_, Config, in, unsubscribe = Type, + #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> + case {Sub, Out, In} of + {none, _, true} -> + State#state{pending_in = false}; + {to, _, true} -> + recv_push(Config, to, undefined), + recv_subscription(Config, Type), + State#state{pending_in = false}; + {from, false, _} -> + recv_push(Config, none, undefined), + recv_subscription(Config, Type), + State#state{subscription = none}; + {from, true, _} -> + recv_push(Config, none, subscribe), + recv_subscription(Config, Type), + State#state{subscription = none}; + {both, _, _} -> + recv_push(Config, to, undefined), + recv_subscription(Config, Type), + State#state{subscription = to}; + _ -> + State + end; +%% RFC6121, A.3.3 +transition(_, Config, in, subscribed = Type, + #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> + case {Sub, Out, In} of + {none, true, _} -> + recv_push(Config, to, undefined), + recv_subscription(Config, Type), + recv_presence(Config, available), + State#state{subscription = to, pending_out = false, peer_available = true}; + {from, true, _} -> + recv_push(Config, both, undefined), + recv_subscription(Config, Type), + recv_presence(Config, available), + State#state{subscription = both, pending_out = false, peer_available = true}; + {from, false, _} -> + %% BUG: we should not transition to 'both' in this state + recv_push(Config, both, undefined), + recv_subscription(Config, Type), + recv_presence(Config, available), + State#state{subscription = both, pending_out = false, peer_available = true}; + _ -> + State + end; +%% RFC6121, A.3.4 +transition(_, Config, in, unsubscribed = Type, + #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> + case {Sub, Out, In} of + {none, true, true} -> + %% BUG: we should receive roster push in this state! + recv_subscription(Config, Type), + State#state{subscription = none, pending_out = false}; + {none, true, false} -> + recv_push(Config, none, undefined), + recv_subscription(Config, Type), + State#state{subscription = none, pending_out = false}; + {none, false, false} -> + State; + {to, false, _} -> + recv_push(Config, none, undefined), + recv_presence(Config, unavailable), + recv_subscription(Config, Type), + State#state{subscription = none, peer_available = false}; + {from, true, false} -> + recv_push(Config, from, undefined), + recv_subscription(Config, Type), + State#state{subscription = from, pending_out = false}; + {both, _, _} -> + recv_push(Config, from, undefined), + recv_presence(Config, unavailable), + recv_subscription(Config, Type), + State#state{subscription = from, peer_available = false}; + _ -> + State + end; +%% Outgoing roster remove +transition(Id, Config, out, remove, + #state{subscription = Sub, pending_in = In, pending_out = Out}) -> + PeerJID = ?config(peer, Config), + PeerBareJID = jid:remove_resource(PeerJID), + Item = #roster_item{jid = PeerBareJID, subscription = remove}, + #iq{type = result, sub_els = []} = + send_recv(Config, #iq{type = set, id = Id, + sub_els = [#roster_query{items = [Item]}]}), + recv_push(Config, remove, undefined), + case {Sub, Out, In} of + {to, _, _} -> + recv_presence(Config, unavailable); + {both, _, _} -> + recv_presence(Config, unavailable); + _ -> + ok + end, + #state{}; +%% Incoming roster remove +transition(_, Config, in, remove, + #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> + case {Sub, Out, In} of + {none, true, _} -> + ok; + {from, false, _} -> + recv_push(Config, none, undefined), + recv_subscription(Config, unsubscribe); + {from, true, _} -> + recv_push(Config, none, subscribe), + recv_subscription(Config, unsubscribe); + {to, false, _} -> + %% BUG: we should receive push here + %% recv_push(Config, none, undefined), + recv_presence(Config, unavailable), + recv_subscription(Config, unsubscribed); + {both, _, _} -> + recv_presence(Config, unavailable), + recv_push(Config, to, undefined), + recv_subscription(Config, unsubscribe), + recv_push(Config, none, undefined), + recv_subscription(Config, unsubscribed); + _ -> + ok + end, + State#state{subscription = none}. + +actions() -> + States = [{Dir, Type} || Dir <- [out, in], + Type <- [subscribe, subscribed, + unsubscribe, unsubscribed, + remove]], + Actions = lists:flatten([[X, Y] || X <- States, Y <- States]), + remove_dups(Actions, []). + +remove_dups([X|T], [X,X|_] = Acc) -> + remove_dups(T, Acc); +remove_dups([X|T], Acc) -> + remove_dups(T, [X|Acc]); +remove_dups([], Acc) -> + lists:reverse(Acc). diff --git a/test/sm_tests.erl b/test/sm_tests.erl new file mode 100644 index 000000000..a55957856 --- /dev/null +++ b/test/sm_tests.erl @@ -0,0 +1,185 @@ +%%%------------------------------------------------------------------- +%%% Author : Evgeny Khramtsov +%%% Created : 16 Nov 2016 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(sm_tests). + +%% API +-compile(export_all). +-import(suite, [send/2, recv/1, close_socket/1, set_opt/3, my_jid/1, + recv_message/1, disconnect/1, send_recv/2, + put_event/2, get_event/1]). + +-include("suite.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +%%%=================================================================== +%%% Single user tests +%%%=================================================================== +single_cases() -> + {sm_single, [sequence], + [single_test(feature_enabled), + single_test(enable), + single_test(resume), + single_test(resume_failed)]}. + +feature_enabled(Config) -> + true = ?config(sm, Config), + disconnect(Config). + +enable(Config) -> + Server = ?config(server, Config), + ServerJID = jid:make(<<"">>, Server, <<"">>), + ct:comment("Send messages of type 'headline' so the server discards them silently"), + Msg = #message{to = ServerJID, type = headline, + body = [#text{data = <<"body">>}]}, + ct:comment("Enable the session management with resumption enabled"), + send(Config, #sm_enable{resume = true, xmlns = ?NS_STREAM_MGMT_3}), + #sm_enabled{id = ID, resume = true} = recv(Config), + ct:comment("Initial request; 'h' should be 0"), + send(Config, #sm_r{xmlns = ?NS_STREAM_MGMT_3}), + #sm_a{h = 0} = recv(Config), + ct:comment("Sending two messages and requesting again; 'h' should be 3"), + send(Config, Msg), + send(Config, Msg), + send(Config, Msg), + send(Config, #sm_r{xmlns = ?NS_STREAM_MGMT_3}), + #sm_a{h = 3} = recv(Config), + ct:comment("Closing socket"), + close_socket(Config), + {save_config, set_opt(sm_previd, ID, Config)}. + +resume(Config) -> + {_, SMConfig} = ?config(saved_config, Config), + ID = ?config(sm_previd, SMConfig), + Server = ?config(server, Config), + ServerJID = jid:make(<<"">>, Server, <<"">>), + MyJID = my_jid(Config), + Txt = #text{data = <<"body">>}, + Msg = #message{from = ServerJID, to = MyJID, body = [Txt]}, + ct:comment("Route message. The message should be queued by the C2S process"), + ejabberd_router:route(Msg), + ct:comment("Resuming the session"), + send(Config, #sm_resume{previd = ID, h = 0, xmlns = ?NS_STREAM_MGMT_3}), + #sm_resumed{previd = ID, h = 3} = recv(Config), + ct:comment("Receiving unacknowledged stanza"), + #message{from = ServerJID, to = MyJID, body = [Txt]} = recv_message(Config), + #sm_r{} = recv(Config), + send(Config, #sm_a{h = 1, xmlns = ?NS_STREAM_MGMT_3}), + ct:comment("Checking if the server counts stanzas correctly"), + send(Config, #sm_r{xmlns = ?NS_STREAM_MGMT_3}), + #sm_a{h = 3} = recv(Config), + ct:comment("Send another stanza to increment the server's 'h' for sm_resume_failed"), + send(Config, #presence{to = ServerJID}), + ct:comment("Closing socket"), + close_socket(Config), + {save_config, set_opt(sm_previd, ID, Config)}. + +resume_failed(Config) -> + {_, SMConfig} = ?config(saved_config, Config), + ID = ?config(sm_previd, SMConfig), + ct:comment("Waiting for the session to time out"), + ct:sleep(5000), + ct:comment("Trying to resume timed out session"), + send(Config, #sm_resume{previd = ID, h = 1, xmlns = ?NS_STREAM_MGMT_3}), + #sm_failed{reason = 'item-not-found', h = 4} = recv(Config), + disconnect(Config). + +%%%=================================================================== +%%% Master-slave tests +%%%=================================================================== +master_slave_cases() -> + {sm_master_slave, [sequence], + [master_slave_test(queue_limit), + master_slave_test(queue_limit_detached)]}. + +queue_limit_master(Config) -> + ct:comment("Waiting for 'send' command from the peer"), + send = get_event(Config), + send_recv_messages(Config), + ct:comment("Waiting for peer to disconnect"), + peer_down = get_event(Config), + disconnect(Config). + +queue_limit_slave(Config) -> + ct:comment("Enable the session management without resumption"), + send(Config, #sm_enable{xmlns = ?NS_STREAM_MGMT_3}), + #sm_enabled{resume = false} = recv(Config), + put_event(Config, send), + ct:comment("Receiving all messages"), + lists:foreach( + fun(I) -> + ID = integer_to_binary(I), + Body = xmpp:mk_text(ID), + #message{id = ID, body = Body} = recv_message(Config) + end, lists:seq(1, 11)), + ct:comment("Receiving request ACK"), + #sm_r{} = recv(Config), + ct:comment("Receiving policy-violation stream error"), + #stream_error{reason = 'policy-violation'} = recv(Config), + {xmlstreamend, <<"stream:stream">>} = recv(Config), + ct:comment("Closing socket"), + close_socket(Config). + +queue_limit_detached_master(Config) -> + ct:comment("Waiting for the peer to disconnect"), + peer_down = get_event(Config), + send_recv_messages(Config), + disconnect(Config). + +queue_limit_detached_slave(Config) -> + #presence{} = send_recv(Config, #presence{}), + ct:comment("Enable the session management with resumption enabled"), + send(Config, #sm_enable{resume = true, xmlns = ?NS_STREAM_MGMT_3}), + #sm_enabled{resume = true} = recv(Config), + ct:comment("Closing socket"), + close_socket(Config). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +single_test(T) -> + list_to_atom("sm_" ++ atom_to_list(T)). + +master_slave_test(T) -> + {list_to_atom("sm_" ++ atom_to_list(T)), [parallel], + [list_to_atom("sm_" ++ atom_to_list(T) ++ "_master"), + list_to_atom("sm_" ++ atom_to_list(T) ++ "_slave")]}. + +send_recv_messages(Config) -> + PeerJID = ?config(peer, Config), + Msg = #message{to = PeerJID}, + ct:comment("Sending messages to peer"), + lists:foreach( + fun(I) -> + ID = integer_to_binary(I), + send(Config, Msg#message{id = ID, body = xmpp:mk_text(ID)}) + end, lists:seq(1, 11)), + ct:comment("Receiving bounced messages from the peer"), + lists:foreach( + fun(I) -> + ID = integer_to_binary(I), + Err = #message{id = ID, type = error} = recv_message(Config), + #stanza_error{reason = 'service-unavailable'} = xmpp:get_error(Err) + end, lists:seq(1, 11)). diff --git a/test/stundisco_tests.erl b/test/stundisco_tests.erl new file mode 100644 index 000000000..ca941983f --- /dev/null +++ b/test/stundisco_tests.erl @@ -0,0 +1,190 @@ +%%%------------------------------------------------------------------- +%%% Author : Holger Weiss +%%% Created : 22 Apr 2020 by Holger Weiss +%%% +%%% +%%% ejabberd, Copyright (C) 2020-2025 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(stundisco_tests). + +%% API +-compile(export_all). +-import(suite, [send_recv/2, disconnect/1, is_feature_advertised/2, + server_jid/1]). + +-include("suite.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +%%%=================================================================== +%%% Single user tests +%%%=================================================================== +single_cases() -> + {stundisco_single, [sequence], + [single_test(feature_enabled), + single_test(stun_service), + single_test(turn_service), + single_test(turns_service), + single_test(turn_credentials), + single_test(turns_credentials)]}. + +feature_enabled(Config) -> + true = is_feature_advertised(Config, ?NS_EXTDISCO_2), + disconnect(Config). + +stun_service(Config) -> + ServerJID = server_jid(Config), + Host = {203, 0, 113, 3}, + Port = ct:get_config(stun_port, 3478), + Type = stun, + Transport = udp, + Request = #services{type = Type}, + #iq{type = result, + sub_els = [#services{ + type = undefined, + list = [#service{host = Host, + port = Port, + type = Type, + transport = Transport, + restricted = false, + username = <<>>, + password = <<>>, + expires = undefined, + action = undefined, + xdata = undefined}]}]} = + send_recv(Config, #iq{type = get, to = ServerJID, sub_els = [Request]}), + disconnect(Config). + +turn_service(Config) -> + ServerJID = server_jid(Config), + Host = {203, 0, 113, 3}, + Port = ct:get_config(stun_port, 3478), + Type = turn, + Transport = udp, + Request = #services{type = Type}, + #iq{type = result, + sub_els = [#services{ + type = undefined, + list = [#service{host = Host, + port = Port, + type = Type, + transport = Transport, + restricted = true, + username = Username, + password = Password, + expires = Expires, + action = undefined, + xdata = undefined}]}]} = + send_recv(Config, #iq{type = get, to = ServerJID, sub_els = [Request]}), + true = check_password(Username, Password), + true = check_expires(Expires), + disconnect(Config). + +turns_service(Config) -> + ServerJID = server_jid(Config), + Host = <<"example.com">>, + Port = 5349, + Type = turns, + Transport = tcp, + Request = #services{type = Type}, + #iq{type = result, + sub_els = [#services{ + type = undefined, + list = [#service{host = Host, + port = Port, + type = Type, + transport = Transport, + restricted = true, + username = Username, + password = Password, + expires = Expires, + action = undefined, + xdata = undefined}]}]} = + send_recv(Config, #iq{type = get, to = ServerJID, sub_els = [Request]}), + true = check_password(Username, Password), + true = check_expires(Expires), + disconnect(Config). + +turn_credentials(Config) -> + ServerJID = server_jid(Config), + Host = {203, 0, 113, 3}, + Port = ct:get_config(stun_port, 3478), + Type = turn, + Transport = udp, + Request = #credentials{services = [#service{host = Host, + port = Port, + type = Type}]}, + #iq{type = result, + sub_els = [#credentials{ + services = [#service{host = Host, + port = Port, + type = Type, + transport = Transport, + restricted = true, + username = Username, + password = Password, + expires = Expires, + action = undefined, + xdata = undefined}]}]} = + send_recv(Config, #iq{type = get, to = ServerJID, sub_els = [Request]}), + true = check_password(Username, Password), + true = check_expires(Expires), + disconnect(Config). + +turns_credentials(Config) -> + ServerJID = server_jid(Config), + Host = <<"example.com">>, + Port = 5349, + Type = turns, + Transport = tcp, + Request = #credentials{services = [#service{host = Host, + port = Port, + type = Type}]}, + #iq{type = result, + sub_els = [#credentials{ + services = [#service{host = Host, + port = Port, + type = Type, + transport = Transport, + restricted = true, + username = Username, + password = Password, + expires = Expires, + action = undefined, + xdata = undefined}]}]} = + send_recv(Config, #iq{type = get, to = ServerJID, sub_els = [Request]}), + true = check_password(Username, Password), + true = check_expires(Expires), + disconnect(Config). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +single_test(T) -> + list_to_atom("stundisco_" ++ atom_to_list(T)). + +check_password(Username, Password) -> + Secret = <<"cryptic">>, + Password == base64:encode(misc:crypto_hmac(sha, Secret, Username)). + +check_expires({_, _, _} = Expires) -> + Now = {MegaSecs, Secs, MicroSecs} = erlang:timestamp(), + Later = {MegaSecs + 1, Secs, MicroSecs}, + (Expires > Now) and (Expires < Later). diff --git a/test/suite.erl b/test/suite.erl index a50bb64df..5fbd70463 100644 --- a/test/suite.erl +++ b/test/suite.erl @@ -1,17 +1,34 @@ %%%------------------------------------------------------------------- -%%% @author Evgeniy Khramtsov <> -%%% @copyright (C) 2013, Evgeniy Khramtsov -%%% @doc +%%% Author : Evgeny Khramtsov +%%% Created : 27 Jun 2013 by Evgeniy Khramtsov %%% -%%% @end -%%% Created : 27 Jun 2013 by Evgeniy Khramtsov <> -%%%------------------------------------------------------------------- +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(suite). %% API -compile(export_all). -include("suite.hrl"). +-include_lib("kernel/include/file.hrl"). +-include("mod_roster.hrl"). %%%=================================================================== %%% API @@ -21,149 +38,511 @@ init_config(Config) -> PrivDir = proplists:get_value(priv_dir, Config), [_, _|Tail] = lists:reverse(filename:split(DataDir)), BaseDir = filename:join(lists:reverse(Tail)), + MacrosPathTpl = filename:join([DataDir, "macros.yml"]), ConfigPath = filename:join([DataDir, "ejabberd.yml"]), LogPath = filename:join([PrivDir, "ejabberd.log"]), SASLPath = filename:join([PrivDir, "sasl.log"]), MnesiaDir = filename:join([PrivDir, "mnesia"]), CertFile = filename:join([DataDir, "cert.pem"]), + SelfSignedCertFile = filename:join([DataDir, "self-signed-cert.pem"]), + CAFile = filename:join([DataDir, "ca.pem"]), {ok, CWD} = file:get_cwd(), {ok, _} = file:copy(CertFile, filename:join([CWD, "cert.pem"])), - ok = application:load(sasl), - ok = application:load(mnesia), - ok = application:load(ejabberd), + {ok, _} = file:copy(SelfSignedCertFile, + filename:join([CWD, "self-signed-cert.pem"])), + {ok, _} = file:copy(CAFile, filename:join([CWD, "ca.pem"])), + copy_file(Config, "spam_jids.txt"), + copy_file(Config, "spam_urls.txt"), + copy_file(Config, "spam_domains.txt"), + copy_file(Config, "whitelist_domains.txt"), + file:write_file(filename:join([CWD, "spam.log"]), []), + {ok, MacrosContentTpl} = file:read_file(MacrosPathTpl), + Password = <<"password!@#$%^&*()'\"`~<>+-/;:_=[]{}|\\">>, + Backends = get_config_backends(), + MacrosContent = process_config_tpl( + MacrosContentTpl, + [{c2s_port, 5222}, + {loglevel, 4}, + {new_schema, false}, + {update_sql_schema, true}, + {s2s_port, 5269}, + {stun_port, 3478}, + {component_port, 5270}, + {web_port, 5280}, + {proxy_port, 7777}, + {password, Password}, + {mysql_server, <<"localhost">>}, + {mysql_port, 3306}, + {mysql_db, <<"ejabberd_test">>}, + {mysql_user, <<"ejabberd_test">>}, + {mysql_pass, <<"ejabberd_test">>}, + {mssql_server, <<"localhost">>}, + {mssql_port, 1433}, + {mssql_db, <<"ejabberd_test">>}, + {mssql_user, <<"ejabberd_test">>}, + {mssql_pass, <<"ejabberd_Test1">>}, + {pgsql_server, <<"localhost">>}, + {pgsql_port, 5432}, + {pgsql_db, <<"ejabberd_test">>}, + {pgsql_user, <<"ejabberd_test">>}, + {pgsql_pass, <<"ejabberd_test">>}, + {priv_dir, PrivDir}]), + MacrosPath = filename:join([CWD, "macros.yml"]), + ok = file:write_file(MacrosPath, MacrosContent), + copy_configtest_yml(DataDir, CWD), + copy_backend_configs(DataDir, CWD, Backends), + setup_ejabberd_lib_path(Config), + case application:load(sasl) of + ok -> ok; + {error, {already_loaded, _}} -> ok + end, + case application:load(mnesia) of + ok -> ok; + {error, {already_loaded, _}} -> ok + end, + case application:load(ejabberd) of + ok -> ok; + {error, {already_loaded, _}} -> ok + end, application:set_env(ejabberd, config, ConfigPath), application:set_env(ejabberd, log_path, LogPath), application:set_env(sasl, sasl_error_logger, {file, SASLPath}), application:set_env(mnesia, dir, MnesiaDir), - [{server_port, 5222}, + [{server_port, ct:get_config(c2s_port, 5222)}, {server_host, "localhost"}, + {component_port, ct:get_config(component_port, 5270)}, + {s2s_port, ct:get_config(s2s_port, 5269)}, {server, ?COMMON_VHOST}, - {user, <<"test_single">>}, - {master_nick, <<"master_nick">>}, - {slave_nick, <<"slave_nick">>}, - {room_subject, <<"hello, world!">>}, + {user, <<"test_single!#$%^*()`~+-;_=[]{}|\\">>}, + {nick, <<"nick!@#$%^&*()'\"`~<>+-/;:_=[]{}|\\">>}, + {master_nick, <<"master_nick!@#$%^&*()'\"`~<>+-/;:_=[]{}|\\">>}, + {slave_nick, <<"slave_nick!@#$%^&*()'\"`~<>+-/;:_=[]{}|\\">>}, + {room_subject, <<"hello, world!@#$%^&*()'\"`~<>+-/;:_=[]{}|\\">>}, {certfile, CertFile}, + {persistent_room, true}, + {anonymous, false}, + {type, client}, + {xmlns, ?NS_CLIENT}, + {ns_stream, ?NS_STREAM}, + {stream_version, {1, 0}}, + {stream_id, <<"">>}, + {stream_from, <<"">>}, + {db_xmlns, <<"">>}, + {mechs, []}, + {rosterver, false}, + {lang, <<"en">>}, {base_dir, BaseDir}, - {resource, <<"resource">>}, - {master_resource, <<"master_resource">>}, - {slave_resource, <<"slave_resource">>}, - {password, <<"password">>} + {receiver, undefined}, + {pubsub_node, <<"node!@#$%^&*()'\"`~<>+-/;:_=[]{}|\\">>}, + {pubsub_node_title, <<"title!@#$%^&*()'\"`~<>+-/;:_=[]{}|\\">>}, + {resource, <<"resource!@#$%^&*()'\"`~<>+-/;:_=[]{}|\\">>}, + {master_resource, <<"master_resource!@#$%^&*()'\"`~<>+-/;:_=[]{}|\\">>}, + {slave_resource, <<"slave_resource!@#$%^&*()'\"`~<>+-/;:_=[]{}|\\">>}, + {update_sql, false}, + {password, Password}, + {backends, Backends} |Config]. +copy_file(Config, File) -> + {ok, CWD} = file:get_cwd(), + DataDir = proplists:get_value(data_dir, Config), + {ok, _} = file:copy(filename:join([DataDir, File]), filename:join([CWD, File])). + +copy_configtest_yml(DataDir, CWD) -> + Files = filelib:wildcard(filename:join([DataDir, "configtest.yml"])), + lists:foreach( + fun(Src) -> + ct:pal("copying ~p", [Src]), + File = filename:basename(Src), + case string:tokens(File, ".") of + ["configtest", "yml"] -> + Dst = filename:join([CWD, File]), + case true of + true -> + {ok, _} = file:copy(Src, Dst); + false -> + ok + end; + _ -> + ok + end + end, Files). + + +copy_backend_configs(DataDir, CWD, Backends) -> + Files = filelib:wildcard(filename:join([DataDir, "ejabberd.*.yml"])), + lists:foreach( + fun(Src) -> + ct:pal("copying ~p", [Src]), + File = filename:basename(Src), + case string:tokens(File, ".") of + ["ejabberd", SBackend, "yml"] -> + Backend = list_to_atom(SBackend), + Macro = list_to_atom(string:to_upper(SBackend) ++ "_CONFIG"), + Dst = filename:join([CWD, File]), + case lists:member(Backend, Backends) of + true -> + {ok, _} = file:copy(Src, Dst); + false -> + ok = file:write_file( + Dst, fast_yaml:encode( + [{define_macro, [{Macro, []}]}])) + end; + _ -> + ok + end + end, Files). + +find_top_dir(Dir) -> + case file:read_file_info(filename:join([Dir, ebin])) of + {ok, #file_info{type = directory}} -> + Dir; + _ -> + find_top_dir(filename:dirname(Dir)) + end. + +setup_ejabberd_lib_path(Config) -> + case code:lib_dir(ejabberd) of + {error, _} -> + DataDir = proplists:get_value(data_dir, Config), + {ok, CWD} = file:get_cwd(), + NewEjPath = filename:join([CWD, "ejabberd-0.0.1"]), + TopDir = find_top_dir(DataDir), + ok = file:make_symlink(TopDir, NewEjPath), + code:replace_path(ejabberd, NewEjPath); + _ -> + ok + end. + +%% Read environment variable CT_DB=mysql to limit the backends to test. +%% You can thus limit the backend you want to test with: +%% CT_BACKENDS=mysql rebar ct suites=ejabberd +get_config_backends() -> + EnvBackends = case os:getenv("CT_BACKENDS") of + false -> ?BACKENDS; + String -> + Backends0 = string:tokens(String, ","), + lists:map( + fun(Backend) -> + list_to_atom(string:strip(Backend, both, $ )) + end, Backends0) + end, + application:load(ejabberd), + EnabledBackends = application:get_env(ejabberd, enabled_backends, EnvBackends), + misc:intersection(EnvBackends, [mnesia, ldap, extauth|EnabledBackends]). + +process_config_tpl(Content, []) -> + Content; +process_config_tpl(Content, [{Name, DefaultValue} | Rest]) -> + Val = case ct:get_config(Name, DefaultValue) of + V when is_integer(V) -> + integer_to_binary(V); + V when is_atom(V) -> + atom_to_binary(V, latin1); + V -> + iolist_to_binary(V) + end, + NewContent = binary:replace(Content, + <<"@@",(atom_to_binary(Name,latin1))/binary, "@@">>, + Val, [global]), + process_config_tpl(NewContent, Rest). + +stream_header(Config) -> + To = case ?config(server, Config) of + <<"">> -> undefined; + Server -> jid:make(Server) + end, + From = case ?config(stream_from, Config) of + <<"">> -> undefined; + Frm -> jid:make(Frm) + end, + #stream_start{to = To, + from = From, + lang = ?config(lang, Config), + version = ?config(stream_version, Config), + xmlns = ?config(xmlns, Config), + db_xmlns = ?config(db_xmlns, Config), + stream_xmlns = ?config(ns_stream, Config)}. + connect(Config) -> - {ok, Sock} = ejabberd_socket:connect( - ?config(server_host, Config), - ?config(server_port, Config), - [binary, {packet, 0}, {active, false}]), - init_stream(set_opt(socket, Sock, Config)). + NewConfig = init_stream(Config), + case ?config(type, NewConfig) of + client -> process_stream_features(NewConfig); + server -> process_stream_features(NewConfig); + component -> NewConfig + end. + +tcp_connect(Config) -> + case ?config(receiver, Config) of + undefined -> + Owner = self(), + NS = case ?config(type, Config) of + client -> ?NS_CLIENT; + server -> ?NS_SERVER; + component -> ?NS_COMPONENT + end, + Server = ?config(server_host, Config), + Port = ?config(server_port, Config), + ReceiverPid = spawn(fun() -> + start_receiver(NS, Owner, Server, Port) + end), + set_opt(receiver, ReceiverPid, Config); + _ -> + Config + end. init_stream(Config) -> - ok = send_text(Config, io_lib:format(?STREAM_HEADER, - [?config(server, Config)])), - {xmlstreamstart, <<"stream:stream">>, Attrs} = recv(), - <<"jabber:client">> = xml:get_attr_s(<<"xmlns">>, Attrs), - <<"1.0">> = xml:get_attr_s(<<"version">>, Attrs), - #stream_features{sub_els = Fs} = recv(), - Mechs = lists:flatmap( - fun(#sasl_mechanisms{list = Ms}) -> - Ms; - (_) -> - [] - end, Fs), - lists:foldl( - fun(#feature_register{}, Acc) -> - set_opt(register, true, Acc); - (#starttls{}, Acc) -> - set_opt(starttls, true, Acc); - (#compression{methods = Ms}, Acc) -> - set_opt(compression, Ms, Acc); - (_, Acc) -> - Acc - end, set_opt(mechs, Mechs, Config), Fs). + Version = ?config(stream_version, Config), + NewConfig = tcp_connect(Config), + send(NewConfig, stream_header(NewConfig)), + XMLNS = case ?config(type, Config) of + client -> ?NS_CLIENT; + component -> ?NS_COMPONENT; + server -> ?NS_SERVER + end, + receive + #stream_start{id = ID, xmlns = XMLNS, version = Version} -> + set_opt(stream_id, ID, NewConfig) + end. + +process_stream_features(Config) -> + receive + #stream_features{sub_els = Fs} -> + Mechs = lists:flatmap( + fun(#sasl_mechanisms{list = Ms}) -> + Ms; + (_) -> + [] + end, Fs), + lists:foldl( + fun(#feature_register{}, Acc) -> + set_opt(register, true, Acc); + (#starttls{}, Acc) -> + set_opt(starttls, true, Acc); + (#legacy_auth_feature{}, Acc) -> + set_opt(legacy_auth, true, Acc); + (#compression{methods = Ms}, Acc) -> + set_opt(compression, Ms, Acc); + (_, Acc) -> + Acc + end, set_opt(mechs, Mechs, Config), Fs) + end. disconnect(Config) -> - Socket = ?config(socket, Config), - ok = ejabberd_socket:send(Socket, ?STREAM_TRAILER), - {xmlstreamend, <<"stream:stream">>} = recv(), - ejabberd_socket:close(Socket), - Config. + ct:comment("Disconnecting"), + try + send_text(Config, ?STREAM_TRAILER) + catch exit:normal -> + ok + end, + receive {xmlstreamend, <<"stream:stream">>} -> ok end, + flush(Config), + ok = recv_call(Config, close), + ct:comment("Disconnected"), + set_opt(receiver, undefined, Config). close_socket(Config) -> - Socket = ?config(socket, Config), - ejabberd_socket:close(Socket), + ok = recv_call(Config, close), Config. starttls(Config) -> + starttls(Config, false). + +starttls(Config, ShouldFail) -> send(Config, #starttls{}), - #starttls_proceed{} = recv(), - TLSSocket = ejabberd_socket:starttls( - ?config(socket, Config), - [{certfile, ?config(certfile, Config)}, - connect]), - init_stream(set_opt(socket, TLSSocket, Config)). + receive + #starttls_proceed{} when ShouldFail -> + ct:fail(starttls_should_have_failed); + #starttls_failure{} when ShouldFail -> + Config; + #starttls_failure{} -> + ct:fail(starttls_failed); + #starttls_proceed{} -> + ok = recv_call(Config, {starttls, ?config(certfile, Config)}), + Config + end. zlib(Config) -> send(Config, #compress{methods = [<<"zlib">>]}), - #compressed{} = recv(), - ZlibSocket = ejabberd_socket:compress(?config(socket, Config)), - init_stream(set_opt(socket, ZlibSocket, Config)). + receive #compressed{} -> ok end, + ok = recv_call(Config, compress), + process_stream_features(init_stream(Config)). auth(Config) -> + auth(Config, false). + +auth(Config, ShouldFail) -> + Type = ?config(type, Config), + IsAnonymous = ?config(anonymous, Config), Mechs = ?config(mechs, Config), HaveMD5 = lists:member(<<"DIGEST-MD5">>, Mechs), HavePLAIN = lists:member(<<"PLAIN">>, Mechs), - if HavePLAIN -> - auth_SASL(<<"PLAIN">>, Config); + HaveExternal = lists:member(<<"EXTERNAL">>, Mechs), + HaveAnonymous = lists:member(<<"ANONYMOUS">>, Mechs), + if HaveAnonymous and IsAnonymous -> + auth_SASL(<<"ANONYMOUS">>, Config, ShouldFail); + HavePLAIN -> + auth_SASL(<<"PLAIN">>, Config, ShouldFail); HaveMD5 -> - auth_SASL(<<"DIGEST-MD5">>, Config); + auth_SASL(<<"DIGEST-MD5">>, Config, ShouldFail); + HaveExternal -> + auth_SASL(<<"EXTERNAL">>, Config, ShouldFail); + Type == client -> + auth_legacy(Config, false, ShouldFail); + Type == component -> + auth_component(Config, ShouldFail); true -> - ct:fail(no_sasl_mechanisms_available) + ct:fail(no_known_sasl_mechanism_available) end. bind(Config) -> - #iq{type = result, sub_els = [#bind{}]} = - send_recv( - Config, - #iq{type = set, - sub_els = [#bind{resource = ?config(resource, Config)}]}), - Config. + U = ?config(user, Config), + S = ?config(server, Config), + R = ?config(resource, Config), + case ?config(type, Config) of + client -> + #iq{type = result, sub_els = [#bind{jid = JID}]} = + send_recv( + Config, #iq{type = set, sub_els = [#bind{resource = R}]}), + case ?config(anonymous, Config) of + false -> + {U, S, R} = jid:tolower(JID), + Config; + true -> + {User, S, Resource} = jid:tolower(JID), + set_opt(user, User, set_opt(resource, Resource, Config)) + end; + component -> + Config + end. open_session(Config) -> - #iq{type = result, sub_els = []} = - send_recv(Config, #iq{type = set, sub_els = [#session{}]}), + open_session(Config, false). + +open_session(Config, Force) -> + if Force -> + #iq{type = result, sub_els = []} = + send_recv(Config, #iq{type = set, sub_els = [#xmpp_session{}]}); + true -> + ok + end, Config. -auth_SASL(Mech, Config) -> - {Response, SASL} = sasl_new(Mech, - ?config(user, Config), - ?config(server, Config), - ?config(password, Config)), - send(Config, #sasl_auth{mechanism = Mech, text = Response}), - wait_auth_SASL_result(set_opt(sasl, SASL, Config)). +auth_legacy(Config, IsDigest) -> + auth_legacy(Config, IsDigest, false). -wait_auth_SASL_result(Config) -> - case recv() of +auth_legacy(Config, IsDigest, ShouldFail) -> + ServerJID = server_jid(Config), + U = ?config(user, Config), + R = ?config(resource, Config), + P = ?config(password, Config), + #iq{type = result, + from = ServerJID, + sub_els = [#legacy_auth{username = <<"">>, + password = <<"">>, + resource = <<"">>} = Auth]} = + send_recv(Config, + #iq{to = ServerJID, type = get, + sub_els = [#legacy_auth{}]}), + Res = case Auth#legacy_auth.digest of + <<"">> when IsDigest -> + StreamID = ?config(stream_id, Config), + D = p1_sha:sha(<>), + send_recv(Config, #iq{to = ServerJID, type = set, + sub_els = [#legacy_auth{username = U, + resource = R, + digest = D}]}); + _ when not IsDigest -> + send_recv(Config, #iq{to = ServerJID, type = set, + sub_els = [#legacy_auth{username = U, + resource = R, + password = P}]}) + end, + case Res of + #iq{from = ServerJID, type = result, sub_els = []} -> + if ShouldFail -> + ct:fail(legacy_auth_should_have_failed); + true -> + Config + end; + #iq{from = ServerJID, type = error} -> + if ShouldFail -> + Config; + true -> + ct:fail(legacy_auth_failed) + end + end. + +auth_component(Config, ShouldFail) -> + StreamID = ?config(stream_id, Config), + Password = ?config(password, Config), + Digest = p1_sha:sha(<>), + send(Config, #handshake{data = Digest}), + receive + #handshake{} when ShouldFail -> + ct:fail(component_auth_should_have_failed); + #handshake{} -> + Config; + #stream_error{reason = 'not-authorized'} when ShouldFail -> + Config; + #stream_error{reason = 'not-authorized'} -> + ct:fail(component_auth_failed) + end. + +auth_SASL(Mech, Config) -> + auth_SASL(Mech, Config, false). + +auth_SASL(Mech, Config, ShouldFail) -> + Creds = {?config(user, Config), + ?config(server, Config), + ?config(password, Config)}, + auth_SASL(Mech, Config, ShouldFail, Creds). + +auth_SASL(Mech, Config, ShouldFail, Creds) -> + {Response, SASL} = sasl_new(Mech, Creds), + send(Config, #sasl_auth{mechanism = Mech, text = Response}), + wait_auth_SASL_result(set_opt(sasl, SASL, Config), ShouldFail). + +wait_auth_SASL_result(Config, ShouldFail) -> + receive + #sasl_success{} when ShouldFail -> + ct:fail(sasl_auth_should_have_failed); #sasl_success{} -> - ejabberd_socket:reset_stream(?config(socket, Config)), - send_text(Config, - io_lib:format(?STREAM_HEADER, - [?config(server, Config)])), - {xmlstreamstart, <<"stream:stream">>, Attrs} = recv(), - <<"jabber:client">> = xml:get_attr_s(<<"xmlns">>, Attrs), - <<"1.0">> = xml:get_attr_s(<<"version">>, Attrs), - #stream_features{sub_els = Fs} = recv(), - lists:foldl( - fun(#feature_sm{}, ConfigAcc) -> - set_opt(sm, true, ConfigAcc); - (#feature_csi{}, ConfigAcc) -> - set_opt(csi, true, ConfigAcc); - (_, ConfigAcc) -> - ConfigAcc - end, Config, Fs); + ok = recv_call(Config, reset_stream), + send(Config, stream_header(Config)), + Type = ?config(type, Config), + NS = if Type == client -> ?NS_CLIENT; + Type == server -> ?NS_SERVER + end, + Config2 = receive #stream_start{id = ID, xmlns = NS, version = {1,0}} -> + set_opt(stream_id, ID, Config) + end, + receive #stream_features{sub_els = Fs} -> + if Type == client -> + #xmpp_session{optional = true} = + lists:keyfind(xmpp_session, 1, Fs); + true -> + ok + end, + lists:foldl( + fun(#feature_sm{}, ConfigAcc) -> + set_opt(sm, true, ConfigAcc); + (#feature_csi{}, ConfigAcc) -> + set_opt(csi, true, ConfigAcc); + (#rosterver_feature{}, ConfigAcc) -> + set_opt(rosterver, true, ConfigAcc); + (#compression{methods = Ms}, ConfigAcc) -> + set_opt(compression, Ms, ConfigAcc); + (_, ConfigAcc) -> + ConfigAcc + end, Config2, Fs) + end; #sasl_challenge{text = ClientIn} -> {Response, SASL} = (?config(sasl, Config))(ClientIn), send(Config, #sasl_response{text = Response}), - wait_auth_SASL_result(set_opt(sasl, SASL, Config)); + wait_auth_SASL_result(set_opt(sasl, SASL, Config), ShouldFail); + #sasl_failure{} when ShouldFail -> + Config; #sasl_failure{} -> ct:fail(sasl_auth_failed) end. @@ -172,34 +551,55 @@ re_register(Config) -> User = ?config(user, Config), Server = ?config(server, Config), Pass = ?config(password, Config), - {atomic, ok} = ejabberd_auth:try_register(User, Server, Pass), - ok. + ok = ejabberd_auth:try_register(User, Server, Pass). -recv() -> +match_failure(Received, [Match]) when is_list(Match)-> + ct:fail("Received input:~n~n~p~n~ndon't match expected patterns:~n~n~s", [Received, Match]); +match_failure(Received, Matches) -> + ct:fail("Received input:~n~n~p~n~ndon't match expected patterns:~n~n~p", [Received, Matches]). + +recv(_Config) -> receive - {'$gen_event', {xmlstreamelement, El}} -> - Pkt = xmpp_codec:decode(fix_ns(El)), - ct:pal("recv: ~p ->~n~s", [El, xmpp_codec:pp(Pkt)]), - Pkt; - {'$gen_event', Event} -> - Event + {fail, El, Why} -> + ct:fail("recv failed: ~p->~n~s", + [El, xmpp:format_error(Why)]); + Event -> + Event end. -fix_ns(#xmlel{name = Tag, attrs = Attrs} = El) - when Tag == <<"stream:features">>; Tag == <<"stream:error">> -> - NewAttrs = [{<<"xmlns">>, <<"http://etherx.jabber.org/streams">>} - |lists:keydelete(<<"xmlns">>, 1, Attrs)], - El#xmlel{attrs = NewAttrs}; -fix_ns(#xmlel{name = Tag, attrs = Attrs} = El) - when Tag == <<"message">>; Tag == <<"iq">>; Tag == <<"presence">> -> - NewAttrs = [{<<"xmlns">>, <<"jabber:client">>} - |lists:keydelete(<<"xmlns">>, 1, Attrs)], - El#xmlel{attrs = NewAttrs}; -fix_ns(El) -> - El. +recv_iq(_Config) -> + receive #iq{} = IQ -> IQ end. + +recv_presence(_Config) -> + receive #presence{} = Pres -> Pres end. + +recv_message(_Config) -> + receive #message{} = Msg -> Msg end. + +decode_stream_element(NS, El) -> + decode(El, NS, []). + +format_element(El) -> + Bin = case erlang:function_exported(ct, log, 5) of + true -> ejabberd_web_admin:pretty_print_xml(El); + false -> io_lib:format("~p~n", [El]) + end, + binary:replace(Bin, <<"<">>, <<"<">>, [global]). + +decode(El, NS, Opts) -> + try + Pkt = xmpp:decode(El, NS, Opts), + ct:pal("RECV:~n~s~n~s", + [format_element(El), xmpp:pp(Pkt)]), + Pkt + catch _:{xmpp_codec, Why} -> + ct:pal("recv failed: ~p->~n~s", + [El, xmpp:format_error(Why)]), + erlang:error({xmpp_codec, Why}) + end. send_text(Config, Text) -> - ejabberd_socket:send(?config(socket, Config), Text). + recv_call(Config, {send_text, Text}). send(State, Pkt) -> {NewID, NewPkt} = case Pkt of @@ -215,25 +615,42 @@ send(State, Pkt) -> _ -> {undefined, Pkt} end, - El = xmpp_codec:encode(NewPkt), - ct:pal("sent: ~p <-~n~s", [El, xmpp_codec:pp(NewPkt)]), - ok = send_text(State, xml:element_to_binary(El)), + El = xmpp:encode(NewPkt), + ct:pal("SENT:~n~s~n~s", + [format_element(El), xmpp:pp(NewPkt)]), + Data = case NewPkt of + #stream_start{} -> fxml:element_to_header(El); + _ -> fxml:element_to_binary(El) + end, + ok = send_text(State, Data), NewID. -send_recv(State, IQ) -> +send_recv(State, #message{} = Msg) -> + ID = send(State, Msg), + receive #message{id = ID} = Result -> Result end; +send_recv(State, #presence{} = Pres) -> + ID = send(State, Pres), + receive #presence{id = ID} = Result -> Result end; +send_recv(State, #iq{} = IQ) -> ID = send(State, IQ), - #iq{id = ID} = recv(). + receive #iq{id = ID} = Result -> Result end. -sasl_new(<<"PLAIN">>, User, Server, Password) -> +sasl_new(<<"PLAIN">>, {User, Server, Password}) -> {<>, fun (_) -> {error, <<"Invalid SASL challenge">>} end}; -sasl_new(<<"DIGEST-MD5">>, User, Server, Password) -> +sasl_new(<<"EXTERNAL">>, {User, Server, _Password}) -> + {jid:encode(jid:make(User, Server)), + fun(_) -> ct:fail(sasl_challenge_is_not_expected) end}; +sasl_new(<<"ANONYMOUS">>, _) -> + {<<"">>, + fun(_) -> ct:fail(sasl_challenge_is_not_expected) end}; +sasl_new(<<"DIGEST-MD5">>, {User, Server, Password}) -> {<<"">>, fun (ServerIn) -> - case cyrsasl_digest:parse(ServerIn) of + case xmpp_sasl_digest:parse(ServerIn) of bad -> {error, <<"Invalid SASL challenge">>}; KeyVals -> - Nonce = xml:get_attr_s(<<"nonce">>, KeyVals), + Nonce = fxml:get_attr_s(<<"nonce">>, KeyVals), CNonce = id(), Realm = proplists:get_value(<<"realm">>, KeyVals, Server), DigestURI = <<"xmpp/", Realm/binary>>, @@ -243,7 +660,12 @@ sasl_new(<<"DIGEST-MD5">>, User, Server, Password) -> MyResponse = response(User, Password, Nonce, AuthzId, Realm, CNonce, DigestURI, NC, QOP, <<"AUTHENTICATE">>), - Resp = <<"username=\"", User/binary, "\",realm=\"", + SUser = << <<(case Char of + $" -> <<"\\\"">>; + $\\ -> <<"\\\\">>; + _ -> <> + end)/binary>> || <> <= User >>, + Resp = <<"username=\"", SUser/binary, "\",realm=\"", Realm/binary, "\",nonce=\"", Nonce/binary, "\",cnonce=\"", CNonce/binary, "\",nc=", NC/binary, ",qop=", QOP/binary, ",digest-uri=\"", @@ -251,7 +673,7 @@ sasl_new(<<"DIGEST-MD5">>, User, Server, Password) -> MyResponse/binary, "\"">>, {Resp, fun (ServerIn2) -> - case cyrsasl_digest:parse(ServerIn2) of + case xmpp_sasl_digest:parse(ServerIn2) of bad -> {error, <<"Invalid SASL challenge">>}; _KeyVals2 -> {<<"">>, @@ -293,34 +715,60 @@ response(User, Passwd, Nonce, AuthzId, Realm, CNonce, hex((erlang:md5(T))). my_jid(Config) -> - jlib:make_jid(?config(user, Config), - ?config(server, Config), - ?config(resource, Config)). + jid:make(?config(user, Config), + ?config(server, Config), + ?config(resource, Config)). server_jid(Config) -> - jlib:make_jid(<<>>, ?config(server, Config), <<>>). + jid:make(<<>>, ?config(server, Config), <<>>). pubsub_jid(Config) -> Server = ?config(server, Config), - jlib:make_jid(<<>>, <<"pubsub.", Server/binary>>, <<>>). + jid:make(<<>>, <<"pubsub.", Server/binary>>, <<>>). proxy_jid(Config) -> Server = ?config(server, Config), - jlib:make_jid(<<>>, <<"proxy.", Server/binary>>, <<>>). + jid:make(<<>>, <<"proxy.", Server/binary>>, <<>>). + +upload_jid(Config) -> + Server = ?config(server, Config), + jid:make(<<>>, <<"upload.", Server/binary>>, <<>>). muc_jid(Config) -> Server = ?config(server, Config), - jlib:make_jid(<<>>, <<"conference.", Server/binary>>, <<>>). + jid:make(<<>>, <<"conference.", Server/binary>>, <<>>). muc_room_jid(Config) -> Server = ?config(server, Config), - jlib:make_jid(<<"test">>, <<"conference.", Server/binary>>, <<>>). + jid:make(<<"test">>, <<"conference.", Server/binary>>, <<>>). + +my_muc_jid(Config) -> + Nick = ?config(nick, Config), + RoomJID = muc_room_jid(Config), + jid:replace_resource(RoomJID, Nick). + +peer_muc_jid(Config) -> + PeerNick = ?config(peer_nick, Config), + RoomJID = muc_room_jid(Config), + jid:replace_resource(RoomJID, PeerNick). + +alt_room_jid(Config) -> + Server = ?config(server, Config), + jid:make(<<"alt">>, <<"conference.", Server/binary>>, <<>>). + +mix_jid(Config) -> + Server = ?config(server, Config), + jid:make(<<>>, <<"mix.", Server/binary>>, <<>>). + +mix_room_jid(Config) -> + Server = ?config(server, Config), + jid:make(<<"test">>, <<"mix.", Server/binary>>, <<>>). id() -> - id(undefined). + id(<<>>). -id(undefined) -> - randoms:get_string(); +id(<<>>) -> + p1_rand:get_string(); id(ID) -> ID. @@ -328,6 +776,7 @@ get_features(Config) -> get_features(Config, server_jid(Config)). get_features(Config, To) -> + ct:comment("Getting features of ~s", [jid:encode(To)]), #iq{type = result, sub_els = [#disco_info{features = Features}]} = send_recv(Config, #iq{type = get, sub_els = [#disco_info{}], to = To}), Features. @@ -343,16 +792,145 @@ set_opt(Opt, Val, Config) -> [{Opt, Val}|lists:keydelete(Opt, 1, Config)]. wait_for_master(Config) -> - put_event(Config, slave_ready), - master_ready = get_event(Config). + put_event(Config, peer_ready), + case get_event(Config) of + peer_ready -> + ok; + Other -> + suite:match_failure(Other, peer_ready) + end. wait_for_slave(Config) -> - put_event(Config, master_ready), - slave_ready = get_event(Config). + put_event(Config, peer_ready), + case get_event(Config) of + peer_ready -> + ok; + Other -> + suite:match_failure(Other, peer_ready) + end. make_iq_result(#iq{from = From} = IQ) -> IQ#iq{type = result, to = From, from = undefined, sub_els = []}. +self_presence(Config, Type) -> + MyJID = my_jid(Config), + ct:comment("Sending self-presence"), + #presence{type = Type, from = MyJID} = + send_recv(Config, #presence{type = Type}). + +set_roster(Config, Subscription, Groups) -> + MyJID = my_jid(Config), + {U, S, _} = jid:tolower(MyJID), + PeerJID = ?config(peer, Config), + PeerBareJID = jid:remove_resource(PeerJID), + PeerLJID = jid:tolower(PeerBareJID), + ct:comment("Adding ~s to roster with subscription '~s' in groups ~p", + [jid:encode(PeerBareJID), Subscription, Groups]), + {atomic, _} = mod_roster:set_roster(#roster{usj = {U, S, PeerLJID}, + us = {U, S}, + jid = PeerLJID, + subscription = Subscription, + groups = Groups}), + Config. + +del_roster(Config) -> + del_roster(Config, ?config(peer, Config)). + +del_roster(Config, PeerJID) -> + MyJID = my_jid(Config), + {U, S, _} = jid:tolower(MyJID), + PeerBareJID = jid:remove_resource(PeerJID), + PeerLJID = jid:tolower(PeerBareJID), + ct:comment("Removing ~s from roster", [jid:encode(PeerBareJID)]), + {atomic, _} = mod_roster:del_roster(U, S, PeerLJID), + Config. + +get_roster(Config) -> + {LUser, LServer, _} = jid:tolower(my_jid(Config)), + mod_roster:get_roster(LUser, LServer). + +recv_call(Config, Msg) -> + Receiver = ?config(receiver, Config), + Ref = make_ref(), + Receiver ! {Ref, Msg}, + receive + {Ref, Reply} -> + Reply + end. + +start_receiver(NS, Owner, Server, Port) -> + MRef = erlang:monitor(process, Owner), + {ok, Socket} = xmpp_socket:connect( + Server, Port, + [binary, {packet, 0}, {active, false}], infinity), + receiver(NS, Owner, Socket, MRef). + +receiver(NS, Owner, Socket, MRef) -> + receive + {Ref, reset_stream} -> + Socket1 = xmpp_socket:reset_stream(Socket), + Owner ! {Ref, ok}, + receiver(NS, Owner, Socket1, MRef); + {Ref, {starttls, Certfile}} -> + {ok, TLSSocket} = xmpp_socket:starttls( + Socket, + [{certfile, Certfile}, connect]), + Owner ! {Ref, ok}, + receiver(NS, Owner, TLSSocket, MRef); + {Ref, compress} -> + {ok, ZlibSocket} = xmpp_socket:compress(Socket), + Owner ! {Ref, ok}, + receiver(NS, Owner, ZlibSocket, MRef); + {Ref, {send_text, Text}} -> + Ret = xmpp_socket:send(Socket, Text), + Owner ! {Ref, Ret}, + receiver(NS, Owner, Socket, MRef); + {Ref, close} -> + xmpp_socket:close(Socket), + Owner ! {Ref, ok}, + receiver(NS, Owner, Socket, MRef); + {'$gen_event', {xmlstreamelement, El}} -> + Owner ! decode_stream_element(NS, El), + receiver(NS, Owner, Socket, MRef); + {'$gen_event', {xmlstreamstart, Name, Attrs}} -> + Owner ! decode(#xmlel{name = Name, attrs = Attrs}, <<>>, []), + receiver(NS, Owner, Socket, MRef); + {'$gen_event', Event} -> + Owner ! Event, + receiver(NS, Owner, Socket, MRef); + {'DOWN', MRef, process, Owner, _} -> + ok; + {tcp, _, Data} -> + case xmpp_socket:recv(Socket, Data) of + {ok, Socket1} -> + receiver(NS, Owner, Socket1, MRef); + {error, _} -> + Owner ! closed, + receiver(NS, Owner, Socket, MRef) + end; + {tcp_error, _, _} -> + Owner ! closed, + receiver(NS, Owner, Socket, MRef); + {tcp_closed, _} -> + Owner ! closed, + receiver(NS, Owner, Socket, MRef) + end. + +%% @doc Retry an action until success, at max N times with an interval +%% `Interval' +%% Shamlessly stolen (with slight adaptations) from snabbkaffee. +-spec retry(integer(), non_neg_integer(), fun(() -> Ret)) -> Ret. +retry(_, 0, Fun) -> + Fun(); +retry(Interval, N, Fun) -> + try Fun() + catch + EC:Err -> + timer:sleep(Interval), + ct:pal("retrying ~p more times, result was ~p:~p", [N, EC, Err]), + retry(Interval, N - 1, Fun) + end. + %%%=================================================================== %%% Clients puts and gets events via this relay. %%%=================================================================== @@ -369,6 +947,7 @@ event_relay() -> event_relay(Events, Subscribers) -> receive {subscribe, From} -> + erlang:monitor(process, From), From ! {ok, self()}, lists:foreach( fun(Event) -> From ! {event, Event, self()} @@ -382,7 +961,19 @@ event_relay(Events, Subscribers) -> (_) -> ok end, Subscribers), - event_relay([Event|Events], Subscribers) + event_relay([Event|Events], Subscribers); + {'DOWN', _MRef, process, Pid, _Info} -> + case lists:member(Pid, Subscribers) of + true -> + NewSubscribers = lists:delete(Pid, Subscribers), + lists:foreach( + fun(Subscriber) -> + Subscriber ! {event, peer_down, self()} + end, NewSubscribers), + event_relay(Events, NewSubscribers); + false -> + event_relay(Events, Subscribers) + end end. subscribe_to_events(Config) -> @@ -407,3 +998,12 @@ get_event(Config) -> {event, Event, Relay} -> Event end. + +flush(Config) -> + receive + {event, peer_down, _} -> flush(Config); + closed -> flush(Config); + Msg -> ct:fail({unexpected_msg, Msg}) + after 0 -> + ok + end. diff --git a/test/suite.hrl b/test/suite.hrl index 49bfff2d9..00b341cb1 100644 --- a/test/suite.hrl +++ b/test/suite.hrl @@ -1,67 +1,109 @@ -include_lib("common_test/include/ct.hrl"). --include_lib("p1_xml/include/xml.hrl"). --include("ns.hrl"). --include("ejabberd.hrl"). +-include_lib("fast_xml/include/fxml.hrl"). +-include_lib("xmpp/include/jid.hrl"). +-include_lib("xmpp/include/ns.hrl"). +-include_lib("xmpp/include/xmpp_codec.hrl"). -include("mod_proxy65.hrl"). --include("xmpp_codec.hrl"). - --define(STREAM_HEADER, - <<"">>). -define(STREAM_TRAILER, <<"">>). -define(PUBSUB(Node), <<(?NS_PUBSUB)/binary, "#", Node>>). --define(EJABBERD_CT_URI, <<"http://www.process-one.net/en/ejabberd_ct/">>). +-define(EJABBERD_CT_URI, <<"https://docs.ejabberd.im/developer/extending-ejabberd/testing/">>). + +-define(recv1(P1), + P1 = (fun() -> + V = suite:recv(Config), + case V of + P1 -> V; + _ -> suite:match_failure([V], [??P1]) + end + end)()). -define(recv2(P1, P2), (fun() -> - case {R1 = recv(), R2 = recv()} of + case {R1 = suite:recv(Config), R2 = suite:recv(Config)} of {P1, P2} -> {R1, R2}; - {P2, P1} -> {R2, R1} + {P2, P1} -> {R2, R1}; + {P1, V1} -> suite:match_failure([V1], [P2]); + {P2, V2} -> suite:match_failure([V2], [P1]); + {V3, P1} -> suite:match_failure([V3], [P2]); + {V4, P2} -> suite:match_failure([V4], [P1]); + {V5, V6} -> suite:match_failure([V5, V6], [P1, P2]) end end)()). -define(recv3(P1, P2, P3), (fun() -> - case R3 = recv() of + case R3 = suite:recv(Config) of P1 -> insert(R3, 1, ?recv2(P2, P3)); P2 -> insert(R3, 2, ?recv2(P1, P3)); - P3 -> insert(R3, 3, ?recv2(P1, P2)) + P3 -> insert(R3, 3, ?recv2(P1, P2)); + V -> suite:match_failure([V], [P1, P2, P3]) end end)()). -define(recv4(P1, P2, P3, P4), (fun() -> - case R4 = recv() of + case R4 = suite:recv(Config) of P1 -> insert(R4, 1, ?recv3(P2, P3, P4)); P2 -> insert(R4, 2, ?recv3(P1, P3, P4)); P3 -> insert(R4, 3, ?recv3(P1, P2, P4)); - P4 -> insert(R4, 4, ?recv3(P1, P2, P3)) + P4 -> insert(R4, 4, ?recv3(P1, P2, P3)); + V -> suite:match_failure([V], [P1, P2, P3, P4]) end end)()). -define(recv5(P1, P2, P3, P4, P5), (fun() -> - case R5 = recv() of + case R5 = suite:recv(Config) of P1 -> insert(R5, 1, ?recv4(P2, P3, P4, P5)); P2 -> insert(R5, 2, ?recv4(P1, P3, P4, P5)); P3 -> insert(R5, 3, ?recv4(P1, P2, P4, P5)); P4 -> insert(R5, 4, ?recv4(P1, P2, P3, P5)); - P5 -> insert(R5, 5, ?recv4(P1, P2, P3, P4)) + P5 -> insert(R5, 5, ?recv4(P1, P2, P3, P4)); + V -> suite:match_failure([V], [P1, P2, P3, P4, P5]) end end)()). +-define(match(Pattern, Result), + (fun() -> + case Result of + Pattern -> + ok; + Mismatch -> + suite:match_failure([Mismatch], [??Pattern]) + end + end)()). + +-define(match(Pattern, Result, PatternRes), + (fun() -> + case Result of + Pattern -> + PatternRes; + Mismatch -> + suite:match_failure([Mismatch], [??Pattern]) + end + end)()). + +-define(send_recv(Send, Recv), + ?match(Recv, suite:send_recv(Config, Send))). + +-define(retry(TIMEOUT, N, FUN), suite:retry(TIMEOUT, N, fun() -> FUN end)). + -define(COMMON_VHOST, <<"localhost">>). -define(MNESIA_VHOST, <<"mnesia.localhost">>). +-define(REDIS_VHOST, <<"redis.localhost">>). -define(MYSQL_VHOST, <<"mysql.localhost">>). +-define(MSSQL_VHOST, <<"mssql.localhost">>). -define(PGSQL_VHOST, <<"pgsql.localhost">>). +-define(SQLITE_VHOST, <<"sqlite.localhost">>). -define(LDAP_VHOST, <<"ldap.localhost">>). -define(EXTAUTH_VHOST, <<"extauth.localhost">>). --define(RIAK_VHOST, <<"riak.localhost">>). +-define(S2S_VHOST, <<"s2s.localhost">>). +-define(UPLOAD_VHOST, <<"upload.localhost">>). + +-define(BACKENDS, [mnesia, redis, mysql, mssql, odbc, pgsql, sqlite, ldap, extauth]). insert(Val, N, Tuple) -> L = tuple_to_list(Tuple), diff --git a/test/upload_tests.erl b/test/upload_tests.erl new file mode 100644 index 000000000..7e2d89958 --- /dev/null +++ b/test/upload_tests.erl @@ -0,0 +1,213 @@ +%%%------------------------------------------------------------------- +%%% Author : Evgeny Khramtsov +%%% Created : 17 May 2018 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(upload_tests). + +%% API +-compile(export_all). +-import(suite, [disconnect/1, is_feature_advertised/3, upload_jid/1, + my_jid/1, wait_for_slave/1, wait_for_master/1, + send_recv/2, put_event/2, get_event/1]). + +-include("suite.hrl"). +-define(CONTENT_TYPE, "image/png"). + +%%%=================================================================== +%%% API +%%%=================================================================== +%%%=================================================================== +%%% Single user tests +%%%=================================================================== +single_cases() -> + {upload_single, [sequence], + [single_test(feature_enabled), + single_test(service_vcard), + single_test(get_max_size), + single_test(slot_request), + single_test(put_get_request), + single_test(max_size_exceed)]}. + +feature_enabled(Config) -> + lists:foreach( + fun(NS) -> + true = is_feature_advertised(Config, NS, upload_jid(Config)) + end, namespaces()), + disconnect(Config). + +service_vcard(Config) -> + Upload = upload_jid(Config), + ct:comment("Retrieving vCard from ~s", [jid:encode(Upload)]), + VCard = mod_http_upload_opt:vcard(?config(server, Config)), + #iq{type = result, sub_els = [VCard]} = + send_recv(Config, #iq{type = get, to = Upload, sub_els = [#vcard_temp{}]}), + disconnect(Config). + +get_max_size(Config) -> + Xs = get_disco_info_xdata(Config), + lists:foreach( + fun(NS) -> + get_max_size(Config, Xs, NS) + end, namespaces()), + disconnect(Config). + +get_max_size(_, _, ?NS_HTTP_UPLOAD_OLD) -> + %% This old spec didn't specify 'max-file-size' attribute + ok; +get_max_size(Config, Xs, NS) -> + Xs = get_disco_info_xdata(Config), + get_size(NS, Config, Xs). + +slot_request(Config) -> + lists:foreach( + fun(NS) -> + slot_request(Config, NS) + end, namespaces()), + disconnect(Config). + +put_get_request(Config) -> + lists:foreach( + fun(NS) -> + {GetURL, PutURL, _Filename, Size} = slot_request(Config, NS), + Data = p1_rand:bytes(Size), + put_request(Config, PutURL, Data), + get_request(Config, GetURL, Data) + end, namespaces()), + disconnect(Config). + +max_size_exceed(Config) -> + lists:foreach( + fun(NS) -> + max_size_exceed(Config, NS) + end, namespaces()), + disconnect(Config). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +single_test(T) -> + list_to_atom("upload_" ++ atom_to_list(T)). + +get_disco_info_xdata(Config) -> + To = upload_jid(Config), + #iq{type = result, sub_els = [#disco_info{xdata = Xs}]} = + send_recv(Config, + #iq{type = get, sub_els = [#disco_info{}], to = To}), + Xs. + +get_size(NS, Config, [X|Xs]) -> + case xmpp_util:get_xdata_values(<<"FORM_TYPE">>, X) of + [NS] -> + [Size] = xmpp_util:get_xdata_values(<<"max-file-size">>, X), + true = erlang:binary_to_integer(Size) > 0, + Size; + _ -> + get_size(NS, Config, Xs) + end; +get_size(NS, _Config, []) -> + ct:fail({disco_info_xdata_failed, NS}). + +slot_request(Config, NS) -> + To = upload_jid(Config), + Filename = filename(), + Size = p1_rand:uniform(1, 1024), + case NS of + ?NS_HTTP_UPLOAD_0 -> + #iq{type = result, + sub_els = [#upload_slot_0{get = GetURL, + put = PutURL, + xmlns = NS}]} = + send_recv(Config, + #iq{type = get, to = To, + sub_els = [#upload_request_0{ + filename = Filename, + size = Size, + 'content-type' = <>, + xmlns = NS}]}), + {GetURL, PutURL, Filename, Size}; + _ -> + #iq{type = result, + sub_els = [#upload_slot{get = GetURL, + put = PutURL, + xmlns = NS}]} = + send_recv(Config, + #iq{type = get, to = To, + sub_els = [#upload_request{ + filename = Filename, + size = Size, + 'content-type' = <>, + xmlns = NS}]}), + {GetURL, PutURL, Filename, Size} + end. + +put_request(_Config, URL0, Data) -> + ct:comment("Putting ~B bytes to ~s", [size(Data), URL0]), + URL = binary_to_list(URL0), + {ok, {{"HTTP/1.1", 201, _}, _, _}} = + httpc:request(put, {URL, [], ?CONTENT_TYPE, Data}, [], []). + +get_request(_Config, URL0, Data) -> + ct:comment("Getting ~B bytes from ~s", [size(Data), URL0]), + URL = binary_to_list(URL0), + {ok, {{"HTTP/1.1", 200, _}, _, Body}} = + httpc:request(get, {URL, []}, [], [{body_format, binary}]), + ct:comment("Checking returned body"), + Body = Data. + +max_size_exceed(Config, NS) -> + To = upload_jid(Config), + Filename = filename(), + Size = 1000000000, + IQErr = + case NS of + ?NS_HTTP_UPLOAD_0 -> + #iq{type = error} = + send_recv(Config, + #iq{type = get, to = To, + sub_els = [#upload_request_0{ + filename = Filename, + size = Size, + 'content-type' = <>, + xmlns = NS}]}); + _ -> + #iq{type = error} = + send_recv(Config, + #iq{type = get, to = To, + sub_els = [#upload_request{ + filename = Filename, + size = Size, + 'content-type' = <>, + xmlns = NS}]}) + end, + check_size_error(IQErr, Size, NS). + +check_size_error(IQErr, Size, NS) -> + Err = xmpp:get_error(IQErr), + FileTooLarge = xmpp:get_subtag(Err, #upload_file_too_large{xmlns = NS}), + #stanza_error{reason = 'not-acceptable'} = Err, + #upload_file_too_large{'max-file-size' = MaxSize} = FileTooLarge, + true = Size > MaxSize. + +namespaces() -> + [?NS_HTTP_UPLOAD_0, ?NS_HTTP_UPLOAD, ?NS_HTTP_UPLOAD_OLD]. + +filename() -> + <<(p1_rand:get_string())/binary, ".png">>. diff --git a/test/vcard_tests.erl b/test/vcard_tests.erl new file mode 100644 index 000000000..fc3adb611 --- /dev/null +++ b/test/vcard_tests.erl @@ -0,0 +1,150 @@ +%%%------------------------------------------------------------------- +%%% Author : Evgeny Khramtsov +%%% Created : 16 Nov 2016 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(vcard_tests). + +%% API +-compile(export_all). +-import(suite, [send_recv/2, disconnect/1, is_feature_advertised/2, + is_feature_advertised/3, server_jid/1, + my_jid/1, wait_for_slave/1, wait_for_master/1, + recv_presence/1, recv/1]). + +-include("suite.hrl"). +-include_lib("stdlib/include/assert.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +%%%=================================================================== +%%% Single user tests +%%%=================================================================== +single_cases() -> + {vcard_single, [sequence], + [single_test(feature_enabled), + single_test(get_set), + single_test(service_vcard)]}. + +feature_enabled(Config) -> + BareMyJID = jid:remove_resource(my_jid(Config)), + true = is_feature_advertised(Config, ?NS_VCARD), + true = is_feature_advertised(Config, ?NS_VCARD, BareMyJID), + disconnect(Config). + +get_set(Config) -> + VCard = + #vcard_temp{fn = <<"Peter Saint-Andre">>, + n = #vcard_name{family = <<"Saint-Andre">>, + given = <<"Peter">>}, + nickname = <<"stpeter">>, + bday = <<"1966-08-06">>, + adr = [#vcard_adr{work = true, + extadd = <<"Suite 600">>, + street = <<"1899 Wynkoop Street">>, + locality = <<"Denver">>, + region = <<"CO">>, + pcode = <<"80202">>, + ctry = <<"USA">>}, + #vcard_adr{home = true, + locality = <<"Denver">>, + region = <<"CO">>, + pcode = <<"80209">>, + ctry = <<"USA">>}], + tel = [#vcard_tel{work = true,voice = true, + number = <<"303-308-3282">>}, + #vcard_tel{home = true,voice = true, + number = <<"303-555-1212">>}], + email = [#vcard_email{internet = true,pref = true, + userid = <<"stpeter@jabber.org">>}], + jabberid = <<"stpeter@jabber.org">>, + title = <<"Executive Director">>,role = <<"Patron Saint">>, + org = #vcard_org{name = <<"XMPP Standards Foundation">>}, + url = <<"http://www.xmpp.org/xsf/people/stpeter.shtml">>, + desc = <<"More information about me is located on my " + "personal website: http://www.saint-andre.com/">>}, + #iq{type = result, sub_els = []} = + send_recv(Config, #iq{type = set, sub_els = [VCard]}), + #iq{type = result, sub_els = [VCard1]} = + send_recv(Config, #iq{type = get, sub_els = [#vcard_temp{}]}), + ?assertEqual(VCard, VCard1), + disconnect(Config). + +service_vcard(Config) -> + JID = server_jid(Config), + ct:comment("Retrieving vCard from ~s", [jid:encode(JID)]), + VCard = mod_vcard_opt:vcard(?config(server, Config)), + #iq{type = result, sub_els = [VCard]} = + send_recv(Config, #iq{type = get, to = JID, sub_els = [#vcard_temp{}]}), + disconnect(Config). + +%%%=================================================================== +%%% Master-slave tests +%%%=================================================================== +master_slave_cases() -> + {vcard_master_slave, [sequence], []}. + %%[master_slave_test(xupdate)]}. + +xupdate_master(Config) -> + Img = <<137, "PNG\r\n", 26, $\n>>, + ImgHash = p1_sha:sha(Img), + MyJID = my_jid(Config), + Peer = ?config(slave, Config), + wait_for_slave(Config), + #presence{from = MyJID, type = available} = send_recv(Config, #presence{}), + #presence{from = Peer, type = available} = recv_presence(Config), + VCard = #vcard_temp{photo = #vcard_photo{type = <<"image/png">>, binval = Img}}, + #iq{type = result, sub_els = []} = + send_recv(Config, #iq{type = set, sub_els = [VCard]}), + #presence{from = MyJID, type = available, + sub_els = [#vcard_xupdate{hash = ImgHash}]} = recv_presence(Config), + #iq{type = result, sub_els = []} = + send_recv(Config, #iq{type = set, sub_els = [#vcard_temp{}]}), + {_, _} = ?recv2(#presence{from = MyJID, type = available, + sub_els = [#vcard_xupdate{hash = undefined}]}, + #presence{from = Peer, type = unavailable}), + disconnect(Config). + +xupdate_slave(Config) -> + Img = <<137, "PNG\r\n", 26, $\n>>, + ImgHash = p1_sha:sha(Img), + MyJID = my_jid(Config), + Peer = ?config(master, Config), + #presence{from = MyJID, type = available} = send_recv(Config, #presence{}), + wait_for_master(Config), + #presence{from = Peer, type = available} = recv_presence(Config), + #presence{from = Peer, type = available, + sub_els = [#vcard_xupdate{hash = ImgHash}]} = recv_presence(Config), + #presence{from = Peer, type = available, + sub_els = [#vcard_xupdate{hash = undefined}]} = recv_presence(Config), + disconnect(Config). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +single_test(T) -> + list_to_atom("vcard_" ++ atom_to_list(T)). + +master_slave_test(T) -> + {list_to_atom("vcard_" ++ atom_to_list(T)), [parallel], + [list_to_atom("vcard_" ++ atom_to_list(T) ++ "_master"), + list_to_atom("vcard_" ++ atom_to_list(T) ++ "_slave")]}. diff --git a/test/webadmin_tests.erl b/test/webadmin_tests.erl new file mode 100644 index 000000000..fa12fd6f8 --- /dev/null +++ b/test/webadmin_tests.erl @@ -0,0 +1,148 @@ +%%%------------------------------------------------------------------- +%%% Author : Pawel Chmielowski +%%% Created : 23 Mar 2020 by Pawel Chmielowski +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 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(webadmin_tests). + +%% API +-compile(export_all). +-import(suite, [disconnect/1, is_feature_advertised/3, upload_jid/1, +my_jid/1, wait_for_slave/1, wait_for_master/1, +send_recv/2, put_event/2, get_event/1]). + +-include("suite.hrl"). +-include_lib("stdlib/include/assert.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +%%%=================================================================== +%%% Single user tests +%%%=================================================================== +single_cases() -> + {webadmin_single, [sequence], + [single_test(login_page), + single_test(welcome_page), + single_test(user_page), + single_test(adduser), + single_test(changepassword), + single_test(removeuser)]}. + +login_page(Config) -> + Headers = ?match({ok, {{"HTTP/1.1", 401, _}, Headers, _}}, + httpc:request(get, {page(Config, ""), []}, [], + [{body_format, binary}]), + Headers), + ?match("basic realm=\"ejabberd\"", proplists:get_value("www-authenticate", Headers, none)). + +welcome_page(Config) -> + Body = ?match({ok, {{"HTTP/1.1", 200, _}, _, Body}}, + httpc:request(get, {page(Config, ""), [basic_auth_header(Config)]}, [], + [{body_format, binary}]), + Body), + ?match({_, _}, binary:match(Body, <<"ejabberd Web Admin">>)). + +user_page(Config) -> + Server = ?config(server, Config), + URL = "server/" ++ binary_to_list(Server) ++ "/user/admin/", + Body = ?match({ok, {{"HTTP/1.1", 200, _}, _, Body}}, + httpc:request(get, {page(Config, URL), [basic_auth_header(Config)]}, [], + [{body_format, binary}]), + Body), + ?match({_, _}, binary:match(Body, <<"ejabberd Web Admin">>)). + +adduser(Config) -> + User = <<"userwebadmin-", (?config(user, Config))/binary>>, + Server = ?config(server, Config), + Password = ?config(password, Config), + Body = make_query( + Config, + "server/" ++ binary_to_list(Server) ++ "/users/", + <<"register/user=", (mue(User))/binary, "®ister/password=", + (mue(Password))/binary, "®ister=Register">>), + Password = ejabberd_auth:get_password_s(User, Server), + ?match({_, _}, binary:match(Body, <<"User ", User/binary, "@", Server/binary, + " successfully registered">>)). + +changepassword(Config) -> + User = <<"userwebadmin-", (?config(user, Config))/binary>>, + Server = ?config(server, Config), + Password = <<"newpassword-", (?config(password, Config))/binary>>, + Body = make_query( + Config, + "server/" ++ binary_to_list(Server) + ++ "/user/" ++ binary_to_list(mue(User)) ++ "/", + <<"change_password/newpass=", (mue(Password))/binary, + "&change_password=Change+Password">>), + ?match(Password, ejabberd_auth:get_password_s(User, Server)), + ?match({_, _}, binary:match(Body, <<"<div class='result'><code>ok</code></div>">>)). + +removeuser(Config) -> + User = <<"userwebadmin-", (?config(user, Config))/binary>>, + Server = ?config(server, Config), + Body = make_query( + Config, + "server/" ++ binary_to_list(Server) + ++ "/user/" ++ binary_to_list(mue(User)) ++ "/", + <<"&unregister=Unregister">>), + false = ejabberd_auth:user_exists(User, Server), + ?match(nomatch, binary:match(Body, <<"<h3>Last Activity</h3>20">>)). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +single_test(T) -> + list_to_atom("webadmin_" ++ atom_to_list(T)). + +basic_auth_header(Config) -> + User = <<"admin">>, + Server = ?config(server, Config), + Password = ?config(password, Config), + ejabberd_auth:try_register(User, Server, Password), + basic_auth_header(User, Server, Password). + +basic_auth_header(Username, Server, Password) -> + JidBin = <<Username/binary, "@", Server/binary, ":", Password/binary>>, + {"authorization", "Basic " ++ base64:encode_to_string(JidBin)}. + +page(Config, Tail) -> + Server = ?config(server_host, Config), + Port = ct:get_config(web_port, 5280), + Url = "http://" ++ Server ++ ":" ++ integer_to_list(Port) ++ "/admin/" ++ Tail, + %% This bypasses a bug introduced in Erlang OTP R21 and fixed in 23.2: + case catch uri_string:normalize("/%2525") of + "/%25" -> + string:replace(Url, "%25", "%2525", all); + _ -> + Url + end. + +mue(Binary) -> + misc:url_encode(Binary). + +make_query(Config, URL, BodyQ) -> + ?match({ok, {{"HTTP/1.1", 200, _}, _, Body}}, + httpc:request(post, {page(Config, URL), + [basic_auth_header(Config)], + "application/x-www-form-urlencoded", + BodyQ}, [], + [{body_format, binary}]), + Body). diff --git a/tools/captcha-ng.sh b/tools/captcha-ng.sh new file mode 100755 index 000000000..bb57385c4 --- /dev/null +++ b/tools/captcha-ng.sh @@ -0,0 +1,132 @@ +#!/bin/bash + +# Copyright © 2021 Adrien Bourmault (neox@os-k.eu) +# 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 script is an example captcha script. +# It takes the text to recognize in the captcha image as a parameter. +# It return the image binary as a result. ejabberd support PNG, JPEG and GIF. + +# The whole idea of the captcha script is to let server admins adapt it to +# their own needs. The goal is to be able to make the captcha generation as +# unique as possible, to make the captcha challenge difficult to bypass by +# a bot. +# Server admins are thus supposed to write and use their own captcha generators. + +# This script relies on ImageMagick. +# It is NOT compliant with ImageMagick forks like GraphicsMagick. + +INPUT=$1 + +TRANSFORMATIONS=(INTRUDER SUM) +DIGIT=(zero one two three four five six seven eight nine ten) + +if test -n "${BASH_VERSION:-''}" ; then + get_random () + { + R=$RANDOM + } +else + for n in $(od -A n -t u2 -N 64 /dev/urandom); do RL="$RL$n "; done + get_random () + { + R=${RL%% *} + RL=${RL#* } + } +fi + +INTRUDER() +{ +NUMBERS=$(echo "$INPUT" | grep -o . | tr '\n' ' ') +SORTED_UNIQ_NUM=$(echo "${NUMBERS[@]}" | sort -u | tr '\n' ' ') +SORT_RANDOM_CMD="$( ( echo x|sort -R >&/dev/null && echo "sort -R" ) || ( echo x|shuf >&/dev/null && echo shuf ) || echo cat)" +RANDOM_DIGITS=$(echo 123456789 | grep -o . | eval "$SORT_RANDOM_CMD" | tr '\n' ' ') +INTRUDER=-1 + +for i in $RANDOM_DIGITS +do + if [[ ! " ${SORTED_UNIQ_NUM[@]} " =~ ${i} ]]; then + INTRUDER=$i + break + fi +done + +# Worst case +if [[ $INTRUDER -eq "-1" ]] +then + printf "Type %s \n without changes" "$INPUT" + return +fi + +for num in ${NUMBERS} +do + get_random + R=$((R % 100)) + + if [[ $R -lt 60 ]]; then + NEWINPUT=${NEWINPUT}${num}${INTRUDER} + else + NEWINPUT=${NEWINPUT}${num} + fi +done + +get_random +R=$((R % 100)) + +if [[ $R -lt 50 ]]; then + printf "Type %s by\n deleting the %s" "$NEWINPUT" "${DIGIT[$INTRUDER]}" +else + printf "Enter %s by\n removing the %s" "$NEWINPUT" "${DIGIT[$INTRUDER]}" +fi +} + + +SUM() +{ +get_random +RA=$((R % 100)) + +if [[ $((INPUT % 2)) -eq 0 ]]; then + A=$((INPUT - RA)) + B=$RA +else + B=$((INPUT - RA)) + A=$RA +fi + +get_random +R=$((R % 100)) + +if [[ $R -lt 25 ]]; then + printf "Type the result\n of %s + %s" "$A" "$B" +elif [[ $R -lt 50 ]]; then + printf "SUMx\n %s and %s" "$A" "$B" +elif [[ $R -lt 75 ]]; then + printf "Add\n %s and %s" "$A" "$B" +else + printf "Enter the result\n of %s + %s" "$A" "$B" +fi +} + + +get_random + +RAND_ITALIC=$((R % 25)) +get_random + +RAND_ANGLE=$((R % 3)) +get_random + +RAND_INDEX=$((R % ${#TRANSFORMATIONS[@]})) + +convert -size 300x60 xc:none -pointsize 20 \ +\( -clone 0 -fill black \ +-stroke black -strokewidth 1 \ +-annotate "${RAND_ANGLE}x${RAND_ITALIC}+0+0" "\n $(${TRANSFORMATIONS[$RAND_INDEX]})" \ +-roll +"$ROLL_X"+0 \ +-wave "$WAVE1_AMPLITUDE"x"$WAVE1_LENGTH" \ +-roll -"$ROLL_X"+0 \) \ +-flatten -crop 300x60 +repage -quality 500 -depth 11 png:- diff --git a/tools/captcha.sh b/tools/captcha.sh index 560a048ad..7885858a2 100755 --- a/tools/captcha.sh +++ b/tools/captcha.sh @@ -1,70 +1,76 @@ #!/bin/sh +# This script is an example captcha script. +# It takes the text to recognize in the captcha image as a parameter. +# It return the image binary as a result. ejabberd support PNG, JPEG and GIF. + +# The whole idea of the captcha script is to let server admins adapt it to +# their own needs. The goal is to be able to make the captcha generation as +# unique as possible, to make the captcha challenge difficult to bypass by +# a bot. +# Server admins are thus supposed to write and use their own captcha generators. + +# This script relies on ImageMagick. +# It is NOT compliant with ImageMagick forks like GraphicsMagick. + 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/tools/check_xep_versions.sh b/tools/check_xep_versions.sh new file mode 100755 index 000000000..60c5fa529 --- /dev/null +++ b/tools/check_xep_versions.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +check_xep() +{ + xep=xep-$1 + int=$(echo $1 | sed 's/^0*//') + [ -f $BASE/doc/$xep ] || curl -s -o $BASE/doc/$xep https://xmpp.org/extensions/$xep.html + title=$(sed '/<title>/!d;s/.*<title>\(.*\)<\/title>.*/\1/' $BASE/doc/$xep) + vsn=$(grep -A1 Version $BASE/doc/$xep | sed '/<dd>/!d;q' | sed 's/.*>\(.*\)<.*/\1/') + imp=$(grep "{xep, $int," $BASE/src/* | sed "s/.*src\/\(.*\).erl.*[0-9], '\([0-9.-]*\)'.*/\1 \2/") + [ "$imp" == "" ] && imp="NA 0.0" + echo "$title;$vsn;${imp/ /;}" +} + +[ $# -eq 1 ] && BASE="$1" || BASE="$PWD" +[ -d $BASE/doc ] || mkdir $BASE/doc + +for x_num in $(grep "{xep" $BASE/src/* | sed "s/,//" | awk '{printf("%04d\n", $2)}' | sort -u) +do + check_xep $x_num +done diff --git a/tools/configure.erl b/tools/configure.erl deleted file mode 100644 index 09d787a54..000000000 --- a/tools/configure.erl +++ /dev/null @@ -1,89 +0,0 @@ -%%%---------------------------------------------------------------------- -%%% File : configure.erl -%%% Author : Alexey Shchepin <alexey@process-one.net> -%%% Purpose : -%%% Created : 27 Jan 2003 by Alexey Shchepin <alexey@process-one.net> -%%% -%%% -%%% ejabberd, Copyright (C) 2002-2015 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(configure). --author('alexey@process-one.net'). - --export([start/0]). - --include("ejabberd.hrl"). --include("logger.hrl"). - -start() -> - Static = case os:getenv("arg") of - false -> - false; - "static" -> - true; - _ -> - false - end, - case Static of - true -> - ExpatLib = "EXPAT_LIB = $(EXPAT_DIR)\\StaticLibs\\libexpatMT.lib\n", - ExpatFlag = "EXPAT_FLAG = -DXML_STATIC\n", - IconvDir = "ICONV_DIR = c:\\sdk\\GnuWin32\n", - IconvLib = "ICONV_LIB = $(ICONV_DIR)\\lib\\libiconv.lib\n", - ZlibDir = "ZLIB_DIR = c:\\sdk\\GnuWin32\n", - ZlibLib = "ZLIB_LIB = $(ZLIB_DIR)\\lib\\zlib.lib\n"; - false -> - ExpatLib = "EXPAT_LIB = $(EXPAT_DIR)\\Libs\\libexpat.lib\n", - ExpatFlag = "", - IconvDir = "ICONV_DIR = c:\\sdk\\GnuWin32\n", - IconvLib = "ICONV_LIB = $(ICONV_DIR)\\lib\\libiconv.lib\n", - ZlibDir = "ZLIB_DIR = c:\\sdk\\GnuWin32\n", - ZlibLib = "ZLIB_LIB = $(ZLIB_DIR)\\lib\\zlib.lib\n" - end, - - EVersion = "ERLANG_VERSION = " ++ erlang:system_info(version) ++ "\n", - EIDirS = "EI_DIR = " ++ code:lib_dir(erl_interface) ++ "\n", - RootDirS = "ERLANG_DIR = " ++ code:root_dir() ++ "\n", - %% Load the ejabberd application description so that ?VERSION can read the vsn key - application:load(ejabberd), - Version = "EJABBERD_VERSION = " ++ binary_to_list(?VERSION) ++ "\n", - ExpatDir = "EXPAT_DIR = c:\\sdk\\Expat-2.0.0\n", - OpenSSLDir = "OPENSSL_DIR = c:\\sdk\\OpenSSL\n", - DBType = "DBTYPE = generic\n", %% 'generic' or 'mssql' - - SSLDir = "SSLDIR = " ++ code:lib_dir(ssl) ++ "\n", - StdLibDir = "STDLIBDIR = " ++ code:lib_dir(stdlib) ++ "\n", - - file:write_file("Makefile.inc", - list_to_binary(EVersion ++ - EIDirS ++ - RootDirS ++ - Version ++ - SSLDir ++ - StdLibDir ++ - OpenSSLDir ++ - DBType ++ - ExpatDir ++ - ExpatLib ++ - ExpatFlag ++ - IconvDir ++ - IconvLib ++ - ZlibDir ++ - ZlibLib)), - halt(). diff --git a/tools/ejabberdctl.bc b/tools/ejabberdctl.bc index 72a5356f2..b0c88fa15 100644 --- a/tools/ejabberdctl.bc +++ b/tools/ejabberdctl.bc @@ -1,15 +1,27 @@ # # bash completion for ejabberdctl # +# For installation and details see: +# https://docs.ejabberd.im/admin/guide/managing/#bash-completion +# + get_help() { - local COMMANDCACHE=/var/log/ejabberd/bash_completion_$RANDOM - ejabberdctl $CTLARGS help >$COMMANDCACHE - if [[ $? == 2 ]] ; then - ISRUNNING=1 - runningcommands=`cat $COMMANDCACHE | grep "^ [a-z]" | awk '{print $1}' | xargs` + local CACHE_BASE=/tmp/ejabberd_bash_completion + local DATESTRING=`date +%F-%H` + local CACHE=$CACHE_BASE.$DATESTRING + local CACHE_COMS=$CACHE.coms + local CACHE_TAGS=$CACHE.tags + if [[ ! -f $CACHE_COMS ]] ; then + rm -f $CACHE_BASE.* + [ -f $CACHE_COMS ] || ejabberdctl $CTLARGS | sed "s|\x1B\[[0-9]*m||g" | grep "^ [a-z]" | awk '{print $1}' | xargs >$CACHE_COMS + [ -f $CACHE_TAGS ] || ejabberdctl $CTLARGS help tags | sed "s|\x1B\[[0-9]*m||g" | grep "^ [a-z]" | awk '{print $1}' | xargs >$CACHE_TAGS + fi + if [[ $? == 2 ]] || [[ -f $CACHE_COMS ]] ; then + ISRUNNING=1 + runningcommands=`cat $CACHE_COMS` + runningtags=`cat $CACHE_TAGS` fi - rm $COMMANDCACHE } _ejabberdctl() @@ -22,8 +34,8 @@ _ejabberdctl() cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" - local startcoms="start debug live" - local startpars="--config-dir --config --ctl-config --logs --spool" + local startcoms="start foreground foreground-quiet live debug etop iexdebug iexlive ping started stopped" + local startpars="--config-dir --config --ctl-config --logs --node --spool" local i=1 local CTLARGS="" @@ -42,7 +54,7 @@ _ejabberdctl() ejabberdctl) # This clause matches even when calling `/sbin/ejabberdctl` thanks to the ##*/ in the case get_help - COMPREPLY=($(compgen -W "--node --auth ${startpars} ${startcoms} ${runningcommands}" -- $cur)) + COMPREPLY=($(compgen -W "--node ${startpars} ${startcoms} ${runningcommands}" -- $cur)) return 0 ;; start|live) @@ -55,7 +67,7 @@ _ejabberdctl() ;; help) get_help - COMPREPLY=($(compgen -W "${runningcommands}" -- $cur)) + COMPREPLY=($(compgen -W "${runningcommands} ${runningtags}" -- $cur)) return 0 ;; --node) @@ -75,7 +87,7 @@ _ejabberdctl() prev2="${COMP_WORDS[COMP_CWORD-2]}" get_help if [[ "$prev2" == --* ]]; then - COMPREPLY=($(compgen -W "--node --auth ${startpars} ${startcoms} ${runningcommands}" -- $cur)) + COMPREPLY=($(compgen -W "--node ${startpars} ${startcoms} ${runningcommands}" -- $cur)) else if [[ $ISRUNNING == 1 ]]; then echo "" diff --git a/tools/emacs-indent.sh b/tools/emacs-indent.sh new file mode 100755 index 000000000..f3caecf4c --- /dev/null +++ b/tools/emacs-indent.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +# To indent and remove tabs, surround the piece of code with: +# %% @indent-begin +# %% @indent-end +# +# Then run: +# make indent +# +# Please note this script only indents the first occurrence. + +FILES=$(git grep --name-only @indent-begin src/) + +for FILENAME in $FILES; do + echo "==> Indenting $FILENAME..." + emacs -batch $FILENAME \ + -f "erlang-mode" \ + --eval "(goto-char (point-min))" \ + --eval "(re-search-forward \"@indent-begin\" nil t)" \ + --eval "(setq begin (line-beginning-position))" \ + --eval "(re-search-forward \"@indent-end\" nil t)" \ + --eval "(setq end (line-beginning-position))" \ + --eval "(erlang-indent-region begin end)" \ + --eval "(untabify begin end)" \ + -f "delete-trailing-whitespace" \ + -f "save-buffer" +done diff --git a/tools/extract-tr.sh b/tools/extract-tr.sh new file mode 100755 index 000000000..99a391427 --- /dev/null +++ b/tools/extract-tr.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env escript +%% -*- erlang -*- + +main(Paths) -> + Dict = fold_erls( + fun(File, Tokens, Acc) -> + extract_tr(File, Tokens, Acc) + end, dict:new(), Paths), + generate_pot(Dict). + +extract_tr(File, [{'?', _}, {var, _, 'T'}, {'(', Line}|Tokens], Acc) -> + case extract_string(Tokens, "") of + {"", Tokens1} -> + err("~s:~B: Warning: invalid string", [File, Line]), + extract_tr(File, Tokens1, Acc); + {String, Tokens1} -> + extract_tr(File, Tokens1, dict:append(String, {File, Line}, Acc)) + end; +extract_tr(_File, [{atom,_,module}, {'(',_}, {atom,_,ejabberd_doc} | _Tokens], Acc) -> + Acc; +extract_tr(File, [{atom, _, F}, {'(',_} | Tokens], Acc) + when (F == mod_doc); (F == doc) -> + Tokens2 = consume_tokens_until_dot(Tokens), + extract_tr(File, Tokens2, Acc); +extract_tr(File, [_|Tokens], Acc) -> + %%err("~p~n", [A]), + extract_tr(File, Tokens, Acc); +extract_tr(_, [], Acc) -> + Acc. + +consume_tokens_until_dot([{dot, _} | Tokens]) -> + Tokens; +consume_tokens_until_dot([_ | Tokens]) -> + consume_tokens_until_dot(Tokens). + +extract_string([{string, _, S}|Tokens], Acc) -> + extract_string(Tokens, [S|Acc]); +extract_string([{')', _}|Tokens], Acc) -> + {lists:flatten(lists:reverse(Acc)), Tokens}; +extract_string(Tokens, _) -> + {"", Tokens}. + +fold_erls(Fun, State, Paths) -> + Paths1 = fold_paths(Paths), + Total = length(Paths1), + {_, State1} = + lists:foldl( + fun(File, {I, Acc}) -> + io:format(standard_error, + "Progress: ~B% (~B/~B)\r", + [round(I*100/Total), I, Total]), + case tokens(File) of + {ok, Tokens} -> + {I+1, Fun(File, Tokens, Acc)}; + error -> + {I+1, Acc} + end + end, {0, State}, Paths1), + State1. + +fold_paths(Paths) -> + lists:flatmap( + fun(Path) -> + case filelib:is_dir(Path) of + true -> + lists:reverse( + filelib:fold_files( + Path, ".+\.erl\$", false, + fun(File, Acc) -> + [File|Acc] + end, [])); + false -> + [Path] + end + end, Paths). + +tokens(File) -> + case file:read_file(File) of + {ok, Data} -> + case erl_scan:string(binary_to_list(Data)) of + {ok, Tokens, _} -> + {ok, Tokens}; + {error, {_, Module, Desc}, Line} -> + err("~s:~n: Warning: scan error: ~s", + [filename:basename(File), Line, Module:format_error(Desc)]), + error + end; + {error, Why} -> + err("Warning: failed to read file ~s: ~s", + [File, file:format_error(Why)]), + error + end. + +generate_pot(Dict) -> + io:format("~s~n~n", [pot_header()]), + lists:foreach( + fun({Msg, Location}) -> + S1 = format_location(Location), + S2 = format_msg(Msg), + io:format("~smsgstr \"\"~n~n", [S1 ++ S2]) + end, lists:keysort(1, dict:to_list(Dict))). + +format_location([A, B, C|T]) -> + format_location_list([A,B,C]) ++ format_location(T); +format_location([A, B|T]) -> + format_location_list([A,B]) ++ format_location(T); +format_location([A|T]) -> + format_location_list([A]) ++ format_location(T); +format_location([]) -> + "". + +format_location_list(L) -> + "#: " ++ string:join( + lists:map( + fun({File, Pos}) -> + io_lib:format("~s:~B", [File, Pos]) + end, L), + " ") ++ io_lib:nl(). + +format_msg(Bin) -> + io_lib:format("msgid \"~s\"~n", [escape(Bin)]). + +escape(Bin) -> + lists:map( + fun($") -> "\\\""; + (C) -> C + end, binary_to_list(iolist_to_binary(Bin))). + +pot_header() -> + string:join( + ["msgid \"\"", + "msgstr \"\"", + "\"Project-Id-Version: 15.11.127\\n\"", + "\"X-Language: Language Name\\n\"", + "\"Last-Translator: Translator name and contact method\\n\"", + "\"MIME-Version: 1.0\\n\"", + "\"Content-Type: text/plain; charset=UTF-8\\n\"", + "\"Content-Transfer-Encoding: 8bit\\n\"", + "\"X-Poedit-Basepath: ../..\\n\"", + "\"X-Poedit-SearchPath-0: .\\n\""], + io_lib:nl()). + +err(Format, Args) -> + io:format(standard_error, Format ++ io_lib:nl(), Args). diff --git a/tools/generate-doap.sh b/tools/generate-doap.sh new file mode 100755 index 000000000..2892aa969 --- /dev/null +++ b/tools/generate-doap.sh @@ -0,0 +1,140 @@ +#!/bin/bash + +# Erlang modules in ejabberd use a custom module attribute [1] +# named -protocol to define what XEPs and RFCs that module implements. +# General protocols are defined in ejabberd.erl +# +# The supported syntax is: +# -protocol({rfc, RFC-NUMBER}). +# -protocol({xep, XEP-NUMBER, XEP-VERSION}). +# -protocol({xep, XEP-NUMBER, XEP-VERSION, EJABBERD-VERSION, STATUS, COMMENTS}). +# Where +# RFC-NUMBER, XEP-NUMBER :: integer() +# XEP-VERSION, EJABBERD-VERSION :: atom() +# STATUS, COMMENTS :: string() +# For example: +# -protocol({rfc, 5766}). +# -protocol({xep, 111, '0.2'}). +# -protocol({xep, 222, '1.2.0', '17.09', "", ""}). +# -protocol({xep, 333, '1.11.2', '21.09', "complete", ""}). +# -protocol({xep, 333, '0.2.0', '21.09', "partial", "Only client X is supported"}). +# +# [1] https://www.erlang.org/doc/reference_manual/modules.html#module-attributes + +write_doap_head() +{ + cat >"$1" <<-'EOF' +<?xml version="1.0" encoding="UTF-8"?> +<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns="http://usefulinc.com/ns/doap#" + xmlns:xmpp="https://linkmauve.fr/ns/xmpp-doap#" + xmlns:schema="https://schema.org/"> + <Project> + <name>ejabberd</name> + <shortdesc>XMPP Server with MQTT Broker and SIP Service</shortdesc> + <description>Robust, Ubiquitous and Massively Scalable Messaging Platform (XMPP Server, MQTT Broker, SIP Service)</description> + <created>2002-11-16</created> + <os>BSD</os> + <os>Linux</os> + <os>macOS</os> + <os>Windows</os> + <programming-langauge>Erlang</programming-langauge> + <programming-langauge>C</programming-langauge> + <category rdf:resource="https://linkmauve.fr/ns/xmpp-doap#category-jabber"/> + <category rdf:resource="https://linkmauve.fr/ns/xmpp-doap#category-server"/> + <category rdf:resource="https://linkmauve.fr/ns/xmpp-doap#category-xmpp"/> + + <homepage rdf:resource="https://www.ejabberd.im"/> + <download-page rdf:resource="https://www.process-one.net/download/ejabberd/"/> + <download-mirror rdf:resource="https://github.com/processone/ejabberd/tags"/> + <license rdf:resource="https://raw.githubusercontent.com/processone/ejabberd/master/COPYING"/> + <schema:logo rdf:resource="https://docs.ejabberd.im/assets/img/footer_logo_e@2x.png"/> + <bug-database rdf:resource="https://github.com/processone/ejabberd/issues"/> + <support-forum rdf:resource="xmpp:ejabberd@conference.process-one.net?join"/> + <repository> + <GitRepository> + <location rdf:resource="https://github.com/processone/ejabberd.git"/> + <browse rdf:resource="https://github.com/processone/ejabberd"/> + </GitRepository> + </repository> + +EOF +} + +write_doap_tail() +{ + cat >>"$1" <<-'EOF' + </Project> +</rdf:RDF> +EOF +} + +write_rfcs() +{ + rfc=rfc$1 + out=$2 + int=$(echo $1 | sed 's/^0*//') + + imp=$(grep "\-protocol({rfc, $int," $BASE/src/* | sed "s/.*src\/\(.*\).erl.*'\([0-9.-x]*\)'.*/\1 \2/") + [ "$imp" = "" ] && imp="NA 0.0" + + echo " <implements rdf:resource=\"https://www.rfc-editor.org/info/$rfc\"/>" >>$out +} + +write_xeps() +{ + xep=xep-$1 + out=$2 + comments2="" + int=$(echo $1 | sed 's/^0*//') + imp=$(grep "\-protocol({xep, $int," $BASE/src/* | sed "s/.*src\/\(.*\).erl.*'\([0-9.-x]*\)'.*/\1 \2/") + [ "$imp" = "" ] && imp="NA 0.0" + + sourcefiles=$(grep "\-protocol({xep, $int," $BASE/src/* | sed "s/.*src\/\(.*\).erl.*'\([0-9.-x]*\)'.*/\1/" | tr '\012' ',' | sed 's|,$||' | sed 's|,|, |g' | sed 's|^ejabberd$||') + + versionsold=$(grep "\-protocol({xep, $int, .*'})\." $BASE/src/* | sed "s/.*'\([0-9.-x]*\)'.*/\1/" | head -1) + versionsnew=$(grep "\-protocol({xep, $int, .*\"})\." $BASE/src/* | sed "s/.*'\([0-9.-x]*\)', '.*/\1/" | head -1) + versions="$versionsold$versionsnew" + + since=$(grep "\-protocol({xep, $int, .*\"})\." $BASE/src/* | sed "s/.*', '\([0-9.-x]*\)',.*/\1/" | head -1) + status=$(grep "\-protocol({xep, $int, .*\"})\." $BASE/src/* | sed "s/.*', \"\([a-z]*\)\", \".*/\1/" | head -1) + + comments=$(grep "\-protocol({xep, $int, .*\"})\." $BASE/src/* | sed "s/.*\", \"\(.*\)\"}.*/\1/" | head -1) + [ -n "$comments" ] && comments2=", $comments" + note="$sourcefiles$comments2" + + { + echo " <implements>" + echo " <xmpp:SupportedXep>" + echo " <xmpp:xep rdf:resource=\"https://xmpp.org/extensions/$xep.html\"/>" + echo " <xmpp:version>$versions</xmpp:version>" + echo " <xmpp:since>$since</xmpp:since>" + echo " <xmpp:status>$status</xmpp:status>" + echo " <xmpp:note>$note</xmpp:note>" + echo " </xmpp:SupportedXep>" + echo " </implements>" + } >>$out +} + +[ $# -eq 1 ] && BASE="$1" || BASE="$PWD" +[ -d $BASE/doc ] || mkdir $BASE/doc +temp=tools/ejabberd.temp +final=ejabberd.doap + +write_doap_head $final + +grep "\-protocol({rfc" $BASE/src/* | sed "s/,//" | awk '{printf("%04d\n", $2)}' | sort -u | while IFS= read -r x_num +do + write_rfcs $x_num $temp +done +echo "" >>$temp + +grep "\-protocol({xep" $BASE/src/* | sed "s/,//" | awk '{printf("%04d\n", $2)}' | sort -u | while IFS= read -r x_num +do + write_xeps $x_num $temp +done + +cat $temp >>$final +rm $temp + +write_doap_tail $final diff --git a/tools/hook_deps.sh b/tools/hook_deps.sh new file mode 100755 index 000000000..3a3271b50 --- /dev/null +++ b/tools/hook_deps.sh @@ -0,0 +1,449 @@ +#!/usr/bin/env escript +%% -*- erlang -*- + +-record(state, {run_hooks = #{}, + run_fold_hooks = #{}, + hooked_funs = {#{}, #{}}, + iq_handlers = {#{}, #{}}, + exports = #{}, + module :: module(), + file :: filename:filename()}). + +main(Paths) -> + State = + fold_beams( + fun(File0, Tree, X, Acc0) -> + BareName = filename:rootname(filename:basename(File0)), + Mod = list_to_atom(BareName), + File = BareName ++ ".erl", + Exports = maps:put(Mod, X, Acc0#state.exports), + Acc1 = Acc0#state{file = File, module = Mod, exports = Exports}, + erl_syntax_lib:fold( + fun(Form, Acc) -> + case erl_syntax:type(Form) of + application -> + case erl_syntax_lib:analyze_application(Form) of + {ejabberd_hooks, {run, N}} when N == 2; N == 3 -> + collect_run_hook(Form, Acc); + {ejabberd_hooks, {run_fold, N}} when N == 3; N == 4 -> + collect_run_fold_hook(Form, Acc); + {ejabberd_hooks, {add, N}} when N == 4; N == 5 -> + collect_run_fun(Form, add, Acc); + {ejabberd_hooks, {delete, N}} when N == 4; N == 5 -> + collect_run_fun(Form, delete, Acc); + {gen_iq_handler, {add_iq_handler, 5}} -> + collect_iq_handler(Form, add, Acc); + {gen_iq_handler, {remove_iq_handler, 3}} -> + collect_iq_handler(Form, delete, Acc); + _ -> + Acc + end; + _ -> + Acc + end + end, Acc1, Tree) + end, #state{}, Paths), + check_hooks_arity(State#state.run_hooks), + check_hooks_arity(State#state.run_fold_hooks), + check_iq_handlers_export(State#state.iq_handlers, State#state.exports), + analyze_iq_handlers(State#state.iq_handlers), + analyze_hooks(State#state.hooked_funs), + RunDeps = build_deps(State#state.run_hooks, State#state.hooked_funs), + RunFoldDeps = build_deps(State#state.run_fold_hooks, State#state.hooked_funs), + emit_module(RunDeps, RunFoldDeps, hooks_type_test). + +collect_run_hook(Form, State) -> + [Hook|Tail] = erl_syntax:application_arguments(Form), + case atom_value(Hook, State) of + undefined -> + State; + HookName -> + Args = case Tail of + [_Host, Args0] -> Args0; + [Args0] -> + Args0 + end, + Arity = erl_syntax:list_length(Args), + Hooks = maps:put({HookName, Arity}, + {State#state.file, erl_syntax:get_pos(Hook)}, + State#state.run_hooks), + State#state{run_hooks = Hooks} + end. + +collect_run_fold_hook(Form, State) -> + [Hook|Tail] = erl_syntax:application_arguments(Form), + case atom_value(Hook, State) of + undefined -> + State; + HookName -> + Args = case Tail of + [_Host, _Val, Args0] -> Args0; + [_Val, Args0] -> Args0 + end, + Arity = erl_syntax:list_length(Args) + 1, + Hooks = maps:put({HookName, Arity}, + {State#state.file, erl_syntax:get_pos(Form)}, + State#state.run_fold_hooks), + State#state{run_fold_hooks = Hooks} + end. + +collect_run_fun(Form, Action, State) -> + [Hook|Tail] = erl_syntax:application_arguments(Form), + case atom_value(Hook, State) of + undefined -> + State; + HookName -> + {Module, Fun, Seq} = case Tail of + [_Host, M, F, S] -> + {M, F, S}; + [M, F, S] -> + {M, F, S} + end, + ModName = module_name(Module, State), + FunName = atom_value(Fun, State), + SeqInt = integer_value(Seq, State), + if ModName /= undefined, FunName /= undefined, SeqInt /= undefined -> + Pos = case Action of + add -> 1; + delete -> 2 + end, + Funs = maps_append( + HookName, + {ModName, FunName, SeqInt, + {State#state.file, erl_syntax:get_pos(Form)}}, + element(Pos, State#state.hooked_funs)), + Hooked = setelement(Pos, State#state.hooked_funs, Funs), + State#state{hooked_funs = Hooked}; + true -> + State + end + end. + +collect_iq_handler(Form, add, #state{iq_handlers = {Add, Del}} = State) -> + [Component, _Host, Namespace, Module, Function] = erl_syntax:application_arguments(Form), + Mod = module_name(Module, State), + Fun = atom_value(Function, State), + Comp = atom_value(Component, State), + NS = binary_value(Namespace, State), + if Mod /= undefined, Fun /= undefined, Comp /= undefined, NS /= undefined -> + Handlers = maps_append( + {Comp, NS}, + {Mod, Fun, + {State#state.file, erl_syntax:get_pos(Form)}}, + Add), + State#state{iq_handlers = {Handlers, Del}}; + true -> + State + end; +collect_iq_handler(Form, delete, #state{iq_handlers = {Add, Del}} = State) -> + [Component, _Host, Namespace] = erl_syntax:application_arguments(Form), + Comp = atom_value(Component, State), + NS = binary_value(Namespace, State), + if Comp /= undefined, NS /= undefined -> + Handlers = maps_append( + {Comp, NS}, + {State#state.file, erl_syntax:get_pos(Form)}, + Del), + State#state{iq_handlers = {Add, Handlers}}; + true -> + State + end. + +check_hooks_arity(Hooks) -> + maps:fold( + fun({Hook, Arity}, _, M) -> + case maps:is_key(Hook, M) of + true -> + err("Error: hook ~s is called with different " + "number of arguments~n", [Hook]); + false -> + maps:put(Hook, Arity, M) + end + end, #{}, Hooks). + +check_iq_handlers_export({HookedFuns, _}, Exports) -> + maps:map( + fun(_, Funs) -> + lists:foreach( + fun({Mod, Fun, {File, FileNo}}) -> + case is_exported(Mod, Fun, 1, Exports) of + true -> ok; + false -> + err("~s:~p: Error: " + "iq handler is registered on unexported function: " + "~s:~s/1~n", [File, FileNo, Mod, Fun]) + end + end, Funs) + end, HookedFuns). + +analyze_iq_handlers({Add, Del}) -> + maps:map( + fun(Handler, Funs) -> + lists:foreach( + fun({_, _, {File, FileNo}}) -> + case maps:is_key(Handler, Del) of + true -> ok; + false -> + err("~s:~p: Error: " + "iq handler is added but not removed~n", + [File, FileNo]) + end + end, Funs) + end, Add), + maps:map( + fun(Handler, Meta) -> + lists:foreach( + fun({File, FileNo}) -> + case maps:is_key(Handler, Add) of + true -> ok; + false -> + err("~s:~p: Error: " + "iq handler is removed but not added~n", + [File, FileNo]) + end + end, Meta) + end, Del). + +analyze_hooks({Add, Del}) -> + Del1 = maps:fold( + fun(Hook, Funs, D) -> + lists:foldl( + fun({Mod, Fun, Seq, {File, FileNo}}, D1) -> + maps:put({Hook, Mod, Fun, Seq}, {File, FileNo}, D1) + end, D, Funs) + end, #{}, Del), + Add1 = maps:fold( + fun(Hook, Funs, D) -> + lists:foldl( + fun({Mod, Fun, Seq, {File, FileNo}}, D1) -> + maps:put({Hook, Mod, Fun, Seq}, {File, FileNo}, D1) + end, D, Funs) + end, #{}, Add), + lists:foreach( + fun({{Hook, Mod, Fun, _} = Key, {File, FileNo}}) -> + case maps:is_key(Key, Del1) of + true -> ok; + false -> + err("~s:~p: Error: " + "hook ~s->~s->~s is added but was never deleted~n", + [File, FileNo, Hook, Mod, Fun]) + end + end, maps:to_list(Add1)), + lists:foreach( + fun({{Hook, Mod, Fun, _} = Key, {File, FileNo}}) -> + case maps:is_key(Key, Add1) of + true -> ok; + false -> + err("~s:~p: Error: " + "hook ~s->~s->~s is deleted but was never added~n", + [File, FileNo, Hook, Mod, Fun]) + end + end, maps:to_list(Del1)). + +build_deps(Hooks, {HookedFuns, _}) -> + maps:fold( + fun({Hook, Arity}, Meta, Deps) -> + case maps:find(Hook, HookedFuns) of + {ok, Funs} -> + ExportedFuns = + lists:map( + fun({M, F, Seq, FunMeta}) -> + {{M, F, Arity}, Seq, FunMeta} + end, Funs), + maps_append_list({Hook, Arity, Meta}, ExportedFuns, Deps); + error -> + maps_append_list({Hook, Arity, Meta}, [], Deps) + end + end, #{}, Hooks). + +module_name(Form, State) -> + try + Name = erl_syntax:macro_name(Form), + 'MODULE' = erl_syntax:variable_name(Name), + State#state.module + catch _:_ -> + atom_value(Form, State) + end. + +atom_value(Form, State) -> + case erl_syntax:type(Form) of + atom -> + erl_syntax:atom_value(Form); + _ -> + warn_type(Form, State, "not an atom"), + undefined + end. + +integer_value(Form, State) -> + case erl_syntax:type(Form) of + integer -> + erl_syntax:integer_value(Form); + _ -> + warn_type(Form, State, "not an integer"), + undefined + end. + +binary_value(Form, State) -> + try erl_syntax:concrete(Form) of + Binary when is_binary(Binary) -> + Binary; + _ -> + warn_type(Form, State, "not a binary"), + undefined + catch _:_ -> + warn_type(Form, State, "not a binary"), + undefined + end. + +is_exported(Mod, Fun, Arity, Exports) -> + try maps:get(Mod, Exports) of + L -> lists:member({Fun, Arity}, L) + catch _:{badkey, _} -> false + end. + +warn_type({var, _, 'Type'}, #state{module = mod_delegation}, "not an atom") -> + ok; +warn_type({var, _, 'NS'}, #state{module = mod_delegation}, "not a binary") -> + ok; +warn_type({var, _, _}, #state{module = gen_mod}, _) -> + ok; +warn_type(Form, State, Warning) -> + log("~s:~p: Warning: " ++ Warning ++ ": ~s~n", + [State#state.file, + erl_syntax:get_pos(Form), + erl_prettypr:format(Form)]). + +emit_module(RunDeps, RunFoldDeps, Module) -> + File = filename:join(["src", Module]) ++ ".erl", + try + {ok, Fd} = file:open(File, [write]), + write(Fd, + "%% Generated automatically~n" + "%% DO NOT EDIT: run `make hooks` instead~n~n", []), + write(Fd, "-module(~s).~n", [Module]), + write(Fd, "-compile(nowarn_unused_vars).~n", []), + write(Fd, "-dialyzer(no_return).~n~n", []), + emit_export(Fd, RunDeps, "run hooks"), + emit_export(Fd, RunFoldDeps, "run_fold hooks"), + emit_run_hooks(Fd, RunDeps), + emit_run_fold_hooks(Fd, RunFoldDeps), + file:close(Fd), + log("Module written to ~s~n", [File]) + catch _:{badmatch, {error, Reason}} -> + err("Error: writing to ~s failed: ~s", [File, file:format_error(Reason)]) + end. + +emit_run_hooks(Fd, Deps) -> + DepsList = lists:sort(maps:to_list(Deps)), + lists:foreach( + fun({{Hook, Arity, {File, LineNo}}, Funs}) -> + write(Fd, "%% called at ~s:~p~n", [File, LineNo]), + Args = string:join( + [[N] || N <- lists:sublist(lists:seq($A, $Z), Arity)], + ", "), + write(Fd, "~s(~s) ->~n ", [Hook, Args]), + Calls = [io_lib:format("_ = ~s:~s(~s)", [Mod, Fun, Args]) + || {{Mod, Fun, _}, _Seq, _} <- lists:keysort(2, Funs)], + write(Fd, "~s.~n~n", + [string:join(Calls ++ ["ok"], ",\n ")]) + end, DepsList). + +emit_run_fold_hooks(Fd, Deps) -> + DepsList = lists:sort(maps:to_list(Deps)), + lists:foreach( + fun({{Hook, Arity, {File, LineNo}}, []}) -> + write(Fd, "%% called at ~s:~p~n", [File, LineNo]), + Args = ["Acc"|lists:duplicate(Arity - 1, "_")], + write(Fd, "~s(~s) -> Acc.~n~n", [Hook, string:join(Args, ", ")]); + ({{Hook, Arity, {File, LineNo}}, Funs}) -> + write(Fd, "%% called at ~s:~p~n", [File, LineNo]), + Args = [[N] || N <- lists:sublist(lists:seq($A, $Z), Arity - 1)], + write(Fd, "~s(~s) ->~n ", [Hook, string:join(["Acc0"|Args], ", ")]), + {Calls, _} = lists:mapfoldl( + fun({{Mod, Fun, _}, _Seq, _}, N) -> + Args1 = ["Acc" ++ integer_to_list(N)|Args], + {io_lib:format("Acc~p = ~s:~s(~s)", + [N+1, Mod, Fun, + string:join(Args1, ", ")]), + N + 1} + end, 0, lists:keysort(2, Funs)), + write(Fd, "~s,~n", [string:join(Calls, ",\n ")]), + write(Fd, " Acc~p.~n~n", [length(Funs)]) + end, DepsList). + +emit_export(Fd, Deps, Comment) -> + DepsList = lists:sort(maps:to_list(Deps)), + Exports = lists:map( + fun({{Hook, Arity, _}, _}) -> + io_lib:format("~s/~p", [Hook, Arity]) + end, DepsList), + write(Fd, "%% ~s~n-export([~s]).~n~n", + [Comment, string:join(Exports, ",\n ")]). + +fold_beams(Fun, State, Paths) -> + Paths1 = fold_paths(Paths), + Total = length(Paths1), + {_, State1} = + lists:foldl( + fun(File, {I, Acc}) -> + io:format("Progress: ~B% (~B/~B)\r", + [round(I*100/Total), I, Total]), + case is_elixir_beam(File) of + true -> {I+1, Acc}; + false -> + {AbsCode, Exports} = get_code_from_beam(File), + Acc2 = lists:foldl( + fun(Form, Acc1) -> + Fun(File, Form, Exports, Acc1) + end, Acc, AbsCode), + {I+1, Acc2} + end + end, {0, State}, Paths1), + State1. + +fold_paths(Paths) -> + lists:flatmap( + fun(Path) -> + case filelib:is_dir(Path) of + true -> + lists:reverse( + filelib:fold_files( + Path, ".+\.beam\$", false, + fun(File, Acc) -> + [File|Acc] + end, [])); + false -> + [Path] + end + end, Paths). + +is_elixir_beam(File) -> + case filename:basename(File) of + "Elixir" ++ _ -> true; + _ -> false + end. + +get_code_from_beam(File) -> + case beam_lib:chunks(File, [abstract_code, exports]) of + {ok, {_, [{abstract_code, {raw_abstract_v1, Forms}}, {exports, X}]}} -> + {Forms, X}; + _ -> + err("No abstract code found in ~s~n", [File]) + end. + +log(Format, Args) -> + io:format(standard_io, Format, Args). + +err(Format, Args) -> + io:format(standard_error, Format, Args), + halt(1). + +write(Fd, Format, Args) -> + file:write(Fd, io_lib:format(Format, Args)). + +maps_append(K, V, M) -> + maps_append_list(K, [V], M). + +maps_append_list(K, L1, M) -> + L2 = maps:get(K, M, []), + maps:put(K, L2 ++ L1, M). diff --git a/tools/make-binaries b/tools/make-binaries new file mode 100755 index 000000000..22ad3e98c --- /dev/null +++ b/tools/make-binaries @@ -0,0 +1,923 @@ +#!/bin/sh + +# Build portable binary release tarballs for Linux/x64 and Linux/arm64. +# +# Author: Holger Weiss <holger@zedat.fu-berlin.de>. +# +# Copyright (c) 2022 ProcessOne, SARL. +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -e +set -u + +export PATH='/usr/local/bin:/usr/bin:/bin' + +myself=${0##*/} + +info() +{ + echo "$myself: $*" +} + +error() +{ + echo >&2 "$myself: $*" +} + +usage() +{ + echo >&2 "Usage: $myself" + exit 2 +} + +mix_version() +{ + # Mix insists on SemVer format. + local vsn="$(printf '%s' "$1" | sed 's/\.0/./')" + + case $vsn in + *.*.*) printf '%s' "$vsn" ;; + *.*) printf '%s.0' "$vsn" ;; + esac +} + +if ! [ -e 'mix.exs' ] || ! [ -e "tools/$myself" ] +then + error "Please call this script from the repository's root directory." + exit 2 +elif [ $# -ne 0 ] +then + usage +fi + +rel_name='ejabberd' +rel_vsn=$(git describe --tags | sed -e 's/-g.*//' -e 's/-/./' | tr -d '[:space:]') +mix_vsn=$(mix_version "$rel_vsn") +crosstool_vsn='1.28.0' +termcap_vsn='1.3.1' +expat_vsn='2.7.2' +zlib_vsn='1.3.1' +yaml_vsn='0.2.5' +ssl_vsn='3.5.3' +otp_vsn='27.3.4.3' +elixir_vsn='1.18.4' +pam_vsn='1.6.1' # Newer Linux-PAM versions use Meson, we don't support that yet. +png_vsn='1.6.45' +jpeg_vsn='9f' +webp_vsn='1.5.0' +gd_vsn='2.3.3' +odbc_vsn='2.3.12' +sqlite_vsn='3490000' +root_dir="${BUILD_DIR:-$HOME/build}" +bootstrap_dir="$root_dir/bootstrap" +ct_prefix_dir="$root_dir/x-tools" +build_dir="$root_dir/$rel_name" +crosstool_dir="crosstool-ng-$crosstool_vsn" +termcap_dir="termcap-$termcap_vsn" +expat_dir="expat-$expat_vsn" +zlib_dir="zlib-$zlib_vsn" +yaml_dir="yaml-$yaml_vsn" +ssl_dir="openssl-$ssl_vsn" +otp_dir="otp_src_$otp_vsn" +elixir_dir="elixir-$elixir_vsn" +pam_dir="Linux-PAM-$pam_vsn" +png_dir="libpng-$png_vsn" +jpeg_dir="jpeg-$jpeg_vsn" +webp_dir="libwebp-$webp_vsn" +gd_dir="libgd-$gd_vsn" +odbc_dir="unixODBC-$odbc_vsn" +sqlite_dir="sqlite-autoconf-$sqlite_vsn" +crosstool_tar="$crosstool_dir.tar.xz" +termcap_tar="$termcap_dir.tar.gz" +expat_tar="$expat_dir.tar.gz" +zlib_tar="$zlib_dir.tar.gz" +yaml_tar="$yaml_dir.tar.gz" +ssl_tar="$ssl_dir.tar.gz" +otp_tar="$otp_dir.tar.gz" +elixir_tar="v$elixir_vsn.tar.gz" +pam_tar="$pam_dir.tar.xz" +png_tar="$png_dir.tar.gz" +jpeg_tar="jpegsrc.v$jpeg_vsn.tar.gz" +webp_tar="$webp_dir.tar.gz" +gd_tar="$gd_dir.tar.gz" +sqlite_tar="$sqlite_dir.tar.gz" +odbc_tar="$odbc_dir.tar.gz" +rel_tar="$rel_name-$mix_vsn.tar.gz" +ct_jobs=$(nproc) +src_dir="$root_dir/src" +platform=$(gcc -dumpmachine) +targets='x86_64-linux-gnu aarch64-linux-gnu' +build_start=$(date '+%F %T') +have_current_deps='false' +dep_vsns_file="$build_dir/.dep_vsns" +dep_vsns='' +deps='crosstool + termcap + expat + zlib + yaml + ssl + otp + elixir + pam + png + jpeg + webp + gd + odbc + sqlite' + +umask 022 + +#' Try to find a browser for checking dependency versions. +have_browser() +{ + for browser in 'lynx' 'links' 'elinks' + do + $browser -dump 'https://ejabberd.im/' >'/dev/null' && return 0 + done + return 1 +} +#. + +#' Check whether the given dependency version is up-to-date. +check_vsn() +{ + local name="$1" + local our_vsn="$2" + local src_url="$3" + local reg_exp="$4" + local cur_vsn=$($browser -dump "$src_url" | + sed -n "s/.*$reg_exp.*/\\1/p" | + head -1) + + if [ "$our_vsn" = "$cur_vsn" ] + then + return 0 + else + error "Current $name version is: $cur_vsn" + error "But our $name version is: $our_vsn" + error "Update $0 or set CHECK_DEPS=false" + exit 1 + fi +} +#. + +#' Check whether our dependency versions are up-to-date. +check_configured_dep_vsns() +{ + check_vsn 'OpenSSL' "$ssl_vsn" \ + 'https://openssl-library.org/source/' \ + 'openssl-\(3\.[1-9]\.[0-9.]*\)\.tar\.gz' + check_vsn 'LibYAML' "$yaml_vsn" \ + 'https://pyyaml.org/wiki/LibYAML' \ + 'yaml-\([0-9][0-9.]*\)\.tar\.gz' + check_vsn 'zlib' "$zlib_vsn" \ + 'https://zlib.net/' \ + 'zlib-\([1-9][0-9.]*\)\.tar\.gz' + check_vsn 'Expat' "$expat_vsn" \ + 'https://github.com/libexpat/libexpat/releases' \ + '\([1-9]\.[0-9]*\.[0-9]*\)' + check_vsn 'Termcap' "$termcap_vsn" \ + 'https://ftp.gnu.org/gnu/termcap/' \ + 'termcap-\([1-9][0-9.]*\)\.tar\.gz' + check_vsn 'SQLite' "$sqlite_vsn" \ + 'https://www.sqlite.org/download.html' \ + 'sqlite-autoconf-\([1-9][0-9]*\)\.tar\.gz' + check_vsn 'ODBC' "$odbc_vsn" \ + 'http://www.unixodbc.org/download.html' \ + 'unixODBC-\([1-9][0-9.]*\)\.tar\.gz' + # + # Linux-PAM uses Meson since version 1.7.0, we don't support that yet. + # + # check_vsn 'Linux-PAM' "$pam_vsn" \ + # 'https://github.com/linux-pam/linux-pam/releases' \ + # '[0-9]\]Linux-PAM \([1-9][0-9.]*\)' + # + check_vsn 'libpng' "$png_vsn" \ + 'http://www.libpng.org/pub/png/libpng.html' \ + 'libpng-\([1-9][0-9.]*\)\.tar\.gz' + check_vsn 'JPEG' "$jpeg_vsn" \ + 'https://www.ijg.org' \ + 'jpegsrc.v\([1-9][0-9a-z]*\)\.tar\.gz' + check_vsn 'WebP' "$webp_vsn" \ + 'https://developers.google.com/speed/webp/download' \ + 'libwebp-\([1-9][0-9.]*\)\.tar\.gz' + check_vsn 'LibGD' "$gd_vsn" \ + 'https://github.com/libgd/libgd/releases' \ + 'gd-\([1-9][0-9.]*\)' + check_vsn 'Elixir' "$elixir_vsn" \ + 'https://elixir-lang.org/install.html' \ + 'v\([1-9][0-9.]*\)\.tar\.gz' +} +#. + +#' Check whether existing dependencies are up-to-date. +check_built_dep_vsns() +{ + for dep in $deps + do + eval dep_vsns=\"\$dep_vsns\$${dep}_vsn\" + done + + if [ -e "$dep_vsns_file" ] + then + if [ "$dep_vsns" = "$(cat "$dep_vsns_file")" ] + then have_current_deps='true' + fi + rm "$dep_vsns_file" + fi +} +#. + +#' Save built dependency versions. +save_built_dep_vsns() +{ + echo "$dep_vsns" >"$dep_vsns_file" +} +#. + +#' Create common part of Crosstool-NG configuration file. +create_common_config() +{ + local file="$1" + + cat >"$file" <<-'EOF' + CT_CONFIG_VERSION="4" + CT_DOWNLOAD_AGENT_CURL=y + CT_OMIT_TARGET_VENDOR=y + CT_CC_LANG_CXX=y + CT_ARCH_64=y + CT_KERNEL_LINUX=y + CT_LINUX_V_3_16=y + CT_LOG_PROGRESS_BAR=n + EOF +} +#. + +#' Create Crosstool-NG configuration file for glibc. +create_gnu_config() +{ + local file="$1" + + create_common_config "$file" + + cat >>"$file" <<-'EOF' + CT_GLIBC_V_2_19=y + EOF +} +#. + +#' Create Crosstool-NG configuration file for musl. +create_musl_config() +{ + local file="$1" + + create_common_config "$file" + + cat >>"$file" <<-'EOF' + CT_EXPERIMENTAL=y + CT_LIBC_MUSL=y + EOF +} +#. + +#' Create Crosstool-NG configuration file for x64. +create_x64_config() +{ + local file="$1" + local libc="$2" + + create_${libc}_config "$file" + + cat >>"$file" <<-'EOF' + CT_ARCH_X86=y + EOF +} +#. + +#' Create Crosstool-NG configuration file for arm64. +create_arm64_config() +{ + local file="$1" + local libc="$2" + + create_${libc}_config "$file" + + cat >>"$file" <<-'EOF' + CT_ARCH_ARM=y + EOF +} +#. + +#' Return our name for the given platform. +arch_name() +{ + local target="$1" + + case $target in + x86_64*) + printf 'x64' + ;; + aarch64*) + printf 'arm64' + ;; + *) + error "Unsupported target platform: $target" + exit 1 + ;; + esac +} +#. + +#' Add native Erlang/OTP "bin" directory to PATH (for bootstrapping and Mix). +add_otp_path() +{ + local mode="$1" + local prefix="$2" + + if [ "$mode" = 'native' ] + then native_otp_bin="$prefix/bin" + elif [ -n "${INSTALL_DIR_FOR_OTP+x}" ] && [ -n "${INSTALL_DIR_FOR_ELIXIR+x}" ] + then + # For github runners to build for non-native systems: + # https://github.com/erlef/setup-beam#environment-variables + native_otp_bin="$INSTALL_DIR_FOR_OTP/bin" + native_elixir_bin="$INSTALL_DIR_FOR_ELIXIR/bin" + export PATH="$native_elixir_bin:$PATH" + fi + export PATH="$native_otp_bin:$PATH" +} +#. + +#' Create and populate /opt/ejabberd directory. +create_data_dir() +{ + local code_dir="$1" + local data_dir="$2" + + mkdir "$data_dir" "$data_dir/database" "$data_dir/logs" + mv "$code_dir/conf" "$data_dir" + chmod 'o-rwx' "$data_dir/"* + curl -fsS -o "$data_dir/conf/cacert.pem" 'https://curl.se/ca/cacert.pem' + sed -i '/^loglevel:/a\ +\ +ca_file: /opt/ejabberd/conf/cacert.pem\ +\ +certfiles:\ + - /opt/ejabberd/conf/server.pem' "$data_dir/conf/$rel_name.yml" +} +#. + +#' Add systemd unit and init script. +add_systemd_unit() +{ + local code_dir="$1" + + sed -e "s|@ctlscriptpath@|/opt/$rel_name-$rel_vsn/bin|g" \ + -e "s|@installuser@|$rel_name|g" 'ejabberd.service.template' \ + >"$code_dir/bin/ejabberd.service" + sed -e "s|@ctlscriptpath@|/opt/$rel_name-$rel_vsn/bin|g" \ + -e "s|@installuser@|$rel_name|g" 'ejabberd.init.template' \ + >"$code_dir/bin/ejabberd.init" + chmod '+x' "$code_dir/bin/ejabberd.init" +} +#. + +#' Add CAPTCHA script(s). +add_captcha_script() +{ + local code_dir="$1" + + cp -p 'tools/captcha'*'.sh' "$code_dir/lib" +} +#. + +#' Use our VT100 to avoid depending on Terminfo, adjust options/paths. +edit_ejabberdctl() +{ + local code_dir="$1" + + sed -i \ + -e "2iexport TERM='internal'" \ + -e '/ERL_OPTIONS=/d' \ + -e 's|_DIR:=".*}/|_DIR:="/opt/ejabberd/|' \ + -e 's|/database|/database/$ERLANG_NODE|' \ + -e 's|#vt100 ||' \ + "$code_dir/bin/${rel_name}ctl" +} +#. + +#' Delete unused files and directories, just to save some space. +remove_unused_files() +{ + local code_dir="$1" + + # Remove shared object file used only in test suite: + find "$code_dir/lib" -name 'otp_test_engine.so' -delete + + # Remove shared object files of statically linked NIFs: + find "$code_dir/lib/crypto-"* "$code_dir/lib/asn1-"* \ + '(' -name 'asn1rt_nif.so' -o \ + -name 'crypto.so' -o \ + -name 'lib' -o \ + -name 'priv' ')' \ + -delete + + # Remove unused ERTS binaries (see systools_make:erts_binary_filter/0): + find "$code_dir/erts-"*'/bin' \ + '(' -name 'ct_run' -o \ + -name 'dialyzer' -o \ + -name 'erlc' -o \ + -name 'typer' -o \ + -name 'yielding_c_fun' ')' \ + -delete + + # Remove unused Mix stuff: + find "$code_dir/bin" -name 'ejabberd' -delete + find "$code_dir/releases" -name 'COOKIE' -delete +} +#. + +#' Strip ERTS binaries, shared objects, and BEAM files. +strip_files() +{ + local code_dir="$1" + local strip_cmd="$2" + + find "$code_dir/lib" \ + -type f \ + -name '*.so' \ + -exec "$strip_cmd" -s '{}' '+' + find "$code_dir/erts-"*'/bin' "$code_dir/lib/"*'/priv/bin' \ + -type f \ + -perm '-u+x' \ + -exec "$strip_cmd" -s '{}' '+' 2>'/dev/null' || : + erl -noinput -eval \ + "{ok, _} = beam_lib:strip_release('$code_dir'), halt()" +} +#. + +#' Build toochain for a given target. +build_toolchain() +{ + local target="$1" + local prefix="$2" + local arch=$(arch_name "$target") + local libc="${target##*-}" + + if [ -d "$prefix" ] + then + info "Using existing toolchain in $prefix ..." + else + if ! [ -x "$bootstrap_dir/bin/ct-ng" ] + then + info "Extracting Crosstool-NG $crosstool_vsn ..." + cd "$src_dir" + tar -xJf "$crosstool_tar" + cd "$OLDPWD" + + info "Building Crosstool-NG $crosstool_vsn ..." + cd "$src_dir/$crosstool_dir" + ./configure --prefix="$bootstrap_dir" + make V=0 + make install + cd "$OLDPWD" + fi + + info "Building toolchain for $arch-$libc ..." + cd "$root_dir" + create_${arch}_config 'defconfig' "$libc" + ct-ng defconfig + ct-ng build CT_PREFIX="$ct_prefix_dir" CT_JOBS="$ct_jobs" + rm -rf '.config' '.build' 'build.log' + cd "$OLDPWD" + fi +} +#. + +#' Build target dependencies. +build_deps() +{ + local mode="$1" + local target="$2" + local prefix="$3" + local arch="$(arch_name "$target")" + local libc="${target##*-}" + local target_src_dir="$prefix/src" + local saved_path="$PATH" + + if [ "$mode" = 'cross' ] + then configure="./configure --host=$target --build=$platform" + else configure='./configure' + fi + + mkdir "$prefix" + + info 'Extracting dependencies ...' + mkdir "$target_src_dir" + cd "$target_src_dir" + tar -xzf "$src_dir/$termcap_tar" + tar -xzf "$src_dir/$sqlite_tar" + tar -xzf "$src_dir/$odbc_tar" + tar -xzf "$src_dir/$expat_tar" + tar -xzf "$src_dir/$zlib_tar" + tar -xzf "$src_dir/$yaml_tar" + tar -xzf "$src_dir/$ssl_tar" + tar -xzf "$src_dir/$otp_tar" + tar -xzf "$src_dir/$elixir_tar" + tar -xzf "$src_dir/$png_tar" + tar -xzf "$src_dir/$jpeg_tar" + tar -xzf "$src_dir/$webp_tar" + tar -xzf "$src_dir/$gd_tar" + tar -xJf "$src_dir/$pam_tar" + cd "$OLDPWD" + + info "Building Termcap $termcap_vsn for $arch-$libc ..." + cd "$target_src_dir/$termcap_dir" + sed -i 's/CFLAGS =/CFLAGS = -Wno-error=implicit-function-declaration/' 'Makefile.in' + $configure --prefix="$prefix" + cat >'config.h' <<-'EOF' + #ifndef CONFIG_H + #define CONFIG_H + #define INTERNAL_TERMINAL "internal:\\\n" \ + "\t:am:bs:ms:xn:xo:\\\n" \ + "\t:co#80:it#8:li#24:vt#3:\\\n" \ + "\t:@8=\\EOM:DO=\\E[%dB:K1=\\EOq:K2=\\EOr:K3=\\EOs:K4=\\EOp:K5=\\EOn:\\\n" \ + "\t:LE=\\E[%dD:RA=\\E[?7l:RI=\\E[%dC:SA=\\E[?7h:UP=\\E[%dA:\\\n" \ + "\t:ac=``aaffggjjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~:\\\n" \ + "\t:ae=^O:as=^N:bl=^G:cb=\\E[1K:cd=\\E[J:ce=\\E[K:cl=\\E[H\\E[J:\\\n" \ + "\t:cm=\\E[%i%d;%dH:cr=^M:cs=\\E[%i%d;%dr:ct=\\E[3g:do=^J:\\\n" \ + "\t:eA=\\E(B\\E)0:ho=\\E[H:k0=\\EOy:k1=\\EOP:k2=\\EOQ:k3=\\EOR:\\\n" \ + "\t:k4=\\EOS:k5=\\EOt:k6=\\EOu:k7=\\EOv:k8=\\EOl:k9=\\EOw:k;=\\EOx:\\\n" \ + "\t:kb=^H:kd=\\EOB:ke=\\E[?1l\\E>:kl=\\EOD:kr=\\EOC:ks=\\E[?1h\\E=:\\\n" \ + "\t:ku=\\EOA:le=^H:mb=\\E[5m:md=\\E[1m:me=\\E[m\\017:mr=\\E[7m:\\\n" \ + "\t:nd=\\E[C:rc=\\E8:rs=\\E>\\E[?3l\\E[?4l\\E[?5l\\E[?7h\\E[?8h:\\\n" \ + "\t:..sa=\\E[0%?%p1%p6%|%t;1%;%?%p2%t;4%;%?%p1%p3%|%t;7%;%?%p4%t;5%;m%?%p9%t\\016%e\\017%;:\\\n" \ + "\t:sc=\\E7:se=\\E[m:sf=^J:so=\\E[7m:sr=\\EM:st=\\EH:ta=^I:ue=\\E[m:\\\n" \ + "\t:up=\\E[A:us=\\E[4m:" + #endif + EOF + make CPPFLAGS="$CPPFLAGS -DHAVE_CONFIG_H=1" + make install + cd "$OLDPWD" + + info "Building zlib $zlib_vsn for $arch-$libc ..." + cd "$target_src_dir/$zlib_dir" + CFLAGS="$CFLAGS -O3 -fPIC" ./configure --prefix="$prefix" --static + make + make install + cd "$OLDPWD" + + info "Building OpenSSL $ssl_vsn for $arch-$libc ..." + cd "$target_src_dir/$ssl_dir" + CFLAGS="$CFLAGS -O3 -fPIC" \ + ./Configure no-shared no-module no-ui-console \ + --prefix="$prefix" \ + --openssldir="$prefix" \ + --libdir='lib' \ + "linux-${target%-linux-*}" + make build_libs + make install_dev + cd "$OLDPWD" + + info "Building Expat $expat_vsn for $arch-$libc ..." + cd "$target_src_dir/$expat_dir" + $configure --prefix="$prefix" --enable-static --disable-shared \ + --without-docbook \ + CFLAGS="$CFLAGS -O3 -fPIC" + make + make install + cd "$OLDPWD" + + info "Building LibYAML $yaml_vsn for $arch-$libc ..." + cd "$target_src_dir/$yaml_dir" + $configure --prefix="$prefix" --enable-static --disable-shared \ + CFLAGS="$CFLAGS -O3 -fPIC" + make + make install + cd "$OLDPWD" + + info "Building SQLite $sqlite_vsn for $arch-$libc ..." + cd "$target_src_dir/$sqlite_dir" + $configure --prefix="$prefix" --enable-static --disable-shared \ + CFLAGS="$CFLAGS -O3 -fPIC" + make + make install + cd "$OLDPWD" + + info "Building ODBC $odbc_vsn for $arch-$libc ..." + cd "$target_src_dir/$odbc_dir" + $configure --prefix="$prefix" --enable-static --disable-shared \ + CFLAGS="$CFLAGS -O3 -fPIC" + make + make install + cd "$OLDPWD" + + info "Building Linux-PAM $pam_vsn for $arch ..." + cd "$target_src_dir/$pam_dir" + $configure --prefix="$prefix" --includedir="$prefix/include/security" \ + --enable-static --disable-shared --disable-logind --disable-doc \ + --disable-examples --enable-db=no \ + CFLAGS="$CFLAGS -O3 -fPIC -Wno-error=implicit-function-declaration" + make + make install + cd "$OLDPWD" + + info "Building libpng $png_vsn for $arch-$libc ..." + cd "$target_src_dir/$png_dir" + $configure --prefix="$prefix" --enable-static --disable-shared \ + CFLAGS="$CFLAGS -O3 -fPIC" + make + make install + cd "$OLDPWD" + + info "Building JPEG $jpeg_vsn for $arch-$libc ..." + cd "$target_src_dir/$jpeg_dir" + $configure --prefix="$prefix" --enable-static --disable-shared \ + CFLAGS="$CFLAGS -O3 -fPIC" + make + make install + cd "$OLDPWD" + + info "Building WebP $webp_vsn for $arch-$libc ..." + cd "$target_src_dir/$webp_dir" + $configure --prefix="$prefix" --enable-static --disable-shared \ + CFLAGS="$CFLAGS -O3 -fPIC" + make + make install + cd "$OLDPWD" + + info "Building LibGD $gd_vsn for $arch-$libc ..." + cd "$target_src_dir/$gd_dir" + $configure --prefix="$prefix" --enable-static --disable-shared \ + --with-zlib="$prefix" \ + --with-webp="$prefix" \ + --with-jpeg="$prefix" \ + --with-png="$prefix" \ + --without-avif \ + --without-fontconfig \ + --without-freetype \ + --without-heif \ + --without-libiconv-prefix \ + --without-liq \ + --without-raqm \ + --without-tiff \ + --without-x \ + --without-xpm \ + CFLAGS="$CFLAGS -O3 -fPIC" + make + make install + cd "$OLDPWD" + + info "Building Erlang/OTP $otp_vsn for $arch-$libc ..." + if [ "$mode" = 'cross' ] + then + add_otp_path "$mode" "$prefix" + export erl_xcomp_sysroot="$prefix" + fi + cd "$target_src_dir/$otp_dir" + # Revert https://github.com/erlang/otp/commit/53ef5df40c733ce3d8215c5c98805f99f378f656 + # because it breaks MSSQL, see https://github.com/processone/ejabberd/issues/4178 + sed -i 's|if(size == 0 && (sql_type == SQL_LONGVARCHAR|if((sql_type == SQL_LONGVARCHAR|g' lib/odbc/c_src/odbcserver.c + # The additional CFLAGS/LIBS below are required by --enable-static-nifs. + # The "-ldl" flag specifically is only needed for ODBC, though. + $configure \ + --prefix="$prefix" \ + --with-ssl="$prefix" \ + --with-odbc="$prefix" \ + --without-javac \ + --disable-dynamic-ssl-lib \ + --enable-static-nifs \ + CFLAGS="$CFLAGS -Wl,-L$prefix/lib" \ + LIBS='-lcrypto -ldl' + make + make install + if [ "$mode" = 'native' ] + then add_otp_path "$mode" "$prefix" + else unset erl_xcomp_sysroot + fi + cd "$OLDPWD" + + info "Building Elixir $elixir_vsn for $arch-$libc ..." + cd "$target_src_dir/$elixir_dir" + make install PREFIX="$prefix" + cd "$OLDPWD" + + export PATH="$saved_path" +} +#. + +#' Build the actual release. +build_rel() +{ + local mode="$1" + local target="$2" + local prefix="$3" + local arch="$(arch_name "$target")" + local libc="${target##*-}" + local rel_dir="$PWD/_build/prod" + local target_data_dir="$prefix/$rel_name" + local target_dst_dir="$prefix/$rel_name-$rel_vsn" + local target_dst_tar="$rel_name-$rel_vsn-linux-$libc-$arch.tar.gz" + local saved_path="$PATH" + + export PATH="$ct_prefix_dir/$target/bin:$PATH" + export CC="$target-gcc" + export CXX="$target-g++" + export CPP="$target-cpp" + export LD="$target-ld" + export AS="$target-as" + export AR="$target-ar" + export NM="$target-nm" + export RANLIB="$target-ranlib" + export OBJCOPY="$target-objcopy" + export STRIP="$target-strip" + export CPPFLAGS="-I$prefix/include" + export CFLAGS="-g0 -O2 -pipe -fomit-frame-pointer -static-libgcc $CPPFLAGS" + export CXXFLAGS="$CFLAGS -static-libstdc++" + export LDFLAGS="-L$prefix/lib -static-libgcc -static-libstdc++" + export ERL_COMPILER_OPTIONS='[no_debug_info]' # Building 25.x fails with 'deterministic'. + + if [ "$mode" = 'cross' ] + then configure="./configure --host=$target --build=$platform" + else configure='./configure' + fi + + if [ $have_current_deps = false ] + then build_deps "$mode" "$target" "$prefix" + fi + + add_otp_path "$mode" "$prefix" + + if [ "$mode" = 'native' ] # In order to only do this once. + then + info "Fetching Mix dependencies" + mix local.hex --force + mix local.rebar --force + fi + + info "Removing old $rel_name builds" + rm -rf '_build' 'deps' + + info "Building $rel_name $rel_vsn for $arch-$libc ..." + ./autogen.sh + eimp_cflags='-fcommon' + eimp_libs='-lwebp -ljpeg -lpng -lz -lm' + export CC="$CC -Wl,-ldl" # Required by (statically linking) epam. + export LIBS="$eimp_libs -lcrypto -lpthread -ldl" + export CFLAGS="$CFLAGS $eimp_cflags" + export LDFLAGS="$LDFLAGS $eimp_libs" + if [ "$mode" = 'cross' ] + then + # Hand over --host/--build to configure scripts of dependencies. + export host_alias="$target" + export build_alias="$platform" + fi + # The cache variable makes cross compilation work. + ac_cv_erlang_root_dir="$prefix/lib/erlang" $configure \ + --with-rebar='mix' \ + --with-sqlite3="$prefix" \ + --enable-user="$rel_name" \ + --enable-all \ + --disable-erlang-version-check + make deps + sed -i 's/ *-lstdc++//g' 'deps/'*'/rebar.config'* # Link statically. + if [ "$mode" = 'cross' ] + then + ln -s "$prefix/lib/erlang" 'lib/erlang' + erts_dir=$(ls -1d 'lib/erlang/erts-'*) + ei_inc="$prefix/lib/erlang/lib/erl_interface-"*'/include' + ei_lib="$prefix/lib/erlang/lib/erl_interface-"*'/lib' + export LDLIBS='-lpthread' + export ERL_EI_INCLUDE_DIR=$(ls -1d $ei_inc) + export ERL_EI_LIBDIR=$(ls -1d $ei_lib) + sed -i "/include_executables/a\\ + include_erts: \"$erts_dir\"," 'mix.exs' + fi + make rel + if [ "$mode" = 'cross' ] + then + sed -i '/include_erts/d' 'mix.exs' + rm 'lib/erlang' + unset LDLIBS ERL_EI_INCLUDE_DIR ERL_EI_LIBDIR + unset host_alias build_alias + fi + + info "Putting together $rel_name $rel_vsn archive for $arch-$libc ..." + mkdir "$target_dst_dir" + tar -C "$target_dst_dir" -xzf "$rel_dir/$rel_tar" + create_data_dir "$target_dst_dir" "$target_data_dir" + add_systemd_unit "$target_dst_dir" + add_captcha_script "$target_dst_dir" + edit_ejabberdctl "$target_dst_dir" + remove_unused_files "$target_dst_dir" + strip_files "$target_dst_dir" "$STRIP" + tar -C "$prefix" --owner="$rel_name" --group="$rel_name" -cf - \ + "$rel_name" "$rel_name-$rel_vsn" | gzip -9 >"$target_dst_tar" + rm -rf "$target_dst_dir" "$target_data_dir" + + info "Created $target_dst_tar successfully." + + unset CC CXX CPP LD AS AR NM RANLIB OBJCOPY STRIP + unset CFLAGS CXXFLAGS LDFLAGS LIBS ERL_COMPILER_OPTIONS + export PATH="$saved_path" +} +#. + +if [ "${CHECK_DEPS:-true}" = 'true' ] +then + if have_browser + then + check_configured_dep_vsns + else + error 'Cannot check dependency versions.' + error 'Install a browser or set CHECK_DEPS=false' + exit 1 + fi +else + info "Won't check dependency versions." +fi + +if ! mkdir -p "$root_dir" +then + error 'Set BUILD_DIR to a usable build directory path.' + exit 1 +fi + +check_built_dep_vsns + +info 'Removing old bootstrap tools ...' +rm -rf "$bootstrap_dir" +mkdir "$bootstrap_dir" + +if [ $have_current_deps = true ] +then + info 'Dependencies are up-to-date ...' +else + # Keep existing toolchains but rebuild everything else. + info 'Removing old builds ...' + rm -rf "$build_dir" + mkdir "$build_dir" + + info 'Removing old source ...' + rm -rf "$src_dir" + mkdir "$src_dir" + + info 'Downloading dependencies ...' + cd "$src_dir" + curl -fsSLO "https://github.com/crosstool-ng/crosstool-ng/releases/download/$crosstool_dir/$crosstool_tar" + curl -fsSLO "https://ftp.gnu.org/gnu/termcap/$termcap_tar" + curl -fsSLO "https://github.com/libexpat/libexpat/releases/download/R_$(printf '%s' "$expat_vsn" | sed 's/\./_/g')/$expat_tar" + curl -fsSLO "https://zlib.net/fossils/$zlib_tar" + curl -fsSLO "https://pyyaml.org/download/libyaml/$yaml_tar" + curl -fsSLO "https://github.com/openssl/openssl/releases/download/openssl-$ssl_vsn/$ssl_tar" + curl -fsSLO "https://github.com/erlang/otp/releases/download/OTP-$otp_vsn/$otp_tar" + curl -fsSLO "https://github.com/elixir-lang/elixir/archive/v$elixir_vsn.tar.gz" + curl -fsSLO "https://github.com/linux-pam/linux-pam/releases/download/v$pam_vsn/$pam_tar" + curl -fsSLO "https://download.sourceforge.net/libpng/$png_tar" + curl -fsSLO "https://www.ijg.org/files/$jpeg_tar" + curl -fsSLO "https://storage.googleapis.com/downloads.webmproject.org/releases/webp/$webp_tar" + curl -fsSLO "https://github.com/libgd/libgd/releases/download/gd-$gd_vsn/$gd_tar" + curl -fsSLO "http://www.unixodbc.org/$odbc_tar" + curl -fsSLO "https://www.sqlite.org/$(date '+%Y')/$sqlite_tar" \ + || curl -fsSLO "https://www.sqlite.org/$(date -d '1 year ago' '+%Y')/$sqlite_tar" \ + || curl -fsSLO "https://www.sqlite.org/$(date -d '2 years ago' '+%Y')/$sqlite_tar" + cd "$OLDPWD" +fi + +mkdir "$bootstrap_dir/bin" +export PATH="$bootstrap_dir/bin:$PATH" # For ct-ng. +export LC_ALL='C.UTF-8' # Elixir insists on a UTF-8 environment. + +for target in $targets +do + prefix="$build_dir/$target" + toolchain_dir="$ct_prefix_dir/$target" + + if [ "$platform" = "$target" ] + then mode='native' + else mode='cross' + fi + build_toolchain "$target" "$toolchain_dir" + build_rel "$mode" "$target" "$prefix" +done + +save_built_dep_vsns + +info "Build started: $build_start" +info "Build ended: $(date '+%F %T')" + +# vim:set foldmarker=#',#. foldmethod=marker: diff --git a/tools/make-installers b/tools/make-installers new file mode 100755 index 000000000..9439fc597 --- /dev/null +++ b/tools/make-installers @@ -0,0 +1,366 @@ +#!/bin/sh + +# Build installers for Linux/x64 and Linux/arm64. +# +# Author: Holger Weiss <holger@zedat.fu-berlin.de>. +# +# Copyright (c) 2022 ProcessOne, SARL. +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -e +set -u + +myself=${0##*/} +architectures='x64 arm64' +iteration=1 + +usage() +{ + echo >&2 "Usage: $myself [-i <iteration>]" + exit 2 +} + +while getopts i: opt +do + case $opt in + i) + iteration="$OPTARG" + ;; + \?) + usage + ;; + esac +done +shift $((OPTIND - 1)) + +if ! [ -e 'mix.exs' ] || ! [ -e "tools/$myself" ] +then + echo >&2 "Please call this script from the repository's root directory." + exit 2 +elif [ $# -ne 0 ] +then + usage +fi +if type 'makeself' >'/dev/null' +then makeself='makeself' +elif type 'makeself.sh' >'/dev/null' +then makeself='makeself.sh' +else + echo >&2 'This script requires makeself: https://makeself.io' + exit 1 +fi + +rel_name='ejabberd' +rel_vsn=$(git describe --tags | sed -e 's/-g.*//' -e 's/-/./' | tr -d '[:space:]') +home_url='https://www.ejabberd.im' +doc_url='https://docs.ejabberd.im' +upgrade_url="$doc_url/admin/upgrade/#specific-version-upgrade-notes" +admin_url="$doc_url/admin/install/next-steps/#administration-account" +default_code_dir="/opt/$rel_name-$rel_vsn" +default_data_dir="/opt/$rel_name" +tmp_dir=$(mktemp -d "/tmp/.$rel_name.XXXXXX") + +trap 'rm -rf "$tmp_dir"' INT TERM EXIT +umask 022 + +create_help_file() +{ + local file="$1" + + cat >"$file" <<-EOF + This is the $rel_name $rel_vsn-$iteration installer for linux-gnu-$arch + + Visit: + $home_url + + ejabberd documentation site: + $doc_url + + EOF +} + +create_setup_script() +{ + local dir="$1" + + cat >"$dir/setup" <<-EOF + #!/bin/sh + + set -e + set -u + + export PATH='/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin' + + user_agrees() + { + local question="\$*" + + if [ -t 0 ] + then + read -p "\$question (y/n) [n] " response + case "\$response" in + [Yy]|[Yy][Ee][Ss]) + return 0 + ;; + [Nn]|[Nn][Oo]|'') + return 1 + ;; + *) + echo 'Please respond with "yes" or "no".' + user_agrees "\$question" + ;; + esac + else # Assume 'yes' if not running interactively. + return 0 + fi + } + + if [ \$(id -u) = 0 ] + then + is_superuser=true + else + is_superuser=false + echo "Running without superuser privileges (installer wasn't invoked" + echo 'with "sudo"), cannot perform system-wide installation this way.' + if ! user_agrees 'Continue anyway?' + then + echo 'Aborting installation.' + exit 1 + fi + fi + + if [ \$is_superuser = true ] + then + code_dir='$default_code_dir' + data_dir='$default_data_dir' + user_name='$rel_name' + group_name='$rel_name' + elif user_agrees "Install $rel_name below \$HOME/opt?" + then + code_dir="\$HOME/opt/$rel_name-$rel_vsn" + data_dir="\$HOME/opt/$rel_name" + user_name="\$(id -u -n)" + group_name="\$(id -g -n)" + else + read -p 'Installation prefix: ' prefix + if printf '%s' "\$prefix" | grep -q '^/' + then + code_dir="\$prefix/$rel_name-$rel_vsn" + data_dir="\$prefix/$rel_name" + user_name="\$(id -u -n)" + group_name="\$(id -g -n)" + else + echo >&2 'Prefix must be specified as an absolute path.' + echo >&2 'Aborting installation.' + exit 1 + fi + fi + + prefix="\$(dirname "\$code_dir")" + conf_dir="\$data_dir/conf" + pem_file="\$conf_dir/server.pem" + uninstall_file="\$code_dir/uninstall.txt" + + if [ -e '/run/systemd/system' ] + then is_systemd=true + else is_systemd=false + fi + if [ -e "\$data_dir" ] + then is_upgrade=true + else is_upgrade=false + fi + if id -u "\$user_name" >'/dev/null' 2>&1 + then user_exists=true + else user_exists=false + fi + + echo + echo 'The following installation paths will be used:' + echo "- \$code_dir" + if [ \$is_upgrade = true ] + then echo "- \$data_dir (existing files won't be modified)" + else echo "- \$data_dir (for configuration, database, and log files)" + fi + if [ \$is_superuser = true ] + then + if [ \$is_systemd = true ] + then + echo '- /etc/systemd/system/$rel_name.service' + if [ \$is_upgrade = false ] + then echo 'The $rel_name service is going to be enabled and started.' + fi + fi + if [ \$user_exists = false ] + then echo 'The $rel_name user is going to be created.' + fi + fi + if ! user_agrees 'Install $rel_name $rel_vsn now?' + then + echo 'Aborting installation.' + exit 1 + fi + echo + + if [ \$user_exists = false ] && [ \$is_superuser = true ] + then useradd -r -d "\$data_dir" "\$user_name" + fi + + host=\$(hostname --fqdn 2>'/dev/null' || :) + if [ -z "\$host" ] + then host='localhost' + fi + + mkdir -p "\$prefix" + tar -cf - '$rel_name' | tar --skip-old-files -C "\$prefix" -xf - + tar -cf - '$rel_name-$rel_vsn' | tar -C "\$prefix" -xf - + + if [ \$is_superuser = true ] + then + if [ \$is_upgrade = false ] + then chown -R -h "\$user_name:\$group_name" "\$data_dir" + fi + chown -R -h "\$(id -u -n):\$group_name" "\$code_dir" + chmod -R g+rX "\$code_dir" + chmod '4750' "\$code_dir/lib/epam-"*'/priv/bin/epam' + else + sed -i "s/^INSTALLUSER=.*/INSTALLUSER=\"\$user_name\"/" \ + "\$code_dir/bin/${rel_name}ctl" + sed -i "s/^USER=.*/USER=\$user_name/" \ + "\$code_dir/bin/$rel_name.init" + sed -i \ + -e "s/^User=.*/User=\$user_name/" \ + -e "s/^Group=.*/Group=\$group_name/" \ + "\$code_dir/bin/$rel_name.service" + fi + if [ "\$code_dir" != '$default_code_dir' ] + then + sed -i "s|$default_code_dir|\$code_dir|g" \ + "\$code_dir/bin/$rel_name.init" \ + "\$code_dir/bin/$rel_name.service" + fi + if [ "\$data_dir" != '$default_data_dir' ] + then + sed -i "s|$default_data_dir|\$data_dir|g" \ + "\$code_dir/bin/${rel_name}ctl" \ + "\$data_dir/conf/$rel_name.yml" \ + "\$data_dir/conf/${rel_name}ctl.cfg" + fi + + if [ \$is_upgrade = false ] + then + sed -i "s/ - localhost$/ - \$host/" "\$conf_dir/$rel_name.yml" + openssl req -x509 \ + -batch \ + -nodes \ + -newkey rsa:4096 \ + -keyout "\$pem_file" \ + -out "\$pem_file" \ + -days 3650 \ + -subj "/CN=\$host" >'/dev/null' 2>&1 || : + if ! [ -e "\$pem_file" ] + then + echo 'Failed to create a TLS certificate for $rel_name.' >&2 + elif [ \$is_superuser = true ] + then + chown "\$user_name:\$group_name" "\$pem_file" + fi + fi + + case \$is_systemd,\$is_superuser in + true,true) + cp "\$code_dir/bin/$rel_name.service" '/etc/systemd/system/' + systemctl -q daemon-reload + if [ \$is_upgrade = false ] + then systemctl -q --now enable '$rel_name' + fi + ;; + true,false) + echo 'You might want to install a systemd unit (see the' + echo "\$code_dir/bin directory for an example)." + ;; + false,*) + echo 'You might want to install an init script (see the' + echo "\$code_dir/bin directory for an example)." + ;; + esac + + echo + echo '$rel_name $rel_vsn has been installed successfully.' + echo + + cat >"\$uninstall_file" <<-_EOF + # To uninstall $rel_name, first remove the service. If you're using systemd: + systemctl --now disable $rel_name + rm -f /etc/systemd/system/$rel_name.service + + # Remove the binary files: + rm -rf \$code_dir + + # If you want to remove your configuration, database and logs: + rm -rf \$data_dir + _EOF + if [ \$is_superuser = true ] + then + cat >>"\$uninstall_file" <<-_EOF + + # To remove the user running $rel_name: + userdel \$user_name + _EOF + fi + + if [ \$is_upgrade = false ] + then + if [ \$is_systemd = true ] && [ \$is_superuser = true ] + then + echo 'Now you can check $rel_name is running correctly:' + echo ' systemctl status $rel_name' + echo + fi + echo 'Next you may want to edit $rel_name.yml to set up hosts,' + echo 'register an account and grant it admin rigts, see:' + echo + echo '$admin_url' + else + echo 'Please check the following web site for upgrade notes:' + echo + echo '$upgrade_url' + echo + if [ \$is_systemd = true ] && [ \$is_superuser = true ] + then + echo 'If everything looks fine, restart the $rel_name service:' + echo ' systemctl restart $rel_name' + else + echo 'If everything looks fine, restart the $rel_name service.' + fi + fi + EOF + chmod +x "$dir/setup" +} + +for arch in $architectures +do + tar_name="$rel_name-$rel_vsn-linux-gnu-$arch.tar.gz" + installer_name="$rel_name-$rel_vsn-$iteration-linux-$arch.run" + + test -e "$tar_name" || tools/make-binaries + echo "$myself: Putting together installer for $arch ..." + tar -C "$tmp_dir" -xzpf "$tar_name" + create_help_file "$tmp_dir/help.txt" + create_setup_script "$tmp_dir" + "$makeself" --help-header "$tmp_dir/help.txt" \ + "$tmp_dir" "$installer_name" "$rel_name $rel_vsn" './setup' + find "$tmp_dir" -mindepth 1 -delete +done +echo "$myself: Created installers successfully." diff --git a/tools/make-packages b/tools/make-packages new file mode 100755 index 000000000..8e3585f48 --- /dev/null +++ b/tools/make-packages @@ -0,0 +1,240 @@ +#!/bin/sh + +# Build DEB and RPM packages for Linux/x64 and Linux/arm64. +# +# Author: Holger Weiss <holger@zedat.fu-berlin.de>. +# +# Copyright (c) 2022 ProcessOne, SARL. +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -e +set -u + +myself=${0##*/} +architectures='x64 arm64' +iteration=1 + +usage() +{ + echo >&2 "Usage: $myself [-i <iteration>]" + exit 2 +} + +while getopts i: opt +do + case $opt in + i) + iteration="$OPTARG" + ;; + \?) + usage + ;; + esac +done +shift $((OPTIND - 1)) + +if ! [ -e 'mix.exs' ] || ! [ -e "tools/$myself" ] +then + echo >&2 "Please call this script from the repository's root directory." + exit 2 +elif [ $# -ne 0 ] +then + usage +fi +if ! type fpm >'/dev/null' +then + echo >&2 'This script requires fpm: https://fpm.readthedocs.io' + exit 1 +fi + +rel_name='ejabberd' +rel_vsn=$(git describe --tags | sed -e 's/-g.*//' -e 's/-/./' | tr -d '[:space:]') +conf_dir="/opt/$rel_name/conf" +pem_file="$conf_dir/server.pem" +tmp_dir=$(mktemp -d "/tmp/.$myself.XXXXXX") + +trap 'rm -rf "$tmp_dir"' INT TERM EXIT +umask 022 + +create_scripts() +{ + local dir="$1" + + cat >"$dir/before-install" <<-EOF + if ! getent group '$rel_name' >'/dev/null' + then groupadd -r '$rel_name' + fi + if ! getent passwd '$rel_name' >'/dev/null' + then useradd -r -m -d '/opt/$rel_name' -g '$rel_name' '$rel_name' + fi + if ! [ -e '$pem_file' ] + then + if ! [ -e '/opt/$rel_name' ] # Huh? + then install -o '$rel_name' -g '$rel_name' -m 750 -d '/opt/$rel_name' + fi + if ! [ -e '$conf_dir' ] + then install -o '$rel_name' -g '$rel_name' -m 750 -d '$conf_dir' + fi + host=\$(hostname --fqdn 2>'/dev/null' || :) + if [ -z "\$host" ] + then host='localhost' + fi + openssl req -x509 \ + -batch \ + -nodes \ + -newkey rsa:4096 \ + -keyout '$pem_file' \ + -out '$pem_file' \ + -days 3650 \ + -subj "/CN=\$host" >'/dev/null' 2>&1 || : + if [ -e '$pem_file' ] + then chown '$rel_name:$rel_name' '$pem_file' + else echo 'Failed to create a TLS certificate for ejabberd.' >&2 + fi + fi + if ! [ -e '/opt/$rel_name/database' ] + then install -o '$rel_name' -g '$rel_name' -m 750 -d '/opt/$rel_name/database' + fi + if ! [ -e '/opt/$rel_name/logs' ] + then install -o '$rel_name' -g '$rel_name' -m 750 -d '/opt/$rel_name/logs' + fi + EOF + + cat >"$dir/after-install" <<-EOF + host=\$(hostname --fqdn 2>'/dev/null' || :) + if [ -n "\$host" ] + then sed -i "s/ - localhost$/ - \$host/" '$conf_dir/$rel_name.yml' + fi + chown 'root:$rel_name' '/opt/$rel_name-$rel_vsn/lib/epam-'*'/priv/bin/epam' + chmod '4750' '/opt/$rel_name-$rel_vsn/lib/epam-'*'/priv/bin/epam' + chown -R -h '$rel_name:$rel_name' '/opt/$rel_name' + chmod 'o-rwx' '/opt/$rel_name/'* + EOF + + cat >"$dir/after-upgrade" <<-EOF + chown 'root:$rel_name' '/opt/$rel_name-$rel_vsn/lib/epam-'*'/priv/bin/epam' + chmod '4750' '/opt/$rel_name-$rel_vsn/lib/epam-'*'/priv/bin/epam' + EOF + + cat >"$dir/after-remove" <<-EOF + rm -f '/opt/$rel_name/.erlang.cookie' + if getent passwd '$rel_name' >'/dev/null' + then userdel '$rel_name' + fi + if getent group '$rel_name' >'/dev/null' + then groupdel '$rel_name' + fi + EOF +} + +package_architecture() +{ + local target="$1" + local host_target="$(uname -m)-$target" + + case $host_target in + x86_64-x64) + printf 'native' + ;; + x86_64-arm64) + printf 'arm64' + ;; + *) + echo >&2 "Unsupported host/target combination: $host_target" + exit 1 + ;; + esac +} + +make_package() +{ + local output_type="$1" + local architecture="$(package_architecture "$2")" + local work_dir="$3" + local include_dirs="$4" + + cd "$work_dir" # FPM's "--chdir" option doesn't work (as I'd expect). + fpm --output-type "$output_type" \ + --input-type 'dir' \ + --name "$rel_name" \ + --version "$rel_vsn" \ + --iteration "$iteration" \ + --license 'GPL-2+' \ + --category 'net' \ + --provides 'stun-server' \ + --provides 'turn-server' \ + --provides 'xmpp-server' \ + --no-depends \ + --no-auto-depends \ + --deb-maintainerscripts-force-errorchecks \ + --deb-systemd-enable \ + --deb-systemd-auto-start \ + --deb-systemd "./$rel_name.service" \ + --deb-init "./$rel_name" \ + --rpm-init "./$rel_name" \ + --config-files "$conf_dir" \ + --directories "/opt/$rel_name" \ + --directories "/opt/$rel_name-$rel_vsn" \ + --architecture "$architecture" \ + --maintainer 'ejabberd Maintainers <ejabberd@process-one.net>' \ + --vendor 'ProcessOne, SARL' \ + --description 'Robust and scalable XMPP/MQTT/SIP server.' \ + --url 'https://ejabberd.im' \ + --before-install './before-install' \ + --after-install './after-install' \ + --before-upgrade './before-install' \ + --after-upgrade './after-upgrade' \ + --after-remove './after-remove' \ + $include_dirs + cd "$OLDPWD" +} + +for arch in $architectures +do + tar_name="$rel_name-$rel_vsn-linux-gnu-$arch.tar.gz" + arch_dir="$tmp_dir/$arch" + opt_dir="$arch_dir/opt" + etc_dir="$arch_dir/etc" + bin_dir="$arch_dir/usr/sbin" + dst_dir="$opt_dir/$rel_name-$rel_vsn" + + test -e "$tar_name" || tools/make-binaries + + echo "$myself: Putting together DEB and RPM packages for $arch ..." + + mkdir -p "$opt_dir" "$bin_dir" + tar -C "$opt_dir" -xzf "$tar_name" + + cat >"$bin_dir/${rel_name}ctl" <<-EOF + #!/bin/sh + exec '/opt/$rel_name-$rel_vsn/bin/${rel_name}ctl' "\$@" + EOF + chmod +x "$bin_dir/${rel_name}ctl" + + mkdir -p "$etc_dir/systemd/system" + mv "$dst_dir/bin/$rel_name.service" "$etc_dir/systemd/system" + mv "$dst_dir/bin/$rel_name.init" "$arch_dir/$rel_name" + sed -i \ + "s|opt/$rel_name-$rel_vsn/bin/${rel_name}ctl|usr/sbin/${rel_name}ctl|g" \ + "$etc_dir/systemd/system/$rel_name.service" "$arch_dir/$rel_name" + + create_scripts "$arch_dir" + make_package 'rpm' "$arch" "$arch_dir" './opt ./usr ./etc' + mv "$etc_dir/systemd/system/$rel_name.service" "$arch_dir" + rm -r "$etc_dir" + make_package 'deb' "$arch" "$arch_dir" './opt ./usr' + mv "$arch_dir/$rel_name"?$rel_vsn*.??? . +done +echo "$myself: Created DEB and RPM packages successfully." diff --git a/tools/opt_types.sh b/tools/opt_types.sh new file mode 100755 index 000000000..bf8f99da6 --- /dev/null +++ b/tools/opt_types.sh @@ -0,0 +1,636 @@ +#!/usr/bin/env escript +%% -*- erlang -*- + +-compile([nowarn_unused_function]). +-record(state, {g_opts = #{} :: map(), + m_opts = #{} :: map(), + globals = [] :: [atom()], + defaults = #{} :: map(), + mod_defaults = #{} :: map(), + specs = #{} :: map(), + mod_specs = #{} :: map()}). + +main([Mod|Paths]) -> + put(otp_version, list_to_integer(erlang:system_info(otp_release))), + State = fold_beams( + fun(File, Form, StateAcc) -> + append(Form, File, StateAcc) + end, #state{}, Paths), + emit_modules(map_to_specs(State#state.m_opts, + State#state.mod_defaults, + State#state.mod_specs)), + emit_config(Mod, + map_to_specs(State#state.g_opts, + State#state.defaults, + State#state.specs), + State#state.globals). + +emit_config(Mod, Specs, Globals) -> + File = filename:join("src", Mod ++ ".erl"), + case file:open(File, [write]) of + {ok, Fd} -> + emit_header(Fd, Mod, Specs, Globals), + emit_funs(Fd, Mod, Specs, Globals); + {error, Reason} -> + err("Failed to open file ~s for writing: ~s", + [File, file:format_error(Reason)]) + end. + +emit_modules(Specs) -> + M = lists:foldl( + fun({{Mod, Opt}, Spec}, Acc) -> + Opts = maps:get(Mod, Acc, []), + Opts1 = [{Opt, Spec}|Opts], + maps:put(Mod, Opts1, Acc) + end, #{}, Specs), + maps:fold( + fun(Mod, OptSpecs, _) -> + ModS = atom_to_list(Mod) ++ "_opt", + File = filename:join("src", ModS ++ ".erl"), + case file:open(File, [write]) of + {ok, Fd} -> + OptSpecs1 = lists:reverse(OptSpecs), + emit_header(Fd, ModS, OptSpecs1), + emit_funs(Fd, Mod, OptSpecs1); + {error, Reason} -> + err("Failed to open file ~s for writing: ~s", + [File, file:format_error(Reason)]) + end + end, ok, M). + +emit_header(Fd, Mod, Specs, Globals) -> + log(Fd, comment(), []), + log(Fd, "-module(~s).~n", [Mod]), + lists:foreach( + fun({{_, Opt}, _}) -> + case lists:member(Opt, Globals) of + true -> + log(Fd, "-export([~s/0]).", [Opt]); + false -> + log(Fd, "-export([~s/0, ~s/1]).", [Opt, Opt]) + end + end, Specs), + log(Fd, "", []). + +emit_header(Fd, Mod, Specs) -> + log(Fd, comment(), []), + log(Fd, "-module(~s).~n", [Mod]), + lists:foreach( + fun({Opt, _}) -> + log(Fd, "-export([~s/1]).", [Opt]) + end, Specs), + log(Fd, "", []). + +emit_funs(Fd, _Mod, Specs, Globals) -> + lists:foreach( + fun({{_, Opt}, Type}) -> + SType = t_to_string(Type), + case lists:member(Opt, Globals) of + true -> + log(Fd, + "-spec ~s() -> ~s.~n" + "~s() ->~n" + " ejabberd_config:get_option({~s, global}).~n", + [Opt, SType, Opt, Opt]); + false -> + log(Fd, + "-spec ~s() -> ~s.~n" + "~s() ->~n" + " ~s(global).~n" + "-spec ~s(global | binary()) -> ~s.~n" + "~s(Host) ->~n" + " ejabberd_config:get_option({~s, Host}).~n", + [Opt, SType, Opt, Opt, Opt, SType, Opt, Opt]) + end + end, Specs). + +emit_funs(Fd, Mod, Specs) -> + lists:foreach( + fun({Opt, Type}) -> + Mod2 = strip_db_type(Mod), + log(Fd, + "-spec ~s(gen_mod:opts() | global | binary()) -> ~s.~n" + "~s(Opts) when is_map(Opts) ->~n" + " gen_mod:get_opt(~s, Opts);~n" + "~s(Host) ->~n" + " gen_mod:get_module_opt(Host, ~s, ~s).~n", + [Opt, t_to_string(Type), Opt, Opt, Opt, Mod2, Opt]) + end, Specs). + +strip_db_type(mod_vcard_ldap) -> + mod_vcard; +strip_db_type(mod_vcard_mnesia) -> + mod_vcard; +strip_db_type(Mod) -> + Mod. + +append({globals, Form}, _File, State) -> + [Clause] = erl_syntax:function_clauses(Form), + Body = lists:last(erl_syntax:clause_body(Clause)), + Gs = lists:map(fun erl_syntax:atom_value/1, + erl_syntax:list_elements(Body)), + Globals = State#state.globals ++ Gs, + State#state{globals = Globals}; +append({Index, Form}, File, State) when Index == #state.defaults; + Index == #state.mod_defaults -> + Mod = module(File), + [Clause] = erl_syntax:function_clauses(Form), + Body = lists:last(erl_syntax:clause_body(Clause)), + case erl_syntax:is_proper_list(Body) of + true -> + Opts = lists:foldl( + fun(E, M) -> + try + [E1, E2|_] = erl_syntax:tuple_elements(E), + Name = erl_syntax:atom_value(E1), + Val = erl_syntax:concrete(E2), + maps:put({Mod, Name}, Val, M) + catch _:_ -> + M + end + end, element(Index, State), erl_syntax:list_elements(Body)), + setelement(Index, State, Opts); + false -> + warn("~s: improper list", [format_file(File, Body)]), + State + end; +append({Index, Form}, File, State) when Index == #state.specs; + Index == #state.mod_specs -> + Specs = element(Index, State), + Mod = module(File), + try + {type, _, 'fun', Form1} = Form, + {type, _, list, Form2} = lists:last(Form1), + Tuples = case Form2 of + [{type, _, union, Form3}] -> Form3; + _ -> Form2 + end, + Specs1 = lists:foldl( + fun({type, _, tuple, [{atom, _, Atom}, Form5]}, Acc) -> + maps:put({Mod, Atom}, Form5, Acc); + (_, Acc) -> + Acc + end, Specs, Tuples), + setelement(Index, State, Specs1) + catch _:_ -> + warn("~s: unsupported type spec", [format_file(File, Form)]), + State + end; +append({Type, Form}, File, State) when Type == opt_type; Type == mod_opt_type -> + Clauses = erl_syntax:function_clauses(Form), + Mod = module(File), + lists:foldl( + fun(Clause, StateAcc) -> + [Arg] = erl_syntax:clause_patterns(Clause), + Body = lists:last(erl_syntax:clause_body(Clause)), + case erl_syntax:type(Arg) of + atom -> + Name = erl_syntax:atom_value(Arg), + case Type of + opt_type -> + GOpts = StateAcc#state.g_opts, + State#state{ + g_opts = append_body({Mod, Name}, Body, GOpts)}; + mod_opt_type -> + MOpts = StateAcc#state.m_opts, + State#state{ + m_opts = append_body({Mod, Name}, Body, MOpts)} + end; + T -> + warn("~s: unexpected option name: ~s", + [format_file(File, Arg), T]), + StateAcc + end + end, State, Clauses). + +append_body(Name, Body, Map) -> + maps:put(Name, Body, Map). + +map_to_specs(Map, Defaults, Specs) -> + lists:keysort( + 1, maps:fold( + fun({Mod, Opt} = Key, Val, Acc) -> + S1 = type_with_default(Key, Val, Defaults), + S2 = case t_is_any(S1) of + true -> + try maps:get(Key, Specs) + catch _:{badkey, _} -> + warn("Cannot derive type for ~s->~s", [Mod, Opt]), + S1 + end; + false -> + S1 + end, + [{Key, S2}|Acc] + end, [], Map)). + +type_with_default({Mod, _} = Key, Val, Defaults) -> + S = try spec(Mod, Val) + catch throw:unknown -> erl_types:t_any() + end, + case t_is_any(S) of + true -> + S; + false -> + try maps:get(Key, Defaults) of + T -> + erl_types:t_sup( + [S, erl_types:t_from_term(T)]) + catch _:{badkey, _} -> + S + end + end. + +spec(Mod, Form) -> + case erl_syntax:type(Form) of + application -> + case erl_syntax_lib:analyze_application(Form) of + {M, {Fun, Arity}} when M == econf; + M == yconf -> + Args = erl_syntax:application_arguments(Form), + spec(Fun, Arity, Args, Mod); + _ -> + t_unknown(Mod) + end; + _ -> + t_unknown(Mod) + end. + +spec(pos_int, 0, _, _) -> + erl_types:t_pos_integer(); +spec(pos_int, 1, [Inf], _) -> + erl_types:t_sup( + erl_types:t_pos_integer(), + erl_types:t_atom(erl_syntax:atom_value(Inf))); +spec(non_neg_int, 0, _, _) -> + erl_types:t_non_neg_integer(); +spec(non_neg_int, 1, [Inf], _) -> + erl_types:t_sup( + erl_types:t_non_neg_integer(), + erl_types:t_atom(erl_syntax:atom_value(Inf))); +spec(int, 0, _, _) -> + erl_types:t_integer(); +spec(int, 2, [Min, Max], _) -> + erl_types:t_from_range( + erl_syntax:integer_value(Min), + erl_syntax:integer_value(Max)); +spec(number, 1, _, _) -> + erl_types:t_number(); +spec(octal, 0, _, _) -> + erl_types:t_non_neg_integer(); +spec(binary, A, _, _) when A == 0; A == 1; A == 2 -> + erl_types:t_binary(); +spec(enum, 1, [L], _) -> + try + Els = erl_syntax:list_elements(L), + Atoms = lists:map( + fun(A) -> + erl_types:t_atom( + erl_syntax:atom_value(A)) + end, Els), + erl_types:t_sup(Atoms) + catch _:_ -> + erl_types:t_binary() + end; +spec(bool, 0, _, _) -> + erl_types:t_boolean(); +spec(atom, 0, _, _) -> + erl_types:t_atom(); +spec(string, A, _, _) when A == 0; A == 1; A == 2 -> + erl_types:t_string(); +spec(any, 0, _, Mod) -> + t_unknown(Mod); +spec(url, A, _, _) when A == 0; A == 1 -> + erl_types:t_binary(); +spec(file, A, _, _) when A == 0; A == 1 -> + erl_types:t_binary(); +spec(directory, A, _, _) when A == 0; A == 1 -> + erl_types:t_binary(); +spec(ip, 0, _, _) -> + t_remote(inet, ip_address); +spec(ipv4, 0, _, _) -> + t_remote(inet, ip4_address); +spec(ipv6, 0, _, _) -> + t_remote(inet, ip6_address); +spec(ip_mask, 0, _, _) -> + erl_types:t_sup( + erl_types:t_tuple( + [t_remote(inet, ip4_address), erl_types:t_from_range(0, 32)]), + erl_types:t_tuple( + [t_remote(inet, ip6_address), erl_types:t_from_range(0, 128)])); +spec(port, 0, _, _) -> + erl_types:t_from_range(1, 65535); +spec(re, A, _, _) when A == 0; A == 1 -> + t_remote(misc, re_mp); +spec(glob, A, _, _) when A == 0; A == 1 -> + t_remote(misc, re_mp); +spec(path, 0, _, _) -> + erl_types:t_binary(); +spec(binary_sep, 1, _, _) -> + erl_types:t_list(erl_types:t_binary()); +spec(beam, A, _, _) when A == 0; A == 1 -> + erl_types:t_module(); +spec(timeout, 1, _, _) -> + erl_types:t_pos_integer(); +spec(timeout, 2, [_, Inf], _) -> + erl_types:t_sup( + erl_types:t_pos_integer(), + erl_types:t_atom(erl_syntax:atom_value(Inf))); +spec(non_empty, 1, [Form], Mod) -> + S = spec(Mod, Form), + case erl_types:t_is_list(S) of + true -> + erl_types:t_nonempty_list( + erl_types:t_list_elements(S)); + false -> + S + end; +spec(unique, 1, [Form], Mod) -> + spec(Mod, Form); +spec(acl, 0, _, _) -> + t_remote(acl, acl); +spec(shaper, 0, _, _) -> + erl_types:t_sup( + [erl_types:t_atom(), + erl_types:t_list(t_remote(ejabberd_shaper, shaper_rule))]); +spec(url_or_file, 0, _, _) -> + erl_types:t_tuple( + [erl_types:t_sup([erl_types:t_atom(file), + erl_types:t_atom(url)]), + erl_types:t_binary()]); +spec(lang, 0, _, _) -> + erl_types:t_binary(); +spec(pem, 0, _, _) -> + erl_types:t_binary(); +spec(jid, 0, _, _) -> + t_remote(jid, jid); +spec(domain, 0, _, _) -> + erl_types:t_binary(); +spec(db_type, 1, _, _) -> + erl_types:t_atom(); +spec(queue_type, 0, _, _) -> + erl_types:t_sup([erl_types:t_atom(ram), + erl_types:t_atom(file)]); +spec(ldap_filter, 0, _, _) -> + erl_types:t_binary(); +spec(sip_uri, 0, _, _) -> + t_remote(esip, uri); +spec(Fun, A, [Form|_], Mod) when (A == 1 orelse A == 2) andalso + (Fun == list orelse Fun == list_or_single) -> + erl_types:t_list(spec(Mod, Form)); +spec(map, A, [F1, F2|OForm], Mod) when A == 2; A == 3 -> + T1 = spec(Mod, F1), + T2 = spec(Mod, F2), + case options_return_type(OForm) of + map -> + erl_types:t_map([], T1, T2); + dict -> + t_remote(dict, dict); + _ -> + erl_types:t_list(erl_types:t_tuple([T1, T2])) + end; +spec(either, 2, [F1, F2], Mod) -> + Spec1 = case erl_syntax:type(F1) of + atom -> erl_types:t_atom(erl_syntax:atom_value(F1)); + _ -> spec(Mod, F1) + end, + Spec2 = spec(Mod, F2), + erl_types:t_sup([Spec1, Spec2]); +spec(and_then, 2, [_, F], Mod) -> + spec(Mod, F); +spec(host, 0, _, _) -> + erl_types:t_binary(); +spec(hosts, 0, _, _) -> + erl_types:t_list(erl_types:t_binary()); +spec(vcard_temp, 0, _, _) -> + erl_types:t_sup([erl_types:t_atom(undefined), + erl_types:t_tuple()]); +spec(options, A, [Form|OForm], Mod) when A == 1; A == 2 -> + case erl_syntax:type(Form) of + map_expr -> + Fs = erl_syntax:map_expr_fields(Form), + Required = options_required(OForm), + {Els, {DefK, DefV}} = + lists:mapfoldl( + fun(F, Acc) -> + Name = erl_syntax:map_field_assoc_name(F), + Val = erl_syntax:map_field_assoc_value(F), + OptType = spec(Mod, Val), + case erl_syntax:atom_value(Name) of + '_' -> + {[], {erl_types:t_atom(), OptType}}; + Atom -> + Mand = case lists:member(Atom, Required) of + true -> mandatory; + false -> optional + end, + {[{erl_types:t_atom(Atom), Mand, OptType}], Acc} + end + end, {erl_types:t_none(), erl_types:t_none()}, Fs), + case options_return_type(OForm) of + map -> + erl_types:t_map(lists:keysort(1, lists:flatten(Els)), DefK, DefV); + dict -> + t_remote(dict, dict); + _ -> + erl_types:t_list( + erl_types:t_sup( + [erl_types:t_tuple([DefK, DefV])| + lists:map( + fun({K, _, V}) -> + erl_types:t_tuple([K, V]) + end, lists:flatten(Els))])) + end; + _ -> + t_unknown(Mod) + end; +spec(_, _, _, Mod) -> + t_unknown(Mod). + +t_from_form(Spec) -> + {T, _} = erl_types:t_from_form( + Spec, sets:new(), {type, {mod, foo, 1}}, dict:new(), + erl_types:var_table__new(), erl_types:cache__new()), + T. + +t_remote(Mod, Type) -> + case get(otp_version) >= 28 of + true -> + t_remote_newopt(Mod, Type); + false -> + t_remote_oldopt(Mod, Type) + end. + +t_remote_newopt(Mod, Type) -> + erl_types:t_nominal({Mod, Type, 0, opaque}, opaque). + +t_remote_oldopt(Mod, Type) -> + D = maps:from_list([{{opaque, Type, []}, + {{Mod, 1, 2, []}, type}}]), + [T] = erl_types:t_opaque_from_records(D), + T. + +t_unknown(_Mod) -> + throw(unknown). + +t_is_any(T) -> + T == erl_types:t_any(). + +t_to_string(T) -> + case erl_types:is_erl_type(T) of + true -> erl_types:t_to_string(T); + false -> erl_types:t_form_to_string(T) + end. + +options_return_type([]) -> + list; +options_return_type([Form]) -> + Opts = erl_syntax:concrete(Form), + proplists:get_value(return, Opts, list). + +options_required([]) -> + []; +options_required([Form]) -> + Opts = erl_syntax:concrete(Form), + proplists:get_value(required, Opts, []). + +format_file(Path, Form) -> + Line = case erl_syntax:get_pos(Form) of + {L, _} -> L; + L -> L + end, + filename:rootname(filename:basename(Path)) ++ ".erl:" ++ + integer_to_list(Line). + +module(Path) -> + list_to_atom(filename:rootname(filename:basename(Path))). + +fold_beams(Fun, State, Paths) -> + Paths1 = fold_paths(Paths), + Total = length(Paths1), + {_, State1} = + lists:foldl( + fun(File, {I, Acc}) -> + io:format("Progress: ~B% (~B/~B)\r", + [round(I*100/Total), I, Total]), + case is_elixir_beam(File) of + true -> {I+1, Acc}; + false -> + AbsCode = get_code_from_beam(File), + Acc2 = case is_behaviour(AbsCode, ejabberd_config) of + true -> + fold_opt(File, Fun, Acc, AbsCode); + false -> + fold_mod_opt(File, Fun, Acc, AbsCode) + end, + {I+1, Acc2} + end + end, {0, State}, Paths1), + State1. + +fold_opt(File, Fun, Acc, AbsCode) -> + lists:foldl( + fun(Form, Acc1) -> + case erl_syntax_lib:analyze_form(Form) of + {function, {opt_type, 1}} -> + Fun(File, {opt_type, Form}, Acc1); + {function, {globals, 0}} -> + Fun(File, {globals, Form}, Acc1); + {function, {options, 0}} -> + Fun(File, {#state.defaults, Form}, Acc1); + {attribute, {spec, {spec, {{options, 0}, Spec}}}} -> + Fun(File, {#state.specs, hd(Spec)}, Acc1); + {attribute, {spec, {{options, 0}, Spec}}} -> + Fun(File, {#state.specs, hd(Spec)}, Acc1); + _ -> + Acc1 + end + end, Acc, AbsCode). + +fold_mod_opt(File, Fun, Acc, AbsCode) -> + lists:foldl( + fun(Form, Acc1) -> + case erl_syntax_lib:analyze_form(Form) of + {function, {mod_opt_type, 1}} -> + Fun(File, {mod_opt_type, Form}, Acc1); + {function, {mod_options, 1}} -> + Fun(File, {#state.mod_defaults, Form}, Acc1); + {attribute, {spec, {spec, {{mod_options, 1}, Spec}}}} -> + Fun(File, {#state.mod_specs, hd(Spec)}, Acc1); + {attribute, {spec, {{mod_options, 1}, Spec}}} -> + Fun(File, {#state.mod_specs, hd(Spec)}, Acc1); + _ -> + Acc1 + end + end, Acc, AbsCode). + +fold_paths(Paths) -> + lists:flatmap( + fun(Path) -> + case filelib:is_dir(Path) of + true -> + Beams = lists:reverse( + filelib:fold_files( + Path, ".+\.beam\$", false, + fun(File, Acc) -> + [File|Acc] + end, [])), + case Beams of + [] -> ok; + _ -> code:add_path(Path) + end, + Beams; + false -> + [Path] + end + end, Paths). + +is_behaviour(AbsCode, Mod) -> + lists:any( + fun(Form) -> + case erl_syntax_lib:analyze_form(Form) of + {attribute, {Attr, {_, Mod}}} + when Attr == behaviour orelse Attr == behavior -> + true; + {attribute, {behaviour, Mod}} -> + true; + _ -> + false + end + end, AbsCode). + +is_elixir_beam(File) -> + case filename:basename(File) of + "Elixir" ++ _ -> true; + _ -> false + end. + +get_code_from_beam(File) -> + try + {ok, {_, List}} = beam_lib:chunks(File, [abstract_code]), + {_, {raw_abstract_v1, Forms}} = lists:keyfind(abstract_code, 1, List), + Forms + catch _:{badmatch, _} -> + err("no abstract code found in ~s", [File]) + end. + +comment() -> + "%% Generated automatically~n" + "%% DO NOT EDIT: run `make options` instead~n". + +log(Format, Args) -> + log(standard_io, Format, Args). + +log(Fd, Format, Args) -> + case io:format(Fd, Format ++ "~n", Args) of + ok -> ok; + {error, Reason} -> + err("Failed to write to file: ~s", [file:format_error(Reason)]) + end. + +warn(Format, Args) -> + io:format(standard_error, "Warning: " ++ Format ++ "~n", Args). + +err(Format, Args) -> + io:format(standard_error, "Error: " ++ Format ++ "~n", Args), + halt(1). diff --git a/tools/prepare-tr.sh b/tools/prepare-tr.sh new file mode 100755 index 000000000..3c5596189 --- /dev/null +++ b/tools/prepare-tr.sh @@ -0,0 +1,120 @@ +#!/bin/bash + +# Frontend for ejabberd's extract-tr.sh + +# How to create template files for a new language: +# NEWLANG=zh +# cp priv/msgs/ejabberd.pot priv/msgs/$NEWLANG.po +# echo \{\"\",\"\"\}. > priv/msgs/$NEWLANG.msg +# make translations + +extract_lang_src2pot () +{ + ./tools/extract-tr.sh src $DEPS_DIR/xmpp/src > $PO_DIR/ejabberd.pot +} + +extract_lang_popot2po () +{ + LANG_CODE=$1 + PO_PATH=$PO_DIR/$LANG_CODE.po + POT_PATH=$PO_DIR/$PROJECT.pot + + msgmerge $PO_PATH $POT_PATH >$PO_PATH.translate 2>>$LOG + mv $PO_PATH.translate $PO_PATH +} + +extract_lang_po2msg () +{ + LANG_CODE=$1 + PO_PATH=$LANG_CODE.po + MS_PATH=$PO_PATH.ms + MSGID_PATH=$PO_PATH.msgid + MSGSTR_PATH=$PO_PATH.msgstr + MSGS_PATH=$LANG_CODE.msg + + cd $PO_DIR || exit + + # Check PO has correct ~ + # Let's convert to C format so we can use msgfmt + PO_TEMP=$LANG_CODE.po.temp + cat $PO_PATH | sed 's/%/perc/g' | sed 's/~/%/g' | sed 's/#:.*/#, c-format/g' >$PO_TEMP + msgfmt $PO_TEMP --check-format + result=$? + rm $PO_TEMP + if [ $result -ne 0 ] ; then + exit 1 + fi + + msgattrib $PO_PATH --translated --no-fuzzy --no-obsolete --no-location --no-wrap | grep "^msg" | tail --lines=+3 >$MS_PATH + grep "^msgid" $PO_PATH.ms | sed 's/^msgid //g' >$MSGID_PATH + grep "^msgstr" $PO_PATH.ms | sed 's/^msgstr //g' >$MSGSTR_PATH + { + echo "%% Generated automatically" + echo "%% DO NOT EDIT: run \`make translations\` instead" + echo "%% To improve translations please read:" + echo "%% https://docs.ejabberd.im/developer/extending-ejabberd/localization/" + echo "" + } >>$MSGS_PATH + paste $MSGID_PATH $MSGSTR_PATH --delimiter=, | awk '{print "{" $0 "}."}' | sort -g >>$MSGS_PATH + + rm $MS_PATH + rm $MSGID_PATH + rm $MSGSTR_PATH + + mv $MSGS_PATH $MSGS_DIR +} + +extract_lang_updateall () +{ + echo "" + echo "Generating POT..." + extract_lang_src2pot + + cd $MSGS_DIR || exit + echo "" + echo "File Missing (fuzzy) Language Last translator" + echo "---- ------- ------- -------- ---------------" + for i in *.msg ; do + LANG_CODE=${i%.msg} + printf "%s" "$LANG_CODE" | awk '{printf "%-6s", $1 }' + + PO=$PO_DIR/$LANG_CODE.po + + extract_lang_popot2po $LANG_CODE + extract_lang_po2msg $LANG_CODE + + MISSING=$(msgfmt --statistics $PO 2>&1 | awk '{printf "%5s", $4+$7 }') + printf " %s" "$MISSING" + + FUZZY=$(msgfmt --statistics $PO 2>&1 | awk '{printf "%7s", $4 }') + printf " %s" "$FUZZY" + + LANGUAGE=$(grep "X-Language:" $PO | sed 's/\"X-Language: //g' | sed 's/\\n\"//g' | awk '{printf "%-12s", $1}') + printf " %s" "$LANGUAGE" + + LASTAUTH=$(grep "Last-Translator" $PO | sed 's/\"Last-Translator: //g' | sed 's/\\n\"//g') + echo " $LASTAUTH" + done + echo "" + rm messages.mo + grep -v " done" $LOG + rm $LOG + + cd .. +} + +EJA_DIR=$(pwd) +PROJECT=ejabberd +DEPS_DIR=$1 +MSGS_DIR=$EJA_DIR/priv/msgs +LOG=/tmp/ejabberd-translate-errors.log +PO_DIR=$EJA_DIR/$DEPS_DIR/ejabberd_po/src/ +if [ ! -f $EJA_DIR/$DEPS_DIR/ejabberd_po/src/ejabberd.pot ]; then + echo "Couldn't find the required ejabberd_po repository in" + echo " $PO_DIR" + echo "Run: ./configure --enable-tools; ./rebar get-deps" + exit 1 +fi +echo "Using PO files from $PO_DIR." + +extract_lang_updateall diff --git a/tools/rebar3-format.sh b/tools/rebar3-format.sh new file mode 100755 index 000000000..2181b6870 --- /dev/null +++ b/tools/rebar3-format.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +# To start formatting a file, add a line that contains: +# @format-begin +# Formatting in that file can later be disabled adding another line with: +# @format-end +# +# It can be reenabled again later in the file. +# +# Finally, call: make format + +REBAR=$1 + +FORMAT() +{ +FPATH=$1 +ERLS=$(git grep --name-only @format-begin "$FPATH"/) + +for ERL in $ERLS; do + perl -n -e 'sub o { open(OUT, ">", sprintf("%s-format-%02d", $f, $n++));}; BEGIN{($f)=@ARGV;o()}; o() if /\@format-/; print OUT $_;' $ERL +done + +EFMTS=$(find "$FPATH"/*-format-* -type f -exec grep --files-with-matches "@format-begin" '{}' ';') +EFMTS2="" +for EFMT in $EFMTS; do + EFMTS2="$EFMTS2 --files $EFMT" +done +$REBAR format $EFMTS2 + +for ERL in $ERLS; do + SPLITS=$(find $ERL-format-* -type f) + rm $ERL + for SPLIT in $SPLITS; do + cat $SPLIT >> $ERL + rm $SPLIT + done +done +} + +FORMAT src +FORMAT test diff --git a/tools/set-dep-versions b/tools/set-dep-versions deleted file mode 100755 index 8d56cde4d..000000000 --- a/tools/set-dep-versions +++ /dev/null @@ -1,78 +0,0 @@ -#!/bin/sh - -set -e -set -u - -export PATH="/usr/local/bin:/usr/local/sbin:/bin:/sbin:/usr/bin:/usr/sbin:$PATH" - -deps_dir='deps' -rebar_script='rebar.config.script' -temp_file=$(mktemp "$rebar_script.XXXXXX") - -trap 'rm -f $temp_file' EXIT INT TERM - -die() -{ - echo >&2 "FATAL: $@." - exit 1 -} - -get_dep_list() -{ - sed -n \ - '/.*{ *\([^,]*\),[^,]*, *{git, *"\([^"]*\)".*/ { - s//\1,\2/ - p - }' "$rebar_script" -} - -get_dep_name() -{ - printf '%s' "${1%%,*}" -} - -get_dep_url() -{ - printf '%s' "${1#*,}" -} - -get_dep_rev() -{ - dep_name=$(get_dep_name "$1") - dep_dir="$deps_dir/$dep_name" - - test -d "$dep_dir" || clone_repo "$dep" - cd "$dep_dir" - printf '%s' "$(git rev-parse --verify HEAD)" - cd "$OLDPWD" -} - -clone_repo() -{ - dep_name=$(get_dep_name "$1") - dep_url=$(get_dep_url "$1") - - cd "$deps_dir" - git clone -q "$dep_url" "$dep_name" - cd "$OLDPWD" -} - -edit_rebar_script() -{ - dep_name=$(get_dep_name "$1") - dep_url=$(get_dep_url "$1") - dep_rev=$(get_dep_rev "$1") - - echo "Using revision $dep_rev of $dep_name" - sed "s|\"$dep_url\"[^}]*}|\"$dep_url\", \"$dep_rev\"}|" \ - "$rebar_script" >"$temp_file" - mv "$temp_file" "$rebar_script" -} - -test -e "$rebar_script" || die 'Please change to ejabberd source directory' -test -d "$deps_dir" || mkdir -p "$deps_dir" - -for dep in $(get_dep_list) -do - edit_rebar_script "$dep" -done diff --git a/tools/update-deps-releases.pl b/tools/update-deps-releases.pl new file mode 100755 index 000000000..b450bd69b --- /dev/null +++ b/tools/update-deps-releases.pl @@ -0,0 +1,567 @@ +#!/usr/bin/perl + +use v5.10; +use strict; +use warnings; + +use File::Slurp qw(slurp write_file); +use File::stat; +use File::Touch; +use File::chdir; +use File::Spec; +use Data::Dumper qw(Dumper); +use Carp; +use Term::ANSIColor; +use Term::ReadKey; +use List::Util qw(first); +use Clone qw(clone); +use LWP::UserAgent; + +sub get_deps { + my ($config, %fdeps) = @_; + + my %deps; + + return { } unless $config =~ /\{\s*deps\s*,\s*\[(.*?)\]/s; + my $sdeps = $1; + + while ($sdeps =~ /\{\s* (\w+) \s*,\s* ".*?" \s*,\s* \{\s*git \s*,\s* "(.*?)" \s*,\s* + (?: + (?:{\s*tag \s*,\s* "(.*?)") | + "(.*?)" | + ( \{ (?: (?-1) | [^{}]+ )+ \} ) )/sgx) { + next unless not %fdeps or exists $fdeps{$1}; + $deps{$1} = { repo => $2, commit => $3 || $4 }; + } + return \%deps; +} +my (%info_updates, %top_deps_updates, %sub_deps_updates, @operations); +my $epoch = 1; + +sub top_deps { + state %deps; + state $my_epoch = $epoch; + if (not %deps or $my_epoch != $epoch) { + $my_epoch = $epoch; + my $config = slurp "rebar.config"; + croak "Unable to extract floating_deps" unless $config =~ /\{floating_deps, \[(.*?)\]/s; + + my $fdeps = $1; + $fdeps =~ s/\s*//g; + my %fdeps = map { $_ => 1 } split /,/, $fdeps; + %deps = %{get_deps($config, %fdeps)}; + } + return {%deps, %top_deps_updates}; +} + +sub update_deps_repos { + my ($force) = @_; + my $deps = top_deps(); + $epoch++; + mkdir(".deps-update") unless -d ".deps-update"; + for my $dep (keys %{$deps}) { + my $dd = ".deps-update/$dep"; + if (not -d $dd) { + say "Downloading $dep..."; + my $repo = $deps->{$dep}->{repo}; + $repo =~ s!^https?://github.com/!git\@github.com:!; + system("git", "-C", ".deps-update", "clone", $repo, $dep); + } elsif (time() - stat($dd)->mtime > 24 * 60 * 60 or $force) { + say "Updating $dep..."; + system("git", "-C", $dd, "pull"); + touch($dd) + } + } +} + +sub sub_deps { + state %sub_deps; + state $my_epoch = $epoch; + if (not %sub_deps or $my_epoch != $epoch) { + $my_epoch = $epoch; + my $deps = top_deps(); + for my $dep (keys %{$deps}) { + my $rc = ".deps-update/$dep/rebar.config"; + $sub_deps{$dep} = { }; + next unless -f $rc; + $sub_deps{$dep} = get_deps(scalar(slurp($rc))); + } + } + return {%sub_deps, %sub_deps_updates}; +} + +sub rev_deps_helper { + my ($rev_deps, $dep) = @_; + if (not exists $rev_deps->{$dep}->{indirect}) { + my %deps = %{$rev_deps->{$dep}->{direct} || {}}; + for (keys %{$rev_deps->{$dep}->{direct}}) { + %deps = (%deps, %{rev_deps_helper($rev_deps, $_)}); + } + $rev_deps->{$dep}->{indirect} = \%deps; + } + return $rev_deps->{$dep}->{indirect}; +} + +sub rev_deps { + state %rev_deps; + state $deps_epoch = $epoch; + if (not %rev_deps or $deps_epoch != $epoch) { + $deps_epoch = $epoch; + my $sub_deps = sub_deps(); + for my $dep (keys %$sub_deps) { + $rev_deps{$_}->{direct}->{$dep} = 1 for keys %{$sub_deps->{$dep}}; + } + for my $dep (keys %$sub_deps) { + $rev_deps{$dep}->{indirect} = rev_deps_helper(\%rev_deps, $dep); + } + } + return \%rev_deps; +} + +sub update_changelog { + my ($dep, $version, @reasons) = @_; + my $cl = ".deps-update/$dep/CHANGELOG.md"; + return if not -f $cl; + my $reason = join "\n", map {"* $_"} @reasons; + my $content = slurp($cl); + if (not $content =~ /^# Version $version/) { + $content = "# Version $version\n\n$reason\n\n$content"; + } else { + $content =~ s/(# Version $version\n\n)/$1$reason\n/; + } + write_file($cl, $content); +} + +sub edit_changelog { + my ($dep, $version) = @_; + my $cl = ".deps-update/$dep/CHANGELOG.md"; + + return if not -f $cl; + + my $top_deps = top_deps(); + my $git_info = deps_git_info(); + + say color("red"), "$dep", color("reset"), " ($top_deps->{$dep}->{commit}):"; + say " $_" for @{$git_info->{$dep}->{new_commits}}; + say ""; + + my $content = slurp($cl); + my $old_content = $content; + + if (not $content =~ /^# Version $version/) { + $content = "# Version $version\n\n* \n\n$content"; + } else { + $content =~ s/(# Version $version\n\n)/$1* \n/; + } + write_file($cl, $content); + + system("$ENV{EDITOR} $cl"); + + my $new_content = slurp($cl); + if ($new_content eq $content) { + write_file($cl, $old_content); + } else { + system("git", "-C", ".deps-update/$dep", "commit", "-a", "-m", "Update changelog"); + } +} + +sub update_app_src { + my ($dep, $version) = @_; + my $app = ".deps-update/$dep/src/$dep.app.src"; + return if not -f $app; + my $content = slurp($app); + $content =~ s/(\{\s*vsn\s*,\s*)".*"/$1"$version"/; + write_file($app, $content); +} + +sub update_deps_versions { + my ($config_path, %deps) = @_; + my $config = slurp $config_path; + + for (keys %deps) { + $config =~ s/(\{\s*$_\s*,\s*".*?"\s*,\s*\{\s*git\s*,\s*".*?"\s*,\s*)(?:{\s*tag\s*,\s*"(.*?)"\s*}|"(.*?)")/$1\{tag, "$deps{$_}"}/s; + } + + write_file($config_path, $config); +} + +sub cmp_ver { + my @a = split /(\d+)/, $a; + my @b = split /(\d+)/, $b; + my $is_num = 1; + + return - 1 if $#a == 0; + return 1 if $#b == 0; + + while (1) { + my $ap = shift @a; + my $bp = shift @b; + $is_num = 1 - $is_num; + + if (defined $ap) { + if (defined $bp) { + if ($is_num) { + next if $ap == $bp; + return 1 if $ap > $bp; + return - 1; + } else { + next if $ap eq $bp or $ap eq "" or $bp eq ""; + return 1 if $ap gt $bp; + return - 1; + } + } else { + return 1; + } + } elsif (defined $bp) { + return - 1; + } else { + return 0; + } + } +} + +sub deps_git_info { + state %info; + state $my_epoch = $epoch; + if (not %info or $my_epoch != $epoch) { + $my_epoch = $epoch; + my $deps = top_deps(); + for my $dep (keys %{$deps}) { + my $dir = ".deps-update/$dep"; + my @tags = `git -C "$dir" tag`; + chomp(@tags); + @tags = sort cmp_ver @tags; + my $last_tag = $tags[$#tags]; + my @new = `git -C $dir log --oneline $last_tag..origin/master`; + my $new_tag = $last_tag; + $new_tag =~ s/(\d+)$/$1+1/e; + chomp(@new); + + my $cl = ".deps-update/$dep/CHANGELOG.md"; + my $content = slurp($cl, err_mode => "quiet") // ""; + if ($content =~ /^# Version (\S+)/) { + if (!grep({$_ eq $1} @tags) && $1 ne $new_tag) { + $new_tag = $1; + } + } + + $info{$dep} = { last_tag => $last_tag, new_commits => \@new, new_tag => $new_tag }; + } + } + return { %info, %info_updates }; +} + +sub show_commands { + my %commands = @_; + my @keys; + while (@_) { + push @keys, shift; + shift; + } + for (@keys) { + say color("red"), $_, color("reset"), ") $commands{$_}"; + } + ReadMode(4); + my $wkey = ""; + while (1) { + my $key = ReadKey(0); + $wkey = substr($wkey.$key, -2); + if (defined $commands{uc($key)}) { + ReadMode(0); + say ""; + return uc($key); + } elsif (defined $commands{uc($wkey)}) { + ReadMode(0); + say ""; + return uc($wkey); + } + } +} + +sub schedule_operation { + my ($type, $dep, $tag, $reason, $op) = @_; + + my $idx = first { $operations[$_]->{dep} eq $dep } 0..$#operations; + + if (defined $idx) { + my $mop = $operations[$idx]; + if (defined $op) { + my $oidx = first { $mop->{operations}->[$_]->[0] eq $op->[0] } 0..$#{$mop->{operations}}; + if (defined $oidx) { + $mop->{reasons}->[$oidx] = $reason; + $mop->{operations}->[$oidx] = $op; + } else { + push @{$mop->{reasons}}, $reason; + push @{$mop->{operations}}, $op; + } + } + return if $type eq "update"; + $mop->{type} = $type; + $info_updates{$dep}->{new_commits} = []; + return; + } + + my $info = deps_git_info(); + + $top_deps_updates{$dep} = {commit => $tag}; + $info_updates{$dep} = {last_tag => $tag, new_tag => $tag, + new_commits => $type eq "tupdate" ? [] : $info->{$dep}->{new_commits}}; + + my $rev_deps = rev_deps(); + @operations = sort { + exists $rev_deps->{$a->{dep}}->{indirect}->{$b->{dep}} ? -1 : + exists $rev_deps->{$b->{dep}}->{indirect}->{$a->{dep}} ? 1 : $a->{dep} cmp $b->{dep} + } (@operations, { + type => $type, + dep => $dep, + version => $tag, + reasons => ($reason ? [$reason] : []), + operations => ($op ? [$op] : [])} + ); + + my $sub_deps = sub_deps(); + + for (keys %{$rev_deps->{$dep}->{direct}}) { + schedule_operation("update", $_, $info->{$_}->{new_tag}, "Updating $dep to version $tag.", [$dep, $tag]); + $sub_deps_updates{$_} = $sub_deps_updates{$_} || clone($sub_deps->{$_}); + $sub_deps_updates{$_}->{$dep}->{commit} = $tag; + } +} + +sub git_tag { + my ($dep, $ver, $msg) = @_; + + system("git", "-C", ".deps-update/$dep", "commit", "-a", "-m", $msg); + system("git", "-C", ".deps-update/$dep", "tag", $ver, "-a", "-m", $msg); +} + +sub git_push { + my ($dep) = @_; + system("git", "-C", ".deps-update/$dep", "push"); + system("git", "-C", ".deps-update/$dep", "push", "--tags"); +} + +sub check_hex_files { + my ($dep) = @_; + my $app = ".deps-update/$dep/src/$dep.app.src"; + return if not -f $app; + my $content = slurp($app); + my @paths; + if ($content =~ /{\s*files\s*,\s*\[([^\]]+)\]/) { + my $list = $1; + push @paths, $1 while $list =~ /"([^"]*?)"/g; + } else { + @paths = ( + "src", "c_src", "include", "rebar.config.script", "priv", + "rebar.config", "rebar.lock", "README*", "readme*", "LICENSE*", + "license*", "NOTICE"); + } + local $CWD = ".deps-update/$dep"; + my @interesting_files = map {File::Spec->canonpath($_)} glob("rebar.config* src/*.erl src/*.app.src c_src/*.c c_src/*.cpp \ + c_src/*.h c_src/*.hpp include/*.hrl"); + + my @matching_files; + for my $path (@paths) { + if (-d $path) { + push @matching_files, map {File::Spec->canonpath($_)} glob("$path/*"); + } else { + push @matching_files, map {File::Spec->canonpath($_)} glob($path); + } + } + my %diff; + @diff{ @interesting_files } = undef; + delete @diff{ @matching_files }; + my @diff = keys %diff; + if (@diff) { + print color("red"), "Dependency ", color("bold red"), $dep, color("reset"), color("red"), " files section doesn't match: ", + join(" ", @diff), color("reset"), "\n"; + + } +} + +update_deps_repos(); + +MAIN: +while (1) { + my $top_deps = top_deps(); + my $git_info = deps_git_info(); + print color("bold blue"), "Dependences with newer tags:\n", color("reset"); + my $old_deps = 0; + for my $dep (sort keys %$top_deps) { + next unless $git_info->{$dep}->{last_tag} ne $top_deps->{$dep}->{commit}; + say color("red"), "$dep", color("reset"), ": $top_deps->{$dep}->{commit} -> $git_info->{$dep}->{last_tag}"; + $old_deps = 1; + } + say "(none)" if not $old_deps; + say ""; + + print color("bold blue"), "Dependences that have commits after last tags:\n", color("reset"); + my $changed_deps = 0; + for my $dep (sort keys %$top_deps) { + next unless @{$git_info->{$dep}->{new_commits}}; + say color("red"), "$dep", color("reset"), " ($top_deps->{$dep}->{commit}):"; + say " $_" for @{$git_info->{$dep}->{new_commits}}; + $changed_deps = 1; + } + say "(none)" if not $changed_deps; + say ""; + + for my $dep (sort keys %$top_deps) { + check_hex_files($dep); + } + + my $cmd = show_commands($old_deps ? (U => "Update dependency") : (), + $changed_deps ? (T => "Tag new release") : (), + @operations ? (A => "Apply changes") : (), + R => "Refresh repositories", + H => "What release to Hex", + E => "Exit"); + last if $cmd eq "E"; + + if ($cmd eq "U") { + while (1) { + my @deps_to_update; + my @od; + my $idx = 1; + for my $dep (sort keys %$top_deps) { + next unless $git_info->{$dep}->{last_tag} ne $top_deps->{$dep}->{commit}; + $od[$idx] = $dep; + push @deps_to_update, $idx++, "Update $dep to $git_info->{$dep}->{last_tag}"; + } + last if $idx == 1; + my $cmd = show_commands(@deps_to_update, E => "Exit"); + last if $cmd eq "E"; + + my $dep = $od[$cmd]; + schedule_operation("update", $dep, $git_info->{$dep}->{last_tag}); + + $top_deps = top_deps(); + $git_info = deps_git_info(); + } + } + + if ($cmd eq "R") { + update_deps_repos(1); + } + if ($cmd eq "H") { + my $ua = LWP::UserAgent->new(); + for my $dep (sort keys %$top_deps) { + say "checking https://hex.pm/packages/$dep/$git_info->{$dep}->{last_tag}"; + my $res = $ua->head("https://hex.pm/packages/$dep/$git_info->{$dep}->{last_tag}"); + if ($res->code == 404) { + say color("red"), "$dep", color("reset"), " ($top_deps->{$dep}->{commit})"; + } + } + } + if ($cmd eq "T") { + while (1) { + my @deps_to_tag; + my @od; + my $idx = 1; + my $count = 0; + for my $dep (sort keys %$top_deps) { + next unless @{$git_info->{$dep}->{new_commits}}; + $count++; + } + for my $dep (sort keys %$top_deps) { + next unless @{$git_info->{$dep}->{new_commits}}; + $od[$idx] = $dep; + my $id = $idx++; + $id = sprintf "%02d", $id if $count > 9; + push @deps_to_tag, $id, "Tag $dep with version $git_info->{$dep}->{new_tag}"; + } + last if $idx == 1; + my $cmd = show_commands(@deps_to_tag, E => "Exit"); + last if $cmd eq "E"; + + my $dep = $od[$cmd]; + my $d = $git_info->{$dep}; + schedule_operation("tupdate", $dep, $d->{new_tag}); + + $top_deps = top_deps(); + $git_info = deps_git_info(); + } + } + + my $changelog_updated = 0; + + if ($cmd eq "A") { + APPLY: { + $top_deps = top_deps(); + $git_info = deps_git_info(); + my $sub_deps = sub_deps(); + + for my $dep (keys %$top_deps) { + for my $sdep (keys %{$sub_deps->{$dep}}) { + next if not defined $top_deps->{$sdep} or + $sub_deps->{$dep}->{$sdep}->{commit} eq $top_deps->{$sdep}->{commit}; + say "$dep $sdep ", $sub_deps->{$dep}->{$sdep}->{commit}, " <=> $sdep ", + $top_deps->{$sdep}->{commit}; + schedule_operation("update", $dep, $git_info->{$dep}->{new_tag}, + "Updating $sdep to version $top_deps->{$sdep}->{commit}.", + [ $sdep, $top_deps->{$sdep}->{commit} ]); + } + } + + %info_updates = (); + %top_deps_updates = (); + %sub_deps_updates = (); + + $top_deps = top_deps(); + $git_info = deps_git_info(); + $sub_deps = sub_deps(); + + print color("bold blue"), "List of operations:\n", color("reset"); + for my $op (@operations) { + print color("red"), $op->{dep}, color("reset"), + " ($top_deps->{$op->{dep}}->{commit} -> $op->{version})"; + if (@{$op->{operations}}) { + say ":"; + say " $_->[0] -> $_->[1]" for @{$op->{operations}}; + } + else { + say ""; + } + } + + say ""; + my %to_tag; + if (not $changelog_updated) { + for my $op (@operations) { + if ($git_info->{$op->{dep}}->{last_tag} ne $op->{version}) { + $to_tag{$op->{dep}} = $op->{version}; + } + } + } + my $cmd = show_commands(A => "Apply", (%to_tag ? (U => "Update Changelogs") : ()), E => "Exit"); + if ($cmd eq "U") { + for my $dep (keys %to_tag) { + edit_changelog($dep, $to_tag{$dep}); + } + redo APPLY; + } + elsif ($cmd eq "A") { + my %top_changes; + for my $op (@operations) { + update_changelog($op->{dep}, $op->{version}, @{$op->{reasons}}) + if @{$op->{reasons}}; + update_deps_versions(".deps-update/$op->{dep}/rebar.config", map {@{$_}[0,1] } @{$op->{operations}}) + if @{$op->{operations}}; + if ($git_info->{$op->{dep}}->{last_tag} ne $op->{version}) { + update_app_src($op->{dep}, $op->{version}); + git_tag($op->{dep}, $op->{version}, "Release $op->{version}"); + } + + $top_changes{$op->{dep}} = $op->{version}; + } + update_deps_versions("rebar.config", %top_changes); + for my $op (@operations) { + if ($git_info->{$op->{dep}}->{last_tag} ne $op->{version}) { + git_push($op->{dep}); + } + } + last MAIN; + } + } + } +} diff --git a/tools/xml_compress_gen.erl b/tools/xml_compress_gen.erl new file mode 100644 index 000000000..d331d7533 --- /dev/null +++ b/tools/xml_compress_gen.erl @@ -0,0 +1,422 @@ +%% File : xml_compress_gen.erl +%% Author : Pawel Chmielowski +%% Purpose : +%% Created : 14 Sep 2018 Pawel Chmielowski +%% +%% +%% ejabberd, Copyright (C) 2002-2025 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(xml_compress_gen). +-author("pawel@process-one.net"). + +-include_lib("xmpp/include/xmpp.hrl"). + +%% API +-export([archive_analyze/3, process_stats/1, gen_code/3]). + +-record(el_stats, {count = 0, empty_count = 0, only_text_count = 0, attrs = #{}, text_stats = #{}}). +-record(attr_stats, {count = 0, vals = #{}}). + +archive_analyze(Host, Table, EHost) -> + case ejabberd_sql:sql_query(Host, [<<"select username, peer, kind, xml from ", Table/binary>>]) of + {selected, _, Res} -> + lists:foldl( + fun([U, P, K, X], Stats) -> + M = case K of + <<"groupchat">> -> + U; + _ -> + <<U/binary, "@", EHost/binary>> + end, + El = fxml_stream:parse_element(X), + analyze_element({El, <<"stream">>, <<"jabber:client">>, M, P}, Stats) + end, {0, #{}}, Res); + _ -> + none + end. + +encode_id(Num) when Num < 64 -> + iolist_to_binary(io_lib:format("~p:8", [Num])). + +gen_code(_File, _Rules, $<) -> + {error, <<"Invalid version">>}; +gen_code(File, Rules, Ver) when Ver < 64 -> + {Data, _} = lists:foldl( + fun({Ns, El, Attrs, Text}, {Acc, Id}) -> + NsC = case lists:keyfind(Ns, 1, Acc) of + false -> []; + {_, L} -> L + end, + {AttrsE, _} = lists:mapfoldl( + fun({AName, AVals}, Id2) -> + {AD, Id3} = lists:mapfoldl( + fun(AVal, Id3) -> + {{AVal, encode_id(Id3)}, Id3 + 1} + end, Id2, AVals), + {{AName, AD ++ [encode_id(Id3)]}, Id3 + 1} + end, 3, Attrs), + {TextE, Id5} = lists:mapfoldl( + fun(TextV, Id4) -> + {{TextV, encode_id(Id4)}, Id4 + 1} + end, Id + 1, Text), + {lists:keystore(Ns, 1, Acc, {Ns, NsC ++ [{El, encode_id(Id), AttrsE, TextE}]}), Id5} + end, {[], 5}, Rules), + {ok, Dev} = file:open(File, [write]), + Mod = filename:basename(File, ".erl"), + io:format(Dev, "-module(~s).~n-export([encode/3, decode/3]).~n~n", [Mod]), + RulesS = iolist_to_binary(io_lib:format("~p", [Rules])), + RulesS2 = binary:replace(RulesS, <<"\n">>, <<"\n% ">>, [global]), + io:format(Dev, "% This file was generated by xml_compress_gen~n%~n" + "% Rules used:~n%~n% ~s~n~n", [RulesS2]), + VerId = iolist_to_binary(io_lib:format("~p:8", [Ver])), + gen_encode(Dev, Data, VerId), + gen_decode(Dev, Data, VerId), + file:close(Dev), + Data. + +gen_decode(Dev, Data, VerId) -> + io:format(Dev, "decode(<<$<, _/binary>> = Data, _J1, _J2) ->~n" + " fxml_stream:parse_element(Data);~n" + "decode(<<~s, Rest/binary>>, J1, J2) ->~n" + " try decode(Rest, <<\"jabber:client\">>, J1, J2, false) of~n" + " {El, _} -> El~n" + " catch throw:loop_detected ->~n" + " {error, {loop_detected, <<\"Compressed data corrupted\">>}}~n" + " end.~n~n", [VerId]), + io:format(Dev, "decode_string(Data) ->~n" + " case Data of~n" + " <<0:2, L:6, Str:L/binary, Rest/binary>> ->~n" + " {Str, Rest};~n" + " <<1:2, L1:6, 0:2, L2:6, Rest/binary>> ->~n" + " L = L2*64 + L1,~n" + " <<Str:L/binary, Rest2/binary>> = Rest,~n" + " {Str, Rest2};~n" + " <<1:2, L1:6, 1:2, L2:6, L3:8, Rest/binary>> ->~n" + " L = (L3*64 + L2)*64 + L1,~n" + " <<Str:L/binary, Rest2/binary>> = Rest,~n" + " {Str, Rest2}~n" + " end.~n~n", []), + io:format(Dev, "decode_child(<<1:8, Rest/binary>>, _PNs, _J1, _J2, _) ->~n" + " {Text, Rest2} = decode_string(Rest),~n" + " {{xmlcdata, Text}, Rest2};~n", []), + io:format(Dev, "decode_child(<<2:8, Rest/binary>>, PNs, J1, J2, _) ->~n" + " {Name, Rest2} = decode_string(Rest),~n" + " {Attrs, Rest3} = decode_attrs(Rest2),~n" + " {Children, Rest4} = decode_children(Rest3, PNs, J1, J2),~n" + " {{xmlel, Name, Attrs, Children}, Rest4};~n", []), + io:format(Dev, "decode_child(<<3:8, Rest/binary>>, PNs, J1, J2, _) ->~n" + " {Ns, Rest2} = decode_string(Rest),~n" + " {Name, Rest3} = decode_string(Rest2),~n" + " {Attrs, Rest4} = decode_attrs(Rest3),~n" + " {Children, Rest5} = decode_children(Rest4, Ns, J1, J2),~n" + " {{xmlel, Name, add_ns(PNs, Ns, Attrs), Children}, Rest5};~n", []), + io:format(Dev, "decode_child(<<4:8, Rest/binary>>, _PNs, _J1, _J2, _) ->~n" + " {stop, Rest};~n", []), + io:format(Dev, "decode_child(_Other, _PNs, _J1, _J2, true) ->~n" + " throw(loop_detected);~n", []), + io:format(Dev, "decode_child(Other, PNs, J1, J2, _) ->~n" + " decode(Other, PNs, J1, J2, true).~n~n", []), + io:format(Dev, "decode_children(Data, PNs, J1, J2) ->~n" + " prefix_map(fun(Data2) -> decode(Data2, PNs, J1, J2, false) end, Data).~n~n", []), + io:format(Dev, "decode_attr(<<1:8, Rest/binary>>) ->~n" + " {Name, Rest2} = decode_string(Rest),~n" + " {Val, Rest3} = decode_string(Rest2),~n" + " {{Name, Val}, Rest3};~n", []), + io:format(Dev, "decode_attr(<<2:8, Rest/binary>>) ->~n" + " {stop, Rest}.~n~n", []), + io:format(Dev, "decode_attrs(Data) ->~n" + " prefix_map(fun decode_attr/1, Data).~n~n", []), + io:format(Dev, "prefix_map(F, Data) ->~n" + " prefix_map(F, Data, []).~n~n", []), + io:format(Dev, "prefix_map(F, Data, Acc) ->~n" + " case F(Data) of~n" + " {stop, Rest} ->~n" + " {lists:reverse(Acc), Rest};~n" + " {Val, Rest} ->~n" + " prefix_map(F, Rest, [Val | Acc])~n" + " end.~n~n", []), + io:format(Dev, "add_ns(Ns, Ns, Attrs) ->~n" + " Attrs;~n" + "add_ns(_, Ns, Attrs) ->~n" + " [{<<\"xmlns\">>, Ns} | Attrs].~n~n", []), + lists:foreach( + fun({Ns, Els}) -> + lists:foreach( + fun({Name, Id, Attrs, Text}) -> + io:format(Dev, "decode(<<~s, Rest/binary>>, PNs, J1, J2, _) ->~n" + " Ns = ~p,~n", [Id, Ns]), + case Attrs of + [] -> + io:format(Dev, " {Attrs, Rest2} = decode_attrs(Rest),~n", []); + _ -> + io:format(Dev, " {Attrs, Rest2} = prefix_map(fun~n", []), + lists:foreach( + fun({AName, AVals}) -> + lists:foreach( + fun({j1, AId}) -> + io:format(Dev, " (<<~s, Rest3/binary>>) ->~n" + " {{~p, J1}, Rest3};~n", [AId, AName]); + ({j2, AId}) -> + io:format(Dev, " (<<~s, Rest3/binary>>) ->~n" + " {{~p, J2}, Rest3};~n", [AId, AName]); + ({{j1}, AId}) -> + io:format(Dev, " (<<~s, Rest3/binary>>) ->~n" + " {AVal, Rest4} = decode_string(Rest3),~n" + " {{~p, <<J1/binary, AVal/binary>>}, Rest4};~n", + [AId, AName]); + ({{j2}, AId}) -> + io:format(Dev, " (<<~s, Rest3/binary>>) ->~n" + " {AVal, Rest4} = decode_string(Rest3),~n" + " {{~p, <<J2/binary, AVal/binary>>}, Rest4};~n", + [AId, AName]); + ({AVal, AId}) -> + io:format(Dev, " (<<~s, Rest3/binary>>) ->~n" + " {{~p, ~p}, Rest3};~n", + [AId, AName, AVal]); + (AId) -> + io:format(Dev, " (<<~s, Rest3/binary>>) ->~n" + " {AVal, Rest4} = decode_string(Rest3),~n" + " {{~p, AVal}, Rest4};~n", + [AId, AName]) + end, AVals) + end, Attrs), + io:format(Dev, " (<<2:8, Rest3/binary>>) ->~n" + " {stop, Rest3};~n" + " (Data) ->~n" + " decode_attr(Data)~n" + " end, Rest),~n", []) + end, + case Text of + [] -> + io:format(Dev, " {Children, Rest6} = decode_children(Rest2, Ns, J1, J2),~n", []); + _ -> + io:format(Dev, " {Children, Rest6} = prefix_map(fun", []), + lists:foreach( + fun({TextS, TId}) -> + io:format(Dev, " (<<~s, Rest5/binary>>) ->~n" + " {{xmlcdata, ~p}, Rest5};~n", + [TId, TextS]) + end, Text), + + io:format(Dev, " (Other) ->~n" + " decode_child(Other, Ns, J1, J2, false)~n" + " end, Rest2),~n", []) + end, + io:format(Dev, " {{xmlel, ~p, add_ns(PNs, Ns, Attrs), Children}, Rest6};~n", [Name]) + end, Els) + end, Data), + io:format(Dev, "decode(Other, PNs, J1, J2, Loop) ->~n" + " decode_child(Other, PNs, J1, J2, Loop).~n~n", []). + + +gen_encode(Dev, Data, VerId) -> + io:format(Dev, "encode(El, J1, J2) ->~n" + " encode_child(El, <<\"jabber:client\">>,~n" + " J1, J2, byte_size(J1), byte_size(J2), <<~s>>).~n~n", [VerId]), + io:format(Dev, "encode_attr({<<\"xmlns\">>, _}, Acc) ->~n" + " Acc;~n" + "encode_attr({N, V}, Acc) ->~n" + " <<Acc/binary, 1:8, (encode_string(N))/binary,~n" + " (encode_string(V))/binary>>.~n~n", []), + io:format(Dev, "encode_attrs(Attrs, Acc) ->~n" + " lists:foldl(fun encode_attr/2, Acc, Attrs).~n~n", []), + io:format(Dev, "encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) ->~n" + " E1 = if~n" + " PNs == Ns -> encode_attrs(Attrs, <<Pfx/binary, 2:8, (encode_string(Name))/binary>>);~n" + " true -> encode_attrs(Attrs, <<Pfx/binary, 3:8, " + "(encode_string(Ns))/binary, (encode_string(Name))/binary>>)~n" + " end,~n" + " E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <<E1/binary, 2:8>>),~n" + " <<E2/binary, 4:8>>.~n~n", []), + io:format(Dev, "encode_child({xmlel, Name, Attrs, Children}, PNs, J1, J2, J1L, J2L, Pfx) ->~n" + " case lists:keyfind(<<\"xmlns\">>, 1, Attrs) of~n" + " false ->~n" + " encode(PNs, PNs, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx);~n" + " {_, Ns} ->~n" + " encode(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx)~n" + " end;~n" + "encode_child({xmlcdata, Data}, _PNs, _J1, _J2, _J1L, _J2L, Pfx) ->~n" + " <<Pfx/binary, 1:8, (encode_string(Data))/binary>>.~n~n", []), + io:format(Dev, "encode_children(Children, PNs, J1, J2, J1L, J2L, Pfx) ->~n" + " lists:foldl(~n" + " fun(Child, Acc) ->~n" + " encode_child(Child, PNs, J1, J2, J1L, J2L, Acc)~n" + " end, Pfx, Children).~n~n", []), + io:format(Dev, "encode_string(Data) ->~n" + " <<V1:4, V2:6, V3:6>> = <<(byte_size(Data)):16/unsigned-big-integer>>,~n" + " case {V1, V2, V3} of~n" + " {0, 0, V3} ->~n" + " <<V3:8, Data/binary>>;~n" + " {0, V2, V3} ->~n" + " <<(V3 bor 64):8, V2:8, Data/binary>>;~n" + " _ ->~n" + " <<(V3 bor 64):8, (V2 bor 64):8, V1:8, Data/binary>>~n" + " end.~n~n", []), + lists:foreach( + fun({Ns, Els}) -> + io:format(Dev, "encode(PNs, ~p = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) ->~n" + " case Name of~n", [Ns]), + lists:foreach( + fun({ElN, Id, Attrs, Text}) -> + io:format(Dev, " ~p ->~n", [ElN]), + case Attrs of + [] -> + io:format(Dev, " E = encode_attrs(Attrs, <<Pfx/binary, ~s>>),~n", [Id]); + _ -> + io:format(Dev, " E = lists:foldl(fun~n", []), + lists:foreach( + fun({AName, AVals}) -> + case AVals of + [AIdS] when is_binary(AIdS) -> + io:format(Dev, " ({~p, AVal}, Acc) ->~n" + " <<Acc/binary, ~s, (encode_string(AVal))/binary>>;~n", + [AName, AIdS]); + _ -> + io:format(Dev, " ({~p, AVal}, Acc) ->~n" + " case AVal of~n", [AName]), + lists:foreach( + fun({j1, AId}) -> + io:format(Dev, " J1 -> <<Acc/binary, ~s>>;~n", + [AId]); + ({j2, AId}) -> + io:format(Dev, " J2 -> <<Acc/binary, ~s>>;~n", + [AId]); + ({{j1}, AId}) -> + io:format(Dev, " <<J1:J1L/binary, Rest/binary>> -> " + "<<Acc/binary, ~s, (encode_string(Rest))/binary>>;~n", + [AId]); + ({{j2}, AId}) -> + io:format(Dev, " <<J2:J2L/binary, Rest/binary>> -> " + "<<Acc/binary, ~s, (encode_string(Rest))/binary>>;~n", + [AId]); + ({AVal, AId}) -> + io:format(Dev, " ~p -> <<Acc/binary, ~s>>;~n", + [AVal, AId]); + (AId) -> + io:format(Dev, " _ -> <<Acc/binary, ~s, " + "(encode_string(AVal))/binary>>~n", + [AId]) + end, AVals), + io:format(Dev, " end;~n", []) + end + end, Attrs), + io:format(Dev, " (Attr, Acc) -> encode_attr(Attr, Acc)~n", []), + io:format(Dev, " end, <<Pfx/binary, ~s>>, Attrs),~n", [Id]) + end, + case Text of + [] -> + io:format(Dev, " E2 = encode_children(Children, Ns, " + "J1, J2, J1L, J2L, <<E/binary, 2:8>>),~n", []); + _ -> + io:format(Dev, " E2 = lists:foldl(fun~n", []), + lists:foreach( + fun({TextV, TId}) -> + io:format(Dev, " ({xmlcdata, ~p}, Acc) -> <<Acc/binary, ~s>>;~n", [TextV, TId]) + end, Text), + io:format(Dev, " (El, Acc) -> encode_child(El, Ns, J1, J2, J1L, J2L, Acc)~n", []), + io:format(Dev, " end, <<E/binary, 2:8>>, Children),~n", []) + end, + io:format(Dev, " <<E2/binary, 4:8>>;~n", []) + end, Els), + io:format(Dev, " _ -> encode_el(PNs, Ns, Name, Attrs, Children, " + "J1, J2, J1L, J2L, Pfx)~nend;~n", []) + end, Data), + io:format(Dev, "encode(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) ->~n" + " encode_el(PNs, Ns, Name, Attrs, Children, " + "J1, J2, J1L, J2L, Pfx).~n~n", []). + +process_stats({_Counts, Stats}) -> + SStats = lists:sort( + fun({_, #el_stats{count = C1}}, {_, #el_stats{count = C2}}) -> + C1 >= C2 + end, maps:to_list(Stats)), + lists:map( + fun({Name, #el_stats{count = C, attrs = A, text_stats = T}}) -> + [Ns, El] = binary:split(Name, <<"<">>), + Attrs = lists:filtermap( + fun({AN, #attr_stats{count = AC, vals = AV}}) -> + if + AC*5 < C -> + false; + true -> + AVC = AC div min(maps:size(AV)*2, 10), + AVA = [N || {N, C2} <- maps:to_list(AV), C2 > AVC], + {true, {AN, AVA}} + end + end, maps:to_list(A)), + Text = [TE || {TE, TC} <- maps:to_list(T), TC > C/2], + {Ns, El, Attrs, Text} + end, SStats). + +analyze_elements(Elements, Stats, PName, PNS, J1, J2) -> + lists:foldl(fun analyze_element/2, Stats, lists:map(fun(V) -> {V, PName, PNS, J1, J2} end, Elements)). + +maps_update(Key, F, InitVal, Map) -> + case maps:is_key(Key, Map) of + true -> + maps:update_with(Key, F, Map); + _ -> + maps:put(Key, F(InitVal), Map) + end. + +analyze_element({{xmlcdata, Data}, PName, PNS, _J1, _J2}, {ElCount, Stats}) -> + Stats2 = maps_update(<<PNS/binary, "<", PName/binary>>, + fun(#el_stats{text_stats = TS} = E) -> + TS2 = maps_update(Data, fun(C) -> C + 1 end, 0, TS), + E#el_stats{text_stats = TS2} + end, #el_stats{}, Stats), + {ElCount, Stats2}; +analyze_element({#xmlel{name = Name, attrs = Attrs, children = Children}, _PName, PNS, J1, J2}, {ElCount, Stats}) -> + XMLNS = case lists:keyfind(<<"xmlns">>, 1, Attrs) of + {_, NS} -> + NS; + false -> + PNS + end, + NStats = maps_update(<<XMLNS/binary, "<", Name/binary>>, + fun(#el_stats{count = C, empty_count = EC, only_text_count = TC, attrs = A} = ES) -> + A2 = lists:foldl( + fun({<<"xmlns">>, _}, AMap) -> + AMap; + ({AName, AVal}, AMap) -> + J1S = size(J1), + J2S = size(J2), + AVal2 = case AVal of + J1 -> + j1; + J2 -> + j2; + <<J1:J1S/binary, _Rest/binary>> -> + {j1}; + <<J2:J2S/binary, _Rest/binary>> -> + {j2}; + Other -> + Other + end, + maps_update(AName, fun(#attr_stats{count = AC, vals = AV}) -> + AV2 = maps_update(AVal2, fun(C2) -> C2 + 1 end, 0, AV), + #attr_stats{count = AC + 1, vals = AV2} + end, #attr_stats{}, AMap) + end, A, Attrs), + ES#el_stats{count = C + 1, + empty_count = if Children == [] -> EC + 1; true -> + EC end, + only_text_count = case Children of [{xmlcdata, _}] -> TC + 1; _ -> TC end, + attrs = A2} + end, #el_stats{}, Stats), + analyze_elements(Children, {ElCount + 1, NStats}, Name, XMLNS, J1, J2). diff --git a/tools/xmpp_codec.erl b/tools/xmpp_codec.erl deleted file mode 100644 index 1d5cd88e0..000000000 --- a/tools/xmpp_codec.erl +++ /dev/null @@ -1,18659 +0,0 @@ -%% Created automatically by XML generator (xml_gen.erl) -%% Source: xmpp_codec.spec - --module(xmpp_codec). - --compile({nowarn_unused_function, - [{dec_int, 3}, {dec_int, 1}, {dec_enum, 2}, - {enc_int, 1}, {get_attr, 2}, {enc_enum, 1}]}). - --export([pp/1, format_error/1, decode/1, decode/2, - is_known_tag/1, encode/1, get_ns/1]). - -decode(_el) -> decode(_el, []). - -decode({xmlel, _name, _attrs, _} = _el, Opts) -> - IgnoreEls = proplists:get_bool(ignore_els, Opts), - case {_name, get_attr(<<"xmlns">>, _attrs)} of - {<<"failed">>, <<"urn:xmpp:sm:2">>} -> - decode_sm_failed(<<"urn:xmpp:sm:2">>, IgnoreEls, _el); - {<<"failed">>, <<"urn:xmpp:sm:3">>} -> - decode_sm_failed(<<"urn:xmpp:sm:3">>, IgnoreEls, _el); - {<<"a">>, <<"urn:xmpp:sm:2">>} -> - decode_sm_a(<<"urn:xmpp:sm:2">>, IgnoreEls, _el); - {<<"a">>, <<"urn:xmpp:sm:3">>} -> - decode_sm_a(<<"urn:xmpp:sm:3">>, IgnoreEls, _el); - {<<"r">>, <<"urn:xmpp:sm:2">>} -> - decode_sm_r(<<"urn:xmpp:sm:2">>, IgnoreEls, _el); - {<<"r">>, <<"urn:xmpp:sm:3">>} -> - decode_sm_r(<<"urn:xmpp:sm:3">>, IgnoreEls, _el); - {<<"resumed">>, <<"urn:xmpp:sm:2">>} -> - decode_sm_resumed(<<"urn:xmpp:sm:2">>, IgnoreEls, _el); - {<<"resumed">>, <<"urn:xmpp:sm:3">>} -> - decode_sm_resumed(<<"urn:xmpp:sm:3">>, IgnoreEls, _el); - {<<"resume">>, <<"urn:xmpp:sm:2">>} -> - decode_sm_resume(<<"urn:xmpp:sm:2">>, IgnoreEls, _el); - {<<"resume">>, <<"urn:xmpp:sm:3">>} -> - decode_sm_resume(<<"urn:xmpp:sm:3">>, IgnoreEls, _el); - {<<"enabled">>, <<"urn:xmpp:sm:2">>} -> - decode_sm_enabled(<<"urn:xmpp:sm:2">>, IgnoreEls, _el); - {<<"enabled">>, <<"urn:xmpp:sm:3">>} -> - decode_sm_enabled(<<"urn:xmpp:sm:3">>, IgnoreEls, _el); - {<<"enable">>, <<"urn:xmpp:sm:2">>} -> - decode_sm_enable(<<"urn:xmpp:sm:2">>, IgnoreEls, _el); - {<<"enable">>, <<"urn:xmpp:sm:3">>} -> - decode_sm_enable(<<"urn:xmpp:sm:3">>, IgnoreEls, _el); - {<<"sm">>, <<"urn:xmpp:sm:2">>} -> - decode_feature_sm(<<"urn:xmpp:sm:2">>, IgnoreEls, _el); - {<<"sm">>, <<"urn:xmpp:sm:3">>} -> - decode_feature_sm(<<"urn:xmpp:sm:3">>, IgnoreEls, _el); - {<<"inactive">>, <<"urn:xmpp:csi:0">>} -> - decode_csi_inactive(<<"urn:xmpp:csi:0">>, IgnoreEls, - _el); - {<<"active">>, <<"urn:xmpp:csi:0">>} -> - decode_csi_active(<<"urn:xmpp:csi:0">>, IgnoreEls, _el); - {<<"csi">>, <<"urn:xmpp:csi:0">>} -> - decode_feature_csi(<<"urn:xmpp:csi:0">>, IgnoreEls, - _el); - {<<"sent">>, <<"urn:xmpp:carbons:2">>} -> - decode_carbons_sent(<<"urn:xmpp:carbons:2">>, IgnoreEls, - _el); - {<<"received">>, <<"urn:xmpp:carbons:2">>} -> - decode_carbons_received(<<"urn:xmpp:carbons:2">>, - IgnoreEls, _el); - {<<"private">>, <<"urn:xmpp:carbons:2">>} -> - decode_carbons_private(<<"urn:xmpp:carbons:2">>, - IgnoreEls, _el); - {<<"enable">>, <<"urn:xmpp:carbons:2">>} -> - decode_carbons_enable(<<"urn:xmpp:carbons:2">>, - IgnoreEls, _el); - {<<"disable">>, <<"urn:xmpp:carbons:2">>} -> - decode_carbons_disable(<<"urn:xmpp:carbons:2">>, - IgnoreEls, _el); - {<<"forwarded">>, <<"urn:xmpp:forward:0">>} -> - decode_forwarded(<<"urn:xmpp:forward:0">>, IgnoreEls, - _el); - {<<"x">>, <<"http://jabber.org/protocol/muc">>} -> - decode_muc(<<"http://jabber.org/protocol/muc">>, - IgnoreEls, _el); - {<<"query">>, - <<"http://jabber.org/protocol/muc#admin">>} -> - decode_muc_admin(<<"http://jabber.org/protocol/muc#admin">>, - IgnoreEls, _el); - {<<"reason">>, - <<"http://jabber.org/protocol/muc#admin">>} -> - decode_muc_admin_reason(<<"http://jabber.org/protocol/muc#admin">>, - IgnoreEls, _el); - {<<"continue">>, - <<"http://jabber.org/protocol/muc#admin">>} -> - decode_muc_admin_continue(<<"http://jabber.org/protocol/muc#admin">>, - IgnoreEls, _el); - {<<"actor">>, - <<"http://jabber.org/protocol/muc#admin">>} -> - decode_muc_admin_actor(<<"http://jabber.org/protocol/muc#admin">>, - IgnoreEls, _el); - {<<"item">>, - <<"http://jabber.org/protocol/muc#admin">>} -> - decode_muc_admin_item(<<"http://jabber.org/protocol/muc#admin">>, - IgnoreEls, _el); - {<<"query">>, - <<"http://jabber.org/protocol/muc#owner">>} -> - decode_muc_owner(<<"http://jabber.org/protocol/muc#owner">>, - IgnoreEls, _el); - {<<"destroy">>, - <<"http://jabber.org/protocol/muc#owner">>} -> - decode_muc_owner_destroy(<<"http://jabber.org/protocol/muc#owner">>, - IgnoreEls, _el); - {<<"reason">>, - <<"http://jabber.org/protocol/muc#owner">>} -> - decode_muc_owner_reason(<<"http://jabber.org/protocol/muc#owner">>, - IgnoreEls, _el); - {<<"password">>, - <<"http://jabber.org/protocol/muc#owner">>} -> - decode_muc_owner_password(<<"http://jabber.org/protocol/muc#owner">>, - IgnoreEls, _el); - {<<"x">>, <<"http://jabber.org/protocol/muc#user">>} -> - decode_muc_user(<<"http://jabber.org/protocol/muc#user">>, - IgnoreEls, _el); - {<<"item">>, - <<"http://jabber.org/protocol/muc#user">>} -> - decode_muc_user_item(<<"http://jabber.org/protocol/muc#user">>, - IgnoreEls, _el); - {<<"status">>, - <<"http://jabber.org/protocol/muc#user">>} -> - decode_muc_user_status(<<"http://jabber.org/protocol/muc#user">>, - IgnoreEls, _el); - {<<"continue">>, - <<"http://jabber.org/protocol/muc#user">>} -> - decode_muc_user_continue(<<"http://jabber.org/protocol/muc#user">>, - IgnoreEls, _el); - {<<"actor">>, - <<"http://jabber.org/protocol/muc#user">>} -> - decode_muc_user_actor(<<"http://jabber.org/protocol/muc#user">>, - IgnoreEls, _el); - {<<"invite">>, - <<"http://jabber.org/protocol/muc#user">>} -> - decode_muc_user_invite(<<"http://jabber.org/protocol/muc#user">>, - IgnoreEls, _el); - {<<"destroy">>, - <<"http://jabber.org/protocol/muc#user">>} -> - decode_muc_user_destroy(<<"http://jabber.org/protocol/muc#user">>, - IgnoreEls, _el); - {<<"decline">>, - <<"http://jabber.org/protocol/muc#user">>} -> - decode_muc_user_decline(<<"http://jabber.org/protocol/muc#user">>, - IgnoreEls, _el); - {<<"reason">>, - <<"http://jabber.org/protocol/muc#user">>} -> - decode_muc_user_reason(<<"http://jabber.org/protocol/muc#user">>, - IgnoreEls, _el); - {<<"history">>, <<"http://jabber.org/protocol/muc">>} -> - decode_muc_history(<<"http://jabber.org/protocol/muc">>, - IgnoreEls, _el); - {<<"query">>, - <<"http://jabber.org/protocol/bytestreams">>} -> - decode_bytestreams(<<"http://jabber.org/protocol/bytestreams">>, - IgnoreEls, _el); - {<<"activate">>, - <<"http://jabber.org/protocol/bytestreams">>} -> - decode_bytestreams_activate(<<"http://jabber.org/protocol/bytestreams">>, - IgnoreEls, _el); - {<<"streamhost-used">>, - <<"http://jabber.org/protocol/bytestreams">>} -> - decode_bytestreams_streamhost_used(<<"http://jabber.org/protocol/bytestreams">>, - IgnoreEls, _el); - {<<"streamhost">>, - <<"http://jabber.org/protocol/bytestreams">>} -> - decode_bytestreams_streamhost(<<"http://jabber.org/protocol/bytestreams">>, - IgnoreEls, _el); - {<<"x">>, <<"jabber:x:delay">>} -> - decode_legacy_delay(<<"jabber:x:delay">>, IgnoreEls, - _el); - {<<"delay">>, <<"urn:xmpp:delay">>} -> - decode_delay(<<"urn:xmpp:delay">>, IgnoreEls, _el); - {<<"paused">>, - <<"http://jabber.org/protocol/chatstates">>} -> - decode_chatstate_paused(<<"http://jabber.org/protocol/chatstates">>, - IgnoreEls, _el); - {<<"inactive">>, - <<"http://jabber.org/protocol/chatstates">>} -> - decode_chatstate_inactive(<<"http://jabber.org/protocol/chatstates">>, - IgnoreEls, _el); - {<<"gone">>, - <<"http://jabber.org/protocol/chatstates">>} -> - decode_chatstate_gone(<<"http://jabber.org/protocol/chatstates">>, - IgnoreEls, _el); - {<<"composing">>, - <<"http://jabber.org/protocol/chatstates">>} -> - decode_chatstate_composing(<<"http://jabber.org/protocol/chatstates">>, - IgnoreEls, _el); - {<<"active">>, - <<"http://jabber.org/protocol/chatstates">>} -> - decode_chatstate_active(<<"http://jabber.org/protocol/chatstates">>, - IgnoreEls, _el); - {<<"headers">>, - <<"http://jabber.org/protocol/shim">>} -> - decode_shim_headers(<<"http://jabber.org/protocol/shim">>, - IgnoreEls, _el); - {<<"header">>, <<"http://jabber.org/protocol/shim">>} -> - decode_shim_header(<<"http://jabber.org/protocol/shim">>, - IgnoreEls, _el); - {<<"pubsub">>, - <<"http://jabber.org/protocol/pubsub">>} -> - decode_pubsub(<<"http://jabber.org/protocol/pubsub">>, - IgnoreEls, _el); - {<<"retract">>, - <<"http://jabber.org/protocol/pubsub">>} -> - decode_pubsub_retract(<<"http://jabber.org/protocol/pubsub">>, - IgnoreEls, _el); - {<<"options">>, - <<"http://jabber.org/protocol/pubsub">>} -> - decode_pubsub_options(<<"http://jabber.org/protocol/pubsub">>, - IgnoreEls, _el); - {<<"publish">>, - <<"http://jabber.org/protocol/pubsub">>} -> - decode_pubsub_publish(<<"http://jabber.org/protocol/pubsub">>, - IgnoreEls, _el); - {<<"unsubscribe">>, - <<"http://jabber.org/protocol/pubsub">>} -> - decode_pubsub_unsubscribe(<<"http://jabber.org/protocol/pubsub">>, - IgnoreEls, _el); - {<<"subscribe">>, - <<"http://jabber.org/protocol/pubsub">>} -> - decode_pubsub_subscribe(<<"http://jabber.org/protocol/pubsub">>, - IgnoreEls, _el); - {<<"affiliations">>, - <<"http://jabber.org/protocol/pubsub">>} -> - decode_pubsub_affiliations(<<"http://jabber.org/protocol/pubsub">>, - IgnoreEls, _el); - {<<"subscriptions">>, - <<"http://jabber.org/protocol/pubsub">>} -> - decode_pubsub_subscriptions(<<"http://jabber.org/protocol/pubsub">>, - IgnoreEls, _el); - {<<"event">>, - <<"http://jabber.org/protocol/pubsub#event">>} -> - decode_pubsub_event(<<"http://jabber.org/protocol/pubsub#event">>, - IgnoreEls, _el); - {<<"items">>, - <<"http://jabber.org/protocol/pubsub#event">>} -> - decode_pubsub_event_items(<<"http://jabber.org/protocol/pubsub#event">>, - IgnoreEls, _el); - {<<"item">>, - <<"http://jabber.org/protocol/pubsub#event">>} -> - decode_pubsub_event_item(<<"http://jabber.org/protocol/pubsub#event">>, - IgnoreEls, _el); - {<<"retract">>, - <<"http://jabber.org/protocol/pubsub#event">>} -> - decode_pubsub_event_retract(<<"http://jabber.org/protocol/pubsub#event">>, - IgnoreEls, _el); - {<<"items">>, - <<"http://jabber.org/protocol/pubsub">>} -> - decode_pubsub_items(<<"http://jabber.org/protocol/pubsub">>, - IgnoreEls, _el); - {<<"item">>, <<"http://jabber.org/protocol/pubsub">>} -> - decode_pubsub_item(<<"http://jabber.org/protocol/pubsub">>, - IgnoreEls, _el); - {<<"affiliation">>, - <<"http://jabber.org/protocol/pubsub">>} -> - decode_pubsub_affiliation(<<"http://jabber.org/protocol/pubsub">>, - IgnoreEls, _el); - {<<"subscription">>, - <<"http://jabber.org/protocol/pubsub">>} -> - decode_pubsub_subscription(<<"http://jabber.org/protocol/pubsub">>, - IgnoreEls, _el); - {<<"x">>, <<"jabber:x:data">>} -> - decode_xdata(<<"jabber:x:data">>, IgnoreEls, _el); - {<<"item">>, <<"jabber:x:data">>} -> - decode_xdata_item(<<"jabber:x:data">>, IgnoreEls, _el); - {<<"reported">>, <<"jabber:x:data">>} -> - decode_xdata_reported(<<"jabber:x:data">>, IgnoreEls, - _el); - {<<"title">>, <<"jabber:x:data">>} -> - decode_xdata_title(<<"jabber:x:data">>, IgnoreEls, _el); - {<<"instructions">>, <<"jabber:x:data">>} -> - decode_xdata_instructions(<<"jabber:x:data">>, - IgnoreEls, _el); - {<<"field">>, <<"jabber:x:data">>} -> - decode_xdata_field(<<"jabber:x:data">>, IgnoreEls, _el); - {<<"option">>, <<"jabber:x:data">>} -> - decode_xdata_field_option(<<"jabber:x:data">>, - IgnoreEls, _el); - {<<"value">>, <<"jabber:x:data">>} -> - decode_xdata_field_value(<<"jabber:x:data">>, IgnoreEls, - _el); - {<<"desc">>, <<"jabber:x:data">>} -> - decode_xdata_field_desc(<<"jabber:x:data">>, IgnoreEls, - _el); - {<<"required">>, <<"jabber:x:data">>} -> - decode_xdata_field_required(<<"jabber:x:data">>, - IgnoreEls, _el); - {<<"x">>, <<"vcard-temp:x:update">>} -> - decode_vcard_xupdate(<<"vcard-temp:x:update">>, - IgnoreEls, _el); - {<<"photo">>, <<"vcard-temp:x:update">>} -> - decode_vcard_xupdate_photo(<<"vcard-temp:x:update">>, - IgnoreEls, _el); - {<<"vCard">>, <<"vcard-temp">>} -> - decode_vcard(<<"vcard-temp">>, IgnoreEls, _el); - {<<"CLASS">>, <<"vcard-temp">>} -> - decode_vcard_CLASS(<<"vcard-temp">>, IgnoreEls, _el); - {<<"CATEGORIES">>, <<"vcard-temp">>} -> - decode_vcard_CATEGORIES(<<"vcard-temp">>, IgnoreEls, - _el); - {<<"KEY">>, <<"vcard-temp">>} -> - decode_vcard_KEY(<<"vcard-temp">>, IgnoreEls, _el); - {<<"SOUND">>, <<"vcard-temp">>} -> - decode_vcard_SOUND(<<"vcard-temp">>, IgnoreEls, _el); - {<<"ORG">>, <<"vcard-temp">>} -> - decode_vcard_ORG(<<"vcard-temp">>, IgnoreEls, _el); - {<<"PHOTO">>, <<"vcard-temp">>} -> - decode_vcard_PHOTO(<<"vcard-temp">>, IgnoreEls, _el); - {<<"LOGO">>, <<"vcard-temp">>} -> - decode_vcard_LOGO(<<"vcard-temp">>, IgnoreEls, _el); - {<<"BINVAL">>, <<"vcard-temp">>} -> - decode_vcard_BINVAL(<<"vcard-temp">>, IgnoreEls, _el); - {<<"GEO">>, <<"vcard-temp">>} -> - decode_vcard_GEO(<<"vcard-temp">>, IgnoreEls, _el); - {<<"EMAIL">>, <<"vcard-temp">>} -> - decode_vcard_EMAIL(<<"vcard-temp">>, IgnoreEls, _el); - {<<"TEL">>, <<"vcard-temp">>} -> - decode_vcard_TEL(<<"vcard-temp">>, IgnoreEls, _el); - {<<"LABEL">>, <<"vcard-temp">>} -> - decode_vcard_LABEL(<<"vcard-temp">>, IgnoreEls, _el); - {<<"ADR">>, <<"vcard-temp">>} -> - decode_vcard_ADR(<<"vcard-temp">>, IgnoreEls, _el); - {<<"N">>, <<"vcard-temp">>} -> - decode_vcard_N(<<"vcard-temp">>, IgnoreEls, _el); - {<<"CONFIDENTIAL">>, <<"vcard-temp">>} -> - decode_vcard_CONFIDENTIAL(<<"vcard-temp">>, IgnoreEls, - _el); - {<<"PRIVATE">>, <<"vcard-temp">>} -> - decode_vcard_PRIVATE(<<"vcard-temp">>, IgnoreEls, _el); - {<<"PUBLIC">>, <<"vcard-temp">>} -> - decode_vcard_PUBLIC(<<"vcard-temp">>, IgnoreEls, _el); - {<<"EXTVAL">>, <<"vcard-temp">>} -> - decode_vcard_EXTVAL(<<"vcard-temp">>, IgnoreEls, _el); - {<<"TYPE">>, <<"vcard-temp">>} -> - decode_vcard_TYPE(<<"vcard-temp">>, IgnoreEls, _el); - {<<"DESC">>, <<"vcard-temp">>} -> - decode_vcard_DESC(<<"vcard-temp">>, IgnoreEls, _el); - {<<"URL">>, <<"vcard-temp">>} -> - decode_vcard_URL(<<"vcard-temp">>, IgnoreEls, _el); - {<<"UID">>, <<"vcard-temp">>} -> - decode_vcard_UID(<<"vcard-temp">>, IgnoreEls, _el); - {<<"SORT-STRING">>, <<"vcard-temp">>} -> - decode_vcard_SORT_STRING(<<"vcard-temp">>, IgnoreEls, - _el); - {<<"REV">>, <<"vcard-temp">>} -> - decode_vcard_REV(<<"vcard-temp">>, IgnoreEls, _el); - {<<"PRODID">>, <<"vcard-temp">>} -> - decode_vcard_PRODID(<<"vcard-temp">>, IgnoreEls, _el); - {<<"NOTE">>, <<"vcard-temp">>} -> - decode_vcard_NOTE(<<"vcard-temp">>, IgnoreEls, _el); - {<<"KEYWORD">>, <<"vcard-temp">>} -> - decode_vcard_KEYWORD(<<"vcard-temp">>, IgnoreEls, _el); - {<<"ROLE">>, <<"vcard-temp">>} -> - decode_vcard_ROLE(<<"vcard-temp">>, IgnoreEls, _el); - {<<"TITLE">>, <<"vcard-temp">>} -> - decode_vcard_TITLE(<<"vcard-temp">>, IgnoreEls, _el); - {<<"TZ">>, <<"vcard-temp">>} -> - decode_vcard_TZ(<<"vcard-temp">>, IgnoreEls, _el); - {<<"MAILER">>, <<"vcard-temp">>} -> - decode_vcard_MAILER(<<"vcard-temp">>, IgnoreEls, _el); - {<<"JABBERID">>, <<"vcard-temp">>} -> - decode_vcard_JABBERID(<<"vcard-temp">>, IgnoreEls, _el); - {<<"BDAY">>, <<"vcard-temp">>} -> - decode_vcard_BDAY(<<"vcard-temp">>, IgnoreEls, _el); - {<<"NICKNAME">>, <<"vcard-temp">>} -> - decode_vcard_NICKNAME(<<"vcard-temp">>, IgnoreEls, _el); - {<<"FN">>, <<"vcard-temp">>} -> - decode_vcard_FN(<<"vcard-temp">>, IgnoreEls, _el); - {<<"VERSION">>, <<"vcard-temp">>} -> - decode_vcard_VERSION(<<"vcard-temp">>, IgnoreEls, _el); - {<<"CRED">>, <<"vcard-temp">>} -> - decode_vcard_CRED(<<"vcard-temp">>, IgnoreEls, _el); - {<<"PHONETIC">>, <<"vcard-temp">>} -> - decode_vcard_PHONETIC(<<"vcard-temp">>, IgnoreEls, _el); - {<<"ORGUNIT">>, <<"vcard-temp">>} -> - decode_vcard_ORGUNIT(<<"vcard-temp">>, IgnoreEls, _el); - {<<"ORGNAME">>, <<"vcard-temp">>} -> - decode_vcard_ORGNAME(<<"vcard-temp">>, IgnoreEls, _el); - {<<"LON">>, <<"vcard-temp">>} -> - decode_vcard_LON(<<"vcard-temp">>, IgnoreEls, _el); - {<<"LAT">>, <<"vcard-temp">>} -> - decode_vcard_LAT(<<"vcard-temp">>, IgnoreEls, _el); - {<<"USERID">>, <<"vcard-temp">>} -> - decode_vcard_USERID(<<"vcard-temp">>, IgnoreEls, _el); - {<<"NUMBER">>, <<"vcard-temp">>} -> - decode_vcard_NUMBER(<<"vcard-temp">>, IgnoreEls, _el); - {<<"LINE">>, <<"vcard-temp">>} -> - decode_vcard_LINE(<<"vcard-temp">>, IgnoreEls, _el); - {<<"CTRY">>, <<"vcard-temp">>} -> - decode_vcard_CTRY(<<"vcard-temp">>, IgnoreEls, _el); - {<<"PCODE">>, <<"vcard-temp">>} -> - decode_vcard_PCODE(<<"vcard-temp">>, IgnoreEls, _el); - {<<"REGION">>, <<"vcard-temp">>} -> - decode_vcard_REGION(<<"vcard-temp">>, IgnoreEls, _el); - {<<"LOCALITY">>, <<"vcard-temp">>} -> - decode_vcard_LOCALITY(<<"vcard-temp">>, IgnoreEls, _el); - {<<"STREET">>, <<"vcard-temp">>} -> - decode_vcard_STREET(<<"vcard-temp">>, IgnoreEls, _el); - {<<"EXTADD">>, <<"vcard-temp">>} -> - decode_vcard_EXTADD(<<"vcard-temp">>, IgnoreEls, _el); - {<<"POBOX">>, <<"vcard-temp">>} -> - decode_vcard_POBOX(<<"vcard-temp">>, IgnoreEls, _el); - {<<"SUFFIX">>, <<"vcard-temp">>} -> - decode_vcard_SUFFIX(<<"vcard-temp">>, IgnoreEls, _el); - {<<"PREFIX">>, <<"vcard-temp">>} -> - decode_vcard_PREFIX(<<"vcard-temp">>, IgnoreEls, _el); - {<<"MIDDLE">>, <<"vcard-temp">>} -> - decode_vcard_MIDDLE(<<"vcard-temp">>, IgnoreEls, _el); - {<<"GIVEN">>, <<"vcard-temp">>} -> - decode_vcard_GIVEN(<<"vcard-temp">>, IgnoreEls, _el); - {<<"FAMILY">>, <<"vcard-temp">>} -> - decode_vcard_FAMILY(<<"vcard-temp">>, IgnoreEls, _el); - {<<"X400">>, <<"vcard-temp">>} -> - decode_vcard_X400(<<"vcard-temp">>, IgnoreEls, _el); - {<<"INTERNET">>, <<"vcard-temp">>} -> - decode_vcard_INTERNET(<<"vcard-temp">>, IgnoreEls, _el); - {<<"PREF">>, <<"vcard-temp">>} -> - decode_vcard_PREF(<<"vcard-temp">>, IgnoreEls, _el); - {<<"INTL">>, <<"vcard-temp">>} -> - decode_vcard_INTL(<<"vcard-temp">>, IgnoreEls, _el); - {<<"DOM">>, <<"vcard-temp">>} -> - decode_vcard_DOM(<<"vcard-temp">>, IgnoreEls, _el); - {<<"PARCEL">>, <<"vcard-temp">>} -> - decode_vcard_PARCEL(<<"vcard-temp">>, IgnoreEls, _el); - {<<"POSTAL">>, <<"vcard-temp">>} -> - decode_vcard_POSTAL(<<"vcard-temp">>, IgnoreEls, _el); - {<<"PCS">>, <<"vcard-temp">>} -> - decode_vcard_PCS(<<"vcard-temp">>, IgnoreEls, _el); - {<<"ISDN">>, <<"vcard-temp">>} -> - decode_vcard_ISDN(<<"vcard-temp">>, IgnoreEls, _el); - {<<"MODEM">>, <<"vcard-temp">>} -> - decode_vcard_MODEM(<<"vcard-temp">>, IgnoreEls, _el); - {<<"BBS">>, <<"vcard-temp">>} -> - decode_vcard_BBS(<<"vcard-temp">>, IgnoreEls, _el); - {<<"VIDEO">>, <<"vcard-temp">>} -> - decode_vcard_VIDEO(<<"vcard-temp">>, IgnoreEls, _el); - {<<"CELL">>, <<"vcard-temp">>} -> - decode_vcard_CELL(<<"vcard-temp">>, IgnoreEls, _el); - {<<"MSG">>, <<"vcard-temp">>} -> - decode_vcard_MSG(<<"vcard-temp">>, IgnoreEls, _el); - {<<"PAGER">>, <<"vcard-temp">>} -> - decode_vcard_PAGER(<<"vcard-temp">>, IgnoreEls, _el); - {<<"FAX">>, <<"vcard-temp">>} -> - decode_vcard_FAX(<<"vcard-temp">>, IgnoreEls, _el); - {<<"VOICE">>, <<"vcard-temp">>} -> - decode_vcard_VOICE(<<"vcard-temp">>, IgnoreEls, _el); - {<<"WORK">>, <<"vcard-temp">>} -> - decode_vcard_WORK(<<"vcard-temp">>, IgnoreEls, _el); - {<<"HOME">>, <<"vcard-temp">>} -> - decode_vcard_HOME(<<"vcard-temp">>, IgnoreEls, _el); - {<<"stream:error">>, - <<"http://etherx.jabber.org/streams">>} -> - decode_stream_error(<<"http://etherx.jabber.org/streams">>, - IgnoreEls, _el); - {<<"unsupported-version">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - decode_stream_error_unsupported_version(<<"urn:ietf:params:xml:ns:xmpp-streams">>, - IgnoreEls, _el); - {<<"unsupported-stanza-type">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - decode_stream_error_unsupported_stanza_type(<<"urn:ietf:params:xml:ns:xmpp-streams">>, - IgnoreEls, _el); - {<<"unsupported-encoding">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - decode_stream_error_unsupported_encoding(<<"urn:ietf:params:xml:ns:xmpp-streams">>, - IgnoreEls, _el); - {<<"undefined-condition">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - decode_stream_error_undefined_condition(<<"urn:ietf:params:xml:ns:xmpp-streams">>, - IgnoreEls, _el); - {<<"system-shutdown">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - decode_stream_error_system_shutdown(<<"urn:ietf:params:xml:ns:xmpp-streams">>, - IgnoreEls, _el); - {<<"see-other-host">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - decode_stream_error_see_other_host(<<"urn:ietf:params:xml:ns:xmpp-streams">>, - IgnoreEls, _el); - {<<"restricted-xml">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - decode_stream_error_restricted_xml(<<"urn:ietf:params:xml:ns:xmpp-streams">>, - IgnoreEls, _el); - {<<"resource-constraint">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - decode_stream_error_resource_constraint(<<"urn:ietf:params:xml:ns:xmpp-streams">>, - IgnoreEls, _el); - {<<"reset">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - decode_stream_error_reset(<<"urn:ietf:params:xml:ns:xmpp-streams">>, - IgnoreEls, _el); - {<<"remote-connection-failed">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - decode_stream_error_remote_connection_failed(<<"urn:ietf:params:xml:ns:xmpp-streams">>, - IgnoreEls, _el); - {<<"policy-violation">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - decode_stream_error_policy_violation(<<"urn:ietf:params:xml:ns:xmpp-streams">>, - IgnoreEls, _el); - {<<"not-well-formed">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - decode_stream_error_not_well_formed(<<"urn:ietf:params:xml:ns:xmpp-streams">>, - IgnoreEls, _el); - {<<"not-authorized">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - decode_stream_error_not_authorized(<<"urn:ietf:params:xml:ns:xmpp-streams">>, - IgnoreEls, _el); - {<<"invalid-xml">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - decode_stream_error_invalid_xml(<<"urn:ietf:params:xml:ns:xmpp-streams">>, - IgnoreEls, _el); - {<<"invalid-namespace">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - decode_stream_error_invalid_namespace(<<"urn:ietf:params:xml:ns:xmpp-streams">>, - IgnoreEls, _el); - {<<"invalid-id">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - decode_stream_error_invalid_id(<<"urn:ietf:params:xml:ns:xmpp-streams">>, - IgnoreEls, _el); - {<<"invalid-from">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - decode_stream_error_invalid_from(<<"urn:ietf:params:xml:ns:xmpp-streams">>, - IgnoreEls, _el); - {<<"internal-server-error">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - decode_stream_error_internal_server_error(<<"urn:ietf:params:xml:ns:xmpp-streams">>, - IgnoreEls, _el); - {<<"improper-addressing">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - decode_stream_error_improper_addressing(<<"urn:ietf:params:xml:ns:xmpp-streams">>, - IgnoreEls, _el); - {<<"host-unknown">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - decode_stream_error_host_unknown(<<"urn:ietf:params:xml:ns:xmpp-streams">>, - IgnoreEls, _el); - {<<"host-gone">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - decode_stream_error_host_gone(<<"urn:ietf:params:xml:ns:xmpp-streams">>, - IgnoreEls, _el); - {<<"connection-timeout">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - decode_stream_error_connection_timeout(<<"urn:ietf:params:xml:ns:xmpp-streams">>, - IgnoreEls, _el); - {<<"conflict">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - decode_stream_error_conflict(<<"urn:ietf:params:xml:ns:xmpp-streams">>, - IgnoreEls, _el); - {<<"bad-namespace-prefix">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - decode_stream_error_bad_namespace_prefix(<<"urn:ietf:params:xml:ns:xmpp-streams">>, - IgnoreEls, _el); - {<<"bad-format">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - decode_stream_error_bad_format(<<"urn:ietf:params:xml:ns:xmpp-streams">>, - IgnoreEls, _el); - {<<"text">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - decode_stream_error_text(<<"urn:ietf:params:xml:ns:xmpp-streams">>, - IgnoreEls, _el); - {<<"time">>, <<"urn:xmpp:time">>} -> - decode_time(<<"urn:xmpp:time">>, IgnoreEls, _el); - {<<"tzo">>, <<"urn:xmpp:time">>} -> - decode_time_tzo(<<"urn:xmpp:time">>, IgnoreEls, _el); - {<<"utc">>, <<"urn:xmpp:time">>} -> - decode_time_utc(<<"urn:xmpp:time">>, IgnoreEls, _el); - {<<"ping">>, <<"urn:xmpp:ping">>} -> - decode_ping(<<"urn:xmpp:ping">>, IgnoreEls, _el); - {<<"session">>, - <<"urn:ietf:params:xml:ns:xmpp-session">>} -> - decode_session(<<"urn:ietf:params:xml:ns:xmpp-session">>, - IgnoreEls, _el); - {<<"query">>, <<"jabber:iq:register">>} -> - decode_register(<<"jabber:iq:register">>, IgnoreEls, - _el); - {<<"key">>, <<"jabber:iq:register">>} -> - decode_register_key(<<"jabber:iq:register">>, IgnoreEls, - _el); - {<<"text">>, <<"jabber:iq:register">>} -> - decode_register_text(<<"jabber:iq:register">>, - IgnoreEls, _el); - {<<"misc">>, <<"jabber:iq:register">>} -> - decode_register_misc(<<"jabber:iq:register">>, - IgnoreEls, _el); - {<<"date">>, <<"jabber:iq:register">>} -> - decode_register_date(<<"jabber:iq:register">>, - IgnoreEls, _el); - {<<"url">>, <<"jabber:iq:register">>} -> - decode_register_url(<<"jabber:iq:register">>, IgnoreEls, - _el); - {<<"phone">>, <<"jabber:iq:register">>} -> - decode_register_phone(<<"jabber:iq:register">>, - IgnoreEls, _el); - {<<"zip">>, <<"jabber:iq:register">>} -> - decode_register_zip(<<"jabber:iq:register">>, IgnoreEls, - _el); - {<<"state">>, <<"jabber:iq:register">>} -> - decode_register_state(<<"jabber:iq:register">>, - IgnoreEls, _el); - {<<"city">>, <<"jabber:iq:register">>} -> - decode_register_city(<<"jabber:iq:register">>, - IgnoreEls, _el); - {<<"address">>, <<"jabber:iq:register">>} -> - decode_register_address(<<"jabber:iq:register">>, - IgnoreEls, _el); - {<<"email">>, <<"jabber:iq:register">>} -> - decode_register_email(<<"jabber:iq:register">>, - IgnoreEls, _el); - {<<"last">>, <<"jabber:iq:register">>} -> - decode_register_last(<<"jabber:iq:register">>, - IgnoreEls, _el); - {<<"first">>, <<"jabber:iq:register">>} -> - decode_register_first(<<"jabber:iq:register">>, - IgnoreEls, _el); - {<<"name">>, <<"jabber:iq:register">>} -> - decode_register_name(<<"jabber:iq:register">>, - IgnoreEls, _el); - {<<"password">>, <<"jabber:iq:register">>} -> - decode_register_password(<<"jabber:iq:register">>, - IgnoreEls, _el); - {<<"nick">>, <<"jabber:iq:register">>} -> - decode_register_nick(<<"jabber:iq:register">>, - IgnoreEls, _el); - {<<"username">>, <<"jabber:iq:register">>} -> - decode_register_username(<<"jabber:iq:register">>, - IgnoreEls, _el); - {<<"instructions">>, <<"jabber:iq:register">>} -> - decode_register_instructions(<<"jabber:iq:register">>, - IgnoreEls, _el); - {<<"remove">>, <<"jabber:iq:register">>} -> - decode_register_remove(<<"jabber:iq:register">>, - IgnoreEls, _el); - {<<"registered">>, <<"jabber:iq:register">>} -> - decode_register_registered(<<"jabber:iq:register">>, - IgnoreEls, _el); - {<<"register">>, - <<"http://jabber.org/features/iq-register">>} -> - decode_feature_register(<<"http://jabber.org/features/iq-register">>, - IgnoreEls, _el); - {<<"c">>, <<"http://jabber.org/protocol/caps">>} -> - decode_caps(<<"http://jabber.org/protocol/caps">>, - IgnoreEls, _el); - {<<"ack">>, <<"p1:ack">>} -> - decode_p1_ack(<<"p1:ack">>, IgnoreEls, _el); - {<<"rebind">>, <<"p1:rebind">>} -> - decode_p1_rebind(<<"p1:rebind">>, IgnoreEls, _el); - {<<"push">>, <<"p1:push">>} -> - decode_p1_push(<<"p1:push">>, IgnoreEls, _el); - {<<"stream:features">>, - <<"http://etherx.jabber.org/streams">>} -> - decode_stream_features(<<"http://etherx.jabber.org/streams">>, - IgnoreEls, _el); - {<<"compression">>, - <<"http://jabber.org/features/compress">>} -> - decode_compression(<<"http://jabber.org/features/compress">>, - IgnoreEls, _el); - {<<"method">>, - <<"http://jabber.org/features/compress">>} -> - decode_compression_method(<<"http://jabber.org/features/compress">>, - IgnoreEls, _el); - {<<"compressed">>, - <<"http://jabber.org/protocol/compress">>} -> - decode_compressed(<<"http://jabber.org/protocol/compress">>, - IgnoreEls, _el); - {<<"compress">>, - <<"http://jabber.org/protocol/compress">>} -> - decode_compress(<<"http://jabber.org/protocol/compress">>, - IgnoreEls, _el); - {<<"method">>, - <<"http://jabber.org/protocol/compress">>} -> - decode_compress_method(<<"http://jabber.org/protocol/compress">>, - IgnoreEls, _el); - {<<"failure">>, - <<"http://jabber.org/protocol/compress">>} -> - decode_compress_failure(<<"http://jabber.org/protocol/compress">>, - IgnoreEls, _el); - {<<"unsupported-method">>, - <<"http://jabber.org/protocol/compress">>} -> - decode_compress_failure_unsupported_method(<<"http://jabber.org/protocol/compress">>, - IgnoreEls, _el); - {<<"processing-failed">>, - <<"http://jabber.org/protocol/compress">>} -> - decode_compress_failure_processing_failed(<<"http://jabber.org/protocol/compress">>, - IgnoreEls, _el); - {<<"setup-failed">>, - <<"http://jabber.org/protocol/compress">>} -> - decode_compress_failure_setup_failed(<<"http://jabber.org/protocol/compress">>, - IgnoreEls, _el); - {<<"failure">>, - <<"urn:ietf:params:xml:ns:xmpp-tls">>} -> - decode_starttls_failure(<<"urn:ietf:params:xml:ns:xmpp-tls">>, - IgnoreEls, _el); - {<<"proceed">>, - <<"urn:ietf:params:xml:ns:xmpp-tls">>} -> - decode_starttls_proceed(<<"urn:ietf:params:xml:ns:xmpp-tls">>, - IgnoreEls, _el); - {<<"starttls">>, - <<"urn:ietf:params:xml:ns:xmpp-tls">>} -> - decode_starttls(<<"urn:ietf:params:xml:ns:xmpp-tls">>, - IgnoreEls, _el); - {<<"required">>, - <<"urn:ietf:params:xml:ns:xmpp-tls">>} -> - decode_starttls_required(<<"urn:ietf:params:xml:ns:xmpp-tls">>, - IgnoreEls, _el); - {<<"mechanisms">>, - <<"urn:ietf:params:xml:ns:xmpp-sasl">>} -> - decode_sasl_mechanisms(<<"urn:ietf:params:xml:ns:xmpp-sasl">>, - IgnoreEls, _el); - {<<"mechanism">>, - <<"urn:ietf:params:xml:ns:xmpp-sasl">>} -> - decode_sasl_mechanism(<<"urn:ietf:params:xml:ns:xmpp-sasl">>, - IgnoreEls, _el); - {<<"failure">>, - <<"urn:ietf:params:xml:ns:xmpp-sasl">>} -> - decode_sasl_failure(<<"urn:ietf:params:xml:ns:xmpp-sasl">>, - IgnoreEls, _el); - {<<"temporary-auth-failure">>, - <<"urn:ietf:params:xml:ns:xmpp-sasl">>} -> - decode_sasl_failure_temporary_auth_failure(<<"urn:ietf:params:xml:ns:xmpp-sasl">>, - IgnoreEls, _el); - {<<"not-authorized">>, - <<"urn:ietf:params:xml:ns:xmpp-sasl">>} -> - decode_sasl_failure_not_authorized(<<"urn:ietf:params:xml:ns:xmpp-sasl">>, - IgnoreEls, _el); - {<<"mechanism-too-weak">>, - <<"urn:ietf:params:xml:ns:xmpp-sasl">>} -> - decode_sasl_failure_mechanism_too_weak(<<"urn:ietf:params:xml:ns:xmpp-sasl">>, - IgnoreEls, _el); - {<<"malformed-request">>, - <<"urn:ietf:params:xml:ns:xmpp-sasl">>} -> - decode_sasl_failure_malformed_request(<<"urn:ietf:params:xml:ns:xmpp-sasl">>, - IgnoreEls, _el); - {<<"invalid-mechanism">>, - <<"urn:ietf:params:xml:ns:xmpp-sasl">>} -> - decode_sasl_failure_invalid_mechanism(<<"urn:ietf:params:xml:ns:xmpp-sasl">>, - IgnoreEls, _el); - {<<"invalid-authzid">>, - <<"urn:ietf:params:xml:ns:xmpp-sasl">>} -> - decode_sasl_failure_invalid_authzid(<<"urn:ietf:params:xml:ns:xmpp-sasl">>, - IgnoreEls, _el); - {<<"incorrect-encoding">>, - <<"urn:ietf:params:xml:ns:xmpp-sasl">>} -> - decode_sasl_failure_incorrect_encoding(<<"urn:ietf:params:xml:ns:xmpp-sasl">>, - IgnoreEls, _el); - {<<"encryption-required">>, - <<"urn:ietf:params:xml:ns:xmpp-sasl">>} -> - decode_sasl_failure_encryption_required(<<"urn:ietf:params:xml:ns:xmpp-sasl">>, - IgnoreEls, _el); - {<<"credentials-expired">>, - <<"urn:ietf:params:xml:ns:xmpp-sasl">>} -> - decode_sasl_failure_credentials_expired(<<"urn:ietf:params:xml:ns:xmpp-sasl">>, - IgnoreEls, _el); - {<<"account-disabled">>, - <<"urn:ietf:params:xml:ns:xmpp-sasl">>} -> - decode_sasl_failure_account_disabled(<<"urn:ietf:params:xml:ns:xmpp-sasl">>, - IgnoreEls, _el); - {<<"aborted">>, - <<"urn:ietf:params:xml:ns:xmpp-sasl">>} -> - decode_sasl_failure_aborted(<<"urn:ietf:params:xml:ns:xmpp-sasl">>, - IgnoreEls, _el); - {<<"text">>, <<"urn:ietf:params:xml:ns:xmpp-sasl">>} -> - decode_sasl_failure_text(<<"urn:ietf:params:xml:ns:xmpp-sasl">>, - IgnoreEls, _el); - {<<"success">>, - <<"urn:ietf:params:xml:ns:xmpp-sasl">>} -> - decode_sasl_success(<<"urn:ietf:params:xml:ns:xmpp-sasl">>, - IgnoreEls, _el); - {<<"response">>, - <<"urn:ietf:params:xml:ns:xmpp-sasl">>} -> - decode_sasl_response(<<"urn:ietf:params:xml:ns:xmpp-sasl">>, - IgnoreEls, _el); - {<<"challenge">>, - <<"urn:ietf:params:xml:ns:xmpp-sasl">>} -> - decode_sasl_challenge(<<"urn:ietf:params:xml:ns:xmpp-sasl">>, - IgnoreEls, _el); - {<<"abort">>, <<"urn:ietf:params:xml:ns:xmpp-sasl">>} -> - decode_sasl_abort(<<"urn:ietf:params:xml:ns:xmpp-sasl">>, - IgnoreEls, _el); - {<<"auth">>, <<"urn:ietf:params:xml:ns:xmpp-sasl">>} -> - decode_sasl_auth(<<"urn:ietf:params:xml:ns:xmpp-sasl">>, - IgnoreEls, _el); - {<<"bind">>, <<"urn:ietf:params:xml:ns:xmpp-bind">>} -> - decode_bind(<<"urn:ietf:params:xml:ns:xmpp-bind">>, - IgnoreEls, _el); - {<<"resource">>, - <<"urn:ietf:params:xml:ns:xmpp-bind">>} -> - decode_bind_resource(<<"urn:ietf:params:xml:ns:xmpp-bind">>, - IgnoreEls, _el); - {<<"jid">>, <<"urn:ietf:params:xml:ns:xmpp-bind">>} -> - decode_bind_jid(<<"urn:ietf:params:xml:ns:xmpp-bind">>, - IgnoreEls, _el); - {<<"error">>, <<"jabber:client">>} -> - decode_error(<<"jabber:client">>, IgnoreEls, _el); - {<<"text">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>} -> - decode_error_text(<<"urn:ietf:params:xml:ns:xmpp-stanzas">>, - IgnoreEls, _el); - {<<"unexpected-request">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>} -> - decode_error_unexpected_request(<<"urn:ietf:params:xml:ns:xmpp-stanzas">>, - IgnoreEls, _el); - {<<"undefined-condition">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>} -> - decode_error_undefined_condition(<<"urn:ietf:params:xml:ns:xmpp-stanzas">>, - IgnoreEls, _el); - {<<"subscription-required">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>} -> - decode_error_subscription_required(<<"urn:ietf:params:xml:ns:xmpp-stanzas">>, - IgnoreEls, _el); - {<<"service-unavailable">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>} -> - decode_error_service_unavailable(<<"urn:ietf:params:xml:ns:xmpp-stanzas">>, - IgnoreEls, _el); - {<<"resource-constraint">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>} -> - decode_error_resource_constraint(<<"urn:ietf:params:xml:ns:xmpp-stanzas">>, - IgnoreEls, _el); - {<<"remote-server-timeout">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>} -> - decode_error_remote_server_timeout(<<"urn:ietf:params:xml:ns:xmpp-stanzas">>, - IgnoreEls, _el); - {<<"remote-server-not-found">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>} -> - decode_error_remote_server_not_found(<<"urn:ietf:params:xml:ns:xmpp-stanzas">>, - IgnoreEls, _el); - {<<"registration-required">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>} -> - decode_error_registration_required(<<"urn:ietf:params:xml:ns:xmpp-stanzas">>, - IgnoreEls, _el); - {<<"redirect">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>} -> - decode_error_redirect(<<"urn:ietf:params:xml:ns:xmpp-stanzas">>, - IgnoreEls, _el); - {<<"recipient-unavailable">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>} -> - decode_error_recipient_unavailable(<<"urn:ietf:params:xml:ns:xmpp-stanzas">>, - IgnoreEls, _el); - {<<"policy-violation">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>} -> - decode_error_policy_violation(<<"urn:ietf:params:xml:ns:xmpp-stanzas">>, - IgnoreEls, _el); - {<<"not-authorized">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>} -> - decode_error_not_authorized(<<"urn:ietf:params:xml:ns:xmpp-stanzas">>, - IgnoreEls, _el); - {<<"not-allowed">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>} -> - decode_error_not_allowed(<<"urn:ietf:params:xml:ns:xmpp-stanzas">>, - IgnoreEls, _el); - {<<"not-acceptable">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>} -> - decode_error_not_acceptable(<<"urn:ietf:params:xml:ns:xmpp-stanzas">>, - IgnoreEls, _el); - {<<"jid-malformed">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>} -> - decode_error_jid_malformed(<<"urn:ietf:params:xml:ns:xmpp-stanzas">>, - IgnoreEls, _el); - {<<"item-not-found">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>} -> - decode_error_item_not_found(<<"urn:ietf:params:xml:ns:xmpp-stanzas">>, - IgnoreEls, _el); - {<<"internal-server-error">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>} -> - decode_error_internal_server_error(<<"urn:ietf:params:xml:ns:xmpp-stanzas">>, - IgnoreEls, _el); - {<<"gone">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>} -> - decode_error_gone(<<"urn:ietf:params:xml:ns:xmpp-stanzas">>, - IgnoreEls, _el); - {<<"forbidden">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>} -> - decode_error_forbidden(<<"urn:ietf:params:xml:ns:xmpp-stanzas">>, - IgnoreEls, _el); - {<<"feature-not-implemented">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>} -> - decode_error_feature_not_implemented(<<"urn:ietf:params:xml:ns:xmpp-stanzas">>, - IgnoreEls, _el); - {<<"conflict">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>} -> - decode_error_conflict(<<"urn:ietf:params:xml:ns:xmpp-stanzas">>, - IgnoreEls, _el); - {<<"bad-request">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>} -> - decode_error_bad_request(<<"urn:ietf:params:xml:ns:xmpp-stanzas">>, - IgnoreEls, _el); - {<<"presence">>, <<"jabber:client">>} -> - decode_presence(<<"jabber:client">>, IgnoreEls, _el); - {<<"priority">>, <<"jabber:client">>} -> - decode_presence_priority(<<"jabber:client">>, IgnoreEls, - _el); - {<<"status">>, <<"jabber:client">>} -> - decode_presence_status(<<"jabber:client">>, IgnoreEls, - _el); - {<<"show">>, <<"jabber:client">>} -> - decode_presence_show(<<"jabber:client">>, IgnoreEls, - _el); - {<<"message">>, <<"jabber:client">>} -> - decode_message(<<"jabber:client">>, IgnoreEls, _el); - {<<"thread">>, <<"jabber:client">>} -> - decode_message_thread(<<"jabber:client">>, IgnoreEls, - _el); - {<<"body">>, <<"jabber:client">>} -> - decode_message_body(<<"jabber:client">>, IgnoreEls, - _el); - {<<"subject">>, <<"jabber:client">>} -> - decode_message_subject(<<"jabber:client">>, IgnoreEls, - _el); - {<<"iq">>, <<"jabber:client">>} -> - decode_iq(<<"jabber:client">>, IgnoreEls, _el); - {<<"query">>, <<"http://jabber.org/protocol/stats">>} -> - decode_stats(<<"http://jabber.org/protocol/stats">>, - IgnoreEls, _el); - {<<"stat">>, <<"http://jabber.org/protocol/stats">>} -> - decode_stat(<<"http://jabber.org/protocol/stats">>, - IgnoreEls, _el); - {<<"error">>, <<"http://jabber.org/protocol/stats">>} -> - decode_stat_error(<<"http://jabber.org/protocol/stats">>, - IgnoreEls, _el); - {<<"storage">>, <<"storage:bookmarks">>} -> - decode_bookmarks_storage(<<"storage:bookmarks">>, - IgnoreEls, _el); - {<<"url">>, <<"storage:bookmarks">>} -> - decode_bookmark_url(<<"storage:bookmarks">>, IgnoreEls, - _el); - {<<"conference">>, <<"storage:bookmarks">>} -> - decode_bookmark_conference(<<"storage:bookmarks">>, - IgnoreEls, _el); - {<<"password">>, <<"storage:bookmarks">>} -> - decode_conference_password(<<"storage:bookmarks">>, - IgnoreEls, _el); - {<<"nick">>, <<"storage:bookmarks">>} -> - decode_conference_nick(<<"storage:bookmarks">>, - IgnoreEls, _el); - {<<"query">>, <<"jabber:iq:private">>} -> - decode_private(<<"jabber:iq:private">>, IgnoreEls, _el); - {<<"query">>, - <<"http://jabber.org/protocol/disco#items">>} -> - decode_disco_items(<<"http://jabber.org/protocol/disco#items">>, - IgnoreEls, _el); - {<<"item">>, - <<"http://jabber.org/protocol/disco#items">>} -> - decode_disco_item(<<"http://jabber.org/protocol/disco#items">>, - IgnoreEls, _el); - {<<"query">>, - <<"http://jabber.org/protocol/disco#info">>} -> - decode_disco_info(<<"http://jabber.org/protocol/disco#info">>, - IgnoreEls, _el); - {<<"feature">>, - <<"http://jabber.org/protocol/disco#info">>} -> - decode_disco_feature(<<"http://jabber.org/protocol/disco#info">>, - IgnoreEls, _el); - {<<"identity">>, - <<"http://jabber.org/protocol/disco#info">>} -> - decode_disco_identity(<<"http://jabber.org/protocol/disco#info">>, - IgnoreEls, _el); - {<<"blocklist">>, <<"urn:xmpp:blocking">>} -> - decode_block_list(<<"urn:xmpp:blocking">>, IgnoreEls, - _el); - {<<"unblock">>, <<"urn:xmpp:blocking">>} -> - decode_unblock(<<"urn:xmpp:blocking">>, IgnoreEls, _el); - {<<"block">>, <<"urn:xmpp:blocking">>} -> - decode_block(<<"urn:xmpp:blocking">>, IgnoreEls, _el); - {<<"item">>, <<"urn:xmpp:blocking">>} -> - decode_block_item(<<"urn:xmpp:blocking">>, IgnoreEls, - _el); - {<<"query">>, <<"jabber:iq:privacy">>} -> - decode_privacy(<<"jabber:iq:privacy">>, IgnoreEls, _el); - {<<"active">>, <<"jabber:iq:privacy">>} -> - decode_privacy_active_list(<<"jabber:iq:privacy">>, - IgnoreEls, _el); - {<<"default">>, <<"jabber:iq:privacy">>} -> - decode_privacy_default_list(<<"jabber:iq:privacy">>, - IgnoreEls, _el); - {<<"list">>, <<"jabber:iq:privacy">>} -> - decode_privacy_list(<<"jabber:iq:privacy">>, IgnoreEls, - _el); - {<<"item">>, <<"jabber:iq:privacy">>} -> - decode_privacy_item(<<"jabber:iq:privacy">>, IgnoreEls, - _el); - {<<"presence-out">>, <<"jabber:iq:privacy">>} -> - decode_privacy_presence_out(<<"jabber:iq:privacy">>, - IgnoreEls, _el); - {<<"presence-in">>, <<"jabber:iq:privacy">>} -> - decode_privacy_presence_in(<<"jabber:iq:privacy">>, - IgnoreEls, _el); - {<<"iq">>, <<"jabber:iq:privacy">>} -> - decode_privacy_iq(<<"jabber:iq:privacy">>, IgnoreEls, - _el); - {<<"message">>, <<"jabber:iq:privacy">>} -> - decode_privacy_message(<<"jabber:iq:privacy">>, - IgnoreEls, _el); - {<<"query">>, <<"jabber:iq:roster">>} -> - decode_roster(<<"jabber:iq:roster">>, IgnoreEls, _el); - {<<"item">>, <<"jabber:iq:roster">>} -> - decode_roster_item(<<"jabber:iq:roster">>, IgnoreEls, - _el); - {<<"group">>, <<"jabber:iq:roster">>} -> - decode_roster_group(<<"jabber:iq:roster">>, IgnoreEls, - _el); - {<<"query">>, <<"jabber:iq:version">>} -> - decode_version(<<"jabber:iq:version">>, IgnoreEls, _el); - {<<"os">>, <<"jabber:iq:version">>} -> - decode_version_os(<<"jabber:iq:version">>, IgnoreEls, - _el); - {<<"version">>, <<"jabber:iq:version">>} -> - decode_version_ver(<<"jabber:iq:version">>, IgnoreEls, - _el); - {<<"name">>, <<"jabber:iq:version">>} -> - decode_version_name(<<"jabber:iq:version">>, IgnoreEls, - _el); - {<<"query">>, <<"jabber:iq:last">>} -> - decode_last(<<"jabber:iq:last">>, IgnoreEls, _el); - {_name, _xmlns} -> - erlang:error({xmpp_codec, {unknown_tag, _name, _xmlns}}) - end. - -is_known_tag({xmlel, _name, _attrs, _} = _el) -> - case {_name, get_attr(<<"xmlns">>, _attrs)} of - {<<"failed">>, <<"urn:xmpp:sm:2">>} -> true; - {<<"failed">>, <<"urn:xmpp:sm:3">>} -> true; - {<<"a">>, <<"urn:xmpp:sm:2">>} -> true; - {<<"a">>, <<"urn:xmpp:sm:3">>} -> true; - {<<"r">>, <<"urn:xmpp:sm:2">>} -> true; - {<<"r">>, <<"urn:xmpp:sm:3">>} -> true; - {<<"resumed">>, <<"urn:xmpp:sm:2">>} -> true; - {<<"resumed">>, <<"urn:xmpp:sm:3">>} -> true; - {<<"resume">>, <<"urn:xmpp:sm:2">>} -> true; - {<<"resume">>, <<"urn:xmpp:sm:3">>} -> true; - {<<"enabled">>, <<"urn:xmpp:sm:2">>} -> true; - {<<"enabled">>, <<"urn:xmpp:sm:3">>} -> true; - {<<"enable">>, <<"urn:xmpp:sm:2">>} -> true; - {<<"enable">>, <<"urn:xmpp:sm:3">>} -> true; - {<<"sm">>, <<"urn:xmpp:sm:2">>} -> true; - {<<"sm">>, <<"urn:xmpp:sm:3">>} -> true; - {<<"inactive">>, <<"urn:xmpp:csi:0">>} -> true; - {<<"active">>, <<"urn:xmpp:csi:0">>} -> true; - {<<"csi">>, <<"urn:xmpp:csi:0">>} -> true; - {<<"sent">>, <<"urn:xmpp:carbons:2">>} -> true; - {<<"received">>, <<"urn:xmpp:carbons:2">>} -> true; - {<<"private">>, <<"urn:xmpp:carbons:2">>} -> true; - {<<"enable">>, <<"urn:xmpp:carbons:2">>} -> true; - {<<"disable">>, <<"urn:xmpp:carbons:2">>} -> true; - {<<"forwarded">>, <<"urn:xmpp:forward:0">>} -> true; - {<<"x">>, <<"http://jabber.org/protocol/muc">>} -> true; - {<<"query">>, - <<"http://jabber.org/protocol/muc#admin">>} -> - true; - {<<"reason">>, - <<"http://jabber.org/protocol/muc#admin">>} -> - true; - {<<"continue">>, - <<"http://jabber.org/protocol/muc#admin">>} -> - true; - {<<"actor">>, - <<"http://jabber.org/protocol/muc#admin">>} -> - true; - {<<"item">>, - <<"http://jabber.org/protocol/muc#admin">>} -> - true; - {<<"query">>, - <<"http://jabber.org/protocol/muc#owner">>} -> - true; - {<<"destroy">>, - <<"http://jabber.org/protocol/muc#owner">>} -> - true; - {<<"reason">>, - <<"http://jabber.org/protocol/muc#owner">>} -> - true; - {<<"password">>, - <<"http://jabber.org/protocol/muc#owner">>} -> - true; - {<<"x">>, <<"http://jabber.org/protocol/muc#user">>} -> - true; - {<<"item">>, - <<"http://jabber.org/protocol/muc#user">>} -> - true; - {<<"status">>, - <<"http://jabber.org/protocol/muc#user">>} -> - true; - {<<"continue">>, - <<"http://jabber.org/protocol/muc#user">>} -> - true; - {<<"actor">>, - <<"http://jabber.org/protocol/muc#user">>} -> - true; - {<<"invite">>, - <<"http://jabber.org/protocol/muc#user">>} -> - true; - {<<"destroy">>, - <<"http://jabber.org/protocol/muc#user">>} -> - true; - {<<"decline">>, - <<"http://jabber.org/protocol/muc#user">>} -> - true; - {<<"reason">>, - <<"http://jabber.org/protocol/muc#user">>} -> - true; - {<<"history">>, <<"http://jabber.org/protocol/muc">>} -> - true; - {<<"query">>, - <<"http://jabber.org/protocol/bytestreams">>} -> - true; - {<<"activate">>, - <<"http://jabber.org/protocol/bytestreams">>} -> - true; - {<<"streamhost-used">>, - <<"http://jabber.org/protocol/bytestreams">>} -> - true; - {<<"streamhost">>, - <<"http://jabber.org/protocol/bytestreams">>} -> - true; - {<<"x">>, <<"jabber:x:delay">>} -> true; - {<<"delay">>, <<"urn:xmpp:delay">>} -> true; - {<<"paused">>, - <<"http://jabber.org/protocol/chatstates">>} -> - true; - {<<"inactive">>, - <<"http://jabber.org/protocol/chatstates">>} -> - true; - {<<"gone">>, - <<"http://jabber.org/protocol/chatstates">>} -> - true; - {<<"composing">>, - <<"http://jabber.org/protocol/chatstates">>} -> - true; - {<<"active">>, - <<"http://jabber.org/protocol/chatstates">>} -> - true; - {<<"headers">>, - <<"http://jabber.org/protocol/shim">>} -> - true; - {<<"header">>, <<"http://jabber.org/protocol/shim">>} -> - true; - {<<"pubsub">>, - <<"http://jabber.org/protocol/pubsub">>} -> - true; - {<<"retract">>, - <<"http://jabber.org/protocol/pubsub">>} -> - true; - {<<"options">>, - <<"http://jabber.org/protocol/pubsub">>} -> - true; - {<<"publish">>, - <<"http://jabber.org/protocol/pubsub">>} -> - true; - {<<"unsubscribe">>, - <<"http://jabber.org/protocol/pubsub">>} -> - true; - {<<"subscribe">>, - <<"http://jabber.org/protocol/pubsub">>} -> - true; - {<<"affiliations">>, - <<"http://jabber.org/protocol/pubsub">>} -> - true; - {<<"subscriptions">>, - <<"http://jabber.org/protocol/pubsub">>} -> - true; - {<<"event">>, - <<"http://jabber.org/protocol/pubsub#event">>} -> - true; - {<<"items">>, - <<"http://jabber.org/protocol/pubsub#event">>} -> - true; - {<<"item">>, - <<"http://jabber.org/protocol/pubsub#event">>} -> - true; - {<<"retract">>, - <<"http://jabber.org/protocol/pubsub#event">>} -> - true; - {<<"items">>, - <<"http://jabber.org/protocol/pubsub">>} -> - true; - {<<"item">>, <<"http://jabber.org/protocol/pubsub">>} -> - true; - {<<"affiliation">>, - <<"http://jabber.org/protocol/pubsub">>} -> - true; - {<<"subscription">>, - <<"http://jabber.org/protocol/pubsub">>} -> - true; - {<<"x">>, <<"jabber:x:data">>} -> true; - {<<"item">>, <<"jabber:x:data">>} -> true; - {<<"reported">>, <<"jabber:x:data">>} -> true; - {<<"title">>, <<"jabber:x:data">>} -> true; - {<<"instructions">>, <<"jabber:x:data">>} -> true; - {<<"field">>, <<"jabber:x:data">>} -> true; - {<<"option">>, <<"jabber:x:data">>} -> true; - {<<"value">>, <<"jabber:x:data">>} -> true; - {<<"desc">>, <<"jabber:x:data">>} -> true; - {<<"required">>, <<"jabber:x:data">>} -> true; - {<<"x">>, <<"vcard-temp:x:update">>} -> true; - {<<"photo">>, <<"vcard-temp:x:update">>} -> true; - {<<"vCard">>, <<"vcard-temp">>} -> true; - {<<"CLASS">>, <<"vcard-temp">>} -> true; - {<<"CATEGORIES">>, <<"vcard-temp">>} -> true; - {<<"KEY">>, <<"vcard-temp">>} -> true; - {<<"SOUND">>, <<"vcard-temp">>} -> true; - {<<"ORG">>, <<"vcard-temp">>} -> true; - {<<"PHOTO">>, <<"vcard-temp">>} -> true; - {<<"LOGO">>, <<"vcard-temp">>} -> true; - {<<"BINVAL">>, <<"vcard-temp">>} -> true; - {<<"GEO">>, <<"vcard-temp">>} -> true; - {<<"EMAIL">>, <<"vcard-temp">>} -> true; - {<<"TEL">>, <<"vcard-temp">>} -> true; - {<<"LABEL">>, <<"vcard-temp">>} -> true; - {<<"ADR">>, <<"vcard-temp">>} -> true; - {<<"N">>, <<"vcard-temp">>} -> true; - {<<"CONFIDENTIAL">>, <<"vcard-temp">>} -> true; - {<<"PRIVATE">>, <<"vcard-temp">>} -> true; - {<<"PUBLIC">>, <<"vcard-temp">>} -> true; - {<<"EXTVAL">>, <<"vcard-temp">>} -> true; - {<<"TYPE">>, <<"vcard-temp">>} -> true; - {<<"DESC">>, <<"vcard-temp">>} -> true; - {<<"URL">>, <<"vcard-temp">>} -> true; - {<<"UID">>, <<"vcard-temp">>} -> true; - {<<"SORT-STRING">>, <<"vcard-temp">>} -> true; - {<<"REV">>, <<"vcard-temp">>} -> true; - {<<"PRODID">>, <<"vcard-temp">>} -> true; - {<<"NOTE">>, <<"vcard-temp">>} -> true; - {<<"KEYWORD">>, <<"vcard-temp">>} -> true; - {<<"ROLE">>, <<"vcard-temp">>} -> true; - {<<"TITLE">>, <<"vcard-temp">>} -> true; - {<<"TZ">>, <<"vcard-temp">>} -> true; - {<<"MAILER">>, <<"vcard-temp">>} -> true; - {<<"JABBERID">>, <<"vcard-temp">>} -> true; - {<<"BDAY">>, <<"vcard-temp">>} -> true; - {<<"NICKNAME">>, <<"vcard-temp">>} -> true; - {<<"FN">>, <<"vcard-temp">>} -> true; - {<<"VERSION">>, <<"vcard-temp">>} -> true; - {<<"CRED">>, <<"vcard-temp">>} -> true; - {<<"PHONETIC">>, <<"vcard-temp">>} -> true; - {<<"ORGUNIT">>, <<"vcard-temp">>} -> true; - {<<"ORGNAME">>, <<"vcard-temp">>} -> true; - {<<"LON">>, <<"vcard-temp">>} -> true; - {<<"LAT">>, <<"vcard-temp">>} -> true; - {<<"USERID">>, <<"vcard-temp">>} -> true; - {<<"NUMBER">>, <<"vcard-temp">>} -> true; - {<<"LINE">>, <<"vcard-temp">>} -> true; - {<<"CTRY">>, <<"vcard-temp">>} -> true; - {<<"PCODE">>, <<"vcard-temp">>} -> true; - {<<"REGION">>, <<"vcard-temp">>} -> true; - {<<"LOCALITY">>, <<"vcard-temp">>} -> true; - {<<"STREET">>, <<"vcard-temp">>} -> true; - {<<"EXTADD">>, <<"vcard-temp">>} -> true; - {<<"POBOX">>, <<"vcard-temp">>} -> true; - {<<"SUFFIX">>, <<"vcard-temp">>} -> true; - {<<"PREFIX">>, <<"vcard-temp">>} -> true; - {<<"MIDDLE">>, <<"vcard-temp">>} -> true; - {<<"GIVEN">>, <<"vcard-temp">>} -> true; - {<<"FAMILY">>, <<"vcard-temp">>} -> true; - {<<"X400">>, <<"vcard-temp">>} -> true; - {<<"INTERNET">>, <<"vcard-temp">>} -> true; - {<<"PREF">>, <<"vcard-temp">>} -> true; - {<<"INTL">>, <<"vcard-temp">>} -> true; - {<<"DOM">>, <<"vcard-temp">>} -> true; - {<<"PARCEL">>, <<"vcard-temp">>} -> true; - {<<"POSTAL">>, <<"vcard-temp">>} -> true; - {<<"PCS">>, <<"vcard-temp">>} -> true; - {<<"ISDN">>, <<"vcard-temp">>} -> true; - {<<"MODEM">>, <<"vcard-temp">>} -> true; - {<<"BBS">>, <<"vcard-temp">>} -> true; - {<<"VIDEO">>, <<"vcard-temp">>} -> true; - {<<"CELL">>, <<"vcard-temp">>} -> true; - {<<"MSG">>, <<"vcard-temp">>} -> true; - {<<"PAGER">>, <<"vcard-temp">>} -> true; - {<<"FAX">>, <<"vcard-temp">>} -> true; - {<<"VOICE">>, <<"vcard-temp">>} -> true; - {<<"WORK">>, <<"vcard-temp">>} -> true; - {<<"HOME">>, <<"vcard-temp">>} -> true; - {<<"stream:error">>, - <<"http://etherx.jabber.org/streams">>} -> - true; - {<<"unsupported-version">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - true; - {<<"unsupported-stanza-type">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - true; - {<<"unsupported-encoding">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - true; - {<<"undefined-condition">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - true; - {<<"system-shutdown">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - true; - {<<"see-other-host">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - true; - {<<"restricted-xml">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - true; - {<<"resource-constraint">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - true; - {<<"reset">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - true; - {<<"remote-connection-failed">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - true; - {<<"policy-violation">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - true; - {<<"not-well-formed">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - true; - {<<"not-authorized">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - true; - {<<"invalid-xml">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - true; - {<<"invalid-namespace">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - true; - {<<"invalid-id">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - true; - {<<"invalid-from">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - true; - {<<"internal-server-error">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - true; - {<<"improper-addressing">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - true; - {<<"host-unknown">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - true; - {<<"host-gone">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - true; - {<<"connection-timeout">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - true; - {<<"conflict">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - true; - {<<"bad-namespace-prefix">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - true; - {<<"bad-format">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - true; - {<<"text">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>} -> - true; - {<<"time">>, <<"urn:xmpp:time">>} -> true; - {<<"tzo">>, <<"urn:xmpp:time">>} -> true; - {<<"utc">>, <<"urn:xmpp:time">>} -> true; - {<<"ping">>, <<"urn:xmpp:ping">>} -> true; - {<<"session">>, - <<"urn:ietf:params:xml:ns:xmpp-session">>} -> - true; - {<<"query">>, <<"jabber:iq:register">>} -> true; - {<<"key">>, <<"jabber:iq:register">>} -> true; - {<<"text">>, <<"jabber:iq:register">>} -> true; - {<<"misc">>, <<"jabber:iq:register">>} -> true; - {<<"date">>, <<"jabber:iq:register">>} -> true; - {<<"url">>, <<"jabber:iq:register">>} -> true; - {<<"phone">>, <<"jabber:iq:register">>} -> true; - {<<"zip">>, <<"jabber:iq:register">>} -> true; - {<<"state">>, <<"jabber:iq:register">>} -> true; - {<<"city">>, <<"jabber:iq:register">>} -> true; - {<<"address">>, <<"jabber:iq:register">>} -> true; - {<<"email">>, <<"jabber:iq:register">>} -> true; - {<<"last">>, <<"jabber:iq:register">>} -> true; - {<<"first">>, <<"jabber:iq:register">>} -> true; - {<<"name">>, <<"jabber:iq:register">>} -> true; - {<<"password">>, <<"jabber:iq:register">>} -> true; - {<<"nick">>, <<"jabber:iq:register">>} -> true; - {<<"username">>, <<"jabber:iq:register">>} -> true; - {<<"instructions">>, <<"jabber:iq:register">>} -> true; - {<<"remove">>, <<"jabber:iq:register">>} -> true; - {<<"registered">>, <<"jabber:iq:register">>} -> true; - {<<"register">>, - <<"http://jabber.org/features/iq-register">>} -> - true; - {<<"c">>, <<"http://jabber.org/protocol/caps">>} -> - true; - {<<"ack">>, <<"p1:ack">>} -> true; - {<<"rebind">>, <<"p1:rebind">>} -> true; - {<<"push">>, <<"p1:push">>} -> true; - {<<"stream:features">>, - <<"http://etherx.jabber.org/streams">>} -> - true; - {<<"compression">>, - <<"http://jabber.org/features/compress">>} -> - true; - {<<"method">>, - <<"http://jabber.org/features/compress">>} -> - true; - {<<"compressed">>, - <<"http://jabber.org/protocol/compress">>} -> - true; - {<<"compress">>, - <<"http://jabber.org/protocol/compress">>} -> - true; - {<<"method">>, - <<"http://jabber.org/protocol/compress">>} -> - true; - {<<"failure">>, - <<"http://jabber.org/protocol/compress">>} -> - true; - {<<"unsupported-method">>, - <<"http://jabber.org/protocol/compress">>} -> - true; - {<<"processing-failed">>, - <<"http://jabber.org/protocol/compress">>} -> - true; - {<<"setup-failed">>, - <<"http://jabber.org/protocol/compress">>} -> - true; - {<<"failure">>, - <<"urn:ietf:params:xml:ns:xmpp-tls">>} -> - true; - {<<"proceed">>, - <<"urn:ietf:params:xml:ns:xmpp-tls">>} -> - true; - {<<"starttls">>, - <<"urn:ietf:params:xml:ns:xmpp-tls">>} -> - true; - {<<"required">>, - <<"urn:ietf:params:xml:ns:xmpp-tls">>} -> - true; - {<<"mechanisms">>, - <<"urn:ietf:params:xml:ns:xmpp-sasl">>} -> - true; - {<<"mechanism">>, - <<"urn:ietf:params:xml:ns:xmpp-sasl">>} -> - true; - {<<"failure">>, - <<"urn:ietf:params:xml:ns:xmpp-sasl">>} -> - true; - {<<"temporary-auth-failure">>, - <<"urn:ietf:params:xml:ns:xmpp-sasl">>} -> - true; - {<<"not-authorized">>, - <<"urn:ietf:params:xml:ns:xmpp-sasl">>} -> - true; - {<<"mechanism-too-weak">>, - <<"urn:ietf:params:xml:ns:xmpp-sasl">>} -> - true; - {<<"malformed-request">>, - <<"urn:ietf:params:xml:ns:xmpp-sasl">>} -> - true; - {<<"invalid-mechanism">>, - <<"urn:ietf:params:xml:ns:xmpp-sasl">>} -> - true; - {<<"invalid-authzid">>, - <<"urn:ietf:params:xml:ns:xmpp-sasl">>} -> - true; - {<<"incorrect-encoding">>, - <<"urn:ietf:params:xml:ns:xmpp-sasl">>} -> - true; - {<<"encryption-required">>, - <<"urn:ietf:params:xml:ns:xmpp-sasl">>} -> - true; - {<<"credentials-expired">>, - <<"urn:ietf:params:xml:ns:xmpp-sasl">>} -> - true; - {<<"account-disabled">>, - <<"urn:ietf:params:xml:ns:xmpp-sasl">>} -> - true; - {<<"aborted">>, - <<"urn:ietf:params:xml:ns:xmpp-sasl">>} -> - true; - {<<"text">>, <<"urn:ietf:params:xml:ns:xmpp-sasl">>} -> - true; - {<<"success">>, - <<"urn:ietf:params:xml:ns:xmpp-sasl">>} -> - true; - {<<"response">>, - <<"urn:ietf:params:xml:ns:xmpp-sasl">>} -> - true; - {<<"challenge">>, - <<"urn:ietf:params:xml:ns:xmpp-sasl">>} -> - true; - {<<"abort">>, <<"urn:ietf:params:xml:ns:xmpp-sasl">>} -> - true; - {<<"auth">>, <<"urn:ietf:params:xml:ns:xmpp-sasl">>} -> - true; - {<<"bind">>, <<"urn:ietf:params:xml:ns:xmpp-bind">>} -> - true; - {<<"resource">>, - <<"urn:ietf:params:xml:ns:xmpp-bind">>} -> - true; - {<<"jid">>, <<"urn:ietf:params:xml:ns:xmpp-bind">>} -> - true; - {<<"error">>, <<"jabber:client">>} -> true; - {<<"text">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>} -> - true; - {<<"unexpected-request">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>} -> - true; - {<<"undefined-condition">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>} -> - true; - {<<"subscription-required">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>} -> - true; - {<<"service-unavailable">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>} -> - true; - {<<"resource-constraint">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>} -> - true; - {<<"remote-server-timeout">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>} -> - true; - {<<"remote-server-not-found">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>} -> - true; - {<<"registration-required">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>} -> - true; - {<<"redirect">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>} -> - true; - {<<"recipient-unavailable">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>} -> - true; - {<<"policy-violation">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>} -> - true; - {<<"not-authorized">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>} -> - true; - {<<"not-allowed">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>} -> - true; - {<<"not-acceptable">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>} -> - true; - {<<"jid-malformed">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>} -> - true; - {<<"item-not-found">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>} -> - true; - {<<"internal-server-error">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>} -> - true; - {<<"gone">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>} -> - true; - {<<"forbidden">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>} -> - true; - {<<"feature-not-implemented">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>} -> - true; - {<<"conflict">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>} -> - true; - {<<"bad-request">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>} -> - true; - {<<"presence">>, <<"jabber:client">>} -> true; - {<<"priority">>, <<"jabber:client">>} -> true; - {<<"status">>, <<"jabber:client">>} -> true; - {<<"show">>, <<"jabber:client">>} -> true; - {<<"message">>, <<"jabber:client">>} -> true; - {<<"thread">>, <<"jabber:client">>} -> true; - {<<"body">>, <<"jabber:client">>} -> true; - {<<"subject">>, <<"jabber:client">>} -> true; - {<<"iq">>, <<"jabber:client">>} -> true; - {<<"query">>, <<"http://jabber.org/protocol/stats">>} -> - true; - {<<"stat">>, <<"http://jabber.org/protocol/stats">>} -> - true; - {<<"error">>, <<"http://jabber.org/protocol/stats">>} -> - true; - {<<"storage">>, <<"storage:bookmarks">>} -> true; - {<<"url">>, <<"storage:bookmarks">>} -> true; - {<<"conference">>, <<"storage:bookmarks">>} -> true; - {<<"password">>, <<"storage:bookmarks">>} -> true; - {<<"nick">>, <<"storage:bookmarks">>} -> true; - {<<"query">>, <<"jabber:iq:private">>} -> true; - {<<"query">>, - <<"http://jabber.org/protocol/disco#items">>} -> - true; - {<<"item">>, - <<"http://jabber.org/protocol/disco#items">>} -> - true; - {<<"query">>, - <<"http://jabber.org/protocol/disco#info">>} -> - true; - {<<"feature">>, - <<"http://jabber.org/protocol/disco#info">>} -> - true; - {<<"identity">>, - <<"http://jabber.org/protocol/disco#info">>} -> - true; - {<<"blocklist">>, <<"urn:xmpp:blocking">>} -> true; - {<<"unblock">>, <<"urn:xmpp:blocking">>} -> true; - {<<"block">>, <<"urn:xmpp:blocking">>} -> true; - {<<"item">>, <<"urn:xmpp:blocking">>} -> true; - {<<"query">>, <<"jabber:iq:privacy">>} -> true; - {<<"active">>, <<"jabber:iq:privacy">>} -> true; - {<<"default">>, <<"jabber:iq:privacy">>} -> true; - {<<"list">>, <<"jabber:iq:privacy">>} -> true; - {<<"item">>, <<"jabber:iq:privacy">>} -> true; - {<<"presence-out">>, <<"jabber:iq:privacy">>} -> true; - {<<"presence-in">>, <<"jabber:iq:privacy">>} -> true; - {<<"iq">>, <<"jabber:iq:privacy">>} -> true; - {<<"message">>, <<"jabber:iq:privacy">>} -> true; - {<<"query">>, <<"jabber:iq:roster">>} -> true; - {<<"item">>, <<"jabber:iq:roster">>} -> true; - {<<"group">>, <<"jabber:iq:roster">>} -> true; - {<<"query">>, <<"jabber:iq:version">>} -> true; - {<<"os">>, <<"jabber:iq:version">>} -> true; - {<<"version">>, <<"jabber:iq:version">>} -> true; - {<<"name">>, <<"jabber:iq:version">>} -> true; - {<<"query">>, <<"jabber:iq:last">>} -> true; - _ -> false - end. - -encode({xmlel, _, _, _} = El) -> El; -encode({last, _, _} = Query) -> - encode_last(Query, - [{<<"xmlns">>, <<"jabber:iq:last">>}]); -encode({version, _, _, _} = Query) -> - encode_version(Query, - [{<<"xmlns">>, <<"jabber:iq:version">>}]); -encode({roster_item, _, _, _, _, _} = Item) -> - encode_roster_item(Item, - [{<<"xmlns">>, <<"jabber:iq:roster">>}]); -encode({roster, _, _} = Query) -> - encode_roster(Query, - [{<<"xmlns">>, <<"jabber:iq:roster">>}]); -encode({privacy_item, _, _, _, _, _} = Item) -> - encode_privacy_item(Item, - [{<<"xmlns">>, <<"jabber:iq:privacy">>}]); -encode({privacy_list, _, _} = List) -> - encode_privacy_list(List, - [{<<"xmlns">>, <<"jabber:iq:privacy">>}]); -encode({privacy, _, _, _} = Query) -> - encode_privacy(Query, - [{<<"xmlns">>, <<"jabber:iq:privacy">>}]); -encode({block, _} = Block) -> - encode_block(Block, - [{<<"xmlns">>, <<"urn:xmpp:blocking">>}]); -encode({unblock, _} = Unblock) -> - encode_unblock(Unblock, - [{<<"xmlns">>, <<"urn:xmpp:blocking">>}]); -encode({block_list} = Blocklist) -> - encode_block_list(Blocklist, - [{<<"xmlns">>, <<"urn:xmpp:blocking">>}]); -encode({identity, _, _, _, _} = Identity) -> - encode_disco_identity(Identity, - [{<<"xmlns">>, - <<"http://jabber.org/protocol/disco#info">>}]); -encode({disco_info, _, _, _, _} = Query) -> - encode_disco_info(Query, - [{<<"xmlns">>, - <<"http://jabber.org/protocol/disco#info">>}]); -encode({disco_item, _, _, _} = Item) -> - encode_disco_item(Item, - [{<<"xmlns">>, - <<"http://jabber.org/protocol/disco#items">>}]); -encode({disco_items, _, _} = Query) -> - encode_disco_items(Query, - [{<<"xmlns">>, - <<"http://jabber.org/protocol/disco#items">>}]); -encode({private, _} = Query) -> - encode_private(Query, - [{<<"xmlns">>, <<"jabber:iq:private">>}]); -encode({bookmark_conference, _, _, _, _, _} = - Conference) -> - encode_bookmark_conference(Conference, - [{<<"xmlns">>, <<"storage:bookmarks">>}]); -encode({bookmark_url, _, _} = Url) -> - encode_bookmark_url(Url, - [{<<"xmlns">>, <<"storage:bookmarks">>}]); -encode({bookmark_storage, _, _} = Storage) -> - encode_bookmarks_storage(Storage, - [{<<"xmlns">>, <<"storage:bookmarks">>}]); -encode({stat, _, _, _, _} = Stat) -> - encode_stat(Stat, - [{<<"xmlns">>, - <<"http://jabber.org/protocol/stats">>}]); -encode({stats, _} = Query) -> - encode_stats(Query, - [{<<"xmlns">>, - <<"http://jabber.org/protocol/stats">>}]); -encode({iq, _, _, _, _, _, _, _} = Iq) -> - encode_iq(Iq, [{<<"xmlns">>, <<"jabber:client">>}]); -encode({message, _, _, _, _, _, _, _, _, _, _} = - Message) -> - encode_message(Message, - [{<<"xmlns">>, <<"jabber:client">>}]); -encode({presence, _, _, _, _, _, _, _, _, _, _} = - Presence) -> - encode_presence(Presence, - [{<<"xmlns">>, <<"jabber:client">>}]); -encode({gone, _} = Gone) -> - encode_error_gone(Gone, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>}]); -encode({redirect, _} = Redirect) -> - encode_error_redirect(Redirect, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>}]); -encode({error, _, _, _, _} = Error) -> - encode_error(Error, - [{<<"xmlns">>, <<"jabber:client">>}]); -encode({bind, _, _} = Bind) -> - encode_bind(Bind, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-bind">>}]); -encode({sasl_auth, _, _} = Auth) -> - encode_sasl_auth(Auth, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-sasl">>}]); -encode({sasl_abort} = Abort) -> - encode_sasl_abort(Abort, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-sasl">>}]); -encode({sasl_challenge, _} = Challenge) -> - encode_sasl_challenge(Challenge, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-sasl">>}]); -encode({sasl_response, _} = Response) -> - encode_sasl_response(Response, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-sasl">>}]); -encode({sasl_success, _} = Success) -> - encode_sasl_success(Success, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-sasl">>}]); -encode({sasl_failure, _, _} = Failure) -> - encode_sasl_failure(Failure, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-sasl">>}]); -encode({sasl_mechanisms, _} = Mechanisms) -> - encode_sasl_mechanisms(Mechanisms, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-sasl">>}]); -encode({starttls, _} = Starttls) -> - encode_starttls(Starttls, - [{<<"xmlns">>, <<"urn:ietf:params:xml:ns:xmpp-tls">>}]); -encode({starttls_proceed} = Proceed) -> - encode_starttls_proceed(Proceed, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-tls">>}]); -encode({starttls_failure} = Failure) -> - encode_starttls_failure(Failure, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-tls">>}]); -encode({compress_failure, _} = Failure) -> - encode_compress_failure(Failure, - [{<<"xmlns">>, - <<"http://jabber.org/protocol/compress">>}]); -encode({compress, _} = Compress) -> - encode_compress(Compress, - [{<<"xmlns">>, - <<"http://jabber.org/protocol/compress">>}]); -encode({compressed} = Compressed) -> - encode_compressed(Compressed, - [{<<"xmlns">>, - <<"http://jabber.org/protocol/compress">>}]); -encode({compression, _} = Compression) -> - encode_compression(Compression, - [{<<"xmlns">>, - <<"http://jabber.org/features/compress">>}]); -encode({stream_features, _} = Stream_features) -> - encode_stream_features(Stream_features, - [{<<"xmlns">>, - <<"http://etherx.jabber.org/streams">>}]); -encode({p1_push} = Push) -> - encode_p1_push(Push, [{<<"xmlns">>, <<"p1:push">>}]); -encode({p1_rebind} = Rebind) -> - encode_p1_rebind(Rebind, - [{<<"xmlns">>, <<"p1:rebind">>}]); -encode({p1_ack} = Ack) -> - encode_p1_ack(Ack, [{<<"xmlns">>, <<"p1:ack">>}]); -encode({caps, _, _, _} = C) -> - encode_caps(C, - [{<<"xmlns">>, <<"http://jabber.org/protocol/caps">>}]); -encode({feature_register} = Register) -> - encode_feature_register(Register, - [{<<"xmlns">>, - <<"http://jabber.org/features/iq-register">>}]); -encode({register, _, _, _, _, _, _, _, _, _, _, _, _, _, - _, _, _, _, _, _, _, _} = - Query) -> - encode_register(Query, - [{<<"xmlns">>, <<"jabber:iq:register">>}]); -encode({session} = Session) -> - encode_session(Session, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-session">>}]); -encode({ping} = Ping) -> - encode_ping(Ping, [{<<"xmlns">>, <<"urn:xmpp:ping">>}]); -encode({time, _, _} = Time) -> - encode_time(Time, [{<<"xmlns">>, <<"urn:xmpp:time">>}]); -encode({text, _, _} = Text) -> - encode_stream_error_text(Text, []); -encode({'see-other-host', _} = See_other_host) -> - encode_stream_error_see_other_host(See_other_host, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>}]); -encode({stream_error, _, _} = Stream_error) -> - encode_stream_error(Stream_error, - [{<<"xmlns">>, - <<"http://etherx.jabber.org/streams">>}]); -encode({vcard_name, _, _, _, _, _} = N) -> - encode_vcard_N(N, [{<<"xmlns">>, <<"vcard-temp">>}]); -encode({vcard_adr, _, _, _, _, _, _, _, _, _, _, _, _, - _, _} = - Adr) -> - encode_vcard_ADR(Adr, - [{<<"xmlns">>, <<"vcard-temp">>}]); -encode({vcard_label, _, _, _, _, _, _, _, _} = Label) -> - encode_vcard_LABEL(Label, - [{<<"xmlns">>, <<"vcard-temp">>}]); -encode({vcard_tel, _, _, _, _, _, _, _, _, _, _, _, _, - _, _} = - Tel) -> - encode_vcard_TEL(Tel, - [{<<"xmlns">>, <<"vcard-temp">>}]); -encode({vcard_email, _, _, _, _, _, _} = Email) -> - encode_vcard_EMAIL(Email, - [{<<"xmlns">>, <<"vcard-temp">>}]); -encode({vcard_geo, _, _} = Geo) -> - encode_vcard_GEO(Geo, - [{<<"xmlns">>, <<"vcard-temp">>}]); -encode({vcard_logo, _, _, _} = Logo) -> - encode_vcard_LOGO(Logo, - [{<<"xmlns">>, <<"vcard-temp">>}]); -encode({vcard_photo, _, _, _} = Photo) -> - encode_vcard_PHOTO(Photo, - [{<<"xmlns">>, <<"vcard-temp">>}]); -encode({vcard_org, _, _} = Org) -> - encode_vcard_ORG(Org, - [{<<"xmlns">>, <<"vcard-temp">>}]); -encode({vcard_sound, _, _, _} = Sound) -> - encode_vcard_SOUND(Sound, - [{<<"xmlns">>, <<"vcard-temp">>}]); -encode({vcard_key, _, _} = Key) -> - encode_vcard_KEY(Key, - [{<<"xmlns">>, <<"vcard-temp">>}]); -encode({vcard, _, _, _, _, _, _, _, _, _, _, _, _, _, _, - _, _, _, _, _, _, _, _, _, _, _, _, _, _, _} = - Vcard) -> - encode_vcard(Vcard, [{<<"xmlns">>, <<"vcard-temp">>}]); -encode({vcard_xupdate, _} = X) -> - encode_vcard_xupdate(X, - [{<<"xmlns">>, <<"vcard-temp:x:update">>}]); -encode({xdata_field, _, _, _, _, _, _, _} = Field) -> - encode_xdata_field(Field, - [{<<"xmlns">>, <<"jabber:x:data">>}]); -encode({xdata, _, _, _, _, _, _} = X) -> - encode_xdata(X, [{<<"xmlns">>, <<"jabber:x:data">>}]); -encode({pubsub_subscription, _, _, _, _} = - Subscription) -> - encode_pubsub_subscription(Subscription, - [{<<"xmlns">>, - <<"http://jabber.org/protocol/pubsub">>}]); -encode({pubsub_affiliation, _, _} = Affiliation) -> - encode_pubsub_affiliation(Affiliation, - [{<<"xmlns">>, - <<"http://jabber.org/protocol/pubsub">>}]); -encode({pubsub_item, _, _} = Item) -> - encode_pubsub_item(Item, - [{<<"xmlns">>, - <<"http://jabber.org/protocol/pubsub">>}]); -encode({pubsub_items, _, _, _, _} = Items) -> - encode_pubsub_items(Items, - [{<<"xmlns">>, - <<"http://jabber.org/protocol/pubsub">>}]); -encode({pubsub_event_item, _, _, _} = Item) -> - encode_pubsub_event_item(Item, - [{<<"xmlns">>, - <<"http://jabber.org/protocol/pubsub#event">>}]); -encode({pubsub_event_items, _, _, _} = Items) -> - encode_pubsub_event_items(Items, - [{<<"xmlns">>, - <<"http://jabber.org/protocol/pubsub#event">>}]); -encode({pubsub_event, _} = Event) -> - encode_pubsub_event(Event, - [{<<"xmlns">>, - <<"http://jabber.org/protocol/pubsub#event">>}]); -encode({pubsub_subscribe, _, _} = Subscribe) -> - encode_pubsub_subscribe(Subscribe, - [{<<"xmlns">>, - <<"http://jabber.org/protocol/pubsub">>}]); -encode({pubsub_unsubscribe, _, _, _} = Unsubscribe) -> - encode_pubsub_unsubscribe(Unsubscribe, - [{<<"xmlns">>, - <<"http://jabber.org/protocol/pubsub">>}]); -encode({pubsub_publish, _, _} = Publish) -> - encode_pubsub_publish(Publish, - [{<<"xmlns">>, - <<"http://jabber.org/protocol/pubsub">>}]); -encode({pubsub_options, _, _, _, _} = Options) -> - encode_pubsub_options(Options, - [{<<"xmlns">>, - <<"http://jabber.org/protocol/pubsub">>}]); -encode({pubsub_retract, _, _, _} = Retract) -> - encode_pubsub_retract(Retract, - [{<<"xmlns">>, - <<"http://jabber.org/protocol/pubsub">>}]); -encode({pubsub, _, _, _, _, _, _, _, _} = Pubsub) -> - encode_pubsub(Pubsub, - [{<<"xmlns">>, - <<"http://jabber.org/protocol/pubsub">>}]); -encode({shim, _} = Headers) -> - encode_shim_headers(Headers, - [{<<"xmlns">>, <<"http://jabber.org/protocol/shim">>}]); -encode({chatstate, active} = Active) -> - encode_chatstate_active(Active, - [{<<"xmlns">>, - <<"http://jabber.org/protocol/chatstates">>}]); -encode({chatstate, composing} = Composing) -> - encode_chatstate_composing(Composing, - [{<<"xmlns">>, - <<"http://jabber.org/protocol/chatstates">>}]); -encode({chatstate, gone} = Gone) -> - encode_chatstate_gone(Gone, - [{<<"xmlns">>, - <<"http://jabber.org/protocol/chatstates">>}]); -encode({chatstate, inactive} = Inactive) -> - encode_chatstate_inactive(Inactive, - [{<<"xmlns">>, - <<"http://jabber.org/protocol/chatstates">>}]); -encode({chatstate, paused} = Paused) -> - encode_chatstate_paused(Paused, - [{<<"xmlns">>, - <<"http://jabber.org/protocol/chatstates">>}]); -encode({delay, _, _} = Delay) -> - encode_delay(Delay, - [{<<"xmlns">>, <<"urn:xmpp:delay">>}]); -encode({legacy_delay, _, _} = X) -> - encode_legacy_delay(X, - [{<<"xmlns">>, <<"jabber:x:delay">>}]); -encode({streamhost, _, _, _} = Streamhost) -> - encode_bytestreams_streamhost(Streamhost, - [{<<"xmlns">>, - <<"http://jabber.org/protocol/bytestreams">>}]); -encode({bytestreams, _, _, _, _, _, _} = Query) -> - encode_bytestreams(Query, - [{<<"xmlns">>, - <<"http://jabber.org/protocol/bytestreams">>}]); -encode({muc_history, _, _, _, _} = History) -> - encode_muc_history(History, - [{<<"xmlns">>, <<"http://jabber.org/protocol/muc">>}]); -encode({muc_decline, _, _, _} = Decline) -> - encode_muc_user_decline(Decline, - [{<<"xmlns">>, - <<"http://jabber.org/protocol/muc#user">>}]); -encode({muc_user_destroy, _, _} = Destroy) -> - encode_muc_user_destroy(Destroy, - [{<<"xmlns">>, - <<"http://jabber.org/protocol/muc#user">>}]); -encode({muc_invite, _, _, _} = Invite) -> - encode_muc_user_invite(Invite, - [{<<"xmlns">>, - <<"http://jabber.org/protocol/muc#user">>}]); -encode({muc_user, _, _, _, _, _, _} = X) -> - encode_muc_user(X, - [{<<"xmlns">>, - <<"http://jabber.org/protocol/muc#user">>}]); -encode({muc_owner_destroy, _, _, _} = Destroy) -> - encode_muc_owner_destroy(Destroy, - [{<<"xmlns">>, - <<"http://jabber.org/protocol/muc#owner">>}]); -encode({muc_owner, _, _} = Query) -> - encode_muc_owner(Query, - [{<<"xmlns">>, - <<"http://jabber.org/protocol/muc#owner">>}]); -encode({muc_item, _, _, _, _, _, _, _} = Item) -> - encode_muc_admin_item(Item, []); -encode({muc_actor, _, _} = Actor) -> - encode_muc_admin_actor(Actor, []); -encode({muc_admin, _} = Query) -> - encode_muc_admin(Query, - [{<<"xmlns">>, - <<"http://jabber.org/protocol/muc#admin">>}]); -encode({muc, _, _} = X) -> - encode_muc(X, - [{<<"xmlns">>, <<"http://jabber.org/protocol/muc">>}]); -encode({forwarded, _, _} = Forwarded) -> - encode_forwarded(Forwarded, - [{<<"xmlns">>, <<"urn:xmpp:forward:0">>}]); -encode({carbons_disable} = Disable) -> - encode_carbons_disable(Disable, - [{<<"xmlns">>, <<"urn:xmpp:carbons:2">>}]); -encode({carbons_enable} = Enable) -> - encode_carbons_enable(Enable, - [{<<"xmlns">>, <<"urn:xmpp:carbons:2">>}]); -encode({carbons_private} = Private) -> - encode_carbons_private(Private, - [{<<"xmlns">>, <<"urn:xmpp:carbons:2">>}]); -encode({carbons_received, _} = Received) -> - encode_carbons_received(Received, - [{<<"xmlns">>, <<"urn:xmpp:carbons:2">>}]); -encode({carbons_sent, _} = Sent) -> - encode_carbons_sent(Sent, - [{<<"xmlns">>, <<"urn:xmpp:carbons:2">>}]); -encode({feature_csi, _} = Csi) -> - encode_feature_csi(Csi, []); -encode({csi, active} = Active) -> - encode_csi_active(Active, - [{<<"xmlns">>, <<"urn:xmpp:csi:0">>}]); -encode({csi, inactive} = Inactive) -> - encode_csi_inactive(Inactive, - [{<<"xmlns">>, <<"urn:xmpp:csi:0">>}]); -encode({feature_sm, _} = Sm) -> - encode_feature_sm(Sm, []); -encode({sm_enable, _, _, _} = Enable) -> - encode_sm_enable(Enable, []); -encode({sm_enabled, _, _, _, _, _} = Enabled) -> - encode_sm_enabled(Enabled, []); -encode({sm_resume, _, _, _} = Resume) -> - encode_sm_resume(Resume, []); -encode({sm_resumed, _, _, _} = Resumed) -> - encode_sm_resumed(Resumed, []); -encode({sm_r, _} = R) -> encode_sm_r(R, []); -encode({sm_a, _, _} = A) -> encode_sm_a(A, []); -encode({sm_failed, _, _} = Failed) -> - encode_sm_failed(Failed, []). - -get_ns({last, _, _}) -> <<"jabber:iq:last">>; -get_ns({version, _, _, _}) -> <<"jabber:iq:version">>; -get_ns({roster_item, _, _, _, _, _}) -> - <<"jabber:iq:roster">>; -get_ns({roster, _, _}) -> <<"jabber:iq:roster">>; -get_ns({privacy_item, _, _, _, _, _}) -> - <<"jabber:iq:privacy">>; -get_ns({privacy_list, _, _}) -> <<"jabber:iq:privacy">>; -get_ns({privacy, _, _, _}) -> <<"jabber:iq:privacy">>; -get_ns({block, _}) -> <<"urn:xmpp:blocking">>; -get_ns({unblock, _}) -> <<"urn:xmpp:blocking">>; -get_ns({block_list}) -> <<"urn:xmpp:blocking">>; -get_ns({identity, _, _, _, _}) -> - <<"http://jabber.org/protocol/disco#info">>; -get_ns({disco_info, _, _, _, _}) -> - <<"http://jabber.org/protocol/disco#info">>; -get_ns({disco_item, _, _, _}) -> - <<"http://jabber.org/protocol/disco#items">>; -get_ns({disco_items, _, _}) -> - <<"http://jabber.org/protocol/disco#items">>; -get_ns({private, _}) -> <<"jabber:iq:private">>; -get_ns({bookmark_conference, _, _, _, _, _}) -> - <<"storage:bookmarks">>; -get_ns({bookmark_url, _, _}) -> <<"storage:bookmarks">>; -get_ns({bookmark_storage, _, _}) -> - <<"storage:bookmarks">>; -get_ns({stat, _, _, _, _}) -> - <<"http://jabber.org/protocol/stats">>; -get_ns({stats, _}) -> - <<"http://jabber.org/protocol/stats">>; -get_ns({iq, _, _, _, _, _, _, _}) -> - <<"jabber:client">>; -get_ns({message, _, _, _, _, _, _, _, _, _, _}) -> - <<"jabber:client">>; -get_ns({presence, _, _, _, _, _, _, _, _, _, _}) -> - <<"jabber:client">>; -get_ns({gone, _}) -> - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>; -get_ns({redirect, _}) -> - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>; -get_ns({error, _, _, _, _}) -> <<"jabber:client">>; -get_ns({bind, _, _}) -> - <<"urn:ietf:params:xml:ns:xmpp-bind">>; -get_ns({sasl_auth, _, _}) -> - <<"urn:ietf:params:xml:ns:xmpp-sasl">>; -get_ns({sasl_abort}) -> - <<"urn:ietf:params:xml:ns:xmpp-sasl">>; -get_ns({sasl_challenge, _}) -> - <<"urn:ietf:params:xml:ns:xmpp-sasl">>; -get_ns({sasl_response, _}) -> - <<"urn:ietf:params:xml:ns:xmpp-sasl">>; -get_ns({sasl_success, _}) -> - <<"urn:ietf:params:xml:ns:xmpp-sasl">>; -get_ns({sasl_failure, _, _}) -> - <<"urn:ietf:params:xml:ns:xmpp-sasl">>; -get_ns({sasl_mechanisms, _}) -> - <<"urn:ietf:params:xml:ns:xmpp-sasl">>; -get_ns({starttls, _}) -> - <<"urn:ietf:params:xml:ns:xmpp-tls">>; -get_ns({starttls_proceed}) -> - <<"urn:ietf:params:xml:ns:xmpp-tls">>; -get_ns({starttls_failure}) -> - <<"urn:ietf:params:xml:ns:xmpp-tls">>; -get_ns({compress_failure, _}) -> - <<"http://jabber.org/protocol/compress">>; -get_ns({compress, _}) -> - <<"http://jabber.org/protocol/compress">>; -get_ns({compressed}) -> - <<"http://jabber.org/protocol/compress">>; -get_ns({compression, _}) -> - <<"http://jabber.org/features/compress">>; -get_ns({stream_features, _}) -> - <<"http://etherx.jabber.org/streams">>; -get_ns({p1_push}) -> <<"p1:push">>; -get_ns({p1_rebind}) -> <<"p1:rebind">>; -get_ns({p1_ack}) -> <<"p1:ack">>; -get_ns({caps, _, _, _}) -> - <<"http://jabber.org/protocol/caps">>; -get_ns({feature_register}) -> - <<"http://jabber.org/features/iq-register">>; -get_ns({register, _, _, _, _, _, _, _, _, _, _, _, _, _, - _, _, _, _, _, _, _, _}) -> - <<"jabber:iq:register">>; -get_ns({session}) -> - <<"urn:ietf:params:xml:ns:xmpp-session">>; -get_ns({ping}) -> <<"urn:xmpp:ping">>; -get_ns({time, _, _}) -> <<"urn:xmpp:time">>; -get_ns({'see-other-host', _}) -> - <<"urn:ietf:params:xml:ns:xmpp-streams">>; -get_ns({stream_error, _, _}) -> - <<"http://etherx.jabber.org/streams">>; -get_ns({vcard_name, _, _, _, _, _}) -> <<"vcard-temp">>; -get_ns({vcard_adr, _, _, _, _, _, _, _, _, _, _, _, _, - _, _}) -> - <<"vcard-temp">>; -get_ns({vcard_label, _, _, _, _, _, _, _, _}) -> - <<"vcard-temp">>; -get_ns({vcard_tel, _, _, _, _, _, _, _, _, _, _, _, _, - _, _}) -> - <<"vcard-temp">>; -get_ns({vcard_email, _, _, _, _, _, _}) -> - <<"vcard-temp">>; -get_ns({vcard_geo, _, _}) -> <<"vcard-temp">>; -get_ns({vcard_logo, _, _, _}) -> <<"vcard-temp">>; -get_ns({vcard_photo, _, _, _}) -> <<"vcard-temp">>; -get_ns({vcard_org, _, _}) -> <<"vcard-temp">>; -get_ns({vcard_sound, _, _, _}) -> <<"vcard-temp">>; -get_ns({vcard_key, _, _}) -> <<"vcard-temp">>; -get_ns({vcard, _, _, _, _, _, _, _, _, _, _, _, _, _, _, - _, _, _, _, _, _, _, _, _, _, _, _, _, _, _}) -> - <<"vcard-temp">>; -get_ns({vcard_xupdate, _}) -> <<"vcard-temp:x:update">>; -get_ns({xdata_field, _, _, _, _, _, _, _}) -> - <<"jabber:x:data">>; -get_ns({xdata, _, _, _, _, _, _}) -> - <<"jabber:x:data">>; -get_ns({pubsub_subscription, _, _, _, _}) -> - <<"http://jabber.org/protocol/pubsub">>; -get_ns({pubsub_affiliation, _, _}) -> - <<"http://jabber.org/protocol/pubsub">>; -get_ns({pubsub_item, _, _}) -> - <<"http://jabber.org/protocol/pubsub">>; -get_ns({pubsub_items, _, _, _, _}) -> - <<"http://jabber.org/protocol/pubsub">>; -get_ns({pubsub_event_item, _, _, _}) -> - <<"http://jabber.org/protocol/pubsub#event">>; -get_ns({pubsub_event_items, _, _, _}) -> - <<"http://jabber.org/protocol/pubsub#event">>; -get_ns({pubsub_event, _}) -> - <<"http://jabber.org/protocol/pubsub#event">>; -get_ns({pubsub_subscribe, _, _}) -> - <<"http://jabber.org/protocol/pubsub">>; -get_ns({pubsub_unsubscribe, _, _, _}) -> - <<"http://jabber.org/protocol/pubsub">>; -get_ns({pubsub_publish, _, _}) -> - <<"http://jabber.org/protocol/pubsub">>; -get_ns({pubsub_options, _, _, _, _}) -> - <<"http://jabber.org/protocol/pubsub">>; -get_ns({pubsub_retract, _, _, _}) -> - <<"http://jabber.org/protocol/pubsub">>; -get_ns({pubsub, _, _, _, _, _, _, _, _}) -> - <<"http://jabber.org/protocol/pubsub">>; -get_ns({shim, _}) -> - <<"http://jabber.org/protocol/shim">>; -get_ns({chatstate, active}) -> - <<"http://jabber.org/protocol/chatstates">>; -get_ns({chatstate, composing}) -> - <<"http://jabber.org/protocol/chatstates">>; -get_ns({chatstate, gone}) -> - <<"http://jabber.org/protocol/chatstates">>; -get_ns({chatstate, inactive}) -> - <<"http://jabber.org/protocol/chatstates">>; -get_ns({chatstate, paused}) -> - <<"http://jabber.org/protocol/chatstates">>; -get_ns({delay, _, _}) -> <<"urn:xmpp:delay">>; -get_ns({legacy_delay, _, _}) -> <<"jabber:x:delay">>; -get_ns({streamhost, _, _, _}) -> - <<"http://jabber.org/protocol/bytestreams">>; -get_ns({bytestreams, _, _, _, _, _, _}) -> - <<"http://jabber.org/protocol/bytestreams">>; -get_ns({muc_history, _, _, _, _}) -> - <<"http://jabber.org/protocol/muc">>; -get_ns({muc_decline, _, _, _}) -> - <<"http://jabber.org/protocol/muc#user">>; -get_ns({muc_user_destroy, _, _}) -> - <<"http://jabber.org/protocol/muc#user">>; -get_ns({muc_invite, _, _, _}) -> - <<"http://jabber.org/protocol/muc#user">>; -get_ns({muc_user, _, _, _, _, _, _}) -> - <<"http://jabber.org/protocol/muc#user">>; -get_ns({muc_owner_destroy, _, _, _}) -> - <<"http://jabber.org/protocol/muc#owner">>; -get_ns({muc_owner, _, _}) -> - <<"http://jabber.org/protocol/muc#owner">>; -get_ns({muc_admin, _}) -> - <<"http://jabber.org/protocol/muc#admin">>; -get_ns({muc, _, _}) -> - <<"http://jabber.org/protocol/muc">>; -get_ns({forwarded, _, _}) -> <<"urn:xmpp:forward:0">>; -get_ns({carbons_disable}) -> <<"urn:xmpp:carbons:2">>; -get_ns({carbons_enable}) -> <<"urn:xmpp:carbons:2">>; -get_ns({carbons_private}) -> <<"urn:xmpp:carbons:2">>; -get_ns({carbons_received, _}) -> - <<"urn:xmpp:carbons:2">>; -get_ns({carbons_sent, _}) -> <<"urn:xmpp:carbons:2">>; -get_ns({feature_csi, _}) -> <<"urn:xmpp:csi:0">>; -get_ns({csi, active}) -> <<"urn:xmpp:csi:0">>; -get_ns({csi, inactive}) -> <<"urn:xmpp:csi:0">>; -get_ns(_) -> <<>>. - -dec_int(Val) -> dec_int(Val, infinity, infinity). - -dec_int(Val, Min, Max) -> - case list_to_integer(binary_to_list(Val)) of - Int when Int =< Max, Min == infinity -> Int; - Int when Int =< Max, Int >= Min -> Int - end. - -enc_int(Int) -> list_to_binary(integer_to_list(Int)). - -dec_enum(Val, Enums) -> - AtomVal = erlang:binary_to_existing_atom(Val, utf8), - case lists:member(AtomVal, Enums) of - true -> AtomVal - end. - -enc_enum(Atom) -> erlang:atom_to_binary(Atom, utf8). - -format_error({bad_attr_value, Attr, Tag, XMLNS}) -> - <<"Bad value of attribute '", Attr/binary, "' in tag <", - Tag/binary, "/> qualified by namespace '", XMLNS/binary, - "'">>; -format_error({bad_cdata_value, <<>>, Tag, XMLNS}) -> - <<"Bad value of cdata in tag <", Tag/binary, - "/> qualified by namespace '", XMLNS/binary, "'">>; -format_error({missing_tag, Tag, XMLNS}) -> - <<"Missing tag <", Tag/binary, - "/> qualified by namespace '", XMLNS/binary, "'">>; -format_error({missing_attr, Attr, Tag, XMLNS}) -> - <<"Missing attribute '", Attr/binary, "' in tag <", - Tag/binary, "/> qualified by namespace '", XMLNS/binary, - "'">>; -format_error({missing_cdata, <<>>, Tag, XMLNS}) -> - <<"Missing cdata in tag <", Tag/binary, - "/> qualified by namespace '", XMLNS/binary, "'">>; -format_error({unknown_tag, Tag, XMLNS}) -> - <<"Unknown tag <", Tag/binary, - "/> qualified by namespace '", XMLNS/binary, "'">>. - -get_attr(Attr, Attrs) -> - case lists:keyfind(Attr, 1, Attrs) of - {_, Val} -> Val; - false -> <<>> - end. - -pp(Term) -> io_lib_pretty:print(Term, fun pp/2). - -pp(last, 2) -> [seconds, text]; -pp(version, 3) -> [name, ver, os]; -pp(roster_item, 5) -> - [jid, name, groups, subscription, ask]; -pp(roster, 2) -> [items, ver]; -pp(privacy_item, 5) -> - [order, action, type, value, kinds]; -pp(privacy_list, 2) -> [name, items]; -pp(privacy, 3) -> [lists, default, active]; -pp(block, 1) -> [items]; -pp(unblock, 1) -> [items]; -pp(block_list, 0) -> []; -pp(identity, 4) -> [category, type, lang, name]; -pp(disco_info, 4) -> - [node, identities, features, xdata]; -pp(disco_item, 3) -> [jid, name, node]; -pp(disco_items, 2) -> [node, items]; -pp(private, 1) -> [xml_els]; -pp(bookmark_conference, 5) -> - [name, jid, autojoin, nick, password]; -pp(bookmark_url, 2) -> [name, url]; -pp(bookmark_storage, 2) -> [conference, url]; -pp(stat, 4) -> [name, units, value, error]; -pp(stats, 1) -> [stat]; -pp(iq, 7) -> [id, type, lang, from, to, error, sub_els]; -pp(message, 10) -> - [id, type, lang, from, to, subject, body, thread, error, - sub_els]; -pp(presence, 10) -> - [id, type, lang, from, to, show, status, priority, - error, sub_els]; -pp(gone, 1) -> [uri]; -pp(redirect, 1) -> [uri]; -pp(error, 4) -> [type, by, reason, text]; -pp(bind, 2) -> [jid, resource]; -pp(sasl_auth, 2) -> [mechanism, text]; -pp(sasl_abort, 0) -> []; -pp(sasl_challenge, 1) -> [text]; -pp(sasl_response, 1) -> [text]; -pp(sasl_success, 1) -> [text]; -pp(sasl_failure, 2) -> [reason, text]; -pp(sasl_mechanisms, 1) -> [list]; -pp(starttls, 1) -> [required]; -pp(starttls_proceed, 0) -> []; -pp(starttls_failure, 0) -> []; -pp(compress_failure, 1) -> [reason]; -pp(compress, 1) -> [methods]; -pp(compressed, 0) -> []; -pp(compression, 1) -> [methods]; -pp(stream_features, 1) -> [sub_els]; -pp(p1_push, 0) -> []; -pp(p1_rebind, 0) -> []; -pp(p1_ack, 0) -> []; -pp(caps, 3) -> [hash, node, ver]; -pp(feature_register, 0) -> []; -pp(register, 21) -> - [registered, remove, instructions, username, nick, - password, name, first, last, email, address, city, - state, zip, phone, url, date, misc, text, key, xdata]; -pp(session, 0) -> []; -pp(ping, 0) -> []; -pp(time, 2) -> [tzo, utc]; -pp(text, 2) -> [lang, data]; -pp('see-other-host', 1) -> [host]; -pp(stream_error, 2) -> [reason, text]; -pp(vcard_name, 5) -> - [family, given, middle, prefix, suffix]; -pp(vcard_adr, 14) -> - [home, work, postal, parcel, dom, intl, pref, pobox, - extadd, street, locality, region, pcode, ctry]; -pp(vcard_label, 8) -> - [home, work, postal, parcel, dom, intl, pref, line]; -pp(vcard_tel, 14) -> - [home, work, voice, fax, pager, msg, cell, video, bbs, - modem, isdn, pcs, pref, number]; -pp(vcard_email, 6) -> - [home, work, internet, pref, x400, userid]; -pp(vcard_geo, 2) -> [lat, lon]; -pp(vcard_logo, 3) -> [type, binval, extval]; -pp(vcard_photo, 3) -> [type, binval, extval]; -pp(vcard_org, 2) -> [name, units]; -pp(vcard_sound, 3) -> [phonetic, binval, extval]; -pp(vcard_key, 2) -> [type, cred]; -pp(vcard, 29) -> - [version, fn, n, nickname, photo, bday, adr, label, tel, - email, jabberid, mailer, tz, geo, title, role, logo, - org, categories, note, prodid, rev, sort_string, sound, - uid, url, class, key, desc]; -pp(vcard_xupdate, 1) -> [photo]; -pp(xdata_field, 7) -> - [label, type, var, required, desc, values, options]; -pp(xdata, 6) -> - [type, instructions, title, reported, items, fields]; -pp(pubsub_subscription, 4) -> [jid, node, subid, type]; -pp(pubsub_affiliation, 2) -> [node, type]; -pp(pubsub_item, 2) -> [id, xml_els]; -pp(pubsub_items, 4) -> [node, max_items, subid, items]; -pp(pubsub_event_item, 3) -> [id, node, publisher]; -pp(pubsub_event_items, 3) -> [node, retract, items]; -pp(pubsub_event, 1) -> [items]; -pp(pubsub_subscribe, 2) -> [node, jid]; -pp(pubsub_unsubscribe, 3) -> [node, jid, subid]; -pp(pubsub_publish, 2) -> [node, items]; -pp(pubsub_options, 4) -> [node, jid, subid, xdata]; -pp(pubsub_retract, 3) -> [node, notify, items]; -pp(pubsub, 8) -> - [subscriptions, affiliations, publish, subscribe, - unsubscribe, options, items, retract]; -pp(shim, 1) -> [headers]; -pp(chatstate, 1) -> [type]; -pp(delay, 2) -> [stamp, from]; -pp(legacy_delay, 2) -> [stamp, from]; -pp(streamhost, 3) -> [jid, host, port]; -pp(bytestreams, 6) -> - [hosts, used, activate, dstaddr, mode, sid]; -pp(muc_history, 4) -> - [maxchars, maxstanzas, seconds, since]; -pp(muc_decline, 3) -> [reason, from, to]; -pp(muc_user_destroy, 2) -> [reason, jid]; -pp(muc_invite, 3) -> [reason, from, to]; -pp(muc_user, 6) -> - [decline, destroy, invites, items, status_codes, - password]; -pp(muc_owner_destroy, 3) -> [jid, reason, password]; -pp(muc_owner, 2) -> [destroy, config]; -pp(muc_item, 7) -> - [actor, continue, reason, affiliation, role, jid, nick]; -pp(muc_actor, 2) -> [jid, nick]; -pp(muc_admin, 1) -> [items]; -pp(muc, 2) -> [history, password]; -pp(forwarded, 2) -> [delay, sub_els]; -pp(carbons_disable, 0) -> []; -pp(carbons_enable, 0) -> []; -pp(carbons_private, 0) -> []; -pp(carbons_received, 1) -> [forwarded]; -pp(carbons_sent, 1) -> [forwarded]; -pp(feature_csi, 1) -> [xmlns]; -pp(csi, 1) -> [type]; -pp(feature_sm, 1) -> [xmlns]; -pp(sm_enable, 3) -> [max, resume, xmlns]; -pp(sm_enabled, 5) -> [id, location, max, resume, xmlns]; -pp(sm_resume, 3) -> [h, previd, xmlns]; -pp(sm_resumed, 3) -> [h, previd, xmlns]; -pp(sm_r, 1) -> [xmlns]; -pp(sm_a, 2) -> [h, xmlns]; -pp(sm_failed, 2) -> [reason, xmlns]; -pp(_, _) -> no. - -enc_bool(false) -> <<"false">>; -enc_bool(true) -> <<"true">>. - -dec_bool(<<"false">>) -> false; -dec_bool(<<"0">>) -> false; -dec_bool(<<"true">>) -> true; -dec_bool(<<"1">>) -> true. - -resourceprep(R) -> - case jlib:resourceprep(R) of - error -> erlang:error(badarg); - R1 -> R1 - end. - -enc_jid(J) -> jlib:jid_to_string(J). - -dec_jid(Val) -> - case jlib:string_to_jid(Val) of - error -> erlang:error(badarg); - J -> J - end. - -enc_utc(Val) -> jlib:now_to_utc_string(Val). - -dec_utc(Val) -> - {_, _, _} = jlib:datetime_string_to_timestamp(Val). - -enc_tzo({H, M}) -> - Sign = if H >= 0 -> <<>>; - true -> <<"-">> - end, - list_to_binary([Sign, - io_lib:format("~2..0w:~2..0w", [H, M])]). - -dec_tzo(Val) -> - [H1, M1] = str:tokens(Val, <<":">>), - H = jlib:binary_to_integer(H1), - M = jlib:binary_to_integer(M1), - if H >= -12, H =< 12, M >= 0, M < 60 -> {H, M} end. - -decode_sm_failed(__TopXMLNS, __IgnoreEls, - {xmlel, <<"failed">>, _attrs, _els}) -> - Reason = decode_sm_failed_els(__TopXMLNS, __IgnoreEls, - _els, undefined), - Xmlns = decode_sm_failed_attrs(__TopXMLNS, _attrs, - undefined), - {sm_failed, Reason, Xmlns}. - -decode_sm_failed_els(__TopXMLNS, __IgnoreEls, [], - Reason) -> - Reason; -decode_sm_failed_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"bad-request">>, _attrs, _} = _el | _els], - Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-stanzas">> -> - decode_sm_failed_els(__TopXMLNS, __IgnoreEls, _els, - decode_error_bad_request(_xmlns, __IgnoreEls, - _el)); - true -> - decode_sm_failed_els(__TopXMLNS, __IgnoreEls, _els, - Reason) - end; -decode_sm_failed_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"conflict">>, _attrs, _} = _el | _els], - Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-stanzas">> -> - decode_sm_failed_els(__TopXMLNS, __IgnoreEls, _els, - decode_error_conflict(_xmlns, __IgnoreEls, - _el)); - true -> - decode_sm_failed_els(__TopXMLNS, __IgnoreEls, _els, - Reason) - end; -decode_sm_failed_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"feature-not-implemented">>, _attrs, _} = _el - | _els], - Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-stanzas">> -> - decode_sm_failed_els(__TopXMLNS, __IgnoreEls, _els, - decode_error_feature_not_implemented(_xmlns, - __IgnoreEls, - _el)); - true -> - decode_sm_failed_els(__TopXMLNS, __IgnoreEls, _els, - Reason) - end; -decode_sm_failed_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"forbidden">>, _attrs, _} = _el | _els], - Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-stanzas">> -> - decode_sm_failed_els(__TopXMLNS, __IgnoreEls, _els, - decode_error_forbidden(_xmlns, __IgnoreEls, - _el)); - true -> - decode_sm_failed_els(__TopXMLNS, __IgnoreEls, _els, - Reason) - end; -decode_sm_failed_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"gone">>, _attrs, _} = _el | _els], - Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-stanzas">> -> - decode_sm_failed_els(__TopXMLNS, __IgnoreEls, _els, - decode_error_gone(_xmlns, __IgnoreEls, _el)); - true -> - decode_sm_failed_els(__TopXMLNS, __IgnoreEls, _els, - Reason) - end; -decode_sm_failed_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"internal-server-error">>, _attrs, _} = _el - | _els], - Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-stanzas">> -> - decode_sm_failed_els(__TopXMLNS, __IgnoreEls, _els, - decode_error_internal_server_error(_xmlns, - __IgnoreEls, - _el)); - true -> - decode_sm_failed_els(__TopXMLNS, __IgnoreEls, _els, - Reason) - end; -decode_sm_failed_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"item-not-found">>, _attrs, _} = _el | _els], - Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-stanzas">> -> - decode_sm_failed_els(__TopXMLNS, __IgnoreEls, _els, - decode_error_item_not_found(_xmlns, __IgnoreEls, - _el)); - true -> - decode_sm_failed_els(__TopXMLNS, __IgnoreEls, _els, - Reason) - end; -decode_sm_failed_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"jid-malformed">>, _attrs, _} = _el | _els], - Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-stanzas">> -> - decode_sm_failed_els(__TopXMLNS, __IgnoreEls, _els, - decode_error_jid_malformed(_xmlns, __IgnoreEls, - _el)); - true -> - decode_sm_failed_els(__TopXMLNS, __IgnoreEls, _els, - Reason) - end; -decode_sm_failed_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"not-acceptable">>, _attrs, _} = _el | _els], - Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-stanzas">> -> - decode_sm_failed_els(__TopXMLNS, __IgnoreEls, _els, - decode_error_not_acceptable(_xmlns, __IgnoreEls, - _el)); - true -> - decode_sm_failed_els(__TopXMLNS, __IgnoreEls, _els, - Reason) - end; -decode_sm_failed_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"not-allowed">>, _attrs, _} = _el | _els], - Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-stanzas">> -> - decode_sm_failed_els(__TopXMLNS, __IgnoreEls, _els, - decode_error_not_allowed(_xmlns, __IgnoreEls, - _el)); - true -> - decode_sm_failed_els(__TopXMLNS, __IgnoreEls, _els, - Reason) - end; -decode_sm_failed_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"not-authorized">>, _attrs, _} = _el | _els], - Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-stanzas">> -> - decode_sm_failed_els(__TopXMLNS, __IgnoreEls, _els, - decode_error_not_authorized(_xmlns, __IgnoreEls, - _el)); - true -> - decode_sm_failed_els(__TopXMLNS, __IgnoreEls, _els, - Reason) - end; -decode_sm_failed_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"policy-violation">>, _attrs, _} = _el - | _els], - Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-stanzas">> -> - decode_sm_failed_els(__TopXMLNS, __IgnoreEls, _els, - decode_error_policy_violation(_xmlns, - __IgnoreEls, - _el)); - true -> - decode_sm_failed_els(__TopXMLNS, __IgnoreEls, _els, - Reason) - end; -decode_sm_failed_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"recipient-unavailable">>, _attrs, _} = _el - | _els], - Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-stanzas">> -> - decode_sm_failed_els(__TopXMLNS, __IgnoreEls, _els, - decode_error_recipient_unavailable(_xmlns, - __IgnoreEls, - _el)); - true -> - decode_sm_failed_els(__TopXMLNS, __IgnoreEls, _els, - Reason) - end; -decode_sm_failed_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"redirect">>, _attrs, _} = _el | _els], - Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-stanzas">> -> - decode_sm_failed_els(__TopXMLNS, __IgnoreEls, _els, - decode_error_redirect(_xmlns, __IgnoreEls, - _el)); - true -> - decode_sm_failed_els(__TopXMLNS, __IgnoreEls, _els, - Reason) - end; -decode_sm_failed_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"registration-required">>, _attrs, _} = _el - | _els], - Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-stanzas">> -> - decode_sm_failed_els(__TopXMLNS, __IgnoreEls, _els, - decode_error_registration_required(_xmlns, - __IgnoreEls, - _el)); - true -> - decode_sm_failed_els(__TopXMLNS, __IgnoreEls, _els, - Reason) - end; -decode_sm_failed_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"remote-server-not-found">>, _attrs, _} = _el - | _els], - Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-stanzas">> -> - decode_sm_failed_els(__TopXMLNS, __IgnoreEls, _els, - decode_error_remote_server_not_found(_xmlns, - __IgnoreEls, - _el)); - true -> - decode_sm_failed_els(__TopXMLNS, __IgnoreEls, _els, - Reason) - end; -decode_sm_failed_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"remote-server-timeout">>, _attrs, _} = _el - | _els], - Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-stanzas">> -> - decode_sm_failed_els(__TopXMLNS, __IgnoreEls, _els, - decode_error_remote_server_timeout(_xmlns, - __IgnoreEls, - _el)); - true -> - decode_sm_failed_els(__TopXMLNS, __IgnoreEls, _els, - Reason) - end; -decode_sm_failed_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"resource-constraint">>, _attrs, _} = _el - | _els], - Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-stanzas">> -> - decode_sm_failed_els(__TopXMLNS, __IgnoreEls, _els, - decode_error_resource_constraint(_xmlns, - __IgnoreEls, - _el)); - true -> - decode_sm_failed_els(__TopXMLNS, __IgnoreEls, _els, - Reason) - end; -decode_sm_failed_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"service-unavailable">>, _attrs, _} = _el - | _els], - Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-stanzas">> -> - decode_sm_failed_els(__TopXMLNS, __IgnoreEls, _els, - decode_error_service_unavailable(_xmlns, - __IgnoreEls, - _el)); - true -> - decode_sm_failed_els(__TopXMLNS, __IgnoreEls, _els, - Reason) - end; -decode_sm_failed_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"subscription-required">>, _attrs, _} = _el - | _els], - Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-stanzas">> -> - decode_sm_failed_els(__TopXMLNS, __IgnoreEls, _els, - decode_error_subscription_required(_xmlns, - __IgnoreEls, - _el)); - true -> - decode_sm_failed_els(__TopXMLNS, __IgnoreEls, _els, - Reason) - end; -decode_sm_failed_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"undefined-condition">>, _attrs, _} = _el - | _els], - Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-stanzas">> -> - decode_sm_failed_els(__TopXMLNS, __IgnoreEls, _els, - decode_error_undefined_condition(_xmlns, - __IgnoreEls, - _el)); - true -> - decode_sm_failed_els(__TopXMLNS, __IgnoreEls, _els, - Reason) - end; -decode_sm_failed_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"unexpected-request">>, _attrs, _} = _el - | _els], - Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-stanzas">> -> - decode_sm_failed_els(__TopXMLNS, __IgnoreEls, _els, - decode_error_unexpected_request(_xmlns, - __IgnoreEls, - _el)); - true -> - decode_sm_failed_els(__TopXMLNS, __IgnoreEls, _els, - Reason) - end; -decode_sm_failed_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Reason) -> - decode_sm_failed_els(__TopXMLNS, __IgnoreEls, _els, - Reason). - -decode_sm_failed_attrs(__TopXMLNS, - [{<<"xmlns">>, _val} | _attrs], _Xmlns) -> - decode_sm_failed_attrs(__TopXMLNS, _attrs, _val); -decode_sm_failed_attrs(__TopXMLNS, [_ | _attrs], - Xmlns) -> - decode_sm_failed_attrs(__TopXMLNS, _attrs, Xmlns); -decode_sm_failed_attrs(__TopXMLNS, [], Xmlns) -> - decode_sm_failed_attr_xmlns(__TopXMLNS, Xmlns). - -encode_sm_failed({sm_failed, Reason, Xmlns}, - _xmlns_attrs) -> - _els = lists:reverse('encode_sm_failed_$reason'(Reason, - [])), - _attrs = encode_sm_failed_attr_xmlns(Xmlns, - _xmlns_attrs), - {xmlel, <<"failed">>, _attrs, _els}. - -'encode_sm_failed_$reason'(undefined, _acc) -> _acc; -'encode_sm_failed_$reason'('bad-request' = Reason, - _acc) -> - [encode_error_bad_request(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>}]) - | _acc]; -'encode_sm_failed_$reason'(conflict = Reason, _acc) -> - [encode_error_conflict(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>}]) - | _acc]; -'encode_sm_failed_$reason'('feature-not-implemented' = - Reason, - _acc) -> - [encode_error_feature_not_implemented(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>}]) - | _acc]; -'encode_sm_failed_$reason'(forbidden = Reason, _acc) -> - [encode_error_forbidden(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>}]) - | _acc]; -'encode_sm_failed_$reason'({gone, _} = Reason, _acc) -> - [encode_error_gone(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>}]) - | _acc]; -'encode_sm_failed_$reason'('internal-server-error' = - Reason, - _acc) -> - [encode_error_internal_server_error(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>}]) - | _acc]; -'encode_sm_failed_$reason'('item-not-found' = Reason, - _acc) -> - [encode_error_item_not_found(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>}]) - | _acc]; -'encode_sm_failed_$reason'('jid-malformed' = Reason, - _acc) -> - [encode_error_jid_malformed(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>}]) - | _acc]; -'encode_sm_failed_$reason'('not-acceptable' = Reason, - _acc) -> - [encode_error_not_acceptable(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>}]) - | _acc]; -'encode_sm_failed_$reason'('not-allowed' = Reason, - _acc) -> - [encode_error_not_allowed(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>}]) - | _acc]; -'encode_sm_failed_$reason'('not-authorized' = Reason, - _acc) -> - [encode_error_not_authorized(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>}]) - | _acc]; -'encode_sm_failed_$reason'('policy-violation' = Reason, - _acc) -> - [encode_error_policy_violation(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>}]) - | _acc]; -'encode_sm_failed_$reason'('recipient-unavailable' = - Reason, - _acc) -> - [encode_error_recipient_unavailable(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>}]) - | _acc]; -'encode_sm_failed_$reason'({redirect, _} = Reason, - _acc) -> - [encode_error_redirect(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>}]) - | _acc]; -'encode_sm_failed_$reason'('registration-required' = - Reason, - _acc) -> - [encode_error_registration_required(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>}]) - | _acc]; -'encode_sm_failed_$reason'('remote-server-not-found' = - Reason, - _acc) -> - [encode_error_remote_server_not_found(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>}]) - | _acc]; -'encode_sm_failed_$reason'('remote-server-timeout' = - Reason, - _acc) -> - [encode_error_remote_server_timeout(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>}]) - | _acc]; -'encode_sm_failed_$reason'('resource-constraint' = - Reason, - _acc) -> - [encode_error_resource_constraint(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>}]) - | _acc]; -'encode_sm_failed_$reason'('service-unavailable' = - Reason, - _acc) -> - [encode_error_service_unavailable(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>}]) - | _acc]; -'encode_sm_failed_$reason'('subscription-required' = - Reason, - _acc) -> - [encode_error_subscription_required(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>}]) - | _acc]; -'encode_sm_failed_$reason'('undefined-condition' = - Reason, - _acc) -> - [encode_error_undefined_condition(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>}]) - | _acc]; -'encode_sm_failed_$reason'('unexpected-request' = - Reason, - _acc) -> - [encode_error_unexpected_request(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>}]) - | _acc]. - -decode_sm_failed_attr_xmlns(__TopXMLNS, undefined) -> - undefined; -decode_sm_failed_attr_xmlns(__TopXMLNS, _val) -> _val. - -encode_sm_failed_attr_xmlns(undefined, _acc) -> _acc; -encode_sm_failed_attr_xmlns(_val, _acc) -> - [{<<"xmlns">>, _val} | _acc]. - -decode_sm_a(__TopXMLNS, __IgnoreEls, - {xmlel, <<"a">>, _attrs, _els}) -> - {H, Xmlns} = decode_sm_a_attrs(__TopXMLNS, _attrs, - undefined, undefined), - {sm_a, H, Xmlns}. - -decode_sm_a_attrs(__TopXMLNS, - [{<<"h">>, _val} | _attrs], _H, Xmlns) -> - decode_sm_a_attrs(__TopXMLNS, _attrs, _val, Xmlns); -decode_sm_a_attrs(__TopXMLNS, - [{<<"xmlns">>, _val} | _attrs], H, _Xmlns) -> - decode_sm_a_attrs(__TopXMLNS, _attrs, H, _val); -decode_sm_a_attrs(__TopXMLNS, [_ | _attrs], H, Xmlns) -> - decode_sm_a_attrs(__TopXMLNS, _attrs, H, Xmlns); -decode_sm_a_attrs(__TopXMLNS, [], H, Xmlns) -> - {decode_sm_a_attr_h(__TopXMLNS, H), - decode_sm_a_attr_xmlns(__TopXMLNS, Xmlns)}. - -encode_sm_a({sm_a, H, Xmlns}, _xmlns_attrs) -> - _els = [], - _attrs = encode_sm_a_attr_xmlns(Xmlns, - encode_sm_a_attr_h(H, _xmlns_attrs)), - {xmlel, <<"a">>, _attrs, _els}. - -decode_sm_a_attr_h(__TopXMLNS, undefined) -> - erlang:error({xmpp_codec, - {missing_attr, <<"h">>, <<"a">>, __TopXMLNS}}); -decode_sm_a_attr_h(__TopXMLNS, _val) -> - case catch dec_int(_val, 0, infinity) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"h">>, <<"a">>, __TopXMLNS}}); - _res -> _res - end. - -encode_sm_a_attr_h(_val, _acc) -> - [{<<"h">>, enc_int(_val)} | _acc]. - -decode_sm_a_attr_xmlns(__TopXMLNS, undefined) -> - undefined; -decode_sm_a_attr_xmlns(__TopXMLNS, _val) -> _val. - -encode_sm_a_attr_xmlns(undefined, _acc) -> _acc; -encode_sm_a_attr_xmlns(_val, _acc) -> - [{<<"xmlns">>, _val} | _acc]. - -decode_sm_r(__TopXMLNS, __IgnoreEls, - {xmlel, <<"r">>, _attrs, _els}) -> - Xmlns = decode_sm_r_attrs(__TopXMLNS, _attrs, - undefined), - {sm_r, Xmlns}. - -decode_sm_r_attrs(__TopXMLNS, - [{<<"xmlns">>, _val} | _attrs], _Xmlns) -> - decode_sm_r_attrs(__TopXMLNS, _attrs, _val); -decode_sm_r_attrs(__TopXMLNS, [_ | _attrs], Xmlns) -> - decode_sm_r_attrs(__TopXMLNS, _attrs, Xmlns); -decode_sm_r_attrs(__TopXMLNS, [], Xmlns) -> - decode_sm_r_attr_xmlns(__TopXMLNS, Xmlns). - -encode_sm_r({sm_r, Xmlns}, _xmlns_attrs) -> - _els = [], - _attrs = encode_sm_r_attr_xmlns(Xmlns, _xmlns_attrs), - {xmlel, <<"r">>, _attrs, _els}. - -decode_sm_r_attr_xmlns(__TopXMLNS, undefined) -> - undefined; -decode_sm_r_attr_xmlns(__TopXMLNS, _val) -> _val. - -encode_sm_r_attr_xmlns(undefined, _acc) -> _acc; -encode_sm_r_attr_xmlns(_val, _acc) -> - [{<<"xmlns">>, _val} | _acc]. - -decode_sm_resumed(__TopXMLNS, __IgnoreEls, - {xmlel, <<"resumed">>, _attrs, _els}) -> - {H, Xmlns, Previd} = decode_sm_resumed_attrs(__TopXMLNS, - _attrs, undefined, undefined, - undefined), - {sm_resumed, H, Previd, Xmlns}. - -decode_sm_resumed_attrs(__TopXMLNS, - [{<<"h">>, _val} | _attrs], _H, Xmlns, Previd) -> - decode_sm_resumed_attrs(__TopXMLNS, _attrs, _val, Xmlns, - Previd); -decode_sm_resumed_attrs(__TopXMLNS, - [{<<"xmlns">>, _val} | _attrs], H, _Xmlns, Previd) -> - decode_sm_resumed_attrs(__TopXMLNS, _attrs, H, _val, - Previd); -decode_sm_resumed_attrs(__TopXMLNS, - [{<<"previd">>, _val} | _attrs], H, Xmlns, _Previd) -> - decode_sm_resumed_attrs(__TopXMLNS, _attrs, H, Xmlns, - _val); -decode_sm_resumed_attrs(__TopXMLNS, [_ | _attrs], H, - Xmlns, Previd) -> - decode_sm_resumed_attrs(__TopXMLNS, _attrs, H, Xmlns, - Previd); -decode_sm_resumed_attrs(__TopXMLNS, [], H, Xmlns, - Previd) -> - {decode_sm_resumed_attr_h(__TopXMLNS, H), - decode_sm_resumed_attr_xmlns(__TopXMLNS, Xmlns), - decode_sm_resumed_attr_previd(__TopXMLNS, Previd)}. - -encode_sm_resumed({sm_resumed, H, Previd, Xmlns}, - _xmlns_attrs) -> - _els = [], - _attrs = encode_sm_resumed_attr_previd(Previd, - encode_sm_resumed_attr_xmlns(Xmlns, - encode_sm_resumed_attr_h(H, - _xmlns_attrs))), - {xmlel, <<"resumed">>, _attrs, _els}. - -decode_sm_resumed_attr_h(__TopXMLNS, undefined) -> - erlang:error({xmpp_codec, - {missing_attr, <<"h">>, <<"resumed">>, __TopXMLNS}}); -decode_sm_resumed_attr_h(__TopXMLNS, _val) -> - case catch dec_int(_val, 0, infinity) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"h">>, <<"resumed">>, __TopXMLNS}}); - _res -> _res - end. - -encode_sm_resumed_attr_h(_val, _acc) -> - [{<<"h">>, enc_int(_val)} | _acc]. - -decode_sm_resumed_attr_xmlns(__TopXMLNS, undefined) -> - undefined; -decode_sm_resumed_attr_xmlns(__TopXMLNS, _val) -> _val. - -encode_sm_resumed_attr_xmlns(undefined, _acc) -> _acc; -encode_sm_resumed_attr_xmlns(_val, _acc) -> - [{<<"xmlns">>, _val} | _acc]. - -decode_sm_resumed_attr_previd(__TopXMLNS, undefined) -> - erlang:error({xmpp_codec, - {missing_attr, <<"previd">>, <<"resumed">>, - __TopXMLNS}}); -decode_sm_resumed_attr_previd(__TopXMLNS, _val) -> _val. - -encode_sm_resumed_attr_previd(_val, _acc) -> - [{<<"previd">>, _val} | _acc]. - -decode_sm_resume(__TopXMLNS, __IgnoreEls, - {xmlel, <<"resume">>, _attrs, _els}) -> - {H, Xmlns, Previd} = decode_sm_resume_attrs(__TopXMLNS, - _attrs, undefined, undefined, - undefined), - {sm_resume, H, Previd, Xmlns}. - -decode_sm_resume_attrs(__TopXMLNS, - [{<<"h">>, _val} | _attrs], _H, Xmlns, Previd) -> - decode_sm_resume_attrs(__TopXMLNS, _attrs, _val, Xmlns, - Previd); -decode_sm_resume_attrs(__TopXMLNS, - [{<<"xmlns">>, _val} | _attrs], H, _Xmlns, Previd) -> - decode_sm_resume_attrs(__TopXMLNS, _attrs, H, _val, - Previd); -decode_sm_resume_attrs(__TopXMLNS, - [{<<"previd">>, _val} | _attrs], H, Xmlns, _Previd) -> - decode_sm_resume_attrs(__TopXMLNS, _attrs, H, Xmlns, - _val); -decode_sm_resume_attrs(__TopXMLNS, [_ | _attrs], H, - Xmlns, Previd) -> - decode_sm_resume_attrs(__TopXMLNS, _attrs, H, Xmlns, - Previd); -decode_sm_resume_attrs(__TopXMLNS, [], H, Xmlns, - Previd) -> - {decode_sm_resume_attr_h(__TopXMLNS, H), - decode_sm_resume_attr_xmlns(__TopXMLNS, Xmlns), - decode_sm_resume_attr_previd(__TopXMLNS, Previd)}. - -encode_sm_resume({sm_resume, H, Previd, Xmlns}, - _xmlns_attrs) -> - _els = [], - _attrs = encode_sm_resume_attr_previd(Previd, - encode_sm_resume_attr_xmlns(Xmlns, - encode_sm_resume_attr_h(H, - _xmlns_attrs))), - {xmlel, <<"resume">>, _attrs, _els}. - -decode_sm_resume_attr_h(__TopXMLNS, undefined) -> - erlang:error({xmpp_codec, - {missing_attr, <<"h">>, <<"resume">>, __TopXMLNS}}); -decode_sm_resume_attr_h(__TopXMLNS, _val) -> - case catch dec_int(_val, 0, infinity) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"h">>, <<"resume">>, __TopXMLNS}}); - _res -> _res - end. - -encode_sm_resume_attr_h(_val, _acc) -> - [{<<"h">>, enc_int(_val)} | _acc]. - -decode_sm_resume_attr_xmlns(__TopXMLNS, undefined) -> - undefined; -decode_sm_resume_attr_xmlns(__TopXMLNS, _val) -> _val. - -encode_sm_resume_attr_xmlns(undefined, _acc) -> _acc; -encode_sm_resume_attr_xmlns(_val, _acc) -> - [{<<"xmlns">>, _val} | _acc]. - -decode_sm_resume_attr_previd(__TopXMLNS, undefined) -> - erlang:error({xmpp_codec, - {missing_attr, <<"previd">>, <<"resume">>, - __TopXMLNS}}); -decode_sm_resume_attr_previd(__TopXMLNS, _val) -> _val. - -encode_sm_resume_attr_previd(_val, _acc) -> - [{<<"previd">>, _val} | _acc]. - -decode_sm_enabled(__TopXMLNS, __IgnoreEls, - {xmlel, <<"enabled">>, _attrs, _els}) -> - {Id, Location, Xmlns, Max, Resume} = - decode_sm_enabled_attrs(__TopXMLNS, _attrs, undefined, - undefined, undefined, undefined, undefined), - {sm_enabled, Id, Location, Max, Resume, Xmlns}. - -decode_sm_enabled_attrs(__TopXMLNS, - [{<<"id">>, _val} | _attrs], _Id, Location, Xmlns, Max, - Resume) -> - decode_sm_enabled_attrs(__TopXMLNS, _attrs, _val, - Location, Xmlns, Max, Resume); -decode_sm_enabled_attrs(__TopXMLNS, - [{<<"location">>, _val} | _attrs], Id, _Location, Xmlns, - Max, Resume) -> - decode_sm_enabled_attrs(__TopXMLNS, _attrs, Id, _val, - Xmlns, Max, Resume); -decode_sm_enabled_attrs(__TopXMLNS, - [{<<"xmlns">>, _val} | _attrs], Id, Location, _Xmlns, - Max, Resume) -> - decode_sm_enabled_attrs(__TopXMLNS, _attrs, Id, - Location, _val, Max, Resume); -decode_sm_enabled_attrs(__TopXMLNS, - [{<<"max">>, _val} | _attrs], Id, Location, Xmlns, _Max, - Resume) -> - decode_sm_enabled_attrs(__TopXMLNS, _attrs, Id, - Location, Xmlns, _val, Resume); -decode_sm_enabled_attrs(__TopXMLNS, - [{<<"resume">>, _val} | _attrs], Id, Location, Xmlns, - Max, _Resume) -> - decode_sm_enabled_attrs(__TopXMLNS, _attrs, Id, - Location, Xmlns, Max, _val); -decode_sm_enabled_attrs(__TopXMLNS, [_ | _attrs], Id, - Location, Xmlns, Max, Resume) -> - decode_sm_enabled_attrs(__TopXMLNS, _attrs, Id, - Location, Xmlns, Max, Resume); -decode_sm_enabled_attrs(__TopXMLNS, [], Id, Location, - Xmlns, Max, Resume) -> - {decode_sm_enabled_attr_id(__TopXMLNS, Id), - decode_sm_enabled_attr_location(__TopXMLNS, Location), - decode_sm_enabled_attr_xmlns(__TopXMLNS, Xmlns), - decode_sm_enabled_attr_max(__TopXMLNS, Max), - decode_sm_enabled_attr_resume(__TopXMLNS, Resume)}. - -encode_sm_enabled({sm_enabled, Id, Location, Max, - Resume, Xmlns}, - _xmlns_attrs) -> - _els = [], - _attrs = encode_sm_enabled_attr_resume(Resume, - encode_sm_enabled_attr_max(Max, - encode_sm_enabled_attr_xmlns(Xmlns, - encode_sm_enabled_attr_location(Location, - encode_sm_enabled_attr_id(Id, - _xmlns_attrs))))), - {xmlel, <<"enabled">>, _attrs, _els}. - -decode_sm_enabled_attr_id(__TopXMLNS, undefined) -> - undefined; -decode_sm_enabled_attr_id(__TopXMLNS, _val) -> _val. - -encode_sm_enabled_attr_id(undefined, _acc) -> _acc; -encode_sm_enabled_attr_id(_val, _acc) -> - [{<<"id">>, _val} | _acc]. - -decode_sm_enabled_attr_location(__TopXMLNS, - undefined) -> - undefined; -decode_sm_enabled_attr_location(__TopXMLNS, _val) -> - _val. - -encode_sm_enabled_attr_location(undefined, _acc) -> - _acc; -encode_sm_enabled_attr_location(_val, _acc) -> - [{<<"location">>, _val} | _acc]. - -decode_sm_enabled_attr_xmlns(__TopXMLNS, undefined) -> - undefined; -decode_sm_enabled_attr_xmlns(__TopXMLNS, _val) -> _val. - -encode_sm_enabled_attr_xmlns(undefined, _acc) -> _acc; -encode_sm_enabled_attr_xmlns(_val, _acc) -> - [{<<"xmlns">>, _val} | _acc]. - -decode_sm_enabled_attr_max(__TopXMLNS, undefined) -> - undefined; -decode_sm_enabled_attr_max(__TopXMLNS, _val) -> - case catch dec_int(_val, 0, infinity) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"max">>, <<"enabled">>, - __TopXMLNS}}); - _res -> _res - end. - -encode_sm_enabled_attr_max(undefined, _acc) -> _acc; -encode_sm_enabled_attr_max(_val, _acc) -> - [{<<"max">>, enc_int(_val)} | _acc]. - -decode_sm_enabled_attr_resume(__TopXMLNS, undefined) -> - false; -decode_sm_enabled_attr_resume(__TopXMLNS, _val) -> - case catch dec_bool(_val) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"resume">>, <<"enabled">>, - __TopXMLNS}}); - _res -> _res - end. - -encode_sm_enabled_attr_resume(false, _acc) -> _acc; -encode_sm_enabled_attr_resume(_val, _acc) -> - [{<<"resume">>, enc_bool(_val)} | _acc]. - -decode_sm_enable(__TopXMLNS, __IgnoreEls, - {xmlel, <<"enable">>, _attrs, _els}) -> - {Max, Xmlns, Resume} = - decode_sm_enable_attrs(__TopXMLNS, _attrs, undefined, - undefined, undefined), - {sm_enable, Max, Resume, Xmlns}. - -decode_sm_enable_attrs(__TopXMLNS, - [{<<"max">>, _val} | _attrs], _Max, Xmlns, Resume) -> - decode_sm_enable_attrs(__TopXMLNS, _attrs, _val, Xmlns, - Resume); -decode_sm_enable_attrs(__TopXMLNS, - [{<<"xmlns">>, _val} | _attrs], Max, _Xmlns, Resume) -> - decode_sm_enable_attrs(__TopXMLNS, _attrs, Max, _val, - Resume); -decode_sm_enable_attrs(__TopXMLNS, - [{<<"resume">>, _val} | _attrs], Max, Xmlns, _Resume) -> - decode_sm_enable_attrs(__TopXMLNS, _attrs, Max, Xmlns, - _val); -decode_sm_enable_attrs(__TopXMLNS, [_ | _attrs], Max, - Xmlns, Resume) -> - decode_sm_enable_attrs(__TopXMLNS, _attrs, Max, Xmlns, - Resume); -decode_sm_enable_attrs(__TopXMLNS, [], Max, Xmlns, - Resume) -> - {decode_sm_enable_attr_max(__TopXMLNS, Max), - decode_sm_enable_attr_xmlns(__TopXMLNS, Xmlns), - decode_sm_enable_attr_resume(__TopXMLNS, Resume)}. - -encode_sm_enable({sm_enable, Max, Resume, Xmlns}, - _xmlns_attrs) -> - _els = [], - _attrs = encode_sm_enable_attr_resume(Resume, - encode_sm_enable_attr_xmlns(Xmlns, - encode_sm_enable_attr_max(Max, - _xmlns_attrs))), - {xmlel, <<"enable">>, _attrs, _els}. - -decode_sm_enable_attr_max(__TopXMLNS, undefined) -> - undefined; -decode_sm_enable_attr_max(__TopXMLNS, _val) -> - case catch dec_int(_val, 0, infinity) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"max">>, <<"enable">>, __TopXMLNS}}); - _res -> _res - end. - -encode_sm_enable_attr_max(undefined, _acc) -> _acc; -encode_sm_enable_attr_max(_val, _acc) -> - [{<<"max">>, enc_int(_val)} | _acc]. - -decode_sm_enable_attr_xmlns(__TopXMLNS, undefined) -> - undefined; -decode_sm_enable_attr_xmlns(__TopXMLNS, _val) -> _val. - -encode_sm_enable_attr_xmlns(undefined, _acc) -> _acc; -encode_sm_enable_attr_xmlns(_val, _acc) -> - [{<<"xmlns">>, _val} | _acc]. - -decode_sm_enable_attr_resume(__TopXMLNS, undefined) -> - false; -decode_sm_enable_attr_resume(__TopXMLNS, _val) -> - case catch dec_bool(_val) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"resume">>, <<"enable">>, - __TopXMLNS}}); - _res -> _res - end. - -encode_sm_enable_attr_resume(false, _acc) -> _acc; -encode_sm_enable_attr_resume(_val, _acc) -> - [{<<"resume">>, enc_bool(_val)} | _acc]. - -decode_feature_sm(__TopXMLNS, __IgnoreEls, - {xmlel, <<"sm">>, _attrs, _els}) -> - Xmlns = decode_feature_sm_attrs(__TopXMLNS, _attrs, - undefined), - {feature_sm, Xmlns}. - -decode_feature_sm_attrs(__TopXMLNS, - [{<<"xmlns">>, _val} | _attrs], _Xmlns) -> - decode_feature_sm_attrs(__TopXMLNS, _attrs, _val); -decode_feature_sm_attrs(__TopXMLNS, [_ | _attrs], - Xmlns) -> - decode_feature_sm_attrs(__TopXMLNS, _attrs, Xmlns); -decode_feature_sm_attrs(__TopXMLNS, [], Xmlns) -> - decode_feature_sm_attr_xmlns(__TopXMLNS, Xmlns). - -encode_feature_sm({feature_sm, Xmlns}, _xmlns_attrs) -> - _els = [], - _attrs = encode_feature_sm_attr_xmlns(Xmlns, - _xmlns_attrs), - {xmlel, <<"sm">>, _attrs, _els}. - -decode_feature_sm_attr_xmlns(__TopXMLNS, undefined) -> - undefined; -decode_feature_sm_attr_xmlns(__TopXMLNS, _val) -> _val. - -encode_feature_sm_attr_xmlns(undefined, _acc) -> _acc; -encode_feature_sm_attr_xmlns(_val, _acc) -> - [{<<"xmlns">>, _val} | _acc]. - -decode_csi_inactive(__TopXMLNS, __IgnoreEls, - {xmlel, <<"inactive">>, _attrs, _els}) -> - {csi, inactive}. - -encode_csi_inactive({csi, inactive}, _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"inactive">>, _attrs, _els}. - -decode_csi_active(__TopXMLNS, __IgnoreEls, - {xmlel, <<"active">>, _attrs, _els}) -> - {csi, active}. - -encode_csi_active({csi, active}, _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"active">>, _attrs, _els}. - -decode_feature_csi(__TopXMLNS, __IgnoreEls, - {xmlel, <<"csi">>, _attrs, _els}) -> - Xmlns = decode_feature_csi_attrs(__TopXMLNS, _attrs, - undefined), - {feature_csi, Xmlns}. - -decode_feature_csi_attrs(__TopXMLNS, - [{<<"xmlns">>, _val} | _attrs], _Xmlns) -> - decode_feature_csi_attrs(__TopXMLNS, _attrs, _val); -decode_feature_csi_attrs(__TopXMLNS, [_ | _attrs], - Xmlns) -> - decode_feature_csi_attrs(__TopXMLNS, _attrs, Xmlns); -decode_feature_csi_attrs(__TopXMLNS, [], Xmlns) -> - decode_feature_csi_attr_xmlns(__TopXMLNS, Xmlns). - -encode_feature_csi({feature_csi, Xmlns}, - _xmlns_attrs) -> - _els = [], - _attrs = encode_feature_csi_attr_xmlns(Xmlns, - _xmlns_attrs), - {xmlel, <<"csi">>, _attrs, _els}. - -decode_feature_csi_attr_xmlns(__TopXMLNS, undefined) -> - undefined; -decode_feature_csi_attr_xmlns(__TopXMLNS, _val) -> _val. - -encode_feature_csi_attr_xmlns(undefined, _acc) -> _acc; -encode_feature_csi_attr_xmlns(_val, _acc) -> - [{<<"xmlns">>, _val} | _acc]. - -decode_carbons_sent(__TopXMLNS, __IgnoreEls, - {xmlel, <<"sent">>, _attrs, _els}) -> - Forwarded = decode_carbons_sent_els(__TopXMLNS, - __IgnoreEls, _els, error), - {carbons_sent, Forwarded}. - -decode_carbons_sent_els(__TopXMLNS, __IgnoreEls, [], - Forwarded) -> - case Forwarded of - error -> - erlang:error({xmpp_codec, - {missing_tag, <<"forwarded">>, __TopXMLNS}}); - {value, Forwarded1} -> Forwarded1 - end; -decode_carbons_sent_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"forwarded">>, _attrs, _} = _el | _els], - Forwarded) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<"urn:xmpp:forward:0">> -> - decode_carbons_sent_els(__TopXMLNS, __IgnoreEls, _els, - {value, - decode_forwarded(_xmlns, __IgnoreEls, - _el)}); - true -> - decode_carbons_sent_els(__TopXMLNS, __IgnoreEls, _els, - Forwarded) - end; -decode_carbons_sent_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Forwarded) -> - decode_carbons_sent_els(__TopXMLNS, __IgnoreEls, _els, - Forwarded). - -encode_carbons_sent({carbons_sent, Forwarded}, - _xmlns_attrs) -> - _els = - lists:reverse('encode_carbons_sent_$forwarded'(Forwarded, - [])), - _attrs = _xmlns_attrs, - {xmlel, <<"sent">>, _attrs, _els}. - -'encode_carbons_sent_$forwarded'(Forwarded, _acc) -> - [encode_forwarded(Forwarded, - [{<<"xmlns">>, <<"urn:xmpp:forward:0">>}]) - | _acc]. - -decode_carbons_received(__TopXMLNS, __IgnoreEls, - {xmlel, <<"received">>, _attrs, _els}) -> - Forwarded = decode_carbons_received_els(__TopXMLNS, - __IgnoreEls, _els, error), - {carbons_received, Forwarded}. - -decode_carbons_received_els(__TopXMLNS, __IgnoreEls, [], - Forwarded) -> - case Forwarded of - error -> - erlang:error({xmpp_codec, - {missing_tag, <<"forwarded">>, __TopXMLNS}}); - {value, Forwarded1} -> Forwarded1 - end; -decode_carbons_received_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"forwarded">>, _attrs, _} = _el | _els], - Forwarded) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<"urn:xmpp:forward:0">> -> - decode_carbons_received_els(__TopXMLNS, __IgnoreEls, - _els, - {value, - decode_forwarded(_xmlns, __IgnoreEls, - _el)}); - true -> - decode_carbons_received_els(__TopXMLNS, __IgnoreEls, - _els, Forwarded) - end; -decode_carbons_received_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Forwarded) -> - decode_carbons_received_els(__TopXMLNS, __IgnoreEls, - _els, Forwarded). - -encode_carbons_received({carbons_received, Forwarded}, - _xmlns_attrs) -> - _els = - lists:reverse('encode_carbons_received_$forwarded'(Forwarded, - [])), - _attrs = _xmlns_attrs, - {xmlel, <<"received">>, _attrs, _els}. - -'encode_carbons_received_$forwarded'(Forwarded, _acc) -> - [encode_forwarded(Forwarded, - [{<<"xmlns">>, <<"urn:xmpp:forward:0">>}]) - | _acc]. - -decode_carbons_private(__TopXMLNS, __IgnoreEls, - {xmlel, <<"private">>, _attrs, _els}) -> - {carbons_private}. - -encode_carbons_private({carbons_private}, - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"private">>, _attrs, _els}. - -decode_carbons_enable(__TopXMLNS, __IgnoreEls, - {xmlel, <<"enable">>, _attrs, _els}) -> - {carbons_enable}. - -encode_carbons_enable({carbons_enable}, _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"enable">>, _attrs, _els}. - -decode_carbons_disable(__TopXMLNS, __IgnoreEls, - {xmlel, <<"disable">>, _attrs, _els}) -> - {carbons_disable}. - -encode_carbons_disable({carbons_disable}, - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"disable">>, _attrs, _els}. - -decode_forwarded(__TopXMLNS, __IgnoreEls, - {xmlel, <<"forwarded">>, _attrs, _els}) -> - {Delay, __Els} = decode_forwarded_els(__TopXMLNS, - __IgnoreEls, _els, undefined, []), - {forwarded, Delay, __Els}. - -decode_forwarded_els(__TopXMLNS, __IgnoreEls, [], Delay, - __Els) -> - {Delay, lists:reverse(__Els)}; -decode_forwarded_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"delay">>, _attrs, _} = _el | _els], Delay, - __Els) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<"urn:xmpp:delay">> -> - decode_forwarded_els(__TopXMLNS, __IgnoreEls, _els, - decode_delay(_xmlns, __IgnoreEls, _el), __Els); - true -> - decode_forwarded_els(__TopXMLNS, __IgnoreEls, _els, - Delay, __Els) - end; -decode_forwarded_els(__TopXMLNS, __IgnoreEls, - [{xmlel, _, _, _} = _el | _els], Delay, __Els) -> - if __IgnoreEls -> - decode_forwarded_els(__TopXMLNS, __IgnoreEls, _els, - Delay, [_el | __Els]); - true -> - case is_known_tag(_el) of - true -> - decode_forwarded_els(__TopXMLNS, __IgnoreEls, _els, - Delay, [decode(_el) | __Els]); - false -> - decode_forwarded_els(__TopXMLNS, __IgnoreEls, _els, - Delay, __Els) - end - end; -decode_forwarded_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Delay, __Els) -> - decode_forwarded_els(__TopXMLNS, __IgnoreEls, _els, - Delay, __Els). - -encode_forwarded({forwarded, Delay, __Els}, - _xmlns_attrs) -> - _els = [encode(_el) || _el <- __Els] ++ - lists:reverse('encode_forwarded_$delay'(Delay, [])), - _attrs = _xmlns_attrs, - {xmlel, <<"forwarded">>, _attrs, _els}. - -'encode_forwarded_$delay'(undefined, _acc) -> _acc; -'encode_forwarded_$delay'(Delay, _acc) -> - [encode_delay(Delay, - [{<<"xmlns">>, <<"urn:xmpp:delay">>}]) - | _acc]. - -decode_muc(__TopXMLNS, __IgnoreEls, - {xmlel, <<"x">>, _attrs, _els}) -> - History = decode_muc_els(__TopXMLNS, __IgnoreEls, _els, - undefined), - Password = decode_muc_attrs(__TopXMLNS, _attrs, - undefined), - {muc, History, Password}. - -decode_muc_els(__TopXMLNS, __IgnoreEls, [], History) -> - History; -decode_muc_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"history">>, _attrs, _} = _el | _els], - History) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_muc_els(__TopXMLNS, __IgnoreEls, _els, - decode_muc_history(__TopXMLNS, __IgnoreEls, _el)); - true -> - decode_muc_els(__TopXMLNS, __IgnoreEls, _els, History) - end; -decode_muc_els(__TopXMLNS, __IgnoreEls, [_ | _els], - History) -> - decode_muc_els(__TopXMLNS, __IgnoreEls, _els, History). - -decode_muc_attrs(__TopXMLNS, - [{<<"password">>, _val} | _attrs], _Password) -> - decode_muc_attrs(__TopXMLNS, _attrs, _val); -decode_muc_attrs(__TopXMLNS, [_ | _attrs], Password) -> - decode_muc_attrs(__TopXMLNS, _attrs, Password); -decode_muc_attrs(__TopXMLNS, [], Password) -> - decode_muc_attr_password(__TopXMLNS, Password). - -encode_muc({muc, History, Password}, _xmlns_attrs) -> - _els = lists:reverse('encode_muc_$history'(History, - [])), - _attrs = encode_muc_attr_password(Password, - _xmlns_attrs), - {xmlel, <<"x">>, _attrs, _els}. - -'encode_muc_$history'(undefined, _acc) -> _acc; -'encode_muc_$history'(History, _acc) -> - [encode_muc_history(History, []) | _acc]. - -decode_muc_attr_password(__TopXMLNS, undefined) -> - undefined; -decode_muc_attr_password(__TopXMLNS, _val) -> _val. - -encode_muc_attr_password(undefined, _acc) -> _acc; -encode_muc_attr_password(_val, _acc) -> - [{<<"password">>, _val} | _acc]. - -decode_muc_admin(__TopXMLNS, __IgnoreEls, - {xmlel, <<"query">>, _attrs, _els}) -> - Items = decode_muc_admin_els(__TopXMLNS, __IgnoreEls, - _els, []), - {muc_admin, Items}. - -decode_muc_admin_els(__TopXMLNS, __IgnoreEls, [], - Items) -> - lists:reverse(Items); -decode_muc_admin_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"item">>, _attrs, _} = _el | _els], Items) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_muc_admin_els(__TopXMLNS, __IgnoreEls, _els, - [decode_muc_admin_item(__TopXMLNS, __IgnoreEls, - _el) - | Items]); - true -> - decode_muc_admin_els(__TopXMLNS, __IgnoreEls, _els, - Items) - end; -decode_muc_admin_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Items) -> - decode_muc_admin_els(__TopXMLNS, __IgnoreEls, _els, - Items). - -encode_muc_admin({muc_admin, Items}, _xmlns_attrs) -> - _els = lists:reverse('encode_muc_admin_$items'(Items, - [])), - _attrs = _xmlns_attrs, - {xmlel, <<"query">>, _attrs, _els}. - -'encode_muc_admin_$items'([], _acc) -> _acc; -'encode_muc_admin_$items'([Items | _els], _acc) -> - 'encode_muc_admin_$items'(_els, - [encode_muc_admin_item(Items, []) | _acc]). - -decode_muc_admin_reason(__TopXMLNS, __IgnoreEls, - {xmlel, <<"reason">>, _attrs, _els}) -> - Cdata = decode_muc_admin_reason_els(__TopXMLNS, - __IgnoreEls, _els, <<>>), - Cdata. - -decode_muc_admin_reason_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_muc_admin_reason_cdata(__TopXMLNS, Cdata); -decode_muc_admin_reason_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_muc_admin_reason_els(__TopXMLNS, __IgnoreEls, - _els, <<Cdata/binary, _data/binary>>); -decode_muc_admin_reason_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_muc_admin_reason_els(__TopXMLNS, __IgnoreEls, - _els, Cdata). - -encode_muc_admin_reason(Cdata, _xmlns_attrs) -> - _els = encode_muc_admin_reason_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"reason">>, _attrs, _els}. - -decode_muc_admin_reason_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_muc_admin_reason_cdata(__TopXMLNS, _val) -> _val. - -encode_muc_admin_reason_cdata(undefined, _acc) -> _acc; -encode_muc_admin_reason_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_muc_admin_continue(__TopXMLNS, __IgnoreEls, - {xmlel, <<"continue">>, _attrs, _els}) -> - Thread = decode_muc_admin_continue_attrs(__TopXMLNS, - _attrs, undefined), - Thread. - -decode_muc_admin_continue_attrs(__TopXMLNS, - [{<<"thread">>, _val} | _attrs], _Thread) -> - decode_muc_admin_continue_attrs(__TopXMLNS, _attrs, - _val); -decode_muc_admin_continue_attrs(__TopXMLNS, - [_ | _attrs], Thread) -> - decode_muc_admin_continue_attrs(__TopXMLNS, _attrs, - Thread); -decode_muc_admin_continue_attrs(__TopXMLNS, [], - Thread) -> - decode_muc_admin_continue_attr_thread(__TopXMLNS, - Thread). - -encode_muc_admin_continue(Thread, _xmlns_attrs) -> - _els = [], - _attrs = encode_muc_admin_continue_attr_thread(Thread, - _xmlns_attrs), - {xmlel, <<"continue">>, _attrs, _els}. - -decode_muc_admin_continue_attr_thread(__TopXMLNS, - undefined) -> - undefined; -decode_muc_admin_continue_attr_thread(__TopXMLNS, - _val) -> - _val. - -encode_muc_admin_continue_attr_thread(undefined, - _acc) -> - _acc; -encode_muc_admin_continue_attr_thread(_val, _acc) -> - [{<<"thread">>, _val} | _acc]. - -decode_muc_admin_actor(__TopXMLNS, __IgnoreEls, - {xmlel, <<"actor">>, _attrs, _els}) -> - {Jid, Nick} = decode_muc_admin_actor_attrs(__TopXMLNS, - _attrs, undefined, undefined), - {muc_actor, Jid, Nick}. - -decode_muc_admin_actor_attrs(__TopXMLNS, - [{<<"jid">>, _val} | _attrs], _Jid, Nick) -> - decode_muc_admin_actor_attrs(__TopXMLNS, _attrs, _val, - Nick); -decode_muc_admin_actor_attrs(__TopXMLNS, - [{<<"nick">>, _val} | _attrs], Jid, _Nick) -> - decode_muc_admin_actor_attrs(__TopXMLNS, _attrs, Jid, - _val); -decode_muc_admin_actor_attrs(__TopXMLNS, [_ | _attrs], - Jid, Nick) -> - decode_muc_admin_actor_attrs(__TopXMLNS, _attrs, Jid, - Nick); -decode_muc_admin_actor_attrs(__TopXMLNS, [], Jid, - Nick) -> - {decode_muc_admin_actor_attr_jid(__TopXMLNS, Jid), - decode_muc_admin_actor_attr_nick(__TopXMLNS, Nick)}. - -encode_muc_admin_actor({muc_actor, Jid, Nick}, - _xmlns_attrs) -> - _els = [], - _attrs = encode_muc_admin_actor_attr_nick(Nick, - encode_muc_admin_actor_attr_jid(Jid, - _xmlns_attrs)), - {xmlel, <<"actor">>, _attrs, _els}. - -decode_muc_admin_actor_attr_jid(__TopXMLNS, - undefined) -> - undefined; -decode_muc_admin_actor_attr_jid(__TopXMLNS, _val) -> - case catch dec_jid(_val) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"jid">>, <<"actor">>, __TopXMLNS}}); - _res -> _res - end. - -encode_muc_admin_actor_attr_jid(undefined, _acc) -> - _acc; -encode_muc_admin_actor_attr_jid(_val, _acc) -> - [{<<"jid">>, enc_jid(_val)} | _acc]. - -decode_muc_admin_actor_attr_nick(__TopXMLNS, - undefined) -> - undefined; -decode_muc_admin_actor_attr_nick(__TopXMLNS, _val) -> - _val. - -encode_muc_admin_actor_attr_nick(undefined, _acc) -> - _acc; -encode_muc_admin_actor_attr_nick(_val, _acc) -> - [{<<"nick">>, _val} | _acc]. - -decode_muc_admin_item(__TopXMLNS, __IgnoreEls, - {xmlel, <<"item">>, _attrs, _els}) -> - {Actor, Continue, Reason} = - decode_muc_admin_item_els(__TopXMLNS, __IgnoreEls, _els, - undefined, undefined, undefined), - {Affiliation, Role, Jid, Nick} = - decode_muc_admin_item_attrs(__TopXMLNS, _attrs, - undefined, undefined, undefined, undefined), - {muc_item, Actor, Continue, Reason, Affiliation, Role, - Jid, Nick}. - -decode_muc_admin_item_els(__TopXMLNS, __IgnoreEls, [], - Actor, Continue, Reason) -> - {Actor, Continue, Reason}; -decode_muc_admin_item_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"actor">>, _attrs, _} = _el | _els], Actor, - Continue, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_muc_admin_item_els(__TopXMLNS, __IgnoreEls, _els, - decode_muc_admin_actor(__TopXMLNS, - __IgnoreEls, _el), - Continue, Reason); - true -> - decode_muc_admin_item_els(__TopXMLNS, __IgnoreEls, _els, - Actor, Continue, Reason) - end; -decode_muc_admin_item_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"continue">>, _attrs, _} = _el | _els], - Actor, Continue, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_muc_admin_item_els(__TopXMLNS, __IgnoreEls, _els, - Actor, - decode_muc_admin_continue(__TopXMLNS, - __IgnoreEls, - _el), - Reason); - true -> - decode_muc_admin_item_els(__TopXMLNS, __IgnoreEls, _els, - Actor, Continue, Reason) - end; -decode_muc_admin_item_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"reason">>, _attrs, _} = _el | _els], - Actor, Continue, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_muc_admin_item_els(__TopXMLNS, __IgnoreEls, _els, - Actor, Continue, - decode_muc_admin_reason(__TopXMLNS, - __IgnoreEls, _el)); - true -> - decode_muc_admin_item_els(__TopXMLNS, __IgnoreEls, _els, - Actor, Continue, Reason) - end; -decode_muc_admin_item_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Actor, Continue, Reason) -> - decode_muc_admin_item_els(__TopXMLNS, __IgnoreEls, _els, - Actor, Continue, Reason). - -decode_muc_admin_item_attrs(__TopXMLNS, - [{<<"affiliation">>, _val} | _attrs], _Affiliation, - Role, Jid, Nick) -> - decode_muc_admin_item_attrs(__TopXMLNS, _attrs, _val, - Role, Jid, Nick); -decode_muc_admin_item_attrs(__TopXMLNS, - [{<<"role">>, _val} | _attrs], Affiliation, _Role, - Jid, Nick) -> - decode_muc_admin_item_attrs(__TopXMLNS, _attrs, - Affiliation, _val, Jid, Nick); -decode_muc_admin_item_attrs(__TopXMLNS, - [{<<"jid">>, _val} | _attrs], Affiliation, Role, - _Jid, Nick) -> - decode_muc_admin_item_attrs(__TopXMLNS, _attrs, - Affiliation, Role, _val, Nick); -decode_muc_admin_item_attrs(__TopXMLNS, - [{<<"nick">>, _val} | _attrs], Affiliation, Role, - Jid, _Nick) -> - decode_muc_admin_item_attrs(__TopXMLNS, _attrs, - Affiliation, Role, Jid, _val); -decode_muc_admin_item_attrs(__TopXMLNS, [_ | _attrs], - Affiliation, Role, Jid, Nick) -> - decode_muc_admin_item_attrs(__TopXMLNS, _attrs, - Affiliation, Role, Jid, Nick); -decode_muc_admin_item_attrs(__TopXMLNS, [], Affiliation, - Role, Jid, Nick) -> - {decode_muc_admin_item_attr_affiliation(__TopXMLNS, - Affiliation), - decode_muc_admin_item_attr_role(__TopXMLNS, Role), - decode_muc_admin_item_attr_jid(__TopXMLNS, Jid), - decode_muc_admin_item_attr_nick(__TopXMLNS, Nick)}. - -encode_muc_admin_item({muc_item, Actor, Continue, - Reason, Affiliation, Role, Jid, Nick}, - _xmlns_attrs) -> - _els = - lists:reverse('encode_muc_admin_item_$actor'(Actor, - 'encode_muc_admin_item_$continue'(Continue, - 'encode_muc_admin_item_$reason'(Reason, - [])))), - _attrs = encode_muc_admin_item_attr_nick(Nick, - encode_muc_admin_item_attr_jid(Jid, - encode_muc_admin_item_attr_role(Role, - encode_muc_admin_item_attr_affiliation(Affiliation, - _xmlns_attrs)))), - {xmlel, <<"item">>, _attrs, _els}. - -'encode_muc_admin_item_$actor'(undefined, _acc) -> _acc; -'encode_muc_admin_item_$actor'(Actor, _acc) -> - [encode_muc_admin_actor(Actor, []) | _acc]. - -'encode_muc_admin_item_$continue'(undefined, _acc) -> - _acc; -'encode_muc_admin_item_$continue'(Continue, _acc) -> - [encode_muc_admin_continue(Continue, []) | _acc]. - -'encode_muc_admin_item_$reason'(undefined, _acc) -> - _acc; -'encode_muc_admin_item_$reason'(Reason, _acc) -> - [encode_muc_admin_reason(Reason, []) | _acc]. - -decode_muc_admin_item_attr_affiliation(__TopXMLNS, - undefined) -> - undefined; -decode_muc_admin_item_attr_affiliation(__TopXMLNS, - _val) -> - case catch dec_enum(_val, - [admin, member, none, outcast, owner]) - of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"affiliation">>, <<"item">>, - __TopXMLNS}}); - _res -> _res - end. - -encode_muc_admin_item_attr_affiliation(undefined, - _acc) -> - _acc; -encode_muc_admin_item_attr_affiliation(_val, _acc) -> - [{<<"affiliation">>, enc_enum(_val)} | _acc]. - -decode_muc_admin_item_attr_role(__TopXMLNS, - undefined) -> - undefined; -decode_muc_admin_item_attr_role(__TopXMLNS, _val) -> - case catch dec_enum(_val, - [moderator, none, participant, visitor]) - of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"role">>, <<"item">>, __TopXMLNS}}); - _res -> _res - end. - -encode_muc_admin_item_attr_role(undefined, _acc) -> - _acc; -encode_muc_admin_item_attr_role(_val, _acc) -> - [{<<"role">>, enc_enum(_val)} | _acc]. - -decode_muc_admin_item_attr_jid(__TopXMLNS, undefined) -> - undefined; -decode_muc_admin_item_attr_jid(__TopXMLNS, _val) -> - case catch dec_jid(_val) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"jid">>, <<"item">>, __TopXMLNS}}); - _res -> _res - end. - -encode_muc_admin_item_attr_jid(undefined, _acc) -> _acc; -encode_muc_admin_item_attr_jid(_val, _acc) -> - [{<<"jid">>, enc_jid(_val)} | _acc]. - -decode_muc_admin_item_attr_nick(__TopXMLNS, - undefined) -> - undefined; -decode_muc_admin_item_attr_nick(__TopXMLNS, _val) -> - _val. - -encode_muc_admin_item_attr_nick(undefined, _acc) -> - _acc; -encode_muc_admin_item_attr_nick(_val, _acc) -> - [{<<"nick">>, _val} | _acc]. - -decode_muc_owner(__TopXMLNS, __IgnoreEls, - {xmlel, <<"query">>, _attrs, _els}) -> - {Config, Destroy} = decode_muc_owner_els(__TopXMLNS, - __IgnoreEls, _els, undefined, - undefined), - {muc_owner, Destroy, Config}. - -decode_muc_owner_els(__TopXMLNS, __IgnoreEls, [], - Config, Destroy) -> - {Config, Destroy}; -decode_muc_owner_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"destroy">>, _attrs, _} = _el | _els], - Config, Destroy) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_muc_owner_els(__TopXMLNS, __IgnoreEls, _els, - Config, - decode_muc_owner_destroy(__TopXMLNS, - __IgnoreEls, _el)); - true -> - decode_muc_owner_els(__TopXMLNS, __IgnoreEls, _els, - Config, Destroy) - end; -decode_muc_owner_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"x">>, _attrs, _} = _el | _els], Config, - Destroy) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<"jabber:x:data">> -> - decode_muc_owner_els(__TopXMLNS, __IgnoreEls, _els, - decode_xdata(_xmlns, __IgnoreEls, _el), - Destroy); - true -> - decode_muc_owner_els(__TopXMLNS, __IgnoreEls, _els, - Config, Destroy) - end; -decode_muc_owner_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Config, Destroy) -> - decode_muc_owner_els(__TopXMLNS, __IgnoreEls, _els, - Config, Destroy). - -encode_muc_owner({muc_owner, Destroy, Config}, - _xmlns_attrs) -> - _els = lists:reverse('encode_muc_owner_$config'(Config, - 'encode_muc_owner_$destroy'(Destroy, - []))), - _attrs = _xmlns_attrs, - {xmlel, <<"query">>, _attrs, _els}. - -'encode_muc_owner_$config'(undefined, _acc) -> _acc; -'encode_muc_owner_$config'(Config, _acc) -> - [encode_xdata(Config, - [{<<"xmlns">>, <<"jabber:x:data">>}]) - | _acc]. - -'encode_muc_owner_$destroy'(undefined, _acc) -> _acc; -'encode_muc_owner_$destroy'(Destroy, _acc) -> - [encode_muc_owner_destroy(Destroy, []) | _acc]. - -decode_muc_owner_destroy(__TopXMLNS, __IgnoreEls, - {xmlel, <<"destroy">>, _attrs, _els}) -> - {Password, Reason} = - decode_muc_owner_destroy_els(__TopXMLNS, __IgnoreEls, - _els, undefined, undefined), - Jid = decode_muc_owner_destroy_attrs(__TopXMLNS, _attrs, - undefined), - {muc_owner_destroy, Jid, Reason, Password}. - -decode_muc_owner_destroy_els(__TopXMLNS, __IgnoreEls, - [], Password, Reason) -> - {Password, Reason}; -decode_muc_owner_destroy_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"password">>, _attrs, _} = _el | _els], - Password, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_muc_owner_destroy_els(__TopXMLNS, __IgnoreEls, - _els, - decode_muc_owner_password(__TopXMLNS, - __IgnoreEls, - _el), - Reason); - true -> - decode_muc_owner_destroy_els(__TopXMLNS, __IgnoreEls, - _els, Password, Reason) - end; -decode_muc_owner_destroy_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"reason">>, _attrs, _} = _el | _els], - Password, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_muc_owner_destroy_els(__TopXMLNS, __IgnoreEls, - _els, Password, - decode_muc_owner_reason(__TopXMLNS, - __IgnoreEls, - _el)); - true -> - decode_muc_owner_destroy_els(__TopXMLNS, __IgnoreEls, - _els, Password, Reason) - end; -decode_muc_owner_destroy_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Password, Reason) -> - decode_muc_owner_destroy_els(__TopXMLNS, __IgnoreEls, - _els, Password, Reason). - -decode_muc_owner_destroy_attrs(__TopXMLNS, - [{<<"jid">>, _val} | _attrs], _Jid) -> - decode_muc_owner_destroy_attrs(__TopXMLNS, _attrs, - _val); -decode_muc_owner_destroy_attrs(__TopXMLNS, [_ | _attrs], - Jid) -> - decode_muc_owner_destroy_attrs(__TopXMLNS, _attrs, Jid); -decode_muc_owner_destroy_attrs(__TopXMLNS, [], Jid) -> - decode_muc_owner_destroy_attr_jid(__TopXMLNS, Jid). - -encode_muc_owner_destroy({muc_owner_destroy, Jid, - Reason, Password}, - _xmlns_attrs) -> - _els = - lists:reverse('encode_muc_owner_destroy_$password'(Password, - 'encode_muc_owner_destroy_$reason'(Reason, - []))), - _attrs = encode_muc_owner_destroy_attr_jid(Jid, - _xmlns_attrs), - {xmlel, <<"destroy">>, _attrs, _els}. - -'encode_muc_owner_destroy_$password'(undefined, _acc) -> - _acc; -'encode_muc_owner_destroy_$password'(Password, _acc) -> - [encode_muc_owner_password(Password, []) | _acc]. - -'encode_muc_owner_destroy_$reason'(undefined, _acc) -> - _acc; -'encode_muc_owner_destroy_$reason'(Reason, _acc) -> - [encode_muc_owner_reason(Reason, []) | _acc]. - -decode_muc_owner_destroy_attr_jid(__TopXMLNS, - undefined) -> - undefined; -decode_muc_owner_destroy_attr_jid(__TopXMLNS, _val) -> - case catch dec_jid(_val) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"jid">>, <<"destroy">>, - __TopXMLNS}}); - _res -> _res - end. - -encode_muc_owner_destroy_attr_jid(undefined, _acc) -> - _acc; -encode_muc_owner_destroy_attr_jid(_val, _acc) -> - [{<<"jid">>, enc_jid(_val)} | _acc]. - -decode_muc_owner_reason(__TopXMLNS, __IgnoreEls, - {xmlel, <<"reason">>, _attrs, _els}) -> - Cdata = decode_muc_owner_reason_els(__TopXMLNS, - __IgnoreEls, _els, <<>>), - Cdata. - -decode_muc_owner_reason_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_muc_owner_reason_cdata(__TopXMLNS, Cdata); -decode_muc_owner_reason_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_muc_owner_reason_els(__TopXMLNS, __IgnoreEls, - _els, <<Cdata/binary, _data/binary>>); -decode_muc_owner_reason_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_muc_owner_reason_els(__TopXMLNS, __IgnoreEls, - _els, Cdata). - -encode_muc_owner_reason(Cdata, _xmlns_attrs) -> - _els = encode_muc_owner_reason_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"reason">>, _attrs, _els}. - -decode_muc_owner_reason_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_muc_owner_reason_cdata(__TopXMLNS, _val) -> _val. - -encode_muc_owner_reason_cdata(undefined, _acc) -> _acc; -encode_muc_owner_reason_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_muc_owner_password(__TopXMLNS, __IgnoreEls, - {xmlel, <<"password">>, _attrs, _els}) -> - Cdata = decode_muc_owner_password_els(__TopXMLNS, - __IgnoreEls, _els, <<>>), - Cdata. - -decode_muc_owner_password_els(__TopXMLNS, __IgnoreEls, - [], Cdata) -> - decode_muc_owner_password_cdata(__TopXMLNS, Cdata); -decode_muc_owner_password_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_muc_owner_password_els(__TopXMLNS, __IgnoreEls, - _els, <<Cdata/binary, _data/binary>>); -decode_muc_owner_password_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_muc_owner_password_els(__TopXMLNS, __IgnoreEls, - _els, Cdata). - -encode_muc_owner_password(Cdata, _xmlns_attrs) -> - _els = encode_muc_owner_password_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"password">>, _attrs, _els}. - -decode_muc_owner_password_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_muc_owner_password_cdata(__TopXMLNS, _val) -> - _val. - -encode_muc_owner_password_cdata(undefined, _acc) -> - _acc; -encode_muc_owner_password_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_muc_user(__TopXMLNS, __IgnoreEls, - {xmlel, <<"x">>, _attrs, _els}) -> - {Status_codes, Items, Invites, Decline, Destroy} = - decode_muc_user_els(__TopXMLNS, __IgnoreEls, _els, [], - [], [], undefined, undefined), - Password = decode_muc_user_attrs(__TopXMLNS, _attrs, - undefined), - {muc_user, Decline, Destroy, Invites, Items, - Status_codes, Password}. - -decode_muc_user_els(__TopXMLNS, __IgnoreEls, [], - Status_codes, Items, Invites, Decline, Destroy) -> - {lists:reverse(Status_codes), lists:reverse(Items), - lists:reverse(Invites), Decline, Destroy}; -decode_muc_user_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"decline">>, _attrs, _} = _el | _els], - Status_codes, Items, Invites, Decline, Destroy) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_muc_user_els(__TopXMLNS, __IgnoreEls, _els, - Status_codes, Items, Invites, - decode_muc_user_decline(__TopXMLNS, __IgnoreEls, - _el), - Destroy); - true -> - decode_muc_user_els(__TopXMLNS, __IgnoreEls, _els, - Status_codes, Items, Invites, Decline, Destroy) - end; -decode_muc_user_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"destroy">>, _attrs, _} = _el | _els], - Status_codes, Items, Invites, Decline, Destroy) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_muc_user_els(__TopXMLNS, __IgnoreEls, _els, - Status_codes, Items, Invites, Decline, - decode_muc_user_destroy(__TopXMLNS, __IgnoreEls, - _el)); - true -> - decode_muc_user_els(__TopXMLNS, __IgnoreEls, _els, - Status_codes, Items, Invites, Decline, Destroy) - end; -decode_muc_user_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"invite">>, _attrs, _} = _el | _els], - Status_codes, Items, Invites, Decline, Destroy) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_muc_user_els(__TopXMLNS, __IgnoreEls, _els, - Status_codes, Items, - [decode_muc_user_invite(__TopXMLNS, __IgnoreEls, - _el) - | Invites], - Decline, Destroy); - true -> - decode_muc_user_els(__TopXMLNS, __IgnoreEls, _els, - Status_codes, Items, Invites, Decline, Destroy) - end; -decode_muc_user_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"item">>, _attrs, _} = _el | _els], - Status_codes, Items, Invites, Decline, Destroy) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_muc_user_els(__TopXMLNS, __IgnoreEls, _els, - Status_codes, - [decode_muc_user_item(__TopXMLNS, __IgnoreEls, - _el) - | Items], - Invites, Decline, Destroy); - true -> - decode_muc_user_els(__TopXMLNS, __IgnoreEls, _els, - Status_codes, Items, Invites, Decline, Destroy) - end; -decode_muc_user_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"status">>, _attrs, _} = _el | _els], - Status_codes, Items, Invites, Decline, Destroy) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_muc_user_els(__TopXMLNS, __IgnoreEls, _els, - case decode_muc_user_status(__TopXMLNS, - __IgnoreEls, _el) - of - undefined -> Status_codes; - _new_el -> [_new_el | Status_codes] - end, - Items, Invites, Decline, Destroy); - true -> - decode_muc_user_els(__TopXMLNS, __IgnoreEls, _els, - Status_codes, Items, Invites, Decline, Destroy) - end; -decode_muc_user_els(__TopXMLNS, __IgnoreEls, [_ | _els], - Status_codes, Items, Invites, Decline, Destroy) -> - decode_muc_user_els(__TopXMLNS, __IgnoreEls, _els, - Status_codes, Items, Invites, Decline, Destroy). - -decode_muc_user_attrs(__TopXMLNS, - [{<<"password">>, _val} | _attrs], _Password) -> - decode_muc_user_attrs(__TopXMLNS, _attrs, _val); -decode_muc_user_attrs(__TopXMLNS, [_ | _attrs], - Password) -> - decode_muc_user_attrs(__TopXMLNS, _attrs, Password); -decode_muc_user_attrs(__TopXMLNS, [], Password) -> - decode_muc_user_attr_password(__TopXMLNS, Password). - -encode_muc_user({muc_user, Decline, Destroy, Invites, - Items, Status_codes, Password}, - _xmlns_attrs) -> - _els = - lists:reverse('encode_muc_user_$status_codes'(Status_codes, - 'encode_muc_user_$items'(Items, - 'encode_muc_user_$invites'(Invites, - 'encode_muc_user_$decline'(Decline, - 'encode_muc_user_$destroy'(Destroy, - [])))))), - _attrs = encode_muc_user_attr_password(Password, - _xmlns_attrs), - {xmlel, <<"x">>, _attrs, _els}. - -'encode_muc_user_$status_codes'([], _acc) -> _acc; -'encode_muc_user_$status_codes'([Status_codes | _els], - _acc) -> - 'encode_muc_user_$status_codes'(_els, - [encode_muc_user_status(Status_codes, []) - | _acc]). - -'encode_muc_user_$items'([], _acc) -> _acc; -'encode_muc_user_$items'([Items | _els], _acc) -> - 'encode_muc_user_$items'(_els, - [encode_muc_user_item(Items, []) | _acc]). - -'encode_muc_user_$invites'([], _acc) -> _acc; -'encode_muc_user_$invites'([Invites | _els], _acc) -> - 'encode_muc_user_$invites'(_els, - [encode_muc_user_invite(Invites, []) | _acc]). - -'encode_muc_user_$decline'(undefined, _acc) -> _acc; -'encode_muc_user_$decline'(Decline, _acc) -> - [encode_muc_user_decline(Decline, []) | _acc]. - -'encode_muc_user_$destroy'(undefined, _acc) -> _acc; -'encode_muc_user_$destroy'(Destroy, _acc) -> - [encode_muc_user_destroy(Destroy, []) | _acc]. - -decode_muc_user_attr_password(__TopXMLNS, undefined) -> - undefined; -decode_muc_user_attr_password(__TopXMLNS, _val) -> _val. - -encode_muc_user_attr_password(undefined, _acc) -> _acc; -encode_muc_user_attr_password(_val, _acc) -> - [{<<"password">>, _val} | _acc]. - -decode_muc_user_item(__TopXMLNS, __IgnoreEls, - {xmlel, <<"item">>, _attrs, _els}) -> - {Actor, Continue, Reason} = - decode_muc_user_item_els(__TopXMLNS, __IgnoreEls, _els, - undefined, undefined, undefined), - {Affiliation, Role, Jid, Nick} = - decode_muc_user_item_attrs(__TopXMLNS, _attrs, - undefined, undefined, undefined, undefined), - {muc_item, Actor, Continue, Reason, Affiliation, Role, - Jid, Nick}. - -decode_muc_user_item_els(__TopXMLNS, __IgnoreEls, [], - Actor, Continue, Reason) -> - {Actor, Continue, Reason}; -decode_muc_user_item_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"actor">>, _attrs, _} = _el | _els], Actor, - Continue, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_muc_user_item_els(__TopXMLNS, __IgnoreEls, _els, - decode_muc_user_actor(__TopXMLNS, - __IgnoreEls, _el), - Continue, Reason); - true -> - decode_muc_user_item_els(__TopXMLNS, __IgnoreEls, _els, - Actor, Continue, Reason) - end; -decode_muc_user_item_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"continue">>, _attrs, _} = _el | _els], - Actor, Continue, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_muc_user_item_els(__TopXMLNS, __IgnoreEls, _els, - Actor, - decode_muc_user_continue(__TopXMLNS, - __IgnoreEls, _el), - Reason); - true -> - decode_muc_user_item_els(__TopXMLNS, __IgnoreEls, _els, - Actor, Continue, Reason) - end; -decode_muc_user_item_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"reason">>, _attrs, _} = _el | _els], Actor, - Continue, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_muc_user_item_els(__TopXMLNS, __IgnoreEls, _els, - Actor, Continue, - decode_muc_user_reason(__TopXMLNS, - __IgnoreEls, _el)); - true -> - decode_muc_user_item_els(__TopXMLNS, __IgnoreEls, _els, - Actor, Continue, Reason) - end; -decode_muc_user_item_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Actor, Continue, Reason) -> - decode_muc_user_item_els(__TopXMLNS, __IgnoreEls, _els, - Actor, Continue, Reason). - -decode_muc_user_item_attrs(__TopXMLNS, - [{<<"affiliation">>, _val} | _attrs], _Affiliation, - Role, Jid, Nick) -> - decode_muc_user_item_attrs(__TopXMLNS, _attrs, _val, - Role, Jid, Nick); -decode_muc_user_item_attrs(__TopXMLNS, - [{<<"role">>, _val} | _attrs], Affiliation, _Role, - Jid, Nick) -> - decode_muc_user_item_attrs(__TopXMLNS, _attrs, - Affiliation, _val, Jid, Nick); -decode_muc_user_item_attrs(__TopXMLNS, - [{<<"jid">>, _val} | _attrs], Affiliation, Role, - _Jid, Nick) -> - decode_muc_user_item_attrs(__TopXMLNS, _attrs, - Affiliation, Role, _val, Nick); -decode_muc_user_item_attrs(__TopXMLNS, - [{<<"nick">>, _val} | _attrs], Affiliation, Role, - Jid, _Nick) -> - decode_muc_user_item_attrs(__TopXMLNS, _attrs, - Affiliation, Role, Jid, _val); -decode_muc_user_item_attrs(__TopXMLNS, [_ | _attrs], - Affiliation, Role, Jid, Nick) -> - decode_muc_user_item_attrs(__TopXMLNS, _attrs, - Affiliation, Role, Jid, Nick); -decode_muc_user_item_attrs(__TopXMLNS, [], Affiliation, - Role, Jid, Nick) -> - {decode_muc_user_item_attr_affiliation(__TopXMLNS, - Affiliation), - decode_muc_user_item_attr_role(__TopXMLNS, Role), - decode_muc_user_item_attr_jid(__TopXMLNS, Jid), - decode_muc_user_item_attr_nick(__TopXMLNS, Nick)}. - -encode_muc_user_item({muc_item, Actor, Continue, Reason, - Affiliation, Role, Jid, Nick}, - _xmlns_attrs) -> - _els = - lists:reverse('encode_muc_user_item_$actor'(Actor, - 'encode_muc_user_item_$continue'(Continue, - 'encode_muc_user_item_$reason'(Reason, - [])))), - _attrs = encode_muc_user_item_attr_nick(Nick, - encode_muc_user_item_attr_jid(Jid, - encode_muc_user_item_attr_role(Role, - encode_muc_user_item_attr_affiliation(Affiliation, - _xmlns_attrs)))), - {xmlel, <<"item">>, _attrs, _els}. - -'encode_muc_user_item_$actor'(undefined, _acc) -> _acc; -'encode_muc_user_item_$actor'(Actor, _acc) -> - [encode_muc_user_actor(Actor, []) | _acc]. - -'encode_muc_user_item_$continue'(undefined, _acc) -> - _acc; -'encode_muc_user_item_$continue'(Continue, _acc) -> - [encode_muc_user_continue(Continue, []) | _acc]. - -'encode_muc_user_item_$reason'(undefined, _acc) -> _acc; -'encode_muc_user_item_$reason'(Reason, _acc) -> - [encode_muc_user_reason(Reason, []) | _acc]. - -decode_muc_user_item_attr_affiliation(__TopXMLNS, - undefined) -> - undefined; -decode_muc_user_item_attr_affiliation(__TopXMLNS, - _val) -> - case catch dec_enum(_val, - [admin, member, none, outcast, owner]) - of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"affiliation">>, <<"item">>, - __TopXMLNS}}); - _res -> _res - end. - -encode_muc_user_item_attr_affiliation(undefined, - _acc) -> - _acc; -encode_muc_user_item_attr_affiliation(_val, _acc) -> - [{<<"affiliation">>, enc_enum(_val)} | _acc]. - -decode_muc_user_item_attr_role(__TopXMLNS, undefined) -> - undefined; -decode_muc_user_item_attr_role(__TopXMLNS, _val) -> - case catch dec_enum(_val, - [moderator, none, participant, visitor]) - of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"role">>, <<"item">>, __TopXMLNS}}); - _res -> _res - end. - -encode_muc_user_item_attr_role(undefined, _acc) -> _acc; -encode_muc_user_item_attr_role(_val, _acc) -> - [{<<"role">>, enc_enum(_val)} | _acc]. - -decode_muc_user_item_attr_jid(__TopXMLNS, undefined) -> - undefined; -decode_muc_user_item_attr_jid(__TopXMLNS, _val) -> - case catch dec_jid(_val) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"jid">>, <<"item">>, __TopXMLNS}}); - _res -> _res - end. - -encode_muc_user_item_attr_jid(undefined, _acc) -> _acc; -encode_muc_user_item_attr_jid(_val, _acc) -> - [{<<"jid">>, enc_jid(_val)} | _acc]. - -decode_muc_user_item_attr_nick(__TopXMLNS, undefined) -> - undefined; -decode_muc_user_item_attr_nick(__TopXMLNS, _val) -> - _val. - -encode_muc_user_item_attr_nick(undefined, _acc) -> _acc; -encode_muc_user_item_attr_nick(_val, _acc) -> - [{<<"nick">>, _val} | _acc]. - -decode_muc_user_status(__TopXMLNS, __IgnoreEls, - {xmlel, <<"status">>, _attrs, _els}) -> - Code = decode_muc_user_status_attrs(__TopXMLNS, _attrs, - undefined), - Code. - -decode_muc_user_status_attrs(__TopXMLNS, - [{<<"code">>, _val} | _attrs], _Code) -> - decode_muc_user_status_attrs(__TopXMLNS, _attrs, _val); -decode_muc_user_status_attrs(__TopXMLNS, [_ | _attrs], - Code) -> - decode_muc_user_status_attrs(__TopXMLNS, _attrs, Code); -decode_muc_user_status_attrs(__TopXMLNS, [], Code) -> - decode_muc_user_status_attr_code(__TopXMLNS, Code). - -encode_muc_user_status(Code, _xmlns_attrs) -> - _els = [], - _attrs = encode_muc_user_status_attr_code(Code, - _xmlns_attrs), - {xmlel, <<"status">>, _attrs, _els}. - -decode_muc_user_status_attr_code(__TopXMLNS, - undefined) -> - undefined; -decode_muc_user_status_attr_code(__TopXMLNS, _val) -> - case catch dec_int(_val, 100, 999) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"code">>, <<"status">>, - __TopXMLNS}}); - _res -> _res - end. - -encode_muc_user_status_attr_code(undefined, _acc) -> - _acc; -encode_muc_user_status_attr_code(_val, _acc) -> - [{<<"code">>, enc_int(_val)} | _acc]. - -decode_muc_user_continue(__TopXMLNS, __IgnoreEls, - {xmlel, <<"continue">>, _attrs, _els}) -> - Thread = decode_muc_user_continue_attrs(__TopXMLNS, - _attrs, undefined), - Thread. - -decode_muc_user_continue_attrs(__TopXMLNS, - [{<<"thread">>, _val} | _attrs], _Thread) -> - decode_muc_user_continue_attrs(__TopXMLNS, _attrs, - _val); -decode_muc_user_continue_attrs(__TopXMLNS, [_ | _attrs], - Thread) -> - decode_muc_user_continue_attrs(__TopXMLNS, _attrs, - Thread); -decode_muc_user_continue_attrs(__TopXMLNS, [], - Thread) -> - decode_muc_user_continue_attr_thread(__TopXMLNS, - Thread). - -encode_muc_user_continue(Thread, _xmlns_attrs) -> - _els = [], - _attrs = encode_muc_user_continue_attr_thread(Thread, - _xmlns_attrs), - {xmlel, <<"continue">>, _attrs, _els}. - -decode_muc_user_continue_attr_thread(__TopXMLNS, - undefined) -> - undefined; -decode_muc_user_continue_attr_thread(__TopXMLNS, - _val) -> - _val. - -encode_muc_user_continue_attr_thread(undefined, _acc) -> - _acc; -encode_muc_user_continue_attr_thread(_val, _acc) -> - [{<<"thread">>, _val} | _acc]. - -decode_muc_user_actor(__TopXMLNS, __IgnoreEls, - {xmlel, <<"actor">>, _attrs, _els}) -> - {Jid, Nick} = decode_muc_user_actor_attrs(__TopXMLNS, - _attrs, undefined, undefined), - {muc_actor, Jid, Nick}. - -decode_muc_user_actor_attrs(__TopXMLNS, - [{<<"jid">>, _val} | _attrs], _Jid, Nick) -> - decode_muc_user_actor_attrs(__TopXMLNS, _attrs, _val, - Nick); -decode_muc_user_actor_attrs(__TopXMLNS, - [{<<"nick">>, _val} | _attrs], Jid, _Nick) -> - decode_muc_user_actor_attrs(__TopXMLNS, _attrs, Jid, - _val); -decode_muc_user_actor_attrs(__TopXMLNS, [_ | _attrs], - Jid, Nick) -> - decode_muc_user_actor_attrs(__TopXMLNS, _attrs, Jid, - Nick); -decode_muc_user_actor_attrs(__TopXMLNS, [], Jid, - Nick) -> - {decode_muc_user_actor_attr_jid(__TopXMLNS, Jid), - decode_muc_user_actor_attr_nick(__TopXMLNS, Nick)}. - -encode_muc_user_actor({muc_actor, Jid, Nick}, - _xmlns_attrs) -> - _els = [], - _attrs = encode_muc_user_actor_attr_nick(Nick, - encode_muc_user_actor_attr_jid(Jid, - _xmlns_attrs)), - {xmlel, <<"actor">>, _attrs, _els}. - -decode_muc_user_actor_attr_jid(__TopXMLNS, undefined) -> - undefined; -decode_muc_user_actor_attr_jid(__TopXMLNS, _val) -> - case catch dec_jid(_val) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"jid">>, <<"actor">>, __TopXMLNS}}); - _res -> _res - end. - -encode_muc_user_actor_attr_jid(undefined, _acc) -> _acc; -encode_muc_user_actor_attr_jid(_val, _acc) -> - [{<<"jid">>, enc_jid(_val)} | _acc]. - -decode_muc_user_actor_attr_nick(__TopXMLNS, - undefined) -> - undefined; -decode_muc_user_actor_attr_nick(__TopXMLNS, _val) -> - _val. - -encode_muc_user_actor_attr_nick(undefined, _acc) -> - _acc; -encode_muc_user_actor_attr_nick(_val, _acc) -> - [{<<"nick">>, _val} | _acc]. - -decode_muc_user_invite(__TopXMLNS, __IgnoreEls, - {xmlel, <<"invite">>, _attrs, _els}) -> - Reason = decode_muc_user_invite_els(__TopXMLNS, - __IgnoreEls, _els, undefined), - {To, From} = decode_muc_user_invite_attrs(__TopXMLNS, - _attrs, undefined, undefined), - {muc_invite, Reason, From, To}. - -decode_muc_user_invite_els(__TopXMLNS, __IgnoreEls, [], - Reason) -> - Reason; -decode_muc_user_invite_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"reason">>, _attrs, _} = _el | _els], - Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_muc_user_invite_els(__TopXMLNS, __IgnoreEls, - _els, - decode_muc_user_reason(__TopXMLNS, - __IgnoreEls, _el)); - true -> - decode_muc_user_invite_els(__TopXMLNS, __IgnoreEls, - _els, Reason) - end; -decode_muc_user_invite_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Reason) -> - decode_muc_user_invite_els(__TopXMLNS, __IgnoreEls, - _els, Reason). - -decode_muc_user_invite_attrs(__TopXMLNS, - [{<<"to">>, _val} | _attrs], _To, From) -> - decode_muc_user_invite_attrs(__TopXMLNS, _attrs, _val, - From); -decode_muc_user_invite_attrs(__TopXMLNS, - [{<<"from">>, _val} | _attrs], To, _From) -> - decode_muc_user_invite_attrs(__TopXMLNS, _attrs, To, - _val); -decode_muc_user_invite_attrs(__TopXMLNS, [_ | _attrs], - To, From) -> - decode_muc_user_invite_attrs(__TopXMLNS, _attrs, To, - From); -decode_muc_user_invite_attrs(__TopXMLNS, [], To, - From) -> - {decode_muc_user_invite_attr_to(__TopXMLNS, To), - decode_muc_user_invite_attr_from(__TopXMLNS, From)}. - -encode_muc_user_invite({muc_invite, Reason, From, To}, - _xmlns_attrs) -> - _els = - lists:reverse('encode_muc_user_invite_$reason'(Reason, - [])), - _attrs = encode_muc_user_invite_attr_from(From, - encode_muc_user_invite_attr_to(To, - _xmlns_attrs)), - {xmlel, <<"invite">>, _attrs, _els}. - -'encode_muc_user_invite_$reason'(undefined, _acc) -> - _acc; -'encode_muc_user_invite_$reason'(Reason, _acc) -> - [encode_muc_user_reason(Reason, []) | _acc]. - -decode_muc_user_invite_attr_to(__TopXMLNS, undefined) -> - undefined; -decode_muc_user_invite_attr_to(__TopXMLNS, _val) -> - case catch dec_jid(_val) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"to">>, <<"invite">>, __TopXMLNS}}); - _res -> _res - end. - -encode_muc_user_invite_attr_to(undefined, _acc) -> _acc; -encode_muc_user_invite_attr_to(_val, _acc) -> - [{<<"to">>, enc_jid(_val)} | _acc]. - -decode_muc_user_invite_attr_from(__TopXMLNS, - undefined) -> - undefined; -decode_muc_user_invite_attr_from(__TopXMLNS, _val) -> - case catch dec_jid(_val) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"from">>, <<"invite">>, - __TopXMLNS}}); - _res -> _res - end. - -encode_muc_user_invite_attr_from(undefined, _acc) -> - _acc; -encode_muc_user_invite_attr_from(_val, _acc) -> - [{<<"from">>, enc_jid(_val)} | _acc]. - -decode_muc_user_destroy(__TopXMLNS, __IgnoreEls, - {xmlel, <<"destroy">>, _attrs, _els}) -> - Reason = decode_muc_user_destroy_els(__TopXMLNS, - __IgnoreEls, _els, undefined), - Jid = decode_muc_user_destroy_attrs(__TopXMLNS, _attrs, - undefined), - {muc_user_destroy, Reason, Jid}. - -decode_muc_user_destroy_els(__TopXMLNS, __IgnoreEls, [], - Reason) -> - Reason; -decode_muc_user_destroy_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"reason">>, _attrs, _} = _el | _els], - Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_muc_user_destroy_els(__TopXMLNS, __IgnoreEls, - _els, - decode_muc_user_reason(__TopXMLNS, - __IgnoreEls, - _el)); - true -> - decode_muc_user_destroy_els(__TopXMLNS, __IgnoreEls, - _els, Reason) - end; -decode_muc_user_destroy_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Reason) -> - decode_muc_user_destroy_els(__TopXMLNS, __IgnoreEls, - _els, Reason). - -decode_muc_user_destroy_attrs(__TopXMLNS, - [{<<"jid">>, _val} | _attrs], _Jid) -> - decode_muc_user_destroy_attrs(__TopXMLNS, _attrs, _val); -decode_muc_user_destroy_attrs(__TopXMLNS, [_ | _attrs], - Jid) -> - decode_muc_user_destroy_attrs(__TopXMLNS, _attrs, Jid); -decode_muc_user_destroy_attrs(__TopXMLNS, [], Jid) -> - decode_muc_user_destroy_attr_jid(__TopXMLNS, Jid). - -encode_muc_user_destroy({muc_user_destroy, Reason, Jid}, - _xmlns_attrs) -> - _els = - lists:reverse('encode_muc_user_destroy_$reason'(Reason, - [])), - _attrs = encode_muc_user_destroy_attr_jid(Jid, - _xmlns_attrs), - {xmlel, <<"destroy">>, _attrs, _els}. - -'encode_muc_user_destroy_$reason'(undefined, _acc) -> - _acc; -'encode_muc_user_destroy_$reason'(Reason, _acc) -> - [encode_muc_user_reason(Reason, []) | _acc]. - -decode_muc_user_destroy_attr_jid(__TopXMLNS, - undefined) -> - undefined; -decode_muc_user_destroy_attr_jid(__TopXMLNS, _val) -> - case catch dec_jid(_val) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"jid">>, <<"destroy">>, - __TopXMLNS}}); - _res -> _res - end. - -encode_muc_user_destroy_attr_jid(undefined, _acc) -> - _acc; -encode_muc_user_destroy_attr_jid(_val, _acc) -> - [{<<"jid">>, enc_jid(_val)} | _acc]. - -decode_muc_user_decline(__TopXMLNS, __IgnoreEls, - {xmlel, <<"decline">>, _attrs, _els}) -> - Reason = decode_muc_user_decline_els(__TopXMLNS, - __IgnoreEls, _els, undefined), - {To, From} = decode_muc_user_decline_attrs(__TopXMLNS, - _attrs, undefined, undefined), - {muc_decline, Reason, From, To}. - -decode_muc_user_decline_els(__TopXMLNS, __IgnoreEls, [], - Reason) -> - Reason; -decode_muc_user_decline_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"reason">>, _attrs, _} = _el | _els], - Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_muc_user_decline_els(__TopXMLNS, __IgnoreEls, - _els, - decode_muc_user_reason(__TopXMLNS, - __IgnoreEls, - _el)); - true -> - decode_muc_user_decline_els(__TopXMLNS, __IgnoreEls, - _els, Reason) - end; -decode_muc_user_decline_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Reason) -> - decode_muc_user_decline_els(__TopXMLNS, __IgnoreEls, - _els, Reason). - -decode_muc_user_decline_attrs(__TopXMLNS, - [{<<"to">>, _val} | _attrs], _To, From) -> - decode_muc_user_decline_attrs(__TopXMLNS, _attrs, _val, - From); -decode_muc_user_decline_attrs(__TopXMLNS, - [{<<"from">>, _val} | _attrs], To, _From) -> - decode_muc_user_decline_attrs(__TopXMLNS, _attrs, To, - _val); -decode_muc_user_decline_attrs(__TopXMLNS, [_ | _attrs], - To, From) -> - decode_muc_user_decline_attrs(__TopXMLNS, _attrs, To, - From); -decode_muc_user_decline_attrs(__TopXMLNS, [], To, - From) -> - {decode_muc_user_decline_attr_to(__TopXMLNS, To), - decode_muc_user_decline_attr_from(__TopXMLNS, From)}. - -encode_muc_user_decline({muc_decline, Reason, From, To}, - _xmlns_attrs) -> - _els = - lists:reverse('encode_muc_user_decline_$reason'(Reason, - [])), - _attrs = encode_muc_user_decline_attr_from(From, - encode_muc_user_decline_attr_to(To, - _xmlns_attrs)), - {xmlel, <<"decline">>, _attrs, _els}. - -'encode_muc_user_decline_$reason'(undefined, _acc) -> - _acc; -'encode_muc_user_decline_$reason'(Reason, _acc) -> - [encode_muc_user_reason(Reason, []) | _acc]. - -decode_muc_user_decline_attr_to(__TopXMLNS, - undefined) -> - undefined; -decode_muc_user_decline_attr_to(__TopXMLNS, _val) -> - case catch dec_jid(_val) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"to">>, <<"decline">>, __TopXMLNS}}); - _res -> _res - end. - -encode_muc_user_decline_attr_to(undefined, _acc) -> - _acc; -encode_muc_user_decline_attr_to(_val, _acc) -> - [{<<"to">>, enc_jid(_val)} | _acc]. - -decode_muc_user_decline_attr_from(__TopXMLNS, - undefined) -> - undefined; -decode_muc_user_decline_attr_from(__TopXMLNS, _val) -> - case catch dec_jid(_val) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"from">>, <<"decline">>, - __TopXMLNS}}); - _res -> _res - end. - -encode_muc_user_decline_attr_from(undefined, _acc) -> - _acc; -encode_muc_user_decline_attr_from(_val, _acc) -> - [{<<"from">>, enc_jid(_val)} | _acc]. - -decode_muc_user_reason(__TopXMLNS, __IgnoreEls, - {xmlel, <<"reason">>, _attrs, _els}) -> - Cdata = decode_muc_user_reason_els(__TopXMLNS, - __IgnoreEls, _els, <<>>), - Cdata. - -decode_muc_user_reason_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_muc_user_reason_cdata(__TopXMLNS, Cdata); -decode_muc_user_reason_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_muc_user_reason_els(__TopXMLNS, __IgnoreEls, - _els, <<Cdata/binary, _data/binary>>); -decode_muc_user_reason_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_muc_user_reason_els(__TopXMLNS, __IgnoreEls, - _els, Cdata). - -encode_muc_user_reason(Cdata, _xmlns_attrs) -> - _els = encode_muc_user_reason_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"reason">>, _attrs, _els}. - -decode_muc_user_reason_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_muc_user_reason_cdata(__TopXMLNS, _val) -> _val. - -encode_muc_user_reason_cdata(undefined, _acc) -> _acc; -encode_muc_user_reason_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_muc_history(__TopXMLNS, __IgnoreEls, - {xmlel, <<"history">>, _attrs, _els}) -> - {Maxchars, Maxstanzas, Seconds, Since} = - decode_muc_history_attrs(__TopXMLNS, _attrs, undefined, - undefined, undefined, undefined), - {muc_history, Maxchars, Maxstanzas, Seconds, Since}. - -decode_muc_history_attrs(__TopXMLNS, - [{<<"maxchars">>, _val} | _attrs], _Maxchars, - Maxstanzas, Seconds, Since) -> - decode_muc_history_attrs(__TopXMLNS, _attrs, _val, - Maxstanzas, Seconds, Since); -decode_muc_history_attrs(__TopXMLNS, - [{<<"maxstanzas">>, _val} | _attrs], Maxchars, - _Maxstanzas, Seconds, Since) -> - decode_muc_history_attrs(__TopXMLNS, _attrs, Maxchars, - _val, Seconds, Since); -decode_muc_history_attrs(__TopXMLNS, - [{<<"seconds">>, _val} | _attrs], Maxchars, Maxstanzas, - _Seconds, Since) -> - decode_muc_history_attrs(__TopXMLNS, _attrs, Maxchars, - Maxstanzas, _val, Since); -decode_muc_history_attrs(__TopXMLNS, - [{<<"since">>, _val} | _attrs], Maxchars, Maxstanzas, - Seconds, _Since) -> - decode_muc_history_attrs(__TopXMLNS, _attrs, Maxchars, - Maxstanzas, Seconds, _val); -decode_muc_history_attrs(__TopXMLNS, [_ | _attrs], - Maxchars, Maxstanzas, Seconds, Since) -> - decode_muc_history_attrs(__TopXMLNS, _attrs, Maxchars, - Maxstanzas, Seconds, Since); -decode_muc_history_attrs(__TopXMLNS, [], Maxchars, - Maxstanzas, Seconds, Since) -> - {decode_muc_history_attr_maxchars(__TopXMLNS, Maxchars), - decode_muc_history_attr_maxstanzas(__TopXMLNS, - Maxstanzas), - decode_muc_history_attr_seconds(__TopXMLNS, Seconds), - decode_muc_history_attr_since(__TopXMLNS, Since)}. - -encode_muc_history({muc_history, Maxchars, Maxstanzas, - Seconds, Since}, - _xmlns_attrs) -> - _els = [], - _attrs = encode_muc_history_attr_since(Since, - encode_muc_history_attr_seconds(Seconds, - encode_muc_history_attr_maxstanzas(Maxstanzas, - encode_muc_history_attr_maxchars(Maxchars, - _xmlns_attrs)))), - {xmlel, <<"history">>, _attrs, _els}. - -decode_muc_history_attr_maxchars(__TopXMLNS, - undefined) -> - undefined; -decode_muc_history_attr_maxchars(__TopXMLNS, _val) -> - case catch dec_int(_val, 0, infinity) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"maxchars">>, <<"history">>, - __TopXMLNS}}); - _res -> _res - end. - -encode_muc_history_attr_maxchars(undefined, _acc) -> - _acc; -encode_muc_history_attr_maxchars(_val, _acc) -> - [{<<"maxchars">>, enc_int(_val)} | _acc]. - -decode_muc_history_attr_maxstanzas(__TopXMLNS, - undefined) -> - undefined; -decode_muc_history_attr_maxstanzas(__TopXMLNS, _val) -> - case catch dec_int(_val, 0, infinity) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"maxstanzas">>, <<"history">>, - __TopXMLNS}}); - _res -> _res - end. - -encode_muc_history_attr_maxstanzas(undefined, _acc) -> - _acc; -encode_muc_history_attr_maxstanzas(_val, _acc) -> - [{<<"maxstanzas">>, enc_int(_val)} | _acc]. - -decode_muc_history_attr_seconds(__TopXMLNS, - undefined) -> - undefined; -decode_muc_history_attr_seconds(__TopXMLNS, _val) -> - case catch dec_int(_val, 0, infinity) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"seconds">>, <<"history">>, - __TopXMLNS}}); - _res -> _res - end. - -encode_muc_history_attr_seconds(undefined, _acc) -> - _acc; -encode_muc_history_attr_seconds(_val, _acc) -> - [{<<"seconds">>, enc_int(_val)} | _acc]. - -decode_muc_history_attr_since(__TopXMLNS, undefined) -> - undefined; -decode_muc_history_attr_since(__TopXMLNS, _val) -> - case catch dec_utc(_val) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"since">>, <<"history">>, - __TopXMLNS}}); - _res -> _res - end. - -encode_muc_history_attr_since(undefined, _acc) -> _acc; -encode_muc_history_attr_since(_val, _acc) -> - [{<<"since">>, enc_utc(_val)} | _acc]. - -decode_bytestreams(__TopXMLNS, __IgnoreEls, - {xmlel, <<"query">>, _attrs, _els}) -> - {Hosts, Used, Activate} = - decode_bytestreams_els(__TopXMLNS, __IgnoreEls, _els, - [], undefined, undefined), - {Dstaddr, Sid, Mode} = - decode_bytestreams_attrs(__TopXMLNS, _attrs, undefined, - undefined, undefined), - {bytestreams, Hosts, Used, Activate, Dstaddr, Mode, - Sid}. - -decode_bytestreams_els(__TopXMLNS, __IgnoreEls, [], - Hosts, Used, Activate) -> - {lists:reverse(Hosts), Used, Activate}; -decode_bytestreams_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"streamhost">>, _attrs, _} = _el | _els], - Hosts, Used, Activate) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_bytestreams_els(__TopXMLNS, __IgnoreEls, _els, - [decode_bytestreams_streamhost(__TopXMLNS, - __IgnoreEls, - _el) - | Hosts], - Used, Activate); - true -> - decode_bytestreams_els(__TopXMLNS, __IgnoreEls, _els, - Hosts, Used, Activate) - end; -decode_bytestreams_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"streamhost-used">>, _attrs, _} = _el - | _els], - Hosts, Used, Activate) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_bytestreams_els(__TopXMLNS, __IgnoreEls, _els, - Hosts, - decode_bytestreams_streamhost_used(__TopXMLNS, - __IgnoreEls, - _el), - Activate); - true -> - decode_bytestreams_els(__TopXMLNS, __IgnoreEls, _els, - Hosts, Used, Activate) - end; -decode_bytestreams_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"activate">>, _attrs, _} = _el | _els], - Hosts, Used, Activate) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_bytestreams_els(__TopXMLNS, __IgnoreEls, _els, - Hosts, Used, - decode_bytestreams_activate(__TopXMLNS, - __IgnoreEls, - _el)); - true -> - decode_bytestreams_els(__TopXMLNS, __IgnoreEls, _els, - Hosts, Used, Activate) - end; -decode_bytestreams_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Hosts, Used, Activate) -> - decode_bytestreams_els(__TopXMLNS, __IgnoreEls, _els, - Hosts, Used, Activate). - -decode_bytestreams_attrs(__TopXMLNS, - [{<<"dstaddr">>, _val} | _attrs], _Dstaddr, Sid, - Mode) -> - decode_bytestreams_attrs(__TopXMLNS, _attrs, _val, Sid, - Mode); -decode_bytestreams_attrs(__TopXMLNS, - [{<<"sid">>, _val} | _attrs], Dstaddr, _Sid, Mode) -> - decode_bytestreams_attrs(__TopXMLNS, _attrs, Dstaddr, - _val, Mode); -decode_bytestreams_attrs(__TopXMLNS, - [{<<"mode">>, _val} | _attrs], Dstaddr, Sid, _Mode) -> - decode_bytestreams_attrs(__TopXMLNS, _attrs, Dstaddr, - Sid, _val); -decode_bytestreams_attrs(__TopXMLNS, [_ | _attrs], - Dstaddr, Sid, Mode) -> - decode_bytestreams_attrs(__TopXMLNS, _attrs, Dstaddr, - Sid, Mode); -decode_bytestreams_attrs(__TopXMLNS, [], Dstaddr, Sid, - Mode) -> - {decode_bytestreams_attr_dstaddr(__TopXMLNS, Dstaddr), - decode_bytestreams_attr_sid(__TopXMLNS, Sid), - decode_bytestreams_attr_mode(__TopXMLNS, Mode)}. - -encode_bytestreams({bytestreams, Hosts, Used, Activate, - Dstaddr, Mode, Sid}, - _xmlns_attrs) -> - _els = lists:reverse('encode_bytestreams_$hosts'(Hosts, - 'encode_bytestreams_$used'(Used, - 'encode_bytestreams_$activate'(Activate, - [])))), - _attrs = encode_bytestreams_attr_mode(Mode, - encode_bytestreams_attr_sid(Sid, - encode_bytestreams_attr_dstaddr(Dstaddr, - _xmlns_attrs))), - {xmlel, <<"query">>, _attrs, _els}. - -'encode_bytestreams_$hosts'([], _acc) -> _acc; -'encode_bytestreams_$hosts'([Hosts | _els], _acc) -> - 'encode_bytestreams_$hosts'(_els, - [encode_bytestreams_streamhost(Hosts, []) - | _acc]). - -'encode_bytestreams_$used'(undefined, _acc) -> _acc; -'encode_bytestreams_$used'(Used, _acc) -> - [encode_bytestreams_streamhost_used(Used, []) | _acc]. - -'encode_bytestreams_$activate'(undefined, _acc) -> _acc; -'encode_bytestreams_$activate'(Activate, _acc) -> - [encode_bytestreams_activate(Activate, []) | _acc]. - -decode_bytestreams_attr_dstaddr(__TopXMLNS, - undefined) -> - undefined; -decode_bytestreams_attr_dstaddr(__TopXMLNS, _val) -> - _val. - -encode_bytestreams_attr_dstaddr(undefined, _acc) -> - _acc; -encode_bytestreams_attr_dstaddr(_val, _acc) -> - [{<<"dstaddr">>, _val} | _acc]. - -decode_bytestreams_attr_sid(__TopXMLNS, undefined) -> - undefined; -decode_bytestreams_attr_sid(__TopXMLNS, _val) -> _val. - -encode_bytestreams_attr_sid(undefined, _acc) -> _acc; -encode_bytestreams_attr_sid(_val, _acc) -> - [{<<"sid">>, _val} | _acc]. - -decode_bytestreams_attr_mode(__TopXMLNS, undefined) -> - tcp; -decode_bytestreams_attr_mode(__TopXMLNS, _val) -> - case catch dec_enum(_val, [tcp, udp]) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"mode">>, <<"query">>, __TopXMLNS}}); - _res -> _res - end. - -encode_bytestreams_attr_mode(tcp, _acc) -> _acc; -encode_bytestreams_attr_mode(_val, _acc) -> - [{<<"mode">>, enc_enum(_val)} | _acc]. - -decode_bytestreams_activate(__TopXMLNS, __IgnoreEls, - {xmlel, <<"activate">>, _attrs, _els}) -> - Cdata = decode_bytestreams_activate_els(__TopXMLNS, - __IgnoreEls, _els, <<>>), - Cdata. - -decode_bytestreams_activate_els(__TopXMLNS, __IgnoreEls, - [], Cdata) -> - decode_bytestreams_activate_cdata(__TopXMLNS, Cdata); -decode_bytestreams_activate_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_bytestreams_activate_els(__TopXMLNS, __IgnoreEls, - _els, <<Cdata/binary, _data/binary>>); -decode_bytestreams_activate_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_bytestreams_activate_els(__TopXMLNS, __IgnoreEls, - _els, Cdata). - -encode_bytestreams_activate(Cdata, _xmlns_attrs) -> - _els = encode_bytestreams_activate_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"activate">>, _attrs, _els}. - -decode_bytestreams_activate_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_bytestreams_activate_cdata(__TopXMLNS, _val) -> - case catch dec_jid(_val) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_cdata_value, <<>>, <<"activate">>, __TopXMLNS}}); - _res -> _res - end. - -encode_bytestreams_activate_cdata(undefined, _acc) -> - _acc; -encode_bytestreams_activate_cdata(_val, _acc) -> - [{xmlcdata, enc_jid(_val)} | _acc]. - -decode_bytestreams_streamhost_used(__TopXMLNS, - __IgnoreEls, - {xmlel, <<"streamhost-used">>, _attrs, - _els}) -> - Jid = - decode_bytestreams_streamhost_used_attrs(__TopXMLNS, - _attrs, undefined), - Jid. - -decode_bytestreams_streamhost_used_attrs(__TopXMLNS, - [{<<"jid">>, _val} | _attrs], _Jid) -> - decode_bytestreams_streamhost_used_attrs(__TopXMLNS, - _attrs, _val); -decode_bytestreams_streamhost_used_attrs(__TopXMLNS, - [_ | _attrs], Jid) -> - decode_bytestreams_streamhost_used_attrs(__TopXMLNS, - _attrs, Jid); -decode_bytestreams_streamhost_used_attrs(__TopXMLNS, [], - Jid) -> - decode_bytestreams_streamhost_used_attr_jid(__TopXMLNS, - Jid). - -encode_bytestreams_streamhost_used(Jid, _xmlns_attrs) -> - _els = [], - _attrs = - encode_bytestreams_streamhost_used_attr_jid(Jid, - _xmlns_attrs), - {xmlel, <<"streamhost-used">>, _attrs, _els}. - -decode_bytestreams_streamhost_used_attr_jid(__TopXMLNS, - undefined) -> - erlang:error({xmpp_codec, - {missing_attr, <<"jid">>, <<"streamhost-used">>, - __TopXMLNS}}); -decode_bytestreams_streamhost_used_attr_jid(__TopXMLNS, - _val) -> - case catch dec_jid(_val) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"jid">>, <<"streamhost-used">>, - __TopXMLNS}}); - _res -> _res - end. - -encode_bytestreams_streamhost_used_attr_jid(_val, - _acc) -> - [{<<"jid">>, enc_jid(_val)} | _acc]. - -decode_bytestreams_streamhost(__TopXMLNS, __IgnoreEls, - {xmlel, <<"streamhost">>, _attrs, _els}) -> - {Jid, Host, Port} = - decode_bytestreams_streamhost_attrs(__TopXMLNS, _attrs, - undefined, undefined, undefined), - {streamhost, Jid, Host, Port}. - -decode_bytestreams_streamhost_attrs(__TopXMLNS, - [{<<"jid">>, _val} | _attrs], _Jid, Host, - Port) -> - decode_bytestreams_streamhost_attrs(__TopXMLNS, _attrs, - _val, Host, Port); -decode_bytestreams_streamhost_attrs(__TopXMLNS, - [{<<"host">>, _val} | _attrs], Jid, _Host, - Port) -> - decode_bytestreams_streamhost_attrs(__TopXMLNS, _attrs, - Jid, _val, Port); -decode_bytestreams_streamhost_attrs(__TopXMLNS, - [{<<"port">>, _val} | _attrs], Jid, Host, - _Port) -> - decode_bytestreams_streamhost_attrs(__TopXMLNS, _attrs, - Jid, Host, _val); -decode_bytestreams_streamhost_attrs(__TopXMLNS, - [_ | _attrs], Jid, Host, Port) -> - decode_bytestreams_streamhost_attrs(__TopXMLNS, _attrs, - Jid, Host, Port); -decode_bytestreams_streamhost_attrs(__TopXMLNS, [], Jid, - Host, Port) -> - {decode_bytestreams_streamhost_attr_jid(__TopXMLNS, - Jid), - decode_bytestreams_streamhost_attr_host(__TopXMLNS, - Host), - decode_bytestreams_streamhost_attr_port(__TopXMLNS, - Port)}. - -encode_bytestreams_streamhost({streamhost, Jid, Host, - Port}, - _xmlns_attrs) -> - _els = [], - _attrs = encode_bytestreams_streamhost_attr_port(Port, - encode_bytestreams_streamhost_attr_host(Host, - encode_bytestreams_streamhost_attr_jid(Jid, - _xmlns_attrs))), - {xmlel, <<"streamhost">>, _attrs, _els}. - -decode_bytestreams_streamhost_attr_jid(__TopXMLNS, - undefined) -> - erlang:error({xmpp_codec, - {missing_attr, <<"jid">>, <<"streamhost">>, - __TopXMLNS}}); -decode_bytestreams_streamhost_attr_jid(__TopXMLNS, - _val) -> - case catch dec_jid(_val) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"jid">>, <<"streamhost">>, - __TopXMLNS}}); - _res -> _res - end. - -encode_bytestreams_streamhost_attr_jid(_val, _acc) -> - [{<<"jid">>, enc_jid(_val)} | _acc]. - -decode_bytestreams_streamhost_attr_host(__TopXMLNS, - undefined) -> - erlang:error({xmpp_codec, - {missing_attr, <<"host">>, <<"streamhost">>, - __TopXMLNS}}); -decode_bytestreams_streamhost_attr_host(__TopXMLNS, - _val) -> - _val. - -encode_bytestreams_streamhost_attr_host(_val, _acc) -> - [{<<"host">>, _val} | _acc]. - -decode_bytestreams_streamhost_attr_port(__TopXMLNS, - undefined) -> - 1080; -decode_bytestreams_streamhost_attr_port(__TopXMLNS, - _val) -> - case catch dec_int(_val, 0, 65535) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"port">>, <<"streamhost">>, - __TopXMLNS}}); - _res -> _res - end. - -encode_bytestreams_streamhost_attr_port(1080, _acc) -> - _acc; -encode_bytestreams_streamhost_attr_port(_val, _acc) -> - [{<<"port">>, enc_int(_val)} | _acc]. - -decode_legacy_delay(__TopXMLNS, __IgnoreEls, - {xmlel, <<"x">>, _attrs, _els}) -> - {Stamp, From} = decode_legacy_delay_attrs(__TopXMLNS, - _attrs, undefined, undefined), - {legacy_delay, Stamp, From}. - -decode_legacy_delay_attrs(__TopXMLNS, - [{<<"stamp">>, _val} | _attrs], _Stamp, From) -> - decode_legacy_delay_attrs(__TopXMLNS, _attrs, _val, - From); -decode_legacy_delay_attrs(__TopXMLNS, - [{<<"from">>, _val} | _attrs], Stamp, _From) -> - decode_legacy_delay_attrs(__TopXMLNS, _attrs, Stamp, - _val); -decode_legacy_delay_attrs(__TopXMLNS, [_ | _attrs], - Stamp, From) -> - decode_legacy_delay_attrs(__TopXMLNS, _attrs, Stamp, - From); -decode_legacy_delay_attrs(__TopXMLNS, [], Stamp, - From) -> - {decode_legacy_delay_attr_stamp(__TopXMLNS, Stamp), - decode_legacy_delay_attr_from(__TopXMLNS, From)}. - -encode_legacy_delay({legacy_delay, Stamp, From}, - _xmlns_attrs) -> - _els = [], - _attrs = encode_legacy_delay_attr_from(From, - encode_legacy_delay_attr_stamp(Stamp, - _xmlns_attrs)), - {xmlel, <<"x">>, _attrs, _els}. - -decode_legacy_delay_attr_stamp(__TopXMLNS, undefined) -> - erlang:error({xmpp_codec, - {missing_attr, <<"stamp">>, <<"x">>, __TopXMLNS}}); -decode_legacy_delay_attr_stamp(__TopXMLNS, _val) -> - _val. - -encode_legacy_delay_attr_stamp(_val, _acc) -> - [{<<"stamp">>, _val} | _acc]. - -decode_legacy_delay_attr_from(__TopXMLNS, undefined) -> - undefined; -decode_legacy_delay_attr_from(__TopXMLNS, _val) -> - case catch dec_jid(_val) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"from">>, <<"x">>, __TopXMLNS}}); - _res -> _res - end. - -encode_legacy_delay_attr_from(undefined, _acc) -> _acc; -encode_legacy_delay_attr_from(_val, _acc) -> - [{<<"from">>, enc_jid(_val)} | _acc]. - -decode_delay(__TopXMLNS, __IgnoreEls, - {xmlel, <<"delay">>, _attrs, _els}) -> - {Stamp, From} = decode_delay_attrs(__TopXMLNS, _attrs, - undefined, undefined), - {delay, Stamp, From}. - -decode_delay_attrs(__TopXMLNS, - [{<<"stamp">>, _val} | _attrs], _Stamp, From) -> - decode_delay_attrs(__TopXMLNS, _attrs, _val, From); -decode_delay_attrs(__TopXMLNS, - [{<<"from">>, _val} | _attrs], Stamp, _From) -> - decode_delay_attrs(__TopXMLNS, _attrs, Stamp, _val); -decode_delay_attrs(__TopXMLNS, [_ | _attrs], Stamp, - From) -> - decode_delay_attrs(__TopXMLNS, _attrs, Stamp, From); -decode_delay_attrs(__TopXMLNS, [], Stamp, From) -> - {decode_delay_attr_stamp(__TopXMLNS, Stamp), - decode_delay_attr_from(__TopXMLNS, From)}. - -encode_delay({delay, Stamp, From}, _xmlns_attrs) -> - _els = [], - _attrs = encode_delay_attr_from(From, - encode_delay_attr_stamp(Stamp, - _xmlns_attrs)), - {xmlel, <<"delay">>, _attrs, _els}. - -decode_delay_attr_stamp(__TopXMLNS, undefined) -> - erlang:error({xmpp_codec, - {missing_attr, <<"stamp">>, <<"delay">>, __TopXMLNS}}); -decode_delay_attr_stamp(__TopXMLNS, _val) -> - case catch dec_utc(_val) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"stamp">>, <<"delay">>, - __TopXMLNS}}); - _res -> _res - end. - -encode_delay_attr_stamp(_val, _acc) -> - [{<<"stamp">>, enc_utc(_val)} | _acc]. - -decode_delay_attr_from(__TopXMLNS, undefined) -> - undefined; -decode_delay_attr_from(__TopXMLNS, _val) -> - case catch dec_jid(_val) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"from">>, <<"delay">>, __TopXMLNS}}); - _res -> _res - end. - -encode_delay_attr_from(undefined, _acc) -> _acc; -encode_delay_attr_from(_val, _acc) -> - [{<<"from">>, enc_jid(_val)} | _acc]. - -decode_chatstate_paused(__TopXMLNS, __IgnoreEls, - {xmlel, <<"paused">>, _attrs, _els}) -> - {chatstate, paused}. - -encode_chatstate_paused({chatstate, paused}, - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"paused">>, _attrs, _els}. - -decode_chatstate_inactive(__TopXMLNS, __IgnoreEls, - {xmlel, <<"inactive">>, _attrs, _els}) -> - {chatstate, inactive}. - -encode_chatstate_inactive({chatstate, inactive}, - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"inactive">>, _attrs, _els}. - -decode_chatstate_gone(__TopXMLNS, __IgnoreEls, - {xmlel, <<"gone">>, _attrs, _els}) -> - {chatstate, gone}. - -encode_chatstate_gone({chatstate, gone}, - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"gone">>, _attrs, _els}. - -decode_chatstate_composing(__TopXMLNS, __IgnoreEls, - {xmlel, <<"composing">>, _attrs, _els}) -> - {chatstate, composing}. - -encode_chatstate_composing({chatstate, composing}, - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"composing">>, _attrs, _els}. - -decode_chatstate_active(__TopXMLNS, __IgnoreEls, - {xmlel, <<"active">>, _attrs, _els}) -> - {chatstate, active}. - -encode_chatstate_active({chatstate, active}, - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"active">>, _attrs, _els}. - -decode_shim_headers(__TopXMLNS, __IgnoreEls, - {xmlel, <<"headers">>, _attrs, _els}) -> - Headers = decode_shim_headers_els(__TopXMLNS, - __IgnoreEls, _els, []), - {shim, Headers}. - -decode_shim_headers_els(__TopXMLNS, __IgnoreEls, [], - Headers) -> - lists:reverse(Headers); -decode_shim_headers_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"header">>, _attrs, _} = _el | _els], - Headers) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_shim_headers_els(__TopXMLNS, __IgnoreEls, _els, - [decode_shim_header(__TopXMLNS, __IgnoreEls, - _el) - | Headers]); - true -> - decode_shim_headers_els(__TopXMLNS, __IgnoreEls, _els, - Headers) - end; -decode_shim_headers_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Headers) -> - decode_shim_headers_els(__TopXMLNS, __IgnoreEls, _els, - Headers). - -encode_shim_headers({shim, Headers}, _xmlns_attrs) -> - _els = - lists:reverse('encode_shim_headers_$headers'(Headers, - [])), - _attrs = _xmlns_attrs, - {xmlel, <<"headers">>, _attrs, _els}. - -'encode_shim_headers_$headers'([], _acc) -> _acc; -'encode_shim_headers_$headers'([Headers | _els], - _acc) -> - 'encode_shim_headers_$headers'(_els, - [encode_shim_header(Headers, []) | _acc]). - -decode_shim_header(__TopXMLNS, __IgnoreEls, - {xmlel, <<"header">>, _attrs, _els}) -> - Cdata = decode_shim_header_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - Name = decode_shim_header_attrs(__TopXMLNS, _attrs, - undefined), - {Name, Cdata}. - -decode_shim_header_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_shim_header_cdata(__TopXMLNS, Cdata); -decode_shim_header_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_shim_header_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_shim_header_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_shim_header_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -decode_shim_header_attrs(__TopXMLNS, - [{<<"name">>, _val} | _attrs], _Name) -> - decode_shim_header_attrs(__TopXMLNS, _attrs, _val); -decode_shim_header_attrs(__TopXMLNS, [_ | _attrs], - Name) -> - decode_shim_header_attrs(__TopXMLNS, _attrs, Name); -decode_shim_header_attrs(__TopXMLNS, [], Name) -> - decode_shim_header_attr_name(__TopXMLNS, Name). - -encode_shim_header({Name, Cdata}, _xmlns_attrs) -> - _els = encode_shim_header_cdata(Cdata, []), - _attrs = encode_shim_header_attr_name(Name, - _xmlns_attrs), - {xmlel, <<"header">>, _attrs, _els}. - -decode_shim_header_attr_name(__TopXMLNS, undefined) -> - erlang:error({xmpp_codec, - {missing_attr, <<"name">>, <<"header">>, __TopXMLNS}}); -decode_shim_header_attr_name(__TopXMLNS, _val) -> _val. - -encode_shim_header_attr_name(_val, _acc) -> - [{<<"name">>, _val} | _acc]. - -decode_shim_header_cdata(__TopXMLNS, <<>>) -> undefined; -decode_shim_header_cdata(__TopXMLNS, _val) -> _val. - -encode_shim_header_cdata(undefined, _acc) -> _acc; -encode_shim_header_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_pubsub(__TopXMLNS, __IgnoreEls, - {xmlel, <<"pubsub">>, _attrs, _els}) -> - {Items, Options, Affiliations, Subscriptions, Retract, - Unsubscribe, Subscribe, Publish} = - decode_pubsub_els(__TopXMLNS, __IgnoreEls, _els, - undefined, undefined, undefined, undefined, undefined, - undefined, undefined, undefined), - {pubsub, Subscriptions, Affiliations, Publish, - Subscribe, Unsubscribe, Options, Items, Retract}. - -decode_pubsub_els(__TopXMLNS, __IgnoreEls, [], Items, - Options, Affiliations, Subscriptions, Retract, - Unsubscribe, Subscribe, Publish) -> - {Items, Options, Affiliations, Subscriptions, Retract, - Unsubscribe, Subscribe, Publish}; -decode_pubsub_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"subscriptions">>, _attrs, _} = _el | _els], - Items, Options, Affiliations, Subscriptions, Retract, - Unsubscribe, Subscribe, Publish) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_pubsub_els(__TopXMLNS, __IgnoreEls, _els, Items, - Options, Affiliations, - decode_pubsub_subscriptions(__TopXMLNS, - __IgnoreEls, _el), - Retract, Unsubscribe, Subscribe, Publish); - true -> - decode_pubsub_els(__TopXMLNS, __IgnoreEls, _els, Items, - Options, Affiliations, Subscriptions, Retract, - Unsubscribe, Subscribe, Publish) - end; -decode_pubsub_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"affiliations">>, _attrs, _} = _el | _els], - Items, Options, Affiliations, Subscriptions, Retract, - Unsubscribe, Subscribe, Publish) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_pubsub_els(__TopXMLNS, __IgnoreEls, _els, Items, - Options, - decode_pubsub_affiliations(__TopXMLNS, __IgnoreEls, - _el), - Subscriptions, Retract, Unsubscribe, Subscribe, - Publish); - true -> - decode_pubsub_els(__TopXMLNS, __IgnoreEls, _els, Items, - Options, Affiliations, Subscriptions, Retract, - Unsubscribe, Subscribe, Publish) - end; -decode_pubsub_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"subscribe">>, _attrs, _} = _el | _els], - Items, Options, Affiliations, Subscriptions, Retract, - Unsubscribe, Subscribe, Publish) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_pubsub_els(__TopXMLNS, __IgnoreEls, _els, Items, - Options, Affiliations, Subscriptions, Retract, - Unsubscribe, - decode_pubsub_subscribe(__TopXMLNS, __IgnoreEls, - _el), - Publish); - true -> - decode_pubsub_els(__TopXMLNS, __IgnoreEls, _els, Items, - Options, Affiliations, Subscriptions, Retract, - Unsubscribe, Subscribe, Publish) - end; -decode_pubsub_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"unsubscribe">>, _attrs, _} = _el | _els], - Items, Options, Affiliations, Subscriptions, Retract, - Unsubscribe, Subscribe, Publish) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_pubsub_els(__TopXMLNS, __IgnoreEls, _els, Items, - Options, Affiliations, Subscriptions, Retract, - decode_pubsub_unsubscribe(__TopXMLNS, __IgnoreEls, - _el), - Subscribe, Publish); - true -> - decode_pubsub_els(__TopXMLNS, __IgnoreEls, _els, Items, - Options, Affiliations, Subscriptions, Retract, - Unsubscribe, Subscribe, Publish) - end; -decode_pubsub_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"options">>, _attrs, _} = _el | _els], Items, - Options, Affiliations, Subscriptions, Retract, - Unsubscribe, Subscribe, Publish) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_pubsub_els(__TopXMLNS, __IgnoreEls, _els, Items, - decode_pubsub_options(__TopXMLNS, __IgnoreEls, - _el), - Affiliations, Subscriptions, Retract, Unsubscribe, - Subscribe, Publish); - true -> - decode_pubsub_els(__TopXMLNS, __IgnoreEls, _els, Items, - Options, Affiliations, Subscriptions, Retract, - Unsubscribe, Subscribe, Publish) - end; -decode_pubsub_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"items">>, _attrs, _} = _el | _els], Items, - Options, Affiliations, Subscriptions, Retract, - Unsubscribe, Subscribe, Publish) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_pubsub_els(__TopXMLNS, __IgnoreEls, _els, - decode_pubsub_items(__TopXMLNS, __IgnoreEls, _el), - Options, Affiliations, Subscriptions, Retract, - Unsubscribe, Subscribe, Publish); - true -> - decode_pubsub_els(__TopXMLNS, __IgnoreEls, _els, Items, - Options, Affiliations, Subscriptions, Retract, - Unsubscribe, Subscribe, Publish) - end; -decode_pubsub_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"retract">>, _attrs, _} = _el | _els], Items, - Options, Affiliations, Subscriptions, Retract, - Unsubscribe, Subscribe, Publish) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_pubsub_els(__TopXMLNS, __IgnoreEls, _els, Items, - Options, Affiliations, Subscriptions, - decode_pubsub_retract(__TopXMLNS, __IgnoreEls, - _el), - Unsubscribe, Subscribe, Publish); - true -> - decode_pubsub_els(__TopXMLNS, __IgnoreEls, _els, Items, - Options, Affiliations, Subscriptions, Retract, - Unsubscribe, Subscribe, Publish) - end; -decode_pubsub_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"publish">>, _attrs, _} = _el | _els], Items, - Options, Affiliations, Subscriptions, Retract, - Unsubscribe, Subscribe, Publish) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_pubsub_els(__TopXMLNS, __IgnoreEls, _els, Items, - Options, Affiliations, Subscriptions, Retract, - Unsubscribe, Subscribe, - decode_pubsub_publish(__TopXMLNS, __IgnoreEls, - _el)); - true -> - decode_pubsub_els(__TopXMLNS, __IgnoreEls, _els, Items, - Options, Affiliations, Subscriptions, Retract, - Unsubscribe, Subscribe, Publish) - end; -decode_pubsub_els(__TopXMLNS, __IgnoreEls, [_ | _els], - Items, Options, Affiliations, Subscriptions, Retract, - Unsubscribe, Subscribe, Publish) -> - decode_pubsub_els(__TopXMLNS, __IgnoreEls, _els, Items, - Options, Affiliations, Subscriptions, Retract, - Unsubscribe, Subscribe, Publish). - -encode_pubsub({pubsub, Subscriptions, Affiliations, - Publish, Subscribe, Unsubscribe, Options, Items, - Retract}, - _xmlns_attrs) -> - _els = lists:reverse('encode_pubsub_$items'(Items, - 'encode_pubsub_$options'(Options, - 'encode_pubsub_$affiliations'(Affiliations, - 'encode_pubsub_$subscriptions'(Subscriptions, - 'encode_pubsub_$retract'(Retract, - 'encode_pubsub_$unsubscribe'(Unsubscribe, - 'encode_pubsub_$subscribe'(Subscribe, - 'encode_pubsub_$publish'(Publish, - []))))))))), - _attrs = _xmlns_attrs, - {xmlel, <<"pubsub">>, _attrs, _els}. - -'encode_pubsub_$items'(undefined, _acc) -> _acc; -'encode_pubsub_$items'(Items, _acc) -> - [encode_pubsub_items(Items, []) | _acc]. - -'encode_pubsub_$options'(undefined, _acc) -> _acc; -'encode_pubsub_$options'(Options, _acc) -> - [encode_pubsub_options(Options, []) | _acc]. - -'encode_pubsub_$affiliations'(undefined, _acc) -> _acc; -'encode_pubsub_$affiliations'(Affiliations, _acc) -> - [encode_pubsub_affiliations(Affiliations, []) | _acc]. - -'encode_pubsub_$subscriptions'(undefined, _acc) -> _acc; -'encode_pubsub_$subscriptions'(Subscriptions, _acc) -> - [encode_pubsub_subscriptions(Subscriptions, []) | _acc]. - -'encode_pubsub_$retract'(undefined, _acc) -> _acc; -'encode_pubsub_$retract'(Retract, _acc) -> - [encode_pubsub_retract(Retract, []) | _acc]. - -'encode_pubsub_$unsubscribe'(undefined, _acc) -> _acc; -'encode_pubsub_$unsubscribe'(Unsubscribe, _acc) -> - [encode_pubsub_unsubscribe(Unsubscribe, []) | _acc]. - -'encode_pubsub_$subscribe'(undefined, _acc) -> _acc; -'encode_pubsub_$subscribe'(Subscribe, _acc) -> - [encode_pubsub_subscribe(Subscribe, []) | _acc]. - -'encode_pubsub_$publish'(undefined, _acc) -> _acc; -'encode_pubsub_$publish'(Publish, _acc) -> - [encode_pubsub_publish(Publish, []) | _acc]. - -decode_pubsub_retract(__TopXMLNS, __IgnoreEls, - {xmlel, <<"retract">>, _attrs, _els}) -> - Items = decode_pubsub_retract_els(__TopXMLNS, - __IgnoreEls, _els, []), - {Node, Notify} = decode_pubsub_retract_attrs(__TopXMLNS, - _attrs, undefined, undefined), - {pubsub_retract, Node, Notify, Items}. - -decode_pubsub_retract_els(__TopXMLNS, __IgnoreEls, [], - Items) -> - lists:reverse(Items); -decode_pubsub_retract_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"item">>, _attrs, _} = _el | _els], - Items) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_pubsub_retract_els(__TopXMLNS, __IgnoreEls, _els, - [decode_pubsub_item(__TopXMLNS, - __IgnoreEls, _el) - | Items]); - true -> - decode_pubsub_retract_els(__TopXMLNS, __IgnoreEls, _els, - Items) - end; -decode_pubsub_retract_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Items) -> - decode_pubsub_retract_els(__TopXMLNS, __IgnoreEls, _els, - Items). - -decode_pubsub_retract_attrs(__TopXMLNS, - [{<<"node">>, _val} | _attrs], _Node, Notify) -> - decode_pubsub_retract_attrs(__TopXMLNS, _attrs, _val, - Notify); -decode_pubsub_retract_attrs(__TopXMLNS, - [{<<"notify">>, _val} | _attrs], Node, _Notify) -> - decode_pubsub_retract_attrs(__TopXMLNS, _attrs, Node, - _val); -decode_pubsub_retract_attrs(__TopXMLNS, [_ | _attrs], - Node, Notify) -> - decode_pubsub_retract_attrs(__TopXMLNS, _attrs, Node, - Notify); -decode_pubsub_retract_attrs(__TopXMLNS, [], Node, - Notify) -> - {decode_pubsub_retract_attr_node(__TopXMLNS, Node), - decode_pubsub_retract_attr_notify(__TopXMLNS, Notify)}. - -encode_pubsub_retract({pubsub_retract, Node, Notify, - Items}, - _xmlns_attrs) -> - _els = - lists:reverse('encode_pubsub_retract_$items'(Items, - [])), - _attrs = encode_pubsub_retract_attr_notify(Notify, - encode_pubsub_retract_attr_node(Node, - _xmlns_attrs)), - {xmlel, <<"retract">>, _attrs, _els}. - -'encode_pubsub_retract_$items'([], _acc) -> _acc; -'encode_pubsub_retract_$items'([Items | _els], _acc) -> - 'encode_pubsub_retract_$items'(_els, - [encode_pubsub_item(Items, []) | _acc]). - -decode_pubsub_retract_attr_node(__TopXMLNS, - undefined) -> - erlang:error({xmpp_codec, - {missing_attr, <<"node">>, <<"retract">>, __TopXMLNS}}); -decode_pubsub_retract_attr_node(__TopXMLNS, _val) -> - _val. - -encode_pubsub_retract_attr_node(_val, _acc) -> - [{<<"node">>, _val} | _acc]. - -decode_pubsub_retract_attr_notify(__TopXMLNS, - undefined) -> - false; -decode_pubsub_retract_attr_notify(__TopXMLNS, _val) -> - case catch dec_bool(_val) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"notify">>, <<"retract">>, - __TopXMLNS}}); - _res -> _res - end. - -encode_pubsub_retract_attr_notify(false, _acc) -> _acc; -encode_pubsub_retract_attr_notify(_val, _acc) -> - [{<<"notify">>, enc_bool(_val)} | _acc]. - -decode_pubsub_options(__TopXMLNS, __IgnoreEls, - {xmlel, <<"options">>, _attrs, _els}) -> - Xdata = decode_pubsub_options_els(__TopXMLNS, - __IgnoreEls, _els, undefined), - {Node, Subid, Jid} = - decode_pubsub_options_attrs(__TopXMLNS, _attrs, - undefined, undefined, undefined), - {pubsub_options, Node, Jid, Subid, Xdata}. - -decode_pubsub_options_els(__TopXMLNS, __IgnoreEls, [], - Xdata) -> - Xdata; -decode_pubsub_options_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"x">>, _attrs, _} = _el | _els], Xdata) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<"jabber:x:data">> -> - decode_pubsub_options_els(__TopXMLNS, __IgnoreEls, _els, - decode_xdata(_xmlns, __IgnoreEls, _el)); - true -> - decode_pubsub_options_els(__TopXMLNS, __IgnoreEls, _els, - Xdata) - end; -decode_pubsub_options_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Xdata) -> - decode_pubsub_options_els(__TopXMLNS, __IgnoreEls, _els, - Xdata). - -decode_pubsub_options_attrs(__TopXMLNS, - [{<<"node">>, _val} | _attrs], _Node, Subid, Jid) -> - decode_pubsub_options_attrs(__TopXMLNS, _attrs, _val, - Subid, Jid); -decode_pubsub_options_attrs(__TopXMLNS, - [{<<"subid">>, _val} | _attrs], Node, _Subid, - Jid) -> - decode_pubsub_options_attrs(__TopXMLNS, _attrs, Node, - _val, Jid); -decode_pubsub_options_attrs(__TopXMLNS, - [{<<"jid">>, _val} | _attrs], Node, Subid, _Jid) -> - decode_pubsub_options_attrs(__TopXMLNS, _attrs, Node, - Subid, _val); -decode_pubsub_options_attrs(__TopXMLNS, [_ | _attrs], - Node, Subid, Jid) -> - decode_pubsub_options_attrs(__TopXMLNS, _attrs, Node, - Subid, Jid); -decode_pubsub_options_attrs(__TopXMLNS, [], Node, Subid, - Jid) -> - {decode_pubsub_options_attr_node(__TopXMLNS, Node), - decode_pubsub_options_attr_subid(__TopXMLNS, Subid), - decode_pubsub_options_attr_jid(__TopXMLNS, Jid)}. - -encode_pubsub_options({pubsub_options, Node, Jid, Subid, - Xdata}, - _xmlns_attrs) -> - _els = - lists:reverse('encode_pubsub_options_$xdata'(Xdata, - [])), - _attrs = encode_pubsub_options_attr_jid(Jid, - encode_pubsub_options_attr_subid(Subid, - encode_pubsub_options_attr_node(Node, - _xmlns_attrs))), - {xmlel, <<"options">>, _attrs, _els}. - -'encode_pubsub_options_$xdata'(undefined, _acc) -> _acc; -'encode_pubsub_options_$xdata'(Xdata, _acc) -> - [encode_xdata(Xdata, - [{<<"xmlns">>, <<"jabber:x:data">>}]) - | _acc]. - -decode_pubsub_options_attr_node(__TopXMLNS, - undefined) -> - undefined; -decode_pubsub_options_attr_node(__TopXMLNS, _val) -> - _val. - -encode_pubsub_options_attr_node(undefined, _acc) -> - _acc; -encode_pubsub_options_attr_node(_val, _acc) -> - [{<<"node">>, _val} | _acc]. - -decode_pubsub_options_attr_subid(__TopXMLNS, - undefined) -> - undefined; -decode_pubsub_options_attr_subid(__TopXMLNS, _val) -> - _val. - -encode_pubsub_options_attr_subid(undefined, _acc) -> - _acc; -encode_pubsub_options_attr_subid(_val, _acc) -> - [{<<"subid">>, _val} | _acc]. - -decode_pubsub_options_attr_jid(__TopXMLNS, undefined) -> - erlang:error({xmpp_codec, - {missing_attr, <<"jid">>, <<"options">>, __TopXMLNS}}); -decode_pubsub_options_attr_jid(__TopXMLNS, _val) -> - case catch dec_jid(_val) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"jid">>, <<"options">>, - __TopXMLNS}}); - _res -> _res - end. - -encode_pubsub_options_attr_jid(_val, _acc) -> - [{<<"jid">>, enc_jid(_val)} | _acc]. - -decode_pubsub_publish(__TopXMLNS, __IgnoreEls, - {xmlel, <<"publish">>, _attrs, _els}) -> - Items = decode_pubsub_publish_els(__TopXMLNS, - __IgnoreEls, _els, []), - Node = decode_pubsub_publish_attrs(__TopXMLNS, _attrs, - undefined), - {pubsub_publish, Node, Items}. - -decode_pubsub_publish_els(__TopXMLNS, __IgnoreEls, [], - Items) -> - lists:reverse(Items); -decode_pubsub_publish_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"item">>, _attrs, _} = _el | _els], - Items) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_pubsub_publish_els(__TopXMLNS, __IgnoreEls, _els, - [decode_pubsub_item(__TopXMLNS, - __IgnoreEls, _el) - | Items]); - true -> - decode_pubsub_publish_els(__TopXMLNS, __IgnoreEls, _els, - Items) - end; -decode_pubsub_publish_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Items) -> - decode_pubsub_publish_els(__TopXMLNS, __IgnoreEls, _els, - Items). - -decode_pubsub_publish_attrs(__TopXMLNS, - [{<<"node">>, _val} | _attrs], _Node) -> - decode_pubsub_publish_attrs(__TopXMLNS, _attrs, _val); -decode_pubsub_publish_attrs(__TopXMLNS, [_ | _attrs], - Node) -> - decode_pubsub_publish_attrs(__TopXMLNS, _attrs, Node); -decode_pubsub_publish_attrs(__TopXMLNS, [], Node) -> - decode_pubsub_publish_attr_node(__TopXMLNS, Node). - -encode_pubsub_publish({pubsub_publish, Node, Items}, - _xmlns_attrs) -> - _els = - lists:reverse('encode_pubsub_publish_$items'(Items, - [])), - _attrs = encode_pubsub_publish_attr_node(Node, - _xmlns_attrs), - {xmlel, <<"publish">>, _attrs, _els}. - -'encode_pubsub_publish_$items'([], _acc) -> _acc; -'encode_pubsub_publish_$items'([Items | _els], _acc) -> - 'encode_pubsub_publish_$items'(_els, - [encode_pubsub_item(Items, []) | _acc]). - -decode_pubsub_publish_attr_node(__TopXMLNS, - undefined) -> - erlang:error({xmpp_codec, - {missing_attr, <<"node">>, <<"publish">>, __TopXMLNS}}); -decode_pubsub_publish_attr_node(__TopXMLNS, _val) -> - _val. - -encode_pubsub_publish_attr_node(_val, _acc) -> - [{<<"node">>, _val} | _acc]. - -decode_pubsub_unsubscribe(__TopXMLNS, __IgnoreEls, - {xmlel, <<"unsubscribe">>, _attrs, _els}) -> - {Node, Subid, Jid} = - decode_pubsub_unsubscribe_attrs(__TopXMLNS, _attrs, - undefined, undefined, undefined), - {pubsub_unsubscribe, Node, Jid, Subid}. - -decode_pubsub_unsubscribe_attrs(__TopXMLNS, - [{<<"node">>, _val} | _attrs], _Node, Subid, - Jid) -> - decode_pubsub_unsubscribe_attrs(__TopXMLNS, _attrs, - _val, Subid, Jid); -decode_pubsub_unsubscribe_attrs(__TopXMLNS, - [{<<"subid">>, _val} | _attrs], Node, _Subid, - Jid) -> - decode_pubsub_unsubscribe_attrs(__TopXMLNS, _attrs, - Node, _val, Jid); -decode_pubsub_unsubscribe_attrs(__TopXMLNS, - [{<<"jid">>, _val} | _attrs], Node, Subid, - _Jid) -> - decode_pubsub_unsubscribe_attrs(__TopXMLNS, _attrs, - Node, Subid, _val); -decode_pubsub_unsubscribe_attrs(__TopXMLNS, - [_ | _attrs], Node, Subid, Jid) -> - decode_pubsub_unsubscribe_attrs(__TopXMLNS, _attrs, - Node, Subid, Jid); -decode_pubsub_unsubscribe_attrs(__TopXMLNS, [], Node, - Subid, Jid) -> - {decode_pubsub_unsubscribe_attr_node(__TopXMLNS, Node), - decode_pubsub_unsubscribe_attr_subid(__TopXMLNS, Subid), - decode_pubsub_unsubscribe_attr_jid(__TopXMLNS, Jid)}. - -encode_pubsub_unsubscribe({pubsub_unsubscribe, Node, - Jid, Subid}, - _xmlns_attrs) -> - _els = [], - _attrs = encode_pubsub_unsubscribe_attr_jid(Jid, - encode_pubsub_unsubscribe_attr_subid(Subid, - encode_pubsub_unsubscribe_attr_node(Node, - _xmlns_attrs))), - {xmlel, <<"unsubscribe">>, _attrs, _els}. - -decode_pubsub_unsubscribe_attr_node(__TopXMLNS, - undefined) -> - undefined; -decode_pubsub_unsubscribe_attr_node(__TopXMLNS, _val) -> - _val. - -encode_pubsub_unsubscribe_attr_node(undefined, _acc) -> - _acc; -encode_pubsub_unsubscribe_attr_node(_val, _acc) -> - [{<<"node">>, _val} | _acc]. - -decode_pubsub_unsubscribe_attr_subid(__TopXMLNS, - undefined) -> - undefined; -decode_pubsub_unsubscribe_attr_subid(__TopXMLNS, - _val) -> - _val. - -encode_pubsub_unsubscribe_attr_subid(undefined, _acc) -> - _acc; -encode_pubsub_unsubscribe_attr_subid(_val, _acc) -> - [{<<"subid">>, _val} | _acc]. - -decode_pubsub_unsubscribe_attr_jid(__TopXMLNS, - undefined) -> - erlang:error({xmpp_codec, - {missing_attr, <<"jid">>, <<"unsubscribe">>, - __TopXMLNS}}); -decode_pubsub_unsubscribe_attr_jid(__TopXMLNS, _val) -> - case catch dec_jid(_val) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"jid">>, <<"unsubscribe">>, - __TopXMLNS}}); - _res -> _res - end. - -encode_pubsub_unsubscribe_attr_jid(_val, _acc) -> - [{<<"jid">>, enc_jid(_val)} | _acc]. - -decode_pubsub_subscribe(__TopXMLNS, __IgnoreEls, - {xmlel, <<"subscribe">>, _attrs, _els}) -> - {Node, Jid} = decode_pubsub_subscribe_attrs(__TopXMLNS, - _attrs, undefined, undefined), - {pubsub_subscribe, Node, Jid}. - -decode_pubsub_subscribe_attrs(__TopXMLNS, - [{<<"node">>, _val} | _attrs], _Node, Jid) -> - decode_pubsub_subscribe_attrs(__TopXMLNS, _attrs, _val, - Jid); -decode_pubsub_subscribe_attrs(__TopXMLNS, - [{<<"jid">>, _val} | _attrs], Node, _Jid) -> - decode_pubsub_subscribe_attrs(__TopXMLNS, _attrs, Node, - _val); -decode_pubsub_subscribe_attrs(__TopXMLNS, [_ | _attrs], - Node, Jid) -> - decode_pubsub_subscribe_attrs(__TopXMLNS, _attrs, Node, - Jid); -decode_pubsub_subscribe_attrs(__TopXMLNS, [], Node, - Jid) -> - {decode_pubsub_subscribe_attr_node(__TopXMLNS, Node), - decode_pubsub_subscribe_attr_jid(__TopXMLNS, Jid)}. - -encode_pubsub_subscribe({pubsub_subscribe, Node, Jid}, - _xmlns_attrs) -> - _els = [], - _attrs = encode_pubsub_subscribe_attr_jid(Jid, - encode_pubsub_subscribe_attr_node(Node, - _xmlns_attrs)), - {xmlel, <<"subscribe">>, _attrs, _els}. - -decode_pubsub_subscribe_attr_node(__TopXMLNS, - undefined) -> - undefined; -decode_pubsub_subscribe_attr_node(__TopXMLNS, _val) -> - _val. - -encode_pubsub_subscribe_attr_node(undefined, _acc) -> - _acc; -encode_pubsub_subscribe_attr_node(_val, _acc) -> - [{<<"node">>, _val} | _acc]. - -decode_pubsub_subscribe_attr_jid(__TopXMLNS, - undefined) -> - erlang:error({xmpp_codec, - {missing_attr, <<"jid">>, <<"subscribe">>, - __TopXMLNS}}); -decode_pubsub_subscribe_attr_jid(__TopXMLNS, _val) -> - case catch dec_jid(_val) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"jid">>, <<"subscribe">>, - __TopXMLNS}}); - _res -> _res - end. - -encode_pubsub_subscribe_attr_jid(_val, _acc) -> - [{<<"jid">>, enc_jid(_val)} | _acc]. - -decode_pubsub_affiliations(__TopXMLNS, __IgnoreEls, - {xmlel, <<"affiliations">>, _attrs, _els}) -> - Affiliations = - decode_pubsub_affiliations_els(__TopXMLNS, __IgnoreEls, - _els, []), - Affiliations. - -decode_pubsub_affiliations_els(__TopXMLNS, __IgnoreEls, - [], Affiliations) -> - lists:reverse(Affiliations); -decode_pubsub_affiliations_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"affiliation">>, _attrs, _} = _el - | _els], - Affiliations) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_pubsub_affiliations_els(__TopXMLNS, __IgnoreEls, - _els, - [decode_pubsub_affiliation(__TopXMLNS, - __IgnoreEls, - _el) - | Affiliations]); - true -> - decode_pubsub_affiliations_els(__TopXMLNS, __IgnoreEls, - _els, Affiliations) - end; -decode_pubsub_affiliations_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Affiliations) -> - decode_pubsub_affiliations_els(__TopXMLNS, __IgnoreEls, - _els, Affiliations). - -encode_pubsub_affiliations(Affiliations, - _xmlns_attrs) -> - _els = - lists:reverse('encode_pubsub_affiliations_$affiliations'(Affiliations, - [])), - _attrs = _xmlns_attrs, - {xmlel, <<"affiliations">>, _attrs, _els}. - -'encode_pubsub_affiliations_$affiliations'([], _acc) -> - _acc; -'encode_pubsub_affiliations_$affiliations'([Affiliations - | _els], - _acc) -> - 'encode_pubsub_affiliations_$affiliations'(_els, - [encode_pubsub_affiliation(Affiliations, - []) - | _acc]). - -decode_pubsub_subscriptions(__TopXMLNS, __IgnoreEls, - {xmlel, <<"subscriptions">>, _attrs, _els}) -> - Subscriptions = - decode_pubsub_subscriptions_els(__TopXMLNS, __IgnoreEls, - _els, []), - Node = decode_pubsub_subscriptions_attrs(__TopXMLNS, - _attrs, undefined), - {Node, Subscriptions}. - -decode_pubsub_subscriptions_els(__TopXMLNS, __IgnoreEls, - [], Subscriptions) -> - lists:reverse(Subscriptions); -decode_pubsub_subscriptions_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"subscription">>, _attrs, _} = _el - | _els], - Subscriptions) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_pubsub_subscriptions_els(__TopXMLNS, __IgnoreEls, - _els, - [decode_pubsub_subscription(__TopXMLNS, - __IgnoreEls, - _el) - | Subscriptions]); - true -> - decode_pubsub_subscriptions_els(__TopXMLNS, __IgnoreEls, - _els, Subscriptions) - end; -decode_pubsub_subscriptions_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Subscriptions) -> - decode_pubsub_subscriptions_els(__TopXMLNS, __IgnoreEls, - _els, Subscriptions). - -decode_pubsub_subscriptions_attrs(__TopXMLNS, - [{<<"node">>, _val} | _attrs], _Node) -> - decode_pubsub_subscriptions_attrs(__TopXMLNS, _attrs, - _val); -decode_pubsub_subscriptions_attrs(__TopXMLNS, - [_ | _attrs], Node) -> - decode_pubsub_subscriptions_attrs(__TopXMLNS, _attrs, - Node); -decode_pubsub_subscriptions_attrs(__TopXMLNS, [], - Node) -> - decode_pubsub_subscriptions_attr_node(__TopXMLNS, Node). - -encode_pubsub_subscriptions({Node, Subscriptions}, - _xmlns_attrs) -> - _els = - lists:reverse('encode_pubsub_subscriptions_$subscriptions'(Subscriptions, - [])), - _attrs = encode_pubsub_subscriptions_attr_node(Node, - _xmlns_attrs), - {xmlel, <<"subscriptions">>, _attrs, _els}. - -'encode_pubsub_subscriptions_$subscriptions'([], - _acc) -> - _acc; -'encode_pubsub_subscriptions_$subscriptions'([Subscriptions - | _els], - _acc) -> - 'encode_pubsub_subscriptions_$subscriptions'(_els, - [encode_pubsub_subscription(Subscriptions, - []) - | _acc]). - -decode_pubsub_subscriptions_attr_node(__TopXMLNS, - undefined) -> - none; -decode_pubsub_subscriptions_attr_node(__TopXMLNS, - _val) -> - _val. - -encode_pubsub_subscriptions_attr_node(none, _acc) -> - _acc; -encode_pubsub_subscriptions_attr_node(_val, _acc) -> - [{<<"node">>, _val} | _acc]. - -decode_pubsub_event(__TopXMLNS, __IgnoreEls, - {xmlel, <<"event">>, _attrs, _els}) -> - Items = decode_pubsub_event_els(__TopXMLNS, __IgnoreEls, - _els, []), - {pubsub_event, Items}. - -decode_pubsub_event_els(__TopXMLNS, __IgnoreEls, [], - Items) -> - lists:reverse(Items); -decode_pubsub_event_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"items">>, _attrs, _} = _el | _els], - Items) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_pubsub_event_els(__TopXMLNS, __IgnoreEls, _els, - [decode_pubsub_event_items(__TopXMLNS, - __IgnoreEls, _el) - | Items]); - true -> - decode_pubsub_event_els(__TopXMLNS, __IgnoreEls, _els, - Items) - end; -decode_pubsub_event_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Items) -> - decode_pubsub_event_els(__TopXMLNS, __IgnoreEls, _els, - Items). - -encode_pubsub_event({pubsub_event, Items}, - _xmlns_attrs) -> - _els = lists:reverse('encode_pubsub_event_$items'(Items, - [])), - _attrs = _xmlns_attrs, - {xmlel, <<"event">>, _attrs, _els}. - -'encode_pubsub_event_$items'([], _acc) -> _acc; -'encode_pubsub_event_$items'([Items | _els], _acc) -> - 'encode_pubsub_event_$items'(_els, - [encode_pubsub_event_items(Items, []) | _acc]). - -decode_pubsub_event_items(__TopXMLNS, __IgnoreEls, - {xmlel, <<"items">>, _attrs, _els}) -> - {Items, Retract} = - decode_pubsub_event_items_els(__TopXMLNS, __IgnoreEls, - _els, [], []), - Node = decode_pubsub_event_items_attrs(__TopXMLNS, - _attrs, undefined), - {pubsub_event_items, Node, Retract, Items}. - -decode_pubsub_event_items_els(__TopXMLNS, __IgnoreEls, - [], Items, Retract) -> - {lists:reverse(Items), lists:reverse(Retract)}; -decode_pubsub_event_items_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"retract">>, _attrs, _} = _el | _els], - Items, Retract) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_pubsub_event_items_els(__TopXMLNS, __IgnoreEls, - _els, Items, - [decode_pubsub_event_retract(__TopXMLNS, - __IgnoreEls, - _el) - | Retract]); - true -> - decode_pubsub_event_items_els(__TopXMLNS, __IgnoreEls, - _els, Items, Retract) - end; -decode_pubsub_event_items_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"item">>, _attrs, _} = _el | _els], - Items, Retract) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_pubsub_event_items_els(__TopXMLNS, __IgnoreEls, - _els, - [decode_pubsub_event_item(__TopXMLNS, - __IgnoreEls, - _el) - | Items], - Retract); - true -> - decode_pubsub_event_items_els(__TopXMLNS, __IgnoreEls, - _els, Items, Retract) - end; -decode_pubsub_event_items_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Items, Retract) -> - decode_pubsub_event_items_els(__TopXMLNS, __IgnoreEls, - _els, Items, Retract). - -decode_pubsub_event_items_attrs(__TopXMLNS, - [{<<"node">>, _val} | _attrs], _Node) -> - decode_pubsub_event_items_attrs(__TopXMLNS, _attrs, - _val); -decode_pubsub_event_items_attrs(__TopXMLNS, - [_ | _attrs], Node) -> - decode_pubsub_event_items_attrs(__TopXMLNS, _attrs, - Node); -decode_pubsub_event_items_attrs(__TopXMLNS, [], Node) -> - decode_pubsub_event_items_attr_node(__TopXMLNS, Node). - -encode_pubsub_event_items({pubsub_event_items, Node, - Retract, Items}, - _xmlns_attrs) -> - _els = - lists:reverse('encode_pubsub_event_items_$items'(Items, - 'encode_pubsub_event_items_$retract'(Retract, - []))), - _attrs = encode_pubsub_event_items_attr_node(Node, - _xmlns_attrs), - {xmlel, <<"items">>, _attrs, _els}. - -'encode_pubsub_event_items_$items'([], _acc) -> _acc; -'encode_pubsub_event_items_$items'([Items | _els], - _acc) -> - 'encode_pubsub_event_items_$items'(_els, - [encode_pubsub_event_item(Items, []) - | _acc]). - -'encode_pubsub_event_items_$retract'([], _acc) -> _acc; -'encode_pubsub_event_items_$retract'([Retract | _els], - _acc) -> - 'encode_pubsub_event_items_$retract'(_els, - [encode_pubsub_event_retract(Retract, - []) - | _acc]). - -decode_pubsub_event_items_attr_node(__TopXMLNS, - undefined) -> - erlang:error({xmpp_codec, - {missing_attr, <<"node">>, <<"items">>, __TopXMLNS}}); -decode_pubsub_event_items_attr_node(__TopXMLNS, _val) -> - _val. - -encode_pubsub_event_items_attr_node(_val, _acc) -> - [{<<"node">>, _val} | _acc]. - -decode_pubsub_event_item(__TopXMLNS, __IgnoreEls, - {xmlel, <<"item">>, _attrs, _els}) -> - {Id, Node, Publisher} = - decode_pubsub_event_item_attrs(__TopXMLNS, _attrs, - undefined, undefined, undefined), - {pubsub_event_item, Id, Node, Publisher}. - -decode_pubsub_event_item_attrs(__TopXMLNS, - [{<<"id">>, _val} | _attrs], _Id, Node, - Publisher) -> - decode_pubsub_event_item_attrs(__TopXMLNS, _attrs, _val, - Node, Publisher); -decode_pubsub_event_item_attrs(__TopXMLNS, - [{<<"node">>, _val} | _attrs], Id, _Node, - Publisher) -> - decode_pubsub_event_item_attrs(__TopXMLNS, _attrs, Id, - _val, Publisher); -decode_pubsub_event_item_attrs(__TopXMLNS, - [{<<"publisher">>, _val} | _attrs], Id, Node, - _Publisher) -> - decode_pubsub_event_item_attrs(__TopXMLNS, _attrs, Id, - Node, _val); -decode_pubsub_event_item_attrs(__TopXMLNS, [_ | _attrs], - Id, Node, Publisher) -> - decode_pubsub_event_item_attrs(__TopXMLNS, _attrs, Id, - Node, Publisher); -decode_pubsub_event_item_attrs(__TopXMLNS, [], Id, Node, - Publisher) -> - {decode_pubsub_event_item_attr_id(__TopXMLNS, Id), - decode_pubsub_event_item_attr_node(__TopXMLNS, Node), - decode_pubsub_event_item_attr_publisher(__TopXMLNS, - Publisher)}. - -encode_pubsub_event_item({pubsub_event_item, Id, Node, - Publisher}, - _xmlns_attrs) -> - _els = [], - _attrs = - encode_pubsub_event_item_attr_publisher(Publisher, - encode_pubsub_event_item_attr_node(Node, - encode_pubsub_event_item_attr_id(Id, - _xmlns_attrs))), - {xmlel, <<"item">>, _attrs, _els}. - -decode_pubsub_event_item_attr_id(__TopXMLNS, - undefined) -> - undefined; -decode_pubsub_event_item_attr_id(__TopXMLNS, _val) -> - _val. - -encode_pubsub_event_item_attr_id(undefined, _acc) -> - _acc; -encode_pubsub_event_item_attr_id(_val, _acc) -> - [{<<"id">>, _val} | _acc]. - -decode_pubsub_event_item_attr_node(__TopXMLNS, - undefined) -> - undefined; -decode_pubsub_event_item_attr_node(__TopXMLNS, _val) -> - _val. - -encode_pubsub_event_item_attr_node(undefined, _acc) -> - _acc; -encode_pubsub_event_item_attr_node(_val, _acc) -> - [{<<"node">>, _val} | _acc]. - -decode_pubsub_event_item_attr_publisher(__TopXMLNS, - undefined) -> - undefined; -decode_pubsub_event_item_attr_publisher(__TopXMLNS, - _val) -> - _val. - -encode_pubsub_event_item_attr_publisher(undefined, - _acc) -> - _acc; -encode_pubsub_event_item_attr_publisher(_val, _acc) -> - [{<<"publisher">>, _val} | _acc]. - -decode_pubsub_event_retract(__TopXMLNS, __IgnoreEls, - {xmlel, <<"retract">>, _attrs, _els}) -> - Id = decode_pubsub_event_retract_attrs(__TopXMLNS, - _attrs, undefined), - Id. - -decode_pubsub_event_retract_attrs(__TopXMLNS, - [{<<"id">>, _val} | _attrs], _Id) -> - decode_pubsub_event_retract_attrs(__TopXMLNS, _attrs, - _val); -decode_pubsub_event_retract_attrs(__TopXMLNS, - [_ | _attrs], Id) -> - decode_pubsub_event_retract_attrs(__TopXMLNS, _attrs, - Id); -decode_pubsub_event_retract_attrs(__TopXMLNS, [], Id) -> - decode_pubsub_event_retract_attr_id(__TopXMLNS, Id). - -encode_pubsub_event_retract(Id, _xmlns_attrs) -> - _els = [], - _attrs = encode_pubsub_event_retract_attr_id(Id, - _xmlns_attrs), - {xmlel, <<"retract">>, _attrs, _els}. - -decode_pubsub_event_retract_attr_id(__TopXMLNS, - undefined) -> - erlang:error({xmpp_codec, - {missing_attr, <<"id">>, <<"retract">>, __TopXMLNS}}); -decode_pubsub_event_retract_attr_id(__TopXMLNS, _val) -> - _val. - -encode_pubsub_event_retract_attr_id(_val, _acc) -> - [{<<"id">>, _val} | _acc]. - -decode_pubsub_items(__TopXMLNS, __IgnoreEls, - {xmlel, <<"items">>, _attrs, _els}) -> - Items = decode_pubsub_items_els(__TopXMLNS, __IgnoreEls, - _els, []), - {Max_items, Node, Subid} = - decode_pubsub_items_attrs(__TopXMLNS, _attrs, undefined, - undefined, undefined), - {pubsub_items, Node, Max_items, Subid, Items}. - -decode_pubsub_items_els(__TopXMLNS, __IgnoreEls, [], - Items) -> - lists:reverse(Items); -decode_pubsub_items_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"item">>, _attrs, _} = _el | _els], Items) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_pubsub_items_els(__TopXMLNS, __IgnoreEls, _els, - [decode_pubsub_item(__TopXMLNS, __IgnoreEls, - _el) - | Items]); - true -> - decode_pubsub_items_els(__TopXMLNS, __IgnoreEls, _els, - Items) - end; -decode_pubsub_items_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Items) -> - decode_pubsub_items_els(__TopXMLNS, __IgnoreEls, _els, - Items). - -decode_pubsub_items_attrs(__TopXMLNS, - [{<<"max_items">>, _val} | _attrs], _Max_items, Node, - Subid) -> - decode_pubsub_items_attrs(__TopXMLNS, _attrs, _val, - Node, Subid); -decode_pubsub_items_attrs(__TopXMLNS, - [{<<"node">>, _val} | _attrs], Max_items, _Node, - Subid) -> - decode_pubsub_items_attrs(__TopXMLNS, _attrs, Max_items, - _val, Subid); -decode_pubsub_items_attrs(__TopXMLNS, - [{<<"subid">>, _val} | _attrs], Max_items, Node, - _Subid) -> - decode_pubsub_items_attrs(__TopXMLNS, _attrs, Max_items, - Node, _val); -decode_pubsub_items_attrs(__TopXMLNS, [_ | _attrs], - Max_items, Node, Subid) -> - decode_pubsub_items_attrs(__TopXMLNS, _attrs, Max_items, - Node, Subid); -decode_pubsub_items_attrs(__TopXMLNS, [], Max_items, - Node, Subid) -> - {decode_pubsub_items_attr_max_items(__TopXMLNS, - Max_items), - decode_pubsub_items_attr_node(__TopXMLNS, Node), - decode_pubsub_items_attr_subid(__TopXMLNS, Subid)}. - -encode_pubsub_items({pubsub_items, Node, Max_items, - Subid, Items}, - _xmlns_attrs) -> - _els = lists:reverse('encode_pubsub_items_$items'(Items, - [])), - _attrs = encode_pubsub_items_attr_subid(Subid, - encode_pubsub_items_attr_node(Node, - encode_pubsub_items_attr_max_items(Max_items, - _xmlns_attrs))), - {xmlel, <<"items">>, _attrs, _els}. - -'encode_pubsub_items_$items'([], _acc) -> _acc; -'encode_pubsub_items_$items'([Items | _els], _acc) -> - 'encode_pubsub_items_$items'(_els, - [encode_pubsub_item(Items, []) | _acc]). - -decode_pubsub_items_attr_max_items(__TopXMLNS, - undefined) -> - undefined; -decode_pubsub_items_attr_max_items(__TopXMLNS, _val) -> - case catch dec_int(_val, 0, infinity) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"max_items">>, <<"items">>, - __TopXMLNS}}); - _res -> _res - end. - -encode_pubsub_items_attr_max_items(undefined, _acc) -> - _acc; -encode_pubsub_items_attr_max_items(_val, _acc) -> - [{<<"max_items">>, enc_int(_val)} | _acc]. - -decode_pubsub_items_attr_node(__TopXMLNS, undefined) -> - erlang:error({xmpp_codec, - {missing_attr, <<"node">>, <<"items">>, __TopXMLNS}}); -decode_pubsub_items_attr_node(__TopXMLNS, _val) -> _val. - -encode_pubsub_items_attr_node(_val, _acc) -> - [{<<"node">>, _val} | _acc]. - -decode_pubsub_items_attr_subid(__TopXMLNS, undefined) -> - undefined; -decode_pubsub_items_attr_subid(__TopXMLNS, _val) -> - _val. - -encode_pubsub_items_attr_subid(undefined, _acc) -> _acc; -encode_pubsub_items_attr_subid(_val, _acc) -> - [{<<"subid">>, _val} | _acc]. - -decode_pubsub_item(__TopXMLNS, __IgnoreEls, - {xmlel, <<"item">>, _attrs, _els}) -> - __Xmls = decode_pubsub_item_els(__TopXMLNS, __IgnoreEls, - _els, []), - Id = decode_pubsub_item_attrs(__TopXMLNS, _attrs, - undefined), - {pubsub_item, Id, __Xmls}. - -decode_pubsub_item_els(__TopXMLNS, __IgnoreEls, [], - __Xmls) -> - lists:reverse(__Xmls); -decode_pubsub_item_els(__TopXMLNS, __IgnoreEls, - [{xmlel, _, _, _} = _el | _els], __Xmls) -> - decode_pubsub_item_els(__TopXMLNS, __IgnoreEls, _els, - [_el | __Xmls]); -decode_pubsub_item_els(__TopXMLNS, __IgnoreEls, - [_ | _els], __Xmls) -> - decode_pubsub_item_els(__TopXMLNS, __IgnoreEls, _els, - __Xmls). - -decode_pubsub_item_attrs(__TopXMLNS, - [{<<"id">>, _val} | _attrs], _Id) -> - decode_pubsub_item_attrs(__TopXMLNS, _attrs, _val); -decode_pubsub_item_attrs(__TopXMLNS, [_ | _attrs], - Id) -> - decode_pubsub_item_attrs(__TopXMLNS, _attrs, Id); -decode_pubsub_item_attrs(__TopXMLNS, [], Id) -> - decode_pubsub_item_attr_id(__TopXMLNS, Id). - -encode_pubsub_item({pubsub_item, Id, __Xmls}, - _xmlns_attrs) -> - _els = __Xmls, - _attrs = encode_pubsub_item_attr_id(Id, _xmlns_attrs), - {xmlel, <<"item">>, _attrs, _els}. - -decode_pubsub_item_attr_id(__TopXMLNS, undefined) -> - undefined; -decode_pubsub_item_attr_id(__TopXMLNS, _val) -> _val. - -encode_pubsub_item_attr_id(undefined, _acc) -> _acc; -encode_pubsub_item_attr_id(_val, _acc) -> - [{<<"id">>, _val} | _acc]. - -decode_pubsub_affiliation(__TopXMLNS, __IgnoreEls, - {xmlel, <<"affiliation">>, _attrs, _els}) -> - {Node, Type} = - decode_pubsub_affiliation_attrs(__TopXMLNS, _attrs, - undefined, undefined), - {pubsub_affiliation, Node, Type}. - -decode_pubsub_affiliation_attrs(__TopXMLNS, - [{<<"node">>, _val} | _attrs], _Node, Type) -> - decode_pubsub_affiliation_attrs(__TopXMLNS, _attrs, - _val, Type); -decode_pubsub_affiliation_attrs(__TopXMLNS, - [{<<"affiliation">>, _val} | _attrs], Node, - _Type) -> - decode_pubsub_affiliation_attrs(__TopXMLNS, _attrs, - Node, _val); -decode_pubsub_affiliation_attrs(__TopXMLNS, - [_ | _attrs], Node, Type) -> - decode_pubsub_affiliation_attrs(__TopXMLNS, _attrs, - Node, Type); -decode_pubsub_affiliation_attrs(__TopXMLNS, [], Node, - Type) -> - {decode_pubsub_affiliation_attr_node(__TopXMLNS, Node), - decode_pubsub_affiliation_attr_affiliation(__TopXMLNS, - Type)}. - -encode_pubsub_affiliation({pubsub_affiliation, Node, - Type}, - _xmlns_attrs) -> - _els = [], - _attrs = - encode_pubsub_affiliation_attr_affiliation(Type, - encode_pubsub_affiliation_attr_node(Node, - _xmlns_attrs)), - {xmlel, <<"affiliation">>, _attrs, _els}. - -decode_pubsub_affiliation_attr_node(__TopXMLNS, - undefined) -> - erlang:error({xmpp_codec, - {missing_attr, <<"node">>, <<"affiliation">>, - __TopXMLNS}}); -decode_pubsub_affiliation_attr_node(__TopXMLNS, _val) -> - _val. - -encode_pubsub_affiliation_attr_node(_val, _acc) -> - [{<<"node">>, _val} | _acc]. - -decode_pubsub_affiliation_attr_affiliation(__TopXMLNS, - undefined) -> - erlang:error({xmpp_codec, - {missing_attr, <<"affiliation">>, <<"affiliation">>, - __TopXMLNS}}); -decode_pubsub_affiliation_attr_affiliation(__TopXMLNS, - _val) -> - case catch dec_enum(_val, - [member, none, outcast, owner, publisher, - 'publish-only']) - of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"affiliation">>, <<"affiliation">>, - __TopXMLNS}}); - _res -> _res - end. - -encode_pubsub_affiliation_attr_affiliation(_val, - _acc) -> - [{<<"affiliation">>, enc_enum(_val)} | _acc]. - -decode_pubsub_subscription(__TopXMLNS, __IgnoreEls, - {xmlel, <<"subscription">>, _attrs, _els}) -> - {Jid, Node, Subid, Type} = - decode_pubsub_subscription_attrs(__TopXMLNS, _attrs, - undefined, undefined, undefined, - undefined), - {pubsub_subscription, Jid, Node, Subid, Type}. - -decode_pubsub_subscription_attrs(__TopXMLNS, - [{<<"jid">>, _val} | _attrs], _Jid, Node, - Subid, Type) -> - decode_pubsub_subscription_attrs(__TopXMLNS, _attrs, - _val, Node, Subid, Type); -decode_pubsub_subscription_attrs(__TopXMLNS, - [{<<"node">>, _val} | _attrs], Jid, _Node, - Subid, Type) -> - decode_pubsub_subscription_attrs(__TopXMLNS, _attrs, - Jid, _val, Subid, Type); -decode_pubsub_subscription_attrs(__TopXMLNS, - [{<<"subid">>, _val} | _attrs], Jid, Node, - _Subid, Type) -> - decode_pubsub_subscription_attrs(__TopXMLNS, _attrs, - Jid, Node, _val, Type); -decode_pubsub_subscription_attrs(__TopXMLNS, - [{<<"subscription">>, _val} | _attrs], Jid, - Node, Subid, _Type) -> - decode_pubsub_subscription_attrs(__TopXMLNS, _attrs, - Jid, Node, Subid, _val); -decode_pubsub_subscription_attrs(__TopXMLNS, - [_ | _attrs], Jid, Node, Subid, Type) -> - decode_pubsub_subscription_attrs(__TopXMLNS, _attrs, - Jid, Node, Subid, Type); -decode_pubsub_subscription_attrs(__TopXMLNS, [], Jid, - Node, Subid, Type) -> - {decode_pubsub_subscription_attr_jid(__TopXMLNS, Jid), - decode_pubsub_subscription_attr_node(__TopXMLNS, Node), - decode_pubsub_subscription_attr_subid(__TopXMLNS, - Subid), - decode_pubsub_subscription_attr_subscription(__TopXMLNS, - Type)}. - -encode_pubsub_subscription({pubsub_subscription, Jid, - Node, Subid, Type}, - _xmlns_attrs) -> - _els = [], - _attrs = - encode_pubsub_subscription_attr_subscription(Type, - encode_pubsub_subscription_attr_subid(Subid, - encode_pubsub_subscription_attr_node(Node, - encode_pubsub_subscription_attr_jid(Jid, - _xmlns_attrs)))), - {xmlel, <<"subscription">>, _attrs, _els}. - -decode_pubsub_subscription_attr_jid(__TopXMLNS, - undefined) -> - erlang:error({xmpp_codec, - {missing_attr, <<"jid">>, <<"subscription">>, - __TopXMLNS}}); -decode_pubsub_subscription_attr_jid(__TopXMLNS, _val) -> - case catch dec_jid(_val) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"jid">>, <<"subscription">>, - __TopXMLNS}}); - _res -> _res - end. - -encode_pubsub_subscription_attr_jid(_val, _acc) -> - [{<<"jid">>, enc_jid(_val)} | _acc]. - -decode_pubsub_subscription_attr_node(__TopXMLNS, - undefined) -> - undefined; -decode_pubsub_subscription_attr_node(__TopXMLNS, - _val) -> - _val. - -encode_pubsub_subscription_attr_node(undefined, _acc) -> - _acc; -encode_pubsub_subscription_attr_node(_val, _acc) -> - [{<<"node">>, _val} | _acc]. - -decode_pubsub_subscription_attr_subid(__TopXMLNS, - undefined) -> - undefined; -decode_pubsub_subscription_attr_subid(__TopXMLNS, - _val) -> - _val. - -encode_pubsub_subscription_attr_subid(undefined, - _acc) -> - _acc; -encode_pubsub_subscription_attr_subid(_val, _acc) -> - [{<<"subid">>, _val} | _acc]. - -decode_pubsub_subscription_attr_subscription(__TopXMLNS, - undefined) -> - undefined; -decode_pubsub_subscription_attr_subscription(__TopXMLNS, - _val) -> - case catch dec_enum(_val, - [none, pending, subscribed, unconfigured]) - of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"subscription">>, <<"subscription">>, - __TopXMLNS}}); - _res -> _res - end. - -encode_pubsub_subscription_attr_subscription(undefined, - _acc) -> - _acc; -encode_pubsub_subscription_attr_subscription(_val, - _acc) -> - [{<<"subscription">>, enc_enum(_val)} | _acc]. - -decode_xdata(__TopXMLNS, __IgnoreEls, - {xmlel, <<"x">>, _attrs, _els}) -> - {Fields, Items, Instructions, Reported, Title} = - decode_xdata_els(__TopXMLNS, __IgnoreEls, _els, [], [], - [], undefined, undefined), - Type = decode_xdata_attrs(__TopXMLNS, _attrs, - undefined), - {xdata, Type, Instructions, Title, Reported, Items, - Fields}. - -decode_xdata_els(__TopXMLNS, __IgnoreEls, [], Fields, - Items, Instructions, Reported, Title) -> - {lists:reverse(Fields), lists:reverse(Items), - lists:reverse(Instructions), Reported, Title}; -decode_xdata_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"instructions">>, _attrs, _} = _el | _els], - Fields, Items, Instructions, Reported, Title) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_xdata_els(__TopXMLNS, __IgnoreEls, _els, Fields, - Items, - case decode_xdata_instructions(__TopXMLNS, - __IgnoreEls, _el) - of - undefined -> Instructions; - _new_el -> [_new_el | Instructions] - end, - Reported, Title); - true -> - decode_xdata_els(__TopXMLNS, __IgnoreEls, _els, Fields, - Items, Instructions, Reported, Title) - end; -decode_xdata_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"title">>, _attrs, _} = _el | _els], Fields, - Items, Instructions, Reported, Title) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_xdata_els(__TopXMLNS, __IgnoreEls, _els, Fields, - Items, Instructions, Reported, - decode_xdata_title(__TopXMLNS, __IgnoreEls, _el)); - true -> - decode_xdata_els(__TopXMLNS, __IgnoreEls, _els, Fields, - Items, Instructions, Reported, Title) - end; -decode_xdata_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"reported">>, _attrs, _} = _el | _els], - Fields, Items, Instructions, Reported, Title) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_xdata_els(__TopXMLNS, __IgnoreEls, _els, Fields, - Items, Instructions, - decode_xdata_reported(__TopXMLNS, __IgnoreEls, _el), - Title); - true -> - decode_xdata_els(__TopXMLNS, __IgnoreEls, _els, Fields, - Items, Instructions, Reported, Title) - end; -decode_xdata_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"item">>, _attrs, _} = _el | _els], Fields, - Items, Instructions, Reported, Title) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_xdata_els(__TopXMLNS, __IgnoreEls, _els, Fields, - [decode_xdata_item(__TopXMLNS, __IgnoreEls, _el) - | Items], - Instructions, Reported, Title); - true -> - decode_xdata_els(__TopXMLNS, __IgnoreEls, _els, Fields, - Items, Instructions, Reported, Title) - end; -decode_xdata_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"field">>, _attrs, _} = _el | _els], Fields, - Items, Instructions, Reported, Title) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_xdata_els(__TopXMLNS, __IgnoreEls, _els, - [decode_xdata_field(__TopXMLNS, __IgnoreEls, _el) - | Fields], - Items, Instructions, Reported, Title); - true -> - decode_xdata_els(__TopXMLNS, __IgnoreEls, _els, Fields, - Items, Instructions, Reported, Title) - end; -decode_xdata_els(__TopXMLNS, __IgnoreEls, [_ | _els], - Fields, Items, Instructions, Reported, Title) -> - decode_xdata_els(__TopXMLNS, __IgnoreEls, _els, Fields, - Items, Instructions, Reported, Title). - -decode_xdata_attrs(__TopXMLNS, - [{<<"type">>, _val} | _attrs], _Type) -> - decode_xdata_attrs(__TopXMLNS, _attrs, _val); -decode_xdata_attrs(__TopXMLNS, [_ | _attrs], Type) -> - decode_xdata_attrs(__TopXMLNS, _attrs, Type); -decode_xdata_attrs(__TopXMLNS, [], Type) -> - decode_xdata_attr_type(__TopXMLNS, Type). - -encode_xdata({xdata, Type, Instructions, Title, - Reported, Items, Fields}, - _xmlns_attrs) -> - _els = lists:reverse('encode_xdata_$fields'(Fields, - 'encode_xdata_$items'(Items, - 'encode_xdata_$instructions'(Instructions, - 'encode_xdata_$reported'(Reported, - 'encode_xdata_$title'(Title, - [])))))), - _attrs = encode_xdata_attr_type(Type, _xmlns_attrs), - {xmlel, <<"x">>, _attrs, _els}. - -'encode_xdata_$fields'([], _acc) -> _acc; -'encode_xdata_$fields'([Fields | _els], _acc) -> - 'encode_xdata_$fields'(_els, - [encode_xdata_field(Fields, []) | _acc]). - -'encode_xdata_$items'([], _acc) -> _acc; -'encode_xdata_$items'([Items | _els], _acc) -> - 'encode_xdata_$items'(_els, - [encode_xdata_item(Items, []) | _acc]). - -'encode_xdata_$instructions'([], _acc) -> _acc; -'encode_xdata_$instructions'([Instructions | _els], - _acc) -> - 'encode_xdata_$instructions'(_els, - [encode_xdata_instructions(Instructions, []) - | _acc]). - -'encode_xdata_$reported'(undefined, _acc) -> _acc; -'encode_xdata_$reported'(Reported, _acc) -> - [encode_xdata_reported(Reported, []) | _acc]. - -'encode_xdata_$title'(undefined, _acc) -> _acc; -'encode_xdata_$title'(Title, _acc) -> - [encode_xdata_title(Title, []) | _acc]. - -decode_xdata_attr_type(__TopXMLNS, undefined) -> - erlang:error({xmpp_codec, - {missing_attr, <<"type">>, <<"x">>, __TopXMLNS}}); -decode_xdata_attr_type(__TopXMLNS, _val) -> - case catch dec_enum(_val, - [cancel, form, result, submit]) - of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"type">>, <<"x">>, __TopXMLNS}}); - _res -> _res - end. - -encode_xdata_attr_type(_val, _acc) -> - [{<<"type">>, enc_enum(_val)} | _acc]. - -decode_xdata_item(__TopXMLNS, __IgnoreEls, - {xmlel, <<"item">>, _attrs, _els}) -> - Fields = decode_xdata_item_els(__TopXMLNS, __IgnoreEls, - _els, []), - Fields. - -decode_xdata_item_els(__TopXMLNS, __IgnoreEls, [], - Fields) -> - lists:reverse(Fields); -decode_xdata_item_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"field">>, _attrs, _} = _el | _els], - Fields) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_xdata_item_els(__TopXMLNS, __IgnoreEls, _els, - [decode_xdata_field(__TopXMLNS, __IgnoreEls, - _el) - | Fields]); - true -> - decode_xdata_item_els(__TopXMLNS, __IgnoreEls, _els, - Fields) - end; -decode_xdata_item_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Fields) -> - decode_xdata_item_els(__TopXMLNS, __IgnoreEls, _els, - Fields). - -encode_xdata_item(Fields, _xmlns_attrs) -> - _els = lists:reverse('encode_xdata_item_$fields'(Fields, - [])), - _attrs = _xmlns_attrs, - {xmlel, <<"item">>, _attrs, _els}. - -'encode_xdata_item_$fields'([], _acc) -> _acc; -'encode_xdata_item_$fields'([Fields | _els], _acc) -> - 'encode_xdata_item_$fields'(_els, - [encode_xdata_field(Fields, []) | _acc]). - -decode_xdata_reported(__TopXMLNS, __IgnoreEls, - {xmlel, <<"reported">>, _attrs, _els}) -> - Fields = decode_xdata_reported_els(__TopXMLNS, - __IgnoreEls, _els, []), - Fields. - -decode_xdata_reported_els(__TopXMLNS, __IgnoreEls, [], - Fields) -> - lists:reverse(Fields); -decode_xdata_reported_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"field">>, _attrs, _} = _el | _els], - Fields) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_xdata_reported_els(__TopXMLNS, __IgnoreEls, _els, - [decode_xdata_field(__TopXMLNS, - __IgnoreEls, _el) - | Fields]); - true -> - decode_xdata_reported_els(__TopXMLNS, __IgnoreEls, _els, - Fields) - end; -decode_xdata_reported_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Fields) -> - decode_xdata_reported_els(__TopXMLNS, __IgnoreEls, _els, - Fields). - -encode_xdata_reported(Fields, _xmlns_attrs) -> - _els = - lists:reverse('encode_xdata_reported_$fields'(Fields, - [])), - _attrs = _xmlns_attrs, - {xmlel, <<"reported">>, _attrs, _els}. - -'encode_xdata_reported_$fields'([], _acc) -> _acc; -'encode_xdata_reported_$fields'([Fields | _els], - _acc) -> - 'encode_xdata_reported_$fields'(_els, - [encode_xdata_field(Fields, []) | _acc]). - -decode_xdata_title(__TopXMLNS, __IgnoreEls, - {xmlel, <<"title">>, _attrs, _els}) -> - Cdata = decode_xdata_title_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - Cdata. - -decode_xdata_title_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_xdata_title_cdata(__TopXMLNS, Cdata); -decode_xdata_title_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_xdata_title_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_xdata_title_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_xdata_title_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_xdata_title(Cdata, _xmlns_attrs) -> - _els = encode_xdata_title_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"title">>, _attrs, _els}. - -decode_xdata_title_cdata(__TopXMLNS, <<>>) -> undefined; -decode_xdata_title_cdata(__TopXMLNS, _val) -> _val. - -encode_xdata_title_cdata(undefined, _acc) -> _acc; -encode_xdata_title_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_xdata_instructions(__TopXMLNS, __IgnoreEls, - {xmlel, <<"instructions">>, _attrs, _els}) -> - Cdata = decode_xdata_instructions_els(__TopXMLNS, - __IgnoreEls, _els, <<>>), - Cdata. - -decode_xdata_instructions_els(__TopXMLNS, __IgnoreEls, - [], Cdata) -> - decode_xdata_instructions_cdata(__TopXMLNS, Cdata); -decode_xdata_instructions_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_xdata_instructions_els(__TopXMLNS, __IgnoreEls, - _els, <<Cdata/binary, _data/binary>>); -decode_xdata_instructions_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_xdata_instructions_els(__TopXMLNS, __IgnoreEls, - _els, Cdata). - -encode_xdata_instructions(Cdata, _xmlns_attrs) -> - _els = encode_xdata_instructions_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"instructions">>, _attrs, _els}. - -decode_xdata_instructions_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_xdata_instructions_cdata(__TopXMLNS, _val) -> - _val. - -encode_xdata_instructions_cdata(undefined, _acc) -> - _acc; -encode_xdata_instructions_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_xdata_field(__TopXMLNS, __IgnoreEls, - {xmlel, <<"field">>, _attrs, _els}) -> - {Options, Values, Desc, Required} = - decode_xdata_field_els(__TopXMLNS, __IgnoreEls, _els, - [], [], undefined, false), - {Label, Type, Var} = - decode_xdata_field_attrs(__TopXMLNS, _attrs, undefined, - undefined, undefined), - {xdata_field, Label, Type, Var, Required, Desc, Values, - Options}. - -decode_xdata_field_els(__TopXMLNS, __IgnoreEls, [], - Options, Values, Desc, Required) -> - {lists:reverse(Options), lists:reverse(Values), Desc, - Required}; -decode_xdata_field_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"required">>, _attrs, _} = _el | _els], - Options, Values, Desc, Required) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_xdata_field_els(__TopXMLNS, __IgnoreEls, _els, - Options, Values, Desc, - decode_xdata_field_required(__TopXMLNS, - __IgnoreEls, - _el)); - true -> - decode_xdata_field_els(__TopXMLNS, __IgnoreEls, _els, - Options, Values, Desc, Required) - end; -decode_xdata_field_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"desc">>, _attrs, _} = _el | _els], Options, - Values, Desc, Required) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_xdata_field_els(__TopXMLNS, __IgnoreEls, _els, - Options, Values, - decode_xdata_field_desc(__TopXMLNS, - __IgnoreEls, _el), - Required); - true -> - decode_xdata_field_els(__TopXMLNS, __IgnoreEls, _els, - Options, Values, Desc, Required) - end; -decode_xdata_field_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"value">>, _attrs, _} = _el | _els], Options, - Values, Desc, Required) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_xdata_field_els(__TopXMLNS, __IgnoreEls, _els, - Options, - case decode_xdata_field_value(__TopXMLNS, - __IgnoreEls, - _el) - of - undefined -> Values; - _new_el -> [_new_el | Values] - end, - Desc, Required); - true -> - decode_xdata_field_els(__TopXMLNS, __IgnoreEls, _els, - Options, Values, Desc, Required) - end; -decode_xdata_field_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"option">>, _attrs, _} = _el | _els], - Options, Values, Desc, Required) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_xdata_field_els(__TopXMLNS, __IgnoreEls, _els, - case decode_xdata_field_option(__TopXMLNS, - __IgnoreEls, - _el) - of - undefined -> Options; - _new_el -> [_new_el | Options] - end, - Values, Desc, Required); - true -> - decode_xdata_field_els(__TopXMLNS, __IgnoreEls, _els, - Options, Values, Desc, Required) - end; -decode_xdata_field_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Options, Values, Desc, Required) -> - decode_xdata_field_els(__TopXMLNS, __IgnoreEls, _els, - Options, Values, Desc, Required). - -decode_xdata_field_attrs(__TopXMLNS, - [{<<"label">>, _val} | _attrs], _Label, Type, Var) -> - decode_xdata_field_attrs(__TopXMLNS, _attrs, _val, Type, - Var); -decode_xdata_field_attrs(__TopXMLNS, - [{<<"type">>, _val} | _attrs], Label, _Type, Var) -> - decode_xdata_field_attrs(__TopXMLNS, _attrs, Label, - _val, Var); -decode_xdata_field_attrs(__TopXMLNS, - [{<<"var">>, _val} | _attrs], Label, Type, _Var) -> - decode_xdata_field_attrs(__TopXMLNS, _attrs, Label, - Type, _val); -decode_xdata_field_attrs(__TopXMLNS, [_ | _attrs], - Label, Type, Var) -> - decode_xdata_field_attrs(__TopXMLNS, _attrs, Label, - Type, Var); -decode_xdata_field_attrs(__TopXMLNS, [], Label, Type, - Var) -> - {decode_xdata_field_attr_label(__TopXMLNS, Label), - decode_xdata_field_attr_type(__TopXMLNS, Type), - decode_xdata_field_attr_var(__TopXMLNS, Var)}. - -encode_xdata_field({xdata_field, Label, Type, Var, - Required, Desc, Values, Options}, - _xmlns_attrs) -> - _els = - lists:reverse('encode_xdata_field_$options'(Options, - 'encode_xdata_field_$values'(Values, - 'encode_xdata_field_$desc'(Desc, - 'encode_xdata_field_$required'(Required, - []))))), - _attrs = encode_xdata_field_attr_var(Var, - encode_xdata_field_attr_type(Type, - encode_xdata_field_attr_label(Label, - _xmlns_attrs))), - {xmlel, <<"field">>, _attrs, _els}. - -'encode_xdata_field_$options'([], _acc) -> _acc; -'encode_xdata_field_$options'([Options | _els], _acc) -> - 'encode_xdata_field_$options'(_els, - [encode_xdata_field_option(Options, []) - | _acc]). - -'encode_xdata_field_$values'([], _acc) -> _acc; -'encode_xdata_field_$values'([Values | _els], _acc) -> - 'encode_xdata_field_$values'(_els, - [encode_xdata_field_value(Values, []) | _acc]). - -'encode_xdata_field_$desc'(undefined, _acc) -> _acc; -'encode_xdata_field_$desc'(Desc, _acc) -> - [encode_xdata_field_desc(Desc, []) | _acc]. - -'encode_xdata_field_$required'(false, _acc) -> _acc; -'encode_xdata_field_$required'(Required, _acc) -> - [encode_xdata_field_required(Required, []) | _acc]. - -decode_xdata_field_attr_label(__TopXMLNS, undefined) -> - undefined; -decode_xdata_field_attr_label(__TopXMLNS, _val) -> _val. - -encode_xdata_field_attr_label(undefined, _acc) -> _acc; -encode_xdata_field_attr_label(_val, _acc) -> - [{<<"label">>, _val} | _acc]. - -decode_xdata_field_attr_type(__TopXMLNS, undefined) -> - undefined; -decode_xdata_field_attr_type(__TopXMLNS, _val) -> - case catch dec_enum(_val, - [boolean, fixed, hidden, 'jid-multi', 'jid-single', - 'list-multi', 'list-single', 'text-multi', - 'text-private', 'text-single']) - of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"type">>, <<"field">>, __TopXMLNS}}); - _res -> _res - end. - -encode_xdata_field_attr_type(undefined, _acc) -> _acc; -encode_xdata_field_attr_type(_val, _acc) -> - [{<<"type">>, enc_enum(_val)} | _acc]. - -decode_xdata_field_attr_var(__TopXMLNS, undefined) -> - undefined; -decode_xdata_field_attr_var(__TopXMLNS, _val) -> _val. - -encode_xdata_field_attr_var(undefined, _acc) -> _acc; -encode_xdata_field_attr_var(_val, _acc) -> - [{<<"var">>, _val} | _acc]. - -decode_xdata_field_option(__TopXMLNS, __IgnoreEls, - {xmlel, <<"option">>, _attrs, _els}) -> - Value = decode_xdata_field_option_els(__TopXMLNS, - __IgnoreEls, _els, error), - Value. - -decode_xdata_field_option_els(__TopXMLNS, __IgnoreEls, - [], Value) -> - case Value of - error -> - erlang:error({xmpp_codec, - {missing_tag, <<"value">>, __TopXMLNS}}); - {value, Value1} -> Value1 - end; -decode_xdata_field_option_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"value">>, _attrs, _} = _el | _els], - Value) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_xdata_field_option_els(__TopXMLNS, __IgnoreEls, - _els, - {value, - decode_xdata_field_value(__TopXMLNS, - __IgnoreEls, - _el)}); - true -> - decode_xdata_field_option_els(__TopXMLNS, __IgnoreEls, - _els, Value) - end; -decode_xdata_field_option_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Value) -> - decode_xdata_field_option_els(__TopXMLNS, __IgnoreEls, - _els, Value). - -encode_xdata_field_option(Value, _xmlns_attrs) -> - _els = - lists:reverse('encode_xdata_field_option_$value'(Value, - [])), - _attrs = _xmlns_attrs, - {xmlel, <<"option">>, _attrs, _els}. - -'encode_xdata_field_option_$value'(Value, _acc) -> - [encode_xdata_field_value(Value, []) | _acc]. - -decode_xdata_field_value(__TopXMLNS, __IgnoreEls, - {xmlel, <<"value">>, _attrs, _els}) -> - Cdata = decode_xdata_field_value_els(__TopXMLNS, - __IgnoreEls, _els, <<>>), - Cdata. - -decode_xdata_field_value_els(__TopXMLNS, __IgnoreEls, - [], Cdata) -> - decode_xdata_field_value_cdata(__TopXMLNS, Cdata); -decode_xdata_field_value_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_xdata_field_value_els(__TopXMLNS, __IgnoreEls, - _els, <<Cdata/binary, _data/binary>>); -decode_xdata_field_value_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_xdata_field_value_els(__TopXMLNS, __IgnoreEls, - _els, Cdata). - -encode_xdata_field_value(Cdata, _xmlns_attrs) -> - _els = encode_xdata_field_value_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"value">>, _attrs, _els}. - -decode_xdata_field_value_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_xdata_field_value_cdata(__TopXMLNS, _val) -> - _val. - -encode_xdata_field_value_cdata(undefined, _acc) -> _acc; -encode_xdata_field_value_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_xdata_field_desc(__TopXMLNS, __IgnoreEls, - {xmlel, <<"desc">>, _attrs, _els}) -> - Cdata = decode_xdata_field_desc_els(__TopXMLNS, - __IgnoreEls, _els, <<>>), - Cdata. - -decode_xdata_field_desc_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_xdata_field_desc_cdata(__TopXMLNS, Cdata); -decode_xdata_field_desc_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_xdata_field_desc_els(__TopXMLNS, __IgnoreEls, - _els, <<Cdata/binary, _data/binary>>); -decode_xdata_field_desc_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_xdata_field_desc_els(__TopXMLNS, __IgnoreEls, - _els, Cdata). - -encode_xdata_field_desc(Cdata, _xmlns_attrs) -> - _els = encode_xdata_field_desc_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"desc">>, _attrs, _els}. - -decode_xdata_field_desc_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_xdata_field_desc_cdata(__TopXMLNS, _val) -> _val. - -encode_xdata_field_desc_cdata(undefined, _acc) -> _acc; -encode_xdata_field_desc_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_xdata_field_required(__TopXMLNS, __IgnoreEls, - {xmlel, <<"required">>, _attrs, _els}) -> - true. - -encode_xdata_field_required(true, _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"required">>, _attrs, _els}. - -decode_vcard_xupdate(__TopXMLNS, __IgnoreEls, - {xmlel, <<"x">>, _attrs, _els}) -> - Photo = decode_vcard_xupdate_els(__TopXMLNS, - __IgnoreEls, _els, undefined), - {vcard_xupdate, Photo}. - -decode_vcard_xupdate_els(__TopXMLNS, __IgnoreEls, [], - Photo) -> - Photo; -decode_vcard_xupdate_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"photo">>, _attrs, _} = _el | _els], - Photo) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_xupdate_els(__TopXMLNS, __IgnoreEls, _els, - decode_vcard_xupdate_photo(__TopXMLNS, - __IgnoreEls, - _el)); - true -> - decode_vcard_xupdate_els(__TopXMLNS, __IgnoreEls, _els, - Photo) - end; -decode_vcard_xupdate_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Photo) -> - decode_vcard_xupdate_els(__TopXMLNS, __IgnoreEls, _els, - Photo). - -encode_vcard_xupdate({vcard_xupdate, Photo}, - _xmlns_attrs) -> - _els = - lists:reverse('encode_vcard_xupdate_$photo'(Photo, [])), - _attrs = _xmlns_attrs, - {xmlel, <<"x">>, _attrs, _els}. - -'encode_vcard_xupdate_$photo'(undefined, _acc) -> _acc; -'encode_vcard_xupdate_$photo'(Photo, _acc) -> - [encode_vcard_xupdate_photo(Photo, []) | _acc]. - -decode_vcard_xupdate_photo(__TopXMLNS, __IgnoreEls, - {xmlel, <<"photo">>, _attrs, _els}) -> - Cdata = decode_vcard_xupdate_photo_els(__TopXMLNS, - __IgnoreEls, _els, <<>>), - Cdata. - -decode_vcard_xupdate_photo_els(__TopXMLNS, __IgnoreEls, - [], Cdata) -> - decode_vcard_xupdate_photo_cdata(__TopXMLNS, Cdata); -decode_vcard_xupdate_photo_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_vcard_xupdate_photo_els(__TopXMLNS, __IgnoreEls, - _els, <<Cdata/binary, _data/binary>>); -decode_vcard_xupdate_photo_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_vcard_xupdate_photo_els(__TopXMLNS, __IgnoreEls, - _els, Cdata). - -encode_vcard_xupdate_photo(Cdata, _xmlns_attrs) -> - _els = encode_vcard_xupdate_photo_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"photo">>, _attrs, _els}. - -decode_vcard_xupdate_photo_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_vcard_xupdate_photo_cdata(__TopXMLNS, _val) -> - _val. - -encode_vcard_xupdate_photo_cdata(undefined, _acc) -> - _acc; -encode_vcard_xupdate_photo_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_vcard(__TopXMLNS, __IgnoreEls, - {xmlel, <<"vCard">>, _attrs, _els}) -> - {Mailer, Adr, Class, Categories, Desc, Uid, Prodid, - Jabberid, Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, Email, Tel, Label, - Fn, Version, N, Photo, Logo, Geo} = - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, - undefined, [], undefined, [], undefined, undefined, - undefined, undefined, undefined, undefined, undefined, - undefined, undefined, undefined, undefined, undefined, - undefined, undefined, undefined, undefined, [], [], [], - undefined, undefined, undefined, undefined, undefined, - undefined), - {vcard, Version, Fn, N, Nickname, Photo, Bday, Adr, - Label, Tel, Email, Jabberid, Mailer, Tz, Geo, Title, - Role, Logo, Org, Categories, Note, Prodid, Rev, - Sort_string, Sound, Uid, Url, Class, Key, Desc}. - -decode_vcard_els(__TopXMLNS, __IgnoreEls, [], Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, Sort_string, - Org, Bday, Key, Tz, Url, Email, Tel, Label, Fn, Version, - N, Photo, Logo, Geo) -> - {Mailer, lists:reverse(Adr), Class, Categories, Desc, - Uid, Prodid, Jabberid, Sound, Note, Role, Title, - Nickname, Rev, Sort_string, Org, Bday, Key, Tz, Url, - lists:reverse(Email), lists:reverse(Tel), - lists:reverse(Label), Fn, Version, N, Photo, Logo, Geo}; -decode_vcard_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"N">>, _attrs, _} = _el | _els], Mailer, Adr, - Class, Categories, Desc, Uid, Prodid, Jabberid, Sound, - Note, Role, Title, Nickname, Rev, Sort_string, Org, - Bday, Key, Tz, Url, Email, Tel, Label, Fn, Version, N, - Photo, Logo, Geo) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, Email, Tel, - Label, Fn, Version, - decode_vcard_N(__TopXMLNS, __IgnoreEls, _el), Photo, - Logo, Geo); - true -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, Email, Tel, - Label, Fn, Version, N, Photo, Logo, Geo) - end; -decode_vcard_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"ADR">>, _attrs, _} = _el | _els], Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, Sort_string, - Org, Bday, Key, Tz, Url, Email, Tel, Label, Fn, Version, - N, Photo, Logo, Geo) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - [decode_vcard_ADR(__TopXMLNS, __IgnoreEls, _el) - | Adr], - Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, Email, Tel, - Label, Fn, Version, N, Photo, Logo, Geo); - true -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, Email, Tel, - Label, Fn, Version, N, Photo, Logo, Geo) - end; -decode_vcard_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"LABEL">>, _attrs, _} = _el | _els], Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, Sort_string, - Org, Bday, Key, Tz, Url, Email, Tel, Label, Fn, Version, - N, Photo, Logo, Geo) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, Email, Tel, - [decode_vcard_LABEL(__TopXMLNS, __IgnoreEls, _el) - | Label], - Fn, Version, N, Photo, Logo, Geo); - true -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, Email, Tel, - Label, Fn, Version, N, Photo, Logo, Geo) - end; -decode_vcard_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"TEL">>, _attrs, _} = _el | _els], Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, Sort_string, - Org, Bday, Key, Tz, Url, Email, Tel, Label, Fn, Version, - N, Photo, Logo, Geo) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, Email, - [decode_vcard_TEL(__TopXMLNS, __IgnoreEls, _el) - | Tel], - Label, Fn, Version, N, Photo, Logo, Geo); - true -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, Email, Tel, - Label, Fn, Version, N, Photo, Logo, Geo) - end; -decode_vcard_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"EMAIL">>, _attrs, _} = _el | _els], Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, Sort_string, - Org, Bday, Key, Tz, Url, Email, Tel, Label, Fn, Version, - N, Photo, Logo, Geo) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, - [decode_vcard_EMAIL(__TopXMLNS, __IgnoreEls, _el) - | Email], - Tel, Label, Fn, Version, N, Photo, Logo, Geo); - true -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, Email, Tel, - Label, Fn, Version, N, Photo, Logo, Geo) - end; -decode_vcard_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"GEO">>, _attrs, _} = _el | _els], Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, Sort_string, - Org, Bday, Key, Tz, Url, Email, Tel, Label, Fn, Version, - N, Photo, Logo, Geo) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, Email, Tel, - Label, Fn, Version, N, Photo, Logo, - decode_vcard_GEO(__TopXMLNS, __IgnoreEls, _el)); - true -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, Email, Tel, - Label, Fn, Version, N, Photo, Logo, Geo) - end; -decode_vcard_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"LOGO">>, _attrs, _} = _el | _els], Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, Sort_string, - Org, Bday, Key, Tz, Url, Email, Tel, Label, Fn, Version, - N, Photo, Logo, Geo) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, Email, Tel, - Label, Fn, Version, N, Photo, - decode_vcard_LOGO(__TopXMLNS, __IgnoreEls, _el), - Geo); - true -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, Email, Tel, - Label, Fn, Version, N, Photo, Logo, Geo) - end; -decode_vcard_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"PHOTO">>, _attrs, _} = _el | _els], Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, Sort_string, - Org, Bday, Key, Tz, Url, Email, Tel, Label, Fn, Version, - N, Photo, Logo, Geo) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, Email, Tel, - Label, Fn, Version, N, - decode_vcard_PHOTO(__TopXMLNS, __IgnoreEls, _el), - Logo, Geo); - true -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, Email, Tel, - Label, Fn, Version, N, Photo, Logo, Geo) - end; -decode_vcard_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"ORG">>, _attrs, _} = _el | _els], Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, Sort_string, - Org, Bday, Key, Tz, Url, Email, Tel, Label, Fn, Version, - N, Photo, Logo, Geo) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, - Sort_string, - decode_vcard_ORG(__TopXMLNS, __IgnoreEls, _el), - Bday, Key, Tz, Url, Email, Tel, Label, Fn, Version, - N, Photo, Logo, Geo); - true -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, Email, Tel, - Label, Fn, Version, N, Photo, Logo, Geo) - end; -decode_vcard_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"SOUND">>, _attrs, _} = _el | _els], Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, Sort_string, - Org, Bday, Key, Tz, Url, Email, Tel, Label, Fn, Version, - N, Photo, Logo, Geo) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - decode_vcard_SOUND(__TopXMLNS, __IgnoreEls, _el), - Note, Role, Title, Nickname, Rev, Sort_string, Org, - Bday, Key, Tz, Url, Email, Tel, Label, Fn, Version, - N, Photo, Logo, Geo); - true -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, Email, Tel, - Label, Fn, Version, N, Photo, Logo, Geo) - end; -decode_vcard_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"KEY">>, _attrs, _} = _el | _els], Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, Sort_string, - Org, Bday, Key, Tz, Url, Email, Tel, Label, Fn, Version, - N, Photo, Logo, Geo) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, - decode_vcard_KEY(__TopXMLNS, __IgnoreEls, _el), Tz, - Url, Email, Tel, Label, Fn, Version, N, Photo, Logo, - Geo); - true -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, Email, Tel, - Label, Fn, Version, N, Photo, Logo, Geo) - end; -decode_vcard_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"VERSION">>, _attrs, _} = _el | _els], - Mailer, Adr, Class, Categories, Desc, Uid, Prodid, - Jabberid, Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, Email, Tel, Label, - Fn, Version, N, Photo, Logo, Geo) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, Email, Tel, - Label, Fn, - decode_vcard_VERSION(__TopXMLNS, __IgnoreEls, _el), - N, Photo, Logo, Geo); - true -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, Email, Tel, - Label, Fn, Version, N, Photo, Logo, Geo) - end; -decode_vcard_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"FN">>, _attrs, _} = _el | _els], Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, Sort_string, - Org, Bday, Key, Tz, Url, Email, Tel, Label, Fn, Version, - N, Photo, Logo, Geo) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, Email, Tel, - Label, - decode_vcard_FN(__TopXMLNS, __IgnoreEls, _el), - Version, N, Photo, Logo, Geo); - true -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, Email, Tel, - Label, Fn, Version, N, Photo, Logo, Geo) - end; -decode_vcard_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"NICKNAME">>, _attrs, _} = _el | _els], - Mailer, Adr, Class, Categories, Desc, Uid, Prodid, - Jabberid, Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, Email, Tel, Label, - Fn, Version, N, Photo, Logo, Geo) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, - decode_vcard_NICKNAME(__TopXMLNS, __IgnoreEls, _el), - Rev, Sort_string, Org, Bday, Key, Tz, Url, Email, - Tel, Label, Fn, Version, N, Photo, Logo, Geo); - true -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, Email, Tel, - Label, Fn, Version, N, Photo, Logo, Geo) - end; -decode_vcard_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"BDAY">>, _attrs, _} = _el | _els], Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, Sort_string, - Org, Bday, Key, Tz, Url, Email, Tel, Label, Fn, Version, - N, Photo, Logo, Geo) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, - decode_vcard_BDAY(__TopXMLNS, __IgnoreEls, _el), - Key, Tz, Url, Email, Tel, Label, Fn, Version, N, - Photo, Logo, Geo); - true -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, Email, Tel, - Label, Fn, Version, N, Photo, Logo, Geo) - end; -decode_vcard_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"JABBERID">>, _attrs, _} = _el | _els], - Mailer, Adr, Class, Categories, Desc, Uid, Prodid, - Jabberid, Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, Email, Tel, Label, - Fn, Version, N, Photo, Logo, Geo) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, - decode_vcard_JABBERID(__TopXMLNS, __IgnoreEls, _el), - Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, Email, Tel, - Label, Fn, Version, N, Photo, Logo, Geo); - true -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, Email, Tel, - Label, Fn, Version, N, Photo, Logo, Geo) - end; -decode_vcard_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"MAILER">>, _attrs, _} = _el | _els], Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, Sort_string, - Org, Bday, Key, Tz, Url, Email, Tel, Label, Fn, Version, - N, Photo, Logo, Geo) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, - decode_vcard_MAILER(__TopXMLNS, __IgnoreEls, _el), - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, Email, Tel, - Label, Fn, Version, N, Photo, Logo, Geo); - true -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, Email, Tel, - Label, Fn, Version, N, Photo, Logo, Geo) - end; -decode_vcard_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"TZ">>, _attrs, _} = _el | _els], Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, Sort_string, - Org, Bday, Key, Tz, Url, Email, Tel, Label, Fn, Version, - N, Photo, Logo, Geo) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, - decode_vcard_TZ(__TopXMLNS, __IgnoreEls, _el), Url, - Email, Tel, Label, Fn, Version, N, Photo, Logo, - Geo); - true -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, Email, Tel, - Label, Fn, Version, N, Photo, Logo, Geo) - end; -decode_vcard_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"TITLE">>, _attrs, _} = _el | _els], Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, Sort_string, - Org, Bday, Key, Tz, Url, Email, Tel, Label, Fn, Version, - N, Photo, Logo, Geo) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, - decode_vcard_TITLE(__TopXMLNS, __IgnoreEls, _el), - Nickname, Rev, Sort_string, Org, Bday, Key, Tz, Url, - Email, Tel, Label, Fn, Version, N, Photo, Logo, - Geo); - true -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, Email, Tel, - Label, Fn, Version, N, Photo, Logo, Geo) - end; -decode_vcard_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"ROLE">>, _attrs, _} = _el | _els], Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, Sort_string, - Org, Bday, Key, Tz, Url, Email, Tel, Label, Fn, Version, - N, Photo, Logo, Geo) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, - decode_vcard_ROLE(__TopXMLNS, __IgnoreEls, _el), - Title, Nickname, Rev, Sort_string, Org, Bday, Key, - Tz, Url, Email, Tel, Label, Fn, Version, N, Photo, - Logo, Geo); - true -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, Email, Tel, - Label, Fn, Version, N, Photo, Logo, Geo) - end; -decode_vcard_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"NOTE">>, _attrs, _} = _el | _els], Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, Sort_string, - Org, Bday, Key, Tz, Url, Email, Tel, Label, Fn, Version, - N, Photo, Logo, Geo) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, - decode_vcard_NOTE(__TopXMLNS, __IgnoreEls, _el), - Role, Title, Nickname, Rev, Sort_string, Org, Bday, - Key, Tz, Url, Email, Tel, Label, Fn, Version, N, - Photo, Logo, Geo); - true -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, Email, Tel, - Label, Fn, Version, N, Photo, Logo, Geo) - end; -decode_vcard_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"PRODID">>, _attrs, _} = _el | _els], Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, Sort_string, - Org, Bday, Key, Tz, Url, Email, Tel, Label, Fn, Version, - N, Photo, Logo, Geo) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, - decode_vcard_PRODID(__TopXMLNS, __IgnoreEls, _el), - Jabberid, Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, Email, Tel, - Label, Fn, Version, N, Photo, Logo, Geo); - true -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, Email, Tel, - Label, Fn, Version, N, Photo, Logo, Geo) - end; -decode_vcard_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"REV">>, _attrs, _} = _el | _els], Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, Sort_string, - Org, Bday, Key, Tz, Url, Email, Tel, Label, Fn, Version, - N, Photo, Logo, Geo) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, - decode_vcard_REV(__TopXMLNS, __IgnoreEls, _el), - Sort_string, Org, Bday, Key, Tz, Url, Email, Tel, - Label, Fn, Version, N, Photo, Logo, Geo); - true -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, Email, Tel, - Label, Fn, Version, N, Photo, Logo, Geo) - end; -decode_vcard_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"SORT-STRING">>, _attrs, _} = _el | _els], - Mailer, Adr, Class, Categories, Desc, Uid, Prodid, - Jabberid, Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, Email, Tel, Label, - Fn, Version, N, Photo, Logo, Geo) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, - decode_vcard_SORT_STRING(__TopXMLNS, __IgnoreEls, - _el), - Org, Bday, Key, Tz, Url, Email, Tel, Label, Fn, - Version, N, Photo, Logo, Geo); - true -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, Email, Tel, - Label, Fn, Version, N, Photo, Logo, Geo) - end; -decode_vcard_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"UID">>, _attrs, _} = _el | _els], Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, Sort_string, - Org, Bday, Key, Tz, Url, Email, Tel, Label, Fn, Version, - N, Photo, Logo, Geo) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, - decode_vcard_UID(__TopXMLNS, __IgnoreEls, _el), - Prodid, Jabberid, Sound, Note, Role, Title, - Nickname, Rev, Sort_string, Org, Bday, Key, Tz, Url, - Email, Tel, Label, Fn, Version, N, Photo, Logo, - Geo); - true -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, Email, Tel, - Label, Fn, Version, N, Photo, Logo, Geo) - end; -decode_vcard_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"URL">>, _attrs, _} = _el | _els], Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, Sort_string, - Org, Bday, Key, Tz, Url, Email, Tel, Label, Fn, Version, - N, Photo, Logo, Geo) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, - decode_vcard_URL(__TopXMLNS, __IgnoreEls, _el), - Email, Tel, Label, Fn, Version, N, Photo, Logo, - Geo); - true -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, Email, Tel, - Label, Fn, Version, N, Photo, Logo, Geo) - end; -decode_vcard_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"DESC">>, _attrs, _} = _el | _els], Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, Sort_string, - Org, Bday, Key, Tz, Url, Email, Tel, Label, Fn, Version, - N, Photo, Logo, Geo) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, - decode_vcard_DESC(__TopXMLNS, __IgnoreEls, _el), - Uid, Prodid, Jabberid, Sound, Note, Role, Title, - Nickname, Rev, Sort_string, Org, Bday, Key, Tz, Url, - Email, Tel, Label, Fn, Version, N, Photo, Logo, - Geo); - true -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, Email, Tel, - Label, Fn, Version, N, Photo, Logo, Geo) - end; -decode_vcard_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"CATEGORIES">>, _attrs, _} = _el | _els], - Mailer, Adr, Class, Categories, Desc, Uid, Prodid, - Jabberid, Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, Email, Tel, Label, - Fn, Version, N, Photo, Logo, Geo) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, - decode_vcard_CATEGORIES(__TopXMLNS, __IgnoreEls, - _el), - Desc, Uid, Prodid, Jabberid, Sound, Note, Role, - Title, Nickname, Rev, Sort_string, Org, Bday, Key, - Tz, Url, Email, Tel, Label, Fn, Version, N, Photo, - Logo, Geo); - true -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, Email, Tel, - Label, Fn, Version, N, Photo, Logo, Geo) - end; -decode_vcard_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"CLASS">>, _attrs, _} = _el | _els], Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, Sort_string, - Org, Bday, Key, Tz, Url, Email, Tel, Label, Fn, Version, - N, Photo, Logo, Geo) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, - decode_vcard_CLASS(__TopXMLNS, __IgnoreEls, _el), - Categories, Desc, Uid, Prodid, Jabberid, Sound, - Note, Role, Title, Nickname, Rev, Sort_string, Org, - Bday, Key, Tz, Url, Email, Tel, Label, Fn, Version, - N, Photo, Logo, Geo); - true -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, Email, Tel, - Label, Fn, Version, N, Photo, Logo, Geo) - end; -decode_vcard_els(__TopXMLNS, __IgnoreEls, [_ | _els], - Mailer, Adr, Class, Categories, Desc, Uid, Prodid, - Jabberid, Sound, Note, Role, Title, Nickname, Rev, - Sort_string, Org, Bday, Key, Tz, Url, Email, Tel, Label, - Fn, Version, N, Photo, Logo, Geo) -> - decode_vcard_els(__TopXMLNS, __IgnoreEls, _els, Mailer, - Adr, Class, Categories, Desc, Uid, Prodid, Jabberid, - Sound, Note, Role, Title, Nickname, Rev, Sort_string, - Org, Bday, Key, Tz, Url, Email, Tel, Label, Fn, Version, - N, Photo, Logo, Geo). - -encode_vcard({vcard, Version, Fn, N, Nickname, Photo, - Bday, Adr, Label, Tel, Email, Jabberid, Mailer, Tz, Geo, - Title, Role, Logo, Org, Categories, Note, Prodid, Rev, - Sort_string, Sound, Uid, Url, Class, Key, Desc}, - _xmlns_attrs) -> - _els = lists:reverse('encode_vcard_$mailer'(Mailer, - 'encode_vcard_$adr'(Adr, - 'encode_vcard_$class'(Class, - 'encode_vcard_$categories'(Categories, - 'encode_vcard_$desc'(Desc, - 'encode_vcard_$uid'(Uid, - 'encode_vcard_$prodid'(Prodid, - 'encode_vcard_$jabberid'(Jabberid, - 'encode_vcard_$sound'(Sound, - 'encode_vcard_$note'(Note, - 'encode_vcard_$role'(Role, - 'encode_vcard_$title'(Title, - 'encode_vcard_$nickname'(Nickname, - 'encode_vcard_$rev'(Rev, - 'encode_vcard_$sort_string'(Sort_string, - 'encode_vcard_$org'(Org, - 'encode_vcard_$bday'(Bday, - 'encode_vcard_$key'(Key, - 'encode_vcard_$tz'(Tz, - 'encode_vcard_$url'(Url, - 'encode_vcard_$email'(Email, - 'encode_vcard_$tel'(Tel, - 'encode_vcard_$label'(Label, - 'encode_vcard_$fn'(Fn, - 'encode_vcard_$version'(Version, - 'encode_vcard_$n'(N, - 'encode_vcard_$photo'(Photo, - 'encode_vcard_$logo'(Logo, - 'encode_vcard_$geo'(Geo, - [])))))))))))))))))))))))))))))), - _attrs = _xmlns_attrs, - {xmlel, <<"vCard">>, _attrs, _els}. - -'encode_vcard_$mailer'(undefined, _acc) -> _acc; -'encode_vcard_$mailer'(Mailer, _acc) -> - [encode_vcard_MAILER(Mailer, []) | _acc]. - -'encode_vcard_$adr'([], _acc) -> _acc; -'encode_vcard_$adr'([Adr | _els], _acc) -> - 'encode_vcard_$adr'(_els, - [encode_vcard_ADR(Adr, []) | _acc]). - -'encode_vcard_$class'(undefined, _acc) -> _acc; -'encode_vcard_$class'(Class, _acc) -> - [encode_vcard_CLASS(Class, []) | _acc]. - -'encode_vcard_$categories'([], _acc) -> _acc; -'encode_vcard_$categories'(Categories, _acc) -> - [encode_vcard_CATEGORIES(Categories, []) | _acc]. - -'encode_vcard_$desc'(undefined, _acc) -> _acc; -'encode_vcard_$desc'(Desc, _acc) -> - [encode_vcard_DESC(Desc, []) | _acc]. - -'encode_vcard_$uid'(undefined, _acc) -> _acc; -'encode_vcard_$uid'(Uid, _acc) -> - [encode_vcard_UID(Uid, []) | _acc]. - -'encode_vcard_$prodid'(undefined, _acc) -> _acc; -'encode_vcard_$prodid'(Prodid, _acc) -> - [encode_vcard_PRODID(Prodid, []) | _acc]. - -'encode_vcard_$jabberid'(undefined, _acc) -> _acc; -'encode_vcard_$jabberid'(Jabberid, _acc) -> - [encode_vcard_JABBERID(Jabberid, []) | _acc]. - -'encode_vcard_$sound'(undefined, _acc) -> _acc; -'encode_vcard_$sound'(Sound, _acc) -> - [encode_vcard_SOUND(Sound, []) | _acc]. - -'encode_vcard_$note'(undefined, _acc) -> _acc; -'encode_vcard_$note'(Note, _acc) -> - [encode_vcard_NOTE(Note, []) | _acc]. - -'encode_vcard_$role'(undefined, _acc) -> _acc; -'encode_vcard_$role'(Role, _acc) -> - [encode_vcard_ROLE(Role, []) | _acc]. - -'encode_vcard_$title'(undefined, _acc) -> _acc; -'encode_vcard_$title'(Title, _acc) -> - [encode_vcard_TITLE(Title, []) | _acc]. - -'encode_vcard_$nickname'(undefined, _acc) -> _acc; -'encode_vcard_$nickname'(Nickname, _acc) -> - [encode_vcard_NICKNAME(Nickname, []) | _acc]. - -'encode_vcard_$rev'(undefined, _acc) -> _acc; -'encode_vcard_$rev'(Rev, _acc) -> - [encode_vcard_REV(Rev, []) | _acc]. - -'encode_vcard_$sort_string'(undefined, _acc) -> _acc; -'encode_vcard_$sort_string'(Sort_string, _acc) -> - [encode_vcard_SORT_STRING(Sort_string, []) | _acc]. - -'encode_vcard_$org'(undefined, _acc) -> _acc; -'encode_vcard_$org'(Org, _acc) -> - [encode_vcard_ORG(Org, []) | _acc]. - -'encode_vcard_$bday'(undefined, _acc) -> _acc; -'encode_vcard_$bday'(Bday, _acc) -> - [encode_vcard_BDAY(Bday, []) | _acc]. - -'encode_vcard_$key'(undefined, _acc) -> _acc; -'encode_vcard_$key'(Key, _acc) -> - [encode_vcard_KEY(Key, []) | _acc]. - -'encode_vcard_$tz'(undefined, _acc) -> _acc; -'encode_vcard_$tz'(Tz, _acc) -> - [encode_vcard_TZ(Tz, []) | _acc]. - -'encode_vcard_$url'(undefined, _acc) -> _acc; -'encode_vcard_$url'(Url, _acc) -> - [encode_vcard_URL(Url, []) | _acc]. - -'encode_vcard_$email'([], _acc) -> _acc; -'encode_vcard_$email'([Email | _els], _acc) -> - 'encode_vcard_$email'(_els, - [encode_vcard_EMAIL(Email, []) | _acc]). - -'encode_vcard_$tel'([], _acc) -> _acc; -'encode_vcard_$tel'([Tel | _els], _acc) -> - 'encode_vcard_$tel'(_els, - [encode_vcard_TEL(Tel, []) | _acc]). - -'encode_vcard_$label'([], _acc) -> _acc; -'encode_vcard_$label'([Label | _els], _acc) -> - 'encode_vcard_$label'(_els, - [encode_vcard_LABEL(Label, []) | _acc]). - -'encode_vcard_$fn'(undefined, _acc) -> _acc; -'encode_vcard_$fn'(Fn, _acc) -> - [encode_vcard_FN(Fn, []) | _acc]. - -'encode_vcard_$version'(undefined, _acc) -> _acc; -'encode_vcard_$version'(Version, _acc) -> - [encode_vcard_VERSION(Version, []) | _acc]. - -'encode_vcard_$n'(undefined, _acc) -> _acc; -'encode_vcard_$n'(N, _acc) -> - [encode_vcard_N(N, []) | _acc]. - -'encode_vcard_$photo'(undefined, _acc) -> _acc; -'encode_vcard_$photo'(Photo, _acc) -> - [encode_vcard_PHOTO(Photo, []) | _acc]. - -'encode_vcard_$logo'(undefined, _acc) -> _acc; -'encode_vcard_$logo'(Logo, _acc) -> - [encode_vcard_LOGO(Logo, []) | _acc]. - -'encode_vcard_$geo'(undefined, _acc) -> _acc; -'encode_vcard_$geo'(Geo, _acc) -> - [encode_vcard_GEO(Geo, []) | _acc]. - -decode_vcard_CLASS(__TopXMLNS, __IgnoreEls, - {xmlel, <<"CLASS">>, _attrs, _els}) -> - Class = decode_vcard_CLASS_els(__TopXMLNS, __IgnoreEls, - _els, undefined), - Class. - -decode_vcard_CLASS_els(__TopXMLNS, __IgnoreEls, [], - Class) -> - Class; -decode_vcard_CLASS_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"PUBLIC">>, _attrs, _} = _el | _els], - Class) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_CLASS_els(__TopXMLNS, __IgnoreEls, _els, - decode_vcard_PUBLIC(__TopXMLNS, __IgnoreEls, - _el)); - true -> - decode_vcard_CLASS_els(__TopXMLNS, __IgnoreEls, _els, - Class) - end; -decode_vcard_CLASS_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"PRIVATE">>, _attrs, _} = _el | _els], - Class) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_CLASS_els(__TopXMLNS, __IgnoreEls, _els, - decode_vcard_PRIVATE(__TopXMLNS, __IgnoreEls, - _el)); - true -> - decode_vcard_CLASS_els(__TopXMLNS, __IgnoreEls, _els, - Class) - end; -decode_vcard_CLASS_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"CONFIDENTIAL">>, _attrs, _} = _el | _els], - Class) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_CLASS_els(__TopXMLNS, __IgnoreEls, _els, - decode_vcard_CONFIDENTIAL(__TopXMLNS, - __IgnoreEls, _el)); - true -> - decode_vcard_CLASS_els(__TopXMLNS, __IgnoreEls, _els, - Class) - end; -decode_vcard_CLASS_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Class) -> - decode_vcard_CLASS_els(__TopXMLNS, __IgnoreEls, _els, - Class). - -encode_vcard_CLASS(Class, _xmlns_attrs) -> - _els = lists:reverse('encode_vcard_CLASS_$class'(Class, - [])), - _attrs = _xmlns_attrs, - {xmlel, <<"CLASS">>, _attrs, _els}. - -'encode_vcard_CLASS_$class'(undefined, _acc) -> _acc; -'encode_vcard_CLASS_$class'(public = Class, _acc) -> - [encode_vcard_PUBLIC(Class, []) | _acc]; -'encode_vcard_CLASS_$class'(private = Class, _acc) -> - [encode_vcard_PRIVATE(Class, []) | _acc]; -'encode_vcard_CLASS_$class'(confidential = Class, - _acc) -> - [encode_vcard_CONFIDENTIAL(Class, []) | _acc]. - -decode_vcard_CATEGORIES(__TopXMLNS, __IgnoreEls, - {xmlel, <<"CATEGORIES">>, _attrs, _els}) -> - Keywords = decode_vcard_CATEGORIES_els(__TopXMLNS, - __IgnoreEls, _els, []), - Keywords. - -decode_vcard_CATEGORIES_els(__TopXMLNS, __IgnoreEls, [], - Keywords) -> - lists:reverse(Keywords); -decode_vcard_CATEGORIES_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"KEYWORD">>, _attrs, _} = _el | _els], - Keywords) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_CATEGORIES_els(__TopXMLNS, __IgnoreEls, - _els, - case decode_vcard_KEYWORD(__TopXMLNS, - __IgnoreEls, - _el) - of - undefined -> Keywords; - _new_el -> [_new_el | Keywords] - end); - true -> - decode_vcard_CATEGORIES_els(__TopXMLNS, __IgnoreEls, - _els, Keywords) - end; -decode_vcard_CATEGORIES_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Keywords) -> - decode_vcard_CATEGORIES_els(__TopXMLNS, __IgnoreEls, - _els, Keywords). - -encode_vcard_CATEGORIES(Keywords, _xmlns_attrs) -> - _els = - lists:reverse('encode_vcard_CATEGORIES_$keywords'(Keywords, - [])), - _attrs = _xmlns_attrs, - {xmlel, <<"CATEGORIES">>, _attrs, _els}. - -'encode_vcard_CATEGORIES_$keywords'([], _acc) -> _acc; -'encode_vcard_CATEGORIES_$keywords'([Keywords | _els], - _acc) -> - 'encode_vcard_CATEGORIES_$keywords'(_els, - [encode_vcard_KEYWORD(Keywords, []) - | _acc]). - -decode_vcard_KEY(__TopXMLNS, __IgnoreEls, - {xmlel, <<"KEY">>, _attrs, _els}) -> - {Cred, Type} = decode_vcard_KEY_els(__TopXMLNS, - __IgnoreEls, _els, undefined, - undefined), - {vcard_key, Type, Cred}. - -decode_vcard_KEY_els(__TopXMLNS, __IgnoreEls, [], Cred, - Type) -> - {Cred, Type}; -decode_vcard_KEY_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"TYPE">>, _attrs, _} = _el | _els], Cred, - Type) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_KEY_els(__TopXMLNS, __IgnoreEls, _els, - Cred, - decode_vcard_TYPE(__TopXMLNS, __IgnoreEls, - _el)); - true -> - decode_vcard_KEY_els(__TopXMLNS, __IgnoreEls, _els, - Cred, Type) - end; -decode_vcard_KEY_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"CRED">>, _attrs, _} = _el | _els], Cred, - Type) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_KEY_els(__TopXMLNS, __IgnoreEls, _els, - decode_vcard_CRED(__TopXMLNS, __IgnoreEls, _el), - Type); - true -> - decode_vcard_KEY_els(__TopXMLNS, __IgnoreEls, _els, - Cred, Type) - end; -decode_vcard_KEY_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cred, Type) -> - decode_vcard_KEY_els(__TopXMLNS, __IgnoreEls, _els, - Cred, Type). - -encode_vcard_KEY({vcard_key, Type, Cred}, - _xmlns_attrs) -> - _els = lists:reverse('encode_vcard_KEY_$cred'(Cred, - 'encode_vcard_KEY_$type'(Type, - []))), - _attrs = _xmlns_attrs, - {xmlel, <<"KEY">>, _attrs, _els}. - -'encode_vcard_KEY_$cred'(undefined, _acc) -> _acc; -'encode_vcard_KEY_$cred'(Cred, _acc) -> - [encode_vcard_CRED(Cred, []) | _acc]. - -'encode_vcard_KEY_$type'(undefined, _acc) -> _acc; -'encode_vcard_KEY_$type'(Type, _acc) -> - [encode_vcard_TYPE(Type, []) | _acc]. - -decode_vcard_SOUND(__TopXMLNS, __IgnoreEls, - {xmlel, <<"SOUND">>, _attrs, _els}) -> - {Phonetic, Extval, Binval} = - decode_vcard_SOUND_els(__TopXMLNS, __IgnoreEls, _els, - undefined, undefined, undefined), - {vcard_sound, Phonetic, Binval, Extval}. - -decode_vcard_SOUND_els(__TopXMLNS, __IgnoreEls, [], - Phonetic, Extval, Binval) -> - {Phonetic, Extval, Binval}; -decode_vcard_SOUND_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"BINVAL">>, _attrs, _} = _el | _els], - Phonetic, Extval, Binval) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_SOUND_els(__TopXMLNS, __IgnoreEls, _els, - Phonetic, Extval, - decode_vcard_BINVAL(__TopXMLNS, __IgnoreEls, - _el)); - true -> - decode_vcard_SOUND_els(__TopXMLNS, __IgnoreEls, _els, - Phonetic, Extval, Binval) - end; -decode_vcard_SOUND_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"EXTVAL">>, _attrs, _} = _el | _els], - Phonetic, Extval, Binval) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_SOUND_els(__TopXMLNS, __IgnoreEls, _els, - Phonetic, - decode_vcard_EXTVAL(__TopXMLNS, __IgnoreEls, - _el), - Binval); - true -> - decode_vcard_SOUND_els(__TopXMLNS, __IgnoreEls, _els, - Phonetic, Extval, Binval) - end; -decode_vcard_SOUND_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"PHONETIC">>, _attrs, _} = _el | _els], - Phonetic, Extval, Binval) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_SOUND_els(__TopXMLNS, __IgnoreEls, _els, - decode_vcard_PHONETIC(__TopXMLNS, __IgnoreEls, - _el), - Extval, Binval); - true -> - decode_vcard_SOUND_els(__TopXMLNS, __IgnoreEls, _els, - Phonetic, Extval, Binval) - end; -decode_vcard_SOUND_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Phonetic, Extval, Binval) -> - decode_vcard_SOUND_els(__TopXMLNS, __IgnoreEls, _els, - Phonetic, Extval, Binval). - -encode_vcard_SOUND({vcard_sound, Phonetic, Binval, - Extval}, - _xmlns_attrs) -> - _els = - lists:reverse('encode_vcard_SOUND_$phonetic'(Phonetic, - 'encode_vcard_SOUND_$extval'(Extval, - 'encode_vcard_SOUND_$binval'(Binval, - [])))), - _attrs = _xmlns_attrs, - {xmlel, <<"SOUND">>, _attrs, _els}. - -'encode_vcard_SOUND_$phonetic'(undefined, _acc) -> _acc; -'encode_vcard_SOUND_$phonetic'(Phonetic, _acc) -> - [encode_vcard_PHONETIC(Phonetic, []) | _acc]. - -'encode_vcard_SOUND_$extval'(undefined, _acc) -> _acc; -'encode_vcard_SOUND_$extval'(Extval, _acc) -> - [encode_vcard_EXTVAL(Extval, []) | _acc]. - -'encode_vcard_SOUND_$binval'(undefined, _acc) -> _acc; -'encode_vcard_SOUND_$binval'(Binval, _acc) -> - [encode_vcard_BINVAL(Binval, []) | _acc]. - -decode_vcard_ORG(__TopXMLNS, __IgnoreEls, - {xmlel, <<"ORG">>, _attrs, _els}) -> - {Units, Name} = decode_vcard_ORG_els(__TopXMLNS, - __IgnoreEls, _els, [], undefined), - {vcard_org, Name, Units}. - -decode_vcard_ORG_els(__TopXMLNS, __IgnoreEls, [], Units, - Name) -> - {lists:reverse(Units), Name}; -decode_vcard_ORG_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"ORGNAME">>, _attrs, _} = _el | _els], Units, - Name) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_ORG_els(__TopXMLNS, __IgnoreEls, _els, - Units, - decode_vcard_ORGNAME(__TopXMLNS, __IgnoreEls, - _el)); - true -> - decode_vcard_ORG_els(__TopXMLNS, __IgnoreEls, _els, - Units, Name) - end; -decode_vcard_ORG_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"ORGUNIT">>, _attrs, _} = _el | _els], Units, - Name) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_ORG_els(__TopXMLNS, __IgnoreEls, _els, - case decode_vcard_ORGUNIT(__TopXMLNS, - __IgnoreEls, _el) - of - undefined -> Units; - _new_el -> [_new_el | Units] - end, - Name); - true -> - decode_vcard_ORG_els(__TopXMLNS, __IgnoreEls, _els, - Units, Name) - end; -decode_vcard_ORG_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Units, Name) -> - decode_vcard_ORG_els(__TopXMLNS, __IgnoreEls, _els, - Units, Name). - -encode_vcard_ORG({vcard_org, Name, Units}, - _xmlns_attrs) -> - _els = lists:reverse('encode_vcard_ORG_$units'(Units, - 'encode_vcard_ORG_$name'(Name, - []))), - _attrs = _xmlns_attrs, - {xmlel, <<"ORG">>, _attrs, _els}. - -'encode_vcard_ORG_$units'([], _acc) -> _acc; -'encode_vcard_ORG_$units'([Units | _els], _acc) -> - 'encode_vcard_ORG_$units'(_els, - [encode_vcard_ORGUNIT(Units, []) | _acc]). - -'encode_vcard_ORG_$name'(undefined, _acc) -> _acc; -'encode_vcard_ORG_$name'(Name, _acc) -> - [encode_vcard_ORGNAME(Name, []) | _acc]. - -decode_vcard_PHOTO(__TopXMLNS, __IgnoreEls, - {xmlel, <<"PHOTO">>, _attrs, _els}) -> - {Type, Extval, Binval} = - decode_vcard_PHOTO_els(__TopXMLNS, __IgnoreEls, _els, - undefined, undefined, undefined), - {vcard_photo, Type, Binval, Extval}. - -decode_vcard_PHOTO_els(__TopXMLNS, __IgnoreEls, [], - Type, Extval, Binval) -> - {Type, Extval, Binval}; -decode_vcard_PHOTO_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"TYPE">>, _attrs, _} = _el | _els], Type, - Extval, Binval) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_PHOTO_els(__TopXMLNS, __IgnoreEls, _els, - decode_vcard_TYPE(__TopXMLNS, __IgnoreEls, - _el), - Extval, Binval); - true -> - decode_vcard_PHOTO_els(__TopXMLNS, __IgnoreEls, _els, - Type, Extval, Binval) - end; -decode_vcard_PHOTO_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"BINVAL">>, _attrs, _} = _el | _els], Type, - Extval, Binval) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_PHOTO_els(__TopXMLNS, __IgnoreEls, _els, - Type, Extval, - decode_vcard_BINVAL(__TopXMLNS, __IgnoreEls, - _el)); - true -> - decode_vcard_PHOTO_els(__TopXMLNS, __IgnoreEls, _els, - Type, Extval, Binval) - end; -decode_vcard_PHOTO_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"EXTVAL">>, _attrs, _} = _el | _els], Type, - Extval, Binval) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_PHOTO_els(__TopXMLNS, __IgnoreEls, _els, - Type, - decode_vcard_EXTVAL(__TopXMLNS, __IgnoreEls, - _el), - Binval); - true -> - decode_vcard_PHOTO_els(__TopXMLNS, __IgnoreEls, _els, - Type, Extval, Binval) - end; -decode_vcard_PHOTO_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Type, Extval, Binval) -> - decode_vcard_PHOTO_els(__TopXMLNS, __IgnoreEls, _els, - Type, Extval, Binval). - -encode_vcard_PHOTO({vcard_photo, Type, Binval, Extval}, - _xmlns_attrs) -> - _els = lists:reverse('encode_vcard_PHOTO_$type'(Type, - 'encode_vcard_PHOTO_$extval'(Extval, - 'encode_vcard_PHOTO_$binval'(Binval, - [])))), - _attrs = _xmlns_attrs, - {xmlel, <<"PHOTO">>, _attrs, _els}. - -'encode_vcard_PHOTO_$type'(undefined, _acc) -> _acc; -'encode_vcard_PHOTO_$type'(Type, _acc) -> - [encode_vcard_TYPE(Type, []) | _acc]. - -'encode_vcard_PHOTO_$extval'(undefined, _acc) -> _acc; -'encode_vcard_PHOTO_$extval'(Extval, _acc) -> - [encode_vcard_EXTVAL(Extval, []) | _acc]. - -'encode_vcard_PHOTO_$binval'(undefined, _acc) -> _acc; -'encode_vcard_PHOTO_$binval'(Binval, _acc) -> - [encode_vcard_BINVAL(Binval, []) | _acc]. - -decode_vcard_LOGO(__TopXMLNS, __IgnoreEls, - {xmlel, <<"LOGO">>, _attrs, _els}) -> - {Type, Extval, Binval} = - decode_vcard_LOGO_els(__TopXMLNS, __IgnoreEls, _els, - undefined, undefined, undefined), - {vcard_logo, Type, Binval, Extval}. - -decode_vcard_LOGO_els(__TopXMLNS, __IgnoreEls, [], Type, - Extval, Binval) -> - {Type, Extval, Binval}; -decode_vcard_LOGO_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"TYPE">>, _attrs, _} = _el | _els], Type, - Extval, Binval) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_LOGO_els(__TopXMLNS, __IgnoreEls, _els, - decode_vcard_TYPE(__TopXMLNS, __IgnoreEls, - _el), - Extval, Binval); - true -> - decode_vcard_LOGO_els(__TopXMLNS, __IgnoreEls, _els, - Type, Extval, Binval) - end; -decode_vcard_LOGO_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"BINVAL">>, _attrs, _} = _el | _els], Type, - Extval, Binval) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_LOGO_els(__TopXMLNS, __IgnoreEls, _els, - Type, Extval, - decode_vcard_BINVAL(__TopXMLNS, __IgnoreEls, - _el)); - true -> - decode_vcard_LOGO_els(__TopXMLNS, __IgnoreEls, _els, - Type, Extval, Binval) - end; -decode_vcard_LOGO_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"EXTVAL">>, _attrs, _} = _el | _els], Type, - Extval, Binval) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_LOGO_els(__TopXMLNS, __IgnoreEls, _els, - Type, - decode_vcard_EXTVAL(__TopXMLNS, __IgnoreEls, - _el), - Binval); - true -> - decode_vcard_LOGO_els(__TopXMLNS, __IgnoreEls, _els, - Type, Extval, Binval) - end; -decode_vcard_LOGO_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Type, Extval, Binval) -> - decode_vcard_LOGO_els(__TopXMLNS, __IgnoreEls, _els, - Type, Extval, Binval). - -encode_vcard_LOGO({vcard_logo, Type, Binval, Extval}, - _xmlns_attrs) -> - _els = lists:reverse('encode_vcard_LOGO_$type'(Type, - 'encode_vcard_LOGO_$extval'(Extval, - 'encode_vcard_LOGO_$binval'(Binval, - [])))), - _attrs = _xmlns_attrs, - {xmlel, <<"LOGO">>, _attrs, _els}. - -'encode_vcard_LOGO_$type'(undefined, _acc) -> _acc; -'encode_vcard_LOGO_$type'(Type, _acc) -> - [encode_vcard_TYPE(Type, []) | _acc]. - -'encode_vcard_LOGO_$extval'(undefined, _acc) -> _acc; -'encode_vcard_LOGO_$extval'(Extval, _acc) -> - [encode_vcard_EXTVAL(Extval, []) | _acc]. - -'encode_vcard_LOGO_$binval'(undefined, _acc) -> _acc; -'encode_vcard_LOGO_$binval'(Binval, _acc) -> - [encode_vcard_BINVAL(Binval, []) | _acc]. - -decode_vcard_BINVAL(__TopXMLNS, __IgnoreEls, - {xmlel, <<"BINVAL">>, _attrs, _els}) -> - Cdata = decode_vcard_BINVAL_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - Cdata. - -decode_vcard_BINVAL_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_vcard_BINVAL_cdata(__TopXMLNS, Cdata); -decode_vcard_BINVAL_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_vcard_BINVAL_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_vcard_BINVAL_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_vcard_BINVAL_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_vcard_BINVAL(Cdata, _xmlns_attrs) -> - _els = encode_vcard_BINVAL_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"BINVAL">>, _attrs, _els}. - -decode_vcard_BINVAL_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_vcard_BINVAL_cdata(__TopXMLNS, _val) -> - case catch base64:decode(_val) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_cdata_value, <<>>, <<"BINVAL">>, __TopXMLNS}}); - _res -> _res - end. - -encode_vcard_BINVAL_cdata(undefined, _acc) -> _acc; -encode_vcard_BINVAL_cdata(_val, _acc) -> - [{xmlcdata, base64:encode(_val)} | _acc]. - -decode_vcard_GEO(__TopXMLNS, __IgnoreEls, - {xmlel, <<"GEO">>, _attrs, _els}) -> - {Lat, Lon} = decode_vcard_GEO_els(__TopXMLNS, - __IgnoreEls, _els, undefined, undefined), - {vcard_geo, Lat, Lon}. - -decode_vcard_GEO_els(__TopXMLNS, __IgnoreEls, [], Lat, - Lon) -> - {Lat, Lon}; -decode_vcard_GEO_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"LAT">>, _attrs, _} = _el | _els], Lat, - Lon) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_GEO_els(__TopXMLNS, __IgnoreEls, _els, - decode_vcard_LAT(__TopXMLNS, __IgnoreEls, _el), - Lon); - true -> - decode_vcard_GEO_els(__TopXMLNS, __IgnoreEls, _els, Lat, - Lon) - end; -decode_vcard_GEO_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"LON">>, _attrs, _} = _el | _els], Lat, - Lon) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_GEO_els(__TopXMLNS, __IgnoreEls, _els, Lat, - decode_vcard_LON(__TopXMLNS, __IgnoreEls, _el)); - true -> - decode_vcard_GEO_els(__TopXMLNS, __IgnoreEls, _els, Lat, - Lon) - end; -decode_vcard_GEO_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Lat, Lon) -> - decode_vcard_GEO_els(__TopXMLNS, __IgnoreEls, _els, Lat, - Lon). - -encode_vcard_GEO({vcard_geo, Lat, Lon}, _xmlns_attrs) -> - _els = lists:reverse('encode_vcard_GEO_$lat'(Lat, - 'encode_vcard_GEO_$lon'(Lon, - []))), - _attrs = _xmlns_attrs, - {xmlel, <<"GEO">>, _attrs, _els}. - -'encode_vcard_GEO_$lat'(undefined, _acc) -> _acc; -'encode_vcard_GEO_$lat'(Lat, _acc) -> - [encode_vcard_LAT(Lat, []) | _acc]. - -'encode_vcard_GEO_$lon'(undefined, _acc) -> _acc; -'encode_vcard_GEO_$lon'(Lon, _acc) -> - [encode_vcard_LON(Lon, []) | _acc]. - -decode_vcard_EMAIL(__TopXMLNS, __IgnoreEls, - {xmlel, <<"EMAIL">>, _attrs, _els}) -> - {X400, Userid, Internet, Home, Pref, Work} = - decode_vcard_EMAIL_els(__TopXMLNS, __IgnoreEls, _els, - false, undefined, false, false, false, false), - {vcard_email, Home, Work, Internet, Pref, X400, Userid}. - -decode_vcard_EMAIL_els(__TopXMLNS, __IgnoreEls, [], - X400, Userid, Internet, Home, Pref, Work) -> - {X400, Userid, Internet, Home, Pref, Work}; -decode_vcard_EMAIL_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"HOME">>, _attrs, _} = _el | _els], X400, - Userid, Internet, Home, Pref, Work) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_EMAIL_els(__TopXMLNS, __IgnoreEls, _els, - X400, Userid, Internet, - decode_vcard_HOME(__TopXMLNS, __IgnoreEls, - _el), - Pref, Work); - true -> - decode_vcard_EMAIL_els(__TopXMLNS, __IgnoreEls, _els, - X400, Userid, Internet, Home, Pref, Work) - end; -decode_vcard_EMAIL_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"WORK">>, _attrs, _} = _el | _els], X400, - Userid, Internet, Home, Pref, Work) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_EMAIL_els(__TopXMLNS, __IgnoreEls, _els, - X400, Userid, Internet, Home, Pref, - decode_vcard_WORK(__TopXMLNS, __IgnoreEls, - _el)); - true -> - decode_vcard_EMAIL_els(__TopXMLNS, __IgnoreEls, _els, - X400, Userid, Internet, Home, Pref, Work) - end; -decode_vcard_EMAIL_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"INTERNET">>, _attrs, _} = _el | _els], X400, - Userid, Internet, Home, Pref, Work) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_EMAIL_els(__TopXMLNS, __IgnoreEls, _els, - X400, Userid, - decode_vcard_INTERNET(__TopXMLNS, __IgnoreEls, - _el), - Home, Pref, Work); - true -> - decode_vcard_EMAIL_els(__TopXMLNS, __IgnoreEls, _els, - X400, Userid, Internet, Home, Pref, Work) - end; -decode_vcard_EMAIL_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"PREF">>, _attrs, _} = _el | _els], X400, - Userid, Internet, Home, Pref, Work) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_EMAIL_els(__TopXMLNS, __IgnoreEls, _els, - X400, Userid, Internet, Home, - decode_vcard_PREF(__TopXMLNS, __IgnoreEls, - _el), - Work); - true -> - decode_vcard_EMAIL_els(__TopXMLNS, __IgnoreEls, _els, - X400, Userid, Internet, Home, Pref, Work) - end; -decode_vcard_EMAIL_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"X400">>, _attrs, _} = _el | _els], X400, - Userid, Internet, Home, Pref, Work) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_EMAIL_els(__TopXMLNS, __IgnoreEls, _els, - decode_vcard_X400(__TopXMLNS, __IgnoreEls, - _el), - Userid, Internet, Home, Pref, Work); - true -> - decode_vcard_EMAIL_els(__TopXMLNS, __IgnoreEls, _els, - X400, Userid, Internet, Home, Pref, Work) - end; -decode_vcard_EMAIL_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"USERID">>, _attrs, _} = _el | _els], X400, - Userid, Internet, Home, Pref, Work) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_EMAIL_els(__TopXMLNS, __IgnoreEls, _els, - X400, - decode_vcard_USERID(__TopXMLNS, __IgnoreEls, - _el), - Internet, Home, Pref, Work); - true -> - decode_vcard_EMAIL_els(__TopXMLNS, __IgnoreEls, _els, - X400, Userid, Internet, Home, Pref, Work) - end; -decode_vcard_EMAIL_els(__TopXMLNS, __IgnoreEls, - [_ | _els], X400, Userid, Internet, Home, Pref, Work) -> - decode_vcard_EMAIL_els(__TopXMLNS, __IgnoreEls, _els, - X400, Userid, Internet, Home, Pref, Work). - -encode_vcard_EMAIL({vcard_email, Home, Work, Internet, - Pref, X400, Userid}, - _xmlns_attrs) -> - _els = lists:reverse('encode_vcard_EMAIL_$x400'(X400, - 'encode_vcard_EMAIL_$userid'(Userid, - 'encode_vcard_EMAIL_$internet'(Internet, - 'encode_vcard_EMAIL_$home'(Home, - 'encode_vcard_EMAIL_$pref'(Pref, - 'encode_vcard_EMAIL_$work'(Work, - []))))))), - _attrs = _xmlns_attrs, - {xmlel, <<"EMAIL">>, _attrs, _els}. - -'encode_vcard_EMAIL_$x400'(false, _acc) -> _acc; -'encode_vcard_EMAIL_$x400'(X400, _acc) -> - [encode_vcard_X400(X400, []) | _acc]. - -'encode_vcard_EMAIL_$userid'(undefined, _acc) -> _acc; -'encode_vcard_EMAIL_$userid'(Userid, _acc) -> - [encode_vcard_USERID(Userid, []) | _acc]. - -'encode_vcard_EMAIL_$internet'(false, _acc) -> _acc; -'encode_vcard_EMAIL_$internet'(Internet, _acc) -> - [encode_vcard_INTERNET(Internet, []) | _acc]. - -'encode_vcard_EMAIL_$home'(false, _acc) -> _acc; -'encode_vcard_EMAIL_$home'(Home, _acc) -> - [encode_vcard_HOME(Home, []) | _acc]. - -'encode_vcard_EMAIL_$pref'(false, _acc) -> _acc; -'encode_vcard_EMAIL_$pref'(Pref, _acc) -> - [encode_vcard_PREF(Pref, []) | _acc]. - -'encode_vcard_EMAIL_$work'(false, _acc) -> _acc; -'encode_vcard_EMAIL_$work'(Work, _acc) -> - [encode_vcard_WORK(Work, []) | _acc]. - -decode_vcard_TEL(__TopXMLNS, __IgnoreEls, - {xmlel, <<"TEL">>, _attrs, _els}) -> - {Number, Pager, Pcs, Bbs, Voice, Home, Pref, Msg, Fax, - Work, Cell, Modem, Isdn, Video} = - decode_vcard_TEL_els(__TopXMLNS, __IgnoreEls, _els, - undefined, false, false, false, false, false, - false, false, false, false, false, false, false, - false), - {vcard_tel, Home, Work, Voice, Fax, Pager, Msg, Cell, - Video, Bbs, Modem, Isdn, Pcs, Pref, Number}. - -decode_vcard_TEL_els(__TopXMLNS, __IgnoreEls, [], - Number, Pager, Pcs, Bbs, Voice, Home, Pref, Msg, Fax, - Work, Cell, Modem, Isdn, Video) -> - {Number, Pager, Pcs, Bbs, Voice, Home, Pref, Msg, Fax, - Work, Cell, Modem, Isdn, Video}; -decode_vcard_TEL_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"HOME">>, _attrs, _} = _el | _els], Number, - Pager, Pcs, Bbs, Voice, Home, Pref, Msg, Fax, Work, - Cell, Modem, Isdn, Video) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_TEL_els(__TopXMLNS, __IgnoreEls, _els, - Number, Pager, Pcs, Bbs, Voice, - decode_vcard_HOME(__TopXMLNS, __IgnoreEls, _el), - Pref, Msg, Fax, Work, Cell, Modem, Isdn, Video); - true -> - decode_vcard_TEL_els(__TopXMLNS, __IgnoreEls, _els, - Number, Pager, Pcs, Bbs, Voice, Home, Pref, Msg, - Fax, Work, Cell, Modem, Isdn, Video) - end; -decode_vcard_TEL_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"WORK">>, _attrs, _} = _el | _els], Number, - Pager, Pcs, Bbs, Voice, Home, Pref, Msg, Fax, Work, - Cell, Modem, Isdn, Video) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_TEL_els(__TopXMLNS, __IgnoreEls, _els, - Number, Pager, Pcs, Bbs, Voice, Home, Pref, Msg, - Fax, - decode_vcard_WORK(__TopXMLNS, __IgnoreEls, _el), - Cell, Modem, Isdn, Video); - true -> - decode_vcard_TEL_els(__TopXMLNS, __IgnoreEls, _els, - Number, Pager, Pcs, Bbs, Voice, Home, Pref, Msg, - Fax, Work, Cell, Modem, Isdn, Video) - end; -decode_vcard_TEL_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"VOICE">>, _attrs, _} = _el | _els], Number, - Pager, Pcs, Bbs, Voice, Home, Pref, Msg, Fax, Work, - Cell, Modem, Isdn, Video) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_TEL_els(__TopXMLNS, __IgnoreEls, _els, - Number, Pager, Pcs, Bbs, - decode_vcard_VOICE(__TopXMLNS, __IgnoreEls, - _el), - Home, Pref, Msg, Fax, Work, Cell, Modem, Isdn, - Video); - true -> - decode_vcard_TEL_els(__TopXMLNS, __IgnoreEls, _els, - Number, Pager, Pcs, Bbs, Voice, Home, Pref, Msg, - Fax, Work, Cell, Modem, Isdn, Video) - end; -decode_vcard_TEL_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"FAX">>, _attrs, _} = _el | _els], Number, - Pager, Pcs, Bbs, Voice, Home, Pref, Msg, Fax, Work, - Cell, Modem, Isdn, Video) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_TEL_els(__TopXMLNS, __IgnoreEls, _els, - Number, Pager, Pcs, Bbs, Voice, Home, Pref, Msg, - decode_vcard_FAX(__TopXMLNS, __IgnoreEls, _el), - Work, Cell, Modem, Isdn, Video); - true -> - decode_vcard_TEL_els(__TopXMLNS, __IgnoreEls, _els, - Number, Pager, Pcs, Bbs, Voice, Home, Pref, Msg, - Fax, Work, Cell, Modem, Isdn, Video) - end; -decode_vcard_TEL_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"PAGER">>, _attrs, _} = _el | _els], Number, - Pager, Pcs, Bbs, Voice, Home, Pref, Msg, Fax, Work, - Cell, Modem, Isdn, Video) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_TEL_els(__TopXMLNS, __IgnoreEls, _els, - Number, - decode_vcard_PAGER(__TopXMLNS, __IgnoreEls, - _el), - Pcs, Bbs, Voice, Home, Pref, Msg, Fax, Work, - Cell, Modem, Isdn, Video); - true -> - decode_vcard_TEL_els(__TopXMLNS, __IgnoreEls, _els, - Number, Pager, Pcs, Bbs, Voice, Home, Pref, Msg, - Fax, Work, Cell, Modem, Isdn, Video) - end; -decode_vcard_TEL_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"MSG">>, _attrs, _} = _el | _els], Number, - Pager, Pcs, Bbs, Voice, Home, Pref, Msg, Fax, Work, - Cell, Modem, Isdn, Video) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_TEL_els(__TopXMLNS, __IgnoreEls, _els, - Number, Pager, Pcs, Bbs, Voice, Home, Pref, - decode_vcard_MSG(__TopXMLNS, __IgnoreEls, _el), - Fax, Work, Cell, Modem, Isdn, Video); - true -> - decode_vcard_TEL_els(__TopXMLNS, __IgnoreEls, _els, - Number, Pager, Pcs, Bbs, Voice, Home, Pref, Msg, - Fax, Work, Cell, Modem, Isdn, Video) - end; -decode_vcard_TEL_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"CELL">>, _attrs, _} = _el | _els], Number, - Pager, Pcs, Bbs, Voice, Home, Pref, Msg, Fax, Work, - Cell, Modem, Isdn, Video) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_TEL_els(__TopXMLNS, __IgnoreEls, _els, - Number, Pager, Pcs, Bbs, Voice, Home, Pref, Msg, - Fax, Work, - decode_vcard_CELL(__TopXMLNS, __IgnoreEls, _el), - Modem, Isdn, Video); - true -> - decode_vcard_TEL_els(__TopXMLNS, __IgnoreEls, _els, - Number, Pager, Pcs, Bbs, Voice, Home, Pref, Msg, - Fax, Work, Cell, Modem, Isdn, Video) - end; -decode_vcard_TEL_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"VIDEO">>, _attrs, _} = _el | _els], Number, - Pager, Pcs, Bbs, Voice, Home, Pref, Msg, Fax, Work, - Cell, Modem, Isdn, Video) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_TEL_els(__TopXMLNS, __IgnoreEls, _els, - Number, Pager, Pcs, Bbs, Voice, Home, Pref, Msg, - Fax, Work, Cell, Modem, Isdn, - decode_vcard_VIDEO(__TopXMLNS, __IgnoreEls, - _el)); - true -> - decode_vcard_TEL_els(__TopXMLNS, __IgnoreEls, _els, - Number, Pager, Pcs, Bbs, Voice, Home, Pref, Msg, - Fax, Work, Cell, Modem, Isdn, Video) - end; -decode_vcard_TEL_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"BBS">>, _attrs, _} = _el | _els], Number, - Pager, Pcs, Bbs, Voice, Home, Pref, Msg, Fax, Work, - Cell, Modem, Isdn, Video) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_TEL_els(__TopXMLNS, __IgnoreEls, _els, - Number, Pager, Pcs, - decode_vcard_BBS(__TopXMLNS, __IgnoreEls, _el), - Voice, Home, Pref, Msg, Fax, Work, Cell, Modem, - Isdn, Video); - true -> - decode_vcard_TEL_els(__TopXMLNS, __IgnoreEls, _els, - Number, Pager, Pcs, Bbs, Voice, Home, Pref, Msg, - Fax, Work, Cell, Modem, Isdn, Video) - end; -decode_vcard_TEL_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"MODEM">>, _attrs, _} = _el | _els], Number, - Pager, Pcs, Bbs, Voice, Home, Pref, Msg, Fax, Work, - Cell, Modem, Isdn, Video) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_TEL_els(__TopXMLNS, __IgnoreEls, _els, - Number, Pager, Pcs, Bbs, Voice, Home, Pref, Msg, - Fax, Work, Cell, - decode_vcard_MODEM(__TopXMLNS, __IgnoreEls, - _el), - Isdn, Video); - true -> - decode_vcard_TEL_els(__TopXMLNS, __IgnoreEls, _els, - Number, Pager, Pcs, Bbs, Voice, Home, Pref, Msg, - Fax, Work, Cell, Modem, Isdn, Video) - end; -decode_vcard_TEL_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"ISDN">>, _attrs, _} = _el | _els], Number, - Pager, Pcs, Bbs, Voice, Home, Pref, Msg, Fax, Work, - Cell, Modem, Isdn, Video) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_TEL_els(__TopXMLNS, __IgnoreEls, _els, - Number, Pager, Pcs, Bbs, Voice, Home, Pref, Msg, - Fax, Work, Cell, Modem, - decode_vcard_ISDN(__TopXMLNS, __IgnoreEls, _el), - Video); - true -> - decode_vcard_TEL_els(__TopXMLNS, __IgnoreEls, _els, - Number, Pager, Pcs, Bbs, Voice, Home, Pref, Msg, - Fax, Work, Cell, Modem, Isdn, Video) - end; -decode_vcard_TEL_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"PCS">>, _attrs, _} = _el | _els], Number, - Pager, Pcs, Bbs, Voice, Home, Pref, Msg, Fax, Work, - Cell, Modem, Isdn, Video) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_TEL_els(__TopXMLNS, __IgnoreEls, _els, - Number, Pager, - decode_vcard_PCS(__TopXMLNS, __IgnoreEls, _el), - Bbs, Voice, Home, Pref, Msg, Fax, Work, Cell, - Modem, Isdn, Video); - true -> - decode_vcard_TEL_els(__TopXMLNS, __IgnoreEls, _els, - Number, Pager, Pcs, Bbs, Voice, Home, Pref, Msg, - Fax, Work, Cell, Modem, Isdn, Video) - end; -decode_vcard_TEL_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"PREF">>, _attrs, _} = _el | _els], Number, - Pager, Pcs, Bbs, Voice, Home, Pref, Msg, Fax, Work, - Cell, Modem, Isdn, Video) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_TEL_els(__TopXMLNS, __IgnoreEls, _els, - Number, Pager, Pcs, Bbs, Voice, Home, - decode_vcard_PREF(__TopXMLNS, __IgnoreEls, _el), - Msg, Fax, Work, Cell, Modem, Isdn, Video); - true -> - decode_vcard_TEL_els(__TopXMLNS, __IgnoreEls, _els, - Number, Pager, Pcs, Bbs, Voice, Home, Pref, Msg, - Fax, Work, Cell, Modem, Isdn, Video) - end; -decode_vcard_TEL_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"NUMBER">>, _attrs, _} = _el | _els], Number, - Pager, Pcs, Bbs, Voice, Home, Pref, Msg, Fax, Work, - Cell, Modem, Isdn, Video) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_TEL_els(__TopXMLNS, __IgnoreEls, _els, - decode_vcard_NUMBER(__TopXMLNS, __IgnoreEls, - _el), - Pager, Pcs, Bbs, Voice, Home, Pref, Msg, Fax, - Work, Cell, Modem, Isdn, Video); - true -> - decode_vcard_TEL_els(__TopXMLNS, __IgnoreEls, _els, - Number, Pager, Pcs, Bbs, Voice, Home, Pref, Msg, - Fax, Work, Cell, Modem, Isdn, Video) - end; -decode_vcard_TEL_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Number, Pager, Pcs, Bbs, Voice, Home, Pref, - Msg, Fax, Work, Cell, Modem, Isdn, Video) -> - decode_vcard_TEL_els(__TopXMLNS, __IgnoreEls, _els, - Number, Pager, Pcs, Bbs, Voice, Home, Pref, Msg, Fax, - Work, Cell, Modem, Isdn, Video). - -encode_vcard_TEL({vcard_tel, Home, Work, Voice, Fax, - Pager, Msg, Cell, Video, Bbs, Modem, Isdn, Pcs, Pref, - Number}, - _xmlns_attrs) -> - _els = lists:reverse('encode_vcard_TEL_$number'(Number, - 'encode_vcard_TEL_$pager'(Pager, - 'encode_vcard_TEL_$pcs'(Pcs, - 'encode_vcard_TEL_$bbs'(Bbs, - 'encode_vcard_TEL_$voice'(Voice, - 'encode_vcard_TEL_$home'(Home, - 'encode_vcard_TEL_$pref'(Pref, - 'encode_vcard_TEL_$msg'(Msg, - 'encode_vcard_TEL_$fax'(Fax, - 'encode_vcard_TEL_$work'(Work, - 'encode_vcard_TEL_$cell'(Cell, - 'encode_vcard_TEL_$modem'(Modem, - 'encode_vcard_TEL_$isdn'(Isdn, - 'encode_vcard_TEL_$video'(Video, - []))))))))))))))), - _attrs = _xmlns_attrs, - {xmlel, <<"TEL">>, _attrs, _els}. - -'encode_vcard_TEL_$number'(undefined, _acc) -> _acc; -'encode_vcard_TEL_$number'(Number, _acc) -> - [encode_vcard_NUMBER(Number, []) | _acc]. - -'encode_vcard_TEL_$pager'(false, _acc) -> _acc; -'encode_vcard_TEL_$pager'(Pager, _acc) -> - [encode_vcard_PAGER(Pager, []) | _acc]. - -'encode_vcard_TEL_$pcs'(false, _acc) -> _acc; -'encode_vcard_TEL_$pcs'(Pcs, _acc) -> - [encode_vcard_PCS(Pcs, []) | _acc]. - -'encode_vcard_TEL_$bbs'(false, _acc) -> _acc; -'encode_vcard_TEL_$bbs'(Bbs, _acc) -> - [encode_vcard_BBS(Bbs, []) | _acc]. - -'encode_vcard_TEL_$voice'(false, _acc) -> _acc; -'encode_vcard_TEL_$voice'(Voice, _acc) -> - [encode_vcard_VOICE(Voice, []) | _acc]. - -'encode_vcard_TEL_$home'(false, _acc) -> _acc; -'encode_vcard_TEL_$home'(Home, _acc) -> - [encode_vcard_HOME(Home, []) | _acc]. - -'encode_vcard_TEL_$pref'(false, _acc) -> _acc; -'encode_vcard_TEL_$pref'(Pref, _acc) -> - [encode_vcard_PREF(Pref, []) | _acc]. - -'encode_vcard_TEL_$msg'(false, _acc) -> _acc; -'encode_vcard_TEL_$msg'(Msg, _acc) -> - [encode_vcard_MSG(Msg, []) | _acc]. - -'encode_vcard_TEL_$fax'(false, _acc) -> _acc; -'encode_vcard_TEL_$fax'(Fax, _acc) -> - [encode_vcard_FAX(Fax, []) | _acc]. - -'encode_vcard_TEL_$work'(false, _acc) -> _acc; -'encode_vcard_TEL_$work'(Work, _acc) -> - [encode_vcard_WORK(Work, []) | _acc]. - -'encode_vcard_TEL_$cell'(false, _acc) -> _acc; -'encode_vcard_TEL_$cell'(Cell, _acc) -> - [encode_vcard_CELL(Cell, []) | _acc]. - -'encode_vcard_TEL_$modem'(false, _acc) -> _acc; -'encode_vcard_TEL_$modem'(Modem, _acc) -> - [encode_vcard_MODEM(Modem, []) | _acc]. - -'encode_vcard_TEL_$isdn'(false, _acc) -> _acc; -'encode_vcard_TEL_$isdn'(Isdn, _acc) -> - [encode_vcard_ISDN(Isdn, []) | _acc]. - -'encode_vcard_TEL_$video'(false, _acc) -> _acc; -'encode_vcard_TEL_$video'(Video, _acc) -> - [encode_vcard_VIDEO(Video, []) | _acc]. - -decode_vcard_LABEL(__TopXMLNS, __IgnoreEls, - {xmlel, <<"LABEL">>, _attrs, _els}) -> - {Line, Home, Pref, Work, Intl, Parcel, Postal, Dom} = - decode_vcard_LABEL_els(__TopXMLNS, __IgnoreEls, _els, - [], false, false, false, false, false, false, - false), - {vcard_label, Home, Work, Postal, Parcel, Dom, Intl, - Pref, Line}. - -decode_vcard_LABEL_els(__TopXMLNS, __IgnoreEls, [], - Line, Home, Pref, Work, Intl, Parcel, Postal, Dom) -> - {lists:reverse(Line), Home, Pref, Work, Intl, Parcel, - Postal, Dom}; -decode_vcard_LABEL_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"HOME">>, _attrs, _} = _el | _els], Line, - Home, Pref, Work, Intl, Parcel, Postal, Dom) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_LABEL_els(__TopXMLNS, __IgnoreEls, _els, - Line, - decode_vcard_HOME(__TopXMLNS, __IgnoreEls, - _el), - Pref, Work, Intl, Parcel, Postal, Dom); - true -> - decode_vcard_LABEL_els(__TopXMLNS, __IgnoreEls, _els, - Line, Home, Pref, Work, Intl, Parcel, Postal, - Dom) - end; -decode_vcard_LABEL_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"WORK">>, _attrs, _} = _el | _els], Line, - Home, Pref, Work, Intl, Parcel, Postal, Dom) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_LABEL_els(__TopXMLNS, __IgnoreEls, _els, - Line, Home, Pref, - decode_vcard_WORK(__TopXMLNS, __IgnoreEls, - _el), - Intl, Parcel, Postal, Dom); - true -> - decode_vcard_LABEL_els(__TopXMLNS, __IgnoreEls, _els, - Line, Home, Pref, Work, Intl, Parcel, Postal, - Dom) - end; -decode_vcard_LABEL_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"POSTAL">>, _attrs, _} = _el | _els], Line, - Home, Pref, Work, Intl, Parcel, Postal, Dom) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_LABEL_els(__TopXMLNS, __IgnoreEls, _els, - Line, Home, Pref, Work, Intl, Parcel, - decode_vcard_POSTAL(__TopXMLNS, __IgnoreEls, - _el), - Dom); - true -> - decode_vcard_LABEL_els(__TopXMLNS, __IgnoreEls, _els, - Line, Home, Pref, Work, Intl, Parcel, Postal, - Dom) - end; -decode_vcard_LABEL_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"PARCEL">>, _attrs, _} = _el | _els], Line, - Home, Pref, Work, Intl, Parcel, Postal, Dom) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_LABEL_els(__TopXMLNS, __IgnoreEls, _els, - Line, Home, Pref, Work, Intl, - decode_vcard_PARCEL(__TopXMLNS, __IgnoreEls, - _el), - Postal, Dom); - true -> - decode_vcard_LABEL_els(__TopXMLNS, __IgnoreEls, _els, - Line, Home, Pref, Work, Intl, Parcel, Postal, - Dom) - end; -decode_vcard_LABEL_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"DOM">>, _attrs, _} = _el | _els], Line, - Home, Pref, Work, Intl, Parcel, Postal, Dom) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_LABEL_els(__TopXMLNS, __IgnoreEls, _els, - Line, Home, Pref, Work, Intl, Parcel, Postal, - decode_vcard_DOM(__TopXMLNS, __IgnoreEls, - _el)); - true -> - decode_vcard_LABEL_els(__TopXMLNS, __IgnoreEls, _els, - Line, Home, Pref, Work, Intl, Parcel, Postal, - Dom) - end; -decode_vcard_LABEL_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"INTL">>, _attrs, _} = _el | _els], Line, - Home, Pref, Work, Intl, Parcel, Postal, Dom) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_LABEL_els(__TopXMLNS, __IgnoreEls, _els, - Line, Home, Pref, Work, - decode_vcard_INTL(__TopXMLNS, __IgnoreEls, - _el), - Parcel, Postal, Dom); - true -> - decode_vcard_LABEL_els(__TopXMLNS, __IgnoreEls, _els, - Line, Home, Pref, Work, Intl, Parcel, Postal, - Dom) - end; -decode_vcard_LABEL_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"PREF">>, _attrs, _} = _el | _els], Line, - Home, Pref, Work, Intl, Parcel, Postal, Dom) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_LABEL_els(__TopXMLNS, __IgnoreEls, _els, - Line, Home, - decode_vcard_PREF(__TopXMLNS, __IgnoreEls, - _el), - Work, Intl, Parcel, Postal, Dom); - true -> - decode_vcard_LABEL_els(__TopXMLNS, __IgnoreEls, _els, - Line, Home, Pref, Work, Intl, Parcel, Postal, - Dom) - end; -decode_vcard_LABEL_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"LINE">>, _attrs, _} = _el | _els], Line, - Home, Pref, Work, Intl, Parcel, Postal, Dom) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_LABEL_els(__TopXMLNS, __IgnoreEls, _els, - case decode_vcard_LINE(__TopXMLNS, - __IgnoreEls, _el) - of - undefined -> Line; - _new_el -> [_new_el | Line] - end, - Home, Pref, Work, Intl, Parcel, Postal, Dom); - true -> - decode_vcard_LABEL_els(__TopXMLNS, __IgnoreEls, _els, - Line, Home, Pref, Work, Intl, Parcel, Postal, - Dom) - end; -decode_vcard_LABEL_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Line, Home, Pref, Work, Intl, Parcel, - Postal, Dom) -> - decode_vcard_LABEL_els(__TopXMLNS, __IgnoreEls, _els, - Line, Home, Pref, Work, Intl, Parcel, Postal, Dom). - -encode_vcard_LABEL({vcard_label, Home, Work, Postal, - Parcel, Dom, Intl, Pref, Line}, - _xmlns_attrs) -> - _els = lists:reverse('encode_vcard_LABEL_$line'(Line, - 'encode_vcard_LABEL_$home'(Home, - 'encode_vcard_LABEL_$pref'(Pref, - 'encode_vcard_LABEL_$work'(Work, - 'encode_vcard_LABEL_$intl'(Intl, - 'encode_vcard_LABEL_$parcel'(Parcel, - 'encode_vcard_LABEL_$postal'(Postal, - 'encode_vcard_LABEL_$dom'(Dom, - []))))))))), - _attrs = _xmlns_attrs, - {xmlel, <<"LABEL">>, _attrs, _els}. - -'encode_vcard_LABEL_$line'([], _acc) -> _acc; -'encode_vcard_LABEL_$line'([Line | _els], _acc) -> - 'encode_vcard_LABEL_$line'(_els, - [encode_vcard_LINE(Line, []) | _acc]). - -'encode_vcard_LABEL_$home'(false, _acc) -> _acc; -'encode_vcard_LABEL_$home'(Home, _acc) -> - [encode_vcard_HOME(Home, []) | _acc]. - -'encode_vcard_LABEL_$pref'(false, _acc) -> _acc; -'encode_vcard_LABEL_$pref'(Pref, _acc) -> - [encode_vcard_PREF(Pref, []) | _acc]. - -'encode_vcard_LABEL_$work'(false, _acc) -> _acc; -'encode_vcard_LABEL_$work'(Work, _acc) -> - [encode_vcard_WORK(Work, []) | _acc]. - -'encode_vcard_LABEL_$intl'(false, _acc) -> _acc; -'encode_vcard_LABEL_$intl'(Intl, _acc) -> - [encode_vcard_INTL(Intl, []) | _acc]. - -'encode_vcard_LABEL_$parcel'(false, _acc) -> _acc; -'encode_vcard_LABEL_$parcel'(Parcel, _acc) -> - [encode_vcard_PARCEL(Parcel, []) | _acc]. - -'encode_vcard_LABEL_$postal'(false, _acc) -> _acc; -'encode_vcard_LABEL_$postal'(Postal, _acc) -> - [encode_vcard_POSTAL(Postal, []) | _acc]. - -'encode_vcard_LABEL_$dom'(false, _acc) -> _acc; -'encode_vcard_LABEL_$dom'(Dom, _acc) -> - [encode_vcard_DOM(Dom, []) | _acc]. - -decode_vcard_ADR(__TopXMLNS, __IgnoreEls, - {xmlel, <<"ADR">>, _attrs, _els}) -> - {Street, Extadd, Pcode, Home, Pref, Pobox, Ctry, - Locality, Work, Intl, Parcel, Postal, Dom, Region} = - decode_vcard_ADR_els(__TopXMLNS, __IgnoreEls, _els, - undefined, undefined, undefined, false, false, - undefined, undefined, undefined, false, false, - false, false, false, undefined), - {vcard_adr, Home, Work, Postal, Parcel, Dom, Intl, Pref, - Pobox, Extadd, Street, Locality, Region, Pcode, Ctry}. - -decode_vcard_ADR_els(__TopXMLNS, __IgnoreEls, [], - Street, Extadd, Pcode, Home, Pref, Pobox, Ctry, - Locality, Work, Intl, Parcel, Postal, Dom, Region) -> - {Street, Extadd, Pcode, Home, Pref, Pobox, Ctry, - Locality, Work, Intl, Parcel, Postal, Dom, Region}; -decode_vcard_ADR_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"HOME">>, _attrs, _} = _el | _els], Street, - Extadd, Pcode, Home, Pref, Pobox, Ctry, Locality, Work, - Intl, Parcel, Postal, Dom, Region) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_ADR_els(__TopXMLNS, __IgnoreEls, _els, - Street, Extadd, Pcode, - decode_vcard_HOME(__TopXMLNS, __IgnoreEls, _el), - Pref, Pobox, Ctry, Locality, Work, Intl, Parcel, - Postal, Dom, Region); - true -> - decode_vcard_ADR_els(__TopXMLNS, __IgnoreEls, _els, - Street, Extadd, Pcode, Home, Pref, Pobox, Ctry, - Locality, Work, Intl, Parcel, Postal, Dom, - Region) - end; -decode_vcard_ADR_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"WORK">>, _attrs, _} = _el | _els], Street, - Extadd, Pcode, Home, Pref, Pobox, Ctry, Locality, Work, - Intl, Parcel, Postal, Dom, Region) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_ADR_els(__TopXMLNS, __IgnoreEls, _els, - Street, Extadd, Pcode, Home, Pref, Pobox, Ctry, - Locality, - decode_vcard_WORK(__TopXMLNS, __IgnoreEls, _el), - Intl, Parcel, Postal, Dom, Region); - true -> - decode_vcard_ADR_els(__TopXMLNS, __IgnoreEls, _els, - Street, Extadd, Pcode, Home, Pref, Pobox, Ctry, - Locality, Work, Intl, Parcel, Postal, Dom, - Region) - end; -decode_vcard_ADR_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"POSTAL">>, _attrs, _} = _el | _els], Street, - Extadd, Pcode, Home, Pref, Pobox, Ctry, Locality, Work, - Intl, Parcel, Postal, Dom, Region) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_ADR_els(__TopXMLNS, __IgnoreEls, _els, - Street, Extadd, Pcode, Home, Pref, Pobox, Ctry, - Locality, Work, Intl, Parcel, - decode_vcard_POSTAL(__TopXMLNS, __IgnoreEls, - _el), - Dom, Region); - true -> - decode_vcard_ADR_els(__TopXMLNS, __IgnoreEls, _els, - Street, Extadd, Pcode, Home, Pref, Pobox, Ctry, - Locality, Work, Intl, Parcel, Postal, Dom, - Region) - end; -decode_vcard_ADR_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"PARCEL">>, _attrs, _} = _el | _els], Street, - Extadd, Pcode, Home, Pref, Pobox, Ctry, Locality, Work, - Intl, Parcel, Postal, Dom, Region) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_ADR_els(__TopXMLNS, __IgnoreEls, _els, - Street, Extadd, Pcode, Home, Pref, Pobox, Ctry, - Locality, Work, Intl, - decode_vcard_PARCEL(__TopXMLNS, __IgnoreEls, - _el), - Postal, Dom, Region); - true -> - decode_vcard_ADR_els(__TopXMLNS, __IgnoreEls, _els, - Street, Extadd, Pcode, Home, Pref, Pobox, Ctry, - Locality, Work, Intl, Parcel, Postal, Dom, - Region) - end; -decode_vcard_ADR_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"DOM">>, _attrs, _} = _el | _els], Street, - Extadd, Pcode, Home, Pref, Pobox, Ctry, Locality, Work, - Intl, Parcel, Postal, Dom, Region) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_ADR_els(__TopXMLNS, __IgnoreEls, _els, - Street, Extadd, Pcode, Home, Pref, Pobox, Ctry, - Locality, Work, Intl, Parcel, Postal, - decode_vcard_DOM(__TopXMLNS, __IgnoreEls, _el), - Region); - true -> - decode_vcard_ADR_els(__TopXMLNS, __IgnoreEls, _els, - Street, Extadd, Pcode, Home, Pref, Pobox, Ctry, - Locality, Work, Intl, Parcel, Postal, Dom, - Region) - end; -decode_vcard_ADR_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"INTL">>, _attrs, _} = _el | _els], Street, - Extadd, Pcode, Home, Pref, Pobox, Ctry, Locality, Work, - Intl, Parcel, Postal, Dom, Region) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_ADR_els(__TopXMLNS, __IgnoreEls, _els, - Street, Extadd, Pcode, Home, Pref, Pobox, Ctry, - Locality, Work, - decode_vcard_INTL(__TopXMLNS, __IgnoreEls, _el), - Parcel, Postal, Dom, Region); - true -> - decode_vcard_ADR_els(__TopXMLNS, __IgnoreEls, _els, - Street, Extadd, Pcode, Home, Pref, Pobox, Ctry, - Locality, Work, Intl, Parcel, Postal, Dom, - Region) - end; -decode_vcard_ADR_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"PREF">>, _attrs, _} = _el | _els], Street, - Extadd, Pcode, Home, Pref, Pobox, Ctry, Locality, Work, - Intl, Parcel, Postal, Dom, Region) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_ADR_els(__TopXMLNS, __IgnoreEls, _els, - Street, Extadd, Pcode, Home, - decode_vcard_PREF(__TopXMLNS, __IgnoreEls, _el), - Pobox, Ctry, Locality, Work, Intl, Parcel, - Postal, Dom, Region); - true -> - decode_vcard_ADR_els(__TopXMLNS, __IgnoreEls, _els, - Street, Extadd, Pcode, Home, Pref, Pobox, Ctry, - Locality, Work, Intl, Parcel, Postal, Dom, - Region) - end; -decode_vcard_ADR_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"POBOX">>, _attrs, _} = _el | _els], Street, - Extadd, Pcode, Home, Pref, Pobox, Ctry, Locality, Work, - Intl, Parcel, Postal, Dom, Region) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_ADR_els(__TopXMLNS, __IgnoreEls, _els, - Street, Extadd, Pcode, Home, Pref, - decode_vcard_POBOX(__TopXMLNS, __IgnoreEls, - _el), - Ctry, Locality, Work, Intl, Parcel, Postal, Dom, - Region); - true -> - decode_vcard_ADR_els(__TopXMLNS, __IgnoreEls, _els, - Street, Extadd, Pcode, Home, Pref, Pobox, Ctry, - Locality, Work, Intl, Parcel, Postal, Dom, - Region) - end; -decode_vcard_ADR_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"EXTADD">>, _attrs, _} = _el | _els], Street, - Extadd, Pcode, Home, Pref, Pobox, Ctry, Locality, Work, - Intl, Parcel, Postal, Dom, Region) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_ADR_els(__TopXMLNS, __IgnoreEls, _els, - Street, - decode_vcard_EXTADD(__TopXMLNS, __IgnoreEls, - _el), - Pcode, Home, Pref, Pobox, Ctry, Locality, Work, - Intl, Parcel, Postal, Dom, Region); - true -> - decode_vcard_ADR_els(__TopXMLNS, __IgnoreEls, _els, - Street, Extadd, Pcode, Home, Pref, Pobox, Ctry, - Locality, Work, Intl, Parcel, Postal, Dom, - Region) - end; -decode_vcard_ADR_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"STREET">>, _attrs, _} = _el | _els], Street, - Extadd, Pcode, Home, Pref, Pobox, Ctry, Locality, Work, - Intl, Parcel, Postal, Dom, Region) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_ADR_els(__TopXMLNS, __IgnoreEls, _els, - decode_vcard_STREET(__TopXMLNS, __IgnoreEls, - _el), - Extadd, Pcode, Home, Pref, Pobox, Ctry, - Locality, Work, Intl, Parcel, Postal, Dom, - Region); - true -> - decode_vcard_ADR_els(__TopXMLNS, __IgnoreEls, _els, - Street, Extadd, Pcode, Home, Pref, Pobox, Ctry, - Locality, Work, Intl, Parcel, Postal, Dom, - Region) - end; -decode_vcard_ADR_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"LOCALITY">>, _attrs, _} = _el | _els], - Street, Extadd, Pcode, Home, Pref, Pobox, Ctry, - Locality, Work, Intl, Parcel, Postal, Dom, Region) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_ADR_els(__TopXMLNS, __IgnoreEls, _els, - Street, Extadd, Pcode, Home, Pref, Pobox, Ctry, - decode_vcard_LOCALITY(__TopXMLNS, __IgnoreEls, - _el), - Work, Intl, Parcel, Postal, Dom, Region); - true -> - decode_vcard_ADR_els(__TopXMLNS, __IgnoreEls, _els, - Street, Extadd, Pcode, Home, Pref, Pobox, Ctry, - Locality, Work, Intl, Parcel, Postal, Dom, - Region) - end; -decode_vcard_ADR_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"REGION">>, _attrs, _} = _el | _els], Street, - Extadd, Pcode, Home, Pref, Pobox, Ctry, Locality, Work, - Intl, Parcel, Postal, Dom, Region) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_ADR_els(__TopXMLNS, __IgnoreEls, _els, - Street, Extadd, Pcode, Home, Pref, Pobox, Ctry, - Locality, Work, Intl, Parcel, Postal, Dom, - decode_vcard_REGION(__TopXMLNS, __IgnoreEls, - _el)); - true -> - decode_vcard_ADR_els(__TopXMLNS, __IgnoreEls, _els, - Street, Extadd, Pcode, Home, Pref, Pobox, Ctry, - Locality, Work, Intl, Parcel, Postal, Dom, - Region) - end; -decode_vcard_ADR_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"PCODE">>, _attrs, _} = _el | _els], Street, - Extadd, Pcode, Home, Pref, Pobox, Ctry, Locality, Work, - Intl, Parcel, Postal, Dom, Region) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_ADR_els(__TopXMLNS, __IgnoreEls, _els, - Street, Extadd, - decode_vcard_PCODE(__TopXMLNS, __IgnoreEls, - _el), - Home, Pref, Pobox, Ctry, Locality, Work, Intl, - Parcel, Postal, Dom, Region); - true -> - decode_vcard_ADR_els(__TopXMLNS, __IgnoreEls, _els, - Street, Extadd, Pcode, Home, Pref, Pobox, Ctry, - Locality, Work, Intl, Parcel, Postal, Dom, - Region) - end; -decode_vcard_ADR_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"CTRY">>, _attrs, _} = _el | _els], Street, - Extadd, Pcode, Home, Pref, Pobox, Ctry, Locality, Work, - Intl, Parcel, Postal, Dom, Region) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_ADR_els(__TopXMLNS, __IgnoreEls, _els, - Street, Extadd, Pcode, Home, Pref, Pobox, - decode_vcard_CTRY(__TopXMLNS, __IgnoreEls, _el), - Locality, Work, Intl, Parcel, Postal, Dom, - Region); - true -> - decode_vcard_ADR_els(__TopXMLNS, __IgnoreEls, _els, - Street, Extadd, Pcode, Home, Pref, Pobox, Ctry, - Locality, Work, Intl, Parcel, Postal, Dom, - Region) - end; -decode_vcard_ADR_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Street, Extadd, Pcode, Home, Pref, Pobox, - Ctry, Locality, Work, Intl, Parcel, Postal, Dom, - Region) -> - decode_vcard_ADR_els(__TopXMLNS, __IgnoreEls, _els, - Street, Extadd, Pcode, Home, Pref, Pobox, Ctry, - Locality, Work, Intl, Parcel, Postal, Dom, Region). - -encode_vcard_ADR({vcard_adr, Home, Work, Postal, Parcel, - Dom, Intl, Pref, Pobox, Extadd, Street, Locality, - Region, Pcode, Ctry}, - _xmlns_attrs) -> - _els = lists:reverse('encode_vcard_ADR_$street'(Street, - 'encode_vcard_ADR_$extadd'(Extadd, - 'encode_vcard_ADR_$pcode'(Pcode, - 'encode_vcard_ADR_$home'(Home, - 'encode_vcard_ADR_$pref'(Pref, - 'encode_vcard_ADR_$pobox'(Pobox, - 'encode_vcard_ADR_$ctry'(Ctry, - 'encode_vcard_ADR_$locality'(Locality, - 'encode_vcard_ADR_$work'(Work, - 'encode_vcard_ADR_$intl'(Intl, - 'encode_vcard_ADR_$parcel'(Parcel, - 'encode_vcard_ADR_$postal'(Postal, - 'encode_vcard_ADR_$dom'(Dom, - 'encode_vcard_ADR_$region'(Region, - []))))))))))))))), - _attrs = _xmlns_attrs, - {xmlel, <<"ADR">>, _attrs, _els}. - -'encode_vcard_ADR_$street'(undefined, _acc) -> _acc; -'encode_vcard_ADR_$street'(Street, _acc) -> - [encode_vcard_STREET(Street, []) | _acc]. - -'encode_vcard_ADR_$extadd'(undefined, _acc) -> _acc; -'encode_vcard_ADR_$extadd'(Extadd, _acc) -> - [encode_vcard_EXTADD(Extadd, []) | _acc]. - -'encode_vcard_ADR_$pcode'(undefined, _acc) -> _acc; -'encode_vcard_ADR_$pcode'(Pcode, _acc) -> - [encode_vcard_PCODE(Pcode, []) | _acc]. - -'encode_vcard_ADR_$home'(false, _acc) -> _acc; -'encode_vcard_ADR_$home'(Home, _acc) -> - [encode_vcard_HOME(Home, []) | _acc]. - -'encode_vcard_ADR_$pref'(false, _acc) -> _acc; -'encode_vcard_ADR_$pref'(Pref, _acc) -> - [encode_vcard_PREF(Pref, []) | _acc]. - -'encode_vcard_ADR_$pobox'(undefined, _acc) -> _acc; -'encode_vcard_ADR_$pobox'(Pobox, _acc) -> - [encode_vcard_POBOX(Pobox, []) | _acc]. - -'encode_vcard_ADR_$ctry'(undefined, _acc) -> _acc; -'encode_vcard_ADR_$ctry'(Ctry, _acc) -> - [encode_vcard_CTRY(Ctry, []) | _acc]. - -'encode_vcard_ADR_$locality'(undefined, _acc) -> _acc; -'encode_vcard_ADR_$locality'(Locality, _acc) -> - [encode_vcard_LOCALITY(Locality, []) | _acc]. - -'encode_vcard_ADR_$work'(false, _acc) -> _acc; -'encode_vcard_ADR_$work'(Work, _acc) -> - [encode_vcard_WORK(Work, []) | _acc]. - -'encode_vcard_ADR_$intl'(false, _acc) -> _acc; -'encode_vcard_ADR_$intl'(Intl, _acc) -> - [encode_vcard_INTL(Intl, []) | _acc]. - -'encode_vcard_ADR_$parcel'(false, _acc) -> _acc; -'encode_vcard_ADR_$parcel'(Parcel, _acc) -> - [encode_vcard_PARCEL(Parcel, []) | _acc]. - -'encode_vcard_ADR_$postal'(false, _acc) -> _acc; -'encode_vcard_ADR_$postal'(Postal, _acc) -> - [encode_vcard_POSTAL(Postal, []) | _acc]. - -'encode_vcard_ADR_$dom'(false, _acc) -> _acc; -'encode_vcard_ADR_$dom'(Dom, _acc) -> - [encode_vcard_DOM(Dom, []) | _acc]. - -'encode_vcard_ADR_$region'(undefined, _acc) -> _acc; -'encode_vcard_ADR_$region'(Region, _acc) -> - [encode_vcard_REGION(Region, []) | _acc]. - -decode_vcard_N(__TopXMLNS, __IgnoreEls, - {xmlel, <<"N">>, _attrs, _els}) -> - {Middle, Suffix, Prefix, Family, Given} = - decode_vcard_N_els(__TopXMLNS, __IgnoreEls, _els, - undefined, undefined, undefined, undefined, - undefined), - {vcard_name, Family, Given, Middle, Prefix, Suffix}. - -decode_vcard_N_els(__TopXMLNS, __IgnoreEls, [], Middle, - Suffix, Prefix, Family, Given) -> - {Middle, Suffix, Prefix, Family, Given}; -decode_vcard_N_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"FAMILY">>, _attrs, _} = _el | _els], Middle, - Suffix, Prefix, Family, Given) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_N_els(__TopXMLNS, __IgnoreEls, _els, - Middle, Suffix, Prefix, - decode_vcard_FAMILY(__TopXMLNS, __IgnoreEls, _el), - Given); - true -> - decode_vcard_N_els(__TopXMLNS, __IgnoreEls, _els, - Middle, Suffix, Prefix, Family, Given) - end; -decode_vcard_N_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"GIVEN">>, _attrs, _} = _el | _els], Middle, - Suffix, Prefix, Family, Given) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_N_els(__TopXMLNS, __IgnoreEls, _els, - Middle, Suffix, Prefix, Family, - decode_vcard_GIVEN(__TopXMLNS, __IgnoreEls, _el)); - true -> - decode_vcard_N_els(__TopXMLNS, __IgnoreEls, _els, - Middle, Suffix, Prefix, Family, Given) - end; -decode_vcard_N_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"MIDDLE">>, _attrs, _} = _el | _els], Middle, - Suffix, Prefix, Family, Given) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_N_els(__TopXMLNS, __IgnoreEls, _els, - decode_vcard_MIDDLE(__TopXMLNS, __IgnoreEls, _el), - Suffix, Prefix, Family, Given); - true -> - decode_vcard_N_els(__TopXMLNS, __IgnoreEls, _els, - Middle, Suffix, Prefix, Family, Given) - end; -decode_vcard_N_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"PREFIX">>, _attrs, _} = _el | _els], Middle, - Suffix, Prefix, Family, Given) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_N_els(__TopXMLNS, __IgnoreEls, _els, - Middle, Suffix, - decode_vcard_PREFIX(__TopXMLNS, __IgnoreEls, _el), - Family, Given); - true -> - decode_vcard_N_els(__TopXMLNS, __IgnoreEls, _els, - Middle, Suffix, Prefix, Family, Given) - end; -decode_vcard_N_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"SUFFIX">>, _attrs, _} = _el | _els], Middle, - Suffix, Prefix, Family, Given) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_vcard_N_els(__TopXMLNS, __IgnoreEls, _els, - Middle, - decode_vcard_SUFFIX(__TopXMLNS, __IgnoreEls, _el), - Prefix, Family, Given); - true -> - decode_vcard_N_els(__TopXMLNS, __IgnoreEls, _els, - Middle, Suffix, Prefix, Family, Given) - end; -decode_vcard_N_els(__TopXMLNS, __IgnoreEls, [_ | _els], - Middle, Suffix, Prefix, Family, Given) -> - decode_vcard_N_els(__TopXMLNS, __IgnoreEls, _els, - Middle, Suffix, Prefix, Family, Given). - -encode_vcard_N({vcard_name, Family, Given, Middle, - Prefix, Suffix}, - _xmlns_attrs) -> - _els = lists:reverse('encode_vcard_N_$middle'(Middle, - 'encode_vcard_N_$suffix'(Suffix, - 'encode_vcard_N_$prefix'(Prefix, - 'encode_vcard_N_$family'(Family, - 'encode_vcard_N_$given'(Given, - [])))))), - _attrs = _xmlns_attrs, - {xmlel, <<"N">>, _attrs, _els}. - -'encode_vcard_N_$middle'(undefined, _acc) -> _acc; -'encode_vcard_N_$middle'(Middle, _acc) -> - [encode_vcard_MIDDLE(Middle, []) | _acc]. - -'encode_vcard_N_$suffix'(undefined, _acc) -> _acc; -'encode_vcard_N_$suffix'(Suffix, _acc) -> - [encode_vcard_SUFFIX(Suffix, []) | _acc]. - -'encode_vcard_N_$prefix'(undefined, _acc) -> _acc; -'encode_vcard_N_$prefix'(Prefix, _acc) -> - [encode_vcard_PREFIX(Prefix, []) | _acc]. - -'encode_vcard_N_$family'(undefined, _acc) -> _acc; -'encode_vcard_N_$family'(Family, _acc) -> - [encode_vcard_FAMILY(Family, []) | _acc]. - -'encode_vcard_N_$given'(undefined, _acc) -> _acc; -'encode_vcard_N_$given'(Given, _acc) -> - [encode_vcard_GIVEN(Given, []) | _acc]. - -decode_vcard_CONFIDENTIAL(__TopXMLNS, __IgnoreEls, - {xmlel, <<"CONFIDENTIAL">>, _attrs, _els}) -> - confidential. - -encode_vcard_CONFIDENTIAL(confidential, _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"CONFIDENTIAL">>, _attrs, _els}. - -decode_vcard_PRIVATE(__TopXMLNS, __IgnoreEls, - {xmlel, <<"PRIVATE">>, _attrs, _els}) -> - private. - -encode_vcard_PRIVATE(private, _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"PRIVATE">>, _attrs, _els}. - -decode_vcard_PUBLIC(__TopXMLNS, __IgnoreEls, - {xmlel, <<"PUBLIC">>, _attrs, _els}) -> - public. - -encode_vcard_PUBLIC(public, _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"PUBLIC">>, _attrs, _els}. - -decode_vcard_EXTVAL(__TopXMLNS, __IgnoreEls, - {xmlel, <<"EXTVAL">>, _attrs, _els}) -> - Cdata = decode_vcard_EXTVAL_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - Cdata. - -decode_vcard_EXTVAL_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_vcard_EXTVAL_cdata(__TopXMLNS, Cdata); -decode_vcard_EXTVAL_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_vcard_EXTVAL_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_vcard_EXTVAL_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_vcard_EXTVAL_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_vcard_EXTVAL(Cdata, _xmlns_attrs) -> - _els = encode_vcard_EXTVAL_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"EXTVAL">>, _attrs, _els}. - -decode_vcard_EXTVAL_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_vcard_EXTVAL_cdata(__TopXMLNS, _val) -> _val. - -encode_vcard_EXTVAL_cdata(undefined, _acc) -> _acc; -encode_vcard_EXTVAL_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_vcard_TYPE(__TopXMLNS, __IgnoreEls, - {xmlel, <<"TYPE">>, _attrs, _els}) -> - Cdata = decode_vcard_TYPE_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - Cdata. - -decode_vcard_TYPE_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_vcard_TYPE_cdata(__TopXMLNS, Cdata); -decode_vcard_TYPE_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_vcard_TYPE_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_vcard_TYPE_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_vcard_TYPE_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_vcard_TYPE(Cdata, _xmlns_attrs) -> - _els = encode_vcard_TYPE_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"TYPE">>, _attrs, _els}. - -decode_vcard_TYPE_cdata(__TopXMLNS, <<>>) -> undefined; -decode_vcard_TYPE_cdata(__TopXMLNS, _val) -> _val. - -encode_vcard_TYPE_cdata(undefined, _acc) -> _acc; -encode_vcard_TYPE_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_vcard_DESC(__TopXMLNS, __IgnoreEls, - {xmlel, <<"DESC">>, _attrs, _els}) -> - Cdata = decode_vcard_DESC_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - Cdata. - -decode_vcard_DESC_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_vcard_DESC_cdata(__TopXMLNS, Cdata); -decode_vcard_DESC_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_vcard_DESC_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_vcard_DESC_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_vcard_DESC_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_vcard_DESC(Cdata, _xmlns_attrs) -> - _els = encode_vcard_DESC_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"DESC">>, _attrs, _els}. - -decode_vcard_DESC_cdata(__TopXMLNS, <<>>) -> undefined; -decode_vcard_DESC_cdata(__TopXMLNS, _val) -> _val. - -encode_vcard_DESC_cdata(undefined, _acc) -> _acc; -encode_vcard_DESC_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_vcard_URL(__TopXMLNS, __IgnoreEls, - {xmlel, <<"URL">>, _attrs, _els}) -> - Cdata = decode_vcard_URL_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - Cdata. - -decode_vcard_URL_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_vcard_URL_cdata(__TopXMLNS, Cdata); -decode_vcard_URL_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_vcard_URL_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_vcard_URL_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_vcard_URL_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_vcard_URL(Cdata, _xmlns_attrs) -> - _els = encode_vcard_URL_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"URL">>, _attrs, _els}. - -decode_vcard_URL_cdata(__TopXMLNS, <<>>) -> undefined; -decode_vcard_URL_cdata(__TopXMLNS, _val) -> _val. - -encode_vcard_URL_cdata(undefined, _acc) -> _acc; -encode_vcard_URL_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_vcard_UID(__TopXMLNS, __IgnoreEls, - {xmlel, <<"UID">>, _attrs, _els}) -> - Cdata = decode_vcard_UID_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - Cdata. - -decode_vcard_UID_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_vcard_UID_cdata(__TopXMLNS, Cdata); -decode_vcard_UID_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_vcard_UID_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_vcard_UID_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_vcard_UID_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_vcard_UID(Cdata, _xmlns_attrs) -> - _els = encode_vcard_UID_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"UID">>, _attrs, _els}. - -decode_vcard_UID_cdata(__TopXMLNS, <<>>) -> undefined; -decode_vcard_UID_cdata(__TopXMLNS, _val) -> _val. - -encode_vcard_UID_cdata(undefined, _acc) -> _acc; -encode_vcard_UID_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_vcard_SORT_STRING(__TopXMLNS, __IgnoreEls, - {xmlel, <<"SORT-STRING">>, _attrs, _els}) -> - Cdata = decode_vcard_SORT_STRING_els(__TopXMLNS, - __IgnoreEls, _els, <<>>), - Cdata. - -decode_vcard_SORT_STRING_els(__TopXMLNS, __IgnoreEls, - [], Cdata) -> - decode_vcard_SORT_STRING_cdata(__TopXMLNS, Cdata); -decode_vcard_SORT_STRING_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_vcard_SORT_STRING_els(__TopXMLNS, __IgnoreEls, - _els, <<Cdata/binary, _data/binary>>); -decode_vcard_SORT_STRING_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_vcard_SORT_STRING_els(__TopXMLNS, __IgnoreEls, - _els, Cdata). - -encode_vcard_SORT_STRING(Cdata, _xmlns_attrs) -> - _els = encode_vcard_SORT_STRING_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"SORT-STRING">>, _attrs, _els}. - -decode_vcard_SORT_STRING_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_vcard_SORT_STRING_cdata(__TopXMLNS, _val) -> - _val. - -encode_vcard_SORT_STRING_cdata(undefined, _acc) -> _acc; -encode_vcard_SORT_STRING_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_vcard_REV(__TopXMLNS, __IgnoreEls, - {xmlel, <<"REV">>, _attrs, _els}) -> - Cdata = decode_vcard_REV_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - Cdata. - -decode_vcard_REV_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_vcard_REV_cdata(__TopXMLNS, Cdata); -decode_vcard_REV_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_vcard_REV_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_vcard_REV_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_vcard_REV_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_vcard_REV(Cdata, _xmlns_attrs) -> - _els = encode_vcard_REV_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"REV">>, _attrs, _els}. - -decode_vcard_REV_cdata(__TopXMLNS, <<>>) -> undefined; -decode_vcard_REV_cdata(__TopXMLNS, _val) -> _val. - -encode_vcard_REV_cdata(undefined, _acc) -> _acc; -encode_vcard_REV_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_vcard_PRODID(__TopXMLNS, __IgnoreEls, - {xmlel, <<"PRODID">>, _attrs, _els}) -> - Cdata = decode_vcard_PRODID_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - Cdata. - -decode_vcard_PRODID_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_vcard_PRODID_cdata(__TopXMLNS, Cdata); -decode_vcard_PRODID_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_vcard_PRODID_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_vcard_PRODID_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_vcard_PRODID_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_vcard_PRODID(Cdata, _xmlns_attrs) -> - _els = encode_vcard_PRODID_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"PRODID">>, _attrs, _els}. - -decode_vcard_PRODID_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_vcard_PRODID_cdata(__TopXMLNS, _val) -> _val. - -encode_vcard_PRODID_cdata(undefined, _acc) -> _acc; -encode_vcard_PRODID_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_vcard_NOTE(__TopXMLNS, __IgnoreEls, - {xmlel, <<"NOTE">>, _attrs, _els}) -> - Cdata = decode_vcard_NOTE_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - Cdata. - -decode_vcard_NOTE_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_vcard_NOTE_cdata(__TopXMLNS, Cdata); -decode_vcard_NOTE_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_vcard_NOTE_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_vcard_NOTE_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_vcard_NOTE_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_vcard_NOTE(Cdata, _xmlns_attrs) -> - _els = encode_vcard_NOTE_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"NOTE">>, _attrs, _els}. - -decode_vcard_NOTE_cdata(__TopXMLNS, <<>>) -> undefined; -decode_vcard_NOTE_cdata(__TopXMLNS, _val) -> _val. - -encode_vcard_NOTE_cdata(undefined, _acc) -> _acc; -encode_vcard_NOTE_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_vcard_KEYWORD(__TopXMLNS, __IgnoreEls, - {xmlel, <<"KEYWORD">>, _attrs, _els}) -> - Cdata = decode_vcard_KEYWORD_els(__TopXMLNS, - __IgnoreEls, _els, <<>>), - Cdata. - -decode_vcard_KEYWORD_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_vcard_KEYWORD_cdata(__TopXMLNS, Cdata); -decode_vcard_KEYWORD_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_vcard_KEYWORD_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_vcard_KEYWORD_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_vcard_KEYWORD_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_vcard_KEYWORD(Cdata, _xmlns_attrs) -> - _els = encode_vcard_KEYWORD_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"KEYWORD">>, _attrs, _els}. - -decode_vcard_KEYWORD_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_vcard_KEYWORD_cdata(__TopXMLNS, _val) -> _val. - -encode_vcard_KEYWORD_cdata(undefined, _acc) -> _acc; -encode_vcard_KEYWORD_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_vcard_ROLE(__TopXMLNS, __IgnoreEls, - {xmlel, <<"ROLE">>, _attrs, _els}) -> - Cdata = decode_vcard_ROLE_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - Cdata. - -decode_vcard_ROLE_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_vcard_ROLE_cdata(__TopXMLNS, Cdata); -decode_vcard_ROLE_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_vcard_ROLE_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_vcard_ROLE_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_vcard_ROLE_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_vcard_ROLE(Cdata, _xmlns_attrs) -> - _els = encode_vcard_ROLE_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"ROLE">>, _attrs, _els}. - -decode_vcard_ROLE_cdata(__TopXMLNS, <<>>) -> undefined; -decode_vcard_ROLE_cdata(__TopXMLNS, _val) -> _val. - -encode_vcard_ROLE_cdata(undefined, _acc) -> _acc; -encode_vcard_ROLE_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_vcard_TITLE(__TopXMLNS, __IgnoreEls, - {xmlel, <<"TITLE">>, _attrs, _els}) -> - Cdata = decode_vcard_TITLE_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - Cdata. - -decode_vcard_TITLE_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_vcard_TITLE_cdata(__TopXMLNS, Cdata); -decode_vcard_TITLE_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_vcard_TITLE_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_vcard_TITLE_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_vcard_TITLE_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_vcard_TITLE(Cdata, _xmlns_attrs) -> - _els = encode_vcard_TITLE_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"TITLE">>, _attrs, _els}. - -decode_vcard_TITLE_cdata(__TopXMLNS, <<>>) -> undefined; -decode_vcard_TITLE_cdata(__TopXMLNS, _val) -> _val. - -encode_vcard_TITLE_cdata(undefined, _acc) -> _acc; -encode_vcard_TITLE_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_vcard_TZ(__TopXMLNS, __IgnoreEls, - {xmlel, <<"TZ">>, _attrs, _els}) -> - Cdata = decode_vcard_TZ_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - Cdata. - -decode_vcard_TZ_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_vcard_TZ_cdata(__TopXMLNS, Cdata); -decode_vcard_TZ_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_vcard_TZ_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_vcard_TZ_els(__TopXMLNS, __IgnoreEls, [_ | _els], - Cdata) -> - decode_vcard_TZ_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_vcard_TZ(Cdata, _xmlns_attrs) -> - _els = encode_vcard_TZ_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"TZ">>, _attrs, _els}. - -decode_vcard_TZ_cdata(__TopXMLNS, <<>>) -> undefined; -decode_vcard_TZ_cdata(__TopXMLNS, _val) -> _val. - -encode_vcard_TZ_cdata(undefined, _acc) -> _acc; -encode_vcard_TZ_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_vcard_MAILER(__TopXMLNS, __IgnoreEls, - {xmlel, <<"MAILER">>, _attrs, _els}) -> - Cdata = decode_vcard_MAILER_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - Cdata. - -decode_vcard_MAILER_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_vcard_MAILER_cdata(__TopXMLNS, Cdata); -decode_vcard_MAILER_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_vcard_MAILER_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_vcard_MAILER_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_vcard_MAILER_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_vcard_MAILER(Cdata, _xmlns_attrs) -> - _els = encode_vcard_MAILER_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"MAILER">>, _attrs, _els}. - -decode_vcard_MAILER_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_vcard_MAILER_cdata(__TopXMLNS, _val) -> _val. - -encode_vcard_MAILER_cdata(undefined, _acc) -> _acc; -encode_vcard_MAILER_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_vcard_JABBERID(__TopXMLNS, __IgnoreEls, - {xmlel, <<"JABBERID">>, _attrs, _els}) -> - Cdata = decode_vcard_JABBERID_els(__TopXMLNS, - __IgnoreEls, _els, <<>>), - Cdata. - -decode_vcard_JABBERID_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_vcard_JABBERID_cdata(__TopXMLNS, Cdata); -decode_vcard_JABBERID_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_vcard_JABBERID_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_vcard_JABBERID_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_vcard_JABBERID_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_vcard_JABBERID(Cdata, _xmlns_attrs) -> - _els = encode_vcard_JABBERID_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"JABBERID">>, _attrs, _els}. - -decode_vcard_JABBERID_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_vcard_JABBERID_cdata(__TopXMLNS, _val) -> _val. - -encode_vcard_JABBERID_cdata(undefined, _acc) -> _acc; -encode_vcard_JABBERID_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_vcard_BDAY(__TopXMLNS, __IgnoreEls, - {xmlel, <<"BDAY">>, _attrs, _els}) -> - Cdata = decode_vcard_BDAY_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - Cdata. - -decode_vcard_BDAY_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_vcard_BDAY_cdata(__TopXMLNS, Cdata); -decode_vcard_BDAY_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_vcard_BDAY_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_vcard_BDAY_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_vcard_BDAY_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_vcard_BDAY(Cdata, _xmlns_attrs) -> - _els = encode_vcard_BDAY_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"BDAY">>, _attrs, _els}. - -decode_vcard_BDAY_cdata(__TopXMLNS, <<>>) -> undefined; -decode_vcard_BDAY_cdata(__TopXMLNS, _val) -> _val. - -encode_vcard_BDAY_cdata(undefined, _acc) -> _acc; -encode_vcard_BDAY_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_vcard_NICKNAME(__TopXMLNS, __IgnoreEls, - {xmlel, <<"NICKNAME">>, _attrs, _els}) -> - Cdata = decode_vcard_NICKNAME_els(__TopXMLNS, - __IgnoreEls, _els, <<>>), - Cdata. - -decode_vcard_NICKNAME_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_vcard_NICKNAME_cdata(__TopXMLNS, Cdata); -decode_vcard_NICKNAME_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_vcard_NICKNAME_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_vcard_NICKNAME_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_vcard_NICKNAME_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_vcard_NICKNAME(Cdata, _xmlns_attrs) -> - _els = encode_vcard_NICKNAME_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"NICKNAME">>, _attrs, _els}. - -decode_vcard_NICKNAME_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_vcard_NICKNAME_cdata(__TopXMLNS, _val) -> _val. - -encode_vcard_NICKNAME_cdata(undefined, _acc) -> _acc; -encode_vcard_NICKNAME_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_vcard_FN(__TopXMLNS, __IgnoreEls, - {xmlel, <<"FN">>, _attrs, _els}) -> - Cdata = decode_vcard_FN_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - Cdata. - -decode_vcard_FN_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_vcard_FN_cdata(__TopXMLNS, Cdata); -decode_vcard_FN_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_vcard_FN_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_vcard_FN_els(__TopXMLNS, __IgnoreEls, [_ | _els], - Cdata) -> - decode_vcard_FN_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_vcard_FN(Cdata, _xmlns_attrs) -> - _els = encode_vcard_FN_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"FN">>, _attrs, _els}. - -decode_vcard_FN_cdata(__TopXMLNS, <<>>) -> undefined; -decode_vcard_FN_cdata(__TopXMLNS, _val) -> _val. - -encode_vcard_FN_cdata(undefined, _acc) -> _acc; -encode_vcard_FN_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_vcard_VERSION(__TopXMLNS, __IgnoreEls, - {xmlel, <<"VERSION">>, _attrs, _els}) -> - Cdata = decode_vcard_VERSION_els(__TopXMLNS, - __IgnoreEls, _els, <<>>), - Cdata. - -decode_vcard_VERSION_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_vcard_VERSION_cdata(__TopXMLNS, Cdata); -decode_vcard_VERSION_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_vcard_VERSION_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_vcard_VERSION_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_vcard_VERSION_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_vcard_VERSION(Cdata, _xmlns_attrs) -> - _els = encode_vcard_VERSION_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"VERSION">>, _attrs, _els}. - -decode_vcard_VERSION_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_vcard_VERSION_cdata(__TopXMLNS, _val) -> _val. - -encode_vcard_VERSION_cdata(undefined, _acc) -> _acc; -encode_vcard_VERSION_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_vcard_CRED(__TopXMLNS, __IgnoreEls, - {xmlel, <<"CRED">>, _attrs, _els}) -> - Cdata = decode_vcard_CRED_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - Cdata. - -decode_vcard_CRED_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_vcard_CRED_cdata(__TopXMLNS, Cdata); -decode_vcard_CRED_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_vcard_CRED_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_vcard_CRED_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_vcard_CRED_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_vcard_CRED(Cdata, _xmlns_attrs) -> - _els = encode_vcard_CRED_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"CRED">>, _attrs, _els}. - -decode_vcard_CRED_cdata(__TopXMLNS, <<>>) -> undefined; -decode_vcard_CRED_cdata(__TopXMLNS, _val) -> _val. - -encode_vcard_CRED_cdata(undefined, _acc) -> _acc; -encode_vcard_CRED_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_vcard_PHONETIC(__TopXMLNS, __IgnoreEls, - {xmlel, <<"PHONETIC">>, _attrs, _els}) -> - Cdata = decode_vcard_PHONETIC_els(__TopXMLNS, - __IgnoreEls, _els, <<>>), - Cdata. - -decode_vcard_PHONETIC_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_vcard_PHONETIC_cdata(__TopXMLNS, Cdata); -decode_vcard_PHONETIC_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_vcard_PHONETIC_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_vcard_PHONETIC_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_vcard_PHONETIC_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_vcard_PHONETIC(Cdata, _xmlns_attrs) -> - _els = encode_vcard_PHONETIC_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"PHONETIC">>, _attrs, _els}. - -decode_vcard_PHONETIC_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_vcard_PHONETIC_cdata(__TopXMLNS, _val) -> _val. - -encode_vcard_PHONETIC_cdata(undefined, _acc) -> _acc; -encode_vcard_PHONETIC_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_vcard_ORGUNIT(__TopXMLNS, __IgnoreEls, - {xmlel, <<"ORGUNIT">>, _attrs, _els}) -> - Cdata = decode_vcard_ORGUNIT_els(__TopXMLNS, - __IgnoreEls, _els, <<>>), - Cdata. - -decode_vcard_ORGUNIT_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_vcard_ORGUNIT_cdata(__TopXMLNS, Cdata); -decode_vcard_ORGUNIT_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_vcard_ORGUNIT_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_vcard_ORGUNIT_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_vcard_ORGUNIT_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_vcard_ORGUNIT(Cdata, _xmlns_attrs) -> - _els = encode_vcard_ORGUNIT_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"ORGUNIT">>, _attrs, _els}. - -decode_vcard_ORGUNIT_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_vcard_ORGUNIT_cdata(__TopXMLNS, _val) -> _val. - -encode_vcard_ORGUNIT_cdata(undefined, _acc) -> _acc; -encode_vcard_ORGUNIT_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_vcard_ORGNAME(__TopXMLNS, __IgnoreEls, - {xmlel, <<"ORGNAME">>, _attrs, _els}) -> - Cdata = decode_vcard_ORGNAME_els(__TopXMLNS, - __IgnoreEls, _els, <<>>), - Cdata. - -decode_vcard_ORGNAME_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_vcard_ORGNAME_cdata(__TopXMLNS, Cdata); -decode_vcard_ORGNAME_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_vcard_ORGNAME_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_vcard_ORGNAME_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_vcard_ORGNAME_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_vcard_ORGNAME(Cdata, _xmlns_attrs) -> - _els = encode_vcard_ORGNAME_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"ORGNAME">>, _attrs, _els}. - -decode_vcard_ORGNAME_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_vcard_ORGNAME_cdata(__TopXMLNS, _val) -> _val. - -encode_vcard_ORGNAME_cdata(undefined, _acc) -> _acc; -encode_vcard_ORGNAME_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_vcard_LON(__TopXMLNS, __IgnoreEls, - {xmlel, <<"LON">>, _attrs, _els}) -> - Cdata = decode_vcard_LON_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - Cdata. - -decode_vcard_LON_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_vcard_LON_cdata(__TopXMLNS, Cdata); -decode_vcard_LON_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_vcard_LON_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_vcard_LON_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_vcard_LON_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_vcard_LON(Cdata, _xmlns_attrs) -> - _els = encode_vcard_LON_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"LON">>, _attrs, _els}. - -decode_vcard_LON_cdata(__TopXMLNS, <<>>) -> undefined; -decode_vcard_LON_cdata(__TopXMLNS, _val) -> _val. - -encode_vcard_LON_cdata(undefined, _acc) -> _acc; -encode_vcard_LON_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_vcard_LAT(__TopXMLNS, __IgnoreEls, - {xmlel, <<"LAT">>, _attrs, _els}) -> - Cdata = decode_vcard_LAT_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - Cdata. - -decode_vcard_LAT_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_vcard_LAT_cdata(__TopXMLNS, Cdata); -decode_vcard_LAT_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_vcard_LAT_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_vcard_LAT_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_vcard_LAT_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_vcard_LAT(Cdata, _xmlns_attrs) -> - _els = encode_vcard_LAT_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"LAT">>, _attrs, _els}. - -decode_vcard_LAT_cdata(__TopXMLNS, <<>>) -> undefined; -decode_vcard_LAT_cdata(__TopXMLNS, _val) -> _val. - -encode_vcard_LAT_cdata(undefined, _acc) -> _acc; -encode_vcard_LAT_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_vcard_USERID(__TopXMLNS, __IgnoreEls, - {xmlel, <<"USERID">>, _attrs, _els}) -> - Cdata = decode_vcard_USERID_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - Cdata. - -decode_vcard_USERID_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_vcard_USERID_cdata(__TopXMLNS, Cdata); -decode_vcard_USERID_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_vcard_USERID_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_vcard_USERID_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_vcard_USERID_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_vcard_USERID(Cdata, _xmlns_attrs) -> - _els = encode_vcard_USERID_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"USERID">>, _attrs, _els}. - -decode_vcard_USERID_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_vcard_USERID_cdata(__TopXMLNS, _val) -> _val. - -encode_vcard_USERID_cdata(undefined, _acc) -> _acc; -encode_vcard_USERID_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_vcard_NUMBER(__TopXMLNS, __IgnoreEls, - {xmlel, <<"NUMBER">>, _attrs, _els}) -> - Cdata = decode_vcard_NUMBER_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - Cdata. - -decode_vcard_NUMBER_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_vcard_NUMBER_cdata(__TopXMLNS, Cdata); -decode_vcard_NUMBER_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_vcard_NUMBER_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_vcard_NUMBER_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_vcard_NUMBER_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_vcard_NUMBER(Cdata, _xmlns_attrs) -> - _els = encode_vcard_NUMBER_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"NUMBER">>, _attrs, _els}. - -decode_vcard_NUMBER_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_vcard_NUMBER_cdata(__TopXMLNS, _val) -> _val. - -encode_vcard_NUMBER_cdata(undefined, _acc) -> _acc; -encode_vcard_NUMBER_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_vcard_LINE(__TopXMLNS, __IgnoreEls, - {xmlel, <<"LINE">>, _attrs, _els}) -> - Cdata = decode_vcard_LINE_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - Cdata. - -decode_vcard_LINE_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_vcard_LINE_cdata(__TopXMLNS, Cdata); -decode_vcard_LINE_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_vcard_LINE_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_vcard_LINE_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_vcard_LINE_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_vcard_LINE(Cdata, _xmlns_attrs) -> - _els = encode_vcard_LINE_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"LINE">>, _attrs, _els}. - -decode_vcard_LINE_cdata(__TopXMLNS, <<>>) -> undefined; -decode_vcard_LINE_cdata(__TopXMLNS, _val) -> _val. - -encode_vcard_LINE_cdata(undefined, _acc) -> _acc; -encode_vcard_LINE_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_vcard_CTRY(__TopXMLNS, __IgnoreEls, - {xmlel, <<"CTRY">>, _attrs, _els}) -> - Cdata = decode_vcard_CTRY_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - Cdata. - -decode_vcard_CTRY_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_vcard_CTRY_cdata(__TopXMLNS, Cdata); -decode_vcard_CTRY_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_vcard_CTRY_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_vcard_CTRY_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_vcard_CTRY_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_vcard_CTRY(Cdata, _xmlns_attrs) -> - _els = encode_vcard_CTRY_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"CTRY">>, _attrs, _els}. - -decode_vcard_CTRY_cdata(__TopXMLNS, <<>>) -> undefined; -decode_vcard_CTRY_cdata(__TopXMLNS, _val) -> _val. - -encode_vcard_CTRY_cdata(undefined, _acc) -> _acc; -encode_vcard_CTRY_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_vcard_PCODE(__TopXMLNS, __IgnoreEls, - {xmlel, <<"PCODE">>, _attrs, _els}) -> - Cdata = decode_vcard_PCODE_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - Cdata. - -decode_vcard_PCODE_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_vcard_PCODE_cdata(__TopXMLNS, Cdata); -decode_vcard_PCODE_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_vcard_PCODE_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_vcard_PCODE_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_vcard_PCODE_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_vcard_PCODE(Cdata, _xmlns_attrs) -> - _els = encode_vcard_PCODE_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"PCODE">>, _attrs, _els}. - -decode_vcard_PCODE_cdata(__TopXMLNS, <<>>) -> undefined; -decode_vcard_PCODE_cdata(__TopXMLNS, _val) -> _val. - -encode_vcard_PCODE_cdata(undefined, _acc) -> _acc; -encode_vcard_PCODE_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_vcard_REGION(__TopXMLNS, __IgnoreEls, - {xmlel, <<"REGION">>, _attrs, _els}) -> - Cdata = decode_vcard_REGION_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - Cdata. - -decode_vcard_REGION_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_vcard_REGION_cdata(__TopXMLNS, Cdata); -decode_vcard_REGION_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_vcard_REGION_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_vcard_REGION_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_vcard_REGION_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_vcard_REGION(Cdata, _xmlns_attrs) -> - _els = encode_vcard_REGION_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"REGION">>, _attrs, _els}. - -decode_vcard_REGION_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_vcard_REGION_cdata(__TopXMLNS, _val) -> _val. - -encode_vcard_REGION_cdata(undefined, _acc) -> _acc; -encode_vcard_REGION_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_vcard_LOCALITY(__TopXMLNS, __IgnoreEls, - {xmlel, <<"LOCALITY">>, _attrs, _els}) -> - Cdata = decode_vcard_LOCALITY_els(__TopXMLNS, - __IgnoreEls, _els, <<>>), - Cdata. - -decode_vcard_LOCALITY_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_vcard_LOCALITY_cdata(__TopXMLNS, Cdata); -decode_vcard_LOCALITY_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_vcard_LOCALITY_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_vcard_LOCALITY_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_vcard_LOCALITY_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_vcard_LOCALITY(Cdata, _xmlns_attrs) -> - _els = encode_vcard_LOCALITY_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"LOCALITY">>, _attrs, _els}. - -decode_vcard_LOCALITY_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_vcard_LOCALITY_cdata(__TopXMLNS, _val) -> _val. - -encode_vcard_LOCALITY_cdata(undefined, _acc) -> _acc; -encode_vcard_LOCALITY_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_vcard_STREET(__TopXMLNS, __IgnoreEls, - {xmlel, <<"STREET">>, _attrs, _els}) -> - Cdata = decode_vcard_STREET_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - Cdata. - -decode_vcard_STREET_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_vcard_STREET_cdata(__TopXMLNS, Cdata); -decode_vcard_STREET_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_vcard_STREET_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_vcard_STREET_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_vcard_STREET_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_vcard_STREET(Cdata, _xmlns_attrs) -> - _els = encode_vcard_STREET_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"STREET">>, _attrs, _els}. - -decode_vcard_STREET_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_vcard_STREET_cdata(__TopXMLNS, _val) -> _val. - -encode_vcard_STREET_cdata(undefined, _acc) -> _acc; -encode_vcard_STREET_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_vcard_EXTADD(__TopXMLNS, __IgnoreEls, - {xmlel, <<"EXTADD">>, _attrs, _els}) -> - Cdata = decode_vcard_EXTADD_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - Cdata. - -decode_vcard_EXTADD_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_vcard_EXTADD_cdata(__TopXMLNS, Cdata); -decode_vcard_EXTADD_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_vcard_EXTADD_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_vcard_EXTADD_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_vcard_EXTADD_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_vcard_EXTADD(Cdata, _xmlns_attrs) -> - _els = encode_vcard_EXTADD_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"EXTADD">>, _attrs, _els}. - -decode_vcard_EXTADD_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_vcard_EXTADD_cdata(__TopXMLNS, _val) -> _val. - -encode_vcard_EXTADD_cdata(undefined, _acc) -> _acc; -encode_vcard_EXTADD_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_vcard_POBOX(__TopXMLNS, __IgnoreEls, - {xmlel, <<"POBOX">>, _attrs, _els}) -> - Cdata = decode_vcard_POBOX_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - Cdata. - -decode_vcard_POBOX_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_vcard_POBOX_cdata(__TopXMLNS, Cdata); -decode_vcard_POBOX_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_vcard_POBOX_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_vcard_POBOX_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_vcard_POBOX_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_vcard_POBOX(Cdata, _xmlns_attrs) -> - _els = encode_vcard_POBOX_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"POBOX">>, _attrs, _els}. - -decode_vcard_POBOX_cdata(__TopXMLNS, <<>>) -> undefined; -decode_vcard_POBOX_cdata(__TopXMLNS, _val) -> _val. - -encode_vcard_POBOX_cdata(undefined, _acc) -> _acc; -encode_vcard_POBOX_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_vcard_SUFFIX(__TopXMLNS, __IgnoreEls, - {xmlel, <<"SUFFIX">>, _attrs, _els}) -> - Cdata = decode_vcard_SUFFIX_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - Cdata. - -decode_vcard_SUFFIX_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_vcard_SUFFIX_cdata(__TopXMLNS, Cdata); -decode_vcard_SUFFIX_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_vcard_SUFFIX_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_vcard_SUFFIX_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_vcard_SUFFIX_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_vcard_SUFFIX(Cdata, _xmlns_attrs) -> - _els = encode_vcard_SUFFIX_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"SUFFIX">>, _attrs, _els}. - -decode_vcard_SUFFIX_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_vcard_SUFFIX_cdata(__TopXMLNS, _val) -> _val. - -encode_vcard_SUFFIX_cdata(undefined, _acc) -> _acc; -encode_vcard_SUFFIX_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_vcard_PREFIX(__TopXMLNS, __IgnoreEls, - {xmlel, <<"PREFIX">>, _attrs, _els}) -> - Cdata = decode_vcard_PREFIX_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - Cdata. - -decode_vcard_PREFIX_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_vcard_PREFIX_cdata(__TopXMLNS, Cdata); -decode_vcard_PREFIX_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_vcard_PREFIX_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_vcard_PREFIX_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_vcard_PREFIX_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_vcard_PREFIX(Cdata, _xmlns_attrs) -> - _els = encode_vcard_PREFIX_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"PREFIX">>, _attrs, _els}. - -decode_vcard_PREFIX_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_vcard_PREFIX_cdata(__TopXMLNS, _val) -> _val. - -encode_vcard_PREFIX_cdata(undefined, _acc) -> _acc; -encode_vcard_PREFIX_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_vcard_MIDDLE(__TopXMLNS, __IgnoreEls, - {xmlel, <<"MIDDLE">>, _attrs, _els}) -> - Cdata = decode_vcard_MIDDLE_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - Cdata. - -decode_vcard_MIDDLE_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_vcard_MIDDLE_cdata(__TopXMLNS, Cdata); -decode_vcard_MIDDLE_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_vcard_MIDDLE_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_vcard_MIDDLE_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_vcard_MIDDLE_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_vcard_MIDDLE(Cdata, _xmlns_attrs) -> - _els = encode_vcard_MIDDLE_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"MIDDLE">>, _attrs, _els}. - -decode_vcard_MIDDLE_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_vcard_MIDDLE_cdata(__TopXMLNS, _val) -> _val. - -encode_vcard_MIDDLE_cdata(undefined, _acc) -> _acc; -encode_vcard_MIDDLE_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_vcard_GIVEN(__TopXMLNS, __IgnoreEls, - {xmlel, <<"GIVEN">>, _attrs, _els}) -> - Cdata = decode_vcard_GIVEN_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - Cdata. - -decode_vcard_GIVEN_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_vcard_GIVEN_cdata(__TopXMLNS, Cdata); -decode_vcard_GIVEN_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_vcard_GIVEN_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_vcard_GIVEN_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_vcard_GIVEN_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_vcard_GIVEN(Cdata, _xmlns_attrs) -> - _els = encode_vcard_GIVEN_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"GIVEN">>, _attrs, _els}. - -decode_vcard_GIVEN_cdata(__TopXMLNS, <<>>) -> undefined; -decode_vcard_GIVEN_cdata(__TopXMLNS, _val) -> _val. - -encode_vcard_GIVEN_cdata(undefined, _acc) -> _acc; -encode_vcard_GIVEN_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_vcard_FAMILY(__TopXMLNS, __IgnoreEls, - {xmlel, <<"FAMILY">>, _attrs, _els}) -> - Cdata = decode_vcard_FAMILY_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - Cdata. - -decode_vcard_FAMILY_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_vcard_FAMILY_cdata(__TopXMLNS, Cdata); -decode_vcard_FAMILY_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_vcard_FAMILY_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_vcard_FAMILY_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_vcard_FAMILY_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_vcard_FAMILY(Cdata, _xmlns_attrs) -> - _els = encode_vcard_FAMILY_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"FAMILY">>, _attrs, _els}. - -decode_vcard_FAMILY_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_vcard_FAMILY_cdata(__TopXMLNS, _val) -> _val. - -encode_vcard_FAMILY_cdata(undefined, _acc) -> _acc; -encode_vcard_FAMILY_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_vcard_X400(__TopXMLNS, __IgnoreEls, - {xmlel, <<"X400">>, _attrs, _els}) -> - true. - -encode_vcard_X400(true, _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"X400">>, _attrs, _els}. - -decode_vcard_INTERNET(__TopXMLNS, __IgnoreEls, - {xmlel, <<"INTERNET">>, _attrs, _els}) -> - true. - -encode_vcard_INTERNET(true, _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"INTERNET">>, _attrs, _els}. - -decode_vcard_PREF(__TopXMLNS, __IgnoreEls, - {xmlel, <<"PREF">>, _attrs, _els}) -> - true. - -encode_vcard_PREF(true, _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"PREF">>, _attrs, _els}. - -decode_vcard_INTL(__TopXMLNS, __IgnoreEls, - {xmlel, <<"INTL">>, _attrs, _els}) -> - true. - -encode_vcard_INTL(true, _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"INTL">>, _attrs, _els}. - -decode_vcard_DOM(__TopXMLNS, __IgnoreEls, - {xmlel, <<"DOM">>, _attrs, _els}) -> - true. - -encode_vcard_DOM(true, _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"DOM">>, _attrs, _els}. - -decode_vcard_PARCEL(__TopXMLNS, __IgnoreEls, - {xmlel, <<"PARCEL">>, _attrs, _els}) -> - true. - -encode_vcard_PARCEL(true, _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"PARCEL">>, _attrs, _els}. - -decode_vcard_POSTAL(__TopXMLNS, __IgnoreEls, - {xmlel, <<"POSTAL">>, _attrs, _els}) -> - true. - -encode_vcard_POSTAL(true, _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"POSTAL">>, _attrs, _els}. - -decode_vcard_PCS(__TopXMLNS, __IgnoreEls, - {xmlel, <<"PCS">>, _attrs, _els}) -> - true. - -encode_vcard_PCS(true, _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"PCS">>, _attrs, _els}. - -decode_vcard_ISDN(__TopXMLNS, __IgnoreEls, - {xmlel, <<"ISDN">>, _attrs, _els}) -> - true. - -encode_vcard_ISDN(true, _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"ISDN">>, _attrs, _els}. - -decode_vcard_MODEM(__TopXMLNS, __IgnoreEls, - {xmlel, <<"MODEM">>, _attrs, _els}) -> - true. - -encode_vcard_MODEM(true, _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"MODEM">>, _attrs, _els}. - -decode_vcard_BBS(__TopXMLNS, __IgnoreEls, - {xmlel, <<"BBS">>, _attrs, _els}) -> - true. - -encode_vcard_BBS(true, _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"BBS">>, _attrs, _els}. - -decode_vcard_VIDEO(__TopXMLNS, __IgnoreEls, - {xmlel, <<"VIDEO">>, _attrs, _els}) -> - true. - -encode_vcard_VIDEO(true, _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"VIDEO">>, _attrs, _els}. - -decode_vcard_CELL(__TopXMLNS, __IgnoreEls, - {xmlel, <<"CELL">>, _attrs, _els}) -> - true. - -encode_vcard_CELL(true, _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"CELL">>, _attrs, _els}. - -decode_vcard_MSG(__TopXMLNS, __IgnoreEls, - {xmlel, <<"MSG">>, _attrs, _els}) -> - true. - -encode_vcard_MSG(true, _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"MSG">>, _attrs, _els}. - -decode_vcard_PAGER(__TopXMLNS, __IgnoreEls, - {xmlel, <<"PAGER">>, _attrs, _els}) -> - true. - -encode_vcard_PAGER(true, _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"PAGER">>, _attrs, _els}. - -decode_vcard_FAX(__TopXMLNS, __IgnoreEls, - {xmlel, <<"FAX">>, _attrs, _els}) -> - true. - -encode_vcard_FAX(true, _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"FAX">>, _attrs, _els}. - -decode_vcard_VOICE(__TopXMLNS, __IgnoreEls, - {xmlel, <<"VOICE">>, _attrs, _els}) -> - true. - -encode_vcard_VOICE(true, _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"VOICE">>, _attrs, _els}. - -decode_vcard_WORK(__TopXMLNS, __IgnoreEls, - {xmlel, <<"WORK">>, _attrs, _els}) -> - true. - -encode_vcard_WORK(true, _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"WORK">>, _attrs, _els}. - -decode_vcard_HOME(__TopXMLNS, __IgnoreEls, - {xmlel, <<"HOME">>, _attrs, _els}) -> - true. - -encode_vcard_HOME(true, _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"HOME">>, _attrs, _els}. - -decode_stream_error(__TopXMLNS, __IgnoreEls, - {xmlel, <<"stream:error">>, _attrs, _els}) -> - {Text, Reason} = decode_stream_error_els(__TopXMLNS, - __IgnoreEls, _els, undefined, - undefined), - {stream_error, Reason, Text}. - -decode_stream_error_els(__TopXMLNS, __IgnoreEls, [], - Text, Reason) -> - {Text, Reason}; -decode_stream_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"text">>, _attrs, _} = _el | _els], Text, - Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-streams">> -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - decode_stream_error_text(_xmlns, __IgnoreEls, - _el), - Reason); - true -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, Reason) - end; -decode_stream_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"bad-format">>, _attrs, _} = _el | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-streams">> -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, - decode_stream_error_bad_format(_xmlns, - __IgnoreEls, - _el)); - true -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, Reason) - end; -decode_stream_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"bad-namespace-prefix">>, _attrs, _} = _el - | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-streams">> -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, - decode_stream_error_bad_namespace_prefix(_xmlns, - __IgnoreEls, - _el)); - true -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, Reason) - end; -decode_stream_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"conflict">>, _attrs, _} = _el | _els], Text, - Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-streams">> -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, - decode_stream_error_conflict(_xmlns, - __IgnoreEls, - _el)); - true -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, Reason) - end; -decode_stream_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"connection-timeout">>, _attrs, _} = _el - | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-streams">> -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, - decode_stream_error_connection_timeout(_xmlns, - __IgnoreEls, - _el)); - true -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, Reason) - end; -decode_stream_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"host-gone">>, _attrs, _} = _el | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-streams">> -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, - decode_stream_error_host_gone(_xmlns, - __IgnoreEls, - _el)); - true -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, Reason) - end; -decode_stream_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"host-unknown">>, _attrs, _} = _el | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-streams">> -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, - decode_stream_error_host_unknown(_xmlns, - __IgnoreEls, - _el)); - true -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, Reason) - end; -decode_stream_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"improper-addressing">>, _attrs, _} = _el - | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-streams">> -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, - decode_stream_error_improper_addressing(_xmlns, - __IgnoreEls, - _el)); - true -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, Reason) - end; -decode_stream_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"internal-server-error">>, _attrs, _} = _el - | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-streams">> -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, - decode_stream_error_internal_server_error(_xmlns, - __IgnoreEls, - _el)); - true -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, Reason) - end; -decode_stream_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"invalid-from">>, _attrs, _} = _el | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-streams">> -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, - decode_stream_error_invalid_from(_xmlns, - __IgnoreEls, - _el)); - true -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, Reason) - end; -decode_stream_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"invalid-id">>, _attrs, _} = _el | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-streams">> -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, - decode_stream_error_invalid_id(_xmlns, - __IgnoreEls, - _el)); - true -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, Reason) - end; -decode_stream_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"invalid-namespace">>, _attrs, _} = _el - | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-streams">> -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, - decode_stream_error_invalid_namespace(_xmlns, - __IgnoreEls, - _el)); - true -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, Reason) - end; -decode_stream_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"invalid-xml">>, _attrs, _} = _el | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-streams">> -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, - decode_stream_error_invalid_xml(_xmlns, - __IgnoreEls, - _el)); - true -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, Reason) - end; -decode_stream_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"not-authorized">>, _attrs, _} = _el | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-streams">> -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, - decode_stream_error_not_authorized(_xmlns, - __IgnoreEls, - _el)); - true -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, Reason) - end; -decode_stream_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"not-well-formed">>, _attrs, _} = _el - | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-streams">> -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, - decode_stream_error_not_well_formed(_xmlns, - __IgnoreEls, - _el)); - true -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, Reason) - end; -decode_stream_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"policy-violation">>, _attrs, _} = _el - | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-streams">> -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, - decode_stream_error_policy_violation(_xmlns, - __IgnoreEls, - _el)); - true -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, Reason) - end; -decode_stream_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"remote-connection-failed">>, _attrs, _} = - _el - | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-streams">> -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, - decode_stream_error_remote_connection_failed(_xmlns, - __IgnoreEls, - _el)); - true -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, Reason) - end; -decode_stream_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"reset">>, _attrs, _} = _el | _els], Text, - Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-streams">> -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, - decode_stream_error_reset(_xmlns, - __IgnoreEls, _el)); - true -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, Reason) - end; -decode_stream_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"resource-constraint">>, _attrs, _} = _el - | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-streams">> -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, - decode_stream_error_resource_constraint(_xmlns, - __IgnoreEls, - _el)); - true -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, Reason) - end; -decode_stream_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"restricted-xml">>, _attrs, _} = _el | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-streams">> -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, - decode_stream_error_restricted_xml(_xmlns, - __IgnoreEls, - _el)); - true -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, Reason) - end; -decode_stream_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"see-other-host">>, _attrs, _} = _el | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-streams">> -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, - decode_stream_error_see_other_host(_xmlns, - __IgnoreEls, - _el)); - true -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, Reason) - end; -decode_stream_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"system-shutdown">>, _attrs, _} = _el - | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-streams">> -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, - decode_stream_error_system_shutdown(_xmlns, - __IgnoreEls, - _el)); - true -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, Reason) - end; -decode_stream_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"undefined-condition">>, _attrs, _} = _el - | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-streams">> -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, - decode_stream_error_undefined_condition(_xmlns, - __IgnoreEls, - _el)); - true -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, Reason) - end; -decode_stream_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"unsupported-encoding">>, _attrs, _} = _el - | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-streams">> -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, - decode_stream_error_unsupported_encoding(_xmlns, - __IgnoreEls, - _el)); - true -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, Reason) - end; -decode_stream_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"unsupported-stanza-type">>, _attrs, _} = _el - | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-streams">> -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, - decode_stream_error_unsupported_stanza_type(_xmlns, - __IgnoreEls, - _el)); - true -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, Reason) - end; -decode_stream_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"unsupported-version">>, _attrs, _} = _el - | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-streams">> -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, - decode_stream_error_unsupported_version(_xmlns, - __IgnoreEls, - _el)); - true -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, Reason) - end; -decode_stream_error_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Text, Reason) -> - decode_stream_error_els(__TopXMLNS, __IgnoreEls, _els, - Text, Reason). - -encode_stream_error({stream_error, Reason, Text}, - _xmlns_attrs) -> - _els = lists:reverse('encode_stream_error_$text'(Text, - 'encode_stream_error_$reason'(Reason, - []))), - _attrs = _xmlns_attrs, - {xmlel, <<"stream:error">>, _attrs, _els}. - -'encode_stream_error_$text'(undefined, _acc) -> _acc; -'encode_stream_error_$text'(Text, _acc) -> - [encode_stream_error_text(Text, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>}]) - | _acc]. - -'encode_stream_error_$reason'(undefined, _acc) -> _acc; -'encode_stream_error_$reason'('bad-format' = Reason, - _acc) -> - [encode_stream_error_bad_format(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>}]) - | _acc]; -'encode_stream_error_$reason'('bad-namespace-prefix' = - Reason, - _acc) -> - [encode_stream_error_bad_namespace_prefix(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>}]) - | _acc]; -'encode_stream_error_$reason'(conflict = Reason, - _acc) -> - [encode_stream_error_conflict(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>}]) - | _acc]; -'encode_stream_error_$reason'('connection-timeout' = - Reason, - _acc) -> - [encode_stream_error_connection_timeout(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>}]) - | _acc]; -'encode_stream_error_$reason'('host-gone' = Reason, - _acc) -> - [encode_stream_error_host_gone(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>}]) - | _acc]; -'encode_stream_error_$reason'('host-unknown' = Reason, - _acc) -> - [encode_stream_error_host_unknown(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>}]) - | _acc]; -'encode_stream_error_$reason'('improper-addressing' = - Reason, - _acc) -> - [encode_stream_error_improper_addressing(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>}]) - | _acc]; -'encode_stream_error_$reason'('internal-server-error' = - Reason, - _acc) -> - [encode_stream_error_internal_server_error(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>}]) - | _acc]; -'encode_stream_error_$reason'('invalid-from' = Reason, - _acc) -> - [encode_stream_error_invalid_from(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>}]) - | _acc]; -'encode_stream_error_$reason'('invalid-id' = Reason, - _acc) -> - [encode_stream_error_invalid_id(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>}]) - | _acc]; -'encode_stream_error_$reason'('invalid-namespace' = - Reason, - _acc) -> - [encode_stream_error_invalid_namespace(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>}]) - | _acc]; -'encode_stream_error_$reason'('invalid-xml' = Reason, - _acc) -> - [encode_stream_error_invalid_xml(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>}]) - | _acc]; -'encode_stream_error_$reason'('not-authorized' = Reason, - _acc) -> - [encode_stream_error_not_authorized(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>}]) - | _acc]; -'encode_stream_error_$reason'('not-well-formed' = - Reason, - _acc) -> - [encode_stream_error_not_well_formed(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>}]) - | _acc]; -'encode_stream_error_$reason'('policy-violation' = - Reason, - _acc) -> - [encode_stream_error_policy_violation(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>}]) - | _acc]; -'encode_stream_error_$reason'('remote-connection-failed' = - Reason, - _acc) -> - [encode_stream_error_remote_connection_failed(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>}]) - | _acc]; -'encode_stream_error_$reason'(reset = Reason, _acc) -> - [encode_stream_error_reset(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>}]) - | _acc]; -'encode_stream_error_$reason'('resource-constraint' = - Reason, - _acc) -> - [encode_stream_error_resource_constraint(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>}]) - | _acc]; -'encode_stream_error_$reason'('restricted-xml' = Reason, - _acc) -> - [encode_stream_error_restricted_xml(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>}]) - | _acc]; -'encode_stream_error_$reason'({'see-other-host', _} = - Reason, - _acc) -> - [encode_stream_error_see_other_host(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>}]) - | _acc]; -'encode_stream_error_$reason'('system-shutdown' = - Reason, - _acc) -> - [encode_stream_error_system_shutdown(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>}]) - | _acc]; -'encode_stream_error_$reason'('undefined-condition' = - Reason, - _acc) -> - [encode_stream_error_undefined_condition(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>}]) - | _acc]; -'encode_stream_error_$reason'('unsupported-encoding' = - Reason, - _acc) -> - [encode_stream_error_unsupported_encoding(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>}]) - | _acc]; -'encode_stream_error_$reason'('unsupported-stanza-type' = - Reason, - _acc) -> - [encode_stream_error_unsupported_stanza_type(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>}]) - | _acc]; -'encode_stream_error_$reason'('unsupported-version' = - Reason, - _acc) -> - [encode_stream_error_unsupported_version(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-streams">>}]) - | _acc]. - -decode_stream_error_unsupported_version(__TopXMLNS, - __IgnoreEls, - {xmlel, <<"unsupported-version">>, - _attrs, _els}) -> - 'unsupported-version'. - -encode_stream_error_unsupported_version('unsupported-version', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"unsupported-version">>, _attrs, _els}. - -decode_stream_error_unsupported_stanza_type(__TopXMLNS, - __IgnoreEls, - {xmlel, - <<"unsupported-stanza-type">>, - _attrs, _els}) -> - 'unsupported-stanza-type'. - -encode_stream_error_unsupported_stanza_type('unsupported-stanza-type', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"unsupported-stanza-type">>, _attrs, _els}. - -decode_stream_error_unsupported_encoding(__TopXMLNS, - __IgnoreEls, - {xmlel, <<"unsupported-encoding">>, - _attrs, _els}) -> - 'unsupported-encoding'. - -encode_stream_error_unsupported_encoding('unsupported-encoding', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"unsupported-encoding">>, _attrs, _els}. - -decode_stream_error_undefined_condition(__TopXMLNS, - __IgnoreEls, - {xmlel, <<"undefined-condition">>, - _attrs, _els}) -> - 'undefined-condition'. - -encode_stream_error_undefined_condition('undefined-condition', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"undefined-condition">>, _attrs, _els}. - -decode_stream_error_system_shutdown(__TopXMLNS, - __IgnoreEls, - {xmlel, <<"system-shutdown">>, _attrs, - _els}) -> - 'system-shutdown'. - -encode_stream_error_system_shutdown('system-shutdown', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"system-shutdown">>, _attrs, _els}. - -decode_stream_error_see_other_host(__TopXMLNS, - __IgnoreEls, - {xmlel, <<"see-other-host">>, _attrs, - _els}) -> - Host = - decode_stream_error_see_other_host_els(__TopXMLNS, - __IgnoreEls, _els, <<>>), - {'see-other-host', Host}. - -decode_stream_error_see_other_host_els(__TopXMLNS, - __IgnoreEls, [], Host) -> - decode_stream_error_see_other_host_cdata(__TopXMLNS, - Host); -decode_stream_error_see_other_host_els(__TopXMLNS, - __IgnoreEls, [{xmlcdata, _data} | _els], - Host) -> - decode_stream_error_see_other_host_els(__TopXMLNS, - __IgnoreEls, _els, - <<Host/binary, _data/binary>>); -decode_stream_error_see_other_host_els(__TopXMLNS, - __IgnoreEls, [_ | _els], Host) -> - decode_stream_error_see_other_host_els(__TopXMLNS, - __IgnoreEls, _els, Host). - -encode_stream_error_see_other_host({'see-other-host', - Host}, - _xmlns_attrs) -> - _els = encode_stream_error_see_other_host_cdata(Host, - []), - _attrs = _xmlns_attrs, - {xmlel, <<"see-other-host">>, _attrs, _els}. - -decode_stream_error_see_other_host_cdata(__TopXMLNS, - <<>>) -> - erlang:error({xmpp_codec, - {missing_cdata, <<>>, <<"see-other-host">>, - __TopXMLNS}}); -decode_stream_error_see_other_host_cdata(__TopXMLNS, - _val) -> - _val. - -encode_stream_error_see_other_host_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_stream_error_restricted_xml(__TopXMLNS, - __IgnoreEls, - {xmlel, <<"restricted-xml">>, _attrs, - _els}) -> - 'restricted-xml'. - -encode_stream_error_restricted_xml('restricted-xml', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"restricted-xml">>, _attrs, _els}. - -decode_stream_error_resource_constraint(__TopXMLNS, - __IgnoreEls, - {xmlel, <<"resource-constraint">>, - _attrs, _els}) -> - 'resource-constraint'. - -encode_stream_error_resource_constraint('resource-constraint', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"resource-constraint">>, _attrs, _els}. - -decode_stream_error_reset(__TopXMLNS, __IgnoreEls, - {xmlel, <<"reset">>, _attrs, _els}) -> - reset. - -encode_stream_error_reset(reset, _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"reset">>, _attrs, _els}. - -decode_stream_error_remote_connection_failed(__TopXMLNS, - __IgnoreEls, - {xmlel, - <<"remote-connection-failed">>, - _attrs, _els}) -> - 'remote-connection-failed'. - -encode_stream_error_remote_connection_failed('remote-connection-failed', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"remote-connection-failed">>, _attrs, _els}. - -decode_stream_error_policy_violation(__TopXMLNS, - __IgnoreEls, - {xmlel, <<"policy-violation">>, _attrs, - _els}) -> - 'policy-violation'. - -encode_stream_error_policy_violation('policy-violation', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"policy-violation">>, _attrs, _els}. - -decode_stream_error_not_well_formed(__TopXMLNS, - __IgnoreEls, - {xmlel, <<"not-well-formed">>, _attrs, - _els}) -> - 'not-well-formed'. - -encode_stream_error_not_well_formed('not-well-formed', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"not-well-formed">>, _attrs, _els}. - -decode_stream_error_not_authorized(__TopXMLNS, - __IgnoreEls, - {xmlel, <<"not-authorized">>, _attrs, - _els}) -> - 'not-authorized'. - -encode_stream_error_not_authorized('not-authorized', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"not-authorized">>, _attrs, _els}. - -decode_stream_error_invalid_xml(__TopXMLNS, __IgnoreEls, - {xmlel, <<"invalid-xml">>, _attrs, _els}) -> - 'invalid-xml'. - -encode_stream_error_invalid_xml('invalid-xml', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"invalid-xml">>, _attrs, _els}. - -decode_stream_error_invalid_namespace(__TopXMLNS, - __IgnoreEls, - {xmlel, <<"invalid-namespace">>, _attrs, - _els}) -> - 'invalid-namespace'. - -encode_stream_error_invalid_namespace('invalid-namespace', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"invalid-namespace">>, _attrs, _els}. - -decode_stream_error_invalid_id(__TopXMLNS, __IgnoreEls, - {xmlel, <<"invalid-id">>, _attrs, _els}) -> - 'invalid-id'. - -encode_stream_error_invalid_id('invalid-id', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"invalid-id">>, _attrs, _els}. - -decode_stream_error_invalid_from(__TopXMLNS, - __IgnoreEls, - {xmlel, <<"invalid-from">>, _attrs, _els}) -> - 'invalid-from'. - -encode_stream_error_invalid_from('invalid-from', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"invalid-from">>, _attrs, _els}. - -decode_stream_error_internal_server_error(__TopXMLNS, - __IgnoreEls, - {xmlel, <<"internal-server-error">>, - _attrs, _els}) -> - 'internal-server-error'. - -encode_stream_error_internal_server_error('internal-server-error', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"internal-server-error">>, _attrs, _els}. - -decode_stream_error_improper_addressing(__TopXMLNS, - __IgnoreEls, - {xmlel, <<"improper-addressing">>, - _attrs, _els}) -> - 'improper-addressing'. - -encode_stream_error_improper_addressing('improper-addressing', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"improper-addressing">>, _attrs, _els}. - -decode_stream_error_host_unknown(__TopXMLNS, - __IgnoreEls, - {xmlel, <<"host-unknown">>, _attrs, _els}) -> - 'host-unknown'. - -encode_stream_error_host_unknown('host-unknown', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"host-unknown">>, _attrs, _els}. - -decode_stream_error_host_gone(__TopXMLNS, __IgnoreEls, - {xmlel, <<"host-gone">>, _attrs, _els}) -> - 'host-gone'. - -encode_stream_error_host_gone('host-gone', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"host-gone">>, _attrs, _els}. - -decode_stream_error_connection_timeout(__TopXMLNS, - __IgnoreEls, - {xmlel, <<"connection-timeout">>, _attrs, - _els}) -> - 'connection-timeout'. - -encode_stream_error_connection_timeout('connection-timeout', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"connection-timeout">>, _attrs, _els}. - -decode_stream_error_conflict(__TopXMLNS, __IgnoreEls, - {xmlel, <<"conflict">>, _attrs, _els}) -> - conflict. - -encode_stream_error_conflict(conflict, _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"conflict">>, _attrs, _els}. - -decode_stream_error_bad_namespace_prefix(__TopXMLNS, - __IgnoreEls, - {xmlel, <<"bad-namespace-prefix">>, - _attrs, _els}) -> - 'bad-namespace-prefix'. - -encode_stream_error_bad_namespace_prefix('bad-namespace-prefix', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"bad-namespace-prefix">>, _attrs, _els}. - -decode_stream_error_bad_format(__TopXMLNS, __IgnoreEls, - {xmlel, <<"bad-format">>, _attrs, _els}) -> - 'bad-format'. - -encode_stream_error_bad_format('bad-format', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"bad-format">>, _attrs, _els}. - -decode_stream_error_text(__TopXMLNS, __IgnoreEls, - {xmlel, <<"text">>, _attrs, _els}) -> - Data = decode_stream_error_text_els(__TopXMLNS, - __IgnoreEls, _els, <<>>), - Lang = decode_stream_error_text_attrs(__TopXMLNS, - _attrs, undefined), - {text, Lang, Data}. - -decode_stream_error_text_els(__TopXMLNS, __IgnoreEls, - [], Data) -> - decode_stream_error_text_cdata(__TopXMLNS, Data); -decode_stream_error_text_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Data) -> - decode_stream_error_text_els(__TopXMLNS, __IgnoreEls, - _els, <<Data/binary, _data/binary>>); -decode_stream_error_text_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Data) -> - decode_stream_error_text_els(__TopXMLNS, __IgnoreEls, - _els, Data). - -decode_stream_error_text_attrs(__TopXMLNS, - [{<<"xml:lang">>, _val} | _attrs], _Lang) -> - decode_stream_error_text_attrs(__TopXMLNS, _attrs, - _val); -decode_stream_error_text_attrs(__TopXMLNS, [_ | _attrs], - Lang) -> - decode_stream_error_text_attrs(__TopXMLNS, _attrs, - Lang); -decode_stream_error_text_attrs(__TopXMLNS, [], Lang) -> - 'decode_stream_error_text_attr_xml:lang'(__TopXMLNS, - Lang). - -encode_stream_error_text({text, Lang, Data}, - _xmlns_attrs) -> - _els = encode_stream_error_text_cdata(Data, []), - _attrs = 'encode_stream_error_text_attr_xml:lang'(Lang, - _xmlns_attrs), - {xmlel, <<"text">>, _attrs, _els}. - -'decode_stream_error_text_attr_xml:lang'(__TopXMLNS, - undefined) -> - undefined; -'decode_stream_error_text_attr_xml:lang'(__TopXMLNS, - _val) -> - _val. - -'encode_stream_error_text_attr_xml:lang'(undefined, - _acc) -> - _acc; -'encode_stream_error_text_attr_xml:lang'(_val, _acc) -> - [{<<"xml:lang">>, _val} | _acc]. - -decode_stream_error_text_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_stream_error_text_cdata(__TopXMLNS, _val) -> - _val. - -encode_stream_error_text_cdata(undefined, _acc) -> _acc; -encode_stream_error_text_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_time(__TopXMLNS, __IgnoreEls, - {xmlel, <<"time">>, _attrs, _els}) -> - {Utc, Tzo} = decode_time_els(__TopXMLNS, __IgnoreEls, - _els, undefined, undefined), - {time, Tzo, Utc}. - -decode_time_els(__TopXMLNS, __IgnoreEls, [], Utc, - Tzo) -> - {Utc, Tzo}; -decode_time_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"tzo">>, _attrs, _} = _el | _els], Utc, - Tzo) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_time_els(__TopXMLNS, __IgnoreEls, _els, Utc, - decode_time_tzo(__TopXMLNS, __IgnoreEls, _el)); - true -> - decode_time_els(__TopXMLNS, __IgnoreEls, _els, Utc, Tzo) - end; -decode_time_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"utc">>, _attrs, _} = _el | _els], Utc, - Tzo) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_time_els(__TopXMLNS, __IgnoreEls, _els, - decode_time_utc(__TopXMLNS, __IgnoreEls, _el), Tzo); - true -> - decode_time_els(__TopXMLNS, __IgnoreEls, _els, Utc, Tzo) - end; -decode_time_els(__TopXMLNS, __IgnoreEls, [_ | _els], - Utc, Tzo) -> - decode_time_els(__TopXMLNS, __IgnoreEls, _els, Utc, - Tzo). - -encode_time({time, Tzo, Utc}, _xmlns_attrs) -> - _els = lists:reverse('encode_time_$utc'(Utc, - 'encode_time_$tzo'(Tzo, []))), - _attrs = _xmlns_attrs, - {xmlel, <<"time">>, _attrs, _els}. - -'encode_time_$utc'(undefined, _acc) -> _acc; -'encode_time_$utc'(Utc, _acc) -> - [encode_time_utc(Utc, []) | _acc]. - -'encode_time_$tzo'(undefined, _acc) -> _acc; -'encode_time_$tzo'(Tzo, _acc) -> - [encode_time_tzo(Tzo, []) | _acc]. - -decode_time_tzo(__TopXMLNS, __IgnoreEls, - {xmlel, <<"tzo">>, _attrs, _els}) -> - Cdata = decode_time_tzo_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - Cdata. - -decode_time_tzo_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_time_tzo_cdata(__TopXMLNS, Cdata); -decode_time_tzo_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_time_tzo_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_time_tzo_els(__TopXMLNS, __IgnoreEls, [_ | _els], - Cdata) -> - decode_time_tzo_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_time_tzo(Cdata, _xmlns_attrs) -> - _els = encode_time_tzo_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"tzo">>, _attrs, _els}. - -decode_time_tzo_cdata(__TopXMLNS, <<>>) -> undefined; -decode_time_tzo_cdata(__TopXMLNS, _val) -> - case catch dec_tzo(_val) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_cdata_value, <<>>, <<"tzo">>, __TopXMLNS}}); - _res -> _res - end. - -encode_time_tzo_cdata(undefined, _acc) -> _acc; -encode_time_tzo_cdata(_val, _acc) -> - [{xmlcdata, enc_tzo(_val)} | _acc]. - -decode_time_utc(__TopXMLNS, __IgnoreEls, - {xmlel, <<"utc">>, _attrs, _els}) -> - Cdata = decode_time_utc_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - Cdata. - -decode_time_utc_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_time_utc_cdata(__TopXMLNS, Cdata); -decode_time_utc_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_time_utc_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_time_utc_els(__TopXMLNS, __IgnoreEls, [_ | _els], - Cdata) -> - decode_time_utc_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_time_utc(Cdata, _xmlns_attrs) -> - _els = encode_time_utc_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"utc">>, _attrs, _els}. - -decode_time_utc_cdata(__TopXMLNS, <<>>) -> undefined; -decode_time_utc_cdata(__TopXMLNS, _val) -> - case catch dec_utc(_val) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_cdata_value, <<>>, <<"utc">>, __TopXMLNS}}); - _res -> _res - end. - -encode_time_utc_cdata(undefined, _acc) -> _acc; -encode_time_utc_cdata(_val, _acc) -> - [{xmlcdata, enc_utc(_val)} | _acc]. - -decode_ping(__TopXMLNS, __IgnoreEls, - {xmlel, <<"ping">>, _attrs, _els}) -> - {ping}. - -encode_ping({ping}, _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"ping">>, _attrs, _els}. - -decode_session(__TopXMLNS, __IgnoreEls, - {xmlel, <<"session">>, _attrs, _els}) -> - {session}. - -encode_session({session}, _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"session">>, _attrs, _els}. - -decode_register(__TopXMLNS, __IgnoreEls, - {xmlel, <<"query">>, _attrs, _els}) -> - {Zip, Xdata, Misc, Address, Instructions, Text, Last, - First, Password, Registered, Date, Phone, State, Name, - Username, Remove, Key, City, Nick, Url, Email} = - decode_register_els(__TopXMLNS, __IgnoreEls, _els, - undefined, undefined, undefined, undefined, - undefined, undefined, undefined, undefined, - undefined, false, undefined, undefined, undefined, - undefined, undefined, false, undefined, undefined, - undefined, undefined, undefined), - {register, Registered, Remove, Instructions, Username, - Nick, Password, Name, First, Last, Email, Address, City, - State, Zip, Phone, Url, Date, Misc, Text, Key, Xdata}. - -decode_register_els(__TopXMLNS, __IgnoreEls, [], Zip, - Xdata, Misc, Address, Instructions, Text, Last, First, - Password, Registered, Date, Phone, State, Name, - Username, Remove, Key, City, Nick, Url, Email) -> - {Zip, Xdata, Misc, Address, Instructions, Text, Last, - First, Password, Registered, Date, Phone, State, Name, - Username, Remove, Key, City, Nick, Url, Email}; -decode_register_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"x">>, _attrs, _} = _el | _els], Zip, Xdata, - Misc, Address, Instructions, Text, Last, First, - Password, Registered, Date, Phone, State, Name, - Username, Remove, Key, City, Nick, Url, Email) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<"jabber:x:data">> -> - decode_register_els(__TopXMLNS, __IgnoreEls, _els, Zip, - decode_xdata(_xmlns, __IgnoreEls, _el), Misc, - Address, Instructions, Text, Last, First, - Password, Registered, Date, Phone, State, Name, - Username, Remove, Key, City, Nick, Url, Email); - true -> - decode_register_els(__TopXMLNS, __IgnoreEls, _els, Zip, - Xdata, Misc, Address, Instructions, Text, Last, - First, Password, Registered, Date, Phone, State, - Name, Username, Remove, Key, City, Nick, Url, - Email) - end; -decode_register_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"registered">>, _attrs, _} = _el | _els], - Zip, Xdata, Misc, Address, Instructions, Text, Last, - First, Password, Registered, Date, Phone, State, Name, - Username, Remove, Key, City, Nick, Url, Email) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_register_els(__TopXMLNS, __IgnoreEls, _els, Zip, - Xdata, Misc, Address, Instructions, Text, Last, - First, Password, - decode_register_registered(__TopXMLNS, - __IgnoreEls, _el), - Date, Phone, State, Name, Username, Remove, Key, - City, Nick, Url, Email); - true -> - decode_register_els(__TopXMLNS, __IgnoreEls, _els, Zip, - Xdata, Misc, Address, Instructions, Text, Last, - First, Password, Registered, Date, Phone, State, - Name, Username, Remove, Key, City, Nick, Url, - Email) - end; -decode_register_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"remove">>, _attrs, _} = _el | _els], Zip, - Xdata, Misc, Address, Instructions, Text, Last, First, - Password, Registered, Date, Phone, State, Name, - Username, Remove, Key, City, Nick, Url, Email) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_register_els(__TopXMLNS, __IgnoreEls, _els, Zip, - Xdata, Misc, Address, Instructions, Text, Last, - First, Password, Registered, Date, Phone, State, - Name, Username, - decode_register_remove(__TopXMLNS, __IgnoreEls, - _el), - Key, City, Nick, Url, Email); - true -> - decode_register_els(__TopXMLNS, __IgnoreEls, _els, Zip, - Xdata, Misc, Address, Instructions, Text, Last, - First, Password, Registered, Date, Phone, State, - Name, Username, Remove, Key, City, Nick, Url, - Email) - end; -decode_register_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"instructions">>, _attrs, _} = _el | _els], - Zip, Xdata, Misc, Address, Instructions, Text, Last, - First, Password, Registered, Date, Phone, State, Name, - Username, Remove, Key, City, Nick, Url, Email) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_register_els(__TopXMLNS, __IgnoreEls, _els, Zip, - Xdata, Misc, Address, - decode_register_instructions(__TopXMLNS, - __IgnoreEls, _el), - Text, Last, First, Password, Registered, Date, - Phone, State, Name, Username, Remove, Key, City, - Nick, Url, Email); - true -> - decode_register_els(__TopXMLNS, __IgnoreEls, _els, Zip, - Xdata, Misc, Address, Instructions, Text, Last, - First, Password, Registered, Date, Phone, State, - Name, Username, Remove, Key, City, Nick, Url, - Email) - end; -decode_register_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"username">>, _attrs, _} = _el | _els], Zip, - Xdata, Misc, Address, Instructions, Text, Last, First, - Password, Registered, Date, Phone, State, Name, - Username, Remove, Key, City, Nick, Url, Email) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_register_els(__TopXMLNS, __IgnoreEls, _els, Zip, - Xdata, Misc, Address, Instructions, Text, Last, - First, Password, Registered, Date, Phone, State, - Name, - decode_register_username(__TopXMLNS, __IgnoreEls, - _el), - Remove, Key, City, Nick, Url, Email); - true -> - decode_register_els(__TopXMLNS, __IgnoreEls, _els, Zip, - Xdata, Misc, Address, Instructions, Text, Last, - First, Password, Registered, Date, Phone, State, - Name, Username, Remove, Key, City, Nick, Url, - Email) - end; -decode_register_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"nick">>, _attrs, _} = _el | _els], Zip, - Xdata, Misc, Address, Instructions, Text, Last, First, - Password, Registered, Date, Phone, State, Name, - Username, Remove, Key, City, Nick, Url, Email) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_register_els(__TopXMLNS, __IgnoreEls, _els, Zip, - Xdata, Misc, Address, Instructions, Text, Last, - First, Password, Registered, Date, Phone, State, - Name, Username, Remove, Key, City, - decode_register_nick(__TopXMLNS, __IgnoreEls, - _el), - Url, Email); - true -> - decode_register_els(__TopXMLNS, __IgnoreEls, _els, Zip, - Xdata, Misc, Address, Instructions, Text, Last, - First, Password, Registered, Date, Phone, State, - Name, Username, Remove, Key, City, Nick, Url, - Email) - end; -decode_register_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"password">>, _attrs, _} = _el | _els], Zip, - Xdata, Misc, Address, Instructions, Text, Last, First, - Password, Registered, Date, Phone, State, Name, - Username, Remove, Key, City, Nick, Url, Email) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_register_els(__TopXMLNS, __IgnoreEls, _els, Zip, - Xdata, Misc, Address, Instructions, Text, Last, - First, - decode_register_password(__TopXMLNS, __IgnoreEls, - _el), - Registered, Date, Phone, State, Name, Username, - Remove, Key, City, Nick, Url, Email); - true -> - decode_register_els(__TopXMLNS, __IgnoreEls, _els, Zip, - Xdata, Misc, Address, Instructions, Text, Last, - First, Password, Registered, Date, Phone, State, - Name, Username, Remove, Key, City, Nick, Url, - Email) - end; -decode_register_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"name">>, _attrs, _} = _el | _els], Zip, - Xdata, Misc, Address, Instructions, Text, Last, First, - Password, Registered, Date, Phone, State, Name, - Username, Remove, Key, City, Nick, Url, Email) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_register_els(__TopXMLNS, __IgnoreEls, _els, Zip, - Xdata, Misc, Address, Instructions, Text, Last, - First, Password, Registered, Date, Phone, State, - decode_register_name(__TopXMLNS, __IgnoreEls, - _el), - Username, Remove, Key, City, Nick, Url, Email); - true -> - decode_register_els(__TopXMLNS, __IgnoreEls, _els, Zip, - Xdata, Misc, Address, Instructions, Text, Last, - First, Password, Registered, Date, Phone, State, - Name, Username, Remove, Key, City, Nick, Url, - Email) - end; -decode_register_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"first">>, _attrs, _} = _el | _els], Zip, - Xdata, Misc, Address, Instructions, Text, Last, First, - Password, Registered, Date, Phone, State, Name, - Username, Remove, Key, City, Nick, Url, Email) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_register_els(__TopXMLNS, __IgnoreEls, _els, Zip, - Xdata, Misc, Address, Instructions, Text, Last, - decode_register_first(__TopXMLNS, __IgnoreEls, - _el), - Password, Registered, Date, Phone, State, Name, - Username, Remove, Key, City, Nick, Url, Email); - true -> - decode_register_els(__TopXMLNS, __IgnoreEls, _els, Zip, - Xdata, Misc, Address, Instructions, Text, Last, - First, Password, Registered, Date, Phone, State, - Name, Username, Remove, Key, City, Nick, Url, - Email) - end; -decode_register_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"last">>, _attrs, _} = _el | _els], Zip, - Xdata, Misc, Address, Instructions, Text, Last, First, - Password, Registered, Date, Phone, State, Name, - Username, Remove, Key, City, Nick, Url, Email) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_register_els(__TopXMLNS, __IgnoreEls, _els, Zip, - Xdata, Misc, Address, Instructions, Text, - decode_register_last(__TopXMLNS, __IgnoreEls, - _el), - First, Password, Registered, Date, Phone, State, - Name, Username, Remove, Key, City, Nick, Url, - Email); - true -> - decode_register_els(__TopXMLNS, __IgnoreEls, _els, Zip, - Xdata, Misc, Address, Instructions, Text, Last, - First, Password, Registered, Date, Phone, State, - Name, Username, Remove, Key, City, Nick, Url, - Email) - end; -decode_register_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"email">>, _attrs, _} = _el | _els], Zip, - Xdata, Misc, Address, Instructions, Text, Last, First, - Password, Registered, Date, Phone, State, Name, - Username, Remove, Key, City, Nick, Url, Email) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_register_els(__TopXMLNS, __IgnoreEls, _els, Zip, - Xdata, Misc, Address, Instructions, Text, Last, - First, Password, Registered, Date, Phone, State, - Name, Username, Remove, Key, City, Nick, Url, - decode_register_email(__TopXMLNS, __IgnoreEls, - _el)); - true -> - decode_register_els(__TopXMLNS, __IgnoreEls, _els, Zip, - Xdata, Misc, Address, Instructions, Text, Last, - First, Password, Registered, Date, Phone, State, - Name, Username, Remove, Key, City, Nick, Url, - Email) - end; -decode_register_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"address">>, _attrs, _} = _el | _els], Zip, - Xdata, Misc, Address, Instructions, Text, Last, First, - Password, Registered, Date, Phone, State, Name, - Username, Remove, Key, City, Nick, Url, Email) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_register_els(__TopXMLNS, __IgnoreEls, _els, Zip, - Xdata, Misc, - decode_register_address(__TopXMLNS, __IgnoreEls, - _el), - Instructions, Text, Last, First, Password, - Registered, Date, Phone, State, Name, Username, - Remove, Key, City, Nick, Url, Email); - true -> - decode_register_els(__TopXMLNS, __IgnoreEls, _els, Zip, - Xdata, Misc, Address, Instructions, Text, Last, - First, Password, Registered, Date, Phone, State, - Name, Username, Remove, Key, City, Nick, Url, - Email) - end; -decode_register_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"city">>, _attrs, _} = _el | _els], Zip, - Xdata, Misc, Address, Instructions, Text, Last, First, - Password, Registered, Date, Phone, State, Name, - Username, Remove, Key, City, Nick, Url, Email) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_register_els(__TopXMLNS, __IgnoreEls, _els, Zip, - Xdata, Misc, Address, Instructions, Text, Last, - First, Password, Registered, Date, Phone, State, - Name, Username, Remove, Key, - decode_register_city(__TopXMLNS, __IgnoreEls, - _el), - Nick, Url, Email); - true -> - decode_register_els(__TopXMLNS, __IgnoreEls, _els, Zip, - Xdata, Misc, Address, Instructions, Text, Last, - First, Password, Registered, Date, Phone, State, - Name, Username, Remove, Key, City, Nick, Url, - Email) - end; -decode_register_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"state">>, _attrs, _} = _el | _els], Zip, - Xdata, Misc, Address, Instructions, Text, Last, First, - Password, Registered, Date, Phone, State, Name, - Username, Remove, Key, City, Nick, Url, Email) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_register_els(__TopXMLNS, __IgnoreEls, _els, Zip, - Xdata, Misc, Address, Instructions, Text, Last, - First, Password, Registered, Date, Phone, - decode_register_state(__TopXMLNS, __IgnoreEls, - _el), - Name, Username, Remove, Key, City, Nick, Url, - Email); - true -> - decode_register_els(__TopXMLNS, __IgnoreEls, _els, Zip, - Xdata, Misc, Address, Instructions, Text, Last, - First, Password, Registered, Date, Phone, State, - Name, Username, Remove, Key, City, Nick, Url, - Email) - end; -decode_register_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"zip">>, _attrs, _} = _el | _els], Zip, - Xdata, Misc, Address, Instructions, Text, Last, First, - Password, Registered, Date, Phone, State, Name, - Username, Remove, Key, City, Nick, Url, Email) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_register_els(__TopXMLNS, __IgnoreEls, _els, - decode_register_zip(__TopXMLNS, __IgnoreEls, - _el), - Xdata, Misc, Address, Instructions, Text, Last, - First, Password, Registered, Date, Phone, State, - Name, Username, Remove, Key, City, Nick, Url, - Email); - true -> - decode_register_els(__TopXMLNS, __IgnoreEls, _els, Zip, - Xdata, Misc, Address, Instructions, Text, Last, - First, Password, Registered, Date, Phone, State, - Name, Username, Remove, Key, City, Nick, Url, - Email) - end; -decode_register_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"phone">>, _attrs, _} = _el | _els], Zip, - Xdata, Misc, Address, Instructions, Text, Last, First, - Password, Registered, Date, Phone, State, Name, - Username, Remove, Key, City, Nick, Url, Email) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_register_els(__TopXMLNS, __IgnoreEls, _els, Zip, - Xdata, Misc, Address, Instructions, Text, Last, - First, Password, Registered, Date, - decode_register_phone(__TopXMLNS, __IgnoreEls, - _el), - State, Name, Username, Remove, Key, City, Nick, - Url, Email); - true -> - decode_register_els(__TopXMLNS, __IgnoreEls, _els, Zip, - Xdata, Misc, Address, Instructions, Text, Last, - First, Password, Registered, Date, Phone, State, - Name, Username, Remove, Key, City, Nick, Url, - Email) - end; -decode_register_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"url">>, _attrs, _} = _el | _els], Zip, - Xdata, Misc, Address, Instructions, Text, Last, First, - Password, Registered, Date, Phone, State, Name, - Username, Remove, Key, City, Nick, Url, Email) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_register_els(__TopXMLNS, __IgnoreEls, _els, Zip, - Xdata, Misc, Address, Instructions, Text, Last, - First, Password, Registered, Date, Phone, State, - Name, Username, Remove, Key, City, Nick, - decode_register_url(__TopXMLNS, __IgnoreEls, - _el), - Email); - true -> - decode_register_els(__TopXMLNS, __IgnoreEls, _els, Zip, - Xdata, Misc, Address, Instructions, Text, Last, - First, Password, Registered, Date, Phone, State, - Name, Username, Remove, Key, City, Nick, Url, - Email) - end; -decode_register_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"date">>, _attrs, _} = _el | _els], Zip, - Xdata, Misc, Address, Instructions, Text, Last, First, - Password, Registered, Date, Phone, State, Name, - Username, Remove, Key, City, Nick, Url, Email) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_register_els(__TopXMLNS, __IgnoreEls, _els, Zip, - Xdata, Misc, Address, Instructions, Text, Last, - First, Password, Registered, - decode_register_date(__TopXMLNS, __IgnoreEls, - _el), - Phone, State, Name, Username, Remove, Key, City, - Nick, Url, Email); - true -> - decode_register_els(__TopXMLNS, __IgnoreEls, _els, Zip, - Xdata, Misc, Address, Instructions, Text, Last, - First, Password, Registered, Date, Phone, State, - Name, Username, Remove, Key, City, Nick, Url, - Email) - end; -decode_register_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"misc">>, _attrs, _} = _el | _els], Zip, - Xdata, Misc, Address, Instructions, Text, Last, First, - Password, Registered, Date, Phone, State, Name, - Username, Remove, Key, City, Nick, Url, Email) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_register_els(__TopXMLNS, __IgnoreEls, _els, Zip, - Xdata, - decode_register_misc(__TopXMLNS, __IgnoreEls, - _el), - Address, Instructions, Text, Last, First, - Password, Registered, Date, Phone, State, Name, - Username, Remove, Key, City, Nick, Url, Email); - true -> - decode_register_els(__TopXMLNS, __IgnoreEls, _els, Zip, - Xdata, Misc, Address, Instructions, Text, Last, - First, Password, Registered, Date, Phone, State, - Name, Username, Remove, Key, City, Nick, Url, - Email) - end; -decode_register_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"text">>, _attrs, _} = _el | _els], Zip, - Xdata, Misc, Address, Instructions, Text, Last, First, - Password, Registered, Date, Phone, State, Name, - Username, Remove, Key, City, Nick, Url, Email) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_register_els(__TopXMLNS, __IgnoreEls, _els, Zip, - Xdata, Misc, Address, Instructions, - decode_register_text(__TopXMLNS, __IgnoreEls, - _el), - Last, First, Password, Registered, Date, Phone, - State, Name, Username, Remove, Key, City, Nick, - Url, Email); - true -> - decode_register_els(__TopXMLNS, __IgnoreEls, _els, Zip, - Xdata, Misc, Address, Instructions, Text, Last, - First, Password, Registered, Date, Phone, State, - Name, Username, Remove, Key, City, Nick, Url, - Email) - end; -decode_register_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"key">>, _attrs, _} = _el | _els], Zip, - Xdata, Misc, Address, Instructions, Text, Last, First, - Password, Registered, Date, Phone, State, Name, - Username, Remove, Key, City, Nick, Url, Email) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_register_els(__TopXMLNS, __IgnoreEls, _els, Zip, - Xdata, Misc, Address, Instructions, Text, Last, - First, Password, Registered, Date, Phone, State, - Name, Username, Remove, - decode_register_key(__TopXMLNS, __IgnoreEls, - _el), - City, Nick, Url, Email); - true -> - decode_register_els(__TopXMLNS, __IgnoreEls, _els, Zip, - Xdata, Misc, Address, Instructions, Text, Last, - First, Password, Registered, Date, Phone, State, - Name, Username, Remove, Key, City, Nick, Url, - Email) - end; -decode_register_els(__TopXMLNS, __IgnoreEls, [_ | _els], - Zip, Xdata, Misc, Address, Instructions, Text, Last, - First, Password, Registered, Date, Phone, State, Name, - Username, Remove, Key, City, Nick, Url, Email) -> - decode_register_els(__TopXMLNS, __IgnoreEls, _els, Zip, - Xdata, Misc, Address, Instructions, Text, Last, First, - Password, Registered, Date, Phone, State, Name, - Username, Remove, Key, City, Nick, Url, Email). - -encode_register({register, Registered, Remove, - Instructions, Username, Nick, Password, Name, First, - Last, Email, Address, City, State, Zip, Phone, Url, - Date, Misc, Text, Key, Xdata}, - _xmlns_attrs) -> - _els = lists:reverse('encode_register_$zip'(Zip, - 'encode_register_$xdata'(Xdata, - 'encode_register_$misc'(Misc, - 'encode_register_$address'(Address, - 'encode_register_$instructions'(Instructions, - 'encode_register_$text'(Text, - 'encode_register_$last'(Last, - 'encode_register_$first'(First, - 'encode_register_$password'(Password, - 'encode_register_$registered'(Registered, - 'encode_register_$date'(Date, - 'encode_register_$phone'(Phone, - 'encode_register_$state'(State, - 'encode_register_$name'(Name, - 'encode_register_$username'(Username, - 'encode_register_$remove'(Remove, - 'encode_register_$key'(Key, - 'encode_register_$city'(City, - 'encode_register_$nick'(Nick, - 'encode_register_$url'(Url, - 'encode_register_$email'(Email, - [])))))))))))))))))))))), - _attrs = _xmlns_attrs, - {xmlel, <<"query">>, _attrs, _els}. - -'encode_register_$zip'(undefined, _acc) -> _acc; -'encode_register_$zip'(Zip, _acc) -> - [encode_register_zip(Zip, []) | _acc]. - -'encode_register_$xdata'(undefined, _acc) -> _acc; -'encode_register_$xdata'(Xdata, _acc) -> - [encode_xdata(Xdata, - [{<<"xmlns">>, <<"jabber:x:data">>}]) - | _acc]. - -'encode_register_$misc'(undefined, _acc) -> _acc; -'encode_register_$misc'(Misc, _acc) -> - [encode_register_misc(Misc, []) | _acc]. - -'encode_register_$address'(undefined, _acc) -> _acc; -'encode_register_$address'(Address, _acc) -> - [encode_register_address(Address, []) | _acc]. - -'encode_register_$instructions'(undefined, _acc) -> - _acc; -'encode_register_$instructions'(Instructions, _acc) -> - [encode_register_instructions(Instructions, []) | _acc]. - -'encode_register_$text'(undefined, _acc) -> _acc; -'encode_register_$text'(Text, _acc) -> - [encode_register_text(Text, []) | _acc]. - -'encode_register_$last'(undefined, _acc) -> _acc; -'encode_register_$last'(Last, _acc) -> - [encode_register_last(Last, []) | _acc]. - -'encode_register_$first'(undefined, _acc) -> _acc; -'encode_register_$first'(First, _acc) -> - [encode_register_first(First, []) | _acc]. - -'encode_register_$password'(undefined, _acc) -> _acc; -'encode_register_$password'(Password, _acc) -> - [encode_register_password(Password, []) | _acc]. - -'encode_register_$registered'(false, _acc) -> _acc; -'encode_register_$registered'(Registered, _acc) -> - [encode_register_registered(Registered, []) | _acc]. - -'encode_register_$date'(undefined, _acc) -> _acc; -'encode_register_$date'(Date, _acc) -> - [encode_register_date(Date, []) | _acc]. - -'encode_register_$phone'(undefined, _acc) -> _acc; -'encode_register_$phone'(Phone, _acc) -> - [encode_register_phone(Phone, []) | _acc]. - -'encode_register_$state'(undefined, _acc) -> _acc; -'encode_register_$state'(State, _acc) -> - [encode_register_state(State, []) | _acc]. - -'encode_register_$name'(undefined, _acc) -> _acc; -'encode_register_$name'(Name, _acc) -> - [encode_register_name(Name, []) | _acc]. - -'encode_register_$username'(undefined, _acc) -> _acc; -'encode_register_$username'(Username, _acc) -> - [encode_register_username(Username, []) | _acc]. - -'encode_register_$remove'(false, _acc) -> _acc; -'encode_register_$remove'(Remove, _acc) -> - [encode_register_remove(Remove, []) | _acc]. - -'encode_register_$key'(undefined, _acc) -> _acc; -'encode_register_$key'(Key, _acc) -> - [encode_register_key(Key, []) | _acc]. - -'encode_register_$city'(undefined, _acc) -> _acc; -'encode_register_$city'(City, _acc) -> - [encode_register_city(City, []) | _acc]. - -'encode_register_$nick'(undefined, _acc) -> _acc; -'encode_register_$nick'(Nick, _acc) -> - [encode_register_nick(Nick, []) | _acc]. - -'encode_register_$url'(undefined, _acc) -> _acc; -'encode_register_$url'(Url, _acc) -> - [encode_register_url(Url, []) | _acc]. - -'encode_register_$email'(undefined, _acc) -> _acc; -'encode_register_$email'(Email, _acc) -> - [encode_register_email(Email, []) | _acc]. - -decode_register_key(__TopXMLNS, __IgnoreEls, - {xmlel, <<"key">>, _attrs, _els}) -> - Cdata = decode_register_key_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - Cdata. - -decode_register_key_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_register_key_cdata(__TopXMLNS, Cdata); -decode_register_key_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_register_key_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_register_key_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_register_key_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_register_key(Cdata, _xmlns_attrs) -> - _els = encode_register_key_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"key">>, _attrs, _els}. - -decode_register_key_cdata(__TopXMLNS, <<>>) -> none; -decode_register_key_cdata(__TopXMLNS, _val) -> _val. - -encode_register_key_cdata(none, _acc) -> _acc; -encode_register_key_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_register_text(__TopXMLNS, __IgnoreEls, - {xmlel, <<"text">>, _attrs, _els}) -> - Cdata = decode_register_text_els(__TopXMLNS, - __IgnoreEls, _els, <<>>), - Cdata. - -decode_register_text_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_register_text_cdata(__TopXMLNS, Cdata); -decode_register_text_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_register_text_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_register_text_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_register_text_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_register_text(Cdata, _xmlns_attrs) -> - _els = encode_register_text_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"text">>, _attrs, _els}. - -decode_register_text_cdata(__TopXMLNS, <<>>) -> none; -decode_register_text_cdata(__TopXMLNS, _val) -> _val. - -encode_register_text_cdata(none, _acc) -> _acc; -encode_register_text_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_register_misc(__TopXMLNS, __IgnoreEls, - {xmlel, <<"misc">>, _attrs, _els}) -> - Cdata = decode_register_misc_els(__TopXMLNS, - __IgnoreEls, _els, <<>>), - Cdata. - -decode_register_misc_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_register_misc_cdata(__TopXMLNS, Cdata); -decode_register_misc_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_register_misc_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_register_misc_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_register_misc_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_register_misc(Cdata, _xmlns_attrs) -> - _els = encode_register_misc_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"misc">>, _attrs, _els}. - -decode_register_misc_cdata(__TopXMLNS, <<>>) -> none; -decode_register_misc_cdata(__TopXMLNS, _val) -> _val. - -encode_register_misc_cdata(none, _acc) -> _acc; -encode_register_misc_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_register_date(__TopXMLNS, __IgnoreEls, - {xmlel, <<"date">>, _attrs, _els}) -> - Cdata = decode_register_date_els(__TopXMLNS, - __IgnoreEls, _els, <<>>), - Cdata. - -decode_register_date_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_register_date_cdata(__TopXMLNS, Cdata); -decode_register_date_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_register_date_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_register_date_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_register_date_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_register_date(Cdata, _xmlns_attrs) -> - _els = encode_register_date_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"date">>, _attrs, _els}. - -decode_register_date_cdata(__TopXMLNS, <<>>) -> none; -decode_register_date_cdata(__TopXMLNS, _val) -> _val. - -encode_register_date_cdata(none, _acc) -> _acc; -encode_register_date_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_register_url(__TopXMLNS, __IgnoreEls, - {xmlel, <<"url">>, _attrs, _els}) -> - Cdata = decode_register_url_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - Cdata. - -decode_register_url_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_register_url_cdata(__TopXMLNS, Cdata); -decode_register_url_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_register_url_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_register_url_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_register_url_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_register_url(Cdata, _xmlns_attrs) -> - _els = encode_register_url_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"url">>, _attrs, _els}. - -decode_register_url_cdata(__TopXMLNS, <<>>) -> none; -decode_register_url_cdata(__TopXMLNS, _val) -> _val. - -encode_register_url_cdata(none, _acc) -> _acc; -encode_register_url_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_register_phone(__TopXMLNS, __IgnoreEls, - {xmlel, <<"phone">>, _attrs, _els}) -> - Cdata = decode_register_phone_els(__TopXMLNS, - __IgnoreEls, _els, <<>>), - Cdata. - -decode_register_phone_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_register_phone_cdata(__TopXMLNS, Cdata); -decode_register_phone_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_register_phone_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_register_phone_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_register_phone_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_register_phone(Cdata, _xmlns_attrs) -> - _els = encode_register_phone_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"phone">>, _attrs, _els}. - -decode_register_phone_cdata(__TopXMLNS, <<>>) -> none; -decode_register_phone_cdata(__TopXMLNS, _val) -> _val. - -encode_register_phone_cdata(none, _acc) -> _acc; -encode_register_phone_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_register_zip(__TopXMLNS, __IgnoreEls, - {xmlel, <<"zip">>, _attrs, _els}) -> - Cdata = decode_register_zip_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - Cdata. - -decode_register_zip_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_register_zip_cdata(__TopXMLNS, Cdata); -decode_register_zip_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_register_zip_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_register_zip_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_register_zip_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_register_zip(Cdata, _xmlns_attrs) -> - _els = encode_register_zip_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"zip">>, _attrs, _els}. - -decode_register_zip_cdata(__TopXMLNS, <<>>) -> none; -decode_register_zip_cdata(__TopXMLNS, _val) -> _val. - -encode_register_zip_cdata(none, _acc) -> _acc; -encode_register_zip_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_register_state(__TopXMLNS, __IgnoreEls, - {xmlel, <<"state">>, _attrs, _els}) -> - Cdata = decode_register_state_els(__TopXMLNS, - __IgnoreEls, _els, <<>>), - Cdata. - -decode_register_state_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_register_state_cdata(__TopXMLNS, Cdata); -decode_register_state_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_register_state_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_register_state_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_register_state_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_register_state(Cdata, _xmlns_attrs) -> - _els = encode_register_state_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"state">>, _attrs, _els}. - -decode_register_state_cdata(__TopXMLNS, <<>>) -> none; -decode_register_state_cdata(__TopXMLNS, _val) -> _val. - -encode_register_state_cdata(none, _acc) -> _acc; -encode_register_state_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_register_city(__TopXMLNS, __IgnoreEls, - {xmlel, <<"city">>, _attrs, _els}) -> - Cdata = decode_register_city_els(__TopXMLNS, - __IgnoreEls, _els, <<>>), - Cdata. - -decode_register_city_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_register_city_cdata(__TopXMLNS, Cdata); -decode_register_city_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_register_city_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_register_city_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_register_city_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_register_city(Cdata, _xmlns_attrs) -> - _els = encode_register_city_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"city">>, _attrs, _els}. - -decode_register_city_cdata(__TopXMLNS, <<>>) -> none; -decode_register_city_cdata(__TopXMLNS, _val) -> _val. - -encode_register_city_cdata(none, _acc) -> _acc; -encode_register_city_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_register_address(__TopXMLNS, __IgnoreEls, - {xmlel, <<"address">>, _attrs, _els}) -> - Cdata = decode_register_address_els(__TopXMLNS, - __IgnoreEls, _els, <<>>), - Cdata. - -decode_register_address_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_register_address_cdata(__TopXMLNS, Cdata); -decode_register_address_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_register_address_els(__TopXMLNS, __IgnoreEls, - _els, <<Cdata/binary, _data/binary>>); -decode_register_address_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_register_address_els(__TopXMLNS, __IgnoreEls, - _els, Cdata). - -encode_register_address(Cdata, _xmlns_attrs) -> - _els = encode_register_address_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"address">>, _attrs, _els}. - -decode_register_address_cdata(__TopXMLNS, <<>>) -> none; -decode_register_address_cdata(__TopXMLNS, _val) -> _val. - -encode_register_address_cdata(none, _acc) -> _acc; -encode_register_address_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_register_email(__TopXMLNS, __IgnoreEls, - {xmlel, <<"email">>, _attrs, _els}) -> - Cdata = decode_register_email_els(__TopXMLNS, - __IgnoreEls, _els, <<>>), - Cdata. - -decode_register_email_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_register_email_cdata(__TopXMLNS, Cdata); -decode_register_email_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_register_email_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_register_email_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_register_email_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_register_email(Cdata, _xmlns_attrs) -> - _els = encode_register_email_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"email">>, _attrs, _els}. - -decode_register_email_cdata(__TopXMLNS, <<>>) -> none; -decode_register_email_cdata(__TopXMLNS, _val) -> _val. - -encode_register_email_cdata(none, _acc) -> _acc; -encode_register_email_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_register_last(__TopXMLNS, __IgnoreEls, - {xmlel, <<"last">>, _attrs, _els}) -> - Cdata = decode_register_last_els(__TopXMLNS, - __IgnoreEls, _els, <<>>), - Cdata. - -decode_register_last_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_register_last_cdata(__TopXMLNS, Cdata); -decode_register_last_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_register_last_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_register_last_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_register_last_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_register_last(Cdata, _xmlns_attrs) -> - _els = encode_register_last_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"last">>, _attrs, _els}. - -decode_register_last_cdata(__TopXMLNS, <<>>) -> none; -decode_register_last_cdata(__TopXMLNS, _val) -> _val. - -encode_register_last_cdata(none, _acc) -> _acc; -encode_register_last_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_register_first(__TopXMLNS, __IgnoreEls, - {xmlel, <<"first">>, _attrs, _els}) -> - Cdata = decode_register_first_els(__TopXMLNS, - __IgnoreEls, _els, <<>>), - Cdata. - -decode_register_first_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_register_first_cdata(__TopXMLNS, Cdata); -decode_register_first_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_register_first_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_register_first_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_register_first_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_register_first(Cdata, _xmlns_attrs) -> - _els = encode_register_first_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"first">>, _attrs, _els}. - -decode_register_first_cdata(__TopXMLNS, <<>>) -> none; -decode_register_first_cdata(__TopXMLNS, _val) -> _val. - -encode_register_first_cdata(none, _acc) -> _acc; -encode_register_first_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_register_name(__TopXMLNS, __IgnoreEls, - {xmlel, <<"name">>, _attrs, _els}) -> - Cdata = decode_register_name_els(__TopXMLNS, - __IgnoreEls, _els, <<>>), - Cdata. - -decode_register_name_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_register_name_cdata(__TopXMLNS, Cdata); -decode_register_name_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_register_name_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_register_name_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_register_name_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_register_name(Cdata, _xmlns_attrs) -> - _els = encode_register_name_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"name">>, _attrs, _els}. - -decode_register_name_cdata(__TopXMLNS, <<>>) -> none; -decode_register_name_cdata(__TopXMLNS, _val) -> _val. - -encode_register_name_cdata(none, _acc) -> _acc; -encode_register_name_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_register_password(__TopXMLNS, __IgnoreEls, - {xmlel, <<"password">>, _attrs, _els}) -> - Cdata = decode_register_password_els(__TopXMLNS, - __IgnoreEls, _els, <<>>), - Cdata. - -decode_register_password_els(__TopXMLNS, __IgnoreEls, - [], Cdata) -> - decode_register_password_cdata(__TopXMLNS, Cdata); -decode_register_password_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_register_password_els(__TopXMLNS, __IgnoreEls, - _els, <<Cdata/binary, _data/binary>>); -decode_register_password_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_register_password_els(__TopXMLNS, __IgnoreEls, - _els, Cdata). - -encode_register_password(Cdata, _xmlns_attrs) -> - _els = encode_register_password_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"password">>, _attrs, _els}. - -decode_register_password_cdata(__TopXMLNS, <<>>) -> - none; -decode_register_password_cdata(__TopXMLNS, _val) -> - _val. - -encode_register_password_cdata(none, _acc) -> _acc; -encode_register_password_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_register_nick(__TopXMLNS, __IgnoreEls, - {xmlel, <<"nick">>, _attrs, _els}) -> - Cdata = decode_register_nick_els(__TopXMLNS, - __IgnoreEls, _els, <<>>), - Cdata. - -decode_register_nick_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_register_nick_cdata(__TopXMLNS, Cdata); -decode_register_nick_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_register_nick_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_register_nick_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_register_nick_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_register_nick(Cdata, _xmlns_attrs) -> - _els = encode_register_nick_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"nick">>, _attrs, _els}. - -decode_register_nick_cdata(__TopXMLNS, <<>>) -> none; -decode_register_nick_cdata(__TopXMLNS, _val) -> _val. - -encode_register_nick_cdata(none, _acc) -> _acc; -encode_register_nick_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_register_username(__TopXMLNS, __IgnoreEls, - {xmlel, <<"username">>, _attrs, _els}) -> - Cdata = decode_register_username_els(__TopXMLNS, - __IgnoreEls, _els, <<>>), - Cdata. - -decode_register_username_els(__TopXMLNS, __IgnoreEls, - [], Cdata) -> - decode_register_username_cdata(__TopXMLNS, Cdata); -decode_register_username_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_register_username_els(__TopXMLNS, __IgnoreEls, - _els, <<Cdata/binary, _data/binary>>); -decode_register_username_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_register_username_els(__TopXMLNS, __IgnoreEls, - _els, Cdata). - -encode_register_username(Cdata, _xmlns_attrs) -> - _els = encode_register_username_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"username">>, _attrs, _els}. - -decode_register_username_cdata(__TopXMLNS, <<>>) -> - none; -decode_register_username_cdata(__TopXMLNS, _val) -> - _val. - -encode_register_username_cdata(none, _acc) -> _acc; -encode_register_username_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_register_instructions(__TopXMLNS, __IgnoreEls, - {xmlel, <<"instructions">>, _attrs, _els}) -> - Cdata = decode_register_instructions_els(__TopXMLNS, - __IgnoreEls, _els, <<>>), - Cdata. - -decode_register_instructions_els(__TopXMLNS, - __IgnoreEls, [], Cdata) -> - decode_register_instructions_cdata(__TopXMLNS, Cdata); -decode_register_instructions_els(__TopXMLNS, - __IgnoreEls, [{xmlcdata, _data} | _els], - Cdata) -> - decode_register_instructions_els(__TopXMLNS, - __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_register_instructions_els(__TopXMLNS, - __IgnoreEls, [_ | _els], Cdata) -> - decode_register_instructions_els(__TopXMLNS, - __IgnoreEls, _els, Cdata). - -encode_register_instructions(Cdata, _xmlns_attrs) -> - _els = encode_register_instructions_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"instructions">>, _attrs, _els}. - -decode_register_instructions_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_register_instructions_cdata(__TopXMLNS, _val) -> - _val. - -encode_register_instructions_cdata(undefined, _acc) -> - _acc; -encode_register_instructions_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_register_remove(__TopXMLNS, __IgnoreEls, - {xmlel, <<"remove">>, _attrs, _els}) -> - true. - -encode_register_remove(true, _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"remove">>, _attrs, _els}. - -decode_register_registered(__TopXMLNS, __IgnoreEls, - {xmlel, <<"registered">>, _attrs, _els}) -> - true. - -encode_register_registered(true, _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"registered">>, _attrs, _els}. - -decode_feature_register(__TopXMLNS, __IgnoreEls, - {xmlel, <<"register">>, _attrs, _els}) -> - {feature_register}. - -encode_feature_register({feature_register}, - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"register">>, _attrs, _els}. - -decode_caps(__TopXMLNS, __IgnoreEls, - {xmlel, <<"c">>, _attrs, _els}) -> - {Hash, Node, Ver} = decode_caps_attrs(__TopXMLNS, - _attrs, undefined, undefined, - undefined), - {caps, Hash, Node, Ver}. - -decode_caps_attrs(__TopXMLNS, - [{<<"hash">>, _val} | _attrs], _Hash, Node, Ver) -> - decode_caps_attrs(__TopXMLNS, _attrs, _val, Node, Ver); -decode_caps_attrs(__TopXMLNS, - [{<<"node">>, _val} | _attrs], Hash, _Node, Ver) -> - decode_caps_attrs(__TopXMLNS, _attrs, Hash, _val, Ver); -decode_caps_attrs(__TopXMLNS, - [{<<"ver">>, _val} | _attrs], Hash, Node, _Ver) -> - decode_caps_attrs(__TopXMLNS, _attrs, Hash, Node, _val); -decode_caps_attrs(__TopXMLNS, [_ | _attrs], Hash, Node, - Ver) -> - decode_caps_attrs(__TopXMLNS, _attrs, Hash, Node, Ver); -decode_caps_attrs(__TopXMLNS, [], Hash, Node, Ver) -> - {decode_caps_attr_hash(__TopXMLNS, Hash), - decode_caps_attr_node(__TopXMLNS, Node), - decode_caps_attr_ver(__TopXMLNS, Ver)}. - -encode_caps({caps, Hash, Node, Ver}, _xmlns_attrs) -> - _els = [], - _attrs = encode_caps_attr_ver(Ver, - encode_caps_attr_node(Node, - encode_caps_attr_hash(Hash, - _xmlns_attrs))), - {xmlel, <<"c">>, _attrs, _els}. - -decode_caps_attr_hash(__TopXMLNS, undefined) -> - undefined; -decode_caps_attr_hash(__TopXMLNS, _val) -> _val. - -encode_caps_attr_hash(undefined, _acc) -> _acc; -encode_caps_attr_hash(_val, _acc) -> - [{<<"hash">>, _val} | _acc]. - -decode_caps_attr_node(__TopXMLNS, undefined) -> - undefined; -decode_caps_attr_node(__TopXMLNS, _val) -> _val. - -encode_caps_attr_node(undefined, _acc) -> _acc; -encode_caps_attr_node(_val, _acc) -> - [{<<"node">>, _val} | _acc]. - -decode_caps_attr_ver(__TopXMLNS, undefined) -> - undefined; -decode_caps_attr_ver(__TopXMLNS, _val) -> - case catch base64:decode(_val) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"ver">>, <<"c">>, __TopXMLNS}}); - _res -> _res - end. - -encode_caps_attr_ver(undefined, _acc) -> _acc; -encode_caps_attr_ver(_val, _acc) -> - [{<<"ver">>, base64:encode(_val)} | _acc]. - -decode_p1_ack(__TopXMLNS, __IgnoreEls, - {xmlel, <<"ack">>, _attrs, _els}) -> - {p1_ack}. - -encode_p1_ack({p1_ack}, _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"ack">>, _attrs, _els}. - -decode_p1_rebind(__TopXMLNS, __IgnoreEls, - {xmlel, <<"rebind">>, _attrs, _els}) -> - {p1_rebind}. - -encode_p1_rebind({p1_rebind}, _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"rebind">>, _attrs, _els}. - -decode_p1_push(__TopXMLNS, __IgnoreEls, - {xmlel, <<"push">>, _attrs, _els}) -> - {p1_push}. - -encode_p1_push({p1_push}, _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"push">>, _attrs, _els}. - -decode_stream_features(__TopXMLNS, __IgnoreEls, - {xmlel, <<"stream:features">>, _attrs, _els}) -> - __Els = decode_stream_features_els(__TopXMLNS, - __IgnoreEls, _els, []), - {stream_features, __Els}. - -decode_stream_features_els(__TopXMLNS, __IgnoreEls, [], - __Els) -> - lists:reverse(__Els); -decode_stream_features_els(__TopXMLNS, __IgnoreEls, - [{xmlel, _, _, _} = _el | _els], __Els) -> - if __IgnoreEls -> - decode_stream_features_els(__TopXMLNS, __IgnoreEls, - _els, [_el | __Els]); - true -> - case is_known_tag(_el) of - true -> - decode_stream_features_els(__TopXMLNS, __IgnoreEls, - _els, [decode(_el) | __Els]); - false -> - decode_stream_features_els(__TopXMLNS, __IgnoreEls, - _els, __Els) - end - end; -decode_stream_features_els(__TopXMLNS, __IgnoreEls, - [_ | _els], __Els) -> - decode_stream_features_els(__TopXMLNS, __IgnoreEls, - _els, __Els). - -encode_stream_features({stream_features, __Els}, - _xmlns_attrs) -> - _els = [encode(_el) || _el <- __Els], - _attrs = _xmlns_attrs, - {xmlel, <<"stream:features">>, _attrs, _els}. - -decode_compression(__TopXMLNS, __IgnoreEls, - {xmlel, <<"compression">>, _attrs, _els}) -> - Methods = decode_compression_els(__TopXMLNS, - __IgnoreEls, _els, []), - {compression, Methods}. - -decode_compression_els(__TopXMLNS, __IgnoreEls, [], - Methods) -> - lists:reverse(Methods); -decode_compression_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"method">>, _attrs, _} = _el | _els], - Methods) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_compression_els(__TopXMLNS, __IgnoreEls, _els, - case decode_compression_method(__TopXMLNS, - __IgnoreEls, - _el) - of - undefined -> Methods; - _new_el -> [_new_el | Methods] - end); - true -> - decode_compression_els(__TopXMLNS, __IgnoreEls, _els, - Methods) - end; -decode_compression_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Methods) -> - decode_compression_els(__TopXMLNS, __IgnoreEls, _els, - Methods). - -encode_compression({compression, Methods}, - _xmlns_attrs) -> - _els = - lists:reverse('encode_compression_$methods'(Methods, - [])), - _attrs = _xmlns_attrs, - {xmlel, <<"compression">>, _attrs, _els}. - -'encode_compression_$methods'([], _acc) -> _acc; -'encode_compression_$methods'([Methods | _els], _acc) -> - 'encode_compression_$methods'(_els, - [encode_compression_method(Methods, []) - | _acc]). - -decode_compression_method(__TopXMLNS, __IgnoreEls, - {xmlel, <<"method">>, _attrs, _els}) -> - Cdata = decode_compression_method_els(__TopXMLNS, - __IgnoreEls, _els, <<>>), - Cdata. - -decode_compression_method_els(__TopXMLNS, __IgnoreEls, - [], Cdata) -> - decode_compression_method_cdata(__TopXMLNS, Cdata); -decode_compression_method_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_compression_method_els(__TopXMLNS, __IgnoreEls, - _els, <<Cdata/binary, _data/binary>>); -decode_compression_method_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_compression_method_els(__TopXMLNS, __IgnoreEls, - _els, Cdata). - -encode_compression_method(Cdata, _xmlns_attrs) -> - _els = encode_compression_method_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"method">>, _attrs, _els}. - -decode_compression_method_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_compression_method_cdata(__TopXMLNS, _val) -> - _val. - -encode_compression_method_cdata(undefined, _acc) -> - _acc; -encode_compression_method_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_compressed(__TopXMLNS, __IgnoreEls, - {xmlel, <<"compressed">>, _attrs, _els}) -> - {compressed}. - -encode_compressed({compressed}, _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"compressed">>, _attrs, _els}. - -decode_compress(__TopXMLNS, __IgnoreEls, - {xmlel, <<"compress">>, _attrs, _els}) -> - Methods = decode_compress_els(__TopXMLNS, __IgnoreEls, - _els, []), - {compress, Methods}. - -decode_compress_els(__TopXMLNS, __IgnoreEls, [], - Methods) -> - lists:reverse(Methods); -decode_compress_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"method">>, _attrs, _} = _el | _els], - Methods) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_compress_els(__TopXMLNS, __IgnoreEls, _els, - case decode_compress_method(__TopXMLNS, - __IgnoreEls, _el) - of - undefined -> Methods; - _new_el -> [_new_el | Methods] - end); - true -> - decode_compress_els(__TopXMLNS, __IgnoreEls, _els, - Methods) - end; -decode_compress_els(__TopXMLNS, __IgnoreEls, [_ | _els], - Methods) -> - decode_compress_els(__TopXMLNS, __IgnoreEls, _els, - Methods). - -encode_compress({compress, Methods}, _xmlns_attrs) -> - _els = lists:reverse('encode_compress_$methods'(Methods, - [])), - _attrs = _xmlns_attrs, - {xmlel, <<"compress">>, _attrs, _els}. - -'encode_compress_$methods'([], _acc) -> _acc; -'encode_compress_$methods'([Methods | _els], _acc) -> - 'encode_compress_$methods'(_els, - [encode_compress_method(Methods, []) | _acc]). - -decode_compress_method(__TopXMLNS, __IgnoreEls, - {xmlel, <<"method">>, _attrs, _els}) -> - Cdata = decode_compress_method_els(__TopXMLNS, - __IgnoreEls, _els, <<>>), - Cdata. - -decode_compress_method_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_compress_method_cdata(__TopXMLNS, Cdata); -decode_compress_method_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_compress_method_els(__TopXMLNS, __IgnoreEls, - _els, <<Cdata/binary, _data/binary>>); -decode_compress_method_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_compress_method_els(__TopXMLNS, __IgnoreEls, - _els, Cdata). - -encode_compress_method(Cdata, _xmlns_attrs) -> - _els = encode_compress_method_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"method">>, _attrs, _els}. - -decode_compress_method_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_compress_method_cdata(__TopXMLNS, _val) -> _val. - -encode_compress_method_cdata(undefined, _acc) -> _acc; -encode_compress_method_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_compress_failure(__TopXMLNS, __IgnoreEls, - {xmlel, <<"failure">>, _attrs, _els}) -> - Reason = decode_compress_failure_els(__TopXMLNS, - __IgnoreEls, _els, undefined), - {compress_failure, Reason}. - -decode_compress_failure_els(__TopXMLNS, __IgnoreEls, [], - Reason) -> - Reason; -decode_compress_failure_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"setup-failed">>, _attrs, _} = _el - | _els], - Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_compress_failure_els(__TopXMLNS, __IgnoreEls, - _els, - decode_compress_failure_setup_failed(__TopXMLNS, - __IgnoreEls, - _el)); - true -> - decode_compress_failure_els(__TopXMLNS, __IgnoreEls, - _els, Reason) - end; -decode_compress_failure_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"processing-failed">>, _attrs, _} = _el - | _els], - Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_compress_failure_els(__TopXMLNS, __IgnoreEls, - _els, - decode_compress_failure_processing_failed(__TopXMLNS, - __IgnoreEls, - _el)); - true -> - decode_compress_failure_els(__TopXMLNS, __IgnoreEls, - _els, Reason) - end; -decode_compress_failure_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"unsupported-method">>, _attrs, _} = _el - | _els], - Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_compress_failure_els(__TopXMLNS, __IgnoreEls, - _els, - decode_compress_failure_unsupported_method(__TopXMLNS, - __IgnoreEls, - _el)); - true -> - decode_compress_failure_els(__TopXMLNS, __IgnoreEls, - _els, Reason) - end; -decode_compress_failure_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Reason) -> - decode_compress_failure_els(__TopXMLNS, __IgnoreEls, - _els, Reason). - -encode_compress_failure({compress_failure, Reason}, - _xmlns_attrs) -> - _els = - lists:reverse('encode_compress_failure_$reason'(Reason, - [])), - _attrs = _xmlns_attrs, - {xmlel, <<"failure">>, _attrs, _els}. - -'encode_compress_failure_$reason'(undefined, _acc) -> - _acc; -'encode_compress_failure_$reason'('setup-failed' = - Reason, - _acc) -> - [encode_compress_failure_setup_failed(Reason, []) - | _acc]; -'encode_compress_failure_$reason'('processing-failed' = - Reason, - _acc) -> - [encode_compress_failure_processing_failed(Reason, []) - | _acc]; -'encode_compress_failure_$reason'('unsupported-method' = - Reason, - _acc) -> - [encode_compress_failure_unsupported_method(Reason, []) - | _acc]. - -decode_compress_failure_unsupported_method(__TopXMLNS, - __IgnoreEls, - {xmlel, <<"unsupported-method">>, - _attrs, _els}) -> - 'unsupported-method'. - -encode_compress_failure_unsupported_method('unsupported-method', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"unsupported-method">>, _attrs, _els}. - -decode_compress_failure_processing_failed(__TopXMLNS, - __IgnoreEls, - {xmlel, <<"processing-failed">>, - _attrs, _els}) -> - 'processing-failed'. - -encode_compress_failure_processing_failed('processing-failed', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"processing-failed">>, _attrs, _els}. - -decode_compress_failure_setup_failed(__TopXMLNS, - __IgnoreEls, - {xmlel, <<"setup-failed">>, _attrs, - _els}) -> - 'setup-failed'. - -encode_compress_failure_setup_failed('setup-failed', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"setup-failed">>, _attrs, _els}. - -decode_starttls_failure(__TopXMLNS, __IgnoreEls, - {xmlel, <<"failure">>, _attrs, _els}) -> - {starttls_failure}. - -encode_starttls_failure({starttls_failure}, - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"failure">>, _attrs, _els}. - -decode_starttls_proceed(__TopXMLNS, __IgnoreEls, - {xmlel, <<"proceed">>, _attrs, _els}) -> - {starttls_proceed}. - -encode_starttls_proceed({starttls_proceed}, - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"proceed">>, _attrs, _els}. - -decode_starttls(__TopXMLNS, __IgnoreEls, - {xmlel, <<"starttls">>, _attrs, _els}) -> - Required = decode_starttls_els(__TopXMLNS, __IgnoreEls, - _els, false), - {starttls, Required}. - -decode_starttls_els(__TopXMLNS, __IgnoreEls, [], - Required) -> - Required; -decode_starttls_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"required">>, _attrs, _} = _el | _els], - Required) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_starttls_els(__TopXMLNS, __IgnoreEls, _els, - decode_starttls_required(__TopXMLNS, __IgnoreEls, - _el)); - true -> - decode_starttls_els(__TopXMLNS, __IgnoreEls, _els, - Required) - end; -decode_starttls_els(__TopXMLNS, __IgnoreEls, [_ | _els], - Required) -> - decode_starttls_els(__TopXMLNS, __IgnoreEls, _els, - Required). - -encode_starttls({starttls, Required}, _xmlns_attrs) -> - _els = - lists:reverse('encode_starttls_$required'(Required, - [])), - _attrs = _xmlns_attrs, - {xmlel, <<"starttls">>, _attrs, _els}. - -'encode_starttls_$required'(false, _acc) -> _acc; -'encode_starttls_$required'(Required, _acc) -> - [encode_starttls_required(Required, []) | _acc]. - -decode_starttls_required(__TopXMLNS, __IgnoreEls, - {xmlel, <<"required">>, _attrs, _els}) -> - true. - -encode_starttls_required(true, _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"required">>, _attrs, _els}. - -decode_sasl_mechanisms(__TopXMLNS, __IgnoreEls, - {xmlel, <<"mechanisms">>, _attrs, _els}) -> - List = decode_sasl_mechanisms_els(__TopXMLNS, - __IgnoreEls, _els, []), - {sasl_mechanisms, List}. - -decode_sasl_mechanisms_els(__TopXMLNS, __IgnoreEls, [], - List) -> - lists:reverse(List); -decode_sasl_mechanisms_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"mechanism">>, _attrs, _} = _el | _els], - List) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_sasl_mechanisms_els(__TopXMLNS, __IgnoreEls, - _els, - case decode_sasl_mechanism(__TopXMLNS, - __IgnoreEls, - _el) - of - undefined -> List; - _new_el -> [_new_el | List] - end); - true -> - decode_sasl_mechanisms_els(__TopXMLNS, __IgnoreEls, - _els, List) - end; -decode_sasl_mechanisms_els(__TopXMLNS, __IgnoreEls, - [_ | _els], List) -> - decode_sasl_mechanisms_els(__TopXMLNS, __IgnoreEls, - _els, List). - -encode_sasl_mechanisms({sasl_mechanisms, List}, - _xmlns_attrs) -> - _els = - lists:reverse('encode_sasl_mechanisms_$list'(List, [])), - _attrs = _xmlns_attrs, - {xmlel, <<"mechanisms">>, _attrs, _els}. - -'encode_sasl_mechanisms_$list'([], _acc) -> _acc; -'encode_sasl_mechanisms_$list'([List | _els], _acc) -> - 'encode_sasl_mechanisms_$list'(_els, - [encode_sasl_mechanism(List, []) | _acc]). - -decode_sasl_mechanism(__TopXMLNS, __IgnoreEls, - {xmlel, <<"mechanism">>, _attrs, _els}) -> - Cdata = decode_sasl_mechanism_els(__TopXMLNS, - __IgnoreEls, _els, <<>>), - Cdata. - -decode_sasl_mechanism_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_sasl_mechanism_cdata(__TopXMLNS, Cdata); -decode_sasl_mechanism_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_sasl_mechanism_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_sasl_mechanism_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_sasl_mechanism_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_sasl_mechanism(Cdata, _xmlns_attrs) -> - _els = encode_sasl_mechanism_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"mechanism">>, _attrs, _els}. - -decode_sasl_mechanism_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_sasl_mechanism_cdata(__TopXMLNS, _val) -> _val. - -encode_sasl_mechanism_cdata(undefined, _acc) -> _acc; -encode_sasl_mechanism_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_sasl_failure(__TopXMLNS, __IgnoreEls, - {xmlel, <<"failure">>, _attrs, _els}) -> - {Text, Reason} = decode_sasl_failure_els(__TopXMLNS, - __IgnoreEls, _els, [], undefined), - {sasl_failure, Reason, Text}. - -decode_sasl_failure_els(__TopXMLNS, __IgnoreEls, [], - Text, Reason) -> - {lists:reverse(Text), Reason}; -decode_sasl_failure_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"text">>, _attrs, _} = _el | _els], Text, - Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_sasl_failure_els(__TopXMLNS, __IgnoreEls, _els, - [decode_sasl_failure_text(__TopXMLNS, - __IgnoreEls, _el) - | Text], - Reason); - true -> - decode_sasl_failure_els(__TopXMLNS, __IgnoreEls, _els, - Text, Reason) - end; -decode_sasl_failure_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"aborted">>, _attrs, _} = _el | _els], Text, - Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_sasl_failure_els(__TopXMLNS, __IgnoreEls, _els, - Text, - decode_sasl_failure_aborted(__TopXMLNS, - __IgnoreEls, - _el)); - true -> - decode_sasl_failure_els(__TopXMLNS, __IgnoreEls, _els, - Text, Reason) - end; -decode_sasl_failure_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"account-disabled">>, _attrs, _} = _el - | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_sasl_failure_els(__TopXMLNS, __IgnoreEls, _els, - Text, - decode_sasl_failure_account_disabled(__TopXMLNS, - __IgnoreEls, - _el)); - true -> - decode_sasl_failure_els(__TopXMLNS, __IgnoreEls, _els, - Text, Reason) - end; -decode_sasl_failure_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"credentials-expired">>, _attrs, _} = _el - | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_sasl_failure_els(__TopXMLNS, __IgnoreEls, _els, - Text, - decode_sasl_failure_credentials_expired(__TopXMLNS, - __IgnoreEls, - _el)); - true -> - decode_sasl_failure_els(__TopXMLNS, __IgnoreEls, _els, - Text, Reason) - end; -decode_sasl_failure_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"encryption-required">>, _attrs, _} = _el - | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_sasl_failure_els(__TopXMLNS, __IgnoreEls, _els, - Text, - decode_sasl_failure_encryption_required(__TopXMLNS, - __IgnoreEls, - _el)); - true -> - decode_sasl_failure_els(__TopXMLNS, __IgnoreEls, _els, - Text, Reason) - end; -decode_sasl_failure_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"incorrect-encoding">>, _attrs, _} = _el - | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_sasl_failure_els(__TopXMLNS, __IgnoreEls, _els, - Text, - decode_sasl_failure_incorrect_encoding(__TopXMLNS, - __IgnoreEls, - _el)); - true -> - decode_sasl_failure_els(__TopXMLNS, __IgnoreEls, _els, - Text, Reason) - end; -decode_sasl_failure_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"invalid-authzid">>, _attrs, _} = _el - | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_sasl_failure_els(__TopXMLNS, __IgnoreEls, _els, - Text, - decode_sasl_failure_invalid_authzid(__TopXMLNS, - __IgnoreEls, - _el)); - true -> - decode_sasl_failure_els(__TopXMLNS, __IgnoreEls, _els, - Text, Reason) - end; -decode_sasl_failure_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"invalid-mechanism">>, _attrs, _} = _el - | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_sasl_failure_els(__TopXMLNS, __IgnoreEls, _els, - Text, - decode_sasl_failure_invalid_mechanism(__TopXMLNS, - __IgnoreEls, - _el)); - true -> - decode_sasl_failure_els(__TopXMLNS, __IgnoreEls, _els, - Text, Reason) - end; -decode_sasl_failure_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"malformed-request">>, _attrs, _} = _el - | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_sasl_failure_els(__TopXMLNS, __IgnoreEls, _els, - Text, - decode_sasl_failure_malformed_request(__TopXMLNS, - __IgnoreEls, - _el)); - true -> - decode_sasl_failure_els(__TopXMLNS, __IgnoreEls, _els, - Text, Reason) - end; -decode_sasl_failure_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"mechanism-too-weak">>, _attrs, _} = _el - | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_sasl_failure_els(__TopXMLNS, __IgnoreEls, _els, - Text, - decode_sasl_failure_mechanism_too_weak(__TopXMLNS, - __IgnoreEls, - _el)); - true -> - decode_sasl_failure_els(__TopXMLNS, __IgnoreEls, _els, - Text, Reason) - end; -decode_sasl_failure_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"not-authorized">>, _attrs, _} = _el | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_sasl_failure_els(__TopXMLNS, __IgnoreEls, _els, - Text, - decode_sasl_failure_not_authorized(__TopXMLNS, - __IgnoreEls, - _el)); - true -> - decode_sasl_failure_els(__TopXMLNS, __IgnoreEls, _els, - Text, Reason) - end; -decode_sasl_failure_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"temporary-auth-failure">>, _attrs, _} = _el - | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_sasl_failure_els(__TopXMLNS, __IgnoreEls, _els, - Text, - decode_sasl_failure_temporary_auth_failure(__TopXMLNS, - __IgnoreEls, - _el)); - true -> - decode_sasl_failure_els(__TopXMLNS, __IgnoreEls, _els, - Text, Reason) - end; -decode_sasl_failure_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Text, Reason) -> - decode_sasl_failure_els(__TopXMLNS, __IgnoreEls, _els, - Text, Reason). - -encode_sasl_failure({sasl_failure, Reason, Text}, - _xmlns_attrs) -> - _els = lists:reverse('encode_sasl_failure_$text'(Text, - 'encode_sasl_failure_$reason'(Reason, - []))), - _attrs = _xmlns_attrs, - {xmlel, <<"failure">>, _attrs, _els}. - -'encode_sasl_failure_$text'([], _acc) -> _acc; -'encode_sasl_failure_$text'([Text | _els], _acc) -> - 'encode_sasl_failure_$text'(_els, - [encode_sasl_failure_text(Text, []) | _acc]). - -'encode_sasl_failure_$reason'(undefined, _acc) -> _acc; -'encode_sasl_failure_$reason'(aborted = Reason, _acc) -> - [encode_sasl_failure_aborted(Reason, []) | _acc]; -'encode_sasl_failure_$reason'('account-disabled' = - Reason, - _acc) -> - [encode_sasl_failure_account_disabled(Reason, []) - | _acc]; -'encode_sasl_failure_$reason'('credentials-expired' = - Reason, - _acc) -> - [encode_sasl_failure_credentials_expired(Reason, []) - | _acc]; -'encode_sasl_failure_$reason'('encryption-required' = - Reason, - _acc) -> - [encode_sasl_failure_encryption_required(Reason, []) - | _acc]; -'encode_sasl_failure_$reason'('incorrect-encoding' = - Reason, - _acc) -> - [encode_sasl_failure_incorrect_encoding(Reason, []) - | _acc]; -'encode_sasl_failure_$reason'('invalid-authzid' = - Reason, - _acc) -> - [encode_sasl_failure_invalid_authzid(Reason, []) - | _acc]; -'encode_sasl_failure_$reason'('invalid-mechanism' = - Reason, - _acc) -> - [encode_sasl_failure_invalid_mechanism(Reason, []) - | _acc]; -'encode_sasl_failure_$reason'('malformed-request' = - Reason, - _acc) -> - [encode_sasl_failure_malformed_request(Reason, []) - | _acc]; -'encode_sasl_failure_$reason'('mechanism-too-weak' = - Reason, - _acc) -> - [encode_sasl_failure_mechanism_too_weak(Reason, []) - | _acc]; -'encode_sasl_failure_$reason'('not-authorized' = Reason, - _acc) -> - [encode_sasl_failure_not_authorized(Reason, []) | _acc]; -'encode_sasl_failure_$reason'('temporary-auth-failure' = - Reason, - _acc) -> - [encode_sasl_failure_temporary_auth_failure(Reason, []) - | _acc]. - -decode_sasl_failure_temporary_auth_failure(__TopXMLNS, - __IgnoreEls, - {xmlel, <<"temporary-auth-failure">>, - _attrs, _els}) -> - 'temporary-auth-failure'. - -encode_sasl_failure_temporary_auth_failure('temporary-auth-failure', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"temporary-auth-failure">>, _attrs, _els}. - -decode_sasl_failure_not_authorized(__TopXMLNS, - __IgnoreEls, - {xmlel, <<"not-authorized">>, _attrs, - _els}) -> - 'not-authorized'. - -encode_sasl_failure_not_authorized('not-authorized', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"not-authorized">>, _attrs, _els}. - -decode_sasl_failure_mechanism_too_weak(__TopXMLNS, - __IgnoreEls, - {xmlel, <<"mechanism-too-weak">>, _attrs, - _els}) -> - 'mechanism-too-weak'. - -encode_sasl_failure_mechanism_too_weak('mechanism-too-weak', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"mechanism-too-weak">>, _attrs, _els}. - -decode_sasl_failure_malformed_request(__TopXMLNS, - __IgnoreEls, - {xmlel, <<"malformed-request">>, _attrs, - _els}) -> - 'malformed-request'. - -encode_sasl_failure_malformed_request('malformed-request', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"malformed-request">>, _attrs, _els}. - -decode_sasl_failure_invalid_mechanism(__TopXMLNS, - __IgnoreEls, - {xmlel, <<"invalid-mechanism">>, _attrs, - _els}) -> - 'invalid-mechanism'. - -encode_sasl_failure_invalid_mechanism('invalid-mechanism', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"invalid-mechanism">>, _attrs, _els}. - -decode_sasl_failure_invalid_authzid(__TopXMLNS, - __IgnoreEls, - {xmlel, <<"invalid-authzid">>, _attrs, - _els}) -> - 'invalid-authzid'. - -encode_sasl_failure_invalid_authzid('invalid-authzid', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"invalid-authzid">>, _attrs, _els}. - -decode_sasl_failure_incorrect_encoding(__TopXMLNS, - __IgnoreEls, - {xmlel, <<"incorrect-encoding">>, _attrs, - _els}) -> - 'incorrect-encoding'. - -encode_sasl_failure_incorrect_encoding('incorrect-encoding', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"incorrect-encoding">>, _attrs, _els}. - -decode_sasl_failure_encryption_required(__TopXMLNS, - __IgnoreEls, - {xmlel, <<"encryption-required">>, - _attrs, _els}) -> - 'encryption-required'. - -encode_sasl_failure_encryption_required('encryption-required', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"encryption-required">>, _attrs, _els}. - -decode_sasl_failure_credentials_expired(__TopXMLNS, - __IgnoreEls, - {xmlel, <<"credentials-expired">>, - _attrs, _els}) -> - 'credentials-expired'. - -encode_sasl_failure_credentials_expired('credentials-expired', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"credentials-expired">>, _attrs, _els}. - -decode_sasl_failure_account_disabled(__TopXMLNS, - __IgnoreEls, - {xmlel, <<"account-disabled">>, _attrs, - _els}) -> - 'account-disabled'. - -encode_sasl_failure_account_disabled('account-disabled', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"account-disabled">>, _attrs, _els}. - -decode_sasl_failure_aborted(__TopXMLNS, __IgnoreEls, - {xmlel, <<"aborted">>, _attrs, _els}) -> - aborted. - -encode_sasl_failure_aborted(aborted, _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"aborted">>, _attrs, _els}. - -decode_sasl_failure_text(__TopXMLNS, __IgnoreEls, - {xmlel, <<"text">>, _attrs, _els}) -> - Data = decode_sasl_failure_text_els(__TopXMLNS, - __IgnoreEls, _els, <<>>), - Lang = decode_sasl_failure_text_attrs(__TopXMLNS, - _attrs, undefined), - {text, Lang, Data}. - -decode_sasl_failure_text_els(__TopXMLNS, __IgnoreEls, - [], Data) -> - decode_sasl_failure_text_cdata(__TopXMLNS, Data); -decode_sasl_failure_text_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Data) -> - decode_sasl_failure_text_els(__TopXMLNS, __IgnoreEls, - _els, <<Data/binary, _data/binary>>); -decode_sasl_failure_text_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Data) -> - decode_sasl_failure_text_els(__TopXMLNS, __IgnoreEls, - _els, Data). - -decode_sasl_failure_text_attrs(__TopXMLNS, - [{<<"xml:lang">>, _val} | _attrs], _Lang) -> - decode_sasl_failure_text_attrs(__TopXMLNS, _attrs, - _val); -decode_sasl_failure_text_attrs(__TopXMLNS, [_ | _attrs], - Lang) -> - decode_sasl_failure_text_attrs(__TopXMLNS, _attrs, - Lang); -decode_sasl_failure_text_attrs(__TopXMLNS, [], Lang) -> - 'decode_sasl_failure_text_attr_xml:lang'(__TopXMLNS, - Lang). - -encode_sasl_failure_text({text, Lang, Data}, - _xmlns_attrs) -> - _els = encode_sasl_failure_text_cdata(Data, []), - _attrs = 'encode_sasl_failure_text_attr_xml:lang'(Lang, - _xmlns_attrs), - {xmlel, <<"text">>, _attrs, _els}. - -'decode_sasl_failure_text_attr_xml:lang'(__TopXMLNS, - undefined) -> - undefined; -'decode_sasl_failure_text_attr_xml:lang'(__TopXMLNS, - _val) -> - _val. - -'encode_sasl_failure_text_attr_xml:lang'(undefined, - _acc) -> - _acc; -'encode_sasl_failure_text_attr_xml:lang'(_val, _acc) -> - [{<<"xml:lang">>, _val} | _acc]. - -decode_sasl_failure_text_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_sasl_failure_text_cdata(__TopXMLNS, _val) -> - _val. - -encode_sasl_failure_text_cdata(undefined, _acc) -> _acc; -encode_sasl_failure_text_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_sasl_success(__TopXMLNS, __IgnoreEls, - {xmlel, <<"success">>, _attrs, _els}) -> - Text = decode_sasl_success_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - {sasl_success, Text}. - -decode_sasl_success_els(__TopXMLNS, __IgnoreEls, [], - Text) -> - decode_sasl_success_cdata(__TopXMLNS, Text); -decode_sasl_success_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Text) -> - decode_sasl_success_els(__TopXMLNS, __IgnoreEls, _els, - <<Text/binary, _data/binary>>); -decode_sasl_success_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Text) -> - decode_sasl_success_els(__TopXMLNS, __IgnoreEls, _els, - Text). - -encode_sasl_success({sasl_success, Text}, - _xmlns_attrs) -> - _els = encode_sasl_success_cdata(Text, []), - _attrs = _xmlns_attrs, - {xmlel, <<"success">>, _attrs, _els}. - -decode_sasl_success_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_sasl_success_cdata(__TopXMLNS, _val) -> - case catch base64:decode(_val) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_cdata_value, <<>>, <<"success">>, __TopXMLNS}}); - _res -> _res - end. - -encode_sasl_success_cdata(undefined, _acc) -> _acc; -encode_sasl_success_cdata(_val, _acc) -> - [{xmlcdata, base64:encode(_val)} | _acc]. - -decode_sasl_response(__TopXMLNS, __IgnoreEls, - {xmlel, <<"response">>, _attrs, _els}) -> - Text = decode_sasl_response_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - {sasl_response, Text}. - -decode_sasl_response_els(__TopXMLNS, __IgnoreEls, [], - Text) -> - decode_sasl_response_cdata(__TopXMLNS, Text); -decode_sasl_response_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Text) -> - decode_sasl_response_els(__TopXMLNS, __IgnoreEls, _els, - <<Text/binary, _data/binary>>); -decode_sasl_response_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Text) -> - decode_sasl_response_els(__TopXMLNS, __IgnoreEls, _els, - Text). - -encode_sasl_response({sasl_response, Text}, - _xmlns_attrs) -> - _els = encode_sasl_response_cdata(Text, []), - _attrs = _xmlns_attrs, - {xmlel, <<"response">>, _attrs, _els}. - -decode_sasl_response_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_sasl_response_cdata(__TopXMLNS, _val) -> - case catch base64:decode(_val) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_cdata_value, <<>>, <<"response">>, __TopXMLNS}}); - _res -> _res - end. - -encode_sasl_response_cdata(undefined, _acc) -> _acc; -encode_sasl_response_cdata(_val, _acc) -> - [{xmlcdata, base64:encode(_val)} | _acc]. - -decode_sasl_challenge(__TopXMLNS, __IgnoreEls, - {xmlel, <<"challenge">>, _attrs, _els}) -> - Text = decode_sasl_challenge_els(__TopXMLNS, - __IgnoreEls, _els, <<>>), - {sasl_challenge, Text}. - -decode_sasl_challenge_els(__TopXMLNS, __IgnoreEls, [], - Text) -> - decode_sasl_challenge_cdata(__TopXMLNS, Text); -decode_sasl_challenge_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Text) -> - decode_sasl_challenge_els(__TopXMLNS, __IgnoreEls, _els, - <<Text/binary, _data/binary>>); -decode_sasl_challenge_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Text) -> - decode_sasl_challenge_els(__TopXMLNS, __IgnoreEls, _els, - Text). - -encode_sasl_challenge({sasl_challenge, Text}, - _xmlns_attrs) -> - _els = encode_sasl_challenge_cdata(Text, []), - _attrs = _xmlns_attrs, - {xmlel, <<"challenge">>, _attrs, _els}. - -decode_sasl_challenge_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_sasl_challenge_cdata(__TopXMLNS, _val) -> - case catch base64:decode(_val) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_cdata_value, <<>>, <<"challenge">>, __TopXMLNS}}); - _res -> _res - end. - -encode_sasl_challenge_cdata(undefined, _acc) -> _acc; -encode_sasl_challenge_cdata(_val, _acc) -> - [{xmlcdata, base64:encode(_val)} | _acc]. - -decode_sasl_abort(__TopXMLNS, __IgnoreEls, - {xmlel, <<"abort">>, _attrs, _els}) -> - {sasl_abort}. - -encode_sasl_abort({sasl_abort}, _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"abort">>, _attrs, _els}. - -decode_sasl_auth(__TopXMLNS, __IgnoreEls, - {xmlel, <<"auth">>, _attrs, _els}) -> - Text = decode_sasl_auth_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - Mechanism = decode_sasl_auth_attrs(__TopXMLNS, _attrs, - undefined), - {sasl_auth, Mechanism, Text}. - -decode_sasl_auth_els(__TopXMLNS, __IgnoreEls, [], - Text) -> - decode_sasl_auth_cdata(__TopXMLNS, Text); -decode_sasl_auth_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Text) -> - decode_sasl_auth_els(__TopXMLNS, __IgnoreEls, _els, - <<Text/binary, _data/binary>>); -decode_sasl_auth_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Text) -> - decode_sasl_auth_els(__TopXMLNS, __IgnoreEls, _els, - Text). - -decode_sasl_auth_attrs(__TopXMLNS, - [{<<"mechanism">>, _val} | _attrs], _Mechanism) -> - decode_sasl_auth_attrs(__TopXMLNS, _attrs, _val); -decode_sasl_auth_attrs(__TopXMLNS, [_ | _attrs], - Mechanism) -> - decode_sasl_auth_attrs(__TopXMLNS, _attrs, Mechanism); -decode_sasl_auth_attrs(__TopXMLNS, [], Mechanism) -> - decode_sasl_auth_attr_mechanism(__TopXMLNS, Mechanism). - -encode_sasl_auth({sasl_auth, Mechanism, Text}, - _xmlns_attrs) -> - _els = encode_sasl_auth_cdata(Text, []), - _attrs = encode_sasl_auth_attr_mechanism(Mechanism, - _xmlns_attrs), - {xmlel, <<"auth">>, _attrs, _els}. - -decode_sasl_auth_attr_mechanism(__TopXMLNS, - undefined) -> - erlang:error({xmpp_codec, - {missing_attr, <<"mechanism">>, <<"auth">>, - __TopXMLNS}}); -decode_sasl_auth_attr_mechanism(__TopXMLNS, _val) -> - _val. - -encode_sasl_auth_attr_mechanism(_val, _acc) -> - [{<<"mechanism">>, _val} | _acc]. - -decode_sasl_auth_cdata(__TopXMLNS, <<>>) -> undefined; -decode_sasl_auth_cdata(__TopXMLNS, _val) -> - case catch base64:decode(_val) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_cdata_value, <<>>, <<"auth">>, __TopXMLNS}}); - _res -> _res - end. - -encode_sasl_auth_cdata(undefined, _acc) -> _acc; -encode_sasl_auth_cdata(_val, _acc) -> - [{xmlcdata, base64:encode(_val)} | _acc]. - -decode_bind(__TopXMLNS, __IgnoreEls, - {xmlel, <<"bind">>, _attrs, _els}) -> - {Jid, Resource} = decode_bind_els(__TopXMLNS, - __IgnoreEls, _els, undefined, undefined), - {bind, Jid, Resource}. - -decode_bind_els(__TopXMLNS, __IgnoreEls, [], Jid, - Resource) -> - {Jid, Resource}; -decode_bind_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"jid">>, _attrs, _} = _el | _els], Jid, - Resource) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_bind_els(__TopXMLNS, __IgnoreEls, _els, - decode_bind_jid(__TopXMLNS, __IgnoreEls, _el), - Resource); - true -> - decode_bind_els(__TopXMLNS, __IgnoreEls, _els, Jid, - Resource) - end; -decode_bind_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"resource">>, _attrs, _} = _el | _els], Jid, - Resource) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_bind_els(__TopXMLNS, __IgnoreEls, _els, Jid, - decode_bind_resource(__TopXMLNS, __IgnoreEls, _el)); - true -> - decode_bind_els(__TopXMLNS, __IgnoreEls, _els, Jid, - Resource) - end; -decode_bind_els(__TopXMLNS, __IgnoreEls, [_ | _els], - Jid, Resource) -> - decode_bind_els(__TopXMLNS, __IgnoreEls, _els, Jid, - Resource). - -encode_bind({bind, Jid, Resource}, _xmlns_attrs) -> - _els = lists:reverse('encode_bind_$jid'(Jid, - 'encode_bind_$resource'(Resource, - []))), - _attrs = _xmlns_attrs, - {xmlel, <<"bind">>, _attrs, _els}. - -'encode_bind_$jid'(undefined, _acc) -> _acc; -'encode_bind_$jid'(Jid, _acc) -> - [encode_bind_jid(Jid, []) | _acc]. - -'encode_bind_$resource'(undefined, _acc) -> _acc; -'encode_bind_$resource'(Resource, _acc) -> - [encode_bind_resource(Resource, []) | _acc]. - -decode_bind_resource(__TopXMLNS, __IgnoreEls, - {xmlel, <<"resource">>, _attrs, _els}) -> - Cdata = decode_bind_resource_els(__TopXMLNS, - __IgnoreEls, _els, <<>>), - Cdata. - -decode_bind_resource_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_bind_resource_cdata(__TopXMLNS, Cdata); -decode_bind_resource_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_bind_resource_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_bind_resource_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_bind_resource_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_bind_resource(Cdata, _xmlns_attrs) -> - _els = encode_bind_resource_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"resource">>, _attrs, _els}. - -decode_bind_resource_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_bind_resource_cdata(__TopXMLNS, _val) -> - case catch resourceprep(_val) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_cdata_value, <<>>, <<"resource">>, __TopXMLNS}}); - _res -> _res - end. - -encode_bind_resource_cdata(undefined, _acc) -> _acc; -encode_bind_resource_cdata(_val, _acc) -> - [{xmlcdata, resourceprep(_val)} | _acc]. - -decode_bind_jid(__TopXMLNS, __IgnoreEls, - {xmlel, <<"jid">>, _attrs, _els}) -> - Cdata = decode_bind_jid_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - Cdata. - -decode_bind_jid_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_bind_jid_cdata(__TopXMLNS, Cdata); -decode_bind_jid_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_bind_jid_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_bind_jid_els(__TopXMLNS, __IgnoreEls, [_ | _els], - Cdata) -> - decode_bind_jid_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_bind_jid(Cdata, _xmlns_attrs) -> - _els = encode_bind_jid_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"jid">>, _attrs, _els}. - -decode_bind_jid_cdata(__TopXMLNS, <<>>) -> undefined; -decode_bind_jid_cdata(__TopXMLNS, _val) -> - case catch dec_jid(_val) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_cdata_value, <<>>, <<"jid">>, __TopXMLNS}}); - _res -> _res - end. - -encode_bind_jid_cdata(undefined, _acc) -> _acc; -encode_bind_jid_cdata(_val, _acc) -> - [{xmlcdata, enc_jid(_val)} | _acc]. - -decode_error(__TopXMLNS, __IgnoreEls, - {xmlel, <<"error">>, _attrs, _els}) -> - {Text, Reason} = decode_error_els(__TopXMLNS, - __IgnoreEls, _els, undefined, undefined), - {Type, By} = decode_error_attrs(__TopXMLNS, _attrs, - undefined, undefined), - {error, Type, By, Reason, Text}. - -decode_error_els(__TopXMLNS, __IgnoreEls, [], Text, - Reason) -> - {Text, Reason}; -decode_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"text">>, _attrs, _} = _el | _els], Text, - Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-stanzas">> -> - decode_error_els(__TopXMLNS, __IgnoreEls, _els, - decode_error_text(_xmlns, __IgnoreEls, _el), - Reason); - true -> - decode_error_els(__TopXMLNS, __IgnoreEls, _els, Text, - Reason) - end; -decode_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"bad-request">>, _attrs, _} = _el | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-stanzas">> -> - decode_error_els(__TopXMLNS, __IgnoreEls, _els, Text, - decode_error_bad_request(_xmlns, __IgnoreEls, _el)); - true -> - decode_error_els(__TopXMLNS, __IgnoreEls, _els, Text, - Reason) - end; -decode_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"conflict">>, _attrs, _} = _el | _els], Text, - Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-stanzas">> -> - decode_error_els(__TopXMLNS, __IgnoreEls, _els, Text, - decode_error_conflict(_xmlns, __IgnoreEls, _el)); - true -> - decode_error_els(__TopXMLNS, __IgnoreEls, _els, Text, - Reason) - end; -decode_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"feature-not-implemented">>, _attrs, _} = _el - | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-stanzas">> -> - decode_error_els(__TopXMLNS, __IgnoreEls, _els, Text, - decode_error_feature_not_implemented(_xmlns, - __IgnoreEls, - _el)); - true -> - decode_error_els(__TopXMLNS, __IgnoreEls, _els, Text, - Reason) - end; -decode_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"forbidden">>, _attrs, _} = _el | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-stanzas">> -> - decode_error_els(__TopXMLNS, __IgnoreEls, _els, Text, - decode_error_forbidden(_xmlns, __IgnoreEls, _el)); - true -> - decode_error_els(__TopXMLNS, __IgnoreEls, _els, Text, - Reason) - end; -decode_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"gone">>, _attrs, _} = _el | _els], Text, - Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-stanzas">> -> - decode_error_els(__TopXMLNS, __IgnoreEls, _els, Text, - decode_error_gone(_xmlns, __IgnoreEls, _el)); - true -> - decode_error_els(__TopXMLNS, __IgnoreEls, _els, Text, - Reason) - end; -decode_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"internal-server-error">>, _attrs, _} = _el - | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-stanzas">> -> - decode_error_els(__TopXMLNS, __IgnoreEls, _els, Text, - decode_error_internal_server_error(_xmlns, - __IgnoreEls, - _el)); - true -> - decode_error_els(__TopXMLNS, __IgnoreEls, _els, Text, - Reason) - end; -decode_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"item-not-found">>, _attrs, _} = _el | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-stanzas">> -> - decode_error_els(__TopXMLNS, __IgnoreEls, _els, Text, - decode_error_item_not_found(_xmlns, __IgnoreEls, - _el)); - true -> - decode_error_els(__TopXMLNS, __IgnoreEls, _els, Text, - Reason) - end; -decode_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"jid-malformed">>, _attrs, _} = _el | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-stanzas">> -> - decode_error_els(__TopXMLNS, __IgnoreEls, _els, Text, - decode_error_jid_malformed(_xmlns, __IgnoreEls, - _el)); - true -> - decode_error_els(__TopXMLNS, __IgnoreEls, _els, Text, - Reason) - end; -decode_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"not-acceptable">>, _attrs, _} = _el | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-stanzas">> -> - decode_error_els(__TopXMLNS, __IgnoreEls, _els, Text, - decode_error_not_acceptable(_xmlns, __IgnoreEls, - _el)); - true -> - decode_error_els(__TopXMLNS, __IgnoreEls, _els, Text, - Reason) - end; -decode_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"not-allowed">>, _attrs, _} = _el | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-stanzas">> -> - decode_error_els(__TopXMLNS, __IgnoreEls, _els, Text, - decode_error_not_allowed(_xmlns, __IgnoreEls, _el)); - true -> - decode_error_els(__TopXMLNS, __IgnoreEls, _els, Text, - Reason) - end; -decode_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"not-authorized">>, _attrs, _} = _el | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-stanzas">> -> - decode_error_els(__TopXMLNS, __IgnoreEls, _els, Text, - decode_error_not_authorized(_xmlns, __IgnoreEls, - _el)); - true -> - decode_error_els(__TopXMLNS, __IgnoreEls, _els, Text, - Reason) - end; -decode_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"policy-violation">>, _attrs, _} = _el - | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-stanzas">> -> - decode_error_els(__TopXMLNS, __IgnoreEls, _els, Text, - decode_error_policy_violation(_xmlns, __IgnoreEls, - _el)); - true -> - decode_error_els(__TopXMLNS, __IgnoreEls, _els, Text, - Reason) - end; -decode_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"recipient-unavailable">>, _attrs, _} = _el - | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-stanzas">> -> - decode_error_els(__TopXMLNS, __IgnoreEls, _els, Text, - decode_error_recipient_unavailable(_xmlns, - __IgnoreEls, - _el)); - true -> - decode_error_els(__TopXMLNS, __IgnoreEls, _els, Text, - Reason) - end; -decode_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"redirect">>, _attrs, _} = _el | _els], Text, - Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-stanzas">> -> - decode_error_els(__TopXMLNS, __IgnoreEls, _els, Text, - decode_error_redirect(_xmlns, __IgnoreEls, _el)); - true -> - decode_error_els(__TopXMLNS, __IgnoreEls, _els, Text, - Reason) - end; -decode_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"registration-required">>, _attrs, _} = _el - | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-stanzas">> -> - decode_error_els(__TopXMLNS, __IgnoreEls, _els, Text, - decode_error_registration_required(_xmlns, - __IgnoreEls, - _el)); - true -> - decode_error_els(__TopXMLNS, __IgnoreEls, _els, Text, - Reason) - end; -decode_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"remote-server-not-found">>, _attrs, _} = _el - | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-stanzas">> -> - decode_error_els(__TopXMLNS, __IgnoreEls, _els, Text, - decode_error_remote_server_not_found(_xmlns, - __IgnoreEls, - _el)); - true -> - decode_error_els(__TopXMLNS, __IgnoreEls, _els, Text, - Reason) - end; -decode_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"remote-server-timeout">>, _attrs, _} = _el - | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-stanzas">> -> - decode_error_els(__TopXMLNS, __IgnoreEls, _els, Text, - decode_error_remote_server_timeout(_xmlns, - __IgnoreEls, - _el)); - true -> - decode_error_els(__TopXMLNS, __IgnoreEls, _els, Text, - Reason) - end; -decode_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"resource-constraint">>, _attrs, _} = _el - | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-stanzas">> -> - decode_error_els(__TopXMLNS, __IgnoreEls, _els, Text, - decode_error_resource_constraint(_xmlns, - __IgnoreEls, _el)); - true -> - decode_error_els(__TopXMLNS, __IgnoreEls, _els, Text, - Reason) - end; -decode_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"service-unavailable">>, _attrs, _} = _el - | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-stanzas">> -> - decode_error_els(__TopXMLNS, __IgnoreEls, _els, Text, - decode_error_service_unavailable(_xmlns, - __IgnoreEls, _el)); - true -> - decode_error_els(__TopXMLNS, __IgnoreEls, _els, Text, - Reason) - end; -decode_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"subscription-required">>, _attrs, _} = _el - | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-stanzas">> -> - decode_error_els(__TopXMLNS, __IgnoreEls, _els, Text, - decode_error_subscription_required(_xmlns, - __IgnoreEls, - _el)); - true -> - decode_error_els(__TopXMLNS, __IgnoreEls, _els, Text, - Reason) - end; -decode_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"undefined-condition">>, _attrs, _} = _el - | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-stanzas">> -> - decode_error_els(__TopXMLNS, __IgnoreEls, _els, Text, - decode_error_undefined_condition(_xmlns, - __IgnoreEls, _el)); - true -> - decode_error_els(__TopXMLNS, __IgnoreEls, _els, Text, - Reason) - end; -decode_error_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"unexpected-request">>, _attrs, _} = _el - | _els], - Text, Reason) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == - <<"urn:ietf:params:xml:ns:xmpp-stanzas">> -> - decode_error_els(__TopXMLNS, __IgnoreEls, _els, Text, - decode_error_unexpected_request(_xmlns, __IgnoreEls, - _el)); - true -> - decode_error_els(__TopXMLNS, __IgnoreEls, _els, Text, - Reason) - end; -decode_error_els(__TopXMLNS, __IgnoreEls, [_ | _els], - Text, Reason) -> - decode_error_els(__TopXMLNS, __IgnoreEls, _els, Text, - Reason). - -decode_error_attrs(__TopXMLNS, - [{<<"type">>, _val} | _attrs], _Type, By) -> - decode_error_attrs(__TopXMLNS, _attrs, _val, By); -decode_error_attrs(__TopXMLNS, - [{<<"by">>, _val} | _attrs], Type, _By) -> - decode_error_attrs(__TopXMLNS, _attrs, Type, _val); -decode_error_attrs(__TopXMLNS, [_ | _attrs], Type, - By) -> - decode_error_attrs(__TopXMLNS, _attrs, Type, By); -decode_error_attrs(__TopXMLNS, [], Type, By) -> - {decode_error_attr_type(__TopXMLNS, Type), - decode_error_attr_by(__TopXMLNS, By)}. - -encode_error({error, Type, By, Reason, Text}, - _xmlns_attrs) -> - _els = lists:reverse('encode_error_$text'(Text, - 'encode_error_$reason'(Reason, - []))), - _attrs = encode_error_attr_by(By, - encode_error_attr_type(Type, _xmlns_attrs)), - {xmlel, <<"error">>, _attrs, _els}. - -'encode_error_$text'(undefined, _acc) -> _acc; -'encode_error_$text'(Text, _acc) -> - [encode_error_text(Text, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>}]) - | _acc]. - -'encode_error_$reason'(undefined, _acc) -> _acc; -'encode_error_$reason'('bad-request' = Reason, _acc) -> - [encode_error_bad_request(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>}]) - | _acc]; -'encode_error_$reason'(conflict = Reason, _acc) -> - [encode_error_conflict(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>}]) - | _acc]; -'encode_error_$reason'('feature-not-implemented' = - Reason, - _acc) -> - [encode_error_feature_not_implemented(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>}]) - | _acc]; -'encode_error_$reason'(forbidden = Reason, _acc) -> - [encode_error_forbidden(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>}]) - | _acc]; -'encode_error_$reason'({gone, _} = Reason, _acc) -> - [encode_error_gone(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>}]) - | _acc]; -'encode_error_$reason'('internal-server-error' = Reason, - _acc) -> - [encode_error_internal_server_error(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>}]) - | _acc]; -'encode_error_$reason'('item-not-found' = Reason, - _acc) -> - [encode_error_item_not_found(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>}]) - | _acc]; -'encode_error_$reason'('jid-malformed' = Reason, - _acc) -> - [encode_error_jid_malformed(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>}]) - | _acc]; -'encode_error_$reason'('not-acceptable' = Reason, - _acc) -> - [encode_error_not_acceptable(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>}]) - | _acc]; -'encode_error_$reason'('not-allowed' = Reason, _acc) -> - [encode_error_not_allowed(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>}]) - | _acc]; -'encode_error_$reason'('not-authorized' = Reason, - _acc) -> - [encode_error_not_authorized(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>}]) - | _acc]; -'encode_error_$reason'('policy-violation' = Reason, - _acc) -> - [encode_error_policy_violation(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>}]) - | _acc]; -'encode_error_$reason'('recipient-unavailable' = Reason, - _acc) -> - [encode_error_recipient_unavailable(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>}]) - | _acc]; -'encode_error_$reason'({redirect, _} = Reason, _acc) -> - [encode_error_redirect(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>}]) - | _acc]; -'encode_error_$reason'('registration-required' = Reason, - _acc) -> - [encode_error_registration_required(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>}]) - | _acc]; -'encode_error_$reason'('remote-server-not-found' = - Reason, - _acc) -> - [encode_error_remote_server_not_found(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>}]) - | _acc]; -'encode_error_$reason'('remote-server-timeout' = Reason, - _acc) -> - [encode_error_remote_server_timeout(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>}]) - | _acc]; -'encode_error_$reason'('resource-constraint' = Reason, - _acc) -> - [encode_error_resource_constraint(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>}]) - | _acc]; -'encode_error_$reason'('service-unavailable' = Reason, - _acc) -> - [encode_error_service_unavailable(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>}]) - | _acc]; -'encode_error_$reason'('subscription-required' = Reason, - _acc) -> - [encode_error_subscription_required(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>}]) - | _acc]; -'encode_error_$reason'('undefined-condition' = Reason, - _acc) -> - [encode_error_undefined_condition(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>}]) - | _acc]; -'encode_error_$reason'('unexpected-request' = Reason, - _acc) -> - [encode_error_unexpected_request(Reason, - [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-stanzas">>}]) - | _acc]. - -decode_error_attr_type(__TopXMLNS, undefined) -> - erlang:error({xmpp_codec, - {missing_attr, <<"type">>, <<"error">>, __TopXMLNS}}); -decode_error_attr_type(__TopXMLNS, _val) -> - case catch dec_enum(_val, - [auth, cancel, continue, modify, wait]) - of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"type">>, <<"error">>, __TopXMLNS}}); - _res -> _res - end. - -encode_error_attr_type(_val, _acc) -> - [{<<"type">>, enc_enum(_val)} | _acc]. - -decode_error_attr_by(__TopXMLNS, undefined) -> - undefined; -decode_error_attr_by(__TopXMLNS, _val) -> _val. - -encode_error_attr_by(undefined, _acc) -> _acc; -encode_error_attr_by(_val, _acc) -> - [{<<"by">>, _val} | _acc]. - -decode_error_text(__TopXMLNS, __IgnoreEls, - {xmlel, <<"text">>, _attrs, _els}) -> - Data = decode_error_text_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - Lang = decode_error_text_attrs(__TopXMLNS, _attrs, - undefined), - {text, Lang, Data}. - -decode_error_text_els(__TopXMLNS, __IgnoreEls, [], - Data) -> - decode_error_text_cdata(__TopXMLNS, Data); -decode_error_text_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Data) -> - decode_error_text_els(__TopXMLNS, __IgnoreEls, _els, - <<Data/binary, _data/binary>>); -decode_error_text_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Data) -> - decode_error_text_els(__TopXMLNS, __IgnoreEls, _els, - Data). - -decode_error_text_attrs(__TopXMLNS, - [{<<"xml:lang">>, _val} | _attrs], _Lang) -> - decode_error_text_attrs(__TopXMLNS, _attrs, _val); -decode_error_text_attrs(__TopXMLNS, [_ | _attrs], - Lang) -> - decode_error_text_attrs(__TopXMLNS, _attrs, Lang); -decode_error_text_attrs(__TopXMLNS, [], Lang) -> - 'decode_error_text_attr_xml:lang'(__TopXMLNS, Lang). - -encode_error_text({text, Lang, Data}, _xmlns_attrs) -> - _els = encode_error_text_cdata(Data, []), - _attrs = 'encode_error_text_attr_xml:lang'(Lang, - _xmlns_attrs), - {xmlel, <<"text">>, _attrs, _els}. - -'decode_error_text_attr_xml:lang'(__TopXMLNS, - undefined) -> - undefined; -'decode_error_text_attr_xml:lang'(__TopXMLNS, _val) -> - _val. - -'encode_error_text_attr_xml:lang'(undefined, _acc) -> - _acc; -'encode_error_text_attr_xml:lang'(_val, _acc) -> - [{<<"xml:lang">>, _val} | _acc]. - -decode_error_text_cdata(__TopXMLNS, <<>>) -> undefined; -decode_error_text_cdata(__TopXMLNS, _val) -> _val. - -encode_error_text_cdata(undefined, _acc) -> _acc; -encode_error_text_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_error_unexpected_request(__TopXMLNS, __IgnoreEls, - {xmlel, <<"unexpected-request">>, _attrs, - _els}) -> - 'unexpected-request'. - -encode_error_unexpected_request('unexpected-request', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"unexpected-request">>, _attrs, _els}. - -decode_error_undefined_condition(__TopXMLNS, - __IgnoreEls, - {xmlel, <<"undefined-condition">>, _attrs, - _els}) -> - 'undefined-condition'. - -encode_error_undefined_condition('undefined-condition', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"undefined-condition">>, _attrs, _els}. - -decode_error_subscription_required(__TopXMLNS, - __IgnoreEls, - {xmlel, <<"subscription-required">>, _attrs, - _els}) -> - 'subscription-required'. - -encode_error_subscription_required('subscription-required', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"subscription-required">>, _attrs, _els}. - -decode_error_service_unavailable(__TopXMLNS, - __IgnoreEls, - {xmlel, <<"service-unavailable">>, _attrs, - _els}) -> - 'service-unavailable'. - -encode_error_service_unavailable('service-unavailable', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"service-unavailable">>, _attrs, _els}. - -decode_error_resource_constraint(__TopXMLNS, - __IgnoreEls, - {xmlel, <<"resource-constraint">>, _attrs, - _els}) -> - 'resource-constraint'. - -encode_error_resource_constraint('resource-constraint', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"resource-constraint">>, _attrs, _els}. - -decode_error_remote_server_timeout(__TopXMLNS, - __IgnoreEls, - {xmlel, <<"remote-server-timeout">>, _attrs, - _els}) -> - 'remote-server-timeout'. - -encode_error_remote_server_timeout('remote-server-timeout', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"remote-server-timeout">>, _attrs, _els}. - -decode_error_remote_server_not_found(__TopXMLNS, - __IgnoreEls, - {xmlel, <<"remote-server-not-found">>, - _attrs, _els}) -> - 'remote-server-not-found'. - -encode_error_remote_server_not_found('remote-server-not-found', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"remote-server-not-found">>, _attrs, _els}. - -decode_error_registration_required(__TopXMLNS, - __IgnoreEls, - {xmlel, <<"registration-required">>, _attrs, - _els}) -> - 'registration-required'. - -encode_error_registration_required('registration-required', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"registration-required">>, _attrs, _els}. - -decode_error_redirect(__TopXMLNS, __IgnoreEls, - {xmlel, <<"redirect">>, _attrs, _els}) -> - Uri = decode_error_redirect_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - {redirect, Uri}. - -decode_error_redirect_els(__TopXMLNS, __IgnoreEls, [], - Uri) -> - decode_error_redirect_cdata(__TopXMLNS, Uri); -decode_error_redirect_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Uri) -> - decode_error_redirect_els(__TopXMLNS, __IgnoreEls, _els, - <<Uri/binary, _data/binary>>); -decode_error_redirect_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Uri) -> - decode_error_redirect_els(__TopXMLNS, __IgnoreEls, _els, - Uri). - -encode_error_redirect({redirect, Uri}, _xmlns_attrs) -> - _els = encode_error_redirect_cdata(Uri, []), - _attrs = _xmlns_attrs, - {xmlel, <<"redirect">>, _attrs, _els}. - -decode_error_redirect_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_error_redirect_cdata(__TopXMLNS, _val) -> _val. - -encode_error_redirect_cdata(undefined, _acc) -> _acc; -encode_error_redirect_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_error_recipient_unavailable(__TopXMLNS, - __IgnoreEls, - {xmlel, <<"recipient-unavailable">>, _attrs, - _els}) -> - 'recipient-unavailable'. - -encode_error_recipient_unavailable('recipient-unavailable', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"recipient-unavailable">>, _attrs, _els}. - -decode_error_policy_violation(__TopXMLNS, __IgnoreEls, - {xmlel, <<"policy-violation">>, _attrs, _els}) -> - 'policy-violation'. - -encode_error_policy_violation('policy-violation', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"policy-violation">>, _attrs, _els}. - -decode_error_not_authorized(__TopXMLNS, __IgnoreEls, - {xmlel, <<"not-authorized">>, _attrs, _els}) -> - 'not-authorized'. - -encode_error_not_authorized('not-authorized', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"not-authorized">>, _attrs, _els}. - -decode_error_not_allowed(__TopXMLNS, __IgnoreEls, - {xmlel, <<"not-allowed">>, _attrs, _els}) -> - 'not-allowed'. - -encode_error_not_allowed('not-allowed', _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"not-allowed">>, _attrs, _els}. - -decode_error_not_acceptable(__TopXMLNS, __IgnoreEls, - {xmlel, <<"not-acceptable">>, _attrs, _els}) -> - 'not-acceptable'. - -encode_error_not_acceptable('not-acceptable', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"not-acceptable">>, _attrs, _els}. - -decode_error_jid_malformed(__TopXMLNS, __IgnoreEls, - {xmlel, <<"jid-malformed">>, _attrs, _els}) -> - 'jid-malformed'. - -encode_error_jid_malformed('jid-malformed', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"jid-malformed">>, _attrs, _els}. - -decode_error_item_not_found(__TopXMLNS, __IgnoreEls, - {xmlel, <<"item-not-found">>, _attrs, _els}) -> - 'item-not-found'. - -encode_error_item_not_found('item-not-found', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"item-not-found">>, _attrs, _els}. - -decode_error_internal_server_error(__TopXMLNS, - __IgnoreEls, - {xmlel, <<"internal-server-error">>, _attrs, - _els}) -> - 'internal-server-error'. - -encode_error_internal_server_error('internal-server-error', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"internal-server-error">>, _attrs, _els}. - -decode_error_gone(__TopXMLNS, __IgnoreEls, - {xmlel, <<"gone">>, _attrs, _els}) -> - Uri = decode_error_gone_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - {gone, Uri}. - -decode_error_gone_els(__TopXMLNS, __IgnoreEls, [], - Uri) -> - decode_error_gone_cdata(__TopXMLNS, Uri); -decode_error_gone_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Uri) -> - decode_error_gone_els(__TopXMLNS, __IgnoreEls, _els, - <<Uri/binary, _data/binary>>); -decode_error_gone_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Uri) -> - decode_error_gone_els(__TopXMLNS, __IgnoreEls, _els, - Uri). - -encode_error_gone({gone, Uri}, _xmlns_attrs) -> - _els = encode_error_gone_cdata(Uri, []), - _attrs = _xmlns_attrs, - {xmlel, <<"gone">>, _attrs, _els}. - -decode_error_gone_cdata(__TopXMLNS, <<>>) -> undefined; -decode_error_gone_cdata(__TopXMLNS, _val) -> _val. - -encode_error_gone_cdata(undefined, _acc) -> _acc; -encode_error_gone_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_error_forbidden(__TopXMLNS, __IgnoreEls, - {xmlel, <<"forbidden">>, _attrs, _els}) -> - forbidden. - -encode_error_forbidden(forbidden, _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"forbidden">>, _attrs, _els}. - -decode_error_feature_not_implemented(__TopXMLNS, - __IgnoreEls, - {xmlel, <<"feature-not-implemented">>, - _attrs, _els}) -> - 'feature-not-implemented'. - -encode_error_feature_not_implemented('feature-not-implemented', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"feature-not-implemented">>, _attrs, _els}. - -decode_error_conflict(__TopXMLNS, __IgnoreEls, - {xmlel, <<"conflict">>, _attrs, _els}) -> - conflict. - -encode_error_conflict(conflict, _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"conflict">>, _attrs, _els}. - -decode_error_bad_request(__TopXMLNS, __IgnoreEls, - {xmlel, <<"bad-request">>, _attrs, _els}) -> - 'bad-request'. - -encode_error_bad_request('bad-request', _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"bad-request">>, _attrs, _els}. - -decode_presence(__TopXMLNS, __IgnoreEls, - {xmlel, <<"presence">>, _attrs, _els}) -> - {Error, Status, Show, Priority, __Els} = - decode_presence_els(__TopXMLNS, __IgnoreEls, _els, - undefined, [], undefined, undefined, []), - {Id, Type, From, To, Lang} = - decode_presence_attrs(__TopXMLNS, _attrs, undefined, - undefined, undefined, undefined, undefined), - {presence, Id, Type, Lang, From, To, Show, Status, - Priority, Error, __Els}. - -decode_presence_els(__TopXMLNS, __IgnoreEls, [], Error, - Status, Show, Priority, __Els) -> - {Error, lists:reverse(Status), Show, Priority, - lists:reverse(__Els)}; -decode_presence_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"error">>, _attrs, _} = _el | _els], Error, - Status, Show, Priority, __Els) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_presence_els(__TopXMLNS, __IgnoreEls, _els, - decode_error(__TopXMLNS, __IgnoreEls, _el), - Status, Show, Priority, __Els); - true -> - decode_presence_els(__TopXMLNS, __IgnoreEls, _els, - Error, Status, Show, Priority, __Els) - end; -decode_presence_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"show">>, _attrs, _} = _el | _els], Error, - Status, Show, Priority, __Els) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_presence_els(__TopXMLNS, __IgnoreEls, _els, - Error, Status, - decode_presence_show(__TopXMLNS, __IgnoreEls, - _el), - Priority, __Els); - true -> - decode_presence_els(__TopXMLNS, __IgnoreEls, _els, - Error, Status, Show, Priority, __Els) - end; -decode_presence_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"status">>, _attrs, _} = _el | _els], Error, - Status, Show, Priority, __Els) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_presence_els(__TopXMLNS, __IgnoreEls, _els, - Error, - [decode_presence_status(__TopXMLNS, __IgnoreEls, - _el) - | Status], - Show, Priority, __Els); - true -> - decode_presence_els(__TopXMLNS, __IgnoreEls, _els, - Error, Status, Show, Priority, __Els) - end; -decode_presence_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"priority">>, _attrs, _} = _el | _els], - Error, Status, Show, Priority, __Els) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_presence_els(__TopXMLNS, __IgnoreEls, _els, - Error, Status, Show, - decode_presence_priority(__TopXMLNS, __IgnoreEls, - _el), - __Els); - true -> - decode_presence_els(__TopXMLNS, __IgnoreEls, _els, - Error, Status, Show, Priority, __Els) - end; -decode_presence_els(__TopXMLNS, __IgnoreEls, - [{xmlel, _, _, _} = _el | _els], Error, Status, Show, - Priority, __Els) -> - if __IgnoreEls -> - decode_presence_els(__TopXMLNS, __IgnoreEls, _els, - Error, Status, Show, Priority, [_el | __Els]); - true -> - case is_known_tag(_el) of - true -> - decode_presence_els(__TopXMLNS, __IgnoreEls, _els, - Error, Status, Show, Priority, - [decode(_el) | __Els]); - false -> - decode_presence_els(__TopXMLNS, __IgnoreEls, _els, - Error, Status, Show, Priority, __Els) - end - end; -decode_presence_els(__TopXMLNS, __IgnoreEls, [_ | _els], - Error, Status, Show, Priority, __Els) -> - decode_presence_els(__TopXMLNS, __IgnoreEls, _els, - Error, Status, Show, Priority, __Els). - -decode_presence_attrs(__TopXMLNS, - [{<<"id">>, _val} | _attrs], _Id, Type, From, To, - Lang) -> - decode_presence_attrs(__TopXMLNS, _attrs, _val, Type, - From, To, Lang); -decode_presence_attrs(__TopXMLNS, - [{<<"type">>, _val} | _attrs], Id, _Type, From, To, - Lang) -> - decode_presence_attrs(__TopXMLNS, _attrs, Id, _val, - From, To, Lang); -decode_presence_attrs(__TopXMLNS, - [{<<"from">>, _val} | _attrs], Id, Type, _From, To, - Lang) -> - decode_presence_attrs(__TopXMLNS, _attrs, Id, Type, - _val, To, Lang); -decode_presence_attrs(__TopXMLNS, - [{<<"to">>, _val} | _attrs], Id, Type, From, _To, - Lang) -> - decode_presence_attrs(__TopXMLNS, _attrs, Id, Type, - From, _val, Lang); -decode_presence_attrs(__TopXMLNS, - [{<<"xml:lang">>, _val} | _attrs], Id, Type, From, To, - _Lang) -> - decode_presence_attrs(__TopXMLNS, _attrs, Id, Type, - From, To, _val); -decode_presence_attrs(__TopXMLNS, [_ | _attrs], Id, - Type, From, To, Lang) -> - decode_presence_attrs(__TopXMLNS, _attrs, Id, Type, - From, To, Lang); -decode_presence_attrs(__TopXMLNS, [], Id, Type, From, - To, Lang) -> - {decode_presence_attr_id(__TopXMLNS, Id), - decode_presence_attr_type(__TopXMLNS, Type), - decode_presence_attr_from(__TopXMLNS, From), - decode_presence_attr_to(__TopXMLNS, To), - 'decode_presence_attr_xml:lang'(__TopXMLNS, Lang)}. - -encode_presence({presence, Id, Type, Lang, From, To, - Show, Status, Priority, Error, __Els}, - _xmlns_attrs) -> - _els = [encode(_el) || _el <- __Els] ++ - lists:reverse('encode_presence_$error'(Error, - 'encode_presence_$status'(Status, - 'encode_presence_$show'(Show, - 'encode_presence_$priority'(Priority, - []))))), - _attrs = 'encode_presence_attr_xml:lang'(Lang, - encode_presence_attr_to(To, - encode_presence_attr_from(From, - encode_presence_attr_type(Type, - encode_presence_attr_id(Id, - _xmlns_attrs))))), - {xmlel, <<"presence">>, _attrs, _els}. - -'encode_presence_$error'(undefined, _acc) -> _acc; -'encode_presence_$error'(Error, _acc) -> - [encode_error(Error, []) | _acc]. - -'encode_presence_$status'([], _acc) -> _acc; -'encode_presence_$status'([Status | _els], _acc) -> - 'encode_presence_$status'(_els, - [encode_presence_status(Status, []) | _acc]). - -'encode_presence_$show'(undefined, _acc) -> _acc; -'encode_presence_$show'(Show, _acc) -> - [encode_presence_show(Show, []) | _acc]. - -'encode_presence_$priority'(undefined, _acc) -> _acc; -'encode_presence_$priority'(Priority, _acc) -> - [encode_presence_priority(Priority, []) | _acc]. - -decode_presence_attr_id(__TopXMLNS, undefined) -> - undefined; -decode_presence_attr_id(__TopXMLNS, _val) -> _val. - -encode_presence_attr_id(undefined, _acc) -> _acc; -encode_presence_attr_id(_val, _acc) -> - [{<<"id">>, _val} | _acc]. - -decode_presence_attr_type(__TopXMLNS, undefined) -> - undefined; -decode_presence_attr_type(__TopXMLNS, _val) -> - case catch dec_enum(_val, - [unavailable, subscribe, subscribed, unsubscribe, - unsubscribed, probe, error]) - of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"type">>, <<"presence">>, - __TopXMLNS}}); - _res -> _res - end. - -encode_presence_attr_type(undefined, _acc) -> _acc; -encode_presence_attr_type(_val, _acc) -> - [{<<"type">>, enc_enum(_val)} | _acc]. - -decode_presence_attr_from(__TopXMLNS, undefined) -> - undefined; -decode_presence_attr_from(__TopXMLNS, _val) -> - case catch dec_jid(_val) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"from">>, <<"presence">>, - __TopXMLNS}}); - _res -> _res - end. - -encode_presence_attr_from(undefined, _acc) -> _acc; -encode_presence_attr_from(_val, _acc) -> - [{<<"from">>, enc_jid(_val)} | _acc]. - -decode_presence_attr_to(__TopXMLNS, undefined) -> - undefined; -decode_presence_attr_to(__TopXMLNS, _val) -> - case catch dec_jid(_val) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"to">>, <<"presence">>, - __TopXMLNS}}); - _res -> _res - end. - -encode_presence_attr_to(undefined, _acc) -> _acc; -encode_presence_attr_to(_val, _acc) -> - [{<<"to">>, enc_jid(_val)} | _acc]. - -'decode_presence_attr_xml:lang'(__TopXMLNS, - undefined) -> - undefined; -'decode_presence_attr_xml:lang'(__TopXMLNS, _val) -> - _val. - -'encode_presence_attr_xml:lang'(undefined, _acc) -> - _acc; -'encode_presence_attr_xml:lang'(_val, _acc) -> - [{<<"xml:lang">>, _val} | _acc]. - -decode_presence_priority(__TopXMLNS, __IgnoreEls, - {xmlel, <<"priority">>, _attrs, _els}) -> - Cdata = decode_presence_priority_els(__TopXMLNS, - __IgnoreEls, _els, <<>>), - Cdata. - -decode_presence_priority_els(__TopXMLNS, __IgnoreEls, - [], Cdata) -> - decode_presence_priority_cdata(__TopXMLNS, Cdata); -decode_presence_priority_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_presence_priority_els(__TopXMLNS, __IgnoreEls, - _els, <<Cdata/binary, _data/binary>>); -decode_presence_priority_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_presence_priority_els(__TopXMLNS, __IgnoreEls, - _els, Cdata). - -encode_presence_priority(Cdata, _xmlns_attrs) -> - _els = encode_presence_priority_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"priority">>, _attrs, _els}. - -decode_presence_priority_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_presence_priority_cdata(__TopXMLNS, _val) -> - case catch dec_int(_val) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_cdata_value, <<>>, <<"priority">>, __TopXMLNS}}); - _res -> _res - end. - -encode_presence_priority_cdata(undefined, _acc) -> _acc; -encode_presence_priority_cdata(_val, _acc) -> - [{xmlcdata, enc_int(_val)} | _acc]. - -decode_presence_status(__TopXMLNS, __IgnoreEls, - {xmlel, <<"status">>, _attrs, _els}) -> - Data = decode_presence_status_els(__TopXMLNS, - __IgnoreEls, _els, <<>>), - Lang = decode_presence_status_attrs(__TopXMLNS, _attrs, - undefined), - {text, Lang, Data}. - -decode_presence_status_els(__TopXMLNS, __IgnoreEls, [], - Data) -> - decode_presence_status_cdata(__TopXMLNS, Data); -decode_presence_status_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Data) -> - decode_presence_status_els(__TopXMLNS, __IgnoreEls, - _els, <<Data/binary, _data/binary>>); -decode_presence_status_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Data) -> - decode_presence_status_els(__TopXMLNS, __IgnoreEls, - _els, Data). - -decode_presence_status_attrs(__TopXMLNS, - [{<<"xml:lang">>, _val} | _attrs], _Lang) -> - decode_presence_status_attrs(__TopXMLNS, _attrs, _val); -decode_presence_status_attrs(__TopXMLNS, [_ | _attrs], - Lang) -> - decode_presence_status_attrs(__TopXMLNS, _attrs, Lang); -decode_presence_status_attrs(__TopXMLNS, [], Lang) -> - 'decode_presence_status_attr_xml:lang'(__TopXMLNS, - Lang). - -encode_presence_status({text, Lang, Data}, - _xmlns_attrs) -> - _els = encode_presence_status_cdata(Data, []), - _attrs = 'encode_presence_status_attr_xml:lang'(Lang, - _xmlns_attrs), - {xmlel, <<"status">>, _attrs, _els}. - -'decode_presence_status_attr_xml:lang'(__TopXMLNS, - undefined) -> - undefined; -'decode_presence_status_attr_xml:lang'(__TopXMLNS, - _val) -> - _val. - -'encode_presence_status_attr_xml:lang'(undefined, - _acc) -> - _acc; -'encode_presence_status_attr_xml:lang'(_val, _acc) -> - [{<<"xml:lang">>, _val} | _acc]. - -decode_presence_status_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_presence_status_cdata(__TopXMLNS, _val) -> _val. - -encode_presence_status_cdata(undefined, _acc) -> _acc; -encode_presence_status_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_presence_show(__TopXMLNS, __IgnoreEls, - {xmlel, <<"show">>, _attrs, _els}) -> - Cdata = decode_presence_show_els(__TopXMLNS, - __IgnoreEls, _els, <<>>), - Cdata. - -decode_presence_show_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_presence_show_cdata(__TopXMLNS, Cdata); -decode_presence_show_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_presence_show_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_presence_show_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_presence_show_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_presence_show(Cdata, _xmlns_attrs) -> - _els = encode_presence_show_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"show">>, _attrs, _els}. - -decode_presence_show_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_presence_show_cdata(__TopXMLNS, _val) -> - case catch dec_enum(_val, [away, chat, dnd, xa]) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_cdata_value, <<>>, <<"show">>, __TopXMLNS}}); - _res -> _res - end. - -encode_presence_show_cdata(undefined, _acc) -> _acc; -encode_presence_show_cdata(_val, _acc) -> - [{xmlcdata, enc_enum(_val)} | _acc]. - -decode_message(__TopXMLNS, __IgnoreEls, - {xmlel, <<"message">>, _attrs, _els}) -> - {Error, Thread, Subject, Body, __Els} = - decode_message_els(__TopXMLNS, __IgnoreEls, _els, - undefined, undefined, [], [], []), - {Id, Type, From, To, Lang} = - decode_message_attrs(__TopXMLNS, _attrs, undefined, - undefined, undefined, undefined, undefined), - {message, Id, Type, Lang, From, To, Subject, Body, - Thread, Error, __Els}. - -decode_message_els(__TopXMLNS, __IgnoreEls, [], Error, - Thread, Subject, Body, __Els) -> - {Error, Thread, lists:reverse(Subject), - lists:reverse(Body), lists:reverse(__Els)}; -decode_message_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"error">>, _attrs, _} = _el | _els], Error, - Thread, Subject, Body, __Els) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_message_els(__TopXMLNS, __IgnoreEls, _els, - decode_error(__TopXMLNS, __IgnoreEls, _el), - Thread, Subject, Body, __Els); - true -> - decode_message_els(__TopXMLNS, __IgnoreEls, _els, Error, - Thread, Subject, Body, __Els) - end; -decode_message_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"subject">>, _attrs, _} = _el | _els], Error, - Thread, Subject, Body, __Els) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_message_els(__TopXMLNS, __IgnoreEls, _els, Error, - Thread, - [decode_message_subject(__TopXMLNS, __IgnoreEls, - _el) - | Subject], - Body, __Els); - true -> - decode_message_els(__TopXMLNS, __IgnoreEls, _els, Error, - Thread, Subject, Body, __Els) - end; -decode_message_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"thread">>, _attrs, _} = _el | _els], Error, - Thread, Subject, Body, __Els) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_message_els(__TopXMLNS, __IgnoreEls, _els, Error, - decode_message_thread(__TopXMLNS, __IgnoreEls, - _el), - Subject, Body, __Els); - true -> - decode_message_els(__TopXMLNS, __IgnoreEls, _els, Error, - Thread, Subject, Body, __Els) - end; -decode_message_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"body">>, _attrs, _} = _el | _els], Error, - Thread, Subject, Body, __Els) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_message_els(__TopXMLNS, __IgnoreEls, _els, Error, - Thread, Subject, - [decode_message_body(__TopXMLNS, __IgnoreEls, _el) - | Body], - __Els); - true -> - decode_message_els(__TopXMLNS, __IgnoreEls, _els, Error, - Thread, Subject, Body, __Els) - end; -decode_message_els(__TopXMLNS, __IgnoreEls, - [{xmlel, _, _, _} = _el | _els], Error, Thread, Subject, - Body, __Els) -> - if __IgnoreEls -> - decode_message_els(__TopXMLNS, __IgnoreEls, _els, Error, - Thread, Subject, Body, [_el | __Els]); - true -> - case is_known_tag(_el) of - true -> - decode_message_els(__TopXMLNS, __IgnoreEls, _els, Error, - Thread, Subject, Body, - [decode(_el) | __Els]); - false -> - decode_message_els(__TopXMLNS, __IgnoreEls, _els, Error, - Thread, Subject, Body, __Els) - end - end; -decode_message_els(__TopXMLNS, __IgnoreEls, [_ | _els], - Error, Thread, Subject, Body, __Els) -> - decode_message_els(__TopXMLNS, __IgnoreEls, _els, Error, - Thread, Subject, Body, __Els). - -decode_message_attrs(__TopXMLNS, - [{<<"id">>, _val} | _attrs], _Id, Type, From, To, - Lang) -> - decode_message_attrs(__TopXMLNS, _attrs, _val, Type, - From, To, Lang); -decode_message_attrs(__TopXMLNS, - [{<<"type">>, _val} | _attrs], Id, _Type, From, To, - Lang) -> - decode_message_attrs(__TopXMLNS, _attrs, Id, _val, From, - To, Lang); -decode_message_attrs(__TopXMLNS, - [{<<"from">>, _val} | _attrs], Id, Type, _From, To, - Lang) -> - decode_message_attrs(__TopXMLNS, _attrs, Id, Type, _val, - To, Lang); -decode_message_attrs(__TopXMLNS, - [{<<"to">>, _val} | _attrs], Id, Type, From, _To, - Lang) -> - decode_message_attrs(__TopXMLNS, _attrs, Id, Type, From, - _val, Lang); -decode_message_attrs(__TopXMLNS, - [{<<"xml:lang">>, _val} | _attrs], Id, Type, From, To, - _Lang) -> - decode_message_attrs(__TopXMLNS, _attrs, Id, Type, From, - To, _val); -decode_message_attrs(__TopXMLNS, [_ | _attrs], Id, Type, - From, To, Lang) -> - decode_message_attrs(__TopXMLNS, _attrs, Id, Type, From, - To, Lang); -decode_message_attrs(__TopXMLNS, [], Id, Type, From, To, - Lang) -> - {decode_message_attr_id(__TopXMLNS, Id), - decode_message_attr_type(__TopXMLNS, Type), - decode_message_attr_from(__TopXMLNS, From), - decode_message_attr_to(__TopXMLNS, To), - 'decode_message_attr_xml:lang'(__TopXMLNS, Lang)}. - -encode_message({message, Id, Type, Lang, From, To, - Subject, Body, Thread, Error, __Els}, - _xmlns_attrs) -> - _els = [encode(_el) || _el <- __Els] ++ - lists:reverse('encode_message_$error'(Error, - 'encode_message_$thread'(Thread, - 'encode_message_$subject'(Subject, - 'encode_message_$body'(Body, - []))))), - _attrs = 'encode_message_attr_xml:lang'(Lang, - encode_message_attr_to(To, - encode_message_attr_from(From, - encode_message_attr_type(Type, - encode_message_attr_id(Id, - _xmlns_attrs))))), - {xmlel, <<"message">>, _attrs, _els}. - -'encode_message_$error'(undefined, _acc) -> _acc; -'encode_message_$error'(Error, _acc) -> - [encode_error(Error, []) | _acc]. - -'encode_message_$thread'(undefined, _acc) -> _acc; -'encode_message_$thread'(Thread, _acc) -> - [encode_message_thread(Thread, []) | _acc]. - -'encode_message_$subject'([], _acc) -> _acc; -'encode_message_$subject'([Subject | _els], _acc) -> - 'encode_message_$subject'(_els, - [encode_message_subject(Subject, []) | _acc]). - -'encode_message_$body'([], _acc) -> _acc; -'encode_message_$body'([Body | _els], _acc) -> - 'encode_message_$body'(_els, - [encode_message_body(Body, []) | _acc]). - -decode_message_attr_id(__TopXMLNS, undefined) -> - undefined; -decode_message_attr_id(__TopXMLNS, _val) -> _val. - -encode_message_attr_id(undefined, _acc) -> _acc; -encode_message_attr_id(_val, _acc) -> - [{<<"id">>, _val} | _acc]. - -decode_message_attr_type(__TopXMLNS, undefined) -> - normal; -decode_message_attr_type(__TopXMLNS, _val) -> - case catch dec_enum(_val, - [chat, normal, groupchat, headline, error]) - of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"type">>, <<"message">>, - __TopXMLNS}}); - _res -> _res - end. - -encode_message_attr_type(normal, _acc) -> _acc; -encode_message_attr_type(_val, _acc) -> - [{<<"type">>, enc_enum(_val)} | _acc]. - -decode_message_attr_from(__TopXMLNS, undefined) -> - undefined; -decode_message_attr_from(__TopXMLNS, _val) -> - case catch dec_jid(_val) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"from">>, <<"message">>, - __TopXMLNS}}); - _res -> _res - end. - -encode_message_attr_from(undefined, _acc) -> _acc; -encode_message_attr_from(_val, _acc) -> - [{<<"from">>, enc_jid(_val)} | _acc]. - -decode_message_attr_to(__TopXMLNS, undefined) -> - undefined; -decode_message_attr_to(__TopXMLNS, _val) -> - case catch dec_jid(_val) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"to">>, <<"message">>, __TopXMLNS}}); - _res -> _res - end. - -encode_message_attr_to(undefined, _acc) -> _acc; -encode_message_attr_to(_val, _acc) -> - [{<<"to">>, enc_jid(_val)} | _acc]. - -'decode_message_attr_xml:lang'(__TopXMLNS, undefined) -> - undefined; -'decode_message_attr_xml:lang'(__TopXMLNS, _val) -> - _val. - -'encode_message_attr_xml:lang'(undefined, _acc) -> _acc; -'encode_message_attr_xml:lang'(_val, _acc) -> - [{<<"xml:lang">>, _val} | _acc]. - -decode_message_thread(__TopXMLNS, __IgnoreEls, - {xmlel, <<"thread">>, _attrs, _els}) -> - Cdata = decode_message_thread_els(__TopXMLNS, - __IgnoreEls, _els, <<>>), - Cdata. - -decode_message_thread_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_message_thread_cdata(__TopXMLNS, Cdata); -decode_message_thread_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_message_thread_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_message_thread_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_message_thread_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_message_thread(Cdata, _xmlns_attrs) -> - _els = encode_message_thread_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"thread">>, _attrs, _els}. - -decode_message_thread_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_message_thread_cdata(__TopXMLNS, _val) -> _val. - -encode_message_thread_cdata(undefined, _acc) -> _acc; -encode_message_thread_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_message_body(__TopXMLNS, __IgnoreEls, - {xmlel, <<"body">>, _attrs, _els}) -> - Data = decode_message_body_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - Lang = decode_message_body_attrs(__TopXMLNS, _attrs, - undefined), - {text, Lang, Data}. - -decode_message_body_els(__TopXMLNS, __IgnoreEls, [], - Data) -> - decode_message_body_cdata(__TopXMLNS, Data); -decode_message_body_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Data) -> - decode_message_body_els(__TopXMLNS, __IgnoreEls, _els, - <<Data/binary, _data/binary>>); -decode_message_body_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Data) -> - decode_message_body_els(__TopXMLNS, __IgnoreEls, _els, - Data). - -decode_message_body_attrs(__TopXMLNS, - [{<<"xml:lang">>, _val} | _attrs], _Lang) -> - decode_message_body_attrs(__TopXMLNS, _attrs, _val); -decode_message_body_attrs(__TopXMLNS, [_ | _attrs], - Lang) -> - decode_message_body_attrs(__TopXMLNS, _attrs, Lang); -decode_message_body_attrs(__TopXMLNS, [], Lang) -> - 'decode_message_body_attr_xml:lang'(__TopXMLNS, Lang). - -encode_message_body({text, Lang, Data}, _xmlns_attrs) -> - _els = encode_message_body_cdata(Data, []), - _attrs = 'encode_message_body_attr_xml:lang'(Lang, - _xmlns_attrs), - {xmlel, <<"body">>, _attrs, _els}. - -'decode_message_body_attr_xml:lang'(__TopXMLNS, - undefined) -> - undefined; -'decode_message_body_attr_xml:lang'(__TopXMLNS, _val) -> - _val. - -'encode_message_body_attr_xml:lang'(undefined, _acc) -> - _acc; -'encode_message_body_attr_xml:lang'(_val, _acc) -> - [{<<"xml:lang">>, _val} | _acc]. - -decode_message_body_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_message_body_cdata(__TopXMLNS, _val) -> _val. - -encode_message_body_cdata(undefined, _acc) -> _acc; -encode_message_body_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_message_subject(__TopXMLNS, __IgnoreEls, - {xmlel, <<"subject">>, _attrs, _els}) -> - Data = decode_message_subject_els(__TopXMLNS, - __IgnoreEls, _els, <<>>), - Lang = decode_message_subject_attrs(__TopXMLNS, _attrs, - undefined), - {text, Lang, Data}. - -decode_message_subject_els(__TopXMLNS, __IgnoreEls, [], - Data) -> - decode_message_subject_cdata(__TopXMLNS, Data); -decode_message_subject_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Data) -> - decode_message_subject_els(__TopXMLNS, __IgnoreEls, - _els, <<Data/binary, _data/binary>>); -decode_message_subject_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Data) -> - decode_message_subject_els(__TopXMLNS, __IgnoreEls, - _els, Data). - -decode_message_subject_attrs(__TopXMLNS, - [{<<"xml:lang">>, _val} | _attrs], _Lang) -> - decode_message_subject_attrs(__TopXMLNS, _attrs, _val); -decode_message_subject_attrs(__TopXMLNS, [_ | _attrs], - Lang) -> - decode_message_subject_attrs(__TopXMLNS, _attrs, Lang); -decode_message_subject_attrs(__TopXMLNS, [], Lang) -> - 'decode_message_subject_attr_xml:lang'(__TopXMLNS, - Lang). - -encode_message_subject({text, Lang, Data}, - _xmlns_attrs) -> - _els = encode_message_subject_cdata(Data, []), - _attrs = 'encode_message_subject_attr_xml:lang'(Lang, - _xmlns_attrs), - {xmlel, <<"subject">>, _attrs, _els}. - -'decode_message_subject_attr_xml:lang'(__TopXMLNS, - undefined) -> - undefined; -'decode_message_subject_attr_xml:lang'(__TopXMLNS, - _val) -> - _val. - -'encode_message_subject_attr_xml:lang'(undefined, - _acc) -> - _acc; -'encode_message_subject_attr_xml:lang'(_val, _acc) -> - [{<<"xml:lang">>, _val} | _acc]. - -decode_message_subject_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_message_subject_cdata(__TopXMLNS, _val) -> _val. - -encode_message_subject_cdata(undefined, _acc) -> _acc; -encode_message_subject_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_iq(__TopXMLNS, __IgnoreEls, - {xmlel, <<"iq">>, _attrs, _els}) -> - {Error, __Els} = decode_iq_els(__TopXMLNS, __IgnoreEls, - _els, undefined, []), - {Id, Type, From, To, Lang} = decode_iq_attrs(__TopXMLNS, - _attrs, undefined, undefined, - undefined, undefined, - undefined), - {iq, Id, Type, Lang, From, To, Error, __Els}. - -decode_iq_els(__TopXMLNS, __IgnoreEls, [], Error, - __Els) -> - {Error, lists:reverse(__Els)}; -decode_iq_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"error">>, _attrs, _} = _el | _els], Error, - __Els) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_iq_els(__TopXMLNS, __IgnoreEls, _els, - decode_error(__TopXMLNS, __IgnoreEls, _el), __Els); - true -> - decode_iq_els(__TopXMLNS, __IgnoreEls, _els, Error, - __Els) - end; -decode_iq_els(__TopXMLNS, __IgnoreEls, - [{xmlel, _, _, _} = _el | _els], Error, __Els) -> - if __IgnoreEls -> - decode_iq_els(__TopXMLNS, __IgnoreEls, _els, Error, - [_el | __Els]); - true -> - case is_known_tag(_el) of - true -> - decode_iq_els(__TopXMLNS, __IgnoreEls, _els, Error, - [decode(_el) | __Els]); - false -> - decode_iq_els(__TopXMLNS, __IgnoreEls, _els, Error, - __Els) - end - end; -decode_iq_els(__TopXMLNS, __IgnoreEls, [_ | _els], - Error, __Els) -> - decode_iq_els(__TopXMLNS, __IgnoreEls, _els, Error, - __Els). - -decode_iq_attrs(__TopXMLNS, [{<<"id">>, _val} | _attrs], - _Id, Type, From, To, Lang) -> - decode_iq_attrs(__TopXMLNS, _attrs, _val, Type, From, - To, Lang); -decode_iq_attrs(__TopXMLNS, - [{<<"type">>, _val} | _attrs], Id, _Type, From, To, - Lang) -> - decode_iq_attrs(__TopXMLNS, _attrs, Id, _val, From, To, - Lang); -decode_iq_attrs(__TopXMLNS, - [{<<"from">>, _val} | _attrs], Id, Type, _From, To, - Lang) -> - decode_iq_attrs(__TopXMLNS, _attrs, Id, Type, _val, To, - Lang); -decode_iq_attrs(__TopXMLNS, [{<<"to">>, _val} | _attrs], - Id, Type, From, _To, Lang) -> - decode_iq_attrs(__TopXMLNS, _attrs, Id, Type, From, - _val, Lang); -decode_iq_attrs(__TopXMLNS, - [{<<"xml:lang">>, _val} | _attrs], Id, Type, From, To, - _Lang) -> - decode_iq_attrs(__TopXMLNS, _attrs, Id, Type, From, To, - _val); -decode_iq_attrs(__TopXMLNS, [_ | _attrs], Id, Type, - From, To, Lang) -> - decode_iq_attrs(__TopXMLNS, _attrs, Id, Type, From, To, - Lang); -decode_iq_attrs(__TopXMLNS, [], Id, Type, From, To, - Lang) -> - {decode_iq_attr_id(__TopXMLNS, Id), - decode_iq_attr_type(__TopXMLNS, Type), - decode_iq_attr_from(__TopXMLNS, From), - decode_iq_attr_to(__TopXMLNS, To), - 'decode_iq_attr_xml:lang'(__TopXMLNS, Lang)}. - -encode_iq({iq, Id, Type, Lang, From, To, Error, __Els}, - _xmlns_attrs) -> - _els = [encode(_el) || _el <- __Els] ++ - lists:reverse('encode_iq_$error'(Error, [])), - _attrs = 'encode_iq_attr_xml:lang'(Lang, - encode_iq_attr_to(To, - encode_iq_attr_from(From, - encode_iq_attr_type(Type, - encode_iq_attr_id(Id, - _xmlns_attrs))))), - {xmlel, <<"iq">>, _attrs, _els}. - -'encode_iq_$error'(undefined, _acc) -> _acc; -'encode_iq_$error'(Error, _acc) -> - [encode_error(Error, []) | _acc]. - -decode_iq_attr_id(__TopXMLNS, undefined) -> - erlang:error({xmpp_codec, - {missing_attr, <<"id">>, <<"iq">>, __TopXMLNS}}); -decode_iq_attr_id(__TopXMLNS, _val) -> _val. - -encode_iq_attr_id(_val, _acc) -> - [{<<"id">>, _val} | _acc]. - -decode_iq_attr_type(__TopXMLNS, undefined) -> - erlang:error({xmpp_codec, - {missing_attr, <<"type">>, <<"iq">>, __TopXMLNS}}); -decode_iq_attr_type(__TopXMLNS, _val) -> - case catch dec_enum(_val, [get, set, result, error]) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"type">>, <<"iq">>, __TopXMLNS}}); - _res -> _res - end. - -encode_iq_attr_type(_val, _acc) -> - [{<<"type">>, enc_enum(_val)} | _acc]. - -decode_iq_attr_from(__TopXMLNS, undefined) -> undefined; -decode_iq_attr_from(__TopXMLNS, _val) -> - case catch dec_jid(_val) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"from">>, <<"iq">>, __TopXMLNS}}); - _res -> _res - end. - -encode_iq_attr_from(undefined, _acc) -> _acc; -encode_iq_attr_from(_val, _acc) -> - [{<<"from">>, enc_jid(_val)} | _acc]. - -decode_iq_attr_to(__TopXMLNS, undefined) -> undefined; -decode_iq_attr_to(__TopXMLNS, _val) -> - case catch dec_jid(_val) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"to">>, <<"iq">>, __TopXMLNS}}); - _res -> _res - end. - -encode_iq_attr_to(undefined, _acc) -> _acc; -encode_iq_attr_to(_val, _acc) -> - [{<<"to">>, enc_jid(_val)} | _acc]. - -'decode_iq_attr_xml:lang'(__TopXMLNS, undefined) -> - undefined; -'decode_iq_attr_xml:lang'(__TopXMLNS, _val) -> _val. - -'encode_iq_attr_xml:lang'(undefined, _acc) -> _acc; -'encode_iq_attr_xml:lang'(_val, _acc) -> - [{<<"xml:lang">>, _val} | _acc]. - -decode_stats(__TopXMLNS, __IgnoreEls, - {xmlel, <<"query">>, _attrs, _els}) -> - Stat = decode_stats_els(__TopXMLNS, __IgnoreEls, _els, - []), - {stats, Stat}. - -decode_stats_els(__TopXMLNS, __IgnoreEls, [], Stat) -> - lists:reverse(Stat); -decode_stats_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"stat">>, _attrs, _} = _el | _els], Stat) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_stats_els(__TopXMLNS, __IgnoreEls, _els, - [decode_stat(__TopXMLNS, __IgnoreEls, _el) | Stat]); - true -> - decode_stats_els(__TopXMLNS, __IgnoreEls, _els, Stat) - end; -decode_stats_els(__TopXMLNS, __IgnoreEls, [_ | _els], - Stat) -> - decode_stats_els(__TopXMLNS, __IgnoreEls, _els, Stat). - -encode_stats({stats, Stat}, _xmlns_attrs) -> - _els = lists:reverse('encode_stats_$stat'(Stat, [])), - _attrs = _xmlns_attrs, - {xmlel, <<"query">>, _attrs, _els}. - -'encode_stats_$stat'([], _acc) -> _acc; -'encode_stats_$stat'([Stat | _els], _acc) -> - 'encode_stats_$stat'(_els, - [encode_stat(Stat, []) | _acc]). - -decode_stat(__TopXMLNS, __IgnoreEls, - {xmlel, <<"stat">>, _attrs, _els}) -> - Error = decode_stat_els(__TopXMLNS, __IgnoreEls, _els, - []), - {Name, Units, Value} = decode_stat_attrs(__TopXMLNS, - _attrs, undefined, undefined, - undefined), - {stat, Name, Units, Value, Error}. - -decode_stat_els(__TopXMLNS, __IgnoreEls, [], Error) -> - lists:reverse(Error); -decode_stat_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"error">>, _attrs, _} = _el | _els], - Error) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_stat_els(__TopXMLNS, __IgnoreEls, _els, - [decode_stat_error(__TopXMLNS, __IgnoreEls, _el) - | Error]); - true -> - decode_stat_els(__TopXMLNS, __IgnoreEls, _els, Error) - end; -decode_stat_els(__TopXMLNS, __IgnoreEls, [_ | _els], - Error) -> - decode_stat_els(__TopXMLNS, __IgnoreEls, _els, Error). - -decode_stat_attrs(__TopXMLNS, - [{<<"name">>, _val} | _attrs], _Name, Units, Value) -> - decode_stat_attrs(__TopXMLNS, _attrs, _val, Units, - Value); -decode_stat_attrs(__TopXMLNS, - [{<<"units">>, _val} | _attrs], Name, _Units, Value) -> - decode_stat_attrs(__TopXMLNS, _attrs, Name, _val, - Value); -decode_stat_attrs(__TopXMLNS, - [{<<"value">>, _val} | _attrs], Name, Units, _Value) -> - decode_stat_attrs(__TopXMLNS, _attrs, Name, Units, - _val); -decode_stat_attrs(__TopXMLNS, [_ | _attrs], Name, Units, - Value) -> - decode_stat_attrs(__TopXMLNS, _attrs, Name, Units, - Value); -decode_stat_attrs(__TopXMLNS, [], Name, Units, Value) -> - {decode_stat_attr_name(__TopXMLNS, Name), - decode_stat_attr_units(__TopXMLNS, Units), - decode_stat_attr_value(__TopXMLNS, Value)}. - -encode_stat({stat, Name, Units, Value, Error}, - _xmlns_attrs) -> - _els = lists:reverse('encode_stat_$error'(Error, [])), - _attrs = encode_stat_attr_value(Value, - encode_stat_attr_units(Units, - encode_stat_attr_name(Name, - _xmlns_attrs))), - {xmlel, <<"stat">>, _attrs, _els}. - -'encode_stat_$error'([], _acc) -> _acc; -'encode_stat_$error'([Error | _els], _acc) -> - 'encode_stat_$error'(_els, - [encode_stat_error(Error, []) | _acc]). - -decode_stat_attr_name(__TopXMLNS, undefined) -> - erlang:error({xmpp_codec, - {missing_attr, <<"name">>, <<"stat">>, __TopXMLNS}}); -decode_stat_attr_name(__TopXMLNS, _val) -> _val. - -encode_stat_attr_name(_val, _acc) -> - [{<<"name">>, _val} | _acc]. - -decode_stat_attr_units(__TopXMLNS, undefined) -> - undefined; -decode_stat_attr_units(__TopXMLNS, _val) -> _val. - -encode_stat_attr_units(undefined, _acc) -> _acc; -encode_stat_attr_units(_val, _acc) -> - [{<<"units">>, _val} | _acc]. - -decode_stat_attr_value(__TopXMLNS, undefined) -> - undefined; -decode_stat_attr_value(__TopXMLNS, _val) -> _val. - -encode_stat_attr_value(undefined, _acc) -> _acc; -encode_stat_attr_value(_val, _acc) -> - [{<<"value">>, _val} | _acc]. - -decode_stat_error(__TopXMLNS, __IgnoreEls, - {xmlel, <<"error">>, _attrs, _els}) -> - Cdata = decode_stat_error_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - Code = decode_stat_error_attrs(__TopXMLNS, _attrs, - undefined), - {Code, Cdata}. - -decode_stat_error_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_stat_error_cdata(__TopXMLNS, Cdata); -decode_stat_error_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_stat_error_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_stat_error_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_stat_error_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -decode_stat_error_attrs(__TopXMLNS, - [{<<"code">>, _val} | _attrs], _Code) -> - decode_stat_error_attrs(__TopXMLNS, _attrs, _val); -decode_stat_error_attrs(__TopXMLNS, [_ | _attrs], - Code) -> - decode_stat_error_attrs(__TopXMLNS, _attrs, Code); -decode_stat_error_attrs(__TopXMLNS, [], Code) -> - decode_stat_error_attr_code(__TopXMLNS, Code). - -encode_stat_error({Code, Cdata}, _xmlns_attrs) -> - _els = encode_stat_error_cdata(Cdata, []), - _attrs = encode_stat_error_attr_code(Code, - _xmlns_attrs), - {xmlel, <<"error">>, _attrs, _els}. - -decode_stat_error_attr_code(__TopXMLNS, undefined) -> - erlang:error({xmpp_codec, - {missing_attr, <<"code">>, <<"error">>, __TopXMLNS}}); -decode_stat_error_attr_code(__TopXMLNS, _val) -> - case catch dec_int(_val) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"code">>, <<"error">>, __TopXMLNS}}); - _res -> _res - end. - -encode_stat_error_attr_code(_val, _acc) -> - [{<<"code">>, enc_int(_val)} | _acc]. - -decode_stat_error_cdata(__TopXMLNS, <<>>) -> undefined; -decode_stat_error_cdata(__TopXMLNS, _val) -> _val. - -encode_stat_error_cdata(undefined, _acc) -> _acc; -encode_stat_error_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_bookmarks_storage(__TopXMLNS, __IgnoreEls, - {xmlel, <<"storage">>, _attrs, _els}) -> - {Conference, Url} = - decode_bookmarks_storage_els(__TopXMLNS, __IgnoreEls, - _els, [], []), - {bookmark_storage, Conference, Url}. - -decode_bookmarks_storage_els(__TopXMLNS, __IgnoreEls, - [], Conference, Url) -> - {lists:reverse(Conference), lists:reverse(Url)}; -decode_bookmarks_storage_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"conference">>, _attrs, _} = _el - | _els], - Conference, Url) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_bookmarks_storage_els(__TopXMLNS, __IgnoreEls, - _els, - [decode_bookmark_conference(__TopXMLNS, - __IgnoreEls, - _el) - | Conference], - Url); - true -> - decode_bookmarks_storage_els(__TopXMLNS, __IgnoreEls, - _els, Conference, Url) - end; -decode_bookmarks_storage_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"url">>, _attrs, _} = _el | _els], - Conference, Url) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_bookmarks_storage_els(__TopXMLNS, __IgnoreEls, - _els, Conference, - [decode_bookmark_url(__TopXMLNS, - __IgnoreEls, _el) - | Url]); - true -> - decode_bookmarks_storage_els(__TopXMLNS, __IgnoreEls, - _els, Conference, Url) - end; -decode_bookmarks_storage_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Conference, Url) -> - decode_bookmarks_storage_els(__TopXMLNS, __IgnoreEls, - _els, Conference, Url). - -encode_bookmarks_storage({bookmark_storage, Conference, - Url}, - _xmlns_attrs) -> - _els = - lists:reverse('encode_bookmarks_storage_$conference'(Conference, - 'encode_bookmarks_storage_$url'(Url, - []))), - _attrs = _xmlns_attrs, - {xmlel, <<"storage">>, _attrs, _els}. - -'encode_bookmarks_storage_$conference'([], _acc) -> - _acc; -'encode_bookmarks_storage_$conference'([Conference - | _els], - _acc) -> - 'encode_bookmarks_storage_$conference'(_els, - [encode_bookmark_conference(Conference, - []) - | _acc]). - -'encode_bookmarks_storage_$url'([], _acc) -> _acc; -'encode_bookmarks_storage_$url'([Url | _els], _acc) -> - 'encode_bookmarks_storage_$url'(_els, - [encode_bookmark_url(Url, []) | _acc]). - -decode_bookmark_url(__TopXMLNS, __IgnoreEls, - {xmlel, <<"url">>, _attrs, _els}) -> - {Name, Url} = decode_bookmark_url_attrs(__TopXMLNS, - _attrs, undefined, undefined), - {bookmark_url, Name, Url}. - -decode_bookmark_url_attrs(__TopXMLNS, - [{<<"name">>, _val} | _attrs], _Name, Url) -> - decode_bookmark_url_attrs(__TopXMLNS, _attrs, _val, - Url); -decode_bookmark_url_attrs(__TopXMLNS, - [{<<"url">>, _val} | _attrs], Name, _Url) -> - decode_bookmark_url_attrs(__TopXMLNS, _attrs, Name, - _val); -decode_bookmark_url_attrs(__TopXMLNS, [_ | _attrs], - Name, Url) -> - decode_bookmark_url_attrs(__TopXMLNS, _attrs, Name, - Url); -decode_bookmark_url_attrs(__TopXMLNS, [], Name, Url) -> - {decode_bookmark_url_attr_name(__TopXMLNS, Name), - decode_bookmark_url_attr_url(__TopXMLNS, Url)}. - -encode_bookmark_url({bookmark_url, Name, Url}, - _xmlns_attrs) -> - _els = [], - _attrs = encode_bookmark_url_attr_url(Url, - encode_bookmark_url_attr_name(Name, - _xmlns_attrs)), - {xmlel, <<"url">>, _attrs, _els}. - -decode_bookmark_url_attr_name(__TopXMLNS, undefined) -> - erlang:error({xmpp_codec, - {missing_attr, <<"name">>, <<"url">>, __TopXMLNS}}); -decode_bookmark_url_attr_name(__TopXMLNS, _val) -> _val. - -encode_bookmark_url_attr_name(_val, _acc) -> - [{<<"name">>, _val} | _acc]. - -decode_bookmark_url_attr_url(__TopXMLNS, undefined) -> - erlang:error({xmpp_codec, - {missing_attr, <<"url">>, <<"url">>, __TopXMLNS}}); -decode_bookmark_url_attr_url(__TopXMLNS, _val) -> _val. - -encode_bookmark_url_attr_url(_val, _acc) -> - [{<<"url">>, _val} | _acc]. - -decode_bookmark_conference(__TopXMLNS, __IgnoreEls, - {xmlel, <<"conference">>, _attrs, _els}) -> - {Password, Nick} = - decode_bookmark_conference_els(__TopXMLNS, __IgnoreEls, - _els, undefined, undefined), - {Name, Jid, Autojoin} = - decode_bookmark_conference_attrs(__TopXMLNS, _attrs, - undefined, undefined, undefined), - {bookmark_conference, Name, Jid, Autojoin, Nick, - Password}. - -decode_bookmark_conference_els(__TopXMLNS, __IgnoreEls, - [], Password, Nick) -> - {Password, Nick}; -decode_bookmark_conference_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"nick">>, _attrs, _} = _el | _els], - Password, Nick) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_bookmark_conference_els(__TopXMLNS, __IgnoreEls, - _els, Password, - decode_conference_nick(__TopXMLNS, - __IgnoreEls, - _el)); - true -> - decode_bookmark_conference_els(__TopXMLNS, __IgnoreEls, - _els, Password, Nick) - end; -decode_bookmark_conference_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"password">>, _attrs, _} = _el - | _els], - Password, Nick) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_bookmark_conference_els(__TopXMLNS, __IgnoreEls, - _els, - decode_conference_password(__TopXMLNS, - __IgnoreEls, - _el), - Nick); - true -> - decode_bookmark_conference_els(__TopXMLNS, __IgnoreEls, - _els, Password, Nick) - end; -decode_bookmark_conference_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Password, Nick) -> - decode_bookmark_conference_els(__TopXMLNS, __IgnoreEls, - _els, Password, Nick). - -decode_bookmark_conference_attrs(__TopXMLNS, - [{<<"name">>, _val} | _attrs], _Name, Jid, - Autojoin) -> - decode_bookmark_conference_attrs(__TopXMLNS, _attrs, - _val, Jid, Autojoin); -decode_bookmark_conference_attrs(__TopXMLNS, - [{<<"jid">>, _val} | _attrs], Name, _Jid, - Autojoin) -> - decode_bookmark_conference_attrs(__TopXMLNS, _attrs, - Name, _val, Autojoin); -decode_bookmark_conference_attrs(__TopXMLNS, - [{<<"autojoin">>, _val} | _attrs], Name, Jid, - _Autojoin) -> - decode_bookmark_conference_attrs(__TopXMLNS, _attrs, - Name, Jid, _val); -decode_bookmark_conference_attrs(__TopXMLNS, - [_ | _attrs], Name, Jid, Autojoin) -> - decode_bookmark_conference_attrs(__TopXMLNS, _attrs, - Name, Jid, Autojoin); -decode_bookmark_conference_attrs(__TopXMLNS, [], Name, - Jid, Autojoin) -> - {decode_bookmark_conference_attr_name(__TopXMLNS, Name), - decode_bookmark_conference_attr_jid(__TopXMLNS, Jid), - decode_bookmark_conference_attr_autojoin(__TopXMLNS, - Autojoin)}. - -encode_bookmark_conference({bookmark_conference, Name, - Jid, Autojoin, Nick, Password}, - _xmlns_attrs) -> - _els = - lists:reverse('encode_bookmark_conference_$password'(Password, - 'encode_bookmark_conference_$nick'(Nick, - []))), - _attrs = - encode_bookmark_conference_attr_autojoin(Autojoin, - encode_bookmark_conference_attr_jid(Jid, - encode_bookmark_conference_attr_name(Name, - _xmlns_attrs))), - {xmlel, <<"conference">>, _attrs, _els}. - -'encode_bookmark_conference_$password'(undefined, - _acc) -> - _acc; -'encode_bookmark_conference_$password'(Password, - _acc) -> - [encode_conference_password(Password, []) | _acc]. - -'encode_bookmark_conference_$nick'(undefined, _acc) -> - _acc; -'encode_bookmark_conference_$nick'(Nick, _acc) -> - [encode_conference_nick(Nick, []) | _acc]. - -decode_bookmark_conference_attr_name(__TopXMLNS, - undefined) -> - erlang:error({xmpp_codec, - {missing_attr, <<"name">>, <<"conference">>, - __TopXMLNS}}); -decode_bookmark_conference_attr_name(__TopXMLNS, - _val) -> - _val. - -encode_bookmark_conference_attr_name(_val, _acc) -> - [{<<"name">>, _val} | _acc]. - -decode_bookmark_conference_attr_jid(__TopXMLNS, - undefined) -> - erlang:error({xmpp_codec, - {missing_attr, <<"jid">>, <<"conference">>, - __TopXMLNS}}); -decode_bookmark_conference_attr_jid(__TopXMLNS, _val) -> - case catch dec_jid(_val) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"jid">>, <<"conference">>, - __TopXMLNS}}); - _res -> _res - end. - -encode_bookmark_conference_attr_jid(_val, _acc) -> - [{<<"jid">>, enc_jid(_val)} | _acc]. - -decode_bookmark_conference_attr_autojoin(__TopXMLNS, - undefined) -> - false; -decode_bookmark_conference_attr_autojoin(__TopXMLNS, - _val) -> - case catch dec_bool(_val) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"autojoin">>, <<"conference">>, - __TopXMLNS}}); - _res -> _res - end. - -encode_bookmark_conference_attr_autojoin(false, _acc) -> - _acc; -encode_bookmark_conference_attr_autojoin(_val, _acc) -> - [{<<"autojoin">>, enc_bool(_val)} | _acc]. - -decode_conference_password(__TopXMLNS, __IgnoreEls, - {xmlel, <<"password">>, _attrs, _els}) -> - Cdata = decode_conference_password_els(__TopXMLNS, - __IgnoreEls, _els, <<>>), - Cdata. - -decode_conference_password_els(__TopXMLNS, __IgnoreEls, - [], Cdata) -> - decode_conference_password_cdata(__TopXMLNS, Cdata); -decode_conference_password_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_conference_password_els(__TopXMLNS, __IgnoreEls, - _els, <<Cdata/binary, _data/binary>>); -decode_conference_password_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_conference_password_els(__TopXMLNS, __IgnoreEls, - _els, Cdata). - -encode_conference_password(Cdata, _xmlns_attrs) -> - _els = encode_conference_password_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"password">>, _attrs, _els}. - -decode_conference_password_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_conference_password_cdata(__TopXMLNS, _val) -> - _val. - -encode_conference_password_cdata(undefined, _acc) -> - _acc; -encode_conference_password_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_conference_nick(__TopXMLNS, __IgnoreEls, - {xmlel, <<"nick">>, _attrs, _els}) -> - Cdata = decode_conference_nick_els(__TopXMLNS, - __IgnoreEls, _els, <<>>), - Cdata. - -decode_conference_nick_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_conference_nick_cdata(__TopXMLNS, Cdata); -decode_conference_nick_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_conference_nick_els(__TopXMLNS, __IgnoreEls, - _els, <<Cdata/binary, _data/binary>>); -decode_conference_nick_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_conference_nick_els(__TopXMLNS, __IgnoreEls, - _els, Cdata). - -encode_conference_nick(Cdata, _xmlns_attrs) -> - _els = encode_conference_nick_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"nick">>, _attrs, _els}. - -decode_conference_nick_cdata(__TopXMLNS, <<>>) -> - undefined; -decode_conference_nick_cdata(__TopXMLNS, _val) -> _val. - -encode_conference_nick_cdata(undefined, _acc) -> _acc; -encode_conference_nick_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_private(__TopXMLNS, __IgnoreEls, - {xmlel, <<"query">>, _attrs, _els}) -> - __Xmls = decode_private_els(__TopXMLNS, __IgnoreEls, - _els, []), - {private, __Xmls}. - -decode_private_els(__TopXMLNS, __IgnoreEls, [], - __Xmls) -> - lists:reverse(__Xmls); -decode_private_els(__TopXMLNS, __IgnoreEls, - [{xmlel, _, _, _} = _el | _els], __Xmls) -> - decode_private_els(__TopXMLNS, __IgnoreEls, _els, - [_el | __Xmls]); -decode_private_els(__TopXMLNS, __IgnoreEls, [_ | _els], - __Xmls) -> - decode_private_els(__TopXMLNS, __IgnoreEls, _els, - __Xmls). - -encode_private({private, __Xmls}, _xmlns_attrs) -> - _els = __Xmls, - _attrs = _xmlns_attrs, - {xmlel, <<"query">>, _attrs, _els}. - -decode_disco_items(__TopXMLNS, __IgnoreEls, - {xmlel, <<"query">>, _attrs, _els}) -> - Items = decode_disco_items_els(__TopXMLNS, __IgnoreEls, - _els, []), - Node = decode_disco_items_attrs(__TopXMLNS, _attrs, - undefined), - {disco_items, Node, Items}. - -decode_disco_items_els(__TopXMLNS, __IgnoreEls, [], - Items) -> - lists:reverse(Items); -decode_disco_items_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"item">>, _attrs, _} = _el | _els], Items) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_disco_items_els(__TopXMLNS, __IgnoreEls, _els, - [decode_disco_item(__TopXMLNS, __IgnoreEls, - _el) - | Items]); - true -> - decode_disco_items_els(__TopXMLNS, __IgnoreEls, _els, - Items) - end; -decode_disco_items_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Items) -> - decode_disco_items_els(__TopXMLNS, __IgnoreEls, _els, - Items). - -decode_disco_items_attrs(__TopXMLNS, - [{<<"node">>, _val} | _attrs], _Node) -> - decode_disco_items_attrs(__TopXMLNS, _attrs, _val); -decode_disco_items_attrs(__TopXMLNS, [_ | _attrs], - Node) -> - decode_disco_items_attrs(__TopXMLNS, _attrs, Node); -decode_disco_items_attrs(__TopXMLNS, [], Node) -> - decode_disco_items_attr_node(__TopXMLNS, Node). - -encode_disco_items({disco_items, Node, Items}, - _xmlns_attrs) -> - _els = lists:reverse('encode_disco_items_$items'(Items, - [])), - _attrs = encode_disco_items_attr_node(Node, - _xmlns_attrs), - {xmlel, <<"query">>, _attrs, _els}. - -'encode_disco_items_$items'([], _acc) -> _acc; -'encode_disco_items_$items'([Items | _els], _acc) -> - 'encode_disco_items_$items'(_els, - [encode_disco_item(Items, []) | _acc]). - -decode_disco_items_attr_node(__TopXMLNS, undefined) -> - undefined; -decode_disco_items_attr_node(__TopXMLNS, _val) -> _val. - -encode_disco_items_attr_node(undefined, _acc) -> _acc; -encode_disco_items_attr_node(_val, _acc) -> - [{<<"node">>, _val} | _acc]. - -decode_disco_item(__TopXMLNS, __IgnoreEls, - {xmlel, <<"item">>, _attrs, _els}) -> - {Jid, Name, Node} = decode_disco_item_attrs(__TopXMLNS, - _attrs, undefined, undefined, - undefined), - {disco_item, Jid, Name, Node}. - -decode_disco_item_attrs(__TopXMLNS, - [{<<"jid">>, _val} | _attrs], _Jid, Name, Node) -> - decode_disco_item_attrs(__TopXMLNS, _attrs, _val, Name, - Node); -decode_disco_item_attrs(__TopXMLNS, - [{<<"name">>, _val} | _attrs], Jid, _Name, Node) -> - decode_disco_item_attrs(__TopXMLNS, _attrs, Jid, _val, - Node); -decode_disco_item_attrs(__TopXMLNS, - [{<<"node">>, _val} | _attrs], Jid, Name, _Node) -> - decode_disco_item_attrs(__TopXMLNS, _attrs, Jid, Name, - _val); -decode_disco_item_attrs(__TopXMLNS, [_ | _attrs], Jid, - Name, Node) -> - decode_disco_item_attrs(__TopXMLNS, _attrs, Jid, Name, - Node); -decode_disco_item_attrs(__TopXMLNS, [], Jid, Name, - Node) -> - {decode_disco_item_attr_jid(__TopXMLNS, Jid), - decode_disco_item_attr_name(__TopXMLNS, Name), - decode_disco_item_attr_node(__TopXMLNS, Node)}. - -encode_disco_item({disco_item, Jid, Name, Node}, - _xmlns_attrs) -> - _els = [], - _attrs = encode_disco_item_attr_node(Node, - encode_disco_item_attr_name(Name, - encode_disco_item_attr_jid(Jid, - _xmlns_attrs))), - {xmlel, <<"item">>, _attrs, _els}. - -decode_disco_item_attr_jid(__TopXMLNS, undefined) -> - erlang:error({xmpp_codec, - {missing_attr, <<"jid">>, <<"item">>, __TopXMLNS}}); -decode_disco_item_attr_jid(__TopXMLNS, _val) -> - case catch dec_jid(_val) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"jid">>, <<"item">>, __TopXMLNS}}); - _res -> _res - end. - -encode_disco_item_attr_jid(_val, _acc) -> - [{<<"jid">>, enc_jid(_val)} | _acc]. - -decode_disco_item_attr_name(__TopXMLNS, undefined) -> - undefined; -decode_disco_item_attr_name(__TopXMLNS, _val) -> _val. - -encode_disco_item_attr_name(undefined, _acc) -> _acc; -encode_disco_item_attr_name(_val, _acc) -> - [{<<"name">>, _val} | _acc]. - -decode_disco_item_attr_node(__TopXMLNS, undefined) -> - undefined; -decode_disco_item_attr_node(__TopXMLNS, _val) -> _val. - -encode_disco_item_attr_node(undefined, _acc) -> _acc; -encode_disco_item_attr_node(_val, _acc) -> - [{<<"node">>, _val} | _acc]. - -decode_disco_info(__TopXMLNS, __IgnoreEls, - {xmlel, <<"query">>, _attrs, _els}) -> - {Xdata, Features, Identities} = - decode_disco_info_els(__TopXMLNS, __IgnoreEls, _els, [], - [], []), - Node = decode_disco_info_attrs(__TopXMLNS, _attrs, - undefined), - {disco_info, Node, Identities, Features, Xdata}. - -decode_disco_info_els(__TopXMLNS, __IgnoreEls, [], - Xdata, Features, Identities) -> - {lists:reverse(Xdata), lists:reverse(Features), - lists:reverse(Identities)}; -decode_disco_info_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"identity">>, _attrs, _} = _el | _els], - Xdata, Features, Identities) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_disco_info_els(__TopXMLNS, __IgnoreEls, _els, - Xdata, Features, - [decode_disco_identity(__TopXMLNS, __IgnoreEls, - _el) - | Identities]); - true -> - decode_disco_info_els(__TopXMLNS, __IgnoreEls, _els, - Xdata, Features, Identities) - end; -decode_disco_info_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"feature">>, _attrs, _} = _el | _els], Xdata, - Features, Identities) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_disco_info_els(__TopXMLNS, __IgnoreEls, _els, - Xdata, - [decode_disco_feature(__TopXMLNS, __IgnoreEls, - _el) - | Features], - Identities); - true -> - decode_disco_info_els(__TopXMLNS, __IgnoreEls, _els, - Xdata, Features, Identities) - end; -decode_disco_info_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"x">>, _attrs, _} = _el | _els], Xdata, - Features, Identities) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<"jabber:x:data">> -> - decode_disco_info_els(__TopXMLNS, __IgnoreEls, _els, - [decode_xdata(_xmlns, __IgnoreEls, _el) - | Xdata], - Features, Identities); - true -> - decode_disco_info_els(__TopXMLNS, __IgnoreEls, _els, - Xdata, Features, Identities) - end; -decode_disco_info_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Xdata, Features, Identities) -> - decode_disco_info_els(__TopXMLNS, __IgnoreEls, _els, - Xdata, Features, Identities). - -decode_disco_info_attrs(__TopXMLNS, - [{<<"node">>, _val} | _attrs], _Node) -> - decode_disco_info_attrs(__TopXMLNS, _attrs, _val); -decode_disco_info_attrs(__TopXMLNS, [_ | _attrs], - Node) -> - decode_disco_info_attrs(__TopXMLNS, _attrs, Node); -decode_disco_info_attrs(__TopXMLNS, [], Node) -> - decode_disco_info_attr_node(__TopXMLNS, Node). - -encode_disco_info({disco_info, Node, Identities, - Features, Xdata}, - _xmlns_attrs) -> - _els = lists:reverse('encode_disco_info_$xdata'(Xdata, - 'encode_disco_info_$features'(Features, - 'encode_disco_info_$identities'(Identities, - [])))), - _attrs = encode_disco_info_attr_node(Node, - _xmlns_attrs), - {xmlel, <<"query">>, _attrs, _els}. - -'encode_disco_info_$xdata'([], _acc) -> _acc; -'encode_disco_info_$xdata'([Xdata | _els], _acc) -> - 'encode_disco_info_$xdata'(_els, - [encode_xdata(Xdata, - [{<<"xmlns">>, - <<"jabber:x:data">>}]) - | _acc]). - -'encode_disco_info_$features'([], _acc) -> _acc; -'encode_disco_info_$features'([Features | _els], - _acc) -> - 'encode_disco_info_$features'(_els, - [encode_disco_feature(Features, []) | _acc]). - -'encode_disco_info_$identities'([], _acc) -> _acc; -'encode_disco_info_$identities'([Identities | _els], - _acc) -> - 'encode_disco_info_$identities'(_els, - [encode_disco_identity(Identities, []) - | _acc]). - -decode_disco_info_attr_node(__TopXMLNS, undefined) -> - undefined; -decode_disco_info_attr_node(__TopXMLNS, _val) -> _val. - -encode_disco_info_attr_node(undefined, _acc) -> _acc; -encode_disco_info_attr_node(_val, _acc) -> - [{<<"node">>, _val} | _acc]. - -decode_disco_feature(__TopXMLNS, __IgnoreEls, - {xmlel, <<"feature">>, _attrs, _els}) -> - Var = decode_disco_feature_attrs(__TopXMLNS, _attrs, - undefined), - Var. - -decode_disco_feature_attrs(__TopXMLNS, - [{<<"var">>, _val} | _attrs], _Var) -> - decode_disco_feature_attrs(__TopXMLNS, _attrs, _val); -decode_disco_feature_attrs(__TopXMLNS, [_ | _attrs], - Var) -> - decode_disco_feature_attrs(__TopXMLNS, _attrs, Var); -decode_disco_feature_attrs(__TopXMLNS, [], Var) -> - decode_disco_feature_attr_var(__TopXMLNS, Var). - -encode_disco_feature(Var, _xmlns_attrs) -> - _els = [], - _attrs = encode_disco_feature_attr_var(Var, - _xmlns_attrs), - {xmlel, <<"feature">>, _attrs, _els}. - -decode_disco_feature_attr_var(__TopXMLNS, undefined) -> - erlang:error({xmpp_codec, - {missing_attr, <<"var">>, <<"feature">>, __TopXMLNS}}); -decode_disco_feature_attr_var(__TopXMLNS, _val) -> _val. - -encode_disco_feature_attr_var(_val, _acc) -> - [{<<"var">>, _val} | _acc]. - -decode_disco_identity(__TopXMLNS, __IgnoreEls, - {xmlel, <<"identity">>, _attrs, _els}) -> - {Category, Type, Lang, Name} = - decode_disco_identity_attrs(__TopXMLNS, _attrs, - undefined, undefined, undefined, undefined), - {identity, Category, Type, Lang, Name}. - -decode_disco_identity_attrs(__TopXMLNS, - [{<<"category">>, _val} | _attrs], _Category, Type, - Lang, Name) -> - decode_disco_identity_attrs(__TopXMLNS, _attrs, _val, - Type, Lang, Name); -decode_disco_identity_attrs(__TopXMLNS, - [{<<"type">>, _val} | _attrs], Category, _Type, - Lang, Name) -> - decode_disco_identity_attrs(__TopXMLNS, _attrs, - Category, _val, Lang, Name); -decode_disco_identity_attrs(__TopXMLNS, - [{<<"xml:lang">>, _val} | _attrs], Category, Type, - _Lang, Name) -> - decode_disco_identity_attrs(__TopXMLNS, _attrs, - Category, Type, _val, Name); -decode_disco_identity_attrs(__TopXMLNS, - [{<<"name">>, _val} | _attrs], Category, Type, Lang, - _Name) -> - decode_disco_identity_attrs(__TopXMLNS, _attrs, - Category, Type, Lang, _val); -decode_disco_identity_attrs(__TopXMLNS, [_ | _attrs], - Category, Type, Lang, Name) -> - decode_disco_identity_attrs(__TopXMLNS, _attrs, - Category, Type, Lang, Name); -decode_disco_identity_attrs(__TopXMLNS, [], Category, - Type, Lang, Name) -> - {decode_disco_identity_attr_category(__TopXMLNS, - Category), - decode_disco_identity_attr_type(__TopXMLNS, Type), - 'decode_disco_identity_attr_xml:lang'(__TopXMLNS, Lang), - decode_disco_identity_attr_name(__TopXMLNS, Name)}. - -encode_disco_identity({identity, Category, Type, Lang, - Name}, - _xmlns_attrs) -> - _els = [], - _attrs = encode_disco_identity_attr_name(Name, - 'encode_disco_identity_attr_xml:lang'(Lang, - encode_disco_identity_attr_type(Type, - encode_disco_identity_attr_category(Category, - _xmlns_attrs)))), - {xmlel, <<"identity">>, _attrs, _els}. - -decode_disco_identity_attr_category(__TopXMLNS, - undefined) -> - erlang:error({xmpp_codec, - {missing_attr, <<"category">>, <<"identity">>, - __TopXMLNS}}); -decode_disco_identity_attr_category(__TopXMLNS, _val) -> - _val. - -encode_disco_identity_attr_category(_val, _acc) -> - [{<<"category">>, _val} | _acc]. - -decode_disco_identity_attr_type(__TopXMLNS, - undefined) -> - erlang:error({xmpp_codec, - {missing_attr, <<"type">>, <<"identity">>, - __TopXMLNS}}); -decode_disco_identity_attr_type(__TopXMLNS, _val) -> - _val. - -encode_disco_identity_attr_type(_val, _acc) -> - [{<<"type">>, _val} | _acc]. - -'decode_disco_identity_attr_xml:lang'(__TopXMLNS, - undefined) -> - undefined; -'decode_disco_identity_attr_xml:lang'(__TopXMLNS, - _val) -> - _val. - -'encode_disco_identity_attr_xml:lang'(undefined, - _acc) -> - _acc; -'encode_disco_identity_attr_xml:lang'(_val, _acc) -> - [{<<"xml:lang">>, _val} | _acc]. - -decode_disco_identity_attr_name(__TopXMLNS, - undefined) -> - undefined; -decode_disco_identity_attr_name(__TopXMLNS, _val) -> - _val. - -encode_disco_identity_attr_name(undefined, _acc) -> - _acc; -encode_disco_identity_attr_name(_val, _acc) -> - [{<<"name">>, _val} | _acc]. - -decode_block_list(__TopXMLNS, __IgnoreEls, - {xmlel, <<"blocklist">>, _attrs, _els}) -> - {block_list}. - -encode_block_list({block_list}, _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"blocklist">>, _attrs, _els}. - -decode_unblock(__TopXMLNS, __IgnoreEls, - {xmlel, <<"unblock">>, _attrs, _els}) -> - Items = decode_unblock_els(__TopXMLNS, __IgnoreEls, - _els, []), - {unblock, Items}. - -decode_unblock_els(__TopXMLNS, __IgnoreEls, [], - Items) -> - lists:reverse(Items); -decode_unblock_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"item">>, _attrs, _} = _el | _els], Items) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_unblock_els(__TopXMLNS, __IgnoreEls, _els, - case decode_block_item(__TopXMLNS, __IgnoreEls, - _el) - of - undefined -> Items; - _new_el -> [_new_el | Items] - end); - true -> - decode_unblock_els(__TopXMLNS, __IgnoreEls, _els, Items) - end; -decode_unblock_els(__TopXMLNS, __IgnoreEls, [_ | _els], - Items) -> - decode_unblock_els(__TopXMLNS, __IgnoreEls, _els, - Items). - -encode_unblock({unblock, Items}, _xmlns_attrs) -> - _els = lists:reverse('encode_unblock_$items'(Items, - [])), - _attrs = _xmlns_attrs, - {xmlel, <<"unblock">>, _attrs, _els}. - -'encode_unblock_$items'([], _acc) -> _acc; -'encode_unblock_$items'([Items | _els], _acc) -> - 'encode_unblock_$items'(_els, - [encode_block_item(Items, []) | _acc]). - -decode_block(__TopXMLNS, __IgnoreEls, - {xmlel, <<"block">>, _attrs, _els}) -> - Items = decode_block_els(__TopXMLNS, __IgnoreEls, _els, - []), - {block, Items}. - -decode_block_els(__TopXMLNS, __IgnoreEls, [], Items) -> - lists:reverse(Items); -decode_block_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"item">>, _attrs, _} = _el | _els], Items) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_block_els(__TopXMLNS, __IgnoreEls, _els, - case decode_block_item(__TopXMLNS, __IgnoreEls, _el) - of - undefined -> Items; - _new_el -> [_new_el | Items] - end); - true -> - decode_block_els(__TopXMLNS, __IgnoreEls, _els, Items) - end; -decode_block_els(__TopXMLNS, __IgnoreEls, [_ | _els], - Items) -> - decode_block_els(__TopXMLNS, __IgnoreEls, _els, Items). - -encode_block({block, Items}, _xmlns_attrs) -> - _els = lists:reverse('encode_block_$items'(Items, [])), - _attrs = _xmlns_attrs, - {xmlel, <<"block">>, _attrs, _els}. - -'encode_block_$items'([], _acc) -> _acc; -'encode_block_$items'([Items | _els], _acc) -> - 'encode_block_$items'(_els, - [encode_block_item(Items, []) | _acc]). - -decode_block_item(__TopXMLNS, __IgnoreEls, - {xmlel, <<"item">>, _attrs, _els}) -> - Jid = decode_block_item_attrs(__TopXMLNS, _attrs, - undefined), - Jid. - -decode_block_item_attrs(__TopXMLNS, - [{<<"jid">>, _val} | _attrs], _Jid) -> - decode_block_item_attrs(__TopXMLNS, _attrs, _val); -decode_block_item_attrs(__TopXMLNS, [_ | _attrs], - Jid) -> - decode_block_item_attrs(__TopXMLNS, _attrs, Jid); -decode_block_item_attrs(__TopXMLNS, [], Jid) -> - decode_block_item_attr_jid(__TopXMLNS, Jid). - -encode_block_item(Jid, _xmlns_attrs) -> - _els = [], - _attrs = encode_block_item_attr_jid(Jid, _xmlns_attrs), - {xmlel, <<"item">>, _attrs, _els}. - -decode_block_item_attr_jid(__TopXMLNS, undefined) -> - erlang:error({xmpp_codec, - {missing_attr, <<"jid">>, <<"item">>, __TopXMLNS}}); -decode_block_item_attr_jid(__TopXMLNS, _val) -> - case catch dec_jid(_val) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"jid">>, <<"item">>, __TopXMLNS}}); - _res -> _res - end. - -encode_block_item_attr_jid(_val, _acc) -> - [{<<"jid">>, enc_jid(_val)} | _acc]. - -decode_privacy(__TopXMLNS, __IgnoreEls, - {xmlel, <<"query">>, _attrs, _els}) -> - {Lists, Default, Active} = - decode_privacy_els(__TopXMLNS, __IgnoreEls, _els, [], - undefined, undefined), - {privacy, Lists, Default, Active}. - -decode_privacy_els(__TopXMLNS, __IgnoreEls, [], Lists, - Default, Active) -> - {lists:reverse(Lists), Default, Active}; -decode_privacy_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"list">>, _attrs, _} = _el | _els], Lists, - Default, Active) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_privacy_els(__TopXMLNS, __IgnoreEls, _els, - [decode_privacy_list(__TopXMLNS, __IgnoreEls, _el) - | Lists], - Default, Active); - true -> - decode_privacy_els(__TopXMLNS, __IgnoreEls, _els, Lists, - Default, Active) - end; -decode_privacy_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"default">>, _attrs, _} = _el | _els], Lists, - Default, Active) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_privacy_els(__TopXMLNS, __IgnoreEls, _els, Lists, - decode_privacy_default_list(__TopXMLNS, - __IgnoreEls, _el), - Active); - true -> - decode_privacy_els(__TopXMLNS, __IgnoreEls, _els, Lists, - Default, Active) - end; -decode_privacy_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"active">>, _attrs, _} = _el | _els], Lists, - Default, Active) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_privacy_els(__TopXMLNS, __IgnoreEls, _els, Lists, - Default, - decode_privacy_active_list(__TopXMLNS, - __IgnoreEls, _el)); - true -> - decode_privacy_els(__TopXMLNS, __IgnoreEls, _els, Lists, - Default, Active) - end; -decode_privacy_els(__TopXMLNS, __IgnoreEls, [_ | _els], - Lists, Default, Active) -> - decode_privacy_els(__TopXMLNS, __IgnoreEls, _els, Lists, - Default, Active). - -encode_privacy({privacy, Lists, Default, Active}, - _xmlns_attrs) -> - _els = lists:reverse('encode_privacy_$lists'(Lists, - 'encode_privacy_$default'(Default, - 'encode_privacy_$active'(Active, - [])))), - _attrs = _xmlns_attrs, - {xmlel, <<"query">>, _attrs, _els}. - -'encode_privacy_$lists'([], _acc) -> _acc; -'encode_privacy_$lists'([Lists | _els], _acc) -> - 'encode_privacy_$lists'(_els, - [encode_privacy_list(Lists, []) | _acc]). - -'encode_privacy_$default'(undefined, _acc) -> _acc; -'encode_privacy_$default'(Default, _acc) -> - [encode_privacy_default_list(Default, []) | _acc]. - -'encode_privacy_$active'(undefined, _acc) -> _acc; -'encode_privacy_$active'(Active, _acc) -> - [encode_privacy_active_list(Active, []) | _acc]. - -decode_privacy_active_list(__TopXMLNS, __IgnoreEls, - {xmlel, <<"active">>, _attrs, _els}) -> - Name = decode_privacy_active_list_attrs(__TopXMLNS, - _attrs, undefined), - Name. - -decode_privacy_active_list_attrs(__TopXMLNS, - [{<<"name">>, _val} | _attrs], _Name) -> - decode_privacy_active_list_attrs(__TopXMLNS, _attrs, - _val); -decode_privacy_active_list_attrs(__TopXMLNS, - [_ | _attrs], Name) -> - decode_privacy_active_list_attrs(__TopXMLNS, _attrs, - Name); -decode_privacy_active_list_attrs(__TopXMLNS, [], - Name) -> - decode_privacy_active_list_attr_name(__TopXMLNS, Name). - -encode_privacy_active_list(Name, _xmlns_attrs) -> - _els = [], - _attrs = encode_privacy_active_list_attr_name(Name, - _xmlns_attrs), - {xmlel, <<"active">>, _attrs, _els}. - -decode_privacy_active_list_attr_name(__TopXMLNS, - undefined) -> - none; -decode_privacy_active_list_attr_name(__TopXMLNS, - _val) -> - _val. - -encode_privacy_active_list_attr_name(none, _acc) -> - _acc; -encode_privacy_active_list_attr_name(_val, _acc) -> - [{<<"name">>, _val} | _acc]. - -decode_privacy_default_list(__TopXMLNS, __IgnoreEls, - {xmlel, <<"default">>, _attrs, _els}) -> - Name = decode_privacy_default_list_attrs(__TopXMLNS, - _attrs, undefined), - Name. - -decode_privacy_default_list_attrs(__TopXMLNS, - [{<<"name">>, _val} | _attrs], _Name) -> - decode_privacy_default_list_attrs(__TopXMLNS, _attrs, - _val); -decode_privacy_default_list_attrs(__TopXMLNS, - [_ | _attrs], Name) -> - decode_privacy_default_list_attrs(__TopXMLNS, _attrs, - Name); -decode_privacy_default_list_attrs(__TopXMLNS, [], - Name) -> - decode_privacy_default_list_attr_name(__TopXMLNS, Name). - -encode_privacy_default_list(Name, _xmlns_attrs) -> - _els = [], - _attrs = encode_privacy_default_list_attr_name(Name, - _xmlns_attrs), - {xmlel, <<"default">>, _attrs, _els}. - -decode_privacy_default_list_attr_name(__TopXMLNS, - undefined) -> - none; -decode_privacy_default_list_attr_name(__TopXMLNS, - _val) -> - _val. - -encode_privacy_default_list_attr_name(none, _acc) -> - _acc; -encode_privacy_default_list_attr_name(_val, _acc) -> - [{<<"name">>, _val} | _acc]. - -decode_privacy_list(__TopXMLNS, __IgnoreEls, - {xmlel, <<"list">>, _attrs, _els}) -> - Items = decode_privacy_list_els(__TopXMLNS, __IgnoreEls, - _els, []), - Name = decode_privacy_list_attrs(__TopXMLNS, _attrs, - undefined), - {privacy_list, Name, Items}. - -decode_privacy_list_els(__TopXMLNS, __IgnoreEls, [], - Items) -> - lists:reverse(Items); -decode_privacy_list_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"item">>, _attrs, _} = _el | _els], Items) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_privacy_list_els(__TopXMLNS, __IgnoreEls, _els, - [decode_privacy_item(__TopXMLNS, __IgnoreEls, - _el) - | Items]); - true -> - decode_privacy_list_els(__TopXMLNS, __IgnoreEls, _els, - Items) - end; -decode_privacy_list_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Items) -> - decode_privacy_list_els(__TopXMLNS, __IgnoreEls, _els, - Items). - -decode_privacy_list_attrs(__TopXMLNS, - [{<<"name">>, _val} | _attrs], _Name) -> - decode_privacy_list_attrs(__TopXMLNS, _attrs, _val); -decode_privacy_list_attrs(__TopXMLNS, [_ | _attrs], - Name) -> - decode_privacy_list_attrs(__TopXMLNS, _attrs, Name); -decode_privacy_list_attrs(__TopXMLNS, [], Name) -> - decode_privacy_list_attr_name(__TopXMLNS, Name). - -encode_privacy_list({privacy_list, Name, Items}, - _xmlns_attrs) -> - _els = lists:reverse('encode_privacy_list_$items'(Items, - [])), - _attrs = encode_privacy_list_attr_name(Name, - _xmlns_attrs), - {xmlel, <<"list">>, _attrs, _els}. - -'encode_privacy_list_$items'([], _acc) -> _acc; -'encode_privacy_list_$items'([Items | _els], _acc) -> - 'encode_privacy_list_$items'(_els, - [encode_privacy_item(Items, []) | _acc]). - -decode_privacy_list_attr_name(__TopXMLNS, undefined) -> - erlang:error({xmpp_codec, - {missing_attr, <<"name">>, <<"list">>, __TopXMLNS}}); -decode_privacy_list_attr_name(__TopXMLNS, _val) -> _val. - -encode_privacy_list_attr_name(_val, _acc) -> - [{<<"name">>, _val} | _acc]. - -decode_privacy_item(__TopXMLNS, __IgnoreEls, - {xmlel, <<"item">>, _attrs, _els}) -> - Kinds = decode_privacy_item_els(__TopXMLNS, __IgnoreEls, - _els, []), - {Action, Order, Type, Value} = - decode_privacy_item_attrs(__TopXMLNS, _attrs, undefined, - undefined, undefined, undefined), - {privacy_item, Order, Action, Type, Value, Kinds}. - -decode_privacy_item_els(__TopXMLNS, __IgnoreEls, [], - Kinds) -> - lists:reverse(Kinds); -decode_privacy_item_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"message">>, _attrs, _} = _el | _els], - Kinds) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_privacy_item_els(__TopXMLNS, __IgnoreEls, _els, - Kinds); - true -> - decode_privacy_item_els(__TopXMLNS, __IgnoreEls, _els, - Kinds) - end; -decode_privacy_item_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"iq">>, _attrs, _} = _el | _els], Kinds) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_privacy_item_els(__TopXMLNS, __IgnoreEls, _els, - Kinds); - true -> - decode_privacy_item_els(__TopXMLNS, __IgnoreEls, _els, - Kinds) - end; -decode_privacy_item_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"presence-in">>, _attrs, _} = _el | _els], - Kinds) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_privacy_item_els(__TopXMLNS, __IgnoreEls, _els, - Kinds); - true -> - decode_privacy_item_els(__TopXMLNS, __IgnoreEls, _els, - Kinds) - end; -decode_privacy_item_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"presence-out">>, _attrs, _} = _el | _els], - Kinds) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_privacy_item_els(__TopXMLNS, __IgnoreEls, _els, - Kinds); - true -> - decode_privacy_item_els(__TopXMLNS, __IgnoreEls, _els, - Kinds) - end; -decode_privacy_item_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Kinds) -> - decode_privacy_item_els(__TopXMLNS, __IgnoreEls, _els, - Kinds). - -decode_privacy_item_attrs(__TopXMLNS, - [{<<"action">>, _val} | _attrs], _Action, Order, Type, - Value) -> - decode_privacy_item_attrs(__TopXMLNS, _attrs, _val, - Order, Type, Value); -decode_privacy_item_attrs(__TopXMLNS, - [{<<"order">>, _val} | _attrs], Action, _Order, Type, - Value) -> - decode_privacy_item_attrs(__TopXMLNS, _attrs, Action, - _val, Type, Value); -decode_privacy_item_attrs(__TopXMLNS, - [{<<"type">>, _val} | _attrs], Action, Order, _Type, - Value) -> - decode_privacy_item_attrs(__TopXMLNS, _attrs, Action, - Order, _val, Value); -decode_privacy_item_attrs(__TopXMLNS, - [{<<"value">>, _val} | _attrs], Action, Order, Type, - _Value) -> - decode_privacy_item_attrs(__TopXMLNS, _attrs, Action, - Order, Type, _val); -decode_privacy_item_attrs(__TopXMLNS, [_ | _attrs], - Action, Order, Type, Value) -> - decode_privacy_item_attrs(__TopXMLNS, _attrs, Action, - Order, Type, Value); -decode_privacy_item_attrs(__TopXMLNS, [], Action, Order, - Type, Value) -> - {decode_privacy_item_attr_action(__TopXMLNS, Action), - decode_privacy_item_attr_order(__TopXMLNS, Order), - decode_privacy_item_attr_type(__TopXMLNS, Type), - decode_privacy_item_attr_value(__TopXMLNS, Value)}. - -encode_privacy_item({privacy_item, Order, Action, Type, - Value, Kinds}, - _xmlns_attrs) -> - _els = lists:reverse('encode_privacy_item_$kinds'(Kinds, - [])), - _attrs = encode_privacy_item_attr_value(Value, - encode_privacy_item_attr_type(Type, - encode_privacy_item_attr_order(Order, - encode_privacy_item_attr_action(Action, - _xmlns_attrs)))), - {xmlel, <<"item">>, _attrs, _els}. - -'encode_privacy_item_$kinds'([], _acc) -> _acc; -'encode_privacy_item_$kinds'([message = Kinds | _els], - _acc) -> - 'encode_privacy_item_$kinds'(_els, - [encode_privacy_message(Kinds, []) | _acc]); -'encode_privacy_item_$kinds'([iq = Kinds | _els], - _acc) -> - 'encode_privacy_item_$kinds'(_els, - [encode_privacy_iq(Kinds, []) | _acc]); -'encode_privacy_item_$kinds'(['presence-in' = Kinds - | _els], - _acc) -> - 'encode_privacy_item_$kinds'(_els, - [encode_privacy_presence_in(Kinds, []) - | _acc]); -'encode_privacy_item_$kinds'(['presence-out' = Kinds - | _els], - _acc) -> - 'encode_privacy_item_$kinds'(_els, - [encode_privacy_presence_out(Kinds, []) - | _acc]). - -decode_privacy_item_attr_action(__TopXMLNS, - undefined) -> - erlang:error({xmpp_codec, - {missing_attr, <<"action">>, <<"item">>, __TopXMLNS}}); -decode_privacy_item_attr_action(__TopXMLNS, _val) -> - case catch dec_enum(_val, [allow, deny]) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"action">>, <<"item">>, - __TopXMLNS}}); - _res -> _res - end. - -encode_privacy_item_attr_action(_val, _acc) -> - [{<<"action">>, enc_enum(_val)} | _acc]. - -decode_privacy_item_attr_order(__TopXMLNS, undefined) -> - erlang:error({xmpp_codec, - {missing_attr, <<"order">>, <<"item">>, __TopXMLNS}}); -decode_privacy_item_attr_order(__TopXMLNS, _val) -> - case catch dec_int(_val, 0, infinity) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"order">>, <<"item">>, __TopXMLNS}}); - _res -> _res - end. - -encode_privacy_item_attr_order(_val, _acc) -> - [{<<"order">>, enc_int(_val)} | _acc]. - -decode_privacy_item_attr_type(__TopXMLNS, undefined) -> - undefined; -decode_privacy_item_attr_type(__TopXMLNS, _val) -> - case catch dec_enum(_val, [group, jid, subscription]) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"type">>, <<"item">>, __TopXMLNS}}); - _res -> _res - end. - -encode_privacy_item_attr_type(undefined, _acc) -> _acc; -encode_privacy_item_attr_type(_val, _acc) -> - [{<<"type">>, enc_enum(_val)} | _acc]. - -decode_privacy_item_attr_value(__TopXMLNS, undefined) -> - undefined; -decode_privacy_item_attr_value(__TopXMLNS, _val) -> - _val. - -encode_privacy_item_attr_value(undefined, _acc) -> _acc; -encode_privacy_item_attr_value(_val, _acc) -> - [{<<"value">>, _val} | _acc]. - -decode_privacy_presence_out(__TopXMLNS, __IgnoreEls, - {xmlel, <<"presence-out">>, _attrs, _els}) -> - 'presence-out'. - -encode_privacy_presence_out('presence-out', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"presence-out">>, _attrs, _els}. - -decode_privacy_presence_in(__TopXMLNS, __IgnoreEls, - {xmlel, <<"presence-in">>, _attrs, _els}) -> - 'presence-in'. - -encode_privacy_presence_in('presence-in', - _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"presence-in">>, _attrs, _els}. - -decode_privacy_iq(__TopXMLNS, __IgnoreEls, - {xmlel, <<"iq">>, _attrs, _els}) -> - iq. - -encode_privacy_iq(iq, _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"iq">>, _attrs, _els}. - -decode_privacy_message(__TopXMLNS, __IgnoreEls, - {xmlel, <<"message">>, _attrs, _els}) -> - message. - -encode_privacy_message(message, _xmlns_attrs) -> - _els = [], - _attrs = _xmlns_attrs, - {xmlel, <<"message">>, _attrs, _els}. - -decode_roster(__TopXMLNS, __IgnoreEls, - {xmlel, <<"query">>, _attrs, _els}) -> - Items = decode_roster_els(__TopXMLNS, __IgnoreEls, _els, - []), - Ver = decode_roster_attrs(__TopXMLNS, _attrs, - undefined), - {roster, Items, Ver}. - -decode_roster_els(__TopXMLNS, __IgnoreEls, [], Items) -> - lists:reverse(Items); -decode_roster_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"item">>, _attrs, _} = _el | _els], Items) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_roster_els(__TopXMLNS, __IgnoreEls, _els, - [decode_roster_item(__TopXMLNS, __IgnoreEls, _el) - | Items]); - true -> - decode_roster_els(__TopXMLNS, __IgnoreEls, _els, Items) - end; -decode_roster_els(__TopXMLNS, __IgnoreEls, [_ | _els], - Items) -> - decode_roster_els(__TopXMLNS, __IgnoreEls, _els, Items). - -decode_roster_attrs(__TopXMLNS, - [{<<"ver">>, _val} | _attrs], _Ver) -> - decode_roster_attrs(__TopXMLNS, _attrs, _val); -decode_roster_attrs(__TopXMLNS, [_ | _attrs], Ver) -> - decode_roster_attrs(__TopXMLNS, _attrs, Ver); -decode_roster_attrs(__TopXMLNS, [], Ver) -> - decode_roster_attr_ver(__TopXMLNS, Ver). - -encode_roster({roster, Items, Ver}, _xmlns_attrs) -> - _els = lists:reverse('encode_roster_$items'(Items, [])), - _attrs = encode_roster_attr_ver(Ver, _xmlns_attrs), - {xmlel, <<"query">>, _attrs, _els}. - -'encode_roster_$items'([], _acc) -> _acc; -'encode_roster_$items'([Items | _els], _acc) -> - 'encode_roster_$items'(_els, - [encode_roster_item(Items, []) | _acc]). - -decode_roster_attr_ver(__TopXMLNS, undefined) -> - undefined; -decode_roster_attr_ver(__TopXMLNS, _val) -> _val. - -encode_roster_attr_ver(undefined, _acc) -> _acc; -encode_roster_attr_ver(_val, _acc) -> - [{<<"ver">>, _val} | _acc]. - -decode_roster_item(__TopXMLNS, __IgnoreEls, - {xmlel, <<"item">>, _attrs, _els}) -> - Groups = decode_roster_item_els(__TopXMLNS, __IgnoreEls, - _els, []), - {Jid, Name, Subscription, Ask} = - decode_roster_item_attrs(__TopXMLNS, _attrs, undefined, - undefined, undefined, undefined), - {roster_item, Jid, Name, Groups, Subscription, Ask}. - -decode_roster_item_els(__TopXMLNS, __IgnoreEls, [], - Groups) -> - lists:reverse(Groups); -decode_roster_item_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"group">>, _attrs, _} = _el | _els], - Groups) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_roster_item_els(__TopXMLNS, __IgnoreEls, _els, - [decode_roster_group(__TopXMLNS, __IgnoreEls, - _el) - | Groups]); - true -> - decode_roster_item_els(__TopXMLNS, __IgnoreEls, _els, - Groups) - end; -decode_roster_item_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Groups) -> - decode_roster_item_els(__TopXMLNS, __IgnoreEls, _els, - Groups). - -decode_roster_item_attrs(__TopXMLNS, - [{<<"jid">>, _val} | _attrs], _Jid, Name, Subscription, - Ask) -> - decode_roster_item_attrs(__TopXMLNS, _attrs, _val, Name, - Subscription, Ask); -decode_roster_item_attrs(__TopXMLNS, - [{<<"name">>, _val} | _attrs], Jid, _Name, - Subscription, Ask) -> - decode_roster_item_attrs(__TopXMLNS, _attrs, Jid, _val, - Subscription, Ask); -decode_roster_item_attrs(__TopXMLNS, - [{<<"subscription">>, _val} | _attrs], Jid, Name, - _Subscription, Ask) -> - decode_roster_item_attrs(__TopXMLNS, _attrs, Jid, Name, - _val, Ask); -decode_roster_item_attrs(__TopXMLNS, - [{<<"ask">>, _val} | _attrs], Jid, Name, Subscription, - _Ask) -> - decode_roster_item_attrs(__TopXMLNS, _attrs, Jid, Name, - Subscription, _val); -decode_roster_item_attrs(__TopXMLNS, [_ | _attrs], Jid, - Name, Subscription, Ask) -> - decode_roster_item_attrs(__TopXMLNS, _attrs, Jid, Name, - Subscription, Ask); -decode_roster_item_attrs(__TopXMLNS, [], Jid, Name, - Subscription, Ask) -> - {decode_roster_item_attr_jid(__TopXMLNS, Jid), - decode_roster_item_attr_name(__TopXMLNS, Name), - decode_roster_item_attr_subscription(__TopXMLNS, - Subscription), - decode_roster_item_attr_ask(__TopXMLNS, Ask)}. - -encode_roster_item({roster_item, Jid, Name, Groups, - Subscription, Ask}, - _xmlns_attrs) -> - _els = - lists:reverse('encode_roster_item_$groups'(Groups, [])), - _attrs = encode_roster_item_attr_ask(Ask, - encode_roster_item_attr_subscription(Subscription, - encode_roster_item_attr_name(Name, - encode_roster_item_attr_jid(Jid, - _xmlns_attrs)))), - {xmlel, <<"item">>, _attrs, _els}. - -'encode_roster_item_$groups'([], _acc) -> _acc; -'encode_roster_item_$groups'([Groups | _els], _acc) -> - 'encode_roster_item_$groups'(_els, - [encode_roster_group(Groups, []) | _acc]). - -decode_roster_item_attr_jid(__TopXMLNS, undefined) -> - erlang:error({xmpp_codec, - {missing_attr, <<"jid">>, <<"item">>, __TopXMLNS}}); -decode_roster_item_attr_jid(__TopXMLNS, _val) -> - case catch dec_jid(_val) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"jid">>, <<"item">>, __TopXMLNS}}); - _res -> _res - end. - -encode_roster_item_attr_jid(_val, _acc) -> - [{<<"jid">>, enc_jid(_val)} | _acc]. - -decode_roster_item_attr_name(__TopXMLNS, undefined) -> - undefined; -decode_roster_item_attr_name(__TopXMLNS, _val) -> _val. - -encode_roster_item_attr_name(undefined, _acc) -> _acc; -encode_roster_item_attr_name(_val, _acc) -> - [{<<"name">>, _val} | _acc]. - -decode_roster_item_attr_subscription(__TopXMLNS, - undefined) -> - none; -decode_roster_item_attr_subscription(__TopXMLNS, - _val) -> - case catch dec_enum(_val, - [none, to, from, both, remove]) - of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"subscription">>, <<"item">>, - __TopXMLNS}}); - _res -> _res - end. - -encode_roster_item_attr_subscription(none, _acc) -> - _acc; -encode_roster_item_attr_subscription(_val, _acc) -> - [{<<"subscription">>, enc_enum(_val)} | _acc]. - -decode_roster_item_attr_ask(__TopXMLNS, undefined) -> - undefined; -decode_roster_item_attr_ask(__TopXMLNS, _val) -> - case catch dec_enum(_val, [subscribe]) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"ask">>, <<"item">>, __TopXMLNS}}); - _res -> _res - end. - -encode_roster_item_attr_ask(undefined, _acc) -> _acc; -encode_roster_item_attr_ask(_val, _acc) -> - [{<<"ask">>, enc_enum(_val)} | _acc]. - -decode_roster_group(__TopXMLNS, __IgnoreEls, - {xmlel, <<"group">>, _attrs, _els}) -> - Cdata = decode_roster_group_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - Cdata. - -decode_roster_group_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_roster_group_cdata(__TopXMLNS, Cdata); -decode_roster_group_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_roster_group_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_roster_group_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_roster_group_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_roster_group(Cdata, _xmlns_attrs) -> - _els = encode_roster_group_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"group">>, _attrs, _els}. - -decode_roster_group_cdata(__TopXMLNS, <<>>) -> - erlang:error({xmpp_codec, - {missing_cdata, <<>>, <<"group">>, __TopXMLNS}}); -decode_roster_group_cdata(__TopXMLNS, _val) -> _val. - -encode_roster_group_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_version(__TopXMLNS, __IgnoreEls, - {xmlel, <<"query">>, _attrs, _els}) -> - {Ver, Os, Name} = decode_version_els(__TopXMLNS, - __IgnoreEls, _els, undefined, - undefined, undefined), - {version, Name, Ver, Os}. - -decode_version_els(__TopXMLNS, __IgnoreEls, [], Ver, Os, - Name) -> - {Ver, Os, Name}; -decode_version_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"name">>, _attrs, _} = _el | _els], Ver, Os, - Name) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_version_els(__TopXMLNS, __IgnoreEls, _els, Ver, - Os, - decode_version_name(__TopXMLNS, __IgnoreEls, - _el)); - true -> - decode_version_els(__TopXMLNS, __IgnoreEls, _els, Ver, - Os, Name) - end; -decode_version_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"version">>, _attrs, _} = _el | _els], Ver, - Os, Name) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_version_els(__TopXMLNS, __IgnoreEls, _els, - decode_version_ver(__TopXMLNS, __IgnoreEls, _el), - Os, Name); - true -> - decode_version_els(__TopXMLNS, __IgnoreEls, _els, Ver, - Os, Name) - end; -decode_version_els(__TopXMLNS, __IgnoreEls, - [{xmlel, <<"os">>, _attrs, _} = _el | _els], Ver, Os, - Name) -> - _xmlns = get_attr(<<"xmlns">>, _attrs), - if _xmlns == <<>>; _xmlns == __TopXMLNS -> - decode_version_els(__TopXMLNS, __IgnoreEls, _els, Ver, - decode_version_os(__TopXMLNS, __IgnoreEls, _el), - Name); - true -> - decode_version_els(__TopXMLNS, __IgnoreEls, _els, Ver, - Os, Name) - end; -decode_version_els(__TopXMLNS, __IgnoreEls, [_ | _els], - Ver, Os, Name) -> - decode_version_els(__TopXMLNS, __IgnoreEls, _els, Ver, - Os, Name). - -encode_version({version, Name, Ver, Os}, - _xmlns_attrs) -> - _els = lists:reverse('encode_version_$ver'(Ver, - 'encode_version_$os'(Os, - 'encode_version_$name'(Name, - [])))), - _attrs = _xmlns_attrs, - {xmlel, <<"query">>, _attrs, _els}. - -'encode_version_$ver'(undefined, _acc) -> _acc; -'encode_version_$ver'(Ver, _acc) -> - [encode_version_ver(Ver, []) | _acc]. - -'encode_version_$os'(undefined, _acc) -> _acc; -'encode_version_$os'(Os, _acc) -> - [encode_version_os(Os, []) | _acc]. - -'encode_version_$name'(undefined, _acc) -> _acc; -'encode_version_$name'(Name, _acc) -> - [encode_version_name(Name, []) | _acc]. - -decode_version_os(__TopXMLNS, __IgnoreEls, - {xmlel, <<"os">>, _attrs, _els}) -> - Cdata = decode_version_os_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - Cdata. - -decode_version_os_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_version_os_cdata(__TopXMLNS, Cdata); -decode_version_os_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_version_os_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_version_os_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_version_os_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_version_os(Cdata, _xmlns_attrs) -> - _els = encode_version_os_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"os">>, _attrs, _els}. - -decode_version_os_cdata(__TopXMLNS, <<>>) -> - erlang:error({xmpp_codec, - {missing_cdata, <<>>, <<"os">>, __TopXMLNS}}); -decode_version_os_cdata(__TopXMLNS, _val) -> _val. - -encode_version_os_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_version_ver(__TopXMLNS, __IgnoreEls, - {xmlel, <<"version">>, _attrs, _els}) -> - Cdata = decode_version_ver_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - Cdata. - -decode_version_ver_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_version_ver_cdata(__TopXMLNS, Cdata); -decode_version_ver_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_version_ver_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_version_ver_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_version_ver_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_version_ver(Cdata, _xmlns_attrs) -> - _els = encode_version_ver_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"version">>, _attrs, _els}. - -decode_version_ver_cdata(__TopXMLNS, <<>>) -> - erlang:error({xmpp_codec, - {missing_cdata, <<>>, <<"version">>, __TopXMLNS}}); -decode_version_ver_cdata(__TopXMLNS, _val) -> _val. - -encode_version_ver_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_version_name(__TopXMLNS, __IgnoreEls, - {xmlel, <<"name">>, _attrs, _els}) -> - Cdata = decode_version_name_els(__TopXMLNS, __IgnoreEls, - _els, <<>>), - Cdata. - -decode_version_name_els(__TopXMLNS, __IgnoreEls, [], - Cdata) -> - decode_version_name_cdata(__TopXMLNS, Cdata); -decode_version_name_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Cdata) -> - decode_version_name_els(__TopXMLNS, __IgnoreEls, _els, - <<Cdata/binary, _data/binary>>); -decode_version_name_els(__TopXMLNS, __IgnoreEls, - [_ | _els], Cdata) -> - decode_version_name_els(__TopXMLNS, __IgnoreEls, _els, - Cdata). - -encode_version_name(Cdata, _xmlns_attrs) -> - _els = encode_version_name_cdata(Cdata, []), - _attrs = _xmlns_attrs, - {xmlel, <<"name">>, _attrs, _els}. - -decode_version_name_cdata(__TopXMLNS, <<>>) -> - erlang:error({xmpp_codec, - {missing_cdata, <<>>, <<"name">>, __TopXMLNS}}); -decode_version_name_cdata(__TopXMLNS, _val) -> _val. - -encode_version_name_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. - -decode_last(__TopXMLNS, __IgnoreEls, - {xmlel, <<"query">>, _attrs, _els}) -> - Text = decode_last_els(__TopXMLNS, __IgnoreEls, _els, - <<>>), - Seconds = decode_last_attrs(__TopXMLNS, _attrs, - undefined), - {last, Seconds, Text}. - -decode_last_els(__TopXMLNS, __IgnoreEls, [], Text) -> - decode_last_cdata(__TopXMLNS, Text); -decode_last_els(__TopXMLNS, __IgnoreEls, - [{xmlcdata, _data} | _els], Text) -> - decode_last_els(__TopXMLNS, __IgnoreEls, _els, - <<Text/binary, _data/binary>>); -decode_last_els(__TopXMLNS, __IgnoreEls, [_ | _els], - Text) -> - decode_last_els(__TopXMLNS, __IgnoreEls, _els, Text). - -decode_last_attrs(__TopXMLNS, - [{<<"seconds">>, _val} | _attrs], _Seconds) -> - decode_last_attrs(__TopXMLNS, _attrs, _val); -decode_last_attrs(__TopXMLNS, [_ | _attrs], Seconds) -> - decode_last_attrs(__TopXMLNS, _attrs, Seconds); -decode_last_attrs(__TopXMLNS, [], Seconds) -> - decode_last_attr_seconds(__TopXMLNS, Seconds). - -encode_last({last, Seconds, Text}, _xmlns_attrs) -> - _els = encode_last_cdata(Text, []), - _attrs = encode_last_attr_seconds(Seconds, - _xmlns_attrs), - {xmlel, <<"query">>, _attrs, _els}. - -decode_last_attr_seconds(__TopXMLNS, undefined) -> - undefined; -decode_last_attr_seconds(__TopXMLNS, _val) -> - case catch dec_int(_val, 0, infinity) of - {'EXIT', _} -> - erlang:error({xmpp_codec, - {bad_attr_value, <<"seconds">>, <<"query">>, - __TopXMLNS}}); - _res -> _res - end. - -encode_last_attr_seconds(undefined, _acc) -> _acc; -encode_last_attr_seconds(_val, _acc) -> - [{<<"seconds">>, enc_int(_val)} | _acc]. - -decode_last_cdata(__TopXMLNS, <<>>) -> undefined; -decode_last_cdata(__TopXMLNS, _val) -> _val. - -encode_last_cdata(undefined, _acc) -> _acc; -encode_last_cdata(_val, _acc) -> - [{xmlcdata, _val} | _acc]. diff --git a/tools/xmpp_codec.hrl b/tools/xmpp_codec.hrl deleted file mode 100644 index 4098a7fd6..000000000 --- a/tools/xmpp_codec.hrl +++ /dev/null @@ -1,486 +0,0 @@ -%% Created automatically by XML generator (xml_gen.erl) -%% Source: xmpp_codec.spec - --record(chatstate, {type :: active | composing | gone | inactive | paused}). - --record(csi, {type :: active | inactive}). - --record(feature_register, {}). - --record(sasl_success, {text :: any()}). - --record(text, {lang :: binary(), - data :: binary()}). - --record(streamhost, {jid :: any(), - host :: binary(), - port = 1080 :: non_neg_integer()}). - --record(sm_resume, {h :: non_neg_integer(), - previd :: binary(), - xmlns :: binary()}). - --record(carbons_enable, {}). - --record(carbons_private, {}). - --record(pubsub_unsubscribe, {node :: binary(), - jid :: any(), - subid :: binary()}). - --record(ping, {}). - --record(delay, {stamp :: any(), - from :: any()}). - --record(muc_history, {maxchars :: non_neg_integer(), - maxstanzas :: non_neg_integer(), - seconds :: non_neg_integer(), - since :: any()}). - --record(pubsub_affiliation, {node :: binary(), - type :: 'member' | 'none' | 'outcast' | 'owner' | 'publish-only' | 'publisher'}). - --record(muc_decline, {reason :: binary(), - from :: any(), - to :: any()}). - --record(sm_a, {h :: non_neg_integer(), - xmlns :: binary()}). - --record(starttls_proceed, {}). - --record(sm_resumed, {h :: non_neg_integer(), - previd :: binary(), - xmlns :: binary()}). - --record(forwarded, {delay :: #delay{}, - sub_els = [] :: [any()]}). - --record(sm_enable, {max :: non_neg_integer(), - resume = false :: any(), - xmlns :: binary()}). - --record(starttls_failure, {}). - --record(sasl_challenge, {text :: any()}). - --record(gone, {uri :: binary()}). - --record(private, {xml_els = [] :: [any()]}). - --record(p1_ack, {}). - --record(feature_sm, {xmlns :: binary()}). - --record(pubsub_item, {id :: binary(), - xml_els = [] :: [any()]}). - --record(pubsub_publish, {node :: binary(), - items = [] :: [#pubsub_item{}]}). - --record(roster_item, {jid :: any(), - name :: binary(), - groups = [] :: [binary()], - subscription = none :: 'both' | 'from' | 'none' | 'remove' | 'to', - ask :: 'subscribe'}). - --record(roster, {items = [] :: [#roster_item{}], - ver :: binary()}). - --record(pubsub_event_item, {id :: binary(), - node :: binary(), - publisher :: binary()}). - --record(sm_r, {xmlns :: binary()}). - --record(muc_actor, {jid :: any(), - nick :: binary()}). - --record(stat, {name :: binary(), - units :: binary(), - value :: binary(), - error = [] :: [{integer(),'undefined' | binary()}]}). - --record('see-other-host', {host :: binary()}). - --record(compress, {methods = [] :: [binary()]}). - --record(starttls, {required = false :: boolean()}). - --record(last, {seconds :: non_neg_integer(), - text :: binary()}). - --record(redirect, {uri :: binary()}). - --record(sm_enabled, {id :: binary(), - location :: binary(), - max :: non_neg_integer(), - resume = false :: any(), - xmlns :: binary()}). - --record(pubsub_event_items, {node :: binary(), - retract = [] :: [binary()], - items = [] :: [#pubsub_event_item{}]}). - --record(pubsub_event, {items = [] :: [#pubsub_event_items{}]}). - --record(sasl_response, {text :: any()}). - --record(pubsub_subscribe, {node :: binary(), - jid :: any()}). - --record(sasl_auth, {mechanism :: binary(), - text :: any()}). - --record(p1_push, {}). - --record(feature_csi, {xmlns :: binary()}). - --record(legacy_delay, {stamp :: binary(), - from :: any()}). - --record(muc_user_destroy, {reason :: binary(), - jid :: any()}). - --record(disco_item, {jid :: any(), - name :: binary(), - node :: binary()}). - --record(disco_items, {node :: binary(), - items = [] :: [#disco_item{}]}). - --record(unblock, {items = [] :: [any()]}). - --record(block, {items = [] :: [any()]}). - --record(session, {}). - --record(compression, {methods = [] :: [binary()]}). - --record(muc_owner_destroy, {jid :: any(), - reason :: binary(), - password :: binary()}). - --record(pubsub_subscription, {jid :: any(), - node :: binary(), - subid :: binary(), - type :: 'none' | 'pending' | 'subscribed' | 'unconfigured'}). - --record(muc_item, {actor :: #muc_actor{}, - continue :: binary(), - reason :: binary(), - affiliation :: 'admin' | 'member' | 'none' | 'outcast' | 'owner', - role :: 'moderator' | 'none' | 'participant' | 'visitor', - jid :: any(), - nick :: binary()}). - --record(muc_admin, {items = [] :: [#muc_item{}]}). - --record(shim, {headers = [] :: [{binary(),'undefined' | binary()}]}). - --record(caps, {hash :: binary(), - node :: binary(), - ver :: any()}). - --record(muc, {history :: #muc_history{}, - password :: binary()}). - --record(stream_features, {sub_els = [] :: [any()]}). - --record(stats, {stat = [] :: [#stat{}]}). - --record(pubsub_items, {node :: binary(), - max_items :: non_neg_integer(), - subid :: binary(), - items = [] :: [#pubsub_item{}]}). - --record(carbons_sent, {forwarded :: #forwarded{}}). - --record(p1_rebind, {}). - --record(compress_failure, {reason :: 'processing-failed' | 'setup-failed' | 'unsupported-method'}). - --record(sasl_abort, {}). - --record(vcard_email, {home = false :: boolean(), - work = false :: boolean(), - internet = false :: boolean(), - pref = false :: boolean(), - x400 = false :: boolean(), - userid :: binary()}). - --record(carbons_received, {forwarded :: #forwarded{}}). - --record(pubsub_retract, {node :: binary(), - notify = false :: any(), - items = [] :: [#pubsub_item{}]}). - --record(vcard_geo, {lat :: binary(), - lon :: binary()}). - --record(compressed, {}). - --record(sasl_failure, {reason :: 'aborted' | 'account-disabled' | 'credentials-expired' | 'encryption-required' | 'incorrect-encoding' | 'invalid-authzid' | 'invalid-mechanism' | 'malformed-request' | 'mechanism-too-weak' | 'not-authorized' | 'temporary-auth-failure', - text = [] :: [#text{}]}). - --record(block_list, {}). - --record(xdata_field, {label :: binary(), - type :: 'boolean' | 'fixed' | 'hidden' | 'jid-multi' | 'jid-single' | 'list-multi' | 'list-single' | 'text-multi' | 'text-private' | 'text-single', - var :: binary(), - required = false :: boolean(), - desc :: binary(), - values = [] :: [binary()], - options = [] :: [binary()]}). - --record(version, {name :: binary(), - ver :: binary(), - os :: binary()}). - --record(muc_invite, {reason :: binary(), - from :: any(), - to :: any()}). - --record(bind, {jid :: any(), - resource :: any()}). - --record(muc_user, {decline :: #muc_decline{}, - destroy :: #muc_user_destroy{}, - invites = [] :: [#muc_invite{}], - items = [] :: [#muc_item{}], - status_codes = [] :: [pos_integer()], - password :: binary()}). - --record(vcard_xupdate, {photo :: binary()}). - --record(carbons_disable, {}). - --record(bytestreams, {hosts = [] :: [#streamhost{}], - used :: any(), - activate :: any(), - dstaddr :: binary(), - mode = tcp :: 'tcp' | 'udp', - sid :: binary()}). - --record(vcard_org, {name :: binary(), - units = [] :: [binary()]}). - --record(vcard_tel, {home = false :: boolean(), - work = false :: boolean(), - voice = false :: boolean(), - fax = false :: boolean(), - pager = false :: boolean(), - msg = false :: boolean(), - cell = false :: boolean(), - video = false :: boolean(), - bbs = false :: boolean(), - modem = false :: boolean(), - isdn = false :: boolean(), - pcs = false :: boolean(), - pref = false :: boolean(), - number :: binary()}). - --record(vcard_key, {type :: binary(), - cred :: binary()}). - --record(vcard_name, {family :: binary(), - given :: binary(), - middle :: binary(), - prefix :: binary(), - suffix :: binary()}). - --record(identity, {category :: binary(), - type :: binary(), - lang :: binary(), - name :: binary()}). - --record(bookmark_conference, {name :: binary(), - jid :: any(), - autojoin = false :: any(), - nick :: binary(), - password :: binary()}). - --record(bookmark_url, {name :: binary(), - url :: binary()}). - --record(bookmark_storage, {conference = [] :: [#bookmark_conference{}], - url = [] :: [#bookmark_url{}]}). - --record(vcard_sound, {phonetic :: binary(), - binval :: any(), - extval :: binary()}). - --record(vcard_photo, {type :: binary(), - binval :: any(), - extval :: binary()}). - --record(vcard_label, {home = false :: boolean(), - work = false :: boolean(), - postal = false :: boolean(), - parcel = false :: boolean(), - dom = false :: boolean(), - intl = false :: boolean(), - pref = false :: boolean(), - line = [] :: [binary()]}). - --record(vcard_adr, {home = false :: boolean(), - work = false :: boolean(), - postal = false :: boolean(), - parcel = false :: boolean(), - dom = false :: boolean(), - intl = false :: boolean(), - pref = false :: boolean(), - pobox :: binary(), - extadd :: binary(), - street :: binary(), - locality :: binary(), - region :: binary(), - pcode :: binary(), - ctry :: binary()}). - --record(xdata, {type :: 'cancel' | 'form' | 'result' | 'submit', - instructions = [] :: [binary()], - title :: binary(), - reported :: [#xdata_field{}], - items = [] :: [[#xdata_field{}]], - fields = [] :: [#xdata_field{}]}). - --record(muc_owner, {destroy :: #muc_owner_destroy{}, - config :: #xdata{}}). - --record(pubsub_options, {node :: binary(), - jid :: any(), - subid :: binary(), - xdata :: #xdata{}}). - --record(pubsub, {subscriptions :: {'none' | binary(),[#pubsub_subscription{}]}, - affiliations :: [#pubsub_affiliation{}], - publish :: #pubsub_publish{}, - subscribe :: #pubsub_subscribe{}, - unsubscribe :: #pubsub_unsubscribe{}, - options :: #pubsub_options{}, - items :: #pubsub_items{}, - retract :: #pubsub_retract{}}). - --record(register, {registered = false :: boolean(), - remove = false :: boolean(), - instructions :: binary(), - username :: 'none' | binary(), - nick :: 'none' | binary(), - password :: 'none' | binary(), - name :: 'none' | binary(), - first :: 'none' | binary(), - last :: 'none' | binary(), - email :: 'none' | binary(), - address :: 'none' | binary(), - city :: 'none' | binary(), - state :: 'none' | binary(), - zip :: 'none' | binary(), - phone :: 'none' | binary(), - url :: 'none' | binary(), - date :: 'none' | binary(), - misc :: 'none' | binary(), - text :: 'none' | binary(), - key :: 'none' | binary(), - xdata :: #xdata{}}). - --record(disco_info, {node :: binary(), - identities = [] :: [#identity{}], - features = [] :: [binary()], - xdata = [] :: [#xdata{}]}). - --record(sasl_mechanisms, {list = [] :: [binary()]}). - --record(sm_failed, {reason :: atom() | #gone{} | #redirect{}, - xmlns :: binary()}). - --record(error, {type :: 'auth' | 'cancel' | 'continue' | 'modify' | 'wait', - by :: binary(), - reason :: atom() | #gone{} | #redirect{}, - text :: #text{}}). - --record(presence, {id :: binary(), - type :: 'error' | 'probe' | 'subscribe' | 'subscribed' | 'unavailable' | 'unsubscribe' | 'unsubscribed', - lang :: binary(), - from :: any(), - to :: any(), - show :: 'away' | 'chat' | 'dnd' | 'xa', - status = [] :: [#text{}], - priority :: integer(), - error :: #error{}, - sub_els = [] :: [any()]}). - --record(message, {id :: binary(), - type = normal :: 'chat' | 'error' | 'groupchat' | 'headline' | 'normal', - lang :: binary(), - from :: any(), - to :: any(), - subject = [] :: [#text{}], - body = [] :: [#text{}], - thread :: binary(), - error :: #error{}, - sub_els = [] :: [any()]}). - --record(iq, {id :: binary(), - type :: 'error' | 'get' | 'result' | 'set', - lang :: binary(), - from :: any(), - to :: any(), - error :: #error{}, - sub_els = [] :: [any()]}). - --record(privacy_item, {order :: non_neg_integer(), - action :: 'allow' | 'deny', - type :: 'group' | 'jid' | 'subscription', - value :: binary(), - kinds = [] :: ['iq' | 'message' | 'presence-in' | 'presence-out']}). - --record(privacy_list, {name :: binary(), - items = [] :: [#privacy_item{}]}). - --record(privacy, {lists = [] :: [#privacy_list{}], - default :: 'none' | binary(), - active :: 'none' | binary()}). - --record(stream_error, {reason :: atom() | #'see-other-host'{}, - text :: #text{}}). - --record(vcard_logo, {type :: binary(), - binval :: any(), - extval :: binary()}). - --record(vcard, {version :: binary(), - fn :: binary(), - n :: #vcard_name{}, - nickname :: binary(), - photo :: #vcard_photo{}, - bday :: binary(), - adr = [] :: [#vcard_adr{}], - label = [] :: [#vcard_label{}], - tel = [] :: [#vcard_tel{}], - email = [] :: [#vcard_email{}], - jabberid :: binary(), - mailer :: binary(), - tz :: binary(), - geo :: #vcard_geo{}, - title :: binary(), - role :: binary(), - logo :: #vcard_logo{}, - org :: #vcard_org{}, - categories = [] :: [binary()], - note :: binary(), - prodid :: binary(), - rev :: binary(), - sort_string :: binary(), - sound :: #vcard_sound{}, - uid :: binary(), - url :: binary(), - class :: 'confidential' | 'private' | 'public', - key :: #vcard_key{}, - desc :: binary()}). - --record(time, {tzo :: any(), - utc :: any()}). - - diff --git a/tools/xmpp_codec.spec b/tools/xmpp_codec.spec deleted file mode 100644 index 61f438cbe..000000000 --- a/tools/xmpp_codec.spec +++ /dev/null @@ -1,2306 +0,0 @@ --xml(last, - #elem{name = <<"query">>, - xmlns = <<"jabber:iq:last">>, - result = {last, '$seconds', '$text'}, - attrs = [#attr{name = <<"seconds">>, - enc = {enc_int, []}, - dec = {dec_int, [0, infinity]}}], - cdata = #cdata{label = '$text'}}). - --xml(version_name, - #elem{name = <<"name">>, - xmlns = <<"jabber:iq:version">>, - result = '$cdata', - cdata = #cdata{label = '$cdata', required = true}}). - --xml(version_ver, - #elem{name = <<"version">>, - xmlns = <<"jabber:iq:version">>, - result = '$cdata', - cdata = #cdata{label = '$cdata', required = true}}). - --xml(version_os, - #elem{name = <<"os">>, - xmlns = <<"jabber:iq:version">>, - result = '$cdata', - cdata = #cdata{label = '$cdata', required = true}}). - --xml(version, - #elem{name = <<"query">>, - xmlns = <<"jabber:iq:version">>, - result = {version, '$name', '$ver', '$os'}, - refs = [#ref{name = version_name, - label = '$name', - min = 0, max = 1}, - #ref{name = version_ver, - label = '$ver', - min = 0, max = 1}, - #ref{name = version_os, - label = '$os', - min = 0, max = 1}]}). - --xml(roster_group, - #elem{name = <<"group">>, - xmlns = <<"jabber:iq:roster">>, - result = '$cdata', - cdata = #cdata{required = true, label = '$cdata'}}). - --xml(roster_item, - #elem{name = <<"item">>, - xmlns = <<"jabber:iq:roster">>, - result = {roster_item, '$jid', '$name', - '$groups', '$subscription', '$ask'}, - attrs = [#attr{name = <<"jid">>, - required = true, - dec = {dec_jid, []}, - enc = {enc_jid, []}}, - #attr{name = <<"name">>}, - #attr{name = <<"subscription">>, - default = none, - enc = {enc_enum, []}, - dec = {dec_enum, [[none,to,from,both,remove]]}}, - #attr{name = <<"ask">>, - enc = {enc_enum, []}, - dec = {dec_enum, [[subscribe]]}}], - refs = [#ref{name = roster_group, label = '$groups'}]}). - --xml(roster, - #elem{name = <<"query">>, - xmlns = <<"jabber:iq:roster">>, - result = {roster, '$items', '$ver'}, - attrs = [#attr{name = <<"ver">>}], - refs = [#ref{name = roster_item, label = '$items'}]}). - --xml(privacy_message, #elem{name = <<"message">>, xmlns = <<"jabber:iq:privacy">>, - result = message}). --xml(privacy_iq, #elem{name = <<"iq">>, xmlns = <<"jabber:iq:privacy">>, - result = iq}). --xml(privacy_presence_in, #elem{name = <<"presence-in">>, - xmlns = <<"jabber:iq:privacy">>, - result = 'presence-in'}). --xml(privacy_presence_out, #elem{name = <<"presence-out">>, - xmlns = <<"jabber:iq:privacy">>, - result = 'presence-out'}). - --xml(privacy_item, - #elem{name = <<"item">>, - xmlns = <<"jabber:iq:privacy">>, - result = {privacy_item, '$order', '$action', '$type', - '$value', '$kinds'}, - attrs = [#attr{name = <<"action">>, - required = true, - dec = {dec_enum, [[allow, deny]]}, - enc = {enc_enum, []}}, - #attr{name = <<"order">>, - required = true, - dec = {dec_int, [0, infinity]}, - enc = {enc_int, []}}, - #attr{name = <<"type">>, - dec = {dec_enum, [[group, jid, subscription]]}, - enc = {enc_enum, []}}, - #attr{name = <<"value">>}], - refs = [#ref{name = privacy_message, - label = '$kinds'}, - #ref{name = privacy_iq, - label = '$kinds'}, - #ref{name = privacy_presence_in, - label = '$kinds'}, - #ref{name = privacy_presence_out, - label = '$kinds'}]}). - --xml(privacy_list, - #elem{name = <<"list">>, - xmlns = <<"jabber:iq:privacy">>, - result = {privacy_list, '$name', '$items'}, - attrs = [#attr{name = <<"name">>, - required = true}], - refs = [#ref{name = privacy_item, - label = '$items'}]}). - --xml(privacy_default_list, - #elem{name = <<"default">>, - xmlns = <<"jabber:iq:privacy">>, - result = '$name', - attrs = [#attr{name = <<"name">>, - default = none}]}). - --xml(privacy_active_list, - #elem{name = <<"active">>, - xmlns = <<"jabber:iq:privacy">>, - result = '$name', - attrs = [#attr{name = <<"name">>, - default = none}]}). - --xml(privacy, - #elem{name = <<"query">>, - xmlns = <<"jabber:iq:privacy">>, - result = {privacy, '$lists', '$default', '$active'}, - refs = [#ref{name = privacy_list, - label = '$lists'}, - #ref{name = privacy_default_list, - min = 0, max = 1, - label = '$default'}, - #ref{name = privacy_active_list, - min = 0, max = 1, - label = '$active'}]}). - --xml(block_item, - #elem{name = <<"item">>, - xmlns = <<"urn:xmpp:blocking">>, - result = '$jid', - attrs = [#attr{name = <<"jid">>, - required = true, - dec = {dec_jid, []}, - enc = {enc_jid, []}}]}). - --xml(block, - #elem{name = <<"block">>, - xmlns = <<"urn:xmpp:blocking">>, - result = {block, '$items'}, - refs = [#ref{name = block_item, - label = '$items'}]}). - --xml(unblock, - #elem{name = <<"unblock">>, - xmlns = <<"urn:xmpp:blocking">>, - result = {unblock, '$items'}, - refs = [#ref{name = block_item, - label = '$items'}]}). - --xml(block_list, - #elem{name = <<"blocklist">>, - xmlns = <<"urn:xmpp:blocking">>, - result = {block_list}}). - --xml(disco_identity, - #elem{name = <<"identity">>, - xmlns = <<"http://jabber.org/protocol/disco#info">>, - result = {identity, '$category', '$type', '$lang', '$name'}, - attrs = [#attr{name = <<"category">>, - required = true}, - #attr{name = <<"type">>, - required = true}, - #attr{name = <<"xml:lang">>, - label = '$lang'}, - #attr{name = <<"name">>}]}). - --xml(disco_feature, - #elem{name = <<"feature">>, - xmlns = <<"http://jabber.org/protocol/disco#info">>, - result = '$var', - attrs = [#attr{name = <<"var">>, - required = true}]}). - --xml(disco_info, - #elem{name = <<"query">>, - xmlns = <<"http://jabber.org/protocol/disco#info">>, - result = {disco_info, '$node', '$identities', '$features', '$xdata'}, - attrs = [#attr{name = <<"node">>}], - refs = [#ref{name = disco_identity, - label = '$identities'}, - #ref{name = disco_feature, - label = '$features'}, - #ref{name = xdata, - label = '$xdata'}]}). - --xml(disco_item, - #elem{name = <<"item">>, - xmlns = <<"http://jabber.org/protocol/disco#items">>, - result = {disco_item, '$jid', '$name', '$node'}, - attrs = [#attr{name = <<"jid">>, - dec = {dec_jid, []}, - enc = {enc_jid, []}, - required = true}, - #attr{name = <<"name">>}, - #attr{name = <<"node">>}]}). --xml(disco_items, - #elem{name = <<"query">>, - xmlns = <<"http://jabber.org/protocol/disco#items">>, - result = {disco_items, '$node', '$items'}, - attrs = [#attr{name = <<"node">>}], - refs = [#ref{name = disco_item, - label = '$items'}]}). - --xml(private, - #elem{name = <<"query">>, - xmlns = <<"jabber:iq:private">>, - result = {private, '$_xmls'}}). - --xml(conference_nick, - #elem{name = <<"nick">>, - xmlns = <<"storage:bookmarks">>, - result = '$cdata'}). - --xml(conference_password, - #elem{name = <<"password">>, - xmlns = <<"storage:bookmarks">>, - result = '$cdata'}). - --xml(bookmark_conference, - #elem{name = <<"conference">>, - xmlns = <<"storage:bookmarks">>, - result = {bookmark_conference, '$name', '$jid', - '$autojoin', '$nick', '$password'}, - attrs = [#attr{name = <<"name">>, - required = true}, - #attr{name = <<"jid">>, - required = true, - dec = {dec_jid, []}, - enc = {enc_jid, []}}, - #attr{name = <<"autojoin">>, - default = false, - dec = {dec_bool, []}, - enc = {enc_bool, []}}], - refs = [#ref{name = conference_nick, - label = '$nick', - min = 0, max = 1}, - #ref{name = conference_password, - label = '$password', - min = 0, max = 1}]}). - --xml(bookmark_url, - #elem{name = <<"url">>, - xmlns = <<"storage:bookmarks">>, - result = {bookmark_url, '$name', '$url'}, - attrs = [#attr{name = <<"name">>, - required = true}, - #attr{name = <<"url">>, - required = true}]}). - --xml(bookmarks_storage, - #elem{name = <<"storage">>, - xmlns = <<"storage:bookmarks">>, - result = {bookmark_storage, '$conference', '$url'}, - refs = [#ref{name = bookmark_conference, - label = '$conference'}, - #ref{name = bookmark_url, - label = '$url'}]}). - --xml(stat_error, - #elem{name = <<"error">>, - xmlns = <<"http://jabber.org/protocol/stats">>, - result = {'$code', '$cdata'}, - attrs = [#attr{name = <<"code">>, - required = true, - enc = {enc_int, []}, - dec = {dec_int, []}}]}). - --xml(stat, - #elem{name = <<"stat">>, - xmlns = <<"http://jabber.org/protocol/stats">>, - result = {stat, '$name', '$units', '$value', '$error'}, - attrs = [#attr{name = <<"name">>, - required = true}, - #attr{name = <<"units">>}, - #attr{name = <<"value">>}], - refs = [#ref{name = stat_error, - label = '$error'}]}). - --xml(stats, - #elem{name = <<"query">>, - xmlns = <<"http://jabber.org/protocol/stats">>, - result = {stats, '$stat'}, - refs = [#ref{name = stat, - label = '$stat'}]}). - --xml(iq, - #elem{name = <<"iq">>, - xmlns = <<"jabber:client">>, - result = {iq, '$id', '$type', '$lang', '$from', '$to', - '$error', '$_els'}, - attrs = [#attr{name = <<"id">>, - required = true}, - #attr{name = <<"type">>, - required = true, - enc = {enc_enum, []}, - dec = {dec_enum, [[get, set, result, error]]}}, - #attr{name = <<"from">>, - dec = {dec_jid, []}, - enc = {enc_jid, []}}, - #attr{name = <<"to">>, - dec = {dec_jid, []}, - enc = {enc_jid, []}}, - #attr{name = <<"xml:lang">>, - label = '$lang'}], - refs = [#ref{name = error, min = 0, max = 1, label = '$error'}]}). - --xml(message_subject, - #elem{name = <<"subject">>, - xmlns = <<"jabber:client">>, - result = {text, '$lang', '$data'}, - cdata = #cdata{label = '$data'}, - attrs = [#attr{name = <<"xml:lang">>, label = '$lang'}]}). - --xml(message_body, - #elem{name = <<"body">>, - xmlns = <<"jabber:client">>, - result = {text, '$lang', '$data'}, - cdata = #cdata{label = '$data'}, - attrs = [#attr{name = <<"xml:lang">>, label = '$lang'}]}). - --xml(message_thread, - #elem{name = <<"thread">>, - xmlns = <<"jabber:client">>, - result = '$cdata'}). - --xml(message, - #elem{name = <<"message">>, - xmlns = <<"jabber:client">>, - result = {message, '$id', '$type', '$lang', '$from', '$to', - '$subject', '$body', '$thread', '$error', '$_els'}, - attrs = [#attr{name = <<"id">>}, - #attr{name = <<"type">>, - default = normal, - enc = {enc_enum, []}, - dec = {dec_enum, [[chat, normal, groupchat, - headline, error]]}}, - #attr{name = <<"from">>, - dec = {dec_jid, []}, - enc = {enc_jid, []}}, - #attr{name = <<"to">>, - dec = {dec_jid, []}, - enc = {enc_jid, []}}, - #attr{name = <<"xml:lang">>, - label = '$lang'}], - refs = [#ref{name = error, min = 0, max = 1, label = '$error'}, - #ref{name = message_subject, label = '$subject'}, - #ref{name = message_thread, min = 0, max = 1, label = '$thread'}, - #ref{name = message_body, label = '$body'}]}). - --xml(presence_show, - #elem{name = <<"show">>, - xmlns = <<"jabber:client">>, - result = '$cdata', - cdata = #cdata{enc = {enc_enum, []}, - dec = {dec_enum, [[away, chat, dnd, xa]]}}}). - --xml(presence_status, - #elem{name = <<"status">>, - xmlns = <<"jabber:client">>, - result = {text, '$lang', '$data'}, - cdata = #cdata{label = '$data'}, - attrs = [#attr{name = <<"xml:lang">>, - label = '$lang'}]}). - --xml(presence_priority, - #elem{name = <<"priority">>, - xmlns = <<"jabber:client">>, - result = '$cdata', - cdata = #cdata{enc = {enc_int, []}, - dec = {dec_int, []}}}). - --xml(presence, - #elem{name = <<"presence">>, - xmlns = <<"jabber:client">>, - result = {presence, '$id', '$type', '$lang', '$from', '$to', - '$show', '$status', '$priority', '$error', '$_els'}, - attrs = [#attr{name = <<"id">>}, - #attr{name = <<"type">>, - enc = {enc_enum, []}, - dec = {dec_enum, [[unavailable, subscribe, subscribed, - unsubscribe, unsubscribed, - probe, error]]}}, - #attr{name = <<"from">>, - dec = {dec_jid, []}, - enc = {enc_jid, []}}, - #attr{name = <<"to">>, - dec = {dec_jid, []}, - enc = {enc_jid, []}}, - #attr{name = <<"xml:lang">>, - label = '$lang'}], - refs = [#ref{name = error, min = 0, max = 1, label = '$error'}, - #ref{name = presence_show, min = 0, max = 1, label = '$show'}, - #ref{name = presence_status, label = '$status'}, - #ref{name = presence_priority, min = 0, max = 1, - label = '$priority'}]}). - --xml(error_bad_request, - #elem{name = <<"bad-request">>, - xmlns = <<"urn:ietf:params:xml:ns:xmpp-stanzas">>, - result = 'bad-request'}). --xml(error_conflict, - #elem{name = <<"conflict">>, - xmlns = <<"urn:ietf:params:xml:ns:xmpp-stanzas">>, - result = 'conflict'}). --xml(error_feature_not_implemented, - #elem{name = <<"feature-not-implemented">>, - xmlns = <<"urn:ietf:params:xml:ns:xmpp-stanzas">>, - result = 'feature-not-implemented'}). --xml(error_forbidden, - #elem{name = <<"forbidden">>, - xmlns = <<"urn:ietf:params:xml:ns:xmpp-stanzas">>, - result = 'forbidden'}). --xml(error_gone, - #elem{name = <<"gone">>, - xmlns = <<"urn:ietf:params:xml:ns:xmpp-stanzas">>, - cdata = #cdata{label = '$uri'}, - result = {'gone', '$uri'}}). --xml(error_internal_server_error, - #elem{name = <<"internal-server-error">>, - xmlns = <<"urn:ietf:params:xml:ns:xmpp-stanzas">>, - result = 'internal-server-error'}). --xml(error_item_not_found, - #elem{name = <<"item-not-found">>, - xmlns = <<"urn:ietf:params:xml:ns:xmpp-stanzas">>, - result = 'item-not-found'}). --xml(error_jid_malformed, - #elem{name = <<"jid-malformed">>, - xmlns = <<"urn:ietf:params:xml:ns:xmpp-stanzas">>, - result = 'jid-malformed'}). --xml(error_not_acceptable, - #elem{name = <<"not-acceptable">>, - xmlns = <<"urn:ietf:params:xml:ns:xmpp-stanzas">>, - result = 'not-acceptable'}). --xml(error_not_allowed, - #elem{name = <<"not-allowed">>, - xmlns = <<"urn:ietf:params:xml:ns:xmpp-stanzas">>, - result = 'not-allowed'}). --xml(error_not_authorized, - #elem{name = <<"not-authorized">>, - xmlns = <<"urn:ietf:params:xml:ns:xmpp-stanzas">>, - result = 'not-authorized'}). --xml(error_policy_violation, - #elem{name = <<"policy-violation">>, - xmlns = <<"urn:ietf:params:xml:ns:xmpp-stanzas">>, - result = 'policy-violation'}). --xml(error_recipient_unavailable, - #elem{name = <<"recipient-unavailable">>, - xmlns = <<"urn:ietf:params:xml:ns:xmpp-stanzas">>, - result = 'recipient-unavailable'}). --xml(error_redirect, - #elem{name = <<"redirect">>, - xmlns = <<"urn:ietf:params:xml:ns:xmpp-stanzas">>, - cdata = #cdata{label = '$uri'}, - result = {'redirect', '$uri'}}). --xml(error_registration_required, - #elem{name = <<"registration-required">>, - xmlns = <<"urn:ietf:params:xml:ns:xmpp-stanzas">>, - result = 'registration-required'}). --xml(error_remote_server_not_found, - #elem{name = <<"remote-server-not-found">>, - xmlns = <<"urn:ietf:params:xml:ns:xmpp-stanzas">>, - result = 'remote-server-not-found'}). --xml(error_remote_server_timeout, - #elem{name = <<"remote-server-timeout">>, - xmlns = <<"urn:ietf:params:xml:ns:xmpp-stanzas">>, - result = 'remote-server-timeout'}). --xml(error_resource_constraint, - #elem{name = <<"resource-constraint">>, - xmlns = <<"urn:ietf:params:xml:ns:xmpp-stanzas">>, - result = 'resource-constraint'}). --xml(error_service_unavailable, - #elem{name = <<"service-unavailable">>, - xmlns = <<"urn:ietf:params:xml:ns:xmpp-stanzas">>, - result = 'service-unavailable'}). --xml(error_subscription_required, - #elem{name = <<"subscription-required">>, - xmlns = <<"urn:ietf:params:xml:ns:xmpp-stanzas">>, - result = 'subscription-required'}). --xml(error_undefined_condition, - #elem{name = <<"undefined-condition">>, - xmlns = <<"urn:ietf:params:xml:ns:xmpp-stanzas">>, - result = 'undefined-condition'}). --xml(error_unexpected_request, - #elem{name = <<"unexpected-request">>, - xmlns = <<"urn:ietf:params:xml:ns:xmpp-stanzas">>, - result = 'unexpected-request'}). - --xml(error_text, - #elem{name = <<"text">>, - result = {text, '$lang', '$data'}, - cdata = #cdata{label = '$data'}, - xmlns = <<"urn:ietf:params:xml:ns:xmpp-stanzas">>, - attrs = [#attr{name = <<"xml:lang">>, - label = '$lang'}]}). - --xml(error, - #elem{name = <<"error">>, - xmlns = <<"jabber:client">>, - result = {error, '$type', '$by', '$reason', '$text'}, - attrs = [#attr{name = <<"type">>, - label = '$type', - required = true, - dec = {dec_enum, [[auth, cancel, continue, - modify, wait]]}, - enc = {enc_enum, []}}, - #attr{name = <<"by">>}], - refs = [#ref{name = error_text, - min = 0, max = 1, label = '$text'}, - #ref{name = error_bad_request, - min = 0, max = 1, label = '$reason'}, - #ref{name = error_conflict, - min = 0, max = 1, label = '$reason'}, - #ref{name = error_feature_not_implemented, - min = 0, max = 1, label = '$reason'}, - #ref{name = error_forbidden, - min = 0, max = 1, label = '$reason'}, - #ref{name = error_gone, - min = 0, max = 1, label = '$reason'}, - #ref{name = error_internal_server_error, - min = 0, max = 1, label = '$reason'}, - #ref{name = error_item_not_found, - min = 0, max = 1, label = '$reason'}, - #ref{name = error_jid_malformed, - min = 0, max = 1, label = '$reason'}, - #ref{name = error_not_acceptable, - min = 0, max = 1, label = '$reason'}, - #ref{name = error_not_allowed, - min = 0, max = 1, label = '$reason'}, - #ref{name = error_not_authorized, - min = 0, max = 1, label = '$reason'}, - #ref{name = error_policy_violation, - min = 0, max = 1, label = '$reason'}, - #ref{name = error_recipient_unavailable, - min = 0, max = 1, label = '$reason'}, - #ref{name = error_redirect, - min = 0, max = 1, label = '$reason'}, - #ref{name = error_registration_required, - min = 0, max = 1, label = '$reason'}, - #ref{name = error_remote_server_not_found, - min = 0, max = 1, label = '$reason'}, - #ref{name = error_remote_server_timeout, - min = 0, max = 1, label = '$reason'}, - #ref{name = error_resource_constraint, - min = 0, max = 1, label = '$reason'}, - #ref{name = error_service_unavailable, - min = 0, max = 1, label = '$reason'}, - #ref{name = error_subscription_required, - min = 0, max = 1, label = '$reason'}, - #ref{name = error_undefined_condition, - min = 0, max = 1, label = '$reason'}, - #ref{name = error_unexpected_request, - min = 0, max = 1, label = '$reason'}]}). - --xml(bind_jid, - #elem{name = <<"jid">>, - xmlns = <<"urn:ietf:params:xml:ns:xmpp-bind">>, - result = '$cdata', - cdata = #cdata{dec = {dec_jid, []}, - enc = {enc_jid, []}}}). - --xml(bind_resource, - #elem{name = <<"resource">>, - xmlns = <<"urn:ietf:params:xml:ns:xmpp-bind">>, - result = '$cdata', - cdata = #cdata{dec = {resourceprep, []}, - enc = {resourceprep, []}}}). - --xml(bind, #elem{name = <<"bind">>, - xmlns = <<"urn:ietf:params:xml:ns:xmpp-bind">>, - result = {bind, '$jid', '$resource'}, - refs = [#ref{name = bind_jid, - label = '$jid', - min = 0, max = 1}, - #ref{name = bind_resource, - min = 0, max = 1, - label = '$resource'}]}). - --xml(sasl_auth, - #elem{name = <<"auth">>, - xmlns = <<"urn:ietf:params:xml:ns:xmpp-sasl">>, - cdata = #cdata{label = '$text', - dec = {base64, decode, []}, - enc = {base64, encode, []}}, - result = {sasl_auth, '$mechanism', '$text'}, - attrs = [#attr{name = <<"mechanism">>, - required = true}]}). - --xml(sasl_abort, - #elem{name = <<"abort">>, - xmlns = <<"urn:ietf:params:xml:ns:xmpp-sasl">>, - result = {sasl_abort}}). - --xml(sasl_challenge, - #elem{name = <<"challenge">>, - xmlns = <<"urn:ietf:params:xml:ns:xmpp-sasl">>, - cdata = #cdata{label = '$text', - dec = {base64, decode, []}, - enc = {base64, encode, []}}, - result = {sasl_challenge, '$text'}}). - --xml(sasl_response, - #elem{name = <<"response">>, - xmlns = <<"urn:ietf:params:xml:ns:xmpp-sasl">>, - cdata = #cdata{label = '$text', - dec = {base64, decode, []}, - enc = {base64, encode, []}}, - result = {sasl_response, '$text'}}). - --xml(sasl_success, - #elem{name = <<"success">>, - xmlns = <<"urn:ietf:params:xml:ns:xmpp-sasl">>, - cdata = #cdata{label = '$text', - dec = {base64, decode, []}, - enc = {base64, encode, []}}, - result = {sasl_success, '$text'}}). - --xml(sasl_failure_text, - #elem{name = <<"text">>, - xmlns = <<"urn:ietf:params:xml:ns:xmpp-sasl">>, - result = {text, '$lang', '$data'}, - cdata = #cdata{label = '$data'}, - attrs = [#attr{name = <<"xml:lang">>, - label = '$lang'}]}). - --xml(sasl_failure_aborted, - #elem{name = <<"aborted">>, - result = 'aborted', - xmlns = <<"urn:ietf:params:xml:ns:xmpp-sasl">>}). --xml(sasl_failure_account_disabled, - #elem{name = <<"account-disabled">>, - result = 'account-disabled', - xmlns = <<"urn:ietf:params:xml:ns:xmpp-sasl">>}). --xml(sasl_failure_credentials_expired, - #elem{name = <<"credentials-expired">>, - result = 'credentials-expired', - xmlns = <<"urn:ietf:params:xml:ns:xmpp-sasl">>}). --xml(sasl_failure_encryption_required, - #elem{name = <<"encryption-required">>, - result = 'encryption-required', - xmlns = <<"urn:ietf:params:xml:ns:xmpp-sasl">>}). --xml(sasl_failure_incorrect_encoding, - #elem{name = <<"incorrect-encoding">>, - result = 'incorrect-encoding', - xmlns = <<"urn:ietf:params:xml:ns:xmpp-sasl">>}). --xml(sasl_failure_invalid_authzid, - #elem{name = <<"invalid-authzid">>, - result = 'invalid-authzid', - xmlns = <<"urn:ietf:params:xml:ns:xmpp-sasl">>}). --xml(sasl_failure_invalid_mechanism, - #elem{name = <<"invalid-mechanism">>, - result = 'invalid-mechanism', - xmlns = <<"urn:ietf:params:xml:ns:xmpp-sasl">>}). --xml(sasl_failure_malformed_request, - #elem{name = <<"malformed-request">>, - result = 'malformed-request', - xmlns = <<"urn:ietf:params:xml:ns:xmpp-sasl">>}). --xml(sasl_failure_mechanism_too_weak, - #elem{name = <<"mechanism-too-weak">>, - result = 'mechanism-too-weak', - xmlns = <<"urn:ietf:params:xml:ns:xmpp-sasl">>}). --xml(sasl_failure_not_authorized, - #elem{name = <<"not-authorized">>, - result = 'not-authorized', - xmlns = <<"urn:ietf:params:xml:ns:xmpp-sasl">>}). --xml(sasl_failure_temporary_auth_failure, - #elem{name = <<"temporary-auth-failure">>, - result = 'temporary-auth-failure', - xmlns = <<"urn:ietf:params:xml:ns:xmpp-sasl">>}). - --xml(sasl_failure, - #elem{name = <<"failure">>, - xmlns = <<"urn:ietf:params:xml:ns:xmpp-sasl">>, - result = {sasl_failure, '$reason', '$text'}, - refs = [#ref{name = sasl_failure_text, - label = '$text'}, - #ref{name = sasl_failure_aborted, - min = 0, max = 1, label = '$reason'}, - #ref{name = sasl_failure_account_disabled, - min = 0, max = 1, label = '$reason'}, - #ref{name = sasl_failure_credentials_expired, - min = 0, max = 1, label = '$reason'}, - #ref{name = sasl_failure_encryption_required, - min = 0, max = 1, label = '$reason'}, - #ref{name = sasl_failure_incorrect_encoding, - min = 0, max = 1, label = '$reason'}, - #ref{name = sasl_failure_invalid_authzid, - min = 0, max = 1, label = '$reason'}, - #ref{name = sasl_failure_invalid_mechanism, - min = 0, max = 1, label = '$reason'}, - #ref{name = sasl_failure_malformed_request, - min = 0, max = 1, label = '$reason'}, - #ref{name = sasl_failure_mechanism_too_weak, - min = 0, max = 1, label = '$reason'}, - #ref{name = sasl_failure_not_authorized, - min = 0, max = 1, label = '$reason'}, - #ref{name = sasl_failure_temporary_auth_failure, - min = 0, max = 1, label = '$reason'}]}). - --xml(sasl_mechanism, - #elem{name = <<"mechanism">>, - xmlns = <<"urn:ietf:params:xml:ns:xmpp-sasl">>, - result = '$cdata'}). - --xml(sasl_mechanisms, - #elem{name = <<"mechanisms">>, - xmlns = <<"urn:ietf:params:xml:ns:xmpp-sasl">>, - result = {sasl_mechanisms, '$list'}, - refs = [#ref{name = sasl_mechanism, - label = '$list'}]}). - --xml(starttls_required, - #elem{name = <<"required">>, - xmlns = <<"urn:ietf:params:xml:ns:xmpp-tls">>, - result = true}). - --xml(starttls, - #elem{name = <<"starttls">>, - xmlns = <<"urn:ietf:params:xml:ns:xmpp-tls">>, - result = {starttls, '$required'}, - refs = [#ref{name = starttls_required, - label = '$required', - min = 0, max = 1, - default = false}]}). - --xml(starttls_proceed, - #elem{name = <<"proceed">>, - xmlns = <<"urn:ietf:params:xml:ns:xmpp-tls">>, - result = {starttls_proceed}}). - --xml(starttls_failure, - #elem{name = <<"failure">>, - xmlns = <<"urn:ietf:params:xml:ns:xmpp-tls">>, - result = {starttls_failure}}). - --xml(compress_failure_setup_failed, - #elem{name = <<"setup-failed">>, - xmlns = <<"http://jabber.org/protocol/compress">>, - result = 'setup-failed'}). --xml(compress_failure_processing_failed, - #elem{name = <<"processing-failed">>, - xmlns = <<"http://jabber.org/protocol/compress">>, - result = 'processing-failed'}). --xml(compress_failure_unsupported_method, - #elem{name = <<"unsupported-method">>, - xmlns = <<"http://jabber.org/protocol/compress">>, - result = 'unsupported-method'}). - --xml(compress_failure, - #elem{name = <<"failure">>, - xmlns = <<"http://jabber.org/protocol/compress">>, - result = {compress_failure, '$reason'}, - refs = [#ref{name = compress_failure_setup_failed, - min = 0, max = 1, label = '$reason'}, - #ref{name = compress_failure_processing_failed, - min = 0, max = 1, label = '$reason'}, - #ref{name = compress_failure_unsupported_method, - min = 0, max = 1, label = '$reason'}]}). - --xml(compress_method, - #elem{name = <<"method">>, - xmlns = <<"http://jabber.org/protocol/compress">>, - result = '$cdata'}). - --xml(compress, - #elem{name = <<"compress">>, - xmlns = <<"http://jabber.org/protocol/compress">>, - result = {compress, '$methods'}, - refs = [#ref{name = compress_method, - label = '$methods'}]}). - --xml(compressed, - #elem{name = <<"compressed">>, - xmlns = <<"http://jabber.org/protocol/compress">>, - result = {compressed}}). - --xml(compression_method, - #elem{name = <<"method">>, - xmlns = <<"http://jabber.org/features/compress">>, - result = '$cdata'}). - --xml(compression, - #elem{name = <<"compression">>, - xmlns = <<"http://jabber.org/features/compress">>, - result = {compression, '$methods'}, - refs = [#ref{name = compression_method, label = '$methods'}]}). - --xml(stream_features, - #elem{name = <<"stream:features">>, - xmlns = <<"http://etherx.jabber.org/streams">>, - result = {stream_features, '$_els'}}). - --xml(p1_push, - #elem{name = <<"push">>, - result = {p1_push}, - xmlns = <<"p1:push">>}). - --xml(p1_rebind, - #elem{name = <<"rebind">>, - result = {p1_rebind}, - xmlns = <<"p1:rebind">>}). - --xml(p1_ack, - #elem{name = <<"ack">>, - result = {p1_ack}, - xmlns = <<"p1:ack">>}). - --xml(caps, - #elem{name = <<"c">>, - xmlns = <<"http://jabber.org/protocol/caps">>, - result = {caps, '$hash', '$node', '$ver'}, - attrs = [#attr{name = <<"hash">>}, - #attr{name = <<"node">>}, - #attr{name = <<"ver">>, - enc = {base64, encode, []}, - dec = {base64, decode, []}}]}). - --xml(feature_register, - #elem{name = <<"register">>, - xmlns = <<"http://jabber.org/features/iq-register">>, - result = {feature_register}}). - --xml(register_registered, - #elem{name = <<"registered">>, - xmlns = <<"jabber:iq:register">>, - result = true}). --xml(register_remove, - #elem{name = <<"remove">>, - xmlns = <<"jabber:iq:register">>, - result = true}). --xml(register_instructions, - #elem{name = <<"instructions">>, - xmlns = <<"jabber:iq:register">>, - result = '$cdata'}). --xml(register_username, - #elem{name = <<"username">>, - xmlns = <<"jabber:iq:register">>, - cdata = #cdata{default = none}, - result = '$cdata'}). --xml(register_nick, - #elem{name = <<"nick">>, - xmlns = <<"jabber:iq:register">>, - cdata = #cdata{default = none}, - result = '$cdata'}). --xml(register_password, - #elem{name = <<"password">>, - xmlns = <<"jabber:iq:register">>, - cdata = #cdata{default = none}, - result = '$cdata'}). --xml(register_name, - #elem{name = <<"name">>, - xmlns = <<"jabber:iq:register">>, - cdata = #cdata{default = none}, - result = '$cdata'}). --xml(register_first, - #elem{name = <<"first">>, - xmlns = <<"jabber:iq:register">>, - cdata = #cdata{default = none}, - result = '$cdata'}). --xml(register_last, - #elem{name = <<"last">>, - xmlns = <<"jabber:iq:register">>, - cdata = #cdata{default = none}, - result = '$cdata'}). --xml(register_email, - #elem{name = <<"email">>, - xmlns = <<"jabber:iq:register">>, - cdata = #cdata{default = none}, - result = '$cdata'}). --xml(register_address, - #elem{name = <<"address">>, - xmlns = <<"jabber:iq:register">>, - cdata = #cdata{default = none}, - result = '$cdata'}). --xml(register_city, - #elem{name = <<"city">>, - xmlns = <<"jabber:iq:register">>, - cdata = #cdata{default = none}, - result = '$cdata'}). --xml(register_state, - #elem{name = <<"state">>, - xmlns = <<"jabber:iq:register">>, - cdata = #cdata{default = none}, - result = '$cdata'}). --xml(register_zip, - #elem{name = <<"zip">>, - xmlns = <<"jabber:iq:register">>, - cdata = #cdata{default = none}, - result = '$cdata'}). --xml(register_phone, - #elem{name = <<"phone">>, - xmlns = <<"jabber:iq:register">>, - cdata = #cdata{default = none}, - result = '$cdata'}). --xml(register_url, - #elem{name = <<"url">>, - xmlns = <<"jabber:iq:register">>, - cdata = #cdata{default = none}, - result = '$cdata'}). --xml(register_date, - #elem{name = <<"date">>, - xmlns = <<"jabber:iq:register">>, - cdata = #cdata{default = none}, - result = '$cdata'}). --xml(register_misc, - #elem{name = <<"misc">>, - xmlns = <<"jabber:iq:register">>, - cdata = #cdata{default = none}, - result = '$cdata'}). --xml(register_text, - #elem{name = <<"text">>, - xmlns = <<"jabber:iq:register">>, - cdata = #cdata{default = none}, - result = '$cdata'}). --xml(register_key, - #elem{name = <<"key">>, - xmlns = <<"jabber:iq:register">>, - cdata = #cdata{default = none}, - result = '$cdata'}). - --xml(register, - #elem{name = <<"query">>, - xmlns = <<"jabber:iq:register">>, - result = {register, '$registered', '$remove', '$instructions', - '$username', '$nick', '$password', '$name', - '$first', '$last', '$email', '$address', - '$city', '$state', '$zip', '$phone', '$url', - '$date', '$misc', '$text', '$key', '$xdata'}, - refs = [#ref{name = xdata, min = 0, max = 1, - label = '$xdata'}, - #ref{name = register_registered, min = 0, max = 1, - default = false, label = '$registered'}, - #ref{name = register_remove, min = 0, max = 1, - default = false, label = '$remove'}, - #ref{name = register_instructions, min = 0, max = 1, - label = '$instructions'}, - #ref{name = register_username, min = 0, max = 1, - label = '$username'}, - #ref{name = register_nick, min = 0, max = 1, - label = '$nick'}, - #ref{name = register_password, min = 0, max = 1, - label = '$password'}, - #ref{name = register_name, min = 0, max = 1, - label = '$name'}, - #ref{name = register_first, min = 0, max = 1, - label = '$first'}, - #ref{name = register_last, min = 0, max = 1, - label = '$last'}, - #ref{name = register_email, min = 0, max = 1, - label = '$email'}, - #ref{name = register_address, min = 0, max = 1, - label = '$address'}, - #ref{name = register_city, min = 0, max = 1, - label = '$city'}, - #ref{name = register_state, min = 0, max = 1, - label = '$state'}, - #ref{name = register_zip, min = 0, max = 1, - label = '$zip'}, - #ref{name = register_phone, min = 0, max = 1, - label = '$phone'}, - #ref{name = register_url, min = 0, max = 1, - label = '$url'}, - #ref{name = register_date, min = 0, max = 1, - label = '$date'}, - #ref{name = register_misc, min = 0, max = 1, - label = '$misc'}, - #ref{name = register_text, min = 0, max = 1, - label = '$text'}, - #ref{name = register_key, min = 0, max = 1, - label = '$key'}]}). - --xml(session, - #elem{name = <<"session">>, - xmlns = <<"urn:ietf:params:xml:ns:xmpp-session">>, - result = {session}}). - --xml(ping, - #elem{name = <<"ping">>, - xmlns = <<"urn:xmpp:ping">>, - result = {ping}}). - --xml(time_utc, - #elem{name = <<"utc">>, - xmlns = <<"urn:xmpp:time">>, - result = '$cdata', - cdata = #cdata{dec = {dec_utc, []}, - enc = {enc_utc, []}}}). - --xml(time_tzo, - #elem{name = <<"tzo">>, - xmlns = <<"urn:xmpp:time">>, - result = '$cdata', - cdata = #cdata{dec = {dec_tzo, []}, - enc = {enc_tzo, []}}}). - --xml(time, - #elem{name = <<"time">>, - xmlns = <<"urn:xmpp:time">>, - result = {time, '$tzo', '$utc'}, - refs = [#ref{name = time_tzo, - label = '$tzo', - min = 0, max = 1}, - #ref{name = time_utc, - label = '$utc', - min = 0, max = 1}]}). - --xml(stream_error_text, - #elem{name = <<"text">>, - result = {text, '$lang', '$data'}, - cdata = #cdata{label = '$data'}, - xmlns = <<"urn:ietf:params:xml:ns:xmpp-streams">>, - attrs = [#attr{name = <<"xml:lang">>, - label = '$lang'}]}). - --xml(stream_error_bad_format, - #elem{name = <<"bad-format">>, - result = 'bad-format', - xmlns = <<"urn:ietf:params:xml:ns:xmpp-streams">>}). --xml(stream_error_bad_namespace_prefix, - #elem{name = <<"bad-namespace-prefix">>, - result = 'bad-namespace-prefix', - xmlns = <<"urn:ietf:params:xml:ns:xmpp-streams">>}). --xml(stream_error_conflict, - #elem{name = <<"conflict">>, - result = 'conflict', - xmlns = <<"urn:ietf:params:xml:ns:xmpp-streams">>}). --xml(stream_error_connection_timeout, - #elem{name = <<"connection-timeout">>, - result = 'connection-timeout', - xmlns = <<"urn:ietf:params:xml:ns:xmpp-streams">>}). --xml(stream_error_host_gone, - #elem{name = <<"host-gone">>, - result = 'host-gone', - xmlns = <<"urn:ietf:params:xml:ns:xmpp-streams">>}). --xml(stream_error_host_unknown, - #elem{name = <<"host-unknown">>, - result = 'host-unknown', - xmlns = <<"urn:ietf:params:xml:ns:xmpp-streams">>}). --xml(stream_error_improper_addressing, - #elem{name = <<"improper-addressing">>, - result = 'improper-addressing', - xmlns = <<"urn:ietf:params:xml:ns:xmpp-streams">>}). --xml(stream_error_internal_server_error, - #elem{name = <<"internal-server-error">>, - result = 'internal-server-error', - xmlns = <<"urn:ietf:params:xml:ns:xmpp-streams">>}). --xml(stream_error_invalid_from, - #elem{name = <<"invalid-from">>, - result = 'invalid-from', - xmlns = <<"urn:ietf:params:xml:ns:xmpp-streams">>}). --xml(stream_error_invalid_id, - #elem{name = <<"invalid-id">>, - result = 'invalid-id', - xmlns = <<"urn:ietf:params:xml:ns:xmpp-streams">>}). --xml(stream_error_invalid_namespace, - #elem{name = <<"invalid-namespace">>, - result = 'invalid-namespace', - xmlns = <<"urn:ietf:params:xml:ns:xmpp-streams">>}). --xml(stream_error_invalid_xml, - #elem{name = <<"invalid-xml">>, - result = 'invalid-xml', - xmlns = <<"urn:ietf:params:xml:ns:xmpp-streams">>}). --xml(stream_error_not_authorized, - #elem{name = <<"not-authorized">>, - result = 'not-authorized', - xmlns = <<"urn:ietf:params:xml:ns:xmpp-streams">>}). --xml(stream_error_not_well_formed, - #elem{name = <<"not-well-formed">>, - result = 'not-well-formed', - xmlns = <<"urn:ietf:params:xml:ns:xmpp-streams">>}). --xml(stream_error_policy_violation, - #elem{name = <<"policy-violation">>, - result = 'policy-violation', - xmlns = <<"urn:ietf:params:xml:ns:xmpp-streams">>}). --xml(stream_error_remote_connection_failed, - #elem{name = <<"remote-connection-failed">>, - result = 'remote-connection-failed', - xmlns = <<"urn:ietf:params:xml:ns:xmpp-streams">>}). --xml(stream_error_reset, - #elem{name = <<"reset">>, - result = 'reset', - xmlns = <<"urn:ietf:params:xml:ns:xmpp-streams">>}). --xml(stream_error_resource_constraint, - #elem{name = <<"resource-constraint">>, - result = 'resource-constraint', - xmlns = <<"urn:ietf:params:xml:ns:xmpp-streams">>}). --xml(stream_error_restricted_xml, - #elem{name = <<"restricted-xml">>, - result = 'restricted-xml', - xmlns = <<"urn:ietf:params:xml:ns:xmpp-streams">>}). --xml(stream_error_see_other_host, - #elem{name = <<"see-other-host">>, - cdata = #cdata{required = true, label = '$host'}, - result = {'see-other-host', '$host'}, - xmlns = <<"urn:ietf:params:xml:ns:xmpp-streams">>}). --xml(stream_error_system_shutdown, - #elem{name = <<"system-shutdown">>, - result = 'system-shutdown', - xmlns = <<"urn:ietf:params:xml:ns:xmpp-streams">>}). --xml(stream_error_undefined_condition, - #elem{name = <<"undefined-condition">>, - result = 'undefined-condition', - xmlns = <<"urn:ietf:params:xml:ns:xmpp-streams">>}). --xml(stream_error_unsupported_encoding, - #elem{name = <<"unsupported-encoding">>, - result = 'unsupported-encoding', - xmlns = <<"urn:ietf:params:xml:ns:xmpp-streams">>}). --xml(stream_error_unsupported_stanza_type, - #elem{name = <<"unsupported-stanza-type">>, - result = 'unsupported-stanza-type', - xmlns = <<"urn:ietf:params:xml:ns:xmpp-streams">>}). --xml(stream_error_unsupported_version, - #elem{name = <<"unsupported-version">>, - result = 'unsupported-version', - xmlns = <<"urn:ietf:params:xml:ns:xmpp-streams">>}). - --xml(stream_error, - #elem{name = <<"stream:error">>, - xmlns = <<"http://etherx.jabber.org/streams">>, - result = {stream_error, '$reason', '$text'}, - refs = [#ref{name = stream_error_text, - label = '$text', - min = 0, max = 1}, - #ref{name = stream_error_bad_format, - min = 0, max = 1, label = '$reason'}, - #ref{name = stream_error_bad_namespace_prefix, - min = 0, max = 1, label = '$reason'}, - #ref{name = stream_error_conflict, - min = 0, max = 1, label = '$reason'}, - #ref{name = stream_error_connection_timeout, - min = 0, max = 1, label = '$reason'}, - #ref{name = stream_error_host_gone, - min = 0, max = 1, label = '$reason'}, - #ref{name = stream_error_host_unknown, - min = 0, max = 1, label = '$reason'}, - #ref{name = stream_error_improper_addressing, - min = 0, max = 1, label = '$reason'}, - #ref{name = stream_error_internal_server_error, - min = 0, max = 1, label = '$reason'}, - #ref{name = stream_error_invalid_from, - min = 0, max = 1, label = '$reason'}, - #ref{name = stream_error_invalid_id, - min = 0, max = 1, label = '$reason'}, - #ref{name = stream_error_invalid_namespace, - min = 0, max = 1, label = '$reason'}, - #ref{name = stream_error_invalid_xml, - min = 0, max = 1, label = '$reason'}, - #ref{name = stream_error_not_authorized, - min = 0, max = 1, label = '$reason'}, - #ref{name = stream_error_not_well_formed, - min = 0, max = 1, label = '$reason'}, - #ref{name = stream_error_policy_violation, - min = 0, max = 1, label = '$reason'}, - #ref{name = stream_error_remote_connection_failed, - min = 0, max = 1, label = '$reason'}, - #ref{name = stream_error_reset, - min = 0, max = 1, label = '$reason'}, - #ref{name = stream_error_resource_constraint, - min = 0, max = 1, label = '$reason'}, - #ref{name = stream_error_restricted_xml, - min = 0, max = 1, label = '$reason'}, - #ref{name = stream_error_see_other_host, - min = 0, max = 1, label = '$reason'}, - #ref{name = stream_error_system_shutdown, - min = 0, max = 1, label = '$reason'}, - #ref{name = stream_error_undefined_condition, - min = 0, max = 1, label = '$reason'}, - #ref{name = stream_error_unsupported_encoding, - min = 0, max = 1, label = '$reason'}, - #ref{name = stream_error_unsupported_stanza_type, - min = 0, max = 1, label = '$reason'}, - #ref{name = stream_error_unsupported_version, - min = 0, max = 1, label = '$reason'} - ]}). - --xml(vcard_HOME, #elem{name = <<"HOME">>, xmlns = <<"vcard-temp">>, result = true}). --xml(vcard_WORK, #elem{name = <<"WORK">>, xmlns = <<"vcard-temp">>, result = true}). --xml(vcard_VOICE, #elem{name = <<"VOICE">>, xmlns = <<"vcard-temp">>, result = true}). --xml(vcard_FAX, #elem{name = <<"FAX">>, xmlns = <<"vcard-temp">>, result = true}). --xml(vcard_PAGER, #elem{name = <<"PAGER">>, xmlns = <<"vcard-temp">>, result = true}). --xml(vcard_MSG, #elem{name = <<"MSG">>, xmlns = <<"vcard-temp">>, result = true}). --xml(vcard_CELL, #elem{name = <<"CELL">>, xmlns = <<"vcard-temp">>, result = true}). --xml(vcard_VIDEO, #elem{name = <<"VIDEO">>, xmlns = <<"vcard-temp">>, result = true}). --xml(vcard_BBS, #elem{name = <<"BBS">>, xmlns = <<"vcard-temp">>, result = true}). --xml(vcard_MODEM, #elem{name = <<"MODEM">>, xmlns = <<"vcard-temp">>, result = true}). --xml(vcard_ISDN, #elem{name = <<"ISDN">>, xmlns = <<"vcard-temp">>, result = true}). --xml(vcard_PCS, #elem{name = <<"PCS">>, xmlns = <<"vcard-temp">>, result = true}). --xml(vcard_POSTAL, #elem{name = <<"POSTAL">>, xmlns = <<"vcard-temp">>, result = true}). --xml(vcard_PARCEL, #elem{name = <<"PARCEL">>, xmlns = <<"vcard-temp">>, result = true}). --xml(vcard_DOM, #elem{name = <<"DOM">>, xmlns = <<"vcard-temp">>, result = true}). --xml(vcard_INTL, #elem{name = <<"INTL">>, xmlns = <<"vcard-temp">>, result = true}). --xml(vcard_PREF, #elem{name = <<"PREF">>, xmlns = <<"vcard-temp">>, result = true}). --xml(vcard_INTERNET, #elem{name = <<"INTERNET">>, xmlns = <<"vcard-temp">>, result = true}). --xml(vcard_X400, #elem{name = <<"X400">>, xmlns = <<"vcard-temp">>, result = true}). --xml(vcard_FAMILY, #elem{name = <<"FAMILY">>, xmlns = <<"vcard-temp">>, result = '$cdata'}). --xml(vcard_GIVEN, #elem{name = <<"GIVEN">>, xmlns = <<"vcard-temp">>, result = '$cdata'}). --xml(vcard_MIDDLE, #elem{name = <<"MIDDLE">>, xmlns = <<"vcard-temp">>, result = '$cdata'}). --xml(vcard_PREFIX, #elem{name = <<"PREFIX">>, xmlns = <<"vcard-temp">>, result = '$cdata'}). --xml(vcard_SUFFIX, #elem{name = <<"SUFFIX">>, xmlns = <<"vcard-temp">>, result = '$cdata'}). --xml(vcard_POBOX, #elem{name = <<"POBOX">>, xmlns = <<"vcard-temp">>, result = '$cdata'}). --xml(vcard_EXTADD, #elem{name = <<"EXTADD">>, xmlns = <<"vcard-temp">>, result = '$cdata'}). --xml(vcard_STREET, #elem{name = <<"STREET">>, xmlns = <<"vcard-temp">>, result = '$cdata'}). --xml(vcard_LOCALITY, #elem{name = <<"LOCALITY">>, xmlns = <<"vcard-temp">>, result = '$cdata'}). --xml(vcard_REGION, #elem{name = <<"REGION">>, xmlns = <<"vcard-temp">>, result = '$cdata'}). --xml(vcard_PCODE, #elem{name = <<"PCODE">>, xmlns = <<"vcard-temp">>, result = '$cdata'}). --xml(vcard_CTRY, #elem{name = <<"CTRY">>, xmlns = <<"vcard-temp">>, result = '$cdata'}). --xml(vcard_LINE, #elem{name = <<"LINE">>, xmlns = <<"vcard-temp">>, result = '$cdata'}). --xml(vcard_NUMBER, #elem{name = <<"NUMBER">>, xmlns = <<"vcard-temp">>, result = '$cdata'}). --xml(vcard_USERID, #elem{name = <<"USERID">>, xmlns = <<"vcard-temp">>, result = '$cdata'}). --xml(vcard_LAT, #elem{name = <<"LAT">>, xmlns = <<"vcard-temp">>, result = '$cdata'}). --xml(vcard_LON, #elem{name = <<"LON">>, xmlns = <<"vcard-temp">>, result = '$cdata'}). --xml(vcard_ORGNAME, #elem{name = <<"ORGNAME">>, xmlns = <<"vcard-temp">>, result = '$cdata'}). --xml(vcard_ORGUNIT, #elem{name = <<"ORGUNIT">>, xmlns = <<"vcard-temp">>, result = '$cdata'}). --xml(vcard_PHONETIC, #elem{name = <<"PHONETIC">>, xmlns = <<"vcard-temp">>, result = '$cdata'}). --xml(vcard_CRED, #elem{name = <<"CRED">>, xmlns = <<"vcard-temp">>, result = '$cdata'}). --xml(vcard_VERSION, #elem{name = <<"VERSION">>, xmlns = <<"vcard-temp">>, result = '$cdata'}). --xml(vcard_FN, #elem{name = <<"FN">>, xmlns = <<"vcard-temp">>, result = '$cdata'}). --xml(vcard_NICKNAME, #elem{name = <<"NICKNAME">>, xmlns = <<"vcard-temp">>, result = '$cdata'}). --xml(vcard_BDAY, #elem{name = <<"BDAY">>, xmlns = <<"vcard-temp">>, result = '$cdata'}). --xml(vcard_JABBERID, #elem{name = <<"JABBERID">>, xmlns = <<"vcard-temp">>, result = '$cdata'}). --xml(vcard_MAILER, #elem{name = <<"MAILER">>, xmlns = <<"vcard-temp">>, result = '$cdata'}). --xml(vcard_TZ, #elem{name = <<"TZ">>, xmlns = <<"vcard-temp">>, result = '$cdata'}). --xml(vcard_TITLE, #elem{name = <<"TITLE">>, xmlns = <<"vcard-temp">>, result = '$cdata'}). --xml(vcard_ROLE, #elem{name = <<"ROLE">>, xmlns = <<"vcard-temp">>, result = '$cdata'}). --xml(vcard_KEYWORD, #elem{name = <<"KEYWORD">>, xmlns = <<"vcard-temp">>, result = '$cdata'}). --xml(vcard_NOTE, #elem{name = <<"NOTE">>, xmlns = <<"vcard-temp">>, result = '$cdata'}). --xml(vcard_PRODID, #elem{name = <<"PRODID">>, xmlns = <<"vcard-temp">>, result = '$cdata'}). --xml(vcard_REV, #elem{name = <<"REV">>, xmlns = <<"vcard-temp">>, result = '$cdata'}). --xml(vcard_SORT_STRING, #elem{name = <<"SORT-STRING">>, xmlns = <<"vcard-temp">>, result = '$cdata'}). --xml(vcard_UID, #elem{name = <<"UID">>, xmlns = <<"vcard-temp">>, result = '$cdata'}). --xml(vcard_URL, #elem{name = <<"URL">>, xmlns = <<"vcard-temp">>, result = '$cdata'}). --xml(vcard_DESC, #elem{name = <<"DESC">>, xmlns = <<"vcard-temp">>, result = '$cdata'}). --xml(vcard_TYPE, #elem{name = <<"TYPE">>, xmlns = <<"vcard-temp">>, result = '$cdata'}). --xml(vcard_EXTVAL, #elem{name = <<"EXTVAL">>, xmlns = <<"vcard-temp">>, result = '$cdata'}). --xml(vcard_PUBLIC, #elem{name = <<"PUBLIC">>, xmlns = <<"vcard-temp">>, result = public}). --xml(vcard_PRIVATE, #elem{name = <<"PRIVATE">>, xmlns = <<"vcard-temp">>, result = private}). --xml(vcard_CONFIDENTIAL, #elem{name = <<"CONFIDENTIAL">>, xmlns = <<"vcard-temp">>, result = confidential}). - --xml(vcard_N, - #elem{name = <<"N">>, - xmlns = <<"vcard-temp">>, - result = {vcard_name, '$family', '$given', '$middle', - '$prefix', '$suffix'}, - refs = [#ref{name = vcard_FAMILY, min = 0, max = 1, label = '$family'}, - #ref{name = vcard_GIVEN, min = 0, max = 1, label = '$given'}, - #ref{name = vcard_MIDDLE, min = 0, max = 1, label = '$middle'}, - #ref{name = vcard_PREFIX, min = 0, max = 1, label = '$prefix'}, - #ref{name = vcard_SUFFIX, min = 0, max = 1, label = '$suffix'}]}). - --xml(vcard_ADR, - #elem{name = <<"ADR">>, - xmlns = <<"vcard-temp">>, - result = {vcard_adr, '$home', '$work', '$postal', '$parcel', - '$dom', '$intl', '$pref', '$pobox', '$extadd', '$street', - '$locality', '$region', '$pcode', '$ctry'}, - refs = [#ref{name = vcard_HOME, default = false, - min = 0, max = 1, label = '$home'}, - #ref{name = vcard_WORK, default = false, - min = 0, max = 1, label = '$work'}, - #ref{name = vcard_POSTAL, default = false, - min = 0, max = 1, label = '$postal'}, - #ref{name = vcard_PARCEL, default = false, - min = 0, max = 1, label = '$parcel'}, - #ref{name = vcard_DOM, default = false, - min = 0, max = 1, label = '$dom'}, - #ref{name = vcard_INTL, default = false, - min = 0, max = 1, label = '$intl'}, - #ref{name = vcard_PREF, default = false, - min = 0, max = 1, label = '$pref'}, - #ref{name = vcard_POBOX, min = 0, max = 1, label = '$pobox'}, - #ref{name = vcard_EXTADD, min = 0, max = 1, label = '$extadd'}, - #ref{name = vcard_STREET, min = 0, max = 1, label = '$street'}, - #ref{name = vcard_LOCALITY, min = 0, max = 1, label = '$locality'}, - #ref{name = vcard_REGION, min = 0, max = 1, label = '$region'}, - #ref{name = vcard_PCODE, min = 0, max = 1, label = '$pcode'}, - #ref{name = vcard_CTRY, min = 0, max = 1, label = '$ctry'}]}). - --xml(vcard_LABEL, - #elem{name = <<"LABEL">>, - xmlns = <<"vcard-temp">>, - result = {vcard_label, '$home', '$work', '$postal', '$parcel', - '$dom', '$intl', '$pref', '$line'}, - refs = [#ref{name = vcard_HOME, default = false, - min = 0, max = 1, label = '$home'}, - #ref{name = vcard_WORK, default = false, - min = 0, max = 1, label = '$work'}, - #ref{name = vcard_POSTAL, default = false, - min = 0, max = 1, label = '$postal'}, - #ref{name = vcard_PARCEL, default = false, - min = 0, max = 1, label = '$parcel'}, - #ref{name = vcard_DOM, default = false, - min = 0, max = 1, label = '$dom'}, - #ref{name = vcard_INTL, default = false, - min = 0, max = 1, label = '$intl'}, - #ref{name = vcard_PREF, default = false, - min = 0, max = 1, label = '$pref'}, - #ref{name = vcard_LINE, label = '$line'}]}). - --xml(vcard_TEL, - #elem{name = <<"TEL">>, - xmlns = <<"vcard-temp">>, - result = {vcard_tel, '$home', '$work', '$voice', '$fax', - '$pager', '$msg', '$cell', '$video', '$bbs', - '$modem', '$isdn', '$pcs', '$pref', '$number'}, - refs = [#ref{name = vcard_HOME, default = false, - min = 0, max = 1, label = '$home'}, - #ref{name = vcard_WORK, default = false, - min = 0, max = 1, label = '$work'}, - #ref{name = vcard_VOICE, default = false, - min = 0, max = 1, label = '$voice'}, - #ref{name = vcard_FAX, default = false, - min = 0, max = 1, label = '$fax'}, - #ref{name = vcard_PAGER, default = false, - min = 0, max = 1, label = '$pager'}, - #ref{name = vcard_MSG, default = false, - min = 0, max = 1, label = '$msg'}, - #ref{name = vcard_CELL, default = false, - min = 0, max = 1, label = '$cell'}, - #ref{name = vcard_VIDEO, default = false, - min = 0, max = 1, label = '$video'}, - #ref{name = vcard_BBS, default = false, - min = 0, max = 1, label = '$bbs'}, - #ref{name = vcard_MODEM, default = false, - min = 0, max = 1, label = '$modem'}, - #ref{name = vcard_ISDN, default = false, - min = 0, max = 1, label = '$isdn'}, - #ref{name = vcard_PCS, default = false, - min = 0, max = 1, label = '$pcs'}, - #ref{name = vcard_PREF, default = false, - min = 0, max = 1, label = '$pref'}, - #ref{name = vcard_NUMBER, - min = 0, max = 1, label = '$number'}]}). - --xml(vcard_EMAIL, - #elem{name = <<"EMAIL">>, - xmlns = <<"vcard-temp">>, - result = {vcard_email, '$home', '$work', - '$internet', '$pref', '$x400', '$userid'}, - refs = [#ref{name = vcard_HOME, default = false, - min = 0, max = 1, label = '$home'}, - #ref{name = vcard_WORK, default = false, - min = 0, max = 1, label = '$work'}, - #ref{name = vcard_INTERNET, default = false, - min = 0, max = 1, label = '$internet'}, - #ref{name = vcard_PREF, default = false, - min = 0, max = 1, label = '$pref'}, - #ref{name = vcard_X400, default = false, - min = 0, max = 1, label = '$x400'}, - #ref{name = vcard_USERID, - min = 0, max = 1, label = '$userid'}]}). - --xml(vcard_GEO, - #elem{name = <<"GEO">>, - xmlns = <<"vcard-temp">>, - result = {vcard_geo, '$lat', '$lon'}, - refs = [#ref{name = vcard_LAT, min = 0, max = 1, label = '$lat'}, - #ref{name = vcard_LON, min = 0, max = 1, label = '$lon'}]}). - --xml(vcard_BINVAL, - #elem{name = <<"BINVAL">>, - xmlns = <<"vcard-temp">>, - cdata = #cdata{dec = {base64, decode, []}, - enc = {base64, encode, []}}, - result = '$cdata'}). - --xml(vcard_LOGO, - #elem{name = <<"LOGO">>, - xmlns = <<"vcard-temp">>, - result = {vcard_logo, '$type', '$binval', '$extval'}, - refs = [#ref{name = vcard_TYPE, min = 0, max = 1, label = '$type'}, - #ref{name = vcard_BINVAL, min = 0, max = 1, label = '$binval'}, - #ref{name = vcard_EXTVAL, min = 0, max = 1, label = '$extval'}]}). - --xml(vcard_PHOTO, - #elem{name = <<"PHOTO">>, - xmlns = <<"vcard-temp">>, - result = {vcard_photo, '$type', '$binval', '$extval'}, - refs = [#ref{name = vcard_TYPE, min = 0, max = 1, label = '$type'}, - #ref{name = vcard_BINVAL, min = 0, max = 1, label = '$binval'}, - #ref{name = vcard_EXTVAL, min = 0, max = 1, label = '$extval'}]}). - --xml(vcard_ORG, - #elem{name = <<"ORG">>, - xmlns = <<"vcard-temp">>, - result = {vcard_org, '$name', '$units'}, - refs = [#ref{name = vcard_ORGNAME, - label = '$name', - min = 0, max = 1}, - #ref{name = vcard_ORGUNIT, - label = '$units'}]}). - --xml(vcard_SOUND, - #elem{name = <<"SOUND">>, - xmlns = <<"vcard-temp">>, - result = {vcard_sound, '$phonetic', '$binval', '$extval'}, - refs = [#ref{name = vcard_BINVAL, min = 0, max = 1, label = '$binval'}, - #ref{name = vcard_EXTVAL, min = 0, max = 1, label = '$extval'}, - #ref{name = vcard_PHONETIC, min = 0, max = 1, label = '$phonetic'}]}). - --xml(vcard_KEY, - #elem{name = <<"KEY">>, - xmlns = <<"vcard-temp">>, - result = {vcard_key, '$type', '$cred'}, - refs = [#ref{name = vcard_TYPE, min = 0, max = 1, label = '$type'}, - #ref{name = vcard_CRED, min = 0, max = 1, label = '$cred'}]}). - --xml(vcard_CATEGORIES, - #elem{name = <<"CATEGORIES">>, - xmlns = <<"vcard-temp">>, - result = '$keywords', - refs = [#ref{name = vcard_KEYWORD, label = '$keywords'}]}). - --xml(vcard_CLASS, - #elem{name = <<"CLASS">>, - xmlns = <<"vcard-temp">>, - result = '$class', - refs = [#ref{name = vcard_PUBLIC, min = 0, max = 1, label = '$class'}, - #ref{name = vcard_PRIVATE, min = 0, max = 1, label = '$class'}, - #ref{name = vcard_CONFIDENTIAL, min = 0, max = 1, label = '$class'}]}). - -%% {vcard_AGENT, -%% #elem{name = <<"AGENT">>, -%% xmlns = <<"vcard-temp">>, -%% result = {vcard_agent, '$vcard', '$extval'}, -%% refs = [#ref{name = vcard, min = 0, max = 1, label = '$vcard'}, -%% #ref{name = vcard_EXTVAL, min = 0, max = 1, label = '$extval'}]}). - --xml(vcard, - #elem{name = <<"vCard">>, - xmlns = <<"vcard-temp">>, - result = {vcard, '$version', '$fn', '$n', '$nickname', '$photo', - '$bday', '$adr', '$label', '$tel', '$email', '$jabberid', - '$mailer', '$tz', '$geo', '$title', '$role', '$logo', - '$org', '$categories', '$note', '$prodid', %% '$agent', - '$rev', '$sort_string', '$sound', '$uid', '$url', '$class', - '$key', '$desc'}, - refs = [#ref{name = vcard_N, min = 0, max = 1, label = '$n'}, - #ref{name = vcard_ADR, label = '$adr'}, - #ref{name = vcard_LABEL, label = '$label'}, - #ref{name = vcard_TEL, label = '$tel'}, - #ref{name = vcard_EMAIL, label = '$email'}, - #ref{name = vcard_GEO, min = 0, max = 1, label = '$geo'}, - #ref{name = vcard_LOGO, min = 0, max = 1, label = '$logo'}, - #ref{name = vcard_PHOTO, min = 0, max = 1, label = '$photo'}, - #ref{name = vcard_ORG, min = 0, max = 1, label = '$org'}, - #ref{name = vcard_SOUND, min = 0, max = 1, label = '$sound'}, - #ref{name = vcard_KEY, min = 0, max = 1, label = '$key'}, - #ref{name = vcard_VERSION, min = 0, max = 1, label = '$version'}, - #ref{name = vcard_FN, min = 0, max = 1, label = '$fn'}, - #ref{name = vcard_NICKNAME, min = 0, max = 1, label = '$nickname'}, - #ref{name = vcard_BDAY, min = 0, max = 1, label = '$bday'}, - #ref{name = vcard_JABBERID, min = 0, max = 1, label = '$jabberid'}, - #ref{name = vcard_MAILER, min = 0, max = 1, label = '$mailer'}, - #ref{name = vcard_TZ, min = 0, max = 1, label = '$tz'}, - #ref{name = vcard_TITLE, min = 0, max = 1, label = '$title'}, - #ref{name = vcard_ROLE, min = 0, max = 1, label = '$role'}, - #ref{name = vcard_NOTE, min = 0, max = 1, label = '$note'}, - #ref{name = vcard_PRODID, min = 0, max = 1, label = '$prodid'}, - #ref{name = vcard_REV, min = 0, max = 1, label = '$rev'}, - %%#ref{name = vcard_AGENT, min = 0, max = 1, label = '$agent'}, - #ref{name = vcard_SORT_STRING, min = 0, max = 1, - label = '$sort_string'}, - #ref{name = vcard_UID, min = 0, max = 1, label = '$uid'}, - #ref{name = vcard_URL, min = 0, max = 1, label = '$url'}, - #ref{name = vcard_DESC, min = 0, max = 1, label = '$desc'}, - #ref{name = vcard_CATEGORIES, default = [], min = 0, max = 1, - label = '$categories'}, - #ref{name = vcard_CLASS, min = 0, max = 1, label = '$class'}]}). - --xml(vcard_xupdate_photo, - #elem{name = <<"photo">>, - xmlns = <<"vcard-temp:x:update">>, - result = '$cdata'}). - --xml(vcard_xupdate, - #elem{name = <<"x">>, - xmlns = <<"vcard-temp:x:update">>, - result = {vcard_xupdate, '$photo'}, - refs = [#ref{name = vcard_xupdate_photo, min = 0, max = 1, - label = '$photo'}]}). - --xml(xdata_field_required, - #elem{name = <<"required">>, - xmlns = <<"jabber:x:data">>, - result = true}). - --xml(xdata_field_desc, - #elem{name = <<"desc">>, xmlns = <<"jabber:x:data">>, result = '$cdata'}). - --xml(xdata_field_value, - #elem{name = <<"value">>, xmlns = <<"jabber:x:data">>, result = '$cdata'}). - --xml(xdata_field_option, - #elem{name = <<"option">>, - xmlns = <<"jabber:x:data">>, - result = '$value', - refs = [#ref{name = xdata_field_value, - label = '$value', - min = 1, max = 1}]}). - --xml(xdata_field, - #elem{name = <<"field">>, - xmlns = <<"jabber:x:data">>, - result = {xdata_field, '$label', '$type', '$var', - '$required', '$desc', '$values', '$options'}, - attrs = [#attr{name = <<"label">>}, - #attr{name = <<"type">>, - enc = {enc_enum, []}, - dec = {dec_enum, [['boolean', - 'fixed', - 'hidden', - 'jid-multi', - 'jid-single', - 'list-multi', - 'list-single', - 'text-multi', - 'text-private', - 'text-single']]}}, - #attr{name = <<"var">>}], - refs = [#ref{name = xdata_field_required, - label = '$required', - default = false, - min = 0, max = 1}, - #ref{name = xdata_field_desc, - label = '$desc', - min = 0, max = 1}, - #ref{name = xdata_field_value, - label = '$values'}, - #ref{name = xdata_field_option, - label = '$options'}]}). - --xml(xdata_instructions, #elem{name = <<"instructions">>, - xmlns = <<"jabber:x:data">>, - result = '$cdata'}). --xml(xdata_title, #elem{name = <<"title">>, - xmlns = <<"jabber:x:data">>, - result = '$cdata'}). --xml(xdata_reported, #elem{name = <<"reported">>, - xmlns = <<"jabber:x:data">>, - result = '$fields', - refs = [#ref{name = xdata_field, - label = '$fields'}]}). --xml(xdata_item, #elem{name = <<"item">>, - xmlns = <<"jabber:x:data">>, - result = '$fields', - refs = [#ref{name = xdata_field, - label = '$fields'}]}). - --xml(xdata, - #elem{name = <<"x">>, - xmlns = <<"jabber:x:data">>, - result = {xdata, '$type', '$instructions', '$title', - '$reported', '$items', '$fields'}, - attrs = [#attr{name = <<"type">>, - required = true, - dec = {dec_enum, [[cancel, form, result, submit]]}, - enc = {enc_enum, []}}], - refs = [#ref{name = xdata_instructions, - label = '$instructions'}, - #ref{name = xdata_title, - label = '$title', - min = 0, max = 1}, - #ref{name = xdata_reported, - label = '$reported', - min = 0, max = 1}, - #ref{name = xdata_item, - label = '$items'}, - #ref{name = xdata_field, - label = '$fields'}]}). - --xml(pubsub_subscription, - #elem{name = <<"subscription">>, - xmlns = <<"http://jabber.org/protocol/pubsub">>, - result = {pubsub_subscription, '$jid', '$node', '$subid', - '$type'}, - attrs = [#attr{name = <<"jid">>, - required = true, - dec = {dec_jid, []}, - enc = {enc_jid, []}}, - #attr{name = <<"node">>}, - #attr{name = <<"subid">>}, - #attr{name = <<"subscription">>, - label = '$type', - dec = {dec_enum, [[none, pending, subscribed, - unconfigured]]}, - enc = {enc_enum, []}}]}). - --xml(pubsub_affiliation, - #elem{name = <<"affiliation">>, - xmlns = <<"http://jabber.org/protocol/pubsub">>, - result = {pubsub_affiliation, '$node', '$type'}, - attrs = [#attr{name = <<"node">>, - required = true}, - #attr{name = <<"affiliation">>, - label = '$type', - required = true, - dec = {dec_enum, [[member, none, outcast, owner, - publisher, 'publish-only']]}, - enc = {enc_enum, []}}]}). - --xml(pubsub_item, - #elem{name = <<"item">>, - xmlns = <<"http://jabber.org/protocol/pubsub">>, - result = {pubsub_item, '$id', '$_xmls'}, - attrs = [#attr{name = <<"id">>}]}). - --xml(pubsub_items, - #elem{name = <<"items">>, - xmlns = <<"http://jabber.org/protocol/pubsub">>, - result = {pubsub_items, '$node', '$max_items', - '$subid', '$items'}, - attrs = [#attr{name = <<"max_items">>, - dec = {dec_int, [0, infinity]}, - enc = {enc_int, []}}, - #attr{name = <<"node">>, - required = true}, - #attr{name = <<"subid">>}], - refs = [#ref{name = pubsub_item, label = '$items'}]}). - --xml(pubsub_event_retract, - #elem{name = <<"retract">>, - xmlns = <<"http://jabber.org/protocol/pubsub#event">>, - result = '$id', - attrs = [#attr{name = <<"id">>, required = true}]}). - --xml(pubsub_event_item, - #elem{name = <<"item">>, - xmlns = <<"http://jabber.org/protocol/pubsub#event">>, - result = {pubsub_event_item, '$id', '$node', '$publisher'}, - attrs = [#attr{name = <<"id">>}, - #attr{name = <<"node">>}, - #attr{name = <<"publisher">>}]}). - --xml(pubsub_event_items, - #elem{name = <<"items">>, - xmlns = <<"http://jabber.org/protocol/pubsub#event">>, - result = {pubsub_event_items, '$node', '$retract', '$items'}, - attrs = [#attr{name = <<"node">>, - required = true}], - refs = [#ref{name = pubsub_event_retract, label = '$retract'}, - #ref{name = pubsub_event_item, label = '$items'}]}). - --xml(pubsub_event, - #elem{name = <<"event">>, - xmlns = <<"http://jabber.org/protocol/pubsub#event">>, - result = {pubsub_event, '$items'}, - refs = [#ref{name = pubsub_event_items, label = '$items'}]}). - --xml(pubsub_subscriptions, - #elem{name = <<"subscriptions">>, - xmlns = <<"http://jabber.org/protocol/pubsub">>, - result = {'$node', '$subscriptions'}, - attrs = [#attr{name = <<"node">>, - default = none}], - refs = [#ref{name = pubsub_subscription, label = '$subscriptions'}]}). - --xml(pubsub_affiliations, - #elem{name = <<"affiliations">>, - xmlns = <<"http://jabber.org/protocol/pubsub">>, - result = '$affiliations', - refs = [#ref{name = pubsub_affiliation, label = '$affiliations'}]}). - --xml(pubsub_subscribe, - #elem{name = <<"subscribe">>, - xmlns = <<"http://jabber.org/protocol/pubsub">>, - result = {pubsub_subscribe, '$node', '$jid'}, - attrs = [#attr{name = <<"node">>}, - #attr{name = <<"jid">>, - required = true, - dec = {dec_jid, []}, - enc = {enc_jid, []}}]}). - --xml(pubsub_unsubscribe, - #elem{name = <<"unsubscribe">>, - xmlns = <<"http://jabber.org/protocol/pubsub">>, - result = {pubsub_unsubscribe, '$node', '$jid', '$subid'}, - attrs = [#attr{name = <<"node">>}, - #attr{name = <<"subid">>}, - #attr{name = <<"jid">>, - required = true, - dec = {dec_jid, []}, - enc = {enc_jid, []}}]}). - --xml(pubsub_publish, - #elem{name = <<"publish">>, - xmlns = <<"http://jabber.org/protocol/pubsub">>, - result = {pubsub_publish, '$node', '$items'}, - attrs = [#attr{name = <<"node">>, - required = true}], - refs = [#ref{name = pubsub_item, label = '$items'}]}). - --xml(pubsub_options, - #elem{name = <<"options">>, - xmlns = <<"http://jabber.org/protocol/pubsub">>, - result = {pubsub_options, '$node', '$jid', '$subid', '$xdata'}, - attrs = [#attr{name = <<"node">>}, - #attr{name = <<"subid">>}, - #attr{name = <<"jid">>, - required = true, - dec = {dec_jid, []}, - enc = {enc_jid, []}}], - refs = [#ref{name = xdata, min = 0, max = 1, - label = '$xdata'}]}). - --xml(pubsub_retract, - #elem{name = <<"retract">>, - xmlns = <<"http://jabber.org/protocol/pubsub">>, - result = {pubsub_retract, '$node', '$notify', '$items'}, - attrs = [#attr{name = <<"node">>, - required = true}, - #attr{name = <<"notify">>, - default = false, - dec = {dec_bool, []}, - enc = {enc_bool, []}}], - refs = [#ref{name = pubsub_item, label = '$items'}]}). - --xml(pubsub, - #elem{name = <<"pubsub">>, - xmlns = <<"http://jabber.org/protocol/pubsub">>, - result = {pubsub, '$subscriptions', '$affiliations', '$publish', - '$subscribe', '$unsubscribe', '$options', '$items', - '$retract'}, - refs = [#ref{name = pubsub_subscriptions, label = '$subscriptions', - min = 0, max = 1}, - #ref{name = pubsub_affiliations, label = '$affiliations', - min = 0, max = 1}, - #ref{name = pubsub_subscribe, label = '$subscribe', - min = 0, max = 1}, - #ref{name = pubsub_unsubscribe, label = '$unsubscribe', - min = 0, max = 1}, - #ref{name = pubsub_options, label = '$options', - min = 0, max = 1}, - #ref{name = pubsub_items, label = '$items', - min = 0, max = 1}, - #ref{name = pubsub_retract, label = '$retract', - min = 0, max = 1}, - #ref{name = pubsub_publish, label = '$publish', - min = 0, max = 1}]}). - --xml(shim_header, - #elem{name = <<"header">>, - xmlns = <<"http://jabber.org/protocol/shim">>, - result = {'$name', '$cdata'}, - attrs = [#attr{name = <<"name">>, - required = true}]}). - --xml(shim_headers, - #elem{name = <<"headers">>, - xmlns = <<"http://jabber.org/protocol/shim">>, - result = {shim, '$headers'}, - refs = [#ref{name = shim_header, label = '$headers'}]}). - --record(chatstate, {type :: active | composing | gone | inactive | paused}). - --xml(chatstate_active, - #elem{name = <<"active">>, - xmlns = <<"http://jabber.org/protocol/chatstates">>, - result = {chatstate, active}}). - --xml(chatstate_composing, - #elem{name = <<"composing">>, - xmlns = <<"http://jabber.org/protocol/chatstates">>, - result = {chatstate, composing}}). - --xml(chatstate_gone, - #elem{name = <<"gone">>, - xmlns = <<"http://jabber.org/protocol/chatstates">>, - result = {chatstate, gone}}). - --xml(chatstate_inactive, - #elem{name = <<"inactive">>, - xmlns = <<"http://jabber.org/protocol/chatstates">>, - result = {chatstate, inactive}}). - --xml(chatstate_paused, - #elem{name = <<"paused">>, - xmlns = <<"http://jabber.org/protocol/chatstates">>, - result = {chatstate, paused}}). - --xml(delay, - #elem{name = <<"delay">>, - xmlns = <<"urn:xmpp:delay">>, - result = {delay, '$stamp', '$from'}, - attrs = [#attr{name = <<"stamp">>, - required = true, - dec = {dec_utc, []}, - enc = {enc_utc, []}}, - #attr{name = <<"from">>, - dec = {dec_jid, []}, - enc = {enc_jid, []}}]}). - --xml(legacy_delay, - #elem{name = <<"x">>, - xmlns = <<"jabber:x:delay">>, - result = {legacy_delay, '$stamp', '$from'}, - attrs = [#attr{name = <<"stamp">>, - required = true}, - #attr{name = <<"from">>, - dec = {dec_jid, []}, - enc = {enc_jid, []}}]}). - --xml(bytestreams_streamhost, - #elem{name = <<"streamhost">>, - xmlns = <<"http://jabber.org/protocol/bytestreams">>, - result = {streamhost, '$jid', '$host', '$port'}, - attrs = [#attr{name = <<"jid">>, - required = true, - dec = {dec_jid, []}, - enc = {enc_jid, []}}, - #attr{name = <<"host">>, - required = true}, - #attr{name = <<"port">>, - default = 1080, - dec = {dec_int, [0, 65535]}, - enc = {enc_int, []}}]}). - --xml(bytestreams_streamhost_used, - #elem{name = <<"streamhost-used">>, - xmlns = <<"http://jabber.org/protocol/bytestreams">>, - result = '$jid', - attrs = [#attr{name = <<"jid">>, - required = true, - dec = {dec_jid, []}, - enc = {enc_jid, []}}]}). - --xml(bytestreams_activate, - #elem{name = <<"activate">>, - xmlns = <<"http://jabber.org/protocol/bytestreams">>, - cdata = #cdata{enc = {enc_jid, []}, dec = {dec_jid, []}}, - result = '$cdata'}). - --xml(bytestreams, - #elem{name = <<"query">>, - xmlns = <<"http://jabber.org/protocol/bytestreams">>, - result = {bytestreams, '$hosts', '$used', '$activate', - '$dstaddr', '$mode', '$sid'}, - attrs = [#attr{name = <<"dstaddr">>}, - #attr{name = <<"sid">>}, - #attr{name = <<"mode">>, - default = tcp, - dec = {dec_enum, [[tcp, udp]]}, - enc = {enc_enum, []}}], - refs = [#ref{name = bytestreams_streamhost, label = '$hosts'}, - #ref{name = bytestreams_streamhost_used, - min = 0, max = 1, label = '$used'}, - #ref{name = bytestreams_activate, - min = 0, max = 1, label = '$activate'}]}). - --xml(muc_history, - #elem{name = <<"history">>, - xmlns = <<"http://jabber.org/protocol/muc">>, - result = {muc_history, '$maxchars', '$maxstanzas', - '$seconds', '$since'}, - attrs = [#attr{name = <<"maxchars">>, - dec = {dec_int, [0, infinity]}, - enc = {enc_int, []}}, - #attr{name = <<"maxstanzas">>, - dec = {dec_int, [0, infinity]}, - enc = {enc_int, []}}, - #attr{name = <<"seconds">>, - dec = {dec_int, [0, infinity]}, - enc = {enc_int, []}}, - #attr{name = <<"since">>, - dec = {dec_utc, []}, - enc = {enc_utc, []}}]}). - --xml(muc_user_reason, - #elem{name = <<"reason">>, - xmlns = <<"http://jabber.org/protocol/muc#user">>, - result = '$cdata'}). - --xml(muc_user_decline, - #elem{name = <<"decline">>, - xmlns = <<"http://jabber.org/protocol/muc#user">>, - result = {muc_decline, '$reason', '$from', '$to'}, - attrs = [#attr{name = <<"to">>, - dec = {dec_jid, []}, - enc = {enc_jid, []}}, - #attr{name = <<"from">>, - dec = {dec_jid, []}, - enc = {enc_jid, []}}], - refs = [#ref{name = muc_user_reason, min = 0, - max = 1, label = '$reason'}]}). - --xml(muc_user_destroy, - #elem{name = <<"destroy">>, - xmlns = <<"http://jabber.org/protocol/muc#user">>, - result = {muc_user_destroy, '$reason', '$jid'}, - attrs = [#attr{name = <<"jid">>, - dec = {dec_jid, []}, - enc = {enc_jid, []}}], - refs = [#ref{name = muc_user_reason, min = 0, - max = 1, label = '$reason'}]}). - --xml(muc_user_invite, - #elem{name = <<"invite">>, - xmlns = <<"http://jabber.org/protocol/muc#user">>, - result = {muc_invite, '$reason', '$from', '$to'}, - attrs = [#attr{name = <<"to">>, - dec = {dec_jid, []}, - enc = {enc_jid, []}}, - #attr{name = <<"from">>, - dec = {dec_jid, []}, - enc = {enc_jid, []}}], - refs = [#ref{name = muc_user_reason, min = 0, - max = 1, label = '$reason'}]}). - --xml(muc_user_actor, - #elem{name = <<"actor">>, - xmlns = <<"http://jabber.org/protocol/muc#user">>, - result = {muc_actor, '$jid', '$nick'}, - attrs = [#attr{name = <<"jid">>, - dec = {dec_jid, []}, - enc = {enc_jid, []}}, - #attr{name = <<"nick">>}]}). - --xml(muc_user_continue, - #elem{name = <<"continue">>, - xmlns = <<"http://jabber.org/protocol/muc#user">>, - result = '$thread', - attrs = [#attr{name = <<"thread">>}]}). - --xml(muc_user_status, - #elem{name = <<"status">>, - xmlns = <<"http://jabber.org/protocol/muc#user">>, - result = '$code', - attrs = [#attr{name = <<"code">>, - dec = {dec_int, [100, 999]}, - enc = {enc_int, []}}]}). - --xml(muc_user_item, - #elem{name = <<"item">>, - xmlns = <<"http://jabber.org/protocol/muc#user">>, - result = {muc_item, '$actor', '$continue', '$reason', - '$affiliation', '$role', '$jid', '$nick'}, - refs = [#ref{name = muc_user_actor, - min = 0, max = 1, label = '$actor'}, - #ref{name = muc_user_continue, - min = 0, max = 1, label = '$continue'}, - #ref{name = muc_user_reason, - min = 0, max = 1, label = '$reason'}], - attrs = [#attr{name = <<"affiliation">>, - dec = {dec_enum, [[admin, member, none, - outcast, owner]]}, - enc = {enc_enum, []}}, - #attr{name = <<"role">>, - dec = {dec_enum, [[moderator, none, - participant, visitor]]}, - enc = {enc_enum, []}}, - #attr{name = <<"jid">>, - dec = {dec_jid, []}, - enc = {enc_jid, []}}, - #attr{name = <<"nick">>}]}). - --xml(muc_user, - #elem{name = <<"x">>, - xmlns = <<"http://jabber.org/protocol/muc#user">>, - result = {muc_user, '$decline', '$destroy', '$invites', - '$items', '$status_codes', '$password'}, - attrs = [#attr{name = <<"password">>}], - refs = [#ref{name = muc_user_decline, min = 0, - max = 1, label = '$decline'}, - #ref{name = muc_user_destroy, min = 0, max = 1, - label = '$destroy'}, - #ref{name = muc_user_invite, label = '$invites'}, - #ref{name = muc_user_item, label = '$items'}, - #ref{name = muc_user_status, label = '$status_codes'}]}). - --xml(muc_owner_password, - #elem{name = <<"password">>, - xmlns = <<"http://jabber.org/protocol/muc#owner">>, - result = '$cdata'}). - --xml(muc_owner_reason, - #elem{name = <<"reason">>, - xmlns = <<"http://jabber.org/protocol/muc#owner">>, - result = '$cdata'}). - --xml(muc_owner_destroy, - #elem{name = <<"destroy">>, - xmlns = <<"http://jabber.org/protocol/muc#owner">>, - result = {muc_owner_destroy, '$jid', '$reason', '$password'}, - attrs = [#attr{name = <<"jid">>, - dec = {dec_jid, []}, - enc = {enc_jid, []}}], - refs = [#ref{name = muc_owner_password, min = 0, max = 1, - label = '$password'}, - #ref{name = muc_owner_reason, min = 0, max = 1, - label = '$reason'}]}). - --xml(muc_owner, - #elem{name = <<"query">>, - xmlns = <<"http://jabber.org/protocol/muc#owner">>, - result = {muc_owner, '$destroy', '$config'}, - refs = [#ref{name = muc_owner_destroy, min = 0, max = 1, - label = '$destroy'}, - #ref{name = xdata, min = 0, max = 1, label = '$config'}]}). - --xml(muc_admin_item, - #elem{name = <<"item">>, - xmlns = <<"http://jabber.org/protocol/muc#admin">>, - result = {muc_item, '$actor', '$continue', '$reason', - '$affiliation', '$role', '$jid', '$nick'}, - refs = [#ref{name = muc_admin_actor, - min = 0, max = 1, label = '$actor'}, - #ref{name = muc_admin_continue, - min = 0, max = 1, label = '$continue'}, - #ref{name = muc_admin_reason, - min = 0, max = 1, label = '$reason'}], - attrs = [#attr{name = <<"affiliation">>, - dec = {dec_enum, [[admin, member, none, - outcast, owner]]}, - enc = {enc_enum, []}}, - #attr{name = <<"role">>, - dec = {dec_enum, [[moderator, none, - participant, visitor]]}, - enc = {enc_enum, []}}, - #attr{name = <<"jid">>, - dec = {dec_jid, []}, - enc = {enc_jid, []}}, - #attr{name = <<"nick">>}]}). - --xml(muc_admin_actor, - #elem{name = <<"actor">>, - xmlns = <<"http://jabber.org/protocol/muc#admin">>, - result = {muc_actor, '$jid', '$nick'}, - attrs = [#attr{name = <<"jid">>, - dec = {dec_jid, []}, - enc = {enc_jid, []}}, - #attr{name = <<"nick">>}]}). - --xml(muc_admin_continue, - #elem{name = <<"continue">>, - xmlns = <<"http://jabber.org/protocol/muc#admin">>, - result = '$thread', - attrs = [#attr{name = <<"thread">>}]}). - --xml(muc_admin_reason, - #elem{name = <<"reason">>, - xmlns = <<"http://jabber.org/protocol/muc#admin">>, - result = '$cdata'}). - --xml(muc_admin, - #elem{name = <<"query">>, - xmlns = <<"http://jabber.org/protocol/muc#admin">>, - result = {muc_admin, '$items'}, - refs = [#ref{name = muc_admin_item, label = '$items'}]}). - --xml(muc, - #elem{name = <<"x">>, - xmlns = <<"http://jabber.org/protocol/muc">>, - result = {muc, '$history', '$password'}, - attrs = [#attr{name = <<"password">>}], - refs = [#ref{name = muc_history, min = 0, max = 1, - label = '$history'}]}). - --xml(forwarded, - #elem{name = <<"forwarded">>, - xmlns = <<"urn:xmpp:forward:0">>, - result = {forwarded, '$delay', '$_els'}, - refs = [#ref{name = delay, min = 0, - max = 1, label = '$delay'}]}). - --xml(carbons_disable, - #elem{name = <<"disable">>, - xmlns = <<"urn:xmpp:carbons:2">>, - result = {carbons_disable}}). - --xml(carbons_enable, - #elem{name = <<"enable">>, - xmlns = <<"urn:xmpp:carbons:2">>, - result = {carbons_enable}}). - --xml(carbons_private, - #elem{name = <<"private">>, - xmlns = <<"urn:xmpp:carbons:2">>, - result = {carbons_private}}). - --xml(carbons_received, - #elem{name = <<"received">>, - xmlns = <<"urn:xmpp:carbons:2">>, - result = {carbons_received, '$forwarded'}, - refs = [#ref{name = forwarded, min = 1, - max = 1, label = '$forwarded'}]}). - --xml(carbons_sent, - #elem{name = <<"sent">>, - xmlns = <<"urn:xmpp:carbons:2">>, - result = {carbons_sent, '$forwarded'}, - refs = [#ref{name = forwarded, min = 1, - max = 1, label = '$forwarded'}]}). - --xml(feature_csi, - #elem{name = <<"csi">>, - xmlns = <<"urn:xmpp:csi:0">>, - result = {feature_csi, '$xmlns'}, - attrs = [#attr{name = <<"xmlns">>}]}). - --record(csi, {type :: active | inactive}). - --xml(csi_active, - #elem{name = <<"active">>, - xmlns = <<"urn:xmpp:csi:0">>, - result = {csi, active}}). - --xml(csi_inactive, - #elem{name = <<"inactive">>, - xmlns = <<"urn:xmpp:csi:0">>, - result = {csi, inactive}}). - --xml(feature_sm, - #elem{name = <<"sm">>, - xmlns = [<<"urn:xmpp:sm:2">>, <<"urn:xmpp:sm:3">>], - result = {feature_sm, '$xmlns'}, - attrs = [#attr{name = <<"xmlns">>}]}). - --xml(sm_enable, - #elem{name = <<"enable">>, - xmlns = [<<"urn:xmpp:sm:2">>, <<"urn:xmpp:sm:3">>], - result = {sm_enable, '$max', '$resume', '$xmlns'}, - attrs = [#attr{name = <<"max">>, - dec = {dec_int, [0, infinity]}, - enc = {enc_int, []}}, - #attr{name = <<"xmlns">>}, - #attr{name = <<"resume">>, - default = false, - dec = {dec_bool, []}, - enc = {enc_bool, []}}]}). - --xml(sm_enabled, - #elem{name = <<"enabled">>, - xmlns = [<<"urn:xmpp:sm:2">>, <<"urn:xmpp:sm:3">>], - result = {sm_enabled, '$id', '$location', '$max', '$resume', '$xmlns'}, - attrs = [#attr{name = <<"id">>}, - #attr{name = <<"location">>}, - #attr{name = <<"xmlns">>}, - #attr{name = <<"max">>, - dec = {dec_int, [0, infinity]}, - enc = {enc_int, []}}, - #attr{name = <<"resume">>, - default = false, - dec = {dec_bool, []}, - enc = {enc_bool, []}}]}). - --xml(sm_resume, - #elem{name = <<"resume">>, - xmlns = [<<"urn:xmpp:sm:2">>, <<"urn:xmpp:sm:3">>], - result = {sm_resume, '$h', '$previd', '$xmlns'}, - attrs = [#attr{name = <<"h">>, - required = true, - dec = {dec_int, [0, infinity]}, - enc = {enc_int, []}}, - #attr{name = <<"xmlns">>}, - #attr{name = <<"previd">>, - required = true}]}). - --xml(sm_resumed, - #elem{name = <<"resumed">>, - xmlns = [<<"urn:xmpp:sm:2">>, <<"urn:xmpp:sm:3">>], - result = {sm_resumed, '$h', '$previd', '$xmlns'}, - attrs = [#attr{name = <<"h">>, - required = true, - dec = {dec_int, [0, infinity]}, - enc = {enc_int, []}}, - #attr{name = <<"xmlns">>}, - #attr{name = <<"previd">>, - required = true}]}). - --xml(sm_r, - #elem{name = <<"r">>, - xmlns = [<<"urn:xmpp:sm:2">>, <<"urn:xmpp:sm:3">>], - result = {sm_r, '$xmlns'}, - attrs = [#attr{name = <<"xmlns">>}]}). - --xml(sm_a, - #elem{name = <<"a">>, - xmlns = [<<"urn:xmpp:sm:2">>, <<"urn:xmpp:sm:3">>], - result = {sm_a, '$h', '$xmlns'}, - attrs = [#attr{name = <<"h">>, - required = true, - dec = {dec_int, [0, infinity]}, - enc = {enc_int, []}}, - #attr{name = <<"xmlns">>}]}). - --xml(sm_failed, - #elem{name = <<"failed">>, - xmlns = [<<"urn:xmpp:sm:2">>, <<"urn:xmpp:sm:3">>], - result = {sm_failed, '$reason', '$xmlns'}, - attrs = [#attr{name = <<"xmlns">>}], - refs = [#ref{name = error_bad_request, - min = 0, max = 1, label = '$reason'}, - #ref{name = error_conflict, - min = 0, max = 1, label = '$reason'}, - #ref{name = error_feature_not_implemented, - min = 0, max = 1, label = '$reason'}, - #ref{name = error_forbidden, - min = 0, max = 1, label = '$reason'}, - #ref{name = error_gone, - min = 0, max = 1, label = '$reason'}, - #ref{name = error_internal_server_error, - min = 0, max = 1, label = '$reason'}, - #ref{name = error_item_not_found, - min = 0, max = 1, label = '$reason'}, - #ref{name = error_jid_malformed, - min = 0, max = 1, label = '$reason'}, - #ref{name = error_not_acceptable, - min = 0, max = 1, label = '$reason'}, - #ref{name = error_not_allowed, - min = 0, max = 1, label = '$reason'}, - #ref{name = error_not_authorized, - min = 0, max = 1, label = '$reason'}, - #ref{name = error_policy_violation, - min = 0, max = 1, label = '$reason'}, - #ref{name = error_recipient_unavailable, - min = 0, max = 1, label = '$reason'}, - #ref{name = error_redirect, - min = 0, max = 1, label = '$reason'}, - #ref{name = error_registration_required, - min = 0, max = 1, label = '$reason'}, - #ref{name = error_remote_server_not_found, - min = 0, max = 1, label = '$reason'}, - #ref{name = error_remote_server_timeout, - min = 0, max = 1, label = '$reason'}, - #ref{name = error_resource_constraint, - min = 0, max = 1, label = '$reason'}, - #ref{name = error_service_unavailable, - min = 0, max = 1, label = '$reason'}, - #ref{name = error_subscription_required, - min = 0, max = 1, label = '$reason'}, - #ref{name = error_undefined_condition, - min = 0, max = 1, label = '$reason'}, - #ref{name = error_unexpected_request, - min = 0, max = 1, label = '$reason'}]}). - -dec_tzo(Val) -> - [H1, M1] = str:tokens(Val, <<":">>), - H = jlib:binary_to_integer(H1), - M = jlib:binary_to_integer(M1), - if H >= -12, H =< 12, M >= 0, M < 60 -> - {H, M} - end. - -enc_tzo({H, M}) -> - Sign = if H >= 0 -> - <<>>; - true -> - <<"-">> - end, - list_to_binary([Sign, io_lib:format("~2..0w:~2..0w", [H, M])]). - -dec_utc(Val) -> - {_, _, _} = jlib:datetime_string_to_timestamp(Val). - -enc_utc(Val) -> - jlib:now_to_utc_string(Val). - -dec_jid(Val) -> - case jlib:string_to_jid(Val) of - error -> - erlang:error(badarg); - J -> - J - end. - -enc_jid(J) -> - jlib:jid_to_string(J). - -resourceprep(R) -> - case jlib:resourceprep(R) of - error -> - erlang:error(badarg); - R1 -> - R1 - end. - -dec_bool(<<"false">>) -> false; -dec_bool(<<"0">>) -> false; -dec_bool(<<"true">>) -> true; -dec_bool(<<"1">>) -> true. - -enc_bool(false) -> <<"false">>; -enc_bool(true) -> <<"true">>. - -%% Local Variables: -%% mode: erlang -%% End: -%% vim: set filetype=erlang tabstop=8: diff --git a/vars.config.in b/vars.config.in index 7621d1390..ded059b3e 100644 --- a/vars.config.in +++ b/vars.config.in @@ -1,50 +1,76 @@ -%%%------------------------------------------------------------------- -%%% @author Evgeniy Khramtsov <ekhramtsov@process-one.net> -%%% @copyright (C) 2013, Evgeniy Khramtsov -%%% @doc +%%%---------------------------------------------------------------------- %%% -%%% @end -%%% Created : 8 May 2013 by Evgeniy Khramtsov <ekhramtsov@process-one.net> -%%%------------------------------------------------------------------- +%%% ejabberd, Copyright (C) 2002-2025 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. +%%% +%%%---------------------------------------------------------------------- + %% Macros {roster_gateway_workaround, @roster_gateway_workaround@}. -{transient_supervisors, @transient_supervisors@}. {full_xml, @full_xml@}. -{nif, @nif@}. -{db_type, @db_type@}. {debug, @debug@}. -{hipe, @hipe@}. +{new_sql_schema, @new_sql_schema@}. -%% Ad-hoc directories with source files {tools, @tools@}. %% Dependencies {odbc, @odbc@}. +{mssql, @mssql@}. {mysql, @mysql@}. {pgsql, @pgsql@}. +{sqlite, @sqlite@}. {pam, @pam@}. {zlib, @zlib@}. -{riak, @riak@}. -{json, @json@}. +{redis, @redis@}. {elixir, @elixir@}. -{lager, @lager@}. -{iconv, @iconv@}. +{stun, @stun@}. +{sip, @sip@}. +{lua, @lua@}. %% Version {vsn, "@PACKAGE_VERSION@"}. %% Variables for overlay template files +{description, "@PACKAGE_NAME@"}. %% Platform-specific installation paths {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"}. +{erl, "{{erts_dir}}/bin/erl"}. +{epmd, "{{erts_dir}}/bin/epmd"}. +{iexpath, "{{release_dir}}/releases/{{vsn}}/iex"}. {localstatedir, "{{release_dir}}/var"}. {libdir, "{{release_dir}}/lib"}. {docdir, "{{release_dir}}/doc"}. +%% OTP release +{config_dir, "{{release_dir}}/conf"}. +{logs_dir, "{{release_dir}}/logs"}. +{spool_dir, "{{release_dir}}/database"}. + +{latest_deps, @latest_deps@}. +{system_deps, @system_deps@}. + +{ldflags, "@LDFLAGS@"}. +{cflags, "@CFLAGS@"}. +{cppflags, "@CPPFLAGS@"}. + %% Local Variables: %% mode: erlang %% End: diff --git a/win32/CheckReqs.ini b/win32/CheckReqs.ini deleted file mode 100644 index 35875f82d..000000000 --- a/win32/CheckReqs.ini +++ /dev/null @@ -1,20 +0,0 @@ -[Settings] -NumFields=2 - -[Field 1] -Type=label -Left=0 -Right=-1 -Top=10 -Bottom=70 -Text="Erlang OTP R10B-7 (version 5.4.9) or newer is required to install Ejabberd.\r\n\r\nIt is not found on your computer.\r\n\r\nPlease install Erlang OTP R10B-7 or newer before installing Ejabberd.\r\n\r\nIts installer can be downloaded from" - -[Field 2] -Type=link -Left=0 -Right=-1 -Top=74 -Bottom=88 -State=http://www.erlang.org/download.html -Text=http://www.erlang.org/download.html - diff --git a/win32/CheckReqs1.ini b/win32/CheckReqs1.ini deleted file mode 100644 index 76b8a9009..000000000 --- a/win32/CheckReqs1.ini +++ /dev/null @@ -1,28 +0,0 @@ -[Settings] -NumFields=3 - -[Field 1] -Type=label -Left=0 -Right=-1 -Top=10 -Bottom=70 -Text="OpenSLL 0.9.7i or newer is not found on your computer.\r\n\r\nTo use SSL and TLS encryption you need an SSL certificate. You can create a selfsigned certificate with OpenSSL.\r\n\r\nOpenSLL installer can be downloaded from" - -[Field 2] -Type=link -Left=0 -Right=-1 -Top=74 -Bottom=88 -State=http://www.slproweb.com/products/Win32OpenSSL.html -Text=http://www.slproweb.com/products/Win32OpenSSL.html - -[Field 3] -Type=label -Left=0 -Right=-1 -Top=93 -Bottom=-10 -Text="If you want to continue installing Ejabberd anyway, click Next." - diff --git a/win32/CheckReqs1H.ini b/win32/CheckReqs1H.ini deleted file mode 100644 index 36076640d..000000000 --- a/win32/CheckReqs1H.ini +++ /dev/null @@ -1,29 +0,0 @@ -[Settings] -NumFields=3 - -[Field 1] -Type=label -Left=0 -Right=-1 -Top=10 -Bottom=70 -Text="OpenSLL 0.9.7i or newer is not found on your computer.\r\n\r\nTo use SSL and TLS encryption you need an SSL certificate. You can create a selfsigned certificate with OpenSSL.\r\n\r\nOpenSLL installer can be downloaded from" - -[Field 2] -Type=link -Left=0 -Right=-1 -Top=74 -Bottom=88 -State=http://www.slproweb.com/products/Win32OpenSSL.html -Text=http://www.slproweb.com/products/Win32OpenSSL.html - -[Field 3] -Type=checkbox -Left=0 -Right=-1 -Top=93 -Bottom=105 -Text="I want to continue installing Ejabberd anyway" -State=0 -Flags=NOTIFY diff --git a/win32/CheckService.ini b/win32/CheckService.ini deleted file mode 100644 index 764832469..000000000 --- a/win32/CheckService.ini +++ /dev/null @@ -1,19 +0,0 @@ -[Settings] -NumFields=2 - -[Field 1] -Type=label -Left=0 -Right=-1 -Top=10 -Bottom=50 -Text="You are installing Ejabberd as Administrator.\r\n\r\nEjabberd will be installed as a Windows service." - -[Field 2] -Type=checkbox -Left=0 -Right=-1 -Top=50 -Bottom=62 -Text="Configure ejabberd service to start automatically" -State=1 diff --git a/win32/CheckUser.ini b/win32/CheckUser.ini deleted file mode 100644 index 3352d69d2..000000000 --- a/win32/CheckUser.ini +++ /dev/null @@ -1,11 +0,0 @@ -[Settings] -NumFields=1 - -[Field 1] -Type=label -Left=0 -Right=-1 -Top=10 -Bottom=-10 -Text="Administrator privileges are recommended for Ejabberd install.\r\n\r\nOtherwise installing Ejabberd as a service will be impossible.\r\n\r\nIf you want to continue installing Ejabberd anyway, click Next." - diff --git a/win32/CheckUserH.ini b/win32/CheckUserH.ini deleted file mode 100644 index 741bada22..000000000 --- a/win32/CheckUserH.ini +++ /dev/null @@ -1,20 +0,0 @@ -[Settings] -NumFields=2 - -[Field 1] -Type=label -Left=0 -Right=-1 -Top=10 -Bottom=50 -Text="Administrator privileges are recommended for Ejabberd install.\r\n\r\nOtherwise installing Ejabberd as a service will be impossible." - -[Field 2] -Type=checkbox -Left=0 -Right=-1 -Top=50 -Bottom=62 -Text="I want to continue installing Ejabberd anyway" -State=0 -Flags=NOTIFY diff --git a/win32/ejabberd.cfg b/win32/ejabberd.cfg deleted file mode 100644 index 7ba191be0..000000000 --- a/win32/ejabberd.cfg +++ /dev/null @@ -1,167 +0,0 @@ -% $Id$ - -%override_acls. - - -% Users that have admin access. Add line like one of the following after you -% will be successfully registered on server to get admin access: -%{acl, admin, {user, "aleksey"}}. -%{acl, admin, {user, "ermine"}}. - -% Blocked users: -%{acl, blocked, {user, "test"}}. - -% Local users: -{acl, local, {user_regexp, ""}}. - -% Another examples of ACLs: -%{acl, jabberorg, {server, "jabber.org"}}. -%{acl, aleksey, {user, "aleksey", "jabber.ru"}}. -%{acl, test, {user_regexp, "^test"}}. -%{acl, test, {user_glob, "test*"}}. - - -% Only admins can use configuration interface: -{access, configure, [{allow, admin}]}. - -% Every username can be registered via in-band registration: -{access, register, [{allow, all}]}. - -% After successful registration user will get message with following subject -% and body: -{welcome_message, - {"Welcome!", - "Welcome to Jabber Service. " - "For information about Jabber visit http://jabber.org"}}. -% Replace them with 'none' if you don't want to send such message: -%{welcome_message, none}. - -% List of people who will get notifications about registered users -%{registration_watchers, ["admin1@localhost", -% "admin2@localhost"]}. - -% Only admins can send announcement messages: -{access, announce, [{allow, admin}]}. - - -% Only non-blocked users can use c2s connections: -{access, c2s, [{deny, blocked}, - {allow, all}]}. - -% Set shaper with name "normal" to limit traffic speed to 1000B/s -{shaper, normal, {maxrate, 1000}}. - -% Set shaper with name "fast" to limit traffic speed to 50000B/s -{shaper, fast, {maxrate, 50000}}. - -% For all users except admins used "normal" shaper -{access, c2s_shaper, [{none, admin}, - {normal, all}]}. - -% For all S2S connections used "fast" shaper -{access, s2s_shaper, [{fast, all}]}. - -% Admins of this server are also admins of MUC service: -{access, muc_admin, [{allow, admin}]}. - -% All users are allowed to use MUC service: -{access, muc, [{allow, all}]}. - -% This rule allows access only for local users: -{access, local, [{allow, local}]}. - - -% Authentification method. If you want to use internal user base, then use -% this line: -{auth_method, internal}. - -% For LDAP authentification use these lines instead of above one: -%{auth_method, ldap}. -%{ldap_servers, ["localhost"]}. % List of LDAP servers -%{ldap_uidattr, "uid"}. % LDAP attribute that holds user ID -%{ldap_base, "dc=example,dc=com"}. % Base of LDAP directory -%{ldap_rootdn, "dc=example,dc=com"}. % LDAP manager -%{ldap_password, "******"}. % Password to LDAP manager - -% For authentification via external script use the following: -%{auth_method, external}. -%{extauth_program, "/path/to/authentification/script"}. - -% For authentification via ODBC use the following: -%{auth_method, odbc}. -%{odbc_server, "DSN=ejabberd;UID=ejabberd;PWD=ejabberd"}. - - -% Host name(s): -{hosts, ["localhost"]}. - -% Default language: -{language, "en"}. - -% Listened ports: -{listen, - [ - {5222, ejabberd_c2s, [{access, c2s}, - {shaper, c2s_shaper}]}, - -% To create selfsigned certificate run the following command from the -% command prompt: -% -% openssl req -new -x509 -days 365 -nodes -out ejabberd.pem -keyout ejabberd.pem -% -% and answer the questions. -% {5222, ejabberd_c2s, [{access, c2s}, -% starttls, {certfile, "./ejabberd.pem"}, -% {shaper, c2s_shaper}]}, - -% When using SSL/TLS ssl option is not recommended (it requires patching -% erlang ssl application). Use tls option instead (as shown below). -% {5223, ejabberd_c2s, [{access, c2s}, -% tls, {certfile, "./ejabberd.pem"}, -% {shaper, c2s_shaper}]}, - - {5269, ejabberd_s2s_in, [{shaper, s2s_shaper}]}, - -% {5555, ejabberd_service, [{access, all}, -% {host, "icq.localhost", [{password, "secret"}]}]}, - - {5280, ejabberd_http, [http_poll, web_admin]} - ]}. - -% If SRV lookup fails, then port 5269 is used to communicate with remote server -{outgoing_s2s_port, 5269}. - - -% Used modules: -{modules, - [ - {mod_register, [{access, register}]}, - {mod_roster, []}, - {mod_shared_roster, []}, - {mod_privacy, []}, - {mod_configure, []}, - {mod_disco, []}, - {mod_stats, []}, - {mod_vcard, []}, - {mod_offline, []}, - {mod_announce, [{access, announce}]}, - {mod_private, []}, - {mod_irc, []}, -% Default options for mod_muc: -% host: "conference." ++ ?MYNAME -% access: all -% access_create: all -% access_admin: none (only room creator has owner privileges) - {mod_muc, [{access, muc}, - {access_create, muc}, - {access_admin, muc_admin}]}, - {mod_pubsub, []}, - {mod_time, []}, - {mod_last, []}, - {mod_version, []} - ]}. - - -% Local Variables: -% mode: erlang -% End: diff --git a/win32/ejabberd.ico b/win32/ejabberd.ico deleted file mode 100644 index 0a572d7f4..000000000 Binary files a/win32/ejabberd.ico and /dev/null differ diff --git a/win32/ejabberd.nsi b/win32/ejabberd.nsi deleted file mode 100644 index 26fdb0618..000000000 --- a/win32/ejabberd.nsi +++ /dev/null @@ -1,770 +0,0 @@ -; NSIS Modern User Interface -; Ejabberd installation script - -;-------------------------------- -;Include Modern UI - - !include "MUI.nsh" - !include "ejabberd.nsh" ; All release specific parameters come from this - -;-------------------------------- -;General - - ;Name and file - !define PRODUCT "Ejabberd" - Name ${PRODUCT} - OutFile "${OUTFILEDIR}\${PRODUCT}-${VERSION}.exe" - ShowInstDetails show - ShowUninstDetails show - - !define MUI_ICON "ejabberd.ico" - !define MUI_UNICON "ejabberd.ico" - !define MUI_HEADERIMAGE - !define MUI_HEADERIMAGE_BITMAP "ejabberd_header.bmp" - !define MUI_WELCOMEFINISHPAGE_BITMAP "ejabberd_intro.bmp" - - -;-------------------------------- -;Configuration - - SetCompressor lzma - -;-------------------------------- -;Reserve Files - - ReserveFile "ejabberd.ico" - ReserveFile "ejabberd.ico" - ReserveFile "ejabberd_header.bmp" - ReserveFile "ejabberd_intro.bmp" - !ifdef HACKED_INSTALLOPTIONS - ReserveFile "CheckUserH.ini" - ReserveFile "CheckReqs1H.ini" - !else - ReserveFile "CheckUser.ini" - ReserveFile "CheckReqs1.ini" - !endif - ReserveFile "CheckReqs.ini" - ReserveFile "CheckService.ini" - !insertmacro MUI_RESERVEFILE_INSTALLOPTIONS - -;-------------------------------- -;Variables - - Var MUI_TEMP - Var STARTMENU_FOLDER - Var ADMIN - Var ENABLE_SERVICE - Var ERLANG_PATH - Var ERLANG_VERSION - Var REQUIRED_ERLANG_VERSION - Var OPENSSL_PATH - Var OPENSSL_VERSION - Var REQUIRED_OPENSSL_VERSION - Var ERLSRV - -;---------------------------------------------------------- -;.onInit uses UserInfo plugin, so it's as high as possible - -Function .onInit - - StrCpy $REQUIRED_ERLANG_VERSION "5.4.9" - StrCpy $REQUIRED_OPENSSL_VERSION "0.9.7c" - - ;Default installation folder - StrCpy $INSTDIR "$PROGRAMFILES\${PRODUCT}" - - ;Get installation folder from registry if available - ClearErrors - ReadRegStr $0 HKLM "SOFTWARE\${PRODUCT}" "" - IfErrors 0 copydir - ReadRegStr $0 HKCU "SOFTWARE\${PRODUCT}" "" - IfErrors skipdir - copydir: - StrCpy $INSTDIR "$0" - - skipdir: - ;Extract InstallOptions INI files - !ifdef HACKED_INSTALLOPTIONS - !insertmacro MUI_INSTALLOPTIONS_EXTRACT "CheckUserH.ini" - !insertmacro MUI_INSTALLOPTIONS_EXTRACT "CheckReqs1H.ini" - !else - !insertmacro MUI_INSTALLOPTIONS_EXTRACT "CheckUser.ini" - !insertmacro MUI_INSTALLOPTIONS_EXTRACT "CheckReqs1.ini" - !endif - !insertmacro MUI_INSTALLOPTIONS_EXTRACT "CheckReqs.ini" - !insertmacro MUI_INSTALLOPTIONS_EXTRACT "CheckService.ini" - - ClearErrors - UserInfo::GetName - IfErrors admin - Pop $0 - UserInfo::GetAccountType - Pop $1 - StrCmp $1 "Admin" admin user - - admin: - StrCpy $ADMIN 1 - Goto skip - - user: - StrCpy $ADMIN 0 - - skip: - -FunctionEnd - -;-------------------------------- -;Interface Settings - - !define MUI_ABORTWARNING - -;-------------------------------- -;Installer/Uninstaller pages - - !insertmacro MUI_PAGE_WELCOME - !insertmacro MUI_PAGE_LICENSE "..\..\COPYING" - Page custom CheckReqs LeaveCheckReqs - Page custom CheckReqs1 LeaveCheckReqs1 - Page custom CheckUser LeaveCheckUser - Page custom CheckService LeaveCheckService - ;!insertmacro MUI_PAGE_COMPONENTS - !insertmacro MUI_PAGE_DIRECTORY - - !insertmacro MUI_PAGE_STARTMENU ${PRODUCT} $STARTMENU_FOLDER - - !insertmacro MUI_PAGE_INSTFILES - - !insertmacro MUI_UNPAGE_WELCOME - !insertmacro MUI_UNPAGE_CONFIRM - !insertmacro MUI_UNPAGE_INSTFILES - -;-------------------------------- -;Languages - - !insertmacro MUI_LANGUAGE "English" - -;-------------------------------- -;Language Strings - -;Description - - LangString DESC_SecEjabberd ${LANG_ENGLISH} "Erlang jabber server." - -;-------------------------------- -;Installer Sections - -Section "Ejabberd" SecEjabberd -SectionIn 1 RO - - SetOutPath "$INSTDIR" - File /r "${TESTDIR}\doc" - File /r "${TESTDIR}\ebin" - File /r "${TESTDIR}\msgs" - File /r "${TESTDIR}\win32" - File "${TESTDIR}\*.dll" - File "${TESTDIR}\inetrc" - File /oname=ejabberd.cfg.example "${TESTDIR}\ejabberd.cfg" - SetOverwrite off - File "${TESTDIR}\ejabberd.cfg" - SetOverwrite on - ;File /r "${TESTDIR}\src" - CreateDirectory "$INSTDIR\log" - -;The startmenu stuff - !insertmacro MUI_STARTMENU_WRITE_BEGIN ${PRODUCT} - - ;Create shortcuts - StrCpy $0 "$SMPROGRAMS\$STARTMENU_FOLDER" - CreateDirectory "$0" - CreateShortCut "$0\Start Ejabberd.lnk" "$ERLANG_PATH\bin\werl.exe" \ - '-sname ejabberd -pa ebin \ - -env EJABBERD_LOG_PATH log/ejabberd.log \ - -s ejabberd -kernel inetrc \"./inetrc\" -mnesia dir \"spool\" \ - -sasl sasl_error_logger {file,\"log/erlang.log\"}' \ - $INSTDIR\win32\ejabberd.ico - CreateShortCut "$0\Edit Config.lnk" "%SystemRoot%\system32\notepad.exe" \ - "$INSTDIR\ejabberd.cfg" - CreateShortCut "$0\Read Docs.lnk" "$INSTDIR\doc\guide.html" - CreateShortCut "$0\Uninstall.lnk" "$INSTDIR\Uninstall.exe" - - !insertmacro MUI_STARTMENU_WRITE_END - -;Create Windows service - StrCmp $ADMIN 1 0 skipservice - - StrCpy $ERLSRV "" - Push $ERLANG_PATH - Push erlsrv.exe - GetFunctionAddress $0 FFCallback - Push $0 - Call FindFiles - - StrCmp $ERLSRV "" skipservice - - nsExec::Exec '"$ERLSRV" list ejabberd' - Pop $0 - StrCmp $0 "error" skipservice - StrCmp $0 "0" 0 installsrv - - nsExec::ExecToLog '"$ERLSRV" remove ejabberd' - Pop $0 - - installsrv: - nsExec::ExecToLog '"$ERLSRV" add ejabberd -stopaction "init:stop()." \ - -onfail restart -workdir "$INSTDIR" \ - -args "-s ejabberd -pa ebin \ - -kernel inetrc \\\"./inetrc\\\" \ - -env EJABBERD_LOG_PATH log/ejabberd.log \ - -sasl sasl_error_logger {file,\\\"log/erlang.log\\\"} \ - -mnesia dir \\\"spool\\\"" -d' - Pop $0 - - StrCmp $ENABLE_SERVICE 0 0 skipservice - nsExec::ExecToLog '"$ERLSRV" disable ejabberd' - Pop $0 - - skipservice: - - ;Create uninstaller - WriteUninstaller "$INSTDIR\Uninstall.exe" - - StrCpy $1 "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT}" - StrCmp $ADMIN 1 admin2 - - WriteRegStr HKCU "Software\${PRODUCT}" "" "$INSTDIR" - WriteRegStr HKCU "$1" "DisplayName" "${PRODUCT} ${VERSION}" - WriteRegStr HKCU "$1" "UninstallString" "$INSTDIR\Uninstall.exe" - WriteRegDWORD HKCU "$1" "NoModify" 1 - WriteRegDWORD HKCU "$1" "NoRepair" 1 - Goto done2 - - admin2: - WriteRegStr HKLM "Software\${PRODUCT}" "" "$INSTDIR" - WriteRegStr HKLM "Software\${PRODUCT}" "Erlsrv" "$ERLSRV" - WriteRegStr HKLM "$1" "DisplayName" "${PRODUCT} ${VERSION}" - WriteRegStr HKLM "$1" "UninstallString" "$INSTDIR\Uninstall.exe" - WriteRegDWORD HKLM "$1" "NoModify" 1 - WriteRegDWORD HKLM "$1" "NoRepair" 1 - - done2: - -SectionEnd ; SecEjabberd - -Function FFCallback - - Exch $0 - StrCpy $ERLSRV $0 - Pop $0 - Push "stop" - -FunctionEnd - -;-------------------------------- -;Descriptions - - !insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN - !insertmacro MUI_DESCRIPTION_TEXT ${SecEjabberd} $(DESC_SecEjabberd) - !insertmacro MUI_FUNCTION_DESCRIPTION_END - -;-------------------------------- -;Uninstaller Section - -Section "Uninstall" - - ClearErrors - UserInfo::GetName - IfErrors admin - Pop $0 - UserInfo::GetAccountType - Pop $1 - StrCmp $1 "Admin" admin - - StrCpy $ADMIN 0 - Goto skipservice - - admin: - StrCpy $ADMIN 1 - ReadRegStr $ERLSRV HKLM "Software\${PRODUCT}" "Erlsrv" - - nsExec::Exec '"$ERLSRV" list ejabberd' - Pop $0 - StrCmp $0 "error" skipservice - StrCmp $0 "0" 0 skipservice - - nsExec::ExecToLog '"$ERLSRV" remove ejabberd' - Pop $0 - - skipservice: - RMDir /r "$INSTDIR\doc" - RMDir /r "$INSTDIR\ebin" - RMDir /r "$INSTDIR\msgs" - RMDir /r "$INSTDIR\win32" - ;RMDir /r "$INSTDIR\src" - RMDir /r "$INSTDIR\log" - Delete "$INSTDIR\*.dll" - Delete "$INSTDIR\inetrc" - Delete "$INSTDIR\ejabberd.cfg.example" - Delete "$INSTDIR\Uninstall.exe" - RMDir "$INSTDIR" - - !insertmacro MUI_STARTMENU_GETFOLDER ${PRODUCT} $MUI_TEMP - - Delete "$SMPROGRAMS\$MUI_TEMP\Start Ejabberd.lnk" - Delete "$SMPROGRAMS\$MUI_TEMP\Edit Config.lnk" - Delete "$SMPROGRAMS\$MUI_TEMP\Read Docs.lnk" - Delete "$SMPROGRAMS\$MUI_TEMP\Uninstall.lnk" - - ;Delete empty start menu parent diretories - StrCpy $MUI_TEMP "$SMPROGRAMS\$MUI_TEMP" - - startMenuDeleteLoop: - RMDir $MUI_TEMP - GetFullPathName $MUI_TEMP "$MUI_TEMP\.." - - IfErrors startMenuDeleteLoopDone - - StrCmp $MUI_TEMP $SMPROGRAMS startMenuDeleteLoopDone startMenuDeleteLoop - startMenuDeleteLoopDone: - - StrCpy $1 "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT}" - StrCmp $ADMIN 1 admin1 - DeleteRegKey HKCU "Software\${PRODUCT}" - DeleteRegKey HKCU $1 - Goto done - admin1: - DeleteRegKey HKLM "Software\${PRODUCT}" - DeleteRegKey HKLM $1 - - done: - -SectionEnd - -LangString TEXT_CU_TITLE ${LANG_ENGLISH} "Checking User Privileges" -LangString TEXT_CU_SUBTITLE ${LANG_ENGLISH} "Checking user privileged required to install Ejabberd." - -Function CheckUser - - StrCmp $ADMIN 1 0 showpage - Abort - - showpage: - !insertmacro MUI_HEADER_TEXT $(TEXT_CU_TITLE) $(TEXT_CU_SUBTITLE) - - !ifdef HACKED_INSTALLOPTIONS - !insertmacro MUI_INSTALLOPTIONS_INITDIALOG "CheckUserH.ini" - !insertmacro MUI_INSTALLOPTIONS_READ $0 "CheckUserH.ini" "Field 2" "State" - GetDlgItem $1 $HWNDPARENT 1 - EnableWindow $1 $0 - !else - !insertmacro MUI_INSTALLOPTIONS_INITDIALOG "CheckUser.ini" - !endif - - !insertmacro MUI_INSTALLOPTIONS_SHOW - -FunctionEnd - -Function LeaveCheckUser - - !ifdef HACKED_INSTALLOPTIONS - !insertmacro MUI_INSTALLOPTIONS_READ $0 "CheckUserH.ini" "Settings" "State" - StrCmp $0 0 validate ;Next button? - StrCmp $0 2 checkbox ;checkbox? - Abort ;Return to the page - - checkbox: - !insertmacro MUI_INSTALLOPTIONS_READ $0 "CheckUserH.ini" "Field 2" "State" - GetDlgItem $1 $HWNDPARENT 1 - EnableWindow $1 $0 - Abort - - validate: - !endif - -FunctionEnd - -LangString TEXT_CU_TITLE ${LANG_ENGLISH} "Configuring Ejabberd Service" -LangString TEXT_CU_SUBTITLE ${LANG_ENGLISH} "Configuring Ejabberd Service." - -Function CheckService - - StrCmp $ADMIN 0 0 showpage - Abort - - showpage: - !insertmacro MUI_HEADER_TEXT $(TEXT_CU_TITLE) $(TEXT_CU_SUBTITLE) - - !insertmacro MUI_INSTALLOPTIONS_INITDIALOG "CheckService.ini" - - !insertmacro MUI_INSTALLOPTIONS_SHOW - -FunctionEnd - -Function LeaveCheckService - - !insertmacro MUI_INSTALLOPTIONS_READ $0 "CheckService.ini" "Field 2" "State" - StrCmp $0 0 0 autostart - StrCpy $ENABLE_SERVICE 0 - Goto endfun - - autostart: - StrCpy $ENABLE_SERVICE 1 - - endfun: - -FunctionEnd - -LangString TEXT_CR_TITLE ${LANG_ENGLISH} "Unsatisfied Requirements" -LangString TEXT_CR_SUBTITLE ${LANG_ENGLISH} "Unsatisfied Ejabberd requirements found." - -Function CheckReqs - - Push "HKLM" - Call FindErlang - Pop $ERLANG_PATH - Pop $ERLANG_VERSION - StrCmp $ERLANG_PATH "" 0 abort - Push "HKCU" - Call FindErlang - Pop $ERLANG_PATH - Pop $ERLANG_VERSION - StrCmp $ERLANG_PATH "" 0 abort - - !insertmacro MUI_HEADER_TEXT $(TEXT_CR_TITLE) $(TEXT_CR_SUBTITLE) - - !insertmacro MUI_INSTALLOPTIONS_INITDIALOG "CheckReqs.ini" - GetDlgItem $R0 $HWNDPARENT 1 - EnableWindow $R0 0 - - - !insertmacro MUI_INSTALLOPTIONS_SHOW - - abort: - Abort - -FunctionEnd - -Function LeaveCheckReqs - - Abort - -FunctionEnd - -Function CheckReqs1 - - Push "HKLM" - Call FindOpenSSL - Pop $OPENSSL_PATH - Pop $OPENSSL_VERSION - StrCmp $OPENSSL_PATH "" 0 abort - Push "HKCU" - Call FindOpenSSL - Pop $OPENSSL_PATH - Pop $OPENSSL_VERSION - StrCmp $OPENSSL_PATH "" 0 abort - - !insertmacro MUI_HEADER_TEXT $(TEXT_CR_TITLE) $(TEXT_CR_SUBTITLE) - - !ifdef HACKED_INSTALLOPTIONS - !insertmacro MUI_INSTALLOPTIONS_INITDIALOG "CheckReqs1H.ini" - !insertmacro MUI_INSTALLOPTIONS_READ $0 "CheckReqs1H.ini" "Field 3" "State" - GetDlgItem $1 $HWNDPARENT 1 - EnableWindow $1 $0 - !else - !insertmacro MUI_INSTALLOPTIONS_INITDIALOG "CheckReqs1.ini" - !endif - - !insertmacro MUI_INSTALLOPTIONS_SHOW - - abort: - Abort - -FunctionEnd - -Function LeaveCheckReqs1 - - !ifdef HACKED_INSTALLOPTIONS - !insertmacro MUI_INSTALLOPTIONS_READ $0 "CheckReqs1H.ini" "Settings" "State" - StrCmp $0 0 validate ;Next button? - StrCmp $0 3 checkbox ;checkbox? - Abort ;Return to the page - - checkbox: - !insertmacro MUI_INSTALLOPTIONS_READ $0 "CheckReqs1H.ini" "Field 3" "State" - GetDlgItem $1 $HWNDPARENT 1 - EnableWindow $1 $0 - Abort - - validate: - !endif - -FunctionEnd - -Function FindErlang - - Exch $R0 - Push $R1 - Push $R2 - Push $R3 - Push $R4 - Push $R5 - - StrCpy $R1 0 - StrCpy $R2 "SOFTWARE\Ericsson\Erlang" - - loop: - StrCmp $R0 HKLM h1 - EnumRegKey $R3 HKCU $R2 $R1 - Goto l1 - h1: - EnumRegKey $R3 HKLM $R2 $R1 - l1: - IntOp $R1 $R1 + 1 - StrCmp $R3 "" endloop - ClearErrors - StrCmp $R0 HKLM h2 - ReadRegStr $R4 HKCU "$R2\$R3" "" - Goto l2 - h2: - ReadRegStr $R4 HKLM "$R2\$R3" "" - l2: - IfFileExists "$R4\bin\erl.exe" 0 loop - Push $REQUIRED_ERLANG_VERSION - Push $R3 - Call CompareVersions - Pop $R5 - StrCmp $R5 1 get - Goto loop - - endloop: - StrCpy $R4 "" - - get: - StrCpy $R0 $R4 - StrCpy $R1 $R3 - - Pop $R5 - Pop $R4 - Pop $R3 - Pop $R2 - Exch $R1 - Exch - Exch $R0 - -FunctionEnd - -Function FindOpenSSL - - Exch $R0 - Push $R1 - Push $R2 - Push $R3 - Push $R4 - Push $R5 - - StrCpy $R1 0 - StrCpy $R2 "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\OpenSSL_is1" - - StrCmp $R0 HKLM h1 - ReadRegStr $R3 HKCU "$R2" "DisplayName" - ReadRegStr $R4 HKCU "$R2" "Inno Setup: App Path" - Goto l1 - h1: - ReadRegStr $R3 HKLM "$R2" "DisplayName" - ReadRegStr $R4 HKLM "$R2" "Inno Setup: App Path" - l1: - - IfFileExists "$R4\bin\openssl.exe" 0 notfound - Goto get - ; TODO check version - ;Push $REQUIRED_OPENSSL_VERSION - ;Push $R3 - ;Call CompareVersions - ;Pop $R5 - ;StrCmp $R5 1 get - - notfound: - StrCpy $R4 "" - - get: - StrCpy $R0 $R4 - StrCpy $R1 $R3 - - Pop $R5 - Pop $R4 - Pop $R3 - Pop $R2 - Exch $R1 - Exch - Exch $R0 - -FunctionEnd - -;---------------------------------------------------------------------- -; CompareVersions -; input: -; top of stack = existing version -; top of stack-1 = needed version -; output: -; top of stack = 1 if current version => neded version, else 0 -; version is a string in format "xx.xx.xx.xx" (number of interger sections -; can be different in needed and existing versions) - -Function CompareVersions - ; stack: existing ver | needed ver - Exch $R0 - Exch - Exch $R1 - ; stack: $R1|$R0 - - Push $R1 - Push $R0 - ; stack: e|n|$R1|$R0 - - ClearErrors - loop: - IfErrors VersionNotFound - Strcmp $R0 "" VersionTestEnd - - Call ParseVersion - Pop $R0 - Exch - - Call ParseVersion - Pop $R1 - Exch - - IntCmp $R1 $R0 +1 VersionOk VersionNotFound - Pop $R0 - Push $R0 - - goto loop - - VersionTestEnd: - Pop $R0 - Pop $R1 - Push $R1 - Push $R0 - StrCmp $R0 $R1 VersionOk VersionNotFound - - VersionNotFound: - StrCpy $R0 "0" - Goto end - - VersionOk: - StrCpy $R0 "1" -end: - ; stack: e|n|$R1|$R0 - Exch $R0 - Pop $R0 - Exch $R0 - ; stack: res|$R1|$R0 - Exch - ; stack: $R1|res|$R0 - Pop $R1 - ; stack: res|$R0 - Exch - Pop $R0 - ; stack: res -FunctionEnd - -;----------------------------------------------------------------------- -; ParseVersion -; input: -; top of stack = version string ("xx.xx.xx.xx") -; output: -; top of stack = first number in version ("xx") -; top of stack-1 = rest of the version string ("xx.xx.xx") -Function ParseVersion - Exch $R1 ; version - Push $R2 - Push $R3 - - StrCpy $R2 1 - loop: - StrCpy $R3 $R1 1 $R2 - StrCmp $R3 "." loopend - StrLen $R3 $R1 - IntCmp $R3 $R2 loopend loopend - IntOp $R2 $R2 + 1 - Goto loop - loopend: - Push $R1 - StrCpy $R1 $R1 $R2 - Exch $R1 - - StrLen $R3 $R1 - IntOp $R3 $R3 - $R2 - IntOp $R2 $R2 + 1 - StrCpy $R1 $R1 $R3 $R2 - - Push $R1 - - Exch 2 - Pop $R3 - - Exch 2 - Pop $R2 - - Exch 2 - Pop $R1 -FunctionEnd - -Function FindFiles - - Exch $R5 # callback function - Exch - Exch $R4 # file name - Exch 2 - Exch $R0 # directory - Push $R1 - Push $R2 - Push $R3 - Push $R6 - - Push $R0 # first dir to search - - StrCpy $R3 1 - - nextDir: - Pop $R0 - IntOp $R3 $R3 - 1 - ClearErrors - FindFirst $R1 $R2 "$R0\*.*" - nextFile: - StrCmp $R2 "." gotoNextFile - StrCmp $R2 ".." gotoNextFile - - StrCmp $R2 $R4 0 isDir - Push "$R0\$R2" - Call $R5 - Pop $R6 - StrCmp $R6 "stop" 0 isDir - loop: - StrCmp $R3 0 done - Pop $R0 - IntOp $R3 $R3 - 1 - Goto loop - - isDir: - IfFileExists "$R0\$R2\*.*" 0 gotoNextFile - IntOp $R3 $R3 + 1 - Push "$R0\$R2" - - gotoNextFile: - FindNext $R1 $R2 - IfErrors 0 nextFile - - done: - FindClose $R1 - StrCmp $R3 0 0 nextDir - - Pop $R6 - Pop $R3 - Pop $R2 - Pop $R1 - Pop $R0 - Pop $R5 - Pop $R4 - -FunctionEnd - diff --git a/win32/ejabberd_header.bmp b/win32/ejabberd_header.bmp deleted file mode 100644 index 14d70ddee..000000000 Binary files a/win32/ejabberd_header.bmp and /dev/null differ diff --git a/win32/ejabberd_intro.bmp b/win32/ejabberd_intro.bmp deleted file mode 100644 index 7b37b0780..000000000 Binary files a/win32/ejabberd_intro.bmp and /dev/null differ diff --git a/win32/inetrc b/win32/inetrc deleted file mode 100644 index 49b18c2eb..000000000 --- a/win32/inetrc +++ /dev/null @@ -1 +0,0 @@ -{registry, win32}.
    ">>), fw(F, <<" \"Powered">>, + "style=\"border:0\" src=\"~ts/powered-by-ejabbe" + "rd.png\" alt=\"Powered by ejabberd - robust, scalable and extensible XMPP server\"/>">>, [Images_dir]), fw(F, <<" \"Powered">>, [Images_dir]), fw(F, <<"">>), fw(F, <<" ">>, [Images_dir]), fw(F, <<" \"Valid">>, [Images_dir]), fw(F, <<"